C++基础【整理版】

C++基础【整理版】


2021年6月18日第一版

第一部分 内存管理

常量的储存位置

常量在C++里的定义就是一个top-level const加上对象类型,常量定义必须初始化。对于局部对象,常量存放在栈区,对于全局对象,常量存放在全局/静态存储区。对于字面值常量,常量存放在常量存储区。

字节大小

类型32位64位
char11
short22
int4大多数为4,少数为8
long48
float48
double48
指针48

大小端模式

大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。
小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。

代码判断大小端

union checkCPU
{
    int a;  //4 bytes
    char b; //1 byte
} c;
c.a = 1;
if (c.b == 1)
    printf("It is Little_endian!\n");
else
    printf("It is Big_endian!\n");

1 在c中,联合体(共用体)的数据成员都是从低地址开始存放。
2 若是小端模式,由低地址到高地址c.a存放为0x01 00 00 00,c.b被赋值为0x01;若是大端模式,由低地址到高地址c.a存放为0x00 00 00 01,c.b被赋值为0x00;
image
根据c.b的值的情况就可以判断cpu的模式了。

大小端转换

#define T(x) (((x&0xff)<<24)|((x&0xff00)<<8)|((x&0xff0000)>>8)|((x&0xff000000)>>24))      //1.四个字节的排放顺序要弄清楚


void transfer(int x)
{
    char a,b,c,d;
   a=(char)(x&0xff);
   b=(char)((x&0xff00)>>8);                         //   2. 字符类型转换的优先级高于移位,所以用括号把移位操作括起来~
   c=(char)((x&0xff0000)>>16);
   d=(char)((x&0xff000000)>>24);
   //printf("0x%x 0x%x 0x%x 0x%x\n",a,b,c,d);
    x=(a<<24)|(b<<16)|(c<<8)|d;
   printf("after transfered x is 0x%x\n",x);
}

C内存分布*

1、栈区(stack)。由编译器自动分配释放 ,存放函数的参数值,局部变量的值,返回地址等。其操作方式类似于数据结构中的栈。
2、堆区(heap)。动态申请的内存空间,就是由malloc分配的内存块,一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3、全局区(静态区)(.bss段和.data段)。存放全局变量和静态变量,初始化的全局变量和静态变量在.data段中, 未初始化的全局变量和未初始化的静态变量放在.bass段中。,程序结束后系统自动释放。
4、常量储存区(.data段) 。存放的是常量不允许修改, 程序结束后由系统释放
5、代码区(.text段)。存放代码,不允许修改,但是可以执行,编译后的二进制文件存放在这里。

C++内存分布*

32bitCPU可寻址4G线性空间,每个进程都有各自独立的4G逻辑地址,其中03G是用户态空间,34G是内核空间,不同进程相同的逻辑地址会映射到不同的物理地址中。其逻辑地址其划分如下:

栈区(stack)。使用栈空间存储函数的返回地址、参数、局部变量、返回值,从高地址向低地址增长。在创建进程时会有一个最大栈大小,Linux可以通过ulimit命令指定。
堆区(heap)。动态申请的内存空间,就是由malloc分配的内存块,一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。

text segment(代码段):包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
data segment(数据段):存储程序中已初始化的全局变量和静态变量。
bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量,对于未初始化的全局变量和静态变量,程序运行main之前时会统一清零。即未初始化的全局变量编译器会初始化为0
heap(堆): 当进程未调用malloc时是没有堆段的,只有调用malloc时采用分配一个堆,并且在程序运行过程中可以动态增加堆大小(移动break指针),从低地址向高地址增长。分配小内存时使用该区域。 堆的起始地址由mm_struct 结构体中的start_brk标识,结束地址由brk标识。
memory mapping segment(映射区):存储动态链接库等文件映射、申请大内存(malloc时调用mmap函数)
stack(栈):使用栈空间存储函数的返回地址、参数、局部变量、返回值,从高地址向低地址增长。在创建进程时会有一个最大栈大小,Linux可以通过ulimit命令指定。

在这里插入图片描述

堆和栈的区别*

  • 申请方式: 栈是系统自动分配,堆是程序员主动申请。
  • 申请后系统响应:分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败栈溢出;申请堆空间,堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表),在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中该块空间的首地址存放的是本次分配空间的大小,便于释放,将该块空间上的剩余空间再次连接在空闲链表上。
  • 栈在内存中是连续的一块空间(向低地址扩展) 最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的
  • 申请效率:栈是有系统自动分配,申请效率高,但程序员无法控制;堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片。
  • 存放的内容:栈中存放的是局部变量,函数的参数;堆中存放的内容由程序员控制。

栈效率高的原因

栈是操作系统提供的数据结构,计算机底层对栈提供了一系列支持:分配专门的寄存器存储栈的地址,压栈和入栈有专门的指令执行;而堆是由C/C++函数库提供的,机制复杂,需要一些列分配内存、合并内存和释放内存的算法,因此效率较低。

内存对齐*

参考地址
什么是内存对齐:编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中。

内存对齐的原则

  • 第一个成员在与结构体变量偏移量为0的地址处。(即结构体的首地址处,即对齐到0处) 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
  • 结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除;
  • 结构体每个成员相对于结构体首地址的偏移量 (offset) 都是该成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在成员之间加上填充字节 (internal padding);
  • 结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在最末一个成员之后加上填充字节 (trailing padding)。
  • 结构体的总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  • 对齐数 = 该结构体成员变量自身的大小与编译器默认的一个对齐数的较小值。

注:VS中的默认对齐数为8,不是所有编译器都有默认对齐数,当编译器没有默认对齐数的时候,成员变量的大小就是该成员的对齐数。

为什么要内存对齐?
主要包括两个原因:
1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

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

内存对齐的优点

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

内存泄漏*

内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的分类:

  1. 堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.

  2. 系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。

  3. 没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。

如何防止内存泄漏*

对于内存泄露,我的个人理解就是程序在运行过程中,自己开辟了空间,用完这块空间后却没有释放。

  • 内部封装:将内存的分配和释放封装到类中,在构造的时候申请内存,析构的时候释放内存。
  • C++中的智能指针是C++中已经对内存泄漏封装好了一个工具,可以直接拿来使用。
  • linux环境下的内存泄漏检查工具Valgrind,另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。

调试内存泄漏

  • 动态分析工具 valgrind, 读写已经释放的内存
  • 读写内存块越界(从前或者从后)
  • 使用还未初始化的变量 将无意义的参数传递给系统调用 内存泄漏

内存泄漏的工具

valgrind ./a.out

段错误

段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况:

  • 访问不存在的内存地址,非关联化一个空指针,使用野指针。
  • 访问系统的保护内存
  • 访问系统的只读内存。试图修改字符串常量的内容。
  • 内存越界。数组越界,变量类型不一致。

double free

Double Free其实就是同一个指针free两次。虽然一般把它叫做double free。其实只要是free一个指向堆内存的指针都有可能产生可以利用的漏洞。

double free的原理其实和堆溢出的原理差不多,都是通过unlink这个双向链表删除的宏来利用的。只是double free需要由自己来伪造整个chunk并且欺骗操作系统

new 和 malloc 的区别,delete 和 free 的区别*

  • malloc、free 是库函数,而new、delete 是关键字。
  • new 申请空间时,无需指定分配空间的大小,编译器会根据类型自行计算;malloc 在申请空间时,需要确定所申请空间的大小。
  • new 申请空间时,返回的类型是对象的指针类型,无需强制类型转换,是类型安全的操作符;malloc 申请空间时,返回的是 void* 类型,需要进行强制类型的转换,转换为对象类型的指针。
  • new 分配失败时,会抛出 bad_malloc 异常,malloc 分配失败时返回空指针。
  • new不仅分配一段内存,而且会调用构造函数,malloc不会。
  • new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。
  • new 操作符从自由存储区上为对象动态分配内存,而 malloc 函数从堆上动态分配内存。(自由存储区不等于堆)

相同点:都可用于申请动态内存和释放内存

注意: delete和free被调用后,内存不会立即回收,指针也不会指向空,delete或free仅仅是告诉操作系统,这一块内存被释放了,可以用作其他用途。但是由于没有重新对这块内存进行写操作,所以内存中的变量数值并没有发生变化,这时候就会出现野指针的情况。因此,释放完内存后,应该把指针指向NULL。

C++如何申请释放内存

关于malloc申请内存问题*

malloc能够申请的空间大小与物理内存的大小没有直接关系,仅与程序的虚拟地址空间相关。程序运行时,堆空间只是程序向操作系统申请划出来的一大块虚拟地址空间。应用程序通过malloc申请空间,得到的是在虚拟地址空间中的地址,之后程序运行所提供的物理内存是由操作系统完成的(内存映射)。

malloc最大的申请空间大小

malloc是从系统获取内存分页,然后将这些分页组织为不同大小的“块”。

  • malloc可以分配的空间的最大大小,首先当然受机器的bit数,也就是可以直接寻址的空间大小制约;32位Linux是用户3G+内核1G。
  • 然后受实际可用资源量的制约。剩余内存,当前虚拟空间的碎片情况。

32位Linux是用户3G+内核1G
限制因素

  1. Lib C库的实现
  2. 操作系统
  3. 硬件
  4. 当前内存的使用状况。

new和delete*

参考地址
new 在申请基本类型空间时,主要会经历两个过程:
(1)调用 operator new(size_t) 或 operator new[] (size_t) 申请空间;
(2)进行强制类型转换(代码如下)

new 在申请 object 空间时,主要会经历三个过程:
(1)调用 operator new(size_t) 或 operator new[] (size_t) 申请空间;
(2)进行强制类型转换;
(3)调用类的构造函数(代码如下)
delete的基本用法:
(1)调用类的析构函数
(2)调用 operator delete(void*); 或 operator delete[] (void*) 释放内存

delete 实现原理?delete 和 delete[] 的区别?*

delete 的实现原理:

  • 首先执行该对象所属类的析构函数;
  • 进而通过调用 operator delete 的标准库函数来释放所占的内存空间。

delete 和 delete [] 的区别:

  • delete 用来释放单个对象所占的空间,只会调用一次析构函数;
  • delete [] 用来释放数组空间,会对数组中的每个成员都调用一次析构函数。

malloc 的原理?malloc 的底层实现?*

malloc 的原理:
Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。
当申请内存小于128K时,会使用系统函数brk在堆区中分配;
当申请内存大于128K时,会使用系统函数mmap在映射区分配。

malloc内存分配原理
malloc基本的实现原理就是维护一个内存空闲链表,当申请内存空间时,搜索内存空闲链表,找到适配的空闲内存空间,然后将空间分割成两个内存块,一个变成分配块,一个变成新的空闲块。如果没有搜索到,那么就会用sbrk()才推进brk指针来申请内存空间。

Malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。

搜索空闲块最常见的算法有:首次适配,下一次适配,最佳适配。

首次适配:第一次找到足够大的内存块就分配,这种方法会产生很多的内存碎片。
下一次适配:也就是说等第二次找到足够大的内存块就分配,这样会产生比较少的内存碎片。
最佳适配:对堆进行彻底的搜索,从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块。

合并空闲块
在释放内存块后,如果不进行合并,那么相邻的空闲内存块还是相当于两个内存块,会形成一种假碎片。所以当释放内存后,我们需要将两个相邻的内存块进行合并。

显式空闲链表
还有一种实现方式则是采用显示空闲链表,这个是真正的链表形式。在之前的有效载荷中加入了之前前驱和后驱的指针,也可以称为双向链表。维护空闲链表的的方式第一种是用后进先出(LIFO),将新释放的块放置在链表的开始处。另一种方法是按照地址的顺序来维护。

malloc 的底层实现:
brk() 函数实现原理:向高地址的方向移动指向数据段的高地址的指针 _enddata。
mmap 内存映射原理

  • 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域;
  • 调用内核空间的系统调用函数 mmap(),实现文件物理地址和进程虚拟地址的一一映射关系;
  • 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝。

内存映射mmp机制

mmap将一个文件或者其它对象映射进内存。

void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

在这里插入图片描述
mmap()会返回一个指针ptr,它指向进程逻辑地址空间中的一个地址,这样以后,进程无需再调用read或write对文件进行读写,而只需要通过ptr就能够操作文件。但是ptr所指向的是一个逻辑地址,要操作其中的数据,必须通过MMU将逻辑地址转换成物理地址,如图1中过程2所示。这个过程与内存映射无关。

建立内存映射并没有实际拷贝数据,这时,MMU在地址映射表中是无法找到与ptr相对应的物理地址的,也就是MMU失败,将产生一个缺页中断,缺页中断的中断响应函数会在swap中寻找相对应的页面,如果找不到(也就是该文件从来没有被读入内存的情况),则会通过mmap()建立的映射关系,从硬盘上将文件读取到物理内存中,如图1中过程3所示。这个过程与内存映射无关。

如果在拷贝数据时,发现物理内存不够用,则会通过虚拟内存机制(swap)将暂时不用的物理页面交换到硬盘上,如图1中过程4所示。这个过程也与内存映射无关。

Swap分区在系统的物理内存不够用的时候,把硬盘内存中的一部分空间释放出来,以供当前运行的程序使用。

内存映射的步骤:

用open系统调用打开文件, 并返回描述符fd.
用mmap建立内存映射, 并返回映射首地址指针start.
对映射(文件)进行各种操作, 显示(printf), 修改(sprintf).
用munmap(void *start, size_t lenght)(解除内存映射)关闭内存映射.
用close系统调用关闭文件fd.

mmp函数的用途

1、将一个普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,这样用内存读写取代I/O读写,以获得较高的性能;
2、将特殊文件进行匿名内存映射,可以为关联进程提供共享内存空间;
3、为无关联的进程提供共享内存空间,一般也是将一个普通文件映射到内存中。

缺页异常

缺页异常:CPU通过地址总线可以访问连接在地址总线上的所有外设,包括物理内存、IO设备等等,但从CPU发出的访问地址并非是这些外设在地址总线上的物理地址,而是一个虚拟地址,由MMU将虚拟地址转换成物理地址再从地址总线上发出,MMU上的这种虚拟地址和物理地址的转换关系是需要创建的,并且MMU还可以设置这个物理页是否可以进行写操作,当没有创建一个虚拟地址到物理地址的映射,或者创建了这样的映射,但那个物理页不可写的时候,MMU将会通知CPU产生了一个缺页异常。

缺页异常的几种情况
1、当MMU中确实没有创建虚拟页物理页映射关系,并且在该虚拟地址之后再没有当前进程的线性区vma的时候,可以肯定这是一个编码错误,这将杀掉该进程;
2、当MMU中确实没有创建虚拟页物理页映射关系,并且在该虚拟地址之后存在当前进程的线性区vma的时候,这很可能是缺页异常,并且可能是栈溢出导致的缺页异常;
3、当使用malloc/mmap等希望访问物理空间的库函数/系统调用后,由于linux并未真正给新创建的vma映射物理页,此时若先进行写操作,将如上面的2的情况产生缺页异常,若先进行读操作虽也会产生缺页异常,将被映射给默认的零页(zero_pfn),等再进行写操作时,仍会产生缺页异常,这次必须分配物理页了,进入写时复制的流程;
4、当使用fork等系统调用创建子进程时,子进程不论有无自己的vma,“它的”vma都有对于物理页的映射,但它们共同映射的这些物理页属性为只读,即linux并未给子进程真正分配物理页,当父子进程任何一方要写相应物理页时,导致缺页异常的写时复制;

描述内存分配方式以及它们的区别*

  • 1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
  • 2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。
  • 3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。

内存分布(ELF)*

ELF(Executeable and Linkable Format,可执行与可链接格式)
在这里插入图片描述
.text:放编译好的二进制可执行代码
.data:已经初始化好的全局变量
.rodata:只读数据,例如字符串常量、const 的变量、虚函数表
.bss:未初始化全局变量,运行时会置 0
.symtab:符号表,记录的则是函数和变量
.strtab:字符串表、字符串常量和变量名

内存分配、管理、RAII*

,英文是 heap,在内存管理的语境下,指的是动态分配内存的区域。这个堆跟数据结构里的堆不是一回事。这里的内存,被分配之后需要手工释放,否则,就会造成内存泄漏
C++ 标准里一个相关概念是自由存储区,英文是 free store,特指使用 new 和 delete 来分配和释放内存的区域。一般而言,这是堆的一个子集:

  • new 和 delete 操作的区域是 free store
  • malloc 和 free 操作的区域是 heap

事实说明,漏掉 delete 是一种常见的情况,这叫“内存泄漏。

,英文是 stack,在内存管理的语境下,指的是函数调用过程中产生的本地变量和调用数据的区域。

重要的概念: 栈上的变量即使发生了异常,其析构函数也会被编译器调用,这个过程被称为栈展开

栈展开: 栈展开(stack unwinding)是指,如果在一个函数内部抛出异常,而此异常并未在该函数内部被捕捉,就将导致该函数的运行在抛出异常处结束,所有已经分配在栈上的局部变量都要被释放。
栈展开百度百科

危害:在栈展开的过程中,如果被释放的局部变量中有指针,而该指针在此前已经用new运算申请了空间,就有可能导致内存泄露。因为栈展开的时候并不会自动对指针变量执行delete(或delete[])操作。

stack overflow,并举个简单例子导致栈溢出

栈溢出概念:
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致栈中与其相邻的变量的值被改变。

栈溢出的原因:

  1. 局部数组过大。当函数内部的数组过大时,有可能导致堆栈溢出。局部变量是存储在栈中的,因此这个很好理解。解决这类问题的办法有两个,一是增大栈空间,二是改用动态分配,使用堆(heap)而不是栈(stack)。
  2. 递归调用层次太多。递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。
  3. 指针或数组越界。这种情况最常见,例如进行字符串拷贝,或处理用户输入等等。

RAII

RAII全称是“Resource Acquisition is Initialization”,直译过来是“资源获取即初始化”,也就是说在构造函数中申请分配资源,在析构函数中释放资源。是C++中管理资源、避免内存泄露的方法。因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。

智能指针(std::shared_ptr和std::unique_ptr)即RAII最具代表的实现,使用智能指针,可以实现自动的内存管理,再也不需要担心忘记delete造成的内存泄漏。毫不夸张的来讲,有了智能指针,代码中几乎不需要再出现delete了

RAII的用法:

C++ 支持将对象存储在栈上面。但是,在很多情况下,对象不能,或不应该,存储在栈上。比如:

  • 对象很大;
  • 对象的大小在编译时不能确定;
  • 对象是函数的返回值,但由于特殊的原因,不应使用对象的值返回

总结: 使用基于栈和析构函数的 RAII,可以有效地对包括堆内存在内的系统资源进行统一管理。

第二部分 构造函数与析构函数

深拷贝和浅拷贝的区别*

参考地址
深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是引用。

浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针

当类的成员变量中有指针变量时,最好使用深拷贝。因为当两个对象指向同一块内存空间,如果使用浅拷贝,当其中一个对象删除后,该块内存空间就会被释放,另外一个对象指向的就是垃圾内存。

浅拷贝带来的问题及如何解决
浅拷贝带来问题的本质在于析构函数释放多次堆内存,使用std::shared_ptr,可以完美解决这个问题。

什么时候调用拷贝构造函数

  • 复制对象时,以一个对象初始化另一个对象;
  • 当函数的参数为对象时,实参传递给形参的实际上是实参的一个拷贝对象,系统自动通过拷贝构造函数实现;
  • 当函数的返回值为一个对象时,该对象实际上是函数内对象的一个拷贝,用于返回函数调用处。

什么时候调用赋值重载运算符

用一个已存在的对象赋值给相同类型的已存在对象。(浅拷贝)

基类的析构函数不是虚函数,会带来什么问题?*

  • 当基类指针指向派生类的时候,如果析构函数不声明为虚函数,在析构的时候,只调用了基类的析构函数,不会调用派生类的析构函数,从而导致内存泄露
  • 那么什么时候才要用虚析构函数呢?通常情况下,程序员的经验是,当类中存在虚函数时要把析构函数写成virtual,因为类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时如果派生类的构造函数中有用new动态产生的内存,那么在其析构函数中务必要delete这个数据,但是一般的像以上这种程序,这种操作只调用了基类的析构函数,而标记成虚析构函数的话,系统会先调用派生类的析构函数,再调用基类本身的析构函数。
  • 一般情况下,在类中有指针成员的时候要写copy构造函数,赋值操作符重载和析构函数。

为什么构造函数一般不定义为虚函数?而析构函数定义为虚函数?*

  1. 存储空间角度:构造函数是在实例化对象的时候进行调用,如果此时将构造函数定义成虚函数,需要通过访问该对象所在的内存空间才能进行虚函数的调用(因为需要通过指向虚函数表的指针调用虚函数表,虽然虚函数表在编译时就有了,但是没有虚函数的指针,虚函数的指针只有在创建了对象才有),但是此时该对象还未创建,便无法进行虚函数的调用。所以构造函数不能定义成虚函数。
  2. 使用的角度考虑:虚函数是基类的指针指向派生类的对象时,通过该指针实现对派生类的虚函数的调用,构造函数是在创建对象时自动调用的。
  3. 从实现上考虑:虚函数表是在创建对象之后才有的,因此不能定义成虚函数。
  4. 类型上考虑,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数。

析构函数一般定义成虚函数,原因:
析构函数定义成虚函数是为了防止内存泄漏,因为当基类的指针或者引用指向或绑定到派生类的对象时,如果未将基类的析构函数定义成虚函数,会调用基类的析构函数,那么只能将基类的成员所占的空间释放掉,派生类中特有的就会无法释放内存空间导致内存泄漏。

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

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

必须在构造函数初始化列表里进行初始化的数据成员有哪些?

常量类型,引用类型,对象成员。

C++中构造函数发生异常会怎样?

  • 构造函数中抛出异常,会导致析构函数不能被调用,但对象本身已申请到的内存资源会被系统释放(已申请到资源的内部成员变量会被系统依次逆序调用其析构函数)
  • 因为析构函数不能被调用,所以可能会造成内存泄露或系统资源未被释放。
  • 构造函数中可以抛出异常,但必须保证在构造函数抛出异常之前,把系统资源释放掉,防止内存泄露。(如何保证???使用auto_ptr???)

构造函数抛出的异常的四种情况

1:构造函数的初始化列表里抛异常,前面已经构造好的成员由编译器负责回收,不会调用析构函数

2:数组元素构造时抛异常,前面已经构造好的元素由编译器回收,不会调用对象的析构函数。

3:多继承中某个基类的构造函数抛异常,已经构造成功的基类对象由编译器回收,不会调用析构函数

4:智能指针,STL 容器 存放auto_ptr, shared_ptr 对象, 类型T构造失败,则前面构成成功的智能对象有编译器回收,不会调用析构函数。

C++中构造函数中this指针暴露了会有什么后果?

参考地址
当一个类正在构造时在构造函数中将this泄露给了其它对象,这在单线程串行执行情况下可能没有什么问题,但是在多线程下那么问题就比较大了。比如线程1负责构造这个对象A但是在构造函数中将this指针泄露给了其它线程所调用的对象B,不巧的是其它线程所调用的对象B看见A有些不爽将其析构了。那么最后A自以为一切构造好了返回,线程1然后对这个A操作,最后可怕的错误(比如段错误)无穷无尽的折磨线程1。

第三部分 虚函数及多态

虚函数表具体是怎样实现运行时多态的?

子类若重写父类虚函数,虚函数表中,该函数的地址会被替换,对于存在虚函数的类的对象,在VS中,对象的对象模型的头部存放指向虚函数表的指针,通过该机制实现多态。

基类指针为什么能够指向派生类

可以指向,但是无法使用不存在于基类只存在于派生类的元素。(所以我们需要虚函数和纯虚函数)原因是这样的:在内存中,一个基类类型的指针是覆盖N个单位长度的内存空间。当其指向派生类的时候,由于派生类元素在内存中堆放是:前N个是基类的元素,N之后的是派生类的元素。于是基类的指针就可以访问到基类也有的元素了,但是此时无法访问到派生类(就是N之后)的元素。

虚函数指针在内存中的分布

C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。

// virtual.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <iostream>
using namespace std;
class Base {
public:
    virtual void f() {
        cout << "base f()" << endl;
    }
    virtual void g() {
        cout << "base g()" << endl;
    }
    virtual void h() {
        cout << "base h()" << endl;
    }
};
class Derived :public Base {
public:
    void f() {
        cout << "derived f()" << endl;
    }
    void g() {
        cout << "derived g()" << endl;
    }
};
typedef void(*pFun)(void);
int _tmain(int argc, _TCHAR* argv[])
{
    Derived d;
    cout << "d的首地址的值:" << &d << endl;
    cout << "d首地址内容:" << *(int*)&d << endl;
    cout << "虚函数表首地址的值、虚函数表第一表项所在内存地址的值:" << (int*)*(int*)&d << endl;
    cout << "虚函数表第一表项内容、Derived::f()函数首地址的值:" << *(int*)*(int*)&d << endl;
    cout << "虚函数表第二表项地址的值:" << (int*)*(int*)&d + 1 << endl;
    cout << "虚函数表第二表项内容、Derived::g()函数首地址的值:" << *((int*)*(int*)&d + 1) << endl;
    pFun p1 = (pFun)(*(int*)*(int*)&d);
    p1();
    p1 = (pFun)(*((int*)*(int*)&d + 1));
    p1();
    Derived d2;
    cout << "d2的首地址的值:" << &d2 << endl;
    cout << "d2首地址内容:" << *(int*)&d2 << endl;
    cout << "虚函数表首地址的值、虚函数表第一表项所在内存地址的值:" << (int*)*(int*)&d2 << endl;
    cout << "虚函数表第一表项内容、Derived::f()函数首地址的值:" << *(int*)*(int*)&d2 << endl;
    cout << "虚函数表第二表项地址的值:" << (int*)*(int*)&d2 + 1 << endl;
    cout << "虚函数表第二表项内容、Derived::g()函数首地址的值:" << *((int*)*(int*)&d2 + 1) << endl;
    cout << "虚函数表第三表项地址的值:" << (int*)*(int*)&d2 + 2 << endl;1)
    cout << "虚函数表第三表项内容、Base:: h()函数首地址的值:" << *((int*)*(int*)&d2 + 2) << endl;
    pFun p2 = (pFun)(*(int*)*(int*)&d2);
    p2();
    p2 = (pFun)(*((int*)*(int*)&d2 + 1));
    p2();
    p2 = (pFun)(*((int*)*(int*)&d2 + 2));
    p2();
    Base a;
    cout << "a的首地址的值:" << &a << endl;
    cout << "a首地址的内容:" << *(int*)&a << endl;
    cout << "虚函数表首地址的值、虚函数第一个表项所在内存地址的值:" << (int*)*(int*)&a << endl;
    cout << "虚函数表第一表项内容、Base::f()函数首地址的值:" << *(int*)*(int*)&a << endl;
    cout << "虚函数表第二表项地址的值:" << (int*)*(int*)&a + 1 << endl;
    cout << "虚函数表第二表项内容、Base::g()函数首地址的值:" << *((int*)*(int*)&a + 1) << endl;
    cout << "虚函数表第三表项地址的值:" << (int*)*(int*)&a + 2 << endl;  //(2)
    cout << "虚函数表第三表项内容、Base::h()函数首地址的值:" << *((int*)*(int*)&a + 2) << endl;
    pFun p3 = (pFun)(*(int*)*(int*)&a);
    p3();
    p3 = (pFun)(*((int*)*(int*)&a + 1));
    p3();
    p3 = (pFun)(*((int*)*(int*)&a + 2));
    p3();
    return 0;
}

什么是多态?多态是如何实现的?*

多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。在基类的函数前加上 virtual 关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

实现方法:多态是通过虚函数实现的,虚函数的地址保存在虚函数表中,虚函数表的地址保存在含有虚函数的类的实例对象的内存空间中。

实现过程

  • 在类中用 virtual 关键字声明的函数叫做虚函数;
  • 存在虚函数的类都有一个虚函数表,当创建一个该类的对象时,该对象有一个指向虚函数表的虚表指针(虚函数表和类对应的,虚表指针是和对象对应);
  • 当基类指针指向派生类对象,基类指针调用虚函数时,基类指针指向派生类的虚表指针,由于该虚表指针指向派生类虚函数表,通过遍历虚表,寻找相应的虚函数。

构成多态的条件:

  • 必须存在继承关系;
  • 继承关系中必须有同名的虚函数,并且它们是覆盖关系(重载不行)。
  • 存在基类的指针,通过该指针调用虚函数。
  • 注意:派生类(子类)中的虚函数必须覆盖(不是重载)基类(父类)中的虚函数,才能通过基类指针访问
  • 所有派生类中具有覆盖关系的同名函数都将自动成为虚函数。
  • virtual 关键字仅用于函数声明,如果函数是在类外定义,则不需要再加上virtual关键字。

虚基类是什么?纯虚函数可不可以实现?什么情况下对纯虚函数进行实现?

虚继承: 用virtual限定符把基类继承说明为虚拟的。

虚基类、虚函数、纯虚函数

虚基类
当某类的部分或者全部直接基类是从另一个共同基类派生而来时,在这些直接基类中从上一级共同基类继承来的成员就拥有相同的名称。在派生类的对象中,这些同名数据成员在内存中同时拥有多个副本,同一个函数名会有多个映射。
解决办法
可以使用作用域分辨符来唯一标识并分别访问它们,也可以将共同基类设置为虚基类,这时从不同路径继承过来的同名函数成员在内存中就只有一份副本,同一个函数也只有一个映射。这样就解决了同名成员的唯一标识问题。
虚基类可以解决多重继承中引用不明确问题(命名冲突和冗余数据问题)。
在这里插入图片描述

其它

  • 虚基类子对象是由最派生类的构造函数通过调用虚基类的构造函数进行初始化的。
  • 派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用;如果未列出,则表示使用该虚基类的缺省构造函数。
  • 从虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中都要列出对虚基类构造函数的调用。但只有用于建立对象的最派生
  • 类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类的构造函数的调用在执行中被忽略,从而保证对虚基类子对象 只初始化一次。
  • 在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行。

虚函数
1, 虚函数是非静态的、非内联的成员函数,而不能是友元函数,但虚函数可以在另一个类中被声明为友元函数。
2, 虚函数声明只能出现在类定义的函数原型声明中,而不能在成员函数的函数体实现的时候声明。
3, 一个虚函数无论被公有继承多少次,它仍然保持其虚函数的特性。
4, 若类中一个成员函数被说明为虚函数,则该成员函数在派生类中可能有不同的实现。当使用该成员函数操作指针或引用所标识的对象时 ,对该成员函数调用可采用动态联编
5, 定义了虚函数后,程序中声明的指向基类的指针就可以指向其派生类。在执行过程中,该函数可以不断改变它所指向的对象,调用不同 版本的成员函数,而且这些动作都是在运行时动态实现的。虚函数充分体现了面向对象程序设计的动态多态性。 纯虚函数 版本的成员函数,而且这些动作都是在运行时动态实现的。虚函数充分体现了面向对象程序设计的动态多态性

纯虚函数
1, 当在基类中不能为虚函数给出一个有意义的实现时,可以将其声明为纯虚函数,其实现留待派生类完成。
2, 纯虚函数的作用是为派生类提供一个一致的接口。
3, 纯虚函数不能实例化,但可以声明指针

什么是虚函数?什么是纯虚函数?*

虚函数:被 virtual 关键字修饰的成员函数,就是虚函数。
纯虚函数

  • 纯虚函数在类中声明时,加上 =0;
  • 含有纯虚函数的类称为抽象类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法;
  • 继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。

说明

  • 抽象类对象不能作为函数的参数,不能创建对象,不能作为函数返回类型;
  • 可以声明抽象类指针,可以声明抽象类的引用;(因为指针和引用方式指向的对象可以是抽象类的派生类型的对象)
  • 子类必须继承父类的纯虚函数,并全部实现后,才能创建子类的对象。

虚函数和纯虚函数的区别?*

虚函数和纯虚函数可以出现在同一个类中,该类称为抽象基类。(含有纯虚函数的类称为抽象基类)
使用方式不同:虚函数可以直接使用,纯虚函数必须在派生类中实现后才能使用;
定义形式不同:虚函数在定义时在普通函数的基础上加上 virtual 关键字,纯虚函数定义时除了加上virtual 关键字还需要加上 =0;
虚函数必须实现,否则编译器会报错;
对于实现纯虚函数的派生类,该纯虚函数在派生类中被称为虚函数,虚函数和纯虚函数都可以在派生类中重写;
析构函数最好定义为虚函数,特别是对于含有继承关系的类;析构函数可以定义为纯虚函数,此时,其所在的类为抽象基类,不能创建实例化对象。

虚函数的实现机制*

实现机制:虚函数通过虚函数表来实现。虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表。虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这张虚函数表就指明了实际应该调用的函数。

虚函数表相关知识点:

  • 虚函数表存放的内容:类的虚函数的地址。
  • 虚函数表建立的时间:编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中。
  • 虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。

注:虚函数表和类绑定,虚表指针和对象绑定。即类的不同的对象的虚函数表是一样的,但是每个对象都有自己的虚表指针,来指向类的虚函数表。

虚函数整体知识点

参考地址

早期绑定

对于简单的继承关系,其子类内存布局,是先有基类数据成员,然后再是子类的数据成员。
在这里插入图片描述

内存分布

程序运行到动态绑定时,通过基类的指针所指向的对象类型,通过vfptr找到其所指向的vtable,然后调用其相应的方法,即可实现多态。
总结(基类有虚函数):
1、每一个类都有虚表。
2、虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
3、派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。

虚继承

在这里插入图片描述
解决菱继承问题,防止共同基类别构造两次

单继承下内存的分布

class Base {
     public:
        virtual void f() { cout << "Base::f" << endl; }
        virtual void g() { cout << "Base::g" << endl; }
        virtual void h() { cout << "Base::h" << endl; }
};

在这里插入图片描述

一般继承(无虚函数覆盖)
在这里插入图片描述
在这里插入图片描述
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。

一般继承(有虚函数覆盖)
在这里插入图片描述
在这里插入图片描述
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。

多继承下内存的分布

多重继承(无虚函数覆盖)
在这里插入图片描述
在这里插入图片描述
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

多重继承(有虚函数覆盖)
在这里插入图片描述
在这里插入图片描述
1) 覆盖的f()函数被放到了虚表中每个父类虚函数的位置。
2) 没有被覆盖的函数依旧。

虚继承

参考地址

虚继承解决了菱形继承中最派生类拥有多个间接父类实例的情况。虚继承的派生类的内存布局与普通继承很多不同,主要体现在:

  • 虚继承的子类,如果本身定义了新的虚函数,则编译器为其生成一个虚函数指针(vptr)以及一张虚函数表。该vptr位于对象内存最前面。
  • vs非虚继承:直接扩展父类虚函数表。
  • 虚继承的子类也单独保留了父类的vprt与虚函数表。这部分内容接与子类内容以一个四字节的0来分界。
  • 虚继承的子类对象中,含有四字节的虚表指针偏移值。

虚基类表
在C++对象模型中,虚继承而来的子类会生成一个隐藏的虚基类指针(vbptr),在Microsoft Visual C++中,虚基类表指针总是在虚函数表指针之后,因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例内存布局的最前面),也可能在类实例的4字节偏移处。
一个类的虚基类指针指向的虚基类表,与虚函数表一样,虚基类表也由多个条目组成,条目中存放的是偏移值。第一个条目存放虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由第一段的分析我们知道,这个偏移值为0(类没有vptr)或者-4(类有虚函数,此时有vptr)。我们通过一张图来更好地理解。

在这里插入图片描述

多继承情况下子类实例的内存结构(存在虚继承)
在这里插入图片描述
在虚继承下,Der通过共享虚基类SuperBase来避免二义性,在Base1,Base2中分别保存虚基类指针,Der继承Base1,Base2,包含Base1, Base2的虚基类指针,并指向同一块内存区,这样Der便可以间接存取虚基类的成员,如下图所示:

在这里插入图片描述
部分虚继承的情况下子类实例的内存结构:

class D:virtual public A,public B,public C

在这里插入图片描述
1)对于无虚继承,父类虚函数表按照声明顺序放置
2)对于虚继承,按照虚继承依次保存虚基类指针

全部虚继承的情况下,子类实例的内存结构

class C:virtual public A,virtual public B

在这里插入图片描述
按照虚继承依次保存虚基类指针

菱形结构继承关系下子类实例的内存结构

在这里插入图片描述

虚函数与重载的区别*

  • 函数重载可以用于非成员函数和类的成员函数,而虚函数只能用于类的成员函数
  • 函数重载可用于构造函数,而虚函数不能用于构造函数
  • 如果对成员函数进行重载,重载的函数与被重载的函数应该是用一个类中的成员函数,不能分属于两个不同继承层次的类,函数重载处理的是横向的重载。虚函数是对同一类族中的基类和派生类的同名函数的处理,即允许在派生类中对基类的成员函数重新定义。虚函数处理的是纵向的同名函数。
  • 重载的函数必须具有相同的函数名,函数类型可以相同也可以不同,但函数的参数个数和参数类型二者中至少有一个不同,否则在编译时无法区分。而虚函数则要求同一类族中的所有虚函数的函数名,函数类型,函数的参数个数和参数类型都全部相同,否则就不是重定义了,也就不是虚函数了
  • 函数重载是在程序编译阶段确定操作的对象的,属于静态关联。虚函数是在程序运行阶段确定操作对象的,属于动态关联

动态绑定和虚函数表

为了实现 C++ 的多态,C++ 使用了一种动态绑定的技术。这个技术的核心是虚函数表(下文简称虚表)。
注意以下两点

  • 每个包含了虚函数的类都包含一个虚表。
  • 虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。

执行函数的动态绑定条件

  • 通过指针来调用函数
  • 指针 upcast 向上转型(继承类向基类的转换称为 upcast,关于什么是 upcast,可以参考本文的参考资料)
  • 调用的是虚函数

重载、重写、隐藏*

重载: 简单的说,就是函数或者方法有相同的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间相互称之为重载函数或者方法。根据参数列表确定调用哪个函数,重载不关心函数返回类型。

重写(覆盖):是指派生类中存在重新定义的函数。函数名、参数列表、返回值类型都必须同基类中被重写的函数一致,只有函数体不同。派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有 virtual 修饰。

隐藏:是指派生类的函数屏蔽了与其同名的基类函数,主要只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。

重写和重载的区别:

范围区别:对于类中函数的重载或者重写而言,重载发生在同一个类的内部,重写发生在不同的类之间(子类和父类之间)。 重写必须继承,重载不用。
参数区别:重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有 virtual 修饰。
virtual 关键字:重写的函数基类中必须有 virtual关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有。

隐藏和重写,重载的区别:
范围区别:隐藏与重载范围不同,隐藏发生在不同类中。
参数区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被 virtual 修饰,基类函数都是被隐藏,而不是重写。

动态绑定是如何实现的*

是通过虚函数来实现的,将基类的成员函数声明为virtual的,是指通过基类的指针或引用访问派生类的虚函数,那么程序会在运行时选择该派生类的函数而不是基类的函数,这种特性成为运行时绑定(动态绑定、晚绑定)。
实现机制: 首先,每一个含有虚函数的类叫做多态类,编译器会给每个多态类至少创建一个虚函数表,它其实是一个函数指针数组,存放着这个类所有的虚函数地址以及该类的类型信息,其中也包括哪些继承但未被改写(overwrite)的虚函数。其次,每一个多态类的对象都有一个隐含的指针成员:虚函指针vptr,它指向所属类中的vtable

静态绑定:静态绑定是指程序在编译阶段确定对象的类型(静态类型)。

多态性有哪些*

(1)编译时的多态(静态):在C++中主要体现在函数模板和函数重载上。很多地方说函数重载不算多态,但是看函数重载的本质,重载函数的调用地址在编译期就绑定了,因此一定意义上也是编译时的多态;

(2)运行时的多态(动态):主要是通过虚函数来实现的,发生在继承体系中,是指通过基类的指针或引用访问派生类中的虚函数。

虚函数、虚函数表的内存布局

参考博客
C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。

不能声明为虚函数的有哪些

  • 普通函数(类的非成员函数)。普通函数只能被重载,定义虚函数的目的是为了重写达到多态,所以普通函数的目的是为了重写达到多态(编译时的多态),所以普通函数声明为虚函数没有必要,因为编译器会在编译时邦定函数。
  • 静态成员函数。静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没有要动态邦定的必要性。不能被继承,只属于该类。
  • 构造函数。如上文的【为什么构造函数一般不定义为虚函数?而析构函数定义为虚函数?*】
  • 友元函数。因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。友元函数不属于类的成员函数,不能被继承。
  • 内联函数。其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的邦定函数)inline函数和virtual函数有着本质的区别,inline函数是在程序被编译时就展开,在函数调用处用整个函数体去替换,而virtual函数是在运行期才能够确定如何去调用的,因而inline函数体现的是一种编译期机制,virtual函数体现的是一种运行期机制。此外,一切virtual函数都不可能是inline函数。

第四部分 指针和引用

参数传递时,值传递、引用传递、指针传递的区别?*

参数传递的三种方式
值传递:形参是实参的拷贝,函数对形参的所有操作不会影响实参。
指针传递:本质上是值传递,只不过拷贝的是指针的值,拷贝之后,实参和形参是不同的指针,通过指针可以间接的访问指针所指向的对象,从而可以修改它所指对象的值。
引用传递:当形参是引用类型时,我们说它对应的实参被引用传递。

函数指针与指针函数

函数指针,即指向函数的指针。首先,它是一个指针,其次,该指针指向一个函数。其一般定义如下所示:

类型名 (*指针名称)(函数参数列表);
int (*pfunc)(int, int);

pfunc是一个指向参数为(int, int)且返回值为int的函数的指针。

指针函数,是一个函数,它的返回值是指针。一般定义如下所示:

类型名 *函数名(函数参数列表);
int *func(int, int);

func是一个返回值为整型指针的函数。

函数指针和指针函数的区别:
本质不同:指针函数本质是一个函数,其返回值为指针;函数指针本质是一个指针变量,其指向一个函数。
定义形式不同:指针函数:int* fun(int tmp1, int tmp2); ,这里* 表示函数的返回值类型是指针类型;函数指针:int (*fun)(int tmp1, int tmp2);,这里* 表示变量本身是指针类型。
用法不同

const和指针的用法(常量指针、指针常量和指向常量的指针常量)

const:关键字来修饰常量,所有常类型的变量的值都是不可更改的,并且在定义的时候就必须被初始化。
常量指针
常量指针是一个常指针,指针的数值不能更改,指针可以更改指向。

int num = 100;
int num2 = 200;  
const int * p = &num;
*p = 200;  // 错误,不能修改数值
p = &num2;  // 能修改指向

指针常量
指针常量和常量指针相反,它可以修改数值,但是不能修改指向。

int num = 100, num2 = 200;
int * const p = &num;
p = &num2;  //错误,不能修改指向
*p = 100;  //能修改数值

指向常量的指针常量
它既不能修改数值,也不能修改地址

int num = 100, num2 = 200;
const int * const p = &num;
p = &num2;  // 不能修改地址
*p = 1000;  // 不能修改数值

智能指针*

参考地址
智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放,

C++11 中智能指针包括以下三种:
共享指针(shared_ptr):资源可以被多个指针共享,使用计数机制表明资源被几个指针共享。通过 use_count() 查看资源的所有者的个数,除了可以通过new来构造,还可以通过 unique_ptr、weak_ptr 来构造,调用 release() 释放资源的所有权,计数减一,当计数减为 0 时,会自动释放内存空间,从而避免了内存泄漏。

shared_ptr 支持安全共享的秘密在于内部使用了“引用计数”。引用计数最开始的时候是 1,表示只有一个持有者。如果发生拷贝赋值——也就是共享的时候,引用计数就增加,而发生析构销毁的时候,引用计数就减少。只有当引用计数减少到 0,也就是说,没有任何人使用这个指针的时候, 它才会真正调用 delete 释放内存。

独占指针(unique_ptr):独享所有权的智能指针,资源只能被一个指针占有,该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造(调用 move() 函数),即一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,可以通过该方法进行赋值,实现所有权的转移。

auto ptr1 = make_unique<int>(42);    // 工厂函数创建智能指针
assert(ptr1 && *ptr1 == 42);         // 此时智能指针有效

auto ptr2 = std::move(ptr1);         // 使用move()转移所有权
assert(!ptr1 && ptr2);               // ptr1变成了空指针

尽量不要对 unique_ptr 执行赋值操作就好了,让它“自生自灭”,完全自动化管理(例如“以new创建对象后因为发生异常而忘记调用delete”)。

弱指针(weak_ptr):weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr.。 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。主要是解决share_ptr循环引用问题。

使用智能指针的原因至少有以下三点:

  • 智能指针能够帮助我们处理内存泄露问题;
  • 它也能够帮我们处理空悬指针的问题;
  • 它还能够帮我们处理比较隐晦的由异常造成的资源泄露

unique_ptr 与shared_ptr 比较

  • shared_ptr 的名字明显表示了它与 unique_ptr 的最大不同点:它的所有权是可以被安全共享的,也就是说支持拷贝赋值,允许被多个“人”同时持有,就像原始指针一样。

auto_ptr(c++98的方案,cpp11已经抛弃)
采用所有权模式。

auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错.

此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!

shared_ptr的实现

牛客问答题

使用智能指针会出现什么问题?怎么解决?*

智能指针可能出现的问题:循环引用。在两个类中分别定义另一个类的对象的共享指针,由于在程序结束后,两个指针相互指向对方的内存空间,导致内存无法释放,即该被调用的析构函数没有被调用,从而出现了内存泄漏

循环引用的解决办法:弱指针(weak_ptr)。

  • weak_ptr 对被 shared_ptr 管理的对象存在非拥有性(弱)引用,在访问所引用的对象前必须先转化为 shared_ptr;
  • weak_ptr 用来打断 shared_ptr 所管理对象的循环引用问题,若这种环被孤立(没有指向环中的外部共享指针),shared_ptr 引用计数无法抵达 0,内存被泄露;令环中的指针之一为弱指针可以避免该情况;
  • weak_ptr 用来表达临时所有权的概念,当某个对象只有存在时才需要被访问,而且随时可能被他人删除,可以用 weak_ptr 跟踪该对象;需要获得所有权时将其转化为 shared_ptr,此时如果原来的 shared_ptr 被销毁,则该对象的生命期被延长至这个临时的 shared_ptr 同样被销毁。

weak_ptr的使用策略?

weak_ptr是c++11引入的特性,主要是为了解决shared_ptr引用循环问题。它本身是个模板类,但是不能直接生成一个实例,必须通过shared_ptr或者另一个weak_ptr来实例化,并且没有重载运算符*和->,因此它不具备指针属性,它提供了lock和expired函数,通过expired可以检查对象是否有效,lock可以返回shared_ptr;当weak_ptr空悬,也称失效,则lock返回的shared_ptr为空,否则返回的shared_ptr会使引用计数增加。

利用weak_ptr有效性检查,可以将weak_ptr作为观察者,当weak_ptr失效时可以不用通知这个观察者。

弱指针就是没有所有权的指针,只是指向这块内存的指针,但是没有把这块内存的生命周期关联起来,也就是说想用的时候可以用,但是不想因此资源被释放。

share_ptr 和unique_ptr的区别

  • 引用计数的存储和管理都是成本,这方面是 shared_ptr 不如 unique_ptr 的地方。
  • shared_ptr 的销毁动作。把指针交给了 shared_ptr 去自动管理,但在运行阶段,引用计数的变动是很复杂的,很难知道它真正释放资源的时机,无法像 Java、Go 那样明确掌控、调整垃圾回收机制。要特别小心对象的析构函数,不要有非常复杂、严重阻塞的操作。
  • shared_ptr 的引用计数也导致了一个新的问题,就是“循环引用”,这在把 shared_ptr 作为类成员的时候最容易出现,典型的例子就是链表节点。

野指针和悬空指针*

悬空指针:若指针指向一块内存空间,当这块内存空间被释放后,该指针依然指向这块内存空间,此时,称该指针为“悬空指针”。(最初指向的内存空间被释放了。)

野指针:野指针是说不确定其指向的指针,有以下三种情况。

出现野指针的三种情况:

  • 指针未初始化。指针变量在定义时不会自动初始化成空指针,而是随机的一个值,可能指向任意空间,这就使得该指针成为野指针。因此指针在初始化时要么指向一个合理的地址,要么初始化为NULL。
  • 指针指向的变量被free或delete后没有置为NULL。在调用free或delete释放空间后,指针指向的内容被销毁,空间被释放,但是指针的值并未改变,仍然指向这块内存,这就使得该指针成为野指针。因此在调用free或 delete之后,应将该指针置为NULL。
  • 指针操作超过所指向变量的生存期。当指针指向的变量的声明周期已经结束时,如果指针仍然指向这块空间,就会使得该指针成为野指针。

如何避免野指针

  • 对于第一种。定义指针变量的同时对它进行初始化操作,定义时将其置为NULL或者指向一个有名变量。
  • 对于第二种。对指针进行free或者delete操作后,将其置为NULL。对于使用 free 的情况,常常定义一个宏或者函数 xfree 来代替 free 置空指针:`#define xfree(x) free(x); x = NULL;
  • 对于第三中。当想给指针赋值时,检查是否已经给他分配了内存空间,如果没有分配就再用malloc/new分配;
  • 其它。C++引入了引用机制,如果使用引用可以达到编程目的,就可以不必使用指针。因为引用在定义的时候,必须初始化,所以可以避免野指针的出现。

引用、引用作为函数参数、引用作为函数的返回值

引用就是某个目标变量的“别名”(alias),对应用的操作与对变量直接操作效果完全相同。申明一个引用的时候,切记要对其进行初始化。

引用型函数参数

  • 可以将引用用于函数的参数,这时形参就是实参的别名
  • 引用型函数参数作用:在函数中修改实参的值;避免实参到形参数值复制的开销,提高传参效率
  • 引用型函数参数可能意外修改实参,如果不希望通过引用修改实参本身,可以将其声明为常引用,在提高传参效率的同时还可以接收常量型实参。

引用型返回值
可以将函数的返回类型声明为引用型,这时函数的返回结果就是return后面数据的别名,在内存中不产生返回值的副本,避免了函数返回值的开销。
函数中返回引用,一定要保证在函数返回以后,该引用的目标依然有效

  • 可以返回全局变量、静态变量和成员变量的引用
  • 可以返回引用型参数的本身
  • 可以返回调用对象自身的引用
  • 可以返回堆中动态创建对象的引用
  • 不能返回局部变量的引用//非常危险

指针和引用的区别*

  • 指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。
  • 是否可变:指针所指向的内存空间在程序运行过程中可以改变,而引用所绑定的对象一旦绑定就不能改变。
  • 是否占内存:指针本身在内存中占有内存空间,引用相当于变量的别名,在内存中不占内存空间。
  • 是否能为空:指针可以为空,但是引用必须绑定对象。
  • 是否能为多级:指针可以有多级,但是引用只能一级。
  • 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄漏;

为什么引用会引起内存泄漏

由于单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。

指针和数组

指针:
指针的本质是一个变量,它保存的目标值是一个内存地址。
指针运算与 * 操作符配合使用能够模拟数组的行为。
数组:
数组是一段连续的内存空间。
数组名可看做指向数组第一个元素的常量指针。

第五部分 关键字

final关键字

c++中final关键的作用最重要就是两个,先强调下:
1.禁止虚函数被重写
2.禁止基类被继承

可以被重载的操作符

在这里插入图片描述

this指针

this指针就是指向当前对象的指针,this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。任何对类成员的直接访问都被看成this的隐式使用。

使用:一种情况就是,在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this;另外一种情况是当参数与成员变量名相同时,如this->n = n (不能写成n = n)。

前置++和后置++

前自增运算符的重载函数先自增,然后返回对象自身的引用;后自增运算符先创建一个对象的副本,然后使用前自增操作调用前自增运算符的重载函数,最后返回对象的副本。

class A
{
public:
	A& operator++()//前置++,返回的是引用
	{
		data +=1;
		return *this;
	}
	const A operator++(int)//后置++,返回的是值
	{
		A old(*this);
		++(*this);	//调用前置++
		return old;
	}
//从代码可以看出,前置++比后置++效率高,不用产生临时对象,不用调用拷贝构造函数
	int data;
};

前置++为了可以连续运算,所以返回对象的引用。为什么返回引用?

  • 与内置类型的行为保持一致。前置++返回的总是被自增的对象本身。因此,++(++a)的效果就是a被自增两次。

后置++返回的是const临时对象,为什么是const对象呢?

  • 如果不是const对象,a(++)++这样的表达式就可以通过编译。但是,其效果却违反了我们的直觉 。a其实只增加了1,因为第二次自增作用在一个临时对象上。
  • 另外,对于内置类型,(i++)++这样的表达式是不能通过编译的。自定义类型的操作符重载,应该与内置类型保持行为一致 。

extern

在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。

extern "C"的作用*

extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

C和C++混合编译

主要区别(针对mix):
C编译识别函数:函数名;C++编译识别函数:函数名+参数;所以,需要解决C++重载特性。

C++源码访问C代码,关键点:

  • 在C++代码中包含C头文件:头文件需要具有链接规范。extern
  • 让header.h同时适应C和C++编译器,进而创建混合语言标题。#ifdef __cplusplus

const作用及用法*

作用:

  • const 修饰成员变量,定义成 const 常量,相较于宏常量,可进行类型检查,节省内存空间,提高了效率。
  • const 修饰成员函数,使得成员函数不能修改任何类型的成员变量(mutable 修饰的变量除外),也不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量。
  • const 修饰函数参数,使得传递过来的函数参数的值不能改变。
  • 对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
  • 在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;

在类中的用法:
const 成员变量:

  • const 成员变量只能在类内声明、定义,在构造函数初始化列表中初始化。
  • const 成员变量只在某个对象的生存周期内是常量,对于整个类而言却是可变的,因为类可以创建多个对象,不同对象的 const 成员变量的值是不同的。因此不能在类的声明中初始化 const 成员变量,类的对象还没有创建,编译器不知道他的值。

const是如何实现的?

  • 首先,以const 修饰的常量值,具有不可变性,这是它能取代预定义语句的基础。
  • C++的编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高,同时,这也是它取代预定义语句的重要基础。
  • const定义也像一个普通的变量定义一样,它会由编译器对它进行类型的检测,消除了预定义语句的隐患
  • 对于ADT/UDT的const对象则需要分配内存空间,储存在栈区或者静态存储区。

define和const的区别*

  • 编译阶段:define 是在编译预处理阶段进行替换,const 是在编译阶段确定其值。
  • 安全性:define 定义的宏常量没有数据类型,只是进行简单的替换,不会进行类型安全的检查;const 定义的常量是有类型的,是要进行判断的,可以避免一些低级的错误。
  • 内存占用:define 定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份,占用的是代码段的空间;const 定义的常量占用静态存储区的空间,程序运行过程中只有一份。
  • 调试:define 定义的宏常量不能调试,因为在预编译阶段就已经进行替换了;const 定义的常量可以进行调试。

const优点

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

typedef和define的区别*

typedef: typedef常用来定义一个标识符及关键字的别名,它是语言编译过程的一部分,但它并不实际分配内存空间。typedef可以增强程序的可读性,以及标识符的灵活性,但它也有“非直观性”等缺点。

原理:#define 作为预处理指令,在编译预处理时进行替换操作,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。typedef 是关键字,在编译时处理,有类型检查功能,用来给一个已经存在的类型一个别名,但不能在一个函数定义里面使用 typedef 。
功能:typedef 用来定义类型的别名,方便使用。#define 不仅可以为类型取别名,还可以定义常量、变量、编译开关等。
作用域:#define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef 有自己的作用域。
指针的操作:typedef 和 #define 在处理指针时不完全一样

static的用法,作用,static变量存在什么区*

参考地址
作用:

  • 修饰全局变量时,表明一个全局变量只对定义在同一文件中的函数可见。
  • 修饰局部变量时,表明该变量的值不会因为函数终止而丢失。
  • 修饰函数时,表明该函数只在同一文件中调用。
  • 修饰类的数据成员,表明对该类所有对象这个数据成员都只有一个实例。即该实例归所有对象共有。则此变量/函数就没有了this指针了,必须通过类名才能访问。
  • 用static修饰不访问非静态数据成员的类成员函数。这意味着一个静态成员函数只能访问它的参数、类的静态数据成员和全局变量。

1.先来介绍它的第一条也是最重要的一条:隐藏。static 作用于全局变量和函数,改变了全局变量和函数的作用域,使得全局变量和函数只能在定义它的文件中使用,在源文件中不具有全局可见性。(注:普通全局变量和函数具有全局可见性,即其他的源文件也可以使用。)
2.static的第二个作用是保持变量内容的持久。static 作用于局部变量,改变了局部变量的生存周期,使得该变量存在于定义后直到程序运行结束的这段时间。
3.static的第三个作用是默认初始化为0(static变量)
4.static的第四个作用:static 作用于类的成员变量和类的成员函数。
(1)类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这就导致 了它仅能访问类的静态数据和静态成员函数。

储存区域: 全局区(静态区)(.bss段和.data段)。共有两种变量存储在静态存储区:全局变量和static变量。

static静态成员变量
static 静态成员变量:

  • 静态成员变量是在类内进行声明,在类外进行定义和初始化,在类外进行定义和初始化的时候不要出现 static 关键字和private、public、protected 访问规则。
  • 静态成员变量相当于类域中的全局变量,被类的所有对象所共享,包括派生类的对象。
  • 静态成员变量可以作为成员函数的参数,而普通成员变量不可以。
  • 静态数据成员的类型可以是所属类的类型,而普通数据成员的类型只能是该类类型的指针或引用。

static 静态成员函数:

  • 静态成员函数不能调用非静态成员变量或者非静态成员函数,因为静态成员函数没有 this 指针。静态成员函数做为类作用域的全局函数。
  • 静态成员函数不能声明成虚函数(virtual)、const 函数和 volatile 函数。

为什么静态成员函数只能访问静态成员变量?*

  • 静态成员函数只属于类本身,随着类的加载而存在,不属于任何对象,是独立存在的
  • 非静态成员当且仅当实例化对象之后才存在,静态成员函数产生在前,非静态成员函数产生在后,故不能访问
  • 内部访问静态成员用self::,而访问非静态成员要用this指针,静态成员函数没有this指针,故不能访问。

struct与class的区别*

参考地址

  • 内部成员变量及成员函数的默认访问属性。struct默认访问级别是public的,而class默认的访问级别是private的
  • 继承关系中默认防控属性的区别。在继承关系,struct默认是public的,而class是private。
  • class还可用于定义模板参数,作用同typename,但是关键字struct不能同于定义模板参数。

violate

C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,通常用于建立语言级别的 memory barrier。

它用来解决变量在“共享”环境下容易出现读取错误的问题。
定义为volatile的变量是说这变量可能会被意想不到地改变,即在你程序运行过程中一直会变,你希望这个值被正确的处理,每次从内存中去读这个值,而不是因编译器优化从缓存的地方读取,比如读取缓存在寄存器中的数值,从而保证volatile变量被正确的读取。

第六部分 编译过程

编译是预处理之后的阶段,它的输入是(经过预处理的)C++ 源码,输出是二进制可执行文件(也可能是汇编文件、动态库或者静态库)。这个处理动作就是由编译器来执行的。

C++的编译过程*

参考地址
C/C++程序编译流程(预处理->编译->汇编->链接)
预处理:读取C/C++源程序,对其中的伪指令(以#开头的指令)进行处理。将所有的“#define”删除,并且展开所有的宏定义;处理所有的条件编译指令,如:“#if”、“#ifdef”、“#elif”、“#else”、“endif”等;处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。
编译:将预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。
汇编:将编译完的汇编代码文件翻译成机器指令,并生成可重定位目标程序的.o文件,该文件为二进制文件,字节编码是机器指令。
链接:汇编程序生成的目标文件,即 .o 文件,并不会立即执行,因为可能会出现:.cpp 文件中的函数引用了另一个 .cpp 文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序 .exe 文件

在这里插入图片描述

动态链接与静态链接*

1、静态链接:

函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。

空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;

更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。

运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。

2、动态链接:

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。

共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;

更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。

性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。

第七部分 类与对象

对象切片

对象切片: c++在将一个派生类转换为基类的过程中,这个对象将被”分割”成只剩下与目的类型(即:转换以后的类型)相匹配的子对象(即:成员变量和函数)

避免对象切片: 在指针、引用情况下,由于多态性(纯虚函数),可以将派生类对象的指针或引用指向或绑定基类,而不会导致对象切片。

什么是面向对象?面向对象的三大特性*

面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含数据(成员变量)和动作(成员方法)。

面向对象的三大特性
封装:将具体的实现过程和数据封装成类,只能通过接口进行访问,降低耦合性
继承:子类继承父类的数据和行方法,子类有父类的非 private 方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被 final 关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。
多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针或引用,指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。

C++类对象的初始化顺序*

构造函数调用顺序:

  • 按照派生类继承基类的顺序,即派生列表中声明的顺序,依次调用基类的构造函数;
  • 按照派生类中成员变量的声名顺序,依次调用派生类中成员变量所属类的构造函数;
  • 执行派生类自身的构造函数。

注:

  • 基类构造函数的调用顺序与派生类的派生列表中的顺序有关;
  • 成员变量的初始化顺序与声明顺序有关;
  • 析构顺序和构造顺序相反。

空类占多少字节?C++ 编译器会给一个空类自动生成哪些函数?*

对于空类,声明编译器不会生成任何的成员函数,只会生成 1 个字节的占位符。
当空类 A 定义对象时,sizeof(A) 仍是为 1,但编译器会生成 6 个成员函数:缺省的构造函数、拷贝构造函数、析构函数、赋值运算符、两个取址运算符

class Empty
{
  public:
    Empty();                            //缺省构造函数
    Empty(const Empty &rhs);            //拷贝构造函数
    ~Empty();                           //析构函数 
    Empty& operator=(const Empty &rhs); //赋值运算符
    Empty* operator&();                 //取址运算符
    const Empty* operator&() const;     //取址运算符(const版本)
};

Empty *e = new Empty();    //缺省构造函数
delete e;                  //析构函数
Empty e1;                  //缺省构造函数                               
Empty e2(e1);              //拷贝构造函数
e2 = e1;                   //赋值运算符
Empty *pe1 = &e1;          //取址运算符(非const)
const Empty *pe2 = &e2;    //取址运算符(const)

基类与派生类指针和引用的转换问题

参考地址
基类对象可以定义一个指针指向派生类,但是派生类不可以定义指针指向基类。
因为:基类如果定义的指针,指针中存放的数据都是基类中存在的,而派生类继承了这些subobject;但是如果派生类定义指针,这个指针所指向的对象不仅仅有基类的subobject,还有自己的一部分定义的数据,用这个指针去访问基类就可能会访问到一些根本不存在的成员。引用也是同理!

基类指针指向派生类时的访问范围:对于非虚函数,调用的都是自身的函数;对于虚函数,调用的是派生类的函数。

友元函数的作用及适用场景

为什么要引入友元函数:在实现类之间数据共享时,减少系统开销,提高效率。

具体来说:为了使其他类的成员函数直接访问该类的私有变量。即:允许外面的类或函数去访问类的私有变量和保护变量,从而使两个类共享同一函数(友元函数不是类的成员函数,是普通函数)

缺点:友元函数会增加耦合度,破环了封装机制,所以友元不宜多用,尽量使用成员函数,除非不得已的情况下才使用友元函数。

什么时候使用友元函数:
1)运算符重载的某些场合需要使用友元。
2)两个类要共享数据的时候

友元类

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。

当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。定义友元类的语句格式如下:
friend class 类名;

使用友元类时注意:
(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明

类大小的计算*

  • 遵循结构体的对齐原则。
  • 与普通成员变量有关,与成员函数和静态成员无关。即普通成员函数,静态成员函数,静态数据成员,静态常量数据成员均对类的大小无影响。因为静态数据成员被类的对象共享,并不属于哪个具体的对象。
  • 虚函数对类的大小有影响,是因为虚函数表指针的影响。
  • 虚继承对类的大小有影响,是因为虚基表指针带来的影响。
  • 空类的大小是一个特殊情况,空类的大小为 1,当用 new 来创建一个空类的对象时,为了保证不同对象的地址不同,空类也占用存储空间。

实例

注意:子类和父类共用虚函数表和虚函数表指针。

#include<iostream>
using namespace std;

class A     
{     
};    

class B     
{  
    char ch;     
    virtual void func0()  {  }   
};   

class C    
{  
    char ch1;  
    char ch2;  
    virtual void func()  {  }    
    virtual void func1()  {  }   
};  

class D: public A, public C  
{     
    int d;     
    virtual void func()  {  }   
    virtual void func1()  {  }  
};     
class E: public B, public C  
{     
    int e;     
    virtual void func0()  {  }   
    virtual void func1()  {  }  
};  

int main(void)  
{  
    cout<<"A="<<sizeof(A)<<endl;    //result=1  
    cout<<"B="<<sizeof(B)<<endl;    //result=16      
    cout<<"C="<<sizeof(C)<<endl;    //result=16  
    cout<<"D="<<sizeof(D)<<endl;    //result=16  
    cout<<"E="<<sizeof(E)<<endl;    //result=32  
    return 0;  
}  

结果分析:
1.A为空类,所以大小为1
2.B的大小为char数据成员大小+vptr指针大小。由于字节对齐,大小为8+8=16
3.C的大小为两个char数据成员大小+vptr指针大小。由于字节对齐,大小为8+8=16
4.D为多继承派生类,由于D有数据成员,所以继承空类A时,空类A的大小1字节并没有计入当中,D继承C,此情况D只需要一个vptr指针,所以大小为数据成员加一个指针大小。由于字节对齐,大小为8+8=16
5.E为多继承派生类,此情况为我们上面所讲的多重继承,含虚函数覆盖的情况。此时大小计算为数据成员的大小+2个基类虚函数表指针大小
考虑字节对齐,结果为8+8+2*8=32

如何让类不能被继承*

  • 借助final关键字,用该关键字修饰的类不能被继承。
  • 借助友元函数,虚继承和私有构造函数来实现。
#include <iostream>
using namespace std;

template <typename T>
class Base{
    friend T;
private:
    Base(){
        cout << "base" << endl;
    }
    ~Base(){}
};

class B:virtual public Base<B>{   //一定注意 必须是虚继承
public:
    B(){
        cout << "B" << endl;
    }
};

class C:public B{
public:
    C(){}     // error: 'Base<T>::Base() [with T = B]' is private within this context
};


int main(){
    B b;  
    return 0;
}

注意
但是当父类中的成员变量、成员函数或者类本身被 final 关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。

限制类的对象在堆或者栈上创建

参考地址

类对象建立的两种方式:
动态分配类对象:就是使用运算符new来创建一个类的对象,在堆上分配内存。例如:A *p = new A();
静态分配类对象:由编译器为对象在栈空间上分配内存,直接调用类的构造函数创建对象。例如:A a,由编译器创建类对象,在栈上分配内存。

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

class A
{
private:
    void *operator new(size_t t) {}    // 注意函数的第一个参数和返回值都是固定的
    void operator delete(void *ptr) {} // 重载了 new 就需要重载 delete
public:
    A() {}
    ~A() {}
};

限制对象只能建立在堆上
最直观的思想:避免直接调用类的构造函数。
1.将析构函数设置为私有。原因:静态对象建立在栈上,是由编译器分配和释放内存空间,编译器为对象分配内存空间时,会对类的非静态函数进行检查,即编译器会检查析构函数的访问性。当析构函数设为私有时,编译器创建的对象就无法通过访问析构函数来释放对象的内存空间,因此,编译器不会在栈上为对象分配内存。

class A
{
public:
    A() {}
    void destory()
    {
        delete this;
    }

private:
    ~A()
    {
    }
};

该方法存在的问题:

  • 用 new 创建的对象,通常会使用 delete 释放该对象的内存空间,但此时类的外部无法调用析构函数,因此类内必须定义一个 destory() 函数,用来释放 new 创建的对象。(delete释放对象的两个历程)
  • 无法解决继承问题,因为如果这个类作为基类,析构函数要设置成 virtual,然后在派生类中重写该函数,来实现多态。但此时,析构函数是私有的,派生类中无法访问。

2.构造函数设置为 protected,并提供一个 public 的静态函数来完成构造,而不是在类的外部使用 new 构造;将析构函数设置为 protected,也必须定义一个destory()函数,用来释放new创建的对象。原因:类似于单例模式,也保证了在派生类中能够访析构函数。通过调用 create() 函数在堆上创建对象,调用destory()函数来释放对象的内存空间。

class A
{
protected:
    A() {}
    ~A() {}

public:
    static A *create()
    {
        return new A();
    }
    void destory()
    {
        delete this;
    }
};

三种访问和控制权

访问权限

在这里插入图片描述
public:用该关键字修饰的成员表示公有成员,该成员不仅可以在类内可以被访问,在类外也是可以被访问的,是类对外提供的可访问接口;
private:用该关键字修饰的成员表示私有成员,该成员仅在类内可以被访问,在类体外是隐藏状态;
protected:用该关键字修饰的成员表示保护成员,保护成员在类体外同样是隐藏状态,但是对于该类的派生类来说,相当于公有成员,在派生类中可以被访问。

继承权限

若继承方式是public,基类成员在派生类中的访问权限保持不变,也就是说,基类中的成员访问权限,在派生类中仍然保持原来的访问权限;
若继承方式是private,基类所有成员在派生类中的访问权限都会变为私有(private)权限;
若继承方式是protected,基类的公有成员和保护成员在派生类中的访问权限都会变为保护(protected)权限,私有成员在派生类中的访问权限仍然是私有(private)权限。

在这里插入图片描述

第八部分 函数与模板

memset

void *memset(void *str, int c, size_t n) 复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。

memset作用:是在一段内存块中填充某个给定的值,它对较大的结构体或数组进行清零操作的一种最快方法。

memcop

int memcmp(const void *str1, const void *str2, size_t n)) 把存储区 str1 和存储区 str2 的前 n 个字节进行比较。

字符串拼接

char *join1(char *a, char *b) {
	char *c = (char *) malloc(strlen(a) + strlen(b) + 1); 
	//局部变量,用malloc申请内存,strlen不算'\0',所以需要+1
	if (c == NULL) exit (1);
	char *tempc = c; //把首地址存下来
	while (*a != '\0') {
	    *c++ = *a++;
	}
	while ((*c++ = *b++) != '\0') {
	    ;
	}
	//注意,此时指针c已经指向拼接之后的字符串的结尾'\0' !
	return tempc;
	//返回值是局部malloc申请的指针变量,需在函数调用结束后free。
}

strcmp 底层实现

int strcmp(const char* str1, const char* str2)  
{  
    while ((*str1) && (*str1 == *str2))  
    {  
        str1++;  
        str2++;  
    }  
  
  
    if (*(unsigned char*)str1 > *(unsigned char*)str2)  
    {  
        return 1;  
    }  
    else if (*(unsigned char*)str1 < *(unsigned char*)str2)  
    {  
        return -1;  
    }  
    else  
    {  
        return 0;  
    }    
} 

字符串复制函数*

内存重叠问题
1、char * strcpy(char* destination,const char * source);
2、char* strncpy(char* destination,const char* source,size_t num);
3、void * memcpy(void* destination,const void* source,size_t num);
4、void * memmove(void* destination,const void* source,size_t num);
功能及用法

char * strcpy(char* destination,const char * source);

将由source指针指示的C 字符串(包括结尾字符)复制到destination指针指示的区域中。该函数不允许source和destination的区域有重叠,同时,为了避免溢出,destination区域应该至少和source区域一样大。

char* strncpy(char* destination,const char* source,size_t num);

复制source的前num字符到destination。如果遇到null字符(’\0’),且还没有到num个字符时,就用(num - n)(n是遇到null字符前已经有的非null字符个数)个null字符附加到destination。注意:并不是添加到destination的最后,而是紧跟着由source中复制而来的字符后面。下面举例说明:

char des[] = “Hello,i am!”;

char source[] = “abc\0def”;

strncpy(des,source,5);

此时,des区域是这样的:a,b,c,\0,\0,i,空格,a,m,!

\0,\0并不是添加在!的后面。

这里,需要注意strncpy仅仅复制到null字符就结束了。

void * memcpy(void* destination,const void* source,size_t num);

将source区域的前num个字符复制到destination中。该函数不检查null字符(即将null字符当作普通字符处理),意味着将复制num个字符才结束。该函数不会额外地引入null字符,即如果num个字符中没有null字符,那么destination中相应字符序列中也没有null字符。同strcpy的区别:允许将source中null字符后面的字符也复制到destination中,而strcpy和strncpy则不可以

void * memmove(void* destination,const void* source,size_t num);

同memcpy完成同样的功能,区别是,memmove允许destination和source的区域有重叠。而其他三个函数不允许。
例子:

char str[] = “This is a test!;
memmove(str+2,str+10,4);

此时,str变成:Thtests a test!

memcmp函数的底层实现

功能描述:memcpy是内存拷贝函数,memcpy函数的功能是从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中。
参数说明:dst:目的地址;src:源地址;size:所需要复制的字节数
注意:不检查地址重叠的情况。

void *mymemcpy(void *dest, const void *src, size_t count)
 {
     assert(dest != NULL || src != NULL);

     char *tmp = (char *)dest;
     char *p = (char *)src;

     while (count--)
     {
         *tmp++ = *p++;
     }
     return dest;
 }

在这里插入图片描述

memmove函数的底层实现*

功能描述:memcpy是内存拷贝函数,memcpy函数的功能是从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中。
参数说明:dst:目的地址;src:源地址;size:所需要复制的字节数
注意:检查是否有地址重叠的情况。

void *memcpy(void *dst, const void *src, size_t size)
{
    char *psrc;
    char *pdst;

    if (NULL == dst || NULL == src)
    {
        return NULL;
    }

    if ((src < dst) && (char *)src + size > (char *)dst) // 出现地址重叠的情况,自后向前拷贝
    {
        psrc = (char *)src + size - 1;
        pdst = (char *)dst + size - 1;
        while (size--)
        {
            *pdst-- = *psrc--;
        }
    }
    else
    {
        psrc = (char *)src;
        pdst = (char *)dst;
        while (size--)
        {
            *pdst++ = *psrc++;
        }
    }

    return dst;
}

在这里插入图片描述

strcpy()和strncpy()的区别

参考地址
strcpy() 函数用来复制字符串,其原型为:
char *strcpy(char *dest, const char *src);
【参数】dest 为目标字符串指针,src 为源字符串指针。
注意:src 和 dest 所指的内存区域不能重叠,且dest 必须有足够的空间放置 src 所包含的字符串(包含结束符NULL)。
【返回值】成功执行后返回目标数组指针 dest。
strcpy() 把src所指的由NULL结束的字符串复制到dest 所指的数组中,返回指向 dest 字符串的起始地址。
注意:如果参数 dest 所指的内存空间不够大,可能会造成缓冲溢出(buffer Overflow)的错误情况,在编写程序时请特别留意,或者用strncpy()来取代。

strncpy()用来复制字符串的前n个字符,其原型为:
char * strncpy(char *dest, const char *src, size_t n);
【参数说明】dest 为目标字符串指针,src 为源字符串指针。
strncpy()会将字符串src前n个字符拷贝到字符串dest。
不像strcpy(),strncpy()不会向dest追加结束标记’\0’,这就引发了很多不合常理的问题,将在下面的示例中说明。

如果src的前n个字节不含NULL字符,则结果不会以NULL字符结束。
如果src的长度小于n个字节,则以NULL填充dest直到复制完n个字节。
src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。
注意:src 和 dest 所指的内存区域不能重叠,且dest 必须有足够的空间放置n个字符。

strcpy函数的底层实现*

char * strcpy(char* _Dest, const char* _Source)
{
	// assert的作用是先计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,
	// 然后通过调用 abort 来终止程序运行。请看下面的程序清单badptr.
    //检查传入参数的有效性
    assert(NULL != _Dest);
    assert(NULL != _Source);
    if (NULL ==_Dest || NULL == _Source)
         return NULL;
    char* ret = _Dest;
    while((*_Dest++ = *_Source++) != '\0') ;
    return ret;
}

为什么要返回char*类型?
为了实现链式连接。返回内容为指向目标内存的地址指针,这样可以在需要字符指针的函数中使用strcpy,例如strlen(strcpy(str1, str2))。

strcpy 函数有什么缺陷?*

strcpy 函数的缺陷:strcpy 函数不检查目的缓冲区的大小边界,而是将源字符串逐一的全部赋值给目的字符串地址起始的一块连续的内存空间,同时加上字符串终止符,会导致其他变量被覆盖。

strncpy函数的底层实现*

char* strncpy(char* dest, const char* src, size_t n)
{
    assert(NULL != _Dest);
    assert(NULL != _Source);
    char* start = dest;	// 保存dest值,在函数结束后返回
    // 一直拷贝,直到n为0或src全部拷贝完毕
    while (n && (*dest++ = *src++)) {
        n--;
    }        
    // 如果count>0,表示src长度小于等于n,需要给dest添加空字节
    if (n > 0) {
        //使用前置--,是因为在跳出while循环后,count少减一次
        while (--n) {
            *dest++ = '\0';
        }           
    }
    return start;
}

inline函数工作原理*

  • 内联函数不是在调用时发生控制转移关系,而是在编译阶段将函数体嵌入到每一个调用该函数的语句块中,编译器会将程序中出现内联函数的调用表达式用内联函数的函数体来替换。
  • 普通函数是将程序执行转移到被调用函数所存放的内存地址,当函数执行完后,返回到执行此函数前的地方。转移操作需要保护现场,被调函数执行完后,再恢复现场,该过程需要较大的资源开销。

内联函数的优点以及与宏的区别*

以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
优点:

  • 内联函数比宏定义更加的安全,因为前者进行参数检查,而后者仅仅是简单地文本替换。
  • 内联函数在调用时不是像一般函数那样要转去执行被调用函数的函数体,执行完成后在转回调用函数中,执行其后的语句;而是在调用处用内联函数体的代码来替换,这样没有函数压栈,将会节省调用的开销,提高运行效率

内联函数与宏函数的区别:

  • 内联函数是在编译时展开,而宏在编译预处理时展开;
  • 在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
  • 内联函数是真正的函数,和普通函数调用的方法一样,在调用点处直接展开,避免了函数的参数压栈操作,减少了调用的开销。而宏定义编写较为复杂,常需要增加一些括号来避免歧义。
  • 宏定义只进行文本替换,不会对参数的类型、语句能否正常编译等进行检查。而内联函数是真正的函数,会对参数的类型、函数体内的语句编写是否正确等进行检查。
  • 内联函数可以作为类的成员函数,可以使用类的保护和私有成员。

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

在C++中,强制建议使用const代替宏常量,使用内联函数代替宏函数,const和内联函数在进行编译时不仅进行替换,而且还会进行参数类型检测,提高了程序的安全性。内联函数可以是普通函数,也可以是类的成员函数;函数式宏不能作为类的成员函数。

auto 类型推导的原理

auto 类型推导的原理
编译器根据初始值来推算变量的类型,要求用 auto 定义变量时必须有初始值。编译器推断出来的 auto 类型有时和初始值类型并不完全一样,编译器会适当改变结果类型使其更符合初始化规则。

strlen和sizeof的区别

sizeof是运算符,可以是类型做参数 ,在编译的时候就将结果计算出来了,得到的是所占空间的字节大小。strlen是函数,只能以char*(字符串)做参数,计算长度是以 ‘\0’(通过strlen的实现得知)为止,在运行的时候才开始计算结果,计算出来的长度不包括’\0’。

lambda表达式

  • lambda表达式是C++11引入的,lambda表达式是一个变量,可以在调用点就地定义函数,限制它的作用域和生命周期,实现函数的局部化。可以使用 auto 自动推导类型存储 lambda 表达式。
  • lambda 表达式是一个闭包,能够像函数一样被调用,像变量一样被传递;lambda表达式使用“[=]”的方式按值捕获,不能修改,使用“[&]”的方式按引用捕获可以修改,空的“[]”则是无捕获(也就相当于普通函数)。捕获引用时必须要注意外部变量的生命周期,防止变量失效;
  • C++14 里可以使用泛型的 lambda 表达式,相当于简化的模板函数,在语法上使用了auto自动推导类型。

闭包:可以把闭包理解为一个“活的代码块”“活的函数”。它虽然在出现时被定义,但因为保存了定义时捕获的外部变量,就可以跳离定义点,把这段代码“打包”传递到其他地方去执行,而仅凭函数的入口参数是无法做到这一点的。

泛型的 lambda
在 C++14 里,lambda 表达式又多了一项新本领,可以实现“泛型化”,相当于简化了的模板函数,具体语法还是利用了“多才多艺”的 auto。

什么是模板?如何实现?

模板:创建类或者函数的蓝图或者公式,分为函数模板类模板
实现方式:模板定义以关键字 template 开始,后跟一个模板参数列表。

  • 模板参数列表不能为空;
  • 模板类型参数前必须使用关键字 class 或者 typename,在模板参数列表中这两个关键字含义相同,可互换使用。
template <typename T, typename U, ...>

函数模板

函数模板:通过定义一个函数模板,可以避免为每一种类型定义一个新函数。

对于函数模板而言,模板类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。
函数模板实例化:当调用一个模板时,编译器用函数实参来推断模板实参,从而使用实参的类型来确定绑定到模板参数的类型

编译器会对函数模板进行两次编译在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译

类模板

类模板:类似函数模板,类模板以关键字 template 开始,后跟模板参数列表。但是,编译器不能为类模板推断模板参数类型,需要在使用该类模板时,在模板名后面的尖括号中指明类型。

第九部分 数据类型

两个浮点数的比较

浮点数并非真正意义上的实数,只是其在某个范围内的近似。
因此两个浮点数比较大小时,不能简单地使用大于小于号进行比较,应该判断两个浮点数差值的绝对值是否近似为0

C++中四种转换类型以及区别*

static_cast:用于数据的强制类型转换,强制将一种数据类型转换为另一种数据类型。

  • 用于基本数据类型的转换。
  • 同一个继承体系中类型的转换。进行上行转换(派生类的指针或引用转换成基类表示)是安全的进行下行转换(基类的指针或引用转换成派生类表示)由于没有动态类型检查,所以是不安全的,最好用 dynamic_cast 进行下行转换。
  • 任意类型与空指针类型void* 之间的转换。

const_cast:强制去掉常量属性,不能用于去掉变量的常量性,只能用于去除指针或引用的常量性,将常量指针转化为非常量指针或者将常量引用转化为非常量引用(注意:表达式的类型和要转化的类型是相同的)。

reinterpret_cast:可以用于任意类型的指针之间的转换,对转换的结果不做任何保证。改变指针或引用的类型、将指针或引用转换为一个足够长度的整型、将整型转化为指针或引用类型。

dynamic_cast:(1)其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时要进行类型检查。
(2)不能用于内置的基本数据类型的强制转换。
(3)dynamic_cast转换如果成功的话返回的是指向类的指针或引用,对于指针,转换失败则返回nullptr,对于引用,转换失败会抛出异常。
(4)使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过。
需要检测有虚函数的原因:类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义。

5、为什么不使用C的强制转换?

C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

第十部分 STL库

迭代器失效

对于序列式容器(如vector,deque),序列式容器就是数组式容器,删除当前的iterator会使后面所有元素的iterator都失效。这是因为vetor,deque使用了连续分配的内存,删除一个元素导致后面所有的元素会向前移动一个位置。所以不能使用erase(iter++)的方式,还好erase方法可以返回下一个有效的iterator。

对于关联容器(如map, set,multimap,multiset),删除当前的iterator,仅仅会使当前的iterator失效,只要在erase时,递增当前iterator即可。这是因为map之类的容器,使用了红黑树来实现,插入、删除一个结点不会对其他结点造成影响。erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。

STL内存分配

参考地址
在内存分配问题中主要是

  • 容器的迭代器为什么会失效?(容器元素的插入)
  • 容器元素的引用(指针)为什么会失效?(内存的重新分配)
    在这里插入图片描述

STL容器底层实现

STL容器的线程安全

  • 1.每次调用容器的成员函数的期间需要锁定。
  • 2.每个容器容器返回迭代器的生存期需要锁定。
  • 3.每个容器在调用算法的执行期需要锁定。

vector 的底层实现

vector是一个动态增长的数组(底层是数组),在堆中分配内存,里面有一个指针指向一片连续的内存空间,当空间装不下的时候会自动申请一片更大的空间(空间配置器)将原来的数据拷贝到新的空间,然后就会释放旧的空间。当删除的时候空间并不会被释放只是清空了里面的数据。

底层是数组,即将元素保存在一段连续的内存空间中。支持快速随机访问。在尾部之外的位置插入删除元素可能会很慢。

vector的插入删除操作会造成迭代器的失效,对于顺序容器的增删操作都会产生这类问题。

为什么扩容都是2倍
扩容原理概述:

  • 新增元素:Vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,在插入新增的元素;
  • 对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了 ;
  • 初始时刻vector的capacity为0,塞入第一个元素后capacity增加为1;
    不同的编译器实现的扩容方式不一样,VS2015中以1.5倍扩容,GCC以2倍扩容。

增长因子=2的原因

  • vector在push_back以成倍增长可以在均摊后达到O(1)的时间复杂度,相对于增长指定大小的O(n)时间复杂度更好。
  • 为了防止申请内存的浪费,现在使用较多的有2倍与1.5倍的增长方式,而1.5倍的增长方式可以更好的实现对内存的重复利用,因为更好。

string的底层实现

string的长度限制:
编译期的限制:字符串的UTF8编码值的字节数不能超过65535,字符串的长度不能超过65534;
运行时限制:字符串的长度不能超过2^31-1,占用的内存数不能超过虚拟机能够提供的最大值。

map的实现

STL的map底层是用红黑树实现的,查找时间复杂度是log(n),但是插入和删除要维持红黑树的自平衡,所以效率较低。但是有序。

unordered_map的实现

unordered_map的底层实现是hash表,用一个vector来作为指针数组来储存结点的指针,vector中的每一元素指向链表,单链表结构(所有节点都需要next指针来指向下一个节点)。
解决hash冲突的方法:开链法,用hash桶实现。
unordered_map的扩容与vector相似,当增长因子为0.75时,每次以两倍容量进行增长。

map与unordered_map 的比较
map
优点:map内部实现是红黑树,有序性是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作,其复杂度为logN
缺点:空间占用率高,因为每一个节点都需要额外保存父节点,孩子节点以及红/黑性质,使得每一个节点都占用大量的空间。
适用处:对于那些有顺序要求的问题,用map会更高效一些。

unordered_map
优点: 内部实现是哈希表,因此其查找速度非常的快,复杂度为O(1)
缺点: 更改哈希表的大小,重构哈希表比较耗费时间。
适用处:对于查找问题,unordered_map会更加高效一些。

set的实现

set的底层实现:红黑树,可以保证内部元素的有序,查找时间复杂度是log(n)。
set为什么不用哈希表实现?
红黑树是有序的, set更加理解为集合,所以在涉及集合操作并、交、差的时候,都需要进行大量的比较操作,所以底层使用有序结构的红黑树就更加恰当了,而不是去追求更低的访问时间复杂度
应用:top K问题

list的实现

list的底层实现是双向链表,在删除或者插入元素的时候只需要释放一个元素空间即可,时间复杂度是常数,但由于不是连续空间,所以在访问的时候,时间复杂度是O(N)。但由于链表的特点,能高效地进行插入和删除。

链表删除的两种情况

  • 删除结点中“值等于某个给定值”的结点;单链表和双向链表都是O(N)
  • 删除给定指针指向的结点。单链表O(n),双链表O(1)。

deque的实现

Vector 容器是单向开口的连续内存空间,deque则是一种双向开口的连续线性空间。所谓的双向开口,意思是可以在头尾两端分别做元素的插入和删除操作,当然,vector 容器也可以在头尾两端插入元素,但是在其头部操作效率奇差,无法被接受。

deque 容器和 vector 容器最大的差异,一在于 deque 允许使用常数项时间对头端进行元素的插入和删除操作。二在于 deque 没有容量的概念,因为它是动态的以分段连续空间组合而成,随时可以增加一段新的空间并链接起来,换句话说,像 vector 那样,”旧空间不足而重新配置一块更大空间,然后复制元素,再释放旧空间”这样的事情在 deque 身上是不会发生的。也因此,deque 没有必须要提供所谓的空间保留(reserve)功能。

deque与vector的扩容相比

  • vector虽然可以成长,但是只能向尾端生长。其实现原理是:(1) 申请更大空间 (2)原数据复制新空间 (3)释放原空间,这种做法所付出的时间代价是非常大的。
  • deque 是由一段一段的定量的连续空间构成。一旦有必要在 deque 前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在 deque 的头端或者尾端。Deque 最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。
    参考链接.

stack的实现

在STL中栈的的默认容器是双端队列 deque,也可以使用 list 和vector 自定义队列,因为 list 和 vector 都提供了删除最后一个元素的操作(出栈)。

字符串跟容器的区别

但字符串和容器完全是两个不同的概念。字符串是“文本”,里面的字符之间是强关系,顺序不能随便调换,否则就失去了意义,通常应该视为一个整体来处理。而容器是“集合”,里面的元素之间没有任何关系,可以随意增删改,对容器更多地是操作里面的单个元素。

map底层为什么用红黑树实现

1、红黑树:
红黑树是一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树,相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,通常使用红黑树。

性质:

  1. 每个节点非红即黑

  2. 根节点是黑的;

  3. 每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是黑的;

  4. 如果一个节点是红色的,则它的子节点必须是黑色的。

  5. 对于任意节点而言,其到叶子点树NULL指针的每条路径都包含相同数目的黑节点;

2、平衡二叉树(AVL树):

红黑树是在AVL树的基础上提出来的。

平衡二叉树又称为AVL树,是一种特殊的二叉排序树。其左右子树都是平衡二叉树,且左右子树高度之差的绝对值不超过1。

AVL树中所有结点为根的树的左右子树高度之差的绝对值不超过1。

将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF,那么平衡二叉树上的所有结点的平衡因子只可能是-1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。

3、红黑树较AVL树的优点:

AVL 树是高度平衡的,频繁的插入和删除,会引起频繁的rebalance,导致效率下降;红黑树不是高度平衡的,算是一种折中,插入最多两次旋转,删除最多三次旋转。

所以红黑树在查找,插入删除的性能都是O(logn),且性能稳定,所以STL里面很多结构包括map底层实现都是使用的红黑树。

4)红黑树旋转:

旋转:红黑树的旋转是一种能保持二叉搜索树性质的搜索树局部操作。有左旋和右旋两种旋转,通过改变树中某些结点的颜色以及指针结构来保持对红黑树进行插入和删除操作后的红黑性质。

map和set有什么区别

map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)。由于 map 和set所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 map 和set的操作行为,都只是转调 RB-tree 的操作行为。

区别
(1)map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。

(2)set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以STL中将set的迭代器设置成const,不允许修改迭代器的值;而map的迭代器则不允许修改key值,允许修改value值。

(3)map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符[ ]在map应用中需要慎用,const_map不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type类型没有默认值也不应该使用。如果find能解决需要,尽可能用find。

vector和list的区别

1)vector底层实现是数组;list是双向链表。
2)vector支持随机访问,list不支持。
3)vector是顺序内存,list不是。
4)vector在中间节点进行插入删除会导致内存拷贝,list不会。
5)vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
6)vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。

vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。
list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。

第十一部分 语言特性

C++语言的五种范式在这里插入图片描述

面向过程是 C++ 里最基本的一种编程范式。它的核心思想是“命令”,通常就是顺序执行的语句、子程序(函数),把任务分解成若干个步骤去执行,最终达成目标。
面向对象是 C++ 里另一个基本的编程范式。它的核心思想是**“抽象”和“封装”,倡导的是把任务分解成一些高内聚低耦合的对象,这些对象互相通信协作来完成任务。它强调对象之间的关系和接口,而不是完成任务的具体步骤。
泛型编程是自 STL(标准模板库)纳入到 C++ 标准以后才逐渐流行起来的新范式,核心思想是“一切皆为类型”,或者说是“参数化类型”“类型擦除”,使用模板而不是继承的方式来复用代码,所以运行效率更高,代码也更简洁。在 C++ 里,泛型的基础就是 template 关键字,然后是
庞大而复杂的标准库**,里面有各种泛型容器和算法,比如 vector、map、sort,等等。
函数式,核心思想是**“一切皆可调用**”,通过一系列连续或者嵌套的函数调用实现对数据的处理。函数式编程的基础就是lambda表达式。
模板元编程。它的核心思想是“类型运算”,操作的数据是编译时可见的“类型”,所以也比较特殊,代码只能由编译器执行,而不能被运行时的 CPU 执行。

C++的特点?C++11的新特性?

参考地址

1.auto 关键字:自动类型推导,编译器会在 编译期间 通过初始值推导出变量的类型,通过 auto 定义的变量必须有初始值。
2.decltype 类型推导。decltype 关键字:decltype 是“declare type”的缩写,译为“声明类型”。和 auto 的功能一样,都用来在编译时期进行自动类型推导。如果希望从表达式中推断出要定义的变量的类型,但是不想用该表达式的值初始化变量,这时就不能再用 auto。decltype 作用是选择并返回操作数的数据类型。
auto与decltype的区别

auto var = val1 + val2; 
decltype(val1 + val2) var1 = 0; 

auto 根据 = 右边的初始值 val1 + val2 推导出变量的类型,并将该初始值赋值给变量 var;decltype 根据 val1 + val2 表达式推导出变量的类型,变量的初始值和与表达式的值无关。
auto 要求变量必须初始化,因为它是根据初始化的值推导出变量的类型,而 decltype 不要求,定义变量的时候可初始化也可以不初始化。
3.lambda 表达式。lambda 表达式,又被称为 lambda 函数或者 lambda 匿名函数。
4.for循环语法
5.右值引用。右值引用:绑定到右值的引用,用 && 来获得右值引用,右值引用只能绑定到要销毁的对象。
右值引用的作用
(1)避免拷贝,提高性能,实现move()
(2)避免重载参数的复杂性,实现forward()
6.nullptr。nullptr:C++ 11 中的关键字,是一种特殊类型的字面值,可以被转换成任意其他类型。NULL:预处理变量,是一个宏,它的值是 0,定义在头文件 中,即 #define NULL 0。

#include <iostream>
#include <vector>
using namespace std;
int main()
{
    int var = 42;
    int &l_var = var;
    int &&r_var = var; // error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int' 错误:不能将右值引用绑定到左值上

    int &&r_var2 = var + 40; // 正确:将 r_var2 绑定到求和结果上
    return 0;
}

6.标准库 move() 函数。通过该函数可获得绑定到左值上的右值引用,该函数包括在 utility 头文件中。该知识点会在后续的章节中做详细的说明。
move实现:

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
     return static_cast<typename remove_reference<T>::type&&>(t);
}

首先,通过右值引用传递模板实现,利用引用折叠原理将右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变。然后我们通过static_cast<>进行强制类型转换返回T&&右值引用,而static_cast之所以能使用类型转换,是通过remove_refrence::type模板移除T&&,T&的引用,获取具体类型T。

7.智能指针。std::shared_ptr;std::weak_ptr。
8.delete 函数和 default 函数。delete 函数:= delete 表示该函数不能被调用。default 函数:= default 表示编译器生成默认的函数,例如:生成默认的构造函数。
9.多线程。std::thread;st::atomic;std::condition_variable
10.STL容器。std::array;std::forward_list;std::unordered_map;std::unordered_set。

NULL和nullptr

NULL是一个宏,C语言中常数0和(void*)0都是空指针常量。为了解决这种二义性,C++11标准引入了关键字nullptr,它作为一种空指针常量。

C和C++的区别和联系*

面向过程的思路:分析解决问题所需的步骤,用函数把这些步骤依次实现。
面向对象的思路:把构成问题的事务分解为各个对象,建立对象的目的,不是完成一个步骤,而是描述某个事务在解决整个问题步骤中的行为。

联系
C++ 既继承了 C 强大的底层操作特性,又被赋予了面向对象机制。它特性繁多,面向对象语言的多继承,对值传递与引用传递的区分以及 const 关键字,等等。

C++ 对 C 的“增强”,表现在以下几个方面:类型检查更为严格。增加了面向对象的机制、泛型编程的机制(Template)、异常处理、运算符重载、标准模板库(STL)、命名空间(避免全局命名冲突)。

Java 和 C++ 的区别

二者在语言特性上有很大的区别:
指针:C++ 可以直接操作指针,容易产生内存泄漏以及非法指针引用的问题;Java 并不是没有指针,虚拟机(JVM)内部还是使用了指针,只是编程人员不能直接使用指针,不能通过指针来直接访问内存,并且 Java 增加了内存管理机制。
多重继承:C++ 支持多重继承,允许多个父类派生一个类,虽然功能很强大,但是如果使用的不当会造成很多问题,例如:菱形继承;Java 不支持多重继承,但允许一个类可以继承多个接口,可以实现 C++ 多重继承的功能,但又避免了多重继承带来的许多不便。
数据类型和类:Java 是完全面向对象的语言,所有函数和变量部必须是类的一部分。除了基本数据类型之外,其余的都作为类对象,包括数组。对象将数据和方法结合起来,把它们封装在类中,这样每个对象都可实现自己的特点和行为。而 C++ 允许将函数和变量定义为全局的。

垃圾回收:

  • Java 语言一个显著的特点就是垃圾回收机制,编程人员无需考虑内存管理的问题,可以有效的防止内存泄漏,有效的使用空闲的内存。
  • Java 所有的对象都是用 new 操作符建立在内存堆栈上,类似于 C++ 中的 new 操作符,但是当要释放该申请的内存空间时,Java 自动进行内存回收操作,C++ 需要程序员自己释放内存空间,并且 Java 中的内存回收是以线程的方式在后台运行的,利用空闲时间。

应用场景:
Java 运行在虚拟机上,和开发平台无关,C++ 直接编译成可执行文件,是否跨平台在于用到的编译器的特性是否有多平台的支持。
C++ 可以直接编译成可执行文件,运行效率比 Java 高。
Java 主要用来开发 Web 应用。
C++ 主要用在嵌入式开发、网络、并发编程的方面。

Python和C++的区别

语言自身:Python 为脚本语言,解释执行,不需要经过编译;C++ 是一种需要编译后才能运行的语言,在特定的机器上编译后运行。
运行效率:C++ 运行效率高,安全稳定。原因:Python 代码和 C++ 最终都会变成 CPU 指令来跑,但一般情况下,比如反转和合并两个字符串,Python 最终转换出来的 CPU 指令会比 C++ 多很多。首先,Python 中涉及的内容比 C++ 多,经过了更多层,Python 中甚至连数字都是 object ;其次,Python 是解释执行的,和物理机 CPU 之间多了解释器这层,而 C++ 是编译执行的,直接就是机器码,编译的时候编译器又可以进行一些优化。
开发效率:Python 开发效率高。原因:Python 一两句代码就能实现的功能,C++ 往往需要更多的代码才能实现。
书写格式和语法不同:Python 的语法格式不同于其 C++ 定义声明才能使用,而且极其灵活,完全面向更上层的开发者。

第十二部分 源码

拷贝构造函数和赋值构造函数

参考链接
拷贝构造函数其实也是构造函数,只不过它的参数是const 的类自身的对象的引用。如果类里面没有指针成员(该指针成员指向动态申请的空间),是没有必要编写拷贝构造函数的。

拷贝构造是确确实实构造一个新的对象,并给新对象的私有成员赋上参数对象的私有成员的值,新构造的对象和参数对象地址是不一样的,所以如果该类中有一个私有成员是指向堆中某一块内存,如果仅仅对该私有成员进行浅拷贝,那么会出现多个指针指向堆中同一块内存,这是会出现问题,如果那块内存被释放了,就会出现其他指针指向一块被释放的内存,出现未定义的值的问题,如果深拷贝,就不会出现问题,因为深拷贝,不会出现指向堆中同一块内存的问题,因为每一次拷贝,都会开辟新的内存供对象存放其值。

拷贝构造函数何时被调用?
a.对象的直接赋值也会调用拷贝构造函数 ;
b.函数参数传递只要是按值传递也调用拷贝构造函数;
c.函数返回只要是按值返回也调用拷贝构造函数。

但是赋值构造函数是将一个参数对象中私有成员赋给一个已经在内存中占据内存的对象的私有成员,赋值构造函数被赋值的对象必须已经在内存中,否则调用的将是拷贝构造函数,当然赋值构造函数也有深拷贝和浅拷贝的问题。当然赋值构造函数必须能够处理自我赋值的问题,因为自我赋值会出现指针指向一个已经释放的内存。还有赋值构造函数必须注意它的函数原型,参数必须是引用类型,返回值也必须是引用类型,否则在传参和返回的时候都会再次调用一次拷贝构造函数。

拷贝构造函数和赋值运算符重载 为什么要使用引用?

因为实参是通过按值传递机制传递的。在可以传递对象cigar之前,编译器需要安排创建该对象的副本。编译器为了处理复制构造函数的这条调用语句,需要调用复制构造函数来创建实参的副本。但是,由于是按值传递,第二次调用同样需要创建实参的副本,因此还得调用复制构造函数,就这样持续不休。最终得到的是对复制构造函数的无穷调用。(其实就是创建副本也是需要调用复制构造函数的),所以解决办法先是要将形参改为引用形参:

string的实现

class String
{
public:
	String(const char *str = NULL);// 普通构造函数  
	String(const String &other);// 拷贝构造函数  
	~String(void);// 析构函数  
	String & operator = (const String &other);// 赋值函数  
private:
	char *m_data;// 用于保存字符串  
};

//普通构造函数  
String::String(const char *str)
{
	if (str == NULL)
	{
		m_data = new char[1];// 得分点:对空字符串自动申请存放结束标志'\0'的,加分点:对m_data加NULL判断  
		*m_data = '\0';
	}
	else
	{
		int length = strlen(str);
		m_data = new char[length + 1];// 若能加 NULL 判断则更好
		strcpy(m_data, str);
	}
}
 
 
// String的析构函数  
String::~String(void)
{
	delete[] m_data; // 或delete m_data;  
}
 
 
//拷贝构造函数  
String::String(const String &other)// 得分点:输入参数为const型  
{		 
	int length = strlen(other.m_data);
	m_data = new char[length + 1];// 若能加 NULL 判断则更好  
	strcpy(m_data, other.m_data);
}
 
 
//赋值函数  
String & String::operator = (const String &other) // 得分点:输入参数为const型  
{
	if (this == &other)//得分点:检查自赋值  
		return *this; 
	if (m_data)
	    delete[] m_data;//得分点:释放原有的内存资源  
	int length = strlen(other.m_data);
	m_data = new char[length + 1];//加分点:对m_data加NULL判断  
	strcpy(m_data, other.m_data);
	return *this;//得分点:返回本对象的引用    
}

new/delete malloc/free

const

static

问题集

异常捕获

try…catch 语句的执行过程是:
执行 try 块中的语句,如果执行的过程中没有异常拋出,那么执行完后就执行最后一个 catch 块后面的语句,所有 catch 块中的语句都不会被执行;
如果 try 块执行的过程中拋出了异常,那么拋出异常后立即跳转到第一个“异常类型”和拋出的异常类型匹配的 catch 块中执行(称作异常被该 catch 块“捕获”),执行完后再跳转到最后一个 catch 块后面继续执行。

#include <iostream>
using namespace std;
int main()
{
    double m ,n;
    cin >> m >> n;
    try {
        cout << "before dividing." << endl;
        if( n == 0)
            throw -1; //抛出int类型异常
        else
            cout << m / n << endl;
        cout << "after dividing." << endl;
    }
    catch(double d) {
        cout << "catch(double) " << d <<  endl;
    }
    catch(int e) {
        cout << "catch(int) " << e << endl;
    }
    cout << "finished" << endl;
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值