const修饰变量
int main(int argc, char* argv[])
{
const int b = 3;
int* c = (int*)&b;
*c = 5;//这里即便把b所在地址的值修改了,但后面调用b的时候,b仍然是3
cout << "*c的值为:" << *c << endl;//5
cout << "b的值为:" << b << endl;//3
return 10086;
}
对于一个const常量,通过指针修改其值后,可以看到指向b的指针输出为5,而b依然是3,没有改变,为什么呢?
调试一下,发现并没有问题,指针指向的内容确实改变了
其实这种情况,编译器会认为b是一个在编译期就可计算出结果的常量,就像宏定义一样,用到b的地方会被编译器替换成3。即在没有通过指针修改b的值的时候,所有b的值都已经默认为3了。
假如通过一个函数的返回值给b赋值,即运行时初始化,这样b的值在编译器就无法确定了
int set_value(int i)
{
return i;
}
int main(int argc, char* argv[])
{
const int b = set_value(3);
int* c = (int*)&b;
*c = 5;//这里即便把b所在地址的值修改了,但后面调用b的时候,b仍然是3
cout << "*c的值为:" << *c << endl;//5
cout << "b的值为:" << b << endl;//5
return 10086;
}
此时,b的值也就变化了。
又知道,constexpr可以赋予常量表达式获得在程序编译阶段计算出结果的能力,上面的例子中,set_value()函数无法在编译时确定b的值,那假如我们将函数用constexpr修饰的话,是不是函数又可以在编译时期得到b的值了,进而,后面所有的b的值就又可以提前确定了,再通过指针修改b,b也不会变化了。验证一下:
确实如此。
还有一个情况,我们知道,全局常量的存储位置是数据段的全局/静态区,这个内存区是只读的,而局部常量存放于堆栈中,虽然不能直接修改,但可以通过指针间接修改。
上面的例子就是因为我们把声明为了局部变量,所以b可以通过指针被间接修改,假如把b声明为一个全局变量,那么就无法再通过指针修改其值了。
constexpr int set_value(int i)
{
return i;
}
const int b = set_value(3);
//只要声明为全局变量,不管能不能在编译期获得值,都不能通过指针修改其内容
int main(int argc, char* argv[])
{
int* c = (int*)&b;
*c = 5;//这里即便把b所在地址的值修改了,但后面调用b的时候,b仍然是3
cout << "*c的值为:" << *c << endl;//5
cout << "b的值为:" << b << endl;//3
return 10086;
}
补充一点:关于const和constexpr的区别
说区别,我其实都不认为二者有什么相似性,充其量有一些联系。
const是变量类型名的一部分,即 part of type name,一个名字叫“const T”或者“T const”的类型,和T这个类型本身处于一种平级的关系,和T不同的就在于这个类型的对象自产生后就不能再更改了。
constexpr是声明的一部分,即 part of a declaration,他不是变量类型的一部分。当他出现在一个变量的声明中时,他要求编译器在编译期间就初始化并确定下来这个变量(否则编译报错);当他出现在一个函数的声明中时,他要求至少有一条return路径可以(但不是必须)在编译中确定下来,即返回的是编译期常量。
二者的联系就在于,在使用constexpr声明一个类型为T的变量时,constexpr会自动把这个变量定义为const T类型。即constexpr在完成它本职工作(告诉编译器这是个编译期常量)的同时,还把原来的T类型改为了const T类型。这就是二者的联系。
链接:https://www.zhihu.com/question/35614219/answer/1477686611
const修饰指针
const int *ptr
int const *ptr //和上面等价
int * const ptr
* 在 const前面,则const直接修饰的指针,指针位置不能瞎动;
* 在 const后面,则const修饰的是 *ptr,则表征指针指向的这个值不能瞎动;
如形参为const A* a,则不能对传递进来的指针的内容进行改变,保护了原指针所指向的内容。
如形参为const A& a,则不能对传递进来的引用对象进行改变,保护了原对象的属性。
如果一个成员函数明确知道其不会修改数据成员,那么最好将其声明为const,因为const成员函数中不允许对数据成员进行修改,如果修改,编译器将报错,这大大提高了程序的健壮性。
如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加const修饰。
例如不要将函数void Func1(int x) 写成void Func1(const int x)。同理不要将函数void Func2(A a) 写成void Func2(const A a)。其中A为用户自定义的数据类型。
对于非内部数据类型的参数而言,像void Func(A a) 这样声明的函数注定效率比较底。因为函数体内将产生A类型的临时对象用于复制参数a,而临时对象的构造、复制、析构过程都将消耗时间。
为了提高效率,可以将函数声明改为void Func(A &a),因为“引用传递”仅借用一下参数的别名而已,不需要产生临时对象。但是函数void Func(A &a) 存在一个缺点:“引用传递”有可能改变参数a,这是我们不期望的。解决这个问题很容易,加const修饰即可,因此函数最终成为void Func(const A &a)。
以此类推,是否应将void Func(int x) 改写为void Func(const int &x),以便提高效率?完全没有必要,因为内部数据类型的参数不存在构造、析构的过程,而复制也非常快,“值传递”和“引用传递”的效率几乎相当。
const修饰函数
const可以修饰类成员函数,防止类成员函数中除了static成员之外的其他成员被修改。
在类中将成员函数修饰为const表明在该函数体内,不能修改对象的数据成员而且不能调用非const函数。为什么不能调用非const函数?因为非const函数可能修改数据成员,const成员函数是不能修改数据成员的,所以在const成员函数内只能调用const函数。即便某个非const成员函数没有改变成员变量,也不能被const成员函数调用。
下图是另一种情况,当类对象指针作为const形参时,只能通过指针调用const成员函数
class people
{
int private_val_1;
public:
String name;
int age;
int high;
bool sex;
public:
people(){}
people(String name, int age, int high, bool sex)
{
this->name = name;
this->age = age;
this->high = high;
this->sex = sex;
}
void Rename(String name)
{
this->name = name;
}
void Printname() const
{
//const表示此成员函数不允许修改成员变量
//也不允许调用其他有修改成员变量权限的成员函数(即非const成员函数)
cout << name << endl;
cout << age << endl;
cout << high << endl;
cout << sex << endl;
}
};
void fun(people* in)
{
in->age++;
//可以调用和修改所有公有变量,因为形参没有const限制
//可以调用所有成员函数,因为形参没有const限制
}
void fun2(const people* in)
{
in->age;//可以调用所有公有变量
in->age++;//错!不可以修改变量
in->Printname();//虽然有const限制,但仍然可以调用没有修改成员变量权限的成员函数
in.Rename();//错!此成员函数没有const限制,有修改成员变量的权限
people* temp = new people();
in = temp;//可以修改
}
void fun3(people* const in)
{
in->age++;//大部分都和fun2相同
//区别如下:不能修改in的指向了
people* temp = new people();
in = temp;//这样就错了
}