一、对于修饰变量的用法
对于const和&的基础用法就不说了。下面说点之前有误区和容易错的地方
const修饰变量的误区
关于const类型,这里有一个我之前的误区,我以为const定义的时候只能用常量初始化,但是实际上是没有这个规定的。const定义的变量可以用各种const,非const对其进行初始化,甚至可以是函数的返回值之类的。
const int a = 5; //在编译的时候进行初始化,即类似于#define,进行全文替换
const int b = size(); //在执行的时候初始化
int n =7;
const int c = n; //在执行的时候初始化
- 1
- 2
- 3
- 4
这个其实非常显然啦,因为在函数传参的时候,就能看出来,形参可以是const的,但是传进去的实参可以是非const的。[1]
对于这个问题就可以看出中文各种博客上面的说法有多垃圾了,就我看过的80%的博客都说const必须要用常量初始化,同时在编译时就完成了初始化。以后技术问题少看中文博客,之前被误导了那么久。
常量指针和指向常量的指针
指向常量的指针
const int *p,表示指向常量的指针,按照之前上篇文章中的方法C++
中复合类型声明的理解从右到左看,p是一个指针,指向const
int,表示的意义就是不能通过p来修改p指向的值,而p本身所指向的地址是可以修改的。(此处也可以写成int const *p是一样的)
常量指针
int *const p,表示p本身是一个const,所以p所指的地址不会变,但是地址里的值是可以修改的。
有个需要注意的地方是:常量引用。
const int i1 = 1024;
const int &ci1 = i1;
int i2 = 2048;
const int &ci2 = i2; //允许将const int&绑定到一个普通int上,其实常量引用可以用任何值进行初始化
- 1
- 2
- 3
- 4
其实常量引用这个说法是一个非常不严谨的说法,因为C++中本身就不允许随便改变引用所绑定的对象。所以所有引用都是常量。而常量引用类比于上面指针的说法,应该是对常量的引用。
还要记住一点就是常量引用可以用任何值进行初始化,即可以用右值对其初始化。
int i = 1024;
const int &ri = i*2;
- 1
- 2
对于这一点,可以用复制构造函数去理解,在C++11标准的右值引用出来之前,就是用常量引用来延长一个右值的生命周期的。其在计算机内的实现可以认为是这样的:
const int tmp = i*2;
const int &ri = tmp;
- 1
- 2
即ri绑定的是一个临时变量。在C最早版本的时候,引用时可以绑定左值和右值的,只不过如果出现上面这种情况,ri绑定一个临时量,如果ri不是const的话,其修改也会导致临时量的修改,而此时临时值已经被销毁了。所以ri必须是const的。
顶层const与底层const
这里有一个顶层const和底层const的区别,正常情况下,const类型的变量可以给任何变量赋值和初始化的。但是指针就不一样了,指针本身是个对象,而其可以指向另外一个对象,所以指针本身是否是const和其所指的是不是一个常量就是两个独立的问题。
顶层const是指const修饰变量本身,即变量/对象本身是;而底层const则表示这个变量所指向/引用的值不可变,即不能通过这个变量修改其值,只有对常量的引用和指向常量的指针是底层const
int i=0;
int *const r3 = &i;
const int r1 = i;
- 1
- 2
- 3
以上例子是顶层const即对象本身是const的。而
const int *r4 = &i;
const int &r5 = i;
- 1
- 2
则是底层const,其所指的对象是常量,即不能通过自己修改原来数的值
对于底层const而言,底层const只能赋值给同样是底层的const,即int* r6 = r4会报错。
注意下面一个问题
const int m = 0; //m本身是const类型的值
const int* m2 = &m; //所以&m是一个const int*类型的,即常量取地址得到的是一个底层const
int* const m3 = &m; //所以第二行没有错,第三行会报错:
const int i = 0; //i是底层const
int &i2 = i; //报错,因为i2不是底层const
- 1
- 2
- 3
- 4
- 5
- 6
第一个报的错误就是 “const int *” 类型的值不能用于初始化 “int *const” 类型的实体
第二个报的错误就是将 “int &” 类型的引用绑定到 “const int” 类型的初始值设定项时,限定符被丢弃.
// 可以将const看成是一种责任,如果一个指针是const int*,则它有这个责任保证任何情况下不能通过它修改数据(直接或者间接的),上面的做法就是一种间接的做法,所以不合法。只有对方也同样有这个责任感的时候才可以赋值,即底层赋值给底层const. 同理如果const int** cp = int** cp也是非法的,为什么因为可以间接的修改最终的int数据--> *cp的值是一个没有const责任的指针,所以这种做法也是非法的。
二、对于修饰函数的用法
引用限定符
先看一个神奇的语句
string s1 = "hello ";
string s2 = "world!";
s1+s2 = "wow!";
string("adfasdf") = "123"; //注意此时一定要加string,因为C++对于常量字符串是用const char*数组保存的,
// char* = "123";
// *char = '9'; // Error: Because they point to the same area which is readonly.
- 1
- 2
- 3
- 4
- 5
- 6
以上这三句话是可以顺利运行的,原因在于s1+s2得到的是一个右值,而string类型的对象,会重载=运算符: string &operator=(const string&)
而=运算符可以作为一个函数进行调用,而对对象的函数进行调用时是不在乎其是左值还是右值,如(s1+s2).size()肯定是可以运行的,所以上面的那个=号也是可以运行的。
还有一点需要注意就是=号返回的是一个左值,所以如果对于连等的情况a1 = a2 = a3,其从右向左一次调用=号,等价于a1 = (a2 = a3),而此时调用的是=是复制运算符,而不是移动复制运算法符,因为返回的是左值。
所以本质上来说,因为函数只能限制传入的参数类型,和返回的类型,并不能限制this本身的类型导致的。
但是现在新标准为了填旧标准的这个坑,就加入了引用限定符(reference qualifier)虽然string这个类为了像前兼容,上述写法还是可以用的,但是对于我们自己写的对象来说可以避免这种情况了。跟const类似,引用限定符是加在参数列表之后的:
Foo &Foo::operator=(const Foo&) &; //this此时只能是可修改的左值
Foo &Foo::operator=(const Foo&) &&; //this此时只能是右值
- 1
- 2
在函数声明结尾加入&和&&来限制this本身。所以此时再重载=运算符,其后加上&,就能避免之前的那种情况了。
还有几点需要注意:
- const和&都只能用来修饰类的非static成员函数,在函数的声明和定义中都要出现
- 一个函数可以右const和&同时修饰,但是此时const必须加在&之前
const限定符
这个其实没啥说的,就是在把const加在参数列表之后。
- 常量函数只能调用常量函数。
- 常量对象只能调用其中的常量函数