文章目录
花絮
- C++ 中的 const 关键字的用法非常灵活,而使用 const 将大大改善程序的健壮性
- const 是 C++ 中常见的类型修饰符,常类型是指使用类型修饰符 const 说明的类型,常类型的变量或对象的值是不能被更新的
一、const 作用
1. 定义 const 常量
const int max = 100;
2. 便于进行类型检查
const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型检查,并且在字符替换时可能产生意料不到的错误
void f(const int i){……}
//对传入的参数进行类型检查,不匹配进行提示
3. 可以保护被修饰的东西
防止意外的修改,增强程序的健壮性
void f(const int i)
{
i = 10; // error
//如果在函数内部修改了i,编译器会报错
}
4. 可以很方便进行参数的调整和修改
同宏定义一样,可以做到不变则已,一变都变
5. 为函数重载提供了一个参考
class A{
void f(); //一个函数
void f() const{}; //上一个函数的重载
};
6. 可以节省空间,避开不必要的内存分配
const 定义常量从汇编角度来看,只是给出了对应的内存地址,而不像 #define 一样给出的是立即数。所以,const 定义的常量在程序运行过程中只有一份拷贝,而 #define 定义的常量在内存中有若干份拷贝
#define PI 3.1415926 //常宏量
const double pi 3.1415926 //此时并未将 pi 放入内存中
……
double i = pi; //此时为 pi 分配内存,以后不再分配
double I = PI; //编译期间进行宏替换,分配内存
double j = pi; //没有分配内存
double J = PI; //再一次进行宏替换,又一次分配内存
7. 提高效率
编译器通常不为普通 const 常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它效率也很高
二、const 的使用
1. 定义常量
(1) const 修饰变量,以下两种定义形式在本质上是一样的。它的含义是:const 修饰的类型为 T 的变量 val,它是不可变的。
T const val = value;
const T val = value;
(2) 将 const 改为外部连接,作用扩大到全局,编译时会分配内存,并且可以不进行初始化,仅仅作为声明,编译器认为在程序其他地方进行了定义。
extend const T val = value;
2. 指针使用 const
(1) 指针本身是常量不可变
如果 const 的位置在
*
的后面,表示指针本身是常量,但是指针指向的变量的值是可以改变的。简单来说,假如指针 p 指向了 变量 a,那么 p 就只能指向 a,不能再指向其他变量了,但是 a 不是常量,a 的值是可以改变的
int a = 10;
int b = 20;
int* const p = &a;
p = &b; //error: assignment of read-only variable 'p'
*p = 30; // 此时 a = 30
(2) 指针所指向的内容是常量不可变
如果你了解 (1),那么这个 const 的效果与 (1) 中 const 的效果刚好相反。如果 const 的位置在
*
的前面,表示指针指向是内容常量,但是指针本身是可变的。简单来说,假如指针 p 指向了 变量 a,那么 变量 a 就变成常量了,a 中的内容就不能被修改了,但是 p 本身可以改变,p 可以指向其他变量。
int a = 10;
int b = 20;
const int* p = &a;
*p = 30; //error: assignment of read-only location '* p'
p = &b; //correct: p 指向了b,*p = 20
(3) 两者都不可变
const int* const p;
(4) 总结
- 如果 const 位于
*
的左侧,则 const 就是用来修饰指针所指向的变量,即指针指向为常量 - 如果 const 位于
*
的右侧,则 const 就是修饰本身,即指针本身是常量
3. 函数使用 const
(1) const 修饰函数参数
- 传递过来的参数在函数内部不可改变(无意义,因为 var 本身就是形参)
void function(const int var);
- 参数指针所指内容为常量不可变
void function(const int* p);
- 参数指针本身是常量不可变
void function(int* const p);
- 参数为引用,为了增加效率同时防止修改,修饰引用参数时
void function(const class& A); //引用参数在函数内不可改变
void function(const int &a); //引用参数在函数内为常量不可变
示例一:引用参数在函数内不可改变
示例二:引用参数在函数内为常量不可变
这样的一个 const 引用传递和最普通的函数按值传递的效果是一模一样的,它禁止对引用的对象的一切修改,唯一不同的是按值传递会建立一个类对象的副本,然后传递过去,而引用是直接传递地址,所以这种传递比按值传递更有效。另外只有引用的 const 传递可以传递一个临时对象,因为临时对象都是 const 属性,且是不可见的,它短时间存在一个局部域中,所以不能使用指针,只有引用的 const 传递才能捕捉到这个家伙。
(2) const 修饰函数返回值
const 修饰函数返回值其实用的并不多,它的含义和 const 修饰普通变量以及指针的含义基本相同
1. const int func1(); //这个其实没多大意义,因为参数返回就是赋值
2. const int* func2(); //调用时:const int* p = func2(); 可以把 func2() 当作一个变量,即指针内容不可变
3. int* const func3(); //调用时:int* const p = func3(); 以把 func2() 当作一个变量,即指针本身不可变
一般情况下,函数的返回值为某个对象时,如果将其声明为 const 时,多用于操作符的重载。通常,不建议用 const 修饰函数的返回值类型为某个对象或者某个对象的引用的情况。
原因如下:如果返回值为某个对象为 const(比如:const A test = A; )或某个对象的引用为 const(const A& test = A;),则返回值具有 const 属性,则返回实例只能访问类 A 中的公有或保护数据成员和 const 成员函数,并且不允许对其进行赋值操作,这在一般情况下很少用到。
4. 类相关 const
(1) const 修饰成员变量
const 修饰类的成员变量,表示成员常量,不能被修改,同时它也只能在初始化列表中赋值
class A{
const int a; //成员常量不能被修改
……
A(int _a) : a(_a){} //只能在初始化列表中赋值
};
【举个栗子】
(2) const 修饰成员函数
- const 修饰类的成员函数,则该成员函数不能修改类中任何非 const 成员函数,一般写在函数的最后来修饰
class A{
void func() const{} //常成员函数,它不能改变对象的成员变量,也不能调用类中任何非 const 成员函数
};
【举个栗子】
- 对于 const 类对象/指针/引用,只能调用类的 const 成员函数。因此,const 修饰成员函数的最重要的作用就是限制对于 const 对象的使用
- const 成员函数不被允许修改它所在对象的任何一个数据成员
对于第 1 点,const 成员函数不被允许修改它所在对象的任何一个数据成员。这里有一个注意点,如果类中存在指针类型的数据即便是 const 函数也只能保证不修改该指针的值,并不能保证不能修改该指针指向的内容
【举个栗子】
class Name {
public:
void setName(const string &s) const;
private:
char *m_sName;
};
void Name::setName(const string &s) const {
// m_sName = s.c_str(); // 错误!不能修改m_sName;
for (int i = 0; i < s.size(); ++i)
m_sName[i] = s[i]; // 不好的风格,但不是错误的
}
m_sName 不能通过setName()函数修改,但是 setName函数可以修改其所指向的字符。
- const 成员函数能够访问对象的 const 成员,而其他函数不可以。
这个结论不严谨。const function 的本质,其实就是会在函数内部,把当前调用者的 this 指针转换成 const this,以保证当前函数对数据成员只有读的权限,没有写的权限。如果在非 const 成员函数里只访问了 const 成员变量,但没有进行修改操作,这时候是可以通过编译的。
【举个栗子】
class A
{
private:
const int a;
public:
A(int _a) : a(_a){}
int getA(){ cout << a; } // ok,可以通过
int setA(){ a = 30; } //error
};
(3) const 修饰类对象/对象指针/对象引用
- const 修饰类对象表示该对象为常量对象,其中的任何成员都不能被修改,对于对象指针和对象引用也是一样
- const 修饰的对象,该对象的任何非 const 成员函数都不能被调用,因为任何非 const 成员函数会有修改成员变量的企图
【举个栗子】
class A
{
private:
const int a;
int b;
public:
A(int _a, int _b) : a(_a), b(_b){}
int getA() const{ return a; }
int getB(){return b; }
void setB(){ b= 30; }
};
int main ()
{
const A a(20, 10);
//a.setB(); //error
//a.getA(); //correct,const 对象只能调用const成员函数
a.getB(); //error,const 对象不能调用非const成员函数
return 0;
}
[结论]
- const对象可以访问const成员函数,但是不能访问非const成员函数
- 非const对象可以调用const 成员函数和非const成员函数
- const成员函数可以调用其他const成员函数,不能调用其他非const成员函数
- 非const成员函数内可以调用其他const成员函数和非const成员函数
三、将 const 类型转换成非 const 类型
【方法】:采用const_cast
进行转换
【用法】:const_cast <type_id> (expression)
【作用】:该运算符用来修改类型的 const 或 volatile 属性。除了const或volatile 修饰之外,type_id 和 expression 类型是一样的。
- 常量指针被转化成非常量指针,并且仍然指向原来的对象
- 常量引用被转换成非常量引用,并且仍然指向原来的对象
- 常量对象被转换成非常量对象
四、使用 const 的一些建议
- 要大胆的使用const,这将给你带来无尽的益处,但前提是你必须搞清楚原委;
- 要避免最一般的赋值操作错误,如将const变量赋值,具体可见思考题;
- 在参数中使用const应该使用引用或指针,而不是一般的对象实例,原因同上;
- const在成员函数中的三种用法(参数、返回值、函数)要很好的使用;
- 不要轻易的将函数的返回值类型定为const;
- 除了重载操作符外一般不要将返回值类型定为对某个对象的const引用;
- 任何不会修改数据成员的函数都应该声明为const 类型。
五、补充重要说明
- 类内部的常量限制:使用这种类内部的初始化语法的时候,常量必须是被一个常量表达式
- 初始化的整型或枚举类型,而且必须是static和const形式。
- 如何初始化类内部的常量:一种方法就是static 和 const 并用,在外部初始化,例如:
class A { public: A() {} private: static const int i; file://注意必须是静态的! };
const int A::i=3;
另一个很常见的方法就是初始化列表:class A { public: A(int i=0):test(i) {} private: const int i; };
还有一种方式就是在外部初始化,- 如果在非const成员函数中,this指针只是一个类类型的;如果在const成员函数中,this指针是一个const类类型的;如果在volatile成员函数中,this指针就是一个
volatile类类型的。- new返回的指针必须是const类型的。
参考链接:关于C++ const 的全面总结