Effective C++ 阅读笔记(二)

本文介绍了C++编程中关于函数声明、异常处理、赋值操作等的最佳实践,帮助开发者避免常见陷阱,提升代码质量和可维护性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

05:了解C++默认编写、调用的函数

  • 如果没声明,编译器会声明一个copy构造函数,一个copy assignment操作符,和一个析构函数;如果没有声明任何构造函数,编译器也会声明要给default构造函数。
  • 包含reference和const成员的class,编译器会拒绝自动编写assignment操作;如果base classes将copy assignment声明为private,编译器将拒绝为derived classes生成copy assignment函数。

06:如果不想使用编译器自动生成的函数、那就明确拒绝

建立新类:

class Uncopyable {
	protected:
		Uncopyable() {} //允许子类构造和析构
		~Uncopyable() {}
	private:
		Uncopyable(const Uncopyable&); //阻止拷贝
		Uncopyable& operator=(const Uncopyable&);
};

然后新类就继承这个类。

其实新标准里面可以使用=delete来进行禁止。

07:为多态基类声明virtual函数

  • polymorphic(带有多态性质的) base classes 应该声明一个 virtual 析构函数。如果 class 带有任何 virtual 析构函数,那他就应该拥有一个 virtual 析构函数。
  • classes 设计的目的如果不是作为 base classes 使用,或者不是为了具备多态性(polymorphically),就不该声明为 virtual 函数

原因是,如果基类虚构函数是 non-virtual 的,那么 derived class 由 base 指针删除时,可能 derived 成分,也就是 derived class 中新声明的变量无法被正确析构。

而声明为析构函数会使得对象占用额外的空间用来保存函数指针。

08:别让异常逃离析构函数

  • 析构函数不应该抛出异常。如果一个析构函数调用的函数可能抛出异常,析构函数应该捕获任何异常,然后吞下他们(不传播)或者结束程序。
  • 如果用户需要对某个操作函数期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作。

比如读取文件操作,最后需要关闭文件。一个较好的的做法可以是:

class FileManager {
	bool isClosed;
	public:
		...
		void close () {
			file.close();
			isClosed = true;
		};
		
		~ FileManager() {
			if (!closed) {
				try {
					file.close();
				} catch (...) {
					//记录对close调用失败,吞下异常或者结束程序
				}
			}
			...
		}
};

可以让用户处理关闭操作,并进行异常捕获,如果没有进行操作,默认提供一个,如果出现了异常,就吞下或者结束程序。

09:不在构造和析构过程中调用virtual函数

  • 不要在析构或构造期间调用 virtual 函数,因为他们不会下降到 derived class,而是在构造啥类型期间,就调用啥类型的 virtual 函数。

在 derived class 的 base class 构造过程中,对象的类型是 base class,并且不仅 virtual 函数会被编译器解析到(resolve to) base class,运行期间的类型信息(runtime type infomation, e.g. dynamic_cast, typeid)也会把对象当作base class。

析构的时候也是这样,一旦 derived class 析构函数执行,对象内的 derived class 成员变量变成先未定义值,进入 base class 之后就变成一个 base class 对象。

但是,如果想要每一个 derived class 被创建时,都有适当的一些信息被输出,要怎么做呢?可以在 base class 内创建一个 non-virtual function,并在构造函数中调用这个函数;derived class 在构造时,将必要的信息传给这个基类的构造函数。

class BaseClass {
	public:
		BaseClass(string& info) {
			...
			printLogInfo();
		}
		
		void printLogInfo(string info} {...}
};

class DerivedClass : public BaseClass {
	public DerivedClass(param) : BaseClass(createInfo(param)) {...}
	string createInfo(parm) {...}
}

10:令operator= 返回一个reference to * this

令赋值操作(assignment) (=, += ,-=, *=)返回一个 reference to *this。

为了实现类似:

int x, y, z;
x = y = z = 15;

之类的连锁赋值。

11:在operator= 中处理自我赋值

  • 确保对象自我赋值时,operator = 有良好的行为。其中计数包括
    • 比较“来源对象”和“目标对象”的地址
    • 精心周到的语句顺序
    • copy-and-swap
  • 确定任何函数如果操作一个以上的对象,其中多个对象是同一个对象时,行为仍然正确

比如有一个类:

class A {
	public:
		A(B& b) { pb = &b; }
	private:
		B* pb;
	...
}

使用下面的 operator= 代码,看上去很合理,但是自我赋值会出现问题:

A& A::operator=(const B& rhs) {
	delete pb;
	pb = new A(*rhs.pb);
	return *this;
}

这时,如果当前对象传入的对象是同一个对象,那么当程序执行完之后,rhs就指向了一块被删除的地址。

一个想法是,在最前面加一个“证同测试(identity test)”来达到“自我赋值”的检验

A& A::operator=(const B& rhs) {
	if (this == &rhs) return *this;
	
	delete pb;
	pb = new A(*rhs.pb);
	return *this;
}

但这样并不具备异常安全性,同时效率比较低

  • 如果new时,内存不足,或者拷贝构造函数出现异常,那么pb都会指向一块被删除的地址
  • if 语句会使程序更大,并且引入一个新的控制流分支,二者都会降低速度。Perfectching, caching 和 pipelining 等指令的效率都会因此降低。

精心安排一下语句的顺序,可以解决异常安全问题:

A& A::operator=(const B& rhs) {
	auto tempPb = pb;
	pb = new A(*rhs.pb);
	delete pb;
	return *this;
}

这是,如果 new 语句发生异常,两个变量中的pb仍然会保持原状。

一个替代方案是 copy and swap技术:

class A {
	...
	void swap(A& rhs); // 交换*this 和 rhs 的函数
	...
}

A& A::operator=(const B& rhs) {
	A temp(rhs);
	swap(temp);
	return *this;
}

或者是:

A& A::operator=(const B rhs) {
	swap(rhs);
	return *this;
}

这种写法利用了两个技术:

  • 某 class 的 copy assignment 操作符可以被声明为“以 by value 方式接受实参”
  • by value 方式会造成一份副本

这种方式牺牲了清晰性,不过将 copying动作 从函数本体移动到 函数参数构造阶段 有时可能会令编译器产生更高效的代码。

12:赋值对象时勿忘其中每一个成分

  • copying 函数应该确保赋值“对象内所有成员变量”以及“所有base class”成分
  • 不要尝试以某个 copying 函数实现另一个 copying 函数,应该将共同的技能放在第三方函数中,并且由两个 copying 函数共同调用。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值