C++学习 day10

发布于 18 天前  37 次阅读


Day 10:继承

本节代码位置:

2-代码/day10-继承/54-继承规则验证.cpp
2-代码/day10-继承/55-私有继承示例.cpp
2-代码/day10-继承/56-继承中的构造函数示例.cpp
2-代码/day10-继承/57-继承中的同名成员示例.cpp
2-代码/day10-继承/58-公有派生类与基类关系示例.cpp
2-代码/day10-继承/59-类适配器示例.cpp
2-代码/day10-继承/60-多重继承示例1.cpp
2-代码/day10-继承/61-菱形继承示例.cpp

1、继承的概念

继承就是在已有类的基础上创建新类。

已有类:基类,也叫父类。
新类:派生类,也叫子类。

继承的作用:

1. 复用已有代码。
2. 减少重复成员。
3. 描述类型之间的关系。

示例:

学生 -> 大学生、中学生、小学生
计算机 -> 台式机、笔记本、平板
车 -> 小汽车、公交车、洒水车

语法:

class 派生类名 : 继承方式 基类名
{
    派生类新增成员;
};

例如:

class Derived : public Base
{
};

根据基类数量,继承可以分为:

单继承:只有一个基类。
多重继承:有两个或两个以上基类。

注意:

class 默认是 private 继承。
struct 默认是 public 继承。
实际写代码时建议把继承方式明确写出来。

2、继承方式和访问权限

继承方式决定基类成员到派生类之后的访问权限。

三种继承方式:

public      公有继承
private     私有继承
protected   保护继承

基类成员自身也有三种访问权限:

public      类内可以访问,类外可以访问。
protected   类内可以访问,派生类内可以访问,类外不能访问。
private     只有本类内部可以访问,派生类和类外都不能直接访问。

公有继承:

基类 public 成员    -> 派生类 public 成员
基类 protected 成员 -> 派生类 protected 成员
基类 private 成员   -> 派生类不可直接访问

私有继承:

基类 public 成员    -> 派生类 private 成员
基类 protected 成员 -> 派生类 private 成员
基类 private 成员   -> 派生类不可直接访问

保护继承:

基类 public 成员    -> 派生类 protected 成员
基类 protected 成员 -> 派生类 protected 成员
基类 private 成员   -> 派生类不可直接访问

重点:

不管哪种继承方式,基类 private 成员都不能在派生类中直接访问。
继承方式主要影响 public 和 protected 成员在派生类中的权限。

3、公有继承规则

54-继承规则验证.cpp 中:

class Base
{
public:
    int x;
private:
    int y;
protected:
    int z;
};

class Derived : public Base
{
public:
    void setValue()
    {
        x = 1;
        // y = 2; // 错误,派生类不能直接访问基类 private 成员
        z = 3;
    }
};

类外使用:

Derived d1;
d1.x = 1;   // 可以,public 继承后 x 仍然是 public
// d1.y = 2; // 错误,private 成员类外不能访问
// d1.z = 3; // 错误,protected 成员类外不能访问

结论:

public 继承保持基类 public/protected 的权限级别。
public 继承常用于表达 is-a 关系。

4、私有继承规则

55-私有继承示例.cpp 中:

class Derived : private Base
{
public:
    void setValue()
    {
        x = 1;
        // y = 2; // 错误
        z = 3;
    }
};

Derived 内部:

x 可以访问,因为 Base::x 原来是 public。
z 可以访问,因为 Base::z 原来是 protected。
y 不可以直接访问,因为 Base::y 是 private。

但是在类外:

Derived d1;
// d1.x = 1; // 错误,private 继承后 x 变成 Derived 的 private 成员
// d1.z = 3; // 错误,protected/private 类外都不能访问

再派生一层:

class Derived2 : public Derived
{
public:
    void test()
    {
        // x = 10; // 错误,x 在 Derived 中已经变成 private
        // z = 30; // 错误,z 在 Derived 中已经变成 private
    }
};

结论:

private 继承会把基类 public/protected 成员变成派生类 private 成员。
下一层派生类不能再直接访问这些成员。

5、继承中的构造和析构

构造函数和析构函数不能被继承。

基类部分由基类构造函数初始化。
派生类新增部分由派生类构造函数初始化。

如果基类有默认构造函数:

创建派生类对象时,会自动调用基类默认构造函数。

如果基类没有默认构造函数,或者想调用带参数构造函数:

必须在派生类构造函数初始化列表中显式调用基类构造函数。

示例:

class Derived : public Base
{
public:
    Derived(int x, int y, int z) : Base(x, y, z), a(0)
    {
        cout << "Derived(int,int,int)" << endl;
    }

private:
    int a;
    Base b;
};

这里:

Base(x, y, z) 初始化继承来的基类子对象。
a(0) 初始化派生类自己的成员。
Base b 是成员对象,会自动调用 Base 的构造函数。

创建派生类对象时,构造顺序:

1. 基类构造函数。
2. 成员对象构造函数。
3. 派生类构造函数。

销毁派生类对象时,析构顺序相反:

1. 派生类析构函数。
2. 成员对象析构函数。
3. 基类析构函数。

重点:

先构造里面,再构造外面。
先析构外面,再析构里面。

6、继承中的同名成员

派生类可以定义和基类同名的成员。

如果出现同名成员,派生类自己的成员会隐藏基类的同名成员。

示例:

class Base
{
public:
    void setX(int x);
    int getX() const;

protected:
    int x;
};

class Derived : public Base
{
public:
    void setX(int x);
    int getX() const;

protected:
    int x;
};

使用:

Derived d1;
d1.setX(100);              // 调用 Derived::setX()
cout << d1.getX() << endl; // 调用 Derived::getX()

如果想访问基类中被隐藏的成员,需要加作用域:

cout << d1.Base::getX() << endl;

注意:

函数隐藏只看函数名。
只要派生类有同名函数,基类中同名函数都会被隐藏。
这和函数重载不同。

7、公有派生类与基类的关系

公有继承表示 is-a 关系。

Derived 是一种 Base。
需要 Base 的地方,可以使用 Derived。

常见用法:

Derived d1(100, 200);

Base b1 = d1; // 用派生类对象初始化基类对象
b1 = d1;      // 用派生类对象给基类对象赋值

Base *p1 = &d1; // 基类指针指向派生类对象
Base &r = d1;   // 基类引用绑定派生类对象

对象切片:

Base b1 = d1;

这句只会保留 Base 部分,Derived 新增的 y 会被切掉。

基类指针和引用:

Base *p1 = &d1;
p1->print();

Base &r = d1;
r.print();

当前代码里 print() 不是虚函数,所以:

通过 Base* 或 Base& 调用 print(),调用的是 Base::print()。
不会自动调用 Derived::print()。

如果要让基类指针根据实际对象类型调用派生类函数,需要后面学习的虚函数和多态。


8、类适配器

Day9 中用组合实现了对象适配器:

class Stack
{
private:
    List _list;
};

Day10 中用私有继承实现类适配器:

class LinkStack : private List
{
public:
    void push(std::string s)
    {
        push_back(s);
    }

    void pop()
    {
        pop_back();
    }

    std::string top() const
    {
        return back();
    }

    bool empty() const
    {
        return List::empty();
    }
};

为什么这里是私有继承?

LinkStack 不是一种 List。
LinkStack 只是用 List 的能力来实现栈。
栈只应该暴露 push、pop、top、empty 等接口。
不应该把 List 的 push_front、pop_front、getElem 等接口暴露出去。

如果写成公有继承:

class LinkStack : public List
{
};

外部就可以这样破坏栈的规则:

LinkStack s;
s.push_front("hello");
s.pop_front();

这不符合栈“后进先出”的特点。

所以这里用私有继承:

private 继承表示“用基类实现自己”。
public 继承表示“自己是一种基类”。

一句话:

LinkStack 是栈,不是链表;它只是借用 List 实现栈,所以用 private 继承。

9、多重继承

多重继承就是一个派生类同时继承多个基类。

语法:

class Derived : public Base1, public Base2
{
};

示例:

class Base1
{
public:
    void print() const;
};

class Base2
{
public:
    void print() const;
};

class Derived : public Base1, public Base2
{
};

如果两个基类中有同名成员:

Derived d1;
// d1.print(); // 错误,二义性

编译器不知道你想调用 Base1::print() 还是 Base2::print()

解决方式:显式指定作用域。

d1.Base1::print();
d1.Base2::print();

重点:

多重继承容易出现同名成员二义性。
遇到二义性时,用 类名::成员名 指定访问路径。

10、菱形继承和虚继承

菱形继承结构:

    A
   / 
  B   C
    /
    D

普通写法:

class A {};
class B : public A {};
class C : public A {};
class D : public B, public C {};

问题:

D 通过 B 继承了一份 A。
D 通过 C 又继承了一份 A。
所以 D 对象中会有两份 A 子对象。

如果 A 中有成员 x

D d;
// d.x = 10; // 错误,不知道是 B 路线的 A::x,还是 C 路线的 A::x

解决方式:虚继承。

class A
{
public:
    int x;
};

class B : virtual public A
{
};

class C : virtual public A
{
};

class D : public B, public C
{
};

也可以写成:

class B : public virtual A {};
class C : public virtual A {};

虚继承的作用:

保证最顶层的 A 在最终派生类 D 中只保留一份。

虚基类初始化:

class D : public B, public C
{
public:
    D(int x) : A(x)
    {
        cout << "D(int)" << endl;
    }
};

重点:

虚基类 A 由最底层的最终派生类 D 负责初始化。
B 和 C 不再各自初始化一份 A。

代价:

虚继承会让对象内部多出一些隐藏指针,用来找到共享的虚基类。
因此对象大小可能变大,结构也更复杂。

11、Day10 总结

1. 继承是在已有类基础上创建新类。
2. 基类也叫父类,派生类也叫子类。
3. public 继承表示 is-a 关系。
4. private 继承表示用基类实现自己。
5. 基类 private 成员不会因为继承而变成派生类可直接访问。
6. public 继承保留 public/protected 权限。
7. private 继承会把基类 public/protected 成员变成派生类 private 成员。
8. 构造顺序是基类、成员对象、派生类。
9. 析构顺序是派生类、成员对象、基类。
10. 派生类同名成员会隐藏基类同名成员。
11. 访问被隐藏的基类成员要用 Base::成员名。
12. 基类对象接收派生类对象会发生对象切片。
13. 当前没有虚函数时,Base* 调用的是 Base 的成员函数。
14. 多重继承可能产生同名成员二义性。
15. 菱形继承会让最顶层基类出现多份拷贝。
16. 虚继承可以让菱形继承中的顶层基类只保留一份。

一句话记忆:

Day10 重点:public 继承表示“是一个”,private 继承表示“用来实现”;构造从基类到派生类,析构反过来;菱形继承用 virtual 解决重复基类。


"When faced with uncertainty, ask the spring breeze."