【C++八股-第十期】C++ 11 新特性

感谢花花,你必Offer

提纲:

👉 八股:

  1. C++11引入的autodecltype关键字分别有什么作用

  2. STL是什么、定义、含义、介绍

  3. 迭代器是什么?迭代器是指针么?

  4. 主要的几个STL容器如何正确删除重复元素

  5. 简单介绍一下函数模板和模板函数

  6. 在C++中你使用过哪些智能指针?

  7. 两个auto指针同时指向一块内存会发生什么?auto指针存在什么潜在问题?

  8. unique_ptr如何保证一个智能指针独占一块内存资源?

  9. shared_ptr会导致内存泄露问题么?怎么解决?

  10. 介绍一下cast类型转换的方式、用途、区别

  11. 你了解 Lambda 表达式么

1. C++11引入的autodecltype关键字分别有什么作用

C++11引入了auto和decltype关键字,使用他们可以在编译期就推导出变量或者表达式的类型,方便开发者编码也简化了代码。

  • auto:让编译器在编译器就推导出变量的类型,可以通过=右边的类型推导出变量的类型。
auto a = 1; // 1是int型,可以自动推导出a是int
  • decltype:相对于auto用于推导变量类型,而decltype则用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算。
cont int &i = 1;
int a = 2;
decltype(i) b = 2; // b是const int&

详细内容请见此帖

2. STL是什么、定义、含义、介绍

STL(Standard Template Library)是“标准模板库”,是C++标准库的一部分,不用单独安装。

STL 本质就是借助C++ 中的模板(Template)把常用的数据结构及其算法都实现了一遍,并且做到了数据结构和算法的分离。

STL常见常用的容器

  • 顺序容器 vector(向量容器) deque(双端队列容器) list(双向链表)
  • 关联容器
    • set(单重集合) multiset(双重集合)
    • map(单重映射表) multimap(多重映射表)
  • 容器适配器 stack(栈) queue(队列) prority_queue(优先级队列)

拓展(了解即可):

STL由以下六部分组成:容器、算法迭代器、函数对象、适配器和内存分配器。其中,后四种组成是为了更好地支持和服务于容器算法

组成 含义
容器 封装数据结构的模板类,例如 vector(向量容器)、list(链表容器)等。
算法 STL 提供了大约 100 种算法,这些算法被设计为模板函数,定义在 std 命名空间中,大部分位于头文件 <algorithm> 中,少部分在头文件 <numeric> 中。
迭代器 迭代器用于在容器中读写数据,是连接容器和算法的桥梁。
函数对象 如果一个类重载了 () 运算符,该类的对象就称为函数对象(或仿函数),该类称为函数对象类。
适配器 适配器可以改变类的接口(模板参数),使不兼容的类能够协同工作。容器、迭代器和函数都有相应的适配器。
内存分配器 为容器类模板提供自定义的内存分配和释放功能。由于内存分配策略的改变通常是高级用户的需求,所以内存分配器对一般用户来说不常用。

3. 迭代器是什么?迭代器是指针么?

迭代器(Iterator)是用于遍历容器(如数组、链表、集合等)中的元素的对象或机制。它提供了一种统一的方式来访问容器中的元素,而不需要了解容器的内部实现细节。

在C++中,迭代器是Standard Template Library(STL标准模板库)的一部分,并且广泛用于各种容器类(如vector、list、set等)。

① 迭代器其本质是 类模板,只是表现上像指针。迭代器并不是指针,只是通过重载了指针的一些操作符封装了指针,如 --->*++等。迭代器把对不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。

②迭代器相当于智能指针,是一个“可遍历STL容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升(lift),迭代器可以根据不同类型的数据结构来实现不同的++,--等操作;

③迭代器返回的是对象引用而不是对象的值所以cout只能输出迭代器使用 * 取值后的值而不能直接输出其自身

拓展(了解即可):

迭代器的类型:

STL中定义了几种类型的迭代器,每种迭代器提供不同的功能:

输入迭代器(Input Iterator):只读访问,单向遍历。

输出迭代器(Output Iterator):只写访问,单向遍历。

前向迭代器(Forward Iterator):读写访问,单向遍历,可多次遍历。

双向迭代器(Bidirectional Iterator):读写访问,双向遍历。

随机访问迭代器(Random Access Iterator):读写访问,支持随机访问。

4. 主要的几个STL容器如何正确删除重复元素

std::vector

std::vector 是一个动态数组,可以包含重复元素。注意在删除元素时要防止迭代器失效的问题。需要正确地使用 std::unique,std::erase 来删除元素时

    std::vector<int> vec = {1, 2, 2, 3, 3, 4, 5, 5, 5};

	//vec -> 1 2 2 3 3 4 5 5 5

    // 使用 std::unique 移除相邻的重复元素,last是个迭代器
    auto last = std::unique(vec.begin(), vec.end());

	//vec -> 1 2 3 4 5 4(last) 5 5 5

    // 调整容器大小,删除重复元素后的多余部分
    vec.erase(last, vec.end());

	//vec -> 1 2 3 4 5

std::list

std::list 是一个双向链表,它的元素在内存中分布是不连续的,每个元素都有指向前一个和后一个元素的指针。由于其特殊的数据结构,删除元素时不会导致其他元素的迭代器失效

    std::list<int> lst = { 1, 2, 2, 3, 3, 4, 5, 5, 5 };

    auto it = lst.begin();
    while (it != lst.end()) {
        // 查找下一个不同于当前元素的元素位置
        auto next_it = std::next(it);
        while (next_it != lst.end() && *next_it == *it) {
            ++next_it;
        }

        // 删除从 it+1 到 next_it-1 的所有重复元素
        lst.erase(std::next(it), next_it);

        // 移动到下一个不同的元素位置
        it = next_it;
    }
}

std::setstd::map

std::set 是一个有序的集合,只能存储唯一值。std::map 是一种键值对的有序关联容器,其中键是唯一的。两者底层用红黑树实现,确保了元素的唯一性。

拓展(了解即可):

在 C++ 的标准库中,std::unique 是一个算法函数,用于在容器中移除连续重复的元素,并返回一个指向不重复区域之后第一个重复元素的迭代器。它通常与 std::sort 结合使用,用于处理需要去除重复元素的容器。

std::erase 用于从容器中删除指定位置或范围内的元素。这个函数的具体行为取决于容器类型的不同,但通常会返回一个指向删除操作后第一个被删除元素之后的位置的迭代器。

5. 简单介绍一下函数模板和模板函数

关注后两个字就知道他们具体是什么了

函数模板(Function Template):

函数模板的重点是模板,是一个通用的函数描述,它可以用来生成特定类型或值的函数实例。定义函数模板的语法如下:

template <typename T>
void myFunction(T value) {
    // 函数体
}

在上面的例子中,myFunction 是一个函数模板,它接受一个类型为T的参数 value,并且可以处理任意类型 T的数据。函数模板中的 T是一个模板参数,可以用于指定函数参数的类型。

在调用时根据实参的类型推导出模板参数 T,从而生成特定类型的函数实例。

myFunction(10);     // 生成 myFunction<int>(int value)
myFunction(3.14);   // 生成 myFunction<double>(double value)

模板函数(Template Function):

模板函数是一种特殊的函数模板实例化,即通过具体类型参数生成的实际函数。在 C++ 中,函数模板实例化后的函数称为模板函数。

上面的myFunction(10)myFunction(3.14) 实际上调用的是模板函数 myFunction<int>myFunction<double> 的实例。

区别与总结:

  • 函数模板 是一个通用的模板描述,用来生成函数实例。

  • 模板函数 是函数模板实例化后的具体函数。

  • 函数模板可以生成多个不同类型的模板函数实例。

  • 模板函数是通过函数模板根据不同的实参类型生成的特定版本。

使用函数模板和模板函数可以使代码更加通用和灵活,适用于处理多种类型或多种参数的情况。

6. 在C++中你使用过哪些智能指针?

智能指针的实质是一个类(或者对象),只是行为表现的像一个指针。

智能指针是 C++ 中用来管理动态分配内存的重要工具,它们基于 RAII(资源获取即初始化)原则,能够在对象生命周期结束时自动释放资源(超出类的作用域时,自动调用析构函数)避免了常见的内存泄漏和二次释放等问题。

  • RAII -> 获取一个资源,一定在一个构造函数中获取,在一个析构函数中析构。

不同类型的智能指针(4种):

auto_ptr(C++98引入,C++11被弃用):

  • auto_ptr 是 C++98 中引入的智能指针,用于管理动态分配的单个对象。
  • 它没有引用计数机制,因此不支持多个指针共享同一块内存,且在赋值时会转移所有权。
  • 由于存在严重的缺陷,C++11 已经弃用了 auto_ptr,现推荐使用更安全和功能更丰富的智能指针。

unique_ptr

  • unique_ptr 用于管理独占所有权的动态分配对象,及一个智能指针独占一块内存资源。当两个智能指针同时指向一块内存,编译报错。
  • 每个 unique_ptr 拥有对其所管理对象的唯一所有权,不能进行复制,但可以进行移动。
  • 当 unique_ptr 超出作用域或被显式释放时,会自动释放其所管理的内存。

shared_ptr

  • shared_ptr 是可以多个指针共享同一块内存的智能指针。
  • 它使用引用计数来跟踪多少个 shared_ptr 共享同一对象,当最后一个 shared_ptr 被销毁时,内存才会被释放。
  • 支持拷贝和赋值操作,每次调用拷贝构造函数或赋值拷贝构造函数会增加引用计数(+1),析构时减少引用计数,当引用计数为零时释放内存。

weak_ptr

  • weak_ptr (弱引用) 是 shared_ptr 的一种辅助类,用于解决 shared_ptr 的循环引用问题。
  • weak_ptr 允许访问由 shared_ptr 管理的对象,但不影响其引用计数。即 weak_ptr 的构造和析构不会引起引用计数的增加或者减少
  • 当所有指向对象的 shared_ptr 被销毁后,即使有 weak_ptr 指向对象,也无法访问该对象。

拓展(了解即可):

选择智能指针的依据:

  • 如果能确保指针独占所有权,使用 unique_ptr。

  • 如果需要多个指针共享同一块内存,使用 shared_ptr。

  • 如果存在潜在的循环引用,可以使用 weak_ptr 辅助解决。

7. 两个auto指针同时指向一块内存会发生什么?auto指针存在什么潜在问题?

① 所有权转移问题:

auto_ptr 的拷贝构造函数和赋值操作符会转移所有权,这意味着一个 auto_ptr 被复制给另一个 auto_ptr 时,源 auto_ptr 将失去对该内存的所有权并变为 null。

    std::auto_ptr<int> p1(new int(10));
    std::auto_ptr<int> p2 = p1; // p1 的所有权转移给 p2  此时 p1 = null

② 双重释放问题:

如果没有明确知道某个 auto_ptr 已经转移了所有权并尝试再次释放同一块内存,就会导致双重释放,进而导致未定义行为和程序崩溃。

8. unique_ptr如何保证一个智能指针独占一块内存资源?

unique_ptr 通过禁止复制操作和支持移动操作来确保独占所有权。

当试图复制 unique_ptr 时,编译器会报错,因为其 拷贝构造函数和拷贝赋值操作符被标记为删除(= delete),禁止了复制行为。

unique_ptr 的独占所有权特性可以防止内存管理中的许多常见问题,如双重释放和未定义行为。

9. shared_ptr会导致内存泄露问题么?怎么解决?

共享指针(shared_ptr)在存在循环引用的情况下,会导致内存泄漏。

共享指针的循环引用问题

当两个类中相互持有 shared_ptr 成员变量,并且对象相互引用时,就会产生循环引用。例如:

class A {
public:
    std::shared_ptr<B> ptrB;
};

class B {
public:
    std::shared_ptr<A> ptrA;
};


 std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();

    a->ptrB = b;
    b->ptrA = a;
    
    // 手动释放智能指针,看不到析构函数调用,存在内存泄漏
    // a.reset();
    // b.reset();

在上述示例中,当 a 和 b 形成循环引用后,它们的引用计数都不会为零,因此析构函数不会被调用,导致内存泄漏。

使用 weak_ptr 解决循环引用

我们将上述例子中其中一个shared_ptr改为weak_ptr指针就可以了。因为 weak_ptr 不会增加引用计数。比如我们将class Bshared_ptr换成weak_ptr

class B {
public:
    std::weak_ptr<A> a_weak_ptr;
};

std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();

// 使用 weak_ptr 打破循环引用
a->b_ptr = b;
b->a_weak_ptr = a;

10. 介绍一下cast类型转换的方式、用途、区别

在 C++ 中,类型转换(type casting)是将一种数据类型转换为另一种数据类型的过程。

方式(4种): static_cast、const_cast、dynamic_cast、reinterpret_cast

静态转换(Static Cast) - 最常用:

  • static_cast用于将一种数据类型强制转换为另一种数据类型。它可以用来转换基本数据类型之间的转换,也可以在继承层次结构中进行向上和向下转换(非多态类型)

  • 示例int num = static_cast<int>(3.14);

常量转换(Const Cast):

const_cast 用于去除对象的 const 属性或将对象从 volatile 类型转换为非 volatile 类型。

需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用(就是下面的ptr)。

int main() {
    const int value = 10; // 使用 const 关键字声明的变量,其值在初始化后不能被修改。

    const int* ptr = &value; // 指向常量整数的指针

    // 尝试修改常量整数的值,编译时会报错
    // *ptr = 20; // 错误:试图修改常量对象

    // 使用 const_cast 去除指针的常量性,允许修改
    int* mutablePtr = const_cast<int*>(ptr);
    *mutablePtr = 20; // 可以修改常量对象的值

    std::cout << "Modified value: " << value << std::endl; // 输出修改后的值

    return 0;
}

动态转换(Dynamic Cast):

  • dynamic_cast 只适用于多态类型的安全转型,主要用于安全向下转型(downcast)。它能够在运行时检查是否可以安全地将基类指针或引用转换为派生类指针或引用。
  • dynamic_cast通过判断变量运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
  • 示例Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);

重新解释转换(Reinterpret Cast):

  • reinterpret_cast是最不安全的转换方式,用于将一个指针类型转换为另一个不同类型的指针,或者将任何类型的整数转换为指针类型,反之亦然。
  • 主要用三种用途:
    • 改变指针或引用的类型
    • 将指针或引用转换为一个足够长度的整形
    • 将整型转换为指针或引用类型。
  • 示例int* intptr = reinterpret_cast<int*>(charptr);

11. 你了解 Lambda 表达式么

Lambda 表达式是 C++11 引入的一种函数对象(即闭包)表示方式,它允许我们在需要函数对象的地方直接定义匿名函数,而不必显式地编写函数对象类或者命名函数。

Lambda 表达式的基本语法:

[capture list] (parameters) -> return_type {
    // 函数体
    // 可以访问 capture list 中的变量
}
  • Capture List(捕获列表):用于捕获外部变量,可以为空、显式捕获特定变量(值捕获或引用捕获)、隐式捕获(按值或按引用)。
  • Parameters(参数列表):与普通函数的参数列表类似,可以为空或包含一个或多个参数。
  • Return Type(返回类型):可以省略(由编译器推导)或显式指定返回类型。
  • 函数体:Lambda 表达式的主体部分,包含具体的操作。

Lambda 表达式的用途:

  • 方便的函数对象:Lambda 表达式允许直接在需要函数对象的地方定义和使用函数,而无需显式创建函数对象类。

  • 简化回调:用于 STL 算法、事件处理器等需要回调函数的地方,可以直接将 lambda 作为回调函数传递,代码更为简洁和直观。

  • 局部性:Lambda 表达式在定义处周围形成一个局部闭包,可以捕获周围的变量,使得在其作用域内能够方便地访问外部变量。

示例:

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // 使用 Lambda 表达式打印所有元素
    std::for_each(numbers.begin(), numbers.end(), [](int num) {
        std::cout << num << " ";
    });
    std::cout << std::endl;

    // 使用 Lambda 表达式计算所有元素的和
    int sum = 0;
    std::for_each(numbers.begin(), numbers.end(), [&sum](int num) {
        sum += num;
    });
    std::cout << "Sum: " << sum << std::endl;

    return 0;
}
#C++八股##C++#
全部评论
右值引用和完美转发没问 定长数组也没
点赞 回复 分享
发布于 2024-07-17 04:44 上海

相关推荐

暮雨轻歌:看起来hr不能接受我菜查看图片
点赞 评论 收藏
分享
评论
10
17
分享

创作者周榜

更多
牛客网
牛客企业服务