1、C++11中引入了哪些新的智能指针类型?请描述它们的用法和区别。
C++11中引入了三种新的智能指针类型:std::unique_ptr, std::shared_ptr, 和 std::weak_ptr。
- std::unique_ptr:
- 用法: std::unique_ptr 是一种独占所有权的智能指针,即同一时间内只能有一个 std::unique_ptr 指向一个给定的对象。当 std::unique_ptr 被销毁时,它所指向的对象也会被自动删除。
- std::shared_ptr:
- 用法: std::shared_ptr 是一种共享所有权的智能指针,即多个 std::shared_ptr 可以同时指向同一个对象。对象的最后一个 std::shared_ptr 被销毁时,该对象才会被删除。
- std::weak_ptr:
- 用法: std::weak_ptr 是一种不控制对象生命周期的智能指针,它是为了解决 std::shared_ptr 相互引用导致的循环引用问题而设计的。std::weak_ptr 需要与 std::shared_ptr 一起使用,它可以从一个 std::shared_ptr 或另一个 std::weak_ptr 创建,但它不会增加引用计数。
区别:
- std::unique_ptr 用于独占所有权的场景,只能有一个指针指向对象,适用于资源管理。
- std::shared_ptr 用于共享所有权的场景,可以有多个指针指向同一个对象,适用于多个对象需要访问同一资源的情况。
- std::weak_ptr 用于辅助 std::shared_ptr,解决循环引用问题,不控制对象的生命周期。
2、C++11中的lambda表达式是什么?如何使用它们?
C++11中引入的lambda表达式是一种方便的编写匿名函数对象的方式。它们常用于短小的函数体,特别是在需要作为参数传递给算法或其他函数的情况下。
基本语法:
[捕获列表](参数列表) -> 返回类型 {
函数体
}
- 捕获列表: 定义了lambda表达式可以访问的外部变量,可以是值捕获、引用捕获或隐式捕获。
- 参数列表: 类似于普通函数的参数列表,用于定义传递给lambda表达式的参数。
- 返回类型: 可以显式指定lambda表达式的返回类型,也可以让编译器自动推断。
- 函数体: 定义了lambda表达式的执行逻辑。
使用示例:
- 基本使用:
auto add = [](int a, int b) -> int {
return a + b;
};
int sum = add(3, 5); // 调用lambda表达式
2.作为函数参数:
std::vector vec = {1, 2, 3, 4, 5};
std::sort(vec.begin(), vec.end(), [](int a, int b) {
return a > b; // 降序排序
});
3.捕获外部变量:
- 值捕获: [=] 捕获所有外部变量的副本。
- 引用捕获: [&] 捕获所有外部变量的引用。
- 混合捕获: [x, &y] 捕获变量 x 的副本和变量 y 的引用。
int x = 10;
auto check = [x](int val) {
return val > x;
};
bool result = check(15); // true
3、shared_ptr支持线程安全嘛
std::shared_ptr 在 C++ 标准库中本身并不是线程安全的。也就是说,如果你从多个线程同时访问和修改同一个 std::shared_ptr 对象(例如,一个线程尝试增加引用计数,而另一个线程尝试减少引用计数),那么可能会出现数据竞争(data race)和未定义行为(undefined behavior)。
然而,有几个重要的点需要注意:
对象的析构是线程安全的:尽管 std::shared_ptr 的操作本身不是线程安全的,但当最后一个 shared_ptr 指向的对象被销毁时,这个对象的析构函数会被安全地调用,即使存在多个线程都在尝试减少 shared_ptr 的引用计数。这是因为 C++ 标准库保证了析构函数的调用是序列化的。
原子操作:你可以使用原子操作(如 std::atomic)来保护对 shared_ptr 的访问。但是,请注意,仅仅保护对 shared_ptr 本身的访问是不够的,因为如果多个线程都在操作同一个 shared_ptr 所指向的对象,那么对这个对象的访问也需要是线程安全的。
线程安全的共享所有权:std::shared_ptr 的主要目的是在多个 shared_ptr 之间安全地共享对象的所有权。只要每个线程都通过其自己的 shared_ptr 来访问对象,并且没有线程直接修改 shared_ptr(而是通过原子操作或其他同步机制),那么对象的共享所有权就是线程安全的。
自定义删除器:如果你为 std::shared_ptr 提供了自定义的删除器(deleter),那么这个删除器应该也是线程安全的。因为当 shared_ptr 的引用计数变为零时,删除器会被调用。
总之,虽然 std::shared_ptr 本身不是线程安全的,但你可以通过其他机制(如原子操作、互斥锁等)来保护对它的访问,从而确保在多线程环境中安全地使用它。同时,还需要确保对 shared_ptr 所指向的对象的访问也是线程安全的。
4、谈谈C++11中的智能指针std::unique_ptr和std::shared_ptr的区别和适用场景。
C++11 引入了智能指针来自动管理内存,主要包括 std::unique_ptr 和 std::shared_ptr。这两种智能指针各有特点,适用于不同的场景。
std::unique_ptr
- 特点:std::unique_ptr 是一种独占式智能指针,它保证同一时间只有一个指针拥有对对象的所有权。当 std::unique_ptr 被销毁时,它所指向的对象也会被自动删除。
- 适用场景: 用于管理资源的独占访问。 实现资源所有权的转移,例如从函数返回动态分配的对象。 用于实现 RAII (Resource Acquisition Is Initialization) 模式,确保资源在任何情况下都能被正确释放。
std::shared_ptr
- 特点:std::shared_ptr 是一种共享式智能指针,它允许多个指针共享对同一个对象的所有权。std::shared_ptr 使用引用计数机制来跟踪有多少个指针共享同一个资源,当最后一个 std::shared_ptr 被销毁时,资源会被自动释放。
- 适用场景: 用于实现共享所有权的资源管理,如共享数据或缓存。 用于实现循环引用的数据结构,如图或双向链表(需配合 std::weak_ptr 使用以避免内存泄漏)。 用于实现 observer 模式,观察者使用 std::shared_ptr 来共享对被观察对象的访问。
5、C++11中的std::array容器与C风格数组相比有哪些优势?请举例说明。
std::array 是 C++11 引入的一种容器,它是对 C 风格数组的一种封装,提供了更安全、更方便的操作。与 C 风格数组相比,std::array 有以下几个优势:
- 类型安全:std::array 是一个强类型的容器,它的大小是类型的一部分,这有助于防止类型不匹配的错误。
- 自动管理大小:std::array 的大小在编译时就确定了,不需要手动管理数组的大小,这减少了出错的可能性。
- 支持标准容器操作:std::array 支持标准容器操作,如 begin(), end(), size(), at(), 等等,这使得它可以与标准库算法一起使用,提高了代码的可读性和可维护性。
- 支持范围 for 循环:std::array 可以直接用于范围 for 循环,这使得遍历数组变得更简单。
- 不会退化为指针:C 风格数组在作为函数参数时会退化为指针,而 std::array 不会,这有助于保持数组的完整性。
示例代码:
#include
#include
int main() {
std::array arr = {1, 2, 3, 4, 5};
// 使用范围 for 循环遍历
for (int num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
// 使用标准容器操作
std::cout << "Array size: " << arr.size() << std::endl;
std::cout << "Element at index 2: " << arr.at(2) << std::endl;
return 0;
}
std::array 提供了比 C 风格数组更安全、更方便的操作,并且支持标准容器接口,使得它可以更好地与 C++ 标准库集成。在 C++ 中,推荐使用 std::array 来代替 C 风格数组。
6、C++11中的std::move语义是什么?如何使用它来优化性能?
在C++11中,std::move 是一个函数模板,用于将对象转换为右值引用,从而允许移动语义的使用。移动语义允许资源的所有权从一个对象转移到另一个对象,这意味着不需要进行资源的复制,从而可以优化性能。
基本用法:
#include
#include
int main() {
std::vector vec1 = {1, 2, 3, 4, 5};
std::vector vec2 = std::move(vec1); // 将vec1的资源移动到vec2
std::cout << "vec1 size: " << vec1.size() << std::endl; // 输出: vec1 size: 0
std::cout << "vec2 size: " << vec2.size() << std::endl; // 输出: vec2 size: 5
return 0;
}
在上面的示例中,std::move(vec1) 将 vec1 转换为右值引用,这允许 vec2 的构造函数通过移动语义接管 vec1 的资源,而不是复制它们。这样,vec1 的资源被转移给 vec2,而 vec1 变为空。
优化性能:
- 减少不必要的复制: 使用 std::move 可以减少在对象赋值或函数返回时发生的不必要的资源复制,特别是对于大型对象或容器,这可以显著提高性能。
- 优化临时对象的使用: 在函数参数传递中使用 std::move 可以避免临时对象的复制,提高效率。
- 实现高效的数据结构: 在实现数据结构如动态数组、链表等时,使用 std::move 可以在元素插入、删除或移动时减少资源复制,提高数据结构的性能。
注意事项:
- 使用 std::move 后,原对象通常处于未定义的状态,不应再使用该对象。
- 在使用 std::move 时需要谨慎,确保不会导致资源泄露或无效引用。
7、C++11中如何使用std::function和std::bind来处理回调函数?
在C++11中,std::function 和 std::bind 提供了灵活的方式来处理回调函数。std::function 是一个通用的函数包装器,可以存储和调用任何可调用对象,如函数指针、成员函数指针、lambda表达式等。std::bind 可以用于绑定函数参数,生成新的可调用对象。
使用 std::function:
#include
#include
void greet(const std::string& name) {
std::cout << "Hello, " << name << "!" << std::endl;
}
int main() {
std::function callback = greet;
callback("Alice"); // 调用回调函数
return 0;
}
在上面的示例中,std::function 用于存储和调用函数 greet。
使用 std::bind:
#include
#include
void greet(const std::string& greeting, const std::string& name) {
std::cout << greeting << ", " << name << "!" << std::endl;
}
int main() {
auto greetAlice = std::bind(greet, "Hello", std::placeholders::_1);
greetAlice("Alice"); // 输出: Hello, Alice!
auto sayHello = std::bind(greet, std::placeholders::_1, "Bob");
sayHello("Hi"); // 输出: Hi, Bob!
return 0;
}
在这个示例中,std::bind 用于创建新的可调用对象 greetAlice 和 sayHello,它们分别绑定了不同的参数。
结合使用 std::function 和 std::bind:
#include
#include
#include
void notify(int eventID) {
std::cout << "Event " << eventID << " occurred." << std::endl;
}
int main() {
std::vector> callbacks;
for (int i = 1; i <= 3; ++i) {
callbacks.push_back(std::bind(notify, i));
}
for (auto& callback : callbacks) {
callback(); // 调用每个回调函数
}
return 0;
}
在这个示例中,std::bind 用于绑定不同的事件ID,然后将绑定后的可调用对象存储在 std::function 容器中,最后遍历容器并调用每个回调函数。
粉丝福利, 免费领取C/C++ 1000道大厂面试题、进阶学习路线)↓↓↓↓↓↓见下面↓↓免费领取↓↓
8、谈谈C++11中的原子操作(atomic operations)及其在多线程编程中的应用。
在C++11中,引入了原子操作(atomic operations)的概念,通过
基本用法:
#include
#include
#include
std::atomic counter(0); // 定义原子类型变量
void incrementCounter() {
for (int i = 0; i < 1000; ++i) {
++counter; // 原子递增操作
}
}
int main() {
std::thread t1(incrementCounter);
std::thread t2(incrementCounter);
t1.join();
t2.join();
std::cout << "Counter: " << counter << std::endl;
return 0;
}
在上面的示例中,counter 是一个原子类型的变量,两个线程分别对其进行递增操作。由于counter是原子类型,因此即使在多线程环境中,每次递增操作也是线程安全的。
在多线程编程中的应用:
- 线程安全的计数器: 如上例所示,原子操作可用于实现线程安全的计数器,确保计数的正确性。
- 锁自由数据结构: 原子操作可以用于实现无锁(lock-free)数据结构,提高并发性能。
- 同步和协调: 原子操作还可以用于线程间的同步和协调,例如使用原子标志来控制线程的执行流程。
9、谈谈C++11中的alignas和alignof关键字及其用途。
在C++11中,引入了两个关键字 alignas 和 alignof,用于处理类型和变量的对齐要求。
alignas:
- alignas 关键字用于指定变量或类型的对齐要求。对齐是指数据在内存中的起始地址必须是某个数(对齐值)的倍数。
- alignas 可以用于提高性能,因为某些硬件平台访问对齐的数据比访问未对齐的数据更快。
- 语法: alignas(对齐值)
示例:
#include
#include
struct alignas(16) AlignedStruct {
int a;
double b;
};
int main() {
std::cout << "Alignment of AlignedStruct: " << alignof(AlignedStruct) << std::endl;
return 0;
}
在上面的示例中,AlignedStruct 被指定为以 16 字节对齐。使用 alignof 可以检查其对齐要求。
alignof:
- alignof 关键字用于查询类型的对齐要求。
- 语法: alignof(类型)
示例:
#include
int main() {
std::cout << "Alignment of int: " << alignof(int) << std::endl;
std::cout << "Alignment of double: " << alignof(double) << std::endl;
return 0;
}
在这个示例中,alignof 被用来查询 int 和 double 类型的对齐要求。
alignas 和 alignof 是C++11中引入的关键字,用于处理数据对齐的问题。alignas 允许指定变量或类型的对齐要求,而 alignof 用于查询类型的对齐要求。这些特性在需要优化内存访问性能或满足特定硬件要求的场景中非常有用。
10、谈谈你对C++11中引入的auto关键字的理解,它在什么情况下特别有用?
在C++11中,auto 关键字被引入作为一种类型推断的机制,它允许编译器自动推断变量的类型。
理解:
- 使用 auto 关键字时,你不需要显式地指定变量的类型,编译器会根据变量的初始化表达式来推断其类型。
- auto 可以用于局部变量、函数参数、返回类型等场景。
特别有用的情况:
- 迭代器: 当你使用STL容器(如 std::vector, std::map 等)时,迭代器的类型可能非常复杂。使用 auto 可以简化代码并提高可读性。
std::vector vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
// 使用迭代器it
}
2.范围for循环: 在范围for循环中,auto 可以自动推断元素的类型,使代码更简洁。
for (auto& element : vec) {
// 使用元素element
}
3.函数返回类型推断: 在C++14中,auto 还可以用于推断函数的返回类型。
auto add(int x, int y) {
return x + y;
}
4.泛型编程: 在泛型编程中,当类型参数非常复杂或者难以显式指定时,auto 可以简化代码。
template
void process(const std::vector& vec) {
for (auto& element : vec) {
// 处理元素element
}
}
总的来说,auto 关键字在C++11中的引入极大地提高了代码的可读性和编写的便捷性,特别是在处理复杂类型、泛型编程以及STL容器和算法时,auto 的使用可以显著简化代码。