Cpp11新标准
# Cpp11新标准
# 1、Cpp11有哪些新特性?
- nullptr替代 NULL
- 引⼊了 auto 和 decltype 这两个关键字实现了类型推导
- 基于范围的 for 循环for(auto& i : res){}
- 类和结构体的中初始化列表
- Lambda 表达式(匿名函数)
- std::forward_list(单向链表)
- 右值引用和move语义
- ...
# 2、auto、decltype和decltype(auto)的用法
# (1) auto (opens new window)
Cpp11新标准引⼊了auto类型说明符,⽤它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应某种 特定的类型说明符(例如 int)不同
auto 让编译器通过初始值来进⾏类型推演。从⽽获得定义变量的类型,所以说 auto 定义的变量必须有初始值。
举个例⼦:
//普通;类型
int a = 1, b = 3;
auto c = a + b;// c为int型
//const类型
const int i = 5;
auto j = i; // 变量i是顶层const, 会被忽略, 所以j的类型是int
auto k = &i; // 变量i是⼀个常量, 对常量取地址是⼀种底层const, 所以k的类型是const int*
const auto l = i; //如果希望推断出的类型是顶层const的, 那么就需要在auto前⾯加上cosnt
//引⽤和指针类型
int x = 2;
int& y = x;
auto z = y; //z是int型不是int& 型
auto& p1 = y; //p1是int&型
auto p2 = &x; //p2是指针类型int*
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 底层const顶层const到底是什么? (opens new window)
int* const p1 = &a; // p1是顶层const
const int* p2 = &a; // p2是底层const
2
区分很简单,看const修饰的是哪个。前者修饰的是p1,即指针本身,所以为顶层属性;后者修饰的*p2,即指针的指向地址的值,所以为底层属性。p1又称为常量指针,p2又称为指向常量的指针。
- 顶层const在赋值给其他变量时,可以忽略顶层属性;
- 底层const在赋值给其他变量时,不能忽略底层属性;
- int*类型可以转换为顶层和底层const,所以它可以给顶层和底层的const赋值;
- 底层const无法转换为顶层const。
# auto的3个推导规则:
**规则1:**声明为auto(不是auto&)的变量,忽视掉初始化表达式的顶层const。即对有const的普通类型(int 、double等)忽视const,对常量指针(顶层const)变为普通指针,对指向常量(底层const)的常量指针(顶层cosnt)变为指向常量的指针(底层const)。
**规则2:**声明为auto&的变量,保持初始化表达式的顶层const或volatile 属性。
**规则3:****若希望auto推导的是顶层const,加上const,即const auto。
# (2) decltype
有的时候我们还会遇到这种情况,我们希望从表达式中推断出要定义变量的类型,但却不想⽤表达式的值去初始化变量。还有可能是函数的返回类型为某表达式的值类型。在这些时候auto显得就无力了,所以Cpp11⼜引⼊了第⼆种类型说明符decltype,它的作⽤是选择并返回操作数的数据类型。在此过程中,编译器只是分析表达式并得到它的类型,却不进⾏实际的计算表达式的值。
int func() {return 0};
//普通类型
decltype(func()) sum = 5; // sum的类型是函数func()的返回值的类型int, 但是这时不会实际调⽤函数func()
int a = 0;
decltype(a) b = 4; // a的类型是int, 所以b的类型也是int
//不论是顶层const还是底层const, decltype都会保留
const int c = 3;
decltype(c) d = c; // d的类型和c是⼀样的, 都是顶层const
int e = 4;
const int* f = &e; // f是底层const
decltype(f) g = f; // g也是底层const
//引⽤与指针类型
//1. 如果表达式是引⽤类型, 那么decltype的类型也是引⽤
const int i = 3, &j = i;
decltype(j) k = 5; // k的类型是 const int&
//2. 如果表达式是引⽤类型, 但是想要得到这个引⽤所指向的类型, 需要修改表达式:
int i = 3, &r = i;
decltype(r + 0) t = 5; // 此时是int类型
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//3. 对指针的解引⽤操作返回的是引⽤类型
int i = 3, j = 6, *p = &i;
decltype(*p) c = j; // c是int&类型, c和j绑定在⼀起
2
3
在这段代码中,i
和j
都是int
类型的变量,分别被初始化为3和6。p
是一个指向i
的整型指针。decltype(*p)
表示*p
的类型,即int&
类型的引用。 因此,c
是一个int&
类型的引用,它被初始化为j
的值。由于c
是一个引用,它引用的是j
的地址,因此对c
的修改会影响到j
的值,反之亦然。
//4. 如果⼀个表达式的类型不是引⽤, 但是我们需要推断出引⽤, 那么可以加上⼀对括号, 就变成了引⽤类型了
int i = 3;
decltype((i)) j = i; // 此时j的类型是int&类型, j和i绑定在了⼀起
2
3
在这段代码中,i
是一个int
类型的变量,被初始化为3。(i)
是一个表达式,它是一个左值引用,引用的是i
的地址。因此,decltype((i))
表示(i)
的类型,即int&
类型的引用。 因此,j
是一个int&
类型的引用,它被初始化为i
的值。由于j
是一个引用,它引用的是i
的地址,因此对j
的修改会影响到i
的值,反之亦然。
# (3) decltype(auto)
decltype(auto)是Cpp14新增的类型指示符,可以⽤来声明变量以及指示函数返回类型。在使⽤时,会将“=”号左边的表达式替换成auto,再根据decltype的语法规则来确定类型。
举个例子:
int e = 4;
const int* f = &e; // f是底层const
decltype(auto) j = f;//j的类型是const int* 并且指向的是e
2
3
# 3、Cpp中NULL和nullptr区别
算是为了与C语言进行兼容而定义的⼀个问题吧
NULL来⾃C语⾔,⼀般由宏定义实现,⽽ nullptr 则是Cpp11的新增关键字。在C语言中,NULL被定义为(void)0, 而在Cpp语言中,NULL则被定义为整数0*。编译器⼀般对其实际定义如下:
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
2
3
4
5
在Cpp中指针必须有明确的类型定义。但是将NULL定义为0带来的另⼀个问题是无法与整数的0区分。因为Cpp中允许有函数重载,所以可以试想如下函数定义情况:
#include <iostream>
using namespace std;
void fun(char* p) {
cout << "char*" << endl;
}
void fun(int p) {
cout << "int" << endl;
}
int main()
{
fun(NULL);
return 0;
}
//输出结果:int
2
3
4
5
6
7
8
9
10
11
12
13
14
那么在传⼊NULL参数时,会把NULL当做整数0来看,如果我们想调用参数是指针的函数,该怎么办呢?。nullptr 在Cpp11被引入用于解决这⼀问题,nullptr可以明确区分整型和指针类型,能够根据环境自动转换成相应的指针 类型,但不会被转换为任何整型,所以不会造成参数传递错误。
nullptr的⼀种实现⽅式如下:
const class nullptr_t{
public:
template<class T> inline operator T*() const{ return 0; }
template<class C, class T> inline operator T C::*() const { return 0; }
private:
void operator&() const;
} nullptr = {};
2
3
4
5
6
7
这段代码定义了一个名为nullptr_t
的类,它是Cpp11标准中用于表示空指针常量的类型。nullptr_t
类中定义了两个模板类型转换运算符,用于将nullptr
转换为指针类型或类成员指针类型。
- 第一个模板类型转换运算符将
nullptr
转换为任意类型的指针类型,其返回值为0
,即空指针常量。 - 第二个模板类型转换运算符将
nullptr
转换为任意类的成员指针类型,其返回值也为0
。
nullptr_t
类还声明了一个私有的地址运算符operator&()
,它的作用是禁止将nullptr
取地址,从而保证nullptr
只能被直接赋值给指针类型或类成员指针类型,而不能被用于一些不安全的指针操作。 最后,代码中还定义了一个名为nullptr
的对象,它是一个默认初始化的nullptr_t
类型的空指针常量对象。
#include <iostream>
int main() {
int* p1 = nullptr; // 将nullptr赋值给指针类型
int MyClass::* p2 = nullptr; // 将nullptr赋值给类成员指针类型
// int* p3 = &nullptr; // 错误,nullptr不能被取地址
std::cout << p1 << std::endl; // 输出 0
std::cout << p2 << std::endl; // 输出 0
return 0;
}
2
3
4
5
6
7
8
9
以上通过模板类和运算符重载的方式来对不同类型的指针进行实例化从而解决了(void*)指针带来参数类型不明的问题,另外由于nullptr是明确的指针类型,所以不会与整形变量相混淆。但nullptr仍然存在⼀定问题,例如:
#include <iostream>
using namespace std;
void fun(char* p) {
cout<< "char* p" <<endl;
}
void fun(int* p) {
cout<< "int* p" <<endl;
}
void fun(int p) {
cout<< "int p" <<endl;
}
int main() {
fun((char*)nullptr);//语句1
fun(nullptr);//语句2
fun(NULL);//语句3
return 0;
}
//运⾏结果:
//语句1:char* p
//语句2:报错,有多个匹配
//语句3:int p
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在这种情况下存在对不同指针类型的函数重载,此时如果传⼊nullptr指针则仍然存在无法区分应实际调用哪个函数,这种情况下必须显示的指明参数类型。
# 4、智能指针的原理、常用的智能指针及实现
# 原理
智能指针是⼀个类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。动态分配的资源,交给⼀个类对象去管理,当类对象生命周期结束时,自动调用析构函数释放资源。
智能指针的实现一般通过一个类来完成,该类中包含一个指向动态分配对象的原始指针,以及一些管理规则,例如引用计数、作用域、线程安全等。
# 常用的智能指针
# (1) shared_ptr
实现原理:主要依赖于引用计数器的方法和控制块的概念,当创建一个
std::shared_ptr
对象时,会同时创建一个控制块(control block),该控制块中包含一个引用计数和指向所管理对象的原始指针。引用计数记录了有多少个std::shared_ptr
对象共享该控制块,初始值为1。当有新的std::shared_ptr
对象指向该控制块时,引用计数加1。当一个std::shared_ptr
对象被销毁时,引用计数减1。当引用计数为0时,即没有std::shared_ptr
对象指向该控制块时,该控制块所管理的对象会被销毁,同时控制块本身也会被销毁。
- 智能指针将⼀个计数器与类指向的对象相关联,引用计数器跟踪共有多少个类对象共享同⼀指针
- 每次创建类的新对象时,初始化指针并将引用计数置为1
- 当对象作为另⼀对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数
- 对⼀个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数
- 调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)
# (2) unique_ptr
实现原理:unique_ptr采用的是移动语义,一个非空的unique_ptr总是拥有它所指向的资源。转移一个unique_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空;所有unique_ptr不支持普通的拷贝和赋值操作,不能用在STL标准容器中;局部变量的返回值除外(因为编译器知道要返回的对象将要被销毁);如果你拷贝⼀个 unique_ptr,那么拷贝结束后,这两个unique_ptr都会指向相同的资源,造成在结束时对同⼀内存指针多次释放而导致程序崩溃。
# (3) weak_ptr
weak_ptr:弱引用。引用计数有⼀个问题就是互相引用形成环(环形引用),这样两个指针指向的内存都无法释放。需要使用weak_ptr通过将其中一个或多个std::shared_ptr
对象转换为std::weak_ptr
对象,来打破环形引用。weak_ptr是⼀个弱引用,它是为了配合shared_ptr而引入的⼀种智能指针,它指向⼀个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是说,它只引用,不计数。如果⼀块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存⼀定是有效的,在使用之前使用函数lock()检查weak_ptr是否为空指针。
需要注意的是,std::weak_ptr
对象不能直接访问所管理的对象,需要先将其转换为std::shared_ptr
对象才能访问所管理的对象。如果在转换过程中发现该对象已经被销毁,则转换后的std::shared_ptr
对象会变成null指针。
# (4) auto_ptr
主要是为了解决“有异常抛出时发生内存泄漏”的问题 。因为发生异常而无法正常释放内存。
auto_ptr有拷贝语义,拷贝后源对象变得无效,这可能引发很严重的问题;而unique_ptr则无拷贝语义,但提供了移动语义,这样的错误不再可能发生,因为很明显必须使用std::move()进行转移。
auto_ptr不支持拷贝和赋值操作,不能用在STL标准容器中。STL容器中的元素经常要支持拷贝、赋值操作,在这过程中auto_ptr会传递所有权,所以不能再STL中使用。
智能指针shared_ptr代码实现:
template<typename T>
class SharedPtr
{
public:
SharedPtr(T* ptr = NULL):_ptr(ptr), _pcount(new int(1)){}
SharedPtr(const SharedPtr& s):_ptr(s._ptr), _pcount(s._pcount) {
(*_pcount)++;
}
SharedPtr<T>& operator=(const SharedPtr& s)
{
if (this != &s)
{
if (--(*(this->_pcount)) == 0)
{
delete this->_ptr;
delete this->_pcount;
}
_ptr = s._ptr;
_pcount = s._pcount;
*(_pcount)++;
}
return *this;
}
T& operator*()
{
return *(this->_ptr);
}
T* operator->()
{
return this->_ptr;
}
~SharedPtr()
{
--(*(this->_pcount));
if (*(this->_pcount) == 0)
{
delete _ptr;
_ptr = NULL;
delete _pcount;
_pcount = NULL;
}
}
private:
T* _ptr;
int* _pcount;//指向引⽤计数的指针
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 5、说⼀说你了解的关于lambda函数的全部知识
利用lambda表达式可以编写内嵌的匿名函数,⽤以替换独立函数或者函数对象;
每当你定义⼀个lambda表达式后,编译器会自动生成⼀个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回⼀个匿名的闭包实例,其实是⼀个右值。所以,我们上面的lambda表达式的结果就是⼀个个闭包。闭包的⼀个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们⼜将其称为 lambda捕捉块。
- 值捕获:通过值捕获,将外部变量复制一份到Lambda函数内部,Lambda函数内部对该变量的修改不会影响到外部变量。
- 引用捕获:通过引用捕获,将外部变量的引用传递给Lambda函数,Lambda函数内部对该变量的修改会影响到外部变量。
lambda表达式的语法定义如下:
[capture](parameters)mutable -> return-type {statement}; //capture是捕获列表,用于指定Lambda函数所能访问的外部变量 // =:以值捕获方式捕获所有外部变量。 // &:以引用捕获方式捕获所有外部变量。 // 变量名:以值捕获方式捕获指定的外部变量。 // &变量名:以引用捕获方式捕获指定的外部变量。 //parameters是参数列表,用于指定Lambda函数的参数 //return-type是返回值类型,用于指定Lambda函数的返回值类型 //statement是函数体,用于实现Lambda函数的具体功能
1
2
3
4
5
6
7
8
9
10lambda必须使用尾置返回来指定返回类型,可以忽略参数列表和返回值,但必须永远包含捕获列表和函数体;
# 6、智能指针的作用
Cpp11中引入了智能指针的概念,⽅便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),⼆次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
智能指针在Cpp11版本之后提供,包含在头文件中,shared_ptr、unique_ptr、weak_ptr。 shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每⼀个shared_ptr的拷贝都指向相同的内存。每使用他⼀次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给⼀个智能指针,⼀个是类,⼀个是指针。例如
std::shared_ptr p4 = new int(1);
的写法是错误的。拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。
unique_ptr“唯⼀”拥有其所指对象,同⼀时刻只能有⼀个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset⽅法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
智能指针类将⼀个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同⼀指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另⼀对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对⼀个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。
weak_ptr 是⼀种不控制对象生命周期的智能指针, 它指向⼀个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的⼀个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的⼀种智能指针来协助 shared_ptr 工作, 它只可以从⼀个 shared_ptr 或另⼀个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少.
# 7、说说你了解的auto_ptr作用
- auto_ptr的出现,主要是为了解决“有异常抛出时发生内存泄漏”的问题;抛出异常,将导致指针p所指向的空间得不到释放而导致内存泄漏;
- auto_ptr构造时取得某个对象的控制权,在析构时释放该对象。我们实际上是创建⼀个auto_ptr类型的局部对象,该局部对象析构时,会将自身所拥有的指针空间释放,所以不会有内存泄漏;
- auto_ptr的构造函数是explicit,阻止了⼀般指针隐式转换为 auto_ptr的构造,所以不能直接将⼀般类型的指针赋值给auto_ptr类型的对象,必须用auto_ptr的构造函数创建对象;
- 由于auto_ptr对象析构时会删除它所拥有的指针,所以使用时避免多个auto_ptr对象管理同⼀个指针;
- Auto_ptr内部实现,析构函数中删除对象用的是delete而不是delete[],所以auto_ptr不能管理数组;
- auto_ptr支持所拥有的指针类型之间的隐式类型转换。
- 可以通过*和->运算符对auto_ptr所有用的指针进行提领操作;
- T* get(),获得auto_ptr所拥有的指针;T* release(),释放auto_ptr的所有权,并将所有用的指针返回。
# 8、智能指针的循环引用
循环引用是指使用多个智能指针share_ptr时,出现了指针之间相互指向,从而形成环的情况,有点类似于死锁的情况,这种情况下,智能指针往往不能正常调用对象的析构函数,从而造成内存泄漏。举个例⼦:
#include <iostream>
using namespace std;
template <typename T>
class Node
{
public:
Node(const T& value)
:_pPre(NULL)
, _pNext(NULL)
, _value(value)
{
cout << "Node()" << endl;
}
~Node()
{
cout << "~Node()" << endl;
cout << "this:" << this << endl;
}
shared_ptr<Node<T>> _pPre;
shared_ptr<Node<T>> _pNext;
T _value;
};
void Funtest()
{
shared_ptr<Node<int>> sp1(new Node<int>(1));
shared_ptr<Node<int>> sp2(new Node<int>(2));
cout << "sp1.use_count:" << sp1.use_count() << endl;
cout << "sp2.use_count:" << sp2.use_count() << endl;
sp1->_pNext = sp2; //sp2的引⽤+1
sp2->_pPre = sp1; //sp1的引⽤+1
cout << "sp1.use_count:" << sp1.use_count() << endl;
cout << "sp2.use_count:" << sp2.use_count() << endl;
}
int main()
{
Funtest();
system("pause");
return 0;
}
//输出结果
//Node()
//Node()
//sp1.use_count:1
//sp2.use_count:1
//sp1.use_count:2
//sp2.use_count:2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52