const和非const函数重载
成员函数后面加const,表示在该函数中不能对类的数据成员进行改变,比如下面的代码:
1 #include <stdio.h> 2 3 class A 4 { 5 private: 6 mutable int aa; 7 public: 8 A(){} 9 int x() 10 { 11 printf("no const\n"); 12 return aa++; 13 } 14 int x() const 15 { 16 printf("const\n"); 17 return aa++; 18 } 19 }; 20 21 int main() 22 { 23 A a1; 24 a1.x(); 25 26 const A a2; 27 a2.x(); 28 29 return 0; 30 }
代码的运行结果为:
首先说明成员函数后面加const这种机制,这是软件工程上的一种机制,在先前的软件开发中,类中各个函数的实现是由不同人完成了,所以大家共同操作同一组的数据成员,这样就会产生一种麻烦,会有些菜鸟或者不遵守规则的家伙胡乱的修改数据成员。当时只能用文档的形式来约束大家,在开发文档里清楚的写上你的这个函数实现不允许任何的修改数据成员,但是这样的文档的形式还是不能很好的约束那些家伙,于是C++语言的设计者Stroustrup想出来一个好的办法:
如果成员函数的后面加上了const,就表示这个成员函数不能对数据成员进行任何的修改动作。我把每个函数的声明都写好,不能修改数据成员的函数我在后面加上const。首先你的函数的实现也必须加上相应的const,否则函数的声明和实现不同,跑步起来;其次只要你在函数后面加上了const,就不能修改数据成员了。这样从编译器的水平拦截修改数据成员的行为,而不是从文档的角度限制这种行为。
当然有特殊情况,就是用mutable关键字修饰过的成员变量可以在声明为const 函数中被改变。
关于mutable:
关键字mutable是C++中一个不常用的关键字,他只能用于类的非静态和非常量数据成员。
我们知道一个对象的状态由该对象的非静态数据成员决定,所以随着数据成员的改变,对像的状态也会随之发生变化!
如果一个类的成员函数被声明为const类型,表示该函数不会改变对象的状态,也就是该函数不会修改类的非静态数据成员。
但是有些时候需要在该类函数中对类的数据成员进行赋值。这个时候就需要用到mutable关键字了。
但是上面的程序还有一个问题,在程序中实现了函数的重载,但是这个重载比较古怪,函数的参数列表是完全一样的,不一样的是函数后面有没有const修饰。程序的实现是这样的,你用const的对象去调用这个函数,就走加const的成员函数,用非const的对象去调用这个函数就走没有加const的成员函数。那么具体的实现怎么样呢?下面继续看:
这里先给出重载的结论:
我们知道,如果函数名相同,在相同的作用域内,其参数类型、参数个数,参数顺序不同等能构成函数重载。有趣的是如果同时在类中,对于函数名相同参数列表也相同的成员函数的const函数和非const函数能够构成重载。
它们被调用的时机为:如果定义的对象是常对象,则调用的是const成员函数,如果定义的对象是非常对象,则调用重载的非const成员函数。
上面的这句话是必须的,也就是如果你定义的对象是const的,那么你的这个对象只能调用带有const的成员函数,如果你调用非const的成员函数,编译器就会报错。
这个是理所当然的,试想你定义的对象是const的,意思就是你的对象的数据成员是不可以被改变的。如果允许你调用非const的成员函数,万一你在该函数中修改了对象的数据成员怎么办。与其说在你试图修改数据成员的时候再拦截你还不如在编译的时候就直接拦截你。看下面的代码:
1 #include <stdio.h> 2 3 class A 4 { 5 private: 6 int aa; 7 public: 8 A(){} 9 int x() 10 { 11 printf("no const\n"); 12 return aa++; 13 } 14 }; 15 16 int main() 17 { 18 const A a2; 19 a2.x(); 20 21 return 0; 22 }
程序会报错:error C2662: 'x' : cannot convert 'this' pointer from 'const class A' to 'class A &'。
这让我想起来了C++老师说的另外的两个问题(太经典了,再一次感觉到了C++的体系结构,杨老师万岁):
(一)看代码:
1 const int x = 5; 2 int *p; 3 p = &x;
这里是绝对会报错的:error C2440: '=' : cannot convert from 'const int *' to 'int *'
你当编译器是傻子啊,你先声明了一个const的变量,表示这个变量不能被修改,然后你又弄一个int类型指针指向它,表示你可以通过指针修改数据,自相矛盾。编译器是不会给你这样的漏洞的。
提示:const int x = 5;const修饰的变量在定义的时候一定要初始化(除非是extern的)。
在看另个一代码:
1 int x; 2 const int *p; 3 p = &x; 4 5 x++; 6 (*p)++;
这里也会报错:error C2166: l-value specifies const object。
错误的语句是(*p)++;当然定义的x是可以被修改的,但是const int *p这个指针的意思是:指针可以随便的摆(就是p可以指向x,也可以指向y),但是有一条:我指向谁,谁不能动。所以const int *p并不是说p的值(p的值是对象的地址)不能改变,而是说p指向的对象的值不能被修改。
(二)重头戏来了
老师说过,C++的编译器会在你的每个非静态的成员函数的参数列表中增加一个this指针,因为所有对象的代码在内存中就一份,this指针的功能是谁调用这个成员函数,该成员函数的这个this指针就指向谁,从而处理谁的数据。(这个理论是这篇文章的重点,重中之重)
那么我们的带const的成员函数和不带const的成员函数的重载是怎么识别的呢?我上面只给了个结论,下面我就来揭开这个神秘的面纱:
其实带const的成员函数和不带const的成员函数的this指针是不同的:
const的成员函数的this指针是:const A*类型的;不带const的成员函数的this指针是:A*类型的;(这里的A就是代码中定义的类,借用一下上面的定义)。
这就恍然大悟了吧:
const成员函数不能修改数据成员的内部实现是这样的,一个对象调用了某个带const的成员函数,那么这个带const的成员函数的this指针就指向了这个对象,这就是上面(一)中的一种情况,const A*类型的指针指向谁,谁不能动(不能被修改)。所以带const的成员函数你休想修改数据成员。只要有调用,你的this指针就得指向这个对象,由于你的this指针是const A*类型的,只要指向了你就不能修改。哈哈~~让你得瑟。
还有一种情况:就是如果一个对象是const A a;这种的,也就是说常量对象。那么这个对象只能调用带const的成员函数,因为如果你调用了非const的成员函数,你就是(一)中的另一种情况,你事先定义了一个const变量,然后在用一个非const的指针指向他,你当编译器傻子啊。因为你的非const的成员函数的this指针是A*类型的,没有const修饰。
总结:const A a对象只能调用const修饰的成员函数,因为a是const修饰的,只能用带cosnt的this指针才可以指向他。
A a可以调用非const修饰的成员函数和const修饰的成员函数。但是编译器会首先进行检查,看看const修饰的成员函数有没有修改数据成员,然后调用非const修饰的成员函数(const修饰的和非const修饰的两个成员函数构成了重载)。虽然在不修改数据成员的情况下调用const修饰的成员函数也是可以的,但是有优先权(这里是我猜的,有时间问老师)。
洗了个澡,好像大概想通了,既然是函数重载,那么就根据重载的基础,参数列表的不同。为什么会优先调用非const修饰的成员函数呢?因为非const修饰的成员函数的参数列表是这样的(A * a,......) 带const修饰的成员函数的列表是这样的(const A* a,......),这里只写出他们的this指针,当然了,重载的区分也是靠的this指针,防止函数调用时发生二义性。A a这样的对象调用函数的时候要把自己的地址传给被调成员函数的this指针,a的地址就是相当于A*这样类型的指针,并没有const修饰啊,所以首先选择最相近的那个。当然了,如果只有一个带const修饰的成员函数,直接调用它也没不会报错的。因为具体的实现就好像这样:const int* p = &x;但是我没有对x修改,只是简单的动了copy了一下它的地址。这当然是允许的。
就像下面的代码:
1 #include <stdio.h> 2 3 class A 4 { 5 private: 6 int aa; 7 public: 8 A(){} 9 10 int x()const 11 { 12 printf("const\n"); 13 14 return 0; 15 } 16 }; 17 18 int main() 19 { 20 A a2; 21 a2.x(); 22 23 return 0; 24 }
这个是可以正常跑的,不会报错。
1 #include <stdio.h> 2 3 class A 4 { 5 private: 6 int aa; 7 public: 8 A(){} 9 10 int x() 11 { 12 printf("no const\n"); 13 14 return 0; 15 } 16 17 int x()const 18 { 19 printf("const\n"); 20 21 return 0; 22 } 23 }; 24 25 int main() 26 { 27 A a2; 28 a2.x(); 29 30 return 0; 31 }
这个是会优先选择非const修饰的成员函数的。
所以,有const修饰的成员函数和没有const修饰的成员函数之间构成的重载,还是靠的参数列表区分的调用关系,两个函数的参数列表的不同点是编译器自动给你的每个成员函数(静态的成员函数除外,因为静态的成员函数没有this指针,具体分析在另一篇博文上)添加的this指针是不同的,一个是普通的,一个是带有const修饰的。
下面的这个例子是C++的运算符的重载,这里你就必须考虑到const修饰的对象的函数调用,也就是说必须做两套成员函数,一套给有const修饰的对象(比如常量字符串)用,一套给没有const修饰的对象用。
时间关系,今天先到这里,还有矩阵分析的作业没有做,下面的代码是别人的,有时间再回来弄
--------------------------------------------------------------------------------------------------------------------
再看为String类实现的[]操作符重载函数:
char& operator[](int posion) // function_1 { return data[posion]; }; 注意,这里该函数的返回值为一个引用,否则str[0] = 'c'这样的语句就会不合法,因为str[0]将是一个左值。
那么,是否提供这样一个function_1就足够了呢?看下面这段代码: const String str= "She"; char c = str[0]; // 错误!编译提示:error C2678: 二进制“[” : 没有找到接受“const String”类型的左操作数的运算符(或没有可接受的转换)
很显然,我们必须还要为const String提供一个const版本的opeartor[]。如下: char& operator[](int posion) const { return data[posion]; } 这样,当使用const的String对象使用[]操作符时,便会调用该const的重载版本。 但是,这样就OK了嘛?虽然上面的那段代码没有问题了,但是其中却隐藏了一个陷阱,看如下代码: const String str = "She"; str[0] = 'T'; 上面这段代码可以编译,运行通过,str变为了"The"!而str声明为const的!!
现在,你应该知道了,对于const的该操作符重载函数其返回值也应该是const的,否则就会出现可以通过其修改const对象的漏洞。修改如下: const char& operator[](int posion) const { return data[posion]; } 好了,现在没有问题了!
我们再回过头来看一下,为了给String提供一个[]操作符来读写指定位置的字符,需要提供如下两个函数,以分别对非const String对象和const String对象提供支持:
char& operator[](int posion)
{ return data[posion]; };
const char& operator[](int posion) const
{ return data[posion]; }