现在的位置: 首页 > 综合 > 正文

《Effective C++》让自己习惯C++:条款1-条款4

2017年10月12日 ⁄ 综合 ⁄ 共 5780字 ⁄ 字号 评论关闭

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

C++实在C的基础上发展而来,C++是兼容C的,包含了C的所有特性,另外增加了新的特性。

C是面向过程的语言,它的侧重点在于算法和数据结构。编写C代码,侧重点在于通过输入数据,设计一定的算法过程,得到输出。

C++是面向对象的语言,它的侧重点在于抽象出对象模型,使这个模型和问题像契合。通过对对象状态的控制,来解决问题。

C++不同于C的部分包含:

class:虽然C语言也有结构体struct,但是它更多的侧重于数据机构,侧重数据的组织。虽然struct在++中也支持class的个各种特性,但是很少用struct去替代class。它们两个一个不同在于class默认成员的访问是private,而struct是public。

template:模板属于泛型编程,泛型编程使得代码独立于特定的数据类型,可以大大减少代码量。

overload:重载是C语言中没有的,在一些代码中经常看到external “C",这是表示以C语言方式编译。因为重载是通过编译时,在函数明后加上函数参数类型来生成函数名实现的,而C语言则不是,所以如果要给C调用,就要加上extern "C"。

除了以上几点,还有其他,例如引用、异常等。

可以将C++分为4个层次:

1、C:C++实在C语言的基础上发展而来的。

2:Object-Oriented C++:这是C++中不同于C的部分,这里主要指面向对象。

3:Template C++:C++中的泛型编程。

4:STL:这是一个标准模板库,它用模板实现了很多容器、迭代器和算法,使用STL往往事半功倍。


条款2:尽量以const、enum、inline替换#define

这句话也可以说:以编译器代替预处理。

例如,你使用预处理定义了圆周率

#define PI 3.1415926

在预处理时, 所有使用PI的地方都将被替换,之后编译器在编译时从未看到过PI。这时如果遇到错误,报错时给出的是301415926,而不是PI,因为PI从未进入到符号表,这将导致错误难以理解。一个替换的方法是如下定义:

const double PI=3.1415926

当用常量替换#define时,有两点要注意

1:替换字符串时,要定义成常量指针,而不是指向常量的指针,例如定义一个名字

#define name "xiaoming"

替换时定义成

char* const name="xiaoming"

而不是

const char* name="xiaoming"

2:专属于class作用域的常量。专属于class的常量将这个常量限定在class的作用域内,而#define定义的常量没有作用域的限制,一旦在某一处有个宏定义,在其后面都有效(除非#undef)。

class GamePlayer{
	static const int NumTurns=5;
	int scores[NumTurns];
};

NumTurns只在GamePlayer的作用域内有效。NumTurns会分配内存(虽然它只有一个实例),我们可以获取它的地址。通过枚举enum可以避免内存分配,当然也就无法获取其地址了。

class GamePlayer{
	enum{NumTurns=5};
	int scores[NumTurns];
};


现在再来看宏定义,加入定义一个比较大小的宏

#define MAX(a,b) ((a)>(b)?(a):(b))

这样调用后a的值为11。

int a=10;
MAX(a++,12);


而这样调用后,a的值为12。这取决于宏的展开。

int a=10; MAX(a++,2);

这样的不确定性是不允许的。

在宏定义中经常见到do{}while{0},至于原因可以看这里

虽然C++中可以降低对宏的依赖,但是宏定义依然非常必要。在一些库中,尤其涉及到跨平台的库中,经常看到非常多的宏定义。另外使用宏,可以提高程序的可读性,例如

#define long long ll

定义长长整型。

最后看一个有意思的题目:判断下面的输出

#include <stdio.h>
#define f(a,b) a##b
#define g(a)   #a
#define h(a) g(a)
int main() {
    printf("%s\n",h(f(1,2)));
    printf("%s\n",g(f(1,2)));
    return 0;
}

答案是:

12

f(1,2)

这涉及到宏的展开过程:Macro arguments are completely macro-expanded before they are substituted into a macro body, unless they are stringified or pasted with other tokens.
After substitution, the entire macro body, including the substituted arguments, is scanned again for macros to be expanded.
The result is that the arguments are scanned twice to expand macro calls in them.

条款3:尽可能使用const

使用关键字const修饰的变量不允许被改动。

使用const修饰指针或指针指向的对象时要注意:

const T * point或T const* point表示point指向的对象是常量。

T * const point;表示point指针是常量指针。

STL中的迭代器类似指针,当需要使用一个常量迭代器时(即迭代器不能改动,其指向的元素可以修改,类似T * const point)

const vector<T>::iterator iter;

如果需要迭代器指向常量,类似const T* point

vector<T>::const_iterator iter;


const成员函数

const成员函数的目的是确认该函数可以用到const对象上。const成员函数使得1、class接口更加容易理解,确认哪些接口可以修改class成员。2、使操作const对象成为可能。

关于第2点,是因为const对象只能调用const成员函数;但是非const对象既可以调用普通成员函数,也可以调用const成员函数。这是因为this指针可以转换为const this,但是const this不能转换为非const this。

一个函数是不是const是可以被重载的。

关于const成员函数有2个概念:bitwise constness和logical constness

bitwise constness是指const对象的每一个bit都不能被修改,这个说法是正确的。但是包含指针时,有时就比较费解。

class CTextBlock{
public:
	char& operator[](std::size_t position)const
	{
		return pText[position];
	}
	char * pText;
      int length;//指针pText指向内存的长度
};

上面的[]重载是个const成员函数,返回一个引用。这个引用并不属于对象,因为指针pText指向的内存并不在类中定义,所以可以修改[]的返回值,这好像有点不符合逻辑,但这的确是符合bitwise constness,因为并没有修改指针pText。所以,const成员函数并不能实现logical constness。

如果我们修改了指针pText指向的内存,那么length可能会变,const函数不能修改,怎么办?

可以使用mutable关键字。

在const和非const成员函数中避免代码复制

在一些类中,const成员函数和non-const成员函数功能类似,在这两个函数中都要执行相同的代码,比如一些检查等。

class CTextBlock{
public:
	const char& operator[](std::size_t position)const
	{
		prepare();//准备
		return pText[position];
	}
	char& operator[](std::size_t position)
	{
		prepare();//准备
		return pText[position];
	}
	void prepare()const;//一些准备动作
	char * pText;
	int length;
};

这样看起来比较臃肿,可以是用来类型转换,把non-const转换为const,再转换其返回值类型

class CTextBlock{
public:
	const char& operator[](std::size_t position)const
	{
		prepare();//准备
		return pText[position];
	}
	char& operator[](std::size_t position)
	{
		return const_cast<char&>(static_cast<const CTextBlock&>(*this)[position]);
	}
	void prepare()const;//一些准备动作
	char * pText;
	int length;
};

条款4:确定对象使用前已先被初始化

在C++中,使用一个对象前要确保这个对象已经初始化。例如
int x;

在一些语境下会初始化为0,但在另一些语境下可能就不会初始化,例如

class Point{
<span style="white-space:pre">	</span>int x,y;
}

如果使用未初始化的对象,可能会导致不确定的行为。所以,在使用对象之前,一定要将其初始化。

对于内置的数据类型,可以手工完成初始化:
int x=0;
const char* text="A C-style string";

double d;
std::cin>>d;//以读取input stream方式完成初始化

对于内置类型以外的数据类型,则通过构造函数完成初始化。

在构造函数中完成初始化时,要区分赋值和初始化。在构造函数体内的是赋值,在初始化列表中的才是初始化。
下面的是赋值:
class Point{
public:
 Point(int x_, int y_)
	{
		x=x_;
		y=y_;
	}
	int x,y;
}

下面才是初始化,要注意,初始化顺序要和声明顺序一致。

class Point{
public:
 Point(int x_, int y_):x(x_),y(y_)
	{
	}
	int x,y;
}

初始化效率往往高于赋值。赋值是先定义变量,在定义的时候可能已经调用了变量的构造函数,之后赋值是调用了赋值操作符。

而初始化是直接调用了复制构造函数。
在一些情况下必须使用初始化方式,有些遍历在定义时就要求初始化,例如const和引用。
在C++的类继承中,基类先于派生类初始化。

下面还要讨论一下“不同编译单元内定义的non-local static对象”的初始化。

static对象的生命周期为从其构造出来到程序结束,在栈和堆上的对象显然不合符。static对象包括全局(global)对象(虽然没有用static修饰)、定义在namespace作用域内、在class、函数内、以及在file作用域内声明的static对象。
函数体内的static对象是local static对象,其他static对象是non-local static对象。
编译单元是指产出单一目标文件的源码,是源码文件加上其包含的头文件(#include files)。

现在有至少两个源码文件,每一个里面至少含有一个non-local static对象。如果一个编译单元的non-local static对象初始化使用了另外一个编译单元的non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对“定义在不同编译单元内的non-local static对象”初始化次序无明确规定。
举个例子:现在有一个FileSystem class。由于某种需求,在global或namespace作用域内有一个non-static对象
class FileSystem{
public:
……
std::size_t numDisks()const;
……
};
extern FileSystem tfs;//定义在global作用域

现在一个客户新建一个类来处理文件系统内的目录,会使用定义在global作用域内的tfs

class Directory{
public:
	Directory( params )
	{
		……
		std::size_t disks=tfs.numDisks();
		……
	}
};

Directory tempDir( params);
如果tfs初始化晚于tempDir,那么tempDir会使用尚未初始化的tfs。tfs和tempDir是不同的人写的源码,定义在不同的编译单元,无法确定哪一个先初始化。
一个改动即可消除上述问题。既然static对象只有一份拷贝,且只初始化一次,很容易想到“单例模式”。使用local static对象,首次使用时初始化,返回其引用即可(local static声明周期是整个程序),以后再使用无需再次初始化。
class FileSystem{
public:
……
std::size_t numDisks()const;
……
};
FileSystem& tfs()
{
	static FileSystem fs;
	return fs;
}

class Directory{
public:
	Directory( params )
	{
		……
		std::size_t disks=tfs.numDisks();
		……
	}
};

Directory tempDir()
{
	static Directory td;
	return td;
}

这样修改之后,客户代码无需修改即可使用。

reference-returning函数往往简单,可是定义为inline函数。使用non-const static对象,在多线程环境下,会有不确定性。如果多个线程同时条用未初始化的reference-returning函数,会有“竞争形势(race conditions)”。消除的方法是:在单线程启动阶段手动调用这些函数。

总结:为了避免使用未初始化的对象,要做三件事1、手动初始化non-member对象。2、使用初始化列表初始化member对象。3、消除初始化次序的不确定性。

抱歉!评论已关闭.