C++那些细节--const与函数

来源:互联网 发布:linux软件包管理 编辑:程序博客网 时间:2024/05/18 03:55

一.简介

const是C++中灰常灰常强大的关键字。const--constant的缩写,简单理解就是不变的意思,它的所有功能都是限定我们修改某个变量。虽然我们最熟悉的是const常量,但是这并不是const最有用的部分。const和函数碰撞在一起的时候,才能激发出const真正能力。

下面看一下const修饰函数本身,修饰函数参数,修饰函数返回值时,都能带来哪些效果。

二.const修饰函数本身


const修饰函数本身,这个是最灵活,也是最复杂,涉及到点最多的地方!!

1.const修饰函数的意义


C++中成员函数后面经常看到const关键字修饰,这是一种很好的习惯吗,在我们的函数不需要修改对象的内容时,果断给它加上const修饰。《Effective C++》中说过,尽量使用const,可以减少很多不必要的麻烦。那么const修饰成员函数究竟有什么作用呢?

a.最主要的就是防止成员函数修改成员变量(非静态成员变量):试想一下,本来我想通过这个函数来输出一下log的,但是一不小心,将判断成员变量的==写成了=,那么如果这个函数是const的,编译的时候会立刻报错的,而如果这个函数不是const的,那么这个函数就会将成员变量修改,(VS貌似会有warning),这将是一个灰常难以发现的bug。所以,当一个函数不应该修改成员变量的时候,果断将其设置为const的。
b.成员函数是不是用const修饰的,也可以很方便的帮助我们理解代码:让我们知道哪些内容可以修改对象成员的变量,哪些不可以,进而更好的帮我们理对象的功能。
c.const成员函数使我们可以操作const对象:因为非const成员函数是不能被const对象调用的,只有被声明为const的成员函数才能被一个const类对象调用。这也是C++通过指针或者引用传递参数的基础之一。

2.const修饰函数的使用方法


其实使用const修饰函数很简单,只需要在函数后面加上一个const即可。不过有一点要注意,当函数定义和声明分开的时候,如果声明加了const,那么定义的时候也需要加上const,这一点与virtual关键字不同。
简单的一个例子:
// C++Test.cpp : 定义控制台应用程序的入口点。//#include "stdafx.h"#include <iostream>using namespace std;class Base{private:int num;public:Base(int n){num = n;}virtual void Show() const ;};void Base::Show() const{cout<<"base show: "<<num<<endl;} int _tmain(int argc, _TCHAR* argv[]){Base* base1 = new Base(1);base1->Show();system("pause");return 0;}
结果:
base show: 1
请按任意键继续. . .

3.加const和不加const表示不同的函数


我们还是用virtual关键字作为比较,当这个类被继承之后,原本为virtual的函数,在子类中即使不写virtual,它仍然是virtual的。但是,我们如果不写const的话,会发生什么呢?这可是一个灰常有意思的实验,瞪大眼睛看好啦!!!
// C++Test.cpp : 定义控制台应用程序的入口点。//#include "stdafx.h"#include <iostream>using namespace std;//基类,包含一个virtual函数,const类型class Base{protected:int num;public:Base(int n){num = n;}virtual void Show() const ;};void Base::Show() const{cout<<"base show: "<<num<<endl;}//派生类,派生了Show,但是!!!忘记写const了class Derived : public Base{public:Derived(int n) : Base(n){}void Show();};void Derived::Show() {cout<<"derived show: "<<num<<endl;} int _tmain(int argc, _TCHAR* argv[]){//基类调用基类,base show木有问题Base* base1 = new Base(1);base1->Show();//基类掉子类,应该触发多态,derived show会被调用吗?Base* derived1 = new Derived(2);derived1->Show();system("pause");return 0;}
结果:
base show: 1
base show: 2
请按任意键继续. . .

virtual函数,正常应该触发多态的,但是!!并没有出现我们想要的结果!!!derived1调用的仍然是基类的Show方法。为什么会出现这种情况呢?原因很简单,virtual并没有失效,而是因为我们根本就没有覆写Show()const函数!!!换句话说,我们在子类又创建了一个非const版本的Show函数!而我们调用的时候,直接调用了基类的Show,跟virtual没有半毛钱关系。

virtual等其他关键字,修饰之后,函数仍然是原来的函数,定义一个不带这个关键字的函数就会报错说重定义。但是,const关键字和它们不一样,有const和没有const的函数是两个不同的函数,它们是可以共存的!!
上面的例子主要就是说明const版本的成员函数和非const版本的成员函数并不相同,说实话绕的有点儿远,不过这个确实是一个非常容易犯的错误,而又非常非常难以发现!本人亲测,在VS默认警告等级上,并没有给出警告。将警告等级调高之后,编译器才给出了警告:
warning C4263: “void Derived::Show(void)”: 成员函数不重写任何基类虚拟成员函数
warning C4264: “void Base::Show(void) const”: 不重写基“Base”中的虚拟成员函数;函数被隐藏。

我们有时候编程的时候并不看警告的,所以,有时候警告可以帮我们发现一些平时注意不到的问题,适当提高警告等级,也是很有用的。
推荐一个好的方法,C++11的一个新特性,个人很喜欢。在我们要覆写一个函数之后,我们最好在后面加上override关键字,这样如果不能override,那么编译器就会报错,提早发现错误总是好的!比如上面那个函数:
//派生类,派生了Show,但是!!!忘记写const了class Derived : public Base{public:Derived(int n) : Base(n){}void Show() override;};void Derived::Show() {cout<<"derived show: "<<num<<endl;}

编译的时候,就会报错:error C3668: “Derived::Show”: 包含重写说明符“override”的方法没有重写任何基类方法。
因为基类中根本没有void Show() cosnt这个方法!

我们将派生类的Show后面加上const修饰,再编译一下,结果就对啦:
//派生类,派生了Showclass Derived : public Base{public:Derived(int n) : Base(n){}void Show() const override;};void Derived::Show() const {cout<<"derived show: "<<num<<endl;}

结果:
base show: 1
derived show: 2
请按任意键继续. . .


4.同时有const和非const函数的时候,调用哪一个


这个问题这么问有点儿牵强,因为我们已经知道了有const修饰和没有const修饰的两个函数是不同的函数,那么很明显,有const属性的只能调用const修饰的函数,没有const属性的就只能调用非const的成员函数了。但是,我们又没有给出函数的参数,函数怎么知道我们要用哪一个呢?这里就是涉及C++底层的一点东东了,虽然听起来很高大上,不过就是传说中的this指针。成员函数调用时是会有这样的一个隐形的参数的,即对象本身的指针--this指针。我们看一个简单的例子:
class Base{protected:int num;public:Base(int n){num = n;}virtual void Show();};void Base::Show(){cout<<"base show non-const: "<<num<<endl;}
这里,我们没有给出const版本的show函数。然后我们使用一个const的对象来调用这个Show函数:
const Base* base = new Base(3);base->Show();
然后,我们编译一下,果然出错了:
C++Test.cpp(73): error C2662: “Base::Show”: 不能将“this”指针从“const Base”转换为“Base &”
什么意思呢?
就是说我们调用的时候,对象传给函数的是一个带有const的this指针,而我们的函数接受的this指针并不需要带有const,所以参数不匹配。

不过上面的例子太牵强了,我们当然不会这样写一个const对象,真正const对象使用得多的时候是在函数参数中!!!
class Base{protected:int num;public:Base(int n){num = n;}virtual void Show() const ;virtual void Show();};void Base::Show() const{cout<<"base show const: "<<num<<endl;}void Base::Show(){cout<<"base show non-const: "<<num<<endl;}//派生类,派生了Showclass Derived : public Base{public:Derived(int n) : Base(n){}void Show();void Show() const;};void Derived::Show() { cout<<"derived show non-const: "<<num<<endl;}void Derived::Show() const{cout<<"derived show const: "<<num<<endl;}void TestFunc(const Base& obj){obj.Show();}int _tmain(int argc, _TCHAR* argv[]){Base base(1);Derived derived(2);TestFunc(base);TestFunc(derived);system("pause");return 0;}
结果:
base show const: 1
derived show const: 2
请按任意键继续. . .

如果我们把TestFunc的参数改为非const的:
void TestFunc(Base& obj){obj.Show();}
结果就变成了:
base show non-const: 1
derived show non-const: 2
请按任意键继续. . .


我们看到,如果TestFunc的参数类型是非const的,那么obj就是非const的,它调用的就是非const版本的Show函数,而如果TestFunc接受的参数是一个const类型的参数,所以内部的obj就是const类型的,进而调用了const类型的Show函数。但是,我们给TestFunc函数的参数并不是const的呀?这是为什么呢?答案是非const类型的参数发生了隐式转换,变成了const类型的。(注意,反过来是不会转换的,即const类型是不会隐式转化为非const类型的)

5.const只能保证非静态成员变量不能被修改


const修饰函数的功能是防止函数修改对象的成员变量的值,但是这个成员变量如果是static的,则并不能接受const的保护。看一个例子:
// C++Test.cpp : 定义控制台应用程序的入口点。//#include "stdafx.h"#include <iostream>using namespace std;class Base{protected:int num;//添加一个static成员变量static int s_num;public:Base(int n){num = n;}virtual void Show() const ;};//static变量在类外进行初始化int Base::s_num = 0;//Show中尝试对s_num进行修改,并没有报错//对num进行修改则会报错void Base::Show() const{//num = 3; 非静态成员变量不能被修改s_num = 2;//静态的成员变量可以被修改cout<<"base show: "<<num<<endl;cout<<"static mem is: "<<s_num<<endl;} int _tmain(int argc, _TCHAR* argv[]){//基类调用基类,base show木有问题Base* base1 = new Base(1);base1->Show();system("pause");return 0;}
结果:
base show: 1
static mem is: 2
请按任意键继续. . .

这里,我们看到,如果修改非静态成员变量,会报错,但是修改静态成员变量时,并没有报错。程序运行之后,静态成员变量也确实被修改了!所以我们看出,const对静态成员变量并没有起到保护作用。

6.const函数中不能修改非静态成员变量,const函数中不能调用非const函数


不能修改变量这个我们都理解,const就是干这个的。但是为什么不能调用非const函数呢?其实用脚丫子想想也能想明白啦>_<const函数本身不改,你调用非const函数,那个函数不保证会不会改,所以为了保险,肯定就只允许调用const函数啦。
class Base{protected:int num;public:Base(int n){num = n;}virtual void Show() const ;virtual void Show();void Change();};void Base::Show() const{//这里会出问题,const函数不能调用非const函数Change();cout<<"base show const: "<<num<<endl;}void Base::Show(){//非const函数没有限制,没有问题Change();cout<<"base show non-const: "<<num<<endl;}void Base::Change(){++num;}
这个我们编译一下,还是那个cosnt this指针的问题:error C2662: “Base::Change”: 不能将“this”指针从“const Base”转换为“Base &”
可见不管我们给没给参数,非静态成员函数都是有一个隐藏的this指针作为参数的。


7.const对象只能调用const成员函数,非const对象可以调用非const成员函数,也可以调用const成员函数


这一条刚才验证过了,const也是一种属性,带有const和没有const的函数是不同的,非const类型的对象可以隐式转化为const类型的对象,但是const类型的对象不能隐式转化成非const类型的对象,这样理解也许能够好理解一点。

8.const能保证对象成员不被修改,但是成员是指针或引用时,引用的对象不保证不被修改


const能保证对象成员不被修改,不过这个保证只能保证一层,换句话说,就是保证成员本身不被修改,但是如果这个成员是指针或者引用的话,cosnt并不能保证其指向的内容不被修改。
看一个例子:
// C++Test.cpp : 定义控制台应用程序的入口点。//#include "stdafx.h"#include <iostream>using namespace std;class Base{protected:int* num;public:Base(int* n){num = n;}virtual void Show() const ;virtual void Show();void Change() const;};void Base::Show() const{cout<<"base show const: "<<*num<<endl;}void Base::Show(){cout<<"base show non-const: "<<*num<<endl;}void Base::Change() const{(*num)++;}void TestFunc(const Base& obj){obj.Show();}int _tmain(int argc, _TCHAR* argv[]){int a = 1;Base base(&a);base.Change();TestFunc(base);system("pause");return 0;}
结果:
base show const: 2
请按任意键继续. . .
跟之前的例子差不多,不过我们把int变量换成了int型指针,而我们的Change函数虽然是个const类型的,但是我们还是成功的修改了指针的内容!!

9.有mutable修饰的成员,仍然可以通过const成员函数修改


从这里可以看出C++的灵活与复杂,虽然const限制了我们的行动,给我们提供了保障,但是还是有不方便的时候,C++又给我们提供了这样一个关键字--mutable。有了这个关键字,那就没有什么能阻止它被修改了。有了mutable,谁也挡不住!
// C++Test.cpp : 定义控制台应用程序的入口点。//#include "stdafx.h"#include <iostream>using namespace std;class Base{protected:mutable int num;public:Base(int n){num = n;}virtual void Show() const ;virtual void Show();void Change() const;};void Base::Show() const{cout<<"base show const: "<<num<<endl;}void Base::Show(){//非const函数没有限制,没有问题cout<<"base show non-const: "<<num<<endl;}void Base::Change() const{++num;}void TestFunc(Base& obj){obj.Show();}int _tmain(int argc, _TCHAR* argv[]){Base base(1);base.Change();TestFunc(base);system("pause");return 0;}
结果:

base show non-const: 2
请按任意键继续. . .

果然被改了!!

三.const修饰函数参数


1.引用或指针传递参数


说到const修饰函数参数,就必须先看一看通过指针或者引用传递参数的情况。刚学C的时候,仅仅会通过值进行参数传递。但是值传递有很多弊端:
1.我们传递给函数中的对象是对象的一个副本,需要调用函数的拷贝构造函数来创建一个临时的对象,返回时也是,如果通过值返回,也需要调用拷贝构造函数,如果是原生数据类型,比如一个int灯还好,但是如果是一个对象,里面有数十个个成员变量,再包含其他的对象,那么,通过值传递这样一个对象简直是灾难!
2.还有一个问题,就是值传递时,对象会发生切割,比如我们的函数接受的是一个Base类型的对象,而传递给它的是一个继承Base的子类,那么在创建那个临时对象的时候,就只创建了Base的部分,额外的部分都被切割掉了,不要说多态,就连普通的额外的函数都不能调用,这明显不是我们想要的结果。

PS:关于拷贝构造函数以及函数调用时会发生什么,在C++那些细节--拷贝构造函数中有写到。

为了克服值传递的弊端,C++提供了另外两种参数传递的方式,指针传递和引用传递。这两种其实没有什么太大的区别,都是通过一个handle间接地把参数传递到函数内部。这样,我们传递参数的过程只是把一个handle传递过去,完全没有那些拷贝构造函数等等一系列的开销。何乐而不为呢?不过,不是所有时候都适合使用引用或者指针传递的,如果传递的内容是C++原生数据类型,本身也不大,如果用指针传递,可能指针本身都比这个对象要大...

使用引用或者指针传递参数有一个很重要的特点,这也是与值传递最不一样的地方:引用或者指针传递进去的对象是对象的本体,而不是对象的副本。换句话说,我们在函数中是可以对对象本身进行修改的。这是个好事,也是个坏事。好事是我们可以利用这一点,进行结果的输出,函数的结果不一定非得return出去,而是可以通过参数再传递出去。但是坏事就是,函数可能会修改不应该修改的地方,为了克服这一个问题,就有了下面我们要介绍的,const修饰函参数。

2.const修饰函数的参数


const修饰函数的参数,是为了防止我们在函数中对传递近来的对象进行修改。这其实就有了之前值传递时的优点,保证不会修改对象。而引用指针传递时又减少函数调用的开销,这样,就兼具了两者的优点。
首先要注意的一点就是,函数的参数必须是输入型的参数,因为我们有时候要考参数来输出函数的结果的,这时候绝对不能加const的。

看一个例子,其实这个例子在说const函数的时候已经写过了,关于const函数,最大的用处就是供const对象调用,而const对象出现最多的地方,恰好就是函数传递进来的参数。
// C++Test.cpp : 定义控制台应用程序的入口点。//#include "stdafx.h"#include <iostream>using namespace std;class Base{protected:int num;public:Base(int n){num = n;}virtual void Show() const ;virtual void Show();void Change() ;};void Base::Show() const{cout<<"base show const: "<<num<<endl;}void Base::Show(){//非const函数没有限制,没有问题cout<<"base show non-const: "<<num<<endl;}void Base::Change(){++num;}void TestFunc(const Base& obj){//const对象调用非const成员函数,会出错//obj.Change();//调用const成员函数没有问题obj.Show();}int _tmain(int argc, _TCHAR* argv[]){Base base(1);TestFunc(base);system("pause");return 0;}
如果在TestFunc中调用change函数的话,仍然会出现那个问题:error C2662: “Base::Change”: 不能将“this”指针从“const Base”转换为“Base &”。
所以,使用cosnt可以在编译的时候就找到我们修改了或者可能修改某些不该修改的地方。

程序结果:
base show const: 1
请按任意键继续. . .

最后再来分析一下,函数接受的参数是const类型的引用,但是我们传递给它的是一个非const类型的对象引用。这里发生了一次隐式转化,非const类型对象引用转化成了const类型对象引用。所以函数里面的obj就变成了const类型,防止我们对其修改,更简单的说,它是只读的。



四.const修饰函数返回值


1.慎用返回指针或引用

虽然指针和引用能够在函数返回时,不构造临时对象,减少开销。但是返回函数内部对象的指针和引用却不是好主意。先看一个例子:
string GetStringByValue(){string name("haha");return name;}string& GetStringByRef(){string name("hehe");return name;}int _tmain(int argc, _TCHAR* argv[]){cout<<"By value: "<<GetStringByValue()<<endl;cout<<"By ref: "<<GetStringByRef()<<endl;system("pause");return 0;}
结果:
By value: haha
By ref:
请按任意键继续. . .

这个例子说不好恰不恰当,不过至少说明了点问题。返回函数内部临时对象的引用(或者指针)是很危险的,而且它的危险是不一定会被发现的。最开始,我使用上面的那个base类的引用返回,结果是正确的。因为这样有很多不确定性。正常来看,函数结束后,相关内容会被释放,如果没有被清空,那片内存就暂时还是会存放我们的对象,这样使用那里的指针暂时不会出错,但是一旦这里被清空,那么,使用这样的指针指向,不一定会出现什么问题。这个例子返回的是string类型,可见,通过值返回的时候是没有问题的,但是通过引用返回时,结果就不对了。

总之,记住一句话:不要轻易返回函数内部对象的handler。

2.返回对象引用的情况


虽然说返回函数的内部对象的引用或者指针可能出现问题,但是,毕竟这样做是合理的。C++没有把这条去掉,就说明它还是有它存在的意义的。
函数返回引用经常在赋值操作中用到。《Effective C++》中曾经提到过,重载operator = 的时候,返回一个*this的引用。这是很重要的一个地方,因为这涉及到是否可以进行连续赋值操作。看一个例子:
// C++Test.cpp : 定义控制台应用程序的入口点。//#include "stdafx.h"#include <iostream>#include <string>using namespace std;class Base{protected:int num;public:Base(int n){num = n;}virtual void Show() const ;virtual void Show();void Change() ;<span style="white-space:pre"></span>//operator = 操作符,返回一个对象本身的引用,用来进行连续赋值Base& operator= (const Base& b);};void Base::Show() const{cout<<"base show const: "<<num<<endl;}void Base::Show(){//非const函数没有限制,没有问题cout<<"base show non-const: "<<num<<endl;}void Base::Change(){++num;}Base& Base::operator=(const Base& b){num = b.num;return *this;}int _tmain(int argc, _TCHAR* argv[]){Base a(1);Base b(2);Base c(3);a = b = c;a.Show();b.Show();system("pause");return 0;}
结果:
base show non-const: 3
base show non-const: 3
请按任意键继续. . .

赋值的顺序是c->b,返回了b,然后这个b又作为参数给了a.operator=(),这个函数,进行b->a操作。

如果我们修改一下顺序呢?改为 (a = b) = c;
结果:
base show non-const: 3
base show non-const: 2
请按任意键继续. . .

这次的结果就变成了b->a,返回了一个a,然后c对这个a的引用进行赋值操作,即c->a。a变成了3,而b没有变。


而如果我们不返回这样一个引用,单步赋值操作仍然能够运行,但是连续赋值操作就会报错啦。


3.const修饰函数返回值

const修饰函数的返回值的时候,只是注意这个返回值不能改变,简单的说就是它是一个const类型的。const类型对象不能干的,它就不能干。还是上面的例子,这次我们就修改一个地方,将返回的引用改为const类型的。
class Base{protected:int num;public:Base(int n){num = n;}virtual void Show() const ;virtual void Show();void Change() ;const Base& operator= (const Base& b);};void Base::Show() const{cout<<"base show const: "<<num<<endl;}void Base::Show(){//非const函数没有限制,没有问题cout<<"base show non-const: "<<num<<endl;}void Base::Change(){++num;}//注意:与修饰函数const一样,声明中写了const,定义中仍然也要写constconst Base& Base::operator=(const Base& b){num = b.num;return *this;}
这次,我们还是像上次那样,分两种情况:a=b=c和 (a=b)=c:
当a=b=c的时候,结果和上面一样,这里不多做解释:

base show non-const: 3
base show non-const: 3
请按任意键继续. . .

但是(a=b)=c时,却编译失败了:
没有找到接受“const Base”类型的左操作数的运算符(或没有可接受的转换)
  C++Test.cpp(26): 可能是“const Base &Base::operator =(const Base &)”
因为顺序变了,b->a,返回了一个a的const引用,而const引用只能使用const类型的函数,没有const类型的operator=函数,因而会失败。

关于const返回引用,有一个对象我们都很熟悉,就是cout后面跟的<<操作符。这个东东能够连续输出的根本原因就在于它返回了本身的引用。这个引用本人猜测应该是const的,因为毕竟不能修改这个(未验证,知道的童鞋可以告诉我哈)。




五.总结


最后还是总结一下,const其实是一系列限制的条件,我们在编程的时候,要尽量使用这个限制条件,虽然肯能有很多地方不方便,但是这大大的降低了我们犯更严重错误的概率,使用了const之后,如果我们不小心修改了不应该修改的内容,编译器在编译的时候就会将问题暴露出来,这比运行时的莫名崩溃好解决得多。
还是引用《Effective C++》中的那句话:尽可能的使用const。




参考链接:
http://blog.csdn.net/lihao21/article/details/8634876
http://blog.csdn.net/clozxy/article/details/5679887
http://blog.chinaunix.net/uid-20771867-id-83257.html
http://wenku.baidu.com/link?url=ZP9BP6oFThhJYwHUJX35iRXWRkT83s5tP2boRQY81CsEL7ht7cRvOkAImNMNdShgV9BC-6WGfzcyz8wDDeo2aLmAgqA5AiPjnIlkormnkWC



0 0
原创粉丝点击