C++11/14 新特性(类型/初始化/智能指针)

数据类型

cpplogo

1.long long

long long 数据类型,提供至少 64 位的整型数据。

2.nullptr

C++11 推荐使用 nullptr 表示空指针,不再推荐使用 NULL 或 0 表示空指针。 NULL为从 C 语言中引进的,在 C 语言中一般被定义为宏:

1
2
3
4
5
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif

在 C 语言中,可以将 void* 隐式转换为任意指针类型,而在 C++ 中,可以为任意类型的指针赋 0 值。所以 c++ 将 NULL 定义为 0. nullptr 是指针类型(nullptr_t),该类型的对象不允许转换到非指针类型。

3.contexpr

1).const expression 常量表达式

是指值不会改变且在编译过程中就能得到计算结果的表达式。字面量属于常量表达式,使用常量表达式初始化的 const 对象也是常量表达式。一个对象或表达式是不是常量表达式要由它的数据类型和初始值共同决定:

1
2
3
4
const int max = 1024;    // max 是常量表达式
const int min = max - 1000; // min 是常量表达式
int nsize = 64; // nsize 不是常量表达式
const int sz = nsize + 1; // nsize 不是常量表达式

尽管 size 变量的初始化值是字面常量 ,但是它的定义数据类型是 int 而不是 const int 。 尽管 sz 定义为 const int ,但其值需要运行时才能得到,所以它也不是常量表达式。

2).constexpr 变量

C++11 中,允许将变量声明为 constexpr 类型,以便由编译器检查变量是否为一个常量表达式:声明为 constexpr 的变量一定是一个常量 ,且必须使用常量表达式初始化:

1
2
3
constexpr int max = 1024;    //是常量表达式
constexpr int min = max - 1000; //是常量表达式
constexpr int sz = get_size(); //只有当 get_size() 是常量表达式时,定义才是常量表达式

3).constexpr 函数

constexpr 可用于指示函数是一个常量表达式。这需要满足:

  • 返回值类型是字面值类型

  • 形参类型是字面值类型

  • 函数体中必须有且仅有一条 return 语句

4).constexpr 与 const

1
2
const int* p = nullptr;    //p 是一个指向整型常量的指针
constexpr int* q = nullptr; //q 是一个指向整型的常量指针

这里的 p 和 q 本质是不一样的。指针 p 指向的整数是常量,而指针 q 本身是常量,其指向的整数则可以不是常量:

1
2
3
4
*p = 5;         //错误 ,p 指向的整数是一个常量,即 *p 是不可修改的
p = new int(0); //正确 , p 是可以修改的
*q = 5; //正确 , q 指向的整数的值是可以修改的
q = new int(0); //错误 , q 是一个常量表达式,不可修改

其中的关键在于constexpr把它所定义的对象置为了顶层const.

4.noexcept

noexcept 指示一个函数不会抛出异常。如果一个使用 noexcept 修饰的函数内部抛出了异常,编译器并不会报错,但

是程序运行时并不会捕获该异常。这会导致程序终止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void f() noexcept
{
throw 1;
}

int main(void)
{
try
{
f();
}
catch (...)
{
cout << "catch Exception" << endl;
}
return 0;
}

5.auto

使用 auto 关键字声明的变量,编译器会推断其类型。这种用法类似于 C# 或 java 中的 var 关键字。由于是在编译期进行类型推断,所以 auto 并不会影响程序运行效率,而且编译器原本就会判断表达式左值和右值类型是否可以匹配,所以使用 auto 关键字也不会影响编译速度。

1
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
void testAuto()
{
auto a = 5; // a 将为 int 类型

//auto val1 = 1, val2 = 0.5; //ERROR , 声明前后类型不一致

const int val3 = 3;

auto val4 = val3;
val4 = 4; //OK val4 不是 const 类型

//auto& val5 = val3;
//val5 = 5; //ERROR val5 不可修改

//auto val6;
//auto* pVal7; //ERROR 必须初始化

//int b = (auto)0.5; //ERROR 不可用于类型转换

int arr[5];
auto phead = arr;
cout << typeid(phead).name() << endl; //OUT: int *

auto& phead2 = arr;
cout << typeid(phead2).name() << endl; //OUT: int [5]
}

使用 auto 有一些需要注意的地方:

  • 使用 auto 声明或定义多个变量时,变量前后的类型必须显式地一至。如上例 val1,val2

  • 将一个 const 修饰的变量赋值给 auto 变量时, const 属性不会被复制。如上例 val4. 除非变量被声明为引用类型,如上例 val5

  • 在堆中声明的 auto 变量必须初始化;

  • auto 不可以用于进行类型转换等类型不明确的操作

  • auto 会将数据退化成数组的指针。如上例 phead. 除非变量声明为引用类型,如 phead2

  • auto 不可以于声明模板参数

6.decltype

decltype 用于获取表达式的类型,而不会对表达式求值。当我们不清楚某个变量的具体类型,但又需要使用这个类型时,就可以使用 decltype ,或者使用某个变量的类型过于复杂时,也可以使用 decltype 简化代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void testDecltype()
{
int val = 5;
decltype(val) val1 = 6;
cout << typeid(val1).name() << endl; //OUT: int

auto fun = []() { cout << "fun called" << endl; return 5; };
decltype(fun()) val2 = 2; //"fun called"将不会被打印。因为decltype 不会调用fun().
cout << typeid(val2).name() << endl; //OUT:int .

int* pval3 = &val;
cout << typeid(pval3).name() << endl; //OUT: int *
decltype(*pval3) val4; //ERROR ,decltype(*pval3) 为 int& 类型

decltype(*pval3 + 0) val5; //OK val5 为 int 类型,因为 (int& + int) 为 int 类型

decltype((val)) val6; //ERROR , decltype((val)) 为 int& 类型
}

使用 decltype 时有一些需要注意的地方:

  • decltype 只获取表达式的类型,而不计算表达式

  • decltype 会获取变量的全部属性,包括 const

  • decltype((val)) 会获取 val& 的类型

初始化

  1. 允许使用花括号对变量进行初始化

  2. 允许在类的声明中直接对成员进行初始化

1
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
struct  Point
{
int x;
int y;
};

class CPoint
{
public:
CPoint() { cout << m_pt.x << "," << m_pt.y << endl; }
private:
//在类的声明中直接进行成员的初始化
int m_x = 0;
int m_y = 0;
Point m_pt = { m_x,m_y };
};

Point pt1{ 1,1 }; //结构体初始化
Point pt2 = { 1,1 };

int val1 = 1;
int val2 = int(2);
int val3 = { 3 };
int val4(4);
int val5{ 5 };

Point getPt()
{
return{ 1,1 }; //使用花括号对匿名对象进行初始化(自动对类型进行推导)
}

智能指针

C++11 的智能指针定义在 头文件中。

1.shared_ptr

shared_ptr 采用引用计数来管理所指向的对象,可以多个 shared_ptr 共享一个原始指针。当有一个新的 shared_ptr 指向同一个对象时,引用计数加 1 ;当shared_ptr 离开作用域时,引用计数减 1 ;当引用计数为 0 时,释放所管理的内存。 可以使用 make_shared 来构造 shared_ptr 智能指针。shared_ptr 也提供了其他的构造函数。比较常见的用法见下例:

1
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//测试类
class TestPtr
{
public:
TestPtr(string strName) :m_strName(new string(strName)) {
cout << "TestPtr " << strName <<" inited" << endl;
}

~TestPtr() {
cout << "TestPtr " << *m_strName << " dested" << endl; delete m_strName;
}
public:
string getName() {
return string(*m_strName);
}

private:
string * m_strName;
};

//删除器
void DeleterFun(TestPtr* p)
{
cout << p->getName() << " DeleterFun called " <<endl;
delete p;
p = nullptr;
}

//删除器
class DeleterFunctor
{
public:
void operator()(TestPtr* p)
{
cout << p->getName() << " DeleterFunctor called" << endl;
delete p;
p = nullptr;
}
protected:
private:
};

void testSharedPtr()
{
shared_ptr<TestPtr> spt = make_shared<TestPtr>("SPT");

//shared_ptr<TestPtr> sp0 = new TestPtr("T3"); //ERROR

shared_ptr<TestPtr> sp1(new TestPtr("T1"));

cout << "Before sp2, use_count of spt " << spt.use_count() << endl;
shared_ptr<TestPtr> sp2(spt);
cout << "After sp2, use_count of spt " << spt.use_count() << endl;

shared_ptr<TestPtr> sp3(new TestPtr("T3"), DeleterFun); //TEST Deleter fun
cout << "Before sp3 reset, use_count of sp3 " << sp3.use_count() << endl;
sp3.reset();
cout << "After sp3 reset, use_count of sp3 " << sp3.use_count() << endl;

shared_ptr<TestPtr> sp4(new TestPtr("T4"),
[](TestPtr* p) {cout << p->getName() << " Deleter lambda called" << endl; delete p; p = nullptr; }); //TEST Deleter fun
sp4.reset();

DeleterFunctor f;
shared_ptr<TestPtr> sp5(new TestPtr("T5"), f); //TEST Deleter Functor
sp5.reset();
}

在该例中,

  • spt 是使用 make_shared 来构造的;

  • sp1 使用原始指针来构造。sp2 使用拷贝构造,这时原智能指针的引用计数加 1. 注意 sp0 是错误的用法,不可以使用 “=” 将原始指针赋值给 shared_ptr。

  • sp3,sp4,sp5 在构造时附带了一个删除器。这个删除器可以是一个函数或仿函数,通过参数接收 shared_ptr 中的原始指针,使得其析构是用户可控的。其中 sp4 的删除器是一个 lambda 表达式。必须要注意的是,使用删除器时,原来指针应该是可以深拷贝的,否则会引发内存泄漏。

可以使用 reset 方法将一个智能指针指向另一个智能指针,此时原智能指针的引用数据 -1。

2.unique_ptr

和 shared_ptr 不同, unipue_ptr 只允许对原始指针的唯一引用,不可以对 unique_ptr 进行拷贝、赋值操作,但是可以使用 release 移交控制权。当 unique_ptr 的控制权移交出去以后,这个智能指针将不再有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void testUniquePtr()
{
unique_ptr<TestPtr> up0 = make_unique<TestPtr>("T0");

//unique_ptr<TestPtr> spA = sp0; //ERROR
//unique_ptr<TestPtr> spB(sp0); //ERROR

unique_ptr<TestPtr> up1(new TestPtr("T1"));
cout << "up1:" << up1->getName() << endl;

unique_ptr<TestPtr> up2(up1.release());
//cout << "sp1:" << sp1->getName() << endl; //ERROR sp1 is released
cout << "up2:" << up2->getName() << endl;

unique_ptr<TestPtr> up3(new TestPtr("T3"));
up3.reset(new TestPtr("T3reset"));
cout << "up3:" << up3->getName() << endl;

unique_ptr<TestPtr, DeleterFunctor> up4(new TestPtr("T4"), DeleterFunctor{});

//unique_ptr<TestPtr,decltype(DeleterFun)> sp5(new TestPtr("T5"), DeleterFun); //ERROR cannot get type of DeleterFun

auto deleter = [](TestPtr* p) {delete p; };
unique_ptr<TestPtr, decltype(deleter)> up6(new TestPtr("T6"), deleter);
}
  • up0 使用 make_unique 构造,up1 使用原始指针构造

  • up2 是由 sp1 转交其控制权来构造的。up1.release() 后,up1 不可以再使用

  • up3 使用原始指针构造以后,又使用了 reset 进行了重置

  • up4,up6 是使用构造时附加了删除器。当它们的引用计数为 0 时,删除器会被调用。和 shared_ptr 不同的是,unique_ptr 附加删除器前需要先声明类型。这里使用 decltype 来获取 lambda表达式的类型。up5 中函数指针在类型无法获取,所以不能用该函数指针作为删除器。这种声明使用的方式略显麻烦,好在这种用法在 C++17 得到了改进,可以像 shared_ptr 的删除器一样使用。

3.weak_ptr

weak_ptr 用于配合 shared_ptr 使用,用来观测 shared_ptr 的使用情况。weak_ptr 可以指向 shared_ptr 绑定的对象而不改变其引用计数,因此不影响对象的生成周期。它的引用一般是为了解决 weak_ptr 循环引用的问题,如二叉树中父节点和子节点的循环引用 。 weal_ptr 并没有重载 “->”,“*” 操作符,也以无法直接通过 weak_ptr 使用对象。weak_ptr 的 expired() 方法手于检测其关联的 shared_ptr 是否过期; lock() 方法手于从 weak_ptr 中获取一个 shared_ptr 。这个时候 weak_ptr 或其关联的 shared_ptr 的引用计数会增加 1.

1
2
3
4
5
6
7
8
9
10
11
12
void testWeakPtr()
{
shared_ptr<TestPtr> sp0 = make_shared<TestPtr>("T0");
weak_ptr<TestPtr> wp0(sp0);
cout << wp0.use_count() << endl;
cout << sp0.use_count() << endl;

shared_ptr<TestPtr> sp1 = wp0.lock();
cout << wp0.use_count() << endl;
cout << sp0.use_count() << endl;
cout << sp1.use_count() << endl;
}

4.enable_shared_from_this

看如下代码:

1
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
class A
{
public:
A(int a) : m_ptr(new int(a)){}
~A()
{
cout << "Release A ";
delete m_ptr;
m_ptr = nullptr;
cout << " completed!" << endl;
}
private:
int *m_ptr;
};

int main()
{
A a(5);
{
shared_ptr<A> ptr = make_shared<A>(a);
shared_ptr<A> ptr2 = make_shared<A>(a);
}

return 0;
}

//OutPut:
//Release A completed!
//Release A

在 main 函数中,当 ptr 和 ptr2 出了作用域时,会出现异常。因为在 {} 作用中,ptr 和 ptr2 同时引用了对象 a ,且它们都认为自己是对 a 对象的唯一引用(引用计数为1),在出作用域后,这两个 shared_ptr 释放了同一个对象而引起崩溃。同样的另一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class B
{
public:
B(int b):m_ptr(new int(b)){}
~B()
{
cout << "Release B ";
delete m_ptr;
m_ptr = nullptr;
cout << " completed!" << endl;
}

public:
shared_ptr<B> get_ptr()
{
//return ?
}
private:
int *m_ptr;
};

我们经常会在类的内部引用该类的实例的指针,或是智能指针,尤其是在多线程程序中。我们因该如何获取类自己的智能指针,即,上例中的 get_ptr() 函数体要如何实现呢?如果这样写:

1
2
3
4
5
shared_ptr<B> get_ptr()
{
shared_ptr<B> ptr(this);
return ptr;
}

是使用了this重新构造了一个shared_ptr, 在出了函数作用域时这个实例将被释放掉。

为了解决这样的问题,引入了 enable_shared_from_this 类,继承该类的类都可以使用 shared_from_this 从weak_ptr安全的生成一个自身的shared_ptr.

1
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
class A : public std::enable_shared_from_this<A>
{
public:
A(int a) : m_ptr(new int(a)){}
~A()
{
cout << "Release A ";
delete m_ptr;
m_ptr = nullptr;
cout << " completed!" << endl;
}
private:
int *m_ptr;
};

int main()
{
{
shared_ptr<A> ptr = make_shared<A>(5);
cout << ptr.use_count() << endl;
shared_ptr<A> ptr2 = ptr->shared_from_this();
cout << ptr.use_count() << endl;
}
return 0;
}

//OutPut
//1
//2
//Release A completed!

可以看到,虽然有两个 shared_ptr 指向同一个对象,但这两个 shared_ptr 对这个对象的引用是累加的,不会引起重复释放的错误。

需要注意一点:shared_from_this 函数只有在 对象的 shared_ptr 实例 实例化完成之后才能使用,这是因为 shared_from_this 所依赖的 weak_ptr 并不在构造函数中设置,而是在 sahred_ptr 实例化的时候对设置。如下的代码是错误的:

1
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
class A : public std::enable_shared_from_this<A>
{
public:
A(int a) : m_ptr(new int(a))
{
shared_ptr<A> a = shared_from_this(); //error
//do sth
}

shared_ptr<A> get_ptr()
{
return shared_from_this();
}

void func()
{
shared_ptr<A> ptr = shared_from_this();
//do sth
}

~A()
{
cout << "Release A ";
delete m_ptr;
m_ptr = nullptr;
cout << " completed!" << endl;
}
private:
int *m_ptr;
};

int main()
{
A a(3);
a.func(); //error

return 0;
}