问题网络收集,答案仅供参考,内容有误,可评论补充,后面有机会再更正!!!
目录
二、C/C++高频面试题
2.1 C/C++的相关概念面试题
2.1.1 new和malloc的区别⭐⭐⭐⭐⭐
2.1.2 malloc的底层实现⭐⭐⭐⭐
(1)Malloc实现原理:
2.1.3在1G内存的计算机中能否malloc(1.2G)?为什么?⭐⭐
2.1.4指针与引用的相同和区别;如何相互转换?⭐⭐⭐⭐⭐
(1)相同:
(2)区别:
(3)转换方法:
2.1.5 C语言内存情况 内存分配的方式⭐⭐⭐
(1)内存分配:
(2)程序的内存空间:
2.1.6 extern“C”的作用⭐⭐⭐
2.1.7 函数头文件的声明前加extern 与不加extern 有什么区别⭐⭐⭐⭐
2.1.8函数参数压栈顺序,即关于__stdcall和__cdecl调用方式的理解⭐⭐⭐
2.1.9重写memcpy()函数需要注意哪些问题⭐⭐
2.1.10数组到底存放在哪里⭐⭐⭐
2.1.11 C++中struct和class的区别 ⭐⭐⭐⭐⭐
2.1.12 char和int之间的转换;⭐⭐⭐
2.1.13 static的用法(定义和用途)⭐⭐⭐⭐⭐
(1)c/c++共有:
(2)c++独有:
2.1.14 const的用法(定义和用途)⭐⭐⭐⭐⭐
2.1.15 const常量和#define的区别,宏定义与全局变量区别⭐⭐⭐⭐
(1)const常量和#define的区别
(2)宏定义与全局变量区别
2.1.16 volatile作用和用法 ⭐⭐⭐⭐⭐
2.1.17有常量指针 指针常量 常量引用 没有 引用常量⭐⭐⭐
2.1.18没有指向引用的指针,因为引用是没有地址的,但是有指向指针的引用⭐⭐⭐
2.1.19 c/c++中变量的作用域⭐⭐⭐⭐⭐
2.1.20 c++中类型转换机制?各适用什么环境?dynamic_cast转换失败时,会出现什么情况?⭐⭐⭐⭐
2.2 继承、多态相关面试题 ⭐⭐⭐⭐⭐
2.2.1继承和虚继承 ⭐⭐⭐⭐⭐
2.2.2多态⭐⭐⭐⭐⭐
2.2.3被隐藏的基类函数如何调用或者子类调用父类的同名函数和父类成员变量 ⭐⭐⭐⭐⭐
(1)C++有两种方法可以调用被隐藏的函数:
(2)子类调用父类的同名函数:
2.2.4多态实现的三个条件、实现的原理 ⭐⭐⭐⭐⭐
2.2.5对拷贝构造函数 深浅拷贝 的理解?什么时候需要自定义拷贝构造函数?⭐⭐⭐
2.2.6析构函数可以抛出异常吗?为什么不能抛出异常?除了资源泄露,还有其他需考虑的因素吗?⭐⭐⭐
2.2.7什么情况下会调用拷贝构造函数(三种情况)⭐⭐⭐
2.2.8析构函数一般写成虚函数的原因⭐⭐⭐⭐⭐
2.2.9构造函数为什么一般不定义为虚函数⭐⭐⭐⭐⭐
2.2.10什么是纯虚函数⭐⭐⭐⭐⭐
2.2.11静态绑定和动态绑定的介绍⭐⭐⭐⭐
2.2.12 C++所有的构造函数 ⭐⭐⭐
2.2.13重写、重载、覆盖和隐藏的区别⭐⭐⭐⭐⭐
2.2.14成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)?⭐⭐⭐⭐
2.2.15如何避免编译器进行的隐式类型转换;(explicit)⭐⭐⭐⭐
补充C/C++:
1、友元函数
2、构造函数和析构函数
3、vector扩容机制、删除机制
(1)扩容机制
(2)STL容器vector的删除机制:
4、C++类的默认函数:
5、STL vector的底层实现
6、容器vector、链表list和双端队列deque的区别?
① new、delete是C++中独有的操作符。使用new创建对象在分配内存的时候会自动调用构造函数,同时也可以完成对对象的初始化,同理delete也能自动调用析构函数。
malloc和free是C/C++中的标准库函数。malloc和 free是库函数而不是运算符,不在编译器控制范围之内,所以不能够自动调用构造函数和析构函数。mallloc只是单纯为变量分配内存,free也只是释放变量的内存。
② new返回的是指定类型的指针,并且可以自动计算所申请内存的大小。
malloc返回的是void*类型,我们需要强行将其转换为实际类型的指针,并且需要指定好要申请内存的大小,malloc不会自动计算的。
③ C++允许重载new/delete操作符,而malloc和free是一个函数,不能重载。
④ new内存分配失败时,会抛出bad_alloc异常。malloc分配内存失败时返回空NULL。
⑤ 内存区域:先了解自由存储区和堆,两者不相等于的。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配。new操作符从自由存储区上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。
答:malloc是一个动态分配内存的函数,还可以通过free释放内存空间。了解一下虚拟内存空间
虚拟内存空间是操作系统实现内存管理的一种机制。操作系统为每个进程维护一个虚拟内存空间。操作系统会将虚拟内存和实际的物理内存进行映射,在CPU芯片上叫做存储器管理单元(Memory Management Unit,MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容是由操作系统管理。虚拟内存使得用户感觉内存空间时连续的。
我们可以看到虚拟内存空间的顶部是内核管理的内存空间,下面则是用户的内存空间,用户空间无权访问内核的内存空间。运行时堆上面有一块黑色区,这就是还未映射的内存。
在已经映射的内存空间结尾有一个break指针,这个指针下面是映射好的内存,可以访问,上面则是未映射的访问,不能访问。可以通过系统调用sbrk(位移量)移动break指针的位置,并返回指针位置,达到申请内存的目。brk(void *addr)系统调用可以直接将brk设置为某个地址,成功返回0,不成功返回-1。而rlimit则是限制进程堆内存容量的指针。
在操作系统角度来看,分配内存有两种方式:① 采用推进brk指针来增加堆的有效区域来申请内存空间。② 采用mmap在进程的虚拟地址空间中找一块空闲的虚拟内存。
这两种方式都是分配虚拟内存,只有当第一次访问虚拟地址空间时,操作系统给进程分配物理内存空间。malloc是采用brk的方式来动态分配内存。
malloc基本的实现原理就是维护一个内存空闲链表,当申请内存空间时,搜索内存空闲链表,找到适配的空闲内存空间,然后将空间分割成两个内存块,一个变成分配块给用户,一个变成新的空闲块并放回空闲链表。如果没有搜索到,那么就会用sbrk()才推进break指针来申请内存空间。调用 free 函数时,最终会将用户释放的内存块连接到空闲链表上。
最后,空闲链会被切成很多的小内存片段,如果用户申请一个大的内存片段,空闲链表上没有可以满足用户要求的片段。malloc()函数会被请求延时,并开始在空闲链表上检查各内存片段,对它们进行内存整理,将相邻的小空闲块合并成较大的内存块。
搜索空闲块最常见的算法有:首次适配,下一次适配,最佳适配。
首次适配:第一次找到足够大的内存块就分配,这种方法会产生很多的内存碎片。
下一次适配:也就是说等第二次找到足够大的内存块就分配,这样会产生比较少的内存碎片。
最佳适配:对堆进行彻底的搜索,从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块。
合并空闲块:在释放内存块后,如果不进行合并,那么相邻的空闲内存块还是相当于两个内存块,会形成一种假碎片。所以当释放内存后,我们需要将两个相邻的内存块进行合并。
(2)显式空闲链表:还有一种实现方式则是采用显示空闲链表,这个是真正的链表形式,在空闲块中使用指针连接空闲块。在之前的有效载荷中加入了前驱空闲块节点起始地址和后驱空闲块节点起始地址的两个指针,也可以称为双向链表。释放空闲链表的的方式第一种是用后进先出(LIFO),将新释放的块放置在链表的开始处。另一种方法是按照地址的顺序来维护。
答:是有可能申请1.2G的内存的。malloc能够申请的空间大小与物理内存的大小没有直接关系,仅与程序的虚拟地址空间相关。应用程序通过malloc函数可以向操作系统申请一块虚拟地址空间,得到的是在虚拟地址空间中的地址,之后程序运行所提供的物理内存是由操作系统完成的。
(我们要申请空间的大小为1.2G=2 30 × 1.2 Byte ,转换为十六进制约为 4CCC CCCC ,这个数值还在 unsigned int 的表示范围。 malloc 函数要求的参数正是unsigned int。综上,是有可能通过malloc( size_t ) 函数调用申请超过该机器物理内存大小的内存块的。)
①引用和指针一样是地址概念,所以本身都是会占用内存的(有的编译器优化后就不占用内存了)。
② 从内存分配上看:两者都是占内存的,程序为指针变量分配内存区域,在32位系统指针变量一般占用4字节内存。
① 指针是一个实体,指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名,引用本质是指针常量,所指向的对象不能改变,但指向的对象的值可以改变。
② 指针和引用的自增(++)运算意义不一样,指针是对内存地址的自增,引用是对值的自增;
量或对象的地址)的大小;
③ 引用使用时无需解引用(*),指针需要解引用;
④ 引用不能为空,指针可以为空;
⑤ 引用只能在定义时被初始化一次,之后不可变;指针可变;
⑥ 引用没有const,指针有const;(本人当初看到这句话表示疑问,这里解释一下:指针有“指针常量”即int * const a,但是引用没有int& const a,不过引用有“常引用”即const int &a = 1)
⑦ “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小,在32位系统指针变量一般占用4字节内存。
① 指针转引用:可以使用 & 操作符将指针转换为引用
② 引用转指针:可以使用 & 操作符将引用变量的地址赋值给指针
① 从静态存储区域分配:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
② 在栈上创建:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
③ 从堆上分配:亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
① 栈区(stack):由编译器自动分配释放,存放为运行函数而分配的局部变量、函数参数、返回数据、返回地址。其操作方式类似于数据结构中的栈。
② 堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
③ 全局区(静态区)(static):存放全局变量、静态数据、常量。程序结束后由系统释放。
④ 文字常量区:常量字符串就是放在这里的。程序结束后由系统释放。
⑤ 程序代码区:存放函数体(类成员函数和全局函数)的二进制代码。
extern "C"的主要作用是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。
注:由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
在C语言中,修饰符extern用在变量或者函数的声明前,进行显式声明,用来说明“此变量/函数是在别处定义的,要在此处引用,链接到定义它的文件中。
总结:标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。此外extern也可用来进行链接指定。
(1)__stdcall和__cdecl都是函数调用约定关键字。
(2)__stdcall:参数由右向左压入堆栈;堆栈由函数本身清理。
(3)__cdecl:参数也是由右向左压入堆栈;但堆栈由调用者清理。
需要考虑地址重叠的情况,标准库也提供了地址重叠时的内存拷贝函数:memmove(),但是该函数把源字符串拷贝到临时buf里,然后再从临时buf里写到目的地址,增加了一次不必要的开销。
数组是一种引用数据类型,数组引用变量只是一个引用,这个引用变量可以指向任何有效的内存,只有当该引用指向有效内存后,才可通过该数组变量来访问数组元素。引用变量是访问真实对象的根本方式。也就是说,如果我们希望在程序中访问数组,则只能通过这个数组的引用变量来访问它。
数组元素和数组变量在内存里是分开存放的。实际的数组元素被存储在堆(heap)内存中;数组引用变量是一个引用类型的变量,被存储在栈(stack)内存中。
在C语言中,struct 只能包含成员变量,不能包含成员函数。而在C++中,struct 类似于 class,既可以包含成员变量,又可以包含成员函数。
① 使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的。
② class 继承默认是 private 继承, struct 继承默认是 public 继承
③ class 可以使用模板,而 struct 不能
Char与int的相互转换,联想ASCII码,字符’0’对应的值为48,所以不能直接加减;
char ch = '9'; // char 转 int
int ch_int = ch - '0'; // 此时 ch_int = 9
int i = 9; // int 转 char
char i_ch = i + '0'; // 此时i_ch ='9'
① 在函数体,一个被声明为静态变量在这一函数被调用过程中维持其值不变(不会重复初始化)。可以修饰全局变量、局部变量和函数。
② 静态局部变量:使变量的生命周期与程序的整个执行过程相同,存储在全局数据区。但是作用域限制在定义该变量的函数内部,这意味该变量不能被其他函数访问,即使属于同一文件。
③ 静态全局变量:作用域限制在定义该变量的文件内部。不能被其他文件访问。避免不同文件中同名变量冲突。修饰后的变量只能在本文件访问,其他文件使用extern关键字无效
④ 静态函数:在函数前面加static。使某个函数只在进行定义的源文件中有效,不能被其他文件所用。此外,其他文件可以建立同名的函数,不互相冲突。
④ 修饰类的静态成员变量:表明对该类的所有对象,这个数据成员都只有一个实例。即该实例归所有对象共有。静态数据成员存储在全局数据区(地址固定)。即:被static修饰的变量,被static修饰的方法是类的静态资源,是类实例间共享的。可以通过类名.变量名直接引用,而不需要new出一个类来。非静态成员函数中可以调用静态成员。
⑤ 用static修饰静态成员函数:这个函数不可以访问非静态成员函数和非静态数据成员。这意味着一个静态成员函数只能访问它的参数、类的静态数据成员和全局变量。
const名叫常量限定符,用来限定特定变量,以通知编译器该变量是不可修改的。避免在函数中对某些不应修改的变量造成可能的改动。
(1)const修饰基本数据类型
(2)const应用到函数中
(3)const在类中的用法
(4)const修饰类对象,定义常量对象
① #define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用。
② #define只是简单的字符串替换,没有类型检查。而编译器会对const进行类型安全检查,可以避免一些低级的错误。
③ const常量有数据类型,而宏常量没有数据类型。
④ const常量可以进行调试的,#define是不能进行调试的,因为在预编译阶段就已经替换掉了。
⑤ #define只是进行展开,有多少地方使用,就替换多少次,它定义的宏常量在内存中有若干个备份(一改全改);const定义的只读变量在程序运行过程中只有一份备份。所以const可节省空间,避免不必要的内存分配,提高效率
⑥ define可以用#ifndef来防止头文件重复引用,而const不能。
① 宏定义不分配内存,全局变量定义需要分配内存;
② 宏定义不区分数据类型,本质上是一段字符,在预处理的时候被替换到引用的位置,而全局变量区分数据类型;
③ 宏定义之后值是不能改变的,全局变量的值是可以改变的;
④ 宏定义只有在定义所在文件,或引用所在文件的其它文件中使用。而全局变量可以在工程所有文件中使用,只需在使用前加一个声明exterm。
使用volatile关键字声明的变量或对象通常具有与优化、多线程相关的特殊属性。通常,volatile关键字用来阻止(伪)编译器对认为无法“被代码本身”改变的代码(变量/对象)进行优化。如在C语言中,volatile关键字可以用来提醒编译器它后面所定义的变量随时有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
总结:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不进行编译优化,以免出错。
优点:防止编译器对代码优化,变量值是直接从变量地址中读取和存储的。
缺点:这种使用过多会导致代码十分庞大。
(1)常量指针(const int* p):本质上是一个指针,是一个指向“常量”的指针,即不能通过指针改变指向对象的值(不能解除引用),但可以更改指向。
(2)指针常量(int* const p):本质上是一个常量,const是修饰p的,即指针的地址是一个常量,不可改变,始终指向一个地址,在创建的同时必须进行初始化
(3)常量引用(const int& a):本质上是一个引用,对常量的引用,不能通过引用改变绑定对象的值
答:因为引用不是对象,没有实际地址,所有不能定义指向引用的指针。*&r需要从右向左看,&表面r是一个引用,*表示引用的类型是一个指针。
作用域是一个变量的有效范围,它在哪儿创建,在哪儿销毁。作用域由变量所在的最近一对括号确定。
(1)全局变量不受作用域的影响。可被本文件及其它文件中的函数所共用,若其它文件中的函数调用此变量,须用extern声明
(2)局部变量出现在一个作用域内,它们是局限于定义的函数内。
(3)寄存器变量是一种局部变量。
(1)转换机制:C风格的转换、函数风格的转换、新式风格的转换。
(2) 在C++中强制类型转换通过关键字来完成,分别是static_cast、 dynamic_cast, const_cast、 和 reinterpret_cast 。
static_cast是静态转换。把 expression 转换为 type 类型,主要用于基本数据类型之间的转换,如把 uint 转换为 int,把 int 转换为 double 等。
dynamic_cast 是动态转换。主要用于类层次间的上行转换或下行转换。上行转换时,dynamic_cast 和 static_cast 的效果是一样的,但下行转换时,dynamic_cast 具有类型检查的功能。
const_cast是常量转换。用来修改 expression 的 const 或 volatile 属性。
reinterpret_cast可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针。
(3)由于dynamic_cast<>需要RTTI信息,如果编译时关闭了RTTI的情况下使用dynamic_cast<>则会报错,
C++面向对象三大特性:封装、继承和多态。
(1)继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属性和方法,被称为子类或者派生类,被继承的类称为父类或者基类。
继承特点:
① 子类拥有父类的所有属性和方法(除了构造函数和析构函数)。
② 子类可以拥有父类没有的属性和方法。
③ 子类是一种特殊的父类,可以用子类来代替父类。
④ 子类对象可以当做父类对象使用。(即父类对象=子类对象,赋值给父类)
(2)虚继承用于解决多继承条件下的菱形继承问题(数据冗余、存在二义性),使得在派生类中只保留一份间接基类的成员。一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针和虚基类表(也被称作虚基表);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为;应用程序不必为每一个派生类编写功能调用,只需要对基类进行处理即可,大大提高程序的可复用性。
继承中构成多态的两个条件:1、必须通过基类的指针或者引用调用虚函数;2、被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
注:虚函数的重写:派生类中有一个跟基类的完全相同虚函数,我们就称子类的虚函数重写了基类的虚函数,完全相同是指:函数名、参数、返回值都相同。另外虚函数的重写也叫作虚函数的覆盖。这样在使用派生类虚函数时,才不会调用到基类的同名类函数。
① 用using关键字
② 作用域操作符,可以调用基类中被隐藏的所有成员函数和变量。
① 子类和父类返回值参数相同,函数名相同,有virtual关键字,则由对象的类型决定调用哪个函数。
② 子类和父类只有函数名相同,没有virtual关键字,则子类的对象没有办法调用到父类的同名函数,父类的同名函数被隐藏了,可以使用作用域操作符强制调用父类的同名函数class::funtion_name或者如果在子类的定义中,使用using将父类的同名函数暴露using class::funtion_name,则可直接调用。
③ 子类和父类参数不同,函数名相同,有virtual关键字,则不存在多态性,子类的对象没有办法调用到父类的同名函数,父类的同名函数被隐藏了,只能强制调用父类的同名函数class::funtion_name。
④ 子类和父类返回值不同,参数相同,函数名相同,有virtual关键字,则编译出错error C2555编译器不允许函数名参数相同返回值不同的函数重载。
(1)三个条件:① 要有继承关系。② 调用函数的对象的类型必须是基类的指针或者引用。③ 基类的方法必须是虚函数,且派生类完成了虚函数的重写。
(2)多态的原理:在执行某个函数时,根据传进的对象不同,会根据对象模型中的虚函数指针找到对应的虚函数表,在虚函数表中找到对应要执行的虚函数指针,进而找到该虚函数并执行。
拷贝构造函数不会尝试改变传进来的参数,所以加const。参数不能是值传递
(1)深拷贝和浅拷贝的理解
对于普通成员,浅拷贝和深拷贝并没有区别。主要区别在于对于指针的拷贝。假设有一个A对象存在一个a指针,指向D内存。用A对象去初始化一个新的对象B,如果是浅拷贝,那么只会拷贝指针b=a,它们都指向D内存区域。如果是深拷贝,不仅拷贝指针b=a,还会申请一块新的内存空间E,原来的a指向D,新的b指向E。区别在于会不会申请新的内存空间。
(2)什么时候需要自定义拷贝构造函数?
前面提到浅拷贝和深拷贝的区别在于会不会复制新的内存空间(前提是类中存在指针类成员变量)。如果是浅拷贝的状态下,析构A对象,必然会释放D内存;析构B对象时,指针b同样指向D内存,此时D内存早已经被释放了。就出现了同一内存空间被释放两次的情况,系统就会报错。而类定义时,如果没有显式定义拷贝构造函数,就会调用默认拷贝构造函数,对应的是浅拷贝。这种情况就需要定义一个显示的拷贝构造函数实现深拷贝。
总结:当类存在指针类成员变量时,默认拷贝构造函数是浅拷贝,会导致二次析构问题,所以要自定义拷贝构造函数。
答:析构函数不能、也不应该抛出异常。
① 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
② 通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
(1)拷贝构造函数的作用和用途?
拷贝构造函数一般用于以下三种情况:
① 当用类的一个对象去初始化该类的另外一个对象时。
② 如果函数的形参是类的对象,调用此函数时,拷贝构造函数会被使用。(引用传递并不调用拷贝构造函数)
③ 如果函数的返回值是类的对象或引用时,函数执行完成返回调用者时,拷贝构造函数会被调用。
答:虚函数:就是在类的成员函数的前面加virtual关键字。
想要回收一个对象申请的资源时,需要调用析构函数。析构函数写成虚函数可以利用动态绑定的而特性,在释放资源的时候除了调用基类的析构函数,还调用派生类的析构函数。如果基类的析构函数不是虚函数的时候,删除派生类对象的时候,只会释放基类申请的资源,而不会释放派生类的资源,会造成内存泄漏。
(1)创建一个对象时需要确定对象的类型,而虚函数是在运行时动态确定其类型。构造一个对象时,由于对象还未创建成功,编译器无法确定对象的实际类型。若构造函数为虚函数,则创建对象永远无法成功。
(2)虚函数的调用需要虚函数表指针vptr,而该指针存放在对象的内存空间中,若构造函数声明为虚函数,那么对象还未创建,还没有内存空间,更没有虚函数表地址vtable用来调用虚构造函数。
(3)虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
(1)纯虚函数:在虚函数后面添加 =0 ,虚函数就成为纯虚函数纯虚函数声明。纯虚函数只有函数的名字,不具备函数的功能,不能被调用。纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对他进行定义。如果在基类中没有保留函数名字,则无法实现多态性。
注意:① 纯虚函数没有函数体;② 最后面的“=0”并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是虚函数”,而且是空的,需要派生类进行重写才能实现相关功能;③ 这是一个声明语句,最后有分号。
(2)虚函数和纯虚函数有什么区别?
① 虚函数定义形式:成员函数前添加 virtual 关键字,纯虚函数在虚函数后添加 =0 ;含有纯虚函数的类,称为抽象类;只含有虚函数的类,不能称为抽象类。
② 虚函数既可以直接使用,也可以被子类重载实现后,以多态的形式调用;而纯虚函数必须被子类重载实现,才能以多态的形式调用,因为纯虚函数在基类中只有声明。
相同:无论虚函数还是纯虚函数,定义中都不能有 static 关键字。因为 static 关键字修饰的内容在编译前就要确定,而虚函数、纯虚函数是在运行时动态绑定的。
注:抽象类:不用定义对象而只作为一种基本类型用作继承的类叫做抽象类(也叫接口类),凡是包含纯虚函数的类都是抽象类,抽象类的作用是作为一个类族的共同基类,为一个类族提供公共接口,抽象类不能实例化出对象。纯虚函数在派生类中重新定义以后,派生类才能实例化出对象。
静态绑定:绑定的是静态类型(对象在声明时采用的类型,在编译期既已确定),所对应的函数或属性依赖于对象的静态类型,发生在编译期。
动态绑定:绑定的是动态类型(通常是指一个指针或引用目前所指对象的类型,是在运行期决定的),所对应的函数或属性依赖于对象的动态类型,发生在运行期。
静态绑定和动态绑定的区别:
① 静态绑定发生在编译期,动态绑定发生在运行期;
② 对象的动态类型可以更改,但是静态类型无法更改;
③ 要想实现动态,必须使用动态绑定;
④ 在继承体系中只有虚函数使用的是动态绑定,其他的全部是静态绑定;
C++中的构造函数可以分为5类:(默认)无参构造函数、一般构造函数、拷贝构造函数、类型转换构造函数(可以将某个其他的数据转换成该类类型的对象)、赋值运算符的重载。
重写(覆盖):是值在派生类中重新对基类中的虚函数重新实现。即函数名和参数都一样,只是函数的实现体不一样
重载:重载是指在同一个类下,不同的函数使用相同的函数名,但是函数的参数个数或类型(参数列表)不同。调用的时候根据函数的参数来区别不同的函数。
隐藏:是指派生类的函数屏蔽了与其同名的基类函数。注意:只要是同名函数,不管参数列表是否相同,基类函数都会被隐藏。
重载和重写的区别
① 范围区别:重写和被重写的函数在不同的类中,重载和被重载的函数在同一类中。
② 参数区别:重写与被重写的函数参数列表相同,重载和被重载的函数参数列表不同。
③ virtual的区别:重写的基类函数必须要有virtual修饰,重载函数和被重载函数可以被virtual修饰,也可以没有。
隐藏与[重写、重载]的区别
① 与重载范围不同:隐藏函数和被隐藏函数在不同类中。
② 参数的区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被virtual修饰,基类函数都是被隐藏,而不是被重写。
(1)与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段,用于初始化类的成员。
(2)用初始化列表会快一些的原因是:少了一次调用拷贝构造函数的过程。而是采用调用赋值函数
答:在构造函数声明的时候加上explicit关键字,能够禁止隐式类型转换。
explicit作用:用来声明类构造函数是显示调用的,而非隐式调用,可以阻止调用构造函数时进行隐式转换。只可用于修饰单参构造函数,因为无参构造函数和多参构造函数本身就是显示调用的,再加上 explicit 关键字也没有什么意义。
答:让一个类中的私有成员可以被其他函数或者类访问,采用关键字friend实现。
构造函数:构造函数是一种特殊的成员函数,不需要用户来调用,定义类对象时被自动执行初始化。如果没有定义构造函数,系统会自动定义一个默认的无参构造函数,它不对成员属性做任何操作,如果我们自己定义了构造函数,系统就不会创建默认构造函数。(可以重载)
析构函数:当类对象的生命周期结束时,会自动的调用。析构函数的作用并不是删除对象,在对象撤销它所占用的内存之前,做一些清理的工作。清理后,这部分内存被系统回收再利用。在设计这个类时,系统默认提供一个析构函数。在对象的生命周期结束的时候,程序就会自动执行析构函数来完成这些工作。(没有参数,不能重载,一个类只能有一个析构)
当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:
① 完全弃用现有的内存空间,重新申请更大的内存空间;
② 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
③ 最后将旧的内存空间释放。
reserve扩容机制:对于容器里的元素数量进行判断,当容器的存储数量已经达到容量时,那么就需要进行一个倍增扩容了。扩容流程为:申请新的内存空间(空间大小为原空间的2倍或1.5倍)—> 把原空间的元素拷贝到新的空间里 —> 释放原空间 —> 数组指针指向新空间。例如:a.reserve(100); //将a的容量(capacity)扩充至100,也就是说现在测试a.capacity();的时候返回值是100.
remove()函数并不会改变vector的大小,只是将所有被删除的元素用下一个未被删除的元素覆盖掉,同时返回一个迭代器。在该迭代器之前的所有元素,保留原容器的顺序,并且不存在被删除的元素,而该迭代器到容器的末尾则不变。
erase()函数会真正地删除元素,并将后面的元素向前移动来填补空缺,同时改变vector的大小,返回一个指向被删除元素的下一个元素的迭代器。clear可以清空容器中的元素
构造函数、析构函数、拷贝构造函数、赋值运算符函数重载、取址运算符函数重载和const修饰的取址操作符重载函数
vector底层所采用的数据结构是一段连续的线性内存空间。通过3个迭代器可以表示出一个 vector 容器。Myfirst 指向的是 vector 容器对象的起始字节位置;_Mylast 指向当前最后一个元素的末尾字节;_myend 指向整个 vector 容器所占用内存空间的末尾字节。
Vector:连续存储的线性内存空间,动态数组,在堆上分配空间。
List:动态链表,在堆上分配空间,每插入一个元素都会分配空间,每删除一个元素都会释放空间。
Deque:双端队列, 动态数组管理元素,提供随机存取,有和vector几乎一样的接口。不同的是deque的动态数组头尾都开放,因此能在头尾两端进行快速安插和删除。
区别:
① vector底层实现是数组;list是双向链表。
② vector支持随机访问,list不支持。
③ vector是顺序内存,list不是。
④ vector在中间节点进行插入删除会导致内存拷贝,list不会。
⑤ vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
⑥ vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。
注:只有需要在首端进行插入/删除操作的时候,还要兼顾随机访问效率(或者说若既需要随机插入/删除,又需要随机访问)才选择deque,否则都选择vector。