C++ Advanced - Note: const、enum、inline;先初始化再使用变量;smart pointer 智能指针;virtual 虚函数;Destructor 析构函数;self-assignment 自赋值;new 和 delete;若不想使用编译器自动生成的函数,就该明确拒绝。
- Created on 2014-05
- STL : Standard Template Library
- TR1 : 一份RFC的规范,描述加入C++标准程序库的诸多新机能。
- Boost : 一个网站,一个开源的C++程序库。大多数TR1的机能以它的工作为基础
perfer consts, enums, and inlines to #defines.
宏语言定义的变量名,如 #define ASPECT_RATIO 1.653中的ASPECT_RATIO,不会进入 symbol table,对于编译器不可见。
- Extension:符号表,一种用于语言翻译器(例如编译器和解释器)中的数据结构。
- 在符号表中,程序源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址。
- 如常数表、变量名表、数组名表、过程名表、标号表等等,统称为符号表。
enum hack
- 在类内,声明 enum{OneConstNum = 5}
- 用以声明一个常数。而且取enum量的地址,但对const常量可取地址。
use const whenever possible
基础:
char str[] = "test";
char *p = str; // non-const ptr, non-const data
const char *p = str; // non-const ptr, const data
char * const p = str; // const ptr, non-const data
const char * const p = str; // const ptr, const data
char const *p = str; // the same as "const char *p = str;"
迭代器:(注意,与前面常识有悖)
std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin(); // iter的作用像个 T* const
*iter = 10; // 没错,改变的只是它所指向的内容
++iter; // 出错,iter本身是const
...
std::vector<int>::const_iterator cIter = vec.begin(); // cIter的作用像个 const T*
*iter = 11; // 出错,*cIter是const
++iter; // 没错,改变cIter
常函数const不能改变任何成员变量,static变量除外。
当const和non-const成员函数有着实质等价的实现时, 令non-const版本调用const版本可避免代码重复。
make sure that objects are initialized before they're used.
赋值和初始化不同:
- 用初始化列表来初始化自定义类型,而且效率更高
- 如果初始化的是built-in类型,则效率与赋值一样;
- 在构造函数内是赋值。
OneClass::OneClass()
:theName(),
theAddress(),
thePhones(),
num(0)
{...}
- 假如一个自定义类型的成员变量,还可以这样使用nothing"()",去调用其default constructor。
- 成员变量初始化顺序:其类内声明的顺序,不关于初始化列表的顺序。
- 所以,初始化列表的顺序最好和其类内声明顺序一样。
编译单元
- 产生单一目标文件(single object file)的那些源码
- 单一源码文件加上其所include的头文件。
local static对象:
- 函数内的static对象
不同的编译单元内的non-local static对象的初始化顺序无明确定义。
- 解决方法:用local static 代替non-local static,即是使用Singleton单例模式。
know what functions C++ silently writes and calls.
- 默认构造函数
- 复制构造函数
- 析构函数
- 赋值操作符
编译器产生的析构函数是non-virtual的。
编译器产生的copy构造函数和copy assignment操作符的版本,只是将non-static成员变量拷贝至目标对象而已。
如果你有声明了一个构造函数(无论有无参数),编译器便不会给你创建default的的构造函数了。
若base class的copy assignment操作符被声明为private,那么编译器会拒绝为其derived class生成一个copy assignment操作符。
- 使用private声明它们,且不给出具体的定义。
- 当客户企图拷贝时,编译器会阻止你;
- 假如是友元或者成员函数试图拷贝的话,
- 因为它们没有具体定义,linker链接器就会组织它们。
可以定义这样一个基类
class Uncopyable{
protected:
Uncopyable(){}
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
}
class DerivedClass: private Uncopyable{
...
};
这样,帮助重用,而且其子类就都不能调用复制构造函数和复制赋值操作符了。
declare destructors virtual in polymorphic base classes.
一个基类指针指向子类对象, 当这个对象析构的时候,如果基类destructor非virtual, 那么只会调用基类的destructor,而子类的则没有被调用, 导致部分销毁对象,内存泄漏。
基类有virtual函数,其子类就必须有相关的函数实现,即使其不重写。
如果一个class不打算成为基类,就不要声明virtual函数。
要实现virtual函数,必须携带vptr(virtual table pointer)虚函数表指针 vptr指向一个有函数指针组成的数组,成为vtbl(virtual table)虚函数表。 每一个带虚函数的class都带有一个相应的virtual table。
当调用虚函数,取决于该对象的vptr虚函数指针所指的virtual table虚函数表, 编译器在其中寻找适当的函数指针。
而且这个虚指针增加了对象的大小,32位系统增加一个4Bytes的vptr,64位则增加8Bytes。 所以类内至少有一个virtual函数,才去声明虚的destructor。
尽量不要集成STL中不含有virtual函数的容器。
总结
- 带有polymorphic多态性质的base classes应该声明virtual destructor。
- 或它带有任何virtual函数,它就该有虚析构函数。
- 若一个class不作为base class使用,或不是为了多态,就别声明虚析构函数。
prevent exceptions from leaving destructors.
析构函数绝对不要吐出异常。 如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常, 然后吞下它们(不传播)或结束程序。
如果客户需要对某个操作函数运行期间抛出的异常作出反应, 那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
C++不欢迎在析构函数中抛出异常。
never call virtual function during construction or destruction
在子类的构造函数运行时,子类自身类型被解析为基类, 调用的virtual函数是基类的,而不是子类的; 析构函数一样,子类自身类型被解析为基类……
解决方法: 最好将初始化代码,另外放在一个init初始化函数内。
have assignment operators return a reference to *this.
x = y = z;//连锁赋值
为了实现连锁赋值,赋值操作符必须返回一个reference指向操作符的左侧实参。
handle assignment to self in operator=.
赋值操作一般会先释放掉左值,再给左值赋值, 假如左值右值是同一个对象,会导致“在停止使用资源之前意外释放了它”! 所以
Widget& Widget::operator=(const Widget &rhs){
if(this == &rhs) return *this; // 证同测试 identity test
...
}
还有其它方法的!
确保对象自我赋值时operator=有良好的行为。 有关技术包括:1.证同测试;2.copy-and-swap;3.精心周到的语句顺序。
2.copy-and-swap;
Widget tmp(rhs);
swap(temp); // 交换*this和rhs的数据
return *this;
3.精心周到的语句顺序。
Bitmap *pOrig = pb;
this.pb = new Bitmap(*rhs.pd); // 先创建复本
delete pOrig; // 再delete原本
return *this;
copy all parts of an object
copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs),
priority(rhs.priority){
...
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs){
Customer::operator=(rhs);
priority = rhs.priority; // 其它成员变量的赋值
return *this;
}
use objects to manage resources.
Investment *pInv = createInvestment();
... // 中间的代码可能抛出异常,可能return,
// 导致最后无法运行到delete那一行,内存泄漏
delete pInv;
可以把资源放到对象里面,利用析构函数自动调用的机制,确保资源释放的问题。 例如智能指针shared_ptr、unique_ptr。
std::auto_ptr<Investment> pInv(createInvestment());
关键:
- 获得资源后立即放进管理对象内。这个观念被称为“资源取得时机便是初始化时机”。 (Resource Acquisition Is Initialization——RAII) 2.管理对象运用析构函数确保资源被释放。 若资源释放动作可能导致抛出异常,看条款8怎么处理。(另外使用一个普通函数进行该操作)
auto_ptr和tr1::shared_ptr、unique_ptr等都在其析构函数内做delete而不是delete[], 意味着别将动态分配的array数组交给智能指针!会内存泄漏。
C++并没有特别针对“动态分配数组”而设计类似的智能指针, vector、string可以取代动态分配而得的数组。 boost::scored_array和boost::shared_array classes,就提供了以上你想要的内容, 可是还没有采纳入C++标准库中。
think carefully about copying behaviour in resource-managing classes.
RAII对象被复制,应该怎么处理,有两种方式:
- 禁止复制。因为这样并不合理。
- 使用类似于shared_ptr的引用计数(reference-count)。
而且要注意:
- 深拷贝底部资源
- 或者转移底部资源的拥有权,如unique_ptr
provide access to raw resource in resource-managing classes.
- APIs往往要求访问原始资源raw resources,所以每个RAII class应该提供一个get()方法,
- 对原始资源的访问可能经由显式转换或隐式转换。
一般隐式转换较方便,显式转换较安全。
class Font(){
...
FontHandle f; // 原始资源
operator FontHandle() const // 隐式转换
{ return f; }
}
use the same form in corresponding uses of new and delete.
避免 typedef std::string AddressLines[4]; // 忘了typedef的用法自己查查!
因为当你 std::string* pal = new AddressLines;
别人不知道该用 delete pal;
还是 delete[] pal;
(这个才正确)。
new时,使用了[],必须在相应的delete表达式中也试用[]。
store newed objects in smart pointers in standalone statments.
以独立语句将new出的对象储存于(置入)智能指针内。 如果不这样做,一旦有一场被抛出,有可能导致难以察觉的资源泄漏。
processWidget(std::tr1::shared_ptr(new Widget), priority()); 不同编译器以何种顺序执行:
- A. new Widget
- B. tr1::shared_ptr的构造函数
- C. 调用priority()
假如以ACB顺序执行,“调用priority()”时可能抛出异常, 导致new出的Widget没有及时放入智能指针, 还是导致内存泄漏了……
所以分开写成这样: std::tr1::shared_ptr pw(new Widget); processWidget(pw, priority());
make interface easy to use correctly and hard to use incorrectly.
函数调用的时候,参数的顺序可能出错,所以可以通过导入相应的类型预防。 如:
Date(const Month& m, const Day& d);
// 而非直接用int指代月份和日,还可以用enum
Investment* createInvestment();
// 这样要求用户记得delete,而且不超过1次
不能将责任推给智能指针,因为用户还是可能忘记使用。所以
std::tr1::shared_ptr<Investment> createInvestment();
强行返回智能指针,先发制人,要求客户使用智能指针。
智能指针可以指定删除器,而非总是使用delete。 std::tr1::shared_ptr pInv(0, getRidOfInvestment); getRidOfInvestment是作为删除器的函数名(函数指针)。 上例并不够好,0只是个int,而非空指针,而智能指针坚持要一个指针,所以
std::tr1::shared_ptr<Investment> pInv(static_cast<Investment*>(0), getRidOfInvestment);
阻止误用的办法:建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。 tr1::shared_ptr支持定制删除器 (custom deleter)。 可以防范DLL问题,可被用来自动解除互斥锁(见条款14,原书)。