目录
一、继承:代码复用的艺术
1、继承概念
代码说明1:继承方式和访问控制
代码说明2:作用域与成员访问
代码说明3:构造函数和析构函数
2、基类和派生类对象赋值转换
派生类对象到基类对象的转换(向上转型):
基类对象到派生类对象的转换(了解):
3、多继承、菱形继承和虚拟继承
单继承
多继承
菱形继承
虚拟继承
菱形继承的实现方式(了解):
4、精选面试题
1. 什么是菱形继承?菱形继承的问题是什么?
2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的
3. 继承和组合的区别?什么时候用继承?什么时候用组合?
二、多态:基类成员和派生类行为的动态绑定
多态的概念
多态的实现
虚函数表原理
C++11的新特性
动态绑定与静态绑定
07-继承——代码复用的艺术
C++是一种功能强大的编程语言对象的继承与多态是C++面向对象编程的核心概念之一,继承为多态提供了结构基础,而多态则是继承的一种应用,使得程序能够以统一的方式处理不同类型的对象。继承和多态提高了代码的灵活性和可维护性,使得代码重用和扩展性成为可能。
1、继承概念
继承是面向对象编程中的一种机制,它允许我们定义一个新类(派生类)来扩展或修改一个已存在的类(基类)。继承体现了现实世界中的“是一个”(is-a)关系。例如,如果有一个Person
基类,我们可以派生出Student
和Teacher
等类。
示例代码:
#include <iostream>
#include <string>
// 基类:Person
class Person {
public:
// 构造函数
Person(std::string name, int age) : _name(name), _age(age) {}
// 公共成员函数
void PrintInfo() const {
std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
}
protected:
std::string _name; // 姓名
int _age; // 年龄
};
// 派生类:Student
class Student : public Person {
public:
// 构造函数
Student(std::string name, int age, int stuid, std::string major)
: Person(name, age), _stuid(stuid), _major(major) {}
// 重写基类的成员函数
void PrintInfo() const {
Person::PrintInfo(); // 调用基类的PrintInfo函数
std::cout << "Student ID: " << _stuid << ", Major: " << _major << std::endl;
}
private:
int _stuid; // 学号
std::string _major; // 专业
};
int main() {
// 创建Person对象
Person person("Alice", 30);
person.PrintInfo();
// 创建Student对象
Student student("Bob", 20, 12345678, "Computer Science");
student.PrintInfo();
return 0;
}
代码说明1:继承方式和访问控制
public继承
:基类的公有(public)和保护(protected)成员在派生类中成为公有成员,而基类的私有(private)成员在派生类中不可见。protected继承
:基类的所有成员(无论是public还是protected)在派生类中都成为受保护的(protected)成员。private继承
:基类的所有成员(无论是public还是protected)在派生类中都成为私有的(private)成员。
在示例代码中,Student
类使用public继承
从Person
类继承,意味着Person
类的公有和保护成员在Student
类中都保持原有的访问级别。
代码说明2:作用域与成员访问
作用域是指在代码中定义的命名空间,决定了如何访问类成员。在继承的情况下,在继承体系中,同名的成员会形成隐藏关系。如果派生类中有与基类同名的成员,则派生类的成员会隐藏基类的同名成员。这意味着在派生类的作用域内,同名成员将优先使用派生类中的版本。
在示例代码中,如果Person
类有一个_name
成员,Student
类也有一个_name
成员,则在Student
类中访问_name
将访问派生类中的版本。如果需要访问基类中的_name
,可以使用作用域解析运算符::
,如Person::_name
。
代码说明3:构造函数和析构函数
派生类的构造函数必须调用基类的构造函数来初始化基类部分。析构函数则保证了正确的清理顺序。
上述代码没有实现析构函数,编译器自动生成的析构函数会自动调用基类析构函数完成析构。
2、基类和派生类对象赋值转换
派生类对象到基类对象的转换(向上转型):
- 这种转换称为向上转型(upcasting),是安全的,因为派生类对象包含基类的所有成员。
- 派生类对象可以自动赋值给基类的对象、指针或引用,这称为切片(slicing),因为只有基类部分被赋值。
- 例如,如果有一个
Student
对象,它可以被赋值给一个Person
类型的引用或指针。
基类对象到派生类对象的转换(了解):
- 这种转换称为向下转型(downcasting),通常是不安全的,因为基类对象不包含派生类的额外成员。
- 基类对象不能直接赋值给派生类对象,因为缺少派生类特有的成员。
提高内容:如果希望将派生类对象被切片而来的基类对象指针向下转型回派生类对象指针,可以使用c++11中引入的dynamic_cast进行转化。C++ | 深入剖析C++中的类型转换-CSDN博客
示例代码:
class Person {
public:
void Print() {
std::cout << _name << std::endl;
}
protected:
std::string _name; // 姓名
private:
int _age; // 年龄
};
class Student : public Person {
protected:
int _stunum; // 学号
};
int main()
{
Person p; // 创建基类对象
Student s; // 创建派生类对象
// 向上转型:安全
p = s; // 将派生类对象赋值给基类对象,发生切片
// 向下转型:不安全,需要显式类型转换
Person* pPtr = &s; // 基类指针指向派生类对象
Student* sPtr = static_cast<Student*>(pPtr);
// 显式转换,如果pPtr指向的是Student对象,则转换成功
// 使用dynamic_cast进行安全的向下转型
Student* safeSPtr = dynamic_cast<Student*>(pPtr);
// 如果pPtr指向Student对象,则转换成功,否则为nullptr
return 0;
}
示例代码中,Student
类通过public
继承方式继承自Person
类。这意味着Student
对象可以自动转换为Person
类型的引用或指针,但Person
对象不能转换为Student
对象。在实际编程中,应该谨慎使用向下转型,并确保类型转换的安全性,以避免潜在的运行时错误。
3、多继承、菱形继承和虚拟继承
单继承
单继承是最简单的继承形式,其中一个类只继承自一个基类。在单继承中,派生类继承了基类的所有公共和受保护成员(但不包括私有成员),并可以添加新的成员或重写基类的成员。
- 清晰简单:只有一个基类,因此继承关系非常清晰。
- 易于理解和维护:由于只有一个直接的基类,派生类的行为和特性容易预测和管理。
多继承
多继承允许一个类从多个基类继承特性。这意味着派生类可以同时继承多个基类的成员。
特点:
- 灵活性高:可以同时继承多个类的属性和方法,提供更丰富的功能组合。
- 复杂性增加:多继承可能导致复杂的继承关系,难以理解和维护。
- 潜在问题:可能产生如菱形继承等问题,需要额外的机制(如虚拟继承)来解决。
菱形继承
菱形继承是一种特殊的多继承场景,其中两个或更多的派生类继承自同一个基类,然后这些派生类又有一个共同的派生类。这种结构在类继承图中形成了一个菱形,因此得名。
菱形继承主要带来两个问题:
- 数据冗余:由于基类的成员在每个派生类中都有拷贝,当两个基类提供了同名的成员时,如果没有明确的指示,编译器可能无法确定应该使用哪一个基类的成员。如:Accistant对象中有两份Person的实例。这会导致内存的浪费。
- 二义性:当最终的派生类需要访问基类中的成员时,可能会不清楚应该访问哪个派生路径上的基类成员。如:访问Accistant中的name成员,是访问Student中的成员变量,还是Teacher中的name成员变量?
虚拟继承
虚拟继承的出现就是为了解决菱形继承,也就是说,虚拟继承是为了解决菱形继承带来的虚拟继承和二义性的问题。
虚拟继承通过在继承列表中使用virtual
关键字来解决菱形继承的问题。当使用虚拟继承时,即使多个基类有一个共同的基类,这个共同的基类也只会被实例化一次。
在上面的例子中,如果我们使用虚拟继承,Child
类的定义将如下所示:
class Base {
public:
int value;
};
class DerivedA : virtual public Base {
};
class DerivedB : virtual public Base {
};
class Child : public DerivedA, public DerivedB {
};
使用虚拟继承后,Child
类将只包含一个Base
的实例,解决了菱形继承的问题。此外,虚拟继承还有助于解决二义性问题,因为它明确了访问路径,避免了成员的二义性。
菱形继承的实现方式(了解):
虚继承表存储指向虚基类实例的指针。它通常位于对象的内存布局的最前面,通过指针找到偏移量获取基类的数据。
4、精选面试题
1. 什么是菱形继承?菱形继承的问题是什么?
菱形继承是一种多继承的继承结构,其中一个类(最末派生类)继承自两个或多个类(中间派生类),而这些中间派生类又共同继承自同一个基类。这种结构在类图上看起来像一个菱形,因此得名。
问题:
- 数据冗余:在没有虚拟继承的情况下,最末派生类会包含多个基类的副本,导致内存浪费。
- 二义性:如果基类中有同名成员,最末派生类可能无法确定应该使用哪个基类的成员,导致二义性问题。
2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的
通过在中间派生类中使用virtual
关键字来指定基类的继承,可以解决菱形继承的问题。
解决数据冗余:
- 使用虚拟继承后,中间派生类会有一个指向基类的指针,而不是复制基类的成员。这样,最末派生类只会有一个基类的实例,避免了数据冗余。
解决二义性:
- 虚拟继承确保了只有一个基类实例,因此消除了二义性问题。如果需要访问基类的成员,编译器可以通过基类指针来确定访问的是哪个实例。
3. 继承和组合的区别?什么时候用继承?什么时候用组合?
- 继承:是一种"是一个"(is-a)的关系,表示一个类(子类)是另一个类(基类)的特殊版本。继承允许子类继承基类的属性和方法,并可以添加或重写它们。
- 组合:是一种"有一个"(has-a)的关系,表示一个类包含另一个类的实例作为其成员变量。组合提供了更大的灵活性,因为组合的类不受限于单一的继承链。
使用继承的情况:
- 当类之间存在自然的"是一个"关系时,例如"狗是动物"。
- 当子类需要重用基类的代码,并且希望扩展或修改基类的行为时。
使用组合的情况:
- 当类之间存在"有一个"关系时,例如"汽车有一个引擎"。
- 当需要更大的灵活性,避免继承带来的紧密耦合和脆弱的基类问题时。
- 当不希望或不需要子类继承基类的所有属性和方法时。
选择继承还是组合,取决于具体的设计需求和类之间的关系。通常,组合提供了更大的灵活性和更低的耦合度,但在某些情况下,继承可以更清晰地表达类之间的关系并简化代码重用。