Effective C++ (一) : 让自己习惯C++
让自己习惯C++
条款01:视C++为一个语言联邦
C++的主要的四个次语言:
-C。C++仍是以C为基础:区块(blocks),语句(statements),预处理器(preprocessor),内置数据类型(built-in data types),数组(array),指针(pointers)
-Object-Oriented C++。这部分是C with Classes所述求的:classes(包括构造函数和析构函数),封装(encapsulation),继承(inheritance),多态(polymorphism),virtual函数(动态绑定)....等等
-Template C++。这是C++的泛型编程部分。
-STL。STL是个template程序库。它对容器,迭代器,算法以及函数对象的规约有极佳的紧密配合与协调。
条款02:尽量以const,enum,inline替换成#define
#define ASPECT_RATIO 1.653
记号ASPECT_RATIO也许从未被编译器看见;也许在编译器开始处理源码之前它就被预处理器移走了,记号名称ASPECT_RATIO可能没进入记号表。解决之道是以一个常量替换上述的宏(#define):
const double AspectRatio = 1.653; //大写名称通常用于宏定义,这里改变名称写法。
作为语言常量,AspectRatio肯定会被编译器看到,会进入记号表。
以常量替换#define两种特殊情况:
1.定义常量指针,有必要将指针const。若要定义一个常量字符串,必须写const两次:
1
const char* const authorName = "amoscykl";
上述的authorName往往如下定义比较好:
2.定义class专属常量:将常量的作用域限制于class内,必须让它成为class的一个成员。为了确保此常量唯一,必须让它成为一个static成员:
1
2
3
4
5
class GamePlayer {
private:
static const int NumTurns = 5; //常量声明式
int scores[NumTurns]; //使用常量
}
无法利用#define创建一个class专属常量。一但宏被定义,它就在其后的编译过程中有效。这就意味着#define不仅不能够用来定义class专属常量,也不能够提供任何封装性,所以没有private #define 这样的东西。const成员变量是可以被封装的。
若编译器不允许"static整数型class常量” 完成"in class初值设定“,可改用"the enum hack”补偿做法。
即枚举类型的数值可充int使用:
对比宏定义和模板函数:
1
#define MAX(a,b) f((a) > (b) ? (a) : (b)) //当写出这种宏,一定要给所有实参加上小括号。
当调用:
调用f之前,a的递增次数取决于和谁比较! 调用此宏很可能遇到麻烦!
模板函数:
1
2
3
4
5
template<typename T>
inline void MAX(const T& a, const T& b)
{
f (a > b ? a : b); //遵循pass by reference-to-const规则 (根据动态绑定的实参调用)
}
这个template产出一整群函数,每个函数都接受两个同型对象。这里不需要为参数加上括号,也不需要操心参数被核算多次...等等
总结:
-对于单纯常量,最好以const对象或enums替换#define
-对于形似函数的宏(macros),最好改用inline函数替换#define
条款03:尽可能使用const
-若关键字出现在 * 左边,表示被指物是常量(但可以通过其它途径改变被值对象的值,不能通过此指针改变)
若关键字出现在 * 右边,表示指针自身是常量;
若出现在两边,表示被植物和指针两者都是常量
迭代器的作用就像个T* 指针。
声明迭代器为const就像声明指针为const一样(T* const指针),表示迭代器不可变(但迭代器指向的对象的值可以改动)。
若希望迭代器指向的值不可变。则需要const_iterator:
1
2
3
4
5
6
7
8
9
10
std::vector<int> vec;
...
const std::vector<int>::iterator iter = //iter的作用像个T* const
vec.begin();
*iter = 10; //没问题,该变iter所指物
++iter; //错误! iter是const
std::vector<int>::const_iterator cIter = //cIter的作用像个const T*
vec.begin();
*cIter = 10; //错误! *cIter是const
++cIter; //没问题,改变cIter;
令函数返回一个常量值,可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。
有理数的operator* 声明式:
若不返回一个const,考虑以下代码:
1
2
3
Rational a,b,c;
...
if (a * b = c); //若把做比较的运算符==打成了=
无意间把==打成了=,就会有无意义的赋值操作。
若返回值是一个const,则赋值操作是错误的,可以预防这样的错误。
const成员函数:
两个成员函数如果只是常量性不同,可以被重载。----C++重要特性
考虑以下class:
TextBlock的operator[]可被这么使用:
1
2
3
4
5
6
7
TextBlock tb("Hello");
tb[0] = 'X'; //正确——写一个no-const TextBlock
std::cout << tb[0]; //调用non-const TextBlock::operator[]
const TextBlock ctb("world!");
ctb[0] = 'X'; //错误——写一个const TextBlock
std::cout << ctb[0]; //调用const TextBlock::operator
/* 成员函数const补充:哲学 (逃 */
哲学的两个流派:bitwise constness 和 logical constness;
bitwise constness阵营主张:const成员函数不可以更改对象内任何非static成员变量
logical constness阵营主张: const成员函数可以修改处理的对象内的某些bits;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CTextBlock {
public:
...
std::size_t length() const;
private:
char* pText;
std::size_t textLength; //最后一次计算的文本区域长度
bool lengthIsValid; //目前的长度是否有效
};
std::size_t CTextBlock::length() const
{
if (!lengthIsValid) {
textLength = std::strlen(pText); //错误!在const成员函数内不能赋值给textLength和lengthIsValid;
lengthIsValid = true;
}
return textLength;
}
length()函数的实现当然是不是bitwise const。
虽然修改对const CTextBlock对象而言可以接受,但编译器不同意(编译器是bitwise constness阵营的),怎么办怎么办??
解决方案:用mutable释放掉non-static成员变量的bitwise constness约束(把非static对象开除出bitwise constness阵营)
修改7、8行代码为:
现在这些成员可以被更改,即使在const成员函数里
在const和non-const成员函数中避免重复。
const是个奇妙且非比寻常的东西。在指针和迭代器身上;在指针、迭代器及reference指涉的对象身上;在函数参数和返回类型身上;
在local变量身上;在成员函数身上,林林总总不一而足。const是个威力强大的助手。尽可能使用它。你会对你的作为感到高兴~
总结:
-将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
-编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”
-当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
条款04:确定对象被使用前已先被初始化
对于构造函数:确保每一个构造函数都将对象的每一个成员初始化。
//区分赋值和初始化
例如以下构造函数:
1
2
3
4
5
6
7
8
9
AB::AB(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
{
//以下几个左值都是类的成员
theName = name;
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
//敲黑板!!以上都是赋值,不是初始化!!
}
这会导致AB对象带有你期望的值,不是最佳做法!
构造函数的最佳写法:使用所谓的member initialization list(成员初值列) 替换赋值动作
这个构造函数和上一个的最终结果相同,但通常效率较高。
默认构造函数也可以使用成员初值列。
1
AB::AB() : theName(), theAddress(), thePhones(), numTimesConsulted(0) {}
//没有指定初值则自动调用default构造函数
重要规则:规定总是在初值列中列出所有成员变量。以免还得记住哪些成员变量可以无需初值。
如果成员初值列遗漏某个成员,它就没有初值,因此可能开启”不明确行为“的潘多拉盒子~
若成员变量是const或reference,就一定需要初值,不能被赋值!
所以最简单的做法就是:总是使用成员初值列。这样做有时候绝对必要,且又往往比赋值更高效。
C++有着固定的”成员初始化次序“:
base classes基类总是比derived classes继承类更早被初始化。
class的成员变量总是以其声明次序被初始化。
例如:
即使在构造函数成员初值列中出现的次序和private的声明次序不同,初始化次序依旧和声明次序相同(即thePhones最先初始化)
为避免你或读者被迷惑,并避免某些可能存在的晦涩错误,当在成员初值列中条列各个成员时,最好总是以其声明次序为次序!
”不同编译单元内定义之non-local static对象”的初始化次序:
static对象,其寿命从被构造出来直到程序结束为止。
这种对象包括:global对象,定义于namespace作用域内的对象,在class内,在函数内,以及在file作用域内被声明为static的对象。
函数内的static对象称为local static对象(对函数而言是local),其它对象称为non-local static对象。
static对象在程序结束时自动销毁,它们的析构函数会在main()结束时被自动调用。
//补充:所谓编译单元是指产出单一目标文件的那些源码。基本上它是单一源码文件加上其所含入的头文件。
真正的问题是:如果某编译单元内的某个non-local static对象的初始化动作使用了另一个编译单元内的某个non-local static对象,
它所用到的这个对象可能尚未被初始化,因为C++对定义在不同编译单元内的non-local static对象的初始化次序并无明确定义。
实例:
1
2
3
4
5
6
7
8
class FileSystem { //来自你的程序库
public:
...
std::size_t numDisks() const; //众多成员函数之一
...
};
extern FileSystem tfs; //预备给客服使用的对象
//tfs代表"the file system"
然后执行:
1
Directory tempDir( params ); //为临时文件而做出的目录
现在,初始化次序的重要性显示出来了:除非tfs在tempDir之前先被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。
如何确定tfs在tempDir之前先被初始化?
唯一需要做的是:
将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。
这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。
即:non-local static 被 local static对象替换了。
这种方法基于:C++保证,函数内的local static对象会在 “函数被调用期间" 或 "首次遇上该对象之定义式” 时被初始化!
所以以"函数调用“(返回一个reference指向local static对象) 替换”直接访问non-local static对象".
这样获得的reference将指向一个历经初始化的对象。
所以,经过此技术施行:
现在这个程序调用就没有问题了,唯一不同的是现在使用tfs()和tempDir()而不再是tfs和tempDir.
也就是说使用函数返回的"指向static对象"的reference,而不再使用static对象本身。
注意:任何一种non-const static对象,不论它是local或non-local,在多线程环境下“等待某事发生”都会有麻烦!
处理麻烦的一种做法:在程序的单线程启动阶段手工调用所有reference-returning函数。
/* 补充,内置类型:包括算术类型和空类型在内的基本数据类型。
算术类型包括:字符型,整型,bool型,浮点型 */
为避免在对象初始化之前过早地使用它们,需要做三件事:
第一,手工初始化内置型non-member对象。
第二,使用成员初值列对付对象的所有成分。
第三,在“初始化次序不确定性”氛围下加强设计!
总结:
-对内置型对象进行手工初始化,因为C++不保证初始化它们。(避免出现不确定情况)
-构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和它们在class中的 声明次序相同。
-为免除“跨编译单元之初始化次序"问题,请以local static对象替换non-local static对象。
上一篇: DDD实战与进阶 - 值对象
下一篇: CloudStack三节点集群搭建
推荐阅读
-
Effective Modern C++ 条款37 在所有路径上,让std::thread对象变得不可连接(unjoinable)
-
荐 看完一个博客你就学会了Python,干货满满(自己总结,适合学过c,或者c++)
-
Effective C++ Term 26 实现一个不抛出异常的 swap
-
Effective C++ (一) : 让自己习惯C++
-
Effective C++笔记之一:声明、定义、初始化与赋值
-
C++ 实验二 NO.1_(3) 1:熟悉DEV环境,练习自己的第一个程序使用DEV集成环境来编辑,运行简单的数据输入和运算实验。(3)编写一个程序,要求:提示输入3个数;显示这3个数,求他们的平均值
-
Effective Modern C++ 笔记(一)——类型推断
-
一文让你彻底明白C++中的const
-
Effective C++ Item 12-复制对象时忽忘其每一个成分
-
Effective C++ --- 让自己习惯C++