c++进阶知识点复习(2)

这是一篇C++复习笔记。参考力扣上的收费教程 C++ 面试突破 整理而成【侵删】,根据我的理解对部分内容做了删减和调整,比如删掉了c++20等比较新的内容(暂时还用不到),过滤了比较老的C++语法,重难点增加了自己的理解。每段代码我都自己调试过,除了编译器导致的差异,基本没问题。

想读完整的原版内容,请移步力扣官网C++ 面试突破

c++语言特性相关

左值、右值

  1. 从实践角度讲,它能够完美解决 C++ 中长久以来为人所诟病的临时对象效率问题
  2. 左值:指表达式结束后依然存在的持久对象。可以取地址,可以通过内置(不包含重载) & 来获取地址,我们可以将一个右值赋给左值。
  3. 右值:表达式结束就不再存在的临时对象。不可取地址,不可以通过内置(不包含重载) & 来获取地址。由于右值不可取地址,因此我们不能将任何值赋给右值。
  4. 使用 = 进行赋值时,= 的左边必须为左值,右值只能出现在 = 的右边。
// x 是左值,666 为右值
int x = 666;   // ok 
int *y = x; // ok
int *z = &666 // error
666 = x; // error
int a = 9; // a 为左值
int b = 4; // b 为左值
int c = a + b // c 为左值 , a + b 为右值
a + b = 42; // error

std::move() std::forward()实现原理

  1. 引用折叠原理
  • 右值传递给上述函数的形参 T&& 依然是右值,即 T&& && 相当于 T&&。
  • 左值传递给上述函数的形参 T&& 依然是左值,即 T&& & 相当于 T&。
  • 我们已经知道折叠原理,通过引用折叠原理可以知道,move() 函数的形参既可以是左值也可以是右值。
  1. 万能模板
    万能模板既可以接受左值作为实参也可以接受右值作为实参

  2. std::move 原理

  • 通过remove_reference 移除引用,得到参数的具体类型
  • 通过static_cast<> 进行强制类型转换,返回type&& 右值引用

move代码:

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
	return static_cast<typename remove_reference<T>::type &&>(t);
}

remove_reference实现


//原始的,最通用的版本
template <typename T> struct remove_reference{
    typedef T type;  //定义 T 的类型别名为 type
};
 
//部分版本特例化,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> //左值引用
{ typedef T type; }
 
template <class T> struct remove_reference<T&&> //右值引用
{ typedef T type; }   
  
//举例如下,下列定义的a、b、c三个变量都是int类型
int i;
remove_refrence<decltype(42)>::type a;             //使用原版本,
remove_refrence<decltype(i)>::type  b;             //左值引用特例版本
remove_refrence<decltype(std::move(i))>::type  b;  //右值引用特例版本 
  1. forward的实现
  • forward 保证了再转发时左值右值不会被更改,实现完美转发。主要解决引用函数参数为右值时,传进来之后又了变量名变成了左值,比如下面代码:
#include <iostream>
using namespace std;

template<typename T>
void fun(T&& tmp) 
{ 
    cout << "fun rvalue bind:" << tmp << endl; 
} 

template<typename T>
void fun(T& tmp) 
{ 
    cout << "fun lvalue bind:" << tmp << endl; 
} 

template<typename T>
void test(T&& x) {
    fun(x); // 10为右值,调用fun(x)时,变成了左值
    fun(std::forward<T>(x));
}

int main() 
{ 
    int a = 10;
    test(10);
    test(a);
    return 0;
}
/*
fun lvalue bind:10
fun rvalue bind:10
fun lvalue bind:10
fun lvalue bind:10
*/

仔细体会下上面的代码,调用test(10),test函数知道x为右值,但是调用fun(x)时,x已经成了左值。通过std::forward(x) 强行在x前面再加上&或者&&,以匹配对应的模板函数。

和move的区别是

  • forward有两个模板函数,分别对应左值和右值
  • move 在进行类型转换时,利用 remove_reference 将外层的引用全部去掉,这样可以将 t 强制转换为指定类型的右值引用,而 forward 则利用引用折叠的技巧,巧妙的保留了变量原有的属性。

指针

指针的基本概念比较好理解,函数指针稍微复杂点,重点提下:

  1. 普通函数指针
  #include <iostream>
using namespace std;

int add(int a, int b){
    return a + b;
}

typedef int (*fun_p)(int, int);

int main(void)
{
    fun_p fn = add;
    cout << fn(1, 6) << endl;
    return 0;
}
  1. 对象函数指针

定义指向成员函数的指针时,要标明指针所属的类。

  #include <iostream>

using namespace std;

class A
{
public:
    int var1, var2; 
	static int x;
	static int get() {
		return 100;
	}

    int add(){
        return var1 + var2;
    }
};



int main()
{
    A ex;
    ex.var1 = 3;
    ex.var2 = 4;
    int *p = &ex.var1; // 指向对象成员变量的指针
    cout << *p << endl;

    int (A::*fun_p)();
	int (*fun_q)();
    fun_p = &A::add; // 指向对象非静态成员函数的指针 fun_p
	fun_q = A::get; // 指向对象静态成员函数的指针 fun_q
	cout << (ex.*fun_p)() << endl;
    cout << (*fun_q)() << endl;
    return 0;
}

全局函数、类函数、类静态成员函数的指针均有点不一样,没有深究,或许c++编译器就这么定义的。

  #include <iostream>
using namespace std;
int fun1(int tmp1, int tmp2)
{
  return tmp1 * tmp2;
}
int fun2(int tmp1, int tmp2)
{
  return tmp1 / tmp2;
}

int main()
{
  int (*fun)(int x, int y); 
  fun = fun1; // ok
  fun = &fun1; // ok 两种写法均可以
  cout << fun(15, 5) << endl; 
  fun = fun2;
  cout << fun(15, 5) << endl; 
  cout<<sizeof(fun1)<<endl; // error
  cout<<sizeof(&fun1)<<endl; // 8
  return 0;
}
/*
运行结果:
75
3
*/
  • 函数名 fun1 存放的是函数的首地址,它是一个函数类型 void,&fun1 表示一个指向函数对象 fun1 的地址,是一个指针类型。它的类型是 int (*)(int,int),因此 fun1 和 &fun1 的值是一样的;
  • &fun1 是一个表达式,函数此时作为一个对象,取对象的地址,该表达式的值是一个指针。
  • 通过打印 sizeof 即可知道 fun1 与 &fun1 的区别;不能对函数调用sizeof,会报错"Invalid application of ‘sizeof’ to a function type"

野指针和悬空指针

  1. 悬空指针:若指针指向一块内存空间,当这块内存空间被释放后,该指针依然指向这块内存空间
void *p = malloc(size);
free(p); // 此时,p 指向的内存空间已释放, p 就是悬空指针。
p = NULL; 
  1. “野指针” 是指不确定其指向的指针,未初始化的指针为“野指针”
void *p; 
// 此时 p 是“野指针”。

强制类型抓换

  1. static_cast,静态转换,即编译器转换,失败会抛编译错误
  • 数据转换
  • 基本数据类型转换
  • 基类<->派生类转换:派生类->基类(上行)是安全的;基类->派生类(下行)是不安全的,编译器不做校验,所以下行最好用dynamic_cast
  • 可以将空指针转化成目标类型的空指针
  • 可以将任何类型的表达式转化成 void 类型
  1. const_cast
    主要用于 const -> 非const; volatile -> 非volatile转换,是单向的,反之不行,即只能作用于常量,将常量、常量指针变成非常量、非常量指针

  2. reinterpret_cast
    改变指针或引用的类型

  • 指针或引用–>足够长度的整型
  • 整型–>指针或引用
  1. dynamic_cast
  • 动态转换,运行时
  • 向上转换和static_cast是一样的效果,只是改变指针的类型,并没有改变所指的内容
  • 向下转换时,指针指向的对象必须是派生类,此时转换成功,否则失败。

即子类对象可用父类指针指引,基类对象不能用子类指引,因为基类可能没有子类的方法,子类指针调用的函数可能不存在会crash

看个例子,基类转子类的两种情况,一种成功,一种失败

  #include <iostream>
#include <cstring>

using namespace std;

class Base
{
public:
    virtual void fun()
    {
        cout << "Base::fun()" << endl;
    }
};

class Derive : public Base
{
public:
    virtual void fun()
    {
        cout << "Derive::fun()" << endl;
    }
};

int main()
{
    Base *p1 = new Derive();
    Base *p2 = new Base();
    Derive *p3 = new Derive();

    //转换成功
    p3 = dynamic_cast<Derive *>(p1);
    if (p3 == NULL)
    {
        cout << "NULL" << endl;
    }
    else
    {
        cout << "NOT NULL" << endl; // 输出
    }

    //转换失败
    p3 = dynamic_cast<Derive *>(p2);
    if (p3 == NULL)
    {
        cout << "NULL" << endl; // 输出
    }
    else
    {
        cout << "NOT NULL" << endl;
    }

    return 0;
}

什么是类型萃取

参考std::move中用到的remove_reference函数,里面typedef _Tp type就是萃取。在编译器获取了模板参数_Tp的类型

  /// remove_reference
template<typename _Tp>
struct remove_reference
{ typedef _Tp   type; };

template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp   type; };

template<typename _Tp>
struct remove_reference < _Tp&& >
{ typedef _Tp   type; };

结构体相等的判断方式

需要重载操作符 == 判断两个结构体是否相等,不能用函数 memcmp 来判断两个结构体是否相等,因为 memcmp 函数是逐个字节进行比较的,而结构体存在内存空间中保存时存在字节对齐,字节对齐时补的字节内容是随机的,会产生垃圾值,所以无法比较。

  #include <iostream>

using namespace std;

struct A
{
    char c;
    int val;
    A(char c_tmp, int tmp) : c(c_tmp), val(tmp) {}

    friend bool operator==(const A &tmp1, const A &tmp2); //  友元运算符重载函数
};

bool operator==(const A &tmp1, const A &tmp2)
{
    return (tmp1.c == tmp2.c && tmp1.val == tmp2.val);
}

int main()
{
    A ex1('a', 90), ex2('b', 80);
    if (ex1 == ex2)
        cout << "ex1 == ex2" << endl;
    else
        cout << "ex1 != ex2" << endl; // 输出
    return 0;
}

不用友元也可以,结构体的函数是public的

struct A {
  char c;
  int val;
  A(char c_tmp, int tmp) : c(c_tmp), val(tmp) {}

  bool operator==(const A &tmp2) {
      return c == tmp2.c && val == tmp2.val;
  };
};

函数模板和类模板的区别

  • 默认参数:函数模板不允许有默认参数,类模板在模板参数列表中可以有默认参数
  • 特化:函数模板只能全特化;而类模板可以全特化,也可以偏特化。
  • 调用方式不同:函数模板可以进行类型推导,可以隐式调用,也可以显式调用;类模板只能显式调用。

函数模板调用方式举例:

#include<iostream>

using namespace std;

template <typename T>
T add_fun(const T & tmp1, const T & tmp2){
    return tmp1 + tmp2;
}

int main(){
    int var1, var2;
    cin >> var1 >> var2;
    cout << add_fun<int>(var1, var2); // 显式调用

    double var3, var4;
    cin >> var3 >> var4;
    cout << add_fun(var3, var4); // 隐式调用
    return 0;
}

模板特化

定义函数模板的特化版本,本质上是接管了编译器的工作,为原函数模板定义了一个特殊实例

#include <iostream>
#include <cstring>

using namespace std;
//函数模板
template <class T>
bool compare(T t1, T t2)
{
    cout << "通用版本:";
    return t1 == t2;
}

template <> //函数模板特化
bool compare(char *t1, char *t2)
{
    cout << "特化版本:";
    return strcmp(t1, t2) == 0;
}

int main(int argc, char *argv[])
{
    char arr1[] = "hello";
    char arr2[] = "abc";
    cout << compare(123, 123) << endl;
    cout << compare(arr1, arr2) << endl;

    return 0;
}
/*
运行结果:
通用版本:1
特化版本:0
*/

可变参数模板

可变参数函数通常是递归的,第一个版本的 print_fun 负责终止递归并打印初始调用中的最后一个实参。第二个版本的 print_fun 是可变参数版本,打印绑定到 t 的实参,并用来调用自身来打印函数参数包中的剩余值。

#include <iostream>

using namespace std;

template <typename T>
void print_fun(const T &t)
{
    cout << t << endl; // 最后一个元素
}

template <typename T, typename... Args>
void print_fun(const T &t, const Args &...args)
{
    cout << t << " ";
    print_fun(args...);
}

int main()
{
    print_fun("Hello", "world", "!");
    return 0;
}
/*运行结果:
Hello wolrd !

*/

C++ I/O与进程同步

C++条件变量

  1. 多线程基本概念
  • 线程
    • c++11 之后标准库里加入了多线程 #include std::thread
    • thread使用方法 std::thread t1(runnable, 1),一个任务函数、一个是参数。也可以放lambda表达式
      std::thread t([](){
        std::cout << "hello world." << std::endl;
      });
      
      t.join(); 
    
    • thread声明即开始run了,thead.join()表示当前线程要等子线程运行完,不加join会报错
    • thread如果不想join,则必须调用thread.detach,表示不等他了,随他自己去吧
    • std::this_thread::sleep_for() 当前线程sleep一段时间
    • std::this_thread::get_id() 获取当前线程id,可以用来判断当前线程是否为主线程
    • mutex #include,互斥量,最原始的锁,但是使用不太方便[lock, unlock]成对使用
    • 基于mutex,有std::lock_guard, 满足RAII,只需要声明,用完自动释放
    std::mutex mtx;
    std::lock_guard<std::mutex> lock(mtx);
    
    • 基于lock_guard,又出现了std::unique_lock,融合了mutex[lock, unlock]和lock_guard的功能

如果对c++多线程不了解,可以先看看我之前写的一篇入门文章,《C++ 多线程入门》
https://mp.weixin.qq.com/s/b1HhzqBkt7mxo9v7pQJZHg

  • wait-基于条件变量 #include <condition_variable> // std::condition_variable
    • std::condition_variable::wait 必须与unique_lock配合使用 lock作为wait参数
      std::unique_lock<std::mutex> lock(mtx);
      std::condition_variable cv:
      cv.wait(lock); // cv必须指定,你等的是哪一把锁啊,不然咋知道你和哪个线程争夺执行权呢
    
  • chrono #include //辅助工具,计时。配合其他类使用
    std::this_thread::sleep_for(std::chrono::seconds(1));
    
  1. 案例, 几个非常好的条件变量 wait notify的例子
  • 例1,三个线程wait,第四个线程notify,熟悉wait_for的用法,代码里有详细注释。
#include <iostream>
#include <atomic>
#include <condition_variable>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;

std::condition_variable cv; // 条件变量 wait
std::mutex cv_m; //原始互斥量 lock
int i;

void waits(int idx)
{
  std::unique_lock<std::mutex> lk(cv_m);
  
  // wait_for(lock, 超时duration, Predicate)
  // Predicate有函数调用功能即可,作为唤醒时附加的判断条件,predicate成立才能真的唤醒,重新获得锁和执行权,此处判断i==1
  // 这里有三个线程调用,分别等待100ms,200ms,300ms
  if (cv.wait_for(lk, idx*100ms, []{return i == 1;})) {
      std::cerr << "Thread " << idx << " finished waiting. i == " << i << '\n';
  } else {
      std::cerr << "Thread " << idx << " timed out. i == " << i << '\n';
  }
}

void signals()
{  
  // 等120ms,让第一个线程等100ms超时,看看超时的效果
  std::this_thread::sleep_for(120ms); 
  std::cerr << "Notifying...\n";
  // 唤醒,第二个、三个线程分别唤醒后 predicate不满足,又继续睡了
  // 好比你妈把你喊醒,叫你上学,你醒了一看才5点,不到6点。又继续睡了
  cv.notify_all(); // 唤醒所有cv.wait
  
  // 当前线程sleep,一共sleep220ms了
  // 此时第二个线程等了200ms,没人叫他,自己超时醒了
  std::this_thread::sleep_for(100ms);
  {  
      // 注意这是在代码块里面,lock_guard出了代码块会unlock
      std::lock_guard<std::mutex> lk(cv_m);
      i = 1; //修改i == 1,让第三个线程能predicate成功
  }
  std::cerr << "Notifying again...\n";
  // 第二次唤醒,第一个、第二个线程已经超时了,第三个线程唤醒了,而且i == 1,那就真的醒了
  cv.notify_all();
}

int main()
{
  std::thread t1(waits, 1), t2(waits, 2), t3(waits, 3), t4(signals);
  t1.join(); // t1 等待 100ms 后未被唤醒,自动超时;
  t2.join(); // t2 在 120 ms 处被唤醒,但 condition 未满足,再此进入阻塞,200ms 时由于超时返回
  t3.join(); // t3 在 120 ms 处被唤醒,但 condition 未满足,再此进入阻塞,220ms 时被 notify ,正常返回。
  t4.join();
}
/*
Thread 1 timed out. i == 0
Notifying...
Thread 2 timed out. i == 0
Notifying again...
Thread 3 finished waiting. i == " 1
*/
  • 例2 notify_one notify_all

    • notify_one随机唤醒一个,即只有一个等待的线程醒了去上学了
    • notify_all唤醒了所有的线程。

    注意,门开锁了,也得一个个出去,即一个线程执行完了,释放锁了,其他的线程才能接着获得线程执行权跟着出门。简言之,notify_one最终只有一个等待线程激活,notify_all最终会激活所有等待线程。

notify_one

  // condition_variable::notify_one
#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable

std::mutex mtx;
std::condition_variable produce,consume;

int cargo = 0;     // shared value by producers and consumers

void consumer () {
std::unique_lock<std::mutex> lck(mtx);
while (cargo==0) consume.wait(lck);
std::cout << cargo << '\n';
cargo=0;
produce.notify_one();
}

void producer (int id) {
std::unique_lock<std::mutex> lck(mtx);
while (cargo!=0) produce.wait(lck);
cargo = id;
consume.notify_one();
}

int main ()
{
std::thread consumers[10],producers[10];
// spawn 10 consumers and 10 producers:
for (int i=0; i<10; ++i) {
    consumers[i] = std::thread(consumer);
    producers[i] = std::thread(producer,i+1);
}

// join them back:
for (int i=0; i<10; ++i) {
    producers[i].join();
    consumers[i].join();
}

return 0;
}

notify_all

  #include <iostream>
#include <condition_variable>
#include <thread>
#include <chrono>

std::condition_variable_any cv;
std::mutex cv_m; // This mutex is used for three purposes:
                // 1) to synchronize accesses to i
                // 2) to synchronize accesses to std::cerr
                // 3) for the condition variable cv
int i = 0;

void waits(int idx)
{
    std::unique_lock<std::mutex> lk(cv_m);
    std::cerr << "Waiting... \n";
    cv.wait(lk, []{return i == 1;});
    std::cerr << "thread:"<< idx <<" ...finished waiting. i == 1\n";
}

void signals()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    {
        std::lock_guard<std::mutex> lk(cv_m);
        std::cerr << "Notifying...\n";
    }
    cv.notify_all();

    std::this_thread::sleep_for(std::chrono::seconds(1));

    {
        std::lock_guard<std::mutex> lk(cv_m);
        i = 1;
        std::cerr << "Notifying again...\n";
    }
    cv.notify_all();
}

int main()
{
    std::thread t1(waits, 1), t2(waits, 2), t3(waits, 3), t4(signals);
    t1.join(); 
    t2.join(); 
    t3.join();
    t4.join();
}
  1. 假唤醒及解决方案

举例说明假唤醒:生产者-消费者模型下,消费者有3个线程处于wait

  • 1)生产者生产一个"面包"后notify_all
  • 2)线程1、2、3均被激活,逐个执行
  • 3)假设线程1先获得锁,把刚生产的"面包"消费了,释放锁
  • 4)假设线程2接着获得锁,也来吃面包,发现面包没了,那可怎么办?那不是要干仗!!
  • 5)接着线程3获得锁,重复线程2的惨剧~~无面包可消费

实际工程中涉及的共享数据和流程比较复杂,很容易出现异常。所以此时最好的办法就是,判断面包还在吗?不在的话我接着睡吧,别起来捣乱了。

lock(mutex);
while( !面包还在吗) { // 注意"!"否定
    // 面包不在了
    condition_wait( cond, mutex);
}
// 面包还在,退出while循环,起来吃面包了
do(something);
// 吃完面包,释放锁
unlock(mutex);

线程同步与异步

mutex 比较简单,不再赘述。补充几个线程同步的知识点。

  1. std::cout 是线程不安全的,多线程中使用cout会乱序,可以考虑加锁实现线程安全的cout

#include <iostream>       
#include <thread>        
#include <mutex>          
  
std::mutex mtx;
void print_block (int n, char c) {
  mtx.lock();
  for (int i=0; i<n; ++i) { std::cout << c; }
  std::cout << '\n';
  mtx.unlock();
}

int main ()
{
  std::thread th1 (print_block,50,'*');
  std::thread th2 (print_block,50,'$');

  th1.join();
  th2.join();

  return 0;
}         
  1. recursive_mutex 。同一个线程可重复访问,递归的场景使用
std::recursive_mutex mtx;

void fun1() {
    mtx.lock();
    mtx.unlock();
}

void fun2() {
    mtx.lock();
    fun1(); // recursive lock 
    mtx.unlock();
};

  1. 共享互斥量 c++17新特性(互斥锁)
    shared_mutex + (unique_lock、shared_lock)共同实现。

实现场景:多个线程同时读,只有一个线程能写入

 #include <iostream>
#include <mutex>  // For std::unique_lock
#include <shared_mutex>
#include <thread>
 
class ThreadSafeCounter {
 public:
  ThreadSafeCounter() = default;
 
  // 多个线程可以同时读取 countter 计数
  unsigned int get() const {
    std::shared_lock lock(mutex_);
    return value_;
  }
 
  // 只有1个线程可以修改 countter 计数
  void increment() {
    std::unique_lock lock(mutex_);
    value_++;
  }
 
  // 只有1个线程可以修改 countter 计数
  void reset() {
    std::unique_lock lock(mutex_);
    value_ = 0;
  }
 
 private:
  mutable std::shared_mutex mutex_;
  unsigned int value_ = 0;
};
 
int main() {
  ThreadSafeCounter counter;
 
  auto increment_and_print = [&counter]() {
    for (int i = 0; i < 3; i++) {
      counter.increment();
      std::cout << std::this_thread::get_id() << ' ' << counter.get() << '\n';
 
      // Note: Writing to std::cout actually needs to be synchronized as well
      // by another std::mutex. This has been omitted to keep the example small.
    }
  };
 
  std::thread thread1(increment_and_print);
  std::thread thread2(increment_and_print);
 
  thread1.join();
  thread2.join();
}
/*
139677317637888 2
139677317637888 3
139677309245184 4
139677309245184 5
139677309245184 6
139677317637888 6
*/              
  1. call_once ,标记仅调用一次

有些场景,比如仅触发一次,还挺有用的,避免冗余的判断逻辑。

#include <iostream>
#include <thread>
#include <mutex>

std::once_flag flag1, flag2;

void simple_do_once()
{
   std::call_once(flag1, [](){ std::cout << "Simple example: called once\n"; });
}

int main()
{
   std::thread st1(simple_do_once);
   std::thread st2(simple_do_once);
   std::thread st3(simple_do_once);
   std::thread st4(simple_do_once);
   st1.join();
   st2.join();
   st3.join();
   st4.join();
}
/*
Simple example: called once
*/ 

### 线程高级用法

1. 成员函数异步调用另一个成员函数,需加上this参数
```c++
class Test{
   private:
   void func1(const std::string s1, int i) {
       std::cout << s1 << std::endl;
   }
   public:
   void func2() {
           std::thread t1(&Test::func1, this, "test", 0);
           t1.detach();
           std::cout << "func2" << std::endl;
   }

类成员函数默认会有个this参数,编译器帮我们干了,换了个线程,找不到this,所以要手动传进去

  1. std::async,可以方便的获取结果
template<class Fn, class... Args>
future<typename result_of<Fn(Args...)>::type> async(launch policy, Fn&& fn, Args&&...args);
  • 第一个参数:异步执行还是同步执行
    • std::launch::async 传递的可调用对象异步执行;
    • std::launch::deferred 传递的可调用对象同步执行;
    • std::launch::async | std::launch::deferred 可以异步或是同步,取决于操作系统,我们无法控制;
    • 如果我们不指定策略,则相当于(3)
  • 第二个参数:接收一个可调用对象
    • 仿函数
    • lambda表达式
    • 类成员函数、普通函数…
  • 第三个参数:函数的参数

看个简单的例子:

#include "pch.h"
#include <iostream>
#include <string>
#include <chrono>
#include <thread>
#include <future>
  
using namespace std::chrono;
  
std::string fetchDataFromDB(std::string recvData) {
  
    std::cout << "fetchDataFromDB start" << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(seconds(5));
    return "DB_" + recvData;
}
  
std::string fetchDataFromFile(std::string recvData) {
  
    std::cout << "fetchDataFromFile start" << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(seconds(3));
    return "File_" + recvData;
}
  
int main() {
  
    std::cout << "main start" << std::this_thread::get_id() << std::endl;
  
    //获取开始时间
    system_clock::time_point start = system_clock::now();
  
    std::future<std::string> resultFromDB = std::async(std::launch::async, fetchDataFromDB, "Data");
  
    //从文件获取数据
    std::future<std::string> fileData = std::async(std::launch::deferred, fetchDataFromFile, "Data");
  
    //知道调用get函数fetchDataFromFile才开始执行
    std::string FileData = fileData.get();
    //如果fetchDataFromDB()执行没有完成,get会一直阻塞当前线程
    std::string dbData = resultFromDB.get();
     
    //获取结束时间
    auto end = system_clock::now();
  
    auto diff = duration_cast<std::chrono::seconds>(end - start).count();
    std::cout << "Total Time taken= " << diff << "Seconds" << std::endl;
  
    //组装数据
    std::string data = dbData + " :: " + FileData;
  
    //输出组装的数据
    std::cout << "Data = " << data << std::endl;
  
    return 0;
}
  1. std::future
    std::async 返回结果即为一个 future
  • 可以通过查询 future 的状态(future_status)来获取异步操作的结果。future_status 有三种状态:

    • deferred:异步操作还没开始;
    • ready:异步操作已经完成;
    • timeout:异步操作超时;
  • 获取 future 结果有三种方式:

  • get

  • wait

  • wait_for

其中 get 等待异步操作结束并返回结果,wait 只是等待异步操作完成,没有返回值,wait_for 是超时等待返回结果

c++ I/O操作-重定向

I/O放到任何语言可以说的话题都很多,这段只是简单提下。

  1. C++ 中的 Streams 对象主要分为三种类型:
  • istream :这种类型的流对象只能从流中执行输入操作;
  • ostream :这些对象只能用于输出操作;
  • iostream :可用于输入和输出操作;
  1. 标准输入输出
  • 标准输出流(cout):通常标准输出设备是显示器。C++ cout 语句是 ostream 类的实例。
  • 标准输入流(cin):通常计算机中的输入设备是键盘。C++ cin 语句是 istream 类的实例,用于从标准输入设备(通常是键盘)读取输入。
  1. I/O重定向

所有流对象都有一个关联的数据成员类 streambuf,是流的缓冲区。

当我们从流中读取数据时,我们不会直接从源中读取数据,而是从链接到源的缓冲区中读取数据

C++ 允许我们为任何流设置流缓冲区,因此重定向流的任务简单地减少为更改与流关联的流缓冲区。

stream_object.rdbuf()://获取返回指向stream_object的流缓冲区的指针
stream_object.rdbuf(streambuf * p)://将流缓冲区设置为p指向的
  • 例:实现cout重定向输出到文件。同理也可以将输入定向到文件。
// Cpp program to redirect cout to a file
#include <fstream>
#include <iostream>
#include <string>

using namespace std;

int main()
{
	fstream file;
	file.open("cout.txt", ios::out);
	string line;

	// 保存 cin 和 cout 的缓冲区 buffer
	streambuf* stream_buffer_cout = cout.rdbuf();
	streambuf* stream_buffer_cin = cin.rdbuf();

	// 获取文件 file 的缓冲区 buffer
	streambuf* stream_buffer_file = file.rdbuf();

	// cout 重定向文件,会生成一个cout.txt文件
	cout.rdbuf(stream_buffer_file);
	cout << "This line written to file" << endl;

	// cout 恢复重定
	cout.rdbuf(stream_buffer_cout);
	cout << "This line is written to screen" << endl;

	file.close();
	return 0;
}
  • 例,清除缓冲区

在各种情况下,可能需要清除不需要的缓冲区,以便在所需的程序中立即获取下一个输入

下面这个程序不能得到正确的效果,ch为空,因为\n保留在缓冲区,作为下一个输入读取

#include<iostream>
#include<vector>
using namespace std;

int main()
{
    int a;
    char ch[80];
    cin >> a;
    cin.getline(ch,80);
    cout << a << endl;
    cout << ch << endl;
    return 0;
}

有两种方法修改:

  • cin.ignore(numeric_limits::max(),‘\n’); ,表示从\n不忽略,max表示尽可能多的忽略
  • cin>>ws

修改后:

  #include<iostream>
#include<ios> //used to get stream size
#include<limits> //used to get numeric limits
using namespace std;

int main()
{
    int a;
    char ch[80];
    cin >> a;

    cin.ignore(numeric_limits<streamsize>::max(),'\n'); //方法一
    cint >> ws;// 方法二
    cin.getline(ch,80);
    cout << a << endl;
    cout << ch << endl;
    return 0;
}

设计模式

单例模式

实现思路:

  • 私有化三个构造函数默认构造、拷贝构造、赋值远算
  • 线程安全
  • static 全局单例
  1. 最简单的懒汉模式
class singleton {
private:
  static singleton* instance;
  singleton(const singleton& temp) {}
  singleton& operator=(const singleton& temp){}
protected:
  singleton() {}
public: 
  static singleton* getInstance() {
  return instance;
}
}
  // 类静态变量要在类内声明,类外初始化
  // 初始化可以访问构造函数
  singleton::instance = new singleton();
  1. 不安全的饿汉模式
class Singleton{
private:
  static Singleton* instance;
  Singleton(const Singleton& temp) {}
  Singleton& operator=(const Singleton& temp){}
  Singleton() {}
public:
  static Singleton* getInstance(){
  if (instance == null) {
    instance = new Singleton();
  }
  return instance;
}
}

Singleton::instance = NULL;
  1. 加锁
    加锁也容易出现阻塞
class Singleton {
private:
  static std::mutex mtx;
  static Singleton* instance;
  Singleton(){}
  Singleton&(const Singleton& temp){}
  Singleton& operator=(const Singleton& temp){}
public:
  static Singleton* getInstance() {
      mtx.lock();
      if (instance == NULL) {
        instance = new Singleton();
      }
      return instance;
      mtx.unlock();
  }
}
  1. 内部静态变量,最优解决方案,多线程访问是会自动挂起
class Singleton{
private:
  Singleton(){}
  Singleton&(const Singleton& tmp){}
  Singleton& operator=(const Singleton& tmp){}
public:
  static Singleton* getInstance() {
    static Singleton instance;
    return &instance;
}
}

工厂模式

“简单工厂”、“工厂方法”、“抽象工厂”

下面以加减乘除的实现为例

  1. 简单工厂
    将创建和初始化的逻辑封装到工厂类内, 把每一种操作封装成一个类,继承一个基类。

#include <iostream>
#include <vector>
using namespace std;

// Here is the product class
class Operation
{
public:
    int var1, var2;
    virtual double GetResult()
    {
        double res = 0;
        return res;
    }
};

class Add_Operation : public Operation
{
public:
    virtual double GetResult()
    {
        return var1 + var2;
    }
};

class Sub_Operation : public Operation
{
public:
    virtual double GetResult()
    {
        return var1 - var2;
    }
};

class Mul_Operation : public Operation
{
public:
    virtual double GetResult()
    {
        return var1 * var2;
    }
};

class Div_Operation : public Operation
{
public:
    virtual double GetResult()
    {
        return var1 / var2;
    }
};

// Here is the Factory class
class Factory
{
public:
    static Operation *CreateProduct(char op)
    {
        switch (op)
        {
        case '+':
            return new Add_Operation();

        case '-':
            return new Sub_Operation();

        case '*':
            return new Mul_Operation();

        case '/':
            return new Div_Operation();

        default:
            return new Add_Operation();
        }
    }
};

int main()
{
    int a, b;
    cin >> a >> b;
    Operation *p = Factory::CreateProduct('+');
    p->var1 = a;
    p->var2 = b;
    cout << p->GetResult() << endl;

    p = Factory::CreateProduct('*');
    p->var1 = a;
    p->var2 = b;
    cout << p->GetResult() << endl;

    return 0;
}
  1. 工厂方法模式
    新增加分类,要修改工厂里的代码,不符合开闭原则。工厂方法在次基础上,将每一个对象的生成都抽出一个工厂

每个工厂对应一个一个类, 基于面向接口编程,所有的工厂都继承同一个工厂基类

#include <iostream>
#include <vector>
using namespace std;

// Here is the product class
class Operation
{
public:
    int var1, var2;
    virtual double GetResult()
    {
        double res = 0;
        return res;
    }
};

class Add_Operation : public Operation
{
public:
    virtual double GetResult()
    {
        return var1 + var2;
    }
};

class Sub_Operation : public Operation
{
public:
    virtual double GetResult()
    {
        return var1 - var2;
    }
};

class Mul_Operation : public Operation
{
public:
    virtual double GetResult()
    {
        return var1 * var2;
    }
};

class Div_Operation : public Operation
{
public:
    virtual double GetResult()
    {
        return var1 / var2;
    }
};

class Factory
{
public:
    virtual Operation *CreateProduct() = 0;
};

class Add_Factory : public Factory
{
public:
    Operation *CreateProduct()
    {
        return new Add_Operation();
    }
};

class Sub_Factory : public Factory
{
public:
    Operation *CreateProduct()
    {
        return new Sub_Operation();
    }
};

class Mul_Factory : public Factory
{
public:
    Operation *CreateProduct()
    {
        return new Mul_Operation();
    }
};

class Div_Factory : public Factory
{
public:
    Operation *CreateProduct()
    {
        return new Div_Operation();
    }
};

int main()
{
    int a, b;
    cin >> a >> b;
    Add_Factory *p_fac = new Add_Factory();
    Operation *p_pro = p_fac->CreateProduct();
    p_pro->var1 = a;
    p_pro->var2 = b;
    cout << p_pro->GetResult() << endl;

    Mul_Factory *p_fac1 = new Mul_Factory();
    Operation *p_pro1 = p_fac1->CreateProduct();
    p_pro1->var1 = a;
    p_pro1->var2 = b;
    cout << p_pro1->GetResult() << endl;

    return 0;
}
  1. 抽象工厂
    在工厂方法的基础上,可能有多个产品,可以在工厂基类里声明多个虚函数。每个工厂实现类,都需要实现能生产多个类别的产品。

看起来感觉就多了一个产品大类

我之前学习李建忠设计模式里,将抽象工厂,以数据库为例,是非常复杂的,但是应对复杂项目,后期威力巨大。并不是这么简单的拓展一个接口即可。

#include <iostream>
#include <vector>
using namespace std;

// Here is the product class
class Operation_Pos
{
public:
    int var1, var2;
    virtual double GetResult()
    {
        double res = 0;
        return res;
    }
};

class Add_Operation_Pos : public Operation_Pos
{
public:
    virtual double GetResult()
    {
        return var1 + var2;
    }
};

class Sub_Operation_Pos : public Operation_Pos
{
public:
    virtual double GetResult()
    {
        return var1 - var2;
    }
};

class Mul_Operation_Pos : public Operation_Pos
{
public:
    virtual double GetResult()
    {
        return var1 * var2;
    }
};

class Div_Operation_Pos : public Operation_Pos
{
public:
    virtual double GetResult()
    {
        return var1 / var2;
    }
};
/*********************************************************************************/
class Operation_Neg
{
public:
    int var1, var2;
    virtual double GetResult()
    {
        double res = 0;
        return res;
    }
};

class Add_Operation_Neg : public Operation_Neg
{
public:
    virtual double GetResult()
    {
        return -(var1 + var2);
    }
};

class Sub_Operation_Neg : public Operation_Neg
{
public:
    virtual double GetResult()
    {
        return -(var1 - var2);
    }
};

class Mul_Operation_Neg : public Operation_Neg
{
public:
    virtual double GetResult()
    {
        return -(var1 * var2);
    }
};

class Div_Operation_Neg : public Operation_Neg
{
public:
    virtual double GetResult()
    {
        return -(var1 / var2);
    }
};
/*****************************************************************************************************/

// Here is the Factory class
class Factory
{
public:
    virtual Operation_Pos *CreateProduct_Pos() = 0;
    virtual Operation_Neg *CreateProduct_Neg() = 0;
};

class Add_Factory : public Factory
{
public:
    Operation_Pos *CreateProduct_Pos()
    {
        return new Add_Operation_Pos();
    }
    Operation_Neg *CreateProduct_Neg()
    {
        return new Add_Operation_Neg();
    }
};

class Sub_Factory : public Factory
{
public:
    Operation_Pos *CreateProduct_Pos()
    {
        return new Sub_Operation_Pos();
    }
    Operation_Neg *CreateProduct_Neg()
    {
        return new Sub_Operation_Neg();
    }
};

class Mul_Factory : public Factory
{
public:
    Operation_Pos *CreateProduct_Pos()
    {
        return new Mul_Operation_Pos();
    }
    Operation_Neg *CreateProduct_Neg()
    {
        return new Mul_Operation_Neg();
    }
};

class Div_Factory : public Factory
{
public:
    Operation_Pos *CreateProduct_Pos()
    {
        return new Div_Operation_Pos();
    }
    Operation_Neg *CreateProduct_Neg()
    {
        return new Div_Operation_Neg();
    }
};

int main()
{
    int a, b;
    cin >> a >> b;
    Add_Factory *p_fac = new Add_Factory();
    Operation_Pos *p_pro = p_fac->CreateProduct_Pos();
    p_pro->var1 = a;
    p_pro->var2 = b;
    cout << p_pro->GetResult() << endl;

    Add_Factory *p_fac1 = new Add_Factory();
    Operation_Neg *p_pro1 = p_fac1->CreateProduct_Neg();
    p_pro1->var1 = a;
    p_pro1->var2 = b;
    cout << p_pro1->GetResult() << endl;

    return 0;
}

观察者模式

使用场景

  1. 状态发生改变,一对多的调用
  2. 一般发出消息的是基础模块,不方便直接拿到业务模块去更新,需要业务模块将更新的逻辑注册到被观察者

一个例子:

#include <iostream>
#include <string>
#include <list>
using namespace std;

class Subject;
//观察者 基类 (内部实例化了被观察者的对象sub)
class Observer
{
protected:
    string name;
    Subject *sub;

public:
    Observer(string name, Subject *sub)
    {
        this->name = name;
        this->sub = sub;
    }
    virtual void update() = 0;
};

class StockObserver : public Observer
{
public:
    StockObserver(string name, Subject *sub) : Observer(name, sub)
    {
    }
    void update();
};

class NBAObserver : public Observer
{
public:
    NBAObserver(string name, Subject *sub) : Observer(name, sub)
    {
    }
    void update();
};
//被观察者 基类 (内部存放了所有的观察者对象,以便状态发生变化时,给观察者发通知)
class Subject
{
protected:
    list<Observer *> observers;

public:
    string action; //被观察者对象的状态
    virtual void attach(Observer *) = 0;
    virtual void detach(Observer *) = 0;
    virtual void notify() = 0;
};

class Secretary : public Subject
{
    void attach(Observer *observer)
    {
        observers.push_back(observer);
    }
    void detach(Observer *observer)
    {
        list<Observer *>::iterator iter = observers.begin();
        while (iter != observers.end())
        {
            if ((*iter) == observer)
            {
                observers.erase(iter);
                return;
            }
            ++iter;
        }
    }
    void notify()
    {
        list<Observer *>::iterator iter = observers.begin();
        while (iter != observers.end())
        {
            (*iter)->update();
            ++iter;
        }
    }
};

void StockObserver::update()
{
    cout << name << " 收到消息:" << sub->action << endl;
    if (sub->action == "梁所长来了!")
    {
        cout << "我马上关闭股票,装做很认真工作的样子!" << endl;
    }
}

void NBAObserver::update()
{
    cout << name << " 收到消息:" << sub->action << endl;
    if (sub->action == "梁所长来了!")
    {
        cout << "我马上关闭NBA,装做很认真工作的样子!" << endl;
    }
}

int main()
{
    Subject *dwq = new Secretary();
    Observer *xs = new NBAObserver("xiaoshuai", dwq);
    Observer *zy = new NBAObserver("zouyue", dwq);
    Observer *lm = new StockObserver("limin", dwq);

    dwq->attach(xs);
    dwq->attach(zy);
    dwq->attach(lm);

    dwq->action = "去吃饭了!";
    dwq->notify();
    cout << endl;
    dwq->action = "梁所长来了!";
    dwq->notify();
    return 0;
}

设计原则

我们一般记成SOLID
其中(里氏替换原则和迪米特法则的首字母都是L)

  1. 单一职责原则 (Single Responsibility Principle)
    每个类、函数的功能定义尽量单一、精简,提高复用和可维护性,避免膨胀

  2. 开闭原则(Open Closed Principle)
    对拓展开放,对修改闭合。尽量拓展,减少对原有类的直接修改,新增的功能尽量不要影响原有的功能,降低耦合风险

  3. 里氏替换原则(Liskov Subsititution Principle)
    子类要实现父类的功能,可以完全替换父类。

  4. 迪米特法则(Law of Demeter),又叫"最少知道法则"
    尽量对外面暴露少的接口,降低业务方使用难度

  5. 接口隔离原则(Interface Segregation Principle)
    业务方尽量不要依赖它不需要的接口,这就要求基础模块能做基本的拆分,业务方按需要依赖

  6. 依赖倒置原则(dependence Inversion Principle)
    依赖接口,不要依赖实现,减少耦合,增加灵活度

猜你喜欢

转载自blog.csdn.net/daozi20/article/details/130479581