C++ 11新特性详解2

简介: C++ 11新特性详解

C++ 11新特性详解1:https://developer.aliyun.com/article/1383796

在C++11中,STL中的容器都是增加了移动构造和移动赋值:

例如:


4. 右值引用引用左值

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。

在C++11中,标准库在中提供了一个有用的函数std::move,std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。从实现上讲,std::move基本等同于一个类型转换:static_cast(lvalue)。


std::move函数可以以非常简单的方式将左值引用转换为右值引用:


  • C++ 标准库使用比如vector::push_back 等这类函数时,会对参数的对象进行复制,连数据也会复制.这就会造成对象内存的额外创建, 本来原意是想把参数push_back进去就行了,通过std::move,可以避免不必要的拷贝操作。
  • std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能.。
  • 对指针类型的标准库对象并不需要这么做。

例如:

#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main()
{
    std::string str = "Hello";
    std::vector<std::string> v;
    //调用常规的拷贝构造函数,新建字符数组,拷贝数据
    v.push_back(str);
    std::cout << "After copy, str is \"" << str << "\"\n";
    //调用移动构造函数,掏空str,掏空后,最好不要使用str
    v.push_back(std::move(str));
    std::cout << "After move, str is \"" << str << "\"\n";
    std::cout << "The contents of the vector are \"" << v[0] << "\", \"" << v[1] << "\"\n";
}


执行结果:

STL容器插入接口函数也增加了右值引用版本:

例如:

int main()
{
  std::list<lhf::string> lt;
  lhf::string s1("1111");
  // 这里调用的是拷贝构造
  lt.push_back(s1);
  // 下面调用都是移动构造
  lt.push_back("2222");
  lt.push_back(std::move(s1));
  return 0;
}

运行结果:

5. 完美转发

模板中的&& 万能引用

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
  Fun(t);
}
int main()
{
  PerfectForward(10); // 右值
  int a;
  PerfectForward(a);  // 左值
  PerfectForward(std::move(a)); // 右值
  const int b = 8;
  PerfectForward(b);  // const 左值
  PerfectForward(std::move(b)); // const 右值
  return 0;
}


由结果可知上面的右值经过&& 万能引用后,全部退化成了左值。

模板中的&& 不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用到完美转发。


std::forward 完美转发在传参的过程中保留对象原生类型属性。

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
  Fun(std::forward<T>(t));
}
int main()
{
  PerfectForward(10); // 右值
  int a;
  PerfectForward(a); // 左值
  PerfectForward(std::move(a)); // 右值
  const int b = 8;
  PerfectForward(b);  // const 左值
  PerfectForward(std::move(b)); // const 右值
  return 0;
}

六、类的新功能

1. 默认成员函数

原来C++类中,有6个默认成员函数:


  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载

默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个:移动构造函数和移动赋值运算符重载。针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:


  • 如果没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
  • 如果自己提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

2. 强制生成默认函数的关键字default

C++11可以让我们更好的控制要使用的默认函数。假设要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。


class Person
{
public:
  Person(const char* name = "", int age = 0)
  :_name(name)
  , _age(age)
  {}
  Person(const Person& p)
  :_name(p._name)
  ,_age(p._age)
  {}
  //强制默认生成移动拷贝构造
  Person(Person&& p) = default;
private:
  bit::string _name;
  int _age;
};
int main()
{
  Person s1;
  Person s2 = s1;
  Person s3 = std::move(s1);
  return 0;
}

3. 禁止生成默认函数的关键字delete

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

class Person
{
public:
  Person(const char* name = "", int age = 0)
  :_name(name)
  , _age(age)
  {}
  Person(const Person& p) = delete;
private:
  bit::string _name;
  int _age;
};
int main()
{
  Person s1;
  Person s2 = s1;
  Person s3 = std::move(s1);
  return 0;
}


七、Lambda表达式

八、智能指针

九、包装器

1. 包装器的概念

function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。它是一种通用、多态的函数封装,它的实例可以对任何可以调用的目标实体进 行存储、复制和调用操作,它也是对 C++ 中现有的可调用实体的一种类型安全的包裹(相对来说,函数 指针的调用不是类型安全的),换句话说,就是函数的容器。当我们有了函数的容器之后便能够更加方便 的将函数、函数指针作为对象进行处理。

std::function在头文件<functional>
// 类模板原型如下
template <class T> function;   // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
/*
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
*/

2. 为什么要有包装器

例如:

有一句代码 ret = func(x);

上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!为什么呢?我们继续往下看。

template<class F, class T>
T useF(F f, T x)
{
  static int count = 0;
  cout << "count:" << ++count << endl;
  cout << "count:" << &count << endl;
  return f(x);
}
double f(double i)
{
  return i / 2;
}
struct Functor
{
  double operator()(double d)
  {
    return d / 3;
  }
};
int main()
{
  // 函数名
  cout << useF(f, 11.11) << endl;
  // 函数对象
  cout << useF(Functor(), 11.11) << endl;
  // lamber表达式
  cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
  return 0;
}


通过上面的程序验证,我们会发现useF函数模板实例化了三份。使用包装器可以很好的解决上面的问题。

// 使用方法如下:
#include <functional>
int f(int a, int b)
{
  return a + b;
}
struct Functor
{
public:
  int operator() (int a, int b)
  {
    return a + b;
  }
};
class Plus
{
public:
  static int plusi(int a, int b)
  {
    return a + b;
  }
  double plusd(double a, double b)
  {
    return a + b;
  }
};
int main()
{
  // 函数名(函数指针)
  std::function<int(int, int)> func1 = f;
  cout << func1(1, 2) << endl;
  // 函数对象
  std::function<int(int, int)> func2 = Functor();
  cout << func2(1, 2) << endl;
  // lamber表达式
  std::function<int(int, int)> func3 = [](const int a, const int b)
  {return a + b; };
  cout << func3(1, 2) << endl;
  // 类的成员函数
  std::function<int(int, int)> func4 = &Plus::plusi;
  //静态成员函数可以不加&符号
  std::function<int(int, int)> func4 = Plus::plusi;
  cout << func4(1, 2) << endl;
  std::function<double(Plus, double, double)> func5 = &Plus::plusd;
  cout << func5(Plus(), 1.1, 2.2) << endl;
  return 0;
}

利用包装器,解决模板的效率低下,实例化多份的问题。

#include <functional>
template<class F, class T>
T useF(F f, T x)
{
  static int count = 0;
  cout << "count:" << ++count << endl;
  cout << "count:" << &count << endl;
  return f(x);
}
double f(double i)
{
  return i / 2;
}
struct Functor
{
  double operator()(double d)
  {
    return d / 3;
  }
};
int main()
{
  // 函数名
  std::function<double(double)> func1 = f;
  cout << useF(func1, 11.11) << endl;
  // 函数对象
  std::function<double(double)> func2 = Functor();
  cout << useF(func2, 11.11) << endl;
  // lamber表达式
  std::function<double(double)> func3 = [](double d)->double { return d /4; };
  cout << useF(func3, 11.11) << endl;
  return 0;
}

利用包装器解决【逆波兰表达式】


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


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

【注意 】


  • 两个整数之间的除法只保留整数部分。
  • 可以保证给定的逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。


代码如下:

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<long long> s;
        map<string, function<long long(long long, long long)> > opFuncMap = 
        {
            {"+", [](long long x, long long y){return x + y;}},
            {"-", [](long long x, long long y){return x - y;}},
            {"*", [](long long x, long long y){return x * y;}},
            {"/", [](long long x, long long y){return x / y;}}
        };
        for(auto& str : tokens)
        {
            if(opFuncMap.count(str)) //操作符
            {
                long long right = s.top();
                s.pop();
                long long left = s.top();
                s.pop();
                s.push(opFuncMap[str](left, right));
            }
            else // 操作数
            {
                s.push(stoll(str));
            }
        }
        return s.top();
    }
};


3. bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。

// 原型如下:
template <class Fn, class... Args>
/* unspecified */ 
bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */
 bind (Fn&& fn, Args&&... args);

可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。

调用bind的一般形式:


auto newCallable = bind(callable,arg_list);


其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。


arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。

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