红魔咖啡馆

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

0%

高阶函数

函数的默认参数

C++中,形参是可以有缺省值的

注:

  • 若某个位置已经有了缺省值,则从该位置之后往后都必须要有缺省值
  • 若函数声明有缺省值,则函数实现就不能有缺省值 (二义性)
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;
int f(int a, int b = 20, int c = 30){ // 形参有缺省值
  return a+b+c;
}
int main(){
  cout << f(10)<<endl; // 有缺省值时可以不传参
  cout << f(10, 30)<<endl; // 若传参则会覆盖缺省值
  return 0; 

}

函数的占位参数

C++中函数的形参列表中可以有占位参数,调用时必须填补该位置

语法:<返回值类型> <函数名>([数据类型]){}

注:

  • 目前占位参数传入的参数无法使用
  • 占位参数可以有缺省值
1
2
3
4
5
6
7
8
#include <iostream>
using namespace std;
void f(int a, int){
  cout << a <<" occupation"<<endl;
}
int main(){
  f(10, 10);
}

函数重载

作用:函数名可以相同,用于提高复用性

满足条件:

  • 同一个作用域下
  • 函数名相同
  • 函数参数类型,个数,顺序不同

函数返回值类型不可以做为函数重载的条件

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
#include <iostream>
using namespace std;

void f(){ // 两函数参数不同,实现重载
  cout << "invoke f"<<endl;
}
void f(int a){
  cout << "invoke f(int "<<a<<')'<<endl;
}
void f(double a){ // 两函数参数类型不同,实现重载
  cout << "invoke f(double "<<a<<')'<<endl;
}
void f(double a, int b){ // 两函数参数顺序不同,实现重载
  cout << "invoke f(double "<<a<<", int "<<b << ')'<<endl;
}
void f(int a, double b){ // 两函数参数顺序不同,实现重载
  cout << "invoke f(int "<<a<<", double "<<b << ')'<<endl;
}

int main(){
  // 根据传入的参数执行对应函数
  f();
  f(10);
  f(3.14);
  f(10,3.14);
  f(3.14,10);
  return 0;

}
1
2
3
4
5
invoke f
invoke f(int 10)
invoke f(double 3.14)
invoke f(int 10, double 3.14)
invoke f(double 3.14, int 10)

引用作为重载条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 引用作为函数重载条件
void f2(int &a){
  cout << "invoke f(int &"<<a<< ')'<<endl;
}
void f2(const int &a){ // const int 与int 类型不同
  cout << "invoke f(const int &"<<a<< ')'<<endl;
}

int main(){
  // 根据传入的参数执行对应函数
  f2(a); // 调用了无const的
  f2(10); // 调用了有const的
  return 0;

}
  • 11行相当于实现int &b = a,是合法的
  • 但若只传值,则11行相当于int &a = 10, 是不合法的,此时12行相当于const int &a = 10,是合法的

缺省值与函数重载

当存在缺省值,使得调用函数传入参数个数和被重载的函数的传入参数个数一样时,会出现二义性,故报错

1
2
3
4
5
6
7
8
9
10
11
12
// 函数重载与默认参数
void f3(int a){
  cout << "invoke f3(int &"<<a<< ')'<<endl;
}
void f3(int a, int b = 10){
  cout << "invoke f3(int &"<<a<< ") with b = 10"<<endl;
}
int main(){
  //f3(10); // 会出错,出现二义性
  return 0;

}

函数封装与绑定

std::function

std::function是一个通用的多态函数封装器,用于将可调用对象进行封装

语法function<R(Args...)> fname = target;

其中R为返回类型,括号内为参数类型

进行类成员函数封装时,括号内第一个参数是类引用,第二个开始才是参数类型

也可以进行类成员变量的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <functional>
double mul(double a, double b){
    return a*b;
}
int main(){
  // 将mul的地址保存到f1对象中
  std::function<double(double,double)> f1 = mul;
  // 通过重载调用运算符来调用mul函数
  double res = f1(1.1,2.3);
  std::cout << res << std::endl;
  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <functional>
struct Linear{
    Linear(float k, float b):k_(k),b_(b) {}
    float f(float x) {return k_* x+b_; }
    float k_, b_;
};

int main(){
    std :: function<float(Linear&, float)> mf = &Linear::f;
    Linear l(1.2,2.3);
    float res = mf(l,5);
    std :: cout << res << std :: endl;
    std :: function<float(Linear&)> k = &Linear :: k_;
    std :: cout << k(l) << std :: endl;
}

std::function实现了类型擦除:即通过单个通用接口类使用各种具体类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <functional>
#include <map>

double add(double a, double b){
  return a+b;
}
struct substract{
  double operator()(double a, double b){return a-b;}

};

int main(){
  std::map<char, std::function<double(double,double)>>cal{
    {'+', add},
    {'-', substract()},
    {'*', [](double a, double b)->double{return a*b;}}
  };
  std::cout <<cal['+'](12.0,13)<<std::endl;
  std::cout <<cal['-'](13.0,6.2)<<std::endl;
  std::cout <<cal['+'](2.3,3.2)<<std::endl;
}

可以看到,通过std::function可以把完全不同的类型按同一个接口统一封装一个类型来使用

std::mem_fn

封装类成员可以直接使用std::mem_fn,返回一个可调用的包装器

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
#include <iostream>
#include <functional>

struct foo{
  double w;
  double cal(double a, double b){return 2*a+w*b;}
  foo& operator+=(double a){
    w+=a;
    return *this;
  }
  void print(){
    std::cout<<"w="<<w<<std::endl;
  }
};

int main(){
  foo f{1.0};
  auto memfn = std::mem_fn(&foo::cal);
  double res = memfn(f, 2.0, 3.0);
  std::cout <<"res="<<res<<std::endl;

  auto op = std::mem_fn(&foo::operator+=);
  auto f2 = op(f,2.0);
  f2.print();
}

std::bind

用于生成一个函数对象,调用该包装器时相当于调用他所包装的函数或者对象f

可以将给定的函数和参数进行绑定,调用时直接使用对应函数与参数

其中,参数可以用std::placeholders占位符占位(_1,_2…),在调用包装器时可以指定传入参数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <functional>
using namespace std::placeholders;
int sum(int a, int b, int c){
  std::cout <<"a="<<a<<", b="<<b<<", c="<<c;
  return a+b+c;
}

int main(){
  auto f = std::bind(sum, 1, _1, 3);
  int res = f(5);
  std::cout<<", res="<<res<<std::endl;
}

注意,如果直接用变量作为bind绑定,绑定后修改变量不会影响结果,因为是用值传入的

如果想用引用传入,使用cref()函数,用于返回一个变量的引用封装器s

宏定义

常量替换

#define 标识符 替换表达式

C++中常用const方式进行常量替换

条件编译

可以定义一个空的宏来用于条件编译

  • #ifdef 宏名:若定义了对应宏则执行下面的宏指令

  • #else:否则执行else下面的宏指令

  • #undef 宏名:取消原来的定义

  • #endif:条件编译的结尾

宏函数

#define 标识符(参数列表) 替换表达式

为了防止运算符优先级造成问题,一般要对表达式加上括号

C++中可以被内联函数取代

#与

  • #将符号转为字符串

e.g.#define PRINT(a) cout<<#a<<"="<<(a)<<" ";

  • ##将两个表达式连接在一起

e.g.#define MEMBER(type, a) type m_##a

多行宏定义

若想定义多行宏定义,则需要在每行结尾加一个

可变参的宏函数

#define 标识符(参数列表,...) 替换表达式

表达式中用__VA_ARGS__标识符表示这些参数,宏展开时,实际参数会替换掉该标识符

1
2
3
4
5
6
7
8
9
#include <stdio.h>

#define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__)

int main() {
    LOG("Hello, %s\n", "World");
    LOG("This is a test.\n");
    return 0;
}

类与对象

C++面向对象的三大特性:封装、继承、多态

C++中,任何事物都为对象,对象有其属性和行为

具有相同性质的对象,我们可以抽象称为类

封装

封装的实现

意义:

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制

语法:class <类名>{ <访问权限>: <属性/行为>}

一些名词:

  • 成员:类中的属性和行为的统称
  • 成员属性(成员变量):类中的属性
  • 成员函数(成员方法):类中的函数

e.g.1 设计一个圆类,求其周长

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>
using namespace std;
// 设计一个圆类,求其周长
const double PI = 3.14;
class circle{
// 访问权限: 公共权限
public:
  // 属性:半径
  int r;
  // 行为:获取周长
  double calC(){
    return 2*PI*r;
  }
};
int main(){
  // 通过类创建一个对象——实例化
  circle c1;
  // 给对象的属性进行赋值,使用.
  c1.r = 10;
  // 调用行为
  cout << "r="<< c1.calC()<<endl;

  return 0;
}

e.g.2 设计一个学生类,属性有姓名和学号,可以赋值并显示姓名和学号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <string>
using namespace std;
// 设计一个学生类,有姓名和学号,可以赋值并显示
class student{
public:
  //属性
  string name; // 姓名
  int id; // 学号

  // 行为
  void display(){
    cout << "name:" << name<<endl;
    cout << "id:" << id <<endl;
  }

  void set_info(string s_name, int s_id){
    name = s_name;
    id = s_id;
  }
};

int main(){
  student stu1;// 实例化
  student stu2;
  // 一般赋值
  stu1.name = "Jack";
  stu1.id = 1;
  stu1.display();
  // 通过行为赋值
  stu2.set_info("Mary", 2);
  stu2.display();
  return 0;
}

权限

类可以将属性和方法放在不同权限下:

  • public:公共权限,成员类内可以访问,类外也可以访问
  • protected:保护权限,成员类内可以访问,类外不可以访问,子类可以访问父类的保护成员
  • private:私有权限,成员类内可以访问,类外不可以访问, 子类不可访问父类的私有成员
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
#include <iostream>
#include <string>
using namespace std;
class person{
public: //公共权限
  string name;
protected: // 保护权限
  string car;
private: // 私有权限
  int password;
public:
 // 类内可以访问
  void f(){
    name = "Jack";
    car = "xiaomisu7";
    password = 123456;
  }
};
int main(){
  person p;
  p.name = "Mary";
  //p.car = "?" // protected 类外不可访问
  //p.password = 123 // private 类外不可访问
  return 0;
}

struct与class的区别

C++中class与struct的唯一区别在于默认的访问权限不同

  • class默认权限私有
  • struct默认权限公共

成员属性私有化

优点:

  • 可以自己控制读写权限
  • 可以检测数据有效性

基本思路:用公有方法对私有属性进行操作

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
#include <iostream>
#include <string>
using namespace std;

class person{
// 属性私有
private:
  string name; // 姓名:可读可写
  int age = 18; // 年龄:只读
  string hobby; // 爱好:只写
// 方法公有
public:
  // 设置姓名
  void setName(string s){
    name = s;
  }
  // 获取姓名
  void getName(){
    cout <<"name:"<<name<<endl;
  }
  // 获取年龄
  void getAge(){
    cout <<"age:"<<age<<endl;
  }
  // 设置爱好
  void setHobby(string s){
    hobby = s;
  }
};
int main(){
  person p;
  p.setName("Jack");
  p.getAge();
  p.setHobby("jerk");

  return 0;
}

对象的初始化和清理

对象的初始化和清理是很重要的安全问题:

  • 一个对象或变量没有初始状态,对其使用后果未知
  • 使用完一个对象或变量,没有及时清理,也会造成安全问题

构造函数与析构函数

C++提供了这两种函数,被编译器自动调用,完成对象初始化与清理工作

这两个工作是强制执行的,若我们不提供,编译器会提供自己的构造函数与析构函数,他们是空实现

构造函数:用于创建对象时为对象成员赋值

  • 语法:<类名>(){}

  • 无返回值,不用写void

  • 函数名与类名相同

  • 构造函数可以有参数,可以发生重载

  • 调用对象时会自动调用构造,只会调用一次

析构函数:用于对象销毁前自动调用,执行清理操作

  • 语法:~<类名>(){}
  • 无返回值,不用写void
  • 函数名与类名相同,名前面加~
  • 析构函数不可以有参数,不可发生重载
  • 程序在对象销毁前会自动调用析构。只会调用一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;

class person{
  
public:
  // 构造函数
  person(){
    cout << "构造函数被调用" << endl;
  }
  // 析构函数
  ~person(){
    cout << "析构函数被调用" << endl;
  }
};

int main(){
  person p; // 栈上的数据,执行完后会被释放,故执行析构函数
  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
#include <iostream>
using namespace std;

class person{
  
public:
  // 无参构造(默认构造)
  person(){
    cout << "无参构造函数被调用" << endl;
  }
  // 有参构造
  person(int a){
    age = a;
    cout << "有参构造函数被调用" << endl;
  }
  // 拷贝构造 
  person(const person &p){
    age = p.age; // 将传入数据拷贝到当前对象
    cout << "拷贝构造函数被调用" << endl;
  }
  // 析构函数
  ~person(){
    cout << "析构函数被调用" << endl;
  }
  int age;
};

int main(){
  // 括号法
  person p; // 默认构造函数被调用(不加括号)
  person p1(10); // 有参构造函数被调用
  person p2(p1); // 拷贝构造函数被调用
  cout << "age of p1:" << p1.age << endl;
  cout << "age of p2:" << p2.age << endl;
  // 显式法
  person p3 = person(20);
  person p4 = person(p3);

  // 隐式转换法
  person p5 = 10; // 相当于 person p5 = person(10);
  person p6 = p5; // 相当于 person p6 = person(p5);
  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
#include <iostream>
using namespace std;

class person{
  
public:
  // 无参构造(默认构造)
  person(){
    cout << "无参构造函数被调用" << endl;
  }
  // 有参构造
  person(int a){
    age = a;
    cout << "有参构造函数被调用" << endl;
  }
  // 拷贝构造  
  person(const person &p){
    age = p.age; // 将传入数据拷贝到当前对象
    cout << "拷贝构造函数被调用" << endl;
  }
  // 析构函数
  ~person(){
    cout << "析构函数被调用" << endl;
  }
  int age;
};

// 使用已经创建完毕的对象来初始化一个新对象
void test1(){
  person p1(20);
  person p2(p1);
}
// 值传递方式给函数参数传值
void test2(person p){
  return ;
}
// 值传递返回局部对象
person test3(){
  person p1;
  return p1;
}

int main(){
  person p1;
  test1();
  test2(p1);
  person p2 = test3();
  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
无参构造函数被调用
----------------
有参构造函数被调用
拷贝构造函数被调用
析构函数被调用
析构函数被调用
----------------
拷贝构造函数被调用
析构函数被调用
----------------
无参构造函数被调用
析构函数被调用
析构函数被调用

构造函数调用规则

默认下,编译器至少给一个类添加三个函数:

  • 默认构造函数(无参,函数体为空)
  • 默认析构函数(无参,函数体为空)
  • 默认拷贝构造函数,对属性值拷贝

调用规则:

  • 若用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数 ,C++不再提供其他构造函数
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include <iostream>
using namespace std;

class person{
  
public:
  // 无参构造(默认构造)
  person(){
    cout << "无参构造函数被调用" << endl;
  }
  // 有参构造
  person(int a){
    age = a;
    cout << "有参构造函数被调用" << endl;
  }
  // // 拷贝构造 
  // person(const person &p){
  //   age = p.age; // 将传入数据拷贝到当前对象
  //   cout << "拷贝构造函数被调用" << endl;
  // }
  // 析构函数
  ~person(){
    cout << "析构函数被调用" << endl;
  }
  int age;
};

int main(){
  person p;
  p.age = 18;
  person p2(p);
  cout << "p2的年龄是:" << p2.age << endl;
  return 0;
}

```

此种情况下,编译器使用了默认拷贝构造,进行了值传递

```c++
#include <iostream>
using namespace std;

class person{
  
public:
  // // 无参构造(默认构造)
  // person(){
  //   cout << "无参构造函数被调用" << endl;
  // }
  // 有参构造
  person(int a){
    age = a;
    cout << "有参构造函数被调用" << endl;
  }
  // 析构函数
  ~person(){
    cout << "析构函数被调用" << endl;
  }
  int age;
};

int main(){
  person p;
  p.age = 18;
  person p2(p);
  cout << "p2的年龄是:" << p2.age << endl;
  return 0;
}

```

此种情况下,由于有有参函数,编译器不会提供默认无参函数,同时会报错没有默认无参函数

```c++
#include <iostream>
using namespace std;

class person{
  
public:
  // // 无参构造(默认构造)
  // person(){
  //   cout << "无参构造函数被调用" << endl;
  // }
  // // 有参构造
  // person(int a){
  //   age = a;
  //   cout << "有参构造函数被调用" << endl;
  // }
  // 拷贝构造 
  person(const person &p){
    age = p.age; // 将传入数据拷贝到当前对象
    cout << "拷贝构造函数被调用" << endl;
  }
  // 析构函数
  ~person(){
    cout << "析构函数被调用" << endl;
  }
  int age;
};

int main(){
  person p;
  p.age = 18;
  person p2(p);
  cout << "p2的年龄是:" << p2.age << 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
#include <iostream>
using namespace std;

class person{
  
public:
  // 无参构造(默认构造)
  person(){
    cout << "无参构造函数被调用" << endl;
  }
  // 有参构造
  person(int a, int iheight){
    age = a;
    height = new int(iheight);
    cout << "有参构造函数被调用" << endl;
  }
  // // 拷贝构造 
  // person(const person &p){
  //   age = p.age; // 将传入数据拷贝到当前对象
  //   cout << "拷贝构造函数被调用" << endl;
  // }
  // 析构函数
  ~person(){
    // 在析构函数中进行堆区数据释放
    if (height!=NULL){
      delete height;
      height = NULL;
    }
    cout << "析构函数被调用" << endl;
  }
  int age;
  int* height; // 身高,使用指针将数据创建在堆区
};

int main(){
  person p1(18,160);
  cout << "p1的年龄是:" << p1.age << endl;
  cout << "p1的身高是:" << *p1.height << endl;
  person p2(p1);
  cout << "p2的年龄是:" << p2.age << endl;
  cout << "p2的身高是:" << *p2.height << endl;
  return 0;
}

该代码会卡住,原因是利用拷贝构造只进行了值传递,p1与p2执行完毕后,均执行析构函数,对height指向的空间进行释放,但是其中一个已经执行释放后,另一个再次释放会产生非法操作,故出错

这也就是浅拷贝的缺点:只进行了值传递,我们需要利用深拷贝来结局

1
2
3
4
5
6
7
// 拷贝构造 
person(const person &p){
  age = p.age; // 将传入数据拷贝到当前对象
  // 深拷贝,在堆区开辟新的空间
  height = new int(*p.height);
  cout << "拷贝构造函数被调用" << endl;
}

即在拷贝构造中,重新给拷贝的指针变量申请一块空间

如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝出现问题

初始化列表

C++提供了初始化列表来初始化属性

语法:<构造函数>(): <属性1>(<值1>), <属性2>(<值2>) ... {}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

class person{
  
public:
  int a;
  int b;
  int c;
  // 初始化列表赋值
  person(int a, int b, int c):a(10), b(20), c(30){
  }
};

int main(){
  person p;
  cout << "a:" << p.a << endl;
  cout << "b:" << p.b << endl;
  cout << "c:" << p.c << endl;
  return 0;
}

类对象作为类成员

C++中的成员可以是另一个类的对象,称为对象成员

1
2
3
4
class A{};
class B{
    A a;
};

该B类中有对象A作为成员,则A为对象成员

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 <string>
using namespace std;
class phone{
public:
  string brand;
  float price;

  phone(string pname){
    brand = pname;
    cout << "phone构造函数被调用" << endl;
  }
  ~phone(){
    cout << "phone析构函数被调用" << endl;
  }
};

class person{
  
public:
  string name;
  phone iphone;

  person(string pname, string pbrand):name(pname),iphone(pbrand){
    //相当于phone iphone = pname;的隐式转换
    cout<< "person构造函数被调用" << endl;
  }
  ~person(){
    cout << "person析构函数被调用" << endl;
  }
  
};
int main(){
  person p("小明","苹果");
  cout << "姓名:" << p.name << endl;
  cout << "手机品牌:" << p.iphone.brand << endl;
  return 0;
}
1
2
3
4
5
6
phone构造函数被调用
person构造函数被调用
姓名:小明
手机品牌:苹果
person析构函数被调用
phone析构函数被调用

注意构造函数的调用顺序:

  • 当其他类对象作为本类成员,构造时先构造类对象,在构造自身
  • 当其他类对象作为本类成员,析构时先析构自身,再析构类对象

静态成员

在成员变量与成员函数前加上关键字static,称为静态成员

分类:

  • 静态成员变量
    • 不属于某个对象,所有对象共享一份数据
    • 编译阶段分配内存
    • 类内声明,类外初始化
  • 静态成员函数
    • 所有成员共享同一个函数
    • 静态成员函数只能访问静态成员变量
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>
using namespace std;

class person{
  
public:
  static int a; // 需要类内声明,类外初始化
  int c;
  static void f(){
    cout << "静态成员函数f调用"<<endl;
    a = 100;
    // c = 200; // 静态成员函数不可以访问非静态成员变量
  }
private:
  static int b; // 静态成员变量也有权限

  
};
int person::a = 100; // 在类外初始化,需要指定作用域
int person::b = 100; // 也需要在类外初始化
int main(){
  person p; // 栈上的数据,执行完后会被释放,故执行析构函数
  cout << p.a<<endl;
  person p2;
  p2.a = 200; // 数据共享,此处改变即一起改变
  cout << p.a<<endl; // 通过对象进行访问
  cout << person::a<<endl; // 通过类名进行访问
  // cout << p.b<<endl; // 不可访问

  // 同样两种访问模式
  p.f();
  person::f();
  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
#include <iostream>
using namespace std;

class Singleton{
private:
  static Singleton* m_insntance; // 存储该类的唯一实例
  // 将构造与析构设为私有
  Singleton(){}
  ~Singleton(){}
public:
  // 删除拷贝构造与赋值运算符构造
  Singleton(const Singleton& obj) = delete;
  Singleton& operator=(const Singleton&) = delete;

  // 创建该唯一实例, 只能通过该方法创建获得对象
  static Singleton* getInstance(){
    if (!m_insntance){
      m_insntance = new Singleton();
    }
    return m_insntance;
  }
private:
  string m_name;
public:
  void setName(const string& name){
    m_name = name;
  }
  const string& getName(){
    return m_name;
  }
};
Singleton* Singleton::m_insntance = NULL;
int main(){
  // 第一次调用创建实例
  Singleton* s1 = Singleton::getInstance();
  cout << s1->getName()<<endl;
  // 第二次调用使用已经创建的实例
  Singleton* s2 = Singleton::getInstance();
  cout << s1->getName()<<endl;
}

C++对象模型与this指针

成员变量与成员函数分开存储

c++中,类内成员变量与成员函数分开存储

只有非静态成员变量才属于类的对象上

对于空对象,编译器会给每个空对象分配一个字节空间,是为了区分空对象占内存的位置

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
#include <iostream>
using namespace std;

class person{
  
};
class person2{
  int a;
};
class person3{
  int a;
  static int b;
  void f(){}
};

int main(){
  person p;
  person2 p2;
  person3 p3;
  // 占用内存为1
  cout << "size of empty class: "<< sizeof(p)<<endl;

  // 占用内存为4
  cout << "size of class person2: "<< sizeof(p2)<<endl;

  // 静态成员与非静态成员函数不属于类的对象上,故占用内存也为4
  cout << "size of class person3: "<< sizeof(p3)<<endl;
  return 0;
}

this指针

C++通过提供this指针解决了区分多个对象调用同一块代码分不清的问题

this指针指向被调用的成员函数所属的对象,不需要定义,直接使用

用途:

  • 形参和成员变量同名时,可以用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用retrun *this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
using namespace std;

class person{
  
public:
  int age;
  person(int age){
    // 当形参和成员变量同名时,this 指针指向被调用成员函数所属对象
    this->age = age; // 此时this->age和定义的成员变量age是同一个
  }
  // 成员函数返回对象本身
  person& addage(person &p){
    this->age+=p.age;
    return *this;
  }
};

int main(){
  // 解决名称冲突
  person p(18); 
  person p2(10);
  cout <<"age:" <<p.age<<endl;

  // 返回对象本身可以实现链式调用
  p2.addage(p).addage(p).addage(p);
  cout <<"age of p2:"<<p2.age<<endl;

  return 0;
}

空指针访问成员函数

C++允许空指针调用成员函数,但是要注意有没有用到this指针

如果用到this指针,要注意保证代码的鲁棒性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;

class person{
  
public:
  int age;
  void show(){
    cout <<"this is person class"<<endl;
  }
  void showage(){
    // 防止指向空 
    if (this==NULL){
      return ;
    }
    // 调用成员变量时,默认在前面加this->,但此时p是空指针,故指向的是空对象,this没有指向数据
    cout << "age = "<<age <<endl;
  }
};

int main(){
  person *p =  NULL;
  p->show();
  p->showage(); 
  return 0;
}

const修饰成员函数

常函数:

  • 成员函数后加const称该函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数内依然可以修改

常对象:

  • 声明对象前加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
#include <iostream>
using namespace std;

class person{
  
public:
  int age;
  mutable int b; // 加上mutable也可以在常函数中修改
  // this指针本身是指针常量,指针指向是不可修改的,指针指向的值是可以修改的
  // 而加上const修饰后,修饰的是this指向的值,让指向的值也不可修改
  // 相当于const person * const this;
  void show() const{
    //this->age = 100; // 此时不可修改 
    this->b = 100;
  }
  void f(){}
  person(){

  }
};

int main(){
  // 常对象
  const person p;
  // p.age = 100; // 常对象下不能修改值
  p.b = 100; // 但有mutable的可以修改
  // p.f() // 常对象只能调用常函数
  p.show();
  return 0;
}

类的特殊成员函数

特殊成员函数包括默认构造函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数,他们均可以用以下两个关键字修饰

  • =delete

    可以在函数声明后添加 =delete来删除当前函数,如删除构造函数、删除不需要的传参类型

  • =default

    加在特殊函数后,可以用来声明一个默认特殊函数,还可以自动使用默认值初始化变量

可平凡复制类

C++中,各种数据类型及他们的数组、指针、枚举等在内存中是连续存储的,可以使用memcpy()函数进行拷贝,称为可平凡复制类型

一个类若满足以下条件,被称为可平凡复制类,也是一种可平凡复制类型

  • 没有虚函数
  • 没有用户自定义的特殊成员函数
  • 有自动生成的析构函数
  • 所有非静态成员变量也是可平凡复制类型
  • 拷贝、移动构造函数,拷贝、移动赋值运算符中至少有一个是未被删除的
  • 对于继承类,其父类要满足上述条件

可以使用<type_traits>库中的is_trivially_copyable<类>::value来判断是否可拷贝

标准布局类

C++中,所有标量类型都是标准布局类型

对一个非继承类,满足以下条件被称为标准布局类

  • 没有虚函数
  • 所有类成员都具有相同访问属性
  • 所有类成员都是标准布局类型
  • 若类是继承类,则满足该类和父类中只有一个类有非静态数据成员,且类型与访问控制满足上述条件

可以使用<type_traits>库中的is_standard_;leyout<类>::value来判断是否可拷贝

友元

友元可以让一个函数或者类访问另一个类中的私有成员

友元的关键字为friend

全局函数做友元

在class中声明对应全局函数,并在最前面加friend关键词,可以声明友元

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
#include <iostream>
using namespace std;

class building{
  friend void fir(building &building); // 声明该全局函数为building类的友元
public:
  string sitting_room;
private:
  string bedroom;

public:
  building(){
    sitting_room = "客厅";
    bedroom = "卧室";
  }
};
void fir(building &building){
  cout << "访问公共属性:"<<building.sitting_room<<endl;
  cout << "访问私有属性:"<<building.bedroom<<endl;

}
int main(){
  building b;
  fir(b);
}

类做友元

在class中声明对应类,并在最前面加friend关键词,可以声明友元,被声明的类可以访问原类中的私有属性

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
#include <iostream>
using namespace std;
class buildings;
class fri{
public:
  buildings* building;
  fri();
  void visit();
};

class buildings{
  friend class fri; // 声明fri类是building类的友元
public:
  string sitting_room;
  buildings();
private:
  string bedroom;
};

// 在类外写成员函数
buildings::buildings(){
  sitting_room = "客厅";
  bedroom = "卧室";
}
fri::fri(){
  building = new buildings;
}

void fri::visit(){
  cout <<"访问公共属性:" <<building->sitting_room<<endl;
  cout <<"访问私有属性:" <<building->bedroom<<endl; // 友元可以访问类中的私有属性
}

int main(){
  fri f;
  f.visit();
}

成员函数做友元

在class中声明对应类中的成员函数,并在最前面加friend关键词,可以声明友元,被声明的成员函数可以访问原类中的私有属性

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
#include 
using namespace std;
class buildings;
class fri{
public:
  buildings* building;
  fri(){
    building = new buildings;
  }
  void visit(); // 让visit可以访问buildings中的私有属性
};

class buildings{
  friend void fri::visit(); // 声明fri类下的visit成员函数是building类的友元
public:
  string sitting_room;
  buildings(){
    sitting_room = "客厅";
    bedroom = "卧室";
  }
private:
  string bedroom;
};

void fri::visit(){
  cout <<"访问公共属性:" <sitting_room<bedroom<

运算符重载

运算符重载可以对已有的运算符重新定义,赋予另一种功能以适用于不同的数据类型

如我们通过写成员函数来实现两个类相加属性后返回新的对象

可以通过编译器提供的通用名称operator来重载运算符以简化运算

重载的本质调用为(以person类为例):

  • 成员函数:person p3 = p1.operator<运算符>(p2)
  • 全局函数:person p3 = p1.operator<运算符>(p1, p2)

注意:

  • 运算符重载也可以实现函数重载
  • 对于内置数据类型的表达式的运算符是不可以重载的
  • 不要滥用运算符重载

加号运算符重载

成员函数:

1
2
3
4
5
6
person operator+(person &p){
    person temp;
    temp.a = this->a+p.a;
    temp.b = this->b+p.b;
    return temp;
 }

全局函数:

1
2
3
4
5
6
person operator+(person &p1, person &p2){
  person temp;
    temp.a = p1.a+p2.a;
    temp.b = p1.b+p2.b;
    return temp;
}

左移运算符重载

1
2
3
4
5
6
7
8
// 只能全局重载左移运算符
// 若在类内重载会称为p.operator<<(cout) 即p<<cout (cout是ostream流中的对象)
// 这里先写cout即可实现cout在左边p在右边
// 返回值作为cout本身的引用,可以实现链式编程
ostream & operator<<(ostream &cout, person &p){
  cout << "a=" << p.a << endl << "b=" << p.b;
  return cout;
}

递增运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 前置++
// 这里返回引用是为了确保只对同一个数据进行操作
myint& operator++(){
  num++; // 先进行自增运算
  return *this; // 再返回自身
}

// 后置++
// 这里为了防止重复,添加一个int占位符来实现函数重载以区分前后置
myint operator++(int){
  myint temp = *this;
  num++;
  return temp;
}

赋值运算符重载

C++编译器会额外给一个类添加赋值运算符的重载函数

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>
using namespace std;
class person{
public:
  int *age;
  person(int age){
    this->age = new int(age); // 年龄放在堆区
  }

  // 将age内存释放
  ~person(){
    if (age!=NULL){
      delete age;
      age = NULL;
    }
  }
};
int main(){
  person p1(1);
  cout << "p1's age:" << *p1.age << endl;
  person p2(2);
  p2 = p1; // 赋值操作
  cout <<"p2's age:"<<*p2.age<<endl;
}

该代码出错,由于age开到了同一块堆区内存,故在p1赋值给p2时,两者的age指针都指向同一块区域

则当p2执行完毕,执行析构函数时,将该堆区内存释放;再等p1执行完毕,执行析构函数时,将已经释放的内存重复释放了,故报错

我们需要通过重载赋值运算符来实现深拷贝

1
2
3
4
5
6
7
8
9
10
// 重载赋值运算符
person& operator=(const person &p){
  // 首先判断是否有属性在堆区,若有先释放,再深拷贝
  if (age!=NULL){
    delete age;
    age = NULL;
  }
  age = new int(*p.age);
  return *this;
}

关系运算符重载

实现自定义数据类型的比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 重载==运算符
  bool operator==(const person &p){
    if (this->name==p.name && this->age==p.age){
      return true;
    }
    else return false;
  }

  // 重载!=运算符
  bool operator!=(const person &p){
    if (this->name==p.name && this->age==p.age){
      return false;
    }
    else return true;
  }

大于小于类似

函数调用运算符重载

  • 函数调用运算符即()
  • 重载后的使用方式很像函数的调用,故也称为仿函数
  • 仿函数没有固定写法
1
2
3
4
5
6
7
8
9
10
11
12
13
class print{
public:
  //重载函数调用运算符
  void operator()(string s){
    cout << s << endl;
  }
};
class add{
public:
  int operator()(int n1, int n2){
    return n1+n2;
  }
};

也可以用匿名函数对象进行调用

继承

有些类与其他类之间除了有共性,还有自己的特性,我们可以用继承的技术来减少重复代码优点:减少重复代码

语法:<子类>: <继承方式> <父类>

其中子类也称作派生类,父类也称基类

子类中的成员包含两部分:

一类是从基类继承过来的,一类是自己增加的成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class page{
public:
  void f1()
  {
    cout << "f1"<<endl;
  }
  void f2(){
    cout << "f2" <<endl;
  }
  void f3(){
    cout << "f3" << endl;
  }

};
class page2:public page{
public:
  void s1(){
    cout << "s1"<<endl;
  }
};

继承方式

即:

  • 私有成员子类均不可访问
  • 公共继承下:各成员权限不变
  • 保护继承下:public变为protected
  • 私有继承下:public与protected变为private

继承中的对象模型

父类中所有非静态成员属性都会被子类继承

而私有属性被编译器隐藏,故访问不到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
using namespace std;
class fa{
  public:
  int a;
  protected:
  int b;
  private:
  int c;
};
class son:public fa{
  public:
  int d;
};
int main(){
  // 结果为16,即父类中所有属性都会被继承
  cout << sizeof(son)<<endl;
}

继承中的构造与析构顺序

子类继承父类后,创建子类对象时,也会调用父类的构造函数

顺序:先构造父类,再构造子类,析构相反

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>
using namespace std;
class base{
  public:
  base(){
    cout <<"base构造函数"<<endl;
  }
  ~base(){
    cout <<"base析构函数"<<endl;
  }
};
class son:public base{
  public:
  son(){
    cout <<"son构造函数"<<endl;
  }
  ~son(){
    cout <<"son析构函数"<<endl;
  }
};
int main(){
  son s;
}
1
2
3
4
base构造函数
son构造函数
son析构函数
base析构函数

同名成员处理方式

当子类和父类出现同名成员:子类的同名成员会隐藏掉所有父类中同名成员(函数重载也会被隐藏)

  • 访问子类同名成员:直接访问
  • 访问父类同名成员:加作用域

静态与非静态处理方式一样

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
#include <iostream>
using namespace std;
class fa{
public:
  int a;
  static int b; 
  fa(){
    a = 100;
  }
  void f(){
    cout <<"f in fa"<<endl;
  }
  static void ff(){
    cout <<"ff in fa"<<endl;
  }
};
class son:public fa{
public:
  int a;
  static int b;
  son(){
    a = 200;
  }
  void f(){
    cout <<"f in son"<<endl;
  }
  static void ff(){
    cout <<"ff in son"<<endl;
  }
};
int fa::b = 10;
int son::b = 20;

int main(){
  son s;
  cout << "a in son:"<<s.a<<endl;
  // 加作用域访问父类中的同名成员变量
  cout << "a in fa:" <<s.fa::a<<endl;
  s.f();
  // 加作用域调用父类中的同名成员函数
  s.fa::f();

  // 非静态同名成员访问:通过对象
  cout <<"static b in son:"<<s.b<<endl;
  cout <<"static b in fa:"<<s.fa::b<<endl;
  s.ff();
  s.fa::ff();

  // 非静态同名成员访问:通过类名
  cout <<"static b in son:"<<son::b <<endl; 
  // 第一个::代表通过类名访问,第二个::代表访问父类作用域下
  cout <<"static b in fa:"<<son::fa::b<<endl;
  son::ff();
  son::fa::ff();

  
}

多继承

C++允许一个类继承多个类

语法:<子类>: <继承方式> <父类1>, <继承方式> <父类2>...

同名成员出现时需要加作用域区分

实际开发时不建议使用多继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
using namespace std;
class fa{
public:
  int a;
  fa(){
    a = 100;
  }
  void f(){
    cout <<"f in fa"<<endl;
  }
};
class fa2{
public:
  int a;
  fa2(){
    a= 200;
  }
  void f(){
    cout <<"f in fa"<<endl;
  }
};
class son:public fa, public fa2{
public:
  int c;
  int d;
  son(){
    c = 300;
    d = 400;
  }
};
int main(){
  // 多继承子类空间
  cout << "size of son:" <<sizeof(son)<<endl;

  son s;
  cout <<"a in fa:"<<s.fa::a<<endl;
  cout <<"a in fa2:"<<s.fa2::a<<endl;

}

菱形继承

概念:两个派生类继承同一个基类,又有某个类同时继承两个派生类

菱形继承时,两个父类拥有相同数据,需要加作用域区分

但该数据对于最后的子类来说存在两份,会浪费空间,需要利用虚继承来解决问题

在需要虚继承的部分的访问权限前添加关键字vritual即可,此时对应父类称为虚基类

原理:此时的父类继承的是虚基类指针(vbptr),指向一个虚基类表,该表存储了指针位置以及对应偏移量,根据偏移量便可以找到继承的那唯一一份数据

缺点:虚继承占用内存很大

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
#include <iostream>
using namespace std;
class animal{
public:
  int age;
};

// sheep 继承了 animal
class sheep: virtual public animal{};

// camel 继承了 animal
class camel: virtual public animal{};

// alpaca 继承了 sheep和camel
class alpaca: public sheep, public camel{};

int main(){
  alpaca st;
  // 菱形继承时,两个父类有相同数据,需要加作用域区分
  st.sheep::age = 18;
  st.camel::age = 20;
  cout << "age of sheep: "<<st.sheep::age<<endl;
  cout << "age of camel: "<<st.camel::age<<endl;
  cout << "age of alpaca: "<<st.alpaca::age<<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
#include<iostream>
using namespace std;

class animal{
public:
  // 虚函数
  virtual void speak(){
    cout <<"speaking"<<endl;
  }
};

class cat: public animal{
public:
  void speak(){
    cout <<"cat is speaking"<<endl;
  }
};

// 地址在编译阶段确定函数地址,但为了让多个类输出对应,需要在运行时确定
// 当给animal中的speak变为虚函数后,便可以实现子类重写父类
void dospeak(animal &ani){ // 父类引用
  ani.speak(); // 但调用的是animal中的speak函数
}
int main(){
  cat cat;
  dospeak(cat); // 父类引用指向了子类
}

在开发时,提倡开闭原则:即对扩展进行开放,对修改进行关闭

纯虚函数与抽象类

多态中,通常父类中的虚函数实现是毫无意义的,都是调用子类中的重写内容

因此可以将虚函数改为纯虚函数

语法:virtual <返回值类型> <函数名>([参数列表]) = 0;

此时这个类称为抽象类

  • 抽象类无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;

class base{
public:
  virtual void func() = 0; // 纯虚函数
};
class son :public base{
public:
// 子类必须重写父类中的纯虚函数
  virtual void func(){
    cout <<"func调用"<<endl;

  }
};
int main(){
  // base b; 不允许实例化抽象类
  base *b = new son;
  b->func();
}

虚析构与纯虚析构

多态使用时,如果子类中有属性开辟到堆区,则父类指针在释放时就无法调用到子类的虚构代码,此时需要将父类中的析构函数改为虚析构或纯虚析构

  • 可以解决父类指针释放子类对象
  • 都需要有函数具体实现

其中,纯虚析构需要类外实现

有纯虚析构后该类便成为了抽象类

语法:virtual ~<类名>(){}virtual ~<类名>()=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
#include <iostream>
using namespace std;

class animal{
public:
  virtual void speak() = 0;
  // 用虚析构解决父类指针释放子类对象时释放不完全的问题
  virtual ~animal(){
    cout <<"animal析构调用"<<endl;
  }
};
class cat: public animal{
public:
  cat(string name){
    cname = new string(name);
  }
  virtual void speak(){
    cout <<*cname<<"'s speaking"<<endl;
  }
  ~cat(){
    if (cname !=NULL){
      cout <<"cat析构调用"<<endl;
      delete cname;
      cname = NULL;
    }
  }

  string *cname;
};

int main(){
  animal * ani = new cat("Tom");
  ani->speak();
  // 父类指针析构时不会调用子类析构函数,导致若子类有析构属性时不会被释放
  delete ani;
}

杂项

override与final限定符

在基类是虚函数的情况下,子类对该虚函数进行重写,在要重写的函数后面加上override可以防止因为子类的拼写错误,数据类型错误等问题,导致无法调用到子类而直接调用了基类

(若不相同会直接报错)

在虚函数声明的结尾加上final可以防止该虚函数被子类重写,在类声明的后面加上final可以防止该类被继承

注意这两个限定符不是保留关键字

const成员函数

在成员函数声明和定义的后面加上关键字const

const成员函数不能修改其成员变量,不能调用非const成员函数,不能通过const类型的对象调用非const成员函数

即常量对象只可以调用常量方法

命名空间

使用

格式:

1
2
3
namespace 标识符{
    // 类,对象,变量,函数等
};

使用:

使用限定名:标识符::名字

同一个命名空间可以在一个或多个文件下重复使用

引入

  • 使用using namespace可以引入某个命名空间中的所有名称,这样就可以直接使用内部对应内容
  • 使用using声明可以引入某个限定标识符,这样就不需要使用限定名了
  • 引入声明区分全局与局部
  • 不限定命名空间的均默认处于全局命名空间global,引入时只需要使用::名字

嵌套

命名空间可以嵌套使用:

1
2
3
4
5
6
7
int a = 1;
namespace Space1{
    int a = 2;
    namespace Space2{
        int a = 3;
    }
}

调用时需要明确指出引用哪个,否则会造成不明确,默认输出全局的

1
2
3
4
using Space1::a;
cout << a <<endl;
using Space1::Space2::a;
cout << a <<endl;

模板与泛型编程

模板

概念:建立通用模具,大大提高复用性,利用模板进行编程的思想被称为泛型编程

机制:函数模板与类模板

函数模板

建立一个通用函数,返回值类型与形参类型可以不具体指定,用虚拟类型代表

语法

1
2
template<typename T>
/*函数声明或定义*/
  • template:声明创建模板
  • typename:表示后面的符号是一种数据类型
  • T:通用数据类型,名称可以替换,但通常是大写字母
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<iostream>
using namespace std;
template<typename T> // 声明模板
void swapt(T &a, T &b){
  T temp = a;
  a = b;
  b = temp;
  
}
int main(){
  int a = 20;
  int b = 10;
  // 方法一:自动类型推导
  swapt(a,b);
  // 方法二:显式指定类型
  swapt<int>(a,b);
  cout << a << " " << b;

}

注意

  • 自动类型推导时,必须推导出一致的数据类型T才能使用

    1
    2
    3
    int a;
    char c;
    swapt(a,c); // 出错
  • 模板必须要确定出T的数据类型才能用

    1
    2
    3
    4
    5
    template<typename T>
    void f(){
        return ;
    }
    f(); // 报错
  • typename可以用class替换(class可以声明函数模板和类模板)

与普通函数的区别

  • 普通函数调用可以发生隐式类型转换
  • 函数模板使用自动类型推导时不可以发生隐式类型转换
  • 使用显式指定类型时可以发生隐式类型转换

调用规则

  • 普通函数与函数模板都可以实现时,优先调用普通函数
  • 可以通过空模板参数列表来强制调用函数模板
  • 函数模板也可以发生重载
  • 如果函数模板可以产生更好的匹配,则优先调用函数模板
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
#include<iostream>
using namespace std;
void print(int a, int b){
  cout <<"普通函数调用"<<endl;
  cout << a <<" "<< b << endl;
}
template<typename T>
void print(T a, T b){
  cout <<"函数模板调用"<<endl;
  cout << a <<" "<< b << endl;
}
template<typename T>
void print(T a, T b, T c){
  cout <<"重载函数模板调用"<<endl;
  cout << a <<" "<< b <<" "<<c << endl;
}
int main(){
  int a = 10;
  int b = 10;
  print(a,b);
  // 更好匹配
  char c = '1';
  char d = '2';
  print(c,d);
  // 强制调用
  print<>(a,b);
  //重载调用
  int e = 10;
  print(a, b, e);
}

类模板

建立一个通用类,类中的成员数据类型可以用虚拟类型代表

语法

1
2
template<typename T>
/*类*/
  • template:声明创建模板
  • typename:表示后面的符号是一种数据类型
  • T:通用数据类型,名称可以替换,但通常是大写字母
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
#include<iostream>
using namespace std;

template<class NameType, class AgeType>
class person{
public:
  person(NameType name, AgeType age){
    this->age = age;
    this->name = name;
  }
  void show(){
    cout <<"name: "<<this->name<<endl;
    cout <<"age: "<<this->age<<endl;
  }
  NameType name;
  AgeType age;
};

int main(){
  // 使用模板参数列表指定类型
  person<string, int> p1("Jack", 18);
  p1.show();


}

与函数模板的区别

  • 类模板没有自动类型推导的方式

    不能写person p("a", 1);这种写法,只能显式指定

  • 类模板在模板参数列表中可以有默认参数

    template<class NameType, class AgeType = int>可以指定默认的数据类型

创建时机

  • 普通类中的成员函数一开始就可以创建
  • 类模板中的成员函数在调用时才创建,因为调用时才能确定数据类型

类模板对象做函数参数

传入方式:

  • 指定传入类型:直接显示对象的数据类型
  • 参数模板化:将对象中的参数变为模板进行传递
  • 整个类模板化:将这个对象类型模板化进行传递
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
#include<iostream>
using namespace std;

template<class NameType, class AgeType>
class person{
public:
  person(NameType name, AgeType age){
    this->age = age;
    this->name = name;
  }
  void show(){
    cout <<"name: "<<this->name<<endl;
    cout <<"age: "<<this->age<<endl;
  }
  NameType name;
  AgeType age;
};
// 指定传入类型
void printp1(person<string, int> &p){
  p.show();
}
// 参数模板化
template<class T1, class T2>
void printp2(person<T1, T2> &p){
  cout <<"T1: "<<typeid(T1).name()<<endl;
  cout <<"T2: "<<typeid(T2).name()<<endl;
  p.show();
}
// 整个类模板化
template<class T>
void printp3(T &p){
  cout <<"T: "<<typeid(T).name()<<endl;
  p.show();
}

int main(){
  person<string, int> p("Jack", 18);
  printp1(p);
  printp2(p);
  printp3(p);

}

类模板与继承

  • 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型

    1
    2
    3
    4
    5
    6
    7
    template<class T>
    class base{
      T m;
    };
    class son:public base<int>{
    
    };
  • 若想灵活指定父类中的T类型,子类也需要变成类模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    template<class T>
    class base{
      T m;
    };
    template<class T1, class T2>
    class son:public base<T2>{
      T1 obj;
    };
    int main(){
      son<int, char> s1; // 此时int为T1对应的obj的类型,char为T2对应父类中T的类型
    }

模板特化

有些特定数据类型需要一些特定的实现方式,需要具体化实现

函数模板特化

在需要特化的函数前添加template<>

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
#include<iostream>
using namespace std;
class person{
public:
  person(string name, int age){
    this->age = age;
    this->name = name;
  }

  string name;
  int age;
};
template<typename T>
bool cmp(T &a, T &b){
  if (a==b){
    return true;
  }
  else{
    return false;
  }
}
// 使用具体化person的版本实现代码,具体化优先调用
template<> bool cmp(person &p1, person &p2){
  if (p1.name==p2.name&&p1.age ==p2.age) return true;
  else return false;
}
int main(){
  person p1("Jack", 10);
  person p2("Jack", 10);
  if (cmp(p1,p2)){
    cout <<"a==b"<<endl;
  }
  else {
    cout <<"a!=b"<<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
30
#include<iostream>
using namespace std;

template<class NameType, class AgeType>
class person{
public:
  person(NameType name, AgeType age);
  void show();
  NameType name;
  AgeType age;
};

// 类外实现
template<class NameType, class AgeType>
person<NameType, AgeType>::person(NameType name, AgeType age){
  this->age = age;
  this->name = name;
}

template<class NameType, class AgeType>
void person<NameType, AgeType>::show(){
  cout <<"name: " <<this->name << endl;
  cout <<"age: "<<this->age<<endl;
}

int main(){
  person<string, int> p("Jack", 18);
  p.show();

}

类模板分文件编写

由于类模板中成员函数在调用时才创建,这导致了分文件编写时链接不到,有两个方法

  • 直接引入.cpp文件

  • 将声明和实现写在同一个文件中,改后缀名为.hpp

常用第二种

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
#pragma once
#include<iostream>
using namespace std;

template<class NameType, class AgeType>
class person{
public:
  person(NameType name, AgeType age);
  void show();
  NameType name;
  AgeType age;
};

// 类外实现
template<class NameType, class AgeType>
person<NameType, AgeType>::person(NameType name, AgeType age){
  this->age = age;
  this->name = name;
}

template<class NameType, class AgeType>
void person<NameType, AgeType>::show(){
  cout <<"name: " <<this->name << endl;
  cout <<"age: "<<this->age<<endl;
}

(hpp文件)

1
2
3
4
5
6
7
8
9
#include<iostream>
using namespace std;
#include "person.hpp"

int main(){
  person<string, int> p("Jack", 18);
  p.show();

}

(调用文件)

类模板与友元

使用场景

  • 全局函数类内实现:直接类内声明友元
  • 全局函数类外实现:需要提前让编译器知道全局函数的存在
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
#include<iostream>
using namespace std;
// 前向声明 类与模板函数
template<class NameType, class AgeType>
class person;
template<class NameType, class AgeType>
void printPerson2(person<NameType, AgeType> p);

template<class NameType, class AgeType>
class person{
  // 类内实现
  friend void printPerson(person<NameType, AgeType> p){
    cout <<"name: " <<p.name << endl;
    cout <<"age: "<<p.age<<endl;
  }
  // 类外实现 需要让编译器提前知道这个函数的存在
  friend void printPerson2<>(person<NameType, AgeType> p);
public:

  person(NameType name, AgeType age){
    this->age = age;
    this->name = name;
  }
private:
  NameType name;
  AgeType age;
};

// 类外实现
template<class NameType, class AgeType>
void printPerson2(person<NameType, AgeType> p){
    cout << "name: " << p.name << endl;
    cout << "age: " << p.age << endl;
}

int main(){
  person<string, int> p("Jack", 18);
  printPerson(p);
  printPerson2(p);
}

完美转发

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>
using namespace std;
void f(const string& s){
  cout <<"左值:"<<s<<endl;
}
void f(string&& s){
  cout <<"右值"<<s<<endl;
}

void g(const string& s){
  f(s);
}
void g(string&& s){
  f(s);
}

int main(){
  // 右值
  g(string("hello1"));

  // 左值
  string s1("hello2");
  g(s1);
}

这段代码,我们希望让右值调用右值的重载函数f,左值调用左值的重载函数f,但最后结果均调用了左值重载函数

这是因为在函数g中,s本身是一个局部变量,拥有地址,是一个左值

这时候我们需要进行类型转换

std::forward

我们当然可以使用static_cast<>()来进行转换,但是这里可以使用forward<>()函数进行转换,结合函数模板,使得代码更加方便易读

1
2
3
4
template<class T>
void g(T&& s){
  f(forward<T>(s));
}

这里在形参列表中T&& s被称为万能引用或转发引用

它既可以绑定左值也可以绑定右值,会在实例化时根据传入参数类型自动推导成对应类型

T的类型取决于传入的是左值还是右值

  • 左值:T被推导为实参类型的引用
  • 右值:T被推导为实参类型的非引用

forward函数实现了一个类型转换

  • 若传入实参是右值,则forward函数会返回一个右值引用
  • 若传入实参是左值,则函数模板T会被替换为该类型的引用,但此时会出现引用的引用(&&&),这时遵循引用折叠规则,得到类型的左值引用

即除了右值引用的右值引用被折叠为右值引用外,其余的只要有左值引用,就会被折叠为左值引用

文件操作

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);
}