effective C++ 笔记

虽然秋招没找到工作,还是得继续学习啊。

条款3 尽可能使用const

1)const char *p;是指向常量的指针,不可以改变指向的值,可以改变指向。

char * const p;是常量指针,不可改变指向,但可以改变指针指向的值。

const char * const p;指向常量的常量指针,值和指向都不可以改变。

迭代器就相当于指针,也可以使用const关键字

vector<int> vec;

const vector<int>::iterator iter = vec.begin();  iter的作用相当于 * const p; 

*iter = 10;没问题,改变指针所指的物。

iter++;错误,iter是常量迭代器,不能改变指向的地址。

vector<int>::const_iterator cIter = vec.end(); 此时iter相当于 const *p;

*cIter = 10;错误,不能该表所指物。

cIter++;正确,可以改变指向的地址。

2)mutable可以使const成员函数改变成员变量的值。在必要的情况下可以使用。

3)

当成员函数有const和non const版本,功能一样时,应在non const成员函数中调用const版本,调用方法如下:

先转换成const数据类型使用:staic_cast<const TextBlock&>(*this)[position],在使用staic_cast<char &>将const转换为non-const。

条款4 确定对象被使用前已被初始化

1)

上图所示成员变量是赋值,而不是初始化,初始化发生的更早,发生在这些成员default构造函数调用之时。

使用成员列表初始化,则省去了default构造函数先给成员变量设初值,在赋予新值这一个步骤。成员列表初始化则直接用实参做为各个成员变量构造函数的实参,这样效率比较高。

2)C++有着固定的成员初始化顺序,基类的初始化总是在子类之前。成员变量的次序最好先声明先初始化,否则容易遗漏。

3)为了避免跨单元编译之初始化顺序,最好使用local static 替换 non-local static.

条款5 了解C++默默编写并调用了哪些函数。

1)编译器会给空类定义一个默认构造函数,复制构造函数,重载赋值运算符,还有默认析构函数。这些函数都是public和inline。

只有当这些函数需要被调用的时候,它们才会被编译器创建出来。对于赋值构造函数和赋值运算符,编译器只是将每一个非静态成员变量的值拷贝到目标对象。

条款6 若不想使用编译器为你自动创建的函数,应该主动拒绝。

1)可以将拷贝构造函数和赋值操作符设置为私有成员。然而这种做法并不安全,因为友元函数和成员函数仍然可以调用他们。

2)可以声明一个不含任何数据成员的空类,将拷贝构造函数和赋值操作符设置为私有的。

条款7 为多态基类设置虚析构函数。

1)当一个基类指针指向子类的时候,删除基类时,若基类析构函数为非虚的,则只会调用基类的析构函数,造成局部删除。

2)在类不希望被继承的时候,盲目使用虚析构函数会增加类占用的内存,并且降低移植性。增加的字节根据编译器所给指针的大小,一般为四字节。

条款8 别让异常逃离析构函数。

1)析构函数绝对不要抛出异常,当使用容器类时,很可能会有多个异常出现,会导致程序奔溃。     

2)析构函数抛出得异常一定要在析构函数内捕捉,然后吞下他们(即什么也不干)或者直接让程序结束运行。

3)如果客户需要对某个操作函数运行期间抛出得异常做出反应,应提供一个普通成员函数来处理,而不是在析构函数中处理。

条款9 绝不再析构和构造过程中调用virtual函数

1)在基类构构造期间,虚函数不是虚的,意思就是假如在基类构造函数中调用虚函数,只会调用基类得版本,而不是继承类得版本。因为在基类构造之时,子类还没有进行成员初始化,如果调用子类版本得成员函数,将会出现未知得错误。

2)同样得道理适用于析构函数,子类调用析构函数先释放子类成员,在调用父类析构函数,这时在析构函数中虚函数不虚。因为如果调用子类的虚构函数会将已经释放的成员在释放一遍,这样会造成不确定的行为。

3)解决上述问题的办法就是把构造函数中调用的函数改为非虚函数,然后由子类构造函数传递必要的参数给父类的构造函数。

总结一下就是在构造函数和析构函数中,基类的调用不会下降为子类,就是不会调用子类的函数。

条款10 令operator= 返回一个 reference to *this

1)

2)在赋值操作符中,返回一个指向左侧的引用,这样就可以链式操作。

条款11 在operator= 中处理自我赋值

1)在赋值的时候可能会有自我赋值的情况,例如 a = a,这种情况可能会出现错误。

2)为了避免出现这种错误,我们应该在赋值前,应进行认同测试,即判断是不是自己,是自己则直接返回当前值,不做任何操作。

if(this == &rhs) return *this;

3)也可以采用不认同方式,先将要复制的对象保存,在进行赋值,这样就不会出现错误。

条款12 复制对象时,千万别忘记了对象的每一个成分。

1)当自己编写了复制构造函数和赋值函数,应在添加成员时,注意也在复制构造函数和复制函数里面添加相应的语句。

2)在编写派生类的复制构造函数和赋值函数时,此时的父类的私有成员是无法访问的,不能进行直接赋值,应该调用父类的复制构造函数和赋值函数。

3)切记,不要再复制构造函数中调用赋值函数,反过来也一样,会造成不明确的行为。

条款13 以对象管理资源

1)在使用完资源后,可能会忘记释放资源,或者语句过早的返回,抛出异常跳过释放语句,造成内存泄漏。

2)为了避免内存泄漏,应使用类来管理资源,这样类的生命周期结束后会自动调用析构函数释放资源。例如使用auto_ptr,智能指针在控制流离开区块或函数就被释放,会自动调用析构函数,其析构函数能使用delete释放所指对象的内存。

条款14 在资源管理类中小心copying行为

1)

这个条款没看大懂。。。先放这里吧,以后懂了再回来补上。。。

RAII,也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。它保证在任何情况下,使用对象时先构造对象,最后析构对象。

条款15 在资源管理类中提供对原始资源的访问

1)许多函数需要访问原始资源,所以每一个RAII calss应该提供一个返回原始资源的函数。

2)对原始资源的访问有显示转换和隐示转换两种,一般而言显示比较安全,隐示对客户比较方便。

条款16 成对使用new和delete时要采取相同的形式

条款17 以独立语句将newed对象置入智能指针

这个条款讲的是一定要用独立语句new对象后传入智能指针,在将指针传给函数调用,不然可能会出现内存泄漏。不能写成这样调用 fun(sharped_ptr<对象> (new 对象), fun1());fun1返回另外一个实参,这种情况下,编译器可能先执行 new 对象,然后执行fun1(),如果fun1出现异常,这个new出来的对象就没有置入智能指针,就会出现内存泄漏。

条款18 让接口容易被正确使用,不易被误用。

看的有点懵逼。。。

条款19 设计class犹如设计type

条款20 宁愿传递常量引用(pass by reference to const)而不是值(pass by value)传递

1)默认情况下C++是按值传递对象至函数。

2)使用按值传递会造成许多不必要得资源浪费,例如调用构造函数,析构函数。

3)按引用传递时,如果不想传入的参数被修改,最好将参数设置为const,防止传入得参数在函数内部被修改。

3)在已值传递的过程中,假如将派生类传递给基类参数,将会切割掉派生类的其他特性,只保留基类的特性,因为将会已派生类做为参数调用基类的构造函数。

4)使用引用传递时,不会出现切割问题。

5)在使用内置类型和STL迭代器和函数对象时,按值传递比较合理。

条款21 必须返回对象时,不要返回其引用

1)在使用引用时,一定要提醒自己这是一个别名,一定要想到他的另外一个名字在哪里,如果没有另外一个名字,那可能会出现错误。例如

这种情况就会出现无法定义的情况,在返回result后,result将会被释放,在对该对象进行操作将会出现无法定义的行为。

2)函数返回指向临时对象的引用或者指针,都将发生不明确的行为。因为临时对象在函数结束后就被释放了。

3)在函数中并在堆中给对象分配内存,然后返回对象的引用或者指针,这样将会造成内存泄漏。如下例子:

总结:函数中不要返回临时对象,或在堆上创建的对象,或这静态对象的指针或者引用。返回值就对了。

条款22 将成员变量声明为private

条款23 最好用非成员非友元函数替换成员函数

条款24 若所有参数皆需类型转换,请为此采用非成员函数。

1)当你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是非成员函数。

条款25 考虑写出一个不抛处异常的swap函数

这个没看懂。。。

条款26 尽可能延后变量定义式的出现

条款27 尽量少做转型动作

1)C++四种不同转型

const_cast通常被用来将对象的常量性质移除,它也是唯一有此能力的C++风格转型操作符。

dynamic_cast主要用来执行安全向下转型,也就是用来决定某对象是不是属于其继承类型中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一耗费重大运行成本的转型动作。

reinterpret_cast意图执行低级转型,实际动作可能取决于编译器,不可移植。

static_cast用来强迫隐示转换,例如将非const转换为const,或者将int转换为double等等。它也可以执行上述各种转换的反向转换。但是它不能将const转换为非const。

2)在派生类的虚函数中使用类型转换将派生类指针转换为基类指针后调用基类的函数时,只会产生一个基类副本,然后修改该副本的值,并不会真正修改继承之后基类的值,但是修改派生类的值会生效。可能讲不太清楚,直接贴代码吧

#include <iostream>
#include <vector>
#include <queue>
#include <cmath>
#include <stack>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;

class A
{
public:
    A(int tmp) : a(tmp){}
    virtual void change()
    {
        a+=1;
    }
    void print()
    {
        cout << "This is A" << a << endl;
    }
private:
    int a;
};

class B : public A
{
public:
    B(int tmp1, int tmp2) : A(tmp1), b(tmp2){}
    void change()
    {
        static_cast<A>(*this).change();
        b += 1;
    }
    void print()
    {
        A::print();
        cout << "This is B" << b << endl;
    }
private:
    int b;
};


int main()
{
    B testb(1, 2);
    testb.change();
    testb.print();
    return 0;
}

该程序运行结果是

在B的change函数中并没有真正改变基类的值,只是修改了副本的值,在贴上反汇编吧,这样理解的更详细。

可以看到在调用类型转换后,调用了A的复制构造函数,然后调用了A的change函数,但是改变的是副本的a的值。这样就造成了局部修改。

3)为了避免2中出现的情况可以直接用类名加函数调用基类的函数。

4)使用dynamic_cast转换非常的耗费时间会影响性能,应该尽量避免使用它。

条款28 避免返回handles指向对象内部的成分

1)这个条款说的应该是不要返回类内被封装的成员。会破坏类的封装性,还有可能会出现指针所指物被释放的可能。

条款29 为异常安全而努力是值得的

1)带有异常安全性的函数不会泄漏任何资源,不允许数据败坏。

找到工作了,飘了一段时间,发现还是要好好学习,冲!

条款30 透彻了解内置(inlining)的里里外外

1)当为虚函数申请inline时,会被编译器拒绝,因为虚函数的调用是在运行时期确定,然而inline函数要在编译期时,用函数代码替换,这样编译器就无能为力了。大部分编译器还会拒绝带有递归和循环的inline函数。

2)在某些情况下,编译器还是会给inline函数生成一个outlined函数本体。

inline void fun(); //创建一个inline函数
void (*pf)() = fun; //创建一个函数指针指向fun
fun(); //这个函数的调用将被inline,也就是用函数文本替换
pf(); //这种情况下就不能inline了,得为函数创建一个函数本体,因为它通过函数指针调用

3)使用inline函数时一定要慎重考虑,只有在那些小型,被频繁调用得函数身上使用。

条款31 将文件间得编译依存关系降到最低

这个把我看蒙了,可能我看的时候人是蒙得吧。。。

条款32 确定你的public继承塑模出is-a关系

1)public继承表示is-a关系,也就是说能对基类进行的操作也对能对派生类进行。这是很重要的一点。

条款33 避免遮掩继承而来的名称

1)内层作用域的名称会遮掩外层作用域的名称

int a; //全局变量
void fun()
{
    int a;  //局部变量
    std::cin >> a;
}

如上述例子,函数内的变量将会遮掩全局变量。

2)当继承基类和它的重载函数,你希望重新定义或覆盖其中一部分,你必须为那些原本会被遮掩的部分每个都引用using声明式。

class base
{
public:
    virtual void fun1();
    virtual void fun1(double x);
};

class derived : public base
{
    using base::fun1;
    void fun1();
};

如果不用using引入基类的函数,当调用derived::fun1(double x)时,编译器会报错,因为派生类fun1()覆盖了基类的函数包括他的重载版本。

条款34 区分接口继承和实现继承

1)纯虚函数有两个最突出特点:它们必须被继承了他们的具体类重新声明,在抽象类中一般没有给出函数定义,也可以给出函数定义。

2)在基类中声明一个纯虚函数的意义是为了使派生类继承函数的接口及一份强制实现。、

条款35 考虑 virtual函数以外的其他选择

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章