1,More Effective C++——基础议题(条款1—4)

条款1: pointer和reference

0 指针(pointer)

C++中的指针可以如下形式进行定义:

int number = 0;
int *ptr = &number; // 可修改number值,也可以改变指针指向。
const int *ptr0 = &number; // 不能修改number值,可改变指向
int const *ptr1 = &number; // 可修改值,不能改变指向
const int const *ptr2 = &number; // 不能修改值,也不能改变指向

1 引用(reference)

C++语言标准引入了“引用”类型。其必须在声明时给予明确定义。如下代码所示。通过修改对ref的修改,我们可以修改变量number的值——ref为number的一个替身。

int number = 0;
int &ref = number;

相应于pointer,我们也可以有如下几种定义形式。由于reference一旦定义,将无法改变其指向,因此,第二种形式非法。

const int &ref = number;  // 合法。不能修改值
int const &ref0 = number; // 非法。

2 比较与综合

由于pointer可以有第二种状态,即null。因此,如果我们欲让pointer什么都不指代,即可赋值给pointer为null。但是reference必须有所指代,即必须完成指向对象的初始化工作。因此,我们得到如下规则:

1,若对象必须指代一个实体,则使用reference。
若指代的实体不能发生变化,则使用reference。
2,若可能需要变量什么都不指,出现空置状态;或者指向对象可以变化,则使用pointer。

条款2:使用C++的转型操作

C语言中只有“自动类型转换”和“强制类型转换”,两种转换都不够暗转——无法知道类型转换是否成功,或者类型转换是否恰当。

C++引入如下四个操作符进行类型转换

1, static_cast:相当于C中的自动转换,如short类型转换成int型。
2, const_cast:转换变量的可修改性。将const类型转换成non-const类型。
3, dynamic_cast:用于基类与派生类之间的类型转换。此种转换通常用于多态,将基类指针转换成派生类指针。转换失败会返回null指针,或抛出exception。该转换只有在virtual-function存在的情况下才有可能成功。
4, reinterpret_cast:常用于转换函数指针。

以下将举出类型转换的实例。

1 static_cast

截取浮点数中的整数部分:

double number = 1.234567;
int integer = static_cast<int>(number);

2 dynamic_cast

class BaseClass {};
class DerivedClass : public BaseClass {};
......
BaseClass *baseArray[10];
derived = dynamic_cast<DerivedClass*>(baseArray[0]);

3 const_cast

移除常量性(const)或者变异性(volatile):

void refresh(int &number);
const int number = 2;
refresh(const_cast<int&>(number)); // 正确
refresh(number); // 错误

4 reinterpret_cast

该类型转换不具备移植性。其常用于“函数指针”的类型转换。如下例所示。

typedef void (*FuncPtr)();
FuncPtr funcPtrArray[10];

int doSomething();

由于doSomething函数的类型FuncPtr的类型不匹配,因此只有使用reinterpret_cast才能够将该函数放入funcPtrArray数组中。如下所示。

funcPtrArray[0] = &doSomething; // 非法,类型不匹配
funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething); // 合法,强制类型转换成功

5 不推荐的方法

若编译器较老,没有提供类型转换操作符,可以利用强制类型转换来定义出自己的宏,但这种方式不够安全,因此不推荐使用。

#define xxxxxx_cast(TYPE, EXPRESSION) ((TYPE)(EXPRESSION))
// 比如
#define const_cast(TYPE, EXPRESSION) ((TYPE)(EXPRESSION))

6 类构造函数默认类型转换

下面代码展示了,由于Fruit类中存在一个接收int类型的构造参数,因此,int类型可以转换成Fruit类型。这种类型转换增加了类的复杂性。C++提供explicit关键字来禁止默认类型转换。

// 例1:
class Fruit {
	Fruit(int number);
}
Fruit aFruit = 2; //合法,因为可以调用一个参数的构造函数。

// 例2
class Fruit0 {
	explicit Fruit(int number);
}

Fruit0 aFruit0 = 2; // 非法,必须明确调用相应的构造函数
Fruit0 aFruit1(0); // 合法,明确调用构造函数

条款3:勿用多态技术处理数组

本条款内容相对过时,其中涉及到的技术问题在现代编译器中得到妥善解决。由于涉及到底层的具体细节,因此这些问题依然值得关注。

1 数组元素长度变化

下面代码定义了基类二叉树,和子类平衡二叉树。假设两种类型的树中,元素均为int。我们打算通过一个函数中序输出两棵树中的节点。

class BST {
public:
	BST() {};
	virtual ~BST() {}
	
	// some function 
	// some variable
}

class BalanceBST : public BST {
public:
	BalanceBST() {}
	virtual ~BalanceBST() {}
	
	// some function
	// some varialbe
}

...
void printArray(ostream& out, const BST tree[], int num) {
	for (int index = 0; index < num; ++index) {
		out << tree[index];
	}
}

BST tree[100];
BalanceBST baBST[100];

printArray(out, tree, 100);
printArray(out, bsBST, 100);

显然,派生类BalanceBST的大小通常和基类BST不一致,当使用派生类调用函数printArray时,使用索引访问tree[index]势必会造成错误,因为:

索引访问数组tree[index],实际是指针算术表达式的简写:*(tree+index)。每次所跨越的空间大小,是基类对象的大小。即tree[index-1]与tree[index]之间相差一个BST大小的空间。

但是,gcc 5.x编译器很好地解决了此种问题——编译器可以正确识别动态类型,并应用于数组中。

2 删除数组元素

同上面情况类似,当我们使用基类数组删除一个派生类数组的时候,只会调用基类的析构函数,从而导致派生类析构函数没有调用,产生内存泄漏的问题。如下所示。

void destruct(BST array[]) {
	delete [] array;
}

BalanceBST *baBST = new BalanceBST[100];
destruct(bsBST);

目前,C++规范中表示,删除一个又基类指针指向的派生类数组,结果未定义。
不要让具有实际意义的具体类继承自另一个类,而是采用组合方式,将会更容易维护。

条款4:非必要不默认构造函数(default constructor)

在类中,不是所有成员变量都具有两种状态——有意义的值和无意义的值。一些对象总是需要一个有意义的值。因此,必须通过传入参数来控制这些成员变量。

默认构造函数没有参数传入的构造函数。当我们不声明构造函数时,编译器会自动给出默认构造函数;一旦我们计划声明一个传入参数的构造函数,那么默认构造函数必须手动声明。

不定义构造函数会出现如下问题。

1 无法定义数组

若不给出默认构造函数,下面的代码将无法执行。

class DefaultClass {
public:
	explicit DefaultClass(int number) {}
}

DefaultClass *sample = new DefaultClass[10];
DefaultClass sample[10];

解决途径有两种
(1) 可以通过先声明指针数组,然后再定义对象来解决。

DefaultClass *sample[10];
for (int index = 0; index < 10; ++index) {
	sample[index] = new DefaultClass(index);
}

(2)采用"placement new"技术,即先开辟一块内存空间,再对该内存空间进行初始化。

// 开辟内存空间
void *rawMemory = operator new[](sizeof(DefaultClass) * 10);
DefaultClass *sample = static_cast<DefaultClass*>(rawMemory);
// 初始化内存
for (int index = 0; index < 10; ++index) {
	new (&sample[index]) DefaultClass(index);
}

多数人不太熟悉此方法。同时,当我们需要释放开辟的内存时,需要用rawMemory来释放,不能使用sample来释放。

for (int index = 9; index >= 0; --index) {
	sample[index].~DefaultClass();
}
operator delete [](rawMemory); 

// 下面为错误的方法
delete [] sample;
2 不能使用模板容器类

STL提供的模板容器类需要调用传入的类模板的构造函数。如下面代码,开辟空间和创建对象一次性完成,因此若没有默认构造参数,容器将无法初始化对象。

std::vector<DefaultClass> defaultVector; // 非法,没有默认构造函数,不能初始化空间。
3 需要深层次传入参数

当类继承体系过于复杂,继承层次比较深的时候,若没有默认构造函数,最底层基类所需的构造参数,可能需要最顶层的派生类来传送,从而导致接口过于庞大。

总结

原本计划每篇文章控制在千余字,稍不留神便到了4500字之多。考虑到英文每个字母都算作一个字,因此篇幅应该还算可以忍受。

本篇4个条款只是作为引子,为后面针对四方面的内容——指针、空间分配、多态、类型转换,作一个简要铺垫。后面内容将紧紧围绕这4个方面内容,作进一步详细讲述。而这4方面,也是C++种较难维护的问题。

猜你喜欢

转载自blog.csdn.net/zhizifengxiang/article/details/82942872
今日推荐