【Cpp】《Effective C++》第一章-让自己习惯C++

  这是我在学习《Effective C++》中总结得出的心得与体会,完全是以我自己的理解所作的笔记,是对个人经验的积累。基于第三版我以每个章节进行总结,全书一共九个章节。

第一章-让自己习惯C++

条款01:视C++为一个语言联邦

  View C++ as a federation of languages.
  在初期Cpp被开发之初,Cpp并没有如今这样丰富的功能,它更应该被称为仅是一个带有面向对象特性的C,即C with Classes。但如今的Cpp已经在其基础纸上新增了诸多特性例如模板更好的支持了泛型编程,STL给我们提供了更多的工具,加上其完全兼容C使其既可以面向对象编程也可以面向过程编程,这就使Cpp成为了一个历史上独一无二绝无仅有的强大存在,其也可以被理解为多个语言的组合,我们这些语言称为次语言(sublanguage)
  也正因Cpp可以被看为多个次语言的组合,使它的语法更为复杂,标准更加难以捉摸,但是当我们在使用某个次语言的时候,守则与通例都会变得更加简单易懂。然而当我们从一个次语言切换到另一个时,守则往往也会改变。
  在《Effective C++》中将Cpp分为了四种次语言,是我们可以更加直观的正视这门复杂的语言。
  4种次语言:
  1、C:Cpp是以C为基础,Cpp的编程是更为高级的C编程,其可以是为是对C的封装及升级,并且Cpp中的诸多语法都是来自于C,例如指针、数组、预处理等等。但当我们仅仅使用C这一次语言的时候的我们则要考虑守则与规范在这一次语言中的极限,因为在这里没有模板,没有异常,没有重载。因此当我们在Cpp内使用纯C进行编程时要注意让自己遵守的规范从C的角度出发,考虑更为底层更为细节的方面。
  2、Object-Oriented C++:这一部分可以看作是Cpp开发之初C with Classes所实现的部分,是带面向对象版本的C。在这一次语言中凸显了Cpp面向对象的特点例如封装、继承、多态、动态绑定等。在使用这部分的次语言时我们则要遵守面向对象在Cpp上的守则。
  3、Template C++:这是Cpp的泛型编程部分,在这一部分有着模板这一概念,而模板的书写往往是在给高级编程搭轮子。模板编程弥漫了整个Cpp,并且带来了崭新的编程范型模板元编程(template meteprogramming/TMP)。唯独模板才适用的规范也并不在少数,但是这些与Cpp主流编程并不冲突。
  4、STL:STL是Cpp中标准的模板库,也是最常用最常见的工具库,其中六大组件互相配合,当然如果你使用STL那么也要遵守STL独有的规范。
  在使用不同的次语言时就有着不同的规范守则,当你从一个次语言切换到另一个时高效编程守则有可能需要你改变策略。例如在对内置类型传参时用值传参(pass-by-value)往往比传引用(pass-by-reference)更加高效,但当你如果从C part of C++这一次语言移往Object-Oriented C++,对于用户自定义的类型在传参时往往是传引用(pass-by-reference)更加高效,因为省略掉了一次拷贝构造,避免了不必要的开销,对于Template C++ 来说也是这样。
  由以上可以看出当我们在使用Cpp进行编程时,使用不同部分的次语言为了更加高效的进行编程往往就需要遵守不同的守则,这些守则有可能在不同的次语言间是相互违背的,所以在不同的次语言见进行转换时我们的策略可能也需要进行改变。这也是为什么要将Cpp分为这些次语言的原因这点很重要。
  牢记一点:Cpp的高效编程视情况而变化,取决于使用Cpp的哪一部分。

尽量以const,enum,inline替换#define

  Prefer consts,enums,and inlines to #defines.
  在这一条款中,要求程序员最好可以用编译器来代替预编译器,因为预编译器并不可以视为语言的一部分。

用const,enum替代宏定义常量

  预编译器中你定义的宏的名称并不会计入符号表内,从而使得调试及排错十分困难。因此我们尽量使用常量定义来代替宏定义,首当其冲就是为了避免宏定义使得其名称无法计入符号表。
  但是对于定义常量来代替宏有两点特殊情况需要讨论:
  1、定义常量指针(constant pointers)。关于常量指针和指向常量的指针在C语言章节就有解释。在本书中这里指的是一个完全是常量的指针,即是一个既是本身无法改变并且所指内容也无法改变的指针,因此我们为了定义这样一个常量需要将const写两遍才行。例:const char* const authorName = "Misaki";,但这种情况下利用Cpp中的STL中的string类反倒更好定义。
  2、类的成员专属常量。这种常量为了让其只有一份实体,因此要让其成为static的。一般情况下我们在定义一个静态的成员变量时都需要在类外进行定义,然而如果这个静态成员变量具有常性,即是const的,并且它为整数类型(integral type),再并且只要不取它们的地址则它无需在类外进行定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;
class GamePlayer
{
public:
static const int NumTurns = 5;
int scores[NumTurns];
};
//const int GamePlayer::NumTurns; //不给予数值的定义式
int main()
{
cout << GamePlayer::NumTurns << endl;
}


5

  当然如果你要取他们的地址或者你的编译器坚持要看到定义式则也是可以加上定义式的,但是 不可以再次给予数值,因为其载声明时已经获得初值了。
  但是我们无法使用宏定义的方式来定义一个成员常量,宏并不在乎作用域,受#define#undef等宏的控制,因此宏并不具备封装性。
  但是有的编译器也许不支持在声明中就获得初值的in-class初值设定,呢么我们只好在定义中加入初值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;
class GamePlayer
{
public:
static const int NumTurns;
//int scores[NumTurns];//编译器要求在编译时就能确定数组的大小
};
const int GamePlayer::NumTurns = 5;
int main()
{
cout << GamePlayer::NumTurns << endl;
}


5

  但这种情况下我们并无法在编译期间获得一个常量值,因此无法用其定义数组,但是也有其他解决方法,改用the enum hack的补偿做法。一个枚举类型的数值可权充int被使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;
class GamePlayer
{
public:
//static const int NumTurns;
enum
{
NumTurns = 5
};
int scores[NumTurns];//编译器要求在编译时就能确定数组的大小
};
//const int GamePlayer::NumTurns = 5;
int main()
{
cout << GamePlayer::NumTurns << endl;
}


5

  这样也可以在编译时就确定常量的值。通过这个enum hack我们也可以直接在编译期间使用常量,然而enum hack在某些方面的表现更接近于#define宏定义而不是const常量,例如取一个const常量的地址是合法的,然而取#defineenum的地址是不合法的。enum hack在很多方面都很实用,并且是模板元编程的而基础技术。

用inline替代宏函数

  另一个#define很坑的地方就是用它去实现宏函数。因为宏只是单纯的文本替换因此我们经常要给所有实参都加上括号来防止意外发生,但这样会大大降低代码可读性。因此书中极大程度建议使用inline函数来代替宏函数,它同样可以节省函数调用的额外开销,但是有着更好的可读性。并且在类中还可以定义专属于类内的private inline函数,而宏定义做不到此事。
  我们总结一下宏的缺点
  1、定义常量不会计入记号表(symbol table),因此不便于调试。
  2、无法定义成员常量,宏并不重视作用域。
  3、宏的使用,尤其是宏函数会使代码可读性变差,不便于理解。
  因此对于常量,最好以constenum替代#define;对于宏函数最好用inline函数替代#define。在用const定义常量的时候也有两点特殊情况需要注意,一个是常量指针一个是类专属常量。
  我们基本可以用enum,const,inline替代#define所有使用场景,降低了对预处理器的使用,但是也并非完全消除,预处理器依然有举足轻重的作用,我们只能尽可能减少对其的使用。比如说当我们在写日志检错时就需要用到宏,需要知道具体是哪一行发生了错误,而如果使用函数则会跳转到函数中。

总结

  1、对于单纯常量,最好以const对象或enums替换#define。
  2、对于宏函数,最好以inline函数替代。

尽可能使用const

  Use const whenever possible.
  const多才多艺,可以用来修饰相当多的变量或函数,一旦被修饰则该对象会被编译器强制约束来保持它自身不会被改变。

const对于指针和迭代器

  对于指针而言 const出现在*左边表示被指示物是常量,如果出现在*右边则表示指针本身是常量,当然其也可以两边都出现表示被指示物和指针本身都是常量。
  通过对STL中容器的模拟实现我们也可以很清楚的知道迭代器底层的实现其实就是一个指针,因此迭代器对const来说也有着和指针一样的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> vec = {1, 2, 3, 4};
//我们给接下来这个迭代器加上const
//其类似于T* const指针,注意不是const T*虽然const写在前面,但这里const强调的是迭代器自身的不可变
//迭代器也不过是一个typedef类型而已
const vector<int>::iterator iter = vec.begin();
*iter = 5;//合法,迭代器所指对象可变
//iter++;//不合法,迭代器自身不可变
cout << vec[0] << endl;
//这里相当于一个const T*指针,指向内容不可改,自身可改
vector<int>::const_iterator const_iter = vec.begin();
//*const_iter = 1;//不合法,指向内容不可改
const_iter++;//合法,自身可变
cout << *const_iter << endl;
}


5
2

const对于函数的应用

  const可以与一个函数的返回值、参数、自身产生关联。在这里我们先讨论对返回值以及参数的关联。
  对于函数的返回值,我们用const进行修饰来避免产生一些意外情况。这里我用书中举得例子,来重载一个有理数类的operator*运算符。

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

  这里参数设置为引用加const修饰的原因是为了减少拷贝构造加快效率同时避免对操作数的修改,很好理解,但是返回值加const或许比较难以想到,这是因为总可能有人会写出这样违法的代码。

1
2
Rational a, b, c;
(a * b) = c;//在a * b结果上调用operator=

  这段代码如果我们对operator*重载的返回值并未加上const编译器是允许编的过这样违法的操作的,但这明显是不合常理的是我们想命令禁止的,因此对于用const来修饰函数的返回值也有着莫大的必要。
  对于const修饰参数已经再熟悉不过了,只要你不想修改它都建议将其定义为const,这是很好的习惯。

const成员函数

  接下来我们着重讨论如何const对于函数自身的修饰,而这些修饰都与类成员函数有关。
  const修饰成员函数自身就是将其修饰为常成员函数,在这种函数中无法修改调用对象自身的数据,因为传入的是一个constthis指针。同时我们还要注意一点很重要的语法,常成员函数与普通成员函数间会构成重载,即使两个成员函数的参数列表完全相同,但只要他们在同一个类中并且函数名相同,仅由常量性constness不同也是可以构成重载的。但是注意此时其构成重载的原因并非是this指针这个参数被const修饰,因为const修饰参数是构不成函数重载的,这里的重载原因是因为函数常量性的不同,与参数无关
  下面这个例子就展现了利用成员函数常量性的不同所构成重载。当普通成员函数与常成员函数发生重载时,普通对象会优先自动调用普通成员函数,常对象会优先自动调用常成员函数。

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
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <string>
using namespace std;
class TextBook
{
public:
//重载operator[]
//这里返回值加const是为了让const对象自身不被修改不造成错误
const char& operator[](size_t position) const
{
return text[position];
}
char& operator[](size_t position)
{
return text[position];
}
private:
string text = "123456";
};
void Print(const TextBook& book1, TextBook& book2)
{
cout << book1[0] << endl;//合法
cout << book2[0] << endl;//合法
book2[0] = '5';//合法,普通对象调用普通成员函数,返回值可改
cout << static_cast<const TextBook&>(book2)[0] << endl;
//static_cast<const TextBook&>(book2)[0] = '1';//非法这里我们将其转换为const类型对象发现可以主动调用常成员函数
//book1[0] = '5';//非法,常量对象调用常成员函数而其返回值是const修饰的因此不可改
}
int main()
{
TextBook book;
Print(book, book);
}


1
1
5

  并且通过以上的代码我还发现虽然普通对象可以调用常成员函数,但是当其与普通成员函数构成重载时我们想要显示调用常成员函数必须将对象转换为const的,这完全没有必要,属于脱裤子放屁的一种手段。

bitwise constness和logical constness

  如果一个成员函数是常成员函数,则就会引申出这样两个流行概念。
  bitwise const阵营的人相信只有成员函数不改变成员的任何变量的时候才可以说其为const的,这也是Cpp中对常量性的定义,const成员函数不可以更改对象内任何的non-static成员变量。
  但是不幸的是在Cpp中很容易对这一观点进行反驳,很多const尽管已经对对象的成员进行了更改依然可以编译通过。比如说成员中有指针的存在。比如以下这个例子。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <string.h>
using namespace std;
class CTextBook
{
friend ostream& operator<<(ostream &cout, const CTextBook& text);
public:
CTextBook(const char* str)
:_text(nullptr)
{
int capacity = strlen(str) + 1;
_text = new char[capacity];
strcpy(_text, str);
}
~CTextBook()
{
if(_text != nullptr)
{
delete[] _text;
}
}
char& operator[](size_t position) const
{
return _text[position];
}
private:
char* _text;
};
ostream& operator<<(ostream &cout, const CTextBook& text)
{
cout << text._text << endl;
return cout;
}
int main()
{
CTextBook text("Hello");
text[0] = 'J';
cout << text;
}


Jello

  以上这个例子中我们在const成员函数中并没有改变成员变量,因此编译器并没有报错,以此欺骗了编译器,但是并不代表我们不能通过const成员函数改变成员内的对象。由此即产生了第二个阵营对const成员函数的理解即logical constness
  这个阵营中的人主张const成员函数可以改变其所处理的对象中的某些成员变量,但是只有在客户端侦测不出的情况下才可以这样。但是还有一些情况下我们要修改一些本身可以修改的成员变量,他们的修改从逻辑角度考虑是合理的,是必须的,但是此时编译器由于语法的关系判定其不符合bitwise constness,因此判定其报错不通过编译。例如以下这种情况。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
#include <string.h>
using namespace std;
class CTextBook
{
friend ostream& operator<<(ostream &cout, const CTextBook& text);
public:
CTextBook(const char* str)
:_text(nullptr)
{
int capacity = strlen(str) + 1;
_text = new char[capacity];
strcpy(_text, str);
}
~CTextBook()
{
if(_text != nullptr)
{
delete[] _text;
}
}
char& operator[](size_t position) const
{
return _text[position];
}
size_t length() const
{
if(!_lengthIsValid)
{
//这里明显是编译不过的,因为其不是bitwiss constness的
_len = strlen(_text);
_lengthIsValid = true;
}
return _len;
}
private:
char* _text;
int _len;
bool _lengthIsValid;
};
ostream& operator<<(ostream &cout, CTextBook& text)
{
cout << text._text << endl;
return cout;
}
int main()
{
CTextBook text("Hello");
text[0] = 'J';
cout << text;
}

  以上这个例子肯定是编译不过的,其不符合编译器的编译标准,但是从逻辑角度来看这些赋值和改变我们是可以接受了,但是编译器不同意怎么办呢?解决这个问题需要用到mutable释放掉non-static成员变量的bitwiss constness约束。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>
#include <string.h>
using namespace std;
class CTextBook
{
friend ostream& operator<<(ostream &cout, const CTextBook& text);
public:
CTextBook(const char* str)
:_text(nullptr)
,_len(0)
,_lengthIsValid(false)
{
int capacity = strlen(str) + 1;
_text = new char[capacity];
strcpy(_text, str);
}
~CTextBook()
{
if(_text != nullptr)
{
delete[] _text;
}
}
char& operator[](size_t position) const
{
return _text[position];
}
size_t length() const
{
if(!_lengthIsValid)
{
//这里明显是编译不过的,因为其不是bitwiss constness的
_len = strlen(_text);
_lengthIsValid = true;
}
return _len;
}
private:
char* _text;
mutable int _len;
mutable bool _lengthIsValid;
};
ostream& operator<<(ostream &cout, const CTextBook& text)
{
cout << text._text << endl;
return cout;
}
int main()
{
CTextBook text("Hello");
text[0] = 'J';
cout << text;
cout << text.length() << endl;
}


Jello
5

  mutable虽然可以解决bitwise constness而不是logical constness的问题。但是不能解决所有的问题,比如constnon-const成员函数中代码冗余的问题,我们要实现两个功能完全相同的函数只是为了让常量性不同的对象调用,这样就会造成代码冗余。最好的解决方法就是non-const函数中调用const函数。注意以上这段话不能反过来,我们不能够在const函数中调用non-const函数。然而要想完成这次调用我们需要有两次转型。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <iostream>
#include <string.h>
#include <assert.h>
using namespace std;
class CTextBook
{
friend ostream& operator<<(ostream &cout, const CTextBook& text);
public:
CTextBook(const char* str)
:_text(nullptr)
,_len(0)
,_lengthIsValid(false)
{
int capacity = strlen(str) + 1;
_text = new char[capacity];
strcpy(_text, str);
}
~CTextBook()
{
if(_text != nullptr)
{
delete[] _text;
}
}
//const函数
const char& operator[](size_t position) const
{
//在这里加上检查,我们先少些点代码,假设这之前有更多的代码
//......
assert(position < length());
return _text[position];
}
//non-const函数
char& operator[](size_t position)
{
//这里只需要调用const版本的函数即可,但是为了调用要先进行类型转换
//这里用到了将本身的对象转换为常对象来调用常成员函数
const char& a = static_cast<const CTextBook&>(*this)[position];
//这里将返回值的const限定去掉
return const_cast<char&>(a);
}
size_t length() const
{
if(!_lengthIsValid)
{
//这里明显是编译不过的,因为其不是bitwiss constness的
_len = strlen(_text);
_lengthIsValid = true;
}
return _len;
}
private:
char* _text;
mutable int _len;
mutable bool _lengthIsValid;
};
ostream& operator<<(ostream &cout, const CTextBook& text)
{
cout << text._text << endl;
return cout;
}
int main()
{
CTextBook text("Hello");
const CTextBook con_text("Hello");
//con_text[0] = 'J';//常量成员调用常成员函数,无法更改
text[0] = 'J';
cout << text;
cout << text.length() << endl;
}


Jello
5

  但是要注意这其中进行了两次转型,这是十分不安全的尽量还是少使用,但是尽管如此我们还是达成了我们避免数据冗余的问题。

总结

  1、将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
  2、扁你其前置实施bitwise constness,但你编写程序时应该使用logical constness
  3、constnon-const成员函数有着实质等价时,令non-const版本调用const版本可避免代码重复。

确定对象被使用前已先被初始化

  Make sure that objects are initialized before they`re used.

C part of C++

  在Cpp中是存在不会被初始化的变量的,无论这些变量是成员变量来自类中,还是内置类型的变量都有可能编译器不会帮助我们初始化,这是十分危险的,因为会导致不明确的行为。如果我们在使用C part of C++时变量往往并不会进行初始化例如数组往往需要我们手动给一个初值,而我们在使用Cpp中的vector时编译器却可以保证即使我们不给初值这个数组也会被初始化,如果熟悉STL库的话,你会知道vecotr此时会将内部的三个成员变量全部初始化为空,长度和容量都为0。所以在使用C part of C++时我们最好手动为每一个变量赋予初值,这是很好的编程习惯。

自定义类型

  对于自定义类型以外的所有类型,他们的初始化都落在了构造函数的身上,我们所要作的就是确保每一个构造函数都将对象的每一个成员初始化。
  但是这里要搞清楚赋值和初始化的概念。这里就牵扯到了构造函数的使用。我们都知道我们可以在构造函数中对成员赋值,而是用初始化列表才可以对成员进行初始化。所以经常可以看到有些对Cpp不是很了解的程序员写出这样的构造函数。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
using namespace std;
class PhoneNumber
{
public:
PhoneNumber()
{
cout << "PhoneNumber()" << endl;
}
PhoneNumber(string theNumber)
{
cout << "PhoneNumber(string theNumber)" << endl;
string _theNumber;
}
PhoneNumber operator=(const PhoneNumber& phoneNumber)
{
_theNumber = phoneNumber._theNumber;
cout << "PhoneNumber operator=(const PhoneNumber& phoneNumber))" << endl;
}
private:
string _theNumber;
};
class ABEntry
{
public:
ABEntry(const string& theName, const string& theAddress, const PhoneNumber& theNumber)
{
//这里的构造函数写法并不是直接对成员进行初始化,更应该说是在初始化后进行了一次赋值
_theName = theName;
_theAddress = theAddress;
//所以这里会调用赋值函数
_theNumber = theNumber;
}
private:
string _theName;
string _theAddress;
PhoneNumber _theNumber;
};
int main()
{
ABEntry abEntry("Misaki", "China", PhoneNumber("181********"));
}


PhoneNumber(string theNumber)
PhoneNumber()
PhoneNumber operator=(const PhoneNumber& phoneNumber))

  这种构造函数的写法并不能为成员变量进行初始化,而是将变量先初始化后再进行赋值,在过程中产生了一次无参构造,一次有参构造,以及一次赋值,这会消耗更多的性能。而最佳的处理手段是将成员变量的初始化放进初始化列表中。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
using namespace std;
class PhoneNumber
{
public:
PhoneNumber()
{
cout << "PhoneNumber()" << endl;
}
PhoneNumber(string theNumber)
{
cout << "PhoneNumber(string theNumber)" << endl;
string _theNumber;
}
PhoneNumber(const PhoneNumber& phoneNumber)
{
_theNumber = phoneNumber._theNumber;
cout << "PhoneNumber(const PhoneNumber& phoneNumber)" << endl;
}
PhoneNumber operator=(const PhoneNumber& phoneNumber)
{
_theNumber = phoneNumber._theNumber;
cout << "PhoneNumber operator=(const PhoneNumber& phoneNumber))" << endl;
}
private:
string _theNumber;
};
class ABEntry
{
public:
//这样才能对成员进行初始化,从结果也可以看出这里少了一次无参的默认构造
ABEntry(const string& theName, const string& theAddress, const PhoneNumber& theNumber)
:_theName(theName)
,_theAddress(theAddress)
,_theNumber(theNumber)
{
}
private:
string _theName;
string _theAddress;
PhoneNumber _theNumber;
};
int main()
{
ABEntry abEntry("Misaki", "China", PhoneNumber("181********"));
}


PhoneNumber(string theNumber)
PhoneNumber(const PhoneNumber& phoneNumber)

  建议再初始化列表中对成员变量进行初始化,并且建议在初始化列表中对所有的成员包括基类都能进行手动初始化,无论是自定义类型还是内置类型,这样就不用考虑哪些成员是可以不初始化的成员,这样的编程习惯也是极好的。
  对于成员变量的初始化顺序,其顺序并不由初始化列表而决定,基类总是优先于派生类进行初始化,而成员变量是以其声明次序进行初始化的,因此如果你如果定义了一个变量并决定把它作为数组的大小来初始化数组,那么你要保证在声明数组前那个代表大小的变量应该现有值。因此为了方便理解,增强可读性我们在构造函数中书写初始化列表时最好是按照声明顺序对其进行初始化。

不同编译单元内定义之non-local static对象的初始化

  这一句话很长,很难理解,我们大概可以理解为在不同文件中的静态(static)变量的初始化顺序。我们想要在一个源文件中使用另一个源文件中定义的全局变量,但是这连个源文件中的变量初始化的先后顺序是并没有明确规定的,因此可能会出现问题。

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
29
30
31
32
33
34
35
36
37
38
39
40
FileSystem.h

#include <iostream>
using namespace std;
class FileSystem
{
public:
size_t numDisks() const
{
return _disks;
}
private:
size_t _disks = 5;
};

第一个.cpp
#include <iostream>
#include "FileSystem.h"
using namespace std;
FileSystem tfs;//其中定义一个全局变量

第二个.cpp
#include <iostream>
#include "FileSystem.h"
using namespace std;
extern FileSystem tfs;
class Directory
{
public:
Directory()
{
_disks = tfs.numDisks();
}
size_t _disks;
};
int main()
{
Directory dir;
cout << dir._disks << endl;
}

  以上这段代码中我们在一个源码文件中定义了一个成员变量,而在另一个源文件中声明外部变量并且使用了这个变量对我们的成员变量进行了初始化,但是我们并无法确定这个外部变量编译器在处理时是否会先对其进行初始化,如果没有初始化而我们又用它对我们的成员进行初始化此时就会无法进行指定的初始化。因此我们要使用一种间接的方式设计它,来保证外部成员一定会先进行初始化。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
FileSystem.h

#include <iostream>
using namespace std;
class FileSystem
{
public:
size_t numDisks() const
{
return _disks;
}
private:
size_t _disks = 5;
};
FileSystem& tfs();

第一个.cpp
#include <iostream>
#include "FileSystem.h"
using namespace std;
//这里的设计模式十分类似于单例模式
//在函数内部定义静态变量,这样保证在调用函数时一定会先对变量进行初始化
//保证初始化顺序
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}

第二个.cpp
#include <iostream>
#include "FileSystem.h"
using namespace std;
class Directory
{
public:
Directory()
{
_disks = tfs().numDisks();
}
size_t _disks;
};
int main()
{
Directory dir;
cout << dir._disks << endl;
}

  这样的设计模式更加安全可靠。

总结

  1、为内置类型进行手动初始化,Cpp并不保证会对其进行初始化。
  2、构造函数最好使用初始化列表对成员进行初始化,而不是使用赋值操作,并且初始化时最好按照声明次序进行初始化,增强可读性。
  3、为免除“跨编译单元之初始化次序”问题,最好书写接口,利用local static对象代替non-local static对象。

-------------本文结束感谢您的阅读!-------------
记录学习每一分,感谢您的赞助