可以将C++视为一个由许多次语言组成的联邦:
每个次语言都有自己的规约。
可以理解为“宁可以编译器替换预处理器”。
例如对于#define ASPECT_RATIO 1.653
,当你运用此常量但获得一个编译错误时,可能会带来困惑,因为这个错误信息也许会提到1.653而不是ASPECT_RATIO
。如果ASPECT_RATIO
定义在一个非你所写的头文件内,你肯定对1.653以及它来自何处毫无概念,于是你将因为追踪它而浪费时间。解决方法是改为:
const double AspectRatio = 1.653; //大写名称通常用于宏,因此此处改变名称写法
作为一个语言常量,AspectRatio
肯定会被编译器看到,当然就会进入记号表内。
注:string
对象通常比其前辈char *-based
合宜。
const char* const authorName = "Scott Meyers";
const std::string authorName("Scott Meyers"); //更好
如果你不想别人获得一个pointerhu哦这reference指向你的某个整数常量,enum可以帮助你实现这个约束。
对于单纯常量,最好以const对象或enums替换#define
。
对于形似函数的宏,最好改用inline函数替换#define
。
当const
和non-const
成员函数有着实质等价的实现时,令non-const
版本调用const
版本可避免代码重复。
函数内的static对象称为local static对象(因为它们对函数而言是local),其他static对象称为non-local static对象。
编译单元是指产出单一目标文件的那些源码。基本上它是单一源码文件加上其所罕入的头文件。
如果某编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对定义于不同编译单元内的non-local static对象的初始化次序并无明确定义。
针对这个问题,需要做的是:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。其实这也是单例模式的一个常见使用手法。这个手法的基础在于C++保证,函数内的local static对象会在该函数被调用期间首次遇上该对象之定义式时被初始化。但是要注意不管是local static还是non-local static对象,在多线程环境下“等待某事发生”都会有麻烦。处理这个麻烦的一个办法是:在程序的单线程启动阶段手工调用所有reference-returning函数,这可以消除与初始化有关的“竞速形势”。
在derived class对象的base class构造期间,对象的类型是base class而不是derived class。不只virtual函数会被编译器解析至base class。若使用运行期类型信息,也会把对象视为base class类型。
class Widget{
public:
...
Widget& operator+=(const Widget& rhs) //这个协议适用于+=,-=,*=等等
{
...
return *this;
}
Widget& operator=(int rhs) //此函数也适用,即使此一操作符的参数类型不符协定
{
...
return *this;
}
}
当编写一个copying函数时,请确保(1)复制所有local成员变量 (2)调用所有base classes内的适当的copying函数。
Copying函数应该确保复制“对象内的所有成员变量”及“所有base"成分。
不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。
获得资源后立刻放进管理对象内,管理对象运用析构函数确保资源被释放。
可使用智能指针来协助资源管理。
以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。
在member函数(它不止可以访问class内的private数据,也可以取用private函数、enum、typedefs等等)和一个non-member,non-friend函数(它无法访问上述任何东西)之间做抉择,两者提供相同机能,那么导致较大封装性的是non-member non-friend函数,因为它不增加”能够访问class内之private成员“的函数数量。
可以将这个相关的non-member non-friend函数与对应的类位于同一个namespace(命名空间中),因为namespace和class不同,namespace可以跨越多个源码文件而class不能。并将与不同方面相关的函数分别放置于不同头文件中,可以减少编译依赖关系。
C++标准库就是这样组织的,每个头文件声明std的某些机能。如果客户只想使用vector相关机能,它不需要#include
。这允许客户只对他们所用的那一小部分系统形成编译相依。
将所有便利函数放在多个头文件内但隶属同一个命名空间,意味客户可以轻松扩展这一组遍历函数。他们需要做的就是添加更多的non-member non-friend函数到此命名空间内。
如果swap缺省实现版对你来说效率不足,试着做以下事情:
类成员函数版本的swap绝不可抛出异常的原因是:swap的一个最好的应用就是帮助classes(和class template)提供强烈的异常安全性保障(条款29对此主题提供了所有细节),这基于一个假设–成员函数版的swap绝不抛出任何异常。高效率的swaps几乎总是基于对内置类型的操作(例如pimpl手法的底层指针),而内置类型上的操作绝不会抛出异常。
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap; //令std::swap在此函数内可用
...
swap(obj1, obj2); //为T型对象调用最佳swap版本
...
}
一旦编译器看到对swap的调用,便查找适当的swap并调用之。C++的名称查找法则确保找到global作用域或T所在之命名空间内的任何T专属的swap。如果没有找到T专属的swap,编译器就使用std内的swap,这得益于using声明式让std::swap
在函数内曝光。
程序库设计者必须评估“将函数声明为Inline”的冲击:inline函数无法随着程序库的升级而升级。换句话说如果f是程序库内的一个Inline函数,客户将"f函数本体“编进其程序中,一旦程序库设计者决定改变f,所有用到f的客户端程序都必须重新编译。这往往是大家不愿意见到的。然而如果f是non-inline函数,一旦它有任何修改,客户端只需要重新链接就好,远比重新编译的负担少很多。如果程序库采取动态链接,升级版函数甚至可以不知不觉地被应用程序吸纳。
并且大部分调试器面对inline函数都束手无策,因为无法在一个并不存在的函数内设置断点。这样一个合乎逻辑的策略就是一开始先不要将任何函数声明为inline,或者至少将inline的实施范围限制在那些”一定要成为inline“的函数身上。
如果使用object references 或者 oject pointers可以完成任务,就不要使用objects。
如果能够,尽量以class声明式替换class定义式。
virtual函数系动态绑定,而缺省参数值却是静态绑定。意思是你可能会在“调用一个定义于derived class内的virtual函数”的同时,却使用base class为它所指定的缺省参数值。C++这样做是为了运行期效率,如果缺省参数值也是动态绑定的话,编译器就必须有某种办法在运行期为virtual函数决定适当的参数缺省值。
可以采用NVI(non-virtual interface)手法:令base class内的一个public non-virtual函数调用Private virtual函数,后者可以被derived class重新定义,这样我们需要给public non-virtual函数设置默认参数即可。
编译器不会自动将一个derived class对象转换为一个base class对象。
private继承纯粹是一种实现技术。private继承意味着implemented-in-terms-of(根据某物实现出)。这与复合(composition)的意义相同,那么如何取舍:尽可能使用复合,必要时才使用Private继承。何时才是必要?主要是当protected成员和/或virtual函数牵扯进来的时候,即当一个意欲成为derived class者想访问一个意欲成为base class者的Protected成分,或为了重新定义一个或多个virtual函数。
C++有个规则:如果解析器在template中遭遇一个嵌套从属名称(例如T::const_iterator
)。它便假设这个名称不是个类型,除非你使用typename告诉它是。
template<typename IterT>
void workWithIterator(IterT iter)
{
typedef typename std::iterator_traits<IterT>::value_type value_type;
value_type temp(*iter);
}
template实参推导过程中从不将隐式类型转换函数纳入考虑。
当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template 内部的friend函数”。
traits使得你在编译期间取得某些类型信息。
Traits并不是C++关键字或者一个预先定义好的构件:它们是一种技术,也是一个C++程序员共同遵守的协议。它对于内置类型和用户自定义类型的表现必须一样好。标准技术是将它放进一个template及其一或多个特化版本中。这样的template在标准库中有若干个,其中针对迭代器者被命名为iterator_traits
。
template<typename IterT>
struct iterator_traits
{
typedef typename IterT::iterator_category iterator_category;
}
template<typename IterT>
struct iterator_traits<IterT *> //针对指针类型进行template偏特化
{
typedef random_access_iterator_tag iterator_category;
}
iterator_traits
的运作方式是针对每一个类型IterT
,在struct iterator_traits
内一定声明某个typedef
名为iterator_category
。这个typedef
用来确认IterT
的迭代器分类。
如何使用一个traits class:
Traits classes使得“类型相关信息”在编译器可用,它们以templates和templates 特化完成实现。
整合重载技术后,trait classes有可能在编译器对类型执行if...else
测试。
当operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,一个所谓的new-handler。为了指明这个“用于处理内存不足”的函数,客户必须调用set_new_handler
,那是声明于
的标准库函数。
namespace std
{
typedef void(*new_handler)();
new_handler set_new_handler(new_handler p) throw();
}