天天看点

Effective C++ 读书笔记(1)

  写在前面:看书的过程总是看了忘,忘了看,着实让人苦恼啊,现在决定用博客来记录自己的看书学习过程,一方面总结知识、记录下来的方法比单纯的看书对知识的印象会更深刻一些,另一方面也算是对学习过程的记录和自己成长的见证。

第一章 让自己习惯 C++

本章共有如下4个条款:

  • 条款1 视 C++为一个联邦
  • 条款2 尽量以 const、enum、inline替换 #define
  • 条款3 尽可能使用const
  • 条款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

继续阅读