cpp面经
1. 说说C++11的新特性有哪些
C++11是一个里程碑式的更新,引入了大量现代化特性:
- 智能指针:
auto_ptr被废弃,引入unique_ptr,shared_ptr,weak_ptr - 右值引用和移动语义:
&&符号,提高性能 - Lambda表达式:匿名函数,简化代码
- 自动类型推导:
auto和decltype - 范围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++中智能指针的特点
- 自动内存管理:基于RAII,离开作用域自动释放
- 明确的所有权语义:
unique_ptr独占,shared_ptr共享 - 线程安全:
shared_ptr的引用计数操作是原子的 - 异常安全:即使发生异常也能保证资源释放
- 可定制删除器:支持自定义释放逻辑
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. 请你回答一下智能指针有没有内存泄露的情况
有,但主要是设计问题而非机制问题:
- 循环引用:
shared_ptr相互引用(需要用weak_ptr解决) - 静态存储期:将
shared_ptr声明为static,生命周期与程序相同 - 缓存未清理:在缓存中使用
shared_ptr但忘记清理 - lambda捕获:在lambda中捕获
shared_ptr但形成循环引用
10. 简述一下C++11中四种类型转换
static_cast:基本类型转换,有继承关系的类指针转换dynamic_cast:安全的向下转型,需要虚函数表,失败返回nullptrconst_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 中的右值引用和移动语义如何工作?
右值引用:
- 用
&&表示,只能绑定到临时对象(右值) - 区分左值(有名字,可持久)和右值(临时,即将销毁)
移动语义:
- 移动构造函数:从临时对象”窃取”资源,避免深拷贝
- 移动赋值运算符:类似移动构造,用于赋值
工作原理:
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:
- 用于类:禁止继承
- 用于虚函数:禁止进一步重写
class Base final {}; // 不能继承Baseclass Base2 {public: virtual void func() final; // 派生类不能重写func};
优势:
- 增强代码清晰度和安全性
- 使编译器能进行更好的优化
- 防止意外的重写或继承
19. C++11 引入的auto 和 decltype有什么区别?它们的使用场景是什么?
| 特性 | AUTO | DECLTYPE |
|---|---|---|
| 推导规则 | 推导值的类型,忽略引用和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::THREAD | STD::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); }}
注意事项:
- 使用
std::unique_lock而不是std::lock_guard wait会在等待时自动释放锁- 使用
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)如何工作,什么场景下适合使用?
协程:可暂停和恢复的函数,用于异步编程
核心组件:
- 协程句柄:表示协程状态
- promise_type:控制协程行为
co_await:暂停点,等待异步操作co_yield:暂停并返回一个值(生成器)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";}
适用场景:
- 异步I/O:网络请求、文件读写
- 生成器:惰性序列生成
- 状态机:简化复杂的状态切换逻辑
25. 解释 C++14 中std::make_unique的作用?
std::make_unique:创建unique_ptr的工厂函数
优势:
- 异常安全:防止内存泄漏
- 代码简洁:避免重复类型名
- 性能:减少内存分配次数(对象和控制块一次分配)
对比:
// 传统方式(可能内存泄漏)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; // 编译期确定大小
优势:
- 性能:将计算移到编译期
- 类型安全:编译期验证
- 优化:编译器可进行更多优化
27. C++ variant与 union有何区别?
| 特性 | C UNION | STD::VARIANT |
|---|---|---|
| 类型安全 | 不安全,需手动跟踪类型 | 安全,始终知道当前类型 |
| 构造/析构 | 不调用,需手动管理 | 自动调用 |
| 存储 | 原始内存,无类型信息 | 包含类型标签 |
| 访问 | 类型转换,容易出错 | std::visit或std::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; }}
注意:
- 使用RAII类管理锁,避免忘记解锁
- 锁的粒度要适中,太小影响性能,太大会降低并发性
29. 如何避免死锁?举例说明一种策略
死锁产生的四个必要条件:
- 互斥访问
- 持有并等待
- 不可抢占
- 循环等待
避免策略:
- 固定顺序上锁:所有线程以相同顺序获取锁
- 使用
std::lock同时锁定多个互斥量 - 使用超时机制:如
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. 如何实现线程池?
线程池核心组件:
- 任务队列
- 工作线程组
- 同步机制(互斥锁、条件变量)
简单实现:
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; }
优化方法:
- 缓存行对齐:
struct alignas(64) Data {
int a;
};
struct alignas(64) Data2 {
int b;
};
Data data1;
Data2 data2;
- padding填充:
struct Data {
int a;
char padding[64 - sizeof(int)]; // 填充到64字节
int b;
};
34. C++中线程的生命周期?
- 创建:
std::thread t(func, args...) - 运行:线程开始执行函数
- 结束:函数执行完毕,线程变为完成状态
- 加入/分离:
t.join():等待线程完成t.detach():分离线程,让其在后台运行
- 销毁: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 {};
编译器自动生成:
- 默认构造函数:
Empty() - 拷贝构造函数:
Empty(const Empty&) - 拷贝赋值运算符:
Empty& operator=(const Empty&) - 析构函数:
~Empty() - 移动构造函数(C++11):
Empty(Empty&&) - 移动赋值运算符(C++11):
Empty& operator=(Empty&&) - 取地址运算符:
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指针
用途:
- 返回对象自身:用于链式调用
- 区分成员和参数:当同名时
- 自引用:在成员函数中返回*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进程内存布局(从低地址到高地址):
- 代码段(.text):只读,存放机器指令
- 数据段:
- .data:已初始化的全局变量和静态变量
- .bss:未初始化的全局变量和静态变量(程序启动时清零)
- 堆(heap):动态分配的内存,向高地址增长
- 内存映射区域:动态库、文件映射等
- 栈(stack):函数调用信息、局部变量,向低地址增长
- 内核空间:操作系统内核使用
图示:
低地址
+------------------+
| .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思想的体现,自动管理动态内存:
std::unique_ptr:独占所有权,不能拷贝只能移动std::shared_ptr:共享所有权,引用计数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扩容机制:
- 当
size() == capacity()时,再添加元素会触发扩容 - 新容量通常是原来的1.5倍或2倍(GCC是2倍,MSVC是1.5倍)
- 分配新内存,拷贝/移动元素,释放旧内存
- 迭代器、指针和引用失效
示例:
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-5行)
- 频繁调用
- 性能关键路径
注意事项:
- 修改内联函数需要重新编译所有包含它的文件
- 过度使用可能导致代码膨胀
- 虚函数和递归函数不能内联
44. 什么是内存泄漏,如何避免?
内存泄漏:程序在堆上动态分配内存后,没有及时释放,且失去了对该内存区域的引用,导致这部分内存无法被再次使用,最终可能耗尽系统内存。
如何避免:
- 使用智能指针(最有效):
std::unique_ptr、std::shared_ptr自动管理内存。 - 遵循RAII原则:资源获取即初始化,利用对象生命周期管理资源。
- 明确所有权:清楚每个动态分配内存由谁负责释放。
- 使用容器:优先使用
std::vector、std::string等替代原始数组。 - 代码规范:
new/delete、new[]/delete[]成对出现- 释放后立即将指针置为
nullptr - 避免返回指向局部变量的指针
检测工具:
- Valgrind(Linux):
valgrind --leak-check=full ./program - AddressSanitizer:编译时添加
-fsanitize=address - Visual Studio诊断工具(Windows)
45. std::unique_ptr和std::shared_ptr的区别是什么?
| 特性 | STD::UNIQUE_PTR | STD::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. 如何调试和解决内存泄漏问题?
调试步骤:
- 重现问题:确保能稳定复现内存泄漏。
- 使用工具检测:# Linux: Valgrindvalgrind –tool=memcheck –leak-check=full ./program# GCC/Clang: AddressSanitizerg++ -fsanitize=address -g program.cpp -o program
- 分析报告:定位泄漏位置和大小。
- 检查代码:
- 未配对的
new/delete - 异常路径下的资源释放
- 容器未清理
- 循环引用(
shared_ptr)
- 未配对的
解决方法:
- 改用智能指针
- 确保异常安全:使用RAII类
- 规范资源管理:每个
new都有明确的释放点
47. C++的内存分配模型是什么?
C++内存分为多个区域:
- 栈(Stack):自动存储期,函数局部变量
- 堆/自由存储区(Heap/Free Store):动态存储期,
new/malloc分配 - 全局/静态存储区:
.data:已初始化的全局/静态变量.bss:未初始化或零初始化的全局/静态变量
- 常量存储区:字符串常量、
const变量 - 代码区:程序指令
示例:
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; // 未定义行为
预防措施:
- 初始化指针为
nullptr - 释放后立即置空
- 使用智能指针
- 避免返回局部变量地址
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();} // 自动释放资源,即使发生异常
优势:
- 自动释放,避免泄漏
- 异常安全
- 代码简洁,资源管理集中
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格式):
- .text:代码段,存放可执行指令
- .data:已初始化的全局/静态变量
- .bss:未初始化的全局/静态变量
- .rodata:只读数据(字符串常量等)
- .heap:动态分配的内存区域
- .stack:栈区域
- 其他:符号表、调试信息等
程序启动过程:
- 操作系统加载可执行文件到内存
- 设置堆栈指针
- 初始化
.data段(从磁盘复制数据) - 清零
.bss段 - 调用全局对象的构造函数
- 跳转到
main函数
判断分配位置:
- 栈上:函数内的局部变量(非
static)、函数参数 - 堆上:通过
new/malloc分配 - 示例:int global_var; // .bssvoid 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; // .dataint 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++内存分配可能会出现哪些问题?
- 内存泄漏:分配后未释放
- 双重释放:同一内存释放两次
- 悬挂指针:使用已释放的内存
- 野指针:使用未初始化的指针
- 缓冲区溢出:访问分配区域外的内存
- 内存碎片:频繁分配释放导致碎片化
- 对齐问题:未对齐访问可能降低性能或崩溃
- new/delete不匹配:
new[]配delete或反之 - 初始化问题:未初始化内存包含垃圾值
56. 什么是内存碎片,怎么避免内存碎片?
内存碎片:内存被分割成许多小块,无法满足大块内存请求
类型:
- 外部碎片:空闲内存分散在小块中
- 内部碎片:分配的内存比请求的大,剩余部分浪费
避免方法:
- 使用内存池:预分配大块内存,内部管理
- 对象池:复用对象,避免频繁分配释放
- 减少分配次数:批量分配,预分配
- 使用适当数据结构:如
std::deque不要求连续内存 - 碎片整理:移动内存块合并空闲空间(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. 内存对齐应用于哪几种数据类型及其规则
内存对齐:数据在内存中的起始地址必须是其对齐值的整数倍
对齐规则:
- 基本类型对齐:通常按自身大小对齐
char:1字节short:2字节int:4字节double:8字节
- 结构体对齐:
- 对齐值为最大成员的对齐值
- 成员按声明顺序存放,可能需要填充
- 总大小为对齐值的整数倍
示例:
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)
为什么需要对齐:
- 性能:CPU访问对齐内存更快
- 硬件要求:某些架构要求对齐访问
- 原子操作:原子操作通常要求对齐
59. 内存池的作用及其实现方法
内存池的作用:
- 提高性能:减少
malloc/new系统调用开销 - 减少碎片:预分配大块,内部管理
- 提高缓存命中率:连续分配的内存更可能在同一缓存行
- 确定性:分配时间可预测,适合实时系统
简单内存池实现:
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; }};
高级内存池特性:
- 多尺寸支持:维护多个空闲链表对应不同大小
- 线程安全:加锁或使用线程局部存储
- 统计信息:记录分配次数、内存使用量
- 调试支持:记录分配调用栈
使用场景:
- 频繁分配释放小对象(如游戏中的粒子系统)
- 实时系统需要确定性的分配时间
- 减少内存碎片的关键系统
60. std::map和std::unordered_map的区别是什么?
底层数据结构:
std::map:基于红黑树实现,保持元素按键排序std::unordered_map:基于哈希表实现,元素无序存储
性能特点:
| 特性 | STD::MAP | STD::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. 什么是迭代器失效,如何避免?
迭代器失效:当容器发生结构性修改(插入、删除、扩容等)后,原先获取的迭代器、指针或引用不再有效。
常见容器迭代器失效情况:
vector:- 插入:如果导致重新分配,所有迭代器失效;否则,插入点后的迭代器失效
- 删除:删除点后的迭代器失效
push_back:如果重新分配则全部失效
deque:- 在头尾插入:迭代器失效,但元素引用仍有效
- 在中间插入:所有迭代器失效
- 删除:删除点附近的迭代器失效
list/set/map:- 插入:不会使其他迭代器失效
- 删除:仅使被删除元素的迭代器失效
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::SET | STD::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的引用计数是如何工作的?
工作原理:
- 控制块:包含引用计数、弱引用计数、自定义删除器等
- 引用计数:记录有多少个
shared_ptr共享同一对象 - 计数变化:
- 构造时:引用计数为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中容器的时间复杂度
| 容器 | 访问 | 查找 | 插入 | 删除 | 备注 |
|---|---|---|---|---|---|
vector | O(1) | O(n) | 尾O(1),其他O(n) | 尾O(1),其他O(n) | 动态数组 |
deque | O(1) | O(n) | 头尾O(1),中间O(n) | 头尾O(1),中间O(n) | 双端队列 |
list | O(n) | O(n) | O(1)(已知位置) | O(1)(已知位置) | 双向链表 |
forward_list | O(n) | O(n) | O(1)(已知前驱) | O(1)(已知前驱) | 单向链表 |
set/map | O(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)
红黑树特性:
- 节点是红色或黑色
- 根节点是黑色
- 所有叶子节点(NIL)是黑色
- 红色节点的两个子节点都是黑色
- 从任一节点到其每个叶子的所有路径包含相同数目的黑色节点
节点结构:
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; // 节点颜色(红/黑) // 构造函数等...};
操作原理:
- 查找:从根节点开始,比较键值,小于当前节点则向左,大于则向右
- 插入:
- 按二叉搜索树规则插入新节点(红色)
- 调整颜色和旋转以保持红黑树性质
- 删除:
- 找到要删除的节点
- 根据子节点情况调整
- 修复红黑树性质
性能保证:红黑树确保树的高度为O(log n),所有操作都在O(log n)时间内完成
68. 说说STL迭代器是怎么删除元素的
不同容器的删除策略:
- 序列容器(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; }}
- 关联容器(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);}
- 无序容器(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容器动态链接可能产生的问题?
主要问题:
- 内存分配/释放跨DLL边界:
- 在一个DLL中分配的内存,在另一个DLL中释放
- 不同DLL可能有不同的堆管理器,导致崩溃
- 模板实例化不一致:
- 不同DLL使用不同编译器/版本编译
- 模板代码在每个DLL中单独实例化,导致类型不兼容
- 异常处理问题:
- 异常在DLL间传递可能导致未定义行为
- 不同DLL使用不同的异常处理机制
解决方案:
- 隐藏STL容器:// DLL接口使用C风格或抽象接口class IDLLInterface {public: virtual void ProcessData(const void* data, size_t size) = 0; virtual ~IDLLInterface() = default;};
- 使用相同的编译环境:
- 所有DLL使用相同编译器、相同版本、相同编译选项
- 使用PIMPL模式:class MyClassImpl; // 在cpp文件中定义class MyClass {private: std::unique_ptr<MyClassImpl> pimpl;public: MyClass(); ~MyClass(); void SomeMethod();};
- 使用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::VECTOR | STD::LIST |
|---|---|---|
| 数据结构 | 动态数组 | 双向链表 |
| 内存布局 | 连续 | 非连续 |
| 随机访问 | O(1) | O(n) |
| 插入/删除 | 尾O(1),其他O(n) | O(1)(已知位置) |
| 缓存友好 | 是 | 否 |
| 额外开销 | 容量预分配 | 每个节点2个指针 |
| 迭代器类型 | 随机访问迭代器 | 双向迭代器 |
适用场景:
使用vector:
- 需要随机访问元素
- 大部分操作在尾部进行
- 元素数量相对稳定
- 对缓存友好性要求高
// 示例:存储数据并进行随机访问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:
- 频繁在任意位置插入/删除
- 不需要随机访问
- 需要稳定迭代器(插入删除不影响其他迭代器)
// 示例:实现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; // 指向分配内存的末尾};
关键操作:
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;}
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实现细节:
- 内存管理:三指针策略(start, finish, end_of_storage)
- 扩容策略:通常2倍扩容,平摊O(1)的插入成本
- 元素访问:直接指针算术运算
- 插入删除:需要移动后续元素
list实现细节:
- 节点结构:双向链表节点
template<typename T>struct ListNode { T data; ListNode* prev; ListNode* next;};
- 内存管理:每个节点单独分配
- 迭代器:包含节点指针,++操作前进到next,–操作后退到prev
时间复杂度总结:
| 操作 | VECTOR | LIST | 说明 |
|---|---|---|---|
| 访问任意元素 | 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成员函数 |
