1.为什么虚函数能实现动态绑定?

虚函数能实现动态绑定主要通过虚函数表和虚函数指针实现的

动态绑定的具体过程:

  • 调用虚函数:

    当通过基类指针或引用调用虚函数时,编译器生成的代码不会直接调用函数,而是通过虚函数指针查找虚函数表中的函数地址。

  • 查找虚函数表:

    • 编译器会根据对象的虚函数指针找到该对象所属类的虚函数表。
    • 在虚函数表中查找对应虚函数的地址。
  • 调用实际函数:

    • 根据虚函数表中的地址,调用实际的函数版本。
    • 如果派生类重写了虚函数,则调用派生类的版本;否则调用基类的版本。

    注:虚函数表 vs 虚函数指针

    image-20250323153658854

2.如何禁止程序自动生成拷贝构造函数?

  • 方法一:将拷贝构造函数和拷贝赋值函数设置为 private
  • 方法二:直接使用 = delete 来显式删除函数

3.你知道回调函数吗?它的作用?

回调函数就相当于一个中断处理函数,由系统在符合你设定的条件时自动调用。(Qt中的槽函数)

4.介绍一下友元函数和友元类的用法和作用

友元(friend)机制允许某些函数或类访问另一个类的私有(private)和保护(protected)成员。

友元关系打破了封装性,但提供了灵活性。

友元函数:友元函数是一个非成员函数(定义在类外的普通函数,不属于任何类),但它可以访问类的私有和保护成员。友元函数在类的外部定义,但在类内部声明为友元。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyClass {
private:
int privateData;

public:
MyClass(int data) : privateData(data) {}

// 声明友元函数
friend void printPrivateData(const MyClass& obj);
};

// 定义友元函数
void printPrivateData(const MyClass& obj) {
std::cout << "Private data: " << obj.privateData << std::endl;
}

int main() {
MyClass obj(10);
printPrivateData(obj); // 输出: Private data: 10
return 0;
}

友元类: 友元类是一个类,它的所有成员函数都可以访问另一个类的私有和保护成员。友元类在类内部声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyClass {
private:
int privateData;

public:
MyClass(int data) : privateData(data) {}

// 声明友元类
friend class FriendClass;
};

class FriendClass {
public:
void printPrivateData(const MyClass& obj) {
std::cout << "Private data: " << obj.privateData << std::endl;
}
};

int main() {
MyClass obj(20);
FriendClass friendObj;
friendObj.printPrivateData(obj); // 输出: Private data: 20
return 0;
}

注:

  • 封装性:友元机制破坏了封装性,应谨慎使用,避免过度依赖。
  • 单向性:友元关系是单向的且没有传递性。如果类A是类B的友元,类B不会自动成为类A的友元。
  • 继承:友元关系不继承。如果类A是类B的友元,类A的派生类不会自动成为类B的友元。

5.delete和delete[]区别?

  • delete只会调用⼀次析构函数。
  • delete[]会调用数组中每个元素的析构函数。

6.类的对象存储空间?

image-20250323161815483

image-20250323162003652

7.构造函数能否声明为虚函数或者纯虚函数,析构函数呢?

析构函数: 析构函数可以为虚函数,并且一般情况下基类析构函数要定义为虚函数。

注1:只有在基类析构函数定义为虚函数时,调用操作符delete销毁指向对象的基类指针时,才能准确调用派生类的析构函数(从该级向上按序调用虚函数),才能准确销毁数据。

注2:析构函数也可以是纯虚函数。

构造函数:构造函数不能定义为虚函数。

虚函数要使用虚函数指针(vptr)调用,而对象没有进行初始化就没有虚函数指针(vptr)。

8.什么是线程?线程与进程的区别是什么?

  • 线程是操作系统调度的最小单位,进程是资源分配的基本单位。
  • 同一进程内的线程共享内存空间,而不同进程有独立的地址空间。

9.线程同步有哪些常见方式?请简要说明。

  • 互斥锁 (Mutex): 防止多个线程同时访问共享资源。
  • 条件变量 (Condition Variable): 用于线程间的通信。
  • 信号量 (Semaphore): 控制对共享资源的访问数量。
  • 原子操作 (Atomic): 保证某些操作的不可分割性。

10.什么是死锁?如何避免死锁?

定义: 多个线程因为循环等待资源而无法继续执行。

避免方法:

  • 按顺序获取锁。
  • 使用超时机制尝试加锁。

11.什么是线程安全?如何实现线程安全?

线程安全的定义:如果多线程程序每一次的运行结果和单线程程序的运行结果始终一样,那么就是线程安全的

实现方法:

  • 使用互斥锁保护共享资源。
  • 使用原子变量(如 std::atomic

12.什么是线程池?为什么使用线程池?

线程池(Thread Pool)是一种用于管理线程的机制,预先创建一组线程,并将任务提交到一个任务队列中,线程从队列中取出任务并执行。

优点:

  • 减少频繁创建和销毁线程的开销。
  • 提高响应速度。
  • 控制并发线程的数量,避免资源耗尽。

13.什么是原子操作?C++中如何使用原子操作?

原子操作(Atomic Operation):是指在多线程环境下不会被线程调度机制打断的操作,这种操作一旦开始,就会一直运行到结束,中间不会有任何线程切换。原子操作是线程安全的基本保证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <atomic>
#include <iostream>
#include <thread>

std::atomic<int> counter(0); // 原子整型变量
void increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // 原子自增操作
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Counter value: " << counter << std::endl;
return 0;
}

14.在多线程环境下,如何正确地停止一个线程?

使用标志变量控制线程的退出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <thread>
#include <atomic>

std::atomic<bool> stop(false);
void worker() {
while (!stop) {
std::cout << "Working...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
std::cout << "Thread stopped.\n";
}
int main() {
std::thread t(worker);
std::this_thread::sleep_for(std::chrono::seconds(2));
stop = true;
t.join();
return 0;
}

15.线程池是怎么构建的,遇到过什么问题是怎样解决的?

构建线程池的一般步骤:

  • 确定线程池大小:线程池的大小通常基于可用的处理器核心数以及程序的具体需求进行设置。
  • 任务队列:用于存放待执行的任务。
  • 工作线程:这是线程池中的实际工作者。
  • 管理机制:包括添加任务、关闭线程池、处理异常等操作。

问题:

  • 访问任务队列时,要加互斥锁和条件变量
  • 析构函数要正确地退出所有线程,如果没有正确管理线程生命周期,可能会出现线程泄漏的情况,即线程完成任务后没有正常退出。