面试问题总结——关于C++(二)

  • 接着上篇面试问题总结——关于C++(一),加油!

在这里插入图片描述

21.内联函数

  ①定义:使用inline关键字修饰的函数即为内联函数,编译器将会尝试进行内联优化,可以避免函数调用的开销,提高代码执行效率。
(理解:正常程序运行时,调用函数这个过程会消耗时间;若使用内联函数,则把要调用的函数直接放到原有程序中,这样就减少了调用的时间,但相应的也就增加了内存。)

  ②使用说明:多次调用的,小而简单的函数适合内联;调用次数极少或大而复杂的函数不适合内联;递归函数不能内联优化;虚函数不能内联优化。

类 inline 函数有两种方法

  (1) 隐喻式:定义在类中的成员函数缺省都是内联的,如果在类定义时就在类内给出函数定义,那当然最好。

class A
{
    
    
	public:void Foo(int x, int y) {
    
     }  //自动的成为内联函数
}

  (2) 明确声明:如果在类中未给出成员函数定义,而又想内联该函数的话,那在类外要加上inline,否则就认为不是内联的。

//头文件
class A
{
    
    
	public:
	void Foo(int x, int y);
}
inline void A::Foo(int x, int y) {
    
     }

22.函数重载为什么函数名字可以一样,函数入口地址是按照函数名给的,那这样岂不是地址完全一样,如何实现重载的呢?

  编译器在编译.cpp文件中当前使用的作用域里的同名函数时,根据函数形参的类型、个数、顺序对函数进行重命名。
  在vs编译器中: 根据返回值类型(不起决定性作用)+形参类型和顺序(起决定性作用)的规则重命名并记录在map文件中。

23.Qt信号槽实现原理

  信号和槽是多对多的关系,即一个信号可以连接多个槽,而一个槽也可以监听多个信号。Qt信号槽的实现原理其实就是函数回调

  Qt的智能指针:QPointer

  Qt内存管理机制:Qt在内部能够维护对象的层次结构,指的是子组件与父组件的关系,在Qt中,删除父对象会将其子对象一起删除。

24.void* 转换

  C++禁止void*的隐式转换, void * 则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换。
举例:

double a = 1.999;
void* vptr = &a;
double* dptr = static_cast<double*>(vptr);
cout << *dptr << endl;  //输出1.999

25.为什么要用基类指针(引用)指向子类对象

  基类指针指向派生类对象最大的优势在于,我们可以实现用数据结构存储不同类对象,并且分别展示出不同类对象的共有特性。
举例:https://blog.csdn.net/qq_33551749/article/details/108402617

26.C++ 中的内存管理、内存分配

  ①栈区(stack): 由编译器自动分配释放 ,一般存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

  ②堆区(heap):动态申请的内存空间,一般由程序员分配释放, 若程序员不释放,程序结束时可能由操作系统自动回收。(注意它与数据结构中的堆是两回事,分配方式倒是类似于链表)

  ③全局区/静态存储区(.bss段和.data段):存放全局变量和静态变量,程序运行结束时操作系统自动释放。(在C语言中,未初始化的放在.bss段中,初始化的放在.data段中,C++中不再区分)

  ④常量存储区(.data段):存放的是常量,程序运行结束时操作系统自动释放。

  ⑤程序代码区(.text段):用来存放代码,编译后的二进制文件存放在这里。
(说明:从操作系统的本身来讲,以上存储区在内存中的分布是如下形式(从低地址到高地址)
.text段 -> .data段 -> .bss段 -> 堆 -> unused –> 栈 -> env

27.栈和堆的区别

  ①申请方式:栈是系统自动分配的,堆是程序员主动申请的。

  ②申请后系统响应
分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败栈溢出;
申请堆空间,堆在内存中呈现的方式类似于记录空闲地址空间的链表,在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中该块空间的首地址存放的是本次分配空间的大小,便于释放,将该块空间上的剩余空间再次连接在空闲链表上。

  ③内存:栈在内存中是连续的一块空间,其最大容量是系统预设好的;堆在内存中的空间是不连续的。

  ④申请效率:栈是有系统自动分配,申请效率高,但程序员无法控制;堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片。

  ⑤存放的内容不同:栈中存放的是局部变量、函数的参数;堆中存放的内容是由程序员控制的。

28.malloc/free 和new/delete 区别

  相同点
都可以申请动态内存和释放内存。

  不同点
①malloc只分配指定大小的堆内存空间,而new可以根据对象类型分配合适的堆内存空间。
②malloc与free是C++/C 语言的标准库函数,new/delete 是C++的运算符。
new将调用constructor(构造函数),而malloc不能;delete将调用destructor(析构函数),而free不能。
④malloc返回类型是void*,使用时需要类型转换,而new返回类型是指向对象类型的指针。

29.全局变量定义在头文件中有什么问题?

  如果在头文件中定义全局变量,当该头文件被多个文件include时,该头文件中的全局变量就会被定义多次,导致重复定义,因此不能在头文件中定义全局变量。

30.函数重载(overload)和函数重写(override)

  函数重载:函数名称可以相同,只要他们的参数列表(参数个数,类型,排列顺序)不同即可。

  函数重写:将基类中某个成员函数声明为虚函数,那么其子类中与该函数具有相同原型的成员函数就也是虚函数,并且对基类中的版型形成覆盖,即函数重写(override)。

  区别:重写是子类与父类的垂直关系,是动态多态性;重载是同一个类中方法之间的水平关系,是静态多态性。
(静态多态性是指程序在编译时就确定了调用哪一个函数;动态多态性是指不在编译时确定调用的是那一个函数,而是在运行过程中才动态指定要调用的函数。)

31.关于虚函数和纯虚函数

  虚函数定义:在类成员方法的声明(不是定义)语句前加“virtual”, 如

virtual void func();

  纯虚函数定义:在虚函数后加“=0”,如

virtual void func()=0;

对于虚函数,子类可以(也可以不)重新定义基类的虚函数,该行为称之为函数重写

对于纯虚函数,子类必须提供纯虚函数的个性化实现。

  虚函数,和纯虚函数的区别
①含有纯虚函数的类被称为抽象类,抽象类不能实例化对象。
②虚函数可以被直接使用,也可以被子类重载以后以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类只有声明而没有定义。

  为什么有虚函数的还要写纯虚函数?
因为在很多情况下,基类本身生成对象是不合理的。

32.关于多态

在如下博客中 二十二.多态(Polymorphic)章节有详细的介绍
https://blog.csdn.net/qq_45445740/article/details/111715046
这个之前面试的时候问到过,让我打开IDE手写一个多态的案例。

  多态的定义:在满足虚函数覆盖(函数重写override)之后,通过指向子类对象的基类指针或者通过引用子类对象的基类引用,调用虚函数,实际执行的是子类中重写的覆盖版本,而不是原本基类中的版本,这种语法现象就是多态。

  多态的条件
①多态语法特性除了要满足虚函数的覆盖条件,还必须是通过指针或引用调用虚函数,才能表现出来。
②调用虚函数的指针也可以是this指针,当通过子类对象调用基类中的成员函数时,其this将是指向子类对象的基类指针,再通过它调用虚函数,同样可以表现多态的语法特性。

  多态的实现
①静态多态:静态多态就是重载,因为在编译期确定,所以称为静态多态。在编译时就可以确定函数地址。
②动态多态:动态多态就是通过继承重写基类的虚函数实现的多态,因为实在运行时决议确定,所以称为动态多态。运行时在虚函数表中寻找调用函数的地址。

33.什么是函数模板?函数模板算多态吗?函数模板底层如何实现?模板实例化在程序的哪个时期?

  ①所谓的函数模板,实际上是建立一个通用的函数,其函数的类型和形参的类型不具体指定,用一个虚拟的类型来代表,这个通用的函数就成为函数模板。凡是函数体相同的函数都可以用这个模板来代替,而不必定义多个函数,只需在模板中定义一次就行了,在调用函数的时候系统会根据实参的类型来取代模板中的虚拟类型,从而实现了不同函数的功能。

  ②模板把函数或类要处理的数据类型参数化,表现为参数的多态性。

  ③编译器的多态,属于静态多态

34.为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数?

  ①当动态申请一个子类对象,使用基类指针指向该对象时,如果不用虚函数,子类的析构函数不能得到调用,也就是为了在释放基类指针时可以释放掉子类的空间,防止内存泄漏。

  ②C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

35.构造函数为什么不是虚函数?

  因为调用虚函数的话需要一个虚函数表指针,但是这个虚函数表指针事实上是存储在对象的内存空间的。问题出来了,假设构造函数是虚的。就须要通过虚表指针来调用。但是对象还没有实例化,也就是内存空间还没有,怎么找虚函数表呢?所以构造函数不能是虚函数。

36.构造函数和析构函数的作用与特性

  构造函数是一种特殊的成员函数,它先为对象分配空间,然后进行初始化成员变量。构造函数的名字必须与类名相同,而不能由用户任意命名。它可以有任意类型的参数,但不能具有返回值类型。它不需要用户来调用,而是在建立对象时自动执行。

  构造函数具有一些特性
①构造函数的名字必须与类名相同,否则编译程序将把它当作一般的成员函数来处理。
②构造函数没有返回值,在定义构造函数时,是不能说明它的类型的,甚至说明为 void类型也不行。
③构造函数的函数体可写在类体内,也可写在类体外。
④构造函数的作用主要是用来对对象进行初始化,用户根据初始化的要求设计函数体和函数参数。在构造函数的函数体中不仅可以对数据成员赋初值,而且可以包含其他语句,但是,为了保持构造函数的功能清晰,一般不提倡在构造函数中加人与初始化无关的内容。
⑤构造函数一般声明为公有成员,但它不需要也不能像其他成员函数那样被显式地调用,它是在定义对象的同时被自动调用的,而且只执行一次。
⑥在实际应用中,通常需要给每个类定义构造函数。如果没有给类定义构造函数,则编译系统自动地生成一个默认构造函数。

  析构函数也是一种特殊的成员函数。它执行与构造函数相反的操作,通常用于执行一些清理任务,如释放分配给对象的内存空间等。

  析构函数有以下一些特点
①析构函数名与类名相同,但它前面必须加一个波浪号(~)。
②析构函数不返回任何值。在定义析构函数时,是不能说明它的类型的,甚至说明为 void类型也不行。
③析构函数没有参数,因此它不能被重载。一个类可以有多个构造函数,但是只能有一个析构函数
④撤销对象时,编译系统会自动地调用析构函数

37.进程、线程、协程

  进程的定义:进程,直观点说,就是保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己独立的地址空间,有自己的堆,上级挂靠单位是操作系统。操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。

  进程具有的特征
①动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
②并发性:任何进程都可以同其他进行一起并发执行;
③独立性:进程是系统进行资源分配和调度的一个独立单位;
④结构性:进程由程序,数据和进程控制块三部分组成。

  线程的定义:线程,有时被称为轻量级进程,是操作系统调度(CPU调度)执行的最小单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID,当前指令指针PC,寄存器和堆栈组成。而进程由内存空间(代码,数据,进程空间,打开的文件)和一个或多个线程组成。

  进程与线程的区别
①线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位。
②一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线。
③进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见。
④多进程的程序要比多线程的程序健壮,但多进程进行切换时要比多线程切换效率要差一些。

  进程与线程的联系
①一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
②资源分配给进程,同一进程的所有线程共享该进程的所有资源。
③处理机分给线程,即真正在处理机上运行的是线程。
④线程在执行过程中,需要协作同步,不同进程的线程间要利用消息通信的办法实现同步。

关于进程和线程的举例
假如一个双向多车道的道路,我们把整条道路看成是一个“进程”的话,那么由中间白色虚线分隔开来的各个车道就是进程中的各个“线程”了。这些线程(车道)共享了进程(道路)的公共资源(土地资源)。这些线程(车道)必须依赖于进程(道路),也就是说,线程不能脱离于进程而存在(就像离开了道路,车道也就没有意义了)。这些线程(车道)之间可以并发执行(各个车道你走你的,我走我的),也可以互相同步(某些车道在交通灯亮时禁止继续前行或转弯,必须等待其它车道的车辆通行完毕)。这些线程(车道)之间依靠代码逻辑(交通灯)来控制运行,一旦代码逻辑控制有误(死锁,多个线程同时竞争唯一资源),那么线程将陷入混乱,无序之中。这些线程(车道)之间谁先运行是未知的,只有在线程刚好被分配到CPU时间片(交通灯变化)的那一刻才能知道。

  协程的定义:是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。协程在子程序内部是可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行。

38.并发和并行

  通俗的讲,一个人"同时"做多件事,就是并发;多个人"同时"做多件事,就是并行。这里的同时,对于操作系统底层来说并不是真正意义上的同时,对于单CPU的计算机来说,同一时间CPU只能干一件事情,但操作系统把CPU的时间划分成长短基本相同的区间,就是"时间片",通过管理将这些时间片轮流分配给各个应用使用,来回的切换给人的感觉就是在同时进行。

  并发和并行两者表示的是CPU执行多个任务的方式。

  并发的定义:并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。

  并行的定义:并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。(注意,系统要有多个CPU才会出现并行)
所以,并发是指在一段时间内宏观上多个程序同时运行。并行指的是同一个时刻,多个任务确实真的在同时运行。

  并发和并行的区别
并发,指的是多个事情,在同一时间段内同时发生了;并行,指的是多个事情,在同一时间点上同时发生了。
并发的多个任务之间是互相抢占资源的;并行的多个任务之间是不互相抢占资源的。
③只有在多CPU的情况中,才会发生并行;否则,看似同时发生的事情,其实都是并发执行的。
在这里插入图片描述

39.vector set map list内部实现,复杂度

vector:内部封装了数组。
在这里插入图片描述
  C++中 vector 如何实现内存分配?
  为了支持随机访问,vector中的元素采用顺序存放,即每一个元素紧挨着前一个元素进行存储。那么现在只有可能出现问题了,当内存中没有足够连续的空间去存放新插入来的元素怎么办?
  C++是这样处理的:重新分配内存空间,将原来旧的元素全部复制到新的存储空间中去,然后再插入新的元素。因此可以看出,如果内存不是特别充足或者内存中没有较大块的空闲空间的,向vector容器中插入元素可能会有相当大的CPU开销。其中最糟糕的情况是每次插入一个元素,程序要将所有的元素复制到一个新的内存块上去,当然对于这种情况c++作了一个折衷的处理:当我们插入元素时,如果老的内存块是连续空闲空间不够,则重新分配一块内存空间,内存空间的大小不是只比旧的内存空间大一个元素的大小,相反而是多分配几个元素空间大小,这样对于接下来的几个新插入元素做到有空间可以插入,这样之后使得vector的性能得到很大提高,不过这是一个折衷的办法。

  set:内部封装了红黑树。所有元素都会根据元素的键值自动排序,set元素的键值就是实值,实值就是键值。set(集合)不允许两个元素有相同的键值。(set的元素不像map那样可以同时拥有实值(value)和键值(key))。

  list:内部封装了双向链表。
在这里插入图片描述
map:内部封装了红黑树。
在这里插入图片描述

40.关于红黑树

  红黑树的定义:红黑树,是一种二叉搜索树(就是左节点值<根节点值<右节点值),但在每个结点上增加一个存储位表示结点的颜色,可以是红色或黑色。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
在这里插入图片描述
  红黑树的性质
  ①每个结点不是红色就是黑色。
  ②根节点是黑色的。
  ③如果一个节点是红色的,则它的两个孩子结点是黑色的。
  ④对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。(每条路径上有相同数量的黑色节点。)
  ⑤每个叶子结点都是黑色的。(此处的叶子结点指的是空结点)

  为什么红黑树能保证最长路径不超过最短路径的两倍?
当一棵树全部是黑色节点的时候,满足上面的所有性质,在此情况下,可以增加红节点,假设3个黑节点,最多可以再增加3个红节点,再增加黑节点的话,就不满足上面的性质了,6/3 = 2,所以说,最长路径的节点数不会超过最短路径的节点数的两倍。

41.map与unordered_map区别及使用

  需要引入的头文件不同

map: #include < map >
unordered_map: #include < unordered_map >

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

  unordered_map:unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的。

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

  unordered_map优点:因为内部实现了哈希表,因此其查找速度非常的快。
  缺点:哈希表的建立比较耗费时间。
  适用性:对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用unordered_map。

42.对象创建限制在堆或栈:如何限制类的对象只能在堆上创建?如何限制对象只能在栈上创建?

  C++ 中的类的对象的建立分为两种:静态建立、动态建立

  静态建立:由编译器为对象在栈空间上分配内存,直接调用类的构造函数创建对象。

例如:A a;

  动态建立:使用 new 关键字在堆空间上创建对象,底层首先调用 operator new() 函数,在堆空间上寻找合适的内存并分配;然后,调用类的构造函数创建对象。

例如:A *p = new A();

  限制对象只能建立在堆上
最直观的思想就是避免直接调用类的构造函数,因为对象静态建立时,会调用类的构造函数创建对象。解决方法:构造函数设置为 protected,并提供一个 public 的静态函数来完成构造,而不是在类的外部使用 new 构造;将析构函数设置为 protected。原因:类似于单例模式,也保证了在派生类中能够访问析构函数。通过调用 create() 函数在堆上创建对象。

  限制对象只能建立在栈上
解决方法:将 operator new() 设置为私有。原因:当对象建立在堆上时,是采用 new 的方式进行建立,其底层会调用 operator new() 函数,因此只要对该函数加以限制,就能够防止对象建立在堆上。

43.关于内存对齐:什么是内存对齐?内存对齐的原则?为什么要进行内存对齐,有什么优点?

  内存对齐的定义:“数据项只能存储在地址是数据项大小的整数倍的内存位置上”。
例如int类型占用4个字节,地址只能在0,4,8等位置上。double类型占用8个字节,地址只能在0,8,16等位置上面。

  struct/class/union内存对齐原则
  ①内置类型数据成员:结构(struct/class)的内置类型数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的起始位置要从自身大小的整数倍开始存储。
  ②结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部“最宽基本类型成员”的整数倍地址开始存储(如struct a里存有struct b,b里有char, int, double等元素,那b应该从8的整数倍位置开始存储)。
  ③收尾工作:结构体的总大小,也就是sizeof的结果必须要对齐到内部"最宽基本类型成员"的整数倍,不足的要补齐。(基本类型不包括struct/class/union)。
  ④sizeof(union) 以结构里面size最大的元素为union的大小,因为在某一时刻,union只有一个成员真正存储于该地址。

  进行内存对齐的原因:(主要是硬件设备方面的问题)
  ①某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常;
  ②某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作;
  ③相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间;
  ④某些处理器虽然支持非对齐数据的访问,但会引发对齐陷阱(alignment trap);
  ⑤某些硬件设备只支持简单数据指令非对齐存取,不支持复杂数据指令的非对齐存取。

  内存对齐的优点:
  ①便于在不同的平台之间进行移植,因为有些硬件平台不能够支持任意地址的数据访问,只能在某些地址处取某些特定的数据,否则会抛出异常;
  ②提高内存的访问效率,因为 CPU 在读取内存时,是一块一块的读取。

猜你喜欢

转载自blog.csdn.net/qq_45445740/article/details/120507448