C++|面试准备二(常考)

一、为什么在公有继承链中基类的析构函数要被声明成虚函数,一个不被继承的类析构函数被声明成虚函数好不好?

(1)公有继承链中基类析构函数声明为虚函数的原因

在公有继承链里,把基类的析构函数声明成虚函数是为了保证在通过基类指针或引用删除派生类对象时,能正确调用派生类和基类的析构函数,避免内存泄漏。

(2)如果一个类不会被继承,把它的析构函数声明为虚函数通常是不好的做法,原因如下:

性能开销 :**虚函数调用需要通过虚函数表来实现,这会带来额外的时间和空间开销。**虚函数表是一个存储类的虚函数地址的数组,每个包含虚函数的对象都会有一个指向虚函数表的指针,这会增加对象的大小。

代码复杂度:虚函数会让代码变得更复杂,增加了理解和维护的难度。

所以,若一个类不会被继承,不应该把它的析构函数声明为虚函数。

二、new一个对象,背后做了哪些事情

在 C++ 里,new 操作符用来动态分配内存并构造对象。下面详细介绍 new 操作符创建对象时所执行的操作:

1. 内存分配

new 表达式首先会调用一个名为 operator new 的函数(通常是全局的或者类特定的重载版本)。

operator new 的职责是分配足够大小的内存块来存储新创建的对象 。这通常涉及在 上查找可用空间。
如果内存分配成功,operator new 会返回一个指向新分配内存的指针;否则,它可能会抛出 std::bad_alloc 异常。

**2.**对象构造:

一旦内存被成功分配,new 表达式接下来会调用对象的构造函数

构造函数负责初始化对象的成员变量 ,并执行其他任何必要的设置或初始化代码。

如果构造函数抛出异常,operator delete 会被自动调用以释放之前分配的内存,防止内存泄漏

3.返回指针:

一旦对象被成功构造,new 表达式会返回一个指向新创建对象的指针

cpp 复制代码
#include <iostream>
using namespace std;
class MyClass {
public:
    // 构造函数
    MyClass() {
        cout << "MyClass 构造函数被调用" << endl;
    }
    // 析构函数
    ~MyClass() {
        cout << "MyClass 析构函数被调用" << endl;
    }
};

int main() {
    // 使用 new 操作符创建对象
    MyClass* obj = new MyClass();

    // 使用对象
    // ...

    // 使用 delete 操作符释放对象
    delete obj;

    return 0;
}

异常处理

若内存分配失败,operator new 函数会抛出 bad_alloc 异常。所以,在使用 new 操作符时,通常需要进行异常处理:

cpp 复制代码
try {
    MyClass* obj = new MyClass();
    // 使用对象
    // ...
    delete obj;
} catch (const bad_alloc& e) {
    cerr << "内存分配失败: " << e.what() << endl;
}

现在更推荐使用智能指针(如 std::unique_ptr 和 std::shared_ptr)来自动管理动态分配的内存的生命周期。

三、extern "C"的作用

格式:extern "C"{};意思是告诉编译器,花括号里的是c代码;

在 C++ 里,extern "C" 是一个特殊的声明,其作用在于告诉 C++ 编译器以 C 语言的方式处理特定的函数或者变量。下面从几个方面详细介绍其作用:

1. 函数名修饰差异

C++ 为了实现函数重载,会对函数名进行修饰,也就是在编译时会在函数名里添加参数类型和返回值类型等信息,以此来生成独一无二的符号名。而 C 语言没有函数重载的概念,不会对函数名进行修饰。使用 extern "C" 能让 C++ 编译器以 C 语言的方式处理函数名,避免函数名被修饰。

2. 实现 C 和 C++ 代码的混合编程

在大型项目里,有时会同时用到 C 和 C++ 代码。使用 extern "C" 可以让 C++ 代码调用 C 语言编写的库,或者让 C 代码调用 C++ 代码里以 C 语言方式编译的部分。

如果让头文件在C和C++代码中都能使用,可采用条件编译和extern"C"的方式

cpp 复制代码
// 头文件 example.h
#ifdef __cplusplus
extern "C" {
#endif

void bar();

#ifdef __cplusplus
}
#endif

// C++代码
#include "example.h"
#include <iostream>

void bar() {
    std::cout << "Bar function called." << std::endl;
}

int main() {
    bar();
    return 0;
}

#ifdef __cplusplus 用来判断是否是 C++ 编译器。如果是 C++ 编译器,就使用 extern "C" 来声明函数;如果是 C 编译器,则直接声明函数。

四、对象的生存周期

对象分为局部对象、全局对象、静态对象、动态分配的对象、对象作为类的成员等几种情况

1. 局部对象

局部对象在函数或者代码块内部被定义,其生存周期从定义处开始,到所在的函数或者代码块结束时终止。

cpp 复制代码
#include <iostream>

void exampleFunction() {
    // 创建局部对象
    int localVar = 10;
    std::cout << "Local variable value: " << localVar << std::endl;
    // 局部对象在函数结束时销毁
}

int main() {
    exampleFunction();
    return 0;
}

2. 全局对象

全局对象在所有函数外部定义,其生存周期从程序开始执行时启动,一直到程序结束才终止。

cpp 复制代码
#include <iostream>

// 定义全局对象
int globalVar = 20;

void anotherFunction() {
    std::cout << "Global variable value: " << globalVar << std::endl;
}

int main() {
    anotherFunction();
    return 0;
}

3. 静态对象

静态对象使用 static 关键字定义,其生存周期从第一次执行到定义处开始,直到程序结束才终止

cpp 复制代码
#include <iostream>

void staticExample() {
    // 定义静态对象
    static int staticVar = 30;
    std::cout << "Static variable value: " << staticVar << std::endl;
    staticVar++;
}

int main() {
    staticExample();
    staticExample();
    return 0;
}

4. 动态分配的对象

动态分配的对象运用 new 运算符创建,使用 delete 运算符销毁。其生存周期从 new 运算符执行时开始,到 delete 运算符执行时结束。

cpp 复制代码
#include <iostream>

int main() {
    // 动态分配对象
    int* dynamicVar = new int(40);
    std::cout << "Dynamic variable value: " << *dynamicVar << std::endl;
    // 销毁动态分配的对象
    delete dynamicVar;
    return 0;
}

5. 对象作为类的成员

对象作为类的成员时,其生存周期与包含它的类对象的生存周期一致

cpp 复制代码
#include <iostream>

class InnerClass {
public:
    InnerClass() {
        std::cout << "InnerClass constructor" << std::endl;
    }
    ~InnerClass() {
        std::cout << "InnerClass destructor" << std::endl;
    }
};

class OuterClass {
public:
    OuterClass() {
        std::cout << "OuterClass constructor" << std::endl;
    }
    ~OuterClass() {
        std::cout << "OuterClass destructor" << std::endl;
    }
    InnerClass innerObj;
};

int main() {
    OuterClass outerObj;
    return 0;
}

五、动态绑定的实现机制

允许在运行时根据对象的实际类型来确定调用哪个方法,而不是在编译时就固定下来。

编译器生成虚函数表

当编译器遇到一个包含虚函数的类时,它会为该类生成一个虚函数表(Virtual Table,简称 V-Table)。虚函数表是一个数组,其中存储了类中所有虚函数的地址。

每个包含虚函数的类都有自己的虚函数表。如果一个类从另一个包含虚函数的类继承,它会继承并扩展这个虚函数表。

对象中存储虚函数表指针

对于每个包含虚函数的类的对象,编译器会在对象中添加一个隐藏的指针,称为虚函数表指针(V-Table Pointer,简称 VPTR)。这个指针指向该对象所属类的虚函数表。

当对象被创建时,其虚函数表指针会被初始化,指向相应类的虚函数表。

调用虚函数时的动态绑定

当通过对象指针或引用调用虚函数时,编译器会生成代码来首先获取对象的虚函数表指针,然后根据虚函数在虚函数表中的索引,找到要调用的实际函数的地址。

在运行时,根据对象的实际类型,其虚函数表指针会指向正确的虚函数表,从而调用到正确的函数。

例如,有一个基类Animal和它的派生类DogAnimal类中有一个虚函数makeSound。当使用Animal类型的指针指向Dog对象并调用makeSound函数时,动态绑定机制会根据对象实际是Dog类型,通过虚函数表找到Dog类中重写的makeSound函数并调用它,而不是调用Animal类中的makeSound函数。

以上为 第二部分的C++方向面试常考题目,如果觉得有帮助可以点赞收藏,会持续更新输出有用的内容,感兴趣可以关注我!

相关推荐
LTPP1 分钟前
Hyperlane:Rust 语言下的轻量级 Web 后端框架教程
前端·面试·架构
努力学习的小廉12 分钟前
【C++】 —— 笔试刷题day_7
开发语言·c++
机智的人猿泰山12 分钟前
java 线程创建Executors 和 ThreadPoolExecutor 和 CompletableFuture 三者 区别
java·开发语言
wuqingshun31415918 分钟前
蓝桥杯 整数变换
数据结构·c++·算法·职场和发展·蓝桥杯
慕容靖翾18 分钟前
APL语言的压力测试
开发语言·后端·golang
努力的搬砖人.22 分钟前
Tomcat相关的面试题
java·经验分享·后端·面试·tomcat
hakesashou40 分钟前
python如何随机选取n个不同的数字
开发语言·python
monstercl1 小时前
【C语言】信号
c语言·开发语言
还是鼠鼠1 小时前
Node.js 模块加载机制--详解
java·开发语言·前端·vscode·前端框架·npm·node.js
禁默1 小时前
C++11之深度理解lambda表达式
开发语言·c++