const的使用.docx

上传人:b****8 文档编号:10196904 上传时间:2023-02-09 格式:DOCX 页数:10 大小:22.44KB
下载 相关 举报
const的使用.docx_第1页
第1页 / 共10页
const的使用.docx_第2页
第2页 / 共10页
const的使用.docx_第3页
第3页 / 共10页
const的使用.docx_第4页
第4页 / 共10页
const的使用.docx_第5页
第5页 / 共10页
点击查看更多>>
下载资源
资源描述

const的使用.docx

《const的使用.docx》由会员分享,可在线阅读,更多相关《const的使用.docx(10页珍藏版)》请在冰豆网上搜索。

const的使用.docx

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中的迭代器和函数对象类型。

对于它们,传值通常更合适。

只在总结,也许不够专业,不够全面,请大家指教。

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 医药卫生 > 药学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1