C++学习笔记



  • C++学习

    本文中记录了一些个人在学习C++过程中的一些疑惑和对这些疑惑的探究过程。在此作一个总结,由于个人能力不足,可能在其中可能会存在一些问题,欢迎交流。

    C++中的引用

    ​ 引用是C++新增的一项重要机制,它使得C++中本生需要指针实现的功能有了另一种实现方法。但引用的本质是什么呢?它跟指针之间有什么联系呢?

    下面给出一段代码:

    int main()
    {
    	int a = 1;
    	int& p1 = a;
    	int* const p2 = &a;
    	
    	int b1 = p1;
    	int b2 = *p2;
    
    	p1 = 8;
    	*p2 = 10;
    }
    

    将上面这段代码在VS2019Debug模式下进行反汇编,得到如下(仅截取部分相关代码):

         3: 	int a = 1;
    00FD17F2 C7 45 F4 01 00 00 00 mov         dword ptr [a],1       ;将1存到a所在的空间
         4: 	int& p1 = a;
    00FD17F9 8D 45 F4             lea         eax,[a]               ;将a的地址存进eax
    00FD17FC 89 45 E8             mov         dword ptr [p1],eax    ;将eax的值(即a的地址)存入p1
         5: 	int* const p2 = &a;
    00FD17FF 8D 45 F4             lea         eax,[a]               ;将a的地址存进eax
    00FD1802 89 45 DC             mov         dword ptr [p2],eax    ;将eax的值(即a的地址)存入p2
         6: 	
         ;读取
         7: 	int b1 = p1;
    00FD1805 8B 45 E8             mov         eax,dword ptr [p1]    ;将p1的值(a的地址)放入eax
    00FD1808 8B 08                mov         ecx,dword ptr [eax]   ;将eax的值,即a的值放入ecx
    00FD180A 89 4D D0             mov         dword ptr [b1],ecx    ;将ecx的值存到b1
         8: 	int b2 = *p2;
    00FD180D 8B 45 DC             mov         eax,dword ptr [p2]    ;将p1的值(a的地址)放入eax
    00FD1810 8B 08                mov         ecx,dword ptr [eax]   ;将eax的值,即a的值放入ecx
    00FD1812 89 4D C4             mov         dword ptr [b2],ecx    ;将ecx的值存到b2
         9: 
         ;修改
        10: 	p1 = 8;
    00FD1815 8B 45 E8             mov         eax,dword ptr [p1]    ;将p1的值(a的地址)放入eax
    00FD1818 C7 00 08 00 00 00    mov         dword ptr [eax],8     ;修改eax值对应的地址处的数据为8
        11: 	*p2 = 10;
    00FD181E 8B 45 DC             mov         eax,dword ptr [p2]    ;将p2的值(a的地址)放入eax
    00FD1821 C7 00 0A 00 00 00    mov         dword ptr [eax],0Ah   ;修改eax值对应的地址处的数据为10
    

    注:

    lea指令和mov指令的区别:

    lea eax,[ebx+8]   ;该语句表示把ebx的地址加8存到eax,(存进去的可以理解为还是一个地址)
    mov eax,[ebx+8]	  ;该语句表示把ebx+8后对应地址的值存进eax
    

    由上可以看出,在底层实现中,引用被当作指针来进行处理,读取和修改的操作均与指针无差异。

    但由于引用不能重新指向另一个变量,所以在理解上可以将其当作指针常量

    函数的重载

    C++与C的一个重要区别就在于,C++可以根据参数的不同来进行重载,但C++是如何实现重载的呢?

    函数重载的汇编实现

    下面给出一段代码:

    int Fun(int i)
    {
    	i = i + 1;
    	return i;
    }
    
    double Fun(double i)
    {
    	i = i + 1;
    	return i;
    }
    
    int main()
    {
    	int a = 1;
    	double b = 1.5;
    
    	int a2 = Fun(a);
    	double b2 = Fun(b);
    }
    

    将上面这段代码在VS2019下进行反汇编,得到结果如下:

       15: 	int a = 1;
    00FE1808 C7 45 F8 01 00 00 00 mov         dword ptr [a],1  
        16: 	double b = 1.5;
    00FE180F F2 0F 10 05 D8 7B FE 00 movsd       xmm0,mmword ptr [__real@3ff8000000000000 (0FE7BD8h)]  
    00FE1817 F2 0F 11 45 E8       movsd       mmword ptr [b],xmm0  
        17: 
        18: 	int a2 = Fun(a);
    00FE181C 8B 45 F8             mov         eax,dword ptr [a]  
    00FE181F 50                   push        eax  
    00FE1820 E8 A1 FA FF FF       call        Fun (0FE12C6h)                 ;call Fun函数,地址为(0FE12C6h)
    00FE1825 83 C4 04             add         esp,4  
    00FE1828 89 45 DC             mov         dword ptr [a2],eax  
        19: 	double b2 = Fun(b);
    00FE182B 83 EC 08             sub         esp,8  
    00FE182E F2 0F 10 45 E8       movsd       xmm0,mmword ptr [b]  
    00FE1833 F2 0F 11 04 24       movsd       mmword ptr [esp],xmm0  
    00FE1838 E8 0B FB FF FF       call        Fun (0FE1348h)                 ;call Fun函数 ,地址为(0FE1348h)
    00FE183D 83 C4 08             add         esp,8  
    00FE1840 DD 5D CC             fstp        qword ptr [b2]  
    

    可以看出,这两个函数所指向的地址已然不同,但在这之中并不明白它是如何实现的。

    在此我们使用gcc -O0 -S 指令,对该cpp文件进行编译,得到最终编译结果如下:

            .file   "test1.cpp"
            .text
            .globl  _Z3Funi
            .type   _Z3Funi, @function
    _Z3Funi:
    .LFB0:
            .cfi_startproc
            pushq   %rbp
            .cfi_def_cfa_offset 16
            .cfi_offset 6, -16
            movq    %rsp, %rbp
            .cfi_def_cfa_register 6
            movl    %edi, -4(%rbp)
            addl    $1, -4(%rbp)
            movl    -4(%rbp), %eax
            popq    %rbp
            .cfi_def_cfa 7, 8
            ret
            .cfi_endproc
    .LFE0:
            .size   _Z3Funi, .-_Z3Funi
            .globl  _Z3Fund
            .type   _Z3Fund, @function
    _Z3Fund:
    .LFB1:
            .cfi_startproc
            pushq   %rbp
            .cfi_def_cfa_offset 16
            .cfi_offset 6, -16
            movq    %rsp, %rbp
            .cfi_def_cfa_register 6
            movsd   %xmm0, -8(%rbp)
            movsd   -8(%rbp), %xmm1
            movsd   .LC0(%rip), %xmm0
            addsd   %xmm1, %xmm0
            movsd   %xmm0, -8(%rbp)
            movsd   -8(%rbp), %xmm0
            popq    %rbp
            .cfi_def_cfa 7, 8
            ret
            .cfi_endproc
    .LFE1:
            .size   _Z3Fund, .-_Z3Fund
            .section        .rodata
            .align 8
    .LC0:
            .long   0
            .long   1072693248
            .ident  "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
            .section        .note.GNU-stack,"",@progbits
    
    

    从上述代码可以看出,代码中的Fun函数,在汇编中被重命名为了_Z3Funi 和 _Z3Fund。

    不难发现,在gcc中,是通过将函数根据参数重命名来实现函数重载,i表示int,d表示double。如果是自定义的结构体,从测试结果来看,gcc是使用全称来进行重命名。(重命名的方式可能随着编译器的不同有一些不同,但机制应该是相同的)

    cpp中调用c中函数的问题

    此时不免会引出一个问题,那么 .cpp 文件中如何调用 .c 文件中的函数呢?C++中编译时对于函数的命名方式不同,可想而知对于函数的调用方式来说也会不同,但C语言中对于函数的编译并无相应的处理,这之间必然产生冲突。

    //test2.h
    #pragma once
    
    int Fun(int i);
    
    //test2.c
    #include "test2.h"
    
    int Fun(int i)
    {
    	i = i + 1;
    	return i;
    }
    
    
    //test.cpp
    int main()
    {
    	int a = Fun(10);
        return 0;
    }
    

    将以上代码编译调试运行会导致报错一个无法解析的外部符号:

    image-20200707011549359

    可以看出,这种冲突确实存在。

    对此,C++引入了 extern "C" 用于解决这类型的冲突。

    对上方代码的头文件进行修改,即可使得对应的函数按照C函数的方式编译。

    //test2.h
    #pragma once
    
    #ifdef __cplusplus
    extern "C" {
    #endif
    	int Fun(int i);
    
    #ifdef __cplusplus
    }
    #endif
    
    //test2.c
    #include "test2.h"
    
    int Fun(int i)
    {
    	i = i + 1;
    	return i;
    }
    
    
    //test.cpp
    int main()
    {
    	int a = Fun(10);
        return 0;
    }
    

    对于该头文件,添加对应的extern “C” { ……},将需要按C语言编译方式的函数框选进去,即可实现.cpp 调用 .c 中的函数。

    对象内存模型

    C++面向对象的三大特征:封装、继承、多态。在个人看来多态可以说是最难但也同时是最能体现其优势的特征了。

    提及多态,常会说:子类指针可以直接赋给父类类型的指针。也就是说,向上的类型转换(子类转换成父类)是类型安全的。

    但同时,C++中存在一种private 关键词,它可以定义类的私有属性,这些属性是不可被子类继承的,那么为什么C++可以保障其向上的类型转换是类型安全的呢?

    简单类继承中的类型安全

    image-20200707013653634

    如上图,如果是非private属性,可以很轻松的理解,子类指针指向的空间只有可能比父类大,而不存在比父类小的可能,而且对应的属性位置也同样相同,所以对于指针的操作来说也同样可行。

    但此时如果父类中存在private属性呢(实际上大多数属性都是设置为private属性),为什么还是可以保证类型安全呢?

    image-20200707015421765

    使用如下代码进行测试:

    #include <iostream>
    
    class Parent
    {
    public:
    	char* name;
    	int age;
    private:
    	int weight;
    };
    
    class Child :public Parent
    {
    private:
    	int some_field;
    };
    
    
    using namespace std;
    
    int main()
    {
    	cout << sizeof(Parent) << endl;
    	cout << sizeof(Child) << endl;
    }
    

    该代码中,子类继承了父类的public属性,没有继承其private属性,同时自己增加了一个新的属性,故应当有三个属性,大小均为4字节,最后输出为12字节。

    最后的输出结果:

    image-20200707014435430

    与最初设想的不同,子类的大小为16字节而不是12字节。

    故此,可以猜想到实际上该属性被继承了下来,仅仅是编译器拒绝了你对其的访问。

    实际上,在vs监视中可以看到:image-20200707014832237

    ​ 可以看出,实际上子类中维护一个父类的对象。这样也更好的理解了为什么在调用子类构造函数时,需要先调用父类的构造函数——因为实际上子类中就维护了一个父类对象。同样也可以理解,为什么在子类析构时,先进行子类的析构函数,再进行父类的析构函数,因为需要先析构自己的属性,再析构父类的对象,在析构父类对象时,必然需要调用父类的析构函数。

    多继承中的类型安全

    ​ 由上面,我们可以看出,在单继承中,子类先在内存中维护了一个父类对象,然后再开辟自己新增属性的空间,将指向子类型的指针,转换成指向父类型完全是可以实现的而且不会有任何访问越界问题。

    ​ 但如果是多继承呢?

    看下面这段代码:

    #include <iostream>
    
    class A
    {
    public:
    	int a;
    };
    
    
    class B
    {
    public:
    	int b;
    };
    
    class C: public A,public B
    {
    public:
    	int c;
    };
    
    using namespace std;
    
    int main()
    {
        C* p = new C;
        
    	//测试1
    	cout << (A*)p << endl;
    	cout << (B*)p << endl;
    	cout << p << endl;
    
    
    	//测试2
    	A* pA = (A*)p;
    	B* pB = (B*)pA;
    	cout << pA << endl;
    	cout << pB << endl;
    	
    	return 1;
    }
    

    该段代码中,C类继承A类和B类。

    通过监视:

    可以得知C类对象的内存结构如图:

    image-20200707142158637

    那么,如果将该对象类型转换为A类的指针,可以很容易实现,那么如果希望其转换为B类的指针呢,C++中又会如何实现呢?

    我们运行上述程序,得到输出结果如下图:

    image-20200707142445446

    前三行输出对应的是 (A*)p (B*)p p 的值,可以看到,在将p指针强行转换为B*类型时,指针的数值自动增加了4,即增加了1个A对象的大小,故在此可以看出,在多继承中,子类指针转换为父类时,他会进行一些相关的指针偏移运算。

    再看4,5行的输出结果,这两行表示的是将p强行转换为(A*),再将A* 转换为 B*.

    可以看出,在对于(A*)类型的指针来说,将其转换为(B*)是不会进行一些相关的指针偏移的,即不能在两个父类之间转换。

    在此时就可能会发生一些有意思的事情,比如 A类中有一个虚函数表,其中第一个函数对应的函数为FunA() , 同时在B类中同样内存偏移的位置也有一个虚函数表,此时由于类型转换后,会导致尽管看上去是B类型指针,而且成功调用了B类中的函数,但实际上调用的为FunA(),因为这两者的内存访问方式相同。

    //class A
    class A{
    public:
    	virtual void FunA();
    
        //other method or field
    }
    class B{
     public:
    	virtual void FunB();
    
        //other method or field  
    }
    
    class C:public A,public B
    {
        
    }
    
    
    int main()
    {
        C* p = new C;
        A* pa1 = (A*)p;
        B* pb1 = (B*)p;
        pa1->FunA();
        pb1->FunB();
        //上述两者访问的是各自的函数
        
        A* pa2 = (A*)p;
        B* pb2 = (B*)pa2;
        pa2->FunA();
        pb2->FunB();
        //上述两者访问的是同一个函数FunA()
    }
    
    

    那么如何解决这个容易发生的冲突呢。在C++ 中引入了 动态交叉类型转换函数:B* pb = dynamic_cast<B*>(pa)

    将上述代码修改:

    int main()
    {
        C* p = new C;
        A* pa1 = (A*)p;
        B* pb1 = (B*)p;
        pa1->FunA();
        pb1->FunB();
        //上述两者访问的是各自的函数
        
        A* pa2 = (A*)p;
        B* pb2 = (B*)pa2;
        pa2->FunA();
        pb2->FunB();
        //上述两者访问的是同一个函数FunA()
        
        A* pa3 = (A*)p;
        B* pb3 = dynamic_cast<B*>(pa3);
        pa3->FunA();
        pb3->FunB();  
        //此时两者访问的又为各自的函数
    }
    

    注: dynamic_cast<>() 函数直接对于多态类进行使用,即被转换的类必须继承自其他类或者其必须含有虚函数。

    菱形继承(虚基类)

    ​ 网上常说 C++ 设定的多继承不好,为什么不好呢,大部分人都会提及 会导致菱形继承问题,那么,菱形继承会导致什么问题呢,C++中又是如何实现解决的呢?

    所谓菱形继承,就是在多次继承中,某一个类继承了多个类,而这多个类又同时继承了同一个类,导致可能存在的数据冲突的问题。

    如下算是一种经典的菱形继承关系:

    image-20200707155400741

    如果动物中定义了一个属性 age ,那么马和驴都会有这个属性,若骡继承了马和驴的话,会导致其内部维护了两个 age 属性(如下图)。这就会导致内存的浪费,同时访问上也会更加复杂。

    image-20200707162112447

    那么C++中是如何解决这个问题的呢?

    看下面的代码:

    #include <iostream>
    
    using namespace std;
    
    
    class Animal
    {
    public:
    	int age;
    };
    
    
    class Horse:virtual public Animal
    {
    
    };
    
    class Donky:virtual public Animal
    {
    	
    };
    
    
    class Mule:public Horse,public Donky
    {
    	
    };
    
    int main()
    {
    	Mule* p = new Mule;
    
    	return 1;
    }
    

    在这段代码中新增加了2个关键词:virtual

    新增后,可以发现:类Mule 的内存结构发生了一些改变:

    image-20200707162607823

    相比于上面的结构,改结构中可以看到多了一个Animal类,那么为什么会多这个Animal类呢?这不是会占用更多的内存吗?

    在原代码的基础上再进行进一步测试。

    class Animal
    {
    public:
    	int age;
    };
    
    
    class Horse:virtual public Animal
    {
    	int H;
    };
    
    class Donky:virtual public Animal
    {
    	int D;
    };
    
    
    class Mule:public Horse,public Donky
    {
    	
    };
    
    int main()
    {
    	cout << sizeof(Animal) << endl;
    	cout << sizeof(Horse) << endl;
    	cout << sizeof(Donky) << endl;
    	cout << sizeof(Mule) << endl;
    	
    	return 1;
    }
    

    增加了一些属性,同时输出各个类的大小,可以得到结果:

    • Animal:4个字节
    • Horse:12个字节
    • Donky:12个字节
    • Mule:20个字节

    以上输出结果有两个异常,相比于常规的没有增加virtual关键字的类来说,马和驴类多了4个字节,同时,在结构上来看骡类应该为28个字节,但此处只有20个字节。

    通过vs2019开发者命令行工具,查询到了Mute类的结构如下图:

    image-20200707165524491

    可以发现多出了一个叫做 vbptr 的东西,这个东西称作虚基类指针,指向基类。

    故此,可以很好的理解,为什么Horse 和 Donky 是12个字节,而Mule 是20个字节了。

    Horse和Donky 中 前4个字节为一个虚基类指针,指向Animal类,再加上自己属性的4个字节,共计12个字节。

    对于Mule来说,他前16个字节维护了Horse 和Donky 中的虚基类指针和 自身的属性,再在最后维护了一个Animal类,故共有20个字节。

    此时,由于Horse 和 Donky 中的虚基类指针指向同一个对象,故此时不会造成在一个Mule中维护了两个age属性,此时也不用通过作用域来访问对应的age属性,消除了继承的同名冲突。

    虚继承和虚基类指针的设计,是为了解除多继承中产生的这些菱形继承问题,但实际上使用起来还是较为不便,因为大部分情况你无法得知哪些类需要设计为虚继承,哪些类不需要,所以实际编写C++中还是尽量避免这种类型的继承方式。



  • 很久很久之前的笔记了hhhh应该有很多错误大家伙见谅了😬



  • 好家伙,这也很详细了,就看你这个学习C++特性了👍



  • 还给出了代码例子,很友好呀,mark一下😁



  • mark了


登录后回复
 

Copyright © 2018 bbs.dian.org.cn All rights reserved.

与 Dian 的连接断开,我们正在尝试重连,请耐心等待