c++继承

目录

为什么要使用继承

继承的概念

派生类定义方法

派生类访问权限控制

继承中的析构和构造

继承中的对象模型

对象构造和析构的调用原则

子类和父类同名成员的处理方法

非自动继承的函数

静态成员在继承中的特点

多继承

多继承概念

菱形继承和虚继承

虚继承实现原理


为什么要使用继承

一个类继承另一个类,这样类中可以少定义一些成员

如果直接定于职工类 代码重复比较严重

继承的概念

c++ 最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类 的成员,还拥有新定义的成员。
一个 B 类继承于 A 类,或称从类 A 派生类 B 。这样的话,类 A 成为基类(父类),类 B 成为派生类(子类)。 派生类中的成员,包含两大部分: 一类是从基类继承过来的,一类是自己增加的成员。 从基类继承过过来的表现其共性,而新增的成员体现了其个性

派生类定义方法

派生类定义格式:

Class 派生类名 : 继承方式 基类名 {
//派生类新增的数据成员和成员函数
}
三种继承方式:
public : 公有继承
private : 私有继承
protected : 保护继承
从继承源上分:
单继承:指每个派生类只直接继承了一个基类的特征
多继承:指多个基类派生出一个派生类的继承关系 , 多继承的派生类直接继承了不止一个基类的特征

cpp 复制代码
#include <iostream>
#include <string.h>

using namespace std;

class Animal
{
    public:
		int age;
		void printf()
		{
			cout << age << endl;
		}
};

class Dog : public Animal
{
	public:
		int tail_len;

		/*相当于拷贝代码
		int age;
		void printf()
		{
			cout << << endl;
		}
		*/
};
void test01()
{
	Dog d;
	d.age = 10;
	d.printf();	
}

派生类访问权限控制

派生类继承基类,派生类拥有基类中全部成员变量和成员方法(除了构造和析构之外的成员方法),但是在派生 类中,继承的成员并不一定能直接访问,不同的继承方式会导致不同的访问权限。 派生类的访问权限规则如下:

cpp 复制代码
​​//基类
class A{
    public:
        int mA;
    protected:
        int mB;
    private:
        int mC;
};
//1. 公有(public)继承
class B : public A{
    public:
        void PrintB(){
            cout << mA << endl; //可访问基类 public 属性
            cout << mB << endl; //可访问基类 protected 属性
            //cout << mC << endl; //不可访问基类 private 属性
        }
};
class SubB : public B{
    void PrintSubB(){
        cout << mA << endl; //可访问基类 public 属性
        cout << mB << endl; //可访问基类 protected 属性
        //cout << mC << endl; //不可访问基类 private 属性
    }
};
void test01(){
    B b; 
    cout << b.mA << endl; //可访问基类 public 属性
    //cout << b.mB << endl; //不可访问基类 protected 属性
    //cout << b.mC << endl; //不可访问基类 private 属性
}
//2. 私有(private)继承
class C : private A{
    public:
        void PrintC(){
            cout << mA << endl; //可访问基类 public 属性
            cout << mB << endl; //可访问基类 protected 属性
            //cout << mC << endl; //不可访问基类 private 属性
        }
};

class SubC : public C{
    void PrintSubC(){
        //cout << mA << endl; //不可访问基类 public 属性
        //cout << mB << endl; //不可访问基类 protected 属性
        //cout << mC << endl; //不可访问基类 private 属性
    }
};
void test02(){
    C c;
    //cout << c.mA << endl; //不可访问基类 public 属性
    //cout << c.mB << endl; //不可访问基类 protected 属性
    //cout << c.mC << endl; //不可访问基类 private 属性
}
//3. 保护(protected)继承
class D : protected A{
    public:
        void PrintD(){
            cout << mA << endl; //可访问基类 public 属性
            cout << mB << endl; //可访问基类 protected 属性
            //cout << mC << endl; //不可访问基类 private 属性
        }
};
class SubD : public D{
    void PrintD(){
        cout << mA << endl; //可访问基类 public 属性
        cout << mB << endl; //可访问基类 protected 属性
        //cout << mC << endl; //不可访问基类 private 属性
    }
};
void test03(){
    D d;
    //cout << d.mA << endl; //不可访问基类 public 属性
    //cout << d.mB << endl; //不可访问基类 protected 属性
    //cout << d.mC << endl; //不可访问基类 private 属性
}

继承中的析构和构造

继承中的对象模型

在 C++ 编译器的内部可以理解为结构体,子类是由父类成员叠加子类新成员而成

cpp 复制代码
#include <iostream>

using namespace std;

class Aclass
{
    public:
        int mA;
        int mB;
};
class Bclass : public Aclass
{
    public:
        int mC;
};
class Cclass : public Bclass
{
    public:
        int mD;
};
void test()
{
    cout << "A size:" << sizeof(Aclass) << endl;
    cout << "B size:" << sizeof(Bclass) << endl;
    cout << "C size:" << sizeof(Cclass) << endl;
}

int main()
{
    test();

    return 0;
}

编译运行

对象构造和析构的调用原则

继承中的构造和析构
子类对象在创建时会首先调用父类的构造函数
父类构造函数执行完毕后,才会调用子类的构造函数
当父类构造函数有参数时,需要在子类初始化列表 ( 参数列表 ) 中显示调用父类构造函数
析构函数调用顺序和构造函数相反

cpp 复制代码
#include <iostream>

using namespace std;

class Base
{
    public:
        Base(int age,string name)
        {
            this->age = age;
            this->name = name;
            cout << "Base构造函数" << endl;
        }
        ~Base()
        {
            cout << "Base析构函数" << endl;
        }
        int age;
        string name;
};
//创建子类对象时,必须先构造父类 需要调用父类的构造函数
class Son:public Base
{
    public:
        Son(int id,int age,string name):Base(age,name)
        {
            this->id = id;
            cout << "Son构造函数" << endl;
        }
        ~Son()
        {
            cout << "Son析构函数" << endl;
        }
        int id;
};
void test()
{
    Son p(10,8,"lucy");
}

int main()
{
    test();

    return 0;
}

编译运行

建的时候先建在里边的父类 再建外边的子类 拆的时候先拆外边的子类 再拆里边的父类 很好理解

子类和父类同名成员的处理方法

如果子类和父类由同名的成员变量,父类的变量会被隐藏,访问的是子类变量

如果子类和父类由同名的成员函数,父类的函数会被隐藏,访问的是子类函数

cpp 复制代码
#include <iostream>

using namespace std;

class Base
{
    public:
		Base(int a)
		{
		
		}

        int a;

};
class Son:public Base
{
	public:
		Son(int a1,int a2):Base(a1),a(a2)
		{
		}
		int a;
};
void test()
{
	Son p(10,20);
	
	cout << p.a << endl;//输出20
}

int main()
{
    test();

    return 0;
}

非自动继承的函数

不是所有的函数都能自动从基类继承到派生类中。构造函数和析构函数用来处理对象的创建和析构操作,构造和析构函数只知道对它们的特定层次的对象做什么,也就是说 构造函数和析构函数 不能被继承,必须为每一个特定的派生类分别创建。
另外 operator= 也不能被继承,因为它完成类似构造函数的行为。也就是说尽管我们知道如何由 = 右边的对象如何初始化= 左边的对象的所有成员,但是这个并不意味着对其派生类依然有效。
在继承的过程中,如果没有创建这些函数,编译器会自动生成它们。

静态成员在继承中的特点

如果子类和父类有同名的静态成员变量,父类中的静态成员变量会被隐藏
如果子类和父类有同名的静态成员函数,父类中的静态成员函数都会被隐藏

class Base{
public:
static int getNum()
{
return sNum;
}
static int getNum(int param)
{
return sNum + param;
}
public:
static int sNum;
};
int Base::sNum = 10;
class Derived : public Base
{
public:
static int sNum;
//基类静态成员属性将被隐藏
#if 0 //重定义一个函数,基类中重载的函数被隐藏
static int getNum(int param1, int param2)
{
return sNum + param1 + param2;
}
#else
//改变基类函数的某个特征,返回值或者参数个数,将会隐藏基类重载的函数
static void getNum(int param1, int param2)
{
cout << sNum + param1 + param2 << endl;
}
#endif
};
int Derived::sNum = 20

多继承

多继承概念

一个类继承了多个类

cpp 复制代码
#include <iostream>

using namespace std;

class A
{
    public:
        int a;
};
class B
{
    public:
        int a;
};

class C:public A,public B
{
	public:
		int a;
};
void test()
{
	C c;
	c.a = 10;
	c.A::a= 20;
	c.B::a = 30;
}

int main()
{
    test();
    return 0;
}

菱形继承和虚继承

两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石型继承。
这种继承所带来的问题:

  1. 羊继承了动物的数据和函数,鸵同样继承了动物的数据和函数,当草泥马调用函数或者数据时,就会产生二义性。
  2. 草泥马继承自动物的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
    上述问题如何解决?对于调用二义性,那么可通过指定调用那个基类的方式来解决,那么重复继承怎么解决?
    对于这种菱形继承所带来的两个问题,c++为我们提供了一种方式,采用虚基类(virtual )。
cpp 复制代码
#include <iostream>

using namespace std;

class BigBase{
    public:
        BigBase(){ mParam = 0; }
        void func(){ cout << "BigBase::func" << endl; }
    public:
        int mParam;
};
class Base1 : public BigBase{};
class Base2 : public BigBase{};
class Derived : public Base1, public Base2{};
int main(){
    Derived derived;
    //1. 对"func"的访问不明确
    //derived.func();
    //cout << derived.mParam << endl;
    cout << "derived.Base1::mParam:" << derived.Base1::mParam << endl;
    cout << "derived.Base2::mParam:" << derived.Base2::mParam << endl;
    //2. 重复继承
    cout << "Derived size:" << sizeof(Derived) << endl; //8
    return 0;
}

上述问题如何解决?对于调用二义性,那么可通过指定调用那个基类的方式来解决,那么重复继承怎么解决?
对于这种菱形继承所带来的两个问题, c++ 为我们提供了一种方式,采用虚基类。那么我们采用虚基类方式将代码 修改如下:

cpp 复制代码
#include <iostream>

using namespace std;

class BigBase{
    public:
        BigBase(){ mParam = 0; }
        void func(){ cout << "BigBase::func" << endl; }
    public:
        int mParam;
};

class Base1 : virtual public BigBase{};
class Base2 : virtual public BigBase{};
class Derived : public Base1, public Base2{};
int main(){
    Derived derived;
    //二义性问题解决
    derived.func();
    cout << derived.mParam << endl;
    //输出结果:12
    cout << "Derived size:" << sizeof(Derived) << endl;
    return 0;
}

虚继承实现原理

以上程序 Base1 , Base2 采用虚继承方式继承 BigBase, 那么 BigBase 被称为虚基类。
通过虚继承解决了菱形继承所带来的二义性问题。
但是虚基类是如何解决二义性的呢?并且 derived 大小为 12 字节,这是怎么回事?



通过内存图,我们发现普通继承和虚继承的对象内存图是不一样的。我们也可以猜测到编译器肯定对我们编写的程序做了一些手脚。
BigBase 菱形最顶层的类,内存布局图没有发生改变。Base1和 Base2 通过虚继承的方式派生自 BigBase, 这两个对象的布局图中可以看出编译器为我们的对象中增加了一 个vbptr (virtual base pointer),vbptr 指向了一张表,这张表保存了当前的虚指针相对于虚基类的首地址的偏移量。Derived 派生于 Base1 和 Base2, 继承了两个基类的 vbptr 指针,并调整了 vbptr 与虚基类的首地址的偏移量。
由此可知编译器帮我们做了一些幕后工作,使得这种菱形问题在继承时候能只继承一份数据,并且也解决了二义 性的问题。现在模型就变成了 Base1 和 Base2 Derived 三个类对象共享了一份 BigBase 数据。
当使用虚继承时,虚基类是被共享的,也就是在继承体系中无论被继承多少次,对象内存模型中均只会出现一个 虚基类的子对象(这和多继承是完全不同的)。即使共享虚基类,但是必须要有一个类来完成基类的初始化(因为所有的对象都必须被初始化,哪怕是默认的),同时还不能够重复进行初始化,那到底谁应该负责完成初始化 呢?C++ 标准中选择在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),
但是虚基类的初始化是由最后的子类完成,其他的初始化语句都不会调用

class BigBase{
public:
BigBase(int x){mParam = x;}
void func(){cout << "BigBase::func" << endl;}
public:
int mParam;
};
class Base1 : virtual public BigBase{
public:
Base1() :BigBase(10){}
//不调用 BigBase 构造
};
class Base2 : virtual public BigBase{
public:
Base2() :BigBase(10){}
//不调用 BigBase 构造
};
class Derived : public Base1, public Base2{
public:
Derived() :BigBase(10){}
//调用 BigBase 构造
};
//每一次继承子类中都必须书写初始化语句
int main(){
Derived derived;
return 0;
}

注意:
虚继承只能解决具备公共祖先的多继承所带来的二义性问题,不能解决没有公共祖先的多继承的 .
工程开发中真正意义上的多继承是几乎不被使用,因为多重继承带来的代码复杂性远多于其带来的便利,多重继 承对代码维护性上的影响是灾难性的,在设计方法上,任何多继承都可以用单继承代替。

相关推荐
Qter_Sean17 分钟前
自己动手写Qt Creator插件
开发语言·qt
何曾参静谧21 分钟前
「QT」文件类 之 QIODevice 输入输出设备类
开发语言·qt
爱吃生蚝的于勒1 小时前
C语言内存函数
c语言·开发语言·数据结构·c++·学习·算法
小白学大数据3 小时前
Python爬虫开发中的分析与方案制定
开发语言·c++·爬虫·python
冰芒猓4 小时前
SpringMVC数据校验、数据格式化处理、国际化设置
开发语言·maven
失落的香蕉4 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
杜杜的man5 小时前
【go从零单排】Closing Channels通道关闭、Range over Channels
开发语言·后端·golang
java小吕布5 小时前
Java中Properties的使用详解
java·开发语言·后端
versatile_zpc6 小时前
C++初阶:类和对象(上)
开发语言·c++