Effective C++ 第二版 27)函数隐式生成 28)名字空间

本文介绍了如何在C++模板类中禁止赋值操作,并通过全局命名空间解决类名冲突的问题。包括声明私有赋值运算符、使用namespace避免全局符号冲突,以及如何在代码中导入和使用不同命名空间内的符号。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

条款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---

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值