Chapter 1
1 让自己习惯C++
条款 01 视 C++ 为一个语言联邦
-
C : C++以C为基础,block、语句、预处理器、内置数据类型、数组、指针都来自于C。当使用C++中的C成分工作时,没有模板(Template)、没有异常(Exceptions)、没有重载(overloading)。
-
Object-Oriented C++ : 也就是 C with classes,classes(包括构造函数和析构函数)、封装(encapsulation)、继承(inheritance)、多态(polymorphism)、virtual函数(动态绑定)……等等。
-
Template C++ : C++的泛型编程部分。
-
STL(Standard Template Library) : 对容器、迭代器、算法以及函数对象对的规约有极佳的紧密配合与协调。
*请记住 : *
1. 高效编程守则视状况而变化,取决于你使用C++的哪一部分。
条款 02 尽量以 const,enum,inline 替代 #define
例 : `#define ASPECT_RATIO 1.653`预处理器会将程序中的`ASPET_RATIO`记号全部替换成数值`1.653`,也就是这个记号不会进入记号表内,这样在程序出错时获取的错误信息会很难分析。
当我们使用#define 实现宏
时,更要小心,必须为宏中的所有实参加上小括号。例 :
#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))//一定要加小括号,这里的例子还体现不出什么,注意加括号的位置就行
int a = 5,b = 0;
//当第一个参数(++a)大于第二个参数时,++a会被执行两次,这显然很容易出问题
CALL_WITH_MAX(++a,b);
CALL_WITH_MAX(++a,b+10);
解决方法 :
-
以一个常量替换上述宏。
const double Aspect_ratio = 1.653;
作为一个语言常量,自然会进入记号表,这样在Debug的时候容易跟踪,且生成的代码量比较小,开支较小,因为宏定义是盲目地替换字段。以 const 代替 #define 时要注意两点 :
(1) 定义常量指针时,要注意顶层 const 和底层 const 的区别。顶层 const 是将 const 放在 “*” 左边,意思是指针指向的常量类型,所以该指针所指向的内容不能被修改;
而底层 const 则是将 const 放在 “*” 右边,意思是我这个指针是一个常量指针,只能指向在初始化时的值,不能指向其他变量。
(2)定义 class 专属常量时,也就是类内 static 成员,例如
class GamePlayer{ private: static const int NumTurns = 5; int scores[NumTurns]; };
注意,上例中的的
static const int NumTurns = 5;
是声明式而不是定义式。C++要求要对使用的任何东西都要提供一个定义式。 但如果它是个 class 专属常量,又是 static 且为整数类型(integral type,例如int,char,bool),只要不取指针,则只需要提供声明式,若有些编译器不支持,则需要在实现文件中定义。(注意非整型必须定义,初值可以放在定义式中)
//.h class CostEstimate{ private: static const double FudgeFactor; }; //.c const double CostEstimate::FudgeFactor = 1.35;
另外,#define 不重视作用域(scope),在定义后编译过程中都有效。
-
使用
enum hack
这种做法比较像
#define
,同样不能取地址,同样会导致非必要的内存分配。 -
对于“实现宏“,用
template inline
函数,同样是代码替换,但这种做法属于函数操作,一切按函数操作就行了,不用操心参数问题,同时遵守作用域和访问规则。
请记住:
1. 对于单纯常量,最好以 const 对象或 enum 替换 #define
2. 对于形似函数的宏(macros),最好改用 inline template 函数替代 #define
条款 03 尽可能使用const
顶层 const : 表示 const 修饰的类型为常量,特别地,对于指针类型,const
在 ‘*’ 右边时表示顶层指针,也就是该指针变量为常量, 与该指针变量指向的对象是否为常量无要求。
int i = 0;
int *const p1 = &i;//const 在‘*’右边,表示p1只能指向i(顶层指针)
底层 const : 与指针和引用类型有关,对于指针类型,当 const
在 ‘*’ 左边(一般写在类型左边)时,表示该指针指向的对象为常量类型, 此时该指针可以指向别的对象,当不能通过指针解引用来更改所指对象的值。
而对于引用类型,都是底层 const,也就是所引用的值为常量,但const引用可以绑定常量与非常量,这是一个特殊的例子。
int i = 0;
const int j = 1;
const int &ref_i = i;//可以绑定非常量
const int &ref_j = j;//可以绑定常量类型
const int &ref = 10;//会创建临时变量,然后绑定
const可以和函数的返回值、参数、函数自身产生关联,尽可能地使用它,可以让编译器帮你更快的发现错误!
const成员函数
将 const
作用在成员函数上,可以区分出常量对象和非常量对象所使用的不同版本的成员函数。const成员函数可以保证不修改类的成员变量。
//.h
class TextBlock{
public:
// ...
const char& operator[](std::size_t position) const//const对象调用此函数
{ return text[position]; }
char& operator[](std::size_t position)//非const对象调用此函数
{ return text[position]; }
private:
std::string text;
};
//.cpp
TextBlock tb("Hello");
std::cout << tb[0];//调用 char& operator[](std::size_t position)
const TextBlock ctb("Hello");
std::cout << ctb[0];//调用 const char& operator[](std::size_t position) const
在C++中,不修改类的成员变量指的是编译器强制实施的“bitwise constness”,也就是保证每一个bit都不允许修改,但现实中,我们可能会希望一部分成员不被修改,一部分成员被修改。
此时,我们可以将能在const成员函数中修改的成员变量声明为mutable
//.h
class CTextBlock{
public:
//...
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength;
mutable bool lengthIsVaild;
};
//.cpp
std::size_t length() const
{
if(!lengthIsVaild)
{
pText = "Hello";//错误,非mutable变量在const成员函数中禁止修改
textLength = std::strlen(pText);//允许修改
lengthIsvaild = true;//允许修改
}
return textLength;
}
在const和non-const成员函数中避免重复
很多时候,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];
}
*/
char& operator[](std::size_t position)
{
return const_cast<char&>(//去除返回值的const属性
static_cast<const TextBlock>(*this)[position];//static_cast安全转型为const对象调用const版本,返回值为const char*
);
}
private:
std::string text;
};
解决冗余的原则是,在非常量的版本中对常量版本进行转型,因为我们永远不对常量版本的东西作任何修改,只单纯调用,这完全符合const的设计 : const成员函数保证不修改成员。这避免了不必要的风险。
请记住:
1. 将某些声明为 const 可帮助编译器侦测出错误用法。 const 可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数 本体。
2. 编译器强制实施 bitwise constness,但你编写程序时应该使用“概念上的常量性(conceptul constness)。”
3. 当 const 和 non-const 成员函数有着实质等价的实现时,领 non-const 版本调用 const 版本可避免代码重复。
条款 04 确定对象被使用前已先被初始化
有些类型(stl)保证内容能被默认初始化,而有些却不行。
因此我们强制规定 : 永远在使用对象之前先将它初始化。
int x = 0;
const char* text = "hello world";
double d;
std::cin >> d;
在C++中,除了内置类型外,初始化往往由构造函数完成。
因此我们强制规定 : 确保每一个构造函数都将对象中的每一个成员初始化。
注意不要混淆初始化和赋值这两种概念。
//.h
class PhoneNumber {...};
class ABEntry{
public:
ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones);
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};
//.cpp
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
{
//赋值
theName = name;
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
该构造函数定义中的行为是赋值,因为在进入构造函数定义也就是大括号部分之前所有的成员变量已经被默认构造函数构造了。此时又 重新执行一遍赋值函数,一共两次动作,开销太大。
因此
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
:theName(name),
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{ }
此时对于各个成员,只执行依次构造函数即可完成初始化。
另外,初始化的次序应该对应声明的次序。可以避免一些隐晦的错误。
现在我们关注“不同编译单元内定义的non-local static对象”的初始化次序 :
- 编译单元 : 产出单一目标问价的源码。基本上是 单一源码文件 + 其所加入到的头文件。注意!编译器编译不同单元的次序无法确定!
- static对象 : 寿命从被构造出来直到程序结束,程序结束时自动调用其析构函数。
- non-local static对象 : 不在函数内的static对象。
- local static对象 : 在函数内的static对象。
我们设想一种这样的情况 :
在一个编译单元内存在一个对象
class FileSystem{
public:
...
std::size_t numDisks() const;
...
};
extern FileSystem tfs;//声明外部变量,预备给其他文件(编译单元)使用
在另一个编译单元内存在另一个对象
class Directroy{
public:
Directory( params );
...
};
Directory::Directory( params )
{
...
std::size_t disks = tfs.numDisks();
}
Directory temp( params );
因为不同编译单元编译的次序是不确定的,我们无法保证Directory对象定义之前,类FileSystem已经被编译,这样会出现错误。
解决方法 :
Singleton模式
因为C++保证 : 函数内的local static对象会在“该函数被调用期间”“首次遇上该对象的定义式”时被初始化。
//编译单元。
class FileSystem {...};
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}
//另一个编译单元
class Directory {...};
Directory::Directory( params )
{
...
std::size_t disks = tfs().numDisks();
...
}
Directory& tempDir()
{
static Directory td;
return td;
}
因此可以保证调用tfs()时,只有FileSystem的定义式被编译器遇到时才给里面的fs对象初始化并返回。
但内含static对象在多线程系统中线程不安全。
*请记住 : *
1. 为内置型对象进行手工初始化,因为C++不保证初始化它们。
2. 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作,但如果有多个构造函数会产生冗余代码时,可以将具 有确定的默认初始化值的变量赋值封装进一个函数,其余还是使用列表初始化。同时,列表初始化的次序也应该与声明次序相同。
3. 为免除”跨编译单元的初始化次序”问题,请以 local static 对象替换 non-local static 对象。