红魔咖啡馆

头发越掉越多,头发越掉越少

0%

右值引用

左值与右值

左值:能用在赋值运算符左侧的表达式

右值:能用在赋值运算符右侧,但不能用在左侧的表达式

1
2
int a = 0;
a = 5;

这里a为左值,5为右值,不能写成5=a

判断方法:

左值:能够获得某个表达式的引用或地址即为左值

1
2
3
4
5
6
7
const int a = 5;
const int& b = a;

int c;
int* p = &c;

int b = &4
  • b取到了a的引用,a是(不可修改的)左值
  • p取到了c的地址,c是左值
  • 4不能取地址,4是右值

C++11后的分类

  • glvalue:泛左值
  • prvalue:纯右值
  • xvalue:将亡值

举例

  • 左值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 整型/浮点型等变量
int a = 8;
double m = 1.5;
int*p = new int{5}; // 指针
*p = 4; // 表达式解引用
int b[5]; 
b[1] = 10; // 数组(不可更改)、数组元素

int& r = a; // 左值引用中右侧的表达式

// 类的数据成员
struct s{
    int id;
}s1;
s1.id = 3; 

// 返回引用的函数的调用表达式
int& refn(){
    static int n = 1;
    return n;
}
refn()=5;
  • 纯右值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int x,y,z;
int m = 3, n = 1;
double d;

x = 2; // 字面值
// 将计算结果存放在临时中间对象中的表达式
x = m+n;
y = -m;
z = n+2;

d = double(z) // 类型转换
    
struct s{};
s obj;
obj = s{}; // 未命名类的对象

s func(){
    return s{};
}
func(); // 函数调用返回对象值时的调用
s (*f)(int) = &func; // 函数地址

// 非静态类成员函数、枚举、数组、this指针、lambda表达式、一些内置运算符表达式

左右值引用

  • 左值引用:普通的引用
1
2
int a = 5;
int& b = a;
  • 右值引用:使用两个&进行引用
1
int &&c = 5;

只能绑定右值,不能绑定左值

应用

函数重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;
void func(int& a){
  cout <<"调用左值引用重载"<<endl;
}
void func(int&& a){
  cout <<"调用右值引用重载"<<endl;
}

int main(){
  int a = 5;
  func(a);
  func(5);
}
1
2
调用左值引用重载
调用右值引用重载

移动语义

移动构造函数

当类中存在指针等变量,而没有设置拷贝构造,编译器会自动生成拷贝构造函数,此时为浅拷贝。我们应该自己设计深拷贝,申请新的内存,并将原来的内容复制到新分配的内存中,并实现赋值运算符的重载函数,释放原有内存,申请新的内存,并复制值

此时参数中为左值引用,称为拷贝赋值运算符重载函数

为了实现移动语义,我们需要实现移动赋值运算符重载函数

此时参数为右值引用,且没有const限定符

该方法少了内存的分配和数据的复制,修改了被移动对象

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 <iostream>
using namespace std;
class CharBuffer{
public:
// 左值函数
  CharBuffer(int size):m_buff(new char[size]), m_size(size){
    cout <<"普通构造函数"<<endl;
  }
  CharBuffer(const CharBuffer& other):m_size(other.m_size), m_buff(new char[m_size]){
    memcpy(m_buff, other.m_buff, m_size);
    cout<<"拷贝构造函数"<<endl;
  }
  CharBuffer& operator=(const CharBuffer& other){
    if (this==&other){
      return *this;
    }
    m_size = other.m_size;
    delete[] m_buff;
    m_buff = new char[m_size];
    memcpy(m_buff, other.m_buff, m_size);
    cout <<"拷贝赋值运算符"<<endl;
  }
// 右值函数
  CharBuffer(CharBuffer&& other):m_size(other.m_size), m_buff(other.m_buff){
    other.m_buff = nullptr;
    other.m_size = 0;
    cout <<"移动构造函数"<<endl;
  }
  CharBuffer& operator=(CharBuffer&& other){
    if (this==&other){
      return *this;
    }
    delete[] m_buff;
    m_size = other.m_size;
    m_buff = other.m_buff;

    other.m_size= 0;
    other.m_buff = nullptr;
    cout <<"移动赋值运算符"<<endl;
  }
  int m_size;
  char* m_buff;

  ~CharBuffer(){
    delete[] m_buff;
    cout <<"析构函数"<<endl;
  }
};

int main(){
  CharBuffer buff2{CharBuffer(100)};
}

拷贝省略

当我们运行语句,构造一个未命名的对象作为右值

CharBuffer buff2{CharBuffer(100)};

按照移动语义,会先通过普通构造函数创建临时对象,然后将该临时对象赋值给buff2,但编译器会对这种代码进行优化,省略调临时对象的创建,直接在目标存储位置构造该对象即输出:

1
2
普通构造函数
析构函数

C++17后,这种优化成为语言规范

返回值优化(RVO)

类似,函数直接返回值的情况下,临时对象的创建也会省略

1
2
3
4
5
6
7
CharBuffer generate(int n){
  return CharBuffer(n);
}

int main(){
  CharBuffer buff2 = generate(100);
}
1
2
普通构造函数
析构函数

具名的返回值优化(NRVO)

1
2
3
4
5
6
7
8
9
10
CharBuffer generate_nv(int n){
  CharBuffer buf(n);
  cout <<&buf<<endl;
  return buf;
}

int main(){
  CharBuffer buff2 = generate_nv(100);
  cout <<&buff2<<endl;
}
1
2
3
4
普通构造函数
0x61fe30
0x61fe30
析构函数

buff2与buff的地址相同,说明他们是对同一个对象的引用,原理同上

std::move()

该函数将传入的参数转换为右值引用并返回

1
2
3
4
int main(){
  CharBuffer buff1(100);
  CharBuffer buff2(move(buff1));
}
1
2
3
4
普通构造函数
移动构造函数
析构函数
析构函数

此时由于传入的转换为了右值引用,故正确调用了移动构造函数

等同于static_cast<CharBuffer&&>(buff1)

xvalue

1
2
3
4
5
6
7
int main(){
  CharBuffer buff2(10);
  {
    CharBuffer buff1(100);
    buff2 = move(buff1);
  }
}

这段代码中,buff1就是一个将亡值,我们使用move函数获得其右值引用,从而将它的资源回收再利用,转移buff2中

常见xvalue

  • 函数返回的右值引用
  • static_cast<T&&>(v)
  • 未命名的右值对象获得的非静态成员变量

移动语义的应用

大部分情况下都不需要移动语义

  • 若一个类涉及到深拷贝,需要复制较多数据,可以使用移动语义
  • 当转移unique_ptr的所有权时,需要移动语义
  • 编写供其他程序使用的STL,需要提供移动语义的支持

智能指针

普通指针出现的错误

  • 内存泄漏:一个指针指向一块动态分配的内存,当该指针离开了所在的作用域,且程序结束后不进行释放,则会导致内存泄漏
  • 悬空指针:指针指向了已经被释放的区域
  • 野指针:指针未初始化使其指向有效内存区域

智能指针

智能指针是一个对象,封装了多态对象指针,可以有效避免上述情况

智能指针会自动申请内存,并在使用结束后释放内存

C++中存在封装了三种智能指针

  • unique_ptr
  • shared_ptr
  • weak+ptr

unique_ptr

unique_ptr与管理对象是一对一的关系

创建

unique_ptr(A) ptr1(new A(参数))

unique_ptr(A) ptr1 =make_unique<A>(参数)(该方法较为安全)

其中A为类名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<memory>
#include<iostream>
using namespace std;
class rectangle{
public:
  rectangle(double w, double h): width(w), height(h){}
  ~rectangle(){
    cout <<"对象被释放"<<endl;
  }
  double area(){
    return width*height;
  }
private:
  double width;
  double height;
};

int main(){
  unique_ptr<rectangle> p(new rectangle(3.5, 4.1));
  cout << p->area()<<endl;
}
1
2
14.35
对象被释放

这里智能指针可以像普通指针一样使用,直接调用成员

离开作用域后,会自动销毁,调用析构函数释放指向的对象

注意:unique_ptr不能使用拷贝构造,也不能被赋值

成员函数

  • T* get();:获得所管理对象的指针
  • T* operator->();:重载间接成员运算符,调用get()函数,返回所管理对象指针
  • T& operator*();:重载解引用运算符,返回所管理对象的引用
  • T* release();:解除对封装对象的管理,返回对象指针
  • void reset(T* newObj):删除原有对象,接管新对象
  • void swap(unique_ptr<T>& other):与其他unique_ptr对象交换所管理的对象
  • move():转移指针所管理对象的所有权(转移后先前的指针就是野指针了)

优点

安全,且开销(时间与空间)与普通指针差距不大,成员函数带来的额外开销也不大,符合RAII

应用

适用于使用普通指针的地方,如容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Packet{
    Packet(long id):m_id(id){};
    long m_id;
    char Data[1000];
};
struct Compare{
	template<template<typename> typename SmartPtr>
    bool operator()(const SmartPtr<Packet>& pA, const SmartPtr<Packet>& pB){
    return pA->m_id<pB->m_id;
    }
};
template<typename SmartPtr>
void sortSmartPtrVector(int n){
    vector<SmartPtr> vecPacket;
    for(int i=0;i<n; i++){
    vecPacket.push_back(SmartPtr(new Packet(rand()%n)));
    }
    sort(vecPacket.begin(), vecPacket.end(),Compare());

}

shared_ptr

多个shared_ptr通过一个共同的引用计数器来管理同一个对象

创建

shared_ptr(A) ptr1(new A(参数))

shared_ptr(A) ptr1 =make_shared<A>(参数)(该方法较为安全)

其中A为类名

控制块

shared_ptr需要分配另一块内存用来存储引用计数

当引用计数为0,即所有指向当前引用的指针都消失后,当前类才会被删除

成员函数

shared_ptr没有release方法

  • long use_count():获取有多少指针管理同一个对象
  • bool unique():返回use_count()是否为1

类型转换

  • dynamic_pointer_cast<>()
  • static_pointer_cast<>()
  • const_pointer_cast<>()

循环引用

使用shared_ptr时可能会出现循环引用的情况,如在使用容器时

此时的引用计数会变为2,但当某个指针离开时只会见一,导致无法自动删除对象导致内存泄漏

我们可以使用weak_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
#include <iostream>
#include <memory>
using namespace std;
class c{
public:
  c(){
    cout <<"构造函数被调用"<<endl;
  }
  ~c(){
    cout <<"析构函数被调用"<<endl;
  }
};

int main(){
  {
    shared_ptr<c> spo;
    {
      cout<<"进入作用域"<<endl;
      shared_ptr<c> spi = make_shared<c>();
      spo = spi;
    }
    cout<<"离开作用域"<<endl;
  }
}

weak_ptr

weak_ptr需要结合shared_ptr来使用

可以将一个shared_ptr对象作为构造函数参数初始化,或直接赋值

weak_ptr只对shared_ptr管理的对象进行观测,不改变对象的引用计数

可以通过对weak_ptr使用lock()函数,来获得一个shared_ptr以获得封装对象的控制权

若该shared_ptr非空,说明可用,且增加一次引用计数,在离开作用域前可以保证所管理对象是有效的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<memory>
#include<iostream>
using namespace std;
class rectangle{};
int main(){
  weak_ptr<rectangle> wp1;
  {
    shared_ptr<rectangle> sp1(new rectangle());
    shared_ptr<rectangle> sp2 = sp1;
    wp1 = sp2;
    shared_ptr<rectangle> sp3 = wp1.lock();
    cout <<"作用域内sp3="<<sp3<<endl;
  }
  shared_ptr<rectangle> sp3 = wp1.lock();
  cout <<"作用域外sp3="<<sp3<<endl;

}

离开内部作用域后,对象的引用计数变为0,此时被析构,故wp1指向了空位置,可以判断对象有效性

使用expired()方法可以询问一个weak_ptr是否有效

类型转换

隐式转换

要求:相互转换的类型兼容

当一个值被赋值到另一个兼容类型时,会自动发生隐式转换

但这样会造成精度损失

类对象之间也可以发生隐式转换

1
2
SubClass* pSub = new SubClass();
BaseClass* pBase = pSub;

如上是将子类指针转换为了基类指针,这是常用的类指针间的隐式转换

若上方SubClassBaseClass之间没有继承关系,他们的指针是无法进行隐式转换的

除非重载运算符或构造函数

1
2
3
4
class classA{};
class classB{
    public: ClassB (const ClassA& a){}
};

这种方法类似于拷贝构造,但参数类型不同,我们称这种为转换构造函数

显式转换

使用变量类型运算符

1
2
int a = 2;
float b = (float)a // 或float(a)

以上将整型a转化为了浮点数

1
2
SubClass obj;
BaseClass* pBase = (BaseClass*) &obj;

以上将子类对象obj的地址转换成了基类的指针,但若两者没有继承关系,则转换时可能会访问到奇怪的位置

动态转换

语法:dynamic_cast <转换类型> (表达式)

只能用于对象的指针和引用的转换

用于多态类的向下转换(向上转换一定可以成功,故可以直接使用普通类型转换)

dynamic_cast进行两个阶段的检查:

  • 编译阶段:检查基类是否是多态类,不是会报错
  • 运行阶段:检查转换对象是派生类对象,不是会返回空指针

动态转换需要RTTI

1
2
3
4
5
6
7
class CBase{
};
class CDerive: public CBase{};

CBase obj;
// 基类不是多态类,无法转换
CDerive *ptr = dynamic_cast<CDerived*>(&obj);
1
2
3
4
5
6
7
8
9
class CBase{
public:
    virtual ~CBase(){}
};
class CDerive: public CBase{};

CBase obj;
// 不是派生类对象,转换失败
CDerive *ptr = dynamic_cast<CDerived*>(&obj);

静态转换

语法:static_cast <转换类型> (表达式)

可用于基类到派生类的向下转换与派生类到基类的向上转换

static_cast只进行编译阶段的检查:检查两个类是否兼容(不要求基类是多态类)

静态转换可以替代隐式转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CBase{};
class CDerive: public CBase{};

class COther{};

CBase *p;
CDerive *p1;
COther other;
CDerive obj;
// 
p = static_cast<CBase*>(&obj);
p1 = static_cast<CDerive*>(&obj);

// 会提示类型转换无效
p = static_cast<CBase*>(&other);
p1 = static_cast<CDerive*>(&other);

重解释转换

语法:reinterpret_cast <转换类型> (表达式)

用于类指针之间的转换,而不会检查类型是否有效

也可用于整型与指针间的转换(注意整型的数据范围能容纳指针数值)

应用:

  • 与内存和硬件直接交互的底层与接口程序
  • 与操作系统组件交互
  • 处理网络或多媒体数据
1
2
3
4
5
6
7
8
9
10
class CBase{};
class CDerive: public CBase{};

class COther{};

CBase *p;
CDerive *p1;
COther other;
p1 = reinterpret_cast<CDerive*>(0xEFL);
long long address = reinterpret_cast<long long>(&other);

常量转换

语法:const_cast &lt;转换类型&gt; (表达式)

用于常量与非常量之间的转换,转换类型必须是指针或引用

应用:

  • 在调用不修改对象但未声明常量参数的函数时,暂时取消对象的常量类型
  • 临时取消对象中的const属性,以便调用成员函数修改其中成员
1
2
3
4
5
int hello(char* str){
    cout <<str<<endl;
}
const char* str = "world!";
hello(const_cast<char*>(str));

注意,进行转换不意味着可以进行修改

类型特性

<type_traits>库用于判断某个类型是否具有某些特征、改变、比较类型间的特征

常用于函数模板类

结合if constexpr(常量表达式)可以实现条件编译(C++=17)

if constexpr是编译时的条件语句,当表达式条件为真时,该分支代码才会编译,否则舍弃

1
2
3
4
5
6
7
8
9
10
template<typename T>
double length(T& t){
    if constexpr (std::is_arithmetic_v<T>){
        if (t<0) return -t;
        return t;
    }
    else if constexpr (std::is_base_of_v<IMeasurable, T>){
        return t.length();
    }
}

结合static_assert

static_assert用于判断编译阶段条件是否为真,若为假则会终止编译,并产生静态断言错误

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
double length(T& t){
    static_assert(std::is_arithmetic_v<T>||Std::is_base_of_v<IMeasurable, T>, "使用了不支持计算长度的类型")
    if constexpr (std::is_arithmetic_v<T>){
        if (t<0) return -t;
        return t;
    }
    else if constexpr (std::is_base_of_v<IMeasurable, T>){
        return t.length();
    }
}

常量表达式

常量表达式指由字面量、常量、函数、运算符组成的表达式,值是一个固定值

编译器在编译时就会将它用结果值替换,而不需要运行时再去计算

constexpr

使用constexpr修饰函数返回值、变量等,编译器可以在编译阶段计算其值,以便用于数组、模板非类型参数、枚举等

也可以提高程序运行效率

constexpr修饰变量

constexpr int a{10};

这里变量必须初始化,初始值必须是一个常量表达式

constexpr修饰函数

这种函数被称为常量表达式函数

若所有实参都是常量表达式,则该调用也是常量表达式

1
2
3
4
5
6
7
constexpr int mul(int a,int b){
    return a*b;
}

constexpr int c = mul(5,3); //常
int d = mul(c,11); // 常
int f = mul(d, 7); // 变

要求:

  • 返回值和传参都是字面类型
  • 函数中不能调用非constexpr函数(<iostream>库, <thread>库都不可以用)
  • 不能抛出异常
  • 对于递归,编译器有递归深度的限制,用参数-fconstexpr-depth控制,默认512

constexpr修饰自定义类型

若想定义自己的constexpr类,可以将类的构造函数定义为常量表达式函数

1
2
3
4
5
6
7
8
9
10
11
class complex{
public:
    constexpr complex(double r = 0, double i = 0):_r(r),_i(i){}
    constexpr double getreal() const{return _r;}
    constexpr double getimg() const{return _i;}
    constexpr void setreal(double v){_r = v;}
    constexpr void setimg(double v){_i=v;}
private:
    double _r;
    double _i;
}

const与constexpr

const与constexpr的效果相同,整型下可以混用

如这是合法的

1
2
const int a = 12;
constexpr int b = a+3;

这是非法的,需要统一使用constexpr

1
2
const float a = 12;
constexpr float b = a+3;

因为const包含运行时的只读变量(多数情况)或编译时的常量(少数情况)

而constexpr指的是编译时的常量

decltype

用法

该运算符可以获得一个变量或表达式的值的类型

语法:decltype(变量\表达式)

用于变量时,可以获得变量类型、引用类型、cv限定符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int a = 3;
decltype(a) b = 5; // b的类型是int

const int a = 3;
decltype(a) b = 5; // b的类型是const int

int a = 3;
int& b = a;
decltype(b) c = a; // c的类型是int&

struct s{
    double a
};

decltype(s::a) a = 0.1; // a的类型是double
s t;
decltype(t.a) a = 0.1; // a的类型是double

用于表达式时:

1
2
3
4
5
6
7
8
9
int a;
decltype((a)) b = a; // b的类型是int&

string f1(){} // 返回string,是右值
string& f2(){} // 返回string&,是左值
string&& f3(){} // 返回string&&,是将亡值
decltype(f1()) s1; // b的类型是string
decltype(f2()) s2 = s1; // b的类型是string&
decltype(f3()) s3 = "hello"; // b的类型是string&&

即推导函数正好是函数的返回值类型

注意:decltype中的表达式仅用于编译时的类型推导,不会真正执行

decltype(auto)

decltype(auto) 变量 = 初始值表达式替换后等同于

decltype(初始值表达式) 变量 = 初始值表达式

此时表达式是可以执行的

value_category

该类模板可以推导出所给的值属于左值、纯右值还是将亡值

经典用法:将表达式的类别表达出来

1
2
#define VALUE_CATEGORY(expr) value_category<decltype((expr))>::value
#define CAT(a) std::cout <<"Category of "<< #a <<":"<<VALUE_CATEGORY(a)<<std::endl;

应用

常用于定义函数模板时,函数返回值的类型推到

1
2
3
4
5
6
template <typename T, typename U>
decltype(auto) add(T&& t, U&& u){
    return std::forward<T>(t)+std::forward<U>(u);
};

auto s = add(string("a"), string("b") )

Algorithm库

copy_if()

语法:copy_if(first, last, out, pred)

作用:按传入的对象pred条件复制容器内容

如实现查找string数组中指定单词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<typename T, typename P>
std::vector<T> Filter(const std::vector<T>& vec, P pred){
    std::vector<T> res;
    std::copy_if(vec.begin(),vec.end(),std::back_inserter(res),pred);
    return res;
}

int main(void){
    std::string word="apple";
    auto hasWord = [&word](const std::string& s) {return s. find(word) != s.npos;};
    std::vector<std::string> words{"An apple","big apple","orange","green apple","grape"};
    std::vector<std::string> res = Filter(words, hasWord);

    for(auto s:res)
    std::cout << s << " " << std::endl;
 
}

C++20中,在range库中进行了拓展

1
2
3
4
5
6
7
8
9
10
11
int main(void){
    std::string word="apple";
    auto hasWord = [&word](const std::string& s) {return s. find(word) != s.npos;};
    std::vector<std::string> words{"An apple","big apple","orange","green apple","grape"};
    std::vector<std::string> res ;
    std::ranges::copy_if(words, std::back_inserter(res),hasWord);

    for(auto s:res)
    std::cout << s << " " << std::endl;
 
}

partition()

语法:copy_if(first, last, pred)

作用:将一个序列按条件分割

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(void)
{

    std::vector<int> numbers{1,23,4,5,67,8,67,23,20,11,32,33,45,17};
    auto bound1 = std::partition(numbers.begin(),numbers.end(), [](int v){return v%3 == 0;});
    auto bound2 = std::partition(bound1, numbers. end(), [](int v){return v%3 == 1;});
    std::cout << "\n{x=3k}:\n";
    std::for_each(numbers.begin(),bound1, [](int v){std::cout << v << "";});
    std::cout << "\n{x=3k+1}:\n";
    std::for_each(bound1,bound2, [](int v){std::cout << v << "";});
    std::cout << "\n{x=3k+2}:\n";
    std::for_each(bound2,numbers.end(), [](int v){std::cout << v << "";});
}

序列运算

  • set_difference():将不是两个序列共有的元素拷贝到单独的元素中(差集)
  • set_symmetric_difference():上述加了绝对值(对称差集)
  • set_intersection()
  • set_union()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(void){

    std::vector<int> numbers1{1,1,2,3,4,4,5,6};
    std::vector<int> numbers2{9,8,7,6,5,5,4};
    std::vector<int> result;

    std::sort(numbers1.begin(),numbers1.end());
    std::sort(numbers2.begin(),numbers2.end());

    std::set_symmetric_difference(numbers1.begin(),numbers1.end(),
    numbers2.begin(),numbers2.end(),
    std::back_insert_iterator(result));

    for(auto v:result)
        std::cout << v << " ";	
   
}

序列操作

  • for_each()
  • transform()
  • remove_if()
  • reduce()
  • inner_product()
  • transform_reduce()
  • transform_exclusive_scan()