《编写高质量代码:改善C++程序的150个建议》读书笔记4

笔记4

本章是关于类和运算符重载的。

(之前写了两个多小时,然后接了个电话,回来什么东西都没有了,CAO......)


1. class和struct的区别。

这个是一般面试里面也会有的题目,之前看到的回答是关于访问权限的,struct默认是public的,而class默认是private的。

其它还有:在继承方面struct默认是public继承,而class默认是private继承。

另外在初始化的时候在大括号初始化方面也有些差别,但是因为C++11中已经统一了,具体也就不提了。


2. 编译器会创建默认的成员函数。

这个在c++笔试题总结1中的16题中已经提到了。

另外,在C++11标准中提供了两个关键字default和delete来控制创建或者不创建默认的成员函数:

class CLS {
public:
	//使用默认的构造函数和析构函数
	CLS() = default;
	virtual ~CLS() = default;
	//不要默认的赋值运算符和拷贝构造函数
	CLS & operator=(const CLS &c) = delete;
	CLS(const CLS &c) = delete;
};

3. 类成员的初始化首选初始化列表。

原因有以下几个:

1) 对于const和引用成员,只能使用初始化列表的方式;

2) 某些赋值操作被禁用的成员也只能使用初始化列表;

3) 某些情况下初始化列表更加高效,至少也不会表赋值操作那种方式差;

4) 由于编译器可能创建默认赋值运算符,而这种默认的情况并不一定满足要求,这就可能存在隐患。

另外还提了一下初始化顺序的问题,需要注意,初始化顺序由类内成员的声明顺序决定,而不是初始化列表中的顺序决定。


4. 某些情况下不能使用对象的复制,那么就要果断的屏蔽复制构造函数和赋值操作符。

具体的做法有几种:

1) 声明成private的。这种方法有个问题就是成员函数和友元还是能够访问;

2) 只声明不定义。这个是可以的,但是如果使用了就会在链接时报错,间接地告诉用户不要使用;

3) 定义一个不能复制和赋值的基类(使用上述的方法),然后自定义的类就继承该类,这个方法已经在Boost库中使用。


5. 注意自定义拷贝函数。

赋值运算符和拷贝构造函数说到底都是一个拷贝,因此统称为拷贝函数。

默认的拷贝构造函数都是浅拷贝,意思是将类中的非static数据成员都复制一份。这样就会产生一个问题,如果类中有指向其它资源的成员,那么浅拷贝后,这个资源就会有两个类中的成员指向它,这就可能导致问题。

因此就有了深拷贝的概念,即资源也被复制了一份。

深拷贝需要自定义拷贝构造函数,这个时候就需要特别的注意。


6. 注意构造函数出错的情况。

构造函数也会出错,但是因为构造函数没有返回值,因此C++标准建议出错时抛出异常。

但是这样也还有问题,比如一个类在构造函数出错前就就已经分配了资源,如果直接抛出异常,那么这些资源就不会被释放,导致内存泄漏。

为了解决这个问题,有几个方法:

1) 在抛出异常(try...catch)之前先释放资源; 

2) 使用init()和release()函数,在构造函数中调用init()函数,来完成构造对象的代码,这个函数可以有返回值,如果返回错误就调用release()。


7. 多态基类的析构函数需要是virtual的;构造函数不能是virtual的。

这个就不多说了。


8. 不要在构造函数和析构函数中调用虚函数。

因为这个时候的虚函数没有多态性。下面是一个例子:

#include <iostream>
using std::cout;
using std::endl;

class Base {
public:
	//使用默认的构造函数和析构函数
	Base() {
		cout << "Base constructor" << endl;
		func();
	}
	virtual ~Base() = default;
	virtual void func(void) {
		cout << "Base func" << endl;
	}
};
class Drived : public Base {
public:
	Drived() :Base() {
		cout << "Drived constructor" << endl;
	}
	void func(void) override {
		cout << "Drived func" << endl;
	}
};
int main() {
	Base *b = new Drived();

	return 0;
}

打印的结果如下:

Base constructor
Base func
Drived constructor
请按任意键继续. . .

注意这里打印的是Base func,即调用的是基类的虚函数func()。


9. 构造函数也可以有默认参数,但是需要注意。

下面是一个错误的例子:

#include <iostream>
#include <string>
using std::string;
using std::cout;
using std::endl;

class CLS {
public:
	CLS(int a, string s = "") : age(a), name(s) {

	}
	CLS(int a) {
		age = a;
		name = "";
	}
private:
	int age;
	string name;
};
int main() {
	CLS *c = new CLS(10);//报错

	return 0;
}

此时的new CLS(10)使编译器不知道要调用哪个构造函数了。


10. 搞清楚重载,重写和隐藏。

重载主要是用来函数上,即同一作用域内,函数具体有相同的函数名和不同的参数。

重写是继承关系中,子类对父类的虚函数的重新实现。

隐藏也是在继承关系中,它是只子类和父类具有相同名字的非虚函数,此时子类的函数就将父类中的同名函数屏蔽掉。


11. 重载operator=有标准的三步走。

1) 一定要检查自赋值,即operator=的实现中要首先保证=两边不是同一个东西(尤其是C++引入引用之后,更容易出现=两边是一样的东西的情况),所以这里还要注意opeartor==的重载,不然=两边的比较可能没有意义,或者是错误的。

2) 最后返回*this。

3) 重载的operator=并不会被继承。这是因为编译器会创建默认的版本,隐藏了父类的重载版本。

所以重载opeartor=的结构一般是下面的的形式:

CLS & CLS::operator=(const CLS &c) {
  //自赋值检查
  //释放原有的空间,申请新空间,数据复制
  //返回*this
}

12. 重载运算符应该是类的成员函数还是友元函数。

将重载运算符作为成员函数和友元函数的差别:

1) 作为成员函数,则默认的一个运算操作数就是this,因此将重载运算符作为成员函数时,双目运算符只有一个参数,单目运算符没有参数;而作为友元的话,参数和运算操作数是对应的;

2) 由于成员函数一个操作数是固定的,因此不能有隐式转换了,而对于友元的话,双目运算符两边的操作数都可以有隐式转换。

一般的规则是:双目运算符用友元函数;单目运算符用成员函数。

当然例外很多,比如:

1) =,函数调用符(),下标运算符[],指针->都和this关系密切,会是成员函数;

2) <<的第一个操作时必须是ostream类型,所以<<只能是友元函数。


13. 某些运算符要成对重载。

比如==和!=,>和<,>=和<=。

另外,某些双目运算符比如A+=B,可以避免A被多次运算,效率上更高。一般+=和+也会成对重载,且为了避免代码重复,operator+就用operator+=来实现。


14. 自增自减有前后缀的差别,因此重载时也要注意。

class CLS {
public:
	CLS & operator++();		//前缀
	const CLS operator++(int);	//后缀
	CLS & operator--();		//前缀
	const CLS operator--(int);	//后缀
};

注意这里的const,表示了CLS++++这样的形式是不对的,跟内置类型int i; i++++会报错保持了一致。


15. 不要重载|| && 和逗号“,“。

原因是这些运算符的左右两个的执行顺序没有办法控制,就会导致与原来的这些运算符的特性不符。


16. 合理使用inline函数来提升效率。

几点说明:

1) inline函数跟宏一样也是代码替换,但是它又遵守函数的类型和作用域规则。

2) inline一般要与函数的定义放在一起使用,光跟声明放在一起没有用。

3) 类内部定义函数体,无论是否使用inline关键词,该函数都是inline的。

几点注意:

1) 编译器要不要内联还是它自己决定的,对于过于复杂的函数,即使写了inline,也可能不会真正内联。

2) 内联也会导致代码膨胀。

3) 不要内联构造函数和析构函数。


17. 慎用私有继承。

私有继承将父类中的所有成员都变成了private,这样父类中的内容就只能作为子类的实现细节,而不能作为子类的接口。

公有继承是is-a的关系,而私有继承就跟is-a没有关系了,只是子类想要用到父类中的某些算法。

大部分情况下可以用组合来替代私有继承,下面是一个例子:

#include <iostream>
using std::cout;
using std::endl;

class Engine {
public:
	void start() {
		cout << "Engine start" << endl;
	}
};
//私有继承版本
class Car1 : private Engine {
public:
	void move() {
		start();
	}
};
//组合版本
class Car2 {
public:
	Car2(Engine *e) {
		this->engine = e;
	}
	void move() {
		if (NULL != engine) {
			engine->start();
		}
	}
private:
	Engine *engine;
};
int main() {
	Car1 *c1 = new Car1();
	c1->move();
	Engine *e = new Engine();
	Car2 *c2 = new Car2(e);
	c2->move();
	return 0;
}

当然也有一些情况下只能使用私有继承,而没有办法使用组合,比如:

1) 子类要访问父类的受保护的成员时。

2) 需要重定义继承了的虚函数时。


18. 不要使用多重继承。


19. 小心对象切片。

这里切面的原因是没有使用对象的指针或者引用,而是使用了对象本身。下面是一个例子:

#include <iostream>
#include <string>
using std::string;
using std::cout;
using std::endl;

class Bird {
public:
	Bird(const string &name):_name(name){}
	virtual string Feature() const {
		return _name + "can fly";
	}
protected:
	string _name;
};
class Parrot : public Bird {
public:
	Parrot(const string &name, const string &food) : Bird(name), _food(food) {}
	virtual string Feature() const {
		return _name + "can fly and eat " + _food;
	}
private:
	string _food;
};
void DescribeBird(Bird b) {
	cout << b.Feature() << endl;
}
int main() {
	Bird bird("Crow");
	DescribeBird(bird);
	Parrot parrot("Polly", "millet");
	DescribeBird(parrot);
	return 0;
}

打印的结果如下:

Crowcan fly
Pollycan fly
请按任意键继续. . .

显然不是我们想要的结果。

这是因为多态必须依靠指向同一类族的指针或者引用来实现。

修改上面的DescribeBird()函数:

void DescribeBird(Bird &b) {
	cout << b.Feature() << endl;
}

问题就解决了。


20. 将数据成员声明为private。


21. 如果要在main函数之前执行某些操作,可以把他们放在全局对象的构造函数中。

下面是一个例子:

#include <iostream>
using std::cout;
using std::endl;

class CLS {
public:
	CLS() {
		cout << "CLS before main()" << endl;
	}
};

CLS cls;
int main() {
	cout << "main()" << endl;
	return 0;
}

执行的结果是:

CLS before main()
main()
请按任意键继续. . .

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值