C++ 继承和派生 万字长文超详解
本文章内容来源于C++课堂上的听课笔记
继承和派生基础
继承是一种概念,它允许一个新创建的类(称为子类或派生类)获取另一个已经存在的类(称为父类或基类)的属性和行为。这就好比是子类继承了父类的特征。想象一下,如果有一个“动物”类,它有一些通用的特征和行为,然后我们有一个“猫”类,它可以继承“动物”类的这些通用特征,如四条腿、呼吸等。这样,“猫”类就继承了“动物”类
派生是指从已有类创建新类的过程。在这个过程中,新类继承了已有类的属性和方法。派生类可以添加新的属性或方法,也可以覆盖或修改继承的属性和方法。这就允许我们基于现有的代码构建和扩展新的功能。在上面的例子中,“猫”类就是通过派生(或继承)从“动物”类创建出来的
继承分为单继承和多重继承,如果一个派生类只有一个基类,就是单继承,否则是多重继承
派生类是基类的具体化,基类是派生类的抽象
假设已经声明了一个基类A,想要声明派生类B的一般形式为:
举个例子:
#include <iostream>
using namespace std;
// 定义基类 Animal
class Animal {
public:
void makeSound() {
cout << "Some generic sound...\n";
}
};
// 定义派生类 Dog,它继承了 Animal 类
class Dog : public Animal {
public:
void bark() {
cout << "Woof! Woof!\n";
}
};
int main() {
// 创建一个 Dog 对象
Dog myDog;
// 调用继承自 Animal 类的方法
myDog.makeSound();
// 调用派生类 Dog 自己的方法
myDog.bark();
return 0;
}
对于继承方式,一般有三种,分别是public,protected,private,如果不写继承方式,默认是private
派生类中的成员包括从基类继承的成员和新增的成员两大部分
构造派生类时,会涉及下面的一些事件:
1.从基类接收成员。派生类把基类全部的成员接收过来
2.调整从基类接收的成员。接收基类成员是程序人员不能选择的,但是程序人员可以对这些成员作某些调整。如: 通过指定继承方式,改变基类成员在派生类中的访问属性; 可以在派生类中声明一个与基类成员同名的成员,覆盖基类的同名成员(与重载是不同的,这里叫做重写override)
3.在声明派生类时增加的成员
4.在声明派生类时,一般还应当自己定义派生类的构造函数和析构函数,因为构造函数和析构函数是不能从基类继承的
继承方式
上面说过,在声明派生类的时候要加一个名叫“继承方式”的东西,这里就对其进行相关的解释
公有继承
基类的公用成员和保护成员在派生类中仍然保持其公用成员和保护成员的属性; 而基类的私有成员在派生类中并没有成为派生类的私有成员,它仍然是基类的私有成员,只有基类的成员函数可以引用它,而不能被派生类的成员函数引用,因此就成为派生类中的不可访问的成员
#include <iostream>
using namespace std;
// 定义基类 Vehicle
class Vehicle {
public:
void startEngine() {
cout << "Engine started.\n";
}
void stopEngine() {
cout << "Engine stopped.\n";
}
private:
int m;
};
// 定义派生类 ElectricCar,它公有继承自 Vehicle
class ElectricCar : public Vehicle {
public:
void chargeBattery() {
cout << "Battery is charging.\n";
cout << m << endl;//无法访问
}
};
int main() {
// 创建一个 ElectricCar 对象
ElectricCar myElectricCar;
// 使用继承自 Vehicle 类的方法
myElectricCar.startEngine();
// 使用 ElectricCar 类自己的方法
myElectricCar.chargeBattery();
// 使用继承自 Vehicle 类的方法
myElectricCar.stopEngine();
return 0;
}
私有继承
私有基类的公用成员和保护成员在派生类中的访问属性相当于派生类中的私有成员,即派生类的成员函数能访问它们,在派生类外不能访问它们。 私有基类的私有成员在派生类中成为不可访问的成员,只有基类的成员函数可以引用它们
对于不需要再往下继承的类的功能可以用私有继承方式把它隐蔽起来,这样,下一层的派生类无法访问它的任何成员
#include <iostream>
#include <string>
using namespace std;
// 定义基类 Person
class Person {
private:
string name;
int age;
public:
Person(const string& n, int a) : name(n), age(a) {}
void displayInfo() {
cout << "Name: " << name << ", Age: " << age << endl;
}
};
// 定义派生类 Employee,私有继承自 Person
class Employee : private Person {
private:
string jobTitle;
public:
// 在 Employee 构造函数中初始化 Person 的成员
Employee(const string& n, int a, const string& title)
: Person(n, a), jobTitle(title) {}
void displayEmployeeInfo() {
// 在 Employee 类的成员函数中,可以访问继承的 Person 成员
displayInfo();
cout << "Job Title: " << jobTitle << endl;
}
};
int main() {
// 创建一个 Employee 对象
Employee employee("John", 30, "Software Engineer");
// 无法直接访问 Person 的私有成员
// employee.displayInfo(); // 这行代码会导致编译错误
// 但可以通过 Employee 的成员函数间接访问继承的 Person 成员
employee.displayEmployeeInfo();
return 0;
}
保护继承
保护成员:保护成员和私有成员类似,都不能直接被外界访问,但唯一不同的是保护成员可以被对应类的派生类的成员函数所访问
保护基类的公用成员和保护成员在派生类中都成了保护成员,其私有成员仍为基类私有。也就是把基类原有的公用成员也保护起来,不让类外任意访问
三种继承方式总结如下
派生类的构造函数和析构函数
上面说过子类不能继承父类的构造函数和析构函数,但有一种方法是可以让子类去调用父类的构造函数的,这样,就可以同时初始化父类成员和子类成员了
一般形式如下:
派生类构造函数名(总参数表列): 基类构造函数名(参数表列)
{派生类中新增数据成员初始化语句}
举一个例子:
Student1(int n,string nam,char s,int a,string ad):Student(n,nam,s)
//派生类构造函数
{ age=a; //在函数体中只对派生类新增的数据成员初始化
addr=ad;
}
其中age,addr是派生类新增成员
也可以在类外实现派生类的构造函数,在类内仅仅声明即可
Student1∷Student1(int n,string nam,char s,int a,string ad):Student(n,nam,s)
{age=a; addr=ad;}
在类中对派生类构造函数作声明时,不包括基类构造函数名及其参数表列
不仅可以利用初始化表对构造函数的数据成员初始化,而且可以利用初始化表调用派生类的基类构造函数,实现对基类数据成员的初始化。也可以在同一个构造函数的定义中同时实现这两种功能。
Student1(int n, string nam,char s,int a, string ad):Student(n,nam,s),age(a),addr(ad){}
在派生类对象释放时,先执行派生类析构函数~Student1( ),再执行其基类析构函数~Student( )
有子对象的构造函数和组合
什么是子对象?简单来说就是一个类的成员中包含一个类对象,这个类对象就是子对象(subobject),这个子对象也是可以直接在构造函数中一起初始化的,非常方便
注意,如果这个子对象是本派生类的基类,还是正常的继承
如果这个子对象是另外一个已定义的类称为类的组合
类的组合和继承一样,是软件重用的重要方式。组合和继承都是有效地利用已有类的资源。但二者的概念和用法不同。继承是纵向的,组合是横向的
#include <iostream>
#include <string>
using namespace std;
// 定义基类 Student
class Student {
private:
string name;
int age;
public:
// 基类 Student 的构造函数
Student(const string& n, int a) : name(n), age(a) {
cout << "Student constructor called." << endl;
}
void displayInfo() {
cout << "Name: " << name << ", Age: " << age << endl;
}
};
// 定义子对象 Grade
class Grade {
private:
char grade;
public:
// Grade 构造函数
Grade(char g) : grade(g) {
cout << "Grade constructor called." << endl;
}
void displayGrade() {
cout << "Grade: " << grade << endl;
}
};
// 定义派生类 Student1,包含一个子对象 Grade
class Student1 : public Student {
private:
Grade studentGrade;
public:
// Student1 构造函数,调用基类 Student 和子对象 Grade 的构造函数
Student1(const string& n, int a, char g)
: Student(n, a), studentGrade(g) {
cout << "Student1 constructor called." << endl;
}
// Student1 类的成员函数,可以访问基类 Student 和子对象 Grade 的成员
void displayStudent1Info() {
displayInfo(); // 访问基类成员函数
studentGrade.displayGrade(); // 访问子对象成员函数
}
};
int main() {
// 创建一个 Student1 对象
Student1 student1("Alice", 20, 'A');
// 调用 Student1 类的成员函数,间接调用基类 Student 和子对象 Grade 的成员函数
student1.displayStudent1Info();
return 0;
}
执行派生类构造函数的顺序是: ① 调用基类构造函数,对基类数据成员初始化; ② 调用子对象构造函数,对子对象数据成员初始化; ③ 再执行派生类构造函数本身,对派生类数据成员初始化
基类构造函数和子对象的次序可以是任意的,如果有多个子对象,派生类构造函数的写法依此类推,应列出每一个子对象名及其参数表列。
多级派生时的构造函数
#include <iostream>
#include <string>
using namespace std;
// 基类 Person
class Person {
private:
string name;
public:
// 基类构造函数
Person(const string& n) : name(n) {
cout << "Person constructor called." << endl;
}
void displayName() {
cout << "Name: " << name << endl;
}
};
// 派生类 Student,继承自 Person
class Student : public Person {
private:
int studentID;
public:
// Student 构造函数,调用基类 Person 构造函数
Student(const string& n, int id) : Person(n), studentID(id) {
cout << "Student constructor called." << endl;
}
void displayStudentInfo() {
displayName(); // 访问基类成员函数
cout << "Student ID: " << studentID << endl;
}
};
// 派生类 GraduateStudent,继承自 Student
class GraduateStudent : public Student {
private:
string researchTopic;
public:
// GraduateStudent 构造函数,调用直接基类 Student 构造函数
GraduateStudent(const string& n, int id, const string& topic)
: Student(n, id), researchTopic(topic) {
cout << "GraduateStudent constructor called." << endl;
}
void displayGraduateStudentInfo() {
displayStudentInfo(); // 访问直接基类 Student 的成员函数
cout << "Research Topic: " << researchTopic << endl;
}
};
int main() {
// 创建一个 GraduateStudent 对象
GraduateStudent gradStudent("John Doe", 12345, "Machine Learning");
// 调用 GraduateStudent 类的成员函数,间接调用基类 Person 和直接基类 Student 的成员函数
gradStudent.displayGraduateStudentInfo();
return 0;
}
在 main
函数中创建 GraduateStudent
对象时,首先调用 Person
类的构造函数,然后调用 Student
类的构造函数,最后调用 GraduateStudent
类的构造函数
派生类构造函数和析构函数注意点
1.当不需要对派生类新增的成员进行任何初始化操作时,派生类构造函数的函数体可以为空,即构造函数是空函数
2.如果在基类中没有定义构造函数,或定义了没有参数的构造函数,那么在定义派生类构造函数时可不写基类构造函数
3.如果在基类和子对象类型的声明中都没有定义带参数的构造函数,而且也不需对派生类自己的数据成员初始化,则可以不必显式地定义派生类构造函数
4.如果在基类或子对象类型的声明中定义了带参数的构造函数,那么就必须显式地定义派生类构造函数
5.如果在基类中既定义无参的构造函数,又定义了有参的构造函数(构造函数重载),则在定义派生类构造函数时,既可以包含基类构造函数及其参数,也可以不包含基类构造函数
6.在执行派生类的析构函数时,系统会自动调用基类的析构函数和子对象的析构函数,对基类和子对象进行清理
7.调用的顺序与构造函数正好相反: 先执行派生类自己的析构函数,对派生类新增加的成员进行清理,然后调用子对象的析构函数,对子对象进行清理,最后调用基类的析构函数,对基类进行清理
多重继承
注意派生类中的构造函数怎么写的
#include <iostream>
#include <string>
using namespace std;
// 基类 Shape(形状)
class Shape {
protected:
int width;
int height;
public:
// Shape 类构造函数
Shape(int w, int h) : width(w), height(h) {
cout << "Shape constructor called." << endl;
}
void displayShapeInfo() {
cout << "Shape - Width: " << width << ", Height: " << height << endl;
}
};
// 基类 Color(颜色)
class Color {
protected:
string color;
public:
// Color 类构造函数
Color(const string& c) : color(c) {
cout << "Color constructor called." << endl;
}
void displayColor() {
cout << "Color: " << color << endl;
}
};
// 派生类 ColoredShape(有颜色的形状),继承自 Shape 和 Color
class ColoredShape : public Shape, public Color {
public:
// ColoredShape 类构造函数,显式调用基类构造函数
ColoredShape(int w, int h, const string& c,int id) : Shape(w, h), Color(c) id(id){
cout << "ColoredShape constructor called." << endl;
}
void displayColoredShapeInfo() {
displayShapeInfo(); // 访问 Shape 类的成员函数
displayColor(); // 访问 Color 类的成员函数
}
int id;
};
int main() {
// 创建一个 ColoredShape 对象
ColoredShape coloredShape(10, 5, "Red");
// 调用 ColoredShape 类的成员函数,间接调用基类 Shape 和 Color 的成员函数
coloredShape.displayColoredShapeInfo();
return 0;
}
虚基类
如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员
C++提供虚基类(virtual base class)的方法,使得在继承间接共同基类时只保留一份成员
虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。因为一个基类可以在生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类。 声明虚基类的一般形式为
class 派生类名: virtual 继承方式 基类名
#include <iostream>
// 基类:动物
class Animal {
public:
Animal(const std::string& name) : name(name) {}
void eat() const {
std::cout << name << " is eating." << std::endl;
}
void sleep() const {
std::cout << name << " is sleeping." << std::endl;
}
private:
std::string name;
};
// 虚基类:飞行动物
class FlyingAnimal : virtual public Animal {
public:
FlyingAnimal(const std::string& name) : Animal(name) {}
void fly() const {
std::cout << Animal::getName() << " is flying." << std::endl;
}
};
// 派生类:哺乳动物
class Mammal : virtual public Animal {
public:
Mammal(const std::string& name) : Animal(name) {}
void giveBirth() const {
std::cout << Animal::getName() << " is giving birth." << std::endl;
}
};
// 派生类:既是哺乳动物又可以飞的动物
class Bat : public FlyingAnimal, public Mammal {
public:
Bat(const std::string& name) : Animal(name), FlyingAnimal(name), Mammal(name) {}
};
int main() {
Bat bat("Batman");
// 通过虚基类,避免了二义性问题
bat.eat();
bat.sleep();
bat.fly();
bat.giveBirth();
return 0;
}
基类和派生类的转换
在C++中,基类和派生类之间的转换主要涉及两种类型:向上转型(Upcasting)和向下转型(Downcasting)。这些转型可能涉及到指针和引用。
向上转型(Upcasting):
向上转型是将派生类的指针或引用转换为基类的指针或引用。这是一个安全的操作,因为派生类对象包含基类的部分。这样的转换通常是自动进行的,不需要显式的转换操作
#include <iostream>
class Base {
public:
virtual void display() const {
std::cout << "Base class display" << std::endl;
}
};
class Derived : public Base {
public:
void display() const override {
std::cout << "Derived class display" << std::endl;
}
void additionalFunction() const {
std::cout << "Additional function in the derived class" << std::endl;
}
};
int main() {
Derived derivedObj;
Base* basePtr = &derivedObj; // 向上转型
// 调用的是 Derived 类的 display 方法
basePtr->display();
// 无法直接调用 Derived 类的 additionalFunction
// basePtr->additionalFunction(); // 这行代码会产生编译错误
return 0;
}
向下转型(Downcasting):
向下转型是将基类的指针或引用转换为派生类的指针或引用。这是一个潜在的危险操作,因为基类可能无法包含派生类的所有信息。因此,向下转型时通常需要进行显式的类型转换,并且应该在确保安全性的情况下进行
class Base {
//...
};
class Derived : public Base {
//...
};
int main() {
Base* basePtr = new Derived();
// 向下转型,需要显式的类型转换
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
if (derivedPtr) {
// 转换成功,可以使用 derivedPtr 操作 Derived 类的成员
} else {
// 转换失败,basePtr 实际上不指向 Derived 类的对象
}
delete basePtr;
return 0;
}
在上述代码中,使用了 dynamic_cast 运算符进行向下转型。这个运算符在运行时检查转型的有效性,如果转型合法,则返回指向派生类对象的指针;否则,返回空指针。
总的来说,向上转型是安全的,而向下转型需要谨慎并使用适当的手段进行检查,以避免潜在的运行时错误。在进行向下转型时,通常使用 dynamic_cast 运算符进行类型检查,或者在一些情况下,可以使用 static_cast 运算符,但要确保转型是安全的。