C++面试总结

C++ 内存对齐

1)内存对齐的定义
数据项只能存储在地址是数据项大小整数倍的内存位置上。现代计算机中内存空间都是按照byte划分的,从理论上讲对任何类型变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
2) 使用原因
平台原因:不同硬件平台对存储空间的处理上存在很大的不同。某些平台对特定类型的数据只能从特定地址开始存取,而不允许其在内存中任意存放;
性能原因:为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次访问;如果不按照平台要求对存放数据进行对齐,会发生内存的二次访问,带来存取效率上的损失。
3)内存对齐的规则
 数据成员对齐规则:结构的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置,要从该成员大小的整数倍开始;
 结构体作为成员:如果结构里有某些结构体成员,则结构体成员要从其内部最大元素的整数倍地址开始存储;
 结构体的总大小,也就是sizeof的结果,必须是内部最大成员的整数倍,如果不是要补齐。

指针和引用

 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名;
 指针可以为空,引用不能为NULL且在定义时必须初始化;
 指针在初始化后可以改变指向,而引用在初始化之后不可再改变(引用的本质是一个指针常量,指向不可以更改,指针的值可以更改);
 指针可以有多级指针(**p),而引用只有一级。

在传递函数参数时,什么时候该使用指针,什么时候该使用引用呢?

 需要返回函数内局部变量内存的时候用指针。使用指针传参需要开辟内存,用完要记得释放指针,不然会内存泄漏。而返回局部变量的引用是没有意义的;
 对栈空间大小比较敏感(比如递归)的时候使用引用。使用引用传递不需要创建临时变量,开销要更小;
 类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式。

C++中的指针参数传递和引用参数传递有什么区别?底层原理你知道吗?

 1) 指针参数传递本质上是值传递,它所传递的是一个地址值。
值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。
值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。
 2) 引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。
被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。
因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。

值传递、指针传递、引用传递的区别和效率

  1. 值传递:有一个形参向函数所属的栈拷贝数据的过程,如果值传递的对象是类对象 或是大的结构体对象,将耗费一定的时间和空间。(传值)
  2. 指针传递:同样有一个形参向函数所属的栈拷贝数据的过程,但拷贝的数据是一个固定为4字节的地址。(传值,传递的是地址值)
  3. 引用传递:同样有上述的数据拷贝过程,但其是针对地址的,相当于为该数据所在的地址起了一个别名。(传地址)
  4. 效率上讲,指针传递和引用传递比值传递效率高。一般主张使用引用传递,代码逻辑上更加紧凑、清晰。

堆和栈的区别

管理方式
内存管理机制
空间大小
碎片问题
生长方向
分配效率
在这里插入图片描述
在这里插入图片描述

你觉得堆快一点还是栈快一点?

因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。
而堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。

指针数组和数组指针

int *p[10]:指针数组:首先它是一个数组,数组的元素都是指针。数组占多少个字节由数组本身的大小决定,每一个元素都是一个指针,在32 位系统下任何类型的指针永远是占4 个字节。
int(*p) [10]:数组指针:首先它是一个指针,它指向一个数组。它指向的数组占多少字节,具体要看数组大小。

指针函数和函数指针?

指针函数本质上是一个函数,函数的返回值是一个指针;
函数指针本质上是一个指针,C++在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址,有了函数指针后,就可用该指针变量调用函数。
char * fun(char *p) 指针函数fun
char * (*pf)(char *p) 函数指针pf
pf = fun 函数指针pf指向指针函数fun
pf§ 通过函数指针pf调用函数fun

new/delete与malloc/free的区别是什么?

1、new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持;
2、使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
3、new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
4、new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete 函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作

new和delete是如何实现的?

new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。
delete先调用析构函数,然后调用operator delete 函数释放内存(通常底层使用free实现)。

malloc申请的存储空间能用delete释放吗?

不能,malloc /free主要为了兼容C,new和delete 完全可以取代malloc /free的。
malloc /free的操作对象都是必须明确大小的,而且不能用在动态类上。
new 和delete会自动进行类型检查和大小,malloc/free不能执行构造函数与析构函数,所以动态对象它是不行的。
当然从理论上说使用malloc申请的内存是可以通过delete释放的。不过一般不这样写的。而且也不能保证每个C++的运行时都能正常。

delete和delete[]区别?

delete只会调用一次析构函数。
delete[]会调用数组中每个元素的析构函数,delete[]时,数组中的元素按逆序的顺序进行销毁。

delete是如何知道释放内存的大小的

在 new [] 一个对象数组时,需要保存数组的维度,在分配数组空间时多分配了 4 个字节,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。

既然有了malloc/free,C++中为什么还需要new/delete呢?直接用 malloc/free不好吗?

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

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

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

宏定义#define和函数有何区别?

 宏定义主要用于定义常量,在预处理阶段进行文本替换,没有返回值;函数调用具有返回值;
 宏定义不检查类型;函数参数需要检查类型;
 宏定义不是语句,不在最后加分号。

宏定义和typedef区别?

 宏定义主要用于定义常量,在预处理阶段进行文本替换,没有返回值;typedef主要用于定义类型别名,typedef是编译的一部分。
 宏定义不检查类型;typedef需要检查数据类型。
 宏定义不是语句,不在最后加分号。

define与inline的区别

 宏定义是关键字在预处理阶段进行文本替换;inline是函数在编译阶段进行替换;
 宏定义不检查类型;inline函数会检查数据类型。
 宏定义不是语句,不在最后加分号。

define宏定义和const的区别

 宏定义在预处理阶段进行文本替换,定义的数据没有分配内存空间;const定义的变量值不能改变,需要分配内存空间
 宏定义不检查类型;const会检查数据类型。
 宏定义不是语句,不在最后加分号

定义和声明的区别

如果是指变量的声明和定义:
从编译原理上来说,声明是仅仅告诉编译器,有个某类型的变量会被使用,但是编译器并不会为它分配任何内存。而定义就是分配了内存。
如果是指函数的声明和定义:
声明:一般在头文件里,对编译器说:这里我有一个函数叫function() 让编译器知道这个函数的存在。
定义:一般在源文件里,具体就是函数的实现过程 写明函数体。
注意:函数的声明对于函数位于主函数前后的顺序有影响

sizeof()和strlen()

sizeof是运算符
sizeof 对数组,得到整个数组所占空间大小;
sizeof 对指针,得到指针本身所占空间大小(4个字节);
当一个类A中没有声明任何成员变量与成员函数,这时sizeof(A)的值是1。
strlen() 是函数,
可以计算字符串的长度,直到遇到结束符NULL才结束,返回的长度大小不包含NULL。

常量指针和指针常量区别

指针常量就是指针本身是常量,指针里面所存储的内存地址是常量,不能改变。但是,内存地址所对应的内容是可以通过指针改变的。(指向不可以改,内容可以改)
常量指针就是指向常量的指针,指针指向的是常量,它指向的内容不能发生改变,不能通过指针来修改它指向的内容。但是,指针自身不是常量,它自身的值可以改变,从而指向另一个常量。(内容不可改,指向可以改)

C++和Python的区别

C++和C语言的区别

C++与Java的区别

C++中struct和class的区别

 struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
 它们最本质的一个区别就是:struct 访问权限默认是 public 的,class 默认是 private 的。

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

常量在C++里使用const关键字定义,常量定义必须初始化。
对于局部对象,常量存放在栈区;
对于全局对象,编译期一般不分配内存,放在符号表中以提高访问效率;
对于字面值常量,存放在常量存储区.

define宏定义和const的区别

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

Const关键字

 修饰变量,说明该变量不可以被修改
 修饰指针,即常量指针和指针常量
 常量引用,经常用于形参类型,既避免了拷贝,又避免了函数对值的修改
 修饰类的成员函数,说明该成员函数内不能修改成员变量

static关键字的作用

 静态变量在程序执行之前就创建,在程序执行的整个周期都存在。可以归类为如下五种:
局部静态变量:作用域仅在定义它的函数体或语句块内,该变量的内存只被分配一次,因此其值在下次函数被调用时仍维持上次的值;
全局静态变量:作用域仅在定义它的文件内,该变量也被分配在静态存储区内,在整个程序运行期间一直存在;
静态函数:在函数返回类型前加static,函数就定义为静态函数。静态函数只是在声明他的文件当中可见,不能被其他文件所用;
类的静态成员变量:在类中,静态成员变量属于整个类所拥有,所有对象共享同一份数据;在编译阶段分配内存;类内声明,类外初始化;
类的静态成员函数:在类中,静态成员变量属于整个类所拥有,所有对象共享同一个函数;静态成员函数只能访问静态成员变量。

final和override关键字

override
当在父类中使用了虚函数时候,你可能需要在某个子类中对这个虚函数进行重写,override指定了子类的这个虚函数是重写的父类的,如果你函数名字不小心打错了的话,编译器是不会编译通过的。
final
当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加 final关键字后被继承或重写,编译器会报错。
初始化和赋值的区别
对于简单类型来说,初始化和赋值没什么区别
对于类和复杂数据类型来说,这两者的区别就大了。

为什么要用extern c

在程序中加上extern "C"后,相当于告诉编译器这部分代码是C语言写的,因此要按照C语言进行编译,而不是C++。

野指针和悬空指针

都是是指向无效内存区域的指针,访问行为将会导致未定义行为。
野指针
野指针,指的是没有被初始化过的指针

悬空指针
悬空指针,指针最初指向的内存已经被释放了的一种指针。

产生原因及解决办法:
野指针:指针变量未及时初始化 => 定义指针变量及时初始化,要么置空。
悬空指针:指针free或delete之后没有及时置空 => 释放操作后立即置空。

什么时候会发生段错误?

段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况:
 使用了野指针
 试图修改字符串常量的内容
 数组越界导致栈溢出

C和C++的类型安全

(1)什么是类型安全?
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。
(2)C的类型安全
C只在局部上下文中表现出类型安全,比如试图从一种结构体的指针转换成另一种结构体的指针时,编译器将会报告错误,除非使用显式类型转换。
(3) C++的类型安全
C++比C更有类型安全性。相比于C语言,C++提供了一些新的机制保障类型安全:
 操作符new返回的指针类型严格与对象匹配,而不是void*
 引入const关键字代替#define constants,它是有类型、有作用域的,而#define constants只是简单的文本替换
 一些#define宏可被改写为inline函数,结合函数的重载,可在类型安全的前提下支持多种类型,当然
 改写为模板也能保证类型安全

C++中的函数重载、重写、隐藏和模板

重载:在同一作用域中,两个函数名相同,但是参数列表不同(个数、类型、顺序),返回值类型没有要求;
重写:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写;
隐藏:派生类中函数与基类中的函数同名,但是这个函数在基类中并没有被定义为虚函数,此时基类的函数会被隐藏;
模板:模板函数是一个通用函数,函数的类型和形参不直接指定而用虚拟类型来代表,只适用于参数个数相同而类型不同的函数。

构造函数和析构函数能不能被重载?

构造函数可以被重载,析构函数不可以被重载。因为构造函数可以有多个且可以带参数,而析构函数只能有一个,且不能带参数。

C++有哪几种的构造函数

两种分类方式
按照参数分类:有参构造和无参构造
按照类型分为:普通构造和拷贝构造

什么情况下会调用拷贝构造函数

 用类的一个实例化对象去初始化另一个对象的时候;
 当函数的返回值是类对象时;
 若函数的形参为类对象,调用函数时,实参赋值给形参。

拷贝构造函数中深拷贝和浅拷贝区别?

 浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。
深拷贝还开辟出一块新的空间用来存放新的值,两个指针指向不同的内存空间。即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝

拷贝构造函数和赋值运算符重载的区别?

 拷贝构造函数是函数,赋值运算符是运算符的重载;
 拷贝构造函数会生成新的类对象,赋值运算符不会;
 拷贝构造函数是用一个已存在的对象去构造一个不存在的对象;而赋值运算符重载函数是用一个存在的对象去给另一个已存在并初始化过的对象进行赋值。

inline内联函数是什么?

当一个函数被声明为内联函数之后,在编译阶段,编译器会用内联函数的函数体取替换程序中出现的内联函数调用表达式,而其他的函数都是在运行时才被替换,这其实就是用空间换时间,提高了函数调用的效率。同时,内联函数具有几个特点:
函数体内的代码不可较长,否则将导致内存消耗代价
函数体内不可出现有循环、递归操作,函数执行时间要比函数调用开销大
内联函数的定义必须出现在内联函数的第一次调用前;
类的成员函数(除了虚函数)会自动隐式的当成内联函数.

内联函数的优缺点

优点:
内联函数在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等操作,从而提高程序运行速度;
inline函数会检查数据类型,宏定义不检查类型;
内联函数在运行时可调试,而宏定义不可以;
在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能;
缺点:
代码膨胀,消耗了更多的内存空间;
inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接;
内联函数其实是不可控的,它只是对编译器的建议,是否对函数内联,决定权在于编译器

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

内联函数以代码复杂为代价,它以省去函数调用的开销来提高执行效率。所以一方面如果内联函数体内代码执行时间相比函数调用开销较大,则没有太大的意义;另一方面每一处内联函数的调用都要复制代码,消耗更多的内存空间,因此以下情况不宜使用内联函数:
函数体内的代码比较长,将导致内存消耗代价
函数体内有循环,函数执行时间要比函数调用开销大

C++的异常处理的方法

在程序执行过程中,由于程序员的疏忽或是系统资源紧张等因素都有可能导致异常,任何程序都无法保证绝对的稳定,常见的异常有:
 数组下标越界
 除法计算时除数为0
 动态分配空间时空间不足
C++ 异常处理涉及到三个关键字:try、catch、throw。
throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
catch: 在想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
try: try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。它后面通常跟着一个或多个 catch 块。

静态变量 什么时候初始化

初始化只有一次,但是可以多次赋值,在主程序之前,编译器已经为其分配好了内存。
静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存。
而在C++中,初始化时在执行相关代码时才会进行初始化,主要是由于C++引入对象后,要进行初始化必须执行
相应构造函数和析构函数。

类成员初始化方式?构造函数的执行顺序 ?为什么用成员初始化列表会快一些?

  1. 赋值初始化,通过在函数体内进行赋值初始化;**列表初始化,**在冒号后使用初始化列表进行初始化。
    这两种方式的主要区别在于:
    对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。
    列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。
  2. 一个派生类构造函数的执行顺序如下:
    ① 虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。
    ② 基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。
    ③ 类类型的成员对象的构造函数(按照初始化顺序)
    ④ 派生类自己的构造函数。
  3. 方法一是在构造函数当中做赋值的操作,而方法二是做纯粹的初始化操作。我们都知道,C++的赋值操作是会产生临时对象的。临时对象的出现会降低程序的效率。

有哪些情况必须用到成员列表初始化?作用是什么?

成员初始化列表的概念,为什么用它会快一些?

成员初始化列表的概念
在类的构造函数中,不在函数体内对成员变量赋值,而是在构造函数的花括号前面使用冒号和初始化列表赋值
效率
用初始化列表会快一些的原因是,对于类型,它少了一次调用构造函数的过程,而在函数体中赋值则会多一次调用。而对于内置数据类型则没有差别。

C++中新增了string,它与C语言中的 char *有什么区别吗?它是如何实现的

string可以进行动态扩展,在每次扩展的时候另外申请一块原空间大小两倍的空间(2^n),然后将原字
符串拷贝过去,并加上新增的内容。

强制类型转换运算符

static_cast
特点:静态转换,编译时执行。

static_cast < type-id > (expression)

应用场合:

  1. 主要用于C++中内置的基本数据类型之间的转换
  2. 基类与派生类之间的转换;
    向下转换父类转子类(不安全,没有提供运行时的类型检查(RTTI)–> pson = pfather(不行); pson = static_cast<cson*>(pfather)可行
    向上转换子类转父类(安全) pfather = pson(可行);pfather = static_cast<cfather*>(pson)可行
  3. 空指针转换为目标类型的空指针(void* p = nullptr // int* pN = static_cast< int* > (p) )

const_cast
特点:去常转换,编译时执行。

const_cast<type_id> (expression)

应用场合: const_cast可以用于修改类型的const或volatile属性,去除指向常数对象的指针或引用的常量性。

reinterpret_cast:
特点:类似于强制转换,编译时执行。
应用场合: 可以用于任意类型的指针之间的转换,对转换的结果不做任何保证。

dynamic_cast:
特点:动态类型转换,运行时执行。
应用场合:

  1. 用于有虚函数的基类和派生类之间指针或引用的转换。对于指针,转换失败则返回nullptr,对于引用,转换失败会抛出异常。非必要不要使用,有额外的函数开销。
    向下转换:父类转子类(不安全,没有提供运行时的类型检查(RTTI)–> pson = pfather(不行); pson = static_cast<cson*>(pfather)可行
    所以在进行向下转换的时,dynamic_cast具有类型检查的功能,比static_cast更安全
    向上转换:子类转父类(安全) pfather = pson(可行);pfather = dynamic_cast<cfather*>(pson)可行
    向上转换时static_cast和dynamic_cast效果是一样的。
  2. 具有类型检查功能

static_cast比C语言中的转换强在哪里?

  1. 更加安全;
  2. 更直接明显,能够一眼看出是什么类型转换为什么类型,容易找出程序中的错误;可清楚地辨别代 码中每个显式的强制转;可读性更好,能体现程序员的意图

请你说说你了解的RTTI

定义:RTTI(Run Time Type Identification)即通过运行时类型识别,程序能够使用基类的指针或引用来检查着这些指针或引用所指的对象的实际派生类型。

RTTI机制产生原因:C++是一种静态类型语言,其数据类型是在编译期就确定的,不能在运行时更改。然而由于面向对象程序设计中多态性的要求,C++中的指针或引用本身的类型,可能与它实际代表(指向或引用)的类型并不一致。有时我们需要将一个多态指针转换为其实际指向对象的类型,就需要知道运行时的类型信息,这就产生了运行时类型识别的要求。

C++中有两个函数用于运行时类型识别,分别是dynamic_cast和typeid,具体如下:

typeid函数返回一个对type_info类对象的引用,可以通过该类的成员函数获得指针和引用所指的实际类型;
dynamic_cast操作符,将基类类型的指针或引用安全地转换为其派生类类型的指针或引用。

C++函数调用的压栈过程

文字化表述
函数的调用过程:
1)从栈空间分配存储空间
2)从实参的存储空间复制值到形参栈空间
3)进行运算
形参在函数未调用之前都是没有分配存储空间的,在函数调用结束之后,形参弹出栈空间,清除形参空间。

写C++代码时有一类错误是 coredump ,很常见,你遇到过吗?怎么调试这个错误?

coredump是程序由于异常或者bug在运行时异常退出或者终止,在一定的条件下生成的一个叫做core的文件,这个core文件会记录程序在运行时的内存,寄存器状态,内存指针和函数堆栈信息等等。对这个文件进行分析可以定位到程序异常的时候对应的堆栈调用信息。

在这里插入图片描述

如何定义一个只能在堆上(栈上)生成对象的类?/类如何只实现静态分配和只能动态分配

只能在堆上
方法: 将析构函数设置为私有
原因:C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。
只能在栈上
方法:将 new 和 delete 重载为私有
原因: 在堆上生成对象,使用 new 关键词操作,其过程分为两阶段:第一阶段,使用 new 在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将 new 操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。

strcpy和memcpy的区别是什么吗

1、复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
2、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
3、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy

说一说strcpy、sprintf与memcpy这三个函数的不同之处

1) 操作对象不同
① strcpy的两个操作对象均为字符串
② sprintf的操作源对象可以是多种数据类型,目的操作对象是字符串
③ memcpy的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
2) 执行效率不同
memcpy最高,strcpy次之,sprintf的效率最低。
3) 实现功能不同
① strcpy主要实现字符串变量间的拷贝
② sprintf主要实现其他数据类型格式到字符串的转化
③ memcpy主要是内存块间的拷贝。

C++三种特性

 封装:把数据和操作绑定在一起封装成抽象的类,仅向用户暴露接口,而对其隐藏具体实现,以此避免外界干扰和不确定性访问;
 继承:让某种类型对象获得另一个类型对象的属性和方法,提高了代码的可维护性;
 多态:让同一事物体现出不同事物的状态,提高了代码的扩展性。

多态的分类

C++多态分为两类
静态多态就是重载,因为在编译期决议确定,所以称为静态多态。在编译时就可以确定函数地址。

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

多态的实现

在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据所指对象的实际类型来调用相应的函数,如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数。
多态满足条件
有继承关系
子类重写父类的虚函数
多态的使用条件
父类指针或引用指向子类对象

C++计算一个类的sizeof

一个空的类sizeof返回1,因为一个空类也要实例化,所谓类的实例化就是在内存中分配一块地址;
一个类如果含有虚函数,则这个类中有一个指向虚函数表的指针,占4个字节;
类内的普通成员函数不参与sizeof的统计,因为sizeof是针对实例的,而普通成员函数,是针对类体的;
静态成员不影响类的大小,被编译器放在程序的数据段中;
普通继承的类sizeof,会得到基类的大小加上派生类自身成员的大小;
当存在虚拟继承时,派生类中会有一个指向虚基类表的指针。所以其大小应为普通继承的大小,再加上虚基类表的指针大小。

一个空类class中有什么?

构造函数、拷贝构造函数、析构函数、赋值运算符重载、取地址操作符重载、被const修饰的取地址操作符重载

虚表指针、虚函数指针和虚函数表

虚函数:用virtual关键字申明的函数叫做虚函数。
虚函数(表)指针:指向虚函数表的一个指针。
虚函数表(虚表):存放虚函数地址的一个指针数组,如果子类实现了父类的某个虚函数,则在虚函数表中覆盖原本基类的那个虚函数指针。
虚表指针:虚表指针是对象的第一个数据成员。
注意:存在虚函数的类都有一个一维的虚函数表叫做虚表。每一个类的对象都有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。

析构函数是否需要是虚函数?

只有当一个类需要当作父类时,才将它的析构函数设置为虚函数。
多态在使用时,如果子类有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构函数,造成内存泄漏。将可能会被继承的父类的析构函数设置为虚函数,就可以保证当我们new一个子类,然后使用父类指针指向该子类对象,释放父类指针时可以释放掉子类的空间,防止内存泄漏;
C++默认的析构函数不是虚函数,着是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。
总结
虚析构和纯虚析构都是用来解决父类指针释放子类对象
如果子类没有堆区数据,可以不写虚析构和纯虚析构
拥有纯虚析构的类属于抽象类(无法实例化对象),纯虚析构需要声明也需要实现。

构造函数能否声明为虚函数或者纯虚函数

不能。虚函数对应一个虚函数表,类中存储一个虚表指针指向这个虚函数表。如果构造函数是虚函数,就需要通过虚函数表调用,可是对象没有初始化就没有虚表指针,无法找到虚函数表,所以构造函数不能是虚函数。

基类的虚函数表存放在内存的什么区,虚表指针vptr的初始化时间

一般分为五个区域:栈区、堆区、函数区(存放函数体等二进制代码)、全局静态区、常量区。
由于虚表指针vptr跟虚函数密不可分,对于有虚函数或者继承于拥有虚函数的基类,对该类进行实例化时,在构造函数执行时会对虚表指针进行初始化,并且存在对象内存布局的最前面。
C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。

为什么内联函数,构造函数,静态成员函数不能为virtual函数?

内联函数
内联函数是在编译时期展开,而虚函数的特性是运行时动态绑定,所以两者矛盾,不能定义内联函数为虚函数。
构造函数
构造函数用来创建一个新的对象,而虚函数的运行是建立在对象的基础上,在构造函数执行时,对象尚未形成,所以不能将构造函数定义为虚函数。
静态成员函数
静态成员函数属于一个类而非某一对象,没有this指针,它无法进行对象的判别。
友元函数
C++不支持友元函数的继承,对于没有继承性的函数没有虚函数。

静态函数和虚函数的区别

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

析构函数的作用,如何起作用?

  1. 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数又编译器自动调用,无需手动调用。
    规则,只要你一实例化对象,系统自动回调用一个构造函数就是你不写,编译器也自动调用一次。
  2. 析构函数:主要作用于对象销毁前系统自动调用,执行清理工作。
    特点:析构函数与构造函数同名,但该函数前面加~。

构造函数和析构函数可以调用虚函数吗,为什么?

  1. 在C++中,提倡不在构造函数和析构函数中调用虚函数;
  2. 构造函数和析构函数调用虚函数时都不使用动态联编,如果在构造函数或析构函数中调用虚函数,则
    运行的是为构造函数或析构函数自身类型定义的版本;
  3. 因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化,因此调用子类的虚函数
    时不安全的,故而C++不会进行动态联编;
  4. 析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析
    构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经销毁,这个时候再调用子类的虚函
    数没有任何意义。

构造函数、析构函数的执行顺序?构造函数和拷贝构造的内部都干了啥?

  1. 构造函数顺序
    ① 基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是
    它们在成员初始化表中的顺序。
    ② 成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,
    而不是它们出现在成员初始化表中的顺序。
    ③ 派生类构造函数。
  2. 析构函数顺序
    ① 调用派生类的析构函数;
    ② 调用成员类对象的析构函数;
    ③ 调用基类的析构函数。

类什么时候会析构?

  1. 对象生命周期结束,被销毁时;
  2. delete指向对象的指针时,或delete指向对象的基类类型指针,而其基类虚构函数是虚函数时;
  3. 对象A是对象B的成员,B的析构函数被调用时,对象A的析构函数也被调用。

构造函数的几种关键字

default
default关键字可以显式要求编译器生成合成构造函数,防止在调用时相关构造函数类型没有定义而报错。
delete
delete关键字可以删除构造函数、赋值运算符函数等
0
将虚函数定义为纯虚函数

构造函数、拷贝构造函数和赋值操作符的区别

构造函数
对象不存在,没用别的对象初始化,在创建一个新的对象时调用构造函数
拷贝构造函数
对象不存在,但是使用别的已经存在的对象来进行初始化
赋值运算符
对象存在,用别的对象给它赋值,这属于重载“=”号运算符的范畴,“=”号两侧的对象都是已存在的

拷贝构造函数和赋值运算符重载的区别?

  1. 拷贝构造函数是函数,赋值运算符是运算符重载。
  2. 拷贝构造函数会生成新的类对象,赋值运算符不能。
  3. 拷贝构造函数是直接构造一个新的类对象,所以在初始化对象前不需要检查源对象和新建对象是否相
    同;赋值运算符需要上述操作并提供两套不同的复制策略,另外赋值运算符中如果原来的对象有内存
    分配则需要先把内存释放掉。
  4. 形参传递是调用拷贝构造函数(调用的被赋值对象的拷贝构造函数),但并不是所有出现"="的地方
    都是使用赋值运算符

简单描述虚继承与虚基类?

定义:在C++中,在定义公共基类A的派生类B、C…的时候,如果在继承方式前使用关键字virtual对继承方式限定,这样的继承方式就是虚继承,公共基类A成为虚基类。这样,在具有公共基类的、使用了虚拟继承方式的多个派生类B、C…的公共派生类D中,该基类A的成员就只有一份拷贝。
作用:一个类有多个基类,这样的继承关系称为多继承。在多继承的情况下,如果不同基类的成员名称相同,匹配度相同, 则会造成二义性。为了避免多继承产生的二义性,在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员

抽象类、接口类、聚合类

抽象类:含有纯虚函数的类
接口类:仅含有纯虚函数的抽象类
聚合类:用户可以直接访问其成员,并且具有特殊的初始化语法形式。满足如下特点:1)所有成员都是 public;2)没有定义任何构造函数;3)没有类内初始化;4)没有基类,也没有 virtual 函数

8、虚表是什么时候生成的

base为基类,其中的函数为虚函数。子类1继承并重写了基类的函数,子类2继承基类但没有重写基类的函数,从结果分析子类体现了多态性,那么为什么会出现多态性,其底层的原理是什么?这里需要引出虚表和虚基表指针的概念。
虚表:虚函数表的缩写,类中含有virtual关键字修饰的方法时,编译器会自动生成虚表。
虚表指针:在含有虚函数的类实例化对象时,对象地址的前四个字节存储的指向虚表的指针。

9、基类的析构函数为什么是虚函数

由于类的多态性,基类指针可以指向派生类的对象,如果删除该基类的指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。所以将析构函数声明为虚函数是十分必要的。在实现多态时,当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生,要将基类的析构函数声明为虚函数。

STL容器有哪些?频繁插入应该用哪个

 vector:底层数据结构为数组。需要快速查找,不需要频繁插入/删除。
 list:底层数据结构为双向链表。需要频繁插入/删除,不需要快速查找。
 deque:底层数据结构为一个中央控制器和多个缓冲区,支持首尾(中间不能)快速增删,也支持随机访问。
 stack:底层一般用deque/list实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时。先进后出
 queue:底层一般用deque/list实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时。先进先出
 set:底层数据结构为红黑树,有序,不重复。
 multiset:底层数据结构为红黑树,有序,可重复。
 map:底层数据结构为红黑树,有序,不重复。
 multimap:底层数据结构为红黑树,有序,可重复。
 unordered_set:底层数据结构为hash表,无序,不重复。
 unordered_multiset:底层数据结构为hash表,无序,可重复 。
 unordered_map :底层数据结构为hash表,无序,不重复。
 unordered_multimap:底层数据结构为hash表,无序,可重复红黑树了解吗

Vector实现

vector是一种序列式容器,其数据安排以及操作方式与array非常类似,两者的唯一差别就是对于空间运用的灵活性,众所周知,array占用的是静态空间,一旦配置了就不可以改变大小,如果遇到空间不足的情况还要自行创建更大的空间,并手动将数据拷贝到新的空间中,再把原来的空间释放。vector则使用灵活的动态空间配置,维护一块连续的线性空间,在空间不足时,可以自动扩展空间容纳新元素,做到按需供给。其在扩充空间的过程中仍然需要经历:重新配置空间,移动数据,释放原空间等操作。这里需要说明一下动态扩容的规则:以原大小的两倍配置另外一块较大的空间。
需要注意的是,频繁对vector调用push_back()对性能是有影响的,这是因为每插入一个元素,如果空间够用的话还能直接插入,若空间不够用,则需要重新配置空间,移动数据,释放原空间等操作,对程序性能会造成一定的影响

Ifndef的作用

10、排序算法知道哪些?描述其中两种的思路
11、TCP和UDP的区别
12、TCP三次握手四次挥手RST状态出现的原因和解决方法
13、TCP粘包的原因,如何避免
14、七层网络模型 每一层都有哪些协议
15、select.poll和epoll 少量活跃进程应用应该使用哪个
16、Linux的常见命令
17、Gdb调试和coredump
18、进程和线程,进程之间的通信

二面
链表 双向链表、环形链表
select.poll和epoll
Multimap
值传递和地址传递
C和C++的区别
静态数据成员和静态成员函数
STL:map和vectorde 区别
重载和重写
虚函数和纯虚函数
TCP和UDP的区别
七层协议
数据库的基本操作
Gdb除了coredump还能调啥
项目

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值