博主头像
小雨淅沥

Some things were meant to be.

OOP课程笔记:第三章 继承和派生类

第三章 继承和派生类

继承性是面向对象程序设计中最重要的机制,这种机制提供了无限重复利用程序资源的一种途径。

通过C++语言中的继承机制,可以扩充和完善旧的程序设计以适应新的需求。这样不仅可以节省程序开发的时间和资源,并且为未来程序增添了新的资源。

在C++中所谓“继承”就是在一个已存在的类的基础上建立一个新的类

已存在的类称为 基类(base class)父类(father class)

新建立的类称为 派生类(derived class)子类(son class)


1.派生新增内容

派生类则除了继承基类的所有可引用成员变量和成员函数

还可另外定义本身的成员变量和处理这些变量的函数


语法格式:

Access 是继承方式,有 private , public , protected 三种

class ClassName:<Access>BaseClassName{
private:
......
;//私有成员说明
public:
......
;//公有成员说明
protected:
......
;//保护成员说明
}

2.派生方式及访问权限

在派生时,原有属性的访问权限在派生类中有所不同。

基类成员类型public 继承protected 继承private 继承
publicpublicprotectedprivate
protectedprotectedprotectedprivate
private不可访问不可访问不可访问

总结:

  1. public 继承不改变基类属性
  2. protected 继承将原来的 public 降级为 protected ,其余不变
  3. private 继承会将所有的属性变为private ,基类的 private 仍然不可访问
  4. 派生类属于原有基类外, private 永远不可访问

3.抽象类

当定义了一个类,这个类只能用作基类来派生出新的类,而不能用这种类来定义对象时,称这种类为抽象类。

类的构造函数或析构函数的访问权限定义为保护时,这种类为抽象类

class A{
    int x, y;
protected:
    A(int a, int b){
        x = a;
        y = b;
    } // 基类初始化
public:
    void ShowA() { cout << "x=" << x << '\t' << "y=" << y << '\n'; }
};
class B : public A{
    int m;
    A a1(1, 1);     // 不合法,派生类中不允许创建基类对象(因为属于类外)
public:
    B(int a, int b, int c) : A(a, b) // 可以在派生类中调用A的构造函数
    {
        m = c;
    }
    void Show(){
        cout << "m=" << m << '\n';
        ShowA();
    }
};
void main(void){
    B b1(1, 2, 3);     // 可以定义派生类对象
    b1.Show();
    A aa(2, 3);        // 不合法,不允许创建A的对象
}

4.多重继承

可以用多个基类来派生一个类

class ClassName:<Access> Class1, <Access> Class2 {
private: ......
    ;//私有成员说明;
public: ......
    ;//公有成员说明;
protected: ......
    ;//保护的成员说明;
};
class D:public A,protected B,private C{
    ....//派生类中新增加成员
};

5.初始化对象成员

构造函数不能被继承,派生类的构造函数必须调用对象所属类的构造函数来初始化对象成员。

派生类构造函数的调用顺序如下:

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

当撤销派生类对象时,析构函数的调用正好相反

当调用基类的构造函数时,根据继承参数表中顺序来调用。

class Base1{
    int x;
public:
    Base1(int a){
        x = a;
        cout << "调用基类1的构造函数 !\n ";
    }
    ~Base1() { cout << "调用基类1的析构函数 !\n "; }
};
class Base2{
    int y;
public:
    Base2(int a){
        y = a;
        cout << "调用基类2的构造函数 !\n ";
    }
    ~Base2() { cout << "调用基类2的析构函数 !\n "; }
};
class Derived : public Base2, public Base1
{
    int z;
public:
    Derived(int a, int b) : Base1(a), Base2(20)
    {
        z = b;
        cout << "调用派生类的构造函数 !\n ";
    }
    ~Derived() { cout << "调用派生类的析构函数 !\n "; }
};
int main(void){
    Derived c(100, 200);    
    return 0;
    // 运行结果如下:
    // 调用基类2的构造函数
    // 调用基类1的构造函数
    // 调用派生类的构造函数
    // 调用派生类的析构函数
    // 调用基类1的析构函数
    // 调用基类2的析构函数
}

在使用参数表初始化时

基类成员构造用基类名,对象成员构造用对象名

下方例子中,基类成员 a , b 都使用对应的基类名称 Base1 , Base2 进行初始化

对象成员 b1 , b2 使用对象名初始化

class Derived : public Base2, public Base1
{
    int z;
    Base1 b1, b2;
public:
    Derived(int a, int b) : Base1(a), Base2(20), b1(200), b2(a + b) 
     //a,b是基类成员用基类名称,对象成员用对象名称
    {
        z = b;
        cout << "调用派生类的构造函数!\n";
    }
    ~Derived() { cout << "调用派生类的析构函数!\n"; }
};
void main(void)
{
    Derived c(100, 200);
}

6.基类与对象成员

从基类中继承的成员和继承后新定义的成员会产生各种情况


成员名称冲突

当派生类中成员的名称和基类中名称冲突时,可以使用类作用符 :: 限定

若不加类作用符限制,则优先调用派生类中的成员

在下例中,使用 c1.y 调用派生类中的 y,使用 c1.B::y 调用基类中的 y

class A{
public:
    int x;
    void Show() { cout << "x=" << x << '\n'; }
};
class B{
public:
    int y;
    void Show() { cout << "y=" << y << '\n'; }
};
class C : public A, public B{
public:
    int y; // 类B和类C均有y的成员
};
void main(void){
    C c1;
    c1.x = 100;
    c1.y = 200;    // 给派生类中的y赋值
    c1.B::y = 300; // 给基类B中的y赋值
    c1.A::Show();
    c1.B::Show();                    // 用作用域运算符限定调用的函数
    cout << "y=" << c1.y << '\n';    // 输出派生类中的y值
    cout << "y=" << c1.B::y << '\n'; // 输出基类B中的y值
}

基类派生次数

任一基类在派生类中只能继承一次,否则会造成成员名的冲突

若在派生类中,确实要有二个以上基类的成员,则可用基类的二个对象作为派生类的成员

把一个类作为派生类的基类或把一个类的对象作为一个类的成员,在使用上是有区别的:

在派生类中可直接使用基类的成员(访问权限允许的话),但要使用对象成员的成员时,必须在对象名后加上成员运算符“.”和成员名。

class Dot{
public:
    float x, y;
    Dot(float a = 0, float b = 0){
        x = a;
        y = b;
    }
    void Show(void) { cout << "x=" << x << '\t' << "y=" << y << endl; }
};
class Line : public Dot{
    Dot d1, d2;        // 在 Line 类中定义两个基类Dot的对象
public:
    Line(Dot dot1, Dot dot2) : d1(dot1), d2(dot2)    // 使用两个基类的对象作为初始化参数
    {
        x = (d1.x + d2.x) / 2;
        y = (d1.y + d2.y) / 2;
    }
    void Showl(void){
        cout << "Dot1: ";
        d1.Show();
        cout << "Dot2: ";
        d2.Show();
        cout << "Length=" << sqrt((d1.x - d2.x) * (d1.x - d2.x) + (d1.y - d2.y) * (d1.y - d2.y)) << endl;
        cout << "Center: " << "x=" << x << '\t' << "y=" << y << endl;
    }
};
void main(void){
    float a, b;
    cout << "Input Dot1: \n";
    cin >> a >> b;
    Dot dot1(a, b); // 调用Dot的构造函数
    cout << "Input Dot2: \n";
    cin >> a >> b;
    Dot dot2(a, b);
    Line line(dot1, dot2);
    line.Showl();
}

7.对象赋值规则

可以将派生类对象的值赋给基类对象,反之不行

只是给从基类继承来的成员赋值

Dot dot;
Line line;
dot = line; 
line = dot; // 不合法

指针赋值规则

可以将一个派生类对象的地址赋给基类的指针变量

此时的指针只能指向从基类继承来的数据

Base *basep;    // 基类指针
basep = &b;        // 指向基类对象
basep = &d;        // 指向派生类对象

引用初始化规则

派生类对象可以初始化基类的引用

Derive d;
Base &basei=d; // 允许使用派生类的对象作为基类的引用

8.派生类向基类的类型转换

赋值兼容规则(向上类型转换)

在任何需要基类对象(或地址)的地方,都可以由其公有派生类的对象(或地址)代替。

公有派生类就是基类的子类型,公有派生类的对象可以自动转换为基类类型,基类的指针和引用可以指向派生类的对象


存在继承关系的类型之间的转换规则

从派生类向基类的类型转换只对指针引用类型有效,对象转换存在切片现象

私有继承保护继承不能应用派生类到基类的类型转换


对象向上类型转换和对象切片

派生类对象在向基类对象转换时,会发生“对象切片”现象——派生类对象被“切片”,直到剩下适合的基类子对象

对象切片实际上是在将它复制到一个新对象时,去掉原来对象的一部分,因此,不建议使用


指针和引用向上类型转换

使用指针向上类型转换只是简单地改变地址的类型,是用不同的方式解读同一段内存空间中的内容,不会真正切除派生类对象的多余部分

引用向上类型转换与指针相似,指针和引用在进行转换时不会发生对象切片的现象

9.虚基类

如果同一个公共的基类在派生类中产生多个拷贝,不仅多占用了存储空间,而且可能会造成多个拷贝中的数据不一致和模糊的引用。

在多重派生的过程中,若使公共基类在派生类中只有一个拷贝,则可将这种基类说明为虚基类

虚基类在基类的类名前加上关键字 virtual

class B:public virtual A{
public:
    int y;
    B(int a = 0, int b = 0):A(b) { y=a;}
};

虚基类构造函数

如果虚基类没有缺省的构造函数,则在每一个派生类的构造函数中都必须有对虚基类构造函数的调用 (且首先调用)。

一般在列表参数中直接显式调用虚基类构造函数

class A{
public:
    int x;
    A(int a = 0) { x = a; }
};
class B : public virtual A{
public:
    int y;
    B(int a = 0, int b = 0) : A(a) { y = b; }
};
class C : public virtual A{
public:
    int z;
    C(int a = 0, int c = 0) : A(a) { z = c; }
};
class D : public B, public C{
public:
    int dx;
    D(int a1, int b, int c, int d, int a2) : B(a1, b), C(a2, c), A(a2)    // 在派生类中调用虚基类构造函数
    {
        dx = d;
    }
};

10.覆盖与隐藏

在派生类中重定义基类接口中的成员函数,会发生覆盖或者隐藏两种情况

形式参数表与返回类型能否处理基类信息
覆盖(override)保持与基类中一致可以
隐藏(name hiding)改变了函数的参数表或返回类型不可以
class B{
public:
    void f(int) {; }
    void f() {; }
    void g(char) { ; }
    void h() {; }
};
class D1 : public B{
public:
    void h() {; }    // 重定义,覆盖了B中的h()
};
class D2 : public B{
public:
    void f() {; }    // 覆盖了B中的f(),同时隐藏了f(int)
    void g() {; }    // 隐藏了B中的g(char)
};

类作用域和名字隐藏

C++中每个类都定义了一个类作用域,派生类的作用域是包含在其基类作用域之内的
作用域的同名隐藏原则:内层作用域中的名字将隐藏外围作用域中相同的名字
作用域中名字的查找规则:由内层向外层逐步查找,找到即隐藏

在下例子中,作用域是 D2 < D1 < B
因此 D2 优先会覆盖 D1 中的函数 f()

class B {
public:
    virtual void f() { cout << "B::f" << endl; }
};

class D1 : public B {
public:
    void f() override { cout << "D1::f" << endl; }  // 覆盖 B::f
};

class D2 : public D1 {
public:
    void f() override { cout << "D2::f" << endl; }  // 覆盖 D1::f
};
OOP课程笔记:第三章 继承和派生类
https://www.rainerseventeen.cn/index.php/Code-Basic/14.html
本文作者 Rainer
发布时间 2025-10-18
许可协议 CC BY-NC-SA 4.0

评论已关闭