C++知识树整理——C++面向对象基础

C++ 面向对象基础

    • 类被创建时被调用的方法,可以用构造特定的初始化方法type(i)来初始化数据,比如传统赋值先初始化对象在赋值,这个方式可以初始化的时候就赋初值
    • 父类的构造函数会在子类构造前被调用,先创建父类再创建子类
    • 复制构造函数

      • 在内存中开辟出对应空间之后拷贝一份目标对象存入自身
    • 拷贝赋值函数

      • 先判断是否是自我赋值,是直接return(如果没有自我赋值检测,那么自身对象的data将被释放,data指针指向的内容将不存在,所以该拷贝会出问题。)
      • 如果不是先释放自身的数据后再赋值拷贝目标对象,同时返回引用保证支持连续赋值。
    • 析构函数

      • 在对象被销毁时调用,释放数据
      • 子类的析构会在父类析构之后才被调用
    • 由于编译系统在我们没有自己定义拷贝构造函数时,会在拷贝对象时调用默认拷贝构造函数,进行的是浅拷贝!即对指针name拷贝后只是简单的将指针指向目的地址,则会导致两个指针指向同一个地址,就上面说的类中有指针的时候必须要实现“三剑客”否则假设同一个地址的内存释放两次,第一次能够正常释放,第二次卸载的时候因为已经卸载了对象再次卸载程序就不可控了
    • 所以,在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数(上面的三剑客),使拷贝后的对象指针成员有自己的内存空间,就是深拷贝
    1. 原理:
      • 智能指针在语法上有三个很关键的地方,第一个是保存的外部指针,在内部声明一个指针如T* px,这个指针将代替传入指针进行相关传入指针的操作
      • 第二个是重载“*”运算符,解引用,返回一个指针所指向的对象
      • 第三个是重载“->”运算符,返回一个指针,对应于上图就是px
    2. 应用:
      • c++的迭代器就是一种智能指针,迭代器重载的“ * ”和“->”,"++"运算符,内部维护了一个 _list_node * node的指针
      • 迭代器的智能指针主要是对指针操作做重载,为什么要重载呢?因为原来的指针太“蠢”了,普通的++只是对原来的内存地址+1,指向的是内存的下一个地址,但是迭代器想要的是指向下一个元素的地址,假设是顺序数组就还能做到,但是如果是链表list就无法支持了,所以封装成了智能指针,在++的操作时其实是获取了当前节点的next指针的位置,将指针指向了next指针指向的元素位置,完成了智能的++
    • C++中, 一个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数), 承担了两个角色。 1 是个构造器 ,2 是个默认且隐含的类型转换操作符。所以, 有时候在我们写下如 AAA = XXX, 这样的代码, 且恰好XXX的类型正好是AAA单参数构造器的参数类型, 这时候编译器就自动调用这个构造器, 创建一个AAA的对象。 但在某些情况下, 却违背了我们的本意。 这时候就要在这个构造器前面加上explicit修饰, 指定这个构造器只能被明确的调用/使用, 不能作为类型转换操作符被隐含的使用。
    • 传值传递的是数据的值,接收端会用一个新的变量接收这个数据的值,等于拷贝了一份给函数使用
    • 而传址等于传递了这个数据的引用(指针),接收端接收了这个引用对这个引用的修改会直接修改传递端的对象本身
    • 如果一个对象中有虚函数就会再这个对象的内存中开辟一个4位的内存空间来存储一个虚指针
    • 虚指针指向一个虚表,这个虚表是一个指针数组,指针指向了在内存中的虚函数地址,指针下标取决于在声明时的函数位置
    • 子类继承父类也同时继承了父类的虚指针和虚表,但是里面所指向的虚函数可以通过子类的复写来修改指向的函数指向新复写的函数地址
    • 虚函数和虚表是面向对象很重要的一个部分,通过他实现了动态的对象和方法的指定,这便是面向对象继承多态的本质
    • 虚函数如果用C语言的代码解释则是:(*p->vptr[n])(p)
    • 在函数前声明代表这个函数体内不会对类中的数据进行操作
    • 在变量前声明代表接下来的操作不会改变这个变量的值
    • new
      • 分配内存
      • 转型(将malloc获得的对象转型为目标对象)
      • 调用构造函数
    • delete
      • 调用析构函数
      • 释放内存
    • 重载new和delete
      • new[] 释放时就必须 delete[] 否则只会释放一次(数组第一个)而其他的数据则会内存泄漏
      • new[]时重载的size比实际的对象大小*个数还要 + 4,因为需要一个int位来存储数组长度
      • new和delete底层其实就是调用了malloc(n)和free(ps)
      • 可以通过操作符重载重新定义new和delete,就可以在单纯的malloc之前自定义自己的操作,比如做缓存池
      • 在stl库中,base_string的实现就重载了new和delete的操作符重载,new string对象时其实并不是new了这个对象,而是内部new了一个Rep对象并封装了自定的对象头来方便对对象做特定的操作。
    • 重载new()和delete()
      • 可以重载多个版本的new(...),但是前期是每个版本都必须要有明确的参数序列,且其中第一个参数必须要是size_t
      • 也可以重载delete() ,写出的多个版本并不会被delete所调用,只有当new所调用的ctor(构造函数)抛出异常时才会调用这些重载的delete()
      • 即使new(...)和delete(...)并未能一一对应,也不会出现任何报错,程序会认为你放弃处理构造函数所抛出的异常。
    • 定义:

      • 总的来说虚析构函数是为了避免内存泄露,而且是当子类中会有指针成员变量时才会使用得到的。也就说虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的
    • 单一一个类对象加不加虚函数对于析构函数来说看不出区别,虚析构函数的作用体现在继承上,现在我们假设实现的类如下:

      #include<iostream>
      using namespace std;
       
      class ClxBase
      {
        public:
          ClxBase() {};
          virtual ~ClxBase() { cout<<"delete ClxBase"<<endl; };
       
          virtual void DoSomething() { cout << "Do something in class ClxBase!" << endl; };
       
      };
       
      class ClxDerived : public ClxBase
      {
        public:
          ClxDerived() {};
          ~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; };
       
          void DoSomething() { cout << "Do something in class ClxDerived!" << endl; };
       
      };
       
      int main(int argc, char const* argv[])
      {
         ClxBase *pTest = new ClxDerived;
         pTest->DoSomething();
         delete pTest;
        return 0;
      }
      
      • 当父类的析构函数加virtual关键字,输出的结果为:

        Do something in class ClxBase!
        Output from the destructor of class ClxDerived!
        delete ClxBase
        

        当父类的析构函数声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,先调动子类的析构函数,再调动父类的析构函数。

      • 当父类的析构函数不加virtual关键字,输出的结果为:

        Do something in class ClxBase!
        delete ClxBase
        

        当父类的析构函数不声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,只调动父类的析构函数,而不调动子类的析构函数。

      • 当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。

    • ++i是先进行+运算后在进行逻辑运算
    • i++是逻辑运算后才进行逻辑运算
    • 从操作符重载层面来看,前++和后++都是重载++这个运算符,所以怎么区分呢,c++就定义了,如果是operator++()则是前++,先进行++操作的复写,如果是operator++(int)则是复写后置++
    • 内存层面看
      • 引用是被编译器封装过的指针
      • 引用的大小和数据的大小一致(编译器封装),指针的大小默认为4
      • 引用的地址和数据地址一致(编译器封装)
    • 使用层面看
      • 引用可以看做是某个被引用变量的别名
      • 引用不能被提前声明比如 int& x; 必须要准确给出赋值并且赋值后不可再修改
      • 引用常用于参数的声明和返回类型的指定
  • 每个仿函数都是某个类重载“()”运算符,然后变成了“仿函数”,实质还是一个类,但看起来具有函数的属性。每个仿函数其实在背后都集成了一个奇怪的类,这个类不用程序员手动显式声明

    • 防止类与类之间重名的问题,也控制了方法和变量的作用域
    • 经验上讲,最好每个类都用对应的namespace括起来
    • this指针其实可以认为是指向当前对象内存地址的一个指针,如基类和子类中有虚函数,一个方法Func中调用了虚函数的Serialize()方法,则this->Serialize()将动态绑定,等价于(*(this->vptr)[n])(this)。可以结合上虚指针和虚函数表来理解。

参考资料:

posted @ 2021-01-11 17:46  陌冉  阅读(140)  评论(0编辑  收藏  举报