【C++知识点】C++11 常用新特性总结(二)

简介: 【C++知识点】C++11 常用新特性总结(二)

智能指针

垃圾回收机制已经大行其道,得到了诸多编程语言的支持,例如 Java、Python、C#、PHP 等。而 C++ 虽然从来没有公开得支持过垃圾回收机制,但 C++98/03 标准中,支持使用 auto_ptr 智能指针来实现堆内存的自动回收;C++11 新标准在废弃 auto_ptr 的同时,增添了 unique_ptr、shared_ptr 以及 weak_ptr 这 3 个智能指针来实现堆内存的自动回收。

所谓智能指针,可以从字面上理解为“智能”的指针。具体来讲,智能指针和普通指针的用法是相似的,不同之处在于,智能指针可以在适当时机自动释放分配的内存。


也就是说,使用智能指针可以很好地避免“忘记释放内存而导致内存泄漏”问题出现。由此可见,C++ 也逐渐开始支持垃圾回收机制了。


shared_ptr

智能指针都是以类模板的方式实现的,shared_ptr 也不例外。

shared_ptr(其中 T 表示指针指向的具体数据类型)的定义位于 <memory> 头文件,并位于 std 命名空间中,因此在使用该类型指针时,程序中应包含如下 2 行代码:

#include <memory>
using namespace std;

和 unique_ptr、weak_ptr 不同之处在于,多个 shared_ptr 智能指针可以共同使用同一块堆内存。并且,由于该类型智能指针在实现上采用的是引用计数机制,即便有一个 shared_ptr 指针放弃了堆内存的“使用权”(引用 计数减 1),也不会影响其他指向同一堆内存的 shared_ptr 指针(只有引用计数为 0 时,堆内存才会被自动释放)。

shared_ptr 智能指针的创建

shared_ptr 类模板中,提供了多种实用的构造函数:

shared_ptr<int> p1; //不传入任何实参
shared_ptr<int> p2(nullptr); //传入空指针 nullptr

空的 shared_ptr 指针,其初始引用计数为 0,而不是 1。

在构建 shared_ptr 智能指针,也可以明确其指向:

shared_ptr<int> p(new int(5));

C++11 标准中还提供了 std::make_shared 模板函数,其可以用于初始化 shared_ptr 智能指针。

shared_ptr<int> p = make_shared<int>(5);

shared_ptr 模板还提供有相应的拷贝构造函数和移动构造函数。

shared_ptr<int> p3;
//调用拷贝构造函数
shared_ptr<int> p4(p3);//或者 shared_ptr<int> p4 = p3;
//调用移动构造函数
shared_ptr<int> p5(move(p4)); //或者 shared_ptr<int> p5 = move(p4);

在初始化 shared_ptr 智能指针时,还可以自定义所指堆内存的释放规则,这样当堆内存的引用计数为 0 时,会优先调用自定义的释放规则。


在某些应用场景中,自定义释放规则是很有必要的。比如,对于申请的动态数组来说,shared_ptr 指针默认的释放规则是不支持释放数组的,只能自定义对应的释放规则,才能正确地释放申请的堆内存。


对于申请的动态数组,释放规则可以使用 C++11 标准中提供的 default_delete 模板类,我们也可以自定义释放规则:

#include <iostream>
using namespace std;
//自定义释放规则
void deleteInt(int* p) {
    delete[]p;
}
int main()
{
    //指定 default_delete 作为释放规则
    shared_ptr<int> p1(new int[3], default_delete<int[]>());
    //初始化智能指针,并自定义释放规则
    shared_ptr<int> p2(new int[3], deleteInt);
    return 0;
}

借助 lamdba,p2 还可以写成这样:

shared_ptr p2(new int[2], [](int* p) {delete[]p; });

unique_ptr

unique_ptr 指针自然也具备“在适当时机自动释放堆内存空间”的能力。和 shared_ptr 指针最大不同之处在于,unique_ptr 指针指向的堆内存无法同其它 unique_ptr 共享,也就是说,每个unique_ptr 指针都独自拥有对其所指堆内存空间的所有权。


weak_ptr

shared_ptr 是采用引用计数的智能指针,多个 shared_ptr 实例可以指向同一个动态对象,并维护了一个共享的引用计数器。 对于引用计数法实现的计数,总是避免不了循环引用的问题,shared_ptr 也不例外。

看案例:

#include <iostream>
using namespace std;
class CB;
class CA
{
public:
    CA() { cout << "CA() called! " << endl; }
    ~CA() { cout << "~CA() called! " << endl; }
    void set_ptr(shared_ptr<CB>& ptr) { m_ptr_b = ptr; }
    void b_use_count() { cout << "b use count : " << m_ptr_b.use_count() << endl; }
    void show() { cout << "this is class CA!" << endl; }
private:
    shared_ptr<CB> m_ptr_b;
};
class CB
{
public:
    CB() { cout << "CB() called! " << endl; }
    ~CB() { cout << "~CB() called! " << endl; }
    void set_ptr(shared_ptr<CA>& ptr) { m_ptr_a = ptr; }
    void a_use_count() { cout << "a use count : " << m_ptr_a.use_count() << endl; }
    void show() { cout << "this is class CB!" << endl; }
private:
    shared_ptr<CA> m_ptr_a;
};
int main(){
    shared_ptr<CA> ptr_a(new CA());
    shared_ptr<CB> ptr_b(new CB());
    cout << "a use count : " << ptr_a.use_count() << endl;
    cout << "b use count : " << ptr_b.use_count() << endl;
    ptr_a->set_ptr(ptr_b);
    ptr_b->set_ptr(ptr_a);
    cout << "a use count : " << ptr_a.use_count() << endl;
    cout << "b use count : " << ptr_b.use_count() << endl;
    return 0; 
}


可以看到最后两个类都没有被析构。

这个时候,可以用 weak_ptr 来解决这个问题,可以把两个类中的一个成员变量改为 weak_ptr 对象即可。 weak_ptr 不会增加引用计数,所以引用就构不成环。

private:
  weak_ptr<CB> m_ptr_b;

可变参数模板

C++11 之前,类模版和函数模版中只能含固定数量的模版参数。

C++11 的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模。

#include <iostream>
using namespace std;
//函数模板的参数个数为0到多个参数,每个参数的类型可以各不相同
template<class...T>
void funcName(T...args) {//args一包形参,T一包类型
    cout << sizeof...(args) << endl;//sizeof...固定语法格式计算获取到模板参数的个数
    cout << sizeof...(T) << endl;//注意sizeof...只能计算...的可变参
}
int main() {
    funcName(100, 200, 300,400,600);
    return 0;
}

参数包展开

递归函数

//递归
#include <iostream>
using namespace std;
void funcName1() {
    cout << "递归终止函数" << endl;
}
template<class T, class ...U>
void funcName1(T frist, U...others) {
    cout << "收到的参数值:" << frist << endl;
    funcName1(others...);//注意这里传进来的是一包形参不能省略... 
}
int main() {
    funcName1(1, 2, 3, 4, 5, 6);
    return 0;
}

使用 if constexpr

//if constexpr
#include <iostream>
using namespace std;
//if constexpr...()//constexpr代表的是常量的意思或者是编译时求值
//c++17中新增一个语句叫做编译期间if语句( constexpr if)
template<class T, class...U>
void funcName2(T frist, U...args) {
    cout << "收到参数:" << frist << endl;
    if constexpr (sizeof...(args) > 0) {
        //constexpr必须有否则无法编译成功,圆括号里面是常量表达式
        funcName2(args...);
    }
}
int main() {
    funcName2(1,2, 3, 4, 5, 6);
    return 0;
}

默认成员函数控制

在 C++ 中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、运算符重载、析构函数和 & 和 const& 的重载、移动构造、移动拷贝构造等函数。

如果在类中显式定义了,编译器将不会重新生成默认版本。

有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成。

#include <iostream>
using namespace std;
class ClassTest
{
public:
    ClassTest(int num):n(num){}
    ClassTest() = default; //显式缺省构造函数 让编译器生成不带参数的默认构造函数,改成delete就不会生成了
    ClassTest(const ClassTest&) = delete; //禁止编译器生成默认的拷贝构造函数
    ClassTest& operator=(const ClassTest& a); //在类中声明,在类外定义时,让编译器生成默认赋值运算符重载
private:
    int n;
};
ClassTest& ClassTest::operator=(const ClassTest& a) = default;//在类外让编译器默认生成
int main() {
    ClassTest c1;
    return 0;
}

新增容器

std::array

  1. 1.std::array 保存在栈内存中,相比堆内存中的 std::vector,它能够灵活的访问容器里面的元素,从而获得更高的性能。
  2. 2.std::array 会在编译时创建一个固定大小的数组,std::array 不能够被隐式的转换成指针,使用 std::array 只需指定其类型和大小即可!
  3. 3.c++11 封装了相关的数组模板类,不同于 C 风格数组,它不会自动退化成 T* 类型,它能作为聚合类型聚合初始化。
  4. std::array 是封装固定大小数组的容器,数组元素下标索引从 0 开始。

定义和初始化

//此处,std::array对象arr表示一个固定大小为5且未初始化的int 
//数组,因此所有5个元素都包含垃圾值。
std::array<int, 5> arr;
//这里,std::array对象arr1表示一个固定大小为10的字符串数组。
std::array<std::string, 10> arr1;
//前2个值将被初始化,其他值为0。
std::array<int, 10> arr3 = { 1, 2 } ;

如下代码有什么问题?

#include <iostream>
#include <array>
using namespace std;
int main() {
    array<int,3> arr = { 1,2,3 };
    int len = 3;
    array<int, len> arr2 = { 4,5,6 }; //错误
    return 0;
}

array<int,len> 非法,数组大小参数必须是常量表达式

访问元素

有 3 种方法可以访问 std::array 中的元素:

  1. 1.运算符 []: 使用运算符 [] 访问 std::array 中的元素。
//创建并初始化一个大小为10的数组。
std::array<int, 10> arr = { 1,2,3,4,5,6,7,8,9,10 } ;
int x = arr[2];
  1. 使用 [] 运算符访问超出范围的元素将导致未定义的行为。
  2. 2.at(): 使用 at() 成员函数访问 std::array 中的元素
//Accessing element using at() function
int x = arr.at(2);
  1. 使用 at() 函数访问任何超出范围的元素将抛出 out_of_range 异常。
  2. 3.std::tuple 的 get<>():
int x = std::get<2>(arr) ;
  1. 使用 get<> 运算符访问超出范围的元素将导致编译时错误。

其他常用函数

std::forward_list

std::forward_list 是一个列表容器,使用方法和 std::list 基本类似。

与 std::list 的双向链表的实现不同,std::forward_list 使用单向链表进行实现,提供了 O(1) 复杂度的元素插入,不支持快速随机访问,也是标准库容器中唯一一个不提供 size() 方法的容器。


forward_list 具有插入、删除表项速度快、消耗内存空间少的特点,但只能向前遍历。与其它序列容器(array、vector、deque)相比,forward_list 在容器内任意位置的成员的插入、提取(extracting)、移动、删除操作的速度更快,因此被广泛用于排序算法。


创建

由于 forward_list 容器以模板类 forward_list(T 为存储元素的类型)的形式被包含在头文件中,并定义在 std 命名空间中。因此,在使用该容器之前,代码中需包含下面两行代码:

#include <forward_list>
using namespace std;


创建 forward_list 容器的方式,大致分为以下 5 种。

1.创建一个没有任何元素的空 forward_list 容器:

std::forward_list<int> values;
  1. 由于 forward_list 容器在创建后也可以添加元素,因此这种创建方式很常见。

2.创建一个包含 n 个元素的 forward_list 容器:

std::forward_list<int> values(10);
  1. 通过此方式创建 values 容器,其中包含 10 个元素,每个元素的值都为相应类型的默认值(int类型的默 认值为 0)。

3.创建一个包含 n 个元素的 forward_list 容器,并为每个元素指定初始值。例如:

std::forward_list<int> values(10, 5);
  1. 如此就创建了一个包含 10 个元素并且值都为 5 个 values 容器。

4.在已有 forward_list 容器的情况下,通过拷贝该容器可以创建新的 forward_list 容器。例如:

std::forward_list<int> value1(10); 
std::forward_list<int> value2(value1);
  1. 注意,采用此方式,必须保证新旧容器存储的元素类型一致。

5.通过拷贝其他类型容器(或者普通数组)中指定区域内的元素,可以创建新的 forward_list 容器。例如:

//拷贝普通数组,创建forward_list容器
int a[] = { 1,2,3,4,5 }; 
std::forward_list<int> values(a, a+5);
//拷贝其它类型的容器,创建forward_list容器
std::array<int,5>arr{ 11,12,13,14,15 }; 
std::forward_list<int>values(arr.begin()+1, arr.end());//拷贝arr容器中的{12,13,14,15}

其他函数

unordered 系列

C++11 引入了两组无序容器:

std::unordered_set/std::unordered_multiset
std::unordered_map/std::unordered_multimap

无序容器中的元素是不进行排序的,内部通过 Hash 表实现,插入和搜索元素的平均复杂度为 O(1)。

#include <unordered_map>
#include <unordered_set>
目录
相关文章
|
21天前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
98 59
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(三)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(二)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(一)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
14天前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
26 0
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析继承机制(三)
【C++】面向对象编程的三大特性:深入解析继承机制
|
1月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析继承机制(二)
【C++】面向对象编程的三大特性:深入解析继承机制
|
1月前
|
安全 程序员 编译器
【C++】面向对象编程的三大特性:深入解析继承机制(一)
【C++】面向对象编程的三大特性:深入解析继承机制
|
1月前
|
存储 编译器 程序员
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(二)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值