红魔咖啡馆

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

0%

并发编程

多个任务的执行方式

  • 顺序:一个接着一个
  • 并行:同一时间执行多个任务(并行是并发的子集)
  • 并发:多个任务在同一个重叠时间段执行

OpenMP

使用#prgma预处理指令可以将代码转变为并行处理

此时需要在编译时添加参数-fopenmp

e.g. 计算\(\pi\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
using namespace std;
double cal(const long num_steps){
  double step = 1.0/num_steps;
  double sum = 0.0;
#pragma omp parallel for reduction(+:sum)
  for (long i = 0; i<num_steps;i++){
    double x = (i+0.5)*step;
    sum+=4.0/(1.0+x*x);
  }
  return sum*step;
}
int main(){
  cout << cal(10);
}

其中第六行语句的意思:

  • omp parallel for表示将for循环每次循环转化为并行计算

  • reduction表示用于reduce计算的变量与运算符

  • 局部变量x在每个线程中修改,不影响其他线程

  • sum是共享对象,不能在并行线程中直接修改,否则导致数据竞争

    sum需要reduce操作,其中reduction表明了需要reduce操作的变量与运算符

STL并行算法库

使用<algorithm><numeric>库中的函数,可以简单实现并行

在支持并行的算法函数中,首个参数可以选择执行策略:

  • std::execution::sequenced_policy:顺序执行策略(实例:seq)
  • std::execution::parallel_policy:并行执行策略(实例:par)
  • std::execution::parallel_unsequenced_policy:多线程加向量化策略(实例:par_seq)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <algorithm>
#include <iostream>
#include <execution>
#include <vector>
int main(){
  auto op = [](int v){std::cout << v << ";\n";};
  std::vector<int> numbers{1,2,3,4,5,6,7,8,9};
  std::cout <<"sequential for_each"<<std::endl;
  for_each(std::execution::seq, numbers.begin(), numbers.end(), op);
  std::cout <<"parallel for_each"<<std::endl;
  for_each(std::execution::par, numbers.begin(), numbers.end(), op);
  std::cout <<"parallel unseq for_each"<<std::endl;
  for_each(std::execution::par_unseq, numbers.begin(), numbers.end(), op);
}

以上没有对cout进行同步措施,会导致输出错误

线程

线程与进程

进程:程序执行过程,相互独立,操作系统为每一个进程分配一个执行控制块来控制进程

线程:进程中的执行过程,系统为每一个线程分配一个栈于线程控制块,线程间共享堆与全局数据

创建线程

使用<thread>类创建线程

thread(F&& f, Args&&... args);

  • 第一个参数为线程的入口函数(线程函数),可以是各种可调用的对象
  • 后面的Args为传递给可调用对象的实参
  • 若第一个参数为非静态成员函数,则后面的第一个参数为调用这个成员函数的类对象地址

thread的返回值会被忽略,故要使用按引用传递的参数,或存储在类对象的数据成员中

用该函数初始化后会创建一个新线程,执行线程函数

线程只能移动构造或移动赋值,防止多个线程对象代表一个执行线程

.join()

该函数会使得当前线程执行结束后等待其他线程回会合,避免出现提前结束导致出现错误

这种等待状态称为阻塞,若对应线程提前结束,则join函数也会直接返回

调用函数时,该线程必须是joinable的,意味着该线程必须是正在执行或执行完毕但未合并的线程,可以使用.joinable()判断是否可合并

不能被合并的情况:

  • 默认构造函数初始化的
  • join()完成的
  • detach()后的
  • 被移动的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<thread>
#include<iostream>
#include<array>

void greeting(int idx){
  printf("Hello from thread %d\n", idx);
}

int main(){
  std::array<std::thread, 10> workers;
  for (int i = 0;i<10;i++){
    workers[i] = std::thread(greeting, i);
  }
  for (auto &workers:workers){
    workers.join();
  }
}
1
2
3
4
5
6
7
8
9
10
Hello from thread 0
Hello from thread 1
Hello from thread 2
Hello from thread 3
Hello from thread 4
Hello from thread 5
Hello from thread 6
Hello from thread 7
Hello from thread 8
Hello from thread 9

Fork and join

可以使用线程实现并行

通过Fork and join模式:把要执行的数据分成n份,由n个线程并行处理自己的那部分数据,再进行汇总,n可以根据cpu核心设置

实现简单,但对于复杂计算,相同数据处理时间不同,导致有些线程很早处理完毕,有的需要更长时间结束

实现transform_reduce函数

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
#include
#include
#include
template
void transform_reduce(Iter first, Iter last, UnaryOp transform, BinaryOp reduce, Result& res){
  for (auto it=first;it numbers;
  numbers.reserve(N);
  for (int i = 0; i < N; ++i) {
    numbers.push_back((double)i/N);
  }
  auto transform = [](double x) { return x * x; };
  auto reduce = [](double x, double y) { return x + y; };
  const int N_thraeds = 8;
  std::vector workers;
  std::array subResults={};
  for (int i = 0; i::iterator,
        decltype(transform),
        decltype(reduce),double>,
        low, high, transform, reduce, std::ref(subResults[i])
      )
    );
  }
  double result = 0;
  for (int i = 0; i < N_thraeds; ++i) {
    workers[i].join();
    result = reduce(result,subResults[i]);
  }
  std::cout << "Result: " << result << std::endl;
  return 0;
}

Divide and conquer

使用分治将一个任务分成两个小任务,由两个线程完成,然后小任务再分成两个小任务,创建两个线程完成,直到不能分解

另外需要设计一个阈值,当线程过多时就不再创建,而是使用同步计算

为了让各个线程任务饱满,可以采用一半采用同步调用,一半分给另一个线程

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
#include<thread>
#include<vector>
#include<array>
#include<iostream>
template<typename Iter, typename UnaryOp, typename BinaryOp, typename Result, std::size_t MinDist = 10>
void transform_reduce(Iter first, Iter last, UnaryOp transform, BinaryOp reduce, Result& res){
  std::size_t distance = std::distance(first, last);
  if (distance==1){
    res = reduce(res, transform(*first));
  }
  else{
    std::size_t half = distance / 2;
    Iter middle = first+half;

    Result r_result = 0;
    Result l_result = 0;

    std::thread l_thread(
      transform_reduce<Iter, UnaryOp, BinaryOp, Result>,
      first, middle, transform, reduce, std::ref(l_result)
    );
    std::thread r_thread(
      transform_reduce<Iter, UnaryOp, BinaryOp, Result>,
      middle, last, transform, reduce, std::ref(r_result)
    );

    l_thread.join();
    r_thread.join();
    res = reduce(res, reduce(l_result, r_result));

  }
}

promise & future

这两个类提供了线程之间简单的通信机制

通过promise来设置值或异常,通过future来获得值或异常

一对promise&future共享一个shared state(共享状态),是一个类对象,提供了在保证线程安全的情况下,设置和获取状态与结果的方法,使用shared_ptr指向该状态

常用于发送-接受模式中

promise

promise类模板提供了按值、按引用传递数据,以及仅传递信号三种版本

对应的成员函数:

  • promise():构造函数,创建一个共享状态
  • promise(promise&& x):移动构造函数,将被移动对象x的共享状态转移过去
  • get_future():创建一个future对象,使用promise中的共享状态,只能调用一次
  • set_value():设置共享状态中的值,并设置为就绪状态
  • set_exception():设置共享状态的异常

注:为了保证一对一性,promise类删除了拷贝构造函数

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<future>
#include<iostream>
void compute_pi(const long num_steps, std::promise<double>&& promise){
  double step = 1.0/ num_steps;
  double sum = 0.0;
  for(long i = 0; i < num_steps; ++i){
    double x = (i + 0.5) * step;
    sum += 4.0 / (1.0 + x * x);
  }
  promise.set_value(sum * step);
}

void display(std::future<double>&& receiver)
{
  double pi = receiver.get();
  std::cout << "Computed value of Pi: " << pi << std::endl;
}

int main(){
  const int N_steps = 100000000;
  std::promise<double> promise;
  auto receiver = promise.get_future(); // 调用获得future对象后就没什么用了
  std::thread th1(compute_pi, N_steps, std::move(promise)); // 将promose对象转换为xvalue,作为实参换入
  std::thread th2(display, std::move(receiver));
  th1.join();
  th2.join();
}

future

future中的方法主要用于获取状态与结果

  • get():获得结果,若对应结果没有处在就绪状态,那么这个调用会阻塞直到就绪
  • wait():等待状态就绪,之后返回,此时调用get可以直接获得结果
  • wait_for():等待指定时长,返回状态码
  • wait_until():等待到指定时刻,返回状态码
  • share():

状态码包括:

  • ready:共享状态就绪
  • timeout:超时
  • deferred:共享状态中有延缓执行的函数(显式请求中才会调用的函数)

若需要让多个线程获得结果,需要shared_future,构建方法如下:

  • 定义对象,使用promise的get_future()作为初始化参数
  • 在原有future上调用方法.share(),此时原有不再使用
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
#include<future>
#include<iostream>
#include<sstream>

void compute_pi(const long num_steps, std::promise<double>&& promise){
  double step = 1.0/ num_steps;
  double sum = 0.0;
  for(long i = 0; i < num_steps; ++i){
    double x = (i + 0.5) * step;
    sum += 4.0 / (1.0 + x * x);
  }
  promise.set_value(sum * step);
}
void display(std::shared_future<double> reveiver){
  static std::mutex mu;
  double pi = reveiver.get();
  printf("Computed value of Pi: %f\n", pi);
}

int main(){
  const int N_steps = 100000000;
  std::promise<double> promise;
  std::shared_future<double> shared_receiver(promise.get_future());
  std::thread th1(compute_pi, N_steps, std::move(promise));
  // 使用shared_future可以让多个线程共享同一个future对象
  std::thread th2(display, shared_receiver);
  std::thread th3(display, shared_receiver);
  th1.join();
  th2.join();
  th3.join();
}

互斥和锁

多线程中,当多个线程队同一个对象进行访问,并且至少有一个在写数据时,会产生数据竞争

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<array>
#include<thread>
#include<iostream>

void inc(int &res){
  for(int i = 0; i < 1000000; ++i){
    res++;
  }
}

int main(){
  int counter = 0;
  std::thread th1(inc, std::ref(counter));
  std::thread th2(inc, std::ref(counter));

  th1.join();
  th2.join();
  std::cout << "Counter: " << counter << std::endl;
}

该程序创建了两个线程进行累加,但最后得到的结果都不是正确结果,且每次都不一样

正常

这是一个正常的执行过程

错误执行

若顺序是这样结果就不对了

互斥量

C++中所有运算默认为非原子操作,即该线程操作随时会被终端或被其他线程访问

mutex提供了一种同步机制,实现多个线程的安全访问

C++11开始,提供了std::mutex来实现互斥原语,是最常用的同步原语之一

有三个主要函数:

  • lock():锁定互斥量,即获得互斥量,若已经被其他线程锁定,则会进入阻塞直到被解锁
  • try_lock():尝试锁定互斥量,若成功锁定则返回true,否则返回false(不保证被其他线程锁定)
  • unlock():解锁,释放互斥量
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<array>
#include<thread>
#include<iostream>
#include<mutex>

class counter{
public:
  void inc(){
    // 对要修改的资源进行保护
    // 保证同一时间只有一个线程可以修改资源
    // 即要么获得互斥量的锁,要么等待
    counter_mutex.lock();
    m_count++; // 临界区
    counter_mutex.unlock();
  }
  int get(){
    int temp;
    counter_mutex.lock();
    temp = m_count; // 临界区
    counter_mutex.unlock();
    return temp;
  }
private:
  int m_count = 0;
  std::mutex counter_mutex;
};
int main(){
  counter c;
  auto increase = [](counter &c){
    for(int i = 0; i < 1000000; ++i){
      c.inc();
    }
  };
  std::thread th1(increase, std::ref(c));
  std::thread th2(increase, std::ref(c));

  th1.join();
  th2.join();
  std::cout << "Counter: " << c.get() << std::endl;
}

其中,被mutex保护的操作区域叫做临界区

为了避免未释放互斥量的问题,我们通常结合以下对象使用

  • lock_guard():可以通过调用上锁后自动解锁,防止忘记解锁以及出现异常离开作用域时没有解锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class counter{
public:
  void inc(){
    std::lock_guard lock(counter_mutex);
    m_count++; // 临界区
  }
  int get(){
    std::lock_guard lock(counter_mutex);
    return m_count;
  }
private:
  int m_count = 0;
  std::mutex counter_mutex;
};
  • unique_lock():更加灵活,可以随时指定锁定与解锁
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
#include<array>
#include<thread>
#include<iostream>
#include<mutex>

class counter{
public:
  void inc(int n){
    std::unique_lock lock(counter_mutex, std::defer_lock);
    while(n--){
      lock.lock(); // 对要修改的资源进行保护
      m_count++; // 临界区
      lock.unlock();
    }
  }
  int get(){
    std::unique_lock lock(counter_mutex);
    return m_count; 
  }
private:
  int m_count = 0;
  std::mutex counter_mutex;
};
int main(){
  counter c;
  auto increase = [](counter &c){
    c.inc(1000000);
  };
  std::thread th1(increase, std::ref(c));
  std::thread th2(increase, std::ref(c));

  th1.join();
  th2.join();
  std::cout << "Counter: " << c.get() << std::endl;
}
  • timed_mutex:有等待时长的互斥
    • try_lock_for():指定时长内获得mutex的所有权
    • try_lock_until():指定时刻前获得mutex的所有权

​ 返回true表明获得mutex,否则返回false

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
#include<mutex>
#include<thread>
#include<iostream>
#include<chrono>
#include<sstream>

using namespace std::chrono_literals;

class TryDemo{
public:
  void print(){
    for (int i = 0; i<10; i++){
      std::unique_lock lock(m_mutex, std::defer_lock);
      if (lock.try_lock_for(100ms)) {
        {
        std::lock_guard guard(cout_mutex);
        std::cout <<"["<<std::this_thread::get_id()<<"] "
                  << "Got the lock, printing " << i << std::endl;
        }
        std::this_thread::sleep_for(100ms);
      } // 尝试获取锁,
      else{
        std::lock_guard guard(cout_mutex);
        std::cout <<"["<<std::this_thread::get_id()<<"] "
                  << "Failed to get the lock, skipping " << i << std::endl;
        std::this_thread::sleep_for(100ms);
       }
    }
  }
private:
  std::timed_mutex m_mutex;
  std::mutex cout_mutex; 
  int m_count = 0;
};

int main(){
  TryDemo demo;
  auto print = [](TryDemo &demo){
    demo.print();
  };
  std::thread th1(print, std::ref(demo));
  std::thread th2(print, std::ref(demo));
  th1.join();
  th2.join();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[2] Got the lock, printing 0
[3] Failed to get the lock, skipping 0
[2] Got the lock, printing 1
[3] Failed to get the lock, skipping 1
[2] Got the lock, printing 2
[3] Failed to get the lock, skipping 2
[2] Got the lock, printing 3
[3] Failed to get the lock, skipping 3
[2] Got the lock, printing 4
[3] Failed to get the lock, skipping 4
[2] Got the lock, printing 5
[2] Got the lock, printing 6
[3] Failed to get the lock, skipping 5        
[3] Got the lock, printing 6
[3] Got the lock, printing 7
[2] Failed to get the lock, skipping 7        
[2] Got the lock, printing 8
[3] Failed to get the lock, skipping 8
[2] Got the lock, printing 9
[3] Failed to get the lock, skipping 9

死锁

单一线程中多次锁定同一个互斥量会导致死锁

常见死锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include
#include
#include

class dosome{
public:
  int f1(){
    std::lock_guard lock(mtx);
    count*=2;
    f2();
    return count;

  }
  void f2(){
    std::lock_guard(mtx); // 已经在外层被锁定过了
    count++;
  }
private:
  std::mutex mtx;
  int count=0;
};

此时所在线程会进入阻塞状态

我们使用`recursive的mutx优化

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

class dosome{
public:
  int f1(){
    std::lock_guard lock(mtx);
    count*=2;
    f2();
    return count;

  }
  void f2(){
    std::lock_guard(mtx); // 已经在外层被锁定过了
    count++;
  }
private:
  std::recursive_mutex mtx;
  int count=0;
};

当被锁定并获得互斥量后,当前线程再调用lock后仍会成功而不会阻塞,直到调用相同数量的unlock()才会被释放

多线程中

多线程中,导致死锁原因较多

  1. 锁定一个互斥量后没有释放,导致后面的线程获取该互斥量时进入阻塞状态

解决方法:使用RAII,即使用lock_guard等自动对象,在离开作用域后自动释放互斥量

  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
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
#include<iostream>
#include<mutex>
#include<thread>
#include <chrono>
using namespace std::chrono_literals;

class dosome{
public:
  void f1(){
    {
      std::lock_guard lock1(mtx1);
      std::lock_guard lock2(cout_mtx);
      count++;
      std::cout << "count: "<<count<<std::endl;
    }
    std::this_thread::sleep_for(1ms);
   

  }
  void f2(){
    {
      std::lock_guard lock1(cout_mtx);
      std::lock_guard lock2(mtx1); 
      count--;
      std::cout << "count: "<<count<<std::endl;
    }
    std::this_thread::sleep_for(1ms);
  }
  void calc(int n){
    for (int i = 0;i<n;i++){
      if (n%2){
        f2();
      }
      else{
        f1();
      }
    }
  }
  
private:
  std::mutex mtx1;
  std::mutex cout_mtx;
  int count=0;
};
int main(){
  dosome d;
  const int N1 = 10000;
  const int N2 = 10001;
  std::thread th1(dosome::calc, &d, N1);
  std::thread th2(dosome::calc, &d, N2);
  th1.join();
  th2.join();
}

这里f1与f2都对两个互斥量进行锁定,顺序不同

主函数设置两个线程分别为10000次,执行f1,10001次,执行f2

此时运行程序会在一定时间发生死锁:

线程1在某个时刻获得了mtx1,线程2在下一时刻获得了cout_mtx

接下来线程1需要获得cout_mtx,但已被2占有,需要进入阻塞等待资源释放

同时线程2需要获得mtx1,但已被1占有,需要进入阻塞等待资源释放

故整个程序进入阻塞,无法执行

阻塞

活锁

对于上述问题,解决方法之一是使用timed_mutex+try_lock_for

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
#include<iostream>
#include<mutex>
#include<thread>
#include <chrono>
using namespace std::chrono_literals;

class dosome{
public:
  void f1(){
    while(1){
      std::unique_lock lock1(mtx1, std::defer_lock);
      std::unique_lock lock2(cout_mtx, std::defer_lock);
      if (!lock1.try_lock_for(100ms)) continue;
      if (!lock2.try_lock_for(100ms)) continue;
      count++;
      std::cout << "count: "<<count<<std::endl;
      break;
    }
  }
  void f2(){
    while(1){
      std::unique_lock lock1(cout_mtx, std::defer_lock);
      std::unique_lock lock2(mtx1, std::defer_lock);
      if (!lock1.try_lock_for(100ms)) continue;
      if (!lock2.try_lock_for(100ms)) continue;
      count--;
      std::cout << "count: "<<count<<std::endl;
      break;
    }
  
  }
  void calc(int n){
    for (int i = 0;i<n;i++){
      if (n%2){
        f2();
      }
      else{
        f1();
      }
    }
  }
  
private:
  std::timed_mutex mtx1;
  std::timed_mutex cout_mtx;
  int count=0;
};
int main(){
  dosome d;
  const int N1 = 10000;
  const int N2 = 10001;
  std::thread th1(dosome::calc, &d, N1);
  std::thread th2(dosome::calc, &d, N2);
  th1.join();
  th2.join();
}

但是运行起来会出现一顿一顿的情况

此时程序进入了一种活锁的状态,即

线程1锁定mtx1,线程2锁定cout_mtx

线程1试图锁定cout_mtx1失败,进入阻塞,超时后释放mtx1进入下个循环,

线程2试图锁定mtx1失败,进入阻塞,超时后释放cout_mtx进入下个循环

以此类推

防止该情况的发生,需要注意调用次序,注意保证按照顺序获得互斥量

更加方便的方法是使用std::lock()函数或scope_lock类统一管理多个互斥量

lock函数

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>
#include<mutex>
#include<thread>
#include <chrono>
using namespace std::chrono_literals;

class dosome{
public:
  void f1(){
    {
      std::lock(mtx1, cout_mtx);
      std::lock_guard lock1(mtx1, std::adopt_lock);
      std::lock_guard lock2(cout_mtx, std::adopt_lock);
      count++;
      std::cout << "count: "<<count<<std::endl;
    }

  }
  void f2(){
    {
      std::lock(mtx1, cout_mtx);
      std::lock_guard lock1(cout_mtx, std::adopt_lock);
      std::lock_guard lock2(mtx1, std::adopt_lock); 
      count--;
      std::cout << "count: "<<count<<std::endl;
    }
  }
  void calc(int n){
    for (int i = 0;i<n;i++){
      if (n%2){
        f2();
      }
      else{
        f1();
      }
    }
  }
  
private:
  std::mutex mtx1;
  std::mutex cout_mtx;
  int count=0;
};
int main(){
  dosome d;
  const int N1 = 10000;
  const int N2 = 10001;
  std::thread th1(dosome::calc, &d, N1);
  std::thread th2(dosome::calc, &d, N2);
  th1.join();
  th2.join();
}

此时lock函数会接管下面的两个lock_guard,当他们被销毁时,析构函数中会自动释放两个互斥量

即使传入的两个互斥量顺序不同,lock的特殊算法也可避免死锁的发生

scope_lock类

该类更加方便,使用多个类进行初始化

构造时可以自动调用lock函数,析构时自动释放

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
#include<iostream>
#include<mutex>
#include<thread>
#include <chrono>
using namespace std::chrono_literals;

class dosome{
public:
  void f1(){
    {
      std::scoped_lock lock(mtx1, cout_mtx);
      count++;
      std::cout << "count: "<<count<<std::endl;
    }

  }
  void f2(){
    {
      std::scoped_lock lock(cout_mtx, mtx1);
      count--;
      std::cout << "count: "<<count<<std::endl;
    }
  }
  void calc(int n){
    for (int i = 0;i<n;i++){
      if (n%2){
        f2();
      }
      else{
        f1();
      }
    }
  }
  
private:
  std::mutex mtx1;
  std::mutex cout_mtx;
  int count=0;
};
int main(){
  dosome d;
  const int N1 = 10000;
  const int N2 = 10001;
  std::thread th1(dosome::calc, &d, N1);
  std::thread th2(dosome::calc, &d, N2);
  th1.join();
  th2.join();
}

读写锁

若存在三个线程读取数据、一个线程写入数据,若使用mutex互斥量进行操作,读写步骤几乎是排队进行的,导致了程序运行效率底下

我们又知道,在没有数据写入的情况下,多个线程同时读取数据是不会发生数据竞争的,不必独占资源

故我们可以规定:

  • 一个线程读取数据时,允许其他线程同时读取数据,不允许其他线程写数据
  • 一个线程写数据时,不允许其他线程读写数据

这样可以显著提高效率

C++14以上,这样的读写锁的功能由shared_mutex类、shared_timed_mutex提供

该类提供两个模式和两个锁:

  • 独占模式:仅有一个线程可以占有互斥量
  • 独占锁:独占模式下,当一个线程获得独占锁,任何其他线程都不能获得独占锁和共享锁
  • 共享模式:可以有多个线程分享互斥量
  • 共享锁:共享模式下,当一个线程获得共享锁,其他线程可获得共享锁,不能获得独占锁

读写锁广泛应用于读多写少的场景下,是一种重要的同步原语

条件变量

条件变量是并发程序中经常用到的同步原语,可以实现在线程间发生通知以实现线程间的同步

工作流

如提供两个线程,用于读取与写入数据,读取要在写入之后

  1. 接收线程获得互斥量mutex,使用条件变量来等待发送线程的通知,等待时释放mutex
  2. 发送线程获得mutex,并对共享数据进行写入,完成后释放mutex
  3. 发送线程使用条件变量发送通知,接收线程接收到通知后重新获得mutex,结束阻塞继续运行
  4. 接收线程读取处理共享数据,处理完毕后释放mutex

使用

标准库conditional_variable实现条件变量原语,包含以下函数:

等待:

  • wait:传入锁的引用以及解除阻塞的条件。进入等待状态时释放锁,结束后获得锁,且都是原子操作
  • wait_for:
  • wait_until:

通知:

  • notify_one:
  • notify_all:

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>
#include<thread>
#include<mutex>
#include <chrono>
#include <condition_variable>
using namespace std::chrono_literals;
class channel{
public:
  void getData(){
    auto tid = std::this_thread::get_id();
    std::unique_lock lck(mutex_);
    std::cout<<"接收方["<<tid<<"]等待数据"<<std::endl;
    condVar.wait(lck);
    std::cout<<"接收方["<<tid<<"]得到数据"<<sharedData<<std::endl;
    sharedData.clear();
  }
  void setData(){
    static int id = 1;
    std::stringstream ss;
    ss << "hello #"<<id;
    {
      std::unique_lock lck(mutex_);
      sharedData = ss.str();
      std::cout  <<"\n发送方:第"<<id<<"条数据写入完毕"<<std::endl;
      id++;
    }
    condVar.notify_one();
    std::this_thread::sleep_for(1ms);
  }
private:
  std::mutex mutex_;
  std::condition_variable condVar;
  std::string sharedData;
};

int main(){
  channel c;
  std::thread write_th(channel::setData, &c);
  std::thread read_th(channel::getData, &c);

  read_th.join();
  write_th.join();
}

丢失唤醒与虚假唤醒

发现程序运行时,有时会出现死锁的状态

从输出发现,写线程在读线程进入等待前就准备好了数据并发出通知,而读线程进入状态后才

准备接受通知,于是错过了通知,一直等待,发生丢失唤醒

除了丢失唤醒,等待函数还会出现虚假唤醒:即等待线程在条件未满足的情况下被唤醒了

原因与底层硬件软件的异常有关

因此我们最好使用wait函数的重载版本,设置一个等待条件

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
#include<iostream>
#include<thread>
#include<mutex>
#include <chrono>
#include <condition_variable>
using namespace std::chrono_literals;
class channel{
public:
  void getData(){
    auto tid = std::this_thread::get_id();
    std::unique_lock lck(mutex_);
    std::cout<<"接收方["<<tid<<"]等待数据"<<std::endl;
    condVar.wait(lck, [this]{return !sharedData.empty();});
    if (sharedData.empty()){
      std::cout<<"接收方["<<tid<<"] 虚假唤醒"<<std::endl;
    }
    else std::cout<<"接收方["<<tid<<"]得到数据"<<sharedData<<std::endl;
    sharedData.clear();
  }
  void setData(){
    static int id = 1;
    std::stringstream ss;
    ss << "hello #"<<id;
    {
      std::unique_lock lck(mutex_);
      sharedData = ss.str();
      std::cout  <<"\n发送方:第"<<id<<"条数据写入完毕"<<std::endl;
      id++;
    }
    condVar.notify_one();
    std::this_thread::sleep_for(1ms);
  }
private:
  std::mutex mutex_;
  std::condition_variable condVar;
  std::string sharedData;
};

int main(){
  channel c;
  std::thread write_th(channel::setData, &c);
  std::thread read_th(channel::getData, &c);

  read_th.join();
  write_th.join();
}

信号量

实现对有限资源的并发访问控制,内部有一个计数器,表示资源的可用数量

线程通过信号量来获得对资源的访问许可,若大于0,可以进行访问,计数器减一

信号量要与互斥量结合使用,用信号量来限制并发访问数量,用互斥量实现对具体数据同步

C++标准库中,有一个类模板counting_semaphore,其中参数LeastMaxValue规定了计数最大值不小于该值,还有他的特化版本binary_semaphore,值设定为1

函数:

  • release:将计数值+1,是原子操作
  • acquire:若计数值大于1,则将计数值-1,如果为0,则该函数阻塞直到计数值大于1
  • try_acquire:成功时将计数值-1,返回true
  • try_acquire_for:阻塞时最多等待的时间
  • try_acquire_until:阻塞时最多等待到某个时刻

例:循环读写buffer

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
110
111
112
113
114
#include <iostream>
#include <thread>
#include <chrono>
#include <random>
#include <array>
#include <semaphore> 
#include <syncstream>

// 简单的随机辅助类
class Random {
public:
    // 构造 [l, r] 的整数随机
    Random(int l, int r)
      : eng(std::random_device{}()),
        dist(l, r)
    {}

    int operator()() {
        return dist(eng);
    }
private:
    std::mt19937 eng;
    std::uniform_int_distribution<int> dist;
};

// RWBuffer 模板
template <size_t BuffSize = 6>
class RWBuffer {
public:
    RWBuffer() {
        buff.fill(' ');    // 填充空格
    }

    // 模拟生产者准备数据
    char prepareData() {
        std::this_thread::sleep_for(std::chrono::milliseconds(rgen()));
        return chargen();
    }

    // 模拟消费者处理数据
    void processData(char /*ch*/) {
        std::this_thread::sleep_for(std::chrono::milliseconds(rgen2()));
    }

    // 原子打印缓存区
    void printBuffer() {
        std::osyncstream sync(std::cout);
        sync << "BUFFER: |";
        for (auto c : buff) {
            sync << c << "|";
        }
        sync << "\n";
    }

    // 写线程函数
    void writeToBuffer(size_t iterations) {

        std::osyncstream(std::cout) << "写线程就绪.\n";
        // 总共写 iterations * BuffSize 次
        for (size_t i = 0; i < iterations * BuffSize; i++) {
            char ch = prepareData();
            sem_writer.acquire();        // 等待有空位
            size_t idx = i % BuffSize;
            buff[idx] = ch;

            std::osyncstream(std::cout) << "写线程:写入 '" << ch << "' 到位置 " << idx << "\t";
            printBuffer();

            sem_reader.release();        // 通知读线程
        }
    }

    // 读线程函数
    void readFromBuffer(size_t iterations) {

        std::osyncstream(std::cout) << "读线程就绪.\n";
        for (size_t i = 0; i < iterations * BuffSize; i++) {
            sem_reader.acquire();        // 等待有数据
            size_t idx = i % BuffSize;
            char ch = buff[idx];
            processData(ch);
            buff[idx] = ' ';

            std::osyncstream(std::cout) << "读线程:读取 '" << ch << "' 从位置 " << idx << "\t";
            printBuffer();

            sem_writer.release();        // 通知写线程
        }
    }

private:
    Random rgen{500,1000};
    Random rgen2{800,1500};
    Random chargen{'A','Z'};
    std::array<char, BuffSize> buff;
    std::counting_semaphore<BuffSize> sem_reader{0};
    std::counting_semaphore<BuffSize> sem_writer{BuffSize};
};

int main(){
    constexpr size_t N = 6;
    constexpr size_t ITERS = 3;

    RWBuffer<N> buffer;

    // 启动写线程和读线程
    std::thread writer(&RWBuffer<N>::writeToBuffer, &buffer, ITERS);
    std::thread reader(&RWBuffer<N>::readFromBuffer, &buffer, ITERS);

    writer.join();
    reader.join();

    return 0;
}

异步任务

将函数调用交给另一个线程异步执行,原有线程继续执行代码,在需要时再去获得执行结果

C++中提供了async与packaged_task进行异步任务

异步

async

语法:

std::future<返回类型> async(std::launch policy, F& f, Args&&... args);

std::future<返回类型> async(F& f, Args&&... args);(默认策略)

执行策略(用整型不同bit位表示):

  • std::launch::async:异步调用,00000001,相当于新建或使用线程池中的线程并在线程中调用函数
  • std::launch::deferred:推迟调用,00000010,async返回的future对象用于存储函数指针、对象、实参拷贝,调用get函数时会同步调用存储的这个函数
  • std::launch::async|std::launch::deferred:组合,00000011,同步还是异步执行根据具体实现决定

packaged_task

方法

  • get_future():获得一个future对象,封装函数的返回结果存储在future对象的共享状态中
  • void operator()(ArgTypes... args):用于调用封装的函数,将返回结果存储在future中,此时会同步执行封装函数
  • reset():用于重复使用packaged_task,清空状态获得一个新的future对象
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
#include<iostream>
#include<thread>
#include<random>
#include<vector>
#include<future>
#include<syncstream>
// 简单的随机辅助类
class Random {
public:
    // 构造 [l, r] 的整数随机
    Random(int l, int r)
      : eng(std::random_device{}()),
        dist(l, r)
    {}

    int operator()() {
        return dist(eng);
    }
private:
    std::mt19937 eng;
    std::uniform_int_distribution<int> dist;
};
float Compute(std::vector<float> &v){
  std::this_thread::sleep_for(std::chrono::seconds(2));
  auto r = std::accumulate(v.begin(), v.end(), 0.0f);
  std::osyncstream(std::cout)<<"执行任务的线程id="<<std::this_thread::get_id()<<std::endl;
  return r;
}
int main(){
  const int RN_MAX = 10000;
  Random rgen(0, RN_MAX);
  std::vector<float> numbers(100, 0.0f);
  std::generate(numbers.begin(), numbers.end(), [&]{return float(rgen()/RN_MAX);});
  std::cout <<"创建任务的线程id:"<<std::this_thread::get_id()<<std::endl;
  std::packaged_task<float(std::vector<float>&)> task(Compute);

  std::cout<<"直接调用"<<std::endl;
  std::future<float> result = task.get_future();
  task(numbers);
  std::cout <<"result="<<result.get()<<std::endl;

  task.reset();

  std::cout<<"线程调用"<<std::endl;
  result = task.get_future();
  std::thread t(std::move(task), std::ref(numbers));
  std::cout<<"result="<<result.get()<<std::endl;
  t.join();
  return 0;
}

线程屏障

线程屏障提供了一个计数器,每个参与任务的线程完成自己的任务后计数器-1,而需要同步的线程可以阻塞到计数器为0时再执行后面的代码

latch

latch是一次性的计数,参数为作为计数器的初始值

  • count_down(n=1):将计数器减去传入的数值,默认为1,是原子操作
  • wait():将线程阻塞到计数器为0后释放
  • try_wait():检查计数器是否为0,返回true为0,返回false不一定为0
  • arrive_and_wait(n=1):执行了count_down和wait

e.g.模拟paobu

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>
#include<thread>
#include<latch>
#include<random>
#include<chrono>

struct Runner{
  std::string name;
  int time = 0;
  void run(std::latch& start, std::latch& end){
    start.wait(); // 等待发令枪响
    // 模拟跑步过程
    auto start_time = std::chrono::system_clock::now();
    std::mt19937 rng(std::random_device{}());
    std::uniform_int_distribution<unsigned int> uniform_dist(0, 2000);
    std::this_thread::sleep_for(std::chrono::milliseconds(9600+uniform_dist(rng)));
    auto end_time = std::chrono::system_clock::now();
    time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();
    end.count_down(1); // 跑完,计数器-1
  } 
  bool operator<(const Runner& other) const {
    return time < other.time;
  }
};

int main(){
  std::vector<Runner> runners = {
    {"Alice"}, {"Bob"}, {"Charlie"}, {"Diana"}, {"Eve"}
  };
  // 等待阶段
  const int runner_count = runners.size();
  std::latch start(1); // 代表发令枪,等到为0时执行后面代码
  std::latch finish(runner_count); // 代表完赛选手数量,有一个选手冲线则-1,直到为0时比赛结束
  std::vector<std::thread> threads;
  for (int i = 0; i<runner_count; ++i) {
    threads.emplace_back(&Runner::run, &runners[i], std::ref(start), std::ref(finish));
  }
  std::cout <<"发令枪响"<< std::endl;
  start.count_down(); // 发令枪响,所有选手开始跑

  finish.wait(); // 等待所有选手跑完,阻塞
  // 结束后统计成绩
  std::cout <<"比赛结束"<< std::endl;
  std::sort(runners.begin(), runners.end());
  for (const auto& runner : runners) {
    std::cout << runner.name << " : " << float(runner.time)/1000 << "秒" << std::endl;
  }
  for(auto& thread : threads) {

      thread.join();
  }
}
1
2
3
4
5
6
7
发令枪响
比赛结束
Eve : 9.946秒
Bob : 10.034秒
Diana : 10.092秒
Alice : 10.842秒
Charlie : 11.621秒

barrier

barrier每次计数到0时,都会把初始值恢复,进行下一轮计数

barrier是个类模板,参数为计数器初始值

  • arrive(n=1):将计数器减去指定参数n

  • wait():等待计数器为0

  • arrive_and_wait(n=1):前两个结合

  • arrive_and_drop(n=1):除了将本次-n,还将下一次的初始值-n

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
#include<iostream>
#include<thread>
#include<barrier>
#include<random>
#include<chrono>
#include<vector>
class participant {
public:
  std::string name;
  participant(const std::string& name) : name(name) {}
  template <typename BarrierType>
  void run(BarrierType& finish){
    // 随机数确定是否能存货
    std::mt19937 rng(std::random_device{}());
    std::uniform_int_distribution<unsigned int> uniform_dist(1000, 2000);
    for (int i = 0; i<5;i++){
      auto rnd = uniform_dist(rng);
      std::this_thread::sleep_for(std::chrono::milliseconds(rnd));
      alive = rnd%3;
      if (alive){
        finish.arrive_and_wait(); // 到达终点,等待其他选手
      }
      else{
        finish.arrive_and_drop(); // 选手掉队,直接结束
        break;
      }
    }
  }
  bool is_alive() const {
    return alive;
  }
private:
  bool alive = true; // 选手是否存活
};

int main(){
  std::vector<participant> participants = {
    {"Alice"}, {"Bob"}, {"Charlie"}, {"Diana"}, {"Eve"}
  };
  const int participant_count = participants.size();
  int round = 0;
  // barrier每轮结束时的回调函数
  auto on_complete = [&participants, &round](){
    round++;
    std::cout <<"第"<< round << "轮幸存者:"<< std::endl;
    int count = 0;
    for (auto & p : participants) {
      if (p.is_alive()) {
        std::cout << p.name << " ";
        count++;
      }
    }
    if (count==0){
      std::cout << "没有幸存者" << std::endl;
    }
    std::cout << std::endl;

  };
  std::barrier finish(participant_count, on_complete); // 参赛者数量为初始值,oncomplete为每轮的回调
  std::vector<std::thread> threads;
  for (auto &p: participants) {
    threads.emplace_back([&p, &finish](){p.run(finish);});
  }
  for (auto &thread : threads) {
    thread.join(); // 等待所有线程结束
  }
  return 0;
}

注:对计数器进行-n操作时,传入的n的值不能大于当前计数值,否则会导致未定义错误

原子操作

原子操作是不能被分割或中断的操作,其他线程只能看到开始与结束的状态

类型

  • Load 加载
  • Store 存储
  • Read Modify Write 读取-修改-写入(RMW)

atomic_flag

它只有true和false两个值,true表示标志被设置,false表示标志被清除

初始化可以使用ATOMIC_FLAG_INIT宏,也可以使用默认构造函数(C++20后)

  • clear():清除标志,是一个原子写操作,函数有一个参数memory_order,指定内存顺序,默认为memory order sequentially consistent

  • test_and_set():将标志设置为true,并返回原有值,是一个RMW操作

(以下为C++20新增)

  • test():读取数据
  • wait(oldvalue):等待条件达成
  • notify_one():通知其中一个线程结束等待
  • notify_all():通知所有线程结束等待

自旋锁

自旋:持续监控变量以等待发生变化的过程

自旋锁消耗资源较高,常用于临界区代码耗时很少的场合

1
2
3
4
5
6
7
8
9
10
11
class SpinLock{
public:
    void lock(){
        while (flag.test_and_set(std::memory_order_acquire)){}
    }
    void unlock(){
        flag.clear(std::memory_order_release);
    }
private:
    std::atomic_flag flag{ATOMIC_FLAG_INIT};
};

例:

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
#include<iostream>
#include<atomic>
#include<thread>
#include<assert.h>

class SpinLock{
public:
    void lock(){
        while (flag.test_and_set(std::memory_order_acquire)){}
    }
    void unlock(){
        flag.clear(std::memory_order_release);
    }
private:
    std::atomic_flag flag{ATOMIC_FLAG_INIT};
};
int main(){
  SpinLock spin;
  int count = 0;
  auto inc = [&](){
    for (int i = 0; i<1000;i++){
      std::lock_guard<SpinLock> lock(spin);
      ++count;
    }
  };
  std::thread t1(inc);
  std::thread t2(inc);
  t1.join();
  t2.join();
  std::cout << "count = " << count << std::endl;
}

这里由于自旋锁包含lock与unlock函数,符合C++中的基本锁类型,故可以使用lock_guard自动加锁解锁

实现了一个一次生产者与消费者的模拟

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<iostream>
#include<atomic>
#include<thread>
#include<assert.h>

struct Channel{
  void getData(){
    std::cout <<"[Consumer] wait for data ready\n";
    dataReady.wait(false); // 等待数据准备好
    std::cout<<"[Consumer] data is:"<<data<<std::endl;
    dataReady.clear();
  }
  void setData(const std::string& v){
    std::this_thread::sleep_for(std::chrono::milliseconds(2000)); // 模拟数据准备时间
    std::cout<< "'[Producer] write data\n";
    data = v;
    dataReady.test_and_set();
    dataReady.notify_one();
  }
private:
  std::atomic_flag dataReady = ATOMIC_FLAG_INIT; // 用于标记数据是否准备好
  std::string data; // 存储数据
  
};

int main(){
  Channel channel;
  std::thread consumer(Channel::getData, &channel);
  std::thread producer(Channel::setData, &channel, "Hello, World!");

  consumer.join();
  producer.join();
}

特化版本

atomic除了主模板还有针对指针、智能指针与数据类型的特化版本

atomic

其中主模板中的T要求是可平凡拷贝的

有无锁的判断

我们可以通过atomic模板提供的is_lock_free()函数判断有无锁,若无锁(通过CPU的原子指令)则函数返回true,否则返回false

  • 基本类型和指针都是无锁的
  • 自定义类型与类型大小与对齐有关:类型大小是\(2^n\)且小于最大对齐数(16字节)

主要的原子操作

加载操作:用于原子地获得当前变量的值

1
2
T load(std::memory_order order = std::memory_order_seq_cst) const noexcept;
operator T() const noexcept;

存储操作:用于将变量原子地修改为要更新的值

1
2
void store(T desired, std::memory_order order = std::memory_order_seq_cst) noexcept;
T operator=(T desired) noexcept;

CAS(比较与交换函数):

通过compare_exchange函数完成

1
2
bool compare_exchange_weak(T& expected, T desired, std::memory_order success, std::memory_order failure) noexcept;
bool compare_exchange_strong(T& expected, T desired, std::memory_order success, std::memory_order failure) noexcept;

符合条件的CAS会直接使用CPU硬件指令来实现

其中weak版可能有虚假失败的情况

例:CAS循环模式

CAS是实现无锁算法的重要方式,经常使用CAS循环实现对原子值更改

1
2
3
4
5
6
7
8
9
int fetch_multiply(std::atomic<int> &value, int multiplier){
    int oldValue = value.load();
    int desire;
    do{
        desired = oldValue*multiplier;
    }while(!value.compare_exchange_strong(oldValuem desired));
    
    return oldValue;
}

原理:加载当前值到oldValue,使用其来计算乘积并存储到本地变量desire,调用compare_exchange以原子方式更新value

若原value等于oldValue,说明没有并发线程修改过value,则更新为desired

若不等与则说明value被其他线程修改,那么将oldValue的值更新为当前value重试

例:使用atomic和CAS实现无锁堆栈

在多线程模式下,将新节点的next指向原节点与将head节点指向新节点是两个独立操作,其中第二步的前提是链表头部节点没有变化

此时另一个线程的操作可能会导致链表头部发生变化

例1

如此时节点A向堆栈push了一个新节点A

例2

此时另一个线程又向堆栈push了一个新节点B,head指向了B

若第一个线程还将head更新为节点A,则会将节点B孤立

例3

此时我们在compare_exchange函数中将A与B节点的next节点进行比较,若相等,说明头部没有变化,那么可以将A作为新的节点,将head指向A

否则说明已经发生变化,就将A的next值更新为head当前值,并更新head

pop操作更简单,将head当前值存储在局部变量中,再把其next与当前值进行比较,若相等则说明头部未改变,将head更新为头节点next值,否则说明发生变化,将局部变量更新为head当前值,继续循环

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
#include<iostream>
#include<atomic>
#include<thread>
#include<memory>
#include<syncstream>

template<typename T>
class LockFreeStack{
private:
  struct Node{
    T data;
    Node* next = nullptr;
    Node(T d):data(d){}
  };
  std::atomic<Node*> head;
public:
  LockFreeStack() = default;
  LockFreeStack(const LockFreeStack&) = delete;
  LockFreeStack& operator=(const LockFreeStack&) = delete;

  void push(T val){
    auto *newNode = new Node(val);
    //std::shared_ptr<Node> newNode = std::make_shared<Node>(val);
    newNode->next = head.load();
    while(!head.compare_exchange_strong(newNode->next, newNode));
    std::osyncstream(std::cout) <<" Pushed: " << val << "\n";
  }

  bool pop(T &result){
    Node* oldHead = head.load();
    do{
      if (oldHead==nullptr){
        std::osyncstream(std::cout)<< " Stack is empty, cannot pop.\n";
        return false;
      }
    }while(oldHead&&!head.compare_exchange_strong(oldHead, oldHead->next));
    if (oldHead){
      result = oldHead->data;
      std::osyncstream(std::cout)<< " Popped: " << result << "\n";
      return true;
    }
    else return false;
  }
};

void pushItems(LockFreeStack<int> &stack, int start, int end){
  for (int i = start; i<end;i++){
    stack.push(i);
    std::this_thread::sleep_for(std::chrono::milliseconds(50));
  }
}
void popItems(LockFreeStack<int> &stack, int count){
  int value;
  for (int i = 0; i<count;i++){
    stack.pop(value);
    std::this_thread::sleep_for(std::chrono::milliseconds(30));
  }
}

int main(){
  LockFreeStack<int> stack;

  std::thread t1(pushItems, std::ref(stack), 1, 10);
  std::thread t2(popItems, std::ref(stack),7);
  std::thread t3(pushItems, std::ref(stack), 10, 20);
  std::thread t4(popItems, std::ref(stack),7);

  t1.join();
  t2.join();
  t3.join();
  t4.join();
  return 0;
}

(这里shared_ptr不是平凡可复制类,故改为了裸指针,同时无法解决ABA问题)

内存模型

顺序一致性模型

程序按照代码编写顺序执行,并且所有线程都看到相同顺序的模型

枚举值名称为memory_order_seq_cst

该模型顺序简单,不易出错,但可能无法充分使用系统和硬件的性能优化

Relaxed模型

relaxed模型下,不对内存顺序作约束,可能出现顺序一致性模型中不会出现的结果

CPU和内存之间会有多级缓存

缓存、编译器的优化都可能改变同一线程不同语句的执行顺序

枚举值名称为memory_order_relaxed

relaxed模型中,线程执行代码对值的更改首先保存在缓存中,当代码执行完毕,才会更新内存中的值

relaxed

如该例子中,线程1与2执行了①和③代码,将缓存中的a与b修改为1,再执行②和④代码,读取内存中的a与b都为0

在代码执行完毕后,缓存中的值才会修改到内存中

Acquire/Release模型

枚举值有如下三种:

  • memory_order_acquire
  • memory_order_release
  • memory_order_acq_rel

通常在原子操作中,

写入数据使用release,即将值写入后,将缓存中的数据发布到内存中,包括之前修改过的其他变量

获取数据使用acquire,即将值从内存中更新到缓存,并读取这个值

对于先加载在修改的,使用acq_rel

在mutex锁中,mutex.lock()与acquire语义是等价的,mutex.unlock()与release语义是等价的,两者之间是临界区

临界区内的代码可以互相调换顺序,但不能移出临界区外

临界区上方的代码可以进入临界区,但不能移出临界区下方

临界区下方的代码可以进入临界区,但不能移出临界区上方

于此对应的memory_order_acquirememory_order_release遵循同样的原则

内存分区模型

内存在计算机中大致分为四个区域:

  • 代码区:存放函数体的二进制代码,由操作系统管理
  • 全局区:存放全局变量,静态变量与常量
  • 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
  • 堆区:由用户分配释放,若不释放程序结束后由操作系统回收

程序运行前

程序编译后,生成exe可执行程序,未执行前分为两个区域

代码区:

  • 存放CPU执行的机器指令
  • 代码区是共享的,对于频繁执行的程序,只需要在内存中有一份代码即可
  • 代码区是只读的,防止程序意外修改他的指令

全局区:

  • 存放全局变量、静态变量与常量(不包括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
#include <iostream>
using namespace std;
// 全局变量
int g_a = 10;
int g_b = 10;
// const修饰的全局变量
const int g_c = 10;
int main(){
  // 局部变量
  int l_a = 10;
  int l_b = 10;
  // 静态局部变量
  static int s_a = 10;
  static int s_b = 10;

  // 常量
  // 字符串常量
  cout << "字符串常量的地址:"<< (void*)&"hello world" << endl;
  // const修饰的局部变量
  const int l_c = 10;

  // 输出
  cout <<"局部变量l_a的地址: " << (void*)&l_a << endl;
  cout <<"局部变量l_b的地址: " << (void*)&l_b << endl;
  cout <<"静态局部变量s_a的地址: " << (void*)&s_a << endl;
  cout <<"静态局部变量s_b的地址: " << (void*)&s_b << endl;
  cout <<"全局变量g_a的地址: " << (void*)&g_a << endl;
  cout <<"全局变量g_b的地址: " << (void*)&g_b << endl;
  cout <<"const修饰的全局变量g_c的地址: " << (void*)&g_c << endl;
  cout <<"const修饰的局部变量l_c的地址: " << (void*)&l_c << endl;

  return 0;
}

输出发现,不同类型的量所在地址位置有较大差异

1
2
3
4
5
6
7
8
9
字符串常量的地址:0x404024
局部变量l_a的地址: 0x61fe1c
局部变量l_b的地址: 0x61fe18
静态局部变量s_a的地址: 0x403018
静态局部变量s_b的地址: 0x40301c
全局变量g_a的地址: 0x403010
全局变量g_b的地址: 0x403014
const修饰的全局变量g_c的地址: 0x404004
const修饰的局部变量l_c的地址: 0x61fe14

程序运行后

栈区:

由编译器自动分配释放,存放函数参数,局部变量等

不要返回局部变量的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;
int* f(int b){ // 形参b也放在栈区
  int a = 10; // 存放在栈区的局部变量,函数执行结束后自动释放
  return &a; // (不要)返回局部变量的地址
}
int main(){
  int *p = f(100);
  // 访问已经释放的内存,可能会导致未定义行为
  // 可能会输出10(编译器做了一次保留),也可能会输出其他值
  cout << *p << endl; 
  cout << *p << endl; 
  return 0;
  

}

堆区:

由用户分配释放,若不释放,程序结束后由操作系统回收

使用关键字new开辟内存

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;
int* f(){ 
  int *p = new int(10); // 使用new在堆区分配内存
  return p; 
}
int main(){
  int *p = f();
  cout << *p << endl; // 输出10
  cout <<"地址: " << (void*)p << endl; // 输出地址
  return 0;
}

关键字new

C++中,使用new关键字在堆区开辟空间,使用delete关键字释放内存

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

int main(){
  int* p = new int(10); // 用new在堆区分配内存
  int* arr = new int[10]; // 用new在堆区分配数组
  for (int i = 0;i<10;i++){
    arr[i]=i;
  }
  cout << *p << endl; // 输出10
  for (int i = 0;i<10;i++){
    cout << arr[i] << " "; // 输出数组元素
  }
  delete p; // 释放单个变量的内存
  delete[] arr; // 释放数组的内存
  return 0;
}

异常与错误

传统错误处理

C风格的传统错误处理通常通过在出现异常时返回一个异常值(如-1),main函数捕获到了异常值后,进行对应的提示与操作

如sqrt函数中,当输入值小于0时,返回-1,main函数捕获后输出错误信息,因为这里可以保证sqrt操作返回的都是非负数,所以可以设置-1值

进步之处:错误信息的输出应由main决定,因为sqrt函数仅仅为一个计算函数,不应该涉及输入输出流

另一种方法:为了不与值域冲突,可以返回一个pair<int, bool>,bool判断是否合法。但这样也会造成

注意:报错与退出逻辑不应该直接写在被调用函数中,合适的方法应该是返回值

Optional

返回Optional数据类型,该类型可以表示一个可能存在也可能不存在的值

当发现异常值传入,我们可以返回一个空值std::nullopt,让main函数捕获

判断时可以使用以下方法:

  • .has_value()判断返回的是否是异常值
  • 或直接用该类型变量判断是否是异常值(存在一个bool的显式转换)
  • .value()获取正常值

缺点:当有多种错误类型区分的需求便无法处理

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 <optional>
#include <vector>
#include <iostream>

std::optional<int> findIndex(const std::vector<int>& data, int value) {
    for (size_t i = 0; i < data.size(); ++i) {
        if (data[i] == value) {
            return i; // 返回找到的索引
        }
    }
    return std::nullopt; // 未找到
}

int main() {
    std::vector<int> data = {1, 2, 3, 4, 5};
    auto index = findIndex(data, 3);

    if (index) {
    	std::cout << "Found at index: " << *index << std::endl;
    } else {
    	std::cout << "Value not found" << std::endl;
    }

    return 0;
}

错误码

通过设置一个全局变量错误码,当出现对应错误时,设置对应错误码并返回-1,main函数捕获并根据错误码处理

错误码的设置可以参考errno.h中的宏定义设置

使用strerror(errno)来输出对应错误

使用setlocate(LC_ALL, "zh-CN.UTF-8")

使用perror 在错误输出前加上一段描述加一个冒号

异常

错误码的缺点:无法自动穿透调用栈,必须编写代码并每一层都检查并保存错误码,一直传递,有时候还需要转换;并且错误码无法携带更多完整信息

这时我们需要使用异常,包含三个部分:try,catch,throw

  • try用于检查后面大括号中代码块所产生的异常
  • catch用于捕获和处理上述异常
  • throw用于在try中抛出异常

其中,throw抛出的异常可以是标准库中定义的异常类,传入string类型的参数用于描述异常

e.g.

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

double divide(double a, double b){
  if (b==0){
    throw runtime_error("Divided by zero");
  }
  return a/b;
}

int main(){
  try{
    int c= divide(1,0);
    cout << c << endl;
  }
  catch(runtime_error& e){
    cout <<"exception: "<<e.what()<<endl; 
  }
}

throw

C++中可以将各种类型的变量、对象与指针作为异常抛出

对应在catch代码中要设置为对应类型即可捕获到对应异常

一般推荐使用自定义或标准库中的异常类,如可以创建错误类继承自std::exception类,并手动修改异常

catch

catch代码块只能捕捉到括号中指定的类型,如果捕获的是类对象,则可以捕获到该类以及继承类中的对象

一个try代码块可以跟多个catch代码块,原则:越专用的越靠前,越通用的越靠后

对于未知异常,可以使用catch(...)捕获,不过不建议捕获自己代码无法处理的异常,而是留给能处理的代码或者让程序终止

资源管理

当函数中内存释放的部分出现在了异常后,则会在抛出异常后结束函数,从而导致内存泄漏

我们可以使用类进行封装,当类对象被自动释放时,会调用析构函数进行释放

我们也可以使用智能指针来封装,当抛出异常后,封装的对象会被自动释放

上述资源管理的方式称为RAII(Resource Acquisition Is Initialization)

高阶函数

函数的默认参数

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会被替换为该类型的引用,但此时会出现引用的引用(&&&),这时遵循引用折叠规则,得到类型的左值引用

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