欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Effective C++ (一) : 让自己习惯C++

程序员文章站 2022-07-15 12:38:25
...

让自己习惯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往往如下定义比较好:



1
const std::string authorName("amoscykl");


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
2
3
4
5
class GamePlayer {
private:
enum { NumTurns = 5 }; //"the enum hack"-NumTurns5
int scores[NumTurns];
}



对比宏定义和模板函数:

1
#define MAX(a,b) f((a) > (b) ? (a) : (b)) //

当调用:


1
2
3
int a =5, b = 0;
MAX(++a,b); //a
MAX(++a,b+10); //a

调用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


-若关键字出现在 * 左边,表示被指物是常量(但可以通过其它途径改变被值对象的值,不能通过此指针改变)

 若关键字出现在 * 右边,表示指针自身是常量;

 若出现在两边,表示被植物和指针两者都是常量



1
2
3
//
void f1(const Widget* pw); //f1Widget
void f2(Widget const * pw); //


迭代器的作用就像个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 = //iterT* const
vec.begin();
*iter = 10; //iter
++iter; //! iterconst
std::vector<int>::const_iterator cIter = //cIterconst T*
vec.begin();
*cIter = 10; //! *cIterconst
++cIter; //cIter;


令函数返回一个常量值,可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。

有理数的operator* 声明式:


1
2
class Rational { ... };
const Rational operator* (const Rational& lhs,const Rational& rhs); //constconst


若不返回一个const,考虑以下代码:

1
2
3
Rational a,b,c;
...
if (a * b = c); //===

无意间把==打成了=,就会有无意义的赋值操作。

若返回值是一个const,则赋值操作是错误的,可以预防这样的错误。


const成员函数:
两个成员函数如果只是常量性不同,可以被重载。----C++重要特性

考虑以下class:


1
2
3
4
5
6
7
8
9
10
11
class TextBlock {
public:
...
/* */
const char& operator[](std::size_t position) const //operator[]for const
{ return text[position]; }
char& operator[](std::size_t postion) //operator[]for no-const
{ return text[position];
private:
std::string text;
};

    

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



1
2
3
4
void print(const TextBlock& ctb) //ctbconst
{
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); //!consttextLengthlengthIsValid;
lengthIsValid = true;
}
return textLength;
}

length()函数的实现当然是不是bitwise const。

虽然修改对const CTextBlock对象而言可以接受,但编译器不同意(编译器是bitwise constness阵营的),怎么办怎么办??


解决方案:用mutable释放掉non-static成员变量的bitwise constness约束(把非static对象开除出bitwise constness阵营)

修改7、8行代码为:


1
2
mutable std::size_t textLength;
mutable bool lengthIsValid;

现在这些成员可以被更改,即使在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
2
3
AB::AB(const std::string& name, const std::string& address; const std::list<PhoneNumber>& phones)
:theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) {}
//copy

这个构造函数和上一个的最终结果相同,但通常效率较高。


默认构造函数也可以使用成员初值列。

1
AB::AB() : theName(), theAddress(), thePhones(), numTimesConsulted(0) {}
    //default


重要规则:规定总是在初值列中列出所有成员变量。以免还得记住哪些成员变量可以无需初值。

如果成员初值列遗漏某个成员,它就没有初值,因此可能开启”不明确行为“的潘多拉盒子~


若成员变量是const或reference,就一定需要初值,不能被赋值!

所以最简单的做法就是:总是使用成员初值列。这样做有时候绝对必要,且又往往比赋值更高效。


C++有着固定的”成员初始化次序“:

base classes基类总是比derived classes继承类更早被初始化。

class的成员变量总是以其声明次序被初始化。

例如:


1
2
3
4
5
6
7
8
9
10
class AB {
public:
AB::AB(const std::string& name, const std::string& address; const std::list<PhoneNumber>& phones)
:theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) {}
private:
std::list<PhoneNumber> thePhones;
std::string theAddress;
int numTimesConsulted;
std::string theName;
}

即使在构造函数成员初值列中出现的次序和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
2
3
4
5
6
7
8
9
10
class Directory {
public:
Directory( params );
...
};
Directory::Directory( params )
{
...
std::size_t disks = tfs.numDisks(); //使tfs
}

然后执行:

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将指向一个历经初始化的对象。


所以,经过此技术施行:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class FileSystem { //
public:
...
std::size_t numDisks() const; //
...
};
FileSystem& tfs() //tfs:FileSystem classstatic.
{
static FileSystem fs; //local static
return fs; //reference
}
class Directory {
public:
Directory( params );
...
};
Directory::Directory( params )
{
...
std::size_t disks = tfs().numDisks(); //使tfs()
}
Directory& tempDir() //tempDir,Directory classstatic
{
static Directory td; //local static
return td; //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对象。


相关标签: C Effective C