C/C++面试八股题(二)
目录:
1.请你说说虚函数的是怎样工作的,大概流程是什么?
2.什么是纯虚函数?有什么作用?
3.什么是多态?多态是需要满足的条件以及优缺点是什么?
4.什么是继承,继承是如何实现的?
5.什么是虚继承,如何实现的?
6.什么是构造函数?
7.什么是析构函数?
8.构造函数和析构函数在继承中的情况?
内容:
1.请你说说虚函数的是怎样工作的,大概流程是什么?
- 定义虚函数首先,在基类中使用关键字virtual来声明一个虚函数。
- 继承和重写(覆盖)虚函数当一个类继承自包含虚函数的基类时,派生类可以选择重写(覆盖)基类中的虚函数。
- 通过基类指针或引用调用虚函数(多态性体现)
- 虚函数表(v - table)机制当一个类包含虚函数时,编译器会为这个类创建一个虚函数表。虚函数表是一个函数指针数组,其中每个元素指向一个虚函数的实现。对于每个包含虚函数的类的对象,对象内存布局的开头会有一个隐藏的指针(通常称为vptr),它指向类的虚函数表。当通过基类指针或引用调用虚函数时,程序会根据对象的vptr找到对应的虚函数表,然后在虚函数表中查找并调用正确的函数。
- 动态绑定和多态当子类继承了父类的时候,子类对象也会继承父类的虚函数表。当子类重写(override)父类中的虚函数时,会将虚函数表中对应的函数地址替换为子类的虚函数地址,从而实现了动态绑定和多态。
2.什么是纯虚函数?有什么作用?
- 纯虚函数(Pure Virtual Function)是在基类中声明但没有提供实现的虚函数。它的声明形式为在函数原型后面加上
= 0
。例:
class Shape { public: virtual double area() = 0; // 这是一个纯虚函数,意味着在Shape类中没有area函数的具体实现 };
- 纯虚函数在基类中起到以下作用:
- 接口定义:纯虚函数主要用于定义接口。它可以让多个不同的派生类遵循相同的函数接口规范,使得程序能够以统一的方式处理不同类型的对象。例如,在图形处理程序中,可以有多种图形(如圆形、三角形等),通过定义
Shape
抽象类中的纯虚函数area
,可以确保每个图形类都有计算面积的功能,并且接口统一。 - 多态性和抽象设计:可以通过基类指针或引用调用纯虚函数,根据对象的实际类型(在运行时确定)来执行相应派生类中的函数实现。这种抽象设计方式可以提高代码的可维护性和可扩展性。例如:在一个绘图系统中,可能会有一个函数接收
Shape
类型的引用,然后调用area
函数来计算要绘制的图形的面积,不管这个图形是矩形、圆形还是其他自定义的图形,只要它是Shape
的派生类并且正确实现了area
函数,这个函数都能正确工作。
3.什么是多态?多态是需要满足的条件以及优缺点是什么?
多态(Polymorphism)是面向对象编程中的一个重要概念,多态就是不同对象对同一行为会有不同的状态。
是指在面向对象编程中,子类对象可以以父类引用的方式存在,从而实现用父类接口调用子类方法的功能。
- 例如,有一个 “动物” 基类,它有 “发出声音” 的行为。“狗”和“猫” 是 “动物” 的派生类,它们都继承了 “发出声音” 这个行为。但是狗的叫声是 “汪汪”,猫的叫声是 “喵喵”。当使用一个统一的方式(比如调用 “发出声音” 这个函数)来处理狗和猫的对象时,它们会表现出不同的行为,这就是多态。
实现多态需要满足的条件:
- 虚函数:基类中被派生类重写的函数必须被声明为虚函数。使用virtual关键字来定义虚函数。
- 继承关系:存在基类(父类)和派生类(子类)之间的继承关系。
- 基类指针或引用调用虚函数:可以创建基类指针或引用,让它们指向派生类对象,然后通过这些指针或引用调用虚函数。在运行时,会根据对象的实际类型来调用相应的函数。
优点:
- 提高代码的可维护性和可扩展性,使代码可以重复利用,并且易于添加新功能。
- 增强程序的灵活性和通用性,统一接口处理多种类型。
缺点:
- 性能开销较大,存在虚函数表和虚指针对内存占用,并且动态绑定的时间成本。
- 增加代码的复杂性,多态的实现涉及到虚函数、派生类的重写、基类指针或引用等概念,这使得代码的结构和逻辑相对复杂。
4.什么是继承,继承是如何实现的?
- 继承是面向对象编程中的一个重要概念,它允许创建一个新类(称为派生类或子类),这个新类可以从一个现有的类(称为基类或父类)获取属性(成员变量)和行为(成员函数)。这就好比子女继承父母的特征一样,派生类继承基类的特性,并且可以在这个基础上添加新的属性和行为,或者修改从基类继承来的属性和行为。
- 继承的实现语法格式使用:(冒号)和public(或private、protected)关键字来实现继承。public表示公有继承,private表示私有继承,protected表示受保护继承。例如:
class Vehicle { // 基类的成员变量和成员函数定义 }; class Car : public Vehicle { // 派生类Car从Vehicle公有继承 // 可以在这里添加新的成员变量和成员函数,并且可以访问Vehicle类的公有和受保护成员 };
- 成员访问控制
- 公有继承:使用public关键字来指定基类与派生类之间的继承关系。在公有继承中,基类的公有成员在派生类中仍然是公有成员,基类的受保护成员在派生类中仍然是受保护成员,基类的私有成员在派生类中不可直接访问。例如,对于上面的Vehicle和Car类,如果Vehicle类有一个公有函数getSpeed(),在Car类中可以直接调用这个函数。
- 私有继承:使用protected关键字基类的公有成员和受保护成员在派生类中都变为私有成员。这种继承方式在实际应用中相对较少使用,因为它会限制派生类对象对基类成员的访问。例如,如果Car类是私有继承自Vehicle,那么Vehicle类的公有函数getSpeed()在Car类中虽然可以访问,但在Car类外部通过Car类对象将无法访问getSpeed()函数。
- 保护继承:使用protected关键字基类的公有成员在派生类中变为受保护成员,基类的受保护成员在派生类中仍然是受保护成员。这种继承方式用于当希望派生类能够访问基类的成员,但又不希望外部通过派生类对象访问这些成员的情况。
5.什么是虚继承,如何实现的?
- 虚继承的定义
- 虚继承是一种在多重继承中解决菱形继承问题的机制。当一个派生类从多个基类派生,而这些基类又有一个共同的基类时,就会出现菱形继承结构。例如,有类 A,类 B 和类 C 都继承自 A,然后类 D 同时继承自 B 和 C,这种结构形状类似菱形。在这种情况下,如果没有虚继承,就可能会导致类 D 中包含两份来自 A 的成员(数据成员和函数成员),这会带来数据冗余和潜在的二义性问题。
- 虚继承通过在继承时使用特定的关键字(在 C++ 中是virtual关键字)来确保在派生类中只有一份共同基类的成员副本。
- 例子
class Base { // 基类成员定义 }; class Derived1 : virtual public Base { // 派生类1的成员定义 }; class Derived2 : virtual public Base { // 派生类2的成员定义 }; class FinalDerived : public Derived1, public Derived2 { // 最终派生类的成员定义 };
- 在这个例子中,
Base
是虚基类,Derived1
和Derived2
虚继承自Base
,然后FinalDerived
继承自Derived1
和Derived2
。这样,在FinalDerived
类中只会有一份Base
类的成员副本,避免了数据冗余。
6.什么是构造函数?
- 构造函数是一种特殊的成员函数,用于在创建对象时初始化对象的成员变量。它与类同名,没有返回值类型(包括
void
)。当创建一个类的对象时,构造函数会自动被调用,以确保对象在使用前处于一个合理的初始状态。 - 例如,对于一个表示二维点的类
Point
,构造函数可以用于初始化点的x
和y
坐标:
class Point { public: int x; int y; Point(int a, int b) { x = a; y = b; } };
- 在这里,
Point(int a, int b)
就是Point
类的构造函数,当创建Point
类的对象时,如Point p(3, 4);
,构造函数会被调用,将p
的x
坐标初始化为 3,y
坐标初始化为 4。
默认构造函数
- 如果一个类没有定义任何构造函数,编译器会自动生成一个默认构造函数。这个默认构造函数会对类的成员变量进行默认初始化(对于基本数据类型,如
int
、double
等会初始化为 0 或类似的默认值;对于类类型成员,会调用其默认构造函数)。
构造函数的作用
- 对象初始化:构造函数确保对象的成员变量在创建时被正确初始化,这对于程序的正确性和稳定性至关重要。如果对象的成员变量没有初始化,可能会导致程序出现未定义行为,如访问未初始化的内存地址导致程序崩溃或产生错误的计算结果。
- 资源分配和初始化相关操作:在构造函数中,除了初始化成员变量,还可以进行一些其他的操作,如动态内存分配(例如,在构造函数中使用
new
关键字为对象的成员指针分配内存)、打开文件、连接数据库等操作,以确保对象在使用前已经完成了所有必要的准备工作。
7.什么是析构函数?
- 析构函数是一种特殊的成员函数,主要用于在对象销毁时执行清理工作。它的名字是在类名前面加上一个波浪号(~),并且没有返回值类型,也不能有参数。
- 例如,对于一个简单的类MyClass,析构函数可以这样定义:
class MyClass { public: ~MyClass() { // 这里进行清理工作 } };
- 自动调用机制 :
- 析构函数会在对象生命周期结束时自动被调用。对象生命周期结束的情况有多种。
- 局部对象:当一个局部对象(在函数内部定义的对象)所在的函数执行结束时,这个对象就会被销毁,析构函数会被调用。例如:
void myFunction() { MyClass obj; // 函数体部分 } // 当myFunction函数执行结束,obj的析构函数会被调用
- 动态分配的对象被释放:如果使用
new
关键字在堆上创建了一个对象,当使用delete
关键字删除这个对象时,析构函数会被调用。例如:
MyClass* ptr = new MyClass(); // 使用ptr指向的对象 delete ptr; // 此时ptr指向对象的析构函数会被调用
- 清理工作的内容:
- 释放资源:最常见的用途是释放对象在生命周期内占用的资源。
- 释放动态分配的内存:如果对象在构造函数或者其他成员函数中使用new操作符分配了内存,析构函数就需要使用delete操作符来释放这些内存。
8.构造函数和析构函数在继承中的情况?
- 构造函数:派生类的构造函数需要调用基类的构造函数来初始化从基类继承来的成员。如果没有在派生类构造函数中显式地调用基类构造函数,编译器会尝试调用基类的默认构造函数。例如:
class Vehicle { public: Vehicle(int s) : speed(s) {} int speed; }; class Car : public Vehicle { public: Car(int s, int n) : Vehicle(s), numberOfSeats(n) {} int numberOfSeats; };
- 这里
Car
类的构造函数通过Vehicle(s)
显式地调用了基类Vehicle
的构造函数来初始化从Vehicle
继承来的speed
成员,同时初始化自己的numberOfSeats
成员。 - 析构函数:在继承关系中,当派生类对象销毁时,析构函数的调用顺序是先调用派生类的析构函数,然后调用基类的析构函数。这是为了确保对象的成员按照正确的顺序释放资源。例如,在
Car
类对象销毁时,先执行Car
类的析构函数,清理Car
类自己分配的资源,然后执行Vehicle
类的析构函数,清理从Vehicle
类继承来的资源。
嵌入式/C++八股 文章被收录于专栏
本人双飞本,校招上岸广和通。此专栏覆盖嵌入式常见面试题,有C/C++相关的知识,数据结构和算法也有嵌入式相关的知识,如操作系统、网络协议、硬件知识。本人也是校招过来的,大家底子甚至项目,可能都不错,但是在面试准备中常见八股可能准备不全。此专栏很适合新手学习基础也适合大佬备战复习,比较全面。最终希望各位友友们早日拿到心仪offer。也希望大家点点赞,收藏,送送小花。这是对我的肯定和鼓励。 持续更新中