1.c++内存分区

C++语言分区:堆、栈、自由存储区、全局/静态存储区、常量存储区(标准答案)
C/C++内存模型根据生命周期的不同有三大区:即自由存储区(C++),动态区、静态区

  • text段: 用于存放程序代码的区域, 编译时确定, 只读。更进一步讲是存放处理器的机器指令,当各个源文件单独编译之后生成目标文件,经连接器链接各个目标文件并解决各个源文件之间函数的引用,与此同时,还得将所有目标文件中的.text段合在一起,但不是简单的将它们“堆”在一起就完事,还需要处理各个段之间的函数引用问题。
  • data段 : 用于存放在编译阶段(而非运行时)就能确定的数据,可读可写。也是通常所说的静态存储区,赋了初值的全局变量、常量和静态变量都存放在这个域。 (已经初始化的数据)
  • bss段: 不在可执行文件中,由系统初始化。(未初始化的数据)
  • heap: 由程序员分配和释放;若程序员不释放,程序结束时操作系统会进行回收(malloc,free,new,delete)
  • stack: 存放函数的参数值和局部变量,由编译器自动分配和释放
  • env:环境变量

堆 heap
由程序员释放:指的是程序员使用malloc/free,或者new/delete(自由存储区,自由存储区可以理解是堆区的一个子集,不完全等价)申请或者销毁的内存所在的区域。
如果程序员没有释放,在程序运行过程中,可能出现内存泄漏的状况(堆上无用的内存没有及时的销毁free/malloc,程序无法在堆上找到空闲的内存,目前的操作系统通常有虚拟内存技术,每个进程都有可能将整个内存空间完全沾满,这样内存泄漏是非常危险的,会导致其他进程的内存页被换下,会导致大量的缺页中断,最后死机蓝屏)
栈 stack
由编译器自动分配释放,存放函数的参数值,局部变量,(形参)等
全局/静态存储区
全局变量和静态变量被分配到同一块内存中。在C语言中分为两个大类三个小类两个大类(.bss .data),三个小类(全局未初始化(.bss的高地址),静态未初始化(.bss的低地址),已初始化(.data))。在 C++ 中的.bss段,他们共同占用同一块内存区。
常量区
存放常量的区间,如字符串常量等,注意在常量区存放的数据一旦经初始化后就不能被修改。 程序结束后由系统释放。
堆和栈的区别:

  • 管理方式不同
    栈,由编译器自动管理,无需程序员手工控制;堆:产生和释放由程序员控制。
  • 空间大小不同
    栈的空间有限;堆内存可以达到4G,。
  • 能否产生碎片不同
    栈不会产生碎片,因为栈是种先进后出的队列。堆则容易产生碎片,多次的new/delete(delete顺序没有考究,没有用的数据没有马上delete或者太长时间没有用)
    会造成内存的不连续,从而造成大量的碎片。
  • 生长方向不同
    堆的生长方式是向上的,栈是向下的。(堆保存在低地址,栈保存在高地址)
  • 分配方式不同
    堆是动态分配的(malloc,new)。栈可以是静态分配和动态分配两种(栈的动态分配是由编译器进行释放alloca函数),但是栈的动态分配由编译器释放。
  • 分配效率不同
    栈是机器系统提供的数据结构,计算机底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令。堆则是由C/C++函数库提供,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。 (主要影响原因,内存碎片,底层支持程度)

堆和自由存储区的区别:
我们常说,由malloc/free 申请或者销毁的内存分布在堆区,由new/delete申请的内存在自由存储区。
但是new/delete在默认情况下是使用了malloc/free 那我们能不能说自由存储区是堆区的子集呢?不能
在C++中我们可以对new进行重载(实际上new 和 delete 有一种形式不能被重载,之后会谈到),我们可以让对象的内存空间不在堆区而在全局区或者其他。这个时候自由存储区就不是堆区的子集。

  • 自由存储区:由new/delete管理的内存区域(new和delete能管理到的内存区域都可以叫自由存储区,是一个逻辑概念)
  • 堆区:实际内存区的一个固定部分(是一个物理概念)
    malloc/free和new/delete的区别:
  • new、delete是C++中的操作符,而malloc和free是标准库函数。操作符可以在类的内部重载,malloc/free不行,唯一的关联只是在默认情况下new/delete调用malloc/free
  • malloc/free(只是分配内存,不能执行构造函数,不能执行析构函数)。
  • new返回的是指定类型的指针,并且可以自动计算所申请内存的大小。而 malloc需要我们计算申请内存的大小,并且在返回时强行转换为实际类型的指针。

2.const和define的区别

  • 就起作用的阶段而言:#define是在编译的预处理阶段起作用(在预处理阶段进行替换),而const是在编译运行的时候起作用(const修饰的只读变量是在编译的时候确定其值)
  • 就起作用的方式而言:#define只是简单的字符串替换,没有类型检查。而const有对应的类型,是要进行判断的,可以避免一些低级的错误
  • 就存储方式而言:#define只是进行展开,有多少地方使用,就替换多少次。它定义的宏常量在内存中存若干个备份;const定义的只读变量在程序中只有一份备份
  • 代码调试的方便程度而言:const常量可以进行调试的,define是不能进行调试的,因为在预编译阶段就已经进行替换了
  • 内存分配而言:编译器通常不为普通的const只读变量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高
#define M 3
const int N = 5;  //此时并未将N放入内存中int i = N;此时为N分配内存,以后不再分配
int l = M;//预编译期间进行宏替换,分配内存
int j = N;//没有内存分配
int J = M;//再进行宏替换,又一次分配内存

const定义的只读变量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的只读变量在程序运行过程中只有一份拷贝(因为它是全局的只读变量,存放在静态区),而#define定义的宏常量在内存中有若干个拷贝。

const的优点

  • const常量有数据类型,而宏常量没有数据类型,编译器可以对const进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误
  • 有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试
  • const在内存中只存储了一份,节省了空间,避免不必要的内存分配,提高了效率

3.数组,std::array和STL的vector数组有什么区别

  • 数组
    这些数的类型必须相同。
    这些数在内存中必须是连续存储的。
  • std::array
    array 容器是 C++ 11 标准中新增的序列容器,简单地理解,它就是在 C++ 普通数组的基础上,添加了一些成员函数和全局函数。在使用上,它比普通数组更安全(原因后续会讲),且效率并没有因此变差。
    和其它容器不同,array 容器的大小是固定的,无法动态的扩展或收缩,这也就意味着,在使用该容器的过程无法借由增加或移除元素而改变其大小,它只允许访问或者替换存储的元素。
  • STL的vector
    ector 容器是 STL 中最常用的容器之一,它和 array 容器非常类似,都可以看做是对 C++ 普通数组的“升级版”。不同之处在于,array 实现的是静态数组(容量固定的数组),而 vector 实现的是一个动态数组,即可以进行元素的插入和删除,在此过程中,vector 会动态调整所占用的内存空间,整个过程无需人工干预。
    v ector 常被称为向量容器,因为该容器擅长在尾部插入或删除元素,在常量时间内就可以完成,时间复杂度为O(1);而对于在容器头部或者中部插入或删除元素,则花费时间要长一些(移动元素需要耗费时间),时间复杂度为线性阶O(n)。

vector容器的底层实现机制:

STL 众多容器中,vector 是最常用的容器之一,其底层所采用的数据结构非常简单,就只是一段连续的线性内存空间。
通过分析 vector 容器的源代码不难发现,它就是使用 3 个迭代器(可以理解成指针)来表示的:

//_Alloc 表示内存分配器,此参数几乎不需要我们关心
template <class _Ty, class _Alloc = allocator<_Ty>>
class vector{...
protected:pointer _Myfirst;pointer _Mylast;pointer _Myend;
};

其中,_Myfirst 指向的是 vector 容器对象的起始字节位置;_Mylast 指向当前最后一个元素的末尾字节;_myend 指向整个 vector 容器所占用内存空间的末尾字节。

图 1 演示了以上这 3 个迭代器分别指向的位置。

在此基础上,将 3 个迭代器两两结合,还可以表达不同的含义,例如:
_Myfirst 和 _Mylast 可以用来表示 vector 容器中目前已被使用的内存空间;
_Mylast 和 _Myend 可以用来表示 vector 容器目前空闲的内存空间;
_Myfirst 和 _Myend 可以用表示 vector 容器的容量。

vector扩大容量的本质:

当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:

  • 完全弃用现有的内存空间,重新申请更大的内存空间;
  • 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
  • 最后将旧的内存空间释放。

emplace_back()和push_back()的区别:

emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同。push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。

相同点:

三者均可以使用下表运算符对元素进行操作,即vector和array都针对下标运算符[]进行了重载
三者在内存的方面都使用连续内存,即在vector和array的底层存储结构均使用数组

不同点

vector属于变长容器,即可以根据数据的插入删除重新构建容器容量;但array和数组属于定长容量。

vector和array提供了更好的数据访问机制,即可以使用front和back以及at访问方式,使得访问更加安全。而数组只能通过下标访问,在程序的设计过程中,更容易引发访问 错误。

vector和array提供了更好的遍历机制,即有正向迭代器和反向迭代器两种

vector和array提供了size和判空的获取机制,而数组只能通过遍历或者通过额外的变量记录数组的size

vector和array提供了两个容器对象的内容交换,即swap的机制,而数组对于交换只能通过遍历的方式,逐个元素交换的方式使用

array提供了初始化所有成员的方法fill

vector提供了可以动态插入和删除元素的机制,而array和数组则无法做到,或者说array和数组需要完成该功能则需要自己实现完成

由于vector的动态内存变化的机制,在插入和删除时,需要考虑迭代的是否失效的问题。

总结:

  • 如果只是需要固定大小的数组,那么应该使用std::array,我们可以使用很多成员函数;
  • 如果需要的是支持插入,删除,扩展的数组,那么建议用std::vector。
  • 需要考虑到性能的时候,如果是进行一些数***算或者算法的时候,对性能要求比较高 。

4.数组和链表,什么时候使用数组结构?什么时候使用链表结构?

数组和链表的区别

  • 数组:
    数组的元素在内存中连续存储的;
    它的优点:因为数据是连续存储的,所以内存地址连续,在查找数据的时候效率比较高;
    它的缺点:在创建的时候,我们需要确定其大小,申请一块连续的内存空间,一经创建就无法改变。在运行的时候,空间的大小是无法随着需要增加和减少而改变的。
    当数据量比较大的时候,有可能会出现越界的情况,数据比较小的时候,又有可能会浪费掉内存空间。在改变数据个数时,插入、删除数据效率比较低。
  • 链表:
    链表是动态申请内存空间,其不需要像数组需要在创建的时候就确定好内存的大小,链表的具体实现有ArrayList和LinkedList,前者默认大小为10,当存储空间不足的时候会自动扩容,而后者没有默认大小,只有你想,你可以一直扩容下去。
    链表只需要在用的时候申请就可以了,根据需求来动态申请或者删除内存空间,对于数据插入和删除比数组灵活。还有就是链表中的数据可以放在内存中的任何地方。

链表和数组使用场景

  • 数组应用场景:
    数据比较少;经常做的运算,是按序号访问数据元素;构建的线性表较稳定。
  • 链表应用场景:
    对线性表的长度或者规模难以估计;频繁做插入删除操作;构建动态性比较强的线性表。

5.static关键字的作用

全局静态变量

在全局变量前加上关键字static,全局变量就定义成一个全局静态变量。
内存位置:静态存储区,在整个程序运行期间一直存在。
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化),编译时初始化。
作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。

局部静态变量

在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。
内存中的位置:静态存储区
初始化:静态局部变量在第一次使用时被首次初始化,即以后的函数调用不再进行初始化,未经初始化的会被程序自动初始化为0;
作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;

静态函数

在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;

类的静态数据成员

  • 静态数据成员可以实现多个对象之间的数据共享,它是类的所有对象的共享成员,它在内存中只占一份空间,如果改变它的值,则各对象中这个数据成员的值都被改变。
  • 静态数据成员是在创建类对象前被分配空间,到程序结束之后才释放,只要类中指定了静态数据成员,即使不定义对象,也会为静态数据成员分配空间。
  • 静态数据成员可以被初始化,但是只能在类外进行初始化,静态成员变量使用前必须先初始化,若未对静态数据成员赋初值,则编译器会自动为其初始化为 0。
  • 静态数据成员既可以通过对象名引用,也可以通过类名引用。
  • 基类定义了static静态成员,则整个继承体系里只有一个这样的成员。无论派生出多少个子类,都有一个static成员实例。

类的静态函数

  • 静态成员函数和静态数据成员一样,他们都属于类的静态成员,而不是对象成员。
  • 非静态成员函数有 this 指针,而静态成员函数没有 this 指针。
  • 静态成员函数主要用来访问静态成员而不能访问非静态成员

6.指针和引用

引用
引用就是C++对C语言的重要扩充。引用就是某一变量的一个别名,对引用的操作与对变量直接操作完全一样。引用的声明方法:类型标识符 &引用名=目标变量名;引用是对象的一个同义词。
指针
指针就是所指对象的地址。
引用和指针的区别

  • 大小:指针有自己的一块空间,使用sizeof看一个指针的大小是4;而引用只是一个别名,其大小是引用对象的大小
  • 初始化:指针可以被初始化为nullptr,在使用中可以指向其它对象;而引用必须被初始化且必须是一个已有对象的引用;引用只能是一个对象的引用,使用时不能被改变;
  • 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
  • 可以有const指针,但是没有const引用;
  • 指针可以有多级指针(** p),而引用只有一级;
  • 指针和引用使用++运算符的意义不一样;

引用的底层实现
引用在内存中也会分配空间,空间中存放的是被引用变量的地址,因此引用可以看作一个常量指针。
对引用取地址操作,其实是对被引用变量取地址,编译器将对引用取地址解释为&(*ptr) 取地址。

7.野指针与悬空指针

  • 野指针就是没有初始化的指针。野指针就是指针指向的位置是不可知的(随机的、不正确
    的、没有明确限制的)指针。
  • 悬空指针就是最初指向的内存已经被释放了的指针。
    解决:其内存被释放后,指向nullptr

8.C++里是怎么定义常量的?常量存放在内存的哪个位置?

常量在C++里的定义就是一个const加上对象类型,常量定义必须初始化。对于局部对象,常量存放在栈区,对于全局对象,静态变量存放在静态存储区。对于常量存放在常量存储区。
const修饰成员函数的目的是什么?
const修饰的成员函数表明函数调用不会对对象做出任何更改,事实上,如果确认不会对对象做更改,就应该为函数加上const限定,这样无论const对象还是普通对象都可以调用该函数。

9.new/delete与malloc/free

new/delete运算符

  • new运算返回所分配内存单元的起始地址,所以需要把返回值保存在一个指针变量中。若分配不成功,返回NULL,并抛出异常。new没有为创建的对象命名,只能通过指针去访问对象或者数组。
  • delete释放内存,只是销毁内存上的对象,但是指针仍然存在,仍然指向原来的内存,保存原来空间的地址。所以我们应该在释放之后将指针置空,以避免后面不小心解引用造成问题。

malloc/free函数

void *malloc(unsigned int size)

  • malloc函数开辟一块长度为size的连续内存空间,返回类型为void类型的指针。在使用malloc开辟一段空间时,void* 要显示的转换为所需要的类型,如果开辟失败,则返回NULL指针。
  • free函数是来释放动态开辟的内存的。

10.strlen和sizeof的区别

  • sizeof操作符的结果类型为size_t,计算的是分配空间的实际字节数。strlen结果类型也为size_t,但strlen是计算的字符串中字符的个数(不包括‘\0’)。
  • sizeof是运算符,可以以类型、函数、做参数 。strlen是函数,只能以char*(字符串)做参数。而且,要想得到的结果正确必须包含 ‘\0’(通过strlen的实现得知)。
  • sizeof是在编译的时候就将结果计算出来了是类型所占空间的字节数,所以以数组名做参数时计算的是整个数组的大小。而strlen是在运行的时候才开始计算结果,这是计算的结果不再是类型所占内存的大小,数组名就退化为指针了。
  • sizeof不能计算动态分配空间的大小

11.const关键字

const修饰普通变量
使用const修饰普通变量,在定义该变量时,必须初始化,并且之后其值不会再改变。
const的引用
把引用绑定到const对象上,称之为对常量引用。对常量引用不能被用作修改它所绑定的对象。

const int ci=1024;
const int &ri=ci; //正确
ri=42; //错误,r1是常量引用,不能修改其绑定对象
int &r2=ci; //错误,这样就可以通过r2改变ci,显然是错误的

指针和const

  • 和引用一样,可以使用指针指向常量,这称为指向常量的指针(指针常量),此时指针指向的是常量,因此无法通过指针改变其指向对象的值,想要存放常量对象的地址,只能使用指向常量的指针。
const int a=1;
const int *ptr=&a; //正确,但是无法改变*ptr,因为ptr所指的是常量
int *p=&a; //错误,a为常量,所以只能使用指向常量的指针
  • 除了指向常量的指针外,还可以使用从const修饰指针,即指针本身是常量,称为常量指针。常量指针必须初始化,并且之后该指针的值就不会再改变。
int a=1;
int *const b=a; //此时b只能指向a,无法指向c等变量,但a不是常量,因此a还是
可以改变的,*b也可以改变,但是b无法改变,类似于常量引用。

说到底,就是const修饰谁,谁就无法改变,不是const修饰的对象就可以改变。
函数中的const参数
const修饰函数参数,表示参数不可变,此时可以使用const引用传递。const引用传递和函数按值传递的效果是一样的,但按值传递会先建立一个类对象的副本, 然后传递过去,而它直接传递地址,所以这种传递比按值传递更高效。

12.函数指针和指针函数

  • 函数指针
    函数指针,其本质是一个指针变量,该指针指向这个函数。总结来说,函数指针就是指向
    函数的指针。
    声明格式:类型说明符 (* 函数名) (参数)
  • 指针函数
    指针函数,简单的来说,就是一个返回指针的函数,其本质是一个函数,而该函数的返回
    值是一个指针。
    声明格式为:类型标识符* 函数名(参数表)

13.C++中struct和class的区别

  • 在C++中,可以用struct和class定义类,都可以继承。
  • struct的默认继承权限和默认访问权限是public,而class的默认继承权限和默认访问权限是private。
  • class可以使用模板,struct不能使用模板。

14.C++ explicit

C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式).

15.inline关键字

  • 在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数。栈空间就是指放置程序的局部数据(也就是函数内数据)的内存空间。
  • 增加了 inline 关键字的函数称为“内联函数”。内联函数和普通函数的区别在于:当编译器处理调用内联函数的语句时,不会将该语句编译成函数调用的指令,而是直接将整个函数体的代码插人调用语句处,就像整个函数体在调用处被重写了一遍一样。有了内联函数,就能像调用一个函数那样方便地重复使用一段代码,而不需要付出执行函数调用的额外开销。很显然,使用内联函数会使最终可执行程序的体积增加。以空间换取时间,或增加空间消耗来节省时间,这是计算机学科中常用的方法。

inline 和宏定义的区别

  • 内联函数在编译时展开,宏在预编译时展开;
  • 内联函数直接嵌入到目标代码中,宏是简单的做文本替换;
  • 内联函数可以完成诸如类型检测,语句是否正确等编译功能,宏就不具有这样的功能。
  • 宏不是函数,inline函数是函数
  • 宏在定义时要小心处理宏参数,一般用括号括起来,否则容易出现二义性。而内联函数不会出现二义性。

使用限制
inline的使用是有所限制的,inline只适合涵数体内代码简单的涵数使用,不能包含复杂的结构控制语句例如while、switch,并且不能内联函数本身不能是直接递归函数(即,自己内部还调用自己的函数)。

16.为什么析构函数必须是虚函数

对于一个基类和派生类来说,在调用构造函数时先调用基类的构造函数,再调用派生类构造函数;而当调用析构函数时,则要先调用派生类的析构函数再调用基类的析构函数。如果定义了一个指向派生类对象的基类指针,当析构函数为普通函数时,释放该基类指针时,只会调用基类的析构函数,而不会调用派生类的析构函数,会导致内存泄漏。当我们把基类析构函数定义为虚函数时,在调用析构函数时,会在程序运行期间根据指向的对象类型到它的虚函数表中找到对应的虚函数(动态绑定),此时找到的是派生类的析构函数,因此调用该析构函数;而调用派生类析构函数之后会再调用基类的析构函数,因此不会导致内存泄漏。

17.为什么C++默认的析构函数不是虚函数

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

18.为什么构造函数不能是虚函数

如果构造函数是虚函数,那么一定有一个已经存在的类对象obj,obj中的虚指针来指向虚表的构造函数地址(通过obj的虚指针来调用); 可是构造函数又是用来创建并初始化对象的,虚指针也是存储在对象的内存空间的; 所以构造函数是虚函数的前提是它要有类的对象存在,但在这之前又没有其他可以创建并初始化类对象的函数,所以矛盾。
总的来说,就是调用虚函数需要有类的对象,但是构造函数就是用来生成对象的,所以矛盾。

19.C++中析构函数的作用

析构函数和构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。析构函数释放对象使用的资源,并销毁对象的非static数据成员。
析构函数是一个类的成员函数,名字由波浪号接类名构成。它没有返回值,也不接受参数。由于析构函数不接受参数,因此它不能被重载。对于一个给定类,只会有唯一一个析构函数。
当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数)。对于某些类,合成析构函数被用来阻止该类型的对象被销毁。否则,合成析构函数的函数体就为空。因此,许多简单的类中没有用显式的析构函数。合成析构函数无法自动释放动态内存。
如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数
在销毁类之前,释放掉申请的内存空间,避免内存泄漏。

类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数。

20.静态函数和虚函数的区别

静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为
用了虚函数表机制,调用的时候会增加一次内存开销。

21.重载、覆盖、隐藏

重载
两个函数名相同,但是参数列表不同(个数,类型,顺序),返回值类型没有要求,在同一作用域中,常用来处理实现功能类似但数据类型不同的问题。单纯的返回值不同不是重载。
覆盖(重写)
子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是
覆盖。其中函数名,参数列表,返回值,调用约定等均相同。对比覆盖和隐藏,不难发现
函数覆盖其实是函数隐藏的特例。如果派生类中定义了一个与基类虚函数同名但是参数列
表不同的非virtual函数,则此函数是一个普通成员函数,并形成对基类中同名虚函数的隐
藏,而非虚函数覆盖。
隐藏(重定义)
指不同作用域中定义的同名函数构成隐藏(不要求函数返回值和函数参数类型相同)。比如派生类成员函数隐藏与其同名的基类成员函数、类成员函数隐藏全局外部函数。
隐藏的实质是;在函数查找时,名字查找先于类型检查。如果派生类中成员和基类中的成员同名,就隐藏掉。编译器首先在相应作用域中查找函数,如果找到名字一样的则停止查找。

22.多态

我们把具有继承关系的多个类型称为多态类型。当使用基类的引用或指针调用基类中定义的函数时,它会有多种执行状态,即多态。多态实际上是一种思想,虚函数是该思想的实现基础。

  • 静态多态主要是重载,在编译的时候就已经确定;
  • 动态多态是用虚函数机制实现的,在运行期间动态绑定。举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。

多态的好处: 提高了代码的维护性(继承保证),提高了代码的扩展性(多态保证)

23.虚函数

概念
基类希望其派生类进行覆盖的函数被称为虚函数。当使用指针或引用调用虚函数时,该调用将被动态绑定。根据引用或指针绑定的对象类型不同,该调用可能执行基类的版本,也可能执行派生类的版本。因此虚函数的解析过程发生在运行时。
实现
虚函数是多态实现的基石,而该虚函数的动态绑定主要是依据虚函数表(虚表)。

  • 类的虚表:每个包含了虚函数的类都包含一个虚表。如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。
  • 虚表指针:指向虚表的指针。类的每个对象都有一个虚表指针,指向该类的虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。虚表指针位于对象内存的最前面,后面才是成员变量。
  • 动态绑定:检测到有虚函数的重写时,编译器会用子类重写的虚函数地址覆盖掉之前父类的虚函数地址,当调用虚函数时,检测到函数是虚函数就会从虚表中找对应的位置调用,若子类没有重写,虚表中的虚函数地址就还是父类的,若子类中有重写,虚表记录的就是子类重写的虚函数地址,即实现了父类的指针调用子类的函数。使用了虚函数,会增加访问内存开销,降低效率.

24.纯虚函数

定义
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加 =0:

virtual void funtion1()=0

引入原因
为了方便使用多态特性,我们常常需要在基类中定义虚函数。
在某些情况下,基类本身是不能生成对象的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性,而在抽象类中只有声明没有定义。声明了纯虚函数的类是一个抽象类,它不能生成对象,只能创建它的派生类的实例。
纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的默认实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

25.抽象类

抽象类是一种特殊的类,它是为了抽象和设计的目的建立的,它处于继承层次结构的较上层。带有纯虚函数的类称为抽象类。
作用
抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,并为派生类提供基类。抽象类是用来捕捉子类的通用特性的,它是被用来创建继承层级里子类的模板。
注意

  • 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。
  • 如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。
  • 如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
  • 在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚函数。
  • 抽象类无法被实例化,无法定义对象

26.拷贝构造函数取引用的原因

拷贝构造函数的发生情况:

  • 将一个对象作为实参传递给一个非引用类型的形参
  • 从一个返回类型为非引用类型的函数返回一个对象
  • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员(A={})
  • 某些类型还会对它们所分配的对象使用拷贝初始化,如insert或push函数,容器会使用拷贝初始化,但是使用emplace时进行直接初始化。

原因:
如果拷贝构造函数的形参不是引用类型,那么我们调用拷贝构造函数时就会发生第一种情况,即拷贝它的实参,但拷贝它的实参,又会需要调用拷贝构造函数,这样就会无限循环。

27.C++中类成员的访问权限

类的访问权限
C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访private、protected 属性的成员。
public: 可以被该类中的函数、子类的函数、友元函数访问,也可以在类的外部使用类的对象访问;
protect: 类的内部可以访问,但是类的外部不能访问。受保护的成员对于派生类的成员和友元是可以访问的。对于派生类来说,派生类的成员或友元只能通过派生类对象访问基类的受保护成员。派生类不能访问基类对象中的受保护成员。
private: 可以被该类中的函数、友元函数访问,但不可以由子类的函数、该类的对象、访问。(只能在该类的内部进行访问)

28.深拷贝和浅拷贝

  • 浅拷贝会把指针变量的地址复制;
  • 深拷贝会重新开辟内存空间。

在类中默认拷贝构造函数可以完成对象的数据成员简单的复制,这也称为浅拷贝。对象的数据资源是由指针指向的堆时,默认的拷贝构造函数只是将指针复制,而不会开辟新的内存空间,这样会有两个指针指向同一块内存,当释放指针的时候,会导致该内存释放两次,会报错。因此当类中存在指针指向的数据时,需要自己写拷贝构造函数。

29.final关键字

1.修饰类,防止该类被继承
2.修饰方法,防止该方法被重写

30.友元函数

类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。

31.struct和union

struct和union都是由多个不同的数据类型成员组成, 但在任何同一时刻, union中只存放了一个被选中的成员, 而struct的所有成员都存在。

  • 在struct中,各成员都占有自己的内存空间,它们是同时存在的。一个struct变量的总长度等于所有成员长度之和。
  • 在union中,所有成员不能同时占用它的内存空间,它们不能同时存在。Union变量的长度等于最长的成员的长度。对于union的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于struct的不同成员赋值是互不影响的。

32.c/c++编译流程

程序编译的过程中就是将用户的文本形式的源代码(c/c++)转化成计算机可以直接执行的机器代码的过程。主要经过四个过程:预处理、编译、汇编和链接。

预处理
预处理用于将所有的包含头文件(include),条件编译指令(#ifdef, #endIf),以及宏定义(#define) 替换成其真正的内容。我们可以利用gcc -E来让文件执行到预处理阶段就停止,此时输出的文件是以.i结尾
预处理之后的程序还是文本,可以用文本编辑器打开。且预处理后的文件明显变大。
编译
这里的编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程。生成的汇编代码以.s结尾。
汇编
汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式,此时的文件已经不能用文本编辑器打开了。我们可以使用gcc -c指令让编译器执行到这个阶段,生成.obj二进制文件。
链接
链接过程将多个目标文件以及所需的库文件链接成最终的可执行文件(executable file)。其中库分为动态链接库以及静态链接库。

  • 静态链接(lib)
    在这种链接方式下,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中(进入exe文件)。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。
  • 动态链接 (dll)
    在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息(只记录名称地址)。在此可执行文件被执行时(执行再调用),动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。

C/C++编译过程对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。

2022年C++校招面经相关推荐

  1. 我的2022届互联网校招分享

    前言 昨天发了我的2021总结,比其他博主发的都晚了一些. 大家的留言我都看了,非常感谢大家的支持,认可和赞扬. 以后工作了,其实相当于从一个阶段,又跳到了,另一个阶段. 重新从0开始. 昨天一共收到 ...

  2. 阿里达摩院-XR实验室2022春招校招内推

    点击下方卡片,关注"CVer"公众号 AI/CV重磅干货,第一时间送达 阿里达摩院-XR实验室2022春招校招 面向对象:2022年11月~2023年10月毕业的应届毕业生 校招面 ...

  3. 腾讯2022应届生校招总包40W

    哈喽,大家好啊,我是Beyonce,最近逛知乎的时候发现一个很热的话题:「如何看待腾讯 2022 校招大厂薪资首发,白菜总包接近 40w ?」 应届生校招40W,再想到往年春节时候,腾讯的小伙伴各种奖 ...

  4. 2022届互联网校招薪资开奖,拼多多最高年薪 75 万!

    近一个月,收到很多读者今年秋招拿到互联网大厂 offer 的喜报. 从他们的信息我也大概知道了今年的秋招应届生的薪资情况,真的是一年比一年高,大厂太舍得砸钱啦. 对于开发岗来说,一线互联网大厂的白菜档 ...

  5. 2022届互联网校招研发薪资汇总,都是钱哇~

    近一个月,我收到很多读者今年秋招拿到互联网大厂 offer 的喜报. 从他们的信息我也大概知道了今年的秋招应届生的薪资情况,真的是一年比一年高,大厂太舍得砸钱啦. 对于开发岗来说,一线互联网大厂的白菜 ...

  6. 2022 鹅厂校招年薪40W相比某某跳动应届生月薪4K被劝退,差距真就这么大吗?

    鹅厂的第一枪 就在前不久,鹅厂率先打破死锁,开启国内互联网大厂校招第一枪! 今年校招研发依旧分为 5 档,股票和去年一样,月薪同比去年上调 2k. 签字费和去年一样,3w,一次性发放. 大量00后职场 ...

  7. 2022华为云校招内推机会

    招聘岗位: 1. 软件开发工程师 2.云计算开发工程师 3.测试工程师 4.AI工程师 招聘地点: 西安 深圳 杭州 东莞 适合人群: 2022年1月1日到12月31日期间的国内高校应届生,2021或 ...

  8. 【回顾】分享一下2022年的校招经历(华为、中兴、字节、联发科、联想等等)

    最近一段时间应该没有面试了,于是整理这两三个月的找工作经历,回顾总结一下.本人半路出家学C++,硕士期间做的项目都是PC端的开发,很多计算机相关的知识都没有得到系统学习,感觉不够扎实,不精也不泛,在秋 ...

  9. 2022届互联网公司校招后端技术研发笔试

    吐血分享:完全能满足你的爱奇艺Java开发工程师校招笔试实操(2021年8月) 前言+说明 一.选择题 1.多选题 (cookie 和session) 2.单选题 (快速排序) 3.多选题 (事务隔离 ...

  10. 【数据服务校招专场】云和恩墨2022届春季校招「数据服务岗位」持续招聘中!...

    数据驱动,成就未来,云和恩墨,不负所托! 云和恩墨创立于2011年,以"数据驱动,成就未来"为使命,是智能的数据技术提供商.我们致力于将数据技术带给每个行业.每个组织,构建数据驱动 ...

最新文章

  1. 怎样的架构设计能力,才能成功拿下阿里P7?
  2. i2cdetect检测不到i2c设备_I2C基础知识入门
  3. Hopsan -- 液压、电力等行业的仿真开源软件
  4. C#制作WinForm控件
  5. HBASE整合ldap权限管理
  6. SharePoint Portal Server之常见问题
  7. leetcode题目整理
  8. linux动态库路径生效,Linux下如何解决动态库的链接问题
  9. Android 实现两个控件水平居中
  10. 移动开发者的未来在哪里?
  11. 联合哈佛大学,Google 要用人工智能来预测地震余震
  12. C语言 · 图形输出
  13. arm-linux-g++ crypto,在Ubuntu中找不到libcrypto
  14. 用word模仿手写字体
  15. 【raid5数据恢复案例】两组分别由4块SAS硬盘组成的raid5阵列,两组阵列组成lvm结构,出现故障后数据恢复方案
  16. 网站建设费用贵和便宜有什么区别?
  17. 每个c语言程序文件的编译错误被分为什么,已打印中央电大C语言考试题库(c语言小题+编程)...
  18. win10照片查看器_解决“Windows照片查看器无法显示此图片,因为计算机上的可用内存可能不足……”...
  19. 全国计算机等级考试(NCRE)
  20. 解决Linux遇到There are no enabled repos.的问题

热门文章

  1. 公司新来一个以前在阿里拿37K的测试,真是麻雀啄牛腚——雀食牛逼
  2. 基于 RTAB-MAP 算法实现真实三维场景重建
  3. 管理类联考-英语 : 基础 [ 一 ]
  4. 互联网十种经典的创业模式
  5. 第三方支付接口测试面试要点
  6. codeforces 1203 E Boxers 贪心
  7. 自注意力(Self-Attention)与Multi-Head Attention机制详解
  8. allergro音乐术语什么意思_意大利语音乐表情术语发音 allegro assai
  9. UE5在UI上播放视频带声音的解决方案
  10. Java数组与八大排序