引言
深入 C++ 的诸多设计细节,了解实际场景的最佳实践,以面向对象的方式重新认识 C++。
将 C++ 视作一系列的语言
Item 1: View C++ as a federation of languages
最初,C++ 只是 C 语言加上一些面向对象的特性,所以 C++ 的原名是 “C with Classes”。 现在的 C++ 已经逐渐成熟,成为一门 多范式的程序设计语言(multiparadigm programming language)。同时支持过程式、面向对象、函数式、泛型编程,以及元编程。
C++ 的灵活使得它在很多问题上并没有统一的规则,而是取决于具体的程序设计范式和当前架构的设计意图。这样的情况下,我们最好把 C++ 看做是一系列的编程语言,而非一种特定的编程语言。
C++ 有四种主要的子语言:
C
:C++ 是基于 C 设计的,你可以只使用 C++ 中 C 的那部分语法。此时你会发现你的程序反映的完全是C的特征:没有模板、没有异常、没有重载。Object-Oriented C++
:面向对象程序设计也是 C++ 的设计初衷:构造与析构、封装与继承、多态、动态绑定的虚函数。Template C++
:这是 C++ 的泛型编程部分,多数程序员很少涉及,但模板在很多情况下仍然很方便。另外 模板元编程(template metaprogramming)也是一个新兴的程序设计范式,虽然有点非主流。STL
:这是一个特殊的模板库,它的容器、迭代器和算法优雅地结合在一起,只是在使用时你需要遵循它的程序设计惯例。当然你也可以基于其他想法来构建模板库。
总之 C++ 并非单一的一门语言,它有很多不同的规则集。因而C++可以被视为四种主要子语言的集合,每个子语言都有自己的程序设计惯例。
C++ 程序设计的惯例并非一成不变,而是取决于你使用C++语言的哪一部分。例如, 在基于C语言的程序设计中,基本类型传参时传值比传引用更有效率。 然而当你接触Object-Oriented C++时会发现,传常量指针是更好的选择。 但是你如果又碰到了STL,其中的迭代器和函数对象都是基于C语言的指针而设计的, 这时又回到了原来的规则:传值比传引用更好。
避免使用 define
Item 2: Prefer consts, enums, and inlines to #defines
尽量使用常量、枚举和内联函数,代替 #define
。我们知道 #define
定义的宏会在编译时进行替换,属于模块化程序设计的概念。 宏是全局的,面向对象程序设计中破坏了封装。因此在 C++ 中尽量避免它!
接着我们具体来看 #define
造成的问题。
不易理解
众所周知,由于预处理器会直接替换的原因,宏定义最好用括号括起来。#define函数将会产生出乎意料的结果:
|
i
自加次数将取决于 j
的大小,然而调用者并不知情。宏的行为不易理解,本质上是因为宏并非 C++ 语言的一部分,它只是源代码的预处理手段。
不利于调试
宏替换发生在编译时,语法检查之前。因此相关的编译错误中不会出现宏名称,我们不知道是哪个宏出了问题。例如:
|
如果 alice
未定义,PERSON=bob;
便会出错:use of undeclared identifier ‘alice’。 然而我们可能不知道 alice
是什么东西,PERSON
才是我们定义的“变量”。
宏替换是在预处理过程中进行的,原则上讲编译器不知道宏的概念。然而,在现代的编译器中(例如Apple LLVM version 6.0), 编译器会记录宏替换信息,在编译错误中给出宏的名称:
test.cpp:8:5: error: use of undeclared identifier 'alice' |
于是,Meyers 提到的这个问题已经不存在了。然而作者的本意在于:尽量使用编译器,而不是预处理器。 因为 #define
并不是 C++ 语言的一部分。
enum
比 const
更好用
既然 #define
不能封装在一个类中,我们可以用 static const
来定义一个常量,并把它的作用于局限在当前类:
class C{ |
通常 C++ 要求所有的声明都给出定义,然而数值类型(char
, int
, long
)的静态常量可以只给声明。这里的 NUM
就是一个例子。 然而,如果你想取 NUM
的地址,则会得到编译错误:
Undefined symbols for architecture x86_64: |
因此如果你要取地址,那么就给出它的定义:
class C{ |
因为声明 NUM
时已经给定了初始值,定义时不允许再次给初始值。 如果使用 enum
,事情会简单很多:
class C{ |