C++ Const详解
1、const限定对象必须初始化
1const int i = 100; //正确:编译时初始化
2const int j; //错误:j是一个未初始化的常量
2、const限定的值不可以改变
1cosnt int i = 100;
2i = 5; //错误:试图向const修饰对象写值
3、cosnt对象仅在文件中有效
1.如果程序包含多个文件,定义同名全局变量不加const,由于全局变量是外部链接所以会重复定义。
2.对全局变量加上const限定符,限制变量的作用域为文件内(内部链接),所以不会重复定义
//a.h
const int ci = 1024; //正确
//b.h
const int ci = 512; //正确
//c.h
const int ci = 2048; //正确
///<编译正确
如果想只需要定义一次,在多个文件之间共享这个const对象
那么对于cosnt变量不管是声明还是定义都添加extern关键字。
//a.h
extern const int ci = 1024;
//b.h
extern const int ci; //与a.h中定义的ci是同一个,值为1024
//c.h
extern const int ci; //与a.h中定义的ci是同一个,值为1024
//< a.h的常量用extern限定使其被其他文件使用
//< b.h, c.h的extern指明ci并非文件独有,它的定义在别处出现
二、const的引用
1、对常量的引用
简称“常量引用”是 “对常量的引用”。
严格来说并不存在常量引用。因为引用不是一个对象。
const int ci = 1024; 2const int &cri = ci;
2、对常量的引用初始化
引用类型必须与所引用对象的类型一致。但有两个例外:
第一种就是在初始化常量引用时允许用任意表达式作为初始,只要改表达式的结果能转成引用类型即可。
第二种基类的引用绑定到派生类对象上(本文不解释)
int i = 10;
const int &cri1 = i; // 允许将const int&绑定到一个普通的int对象上
const int &cri2 = 42; //正确:绑定一个临时量对象 //编译器会给常量42开辟一片内存,并将引用名作为这片内存的别名
const int &cri3 = cri1 * 2; //正确:绑定一个临时量对象 ;
int &ri4 = cri1 * 2; //错误:ri4是一个普通的非常量引用
double d = 3.14;
const int & cri4 = d; //正确:绑定一个临时量对象
int &d=15; //error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
int* p = (int *)&c; //const转非const必须显式
*p = 10; //修改了为15开辟的内存内容
常量引用之所以可以这样初始化,它绑定的是一个临时量的对象
以最后一个为例,编译器把上述代码变成下面的形式:
double d = 3.14;
const int temp = d; //由双精度浮点数生成一个临时的整型常量
const int &cri4 = temp; //让cri4绑定这个临时量
非const的引用不可以引用一个const的对象
const int ci = 1;
int &ri = ci; //错误
const的引用可以引用一个非const的对象
int i = 42;
int &ri = i; //引用ri绑定对象i
const int &cri = i; //(合法行为)cri也绑定对象i,但是不允许通过cri修改i的值
ri = 0; //ri非常量,i的值修改为0
cri = 0; //错误:cri是一个常量引用
三、指针和const
1、指向常量的指针(pointer to const–指针常量)
指向常量的指针可以指向常量和非常量对象
普通指针不可以指向常量对象
const double pi = 3.14;
double *ptrd = π //错误:ptrd是一个普通指针,想要常量对象的地址,只能使用指向常量的指针
const double *cptrd = π //正确:指向常量
*cptrd = 100; //错误:不能给*cptrd赋值
指针的类型必须与其所指对象的类型一致。但有两个例外:
第一种就是允许另一个指向常量的指针指向一个非常量对象
第二种基类的指针指向派生类对象上(本文不解释)
double d = 3.14; 2const int *cptri = d;
2、const指针(const pointer–常量指针)
指针是对象而引用不是,因此指针可以像其他类型的对象一样,允许把指针本身设置常量。
常量指针必须初始化,一但初始化完成,则值不能再改变
int i = 0;
int j = 1;
int *const cptri = &i; //cptri将一直指向i
*cptri = 100; //i的值修改为100
cptri = &j; //错误:cptri的值不能修改
const double pi = 3.14;
const double * const cprtpi = & pi; //cprtpi 是一个指向常量的常量指针
*cprtpi = 2.14; //错误: cprtpi也是指向常量的
四、顶层const和底层const
定义
顶层cosnt:本身是一个常量。
底层cosnt:指向或引用的对象是一个常量。
int i = 0;
int *cosnt p1 = &i; //不能改p1的值,顶层cosnt
const int ci = 42; //不能改ci的值,顶层cosnt
const int *p2 = &ci; //允许改变p2的值,底层cosnt
const int *const p3 = p2; //靠右边的const是顶层cosnt,靠左边的const是底层cosnt
const int &r = ci; //声明引用的const都是底层cosnt
操作
当执行对象拷贝操作时,顶层cosnt不受影响:
i = ci; //正确:ci顶层const,操作无影响
p2 = p3; //正确:p2和p3指向的对象类型相同,p3顶层cosnt的部分不影响
当执行对象拷贝操作时,关于底层cosnt,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。
一般来说非常量可以转换成常量,反之不行
int *p = p3; //错误:p3包含底层const,而p没有
p2 = p3; //正确:p2和p3都是底层cosnt
p2 = &i; //正确:非常量转换常量
int &r = ci; //错误:普通的int& 不能绑到int常量上
const int &r2 = i; //正确:cosnt int&可以绑定到普通int上
五、const形参和实参
1、顶层const形参
顶层const作用对象本身
const int ci = 10;
int i = ci; //正确
int *const cp = &i;
int *p = cp; //正确
当实参初始化形参时会忽略掉顶层const。也就是,形参的顶层cosnt被忽略掉了。
所以当形参是const时,传给常量对象或者非常量对象都是可以的。
void func(const int i){/**func能够读取i,但是不能向i写值*/}
调用func函数时, 即可以传入const int也可以传入int。
顶层const带来重复定义问题
形参忽略掉了顶层const可能产生异想不到的结果
1void func(const int i){/**func能够读取i,但是不能向i写值*/} 2 void func(int i){/*...*/} //错误:重复定义了func
因为顶层cosnt被忽略掉了,所以上面的代码两个func函数的参数可以完全一样。因此发生了重复定义
2、指针或引用形参与cosnt
我们可以使用非常量初始化一个底层cosnt对象,但是反过来不行;
同时一个普通的引用必须同类型对象初始化
int i = 10;
const int *cp = &i; //正确:常量指针可以指向非常量对象
const int &r = i; //正确:常量引用可以绑定非常量对象
const int &r2 = 10; //正确:绑定临时量对象
int *p = cp; //错误:底层cosnt:常量转非常量
int &r3 = r; //错误:底层cosnt:常量转非常量
int &r4 = 100; //错误:不能用字面值初始化非常量引用
将上面初始化规则引用到参数传递(初始化形参)上:
void func(int *);
void func(int &);
int i = 0;
const int ci = i;
unsigned int ui = 0;
func(&i); //正确:调用int*形参
func(&ci); //错误:不能用const int*初始int*,底层const不能转换成非cosnt
func(i); //正确:调用int& 形参
func(ci); //错误:不能吧普通引用绑定到const对象上
func(42); //错误:不能用字面值初始化非常量引用
func(ui); //错误:类型不匹配
3、重载和const
顶层cosnt不影响传入函数的对象,一个拥有顶层const的形参无法和另一个没有顶层const的形参区分出来。
Record lookup(Phone);
Record lookup(const Phone); //重复声明了Record lookup(Phone)
Record lookup(Phone*);
Record lookup(Phone *const); //重复声明了Record lookup(Phone*)
如果形参是底层const,可以实现重载
Record lookup(Phone& ); //函数作用于Phone的引用
Record lookup(const Phone&); //新函数:作用于常量引用
Record lookup(Phone*); //新函数。作用于指向Phone指针
Record lookup(const Phone *); //新函数,作用于指向常量的指针
六、类和const
1、cosnt成员函数
class A{
public:
void func() const;
}
//表示在常量成员函数func里this是一个指向常量的指针
//可以想象为void func(const A* const this);
//也就是说func可以读取它的对象的数据,但是不能写入新值。
通过区分成员函数是否是常量成员函数,可以对其进行重载。
七、模板类型左值引用参数推断类型
当一个函数参数是模板类型参数的一个普通(左值)引用时(即,T&),绑定规则告诉我们,只能传递给它一个左值。
template<typename T> void func1(T&); //实参必须是一个左值
func1(i); //i是一个int;模板参数类型T是int。
func1(ci); //ci是一个const int;模板参数T是cosnt int
func1(10); //错误:传递给一个&参数的实参必须是一个左值
如果一个函数参数是const T&,则可以传递给它任何值实参:一个对象(cosnt或非cosnt)、一个临时对象或是一个字面常量值。
template<typename T> void func2(const T&); //可以接受一个右值
func2(i); //i是一个int;模板参数类型T是int。
func2(ci); //ci是一个const int;模板参数T是int
func2(10); //const &参数可以绑定到一个右值,T是int
八、constexpr和常量表达式
1、常量表达式
常量表达式是指不会改变并且编译过程就能的到计算结果的表达式。
字面值, 用常量表达式初始化的const对象
const int max =20; //max常量表达式
const int limit = max + 1; //limit常量表达式
int i = 100; //i不是常量表达式, i值会改变
const int sz = get_size(); //sz不是常量表达式,sz运行时才能得到具体指
2、constexpr变量
在复杂的系统中,很难分辨一个初始值到底是不是常量表达式。
c++11新标准中规定,允许将变量声明为constexpr类型以便有编译器来验证变量的值是否是一个常量表达式。
声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。
constexpr int m = 20; //20是常量表达式
constexpr int l = m + 1; //m + 1是常量表达式
constexpr int sz = size();
//只有当size()是一个m + 1函数时才是一条正确的表达式
虽然不能用普通函数作为constexpr变量的初始值,
但新标准允许定义一种特殊的constexpr函数。这种函数应该足够简单,简单到编译时就可以计算其结果。
这样就可以用constexpr函数去初始化constexpr变量
3、字面值类型
常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见,容易得到,就把它们称为“字面值”。
算术类型、引用、和指针,字面值常量类是字面值类型。
自定义类、IO类不属于该类型。
4、指针和constexpr
在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指对象无关
const int *p = nullptr; //p是一个指向整型常量的指针
constexpr int *q = nullptr; //q是一个指向整数的常量
//> p 指向常量的指针, q常量指针(顶层cosnt)
constexpr把它多定义的对象置为顶层const
与其他常量指针类似, constexpr可以指向常量或非常量
int j = 0;
constexpr int i = 100; //i的类型是整型常量
constexpr const int *p = &i; //p是常量指针,指向整型常量o
constexpr int * p1 = &j; //p1是常量指针,指向整数j
5、constexpr函数
constexpr函数是指能用于常量表达式的函数。
定义constexpr函数与其函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句:
constexpr int size() {return 100;}
constexpr int sz = size(); //正确:sz是一个常量表达式
执行该初始化任务时,编译器把对constexpr函数的调用替换成其结果值。
为了能在编译过程中随时展开,constexpr函数被隐式的指定为内联函数。
我们允许constexpr函数的返回值并非一个常量
constexpr size_t scale(size_t cnt) { return size() * cnt;}
当scale的实参是常量表达式时,它的返回值也是常量表达式;反之不然:
int arr[scale(10)]; //正确:10是常量表达式,scale(10)是常量表达式。
int i= 2; //i不是常量表达式
int a2[scale(i)]; //错误:scale(i)不是常量表达式
九、const_cast
const_cast只能修改运算对象的底层const,const_cast可以去掉const性质。
const char *pc;
char *p = const_cast<char *>(pc); //正确
const_cast和重载
const string &shorterString(cosnt string &s1, cosnt string &s2)
{ return s1.szie() <= s2.size() ? s1 : s2; }
//函数的返回类型都是const string的引用。当我们用两个实参调用这个函数,返回的结果仍然const string的引用。
//下面的shorterString当实参不是常量时,得到的结果是一个普通的引用。
string &shorterString(string &s1, string &s12)
{ auto &r = shorterString(const_cast<const string&>)(s1), const_cast<const string&>)(s2)); return const_cast<string&>(r); }
//const_cast将string &强制转换为const string&
十、常方法和常对象
1.常方法 其中this指针由Test* const--> const Test*const
void func() const;
①可以访问对象中的常成员,也可以访问普通成员
②该方法不允许修改任何数据的值
特别地, 静态数据成员在const函数中可以修改
2.常对象:常对象只能调用该类中的常成员函数常方法
3.常数据成员:
①只能通过构造函数的初始化表就行初始化,其他方式皆不允许
②可以被const成员函数访问,也可以被该类中普通函数访问(但不允许修改其中的值)