C++面试问题集锦
以下是针对C++工程师岗位的面试问题集锦,涵盖基础知识、编程能力、项目经验等方面:
-
基础语法与语言特性:
vector
和map
的内部实现原理及常用操作?- C++ 中的引用和指针有什么区别?
- 什么是构造函数、析构函数?拷贝构造函数和赋值运算符的区别?
- 虚函数如何实现多态?虚函数表的作用是什么?
-
面向对象与设计模式:
- 解释封装、继承、多态的概念及其在 C++ 中的实现。
- 常见的设计模式有哪些?是否使用过如单例模式、工厂模式等?
- 如何实现一个不能被继承的类?
-
内存管理与性能优化:
- C++ 中的内存分配方式有哪些?堆和栈的区别是什么?
- 智能指针(
shared_ptr
,unique_ptr
,weak_ptr
)的工作原理? - 内存泄漏如何检测和避免?
-
STL 与模板编程:
- STL 容器的线程安全性如何?
- 迭代器失效的情况有哪些?如何避免?
- 模板的特化与偏特化有什么区别?
-
三维几何与数值计算(针对 BIM/CAD 开发):
- 熟悉哪些常见的三维几何变换算法?
- 数值计算中的浮点误差如何处理?
- OpenGL 的基本渲染流程是怎样的?
-
软件工程与开发实践:
- 是否了解瀑布模型、敏捷开发等软件开发流程?
- 如何进行代码调试和性能优化?
- Git 常用命令有哪些?如何解决冲突?
-
项目经验与问题解决:
- 请介绍自己做过的最好的程序或项目。
- 遇到过哪些技术难点?是如何解决的?
- 对加班和高强度工作压力的看法?
-
并发编程:
- C++11 引入了哪些多线程相关的新特性?
- 如何使用
std::mutex
和std::lock_guard
实现线程同步? - 多线程中死锁的原因及解决办法?
6.1. 数据类型
char 8位bit 1个字节, unsigned char 几个字节?
6.2. C中static有什么作用
正确答案:
(1)隐藏。当我们同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性,故使用static在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。
(2)static的第二个作用是保持变量内容的持久。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量。
(3)static的第三个作用是默认初始化为0.
6.3. virtual函数
1、如何声明一个虚函数?
在类里,加virtual关键字。
6.4. 友元函数
友元函数可以访问当前类中的所有成员,包括public、protected、private 属性的。
- 将非成员函数声明为友元函数。
- 将其他类的成员函数声明为友元函数
6.5. inline 与宏定义区别
6.5.1. inline关键字(内联函数)
在C++中,inline是一个用于函数定义的修饰符,格式是直接在函数返回类型的最前面加上这个inline,
产生效果是建议编译器将整个函数体代码段插入到每个调用点然后展开,从而消除函数调用的开销(代码转移到函数对应的内存地址执行,会记忆转移前的地址,反复调用就会转移多次),
inline直接将函数嵌入到函数开始调用的那块。
inline
与宏定义在 C++ 中的主要区别如下:
6.5.2. 类型检查
inline
:会进行类型检查,因为它是函数调用机制的一部分,编译器会对参数进行类型匹配。- 宏定义(
#define
):不会进行类型检查,它只是简单的文本替换,在预处理阶段完成。
6.5.3. 调试支持
inline
:支持调试,因为它是真正的函数,可以设置断点和单步执行。- 宏定义:不支持调试,因为它只是代码的展开,无法直接调试宏本身。
6.5.4. 作用域
inline
:受 C++ 作用域限制,遵循类、命名空间等作用域规则。- 宏定义:不受作用域限制,一旦定义,可以在任何地方使用(除非被
#undef
取消)。
6.5.5. 函数语义
inline
:具有函数语义,如参数传递、返回值、局部变量等。- 宏定义:没有函数语义,只是简单的字符串替换,可能导致意外行为(例如副作用)。
6.5.6. 错误提示
inline
:编译错误提示更清晰,可以直接指出函数中的错误。- 宏定义:错误提示难以定位,通常只能提示宏展开后的代码错误。
6.5.7. 性能影响
inline
:无额外开销,编译器会根据需要决定是否内联优化。- 宏定义:可能带来重复计算或代码膨胀,特别是在多次使用时。
6.5.8. 示例对比
6.5.8.1. 使用 inline
:
inline int add(int a, int b) {return a + b;
}
- 编译器会进行类型检查,确保传入的是
int
类型。 - 支持调试,可设置断点。
6.5.8.2. 使用宏定义:
#define ADD(a, b) ((a) + (b))
- 预处理器直接替换,不做类型检查。
- 如果传入表达式
(x++) + (y++)
,可能会导致副作用。
6.5.9. 总结对比表格:
对比项 | inline 函数 | 宏定义(#define ) |
---|---|---|
类型检查 | ✅ 会进行类型检查 | ❌ 不会进行类型检查 |
调试支持 | ✅ 支持 | ❌ 不支持 |
作用域 | ✅ 受作用域限制 | ❌ 不受作用域限制 |
函数语义 | ✅ 具有函数语义 | ❌ 没有函数语义 |
错误提示 | ✅ 更清晰 | ❌ 难以定位错误来源 |
性能影响 | ✅ 无额外开销 | ❌ 可能导致重复计算或代码膨胀 |
6.5.10. 推荐使用场景:
- 优先使用
inline
:适用于需要性能优化且逻辑较复杂的代码块。 - 宏定义谨慎使用:适用于简单的常量定义(如
#define PI 3.14
)或条件编译控制。
6.6. delete与 delete []区别
delete [] arry 和 delete arry 一样吗?不一样请说明;
delete只会调用一次析构函数,而delete[]会调用每一个成员的析构函数。
避免混用,否则可能导致内存泄漏或程序崩溃。
在 C++ 中,delete
和 delete[]
的主要区别在于它们用于释放不同类型的内存分配:
6.6.1. 用途
delete
:用于释放通过new
分配的单个对象。delete[]
:用于释放通过new[]
分配的数组对象。
6.6.2. 析构函数调用
delete
:只调用一次析构函数(针对单个对象)。delete[]
:会为数组中的每个元素调用析构函数。
如果使用
delete
去释放一个由new[]
分配的数组,只会调用第一个元素的析构函数,而不会调用其余元素的析构函数,这可能导致资源泄漏或未定义行为。
6.6.3. 内存释放方式
delete
:释放单个对象所占用的内存。delete[]
:释放整个数组占用的连续内存块。
6.6.4. 使用匹配原则
new
必须与delete
配对使用。new[]
必须与delete[]
配对使用。
如果不遵循这个规则,会导致未定义行为。
6.6.5. ✅ 正确示例
int* pSingle = new int;
delete pSingle; // 正确:释放单个对象int* pArray = new int[10];
delete[] pArray; // 正确:释放数组
6.6.6. ❌ 错误示例
int* pArray = new int[10];
delete pArray; // ❌ 错误:只调用第一个元素的析构函数,内存未完全释放int* pSingle = new int;
delete[] pSingle; // ❌ 错误:行为未定义,可能崩溃或数据损坏
6.6.7. 🔍 更深入解释(来自《More Effective C++》)
当使用 delete[]
时:
- 它会为数组中的每一个元素调用析构函数。
- 然后调用
operator delete[]
来释放内存。
而 delete
只会为单个对象调用析构函数,并调用 operator delete
。
6.6.8. 📌 总结对比表格
特性 | delete | delete[] |
---|---|---|
匹配的分配方式 | new | new[] |
析构函数调用次数 | 1 次 | 数组中每个元素都调用 |
内存释放方式 | 单个对象内存 | 整个数组的连续内存 |
使用错误后果 | 可能导致资源泄漏或未定义行为 | 未定义行为(程序可能崩溃或出错) |
6.6.9. ✅ 推荐实践
始终确保:
new
对应delete
new[]
对应delete[]
避免混用,否则可能导致内存泄漏或程序崩溃。
在More Effective C++中有更为详细的解释:“当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用operator delete来释放内存。”delete与new配套,delete []与new []配套
6.7. 参数传递有几种方式;
传值,传指针或者引用
6.7.1. C++中引用和指针的区别?
- 引用必须被初始化,指针不必。
- 引用初始化以后不能被改变,指针可以改变所指的对象。
- 不存在指向空值的引用,但是存在指向空值的指针。
6.8. 结构与联合有和区别?
(1). 结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。
(2). 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。
6.9. 基类的析构函数不是虚函数,会带来什么问题?
派生类的析构函数用不上,会造成资源的泄漏。
6.10. Thread
#include <pthread.h>
以下是关于 C++ 多线程方面常见的面试题及参考答案,适用于 C++ 工程师岗位的准备:
6.10.1. C++11 引入了哪些多线程相关的新特性?
std::thread
:用于创建和管理线程。std::mutex
和std::lock_guard
/std::unique_lock
:用于线程同步,防止数据竞争。std::atomic
:提供原子操作,避免对共享变量的操作产生竞争条件。std::condition_variable
:实现线程间的通信,如等待特定条件成立后再继续执行。std::future
和std::promise
:支持异步编程,获取异步任务的结果。
6.10.2. 如何创建一个线程并传递参数?
#include <iostream>
#include <thread>void threadFunc(int x) {std::cout << "Thread function received: " << x << std::endl;
}int main() {std::thread t(threadFunc, 42); // 传递参数 42t.join(); // 等待线程结束return 0;
}
注意:传递参数时,如果需要引用传递,必须使用
std::ref()
或std::cref()
。
6.10.3. std::thread
的 join()
和 detach()
有什么区别?
join()
:主线程会阻塞等待子线程完成后再继续执行。确保线程生命周期可控。detach()
:主线程与子线程分离,子线程独立运行,不再受主线程控制。此时需注意资源管理和生命周期问题。
6.10.4. 什么是线程安全?如何保证线程安全?
- 线程安全:多个线程并发访问某个函数或对象时,不会导致数据损坏或逻辑错误。
- 保证方式:
- 使用互斥锁(
std::mutex
)保护共享资源。 - 使用原子操作(
std::atomic
)处理简单类型的数据。 - 避免共享状态,采用无状态设计或消息传递机制。
- 使用线程局部存储(
thread_local
)避免数据竞争。
- 使用互斥锁(
6.10.5. 解释 std::mutex
、std::lock_guard
和 std::unique_lock
的作用及区别。
std::mutex
:最基本的互斥量,提供lock()
和unlock()
方法。std::lock_guard
:RAII 风格的锁管理工具,在构造时加锁,析构时自动解锁,适用于简单的临界区保护。std::unique_lock
:更灵活的锁管理器,支持延迟加锁、尝试加锁、超时加锁等高级功能。
6.10.6. 如何避免死锁?
-
死锁四大必要条件:
- 互斥
- 持有并等待
- 不可抢占
- 循环等待
-
解决方案:
- 统一加锁顺序(按固定顺序加锁)
- 使用
std::lock()
同时锁定多个互斥量 - 设置超时机制(如
std::unique_lock::try_lock_for()
) - 减少锁的粒度,尽量使用细粒度锁
6.10.7. 什么是条件变量(std::condition_variable
)?如何使用?
- 作用:用于线程间通信,一个线程等待某个条件成立,另一个线程通知其继续执行。
- 典型用法:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>std::condition_variable cv;
std::mutex mtx;
bool ready = false;void waitFunc() {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, []{ return ready; }); // 等待条件成立std::cout << "Wait thread continues." << std::endl;
}void notifyFunc() {std::this_thread::sleep_for(std::chrono::seconds(1));{std::lock_guard<std::mutex> lock(mtx);ready = true;}cv.notify_one(); // 通知等待线程
}int main() {std::thread t1(waitFunc);std::thread t2(notifyFunc);t1.join();t2.join();return 0;
}
6.10.8. 什么是 std::atomic
?它解决了什么问题?
std::atomic<T>
是一种模板类,用于声明原子类型的变量。- 它保证了对变量的读写操作是不可中断的,从而避免了多线程环境下因并发访问而导致的数据竞争问题。
- 常见用途:计数器、标志位、状态机等。
6.10.9. std::async
和 std::future
的作用是什么?
std::async
:启动一个异步任务,并返回一个std::future
对象用于获取结果。std::future
:代表异步任务的结果,可通过get()
获取值或异常。
示例:
#include <iostream>
#include <future>int compute() {return 42;
}int main() {std::future<int> fut = std::async(compute);std::cout << "Result: " << fut.get() << std::endl; // 输出 42return 0;
}
6.10.10. 什么是线程池?C++ 中如何实现?
- 线程池:预先创建一组线程,用于处理多个任务,减少频繁创建销毁线程的开销。
- 实现思路:
- 使用队列保存任务。
- 多个工作线程从队列中取出任务并执行。
- 使用互斥锁和条件变量进行同步。
C++ 标准库没有内置线程池,但可以借助第三方库(如 Boost.Thread)或自行实现。
以上内容涵盖了 C++ 多线程方面的核心知识点,包括线程创建、同步机制、通信机制、异步任务和线程池等内容,适用于 C++ 工程师岗位的面试准备。
6.11. 全局变量和局部变量有什么区别?
是怎么实现的?操作系统和编译器是怎么知道的?
生命周期不同:
全局变量随主程序创建和创建,随主程序销毁而销毁;局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;
使用方式不同:通过声明后全局变量程序的各个部分都可以用到;局部变量只能在局部使用;分配在栈区。
操作系统和编译器通过内存分配的位置来知道的,全局变量分配在全局数据段并且在程序开始运行的时候被加载。局部变量则分配在堆栈里面 。
6.12. C++ 构造函数有哪几种?
在 C++ 中,构造函数是用于初始化对象的特殊成员函数。根据不同的使用场景和特性,C++ 提供了以下几种类型的构造函数:
6.12.1. 默认构造函数(Default Constructor)
- 定义:没有参数或所有参数都有默认值的构造函数。
- 作用:当创建对象时未提供任何参数时自动调用。
- 示例:
class MyClass { public:MyClass() {} // 默认构造函数 };
6.12.2. 带参构造函数(Parameterized Constructor)
- 定义:带有参数的构造函数。
- 作用:用于根据传入的参数对对象进行初始化。
- 示例:
class MyClass { public:MyClass(int x, const std::string& name) {// 初始化代码} };
6.12.3. 拷贝构造函数(Copy Constructor)
-
定义:以同类对象为参数的构造函数。
-
作用:用于创建一个新对象作为已有对象的副本。
-
格式:
class MyClass { public:MyClass(const MyClass& other) {// 拷贝逻辑} };
-
触发时机:
- 显式拷贝构造
MyClass obj2(obj1);
- 对象作为参数传递给函数时
- 函数返回对象时
- 显式拷贝构造
6.12.4. 移动构造函数(Move Constructor)(C++11 引入)
-
定义:以右值引用(
T&&
)为参数的构造函数。 -
作用:避免深拷贝,将资源从临时对象“移动”到新对象。
-
格式:
class MyClass { public:MyClass(MyClass&& other) noexcept {// 移动资源} };
-
适用场景:临时对象(如返回值、强制转换为
std::move()
的对象)
6.12.5. 委托构造函数(Delegating Constructor)(C++11 引入)
- 定义:在一个构造函数中调用另一个构造函数来完成部分初始化。
- 作用:减少重复代码。
- 示例:
class MyClass { public:MyClass(int x) : MyClass(x, "default") {} // 委托构造MyClass(int x, const std::string& name) {// 实际初始化逻辑} };
6.12.6. 显式构造函数(Explicit Constructor)
- 定义:使用
explicit
关键字修饰的构造函数。 - 作用:防止隐式类型转换。
- 示例:
class MyClass { public:explicit MyClass(int x) {// 初始化逻辑} };void func(MyClass obj); func(10); // 错误!不能隐式转换 int -> MyClass
6.12.7. 默认生成的构造函数
- 如果类没有自定义任何构造函数,编译器会自动生成以下构造函数(如果需要):
- 默认构造函数
- 拷贝构造函数
- 移动构造函数(C++11 起)
- 可以通过
= default
显式要求编译器生成。
class MyClass {
public:MyClass() = default; // 使用默认实现MyClass(const MyClass&) = default;
};
6.12.8. 删除的构造函数(Deleted Constructor)
- 使用
= delete
明确禁止某些构造方式。 - 示例:禁止拷贝构造
class MyClass {
public:MyClass(const MyClass&) = delete; // 禁止拷贝构造
};
6.12.9. 总结表格
构造函数类型 | 是否必须 | 是否可重载 | 特点 |
---|---|---|---|
默认构造函数 | 否 | 否 | 无参或全默认参数 |
带参构造函数 | 否 | 是 | 接收参数并初始化 |
拷贝构造函数 | 否 | 否 | 参数为常量左值引用 |
移动构造函数 | 否 | 否 | 参数为右值引用,C++11 引入 |
委托构造函数 | 否 | 否 | 调用其他构造函数 |
显式构造函数 | 否 | 是 | 防止隐式转换 |
默认构造函数(编译器生成) | 编译器自动处理 | 否 | 自动生成,若未自定义 |
删除的构造函数 | 否 | 否 | 禁止特定构造行为 |
掌握这几种构造函数的用途和写法,有助于写出更安全、高效的面向对象代码,并在 C++ 工程师面试中展现扎实的基础知识。
6.13. dynamic_cast static_cast
static_cast关键字( 编译时类型检查 )
const_cast<>() reinterpret_cast<>()
6.14. 智能指针
C++智能指针是面试中经常涉及的热门话题之一。以下是一些常见的关于C++智能指针的面试题及简要解析:
6.14.1. 什么是智能指针?为什么需要使用智能指针?
- 答案:
- 智能指针是一种封装普通指针的对象,用于自动管理动态分配的内存。
- 它通过在对象生命周期结束时自动释放资源(RAII机制)来避免内存泄漏和悬空指针等问题。
6.14.2. 列举 C++ 中常用的智能指针类型,并说明它们的区别?
- 答案:
std::unique_ptr
:- 独占所有权,不能复制,只能移动(move)。
- 适用于单个对象或数组的独占管理。
std::shared_ptr
:- 共享所有权,内部使用引用计数管理资源。
- 当最后一个
shared_ptr
被销毁时,资源才会被释放。
std::weak_ptr
:- 观察共享资源而不增加引用计数,避免循环引用问题。
- 必须配合
shared_ptr
使用。
6.14.3. 如何正确使用 std::unique_ptr
?
- 答案:
- 推荐使用
std::make_unique
创建unique_ptr
。 - 示例:
std::unique_ptr<int> ptr = std::make_unique<int>(10);
- 不能复制,但可以移动:
auto ptr2 = std::move(ptr); // 合法 auto ptr3 = ptr; // 不合法,编译错误
- 推荐使用
6.14.4. 如何正确使用 std::shared_ptr
?
- 答案:
- 推荐使用
std::make_shared
创建shared_ptr
。 - 示例:
std::shared_ptr<int> ptr = std::make_shared<int>(10);
- 可以复制,多个
shared_ptr
共享同一资源:std::shared_ptr<int> ptr2 = ptr; // 合法,引用计数加1
- 推荐使用
6.14.5. std::weak_ptr
的作用是什么?如何使用它?
- 答案:
- 避免
shared_ptr
循环引用导致内存泄漏。 - 使用方法:
std::shared_ptr<int> shared = std::make_shared<int>(10); std::weak_ptr<int> weakPtr = shared;if (auto locked = weakPtr.lock()) { // 获取 shared_ptr// 使用 locked } else {// 资源已被释放 }
- 避免
6.14.6. 如何避免 shared_ptr
的循环引用问题?
- 答案:
- 使用
std::weak_ptr
来打破循环引用。 - 示例:
struct B;struct A {std::shared_ptr<B> b_ptr; };struct B {std::weak_ptr<A> a_ptr; // 使用 weak_ptr 避免循环引用 };
- 使用
6.14.7. std::unique_ptr
和 std::shared_ptr
的性能差异是什么?
- 答案:
std::unique_ptr
更轻量,没有引用计数开销。std::shared_ptr
需要维护引用计数,效率相对较低,但支持共享所有权。
6.14.8. 智能指针是否能完全替代裸指针?
- 答案:
- 大多数情况下可以替代,但在某些底层操作或性能敏感场景可能仍需要裸指针。
- 使用智能指针是现代 C++ 编程的最佳实践。
6.14.9. 如何自定义删除器?
- 答案:
- 对于
unique_ptr
和shared_ptr
,可以通过传递删除器函数或 Lambda 表达式来自定义资源释放逻辑。 - 示例:
std::unique_ptr<int, void(*)(int*)> ptr(new int(10), [](int* p) {delete p; });
- 对于
6.14.10. std::make_shared
相比 new
有什么优势?
- 答案:
- 提高代码安全性(避免内存泄漏)。
- 性能优化:
make_shared
在一次内存分配中完成控制块和对象的分配,而直接使用new
会进行两次分配。
以上是 C++ 智能指针相关的常见面试题,掌握这些内容可以帮助你更好地应对 C++ 技术面试!
6.15. 模板
template class Stack { … };
以下是关于 C++ 模板相关的面试题及参考答案,适用于 C++ 工程师岗位的准备:
6.15.1. 什么是模板?它在 C++ 中的作用是什么?
- 模板(Template) 是 C++ 的一种泛型编程机制,允许编写与数据类型无关的代码。
- 作用:
- 实现通用算法和数据结构(如
std::vector
,std::sort
) - 提高代码复用性
- 支持编译时多态(不同于运行时虚函数机制)
- 实现通用算法和数据结构(如
6.15.2. C++ 中模板有哪些类型?
类型 | 描述 |
---|---|
函数模板(Function Template) | 定义一个通用函数,支持多种类型 |
类模板(Class Template) | 定义一个通用类,支持多种类型 |
变量模板(Variable Template)(C++14) | 定义一个变量,其值可以依赖模板参数 |
别名模板(Alias Template)(C++11) | 为已有类型定义带模板参数的别名 |
6.15.3. 如何定义一个函数模板?
template<typename T>
T max(T a, T b) {return (a > b) ? a : b;
}
- 使用
template<typename T>
或template<class T>
声明模板参数 - 编译器会根据传入的类型自动推导
T
并生成对应的函数
6.15.4. 如何定义一个类模板?
template<typename T>
class Stack {
private:std::vector<T> elements;
public:void push(const T& value);T pop();
};
- 所有成员函数都必须是模板函数或显式特化
- 成员函数通常在 [.h] 文件中实现(否则链接时找不到定义)
6.15.5. 什么是模板特化?如何使用?
- 模板特化(Template Specialization) 是为特定类型提供不同的实现。
- 全特化(Full Specialization):为某一具体类型定制实现
template<>
class Stack<std::string> {// 针对字符串类型的特殊实现
};
- 偏特化(Partial Specialization):为一类类型(如指针、引用等)定制实现
template<typename T>
class Stack<T*> {// 针对所有指针类型的实现
};
6.15.6. 什么是模板的实例化?如何触发?
- 模板实例化(Instantiation) 是将模板替换为实际类型的编译过程。
- 触发方式:
- 显式实例化:
template class Stack<int>;
- 隐式实例化:调用模板函数或创建模板类对象时自动触发
- 显式实例化:
6.15.7. 模板元编程(TMP)是什么?有什么用途?
- 模板元编程(Template Metaprogramming) 是利用模板在编译时执行计算的技术。
- 示例:编译时常量计算
template<int N>
struct Factorial {static const int value = N * Factorial<N - 1>::value;
};template<>
struct Factorial<0> {static const int value = 1;
};
- 用途:
- 提升性能(编译时计算)
- 类型萃取(Type Traits)
- SFINAE(Substitution Failure Is Not An Error)
6.15.8. 什么是 SFINAE?如何在模板中使用?
- SFINAE(Substitution Failure Is Not An Error) 是模板匹配失败时不报错的机制。
- 应用于模板重载决议,常用于条件选择不同实现。
template<typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
add(T a, T b) {return a + b;
}template<typename T>
typename std::enable_if<!std::is_integral<T>::value, T>::type
add(T a, T b) {return a + b;
}
- 如果类型不匹配,编译器不会报错,而是尝试下一个重载版本。
6.15.9. 什么是模板参数推导(Template Argument Deduction)?
- 模板参数推导 是编译器根据函数实参自动确定模板参数的过程。
- 示例:
template<typename T>
void print(T value);print(42); // 推导 T 为 int
print("Hello"); // 推导 T 为 const char*
- 注意:不能从函数返回值推导类型
6.15.10. 模板的默认参数如何设置?
- 和函数参数一样,模板参数也可以设置默认值。
template<typename T = int, typename U = double>
class Pair {T first;U second;
};
- 使用时可省略默认参数:
Pair<> p; // 相当于 Pair<int, double>
6.15.11. 什么是变长模板(Variadic Templates)?如何使用?
- 变长模板(Variadic Templates) 是 C++11 引入的支持任意数量模板参数的机制。
- 常用于实现类似
printf
的可变参数函数。
template<typename T>
void print(T value) {std::cout << value << std::endl;
}template<typename T, typename... Args>
void print(T first, Args... rest) {std::cout << first << ", ";print(rest...);
}
- 支持递归展开参数包(Parameter Pack)
6.15.12. 模板与宏的区别是什么?
对比项 | 模板 | 宏 |
---|---|---|
类型安全 | 是 | 否 |
编译器处理 | 在编译阶段进行类型检查 | 在预处理阶段替换文本 |
调试支持 | 支持 | 不支持 |
性能影响 | 无额外开销(内联优化) | 可能导致重复计算 |
错误提示 | 更清晰 | 难以定位错误来源 |
6.15.13. 模板会导致代码膨胀吗?如何避免?
- 是的,模板可能导致代码膨胀(Code Bloat),即为每个类型生成一份独立的代码副本。
- 避免方法:
- 使用非模板基类封装公共逻辑
- 对相似类型使用类型转换减少重复
- 使用策略模式替代模板
6.15.14. 什么是模板偏序(Partial Ordering)?
- 模板偏序 是多个模板函数/类匹配时,编译器决定哪个更“特化”的规则。
- 举例:
template<typename T>
void foo(T); // 版本1template<typename T>
void foo(T*); // 版本2
当调用 foo(int*)
时,版本2 更特化,优先匹配。
6.15.15. 什么是模板的分离编译(Separation Model)?
- 分离编译 是早期 C++ 标准提出的一种模型,允许将模板声明放在 [.h](file://e:\rep\diary\C\qt\eigen\mandelbrot.h),实现放在 [.cpp](file://e:\rep\diary\C\C++17.cpp)。
- 但大多数现代编译器要求模板实现也必须在头文件中,否则会报链接错误。
- 替代方案:
- 显式实例化
- 将实现写在
.tpp
文件并包含进 [.h](file://e:\rep\diary\C\qt\eigen\mandelbrot.h)
6.15.16. 什么是模板的萃取技术(Type Traits)?
- Type Traits 是通过模板获取类型信息或修改类型属性的技术。
- 常见标准库支持(
<type_traits>
):
std::is_integral<T>::value // 判断是否为整数类型
std::remove_const<T>::type // 移除 const 修饰符
std::enable_if // 条件启用模板
std::conditional // 条件选择类型
- 应用于泛型编程中的类型判断和适配
6.15.17. 模板与继承相比有何优劣?
对比项 | 模板 | 继承 |
---|---|---|
复用方式 | 编译时复用 | 运行时复用 |
性能 | 无运行时开销 | 有虚函数表开销 |
灵活性 | 更灵活,支持任意类型 | 仅限类层次结构 |
调试难度 | 较复杂 | 相对简单 |
适用场景 | 泛型容器、算法 | 面向对象设计、接口抽象 |
6.15.18. 如何防止模板被滥用?
- 建议做法:
- 限制模板使用范围,明确接口意图
- 使用
concept
(C++20)限制模板参数类型 - 避免过度泛化,合理使用继承和接口
- 使用 SFINAE 控制模板匹配行为
6.15.19. C++20 中的 concept
是什么?如何使用?
- Concept 是 C++20 引入的语法特性,用于约束模板参数的类型要求。
- 示例:
template<typename T>
concept Integral = std::is_integral_v<T>;template<Integral T>
T add(T a, T b) {return a + b;
}
- 优势:
- 提高代码可读性
- 编译错误更清晰
- 替代复杂的 SFINAE 技术
6.15.20. 模板与 STL 的关系是什么?
- STL(Standard Template Library) 完全基于模板实现。
- 主要组件:
- 容器(
vector
,map
,list
) - 算法(
sort
,find
,transform
) - 迭代器(
begin()
,end()
) - 函数对象(
function
,bind
)
- 容器(
- 所有组件都是模板实现,支持泛型编程和高效复用。
以上内容涵盖了 C++ 模板的核心概念、常见用法、高级技巧及与现代 C++ 新特性的结合。掌握这些知识点有助于在实际开发中写出高效、可维护的泛型代码,并在 C++ 工程师面试中展现出扎实的技术功底。
6.16. STL
容器(Container),是一种数据结构,如list,vector,和deques ,以模板类的方法提供。为了访问容器中的数据,可以使用由容器类输出的迭代器;
算法(Algorithm)
迭代器(Iterator)
仿函数(Functor)
适配器(Adaptor)
分配器(allocator)
Vector:将元素置于一个动态数组中加以管理,可以随机存取元素(用索引直接存取),数组尾部添加或移除元素非常快速。但是在中部或头部安插元素比较费时;
Deque:是“double-ended queue”的缩写,可以随机存取元素(用索引直接存取),数组头部和尾部添加或移除元素都非常快速。但是在中部或头部安插元素比较费时;
List:双向链表,不提供随机存取(按顺序走到需存取的元素,O(n)),在任何位置上执行插入或删除动作都非常迅速,内部只需调整一下指针;
Set/Multiset:内部的元素依据其值自动排序,Set内的相同数值的元素只能出现一次,Multisets内可包含多个数值相同的元素,内部由二叉树实现,便于查找;
Map/Multimap:Map的元素是成对的键值/实值,内部的元素依据其值自动排序,Map内的相同数值的元素只能出现一次,Multimaps内可包含多个数值相同的元素,内部由二叉树实现,便于查找;
容器类自动申请和释放内存,无需new和delete操作。
Map和Set的底层实现是红黑树,而vector、deque、list的底层实现则分别是数组、双向链表。
STL中算法大致分为四类:
非可变序列算法:指不直接修改其所操作的容器内容的算法。
可变序列算法:指可以修改它们所操作的容器内容的算法。
排序算法:对序列进行排序和合并的算法、搜索算法以及有序序列上的集合操作。
数值算法:对容器内容进行数值计算。
vector 性能