C++学习笔记day47-----C++98-继承中的构造函数,析构函数,拷贝构造函数,拷贝赋值函数,多重继承,虚继承

继承中的构造函数
当通过一个子类创建一个新的对象时,编译器会根据子类在初始化表中指明的基类的初始化方式去调用基类相应的构造函数。如果子类的初始化表中,并没有指明基类的初始化方式,编译器将会调用基类的无参构造函数。由于子类在声明的时候,会写明自己的继承表,所以编译器执行的时候,总是先看到基类,所以编译器最先初始化的就是其基类,然后再是子类的其他成员。最后执行子类构造函数的函数体。
所以,不能将基类的初始化放在函数体中。其理由和成员子对象类似。

继承中的析构函数
当一个子类对象到生命周期尽头的时候,它是先调用子类自身的析构函数,然后调用成员子对象的析构函数,最后调用基类的析构函数。无论子类的析构函数,是否是自定义的,编译器都会自动的调用基类的析构函数。
注意:如果使用new创建了一个子类对象,然后delete指向这个子类对象的基类指针,很有可能造成内存泄漏。指向子类对象的基类指针,只能访问子类对象的一部分,那么使用它去delete,也只能delete子类对象的一部分。

#include <iostream>
using namespace std;
class Base{
public:
    Base(void):m_i(0){
        cout << "基类的无参构造" << endl;
    }
    Base(int i):m_i(i){
        cout << "带有整型参数的构造" << endl;
    }
    ~Base(void){
        cout << "Base的析构函数" << endl;
    }
    int m_i;
};
class Member{
public:
    Member(void):m_i(0){
        cout << "me的无参构造" << endl;
    }
    Member(int i):m_i(i){
        cout << "me有参" << endl;
    }
    ~Member(void){
        cout << "Member的析构函数" << endl; 
    }
    int m_i;
};
class Drived:public Member,public Base{
public:
    Drived(void){}
    //指明基类子对象初始化方式
    Drived(int i):Member(i),Base(i){}
    ~Drived(void){
        cout << "Drived的析构函数" << endl;
    }
};
int main(void){
    /*
    Drived d1;
    cout << d1.Base::m_i << endl;
    Drived d2(100);
    cout << d2.Member::m_i << endl;
    */
    //指向子类对象的基类指针
    Base *pb = new Drived;
    delete (Drived *)pb;
    pd = NULL;
    return 0;
}

继承中的拷贝构造函数
如果子类没有定义拷贝构造函数,在使用子类对象进行拷贝构造的时候,编译器会调用其缺省的拷贝构造函数,然后自动调用基类的拷贝构造函数,对子类对象中的基类子对象部分进行拷贝构造。
如果子类自定义了拷贝构造函数,必须在拷贝构造函数的初始化表中,调用基类的拷贝构造函数,为子类对象中的积累子对象部分进行拷贝构造。

继承中的拷贝赋值函数
如果子类没有定义拷贝赋值函数,在使用子类对象进行拷贝赋值的时候,编译器会调用子类的缺省拷贝赋值函数,然后自动调用基类子对象的拷贝构造函数。
如果子类自定义了拷贝赋值函数,在使用子类对象进行拷贝赋值的时候,需要在子类的拷贝赋值函数中显式的调用基类子对象的拷贝赋值函。

#include <iostream>
using namespace std;
class Base{
public:
    Base(void):m_i(0){}
    Base(int i):m_i(i){}
    Base(const Base &that):m_i(that.m_i){
        cout << "Base(const Base &that)" << endl;
    }
    Base &operator=(const Base &that){
        cout << "Base &operator=(const Base &that)" << endl;
        if(this != &that){
            this -> m_i = that.m_i;
        }
        return *this;
    }
    int m_i;
};
class Derived:public Base{
public:
    Derived(void):m_i(0){}
    Derived(int i,int m):Base(i),m_i(m){}
    Derived(const Derived &that)//指明基类要用拷贝的方式进行初始化
        :Base(that),m_i(that.m_i){}
    Derived &operator=(const Derived &that){
        cout << "Derived &operator=(const Derived &that)" << endl;
        if(this != &that){
           //(Base)*this = that;
            (*this).Base::operator=(that);
            m_i = that.m_i;
        }
        return *this;
    }
    int m_i;
};
int main(void){
    Derived d1(100,200);
    cout << "d1.m_i =" << d1.m_i 
        << ",d1.Base::m_i = " << d1.Base::m_i << endl;
    Derived d2(d1);
    cout << "d2.m_i =" << d2.m_i 
        << ",d2.Base::m_i = " << d2.Base::m_i << endl;
    Derived d3;
    d3 = d1;
    cout << "d3.m_i =" << d3.m_i 
        << ",d3.Base::m_i = " << d3.Base::m_i << endl;
    return 0;
}

多重继承
一个子类同时继承多个基类。
多重继承中的向上造型
多重继承产生的子类创建对象后。根据向上造型的原理,子类所有的基类类型指针都可以直接指向子类对象的内存空间,编译器也不会报错。编译器会根据指针的类型将其偏移,将对应的基类类型指针指向子类对象内存空间和该类型匹配的那片区域。所以,当子类的多个不同的基类类型指针指向子类对象的时候,指针的值会不一样,存在偏移。

#include <iostream>
using namespace std;
class Phone{
public:
    Phone(const string &number):m_number(number){}
    void call(const string &number){
        cout << m_number << "打给" 
            << number << endl;
    }
private:
    string m_number;
};

class Player{
public:
    Player(const string &media):m_media(media){}
    void play(const string &music){
        cout << "播放" << music << endl; 
    }
private:
    string m_media;
};

class Computer{
public:
    Computer(const string &os):m_os(os){}
    void run(const string &app){
        cout << "在" << m_os 
            << "上运行一个" << app << endl;
    }
private:
    string m_os;
};

class SmartPhone:public Phone,public Player,public Computer{
public:
    SmartPhone(const string &number,const string &media,\
            const string &os)
        :Phone(number),Player(media),Computer(os){}
private:
};
int main(void){
    SmartPhone chuizi("15168281470","Mp4","锤子");
    chuizi.call("13777415260");
    chuizi.play("炫个脑袋");
    chuizi.run("王者荣耀");
    SmartPhone *p_S = &chuizi;
    Phone *p_P = p_S;
    Player *p_Pl = p_S;
    Computer *p_C = p_S;
    cout << "p_P = " << p_P << ",p_Pl = " << p_Pl
        <<",p_C = " << p_C << endl;
    return 0;
}

多重继承中的标识符冲突问题
一个子类的多个基类中,可能存在相同的标识符,比方说,A基类中的成员变量a,和B基类中的成员函数a,重复了,当一个子类同时继承它们之后,通过子类对象调用a的时候,编译器就会报歧义错误,编译器不知道程序员想要的是成员变量A::a,还是成员函数B::a。
解决方法就是,对重名的标识符,再调用的时候通过类名限定符来显式的标识”A::a”.

#include <iostream>
using namespace std;
class Base1{
public:
    void func(void){
        cout << "this is Base1" << endl;
    }
};
class Base2{
public:
    void func(int i){
        cout << "this is Base2" << endl;
    }
};
class Derived:public Base1,public Base2{
    //通过using生命让它们在子类中形成重载
    //using Base1::func;
    //using Base2::func;
    //不建议这么使用,如果是一个函数,一个变量重名,就不能用。

};
int main(void){
    Derived d;
    d.Base1::func();
    return 0;
}

钻石继承问题
A
/ \
B C
\ /
D
A类是B类和C类的基类,B类和C类又是D的基类。通过以下例子先来知晓问题所在。

#include <iostream>
using namespace std;
class A{
public:
    A(int data):m_data(data){}
protected:
    int m_data;
};

class B:public A{
public:
    B(int data):A(data){}
    void set(int data){
        m_data = data;
    }
};

class C:public A{
public:
    C(int data):A(data){}
    int get(void){
        return m_data;
    }
};

class D:public B,public C{
public:
    D(int data):B(data),C(data){}
};

int main(void){
    D d(100);
    cout << "d.get() = " << d.get() << endl;
    d.set(200);
    cout << "d.get() = " << d.get() << endl;
    return 0;
}

程序运行结束后发现两次答应的结果一致,d.set(200)好像并没有起效果。
D类继承了B类和C类,而B类和C类,都继承了一次A类。
在D的内存空间中存在一份通过B类继承的m_ data,和一份通过C类继承m_data。
set修改的是D通过B类继承的m_ data,而get得到的是D类通过C继承的m_data
这就是钻石继承中的问题,汇聚子类所创建的对象中保持着多份基类的实例。

虚继承
C++中提供了虚继承的手段来保证,在汇聚子类创建的对象中只有一份实例。
1)在中间类的继承表中用virtual声明
2)在汇聚子类中,再次继承定义实例的基类。

#include <iostream>
using namespace std;
class A{
public:
    A(int data):m_data(data){
        cout << "A:addr = " << this 
            << ",A:sizeof " << sizeof(A) << endl;
    }
protected:
    int m_data;
};

class B:virtual public A{
public:
    B(int data):A(data){
        cout << "B:addr = " << this 
            << ",B:sizeof " << sizeof(B) << endl;
    }
    void set(int data){
        m_data = data;
    }
};

class C:virtual public A{
public:
    C(int data):A(data){
        cout << "C:addr = " << this 
            << ",C:sizeof " << sizeof(C) << endl;
    }
    int get(void){
        return m_data;
    }
};

class D:public B,public C{
public:
    //虚继承时,末端汇聚子类负责构造公共基类(A)子对象
    D(int data):B(data),C(data),A(data){
        cout << "D:addr = " << this 
            << ",D:sizeof " << sizeof(D) << endl;
    }
};

int main(void){

    D d(100);
    cout << "d.get() = " << d.get() << endl;
    d.set(200);
    cout << "d.get() = " << d.get() << endl;

    //B b(100);
    return 0;
}

题外话
虚基类的原理大致如下:
一个子类在继承基类的时候,被vritual修饰之后,会生成一个虚表指针,这个指针,指向一张表的中间位置,表的前半部分记录了子类对象地址的起始位置到从基类中继承的数据的起始位置偏移量。可以理解成,当使用vritual修饰之后,子类对象不像传统的单继承一样,将基类的成员变量放在自己的内部(虽然在内存上还是连在一起),而是通过一个虚表指针寻址。
这样的机制,在钻石继承中,就可以使B,C两个基类不需要再自己创建m_data,仅仅只需要用自己的虚标指针找到D所维护的m _data即可。
注意,虚标指针产生有两种情况:
第一种,类中声明了虚函数,会产生一个虚表指针,这个时可以被继承的。
第二种,子类通过virtual的方式继承一个基类,编译器会为子类生成一个虚标指针,这个指针,是不会继承的,而是绑定在每个子类对象里的。
注意,第二种情况下,如果基类中压根就没有成员变量,则子类就没有必要也不会生成一个虚表指针。
虚基类

—————————————————
二十一、继承()
5、子类的构造函数
1) 如果子类的构造函数,没有指明基类子对象的初始化方式,那么编译器会自动的调用基类的无参构造函数来初始化该子对象。
2) 如果希望基类子对象以有参的方式被初始化,则必须是在子类对象的初始化表中指明基类的初始化方式。
(不能写在子类的构造函数的函数体中,和成员子对象一样,基类子对象的初始化也是在执行子类的构造函数体之前完成的)

如果一个子类中包含基类子对象和成员子对象,那么会先初始化基类子对象,这个是符合之前所说的谁先声明就初始化谁的原则,因为在子类的头部,是标明了继承了哪些基类,因为编译器的执行顺序,所以是先看到继承表,根据基类的声明顺序来决定初始化的顺序。经过验证,如果一个子类继承了两个类,哪个类先声明就先初始化哪个类。

6、子类的析构函数
1) 子类的析构函数,无论自己实现还是缺省提供的,都会自动调用基类的析构函数,析构继承子对象.
2) 子类对象的销毁过程
–> 执行析构函数
–> 析构成员子对象
–> 析构基类子对象
–> 释放内存
3) 基类的析构函数不能调用子类的析构函数,delete一个指向子类对象的基类指针,实际被调用的仅仅只是基类的析构函数,子类的析构函数不会被执行,有内存泄露的风险。

7、子类的拷贝构造和拷贝赋值
1) 子类的拷贝构造
–>如果子类没有定义拷贝构造函数,那么编译器会为子类提供缺省的拷贝构造函数,该函数会自动调用基类的拷贝构造函数,初始化基类子对象。
–>如果子类自己定义了拷贝构造函数,那么必须要使用初始化表指明基类子对象也以拷贝的方式进行初始化。
2) 拷贝赋值操作附函数
–>如果子类没有定义拷贝赋值函数,编译器会为子类提供缺省的拷贝赋值函数,该函数会自动调用基类的拷贝赋值函数,赋值基类子对象
–>如果子类自己定义了拷贝赋值函数,需要显式的调用基类的拷贝赋值函数,完成对基类子对象的复制。

8、多重继承
1) 一个子类同时继承多个基类,这样的继承方式成为多重继承。

    电话  计算机  播放器
       \    |     /
         智能手机

2) 多重继承在向上造型
编译器会根据各个基类子对象的内存布局,自动的进行适当的偏移计算,保证指针的类型和所指向的目标类型一致。

3) 名字冲突问题
–>一个子类的多个基类如果存在相同的标识符,当通过子类访问这些标识符的时候,编译器会报歧义错误—名字冲突。
–>解决名字冲突的常规做法就是显式通过”类名::”,指明所访问的名字属于哪个基类。
–>如果产生冲突的标识符是两个函数,而且满足参数不同的重载条件,也可以通过using声明的方式,让它们在子类中形成重载,通过重载匹配来解决。//不推荐

9、钻石继承
1) 概念:一个子类的多个基类源自共同的基类祖先,这样的继承结构成为钻石结构。
A
/ \
B C
\ /
D
2) 公共基类(A)子对象在汇聚子类(D)对象中,存在多个实例,通过汇聚子类访问公共基类的成员时,会因为继承路径不同,导致结果不一致。

3) 通过虚继承,可以让公共基类子对象在汇聚子对象中实例唯一,并为所有的中间类共享。即使沿着不同的继承路径所访问到的公共基类成员也是一致的。

10、虚继承的语法

1) 在继承表中使用virtual修饰
2) 位于继承链的末端子类,负责构造公共基类子对象

当子类继承基类的时候,基类使用virtual修饰后,子类会生成一个虚表指针,这个指针指向了一张表的中间位置,在这张表的前半部分位置,记录了子类对象中子类对象的起始地址到 从基类继承的数据的地址的起始地址的偏移量。
这就是为什么子类对象的尺寸变大了4个字节。也通过这样的方式解决了钻石继承的问题。

猜你喜欢

转载自blog.csdn.net/displaymessage/article/details/80585734