C++高频面试题精选 - 第五部分(题目41-50)

C++高频面试题精选 - 第五部分(题目41-50)

高级主题

题目41: 什么是类型萃取(Type Traits)?如何自定义?

解答:

类型萃取是利用模板元编程在编译期查询和变换类型信息的技术。标准库在<type_traits>头中提供了大量现成的萃取工具,我们也可以自定义。

1. 标准库常用的类型萃取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <type_traits>

// 类型查询
static_assert(std::is_integral_v<int>); // 整数类型?
static_assert(std::is_floating_point_v<double>); // 浮点类型?
static_assert(std::is_pointer_v<int*>); // 指针类型?
static_assert(std::is_reference_v<int&>); // 引用类型?
static_assert(std::is_const_v<const int>); // const修饰?
static_assert(std::is_same_v<int, int>); // 类型相同?

// 类型变换
using T1 = std::remove_const_t<const int>; // → int
using T2 = std::remove_reference_t<int&>; // → int
using T3 = std::add_pointer_t<int>; // → int*
using T4 = std::decay_t<const int&>; // → int

2. 自定义类型萃取 - 检测成员函数存在性:

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
// 检测是否有 toString() 成员函数
template<typename T, typename = void>
struct has_toString : std::false_type {};

template<typename T>
struct has_toString<T, std::void_t<decltype(std::declval<T>().toString())>>
: std::true_type {};

class Foo {
public:
string toString() { return "Foo"; }
};

class Bar {};

static_assert(has_toString<Foo>::value); // true
static_assert(!has_toString<Bar>::value); // true

// 利用萃取结果选择行为
template<typename T>
string convert(const T& obj) {
if constexpr (has_toString<T>::value) {
return obj.toString();
} else {
return "[无toString方法]";
}
}

3. 自定义类型萃取 - 检测类型是否可迭代:

1
2
3
4
5
6
7
8
9
10
11
template<typename T, typename = void>
struct is_iterable : std::false_type {};

template<typename T>
struct is_iterable<T, std::void_t<decltype(std::begin(std::declval<T>())),
decltype(std::end(std::declval<T>()))>>
: std::true_type {};

static_assert(is_iterable<vector<int>>::value); // true
static_assert(is_iterable<string>::value); // true
static_assert(!is_iterable<int>::value); // true

4. 自定义类型萃取 - 获取函数返回类型:

1
2
3
4
5
6
7
8
9
template<typename F, typename... Args>
struct return_type_of {
using type = std::invoke_result_t<F, Args...>;
};

int add(int a, int b) { return a + b; }

using RetType = return_type_of<decltype(&add), int, int>::type;
static_assert(std::is_same_v<RetType, int>);

5. 实际应用 - 根据类型特征优化序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 基本类型直接写入
template<typename T>
std::enable_if_t<std::is_arithmetic_v<T>, void>
serialize(ostream& os, const T& value) {
os.write(reinterpret_cast<const char*>(&value), sizeof(T));
}

// string特殊处理
void serialize(ostream& os, const string& s) {
size_t len = s.size();
os.write(reinterpret_cast<const char*>(&len), sizeof(len));
os.write(s.data(), len);
}

// 容器递归序列化
template<typename T>
std::enable_if_t<is_iterable<T>::value, void>
serialize(ostream& os, const T& container) {
size_t size = std::size(container);
serialize(os, size);
for (const auto& item : container) {
serialize(os, item);
}
}

6. C++20 Concepts 作为替代:

1
2
3
4
5
6
7
8
9
10
// 用concept定义约束,比type_traits更清楚
template<typename T>
concept HasToString = requires(T obj) {
{ obj.toString() } -> std::same_as<string>;
};

template<HasToString T>
string convert(const T& obj) {
return obj.toString();
}

口头解答:
“类型萃取是编译期获取和变换类型信息的技术。标准库提供了大量现成工具,比如is_integral判断是否是整数,remove_const移除const修饰符等。自定义类型萃取通常用void_t和declval组合来检测某个类型是否有特定的成员函数或操作符。这在写通用库代码时非常有用,比如根据类型特征选择不同的序列化策略。C++20的Concepts提供了更清晰的语法来表达这些约束,推荐优先使用Concepts。”


题目42: 解释虚继承和菱形继承问题

解答:

菱形继承是多重继承中一个经典的问题,虚继承是C++提供的解决方案。

1. 菱形继承的问题:

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
class Base {
public:
int value = 10;
void show() { cout << "Base: " << value << "\n"; }
};

class Left : public Base {
public:
void leftFunc() { cout << "Left\n"; }
};

class Right : public Base {
public:
void rightFunc() { cout << "Right\n"; }
};

// 菱形结构:Derived同时继承Left和Right,它们都继承Base
class Derived : public Left, public Right {
};

// 问题1:Derived包含两份Base的副本
// Derived的内存布局:
// +------------------+
// | Left::Base | ← 第一份Base
// | Left自身数据 |
// +------------------+
// | Right::Base | ← 第二份Base
// | Right自身数据 |
// +------------------+

// 问题2:访问Base的成员时有歧义
Derived d;
// d.value; // 编译错误!无法确定是Left::Base::value还是Right::Base::value
d.Left::value = 1; // 必须限定才能访问
d.Right::value = 2; // 两份副本值不同!

2. 虚继承解决方案:

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
class Base {
public:
int value = 10;
void show() { cout << "Base: " << value << "\n"; }
};

// 用virtual关键字声明虚继承
class Left : virtual public Base {
public:
void leftFunc() { cout << "Left\n"; }
};

class Right : virtual public Base {
public:
void rightFunc() { cout << "Right\n"; }
};

class Derived : public Left, public Right {
};

// 虚继承后的内存布局(简化):
// +------------------+
// | vbptr (左) | → 指向Base在内存中的偏移
// | Left自身数据 |
// +------------------+
// | vbptr (右) | → 指向Base在内存中的偏移
// | Right自身数据 |
// +------------------+
// | Base(共享) | ← 只有一份!
// +------------------+

Derived d;
d.value = 42; // 没有歧义了,只有一份Base
d.show(); // 输出:Base: 42

3. 构造顺序的特殊性:

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
class Base {
public:
Base(int v) : value(v) { cout << "Base(" << v << ")\n"; }
int value;
};

class Left : virtual public Base {
public:
Left(int v) : Base(v) { cout << "Left\n"; }
};

class Right : virtual public Base {
public:
Right(int v) : Base(v) { cout << "Right\n"; }
};

class Derived : public Left, public Right {
public:
// 虚基类必须由最派生的类初始化
Derived() : Base(100), Left(1), Right(2) {
cout << "Derived\n";
}
};

// 构造顺序:
// Base(100) ← 虚基类最先,用Derived的初始化参数
// Left ← Left中的Base(1)被忽略
// Right ← Right中的Base(2)被忽略
// Derived

4. 虚继承的代价:

  • 每个虚继承增加一个vbptr(虚基类表指针),增加sizeof
  • 访问虚基类成员需要通过vbptr间接访问,略慢
  • 构造逻辑变得复杂(最派生类负责初始化虚基类)
  • 不能用简单的static_cast转换

5. 实际应用 - iostream体系:

1
2
3
4
5
6
7
8
9
// 标准库中的典型应用
class ios_base { }; // 虚基类

class istream : virtual public ios_base { }; // 输入流
class ostream : virtual public ios_base { }; // 输出流

class iostream : public istream,
public ostream { }; // 输入输出流
// 通过虚继承避免ios_base的菱形继承

6. 设计建议:

虚继承虽然解决了菱形问题,但代价不小。实际设计时应该:

  • 优先考虑组合(has-a)而非继承(is-a)
  • 尽量避免多重继承
  • 如果必须多重继承,考虑用接口类(纯虚函数类,无成员变量)
  • 虚继承仅在真正需要时使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 推荐:用接口(无状态的纯虚类)进行多重继承
class IDrawable {
public:
virtual void draw() = 0;
virtual ~IDrawable() {}
};

class IPrintable {
public:
virtual void print() = 0;
virtual ~IPrintable() {}
};

// 接口类无成员变量,不存在菱形问题
class Document : public IDrawable, public IPrintable {
string content; // 状态只在最底层
public:
void draw() override { }
void print() override { }
};

口头解答:
“菱形继承是多重继承中的经典问题。当A继承B和C,B和C都继承D时,A就包含了两份D的副本,访问D的成员时产生歧义。虚继承通过让D只保留一份共享副本来解决。它的实现原理是通过vbptr间接访问基类,构造时由最派生的类负责初始化虚基类。虚继承有代价:增加内存、间接访问、构造逻辑更复杂。实际开发中应尽量避免,优先用组合。如果确实需要多重继承,最好只继承无状态的接口类(纯虚函数),这样就不存在菱形问题。”


题目43: 什么是类型擦除?如何实现?

解答:

类型擦除是一种设计模式,用于隐藏具体类型的信息,提供统一的接口。它让不同类型的对象可以通过同一接口进行操作,且不要求这些类型具有继承关系。

1. std::function - 标准库的类型擦除:

1
2
3
4
5
6
7
8
9
// std::function隐藏了具体的可调用对象类型
function<int(int, int)> op;

op = [](int a, int b) { return a + b; }; // lambda
op = plus<int>(); // 函数对象
op = add_function; // 函数指针

// 调用者无需知道具体类型
cout << op(3, 4);

2. std::any - 任意类型存储:

1
2
3
4
5
6
7
8
any value;
value = 42;
value = "hello";
value = vector<int>{1, 2, 3};

if (value.type() == typeid(string)) {
cout << any_cast<string>(value);
}

3. 自定义类型擦除实现:

这是理解类型擦除原理的关键。核心思想是:定义一个接口基类(Concept),为每个具体类型生成一个模板包装类(Model),外部只看到接口。

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
55
56
57
58
59
60
61
62
63
64
65
// 目标:实现一个能存储任何"可绘制"对象的容器

// Step1:定义接口
class DrawableConcept {
public:
virtual void draw() const = 0;
virtual unique_ptr<DrawableConcept> clone() const = 0;
virtual ~DrawableConcept() {}
};

// Step2:模板包装类
template<typename T>
class DrawableModel : public DrawableConcept {
T object; // 存储具体对象的副本
public:
DrawableModel(T obj) : object(std::move(obj)) {}

void draw() const override {
object.draw(); // 委托给具体对象
}

unique_ptr<DrawableConcept> clone() const override {
return make_unique<DrawableModel>(*this);
}
};

// Step3:外部接口类(类型擦除的"门面")
class Drawable {
unique_ptr<DrawableConcept> impl;
public:
template<typename T>
Drawable(T obj) : impl(make_unique<DrawableModel<T>>(std::move(obj))) {}

Drawable(const Drawable& other) : impl(other.impl->clone()) {}
Drawable& operator=(const Drawable& other) {
impl = other.impl->clone();
return *this;
}

void draw() const { impl->draw(); }
};

// Step4:使用 - 具体类不需要继承任何基类!
class Circle {
double radius;
public:
Circle(double r) : radius(r) {}
void draw() const { cout << "Drawing circle r=" << radius << "\n"; }
};

class Square {
double side;
public:
Square(double s) : side(s) {}
void draw() const { cout << "Drawing square s=" << side << "\n"; }
};

// 不同类型通过统一接口使用
vector<Drawable> shapes;
shapes.emplace_back(Circle(5.0));
shapes.emplace_back(Square(3.0));

for (const auto& shape : shapes) {
shape.draw(); // 多态调用,但没有继承关系
}

4. 类型擦除 vs 虚函数多态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 虚函数多态:要求继承
class Shape {
public:
virtual void draw() = 0;
};

class Circle : public Shape { // 必须继承Shape
void draw() override { }
};

// 类型擦除:不要求继承
class Circle { // 独立的类,不继承任何东西
public:
void draw() const { }
};

Drawable d(Circle(5.0)); // 直接包装

5. 性能考量:

类型擦除的代价包括:

  • 动态内存分配(存储对象副本)
  • 虚函数调用开销
  • 如果需要拷贝,每次拷贝都是深拷贝

对于性能敏感场景,可以使用小对象优化(SBO):如果对象足够小,直接存储在内部buffer中,避免堆分配。

1
2
3
4
5
6
7
class Drawable {
// 小对象优化:对象 <= 32字节直接存储
alignas(16) char buffer[32];
bool uses_heap;

// ...
};

口头解答:
“类型擦除是一种设计模式,核心目的是隐藏具体类型,提供统一接口。std::function就是最典型的例子,它可以存储任何符合签名的可调用对象。自定义实现的核心思路是三层结构:定义一个接口基类(Concept),用模板生成包装类(Model)来适配具体类型,最外层是对外暴露的门面类。与虚函数多态相比,类型擦除最大的优势是不要求具体类继承某个基类,实现了’结构子类型’——只要满足接口要求就行。代价是有动态分配和虚调用的开销。Rust的dyn trait和Java的接口本质上都是类似的概念。”


题目44: 解释异常处理的最佳实践

解答:

C++的异常处理功能强大,但使用不当容易造成资源泄漏或隐藏bug。

1. 基本抛出和捕获规范:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 抛出:优先抛出值
throw runtime_error("出错了"); // ✓ 抛值

// 捕获:用const引用
try {
riskyOperation();
} catch (const runtime_error& e) { // ✓ const引用
cout << e.what() << "\n";
} catch (const exception& e) { // 基类放后面
cout << "未知异常: " << e.what() << "\n";
} catch (...) { // 最后的兜网
cout << "无法识别的异常\n";
}

2. 异常安全的三个等级:

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
class DataProcessor {
vector<int> data;
int processed_count = 0;

public:
// nothrow保证:绝不抛异常
void reset() noexcept {
data.clear();
processed_count = 0;
}

// 基本保证:异常后对象仍在有效状态,但状态可能改变
void addItem(int item) {
data.push_back(item); // 可能抛bad_alloc
processed_count++; // 如果上面抛了,这行不执行
// 抛异常后:data可能改变,processed_count未变 → 不一致!
}

// 强保证:异常后对象状态恢复到调用前
void addItemStrong(int item) {
vector<int> temp = data; // 先备份
temp.push_back(item); // 对备份操作
data.swap(temp); // swap不抛,原子替换
processed_count++; // 到这里不会抛
}
};

3. RAII保证异常安全:

1
2
3
4
5
6
7
8
9
10
void dangerousOperation() {
// ✗ 手动管理,异常不安全
Resource* r = acquire();
process(r); // 如果这里抛异常
release(r); // 这行不执行,资源泄漏!

// ✓ RAII,异常安全
unique_ptr<Resource> r(acquire());
process(r.get()); // 异常也会自动释放
}

4. 自定义异常类系统:

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
// 建议创建异常层次结构
class AppException : public std::exception {
string message;
public:
explicit AppException(string msg) : message(std::move(msg)) {}
const char* what() const noexcept override { return message.c_str(); }
};

class ValidationError : public AppException {
string field;
public:
ValidationError(string f, string msg)
: AppException("验证错误[" + f + "]: " + msg), field(std::move(f)) {}
const string& getField() const { return field; }
};

class DatabaseError : public AppException {
int error_code;
public:
DatabaseError(int code, string msg)
: AppException("数据库错误[" + to_string(code) + "]: " + msg),
error_code(code) {}
};

// 使用
try {
validateInput(data);
} catch (const ValidationError& e) {
cout << "字段 " << e.getField() << " 验证失败\n";
} catch (const DatabaseError& e) {
cout << "DB错误: " << e.what() << "\n";
} catch (const AppException& e) {
cout << "应用错误: " << e.what() << "\n";
}

5. noexcept的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Widget {
public:
// 移动操作应标记noexcept
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;

// 析构函数默认noexcept(C++11)
~Widget(); // 隐式noexcept

// swap应该noexcept
void swap(Widget& other) noexcept;
};

// noexcept的影响:
// 1. vector在扩容时优先调用noexcept的移动构造
// 2. 编译器可以更好地优化
// 3. 如果标记noexcept但实际抛了,程序终止

6. 不该用异常的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ✗ 用异常做控制流
int find_value(const vector<int>& v, int target) {
try {
for (size_t i = 0; i < v.size(); ++i) {
if (v[i] == target) throw i; // 用异常返回索引 — 恶心!
}
} catch (size_t index) {
return index;
}
return -1;
}

// ✓ 正常控制流
int find_value(const vector<int>& v, int target) {
auto it = find(v.begin(), v.end(), target);
return it != v.end() ? (it - v.begin()) : -1;
}

// ✗ 用异常处理"正常"情况
// ✓ 异常只用于真正的exceptional情况(编程错误、资源耗尽等)

7. 常见陷阱:

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
// 陷阱1:析构函数中抛异常
class Bad {
public:
~Bad() {
cleanup(); // 如果抛异常,程序终止!
}
};

// 正确做法
class Good {
public:
~Good() {
try {
cleanup();
} catch (...) {
// 记录错误,不抛出
log_error("cleanup failed");
}
}
};

// 陷阱2:catch(exception)而不是catch(const exception&)
try { } catch (exception e) { } // ✗ 切片!
try { } catch (const exception& e) { } // ✓

// 陷阱3:捕获顺序错误
try { }
catch (const exception& e) { } // ✗ 太宽泛,放在前面
catch (const runtime_error& e) { } // ✗ 永远到不了这里

口头解答:
“C++异常处理最重要的几个原则:一是抛值捕引用,避免对象切片;二是RAII保证异常安全,资源管理全靠对象生命周期;三是异常安全有三个等级——nothrow、基本保证和强保证,强保证最好但代价也最大;四是自定义异常类应该有层次结构,便于分类捕获;五是移动构造和swap要标记noexcept,否则vector扩容时会退回到拷贝;六是不要用异常做控制流,异常是真正exceptional情况的处理机制;七是析构函数绝不抛异常,要catch所有。”


题目45: std::move和std::forward的深度理解

解答:

很多人对move和forward的理解停留在表面。理解它们的关键在于:它们本身不做任何”移动”或”转发”操作,它们只是类型转换工具。

1. std::move - 无条件转换为右值引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
// move的实质
template<typename T>
typename remove_reference<T>::type&& move(T&& t) noexcept {
return static_cast<typename remove_reference<T>::type&&>(t);
}

// 它做的就是把任何东西转换成右值引用
int x = 10;
int&& rx = std::move(x); // 把左值x转成右值引用

// "移动"真正发生在接收端(移动构造/赋值)
string s1 = "hello";
string s2 = std::move(s1); // move只是转换类型,string的移动构造才真正移动

2. std::forward - 条件转换,保持值类别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// forward的实质(简化)
template<typename T>
T&& forward(typename remove_reference<T>::type& t) noexcept {
return static_cast<T&&>(t);
}

// 关键区别:
// - move:总是转换为右值引用
// - forward:保持原来的值类别(左值还是左值,右值还是右值)

template<typename T>
void wrapper(T&& arg) { // T&&是万能引用
target(std::forward<T>(arg)); // 保持arg的值类别
}

int x = 10;
wrapper(x); // arg是左值引用 → forward保持为左值
wrapper(42); // arg是右值引用 → forward保持为右值

3. 万能引用 vs 右值引用的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 右值引用:只绑定右值
void func(int&& x) { } // 只接受右值

// 万能引用:绑定任何东西(必须是模板参数 + &&)
template<typename T>
void func(T&& x) { } // 左值右值都接受

// 万能引用的条件:
// 1. 必须是模板参数
// 2. 必须是 T&& 形式(不能是 const T&&, vector<T>&&等)

class Widget {
public:
void func(Widget&& w) { } // ✗ 不是万能引用,是右值引用
};

template<typename T>
void func(const T&& arg) { } // ✗ 不是万能引用,const阻止了推导

4. 引用折叠规则(完美转发的理论基础):

1
2
3
4
5
6
7
8
9
10
11
12
13
// 当模板参数推导时,引用会按规则"折叠":
// T = int& → T&& = int& && → int& (左值引用)
// T = int → T&& = int && → int&& (右值引用)

template<typename T>
void wrapper(T&& arg) {
// 传入左值: T = int&, arg类型 = int& && = int&
// 传入右值: T = int, arg类型 = int&&

// 在函数体内,arg总是左值(有名字)
// 所以必须forward来恢复原来的值类别
target(std::forward<T>(arg));
}

5. 常见错误与正确用法:

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
// 错误1:对万能引用用move而不是forward
template<typename T>
void wrong(T&& arg) {
target(std::move(arg)); // ✗ 总是当右值,丢失左值信息
}

// 正确:用forward
template<typename T>
void right(T&& arg) {
target(std::forward<T>(arg)); // ✓ 保持值类别
}

// 错误2:返回局部变量时用move
Widget create() {
Widget w;
return std::move(w); // ✗ 阻止RVO
}

// 正确
Widget create() {
Widget w;
return w; // ✓ 编译器优化
}

// 错误3:move const对象
const Widget w;
Widget w2 = std::move(w); // ✗ move无效,调用拷贝构造
// const T&&绑定到const Widget&&, 但移动构造参数是Widget&&, 不匹配
// 退回到 const Widget& → 拷贝构造

// 错误4:对函数参数用forward而不是move
void func(Widget&& w) { // 右值引用参数
use(std::forward<Widget>(w)); // ✗ 语义混乱
use(std::move(w)); // ✓ 右值引用用move
}

6. 总结一句话:

1
2
3
4
万能引用 T&& → 用 std::forward<T>
右值引用 X&& → 用 std::move
非模板参数 → 不用任何转换
返回局部变量 → 直接返回,不加move/forward

口头解答:
“很多人以为move真的在’移动’东西,forward真的在’转发’。实际上它们都只是类型转换。move无条件把东西转成右值引用,告诉编译器可以用移动构造;forward条件转换,保持原来的左值或右值身份。真正理解完美转发的关键是万能引用和引用折叠。万能引用是模板参数+&&的形式,它能绑定左值和右值。在函数体内,所有参数都是左值,所以需要forward来恢复原来的值类别。常见的错误是在万能引用中用move,或者返回局部变量时加move阻止RVO。简单记住:万能引用用forward,右值引用用move,返回局部变量什么都不加。”


题目46: 解释内存序(Memory Ordering)

解答:

内存序控制了多核CPU之间内存操作的可见性和顺序,是并发编程中最难以理解的概念之一。

1. 为什么需要内存序:

1
2
3
4
5
6
7
8
9
10
11
12
// 线程1
data = 1; // 写数据
flag.store(true); // 设置标志

// 线程2
if (flag.load()) {
cout << data; // 期望读到1,但可能读到0!
}

// 问题:编译器和CPU都可能重排指令
// CPU为了性能,可能先执行flag的store,再执行data的写
// 线程2看到flag=true时,data可能还是旧值

2. 六种内存序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 从宽松到严格:
// relaxed < acquire/release < seq_cst

// memory_order_relaxed:只保证原子性,不保证顺序
atomic<int> counter;
counter.fetch_add(1, memory_order_relaxed); // 只需原子递增

// memory_order_acquire:读操作,后面的读写不能被提前
// memory_order_release:写操作,前面的读写不能被延迟
flag.store(true, memory_order_release); // 线程1:release写
if (flag.load(memory_order_acquire)) { } // 线程2:acquire读

// memory_order_seq_cst:顺序一致(默认),最严格
flag.store(true); // 默认seq_cst
flag.load(); // 默认seq_cst

3. acquire-release配对:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
atomic<bool> ready(false);
int data = 0;

// 生产者(release)
void producer() {
data = 42; // 写数据
ready.store(true, memory_order_release); // release保证前面的写在此之前可见
}

// 消费者(acquire)
void consumer() {
while (!ready.load(memory_order_acquire)) { } // acquire保证后面读到的数据是最新的
cout << data; // 保证读到42
}

// acquire-release建立"同步关系":
// release之前的所有写,在对应acquire之后都可见

4. 典型应用 - 自实现spinlock:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Spinlock {
atomic<bool> locked(false);
public:
void lock() {
while (locked.exchange(true, memory_order_acquire)) {
// 自转等待
}
}

void unlock() {
locked.store(false, memory_order_release);
}
};

// acquire/release确保:
// lock之后的操作不会被提前到lock之前
// unlock之前的操作不会被延迟到unlock之后

5. 典型应用 - 双重检查锁定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Singleton {
static atomic<Singleton*> instance;
static mutex mtx;

public:
static Singleton* getInstance() {
Singleton* p = instance.load(memory_order_acquire);
if (p == nullptr) {
lock_guard<mutex> lock(mtx);
p = instance.load(memory_order_relaxed);
if (p == nullptr) {
p = new Singleton();
instance.store(p, memory_order_release);
}
}
return p;
}
};

6. 性能差异:

1
2
3
4
5
6
7
8
性能:relaxed > acquire/release > seq_cst
安全性:relaxed < acquire/release < seq_cst

选择策略:
- 计数器、标志:relaxed通常足够
- 生产者-消费者:acquire/release
- 不确定的情况:seq_cst(默认,安全但慢)
- 多个原子变量的复杂协调:seq_cst

7. 架构差异:

1
2
3
4
5
6
7
8
x86/x64:强内存序
- 几乎不需要显式fence
- relaxed和seq_cst性能差别小

ARM/RISC-V:弱内存序
- 需要显式fence来保证顺序
- 错误的内存序很容易导致bug
- acquire/release有明显性能收益

口头解答:
“内存序是多核并发编程中最底层也最复杂的概念。问题根源是现代CPU为了性能会重排指令,不同核看到的内存操作顺序可能不同。C++11通过atomic的内存序参数来控制这种行为。最宽松的relaxed只保证原子性,什么顺序都不保证,适合简单计数器。acquire-release是最常用的配对:release保证它之前的写都对外可见,acquire保证它之后读到的都是最新的,常用于生产者-消费者。seq_cst最严格,保证所有线程看到相同的操作顺序,是默认的,安全但慢。实际开发中,大多数场景用默认的seq_cst就好,只有对性能非常敏感时才去优化到acquire/release或relaxed。”


题目47: 线程安全的单例模式有哪些实现方式?

解答:

单例模式保证一个类只有一个实例。在多线程环境下实现它需要特别谨慎。

1. Meyers’ Singleton(最推荐):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11保证线程安全初始化
return instance;
}

private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};

// 使用
Singleton& s = Singleton::getInstance();

C++11标准(§6.7)明确保证:静态局部变量的初始化是线程安全的,只执行一次。如果多个线程同时到达,其他线程会阻塞直到初始化完成。这是最简单、最高效的方式。

2. 双重检查锁定(DCLP):

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
class Singleton {
static atomic<Singleton*> instance;
static mutex mtx;

Singleton() = default;
public:
static Singleton* getInstance() {
Singleton* p = instance.load(memory_order_acquire);
if (p == nullptr) {
lock_guard<mutex> lock(mtx);
p = instance.load(memory_order_relaxed);
if (p == nullptr) {
p = new Singleton();
instance.store(p, memory_order_release);
}
}
return p;
}
};

atomic<Singleton*> Singleton::instance{nullptr};
mutex Singleton::mtx;

// 工作原理:
// 1. 先不加锁检查(快路径)
// 2. 如果为null,加锁
// 3. 加锁后再检查(避免双重初始化)
// 4. 用acquire/release保证对象完全构造后才对外可见

3. std::call_once:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton {
static Singleton* instance;
static once_flag flag;

Singleton() = default;
public:
static Singleton* getInstance() {
call_once(flag, []() {
instance = new Singleton();
});
return instance;
}
};

Singleton* Singleton::instance = nullptr;
once_flag Singleton::flag;

4. 各方式对比:

1
2
3
4
5
6
方式              优点                    缺点
Meyers' 最简洁、线程安全 无法传参、栈分配
DCLP 延迟初始化、堆分配 复杂、容易写错
call_once 简洁、标准库支持 需要静态成员

推荐顺序:Meyers' > call_once > DCLP

5. 带参数的单例:

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
class Config {
map<string, string> data;

Config(const string& path) {
// 从文件加载配置
loadFromFile(path);
}

public:
static Config& getInstance(const string& path = "") {
static Config* instance = nullptr;
static once_flag flag;

call_once(flag, [&path]() {
instance = new Config(path);
});

// 注意:参数只在首次调用时有效
return *instance;
}

string get(const string& key) const {
auto it = data.find(key);
return it != data.end() ? it->second : "";
}
};

// 首次调用必须带参数
Config& config = Config::getInstance("/etc/app.conf");
// 后续调用参数被忽略
Config& config2 = Config::getInstance();

6. 销毁控制:

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
class Singleton {
static Singleton* instance;
static bool destroyed;

public:
static Singleton* getInstance() {
if (destroyed) {
instance = new Singleton();
destroyed = false;
}
if (!instance) {
instance = new Singleton();
}
return instance;
}

static void destroy() {
delete instance;
instance = nullptr;
destroyed = true;
}

private:
~Singleton() {}
};

口头解答:
“线程安全的单例有几种实现方式,最推荐的是Meyers’ Singleton——就是在静态方法里声明静态局部变量并返回引用。C++11标准保证了静态局部变量的初始化是线程安全的,只执行一次,所以这种方式既简洁又高效。如果需要堆分配或更多控制,可以用双重检查锁定,但要注意用atomic和正确的内存序,否则很容易出bug。call_once是另一个好选择,标准库提供的,语义很清楚。选择建议是优先Meyers’,它几乎没有缺点。不过单例本身是个争议性的模式,它引入了全局状态和隐藏依赖,不利于测试。现代软件工程更推荐依赖注入。”


题目48: 复制省略(Copy Elision)的规则和实际效果

解答:

复制省略是编译器优化技术,消除不必要的对象拷贝和移动。理解它有助于写出更高效的代码。

1. 核心概念:

编译器可以直接在目标位置构造对象,跳过中间的拷贝/移动步骤。这不只是优化,在C++17中对某些情况是语言标准强制要求的行为。

2. 何时触发:

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
class Widget {
public:
Widget() { cout << "构造\n"; }
Widget(const Widget&) { cout << "拷贝\n"; }
Widget(Widget&&) noexcept { cout << "移动\n"; }
};

// 情况1:返回临时对象(C++17强制消除)
Widget create() {
return Widget(); // 保证:只输出"构造"
}
Widget w1 = create();

// 情况2:直接初始化临时对象(C++17强制消除)
Widget w2 = Widget(); // 保证:只输出"构造"

// 情况3:NRVO - 返回命名对象(可选优化)
Widget createNamed() {
Widget w;
w.init();
return w; // 编译器可能优化,也可能移动
}
Widget w3 = createNamed();
// NRVO成功:只输出"构造"
// NRVO失败:输出"构造" + "移动"

3. 不能触发复制省略的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 返回参数
Widget func(Widget w) {
return w; // ✗ w不是局部变量,不NRVO
// 但C++11保证会尝试移动而不是拷贝
}

// 条件返回
Widget func(bool flag) {
Widget a, b;
return flag ? a : b; // ✗ 编译器无法确定返回哪个
}

// 返回成员变量
class Container {
Widget w;
public:
Widget get() { return w; } // ✗ 成员,不是局部变量
};

// 显式move阻止优化
Widget func() {
Widget w;
return std::move(w); // ✗ move阻止了NRVO!
}

4. 验证和实际效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Widget create() {
return Widget();
}

// 没有优化时的开销(理论):
// 1. 在create的栈帧上构造Widget
// 2. 移动到返回值
// 3. 销毁临时对象

// 优化后:
// 1. 直接在调用者的空间构造Widget
// 零额外开销!

// 可以用-fno-elide-constructors禁用来验证

5. C++17之前和之后的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
// C++14:优化是可选的
class NonMovable {
public:
NonMovable(const NonMovable&) = delete;
NonMovable(NonMovable&&) = delete;
};

NonMovable create() {
return NonMovable();
}
NonMovable obj = create();
// C++14:可能编译错误(没有拷贝/移动构造)
// C++17:保证编译通过(强制复制消除)

6. 实际建议:

1
2
3
4
5
✓ 直接返回,不加move:return w;
✗ 返回时加move:return std::move(w); // 阻止优化
✓ 返回临时对象:return Widget(args);
✗ 返回const对象:const Widget func(); // 阻止移动
✓ 单个return路径:尽量有一个return,有利于NRVO

口头解答:
“复制省略就是编译器直接在目标位置构造对象,避免不必要的拷贝或移动。C++17对纯右值强制要求这个优化,所以返回Widget()这种情况是保证零拷贝的。NRVO是返回命名局部变量的情况,仍然是可选优化,但大多数编译器都会做。最重要的实践原则是:不要画蛇添足地在return加std::move,这反而会阻止优化。让编译器自己去决定最优的策略。另外C++17之后,即使类不可拷贝不可移动,只要返回的是临时对象,也能编译通过。”


题目49: 解释C++中常见的设计模式及实现

解答:

设计模式是解决常见软件设计问题的可复用方案。以下介绍几个在C++中常用且面试常考的模式。

1. 策略模式(Strategy):

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
// 策略接口
class SortStrategy {
public:
virtual void sort(vector<int>& data) = 0;
virtual ~SortStrategy() {}
};

// 具体策略
class BubbleSort : public SortStrategy {
public:
void sort(vector<int>& data) override {
// 冒泡排序实现
}
};

class QuickSort : public SortStrategy {
public:
void sort(vector<int>& data) override {
// 快速排序实现
}
};

// 上下文
class Sorter {
unique_ptr<SortStrategy> strategy;
public:
void setStrategy(unique_ptr<SortStrategy> s) {
strategy = std::move(s);
}

void sort(vector<int>& data) {
strategy->sort(data);
}
};

// 现代C++风格:用function替代虚函数
class ModernSorter {
function<void(vector<int>&)> strategy;
public:
void setStrategy(function<void(vector<int>&)> s) {
strategy = std::move(s);
}

void sort(vector<int>& data) {
strategy(data);
}
};

2. 观察者模式(Observer):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Event {
vector<function<void(const string&)>> listeners;
public:
void subscribe(function<void(const string&)> listener) {
listeners.push_back(std::move(listener));
}

void notify(const string& data) {
for (const auto& listener : listeners) {
listener(data);
}
}
};

// 使用
Event onDataChanged;
onDataChanged.subscribe([](const string& data) {
cout << "Logger: " << data << "\n";
});
onDataChanged.subscribe([](const string& data) {
cout << "UI更新: " << data << "\n";
});

onDataChanged.notify("新数据到达");

3. 工厂模式(Factory):

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
class Shape {
public:
virtual void draw() = 0;
virtual ~Shape() {}
};

class Circle : public Shape {
public:
void draw() override { cout << "画圆\n"; }
};

class Rectangle : public Shape {
public:
void draw() override { cout << "画矩形\n"; }
};

// 工厂方法
class ShapeFactory {
public:
static unique_ptr<Shape> create(const string& type) {
if (type == "circle") return make_unique<Circle>();
if (type == "rectangle") return make_unique<Rectangle>();
throw invalid_argument("未知形状: " + type);
}
};

auto shape = ShapeFactory::create("circle");
shape->draw();

4. CRTP(Curiously Recurring Template Pattern):

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
// CRTP实现静态多态,避免虚函数开销
template<typename Derived>
class Base {
public:
void process() {
// 调用派生类的实现(编译期确定)
static_cast<Derived*>(this)->doProcess();
}
};

class ConcreteA : public Base<ConcreteA> {
public:
void doProcess() {
cout << "ConcreteA处理\n";
}
};

class ConcreteB : public Base<ConcreteB> {
public:
void doProcess() {
cout << "ConcreteB处理\n";
}
};

// CRTP的实际应用:实现Comparable
template<typename Derived>
class Comparable {
public:
bool operator<(const Derived& other) const {
return static_cast<const Derived*>(this)->compareTo(other) < 0;
}
bool operator>(const Derived& other) const {
return static_cast<const Derived*>(this)->compareTo(other) > 0;
}
bool operator==(const Derived& other) const {
return static_cast<const Derived*>(this)->compareTo(other) == 0;
}
};

class Age : public Comparable<Age> {
int value;
public:
Age(int v) : value(v) {}
int compareTo(const Age& other) const {
return value - other.value;
}
};

5. Builder模式:

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
class QueryBuilder {
string table;
vector<string> conditions;
vector<string> columns = {"*"};
int limit_val = -1;

public:
QueryBuilder& from(const string& t) {
table = t;
return *this;
}

QueryBuilder& select(initializer_list<string> cols) {
columns = cols;
return *this;
}

QueryBuilder& where(const string& condition) {
conditions.push_back(condition);
return *this;
}

QueryBuilder& limit(int n) {
limit_val = n;
return *this;
}

string build() {
string query = "SELECT ";
for (size_t i = 0; i < columns.size(); ++i) {
query += columns[i];
if (i + 1 < columns.size()) query += ", ";
}
query += " FROM " + table;
if (!conditions.empty()) {
query += " WHERE ";
for (size_t i = 0; i < conditions.size(); ++i) {
query += conditions[i];
if (i + 1 < conditions.size()) query += " AND ";
}
}
if (limit_val > 0) query += " LIMIT " + to_string(limit_val);
return query;
}
};

// 链式调用
string query = QueryBuilder()
.from("users")
.select({"name", "email"})
.where("age > 18")
.where("active = 1")
.limit(10)
.build();

口头解答:
“设计模式是经验总结的解决常见问题的方案。策略模式让算法可替换,现代C++中可以用std::function代替虚函数来实现。观察者模式用于事件通知,也可以用function的vector实现。工厂模式封装对象创建逻辑,配合unique_ptr很好用。CRTP是C++特有的技巧,用模板实现静态多态,避免虚函数的开销。Builder模式通过链式调用构建复杂对象,代码很清晰。现代C++中,很多传统的面向对象设计模式都可以用模板、lambda、智能指针等工具简化实现。”


题目50: C++程序的常见性能优化技巧

解答:

性能优化是一个系统工程,需要从多个层面考虑。核心原则是”先测量,后优化”。

1. 算法和数据结构层面:

1
2
3
4
5
6
7
8
9
10
11
12
13
// ✗ O(n²) 查找重复
bool hasDuplicate(const vector<int>& v) {
for (size_t i = 0; i < v.size(); ++i)
for (size_t j = i+1; j < v.size(); ++j)
if (v[i] == v[j]) return true;
return false;
}

// ✓ O(n) 用哈希集
bool hasDuplicate(const vector<int>& v) {
unordered_set<int> seen(v.begin(), v.end());
return seen.size() != v.size();
}

2. 容器选择和预分配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✗ 多次扩容
vector<int> v;
for (int i = 0; i < 100000; ++i)
v.push_back(i); // log(n)次扩容

// ✓ 预分配
vector<int> v;
v.reserve(100000); // 一次分配
for (int i = 0; i < 100000; ++i)
v.push_back(i);

// ✓ 直接构造
vector<int> v(100000);
iota(v.begin(), v.end(), 0);

3. 减少拷贝:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✗ 不必要拷贝
void process(vector<int> v) { } // 值传参,拷贝
for (auto item : container) { } // 值遍历,拷贝

// ✓ 引用或移动
void process(const vector<int>& v) { } // const引用
for (const auto& item : container) { } // 引用遍历

// ✓ 移动大对象
vector<int> getResult() {
vector<int> result;
// 计算结果
return result; // RVO或移动,不拷贝
}

4. 缓存友好的访问模式:

1
2
3
4
5
6
7
8
9
// ✗ 列优先访问(缓存不友好)
for (int i = 0; i < rows; ++i)
for (int j = 0; j < cols; ++j)
sum += matrix[j][i]; // 跳跃访问

// ✓ 行优先访问(缓存友好)
for (int i = 0; i < rows; ++i)
for (int j = 0; j < cols; ++j)
sum += matrix[i][j]; // 顺序访问

5. 字符串优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✗ 循环拼接(O(n²))
string result;
for (const auto& s : strings)
result += s; // 每次可能重新分配

// ✓ 预计算长度
string result;
size_t total = 0;
for (const auto& s : strings) total += s.size();
result.reserve(total);
for (const auto& s : strings) result += s;

// ✓ string_view避免拷贝
void process(string_view sv) { } // 零拷贝

6. 智能指针选择:

1
2
3
4
5
6
7
8
9
// 性能优先级(从快到慢):
// 1. 栈对象 - 最快
Widget w;

// 2. unique_ptr - 零开销
auto p = make_unique<Widget>();

// 3. shared_ptr - 有引用计数开销
auto sp = make_shared<Widget>(); // 尽量少用

7. 避免不必要的分支:

1
2
3
4
5
6
7
// ✗ 分支密集(分支预测失败多)
for (int i = 0; i < n; ++i)
if (data[i] > 0) sum += data[i];

// ✓ 无分支版本
for (int i = 0; i < n; ++i)
sum += data[i] * (data[i] > 0); // 利用bool转int

8. 编译器优化和工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 编译选项
// -O2:推荐的优化级别
// -O3:更激进(某些场景可能反效果)
// -march=native:针对当前CPU优化

// 性能测量
auto start = chrono::high_resolution_clock::now();
// ... 代码
auto end = chrono::high_resolution_clock::now();
auto ms = chrono::duration_cast<chrono::microseconds>(end - start).count();

// 工具推荐:
// perf (Linux):CPU级别分析
// Valgrind/Callgrind:详细的函数级分析
// Google Benchmark:微基准测试框架

9. 并行化:

1
2
3
4
5
6
7
8
9
10
11
12
13
// C++17并行算法
#include <execution>
#include <algorithm>

vector<int> data(1000000);

// 串行
sort(data.begin(), data.end());

// 并行
sort(execution::par_unseq, data.begin(), data.end());

// 或使用线程池处理独立任务

10. 核心原则总结:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
优化优先级(从高到低):
1. 算法复杂度优化(O(n²) → O(n log n))
2. 数据结构选择
3. 减少内存分配和拷贝
4. 缓存友好的访问模式
5. 减少分支
6. 编译器优化选项
7. 并行化
8. 低级别优化(SIMD等)

核心原则:
- 先测量,后优化(不要猜)
- 优化热点(20%的代码占80%的时间)
- 保持代码可读性
- 每次只改变一个变量,测量效果

口头解答:
“C++性能优化要从多层面考虑,但核心原则是先测量后优化,不要盲目猜测瓶颈。算法层面是最重要的,选对算法能带来数量级的提升。然后是减少不必要的拷贝,利用引用、移动、RVO等。容器操作要预分配空间,用emplace代替push。内存访问模式也很重要,按行遍历比按列遍历缓存友好得多。字符串操作要预reserve,考虑用string_view。智能指针优先用unique_ptr。现代编译器很智能,很多优化它们会自动做,我们要做的是写清晰的代码,让编译器能更好地优化。还有就是C++17的并行算法很方便,对数据量大的场景可以直接用并行策略。”


50题全部完成!

总结与学习建议:

本文档涵盖了C++面试中最常见的50个问题,分为五大模块:

模块 题目范围 核心知识点
基础与OOP 1-10 语法基础、三大特性、虚函数、拷贝语义
内存与STL 11-20 内存管理、RAII、容器、算法、迭代器
智能指针与多线程 21-30 unique/shared/weak_ptr、线程同步、原子操作
模板与高级特性 31-40 模板特化、SFINAE、移动语义、lambda、新标准特性
高级主题 41-50 类型擦除、内存序、设计模式、性能优化

学习路径建议:

  1. 先掌握基础(1-10),确保概念清晰
  2. 深入内存管理(11-15),这是C++的核心能力
  3. 熟练智能指针(21-25),现代C++必备
  4. 理解多线程(26-30),工作中高频需求
  5. 掌握模板(31-35),提升到高级开发者水平
  6. 了解高级特性(36-50),应对高难度面试

面试技巧:

  1. 先简要回答核心要点,再逐步展开
  2. 结合实际代码示例说明
  3. 提及适用场景和注意事项
  4. 展示对现代C++新标准的了解
  5. 对于不确定的问题,诚实表达并展示学习意愿

祝你面试顺利!🎯