条款十八:让接口容易被使用,不易被误用
1.我们应该预防“接口被误用”。 假设有这么一个构造函数
#include <iostream>
using namespace std;
class Data
{
public:
Data(int month, int day, int year) {}
};
int main()
{
Data d1(30, 3, 1995);
Data d2(1995, 30, 3);
return 0;
}
为了防止他们被误用,所以我们最好给年月日都分别导入一个外覆类型。
以及,限制月份只有12个月,可以预先定义所有有效月份。(不用枚举是因为枚举可以转int)
#include <iostream>
using namespace std;
struct day
{
day(int x) :val(x) {}
int val;
};
struct month
{
static month Jan() { return month(1); }
static month Feb() { return month(2); }
int val() { return _val; }
private:
int _val;
month(int x) :_val(x) {}
};
struct year
{
year(int x) :val(x) {}
int val;
};
class Data
{
public:
Data(month month, day day, year year) {}
};
int main()
{
Data d1(month::Feb(), 3, 1995);
Data d2(month::Jan(), 30, 3);
return 0;
}
2.避免无端与内置类型不兼容,提供行为一致性的接口 stl容器的接口就十分的一致性,比如每个容器都有size成员函数,表示容器内有多少对象。 而像java或者.net则有Length和Count使得开发人员混淆。
3.不要让接口要求客户必须记得做某事。 因为客户可能会忘记做那件事,导致不可预料的结果 如一个函数动态分配对象并返回对象的指针给用户,如: A* createA() 意味着A必须被用户delete,而且很有可能客户删除A超过一次。这就存在两个错误的机会了!
所以我们应该用智能指针去管理它,最好直接返回智能指针给客户,这样直接彻底消除忘记删除对象的可能性。而不是让客户去管理它,消除客户的资产管理责任。
4.”corss-DLL problem”问题 即,对象在动态链接库DLL中被new创建,却在另一个DLL销毁。会导致运行期错误。 我们可以定制shared_ptr的删除器,他会追踪记录,当对象引用计数为0的时候调用那个DLL的delete,而不是另一个DLL的delete
请记住: 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。 ”促进正确使用“的办法包括接口的一致性,以及内置类型的行为兼容。 ”阻止误用”的办法包括建立新类型,限制类型上的操作,束缚对象值,以及消除客户的资产管理责任。 trl:shared_ptr支持定制型删除器,这可防范DLL问题,可被用来自动接触互斥锁(条款14),等。
条款十九:设计class犹如设计type
当你定义一个新的class,就犹如定义了一个新的type。这是一个艰巨的任务,那么如何设计呢?
(1) 新type的对象应该如何被创建和销毁? 这会影响到你的class的构造函数和析构函数以及内存分配函数和释放函数(operator new ,operator new[],operator delete和operator delete[]–见第8章)的设计,当然前提是如果你打算撰写它们。
(2)对象的初始化和对象的赋值该有什么样的差别? 这个答案决定你的构造函数和赋值(assignment)操作符的行为,以及其间的差异。很重要的是别混淆了“初始化”和“赋值”,因为它们对应于不同的函数调用。
(3)新type的对象如果被passed by value(以值传递),意味着什么? 记住,copy构造函数用来定义一个type的pass-by-value该如何实现。
(4)什么是新type的“合法值”? 对class的成员变量而言,通常只有某些数值集是有效的。那些数值集决定你的class必须维护的约束条件(invariants),也就决定了你的成员函数(特别是构造函数、赋值操作符合所谓“setter”函数)必须进行的错误检查工作。它也影响函数抛出的异常、以及(极少被使用的)函数异常明细列(exception specifications)。
(5)你的新type需要配合某个继承图系(inheritance graph)吗? 如果你继承自某些既有的classes,你就受到那些classes的设计的束缚,特别是收到“它们的函数是virtual 或non-virtual”的影响(见条款34和条款36)。如果你允许其他classes继承你的class,那会影响你所声明的函数-尤其是析构函数-是否为virtual(见条款7)。
(6)你的新type需要什么样的转换? 你的type生存与其他types之间,因而彼此该有转换行为吗?如果你希望允许类型T1之物被隐式转换为类型T2之物,就必须在class T1内写一个类型转换函数(operator T2)或在class T2内写一个non-explicit-one-argument(可被单一实参调用)的构造函数。如果你只允许explicit构造函数存在,就得写出专门负责执行转换的函数,且不得为类型转换操作符(type conversion operators)或non-explicit-one-argument构造函数。(条款15有隐式和显示转换函数的范例。)
(7)什么样的操作符和函数对此新type而言是合理的? 这个问题的答案决定将为你的class声明哪些函数。其中某些该是member函数,某些则否(见条款23,24,46)。
(8)什么样的标准函数应该驳回? 那些正是你必须声明为private者(见条款6)。
(9)谁该取用新type的成员? 这个提问可以帮助你决定哪个成员为public,哪个成员为protected,哪个为private.它也帮助你决定哪一个classes 和/或 functions应该是friends,以及将它们嵌套于另一个之内是否合理。
(10)什么是新type的“未声明接口”(undeclared interface)? 它对效率、异常安全性(见条款29)以及资源运用(例如多任务锁定和动态内存)提供何种保证? 你在这些方面提供的保证将为你的class实现代码加上相应的约束条件。
(11)你的新type有多么一般化? 或许你其实并非定义一个新type,而是定义一整个types家族。果真如此你就不应该定义一个新class,而是应该定义一个新的class template.
(12)你真的需要一个新type吗? 如果只是定义新的derived class以便为既有的class添加机能,那么说不定单纯定义一或多个non-member函数或templates,更能够达到目标。
这些问题不容易回答,所以定义出高效的classes是一种挑战。然后如果能够设计至少像C++内置类型一样好的用户自定义(user-defined)classes,一切函数便都值得。
请记住: Class的设计就是type的设计。在定义一个新type之前,请确定你已经考虑过本条款覆盖的所有主题。
条款二十:宁以pass-by-refernce-to-const替换pass-by-value
1.宁以pass-by-refernce-to-const替换pass-by-value 因为传值会产生昂贵的拷贝构造操作和析构操作。而且如果对象内有其他对象,或者继承自其他对象,会造成更大的开销! 但如果是pass by refernce to const就能回避所有构造和析构操作,这种传递方式效率特别高,因为没有任何对象的创建,而且可以避免切割问题 切割问题:当一个继承对象by value方式传递并视为一个base class对象,那么dervied对象的特化性质全被切割掉了,只留下base对象。因为正是base class对象构造了它。
2.此规则不适用于内置类型 references实现原理是指针,所以你的对象属于内置类型(int),那么pass by value会比pass by reference效率高。 此规则也同样适用于stl,因为习惯上它们都被设计为passed by value。
请记住: 尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高校。并可解决切个问题。 以上规则并不适用于内置类型,以及stl的迭代器和函数对象。对它们用pass-by-value比较妥当。
条款二十一:必须返回对象时,别妄想返回其reference
别无脑的返回reference, 看以下代码:
#include <iostream>
using namespace std;
class A
{
public:
A(int x) :value(x) {}
int value;
const A& operator*(const A& rhs)
{
A res(value * rhs.value);
return res;
}
};
int main()
{
A a(2);
A b(3);
A c = a * b;
return 0;
}
此时operator*返回的是local对象,那么在函数退出前就被销毁了。
那如果改用A*呢?
friend A& operator*(const A& lhs,const A& rhs)
{
int _x = (lhs.value) * (rhs.value);
A* res=new A(_x);
return *res;
}
那这样就会面临没有delete的情况, 尤其是当你w=xyz的时候,你调用了两次operator*意味着new了两次,你不得不delete两次,但是你无法取得引用背后隐藏的指针,最后造成内存泄漏。
正确写法应该是:
friend A operator*(const A& lhs,const A& rhs)
{
int _x = (lhs.value) * (rhs.value);
A res(_x);
return res;
}
但这样会承受构造和析构成本。
总结:当你必须在”返回一个refernce”和”返回一个object“之间抉择时,你的工作就是挑出行为正确的那个。
请记住:绝不要返回pointer或reference指向一个local stack对象,或返回refernce指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款四已经在为”在单线程环境中合理返回refernce”指向同一个local static对象”提供一份设计实例。
条款二十二:将成员变量声明为private
类似C#的属性一样,C++可以通过某些手段达到和C#属性一样的效果,只读或者只写,或者可读可写。 将成员变量设置为private为的是封装性,而且这样外界只能靠成员函数影响它们。 protected其实很打咩,因为当我们取消了某个protected成员变量,所有的derived classes都会被破坏,这和public其实如出一辙,所以不要觉得protected的封装性高过public。 从封装的角度看,访问权限只有private和其他。
请记住:切记将成员变量声明为private,这可赋予客户访问数据的一致性,可细微划分访问控制,允诺约束条件获得保证,并提供class作者以充分的实现弹性。
条款二十三:宁以non-member,non-friend替换menber函数
这一篇有点绕,我也没怎么看懂,大概就是尽可能使用非成员函数调用成员来增加封装性,包裹弹性和机能扩充性。
#include <iostream>
using namespace std;
namespace Aclass
{
class A
{
public:
void clear1() {}
void clear2() {}
void remove() {}
void clearAll() { clear1(); clear2(); remove(); }
};
void ClearA(A& a) { a.clear1(); a.clear2(); a.clearAll(); }
}
int main()
{
Aclass::A a;
a.clearAll();
Aclass::ClearA(a);
return 0;
}
请记住:宁可拿non-member、non-friend函数替换member函数,这样做可以增加封装性、包裹弹性和机能扩充性。
条款二十四:若所有参数皆需类型转换,请为此采用non-member函数。
看下面这个代码:
#include <iostream>
using namespace std;
class A
{
public:
A(int x) :value(x) {}
int value;
const A operator*(const A& rhs)const
{
return A(value * rhs.value);
}
};
int main()
{
A a(10);
A b = a * 10;
A c = 10 * a;
return 0;
}
为什么A b = a * 10;可以 而A c = 10 * a;不行 因为a*10的10做了隐式类型转换 实际上是A b1 = a * (const A(10));
但这可不好,乘法不满足交换律?? 所以最佳改进方案是用非成员函数:
#include <iostream>
using namespace std;
class A
{
public:
A(int x) :value(x) {}
int value;
};
const A operator*(const A& lhs, const A& rhs)
{
return A(lhs.value * rhs.value);
}
int main()
{
A a(10);
A b = a * 10;
A c = 10 * a;
return 0;
}
至于为什么不用友元函数?不知道,这是一个值得争议的话题,书上是认为不该用的。
请记住: 如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member
条款二十五:考虑写出一个不抛异常的swap函数
本人理解浅薄,未看懂书上内容,所以找了一篇高质量文章代替
条款25: 考虑写出一个不抛异常的swap函数
|