写在前面:看书的过程总是看了忘,忘了看,着实让人苦恼啊,现在决定用博客来记录自己的看书学习过程,一方面总结知识、记录下来的方法比单纯的看书对知识的印象会更深刻一些,另一方面也算是对学习过程的记录和自己成长的见证。
第一章 让自己习惯 C++
本章共有如下4个条款:
条款1 视 C++为一个联邦
C++ 发展至今,已经是一个多重泛型编程语言,它支持:过程形式、面向对象形式、函数形式、泛型形式、元编程形式。可以把 C++ 看成由下面4个部分组成
- C :C是C++的基础,所以C++是完全兼容C语言的,当然,有部分的关键字和结构还是有一些区别的,如static关键字C++有了更为复杂的含义。如struct,c++拓展了结构体类型的面向对象特性,等等还有一些其他的,但是主要是拓展性的区别。简而言之,C++就是提供了更为高可用的高效编程,如模板(templates)、异常(exception)、重载(overload)等等,这些在c里面都是没有的。
- Object-Oriented C++:这部分就是C++的语言特性相关,包括面向对象的内容:classes(构造函数、析构函数)、封装(encapsulation)、继承(inheritance)、多态(polymorphism)、virtual函数(dynamic binding)、等等…这些就是OO的在C++上的具体实现。
- Template C++:这是C++的泛型编程(generic programming)部分,就是C++的函数模板(function template)、类模板(classes template),它们使得泛型编程成为可能。
- STL:STL是C++的标准模板库(Standard Template Library),它实现了一系列通用的容器(containers)、迭代器(itrators)、算法(algorithms)、以及函数对象(function objects),并且以巧妙并且不失高效的设计模式将它们连贯在一起,程序员可以通过使用STL来更为快速的开发高效的程序。
条款2 尽量以 const、enum、inline替换 #define
#define不被视为语言的一部分,在编译器处理代码之前由预处理器来处理,其特点如下:
- 不对数据分配内存空间,不进行类型检查
- 编译时只做简单的文本插入替换
- 不是语句,不在最后加分号
使用建议
- 对于单纯常量,最好以 const 对象或 enums 替换 #define
- 对于形似函数的宏,最好改用 inline 函数替换 #define
关于第二点,看如下例子:
//以 a 和 b 的较大值调用 f
#define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b))
写这种宏要注意加括号,且用起来容易出问题,可以改成下面的形式:
template <typename T>
inline void callWithMax(const T& a, const T& b)
{
f(a>b?a:b);
}
另外提到的比较重要的一点是:enum、static实现编译期间就要知道数组的大小功能
//利用 static 实现
class GamePlayer
{
private:
static const int NumTurns = 5;//常量声明式,还未定义!static要在类外定义。
int scores[NumTurns];//使用该常量
};
const int GamePlayer::NumTurns;//声明时已经有了初值,定义时不能有了,会报错。
//利用 enum 实现
class GamePlayer
{
//enum理解为记号
enum {NumTurns = 5};//令NumTurns成为5的记号
int scores[NumTurns];
};
条款3 尽可能使用const
const 作用总结如下
- 修饰普通变量:如
const int a=1;
- 修饰引用:不能通过别名修改原变量的值
int a=1;
const int &b=a;
a=2;//正确
b=3;//错误
const int c=5;
const int &d=c;//正确
int &e=c;//错误
-
修饰指针:离什么近就表示什么不可变
- 1)、const char* p表示被指物不可改变——常量(内容)指针
char s1[100],s2[100]; const char* p=s1; p=s2;//正确 p[0]='a';//错误,不能通过指针修改所指内存中的内容 *p='a';//错误
- 2)、char* const p表示指针不可以改变——指针常量
char s1[100],s2[100]; char* const p=s1; *p='a';//正确 p=s2;//错误
- 3)、const char* const p表示指针和被指物均不可改变——常指针常量
-
修饰函数
-
出现在返回值前面
const int fun1();
const int *fun2(int &a);
int * const fun3(int &a);
-
常引用作形参,表明是输入参数
fun(const int& a);
-
-
修饰类数据成员:值不能变,只能在构造函数初始化列表中初始化
-
修饰类函数成员:只能引用本类中的数据成员,不能修改它们,也不能调用类中任何非const 成员函数
形式:void fun() const;
-
修饰类对象:只能调用对象的 const 成员函数,不能修改成员。
class A
{
private:
int x;
public:
A(int a):x(a){}
void fun1()
{
x=2;//正确
cout<<"fun1"<<endl;
}
void fun2() const
{
x=2;//错误
cout<<x<<endl;//正确
}
}
int main()
{
A p(5);
p.fun1();p.fun2();//正确
const A q(6);
q.fun1();//错误
q.fun2();//正确
return 0;
}
文中有一句话值得注意:两个成员函数如果只是常量性不同,可以被重载。
如下的两个函数算是重载的:
void func();
void func() const;
条款4 确定对象被使用前先被初始化
1、对象的初始化责任落在构造函数上,所以要确保构造函数将对象的每一个成员都初始化,注意两点:
- 构造函数对成员变量的初始化是在进入构造函数本体之前;
- 如果在构造函数体内(会使用 pass by value)是赋值,而前者效率更高。
//赋值操作
A(int a,int b,int c)
{
x=a;
y=b;
z=c;
}
//在列表中初始化
A(int a,int b,int c):x(a),y(b),z(c){}
2、必须在列表中初始化的成员
- 初始化一个 const 成员
- 初始化一个 reference 成员
- 调用一个基类的构造函数,而该函数有一组参数
- 调用一个数据成员对象的构造函数,而该函数有一组参数
3、初始化顺序
- 构造函数调用顺序为:基类—>子对象—>当前类
- 数据成员初始化顺序为声明时的顺序,跟在初始化列表中的顺序无关
还有非常重要的一点:跨编译单元的静态对象初始化问题
文中提到,C++ 对定义于不同编译单元内的静态对象的初始化次序无明确定义。C++ 标准中有规定,在同一个编译单元内,静态变量初始化的顺序就是定义的顺序,跨编译单元的静态变量的初始化顺序未定义!!!具体的初始化顺序取决于编译器的实现。
看下面例子
/*第一个源码文件*/
class FileSystem
{
public:
std::size_t numDisks() const;
};
extern FileSystem tfs;
/*第二个源码文件*/
class Directory
{
public:
Directory();
};
Directory::Directory()
{
std::size_t disks = tfs.numDisks();
}
/*创建一个Directory对象*/
Directory tempDir(); //(1)式
上述代码中的(1)式,除非 tfs 在 tempDir 之前先被初始化,否则 tempDir 的构造函数会用到尚未初始化的 tfs,但实际上这是无法保证的。解决这个问题的办法是:将每个non-local static对象 搬到自己的专属函数内,这些函数返回一个reference指向它所含的对象,然后用户调用这些函数。
/*第一个源码文件*/
class FileSystem
{
public:
std::size_t numDisks() const;
};
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}
/*第二个源码文件*/
class Directory
{
public:
Directory();
};
Directory::Directory()
{
std::size_t disks = tfs().numDisks();
}
Directory& tempDir()
{
static Directory td;
return td;
}
进行上述修改以后,调用的方式直接使用 tfs() 和 tempDir() 代替 tfs 和 tempDir。
上述方法的基础在于:C++ 保证,函数内的 local static 对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。也就是说,如果一个静态对象被定义在函数内,直到它所在的函数被第一次调用时才会被初始化。利用这样的特性,我们可以确保一个静态实例被读取时已被初始化。这就消除了跨编译单元的静态对象的构造顺序不确定问题。
更多的例子在 C++静态变量的初始化 这篇文章中也有提到。
参考文章:
1、https://segmentfault.com/a/1190000014348286
2、https://blog.csdn.net/vict_wang/article/details/85763265
3、https://www.jianshu.com/p/35ddcc56b735
4、https://www.jianshu.com/p/dd34cee5242c