1、使用命名空间特性会产生冗长的符号名,尤其是那些包含在几层嵌套命名空间中的符号。好在C++提供了使用using关键字,是的命名空间里的符号更加容易使用:
using namespace std;
string str("Look, no std::");
而更好的方式(因为它限定了引入全局命名空间的符号范围)是:
using std::string;
string str("Look, no std::");
然而,任何时候都不应该在公用API头文件的全局作用域内使用using关键字!这样做会导致所引用命名空间的全部符号在全局命名空间可见。这便破坏了使用命名空间的初衷。如果你希望在头文件中引用另一个命名空间的符号,应当使用完整限定名,例如:std::string。
2、“三大件”(Big Three)规则:析构函数、复制构造函数和赋值操作符这3个成员函数始终应该一起出现。
如果类分配了资源,则应该遵循“三大件”规则,同时定义析构函数、复制构造函数和赋值操作符。
3、为那些只接受一个参数的构造函数添加explicit是一种好的做法,用于阻止构造对象时特定的构造函数被隐式调用。
4、讨论将操作符定义为自由函数还是方法的最佳做法。
C++标准要求将下列操作符声明为成员方法,以确保它们接受左值作为其第一个操作数:
1> = 辅助;
2> [] 下标;
3> -> 类成员访问;
4> ->* 指针成员选择;
5> () 函数调用;
6> (T) 类型转换,即C风格的转换;
7> new/delete
其余的可重载操作符既可以定义为自由函数,也可以定义为类成员函数。站在良好API设计的角度,建议在定义操作符时尽量选择自由函数,而非类成员方法,原因如下。
1> 操作符对称性。如果二元操作符被定义为类的方法,它就必须有一个能用作左操作数的对象。以*操作符为例,这意味着用户能够编写类似currency*2这种表达式,(前提是已经定义了接受int类型的隐式构造函数或特定的*操作符。)但不能编写2*currency这种表达式,因为2.operator*(currency)没有意义。这破坏了用户期望的操作符交换性,即x*y应该和y*x等价。把*操作符声明为自由函数能带来很多益处。这样,如果没有显示声明构造函数,自由函数可以为左操作数和右操作数进行隐式类型转换。
2>降低耦合度。自由函数不能访问类的私有细节。由于它只能访问公有方法,因此降低了与类的耦合度。这是第2章所述的通用API设计理念:将不必访问私有成员或受保护的成员的类方法定义为自由函数,从而降低API的耦合度。
总结:除非操作符必须访问私有成员或受保护的成员,或它是=、[]、->、->*、()、(T)、new、delete其中之一,否则应该尽量将其声明为自由函数。
5、为类添加操作符
+=操作符会修改对象的内容,而我们知道所有的成员变量应该都是私有的,因此很可能需要将+=操作符定义为成员方法。另外,由于+操作符不会修改左操作数,从而也就不必访问私有成员,所以可以定义为自由函数。把这个操作符定义为自由函数还可以满足对称性的要求。实际上,+操作符可以通过+=操作符实现,这允许我们重用代码并且提供更加一致的行为。同时也减少了派生类需要重载的方法数量。
Currency operator + (const Currency& lhs, const Currency& rhs)
{
return Currency(lhs) += rhs;
}
同样的技巧可以应用于其他算术操作符,比如-、-=、*、*=、/以及/=。例如,*=可以实现成员函数,而*可以使用*=操作符实现为自由函数。
至于关系操作符==、!=、<、<=、>及>=,必须实现为自由函数以保证对称性。在Currency类这个例子中,可以使用公有的GetValue()方法实现这些操作符。但如果这些操作符还需要访问对象的私有成员,那么有一种方法可以解决这个矛盾。这种情况下,可以提供公有方法来测试等于和小于条件,比如IsEqualTo()和IsLessThan()。进而可以用这两种基本函数实现所有关系操作符。
bool operator == (const Currency& lhs, const Currency& rhs)
{
return lhs.IsEqualTo(rhs);
}
bool operator != (const Currency& lhs, const Currency& rhs)
{
return !(lhs == rhs);
}
bool operator < (const Currency& lhs, const Currency& rhs)
{
return lhs.IsLessThan(rhs);
}
bool operator <= (const Currency& lhs, const Currency& rhs)
{
return !(lhs > rhs);
}
bool operator > (const Currency& lhs, const Currency& rhs)
{
return rhs < lhs;
}
bool operator >= (const Currency& lhs, const Currency& rhs)
{
return rhs <= lhs;
}
最后考虑的操作符是<<, 我会把它用于流的输出(而非移位)。流操作符应该声明为自由函数,因为第一个参数是流对象。你也可以使用公有的GetValue()方法达到此目的。如果流操作符需要访问类的私有成员,那么可以为<<创建公有的ToString()方法供其调用,从而避免使用友元。
6、转换操作符
转换操作符用于定义将对象自动转换成不同类型的对象。典型的例子是定义定制的字符串类,它可以传递给接收const char*指针的函数,比如C标准库函数strcmp()和strlen()。
class MyString
{
public:
MyString(const char* string);
//把MyString转换成C风格的字符串
operator const char* () { return mBuffer; }
private:
char* mBuffer;
int mLength;
};
MyString mystr("Haggis");
int same = strcmp(mystr, "Edible");
int len = strlen(mystr);
注意,转换操作符没有指定返回值类型。这是因为编译器可以根据操作符名字推断出类型,而且转换操作符没有参数。
总结:给类添加转换操作符,从而利用自动类型强制转换。
7、避免使用#define定义常量
#define预处理指令本质上是用一个字符串替换源码中的另一个字符串。例如:
#define SETUP_NOISE(i, b0, b1, r0, r1) \
t = vec[i] + 0x1000; \
b0 = (lltrunc(t)) & 0xff; \
b1 = (b0 + 1) & 0xff; \
r0 = t - lltrunc(t); \
r1 = r0 - 1.f;
无论如何都不应该在公有API头文件中用这种方式使用#define, 因为它会泄露实现细节。如果想在.cpp文件中使用这一技巧,并且理解#define的所有特性,那么尽管去做,但是绝不要在公有头文件中这样做。
剩下的问题是使用#define为API指定常量,比如:
#define MORPH_FADEIN_TIME 0.3f
#define MORPH_IN_TIME 1.1f
#define MORPH_FADEOUT_TIME 1.4f
应该避免采用#define的这种用法(当然,除非你正在编写纯C API),原因如下所述。
1>没有指定类型。#define不涉及为定义的常量做任何类型检查。因此必须确保显示地指定定义的常量类型,以避免歧义,比如对单精度浮点型常量使用f后缀。如果将一个浮点常量定义为10, 那么某些情况下它会被假定为整型,继而引起意料之外的数学取整错误。
2>没有指定作用域。#define语句是全局的,不会被限制于诸如单个类的特定作用域。可以使用#undef预处理指令取消之前的#define,但这对于声明客户能够使用的常量而言没有什么意义。
3>没有访问控制。不能把#define标记为公有的、受保护的或私有的。它本质上是公有的。因此不能使用#define指定一个只能被你定义的某个基类的派生类访问的常量。
4>没有符号。前面给出的例子中,像MORPH_IN_TIME这样的符号名可能会被预处理器从代码中剥离,这样,编译器就无法看见这个名字,也就不能将其加入符号表。当客户试图调试使用API的代码时,这可能会隐藏一些重要信息,因为客户在调试器中只能看到一些没有任何描述性名字的常量值。
与使用#define声明API常量相比,更可取的办法是声明const常量,虽然定义const变量在某种程度上可能会使客户代码更臃肿。这里仅给出#define示例的另一个更好的版本:
class Morph
{
public:
static const float FadeInTime;
static const float InTime;
static const float FadeOutTime;
...
};
这些常量的实际值在关联的.cpp文件中指定。(如果确实想让用户知道这些常量的值,可以在Morph类的API文档中告知这些信息。)注意,这种表示法不存在前面提到的任何问题:常量被表明为浮点型,作用域在Morph类中,显示标记为公有访问,并且会在符号表中生成表项。
总结:使用静态const数据成员而非#define表示类常量。
#define的另一个用途是为给定的变量提供一系列可能的值。例如,
#define LEFT_JUSTIFIED 0
#define RIGHT_JUSTIFIED 1
#define CENTER_JUSTIFIED 2
#define FULL_JUSTIFIED 3
可以通过enum关键字定义枚举类型,以便更好地表达这一语义。使用枚举使得类型更加安全,因为此时编译器会使用符号名而不是直接以整数设定枚举值(除非把int显示转换成枚举类型)。这也让传递非法值更加困难,比如在前面给出的例子中无法传递-1或者23。可以把前面的#define定义代码转换成枚举类型,如下所示:
enum JustificationType {
LEFT_JUSTIFIED,
RIGHT_JUSTIFIED,
CENTER_JUSTIFIED,
FULL_JUSTIFIED
};