Effective C++ 18 ~ 25 设计与声明 包括让接口容易被正确使用,不易被误用 设计class犹如设计type 尽量用pass-by-reference-to-const(const引用

所谓软件设计,是令软件做出你希望做的事的步骤和做法

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

1. 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。

问题:

class Date
{
public:
	Date(int month, int day, int year);
	...
};

如果调用Date() 则可能引发以下问题:

  • 也许会以错误的次序传递参数
  • 可能传递一个无效的月份或天数

解决:

导入简单的外覆类型来区别天数、月份和年份,然后在Date构造函数中使用这些类型

class Day // Day class
{
public:
	explicit Day(int d):val(d){}
private:
	int val;
};

class Month // month class
{
public:
	explicit Month(int m):val(m){}
private:
	int val;
};

class Year // year class
{
public:
	explicit Year(int y):val(y){}
private:
	int val;
};

class Date
{
public:
	Date(const Month& m, const Day& d, const Year& y);
	...
}
Date d(30, 3, 1995); // error
Date d(Month(3), Day(30), year(1995)); // ok,类型正确

上述写法还可能传递一个无效的月份或天数

更好的写法

限制month的数值

class Month
{
public:	
	static Month Jan(){return Month(1);} // 以函数替换对象
	static Month Feb(){return Month(2);} // 表示某个特定月份
	...
	static Month Dec(){return Month(12);}
privateexplicit Month(int m); // 阻止生成新的月份
	... //这是月份专属数据
};

Date d(Month::Mar(), Day(30), Year(1995));

2. “促进正确使用”的办法包括接口的一致性,以及与内存类型的行为兼容。

3. “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。

任何接口如果要求客户必须记得做某些事情,就是有着“不正确使用”的倾向

问题:

Investment* createInvestment();

客户可能会没有删除指针,或删除同一个指针超过一次

解决:

强制客户使用智能指针,这可以组织一大群客户犯下资源泄露的错误

std::shared_ptr<Investment> createInvestment();

4. shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解除互斥锁等等。

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

设计class应该带着设计语言内置类型一样的谨慎来研讨class的设计

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

新type的对象应该如何被创建和销毁?

这会影响到你的class的构造函数和析构函数以及内存分配函数和释放函数的设计。

对象的初始化和对象的赋值该有什么样的差别?

这个决定你的构造函数和赋值操作符的行为,以及其间的差异,别混淆“初始化”和“赋值”

新type的对象如果被passed by value(以值传递),意味着什么?

copy构造函数用来定义一个type的pass-by-value 该如何实现

什么是新type的“合法值”?

成员函数(特别是构造函数、赋值操作符)必须进行错误检查工作。

你的新type需要配合某个继承图系吗?

如果你继承自某些既有的class,你就会受那些class的设计束缚,尤其是受到它们的函数是virtual和non-virtual的影响。

你的新的type需要什么样的转换?

如果只允许explicit构造函数存在,就得写出专门负责转换的函数。

什么样的操作符和函数对此新type而言是合理的?

这个问题的答案决定你将为你的class声明哪些函数,其中某些该是member函数,某些则否。

什么样的标准函数应该驳回?

那些正是你必须声明为private。

谁该取用新type的成员?

这个提问可以帮助你决定哪个成员为public,哪个是protected,哪个是private。它也帮助你决定哪一个class和function应该是friend,以及将它们嵌套于另一个之内是否合理。

什么是新type的“未声明接口”?

它对效率、异常安全性以及资源运用(例如多任务锁定和动态内存)提供何种保证?
需要为你的class实现代码加上相应的约束条件。

你的新type有多么一般化?

或许你就是定义一整个type家族,那就不应该定义新class,而是应该定义一个新的class template。

你真的需要一个新的type吗?

如果只是定义新的derived class以便为既有的class添加机能,可以单纯定义一个或多个non-member函数或者template,更能够达到目标。

条例20 : 尽量用pass-by-reference-to-const(const引用)替换pass-by-value(传值)

1. 尽量以pass-by-reference-to-const 替换 pass-by-value。前者通常比较高效,并可避免切割问题。

使用以下代码有什么缺点?

bool validateStudent(Student s); //以by value 方式接受Student

使用多次析构函数和构造函数。

使用const 引用代替

bool validateStudent(const Student * s );
优点 :
  1. 这种传递方式的效率高很多,没有任何构造函数和析构函数的调用,因为没有任何新对象被创建。
  2. 避免slicing(对象切割)问题
    slicing(对象切割)问题: 当一个derived class对象以by value 方式传递并被视为一个base class 对象,base class 的copy构造函数会被调用,而derived class 对象的不同实现被切割掉了,仅仅留下了一个base class对象。
//图形窗口系统
void printDisplay(Window w) // 不正确,参数可能被切割 
{ w.display(); }
class Window
{
public:
	virtual void display() const;// 显示窗口和其他内容
};

class WindowWithScrollBars: public Window
{
public:
	virtual void display() const;
};

当调用

WindowWithScrollBars wwsb;
printDisplay(wwsb);

参数wwsb会被看作是window对象,它作为WindowWithScrollBars的所有特化特征都会被切割,使用const 引用可解决这个问题
解决

void printDisplay(const Window& w) 
{
	w.display();
}

2. 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言pass-by-value往往比较适当。

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

1. 绝不要返回pointer或reference指向一个local stack对象,或返回一个heap-allocated(已分配堆)对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款4已经为“在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例。

一旦程序员领悟了传值的效率高,想要舍弃传值的做法,就会犯下致命错误

问题

class Rational{
pubilc: 
	Rational(
		int numerator = 0;
		int denominator = 1;
		.....
	);
	private:
	int n, d;
	friend
	const Rational
	operator * (const Ration & lhs,const Rational & rhs*);
}

对于 operator * (const Ration & lhs,const Rational & rhs*);
如果改而传递引用(reference),就不用代价,但这是不可行

分析

函数创建新对象有两个途径

1. 在stack空间创建

const Rational& operator*(const Rational& lhs, Rational& rhs)
{
	Rational result(lhs.n * rhs.n, lhs.d * rhs.d); // 在stack创建对象,糟糕的代码
	return result;
}

必须要执行构造析构函数,result 是个local对象,在函数退出之前就被销毁了,因此会指向未知位置,这是个未定义行为

2. 在heap空间创建

const Rational& operator*(const Rational& lhs, Rational& rhs)
{
	Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d); // 在heap创建对象,糟糕的代码
	return *result;
}
  • 还是必须要执行构造析构函数
  • 没有对new出来的对象实施delete,内存泄漏
Rational w, x, y, z;
w = x * y * z; // 与operator*(operator*(x, y), z) 等价

没有合理的办法让他们取得引用后面隐藏的指针,导致资源泄露

使用static 对象

const Rational& operator*(const Rational& lhs, Rational& rhs)
{
	// 警告,又是一堆烂代码
	static Rational result;
	result = Rational(lhs.n * rhs.n, lhs.d * rhs.d);
	return result;
}

  1. 多线程安全性差
bool operator==(const Rational& lhs, Rational& rhs);
Rational a, b, c, d;
if((a * b) == (c * d)){}

其中(a * b) == (c * d)始终为Ture
这个可以等价为

if(operator==(operator*(a, b), operator*(c, d))){}

用了两次operator * 而且是static对象,比较的时候是同一个,因为他一直更新

真正的写法

返回对象的值而非引用

inline const Rational operator* (const Rational& lhs, Rational& rhs)
{
	return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

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

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

赋予客户访问数据的一致性

对于每一个变量获取都是一个函数,不用取记住是否使用小括号

可细微划分访问控制

可以精确控制哪些变量是“不准访问”、“只读访问”、“读写访问”以及“惟写访问”

2. protected并不比public更具有封装性,只有private提供封装。

在改变某些变量时,class客户一点也不会知道class的内部实现已经发生了变化,而使用public,当变量发生变化,客户会马上知道

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

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

比较以下函数
member函数

class WebBrowser
{
public:
	...
	void clearEverything(); // 调用clearCache、clearHistory和removeCookies
	...
};

和non-member函数

class clearBrowser(WebBrowser& wb)
{
	wb.clearCache();
	wb.clearHistory();
	wb.removeCookies();
}

哪个比较好?
使用non-member 函数,A因为提供non-member函数可允许对WebBrowser相关机能有较大的包裹弹性,而那最终导致较低的编译相依度,增加WebBrowser可延伸性,B且封装性好

封装

如果某些东西被封装,它就不再可见。愈少人看见它,我们就愈大的弹性去改变它,我们改变那些东西的能力就愈大。

对于封装机能上,两着相同,但non-member 具有较大封装性,因为non-member函数并不增加“能够访问class内private成分”的函数数量。

在C++中,比较自然的做法是让clearBrowser称为一个non-member函数并且位于WebBrowser所在的同一个namespace(命名空间)内

namespace WebBrowserStuff
{
	class WebBrowser{ ... };
	void clearBrowser(WebBrowser& wb);
	...
}

2. 将所有便利函数放在多个头文件内但隶属同一个命名空间(namespace),这是C++标准程序库的组织方式。

//头文件"webbrowser.h"-----这个头文件针对WebBrowser自身
//及WebBrowser核心机能
namespace WebBrowserStuff
{
	class WebBrowser{ ... };
	... // 核心机能,例如几乎所有客户都需要的non-member函数
}

//头文件"webbrowserbookmarks.h"
namespace WebBrowserStuff
{
	... // 与书签相关的便利函数 
}

//头文件"webbrowsercookies.h"
namespace WebBrowserStuff
{
	... // 与cookies相关的便利函数 
}

C++标准程序库的组织方式
  • 用户需要哪些机能就包含哪些头文件
  • 用户扩展时方便

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

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

class 支持隐式类型是个不好的注意,常见的例外是建立数值类型

例子:

class Rational
{
public:
	// 构造函数刻意不为explicit,允许int-to-Rational隐式转换
	Rational(int numerator = 0, int denominator = 1);
	const Rational operator*(const Rational& rhs) const; // operator* 方法
	...
};

问题:

上述代码在执行以下代码会发现不能处理混合式运算

Rational oneHalf(1, 2);
Rational result = oneHalf * 2; // 很好
result = 2 * oneHalf; // 错误
//用函数形式等价于
result = oneHalg.operator*(2);
result = 2.operator*(oneHalf);
原因:

可以发现2不是Raional类,他没有相应的成员函数,因此发生错误

追问: 为什么第一种写法(Rational result = oneHalf * 2 )可行?

因为这里发生了隐式类型转换,2本身是int,调用了Rational 的构造函数,所以上述写法只能将第二个参数隐式转化。

解决:

让operator 成为一个non-member函数*

class Rational // 不包括operator*
{...};

const Rational operator*(const Rational& lhs, const Rational& rhs); // non-member函数

这样第一个参数和第二个参数都支持隐式类型转换

追问 : operator* 是否应该为friend ?

答案是否定的,因为operator* 完全藉由Rational的public接口完成任务。

又一个发现 :

member函数的反面是non-member函数,而不是friend函数。

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

1. 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。

2. 如果你提供一个member swap,也该提供一个non-member swap用来调用前者,对于class(而非template),也请特化std::swap。

3. 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。(利用名称查找法则 为“T”型对象调用最佳的swap)

4. 为“用户定义类型”进行std template全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。

swap是个很有用的函数,它是STL的一部分,后来成为异常安全性编程的脊柱,以及用来处理自我赋值可能性的一个常见机制。

Swap 函数

缺省实现:两对象的值彼此赋予对方(不好)
namespace std
{
	template<typename T>
	void swap(T& a, T& b) // 置换a和b的值
	{
		T temp(a);
		a = b;
		b = temp;
	}
}
缺点 :

效率低(见条款19),需要复制三个对象

更好的实现方法 ,使用指针交换

使用‘pimpl手法’ (指向实现的指针)来实现交换

class WidgetImpl
{
public:
	...
private:
	int a, b, c;           //可能有许多数据,
	std::vector<double> v; //意味复制时间很长。
	...
};

class Widget
{
public:
	Widget(const Widget& rhs);
	Widget& operator=(const Widget& ths) //复制Widget时,令它复制其WidgetImpl对象
	{
		...
		*pImpl = *(rhs.pImpl);
		... 
	}
	...
private:
	WidgetImpl* pImpl; 
};
好处 :

效率高(见条款19)

问题:
  • 原话 :template<>表示他是std:: swap的全特化版本,而函数名称之后的Widget表示着一特化版本针对”T 是 Widget ”而设计
  • 我的理解 :template 模板中没有Widget ,我们不能自己加一个Widget 类到模板里面
解决 (std::swap针对Widget特化):
class Widget // 与前同,唯一的区别是增加swap函数
{
public:
	...
	void swap(Widget& other)
	{
		using std::swap; // 这个声明是必要的
		swap(pImpl, other.pImpl); // 若要置换Widget就是置换其pImpl指针	
	}
	...
};

namespace std
{
	template<> // 这是std::swap针对“T是Widget的特化版本”
	void swap<Widget>(Widget& a, Widget& b)
	{
		a.swap(b); //若要置换Widget,调用其swap成员函数。
	}
}

这种做法不止能通过编译,还与STL容器有一致性,因为所有STL容器也都有public swap成员函数和std : : swap特化版本

进一步的问题 : 当你想偏特化一个 function template 时
  1. 错误代码
namespace std{
	template <typename T>
	void swap< Widget<T> >(Widget <T> & a, Widget <T> &b){
		a.swap(b);
	}
}

原因 : C++ 只允许对class template 偏特化,在function templates 是不能编译的
2. 看起来正确的代码?添加一个重载版本

namespace std{
	template <typename T>
	void swap (Widget <T> & a, Widget <T> &b){
		a.swap(b);
	}
}

问题: 一般而言这样写没错,但std 是特殊的,管理规则特殊,用户可以全特化 std内的模板
不可以添加新的模板std 的内容完全由 C ++ 委员会决定

进一步解决 :

声明 non -member swap

namespace WidgetStuff{
	template <typename T>
	class Widget{};
	void swap (Widget <T> & a, Widget <T> &b){
		a.swap(b);
	}
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值