1. const修饰符的作用
- const类型定义: 指明变量或对象的值是不能被更新,引入目的是为了取代预编译指令
- 可以保护被修饰的东西,防止意外的修改,增强程序的健壮性
- 编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高
- 可以节省空间,避免不必要的内存分配
2. 初始化和const
const修改全局变量是存储在全局区(即静态存储区),修饰局部变量时存储在栈区,const修饰的对象一旦创建后其值便不能再改变 ,类的数据成员只是声明,如果const修饰类的数据成员,那么必须要有构造函数来初始化这个const对象,其他情况必须在声明的地方就初始化。
默认状态下,const对象仅在文件内有效,如果想在别的文件使用此对象,需要使用关键字extern
3. const和引用
我们把const修饰的引用称为”常用引用“,常量引用不能直接修改所引用的对象,看下面的代码:
const int ci = 1024;//ci是一个int型的常量
const int &r1 = ci;//正确,r1是一个常量引用,并且r1本身也是一个常量
r1 = 42;//错误,引用被const限制了,不能修改所引用对象的值了
int &r2 = ci;//错误,试图让一个非常量引用指向一个常量对象
一般来说引用的类型和其所引用的对象的类型一致,但有两个意外,这里是其中一种,
- 初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可
int i = 42;
const int &r1 = i; //允许讲const int&绑定到一个普通int对象上
const int &r2 = 42; //正确,r2是一个常量引用
const int &r3 = r1 * 2;//正确,r3是一个常量引用
int &r4 = r1 * 2; //错误,右边是常量而左边并不是常量
int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0;//r1,r2都是i的引用
cout << "i " << i << endl;//0
cout << "r1 " << r1 << endl;//0
cout << "r2 " << r2 << endl;//0
r2 = 10 //错误,不能通过r2来修改i的值,因为有了const的限定
常量引用只是对引用可参与的操作做出了限定,对于引用的对象本身是不是常量未作限定,因此上面的代码中i不是常量,可以r1这个引用来修改它的值
4. const和指针
- 指向常量的指针不能用于改变其所指对象的值,只能使用常量指针(即对指针加const修饰)
- 常量指针都不可以去修改所指向的对象,常量引用也是不能修改所引用的对象,不管对象是不是常量都不能修改
const double pi = 3.1415; //pi是一个常量,它的值不能改变
double *ptr = π //错误:指向常量的指针必须是常量指针
const double *cptr = π //正确
*cptr = 42; //错误:常量指针不能修改对应的值
-
const指针即指针存放的地址就不能再改变,我们称为指针常量,*在前
int errNumb = 0;
int *const curErr = &errNumb;// curErr将一直指向errNumb这块内存
const double pi = 3.14;
const double *const pip = π //从右向左依次解修饰符,首先pip是一个常量,然后是*说明pip是一个常量指针,
//再然后是double说明是指向double的指针,最后是const说明这个double不能变,即指向的对象是一个常量的double
-
和引用一样,常量指针指向的对象并不一定是一个常量,虽然不能通过该指针修改,但是可以通过其他指针修改。
-
顶层const指的是指针本身是不是常量,底层const指的是指针指向的对象是不是常量
int i = 0;
int *const p1 = &i;//p1是一个顶层const,可以修改*p1,但是不可以修改p1
int ci = 42;//ci是一个const变量,本身值不允许发生变化都可以称为顶层const
const int *p2 = &ci;//是一个底层const,不允许修改*p2,但可以修改p2
const int *const p3 = p2;//既是底层const也是顶层const,*p3和p3都不可以修改
const int &r = ci;//r是一个常量引用
*p2 = 3; //错误,表达式左边必须是可修改的值
p1 = &i;//错误,表达式左边必须是可修改的值
5. const形参和实参
-
当用实参初始化形参时会忽略掉形参的顶层const,即实参值本身是const或者不是无所谓
void func(const int i);//传入的i可以是const,也可以是非const,但是func不能修改i
void func(int i);//错误,重复定义
-
但如果形参不是顶层const,那么实参也不能是顶层const,也就是说如果形参是可变的,传进来的值必须可变才行
int i = 0;
const int ci = i;//一个顶层const
//void reset(int* ptr); void reset(int& ptr);
reset(&i);//调用传入指针的函数
reset(&ci);//错误,实参是一个常量,是指向const int的指针,那么&ci是const int*,不能传递给int*
reset(i);//调用传入引用的reset函数
reset(ci);//错误,形参变量,实参也必须是变量,ci是常量
reset(42);//错误,字面值是一个常量,形参可变,不能传入常量
-
我们在写函数的时候形参应尽量使用常量引用
-
传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标 对象(在主调函数中)的操作。
-
使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作; 而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的 副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。
- 使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用”*指针变量名”的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
6. const返回值
返回值为const值,或者const指针,const引用时该如何考虑呢?
-
如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const 修饰没有任何价值,那个临时存储单元确实是个const int。
const int func1(int a);
int func1(int a);
//以上两个函数没有任何差异
-
如果给以“指针传递”方式的函数返回值加const 修饰,那么返回值可以是指针常量,也可以是常量指针
/*
第一种返回指针常量,//const毫无意义
*/
int* const func1(int a);//返回一个指针常量,虽然想返回一个顶层const的值,但是由于函数返回时复制到
//外部的临时存储单元中,这个const没有意义,虽然临时存储单元的指向确实无法改变
int* b = func1(2);//可以用int*接收返回值,b指向的值可以修改,b的指向也可以修改
/*
以上调用相当于
int* const c = func1(2);
int* b = c;
*/
const int* b = func1(2);//也可以用const int*接收返回值,
//此时的b是一个底层const,也就是说int* const转换成了const int*
/*
以上调用相当于
int* const c = func1(2);
const int* b = c;
*/
/*
第二种返回常量指针
*/
const int*func1(int a);//返回一个常量指针,这时是有意义的,返回的也确实是const int*
int* b = func1(2);//错误,因为const int*不能转成int*
const int* b = func1(2);//正确,b指向的值无法改变
/*
第三种返回常量指针
*/
int const*func1(int a);//和第二种完全一样
-
const修饰函数的引用返回值,函数返回值采用“引用传递”的场合并不多,这种方式一般只出现在类的赋值函数和拷贝构造函数中,赋值函数和一些操作符函数目的是为了实现链式表达,拷贝构造函数传引用是为了避免逻辑错误,因为函数返回时如果是返回值的话,会再次调用拷贝构造函数这样会无限循环,因此需要返回引用。返回的引用不能是函数内的临时变量的引用
class B{
public:
int val = 0;
B(int val):val(val){};
B& operator=(const B &other) {
val = other.val;
return *this;
}
};
首先说明为什么这里要传引用而不是传值,即为什么不是B operate = (const B &other)?
类的成员函数默认都有一个this指针代表对象本身作为函数的形参,只是隐藏起来了;
a = b = c可以写成a = (b = c),若是传值,则返回的值是一个临时变量,如下:
B tmp = (b = c);//左边的等号是拷贝的意思不是操作符=,tmp的值和c的值是相等的
a = tmp;//此时a和b的值都被赋成了c
//考虑下面的代码块
B b1(3), b2(4),b3(5);
cout << "b1: " << b1.val << endl;
cout << "b2: " << b2.val << endl;
cout << "b3: " << b3.val << endl;
cout << "b1=b2=b3" << endl;
b1 = b2 = b3;
cout << "b1: " << b1.val << endl;
cout << "b2: " << b2.val << endl;
cout << "b3: " << b3.val << endl;
/*
输出:
b1: 3
b2: 4
b3: 5
b1=b2=b3
b1: 5
b2: 5
b3: 5
*/
这里是正确的赋值了,但是如果考虑非正常的链式(a = b) = c,我们的本意是想先把b的值赋给a,再把c的值赋给a
//以上链式相当于
B tmp = (a = b);//左边的等号是拷贝的意思不是操作符=,tmp的值和a的值是相等的
tmp = c;//此时tmp的值被赋成了c,并没有改变a,只是改变了这个临时变量而已
//考虑下面的代码块
B b1(3), b2(4),b3(5);
cout << "b1: " << b1.val << endl;
cout << "b2: " << b2.val << endl;
cout << "b3: " << b3.val << endl;
cout << "(b1=b2)=b3" << endl;
(b1 = b2) = b3;
cout << "b1: " << b1.val << endl;
cout << "b2: " << b2.val << endl;
cout << "b3: " << b3.val << endl;
/*
输出:
b1: 3
b2: 4
b3: 5
(b1=b2)=b3
b1: 4
b2: 4
b3: 5
*///不符合我们的预期结果
换成传引用之后,,以上结果都符合我们的预期,在(a = b) = c的链式中,可理解如下:
B &tmp = (a = b);//返回的是a的引用,即tmp是a的引用
tmp = c;//就通过a的引用tmp把a的值修改成了c
现在理解清楚返回值是引用的意义后,考虑用const限定引用返回值,即const B& operator=(const B &other)
我们首先考虑a = b = c链式
const B& tmp = (b = c);//因为b = c的返回值是b的常量引用,那么接收值也必须是常量引用,此时的B是一个底层cosnt
//但引用具有一旦被绑定后就不能再转移的特性,也就是说引用本身就具有了顶层const的特性
a = tmp;//a也被赋值为b的值,此时b的值和c的值是相等的
//总结a = b = c链式没有影响
我们接着考虑(a = b) = c链式
const B& tmp = (a = b);
tmp = c;//这里的语句是错误的,因为tmp是常量引用,底层const,不能通过tmp修改a的值
int& func1(int& a) {
return a;
}
const int& func2(int& a) {//这里可以是int const&, int& const都一样
return a;
}
int a = 1;
int b = func1(a);
int c = func2(a);
c = 3;
b = 2;
//以上代码都是合法的,也就是说这时的const起不到任何作用
7. const函数
class Stack
{
public:
void Push(int elem);
int Pop(void);
int GetCount(void) const; // const 成员函数
private:
int m_num;
int m_data[100];
};
int Stack::GetCount(void) const
{
++m_num; // 编译错误,企图修改数据成员m_num
Pop(); // 编译错误,企图调用非const 函数
return m_num;
}