C++ Primer学习笔记——const限定符
有时候,我们希望定义一种值不能改变的变量,即只读变量,为了满足这一要求,可以用关键字 const 对变量加以限定。因为 const 对象一旦创建就不可以改变,因此 const 对象必须初始化。
1、 初始化 const 对象
const int a=0; //正确,编译时初始化
//const int b; //错误,b未初始化
int i=1,j;
const int c=i; //正确,i的值拷贝给c
j=c; //正确,c的值拷贝给j
2、 const 引用
用 const 修饰引用,表示该引用是一个只读的引用,即通过该引用只能读取绑定在该引用的对象的值。由于这一特性,与普通引用不同的是 const 引用能通过字面值常量对其初始化。
用 const 对象初始化 const 引用时,没什么问题,对象的值不能更改,也不能通过引用更改 const 的值。但是,当用一个非 const 初始化 const 引用时,虽然不能通过引用更改对象的值,但却可以通过对象直接修改对象的值。这样讲可能有点晕,直接上代码。
int a1 = 0;
int a2 = 1;
const int a3 = 2;
//int &a = a3; //错误,用一个 const 对象初始化普通引用,若是这种情况编译能通过的话,那站在a的角度,
//a认为可以通过其修改被绑定的值(实际上不能修改),这样显然会出大问题
const int &b = a1; //b是a1的常引用,不能通过b修改a1的值
//b = 1; //错误,不能通过b修改a1的值
const float &b0 = a1; //此处,若是b0不是const引用,编译是通不过的,由于a1(int型)可以转float型,
//因此,先会通过a1构造出一个float型的临时常量,再用这个临时量来初始化b0,这里其实和下面b1的初始化差不多
//注:所谓临时量就是当编译器需要一个空间暂存表达式的求值结果时创建的一个未命名对象,
//通常会在函数传参、隐式类型转换、函数返回值时使用
const float &b1 = 1; //用字面值初始化 const 引用
cout << "更改a1前:" << endl;
cout << "b: " << b << endl;
cout << "b0: " << b0 << endl;
cout << "b1: " << b1 << endl;
cout << "更改a1后:" << endl;
a1 = 2; //直接修改a1的值
cout << "b: " << b << endl;
cout << "b0: " << b0 << endl;
cout << "b1: " << b1 << endl;
运行结果如下图:
3、 指向 const 对象的指针
与引用一样,可以用指针指向常量或者非常量,这类似于常量引用,指向常量的指针不能被用于修改被指向对象的值。
const double pi = 3.14; //pi是个常量,它的值不能改变
//double *ptr = π //错误:ptr是普通指针
const double *cptrt; //正确:指向 const 的指针可以不在声明时初始化
const double *cptr = π //正确:cptr可以指向一个double常量
//*cptr = 42; //错误:不能给*cptr赋值
const double pi2 = 3.141;
double pi3 = 3.1415;
cptr = &pi2; //正确:虽然不能修改cptr指向的对象值,但却可以更改cptr指向的对象
//注:const 修饰的是cptr指向的对象,而不是cptr
cptr = &pi3; //正确
cout << "修改前*cptr:" << *cptr << " ";
pi3 = 3.3;
cout << "修改后*cptr:" << *cptr << endl;
运行结果如下,这里其实和 const 引用类似。
4、 const 指针
指针是对象,而引用不是,因此,允许把指针定为常量,这就是常量指针。常量指针必须初始化,一旦初始化,则它的值(就是放在指针里的那个地址)不允许更改。但是,可以通过指针修改被指向对象的值(如果该常指针指向的不是 const 对象)。
int A = 0;
int *const a = &A;
*a = 1; //正确:可以通过*a来修改A
const int B = 1;
//a = &B; //错误:a中存的地址不能变
//int *const b = &B; //错误:在b的角度,它指向的是非const对象,
//若这里编译能通过的话,就可以通过*b来修改B,这是不行的
const int *const b = &B; //正确:b是一个指向常对象(指向的对象的值不能变)的常指针(指针里存的地址不能变)
5、 顶层 const 与底层 const
如前所述,指针本身是个对象,它又可以指向另一个对象。因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个独立的问题。用名词 顶层 const 表示指针本身是常量,而用名词 底层 const 来表示指针所指的对象是常量。
更一般的,顶层 const 可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层 const 则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层 const 也可以是底层 const ,这一点与其他类型区别较明显。
当执行拷贝操作时,常量是顶层const还是底层const区别明显,其中顶层const不受什么影响。(摘自 C++ Primer)
底层const拷贝时,或者拷贝双方都有底层const资格,或者将无底层const修饰的变量拷入有底层const修饰的量。
//摘自 C++ Primer
int i = 0;
int *const p1 = &i; //不能修改p1的值,这是个顶层const,但能修改*p1
const int ci = 42; //不能修改ci的值,这是个顶层const
const int *p2 = &ci; //允许修改p2的值,这是个底层const,但不能修改*p2
const int *const p3 = p2; //靠右的是顶层const,靠左的是底层const
const int &r = ci; //用于声明引用的都是底层const
i = ci; //正确,拷贝ci的值,ci是顶层const修饰,对操作无影响
p2 = p3; //正确,p2是底层const修饰,因此可以改变p2里保存的地址
//p3也是底层const修饰,因此可以让p2指向p3指向的对象(因为都##不可通过p2,p3修改对象)
//若p2不是底层const修饰,则不能这么操作,理由上面已经有说明了
//int *p = p3; //错误:p不是底层const修饰
p2 = &i; //正确:i无底层const,p2有底层const
//int &r = ci; //错误:普通引用不能绑定在常量上
const int &r2 = i; //正确:const int 引用 可以绑定在普通int上
6、const 修饰类成员变量
const 类成员变量和之前所述的基本没啥不同。有一个要注意的就是一定要初始化。而初始化的方式有两种。一种是在声明的时候初始化,另一种是在构造函数的初始化列表初始化。若同时在声明的时候初始化以及在构造函数初始化列表中初始化,那构造对象时,若该构造函数的初始化列表中有该 const 成员的初始化,则进行这个初始化(不进行声明时的初始化),若构造函数的初始化列表没有该 const 成员的初始化,则进行声明时的初始化。可能说晕了,直接上代码:
#include<iostream>
using namespace std;
class A {
public:
int n;
const int nn = 1; //这里也可以不初始化,这里不初始化的话,一定要在初始化列表初始化
public:
A() :n{ 1 } {
//nn = 1; //就算声明时没初始化,也不能这么做,在构造函数的函数体里是赋值,不是初始化
cout << "无参数构造函数" << endl;
};
A(int a) :nn(a), n(a) {//这里的初始化顺序是按照声明的顺序来,与初始化列表的顺序无关,
//即先初始化n,再nn
cout << "单参数构造函数" << endl;
};
};
int main()
{
A a1;
A a2(2);
cout << "a1: " << a1.n << " " << a1.nn << endl;
cout << "a2: " << a2.n << " " << a2.nn << endl;
system("PAUSE");
return 0;
}
结果如下图:
7、 const 修饰类成员函数
当类的对象调用类成员函数时,成员函数其实是通过一个名为 this 的隐式指针来访问调用它的对象。这样的话问题就来了。设想类 A 现在有一个 const 对象,要调用一个成员函数,那么就要将它的 this(const A *类型) 指针传递给该成员函数,而成员函数中 this 是 A* 的类型。根据之前讲到的,不具底层 const 资格的指针被具有底层 const 资格的指针赋值,显然要炸。为此,通过在成员函数的参数表后加 const 来说明这是个常成员函数(即 this 是 const A* 类型)。
//给第6小节的类A加上下面两个成员函数,其它不变
int fun() const{
cout << "常成员函数!" << endl;
}
int fun1() {
cout << "非常成员函数!" << endl;
}
const A a;
A b;
a.fun(); //正确:底层const的this拷给底层const的this
//a.fun1(); //错误:底层const的this拷给非底层const的this
b.fun(); //正确:非底层const的this拷给底层const的this
b.fun1(); //正确:非底层const的this拷给非底层const的this