大厂C++面试---同城旅行

借鉴链接:https://mp.weixin.qq.com/s/l8ajchkWzvGOOzqRFdQMrA

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

int arr[5];
size_t size = sizeof(arr); // size = 20 (5 * 4 字节)

1.2 strlen是C/C++中的函数,用于获取一个以null为终止字符数组(C字符串)的长度,即字符数。它是在运行时计算的,遍历字符数组直到遇到null终止符。主要用于计算C字符串的长度。

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

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

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

3、new和malloc?
3.1 new
a. new 是C++中的运算符,而不是函数。它使用对象的构造函数来分配内存。
b. 当使用 new 分配内存时,它会执行以下操作:
1)分配足够的内存以容纳对象或对象数组;
2)调用对象的构造函数来初始化内存中的对象;
3)返回指向已分配内存的指针。
c. new 不需要显式指定分配的内存大小,因为它会自动计算对象或数组的大小
3.2 malloc
a. malloc 是C语言标准库函数,也可以在C++中使用。它不执行对象的构造函数;
b. 当使用 malloc 分配内存时,它会执行以下操作:
1)分配指定大小的内存块;
2)返回指向已分配内存的指针。
c. malloc 需要显式指定分配的内存大小,通常使用 sizeof 运算符来计算对象或数组的大小。
3.3 区别
1)new 会调用对象的构造函数来初始化内存中的对象,而 malloc 不会。这使得 new 更适合用于C++类对象。
2)new 不需要显式指定内存大小,而 malloc 需要显式指定内存大小。
3)new 是类型安全的,因为它知道要分配的是什么类型的对象。malloc 不了解对象的类型。
4)new 返回指向已分配内存的对象指针,而 malloc 返回 void* 指针,需要进行类型转换。

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

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

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

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

6、手撕冒泡排序
6.1 思路
通过多次遍历待排序的元素,比较相邻的两个元素,如果它们的顺序不正确(例如,如果要升序排序,当前元素比下一个元素大),则交换它们的位置,直到整个序列有序。
具体来说,冒泡排序的过程如下:
a. 从数组的第一个元素开始,比较它与下一个元素。
b. 如果当前元素大于下一个元素(如果要升序排序),则交换它们的位置。
c. 移动到下一个元素,重复步骤1和2,直到遍历整个数组一次。此时,最大的元素已经被推到了数组的末尾。
d. 重复步骤a至c,但忽略已经排序好的末尾元素,继续对剩余的元素进行遍历和比较。
e. 重复以上步骤,直到没有需要交换的元素,整个数组已经排好序。

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

#include 
#include 

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中的元素是按照二叉搜索树(又名二叉查找树、二叉排序树,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值)存储的,使用中序遍历可将键值按照从小到大遍历出来

优缺点:
优点

a有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作。
b)红黑树,内部实现一个红黑树使得map的很多操作在lgn的时间复杂度下就可以实现,因此效率非常的高

缺点
空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间。

使用示例:

#include 
#include 

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、STL是否线程安全?C++11有什么保证线程安全的特性?

STL(标准模板库)在C++中不是线程安全的。STL容器和算法通常不提供内置的线程安全机制,因此在多线程环境中使用STL容器和算法时需要采取额外的措施来确保线程安全。
a)C++11引入了互斥量mutex)以及其包装类std::lock_guard,它们可以用来在多线程环境中同步对共享数据的访问。通过在对STL容器和算法的访问前后使用互斥量,可以实现线程安全。
b)C++11引入了std::atomic模板,用于实现原子操作,以确保多线程环境下对共享变量的安全操作。它包括一系列原子类型,如std::atomic、std::atomic等。
c)C++11引入了std::condition_variable,它允许线程在等待特定条件成立时阻塞,直到其他线程满足条件并通知它。
dstd::future 和 std::promise
这些C++11特性用于实现异步编程,可以在多线程环境中方便地返回和获取异步操作的结果。

你可能感兴趣的:(C++学习,c++,面试)