Effective C++ Notes 4. 设计与声明

条款18:让接口容易被正确使用,不易被误用

==> 请记住:

(1)好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
(2)“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
(3)“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
(4)tr1::shared_ptr 支持定制型删除器(custom deleter)。这可防范DLL问题,可被用来自动解除互斥锁(mutex)等等。


条款19:设计class犹如设计type

==> 请记住:

(1)class的设计就是type的设计。在定义一个新type之前,请确定你已经考虑过本条款覆盖的所有讨论主题。


条款20:宁以pass-by-reference-to-const 替换 pass-by-value

==> 请记住:
(1)尽量以pass-by-reference-to-const 替换 pass-by-value。前者通常比较高效,并可避免切割问题(slicing problem)。
(2)以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。

==> 解析:

相比于“引用传递”,“值传递”方式的缺点是:
① 对象拷贝 + 拷贝/析构函数调用 导致的 效率低下
② 值传递 可能会导致 派生类传给基类对象时发生 类型截断

详细阐述:

对于一个自定义的类对象做为函数参数进行 值传递 时,除了会进行数据成员的内存复制以外,还会造成 函数调用的额外开销。

因为 值传递 依靠的是对象的拷贝构造函数 和 析构函数释放。

例如:

class Base {
     
private:
	string a;
	string b;
};

class Derived : public Base {
     
private:
	string c;
	string d;
};

void func(Derived d) {
     

}

当调用函数 func() 以“值传递”的方式传入参数 Derived d 时,会分别调用Derived类、Base类、string *4 共计 6个对象的拷贝构造函数;
而当函数func执行完毕退出作用域时,又将调用这 6个对象的析构函数。

另外,“值传递”的方式在传递有继承关系的对象时,可能会发生类型截断。

例如:

class Base {
     
};

class Derived : public Base {
     
};

void func(Base b) {
     	//函数形参为基类类型
}

int main() {
     
	Derived d;
	func(d);	//调用func函数时传入派生类对象,此时将发生类型截断,实际传入的只是d中的基类部分,派生类部分将丢失
}

而使用引用的方式传递有继承关系的对象则可避免类型截断。
这是因为,引用的底层实现其实是 指针,传递引用实际上传递的就是指针。

对于内置类型,使用值传递的方式即可,无须使用引用传递。因为内置类型一般较小且不涉及拷贝函数的调用,例如一个int型本身的大小与指针相当。

适用于 pass-by-value 的类型包括:
① 内置类型;② STL的迭代器;③ 函数。

对于自定义的类型,即使类型体积较小,也应尽量使用引用传递的方式调用,因为代码在后续的维护和优化中可能被修改而使其体积变大。要用发展的眼光看待代码。


条款21:必须返回对象时,别妄想返回其reference

==> 请记住:
(1)绝不要返回 pointer 或 reference 指向一个local stack 对象,或返回 reference 指向一个 heap-allocated 对象,或返回 pointer 或 reference 指向一个local static 对象而有可能同时需要多个这样的对象。

==> 解析:

虽然“引用传递”的效率要高于“值传递”,但并不意味着所有应用场景都要使用“引用传递”代替“值传递”。

例如,当需要返回函数中的 栈上的局部变量,或者需要返回指向一个堆上对象的指针/引用时,就必须使用“值传递”。

时刻记住 所谓reference只是一个名称,代表某个既有对象。 任何时候看到一个reference声明式,你都应该立刻问自己,它的另一个名称是什么?因为它一定是某物的另一个名称。


条款22:将成员变量声明为private:

==> 请记住:

(1)切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
(2)protected并不比public更具封装性。

==> 解析:

将成员变量声明为 private 的理由:

(从上向下 重要性递增)

① 接口一致性:

将所有的成员变量都声明为private,则public接口中将全都是函数,这样用户在调用类时就不需要考虑写法中是否要加上小括号;

② 控制访问类的成员变量的方法:

只能通过public接口提供的有限方式访问操作成员变量,可以控制外部访问类的成员的方式。
例如使某些成员变量只读、某些成员变量可读写、甚至某些成员变量只写(虽然这并没什么实际意义)。

③ 封装:

客户只管调用类的接口,不必关心接口后的实现方式是否发生了变化。

一个直观的例子:
假设你正在写一个自动测速程序,当汽车经过,其速度便被计算并填入一个收集器中:

class SpeedDataCollection {
     
public:
	void addValue(int speed);		//添加一个新采集的速度样本
	double averageSoFar() const;	//返回平均值
};

averageSoFar() 函数在项目开发初期可能只具有简单的计算所有采集到的速度数据平均值的功能,然而随着时间推进,averageSoFar() 的计算方式可能被优化到更复杂、更合理的算法,而此时类的用户是无需感知的,调用这一接口的用户代码无需做任何改变。

还是那句话,“以发展的眼光看待代码”,代码并不是一成不变的,随着时间的推移可能被优化、被修改、被维护。将接口与实现分离,降低了不同模块间的耦合度,用户代码可以在类的内部实现被修改时做到无感知。

public部分应该只存放类对外部的接口,public意味着不封装,而几乎可以说,不封装意味不可改变,特别是被多个用户广泛使用的classes而言。

类的public接口不应被频繁修改,它们就像是不同模块之间的interface一样。
不同的地方在于 interface 是两个对象间交互的接口(双方各有输入/输出);而 public接口的提供者–类,是提供功能的实现者,调用接口的一方是类的使用者。

“protected 相较于派生类”与“public相较于类外用户”来说同等重要,一旦某个protected接口函数被删除,所有使用它的派生类都将受到破坏。

所以,protected 并不比 public 更具封装性。


条款23:宁以non-member、non-friend替换member函数

==> 请记住:

(1)宁可拿 non-member、non-friend 函数替换 member函数,这样做可以增加封装性、包裹弹性(packaging flexibility)和 机能扩充性。


条款24:若所有参数皆需类型转换,请为此采用non-member函数

==> 请记住:

(1)如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。

==> 解析:

正常情况下,当实现一个与类类型相关的功能函数时,可选择将其实现成 成员函数、非成员函数、友元函数。

出于“面向对象精神”(保持封装性,一个操作类的函数应该放在类内接口),优先选择将其实现为 成员函数。

然而,有一些情况比较特殊,某些函数并不是适合作为成员函数。例如当这个函数的所有形参都需要支持隐式类型转换时,就应该将其实现为类外的非成员函数。理由如下:

成员函数(member function)中的隐式形参 this指针不能被隐式类型转换,只有明确出现在成员函数形参列表中的参数才可以被隐式转换。

所以,当成员函数的所有参数(包括隐式形参this在内)都需要支持类型转换时,应该将其改为定义成 非成员函数(non-member function)。

举例:
当实现一个类型之间的乘法(或加法、减法)运算符重载时,显然需要支持类既可以出现在 * 运算符左侧,也可以出现在 * 运算符右侧(乘法有交换律)。

如果将 operator* 放在类内:

class Base {
     
public:
	Base operator&(const Base& rhs) {
     
		return Base(this->val * rhs.val);
	}
private:
	int val;
};

当在类外使用时:

int main() {
     
	Base obj_1;
	int a = 10;
	
	Base obj_2 = obj_1 * a;	//正确
	//obj_1 * a = obj1.operator*(10);
	//将发生 int a 向 Base 的类型转换

	Base obj_3 = a * obj_1;	//错误
	//a * obj_1 = 10.operator*(obj_1);
}

如果将 operator* 放在类外:

class Base {
     
public:
	...
private:
	int val;
};

Base operator*(const Base& lhs, const Base& rhs) {
     
	return Base(lhs.val * rhs.val);
}

int main() {
     
	Base obj;
	int a;
	obj * a;	//正确
	a * ojb;	//正确,a * obj = operator*(a, obj);
}

条款25:考虑写出一个不抛出异常的swap函数

==> 请记住:

(1)当 std::swap 对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
(2)如果你提供一个 member swap,也该提供一个 non-member swap用来调用前者。对于 classes(而非templates),也请特化std::swap。
(3)调用swap时应针对std::swap 使用using 声明式,然后调用swap并且不带任何“命名空间资格修饰”。
(4)为“用户定义类型”进行 std templates 全特化是好的,但千万不要尝试在 std 内加入某些对std而言全新的东西。

你可能感兴趣的:(C/C++)