条款27 如果不想使用隐式生成的函数就要显式地禁止
假设想写一个类模板Array, 它生成的类除了可以进行上下限检查外, 其他行为和C++标准数组一样; 设计中面临的一个问题是怎么禁止Array对象之间的赋值操作;
Note 对标准C++数组来说赋值是非法的;
1
2
3
|
double values1[10]; double values2[10]; values1
= values2; //
错误! |
如果你不想使用某个函数, 只需简单地把它从类中移出;
Note 赋值运算符是比较特别的成员函数, 当你没有写这个函数时, C++会帮你写一个;
Solution: 声明这个函数(operator=), 声明为private; [无需定义]; 显式地声明一个成员函数, 可以防止编译器的自动生成版本, 声明为private, 防止其他人调用;
但是这个方法还不够安全, 成员函数和友元函数还是可以调用私有函数, 如果你不去定义(实现)这个函数的话, 当无意间调用了这个函数时, 程序在链接时就会报错;
e.g. 对于Array来说, 模板定义可以像这样:
1
2
3
4
5
6
7
|
template < class T> class Array
{ private : //
不要定义这个函数! Array&
operator=( const Array&
rhs); ... }; |
>当用户试图对Array对象执行赋值操作时, 编译器就会报错, 当你无意间在成员或友元函数中调用它时, 链接器就会提醒;
这个例子适用于所有条款45中介绍的每一个编译器会自动生成的函数; 实际应用中, 赋值和拷贝构造函数具有行为上的相似性, 这意味着大多数时候你想禁止其中一个时, 也要禁止另一个;
条款28 划分全局名字空间
全局空间最大的问题是它本身只有一个; 在大型软件项目中, 经常有人把定义的名字都放在全局空间, 从而不可避免地导致名字冲突;
e.g. library1.h定义了一些常量: const double LIB_VERSION = 1.204; 类似地, library2.h也定义了: const int LIB_VERSION = 3;
当某个程序同时包含library1.h和library2.h的时候就会出问题; 对于这类问题, 只能自己编辑头文件来消除名字冲突; [还可以骂两句, 搞搞破坏, 发泄发泄之类的]
作为程序员, 你可以尽力使自己写的程序库不给别人带来这些问题; 例如, 可以预先想一些不大可能造成冲突的某种前缀, 加载每个全局符号之前, 这样组合的标识符会看起来比较奇怪;
比较好的方法是使用C++ namespace; namespace本质上和使用前缀的方法一样, 只是避免了要写前缀, 避免别人看到的是强行加上前缀的名字;
e.g. 前缀:
1
2
3
|
const double sdmBOOK_VERSION
= 2.0; //
在这个程序库中, 每个符号以"sdm"开头 class sdmHandle
{ ... }; sdmHandle&
sdmGetHandle(); //
为什么函数要这样声明?参见条款47 |
namespace:
1
2
3
4
5
|
namespace sdm
{ const double BOOK_VERSION
= 2.0; class Handle
{ ... }; Handle&
getHandle(); } |
用户可以通过三种方法访问名字空间里的符号:
1) 将名字空间的所有符号全部引入到某一用户空间; 2) 将部分符号引入到某一用户空间; 3) 通过修饰符显式地一次性使用某个符号;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
void f1() { using namespace sdm; //
使得sdm 中的所有符号不用加修饰符就可以使用 cout
<< BOOK_VERSION; //
解释为sdm::BOOK_VERSION Handle
h = getHandle(); //
Handle 解释为sdm::Handle, getHandle 解释为sdm::getHandle } void f2() { using sdm::BOOK_VERSION; //
使得仅BOOK_VERSION 不用加修饰符就可以使用 cout
<< BOOK_VERSION; //
解释为sdm::BOOK_VERSION Handle
h = getHandle(); //
错误! Handle 和getHandle都没有引入到本空间 } void f3() { cout
<< sdm::BOOK_VERSION; //
使得BOOK_VERSION在本语句有效 double d
= BOOK_VERSION; //
错误! BOOK_VERSION不在本空间 Handle
h = getHandle(); //
错误! Handle 和getHandle 都没有引入到本空间 } |
Note 有些名字空间没有名字, 没有命名的名字空间一般用于限制名字空间内部元素的可见性, M31;
名字空间带来的好处之一: 潜在的二义性不会再造成错误(条款26); 从许多不同的名字空间引入名一个符号名不会造成冲突; (还没有使用这个符号的状态下)
e.g. 除了sdm外, 还要使用:
1
2
3
4
5
|
namespace AcmeWindowSystem
{ ... typedef int Handle; ... } |
只要不引用符号Handle, 使用sdm和AcmeWindowSystem时就不会有冲突, 假如真的要引用, 可以明确地指明是哪个名字空间的Handle:
1
2
3
4
5
6
7
8
9
|
void f() { using namespace sdm; //
引入sdm 里的所有符号 using namespace AcmeWindowSystem; //
引入Acme 里的所有符号 ... //
自由地引用sdm和Acme 里除Handle 之外的其它符号 Handle
h; //
错误! 哪个Handle? sdm::Handle
h1; //
正确, 没有二义 AcmeWindowSystem::Handle
h2; //
也没有二义 } |
假如用常规的基于头文件的方法来做, 只是简单地包含sdm.h和acme.h, 由于Handle有多个定义, 编译无法通过;
C++标准库几乎所有的东西都存在于名字空间std中; 它以一种直接的方式影响到你: C++提供了看起来有趣的, 没有扩展名的头文件: <iostream>, <string>等(条款49);
在一些古老的编译器中, 可以用struct来近似实现namespace: 先创建一个结构来 保存全局符号名, 然后将这些全局符号名作为静态成员放入结构中:
1
2
3
4
5
6
7
|
//
用于模拟名字空间的一个结构的定义 struct sdm
{ static const double BOOK_VERSION; class Handle
{ ... }; static Handle&
getHandle(); }; const double sdm::BOOK_VERSION
= 2.0; //
静态成员的定义 |
[内部类]
现在, 如果有人想访问这些全局符号名, 只要简单地在他们前面加上结构名作为前缀:
1
2
3
4
5
|
void f() { cout
<< sdm::BOOK_VERSION; sdm::Handle
h = sdm::getHandle(); } |
如果全局范围内实际上没有名字冲突, 用户会觉得加修饰符麻烦而多余;
让用户选择使用它们或忽略的办法:
对于类型名, 可以用类型定义 typedef来显式地去掉空间引用: e.g. 假设结构s(模拟的名字空间)内有个类型名T, 可以使用typedef来使得T成为S::T的同义词: typedef sdm::Handle Handle;
对于结构中的每个(静态)对象X, 可以提供一个(全局)引用X, 初始化为S::X; const double& BOOK_VERSION = sdm::BOOK_VERSION; (条款47) 处理函数的方法和处理对象一样, 要注意即使定义函数的引用是合法的, 但代码的维护者会更喜欢使用函数指针:
1
2
|
//
getHandle 是指向 sdm::getHandle 的 const 指针 (见条款21) sdm::Handle&
(* const getHandle)()
= sdm::getHandle; |
Note getHandle是一个常量指针; 因为不允许用户将指针指向别的东西;
定义函数的引用:
1
2
|
//
getHandle 是指向sdm::getHandle 的引用 sdm::Handle&
(&getHandle)() = sdm::getHandle; |
除了初始化的方式外, 函数的引用和函数的常指针在行为上完全相同;
有了类型定义和引用, 不会遇到全局名字冲突的用户就会使用没有修饰符的类型和对象名; 那些有全局名字冲突的用户就忽略类型和引用的定义, 以待修饰符的符号名取代; 要注意不是所有用户都想使用简写名, 所以要把类型定义和引用放在单独的头文件中, 不要把它和(模拟namespace)结构的定义混在一起;
struct只是namespace的相似应用, 但实际上很多方面有欠缺; 明显的一点是对运算符的处理, 如果运算符被定义为结构 的静态成员, 他就只能通过函数调用来使用, 而不能像常规的运算符所设计的自然的中缀语法使用;
1
2
3
4
5
6
7
8
9
10
11
12
13
|
//
定义一个模拟名字空间的结构,结构内部包含Widgets 的类型和函数。Widgets 对象支持operator+进行加法运算 struct widgets
{ class Widget
{ ... }; //
参见条款21:为什么返回const static const Widget
operator+( const Widget&
lhs, const Widget&
rhs); ... }; //
为上面所述的Widge 和operator+建立全局(无修饰符的)名称 typedef widgets::Widget
Widget; const Widget
(* const operator+)( const Widget&, const Widget&); //
错误! operator+不能是指针名 Widget
w1, w2, sum; sum
= w1 + w2; //
错误! 本空间没有声明参数为Widgets 的operator+ sum
= widgets::operator+(w1, w2); //
合法, 但不是 "自然"的语法 |
---类的设计和声明 End---