C++11新特性总结(2)

简介: C++11新特性总结(2)

七. Lambda表达式

如下, 先进行一个简单的使用

struct Cars {
  int carnum;
  int price;
  string name;
};
struct cmp {
  bool operator()(Cars& c1, Cars& c2) {
    return c1.price < c2.price;
  }
};
bool cmp2(const Cars& c1, const Cars& c2) {
  return c1.price < c2.price;
}
int main() {
  auto fun = [] {};   //这个是最为简单的lambda表达式啥都不干
  fun();          //调用, 使用方式像极了无参仿函数调用
  auto add = [](double a, double b)->double { return a + b; };
  cout << add(2.7, 3.7) << endl;
  //然后是常用方式:  代替 仿函数使用
  Cars cars[] = { 
    {100, 150000, "长城"}
  , {55, 20000, "宝马摩托"}
  , {455, 1000, "小电瓶"}
  , {1000, 500, "自行车"}};
  //形式1:
  sort(cars, cars + sizeof(cars) / sizeof(Cars)
    , [](Cars& c1, Cars& c2)->bool {return c1.price < c2.price; });
  //形式2:
  sort(cars, cars + sizeof(cars) / sizeof(Cars), cmp());  //传入匿名可调用对象
  //形式3:
  sort(cars, cars + sizeof(cars) / sizeof(Cars), cmp2); //传入函数指针
  return 0;
}

上述我们利用了 lambda 表达式来代替了仿函数的使用, 这个也是lambda表达式的常用形式之一, 在很多时候都可以见到上述的这种使用场景...  但是有没有思考过为什么吗?


可调用对象(仿函数). 函数指针, lambda表达式   底层处理是否是类似 或者说甚至一样的???


其实 lambda表达式的底层处理就是完全处理成了仿函数的.

  • 其实本质上 lambda的底层只是有很多的修饰, 如果把修饰看成是类名 本质就完全是类的 operator() 的重载 仿函数的底层处理形式了

侯杰老师在讲解 lambda底层的时候也曾阐述 lambda 的底层处理就是按照仿函数, 当作类来进行处理的


针对对于自定义对象的 sort 还有一点点小小的技巧, 可以在我们需要 sort 的自定义类中去重载一下operator < 函数,  直接不需要在自己传入排序规则了.......


why???   上述 重载一下 operator < 就可以达到重建排序规则   ()  的效果

  • 因为默认调用的其实就是 less<>{}  说白了就是  operator <   验证如下代码:   可以达到同上述一毛一样的效果.
struct Cars {
  int carnum;
  int price;
  string name;
  bool operator<(Cars& c) const {
    return price < c.price;
  }
};
int main() {
  Cars cars[] = {
    {100, 150000, "长城"}
  , {55, 20000, "宝马摩托"}
  , {455, 1000, "小电瓶"}
  , {1000, 500, "自行车"} };
  sort(cars, cars + sizeof(cars) / sizeof(Cars));
  return 0;
}

八. 包装器 (适配器) (function包装器)

function 是 C++中的类模板, 也是一个包装器.


说到包装器, 首先就要思考              函数指针, 仿函数, Lambda表达式


上章就提到了 三者底层可能差不大多, 使用的情景也是各有雷同, 包装器 其实就可以算是将上述三者进行一个统一, 适配成一个东西    如下 : function 包装器可以实现对三者的统一包装

//函数指针
int add1(int a, int b) {
  return a + b;
}
//仿函数
struct Add {
  int operator()(int a, int b) {
    return a + b;
  }
  int a, b;
};
int main() {
  auto add2 = [](int a, int b){ return a + b; };  //当然可以在()->指定后置返回类型
  //auto add2 = [](int a, int b)->int { return a + b; };  
  function<int(int, int) > func1 = add1;    //函数名
  function<int(int, int) > func2 = Add();   //函数对象
  function<int(int, int) > func3 = add2;    //lambda表达式
  std::cout << func1(3, 5) << std::endl;
  std::cout << func2(3, 5) << std::endl;
  std::cout << func3(3, 5) << std::endl;
  while (1);
  return 0;
}

思考一个问题,  为什么需要 function 这个包装器, 直接使用三者不可以吗???

包装器的好处????    统一了可调用对象的类型, 并且指定了参数和返回值类型

1. 简化了函数指针这样的复杂指针的使用, 函数指针复杂难以理解

2. 方便了作为参数时候的传入

3. 仿函数是一个类名没有指定参数和返回值需要知道就需要去看这个operator () 重载获取

4. lambda 在语法层, 看不到类型, 底层存在类型, 但是也是lambda_uuid, 也很难看


我觉得function 出现的 最最重要的原因就是有了一个确切的类型,  使用简单方便,


解决函数指针使用复杂的问题, 解决仿函数不能指定参数类型的问题,  要知道参数类型还要跑去看哪个 operator()     以及解决    lambda没有具体类型的问题.


实际案例:


150. 逆波兰表达式求值


根据 逆波兰表示法,求表达式的值。


有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。

未使用 function代码:

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        for(int i=0;i<tokens.size();i++){
            if(tokens[i]=="+"||tokens[i]=="-"||tokens[i]=="*"||tokens[i]=="/"){
                int rhs=st.top();
                st.pop();
                int lhs=st.top();
                st.pop();
                switch(tokens[i][0]){
                    case '+':st.push(lhs+rhs);break;
                    case '-':st.push(lhs-rhs);break;
                    case '*':st.push(lhs*rhs);break;
                    case '/':st.push(lhs/rhs);break;
                }
                continue;
            }
            st.push(stoi(tokens[i]));         
        }
        //然后就是最后的结果了
        return st.top();
    }
};

使用 function 的代码:

class Solution {
    //使用包装器进行复用???  如何利用包装器??
    //需要的是 function 和  对应的 op 对应起来...
    //如何对应 使用的就是map 对应  map<string, function<int(int, int) > > opmap
    //逆波兰表达式:  左 右 op
    //遇到 op 的时候 说明前面的就是 l + r
    //每一个运算结果需要重新入栈
public:
    int evalRPN(vector<string>& tokens) {
        stack<int > numst;
        map<string, function<int(int, int) > > opmap = {
            {"+", [](int a, int b)->int{ return a + b;}} ,
            {"-", [](int a, int b)->int{ return a - b;}} ,
            {"*", [](int a, int b)->int{ return a * b;}} ,
            {"/", [](int a, int b)->int{ return a / b;}} 
        };
        for (auto& e : tokens) {
            if (e == "+" || e == "-" || e == "*" || e == "/") {
                int r = numst.top(); numst.pop();
                int l = numst.top(); numst.pop();       //先提取的是r 后 l
                numst.push(opmap[e](l, r));
            } else {
                numst.push(stoi(e));
            }
        }
        return numst.top();
    }
};

九. 线程库

简单的用起来

int main() {
  size_t n = 100;
  thread t1([n]{
    for (size_t i = 0; i < n; i += 2) {
      cout << i << endl;
    }
  });
  cout << t1.get_id() << endl;    //线程id  
  thread t2([n]{
    for (size_t i = 1; i < n; i += 2) {
      cout << i << endl;
    }
  });
  cout << t2.get_id() << endl;    //线程id  
  t1.join();
  t2.join();      //主线程阻塞等待子线程的死亡
  while (1);      //等待他们结束
  return 0;
}
  • . 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的 状态。
  • 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。 线程函数一般情况下可按照以下三种方式提供: 函数指针    lambda表达式     函数对象
void TFun() {
  cout << "函数指针" << endl;
}
struct TF {
  void operator()() {
    cout << "函数对象" << endl;
  }
};
int main() {
  thread t1(TFun);    //传入函数指针
  TF tf;
  thread t2(tf);      //可调用对象(仿函数)
  thread t3([]() {cout << "Lambda" << endl; });
  t1.join();
  t2.join();
  t3.join();              //join 主线程挂起等待三个线程结束返回
  return 0;
}

thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个 线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。


可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效


采用无参构造函数构造的线程对象

线程对象的状态已经转移给其他线程对象

线程已经调用jion或者detach结束

线程函数参数

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在 线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。   (线程函数参数传入是以值拷贝的形式拷贝到栈空间中的, 所以既是是引用类型, 在线程中修改后外部实参也是无法修改的)   如何处理这个问题, 如下代码解释


std::ref();    使用这个函数转换之后传入的线程函数参数才是真正的引用, 线程中改变, 外面也会改变

class Fun {
public:
  void operator()() {
    cout << "operator()" << endl;
  }
};
void ThreadFunc1(int& x)
{
  x += 10;
}
void ThreadFunc2(int* x)
{
  *x += 10;
}
int main() {
//测试一波:
  int a = 10;
  thread t1(ThreadFunc1, a);      //传入a 
  cout << a << endl;          //?? a 是否改变?
  //上述发现 a 没有改变
  //如何可以使得传入的数据不需要进行拷贝, 而是原有数据?
   如果想要通过形参改变外部实参时,必须借助std::ref()函数
  thread t3(ThreadFunc1, std::ref(a));//才不会传入拷贝本
  cout << a << endl;
  thread t2(ThreadFunc2, &a);     //这样看一看???
  cout << a << endl;          //a改变了, 因为这个传入的是地址进去
  t1.join(); 
  t2.join();
  return 0;
}

十. 原子操作


多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。

//多线程对于共享数据的写操作带来的问题...    

unsigned long num = 0L;     //先定义全局的数据
void tf(size_t n) {
  for (size_t i = 0; i < n; ++i) {
    num += 1;
  }
}
int main() {
  thread t1(tf, 10000000);
  thread t2(tf, 10000000);
  t1.join();
  t2.join();  
  cout << num << endl;
  return 0;
}
  • 发现一个大问题, 上述根本无法获取我们想要的结果甚至, 每一次运行结果都是不一样的

解决上述问题的方式1: 在 C++98 中采取的是加锁的方式实现避免函数的重入问题,

lock();

操作临界资源  (写入操作)

unlock();

unsigned long num = 0L;     //先定义全局的数据
mutex mtx;
void tf(size_t n) {
  for (size_t i = 0; i < n; ++i) {
    mtx.lock();
    num += 1;
    mtx.unlock();
  }
}
int main() {
  thread t1(tf, 10000000);
  thread t2(tf, 10000000);
  t1.join();
  t2.join();  
  cout << num << endl;
  return 0;
}

加锁确实是可以解决上述的问题, 但是不停的解锁解锁, 效率会变得特别低,  时间消耗也会大大增加, 不停的加锁解锁, 虽然也解决了问题, 保护了临界资源..  但是程序运行时延性大大增加, 而且对于锁控制不好还会死锁, 于是C++11 搞出来一个原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入 的原子操作类型,使得线程间数据的同步变得非常高效。

atomic_long num{ 0 };//定义全局的原子操作数据
void tf(size_t n) {
  for (size_t i = 0; i < n; ++i) {
    num += 1;
  }
}
int main() {
  thread t1(tf, 10000000);
  thread t2(tf, 10000000);
  t1.join();
  t2.join();
  cout << num << endl;
  return 0;
}

有了原子操作数据, 确实针对这些数据的操作不再需要加锁保护了, 但是如果是一段代码段的原子操作, 就还是不得不使用锁来实现, 但是只要设计到锁就可能发生死锁, C++11为了预防死锁, C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock。

  • unique_lock   和  lock_guard  都是对于锁的一种封装模板类, 实现对于锁的管理,

  • 然后接下来是一个简单的实现
// RAII
namespace tyj
{
  template<class Lock>
  class lock_guard
  {
  public:
    lock_guard(Lock& lock)
      :_lock(lock)
    {
      _lock.lock();
      cout << "加锁" << endl;
    }
    /*void lock()
    {
    _lock.lock();
    }
    void unlock()
    {
    _lock.unlock();
    }*/
        //对于lock_guard是没有上述操作的, 它仅仅只是做垃圾回收
        //出作用域自动回收锁, 调用析构解锁
    ~lock_guard()
    {
      _lock.unlock();
      cout << "解锁" << endl;
    }
    lock_guard(const lock_guard<Lock>& lock) = delete;
  private:
    Lock& _lock;
  };
}

只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数 成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁 问题。


向比较  lock_guard 而言, unique_lock 提供了更多的操作


上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock


修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有 权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)

条件变量引入以及条件变量对象和互斥对象配合实现一个案例

要求 : 支持两个线程交替打印,一个打印奇数,一个打印偶数

int main() {
  mutex mtx;    //定义锁 为后面的完成要求做准备
  bool flag = 1;//flag = 1 打印偶数  flag = 0 打印奇数 配合 condion_variable使用
  condition_variable _cond; //定义条件变量为后序相互耦合关联式打印埋伏笔
  thread t1([&](){
    unique_lock<mutex> _lock(mtx);
    int i = 0;
    while (i < 100) {
      while (!flag) _cond.wait(_lock);//不满足flag 一直等
      //说明满足了
      cout << "i: " << i << endl;
      flag = 0;      //修改让t2去打印
      _cond.notify_one();//唤醒t2打印奇数了
      i += 2;
    }
  });
  thread t2([&](){
    unique_lock<mutex> _lock(mtx);
    int j = 1;
    while (j < 100) {
      while (flag) _cond.wait(_lock);//满足flag 说明这个时候在打印偶数
      //说明满足了
      cout << "j: " << j << endl;
      flag = 1;      //修改让t2去打印
      _cond.notify_one();//唤醒t2打印奇数了
      j += 2;
    }
  });
  t1.join();
  t2. join();
  return 0;
}

十一. 总结本章

首先本章介绍了初始化参数列表{} 进行统一的初始化


{} 的本质是一个类型 叫做 initializer_list , 支持使用{} 构造的本质是支持传入initializer_list做参数的构造函数


然后引入了右值引用, 可以取地址的是左值, 右值是不可以去地址的值, 一旦给右值取别名, 右值就会退化, 就会分配空间 + 地址 退化为左值


然后通过右值引用引出移动构造,  移动构造相比拷贝构造好处体现在深拷贝上面, 他和深拷贝不同的是不需要重新开底层的存储空间  + 转移数据, 直接窃取右值的底层空间


模板右值引用: 万能引用,   引用接收之后所有的右值会退化为左值, 想要保持住右值属性不退化, 需要进行  std::forward<>()完美转发, 保持右值属性


然后是 lambda表达式的引出, [捕获列表](参数列表)->后置返回类型{函数体} 且lambda表达式的底层处理就是 类的可调用对象      operator() 运算符重载


function 包装器 对于 函数指针  仿函数    lambda表达式的统一封装....   包装:  好处, 使用起来更加方便,  指定好了参数和返回值类型, 作为参数传入也更加方便灵活...


thread 线程类库,  C++11 支持的线程库, 参数的传入以值拷贝形式, 要想传入的是真正的引用 必须进行 std::ref()处理


原子操作:   创建了一套原子操作数据类型 atomic_long 等等以atomic开头的支持原子操作的数据类型, 相比 使用mutex 更加高效, 且不会死锁


但是由于对于代码段的原子操作,   原子操作的数据类型   无能为力, 只能使用 mutex, 使用锁为了避免死锁, C++11  产生了 锁的管理模板类  unique_lock 和 lock_guard 进行管理锁, 在 对象结束的时候调用析构解锁, 不至于一直死锁


因为一直使用锁, 效率极低, 所以 可以使用  condition_variable 配合锁使用完成一些特殊的要求, 以及提高效率, 不至于让系统一直不停的加锁解锁,  因为加锁解锁 耗费CPU资源



相关文章
|
26天前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
100 59
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(三)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(二)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(一)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
19天前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
30 0
|
2月前
|
编译器 C++ 计算机视觉
C++ 11新特性之完美转发
C++ 11新特性之完美转发
45 4
|
2月前
|
Java C# C++
C++ 11新特性之语法甜点1
C++ 11新特性之语法甜点1
31 4
|
2月前
|
安全 程序员 编译器
C++ 11新特性之auto和decltype
C++ 11新特性之auto和decltype
37 3
|
2月前
|
设计模式 缓存 安全
C++ 11新特性之week_ptr
C++ 11新特性之week_ptr
33 2