cpp面经

1. 说说C++11的新特性有哪些

C++11是一个里程碑式的更新,引入了大量现代化特性:

  • 智能指针auto_ptr被废弃,引入unique_ptr, shared_ptr, weak_ptr
  • 右值引用和移动语义&&符号,提高性能
  • Lambda表达式:匿名函数,简化代码
  • 自动类型推导autodecltype
  • 范围for循环for (auto& item : container)
  • 空指针常量nullptr替代NULL
  • 委托构造函数和继承构造函数
  • override和final关键字:明确虚函数重写和禁止继承
  • 可变参数模板:支持任意数量的模板参数
  • 标准库扩展std::thread, std::chrono, std::array, std::tuple
  • 静态断言static_assert编译期断言

2. 说说C++中智能指针和指针的区别是什么?

特性普通指针智能指针
内存管理手动管理,需要new/delete自动管理,基于RAII和引用计数
所有权不明确,多个指针可能指向同一对象明确的所有权语义
内存安全容易产生悬空指针、内存泄漏自动释放,避免内存泄漏
性能开销无额外开销有少量运行时开销
线程安全不安全shared_ptr引用计数操作是原子的

3. 说说C++中的智能指针有哪些?分别解决的问题以及区别?

  • unique_ptr
    • 解决的问题:独占所有权的内存管理
    • 特点:同一时间只能有一个unique_ptr指向对象,不支持拷贝,支持移动
    • 适用场景:不需要共享所有权的场景
  • shared_ptr
    • 解决的问题:共享所有权的内存管理
    • 特点:通过引用计数管理,多个shared_ptr可以指向同一对象
    • 适用场景:需要多个指针共享同一对象的场景
  • weak_ptr
    • 解决的问题shared_ptr的循环引用问题
    • 特点:不增加引用计数,需要时可以通过lock()转为shared_ptr
    • 适用场景:打破循环引用,观察者模式

4. 简述C++右值引用与转移语义

  • 右值引用:用&&表示,可以绑定到临时对象(右值)
  • 转移语义:通过std::move将左值转为右值引用,避免不必要的拷贝
  • 示例
std::vector<int> v1 = {1, 2, 3};std::vector<int> v2 = std::move(v1); // 转移,v1现在为空
  • 优势:大幅提升性能,特别是对于包含动态内存的类

5. 简述C++中智能指针的特点

  1. 自动内存管理:基于RAII,离开作用域自动释放
  2. 明确的所有权语义unique_ptr独占,shared_ptr共享
  3. 线程安全shared_ptr的引用计数操作是原子的
  4. 异常安全:即使发生异常也能保证资源释放
  5. 可定制删除器:支持自定义释放逻辑

6. weak_ptr能不能知道对象计数为0,为什么?

可以

  • weak_ptr可以通过expired()方法判断对象是否已被释放
  • weak_ptr不增加引用计数,但会观察引用计数
  • 当所有shared_ptr都被销毁时,expired()返回true

7. weak_ptr如何解决shared_ptr的循环引用问题?

循环引用示例

class B;class A {    shared_ptr<B> b_ptr;};class B {    shared_ptr<A> a_ptr;  // 循环引用!};

解决方案

class B;class A {    shared_ptr<B> b_ptr;};class B {    weak_ptr<A> a_ptr;  // 使用weak_ptr打破循环};

原理weak_ptr不增加引用计数,当其他shared_ptr都释放时,对象可以被正确销毁。


8. 说说智能指针及其实现,shared_ptr线程安全性,原理

实现原理

  • 包含两个指针:一个指向管理对象,一个指向控制块(包含引用计数)
  • 构造时引用计数+1,析构时-1,为0时删除对象

shared_ptr线程安全性

  • 引用计数操作是原子的,线程安全
  • 指向的对象本身不是线程安全的,需要额外同步
  • 多个线程同时读写同一个shared_ptr对象需要同步

9. 请你回答一下智能指针有没有内存泄露的情况

有,但主要是设计问题而非机制问题

  1. 循环引用shared_ptr相互引用(需要用weak_ptr解决)
  2. 静态存储期:将shared_ptr声明为static,生命周期与程序相同
  3. 缓存未清理:在缓存中使用shared_ptr但忘记清理
  4. lambda捕获:在lambda中捕获shared_ptr但形成循环引用

10. 简述一下C++11中四种类型转换

  • static_cast:基本类型转换,有继承关系的类指针转换
  • dynamic_cast:安全的向下转型,需要虚函数表,失败返回nullptr
  • const_cast:移除const/volatile属性
  • reinterpret_cast:低级别的强制转换,不相关类型指针间的转换

11. 简述一下C++11中auto的具体用法

  • 自动类型推导
auto i = 42;           // intauto d = 3.14;         // doubleauto v = std::vector<int>();  // std::vector<int>
  • 简化迭代器
for (auto it = vec.begin(); it != vec.end(); ++it)
  • lambda表达式类型
auto lambda = [](int x) { return x * x; };

12. 简述一下C++11中的可变参数模板新特性

  • 支持任意数量模板参数
template<typename... Args>void func(Args... args);
  • 参数包展开
template<typename... Args>void print(Args... args) {    (std::cout << ... << args);  // C++17折叠表达式}
  • 应用std::make_shared, std::tuple

13. 简述一下C++11中Lambda新特性

语法[捕获列表](参数列表) -> 返回类型 { 函数体 }

捕获方式

  • []:不捕获任何变量
  • [=]:值捕获所有外部变量
  • [&]:引用捕获所有外部变量
  • [x, &y]:值捕获x,引用捕获y

示例

auto sum = [](int a, int b) -> int { return a + b; };auto result = sum(1, 2);  // 3

14. 构造函数初始化列表?

  • 位置:构造函数参数列表后的冒号部分
  • 用途:初始化成员变量和基类
  • 优势
    • 对于const成员和引用成员必须使用
    • 性能更好,避免先默认构造再赋值
    • 初始化顺序与声明顺序一致

示例

class MyClass {    int a;    const int b;public:    MyClass(int x, int y) : a(x), b(y) {}  // 初始化列表};

15. 指向函数的指针–函数指针?

  • 定义:指向函数的指针变量
  • 语法返回类型 (*指针名)(参数列表)
  • 用途:回调函数、函数表、策略模式

示例

int add(int a, int b) { return a + b; }int (*func_ptr)(int, int) = &add;int result = func_ptr(1, 2);  // 3

现代替代std::function和lambda表达式


16. 防止头文件被重复包含?

方法1:预处理指令(传统方式)

#ifndef MY_HEADER_H#define MY_HEADER_H// 头文件内容#endif // MY_HEADER_H

方法2:#pragma once(现代方式)

#pragma once// 头文件内容

对比

  • #ifndef:标准C++,可移植性好,但要确保宏名唯一
  • #pragma once:简洁,编译器优化更好,但不是所有编译器都支持

17. C++11 中的右值引用和移动语义如何工作?

右值引用

  • &&表示,只能绑定到临时对象(右值)
  • 区分左值(有名字,可持久)和右值(临时,即将销毁)

移动语义

  1. 移动构造函数:从临时对象”窃取”资源,避免深拷贝
  2. 移动赋值运算符:类似移动构造,用于赋值

工作原理

class MyString {    char* data;public:    // 移动构造函数    MyString(MyString&& other) noexcept : data(other.data) {        other.data = nullptr;  // 使源对象处于有效但安全的状态    }        // 移动赋值运算符    MyString& operator=(MyString&& other) noexcept {        if (this != &other) {            delete[] data;      // 释放当前资源            data = other.data;  // 窃取资源            other.data = nullptr;        }        return *this;    }};

性能提升:在以下场景避免不必要的拷贝:

  • 函数返回局部对象
  • 容器重新分配(如vector扩容)
  • 标准库算法(如sort)

18. C++11 引入的override 和 final关键字有何作用?为什么要使用它们?

override

  • 明确表示该函数是重写基类的虚函数
  • 如果签名不匹配,编译器会报错,避免意外创建新函数
class Base {public:    virtual void func(int);};​class Derived : public Base {public:    void func(int) override;  // 正确    // void func(double) override;  // 错误:不是基类虚函数};

final

  1. 用于类:禁止继承
  2. 用于虚函数:禁止进一步重写
class Base final {};  // 不能继承Base​class Base2 {public:    virtual void func() final;  // 派生类不能重写func};

优势

  • 增强代码清晰度和安全性
  • 使编译器能进行更好的优化
  • 防止意外的重写或继承

19. C++11 引入的auto 和 decltype有什么区别?它们的使用场景是什么?

特性AUTODECLTYPE
推导规则推导的类型,忽略引用和const推导表达式的精确类型
是否需要初始化必须初始化不需要初始化
主要用途简化复杂类型声明获取表达式的类型,用于元编程

示例

int x = 0;const int& rx = x;​auto a = rx;           // a是int(忽略const和引用)decltype(rx) b = x;    // b是const int&(保留所有修饰符)​// auto的典型用途auto it = vec.begin();  // 简化迭代器类型​// decltype的典型用途template<typename T, typename U>auto add(T t, U u) -> decltype(t + u) {  // 返回类型推导    return t + u;}

20. C++11 中的 std::thread 和std::async有什么区别?什么时候使用哪一个?

特性STD::THREADSTD::ASYNC
抽象级别底层线程接口高级任务抽象
返回值无直接返回值返回std::future
异常处理线程内异常需手动处理异常传播到future.get()
线程管理必须join或detach自动管理(可延迟启动)
线程复用无,每次创建新线程可能使用线程池

使用场景

  • 使用std::thread
    • 需要精细控制线程行为
    • 不需要返回结果
    • 需要长时间运行的后台任务
  • 使用std::async
    • 需要异步执行并获取结果
    • 想利用可能的线程池优化
    • 希望异常能传播回调用者

示例

// std::thread:直接控制std::thread t([]() {    // 执行任务});t.join();​// std::async:任务式auto future = std::async(std::launch::async, []() {    return compute_result();  // 返回计算结果});auto result = future.get();  // 获取结果(可能阻塞)

21. std::move和std::forward 的区别?

std::move

  • 无条件转换为右值引用
  • 用于启用移动语义
  • 实质:static_cast<T&&>(t)

std::forward

  • 条件性转换,保持值类别
  • 用于完美转发
  • 仅用于模板函数中的通用引用

关键区别

template<typename T>void wrapper(T&& arg) {  // 通用引用    // move:总是转为右值(可能错误)    // func(std::move(arg));  // 错误:如果是左值也会被移动        // forward:保持原来的值类别    func(std::forward<T>(arg));  // 正确:左值保持左值,右值转为右值}

完美转发示例

template<typename T, typename... Args>unique_ptr<T> make_unique(Args&&... args) {    return unique_ptr<T>(new T(std::forward<Args>(args)...));}

22. 如何使用std::condition_variable实现生产者-消费者模式?

#include <queue>#include <mutex>#include <condition_variable>​std::queue<int> data_queue;std::mutex mtx;std::condition_variable cond_var;const int MAX_SIZE = 10;​// 生产者void producer() {    while (true) {        std::unique_lock<std::mutex> lock(mtx);                // 等待队列不满        cond_var.wait(lock, []{             return data_queue.size() < MAX_SIZE;         });                // 生产数据        int data = produce_data();        data_queue.push(data);                lock.unlock();        cond_var.notify_one();  // 通知消费者    }}​// 消费者void consumer() {    while (true) {        std::unique_lock<std::mutex> lock(mtx);                // 等待队列不空        cond_var.wait(lock, []{             return !data_queue.empty();         });                // 消费数据        int data = data_queue.front();        data_queue.pop();                lock.unlock();        cond_var.notify_one();  // 通知生产者        process_data(data);    }}

注意事项

  1. 使用std::unique_lock而不是std::lock_guard
  2. wait会在等待时自动释放锁
  3. 使用while循环检查条件,防止虚假唤醒

23. C++17 中的结构化绑定(Structured Bindings)是什么?

结构化绑定:将元组、结构体或数组分解为独立变量

语法auto [var1, var2, ...] = expression;

应用场景

// 1. 结构体/类struct Point { double x, y; };Point p{1.0, 2.0};auto [x, y] = p;  // x=1.0, y=2.0​// 2. 数组int arr[] = {1, 2, 3};auto [a, b, c] = arr;  // a=1, b=2, c=3​// 3. 元组/pairstd::tuple<int, std::string, double> tup(1, "test", 3.14);auto [id, name, value] = tup;​// 4. map遍历(结合范围for)std::map<int, std::string> m = {{1, "one"}, {2, "two"}};for (auto& [key, value] : m) {    std::cout << key << ": " << value << std::endl;}

优势:代码更简洁,避免使用std::get<N>或临时变量


24. C++20 的协程(coroutines)如何工作,什么场景下适合使用?

协程:可暂停和恢复的函数,用于异步编程

核心组件

  1. 协程句柄:表示协程状态
  2. promise_type:控制协程行为
  3. co_await:暂停点,等待异步操作
  4. co_yield:暂停并返回一个值(生成器)
  5. co_return:结束协程

简单协程示例

#include <coroutine>#include <iostream>​struct task {    struct promise_type {        task get_return_object() { return {}; }        std::suspend_never initial_suspend() { return {}; }        std::suspend_never final_suspend() noexcept { return {}; }        void return_void() {}        void unhandled_exception() {}    };};​task my_coroutine() {    std::cout << "Hello ";    co_await std::suspend_always{};    std::cout << "World!\n";}

适用场景

  1. 异步I/O:网络请求、文件读写
  2. 生成器:惰性序列生成
  3. 状态机:简化复杂的状态切换逻辑

25. 解释 C++14 中std::make_unique的作用?

std::make_unique:创建unique_ptr的工厂函数

优势

  1. 异常安全:防止内存泄漏
  2. 代码简洁:避免重复类型名
  3. 性能:减少内存分配次数(对象和控制块一次分配)

对比

// 传统方式(可能内存泄漏)void func() {    Widget* w = new Widget();    process(w);  // 如果process抛出异常,内存泄漏!    delete w;}​// make_unique方式(异常安全)void func() {    auto w = std::make_unique<Widget>();    process(w.get());  // 即使抛出异常,w也会自动释放}

实现原理(简化):

template<typename T, typename... Args>std::unique_ptr<T> make_unique(Args&&... args) {    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));}

26. constexpr在现代 C++ 中的作用是什么?

constexpr:表示编译期常量或编译期可求值的函数

演进

  • C++11:严格限制,函数体只能有一个return
  • C++14:放宽限制,支持循环、局部变量等
  • C++20:支持虚函数、try-catch、dynamic_cast等

应用

// 1. 常量表达式函数constexpr int factorial(int n) {    return n <= 1 ? 1 : n * factorial(n - 1);}​// 2. 编译期计算constexpr int val = factorial(5);  // 编译期计算​// 3. 数组大小int arr[val];  // 有效​// 4. 模板参数template<int N>struct Array {};​Array<val> arr_obj;  // 编译期确定大小

优势

  1. 性能:将计算移到编译期
  2. 类型安全:编译期验证
  3. 优化:编译器可进行更多优化

27. C++ variant与 union有何区别?

特性C UNIONSTD::VARIANT
类型安全不安全,需手动跟踪类型安全,始终知道当前类型
构造/析构不调用,需手动管理自动调用
存储原始内存,无类型信息包含类型标签
访问类型转换,容易出错std::visitstd::get
异常安全
可包含类型POD类型(C++11后可包含非平凡类型但有风险)任意类型

union示例(危险)

union Data {    int i;    double d;    char str[20];};​Data data;data.i = 10;// 错误:以double方式读取int数据std::cout << data.d;  // 未定义行为

variant示例(安全)

std::variant<int, double, std::string> v;​v = 42;  // 存储intv = "hello";  // 存储string​// 安全访问try {    std::cout << std::get<int>(v);  // 抛异常:当前存储的是string} catch (const std::bad_variant_access&) {    // 处理错误}​// 使用visitor模式std::visit([](auto&& arg) {    using T = std::decay_t<decltype(arg)>;    if constexpr (std::is_same_v<T, int>) {        std::cout << "int: " << arg;    } else if constexpr (std::is_same_v<T, std::string>) {        std::cout << "string: " << arg;    }}, v);

建议:在现代C++中,优先使用std::variant,它更安全、更易用。


28. 如何使用std::mutex实现线程同步?

std::mutex是最基本的互斥锁,用于保护共享数据,防止多线程同时访问。

基本用法

#include <iostream>#include <thread>#include <mutex>​std::mutex mtx;int shared_data = 0;​void increment() {    for (int i = 0; i < 10000; ++i) {        mtx.lock();      // 加锁        ++shared_data;   // 临界区操作        mtx.unlock();    // 解锁    }}​int main() {    std::thread t1(increment);    std::thread t2(increment);        t1.join();    t2.join();        std::cout << "Result: " << shared_data << std::endl; // 20000    return 0;}

更安全的方式(RAII)

void increment() {    for (int i = 0; i < 10000; ++i) {        std::lock_guard<std::mutex> lock(mtx); // 构造时加锁,析构时自动解锁        ++shared_data;    }}

注意

  1. 使用RAII类管理锁,避免忘记解锁
  2. 锁的粒度要适中,太小影响性能,太大会降低并发性

29. 如何避免死锁?举例说明一种策略

死锁产生的四个必要条件

  1. 互斥访问
  2. 持有并等待
  3. 不可抢占
  4. 循环等待

避免策略

  1. 固定顺序上锁:所有线程以相同顺序获取锁
  2. 使用std::lock同时锁定多个互斥量
  3. 使用超时机制:如try_lock_for

示例(固定顺序)

std::mutex mtx1, mtx2;

void process1() {
std::lock_guard<std::mutex> lock1(mtx1); // 先锁mtx1
std::lock_guard<std::mutex> lock2(mtx2); // 再锁mtx2
// 操作共享数据
}

void process2() {
std::lock_guard<std::mutex> lock1(mtx1); // 同样先锁mtx1
std::lock_guard<std::mutex> lock2(mtx2); // 再锁mtx2
// 操作共享数据
}

使用std::lock

void safe_process() {    std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);    std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);    std::lock(lock1, lock2); // 同时锁定,避免死锁    // 操作共享数据}

30. 解释 std::future和std::promise的用法

std::promise:在线程间传递结果的机制 std::future:获取异步操作的结果

基本用法

#include <iostream>#include <thread>#include <future>​void compute(std::promise<int>&& prom, int a, int b) {    int result = a + b;    prom.set_value(result); // 设置结果}​int main() {    std::promise<int> prom;    std::future<int> fut = prom.get_future(); // 获取与promise关联的future        std::thread t(compute, std::move(prom), 5, 3);        int result = fut.get(); // 阻塞等待结果    std::cout << "Result: " << result << std::endl; // 8        t.join();    return 0;}

异常传播

void compute_with_exception(std::promise<int>&& prom) {
try {
throw std::runtime_error("Calculation error");
prom.set_value(42);
} catch(...) {
prom.set_exception(std::current_exception()); // 传递异常
}
}

31. 如何实现线程池?

线程池核心组件

  1. 任务队列
  2. 工作线程组
  3. 同步机制(互斥锁、条件变量)

简单实现

class ThreadPool {public:    ThreadPool(size_t thread_count) : stop(false) {        for (size_t i = 0; i < thread_count; ++i) {            workers.emplace_back([this] {                while (true) {                    std::function<void()> task;                    {                        std::unique_lock<std::mutex> lock(queue_mutex);                        condition.wait(lock, [this] {                            return stop || !tasks.empty();                        });                        if (stop && tasks.empty()) return;                        task = std::move(tasks.front());                        tasks.pop();                    }                    task(); // 执行任务                }            });        }    }        template<typename F>    auto enqueue(F&& f) -> std::future<decltype(f())> {        using return_type = decltype(f());        auto task = std::make_shared<std::packaged_task<return_type()>>(            std::forward<F>(f)        );        std::future<return_type> res = task->get_future();        {            std::unique_lock<std::mutex> lock(queue_mutex);            if (stop) throw std::runtime_error("enqueue on stopped ThreadPool");            tasks.emplace([task]() { (*task)(); });        }        condition.notify_one();        return res;    }        ~ThreadPool() {        {            std::unique_lock<std::mutex> lock(queue_mutex);            stop = true;        }        condition.notify_all();        for (std::thread& worker : workers) {            worker.join();        }    }​private:    std::vector<std::thread> workers;    std::queue<std::function<void()>> tasks;    std::mutex queue_mutex;    std::condition_variable condition;    bool stop;};

32. std::atomic在 C++11 中如何实现原子操作?

std::atomic:提供无锁的原子操作,通过CPU指令实现

实现原理

  • 对于简单类型(int, bool等),使用CPU的原子指令
  • 对于复杂类型,可能使用内部锁

常用操作

std::atomic<int> counter(0);​// 原子操作counter.fetch_add(1);              // 原子加counter.fetch_sub(1);              // 原子减int val = counter.load();          // 原子读counter.store(42);                 // 原子写bool success = counter.compare_exchange_weak(expected, desired); // CAS

内存顺序

counter.fetch_add(1, std::memory_order_relaxed);  // 最宽松counter.fetch_add(1, std::memory_order_acquire);  // 获取语义counter.fetch_add(1, std::memory_order_release);  // 释放语义counter.fetch_add(1, std::memory_order_seq_cst);  // 顺序一致性(默认)

33. 解释什么是false sharing及其优化方法?

False Sharing(伪共享):多个线程访问同一缓存行中的不同变量,导致缓存行频繁失效

示例

struct Data {
int a; // 线程1频繁访问
int b; // 线程2频繁访问
};

Data data;

void thread1() { for (int i = 0; i < 1000000; ++i) ++data.a; }
void thread2() { for (int i = 0; i < 1000000; ++i) ++data.b; }

优化方法

  1. 缓存行对齐
struct alignas(64) Data {
int a;
};
struct alignas(64) Data2 {
int b;
};

Data data1;
Data2 data2;
  1. padding填充
struct Data {
int a;
char padding[64 - sizeof(int)]; // 填充到64字节
int b;
};

34. C++中线程的生命周期?

  1. 创建std::thread t(func, args...)
  2. 运行:线程开始执行函数
  3. 结束:函数执行完毕,线程变为完成状态
  4. 加入/分离
    • t.join():等待线程完成
    • t.detach():分离线程,让其在后台运行
  5. 销毁:thread对象析构前必须join或detach

完整示例

void worker() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Worker done\n";
}

int main() {
std::thread t(worker);

// 必须选择以下一种
t.join(); // 等待线程完成
// 或
t.detach(); // 分离线程

return 0;
}

35. 定义一个空类编译器做了哪些操作?

空类class Empty {};

编译器自动生成

  1. 默认构造函数Empty()
  2. 拷贝构造函数Empty(const Empty&)
  3. 拷贝赋值运算符Empty& operator=(const Empty&)
  4. 析构函数~Empty()
  5. 移动构造函数(C++11):Empty(Empty&&)
  6. 移动赋值运算符(C++11):Empty& operator=(Empty&&)
  7. 取地址运算符Empty* operator&()const Empty* operator&() const

验证

Empty e1;            // 默认构造
Empty e2(e1); // 拷贝构造
Empty e3 = e1; // 拷贝构造
e2 = e1; // 拷贝赋值
Empty e4(std::move(e1)); // 移动构造
e3 = std::move(e2); // 移动赋值

大小:空类大小为1字节(用于占位,确保对象有唯一地址)


36. 友元函数和友元类

友元:允许非成员函数或其他类访问私有成员

友元函数

class MyClass {
private:
int secret;
public:
MyClass(int s) : secret(s) {}
friend void showSecret(const MyClass& obj); // 声明友元函数
};

void showSecret(const MyClass& obj) {
std::cout << obj.secret; // 可以访问私有成员
}

友元类

class MyClass {
private:
int secret;
public:
MyClass(int s) : secret(s) {}
friend class FriendClass; // 声明友元类
};

class FriendClass {
public:
void show(const MyClass& obj) {
std::cout << obj.secret; // 可以访问MyClass的私有成员
}
};

特点

  • 友元关系不可传递
  • 友元关系不可继承
  • 破坏了封装,应谨慎使用

37. 什么情况下,类的析构函数应该声明为虚函数?为什么?

当类可能被继承时,基类的析构函数应该声明为虚函数

原因:如果通过基类指针删除派生类对象,而基类析构函数不是虚函数,则只会调用基类析构函数,导致派生类部分资源泄露

示例

class Base {public:    Base() { std::cout << "Base构造\n"; }    virtual ~Base() { std::cout << "Base析构\n"; } // 虚析构函数};​class Derived : public Base {public:    Derived() { std::cout << "Derived构造\n"; }    ~Derived() { std::cout << "Derived析构\n"; }};​int main() {    Base* ptr = new Derived();    delete ptr; // 正确调用Derived和Base的析构函数    return 0;}

输出

Base构造Derived构造Derived析构Base析构

如果Base的析构函数不是虚函数,输出中不会出现”Derived析构”,导致资源泄漏。


38. 编写一个有构造函数,析构函数,赋值函数,和拷贝构造函数的String类?

#include <cstring>#include <iostream>​class MyString {private:    char* data;    size_t length;​public:    // 构造函数    MyString(const char* str = nullptr) {        if (str) {            length = strlen(str);            data = new char[length + 1];            strcpy(data, str);        } else {            length = 0;            data = new char[1];            data[0] = '\0';        }    }​    // 析构函数    ~MyString() {        delete[] data;    }​    // 拷贝构造函数    MyString(const MyString& other) {        length = other.length;        data = new char[length + 1];        strcpy(data, other.data);    }​    // 拷贝赋值运算符    MyString& operator=(const MyString& other) {        if (this != &other) { // 防止自赋值            delete[] data;     // 释放原有资源            length = other.length;            data = new char[length + 1];            strcpy(data, other.data);        }        return *this;    }​    // 移动构造函数(C++11)    MyString(MyString&& other) noexcept         : data(other.data), length(other.length) {        other.data = nullptr;        other.length = 0;    }​    // 移动赋值运算符(C++11)    MyString& operator=(MyString&& other) noexcept {        if (this != &other) {            delete[] data;            data = other.data;            length = other.length;            other.data = nullptr;            other.length = 0;        }        return *this;    }​    // 其他成员函数    const char* c_str() const { return data; }    size_t size() const { return length; }};

39. this指针的理解?

this指针:在每个非静态成员函数中,this是一个指向当前对象的const指针

用途

  1. 返回对象自身:用于链式调用
  2. 区分成员和参数:当同名时
  3. 自引用:在成员函数中返回*this

示例

class MyClass {
int value;
public:
void setValue(int value) {
this->value = value; // 区分成员变量和参数
}

MyClass& increase() {
++value;
return *this; // 返回对象引用,支持链式调用
}

void print() const {
std::cout << this->value; // 显式使用this
}
};

链式调用

MyClass obj;
obj.setValue(5).increase().increase().print(); // 输出7

40. 程序加载时的内存分布?

Linux进程内存布局(从低地址到高地址):

  1. 代码段(.text):只读,存放机器指令
  2. 数据段
    • .data:已初始化的全局变量和静态变量
    • .bss:未初始化的全局变量和静态变量(程序启动时清零)
  3. 堆(heap):动态分配的内存,向高地址增长
  4. 内存映射区域:动态库、文件映射等
  5. 栈(stack):函数调用信息、局部变量,向低地址增长
  6. 内核空间:操作系统内核使用

图示

低地址
+------------------+
| .text | 代码段
+------------------+
| .data | 已初始化数据
+------------------+
| .bss | 未初始化数据
+------------------+
| 堆 | ↑ 动态增长
| | |
| | |
| 内存映射区 | |
| | |
| | |
| 栈 | ↓ 动态增长
+------------------+
高地址

验证代码

#include <iostream>​int global_init = 42;           // .data段int global_uninit;              // .bss段static int static_var = 10;     // .data段​int main() {    int local_var = 5;          // 栈    int* heap_var = new int(7); // 堆        std::cout << "全局初始化: " << &global_init << std::endl;    std::cout << "全局未初始化: " << &global_uninit << std::endl;    std::cout << "静态变量: " << &static_var << std::endl;    std::cout << "局部变量: " << &local_var << std::endl;    std::cout << "堆变量: " << heap_var << std::endl;        delete heap_var;    return 0;}

41. 智能指针?

智能指针是RAII思想的体现,自动管理动态内存:

  1. std::unique_ptr:独占所有权,不能拷贝只能移动
  2. std::shared_ptr:共享所有权,引用计数
  3. std::weak_ptr:弱引用,不增加引用计数

示例

#include <memory>#include <iostream>​// unique_ptr:独占std::unique_ptr<int> up1(new int(10));std::unique_ptr<int> up2 = std::move(up1); // 只能移动​// shared_ptr:共享std::shared_ptr<int> sp1 = std::make_shared<int>(20);std::shared_ptr<int> sp2 = sp1; // 引用计数+1​// weak_ptr:弱引用std::weak_ptr<int> wp = sp1;if (auto spt = wp.lock()) { // 转换为shared_ptr    std::cout << *spt << std::endl;}

自定义删除器

// 文件指针std::unique_ptr<FILE, decltype(&fclose)> file_ptr(fopen("test.txt", "r"), fclose);​// 数组std::unique_ptr<int[]> arr(new int[10]);arr[0] = 1;

42. vector扩容原理说明?

vector扩容机制

  1. size() == capacity()时,再添加元素会触发扩容
  2. 新容量通常是原来的1.5倍或2倍(GCC是2倍,MSVC是1.5倍)
  3. 分配新内存,拷贝/移动元素,释放旧内存
  4. 迭代器、指针和引用失效

示例

std::vector<int> vec;std::cout << "初始容量: " << vec.capacity() << std::endl;​for (int i = 0; i < 100; ++i) {    vec.push_back(i);    std::cout << "size: " << vec.size()               << ", capacity: " << vec.capacity() << std::endl;}

优化策略

// 1. 预分配容量std::vector<int> vec;vec.reserve(100); // 预分配100个元素空间​// 2. 使用emplace_back代替push_back(避免临时对象)vec.emplace_back(42);​// 3. 使用shrink_to_fit释放多余容量vec.shrink_to_fit();

扩容代码模拟

template<typename T>class SimpleVector {    T* data;    size_t size_;    size_t capacity_;        void reallocate(size_t new_capacity) {        T* new_data = new T[new_capacity];        for (size_t i = 0; i < size_; ++i) {            new_data[i] = std::move(data[i]); // 移动元素        }        delete[] data;        data = new_data;        capacity_ = new_capacity;    }    public:    void push_back(const T& value) {        if (size_ == capacity_) {            size_t new_cap = capacity_ == 0 ? 1 : capacity_ * 2;            reallocate(new_cap);        }        data[size_++] = value;    }};

43. 内联函数与普通函数的区别?

特性内联函数普通函数
调用方式编译时展开代码运行时调用
性能减少函数调用开销,可能增大代码体积有调用开销,代码体积小
调试较难调试(无明确调用点)易于调试
定义位置通常在头文件中在源文件中
编译器决定仅是建议,编译器可能忽略必须按函数调用
递归通常不支持支持
虚函数不能是虚函数可以是虚函数

内联函数定义

// 头文件 inline.hinline int max(int a, int b) {    return a > b ? a : b;}​class MyClass {public:    inline void method() { // 类内定义自动为inline        // ...    }        void anotherMethod(); // 声明};​// 类外定义需要显式inlineinline void MyClass::anotherMethod() {    // ...}

使用场景

  1. 函数体小(1-5行)
  2. 频繁调用
  3. 性能关键路径

注意事项

  • 修改内联函数需要重新编译所有包含它的文件
  • 过度使用可能导致代码膨胀
  • 虚函数和递归函数不能内联

44. 什么是内存泄漏,如何避免?

内存泄漏:程序在堆上动态分配内存后,没有及时释放,且失去了对该内存区域的引用,导致这部分内存无法被再次使用,最终可能耗尽系统内存。

如何避免

  1. 使用智能指针(最有效):std::unique_ptrstd::shared_ptr自动管理内存。
  2. 遵循RAII原则:资源获取即初始化,利用对象生命周期管理资源。
  3. 明确所有权:清楚每个动态分配内存由谁负责释放。
  4. 使用容器:优先使用std::vectorstd::string等替代原始数组。
  5. 代码规范
    • new/deletenew[]/delete[]成对出现
    • 释放后立即将指针置为nullptr
    • 避免返回指向局部变量的指针

检测工具

  • Valgrind(Linux):valgrind --leak-check=full ./program
  • AddressSanitizer:编译时添加-fsanitize=address
  • Visual Studio诊断工具(Windows)

45. std::unique_ptr和std::shared_ptr的区别是什么?

特性STD::UNIQUE_PTRSTD::SHARED_PTR
所有权独占所有权,不可拷贝共享所有权,可拷贝
实现简单,无额外开销引用计数,控制块开销
性能接近裸指针引用计数操作有原子开销
使用场景明确唯一所有者的资源多个对象共享的资源
循环引用不存在可能发生,需用weak_ptr解决
自定义删除器需作为模板参数可作为构造函数参数

示例

// unique_ptr:独占,只能移动std::unique_ptr<int> up1(new int(5));// auto up2 = up1; // 错误:不能拷贝auto up2 = std::move(up1); // 正确:转移所有权​// shared_ptr:共享,引用计数std::shared_ptr<int> sp1 = std::make_shared<int>(10);auto sp2 = sp1; // 引用计数变为2

46. 如何调试和解决内存泄漏问题?

调试步骤

  1. 重现问题:确保能稳定复现内存泄漏。
  2. 使用工具检测:# Linux: Valgrindvalgrind –tool=memcheck –leak-check=full ./program​# GCC/Clang: AddressSanitizerg++ -fsanitize=address -g program.cpp -o program
  3. 分析报告:定位泄漏位置和大小。
  4. 检查代码
    • 未配对的new/delete
    • 异常路径下的资源释放
    • 容器未清理
    • 循环引用(shared_ptr

解决方法

  1. 改用智能指针
  2. 确保异常安全:使用RAII类
  3. 规范资源管理:每个new都有明确的释放点

47. C++的内存分配模型是什么?

C++内存分为多个区域:

  1. 栈(Stack):自动存储期,函数局部变量
  2. 堆/自由存储区(Heap/Free Store):动态存储期,new/malloc分配
  3. 全局/静态存储区
    • .data:已初始化的全局/静态变量
    • .bss:未初始化或零初始化的全局/静态变量
  4. 常量存储区:字符串常量、const变量
  5. 代码区:程序指令

示例

int global_var = 1;          // .dataint zero_var;                // .bssconst int const_var = 2;     // 常量区​int main() {    int local_var = 3;       // 栈    int* heap_var = new int; // 堆    static int static_var;   // .bss    delete heap_var;}

48. 解释栈和堆内存,它们的区别?

方面栈(STACK)堆(HEAP)
管理方式编译器自动管理程序员手动管理
分配速度快(移动栈指针)慢(查找合适内存块)
大小限制较小(默认几MB)较大(受系统虚拟内存限制)
生命周期函数执行期间直到显式释放
碎片问题有(外部/内部碎片)
访问安全自动边界检查(某些编译器)无,需程序员保证
生长方向向低地址增长向高地址增长

示例

void function() {    int stack_var = 10;           // 栈上分配    int* heap_var = new int(20);  // 堆上分配    delete heap_var;              // 必须手动释放} // stack_var自动释放

49. 指针悬挂(Dangling Pointer)和野指针(Wild Pointer)的区别?

野指针(Wild Pointer)

  • 定义:未初始化的指针,指向随机内存地址
  • 产生原因:声明后未赋值
  • 示例:int* ptr; // 野指针*ptr = 5; // 未定义行为

悬挂指针(Dangling Pointer)

  • 定义:指向已释放内存的指针
  • 产生原因:内存释放后指针未置空
  • 示例:int* ptr = new int(10);delete ptr; // ptr现在是悬挂指针*ptr = 20;  // 未定义行为

预防措施

  1. 初始化指针为nullptr
  2. 释放后立即置空
  3. 使用智能指针
  4. 避免返回局部变量地址

50. RAII 如何协助资源管理与内存泄漏防止?

RAII(Resource Acquisition Is Initialization):资源获取即初始化,利用对象生命周期管理资源。

工作原理

  • 构造函数获取资源
  • 析构函数释放资源
  • 异常安全:栈展开时自动调用析构函数

示例

class FileHandler {    FILE* file;public:    FileHandler(const char* filename) : file(fopen(filename, "r")) {        if (!file) throw std::runtime_error("Open failed");    }        ~FileHandler() {        if (file) fclose(file);    }        // 禁用拷贝    FileHandler(const FileHandler&) = delete;    FileHandler& operator=(const FileHandler&) = delete;        // 允许移动    FileHandler(FileHandler&& other) noexcept : file(other.file) {        other.file = nullptr;    }        void read() {        // 使用file...    }};​// 使用{    FileHandler fh("test.txt");  // 资源获取    fh.read();} // 自动释放资源,即使发生异常

优势

  1. 自动释放,避免泄漏
  2. 异常安全
  3. 代码简洁,资源管理集中

52. malloc和局部变量分配在堆还是栈?

  • malloc分配在堆上malloc是C标准库函数,从堆中分配内存
  • 局部变量分配在栈上:函数内定义的自动变量(非static)在栈上分配

示例

void example() {
int local_var = 10; // 栈上分配
int* heap_var = (int*)malloc(sizeof(int)); // 堆上分配
*heap_var = 20;
free(heap_var); // 必须手动释放
} // local_var自动释放

注意:C++中通常使用new/delete而非malloc/free


53. 程序有哪些section,分别的作用?程序启动的过程?怎么判断数据分配在栈上还是堆上?

程序的section(ELF格式)

  1. .text:代码段,存放可执行指令
  2. .data:已初始化的全局/静态变量
  3. .bss:未初始化的全局/静态变量
  4. .rodata:只读数据(字符串常量等)
  5. .heap:动态分配的内存区域
  6. .stack:栈区域
  7. 其他:符号表、调试信息等

程序启动过程

  1. 操作系统加载可执行文件到内存
  2. 设置堆栈指针
  3. 初始化.data段(从磁盘复制数据)
  4. 清零.bss
  5. 调用全局对象的构造函数
  6. 跳转到main函数

判断分配位置

  • 栈上:函数内的局部变量(非static)、函数参数
  • 堆上:通过new/malloc分配
  • 示例:int global_var;                 // .bss​void func(int param) {          // param在栈上    int local_var;              // 栈上    static int static_var;      // .bss    int* heap_var = new int;    // 堆上    // …}

54. 初始化为0的全局变量在bss还是data?

在.bss段。因为.bss段专门用于存放未初始化或零初始化的全局/静态变量,这样可节省可执行文件空间。

验证

#include <iostream>​int global_uninit;      // .bssint global_zero = 0;    // .bssint global_val = 42;    // .data​int main() {    static int static_zero = 0;  // .bss    static int static_val = 10;  // .data        std::cout << &global_uninit << std::endl;    std::cout << &global_zero << std::endl;    std::cout << &global_val << std::endl;    return 0;}

55. 你知道C++内存分配可能会出现哪些问题?

  1. 内存泄漏:分配后未释放
  2. 双重释放:同一内存释放两次
  3. 悬挂指针:使用已释放的内存
  4. 野指针:使用未初始化的指针
  5. 缓冲区溢出:访问分配区域外的内存
  6. 内存碎片:频繁分配释放导致碎片化
  7. 对齐问题:未对齐访问可能降低性能或崩溃
  8. new/delete不匹配new[]delete或反之
  9. 初始化问题:未初始化内存包含垃圾值

56. 什么是内存碎片,怎么避免内存碎片?

内存碎片:内存被分割成许多小块,无法满足大块内存请求

类型

  • 外部碎片:空闲内存分散在小块中
  • 内部碎片:分配的内存比请求的大,剩余部分浪费

避免方法

  1. 使用内存池:预分配大块内存,内部管理
  2. 对象池:复用对象,避免频繁分配释放
  3. 减少分配次数:批量分配,预分配
  4. 使用适当数据结构:如std::deque不要求连续内存
  5. 碎片整理:移动内存块合并空闲空间(C++不直接支持)

内存池示例

class MemoryPool {    struct Block {        Block* next;    };        Block* free_list;    size_t block_size;    public:    MemoryPool(size_t size, size_t count) : block_size(size) {        char* memory = new char[block_size * count];        free_list = reinterpret_cast<Block*>(memory);                Block* current = free_list;        for (size_t i = 1; i < count; ++i) {            current->next = reinterpret_cast<Block*>(memory + i * block_size);            current = current->next;        }        current->next = nullptr;    }        void* allocate() {        if (!free_list) return nullptr;        Block* block = free_list;        free_list = free_list->next;        return block;    }        void deallocate(void* ptr) {        Block* block = static_cast<Block*>(ptr);        block->next = free_list;        free_list = block;    }        ~MemoryPool() {        delete[] reinterpret_cast<char*>(free_list);    }};

58. 内存对齐应用于哪几种数据类型及其规则

内存对齐:数据在内存中的起始地址必须是其对齐值的整数倍

对齐规则

  1. 基本类型对齐:通常按自身大小对齐
    • char:1字节
    • short:2字节
    • int:4字节
    • double:8字节
  2. 结构体对齐
    • 对齐值为最大成员的对齐值
    • 成员按声明顺序存放,可能需要填充
    • 总大小为对齐值的整数倍

示例

struct Example {
char a; // 偏移0,大小1
// 填充3字节
int b; // 偏移4,大小4
double c; // 偏移8,大小8
}; // 大小16,对齐8

// 验证
static_assert(sizeof(Example) == 16);
static_assert(alignof(Example) == 8);

控制对齐

// C++11 alignasstruct alignas(16) AlignedStruct {    int a;    double b;};​// 编译器指令#pragma pack(push, 1)struct PackedStruct {    char a;    int b;};#pragma pack(pop)

为什么需要对齐

  1. 性能:CPU访问对齐内存更快
  2. 硬件要求:某些架构要求对齐访问
  3. 原子操作:原子操作通常要求对齐

59. 内存池的作用及其实现方法

内存池的作用

  1. 提高性能:减少malloc/new系统调用开销
  2. 减少碎片:预分配大块,内部管理
  3. 提高缓存命中率:连续分配的内存更可能在同一缓存行
  4. 确定性:分配时间可预测,适合实时系统

简单内存池实现

class SimpleMemoryPool {    struct Block {        Block* next;    };        static const size_t BLOCK_SIZE = 64;    static const size_t POOL_SIZE = 1000;        Block* free_list;    char* memory_pool;    public:    SimpleMemoryPool() {        // 分配一大块内存        memory_pool = new char[BLOCK_SIZE * POOL_SIZE];                // 初始化空闲链表        free_list = reinterpret_cast<Block*>(memory_pool);        Block* current = free_list;                for (size_t i = 1; i < POOL_SIZE; ++i) {            current->next = reinterpret_cast<Block*>(                memory_pool + i * BLOCK_SIZE);            current = current->next;        }        current->next = nullptr;    }        void* allocate() {        if (!free_list) {            // 可扩展:分配新的内存块            return nullptr;        }                Block* block = free_list;        free_list = free_list->next;        return static_cast<void*>(block);    }        void deallocate(void* ptr) {        if (!ptr) return;                Block* block = static_cast<Block*>(ptr);        block->next = free_list;        free_list = block;    }        ~SimpleMemoryPool() {        delete[] memory_pool;    }};

高级内存池特性

  1. 多尺寸支持:维护多个空闲链表对应不同大小
  2. 线程安全:加锁或使用线程局部存储
  3. 统计信息:记录分配次数、内存使用量
  4. 调试支持:记录分配调用栈

使用场景

  • 频繁分配释放小对象(如游戏中的粒子系统)
  • 实时系统需要确定性的分配时间
  • 减少内存碎片的关键系统

60. std::map和std::unordered_map的区别是什么?

底层数据结构

  • std::map:基于红黑树实现,保持元素按键排序
  • std::unordered_map:基于哈希表实现,元素无序存储

性能特点

特性STD::MAPSTD::UNORDERED_MAP
查找复杂度O(log n)平均O(1),最坏O(n)
插入复杂度O(log n)平均O(1),最坏O(n)
删除复杂度O(log n)平均O(1),最坏O(n)
迭代顺序按键升序(稳定)无序(取决于哈希函数)
内存开销较低(树节点)较高(桶+链表/红黑树)

使用要求

  • std::map:键类型必须支持<操作符或自定义比较函数
  • std::unordered_map:键类型必须支持哈希函数和==比较

示例

#include <map>
#include <unordered_map>
#include <iostream>

int main() {
std::map<int, std::string> m1 = {{3, "three"}, {1, "one"}, {2, "two"}};
std::unordered_map<int, std::string> m2 = {{3, "three"}, {1, "one"}, {2, "two"}};

std::cout << "map顺序: ";
for (const auto& p : m1) std::cout << p.first << " "; // 1 2 3

std::cout << "\nunordered_map顺序: ";
for (const auto& p : m2) std::cout << p.first << " "; // 不确定顺序

return 0;
}

61. 什么是迭代器失效,如何避免?

迭代器失效:当容器发生结构性修改(插入、删除、扩容等)后,原先获取的迭代器、指针或引用不再有效。

常见容器迭代器失效情况

  1. vector
    • 插入:如果导致重新分配,所有迭代器失效;否则,插入点后的迭代器失效
    • 删除:删除点后的迭代器失效
    • push_back:如果重新分配则全部失效
  2. deque
    • 在头尾插入:迭代器失效,但元素引用仍有效
    • 在中间插入:所有迭代器失效
    • 删除:删除点附近的迭代器失效
  3. list/set/map
    • 插入:不会使其他迭代器失效
    • 删除:仅使被删除元素的迭代器失效
  4. unordered_map/unordered_set
    • 插入可能导致重新哈希,使所有迭代器失效
    • 删除:仅被删除元素的迭代器失效

如何避免

// 正确做法:使用erase返回值更新迭代器
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ) {
if (*it % 2 == 0) {
it = vec.erase(it); // erase返回下一个有效迭代器
} else {
++it;
}
}

// 或使用算法+erase惯用法
vec.erase(std::remove_if(vec.begin(), vec.end(),
[](int x) { return x % 2 == 0; }),
vec.end());

62. std::set和std::unordered_set的区别是什么?

底层实现

  • std::set:基于红黑树,元素有序
  • std::unordered_set:基于哈希表,元素无序

性能对比

特性STD::SETSTD::UNORDERED_SET
查找O(log n)平均O(1),最坏O(n)
插入O(log n)平均O(1),最坏O(n)
删除O(log n)平均O(1),最坏O(n)
内存每个节点额外2个指针哈希表+桶+链表开销
有序遍历支持不支持

使用示例

#include <set>
#include <unordered_set>
#include <iostream>

int main() {
std::set<int> s1 = {3, 1, 2, 4};
std::unordered_set<int> s2 = {3, 1, 2, 4};

std::cout << "set: ";
for (int x : s1) std::cout << x << " "; // 1 2 3 4

std::cout << "\nunordered_set: ";
for (int x : s2) std::cout << x << " "; // 顺序不确定

return 0;
}

63. 如何删除std::vector中的特定元素?

方法1:erase-remove惯用法(推荐)

#include <vector>#include <algorithm>#include <iostream>​int main() {    std::vector<int> vec = {1, 2, 3, 2, 4, 2, 5};        // 删除所有值为2的元素    vec.erase(std::remove(vec.begin(), vec.end(), 2), vec.end());    // vec: {1, 3, 4, 5}        // 删除满足条件的元素(如所有偶数)    vec.erase(std::remove_if(vec.begin(), vec.end(),                             [](int x) { return x % 2 == 0; }),              vec.end());    // vec: {1, 3, 5}        return 0;}

方法2:手动循环删除

std::vector<int> vec = {1, 2, 3, 2, 4, 2, 5};for (auto it = vec.begin(); it != vec.end(); ) {    if (*it == 2) {        it = vec.erase(it);  // 返回下一个有效迭代器    } else {        ++it;    }}

方法3:交换技巧(如果顺序不重要)

std::vector<int> vec = {1, 2, 3, 2, 4, 2, 5};auto end = vec.end();for (auto it = vec.begin(); it != end; ) {    if (*it == 2) {        --end;        std::iter_swap(it, end);  // 将要删除的交换到末尾    } else {        ++it;    }}vec.erase(end, vec.end());

64. std::shared_ptr的引用计数是如何工作的?

工作原理

  1. 控制块:包含引用计数、弱引用计数、自定义删除器等
  2. 引用计数:记录有多少个shared_ptr共享同一对象
  3. 计数变化
    • 构造时:引用计数为1
    • 拷贝构造:引用计数+1
    • 赋值:原对象引用计数-1,新对象引用计数+1
    • 析构:引用计数-1,为0时删除对象

实现简化

template<typename T>class SharedPtr {    T* ptr;    int* ref_count;    public:    // 构造函数    SharedPtr(T* p = nullptr) : ptr(p), ref_count(new int(1)) {}        // 拷贝构造函数    SharedPtr(const SharedPtr& other)         : ptr(other.ptr), ref_count(other.ref_count) {        ++(*ref_count);    }        // 拷贝赋值运算符    SharedPtr& operator=(const SharedPtr& other) {        if (this != &other) {            // 减少原引用计数            if (--(*ref_count) == 0) {                delete ptr;                delete ref_count;            }            // 拷贝新的            ptr = other.ptr;            ref_count = other.ref_count;            ++(*ref_count);        }        return *this;    }        // 析构函数    ~SharedPtr() {        if (--(*ref_count) == 0) {            delete ptr;            delete ref_count;        }    }};

循环引用问题

struct Node {    std::shared_ptr<Node> next;};​auto node1 = std::make_shared<Node>();auto node2 = std::make_shared<Node>();node1->next = node2;node2->next = node1; // 循环引用,内存泄漏!

65. STL中容器的时间复杂度

容器访问查找插入删除备注
vectorO(1)O(n)尾O(1),其他O(n)尾O(1),其他O(n)动态数组
dequeO(1)O(n)头尾O(1),中间O(n)头尾O(1),中间O(n)双端队列
listO(n)O(n)O(1)(已知位置)O(1)(已知位置)双向链表
forward_listO(n)O(n)O(1)(已知前驱)O(1)(已知前驱)单向链表
set/mapO(log n)O(log n)O(log n)O(log n)红黑树
unordered_set/unordered_map平均O(1)平均O(1)平均O(1)平均O(1)哈希表

66. 如何自己实现一个简化版的std::vector?

template<typename T>class SimpleVector {private:    T* data;          // 动态数组指针    size_t size_;     // 当前元素数量    size_t capacity_; // 当前容量        void reallocate(size_t new_capacity) {        T* new_data = new T[new_capacity];                // 移动已有元素        for (size_t i = 0; i < size_; ++i) {            new_data[i] = std::move(data[i]);        }                delete[] data;        data = new_data;        capacity_ = new_capacity;    }    public:    // 构造函数    SimpleVector() : data(nullptr), size_(0), capacity_(0) {}        // 析构函数    ~SimpleVector() {        delete[] data;    }        // 拷贝构造函数    SimpleVector(const SimpleVector& other)         : size_(other.size_), capacity_(other.size_) {        data = new T[capacity_];        for (size_t i = 0; i < size_; ++i) {            data[i] = other.data[i];        }    }        // 移动构造函数    SimpleVector(SimpleVector&& other) noexcept         : data(other.data), size_(other.size_), capacity_(other.capacity_) {        other.data = nullptr;        other.size_ = 0;        other.capacity_ = 0;    }        // 拷贝赋值运算符    SimpleVector& operator=(const SimpleVector& other) {        if (this != &other) {            delete[] data;            size_ = other.size_;            capacity_ = other.size_;            data = new T[capacity_];            for (size_t i = 0; i < size_; ++i) {                data[i] = other.data[i];            }        }        return *this;    }        // 添加元素    void push_back(const T& value) {        if (size_ == capacity_) {            size_t new_cap = capacity_ == 0 ? 1 : capacity_ * 2;            reallocate(new_cap);        }        data[size_++] = value;    }        void push_back(T&& value) {        if (size_ == capacity_) {            size_t new_cap = capacity_ == 0 ? 1 : capacity_ * 2;            reallocate(new_cap);        }        data[size_++] = std::move(value);    }        // 删除元素    void pop_back() {        if (size_ > 0) {            --size_;        }    }        // 访问元素    T& operator[](size_t index) { return data[index]; }    const T& operator[](size_t index) const { return data[index]; }        // 容量管理    size_t size() const { return size_; }    size_t capacity() const { return capacity_; }    bool empty() const { return size_ == 0; }        // 迭代器支持    T* begin() { return data; }    T* end() { return data + size_; }    const T* begin() const { return data; }    const T* end() const { return data + size_; }};

67. 简述STL中的map的实现原理

底层数据结构:红黑树(Red-Black Tree)

红黑树特性

  1. 节点是红色或黑色
  2. 根节点是黑色
  3. 所有叶子节点(NIL)是黑色
  4. 红色节点的两个子节点都是黑色
  5. 从任一节点到其每个叶子的所有路径包含相同数目的黑色节点

节点结构

template<typename Key, typename Value>struct RBTreeNode {    using value_type = std::pair<const Key, Value>;        value_type data;      // 键值对    RBTreeNode* left;     // 左子树    RBTreeNode* right;    // 右子树    RBTreeNode* parent;   // 父节点    bool color;           // 节点颜色(红/黑)        // 构造函数等...};

操作原理

  • 查找:从根节点开始,比较键值,小于当前节点则向左,大于则向右
  • 插入
    1. 按二叉搜索树规则插入新节点(红色)
    2. 调整颜色和旋转以保持红黑树性质
  • 删除
    1. 找到要删除的节点
    2. 根据子节点情况调整
    3. 修复红黑树性质

性能保证:红黑树确保树的高度为O(log n),所有操作都在O(log n)时间内完成


68. 说说STL迭代器是怎么删除元素的

不同容器的删除策略

  1. 序列容器(vector, deque, list):std::vector<int> vec = {1, 2, 3, 4, 5};​// 方法1:使用erase返回值for (auto it = vec.begin(); it != vec.end(); ) {    if (*it % 2 == 0) {        it = vec.erase(it);  // 返回下一个有效迭代器   } else {        ++it;   }}
  2. 关联容器(set, map):std::set<int> s = {1, 2, 3, 4, 5};​// 方法1:使用erase返回值(C++11起)for (auto it = s.begin(); it != s.end(); ) {    if (*it % 2 == 0) {        it = s.erase(it);  // 返回下一个迭代器   } else {        ++it;   }}​// 方法2:记录要删除的键std::vector<int> to_erase;for (const auto& val : s) {    if (val % 2 == 0) to_erase.push_back(val);}for (int val : to_erase) {    s.erase(val);}
  3. 无序容器(unordered_map, unordered_set):std::unordered_map<int, std::string> um = {{1, “a”}, {2, “b”}, {3, “c”}};​for (auto it = um.begin(); it != um.end(); ) {    if (it->first % 2 == 0) {        it = um.erase(it);  // C++11起支持   } else {        ++it;   }}

重要原则:不要使用被erase的迭代器继续遍历,必须先更新迭代器


69. 说说STL中resize和reserve的区别

特性RESIZE(N)RESERVE(N)
作用改变容器大小改变容器容量
影响元素可能增加或减少元素数量不改变元素数量
新元素如果n>size,添加默认构造的元素不添加新元素
内存可能重新分配内存仅当n>capacity时重新分配
使用场景需要特定数量的元素预分配内存避免多次重新分配

示例

#include <vector>#include <iostream>​int main() {    std::vector<int> vec;        vec.reserve(100);  // 分配100个元素的内存,size=0    std::cout << "capacity: " << vec.capacity()  // 100              << ", size: " << vec.size() << std::endl;  // 0        vec.resize(50);    // size变为50,新增的50个元素默认初始化为0    std::cout << "capacity: " << vec.capacity()  // 100              << ", size: " << vec.size() << std::endl;  // 50        vec.resize(100, 1); // size变为100,新增的50个元素初始化为1    std::cout << "capacity: " << vec.capacity()  // 100              << ", size: " << vec.size() << std::endl;  // 100        vec.resize(30);    // size变为30,删除后面70个元素    std::cout << "capacity: " << vec.capacity()  // 100(不变)              << ", size: " << vec.size() << std::endl;  // 30        return 0;}

70. 说说STL容器动态链接可能产生的问题?

主要问题

  1. 内存分配/释放跨DLL边界
    • 在一个DLL中分配的内存,在另一个DLL中释放
    • 不同DLL可能有不同的堆管理器,导致崩溃
  2. 模板实例化不一致
    • 不同DLL使用不同编译器/版本编译
    • 模板代码在每个DLL中单独实例化,导致类型不兼容
  3. 异常处理问题
    • 异常在DLL间传递可能导致未定义行为
    • 不同DLL使用不同的异常处理机制

解决方案

  1. 隐藏STL容器:// DLL接口使用C风格或抽象接口class IDLLInterface {public:    virtual void ProcessData(const void* data, size_t size) = 0;    virtual ~IDLLInterface() = default;};
  2. 使用相同的编译环境
    • 所有DLL使用相同编译器、相同版本、相同编译选项
  3. 使用PIMPL模式:class MyClassImpl;  // 在cpp文件中定义​class MyClass {private:    std::unique_ptr<MyClassImpl> pimpl;public:    MyClass();    ~MyClass();    void SomeMethod();};
  4. 使用COM或纯虚接口

71. 说说map和unordered_map的区别?底层实现

这个问题与第60题类似,但更强调底层实现:

std::map(红黑树实现)

// 简化节点结构template<typename Key, typename Value>struct TreeNode {    std::pair<const Key, Value> data;    TreeNode* left;    TreeNode* right;    TreeNode* parent;    bool color; // 红或黑};​// 查找过程TreeNode* find(const Key& key) {    TreeNode* current = root;    while (current) {        if (key < current->data.first) {            current = current->left;        } else if (current->data.first < key) {            current = current->right;        } else {            return current; // 找到        }    }    return nullptr;}

std::unordered_map(哈希表实现)

// 简化结构template<typename Key, typename Value>class HashTable {    std::vector<std::list<std::pair<const Key, Value>>> buckets;    size_t element_count;        size_t hash_function(const Key& key) {        return std::hash<Key>()(key) % buckets.size();    }    public:    // 查找过程    Value* find(const Key& key) {        size_t bucket_idx = hash_function(key);        auto& bucket = buckets[bucket_idx];                for (auto& pair : bucket) {            if (pair.first == key) {                return &pair.second;            }        }        return nullptr;    }};

72. 说说vector和list的区别,分别适用于什么场景?

特性STD::VECTORSTD::LIST
数据结构动态数组双向链表
内存布局连续非连续
随机访问O(1)O(n)
插入/删除尾O(1),其他O(n)O(1)(已知位置)
缓存友好
额外开销容量预分配每个节点2个指针
迭代器类型随机访问迭代器双向迭代器

适用场景

使用vector

  1. 需要随机访问元素
  2. 大部分操作在尾部进行
  3. 元素数量相对稳定
  4. 对缓存友好性要求高
// 示例:存储数据并进行随机访问std::vector<float> sensor_readings;for (int i = 0; i < 1000; ++i) {    sensor_readings.push_back(read_sensor());}// 快速访问任意位置float avg = (sensor_readings[100] + sensor_readings[200]) / 2;

使用list

  1. 频繁在任意位置插入/删除
  2. 不需要随机访问
  3. 需要稳定迭代器(插入删除不影响其他迭代器)
// 示例:实现LRU缓存std::list<std::pair<int, std::string>> cache;std::unordered_map<int, decltype(cache)::iterator> map;​void access(int key) {    auto it = map.find(key);    if (it != map.end()) {        // 移动到链表头部        cache.splice(cache.begin(), cache, it->second);    }}

73. 简述vector的实现原理

核心数据结构

template<typename T>class Vector {    T* start;          // 指向数组开始    T* finish;         // 指向最后一个元素的下一个位置    T* end_of_storage; // 指向分配内存的末尾};

关键操作

  1. push_back
void push_back(const T& value) {    if (finish == end_of_storage) {        // 需要扩容        size_t old_size = finish - start;        size_t new_capacity = old_size == 0 ? 1 : old_size * 2;                T* new_start = allocate(new_capacity);        // 移动旧元素到新内存        for (size_t i = 0; i < old_size; ++i) {            construct(new_start + i, std::move(start[i]));            destroy(start + i);        }                deallocate(start);        start = new_start;        finish = start + old_size;        end_of_storage = start + new_capacity;    }        construct(finish, value);    ++finish;}
  1. insert
iterator insert(iterator position, const T& value) {    size_t offset = position - start;        if (finish == end_of_storage) {        // 需要扩容        size_t new_capacity = (finish - start) * 2;        T* new_start = allocate(new_capacity);                // 复制position之前的元素        for (size_t i = 0; i < offset; ++i) {            construct(new_start + i, std::move(start[i]));        }                // 在position处构造新元素        construct(new_start + offset, value);                // 复制position之后的元素        for (size_t i = offset; i < finish - start; ++i) {            construct(new_start + i + 1, std::move(start[i]));        }                // 清理旧内存        for (size_t i = 0; i < finish - start; ++i) {            destroy(start + i);        }        deallocate(start);                start = new_start;        finish = start + (finish - start) + 1;        end_of_storage = start + new_capacity;                return start + offset;    }        // 不需要扩容的情况    if (position != finish) {        // 移动position后的元素        construct(finish, std::move(*(finish - 1)));        std::move_backward(position, finish - 1, finish);        *position = value;    } else {        construct(finish, value);    }        ++finish;    return start + offset;}

74. map和set有什么区别,分别又是怎么实现的?

主要区别

  • map:存储键值对,提供键到值的映射
  • set:只存储键,检查键是否存在

相同点

  • 都基于红黑树实现
  • 元素自动排序(按键)
  • 查找、插入、删除都是O(log n)

实现对比

// set节点(存储键)template<typename Key>struct SetNode {    Key key;    SetNode* left;    SetNode* right;    SetNode* parent;    bool color;};​// map节点(存储键值对)template<typename Key, typename Value>struct MapNode {    std::pair<const Key, Value> data;  // 键是const    MapNode* left;    MapNode* right;    MapNode* parent;    bool color;};

使用示例

#include <map>#include <set>​// map:键值映射std::map<std::string, int> student_scores = {    {"Alice", 90},    {"Bob", 85},    {"Charlie", 95}};int bob_score = student_scores["Bob"];  // 85​// set:唯一键集合std::set<std::string> unique_names = {"Alice", "Bob", "Charlie"};bool has_alice = unique_names.count("Alice") > 0;  // true

75. STL中vector与list具体是怎么实现的?常见操作的时间复杂度是多少?

vector实现细节

  1. 内存管理:三指针策略(start, finish, end_of_storage)
  2. 扩容策略:通常2倍扩容,平摊O(1)的插入成本
  3. 元素访问:直接指针算术运算
  4. 插入删除:需要移动后续元素

list实现细节

  1. 节点结构:双向链表节点
template<typename T>struct ListNode {    T data;    ListNode* prev;    ListNode* next;};
  1. 内存管理:每个节点单独分配
  2. 迭代器:包含节点指针,++操作前进到next,–操作后退到prev

时间复杂度总结

操作VECTORLIST说明
访问任意元素O(1)O(n)vector直接计算地址
在尾部插入平摊O(1)O(1)vector可能扩容
在头部插入O(n)O(1)vector需要移动所有元素
在中间插入O(n)O(1)(已知位置)
删除尾部元素O(1)O(1)
删除头部元素O(n)O(1)
删除中间元素O(n)O(1)(已知位置)
查找元素O(n)O(n)都需要遍历
排序O(n log n)O(n log n)list有特殊sort成员函数