Effective C++ 55:熟悉一下 Boost
Boost是一个C++开发者的社区,作为C++标准的试验场,收容了很多高质量、开源的、跨平台、独立于编译器的C++库,包括许多TR1组件的实现。
深入 C++ 的诸多设计细节,了解实际场景的最佳实践,以面向对象的方式重新认识 C++。
Boost是一个C++开发者的社区,作为C++标准的试验场,收容了很多高质量、开源的、跨平台、独立于编译器的C++库,包括许多TR1组件的实现。
标准C++库是由STL, iostream, 本地化,C99组成的。TR1添加了智能指针、通用函数指针、哈希容器、正则表达式以及其他10个组件。 TR1在是一个标准(standard),为了使用TR1你需要一个TR1的实现(implementation),Boost就是一个很好的TR1实现。
请严肃对待所有warning,要追求最高warning级别的warning-free代码;但不要依赖于warning,可能换个编译器有些warning就不在了。
new和delete是要成对的,因为当构造函数抛出异常时用户无法得到对象指针,因而delete的责任在于C++运行时。 运行时需要找到匹配的delete并进行调用。因此当我们编写了"placement new"时,也应当编写对应的"placement delete", 否则会引起内存泄露。在编写自定义new和delete时,还要避免不小心隐藏它们的正常版本。
new需要无限循环地获取资源,如果没能获取则调用"new handler",不存在"new handler"时应该抛出异常; new应该处理size为零的情况; delete应该兼容空指针; new/delete作为成员函数应该处理size > sizeof(Base)的情况(因为继承的存在)。
实现一个operator new很容易,但实现一个好的operator new却很难。
new申请内存失败时会抛出"bad alloc"异常,此前会调用一个由set_new_handler()指定的错误处理函数("new-handler")。
模板元编程(Template Metaprogramming,TMP)就是利用模板来编写那些在编译时运行的C++程序。模板元程序(Template Metaprogram)是由C++写成的,运行在编译器中的程序。当程序运行结束后,它的输出仍然会正常地编译。
C++中的 Traits 类可以在编译期提供类型信息,它是用Traits模板及其特化来实现的。通过方法的重载,可以在编译期对类型进行"if...else"判断。我们通过STL中的一个例子来介绍Traits的实现和使用。
如果所有参数都需要隐式类型转换,该函数应当声明为非成员函数。本文把这个观点推广到类模板和函数模板。但是在类模板中,需要所有参数隐式转换的函数应当声明为友元并定义在类模板中。
成员函数模板可以使得函数可以接受所有兼容的类型。如果你用成员函数模板声明了拷贝构造函数和赋值运算符,仍然需要手动编写普通拷贝构造函数和拷贝运算符。
把模板中参数无关的代码重构到模板外便可以有效地控制模板产生的代码膨胀。另外代码膨胀也可以由类型模板参数产生:对于非类型模板参数产生的代码膨胀,用函数参数或数据成员来代替模板参数即可消除冗余;对于类型模板参数产生的代码膨胀,可以让不同实例化的模板类共用同样的二进制表示。
从面相对象C++转移到模板C++时,你会发现类继承在某些场合不在好使了。比如父类模板中的名称对子类模板不是直接可见的,需要通过this->前缀、using或显式地特化模板父类来访问父类中的名称。
模板参数前的typename和class没有任何区别;但 typename还可以用来帮编译器识别嵌套从属类型名称,基类列表和成员初始化列表除外。
面向对象设计中的类(class)考虑的是显式接口(explicit interface)和运行时多态,而模板编程中的模板(template)考虑的是隐式接口(implicit interface)和编译期多态。
多继承比单继承复杂,引入了歧义的问题,以及虚继承的必要性;虚继承在大小、速度、初始化/赋值的复杂性上有不小的代价,当虚基类中没有数据时还是比较合适的;多继承有时也是有用的。典型的场景便是:public继承自一些接口类,private继承自那些实现相关的类。
子类继承了父类的实现,而没有继承任何接口。 private继承和对象组合类似,都可以表示"is-implemented-in-terms-with"的关系。但对象组合往往比继承提供更大的灵活性。
一个类型包含另一个类型的对象时,我们这两个类型之间是组合关系。组合是比继承更加灵活的软件复用方法。
因为虽然虚函数的是动态绑定的,但默认参数是静态绑定的。只有动态绑定的东西才应该被重写。
Never redefine an inherited non-virtual function.
非虚接口范式(NVI idiom)可以实现模板方法设计模式。用函数指针代替虚函数,可以实现策略模式。用function代替函数指针,可以支持所有兼容目标函数签名的可调用对象。用另一个类层级中的虚函数来提供策略,是策略模式的惯例实现。
当你public继承一个类时,接口是一定会被继承的,你可以选择子类是否应当继承实现。不继承实现,只继承方法接口:纯虚函数。继承方法接口,以及默认的实现:虚函数。继承方法接口,以及强制的实现:普通函数。
子类中的名称会隐藏父类中所有同名的属性。public继承表示这"is-a"的关系,应该避免这样做。使用using声明或者转发函数可以使父类名称再次可见。
C++类的继承比现实世界中的继承关系更加严格:任何适用于父类的性质都要适用于子类!
最小化编译依赖的一般做法是依赖于声明而非定义,这个想法可以通过句柄类或接口类来实现。库的声明应当包括“完整的”和“只有声明的”两种形式。
inline(内联函数)避免了宏的缺点,也不需要付出函数调用的代价。也方便了编译器基于上下文的优化。但inline函数可能会造成目标代码膨胀和指令缓存的Miss。
异常安全是指当异常发生时,不会泄漏资源,也不会使系统处于不一致的状态。通常有三个异常安全级别:基本保证、强烈保证、不抛异常(nothrow)保证。
这里的“句柄”(handle)包括引用、指针和迭代器。这样可以增加类的封装性、使得`const`函数更加`const`,也避免了空引用的创建(dangling handles)。
C++的类型检查只在编译时执行,运行时没有类型错误的概念。理论上讲只要你的代码可以编译那么就运行时就不会有不安全的操作发生。但C++允许类型转换,也正是类型转换破坏了理论上的类型系统。
这一规则在任何编程语言中都适用,一方面可以避免无用的构造使得程序更高效,另一方面作用域的缩小会使程序更加清晰。
提供一个更加高效的,不抛异常的公有成员函数(比如 `Widget::swap`)。在你类(或类模板)的同一命名空间下提供非成员函数 `swap`,调用你的成员函数。如果你写的是类而不是类模板,请偏特化 `std::swap`,同样应当调用你的成员函数。调用时,请首先用 `using` 使 `std::swap` 可见,然后直接调用 `swap`。
如果运算符的所有“元”都需要隐式转换时,请重载该运算符为友函数。
相比于成员函数,非成员函数提供了更好的封装,包的灵活性(更少的编译依赖),以及功能扩展性。
数据成员声明为私有可以提供一致的接口语法,提供细粒度的访问控制,易于维护类的不变式,同时可以让作者的实现更加灵活。而且我们会看到,`protected`并不比`public`更加利于封装。
永远不要返回局部对象的引用或指针或堆空间的指针,如果客户需要多个返回对象时也不能是局部静态对象的指针或引用。
Item 20: Prefer pass-by-reference-to-const to pass-by-value C++函数的参数和返回值默认采用传值的方式,这一特性是继承自 C 语言的。如果不特殊指定, 函数参数将会初始化为实参的拷贝,调用者得到的也是返回值的一个副本。 这些拷贝是通过调用对象的拷贝构造函数完成的,正是这一方法的调用使得拷贝的代价可能会很高。 通常来讲,传递常量引用比传值更好,同时避免了截断问题。但是内置类型和 STL 迭代器,还是传值更加合适。
在面向对象语言中,开发者的大部分时间都用在了增强你的类型系统。这意味着你不仅是类的设计者,更是类型设计者。重载函数和运算符、控制内存分配和释放、定义初始化和销毁操作……良好的类型有着自然的语法、直观的语义,以及高效的实现。你在定义类时需要像一个语言设计者一样地小心才行!
总之,好的接口容易被正确使用,不易被误用。可以为内置类型提供一致的接口来方便正确的使用。识别误用的手段包括:创建新的类型、限制类型的操作、限制对象的值、移除客户的资源管理责任。
在单独的语句中将new的对象放入智能指针,这是为了由于其他表达式抛出异常而导致的资源泄漏。因为C++不同于其他语言,函数参数的计算顺序很大程度上决定于编译器。
如果你用`new`申请了动态内存,请用`delete`来销毁;如果你用`new xx[]`申请了动态内存,请用`delete[]`来销毁。
资源管理对象需要提供对原始资源访问。获取资源的方式有两类:隐式地获取和显式地获取。通常来讲,显式的资源获取会更好,它最小化了无意中进行类型转换的机会**
资源管理对象的拷贝行为取决于资源本身的拷贝行为,同时资源管理对象也可以根据业务需要来决定自己的拷贝行为
创建资源后立即放入资源管理对象中,并利用资源管理对象的析构函数来确保资源被释放。复制一个 auto_ptr 会使它变成空
在一个成熟的面向对象的C++系统中,只有两种拷贝对象的方式:复制构造函数和赋值运算符。当重载拷贝函数时,首先要完整复制当前对象的数据(local data);然后调用所有父类中对应的拷贝函数。
赋值运算符的重载要注意自赋值安全和异常安全。有三种方法: 1. 判断两个地址是否相同 2. 仔细地排列语句顺序 3. Copy and Swap
这是关于赋值运算符的编程惯例,用来支持链式的赋值语句。
父类构造期间,对虚函数的调用不会下降至子类。如果这并非你的意图,请不要这样做!
由于析构函数常常被自动调用,在析构函数中抛出的异常往往会难以捕获,引发程序非正常退出或未定义行为。
析构函数声明为虚函数恐怕是面试中最常见的问题之一。目的在于以基类指针调用析构函数时能够正确地析构子类部分的内存。
有时候我们希望禁用掉这些函数。比如对于一个单例而言,我们不希望它能够被直接构造,或者拷贝。我们通过把自动生成的函数设为`private`来禁用它。
在C++中,编译器会自动生成一些你没有显式定义的函数,它们包括:构造函数、析构函数、复制构造函数、`=`运算符。
出于效率原因,C++不保证**非成员对象的内置型**的初始化。对于成员变量的内置类型,会在构造函数进入之前进行初始化。
尽量使用常量,以逻辑常量的方式编写常量方法,使用普通方法调用常量方法可避免代码重复。
尽量使用常量、枚举和内联函数,代替`#define`。
C++程序设计的惯例并非一成不变,而是取决于你使用C++语言的哪一部分。