C++构造函数、虚函数

一:构造函数

    基本构造函数:

执行过程:1)传参   2)给类数据成员开辟空间     3)执行冒号语法给数据成员初始化    4)执行构造函数括号里面的内容

例如:

class Base{
public:
# 冒号语法后面的内容相当于int a = 10;(初始化)
  Base(int var) : m_Var(var){
      }
private:
   int m_Var;
};

    拷贝构造函数:

一般不需要自己编写,系统默认的拷贝构造函数就能抗住了,但是有些情况需要在构造的时候开辟空间,这时候就需要拷贝构造函数了,以下几种情况都会自动调用拷贝构造函数:

1)用一个已有的对象初始化一个新对象的时候

2)将一个对象以值传递的方式传给形参的时候

3)函数返回一个对象的时候

 class Base{
     public:
        Base(int var) : m_Var(var){
        }
     //拷贝构造函数
     Base(Base &ref) : m_Var(ref.m_Var){
     }
     private:
     int m_Var;
 };


具体例子:String的拷贝构造函数
String::String(const String &other) 
{
     int length = strlen(other.m_data);
     m_data = new char[length+1]; // 若能加 NULL 判断则更好
     strcpy(m_data, other.m_data);
}

    普通派生类构造函数:

执行过程:1)传参     2)根据继承时的声明顺序构造基类    3)给类数据成员开辟空间    4)执行冒号语法后面的语句    5)执行构造函数函数体语句:

 class Base{
 public:
      Base(int b) : m_b(b){
      }
 private:
      int m_b;
  };
 
class Derived : public Base{
public:
     //普通派生类构造函数的写法
     Derived(int b, int d) : Base(b), m_d(d){
     }
private:
     int m_d;
};

    虚继承的派生类构造函数:

虚继承主要是针对多继承时,出现二义性问题而提出的。比如,如下代码就需要用到虚继承,否则的话Derived类继承时,Base类就会不明确,执行步骤为:1)传参 2)创建基类,注意这时候需要显示创建所有“有参构造函数”的基类,包括直接基类,间接基类。 3)给类数据成员开辟空间  4)执行冒号语法  5)执行构造函数函数体;

 class Base{
 public:
      Base(int b) : m_b(b){
      }
 private:
      int m_b;
 };
 
class Base1 : virtual public Base{
 public:
     Base1(int b, int b1) : Base(b), m_b1(b1)     {
     }
 private:
     int m_b1;
}; 

class Base2 : virtual public Base{
 public:
     Base2(int b, int b2) : Base(b), m_b2(b2){
     }
 private:
     int m_b2;
};
 //虚继承,避免二义性
class Derived : public Base1, public Base2{
 public:
     Derived(int b, int b1, int b2, int d) : Base(b), Base1(b, b1), Base2(b, b2), m_d(d){
 //注意冒号语法后面的顺序无所谓,创造基类是按照上面的继承声明顺序来进行的...
     }
 private:
     int m_d;
};

二:虚析构函数

虚析构一般伴随着多态而产生,多态主要方式就是用基类的指针或引用指向或引用派生类,而形成多态。但是这样就会存在一个问题,当我们析构的时候,由于是基类的指针,就会调用的是基类的构造函数,从而造成派生内存溢出。为了解决这个问题,引入了虚析构的概念。将基类的构造函数声明为虚,从而使其在调用析构函数的时候能够准确的调用派生类的析构函数。如下代码必须用到虚析构才能准确的析构派生类,并释放其占有内存:

 class Base{
  public:
      Base(int b) : m_b(b) {
      }
      //虚析构,使基类指针能准确的释放所指向的派生类里面的内容
      virtual ~Base(){
     }
 private:
     int m_b;
};
 
class Derived : public Base{
 public:
     Derived(int b, char *pStr) : Base(b){ 
         m_pStr = new char[strlen(pStr)+1];
         strcpy(m_pStr,pStr);
     }
     ~Derived(){
         delete m_pStr;
         m_pStr = NULL;
     }
 private:
     char *m_pStr;
};
 
int main(void) {
     char *pStr = "abcdefg";
     Base *b = new Derived(1,pStr);
     delete b;
     return 0;
}

以上引用该博客

三:虚函数

虚函数的基础这里不做记录,主要是整理一下对虚函数的理解。

虚函数很重要的一个作用就是实现多态,其实现过程为通过父类指针指向子类对象,单就父类指针指向子类对象时应当注意的是:

1,如果以一个基础类指针指向一个衍生类对象(派生类对象),那么经由该指针只能访问基础类定义的函数(静态联翩)
2,如果以一个衍生类指针指向一个基础类对象,必须先做强制转型动作(explicit cast),这种做法很危险,也不符合生活习惯,在程序设计上也会给程序员带来困扰。(一般不会这么去定义)
3,如果基础类和衍生类定义了相同名称的成员函数,那么通过对象指针调用成员函数时,到底调用那个函数要根据指针的原型来确定,而不是根据指针实际指向的对象类型确定

关键字virtual只用在类定义里的函数声明中,写函数体时不用

虚函数就是为了解决1、3的问题。

在使用指向子类对象的基类指针,并调用子类中的覆盖函数时,如果该函数不是虚函数,那么将调用基类中的该函数;如果该函数是虚函数,则会调用子类中的该函数

当一个类存在虚函数时,通过该类创建的对象实例,会在内存空间的前4字节保存一个指向虚函数表的指针__vfptr。即使该类的父类没有虚函数(也就没有形成虚表),那么尽管继承了父类的成员变量,这些成员变量的位置依然排在指向虚函数标的指针__vfptr后面。__vfptr指向的虚函数表,是类独有的,而且被该类的所有对象共享。

class Base{
public:
    int base_1;
    int base_2;

    virtual void base_fun1() {}
    virtual void base_fun2() {}
};

如果Base类实例了两个对象b1和b2,他们的内存分布如下:

可以看到&b1和&b2是不同地址的,但是__vfptr的指向却是同一个虚函数表。所以验证了那句:被该类的所有对象共享。其重点就是:

  1. 她是编译器在编译时期为我们创建好的, 只存在一份
  2. 定义类对象时, 编译器自动将类对象的__vfptr指向这个虚函数表

子类和父类不是同一类,这个应当注意。

虚函数表的实质,是一个虚函数地址的数组,它包含了类中每个虚函数的地址,既有当前类定义的虚函数,也有覆盖父类的虚函数,也有继承而来的虚函数。虚函数顺序与虚函数在类中的声明位置有关,在类中第一个声明的虚函数在虚表中的位置总是第一个,第二个虚函数则排放在虚表中的第二个位置,依次排放。如果子类中有新虚函的添加,那么直接放在该虚函数的表(数组)的最后,如果子类中重写了父类虚函数的,那么新函数的地址将覆盖原有地址。所以一个类的虚函数数量再怎么增加,他这个类的大小是不变的(其实普通成员函数也不占该类的大小,区别后续),变化的是__vfptr指向的虚函数表这个数组的大小。

直接调用与间接调用(虚调用)

  • 通过类对象的方式调用虚函数称为直接调用,编译器直接生成调用该虚函数的代码
    例:

  • int main(int argc, char* argv[]){
      CTest t;
      t.ShowInfo();
      t.ShowInfo1();
      t.ShowInfo2();
      return 0;
    }

    观察上面代码的反汇编代码:

    CTest t;
    008D1A58  lea         ecx,[t]  
    008D1A5B  call        CTest::CTest (08D1096h)  
      t.ShowInfo();
    008D1A60  lea         ecx,[t]  
    008D1A63  call        CTest::ShowInfo (08D10B4h)  
      t.ShowInfo1();
    008D1A68  lea         ecx,[t]  
    008D1A6B  call        CTest::ShowInfo1 (08D11B3h)  
      t.ShowInfo2();
    008D1A70  lea         ecx,[t]  
    008D1A73  call        CTest::ShowInfo2 (08D104Bh)  
      return 0;
    008D1A78  xor         eax,eax  

    可以看出VC++编译器生成的直接调用对应虚函数的代码

  • 通过指向对象的指针或引用调用虚函数,称为间接调用,编译器生成直接调用虚函数的代码,而是通过虚表指针
    取出虚表内的虚函数指针,然后用虚函数指针调用虚函数,例:

    int main(int argc, char* argv[]){
      CTest t;
      CTest & rt = t;
      CTest * pt = &t;
      rt.ShowInfo2();
      pt->ShowInfo();
      return 0;
    }

    观察上面代码的反汇编代码:

    CTest t;
      00121A58  lea         ecx,[t]  
      00121A5B  call        CTest::CTest (0121096h)  
        CTest & rt = t;
      00121A60  lea         eax,[t]  
      00121A63  mov         dword ptr [rt],eax  
        CTest * pt = &t;
      00121A66  lea         eax,[t]  
      00121A69  mov         dword ptr [pt],eax  
        rt.ShowInfo2();
      00121A6C  mov         eax,dword ptr [rt]    //取对象t的地址
      00121A6F  mov         edx,dword ptr [eax]   //取虚表指针
      00121A71  mov         esi,esp  
      00121A73  mov         ecx,dword ptr [rt]  
      00121A76  mov         eax,dword ptr [edx+8] //取出ShowInfo2在虚表中的函数指针
      00121A79  call        eax                   //调用虚函数ShowInfo2
      00121A7B  cmp         esi,esp                
      00121A7D  call        __RTC_CheckEsp (0121140h)  
        pt->ShowInfo();
      00121A82  mov         eax,dword ptr [pt]    //取对象t的地址
      00121A85  mov         edx,dword ptr [eax]   //取虚表指针
      00121A87  mov         esi,esp  
      00121A89  mov         ecx,dword ptr [pt]    
      00121A8C  mov         eax,dword ptr [edx+4]  //取出ShowInfo在虚表中的函数指针
      00121A8F  call        eax                    //调用虚函数ShowInfo
      00121A91  cmp         esi,esp  
      00121A93  call        __RTC_CheckEsp (0121140h)  
        return 0;
      00121A98  xor         eax,eax  

    在普通成员函数中调用虚函数依然是间接调用

  • 例:在CTest类中加入一个如下的成员函数,使用如下代码测试:

    void Test() { 
      ShowInfo(); 
    }
    int main(int argc, char* argv[]){
      CTest t;
      t.Test();
      return 0;
    }

    Test函数对应的反汇编代码如下:

    void Test() { ShowInfo(); }
    013719D0  push        ebp  
    013719D1  mov         ebp,esp  
    013719D3  sub         esp,0CCh  
    013719D9  push        ebx  
    013719DA  push        esi  
    013719DB  push        edi  
    013719DC  push        ecx  
    013719DD  lea         edi,[ebp-0CCh]  
    013719E3  mov         ecx,33h  
    013719E8  mov         eax,0CCCCCCCCh  
    013719ED  rep stos    dword ptr es:[edi]  
    013719EF  pop         ecx  
    013719F0  mov         dword ptr [this],ecx  
    013719F3  mov         eax,dword ptr [this]  
    013719F6  mov         edx,dword ptr [eax]  
    013719F8  mov         esi,esp  
    013719FA  mov         ecx,dword ptr [this]  //取虚表指针
    013719FD  mov         eax,dword ptr [edx+4] //从虚表中取虚函数ShowInfo的指针
    01371A00  call        eax                   //调用虚函数
    01371A02  cmp         esi,esp  
    01371A04  call        __RTC_CheckEsp (01371140h)  
    01371A09  pop         edi  
    01371A0A  pop         esi  
    01371A0B  pop         ebx  
    01371A0C  add         esp,0CCh  
    01371A12  cmp         ebp,esp  
    01371A14  call        __RTC_CheckEsp (01371140h)  
    01371A19  mov         esp,ebp  
    01371A1B  pop         ebp  
    01371A1C  ret

    可以看出在成员函数中调用虚函数是间接调用,也是根据虚表来调用

  • 在构造函数和析构函数中不会通过虚表来调用虚函数,而是直接在编译时生成直接调用虚函数的代码
    例:

    CTest():m_nTest(1){
      ShowInfo1();
    }
    
    ~CTest(){
      ShowInfo1();
    }

    对应反汇编代码如下:

    CTest():m_nTest(1)
    000D181C  mov         eax,dword ptr [this]  
    000D181F  mov         dword ptr [eax+4],1  
        ShowInfo1();
    000D1826  mov         ecx,dword ptr [this]  
    000D1829  call        CTest::ShowInfo1 (0D11C2h)  
     ~CTest()
    
    {
        000D1860  push        ebp  
        000D1861  mov         ebp,esp  
        000D1863  push        0FFFFFFFFh  
        000D1865  push        0D60D0h  
        000D186A  mov         eax,dword ptr fs:[00000000h]  
        000D1870  push        eax  
        000D1871  sub         esp,0CCh  
        000D1877  push        ebx  
        000D1878  push        esi  
        000D1879  push        edi  
        000D187A  push        ecx  
        000D187B  lea         edi,[ebp-0D8h]  
        000D1881  mov         ecx,33h  
        000D1886  mov         eax,0CCCCCCCCh  
        000D188B  rep stos    dword ptr es:[edi]  
        000D188D  pop         ecx  
        000D188E  mov         eax,dword ptr [__security_cookie (0DB004h)]  
        000D1893  xor         eax,ebp  
        000D1895  push        eax  
        000D1896  lea         eax,[ebp-0Ch]  
        000D1899  mov         dword ptr fs:[00000000h],eax  
        000D189F  mov         dword ptr [this],ecx  
        000D18A2  mov         eax,dword ptr [this]  
        000D18A5  mov         dword ptr [eax],offset CTest::`vftable' (0D8B34h)  
            ShowInfo1();
        000D18AB  mov         ecx,dword ptr [this]  
        000D18AE  call        CTest::ShowInfo1 (0D11C2h)   //直接调用
    }

    引用自:https://www.cnblogs.com/UnknowCodeMaker/p/11279021.html

例子:

class Base{
    public:
        void func1(){
            func2();
        }
        virtual void func2(){
            cout<<"Base::func2()"<<endl;
        }
};

class Derived : public Base{
    public:
        virtual void func2(){
            cout<<"Derived:func2()"<<endl;
        }
};

int main(){
     Derived d;
     Base *pBase = &d;
     pBase->func1();
     return 0;
 }

是不是大家觉得 pBase 指针对象虽然指向的是派生类对象,但是派生类里没有 fun1 成员函数,则就调用基类的 fun1 成员函数,Base::fun1() 里又会调用基类的 fun2 成员函数,所以输出结果是Base::fun2() ?假设我把上面的代码转换一下, 大家还觉得输出的是 Base::fun2() 吗:

class Base 
{
public:
    void fun1() 
    { 
        this->fun2();  // this是基类指针,fun2是虚函数,所以是多态
    }
}

this 指针的作用就是指向成员函数所作用的对象, 所以非静态成员函数中可以直接使用 this 来代表指向该函数作用的对象的指针。

pBase 指针对象指向的是派生类对象,派生类里没有 fun1 成员函数,所以就会调用基类的 fun1 成员函数,在Base::fun1() 成员函数体里执行 this->fun2() 时,实际上指向的是派生类对象的 fun2 成员函数。

所以正确的输出结果是:

Derived:fun2()

所以我们需要注意:

在非构造函数,非析构函数的成员函数中调用「虚函数」,也是多态!!

虚函数实现多态原因:

    将基类指针指向派生类可以实现多态是因为虚函数,但是其整个过程是如何呢?

首先,是基类指针指向派生类,或者说派生类地址赋给基类指针。这中间涉及到了类型的强制转换。首先刨开继承关系,每个类都有相应的大小尺寸,这就要分为从大尺寸到小尺寸的转换和小尺寸到大尺寸的转换。因此在内存上,数据就产生了补零和丢弃两种结果

  • 若目标操作数的类型尺寸小于源操作数的类型尺寸,则以目标操作数的类型尺寸为准,目标操作数的超出源操作数类型尺寸的剩余高地址部分的字节数据会被丢弃
  • 若目标操作数的类型尺寸大于源操作数的的尺寸类型,则目标操作数的所有字节数据会按低地址到高地址的顺序依次拷贝到目标操作数,目标操作数超出源操作数尺寸的剩余高地址部分保持字节数据,编译器会以字节0填充

因此,两个毫不相关的类实例相互强制转换会产生不可预知的后果,赋值操作符左边的类实例的数据完全被篡改,这是一种非常危险的行为。

对于有继承链相关的类型转换(派生类转为基础类指针,基础类转为派生类很少用,这里不做讨论),其实涉及到了‘对象切片’,切片后的影响为:当将派生类对象分配给基类对象(Upcast)时,派生类对象的所有成员都将复制到基类对象,但派生类原创的成员(属性和方法),这些成员被编译器"阉割"掉。

那么派生通过继承得到的基类成员函数的地址,在对象切片的作用下有三种情况。

  • 对于非虚成员函数来说,基类对象只能得到基类原创定义且可被继承的成员函数的地址,派生类原创定义的成员函数的地址,对于upcast操作后的基类对象是不可见的。
  • 对于虚成员函数来说,如下三种情况,对于基类对象运行时绑定哪个虚成员函数的地址,是依据填入基类的虚表的函数地址来判断的。
    • 若该函数是派生类原创定义的,对于upcast操作后的基类对象是不可见的。
    • 若该函数是基类原创定义且未被派生类重写,对于upcast操作后的基类对象,该基类版本的虚函数可见。
    • 若该函数是基类原创定义且已被派生类重写,对与upcast操作后的基类对象,该派生类版本的虚成员函数可见。

父类公开或受保护的成员函数(包括虚函数)同样是被派生类继承,但继承的只是父类成员函数的调用权,在继承关系中,派生类从基类继承的成员函数实质上继承的是存储在代码段(Code Segment)内存区中,基类可共享的成员函数的内存地址,因为每个成员函数都有一个唯一的内存地址。引用自该博客

例子:

#include<iostream>
#include<string>
using namespace std;
class Employee{
  private:
    bool p_isValid=true;
  public:
    std::string m_name;
    double m_salary=1500;
    bool m_iService=true;
    Employee(){}
    Employee(const std::string &name):m_name(name){}
    Employee(const std::string &name,double salary):
    m_name(name),
    m_salary(salary){}

    virtual void show_info(){
      std::cout<<"显示员工信息"<<std::endl;
    }

    void show_status(){
      std::cout<<"当前员工的账户状态:";
      if(!p_isValid){
        std::cout<<"已禁用"<<std::endl;
      }else{
        std::cout<<"已启用"<<std::endl;
      }
    }

    virtual void add_salary(double k){
      m_salary+=k;
      std::cout<<m_name<<" 加薪后:¥"<<m_salary<<std::endl;
    }
    
    void show_salary(){
      std::cout<<m_name<<":¥"<<m_salary<<std::endl;
    }
    
    virtual void search(){
      std::cout<<"查找用户功能"<<std::endl;
    }
};

class Manager:public Employee{
  public:
    int level=0;
    std::string group="administrator";
    Manager(const std::string &name){
      m_name=name;
      m_salary=10000;
    }
    virtual void depart_mgnt(){
      std::cout<<"部门管理功能"<<std::endl;
    }
    void show_undering(){
      std::cout<<"直属员工管理"<<std::endl;
    }
};
class Supervisor:public Employee{
  public:
    Supervisor(const std::string &name){
      m_name=name;
      m_salary=5000;
    }
};
int main(void){
  std::string e_name="职员";
  std::string m_name="经理";
  Employee *e=new Employee(e_name);
  Manager *m=new Manager(m_name);
  Employee *e2=m; //upcast
  e2->show_salary();
  //类型转换后只有,编译器只认定e2有40个字节
  std::cout<<sizeof(*e2)<<std::endl;
//可以正常访问,因为show_salary继承在基类
 e2->show_salary(); 

//不可访问,因为level是Manager类实例原创的数据成员
 e2->level;
//不可访问,因为show_undering方法是
//Manager类实例原创的成员函数
 e2->show_undering();
  
}

通过该图可以看出,派生类里面的成员函数和虚函数:只有在基础类里面原创定义的变量被复制进来。尽管虚表指针的值也被复制进来,但虚表内只复制了前三个函数。其实虚表指针指向的是三个元素的数组,同样,类型转换的时候也被切片了三个。

是否有虚成员变量的概念?

抱歉,没有。并且,一定要注意,如果派生类里面的定义的成员变量的名字和基础类一模一样(在虚函数里叫做重写),应该注意不要被迷惑了:

#include 
using namespace std ;
class A{ 
public:
A() { m_c = 'A' ; }
char m_c ;
} ;
class B : public A{ 
public:
B() { m_c = 'B' ; }
char m_c ;
} ;
int main(){ 
A a ;
B b ;

A * p = &b ;
cout << p->m_c << endl ;
cout << ((B*)p)->m_c << endl ;
return 0 ;
}

/************输出***************/
A
B

注意,派生类B里面的m_c这个字符,是重新定义而不是继承过来的。

构造和析构函数是否能为虚函数?

虚函数的意义

虚函数的实际意义非常之大。比如在实际开发过程中,会用到别人封装好的框架和类库,我们可以通过继承其中的类,并覆盖基类中的函数,来实现自定义的功能。但是,有些函数是需要框架来调用,并且API需要传入基类指针类型的参数。而使用虚函数就可以,将指向子类对象的基类指针来作为参数传入API,让API能够通过基类指针,来调用我们自定义的子类函数。这就是多态性的真正体现。

重写,重载,重定义区分:

重写(覆盖)

  • 是指派生类函数重写(覆盖)基类函数
  • 不同的范围,分别位于基类和派生类中
  • 函数的名字相同
  • 参数相同
  • 基类函数必须有virtual关键字

重载

  • 成员函数
  • 形同的范围(在同一个类中)
  • 函数的名字相同
  • virtual关键字可有可无

重定义(隐藏)

  • 派生类屏蔽了与其同名的基类函数
  • 如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏
  • 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有关键字,此时,基类的函数被隐藏
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值