分享
 
 
 

Effective C++ 2e Item21

王朝c/c++·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

条款21: 尽可能使用const

使用const的好处在于它允许指定一种语意上的约束——某种对象不能被修改——编译器具体来实施这种约束。通过const,你可以通知编译器和其他程序员某个值要保持不变。只要是这种情况,你就要明确地使用const ,因为这样做就可以借助编译器的帮助确保这种约束不被破坏。

const关键字实在是神通广大。在类的外面,它可以用于全局或名字空间常量(见条款1和47),以及静态对象(某一文件或程序块范围内的局部对象)。在类的内部,它可以用于静态和非静态成员(见条款12)。

对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const,还有,两者都不指定为const:

char *p = "Hello"; // 非const指针,

// 非const数据

const char *p = "Hello"; // 非const指针,

// const数据

char * const p = "Hello"; // const指针,

// 非const数据

const char * const p = "Hello"; // const指针,

// const数据

语法并非看起来那么变化多端。一般来说,你可以在头脑里画一条垂直线穿过指针声明中的星号(*)位置,如果const出现在线的左边,指针指向的数据为常量;如果const出现在线的右边,指针本身为常量;如果const在线的两边都出现,二者都是常量。

在指针所指为常量的情况下,有些程序员喜欢把const放在类型名之前,有些程序员则喜欢把const放在类型名之后、星号之前。所以,下面的函数取的是同种参数类型:

class Widget { ... };

void f1(const Widget *pw); // f1取的是指向

// Widget常量对象的指针

void f2(Widget const *pw); // 同f2

因为两种表示形式在实际代码中都存在,所以要使自己对这两种形式都习惯。

const的一些强大的功能基于它在函数声明中的应用。在一个函数声明中,const可以指的是函数的返回值,或某个参数;对于成员函数,还可以指的是整个函数。

让函数返回一个常量值经常可以在不降低安全性和效率的情况下减少用户出错的几率。实际上正如条款29所说明的,对返回值使用const有可能提高一个函数的安全性和效率,否则还会出问题。

例如,看这个在条款19中介绍的有理数的operator*函数的声明:

const Rational operator*(const Rational& lhs,

const Rational& rhs);

很多程序员第一眼看到它会纳闷:为什么operator*的返回结果是一个const对象?因为如果不是这样,用户就可以做下面这样的坏事:

Rational a, b, c;

...

(a * b) = c; // 对a*b的结果赋值

我不知道为什么有些程序员会想到对两个数的运算结果直接赋值,但我却知道:如果a,b和c是固定类型,这样做显然是不合法的。一个好的用户自定义类型的特征是,它会避免那种没道理的与固定类型不兼容的行为。对我来说,对两个数的运算结果赋值是非常没道理的。声明operator*的返回值为const可以防止这种情况,所以这样做才是正确的。

关于const参数没什么特别之处要强调——它们的运作和局部const对象一样。(但,见条款M19,const参数会导致一个临时对象的产生)然而,如果成员函数为const,那就是另一回事了。

const成员函数的目的当然是为了指明哪个成员函数可以在const对象上被调用。但很多人忽视了这样一个事实:仅在const方面有不同的成员函数可以重载。这是C++的一个重要特性。再次看这个String类:

class String {

public:

...

// 用于非const对象的operator[]

char& operator[](int position)

{ return data[position]; }

// 用于const对象的operator[]

const char& operator[](int position) const

{ return data[position]; }

private:

char *data;

};

String s1 = "Hello";

cout << s1[0]; // 调用非const

// String::operator[]

const String s2 = "World";

cout << s2[0]; // 调用const

// String::operator[]

通过重载operator[]并给不同版本不同的返回值,就可以对const和非const String进行不同的处理:

String s = "Hello"; // 非const String对象

cout << s[0]; // 正确——读一个

// 非const String

s[0] = 'x'; // 正确——写一个

// 非const String

const String cs = "World"; // const String 对象

cout << cs[0]; // 正确——读一个

// const String

cs[0] = 'x'; // 错误!——写一个

// const String

另外注意,这里的错误只和调用operator[]的返回值有关;operator[]调用本身没问题。 错误产生的原因在于企图对一个const char&赋值,因为被赋值的对象是const版本的operator[]函数的返回值。

还要注意,非const operator[]的返回类型必须是一个char的引用——char本身则不行。如果operator[]真的返回了一个简单的char,如下所示的语句就不会通过编译:

s[0] = 'x';

因为,修改一个“返回值为固定类型”的函数的返回值绝对是不合法的。即使合法,由于C++“通过值(而不是引用)来返回对象”(见条款22)的内部机制的原因,s.data[0]的一个拷贝会被修改,而不是s.data[0]自己,这就不是你所想要的结果了。

让我们停下来看一个基本原理。一个成员函数为const的确切含义是什么?有两种主要的看法:数据意义上的const(bitwise constness)和概念意义上的const(conceptual constness)。

bitwise constness的坚持者认为,当且仅当成员函数不修改对象的任何数据成员(静态数据成员除外)时,即不修改对象中任何一个比特(bit)时,这个成员函数才是const的。bitwise constness最大的好处是可以很容易地检测到违反bitwise constness规定的事件:编译器只用去寻找有无对数据成员的赋值就可以了。实际上,bitwise constness正是C++对const问题的定义,const成员函数不被允许修改它所在对象的任何一个数据成员。

不幸的是,很多不遵守bitwise constness定义的成员函数也可以通过bitwise测试。特别是,一个“修改了指针所指向的数据”的成员函数,其行为显然违反了bitwise constness定义,但如果对象中仅包含这个指针,这个函数也是bitwise const的,编译时会通过。这就和我们的直觉有差异:

class String {

public:

// 构造函数,使data指向一个

// value所指向的数据的拷贝

String(const char *value);

...

operator char *() const { return data;}

private:

char *data;

};

const String s = "Hello"; // 声明常量对象

char *nasty = s; // 调用 operator char*() const

*nasty = 'M'; // 修改s.data[0]

cout << s; // 输出"Mello"

显然,在用一个值创建一个常量对象并调用对象的const成员函数时一定有什么错误,对象的值竟然可以修改!(关于这个例子更详细的讨论参见条款29)

这就导致conceptual constness观点的引入。此观点的坚持者认为,一个const成员函数可以修改它所在对象的一些数据(bits) ,但只有在用户不会发觉的情况下。例如,假设String类想保存对象每次被请求时数据的长度:

class String {

public:

// 构造函数,使data指向一个

// value所指向的数据的拷贝

String(const char *value): lengthIsValid(false) { ... }

...

size_t length() const;

private:

char *data;

size_t dataLength; // 最后计算出的

// string的长度

bool lengthIsValid; // 长度当前

// 是否合法

};

size_t String::length() const

{

if (!lengthIsValid) {

dataLength = strlen(data); // 错误!

lengthIsValid = true; // 错误!

}

return dataLength;

}

这个length的实现显然不符合“bitwise const”的定义——dataLength 和lengthIsValid都可以修改——但对const String对象来说,似乎它一定要是合法的才行。但编译器也不同意, 它们坚持“bitwise constness”,怎么办?

解决方案很简单:利用C++标准组织针对这类情况专门提供的有关const问题的另一个可选方案。此方案使用了关键字mutable,当对非静态数据成员运用mutable时,这些成员的“bitwise constness”限制就被解除:

class String {

public:

... // same as above

private:

char *data;

mutable size_t dataLength; // 这些数据成员现在

// 为mutable;他们可以在

mutable bool lengthIsValid; // 任何地方被修改,即使

// 在const成员函数里

};

size_t String::length() const

{

if (!lengthIsValid) {

dataLength = strlen(data); // 现在合法

lengthIsValid = true; // 同样合法

}

return dataLength;

}

mutable在处理“bitwise-constness限制”问题时是一个很好的方案,但它被加入到C++标准中的时间不长,所以有的编译器可能还不支持它。如果是这样,就不得不倒退到C++黑暗的旧时代去,在那儿,生活很简陋,const有时可能会被抛弃。

类C的一个成员函数中,this指针就好象经过如下的声明:

C * const this; // 非const成员函数中

const C * const this; // const成员函数中

这种情况下(即编译器不支持mutable的情况下),如果想使那个有问题的String::length版本对const和非const对象都合法,就只有把this的类型从const C * const改成C * const。不能直接这么做,但可以通过初始化一个局部变量指针,使之指向this所指的同一个对象来间接实现。然后,就可以通过这个局部指针来访问你想修改的成员:

size_t String::length() const

{

// 定义一个不指向const对象的

// 局部版本的this指针

String * const localThis =

const_cast<String * const>(this);

if (!lengthIsValid) {

localThis->dataLength = strlen(data);

localThis->lengthIsValid = true;

}

return dataLength;

}

做的不是很漂亮。但为了完成想要的功能也就只有这么做。

当然,如果不能保证这个方法一定可行,就不要这么做:比如,一些老的“消除const”的方法就不行。特别是,如果this所指的对象真的是const,即,在定义时被声明为const,那么,“消除const”就会导致不可确定的后果。所以,如果想在成员函数中通过转换消除const,就最好先确信你要转换的对象最初没有被定义为const。

还有一种情况下,通过类型转换消除const会既有用又安全。这就是:将一个const对象传递到一个取非const参数的函数中,同时你又知道参数不会在函数内部被修改的情况时。第二个条件很重要,因为对一个只会被读的对象(不会被写)消除const永远是安全的,即使那个对象最初曾被定义为const。

例如,已经知道有些库不正确地声明了象下面这样的strlen函数:

size_t strlen(char *s);

strlen当然不会去修改s所指的数据——至少我一辈子没看见过。但因为有了这个声明,对一个const char *类型的指针调用这个函数时就会不合法。为解决这个问题,可以在给strlen传参数时安全地把这个指针的const强制转换掉:

const char *klingonGreeting = "nuqneH"; // "nuqneH"即"Hello"

//

size_t length =

strlen(const_cast<char*>(klingonGreeting));

但不要滥用这个方法。只有在被调用的函数(比如本例中的strlen)不会修改它的参数所指的数据时,才能保证它可以正常工作。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有