从基本用法说起
1. 修饰变量
const是constant的一个表示,字面上来看,以const修饰的是一个常量。实际上,这是一个误解,或者说是历史遗留的问题,用readonly来表示const更为妥当。
最简单的例子,我们都会使用:
const int x = 5;
x = 10;//error
下面我们看一个例子,可能和你印象中的const就不一样了~
int main(int argc, char* argv[]) {
const int x = 10;
int* px = (int*)&x;
printf("x at address : %p\n", &x);
printf("px at address : %p\n", px);
*px = 20;
printf("x is : %d\n", x);
printf("px is : %d\n", *px);
}
在上述程序中,我们试图使用px间接改变x的值。
这个的结果是什么呢?
说出来你可能不信,是这样的:
显然,x的地址和px指向的地址是一致的,相信大家不会有疑问。关键是我们通过px间接改变了x的值,尽管px指向的地址的内容已经变为了20,但是x的值还是10。
有人会说,这证明了const就是标志一个常量!通过指针也无法修改!
实际上,003AFE2C这块内存的值已经被修改为20了。
但是为什么x的值输出还是10呢?
这是因为,x的值在编译期就已经知道是10了,那么在读取x的值的时候,就会被编译器直接替换为10。
但是,我们要明确一点,就是const修饰的值未必在编译期就可以确定。
我们再看一个例子,来说明这一点:
int main(int argc, char* argv[]) {
int scale = 5;
const int y = 5 * scale;//y的值在运行时才可以确定
int* py = (int*)&y;
printf("y at address : %p\n", &y);
printf("py at address : %p\n", py);
*py = 20;
printf("y is : %d\n", y);
printf("py is : %d\n", *py);
getchar();
return 0;
}
上述程序的运行结果是这样的:
可以看出,由于y的值在编译期没有确定,在运行时,读取y的值要到0026F9C0中去找寻数据,此时通过py已经把y的值修改,所以此时输出y就变成了20。
我们还可以通过下面这个例子进一步说明const修饰的值未必在编译期就可以确定。
int main(int argc, char* argv[]) {
const int x = 10;
int scale = 5;
const int y = 5 * scale;
int arrx[x];//fine,编译期已经确定了x是10
int arry[y];//error,表达式必须含有常量值, 因为y的值在编译期没有确定
getchar();
return 0;
}
2. 修饰类的数据成员
可以用const修饰类内成员,需要注意的是一定要给定一个初始值:
class Test {
public:
const int x;
Test() {}//error,提示没有对x进行初始化,仅声明了x而已
};
//正确写法
class Test {
public:
const int x;
Test() : x(0){//只能用两列表初始化话的方式
}
};
//或者
class Test {
public:
const int x = 10;
};
3. 修饰类的成员函数
const修饰类的成员函数,则该成员函数不能修改类中任何非const成员函数。一般写在函数的最后来修饰。
class A
{
void function()const; //常成员函数, 它不改变对象的成员变量.
}
对于const类对象/指针/引用,只能调用类的const成员函数,因此,const修饰成员函数的最重要作用就是限制对于const对象的使用。
a. const成员函数不被允许修改它所在对象的任何一个数据成员。
b. const成员函数能够访问对象的const成员,而其他成员函数不可以。
指针和const
上述的都是一些基本的用法,下面谈一谈const和指针。
我们最常见的const和指针的组合就是 :
int x = 10;
const int* px = &x;
这表示指针指向的那块内存是只读的,但是px还可以指向其他的地址:
int x = 10;
const int* px = &x;
int y = 20;
px = &y;
还有一种不是特别常见的组合方式:
int x = 10;
int* const px = &x;
这表示指针本身是一个const,它必须初始化指向一个地址。然后不能改变,但是那块地址的内容是可以改变的。
例如:
int x = 10;
int* const px = &x;
*px = 20;
printf("x is : %d\n", x);//x = 20
int y = 20;
px = &y;//error
为了对指针的const做出区分,C++给予了不同的命名:
顶层const(top level const) : 意指指针本身是一个const。顶层const同样适用于其他的数据类型
const int x = 10, const double = 3.14, const ClassType obj("xxx")
等都属于顶层const,只不过只有牵扯到指针的const的时候我们才使用这样的术语。底层const(low level const) : 意指指针指向的对象是const属性的。
当然,我们可以把两者用在一起,顶层和底层const共存:
int x = 10;
const int* const p = &x;
那么上述代码p的语义有两层含义 : (1)p本身是const,不可指向别的对象 (2)p指向的对象是const的,不可以使用p来修改x。(当然,这里你可以通过修改x的值来改变*p)。
最后,我们总结一下const和指针用在一起的时候的写法:
const int* p = &x(等同于int const* p = &x),底层const。
int* const p = &x,顶层const。
一开始可能会被不同的写法搞混,有一个简单的技巧:
如果*在const的左边,那么这是一个顶层const
如果*在const的右边,那么这是一个底层const
C++11的constexpr
之前我们说过,const表示一个只读的含义,且在编译期它的值未必能够确定,从C++11开始,标准加入了一个constexpr的关键词,中文译为字面值常量,它是在编译器就可以确定的值。
我们做个简单的对比:
int main(){
int x = 10;
const int c1 = x;
constexpr int c2 = x;//error,x在编译期无法确定,不能给c2赋值
}
当然,我们有种比较特殊的情况:
const int x = 10;
int main(){
constexpr int c = x;//ok
}
由于x这次是在堆上分配的内存,在编译期它的值就可以被确定,所以这样赋值是ok的。
同理,我们可以这样做:
int x = 10;
int main(int argc, char* argv[]) {
constexpr int* p = &x;
}
和这样做:
const int x = 10;
const int y = x * 5;
int main(int argc, char* argv[]) {
constexpr int p = y;
getchar();
return 0;
}
如果这里有不明白的地方,推荐找一下C++编译器和运行期的相关知识来补充一下。
其余的用法constexpr和const差不多,也可以用用来修饰函数和指针等,不过,这里有个很特殊的地方:
constexpr int inc(int x) {
return x + 1;
}
int main(int argc, char* argv[]) {
int x = 10;
int arraya[inc(10)];//ok
int arrayb[inc(x)];//error
getchar();
return 0;
}
同这个例子,我们可以小结。
所以,对于constexpr需要两方面看待。constexpr修饰的函数,简单的来说,如果其传入的参数可以在编译时期计算出来,那么这个函数就会产生编译时期的值。但是,传入的参数如果不能在编译时期计算出来,那么constexpr修饰的函数就和普通函数一样了。不过,我们不必因此而写两个版本,所以如果函数体适用于constexpr函数的条件,可以尽量加上constexpr。而检测constexpr函数是否产生编译时期值的方法很简单,就是利用std::array需要编译期常值才能编译通过的小技巧。这样的话,即可检测你所写的函数是否真的产生编译期常值了。
作者:蓝色
链接:https://www.zhihu.com/question/35614219/answer/63798713
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。