首先,必须考虑客户可能做出什么样的错误。
例如:
class Date{ public: Date(int month, int day, int year); }; Date d(30, 3, 1995);//不合理,应该(3, 30, 1995) Date d(2, 30, 1995);//不合理,2月30号无效日期
考虑导入简单的外覆类型,具备了更高的类型安全性:
struct Day{ explicit Day(int d) :val(d) { } int val; }; struct Month{ explicit Month(int m) :val(m) { } int val; }; struct Year{ explicit Year(int y) :val(y) { } int val; }; class Date{ public: Date(const Month& m, const Day& d, const Year& y); //... }; Date d(30, 3, 1995);//错误!不正确类型 Date d(Day(30), Month(3), Year(1995));//错误!不正确的类型 Date d(Month(3), Day(30), Year(1995));//OK,类型正确
请记住:
//std::shared_ptr的删除器使用示例 #include <iostream> #include <memory> struct Foo { int i; }; void foo_deleter(Foo * p) { std::cout << "foo_deleter called!\n"; delete p; } int main() { std::shared_ptr<int> aptr; { // 创建拥有一个 Foo 和删除器的 shared_ptr auto foo_p = new Foo; std::shared_ptr<Foo> r(foo_p, foo_deleter); aptr = std::shared_ptr<int>(r, &r->i); // 别名使用构造函数 // aptr 现在指向 int ,但管理整个 Foo } // r 被销毁(不调用删除器) // 获得指向删除器的指针: if(auto del_p = std::get_deleter<void(*)(Foo*)>(aptr)) { std::cout << "shared_ptr<int> owns a deleter\n"; if(*del_p == foo_deleter) std::cout << "...and it equals &foo_deleter\n"; } else std::cout << "The deleter of shared_ptr<int> is null!\n"; } // 于此调用删除器
设计高效的class,需要面对的问题:
c++编译器的底层,reference往往以指针实现出来,因此pass-by-value通常意味真正传递的指针。因此如果你有个对象属于内置类型,pass-by-value往往比pass-by-reference的效率更高。这一点也适用于STL的迭代器和函数对象,因此习惯上它们都被设计为pass-by-value。
内置类型都相当小,因此有人认为,所有小型types都是pass-by-values的合格候选人,甚至它们都是用户自定义的class亦然,这是个不可靠的推论。对象小并不意味着其copy构造函数并不昂贵。许多对象 - 包括大多数STL容器 - 内含的东西只比一个指针多一些,但复制这种对象却需承担“复制那些指针所指的每一样东西”。那将非常昂贵。另外还有某些编译器对待“内置类型”和“用户自定义类型”的态度截然不同。纵使两者拥有相同的底层描述。如编译器拒绝把只由一个double组成的对象放进缓存器内,却很乐意在一个正规基础上对光秃秃的doubles那么做。另外作为一个用户自定义类型,其大小可能会有所变化。
请记住:
在stack空间创建新对象,
const Rational& operator*(const Rational& lhs, const Rational& rhs) { Rational result(lhs.n * rhs.n, lhs.d * rhs.d); return result; }
这个函数返回一个reference指向result,但result是个local对象,而local对象在函数退出前被销毁了。
在heap空间创建对象,
const Rational& operator*(const Rational& lhs, const Rational& rhs) { Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d); return *result; } //像下面这样就会有资源泄露问题 //例1. Rationl w,x,y,z; w = x * y * z; //与operator*(operator*(x, y), z)相同
除了依旧必须付出一个“构造函数调用”代价,还有一个新的问题,谁该对new出来的对象进行delete?
如代码块中例1,同一个语句内调用了两次operator*,因而两次使用new,也就需要两次delete。但却没有合理的办法让operator *使用者进行那些delete调用,因为没有合理的办法让他们去的operator *返回的reference背后隐藏的那个指针。
为了避免任何构造函数被调用,有可能想到这样的实现代码,
const Rational& operator* (const Rational& lhs, const Rational& rhs) { static Rational result; result = /*...*/; return result; } //首先,显而易见这样不是多线程安全的 //此外,看如下这种情况, bool operator==(const Rational& lhs, const Rational& rhs); Rational a,b,c,d; if((a * b) == (c * d)) { //... } else { //... } //这样表达式(a * b) == (c * d)总是被核算为true //其等价函数形式(operator==(operator*(a, b), operator*(c, d))), //这样子两个operator*返回的reference指向的是同一个static Rational对象
请记住:
为什么成员变量不该是public?
请记住:
切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
protected并不比public更具封装性。
假设我们有一个protected成员变量,而我们最终取消了它,所有使用它的子类都会被破坏,那往往也是个不可知的大量。
//一个网页浏览器类 class WebBrowser{ public: void clearCache();//清除下载元素高速缓存区 void clearHistory();//清除访问过的URLs历史记录 void removeCookies();//移除系统中的所有cookies };
如果想一整个执行所有这些动作,可以有两种方式,
//1.WebBrower也提供这样一个函数 class WebBrower{ public: void clearEverything() { clearCache(); clearHistory(); removeCookies(); } }; //2.提供一个普通的no-member函数 void clearBrower(WebBrower& wb) { wb.clearCache(); wb.clearHistory(); wb.removeCookies(); }
哪一个比较好?
面向对象守则要求数据应该尽可能被封装,然而与直观相反的,member函数clearEverything带来的封装性比non-member函数clearBrowser低。此外,提供non-member函数可允许对WebBrowser相关机能有较大的包裹弹性,能有更低的编译相侬度,增加WebBrowser的可延伸性。
愈少代码可以看到数据,愈多的数据可被封装,而我们也就愈能自由地改变对象数据,例如改变成员变量的数量、类型等等。成员变量应该是private,如果不是,就有无限量的函数可以访问它们,也就毫无封装性。能够访问private成员变量的函数只有class的member函数和friend函数而已。如果在一个member函数和一个non-member-non-friend函数做抉择,且两者提供相同功能,那么,使用non-member-non-friend函数封装性更好,因为它并不增加能够访问class内之private成分的函数数量。
让clearBrowser成为一个non-member函数并且位于WebBrowser所在的同一个namespace内,比起在class内,能降低编译依存性。
swap是个有趣的函数。原本它只是STL的一部分,而后成为异常安全性编程的脊柱,以及用来处理自我赋值可能性的一个常见机制。
首先,如果swap缺省实现码对你的class或class template提供可接受的效率,你不需要额外做任何事。任何尝试置换那种对象的人会取得缺省版本,而那将有良好的运作。
其次,如果swap缺省实现版的效率不足(大概率是class或template使用了pimpl手法),
提供一个public swap成员函数,让它高效的置换你的类型的两个对象值。
class Widget{ public: void swap(Widget& other) { using std::swap; swap(pImpl, other.pImpl); } private: WidgetImpl* pImpl; }; namespace std { template<> void swap(Widget)(Widget& a, Widget& b)//修订后的std::swap特化版本 { a.swap(b); } }
在你的class或template所在的命名空间内提供一个non-member swap函数,并令它调用上述swap成员函数。
/*****************************************************/ namespace std{ template<typename T> void swap<Widget<T> >(Widget<T>& a, Widget<T>& b)//错误,不合法 { a.swap(b); } }; //这里企图偏特化一个function template(std::swap),但C++只允许对class templates偏特化, //在function templates身上偏特化是行不通的。 /*****************************************************/ //当你打算偏特化一个function template时,惯常做法是简单的为它添加一个重载版本。 //如下: namespace std{ template<typename T> void swap(Widget<T>& a, Widget<T>& b) { a.swap(b); } }; //为了让其他人调用swap时能够取得我们提供的高效的template特定版本,我们还是声明一个non-member swap让它调用member swap,但不再将那个non-member swap声明为std::swap的特化版本或重载版本。 namespace Widdget{ template<typename T> class Widget { /*..同前,内含swap成员函数..*/ }; template<typename T> //non-membee swap函数 void swap(Widget<T>& a, Widget<T>& b) //这里不属于std命名空间 { a.swap(b); } };
如果你正在编写一个class(非class template),为你的class特化std::swap。并令它调用你的swap成员函数。
//如果想让你的“class专属版”swap在尽可能多的语境下调用,还是需同时在该class所在命名空间内写一个non-member版本以及一个std::swap特化版本 //希望调用T专属版本,并在该版本不存在的情况下调用std内的一般化版本, template<typename T> void doSomething(T& obj1, T& obj2) { using std::swap;//令std::swap在此函数内可用 //... swap(obj1, obj2);//为T类型对象调用最佳swap版本 } //一旦编译器看到对swap的调用,它们会查找适当的swap并调用之 //C++的查找法则确保将找到global作用域或T所在之命名空间内的任何T专属的swap
成员版的swap绝不抛出异常。高效率的swap几乎总是基于对内置类型(如pimpl手法的底层指针),而内置类型上的操作绝不会抛出异常。
请记住:
只要定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流到达这个变量定义式时,你便得承受构造成本;当这个变量离开其作用域时,你便得承受析构成本。
“通过默认构造函数构造对象然后对它赋值”比“直接在构造时指定初值”效率差。
对于循环呢?
//方法A:定义于循环外 Widget w; for(int i = 0; i < n; ++i) { w = 取决于i的某个值; ... } //方法B:定义于循环内 for(int i = 0; i < n; ++i) { Widget w(取决于i的某个值); ... }
方法A:1个构造 + 1个析构 + n个赋值操作
方法B:n个构造 + n个析构
如果classes的一个赋值成本低于一组构造+析构成本,做法A大体而言比较高效,尤其当n值很大的时候。否则做法B或许比较号。此外做法A造成名称w的作用域比做法B更大,有时会对程序的可理解性和易维护性造成冲突。
因此除非(1)知道赋值成本比“构造+析构”成本低,(2)你正在处理代码钟效率高度敏感的部分,否则你应该使用做法B。
C++提供四种新式类型转换:
需记住:
class Point{ public: Point(int x, int y); void setX(int newVal); void setY(int newVal); }; struct RectData{ Point ulhc;//左上角点 Point lrhc;//右下角点 }; class Rectangle{ public: //可通过编译,但是是错误的,这两个成员函数 Point& upperLeft() const { return pData->ulhc; } Point& lowerRight() const { return pData->lrhc; } private: std::shared_ptr<RecData> pData; }; //例: Point coord1(0,0); Point coord2(100, 100); const Rectangle rec(coord1, coord2); //upperLeft的调用者能够使用被返回reference来更改成员,但rec其实应该是不可变的 rec.upperLeft().setX(50);
这个示例带来的两个教训是,第一,成员变量的封装性最多只等于“返回其reference”的函数的访问级别。第二,如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。
class Rectangle{ public: const Point& upperLeft() const { return pData->ulhc; } const Point& lowerRight() const { return pData->lrhc; } };
像上面这样声明的话,客户可以读取矩形的Points,但不能涂写它们。但即使如此,upperLeft和lowerRight还是返回了“代表对象内部”的handles,有可能导致dangling handles(空悬号码牌)问题。
class GUIBbject { //... }; const Rectangle boundingBox(const GUIObject& obj); //客户又可能这么使用这个函数 GUIObject* pgo; const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft()); //对boundingBox的调用获得一个新的、暂时的Rectangle对象。这个对象没有名称,我们权且称它为temp。随后upperLeft作用于temp身上,返回一个reference指向temp的一个内部成分,更具体地说是指向一个用以标示temp的Points.于是pUpperLeft指向那个Point对象。到目前为止还没有什么问题,但是在这条语句结束后,temp将被销毁,而那间接导致temp内的Points析构。最终导致pUpperLeft指向一个不再存在的对象;也就是说一旦产出pUpperLeft的那个语句结束,pUpperLeft也就变成空悬,虚吊。
异常安全函数提供以下三个保证之一:
请记住:
inline函数背后的整体观念是,将“对此函数的每一个调用”都以函数本体替换之。这样做可能增加你的目标码大小,在一台内存有限的机器上,过度热衷inlining会造成程序体积太大,导致额外的换页行为,降低指令高速缓存装置的击中率,以及伴随这些而来的效率损失。如果inline函数的本地很小,函数inlining可能产生较小的目标码和较高的指令高速缓存装置击中率。
inline是个申请,编译器可加以忽略。大部分编译器拒绝将太过复杂的函数inlining;而所有对virtual函数的调用也都会使inlining落空,因为virtual意味“等待,直到运行期才确定调用哪个函数”,而inline意味“执行前,先将调用动作替换为被调用函数的本体”。
有时候虽然编译器有意愿inlining某个函数,还是有可能为该函数生成一个函数本体。例如,如果程序要取某个inline函数的地址,编译器通常必须为此函数生成一个outlined函数本体,因为编译器不能提出一个指针指向并不存在的函数。与此一块提到一点,编译器通常不对“通过函数指针而进行的调用”实施inlining,这意味着对inline函数的调用有可能被inlined,也可能不被inlined,取决于该调用的实施方式。
构造函数和析构函数往往是inlining的糟糕候选人。
如果func是程序库内的一个inline函数,将"func函数本体"编进程序之后,一旦程序设计者决定改变func,所有用到func的程序都必须重新编译。然而如果func是non-inline函数,一旦它有任何修改,程序只需要重新链接就好,远比重新编译的负担少很多。
从纯粹实用观点出发,inline函数难以进行调试,大部分调试器面对inline函数都束手无策。
需记住,