很好的面试资料: 整理了一年的Linux C++武林秘籍,你早晚用得到(C++进阶必看) - 知乎
转载并简单整理自知乎 程序喵大人
1.左值引用与完美转发等
基本概念:
左值、右值 :放在等号左边,可以取地址并且有名字 的就叫左值,反之叫右值
1 2
| int a = b + c ; int a = 4;
|
纯右值:用于初始化一个对象。 运算表达式产生的临时变量、不和对象关联的原始字面量、返回非引用的函数调用、lambda表达式、this指针
将亡值:代表一个“即将结束生命周期”的对象,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| int main() { std::string str = "Hello World";
std::string new_str = std::move(str);
std::cout << "str after move: \"" << str << "\"" << std::endl; std::cout << "new_str: \"" << new_str << "\"" << std::endl;
return 0; }
A a; auto c = std::move(a);
A a; auto d = static_cast<A&&>(a);
|
左值、右值引用:
右值引用。是一种特殊的引用类型,主要设计用来绑定到右值(特别是将亡值),并表明所引用的对象资源可以被安全地”移动”或”窃取”。
1 2 3 4 5 6 7 8 9
| int a=5; int& b=a; b=4; int& c = 10; const int &d = 10;
int a=4; int&& b=a; int&& c=std::move(a);
|
移动语义
浅拷贝容易导致资源重复释放等问题,深拷贝会导致额外的开销。
因此引入了移动语义,也就是实现了所有权管理,偷掉一些将亡值的资源
C++中所有的STL都实现了移动语义,方便使用。
实现移动语义的关键组件
1.右值引用(T&&)
2.移动构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class MyString { private: char* m_data; size_t m_size; public: MyString(MyString&& other) noexcept : m_data(other.m_data), m_size(other.m_size) { other.m_data = nullptr; other.m_size = 0; } };
|
3.移动赋值运算符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class MyString { public: MyString& operator=(MyString&& other) noexcept { if (this != &other) { delete[] m_data; m_data = other.m_data; m_size = other.m_size; other.m_data = nullptr; other.m_size = 0; } return *this; } };
|
编译器优化:RVO/NRVO
现代编译器会进行返回值优化(RVO)和命名返回值优化(NRVO),有时甚至比移动语义更高效
返回值优化的时机:
- return的值类型与函数的返回值类型相同
- return的是一个局部对象
1 2 3 4 5 6 7 8 9
| MyString create_string() { return MyString("Hello"); }
MyString create_string_nrvo() { MyString result("Hello"); return result; }
|
完美转发
完美转发指的是在函数模板中,将参数以原始的值类别(左值或右值)和const/volatile限定符完全不变地转发给另一个函数。
简单来说:保持参数的所有特性不变地传递下去。
为什么需要完美转发?
在没有完美转发之前,泛型编程中会遇到参数类别丢失的问题:
1 2 3
| void wrapper(Foo& arg) { callee(arg); } void wrapper(const Foo& arg) { callee(arg); } void wrapper(Foo&& arg) { callee(std::move(arg)); }
|
使用模板可以简化
1 2 3 4
| template<typename T> void wrapper(T arg) { callee(arg); }
|
但问题是:
1 2
| template<typename T> void wrapper(T&& arg);
|
这里的T&&
- 当传入右值时:
T 推导为 U,于是参数类型为 U&& → 右值引用;
- 当传入左值时:
T 推导为 U&,于是参数类型为 U& && → 引用折叠 → U&。
引用折叠规则:
& & → &
& && → &
&& & → &
&& && → &&
所以 T&& 在模板中既能绑定左值,也能绑定右值。
std::forward
在 wrapper 中直接写:
会把所有参数当成左值传递,右值的性质丢失。
需要用 std::forward<T>(arg) 来“条件转发”:
- 如果
T 推导为 U&,则 std::forward<T>(arg) 返回 U&。
- 如果
T 推导为 U,则 std::forward<T>(arg) 返回 U&&。
这样右值保持右值,左值保持左值,实现完美转发。
典型写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include <iostream> #include <utility>
void callee(int& x) { std::cout << "callee(int&)\n"; } void callee(const int& x) { std::cout << "callee(const int&)\n"; } void callee(int&& x) { std::cout << "callee(int&&)\n"; }
template<typename T> void wrapper(T&& arg) { callee(std::forward<T>(arg)); }
int main() { int a = 42; const int b = 100; wrapper(a); wrapper(b); wrapper(10); }
|
典型应用场景
1.壳函数
例如日志、装饰器模式、函数调用计时器
1 2 3 4 5
| template<typename F, typename... Args> void log_and_call(F&& f, Args&&... args) { std::cout << "Calling function...\n"; std::forward<F>(f)(std::forward<Args>(args)...); }
|
2.容器构造器
1 2 3 4 5 6 7 8 9 10 11
| template<typename... Args> void emplace_back(Args&&... args) { new (&storage[size++]) T(std::forward<Args>(args)...); }
names.push_back(std::move(temp)); names.emplace_back("David");
|
2.智能指针
C++11 标准库在 <memory> 头文件中提供了三种主要智能指针:
std::unique_ptr
std::shared_ptr
std::weak_ptr
unique_ptr
std::unique_ptr是一个独占型的智能指针,它不允许其它智能指针共享其内部指针,也不允许unique_ptr的拷贝和赋值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <iostream> #include <memory>
struct Foo { Foo() { std::cout << "Foo ctor\n"; } ~Foo() { std::cout << "Foo dtor\n"; } };
int main() { std::unique_ptr<Foo> p1(new Foo()); std::unique_ptr<Foo> p3 = std::move(p1);
if (!p1) std::cout << "p1 is empty\n"; }
|
其内部就是对指针包了一层、然后禁用其拷贝构造、赋值函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| template<typename T, typename Deleter = std::default_delete<T>> class unique_ptr { private: T* ptr; Deleter del; public: explicit unique_ptr(T* p = nullptr) : ptr(p) {} ~unique_ptr() { if (ptr) del(ptr); }
unique_ptr(const unique_ptr&) = delete; unique_ptr& operator=(const unique_ptr&) = delete;
unique_ptr(unique_ptr&& other) noexcept : ptr(other.ptr) { other.ptr = nullptr; } unique_ptr& operator=(unique_ptr&& other) noexcept { if (this != &other) { reset(); ptr = other.ptr; other.ptr = nullptr; } return *this; }
T* get() const { return ptr; } T& operator*() const { return *ptr; } T* operator->() const { return ptr; }
void reset(T* p = nullptr) { if (ptr) del(ptr); ptr = p; } };
|
shared_ptr
shared_ptr 使用了引用计数,每一个shared_ptr的拷贝都指向相同的内存,每次拷贝都会触发引用计数+1, 每次生命周期结束析构的时候引用计数-1,在最后一个shared_ptr析构的时候,内存才会释放。
注意点:
- 可以自定义删除器,在引用计数为0的时候自动调用删除器来释放对象的内存:
std::shared_ptrptr(newint,[](int*p){deletep;});1
| std::shared_ptrptr(newint,[](int*p){deletep;});
|
不要用一个裸指针初始化多个shared_ptr,会出现double_free导致程序崩溃
通过shared_from_this()返回this指针,不要把this指针作为shared_ptr返回出来,因为this指针本质就 是裸指针,通过this返回可能会导致重复析构,不能把this指针交给智能指针管理。
1 2 3 4 5 6
| classA{ shared_ptr<A>GetSelf(){ returnshared_from_this(); } };
|
尽量使用make_shared,少用new。
不要delete get()返回来的裸指针。
要避免循环引用,循环引用导致内存永远不会被释放,造成内存泄漏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <iostream> #include <memory>
struct Node { std::shared_ptr<Node> next; std::shared_ptr<Node> prev; ~Node() { std::cout << "Node destroyed\n"; } };
int main() { auto n1 = std::make_shared<Node>(); auto n2 = std::make_shared<Node>();
n1->next = n2; n2->prev = n1;
std::cout << "main end\n"; }
|
main函数解释时,栈上的 n1 被销毁 -> n1对象 计数 -1 = 1 栈上的 n2 被销毁 -> n2对象 计数 -1 = 1
两者的引用计数都不为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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| template<typename T> class shared_ptr { private: T* ptr; ControlBlock* ctrl;
struct ControlBlock { size_t strong_count; size_t weak_count; ControlBlock() : strong_count(1), weak_count(0) {} };
public: explicit shared_ptr(T* p = nullptr) { ptr = p; if (p) ctrl = new ControlBlock(); else ctrl = nullptr; }
~shared_ptr() { release(); }
void release() { if (ctrl) { if (--ctrl->strong_count == 0) { delete ptr; if (ctrl->weak_count == 0) { delete ctrl; } } } }
shared_ptr(const shared_ptr& other) { ptr = other.ptr; ctrl = other.ctrl; if (ctrl) ctrl->strong_count++; }
shared_ptr(shared_ptr&& other) noexcept { ptr = other.ptr; ctrl = other.ctrl; other.ptr = nullptr; other.ctrl = nullptr; }
size_t use_count() const { return ctrl ? ctrl->strong_count : 0; } };
|
weak_ptr
解决循环引用可能出现内存泄漏的问题
弱引用不参与资源的管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <iostream> #include <memory>
struct Node { std::shared_ptr<Node> next; std::weak_ptr<Node> prev; ~Node() { std::cout << "Node destroyed\n"; } };
int main() { auto n1 = std::make_shared<Node>(); auto n2 = std::make_shared<Node>();
n1->next = n2; n2->prev = n1;
std::cout << "main end\n"; }
|
在使用 weak_ptr 访问对象时,必须检查其有效性。
3.函数式编程 std::fuction与lambda
lambda表达式,
提供匿名函数的方式,快速定义和使用函数对象
要注意悬空引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| std::function<void()> createCallback() { int local_var = 42; return [&]() { std::cout << local_var; }; }
int main() { auto cb = createCallback(); cb(); }
for(int id = enCoef9_Rr ; id <= enCoef9_Bb ;id++){ connect(m_spinBoxs[id],QOverload<double>::of(&QDoubleSpinBox::valueChanged),this,[&](double value){ OnCoefMatrixSlot(id,value); }); }
|
默认情况下,以值方式 [=] 捕获的变量在 Lambda 体中是 const 的。如果你想修改它们,必须在参数列表后加上 mutable 关键字。
1 2 3 4 5 6 7 8 9 10 11 12 13
| int main() { int count = 0;
auto f = [count]() mutable { count++; std::cout << count; }; f(); std::cout << count; }
|
std::fuction
通用的函数包装器,为各种可调用实体(普通函数、函数指针、lambda、std::bind 表达式、函数对象等)提供了一个统一的类型。这使得我们可以像使用普通变量一样存储和传递函数,极大地增加了代码的灵活性。
std::bind
它可以用来绑定一个可调用对象的部分参数,重新排列参数顺序,或者设置默认参数,从而生成一个新的可调用对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #include <iostream> #include <functional> using namespace std::placeholders;
void print_sum(int a, int b, int c) { std::cout << a + b + c << std::endl; }
void print_coordinates(int x, int y, int z) { std::cout << "(" << x << ", " << y << ", " << z << ")\n"; }
class MyClass { public: void member_func(int x, const std::string& msg) { std::cout << "Member func: " << x << ", " << msg << std::endl; } };
int main() { auto bind_func1 = std::bind(print_sum, _1, _2, 10); bind_func1(5, 3);
auto bind_func2 = std::bind(print_coordinates, _3, _1, _2); bind_func2(10, 20, 30);
MyClass obj; auto bind_member = std::bind(&MyClass::member_func, &obj, _1, "Hello"); bind_member(42);
std::function<void(int, int)> func = std::bind(print_sum, _1, _2, 100); func(50, 25);
return 0; }
|
4.线程
C++11 是 C++ 标准中第一次正式引入跨平台的线程库支持的版本,它将并发和多线程编程纳入了语言和标准库,使得开发者不再依赖 pthread、Windows API 等平台特定的接口。
c++11新特性之线程相关所有知识点 - 简书
基础实现:std::thread
用于创建和管理线程。
构造时接收一个可调用对象(函数、lambda、函数对象等),作为线程的入口。
提供 join()(阻塞等待子线程结束)和 detach()(分离线程,让其后台运行)两种线程生命周期管理方式。
禁止拷贝(避免二义性),支持移动语义
1 2 3 4 5 6 7 8 9 10 11
| #include <iostream> #include <thread>
void worker(int id) { std::cout << "Thread " << id << " is running\n"; }
int main() { std::thread t(worker, 1); t.join(); }
|
std::mutex
1 2 3 4 5 6 7 8 9 10 11
| std::mutex m; { std::lock_guard<std::mutex> lock(m); }
std::unique_lock<std::mutex> lock(m); lock.unlock(); lock.lock();
unique_lock开销比lock_guard更大
|
std::condition_variable
用于线程之间同步,必须与互斥量一起使用。
wait(std::unique_lock<std::mutex>& lock, Predicate pred):阻塞当前线程,直到被唤醒且条件谓词 pred 为 true。它会自动释放锁,并在被唤醒后重新获取锁。
notify_one():唤醒一个等待中的线程(如果有)。
notify_all():唤醒所有等待中的线程。
典型的生产者消费者写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| #include <iostream> #include <thread> #include <mutex> #include <condition_variable> #include <queue>
std::queue<int> data_queue; std::mutex mtx; std::condition_variable cv; constexpr int MAX_ITEMS = 10;
void producer() { for (int i = 0; i < MAX_ITEMS; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); { std::lock_guard<std::mutex> lock(mtx); data_queue.push(i); std::cout << "Produced: " << i << std::endl; } cv.notify_one(); } }
void consumer() { while (true) { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, []{ return !data_queue.empty(); });
int value = data_queue.front(); data_queue.pop(); lock.unlock();
std::cout << "Consumed: " << value << std::endl; if (value == MAX_ITEMS - 1) break; } }
int main() { std::thread prod(producer); std::thread cons(consumer);
prod.join(); cons.join(); }
|
std::future 和 std::promise - 异步结果
std::future表示一个未来会产生的值,用get()获取结果,可能会阻塞
std::promise提供结果的生产者,和future成对出现,也就是说一个是生产者,一个是消费者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <iostream> #include <thread> #include <future>
void worker(std::promise<int> p) { int result = 42; p.set_value(result); }
int main() { std::promise<int> p; std::future<int> f = p.get_future();
std::thread t(worker, std::move(p)); std::cout << "Result from worker: " << f.get() << std::endl;
t.join(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <iostream> #include <thread> #include <future>
void worker(std::promise<int> p) { try { throw std::runtime_error("something went wrong"); } catch (...) { p.set_exception(std::current_exception()); } }
int main() { std::promise<int> p; std::future<int> f = p.get_future();
std::thread t(worker, std::move(p));
try { std::cout << f.get() << std::endl; } catch (const std::exception &e) { std::cout << "Caught exception: " << e.what() << std::endl; }
t.join(); }
|
std::packaged_task
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <future> #include <iostream>
int heavy_work(int x) { return x * x; }
int main() { std::packaged_task<int(int)> task(heavy_work); std::future<int> fut = task.get_future();
std::thread t(std::move(task), 10); t.detach();
std::cout << "Result: " << fut.get() << std::endl; return 0; }
|
std::async
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <iostream> #include <future> #include <chrono>
int compute() { std::this_thread::sleep_for(std::chrono::seconds(2)); return 42; }
int main() { std::future<int> fut = std::async(std::launch::async, compute);
std::cout << "Doing other work...\n";
int result = fut.get(); std::cout << "The answer is: " << result << std::endl;
return 0; }
|
std::atomic
std::atomic 是一个模板类(如 std::atomic<int>, std::atomic<bool>),它包装了一个类型,并提供了一系列保证原子操作的成员函数。原子操作意味着该操作从任何线程的视角看,都是不可分割的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <atomic>
std::atomic<int> counter(0);
int new_value = compute_expensive_value(); counter.store(new_value, std::memory_order_release);
int current_value = counter.load(std::memory_order_acquire);
counter.fetch_add(1, std::memory_order_relaxed);
|
关于内存序,这是一个比较复杂的问题,X86\Riscv的不同强弱内存序也有不同定义与实现。
//std::atomic_flag 是专为实现自旋锁而设计的最简单的原子布尔类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
#include <atomic>
class Spinlock { private: std::atomic_flag lock_flag = ATOMIC_FLAG_INIT;
public: void lock() { while (lock_flag.test_and_set(std::memory_order_acquire)) {
} }
void unlock() { lock_flag.clear(std::memory_order_release); } };
Spinlock my_lock; int shared_data = 0;
void critical_section() { my_lock.lock(); shared_data++; my_lock.unlock(); }
|
自旋锁与互斥锁的比较
| 特性 |
自旋锁 (Spinlock) |
互斥锁 (Mutex, e.g., std::mutex) |
| 等待机制 |
忙等待 (Busy-Waiting)。线程在CPU上循环检查,不放弃CPU时间片。 |
阻塞等待 (Blocking-Wait)。线程被操作系统挂起,放入等待队列,放弃CPU时间片。 |
| 开销 |
获取/释放锁的开销极小(主要是原子CPU指令)。但等待期间消耗CPU周期。 |
获取/释放锁的开销较大(需要进入操作系统内核进行线程调度)。但等待期间不消耗CPU。 |
| 适用场景 |
1. 临界区代码非常短(执行速度快)。 2. 等待时间极短。 3. 不希望发生线程上下文切换(因其开销可能比短暂等待更大)。 例如: 内核编程、无锁数据结构、性能关键的底层代码。 |
1. 临界区代码执行时间较长。 2. 等待时间可能较长或不可预测。 3. 适用于绝大多数应用程序级别的并发。 例如: 文件操作、数据库访问、复杂的计算过程。 |
| 缺点 |
浪费CPU资源。如果锁被长时间持有,自旋线程会空转CPU,导致性能下降(“烧CPU”)。 |
上下文切换开销。线程切换需要保存和恢复寄存器状态,开销较大。 |
短期持有用自旋,长期持有用互斥
5.类型推导
auto
让编译器在编译期根据初始化表达式自动推导出变量的类型
decltype
查询一个表达式(而非初始化器)的类型。它返回该表达式的精确类型,包括引用和 const 限定符。