继承
1. 继承的基本概念
- 继承是面向对象编程的核心特性之一,它允许一个类(子类或派生类)从另一个类(父类或基类)继承数据成员和成员函数,但不能继承父类的private私有成员、不能继承构造函数、拷贝构造函数、析构函数、=操作符重载,但可以被子类复用。
- 通过继承,子类可以复用父类的代码,并可以在子类中扩展或重写父类的功能。
2. 继承的语法
-
C++中使用
:
符号来表示继承。继承时可以指定继承的访问权限(public
、protected
、private
)。 -
语法格式:
class BaseClass { // 基类内容 }; class DerivedClass : public BaseClass { // 派生类内容 };
3. 继承的访问控制
- Public 继承:
- 父类的
public
成员在子类中仍然是public
,protected
成员在子类中是protected
。 - 这是最常用的继承方式。
- 父类的
- Protected 继承:
- 父类的
public
和protected
成员在子类中都变为protected
。 - 用于需要限制子类接口但仍想允许继承的情况下。
- 父类的
- Private 继承:
- 父类的
public
和protected
成员在子类中都变为private
。 - 使用这种继承方式,子类仅可以在内部使用父类的成员,对外界不可见。
- 父类的
4. 继承的类型
-
单继承: 一个子类只能有一个直接父类。
class Base {}; class Derived : public Base {};
-
多继承: 一个子类可以有多个父类。
class Base1 {}; class Base2 {}; class Derived : public Base1, public Base2 {};
- 注意: 多继承可能导致命名冲突和二义性问题,如父类中有相同的成员函数或数据成员。
5. 构造函数与析构函数
-
构造函数:
- 先按父类的继承顺序构造父类
- 按类内对象的声明顺序构造其它类的对象,先声明的先构造,后声明的后构造
- 再执行本类构造;先执行初始化列表,再执行本类构造函数内部
class Parent1 { public: int data1; Parent1(){cout << "Parent1" << endl; } Parent1(int data){cout << "Parent1" << endl; } ~Parent1(){cout << "~Parent1()" << endl; } }; class Parent2 { public: int data2; Parent2(){cout << "Parent2" << endl; } Parent2(int data){cout << "Parent2" << endl; } ~Parent2(){cout << "~Parent2()" << endl; } }; class Member1 { public: int data; Member1(){ cout << "Member1()" << endl; } Member1(int data) { cout << "Member1(int data)" << endl; this->data = data;} ~Member1(){ cout << "~Member1()" << endl; } }; class Member2 { public: int data; Member2(){ cout << "Member2()" << endl; } Member2(int data) { cout << "Member2(int data)" << endl; this->data = data;} ~Member2(){ cout << "~Member2()" << endl; } }; class Student : public Parent2,public Parent1 { public: string name; int age; float grade; Member2 m2; Member1 m1; //其它类的对象作本类的数据成员 Student():m1(0),m2(0),Parent1(0),Parent2(0) { cout << "Student()" << endl; this->name = ""; this->age = 0; this->grade = 0; } Student(const string &name,int age,float grade,int data1,int data2):m1(data1),m2(data2),Parent1(0),Parent2(0) { cout << "Student(const string &name,int age,float grade)" << endl; this->name = name; this->age = age; this->grade = grade; } ~Student() { cout << "~Student()" << endl; } }; int main() { Student stu1; } 输出: Parent2 Parent1 Member2(int data) Member1(int data) Student() ~Student() ~Member1() ~Member2() ~Parent1() ~Parent2()
-
析构函数:
- 析构函数调用顺序与构造函数相反。首先调用子类的析构函数,然后调用父类的析构函数。
6. 继承的优缺点
- 优点:
- 代码复用: 通过继承,子类可以重用父类的代码,减少重复代码。
- 扩展性: 继承提供了扩展类功能的途径,可以在子类中添加新的功能或修改现有功能。
- 缺点:
- 耦合性增加: 继承导致子类与父类之间的紧密耦合,如果父类发生变化,子类可能需要调整。
- 复杂性增加: 尤其是在使用多继承时,代码的复杂性和维护难度会显著增加。
7. 继承与组合
- 在设计类时,继承并不是唯一的选择。组合(Composition)也是一个重要的设计原则,它指的是在一个类中包含另一个类的对象,而不是通过继承来扩展类的功能。
- 一般来说,如果“某个类是另一个类的一种类型”,使用继承;如果“某个类拥有另一个类的功能”,使用组合。
多继承
1. 多继承的基本语法
- 在 C++ 中,一个派生类可以继承多个基类,基类之间使用逗号分隔。
class Base1 {
public:
void show() {
std::cout << "Base1 show" << std::endl;
}
};
class Base2 {
public:
void display() {
std::cout << "Base2 display" << std::endl;
}
};
class Derived : public Base1, public Base2 {
};
- 在这个例子中,
Derived
类继承了Base1
和Base2
,因此它可以访问这两个基类的公有成员。
2. 多继承遇见的问题
1. 菱形继承问题(钻石继承问题)
1.1 问题描述
-
菱形继承问题发生在如下情况:一个派生类继承自两个基类,而这两个基类又继承自同一个祖先类。这样,派生类会继承祖先类的两份副本,导致数据成员和函数的二义性问题。
class A { public: int data; void show() { std::cout << "Class A" << std::endl; } }; class B : public A { }; class C : public A { }; class D : public B, public C { };
在上面的例子中,
D
类将继承A
类的两个副本,一份来自B
类,另一份来自C
类。这就引发了问题:当访问A
类的成员时,如data
或show()
,编译器会不知道该访问哪一个。
1.2 解决方法
方法一: 虚继承
-
通过虚继承,我们可以确保派生类只继承祖先类的一份副本。
class A { public: int data; void show() { std::cout << "Class A" << std::endl; } }; class B : virtual public A { }; class C : virtual public A { }; class D : public B, public C { };
让
B
和C
虚继承A
。虚继承使得B
和C
共享同一个A
类的实例,从而在D
中只有一个A
类的实例。工作原理
- 虚基类表: 虚继承通过一种机制实现,该机制在派生类中包含一个指向虚基类的指针表。这个表用于确保无论通过哪个派生类访问基类,都指向同一个基类实例。
- 构造顺序: 在虚继承中,虚基类的构造函数会被最底层派生类负责调用,这也是为了确保基类的成员只被初始化一次。
方法二: 使用类域指定使用哪个类的成员
-
如果不想使用虚继承,我们可以在访问祖先类的成员时,明确指出使用哪一个基类的成员。
D obj; obj.B::data = 5; // 使用 B 继承的 A 类的 data 成员 obj.C::show(); // 使用 C 继承的 A 类的 show() 函数
这种方式虽然解决了二义性,但代码的可读性和维护性较差,因此不推荐作为常规解决方案。
2. V形继承问题
2.1 问题描述
-
V 形继承与菱形继承类似,不同的是,它的派生类从两个基类继承,而这两个基类没有共同的祖先类。但由于这两个基类中可能存在相同的成员函数或数据成员,仍会引发类似的二义性问题。
class A { public: void show() { std::cout << "Class A" << std::endl; } }; class B { public: void show() { std::cout << "Class B" << std::endl; } }; class C : public A, public B { };
在上面的例子中,
C
类从A
和B
两个基类分别继承了show()
函数。当我们在C
类中调用show()
时,编译器会不知该调用哪个版本。
2.2 解决方法
方法: 使用类域指定使用哪个类的成员
-
由于 V 形继承的问题主要是函数或成员的二义性问题,可以通过类域来明确指定要调用的函数。
C obj; obj.A::show(); // 调用 A 类的 show() obj.B::show(); // 调用 B 类的 show()
这种方式可以有效解决二义性问题,但同样会增加代码的复杂度。
总结
- 菱形继承问题:由于派生类从多个路径继承自同一个基类,导致该基类的成员出现多个拷贝。解决方法包括使用虚继承或明确指定使用哪个基类的成员。
- V形继承问题:虽然没有共同的祖先类,但由于基类中可能存在同名成员,也会引发二义性问题。解决方法主要是通过类域明确指定使用哪个基类的成员。
虚函数
1. 虚函数的基本概念
- **虚函数(Virtual Function)**是一个在基类中使用
virtual
关键字声明的成员函数。它允许子类重写该函数,以实现不同的功能。 - 虚函数的主要目的是支持多态性,即通过基类指针或引用调用子类的函数版本。
2. 虚函数的声明
-
在基类中,使用
virtual
关键字声明虚函数。 -
子类可以重写(override)这个虚函数,而不需要再次使用
virtual
关键字,但最好显式使用override
关键字以增加代码的可读性和安全性。 -
虚函数在基类和派生类中的声明形式:
class Base { public: virtual void show() { cout << "Base show" << endl; } }; class Derived : public Base { public: void show() override { cout << "Derived show" << endl; } };
3. 虚函数的特性
- 动态绑定(Dynamic Binding):
- 当通过基类指针或引用调用虚函数时,函数调用是在运行时解析的(动态绑定)。这意味着程序将调用实际对象类型(即子类)的函数版本。
- 这种机制允许实现多态行为,即同一函数名在不同上下文中表现出不同的行为。
- 非虚函数:
- 如果一个函数在基类中没有声明为虚函数,那么在使用基类指针或引用调用该函数时,调用的是基类的版本(静态绑定)。
4. 虚函数表(V-Table)与虚指针(V-Ptr)
- 虚函数表(V-Table)
- 定义: 每个包含虚函数的类都有一个虚函数表,它是一个指针数组,数组中存储的是指向该类的虚函数的地址。每个类的虚函数表是由编译器在编译时生成的。
- **内容**: 虚函数表中的每个条目对应一个虚函数,按顺序存储该类及其父类中的虚函数的地址。子类的虚函数表会包含它自己的重写函数的地址,以及从父类继承的虚函数的地址。
- 虚指针(V-Ptr)
-
每个对象(包含虚函数的类的实例)都有一个隐藏的指针,称为虚指针(V-Ptr),指向该对象所属类的虚函数表。当对象被创建时,构造函数会初始化虚指针,使其指向相应的虚函数表。
-
在调用虚函数时,程序会通过对象的虚指针找到对应的虚函数表,然后根据虚函数表中的函数指针调用实际的函数实现。
5. 纯虚函数(Pure Virtual Function)
-
纯虚函数是没有具体实现的虚函数,用于定义接口。基类中的纯虚函数要求所有派生类必须提供自己的实现。
-
纯虚函数的声明:
class Base { public: virtual void show() = 0; // 纯虚函数 };
-
抽象类:
- 包含至少一个纯虚函数的类被称为抽象类。抽象类不能被实例化,只能作为基类供派生类继承。
6. 虚析构函数和纯虚析构函数
虚析构函数
-
作用:
-
如果一个类包含虚函数,且可能通过基类指针删除派生类对象,那么基类的析构函数应当声明为虚函数。
-
这是为了确保在删除基类指针时,派生类的析构函数能够正确调用,避免资源泄漏。
-
class Base {
public:
virtual ~Base() { cout << "Base destructor" << endl; }
};
class Derived : public Base {
public:
~Derived() { cout << "Derived destructor" << endl; }
};
纯虚析构函数
1.1 概念
- 纯虚析构函数是一种特殊类型的虚析构函数,用于使类成为抽象类,并强制派生类实现自己的析构函数。
- 它声明为虚函数并且被定义为
= 0
,表示该函数没有提供具体的实现,必须在派生类中提供实现。
1.2 定义
class Base {
public:
virtual ~Base() = 0; // 纯虚析构函数
};
Base::~Base() {
// 纯虚析构函数的实现,必须提供
cout << "Base pure virtual destructor" << endl;
}
2. 使用纯虚析构函数
2.1 使类成为抽象类
- 声明一个纯虚析构函数使得基类
Base
成为抽象类,即不能直接实例化。 - 派生类必须实现纯虚析构函数才能实例化。
2.2 提供纯虚析构函数的实现
- 虽然纯虚析构函数没有具体的实现,但必须提供一个实现以便在对象销毁时正确调用。
2.3 代码示例
#include <iostream>
using namespace std;
class Base {
public:
virtual ~Base() = 0; // 纯虚析构函数
};
Base::~Base() {
cout << "Base pure virtual destructor" << endl;
}
class Derived : public Base {
public:
~Derived() override {
cout << "Derived destructor" << endl;
}
};
int main() {
Base* b = new Derived();
delete b; // 正确调用 Derived 的析构函数,然后调用 Base 的析构函数
return 0;
}
3. 注意事项
3.1 确保析构函数实现
- 纯虚析构函数必须提供一个实现,即使它本身是纯虚的。否则,编译器会报错。
- 实现通常是为了确保在销毁基类部分时能够完成必要的清理操作。
3.2 派生类必须实现析构函数
- 派生类必须实现其析构函数。否则,派生类也将成为抽象类,无法实例化。
3.3 避免资源泄漏
- 确保在派生类中实现析构函数时,正确地调用基类的析构函数,以避免资源泄漏。
4. 总结
- 纯虚析构函数使类成为抽象类,并强制所有派生类实现析构函数。
- 必须实现纯虚析构函数,确保在销毁对象时能够正确调用基类的析构函数。
- 派生类必须提供析构函数实现,确保资源能够正确释放。
7. 虚函数的性能影响
- 虚函数在运行时的调用会有少量的性能开销,因为它涉及动态绑定和通过虚函数表查找函数地址。
- 这种开销在多数应用场景下是可以忽略的,但在性能关键的代码中需要注意。
8. 虚函数与多态性
- **多态性(Polymorphism)**是虚函数最重要的应用,它允许程序根据实际对象类型调用对应的函数版本,从而实现灵活的接口设计。
- 通过多态性,基类指针可以指向不同的派生类对象,且调用相应的虚函数时会执行派生类的实现。
void display(Base* base) {
base->show(); // 调用的是实际对象的 show() 函数
}
抽象类
1. 定义抽象类
抽象类是一个包含至少一个纯虚函数的类。它不能被直接实例化,只能作为基类存在。
-
纯虚函数的定义方式是将函数声明后加上
= 0
,例如:class AbstractClass { public: virtual void pureVirtualFunction() = 0; // 纯虚函数 };
2. 纯虚函数
-
声明:纯虚函数在类中声明时后面要加上
= 0
,这样它就成为了一个抽象类的一部分。它表示这个函数在基类中没有实现,必须由派生类提供实现。class AbstractClass { public: virtual void pureVirtualFunction() = 0; // 纯虚函数 };
-
实现:派生类必须实现所有基类中的纯虚函数。否则,派生类仍然是抽象类,不能被实例化。例如:
class ConcreteClass : public AbstractClass { public: void pureVirtualFunction() override { // 实现纯虚函数 } };
3. 构造函数和析构函数
-
构造函数:抽象类可以有构造函数。虽然抽象类不能直接实例化,但构造函数可以用来初始化某些成员变量。例如:
class AbstractClass { public: AbstractClass() { // 构造函数的实现 } };
-
析构函数:抽象类通常会定义虚析构函数,以确保当通过基类指针删除派生类对象时,派生类的析构函数能够正确调用。否则,可能导致资源泄漏或未定义行为。例如:
class AbstractClass { public: virtual ~AbstractClass() { // 虚析构函数的实现 } };
4. 抽象类的作用
- 定义接口:抽象类用于定义一个接口,这个接口规定了所有派生类必须实现的函数。通过抽象类,你可以强制派生类遵循某种协议,确保其具有一定的功能。
- 多态:抽象类可以用于多态操作。你可以通过基类指针或引用来调用派生类的实现,这使得程序可以动态地决定要调用哪个派生类的方法。
5. 派生类
-
实现要求:派生类必须实现所有基类中的纯虚函数,否则它仍然是一个抽象类,不能被实例化。例如:
class ConcreteClass : public AbstractClass { public: void pureVirtualFunction() override { // 实现纯虚函数 } };
-
可实例化:一旦派生类实现了所有的纯虚函数,它就变成了具体类,可以被实例化。例如:
ConcreteClass obj; // 这是合法的
6. 注意事项
-
不能实例化:抽象类本身不能被实例化。例如:
AbstractClass obj; // 错误:无法实例化抽象类
-
多重继承:在多重继承中,如果多个基类都包含纯虚函数,派生类必须实现所有这些纯虚函数。例如:
class Base1 { public: virtual void foo() = 0; // 纯虚函数 }; class Base2 { public: virtual void bar() = 0; // 纯虚函数 }; class Derived : public Base1, public Base2 { public: void foo() override { // 实现 Base1 的纯虚函数 } void bar() override { // 实现 Base2 的纯虚函数 } };
这里,
Derived
类必须实现foo
和bar
,才能成为具体类。
静态联编与动态联编
1. 静态联编(Static Binding)
-
概念:
- 静态联编,也称为早期绑定(Early Binding),是在编译时确定函数调用的实现。这意味着编译器在编译阶段就已经决定了哪个函数会被调用。
- 静态联编的函数通常是非虚函数,或者通过对象直接调用的函数。
-
实现方式:
- 当通过对象调用一个非虚函数时,编译器在编译时就已经知道应该调用哪个函数。这种调用不需要在运行时进行任何查找,因此执行速度更快。
-
代码示例:
class Base { public: void show() { cout << "Base show" << endl; } // 非虚函数 }; class Derived : public Base { public: void show() { cout << "Derived show" << endl; } }; int main() { Base b; b.show(); // 静态联编,调用 Base::show return 0; }
-
优点:
- 性能高: 因为函数调用在编译时已经确定,省去了运行时的查找过程,因此执行速度较快。
- 实现简单: 静态联编的实现相对简单,不需要虚函数表等机制的支持。
-
缺点:
- 灵活性低: 由于函数绑定在编译时已经确定,不能实现多态性。
2. 动态联编(Dynamic Binding)
-
概念:
- 动态联编,也称为晚期绑定(Late Binding),是在运行时根据实际对象类型决定调用哪个函数。这通常通过虚函数实现。
- 动态联编是C++实现多态性的重要机制,允许在基类指针或引用上调用派生类的重写函数。
-
实现方式:
- 动态联编通常依赖于虚函数表(V-Table)和虚指针(V-Ptr)。当通过基类指针或引用调用虚函数时,程序在运行时查找虚函数表,确定实际调用的函数。
-
代码示例:
class Base { public: virtual void show() { cout << "Base show" << endl; } // 虚函数 }; class Derived : public Base { public: void show() override { cout << "Derived show" << endl; } }; int main() { Base* b = new Derived(); b->show(); // 动态联编,调用 Derived::show delete b; return 0; }
-
优点:
- 支持多态性: 通过动态联编,基类指针可以调用派生类的函数,实现多态性。
- 灵活性高: 函数调用的实现是在运行时决定的,程序可以根据实际对象类型做出不同的响应。
-
缺点:
- 性能开销: 由于需要在运行时进行函数查找,动态联编比静态联编有一定的性能开销。
- 复杂度增加: 动态联编需要虚函数表和虚指针的支持,增加了实现的复杂性。
3. 静态联编与动态联编的选择
- 静态联编适用于不需要多态性的场景,如普通函数调用、性能要求较高的场景。
- 动态联编则用于需要多态性的场景,尤其是在设计需要扩展性和灵活性的系统时,动态联编是必不可少的。
4. 静态联编的注意事项
- 虚函数与非虚函数的混用:
- 如果基类中的函数没有声明为虚函数,但在派生类中被重写,当通过基类指针或引用调用该函数时,调用的仍然是基类的版本(静态联编)。
- 解决方法:如果需要多态行为,确保基类的函数被声明为
virtual
。
- 对象切片(Object Slicing):
- 当基类对象被复制或赋值给派生类对象时,派生类的部分(新增的成员变量和函数)会被“切掉”,只保留基类的部分。这是因为静态联编只考虑基类的成员。
- 解决方法:尽量避免直接使用基类对象指针或引用操作派生类对象。如果必须使用,应确保函数是虚函数,并通过基类指针或引用进行操作。
- 函数的隐藏(重写的错误):
- 如果在派生类中声明了一个与基类同名但参数不同的函数,基类中的同名函数会被隐藏。
- 解决方法:在派生类中显式使用
using
关键字引入基类的同名函数,或者避免同名函数。
5. 动态联编的注意事项
- **确保虚析构函数:
- 如果一个类包含虚函数,而它可能会被用作基类(通过基类指针或引用删除派生类对象),那么基类的析构函数应该声明为虚函数。
- 这样可以确保在删除基类指针时,派生类的析构函数也会被正确调用,避免资源泄漏。
- 虚函数的调用方式:
- 虚函数只能通过对象的指针或引用来实现动态联编。如果通过对象直接调用虚函数,则会采用静态联编,即调用的是对象所属类的版本。
- 解决方法:始终通过基类的指针或引用调用虚函数,以确保实现多态性。
- 性能开销的考虑:
- 动态联编引入了一定的性能开销,因为每次调用虚函数时,程序需要查找虚函数表。这种开销在性能敏感的代码中可能需要考虑。
- 解决方法:如果性能至关重要,可以在关键路径中避免使用虚函数,或者使用静态联编。
- 避免多重继承中的二义性:
- 在多重继承中,不同的基类可能会有相同名称的虚函数,导致二义性问题。
- 解决方法:明确指定要调用哪个基类的虚函数,或者使用虚继承来减少二义性。
- 谨慎使用纯虚函数:
- 纯虚函数要求所有派生类必须实现它。如果派生类没有实现该函数,则派生类也将成为抽象类,无法实例化。
- 解决方法:在设计接口时,确保确实需要所有派生类都实现该函数,否则可以提供一个默认实现。
函数重载和函数重写
主要区别
特性 | 函数重载 | 函数重写 |
---|---|---|
定义位置 | 同一作用域内(类内部或全局作用域) | 派生类中重写基类中的虚函数 |
函数签名 | 函数名相同但参数列表不同 | 函数名、参数列表和返回类型相同 |
编译时/运行时 | 编译时决定函数调用(静态联编) | 运行时决定函数调用(动态绑定) |
虚函数 | 不需要虚函数 | 基类中的函数需要声明为虚函数 |
作用 | 提供不同的函数版本以处理不同类型的参数 | 提供不同的实现以扩展或修改基类的行为 |
总结
- 函数重载是同一作用域内对函数名的不同定义,主要用于处理不同的参数。
- 函数重写是子类对基类虚函数的重新定义,主要用于实现多态性和扩展基类的功能。
函数重写时被屏蔽的情况
1. 子类函数名与父类相同,参数相同,但父类没有 virtual
关键字
错误现象:
- 如果在父类中定义了一个函数,但没有使用
virtual
关键字,那么即使子类中定义了一个同名且参数列表相同的函数,这个子类函数也不会重写父类函数。相反,它会被视为一个新的、与父类无关的函数。这种情况下,父类指针或引用指向子类对象时,调用的仍然是父类版本的函数(静态联编)。
示例:
class Base {
public:
void show() { // 没有virtual关键字
std::cout << "Base show" << std::endl;
}
};
class Derived : public Base {
public:
void show() { // 试图重写Base类的show函数
std::cout << "Derived show" << std::endl;
}
};
int main() {
Base* ptr = new Derived();
ptr->show(); // 输出: "Base show"
delete ptr;
return 0;
}
解释:
- 由于
Base
类中的show()
函数不是虚函数,编译器在编译时会静态地将ptr->show()
绑定到Base
类的show()
函数。因此,即使指针实际上指向Derived
类的对象,也不会调用Derived
类的show()
函数。这种情况下,无法实现多态性。
2. 子类函数名与父类相同,参数不同,此时不管父类有无 virtual
都是“屏蔽”
错误现象:
- 如果子类中的函数名称与父类中的函数名称相同,但参数列表不同,无论父类函数是否使用
virtual
关键字,子类中的新函数都会“屏蔽”父类中的所有同名函数。换句话说,父类中的函数不会被重写或覆盖,而是被隐藏。
示例:
class Base {
public:
virtual void show() { // 虚函数
std::cout << "Base show" << std::endl;
}
};
class Derived : public Base {
public:
void show(int x) { // 参数不同,试图重载show函数
std::cout << "Derived show with int " << x << std::endl;
}
};
int main() {
Derived obj;
obj.show(10); // 输出: "Derived show with int 10"
Base* ptr = &obj;
ptr->show(); // 输出: "Base show"
return 0;
}
解释:
- 在
Derived
类中,show(int x)
函数与Base
类中的show()
函数具有相同的名字但不同的参数列表。这导致Base
类的show()
函数被隐藏,但不会被重写。Derived
类中并没有show()
的重写版本,因此当通过基类指针调用show()
函数时,仍然会调用Base
类的版本。这种“屏蔽”行为使得多态性无法正常工作。
总结
- 屏蔽错误 1: 当父类函数没有
virtual
关键字时,子类中相同签名的函数不会触发多态,而是静态绑定父类函数。 - 屏蔽错误 2: 当子类函数与父类函数同名但参数不同,即使父类函数是虚函数,子类函数也不会重写它们,而是将其隐藏。这种情况下无法通过基类指针或引用调用子类函数。
多态
允许同一个函数在不同对象上具有不同的表现形式。
-
多态性可以通过函数重载和函数重写、运算符重载和虚函数来实现。
-
多态还可以使用模板技术: 函数定义时没有确定参数的类型,在调用时才确定参数的类型
1. 编译时多态性(静态多态性)
- 函数重载(Function Overloading): 同名函数可以有不同的参数列表(类型和数量),编译器根据调用时的参数类型选择相应的函数。
- 运算符重载(Operator Overloading): 运算符可以被重载以对用户自定义的类型执行特定操作。
示例:
class Print {
public:
void display(int i) {
std::cout << "Integer: " << i << std::endl;
}
void display(double d) {
std::cout << "Double: " << d << std::endl;
}
};
2. 运行时多态性(动态多态性)
- 运行时多态性通过基类指针或引用调用派生类对象的虚函数来实现。C++ 的动态多态性通过虚函数表(vtable)和虚指针(vptr)机制实现。
2.1 虚函数(Virtual Functions)
- 虚函数是一个在基类中使用
virtual
关键字声明的函数,它允许在派生类中重写。通过基类指针或引用调用虚函数时,将执行派生类的重写版本。
class Base {
public:
virtual void show() {
std::cout << "Base class" << std::endl;
}
};
class Derived : public Base {
public:
void show() override {
std::cout << "Derived class" << std::endl;
}
};
int main() {
Base* ptr;
Derived obj;
ptr = &obj;
// 调用的是 Derived 类的 show 函数
ptr->show(); // Output: "Derived class"
return 0;
}
2.2 纯虚函数与抽象类(Pure Virtual Functions and Abstract Classes)
- 纯虚函数是一个没有实现的虚函数,必须在派生类中重写。含有纯虚函数的类称为抽象类,无法直接实例化。
class AbstractBase {
public:
virtual void display() = 0; // 纯虚函数
};
class ConcreteDerived : public AbstractBase {
public:
void display() override {
std::cout << "Concrete implementation" << std::endl;
}
};
int main() {
ConcreteDerived obj;
obj.display(); // Output: "Concrete implementation"
return 0;
}
3. 多态性与继承
- 多态性通常与继承紧密结合,通过基类指针或引用可以调用不同派生类的重写函数,实现在运行时选择合适的函数版本。
4. 多态的优点
- 代码复用: 通过多态性,可以使用基类指针或引用处理不同派生类的对象,减少代码重复。
- 扩展性: 新的派生类可以添加新的功能而不需要修改基类代码。
5. 虚函数表(vtable)与性能考虑
- 虚函数的调用涉及到 vtable 查找,可能会引入一些运行时开销,因此在性能敏感的应用中需要谨慎使用虚函数。
6. 常见问题与注意事项
- 切片问题(Slicing Problem): 当使用基类对象赋值派生类对象时,派生类的特有数据成员可能会丢失。
- 构造函数与析构函数的多态性: 构造函数不能是虚函数,而基类的析构函数通常应该是虚函数,以确保正确调用派生类的析构函数。