总结——C++方面

文章目录

C++和C的区别

设计思想上:
C++是面向对象的语言,而C是面向过程的结构化编程语言

语法上:

C++具有封装、继承和多态三种特性

C++相比C,增加多许多类型安全的功能,比如强制类型转换、

C++支持范式编程,比如模板类、函数模板

extern“C”

C++调用C函数需要extern C,因为C语言没有函数重载。
作用:
1.使用本文件之外的文件中定义的全局变量
2.在c++中使用c语言定义的函数

C++11中的可变参数模板、右值引用和lambda这几个新特性

可变参数模板:
C++11的可变参数模板,对参数进行了高度泛化,可以表示任意数目、任意类型的参数,其语法为:在class或typename后面带上省略号”。

例如:

Template<class ... T>
void func(T ... args)
{
cout<<”num is”<<sizeof ...(args)<<endl;
}
func();//args不含任何参数

func(1);//args包含一个int类型的实参

func(1,2.0)//args包含一个int一个double类型的实参

其中T叫做模板参数包,args叫做函数参数包

省略号作用如下:

1)声明一个包含0到任意个模板参数的参数包

2)在模板定义得右边,可以将参数包展成一个个独立的参数

C++11可以使用递归函数的方式展开参数包,获得可变参数的每个值。通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数。例如:

#include using namespace std;

// 最终递归函数

void print()

{

cout << "empty" << endl;

}

// 展开函数

template void print(T head, Args... args)
{
cout << head << ","; print(args...);
}
int main()
{
print(1, 2, 3, 4); return 0;
}

参数包Args …在展开的过程中递归调用自己,没调用一次参数包中的参数就会少一个,直到所有参数都展开为止。当没有参数时就会调用非模板函数printf终止递归过程。

右值引用:

C++中,左值通常指可以取地址,有名字的值就是左值而不能取地址,没有名字的就是右值。而在指C++11中,右值是由两个概念构成,将亡值和纯右值。纯右值是用于识别临时变量和一些不跟对象关联的值,比如1+3产生的临时变量值,2、true等,而将亡值通常是指具有转移语义的对象,比如返回右值引用T&&的函数返回值等。

C++11中,右值引用就是对一个右值进行引用的类型。由于右值通常不具有名字,所以我们一般只能通过右值表达式获得其引用,比如:

T && a=ReturnRvale();

假设ReturnRvalue()函数返回一个右值,那么上述语句声明了一个名为a的右值引用,其值等于ReturnRvalue函数返回的临时变量的值。

基于右值引用可以实现转移语义和完美转发新特性。

移动语义:

对于一个包含指针成员变量的类,由于编译器默认的拷贝构造函数都是浅拷贝,所有我们一般需要通过实现深拷贝的拷贝构造函数,为指针成员分配新的内存并进行内容拷贝,从而避免悬挂指针的问题。

但是如下列代码所示:

在这里插入图片描述
当类HasPtrMem包含一个成员函数GetTemp,其返回值类型是HasPtrMem,如果我们定义了深拷贝的拷贝构造函数,那么在调用该函数时需要调用两次拷贝构造函数。第一次是生成GetTemp函数返回时的临时变量,第二次是将该返回值赋值给main函数中的变量a。与此对应需要调用三次析构函数来释放内存。

而在上述过程中,使用临时变量构造a时会调用拷贝构造函数分配对内存,而临时对象在语句结束后会释放它所使用的堆内存。这样重复申请和释放内存,在申请内存较大时会严重影响性能。因此C++使用移动构造函数,从而保证使用临时对象构造a时不分配内存,从而提高性能。

如下列代码所示,移动构造函数接收一个右值引用作为参数,使用右值引用的参数初始化其指针成员变量。
在这里插入图片描述

其原理就是使用在构造对象a时,使用h.d来初始化a,然后将临时对象h的成员变量d指向nullptr,从而保证临时变量析构时不会释放对内存。
在这里插入图片描述

完美转发:

完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另一个函数,即传入转发函数的是左值对象,目标函数就能获得左值对象,转发函数是右值对象,目标函数就能获得右值对象,而不产生额外的开销。

因此转发函数和目标函数参数一般采用引用类型,从而避免拷贝的开销。其次,由于目标函数可能需要能够既接受左值引用,又接受右值引用,所以考虑转发也需要兼容这两种类型。

C++11采用引用折叠的规则,结合新的模板推导规则实现完美转发。其引用折叠规则如下:

在这里插入图片描述
因此,我们将转发函数和目标函数的参数都设置为右值引用类型,

在这里插入图片描述
当传入一个X类型的左值引用时,转发函数将被实例为:
在这里插入图片描述

经过引用折叠,变为:
在这里插入图片描述

当传入一个X类型的右值引用时,转发函数将被实例为:

在这里插入图片描述
经过引用折叠,变为:
在这里插入图片描述

除此之外,还可以使用forward()函数来完成左值引用到右值引用的转换:
在这里插入图片描述

Lambda表达式:

Lambda表达式定义一个匿名函数,并且可以捕获一定范围内的变量,其定义如下:

capturemutable->return-type{statement}

其中,

[capture]:捕获列表,捕获上下文变量以供lambda使用。同时[]是lambda寅初复,编译器根据该符号来判断接下来代码是否是lambda函数。

(Params):参数列表,与普通函数的参数列表一致,如果不需要传递参数,则可以连通括号一起省略。

mutable是修饰符,默认情况下lambda函数总是一个const函数,Mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略。

->return-type:返回类型是返回值类型

{statement}:函数体,内容与普通函数一样,除了可以使用参数之外,还可以使用所捕获的变量。

Lambda表达式与普通函数最大的区别就是其可以通过捕获列表访问一些上下文中的数据。其形式如下:

在这里插入图片描述

Lambda的类型被定义为“闭包”的类,其通常用于STL库中,在某些场景下可用于简化仿函数的使用,同时Lambda作为局部函数,也会提高复杂代码的开发加速,轻松在函数内重用代码,无须费心设计接口。

C++源文件从文本到可执行文件经历的过程?

对于C++源文件,从文本到可执行文件一般需要四个过程:
**预处理阶段:**对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。

**编译阶段:**将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件

汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件

**链接阶段:**将多个目标文件及所需要的库连接成最终的可执行目标文件

一.四个步骤
对于C/C++编写的程序,从源代码到可执行文件,一般经过下面四个步骤:
1).预处理,产生.ii文件
2).编译,产生汇编文件(.s文件)
3).汇编,产生目标文件(.o或.obj文件)
4).链接,产生可执行文件(.out或.exe文件)

以hello.c为例,这个过程可以用下面的图来表示
在这里插入图片描述
二.预处理
预处理主要包含下面的内容:
a.对所有的“#define”进行展开;
b.处理所有的条件编译指令,比如“#if”,“#ifdef”,“#elif”,“#else”,“#endif”
c.处理**“#include”指令,这个过程是递归的,也就是说被包含的文件可能还包含其他文件
d.删除所有的注释“//”和“/
/”
e.添加行号和文件标识
f.保留所有的“#pragma”编译器指令
经过预处理后的.ii文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到.ii文件中。

三.编译
编译的过程就是将预处理完的文件进行一系列词法分析,语法分析,语义分析及优化后生成相应的汇编代码文件(.s文件)

四.汇编
汇编器是将汇编代码转变成机器可以执行的代码,每一个汇编语句几乎都对应一条机器指令。最终产生目标文件(.o或.obj文件)。

五.链接
链接的过程主要包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)

原文链接:https://blog.csdn.net/sheng_ai/article/details/47860403

include头文件的顺序以及双引号””和尖括号<>的区别?

Include头文件的顺序:对于include的头文件来说,如果在文件a.h中声明一个在文件b.h中定义的变量,而不引用b.h。那么要在a.c文件中引用b.h文件,并且要先引用b.h,后引用a.h,否则汇报变量类型未声明错误。
双引号和尖括号的区别:编译器预处理阶段查找头文件的路径不一样

对于使用双引号包含的头文件,查找头文件路径的顺序为:

当前头文件目录
编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径

对于使用尖括号包含的头文件,查找头文件的路径顺序为:

编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径

#include <>格式:引用标准库头文件,编译器从标准库目录开始搜索
#incluce ""格式:引用非标准库的头文件,编译器从用户的工作目录开始搜索

预处理器发现 #include 指令后,就会寻找后跟的文件名并把这个文件的内容包含到当前文件中。被包含文件中的文本将替换源代码文件中的#include指令,就像你把被包含文件中的全部内容键入到源文件中的这个位置一样。
#include 指令有两种使用形式
#include <stdio.h> 文件名放在尖括号中
#include “mystuff.h” 文件名放在双引号中

尖括号< 和> 括起来表明这个文件是一个工程或标准头文件。查找过程会检查预定义的目录,我们可以通过设置搜索路径环境变量或命令行选项来修改这些目录。
如果文件名用一对引号括起来则表明该文件是用户提供的头文件,查找该
文件时将从当前文件目录(或文件名指定的其他目录)中寻找文件,然后再在标准位置寻找文件。

原文链接:https://blog.csdn.net/finded/article/details/50478885

new和malloc的区别

1、new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配
2、new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。

3、new不仅分配一段内存,而且会调用构造函数,malloc不会。

4、new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。

5、new是一个操作符可以重载,malloc是一个库函数。

6、malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作。

7、new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。

8、申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。

malloc的原理,另外brk系统调用和mmap系统调用的作用分别是什么?

Malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,**malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。**当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。
当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。

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

C++函数栈空间的最大值

默认是1M,不过可以调整

C语言是怎么进行函数调用的?

每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的esp指针压栈。

C++如何处理返回值?

生成一个临时变量,把它的引用作为函数参数传入函数内。

C语言参数压栈顺序?

从右到左

C++的内存管理是怎样的?

在这里插入图片描述
在这里插入图片描述
在C++中,虚拟内存分为代码段数据段BSS段堆区、文件映射区以及栈区六部分。
代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码

数据段:存储程序中已初始化的全局变量和静态变量

bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。

堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。

映射区:存储动态链接库以及调用mmap函数进行的文件映射

栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值
在这里插入图片描述
32bitCPU可寻址4G线性空间,每个进程都有各自独立的4G逻辑地址,其中0~3G是用户态空间,3~4G是内核空间,不同进程相同的逻辑地址会映射到不同的物理地址中。其逻辑地址其划分如下:

各个段说明如下:

3G用户空间和1G内核空间

静态区域:

text segment(代码段):包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。

data segment(数据段):存储程序中已初始化的全局变量和静态变量

**bss segment:**存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量,对于未初始化的全局变量和静态变量,程序运行main之前时会统一清零。即未初始化的全局变量编译器会初始化为0

动态区域:

heap(堆): 当进程未调用malloc时是没有堆段的,只有调用malloc时采用分配一个堆,并且在程序运行过程中可以动态增加堆大小(移动break指针),从低地址向高地址增长。分配小内存时使用该区域。 堆的起始地址由mm_struct 结构体中的start_brk标识,结束地址由brk标识。

memory mapping segment(映射区):存储动态链接库等文件映射、申请大内存(malloc时调用mmap函数)

stack(栈):使用栈空间存储函数的返回地址、参数、局部变量、返回值,从高地址向低地址增长。在创建进程时会有一个最大栈大小,Linux可以通过ulimit命令指定。

什么是内存泄露?如何判断内存泄漏?

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

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

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

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

内存泄漏通常是由于调用了malloc/new等内存申请的操作,但是缺少了对应的free/delete。为了判断内存是否泄露,我们一方面可以使用linux环境下的内存泄漏检查工具Valgrind,另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。

什么时候会发生段错误

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

试图修改字符串常量的内容

产生段错误就是访问了错误的内存段,一般是你没有权限,或者根本就不存在对应的物理内存,尤其常见的是访问0地址。

一般来说,段错误就是指访问的内存超出了系统所给这个程序的内存空间,通常这个值是由gdtr来保存的,它是一个48位的寄存器,其中的32位是保 存由它指向的gdt表,后13位保存相应于gdt的下标,最后3位包括了程序是否在内存中以及程序在cpu中的运行级别,指向的gdt是以64位为一个单 位的表,在这张表中就保存着程序运行的代码段、数据段的起始地址、与此相应的段限和页面交换、程序运行级别还有内存粒度等等的信息。一旦一个程序发生了越 界访问,cpu就会产生相应的异常保护,于是segmentation fault就出现了.

在编程中以下几类做法容易导致段错误,基本是是错误地使用指针引起的:

1)访问系统数据区,尤其是往系统保护的内存地址写数据,最常见的就是给一个指针以0地址;
2)内存越界(数组越界,变量类型不一致等) 访问到不属于你的内存区域。

另外,缓存溢出也可能引起“段错误”,对于这种while(1) {do}的程序,这个问题最容易发生,多此sprintf或着strcat有可能将某个buff填满,溢出,所以每次使用前,最好memset一下,不过要是一开始就是段错误,而不是运行了一会儿出现的,缓存溢出的可能性就比较小。

共享内存相关api

Linux允许不同进程访问同一个逻辑内存,提供了一组API,头文件在sys/shm.h中。
1)新建共享内存shmget

int shmget(key_t key,size_t size,int shmflg);

key:共享内存键值,可以理解为共享内存的唯一性标记。

size:共享内存大小

shmflag:创建进程和其他进程的读写权限标识。

返回值:相应的共享内存标识符,失败返回-1

2)连接共享内存到当前进程的地址空间shmat

void *shmat(int shm_id,const void *shm_addr,int shmflg);

shm_id:共享内存标识符

shm_addr:指定共享内存连接到当前进程的地址,通常为0,表示由系统来选择。

shmflg:标志位

返回值:指向共享内存第一个字节的指针,失败返回-1

3)当前进程分离共享内存shmdt

int shmdt(const void *shmaddr);

4)控制共享内存shmctl

和信号量的semctl函数类似,控制共享内存

int shmctl(int shm_id,int command,struct shmid_ds *buf);

shm_id:共享内存标识符

command: 有三个值

IPC_STAT:获取共享内存的状态,把共享内存的shmid_ds结构复制到buf中。

IPC_SET:设置共享内存的状态,把buf复制到共享内存的shmid_ds结构。

IPC_RMID:删除共享内存

buf:共享内存管理结构体。

在这里插入图片描述

共享内存区是最快的IPC(进程间通信)形式。
用共享内存从服务器拷贝文件数据到客户端:

共享内存基本API:

#include<sys/ipc.h>
#include<sys/shm.h>

1. int shmget(key_t key,size_t size,int shmflg);
功能:用来创建共享内存

key:是这个共享内存段的名字
size:共享内存的大小
shmflg:相当于权限位(如0666)
返回值是共享内存段的标识码shmid,
例如:shmid = shmget(0x1111, 128, 0666);
//创建共享内存 , 相当于打开打开文件
//若共享内存存在 则使用 fopen()
//若共享内存 不存在 则报错 -1

shmid = shmget(0x1111, 128, 0666 | IPC_CREAT);
//创建共享内存 , 相当于打开打开文件
//若共享内存存在 则使用 fopen()
//若共享内存 不存在 则创建

shmid = shmget(0x1111, 128, 0666 | IPC_CREAT | IPC_EXCL);
//创建共享内存 , 相当于打开文件
//若共享内存存在 则报错
//若共享内存 不存在 则创建
//作用 IPC_EXCL判断存在不存在的标志 避免已经存在的文件 被覆盖

**2. void shmat(int shmid, const void shmaddr, int shmflg);0xaa11
功能:将共享内存段连接到进程地址空间

shmaddr:指定连接的地址,因为内存地址是段页式管理,所以有可能传入的地址并不就是那一页的开头位置,所以传入一个地址,传出的仍然是一个地址,传出的是具体开始存储的地址。所以我们通常传入NULL,让编译器直接分配个合适的位置给我们。

shmflg:它的两个取值可能是SHM_RND和SHM_RDONLY.

例: void *p = shmat(shmid, NULL, 0);

返回值:成功返回一个指针,指向共享内存第一个节,失败返回-1;

*3, int shmdt(const void shmaddr);
功能:将共享内存段与当前进程脱离,但并不等于删除共享内存段

*4, int shmctl(int shmid,int cmd,struct shmid_ds buf);
功能:用于控制共享内存

cmd:将要采取的动作

1,IPC_STAT 把shmid_ds结构中的数据设置为共享内存的当前关联值

2,IPC_SET 在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值

3,IPC_RMID 删除共享内存段

buf: 指向一个保存着共享内存的模式状态和访问权限的数据结构

例: shmctl(shmid, IPC_RMID, NULL);

//删除共享内存

若想要把旧的共享内存里面的内容保存下来,则传入一个地址,用来完成保存的功能

为什么链接共享内存时要设计shmid,创建时要传入key:

共享内存私有:

在这里插入图片描述

reactor模型组成

从这个描述中,我们知道Reactor模式首先是事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。如果用图来表达:
在这里插入图片描述
从结构上,这有点类似生产者消费者模式,即有一个或多个生产者将事件放入一个Queue中,而一个或多个消费者主动的从这个Queue中Poll事件来处理;而Reactor模式则并没有Queue来做缓冲,每当一个Event输入到Service Handler之后,该Service Handler会主动的根据不同的Event类型将其分发给对应的Request Handler来处理。

reactor模型要求主线程只负责监听文件描述上是否有事件发生,有的话就立即将该事件通知工作线程,除此之外,主线程不做任何其他实质性的工作,读写数据、接受新的连接以及处理客户请求均在工作线程中完成。其模型组成如下:
在这里插入图片描述
1)**Handle:**即操作系统中的句柄,是对资源在操作系统层面上的一种抽象,它可以是打开的文件、一个连接(Socket)、Timer等。由于Reactor模式一般使用在网络编程中,因而这里一般指Socket Handle,即一个网络连接。

2)Synchronous Event Demultiplexer(同步事件复用器):阻塞等待一系列的Handle中的事件到来,如果阻塞等待返回,即表示在返回的Handle中可以不阻塞的执行返回的事件类型。这个模块一般使用操作系统的select来实现。

3)Initiation Dispatcher:用于管理Event Handler,即EventHandler的容器,用以注册、移除EventHandler等;另外,它还作为Reactor模式的入口调用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle将其分发给对应的Event Handler处理,即回调EventHandler中的handle_event()方法。

4)Event Handler:定义事件处理方法:handle_event(),以供InitiationDispatcher回调使用。

5)Concrete Event Handler:事件EventHandler接口,实现特定事件处理逻辑。

1、标准定义
两种I/O多路复用模式:Reactor和Proactor

一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。分离器对象可将来自事件源的I/O事件分离出来并分发到对应的read/write事件处理器(Event Handler)。开发人员预先注册需要处理的事件及其事件处理器(或回调函数);事件分离器负责将请求事件传递给事件处理器。两个与事件分离器有关的模式是Reactor和Proactor。Reactor模式采用同步IO,而Proactor采用异步IO。

在Reactor中,事件分离器负责等待文件描述符或socket为读写操作准备就绪,然后将就绪事件传递给对应的处理器,最后由处理器负责完成实际的读写工作。

而在Proactor模式中,处理器–或者兼任处理器的事件分离器,只负责发起异步读写操作。IO操作本身由操作系统来完成。传递给操作系统的参数需要包括用户定义的数据缓冲区地址和数据大小,操作系统才能从中得到写出操作所需数据,或写入从socket读到的数据。事件分离器捕获IO操作完成事件,然后将事件传递给对应处理器。比如,在windows上,处理器发起一个异步IO操作,再由事件分离器等待IOCompletion事件。典型的异步模式实现,都建立在操作系统支持异步API的基础之上,我们将这种实现称为“系统级”异步或“真”异步,因为应用程序完全依赖操作系统执行真正的IO工作。

举个例子,将有助于理解Reactor与Proactor二者的差异,以读操作为例(类操作类似)。
在Reactor中实现读:

  • 注册读就绪事件和相应的事件处理器

  • 事件分离器等待事件

  • 事件到来,激活分离器,分离器调用事件对应的处理器。

  • 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。
    在Proactor中实现读:

  • 处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。

  • 事件分离器等待操作完成事件

  • 在分离器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分离器读操作完成。

  • 事件分离器呼唤处理器。

  • 事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分离器。

可以看出,两个模式的相同点,都是对某个IO事件的事件通知(即告诉某个模块,这个IO操作可以进行或已经完成)。在结构上,两者也有相同点:demultiplexor负责提交IO操作(异步)、查询设备是否可操作(同步),然后当条件满足时,就回调handler;不同点在于,异步情况下(Proactor),当回调handler时,表示IO操作已经完成;同步情况下(Reactor),回调handler时,表示IO设备可以进行某个操作(can read or can write)。

2、通俗理解
使用Proactor框架和Reactor框架都可以极大的简化网络应用的开发,但它们的重点却不同。

Reactor框架中用户定义的操作是在实际操作之前调用的。比如你定义了操作是要向一个SOCKET写数据,那么当该SOCKET可以接收数据的时候,你的操作就会被调用;而Proactor框架中用户定义的操作是在实际操作之后调用的。比如你定义了一个操作要显示从SOCKET中读入的数据,那么当读操作完成以后,你的操作才会被调用。

Proactor和Reactor都是并发编程中的设计模式。在我看来,他们都是用于派发/分离IO操作事件的。这里所谓的IO事件也就是诸如read/write的IO操作。"派发/分离"就是将单独的IO事件通知到上层模块。两个模式不同的地方在于,Proactor用于异步IO,而Reactor用于同步IO。

3、备注
其实这两种模式在ACE(网络库)中都有体现;如果要了解这两种模式,可以参考ACE的源码,ACE是开源的网络框架,非常值得一学。。

设计一下如何采用单线程的方式处理高并发

在单线程模型中,可以采用I/O复用来提高单线程处理多个请求的能力,然后再采用事件驱动模型,基于异步回调来处理事件来

(1)系统拆分,将一个系统拆分为多个子系统,用dubbo来搞。然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,不也可以抗高并发么。

(2)缓存,必须得用缓存。大部分的高并发场景,都是读多写少,那你完全可以在数据库和缓存里都写一份,然后读的时候大量走缓存不就得了。毕竟人家redis轻轻松松单机几万的并发啊。没问题的。所以你可以考虑考虑你的项目里,那些承载主要请求的读场景,怎么用缓存来抗高并发。

(3)MQ,必须得用MQ。可能你还是会出现高并发写的场景,比如说一个业务操作里要频繁搞数据库几十次,增删改增删改,疯了。那高并发绝对搞挂你的系统,你要是用redis来承载写那肯定不行,人家是缓存,数据随时就被LRU了,数据格式还无比简单,没有事务支持。所以该用mysql还得用mysql啊。那你咋办?用MQ吧,大量的写请求灌入MQ里,排队慢慢玩儿,后边系统消费后慢慢写,控制在mysql承载范围之内。所以你得考虑考虑你的项目里,那些承载复杂写业务逻辑的场景里,如何用MQ来异步写,提升并发性。MQ单机抗几万并发也是ok的,这个之前还特意说过。

(4)分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,好吧,那么就将一个数据库拆分为多个库,多个库来抗更高的并发;然后将一个表拆分为多个表,每个表的数据量保持少一点,提高sql跑的性能。

(5)读写分离,这个就是说大部分时候数据库可能也是读多写少,没必要所有请求都集中在一个库上吧,可以搞个主从架构,主库写入,从库读取,搞一个读写分离。读流量太多的时候,还可以加更多的从库。

(6)Elasticsearch,可以考虑用es。es是分布式的,可以随便扩容,分布式天然就可以支撑高并发,因为动不动就可以扩容加机器来抗更高的并发。那么一些比较简单的查询、统计类的操作,可以考虑用es来承载,还有一些全文搜索类的操作,也可以考虑用es来承载。

原文链接:https://blog.csdn.net/admin19921022/article/details/90599155

strcpy和strlen

strcpy是字符串拷贝函数,原型:
char strcpy(char dest, const char *src);

从src逐字节拷贝到dest,直到遇到’\0’结束,因为没有指定长度,可能会导致拷贝越界,造成缓冲区溢出漏洞,安全版本是strncpy函数。
strlen函数是计算字符串长度的函数,返回从开始到’\0’之间的字符个数。

字符串拷贝strcpy函数

字符串拷贝函数,strcpt(st1,str2),注意1要比2长,否则会越界,越界会造成非法访问;
在拷贝时,会从源地址往后面拷贝,直到遇到‘\0’为止,为了防止越界,可以在拷贝的最后加上‘\0’;
或者说两个长度相等的字符串进行strcpy时,最后一位必须是’\0’,要不就别拷,要拷就加’\0’;
完整的strcpy函数:

char * strcpy (char * str1 , const char * str2) //源字符串用const修饰防止修改
{
	assert(  (str1 != NULL) && (str2 != NULL)  ); //检查指针有效性
	char *address  = str1; 
	while(  (*str1++ = * str2++) != '\0'); //先赋值指针,赋值后都加1,只要str1不是'\0'就说明没到终点可以继续
	return address;
}

检查代码题常出的错误

1:传入形参并不能真正改变形参的值,执行完之后为空;
2:在函数GetMemory中和Test中没有malloc对应的free,造成内存泄露

void GetMemory( char *p )
{
 p = (char *) malloc( 100 );
}
void Test( void ) 
{
 char *str = NULL;
 GetMemory( str ); 
 strcpy( str, "hello world" );
 printf( str );
}

3.p[]数组为函数内的局部自动变量,在函数返回后,内存已经被释放。这是许多程序员常犯的错误,其根源在于不理解变量的生存期

char p[] = "hello world"; 
return p; 

4.使用malloc后应该检查是否申请成功

if ( *p == NULL )
{
 ...//进行申请内存失败处理
}
同时应考虑num>0

5.free指针后,可能会变成野指针,还要加上str = NULL
而且也没有判断是否申请成功

void Test( void )
{
 char *str = (char *) malloc( 100 );
 strcpy( str, "hello" );
 free( str ); 
 ... //省略的其它语句
}

6.定义函数没有返回值
7. 在swap函数中,p是一个“野”指针,有可能指向系统区,导致程序运行的崩溃

swap( int* p1,int* p2 )
{
 int *p;
 *p = *p1;
 *p1 = *p2;
 *p2 = *p;
}

应该是int p;

不同类型if与零值比较

bool: var !var
int : var0 var != 0
float:浮点型变量并不精确,所以不可将float变量用“
”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式,所以

const float e = 0.00001;
if ( (var <= e) && (var>=-e)  )

sizeof

int arr[] = {1, 2, 3, 4};

sizeof(arr) = 16; //数组名为全部长度

char arr[] = "abc";

sizeof(arr) = 4; //有个‘\0’

strlen(arr)=3 //长度是3

char arr[] = {'1', '2', '3'};

sizeof(arr) = 3; //全部长度

strlen(arr)=3
//arr1 is an array of ints
int *source=arr1;
size_t sz=sizeof(arr1)/sizeof(*arr1);//number of elements
int *dest=new int[sz];
while(source!=arr1+sz)
    *dest++=*source++;//copy element and increment pointers

1.source是一个指向数组arr1的第一个元素的指针.(开始时,source与arr1的值是一样的.)

2.sizeof操作符的作用是返回一个对象或类型名的长度,返回值类型为size_t,长度的单位是字节.

3.对数组做sizeof操作等效于将对其元素类型做sizeof操作的结果乘上数组元素的个数.因为sizeof返回整个数组在内存中的存储长度.

4.*sizeof(arr1),对指针arr1做sizeof操作,返回arr1所指的数组的第一个int型元素的类型长度.在这里,下面三种写法是等效的:

*sizeof(*arr1)=sizeof(source)=sizeof(int)

5.source!=arr+sz,这里是在比较地址(没有解引用),这里的数组名arr就自动转化成了指向数组第一个元素的指针,加几就指向其后面的第几个元素.

6.比较sizeof(arr1)和sizeof(arr1).arr1不解引用做sizeof操作,编译器能判断出来这就是一个普通的指针还是一个数组名指针,如果这是个数组名,就返回整个数组在内存中的存储长度,否则就返回存放指针所需的内存大小,比如指针p里存放的地址是0x22ff50,那么存放这个地址需要4字节,则sizeof§返回4.而对于arr1,这里arr1只是一个普通的指针.

char a[7]={'1','2','3','4','5','6','7'};
char *b=&a[0];
char *c=a;
cout<<sizeof(a)<<endl; //数组长度
cout<<sizeof(a+2)<<endl; //第三个元素长度
cout<<sizeof(b)<<endl; //只是个指针,和a不同
cout<<sizeof(c)<<endl; //只是个指针

输出的结果为:

7

4

4

4

int main(){
int data[]={2,45,7,8,9,23,4,6,7,8,0};
 int data1[12];
 int *data2 = new int[13];
 char *data3 = new char[10];
 cout<<length(data)<<" "<<sizeof(data[0])<<" "<<sizeof(data)<<endl;
 cout<<length(data1)<<" "<<sizeof(data1[0])<<" "<<sizeof(data1)<<endl;
 cout<<length(data2)<<" "<<sizeof(data2[0])<<" "<<sizeof(data2)<<endl;
 cout<<length(data3)<<" "<<sizeof(data3[0])<<" "<<sizeof(data3)<<endl;

1和2的指针具有特殊性,数组指针代表全部,但是后面new出来的仅仅指向第一个元素,失去这个数组指针的标志了
在这里插入图片描述

void Func ( char str[100] )
{
 sizeof( str ) = ?
}
void *p = malloc( 100 );
sizeof ( p ) = ?

4
4


**数组名作为函数形参时,沦为普通指针。 **


Windows NT 32位平台下,指针的长度(占用内存的大小)为4字节,故sizeof( str ) 、sizeof ( p ) 都为4。

以下四行代码的区别是什么?

const char * arr = "123";

char * brr = "123";

const char crr[] = "123";

char drr[] = "123";

参考答案
参考回答:

const char * arr = “123”;
//字符串123保存在常量区,const本来是修饰arr指向的值不能通过arr去修改,但是字符串“123”在常量区,本来就不能改变,所以加不加const效果都一样

char * brr = “123”;

//字符串123保存在常量区,这个arr指针指向的是同一个位置,同样不能通过brr去修改"123"的值

const char crr[] = “123”;

//这里123本来是在栈上的,但是编译器可能会做某些优化,将其放到常量区

char drr[] = “123”;

//字符串123保存在栈区,可以通过drr去修改

写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个。

#define MIN(A,B) (  (A)<=(B)?(A):(B)  )

宏定义可以实现类似于函数的功能,但是它终归不是函数,而宏定义中括弧中的“参数”也不是真的参数,在宏展开的时候对“参数”进行的是一对一的替换。
程序员对宏定义的使用要非常小心,特别要注意两个问题:
(1)谨慎地将宏定义中的“参数”和整个宏用用括弧括起来,且一定不能加;
( 2) 防止宏的副作用。

least = MIN(*p++, b);

宏定义#define MIN(A,B) ((A) <= (B) ? (A) : (B))对MIN(*p++, b)的作用结果是:

((*p++) <= (b) ? (*p++) : (b))
这个表达式会产生副作用,指针p会作2次++自增操作。

编译宏

#ifndef __INCvxWorksh
#define __INCvxWorksh 
#ifdef __cplusplus
extern "C" {
#endif 
/*...*/ 
#ifdef __cplusplus
}
#endif 
#endif /* __INCvxWorksh */

的作用是防止被重复引用
作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在symbol库中的名字与C语言的不同。例如,假设某个函数的原型为:
void foo(int x, int y);
该函数被C编译器编译后在symbol库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。_foo_int_int这样的名字包含了函数名和函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。
为了实现C和C++的混合编程,C++提供了C连接交换指定符号extern "C"来解决名字匹配问题,函数声明前加上extern "C"后,则编译器就会按照C语言的方式将该函数编译为_foo,这样C语言中就可以调用C++的函数了

string类的编写

编写类String的构造函数、析构函数和赋值函数,已知类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];  //要长1位
 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];     
 strcpy(m_data, other.m_data); 
}
//赋值函数
String & String::operator =(const String &other) // 得分点:输入参数为const型
{ 
 if(this == &other)   //得分点:检查自赋值
 return *this; 
 delete [] m_data;     //得分点:释放原有的内存资源
 int length = strlen( other.m_data ); 
 m_data = new char[length+1];  
 strcpy( m_data, other.m_data ); 
 return *this;         //得分点:返回本对象的引用
}

C++赋值函数详解

赋值函数
每个类只有一个赋值函数  
由于并非所有的对象都会使用拷贝构造函数和赋值函数,程序员可能对这两个函数有些轻视。
   1,**如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。**倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。
以类String的两个对象a,b为例,假设a.m_data的内容为“hello”,b.m_data的内容为“world”。   
现将a赋给b,缺省赋值函数的“位拷贝”意味着执行b.m_data = a.m_data。
这将造成三个错误:
一是b.m_data原有的内存没被释放,造成内存泄露;
二是b.m_data和a.m_data指向同一块内存,a或b任何一方变动都会影响另一方;
三是在对象被析构时,m_data被释放了两次。   
2,拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。**拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。**以下程序中,第三个语句和第四个语句很相似,你分得清楚哪个调用了拷贝构造函数,哪个调用了赋值函数吗?  
  String a(“hello”);   
String b(“world”);
String c = a;   // 调用了拷贝构造函数,最好写成 c(a);
   c = b;      // 调用了赋值函数   
本例中第三个语句的风格较差,宜改写成String c(a) 以区别于第四个语句。
类String拷贝构造函数与普通构造函数的区别是:在函数入口处无需与NULL进行比较,这是因为“引用”不可能是NULL,而“指针”可以为NULL。

类String的赋值函数比构造函数复杂得多,分四步实现:

1)**第一步,检查自赋值。**你可能会认为多此一举,难道有人会愚蠢到写出 a = a 这样的自赋值语句!的确不会。但是间接的自赋值仍有可能出现,例如   // 内容自赋值   b = a;   …   c = b;   …   a = c;   // 地址自赋值   b = &a;   …   a = *b;   也许有人会说:“即使出现自赋值,我也可以不理睬,大不了化点时间让对象复制自己而已,反正不会出错!”   他真的说错了。看看第二步的delete,自杀后还能复制自己吗?所以,如果发现自赋值,应该马上终止函数。注意不要将检查自赋值的if语句   if(this == &other)   错写成为   if( *this == other)

2)第二步,用delete释放原有的内存资源。如果现在不释放,以后就没机会了,将造成内存泄露。   
3)第三步,分配新的内存资源,并复制字符串。注意函数strlen返回的是有效字符串长度,不包含结束符‘\0’。函数strcpy则连‘\0’一起复制。

4)第四步,返回本对象的引用,目的是为了实现象 a = b = c 这样的链式表达。注意不要将 return *this 错写成 return this 。那么能否写成return other 呢?效果不是一样吗?    不可以!因为我们不知道参数other的生命期。有可能other是个临时对象,在赋值结束后它马上消失,那么return other返回的将是垃圾。   偷懒的办法处理拷贝构造函数与赋值函数   如果我们实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,怎么办?

C++中构造函数,拷贝构造函数和赋值函数的区别和实现

C++中一般创建对象,拷贝或赋值的方式有构造函数,拷贝构造函数,赋值函数这三种方法。下面就详细比较下三者之间的区别以及它们的具体实现

1.构造函数
构造函数是一种特殊的类成员函数,是当创建一个类的对象时,它被调用来对类的数据成员进行初始化和分配内存。(构造函数的命名必须和类名完全相同)

首先说一下一个C++的空类,编译器会加入哪些默认的成员函数

·默认构造函数和拷贝构造函数

·析构函数

·赋值函数(赋值运算符)

·取值函数

**即使程序没定义任何成员,编译器也会插入以上的函数!

注意:构造函数可以被重载,可以多个,可以带参数;
析构函数只有一个,不能被重载,不带参数

而默认构造函数没有参数,它什么也不做。当没有重载无参构造函数时,

A a就是通过默认构造函数来创建一个对象

下面代码为构造函数重载的实现

[cpp] view plain copy

class A  
{  
int m_i;  
Public:  
  A()   
{  
 Cout<<”无参构造函数”<<endl;  
}  
A(int i):m_i(i) {}  //初始化列表  
}</span>  
 

2.拷贝构造函数

拷贝构造函数是C++独有的,它是一种特殊的构造函数,用基于同一类的一个对象构造和初始化另一个对象。

当没有重载拷贝构造函数时,通过默认拷贝构造函数来创建一个对象

A a;

A b(a);

A b=a; 都是拷贝构造函数来创建对象b

强调:这里b对象是不存在的,是用a 对象来构造和初始化b的!!

先说下什么时候拷贝构造函数会被调用:

在C++中,3种对象需要复制,此时拷贝构造函数会被调用

1)一个对象以值传递的方式传入函数体
2)一个对象以值传递的方式从函数返回
3)一个对象需要通过另一个对象进行初始化

什么时候编译器会生成默认的拷贝构造函数:

1)如果用户没有自定义拷贝构造函数,并且在代码中使用到了拷贝构造函数,编译器就会生成默认的拷贝构造函数。但如果用户定义了拷贝构造函数,编译器就不在生成。

2)如果用户定义了一个构造函数,但不是拷贝构造函数,而此时代码中又用到了拷贝构造函数,那编译器也会生成默认的拷贝构造函数。

因为系统提供的默认拷贝构造函数工作方式是内存拷贝,也就是浅拷贝。如果对象中用到了需要手动释放的对象,则会出现问题,这时就要手动重载拷贝构造函数,实现深拷贝。

下面说说深拷贝与浅拷贝:

浅拷贝:如果复制的对象中引用了一个外部内容(例如分配在堆上的数据),那么在复制这个对象的时候,让新旧两个对象指向同一个外部内容,就是浅拷贝。(指针虽然复制了,但所指向的空间内容并没有复制,而是由两个对象共用)

深拷贝:如果在复制这个对象的时候为新对象制作了外部对象的独立复制,就是深拷贝。

以上描述不详细,具体参见:http://www.cnblogs.com/liushui-sky/p/7728839.html

拷贝构造函数重载声明如下:

A (const A&other)

下面为拷贝构造函数的实现:

[cpp] view plain copy

class A  
{  
  int m_i  
  A(const A& other):m_i(other.m_i)  
{  
  Cout<<”拷贝构造函数”<<endl;  
}  
}</span>  

3.赋值函数
当一个类的对象向该类的另一个对象赋值时,就会用到该类的赋值函数。

当没有重载赋值函数(赋值运算符)时,通过默认赋值函数来进行赋值操作

A a;

A b;

b=a;

强调:这里a,b对象是已经存在的,是用a 对象来赋值给b的!!

赋值运算的重载声明如下:

A& operator = (const A& other)

通常大家会对拷贝构造函数和赋值函数混淆,这儿仔细比较两者的区别:

1)拷贝构造函数是一个对象初始化一块内存区域,这块内存就是新对象的内存区,而赋值函数是对于一个已经被初始化的对象来进行赋值操作。

[cpp] view plain copy

class  A;  
A a;  
A b=a;   //调用拷贝构造函数(b不存在)  
A c(a) ;   //调用拷贝构造函数  
  
/****/  
  
class  A;  
A a;  
A b;     
b = a ;   //调用赋值函数(b存在)</span>  
 

2)一般来说在数据成员包含指针对象的时候,需要考虑两种不同的处理需求:一种是复制指针对象,另一种是引用指针对象。拷贝构造函数大多数情况下是复制,而赋值函数是引用对象

3)实现不一样。拷贝构造函数首先是一个构造函数,它调用时候是通过参数的对象初始化产生一个对象。赋值函数则是把一个新的对象赋值给一个原有的对象,所以如果原来的对象中有内存分配要先把内存释放掉,而且还要检察一下两个对象是不是同一个对象,如果是,不做任何操作,直接返回。(这些要点会在下面的String实现代码中体现)

!!!如果不想写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,最简单的办法是将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。如:

[cpp] view plain copy

class A  
{  
 private:  
 A(const A& a); //私有拷贝构造函数  
 A& operate=(const A& a); //私有赋值函数  
}</span>  

如果程序这样写就会出错:

[cpp] view plain copy

A a;  
A b(a); //调用了私有拷贝构造函数,编译出错  
  
A b;  
b=a; //调用了私有赋值函数,编译出错</span>  
 

所以如果类定义中有指针或引用变量或对象,为了避免潜在错误,最好重载拷贝构造函数和赋值函数。

特别注意:

Question 1:类中的赋值函数中的参数为什么加const?

Answer:参数使用cosnt的原因有两个:

1.防止类中的赋值函数对用来赋值的“原对象”进行修改
2.若赋值函数的形参加上const,则赋值函数接受的实参对象既可以是const对象,也可以是非const对象;否则赋值函数能够接受的对象只能是非const对象,而不能是const对象。

Question 2:类中的赋值函数中的参数为什么使用引用?

Answer:避免调用类中的拷贝构造函数在内存中开辟空间来创建形参对象,而是让形参成为实参的别名,从而节省时间和空间,提供编程效率

Question 3:类中的赋值函数的返回值类型为什么是Student&,不可以是Student或void吗?

Answer:在C++中,系统支持变量之间的连续赋值,如:

1 int a=10;
2 int b,c;
3 b=c=a;//变量之间的连续赋值,b、c的值均为10

其本质是先将变量a的值赋值给变量c,然后将赋值后的变量c返回到赋值运算符=的右边,再将其赋值给变量b,即:

1 int a=10;
2 b=(c=a); //即c=a,b=c;

同理,如果类中赋值函数的返回值类型为void,即类中的赋值函数没有返回值,此时编译器将不支持对该类中的对象进行连续赋值

而赋值函数的返回值类型之所以是Student&而非Student是为了避免函数在返回对象时调用类中的拷贝构造函数在内存中创建临时对象,从而提高程序的运行效率

Question 4:语句“if(this!=&stu)”的作用是什么?

Answer:通过比较赋值者和被赋值者的地址是否相同来判断两者是否是同一个对象,从而避免自赋值(即自己给自己赋值)的情况的发生,原因如下:

• 为了提高程序的运行效率。自己给自己赋值是完全无意义的行为,只会浪费程序的时间资源。

• 当类中的数据成员中含有指针时,自赋值操作可能会带来灾难性的后果。例如假设对象a和b中都含有一个指针ptr,它们分别指向一块通过new动态开辟的内存空间A和内存空间B,当将对象a赋值给对象b时,要求先将对象b中指针ptr指向的内存空间B通过delete释放掉(否则将造成内存泄漏),然后在内存中重新开辟内存空间C以拷贝内存空间A中的内容,并让对象b的指针ptr重新指向内存空间C,从而完成赋值。如果此时允许对象的自赋值,那么对象会在自赋值前先释放自己指针所指的内存空间,然后重新在内存中开辟空间,然而由于之前的内存空间已经释放,此时新内存空间希望拷贝的内容将不复存在,因而造成灾难性后果。

因此,对于类中的赋值函数,一定要先检查是否是自赋值,如果是,直接return *this。
对于类中的赋值函数,一定要先检查是否是自赋值,如果是,直接return *this。
Question 5:自定义类的对象之间的赋值操作的本质是什么?

Answer:本质是调用类中的成员函数(operator=()函数)。

总结

构造函数(传入指针):
1.检查是否为空
2.赋给成员变量值

析构函数():
释放成员变量

拷贝构造(传入引用):
赋给成员变量值

赋值函数(传入引用):
1.检查自赋值
2.释放之前的成员变量值
3.赋给成员变量值
4.返回this指针

C++中拷贝赋值函数的形参能否进行值传递?

不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数。。如此循环,无法完成拷贝,栈也会满。

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

C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员

这三个关键字主要用来控制类或者结构体成员变量和成员函数的访问权限,称为成员访问限定符,分别代表公有的、受保护的、私有的,它们所表达的封装程度不同,在使用时需要特别注意。

访问权限限定符的使用又跟类本身的性质有关,即继承对访问权限的使用有一定的影响,本文主要探讨无继承情况下的访问。

public限定符
被public限定符所修饰的成员变量和函数可以被类的函数、子类的函数、友元函数,也可以由类的对象来访问,即可以使用成员运算符来访问。这里的友元函数,可以是该类的友元函数,也可以是该类的友元类的成员函数。使用形式如下:

class temp{
    public:
       int a; //修饰成员变量
       temp() //修饰成员函数,这里修饰构造函数
       {
         //初始化类对象成员
       }
}

protected限定符
**protected限定符修饰的成员变量和成员函数可以被该类的成员函数访问,但是不能被类对象所访问,即不能通过类对象的成员运算符来访问。**另外,**这些成员可以被子类的函数和友元函数访问,相比public成员 少了一个可以使用类对象直接访问的特性。**具体使用与public类似,这里不再贴出代码。

private限定符
被private限定符修饰的成员变量只能被该类的方法和友元函数访问,子类函数无法访问,在这三个限定符中封装程度是最高的,一般来说,应该尽可能将类的成员变量声明为private而不是其他,减少成员变量的暴露,只提供getter和settter方法给外界访问,这样能提高类的安全性。具体使用与public类似。

C++中struct和class的区别

在C++中,可以用struct和class定义类,都可以继承。区别在于:struct的默认继承权限和默认访问权限是public,而class的默认继承权限和默认访问权限是private。
另外,class还可以定义模板类形参,比如template <class T, int i>。

C++中析构函数的作用

析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
析构函数名也应与类名相同,只是在函数名前面加一个位取反符~,例如~stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载

如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。

如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。

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

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

将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。

如果基类指针向派生类对象,则删除此指针时,我们希望调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。

若使用基类指针操作派生类,需要防止在析构时,只析构基类,而不析构派生类。

但是,如果析构函数不被声明成虚函数,则编译器采用的绑定方式是静态绑定,在删除基类指针时,只会调用基类析构函数,而不调用派生类析构函数,这样就会导致基类指针指向的派生类对象析构不完全。若是将析构函数声明为虚函数,则可以解决此问题。

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

虚函数和多态

多态的实现主要分为静态多态和动态多态静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。
虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。

重载和重写

重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中
重写:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写

static关键字作用:

(1)函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
(2)在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
(3)在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
(4)在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
(5)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

1. 全局静态变量

在全局变量前加上关键字static,全局变量就定义成一个全局静态变量.

静态存储区,在整个程序运行期间一直存在。

初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);

作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾

2. 局部静态变量

在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。

内存中的位置:静态存储区

初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);

作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;

3. 静态函数

在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。

函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;

warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;

4. 类的静态成员

在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用

5. 类的静态函数

静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。

在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);

对于函数定义和代码块之外的变量声明,static修改标识符的链接属性,由默认的external变为internal,作用域和存储类型不改变,这些符号只能在声明它们的源文件中访问。
对于代码块内部的变量声明,static修改标识符的存储类型,由自动变量改为静态变量,作用域和链接属性不变。这种变量在程序执行之前就创建,在程序执行的整个周期都存在。

对于被static修饰的普通函数,其只能在定义它的源文件中使用,不能在其他源文件中被引用

对于被static修饰的**类成员变量和成员函数,它们是属于类的,**而不是某个对象,所有对象共享一个静态成员。静态成员通过<类名>::<静态成员>来使用。

1.加了static关键字的全局变量只能在本文件中使用。例如在a.c中定义了static int a=10;那么在b.c中用extern int a是拿不到a的值得,a的作用域只在a.c中。
2.static定义的静态局部变量分配在数据段上普通的局部变量分配在栈上,会因为函数栈帧的释放而被释放掉。
3. 对一个类中成员变量和成员函数来说,加了static关键字,则此变量/函数就没有了this指针了,必须通过类名才能访问

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

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

请你来说一下静态函数和虚函数的区别

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

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

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

const修饰成员函数的目的是什么?

const修饰的成员函数表明函数调用不会对对象做出任何更改,事实上,如果确认不会对对象做更改,就应该为函数加上const限定,这样无论const对象还是普通对象都可以调用该函数。

两个函数,一个带const,一个不带,会有问题吗?

不会,这相当于函数的重载。

const关键字作用:

(1)欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
(2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
(3)在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的 成员变量;
(5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。例如:

const classA operator*(const classA& a1,const classA& a2);  
operator*的返回结果必须是一个const对象。如果不是,这样的变态代码也不会编译出错:  
classA a, b, c;  
(a * b) = c; // 对a*b的结果赋值  

操作(a * b) = c显然不符合编程者的初衷,也没有任何意义。

隐式类型转换

首先,对于内置类型,低精度的变量给高精度变量赋值会发生隐式类型转换,其次,对于只存在单个参数的构造函数的对象构造来说,函数调用可以直接使用该参数传入,编译器会自动调用其构造函数生成临时对象。

c++中四种cast转换

参考回答:

C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast

1、const_cast

用于将const变量转为非const

用法:const_cast<type_id> (expression)
该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。
常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。

2、static_cast

用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
用法:static_cast < type-id > ( expression )
该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:
* 用于类层次结构中基类和子类之间指针或引用的转换。进行上行转换(把子类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成子类表示)时,由于没有动态类型检查,所以是不安全的。
* 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
* 把空指针转换成目标类型的空指针
* 把任何类型的表达式转换成void类型。

注意:static_cast不能转换掉expression的const、volitale、或者__unaligned属性。

3、dynamic_cast

用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。

向上转换:指的是子类向基类的转换

向下转换:指的是基类向子类的转换

它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。

用法:dynamic_cast < type-id > ( expression )
该运算符把expression转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void *;如果type-id是类指针类型,那么expression也必须是一个指针,如果type-id是一个引用,那么expression也必须是一个引用。
dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。另外,dynamic_cast还支持交叉转换(cross cast)。

4、reinterpret_cast

几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;

用法:reinpreter_cast (expression)
type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。
该运算符的用法比较多。

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

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

RTTI

运行时类型检查,在C++层面主要体现在dynamic_cast和typeid,VS中虚函数表的-1位置存放了指向type_info的指针。对于存在虚函数的类型,typeid和dynamic_cast都会去查询type_info

指针和引用的区别

1.指针有自己的一块空间,而引用只是一个别名;
2.使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;

3.指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;

4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;

5.可以有const指针,但是没有const引用;

6.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;

7.指针可以有多级指针(**p),而引用至于一级;

8.指针和引用使用++运算符的意义不一样;

9.如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

定义:
1、引用:

C++是C语言的继承,它可进行过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。引用就是C++对C语言的重要扩充。引用就是某一变量的一个别名,对引用的操作与对变量直接操作完全一样。引用的声明方法:类型标识符 &引用名=目标变量名;引用引入了对象的一个同义词。定义引用的表示方法与定义指针相似,只是用&代替了*。

2、指针:

指针利用地址,它的值直接指向存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。

c++中的smart pointer四个智能指针

(更多参考https://blog.csdn.net/k346k346/article/details/81478223)

智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。C++ 11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个智能指针引用。该引用计数的内存在堆上分配。当新增一个时引用计数加1,当过期时引用计数减一。只有引用计数为0时,智能指针才会自动释放引用的内存资源。对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。

C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。
为什么要使用智能指针:

智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

1. 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的缺点是:存在潜在的内存崩溃问题

2. unique_ptr(替换auto_ptr)

unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。

采用所有权模式,还是上面那个例子

unique_ptr<string> p3 (new string ("auto"));   //#4
unique_ptr<string> p4;                       //#5
p4 = p3;//此时会报错!!

编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。

另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

注:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:

unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;

3. shared_ptr

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。

成员函数:

use_count 返回引用计数的个数

unique 返回是否是独占所有权( use_count 为 1)

swap 交换两个 shared_ptr 对象(即交换所拥有的对象)

reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少

get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr sp(new int(1)); sp 与 sp.get()是等价的

4. weak_ptr

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。
当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
为了解决循环引用导致的内存泄漏,引入了weak_ptr弱指针,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。
weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout<<"A delete\n";
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout<<"B delete\n";
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout<<pb.use_count()<<endl;
cout<<pa.use_count()<<endl;
}
int main()
{
fun();
return 0;
}

可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_; 改为weak_ptr pb_; 运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。

注意的是我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); 英文pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr p = pa->pb_.lock(); p->print();

fork函数的作用

Fork:创建一个和当前进程映像一样的进程可以通过fork( )系统调用:

#include <sys/types.h>

#include <unistd.h>

pid_t fork(void);

成功调用fork( )会创建一个新的进程,它几乎与调用fork( )的进程一模一样,这两个进程都会继续运行。在子进程中,成功的fork( )调用会返回0。在父进程中fork( )返回子进程的pid。如果出现错误,fork( )返回一个负值。

最常见的fork( )用法是创建一个新的进程,然后使用exec( )载入二进制映像,替换当前进程的映像。这种情况下,派生(fork)了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。这种“派生加执行”的方式是很常见的。

在早期的Unix系统中,创建进程比较原始。当调用fork时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中。但从内核角度来说,逐页的复制方式是十分耗时的。现代的Unix系统采取了更多的优化,例如Linux,采用了写时复制的方法,而不是对父进程空间进程整体复制。

C++的fork函数用来“复制”一份主程序,即创建主进程的子进程。调用fork的同时,我的理解是,已经在内存中创建了“副本”进程,同时返回pid,所以在返回值之前,已经是主进程和子进程同时在运行了(如果fork成功的话),这样,在程序的运行过程中,一次fork返回了两次值,在父进程中,fork返回新创建子进程的进程ID,在子进程中,fork返回0,这时候就能够同时跑两个进程了。

fork,wait,exec函数

父进程产生子进程使用fork拷贝出来一个父进程的副本,此时只拷贝了父进程的页表,两个进程都读同一块内存,当有进程写的时候使用写实拷贝机制分配内存,exec函数可以加载一个elf文件去替换父进程,从此父进程和子进程就可以运行不同的程序了。fork从父进程返回子进程的pid,从子进程返回0.调用了wait的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回0,错误返回-1。exec执行成功则子进程从新的程序开始运行,无返回值,执行失败返回-1

map和set有什么区别,分别又是怎么实现的?

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

(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。

集合,所有元素都会根据元素的值自动被排序,且不允许重复。
底层实现:红黑树

set 底层是通过红黑树(RB-tree)来实现的,由于红黑树是一种平衡二叉搜索树,自动排序的效果很不错,所以标准的 STL 的 set 即以 RB-Tree 为底层机制。又由于 set 所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 set 操作行为,都只有转调用 RB-tree 的操作行为而已。

适用场景:有序不重复集合

2、map

映射。map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。

底层:红黑树

适用场景:有序键值对不重复映射

STL有什么基本组成

STL主要由:以下几部分组成:
容器迭代器仿函数算法分配器配接器
他们之间的关系:分配器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配接器用来套接适配仿函数

STL中map与Multimap

1、Map映射,map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。
底层实现:红黑树

适用场景:有序键值对不重复映射

2、Multimap

多重映射。multimap 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。允许键值重复。

底层实现:红黑树

适用场景:有序键值对可重复映射

vector和list的区别,应用,越详细越好

1、概念:
1)Vector

连续存储的容器,动态数组,在堆上分配空间

底层实现:数组

两倍容量增长:

vector 增加(插入)新元素时,如果未超过当时的容量,则还有剩余空间,那么直接添加到最后(插入指定位置),然后调整迭代器。

如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。

性能:

访问:O(1)

插入:在最后插入(空间够):很快

在最后插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。

在中间插入(空间够):内存拷贝

在中间插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。

删除:在最后删除:很快

在中间删除:内存拷贝

适用场景:经常随机访问,且不经常对非尾节点进行插入删除。

2、List

动态链表,在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。

底层:双向链表

性能:

访问:随机访问性能很差,只能快速访问头尾节点。

插入:很快,一般是常数开销

删除:很快,一般是常数开销

适用场景:经常插入删除大量数据

2、区别:

1)vector底层实现是数组;list是双向 链表。

2)vector支持随机访问,list不支持。

3)vector是顺序内存,list不是。

4)vector在中间节点进行插入删除会导致内存拷贝,list不会。

5)vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。

6)vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。

3、应用

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

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

STL中迭代器的作用,有指针为何还要迭代器

1、迭代器
Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。

由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL的list、vector、stack等容器类及ostream_iterator等扩展iterator。

2、迭代器和指针的区别

迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符,->、*、++、–等。迭代器封装了指针,是一个“可遍历STL( Standard Template Library)容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。

迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用*取值后的值而不能直接输出其自身。

3、迭代器产生原因

Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。

STL的allocaotr

STL的分配器用于封装STL容器在内存管理上的底层细节。在C++中,其内存配置和释放如下:
new运算分两个阶段:(1)调用::operator new配置内存;(2)调用对象构造函数构造对象内容

delete运算分两个阶段:(1)调用对象析构函数;(2)调用::operator delete释放内存

为了精密分工,STL allocator将两个阶段操作区分开来:内存配置有alloc::allocate()负责,内存释放由alloc::deallocate()负责;对象构造由::construct()负责,对象析构由::destroy()负责。

同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL采用了两级配置器,当分配的空间大小超过128B时,会使用第一级空间配置器;当分配的空间大小小于128B时,将使用第二级空间配置器。第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。

一、简述
C++的STL中定义了很多容器,容器的第二个模板参数通常为allocator类型。标准库中allocator类定义在头文件memory中,用于帮助将内存分配和对象的构造分离开来。它分配的内存是原始的、未构造的。和vector等一样,allocator也是一个模板类,为了定义一个allocator对象,我们需指明这个allocator可以分配的对象类型,这样allocator好根据给定的对象类型来确定合适的内存空间大小和对齐位置,例:

allocator<string> alloc;  定义了一个可以分配string的allocator对象
auto const p=alloc.allocate(n);  //分配n个未初始化的string内存,即为n个空string分配了内存,当然正如上面所说,分配的内存是原始的,未构造的。

——————————————————————————
二、allocator用法概述
常见操作总结如下:
allocator<T> a 定义了一个名为a的allocator对象,它可以为类型T的对象分配内存
a.allocator(n) 分配一段原始的、未构造的内存,这段内存能保存n个类型为T的对象
a.deallocate(p,n) 释放T指针p地址开始的内存,这块内存保存了n个类型为T的对象,p必须是一个先前由allocate返回的指针,且n必须是p创建时所要求的大小,且在调用该函数之前必须销毁在这片内存上创建的对象。要求还蛮多的哈,这是因为在创建过程中我们分配的是最原始的内存,所以在释放内存时也是只能严格释放这片最原始的内存。
a.construct(p,args) p必须是一个类型为T
的指针,指向一片原始内存,arg将被传递给类型为T的构造函数,用来在p指向的原始内存上构建对象。

a.destory(p) p为T*类型的指针,用于对p指向的对象执行析构函数

——————————————————————————

三、详情
1.allocate用于分配原始内存

正如标题所说,allocator出来的内存是最原始的,未构造的内存。相当于开辟新天地,我们将在这片新天地上盖高楼建大厦。它的construct成员函数接受一个指针和零个或多个额外的参数,在给定位置构造对象,额外的参数是用于初始化构造对象的。

auto q=p;  //q指向最后构造的元素之后的位置
alloc.construct(q++);   //*q为空字符串
alloc.construct(q++,10,'c');  //*q为cccccccccc
alloc.construct(q++,"hi");  //*q为hi

用完对象后,必须对这种构造的的对象调用destory销毁,它接受一个指针,对指向的对象执行析构函数。

while(q!=p)
    alloc.destory(--q);

循环开始处,q是指向最后构造的元素之后的一个位置,调用destory之前我们先对q进行递减操作,所以第一次调用destory销毁的是最后一个元素,依次执行销毁操作直到q和p相等。我们只能对真正构造了的元素进行destory操作。一旦元素被销毁,就可以重新使用这部分内存来保存其他string或归还给系统,释放内存通过调用deallocate完成。
alloc.deallocate(p,n);
其中p不能为空,必须指向allocate分配的内存,而且大小参数n也必须与调用allocated分配内存时提供的大小参数相等。
——————————————————————————
四、两个伴随算法
allocator还有两个伴随算法,用于在未初始化的内存块中创建对象,这些函数在给定目的位置创建元素,而不是由系统分配内存给他们,同样它们也位于头文件memory中。

uninitialized_copy(b,e,b2) 从迭代器b和e指出的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中,b2指向的内存必须足够大,能容纳输入序列中元素的拷贝。

uninitialized_copy_n(b,n,b2) 从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存中

uninitialized_fill(b,e,t) 在迭代器b和e指定的原始内存范围中创建对象,对象的值均为t的拷贝

uninitalized_fiil_n(b,n,t) 在迭代器b指向的内存地址开始创建n个对象,b必须指向足够大的未构造的原始内存,能够容乃给定数量的对象

以上函数将返回一个迭代器,指向最后一个构造的元素之后的位置。

——————————————————————————

五、实例
假定有一个int的vector,希望将它的内容拷贝到动态内存中,我们将分配一块比vector中元素所占空间大一倍的动态内存,然后将原vector中的元素拷贝到前一半空间,后一半用一个给定值进行填充。

//分配比vi向量所占空间大一倍的动态内存
auto p=alloc.allocate(vi.size()*2);
//通过拷贝vi中的元素来构造从p开始的元素
auto q=uninitialized_copy(vi.begin(),vi.end(),p);
//将剩余元素初始化为42
uninitialized_fill_n(q,vi.size(),42);

——————————————————————————

六、总结
为什么会有allocator?

原因是new在内存分配上面有一些局限性,new的机制是将内存分配和对象构造组合在一起,同样的,delete也是将对象析构和内存释放组合在一起。但当分配一块大块内存时,我们想要自己在这块内存上构建对象,就像建房子,我们弄到一块地,想自己开发更赚钱,或更满足自己的需求,这中情况下我们希望将内存分配和对象构造分离,这样就可实现,我们可以事先得到大块内存,然后真正需要时就在这块内存上创建对象。

STL里resize和reserve的区别

resize():改变当前容器内含有元素的数量(size()),eg: vectorv; v.resize(len);v的size变为len,如果原来v的size小于len,那么容器新增(len-size)个元素,元素的值为默认为0.当v.push_back(3);之后,则是3是放在了v的末尾,即下标为len,此时容器是size为len+1;
reserve():改变当前容器的最大容量(capacity),它不会生成元素,只是确定这个容器允许放入多少对象,如果reserve(len)的值大于当前的capacity(),那么会重新分配一块能存len个对象的空间,然后把之前v.size()个对象通过copy construtor复制过来,销毁之前的内存;
测试代码如下:

#include <iostream>
#include <vector>
using namespace std;
int main() {
    vector<int> a;
    a.reserve(100);
    a.resize(50);
    cout<<a.size()<<"  "<<a.capacity()<<endl;
        //50  100
    a.resize(150);
    cout<<a.size()<<"  "<<a.capacity()<<endl;
        //150  200
    a.reserve(50);
    cout<<a.size()<<"  "<<a.capacity()<<endl;
        //150  200
    a.resize(50);
    cout<<a.size()<<"  "<<a.capacity()<<endl;
        //50  200    
}

++i和i++的实现

  1. ++i 实现:
int&  int::operator++()
{
*this +=1return *this}
  1. i++ 实现:
const int  int::operatorint{
int oldValue = *this++*this);
return oldValue;
}

写个函数在main函数执行前先运行

__attribute((constructor))void before()
{
    printf("before main\n");
}

STL迭代器删除元素

这个主要考察的是迭代器失效的问题。1.对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器;2.对于关联容器map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。3.对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。

epoll原理

调用顺序:
int epoll_create(int size);

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

首先创建一个epoll对象,然后使用epoll_ctl对这个对象进行操作,把需要监控的描述添加进去,这些描述如将会以epoll_event结构体的形式组成一颗红黑树,接着阻塞在epoll_wait,进入大循环,当某个fd上有事件发生时,内核将会把其对应的结构体放入到一个链表中,返回有事件发生的链表。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值