- const的作用:
- 指定一个语义约束(即指定一个不应该被改动的对象),而编译器会强制实施射中约束
- const约束对象的访问性质:使得对象只能读,不能写:不允许修改对象的值.
- const 是constant 的缩写,“恒定不变”的意思。被const 修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。所以很多C++程序设计书籍建议:“Use const whenever you need”。
- const的使用场景:
- 修饰变量,说明该变量不可以改变
- 修饰指针,分为指向常量的指针和指针常量
- 常量引用,经常用于形参类型,即避免了拷贝,又避免了函数对值的修改
- 修饰成员变量,说明该成员函数不能修改成员变量
char greeting[] = "Hello";
char *p greeting; // 指针、数据都可变
const char * p = greeting; // 指针可变,数据是常量
char const * p = greeting; // 指针可变,数据是常量
char * const p = greeting; // 数据可变,指针是常量
const char * const p = greeting; // 指针、数据不可变
const
const修饰变量
C的const表示修饰只读变量,但是我们可以恶意强制操作const对象。
// C
const int num = 100;
(int *)&num = 4; //可以间接修改
c++的const修饰的变量有内存实体,但是只能是右值;
C++的const变量一旦初始化,就会强制告诉编译器对这块内存的修改无效,不可能通过它去操作这块内存的值。
// c++
const int i = 0; //是一个右值,有内存实体
i = 11; //错误:向只读变量‘i’赋值
const int a{1.1}; //错误:从“double”转换到“int”需要收缩转换,请初始化成相同类型的常量
C++必须初始化const修饰的变量,除非是extern修饰的变量,C中如果不初始化const常量会警告,CPP中会报错
// C++
const int i ; // 错误:未初始化的常量‘i’ [-fpermissive]
extern const int a; //常量 变量 "a" 需要初始值设定项[严重性 “a” : 如果不是外部的,则必须初始化常量对象
const变量的初始值值可以是任意复杂的表达式
C与CPP的const之间最大的区别是如果CPP中的const初始化为寄存器常量,就会像define一样优化,如果是初始化为内存变量,编译器就不会去优化
#include <iostream>
#include <array>
#include <algorithm>
void main()
{
const int num{10};
*(int *)&num = 3;
std::cout << num << std::endl; //10
std::cout << *(&num) << std::endl;//10
int a{ 10 };
const int num1{ a };
*(int *)&num1 = 3;
std::cout << num1 << std::endl; //3
std::cout << *(&num1) << std::endl;//3
std::cin.get();
}
原因:如果是寄存器常量初始化const变量,寄存器直接优化,使用10替换;如果是用内存变量初始化const变量,寄存器不敢优化,必须老老实实读内存。
总结
- 编译器会将const表示修饰的是一个伪常量,不可以直接或者间接修改
- const修饰的常量必须初始化,最好使用{}初始化成一样的数据类型,建议使用{}而不是()初始化,因为{}会检查收缩转换
- const修饰的名字如果用寄存器常量如10初始化那么它是一个常量,如果用变量初始化那么它是一个变量。也就是说,如果是用寄存器常量修饰const变量,那么编译器就会自动优化成一个常量,如果是内存变量初始化const变量,编译器就不敢随便优化了
const与别名:
关于const pstring *ps
- ps是一个指针,指向的类型由下一个符号决定
- const是对给定类型的修饰,而pstring实际上就是一个类型,表示指向char的指针,因此,const修饰的是pstring,即const pstring就是指向常量的指针
const与迭代器
STL迭代器是以指针为根据塑造处理的,所以迭代器的作用就像个T*指针。
- 声明迭代器为const,表示这个迭代器不得指向不同的东西,但是它所指的值是可以改动的
- 如果你希望迭代器所指的东西不可改动,需要使用`const_iterator`
#include <vector>
int main(int argc, char* argv[])
{
std::vector<int> vec = {1, 2};
const std::vector<int>::iterator iter = vec.begin();
*iter = 10;
// ++iter; // error: iter 是const
std::vector<int>::const_iterator cIter = vec.begin();
++cIter; // ok
// *cIter = 10; // error:*citer是const
return 0;
}
C++中const与强制转换
C风格的强制转换
为什么下面的结果会是那样,没有想明白
const int num = 5; /*是一个右值,有内存实体*/
const int *pnum0 = # //内存初始化,因此不优化
int *pnum1 = (int *)# //C风格强制转换
*pnum1 = 55;
cout << pnum0 << "\t"<< pnum1 << "\t"<< &(*pnum1) << endl // 0x7ffe487a6b7c 0x7ffe487a6b7c 0x7ffe487a6b7c ---> pnum0 = pnum1 = &(*pnum1),他们指向同一块内存
<< *pnum0 << "\t"<< *pnum1 << "\t"<< num << endl ; //55 55 5
const int num = 5;
int *pnum2 = const_cast<int *>(&num); //强制去掉const属性
const int *pnum0 = #
*pnum2 = 55;
cout << pnum0 << "\t" << pnum2 << "\t" << &(*pnum2) << "\t" << &num << endl
<< *pnum2 << "\t" << num << endl;
//pnum0 = pnum1 = &(*pnum1),他们指向同一块内存
//*pnum1 = 55,num = 5
不可能通过强制类型转换修改const的值
为什么要这样设计:
#include <iostream>
using namespace std;
/*去银行存钱,你传入的是变量,但是存钱程序必须传入让你的钱变成常属性,以防止被人恶意修改
可以给一个可读写内存临时限制为只读内存,可以将一个只读内存限制为只读内存const int & c
*/
int Sele(const int & c)
{
//c -= 100; // ERROR
cout << &c << endl;
return c;
}
//
int Sele1(const int & c)
{
int z = const_cast<int &>(c); //强制去掉const属性.推测:重新开辟一段内存,将c的数据复制到新内存,新内存没有const属性,可以修改。但是原来的内存仍是const
z += 1;
cout << &z << endl;
return z;
}
int main()
{
//强引用
const int num1 = 100;
int num2 = 100;
cout << "const int \t"<< &num1 << "\t" << Sele1(num1) << "\t" << num1 << endl;
cout << " int \t"<< &num2 << "\t" << Sele1(num2) << "\t" <<num2 << endl; // //num2和c的地址相同,引用操作的是同一块内存,只是在作为传参时给这块内存加入一个只可以读的权限,不让被人去修改这块内存
}
const的引用
可以把引用绑定到const对象上,这个叫做对常量的引用
指向常量的引用仅仅要求不能通过该引用改变对象的值,但是没有规定那个对象的值不能通过其他途径改变
const与指针
指向常量的指针仅仅要求不能通过该指针改变对象的值,但是没有规定那个对象的值不能通过其他途径改变
指针是对象而引用不是,因此允许把指针本身定义为常量。常量指针必须初始化,而且一旦初始化完成,则它就不能改变它的指向
阅读变量时要从右往左读,比如 int *const curErr:
- 离curErr最近的是const,表示curErr是一个常量对象,表示它的值一旦决定了就不能在改变。对象的类型由声明符的其他部分决定
- 声明符的下一个符号是*,表示curErr是一个常量指针对象。它的值应该是一个地址,这个地址能够存放什么内容由下一个符号决定
- 而下一个符号是int,表示只能指向一个int内存
const int *p0:
- 离p0最近的是*,表示它是一个指针,它的值应该是一个地址,这个地址能够存放什么内容由下一个符号决定
- 而下一个符号是int,表示只能指向一个int内存
- 在下一个符号是const,表示我们不能通过p0去改变它指向的变量的值
一个准则:非常量可以转换为常量,反之不行
#include <iostream>
#include <array>
#include <algorithm>
void main()
{
const int a{ 1 }; //会优化
const int b{ 4 };
int c{ 6 };
const int *p{ &a }; //p是变量,指向一个常量
//*p = 12; //error:*p是一个常量
p = &b; //p是一个变量,可以改变指向
p = &c;
c = 11; //c仍然是一个整形变量
std::cout << c << std::endl; //11
// *p = 12; //error:*p是一个常量
std::cin.get();
}
#include <iostream>
#include <array>
#include <algorithm>
void main12()
{
const int a{ 1 }; //会优化
const int b{ 4 };
int c{ 6 };
int d{ 0 };
//int * const p{ &a }; //p是指针常量,不可以改变指向,而且是指向一个int类型的变量
int * const p{ &c };
//p = &d;
*p = 22; //改变c
std::cout << c << std::endl; //22
c = 11;
std::cout << c << std::endl; //11
std::cin.get();
}
const修饰成员函数
在一个函数声明式内,const可以和函数返回值、各参数、函数自身(如果是成员函数)产生关联
- 之所以有const成员函数,是为了确认该成员函数可以作用域const对象身上
#include <string>
#include <cstring>
#include <iostream>
class TextBlock{
public:
TextBlock(std::string text){
text = text;
}
const char * operator[](std::size_t position) const { // operator[] for const object
printf("const\n");
return reinterpret_cast<const char *>(text[position]);
}
char& operator[](std::size_t position){ // operator[] for no-const object
printf("noconst\n");
return text[position];
}
private:
std::string text;
};
int main(int argc, char* argv[])
{
TextBlock tb("Hello"); // operator[] for no-const object
tb[0] = 'x'; // ok
const TextBlock ctb("Hello"); // // operator[] for const object
// ctb[0] = 'x'; // error
return 0;
}
- 成员函数如果是const意味着什么?这里有两个概念:bitwise constness(又称physical constness)和logical constness
bitwise constness
bitwise constness主张:
- 成员函数只有在不更改对象的任何成员变量(static)除外才可以说是const。也就是说它不更改对象内的任何一个bit。
- 这种论点的好处是很容易侦测违反点:编译器只需要寻找成员变量的赋值动作即可。
- bitwise constness正是C++对常量性(constness)的定义,因此<font color=blue>const成员函数不可以更改对象内任何非静态成员变量。(而且不能调用非const函数)。为什么不能调用非const函数?因为非const函数可能修改数据成员,const成员函数是不能修改数据成员的,所以在const成员函数内只能调用const函数。
#include <iostream>
using namespace std;
class A{
private:
int i;
public:
void set(int n){ //set函数需要设置i的值,所以不能声明为const
i = n;
}
int get() const{ //get函数返回i的值,不需要对i进行修改,则可以用const修饰。防止在函数体内对i进行修改。
return i;
}
};
logical constness
如果我们就是想要在const成员函数中修改普通成员变量怎么办?----- 方法是使用C++的另一个关键字mutable释放掉非静态成员变量的bitwise constness约束。
class CTextBlock{
public:
std::size_t length() const ;
private:
char *pText;
mutable std::size_t textLength;
mutable bool lengthIsValid;
};
std::size_t CTextBlock::length() const {
if (!lengthIsValid){
textLength = std::strlen(pText);
lengthIsValid = true;
}
return textLength;
}
这叫做logical constness。logical constness主张:一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦测不出的情况下才可以这样。
在const和non-const成员函数中避免重复
mutale是个解决方法。但它不能解决所有的const相关难题。
举个例子:假设TextBlock内的operator[]不但只是返回一个引用指向某字符,也执行边界检查、志记访问信息、数据完整性检验。把这些同时放进const和non-const operator[]中:
class CTextBlock{
public:
const char & operator[](std::size_t position) const {
// ... 执行边界检查、志记访问信息、数据完整性检验
return text[position];
}
char & operator[](std::size_t position) {
// ... 执行边界检查、志记访问信息、数据完整性检验
return text[position];
}
private:
std::string text;
};
这样的写法会有很多问题:代码重复、编译时间、维护等。 你可能会将边界访问...等代码转移到另一个(private)成员函数并让两个版本的operator[]调用它,但还是重复了一些代码,比如函数调用,两次return语句等。
你真正应该做的是实现operator[]的功能一次并使用它两次。也就是说,你必须令其中一个调用另一个,即令non-const operator[]调用其const函数
class CTextBlock{
public:
const char & operator[](std::size_t position) const {
return text[position];
}
char & operator[](std::size_t position) {
return const_cast<char &>( // 将op[]返回值的const移除
static_cast<const CTextBlock&>(*this) // 为*this加上const
[position] // 调用const op[]
);
}
private:
std::string text;
};
上面代码有两个转型操作:
- 第一次为*this添加const(这里将`*this`从原始类型CTextBlock&转型为const CTextBlock&),这使得接下来调用operator[]时得以调用const版本
- 第二次从const operator[]的返回值移除const
添加const的那一次转型强迫进行了一次安全转型(将non-const对象转为const对象),所以需要用static_cast。第二次转型需要移除const,所以需要用到const_cast。
注意:
- 不要令const版本调用non-const版本以避免代码重复:const成员函数承诺绝不改变其对象,non-const成员函数却没有这样的承诺。如果在const函数内调用了non-const函数,就是冒了这样的风险:你曾经承诺不改动的那个对象改动了。
- 令non-const版本调用const版本才是安全的:non-cosnt成员函数本来就可以对其对象做任何动作,所以在其中调用一个const成员函数并不会带来风险
const 修饰函数的参数
如果参数作为输出用,不论它是什么数据类型,也不论它采用“指针传递”还是“引用传递”,都不能加const 修饰,否则该参数将失去输出功能。const 只能修饰输入参数,这又分为以下几种情况。
(1) const 用于修饰“指针传递”的参数,以防意外改动指针所指数据
例如某个 StringCopy 函数:
void StringCopy(char *strDestination, const char *strSource);
其中 strSource 是输入参数,strDestination 是输出参数。给 strSource 加上 const 修饰后,如果函数体内的语句试图改动 strSource 的内容,编译器将指出错误。
当然也有方法绕过这个限制,例如可以在函数体内重命名一个指针 char * strSource2 = strSource ; 即可改动 strSource 所指的数值。
(2)const 用于修饰“指针传递”的参数,以防意外改动指针本身
考虑如下代码:
void swap ( int * const p1 , int * const p2 )
该定义将限制在函数 swap 内修改指针 p1 和 p2 的指向。
(3)值传递的效率问题
如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加const 修饰。
例如不要将函数 void Func1 (int x) 写成 void Func1(const int x)。同理不要将函数 void Func2(MyClass a) 写成void Func2(const MyClass a)。其中 MyClass 为用户自定义的数据类型。
然而,对于非内部数据类型的参数而言,类似 void Func(MyClass a) 这样声明的函数效率会比较底,这是因为函数体内将产生MyClass类型的临时对象用于复制参数a,而临时对象的构造、复制、析构过程都将消耗时间。
为了提高效率,可以将函数声明改为void Func (MyClass &a),因为“引用传递”仅借用一下参数的别名而已,不需要产生临时对象。
但是函数void Func(MyClass & a) 存在一个缺点:“引用传递”有可能改变参数a,这是我们不期望的。
解决这个问题很容易,加const修饰即可,因此函数最终成为void Func(const MyClass &a)。
以此类推,是否应将void Func(int x) 改写为void Func(const int &x),以便提高效率?
答案是完全没有必要,因为内部数据类型的参数不存在构造、析构的过程,而复制也非常快,“值传递”和“引用传递”的效率几乎相当。所以对于内部数据类型的输入参数,不要将“值传递”的方式改为“const 引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。
const 修饰函数的返回值
也是用const来修饰返回的指针或引用,保护指针指向的内容或引用的内容不被修改,也常用于运算符重载。归根究底就是 使得函数调用表达式不能作为左值。
#include <iostream>
using namespace std;
class A {
private:
int i;
public:
A(){i=0;}
int & get(){
return i;
}
};
void main(){
A a;
cout<<a.get()<<endl; //数据成员值为0
a.get()=1; //尝试修改a对象的数据成员为1,而且是用函数调用表达式作为左值。
cout<<a.get()<<endl; //数据成员真的被改为1了,返回指针的情况也可以修改成员i的值,所以为了安全起见最好在返回值加上const,使得函数调用表达式不能作为左值
}
(1)const 修饰函数返回值(返回指针)
如果给以“指针传递”方式的函数返回值加 const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。
例如函数
const char * GetString(void);
如下语句将出现编译错误:
char *str = GetString();
正确的用法是
const char *str = GetString();
(2) const 用于修饰“返回引用”函数的返回值
如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const 修饰没有任何价值。例如把函数int GetInt(void) 写成const int GetInt(void)是没有意义的。
如果返回值不是内部数据类型,将函数MyClass GetObj(void) 改写为const Myclass & GetObj(void)的确能提高效率。但此时千万千万要小心,一定要搞清楚函数究竟是想返回一个对象的“拷贝”还是仅返回“别名”就可以了,否则程序会出错。
这里对函数返回值使用 const 的目的在于限制不能将函数调用表达式作为左值使用。例如有如下函数:
int & min ( int &i, int &j);
可以对函数调用进行赋值,因为它返回的是左值: min ( a , b )=4;
但是,如果对函数的返回值限定为 const 的,即丁奕:const int & min ( int & i, int &j );
那么,就不能对 min ( a, b ) 调用进行赋值了。
事实上,函数返回值采用“引用传递”的场合并不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。(还可以用于运算符重载)
例如:
class A
{
A & operate = (const A & other); // 赋值函数
} ;
A a, b, c; // a, b, c 为A 的对象
a = b = c; // 正常的链式赋值
(a = b) = c; // 不正常的链式赋值,但合法
如果将赋值函数的返回值加const 修饰,那么该返回值的内容不允许被改动。上例中,语句 a = b = c 仍然正确,但是语句 (a = b) = c 则是非法的。
cosntexpr与常量表达式
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。比如:字面值
一个对象(或者表达式)是不是常量表达式由它的数据类型和初始值共同决定:
- 虽然staff_size的初始值是个字面量常量,但是由于它的数据类型只是一个普通int而不是const int,所以它不是常量表达式
- 虽然sz本身是一个常量,但是它的具体值直到运行时才能获取到,所以它也不是常量表达式
我们基本上不可能分配出一个初始值是不是常量表达式,因此C++11中引入了constexpr类型以让编译器来验证变量是不是一个常量表达式。
声明为constexpr的变量一定是一个常量,而且必须由常量表达式初始化:
常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也容易得到,就将它们称为 字面量
- 算术类型、引用、指针是字面值类型
- 一个constexpr指针的初始值必须是nullptr或者0,或者是储存于某个固定地址中的对象
- 函数体内定义的变量一般来说并非存放在固定地支中,因此constexpr不能指向这样的变量
- 定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针
- 允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样有固定地址,因此,它们能用来初始化constexpr指针和constexpr引用
- 在constexpr声明中如果定义了一个指针,constexpr符号只对指针有效,与指针所指的对象无关
- 自定义类、IO库、string类想不是字面值类型,因此这些值也不能被定义成constexpr
const的作用域
const默认只在本文件内部有效,它的作用域是文件作用域,除非使用extern扩展它的作用域
- 默认情况下,全部变量的链接性是外部的,但是const全局变量的链接性是内部的。也就是说,在C++看来,全局const就像使用了static一样
面试:const作用
(1)const修饰基本数据类型:基本数据类型,修饰符 const 可以⽤在类型说明符前,也可以⽤在类型说明符后, 其结果是⼀样的。在使⽤这些常ᰁ的时候,只要不改变这些常量的值即可。
(2)const修饰指针变量和引⽤变量
阅读变量时要从右往左读,比如 int *const curErr:
- 离curErr最近的是const,表示curErr是一个常量对象,表示它的值一旦决定了就不能在改变。对象的类型由声明符的其他部分决定
- 声明符的下一个符号是*,表示curErr是一个常量指针对象。它的值应该是一个地址,这个地址能够存放什么内容由下一个符号决定
- 而下一个符号是int,表示只能指向一个int内存
const int *p0:
- 离p0最近的是*,表示它是一个指针,它的值应该是一个地址,这个地址能够存放什么内容由下一个符号决定
- 而下一个符号是int,表示只能指向一个int内存
- 在下一个符号是const,表示我们不能通过p0去改变它指向的变量的值
(3)const 修饰函数参数:
防止传入的参数代表的内容在函数体内被改变,但仅对指针和引用有意义。因为如果是按值传递,传给参数的仅仅是实参的副本,即使在函数体内改变了形参,实参也不会得到影响。如:
void fun(const int i){
i = 10;
}
在函数体内是不能改变i的值的,但是没有任何实际意义。
const修饰的函数参数是指针时,代表 在函数体内不能修改该指针所指的内容,起到保护作用,在字符串复制的函数中保证不修改源字符串的情况下,实现字符串的复制。
void fun(const char * src, char * des){ //保护源字符串不被修改,若修改src则编译出错。
strcpy(des,src);
}
void main(){
char a[10]="china";
char b[20];
fun(a,b);
cout<<b<<endl;
}
const修饰引用时:如果函数参数为用户自定义的类对象如:
void h(A a){
…………
…………
}
传递进来的参数a是实参对象的副本,要调用构造函数来构造这个副本,而且函数结束后要调用析构函数来释放这个副本,在空间和时间上都造成了浪费,所以函数参数为类对象的情况,推荐用引用。但按引用传递,造成了安全隐患,通过函数参数的引用可以修改实参的内部数据成员,所以用const来保护实参。
void h(const A & a){
…………
…………
}
(4)const修饰函数返回值
仅返回指针或者引用才有用,保护指针指向的内容或引用的内容不被修改。目的使得函数调用表达式不能作为左值
(5)const在类中
const 成员变量:
- 只在某个对象⽣命周期内是常量,⽽对于整个类⽽⾔是可以改变的。因为 类可以创建多个对象,不同的对象其 const 数据成员值可以不同。所以不能在类的声明中初始化 const 数据成员, 因为类的对象在没有创建时候,编译器不知道 const 数据成员的值是什么。const 数据成员的初始化只能在类的构 造函数的初始化列表中进⾏。
const成员函数:
- const成员函数的主要目的是防止成员函数修改对象的内容。要注意,const关键字和static关键字对于成员函数是不可以同时使用的,因为static关键字修饰静态成员函数不含有this指针,也就是不能实例化,const成员函数又必须具体到某一个函数。
- const成员函数如果一定要修改某个变量,可以用mutable进行修饰。成员变量中如果想建立在整个类中都恒定的常量,应该用类中枚举常量或者static const实现
(6)const 修饰类对象
,定义常量对象:
- 常量对象只能调常量对象函数,但不能调用非const成员函数。
- 因为对象调用成员函数时,会在形参列表的最前面(隐式的)加一个形参this,this指针默认指向调用函数的当前对象。所以,this是一个常量指针test *const,也就是说不可以修改this指针代表的地址。但是当成员函数的参数列表后加了const关键值(void print() const;),此成员函数为常ᰁ成员函数,此时它的隐式this形参为 const test * const,即不可以通过 this 指针来改变指向对象的值。
class myClass{
public:
void cc() const {
}
void add(){
}
};
int main()
{
const myClass *m1 = new myClass;
m1->cc();
myClass * m2 = new myClass;
m2->add();
m2->cc();
return 0;
}