继承和派生知识点总结 C++程序设计与算法笔记总结(四) 北京大学 郭炜(二)

简介: 继承和派生知识点总结 C++程序设计与算法笔记总结(四) 北京大学 郭炜(二)

派生类覆盖基类成员

派生类可以定义一个和基类成员同名的成员,这叫覆盖。在派生类中访问这类成员时,缺省的情况是访问派生类中定义的成员。要在派生类中访问由基类定义的同名成员时,要使用作用域符号::

在C++中,派生类可以覆盖(重写)基类的成员函数或成员变量,从而实现自己的功能。这种覆盖的方式也称为重载。

下面以一个简单的例子说明派生类如何覆盖基类的成员。

#include <iostream>
#include <string>
using namespace std;
// 基类:人
class Person {
public:
    Person(string name, int age) : m_name(name), m_age(age) {}
    virtual void display() { // 基类的display()函数可以被派生类覆盖,使用virtual关键字进行声明
        cout << "姓名:" << m_name << endl;
        cout << "年龄:" << m_age << endl;
    }
private:
    string m_name;
    int m_age;
};
// 派生类:学生
class Student : public Person {
public:
    Student(string name, int age, string school) : Person(name, age), m_school(school) {}
    void display() { // 派生类覆盖了基类的display()函数
        cout << "姓名:" << Person::m_name << endl;
        cout << "年龄:" << Person::m_age << endl;
        cout << "学校:" << m_school << endl;
    }
private:
    string m_school;
};
int main() {
    Person* p1 = new Person("李四", 20); // 基类指针指向基类对象
    p1->display();
    Person* p2 = new Student("张三", 18, "清华大学"); // 基类指针指向派生类对象
    p2->display();
    return 0;
}

在上述代码中,Person是基类,Student是派生类。Student覆盖了Person的display()函数,输出了自己的属性和基类的属性。在main()函数中,指针p1指向基类对象,指针p2指向派生类对象,分别调用了它们的display()函数。

需要注意的是,在使用派生类覆盖基类成员时,需要满足以下条件:

  1. 派生类中的函数名和基类中的函数名相同;
  2. 派生类和基类的函数参数列表相同;
  3. 派生类和基类的函数返回类型相同或者是类型兼容的(如派生类可以返回基类的指针);
  4. 派生类的访问级别不能低于基类的访问级别;
  5. 基类的成员函数必须使用virtual关键字进行声明,以便派生类可以覆盖它。

通过派生类覆盖基类成员,可以实现多态性,使得程序更加灵活和可扩展。在实际编程中,也经常会用到这种技术。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kJNRidXs-1687179456364)(2023-06-19-20-48-26.png)]

类的保护成员

另一种存取权限说明符:protected

• 基类的private成员:可以被下列函数访问

– 基类的成员函数

– 基类的友元函数

• 基类的public成员:可以被下列函数访问

– 基类的成员函数

– 基类的友元函数

– 派生类的成员函数

– 派生类的友元函数

– 其他的函数

• 基类的protected成员:可以被下列函数访问

– 基类的成员函数

– 基类的友元函数

– 派生类的成员函数可以访问当前对象和其它对象的基类的保护成员

在C++中,类的保护成员是指只能在类的成员函数内部和派生类的成员函数中访问,而不能在类外访问的成员。保护成员的访问级别介于公有成员和私有成员之间。

下面以一个简单的例子说明类的保护成员的使用。

#include <iostream>
#include <string>
using namespace std;
// 基类:动物
class Animal {
public:
    Animal(string name, string color) : m_name(name), m_color(color) {}
    void display() {
        cout << "名称:" << m_name << endl;
        cout << "颜色:" << m_color << endl;
        cout << "年龄:" << m_age << endl; // 可以在基类成员函数中访问保护成员
    }
protected:
    int m_age; // 保护成员
private:
    string m_name;
    string m_color;
};
// 派生类:狗
class Dog : public Animal {
public:
    Dog(string name, string color, int age) : Animal(name, color) {
        m_age = age; // 可以在派生类的成员函数中访问保护成员
    }
    void bark() {
        cout << "汪汪叫!" << endl;
    }
};
int main() {
    Dog d("旺财", "棕色", 3);
    d.display();
    d.bark();
    // cout << d.m_age << endl; // 保护成员不能在类外访问
    return 0;
}

在上述代码中,Animal是基类,Dog是派生类。基类Animal中定义了一个保护成员m_age,派生类Dog中通过继承可以访问到这个保护成员,因此可以在派生类的成员函数中对m_age进行赋值。在main()函数中,通过Dog的对象d调用了它们的成员函数。

需要注意的是,保护成员可以被派生类访问,但是不能被类外访问。如果在类外访问保护成员,编译器会报错。保护成员的作用在于,它可以在保证封装性的同时,让派生类能够访问基类的成员,从而实现更加灵活和可扩展的类设计。

派生类的构造函数

在C++中,派生类的构造函数可以调用基类的构造函数,以初始化基类的成员变量。派生类的构造函数可以有自己的参数列表,但是必须调用基类的构造函数来初始化基类的成员变量。如果派生类没有显式地调用基类的构造函数,则编译器会默认调用基类的默认构造函数。

下面以一个简单的例子说明派生类的构造函数的使用。

#include <iostream>
#include <string>
using namespace std;
// 基类:人
class Person {
public:
    Person(string name, int age) : m_name(name), m_age(age) {
        cout << "调用了Person的构造函数" << endl;
    }
    void display() {
        cout << "姓名:" << m_name << endl;
        cout << "年龄:" << m_age << endl;
    }
private:
    string m_name;
    int m_age;
};
// 派生类:学生
class Student : public Person {
public:
    Student(string name, int age, string school) : Person(name, age), m_school(school) {
        cout << "调用了Student的构造函数" << endl;
    }
    void display() {
        cout << "姓名:" << Person::m_name << endl;
        cout << "年龄:" << Person::m_age << endl;
        cout << "学校:" << m_school << endl;
    }
private:
    string m_school;
};
int main() {
    Student s("张三", 18, "清华大学");
    s.display();
    return 0;
}

在上述代码中,Person是基类,Student是派生类。Student的构造函数中调用了Person的构造函数,以初始化Person的成员变量。在main()函数中,创建了Student的对象s,并调用了它的成员函数。

需要注意的是,派生类的构造函数必须调用基类的构造函数,以初始化基类的成员变量。在派生类的构造函数内部,可以使用初始化列表来调用基类的构造函数,并对派生类的成员变量进行初始化。如果没有使用初始化列表,则编译器会默认调用基类的默认构造函数和派生类的默认构造函数(如果有的话)。在使用派生类的构造函数时,也需要考虑构造函数的重载、默认参数等问题。

在创建派生类的对象时,需要调用基类的构造函数:初始化派生类对象中从基类继承的成员。在执行一个派生类的构造函数之前,总是先执行基类的构造函数。

• 调用基类构造函数的两种方式

– 显式方式:在派生类的构造函数中,为基类的构造函数提供参数.

derived::derived(arg_derived-list):base(arg_base-list)

– 隐式方式:在派生类的构造函数中,省略基类构造函数时,派生类的构造函数则自动调用基类的默认构造函数.

• 派生类的析构函数被执行时,执行完派生类的析构函数后,自动调用基类的析构函数。

包含成员对象的派生类的构造函数写法

class Skill {
public:
Skill(int n) { }
};
class FlyBug: public Bug {
int nWings;
Skill sk1, sk2;
public:
FlyBug( int legs, int color, int wings);
};
FlyBug::FlyBug( int legs, int color, int wings):
Bug(legs,color),sk1(5),sk2(color) ,nWings(wings) {
}

当一个类包含成员对象时,派生类必须在其构造函数中显式地调用成员对象的构造函数来初始化它们。派生类的构造函数必须使用初始化列表来调用基类的构造函数和成员对象的构造函数。

下面以一个简单的例子说明包含成员对象的派生类的构造函数的写法。

#include <iostream>
#include <string>
using namespace std;
// 基类:人
class Person {
public:
    Person(string name, int age) : m_name(name), m_age(age) {
        cout << "调用了Person的构造函数" << endl;
    }
    void display() {
        cout << "姓名:" << m_name << endl;
        cout << "年龄:" << m_age << endl;
    }
private:
    string m_name;
    int m_age;
};
// 成员对象:地址
class Address {
public:
    Address(string province, string city) : m_province(province), m_city(city) {
        cout << "调用了Address的构造函数" << endl;
    }
    void display() {
        cout << "地址:" << m_province << "省" << m_city << "市" << endl;
    }
private:
    string m_province;
    string m_city;
};
// 派生类:学生
class Student : public Person {
public:
    Student(string name, int age, string school, string province, string city) : Person(name, age), m_school(school), m_address(province, city) {
        cout << "调用了Student的构造函数" << endl;
    }
    void display() {
        cout << "姓名:" << Person::m_name << endl;
        cout << "年龄:" << Person::m_age << endl;
        cout << "学校:" << m_school << endl;
        m_address.display();
    }
private:
    string m_school;
    Address m_address; // 包含成员对象
};
int main() {
    Student s("张三", 18, "清华大学", "北京", "海淀");
    s.display();
    return 0;
}

在上述代码中,Person是基类,Address是成员对象,Student是派生类。在Student的构造函数中,需要使用初始化列表来调用Person的构造函数和Address的构造函数,以初始化它们的成员变量。在main()函数中,创建了Student的对象s,并调用了它的成员函数。

需要注意的是,在派生类的构造函数中调用成员对象的构造函数,需要在初始化列表中使用成员对象的名称来进行调用,而不是使用构造函数的名称来进行调用。此外,也需要考虑构造函数的重载、默认参数等问题。

封闭派生类对象的构造函数的执行顺序

在创建派生类的对象时:

  1. 先执行基类的构造函数,用以初始化派生类对象中从基类继承的成员;
  2. 再执行成员对象类的构造函数,用以初始化派生类对象中成员对象。
  3. 最后执行派生类自己的构造函数

在派生类对象消亡时:

  1. 先执行派生类自己的析构函数
  2. 再依次执行各成员对象类的析构函数
  3. 最后执行基类的析构函数
    析构函数的调用顺序与构造函数的调用顺序相反。

public继承的赋值兼容规则

class base { };
class derived : public base { };
base b;
derived d;

• 如果派生方式是 private或protected,则上述三条不可行。

在C++中,公有继承(public inheritance)的赋值兼容规则遵循“is-a”关系,即派生类对象可以赋值给基类对象,但是反过来基类对象不能赋值给派生类对象。

具体来说,如果一个基类指针或引用指向一个派生类对象,那么该指针或引用可以调用基类对象中的成员函数,但是不能调用派生类对象中新增的成员函数。如果需要调用派生类对象中新增的成员函数,可以使用动态类型识别(dynamic_cast)来实现。

下面以一个简单的例子说明公有继承的赋值兼容规则。

#include <iostream>
#include <string>
using namespace std;
// 基类:人
class Person {
public:
    Person(string name, int age) : m_name(name), m_age(age) {}
    void display() {
        cout << "姓名:" << m_name << endl;
        cout << "年龄:" << m_age << endl;
    }
private:
    string m_name;
    int m_age;
};
// 派生类:学生
class Student : public Person {
public:
    Student(string name, int age, string school) : Person(name, age), m_school(school) {}
    void display() {
        cout << "姓名:" << Person::m_name << endl;
        cout << "年龄:" << Person::m_age << endl;
        cout << "学校:" << m_school << endl;
    }
    void study() {
        cout << "学生正在学习" << endl;
    }
private:
    string m_school;
};
int main() {
    Person* p1 = new Student("张三", 18, "清华大学");
    p1->display(); // 调用基类成员函数
    // p1->study(); // 错误,无法调用派生类新增的成员函数
    Person& p2 = *(new Student("李四", 19, "北京大学"));
    p2.display(); // 调用基类成员函数
    // p2.study(); // 错误,无法调用派生类新增的成员函数
    return 0;
}

在上述代码中,Person是基类,Student是派生类。在main()函数中,使用基类指针p1和基类引用p2分别指向了一个派生类对象。通过p1和p2可以调用基类对象中的成员函数,但是不能调用派生类对象中新增的成员函数。

需要注意的是,公有继承的赋值兼容规则只适用于指针和引用,而不适用于对象。如果需要使用基类对象来初始化派生类对象,需要使用转换操作符(static_cast、dynamic_cast等)来进行类型转换。

直接基类和间接基类

在C++中,一个派生类可以从一个或多个基类中继承成员变量和成员函数。基类可以分为直接基类和间接基类两种类型。

直接基类是指在派生类的声明中明确指定的基类。例如:

class B { ... };
class D : public B { ... };

在上述代码中,B是D的直接基类。

间接基类是指在派生类的基类中递归地包含其他基类。例如:

class B { ... };
class C : public B { ... };
class D : public C { ... };

在上述代码中,C是D的直接基类,B是D的间接基类。因为D继承了C中的成员变量和成员函数,而C又继承了B中的成员变量和成员函数。

需要注意的是,派生类在继承基类时,只能通过直接基类来访问其成员函数和成员变量,不能通过间接基类来访问。如果需要访问间接基类中的成员函数和成员变量,可以通过直接基类的成员函数来调用。

下面以一个简单的例子说明直接基类和间接基类的概念。

#include <iostream>
using namespace std;
class A {
public:
    void f() {
        cout << "调用了A的f函数" << endl;
    }
};
class B : public A {
public:
    void g() {
        cout << "调用了B的g函数" << endl;
    }
};
class C : public B {
public:
    void h() {
        cout << "调用了C的h函数" << endl;
    }
};
int main() {
    C c;
    c.f(); // 调用直接基类A的成员函数
    c.g(); // 调用直接基类B的成员函数
    c.h(); // 调用自身的成员函数
    return 0;
}

在上述代码中,A是C的间接基类,B是C的直接基类。在main()函数中,创建了C的对象c,并调用了它的成员函数。可以看到,只有直接基类的成员函数和自身的成员函数可以被调用,间接基类的成员函数无法被直接调用。需要通过直接基类的成员函数来间接调用。

目录
相关文章
|
1月前
|
算法
【❤️算法笔记❤️】-每日一刷-19、删除链表的倒数第 N个结点
【❤️算法笔记❤️】-每日一刷-19、删除链表的倒数第 N个结点
69 1
|
1月前
|
算法 索引
❤️算法笔记❤️-(每日一刷-141、环形链表)
❤️算法笔记❤️-(每日一刷-141、环形链表)
46 0
|
1月前
|
算法
【❤️算法笔记❤️】-(每日一刷-876、单链表的中点)
【❤️算法笔记❤️】-(每日一刷-876、单链表的中点)
44 0
|
1月前
|
算法
【❤️算法笔记❤️】-每日一刷-23、合并 K 个升序链表
【❤️算法笔记❤️】-每日一刷-23、合并 K 个升序链表
32 0
|
1月前
|
存储 算法
【❤️算法笔记❤️】-每日一刷-21、合并两个有序链表
【❤️算法笔记❤️】-每日一刷-21、合并两个有序链表
93 0
|
1月前
|
算法 API 计算机视觉
人脸识别笔记(一):通过yuface调包(参数量54K更快更小更准的算法) 来实现人脸识别
本文介绍了YuNet系列人脸检测算法的优化和使用,包括YuNet-s和YuNet-n,以及通过yuface库和onnx在不同场景下实现人脸检测的方法。
46 1
|
1月前
|
JSON 算法 数据可视化
测试专项笔记(一): 通过算法能力接口返回的检测结果完成相关指标的计算(目标检测)
这篇文章是关于如何通过算法接口返回的目标检测结果来计算性能指标的笔记。它涵盖了任务描述、指标分析(包括TP、FP、FN、TN、精准率和召回率),接口处理,数据集处理,以及如何使用实用工具进行文件操作和数据可视化。文章还提供了一些Python代码示例,用于处理图像文件、转换数据格式以及计算目标检测的性能指标。
63 0
测试专项笔记(一): 通过算法能力接口返回的检测结果完成相关指标的计算(目标检测)
|
1月前
|
算法
❤️算法笔记❤️-(每日一刷-160、相交链表)
❤️算法笔记❤️-(每日一刷-160、相交链表)
18 1
|
2月前
|
存储 算法 安全
超级好用的C++实用库之sha256算法
超级好用的C++实用库之sha256算法
101 1
|
1月前
|
算法 数据处理 C++
c++ STL划分算法;partition()、partition_copy()、stable_partition()、partition_point()详解
这些算法是C++ STL中处理和组织数据的强大工具,能够高效地实现复杂的数据处理逻辑。理解它们的差异和应用场景,将有助于编写更加高效和清晰的C++代码。
23 0
下一篇
无影云桌面