C++继承

一、定义

继承(英语:inheritance)是面向对象软件技术当中的一个概念。这种技术使得复用以前的代码非常容易,能够大大缩短开发周期,降低开发费用。

例如:定义一个汽车类,它包含了车的一些基本属性(如颜色、轮胎、排气量等)

然后,由汽车类,派生出小汽车类,它包含汽车类的所有成员属性,并且自己还有一些新的属性

此时,利用继承编写小汽车类,可以减少重复代码,提高代码的复用性

二、基本语法

class 类名 : 继承方式 父类名

三、继承方式

在 C++ 中,一共有三种继承方式:

  • 公共继承

  • 保护继承

  • 私有继承

三种继承方式,使得子类继承下来的属性具有不同的访问权限,下面开始一一讲解

1. 公共继承(public)

首先给出父类 Dad

// 父类
class Dad
{
public:
    int a;

protected:
    int b;

private:
    int c;
};

公共继承,在父类中,访问权限为 public、protected 的属性,继承到子类后,访问权限不变,但子类无法访问父类中访问权限为 private 的成员(注意:不是无法继承)

例如,给出 Son0 类

// 公共继承,不改变 Dad 属性的权限
// 但无法访问 Dad 的私有成员
class Son0 : public Dad
{
    void test(void)
    {   
        a = 10; // 仍为 public
        b = 10; // 仍为 protected
        // c = 10; // 无法访问 Dad 的私有成员
    }
};

再给出验证代码

// 验证公共继承
void test0(void)
{
    Son0 s0;
    s0.a = 0; // s0.a 在 Son0 中为公共成员属性
    // s0.b = 0; // s0.b 在 Son0 中为保护成员属性,类外不可访问
}

2. 保护继承(protected)

保护继承,在父类中,访问权限为 public、protected 的属性,继承到子类后,访问权限均为protected,子类也无法访问父类中访问权限为 private 的成员

例如,给出 Son1 类

// 保护继承,继承后的属性均为 protected
// 也不能访问 Dad 的私有成员
class Son1 : protected Dad
{
public:
    void test(void)
    {
        a = 10; // 变为 protected
        b = 10; // 仍为 protected
        // c = 10; // 无法访问 Dad 的私有成员
    }
};

再给出验证代码

// 验证保护继承
void test1(void)
{
    Son1 s1;
    // s1.a = 10; // s1.a 在 Son1 中为保护成员属性,类外不可访问
    // s1.b = 10; // s1.b 在 Son1 中为保护成员属性,类外不可访问
}

3. 私有继承(private)

保护继承,在父类中,访问权限为 public、protected 的属性,继承到子类后,访问权限均为private,子类也无法访问父类中访问权限为 private 的成员

例如,给出 Son2 类

// 私有继承,继承后的属性均为 private
// 也不能访问 Dad 的私有成员
class Son2 : private Dad
{
public:
    void test(void)
    {
        a = 10; // 变为 private
        b = 10; // 仍为 private
        // c = 10; // 无法访问 Dad 的私有成员
    }
};

再给出验证代码

void test2(void)
{
    Son2 s2;
    // s2.a = 10; // s1.a 在 Son2 中为私有成员属性,类外不可访问
    // s2.b = 10; // s1.b 在 Son2 中为私有成员属性,类外不可访问
}
//--------------------------------------------------------------
// 进一步验证了,私有继承,继承后的属性均为 private
// 即使是公共继承 Son2,也无法访问 Son2 的成员属性
// 因为 Son2 的成员属性均从 Dad 私有继承而来,权限均为私有
class SonOfSon2 : public Son2
{
public:
    void test(void)
    {
        // a = 10; // 无法访问 Son2 的私有成员
        // b = 10; // 无法访问 Son2 的私有成员
        // c = 10; // 无法访问 Dad 的私有成员
    }
};

三种继承方式总结

  • 公有继承,继承的成员属性访问权限不变

  • 保护继承,继承的成员属性访问权限均为 protected

  • 私有继承,继承的成员属性访问权限均为 private

此外,三种继承方式均不能访问父类的私有成员,但这并不代表没有继承父类中的私有成员

// 虽然无法访问父类私有成员
// 但父类的私有成员也是被继承下来了的
// 只是编译器隐藏了,我们无法访问
void test2(void)
{
    // 输出结果为 12(三个整形数据)
    std::cout << "Sizeof Son0 = " << sizeof(Son0) << std::endl;
}

根据上面的代码,可以很直观的看到:Son0 继承了来自父类的所有成员

四、构造与析构顺序

在实例化一个子类时,是先调用父类的构造函数还是子类的构造函数呢?

在销毁一个子类时,是先调用父类的析构函数还是子类的析构函数呢?

直接上代码:

// 父类
class Dad
{
public:
    Dad()
    {
        std::cout << "Calling Dad's constructor function." << std::endl;
    }
    ~Dad()
    {
        std::cout << "Calling Dad's destructor function." << std::endl;
    }
};
// 子类
class Son0 : public Dad
{
public:
    Son0()
    {
        std::cout << "Calling Son0's constructor function." << std::endl;
    }
    ~Son0()
    {
        std::cout << "Calling Son0's destructor function." << std::endl;
    }
};
// 验证构造函数、析构函数的调用时机:
// 栈的 FILO 性质?
void test3(void)
{
    Son0 s0;
}

调用 test3 函数,输出结果为:

Calling Dad's constructor function.
Calling Son0's constructor function.
Calling Son0's destructor function.
Calling Dad's destructor function.

根据输出结果,可以发现:

  • 实例化一个子类时,先调用父类的构造函数,再调用子类的构造函数(先有父亲再有儿子)

  • 销毁一个子类时,先调用子类的析构函数,再调用父类的析构函数(白发人送黑发人)

五、继承中的同名成员处理

在继承中,难免会遇到子类与父类,存在成员属性名称相同的情况

同名非静态成员处理方式

先给出父类与子类

class Dad
{
public:
    Dad()
        :a(114514){}
    int a;
    void func(void)
    {
        std::cout << "Calling Dad's void func(void)." << std::endl;
    }
    void func(int a)
    {
        std::cout << "Calling Dad's void func(int a)." << std::endl;
    }
};

class Son : public Dad
{
public:
    Son()
        :a(2023){}
    int a;
    void func(void)
    {
        std::cout << "Calling Son's func(void)." << std::endl;
    }
};

然后,实例化一个 Son 类

Son s;
  • 若访问 Son 中的同名成员,直接访问即可

// 直接访问,访问的是子类中的 a
std::cout << s.a << std::endl; 
// 成员函数的调用也是一样的
// 直接调用,调用的是子类中的 func
s.func(); 
  • 若访问 Dad 中的同名成员,需要加上作用域

// 想要访问父类中的 a,需要加上作用域
std::cout << s.Dad::a << std::endl;
// s.func(100); // 直接调用,就算有父类同名函数参数与子类不同,编译器也不认
// 还得加上作用域
s.Dad::func();
s.Dad::func(100);

注意: 就算有父类同名函数参数与子类不同,也就是发生了重载直接访问并加上参数,仍然无法调用父类的函数

同名静态成员处理方式

对于静态成员,有两种方式访问

  • 通过对象访问

  • 通过类名访问

同样地,给出父类与子类

class Dad
{
public:
    static int b;
    static void static_func(void)
    {
        std::cout << "Calling Dad's static void static_func(void)." << std::endl;
    }
    static void static_func(int a)
    {
        std::cout << "Calling Dad's static void static_func(int a)." << std::endl;
    }
};

int Dad::b = 114514; // 静态变量初始化

class Son : public Dad
{
public:
    static int b;
    void func(void)
    {
        std::cout << "Calling Son's func(void)." << std::endl;
    }
    static void static_func(void)
    {
        std::cout << "Calling Son's static void static_func(void)." << std::endl;
    }
};

int Son::b = 2023;

与访问非静态同名成员一样,想要访问父类的同名成员,需加上作用域

通过对象访问
// 通过对象访问
Son s;
// 直接访问,访问的是子类中的 b
std::cout << s.b << std::endl;
// 想要访问父类中的 b,需要加上作用域
std::cout << s.Dad::b << std::endl;
// 直接调用,调用的是子类中的 static_func
s.static_func();
// s.static_func(1); // 直接调用,就算有父类同名函数参数与子类不同,编译器也不认
// 想要调用父类中的 static_func,需要加上作用域
s.Dad::static_func();
s.Dad::static_func(1);
通过类名访问
// 通过类名访问
// 直接访问,访问的是子类中的 b
std::cout << Son::b << std::endl;
// 想要访问父类中的 b,需要加上作用域
std::cout << Son::Dad::b << std::endl;
// 直接调用,调用的是子类中的 static_func
Son::static_func();
// 想要调用父类中的 static_func,需要加上作用域
Son::Dad::static_func();
Son::Dad::static_func(1);

六、多继承

一个子类可以同时有多个父类,子类继承多个父类的行为称为多继承

在实际开发中,通常不建议使用多继承,因为可能会产生同名现象,造成不必要的麻烦

语法

class 子类名 : 继承方式 父类一, 继承方式 父类二 ...

实例

class Dad0
{
public:
    int a;
    Dad0()
        :a(0){}
};

class Dad1
{
public:
    int a;
    Dad1()
        :a(0){}
};

class Son : public Dad0, public Dad1
{
public:
    int c;
    int d;
    Son()
        :c(0), d(0){}
};

void test(void)
{
    Son s;
    // 当两个父类成员有重名现象,需加作用域区分
    // s.a = 11; // 报错:a 不明确
    s.Dad0::a = 11;
    s.Dad1::a = 4;
    s.c = 51;
    s.d = 4;
}

七、菱形继承

菱形继承是多继承的一种特殊情况

定义

两个子类继承同一个父类而又有子类同时继承这两个子类,这种继承方式形如菱形,因此叫做菱形继承

引入

观察上面的图片,可以发现:羊驼类发生了多继承

我们就以这四个类为例,讲讲菱形继承

class Animal
{
public:
    std::string color;
};
class Sheep : public Animal {};
class Camel : public Animal {};
class Alpaca : public Sheep, public Camel {};

void test(void)
{
    Alpaca alpaca;
    // alpaca.color = "white"; 报错:color 不明确
    alpaca.Sheep::color = "white"; // 合法
    alpaca.Camel::color = "brown"; // 也合法
}

在 test 函数中,实例化了一个羊驼。

然而,当我们想对羊驼的颜色进行赋值时,发现无法直接访问

为了访问到 color,需要加上作用域

但是,加上作用域,虽然可以正常访问,但是我们发现,alpaca 类中有两个 color,这是毫无意义的(一只羊驼怎么会同时有两种颜色捏)

为了减少资源浪费,同时让 color 有意义,我们需要用到 虚继承

虚继承的定义

C++使用虚拟继承(Virtual Inheritance),使得派生类如果继承基类多次,但只有一份基类的拷贝在派生类对象中。

语法

class 子类名 : virtual 继承方式 父类一, virtual 继承方式 父类二 ...

可以发现,与多继承语法唯一不同的就是:在继承方式前加上了 virtual 关键字

对引入的修改

利用虚继承,将引入部分的代码修改如下

class Animal
{
public:
    std::string color;
};
class Sheep : virtual public Animal {};
class Camel : virtual public Animal {};
class Alpaca : virtual public Sheep, public Camel {};

void test(void)
{
    Alpaca alpaca;
    alpaca.color = "white";
    // 当然,仍可以按以下两种方式进行赋值
    alpaca.Sheep::color = "white";
    alpaca.Camel::color = "white";
}

与不使用虚继承相比,使用虚继承后此时只有一份 color 的拷贝在 alpaca 对象中,这样能够减少资源浪费,同时也让 color 有意义

八、总结

利用继承编写子类,可以减少重复代码,提高代码的复用性

继承的三种方式

  • 公有继承,继承的成员属性访问权限不变

  • 保护继承,继承的成员属性访问权限均为 protected

  • 私有继承,继承的成员属性访问权限均为 private

实例化子类与销毁子类,父类与子类的构造函数、析构函数调用顺序

  • 实例化一个子类时,先调用父类的构造函数,再调用子类的构造函数(先有父亲再有儿子)

  • 销毁一个子类时,先调用子类的析构函数,再调用父类的析构函数(白发人送黑发人)

当父类、子类存在重名成员时

  • 访问子类同名成员,直接访问即可

  • 访问父类同名成员,加上父类作用域

实际开发中,不建议使用多继承

利用虚继承,解决菱形继承带来的问题

最后,感谢你耐心看到这里,希望我的文章对你有所帮助

  • 18
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值