C++一些深入理解

1 虚函数

  • 虚继承:在菱形继承中,创立多个基类。

    • 还有的编译器是在对象中拥有多个vptr,分别指向不同的虚函数表,这样使用时就不会出错。
    • 因为虚继承比较复杂,而且是间接寻址,效率比较低,所以尽量少用。
  • 虚函数

    • 每一个拥有虚函数的类都会产生一个指向虚函数表的指针。一个单一继承类中只会有一个虚函数表,每个表会包含全部虚函数的地址。
    • 每个虚函数都会被指派一个固定的索引值,这个索引在整个继承体系中保持与特定的虚函数关联。比如:索引 0 为类类型信息,索引 1 为虚析构,索引 2 为纯虚函数…等。
    • 虚函数实现多态原理:当派生类继承基类时,在基类中利用virtual关键字定义若干个虚函数,会生成基类的虚表A,表中存放着所有虚函数的地址
    • 而在派生类中重写某个虚函数func1时,派生类的的虚表B中,会用重写的虚函数地址覆盖原来的函数func1的地址,其它虚函数地址不变。
    • 程序采用迟绑定的编译模式,当基类指针指向基类对象时,基类对象的vptr指向虚表A,就会运行基类的func1;而基类指针指向派生类对象时,派生类对象的vptr指向虚表B,就会运行派生类的func1。这样,根据基类指针指向不同的对象,进而调用不同的函数体,就会有不同的表现形式,即实现多态。
    • 静态函数、友元函数、构造函数、内联函数均不能声明为虚函数

3 关键字const、static、extern、volatile总结

3.1 关键字const

  • 常量修饰

    const int a = 10;
    int const a = 10;
    
  • 指针修饰

    int a = 10;
     
    //指针指向的内容不能改变
    const int* p = &a;			
    int const* p = &a;         
     
    //指针本身不能改变(指针不能指向其他内容)
    int* const p = &a;          
     
    //指针本身,指针指向的内容都不能改变
    const int* const p = &a;   
    int const* const p = &a;   
    
  • 引用修饰

    int a = 100;
    
    //不能通过引用修改a
    const int &a1 = a;
    int const &a2 = a;
    
  • 函数参数修饰

    //传递进来的参数无法改变
    void func (const int &n)
    {
         n = 10;        // 编译错误 
    }
    
  • 函数返回值修饰

    //若函数的返回值是指针,且用const修饰,则函数返回值指向的内容是常数,不可被修改。且此返回值仅能赋值给const修饰的相同类型的指针。
    //如果返回值是引用,不能通过引用修改原值
    
    //返回的指针所指向的内容不能修改
    const int* func()   
    {
     	//...
        return p;
    }
    
    //返回的引用无法改变
    const int &func()
    {
        //...
        return p;
    }
    
  • 类成员函数修饰

    //不可以修改该类的成员变量
    class A{
        void func() const;
    };
    
  • 修饰类对象

    //类对象只能调用该对象的const成员函数。
    class A {
        void func() const;
    };
    const A a;
    a.func();
    

3.2 关键字static

  • 修改变量的生命周期为全局:当static 作用于代码块内部的变量声明时,。从自动变量变为静态变量,但变量的属性和作用域不受影响。

    void fun()
    {
        static int i = 0;
        i++;
    }
    
  • 修改函数或变量的链接属性为文件内:当 static 作用于函数定义时或者用于代码块之外的变量声明时,修改的是其作用域。从外部链接属性(external)变为内部链接属性(internal),但标识符的存储类型和作用域不受影响。也就是说变量或者函数只能在当前源文件中访问,不能在其他源文件中访问。

    // a.h
    static int a = 10;
    static void fun();
    
    //b.cpp
    int main()
    {
        a = 100;	//错误
        fun();		//错误
        return 0;
    }
    
  • 修饰类成员变量为类域中的全局变量:对于类的每个对象来说,它是共有的。它在整个程序中只有一份拷贝,只在定义时分配一次内存,供该类所有的对象使用,其值可以通过每个对象来更新。

    class A
    {
    public:
        static int num;     //声明
    };
     
    int A::num = 10;    //定义
    int main()
    {
        A obj;
        std::cout << obj.num << std::endl;   //访问
        std::cout << A::num << std::endl;   //访问
        return 0;
    }
    
  • 修饰类成员函数分离与类的关系:静态成员函数也是属于类,而不属于任何一个类的实体对象,因此,静态成员函数不含有this指针。同时,它也不能访问类中其它的非静态数据成员和函数。(非静态成员函数可以访问静态数据数据成员和静态成员函数)

    class B
    {
    public:
        static void fun(){}
    };
    
    int main()
    {
        B obj;
        obj.fun();
        B::fun();
        
        return 0;
    }
    

3.3 关键字extern

  • extern关键字修饰的变量都是全局变量,表示该变量在别的文件中已有声明。

  • 若一个变量需要在同一个工程中不同文件里直接使用或修改,则需要将变量做extern声明。只需将该变量在其中一个文件中定义,然后在另外一个文件中使用extern声明便可使用,但两个变量类型需一致。

    //1.c
    int i; 
     
    //2.c或3.c
    extern int i;
    i = 100;		//实际使用的是1.c中的
    
    //1.h中声明
    extern int i; 
     
    //1.c文件中定义
    int i =0; 
     
    //其他要使用i变量的c源文件只需要include"1.h"就可以
    
  • 如果在函数首部的最左端冠以关键字extern,则表示此函数是外部函数,可以供其他文件调用。C语言规定,如果在定义函数时省略extern,则隐含为外部函数。在文件中要调用其他文件中的外部函数,则需要在文件中用extern声明该外部函数,然后就可以使用。

3.4 关键字volatile

  • 一个使用volatile关键字定义变量,其实就是告诉编译系统这变量可能会被意想不到地改变。那么编译时,编译器就不会自作主张的去假设这个变量的值,而进行代码的优化了。确切的说就是,编译器在编译代码时,优化器每次遇到这个变量,都会到内存中重新读取,而不会使用保存在寄存器里的备份来对代码进行优化。

  • 使用的情况

    • 在中断服务程序中修改的,供其它程序检测的变量,通常需要定义为volatile;
    • 在多任务环境下,各任务间共享的标志,通常也需要定义为volatile;
    • 存储器映射的硬件寄存器通常也需要定义为volatile,因为每次对它的读写都可能有不同意义;
  • 作用:在进行编译时不优化,在执行的时候不缓存, 每次都需要从内存中读出(保证内存的可见性)多用于多线程或多CPU编程。

    #include <stdio.h>
     
    int main()
    {
    	const int n = 10;
    	int *p = (int*)&n;
    	*p = 20;
    	printf("%d\n", n);	//输出10
    	return 0;
    }
    
    #include <stdio.h>
     
    int main()
    {
    	volatile const int n = 10;
    	int *p = (int*)&n;
    	*p = 20;
    	printf("%d\n", n);	//输出20
    	return 0;
    }
    

4 标准类型转换符

4.1 static_cast

  • static_cast < type-id > ( exdivssion )编译时使用类型信息执行转换

    该运算符把exdivssion转换为type-id类型,在转换执行必要的检测(诸如指针越界计算, 类型检查). 其操作数相对是安全的

    1. 类层次指针转换

      // 类层:A -> B
      A *a = new A();
      B *b = new B();
      B *bp = static_cast<B *>(a);	//基类转子类,无动态检查,不安全
      A *ap = static_cast<A *>(b);	//子类转基类,安全
      
    2. 基本数据类型转换,比如:enum->int、int->char...需要自己保证安全

    3. 把空指针转换成目标类型的空指针。

    4. 把任何类型的表达式转换成void类型。

4.2 dynamic_cast

  • dynamic_cast < type-id > ( exdivssion )运行时使用类型信息执行转换
    1. 该运算符把exdivssion转换成type-id类型的对象。type-id必须是类的指针、类的引用或者void *
    2. 如果type-id是类指针类型,那么exdivssion也必须是一个指针,如果type-id是一个引用,那么exdivssion也必须是一个引用。
    3. dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。
    4. 在转换时会动态检查类型,失败返回空。所以是安全的。

4.3 reinterpret_cast

  • reinterpret_cast< type-id >(exdivssion)重新解释给出的对象的比特模型,而没有进行二进制转换
    1. type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。
    2. 它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,再把该整数转换成原类型的指针,还可以得到原先的指针值)。

4.4 const_cast

  • const_cast< type-id > (exdivssion)修改类型的const或volatile属性,type_id和exdivssion的类型是一样的。

    • 常量指针被转化成非常量指针,并且仍然指向原来的对象。

      int a = 100;
      const int * a1 = &a;
      int *b = const_cast<int *>(a1);
      *b = 200;
      
    • 常量引用被转换成非常量引用,并且仍然指向原来的对象。

      int a = 100;
      const int & a1 = a;
      int &b = const_cast<int &>(a1);
      b = 200;
      

2 深拷贝与浅拷贝的区别

  • 深拷贝(deepCopy):是增加了一个指针并且申请了一个新的内存,把对象复制了一份。
  • 浅拷贝(shallowCopy):只是增加了一个指针指向已存在的内存地址。

5 重载与重写

  • 重载:在同一作用域(模块/类)内,具有相同函数名(运算符名),但参数的个数、类型或返回值类型不同。重载明显是一种静态多态,在编译期间就被编译器确定。编译器会为每一个函数生成一个唯一函数标识。
  • 重写(覆盖):在父类和子类中,各有一个函数名,参数信息都相同的函数。
    • 静态重写:编译时根据指针的类型(父类或子类)调用相应的函数。
    • 动态重写:父类的函数前加上virtual修饰关键字,子类重写此函数,运行时会根据实例化类型调用相应函数。

6 new和delete

6.1 new、delete、malloc、free

  • new和malloc都是申请内存,但是new会调用类的构造函数;delete和free都是释放内存,但是delete会调用析构函数。

6.2 delete与 delete []区别

  • delete只会调用一次析构函数,而delete[]会调用每一个成员的析构函数。在More Effective C++中有更为详细的解释:“当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用operator delete来释放内存。”delete与new配套,delete []与new []配套。

7 面向对象

  • 封装:隐藏代码实现的细节。
  • 继承:拓展已存在的模块。
  • 多态:一个接口多种实现。

8 虚函数实现

8.1 介绍

虚函数主要是为了实现多态机制。

虚函数的实现主要由两部分组成:虚函数指针与虚函数表。

虚函数指针:每一个有虚函数的类至少有一个指针,指向自己所使用的虚函数表。这个函数指针是由编译器自动添加的,我们一般称为*__vptr。在实例化此类时,此指针会自动被创建,并指向例化类类型的函数。

虚函数表:每一种有虚函数的类类至少有一个虚函数表,虚函数表属于类,而不属于某个具体的对象,一个类只需要一个虚表,同一个类的所有对象都使用同一个虚表。对于基类与派生类,基类有基类的虚函数表,派生类有派生类的虚函数表

8.2 基本原理

当单继承时,派生类只会存在一个虚函数指针和虚函数表。

当多继承时,派生类会存在多个虚函数指针和虚函数表。C类的虚函数和基类A共用一个虚函数表,一般称A为C的主基类。

8.3 纯虚和虚析构函数

纯虚函数:基类中只声明不定义的虚函数,提供多态实现的接口。(在很多情况下,基类本身生成对象是不合情理的,这时就要在基类中声明一个纯虚函数,将基类变为一个抽象类,就无法生成基类对象)

  • 含有纯虚函数的基类是纯虚基类,不能生成对象。
  • 派生类中必须对基类中声明的纯虚函数进行定义,否则无法编译。

虚析构函数:在继承关系类在释放资源时,避免只时调用了基类的析构,而导致了派生类的析构没有被调用,从而造成资源泄漏的影响。

posted @   09w09  阅读(30)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
点击右上角即可分享
微信分享提示