[essential c++] 读书笔记

#1 ========================================================================================================

空容器:没有任何元素,只表明当前容器内存放什么样的数据
vector<A> v1;
deque<int> d1;

指定容器容量:指定容量,每个元素都使用默认值
vector<A> v1(10);        //10个元素,每个都使用A的默认构造

指定容量和初值:指定容量,给所有元素相同的初值
vector<A> v1(10,A(1));        //使用临时变量A(1),给容器内的10个元素赋初值
        (需要A有响应的转换/构造函数,比如这里A就要有 A(int) 构造函数)
        
使用两个迭代器之间的值:所有容器都有一个构造函数,那就是接收两个迭代器,使用这两个迭代器中间的值来构造自己
vector<A> v2(v1.begin(),v1.end());        //v2使用v1的所有值来构造自己

使用其他容器构造:所有容器都具备拷贝构造函数
vector<A> v2(v1);            //使用v1来构造v2

使用其他容器赋值:所有容器都具备赋值拷贝函数
vector<A> v2 = v1;            //使用v1给v2赋值

#2 ========================================================================================================

容器的移除元素动作不会调用析构函数,因此如果把对象从容器中移除,请确保使用了 共享指针 或者 手动释放对象。


#3 ========================================================================================================

常用算法:
https://docs.microsoft.com/zh-cn/previous-versions/yah1y2x8(v=vs.120)?redirectedfrom=MSDN


#4 ========================================================================================================

常用的函数对象:
#include <functional>

常用函数对象的adaptor    :  一些对adaptor再包装的函数对象

(!!!)#5 ==============================================================================================

Iterator inserter 迭代器插入器

上面提到了,可以使用 拷贝构造 和 赋值拷贝 来给新的容器赋值/初始化。这个时候被赋值的容器会自动增长到
实参的大小。

比如:v1有10个元素,v2是空容器(或小于10),这个时候 拷贝构造 和 拷贝赋值 都会让v2变成10个元素。
可见,STL在实现容器模板类的时候考虑到了自动扩容的问题。

但是,如果拷贝和移动相关的泛型算法缺没有考虑到这个问题。还是上面的例子,如果使用copy将 v1 的 2~8号元素
copy给v2 ,则会 coredump,因为v2 是空的(或小于 7的容量)。
(!)这说明,泛型算法的不具备扩容能力。我们在使用copy相关的拷贝/移动算法时,必须保证目的端有足够的容量来接纳
即将到来的数据源。
这个陷阱很危险!!!

根本原因;之所以会有上面的问题,在于copy这些算法的本质是使用了模板参数类的赋值运算符作为底部支撑,因此
          在解引用赋值的时候会出现解空地址的问题。
          
那么如果来修补这个问题???STL提供了三种 adaptor 修饰 copy类函数的第三个参数(目的容器)。
1) back_inserter(v1)                //在目的容器的尾端增加将要被赋值的元素,比如v1有10个元素,这10个全
                                    //都拷贝,那么按照之前的说法,v2的容量至少要是10,但是使用这个adapter
                                    //以后就不需要考虑这个问题了,adapter会自己解决扩容的问题。
                                    // copy(v1.begin(),v1.end(),back_inserter(v2));        把v1的所有内容追加到v2尾部
                                    //,会保留v2的原有内容
2) inserter(v1,v1.begin()++)        //在指定位置插入,copy(++v1.begin().--v1.end(),inserter(v2,++v2.begin()))
                                    //将v1 的 第2 ~ 倒数第2 的元素拷贝到 v2的第2个元素位置,v2的其他元素被排
                                    //到 v1 拷贝过来的元素后面。
3) front_inserter(v1)                //同上,在首部插入,注:考虑到容器的数据结构,此适配器只能作用于可头部增加的
                                    //容器————>list 和 deque


#6 ========================================================================================================

mutable的潜台词:

mutable用来解除const的限制,比如我们有一个const实例 const A c_a ; 那么我们无法通过A的成员函数
来修改其成员变量,即便这个成员变量是public的也不行。那么我们如何应对这种场景呢,毕竟实例一旦
被定义成const就表示其中所有成员都是const的,这个时候就可以用mutable修饰想要特殊处理的成员变量。
那么这个时候就可以修改了,即便是cosnt 实例也不影响。

#7 ========================================================================================================

拷贝构造 、拷贝赋值、移动构造、移动赋值 ,这四种 复制类 的动作,都需要在实施 复制 之前对现有对象和
目标对象进行重复检查,即 “不能把自己复制给自己” --->   

        if(this!=&target)    //如果不相同
        {
            //复制值的动作
        }
        return *this;

#8 ========================================================================================================

递增/递减  的前置 和  后置

前置:operator++()    operator--()
后置:operator++(int)  operator--(int)

从众多开源项目的源码中也可窥见,前置用的比较多,因此在编译器的规则中,也将前置动作视为惯用的,这
也便于助记,即前置的重载括号内没有东西,后置重载括号内有int


#9 ========================================================================================================

指向 类成员函数的指针:

指向普通函数的指针很简单,指明返回值和入参列表的类型即可,然后通过通过取值 * 或 括号() 调用即可:
    void func(int){
        ....
    }
    typedef void(*pf)(int);
    pfunc pf_1 = func();

    
类比,指向类成员函数的指针需要注意如下几点:
1)需要遵循类的 访问权限控制,即外部指针 只能指向类的 public成员函数,不能指向private,但是如果指针就是
   类的成员,则可以指向private成员函数
2)定义时,需要指明类名,typedef void (A::*pf)(int);
3)使用时,需要 使用 .* 或者 ->* ,或者如果是 静态成员函数被指向了,那么可以直接 A::* 。 即需要绑定到指定
   的类实例才能调用(静态成员函数除外)
4)成员函数地址给指针时要用取地址运算符

例子:

    typedef bool(A::*p_A_fun)(void);            //能够 指向 A中所有 “bool返回值,无入参” 成员函数的指针类型

    p_A_fun p_pub_func;
    p_pub_func = &A::pub_func;                //让p_pub_func指向pub_func成员函数
    
    A a;
    A* pa = new A();
    (a.*p_pub_func)();                        //局部实例访问
    (pa->*p_pub_func)();                    //堆实例访问
    (A().*p_pub_func)();                    //临时实例访问
   
   具体参考 code-cpp 中 “指向成员函数的指针”
   
   
#10 ========================================================================================================

虚函数有两种形态:
1.有实现                (普通虚函数)
2.无实现(也可以有),同时需要指定为纯虚 =0        (纯虚函数)

(!!!)
父类如果有普通虚函数(非=0,且有自己的实现),那么它的潜台词是:所有继承我的子类,都可以选择重写这些普通虚函数。
父类如果有纯虚函数(=0),那么它的潜台词是:所有继承我的子类,都必须重写这些虚函数,注意,这里强调的是子类必须
                                         重写,而没有强调自己是否实现了。
                                         
    注:纯虚函数 和 普通虚函数 的区别仅仅是 告知子类 “是否必须重写” , 而没有告知子类 “父类是否有自己的实现”。
    
    因此。可以在父类中给纯虚函数以实现(但是,父类还是不能实例化,但凡包含纯虚函数的父类都不能实例化),子类
    可以通过 B::func() 来调用这些有实现的纯虚函数。

子类继承基类的虚函数也是虚函数,不需要手动指定virtual,默认是虚函数,当然,也可以加


    PS:虚函数与纯虚函数的区别就在于,是否要求子类必须实现自己,其他并无差异,附加影响就是有纯虚
        函数的类不能由程序员创建(可以在子类的初始化列表中创建)。

#11 ========================================================================================================

子类的成员函数中可以自由访问 基类的 public 和 protected 成员,仿佛这些成员就是自己的,如果是虚函数或者是
函数函数重载,那么如果期望调用不同的实现,则需要加上类名:: , 如果不存在重载问题,则直接使用即可。

    ps :成员函数都有一个隐藏参数,即第一个参数 this,用来指向当前函数的调用者,那么如果在
         成员函数里调用了另外一个虚成员函数,这其实相当于 this->vfunc() , 即调用调用者的虚函数
         ,那么这就存在 动态绑定(多态)问题了,因此这个行为是运行时才能决定的。如果我们想在
         编译时就决定,那么只需要使用class scope运算符 :: 即可。
         
         
#12 ========================================================================================================         

用 reference 而不是 pointer 作为函数入参的一个原因:
reference永远无法指向空对象,而pointer可能是空指针,因此,我们使用reference就不需要判断是否为空的问题


用 pointer 而不是 reference 作为函数入参的一个原因:
reference一旦指定就无法变更指向,当然,但部分情况下函数入参都不会变来变去,毕竟传入的参数都变来变去还
怎么运算


#12 ========================================================================================================    

纯虚类(抽象类) 不能被手动实例化,但是在其子类被实例化的时候,编译器会生成其对象。因此,如果抽象类有
成员变量,那么可以给抽象类定义一个有初始化列表的构造函数,然后在其子类的构造时调用即可。


#13 ========================================================================================================    

针对抽象类能否收动创建的一些思考:

抽象类,是指包含了纯虚函数的类(不管此函数是否有实现,只要被=0标注了就是),那么编译器就认为这种类是不完整的
类,因此不允许程序员自行开辟空间来创建实例。但是如果抽象类作为基类被继承了,根据子类构造的执行流程,基类
的构造函数会被调用,那么这就相当于编译器可以调用抽象类的构造函数(程序员不能创建实例),即可以创建抽象类
实例,只不过这个是存在于子类的空间中。

#14 ========================================================================================================    

RTTI   执行期的类型鉴定机制

使用 typeid 作用与 对象(注意,不是指针)上,可以获得一个 type_info 对象,这个对象中存放了当前实例的
所有信息,其中就包裹一个 存放了 类名的字符串,可以使用name()函数来读取这个字符串。

    比如  typeid(*this); 可以在成员函数中获取当前成员函数调用者的相关信息

#15 ========================================================================================================    

非虚函数 跟着指针走,因此如果想通过基类指针访问子类实例的非虚函数,就需要先把指针转型:

    Animal* pAnimal = new Dog();
    Dog* pdog = static_cast<Dog*>(panimal);

注:上面的转型是,把基类指针转换成子类指针,在继承图上来看是自上而下的(基类在图中处于上层),因此叫做向下转型。
    向下转型是危险的,编码者必须知道指针确实是指向一个子类实例,如果指向的是一个基类实例,那么这种转型将出错。
    
    向上转型,把一个子类指针转换成基类指针,这种转型始终是安全的,因为基类实例在内存层面始终处于子类实例的内部。
    
通常在进行向下转型之前,需要先通过typeid来判断下指针指向的实例到底是子类实例还是基类实例,如果是基类实例,那么
就表示其不能被基类指针指向,也就不能进行向下转型。

    ps:转型涉及两个要点,1)指针类型,2)指针指向的实例类型。而转型是指指针类型的转换,但是指针指向的实例类型
        直接决定了指针类型的转换是否安全合法。
        
        
#16 ========================================================================================================    
        
dynamic_cast

上面说了static_cast转型的问题。static_cast属于“强制”转换,不会进行任何判断,所以不安全。

dynamic_cast内部实现会先用typeid来判断是否可转换,如果可以转换则转换,否则返回0,同时转化动作也不会发生。

可以认为 dynamic_cast 是对 static_cast 的一种封装。


    (!)注:如果想使用dynamic_cast函数,则操作的实例必须有虚函数,因为dynamic_cast需要去虚函数表中
              搜集数据来完成自己的功能。
              dynamic_cast不能用作普通类型转换。
    
    
(!!!)    
#17 ========================================================================================================    
        
四种 cast

static_cast主要用于
1.基本类型的转换,比如int转char
2.类的上行转换,子类的指针或者引用转换为基类(安全)
3.类的下行转换,基类的指针或引用转换为子类(不安全,没有类型检查)

dynamic_cast用于类的指针或引用的转换
1.类的上行转换,子类的指针或引用转化为基类(安全)
2.类的下行转换,基类的指针或引用转化为子类
(因为有类型检查所以是安全的,但类型检查需要运行时类型信息,这个信息位于虚函数表中,所以必须要有虚函数)

const_cast 主要是用于改变类型的常量属性:常量指针转化为非常量指针;常量引用转化为非常量引用;常量对象转化为非常量对象

reinterpret_cast用于非关联类型的转换,操作结果是一个指针到其他指针的二进制拷贝,没有类型检查。

        
#18 ========================================================================================================
        
局部资源管理器

在使用一些包含资源分配和释放的代码时,可能在分配了内存后,释放内存前这中间的某个位置出现了异常,这就
会导致后面释放资源的代码不会执行。解决上述问题有3种方法:

1)通过try将原有语句包裹起来,然后在catch中再写一遍释放资源的代码:

        try{
        
            //分配资源
            //...dothing
            //释放资源
            
        }catch(...){
            //释放资源
        }

    这个写法的好处在于 “通俗明了” ,缺点在于代码量明显增加了不少,而且释放资源的代码还被写了两次
    
2)将流程封装成一个类,然后把分配资源 和 dothing 的动作放在构造函数里,把释放资源的动作放在析构函数里
   然后在使用时只创建临时变量,那么临时变量在离开作用域时会自动调用析构,这样即便是出现异常,亦可以保证
   资源的释放
   
        class A{
            A(){
                //分配资源
                //dothing
            }
            ~A(){
                //释放资源
            }
        }
        
        
        void func(){
        
            A a;        //局部变量,离开作用域会自动调用析构,而出现异常时,异常会一层层沿着调用栈传递,因此可以确保离开作用域
        
        }
   
   
   
3)使用智能指针,智能指针是模板,其接受任意地址作为构造入参,当智能指针离开作用域时,其指向的内存单元
    也会被释放,智能指针的实现和 2)一样,即在析构函数中释放自己构造时持有的内存地址
    
        void func(){
        
            auto_ptr<A> pa(new A());
        
        }


#19 ========================================================================================================

锁的类实现

Windows:

class Lock
{
public:
    Lock(const CRITICAL_SECTION& lock):m_lock(lock){
        EnterCriticalSection(&m_lock);
    }
    ~Lock(){
        LeaveCriticalSection(&m_lock);
    }

priavte:
    CRITICAL_SECTION m_lock;
}

Linux:

class Lock
{
public:
    Lock(const Mutex& lock):m_lock(lock){
        //mutex - m_lock acquire
    }
    ~Lock(){
        //mutex - m_lock release
    }

priavte:
    Mutex m_lock;
}

#20 ========================================================================================================

标准异常

下面说的异常都是被封装成了类,因此在catch的时候应该这样写:

    try{
    
        new A();
        
    }catch(bad_alloc exc){        //这里的bac_alloc是异常类名,exc是临时异常变量名
    
        //...
    
    }catch(bad_alloc){            //也可以只捕捉某个类型的异常,而不创建临时变量
    
        //...
    }

new 无法获得足够的内存   ---->   bad_alloc


我们可以指定nothrow来表示告知函数不应当抛出异常,具体使用时再行查阅


PS:我们可以自定义一个普通类,然后把它作为异常抛出,当然为了做区分,类名最好叫做xxx_exception。

   
   
   


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值