条款20 避免public接口出现数据成员
一致性, 所有public接口里都是函数;
安全性, 可以控制数据成员的访问权限;
1
2
3
4
5
6
7
8
9
10
11
12
|
class
AccessLevels {
public
:
int
getReadOnly()
const
{
return
readOnly; }
void
setReadWrite(
int
value) { readWrite = value; }
int
getReadWrite()
const
{
return
readWrite; }
void
setWriteOnly(
int
value) { writeOnly = value; }
private
:
int
noAccess;
// 禁止访问这个int
int
readOnly;
// 可以只读这个int
int
readWrite;
// 可以读/写这个int
int
writeOnly;
// 可以只写这个int
};
|
功能分离 functional abstraction: 如果用函数来实现对数据成员的访问, 内部可以用一段算法来更新这个数据成员, 而用户完全不需要知道;
e.g. 假设写一个自动化仪器检测汽车行驶速度; 每辆车行驶过来时, 计算出的速度添加到一个集中了当前所有汽车速度数据的集合中:
1
2
3
4
5
|
class
SpeedDataCollection {
public
:
void
addValue(
int
speed);
// 添加新速度值
double
averageSoFar()
const
;
// 返回平均速度
};
|
实现averageSoFar(), 1)方法是用类的数据成员来保存当前收集的所有速度数据的运行平均值; 2)方法是在averageSoFar每次被调用时才检查集合并计算结果;
方法1) 使得每个SpeedDataCollection对象更大, 必须为保存运行值的数据成员分配空间; 但实现高效, 返回数据的函数是内联的;
方法2) 运行相对慢, 每次调用都要计算平均值, 但每个对象都相对小;
在内存紧张的机器里[嵌入式?]或者在不频繁计算平均值的程序里, 2)相对好些; 但在频繁获取平均值的程序里, 要求速度不管内存的话 1)更适合; 重要的是, 用成员函数来访问平均值, 就可以使用任意的方法, 具有很大的灵活性 [有点牵强...接口调用的可能是Imp的函数, 也可能是返回Imp的成员, 效率和内存都不是问题];
Note 要把数据成员隐藏在功能之外, 就要避免在public接口里放数据成员, 一致性+访问控制;
条款21 尽可能使用const
const-对象不能被修改, 编译器会实施这种约束;
const在类外面, 可以用于全局或名字空间常量, 在类内部可以用于静态和非静态成员;
对于指针, 可以指定指针本身为const, 也可以指定指针所指的数据为const, 或者同时都const;
1
2
3
4
|
char
*p =
"Hello"
;
// 非const 指针,非const 数据
const
char
*p =
"Hello"
;
// 非const 指针, const 数据
char
*
const
p =
"Hello"
;
// const 指针, 非const 数据
const
char
*
const
p =
"Hello"
;
// const 指针, const 数据
|
Note const出现在*左边, 指针指向的数据为常量; 在*右边, 指针本身为常量;
const放在类型名前后都一样:
1
2
|
void
f1(
const
Widget *pw);
// 指向Widget 常量对象的指针
void
f2(Widget
const
*pw);
// 同f2
|
在函数声明中, const可以修饰函数的返回值, 参数或整个函数(成员函数);
让函数返回常量值可以在不降低安全性和效率的情况下减少用户的出错率;
e.g.
1
|
const
Rational operator*(
const
Rational& lhs,
const
Rational& rhs);
|
如果operator*的返回值不是const对象, 用户可能出现"对返回值赋值"这样的操作:
1
2
|
Rational a, b, c;
(a * b) = c;
// 对a*b 的结果赋值
|
如果a b c是固定类型, 这样做显然不合法; 对运算结果赋值make no sense;
Note 好的用户自定义类型会避免不符合直觉, 与固定类型不兼容的行为;
const参数, 运作和局部const对象一样;
const成员函数, 指明哪个成员函数可以在const对象上被调用 [成员变量不可修改]
Note const成员函数可以实现重载; const对象和非const对象调用重载;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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[]
|
>通过const重载operator[], 可以对const和非const String进行不同处理; [函数的声明和定义都需要加上const]
1
2
3
4
5
6
|
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
|
>这里的错误是因为const对象调用的operator[]返回了const char&, 不可赋值;
Note 非const operator[]的返回类型必须是一个char的引用, 不可以是char本身;
如果operator[]返回的是char自身: s[0] = 'x'; 无法通过编译; 因为修改一个"返回值为固定类型"的函数的返回值是非法的; 即使合法, C++通过值(不是引用)来返回对象的内部机制决定, s.data[0]的一个拷贝会被修改, 而不是s.data[0]自己; [值传递, 编译器自动拷贝]
一个成员函数为const的确切含义是什么: 1)数据意义上的const (bitwise constness)和 2)概念意义上的const (conceptual constness);
1)的坚持者认为, 只有当成员函数不修改对象的任何数据成员(静态数据成员除外)时, 不修改任何一个比特bit时, 成员函数才是const的; 1)的好处是可以容易地检测到违反bitwise constness的规定的事件: 编译器只要寻找是否有对数据成员的赋值; 1) 正是C++对const问题的定义: const成员函数不允许修改所在对象的数据成员;
可是很多不遵守bitwise constness的成员函数也可以通过bitwise测试; 一个"修改了指针所指向的数据"的成员函数, 其行为违反了1)的定义, 但如果对象中包含这个指针, 函数也是bitwise const的, 编译能通过, 和我们的直觉不符:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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"
|
>值可以被修改;
这导致conceptual constness观点的引入, 2)的坚持者认为, const成员函数可以修改它所在对象的一些数据bits, 但只有在用户不发觉的情况下;
e.g. String类想保存对象每次被请求时数据的长度:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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定义; 编译无法通过;
Solution:
1) 利用C++标准组织提供的方案: 关键字mutable, 对非静态数据成员使用mutable时, 这些成员的"bitwise constness"限制会被解除;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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;
2) 类C的成员函数中, this就好象指针经过声明:
1
2
|
C *
const
this
;
// 非const 成员函数中
const
C *
const
this
;
// const 成员函数中
|
编译器不支持mutable的情况下, 就只有把this的类型从const C* const改成 C* const; 通过初始化一个局部变量指针, 指向this所指的对象来间接实现;
1
2
3
4
5
6
7
8
9
10
|
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;
}
|
>如果this所指的对象原本就声明为const, 那么消除const(const_cast)就可能导致不可知的后果;
3) 通过类型转换消除const是安全的:
将一个const对象传递到一个非const参数的函数中, 同时确定参数在函数内部不会被修改;
e.g. 已知有些库错误地声明了strlen(): size_t strlen(char *s); 对于const char*类型的指针参数, 调用函数将不合法;
1
2
|
const
char
*klingonGreeting =
"nuqneH"
;
// "nuqneH"即"Hello"
size_t
length =
strlen
(
const_cast
<
char
*>(klingonGreeting));
|
>传递参数时强制转换;
Note 确定被调用的函数不会修改参数所指的数据时, 才能保证安全性;