以下是更多 C++ 常见八股文内容:
一、智能指针相关
-
请解释一下 C++ 中的智能指针
- 智能指针是一种用于管理动态分配对象的资源的类。它的主要目的是自动管理对象的生命周期,防止内存泄漏、悬空指针等问题。
- 在 C++ 中,主要有
std::unique_ptr
、std::shared_ptr
和std::weak_ptr
三种智能指针。 -
std::unique_ptr
:- 独占式拥有对象。一个对象只能被一个
std::unique_ptr
所指向。 - 当
std::unique_ptr
被销毁时(例如离开作用域),它所指向的对象会被自动删除。 - 例如:
std::unique_ptr<int> up = std::make_unique<int>(5); // 不需要手动释放内存,当up离开作用域时,所指向的int对象会被自动删除
-
std::shared_ptr
:- 采用引用计数的方式来管理对象。多个
std::shared_ptr
可以指向同一个对象。 - 当最后一个指向对象的
std::shared_ptr
被销毁时,对象才会被删除。 - 例如:
std::shared_ptr<int> sp1 = std::make_shared<int>(10); std::shared_ptr<int> sp2 = sp1; // 此时有两个shared_ptr指向同一个int对象,引用计数为2 // 当sp1和sp2都离开作用域后,int对象才会被删除
-
std::weak_ptr
:- 是一种弱引用,它不控制对象的生命周期。主要用于解决
std::shared_ptr
循环引用的问题。 - 例如,在一个双向链表结构中,如果两个节点都用
std::shared_ptr
相互指向对方,会导致引用计数永远不为 0,造成内存泄漏。使用std::weak_ptr
可以打破这种循环引用。 - 可以通过
std::weak_ptr
的lock
方法获取对应的std::shared_ptr
,如果对象已经被销毁,lock
返回nullptr
。
- 是一种弱引用,它不控制对象的生命周期。主要用于解决
-
std::shared_ptr
的循环引用是怎么回事?如何解决?- 循环引用问题:
- 当有两个或多个对象通过
std::shared_ptr
相互引用时,就可能出现循环引用的情况。 - 例如,考虑如下类定义:
- 当有两个或多个对象通过
- 循环引用问题:
- 采用引用计数的方式来管理对象。多个
- 独占式拥有对象。一个对象只能被一个
class A; class B; class A { public: std::shared_ptr<B> b; A() {} ~A() {} }; class B { public: std::shared_ptr<A> a; B() {} ~B() {} }; int main() { std::shared_ptr<A> a = std::make_shared<A>(); std::shared_ptr<B> b = std::make_shared<B>(); a->b = b; b->a = a; // 此时a和b相互引用,它们的引用计数永远不会降为0,导致内存泄漏 return 0; }
- 解决方法:
- 使用
std::weak_ptr
来打破循环引用。将其中一个std::shared_ptr
改为std::weak_ptr
。 - 修改后的代码如下:
- 使用
class A; class B; class A { public: std::weak_ptr<B> b; A() {} ~A() {} }; class B { public: std::shared_ptr<A> a; B() {} ~B() {} }; int main() { std::shared_ptr<A> a = std::make_shared<A>(); std::shared_ptr<B> b = std::make_shared<B>(); a->b = b; b->a = a; // 此时不会出现内存泄漏,因为std::weak_ptr不增加引用计数 return 0; }
二、异常处理相关
-
请简述 C++ 中的异常处理机制
- C++ 中的异常处理通过
try - catch
语句块实现。 - 在
try
块中放置可能抛出异常的代码。例如:try { int num1 = 10; int num2 = 0; if (num2 == 0) { throw std::runtime_error("除数不能为0"); } int result = num1 / num2; } catch (const std::runtime_error& e) { std::cout << "捕获到异常: " << e.what() << std::endl; }
- 当
try
块中的代码抛出异常时,程序会立即停止try
块中当前代码的执行,并开始在try
块后面的catch
块中查找匹配的异常类型。 - 如果找到匹配的
catch
块(异常类型匹配或者是异常类型的基类),则执行catch
块中的代码来处理异常。 - 还可以有多个
catch
块来捕获不同类型的异常。例如:try { // 可能抛出不同类型异常的代码 } catch (const std::runtime_error& e) { // 处理runtime_error类型的异常 } catch (const std::logic_error& e) { // 处理logic_error类型的异常 } catch (...) { // 捕获所有其他类型的异常 }
-
在函数中抛出异常时需要注意什么?
- 函数的异常声明:在 C++ 中,可以在函数声明中使用
noexcept
关键字表示函数不会抛出异常,或者列出函数可能抛出的异常类型。例如:void func1() noexcept; void func2() throw(std::runtime_error, std::logic_error);
- 资源管理:如果函数在抛出异常前分配了资源(如动态内存、文件句柄等),需要确保在异常抛出时这些资源能够被正确释放。一种常见的方法是使用 RAII(Resource Acquisition Is Initialization)技术,例如通过智能指针或类的构造函数和析构函数来管理资源。
- 异常安全:函数应该具有一定的异常安全性。例如,基本的异常安全保证是如果函数在执行过程中抛出异常,程序的状态不会被破坏(例如不会出现内存泄漏、数据结构处于不一致的状态等)。更高级的异常安全保证还包括强异常安全(操作如果失败,程序状态会回滚到操作之前的状态)等。
- 函数的异常声明:在 C++ 中,可以在函数声明中使用
- C++ 中的异常处理通过
三、模板相关
-
请解释函数模板和类模板的实例化过程
-
函数模板实例化:
- 函数模板是一种通用的函数定义,可以用于生成针对不同类型的函数实例。
- 例如,有如下函数模板:
template <typename T> T add(T a, T b) { return a + b; }
- 当调用
add(1, 2)
时,编译器会根据实参的类型(这里是int
)自动实例化一个int
版本的add
函数,就好像有一个如下定义的函数:int add(int a, int b) { return a + b; }
- 如果调用
add(1.0, 2.0)
,则会实例化一个double
版本的add
函数。 -
类模板实例化:
- 类模板定义了一个通用的类结构,在使用类模板时需要实例化。
- 例如,有类模板:
template <typename T, int N> class Array { public: T data[N]; };
- 当使用
Array<int, 5>
时,编译器会实例化一个特定的类,其中T
被替换为int
,N
被替换为5
。这个实例化后的类就像一个普通的类一样,可以创建对象,如Array<int, 5> arr;
。 -
什么是模板特化?为什么要使用模板特化?
- 定义:
- 模板特化是指为特定的类型或一组类型定制模板的实现。
- 例如,对于一个通用的
compare
函数模板:template <typename T> bool compare(T a, T b) { return a < b; }
- 如果要为
char*
类型提供特殊的比较逻辑(比较字符串内容而不是指针地址),可以进行模板特化:template <> bool compare<char*>(char* a, char* b) { return strcmp(a, b) < 0; }
-
使用原因:
- 当通用的模板实现对于某些特定类型不能提供合适的行为时,就需要模板特化。
- 例如,在处理容器类型时,对于
vector
和list
可能需要不同的算法实现,尽管它们都是容器,但内部结构不同(vector
是连续存储,list
是链表结构),此时可以通过模板特化来针对vector
和list
分别提供高效的算法实现。
- 定义:
-
四、STL(标准模板库)相关
-
vector
和list
在插入和删除操作上有什么区别?-
插入操作:
-
vector
:- 在
vector
的末尾插入元素(使用push_back
)通常是一个很快的操作,时间复杂度为 (平均情况,不考虑动态扩容)。 - 但是在
vector
的中间或开头插入元素(使用insert
)可能会比较慢,因为需要移动插入位置之后的所有元素来为新元素腾出空间,时间复杂度为 ,其中 是vector
的大小。
- 在
-
list
:- 在
list
中任何位置插入元素(使用insert
或者push_front
、push_back
)的时间复杂度都是 ,因为list
是双向链表结构,插入操作只需要修改几个指针即可。
- 在
-
-
删除操作:
-
vector
:- 在
vector
的末尾删除元素(使用pop_back
)时间复杂度为 。 - 在
vector
中间或开头删除元素(使用erase
)需要移动删除位置之后的所有元素,时间复杂度为 。
- 在
-
list
:- 在
list
中删除任何位置的元素(使用erase
或者pop_front
、pop_back
)的时间复杂度都是 ,同样是因为只需要修改几个指针。
- 在
-
-
-
map
和unordered_map
的区别是什么?-
数据结构:
-
map
:map
是基于红黑树(一种自平衡二叉查找树)实现的关联容器。- 它的元素是按照键值自动排序的,在插入和查找操作时,时间复杂度为 ,其中 是容器中的元素个数。
-
unordered_map
:unordered_map
是基于哈希表实现的关联容器。- 它不保证元素的顺序,插入和查找操作在平均情况下时间复杂度为 ,但在最坏情况下(哈希冲突严重时)可能会退化为 。
-
-
迭代器遍历:
-
map
:- 由于
map
中的元素是有序的,通过迭代器遍历map
时,元素是按照键值的顺序访问的。
- 由于
-
unordered_map
:- 遍历
unordered_map
时,元素的访问顺序是无序的,取决于哈希表中元素的存储位置。
- 遍历
-
-
键值要求:
-
map
:- 对于
map
,键值类型必须定义了小于运算符(<
),因为红黑树的操作依赖于元素的顺序比较。
- 对于
-
unordered_map
:- 对于
unordered_map
,键值类型需要定义哈希函数和相等比较运算符(==
),以便在哈希表中进行元素的存储和查找操作。
- 对于
-
-
喜欢的同学可以点点关注!咱们下期再分享更干货的知识!