1. C++ 中的多态
多态(Polymorphism)是面向对象编程中的一个重要特性,它允许使用相同的接口来表示不同的类型。由于派生类重写基类方法,然后用基类引用指向派生类对象,调用方法时候会进行动态绑定,这就是多态。
多态分为静态多态和动态多态:
1. 静态多态:编译器在编译期间完成的,编译器会根据实参类型来推断该调用哪个函数,如果有对应的函数,就调用,没有则在编译时报错。
静态多态(编译时多态):
- 静态多态通过函数重载、运算符重载和模板实现。它在编译期间就决定了调用哪个函数。
函数重载示例:
#include <iostream>
using namespace std;
class Print {
public:
void display(int i) {
cout << "Integer: " << i << endl;
}
void display(double f) {
cout << "Float: " << f << endl;
}
};
int main() {
Print obj;
obj.display(10); // 调用int版本
obj.display(3.14); // 调用double版本
return 0;
}
在这里,display
函数被重载,不同的参数类型决定了在编译期哪个函数被调用。
2.动态多态:其实要实现动态多态,需要几个条件——即动态绑定条件:
1. 虚函数。基类中必须有虚函数,在派生类中必须重写虚函数。
2. 通过基类类型的指针或引用来调用虚函数。
动态多态(运行时多态):
- 动态多态是通过虚函数实现的,在运行时决定调用哪个函数。
- 使用基类指针或引用指向派生类对象,可以根据实际的对象类型调用相应的派生类的函数。
动态多态示例:
#include <iostream> using namespace std; class Base { public: virtual void show() { // 虚函数 cout << "Base class show" << endl; } virtual ~Base() {} // 虚析构函数,防止内存泄漏 }; class Derived : public Base { public: void show() override { cout << "Derived class show" << endl; } }; int main() { Base *bptr = new Derived(); // 基类指针指向派生类对象 bptr->show(); // 动态绑定,调用 Derived 类的 show() delete bptr; // 防止内存泄漏 return 0; }
- 基类指针(
Base* bptr
)实际指向了Derived
对象,运行时根据对象的实际类型,调用了Derived
类的show()
方法。
总结:
- 静态多态:通过函数重载和模板实现,在编译时决定调用哪个函数。
- 动态多态:通过虚函数实现,依赖于运行时的动态绑定。
2. 为什么要虚析构,为什么不能虚构造?
虚析构的必要性:
- 当通过基类指针删除派生类对象时,如果基类析构函数不是虚函数,将不会调用派生类的析构函数,可能会导致派生类的资源没有被释放。
非虚析构的示例:
#include <iostream> using namespace std; class Base { public: ~Base() { cout << "Base destructor" << endl; } }; class Derived : public Base { public: ~Derived() { cout << "Derived destructor" << endl; } }; int main() { Base *bptr = new Derived(); delete bptr; // 只会调用Base的析构函数 return 0; }
- 输出只会显示
Base destructor
,Derived
类的析构函数不会被调用,从而导致资源泄漏。
虚析构的解决方案:
#include <iostream> using namespace std; class Base { public: virtual ~Base() { // 虚析构函数 cout << "Base destructor" << endl; } }; class Derived : public Base { public: ~Derived() { cout << "Derived destructor" << endl; } }; int main() { Base *bptr = new Derived(); delete bptr; // 调用Derived的析构函数 return 0; }
- 输出会显示:
Derived destructor Base destructor 表明派生类的析构函数得到了正确调用,避免了资源泄漏。
为什么不能虚构造?
- 构造函数用于初始化对象,而在构造期间,虚函数表还没有建立或准备好。构造函数依赖编译时的类型,不可能在构造时进行动态绑定。
- 而且,构造函数负责创建对象的基类部分和派生类部分,如果构造函数是虚的,会产生语义上的混淆。
- 虚析构:将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。如果基类的析构函数不是虚函数,在特定情况下会导致派生类无法被析构。 1. 用派生类类型指针绑定派生类实例,析构的时候,不管基类析构函数是不是虚函数,都会正常析构 2. 用基类类型指针绑定派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基类,不会析构派生类对象,从而造成内存泄漏。为什么会出现这种现象呢,个人认为析构的时候如果没有虚函数的动态绑定功能,就只根据指针的类型来进行的,而不是根据指针绑定的对象来进行,所以只是调用了基类的析构函数;如果基类的析构函数是虚函数,则析构的时候就要根据指针绑定的对象来调用对应的析构函数了。 C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。
- 不能虚构造: 1. 从存储空间角度:虚函数对应一个vtale,这个表的地址是存储在对象的内存空间的。如果将构造函数设置为虚函数,就需要到vtable中调用,可是对象还没有实例化,没有内存空间分配,如何调用。 2. 从实现上看,vtable在构造函数调用后才建立,因而构造函数不可能成为虚函数。
3. 模板类是在什么时候实现的?
模板类的定义和实现通常在头文件中一起写出,它们在编译时实例化,即编译器在遇到模板被使用时,才根据实际的模板参数生成相应的代码。这也是为什么模板函数的定义通常需要与声明放在同一个文件里。
模板类的示例:
#include <iostream> using namespace std; template<typename T> class Box { private: T value; public: Box(T val) : value(val) {} void display() { cout << "Value: " << value << endl; } }; int main() { Box<int> intBox(10); // 模板实例化为int类型 Box<double> doubleBox(5.5); // 模板实例化为double类型 intBox.display(); // 输出:Value: 10 doubleBox.display(); // 输出:Value: 5.5 return 0; }
- 在编译时,根据
Box<int>
和Box<double>
的使用情况,编译器生成具体的Box<int>
和Box<double>
代码。
总结:
- 模板在编译时进行实例化。
- 模板代码通常写在头文件中,防止出现链接错误。
4. 构造函数为什么不能被声明为虚函数?
构造函数的主要任务是创建并初始化对象,而在构造函数中虚函数表(vtable)尚未建立。
- 构造函数依赖编译时类型,即构造对象的类型在编译时就已经确定,不需要依赖虚函数机制。
- 另一方面,虚函数机制依赖于虚表(vtable),而虚表是在构造函数执行完毕后才建立的。如果构造函数是虚的,虚表还没有准备好,因此无法进行动态绑定。
5. 什么是常函数,有什么作用?
常函数(const
成员函数)是在函数声明时,在函数名之后添加const
关键字,表明该函数不会修改对象的任何成员变量。
常函数的示例:
class MyClass {
private:
int value;
public:
MyClass(int v) : value(v) {}
int getValue() const { // 常函数
return value;
}
void setValue(int v) {
value = v; // 非常函数,可以修改成员变量
}
};
作用:
- 常函数的主要作用是保证对象的状态不会在函数中被改变,尤其是在使用
const
对象时。 - 如果一个对象是常量(
const
),则只能调用常函数,无法调用非常函数。
int main() { const MyClass obj(10); // obj.setValue(20); // 错误:不能调用非常函数 cout << obj.getValue() << endl; // 正确:常函数可以被const对象调用 return 0; }
6. 什么是虚继承,解决什么问题,如何实现?
虚继承的定义:
- 虚继承用于解决菱形继承问题。当多个派生类通过不同路径继承同一个基类时,会导致基类的成员在最终派生类中存在多份拷贝,造成数据冗余或歧义。虚继承可以确保最终派生类中只有一份基类的拷贝。
菱形继承问题的示例:
#include <iostream> using namespace std; class Base { public: int value; }; class Derived1 : public Base { // 非虚继承 }; class Derived2 : public Base { // 非虚继承 }; class FinalDerived : public Derived1, public Derived2 { public: void setValue(int v) { // value = v; // 错误:编译器无法确定要修改 Derived1 还是 Derived2 的 value } };
在这个例子中,FinalDerived
类中有两个Base
类的拷贝:一个来自Derived1
,另一个来自Derived2
。因此,value
存在二义性。
虚继承解决菱形继承问题:
#include <iostream> using namespace std; class Base { public: int value; }; class Derived1 : virtual public Base { // 虚继承 }; class Derived2 : virtual public Base { // 虚继承 }; class FinalDerived : public Derived1, public Derived2 { public: void setValue(int v) { value = v; // 不会有二义性,因为Base类只有一份拷贝 } };
- 通过虚继承,
FinalDerived
类中只有一份Base
类的拷贝,避免了菱形继承问题。
7. 虚函数和纯虚函数,以及实现原理
虚函数:
- 虚函数是使用
virtual
关键字声明的函数,允许派生类覆盖基类中的实现。虚函数支持运行时多态,通过动态绑定调用派生类的函数。
纯虚函数:
- 纯虚函数是一种特殊的虚函数,它在基类中没有定义,必须在派生类中实现。
- 语法:将函数声明中的
= 0
表示为纯虚函数。
虚函数与纯虚函数示例:
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() { // 虚函数
cout << "Base class show" << endl;
}
virtual void print() = 0; // 纯虚函数
};
class Derived : public Base {
public:
void show() override {
cout << "Derived class show" << endl;
}
void print() override {
cout << "Derived class print" << endl;
}
};
int main() {
Base *ptr = new Derived();
ptr->show(); // 调用Derived类的show
ptr->print(); // 调用Derived类的print
delete ptr;
return 0;
}
Base
类中的print()
函数是纯虚函数,派生类Derived
必须提供实现。
实现原理:
虚函数通过**虚函数表(vtable)**实现。每个包含虚函数的类都会维护一个虚函数表,表中存储了指向实际函数的指针。在运行时,编译器会根据对象的实际类型查找虚表,调用相应的函数。
8. 纯虚函数能实例化吗,为什么?派生类要实现吗,为什么?
纯虚函数的类不能实例化:
- 包含纯虚函数的类称为抽象类。由于纯虚函数没有实现,所以抽象类不能被实例化,否则在调用纯虚函数时会无从执行。
- 示例:
class AbstractClass {
public:
virtual void func() = 0; // 纯虚函数
};// AbstractClass obj; // 错误:抽象类不能实例化
派生类必须实现纯虚函数:
- 如果派生类不实现基类中的纯虚函数,派生类也会变成抽象类,无法实例化。因此,派生类必须实现所有继承的纯虚函数。
9.虚函数和纯虚函数区别。
1. 虚函数(Virtual Function)
定义:
- 虚函数是基类中使用
virtual
关键字声明的成员函数。它允许派生类提供自己的实现,支持运行时多态。
特点:
- 具有函数体:虚函数在基类中有完整的实现,派生类可以选择是否覆盖它。
- 可选的覆盖:派生类可以重写虚函数,也可以不重写。如果派生类不提供自己的实现,调用该函数时会使用基类的版本。
- 动态绑定:虚函数通过虚表(vtable)实现动态绑定,运行时根据实际对象的类型选择调用合适的函数版本。
示例:
#include <iostream> using namespace std; class Base { public: virtual void show() { // 虚函数 cout << "Base class show" << endl; } }; class Derived : public Base { public: void show() override { // 重写虚函数 cout << "Derived class show" << endl; } }; int main() { Base *b = new Derived(); b->show(); // 调用Derived类的show delete b; return 0; }
- 这里
Base
类中定义了虚函数show()
,Derived
类重写了该函数。在运行时,通过基类指针调用的show()
实际调用了Derived
类的实现。
2. 纯虚函数(Pure Virtual Function)
定义:
- 纯虚函数是没有实现的虚函数,它在基类中声明,但不提供函数体,需要在派生类中实现。纯虚函数的声明使用
= 0
表示。
特点:
- 没有函数体:纯虚函数在基类中没有定义,仅作为接口存在。
- 抽象类:包含纯虚函数的类是抽象类,不能被实例化。如果一个类包含一个或多个纯虚函数,它就无法创建对象,除非派生类实现所有的纯虚函数。
- 必须被重写:所有派生类必须实现基类中的纯虚函数,否则派生类也将成为抽象类,无法实例化。
示例:
#include <iostream> using namespace std; class Base { public: virtual void show() = 0; // 纯虚函数 }; class Derived : public Base { public: void show() override { // 实现纯虚函数 cout << "Derived class show" << endl; } }; int main() { Base *b = new Derived(); b->show(); // 调用Derived类的show delete b; return 0; }
- 在此例中,
Base
类定义了一个纯虚函数show()
,因此它是一个抽象类,不能实例化。但Derived
类实现了该纯虚函数,因此可以创建Derived
类的对象,并通过基类指针调用重写的函数。
3. 虚函数与纯虚函数的区别
比较维度 | 虚函数(Virtual Function) | 纯虚函数(Pure Virtual Function) |
---|---|---|
函数体 | 在基类中有实现,通常派生类可以选择重写或者使用基类实现。 | 在基类中没有函数体,必须在派生类中实现。 |
类的类型 | 包含虚函数的类不是抽象类,可以实例化。 | 包含纯虚函数的类是抽象类,不能实例化。 |
派生类实现 | 派生类可以重写虚函数,但不是必须的。 | 派生类必须实现纯虚函数,除非它本身也是抽象类。 |
多态性 | 支持运行时多态,基类指针或引用可以调用派生类的重写函数。 | 也是运行时多态的一部分,依赖派生类的实现来调用函数。 |
使用目的 | 提供一个可以被覆盖的默认实现,但允许派生类根据需要进行重写。 | 定义一个接口,强制派生类提供自己的实现。 |
实现原理 | 通过虚函数表(vtable)实现动态绑定。 | 也是通过虚函数表(vtable)实现动态绑定,但必须有实现函数。 |
4. 使用场景和设计目的
-
虚函数:当你希望在基类中提供函数的默认行为,但允许派生类根据需要选择重写时,可以使用虚函数。这种设计允许部分多态行为,而不强制所有派生类都必须实现该函数。
-
纯虚函数:当你希望基类只定义接口,并且要求所有派生类都必须提供自己的实现时,使用纯虚函数。这种设计常见于抽象基类中,它们定义了派生类的共同行为接口,但不提供具体实现。
5. 总结
- 虚函数是可以在基类中提供实现的函数,派生类可以选择重写它们,也可以直接使用基类的实现。
- 纯虚函数定义了一个必须由派生类实现的接口,它没有实现,派生类必须提供它的定义,否则它们也将是抽象类,无法实例化。