大家好,我是阿Q。
今天是个好日子,因为今天是周五哈哈哈,上了七天班终于休息啦!
最近私我的小伙伴还挺多,有的还没来得及拉进群,不要介意哈,后边会有知识星球拉大家进去。
今天给大家分享一位同学面试同程旅行的面试题。
来源:
https://www.nowcoder.com/feed/main/detail/efb0a2d156e84784b541ff713b7cbe1c
1、sizeof与strlen的区别?
sizeof是C/C++中的操作符,用于获取一个数据类型或变量所占用的字节数。它是在编译时计算的,返回的是数据类型或变量的字节数,而不是字符数。主要用于静态分配内存和获取数据类型的大小。 strlen是C/C++中的函数,用于获取一个以null终止字符数组(C字符串)的长度,即字符数。它是在运行时计算的,遍历字符数组直到遇到null终止符。主要用于计算C字符串的长度。
int arr[5];size_t size = sizeof(arr); // size = 20 (5 * 4 字节)
const char* str = "Hello, World!";size_t length = strlen(str); // length = 13
sizeof通常用于获取数据类型的大小,而
strlen用于计算C字符串的长度。
运算符 运算符是C++中用于执行各种操作的特殊符号或关键字。它们可以用于操作各种数据类型,包括算术运算、逻辑运算、比较运算、位运算等。 可以分为一元运算符(作用在一个操作数上,例如: ++
、--
)、二元运算符(作用在两个操作数上,例如:+
、-
)以及三元运算符(例如:条件运算符? :
)。过载运算符:C++允许用户自定义类的运算符重载,这允许你自定义如何处理类对象的运算。 它是编译器内置的一部分,它们具有特定的语法和预定义的行为,这意味着它们通常更高效。 函数 函数是C++中的自包含代码单元,它们封装了一系列操作,可以在需要的时候多次调用。 函数的作用是执行特定的任务或操作,可以接受参数(输入)并返回结果(输出)。 函数的参数和返回类型可以是各种数据类型,包括用户自定义类型。 主要优势在于它们提供了代码重用的机制,允许将代码划分为小的可管理单元。 C++中也有运算符重载的概念,这允许用户自定义运算符的行为,但与运算符重载不同,它需要使用函数来实现。
运算符是用于执行操作的特殊符号,而函数是自包含代码单元,用于执行一系列操作。 运算符通常与基本数据类型一起使用,而函数可以用于各种操作,包括处理复杂的数据结构和对象。 运算符具有内置的语法和行为,而函数的行为需要在函数体内定义。 运算符重载是C++中的一个特性,允许用户自定义运算符的行为,而函数是C++中的基本构建块之一,用于组织和执行代码。
new
new
是C++中的运算符,而不是函数。它使用对象的构造函数来分配内存。当使用 new
分配内存时,它会执行以下操作:分配足够的内存以容纳对象或对象数组。 调用对象的构造函数来初始化内存中的对象。 返回指向已分配内存的指针。 new
不需要显式指定分配的内存大小,因为它会自动计算对象或数组的大小。
malloc
malloc
是C语言标准库函数,也可以在C++中使用。它不执行对象的构造函数。当使用 malloc
分配内存时,它会执行以下操作:分配指定大小的内存块。 返回指向已分配内存的指针。 malloc
需要显式指定分配的内存大小,通常使用sizeof
运算符来计算对象或数组的大小。
new
会调用对象的构造函数来初始化内存中的对象,而malloc
不会。这使得new
更适合用于C++类对象。new
不需要显式指定内存大小,而malloc
需要显式指定内存大小。new
是类型安全的,因为它知道要分配的是什么类型的对象。malloc
不了解对象的类型。new
返回指向已分配内存的对象指针,而malloc
返回void*
指针,需要进行类型转换。
C++中的智能指针,如 std::shared_ptr
和std::unique_ptr
,可以自动管理内存。它们会在不再需要时自动释放内存,从而避免了手动释放内存的错误。创建对象时,分配资源,并在对象生命周期结束时自动释放资源。例如,使用 std::fstream
来打开文件,它将在退出作用域时自动关闭文件。使用标准库容器类,如 std::vector
、std::map
等,它们在元素不再需要时会自动处理内存的释放。C++11引入了 std::shared_ptr
和std::unique_ptr
的容器,如std::vector<std::shared_ptr<T>>
,以便更容易管理动态分配的对象。在使用智能指针时,小心循环引用问题。循环引用可能导致内存泄漏。为了避免这种情况,可以使用 std::weak_ptr
来打破引用环。如果你使用 new
分配内存,则应该使用delete
释放它,如果使用new[]
分配数组,则应使用delete[]
释放。在分配和释放内存时一定要保持一致。使用工具如Valgrind(Linux/Unix)、Dr. Memory(Windows)、AddressSanitizer(Clang编译器)、或MemorySanitizer(Clang编译器)来检测内存泄漏。 定期进行代码审查,以寻找可能导致内存泄漏的问题。 为了调试,可以记录内存分配和释放的情况,以便更容易识别内存泄漏。
悬空指针(Dangling Pointer): 定义:悬空指针是指已经被释放的内存或对象的指针,但仍然保留了指向该内存或对象的地址。 产生原因:悬空指针通常由于在指针指向的内存被释放后,未将指针重置为 nullptr
或其他有效值。危害:使用悬空指针可能导致读取无效内存,修改已经释放的内存,或调用已经销毁的对象的方法,从而引发崩溃或未定义的行为。 野指针(Wild Pointer): 定义:野指针是指未初始化或赋值的指针,它包含一个未知的地址,通常指向内存中的随机位置。 产生原因:野指针通常由于在创建指针后未初始化或在释放内存后未将指针置为 nullptr
。危害:使用野指针可能导致程序访问随机内存,引发崩溃或未定义的行为。
在创建指针后,确保初始化它,或者将其设置为 nullptr
。在释放内存后,立即将指针置为 nullptr
,以避免悬空指针。使用智能指针(如 std::shared_ptr
和std::unique_ptr
)来管理资源,从而避免手动释放内存,减少悬空指针的风险。遵循良好的内存管理实践,定期检查代码以查找并修复悬空指针和野指针问题。 使用工具如Valgrind(Linux/Unix)、Dr. Memory(Windows)、或编译器的内存检测工具来检测和修复指针问题。
思想
从数组的第一个元素开始,比较它与下一个元素。 如果当前元素大于下一个元素(如果要升序排序),则交换它们的位置。 移动到下一个元素,重复步骤1和2,直到遍历整个数组一次。此时,最大的元素已经被推到了数组的末尾。 重复步骤1至3,但忽略已经排序好的末尾元素,继续对剩余的元素进行遍历和比较。 重复以上步骤,直到没有需要交换的元素,整个数组已经排好序。
参考代码
#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;}
优点 有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作。 红黑树,内部实现一个红黑树使得map的很多操作在lgn的时间复杂度下就可以实现,因此效率非常的高。 缺点
#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;// 遍历mapfor (const auto& pair : wordCount) {std::cout << pair.first << ": " << pair.second << std::endl;}return 0;}
结构化绑定(Structured Bindings):允许从复杂的数据结构中轻松提取元素,例如元组、数组、结构体等。 if constexpr:引入了 if constexpr
语法,用于在编译时进行条件编译,实现更灵活的模板元编程。折叠表达式(Fold Expressions):允许将一系列操作(如折叠、拼接等)应用于参数包,使模板编程更简洁。 constexpr if:引入了 constexpr if
,允许根据编译时条件来选择不同的代码分支,提高编译时计算能力。变量模板(Variable Templates):允许将模板应用于常量值,实现通用的编程模式。 新增标准库特性:C++17引入了一些新的标准库组件,如 std::optional
、std::any
、std::variant
等,以增强标准库的功能。抛出表达式(Expression Evaluation Order):规定了表达式的求值顺序,以提高代码的可预测性。
Concepts概念:引入了概念,允许在模板编程中更好地控制参数类型,提高模板代码的可读性。 范围基于for循环(Range-Based For Loops):支持基于范围的for循环的操作,使代码更加简洁。 协程(Coroutines):引入了协程支持,允许编写更高效、更易读的异步代码。 三向比较(Three-Way Comparison):改进了类型之间的比较操作,以增加可读性和性能。 模块(Modules):引入了模块系统,以替代传统的头文件包含,提高代码组织和编译速度。 标准库改进:C++20带来了对标准库的多项增强,如 std::span
、std::format
、std::stop_token
等。std::chrono和std::time:提供更精确的时间点和时间段表示,以便处理时间和日期。 其他改进:C++20还包括对语法、性能和代码可读性的多项改进,如结构化绑定的增强、范围算法的增加等。
这里给大家贴一个优秀的博客,自己可以去看看: https://blog.csdn.net/qq_41854911/article/details/119657617
9、STL是否线程安全?C++11有什么保证线程安全的特性?
C++11引入了互斥量(mutex)以及其包装类std::lock_guard,它们可以用来在多线程环境中同步对共享数据的访问。通过在对STL容器和算法的访问前后使用互斥量,可以实现线程安全。 C++11引入了std::atomic模板,用于实现原子操作,以确保多线程环境下对共享变量的安全操作。它包括一系列原子类型,如std::atomic<int>、std::atomic<bool>等。 C++11引入了std::condition_variable,它允许线程在等待特定条件成立时阻塞,直到其他线程满足条件并通知它。 std::future 和 std::promise:这些C++11特性用于实现异步编程,可以在多线程环境中方便地返回和获取异步操作的结果。
std::vector的底层数据结构是一个连续的动态数组,元素在内存中排列成一系列的单元。这使得通过索引来访问元素非常高效,因为它可以通过内存指针和偏移量直接计算出元素的地址。
std::vector的元素数量接近当前内存块的容量(由
capacity()函数获取),它需要进行扩容,以分配更多的内存。通常,
std::vector会将容量翻倍,以确保均摊时间复杂度仍然是常数时间。扩容时会发生以下步骤:
分配新的内存块,通常是当前容量的两倍。 将现有元素复制到新的内存块中。 释放旧的内存块。 更新 begin()
和end()
迭代器以指向新的内存块。
std::vector的一个性能瓶颈,因为它需要分配新内存并复制现有元素。这可能会导致分配内存和数据复制的开销。为了减少扩容时的性能损耗,可以考虑以下方法:
预分配足够大的容量:如果您事先知道 std::vector
将包含大量元素,可以使用reserve()
函数来预先分配足够的内存。这将减少扩容的频率。避免频繁的插入和删除:如果需要频繁插入和删除元素,而且元素数量变化较大, std::vector
可能不是最佳选择。考虑使用std::list
或std::deque
等数据结构,它们更适合频繁的插入和删除操作。使用移动语义:如果您需要将元素从一个 std::vector
移动到另一个,使用移动语义而不是复制元素,以降低性能开销。C++11引入的std::move
可以帮助实现这一点。自定义内存分配策略:如果性能至关重要,您还可以考虑自定义内存分配策略,以更精细地控制内存管理,但这通常比较复杂。
std::vector的
end()迭代器指向容器的最后一个元素的下一个位置。具体来说,
end()迭代器指向一个虚拟的元素,这个元素位于容器的末尾,它并不包含任何有效的数据,因此不能用于访问数据。
当向 std::vector
中添加或删除元素时,特别是在中间位置插入或删除元素,可能会导致迭代器失效。这是因为扩容或移动元素可能会改变它们的物理位置。当 std::vector
容量不足,需要扩容时,之前的迭代器会失效,因为新的内存块被分配并且元素被移动。当使用 clear()
函数清空容器时,所有迭代器都会失效。如果删除 std::vector
中的元素并且之后尝试使用指向已删除元素的迭代器,会导致未定义行为。如果容器本身被销毁,那么任何与该容器相关的迭代器都会失效。
TIME_WAIT状态允许另一端可能还有未被接收的数据发送,以确保它们被接收。 防止新建立的连接意外地接收到之前连接的残留数据,这可能会导致数据混淆或不完整。 当TIME_WAIT状态结束后,操作系统可以释放与该连接相关的资源,例如端口号等。
当TCP连接出现错误时,可以使用RST报文来立即终止连接,而不进行正常的四次握手关闭。 服务器可以使用RST报文来拒绝某个连接请求,通常因为无法处理请求或者由于安全原因。 在某些情况下,RST报文可以用于快速终止连接,而不等待正常的连接终止过程。
TIME_WAIT状态用于确保旧连接的数据不会与新连接混淆,同时等待任何可能未被接收的数据。 RST报文用于立即终止连接,通常用于错误处理或快速终止连接的情况。
select和
epoll都是用于多路复用 I/O 操作的机制,它们允许一个单独的进程或线程管理多个文件描述符(sockets、文件等)的读写操作。
select
跨平台,几乎在所有主要操作系统上都可用,包括Linux和Windows系统。 在处理大量文件描述符时效率较低,因为它使用线性扫描来查找可读或可写的文件描述符,这意味着它的时间复杂度为 O(n),其中 n 是文件描述符的数量。 通常限制了文件描述符的数量,因此在处理大量并发连接时可能会遇到限制。 不会告诉你文件描述符的状态发生了什么改变,它只会告诉你哪些文件描述符当前可读或可写。这可能需要额外的逻辑来追踪状态改变。 可以指定超时时间,允许在一段时间内等待事件,然后进行处理。
epoll
epoll
是Linux特有的多路复用机制,不适用于所有操作系统。使用了回调机制,只有就绪的文件描述符才会触发回调,因此它在处理大量文件描述符时效率更高,时间复杂度为 O(1)。 没有文件描述符数量的硬限制,允许管理大量的并发连接。 提供了更详细的事件通知,包括文件描述符的连接建立、数据可读、数据可写等,而不仅仅是文件描述符的可读或可写。 适用于高性能服务器,它能够轻松处理大量的并发连接。
好了,就这么多,想加入知识星球的小伙伴们赶紧去主页找我啦,占个免费的名额。
最后大家周末愉快~
文章转载自阿Q正砖,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




