声明:告诉编译器某个东西的名称和类型,但略去细节。
定义:提供编译器一些声明所遗漏的细节,对对象而言,定义是编译器为其分配内存的地方。
初始化:给予对象初值的过程。
初看题目,对这个语言联邦甚是不解,看完条款1,恍然大悟。所谓语言联邦,即C++由多种编程范式组成,包括:
C++高效编程守则视状态而变化,取决于你是使用C++的哪一部分。
请记住:
const对象或者enum替换#defineinline函数替换#define“宁可用编译器替换预处理器”。宏定义出错溯源困难的原因是:宏定义所使用的名称可能并未进入符号表。宏定义编译时报错,给的错误提示是它所替代的真实值,如果这个宏定义在其他头文件中,查找错误无疑是耗时的。#define就无法做到创建类专属常量。
以常量替换#define有两种特殊情况:
conststatic const修饰,因为要保证这个常量在所有类对象中只存在一份,所以用static修饰,而又要求是常量,所以用const修饰;只要不取这个常量的地址,无需提供定义,只需在类中声明,例如static const int num = 5;,但是,如果坚持要获得这个常量的地址,那么需要在类外提供定义,例如const int Base::num;,由于class常量已经在声明的时候获得初值,所以在类外定义的时候无需再设初值。在类中还可以使用enum来完成第2点中的任务。且enum和#define很相似,因为都无法取地址。
通常不同编译器的规则不太一样,所以使用static const还是enum要根据编译器来定。
令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。例如,如果返回类型没有const,那么可能出现(a*b)=c;的情况。一个“良好的用户自定义类型”的特征是他们避免无端地与内置类型不兼容。
如果两个成员函数只是常量性不同,可以被重载。将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象。
bitwise constness和logical constness
mutable修饰请记住:
请记住:
需要着重强调的是:在构造函数中,初值列中才叫初始化,函数体内是叫赋值!这么也就可以理解,为什么使用初值列比在函数体中“初始化”高效了!而且,对于成员变量是const或者reference的,不能进行复制操作,只能在初值列初始化。
C++有着十分固定的“成员初始化次序”,次序总是相同的:基类早于派生类初始化,类中的成员变量总是以声明的次序被初始化。
如果你自己没有声明,编译器会为一个类声明一个copy构造函数、copy赋值操作符、析构函数,如果你没有声明任何构造函数,编译器会为你声明一个默认构造函数。这些编译器声明的函数都是public inline的。不过,唯有当这些函数被需要,或者说是被调用时,他们才会被编译器创造出来。
编译器产出的析构函数默认是non-virtual的,除非这个类的基类自身声明有虚析构函数。
如果你打算在一个内含引用成员的类内支持赋值操作,你必须自己定义一个拷贝赋值操作符。因为,如果你不指定,编译器生成的默认拷贝赋值操作符会导致你的引用被另一个对象所修改。
请记住:
请记住:
private并且不予实现。继承Uncopyable这样的基类也是一种做法。virtual析构函数请记住:
请记住
所谓的不吐出异常,指的是,在析构函数中就把该异常给处理掉了。
为什么不能让异常逃离析构函数?举个例子,对于一个vector的vector,vector负责析构容器中的每个Widget,当析构第一个元素期间抛出异常,析构函数没有处理,第二个元素析构又抛出异常,此时有两个异常,会导致程序不明确的行为。
请记住:
无论是在构造还是析构过程,当前对象内并不能看到派生类的信息(构造先构造父类,析构先析构派生类)。
对于在构造或者析构中间接调用的虚函数危害比较大,因为它通常不会引起任何编译器和连接器抱怨。
请记住:
x = y = z = 15;这样的语句请记住:
自我赋值可能存在的问题是:在停止使用资源之前意外地释放了该资源。
证同测试
Widget& Widget::operator=(const Widget& rhs){
if(this == rhs) return *this;
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
Widget& Widget::operator=(const Widget& rhs){
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
Widget& Widget::operator=( Widget rhs){ // 注意,是值传递
Wodget tmp(rhs);
swap(tmp); // 交换rhs和this的数据
return *this;
}
请记住:
请记住:
现在auto_ptr已被弃用,取而代之的是unique_ptr。unique_ptr不支持复制、赋值操作,使用move进行所有权转让。
RAII:Resource Acquisition Is Initialization。资源获取即初始化。
请记住:
所以,智能指针的使用是看使用场景的:例如我们只希望有一个指针指向一个对象,那么我们可以使用unique_ptr,允许复制就是用shared_ptr
shared_ptr可以指定析构方法,我们在自定义的资源管理类中,可以引入shared_ptr帮我们完成一些任务。
请记住:
class Font{
public:
...
operator FontHandle() const {
return f;
}
...
}
请记住:
如果你偏不听建议,非得反着写,编译器会给一个warning。
delete最大的问题在于:即将被删除的内存之内究竟存在多少对象。
请记住:
如果不这样做,比如func(shared_ptr,在某些编译器下,可能会把,new,priority(),智能指针的构造顺序搞混。当priority()置于其他两者中间时,且发生异常,new动作遗漏指针,发生内存泄漏。
使用建议中的方案,不会发生重新排列。
请记住:
请记住:
请记住:
pass-by-reference-to-const之所以高效,是因为不需要在传参的时候调用copy构造函数,对于一些类来说,调用copy构造是相当耗时的,不高效的。
对象小并不意味着它的copy构造函数不昂贵;自定义的小对象,其他小容易发生变化。
请记住:
-fno-elide-constructors参数,那么编译器会给你做优化,通过调整栈对象的构造顺序,使得返回值时,可以啥都不用做,直接用函数中构造的变量。A funConference(A& a){
A tmp(a);
return tmp;
}
int main(){
A a;
A b = funConference(a);
}
请记住:
请记住:
请记住:
请记住:
class Widget{
public:
...
void swap(Widget& other){
using std::swap;
swap(pImpl, other.pImpl);
}
...
}
namespace std{
template<>
void swap<Widget>(Widget&a, Widget&b){
a.swap(b);
}
}
请记住:
你不应该只延后变量的定义,指导非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。
如果一个变量只在循环内使用,那么把它定义于循环外并每次给他赋值比较好,还是把它定义于循环内较好呢?这个问题值得思考。
请记住:
如果可以,尽量避免转型,特别是在注重效率的代码中使用dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。
如果转型是必要的,试着将它隐藏域某个函数背后。客户随后可以调用该函数,而不需要将转型放进他们自己的代码中。
宁可使用C++-style转型,不要使用旧式转型。前者很容易辨别出来,而且也比较有着分门别类的职掌。
(T) expression
T(expression)
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
const_cast通常被用来将对象的常量性转除。它也是唯一有此能力的C++-style转型操作符;reinterpret_cast意图执行低级转型,实际动作由编译器决定,这也就意味着它不可移植,很少使用;static_cast用来强迫隐式转换,一般用在内置类型的一些转换。
dynamic_cast主要用于“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型,它是唯一无法由旧式语法执行的动作。通常一个继承体系下,如果有虚函数,一般不用dynamic_cast。一般用在那种不能设置为虚函数的函数调用。dynamic_cast执行速度慢,是因为他会使用RTTI机制,使用type_id找类型,耗时。
有一种情况:
virtual void onResize() {
static_cast<Window>(*this).onResize();
}
这段代码,static_cast中传值方法是值传递,值传递肯定要一个copy的动作,所以会产生一个副本,所以,修改这个副本,不会影响this指针。
使用
virtual void onResize() {
Window::onResize();
}
消除这个问题。
请记住:
请记住:
带有异常安全的函数会:1.不泄露任何资源;2.不允许数据败坏
异常安全函数提供一下三种保证之一:
copy-and-swap:为你打算修改的对象做出一份副本,然后在副本身上进行一切必要修改,然后将修改后的数据和原件置换。
请记住;
编译器如果要对函数做inline,那么它必须知道inline函数长什么样子。这句话说明,inline得在头文件中,发生在编译期。
虚函数、构造、析构函数不适合inline。
inline会引发的问题有:代码膨胀,修改inline导致全重编译。
记住中第二点说:不要只因为function template出现在头文件,就将他声明为inline。因为,template本身就会代码膨胀,inline也会导致这个问题,如果两个在一起,那就是膨胀+膨胀。
请记住:
pimpl手法:在一个类中存放具体实现类的指针。这样,指针大小在32位系统上固定是4字节,无需知道具体实现类的大小。这解决了”接口与实现的分离“。是一种Handle class。
降低编译依存关系:
抽象基类是interface class。
Handle class和Interface class可降低编译依存性。
请记住:
请记住:
如果只想继承父类的部分函数,就可以使用private继承,然后设置一个转交函数,转交函数中使用父类作用域调用父类函数。
条款中,父类有多个重载函数的的情况,不论是虚函数还是非虚函数,都会发生遮掩。
请记住:
请记住:
所以,替换虚函数的方案有:
NVI手法:non-virtual interface
请记住:
请记住:
请记住:
请记住:
private继承意味着implemented-in-terms-of(根据某类实现出)。他通常比复合的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
和复合不同,private继承可以造成empty base最优化(EBO,空白基类最优化)。这对致力于”对象尺寸最小化“的程序库开发人员而言,可能很重要。
总的来说,private继承,可以理解为has-a的关系。但这么说不全对。
如果class之间的继承关系是private,编译器不会自动将一个derived class对象转化为一个base class对象。
如果你让D继承B,你的用意应该是:为了采用B内已经备妥的某些特征,不是因为B对象和D对象有任何观念上的关系。
private继承在软件设计层面上没有任何意义,其意义只在于软件实现层面。
继承空白基类的情况有:NoCopyAble、unary_function、binary_function
请记住:
之所以用virtual继承,是因为在菱形继承体系中,基类的一个变量可能会导致孙类里面有两个副本,这就乱套了。
虚继承的原理:实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚表(virtual table),虚表中记录了vbptr与本类的偏移地址;第二项是vbptr到共有基类元素之间的偏移量。
请记住:
请记住:
typename必须作为嵌套从属类型名称的前缀词
C++有个规则:如果解析器在template中遭遇一个嵌套从属名称,它便假设这个名称不是一个类型。所以,得使用typename
从属名称、嵌套从属名称、非从属名称
请记住:
this->指涉模板基类内的成员名称,或藉由一个明白写出的”基类资格修饰符“完成所谓基类资格修饰符:using、作用域说明符::
这种情况:继承模板类调用模板父类的函数,如果像普通继承体系下那么调用,编译器是找不到父类的这个被调用函数的。这种情况出现的原因是:当编译器看到继承模板类时,模板父类并没有被具现化,那么编译器就不知道父类看起来像什么,也就没办法找到父类的成员函数。
模板全特化:类型参数被全部定义,没有其他模板参数可定义
请记住:

请记住:
一般成员模板函数用于copy构造的时候,都要声明为non-explicit,因为需要允许隐式转换的功能。
对于第二点,如果是泛化的copy构造,没有具现化这个构造函数,那么模板类中就没有拷贝构造函数,那就会生成默认的拷贝构造函数,但是这个可能不是我们想要的,所以需要自己定义一个非泛化的拷贝构造。
请记住:
请记住:
对于第二点,运行期的工作,通过模板转交到了编译期,感觉这种设计很优雅
五种类型的迭代器:
请记住:
先感叹一句,太特么牛逼了!
TMP是被发现的,而不是被发明的。
TMP优点:1.他让某些事情更容易;2.将工作从运行期转到编译期(a。错误能在编译期侦察;b。更高效);3.较小的可执行文件,较短的运行期,较少的内存需求;TMP缺点:编译时间变长,代码膨胀
TMP已被证明是个”图灵完全“机器,意思是它的威力大到足以计算任何事物
TMP的体现:上一个条款的编译期if…else预测,矩阵乘法优化(不会产生临时变量)
其他的,有点没理解透,还需要进一步哟~
请记住:
new-handler是分配内存异常的处理函数,set-new-handler是设置这个处理函数的函数
new-handler必须做以下事情:
请记住:
请记住:
void* operator new(size_t size) throw(bad_alloc){
using namespace std;
if(size != sizeof(BaseClass)){ //这其实包含了size=0的情况
return ::operator new(size);
}
while(true){
尝试分配size byte;
if(分配成功)
return (一个指针,指向分配得来的内存)
// 分配失败;找到目前的new-handler函数
new_handler globalHandler = set_new_handler(0); // 因为无法直接获取new_handler
set_new_handler(globalHandler);
if(globalHandler) (*globalHandler)();
else throw std::bad_alloc();
}
}
请记住:
对于第一点,placement new对应的placement delete版本通常(不是通常,是一定)在placement new发生错误的时候才会被调用。正常的delete是会调用常规的delete版本。如果不提供placement new对应版本的placement delete,那万一在对象构造函数调用的时候失败了,就无法去释放operator new申请的空间,这就内存泄漏了。
C++在global作用域内提供以下形式的operator new:
void* operator new(size_t) throw(bad_alloc); // normal new
void* operator new(size_t, void*) throw(); // placement new
void* operator new(size_t, nothrow_t&) throw(); // nothrow new
所以,针对第二点,要避免无意识地覆盖全局operator new和operator delete就要在类中声明定义对应对operator new和operator delete来分别调用全局的operator new和operator delete
请记住:
请记住:
请记住: