红魔咖啡馆

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

0%

文件操作

C++中提供了文件流来对文件进行操作

需要引入头文件fstream

文件类型:

  • 文本文件:以ASCII码存储
  • 二进制文件:以二进制形式存储

操作文件三大类:

  • ofstream:写
  • ifstream:读
  • fstream:读/写

文本文件

写文件

1
2
3
4
5
6
7
8
#include <fstream>
using namespace std;
int main(){
    ofstream ofs; // 创建流对象
    ofs.open("路径", <打开方式>) // 指定打开方式
    ofs << "写入的数据"; // 写内容
    ofs.close(); // 关闭文件
}

其中打开方式有:

打开方式 解释
ios::in 读文件而打开文件
ios::out 写文件而打开文件
ios::ate 文件打开时指针位于文件末尾,可以移动指针进行读写
ios::app 所有读写操作都会添加到文件末尾
ios::trunc 若文件存在则先删除再创建
ios::binary 二进制方式操作

可以使用|操作符来配合使用多个文件操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
#include<fstream>
using namespace std;
int main(){
  // 创建流对象
  ofstream ofs;
  // 指定打开方式
  ofs.open("test.txt", ios::out);
  // 写内容
  ofs << "name: Li Hua"<<endl;
  ofs << "age: male"<<endl;
  // 关闭文件
  ofs.close();
}

读文件

1
2
3
4
5
6
7
8
#include <fstream>
using namespace std;
int main(){
    ifstream ofs; // 创建流对象
    ofs.open("路径", <打开方式>) // 打开文件并判断是否成功
    ofs << "写入的数据"; // 读取数据
    ofs.close(); // 关闭文件
}

其中读取数据有多种方式:

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
#include<iostream>
#include<fstream>
#include <string>
using namespace std;
int main(){
  // 创建流对象
  ifstream ifs;
  // 打开文件
  ifs.open("test.txt", ios::in);
  // 判断打开是否成功
  if(!ifs.is_open()){
    cout <<"Open failed";
    return 0;
  }
  // 读取数据
  // 方式1: 使用字符数组,但遇到空格也会停下
  char buf[1024]={};
  while(ifs>>buf){
    cout << buf << endl;
  }

  // 方式2: 使用getline,只有遇到回车才会停下
  char buf2[1024]={};
  while(ifs.getline(buf2, sizeof buf2)){
    cout << buf << endl;
  }

  // 方式3:使用string
  string bufs;
  while(getline(ifs, bufs)){
    cout << buf << endl;
  }

  // 方式4:使用char,一个个读取
  char c;
  while((c=ifs.get())!=EOF){
    cout << c;
  }
  ifs.close();

}

二进制文件

二进制打开方式指定为ios::binary

写文件

注意:写入二进制文件时,不建议使用string,会引发指针异常

使用write方式来写入文件,而不是用流式输入,对一个对象我们使用const char*这样一个常量指针类型取得对象实例地址后进行写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
#include<fstream>
using namespace std;
class person{
public:
  char name[64];
  int age;
};
int main(){
  ofstream ofs;
  ofs.open("person.txt", ios::out|ios::binary);
  person p = {"Jack", 18};
  ofs.write((const char*)&p, sizeof(person));
  ofs.close();
}

读文件

使用read方式来读文件

参数原型:istream& read(char *buffer, int len);

即字符指针buffer指向内存中一段存储空间,len为读写的字节数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<iostream>
#include<fstream>
using namespace std;
class person{
public:
  char name[64];
  int age;
};

int main(){
  ifstream ifs;
  ifs.open("person.txt", ios::in|ios::binary);
  if(!ifs.is_open()){
    cout <<"Open failed";
    return 0;
  }
  person p;
  ifs.read((char *)&p, sizeof(person));
  cout << "name: "<<p.name<<endl;
  cout << "age: "<<p.age<<endl;

  ifs.close();
}

协程

协程函数

若一个函数中出现了以下三个关键字之一,则编译器就知道这是一个协程函数

  • co_await
  • co_yield
  • co_return

并且,协程函数必须返回一个符合要求的协程对象,若有返回值,需要通过协程对象获得

协程对象

协程函数的返回值是自定义的一个协程对象

模板

通常需要内嵌一个名为promise_type的结构定义,内部需要实现一些函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>
#include<coroutine>
struct MyCoroutine{
  struct promise_type {
    MyCoroutine get_return_object() {
      return MyCoroutine();
    }
    std::suspend_never initial_suspend() { return {}; }
    std::suspend_never final_suspend() noexcept { return {}; }
    void return_void() {}
    void unhandled_exception() {}
  };
};
MyCoroutine coroFunc(){
  std::cout <<"hello world" << std::endl;
  co_return;
}
int main(){
  MyCoroutine coro = coroFunc();
  return 0;
}

此时coroFunc刚开始执行就被挂起,MyCoroutine对象也没有实现任何协程,故后面的代码不会执行,稍微进行修改

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
#include<iostream>
#include<coroutine>
struct MyCoroutine{
  struct promise_type {
    MyCoroutine get_return_object() {
      // 获得协程句柄
      return MyCoroutine(std::coroutine_handle<promise_type>::from_promise(*this));
    }
    std::suspend_always initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void return_void() {}
    void unhandled_exception() {}
  };
  // 通过构造函数传给新创建的对象
  // 调用了协程句柄的resume函数
  MyCoroutine(std::coroutine_handle<promise_type> h) : handle(h) {}
  void resume() {
    if (handle) {
      handle.resume(); // 恢复协程执行
    }
  }
private:
  std::coroutine_handle<promise_type> handle; // 封装了一个指向协程帧的指针,并提供函数
};
MyCoroutine coroFunc(){
  std::cout <<"hello world" << std::endl;
  co_return;
}

int main(){
  MyCoroutine coro = coroFunc();
  coro.resume(); // 恢复协程执行
  return 0;
}

返回值

若需要提供返回值,则需要将return_void改为return_value,并添加get函数存储并获取反返回值:

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
#include<iostream>
#include<coroutine>
struct MyCoroutine{
  struct promise_type {
    MyCoroutine get_return_object() {
      // 获得协程句柄
      return MyCoroutine(std::coroutine_handle<promise_type>::from_promise(*this));
    }
    std::suspend_always initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void return_value(int v) {_value = v;}
    void unhandled_exception() {}
    public:
      int get(){return _value;}
    private:
      int _value; // 存储协程返回值
  };
  // 通过构造函数传给新创建的对象
  // 调用了协程句柄的resume函数
  MyCoroutine(std::coroutine_handle<promise_type> h) : handle(h) {}
  int get(){
    handle.resume(); // 恢复协程执行
    return handle.promise().get(); // 获取协程返回值
  }
private:
  std::coroutine_handle<promise_type> handle; // 封装了一个指向协程帧的指针,并提供函数
};
MyCoroutine coroFunc(){
  std::cout <<"hello world" << std::endl;
  co_return 666;
}

int main(){
  MyCoroutine coro = coroFunc();
  int value = coro.get();
  std::cout << "value= " << value << std::endl;
  return 0;
}

co_yield

如果要使用co_yield,需要添加yield_value函数

由于co_yield不是函数执行的结尾,故需要返回一个可等待对象,并使用co_await对该对象进行等待,即

1
2
co_yield v;
co_await promise.yield_value(v);

或者返回一个suspend_always对象,让程序挂起

1
auto yield_value(int v) { _value = v; return std::suspend_always{};}

为方便使用,还可以重载一下布尔运算符,通过调用handle的done函数来判断协程是否执行完毕

1
operator bool(){return !handle.done();}

在主函数中,就可以通过一个循环对协程执行进行挂起和继续操作,并获得每次的返回值

1
2
3
4
5
MyCoroutine coro = coroFunc();
while(coro){
    int value = coro.get();
    std::cout << "value= " << value << std::endl;
}

应用:实现发生器(Generator)

一个斐波那契数列生成器

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
#include<iostream>
#include<coroutine>
struct MyCoroutine{
  struct promise_type {
    MyCoroutine get_return_object() {
      // 获得协程句柄
      return MyCoroutine(std::coroutine_handle<promise_type>::from_promise(*this));
    }
    std::suspend_always initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void return_value(int v) {_value = v;}
    auto yield_value(int v) { _value = v; return std::suspend_always{};}
    void unhandled_exception() {}
    public:
      int get(){return _value;}
    private:
      int _value; // 存储协程返回值
  };
  // 通过构造函数传给新创建的对象
  // 调用了协程句柄的resume函数
  MyCoroutine(std::coroutine_handle<promise_type> h) : handle(h) {}
  operator bool(){return !handle.done();} // 重载bool运算符,判断协程是否完成
  int get(){
    handle.resume(); // 恢复协程执行
    return handle.promise().get(); // 获取协程返回值
  }
private:
  std::coroutine_handle<promise_type> handle; // 封装了一个指向协程帧的指针,并提供函数
};

MyCoroutine fib(){
  int a = 0, b = 1, n = 40;
  co_yield a;
  co_yield b;
  while(n--){
    int r = a+b;
    co_yield r; // 每生成一项就把协程挂起,等待外部调用resume函数
    a = b;
    b = r;
  }
  co_return a+b;
}

int main(){
  MyCoroutine coro = fib();
  while(coro){
    int value = coro.get();
    std::cout << "value= " << value << std::endl;
  }
  return 0;
}

co_await

语法:co_await 表达式

表达式可以是可等待对象,重载co_await()运算符,也可以是一个promise类型的await_transform()函数,将表达式作为函数参数并返回可等待对象

可等待类型awaitable的定义如下:

1
2
3
4
5
6
struct awaitable
{
    bool await_ready();
    void await_suspend(std::coroutine_handle<> h);
    auto await_resume();
};

调用对象时,首先调用await_ready(),若返回值为false,则调用await_suspend()让程序挂起,当协程继续执行后,会接下来执行await_resume(),若返回值为true,则直接执行await_resume()

内置类型suspend_always中await_ready()返回false,其余两个函数什么都不做,作用是将程序挂起

内置类型suspend_never中await_ready()返回true,其余两个函数什么都不做

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
#include<coroutine>
#include<iostream>
#include<thread>
struct promise;
struct MyCoroutine: std::coroutine_handle<promise>{
  using promise_type = ::promise;
};

struct promise{
  MyCoroutine get_return_object() {
    return {MyCoroutine::from_promise(*this)};
  }
  auto initial_suspend() { return std::suspend_always{}; }
  auto final_suspend() noexcept { return std::suspend_always{}; }
  void unhandled_exception() {}
};

struct awaitable{
  bool await_ready(){
    std::cout << "await_ready called" << std::endl;
    return false; // 返回false表示协程需要挂起
  }
  void await_suspend(std::coroutine_handle<> h){
    std::cout << "await_suspend called" << std::endl;
    // 在这里可以执行一些异步操作
    // ...
      h.resume(); // 恢复协程执行
  }
  auto await_resume() {
    std::cout << "await_resume called" << std::endl;
    return 666; // 返回协程的结果
  }
};  

MyCoroutine coroFunc(){
  std::cout << "coroFunc started" << std::endl;
  co_await awaitable{}; // 使用协程挂起
  std::cout << "coroFunc resumed" << std::endl;
}

int main(){
  MyCoroutine coro = coroFunc();
  std::cout << "main started" << std::endl;
  coro.resume(); // 恢复协程执行
  std::cout << "main finished" << std::endl;
  return 0;
}
1
2
3
4
5
6
7
main started
coroFunc started
await_ready called
await_suspend called
await_resume called
coroFunc resumed
main finished

根据返回结果,证明了函数调用的顺序是ready->suspend->resume

引用

语法

本质:引用在内部的实现是一个指针常量(指针指向不可修改,但指针指向的值可修改)

作用:即给变量起别名,即两者操作同一块内存

语法:<数据类型> &<别名> = <原名>

注意:

  • 引用必须初始化
  • 引用在初始化后不可改变
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;
int main(){
  int a = 10;
  int &b = a; // b 是 a 的引用
  cout << "a: " << a << endl; 
  cout << "b: " << b << endl;
  b = 20; // 修改 b 的值, a 的值也会改变
  cout << "a: " << a << endl;
  cout << "b: " << b << endl;
  return 0;
}

引用作为函数参数

作用:函数传参时可以让形参修饰实参

优点:简化指针修改实参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;
void Swap(int &a, int &b){ // 使用形参修饰实参
  int temp = a;
  a = b;
  b = temp;
}
int main(){
  int a = 10;
  int b = 20;
  Swap(a, b);
  cout << "a: " << a << endl;
  cout << "b: " << b << endl;
  return 0;
}

引用作为函数返回值

作用:

  • 引用可以作为函数的返回值存在
  • 函数的调用可以作为左值

不要返回局部变量引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;
// 函数返回引用
// 注意:返回局部变量的引用是危险的,因为局部变量在函数结束后会被销毁
int& test(){
  static int a = 10; //静态变量,存在全局区
  return a; // 返回局部变量的引用
}
int main(){
  int &ref = test();
  cout << "ref: " << ref << endl; // 输出 10
  test() = 20; // 返回引用的函数的调用可以作为左值, 相当于直接对a赋值
  cout << "ref: " << ref << endl; // 输出 20
  return 0;
}

## 常量引用

常量引用常用来修饰形参,防止误操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;
void print(const int &val){
  // 由于此处是引用,在函数内部对val进行修改,外部的b也会改变
  // 形参前加const后,val变成了一个常量引用,内部val就不允许修改了
  // val = 1000; 
  cout << val;

}
int main(){
  int a = 10;
  // 常量引用,相当于编译器自动创建了一个临时空间,并将ref指向该空间
  // 此时不许修改
  const int& ref = 10; 
  
  // 实际应用
  int b = 100;
  print(b);
}

右值引用

左值与右值

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

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

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()