使用常对象——为共用数据加装一个名为const的玻璃罩

2024-03-03 11:48

本文主要是介绍使用常对象——为共用数据加装一个名为const的玻璃罩,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原创案例讲解——”玻璃罩const”系列的三篇文章:

1. 使用常对象——为共用数据加装一个名为const的玻璃罩

2. 常(const)+ 对象 + 指针:玻璃罩到底保护哪一个

3. 对象更有用的玻璃罩——常引用


  话题的引入:C++采取了不少有效的措施(如设private保护)以增加数据的安全性,但也可以在不同的场合通过不同的途径访问同一个数据对象。有时在无意之中的误操作会改变有关数据的状况,而这是人们不希望出现的。

  例如:

//程序1
#include <iostream>
using namespace std;
class  Test
{
private: int x, y;
public:Test(int a, int b){x=a;y=b;}void printxy();
} ;
void Test::printxy()  //用于显示x*y的值
{x=2;  //这是一个bugcout<<"x*y="<<x*y<<endl;
}
void main(void)
{    Test t(3,5);t.printxy( );system("pause");
}

  这段程序的运行结果为x*y=10。显然不是我们期望的。原因在于,本来printxy()只是用来输出结果,需求当中只是“读取”类中数据成员即可;而现在,其中包含改变数据成员值的语句x=2,现有的程序没有表达出printxy()不能修改x值这一约事,从而程序失去了一种“免疫力”——自动拒绝不应该有的对类中数据成员值的修改。按照相关的机制,尽管x是私有的,但在整个类的内部,默认是任何成员函数都可以访问的。

  在这一段短程序中,可能此事一眼可以看出。但需要考虑在一个大工程中,涉及上百个类,几万甚至更多行的代码,由数(十)名程序员合作完成的一个工程中,这个小小的x=2;是会犯下滔天罪行的。强调程序员要仔细、认真是必要的,但质量的保证不应该仅凭程序员在体力、脑力上的投入,应该有种机制预以保证,或者有一定的防范措施,才是最根本的。这同时也可以降低处理这类事情的成本。好比在社会领域,强调人人做个好人是必要的,但铲除各种丑恶现象,制度建设和执行更重要。

  C++中给出应对的办法就是,使用const修饰符,声明程序中的某些部件具有“常”的性质:即不允许改变、不可改变、不应该改变等。我愿将之形象地比喻为一个玻璃罩——其中的成员只可读取,不可修改(只可以看,但不能摸,不能动)。

  本文从常对象、常数据成员、常成员函数三个角度给出这个问题的解答。系列文章中的其他两篇涉及常指针和常引用。


  一、常对象——const对象

  凡希望保证数据成员不被改变的对象,可以声明为常对象,常对象中的所有成员的值都不能被修改。

  常对象两种等价的定义形式:

     类名 const 对象名[(实参表列)];

     const 类名 对象名[(实参表列)];

  在定义常对象时,必须要在初始化时给出初值。此外,对于常对象中数据成员的任何修改都是非法的,编译器将给出编译错误提示。例如:

//程序2
#include <iostream>
using namespace std;
class  Test
{
private: int x, y;
public:Test(int a, int b){x=a;y=b;}void printxy();
} ;
void Test::printxy()  //用于显示x*y的值
{x=2;  //这是一个bugcout<<"x*y="<<x*y<<endl;
}
void main(void)
{    const Test t(3,5);//将对象t定义为常对象t.printxy( );system("pause");
}

  在编译时,第20行会提示错误:error C2662: “Test::printxy”: 不能将“this”指针从“const Test”转换为“Test &”。原因在于printxy()成员函数中要对数据成员进行修改,而当前对象却是一个常对象。

  在实际应用中,其值只能在初始化时置,且不能再修改的情况很多。比如有表示时间的类Date,定义一个日期:Date myBirthday(yyyy,mm,dd)表示生日,这个值是在程序中要保证不被修改的。回顾更简单的情况,变量前加const修饰符定义的是常变量,如const double pi=3.14;,一样的道理。

  关于这个错误提示作些解释(看这些解释头疼的读者暂时可以略过下一段,但过后要看。目前不能理解的,需要在this指针和const的其他用法上有所积累。另外,下一段也可以作为this指针和const其他用法的案例使用。)

  我们知道,每一个成员函数都包含一个特殊的指针——this,其值为当前正在调用成员函数的对象的起始地址,即指向当前对象。printxy()被声明为无参函数,而实际是隐含一个this指针参数的,即

void Test::printxy(Test *this)
{this->x = 2;  cout<<"x*y="<<(this->x)*(this->y)<<endl;
}

  这样,在第20行调用t.printxy();时,相当于t.printxy(&t);,即将对象 t 的地址传递给形参this。从实参 &t 应当和形参对应的角度,t 是常对象,printxy()函数的原型应该是:void printxy(const Test *this),而非void printxy(Test *this)。至于提示中“const Test”转换为“Test &”中的&,等理解了常引用后再作讨论。

  面对这个错误提示,我们排除bug,将原程序第14行x=2;删除,这是符合需求的。但是,问题并没有得到解决。编译仍然出错,需要考虑常成员函数了。


  二、常成员函数——const成员函数

  要引用常对象中的数据成员,需将该成员函数声明为const型函数,即常成员函数。常成员函数的原型为:

     返回值类型 成员函数名[(形参表列)] const;

  对于去除了上面bug的程序:

//程序3
#include <iostream>
using namespace std;
class  Test
{
private: int x, y;
public:Test(int a, int b){x=a;y=b;}void printxy();
} ;
void Test::printxy() //功能:输出x*y的值
{cout<<"x*y="<<x*y<<endl;
}
void main(void)
{    const Test t(3,5);//将对象t定义为常对象t.printxy( );system("pause");
}

  其中存在的问题就是,printxy()是“非const成员函数”,而被const对象调用,违反了“常对象只能调用常成员函数”的规则。

  程序应该改为:

//程序4
#include <iostream>
using namespace std;
class  Test
{
private: int x, y;
public:Test(int a, int b){x=a;y=b;}void printxy() const;
} ;
void Test::printxy() const //用于显示x*y的值
{cout<<"x*y="<<x*y<<endl;
}
void main(void)
{    const Test t(3,5);//将对象t定义为常对象t.printxy( );system("pause");
}

  注意,const是函数类型的一部分,在声明函数和定义函数时都要有const关键字,在调用时不必加const。常对象中的成员函数未加const,编译系统认为其是非const成员函数。常成员函数可以访问常对象中的数据成员,但不允许修改常对象中数据成员的值。

  那么,在将一个对象定义为常对象时,是否意味着其所属类中的所有成员函数都应该为const成员函数呢?不是这样的。例如下面的程序,常对象t1只能调用常成员函数;对象t2不是常对象,则可以调用非const函数setX();在其中还可以修改数据成员 x 的值。

//程序5
#include <iostream>
using namespace std;
class  Test
{
private: int x; int y;
public:Test(int a, int b){x=a;y=b;}void printxy() const;void setX(int n) {x=n;}void setY(int n) {y=n;}
} ;
void Test::printxy() const //用于显示x*y的值
{cout<<"x*y="<<x*y<<endl;
}
void main(void)
{    const Test t1(3,5);//将对象t1定义为常对象t1.printxy( ); //t1.setX(2);将招致错误Test t2(4,7);//将对象t定义为非const对象t2.setX(2);//合法的调用t2.printxy();system("pause");
}

  这个程序给我们留下的最实用的启示是,如果一个函数,如printxy(),无论何时都不会改变数据成员的值,不管将来是用于const对象,还是非const对象,都将其定义为const成员函数。


  三、常数据成员——const数据成员

  使用常对象是一个非常强的要求,其中的所有数据成员将不能改变。而实际中的需求是这样的:一个对象中,个别数据成员的值经初始化后不允许改变,其他的可以改变。这时,不要将对象设为常对象,而是使用常数据成员——const数据成员。

  声明常数据成员也用关键字const,其形式为:

    const 数据类型 数据成员名;

  看下面的例子:

//程序6
#include <iostream>
using namespace std;
class  Test
{
private: const int x; //const数据成员int y;       //非const数据成员
public:Test(int a, int b){x=a;y=b;}void printxy() const;void setX(int n) {x=n;}void setY(int n) {y=n;}
} ;
void Test::printxy() const //函数中一定不修改数据成员值,定义为const成员函数
{cout<<"x*y="<<x*y<<endl;
}
void main(void)
{    Test t(3,5);t.setX(2);t.setY(4);t.printxy( ); system("pause");
}
  程序编译时,出现3个错误。

  先说第12行 void setX(int n) {x=n;} 的错误:error C2166: 左值指定 const 对象。显然,赋值表达式 x=n 中的左值 x 已经被声明为const数据成员了,只允许初始化,不允许赋值。这个函数是不允许存在在。

  第10行是构造函数Test(int a, int b){x=a;y=b;},按道理const数据成员可以被初始化,但现在的问题是对const数据成员 x 的初始化的方式不对,与前同样的理由不能用赋值表达式完成(C++中初始化和赋值是有区别的,请自行搜索,这方面的资料很多。)。另外的一个错误提示“error C2758: “Test::x”: 必须在构造函数基/成员初始值设定项列表中初始化”告诉我们如何解决这个问题。构造函数的写法上存在问题。Test(int a, int b):x(a){y=b;}能够保证没有编译错误,而Test(int a, int b):x(a),y(b){}是最佳写法。《Effective C++》书中告诫我们: 尽量使用初始化而不要在构造函数里赋值(条款12)。

  所以,程序改为:

//程序7
#include <iostream>
using namespace std;
class  Test
{
private: const int x; //const数据成员int y;       //非const数据成员
public:Test(int a, int b):x(a),y(b){}//Test(int a, int b):x(a){y=b;}也是一个不会发生错误的写法void printxy() const;//void setX(int n) {x=n;}  //修改const数据成员是不允许的void setY(int n) {y=n;}
} ;
void Test::printxy() const //函数中一定不修改数据成员值,定义为const成员函数
{cout<<"x*y="<<x*y<<endl;
}
void main(void)
{    Test t(3,5);//t.setX(2);t.setY(4);t.printxy( ); system("pause");
}
  上面的程序中还有一个有趣的话题,printxy()是个const成员函数,它能够访问const数据成员,也能访问非const数据成员。将printxy()声明为非const成员函数,取消了不得修改数据成员的限制,是个更宽松的要求,同样对const数据成员和非const数据成员都能访问,不过若要修改,只能修改y,而不能修改x。
//程序8
#include <iostream>
using namespace std;
class  Test
{
private: const int x; //const数据成员int y;       //非const数据成员
public:Test(int a, int b):x(a),y(b){}void printxy();void setY(int n) {y=n;}
} ;
void Test::printxy()  //改为非const成员函数,仅为演示语法,实际已经背离了需求
{x=5; //这一句的存在引发错误:error C2166: 左值指定 const 对象y=4;cout<<"x*y="<<x*y<<endl;
}
void main(void)
{    Test t(3,5);t.setY(4);t.printxy( ); system("pause");
}

  如果所有的数据成员都允许修改,我们将不涉及有些恼人的“常”问题。对于已经讨论的常数据成员的做法,适合于大量数据成员允许修改,而少量不允许修改,这时将少量的定义为const数据成员。

  另一方面,如果所有的数据成员都不允许修改,我们在定义对象时将对象定义为常对象处理(本文第一部分)。那么大量数据成员不允许修改,只有少量允许修改时该怎么办呢?总不至于100个不允许修改的数据成员加const,只为满足那1个允许修改成员吧?

  在ANSI C++给出了特例:编程时一定要修改常对象中的某个数据成员,该数据成员声明为mutable,如正面的程序: 

//程序9
#include <iostream>
using namespace std;
class  Test
{
private: mutable int x; //即使在const对象中,x也是可以修改的int y;
public:Test(int a, int b){x=a;y=b;}void printxy() const;
} ;
void Test::printxy() const //用于显示x*y的值
{x=2; //此处确实可以在const成员函数中修改const对象中声明为mutable的数据成员,cout<<"x*y="<<x*y<<endl;
}
void main(void)
{    const Test t(3,5);//将对象t定义为常对象t.printxy( );system("pause");
}

  四、总结

  不加限制的对象享受着极端的自由,也为bug的滋生提供了土壤。将对象定义为常对象则走入了另一个极端,谁也不许动。于是处在这两个方案中间的做法给我们提供了便利,将不可修改的数据成员声明为const数据成员,从而保证其除了初始化不能被修改;用const成员函数限制函数中不得对数据成员进行修改。再重复一遍:将不可修改的数据成员声明为const数据成员,从而保证其除了初始化不能被修改;用const成员函数限制函数中不得对数据成员进行修改。

  不过,这样做也带来了多种组合困扰刚入道的菜鸟,即使老鸟也不能“背会”这些规则。这些内容不是靠背书得到的,一些良好的设计原则会给出指导,彻底理解其实也不难。熟悉了,不过尔尔。

数据成员非const成员函数const成员函数
非const数据成员可引用,可改变值可引用,不可改变值
const数据成员可引用,不可改变值可引用,不可改变值
const对象的数据成员不可引用,不可改变可引用,不可改变值

  念熟这张表格还是很有成就感的一件事情。只不过念起来太拗口,我常把“非const”念作“可变的”,倒也减轻了大脑的负担,且不至于引起太大的歧义。另外,我还把“非const成员函数”看成肉食动物,本事大呗,不光看,还要修改;而把“const成员函数”看成食草动物,只图自保,但也要生存,温顺而没有野心。相应的,"非const数据成员”不好消化,"const数据成员"好消化。这样想想,可以增加思维的形象性。

  下面,通过表格中给出一些代码,我将给出更直观的对这一干关系的解释,希望帮助读者更容易地掌握相关内容。

数据成员非const成员函数const成员函数
class{
private: 
 int x; //非const数据成员
 int y;//非const数据成员
……}
void printxy()
{cout<<"x*y="<<x*y<<endl;}//可引用;
void setX(int n) {x=n;}//可改变值
void setY(int n) {y=n;}//可改变值
void printxy()const
 { cout<<"x*y="<<x*y<<endl;//可引用
    x=2;//出错:不可改变值
}
class{
private: 
 const int x; //const数据成员
 int y;//非const数据成员
……}
void printxy()
{cout<<"x*y="<<x*y<<endl;}//可引用;
void setX(int n) {x=n;}//错误:不可改变值x
void setY(int n) {y=n;}//可改变值y
void printxy() const
{ cout<<"x*y="<<x*y<<endl;//可引用
  x=2;//错误:不可改变值x
  y=4;//错误:不可改变值y
}
class Test{
private: 
 int x; 
 int y;
……}
const Test t(3,5);//t.x,t.y为const对象的数据成员
void Test::printxy()
{cout<<"x*y="<<x*y<<endl;}
void Test::setX(int n) {x=n;}
……
t.printxy();//不可引用
t.setX(1);//不可改变
void Test::printxy() const
{cout<<"x*y="<<x*y<<endl;}
void Test::setX(int n) const
  {x=n;} //此处的改变就不合法
……
t.printxy();//可引用
t.setX(1);//不可改变



相关阅读:C++中const用法总结

本博相关:

(全文完)

这篇关于使用常对象——为共用数据加装一个名为const的玻璃罩的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/769524

相关文章

大模型研发全揭秘:客服工单数据标注的完整攻略

在人工智能(AI)领域,数据标注是模型训练过程中至关重要的一步。无论你是新手还是有经验的从业者,掌握数据标注的技术细节和常见问题的解决方案都能为你的AI项目增添不少价值。在电信运营商的客服系统中,工单数据是客户问题和解决方案的重要记录。通过对这些工单数据进行有效标注,不仅能够帮助提升客服自动化系统的智能化水平,还能优化客户服务流程,提高客户满意度。本文将详细介绍如何在电信运营商客服工单的背景下进行

基于MySQL Binlog的Elasticsearch数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张M

关于数据埋点,你需要了解这些基本知识

产品汪每天都在和数据打交道,你知道数据来自哪里吗? 移动app端内的用户行为数据大多来自埋点,了解一些埋点知识,能和数据分析师、技术侃大山,参与到前期的数据采集,更重要是让最终的埋点数据能为我所用,否则可怜巴巴等上几个月是常有的事。   埋点类型 根据埋点方式,可以区分为: 手动埋点半自动埋点全自动埋点 秉承“任何事物都有两面性”的道理:自动程度高的,能解决通用统计,便于统一化管理,但个性化定

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

使用SecondaryNameNode恢复NameNode的数据

1)需求: NameNode进程挂了并且存储的数据也丢失了,如何恢复NameNode 此种方式恢复的数据可能存在小部分数据的丢失。 2)故障模拟 (1)kill -9 NameNode进程 [lytfly@hadoop102 current]$ kill -9 19886 (2)删除NameNode存储的数据(/opt/module/hadoop-3.1.4/data/tmp/dfs/na

异构存储(冷热数据分离)

异构存储主要解决不同的数据,存储在不同类型的硬盘中,达到最佳性能的问题。 异构存储Shell操作 (1)查看当前有哪些存储策略可以用 [lytfly@hadoop102 hadoop-3.1.4]$ hdfs storagepolicies -listPolicies (2)为指定路径(数据存储目录)设置指定的存储策略 hdfs storagepolicies -setStoragePo

Hadoop集群数据均衡之磁盘间数据均衡

生产环境,由于硬盘空间不足,往往需要增加一块硬盘。刚加载的硬盘没有数据时,可以执行磁盘数据均衡命令。(Hadoop3.x新特性) plan后面带的节点的名字必须是已经存在的,并且是需要均衡的节点。 如果节点不存在,会报如下错误: 如果节点只有一个硬盘的话,不会创建均衡计划: (1)生成均衡计划 hdfs diskbalancer -plan hadoop102 (2)执行均衡计划 hd

Hadoop数据压缩使用介绍

一、压缩原则 (1)运算密集型的Job,少用压缩 (2)IO密集型的Job,多用压缩 二、压缩算法比较 三、压缩位置选择 四、压缩参数配置 1)为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器 2)要在Hadoop中启用压缩,可以配置如下参数

Makefile简明使用教程

文章目录 规则makefile文件的基本语法:加在命令前的特殊符号:.PHONY伪目标: Makefilev1 直观写法v2 加上中间过程v3 伪目标v4 变量 make 选项-f-n-C Make 是一种流行的构建工具,常用于将源代码转换成可执行文件或者其他形式的输出文件(如库文件、文档等)。Make 可以自动化地执行编译、链接等一系列操作。 规则 makefile文件

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化,使其看起来更清晰,同时保持尺寸不变,通常涉及到图像处理技术如锐化、降噪、对比度增强等 影响照片清晰度的因素 影响照片清晰度的因素有很多,主要可以从以下几个方面来分析 1. 拍摄设备 相机传感器:相机传