C++ 八股文(高级 C++ 特性)
1. C++ 中的右值引用和移动语义是什么?
1. 右值引用概念: 使用&&声明的引用类型,可以绑定到临时对象(右值),左值是有名字的对象右值是临时对象,右值引用延长临时对象的生命周期,是实现移动语义的基础。
2. 移动语义: 转移资源所有权而不是拷贝,避免深拷贝提高性能,移动后的对象处于有效但未定义状态,通过移动构造函数和移动赋值运算符实现,时间复杂度从O(n)降到O(1)。
3. 值类别: 左值lvalue(有名字可取地址),纯右值prvalue(临时对象字面量),将亡值xvalue(std::move的结果),左值引用绑定左值右值引用绑定右值,const左值引用可以绑定右值。
4. 使用场景: 返回大对象避免拷贝,容器插入临时对象,unique_ptr转移所有权,完美转发保持值类别,swap操作使用移动提高效率。
class Buffer {
char* data;
size_t size;
public:
// 移动构造
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // 转移所有权
}
// 移动赋值
Buffer& operator=(Buffer&& other) noexcept {
if(this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};
Buffer b1(1024);
Buffer b2 = std::move(b1); // 移动,不拷贝
2. C++ 中的 std::move 和 std::forward 有什么区别?
1. std::move作用: 无条件将左值转换为右值引用,不移动任何东西只是类型转换,告诉编译器可以移动这个对象,实际移动由移动构造/赋值完成,使用后源对象不应再使用。
2. std::forward作用: 完美转发保持参数的值类别,左值转发为左值右值转发为右值,用于模板函数转发参数,避免不必要的拷贝,实现泛型代码的高效转发。
3. 使用场景: move用于明确表示转移所有权,forward用于模板函数的参数转发,move是无条件转换forward是有条件转换,move用于实现移动语义forward用于实现完美转发。
4. 实现原理: move等价于static_cast<T&&>,forward根据模板参数决定转换类型,都是编译期操作无运行时开销,正确使用可以显著提高性能。
// std::move:无条件转换为右值
std::string str = "hello";
std::string str2 = std::move(str); // str被移动
// std::forward:完美转发
template<typename T>
void wrapper(T&& arg) {
func(std::forward<T>(arg)); // 保持arg的值类别
}
wrapper(10); // 右值,forward转发为右值
int x = 10;
wrapper(x); // 左值,forward转发为左值
3. C++11 中的 Lambda 表达式是什么?
1. 基本语法:捕获列表 mutable noexcept -> 返回类型 { 函数体 },捕获列表指定外部变量的访问方式,参数列表和普通函数相同,返回类型可以省略由编译器推导。
2. 捕获方式: []不捕获,[=]按值捕获所有变量,[&]按引用捕获所有变量,[this]捕获this指针,[x, &y]混合捕获,[=, &x]默认按值x按引用,C++14支持初始化捕获。
3. 特性: mutable允许修改按值捕获的变量,noexcept声明不抛异常,泛型lambda使用auto参数(C++14),constexpr lambda编译期计算(C++17),模板lambda(C++20)。
4. 使用场景: STL算法的谓词函数,回调函数和事件处理,线程函数,延迟计算和惰性求值,替代函数对象简化代码。
// 基本用法
auto add = [](int a, int b) { return a + b; };
int sum = add(3, 4);
// 捕获外部变量
int x = 10;
auto f1 = [x]() { return x * 2; }; // 按值捕获
auto f2 = [&x]() { x++; }; // 按引用捕获
auto f3 = [=, &x]() { return x + y; }; // 混合捕获
// 初始化捕获(C++14)
auto f4 = [ptr = std::make_unique<int>(42)]() { return *ptr; };
// 泛型lambda(C++14)
auto print = [](auto x) { std::cout << x; };
4. 如何使用 C++ 中的 constexpr 定义常量表达式?
1. 基本概念: constexpr声明的变量或函数可以在编译期求值,比const更强要求编译期可计算,constexpr变量必须用常量表达式初始化,constexpr函数可以在编译期或运行期执行。
2. constexpr函数: 函数体必须是单一return语句(C++11)或满足一定限制(C++14放宽),参数和返回值必须是字面类型,可以递归调用,用常量参数调用时编译期求值。
3. constexpr类: 构造函数可以是constexpr,成员函数可以是constexpr,C++14支持constexpr成员变量修改,可以创建编译期对象,用于模板元编程和编译期计算。
4. 使用场景: 数组大小和模板参数,编译期计算避免运行时开销,类型萃取和元编程,constexpr if实现编译期分支(C++17),性能优化和代码生成。
// constexpr变量
constexpr int size = 100;
int arr[size]; // 编译期常量
// constexpr函数
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n-1);
}
constexpr int val = factorial(5); // 编译期计算
// constexpr类
class Point {
int x, y;
public:
constexpr Point(int x_, int y_) : x(x_), y(y_) {}
constexpr int getX() const { return x; }
};
constexpr Point p(3, 4);
constexpr int x = p.getX(); // 编译期求值
// constexpr if(C++17)
template<typename T>
auto getValue(T t) {
if constexpr (std::is_pointer_v<T>)
return *t;
else
return t;
}
5. C++ 中如何实现自定义的内存管理器?
1. 基本方法: 重载operator new和operator delete全局或类级别,实现allocate和deallocate函数,使用内存池预分配内存,实现STL兼容的allocator接口。
2. 内存池实现: 预分配大块内存减少系统调用,维护空闲链表快速分配,固定大小块或多级大小,使用位图或链表管理空闲块,注意线程安全和内存对齐。
3. STL allocator: 实现allocate、deallocate、construct、destroy,定义value_type等类型别名,支持rebind转换为其他类型的allocator,C++17简化了allocator接口。
4. 使用场景: 频繁分配释放小对象,减少内存碎片,实时系统避免不确定延迟,嵌入式系统精确控制内存,性能敏感的应用优化分配速度。
// 简单内存池
class MemoryPool {
union Block { Block* next; char data[32]; };
Block* freeList = nullptr;
public:
void* allocate() {
if(!freeList) expandPool();
Block* block = freeList;
freeList = freeList->next;
return block;
}
void deallocate(void* ptr) {
Block* block = static_cast<Block*>(ptr);
block->next = freeList;
freeList = block;
}
};
// STL allocator
template<typename T>
class PoolAllocator {
public:
using value_type = T;
T* allocate(size_t n) {
return static_cast<T*>(pool.allocate());
}
void deallocate(T* p, size_t n) {
pool.deallocate(p);
}
};
std::vector<int, PoolAllocator<int>> vec;
6. C++ 中的模板元编程(Template Metaprogramming)是什么?
1. 基本概念: 使用模板在编译期进行计算和代码生成,模板实例化时执行计算,结果是编译期常量或类型,图灵完备可以实现任意计算,性能优于运行期计算但编译时间长。
2. 实现技术: 模板递归实现循环,模板特化实现条件分支,类型作为值传递,constexpr函数简化元编程(C++11),变参模板处理任意数量参数(C++11)。
3. 应用场景: 编译期计算常量(阶乘、斐波那契),类型萃取和类型操作,表达式模板优化数值计算,策略模式的编译期选择,静态多态和零开销抽象。
4. 现代替代: constexpr函数更易读易写,constexpr if简化条件分支(C++17),concepts约束模板参数(C++20),fold expression简化变参处理(C++17)。
// 编译期计算阶乘
template<int N>
struct Factorial {
static constexpr int value = N * Factorial<N-1>::value;
};
template<>
struct Factorial<0> {
static constexpr int value = 1;
};
constexpr int f5 = Factorial<5>::value; // 120
// 类型选择
template<bool Cond, typename T, typename F>
struct If { using type = T; };
template<typename T, typename F>
struct If<false, T, F> { using type = F; };
// 现代方式:constexpr函数
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n-1);
}
7. 如何实现 C++ 中的类型萃取(Type Traits)?
1. 基本概念: 在编译期查询和操作类型信息,标准库提供std::is_integral、std::is_pointer等,使用模板特化实现,返回编译期常量或类型,用于泛型编程和SFINAE。
2. 实现方法: 主模板定义默认行为,特化模板处理特定类型,使用std::true_type和std::false_type表示布尔值,使用type成员表示类型,C++17的std::void_t简化实现。
3. 常用traits: is_same判断类型相同,is_base_of判断继承关系,remove_const移除const,add_pointer添加指针,enable_if条件启用,decay类型退化。
4. 使用场景: SFINAE选择重载函数,constexpr if编译期分支,概念约束模板参数,类型转换和适配,实现泛型算法的类型检查。
// 判断是否是指针
template<typename T>
struct is_pointer : std::false_type {};
template<typename T>
struct is_pointer<T*> : std::true_type {};
// 移除const
template<typename T>
struct remove_const { using type = T; };
template<typename T>
struct remove_const<const T> { using type = T; };
// 使用示例
template<typename T>
void process(T value) {
if constexpr (is_pointer<T>::value) {
// 处理指针
} else {
// 处理非指针
}
}
// SFINAE
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
add(T a, T b) { return a + b; }
8. C++ 中的类型推导(Type Deduction)是什么?
8. C++ 中的类型推导(Type Deduction)是什么?
1. auto推导: 根据初始化表达式推导变量类型忽略顶层const和引用,auto&保留引用const auto保留const,decltype(auto)保留所有类型信息(C++14),简化复杂类型声明提高代码可读性。
2. 模板参数推导: 根据函数参数推导模板参数类型,T推导为去除引用和const的类型,T&推导为引用类型,T&&是转发引用根据参数推导左值或右值引用。
3. decltype推导: 推导表达式的类型,decltype(变量)得到变量的声明类型,decltype(表达式)得到表达式的值类别类型,用于返回类型推导和完美转发。
4. 推导规则: 数组退化为指针函数退化为函数指针,引用折叠规则T& &变为T&,转发引用的特殊规则,C++17的类模板参数推导CTAD如std::pair p(1, 2.0)。
auto x = 42; // int auto& y = x; // int& decltype(auto) w = (x); // int&保留引用 template<typename T> void f(T&& param); // 转发引用 f(42); // T推导为int f(x); // T推导为int& std::pair p(1, 2.0); // CTAD推导为pair<int, double>
9. C++11 引入的 nullptr 有什么用法?
1. 基本概念: nullptr是空指针字面量类型是std::nullptr_t,可以隐式转换为任何指针类型不能转换为整数类型,解决NULL的二义性问题NULL通常定义为0导致重载歧义。
2. NULL的问题: NULL可以隐式转换为整数导致重载歧义,在模板中类型推导错误,nullptr明确表示空指针避免混淆,函数重载时nullptr匹配指针类型而NULL可能匹配整数。
3. 使用场景: 初始化指针为空,函数参数传递空指针,返回空指针,与nullptr比较判断指针是否为空,模板编程中表示空指针。
4. 最佳实践: 总是使用nullptr代替NULL或0,指针比较使用nullptr而不是0,函数重载时nullptr匹配指针类型,避免使用NULL宏提高代码清晰度。
void f(int);
void f(char*);
f(NULL); // 歧义可能调用f(int)
f(nullptr); // 明确调用f(char*)
int* p = nullptr; // 推荐
if(p == nullptr) {} // 推荐
10. C++ 中的 auto 关键字是什么?
1. 基本用法: 自动推导变量类型根据初始化表达式,简化复杂类型声明必须初始化才能推导,推导规则与模板参数推导类似忽略顶层const和引用。
2. 推导规则: auto推导为值类型,auto&推导为引用,const auto推导为const,auto&&是转发引用,auto*显式推导为指针,decltype(auto)保留所有类型信息。
3. 使用场景: 迭代器类型简化,lambda表达式类型,复杂模板类型,范围for循环,函数返回类型推导(C++14),结构化绑定(C++17)。
4. 注意事项: 可能降低代码可读性需要权衡,隐藏类型转换可能导致性能问题,初始化列表需要显式类型,auto不能用于函数参数(C++20前)。
auto x = 42; // int
auto vec = std::vector<int>{}; // vector<int>
auto& c = x; // int&引用
for(auto& [key, val] : map) {} // C++17结构化绑定
auto add(int a, int b) { return a + b; } // C++14返回类型推导
11. C++ 中的 noexcept 关键字如何使用?
1. 基本概念: 声明函数不抛出异常noexcept是类型的一部分,违反noexcept会调用std::terminate,编译器可以优化noexcept函数,移动构造和移动赋值应该声明noexcept。
2. 条件noexcept: noexcept(表达式)根据表达式结果决定,noexcept(noexcept(expr))检查表达式是否noexcept,用于模板函数根据参数决定异常规范。
3. 性能优化: 容器操作优先使用noexcept移动没有noexcept会使用拷贝降低性能,析构函数默认noexcept,swap函数应该noexcept提高容器性能。
4. 使用建议: 移动操作总是声明noexcept,析构函数不要抛异常,swap和移动赋值声明noexcept,不确定是否抛异常时不要声明,使用noexcept(false)显式允许异常。
void f() noexcept { /* 不抛异常 */ }
template<typename T>
void swap(T& a, T& b) noexcept(noexcept(T(std::move(a)))) {
// 条件noexcept
}
class Widget {
Widget(Widget&&) noexcept { /* 移动构造应该noexcept */ }
};
12. C++ 中的 typeid 是如何工作的?
1. 基本概念: typeid运算符返回std::type_info对象表示类型信息用于运行时类型识别RTTI,需要启用RTTI编译选项,多态类型返回动态类型非多态返回静态类型。
2. 使用方式: typeid(表达式)或typeid(类型)返回const type_info&,可以用==比较类型,name()返回类型名称实现定义格式不可移植,hash_code()返回哈希值用于容器。
3. 多态行为: 对于多态类型有虚函数返回对象的实际类型,对于非多态类型返回静态类型,指针类型返回指针本身的类型不是指向的类型需要解引用才能获取动态类型。
4. 注意事项: 有性能开销应避免频繁使用,type_info不可拷贝只能引用,name()返回的字符串格式不可移植,现代C++推荐使用模板和类型萃取代替RTTI。
int x = 42;
std::cout << typeid(x).name(); // 输出类型名称
if(typeid(x) == typeid(int)) {} // 比较类型
class Base { virtual ~Base() {} };
Base* ptr = new Derived;
typeid(*ptr); // Derived动态类型
typeid(ptr); // Base*指针类型
13. 如何使用 C++ 实现延迟计算(Lazy Evaluation)?
1. 基本概念: 延迟计算直到需要结果时才执行避免不必要的计算提高性能,使用闭包或函数对象封装计算,结果可以缓存避免重复计算memoization。
2. 实现方法: lambda表达式封装计算,std::function存储可调用对象,std::optional表示可能未计算的值,使用std::once_flag确保只计算一次,表达式模板延迟求值。
3. 应用场景: 短路求值避免无效计算,无限序列和生成器,表达式优化和融合,条件计算只在需要时执行,数据库查询的延迟加载。
4. 实现技巧: 使用std::invoke_result推导返回类型,使用std::shared_ptr共享计算结果,C++20的ranges库提供延迟视图如views::transform,结合std::future实现异步延迟计算。
. auto推导: 根据初始化表达式推导变量类型,忽略顶层const和引用,auto&保留引用,const auto保留const,decltype(auto)保留所有类型信息(C++14)。
2. 模板参数推导: 根据函数参数推导模板参数类型,T推导为去除引用和const的类型,T&推导为引用类型,T&&是转发引用根据参数推导左值或右值引用。
3. decltype推导: 推导表达式的类型,decltype(变量)得到变量的声明类型,decltype(表达式)得到表达式的值类别类型,用于返回类型推导和完美转发。
4. 推导规则: 数组退化为指针,函数退化为函数指针,引用折叠规则(T& &变为T&),转发引用的特殊规则,C++17的类模板参数推导(CTAD)。
// auto推导
auto x = 42; // int
auto& y = x; // int&
const auto z = x; // const int
auto ptr = &x; // int*
decltype(auto) w = (x); // int&(保留引用)
// 模板推导
template<typename T>
void f(T param); // T推导为int
f(42);
template<typename T>
void g(T& param); // T推导为int,param是int&
int x = 42;
g(x);
// 转发引用
template<typename T>
void h(T&& param); // 万能引用
h(42); // T推导为int,param是int&&
h(x); // T推导为int&,param是int&
// CTAD(C++17)
std::pair p(1, 2.0); // std::pair<int, double>
std::vector v{1, 2, 3}; // std::vector<int>
9. C++11 引入的 nullptr 有什么用法?
1. 基本概念: nullptr是空指针字面量,类型是std::nullptr_t,可以隐式转换为任何指针类型,不能转换为整数类型,解决NULL的二义性问题。
2. NULL的问题: NULL通常定义为0或((void*)0),可以隐式转换为整数导致重载歧义,在模板中类型推导错误,nullptr明确表示空指针避免混淆。
3. 使用场景: 初始化指针为空,函数参数传递空指针,返回空指针,与nullptr比较判断指针是否为空,模板编程中表示空指针。
4. 最佳实践: 总是使用nullptr代替NULL或0,指针比较使用nullptr而不是0,函数重载时nullptr匹配指针类型,避免使用NULL宏。
// nullptr vs NULL
void f(int);
void f(char*);
f(NULL); // 歧义:可能调用f(int)
f(nullptr); // 明确:调用f(char*)
// 指针初始化
int* p1 = nullptr; // 推荐
int* p2 = NULL; // 不推荐
int* p3 = 0; // 不推荐
// 指针判断
if(p1 == nullptr) {} // 推荐
if(p1 == NULL) {} // 不推荐
if(!p1) {} // 可以
// 模板中使用
template<typename T>
void process(T* ptr) {
if(ptr == nullptr) return;
}
process<int>(nullptr); // 正确推导
10. C++ 中的 auto 关键字是什么?
1. 基本用法: 自动推导变量类型根据初始化表达式,简化复杂类型声明,必须初始化才能推导,推导规则与模板参数推导类似,忽略顶层const和引用。
2. 推导规则: auto推导为值类型,auto&推导为引用,const auto推导为const,auto&&是转发引用,auto*显式推导为指针,decltype(auto)保留所有类型信息。
3. 使用场景: 迭代器类型简化,lambda表达式类型,复杂模板类型,范围for循环,函数返回类型推导(C++14),结构化绑定(C++17)。
4. 注意事项: 可能降低代码可读性需要权衡,隐藏类型转换可能导致性能问题,初始化列表需要显式类型,auto不能用于函数参数(C++20前)。
// 基本用法
auto x = 42; // int
auto y = 3.14; // double
auto z = "hello"; // const char*
auto vec = std::vector<int>{}; // std::vector<int>
// 引用和const
int a = 10;
auto b = a; // int(拷贝)
auto& c = a; // int&(引用)
const auto d = a; // const int
// 迭代器简化
std::map<std::string, int> m;
for(auto it = m.begin(); it != m.end(); ++it) {} // 简化
for(auto& [key, val] : m) {} // C++17结构化绑定
// 返回类型推导(C++14)
auto add(int a, int b) { return a + b; }
// lambda
auto f = [](int x) { return x * 2; };
11. C++ 中的 noexcept 关键字如何使用?
1. 基本概念: 声明函数不抛出异常,noexcept是类型的一部分,违反noexcept会调用std::terminate,编译器可以优化noexcept函数,移动构造和移动赋值应该声明noexcept。
2. 条件noexcept: noexcept(表达式)根据表达式结果决定,noexcept(noexcept(expr))检查表达式是否noexcept,用于模板函数根据参数决定异常规范。
3. 性能优化: 容器操作优先使用noexcept移动,没有noexcept会使用拷贝降低性能,析构函数默认noexcept,swap函数应该noexcept。
4. 使用建议: 移动操作总是声明noexcept,析构函数不要抛异常,swap和移动赋值声明noexcept,不确定是否抛异常时不要声明,使用noexcept(false)显式允许异常。
// 基本用法
void f() noexcept { // 不抛异常
// 如果抛异常会调用std::terminate
}
// 条件noexcept
template<typename T>
void swap(T& a, T& b) noexcept(noexcept(T(std::move(a)))) {
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
// 移动构造
class Widget {
public:
Widget(Widget&& other) noexcept { // 应该noexcept
// 移动资源
}
};
// 检查是否noexcept
static_assert(noexcept(Widget(std::declval<Widget>())));
// 容器优化
std::vector<Widget> vec;
vec.push_back(Widget{}); // 使用noexcept移动而不是拷贝
12. C++ 中的 typeid 是如何工作的?
1. 基本概念: typeid运算符返回std::type_info对象表示类型信息,用于运行时类型识别(RTTI),需要启用RTTI编译选项,多态类型返回动态类型非多态返回静态类型。
2. 使用方式: typeid(表达式)或typeid(类型),返回const type_info&,可以用==比较类型,name()返回类型名称(实现定义),hash_code()返回哈希值。
3. 多态行为: 对于多态类型(有虚函数)返回对象的实际类型,对于非多态类型返回静态类型,指针类型返回指针本身的类型不是指向的类型,需要解引用才能获取动态类型。
4. 注意事项: 有性能开销应避免频繁使用,type_info不可拷贝只能引用,name()返回的字符串格式不可移植,现代C++推荐使用模板和类型萃取代替RTTI。
#include <typeinfo>
// 基本用法
int x = 42;
std::cout << typeid(x).name(); // 输出类型名称(实现定义)
// 比较类型
if(typeid(x) == typeid(int)) {}
// 多态类型
class Base { virtual ~Base() {} };
class Derived : public Base {};
Base* ptr = new Derived;
std::cout << typeid(*ptr).name(); // Derived(动态类型)
std::cout << typeid(ptr).name(); // Base*(指针类型)
// 异常处理
try {
Base& ref = dynamic_cast<Derived&>(*ptr);
} catch(std::bad_cast& e) {
std::cout << typeid(e).name();
}
13. 如何使用 C++ 实现延迟计算(Lazy Evaluation)?
1. 基本概念: 延迟计算直到需要结果时才执行,避免不必要的计算提高性能,使用闭包或函数对象封装计算,结果可以缓存避免重复计算。
2. 实现方法: lambda表达式封装计算,std::function存储可调用对象,std::optional表示可能未计算的值,memoization缓存计算结果,表达式模板延迟求值。
3. 应用场景: 短路求值避免无效计算,无限序列和生成器,表达式优化和融合,条件计算只在需要时执行,数据库查询的延迟加载。
4. 实现技巧: 使用std::invoke_result推导返回类型,使用std::once_flag确保只计算一次,使用std::shared_ptr共享计算结果,C++20的ranges库提供延迟视图。
本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。