C++

C++类

基础内容

Posted by AIaimuti on November 13, 2019

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

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

C++中struct和class的区别

  • 在C++中,可以用struct和class定义类,都可以继承。
  • 区别在于:structural的默认继承权限和默认访问权限是public,而class的默认继承权限和默认访问权限是private。

什么是右值引用,跟左值又有什么区别

右值引用是C++11中引入的新特性 , 它实现了转移语义和精确传递。它的主要目的有两个方面:

  1. 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。

  2. 能够更简洁明确地定义泛型函数。

左值和右值的概念:

  • 左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。

  • 右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。

右值引用和左值引用的区别:

  1. 左值可以寻址,而右值不可以。

  2. 左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。

  3. 左值可变,右值不可变(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变)。

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

对于C++源文件,从文本到可执行文件一般需要四个过程:

  • 预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。

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

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

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

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指定的头文件路径

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

Malloc函数用于动态分配内存。

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

Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。

  • 当申请内存小于128K时,会使用系统函数brk在堆区中分配;
  • 而当申请内存大于128K时,会使用系统函数mmap在映射区分配。

C++的内存管理

在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。

静态区域:

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

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

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

动态区域:

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

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

  • stack(栈):使用栈空间存储函数的返回地址、参数、局部变量.

什么是memory leak,也就是内存泄漏

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

内存泄漏的分类:

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

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

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

如何判断内存泄漏

内存泄漏通常是由于调用了malloc/new等内存申请的操作,但是缺少了对应的free/delete。为了判断内存是否泄露,

  • 我们一方面可以使用linux环境下的内存泄漏检查工具Valgrind,
  • 另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。

段错误

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

  • 使用野指针
  • 试图修改字符串常量的内容

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。

共享内存相关api

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

  1. 新建共享内存shmget
  • int shmget(key_t key,size_t size,int shmflg);

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

  • size:共享内存大小

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

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

  1. 连接共享内存到当前进程的地址空间shmat
  • void *shmat(int shm_id,const void *shm_addr,int shmflg);

  • shm_id:共享内存标识符

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

  • shmflg:标志位

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

  1. 当前进程分离共享内存shmdt
  • int shmdt(const void *shmaddr);
  1. 控制共享内存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:共享内存管理结构体。

reactor模型组成

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接口,实现特定事件处理逻辑。

C++ STL 的内存优化

二级配置器结构

STL内存管理使用二级内存配置器。

  1. 第一级配置器

第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重新配置等操作,并且能在内存需求不被满足的时候,调用一个指定的函数。 一级空间配置器分配的是大于128字节的空间,如果分配不成功,调用句柄释放一部分内存,如果还不能分配成功,抛出异常。

2.第二级配置器

在STL的第二级配置器中多了一些机制,避免太多小区块造成的内存碎片,小额区块带来的不仅是内存碎片,配置时还有额外的负担。区块越小,额外负担所占比例就越大。

  1. 分配原则
    • 如果要分配的区块大于128bytes,则移交给第一级配置器处理。
    • 如果要分配的区块小于128bytes,则以内存池管理(memory pool),又称之次层配置(sub-allocation):每次配置一大块内存,并维护对应的16个空闲链表(free-list)。下次若有相同大小的内存需求,则直接从free-list中取。如果有小额区块被释放,则由配置器回收到free-list中。
    • 当用户申请的空间小于128字节时,将字节数扩展到8的倍数,然后在自由链表中查找对应大小的子链表
    • 如果在自由链表查找不到或者块数不够,则向内存池进行申请,一般一次申请20块
    • 如果内存池空间足够,则取出内存
    • 如果不够分配20块,则分配最多的块数给自由链表,并且更新每次申请的块数
    • 如果一块都无法提供,则把剩余的内存挂到自由链表,然后向系统heap申请空间,如果申请失败,则看看自由链表还有没有可用的块,如果也没有,则最后调用一级空间配置器

二级内存池

二级内存池采用了16个空闲链表,这里的16个空闲链表分别管理大小为8、16、24……120、128的数据块。这里空闲链表节点的设计十分巧妙,这里用了一个联合体既可以表示下一个空闲数据块(存在于空闲链表中)的地址,也可以表示已经被用户使用的数据块(不存在空闲链表中)的地址。

  1. 空间配置函数allocate 首先先要检查申请空间的大小,如果大于128字节就调用第一级配置器,小于128字节就检查对应的空闲链表,如果该空闲链表中有可用数据块,则直接拿来用(拿取空闲链表中的第一个可用数据块,然后把该空闲链表的地址设置为该数据块指向的下一个地址),如果没有可用数据块,则调用refill重新填充空间。
  2. 空间释放函数deallocate 首先先要检查释放数据块的大小,如果大于128字节就调用第一级配置器,小于128字节则根据数据块的大小来判断回收后的空间会被插入到哪个空闲链表。
  3. 重新填充空闲链表refill 在用allocate配置空间时,如果空闲链表中没有可用数据块,就会调用refill来重新填充空间,新的空间取自内存池。缺省取20个数据块,如果内存池空间不足,那么能取多少个节点就取多少个。 从内存池取空间给空闲链表用是chunk_alloc的工作,首先根据end_free-start_free来判断内存池中的剩余空间是否足以调出nobjs个大小为size的数据块出去,如果内存连一个数据块的空间都无法供应,需要用malloc取堆中申请内存。 假如山穷水尽,整个系统的堆空间都不够用了,malloc失败,那么chunk_alloc会从空闲链表中找是否有大的数据块,然后将该数据块的空间分给内存池(这个数据块会从链表中去除)。

总结:

  1. 使用allocate向内存池请求size大小的内存空间,如果需要请求的内存大小大于128bytes,直接使用malloc。
  2. 如果需要的内存大小小于128bytes,allocate根据size找到最适合的自由链表。 a. 如果链表不为空,返回第一个node,链表头改为第二个node。 b. 如果链表为空,使用blockAlloc请求分配node。 x. 如果内存池中有大于一个node的空间,分配竟可能多的node(但是最多20个),将一个node返回,其他的node添加到链表中。 y. 如果内存池只有一个node的空间,直接返回给用户。 z. 若果如果连一个node都没有,再次向操作系统请求分配内存。 ①分配成功,再次进行b过程。 ②分配失败,循环各个自由链表,寻找空间。 I. 找到空间,再次进行过程b。 II. 找不到空间,抛出异常。
  3. 用户调用deallocate释放内存空间,如果要求释放的内存空间大于128bytes,直接调用free。
  4. 否则按照其大小找到合适的自由链表,并将其插入。

C++如何处理内存泄漏

使用varglind,mtrace检测

C++11有哪些新特性

C++11 最常用的新特性如下:

auto关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导

nullptr关键字:nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。

智能指针:C++11新增了std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。

初始化列表:使用初始化列表来对类进行初始化

右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率

atomic原子操作用于多线程资源互斥操作

新增STL容器array以及tuple

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函数返回的临时变量的值。

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

移动语义:

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

Lambda表达式:

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

capturemutable->return-type{statement}

其中,

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

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

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

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

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

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

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