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语言 C++ 开发者
深入探索C++:特性、代码实践及流程图解析
深入探索C++:特性、代码实践及流程图解析
|
1月前
|
算法 数据处理 C++
【C++ 20 新特性 算法和迭代器库的扩展和泛化 Ranges】深入浅出C++ Ranges库 (Exploring the C++ Ranges Library)
【C++ 20 新特性 算法和迭代器库的扩展和泛化 Ranges】深入浅出C++ Ranges库 (Exploring the C++ Ranges Library)
227 1
|
5天前
|
编译器 C语言 C++
C++一分钟之-C++11新特性:初始化列表
【6月更文挑战第21天】C++11的初始化列表增强语言表现力,简化对象构造,特别是在处理容器和数组时。它允许直接初始化成员变量,提升代码清晰度和性能。使用时要注意无默认构造函数可能导致编译错误,成员初始化顺序应与声明顺序一致,且在重载构造函数时避免歧义。利用编译器警告能帮助避免陷阱。初始化列表是高效编程的关键,但需谨慎使用。
20 2
|
26天前
|
编译器 C语言 C++
C++的基本特性和语法
C++的基本特性和语法
18 1
|
1月前
|
自然语言处理 编译器 C语言
【C++】C++ 入门 — 命名空间,输入输出,函数新特性
本文章是我对C++学习的开始,很荣幸与大家一同进步。 首先我先介绍一下C++,C++是上个世纪为了解决软件危机所创立 的一项面向对象的编程语言(OOP思想)。
44 1
【C++】C++ 入门 — 命名空间,输入输出,函数新特性
|
9天前
|
C++
C++ 是一种面向对象的编程语言,它支持对象、类、继承、多态等面向对象的特性
C++ 是一种面向对象的编程语言,它支持对象、类、继承、多态等面向对象的特性
|
13天前
|
程序员 C语言 C++
【C++语言】继承:类特性的扩展,重要的类复用!
【C++语言】继承:类特性的扩展,重要的类复用!
|
1月前
|
安全 算法 程序员
探索C++的魅力:语言特性、编程实践及代码示例
C++是广泛应用的编程语言,尤其在系统级编程、应用开发、游戏和嵌入式系统中广泛使用。其主要特性包括:面向对象编程(封装、继承、多态),泛型编程(通过模板实现代码复用和类型安全),以及丰富的标准库和第三方库。在编程实践中,需注意内存管理、异常处理和性能优化。示例代码展示了面向对象和泛型编程,如类的继承和泛型函数的使用。C++的内存管理和库支持使其在解决复杂问题时具有高效和灵活性。
|
1月前
|
存储 程序员 C语言
深入理解C++:从语言特性到实践应用
深入理解C++:从语言特性到实践应用
33 3
|
1月前
|
存储 安全 编译器
【C++】类的六大默认成员函数及其特性(万字详解)
【C++】类的六大默认成员函数及其特性(万字详解)
45 3