基本语法


1. static关键字的作用

static有静态的意思,在代码的不同块中出现,表示的含义不同

  • 全局静态变量

在全局变量前加上关键字 static,全局变量就定义成一个全局静态变量。
  内存中的位置:静态存储区,在整个程序运行期间一直存在。
  初始化:未经初始化的全局静态变量会被自动初始化为 0(自动对象的值是任意的,除非他被显式初始化)
  作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。

  • 局部静态变量

在局部变量之前加上关键字 static,局部变量就成为一个局部静态变量。
  内存中的位置:静态存储区。
  初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化)
  作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变。

  • 静态函数

  在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
函数的实现使用 static 修饰,那么这个函数只可在本 cpp 内使用,不会同其他 cpp 中的同名函数引起冲突。
  warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰。

  • 类的静态成员

  在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用。
静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
  在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);

2. C++和C的区别

设计上
  C语言是面向过程的结构化编程语言,C++是C语言的超集,包含面向过程,面向对象,模板化编程
语法上
  C++具有面向对象封装继承多态三大特性
  C++相对于C增加了许多类型安全功能,比如强制类型转换等
  C++支持范式编程,比如模板类,模板函数等

3. C++中的四种cast转换

C++中的基本类型转换

4. C++中的指针和引用的区别

  1. 引用:C++是C语言的继承,它可进行过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。引用就是C++对C语言的重要扩充。引用就是某一变量的一个别名,对引用的操作与对变量直接操作完全一样。引用的声明方法:类型标识符 &引用名=目标变量名;引用引入了对象的一个同义词。定义引用的表示方法与定义指针相似,只是用&代替了*。
  1. 指针:指针利用地址,它的值直接指向存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。
  • 指针有自己的一块空间,而引用只是一个别名;
  • 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
  • 指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;
  • 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;
  • 可以有const指针,但是没有const引用;
  • 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
  • 指针可以有多级指针(**p),而引用至于一级;
  • 指针和引用使用++运算符的意义不一样;
  • 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

5. C++的智能指针 shard_ptr, unique_ptr, weak_ptr, auto_ptr

  智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。   C++里面的四个智能指针:auto_ptr,shared_ptr,weak_ptr,unique_ptr其中后三个是c++11支持,并且第一个已经被11弃用。
  智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

  1. auto_ptr (C++98方案, C++11已经抛弃)

  2. shard_ptr 共享的智能指针 (替换auto_ptr)

  3. unique_ptr 独占的智能指针

  4. weak_ptr 弱引用的智能指针,用来监视 shared_ptr

6. 数组和指针的区别

指针数组
保存数据的地址保存数据
间接访问数据,首先获得指针的内容,然后将其作为地址,从地址中提取数据直接访问数据
通常用于动态的数据结构通常用于固定数目且数据类型相同的元素
通过Malloc分配内存,free释放内存隐式的分配和删
通常指向匿名数据,操作匿名函数自身即为数据名

7. 什么是野指针,什么是空指针

野指针是指向一个已删除的对象或未申请访问受限内存区域的指针。 避免野指针

指针变量没有被初始化。 初始化声明为NULL 指针背free或者delete后设置为NULL 指针超变量作用域范围, 在作用域结束之前释放地址空间 并指向NULL

8. 智能指针有没有可能内存泄露,如果有,如何解决

  当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏
  为了解决循环引用导致的内存泄漏,引入了weak_ptr弱指针,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。

9. 什么是函数指针,指针函数

  • 函数指针本身是一个指向函数的指针变量
  • 指针函数本身是一个函数,函数的返回值是指针
  • 函数指针一般用来调用函数和做函数的参数,比如回调函数。

10. 什么是指针数组,数组指针

  • 指针数组
    • int *p[n] []优先级高 西安结合形成数组 在说明是一个指针数组,含有n个指针类型的数组元素
    • p++指向下一个数组元素
  • 数组指针(行指针)
    • int(*p)[n]()优先级高,首先说明p是一个指针,指向一个整形的以为数组,一维数组长度为n
    • p++跨过n个整形数据长度
    • 二维数组的行指针++, 指向下一行行指针位置

11. 简述指针常量和常量指针的区别

  • 指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。
  • 指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。

注意:无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。

12. 谈一下sizeof和strlen的区别

  • sizeof是一个操作符,strlen是库函数。
  • sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为'\0'的字符串作参数。
  • 编译器在编译时就计算出了sizeof的结果,而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数。
  • 据类型占内存的大小,而strlen计算的是字符串实际的长度。
  • 数组做sizeof的参数不退化,传递给strlen就退化为指针了

13. ++i 和 i++ 的区别

  ++i先自增1,再返回,i++先返回i,再自增1

14. 说一下extern “C”的含义

  extern"C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern"C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
  这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern"C"就是其中的一个策略。
  一般在C++中调用C语言代码或者在C++头文件中使用。

15. new/delete和maloc/free的区别

  • newdelete是操作符,可以重载,只能在C++ 中使用。
  • mallocfree是函数,可以覆盖,C、C++ 中都可以使用。
  • new 可以调用对象的构造函数,对应的delete调用相应的析构函数。
  • malloc 仅仅分配内存,free仅仅回收内存,并不执行构造和析构函数
  • newdelete返回的是某种数据类型指针,mallocfree 返回的是void 指针。

注意malloc申请的内存空间要用free释放,而new申请的内存空间要用delete释放,不要混用。

16. 变量的声明和定义有什么区别

  变量的定为变量分配地址和存储空间,变量的声明不分配地址。一个变量可以在多个地方声明,但是只在一个地方定义。加入extern修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。
说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间,如外部变量。

17. 简述#ifdef、#else、#endif和#ifndef的作用

  • 利用#ifdef、#endif将某程序功能模块包括进去,以向特定用户提供该功能。在不需要时用户可轻易将其屏 蔽。
  • 在调试代码前加上标记,以便于追踪和调试。
  • 应对硬件的限制。由于一些具体应用环境的硬件不一样,限于条件,本地缺乏这种设备,只能绕过硬件,直 接写出预期结果。

18. 写出int 、bool、 float 、指针变量与 “零值”比较的if 语句

C
//int与零值比较
if ( n == 0 )
if ( n != 0 )
//bool与零值比较
if (flag) // 表示flag为真
if (!flag) // 表示flag为假
//float与零值比较
const float EPSINON = 0.00001;
if ((x >= - EPSINON) && (x <= EPSINON) //其中EPSINON是允许的误差(即精度)。
//指针变量与零值比较
if (p == NULL)
if (p != NULL)

19. 结构体可以直接赋值吗

  声明时可以直接初始化,同一结构体的不同对象之间也可以直接赋值,但是当结构体中含有指针“成员”时一定要小心。
注意:当有多个指针指向同一段内存时,某个指针释放这段内存可能会导致其他指针的非法操作。因此在释放前一定要确保其他指针不再使用这段内存空间。

20. volatile有什么作用

  • 状态寄存器一类的并行设备硬件寄存器。
  • 一个中断服务子程序会访问到的非自动变量。
  • 多线程间被几个任务共享的变量。

21. 一个参数可以既是const又是volatile吗

  可以,用const和volatile同时修饰变量,表示这个变量在程序内部是只读的,不能改变的,只在程序外部条件变化下改变,并且编译器不会优化这个变量。每次使用这个变量时,都要小心地去内存读取这个变量的值,而不是去寄存器读取它的备份。
  注意:在此一定要注意const的意思,const只是不允许程序中的代码改变某一变 量,其在编译期发挥作用,它并没有实际地禁止某段内存的读写特性。

22. 结构体内存对齐问题

  结构体作为一种复合数据类型,其构成元素既可以是基本数据类型的变量,也可以是一些复合型类型数据。对此,编译器会自动进行成员变量的对齐以提高运算效率。默认情况下,按自然对齐条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同,向结构体成员中size最大的成员对齐。
  许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,而这个k则被称为该数据类型的对齐模数。

23. 全局变量和局部变量有什么区别?

  • 全局变量是整个程序都可访问的变量,谁都可以访问,生存期在整个程序从运行到结束(在程序结束时所占内存释放);
  • 而局部变量存在于模块(子程序,函数)中,只有所在模块可以访问,其他模块不可直接访问,模块结束(函数调用完毕),局部变量消失,所占据的内存释放。
  • 操作系统和编译器,可能是通过内存分配的位置来知道的,全局变量分配在全局数据段并且在程序开始运行的时候被加载.局部变量则分配在堆栈里面。

24. 简述C、C++程序编译的内存分配情况

一个C、C++程序编译时内存分为5大存储区:堆区、栈区、全局区、文字常量区、程序代码区。

  • 从静态存储区域分配

  内存在程序编译时就已经分配好,这块内存在程序的整个运行期间都存在。速度快、不容易出错,因为有系统会善后。例如全局变量,static变量,常量字符串等。

  • 在栈上分配

  在执行函数时,函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。大小为2M。

  • 从堆上分配

  即动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活。如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块。

25. typedef 和define 有什么区别

  • 用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义 常量,以及 书写复杂使用频繁的宏。
  • 执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其 发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。
  • 作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在define 声明后的引用 都是正确的。
  • 对指针的操作不同:typedefdefine 定义的指针时有很大的区别。

注意typedef定义是语句,因为句尾要加上分号。而define不是语句,千万不能在句尾加分号。

26. 简述strcpy、sprintf 与memcpy 的区别

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

27. 请解析(*(void (*)( ) )0)( )的含义

  • void(*)(): 是一个返回值为void,参数为空的函数指针
  • (void(*)())0: 把0强转成一个返回值为void,参数为空的函数指针
  • *(void (*)( ) )0: 上面基础上加*表示值函数指针的名字 比如calc()函数,calc就是函数名字
  • (*(void (*)( ) )0)( ): 执行上述函数 函数的调用

28. 指针常量和常量指针的区别

  指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。
注意:无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。

29. C语言的结构体和C++的有什么区别

  • C语言的结构体是不能有函数成员的,而C++的类可以有。
  • C语言的结构体中数据成员是没有private、public和protected访问限定的。而C++的类的成员有这些访问限定。
  • C语言的结构体是没有继承关系的,而C++的类却有丰富的继承关系。

注意:虽然C的结构体和C++的类有很大的相似度,但是类是实现面向对象的基础。而结构体只可以简单地理解为类的前身。

30. c++的拷贝构造形参为啥必须是const对象

  • 防止拷贝构造内部修改原始对象
  • 可以接受临时对象或者右值引用
  • 拷贝构造本身目的是为了创建一个副本,用const修饰符合语义
  • 如果是非const对象,那么无法传入const对象的参数

容器和算法


1. 实现一个函数,在main函数之前运行

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

2. 谈一下shared_ptr的实现

核心要理解引用计数,什么时候销毁底层指针,还有赋值,拷贝构造时候的引用计数的变化,析构的时候要判断底层指针的引用计数为0了才能真正释放底层指针的内存

3. 同时定义两个函数 一个带const 一个不带,会有问题么

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

4. map和set有什么区别 分别如何实现的

  map、set、multiset、multimap的底层实现都是红黑树,epoll模型的底层数据结构也是红黑树,linux系统中CFS进程调度算法,也用到红黑树。
红黑树的特性

  • 每个结点或是红色或是黑色;
  • 根结点是黑色;
  • 每个叶结点是黑的;
  • 如果一个结点是红的,则它的两个儿子均是黑色;
  • 每个结点到其子孙结点的所有路径上包含相同数目的黑色结点。

区别

  • map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。
  • set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以STL中将set的迭代器设置成const,不允许修改迭代器的值;而map的迭代器则不允许修改key值,允许修改value值。
  • map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符[ ]在map应用中需要慎用,const_map不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type类型没有默认值也不应该使用。如果find能解决需要,尽可能用find。

5. 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()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。

6. STL可以用迭代器删除元素么

这个主要考察的是迭代器失效的问题

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

7. STL中map的数据存放形式

  红黑树。unordered map底层结构是哈希表

8. STL的基本组成

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

9. STL中的map和unordered_map

map:

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

Multimap:

  1. 多重映射。multimap 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。允许键值重复。
  2. 底层实现:红黑树
  3. 适用场景:有序键值对可重复映射

10. vector和list的区别

Vector:

  1. 概念
    • 连续存储的容器,动态数组,在堆上分配空间
    • 底层实现:数组
    • 两倍容量增长:
    • vector 增加(插入)新元素时,如果未超过当时的容量,则还有剩余空间,那么直接添加到最后(插入指定位置),然后调整迭代器。
    • 如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。
  2. 性能:
    • 访问:O(1)
    • 插入:在最后插入(空间够):很快
    • 在最后插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
    • 在中间插入(空间够):内存拷贝
    • 在中间插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
    • 删除:在最后删除:很快
    • 在中间删除:内存拷贝
  3. 适用场景:
    • 经常随机访问,且不经常对非尾节点进行插入删除。

List:

  1. 概念

    • 动态链表,在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。
    • 底层:双向链表
  2. 性能:

    • 访问:随机访问性能很差,只能快速访问头尾节点。
    • 插入:很快,一般是常数开销
    • 删除:很快,一般是常数开销
  3. 适用场景:

    • 经常插入删除大量数据

区别

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

应用

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

11. STL迭代器的作用,有指针了为何还要用迭代器

  1. 迭代器

Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。
由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL的list、vector、stack等容器类及ostream_iterator等扩展iterator。

  1. 迭代器和指针的区别

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

  1. 迭代器产生原因

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

12. STL的resize和reserve的区别

  reserve是直接扩充到已经确定的大小,可以减少多次开辟、释放空间的问题(优化push_back),就可以提高效率,其次还可以减少多次要拷贝数据的问题。reserve只是保证vector中的空间大小(capacity)最少达到参数所指定的大小n。reserve()只有一个参数。   resize()可以改变有效空间的大小,也有改变默认值的功能。capacity的大小也会随着改变。resize()可以有多个参数。

13. STL的vector的底层原理

  vector底层是一个动态数组,包含三个迭代器,start和finish之间是已经被使用的空间范围,end_of_storage是整块连续空间包括备用空间的尾部。当空间不够装下数据(vec.push_back(val))时,会自动申请另一片更大的空间(1.5倍或者2倍),然后把原来的数据拷贝到新的内存空间,接着释放原来的那片空间。
  当释放或者删除(vec.clear())里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。因此,对vector的任何操作一旦引起了空间的重新配置,指向原vector的所有迭代器会都失效了。

14. 什么情况下用vector,什么情况下用list,什么情况下用deque

  vector可以随机存储元素(即可以通过公式直接计算出元素地址,而不需要挨个查找),但在非尾部插入删除数据时,效率很低,适合对象简单,对象数量变化不大,随机访问频繁。除非必要,我们尽可能选择使用vector而非deque,因为deque的迭代器比vector迭代器复杂很多。
  list不支持随机存储,适用于对象大,对象数量变化频繁,插入和删除频繁,比如写多读少的场景。需要从首尾两端进行插入或删除操作的时候需要选择deque。

15. priority_queue的底层原理

  priority_queue:优先队列,其底层是用堆来实现的。在优先队列中,队首元素一定是当前队列中优先级最高的那一个。

16. 为何map和set的插入删除效率比其他序列容器高

  因为不需要内存拷贝和内存移动

17. 为何map和set每次Insert之后,以前保存的iterator不会失效?

  因为插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。

18. 为何map和set不能像vector一样有个reserve函数来预分配数据?

  在map和set内部存储的已经不是元素本身了,而是包含元素的结点。也就是说map内部使用的Alloc并不是map<Key, Data, Compare, Alloc>声明的时候从参数中传入的Alloc。

19. hash_map与map的区别?什么时候用hash_map,什么时候用map?

  • 构造函数:hash_map需要hash function和等于函数,而map需要比较函数(大于或小于)。
  • 存储结构:hash_map以hashtable为底层,而map以RB-TREE为底层。
  • 总的说来,hash_map查找速度比map快,而且查找速度基本和数据量大小无关,属于常数级别。而map的查找速度是logn级别。但不一定常数就比log小,而且hash_map还有hash function耗时。
  • 如果考虑效率,特别当元素达到一定数量级时,用hash_map。
  • 考虑内存,或者元素数量较少时,用map。

20. 迭代器失效的问题

核心问题是插入操作

  • 对于vectorstring,如果容器内存被重新分配,iterators,pointers,references失效;如果没有重新分配,那么插入点之前的iterator有效,插入点之后的iterator失效;
  • 对于deque,如果插入点位于除frontback的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,deque的迭代器失效,但referencepointers有效;
  • 对于listforward_list,所有的iterator,pointerrefercnce有效。 删除操作:
  • 对于vectorstring,删除点之前的iterators,pointers,references有效;off-the-end迭代器总是失效的;
  • 对于deque,如果删除点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入
  • 元素到front和back时,off-the-end失效,其他的iterators,pointers,references有效;
  • 对于listforward_list,所有的iterator,pointerrefercnce有效。
  • 对于关联容器map来说,如果某一个元素已经被删除,那么其对应的迭代器就失效了,不应该再被使用,否则会导致程序无定义的行为。

21. STL线程不安全的情况

  • 在对同一个容器进行多线程的读写、写操作时;
  • 在每次调用容器的成员函数期间都要锁定该容器;
  • 在每个容器返回的迭代器(例如通过调用begin或end)的生存期之内都要锁定该容器;
  • 在每个在容器上调用的算法执行期间锁定该容器

类和数据抽象


1. C++可以定义引用数据成员么

  可以,必须通过成员函数初始化列表初始化

2. 面向对象的三大特性

  • 封装性:将客观事物抽象成类,每个类对自身的数据和方法实行protection (private,protected,public)。
  • 继承性:广义的继承有三种实现形式:实现继承(使用基类的属性和方法而无需额外编码的能力)、可 视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。
  • 多态性:是将父类对象设置成为和一个或更多它的子对象相等的技术。用子类对象给父类对象赋值 之后,父类对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。

3. C++空类有哪些成员函数

  • 缺省构造函数。
  • 缺省拷贝构造函数。
  • 缺省析构函数。
  • 缺省赋值运算符。
  • 缺省取址运算符。
  • 缺省取址运算符 const。

注意:有些书上只是简单的介绍了前四个函数。没有提及后面这两个函数。但后面这两个函数也是空类的默认函数。另外需要注意的是,只有当实际使用这些函数的时候,编译器才会去定义它们。

4. 谈谈拷贝构造函数和赋值运算符的认识

拷贝构造函数和赋值运算符重载有以下两个不同之处

  • 拷贝构造函数生成新的类对象,而赋值运算符不能。
  • 由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象 是否和新建对象 相同。而赋值运算符则需要这个操作,另外赋值运算中如果原来的对象中有内存分配要先把内存释放掉。

注意:当有类中有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不要使用默认的。

5. 说一下C++中析构函数的作用

  析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
析构函数名也应与类名相同,只是在函数名前面加一个位取反符~,例如~stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。
  如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。
  如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。
  类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数。

6. 静态函数和虚函数的区别

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

7. 什么是重载和覆盖

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

8. 说说你理解的虚函数和多态

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

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

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

10. 说说虚函数表如何实现运行时多态

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

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

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

12. C++中struct和class的区别

在C++中,class和struct做类型定义是只有两点区别:

  • 默认继承权限不同,class继承默认是private继承,而struct默认是public继承
  • class还可用于定义模板参数,像typename,但是关键字struct不能同于定义模板参数C++保留struct关键字,原因
  • 保证与C语言的向下兼容性,C++必须提供一个struct
  • C++中的struct定义必须百分百地保证与C语言中的struct的向下兼容性,把C++中的最基本的对象单元规定为class而不是struct,就是为了避免各种兼容要求的限制
  • 对struct定义的扩展使C语言的代码能够更容易的被移植到C++中

13. C++的虚函数指针的初始化过程

  • 当一个类本身定义了虚函数,或者基类有虚函数时,为了支持多态机制,编译器会为改类添加一个虚函数指针vptr。虚函数指针一般放在对象内存布局的第一个位置,对象地址就是虚函数指针的地址。
  • 虚函数指针的地址与对象的地址是一致的,虚函数指针指向的内容就是虚函数表的地址,再将虚函数表的地址转换为int类型,方便以后移动指向虚函数表中的slot。虚函数表中的slot储存的就是每个虚函数的地址,在此基础上增加地址就可以指向不同的slot,所以将对应的地址转化为函数指针的形式,就能通过赋值给函数指针变量来调用对应虚函数。
  • 编译器为每个带有虚函数的类的对象自动创建一个虚表指针即vptr,这个指针指向了对象所属类的虚表。在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向所属类的虚表,所以在调用虚函数的时候能找到正确的函数。
  • 虚指针在进入构造函数钱初始化完成
  • 对象初始化顺序
      当无虚基类的时候:按照继承时候声明的顺序进行基类构造函数,这个是为了将继承的部分进行初始化;按照成员对象在类中的声明顺序进行成员对象的初始化,这是因为在创建对象之前必须对成员进行初始化,这部分可以在构造函数初始化列表中进行;执行子类的构造函数体,对各部分进行初始化。
      当有虚基类的时候:第一步应当是对于所有虚基类的子对象,按照他们在类中定义出现的位置,从上到下,从左到右进行初始化,之后就按照以上步骤进行初始化即可。

14. 类的静态变量如何初始化

  类的静态成员在类内声明,类外初始化,静态成员变量在编译时候存放在静态据,定义过程编译时候完成,所以一定要在类外定义,一般同时初始化

15. 深拷贝和浅拷贝

  • 对于已知对象的赋值拷贝,编译系统会自动调用构造函数-拷贝构造,如果未定义拷贝构造函数,系统会自动生成一个拷贝构造函数对
  • 如果类包含指针变量,只会拷贝指针指向,不会拷贝具体的值,会出现内存泄露。
  • 深拷贝需要自己写拷贝构造函数,同时将原先对象的指针指向内容拷贝新申请的内存空间。
  • 函数的参数为对象时,实参传递给形参实际上就是拷贝对象,会调用拷贝构造
  • 函数返回值是一个对象时,函数会调用拷贝构造,用用返回
  • 浅拷贝带来的核心问题是析构函数多次释放堆内存, 可以使用std::shared_ptr解决

16. 析构函数是否可以为虚函数,为什么

  析构函数必须是虚函数,但是C++默认的析构函数不是虚函数。
  将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
  C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

17. 简述类成员函数的重写、重载和隐藏的区别

  1. 重写和重载主要有以下几点不同。
    • 范围的区别:被重写的和重写的函数在两个类中,而重载和被重载的函数在同一个类中。
    • 参数的区别:被重写函数和重写函数的参数列表一定相同,而被重载函数和重载函数的参数列表一定不同。
    • virtual的区别:重写的基类中被重写的函数必须要有virtual修饰,而重载函数和被重载函数可以被virtual修饰,也可以没有。
  2. 隐藏和重写、重载有以下几点不同。
    • 与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。
    • 参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。当参数不相同时,无论基类中的参数是否被virtual修饰,基类的函数都是被隐藏,而不是被重写。

注意:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同,达到的目的也是完全不同的,覆盖是动态态绑定的多态,而重载是静态绑定的多态。

泛型编程


编译和底层

1. 如何对STL进行内存优化

1> 二级配置器结构 STL内存管理使用二级内存配置器。

  • 第一级配置器
    • 第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重新配置等操作,并且能在内存需求不被满足的时候,调用一个指定的函数。一级空间配置器分配的是大于128字节的空间,如果分配不成功,调用句柄释放一部分内存,如果还不能分配成功,抛出异常
  • 第二级配置器
    • 在STL的第二级配置器中多了一些机制,避免太多小区块造成的内存碎片,小额区块带来的不仅是内存碎片,配置时还有额外的负担。区块越小,额外负担所占比例就越大。
  • 分配原则
    • 如果要分配的区块大于128bytes,则移交给第一级配置器处理。
    • 如果要分配的区块小于128bytes,则以内存池管理(memory pool),又称之次层配置(sub-allocation):每次配置一大块内存,并维护对应的16个空闲链表(free-list)。下次若有相同大小的内存需求,则直接从free-list中取。如果有小额区块被释放,则由配置器回收到free-list中。
    • 当用户申请的空间小于128字节时,将字节数扩展到8的倍数,然后在自由链表中查找对应大小的子链表
    • 如果在自由链表查找不到或者块数不够,则向内存池进行申请,一般一次申请20块
    • 如果内存池空间足够,则取出内存
    • 如果不够分配20块,则分配最多的块数给自由链表,并且更新每次申请的块数
    • 如果一块都无法提供,则把剩余的内存挂到自由链表,然后向系统heap申请空间,如果申请失败,则看看自由链表还有没有可用的块,如果也没有,则最后调用一级空间配置器

2> 二级内存池

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

  • 空间配置函数allocate

首先先要检查申请空间的大小,如果大于128字节就调用第一级配置器,小于128字节就检查对应的空闲链表,如果该空闲链表中有可用数据块,则直接拿来用(拿取空闲链表中的第一个可用数据块,然后把该空闲链表的地址设置为该数据块指向的下一个地址),如果没有可用数据块,则调用refill重新填充空间。

  • 空间释放函数deallocate

首先先要检查释放数据块的大小,如果大于128字节就调用第一级配置器,小于128字节则根据数据块的大小来判断回收后的空间会被插入到哪个空闲链表。

  • 重新填充空闲链表refill

在用allocate配置空间时,如果空闲链表中没有可用数据块,就会调用refill来重新填充空间,新的空间取自内存池。缺省取20个数据块,如果内存池空间不足,那么能取多少个节点就取多少个。
从内存池取空间给空闲链表用是chunk_alloc的工作,首先根据end_free-start_free来判断内存池中的剩余空间是否足以调出nobjs个大小为size的数据块出去,如果内存连一个数据块的空间都无法供应,需要用malloc取堆中申请内存。
假如山穷水尽,整个系统的堆空间都不够用了,malloc失败,那么chunk_alloc会从空闲链表中找是否有大的数据块,然后将该数据块的空间分给内存池(这个数据块会从链表中去除)。

3> 总结:

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

2. C++函数栈空间最大值是多少

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

3. 说说你了解的RTTI

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

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

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

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

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

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

5. include头文件的顺序和双引号和尖括号的区别

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

  1. 当前头文件目录
  2. 编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
  3. 系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
  4. 对于使用尖括号包含的头文件,查找头文件的路径顺序为:
  5. 编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
  6. 系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径

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

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

7. 说说C++的内存是如何管理的

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

代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段:存储程序中已初始化的全局变量和静态变量。
bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
映射区:存储动态链接库以及调用mmap函数进行的文件映射。
栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值。

8. 什么是memory leak,如何判断内存泄露,如何处理内存泄漏

memeory lead 内存泄漏(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,另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。

处理内存泄露 使用varglind,mtrace检测

9. 什么时候会发生段错误

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

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

10. 什么是reactor模型

Reactor多线程模型

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

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

12. 简述一下C++的内存分配情况

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

各个段说明如下: 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命令指定。

C++11


1. C++11有那些新特性

C++11 最常用的新特性如下: auto关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导

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

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

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

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

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

新增STL容器array以及tuple

2. 什么是右值引用,和左值有啥区别

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

  1. 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
  2. 能够更简洁明确地定义泛型函数 左值和优质的概念:
  • 左值:能取地址,或者具名对象,表达式结束后依然存在的持久对象;
  • 右值:不能取地址,匿名对象,表达式结束后就不再存在的临时对象;区别:
  • 左值能寻址,右值不能;
  • 左值能赋值,右值不能;
  • 左值可变,右值不能(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变);