Const 的各种用法
此文参考了下面几篇文章并做了一些修改,首先对原作者表示感谢!!
https://blog.csdn.net/gamekit/article/details/53838680
https://blog.csdn.net/z1026544682/article/details/52159877
https://blog.csdn.net/sinat_27456831/article/details/50969140
const 是constant 的缩写,“恒定不变”的意思。被const 修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。所以很多C++程序设计书籍建议:“Use const whenever you need”。
一、修饰全局或局部变量
1. 修饰指针,防止指针被修改
1.1.const指针不能取地址,不能传递给普通指针。
const ClassA* a;
ClassA** b = &a; //编译错误,不能对const指针取地址
ClassA* c = a; //编译错误,不能将const指针转普通指针
1.2. const指针与指向const的指针
当使用带有const的指针时其实有两种意思。一种指的是你不能修改指针本身的内容,另一种指的是你不能修改指针指向的内容。
指向const的指针,它的意思是指针指向的内容是不能被修改的。它有两种写法。
const int* p; (推荐)
int const* p;
再说const指针,它的意思是指针本身的值是不能被修改的。它只有一种写法
int* const p=一个地址;//因为指针本身的值是不能被修改的所以它必须被初始化
举例说明:
int a=3;
int b;
/*定义指向const的指针(指针指向的内容不能被修改)*/
const int* p1;
int const* p2;
/*定义const指针(由于指针本身的值不能改变所以必须得初始化)*/
int* const p3=&a;
/*指针本身和它指向的内容都是不能被改变的所以也得初始化*/
const int* const p4=&a;
int const* const p5=&b;
p1=p2=&a; //正确
*p1=*p2=8; //编译错误(指针指向的内容不能被修
*p3=5; //正确
p3=p1; //编译错误(指针本身的值不能改变)
p4=p5;//编译错误 (指针本身和它指向的内容都是不能被改变)
*p4=*p5=4; //编译错误(指针本身和它指向的内容都是不能被改变)
2. 修饰普通类型,说明此变量不能修改,也不能取地址
const int a;
a++; //编译错误
int* c= &a; //编译错误,不能取地址,否则就具备了改a的能力
int b = a; //可以编译通过,不能改a的值,可以更改b的值
一般情况下建议用const声明全局变量,对比#define好处有:
- 便于进行类型检查
const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误 - 防止意外的修改,增强程序的健壮性。
- 可以节省空间,避免不必要的内存分配
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝 - 提高了效率
编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高
3. 修饰引用类型,此引用的值不能被修改。
二、修饰函数参数,与修饰变量类似
1.需要注意:
修饰引用类型参数时,虽然参数的值不能被修改看似失去意义,但是传递const引用时,可以起到不copy对象又能保护对象的目的
void test(const int& a)
{
a = 2; //编译错误,不能修改const引用类型的值
}
void test(const ClassA& a) //传递的时候,不需要copy一个新的ClassA,又能保护a
{
}
对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const 引用传递”,目的是提高效率。例如将void Func(A a) 改为void Func(const A &a)。
但是对于内部数据类型的输入参数,不要将“值传递”的方式改为“const 引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void Func(int x) 不应该改为void Func(const int &x)。
2.2 形参是const的引用或指针时,必须用const修饰
#include <iostream>
#include <string>
using namespace std;
void print_str( string & s)
{
cout<<s<<endl;
}
int main()
{
print_str("hello world"); //编译错误,print_str()中,s未用const修饰,可以更改。
return 0;
}
普通形参加不加const限定符对实参没有影响,引用形参和指针形参前面没有const限定符时,实参必须是非const的,而前面有const限定符时对实参也没有什么影响。
三、用const修饰函数返回值,说明函数的返回类型是const的,功能类似于函数参数
此时返回值只能赋值给const修饰的同类型变量
const char* test()
{
......
}
char* str = test(); //错误
const char* str = test(); //正确
如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const 修饰没有任何价值。
例如不要把函数int GetInt(void) 写成const int GetInt(void)。
同理不要把函数A GetA(void) 写成const A GetA(void),其中A 为用户自定义的数据类型。
如果返回值不是内部数据类型,将函数A GetA(void) 改写为const A & GetA(void)的确能提高效率。但此时千万千万要小心,一定要搞清楚函数究竟是想返回一个对象的“拷贝”还是仅返回“别名”就可以了,否则程序会出错。
函数返回值采用“引用传递”的场合并不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达
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 则是非法的。
四、用const修饰函数,说明函数不会修改成员变量的值
1. 任何不会修改数据成员的函数都应该声明为const 类型。
如果在编写const 成员函数时,不慎修改了数据成员,或者调用了其它非const 成员函数,编译器将指出错误,这无疑会提高程序的健壮性。以下程序中,类stack 的成员函数GetCount 仅用于计数,从逻辑上讲GetCount 应当为const 函数。编译器将指出GetCount 函数中的错误。
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;
}
2. const成员函数存在的另一个意义在于它能被const常对象调用。
我们都知道,在定义一个对象或者一个变量时,如果在类型前加一个const,如const int x;,则表示定义的量为一个常量,它的值不能被修改。但是创建的对象却可以调用成员函数,调用的成员函数很有可能改变对象的值,比如下面这段程序:
const list myList;
mylist.DeleteNode(3);//错误,DeleteNode是非const成员函数,删除一个节点会改变length的值
mylist.GetLength();//正确,Get length()未改变任何值,被声明为const成员函数
3. mutable类型
然而,有些时候,我们却必须要让const函数具有修改某个成员数据值的能力。比如一些内部的状态量,对外部用户无所谓,但是对整个对象的运行却大有用处,如支持缓存的技术。遇到这种问题,我们可以把一个成员数据定义为mutable(多变的),它表示这个成员变量可以被const成员函数修改却不违法。比如下面定义了一个is_valid类成员:
class List
{
Private:
...
mutable bool is_valid;
...
Public:
bool checkList() const
{
if(length >=1)
return is_valid = true;
else
return is_valid = false; // 正确
};
}
这样,即使像CheckList这样的const成员函数修改它也是合法的。
但需要注意的时,不可滥用mutabe描述符,如果在某个类中只有少数一部分是被允许const常量函数修改的,使用mutable是再合适不过的。如果大部分数据都定义为mutable,那么最好将这些需要修改的数据放入另一个独立的对象里,并间接地访问它。
3. 在相同的函数参数及相同的名字的情况下,const函数与非const函数可以构成重载函数,但是const成员函数不能改变任何的非静态变量
- const对象默认调用const成员函数,非const对象默认调用非const成员函数;
若非const对象想调用const成员函数,则需要显示的转化,例如(const Student&)obj.getAge();
若const对象想调用非const成员函数,同理进行强制类型转换const_cast < Student&>(constObj).getAge();(注意constObj一定要加括号)
当类中只有一种函数存在的情况
- 非const对象可以调用const成员函数或者非const成员函数
- const对象只能调用const成员函数,若直接调用非const成员函数编译器会报错。
const 成员
函数的声明看起来怪怪的:const 关键字只能放在函数声明的尾部,大概是因为其它地方都已经被占用了。
五、关于Const函数的几点规则:
5.1 const使用规则
a. const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.
b. const对象的成员是不可修改的,然而const对象通过指针维护的对象却是可以修改的.
c. const成员函数不可以修改对象的数据,不管对象是否具有const性质.它在编译时,以是否修改成员数据为依据,进行检查.
e. 然而加上mutable修饰符的数据成员,对于任何情况下通过任何手段都可修改,自然此时的const成员函数是可以修改它的
5.2 几点建议:
1.要大胆的使用const,这将给你带来无尽的益处,但前提是你必须搞清楚原委;
2.要避免最一般的赋值操作错误,如将const变量赋值;
3.在参数中使用const应该使用引用或指针,而不是一般的对象实例,原因同上;
4.const在成员函数中的三种用法(参数、返回值、函数)要很好的使用;
5.不要轻易的将函数的返回值类型定为const;
6.除了重载操作符外一般不要将返回值类型定为对某个对象的const引用;
7.任何不会修改数据成员的函数都应该声明为const 类型。
5.3 补充重要说明
- 类内部的常量限制:使用这种类内部的初始化语法的时候,常量必须是被一个常量表达式初始化的整型或枚举类型,而且必须是static和const形式。
- 如何初始化类内部的常量:一种方法就是static 和 const 并用,在外部初始化,例如:
class A {
public:
A() {}
private:
static const int i; //注意必须是静态的!
};
const int A::i=3;
另一个很常见的方法就是初始化列表:
class A {
public:
A(inti=0):test(i) {}
private:
const int i;
};
还有一种方式就是在外部初始化。
3. 如果在非const成员函数中,this指针只是一个类类型的;如果在const成员函数中,this指针是一个const类类型的;如果在volatile成员函数中,this指针就是一个volatile类类型的。
4. new返回的指针必须是const类型的。
六、将Const类型转化为非Const类型的方法
采用const_cast 进行转换。
用法:const_cast (expression)
该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。
· 常量指针被转化成非常量指针,并且仍然指向原来的对象;
· 常量引用被转换成非常量引用,并且仍然指向原来的对象;
· 常量对象被转换成非常量对象。