C++ Advanced - Note: class 类;宁以 pass-by-reference-to-const 替换 pass-by-value;错误返回 reference;private 私有;宁以 non-member、non-friend 替换 member 函数;类型转换;不抛出异常的 swap 函数;尽可能延后变量定义式的出现时间;minimize casting 尽量少做转型操作。
- Created on 2014-05
treat class design as type design.
注意:
- 新type的对戏那个如何被创建和销毁: 以及构造、析构函数,内存分配和释放函数。
- 对象的初始化和对象的赋值有什么区别。
- 新type对象如果被以值传递,意味着什么?
- 什么是新type的合法值?
- 你的新type需要配合某个继承图系吗?(inheritance graph)
- 你的新type需要什么样的类型转换?
- 什么样的操作符和函数对此新type是合理的?
- 什么样的标准函数应该驳回?(注意必须声明为private者)
- 谁该取用新type的成员?决定哪些是private/public/protected/friend 等。
- 什么是新type的未声明接口(undeclared interface)?
- 你的新type有多么一般化?考虑它成为一个类模版。class template
perfer pass-by-reference-to-const to pass-by-value.
缺省情况下C++以by value方式传递对象至函数, 函数参数就是实际实参的复本,由对象的copy构造函数提供。
pass-by-reference-to-const效率更高,减少了复本对象及其成员对象的构造和析构。
而且还可以避免slicing(对象切割)的问题。
当一个derived class对象以by-value方式传递并被视为base class对象, base class的copy构造函数会被调用,导致derived class对象的那些特化兴致被切割掉了, 只剩下一个base class对象。
一般而言,可以合理假设:内置对象和STL的迭代器和函数对象,可以pass-by-value!
don't try to return a reference when you must return an object.
任何函数如果返回一个reference或pointer指向某个local对象,都会一败涂地!
TestObj& retLocalObj2(){
TestObj a(2);
return a;
}
TestObj& retLocalObj3(){
TestObj *a = new TestObj(3);
return *a;
}
TestObj* retLocalObj4(){
TestObj *a = new TestObj(4);
return a;
}
- 以上第一个例中,local object建立在栈上,返回时local object已会被释放。
- 第二、三个例中,local object建立在堆上,返回的local object不会被释放,
但是,之后谁能对这些临时的对象释放,delete?例如:
w = retLocalObj2() * retLocalObj3() * retLocalObj4();
明显没有机会释放中间变量,导致内存泄漏。
正确做法: 必须返回新对象,就让那个函数直接返回一个新对象
inline const Rational operator*(const Rational& lhs, const Rational& rhs){
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
必须承受由此带来的构造、析构成本。
- 绝不要返回pointer和reference指向一个local stack对象,
- 或返回reference指向一个heap-allocated对象,
- 或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
declare data members private
切记将成员变量声明为private。这可赋予客户访问数据的一致性、 可细微划分放哪高温控制、允诺约束条件获得保证, 并提供class作者以充分的实现弹性和日后的修改空间。 protected并不比public更具有封装性, private可以使其成员变量,对其derived class更有封装性。
prefer non-member non-friend functions to member function
class WebBrowser{
public:
void clearCache();
void clearHistory();
void removeCookie();
// void clearEverything(); // 使用这个成员函数调用前三个函数不够好
...
}
void clearBrowser(){
wb.clearCache();
wb...
} // 这样更好,有更好的封装性、包裹弹性(packaging flexible)和机能扩充性。
为什么呢? 因为,在类内,越少的代码能够做同一件事,封装性越好 clearEverything()也做到了clearCache()...等的函数的工作。
在所有函数必须定义在类内的语言来说, 可以另外定义一个WebBrowser的工具类utility class, 在其中定义一个static member函数完成相关功能。
declare non-member functions when type conversions should apply to all parameters.
class Rational{
public:
...
const Rational operator*(const Rational* rhs) const;
}
result = oneHalf * 2; // 正确
result = 2 * oneHalf; // 错误!int 2 无法隐式转换为Rational类型
将operator*在类外定义即可:
const Rational operator*(const Rational* lhs, const Rational* rhs){...}
之前出错的语句也可以正常运行了!
其实可以将它定义为class Rational的friend函数,但是应该尽量避免,原因未详述。
如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数) 进行类型转换,那么这个函数必须是个non-member。
consider support for a non-throwing swap.
- 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常;
- 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。 对于 classes(而非templates),也请特化std::swap。
- 调用swap时应针对std::swap使用using声明式,然后调用swap, 并且不带有任何“命名空间资格修饰”。
- 为“用户定义类型”进行std templates全特化是最好的, 但千万不要尝试在std内加入某些对std而言全新的东西。
namespace std{
template<typename T> // std::swap的典型实现;
void swap(T& a, T& b){
T temp(a); // 只要T类型支持copying构造函数和copy assignment操作符即可
a = b;
b = temp;
}
}
但是有些用户定义类型,复制的动作并非总有必要。因为, 主要情况是,有些成员变量只是指针,指向一个对象,内含真正数据, (这种设计的常见表现形式是所谓的pimpl手法——pointer to implementation) 两个对象只需要交换这个指针值即可。 所以,可以针对这个用户定义类型,让std::swap进行特化:
namespace std{
template<> // 这是std::swap针对T是Widget的特化版本
void swap<Widget>(Widget& a, Widget& b){
swap(a.pImpl, b.pImpl);
}
} // 但无法通过编译,因为pImpl是private成员。
真正解决方法:
class Widget{
public:
void swap(Widget& other){
using std::swap; // 令std::swap在此函数内可用
swap(pImpl, other.pImpl); // 编译器根据实际情况,
// 调用T专属的版本,或者std中一般化(泛化)的版本
}
}
namespace std{
template<>
void swap<Widget>(Widget& a, Widget& b){
a.swap(b);
}
}
劝告:成员版swap绝不可抛出异常。
postpone variable definitions as long as possible.
- 当一个变量需要使用时,才去声明它。
- 为了提高效率,构造时就初始化好它!例:
std::string encryted; // 先使用default构造函数
encrypted = password; // 再用赋值操作符……
不如
std::string encryted(password); // 使用copy构造函数初始化了
循环时怎么办?
Widget w;
for(...){
w = xxx;
}
还是
for(...){
Widget w = xxx;
}
前者效率高一点,但是w作用域扩大,可理解性和易维护性变差!
只有两种情况才使用前者的做法:
- (1)知道赋值比“构造+析构”的成本低
- (2)你正在处理代码中效率高度敏感的部分(performance-sensitive)
minimize casting
(T)expr; // 两种旧式转型
T(expr);
const_cast<T>(expr); // 将对象的常量性去除(cast away the constness)
dynamic_cast<T>(expr); // 安全向下转型(safe downcasting)
// 决定某对象是否归属继承体系中的某个类型(之后细谈)
// 唯一无法由旧式语法执行的动作
static_cast<T>(expr); // 强迫隐式转换(implicit conversions)
// 将non-const转换为const,反向操作不能,只能用const_cast
// 或将int转成double,或相反
// 将void*转成type*
// 将ptr-to-base转为ptr-to-derived
reinterpret_cast<T>(expr); // 企图进行低级转型,实际动作和结果取决于编译器
// 所以它不可移植。将一个long或int转成指针都可以。
新式转型比旧式:
- (1)更加容易辨认,易读
- (2)转型动作的目标窄化,编译器容易判断出错误
- 尽量避免转型,在注重效率的代码中,避免dynamic_cast, 最好试着发展无须转型的替代设计。
- 如转型是必要的,试着将它隐藏于某个函数背后。 客户可以调用该函数,使其不用将转型过程置于其代码中。
- 宁可使用新式转型语法,不要使用旧式转型。清晰。
avoid returning "handles" to object internals.
避免返回handles(包括references、ptr、iterator迭代器)指向对象内部, 保证封装性,帮助const成员函数的行为像个const, 并将“虚吊号码牌”(dangling handles)的可能性降至最低。 (虚吊号码牌,即是野指针,对象已被销毁,但是指向这个地方的指针还在)
strive for exception-safe code.
exception-safe 异常安全 的两个条件: 当异常抛出时, (1)不泄露任何资源。 不会代码的出错中断,导致没有delete或者释放掉资源、互斥锁等。 (2)不允许数据败坏。 因为new失败,可能导致一个指针成为野指针。 内部的变量、状态,非原子性,不一致。
异常安全函数——提供以下三个保证之一:
- (1)基本承诺。 若异常被抛出,程序内的任何事物仍然保持在有效状态下。
- (2)强烈保证。 若一场抛出,程序状态不改变。(即是变化都是原子性的。) 要不是成功执行的状态,要不处于函数调用前的状态。
- (3)不抛掷(throw)保证。 绝不抛出异常。总是能够完成承诺的功能。
条款29给了我很大震动,这个条款很长,还是从原书重读较好。
因为没有想到,这个代码的严谨性超过了我以前的想象! 以下给出最好的那个代码版本:
struct PMImpl{
std::tr1::shared_ptr<Image> bgImage;
int imageChanges;
}; // 为了swap-and-copy而设计的(之前的条款有说)
class PrettyMenu{
...
private:
Mutex mutex; // 互斥量
std::tr1::shared_ptr<PMImpl> pImpl; // 为了swap-and-copy而设计的
};
void PrettyMenu::changeBackground(std::istream& imgSrc){
using std::swap;
Lock ml(&mutex);
std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
pNew->bgImge.reset(new Image(imgSrc));
++pNew->imageChanges;
swap(pImpl, pNew);
}
“强烈保证”往往能够以copy-and-swap实现出来, 但强烈保证并非对所有函数都可实现或具有现实意义。
understand the ins and outs of inlining
在class声明处,就定义函数过程的,都会隐喻为inline。
virtual函数不能够被inline。 千万别将构造和析构函数inline!
调试器,无法对inline函数设置断点。
将大多数inlining限制在小型、被频繁调用的函数身上。 这可使日后的调试过程和二进制升级(binaryupgradability)更容易, 也可使潜在的代码膨胀问题最小化,使程序速度提升的机会最大化。
不要只因为function template出现在头文件,就将它们声明为inline。