面经总结|秋招—— C++ 篇(二)|2021 |
24612
2020.12.01
2020.12.05
发布于 未知归属地

0f3c2d459d07e245b4439a8ce2159099.png

写在前面

本篇是即 上一篇面经 之后的第二篇面经,涉及 C++ 在面试过程中常问道的知识点,也是笔者在面试过程中曾经被问到的题目,但是由于公司的不同,面试官的不同,问道的问题的深度也不一样,建议在复习基础知识的时候,首先拓展自己的宽度,当宽度足够了再加深自己的深度。举个非常简单的例子:面试官可能会问道多态是怎么实现的,开始复习的时候可能仅能回答上来“多态是通过虚函数来实现的”;那么更为理想的答案是在回答的过程中我们也简单把“虚函数的底层实现方式”简单描述下,这就体现出我们回答的问题是有深度的。

更新目录及链接:

🔗 2021 秋招面经总结 —— C++ 篇(一)
🔗 2021 秋招面经总结 —— C++ 篇(二)


(11)计算下面几个类的大小:

参考链接:🔗 这里

类本身没有大小,这里类的大小是指:类对象所占的大小。

  • 类的大小遵循结构体的对齐规则
  • 类的大小与普通成员函数和静态成员无关(包括:普通成员函数、静态成员函数、静态数据成员、静态常量数据成员),与普通数据成员有关
  • 虚函数对类的大小有影响,是因为虚函数指针的影响
  • 虚继承对类的大小有影响,是因为虚基表指针带来的影响
  • 空类的大小是 1
#include <bits/stdc++.h>
using namespace std;

class A{
    //静态成员
    static int a;
    const static int b;    
    static int fun1(){}

    //普通成员函数
    void fun(){}
};

//普通数据成员
class B{
    int a;    
};

//虚函数
class C{
    virtual int fun(){}
};

//结构体对齐原则
class D{
    int a;
    virtual int fun(){}
};
int main(){
    cout << sizeof(A) << endl;//1
    cout << sizeof(B) << endl;//4:表示一个整型变量的大小
    cout << sizeof(C) << endl;//8:虚函数表的指针的大小
    cout << sizeof(D) << endl;//16:整型变量的大小+虚函数指针的大小+对齐原则(4)
    return 0;
}

(12)给一个代码,求输出结果

class A
{
public:
	A(int x){}
};

问:A a = 1;是否正确, 如果正确, 那么它调用了哪些函数?,
这里会进行隐式转化 A a(1).implicit
正确

这类题目更常见的是在基类和子类有不同实现方法。(虚函数相关,例子很多,不多说了)

(13)C++的STL介绍

(这个系列也很重要,建议侯捷老师的这方面的书籍与视频),其中包括内存管理allocator,函数,实现机理,多线程实现等,笔者也整理过STL相关的面经,有兴趣的可以自行查阅

(14)STL 源码中的 hash 表的实现

hashtable 是采用开链法来完成的,(vector + list)

  • 底层键值序列采用 vector 实现,vector 的大小取的是质数,且相邻质数的大小约为 2 倍关系,当创建 hashtable 时,会自动选取一个接近所创建大小的质数作为当前 hashtable 的大小;
  • 对应键的值序列采用单向 list 实现;
  • 当 hashtable 的键 vector 的大小重新分配的时候,原键的值 list 也会重新分配,因为 vector 重建了相当于键增加了,那么原来的值对应的键可能就不同于原来分配的键,这样就需要重新确定值的键。

(15)STL 中 unordered_map 和 map 的区别

  • 底层实现不同:
    unordered_map 底层实现是一个哈希表,元素无序
    map 底层实现是红黑树,其内部所有的元素都是有序的,因此对 map 的所有操作,其实都是对红黑树的操作
  • 优缺点:
    unordered_map:查找效率高;但是建立哈希表比较耗费时间
    map:内部元素有序,查找和删除操作都是 logn 的时间复杂度;但是维护红黑树的存储结构需要占用一定的内存空间
  • 适用情况:
    对于要求内部元素有序的使用 map,对于要求查找效率的用 unordered_map

(16)STL 中 vector 的实现

vector 是一个动态数组,底层实现是一段连续的线性内存空间。
扩容的本质:当 vector 实际所占用的内存空间和容量相等时,如果再往其中添加元素需要进行扩容。其步骤如下:

  • 首先,申请一块更大的存储空间,一般是增加当前容量的 50% 或者 100%,和编译器有关;
  • 然后,将旧内存空间的内容,按照原来的顺序放到新的空间中
  • 最后,将旧内存空间的内容释放掉,本质上其存储空间不会释放,只是删除了里面的内容。

从 vector 扩容的原理也可以看出:vector 容器释放后,与其相关的指针、引用以及迭代器会失效的原因。

(17)vector 使用的注意点及其原因,频繁对 vector 调用 push_back() 对性能的影响和原因

主要是在插入元素方面:插入元素需要考虑元素的移动问题和是否需要扩容的问题
频繁的调用 push_back() 也是扩容的问题对性能的影响

(18)C++ 中的重载和重写(覆盖)的区别:

  • 对于类中函数的重载或者重写而言,重载发生在同一个类的内部,重写发生在不同的类之间,子类和父类之间
  • 重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有 virtual 修饰
  • virtual 关键字:重写的函数基类中必须有 virtual 关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有

(19)C++ 内存管理(热门问题,问过)

参考:🔗 这里
C++ 内存分区:栈、堆、自由存储区、全局/静态存储区、常量区

  • 栈:存放函数的局部变量,由编译器自动分配和释放
  • 堆:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收
  • 自由存储区:和堆十分相似,存放由 new 分配的内存块,由 delete 释放内存
  • 全局区/静态区:存放全局变量和静态变量
  • 常量存储区:存放的是常量,不允许修改

堆和自由存储区的区别:
参考链接:🔗 这里

  • 自由存储是 C++ 中通过 new 与 delete 动态分配和释放对象的抽象概念,而堆是 C 语言和操作系统的术语,是操作系统维护的一块动态分配内存
  • new 所申请的内存区域在 C++ 中成为自由存储区。藉由堆实现的自由存储,可以说 new 所申请的内存区域在堆上
  • 堆和自由存储区有区别,并非等价。使用 new 来分配内存,程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。

指针和数组的对比:

  1. 字符数组的内容可以改变,字符指针的内容不可以改变
char a[] = "hello";
a[0] = 'l';
cout << a << endl;
char *p = "world";//p指向常量字符串
p[0] = 'h'; //运行出错
  1. 字符数组之间不能赋值,不能直接用比较运算符直接比较,若进行赋值调用 strcpy() 函数,若进行比较调用 strcmp() 函数;
char a[] = "hello";
char b[10];
strcpy(b, a);//不能用b = a;
if(strcmp(a, b) == 0)//不能用a==b
	cout << "endl";
  1. 运算符 sizeof 可以计算出字符数组的容量,但是计算字符指针时,得到的是指针变量所占用的空间,而不是指针所指向空间的大小。
char a[] = "hello";
char *p = a;
cout << sizeof a << endl;//6字节
cout << sizeof p << endl;//8字节(64位)

但是当数组作为参数传递时,数组会自动退化为指针

void fun(char a[100]){
	cout << sizeof a << endl;//结果是8(64位)
}

调用函数申请空间

  1. 指针作为函数的参数传递:
void GetMemory(char *p, int num)
{
 p = (char *)malloc(sizeof(char) * num);
}

void Test(void)
{
 char *str = NULL;
 GetMemory(str, 100); // str 仍然为 NULL
 strcpy(str, "hello"); // 运行错误
}

上述程序出错的原因在于:调用函数时,传递的是变量的值“值传递”,在 GetMemory 函数内部改变的是参数 p 的副本,并不会影响参数 p 本身,也就是说 str 并没有获得内存。这样程序最终会导致内存泄漏,调用 GetMemory 函数多少次,就会出现多少次内存泄漏,因为在函数内部只动态申请了内存,并没有用 free 释放掉动态内存空间。

如果想要通过这种方式申请空间,将 GetMemory 函数的形参改为“指向指针的指针”

void GetMemory2(char **p, int num)
{
 *p = (char *)malloc(sizeof(char) * num);
}
void Test2(void)
{
 char *str = NULL;
 GetMemory2(&str, 100); // 注意参数是 &str,而不是str
 strcpy(str, "hello");
 cout<< str << endl;
 free(str);//特别注意:这里是将动态申请的内存空间释放掉
}
  1. 用函数的返回值来传递动态申请的内存空间
char *GetString(void)
{
 char p[] = "hello world";
 return p; // 编译器将提出警告:将局部变量返回
}
void Test4(void)
{
 char *str = NULL;
 str = GetString(); // str 的内容是垃圾
 cout<< str << endl;
}
char *GetString2(void)
{
 char *p = "hello world";
 return p;//编译器给出警告:将string const转化成 char*
}
void Test5(void)
{
 char *str = NULL;
 str = GetString2();
 cout<< str << endl;
}

在一个函数中如何返回一个变量的指针?
对于普通变量而言,是存储在栈内存中,当函数调用完毕后,栈内存的空间会被释放,如果返回局部变量的指针,回到主调函数后,该指针是悬挂指针指向垃圾内存,如何来处理这种情况呢?

可以返回存储在堆内存或者全局区的变量的指针:

  • 返回静态类型对象的指针,会导致占用大量的内存空间
  • 返回存放在堆上的对象的指针,该对象所在的内存空间由用户控制什么时候释放,那如何知道什么时候释放合适呢?可以用只智能指针来处理,智能指针内部有计数器,当计数器为 0 时,会自动释放该内存。
#include <iostream>
using namespace std;
 
int func1(int param)
{
 int local_param = param;
 
 return local_param;
}

//返回静态变量的地址 
int* func2(int param)
{
 static int local_param = param;
 
 return &local_param;
}
 
//返回存放在堆上变量的地址* 
int *func3(int param)
{
 int *new_param = new int(param);
 
 return new_param;
}
 
 
int main()
{
 int *pNewVar = func3(3);
 int *pVar = func2(2);
 
 cout << *pVar << endl;//2
 cout << *pNewVar << endl;//3
 cout << func1(1) << endl;
 
 return 0;
}

出现野指针的情形:

  1. 指针定义的时候未初始化
  2. 指针指向动态分配的内存空间在释放(delete 或 free)后,未置为 NULL,让人误以为是合法指针
  3. 指针操作超过了变量的作用范围。例如:在函数中将一个局部变量的地址作为函数的返回值,这里编译器会给出警告,因为离开该函数后,局部变量的空间就会释放掉,返回的地址(指针)相当于是野指针。

深拷贝和浅拷贝带来的问题:
类中默认的拷贝构造函数和赋值构造函数都是浅拷贝,当类的成员变量中出现指针变量时,最好使用深拷贝,避免内存空间多次释放的问题出现。

内存泄漏:
内存泄漏常指的是堆内存泄漏,当然还包括系统资源的泄漏

参考链接:🔗 这里
野指针: 指针指向的内存空间已经释放掉
悬挂指针: 指针指向的内存空间由于中间改变了其指向,之前的内存空间已无法释放,个人认为是和内存泄漏。
感觉二者并没有什么区别!!!!
野指针和悬挂指针都是指向垃圾内存的


写在最后

本篇中知识点小结:

  • 类大小的计算
  • STL 相关知识点:hash 表的实现原理,vector 的实现原理,vector 使用注意的地方,unordered_map 和 map 的区别
  • 重载和重写的区别
  • 内存管理:内存分区,堆和自由存储区的区别,指针和数组的比较,内存泄漏深拷贝和浅拷贝,悬挂指针和野指针
    本篇中涉及到一些非常细节的知识点,建议自己写些程序测试下,有疑问的地方欢迎大家来评论区讨论,这是笔者自己整理的面经,难免有疏忽的地方,若是哪里不正确欢迎大家指正,作为鼓励大家记得点赞收藏哦。。。后续笔者会继续整理 C++ 方面的面经,敬请关注~~~~~
评论 (6)