条款01:视C++为一个语言联邦
==> 请记住:
(1)C++高效编程守则视状况而变化,取决于你使用C++的哪一部分。
==> 解析:
使用C++的模式可分为四种:
C、 Object-Oriented C++、 Template C++、 STL。
条款02:尽量以const,enum,inline替换#define
==> 请记住:
(1)对于单纯常量,最好以 const 对象 或 enum 替换 #define。
(2)对于形似函数的宏(macros),最好改用 inline函数 替换 #define。
==> 解析:
1. 使用const替换#define的场景:
GCC编译的 4个阶段:
① 预处理: .c --> .i //#include、#define等将在预处理阶段展开
② 编译: .i --> .s //将C语言翻译为汇编语言
③ 汇编: .s --> .o //使用汇编文件生成目标文件
④ 链接: .o + .o --> a.out //多个.o目标文件链接生成可执行文件
对于使用#define 与 const 定义一个变量的不同形式:
#define ASPECT_RATIO 1.653
const double ASPECT_RATIO = 1.653;
使用const替换#define的含义是 “宁可以编译器替换预处理器”,因为#define定义的变量将会在预处理阶段进行替换 而不会出现被编译器处理,因此当编译报错时,错误信息只会提示“1.653”而不会提示“ASPECT_RATIO”变量名,这给调试带来了困难。
2. 使用enum替换#define的场景:
另外一种情况,当const变量是类的成员时,将不能用来表示数组的大小:(无论是public还是private成员都不行)
//错误:const变量作为成员时不能用来表示数组大小
class GamePlayer {
private:
const int NumTurns = 5;
int scores[NumTurns];
};
//正确:static-const 类型的变量作为成员时可以用来表示数组大小
class GamePlayer {
private:
static const int NumTurns = 5;
int scores[NumTurns];
};
//错误:static非const类型的变量不能用来表示数组大小
class GamePlayer {
private:
static int NumTurns = 5;
int scores[NumTurns];
};
然而,如果const变量不是作为类的成员,而是在类外的普通变量时,可以用来表示数组大小:
int main() {
const int size = 5;
int array[size]; //正确,编译通过
}
这种情况下,可以用 enum类型来替换#define:
class GamePlayer {
private:
enum { NumTurns = 5 };
int scores[NumTurns];
};
enum的行为更像#define而不像const,取一个enum的地址不合法,而取一个const的地址是合法的。
在C/C++中,enum枚举类型被当做 int 或者 unsigned int 类型来处理。
enum类型的几个特点:
(1)不论enum类型中包含有多少个元素,其大小都与一个int型大小相等;
(2)第一个枚举类型的默认值为整型的0,后续枚举成员的值在前一个成员上加1。(如果将enum中某个成员的值改写,则跟在它后面的元素继续在此基础上累加1);
(3)定义enum类型和变量的方式与struct、union类似。
//sizeof(enum) = sizeof(int) :
enum DAY {
MONDAY,
TUESDAY = 2,
WEDSDAY,
THURSDAY = 10,
FRIDAY,
SATURDAY,
SUNDAY
};
int main() {
cout << sizeof(int) << ' ' << sizeof(DAY) << endl;
cout << MONDAY << ' ' << WEDSDAY << ' ' << FRIDAY << endl;
}
//输出结果:
//4 4
//0 3 11
//第一种定义 enum变量的方式:
enum DAY {
} day; //既定义enum类型,又定义enum变量
//第二种定义 enum变量的方式:
enum DAY {
} ; //只定义了enum类型
enum DAY day;
//第三种定义 enum变量的方式:
typedef enum DAY {
} DAY;
DAY day;
//第四种定义 enum变量的方式:
enum {
MONDAY = 1,
TUESDAY
} day; //此时day默认值为0,后续可再给enum变量day赋值:day = MONDAY;
enum枚举类型 在实际业务中的使用举例:
typedef enum en_socktype {
SOCK_NON_TYPE = 0,
SOCK_TCP_SERVER,
SOCK_TCP_SESSION,
SOCK_TCP_CLIENT,
SOCK_UDP_SESSION
} EN_SOCKTYPE; //定义一个enum枚举类型:EN_SOCKTYPE
class CBaseIOStream {
public:
CBaseIOStream();
void SetSockType(EN_SOCKTYPE entype) { m_socktype = entype; }
EN_SOCKTYPE GetSockType() const { return m_socktype; }
private:
EN_SOCKTYPE m_socktype; //定义一个EN_SOCKTYPE枚举类型的变量 m_socktype
};
CBaseIOStream::CBaseIOStream() : m_socktype(SOCK_NON_TYPE) {} //在构造函数中对枚举类型变量赋值
如果不使用enum枚举类型,使用#define的形式将使代码看起来没那么简洁直观:
#define SOCK_NON_TYPE 0
#define SOCK_TCP_SERVER 1
#define SOCK_TCP_SESSION 2
#define SOCK_TCP_CLIENT 3
#define SOCK_UDP_SESSION 4
···
enum相较于 #define 的优势是:
(1)enum类型变量的展开发生在编译期,而#define定义的变量在预处理期进行替换,变量名无法在编译出错时体现;
(2)enum使数据更简洁、更易读,且可以定义枚举型变量,变量的值可修改。
3. 使用inline替换#define的场景:
使用 #define 实现宏的作用是:避免函数调用带来的额外开销。
但#define实现宏时要格外小心,保证所有的实参都要加上小括号,例如:
#define CALL_WITH_MAX(a, b) f( (a) > (b) ? (a) : (b) )
且#define实现的宏容易引起意想不到的错误,例如:
//同样调用宏CALL_WITH_MAX,但由于第二个参数的值不同,引起a被累加的次数不同,造成所得结果不同
int a = 5, b = 0;
CALL_WITH_MAX(++a, b); //a被累加两次
CALL_WITH_MAX(++a, b + 10); //a被累加一次
使用 模板化的inline函数同样可以达到避免函数调用的效果,同时避免了上述提到的#define实现的宏的缺点。
并且,inline内联函数具有一个#define无法实现的功能:
inline可以在类的private作用域内声明,而#define无法做到访问权限控制。
例如:
template <typename T>
inline void callWithMax(const T& a, const T& b) {
f(a > b ? a : b);
}
条款03:尽可能使用const
==> 请记住:
(1)将某些东西声明为 const 可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
(2)编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”(conceptual constness)。
(3)当 const 和 non-const 成员函数有着实质等价的实现时,令 non-const 版本调用const版本可避免代码重复。
条款04:确定对象被使用前先被初始化
==> 请记住:
(1)为内置型对象进行手工初始化,因为C++不保证初始化它们。
(2)构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
(3)为免除“跨编译单元之初始化次序”问题,请以local static对象 替换 non-local static对象。
==> 解析:
(1)对于局部变量C++不会对其进行初始化,这可能会导致未定义的行为,因此需要保证对内置类型进行手动初始化;
(2)赋值操作相当于在初始化后再对对象进行赋值,这会导致程序的效率降低,因此最好使用构造函数的初始化值列表进行初始化,而是在构造函数体内进行赋值操作;
(3)C++对定义于不同编译单元(源文件+头文件)内的non-local static对象(非局部静态变量,可以理解为全局变量)的初始化相对次序并无明确定义。
这会导致的问题是:当两个以上的编译单元中的全局变量有依赖关系,例如某个源文件中的全局变量对象的初始化需要使用另一个编译单元中的全局对象,而它所用到的这个对象可能还没有被初始化,此时就可能会产生非预期的结果。
例如:
header.h
:
#include <unistd.h>
class FileSystem {
public:
FileSystem() {
num = 200;
}
size_t numDisks() const {
return num;
}
private:
size_t num;
};
source_a.cpp
:
#include "header.h"
FileSystem tfs; //定义一个全局变量,在默认初始化时会将num赋值为100
source_b.cpp
:
#include "header.h"
#include <iostream>
using namespace std;
extern FileSystem tfs; //声明tfs
class Dictory {
public:
Dictory() {
disks = tfs.numDisks();
}
void print() const {
cout << "disks: " << disks << endl;
}
private:
size_t disks;
};
Dictory d; //定义另一个全局变量,此全局变量的默认初始化依赖另一个全局变量tfs,而C++编译器不保证来自不同编译单元的全局变量的初始化顺序,所以这可能得到非预期结果
int main() {
d.print();
return 0;
}
如果使用编译命令:
g++ source_a.cpp source_b.cpp -o source -std=c++11
则输出结果是:
disks: 200
如果更换编译时源文件的摆放顺序:
g++ source_b.cpp source_a.cpp -o source -std=c++11
则输出结果是:
disks: 0
说明C++编译器是无法保证不同编译单元中的全局变量的初始化顺序的。
解决这类问题的方法是:
将全局对象放入到函数中,将其改为封装在函数内部的 static静态局部变量,利用静态局部变量在首次被调用时被初始化,并在主程序退出时被释放 的特性。
修改后:
source_a.cpp
:
#include "header.h"
//FileSystem tfs; //定义一个全局变量,在默认初始化时会将num赋值为100
FileSystem& tfs() {
static FileSystem fs;
return fs;
}
source_b.cpp
:
#include "header.h"
#include <iostream>
using namespace std;
//extern FileSystem tfs; //声明tfs
extern FileSystem& tfs();
class Dictory {
public:
Dictory() {
//disks = tfs.numDisks();
disks = tfs().numDisks();
}
void print() const {
cout << "disks: " << disks << endl;
}
private:
size_t disks;
};
Dictory d;
int main() {
d.print();
return 0;
}
修改后,可保证在调用tfs()函数一定会初始化fs变量,程序的输出结果会一直是:
disks: 200
另外,关于“多个cpp文件如何定义全局变量与常量”:
当多个源文件中需要同时引用一个全局变量时,全局变量的定义不能放在头文件中,这会引起“重复定义”的错误。正确的做法是将全局变量的定义放在某一个源文件中,然后在头文件中对其进行extern声明,其他源文件包含头文件即可。
(本质在于:变量只能被定义一次,但可以被声明多次)
例如:
错误实例:
//header.h
int a = 1; //头文件中定义全局变量a
//source_a.cpp
#include "header.h"
//source_b.cpp
#include "header.h"
int main() {
return 0;
}
编译报错:
duplicate symbol '_a'
正确做法:
//header.h
extern int a;
//source_a.cpp
#include "header.h"
int a = 1;
//source_b.cpp
#include "header.h"
#include <iostream>
using namespace std;
int main() {
cout << a << endl;
return 0;
}
输出结果:
1