后端开发面经系列 -- 同程旅行C++一面

同程旅行C++一面

公众号:阿Q技术站

1、sizeof与strlen的区别?

  1. sizeof是C/C++中的操作符,用于获取一个数据类型或变量所占用的字节数。它是在编译时计算的,返回的是数据类型或变量的字节数,而不是字符数。主要用于静态分配内存和获取数据类型的大小。

    int arr[5];
    size_t size = sizeof(arr); // size = 20 (5 * 4 字节)
    
  2. strlen是C/C++中的函数,用于获取一个以null终止字符数组(C字符串)的长度,即字符数。它是在运行时计算的,遍历字符数组直到遇到null终止符。主要用于计算C字符串的长度。

    const char* str = "Hello, World!";
    size_t length = strlen(str); // length = 13
    

需要注意的是,sizeof通常用于获取数据类型的大小,而strlen用于计算C字符串的长度。

2、运算符和函数有什么区别?

  1. 运算符
    • 运算符是C++中用于执行各种操作的特殊符号或关键字。它们可以用于操作各种数据类型,包括算术运算、逻辑运算、比较运算、位运算等。
    • 可以分为一元运算符(作用在一个操作数上,例如:++--)、二元运算符(作用在两个操作数上,例如:+-)以及三元运算符(例如:条件运算符? :)。
    • 过载运算符:C++允许用户自定义类的运算符重载,这允许你自定义如何处理类对象的运算。
    • 它是编译器内置的一部分,它们具有特定的语法和预定义的行为,这意味着它们通常更高效。
  2. 函数
    • 函数是C++中的自包含代码单元,它们封装了一系列操作,可以在需要的时候多次调用。
    • 函数的作用是执行特定的任务或操作,可以接受参数(输入)并返回结果(输出)。
    • 函数的参数和返回类型可以是各种数据类型,包括用户自定义类型。
    • 主要优势在于它们提供了代码重用的机制,允许将代码划分为小的可管理单元。
    • C++中也有运算符重载的概念,这允许用户自定义运算符的行为,但与运算符重载不同,它需要使用函数来实现。

区别:

  • 运算符是用于执行操作的特殊符号,而函数是自包含代码单元,用于执行一系列操作。
  • 运算符通常与基本数据类型一起使用,而函数可以用于各种操作,包括处理复杂的数据结构和对象。
  • 运算符具有内置的语法和行为,而函数的行为需要在函数体内定义。
  • 运算符重载是C++中的一个特性,允许用户自定义运算符的行为,而函数是C++中的基本构建块之一,用于组织和执行代码。

3、new和malloc?

  1. new

    • new 是C++中的运算符,而不是函数。它使用对象的构造函数来分配内存。

      • 当使用 new 分配内存时,它会执行以下操作:
        • 分配足够的内存以容纳对象或对象数组。
        • 调用对象的构造函数来初始化内存中的对象。
        • 返回指向已分配内存的指针。
    • new 不需要显式指定分配的内存大小,因为它会自动计算对象或数组的大小。

  2. malloc

    • malloc 是C语言标准库函数,也可以在C++中使用。它不执行对象的构造函数。

    • 当使用 malloc 分配内存时,它会执行以下操作:

      • 分配指定大小的内存块。
      • 返回指向已分配内存的指针。
    • malloc 需要显式指定分配的内存大小,通常使用 sizeof 运算符来计算对象或数组的大小。

区别:

  • new 会调用对象的构造函数来初始化内存中的对象,而 malloc 不会。这使得 new 更适合用于C++类对象。
  • new 不需要显式指定内存大小,而 malloc 需要显式指定内存大小。
  • new 是类型安全的,因为它知道要分配的是什么类型的对象。malloc 不了解对象的类型。
  • new 返回指向已分配内存的对象指针,而 malloc 返回 void* 指针,需要进行类型转换。

4、内存泄漏与规避方法?

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

规避方法:

  1. C++中的智能指针,如std::shared_ptrstd::unique_ptr,可以自动管理内存。它们会在不再需要时自动释放内存,从而避免了手动释放内存的错误。
  2. 创建对象时,分配资源,并在对象生命周期结束时自动释放资源。例如,使用std::fstream来打开文件,它将在退出作用域时自动关闭文件。
  3. 使用标准库容器类,如std::vectorstd::map等,它们在元素不再需要时会自动处理内存的释放。
  4. C++11引入了std::shared_ptrstd::unique_ptr的容器,如std::vector<std::shared_ptr<T>>,以便更容易管理动态分配的对象。
  5. 在使用智能指针时,小心循环引用问题。循环引用可能导致内存泄漏。为了避免这种情况,可以使用std::weak_ptr来打破引用环。
  6. 如果你使用new分配内存,则应该使用delete释放它,如果使用new[]分配数组,则应使用delete[]释放。在分配和释放内存时一定要保持一致。
  7. 使用工具如Valgrind(Linux/Unix)、Dr. Memory(Windows)、AddressSanitizer(Clang编译器)、或MemorySanitizer(Clang编译器)来检测内存泄漏。
  8. 定期进行代码审查,以寻找可能导致内存泄漏的问题。
  9. 为了调试,可以记录内存分配和释放的情况,以便更容易识别内存泄漏。

5、悬空指针与野指针?

  1. 悬空指针(Dangling Pointer):
    • 定义:悬空指针是指已经被释放的内存或对象的指针,但仍然保留了指向该内存或对象的地址。
    • 产生原因:悬空指针通常由于在指针指向的内存被释放后,未将指针重置为nullptr或其他有效值。
    • 危害:使用悬空指针可能导致读取无效内存,修改已经释放的内存,或调用已经销毁的对象的方法,从而引发崩溃或未定义的行为。
  2. 野指针(Wild Pointer):
    • 定义:野指针是指未初始化或赋值的指针,它包含一个未知的地址,通常指向内存中的随机位置。
    • 产生原因:野指针通常由于在创建指针后未初始化或在释放内存后未将指针置为nullptr
    • 危害:使用野指针可能导致程序访问随机内存,引发崩溃或未定义的行为。

所以使用指针的时候,需要注意以下几点:

  • 在创建指针后,确保初始化它,或者将其设置为nullptr
  • 在释放内存后,立即将指针置为nullptr,以避免悬空指针。
  • 使用智能指针(如std::shared_ptrstd::unique_ptr)来管理资源,从而避免手动释放内存,减少悬空指针的风险。
  • 遵循良好的内存管理实践,定期检查代码以查找并修复悬空指针和野指针问题。
  • 使用工具如Valgrind(Linux/Unix)、Dr. Memory(Windows)、或编译器的内存检测工具来检测和修复指针问题。

6、手撕冒泡排序?

思想

通过多次遍历待排序的元素,比较相邻的两个元素,如果它们的顺序不正确(例如,如果要升序排序,当前元素比下一个元素大),则交换它们的位置,直到整个序列有序。

具体来说,冒泡排序的过程如下:

  1. 从数组的第一个元素开始,比较它与下一个元素。
  2. 如果当前元素大于下一个元素(如果要升序排序),则交换它们的位置。
  3. 移动到下一个元素,重复步骤1和2,直到遍历整个数组一次。此时,最大的元素已经被推到了数组的末尾。
  4. 重复步骤1至3,但忽略已经排序好的末尾元素,继续对剩余的元素进行遍历和比较。
  5. 重复以上步骤,直到没有需要交换的元素,整个数组已经排好序。

冒泡排序的主要特点是它多次遍历数组,每次遍历都会将一个最大(或最小,根据排序顺序)的元素冒泡到正确的位置,因此称为冒泡排序。这个算法的时间复杂度为O(n^2),其中n是待排序元素的数量。尽管它不是最高效的排序算法,但它非常简单,容易理解,适用于小型数据集或已接近排序状态的数据。

参考代码

#include <iostream>
#include <vector>

void bubbleSort(std::vector<int> &arr) {
    int n = arr.size();
    bool swapped;

    for (int i = 0; i < n - 1; i++) {
        swapped = false;

        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                // 交换arr[j]和arr[j+1]
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                swapped = true;
            }
        }

        // 如果在一轮遍历中没有发生交换,说明数组已经有序,可以提前结束
        if (!swapped) {
            break;
        }
    }
}

int main() {
    std::vector<int> arr = {64, 34, 25, 12, 22, 11, 90};
    
    std::cout << "原始数组: ";
    for (int num : arr) {
        std::cout << num << " ";
    }
    
    bubbleSort(arr);

    std::cout << "\n排序后的数组: ";
    for (int num : arr) {
        std::cout << num << " ";
    }

    return 0;
}

7、说一下map?

在C++中,map内部实现了一个红黑树(红黑树是非严格平衡二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉搜索树(又名二叉查找树、二叉排序树,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值)存储的,使用中序遍历可将键值按照从小到大遍历出来。

优缺点:

  • 优点
    • 有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作。
    • 红黑树,内部实现一个红黑树使得map的很多操作在lgn的时间复杂度下就可以实现,因此效率非常的高。
  • 缺点
    • 空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间。

使用示例:

#include <iostream>
#include <map>

int main() {
    std::map<std::string, int> wordCount;
    
    // 插入键-值对
    wordCount["apple"] = 5;
    wordCount["banana"] = 3;
    
    // 查找键的值
    std::cout << "Count of 'apple': " << wordCount["apple"] << std::endl;
    
    // 遍历map
    for (const auto& pair : wordCount) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }
    
    return 0;
}

8、C++的现代特性 17和20新特性?

C++17 新特性:

  1. 结构化绑定(Structured Bindings): 允许从复杂的数据结构中轻松提取元素,例如元组、数组、结构体等。
  2. if constexpr: 引入了if constexpr语法,用于在编译时进行条件编译,实现更灵活的模板元编程。
  3. 折叠表达式(Fold Expressions): 允许将一系列操作(如折叠、拼接等)应用于参数包,使模板编程更简洁。
  4. constexpr if: 引入了constexpr if,允许根据编译时条件来选择不同的代码分支,提高编译时计算能力。
  5. 变量模板(Variable Templates): 允许将模板应用于常量值,实现通用的编程模式。
  6. 新增标准库特性: C++17引入了一些新的标准库组件,如std::optionalstd::anystd::variant等,以增强标准库的功能。
  7. 抛出表达式(Expression Evaluation Order): 规定了表达式的求值顺序,以提高代码的可预测性。

C++20 新特性:

  1. Concepts概念: 引入了概念,允许在模板编程中更好地控制参数类型,提高模板代码的可读性。
  2. 范围基于for循环(Range-Based For Loops): 支持基于范围的for循环的操作,使代码更加简洁。
  3. 协程(Coroutines): 引入了协程支持,允许编写更高效、更易读的异步代码。
  4. 三向比较(Three-Way Comparison): 改进了类型之间的比较操作,以增加可读性和性能。
  5. 模块(Modules): 引入了模块系统,以替代传统的头文件包含,提高代码组织和编译速度。
  6. 标准库改进: C++20带来了对标准库的多项增强,如std::spanstd::formatstd::stop_token等。
  7. std::chrono和std::time: 提供更精确的时间点和时间段表示,以便处理时间和日期。
  8. 其他改进: C++20还包括对语法、性能和代码可读性的多项改进,如结构化绑定的增强、范围算法的增加等。

这里给大家贴一个优秀的博客,自己可以去看看:https://blog.csdn.net/qq_41854911/article/details/119657617

9、STL是否线程安全?C++11有什么保证线程安全的特性?

STL(标准模板库)在C++中不是线程安全的。STL容器和算法通常不提供内置的线程安全机制,因此在多线程环境中使用STL容器和算法时需要采取额外的措施来确保线程安全。

  1. C++11引入了互斥量(mutex)以及其包装类std::lock_guard,它们可以用来在多线程环境中同步对共享数据的访问。通过在对STL容器和算法的访问前后使用互斥量,可以实现线程安全。
  2. C++11引入了std::atomic模板,用于实现原子操作,以确保多线程环境下对共享变量的安全操作。它包括一系列原子类型,如std::atomic、std::atomic等。
  3. C++11引入了std::condition_variable,它允许线程在等待特定条件成立时阻塞,直到其他线程满足条件并通知它。
  4. std::future 和 std::promise: 这些C++11特性用于实现异步编程,可以在多线程环境中方便地返回和获取异步操作的结果。

10、vector的底层原理和扩容机制?扩容的性能损耗怎样尽可能的减少?

底层原理:std::vector 的底层数据结构是一个连续的动态数组,元素在内存中排列成一系列的单元。这使得通过索引来访问元素非常高效,因为它可以通过内存指针和偏移量直接计算出元素的地址。

扩容机制:当std::vector的元素数量接近当前内存块的容量(由capacity()函数获取),它需要进行扩容,以分配更多的内存。通常,std::vector会将容量翻倍,以确保均摊时间复杂度仍然是常数时间。扩容时会发生以下步骤:

  • 分配新的内存块,通常是当前容量的两倍。
  • 将现有元素复制到新的内存块中。
  • 释放旧的内存块。
  • 更新begin()end()迭代器以指向新的内存块。

扩容操作是std::vector的一个性能瓶颈,因为它需要分配新内存并复制现有元素。这可能会导致分配内存和数据复制的开销。为了减少扩容时的性能损耗,可以考虑以下方法:

  • 预分配足够大的容量: 如果您事先知道std::vector将包含大量元素,可以使用reserve()函数来预先分配足够的内存。这将减少扩容的频率。
  • 避免频繁的插入和删除: 如果需要频繁插入和删除元素,而且元素数量变化较大,std::vector可能不是最佳选择。考虑使用std::liststd::deque等数据结构,它们更适合频繁的插入和删除操作。
  • 使用移动语义: 如果您需要将元素从一个std::vector移动到另一个,使用移动语义而不是复制元素,以降低性能开销。C++11引入的std::move可以帮助实现这一点。
  • 自定义内存分配策略: 如果性能至关重要,您还可以考虑自定义内存分配策略,以更精细地控制内存管理,但这通常比较复杂。

11、vector的end迭代器的指向?迭代器的失效场景?

std::vectorend() 迭代器指向容器的最后一个元素的下一个位置。具体来说,end() 迭代器指向一个虚拟的元素,这个元素位于容器的末尾,它并不包含任何有效的数据,因此不能用于访问数据。

一些迭代器的失效场景:

  1. 当向 std::vector 中添加或删除元素时,特别是在中间位置插入或删除元素,可能会导致迭代器失效。这是因为扩容或移动元素可能会改变它们的物理位置。
  2. std::vector 容量不足,需要扩容时,之前的迭代器会失效,因为新的内存块被分配并且元素被移动。
  3. 当使用 clear() 函数清空容器时,所有迭代器都会失效。
  4. 如果删除 std::vector 中的元素并且之后尝试使用指向已删除元素的迭代器,会导致未定义行为。
  5. 如果容器本身被销毁,那么任何与该容器相关的迭代器都会失效。

12、TCP的TIME_WAIT场景,RST报文?

TIME_WAIT是TCP连接的一个状态,它发生在连接被主动关闭(即主动关闭的一方先发送FIN)后,用于确保连接的完整终止和释放。

主要目的:

  1. TIME_WAIT状态允许另一端可能还有未被接收的数据发送,以确保它们被接收。
  2. 防止新建立的连接意外地接收到之前连接的残留数据,这可能会导致数据混淆或不完整。
  3. 当TIME_WAIT状态结束后,操作系统可以释放与该连接相关的资源,例如端口号等。

TIME_WAIT状态的持续时间通常是2倍的最大报文段寿命(2MSL,Maximum Segment Lifetime),它通常为1到2分钟。这样可以确保旧连接的数据完全被清理,不会影响后续连接。

RST报文是TCP中的一种控制报文,用于立即终止一个连接。

主要用途:

  1. 当TCP连接出现错误时,可以使用RST报文来立即终止连接,而不进行正常的四次握手关闭。
  2. 服务器可以使用RST报文来拒绝某个连接请求,通常因为无法处理请求或者由于安全原因。
  3. 在某些情况下,RST报文可以用于快速终止连接,而不等待正常的连接终止过程。

TIME_WAIT状态和RST报文都与TCP连接的终止过程有关,但它们具有不同的目的和用途。在终止连接时:

  • TIME_WAIT状态用于确保旧连接的数据不会与新连接混淆,同时等待任何可能未被接收的数据。
  • RST报文用于立即终止连接,通常用于错误处理或快速终止连接的情况。

13、select和epoll的区别?

selectepoll 都是用于多路复用 I/O 操作的机制,它们允许一个单独的进程或线程管理多个文件描述符(sockets、文件等)的读写操作。

  1. select
  • 跨平台,几乎在所有主要操作系统上都可用,包括Linux和Windows系统。
  • 在处理大量文件描述符时效率较低,因为它使用线性扫描来查找可读或可写的文件描述符,这意味着它的时间复杂度为 O(n),其中 n 是文件描述符的数量。
  • 通常限制了文件描述符的数量,因此在处理大量并发连接时可能会遇到限制。
  • 不会告诉你文件描述符的状态发生了什么改变,它只会告诉你哪些文件描述符当前可读或可写。这可能需要额外的逻辑来追踪状态改变。
  • 可以指定超时时间,允许在一段时间内等待事件,然后进行处理。
  1. epoll
  • epoll 是Linux特有的多路复用机制,不适用于所有操作系统。
  • 使用了回调机制,只有就绪的文件描述符才会触发回调,因此它在处理大量文件描述符时效率更高,时间复杂度为 O(1)。
  • 没有文件描述符数量的硬限制,允许管理大量的并发连接。
  • 提供了更详细的事件通知,包括文件描述符的连接建立、数据可读、数据可写等,而不仅仅是文件描述符的可读或可写。
  • 适用于高性能服务器,它能够轻松处理大量的并发连接。

其余项目及实习经历不在这儿赘述。

来源:https://www.nowcoder.com/feed/main/detail/efb0a2d156e84784b541ff713b7cbe1c

  • 27
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值