写在前面

今天主要分享的内容是const,我认为const是对程序设计者的约束。程序员在设计一个类的时候,觉得某些东西是不能被用户(或者是自己)修改的,就会给这些东西前面加上const,但是这个const又会给程序员带来不小的麻烦,因此对于const的使用应该是慎之又慎的。那么对于实在是没办法的情况,真的必须得修改const修饰的东西的时候,其实我们也有解决办法,那就是const_cast

const

首先我们先来思考一个问题,如果让函数返回一个const修饰的对象,那么如果在main里面用这个const修饰的对象来构建新的对象,那么main里面的对象可以修改吗?让我们来试一下。

首先构建我们的测试用例:

这个测试用例比较长,我在解释的时候进行分段解释吧。

首先我们来看类,写一个类,就先写下三个类成员函数,构造函数,拷贝构造函数和virtual的析构函数,具体的原因在该系列的别的文章中已经有提及。接下来看seeI()函数,这个函数就是用来打印对象的成员变量i的数值大小。

接下来来看我们所要测试的函数,就是这个func1()函数,它返回了一个const的A类对象,在函数里面它打印了要返回的对象的地址。

接下来看main函数里面干了什么。

main函数里面每个输出语句就是我想验证的东西。我们先来看第一个,在main函数开始的时候,我用func1()返回的对象来构造了一个对象,聪明的同学应该发现了这里调用的是A类的拷贝构造函数。那么第一个输出语句,我想看看我构造出来的对象内存和函数返回的对象内存有什么关系,结果如下所示:

说来也奇怪,这俩对象的地址居然是一样的。这就很值得玩味了,我猜编译器是这样处理的:既然返回的是函数栈内的对象,反正函数栈是要被回收的,那倒不如直接把那块内存经由A类的拷贝构造进行memberwise的拷贝,直接用函数栈里的内存生成main里面那个a对象,所以这俩的地址是一样的。

接下来我们来看第二个输出语句,那就是我想试试看main里面的对象a的内存是否可变的,结果如下所示:

答案很显然,对象的内存是可变的,因为虽然用的是同一块内存,但是确实一个新的对象,这个新对象并没有被const修饰,所以他的内存当然可以修改。

main里面再之后的输出的作用是,我想再看看,能否用指针进行修改a的内存,结果告诉我当然是可以的。

那说到这里可能就要问了,那函数返回一个const的对象到底还有什么意义呢?

const一个对象意思就是说它不能被修改,也就是说不能做左值。

const_cast

好,接下来我们来看一下,如果一个const的对象在一些万不得已的情况下要修改数值的时候我们应该怎么办呢?那就要轮到const_cast出场了,看下面这个测试用例:

我const了一个int类型的对象i,然后尝试修改它的值,让我们来看看编译器说了啥:

意料之中,情理之中。那么我们该如何修改对象i的数值呢?看下面这个例程:

这段部分我就是想试试,const_cast是否真的能做到修改const的变量。结果如下所示:

编译是通过了,而且可以运行。但是通过const_cast转换的指针修改内存的数值之后,原来的内存数值却没有改变。本来这是无可厚非的,但是转换后的指针和原来数据内存的地址居然是一样的(红框内的数据)。

这说明了啥呢?说明我通过2个相同地址的指针索引内存,1个指针修改了内存,通过另外1个指针去看内存数据的时候,内存居然没有发生改变,这就很奇怪了。

其实这就是c++的设计哲学了,那就是说一个初始化的const的变量,在之后的任何时间都不能被改变!那为什么还要有const_cast转换的变量呢?想象某个特殊的场景,某个函数要一个非const的变量(函数内保证不会修改变量),但是传进去的变量是一个const的变量,这个时候编译是通不过的,因此这个时候我们就需要标准转换符const_cast了。其余时候,尽量别用这个符号。

回到之前那个问题,为什么通过同一个地址索引内存会有不同的结果呢?我想是被编译优化了的原因,因为const修饰的变量是会存放到程序的常量区,不可修改的。