Th3.15:继承的构造函数、多重继承、虚继承之详述

 本博客将记录:新经典课程知识点的第15节的笔记!

本小节的知识点分别是继承的构造函数、多重继承、虚继承

今天总结的知识分为以下5个点:

(1)继承的构造函数
(2)多重继承
    (2.1)多重继承概述
    (2.2)静态成员变量
    (2.3)派生类构造函数与析构函数
    (2.4)从多个父类继承构造函数
(3)类型转换

(4)虚基类、虚继承(虚派生)
(5)总结

(1)继承的构造函数:

(我觉得还是少用为妙,因为这样写了会隐藏了子类的构造函数的细节实现,不利于日后维护代码!为了防止自己以后读别人写的代码遇到这样的用法时不发懵,了解一下即可)

        继承时,子类只能继承其直接基类(父类)的构造函数。默认(也即编译器自动给我们生成的)、拷贝、移动构造函数都是不能被继承的。那么怎么写继承的构造函数呢?

        格式:using 类名::类名(构造函数名);

        注意:

        ①如果基类的构造函数中带有默认的参数值,那么编译器一遇到using A::A;时,就会帮我们在子类中构造出多个构造函数来:

具体的构造规则如下:

        a)带有all参数的构造函数

        b)其余的构造函数,每个分别省略掉一个默认参数。 

比如:

class A {
public:
    int a1, a2, a3;
    A(int i, int j, int k = 5):a1(i),a2(j),a3(k) {}
};
class B :public A {
public:
    using A::A;//继承A的构造函数。其中的using关键字是让某个变量/函数在当前的作用域内可见的意思。
               //遇到这条代码时,编译器就会将用基类中的每个构造函数来生成一个与之对应的子类的构造函数
               //==> B(构造函数形参列表):A(A的构造函数形参列表){}
               //==> B(int i,int j,int k):A(i,j,k){}
    //又因为有默认参数,所以
    //此时的using A::A; ==>
    //B(int i, int j, int k) :A(i, j, k) {}
    //B(int i,int j) :A(i,j){}

};

        ②如果子类中只含有使用using 类名::类名;继承过来的构造函数时,编译器不认为我们给该子类定义了构造函数的。因此编译器还会为我们自动生成子类的默认构造函数。

(2)多重继承:

    (2.1)多重继承概述:

        多重继承:顾名思义,就是一个子类是继承自多个(2个及以上)父类的意思(一个孩子有很多个爹)。

class GrandFather {
public:
    int m_Age;
    string m_Name;
    GrandFather(int age,string name):m_Age(age),m_Name(name){}
    virtual void showInfo() {
        cout << "GrandFather's Name: " << this->m_Name << endl;
        cout << "GrandFather's Age: " << this->m_Age << endl;
    }
    virtual ~GrandFather() {}
};
class A :public GrandFather {//类A公有继承自类GrandFather
public:
    A(int age, string name) :GrandFather(age, name) {}
};
class B{//类B是独立的一个类
public:
    int m_b;
    B(int mb):m_b(mb) {}
    virtual ~B() {}
};
class C:public A,public B {//类C公有继承自类A和类B
public:
    C(int age, string name,int mb) :A(age, name),B(mb) {}
    virtual void showInfo() {
        cout << "C's Name: " << this->m_Name << endl;
        cout << "C's Age: " << this->m_Age << endl;
        cout << "C's mb: " << this->m_b << endl;
    }
    virtual ~C() {}
};

        为了让大家更加清楚多继承的结果关系,我这里画出示意图:

     (2.2)静态成员变量:

        在GrandFather类中声明一个静态成员变量并在类外给出定义:

class GrandFather {
public:
    int m_Age;
    string m_Name;
    GrandFather(int age,string name):m_Age(age),m_Name(name){}
    virtual void showInfo() {
        cout << "GrandFather's Name: " << this->m_Name << endl;
        cout << "GrandFather's Age: " << this->m_Age << endl;
    }
    virtual ~GrandFather() {}
public:
    static int grandStaticVar;//类内声明静态成员变量
};
int GrandFather::grandStaticVar = 100;//类外定义一个静态成员变量(也即给该静态成员变量分配内存)
//如果你的后续代码中不需要用到该静态成员变量的话,你就不需要再定义该静态成员变量了!
//如果你后续的代码中使用到该变量,就必须要在类外定义一下该静态成员变量,否则就会链接出错!
//main中:
multiSucc::C cc(123, "lyf",11);
cout << multiSucc::GrandFather::grandStaticVar <<"\t"// == 100
<< multiSucc::A::grandStaticVar << "\t"// == 100
<< multiSucc::C::grandStaticVar << "\t"// == 100
<< cc.grandStaticVar << endl;// == 100
    
cc.grandStaticVar = 110;

cout << multiSucc::GrandFather::grandStaticVar << "\t"// == 110
    << multiSucc::A::grandStaticVar << "\t"// == 110
    << multiSucc::C::grandStaticVar << "\t"// == 110
    << cc.grandStaticVar << endl;// == 110

运行结果:

     (2.3)派生类构造函数与析构函数:

a)构造一个派生类对象将同时构造并初始化所有的直接基类的成分。

b)派生类的构造函数初始化列表只初始化它的直接基类,在多继承时也不例外。这样就会一层一层地把参数传递回最原始的基类,并把all的基类成分都构造好。

c)基类的构造顺序跟“派生列表”中基类的出现顺序是保持一致的!析构顺序则相反。

d)每个类的析构函数都只会do自己这个类本身的成分的释放工作(类与类之间的析构函数是互不干扰的)。

class GrandFather {
public:
    int m_Age;
    string m_Name;
    GrandFather(int age,string name):m_Age(age),m_Name(name){
        cout << "GrandFather的构造函数执行!" << endl;
    }
    virtual void showInfo() {
        cout << "GrandFather's Name: " << this->m_Name << endl;
        cout << "GrandFather's Age: " << this->m_Age << endl;
    }
    virtual ~GrandFather() {
        cout << "GrandFather的析构函数执行!" << endl;
    }
};
class A :public GrandFather {
public:
    A(int age, string name) :GrandFather(age, name) {
        cout << "A的构造函数执行!" << endl;
    }
    virtual ~A() {
        cout << "A的析构函数执行!" << endl;
    }
};
class B{
public:
    int m_b;
    B(int mb):m_b(mb) {
        cout << "B的构造函数执行!" << endl;    
    }
    virtual ~B() {
        cout << "B的析构函数执行!" << endl;
    }
};
class C:public A,public B {//这行就是所谓的“派生列表”
public:
    C(int age, string name,int mb) :A(age, name),B(mb) {
        cout << "C的构造函数执行!" << endl;
    }
    virtual void showInfo() {
        cout << "C's Name: " << this->m_Name << endl;
        cout << "C's Age: " << this->m_Age << endl;
        cout << "C's mb: " << this->m_b << endl;
    }
    virtual ~C() {
        cout << "C的析构函数执行!" << endl;
    }
};
//main中:
multiSucc::C cc(123, "lyf", 11);

运行结果:

 如果把派生列表修改为class C:public B,public A 则运行结果为:

补充:这里补充一个隐式初始化基类的问题。

在B类中添加默认构造函数
B() {
    cout << "B的默认构造函数执行!" << endl;
}
将C类中的构造函数修改为:
C(int age, string name, int mb) :A(age, name) {
    cout << "C的构造函数执行!" << endl;
}

运行结果:

        这里在C类的构造函数中没有显式地调用B类的构造函数,但是编译器还是会为我们调用B类的默认构造函数进行隐式地初始化。

    (2.4)从多个父类继承构造函数:

        从多个父类中继承构造函数时,如果用using继承过来的构造函数相同(也即参数一样,函数体实现一样的构造函数),这样用就是错误的语法!

        面对此种case你只能给该子类自定义一个新的属于该类的构造函数,而无法使用using来帮你节省定义构造函数都工作量了!

class A {
public:
    A(int tv) {}
};
class B {
public:
    B(int tv) {}
};
class C : public A,public B {
public:
    using A::A;//继承A类的构造函数 ==> C(int tv):A(tv){}
    using B::B;//错误!继承B类的构造函数是 ==> C(int tv):B(tv){}该构造函数和继承自A类的一模一样
};

运行结果:

 说明子类C多继承自父类A和父类B的构造函数已经重定义了。因此我们只能自己定义一个属于子类C的构造函数:

class A {
public:
    A(int tv) {}
};
class B {
public:
    B(int tv) {}
};
class C : public A,public B {
public:
    using A::A;//继承A类的构造函数 ==> C(int tv):A(tv){}
    using B::B;//继承B类的构造函数 ==> C(int tv):B(tv){}
    C(int tv):A(tv),B(tv){
        cout << "C类的构造函数执行了!" << endl;
    }
};
//main中:
C ctest(888);

运行结果:

 (3)类型转换:

        在前面的单继承中,我们学过,基类指针是可以指向一个派生类(子类)对象的:因为编译器会帮助我们隐式地执行这种派生类到基类的转换。究其原因:因为每个派生类对象都会包含基类对象的成分。

        在多继承中,基类指针也是可以指向一个派生类(子类)对象的!

        因此以下式子都是正确的用法:

//以前述的(2)节中的代码为例子:
using namespace multiSucc;
GrandFather* pg = new C(23,"lzf",23);
A* pa = new C(23, "lzf", 23);
B* pb = new C(23, "lzf", 23);
C myc(23, "lzf", 23);
GrandFather g(myc);

        如果以上式子你有所疑惑可以翻阅我的3.11小节的博客总结,这里就不多赘述了! 

(4)虚基类、虚继承(虚派生):

        派生列表中,同一个基类只能出现一次,但是如下两种特殊情况例外:

        a)派生类可以通过它的两个直接基类分别继承自同一个间接基类。

(这也是典型的钻石继承问题,Effective C++ term40给予了我们几条重要的忠告!可翻看!!!)

 请看以下代码:

class GrandFather {
public:
    GrandFather(int age):m_Age(age){ cout << "GrandFather的构造函数执行!" << endl; }
    virtual ~GrandFather() {
        cout << "GrandFather的析构函数执行!" << endl;
    }
};
class A1 :public GrandFather {
public:
    A1(int age) :GrandFather(age)  { cout << "A1的构造函数执行!" << endl; }
    virtual ~A1() {
        cout << "A1的析构函数执行!" << endl;
    }
};
class A2 :public GrandFather {
public:
    A2(int age) :GrandFather(age)  { cout << "A2的构造函数执行!" << endl; }
    virtual ~A2() {
        cout << "A2的析构函数执行!" << endl;
    }
};
class C:public A1, public A2{
public:
    C(int age):A1(age),A2(age) { cout << "C的构造函数执行!" << endl; }
    virtual ~C() {
        cout << "C的析构函数执行!" << endl;
    }
};
int main(){
    using namespace multiSucc;
    C ctest(888);
    return 0;
}

运行结果:

        b)直接继承某个基类,然后通过另一个基类间接继承该类。

 请看以下代码:

class GrandFather {
public:
    GrandFather(int age):m_Age(age){ cout << "GrandFather的构造函数执行!" << endl; }
    virtual ~GrandFather() {
        cout << "GrandFather的析构函数执行!" << endl;
    }
};
class A1 :public GrandFather {
public:
    A1(int age) :GrandFather(age) { cout << "A1的构造函数执行!" << endl; }
    virtual ~A1() {
        cout << "A1的析构函数执行!" << endl;
    }
};
class C:public A1, public GrandFather{
public:
    C(int age):A1(age),GrandFather(age)  { cout << "C的构造函数执行!" << endl; }
    virtual ~C() {
        cout << "C的析构函数执行!" << endl;
    }
};
int main(){
    using namespace multiSucc;
    C ctest(888);
    return 0;
}

运行结果:

        但是,这两种特殊情况都会导致--》当我们对这同一个基类的成员变量进行读写操作时,会造成不明确的error!如下图所示:

        所以,即便这两种继承同一个基类的特殊情况一开始可以把编译器蒙骗住,让你的代码通过了,但是后面你想do事情的时候却手足无措。因为这里你造成爷爷类GrandFather被继承了2次,其成员变量就被构造了2次,而且还是分属于A1和A2的不同父类成分,但是在类C继承过来后两者名字相同,从而造成名字冲突,这是非常不好的代码!这就是多继承时对于基类成员变量的二义性问题。

        为了解决这种多继承时所出现的问题,引入了虚基类虚继承这个重要的知识点!

        虚基类(virtual base class): 无论这个基类在后续的继承体系中会出现多少次(被继承多少次),在其派生类中,都只会包含唯一一个共享的基类子内容成分。

        在上述的例子中,让A1和A2都虚继承自GrandFather类的话,那么这个GrandFather类就成为了虚基类,此时生成类C的对象并对虚继承自GrandFather类的成员变量do读写操作时就no problem了!

        虚继承:用virtual关键字do继承!

        格式:class 子类: virtual 继承类型 基类 | class 子类:继承类型 virtual 基类

        举例:class A1 :virtual public GrandFather 表示类A1从GrandFather这个基类中do虚继承

                   class A2 :public virtual GrandFather 表示类A2从GrandFather这个基类中do虚继承

        注意:

        ①只要子类virtual继承了基类,那么该基类就自动成为“虚基类”了!

        ②只会对孙子类起作用!

        ③该虚基类必须给其all的子类都虚继承过去后,才能确保该虚基类的孙子类能够虚继承其本身,只产生一份虚基类的成分!

        在上述的例子代码中,当A1和A2这两个类都虚继承自GrandFather类时,又因此类C继承自A1和A2,那么此时GrandFather类就是虚基类了,那么其孙子类C就只会产生一份虚基类成分的代码或者说孩子类只会生成一份共享的虚基类的实例对象(也即此时孙子类也虚继承了虚基类了)

        请看以下代码:

class GrandFather {
public:
    int m_Age;
    GrandFather(int age):m_Age(age) {
        cout << "GrandFather的构造函数执行!" << endl;
    }
    virtual ~GrandFather() {
        cout << "GrandFather的析构函数执行!" << endl;
    }
};
class A1 : public virtual GrandFather {//表示类A1从GrandFather这个基类中do虚继承
public:
    A1(int age):GrandFather(age) {
        cout << "A1的构造函数执行!" << endl;
    }
    virtual ~A1() {
        cout << "A1的析构函数执行!" << endl;
    }
};
class A2 :virtual public GrandFather {
public:
    A2(int age) :GrandFather(age) {
        cout << "A2的构造函数执行!" << endl;
    }
    //A2(int age, string name) :GrandFather(age, name) {
    //    cout << "A2的构造函数执行!" << endl;
    //}
    virtual ~A2() {
        cout << "A2的析构函数执行!" << endl;
    }
};
class C:public A1, public A2 {
public:
    C(int age):A1(age), A2(age),GrandFather(age) {//虚继承时,是直接由最底层的孙子来初始化爷爷的成分的!
        cout << "C的构造函数执行!" << endl;
    }
    virtual ~C() {
        cout << "C的析构函数执行!" << endl;
    }
};
int main(){
    using namespace multiSucc;
    C ctest(888);
    ctest.m_Age = 99999;
    return 0;
}

运行结果:

        成功运行!不会再产生对继承过来的成员变量m_Age访问的ambiguous不明确很模糊的问题了!也即deal了基类成员变量的二义性问题!

        当然,在多继承时,你也可以直接给继承同一个基类的子类用同名成员变量覆盖继承过来的同名变量的确可以达到不用虚继承的效果,但是这样do的前提是破坏了各个子类的构造函数的继承写法(也即每一个孩子类都必须用其直接基类的构造函数来对应地初始化其直接基类的成分!),让他们不去初始化对应继承过来的基类的m_Age,这样会导致每一个子类的对象中都产生多余的一份基类的你没有用到的m_Age,这样既不符合Effective c++的思想,又把多继承写得效率很低呀!!!

        补充说明3个点:
        ①现在是用孙子C类来构造虚基类GrandFather类的成分;若孙子C类又给别的孩子类继承后,则会由C类的孩子去初始化GrandFather类的成分了。换句话说:虚基类(GrandFather)的成分是由最底层的派生类来初始化的!

        ②初始化顺序问题:编译器一定会先初始化虚基类成分,然后再按照派生列表的顺序,来初始化其他类的成分。

        比如将上述代码中的C类的初始化列表修改为: class C:public A2, public A1

则运行结果为:

        ③如果在继承体系中,出现了多个虚继承的情况时,也即含有多个虚基类的情况时,到底哪一个虚基类会先被初始化呢?

        答:此时,编译器就会按照你最底一层的类中的派生(继承)列表往回追溯,逐个逐个地看是否这些直接基类含有虚基类,若有的话就会先构造该类的成分,否则就按照继承列表的顺序来构造!(先追溯到哪个虚基类,就先构造哪个虚基类中的对象的成分)

        请看以下代码:

class GrandFather {
public:
    int m_Age;
    GrandFather(int age):m_Age(age) { cout << "GrandFather的构造函数执行!" << endl; }
    virtual ~GrandFather() {
        cout << "GrandFather的析构函数执行!" << endl;
    }
};
class A1 : public virtual GrandFather {//表示类A1从GrandFather这个基类中do虚继承
public:
    A1(int age):GrandFather(age) { cout << "A1的构造函数执行!" << endl; }
    virtual ~A1() {
        cout << "A1的析构函数执行!" << endl;
    }
};
class A2 :virtual public GrandFather {//表示类A1从GrandFather这个基类中do虚继承
public:
    A2(int age) :GrandFather(age) { cout << "A2的构造函数执行!" << endl; }
    virtual ~A2() {
        cout << "A2的析构函数执行!" << endl;
    }
};
class B{
public:
    int m_b;
    B(int mb):m_b(mb) { cout << "B的构造函数执行!" << endl; }
    virtual ~B() {
        cout << "B的析构函数执行!" << endl;
    }
};
class BB : public virtual  B{//表示类BB从B这个基类中do虚继承
public:
    BB(int mb) :B(mb) { cout << "BB的构造函数执行!" << endl; }
    virtual ~BB() {
        cout << "BB的析构函数执行!" << endl;
    }
};
class C:public A1, public A2,public BB {
//A1和A2和BB这个类都是do虚继承的,那么编译器就会按照继承列表按顺序地调用对应的虚基类的构造函数do事情!
public:
    C(int age):A1(age), A2(age),GrandFather(age),BB(age),B(age) {//虚继承时,实际上是直接由孙子来初始化爷爷的成分的!
        cout << "C的构造函数执行!" << endl;
    }
    virtual ~C() {
        cout << "C的析构函数执行!" << endl;
    }
};

运行结果:

        从运行结果可见,符合我上述所说的第③点补充说明!

(5)总结:

        1)若非必要,请一定不要选择用多继承的方式来编写你的代码!十分重要到建议:能用单继承解决问题,就不要用多继承!能不用virtual虚继承就尽量不用virtual继承来写代码!

        2若实在必要用到多继承时,务必细心地编写你的代码,并时刻提醒自己到底是否需要用虚继承的方式来do多继承!(当产生成员变量的二义性问题时,就需要用虚继承来do多继承!)

        3)学习多继承这个知识点不是说你一定要去常用它,而是为了让你去阅读别人的代码时不发懵!

        好,那么以上就是这一3.15小节我所回顾的内容的学习笔记,希望你能读懂并且消化完,也希望自己能牢记这些小小的细节知识点,加油吧,我们都在coding的路上~

### 解决PyCharm无法加载Conda虚拟环境的方法 #### 配置设置 为了使 PyCharm 能够成功识别并使用 Conda 创建的虚拟环境,需确保 Anaconda 的路径已正确添加至系统的环境变量中[^1]。这一步骤至关重要,因为只有当 Python 解释器及其关联工具被加入 PATH 后,IDE 才能顺利找到它们。 对于 Windows 用户而言,在安装 Anaconda 时,默认情况下会询问是否将它添加到系统路径里;如果当时选择了否,则现在应该手动完成此操作。具体做法是在“高级系统设置”的“环境变量”选项内编辑 `Path` 变量,追加 Anaconda 安装目录下的 Scripts 文件夹位置。 另外,建议每次新建项目前都通过命令行先激活目标 conda env: ```bash conda activate myenvname ``` 接着再启动 IDE 进入工作区,这样有助于减少兼容性方面的问题发生概率。 #### 常见错误及修复方法 ##### 错误一:未发现任何解释器 症状表现为打开 PyCharm 新建工程向导页面找不到由 Conda 构建出来的 interpreter 列表项。此时应前往 Preferences/Settings -> Project:...->Python Interpreter 下方点击齿轮图标选择 Add...按钮来指定自定义的位置。按照提示浏览定位到对应版本 python.exe 的绝对地址即可解决问题。 ##### 错误二:权限不足导致 DLL 加载失败 有时即使指定了正确的解释器路径,仍可能遇到由于缺乏适当的操作系统级许可而引发的功能缺失现象。特别是涉及到调用某些特定类型的动态链接库 (Dynamic Link Library, .dll) 时尤为明显。因此拥有管理员身份执行相关动作显得尤为重要——无论是从终端还是图形界面触发创建新 venv 流程均如此处理能够有效规避此类隐患。 ##### 错误三:网络连接异常引起依赖下载超时 部分开发者反馈过因网速慢或者其他因素造成 pip install 操作中途断开进而影响整个项目的初始化进度条卡住的情况。对此可尝试调整镜像源加速获取速度或是离线模式预先准备好所需资源包后再继续后续步骤。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fanfan21ya

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值