const的使用.docx
《const的使用.docx》由会员分享,可在线阅读,更多相关《const的使用.docx(10页珍藏版)》请在冰豆网上搜索。
const的使用
一const基础
如果const关键字不涉及到指针,我们很好理解,下面是涉及到指针的情况:
intb=500;
constint*a=&b;[1]
intconst*a=&b;[2]
int*consta=&b;[3]
constint*consta=&b;[4]
如果你能区分出上述四种情况,那么,恭喜你,你已经迈出了可喜的一步。
不知道,也没关系,我们可以参考《Effectivec++》Item21上的做法,如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。
因此,[1]和[2]的情况相同,都是指针所指向的内容为常量(const放在变量声明符的位置无关),这种情况下不允许对内容进行更改操作,如不能*a=3;[3]为指针本身是常量,而指针所指向的内容不是常量,这种情况下不能对指针本身进行更改操作,如a++是错误的;[4]为指针本身和指向的内容均为常量。
另外const的一些强大的功能在于它在函数声明中的应用。
在一个函数声明中,const可以修饰函数的返回值,或某个参数;对于成员函数,还可以修饰是整个函数。
有如下几种情况,以下会逐渐的说明用法:
A&operator=(constA&a);
voidfun0(constA*a);
voidfun1()const;//fun1()为类成员函数
constAfun2();
二const的初始化
先看一下const变量初始化的情况
1)非指针const常量初始化的情况:
Ab;
constAa=b;
2)指针(引用)const常量初始化的情况:
A*d=newA();
constA*c=d;
或者:
constA*c=newA();
引用:
Af;
constA&e=f;//这样作e只能访问声明为const的函数,而不能访问一般的成员函数;
[思考1]:
以下的这种赋值方法正确吗?
constA*c=newA();
A*e=c;
[思考2]:
以下的这种赋值方法正确吗?
A*constc=newA();
A*b=c;
三作为参数和返回值的const修饰符
其实,不论是参数还是返回值,道理都是一样的,参数传入时候和函数返回的时候,初始化const变量
1修饰参数的const,如voidfun0(constA*a);voidfun1(constA&a);
调用函数的时候,用相应的变量初始化const常量,则在函数体中,按照const所修饰的部分进行常量化,如形参为constA*a,则不能对传递进来的指针的内容进行改变,保护了原指针所指向的内容;如形参为constA&a,则不能对传递进来的引用对象进行改变,保护了原对象的属性。
[注意]:
参数const通常用于参数为指针或引用的情况;
2修饰返回值的const,如constAfun2();constA*fun3();
这样声明了返回值后,const按照"修饰原则"进行修饰,起到相应的保护作用。
constRationaloperator*(constRational&lhs,constRational&rhs)
{
returnRational(lhs.numerator()*rhs.numerator(),
lhs.denominator()*rhs.denominator());
}
返回值用const修饰可以防止允许这样的操作发生:
Rationala,b;
Radionalc;
(a*b)=c;
一般用const修饰返回值为对象本身(非引用和指针)的情况多用于二目操作符重载函数并产生新对象的时候。
[总结]一般情况下,函数的返回值为某个对象时,如果将其声明为const时,多用于操作符的重载。
通常,不建议用const修饰函数的返回值类型为某个对象或对某个对象引用的情况。
原因如下:
如果返回值为某个对象为const(constAtest=A实例)或某个对象的引用为const(constA&test=A实例),则返回值具有const属性,则返回实例只能访问类A中的公有(保护)数据成员和const成员函数,并且不允许对其进行赋值操作,这在一般情况下很少用到。
[思考3]:
这样定义赋值操作符重载函数可以吗?
constA&operator=(constA&a);
四类成员函数中const的使用
一般放在函数体后,形如:
voidfun()const;
如果一个成员函数的不会修改数据成员,那么最好将其声明为const,因为const成员函数中不允许对数据成员进行修改,如果修改,编译器将报错,这大大提高了程序的健壮性。
五使用const的一些建议
1要大胆的使用const,这将给你带来无尽的益处,但前提是你必须搞清楚原委;
2要避免最一般的赋值操作错误,如将const变量赋值,具体可见思考题;
3在参数中使用const应该使用引用或指针,而不是一般的对象实例,原因同上;
4const在成员函数中的三种用法(参数、返回值、函数)要很好的使用;
5不要轻易的将函数的返回值类型定为const;
6除了重载操作符外一般不要将返回值类型定为对某个对象的const引用;
本人水平有限,欢迎批评指正,可以联系kangjd@
[思考题答案]
1这种方法不正确,因为声明指针的目的是为了对其指向的内容进行改变,而声明的指针e指向的是一个常量,所以不正确;
2这种方法正确,因为声明指针所指向的内容可变;
3这种做法不正确;
在constA:
:
operator=(constA&a)中,参数列表中的const的用法正确,而当这样连续赋值的时侯,问题就出现了:
Aa,b,c:
(a=b)=c;
因为a.operator=(b)的返回值是对a的const引用,不能再将c赋值给const常量。
const给人的第一印象就是定义常量。
(1)const用于定义常量。
例如:
constintN=100;constintM=200;这样程序中只要用到N、M就分别代表为整型100、200,N、M为一常量,在程序中不可改变。
但有人说他编程时从来不用const定义常量。
我相信。
但他是不懂得真正的编程艺术,用const定义常量不仅能方便我们编程而且能提高程序的清晰性。
你是愿意看到程序中100、200满天飞,还是愿意只看到简单清晰的N、M。
相信有没有好处你慢慢体会。
还有人说他不用const定义常量,他用#define宏定义常量。
可以。
但不知道你有没有发现有时#define宏并没有如你所愿在定义常量。
下面我们比较比较const和#define。
1。
(a)const定义常量是有数据类型的:
这样const定义的常量编译器可以对其进行数据静态类型安全检查,而#define宏定义的常量却只是进行简单的字符替换,没有类型安全检查,且有时还会产生边际效应(不如你愿处)。
所谓边际效应举例如下:
#defineN100#defineM200+N当程序中使用M*N时,原本想要100*(200+N)的却变成了100*200+N。
(b)#define宏定义常量却没有。
#define<宏名><字符串>,字符串可以是常数、表达式、格式串等。
在程序被编译的时候,如果遇到宏名就哟内指定的字符串进行替换,然后再进行编译。
2。
有些调试程序可对const进行调试,但不对#define进行调试。
3。
当定义局部变量时,const作用域仅限于定义局部变量的函数体内。
但用#define时其作用域不仅限于定义局部变量的函数体内,而是从定义点到整个程序的结束点。
但也可以用#undef取消其定义从而限定其作用域范围。
只用const定义常量,并不能起到其强大的作用。
const还可修饰函数形式参数、返回值和类的成员函数等。
从而提高函数的健壮性。
因为const修饰的东西能受到c/c++的静态类型安全检查机制的强制保护,防止意外的修改。
(2)const修饰函数形式参数
形式参数有输入形式参数和输出形式参数。
参数用于输出时不能加const修饰,那样会使函数失去输出功能。
因为const修饰的东西是不能改变的。
const只能用于修饰输入参数。
谈const只能用于修饰输入参数之前先谈谈C++函数的三种传递方式。
C++函数的三种传递方式为:
值传递、指针传递和引用传递。
简单举例说明之,详细说明请参考别的资料。
值传递:
voidfun(intx){x+=5;//修改的只是y在栈中copyx,x只是y的一个副本,在内存中重新开辟的一块临时空间把y的值送给了x;这样也增加了程序运行的时间,降低了程序的效率。
}voidmain(void){inty=0;fun(y);cout<<\"y=\"<voidfun(int*x){*x+=5;//修改的是指针x指向的内存单元值}voidmain(void){inty=0;fun(&y);cout<<<<\"y=\"<voidfun(int&x){x+=5;//修改的是x引用的对象值&x=y;}voidmain(void){inty=0;fun(y);cout<<<<\"y=\"<当输入参数用“值传递”方式时,我们不需要加const修饰,因为用值传递时,函数将自动用实际参数的拷贝初始化形式参数,当在函数体内改变形式参数时,改变的也只是栈上的拷贝而不是实际参数。
但要注意的是,当输入参数为ADT/UDT(用户自定义类型和抽象数据类型)时,应该将“值传递”改为“const&传递”,目的可以提高效率。
例如:
voidfun(Aa);//效率底。
函数体内产生A类型的临时对象用于复制参数a,但是临时对象的//构造、复制、析构过程都将消耗时间。
voidfun(Aconst&a);//提高效率。
用“引用传递”不需要产生临时对象,省了临时对象的//构造、复制、析构过程消耗的时间。
但光用引用有可能改变a,所以加const
当输入参数用“指针传递”方式时,加const修饰可防止意外修改指针指向的内存单元,起到保护作用。
例如:
voidfunstrcopy(char*strdest,constchar*strsrc)//任何改变strsrc指向的内存单元,//编译器都将报错些时保护了指针的内存单元,也可以保护指针本身,防止其地址改变。
例如:
voidfunstrcopy(char*strdest,constchar*conststrsrc)
(3)const修饰函数的返回值
如给“指针传递”的函数返回值加const,则返回值不能被直接修改,且该返回值只能被赋值给加const修饰的同类型指针。
例如:
constchar*GetChar(void){};赋值char*ch=GetChar();//错误constchar*ch=GetChar();//正确
(4)const修饰类的成员函数(函数定义体)
任何不会修改数据成员的函数都应用const修饰,这样当不小心修改了数据成员或调用了非const成员函数时,编译器都会报错。
const修饰类的成员函数形式为:
intGetCount(void)const;(5)用传引用给const取代传值缺省情况下,C++以传值方式将对象传入或传出函数(这是一个从C继承来的特性)。
除非你特别指定其它方式,否则函数的参数就会以实际参数(actualargument)的拷贝进行初始化,而函数的调用者会收到函数返回值的一个拷贝。
这个拷贝由对象的拷贝构造函数生成。
这就使得传值(pass-by-value)成为一个代价不菲的操作。
例如,考虑下面这个类层级结构:
classPerson{ public:
Person();//parametersomittedforsimplicity virtual~Person();//seeItem7forwhythisisvirtual ...
private:
std:
:
stringname; std:
:
stringaddress;};
classStudent:
publicPerson{ public:
Student();//parametersagainomitted ~Student(); ...
private:
std:
:
stringschoolName; std:
:
stringschoolAddress;};
现在,考虑以下代码,在此我们调用一个函数——validateStudent,它得到一个Student参数(以传值的方式),并返回它是否验证有效的结果:
boolvalidateStudent(Students);//functiontakingaStudent//byvalue
Studentplato;//PlatostudiedunderSocrates
boolplatoIsOK=validateStudent(plato);//callthefunction
当这个函数被调用时会发生什么呢?
很明显,Student的拷贝构造函数被调用,用plato来初始化参数s。
同样明显的是,当validateStudent返回时,s就会被销毁。
所以这个函数的参数传递代价是一次Student的拷贝构造函数的调用和一次Student的析构函数的调用。
但这还不是全部。
一个Student对象内部包含两个string对象,所以每次你构造一个Student对象的时候,你也必须构造两个string对象。
一个Student对象还要从一个Person对象继承,所以每次你构造一个Student对象的时候,你也必须构造一个Person对象。
一个Person对象内部又包含两个额外的string对象,所以每个Person的构造也承担着另外两个string的构造。
最终,以传值方式传递一个Student对象的后果就是引起一次Student的拷贝构造函数的调用,一次Person的拷贝构造函数的调用,以及四次string的拷贝构造函数调用。
当Student对象的拷贝被销毁时,每一个构造函数的调用都对应一个析构函数的调用,所以以传值方式传递一个Student的全部代价是六个构造函数和六个析构函数!
好了,这是正确的和值得的行为。
毕竟,你希望你的全部对象都得到可靠的初始化和销毁。
尽管如此,如果有一种办法可以绕过所有这些构造和析构过程,应该变得更好,这就是:
传引用给const(passbyreference-to-const):
boolvalidateStudent(constStudent&s);
这样做非常有效:
没有任何构造函数和析构函数被调用,因为没有新的对象被构造。
被修改的参数声明中的const是非常重要的。
validateStudent的最初版本接受一个Student值参数,所以调用者知道它们屏蔽了函数对它们传入的Student的任何可能的改变;validateStudent也只能改变它的一个拷贝。
现在Student以引用方式传递,同时将它声明为const是必要的,否则调用者必然担心validateStudent改变了它们传入的Student。
以传引用方式传递参数还可以避免切断问题(slicingproblem)。
当一个派生类对象作为一个基类对象被传递(传值方式),基类的拷贝构造函数被调用,而那些使得对象的行为像一个派生类对象的特殊特性被“切断”了。
你只剩下一个纯粹的基类对象——这没什么可吃惊的,因为是一个基类的构造函数创建了它。
这几乎绝不是你希望的。
例如,假设你在一组实现一个图形窗口系统的类上工作:
classWindow{ public:
... std:
:
stringname()const;//returnnameofwindow virtualvoiddisplay()const;//drawwindowandcontents};
classWindowWithScrollBars:
publicWindow{ public:
... virtualvoiddisplay()const;};
所有Window对象都有一个名字,你能通过name函数得到它,而且所有的窗口都可以显示,你可一个通过调用display函数来做到这一点。
display为virtual的事实清楚地告诉你:
一个纯粹的基类的Window对象的显示方法有可能不同于专门的WindowWithScrollBars对象的显示方法。
现在,假设你想写一个函数打印出一个窗口的名字,并随后显示这个窗口。
以下这个函数的写法是错误的:
voidprintNameAndDisplay(Windoww)//incorrect!
parameter{ //maybesliced!
std:
:
cout< 考虑当你用一个WindowWithScrollBars对象调用这个函数时会发生什么:
WindowWithScrollBarswwsb;
printNameAndDisplay(wwsb);
参数w将被作为一个Window对象构造——它是被传值的,记得吗?
而且使wwsb表现得像一个WindowWithScrollBars对象的特殊信息都被切断了。
在printNameAndDisplay中,全然不顾传递给函数的那个对象的类型,w将始终表现得像一个Window类的对象(因为它就是一个Window类的对象)。
特别是,在printNameAndDisplay中调用display将总是调用Window:
:
display,绝不会是WindowWithScrollBars:
:
display。
绕过切断问题的方法就是以传引用给const的方式传递w:
voidprintNameAndDisplay(constWindow&w)//fine,parameterwon’t{ //besliced std:
:
cout< 现在w将表现得像实际传入的那种窗口。
如果你掀开编译器的盖头偷看一下,你会发现用指针实现引用是非常典型的做法,所以以引用传递某物实际上通常意味着传递一个指针。
由此可以得出结论,如果你有一个内建类型的对象(例如,一个int),以传值方式传递它常常比传引用方式更高效。
那么,对于内建类型,当你需要在传值和传引用给const之间做一个选择时,没有道理不选择传值。
同样的建议也适用于STL中的迭代器(iterators)和函数对象(functionobjects),因为,作为惯例,它们就是为传值设计的。
迭代器(iterators)和函数对象(functionobjects)的实现有责任保证拷贝的高效并且不受切断问题的影响。
(这是一个“规则如何变化,依赖于你使用C++的哪一个部分”的实例。
)
内建类型很小,所以有人就断定所有的小类型都是传值的上等候选者,即使它们是用户定义的。
这样的推论是不可靠的。
仅仅因为一个对象小,并不意味着调用它的拷贝构造函数就是廉价的。
很多对象——大多数STL容器也在其中——容纳的和指针一样,但是拷贝这样的对象必须同时拷贝它们指向的每一样东西。
那可能是非常昂贵的。
即使当一个小对象有一个廉价的拷贝构造函数,也会存在性能问题。
一些编译器对内建类型和用户定义类型并不一视同仁,即使他们有同样的底层表示。
例如,一些编译器拒绝将仅由一个double组成的对象放入一个寄存器中,即使在常规上它们非常愿意将一个纯粹的double放入那里。
如果发生了这种事情,你以传引用方式传递这样的对象更好一些,因为编译器理所当然会将一个指针(引用的实现)放入寄存器。
小的用户定义类型不一定是传值的上等候选者的另一个原因是:
作为用户定义类型,它的大小常常变化。
一个现在较小的类型在将来版本中可能变得更大,因为它的内部实现可能会变化。
甚至当你换了一个不同的C++实现时,事情都可能会变化。
例如,就在我这样写的时候,一些标准库的string类型的实现的大小就是另外一些实现的七倍。
通常情况下,你能合理地假设传值廉价的类型仅有内建类型及STL中的迭代器和函数对象类型。
对其他任何类型,请遵循本Item的建议,并用传引用给const取代传值。
ThingstoRemember
·用传引用给const取代传值。
典型情况下它更高效而且可以避免切断问题。
·这条规则并不适用于内建类型及STL中的迭代器和函数对象类型。
对于它们,传值通常更合适。
只在总结,也许不够专业,不够全面,请大家指教。