如今的 c++ 已经是一个 多重泛型编程语言,一个同时支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式
的语言。将 c++ 看作一个联邦,主要包括四个部分:
这条条款实际上意味着,“宁可用编译器替换预编译器”。
#define ASPECT_RATIO 1.6
上面的这种语句当发生错误时,编译器产生的错误信息可能提到的时 1.6 而非 ASPECT_RATIO。当你查找错误时,这个举动
会徒增难度。
解决的方法是 使用常量替代 上述的宏定义:
const double AspectRatio = 1.6;
这种方式不仅解决了 编译器无法找到对应的记号的问题,同时也解决了宏定义可能产生多份复制的情况。
使用常量替换 #define 还需要讨论两个小问题。
因为常量定义通常被放在头文件中,所以为了防止程序某处对其内容进行修改,需要将指针声明为 const,不仅是所指之物。
const char* const authorName = "hello world";
或者你也可以选择使用某些对象进行替换,比如上例就可以使用 string 对象来实现
const std::string authorName("hello world");
为了将常量限制在 class scoop 中,且限制只存在一个常量。需要使用 static 关键字来实现。
class GamePlayer {
private:
static const int NumTurns = 5; // 常量声明式
int scores[NumTurns];
};
const int GamePlayer::NumTurns; // 常量定义式
c++ 要求你对你所使用的任何东西提供一个定义式。特别是 class 专属常量,又是 static 且为整数类型。这就需要特殊
处理。只要不取他们的地址,你可以只声明他们而不用定义。而如果你需要获取某个 class 专属常量的地址,或者编译器坚
持要看到一个定义式,你就必须提供上面最后这样类似的定义式。其中的定义式部分并没有提供特定的初值,因为在声明中已
经给出了初值,因此在定义式中不能再设置。
但是有时候,编译器可能不允许 “static 整型 class 常量完成 in class 初值设定”。而如果你又坚持要在编译期间知道
数组的大小,你可以改用所谓的 “the enum 可权充 ints 使用” 的技巧。
class GamePlayer {
private:
enum { NumTurns = 5 };
int scores[NumTurns];
};
这种 enum hack 方式值得学习,它具有几点好处:
通常希望使用 宏定义 的方式来实现一个看起来像函数的东西,因为不会产生调用带来的额外开销。但是有时候会产生更加难
以控制的局面。
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
int a = 5, b = 6;
CALL_WITH_MAX(++a, b); // 比较完成后,a = 7
CALL_WITH_MAX(++a, b + 10); // 比较完成后,a = 8
上面这段代码显然不会按照原有的设想运行,但是如果你使用 template inline 函数来书写则不会产生这种结果。
template <typename T>
inline void callWithMax(const T& a, const T& b) {
f(a > b ? a : b);
}
除了上面这点保证,inline 也会保证遵守作用域原则,而 #define 则不会。
const 允许你指定一个语义约束,而编译器会强制实施这项约束。
关于指针的 const 使用:
int a = 10;
int b = 11;
const int* p = &a;
int* const q = &a;
*p = 12; // 错误
p = &b; // 正确
*q = 12; // 正确
q = &b; // 错误
同样的,使用 STL 迭代器,也需要区分不同的 const:
std::vector<int> vec;
const std::vector<int>::iterator iter = vec.begin();
std::vector<int>::const_iterator cIter = vec.begin();
iter++; // 错误
cIter++; // 正确
*iter = 12; // 正确
*cIter = 12; // 错误
上述代码中前者实际相当于 iter 是不可更改的,即 int* const
类型,而后者则是 const int*
类型,即被指物是
不可更改的。
在函数声明中使用 const 进行限制,包括函数返回值、参数、函数自身,可以降低因使用者错误使用而造成的意外。
class Rational{
public:
const Rational operator*(const Rational& lhs, const Rational& rhs);
};
Rational a, b, c;
...
if (a * b = c) {...} // == 意外书写为了 =
在上述的代码中,使用 const 来限定返回值可以防止 (a * b) = c
这类代码通过编译,因为这显然是不合理的。
将 const 实施于成员函数,是为了确认该成员函数可作用 const 对象。
[!tip]
两个成员函数,如果只是常量性不同,可以被重载
class TextBlock {
public:
const char& operator[](std::size_t position) const {
return text[position];
}
char& operator[](std::size_t position) {
return text[position];
}
private:
std::string text;
};
void print(const TextBlock& ctb) {
std::cout << ctb[0];
}
TextBlock tb("hello");
TextBlock ctb("hello");
tb[0] = 'x'; // 正确
ctb[0] = 'x'; // 错误
对于这个观点,存在两个流行概念:bitwise constess 和 logical constness
此阵营的人认为,成员函数只有不更改对象之任何成员变量时才可以说是 const 的。但是这也存在漏洞,比如将数据
存储为 char* 而非 string 的话就可能会产生问题。
class CTextBlock {
public:
char& operator[] (std::size_t position) const {
return pText[position];
}
private:
char* pText;
};
尽管 operator[] 中并没有对任何数据进行更改,但是却返回了一个指向对象内部值的 reference。如果调用者在后续对返
回值进行更改,仍然是可行的,但是却违背了 const 的约定。
这一派则主张,一个 const 成员函数可以修改它所处理的对象内的某些 bits,但是只有在用户侦测不出的情况下才可以。
class CTextBlock {
public:
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength;
mutable bool lengthIsValid;
};
std::size_t CTextBlock::length() const {
if(!lengthIsValid) {
textLength = std::strlen(pText);
lengthIsValid = true;
}
return textLength;
}
上面这段代码显然不符合 bitwise const,因为 length 函数改变了其中的值,但是却符合 logical constness。此外,
其中使用 mutable 关键字进行限定,其目的是当编译器坚持 bitwise const 时,使用该关键字允许释放掉 non-static
成员变量的 bitwise constness 约束。
随着代码越来越复杂,const 与 non-const 代码之间可能出现大量的重复内容,比如:
class TextBlock {
public:
const char& operator[] (std::size_t position) const {
// 边界检查
// 日志记录
// 数据完整性校验
return text[position];
}
char& operator[] (std::size_t position) {
// 边界检查
// 日志记录
// 数据完整性校验
return text[position];
}
private:
std::string text;
};
上面这段代码通过使用 non-const 版本调用 const 函数的方式来进行简化,能够大大降低代码的复杂度。
class TextBlock {
public:
const char& operator[] (std::size_t position) const {
// 边界检查
// 日志记录
// 数据完整性校验
return text[position];
}
char& operator[] (std::size_t position) {
return const_cast<char&>(
static_cast<const TextBlock&> (*this)[position]
);
}
private:
std::string text;
};
如果反过来使用 const 函数调用 non-const 函数,怎么样?不要这么做,因为 const 成员函数承诺不对其中的对象进行
逻辑状态的更改,如果反向调用则会产生修改的风险。
关于 “将对象初始化”,c++ 似乎反复无常。但是现在,我们有一些规则,描述“对象的初始化动作何时一定发生,何时不一定
发生”。
通常,如果使用 c part of c++ 而且初始化可能招致运行期成本,那么就不会保证发生初始化。但是当你进入 non-c part
of c++,则具有保证。
这似乎是个无法决定的状态,最佳的处理方法就是永远在使用对象之前先将它初始化。
对于内置类型以外的其他东西,初始化责任则落在了构造函数身上。就需要确保每一个构造函数都将对象的每一个成员初始化。
class PhoneNumber {};
class ABEntry {
public:
ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones);
private:
string theName;
string theAddress;
list<PhoneNumber> thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry (const string& name, const string& address, const list<PhoneNumber>& phones)
: theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) {}
上面使用 member initialization list 来实现初始化,而非在 constructor 内使用赋值来进行初始化操作。因为,前
者发生在 constructor 开始执行本体和 default constructor 之前。这种方式效率更高。
许多 classes 拥有多个构造函数,每个构造函数都有自己的成员初值列。如果这种 classes 存在许多成员变量,多份成员
初值列的存在就会导致重复。这种情况下,可以将部分 “赋值表现和初始化一样好” 的成员变量放置在 constructor 进行
进行操作。并且可以将重合部分提取为 private 的“伪初始化”函数。
关于 “成员初始化次序”,c++ 中有着严格的次序规定:base classes 更早于 derived classes 被初始化。成员变量总是
以声明次序进行初始化,无论成员初值列的次序如何。
如果你能够按照上面的这些约束进行编码,那么只剩最后一件值得关心的事情 “不同编译单元内定义之 non-local static
对象” 的初始化次序。
static 对象是指 global 对象,定义于 namespace 作用域内的对象,在 class 内、函数内、以及在 file 作用域内被声
明为 static 的对象。他们的寿命从构造出来直至程序结束为止。其中在函数内的 static 对象被称为 local static 对象。
而其他则都被称为 non-local static 对象。
编译单元是指产生单一目标文件的那些源码。
// 1.cpp
class FileSystem {
public:
std::size_t numDisk() const;
};
extern FileSystem tfs;
// 2.cpp
class Director {
public:
Directory(params) {
std::size_t disks = tfs.numDisks();
}
};
Directory tempDir(params);
上面这两段代码,其中 tfs 以及tempDir 是 non-local static 对象,而且存放在不同的源码文件中。从中可以看到二者
存在直接关系,即 tempDir 需要借助 tfs 完成初始化操作。但是 c++ 没有规定 non-local static 对象在不同编译单元
内定义初始化的顺序,因此很容易产生错误。
解决办法十分简单,通过 singleton 模式,将 non-local static 对象转换成 local static 对象,即将前者放入一个
专属函数中实现。
// 1.cpp
class FileSystem {
public:
std::size_t numDisk() const;
};
FileSystem& tfs() {
static FileSystem fs;
return fs;
}
// 2.cpp
class Director {
public:
Directory(params) {
std::size_t disks = tfs().numDisks();
}
};
Directory& tempDir() {
static Directory td;
return td;
}
这种结构下,函数十分单纯,可以通过 inline 来实现,尤其是如果他们被频繁调用的话。
[!note]
在 more effective c++ 中,作者在条款26中提到,不要产生内含 local static 对象的 inline non-member functions。
主要是因为 inline non-member functions 存在内部连接,可能会将 local static 对象复制多份。但是,其中注释
也解释道自 1996年7月,ISO/ANSI 委员会便将 inline 函数的默认连接由内部改为了外部。因此现在可以使用 inline
来标记存在 local static 对象的 non-member functions。