C++面试高频(C++新特性)
C++新特性
1 简述C++11有什么新特性?⭐⭐⭐
- 自动类型推导(Type Inference):引入了 auto 关键字,允许编译器根据初始化表达式的类型自动推导变量的类型。
- 统一的初始化语法(Uniform Initialization Syntax):引入了用花括号 {} 进行初始化的统一语法,可以用于初始化各种类型的对象,包括基本类型、数组、结构体、类等。
- 右值引用(Rvalue References):引入了 && 符号,用于声明右值引用。右值引用具有区分左值和右值的能力,提供了移动语义和完美转发的基础。
- 移动语义(Move Semantics):通过右值引用和移动构造函数(Move Constructor)实现,用于高效地转移资源拥有权,避免不必要的复制和内存分配。
- lambda 表达式(Lambda Expressions):引入了类似于匿名函数的语法,允许在代码中创建匿名函数对象,方便地编写更简洁的、具有局部作用域的函数。
- 并发支持(Concurrency Support):引入了多线程和原子操作的支持,包括线程库、原子类型、互斥锁、条件变量等,使得并发编程更加方便和安全。
- 新的智能指针(Smart Pointers):引入了 std::shared_ptr、std::unique_ptr、std::weak_ptr 等智能指针类模板,提供了更安全、更方便的内存管理机制。
- 静态断言(Static Assert):引入了 static_assert 关键字,允许在编译时对表达式进行静态断言,用于自定义的编译时检查和错误提示。
- 新的标准库组件:包括了正则表达式库、基于范围的循环(Range-based for loop)、哈希表(std::unordered_map、std::unordered_set)、随机数库、异步任务库(std::async)、类型特征工具(std::is_same、std::is_convertible 等)等。
2 auto关键字⭐⭐⭐
优点
- 代码简洁性:可避免书写冗长复杂的类型名称,使代码更简洁易读,尤其在处理标准库容器迭代器或模板类型时优势明显。
- 提高代码可维护性:当代码中的类型发生变化,使用
auto
无需手动修改所有相关变量的类型声明,减少出错可能性。 - 支持匿名类型:处理 lambda 表达式时,由于其类型是编译器生成的匿名类型,无法显式写出,使用
auto
可方便存储和使用。
缺点
- 降低代码可读性:过度使用
auto
会使代码可读性降低,在类型不明确时,阅读者需花费更多时间推断变量实际类型。 - 类型推导可能不符合预期:在某些复杂情况下,编译器推导的类型可能并非开发者期望的类型,涉及引用、常量性等问题时,需开发者深入理解类型推导规则。
- 调试难度增加:调试时,因变量类型由编译器推导,可能给调试带来困难,使用调试工具时难以直观查看变量具体类型。
使用场景
- 迭代器遍历:使用标准库容器迭代时,
auto
可避免写出冗长的迭代器类型,使代码更简洁。 - Lambda 表达式:lambda 表达式类型是匿名的,只能用
auto
存储。 - 模板编程:模板类型可能非常复杂,使用
auto
可简化代码、提高可维护性。 - 初始化复杂表达式:当变量初始化表达式复杂、类型难以确定时,可用
auto
让编译器自动推导类型。
3 Lambda表达式⭐⭐⭐
lambda 表达式的基本语法如下:
[capture](parameters) -> return_type { body }
其中:
capture
:用于从外部作用域捕获变量,可以是值捕获或引用捕获。parameters
:函数参数列表。return_type
:函数返回类型。可以省略,会根据返回表达式自动推导。body
:函数体,可以包含任意合法的代码。
优点
- 代码简洁性:可在需要函数对象的地方直接定义,无需额外定义命名的函数或函数对象类,让代码更紧凑。
- 提高代码的可读性和可维护性:当函数逻辑仅在特定上下文中使用时,将函数定义和使用放在一起,使代码意图更清晰。
- 捕获外部变量:能通过捕获列表捕获外部作用域的变量,在函数体中使用,为函数实现提供更多灵活性。
- 延迟执行:可将 lambda 表达式存储在变量中,在需要时调用,实现延迟执行功能。
缺点
- 降低代码的复用性:通常是匿名的且为特定上下文设计,难以在其他地方复用。若函数逻辑需多处使用,定义命名函数或函数对象更合适。
- 调试难度增加:调试时可能带来困难,尤其在嵌套较深或捕获大量外部变量时,调试器难以清晰显示其状态。
- 性能开销:尽管现代编译器有优化,但在某些情况下,lambda 表达式的创建和调用可能带来性能开销,特别是捕获大量数据时。
使用场景:
- 算法函数对象:作为 STL 的算法函数对象,lambda 表达式可以方便地用于操作容器中的元素。
- 回调函数:作为回调函数传递给其他函数,lambda 表达式可以提供一种简洁的实现方式。
- 并行编程:在并行编程的场景下,lambda 表达式可以用于定义线程函数或并行执行的任务。
作为算法函数对象:
std::vector<int> nums = {1, 2, 3, 4, 5}; std::for_each(nums.begin(), nums.end(), [](int num) { std::cout << num << " "; });
作为回调函数:
void doSomething(int a, int b, std::function<int(int, int)> callback) { int result = callback(a, b); std::cout << "Result: " << result << std::endl; } doSomething(5, 3, [](int x, int y) { return x + y; });
并行编程:
std::vector<int> nums = {1, 2, 3, 4, 5}; std::vector<int> squares(nums.size()); #pragma omp parallel for for (size_t i = 0; i < nums.size(); ++i) { squares[i] = nums[i] * nums[i]; }
4 理解左值和右值?⭐⭐⭐
左值(lvalue)和右值(rvalue)是 C++ 中非常基础且重要的概念,它们描述了表达式的属性,对理解 C++ 中的引用、移动语义等高级特性起着关键作用。以下为你详细介绍这两个概念:
基本定义
- 左值(lvalue):左值是一个表示对象的表达式,它可以出现在赋值语句的左边,具有一个确定的内存地址,可以被取地址。简单来说,左值是一个有名字、可以被引用的对象。
- 右值(rvalue):右值是一个临时的、即将被销毁的表达式,它只能出现在赋值语句的右边,不能被取地址。右值通常是字面量、临时对象或者函数返回的临时结果。
示例区分
左值示
int num = 10; // num 是一个左值,它有自己的内存地址,可以被取地址 int& ref = num; // 可以使用左值引用绑定到左值 num
在上述代码中,num
是一个左值,因为它代表一个具体的对象,有自己的内存地址,并且可以被引用。
右值示例
int result = 3 + 5; // 3 + 5 是一个右值,它是一个临时的计算结果,没有自己的内存地址 // int& badRef = 3 + 5; // 错误:不能使用左值引用绑定到右值
在这个例子中,3 + 5
的结果是一个右值,它是一个临时的表达式结果,没有固定的内存地址,不能被取地址。
5 什么是右值引用?它的作用?⭐⭐⭐
右值引用(R-value reference)是 C++11 引入的一种新的引用类型,用于标识和操作右值。
右值引用使用 &&
符号进行声明,例如 int&&
表示一个右值引用类型的整数。右值引用具有以下几个重要的特性和作用:
- 标识右值:右值引用主要用于标识和操作右值(临时值、表达式结果、将被销毁的值等)。右值引用只能绑定到右值,不能绑定到左值。
- 移动语义:右值引用支持移动语义,通过对临时对象的资源所有权进行移动而不是复制,提高了操作的效率。例如,在对象的拷贝构造函数和拷贝赋值运算符中,可以通过移动构造函数和移动赋值运算符来实现对资源的转移。
- 完美转发:右值引用也用于实现完美转发,即在函数模板中保持参数的值类别。通过使用右值引用参数,可以将传递给函数的右值或左值转发到其他函数,保持传递参数的原始值类别。
右值引用的作用主要体现在以下几个方面:
- 避免不必要的拷贝:通过标识和操作右值,可以避免在操作临时对象时进行不必要的拷贝操作,提高程序的性能。
- 实现移动语义:通过右值引用和移动操作,可以在对象的资源拷贝过程中,将资源所有权从一个对象转移给另一个对象,避免了不必要的资源拷贝。
- 支持完美转发:通过右值引用,可以保持传递参数的值类别,实现参数的完美转发,避免了临时对象的额外拷贝操作。
以下是一个简单示例,展示了右值引用的使用:
void processValue(int&& value) { // 对右值进行操作 // ... } int main() { int x = 10; processValue(5); // 临时值 5 是一个右值 processValue(x); // x 是一个左值,无法绑定到右值引用 return 0; }
在这个示例中,processValue()
函数接受一个右值引用参数,可以绑定到临时值 5,但无法绑定到变量 x
。右值引用可以用于对右值进行特定的操作,提高代码的效率和灵活性。
6 说说移动语义的原理⭐⭐⭐⭐⭐
- 移动语义为了避免临时对象的拷贝,为类增加移动构造函数。移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间同时将要拷贝的对象复制过来,而是"拿"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr
7 迭代器和指针有什么区别?有了指针干嘛还要迭代器?⭐⭐⭐⭐⭐
- 迭代器不是指针,是类模板,表现的像指针。它只是模拟了指针的一些功能,通过重载了指针的一些操作符,如
-->
、*
、++
、--
等。 - 迭代器封装了指针,是一个“可遍历STL容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,相当于智能指针。而迭代器的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。这就是迭代器产生的原因。
8 请你说说智能指针,智能指针为什么不用手动释放内存了?⭐⭐⭐⭐⭐
- 使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等。
- 正是因为指针存在这样的问题,C++便引入了智能指针来更好的管理堆内存。智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
- 因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,自动释放资源。这样程序员就不用再担心内存泄露的问题了。
C++里面有四个指针:auto_ptr、unique_ptr、shared_ptr、weak_ptr,后面三个是C++11支持的,第一个被C++11弃用。
9 auto_ptr有什么样的问题⭐⭐⭐⭐⭐
- 看如下代码:
auto_ptr<string> p1 (new string ("I am jiang douya.")); auto_ptr<string> p2; p2 = p1; //auto_ptr不会报错.
auto指针存在的问题是,两个智能指针同时指向一块内存,就会两次释放同一块资源,存在潜在的内存崩溃问题!因此auto指针被C++11弃用。应该用unique指针替代auto指针。
10 unique_ptr指针实现原理⭐⭐⭐⭐⭐
unique指针规定一个智能指针独占一块内存资源。当两个智能指针同时指向一块内存,编译报错。
我们只需要将拷贝构造函数和赋值拷贝构造函数申明为private或delete。不允许拷贝构造函数和赋值操作符
11 shared_ptr实现原理⭐⭐⭐⭐⭐
std::shared_ptr 是 C++ 标准库 <memory> 头文件中提供的一种智能指针,用于管理动态分配的对象,其核心目标是实现对象的自动内存管理,避免内存泄漏。下面详细介绍 std::shared_ptr 的实现原理。
基本概念
- std::shared_ptr 采用引用计数的方式来管理对象的生命周期。引用计数是一种记录有多少个 std::shared_ptr 实例共享同一个对象的机制。当引用计数变为 0 时,说明没有任何 std::shared_ptr 实例再引用该对象,此时就会自动释放该对象所占用的内存。
实现细节
1. 引用计数
std::shared_ptr 内部维护了一个引用计数,它通常存储在一个控制块(control block)中。控制块是一个额外的内存区域,除了引用计数外,还可能包含其他信息,如弱引用计数(用于 std::weak_ptr)和删除器(用于指定对象的释放方式)。
2. 构造和赋值操作
构造函数:当创建一个 std::shared_ptr 并让它指向一个新对象时,会同时创建一个控制块,并将引用计数初始化为 1。例如:
#include <memory> int main() { std::shared_ptr<int> ptr(new int(42)); // 创建一个 shared_ptr 并指向一个新的 int 对象,引用计数为 1 return 0; }
拷贝构造函数:当使用一个 std::shared_ptr 来初始化另一个 std::shared_ptr 时,它们会共享同一个控制块,并且引用计数会加 1。例如:
#include <memory> int main() { std::shared_ptr<int> ptr1(new int(42)); // 引用计数为 1 std::shared_ptr<int> ptr2 = ptr1; // 引用计数变为 2 return 0; }
赋值运算符:当一个 std::shared_ptr 被赋值给另一个 std::shared_ptr 时,原 std::shared_ptr 的引用计数会减 1(如果减到 0 则释放对象),新 std::shared_ptr 的引用计数会加 1。例如:
#include <memory> int main() { std::shared_ptr<int> ptr1(new int(42)); // 引用计数为 1 std::shared_ptr<int> ptr2(new int(100)); // 引用计数为 1 ptr2 = ptr1; // ptr2 原引用计数减为 0,释放原对象;ptr1 引用计数加 1 变为 2 return 0; }
3. 析构操作
当一个 std::shared_ptr 离开其作用域时,它的析构函数会被调用。析构函数会将引用计数减 1,如果引用计数变为 0,则会释放对象所占用的内存,并销毁控制块。例如:
#include <memory> void func() { std::shared_ptr<int> ptr(new int(42)); // 引用计数为 1 // ptr 离开作用域,引用计数减为 0,释放对象 } int main() { func(); return 0; }
4. 删除器
std::shared_ptr 允许用户指定一个删除器(deleter),用于自定义对象的释放方式。删除器是一个可调用对象,当引用计数变为 0 时,会调用该删除器来释放对象。例如:
#include <memory> #include <iostream> void customDeleter(int* ptr) { std::cout << "Custom deleting..." << std::endl; delete ptr; } int main() { std::shared_ptr<int> ptr(new int(42), customDeleter); return 0; }
总结
std::shared_ptr 通过引用计数和控制块来实现对象的自动内存管理。多个 std::shared_ptr 可以共享同一个对象,通过引用计数的增减来控制对象的生命周期。当引用计数变为 0 时,会自动释放对象所占用的内存。同时,std::shared_ptr 还支持自定义删除器,提供了更灵活的内存管理方式。
12 shared_ptr会不会出现内存泄露?怎么解决?⭐⭐⭐⭐⭐
会出现内存泄露问题。
- 共享指针的循环引用计数问题:当两个类中相互定义shared_ptr成员变量,同时对象相互赋值时,就会产生循环引用计数问题,最后引用计数无法清零,资源得不到释放。
- 可以使用weak_ptr,weak_ptr是弱引用,weak_ptr的构造和析构不会引起引用计数的增加或减少。我们可以将其中一个改为weak_ptr指针就可以了。比如我们将class B里shared_ptr换成weak_ptr。
13 weak_ptr 实现原理⭐⭐⭐⭐⭐
std::weak_ptr 是 C++ 标准库 <memory> 头文件中提供的一种智能指针,它主要用于解决 std::shared_ptr 存在的循环引用问题。下面详细介绍 std::weak_ptr 的实现原理。
基本概念
- std::weak_ptr 是一种弱引用智能指针,它不会增加所指向对象的引用计数,因此不会影响对象的生命周期。它通常与 std::shared_ptr 配合使用,主要用于观察 std::shared_ptr 所管理的对象,并且可以在需要时临时获取一个 std::shared_ptr 来访问该对象。
实现细节
1. 控制块
和 std::shared_ptr 一样,std::weak_ptr 也依赖于控制块(control block)。控制块是一个额外的内存区域,除了存储 std::shared_ptr 所使用的引用计数(强引用计数)外,还会存储一个弱引用计数,用于记录有多少个 std::weak_ptr 指向同一个对象。
2. 构造和赋值操作
构造函数:std::weak_ptr 可以通过 std::shared_ptr 或者另一个 std::weak_ptr 来构造。在构造过程中,它会共享同一个控制块,但不会增加强引用计数,只会增加弱引用计数。例如:
#include <memory> int main() { std::shared_ptr<int> sharedPtr(new int(42)); // 强引用计数为 1,弱引用计数初始为 0 std::weak_ptr<int> weakPtr(sharedPtr); // 强引用计数不变仍为 1,弱引用计数变为 1 return 0; }
赋值运算符:当一个 std::weak_ptr 被赋值给另一个 std::weak_ptr 时,它们会共享同一个控制块,并且对应的弱引用计数会进行相应的调整。例如,将一个 std::weak_ptr 赋值给另一个已存在的 std::weak_ptr 时,原 std::weak_ptr 对应的弱引用计数减 1,新 std::weak_ptr 对应的弱引用计数加 1。
3. 弱引用计数的作用
弱引用计数主要用于跟踪有多少个 std::weak_ptr 指向同一个对象。当强引用计数变为 0 时,说明没有 std::shared_ptr 再引用该对象,此时会释放对象所占用的内存,但控制块不会立即销毁,而是等到弱引用计数也变为 0 时才会销毁。这是因为 std::weak_ptr 可能还需要通过控制块来判断对象是否已经被释放。
4. 检查对象是否有效
std::weak_ptr 提供了 expired() 方法来检查它所指向的对象是否已经被释放。该方法通过检查控制块中的强引用计数来判断对象的有效性。如果强引用计数为 0,说明对象已经被释放,expired() 方法返回 true;否则返回 false。例如:
#include <memory> #include <iostream> int main() { std::shared_ptr<int> sharedPtr(new int(42)); std::weak_ptr<int> weakPtr(sharedPtr); std::cout << "Is object expired? " << (weakPtr.expired() ? "Yes" : "No") << std::endl; sharedPtr.reset(); // 释放 sharedPtr,强引用计数变为 0 std::cout << "Is object expired? " << (weakPtr.expired() ? "Yes" : "No") << std::endl; return 0; }
5. 获取 std::shared_ptr
std::weak_ptr 提供了 lock() 方法,用于在对象仍然有效的情况下获取一个 std::shared_ptr 来访问该对象。在调用 lock() 方法时,会先检查控制块中的强引用计数,如果强引用计数大于 0,说明对象仍然有效,此时会创建一个新的 std::shared_ptr 并返回,同时强引用计数加 1;如果强引用计数为 0,说明对象已经被释放,lock() 方法会返回一个空的 std::shared_ptr。例如:
#include <memory> #include <iostream> int main() { std::shared_ptr<int> sharedPtr(new int(42)); std::weak_ptr<int> weakPtr(sharedPtr); if (auto newSharedPtr = weakPtr.lock()) { std::cout << "Object is valid: " << *newSharedPtr << std::endl; } sharedPtr.reset(); if (auto newSharedPtr = weakPtr.lock()) { std::cout << "Object is valid: " << *newSharedPtr << std::endl; } else { std::cout <
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
该专栏面向嵌入式开发工程师、C++开发工程师,包括C语言、C++,操作系统,ARM架构、RTOS、Linux基础、Linux驱动、Linux系统移植、计算机网络、数据结构与算法、数电基础、模电基础、5篇面试题目、HR面试常见问题汇总和嵌入式面试简历模板等文章。超全的嵌入式软件工程师面试题目和高频知识点总结! 另外,专栏分为两个部分,大家可以各取所好,为了有更好的阅读体验,后面会持续更新!!!