item26 尽可能延后变量定义式出现的时间
太快定义变量并且最终未使用,仍需耗费这些成本(构造&析构)
std::string encryptPassword(const std::string& password){
using namespace std;
string encrypted;
if(password.length()<MinmumPasswordLength){
throw logic_error("Password is too short");
}
//...
return encrypted;
}
上述代码中encrypted太早定义了,因为可能并不会对他赋值;
此外虽然它定义了,但是却没有初始化,因此下次会调用拷贝赋值或者拷贝构造;
:::tips
尽量延后定义式出现的时间,甚至应该尝试延后知道能够给他初值为止;
:::
std::string encryptPassword(const std::string& password){
using namespace std;
if(password.length()<MinmumPasswordLength){
throw logic_error("Password is too short");
}
//...
std::string encrypted(password);
return encrypted;
}
item27 尽量少做转型动作
转型语法的形式
通常有三种不同的转型形式:
- 旧式转型:
(T) expression //将expression转型为T
T(expression)
新式转型
const_cast<T> (expression);
dynamic_cast<T> (expression);
reinterpret_cast<T> (expression);
static_cast<T> (expression)
const_cast
:用来将常量性转除,C++中唯一可以进行此操作的转型操作符 - dynamic_cast
:执行安全向下转型。无法通过旧式语法执行,可能耗费较大运行成本 static_cast
:强迫隐式转换。但是无法将const转成non-const(但是反之可以); :::tips 使用新式转型可以带来好处: 更容易在代码中查找出来
- 转型动作的目标更狭窄,编译器更好判断错误运用。 :::
转型带来的开销和问题
转型实际上是会带来开销或者产生代码的,比如用一个基类指针去指向派生类的地址,可能会有offset(偏移量)被施加在派生类上,从而取得正确的基类指针值。
此外,转型可能会产生临时对象或者副本。
class Window
{
private:
/* data */
public:
virtual void onResize();
};
class SpecialWindow:public Window
{
private:
/* data */
public:
virtual void onResize(){
static_cast<Window>(*this).onResize();
}
};
上述实现中,derived class
中onResize
先是将自己的**this**
指针转型成**Base class**
类型,然后调用基类的**onResize**
,这是不行的!
:::tips
此时调用的并不是当前对象上的函数,而是转型动作建立的一个临时副本上的函数
如果onResize改动了内容,那么实际上改动的只是副本对象的内容,而非当前对象的内容。
:::
正确方式:
virtual void onResize(){
Window::onResize();
}
dynamic_cast带来的开销
dynamic_cast
的许多实现版本执行速度相当慢,例如有一个很普遍的实现版本是基于class名称的字符串比较。每实现一次就会调用一次strcmp
。
:::info
之所以需要dynamic_cast
,通常是因为想要在一个用户认定的derived class对象身上执行derived class的操作函数,但是手上只有指向base class的指针或者引用。
:::
一般有两种解决方案:
- 使用容器并且在其中存储直接指向derived class对象的指针(通常是智能指针);
使用虚函数。 :::tips
如果可以,尽量避免转型,特别是在注重效率的代码中避免
dynamic_cast
- 如果转型是必要的,尽量将他隐藏在某个函数背后,客户可以随后调用此函数而不用将转型放在自己的代码内
- 尽量使用新式的转型。 :::
item28 避免返回handles指向对象内部成分
有些程序返回reference的形式的成员变量;这在某种程度上会改变成员变量的封装性 :::tips
- 成员变量的封装性只等于返回其
reference
的函数的访问级别。 - 如果
const
成员函数返回一个reference
,后者所指数据与对象自身有关,且他又被存储在对象之外,那么函数调用者可以修改此数据。 - 返回的如果是迭代器或者指针也会有同样的问题
:::
:::info
解决方案:只对他们的返回值加上
const
即可; :::
item29 为“异常安全”而努力是值得的
异常安全性函数
异常被抛出时,异常安全性函数会 :::info
- 不泄露任何资源,保证动态创建的资源会被释放。
不允许数据被破坏,保证要么原始数据完好,要么新的数据替换。 ::: 异常安全性函数会提供以下三个保证之一: :::tips
基本承诺:如果异常被抛出,程序内任何事物仍保持在有效状态下。
- 强烈保证:如果异常被抛出,程序状态不变;(换句话说,无异常则完全成功,否则恢复到调用函数前状态)
- 不抛掷保证,承诺绝不抛出异常。
:::
一般而言,不抛置保证很难实现,常常回在基本和强烈中间选择。
copy and swap策略
:::info
- 为打算修改的对象做出一个副本
- 在该副本上做出一切修改
- 若有修改抛出异常,则原来对象仍未改变
- 否则将修改过的副本和源对象在一个不抛置异常的
swap
中交换 :::
:::danger 函数提供的“异常安全保证”通常最高只等于其调用之各个函数的“异常安全保证”中的最弱者 :::
item30 透彻了解inlining的里里外外
inline函数是什么?
inline是C++关键字,在函数声明或定义中,函数返回类型前加上关键字inline,即可以把函数指定为内联函数。这样可以解决一些频繁调用的函数大量消耗栈空间(栈内存)的问题。关键字inline必须与函数定义放在一起才能使函数成为内联函数,仅仅将inline放在函数声明前面不起任何作用。inline是一种“用于实现”的关键字,而不是一种“用于声明”的关键字。对于内联函数,C++有可能直接用函数体代码来替代对函数的调用,这一过程称为函数体的内联展开。
:::tips
Key:用空间换时间
:::
inline函数通常置于哪里?
inline函数通常在头文件中,因为函数需要在编译时吧inline展开,替换时需要知道该函数长得什么样子。
inline常不做用于哪些函数?
:::info
virtual
函数- 构造和析构
:::
template
通常也被放在头文件内,理由和inline
一样:编译器需要在编译的时候知道template
的全部内容。但是这并不意味着我们要把template
声明为inline
item31 将文件间的编译依存关系降至最低
对于编译依存性高的代码,一个文件改动了可能所有涉及该文件的代码都需要重新编译;
通过用前置声明替代定义的方式并不能改变这一问题,因编译器在编译期间需要知道对象的大小。
handle class
pointer to implementation
:::tips 一个技巧是,通过声明的依存性替换定义的依存性。将需要的类分割成两个:
- 一个只提供接口
- 一个负责实现接口
这样修改时只需要编译负责实现的部分? :::
//Person.h的内容
class PersonImpl;
class Date;
class Address;
class Person{
public:
Person(string& name,Date& birthday,Address& addr);
string name();
string birthday();
string address();
private:
shared_ptr<PersonImpl> pImpl;
}
//-------------------------
#include"Person.h"
#include"PersonImpl.h"
Person::Person(string& name,Date& birthday,Address& addr)
:pImpl(new PersonImpl(name,birthday,addr))
{}
string Person::birthday()const{
return pImpl->birthday();
}
- 如果使用引用或者指针可以完成任务,就不要使用对象
- 如果能够,尽量以
class
的声明替代class
的定义式。 - 为声明和定义提供不同的头文件。
interface class
另一种方式实现handle class 的形式是使用将Person
类变成一个特殊的抽象基类。这种类的目的是为了描述派生类的接口,通常不带成员变量也没有构造函数,只有一个virtual
的析构函数。
客户必须以class Person{
public:
virtual ~Person();
virtual std::string name() const =0;
virtual std::string birthday() const =0;
virtual std::string address() const =0;
}
Person
的pointer
或者reference
来撰写应用程序,除非interface class的接口被修改了,否则客户不需要重新编译。