1、什么是多态?
多态(Polymorphism)是面向对象编程中的一个重要概念,它允许同一个接口表现出不同的行为。在C++中,多态性主要通过虚函数来实现,分为编译时多态(静态多态)和运行时多态(动态多态)。
多态的类型:
编译时多态(静态多态):在编译阶段就能确定调用哪个函数,通常通过函数重载、运算符重载和模板来实现。特点:在编译期决定函数调用,效率高,但灵活性相对较弱。
//示例函数重载
class Printer {
public:
void print(int i) { cout << "Printing int: " << i << endl; }
void print(double d) { cout << "Printing double: " << d << endl; }
};
int main() {
Printer p;
p.print(10); // 调用 print(int i)
p.print(3.14); // 调用 print(double d)
}
运行时多态(动态多态):在运行时根据对象的类型决定调用哪个函数。通过虚函数和继承实现。
特点:在运行时通过基类指针或引用指向派生类对象,从而动态地决定函数调用,灵活性高,但效率比静态多态低一些。
/*虚函数实现动态多态 在这个例子中,基类 Animal 的指针根据对象的具体类型,动态决定调用 Dog 的 makeSound 或 Cat 的 makeSound,这就是运行时多态。*/
class Animal {
public:
virtual void makeSound() { cout << "Animal sound" << endl; }
};
class Dog : public Animal {
public:
void makeSound() override { cout << "Woof!" << endl; }
};
class Cat : public Animal {
public:
void makeSound() override { cout << "Meow!" << endl; }
};
int main() {
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
animal1->makeSound(); // 输出 "Woof!",调用 Dog 的 makeSound()
animal2->makeSound(); // 输出 "Meow!",调用 Cat 的 makeSound()
delete animal1;
delete animal2;
}
多态的实现原理:
运行时多态依赖于虚函数表(vtable)。当类中定义虚函数时,编译器会为该类创建一个虚函数表,表中存储指向类中虚函数的地址。每个对象包含一个指向该虚表的指针(vptr)。在运行时,通过vptr查找实际调用的函数地址,从而实现动态多态。
多态的作用:
代码灵活:多态允许你通过基类指针或引用操作派生类对象,而不必关心派生类的具体类型,提供了更高的灵活性。
可扩展性:新功能可以通过继承并重写虚函数来实现,而无需修改现有代码,方便系统的扩展。
代码复用:基类可以提供通用的接口和功能,而具体的实现则由派生类完成,减少代码重复。
总结:
多态是对象在不同上下文中表现出不同行为的能力。
静态多态是在编译时决定的函数调用,而动态多态是在运行时根据对象类型动态决定的函数调用。
动态多态通过虚函数、继承和虚函数表实现,极大地提高了代码的灵活性与可扩展性。
2、什么是重载、重写(覆盖)、重定义(隐藏)?
总结:
重载(Overloading):同一作用域中,函数名相同,但参数不同。
重写(Overriding):派生类中重新实现与基类虚函数相同的函数,用于多态。
重定义(Hiding):派生类中定义了与基类同名但参数不同的函数,隐藏基类的同名函数。
- 重载(Overloading)
重载是指在同一个类中,多个函数名称相同,但它们的参数列表不同(参数类型、数量或顺序不同),编译器根据调用时传递的参数类型和数量来决定调用哪个函数。
特点:
发生在同一个作用域。
参数列表必须不同,返回类型可以相同也可以不同。
可以重载普通函数、构造函数和运算符。
class Math {
public:
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
int add(int a, int b, int c) { return a + b + c; }
};
int main() {
Math math;
cout << math.add(2, 3) << endl; // 调用 add(int, int)
cout << math.add(2.5, 3.5) << endl; // 调用 add(double, double)
cout << math.add(1, 2, 3) << endl; // 调用 add(int, int, int)
}
- 重写(Overriding,覆盖)
重写是指在派生类中,重新定义与基类中的虚函数相同的函数,即函数名、参数列表、返回类型必须完全相同。重写主要用于实现运行时多态。重写的函数必须是虚函数,通过基类指针或引用调用时,动态决定调用派生类的实现。
特点:
发生在继承关系中。
函数签名(函数名、参数列表和返回类型)必须与基类中的虚函数完全相同。
重写的函数必须是虚函数,且派生类中的函数也默认是虚函数(使用override关键字可以明确表明函数是重写的)。
class Animal {
public:
virtual void sound() { cout << "Animal sound" << endl; }
};
class Dog : public Animal {
public:
void sound() override { cout << "Woof!" << endl; } // 重写基类的虚函数
};
int main() {
Animal* animal = new Dog();
animal->sound(); // 输出 "Woof!",调用派生类 Dog 的 sound 函数
delete animal;
}
- 重定义(Hiding,隐藏)
重定义是指在派生类中,定义了与基类同名但参数列表不同的函数。由于在派生类中定义了同名函数,基类中的同名函数会被隐藏,调用时只能使用派生类的函数,而基类的函数即便参数列表不同也无法通过派生类对象访问。
特点:
发生在继承关系中。
派生类的函数参数列表可以与基类不同,基类中的同名函数被隐藏。
如果基类的函数想继续保留,可以通过using声明重新引入。
class Base {
public:
void display(int a) { cout << "Base class display: " << a << endl; }
};
class Derived : public Base {
public:
void display(double a) { cout << "Derived class display: " << a << endl; }
};
int main() {
Derived obj;
obj.display(5.5); // 调用 Derived::display(double)
// obj.display(5); // 编译错误,Base::display(int) 被隐藏
}
3、多态的实现原理?
见1题。
4、inline函数可以是虚函数吗?
可以、但是inline只是一个建议。当一个函数是虚函数以后,多态调用中,inline失效了。
**可以,inline只是一个建议。**当一个函数是虚函数时,编译器可能依然会将它内联,但前提是编译器可以确定具体的函数调用对象。通常情况下,如果通过具体对象调用虚函数(即编译器能够知道对象的静态类型),虚函数仍可能被内联。
然而,在多态调用(即通过基类指针或引用调用虚函数)中,由于编译器需要在运行时通过虚表动态决定调用哪个版本的函数,inline优化就无法生效。因为内联要求编译器在编译时知道要调用的具体函数,而多态性导致这一点无法确定,因此在这种情况下内联失效了。
5、static函数(静态成员)可以是虚函数吗?
不能,因为静态成员函数没有this指针,使用 类型::成员 函数的调用方式(类域指定的方式,如 Person::Func2())无法访问虚函数表,所以静态成员函数无法放进虚函数表。虚函数是为了实现多态,多态都是运行时去虚表找决议。static成员函数都是在编译时决议,他是virtual没有价值。
静态成员函数不能是虚函数的原因:
虚函数依赖对象实现:虚函数的多态性需要在运行时根据对象的实际类型来决定调用哪个函数,而静态成员函数不属于任何具体对象,因此无法通过虚表进行动态绑定。
没有 this 指针:虚函数通常需要 this 指针来访问对象的成员,但静态成员函数没有 this 指针,所以无法实现虚函数的特性。
结论:
静态成员函数不能是虚函数,因为虚函数依赖对象实现多态性,而静态成员函数与对象无关,不支持动态绑定。因此,static 和 virtual 是相互冲突的,无法在同一个函数上同时使用。
6、构造函数可以是多态吗?
不可以,virtual函数是为了实现多态,运行时去虚表找对应虚函数进行调用,对象中虚表指针都是构造函数初始化列表阶段才初始化的。
构造函数虚函数是没有意义的。
构造函数不能是多态的。构造函数在 C++ 中无法实现多态,主要原因如下:
构造函数不参与虚函数机制:
虚函数的多态性依赖于对象的类型在运行时动态绑定,而构造函数是在对象创建时调用的。在构造对象的过程中,虚表还没有被初始化或设置,因此无法实现多态行为。
构造函数的目的是初始化对象:构造函数的主要任务是初始化对象的成员变量和资源,它负责生成对象本身。多态依赖于已有的对象实例,但构造函数在创建对象的过程中,无法确定派生类的行为。
虚表初始化顺序:虚表(VTable)是在构造函数执行完毕后,派生类的构造函数才能设置。因此,在调用基类构造函数时,多态机制尚未建立,无法进行动态绑定。
总结:构造函数不能是多态的,因为多态依赖虚表和动态绑定,而虚表在构造函数调用期间尚未建立或完成初始化。
7、析构函数可以是虚函数吗?
可以。而且析构函数建议虚函数
8、拷贝构造和operator= 可以是虚函数吗?
拷贝构造不可以。拷贝构造也是构造函数,和构造函数一样,没有this指针,无法设置虚函数。
operator赋值 语法上可以(质疑),但是没有实际价值 。
8、对象访问普通函数快还是虚函数更快?
如果虚函数不构成多态(通过具体对象调用),编译器可以进行静态绑定或内联优化,调用开销与普通函数几乎一致。如果构成多态,普通函数更快。
如果虚函数构成多态(通过基类指针或引用调用),则需要进行虚表查找,存在动态绑定的开销。
普通函数调用:普通函数是静态绑定的,在编译时就已经确定调用哪个函数。调用过程是直接的,编译器会在生成代码时直接插入该函数的地址。调用速度更快,因为不需要额外的查找过程。
虚函数调用:虚函数是动态绑定的,依赖于对象的动态类型。在运行时通过虚表(VTable)来查找并调用正确的函数实现。虚函数调用涉及额外的步骤:首先,通过对象的虚表指针找到虚表,然后根据虚表中的函数指针找到具体的函数实现。这一过程增加了运行时开销。调用速度相对较慢,因为多了虚表查找的步骤。
总结:普通函数调用更快,因为它是静态绑定,编译时直接确定,不涉及任何额外查找。虚函数调用稍慢,因为需要通过虚表查找函数指针,存在运行时开销。
9、虚函数表是在什么阶段生成的,存在那的?
虚表在编译阶段生成,用于存储虚函数的指针。
虚表指针(vptr)在运行时存储于每个对象的内存中,用于动态绑定。
虚表本身是存储在全局内存区域,且每个类只有一个虚表。虚表(VTable)本身是存储在静态区域,通常是全局内存区域。虚函数表是编译器生成的全局结构,每个类有一个虚表,与对象无关,因此它不会随每个对象重复存储。
区域划分说明:
常量区:程序中不可修改的常量数据,如字符串字面量和 const 常量。
全局数据区(静态区):存储全局变量、静态变量和类的虚表。虚表属于这个区域。
栈区:用于存储局部变量、函数参数等。
堆区:用于动态分配的内存,如使用 new 分配的对象。
总结:虚表存储在静态区域(全局数据区),它在程序生命周期内存在。
虚表的内容(函数指针)会动态指向不同的函数实现,因此虚表不是存储在常量区,而是在静态内存区域的部分。
10、C++的菱形继承问题是什么?虚继承的原理是什么?
总结:
菱形继承问题:当一个类通过多个路径继承同一个基类时,会导致基类的多次拷贝,产生访问歧义,使用虚继承可以解决这个问题。
虚函数的原理:通过虚表和虚表指针实现动态多态,允许程序在运行时根据实际对象类型调用合适的函数版本。
菱形继承是指一个类通过多个继承路径从同一个基类继承,这种结构会导致一些问题,特别是关于基类成员的多次拷贝和访问歧义。
问题1:基类成员的多次拷贝
由于 B 和 C 都继承了 A,而 D 又从 B 和 C 继承,因此 D 类中会有两份 A 类的副本。这导致基类的成员函数和成员变量在 D 中存在两份。如果调用 D 对象的 func() 函数,编译器不知道该调用 B 中的 A::func() 还是 C 中的 A::func(),这会导致访问歧义。
解决方案:虚继承
为了解决这个问题,可以使用虚继承,即通过 virtual 关键字来指定基类 A 只保留一份副本。虚继承确保无论 A 被继承多少次,最终在派生类 D 中只会存在 一个 A 类的实例。使用虚继承后,D 类中只有一个 A 的实例,B 和 C 共享这个实例,从而避免了多次拷贝和访问歧义。
虚继承的原理:
虚继承的原理是为了避免在多重继承(尤其是菱形继承)时,基类的多次拷贝问题。它通过引入虚基表(VBTable)和虚基表指针(vbptr)来确保派生类中只有一个共享的基类实例。这种机制可以解决多路径继承时的重复继承问题,保证数据一致性。
虚继承通过让派生类共享一个共同的基类实例来解决这一问题。实现虚继承时,编译器会生成额外的数据结构和指针来确保基类在派生类中只有一个实例。
虚继承的实现步骤:
虚基表(VBTable):当一个类使用虚继承时,编译器会为其生成一个虚基表(VBTable)。这个表中存储了指向虚基类的地址偏移量,确保派生类在访问虚基类时能够找到唯一的基类实例。
虚基表指针(vbptr):每个虚继承的类中,都会有一个隐藏的虚基表指针(vbptr),指向虚基表。这相当于一个中间指针,用于帮助派生类找到基类的唯一实例。
偏移量访问虚基类:当派生类访问虚基类的成员时,编译器通过 vbptr 和 VBTable 确定虚基类在内存中的位置,确保派生类总是引用同一个基类实例。
虚函数的原理:
虚函数(virtual function)是用于实现动态多态的机制,允许程序根据运行时的对象类型(而非编译时的类型)调用适当的函数版本。虚函数的核心机制依赖于虚函数表(VTable)和虚表指针(vptr)。
虚函数的原理:
虚表(VTable):每个定义了虚函数的类都有一个虚表(VTable),虚表中存储该类的所有虚函数的函数指针。如果子类重写了基类的虚函数,子类的虚表中会包含指向子类实现的函数指针,而不是基类的版本。
虚表指针(vptr):每个对象都包含一个隐藏的指针,称为虚表指针(vptr),它指向该对象所属类的虚表。当对象创建时,构造函数负责初始化 vptr,指向对应类的虚表。
运行时多态:当通过基类指针或引用调用虚函数时,程序会通过对象的 vptr 查找虚表,然后从虚表中获取对应的函数指针并调用。这种机制允许在运行时根据实际的对象类型调用正确的函数版本,即实现动态多态。
虚函数的调用过程:
编译时:编译器无法确定虚函数的具体调用对象。
运行时:程序通过对象的 vptr 找到虚表,根据虚表中的函数指针调用具体的函数。
虚函数表与虚基表的区别
总结:
虚函数表用于动态多态的虚函数调用,通过基类指针调用派生类的函数。
虚基表用于虚继承中处理菱形继承问题,确保基类不会被多次拷贝,派生类只会继承一个基类实例。
11、什么是抽象类?抽象类的作用?
抽象类是包含至少一个纯虚函数的类,不能直接实例化对象,必须通过派生类重写其纯虚函数后才能实例化。抽象类通常作为接口使用,规定派生类必须实现某些功能。
抽象类的特点:
纯虚函数:抽象类中至少有一个纯虚函数,形式为 virtual 函数名() = 0;。
不能实例化:抽象类不能创建对象,必须通过派生类实现纯虚函数后才能实例化派生类对象。
派生类的实现:派生类继承抽象类时,必须实现所有的纯虚函数,否则该派生类也将成为抽象类。
抽象类的作用:
接口设计:抽象类可以定义一组规范,派生类必须按照这些规范去实现。这种方式提供了一种设计接口的机制。
多态性:抽象类常用于多态的实现,通过基类指针或引用调用派生类的具体实现,而不关心派生类的具体类型。
代码复用:抽象类可以为派生类提供公共接口,减少代码重复,让多个派生类共享基础的功能。
抽象类的作用总结:
通过定义接口,规范派生类的行为。
提供多态机制,使代码更加灵活和可扩展。
提供公共的功能和接口,减少代码冗余。