只要定义了一个变量而其类型带有一个构造函数或析构函数,那么
即使这个变量最终并未被使用,仍然需要耗费这些成本,所以应该尽可能避免这种情形,即延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。
如果变量只在循环内使用,那么把它定义于循环外并在每次循环迭代是赋值给它比较好,还是该把它定义在循环内?
// 方法A:定义于循环外
Widget w;
for(inti i = 0; i < n; ++i) {
w = 取决于i的某个值;
// ...
}
// 方法B:定义于循环内
for(int i = 0; i < n; ++i) {
Widget w(取决于i的某个值);
// ...
}
以上的两种做法的成本如下:
1
个构造函数 + 1
个析构函数 + n
个赋值操作n
个构造函数 + n
个析构函数从效率上看,如果类的一个赋值成本低于一组构造和析构成本,做法A
大体而言比较高效,尤其是当n
的数值很大时。否则做法B
更好。
从可理解性和维护性上看,做法A
造成w
的作用域比做法B更大,可维护性和可理解性相对较差。
显然,转型(casts
)会破坏类型系统(type system
),但是有时也不得不这样做。
转型分类包括:
旧式转型(old-style casts
):
(T) expression
T(expression)
新式转型(C++-style casts
):
const_cast<T>(expression);
cast away the constness
)。dynamic_cast<T>(expression);
reinterpret_cast<T>(expression);
int*
转换为char*
,比较危险的转换!static_cast<T>(expression);
non-const对象
转换为const对象
,int
转为double
等等。唯一例外是无法将const
转为non-const
,这个只要const_cast
才能做到。应该尽可能使用新式转型,主要有两点原因:
grep
等),因而得以简化"找出类型系统在哪个地方呗破坏"的过程。尽管如此,还是应该尽量少做转型,原因如下:
转型不只是告诉编译器把某种类型视为另一种类型这么简单,任何一个转型动作往往令编译器编译出运行期间执行的代码。
int
转型为double
几乎肯定会产生一些代码,因为在大部分体系结构中,int
的底层表述不同于double
的底层表述。
int x, y;
// …
double d = static_cast
会有个偏移量在运行期被实施于Derived*
指针上,用以取得正确的Base*
地址。
class Base {};
class Derived : public Base {};
Derived d;
Base* pb = &d;
很容易写出似是而非的代码。
class Window {
public:
virtual void onResize();
// …
};
// 错误的用法,因为转型并非在当前对象身上调用Window::onResize(),而是当前对象的base class成分的副本上调用Window::onResize()。
class SpecialWindow: public Window {
public:
virtual void onResize() {
static_cast
// 进行SpecialWindow的专属行为
}
};
// 正确的用法
class SpecialWindow: public Window {
public:
virtual void onResize() {
Window::onResize();
// 进行SpecialWindow的专属行为
}
};
继承中的类型转换效率低:
C++
通过dynamic_cast
实现继承中的类型转换,通常是因为想在一个认定为derived class
对象身上执行derived class
操作没,但是拥有的是一个指向base的指针或引用
。这样做其效率都是相当低的!这种情况下有两种办法可以避免转型:
derived class
对象的指针derived class
中的操作上升到base class
内,成为virtual
函数,base class
提供一份缺省实现dynamic_casts
。如果有个设计需要转型动作,试着发展无需转型的替代设计。C++-style
(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职责。references
、指针和迭代器统统都是所谓的handles
,即号码牌,用来取得某个对象。
handles
,那么相当于成员变量的封装性从private
上升到public
,这显然与条款22:将成员变量声明为private
冲突。const成员函数
返回一个这个指针所指对象的引用,并不会造成指针被修改,也就符合bitwise constness
,但是通过这个引用却可以改变对象实际的数据。handles
指向一个临时对象,那么返回后临时对象被销毁,handles
就会成为"虚吊的",只要handles
被传出去,就会面临"handles
比其所指对象更长寿"的风险。handles
(包括references
、指针和迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const
成员函数的行为像个const
,并将发生"虚吊号码牌"(dangling handles
)的可能性降至最低。考虑一个例子,有一个菜单类,changeBg函数可以改变它的背景,切换背景计数,同时提供线程安全。
class Menu {
public:
// ...
void changeBg(std::istream& src);
private:
Mutex mutex; // 互斥器,提供多种线程互斥访问
Image* bg; // 背景图片
int changeCount; // 切换背景次数
};
void Menu::changeBg(std::istream& src) {
lock(&mutex);
delete bg;
++changeCount;
bg = new Image(src);
unlock(&mutex);
}
异常安全的两个条件:
new Image(src)
发生异常,那么unlock
就永远不会被调用,因此锁资源会泄漏。new Image(src)
发生异常,背景图片会被删除,计数也会改变,但是新背景并未设置成功。对于资源泄漏,条款13:以对象管理资源可以解决;使用智能指针指定删除器也可以解决;
void Menu::changeBg(std::istream& src) {
Lock m1(&mutex);
delete bg;
++changeCount;
bg = new Image(src);
}
对于数据破坏,需要注意异常安全函数的三个保证:
对于前面的例子,可以使用智能指针,以及重排changeBg的语句顺序来满足"强烈保证"。
class Menu {
public:
// ...
void changeBg(std::istream& src);
private:
Mutex mutex; // 互斥器,提供多种线程互斥访问
std::shared_ptr<Image> bg; // 背景图片
int changeCount; // 切换背景次数
};
void Menu::changeBg(std::istream& src) {
Lock m1(&mutex);
bd.reset(new Image(src)); // 以new的执行结果设定bg内部指针
++changeCount;
}
可以使用 copy-and-swap
策略,它通常能够为对象提供异常安全的"强烈保证"。当我们要改变一个对象时,先把它复制一份,然后去修改它的副本,改好了再与原对象交换。
struct MentImpl {
std::shared_ptr<Image> bg; // 背景图片
int changeCount; // 切换背景次数
};
class Menu {
public:
// ...
void changeBg(std::istream& src);
private:
Mutex mutex; // 互斥器,提供多种线程互斥访问
std::shared_ptr<MentImpl> pImpl;
};
void Menu::changeBg(std::istream& src) {
using std::swap;
Lock m1(&mutex); // 获得mutex的副本数据
std::shared_ptr<MenuImpl> pNew(new MenuImpl(*pImpl));
pNew->bg.reset(new Image(src)); // 修改副本
++pNew->changeCount;
swap(pImpl, pNew); // 置换数据,释放mutex
}
Exception-safe functions
)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛出异常型。copy-and_swap
实现出来,但"强烈保证"并非对所有函数都可实现或具备现实意义。使用inline
的优劣:
inline
的优化;提出inline
的两种方式:
大部分编译器拒绝将太过复杂的(例如带有循环或递归)函数inlining
,而所有对virtual
函数的调用也会拒绝inlining
,因为virtual
意味着"等待,直到运行期才能确定调用哪个函数",而inline
意味准备"执行前,先将调用动作替换为被调用函数的本体"。
总的来说,一个表面上看似inline
的函数是否真是inline
,取决于你的环境,主要取决于编译器。幸运的是大多数编译器提供了一个诊断级别:如果它们无法将你要求的函数inline
化,会给你一个警告信息。inline
函数无法随着程序库的升级而升级。
构造函数和析构函数往往是inlining
的糟糕候选人。对于以下代码:
class Base {
public:
// ...
private:
std::string bm1, bm2;
};
class Derived: public Base {
public:
Derived() {}
// ...
private:
std::string bm1, bm2, bm3;
};
虽然看上去Derived构造函数
为空,符合一个函数成为inline的特性。但是为了确保C++
对于"对象被创建和被销毁时发生什么事"做出的各式各样的保证,编译器会在其中安插代码。因此实际的Derived
构造函数可能是这样的:
Derived::Derived() {
Base::Base();
try {dm1.std::string::string();}
catch() {
Base::~Base();
throw;
}
try {dm2.std::string::string();}
catch() {
dm1.std::string::~string();
Base::~Base();
throw;
}
try {dm3.std::string::string();}
catch() {
dm2.std::string::~string();
dm1.std::string::~string();
Base::~Base();
throw;
}
}
对于上述代码。显而易见,真正的编译器会以更精细复杂的做法来处理异常。相同理由也使用与析构函数。
80-20经验法则:平均而言一个程序往往将80%的执行时间花费在20%的代码上。这个法则提醒我们,作为一个软件开发者,你的目标是找出这可以有效增进程序整体效率的20%代码,然后将它inline
或竭尽所能地将它瘦身。但除非你选对目标,否则一切都是徒劳。
inlining
限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,是程序的速度提升机会最大化。function templates
出现在头文件,就将它们声明为inline
。C++并没有把"将接口从实现中分离"这件事做的很好,例如:
#include <string>
#include "date.h"
#include "address.h"
class Person {
public:
// ...
private:
std::string theName;
Date theBirthDate;
Address theAddress;
};
如果没有引入头文件,那么编译将无法通过。但是这样会在Person
定义文件和其含入文件之间形成了一种编译依赖关系。如果这些头文件中任何一个被改变,或这些文件多依赖的其他头文件有任何改变,那么每个含有Person class
的文件都得重新编译,任何使用Person class
的文件也必须重新编译,这样的连串编译依存关系会对许多项目造成难以形容的灾难。
可能会想着将实现细目分开:
namespace std {
class string; // 前置声明,但不正确
}
class Data; // 前置声明
class Address; // 前置声明
class Person {
public:
// ...
};
如果这样做,Person
的客户就只需要在Person
接口被修改过时才重新编译,但是这种想法存在两个问题:
string
并不是class
,它是个typedef
,上述前置声明不正确,正确的前置声明较为复杂。解决这个问题的方法主要有两种:
第一种方法:handle classes
,把Person
分割成两个类:一个只提供接口(Person
),一个负责实现接口(PersonImpl
)。即使用条款25:接口class中只包含一个负责实现接口的class的指针,因此任何改变都只是在负责实现接口的class中进行。这正是接口和实现分离,这种情况下,想Person
这样使用pImpl
的class
往往被称为handle classes
。
#include
#include
class PersonImpl;
class Person {
public:
Person(string& name);
string name() const;
private:
std::shared_ptr
};
Person::Person(string& name): pImpl(new PersonImpl(name)){}
string Person::name() {
return pImpl->name();
}
第二种方法:Interface classes
,令Person
成为一种特殊的abstract class
:
class Person {
public:
virtual ~Person();
virtual std::string name() const = 0;
static std::shared_ptr<Person> create(string& name); // 由于客户不能实例化这个类,只能使用它的引用和指针,所以需要提供一种方法来获得一个实例
};
std::shared_ptr
return std::shared_ptr
}
class RealPerson: public Person {
public:
RealPerson(const std::string& name): theName(name) {}
virtual ~RealPerson(){}
std::string name() const;
private:
std::string theName;
};
// 使用
std::shared_ptr
std::cout << p->name() << std::endl;
Handle classes
和 Interface classes
。full and declaration-only forms
)的形式存在。这种做法不论是否涉及templates
都适用。手机扫一扫
移动阅读更方便
你可能感兴趣的文章