松垮垮 松垮垮
首页
  • GPU并行编程
  • 图形学
  • 归并算法
  • 计算机视觉
  • css
  • html
  • JavaScript
  • vue
  • 压缩命令
  • cmdline
  • Docker
  • ftrace跟踪技术
  • gcov代码覆盖率测试
  • GDB
  • git
  • kgdb
  • linux操作
  • markdown
  • systemtap
  • valgrind
  • 设计模式
  • 分布式
  • 操作系统
  • 数据库
  • 服务器
  • 网络
  • C++
  • c语言
  • go
  • JSON
  • Makefile
  • matlab
  • OpenGL
  • python
  • shell
  • 正则表达式
  • 汇编
  • GPU并行编程
  • mysql
  • nginx
  • redis
  • 网络
  • 计算机视觉
  • 进程管理
  • linux调试
  • 【Python】:re.error bad escape i at position 4
  • 搭建ai知识助手
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

松垮垮

c++后端开发工程师
首页
  • GPU并行编程
  • 图形学
  • 归并算法
  • 计算机视觉
  • css
  • html
  • JavaScript
  • vue
  • 压缩命令
  • cmdline
  • Docker
  • ftrace跟踪技术
  • gcov代码覆盖率测试
  • GDB
  • git
  • kgdb
  • linux操作
  • markdown
  • systemtap
  • valgrind
  • 设计模式
  • 分布式
  • 操作系统
  • 数据库
  • 服务器
  • 网络
  • C++
  • c语言
  • go
  • JSON
  • Makefile
  • matlab
  • OpenGL
  • python
  • shell
  • 正则表达式
  • 汇编
  • GPU并行编程
  • mysql
  • nginx
  • redis
  • 网络
  • 计算机视觉
  • 进程管理
  • linux调试
  • 【Python】:re.error bad escape i at position 4
  • 搭建ai知识助手
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • C++

    • C++11
      • 用户定义
      • 原生字符串字面值(raw string literal)
      • 左右值引用
      • 移动语义
        • 转移构造函数
        • 转移赋值函数
      • 标准库函数 std::move
      • 完美转发 std::forward
        • 引用折叠
        • std::forward
      • 通用引用
      • 可变参数模板函数
        • 定义
        • 参数包的展开
      • 可变参数模板类
        • 继承方式展开参数包
        • 模板递归和特化方式展开参数包
      • 定义
      • 仿函数:重载(不是c++11的内容)
      • std::bind绑定器
        • std::function模板
        • std::bind模板
        • std::bind模板和std::function模板配合
      • lambda表达式
        • 定义
        • lambda与仿函数
        • lambda类型的存储和操作
        • 意义
      • function
      • unique_ptr
      • shared_ptr
        • 使用的一些问题
      • weak_ptr
      • make_shared
      • make_unique
      • 智能指针的自定义释放函数
      • sharedfromthis和对象保活
      • 智能指针对象大小
      • 编译器对模板的右尖括号做单独处理
      • 模板的特化和偏特化
      • 模板的别名
        • 函数模板的默认模板参数
    • C++17
    • c++规范
    • cmake和宏开关
    • 为了高效
    • 作用域和生命周期
    • 关键字
    • 内存分配
    • 基础
    • 容器
    • 对象和类
    • 线程
  • c语言
  • Go

  • JSON
  • Makefile
  • matlab

  • OpenGL
  • python

  • shell
  • 正则表达式
  • 汇编
  • 语言
  • C++
songkuakua
2025-02-15
目录

C++11

# C++11

Owner: -QVQ-

  1. 自动类型推导 高级for循环
  2. (性能)常量表达式 左值引用-移动语义-完美转发
  3. 可变参数模板-类-函数
  4. 闭包-仿函数和bind绑定器(新的调用实体)-lambda
  5. 智能指针
  6. 类的改进-继承构造-委托构造
  7. 线程

重载+ 、左移重载 、前置++ 、后置++、=、==:

点击查看
**加号运算符重载**

//成员函数重载+

Person operator+(Person& p2)

{

Person temp;

temp.m_A = p2.m_A + m_A;

temp.m_B = p2.m_B + m_B;

return temp;

}

**左移运算符重载**

//只能在全局函数实现左移重载

//ostream对象只能有一个

ostream& operator<<(ostream& out, Person p) {//注意当类中有堆区时这里要改成person &p

out << p.m_B;

return out;

}

**//前置++**

MyInteger& operator++() {

m_Num++;

return *this;

}

**//后置++**

MyInteger
operator++(int) {

MyInteger temp = *this;

m_Num++;

return temp;

}

**重载=**

如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题

class Person

{

public:

Person(int age)

{

//将年龄数据开辟到堆区

m_Age = new int(age);

}

//重载赋值运算符

Person& operator=(Person &p)

{

if (m_Age != NULL)

{

delete m_Age;

m_Age = NULL;

}//提供深拷贝 解决浅拷贝的问题

m_Age = new int(*p.m_Age);

return *this;//返回自身便于实现链式赋值;a=b=c;

}

~Person()

{

if (m_Age != NULL)

{

delete m_Age;

m_Age = NULL;

}

}

//年龄的指针

int *m_Age;

};

**重载==**

bool operator==(Person & p)

{

if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)

{

return true;

}

else

{

return false;

}

}

**函数调用运算符重载**

- 函数调用运算符
() 也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活

class MyPrint

{

public:

void operator()(string text)

{

cout << text << endl;

}

};

void test01()

{

//重载的()操作符 也称为仿函数

MyPrint myFunc;

myFunc("hello
world");

}

class MyAdd

{

public:

int operator()(int v1, int v2)

{

return v1 + v2;

}

};

void test02()

{

MyAdd add;

int ret = add(10, 10);

cout << "ret = "
<< ret << endl;

//匿名对象调用

cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;

}

静态多态:函数重载和运算符重载,编译阶段确定函数地址

动态多态: 派生类和虚函数实现,运行时确定函数地址

STL大体分为六大组件,分别是:容器、算法、迭代器、仿函数(函数对象)、适配器(配接器)、空间配置器

- 容器:用于存储和管理数据。
- 算法:用于操作容器中的数据。
- 迭代器:用于遍历容器中的数据。
- 仿函数:类似于函数,可作为算法的参数传递。
- 适配器:用于将一种容器类型转换为另一种容器类型。
- 空间配置器:用于管理容器的内存分配和释放。
  • 顺序容器、关联容器、无序容器

    序列式容器

    • vector:动态数组,支持随机访问,尾部插入和删除操作效率高,中间插入和删除效率低。空间预先分配,不够时扩容为原来的2倍。
    • list:双向链表,支持双向遍历、插入、删除等操作,但不支持随机访问。
    • deque:双端队列,支持随机访问,头尾插入和删除操作效率高,中间插入和删除效率低。空间预先分配,不够时扩容为原来的2倍。

    关联容器

    • set:集合,内部实现为红黑树,支持自动排序、搜索、插入和删除操作。元素必须唯一。(key 和值 value)(自平衡的二叉查找树)
    • multiset:多重集合,内部实现为红黑树,支持自动排序、搜索、插入和删除操作。元素可以重复。
    • map:映射,内部实现为红黑树,支持自动排序、搜索、插入和删除操作。元素由键值对构成,键必须唯一。(pair)
    • multimap:多重映射,内部实现为红黑树,支持自动排序、搜索、插入和删除操作。元素由键值对构成,键可以重复。

    无序关联容器

    • unordered_set:集合,内部实现为哈希表,支持自动排序、搜索、插入和删除操作。元素必须唯一。
    • unordered_multiset:多重集合,内部实现为哈希表,支持自动排序、搜索、插入和删除操作。元素可以重复。
    • unordered_map:映射,内部实现为哈希表,支持自动排序、搜索、插入和删除操作。元素由键值对构成,键必须唯一。
    • unordered_multimap:多重映射,内部实现为哈希表,支持自动排序、搜索、插入和删除操作。元素由键值对构成,键可以重复。

    stack

面向对象和面向过程的区别

面向过程:

注重步骤与流程

数据和行为在代码中是分离的,不具备封装性。

适合用于功能比较简单

面向对象:

以对象为中心,以类为单位进行设计。

继承、多态、封装性。

适合用于功能比较复杂

# 自动类型推导

自动类型推导 auto关键字 auto b = {1,2,3};//b 为class std::initializer_list<int> //不能用于函数的参数定义 int abc(auto cc)//错误 //必须初始化 auto b;//错误 //不能初始化数组 auto a[3] = {1,2,3}; //不能放入模板实例化 vector<auto> b ;//错误
decltype 自动推导出变量的值并作为关键字可以使用 int b = 1;``decltype(b) a;cout << typeid(a).name()<<endl; //输出int //匿名类型的枚举变量定义 enum{Ok,Error)flag;decltype(flag)flag2;
追踪返回类型 auto的高级用法 可以指定返回的函数类型 //单独使用auto auto func2(int a,int b)->int{ return a+b:} //和decltype配合使用 auto func3(int a,double b)->decltype(a+b){ return a +b;} //配合模板的使用 template<class T1,class T2>auto mul(Tl &t1,T2 &t2)->decltype(t1*t2){``return t1*t2;} //如果不用auto指定就可能出现损失精度的情况,比如t1是int,t2是doubel,如果不用auto指定了int,那就损失了 精度。如果t1是int,t2是int,指定了double又会浪费空间

# 易用性的改进

易用性的改进 特性 描述 示例代码 注意事项
初始化 C++11 引入初始化列表 使用 `{}` 的方式调用构造函数进行初始化 int a[]{1, 3, 5};
int i = {1};
int a{3};
int* arr = new int[3]{1, 2, 3};
int a=1024; char b=a; // 数据丢失,类型收窄
char b{a}; // 直接报错,C++11 可自动判断类型收窄问题
高级 for 循环 可以直接遍历数组 int a[3] = {1,2,3};
for(int i : a){ cout << i; }
- **注意**:函数参数里的数组是指针,不确定范围,不能用高级 for
- **错误**:
void abc(int a[3]){ for(int i:a) }
- **正确**:
int a[3]{1,2,3}; void abc() { for (int i : a) { cout << i << endl; } }
静态断言 static_assert 编译时检查条件 static_assert(sizeof(int) == 4, "int must be 4 bytes long"); static_assert 里的表达式必须是常量
**正确**:
const int a = 1; static_assert(a == 1, "OK");
**错误**:
int a = 1; static_assert(a == 1, "Error");
noexcept 修饰符 C++11 取消 `throw()` 防止函数抛出异常 void func() noexcept { } 如果 `noexcept` 函数内抛异常,会编译报错
nullptr 关键字 解决 NULL 二义性问题 C++11 引入 `nullptr` 作为类型安全的指针空值 **错误**: fun(NULL); // NULL 可能解析为 `int`
**正确**: fun(nullptr); // `nullptr` 只能赋给指针
int a = NULL; // 通过
int a = nullptr; // 语法错误
常量表达式 constexpr 允许在编译时进行计算 **错误**:
int GetConst() { return 3; } int arr[GetConst()];
**正确**:
constexpr int GetConst() { return 3; } int arr[GetConst()];
- `constexpr` 不能调用普通函数
- 不能使用全局变量
- 必须在使用前定义
C++11 线程安全 局部静态对象的初始化 C++11 规定局部静态变量的初始化是 **线程安全** 的,仅在 **首次访问** 时初始化一次

# 用户定义

重载“”操作符的返回值,可以起到简化代码的读写

格式:返回值 operator"" _函数名(参数列表) { }

参数列表内容只能为如下:

char const * unsigned long long long double char const *, size_t//常用

wchar_t const *, size_t char16_t const *, size_t char32_t const *, size_t

其中后四个参数列表适用于字符串,size_t会自动得到长度信息

示例:

  1. size_t operator"" _len(char const * str, size_t size){ return size; }

使用:

cout << "mike"_len <<endl;//4

# 原生字符串字面值(raw string literal)

使用户书写的字符串“所见即所得”

格式:R"(随意格式的字符串)"

例子:

cout << R"(s\n f)/nm)";

输出:

s\n f)/nm

# 右值引用

# 左右值引用

移动语义: 以移动而非深拷贝的方式初始化含有指针成员的类对象。移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。能够大幅度提高 C++ 应用程序的性能。(临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。)

**左值:**表示可以取地址的、有名字的,const int &b 、int、const int

**右值:**表示字面常量、表达式、函数的非引用返回值等,左值引用(int &a)也是右值

左值引用:

“const 类型 &”为 “万能”的左值引用类型,它可以接受非常量左值、常量左值、右值对其进行初始化;

int &a = 2; // 左值引用int &a绑定到右值2,编译失败, err

const int &c = b; // 常量左值引用const int &c绑定到非常量左值b,编译通过, ok

//相当于c变量和b变量是同一个变量空间,此时c=b=2,修改一个另一个也改变

//但是c是常量不能改变,c = 10 不行,但可以修改b,b = 10,此时c = 10

const int &b = 2; // 常量左值引用const int &b绑定到右值2,编程通过, ok

//相当于为2分配了一个空间,b得到这个空间的地址

右值引用:

int && r1 = 22;//相当于为22分配一个空间,这个空间的地址给r1,和int r1 =22使用上没有区别

但性能上,int r1 =22是生成22的空间和r1的空间,再把22拷贝给r1,效率低些

int && r2 = r1; //err右值引用是不能够绑定到任何的左值的

const int&& a = 10;//编译器不会报错,但无实际用处

//一方面右值引用主要用于移动语义和完美转发,需要修改常量右值

//另一方面常量右值引用的作用是引用一个不可修改的右值,这项工作完全可以交给常量左值引用完成

类中可以定义左值变量不初始化,类外不行

//int& a;//错误必须初始化
int& a = 1;//正确

class stu{
	//stu(){}//error,必须初始化int& s
	stu(int a):a_(a){}
	int &a_;//可以,但必须在类的实例化时就初始化
}
1
2
3
4
5
6
7
8

C++左值引用和右值引用:

int&& r1 = 22; // 相当于为22分配一个空间,这个空间的地址给r1,和int r1 = 22使用上没有区别

// 但性能上,int r1 = 22是生成22的空间和r1的空间,再把22拷贝给r1,效率低些
//实际上对于这种简单值,编译器可能做了优化,对于复杂值使用右值引用才有意义

1
2
3
4
5
引用类型 非常量左值 常量左值 非常量右值 常量右值 使用场景
非常量左值引用 Y N N N 无
int b = 1;
int &a = b;
常量左值引用 Y Y Y Y 常用于类中构建拷贝构造函数
int b =1;
const &c = b; const int b =1;
const &a = b; const &a = 1; const int a = 1;
const int& b= move(a);
//相当于c变量和b变量是同一个变量空间,此时c=b=2,修改一个另一个也改变
//但是c是常量不能改变,c = 10 不行,但可以修改b,b = 10,此时c = 10
非常量右值引用 N N Y N 移动语义、完美转发
int &&a=1;
常量右值引用 N N Y Y 无实际用途
const int&& i = 1; const int a = 1;
const int&& b= move(a);

注意:

函数返回值为右值,引用类型作为返回值为左值 无论是函数返回值为引用类型,还是外部用右值引用接收 都会延长函数内部定义的变量的生命周期,

但是这会带来一些问题!,如下是一个类作为返回值时为什么不要用引用的方式的例子

class stu{
public:
	~stu(){
		a = 0;
	}
	int a = 10;
}

//对于右值作为函数返回值
stu&& test1(void) {
	stu a;
	return move(a);//这里虽然调用的右值引用,但生命周期结束了调用析构函数
}
int main(){
	stu&& a1 = test1();//这里a1得到的是调用了析构函数的对象,在工程项目中有极大风险
	stu a1 = test1();//这是安全的,但这实际上并没有起到引用的效果,生成了两个对象,将调用两次构造析构
	cout << a1.a << endl;//这里将输出0,这表示已经调用过类的析构函数了
}

//同理,对于左值作为函数返回值,也一样存在风险
stu& test1(void) {
	stu a;
	return a;
}
int main(){
	stu& a1 = test1();//同上
	stu a1 = test1();//同上
	cout << a1.a << endl;//同上
}
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

# 移动语义

# 转移构造函数

  • 对于如下类MyString

    class MyString
    {
    public:
        MyString(const char *tmp = "abc")
        {//普通构造函数
            len = strlen(tmp);  //长度
            str = new char[len+1]; //堆区申请空间
            strcpy(str, tmp); //拷贝内容
    
            cout << "普通构造函数 str = " << str << endl;
        }
    
        MyString(const MyString &tmp)
        {//拷贝构造函数
            len = tmp.len;
            str = new char[len + 1];
            strcpy(str, tmp.str);
    
            cout << "拷贝构造函数 tmp.str = " << tmp.str << endl;
        }
    
        //移动构造函数
        //参数是非const的右值引用
        MyString(MyString && t)
        {
            str = t.str; //拷贝地址,没有重新申请内存
            len = t.len;
    
            //原来指针置空
            t.str = NULL;
            cout << "移动构造函数" << endl;
        }
    
        MyString &operator= (const MyString &tmp)
        {//赋值运算符重载函数
            if(&tmp == this)
            {
                return *this;
            }
    
            //先释放原来的内存
            len = 0;
            delete []str;
    
            //重新申请内容
            len = tmp.len;
            str = new char[len + 1];
            strcpy(str, tmp.str);
             cout << "赋值运算符重载函数 tmp.str = " << tmp.str << endl;
            return *this;
    
        }
    
        ~MyString()
        {//析构函数
            cout << "析构函数: ";
            if(str != NULL)
            {
                cout << "已操作delete, str =  " << str;
                delete []str;
                str = NULL;
                len = 0;
    
            }
            cout << endl;
        }
    
    private:
        char *str = NULL;
        int len = 0;
    };
    
    
    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
    68
    69
    70
    71
    72
MyString func() //返回普通对象,不是引用
{
    MyString obj("mike");

    return obj;
}
int main()
{
    MyString &&tmp = func(); //右值引用接收
		//MyString tmp = func(); //输出结果一样
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12

执行结果:

普通构造函数 str = mike
移动构造函数
析构函数:
析构函数: 已操作delete, str =  mike
1
2
3
4

用拷贝函数,MyString(MyString t)会发生再创建MyString的行为

用移动语义,MyString(MyString && t),不会发生再创建MyString的行为,提高了性能

移动语义在堆空间的应用:

通过基于移动语义的移动构造函数,在传入函数时并没有拷贝,而是直接使用的变量,先前申请的obj局部变量随函数func()结束,但其中申请的堆空间给了现在的tmp

func()中obj申请堆空间,将内容拷贝给tmp,tmp申请堆空间存放内容后,obj释放堆空间,浪费了性能

# 转移赋值函数

加上如下内容

//移动赋值函数
    //参数为非const的右值引用
    MyString &operator=(MyString &&tmp)
    {
        if(&tmp == this)
        {
            return *this;
        }

        //先释放原来的内存
        len = 0;
        delete []str;

        //无需重新申请堆区空间
        len = tmp.len;
        str = tmp.str; //地址赋值
        tmp.str = NULL;

        cout << "移动赋值函数\n";

        return *this;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 标准库函数 std::move

std::move() 函数,将左值强制转换成对应的右值

move是将对象的所有权从一个对象移到另一个对象,没有内存的搬迁或拷贝,因此可以提高效率改善性能

int a; int &&r1 = a; // 编译失败 int &&r2 = std::move(a); // 编译通过

clock_t start = clock();
	abc a1= { "wewefnwngwaggw", "fagsdgadsgasdgasdga" };
	for (int i = 0; i < 100300; i++) {
		abc a2 = a1;
		//abc&& a2 = move(a1);//上面约500个时钟,这一条约20个时钟单位
		//abc& a2 = a1;//左值引用和右值引用差别不大
		a2.str += "1";
		a1 = move(a2);
		//a1 = a2;//这一条和上一条耗时差别不大
	}
	clock_t end = clock();
1
2
3
4
5
6
7
8
9
10
11

使用:当用push_back这类函数时,会先生成一个临时对象再拷贝,从而降低了性能。emplace_back是生成临时对象并直接给这个对象

实现:

通过右值引用+折叠引用实现完美转发,传入参数,再用类型转换转为右值,

返回时用类模板的偏特化实现

template <class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
    return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}

//模板特例化
template <class _Ty>
struct remove_reference {
    using type                 = _Ty;
    using _Const_thru_ref_type = const _Ty;
};

template <class _Ty>
struct remove_reference<_Ty&> {
    using type                 = _Ty;
    using _Const_thru_ref_type = const _Ty&;
};

template <class _Ty>
struct remove_reference<_Ty&&> {
    using type                 = _Ty;
    using _Const_thru_ref_type = const _Ty&&;
};

template <class _Ty>
using remove_reference_t = typename remove_reference<_Ty>::type;
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

# 完美转发 std::forward

适用于这样的场景:需要将一组参数原封不动的传递给另一个函数

“原封不动”不仅仅是参数的值不变,还有以下两组属性:左值/右值和 const/non-const。

完美转发就是在参数传递过程中,所有这些属性和参数值都不能改变,同时,而不产生额外的开销

对于使用而言,传入函数内的参数的使用,和在主函数的使用没有区别,并且运行结果也能保存下来。对这个参数的使用,就等同于将在主函数执行了函数内的代码一般

对于如下情景:

template<typename T>void print(T& t){
    std::cout<< "Lvalue ref"<< std::endl;
}

template<typename T>void print(T&& t){
    std::cout<< "Rvalue ref"<< std::endl;
}

template<typename T>void testForward(T&& v){//完美转发要求这里必须是&&右值引用
    print(v);//v此时已经是个左值了,永远调用左值版本的print
    print(std::forward<T>(v));//完美转发
    print(std::move(v));//永远调用右值版本的print
std::cout<< "======================"<< std::endl;
}

int main(int argc,char* argv[])
{
int x= 1;
    testForward(x);//实参为左值
    testForward(std::move(x));//实参为右值
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

print(v);//v此时已经是个左值了,永远调用左值版本的print

print(std::move(v));//永远调用右值版本的print

C++11是通过引入“引用折叠”(reference collapsing)的新语言规则,并结合新的模板推导规则来完成完美转发。

# 引用折叠

对于如下问题

typedef const int T; typedef T & TR; TR &v = 1;

C++11中的引用折叠规则:

TR的类型定义 声明v的类型 v的实际类型
T & TR T &
T & TR & T &
T & TR && T &
T && TR T &&
T && TR & T &
T && TR && T &&

总结:TR和V的声明中如果出现了引用,只要有出现&,v的实际类型都是&

即T& > T&& > T

# std::forward

C++11中,std::forward可以保存参数的左值或右值特性:

#include <iostream>
using namespace std;

template <typename T> void process_value(T & val)
{
    cout << "T &" << endl;
}

template <typename T> void process_value(T && val)
{
    cout << "T &&" << endl;
}

template <typename T> void process_value(const T & val)
{
    cout << "const T &" << endl;
}

template <typename T> void process_value(const T && val)
{
    cout << "const T &&" << endl;
}

//函数 forward_value 是一个泛型函数,它将一个参数传递给另一个函数 process_value
template <typename T> void forward_value(T && val) //完美转发必须要有这个中间函数,完美转发要求参数必须是为右值引用
{
    process_value( std::forward<T>(val) );//C++11中,std::forward可以保存参数的左值或右值特性
}

int main()
{
    int a = 0;
    const int &b = 1;

    forward_value(a); //输出 T &
    forward_value(b); // 输出const T &
    forward_value(2); // 输出T &&
    forward_value( std::move(b) ); //输出 const T &&

    return 0;
}
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

# 通用引用

**定义:**匹配左值表达式时,它表现的像一个左值引用;匹配右值表达式时,它表现的像一个右值引用。

构成通用引用的条件:

  • 必须精确满足T&&这种形式(即使加上const也不行)
  • 类型T必须是通过推断得到的(最常见的就是模板函数参数)

常量左值引用,传入的参数既可以是右值又可以是左值 通用引用也可以传入左值和右值,在这一点,他们是有共同的性质,但他们是不一样的

在C++11中,通用引用主要是为了实现在模板的推导中完美转发的功能。

例子

//r3是右值引用,其余是通用引用

int lvalue = 3;
auto&& r1 = lvalue;//左值引用
auto&& r2 = 4;//右值引用

int&& r3 = 5;//右值引用
auto&& r4 = r3; //等价于int& r4 = r3;//左值引用

std::vector<int> vect;
auto&& r5 = vect[0];//左值引用

template<typename T>
void func(T&& p);

func(lvalue);//左值引用
func(10);//右值引用
//注意是自动推导出来的才行
func<int>(10);//此时只能是右值引用
//func<int>(lvalue);//错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

注意:

模板类,在实例化时已经决定了类型,所以并不能算为推断

如下类型都不是通用引用

template<typename T>
class Bar
{
  private:
    T _m;
  public:
    Bar(T&& t); //不是通用引用,而是右值引用
};

template <class T, class Allocator = allocator<T> >
class vector
{
public:
    void push_back(T&& x); //不是通用引用,而是右值引用 
};
//vector类型是确定的,所以不是通用引用
template<typename S>
void func(std::vector<S>&& p);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

通用引用类型的本身是一个左值,

C++11中的通用引用 (opens new window)

# 可变参数模板

# 可变参数模板函数

# 定义

template<class ... T>
 void func(T ... args)
{//可变参数模板函数
	  T a[] = {args...};//可以实现把参数都拷贝给数组a,
		int length = sizeof...(args)//得到数组大小
    //sizeof...(sizeof后面有3个小点)计算变参个数
		cout<<length;
}

int main()
{
    func();     // 0
    func(1);    // 1
    func(2, 1.0);   // 2

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 参数包的展开

递归方式展开

通过递归函数展开参数包,需要提供递归终止函数

//递归终止函数
void debug()
{
    cout << "empty\n";
}

//展开函数
template <class T, class ... Args>
void debug(T first, Args ... last)
{
    cout << "parameter " << first << endl;
    debug(last...);
}

//递归调用
debug(1, 2, 3, 4);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

//上诉代码递归调用顺序如下:

debug(1, 2, 3, 4); debug(2, 3, 4); debug(3, 4); debug(4); debug();

非递归方法展开:

方式一:

对于

template <class T>
T print(T arg)
{
    cout << arg;
    return  arg;
}

template <class ... Args>
void expand(Args ... args)
{
    int a[] = {print(args)...};

}

int main()
{
    expand(1, 2, 3, 4);

    return 0;
}//输出1234
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

方式二:

template <class T>
void print(T arg)
{
    cout << arg << endl;
}

template <class ... Args>
void expand(Args ... args)
{
    int a[] = { (print(args), 0)... };//逗号表达式
}

int main()
{
    expand(1, 2, 3, 4);

    return 0;
}//输出1234
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 可变参数模板类

# 继承方式展开参数包

template<typename... A> class BMW{};  // 变长模板的声明

template<typename Head, typename... Tail>  // 递归的偏特化定义
class BMW<Head, Tail...> : public BMW<Tail...>
{//当实例化对象时,则会引起基类的递归构造
public:
    BMW()
    {
        printf("type: %s\n", typeid(Head).name());
    }

    Head head;
};

template<> class BMW<>{};  // 边界条件

int main()
{
    BMW<int, char, float> car;
    /*
    运行结果:
        type: float
        type: cchar
        type: int
    */

    return 0;
}
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

先Head=int,char和float作为Tail传入基类public BMW<Tail...>

Head=char,float作为Tail传入基类public BMW<Tail...>

Head=float, 调用边界条件template<> class BMW<>{}

弹出栈,开始构建

# 模板递归和特化方式展开参数包

template <long... nums> struct Multiply;// 变长模板的声明

template <long first, long... last>
struct Multiply<first, last...> // 变长模板类
{
    static const long val = first * Multiply<last...>::val;
};

template<>
struct Multiply<> // 边界条件
{
    static const long val = 1;
};

int main()
{
    cout << Multiply<2, 3, 4, 5>::val << endl; // 120

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 闭包

# 定义

带有上下文的函数,属于有状态的函数(仿佛就是类换了个名字),有权访问另一个函数作用域中变量的函数

一个函数,带上了一个状态(即这个闭包有自己的变量),就变成了闭包了

闭包的状态捆绑,必须发生在运行时

闭包(closure)是使用了它的外部函数局部变量的函数。

具体来讲,如果 A 是一个函数,而 B 是在 A 中定义的一个函数(表达式),且在 B 的函数体中使用了 A 的局部变量 a,那么就将函数 B 称为闭包。

特点:

  • 变量常驻在内存
  • 可以避免使用全局变量污染命名空间
  • 内存消耗大

# 仿函数:重载(不是c++11的内容)

类中对()这个操作符的重载

class MyFunctor{

int operator()() { cout<<"hello" }

}

使用:

MyFunctor a;//MyFunctor a()就成调用构造函数了

a();//hello

# std::bind绑定器

# std::function模板

#include <functional>

可调用实体主要包括:函数、函数指针、函数引用、可以隐式转换为函数指定的对象,或者实现了opetator()的对象。

C++11中,新增加了一个std::function类模板,它是对C++中现有的可调用实体的一种类型安全的包裹。通过指定它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们。

最大的用处就是在实现函数回调,函数回调允许将一个函数作为参数传递给另一个函数,以便在后者执行时调用前者。这种技巧通常用于实现事件处理、回调函数等功能。

它不能被用来检查相等或者不相等,但是可以与NULL或者nullptr进行比较

示例:

绑定一个普通函数:

void func(void);
function< void(void) > f1 = func;//绑定
1
2

绑定一个类中的静态函数:

class Foo{
public:
    static int foo_func(int a){};
}
function< int(int) > f2 = Foo::foo_func;//绑定
1
2
3
4
5

绑定一个仿函数:

class Bar{
public:
int operator()(int a){};
}
Bar obj;
f2 = obj;//绑定
f2(222); //调用
1
2
3
4
5
6
7

# std::bind模板

#include <functional>

它可以预先把指定可调用实体的某些参数绑定到已有的变量,产生一个新的可调用实体

C++98中,bind1st和bind2nd,分别可以用来绑定functor的第一个和第二个参数,限制太大,可用性低

C++11中,提供了std::bind,它绑定的参数的个数不受限制,绑定的具体哪些参数也不受限制

定义:bind(可调用实体,可调用实体所在的类实例(可以没有),参数列表……)

void func(int x, int y)
{
    cout << x << " " << y << endl;
}

int main()
{
    bind(func, 1, 2)();                     //输出:1 2
    bind(func, std::placeholders::_1, 2)(1);//输出:1 2

    using namespace std::placeholders;    // adds visibility of _1, _2, _3,...
    bind(func, 2, _1)(1);       //输出:2 1
    bind(func, 2, _2)(1, 2);    //输出:2 2
    bind(func, _1, _2)(1, 2);   //输出:1 2
    bind(func,_2, _1)(1, 2);    //输出:2 1

    //err, 调用时没有第二个参数
    //bind(func, 2, _2)(1);

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

//std::placeholders::_1是占位符,表示这个位置将在函数调用时,被传入的第一个参数所替代。

//std::placeholders::_2是占位符,被传入的第二个参数所替代

  • 测试代码

    #include <iostream>
    #include <functional>
    
    using namespace std;
    
    void f(int& a, int& b, int& c) {
        cout << "in function a = " << a << "  b = " << b << "  c = " << c << endl;
        cout << "in function a = " << &a << "  b = " << &b << "  c = " << &c << endl;
        a += 1;
        b += 10;
        c += 100;
    }
    
    int main() {
    
        int n1 = 1, n2 = 10, n3 = 100;
    
        function<void()> f1 = bind(f, n1, n2, ref(n3));
    
        f1();
        cout << "out function a = " << n1 << "  b = " << n2 << "  c = " << n3 << endl;
        cout << "out function a = " << &n1 << "  b = " << &n2 << "  c = " << &n3 << endl;
        f1();
        cout << "out function a = " << n1 << "  b = " << n2 << "  c = " << n3 << endl;
        cout << "out function a = " << &n1 << "  b = " << &n2 << "  c = " << &n3 << endl;
        f1();    f1();    f1();    f1();    f1();    f1();    f1();    f1();
        cout << "out function a = " << n1 << "  b = " << n2 << "  c = " << n3 << endl;
        cout << "out function a = " << &n1 << "  b = " << &n2 << "  c = " << &n3 << endl;
        return 0;
    
    }
    
    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

# std::bind模板和std::function模板配合

通过std::bind和std::function配合使用,所有的可调用对象均有了统一的操作方法

这里的function<int(int)>其中的参数列表只是代表你在调用的时候需要传入的参数,和在后面bind里的参数个数和类型没有关系。

创建:int abc(int a, int b) { return a + b; }

function<int()> f5 = bind(&abc, 2, 1);

调用:f5()

如果函数内没有使用这个参数,即使传入了也没有任何意义

但如果bind里有一个占位符,那就必须有一个传入参数,这里的占位符不能初始化

创建:function<int(int)> f4 = bind([](int a, char b) { return b+a; }, placeholders::_1, 'a');

调用:f4(2)

class Test
{
public:
    int i = 0;

    void func(int x, int y)
    {
        cout << x << " " << y << endl;
    }
};

int main()
{
    Test obj; //创建对象
		//函数对象的调用
    function<void(int, int)> f1 = bind(&Test::func, &obj, _1, _2);
    f1(1, 2);   //输出:1 2

    function< int &()> f2 = bind(&Test::i, &obj);
    f2() = 123;//不懂为什么变量指针能作为函数调用
    cout << obj.i << endl;//结果为 123

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

std::bind模板与引用:

对于如下代码,

void func1(int a, int& b, int& c) {
    a += 1;     //a为值拷贝传递,此时调用结果不保存,不影响外部n2的值
    b += 10;    //b为引用传入,调用结果保存在 function<void()> f1自己的地址中,先当于b为静态变量,成了函数的一个状态
								//但b的地址和外部main函数中n2地址不同,所以不影响外部n2的值
    c += 100;   //c为移动传入,使用的是外部变量n2的地址空间,所以此次修改能保存下来,并影响外部n2的值
}

int main() {
    int n1 = 1;
		int n2 = 10; 
		int n3 = 100;
    function<void()> f1 = bind(func1, n1, n2, ref(n3));
		cout<<n1;//1
		cout<<n2;//10
		cout<<n3;//200
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# lambda表达式

# 定义

格式:[捕获列表](参数列表) mutable exception → 返回类型{函数体;};

[捕获列表]:

标识一个lambda的开始,不能省略

到函数定义为止所在作用范围内可见的局部变量(包括所在类的this)

其中可以有如下形式:

  • 空

  • = 函数体内可以使用到函数定义为止所在作用范围内可见的局部变量(包括所在类的this),值传递方式

    lambda内不能对值传递的参数修改

  • & 函数体内可以使用到函数定义为止所在作用范围内可见的局部变量(包括所在类的this),引用传递方式

  • this 函数体内可以使用lambda所在类中的成员变量。是可修改类型的

  • a 将a按值传递,默认a为不可修改类型(const),如果要修改添加mutable修饰符。

  • &a。将a按引用进行传递。

  • a, &b。将a按值进行传递,b按引用进行传递。

  • =,&a, &b。除a和b按引用进行传递外,其他参数都按值进行传递

  • &, a, b。除a和b按值进行传递外,其他参数都按引用进行传递

(参数列表):

标识重载的()操作符的参数,可以省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。

mutable:可修改标识符

可省略,[捕获列表]的传入按值传递的参数时,加上mutable修饰符后,可修改按值传入来的拷贝

exception:错误抛出标示符

可省略, 用于指定函数抛出的异常,如抛出整数类型的异常,可以使用throw(int)

返回类型:

标识函数返回值的类型,当返回值为void或只有一处return时(编译器可自动推断出返回值类型),可省略

基本例子:

class Test
{
public:
    int i = 0;

    void func(int x, int y)
    {
        auto x1 = []{ return i; };          //err, 没有捕获外部变量
				auto x2 = [=]{ return i+x+y; };     //ok, 值传递方式捕获所有外部变量
				auto x4 = [this]{ return ++i; };      //ok, 捕获this指针,无关引用或值传递全局变量的传入即可修改
				auto x5 = [this]{ return i+x+y; };  //err, 没有捕获x, y
				auto x6 = [this, x, y]{ return i+x+y; };//ok, 捕获this指针, x, y
				auto x7 = [x]{x++; };//err,不可修改
				auto x8 = [x] mutable {x++; };//ok,可修改
				auto x9 = [&x] {x++;}; //ok,可修改
				auto x10 = [this]{ return i++; };        //ok, 捕获this指针, 并修改成员的值
		}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

**注意:**值传递进lambda表达式,一经传入即使外部变量的值改变了,lambda表达式内的变量不会改变

    int j = 13;
    auto val = [=] { return j;};
    auto ref = [&] { return j;};
    j++;
    cout << "val: " << val() << endl;//val: 13    //值传递后,一经传入不会随外界值的改变而改变
    cout << "ref: " << ref() << endl;//ref: 14    //引用传递后,会随外界值的改变而改变
1
2
3
4
5
6

# lambda与仿函数

在使用上是一样的,但lambda更安全和一次性

除去在语法层面上的不同,lambda和仿函数有着相同的内涵——都可以捕获一些变量作为初始化状态,并接受参数进行运行。

而事实上,仿函数是编译器实现lambda的一种方式,通过编译器都是把lambda表达式转化为一个仿函数对象。因此,在C++11中,lambda可以视为仿函数的一种等价形式。

class MyFunctor
{
public:
    MyFunctor(int tmp) : round(tmp) {}
    int operator()(int tmp) { return tmp + round; }

private:
    int round;
};

int main()
{
    //仿函数
    int round = 2;
    MyFunctor f1(round);//调用构造函数
    cout << "result1 = " << f1(1) << endl; //operator()(int tmp)

    //lambda表达式
    auto f2 = [=](int tmp) -> int { return tmp + round; } ;
    cout << "result2 = " << f2(1) << endl;

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# lambda类型的存储和操作

lambda表达式的类型在C++11中被称为“闭包类型”,每一个lambda表达式则会产生一个临时对象(右值)。因此,严格地将,lambda函数并非函数指针。

C++11标准允许没有捕获任何变量的lambda表达式向函数指针的转换

但函数指针无法转换为lambda

使用std::function和std::bind来存储和操作lambda表达式:

int main()
{
    //使用std::function和std::bind来存储和操作lambda表达式
    function<int(int)> f1 = [](int a) { return a; };
    function<int()> f2 = bind([](int a){ return a; }, 123);
		function<int(int)> f3 = bind([](int a) { return a; }, placeholders::_1);

    cout << "f1 = " << f1(123) << endl;
    cout << "f2 = " << f2() << endl;
		cout << "f3 = " << f3(123) << endl;

}
1
2
3
4
5
6
7
8
9
10
11
12

使用自动类型 推导来存储和操作lambda表达式:

auto f3 = [](int x, int y)->int{ return x + y; }; //lambda表达式,没有捕获任何外部变量

decltype(f3) p3 = f3; // 通过decltype获得lambda的类型

使用函数指针

int (*abc)(int a, int b) = [](int x, int y)->int { return x + y; }; //函数指针类型

cout <<abc(1,2) << endl;

# 意义

就地封装短小的功能闭包,lambda函数比较轻便,即用即扔,很适合需要完成某一项只在此一处使用的简单功能

对于如下数组内的元素,只输出偶数,功能的实现:

vector<int> nums{0,1,2,3,4,5,6,7,8,9};

  1. 普通遍历

for (int i : nums) if (i%2 == 0) cout<<i;

  1. 仿函数和算法for_each
class LNums
{
public:
    void operator () (int i) const{//仿函数
        if (i%2 == 0)  cout<<i
    }
}

for_each(nums.begin(), nums.end(), LNums());//第三个参数生成一个LNums类的实例,再传入后变为仿函数,作为谓词
1
2
3
4
5
6
7
8
9
  1. lambda函数和算法for_each

for_each(nums.begin(), nums.end(), [=](){

if (i%2 == 0) cout<<i

})

# 包装器(外覆器)wrapper

  • 模板bind可替代bind1st和bind2nd,且更灵活
  • 模板mem_fn让您能够将成员函数 (opens new window)作为常规函数进行传递
  • 模板reference_wrapper让您能够创建像引用但可被复制的对象
  • 包装器function让您能够以统一的方式处理多种类似于函数的形式

# function

为什么使用function:模版下,函数调用类型太丰富(函数名、函数指针、函数对象或有名称的lambda表达式),对模版实例化时,不同调用类型可能会生成不同的调用对象,从而导致:

  • 编译时效率低:增加了编译时间和生成的代码量。
  • 运行时效率低:不同类型的可调用对象可能有不同的调用约定,这可能导致运行时的额外开销。

使用包装器可以将这些不同类型的可调用对象统一,从而简化模板的实现,提高编译和运行效率。

不同的调用约定会增加开销:

  1. 不同的调用约定可能需要额外的指令、不同的寄存器来传递参数、执行额外的栈操作
  2. 无法应用某些优化技术
  • 不使用包装器的代码

    #include <iostream>
    template <typename T, typename F>
    T use_f(T v, F f)
    {
        static int count = 0;
        count++;
        std::cout << " use_f count = " << count
            << ", &count = " << &count << std::endl;
        return f(v);
    }
    class Fp
    {
    private:
        double z_;
    public:
        Fp(double z = 1.0) : z_(z) {}
        double operator()(double p) { return z_ * p; }
    };
    class Fq
    {
    private:
        double z_;
    public:
        Fq(double z = 1.0) : z_(z) {}
        double operator()(double q) { return z_ + q; }
    };
    
    #include "somedefs.h"
    #include <iostream>
    #include <functional>
    double dub(double x) { return 2.0 * x; }
    double square(double x) { return x * x; }
    int main()
    {
        using namespace std;
        double y = 1.21;
        //此处实例化了五个use_f模板函数,导致编译代码多,效率低
        cout << "普通版本******************************************************" << endl;
        cout << "Function pointer dub:\n";
        cout << " " << use_f(y, dub) << endl;
        cout << "Function pointer square:\n";
        cout << " " << use_f(y, square) << endl;
        cout << "Function object Fp:\n";
        cout << " " << use_f(y, Fp(5.0)) << endl;
        cout << "Function object Fq:\n";
        cout << " " << use_f(y, Fq(5.0)) << endl;
        cout << "Lambda expression 1:\n";
        cout << " " << use_f(y, [](double u) {return u * u; }) << endl;
        cout << "Lambda expression 2:\n";
        cout << " " << use_f(y, [](double u) {return u + u / 2.0; }) << endl;
        cout << "优化版本******************************************************" << endl;
        //考虑到上述函数的参数和返回值(call signature)都为 double(double),
        //因此使用function<double(double)>装饰它创建六个包装器,实现只实例化一个use_f函数模板
        function<double(double)> ef1 = dub;
        function<double(double)> ef2 = square;
        function<double(double)> ef3 = Fq(10.0);
        function<double(double)> ef4 = Fp(10.0);
        function<double(double)> ef5 = [](double u) {return u * u; };
        function<double(double)> ef6 = [](double u) {return u + u / 2.0; };
        cout << "Function pointer dub:\n";
        cout << " " << use_f(y, ef1) << endl;
        cout << "Function pointer square:\n";
        cout << " " << use_f(y, ef2) << endl;
        cout << "Function object Fp:\n";
        cout << " " << use_f(y, ef3) << endl;
        cout << "Function object Fq:\n";
        cout << " " << use_f(y, ef4) << endl;
        cout << "Lambda expression 1:\n";
        cout << " " << use_f(y, ef5) << endl;
        cout << "Lambda expression 2:\n";
        cout << " " << use_f(y, ef6) << endl;
    
        return 0;
    }
    
    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
    68
    69
    70
    71
    72
    73
    74
  • 使用包装器的代码

    #include <iostream>
    #include <functional>
    //使用function模板
    template <typename T>
    T use_f(T v, std::function<T(T)> f)
    {
        static int count = 0;
        count++;
        std::cout << " use_f count = " << count
            << ", &count = " << &count << std::endl;
        return f(v);
    }
    class Fp
    {
    private:
        double z_;
    public:
        Fp(double z = 1.0) : z_(z) {}
        double operator()(double p) { return z_ * p; }
    };
    class Fq
    {
    private:
        double z_;
    public:
        Fq(double z = 1.0) : z_(z) {}
        double operator()(double q) { return z_ + q; }
    };
    
    #include "somedefs.h"
    
    double dub(double x) { return 2.0 * x; }
    double square(double x) { return x * x; }
    int main()
    {
        using namespace std;
        double y = 1.21;
        typedef function<double(double)> fdd; // simplify the type declaration
        cout << "Function pointer dub:\n";
        cout << use_f(y, fdd(dub)) << endl; // create and initialize object to dub
        cout << "Function pointer square:\n";
        cout << use_f(y, fdd(square)) << endl;
        cout << "Function object Fp:\n";
        cout << use_f(y, fdd(Fp(5.0))) << endl;
        cout << "Function object Fq:\n";
        cout << use_f(y, fdd(Fq(5.0))) << endl;
        cout << "Lambda expression 1:\n";
        cout << use_f(y, fdd([](double u) {return u * u; })) << endl;
        cout << "Lambda expression 2:\n";
        cout << use_f(y, fdd([](double u) {return u + u / 2.0; })) << endl;
        return 0;
    }
    
    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

# 智能指针

#include <memory>

智能指针通过使用引用计数来跟踪指向对象的指针数量。当引用计数为零时,智能指针会自动释放对象。这样,智能指针可以避免内存泄漏。

# unique_ptr

初始化:

unique_ptr<int> up1(new int(11)); // 无法复制的unique_ptr

转移所有权:

unique_ptr持有对对象的独有权,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。

//unique_ptr<int> up2 = up1; // err, 不能通过编译
//std::unique_ptr<int> sp2(sp1);// err, 不能通过编译

//以右值的方式可以转移独有权
unique_ptr<int> up3 = move(up1); // 现在p3是数据的唯一的unique_ptr,up1置空

//同理,可以通过函数的返回值来转移
std::unique_ptr<int> func(int val)
{
    std::unique_ptr<int> up(new int(val));
    return up;
}
std::unique_ptr<int> sp1 = func(123);
1
2
3
4
5
6
7
8
9
10
11
12
13

shared_ptr (const shared_ptr<U>& x, element_type* p) //别名构造

**释放空间:**离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。

up3.reset(new int(44)); //"绑定"动态对象

up3.reset(); // 显式释放内存,无返回值

p.reset(q.d); //将p中内置指针换为q,并且用d来释放p之前所指的空间

up3 = nullptr;//同上等价

unique_ptr<int> up4;

up4.reset(); // 对一个没有分配的指针释放空间,不会导致运行时错误

释放控制权:

int *p = up5.release();//不会释放内存,shared_ptr不能用这个

up5.reset();

判断:

up5.unique();

判断是否只有up5指向这个地址,空指针返回false

up5.bool();//等价up5.get()==NULL

返回值是否是空指针

访问:

cout<<*up3;//和一般指针一样的访问

int* p = new int(10);
unique_ptr<int> up1;
up1.reset(p);
up1.reset(new int(30));
cout << *p;//乱码,空间被释放了
1
2
3
4
5

element_type* get() const noexcept;

返回存储的指针地址

int* p = new int (10);
std::shared_ptr<int> a (p); //此时a.get()==p
// 下面三个输出的值都是10
std::cout << *a.get() << "\n";
std::cout << *a << "\n";
std::cout << *p << "\n";
1
2
3
4
5
6

空间处理:

void swap (shared_ptr& x) noexcept;

std::shared_ptr<int> foo (new int(10));
std::shared_ptr<int> bar (new int(20));
foo.swap(bar);
1
2
3

void reset (U* p);

将p所指向的空间计数器-1,重新给p分配空间

shared_ptr<int> sp;
sp.reset(new int);
*sp = 1;
shared_ptr<int> sp2 = sp;
sp.reset(new int);
*sp = 2;//sp为2,sp1为1
1
2
3
4
5
6

# shared_ptr

相当于硬链接

允许多个该智能指针共享“拥有”同一堆分配对象的内存,这通过引用计数(reference counting)实现

打印计数器wp.use_count()

shared_ptr<int> p;
shared_ptr<int> q = p;

1
2
3

没有release()

shared_ptr<int> sp1(new int(22));
shared_ptr<int> sp2 = sp1;

cout << "count: " << sp2.use_count() << endl; //打印引用计数
sp1.reset();    //显式让引用计数减1
1
2
3
4
5
//不能对一个指针多次加入shared_ptr,因为shared_ptr析构时再次释放了已经释放了的空间
int* p = new int(10);
shared_ptr<int> sp1(p);
shared_ptr<int> sp2(p);//程序中断
1
2
3
4
//释放掉加入了共享指针的指针空间会直接报错,也是因为shared_ptr析构时再次释放了已经释放了的空间
int* p = new int(10);
shared_ptr<int> sp1(p);
shared_ptr<int> sp2 = sp1;
delete p;
1
2
3
4
5

总结:普通指针最好不要和智能指针复用

可以自定义删除器

std::shared_ptr<int> p7(new int[10], [](int* p) {delete[]p; });//这里的第二个参数就是释放空间时的规则

# 使用的一些问题

  1. 交叉引用,使用weak_ptr避免
  2. 多线程问题
  3. 左值引用智能指针
void test(const std::shared_ptr<A>& sp1) {
    cout << sp1.use_count() << endl;//输出1,
    //即以左值引用的方式使用智能智能,并不会使得引用计数器+1
    //那么如果外部释放了这个资源,这里将访问释放了的地址
}

int main()
{
    std::shared_ptr<A> sp1(new A());
    test(sp1);
}
1
2
3
4
5
6
7
8
9
10
11
int main()
{    
    std::shared_ptr<T> sp1(new T());
    const auto& sp2 = sp1;//左值引用,并不会增加计数器
    sp1.reset();//此时对象以及释放掉
    
    //由于sp2已经不再持有对象的引用,程序会在这里出现意外的行为
    sp2->doSomething();

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11

# weak_ptr

相当于软链接

它可以从一个shared_ptr或另一个weak_ptr对象构造(只能对shared_ptr的数据结构操作),它的构造和析构不会引起引用计数的增加或减少。

打印计数器wp.use_count()

wp.reset()//参数只能为空,即不能重新分配内存,它的释放空间只影响自己不能访问这个空间

没有release()

访问:它可以指向shared_ptr指针指向的对象内存,却并不拥有该内存,而使用weak_ptr成员lock,则可返回其指向内存的一个share_ptr对象,且在所指对象内存已经无效时,返回指针空值nullptr。

*wp.lock();原子的返回这个地址的shared_ptr指针

wp.expired() 返回这个指针是否释放了

  • 对于weak_ptr的使用
//error
if(!wp.expired())wp->setValue(1);
//true
auto s = wp.lock();
if(s)s->setValue(1);
1
2
3
4
5
  • 循环引用是指两个或多个对象之间互相持有对方的引用,导致对象无法被正确地释放而造成内存泄漏。

weak_ptr可以避免循环引用的情况

//一个循环引用的例子
class A;//前置声明
class B {
public:
    std::shared_ptr<A> aPtr;
};
class A {
public:
    std::shared_ptr<B> bPtr;
};

std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a.bptr = b;
b.aptr = a;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# make_shared

创建返回一个shared_ptr对象

template <class T, class... Args> shared_ptr<T> make_shared(Args&&... args);

args为多个构造函数参数

在语义上和shared_ptr相同

优点:

  1. 减少内存分配次数,提高性能:
shared_ptr<widget> sp(new widget);//分配2次内存,
//先分配一次内存给widget,再分配一次内存给智能指针对象(往里面填入widget对象的指针)
shared_ptr<widget> sp = make_shared<widget>();//分配一次内存
1
2
3
  1. 潜在的资源泄露

因为内存编排不确定代码的执行顺序,对于如下语句

shared_ptr<widget> sp(new widget);
computerPriority();//如果这个函数会发生崩溃
1
2

如果实际的执行顺序为:

  1. new widget
  2. computerPriority()
  3. 执行shared_ptr的构造函数

那么computerPriority() 函数异常中止时,第三步不会执行,则已经分配的new对象无法析构掉

而使用make_shared则不会出现这样的问题

缺点:

  1. 不能用{}的方式初始化make函数的对象
  2. 不能指定删除器

如果想指定删除器,又想避免内存泄露,则

shared_ptr<widget> sp(new widget,[](){
	//删除器的代码
});
computerPriority(move(sp));//即使函数内不使用这个值,目的是为了强行让编译器的代码执行顺序确定下来
1
2
3
4

# make_unique

//shared_ptr分配成功时,Example分配失败,则发生了内存泄漏
auto sp = std::shared_ptr<Example>(new Example(argument));

//安全,但是效率更低,做了两次分配
auto song = new Song(L"Ode to Joy", L"Beethoven");
std::shared_ptr<Song> sp0(song);

//更安全和更有效率
auto msp = std::make_shared<Example>(argument);
1
2
3
4
5
6
7
8
9

创建unique_ptr并将其返回到指定类型的对象,该对象通过指定的参数进行构造

// make_unique<T>
template <class T, class... Args>
unique_ptr<T> make_unique(Args&&... args);//用于单个对象的构造

// make_unique<T[]>
template <class T>
unique_ptr<T> make_unique(size_t size);//用于数组的构造

// make_unique<T[N]> disallowed
template <class T, class... Args>
/* unspecified */ make_unique(Args&&...) = delete;  //不如vector
1
2
3
4
5
6
7
8
9
10
11

例子

unique_ptr<Animal> p1 = make_unique<Animal>();
unique_ptr<Animal[]> p3 = make_unique<Animal[]>(5);//数组的构造
1
2

# 智能指针的自定义释放函数

//智能指针模板只有两个参数,第二个参数为删除器的函数指针类型
std::unique_ptr<Socket, void(*)(Socket * pSocket)> spSocket(new Socket(), [](Socket* pSocket) {
        //关闭句柄
        pSocket->close();
        //TODO: 你甚至可以在这里打印一行日志...
        delete pSocket;
    });
1
2
3
4
5
6
7

# shared_from_this和对象保活

当一个类想返回包含自己的一个shared_ptr对象时

enable_shared_from_this<T> 是一个模板类,使用时使目标类继承这个类

class A : public std::enable_shared_from_this<A>//使对象类继承enable_shared_from_this
{
public:
    A() {}
    ~A(){}

    std::shared_ptr<A> getSelf()
    {
        //return make_shared<A>(*this);//使用这种方式,实际上返回的是一个独立的shared_ptr对象
        //这会使得有两个shared_ptr对象都以为自己独占这个地址,从而导致两次释放同一个地址空间
        return shared_from_this();//更安全
    }
};
int main()
{
    std::shared_ptr<A> sp1(new A());
    std::shared_ptr<A> sp2 = sp1->getSelf();
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

使用场景:

在回调函数中拿到传递的参数时,通过对象的getSelf() 拿到一个安全的智能指针,并使用

注意一些问题:

  1. 使用getSelf()的对象不要是栈空间释放的,不然会出现多次释放同一空间
int main()
{
    A a;//作用域结束后释放空间
    std::shared_ptr<A> sp2 = a.getSelf();//作用域结束后也释放空间
    return 0;
}
1
2
3
4
5
6
  1. 避免循环引用
class A : public std::enable_shared_from_this<A>
{
public:
    A(){}
    ~A(){}
    void func(){
        m_SelfPtr = shared_from_this();
    }

public:
    std::shared_ptr<A>  m_SelfPtr;//对象自己管理自己,引用计数器永远不能为0
};

int main()
{

  std::shared_ptr<A> spa(new A());
  spa->func();//对象自己管理自己,循环引用
  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 智能指针对象大小

unique_ptr类型大小等同于指针大小

shared_ptr类型大小=指针大小*2

# 模板的优化

# 编译器对模板的右尖括号做单独处理

问题:

连续两个右尖括号(>>)会被编译解释成右移操作符,而不是模板参数表的形式

C++98标准是让程序员在>>之间填上一个空格

template <int i> class X{};
template <class T> class Y{};

int main()
{
    Y<X<1> > x1;    // ok, 编译成功
    Y<X<2>> x2;     // err, 编译失败

    return 0;
};
1
2
3
4
5
6
7
8
9
10

在C++11标准中,要求编译器对模板的右尖括号做单独处理,使编译器能够正确判断出">>"是一个右移操作符还是模板参数表的结束标记。

# 模板的特化和偏特化

模板的实例化(instantiation)指函数模板(类模板)生成模板函数(模板类)的过程

模板参数在某种特定类型下的具体实现称为模板的特化

偏特化是指将部分参数类型指定为具体类型

**SFINAE:**当将模板形参替换为显式指定的类型或推导的类型失败时,从重载集中丢弃这个特化,而非导致编译失败

# 模板的别名

#include <type_traits> //std::is_same

对类型起别名:

using uint = unsigned int; typedef unsigned int UINT;

判断类型是否一致

is_same<uint, UINT>::value//一致返回1

# 函数模板的默认模板参数

1、普通函数带默认参数,c++98 编译通过,c++11 编译通过 void DefParm(int m = 3) {}

2、类模板是支持默认的模板参数,c++98 编译通过,c++11 编译通过 template <typename T = int> class DefClass {};

3、函数模板的默认模板参数, c++98 - 编译失败,c++11 - 编译通过 template <typename T = int> void DefTempParm() {}

类模板的默认模板参数必须从右往左定义,函数模板的默认模板参数则没这个限定:

template<class T, int i = 0> class DefClass3;
template<int i = 0, class T> class DefClass4;         // 无法通过编译

template<class T1 = int, class T2> void DefFunc1(T1 a, T2 b);
template<int i = 0, class T> void DefFunc2(T a);
1
2
3
4
5
上次更新: 2025/02/21, 14:57:10
C++17

C++17→

最近更新
01
搭建ai知识助手
02-23
02
边缘检测
02-15
03
css
02-15
更多文章>
Theme by Vdoing | Copyright © 2025-2025 松垮垮 | MIT License | 蜀ICP备2025120453号 | 川公网安备51011202000997号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 纯净模式