之前对const这个关键字也有一些了解,但是也就停留在一知半解的状态,面试的时候别人一问,才知道自己孤陋寡闻了。今天特地总结了一下const的各种用法。也许const关键字是在c++中用途最多最广的关键字了。
const顾名思义就是为了定义常量而设计的,即不想改变数据的值。const可以修饰变量,指针,对象,函数参数,函数返回值,以及成员函数本身。用const修饰的各种对象都有哪些意义,哪些表现?
1、const修饰基本数据类型(布尔型bool、字符型char、整型int、浮点型float、双精度浮点型double)
当const修饰这些基本数据类型时,定义的时候必须初始化,并且const关键字和基本数据类型的先后顺序是可随意的。
const bool b = true;
char const c = 'a';
如果没有初始化,编译器会提示错误:
const int i;
提示错误为:const.cpp:33:12: 错误:未初始化的常量‘i’ [-fpermissive]
使用const修饰的基本数据类型,相当于常量,是不允许修改的,只能读取。
2、const修饰指针
const修饰指针的时候就有点难以理解了,这就是指针常量和常量指针的区别
(1)指针常量
const int *p; //p是指针常量
int const *p1; //p1也是指针常量
int i = 9, j = 10;
p = &i; //p在定义的时候可以不初始化,可以之后再赋值
p1 = &j; //p1的值是可以被改变的
定义指针常量,只要记住const关键字在星号*的前面,就是指针常量,const关键字和数据类型的先后顺序不重要,都可以。定义指针常量的时候,指针的值是可以不用初始化的,这点跟const修饰基本数据类型时在定义的时候必须初始化是不同的。指针常量可以在定义之后再赋值。
指针常量的作用是告诉编译器不能使用这个指针去改变所指向的内存地址的值。即指针所指的地址的值是常量,不能被改变。但是指针本身的值是可以被改变的。从这个道理讲,这里的const修饰的是指针所指向的内容,内容是常量。
(2)常量指针
int i = 10, j = 9;
int* const p = &i; //p为常量指针,定义的时候必须初始化
*p = 1; //正确,可以通过常量指针去改变所指地址的值
//p = &j //错误,不能改变指针的值,这个指针是常量,在定义的时候就必须初始化,初始化后就不能更改
常量指针的定义是const关键字放在星号(*) 后面,变量的前面。常量指针在定义的时候也必须初始化,这个跟const修饰基本数据类型的情形是一样的。这个也比较好理解,因为定义过后就不允许改变了,所有初始的时候必须指定一个有意义的值,不允许指向随机的地址,这样也没法使用。
常量指针的作用是告诉编译器,这个指针是一个常量,在初始化的时候就指向固定的内存地址,不允许再改变它指向其他地方。从这个角度来说,这里的const是修饰指针本身的。指针本身是常量,不允许改变,但是可以使用它来改变它所指向的地址的内容。
(3)常指针常量
讲了前面的指针常量和常量指针,不知道大家有没有想过,指针常量和常量指针一起用,定义一个指针,这个指针本身是常量,它所指向的内存地址的值也不能被改变?实际上是可以的,这个就是常指针常量,定义的时候也必须被初始化。定义如下:
int i = 9;
const int* const p = &i; //常指针常量
3、const修饰函数的参数
void printf(const int i, const char *p)
{
cout << i << " " << p << end;
}
const修饰函数参数,目的也是很明确,就是不想改变参数的值,一般会更多针对的是传递引用和指针的情况,设计者不想使用者在函数内部通过指针或者引用修改对象的内容。
4、const修饰对象
class A
{
public:
A():i(1)
{}
public:
int i;
};
int main(int argc, char** argv)
{
const A a;
cout << a.i << endl; //正确,const修饰的对象,可以读取(访问)对象的成员
//a.i = 100; //错误,不能修改const修改的对象
const A *p = new A; //正确,const修饰的对象,可以读取(访问)对象的成员
cout << p->i << endl; //错误,不能修改const修改的对象
//p->i = 100;
delete p;
return 0;
}
const 修饰一个类对象(在栈上创建)或者修饰一个类对象的指针(例如用new操作符在堆中创建),情形也是类似的。就是只能读取对象的成员变量的值,而不能修改成员变量的值。并且还有一个重要的特性,就是只能调用类的const成员函数,而不能调用类的非const成员函数。因为只有const成员函数才承诺不修改对象的内容。这一点我们将很快在下面一小节中讲到。
5、const修饰普通成员函数本身
(1)实现原理
注意这里特别强调修饰的是普通成员函数,而不是全局函数,也不包括静态成员函数。因为const是不能修饰全局函数和静态成员函数的。为什么呢,这还得从此处const修饰的对象讲起。const修饰普通成员函数,主要是修饰this指针的.。大家学c++的都知道,每个类对象内部都有一个默认的指针,this指针,是指向对象本身的,实际上c++的内部实现也是通过这个this指针来访问对象自己的成员变量的。并且我们知道,这个指针是一个常量,不能被改变的。但是我们可以通过这个指针去访问和修改对象的内容。这个不就是常量指针的定义吗?我们应该能想到,普通成员函数内的this指针,是如下定义的:
ClassName* const this;
在我们调用普通成员函数的时候,会传入这个this指针,这也就是为什么,我们在成员函数里可以访问成员变量。都是隐含通过这个指针来访问的。如果通过函数参数this是安照以上的定义,我们的确就能通过this指针来访问修改它所指向的内容。这就是普通函数中的this。
const修饰成员函数,结果会怎样呢?const修饰成员函数,作用也是防止在函数内部修改对象的内容,只能访问,不能修改。但是怎么实现的?还是通过const关键字作用在this指针上。const修饰的成员函数,函数参数this是如下定义的:
const ClassName* const this;
把this指针定义为常指针常量,自然就不能利用它来修改对象的内容了,只能访问。这一切就豁然开朗了。const成员函数的定义如下:
//const.h
class A
{
public:
A():i(0)
{}
void print() const; //常成员函数定义
public:
int i;
};
//const.cpp
void A::print() const //在函数实现中,const关键字也不能省略
{
cout << i << endl;
}
const修饰成员函数的时候,const关键字出现在函数参数列表的后面,并且在函数定义和实现中都要加。
回到开头的问题,const不会修饰全局函数和静态成员函数,因为这两种函数都没有this指针。
另外,const成员函数也不能调用非const成员函数,因为如果调用了非const成员函数,也会有对对象被修改的风险
(2)关于const成员函数和非const成员函数构成的重载
在没有仔细研究const之前,在我的认识里,构成重载的条件如下:
a、函数名相同
b、函数签名不同(函数参数类型不同,顺序不同,个数不同)
但是在了解了const的用法之后,在函数名相同,函数签名也相同,只有是否为const成员函数这点不同,也可以构成函数的重载
#include<iostream>
using namespace std;
class A
{
public:
A():i(0){}
void Print() const //const成员函数
{
cout << i << "const print" << endl;
}
void Print() //非const成员函数
{
cout << i << "none const print" << endl;
}
void out(const int i, const char *p)
{
cout << i << p << endl;
}
public:
int i;
};
int main(int argc, char** argv)
{
A a;
const A b;
a.Print(); //调用非const成员函数
b.Print(); //调用const成员函数
const A& r = a;
r.Print(); //通过转化,将非const对象转化为const对象,调用的是const成员函数
const_cast<A&>(b).Print(); //通过转化,将const对象转化为非const对象,调用的是非const成员函数
return 0;
}
如上,编译通过,两个Print函数构成重载,并且,const修饰的对象,只能调用类的const成员函数,而不能调用类的非const成员函数。如果是非const对象,优先调用非const的Printf函数,如果没有非const的Print函数,才会去调用const的print函数。如果const对象想要调用非const成员函数,非const对象想要调用const函数,则都需要进行相应的转换才行。
6、const修饰函数返回值
const修饰函数返回值,主要是针对对返回的引用或者指针,防止通过返回的指针或者引用来修改对象的内容,归根究底是使函数调用的表达式不能为左值。也经常用在操作符重载的返回值中。
class A
{
public:
A():i(1){}
public:
int& get(){ return i;}
private:
int i;
}
int main(int argc, char** argv)
{
A a;
a.get() = 10; //这样是没有错的,因为返回的是i的引用,并且没有声明为const,这就可以利用这个引用来给变量赋值
cout << a.get() << endl;//输出10
}
总之const的用处很多,也不知道我总结的全了没有,如果日后发现还有新的用途,再继续添加吧。
总结参考了下面几篇博文:
https://blog.youkuaiyun.com/SwordArcher/article/details/78740890
https://blog.youkuaiyun.com/u014630623/article/details/51290954
https://blog.youkuaiyun.com/chen956/article/details/50596109