C++面试问题总结
https://www.nowcoder.com/discuss/124724
new和malloc的区别
答:
- new是操作符,malloc是库函数
- new在调用时先分配内存,然后调用构造函数,释放时调用析构函数
- new自动分配内存,而malloc需要程序员指定大小
- new可以被重载,malloc不行
- new发生错误时抛出异常,malloc返回null
delete与 delete []区别
delete只会调用一次析构函数,而delete[]会调用每一个成员的析构函数。在More Effective C++中有更为详细的解释:“当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用operator delete来释放内存。”delete与new配套,delete []与new []配套
struct和class区别
答:
- struct一般用于描述数据结构集合,而class是对一个对象数据的封装
本质区别在于strict默认继承访问权限为public,而class默认继承访问权限为private
结构体struct在C++和C中的区别
答:
- C++的struct在声明时哭具备成员函数,静态函数,并且可以进行数据成员的初始化
- C++的struct可以从其他类或者结构体中继承
- C的strict默认访问权限全为public,不可以修改
static作用
对变量:
1.局部变量:
在局部变量之前加上关键字static,局部变量就被定义成为一个局部静态变量。
1)内存中的位置:静态存储区
2)初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)
3)作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域随之结束。
这边需要注意的是:当static用来修饰局部变量的时候,它就改变了局部变量的存储位置(从原来的栈中存放改为静态存储区)及其生命周期(局部静态变量在离开作用域之后,并没有被销毁,而是仍然驻留在内存当中,直到程序结束,只不过我们不能再对他进行访问),但未改变其作用域。
2.全局变量
在全局变量之前加上关键字static,全局变量就被定义成为一个全局静态变量。
1)内存中的位置:静态存储区(静态存储区在整个程序运行期间都存在)
2)初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)
3)作用域:全局静态变量在声明他的文件之外是不可见的。准确地讲从定义之处开始到文件结尾。
注:static修饰全局变量,并未改变其存储位置及生命周期,而是改变了其作用域,使当前文件外的源文件无法访问该变量,好处如下:(1)不会被其他文件所访问,修改(2)其他文件中可以使用相同名字的变量,不会发生冲突。对全局函数也是有隐藏作用。而普通全局变量只要定义了,任何地方都能使用,使用前需要声明所有的.c文件,只能定义一次普通全局变量,但是可以声明多次(外部链接)。注意:全局变量的作用域是全局范围,但是在某个文件中使用时,必须先声明。
对类中的:
1.成员变量
用static修饰类的数据成员实际使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象。因此,static成员必须在类外进行初始化(\初始化格式: int base::var=10;)*,而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化 。因为静态成员属于整个类,而不属于某个对象,如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的。*
特点:
1.不要试图在头文件中定义(初始化)静态数据成员。在大多数的情况下,这样做会引起重复定义这样的错误。即使加上#ifndef #define #endif或者#pragma once也不行。
2.静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以。
3.静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为 所属类类型的指针或引用。
2.成员函数
- 用static修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this指针。
- 静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。base::func(5,3);当static成员函数在类外定义时不需要加static修饰符。
- 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。因为静态成员函数不含this指针。
const关键字的作用
const的作用:
1.限定变量为不可修改。
2.限定成员函数不可以修改任何数据成员。
(1)欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
(2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
(3)在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的 成员变量;
(5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。
const与指针:
const char *p 表示 指向的内容不能改变。
char * const p,就是将P声明为常指针,它的地址不能改变,是固定的,但是它的内容可以改变。
const和define区别
#define 是在编译的预处理阶段起作用,而 const 是在 编译、运行的时候起作用。
#define 只是简单的字符串替换,没有类型检查。而 const 有对应的数据类型,是要进行判断的,可以避免一些低级的错误。
#define 只是进行展开,有多少地方使用,就替换多少次,它定义的宏常量在内存中有若干个备份; const 定义的只读变量在程序运行过程中只有一份备份。
const 常量可以进行调试的,define 是不能进行调试的,因为在预编译阶段就已经替换掉了。
编译器通常不为普通 const 常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高
为什么不可以同时用const和static修饰成员函数?
C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。
我们也可以这样理解:两者的语意是矛盾的。static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系。因此不能同时用它们。
说说什么是函数指针,如何定义函数指针,有什么使用场景
答:函数指针是:指向函数的指针变量。每个函数都有一个入口地址,该入口地址就是函数指针所指向的地址
使用场景:一般用于回调函数,在播放器中,一般用于获取当前播放时长和一些播放信息。
函数指针和指针函数的区别
函数指针定义:函数指针是指向函数的指针变量。因此“函数指针”本身首先应是指针变量,只不过该指针变量指向函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。
指针函数定义:指针函数的落脚点是一个函数,这个函数的返回值是一个指针,与普通函数类似,只是返回的数据类型不一样而已。
指针和引用的区别
本质上的区别是,指针是一个新的变量,只是这个变量存储的是另一个变量的地址,我们通过访问这个地址来修改变量。
而引用只是一个别名,还是变量本身。对引用进行的任何操作就是对变量本身进行操作,因此以达到修改变量的目的。
注:
(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:
int a=1;int *p=&a;
int a=1;int &b=a;
上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。
而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。
(2)可以有const指针,但是没有const引用(const引用可读不可改,与绑定对象是否为const无关)
注:引用可以指向常量,也可以指向变量。例如int &a=b,使引用a指向变量b。而为了让引用指向常量,必须使用常量引用,如const int &a=1; 它代表的是引用a指向一个const int型,这个int型的值不能被改变,而不是引用a的指向不能被改变,因为引用的指向本来就是不可变的,无需加const声明。即指针存在常量指针int const *p和指针常量int *const p,而引用只存在常量引用int const &a,不存在引用常量int& const a。
(3)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)
(4)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;
(5)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。
(6)”sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;
(7)指针和引用的自增(++)运算意义不一样;
(8)指针使用时需要解引用(*),引用则不需要;
野指针如何产生,如何避免
答:产生原因:一般是因为释放内存后没有把指针及时的置空
避免方法:
- 初始化的时候置空(void *p=NULL;)
- 申请内存的时候判空(assert(p);)
- 指针释放后置空(free(p);p=NULL;)
悬空指针与野指针区别
- 悬空指针:当所指向的对象被释放或者收回,但是没有让指针指向NULL;
- 野指针:那些未初始化的指针,或者说是指向位置地址的指针
Inline内联函数和宏函数的区别
inline:在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数。
主要区别在于:宏函数是在预编译时候把所有宏名用宏体代替,简单来说就是字符串的代替;而内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方将内联函数展开,这样子可以省去函数的调用开销,提高效率
decltype和volatile作用:
decltype:从表达式中推断出要定义变量的类型,但却不想用表达式的值去初始化变量。还有可能是函数的返回类型为某表达式的的值类型。
volatile:volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
堆和栈的区别
答:
1、堆栈空间分配区别
栈(操作系统):由操作系统(编译器)自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
2、堆栈缓存方式区别
栈使用的是一级缓存, 它们通常都是被调用时处于存储空间中,调用完毕立即释放。
堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
3、堆栈数据结构区别
堆(数据结构):堆可以被看成是一棵树,如:堆排序。
栈(数据结构):一种先进后出的数据结构。
内存泄漏产生的主要原因
答:内存泄漏一般是指堆内存的泄漏,也就是程序在运行过程中动态申请的内存空间不再使用后没有及时释放,导致那块内存不能被再次使用。更广义的内存泄漏还包括未对系统资源的及时释放,比如句柄、socket等没有使用相应的函数释放掉,导致系统资源的浪费。
一般出现内存泄露的主要原因是:
- new或malloc后没有delete或者free
- 子类继承父类时,父类的析构函数不是虚函数
- windows句柄资源没有释放掉
如果父类的析构函数不是虚函数,则不会触发动态绑定(多态),结果就是只会调用父类的析构函数,而不会调用子类的析构函数,从而可能导致子类的内存泄漏(如果子类析构函数中存在free delete 等释放内存操作时)。也就是说当我们想去借助父类指针去销毁子类对象的时候,不能去销毁子类对象时,如果没有虚析构函数,那么释放一个由基类指针指向的派生类对象时,不会触发动态绑定,则只会调用基类的析构函数,不会调用派生类的。派生类中申请的空间则得不到释放导致内存泄漏。这也就是为什么基类需要虚析构函数的原因
C++的内存分区
- 栈区(stack):主要存放函数参数以及局部变量,由系统自动分配释放。
- 堆区(heap):由用户通过 malloc/new 手动申请,手动释放。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
- 全局/静态区:存放全局变量、静态变量;程序结束后由系统释放。
- 字符串常量区:字符串常量就放在这里,程序结束后由系统释放。
- 代码区:存放程序的二进制代码。
简述什么是面向对象编程
答:面向对象是一种编程思维,把一切东西都看成是一个个对象。每个对象都拥有各自的属性,把这些对象的拥有的属性变量和操作这些属性变量的函数打包成一个个类来进行表示。
- 面向过程:根据业务逻辑从上到下写代码
- 面向对象:将数据和函数绑定在一起,进行封装,这样能够更快速的开发程序,减少重复代码的重写过程
简述面向对象三大特性
- 封装 将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互
- 继承
- 多态 用父类的指针指向子类的实例,然后通过父类的指针调用实际子类的成员函数,实现多态(有两种方式:重写和重载)
重写:派生类存在和父类相同的函数(函数名,参数列表,返回值类型都一样),只是派生类中的该函数的函数体与父类中该函数的函数体不同(花括号内的东西不一样)
重载:同一个访问区间中存在不同参数列的同名函数
什么是多态?多态有什么用途?
C++ 多态有两种:静态多态(早绑定)、动态多态(晚绑定)。静态多态是通过函数重载实现的;动态多态是通过虚函数实现的。
1.定义:“一个接口,多种方法”,程序在运行时才决定要调用的函数。
2.实现:C++多态性主要是通过虚函数实现的,虚函数允许子类重写override(注意和overload的区别,overload是重载,是允许同名函数的表现,这些函数参数列表/类型不同)。
注:多态与非多态的实质区别就是函数地址是静态绑定还是动态绑定。如果函数的调用在编译器编译期间就可以确定函数的调用地址,并产生代码,说明地址是静态绑定的;如果函数调用的地址是 需要在运行期间才确定,属于动态绑定。
3.目的:接口重用。封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。
4.用法:声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。
用一句话概括:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
关于重载、重写、隐藏的区别
Overload(重载):在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
Override(覆盖或重写):是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
注:重写基类虚函数的时候,会自动转换这个函数为virtual函数,不管有没有加virtual,因此重写的时候不加virtual也是可以的,不过为了易读性,还是加上比较好。
Overwrite(重写):隐藏,是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
构造函数类型以及作用
构造函数一共有三类:默认构造函数(无参构造函数)、一般构造函数、拷贝构造函数
默认构造函数(无参构造函数):如果创建一个类你没有写任何构造函数,则系统会自动生成默认的构造函数,或者写了一个不带任何形参的构造函数
一般构造函数:一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同(基于c++的重载函数原理)
拷贝构造函数:拷贝构造函数参数为类对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中。参数(对象的引用)是不可变的(const类型)。此函数经常用在函数调用时用户定义类型的值传递及返回。
深浅拷贝的区别
- 所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员(new对象),那么浅拷贝就会出问题了
- 在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间
参考链接:https://www.cnblogs.com/alantu2018/p/8459250.html
错误写法:
1 | // ConsoleApplication2.cpp : 定义控制台应用程序的入口点。 |
正确写法:
1 | #include "stdafx.h" |
什么是默认拷贝构造函数
很多时候在我们都不知道拷贝构造函数的情况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值
虚函数和纯虚函数区别
虚函数是允许被其子类重新定义的成员函数。
虚函数的声明:virtual returntype func(parameter);引入虚函数的目的是为了动态绑定;
纯虚函数声明:virtual returntype func(parameter)=0;引入纯虚函数是为了派生接口。(使派生类仅仅只是继承函数的接口)
定义一个函数为虚函数,不代表函数为不被实现的函数。
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才代表函数没有被实现。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加 =0:
例如:
1 | virtual void funtion()=0 |
引入原因
在很多情况下,基类本身生成对象是不合情理的。为了解决这个问题,方便使用类的多态性,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;)纯虚函数不能再在基类中实现,编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。称带有纯虚函数的类为抽象类。
特点:
- 当想在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;(避免类被实例化且在编译时候被发现,可以采用此方法),也就是说声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。
- 这个方法必须在派生类(derived class)中被实现;
目的:使派生类仅仅只是继承函数的接口。相当于在告诉子类的设计者,“你必须提供一个纯虚函数的实现,虽然我不知道你会怎样实现它”。
基类为什么需要虚析构函数?
防止内存泄漏。当我们想去借助父类指针去销毁子类对象的时候,不能去销毁子类对象时,如果没有虚析构函数,那么释放一个由基类指针指向的派生类对象时,不会触发动态绑定,则只会调用基类的析构函数,不会调用派生类的。派生类中申请的空间则得不到释放导致内存泄漏。
哪些函数不能成为虚函数?
不能被继承的函数和不能被重写的函数。
1)普通函数
普通函数不属于成员函数,是不能被继承的。普通函数只能被重载,不能被重写,因此声明为虚函数没有意义。因为编译器会在编译时绑定函数。
而多态体现在运行时绑定。通常通过基类指针指向子类对象实现多态。
2)友元函数
友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。
3)构造函数
首先说下什么是构造函数,构造函数是用来初始化对象的。假如子类可以继承基类构造函数,那么子类对象的构造将使用基类的构造函数,而基类构造函数并不知道子类的有什么成员,显然是不符合语义的。从另外一个角度来讲,多态是通过基类指针指向子类对象来实现多态的,在对象构造之前并没有对象产生,因此无法使用多态特性,这是矛盾的。因此构造函数不允许继承。
4)内联成员函数
我们需要知道内联函数就是为了在代码中直接展开,减少函数调用花费的代价。也就是说内联函数是在编译时展开的。而虚函数是为了实现多态,是在运行时绑定的。因此显然内联函数和多态的特性相违背。
5)静态成员函数
首先静态成员函数理论是可继承的。但是静态成员函数是编译时确定的,无法动态绑定,不支持多态,因此不能被重写,也就不能被声明为虚函数。、
什么是虚函数表
虚函数表:
多态是由虚函数实现的,而虚函数主要是通过虚函数表(V-Table)来实现的。
如果一个类中包含虚函数(virtual修饰的函数),那么这个类就会包含一张虚函数表,虚函数表存储的每一项是一个虚函数的地址。
这个类的每一个对象都会包含一个虚指针(虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能),这个虚指针指向虚函数表。
注:对象不包含虚函数表,只有虚指针,类才包含虚函数表,派生类会生成一个兼容基类的虚函数表。
- 原始基类的虚函数表
下图是原始基类的对象,可以看到虚指针在地址的最前面,指向基类的虚函数表(假设基类定义了3个虚函数)
- 单继承时的虚函数(无重写基类虚函数)
假设现在派生类继承基类,并且重新定义了3个虚函数,派生类会自己产生一个兼容基类虚函数表的属于自己的虚函数表。
Derive Class继承了Base Class中的3个虚函数,准确说是该函数的实体地址被拷贝到Derive Class的虚函数列表中,派生新增的虚函数置于虚函数列表后面,并按声明顺序摆放。
- 单继承时的虚函数(重写基类虚函数)
现在派生类重写基类的x函数,可以看到这个派生类构建自己的虚函数表的时候,修改了base::x()这一项,指向了自己的虚函数。
- 多重继承时的虚函数(class Derived :public Base1,public Base2)
这个派生类多重继承了两个基类base1,base2,因此它有两个虚函数表。
它的对象会有多个虚指针(据说和编译器相关),指向不同的虚函数表。
详细解释可以参考博客:https://www.cnblogs.com/jin521/p/5602190.html
什么是菱形继承,如何解决
菱形继承:假设我们有类B和类C,它们都继承了相同的类A。另外我们还有类D,类D通过多重继承机制继承了类B和类C。因为上述图表的形状类似于钻石(或者菱形),因此这个问题被形象地称为钻石问题(菱形继承问题)
解决方法:继承时候改成虚基类,D把A放到了对象组的最下面,这个A同时属于B和C,通过虚基表指针指向虚继表,虚基表中存储的偏移量(头部),通过偏移量可以找到公共的A空间。
1 | class A{ |
虚函数表存放的内容什么时候写入
在构造函数内部我们已经可以通过人为的方式,修改虚表指针的指向了,因此,我们有理由相信,在此之前虚表指针与虚表已经建立了联系。也就是说虚函数表是跟随类的,因此在编译阶段就应该初始化好这张表。而虚函数表指针是跟随对象产生的,所以应该要在编译阶段,由编译器在构造函数内部添加一段隐藏代码,隐藏代码的功能是把虚函数表的地址赋值给虚函数表指针。当对象在运行阶段被创建时,调用构造函数,在构造函数内部完成虚函数表指针的赋值操作,也就是把虚函数表写入对象的时机。
仿函数
仿函数(functor),就是使一个类的使用看上去像一个函数。其实现就是在一个类中实现一个operator(),这个类就具有了类似函数的行为,就是一个仿函数类了。
有些功能的的代码,会在不同的成员函数中用到,想复用这些代码有两种方法:
公共的函数:这是一个解决方法,不过函数用到的一些变量,就可能成为公共的全局变量,再说为了复用这么一片代码,就要单立出一个函数,也不是很好维护。
仿函数,写一个简单类,除了那些维护一个类的成员函数外,就只是实现一个operator(),在类实例化时,就将要用的,非参数的元素传入类中
示例代码:
1 | class MyAdd |
函数模板和类模版
函数模板
格式:
1 | template <typename type> return-type function-name(parameter list) |
在这里,type 是函数所使用的数据类型的占位符名称。这个名称可以在函数定义中使用。
下面是函数模板的实例,返回两个数中的最大值:
1 | #include <iostream> |
类模板
格式:
1 | template <class type> class class-name { |
在这里,type 是占位符类型名称,可以在类被实例化的时候进行指定。您可以使用一个逗号分隔的列表来定义多个泛型数据类型。
下面的实例定义了类 Stack<>,并实现了泛型方法来对元素进行入栈出栈操作:
1 | #include <iostream> |
19.类模板和模板类的区别
sizeof和strlen的区别?
功能不同:
sizeof是操作符,参数为任意类型,主要计算类型占用内存大小。
strlen()是函数,其函数原型为:extern unsigned int strlen(char s);其参数为char,strlen只能计算以”\0”结尾字符串的长度,计算结果不包括”\0”
32位,64位系统中,各种常用内置数据类型占用的字节数?
除*与long 不同其余均相同。
32位操作系统
char :1个字节(固定)
(即指针变量): 4个字节(32位机的寻址空间是4个字节。同理64位编译器)(变化)
short int : 2个字节(固定)
int: 4个字节(固定)
unsigned int : 4个字节(固定)
float: 4个字节(固定)
double: 8个字节(固定)
long: 4个字节
unsigned long: 4个字节(变化*,其实就是寻址控件的地址长度数值)
long long: 8个字节(固定)
64位操作系统
char :1个字节(固定)
*(即指针变量): 8个字节
short int : 2个字节(固定)
int: 4个字节(固定)
unsigned int : 4个字节(固定)
float: 4个字节(固定)
double: 8个字节(固定)
long: 8个字节
unsigned long: 8个字节(变化*其实就是寻址控件的地址长度数值)
long long: 8个字节(固定)
STL相关问题
STL基本组成部分
通常认为,STL是由容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成,其中后面 4 部分是为前 2 部分服务的,它们各自的含义如表 1 所示。
STL的组成 | 含义 |
---|---|
容器 | 一些封装数据结构的模板类,例如 vector 向量容器、list 列表容器等。 |
算法 | STL 提供了非常多(大约 100 个)的数据结构算法,它们都被设计成一个个的模板函数,这些算法在 std 命名空间中定义,其中大部分算法都包含在头文件 |
迭代器 | 在 C++ STL 中,对容器中数据的读和写,是通过迭代器完成的,扮演着容器和算法之间的胶合剂。 |
函数对象 | 如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数)。 |
适配器 | 可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器。 |
内存分配器 | 为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用。 |
STL常见的容器和实现原理
1.array :固定大小的顺序容器,它们保存了一个以严格的线性顺序排列的特定数量的元素
2.vector动态数组:
- 原理:连续的内存空间
- 性能:查找删除与数组性能一样,增加元素引发扩容时会有性能压力,一般为当前大小的两倍,然后把原数组的内容拷贝过去,接着释放原来的空间
size()表示数组中元素个数有多少,capacity()表示数组有多大容量
3.list 链表:
- 原理:双向链表
- 性能:常量性能的增删,不支持随机访问
4.deque:
- 原理:双端数组
- 性能:可以对头端进行插入删除操作
- deque内部工作原理:deque内部有一个中控器,维护每段缓冲区中的内容,缓冲区中存放真实数据。中控器维护的是每个缓冲区的地址,使得使用deque时像一片连续的内存空间。
4.map, multimap:
- 原理:以Key建立的红黑树
- 区别:multimap不存在at操作
红黑树虽然本质上是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)
5.set,multiset:
- 原理:红黑树
红黑规则:
- 节点不是黑色,就是红色(非黑即红)
- 根节点为黑色
- 叶节点为黑色(叶节点是指末梢的空节点
Nil
或Null
) - 一个节点为红色,则其两个子节点必须是黑色的(根到叶子的所有路径,不可能存在两个连续的红色节点)
- 每个节点到叶子节点的所有路径,都包含相同数目的黑色节点(相同的黑色高度)
6.unordered_map:
所有无序容器的底层实现都是Hash Map
- 原理:序容器存储键值对时,会先申请一整块连续的存储空间,但此空间并不用来直接存储键值对,而是存储各个链表的头指针,我们称其为桶,各键值对真正的存储位置是各个链表的节点。如图:
Pi表示存储的各个键值对
STL 标准库通常选用 vector 容器存储各个链表的头指针\
- 插入:将Key带入hash函数,会得到一个hash值(一个整数,用 H 表示);将 H 和无序容器拥有桶()的数量 n 做整除运算(即 H % n),该结果即表示应将此键值对存储到的桶的编号;建立一个新节点存储此键值对,同时将该节点链接到相应编号的桶上。 负载因子:容器存储的总键值对 / 桶数 负载因子越大,意味着容器越满,即各链表中挂载着越多的键值对,这无疑会降低容器查找目标键值对的效率;反之,负载因子越小,容器肯定越空,但并不一定各个链表中挂载的键值对就越少。默认情况下,无序容器的最大负载因子为 1.0。如果操作无序容器过程中,使得最大复杂因子超过了默认值,则容器会自动增加桶数,并重新进行哈希,以此来减小负载因子的值。需要注意的是,此过程会导致容器迭代器失效,但指向单个键值对的引用或者指针仍然有效。这就是为什么在操作无序容器过程中,键值对的存储顺序有时会“莫名”的发生变动。
- 查找:将Key利用hash函数计算得到hash值H;H%n得到桶号码在这个桶的链表中找到key 获取其value
迭代器作用,什么时候失效
STL容器分为序列式容器和关联式容器:
对于序列式容器(list除外),对某节点操作时,会使对应节点及其后续迭代器失效
vector
- push_back():在元素末尾添加元素,当还有空间时,只会使尾迭代器失效。当空间已满,会将原数组拷贝到扩容后的新数组,使原迭代器全部失效。
- pop_back():尾删除,会使尾迭代器失效。
- insert():当有剩余空间时,会使插入点及以后的迭代器失效,无剩余空间时,所有迭代器全部失效
- erase():删除点及其以后的迭代器全部失效
deque
- push_back():使尾迭代器失效
- push_front():使头迭代器失效
- pop_back():使尾迭代器失效
- pop_front():使头迭代器失效
- insert()和erase():判断插入或删除点前后的元素数量,如果前方元素数量较少,则将前方所有元素向前移动,使插入点及其以前的迭代器全部失效。如果后方元素数量较少,则将后方所有元素向后移动,使插入点及其以后的迭代器全部失效。
list
- 由于list是用指针连接各个节点的,所以对节点进行操作时,只会使当前的迭代器失效,不会影响其他迭代器。
关联式容器
- 同list,只会影响当前节点的迭代器,不会对其他迭代器产生影响。
Vector具体原理:
1.vector是动态数组,所以和数组一样拥有一段连续的内存空间,并且起始地址不变。
2.因为vector地址空间是连续的,所以能高效的进行随机访问,时间复杂度为o(1)。
3.在vector中插入和删除元素,需要对现有元素进行复制、移动,时间复杂度为o(n)。
4.如果vector中存储的对象很大,或者构造函数复杂,那么插入等开销会很大。因为拷贝现有对象时需要调用拷贝构造函数。
vector扩容原理说明
- 新增元素:Vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,在插入新增的元素;
- 对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了 ;
- 初始时刻vector的capacity为0,塞入第一个元素后capacity增加为1;
- 不同的编译器实现的扩容方式不一样,VS2015中以1.5倍扩容,GCC以2倍扩容。
- vector在push_back以成倍增长可以在均摊后达到O(1)的事件复杂度,相对于增长指定大小的O(n)时间复杂度更好。
- 为了防止申请内存的浪费,现在使用较多的有2倍与1.5倍的增长方式,而1.5倍的增长方式可以更好的实现对内存的重复利用,因为更好。
vector扩容为什么以2倍增长
1、时间和空间的权衡,简单来说, 空间分配的多,平摊时间复杂度低,但浪费空间也多。
2、均摊添加每个元素的开销最小。
vector扩容为什么以1.5倍增长
当 k =1.5 时,在几次扩展以后,可以重用之前的内存空间了。
理想分配方案是是在第N次分配的时候能重用之前N-1次释放的内存,如果按照1.5分配,1,1.5,3,4.5……当你需要分配4.5时,前面已分配5.5,你可以直接利用,把旧数据move过去。但选择两倍的增长比如像这样:1,2,4,8,16,32,… 每次需要申请的空间都大于用到前面释放的内存(4>2+1),无法重用。
vector常用接口:
• 清空vector可以使用成员函数c.clear()
• 判断vector是否为空,可以使用成员函数empty(),如果为空返回true,否则返回false
• vector输出最后一个元素的引用可以用back()成员函数,如果容器为空,则行为未定义
• vector输出第一个元素的引用可以用front()成员函数,如果容器为空,则行为未定义
• vector支持用下标访问元素,类似数组一样c[n]其中n是一个无符号整数,如果n大于容器的长度,那么行为未定义
• vector为了防止越界访问,其中有成员函数c.at(n),返回下标为n的元素的引用。如果下标越界,那么抛出out_of_range的异常
• pop_back()成员函数用来删除vector中的最后一个元素,如果容器为空会出现未定义行为。
• c.erase(it)成员函数,删除迭代器it所指向的元素,返回一个指向被删除元素之后的迭代器,如果it指向最后一个元素,那么返回以为尾后迭代器(通常是end())。若it就是end(),那么行为未定义。
• c.erase(beg,ed)删除[beg,ed)范围的元素,同时返回最后一个元素的后面的迭代器,如果ed就是尾后迭代器,那么还返回一个尾后迭代器。
vector中begin和end函数返回的是什么?
begin返回的是第一个元素的迭代器,end返回的是最后一个元素后面位置的迭代器。前闭后开区间【)
vector中的reserve和resize的区别
reserve是直接扩充到已经确定的大小,可以减少多次开辟、释放空间的问题(优化push_back),就可以 提高效率,其次还可以减少多次要拷贝数据的问题。reserve只是保证vector中的空间大小(capacity)最少 达到参数所指定的大小n。reserve()只有一个参数。
resize()可以改变有效空间的大小,也有改变默认值的功能。capacity的大小也会随着改变。resize()可以有 多个参数。
vector的reserve和capacity的区别?
reserve()用于让容器预留空间,避免再次分配内存;capacity()返回在重新进行分配以前所能容纳的元素数量。
vector中的size和capacity的区别
size表示当前vector中有多少个元素(finish - start);
capacity函数则表示它已经分配的内存中可以容纳多少元素(end_of_storage - start);
vector迭代器失效的情况
当插入一个元素到vector中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效。 当删除容器中一个元素后,该迭代器所指向的元素已经被删除,那么也造成迭代器失效。erase方法会返回下 一个有效的迭代器,所以当我们要删除某个元素时,需要it=vec.erase(it)。
正确释放vector的内存(clear(), swap(), shrink_to_fit())
vec.clear():清空内容,但是不释放内存。
vector().swap(vec):清空内容,且释放内存,想得到一个全新的vector。 vec.shrink_to_fit():请求容器降低其capacity和size匹配。 vec.clear();vec.shrink_to_fit();:清空内容,且释放内存。
vector中erase方法与算法(algorithn)中的remove方法区别
vector中erase方法真正删除了元素,迭代器不能访问了。
remove只是简单地将元素移到了容器的最后面,迭代器还是可以访问到。因为algorithm通过迭代器进行操作,不知道容器的内部结构,所以无法进行真正的删除。
vector与list区别
https://www.cnblogs.com/shijingjing07/p/5587719.html
vector与deque的区别:
vector对于头部的插入删除效率低,数据量越大效率越低。
deque相对而言,对头部的插入删除速度会比vector块。
vector访问元素时的速度会比deque快,这和两者的内部实现有关。
List原理
list是由双向链表实现的,因此内存空间是不连续的。
list的随机访问效率不好,需要遍历元素,时间复杂度为o(n)。
3.底层是双向链表,所以每个元素有两个指针的额外空间开销。
4.在任何位置都能高效地插入和删除元素。只要改变元素的指针值,不需要拷贝元素。
vector、list、queue选择原则:
- 需要对数据高效地随机访问(存取),而不在乎插入和删除的效率,采用vector
- 需要大量插入、删除数据,而不关心随机访问数据,采用list
- 需要随机访问数据,而且关心前后增删数据的能力,采用deque
- 对数据中间的增删操作比较多:采用list,建议在排序的基础上,批量进行增删可以对运行效率提供最大的保证
map的底层实现
容器的数据结构是采用红黑树进行管理,插入的元素健位不允许重复,所使用的节点元素的比较函数,只对元素的健值进行比较,元素的各项数据可通过健值检索出来。map容器是一种关联容器。
map和unordered_map的实现机理:
map:是基于红黑树来实现的(红黑树是非常严格的平衡二叉搜索树),红黑树具有自动排序功能,红黑树的每一个节点都代表着map中的一个元素,因此对于map的查找,删除和插入操作都是对红黑树的操作。
unordered_map:是基于哈希表来实现的,查找的时间复杂度是O(1),在海量数据处理中有着广泛的应用。
map和unordered_map的优缺点
map的优点:(1)map是有序的(2)基于红黑树实现,查找的时间复杂度是O(n)
map的缺点:空间占用率比较高,因为内部实现了红黑树,虽然提高了运行效率,但是每个节点都要保存父亲节点和孩子节点和红黑树的性质,使得每一个节点都占用大量的空间。
适用的情况:对于要有序的结构,适用map
unordered_map的优点:因为内部是哈希表来实现的,所以查找效率会非常高
unordered_map的缺点:哈希表的建立比较费时
适用的情况:对于查找问题,适用unordered_map会更好一点。
map插入元素方法
map<int, string> mapStudent;
1 mapStudent.insert(pair<int, string>(1, “student_one”));
2 mapStudent.insert(map<int, string>::value_type (1, “student_one”));
3 mapStudent[1] = “student_one”;
以上三种用法,虽然都可以实现数据的插入,但是它们是有区别的,当然了第一种和第二种在效果上是完成一样的,用insert函数插入数据,在数据的插入上涉及到集合的唯一性这个概念,即当map中有这个关键字时,insert操作是插入数据不了的,但是用数组方式就不同了,它可以覆盖以前该关键字对应的值
为什么map和set的插入删除效率比其他序列容器高
因为不需要内存拷贝和内存移动
当数据元素增多时(从10000到20000),map的set的查找速度会怎样变化?
红黑树采用二分查找法,时间复杂度为logn,所以从10000增到20000时,查找次数从log10000=14次到 log20000=15次,多了1次而已。
map 、set、multiset、multimap的特点
set和multiset会根据特定的排序准则自动将元素排序,set中元素不允许重复,multiset可以重复。 map和multimap将key和value组成的pair作为元素,根据key的排序准则自动将元素排序(因为红黑树也是 二叉搜索树,所以map默认是按key排序的),map中元素的key不允许重复,multimap可以重复。 map和set的增删改查速度为都是logn,是比较高效的。
为什么map和set每次insert之后, 以前保存的iterator不会失效?
存储的是结点,不需要内存拷贝和内存移动。 插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内 存的指针也不会变。 6
为何map和set不能像vector一样有个reserve函数来预分配数据?
在map和set内部存储的已经不是元素本身了,而是包含元素的结点。也就是说map内部使用的Alloc并不是 map声明的时候从参数中传入的Alloc。
set底层实现
底层是红黑树,set会根据待定的排序准则,自动将元素排序。不允许元素重复。
set, multiset (map,multimap)
set和multiset会根据特定的排序准则自动将元素排序,set中元素不允许重复,multiset可以重复。因为是排序的,所以set中的元素不能被修改,只能删除后再添加。
set的底层实现实现为什么不用哈希表而使用红黑树?
set中元素是经过排序的,红黑树也是有序的,哈希是无序的 如果只是单纯的查找元素的话,那么肯定要选哈希表了,因为哈希表在的最好查找时间复杂度为O(1),并且 如果用到set中那么查找时间复杂度的一直是O(1),因为set中是不允许有元素重复的。而红黑树的查找时 间复杂度为O(lgn)
hash表
hash表的实现,包括STL中的哈希桶长度常数。
hash表的实现主要涉及两个问题:散列函数和碰撞处理。
1)hash function (散列函数)。最常见的散列函数:f(x) = x % TableSize .
2)碰撞问题(不同元素的散列值相同)。解决碰撞问题的方法有许多种,包括线性探测、二次探测、开链等做法。SGL版本使用开链法,使用一个链表保持相同散列值的元素。
你怎样理解迭代器?
Iterator(迭代器)用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示,相当于智能指针。
迭代器失效问题
vector 迭代器
当插入一个元素后,插入点之前的迭代器如果未扩容则不受影响,插入点之后的迭代器失效;
当插入一个元素后,capacity 如果有变化,则容器需要重新分配内存,所有迭代器都会失效;
当进行删除操作后,指向删除点及之后元素的迭代器全部失效。
deque 迭代器
在容器 begin/end 插入操作所有迭代器不受影响;
在容器非 begin/end 的位置插入和删除操作都会使指向该容器元素的所有迭代器失效。
在容器 begin/end 删除元素会使指向被删除元素的迭代器失效;
List/forward_list 迭代器
list insert 操作不会使 list 迭代器失效;
list erase 操作会使当前指向被删除元素的迭代器失效,其它迭代器正常。
set 迭代器
set 的 insert 操作不会使 set 迭代器失效;
set erase操作会使当前指向被删除元素的迭代器失效,其它迭代器正常。
map 迭代器
map 的 insert 操作不会使 map 迭代器失效;
map erase 删除操作会使当前指向被删除元素的迭代器失效
vector为何每次insert之后,以前保存的iterator不会失效?
答:iterator这里就相当于指向节点的指针,内存没有变,指向内存的指针怎么会失效呢(当然被删除的那个元素本身已经失效了)。相对于vector来说,每一次删除和插入,指针都有可能失效,调用push_back在尾部插入也是如此。因为为了保证内部数据的连续存放,iterator指向的那块内存在删除和插入过程中可能已经被其他内存覆盖或者内存已经被释放了。即使时push_back的时候,容器内部空间可能不够,需要一块新的更大的内存,只有把以前的内存释放,申请新的更大的内存,复制已有的数据元素到新的内存,最后把需要插入的元素放到最后,那么以前的内存指针自然就不可用了。特别时在和find等算法在一起使用的时候,牢记这个原则:不要使用过期的iterator。
vector、list、map、deque用erase(it)后,迭代器的变化。
vector和deque是序列式容器,其内存分别是连续空间和分段连续空间,删除迭代器it后,其后面的迭代器都失效了,此时it及其后面的迭代器会自动加1,使it指向被删除元素的下一个元素。
list删除迭代器it时,其后面的迭代器都不会失效,将前面和后面连接起来即可。
map也是只能使当前删除的迭代器失效,其后面的迭代器依然有效。
不允许有遍历行为的容器有哪些(不提供迭代器)?
1)queue,除了头部外,没有其他方法存取deque的其他元素。
2)stack(底层以deque实现),除了最顶端外,没有任何其他方法可以存取stack的其他元素。
3)heap,所有元素都必须遵循特别的排序规则,不提供遍历功能。
stl中alloc
SGI 版本STL的默认配置器std::alloc。参见:《STL源码剖析》
1)考虑到小型区块所可能造成的内存碎片问题,SGI设计了双层配置器。第一级配置器直接使用malloc()和free();第二级则视情况采取不同的策略:当配置区块超过128bytes时,视为“足够大”,便调用第一级配置器;当配置区块小于128bytes时,视之为“过小”,为了降低额外负担,便采用memory pool(内存池)整理方式,而不在求助于第一级配置器。
2)内存池的核心:内存池和16个自由链表(各自管理8,16,…,128bytes的小额区块)。在分配一个小区块时,首先在所属自由链表中寻找,如果找到,直接抽出分配;若所属自由链表为空,则请求内存池为所属自由链表分配空间;默认情况下,为该自由链表分配20个区块,若内存池剩余容量不足,则分配可分配的最大容量;若内存池连一个区块都无法分配,则调用chunk_alloc为内存池分配一大块区块;若内存不足,则尝试调用malloc分配,否则返回bad_alloc异常。
STL线程不安全的情况
在对同一个容器进行多线程的读写、写操作时;
在每次调用容器的成员函数期间都要锁定该容器;
在每个容器返回的迭代器(例如通过调用begin或end)的生存期之内都要锁定该容器; 在每个在容器上调用的算法执行期间锁定该容器。
priority_queue的底层原理
priority_queue:优先队列,其底层是用堆来实现的。在优先队列中,队首元素一定是当前队列中优先级最高的那一个。
说几个C++11的新特性
- auto类型推导
- 空指针常量nullptr
- 范围for循环
- lambda函数
- override 和 final 关键字
- 线程支持、智能指针等
(1)auto
C++11中引入auto第一种作用是为了自动类型推导
auto的自动类型推导,用于从初始化表达式中推断出变量的数据类型。通过auto的自动类型推导,可以大大简化我们的编程工作
auto实际上实在编译时对变量进行了类型推导,所以不会对程序的运行效率造成不良影响
另外,似乎auto并不会影响编译速度,因为编译时本来也要右侧推导然后判断与左侧是否匹配。
使用auto实现任意两个数的加法
decltype实际上有点像auto的反函数,auto可以让你声明一个变量,而decltype则可以从一个变量或表达式中得到类型
1 | int x = 3; |
(2)nullptr空指针
nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,因为NULL实际上代表的是0,以往我们使用NULL
表示空指针。它实际上是个为0的int
值
(3)for容器遍历
在C++11 中for循环可以使用类似java的简化的for循环,可以用于遍历数组,容器,string以及由begin和end函数定义的序列(即有Iterator),示例代码如下:
1 | map<string, int> m{{"a", 1}, {"b", 2}, {"c", 3}}; |
(4)lambda表达式
使用lambda表达式统计字符串个数,以及大写字母个数
(5)override、final
override表示重写基类的虚函数
final表示禁止重写基类虚函数
(6)move构造函数
C++11中,std::move()函数位于头文件中,这个函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,通过右值引用使用该值,实现移动语义。 注意:被转化的左值,其声明周期并没有随着左右值的转化而改变,即std::move转化的左值变量lvalue不会被销毁。
左值:指向稳定的内存空间
右值:指向临时的内存空间
(7)线程库
C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件,该头文件声明了 std::thread
线程类。
线程的启动
C++线程库通过构造一个线程对象来启动一个线程,该线程对象中就包含了线程运行时的上下文环境,比如:线程函数、线程栈、线程起始状态等以及线程ID等,所有操作全部封装在一起,最后在底层统一传递给 _beginthreadex() 创建线程函数来实现 (注意 : _beginthreadex是windows中创建线程的底层c函数)。 std::thread()创建一个新的线程可以接受任意的可调用对象类型(带参数或者不带参数),包括lambda表达 式(带变量捕获或者不带),函数,函数对象,以及函数指针。
1 | // 使用lambda表达式作为线程函数创建线程 |
线程的结束
启动了一个线程后,当这个线程结束的时候,如何去回收线程所使用的资源呢?thread库给我们两种选择:
加入式:join()
join():会主动地等待线程的终止。在调用进程中join(),当新的线程终止时,join()会清理相关的资源,然后返回,调用线程再继续向下执行。由于join()清理了线程的相关资源,thread对象与已销毁的线程就没有关系了,因此一个线程的对象每次你只能使用一次join(),当你调用的join()之后joinable()就将返回false了。
分离式:detach()
detach:会从调用线程中分离出新的线程,之后不能再与新线程交互。就像是你和你女朋友分手,那之后你们就不会再有联系(交互)了,而她的之后消费的各种资源也就不需要你去买单了(清理资源)。此时调用joinable()必然是返回false。分离的线程会在后台运行,其所有权和控制权将会交给 c++运行库。同时,C++运行库保证,当线程退出时,其相关资源的能够正确的回收。
注意:必须在thread对象销毁之前做出选择,这是因为线程可能在你加入或分离线程之前,就已经结束了, 之后如果再去分离它,线程可能会在thread对象销毁之后继续运行下去。
原子性操作库(atomic)
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只 读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。C++98中传统的解决方式:可以对共享修改的数据可以加锁保护。虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。因此C++11中引入了原子操作。