Cpp记录10.1k

更新于 2015/7/14 21:14:21

再记录 operator= 重载

  • 因为在类的设计中 重载 = 可能是在所难免的,如果你的类设计比较复杂。
  • 之所以需要重载它是为了当类中包含了复杂的关系的时候,提供了一个人为的保证,最典型的莫过于对象的拷贝:
    1. 假设 类中有一个成员是指针,指向一个对象,这个对象可以是数组,也可以是其他类或者容器。
    2. 当我们执行赋值操作符的时候,假定这里规范的使用赋值运算符,让其一定能调用赋值拷贝构造函数 = ,此时对于这个指针成员,就有两种情形: 拷贝指针 或者 拷贝对象
  • 拷贝指针,即便不重载运算符 = 编译器也会帮你自动实现,这在国内大概就叫做 浅拷贝 ,如果仅仅拷贝指针,这就引发了一个问题,多个对象共享一个实际内存,当执行析构函数的时候,会造成多次析构同一个内存块。
  • 拷贝对象,这是普遍的解决方法,在拷贝的同时,将指针所指向的对象同样做一份拷贝,似乎也不错,但是如果对象太大,似乎并不是什么很喜人的事 情,但也不失为一种好的解决方案
  • 当使用拷贝对象这个方案的时候,发现内存使用超过自己所能承受的范围,我们就应该重新考虑一下拷贝指针这个方法,即我们勀使用一个 计数器,用来记录有多少个指针指向这个共享的内存块(即对象),这很像 C++11 中智能指针 shared_ptr 的做法,实际上的确可以使用 它来作为解决方法,当然也可以自己实现。
  • 实现的方法就是增加一个计数成员(不是在本类中,而是在指针指向的对象的类中,当然容器和数组另找方法实现,可以考虑用静态成员),用来记录是否还有指针指向这个内存,对象每次析构的时候,减少计数器。如果还有指针指向它那么在析构函数执行时就不释放那块内存。
  • 总之,总是重载赋值拷贝构造函数 = 总是一个好的习惯。

自动类型转换

  • 对于内建类型,类型转换自然由语言来定义,而自己的类,自然也是可以有的

    1. 加上一个特殊的构造函数即可:

      ClDefByMe(const otherClass&);
      

      只需要如此定义了构造函数,我们就能实现自动类型转换,但是。

    • 往往自动类型转换不是什么好事,因为我们如果定义了这么一个构造函数,当我们传递otherClass类型的对象给某个需要 ClDefByMe作为参数的函数的时候,编译器甚至不会警告你,一个更好的办法就是告诉编译器,别偷偷的做自动类型转换,即使我的确有类型转换的意思:

      explicit ClDefByMe(const otherClass&);
      

      只要加了关键字 explicit 我们就只能通过显式的类型转换才能成功通过编译

      void testFunc(const ClDefByMe & other)
      {
      //..Do Something
      }
      otherClass tmp1 = new otherClass;
      //调用
      testFunc(tmp1);// 添加关键字 explicit 后报错,需要显式的转换
      testFunc(ClDefByMe(tmp)); //编译通过
      

      同样的理由,尽量给自己的构造函数加上关键字 explicit 防止发生自己不知道的默认类型转换,或者隐式调用构造函数。

    • 隐式调用构造函数的情况是,当某个构造函数只有一个参数(有多个参数,但只有一个参数无默认值,其他都有默认值的也属于这个范围),那么也会达成触发隐式调用构造函数的条件。
    • 例如ClDefByMe有一个构造函数,需要一个int类型的参数,那么:

      //testFunc(1);  //是合法的
      //testFunc(1.0) //是合法的
      testFunc(ClDefByMe(1));   //合法
      testFunc(ClDefByMe(1.0)); //合法
      
    1. 在类中重载一个特殊的运算符:

      class ClDefByMe{
      ...
          operator otherClass()const {
              return otherClass();//某个otherClass的构造函数
          }
      
    2. 然而并不是自动类型转换一无是处,它能够让类很好的适应由C语言带过来的库函数,最大的体现便是,C形式的字符串 const char*。只要我们在类中提过了如实现2这种形式的运算符重载,就能让自己的类在C语言的库函数中畅通无阻。

重载 new 和 delete

  • 全局重载 和 类内重载
  • 全局重载也就是让每个地方用到的 new/delete 都是你重载过的效果
  • 类内重载就像重载运算符一样,只对特别的类有效
  • 可以分为两种:

    void* operator new(site_t sizes);
    void  operator delete(void* dele_p);
    
    • 在重载的实现中,如果想分配的内存要在堆上,那么就使用 C语言 提供的内存管理机制malloc, calloc, realloc, free,并且如果这是重载全局的 new/delete,就不能使用 istream 之类的流对象进行输入输出,因为这些流会调用原来的 new/delete 去分配内存空间创建对戏那个,但是实际上此时已经不存在原来的 new/delete 了。
    • 如果重载发生在了某个特定的类中,那么依旧可以调用全局的 new/delete 帮自己完成重载的工作,调用方法便是通过,域解析作用符 ::
    • 在重载的实现中,如果想分配的内存要在栈上,那么就需要在类设计的时候,预先定义好一个 内存池,这个 内存池 的大小是固定的,无法改变,但是我们可以使用它来实现一个内存分配的效果,即将这个内存池看作一个系统内存,而我们重载的 new/delete 负责从里面分配和归还内存,这能达到一个目的,可以精确操控内存,让任意一个对象落在我们想要让他们出现的内存里。
  • 实现的时候,若是类内重载,则可以使用全局的 new/delete,进行辅助设计:

    void* operator new(site_t sizes)
    {
        return ::new char[sizes];
    }
    

    其中 sizes 是经过编译器计算得到的,并不需类设计这操心,在实现完之后,只需要正常使用即可

  • 对于new/delete的重载,我们仅仅只能改变内存分配的位置而已。
  • new所分配的内存会比原先所要求的内存要大一些,多出来的内存是用来存储一些有用的信息,即本次分配了多少个内存,(每个内存是对象的大小),以便delete的操作。
  • 相应的,我们也可以传递别的参数给new,以此来调用不同的构造函数来初始化:

    void operator new(size_t sizes, void* arg1)
    {
        return arg1; ...
    //..调用
    int arr_[11] = {0};
    //假设有一个构造函数,将唯一的成员初始化为括号内传递的参数的值
    ClDefByMe* tmp = new(arr_) ClDefByMe(22);//在这个数组里创建对象。
                                             //每个对象大小为 int
    //销毁的时候无法使用delete否则会造成程序错误,只能显式调用析构函数
    //这相当于另一种内存池的实现。
    

    使用的方式:

    //下一个对象可以是
    ClDefByMe* tmp2 = new(arr_+1) ClDefByMe(33); //此处+1 代表下一个对象的位置
    

    但是因为没有内存检测,所以需要很小心的使用,不能让其越界,因为C++C 语言一样,是不提供越界检查的。

    //此时的内存示意|22|33|0|0|0|0|0|0|0|0|0| <<代表arr_的内存示意图
    ClDefByMe* tmp3 = new(arr_+3) ClDefByMe(44);
    //此时的内存示意|22|33|0|44|0|0|0|0|0|0|0|
    tmp3->~ClDefByMe(); //只能显式调用析构函数,而不能使用 delete
                         //调用完析构函数,发现内存中的值依旧存在,但实际上已经
                        //被“销毁”了,涉及到计算机的删除的实现。
    

类设计中的关键字 public, private, protected

  • 在类声明定义中,可以设定外部对象对本身成员的访问权限
    • public: 代表任何权限的对象都能访问
    • private: 代表只有该类设计者本身和友元(类/函数)能够访问,继承类也无法直接访问
    • protected: 代表只有 类设计者本身 和 友元 还有 继承类 能访问
  • 在类继承中

    • public 继承 代表将基类的公有,私有,保护关系,原封不动的继承
    • 不写或者 private 继承 代表 所有成员都作为继承类的私有成员。

      • 当所有继承来的成员都成为是 pirvate 时,可以使用 using 关键字使得其权限改变。例如:

        class Deri : ClDefByMe{
        public:
            using ClDefByMe::counts; //使得counts由private变为public
            using ClDefByMe::address; //使得接口函数address()成为public
        ...
        

继承中的重定义

  • 在继承的时候,继承类对于基类的成员都照搬继承,但是当基类中的成员函数,在继承类中被重写,被改变返回值或者参数列表,那么基类所继承过来的同名函数将被 隐藏

    class ClDefByMe{
        int counts;
    public:
        //...
        void output() { 
            cout << "There is ClDefByMe -- " << counts << endl;
        }
        void output(int args){
            cout << "There is ClDefByMe -- " << args << "of" << counts << endl;
        }
    //...
    };
    class Deri : public ClDefByMe{
    public:
        void output(){
            cout << "There is Deri --" << endl;
        }
    //...
    };
    Deri tmp;
    

    使用 tmp.output(); 调用的是 Deri 中的 output(),当选择编译 tmp.output(11); 时,编译器会提示你

    no matching function for call to ‘Deri::output(int)’

    这便是继承类隐藏了基类的实现,如果要调用基类的 output函数,只能显式调用,依旧是采用域解析操作符 :: 实现: tmp.ClDefByMe::output();

  • 如果在继承类中出现了和基类一致的函数名,无论修改其返回值或者参数列表与否,都会 将基类中的同名函数隐藏,如果没有出现,那么继承类可以正常的调用基类的接口函数。

继承的函数

  • 并不是所有公有函数都会被继承

    • 构造函数,并不会被继承,虽然看起来像是继承(即基类中可以调用)
    • 重载或者自动生成的 operator= 不会被继承,因为它表现的十分类似一个构造函数干的事情
  • 当我们试图去创建一个对象的时候,构造函数会按照既定的顺序(基类到继承类)依次调用

  • 其中 拷贝构造函数在被调用的时候,会默认去调用其上一级基类的 默认构造函数 而非 上一级基类的 拷贝构造函数,如果要调用上一级的 拷贝构造函数, 就需要显式地在 初始化列表 中调用,否则可能会导致拷贝失败。
    • 出现 拷贝构造函数 调用上一级基类的 默认构造函数 的情况,只有当类设计者自行编写 拷贝构造函数 且未显式调用上一级基类的 拷贝构造函数 才会发生
    • 故设计类时:
      1. 如果决定让 拷贝构造函数 由编译器自动生成,那么就证明新类(即当前类)中没有 复杂类型 成员(或复杂类型够健全),那么编译器自动生成的拷贝构造函数会自动调用基类的拷贝构造函数,以及类成员的拷贝构造函数。
      2. 如果决定自己实现 拷贝构造函数 那么久需要记住,一定要显式的调用基类的拷贝构造函数,否则将会出错(编译器会默认调用基类的默认构造函数)
  • operator= 也是如此。

虚函数 和 多态

  • virtual关键字修饰的函数(虚函数)的重定义叫做 重写, 在 C++11 中可以使用 override 来防止不小心写成重载

    class ClDefByMe{
    //...
        virtual void output() const{ 
            cout << "counts = " << counts << endl;
        }
    //...
    class Deri : public ClDefByMe{
    //...
        void output() const override{ //如果不小心写成重载,编译器就会提醒你。
            cout << "counts of Deri = " << counts << endl;
        }
    

    无论是传递参数的形式,或者数组(容器也一样)的存储,只要是 指针/引用 的形式,都能触发多态效果。

  • 虚函数的一个最关键的性质就是 延后绑定 ,用来实现 多态
  • 对于虚函数而言,在类设计的继承体系中,要么不用,要么就到处都用
  • 绑定(提前绑定 和 延后绑定)

    1. 提前绑定 这是 C 与 C++ 共有的特性
    2. 延后绑定,当运行时(runtime)才进行绑定,只对 引用 以及 指针 有效,也就是说对实际的对象是不起作用的。绑定的内容就是决定调用继承体系中某个类的某个函数。
    3. 如果使用的是 完整的类对象 调用虚函数,编译器会将其自动处理(可能)为 提前绑定(因为即使此时时候延后绑定也是同样的结果,只是耗时而已,但更简单),因为这时候所有的信息都是完整的,足以判断调用哪个版本的函数。而指针和引用则不同,某个类型的 指针/引用 也可以指向其继承类的对象,所以需要延后绑定来判断。
  • 虚函数是如何被调用的,实现的机制就是指针

    • 首先一个类中有至少一个的虚函数。
    • 编译器隐式地创建了一张表,整个继承体系的每个类都拥有一张这种表,称之为虚函数表(VTABLE),表中存放着类中的所有虚函数
    • 在每个类(继承类和基类)中,自行添加一个成员 vpointer(简写为VPTR),类型是指针的指针,指向这张虚函数表(虚函数表中保存着函数的地址)。
    • 每次通过父类的指针或引用调用虚函数的时候,编译器就会迅速地通过VTPR去虚函数表中查找正确的函数地址,并调用它。
    • 无论有多少虚函数,都只有每个类都只有一个 VPTR,原因很明显。
  • 关系呈现

     ___________           ___________________________
    | ClDefByMe |      ---->|  ClDefByMe::output      |
    |______vptr_| ____|    |______其他虚函数__________|
    _____________          ___________________________
    | Deri      |     ---->|     Deri::output        |    
    |______vptr_| ____|    |______其他虚函数__________|
    
    • 虚函数表中的函数指针,顺序是一定的,由编译器决定如何排序。可以保证的是所有表的顺序是一致的,和类设计这按什么顺序重写无关。
    • 也就是说,表中相同位置的函数指向的是同名函数,如果没有在继承类中重写某个虚函数,则继承类会延用基类的版本。这些工作都在一个类对象创建的时候完成,也就是构造函数的工作(初始化VPTR)。
  • 纯虚函数 与 抽象基类

    • 抽象就是将所有相关物体具有的共性提取出来,抽象程度越高代表的特性也就越多,也越难以理解,C++中抽象基类无法创建实际对象,只是一个承载接口的平台,用以被继承。
    • 纯虚函数是抽象基类的特点,其可以只有声明而,没有定义。但是抽象基类的继承类必须重写该虚函数(这个只对最近一级的继承类有效)。

      class ClDefByMe{
      //..
          virtual void output() const = 0; //纯虚函数,无函数体
      //...
      class Deri : public ClDefByMe{
      //..
          void output() const override{
          //Do something
      }
      
    • 这也是为什么抽象基类无法创建对象的原因,由于抽象基类含有纯虚函数,而编译器依然会为其生成一张VTABLE表,一个VPTR指针,但是仅仅只是预留了位置给出纯虚函数,表中并没有有用的实际信息,而一个包含不完整信息的类是无法创建对象的,所以只要一个类拥有了纯虚函数,其就无法创建实际对象。
    • 这也能及时发现是否误写为按值传递,导致动态绑定没有成功,因为抽象基类没有办法创建对象,也就无法按值传递了。
  • 由于在继承类中重定义(重写)一个在基类中存在重载的函数,会隐藏所有的重载版本,对于虚函数来说,其无法改变返回值和参数:

    • 返回值,在不改变参数的条件下,改变返回值,会无法通过编译,因为如果在继承类中重定义了一个在基类中重载的函数,那么基类的所有重载函数都将在继承类中被隐藏,导致如果基类的这个 重写的函数 如果不与继承类的虚函数一致的话,将会导致 多态失效 ,这将会导致某些后续的问题,所以编译器阻止了修改返回值的行为。
    • 参数,如果修改了参数,就相当于重载了这个函数,那么将隐藏基类继承来的虚函数及其重载函数,此时如果将其上切(upcasting),可以恢复被隐藏的函数。
    • 当虚函数的返回值是 继承体系中类的指针或者引用 的时候,在继承类中可以修改返回值的类型,类型可以是继承体系中的任意的类,只要在继承之中即可。

      class ClDefByMe{
      //..
          virtual ClDefByMe* output(){
              return this;
          }
      //...
      class Deri : public ClDefByMe{
      //..
          Deri* output() override{
              return this;
          }
      

      编译通过,由于添加了 override 关键字,我们可以确保这的确是虚函数的重写

  • 在构造函数里调用虚函数,那就是调用本类(本地)的虚函数版本,并不涉及到延后绑定机制
  • 在析构函数里也是如此
  • 成为虚函数的析构函数

    • 让析构函数成为虚函数的目的就是为了防止在程序员动态分配内存(heap上),并且销毁对象时可能出现的错误
    • 这种错误出现在,当你对析构函数使用多态这种机制的时候:

      • 在创建一个对象,起先并不知道其类型,用继承体系中的基类指针指向(即使用new创建),在之后的某个情形下,使用 delete 销毁,如果此时析构函数非虚函数,则会导致内存泄漏的Bug,因为此时 delete 只会调用当前类型版本的析构函数,对于其基类的析构函数并不会调用.

        ________________________
        |        Deri2         |
        |    _______________   |
        |    |    Deri1    |   |
        |    |    ______   |   |
        |    |    |Base|   |   |
        |    |    |____|   |   |
        |    |_____________|   |
        |______________________|
        

        其中如果让析构函数成为虚函数,则 delete 时会层层调用各个类的析构函数,如果为普通成员函数,则只会调用某一层的析构函数。

  • 纯虚函数的 析构函数

    • 它要有一个函数体,即所谓的定义。可以显式的定义它为 内联。因为要给它函数体,所以必定要在类外定义,这就无法成为内联函数,所以需要显式指定

      class ClDefByMe{
      //..
          virtual ~ClDefByMe() = 0;
      ...
      inline ClDefByMe::~ClDefByMe() {} //即使没做任何事
      
    • 对于析构函数而言,当它是纯虚函数 时,其继承类并没有被强制要求 需要重写这个函数,为了达到这个目的,所以我们给了基类的纯虚函数 一个函数体,这样编译器就能自动为我们生成继承类的析构函数版本。

C++ 显式类型转换

  • 一共四个转换:
    1. static_cast
    2. const_cast
    3. reinterpret_cast
    4. dynamic_cast
  • 之所以需要他们,是因为由 C语言继承过来的强制类型转换含有许多漏洞,它无法给予我们任何信息,只是简单的执行了类型转换。
  1. 用于普通的类型转换,即强制类型转换能做的,它也能做,区别于是否在必要的时候提供一个警告信息:

    int i = 0
    float j = 0;
    long k = 0;
    i = k;
    i = static_cast<int>(k);
    j = static_cast<int>(k);
    
  2. 用来去除 const/volatile 属性

    const int i = 0;
    int * j = const_cast<int*>(&i);
    

转载注明: http://www.wushxin.top/2015/07/09/Cpp%E8%AE%B0%E5%BD%9510.1k.html