C++基础面经

C++基础面经

面向对象相关

1. 面向对象是什么?

  • 面向对象是一种编程思想,把一切东西看作一个个对象,每个对象有他们各自的属性。
  • 把这些对象拥有的属性和操作这些属性的函数打包成一个类来表示。.

2. 面向对象的三大特征是什么?

  • 面向对象的三大特征是继承,封装,多态。
  • 继承,可以使用现有的类的所有功能,在无需重新编写原来代码的基础上进行扩展。
  • 封装,将属性和操作属性的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
  • 多态,用父类型别的指针指向子类的实例,通过父类的指针调用子类的成员函数。

3. 重载和重写的区别是什么?

  • 重写,是指派生类中存在重新定义的函数。函数名,参数列表,返回值类型都和基类中的函数一样。基类中的被重写函数需要有 virtual 修饰。
  • 重载,是指声明多个多个具有不同参数列表的同名函数,根据参数列表决定调用哪个函数。

4. 重载和重写是怎么实现的?

  • 对于重载,在预编译阶段,可以通过命名倾轧来修改函数名,以区分参数列表不同的同名函数。
  • 对于重写,通过在基类的函数前加上 virtual 关键字,运行时会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

5. 构造函数的种类有哪些?

  • 默认构造函数和初始化构造函数,在定义类的对象的时候,完成对象的初始化工作。
  • 拷贝构造函数,将已存在对象的值,赋给当前的对象。
  • 移动构造函数,使用右值引用作为参数,将一个对象的资源更高效的转移到另一个对象上,避免不必要的资源拷贝。

6. 定义一个空类时,默认生成哪些函数?

  • 无参构造函数,定义类的时候完成初始化的操作。
  • 拷贝构造函数,将已存在对象的值,赋给当前的对象。
  • 赋值运算符。
  • 析构函数。

7. 类对象初始化的顺序是什么?

  • 父类构造函数–>成员类对象构造函数–>自身构造函数。

8. 向下转型和向上转型是什么?

  • 向下转型,父类转换为子类,可以采用强制转换,但是不安全。
  • 向上转型,子类转换为父类,使用 dynamic_cast(expression),这种转换比较安全。

9. 深拷贝和浅拷贝是什么?

  • 浅拷贝为值拷贝,将源对象的值拷贝到目标对象中去。本质上源对象和目标对象共用一份实体,只是所引用的变量名不同,地址还是相同的。
  • 深拷贝为开辟一片新的空间,将源对象中的值拷贝到目的对象中去,这两个指针指向了不同的内存位置。深拷贝通过拷贝构造函数和赋值运算符重载实现。

10. 模板类是在什么时候实现的?

  • 模板的实例化分为显示实例化和隐式实例化,前者是告诉模板应该使用什么样的类型去生成具体的函数,后者是编译器在编译的过程中去决定用什么类型实例化一个模板。
  • 模板的具体化,当函数模板使用某种类型实例化后的类或函数不满足要求时,可以考虑对模板进行具体化,具体化可以修改模板的定义,当遇到需要具体化的类型时,按照自己的需求处理。

11. 类继承时,不同关键字的访问权限?

  • 类中的成员可以分为三种类型,public,protected,public。
  • private继承,子类对象不可以访问父类的任何成员。
  • public继承,子类对象可以访问父类的 public,但不能访问基类中的private和protected成员。
  • protect继承,子类可以访问父类的 public,protected,但不能访问基类中的private成员。

12. 类内可以定义引用数据成员嘛?

  • 可以,但是需要遵循三个原则:
  • 不能使用默认构造函数进行初始化,必须提供构造函数来进行初始化。
  • 提供构造函数的形参必须也是引用类型。
  • 不能在构造函数体中初始化,必须在初始化列表中初始化。

13. 构造函数为什么不能声明为虚函数?

  • 虚函数主要用于在信息不全的情况下,可以使重载的函数得到对应的调用,构造函数本身就是要初始化实例,那么虚函数就没有意义。
  • 此外虚函数是在构造函数之后才建立的,所以构造函数不可能称为虚函数。

1. main 函数执行之前和之后的会执行哪些代码?

main 函数执行之前,主要就是初始化系统相关资源:

  • 设置栈指针。
  • 初始化静态变量和全局变量,即 data 段中的内容。
  • 将未初始化的的全局变量赋值,即 bss 段中的内容。
  • 全局对象初始化,在 main 之前调用构造函数。
  • 将 main 函数的参数 argc,argv 传递给 main 函数,开始执行 main 函数。
  • attribute((constructor))

main 函数执行之后:

  • 全局对象的析构会在 main 函数之后进行。
  • 使用 atexit 注册一个函数,会在 main 之后执行。
  • attribute((destructor))

2. 结构体内存对齐的问题?

内存对齐是为了增加CPU访问数据的效率,减少读取数据的次数。

  • 结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同。
  • 第一个成员的大小需要是对齐值的整数倍,对齐后整体的大小也需要是对齐值的整数倍,对齐值为按结构体成员中最大的 size。
  • 可以通过 alignof 计算出类型的对齐方式,alignas 指定结构体的对齐值。

4. 函数传参时,什么时候该用指针,什么时候该用引用?

  • 当需要返回函数内局部变量的内存的时候用指针。
  • 对栈空间比较敏感的时候用引用,比如说递归函数。不需要创建临时变量,开销小。
  • 类作为参数传递时时候用引用,这是C++类标准的传递方式。

6. 区分下列指针?

  • int *p[10]:指针数组,该数组大小为10,每个位置存放了一个指针。
  • int (*p)[10]:数组指针,该指针指向了一个数组,该数组大小为10。
  • int *p(int):函数声明,函数名为p,参数为int,返回值为 int * 指针。
  • int (*p)(int):函数指针,该指针指向一个函数,函数的参数为int。

8. new 和 delete 如何实现?

  • new的实现过程是:首先调用名为operator new的标准库函数,分配足够大的原始内存,以保存指定类型的一个对象;接下来运行该类型的一个构造函数,初始化该构造对象;最后返回构造后的的对象的指针。
  • delete的实现过程:对指针指向的对象调用析构函数;然后通过名为operator delete的标准库函数释放该对象所用内存。

指针,引用

指针和引用的区别?

  • 指针是一个变量,存储的是一个地址,引用是原来的变量的一个别名。
  • 指针可以为空,引用不能为NULL且在定义时必须初始化。
  • 指针在初始化后可以改变指向,而引用在初始化之后不可再改变。
  • sizeof指针得到的是本指针的大小,sizeof引用得到的是所引用变量的大小。
  • 当作为参数传递时,传入指针相当于拷贝一个新的指针指向原来的地址,而引用是直接对所引对象的地址进行操作。

堆,栈

堆和栈的区别?

  • 管理方式不同:堆由程序员控制,容易产生内存泄漏。栈由系统自动管理。
  • 空间大小不同:堆是由低向高扩展,是不连续的内存区域,大小可以灵活调整。栈是由高向低扩展,是连续的内存空间,大小固定。
  • 碎片问题不同:堆中频繁使用 new/delete 会造成大量碎片,降低效率。栈的进出一一对应,不会产生碎片,效率高。
  • 分配效率不同:堆由C++函数库提供,机制比较复杂效率低。栈由系统底层提供,有专门的寄存器存放栈指针,效率高。

malloc、new

malloc 和 new 的区别?

  • malloc 和 free是标准库函数,支持覆盖;new 和 delete 是运算符,支持重载。
  • malloc 仅仅分配内存空间,free 仅仅回收空间,不具备调用构造函数和析构函数功能,new 和 delete 除了分配回收功能外,还会调用构造函数和析构函数。
  • malloc 和 free 返回的是 void 类型指针,必须进行类型转换,new 和 delete 返回的是具体类型指针。

有了 malloc 为啥还需要 new?

  • malloc 和 new 都是用来申请内存的。
  • 但是在对非基本数据类型的对象使用时,对象的创建还需要执行构造函数,销毁的时候要执行析构函数。而malloc/free是库函数,是已经编译的代码,所以不能把构造函数和析构函数的功能强加给malloc/free。

被 free 回收的内存是否立即返回给操作系统?

  • 不是,被free回收的内存会首先被 ptmalloc 使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。

宏定义

宏定义 和 函数 的区别?

  • 宏在预处理阶段完成替换,相当于直接插入了代码,执行起来更快;函数调用在运行时需要跳转到具体调用函数。
  • 宏定义属于在结构中插入代码,没有返回值;函数调用具有返回值。
  • 宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型。
  • 宏定义不是语句,不在在最后加分号;函数是语句,要加分号标识结束。

宏定义和 typedef 的区别?

    • 宏替换发生在预处理阶段,属于文本插入替换;typedef是在编译时被使用。
  • 宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
  • 宏不检查类型;typedef会检查数据类型。
  • 宏不是语句,不在在最后加分号;typedef是语句,要加分号标识结束。

宏定义 和 const 的区别?

  • 宏定义是在预处理阶段被使用的,而 const 是在编译、运行的时候被使用的。
  • 宏定义只做替换,不做类型检查和计算,而 const 是有类型检查的。
  • 宏定义的数据没有分配内存空间,只是插入替换掉;const定义的变量只是值不能改变,但要分配内存空间。

sizeof,strlen

sizeof 和 strlen 的区别?

  • sizeof是运算符,并不是函数,结果在编译时得到;strlen是字符处理的库函数。
  • sizeof参数可以是任何数据的类型或者数据;strlen 的参数只能是字符指针且结尾是’\0’的字符串。
  • 因为sizeof值在编译时确定,所以不能用来得到动态分配存储空间的大小。

strcut、class

struct 和 class 的区别?

  1. 结构体是值类型的;类是引用类型的。
  2. 结构体用栈来存储;类用堆来存储。
  3. 结构体继承默认是public,类的继承默认是private。
  4. 结构体不可以为protected,类可以为protected。

-------------------------------------------------------

Debug

如何快速定位错误出现的地方?

  1. 如果是简单的错误,可以直接双击错误列表里的错误项或者生成输出的错误信息中带行号的地方就可以让编辑窗口定位到错误的位置上。
  2. 对于复杂的模板错误,最好使用生成输出窗口。

C程序

C++代码执行经历了什么?

  • 预处理:主要处理源代码文件中的以“#”开头的预编译指令。
  • 编译:将预编译之后生成的.i文件,进行一系列语法分析、语义分析及优化后,生成相应的汇编代码文件。
  • 汇编:将汇编代码转变成机器可以执行的指令。
  • 链接:将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。

动态编译和静态编译?

  • 静态编译,编译器在编译可执行文件时,把需要用到的对应动态链接库中的部分提取出来,链接到可执行文件中去。
  • 动态编译的可执行文件需要附带一个动态链接库,在执行时,需要调用其对应动态链接库的命令。

L垃圾回收

为什么C++没有垃圾回收机制?

  • 实现一个垃圾回收器会带来额外的空间和时间开销。
  • 需要开辟一定的空间保存指针的引用计数并且标记。然后需要再开辟一个线程在空闲的时候进行释放操作。

N内联函数

为什么不把所有函数都写成内联函数?

  • 引入内联函数的目的是以代码复杂为代价,省去函数调用的开销来提高执行效率。
  • 如果函数体内的代码比较长,将导致内存消耗为代价,不宜使用内联函数。
  • 如果函数体内有循环,函数执行时间要比函数调用开销大时,也不宜使用内联函数。

S锁

有哪些锁?

  • 读写锁:多个读可以同时进行,但是写只能有一个进行。写优先于读。
  • 互斥锁:一个时刻只能有一个线程拥有锁,其他线程进行等待。
  • 条件变量:互斥锁一个明显的缺点是他只有两种状态,锁定和非锁定。而条件变量通过允许线程阻塞,等待另一个线程发送唤醒信号,通常和互斥锁一起使用。当条件不满足时,线程往往解开相应的互斥锁并阻塞线程然后等待。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒被阻塞的部分线程。
  • 自选锁:如果线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止,比较消耗内存。

猜你喜欢

转载自blog.csdn.net/qq_39547794/article/details/129669292