【C++知识点】C++11 常用新特性总结(一)

简介: 【C++知识点】C++11 常用新特性总结(一)

C++11 新特性

自动类型推导 auto

基本语法

在C++11之前,auto关键字用来指定存储期。在新标准中,它的功 能变为类型推断。

auto现在成了一个类型的占位符,通知编译器去根据初始化代码推 断所声明变量的真实类型。

各种作用域内声明变量都可以用到它。例如,名空间中,程序块中,或是for循环的初始化语句中。

在没有auto以前,遍历一个容器需要这样来书写一个迭代器:

#include <iostream>
#include <vector>
using namespace std;
int main() {
  vector<int> vc = { 1,2,3,4,5,6 };
  for (vector<int>::iterator it = vc.begin(); it != vc.end(); it++) {
    cout << *it << " ";
  }
  return 0;
}

有了auto之后,可以写出如下代码:

#include <iostream>
#include <vector>
using namespace std;
int main() {
    vector<int> vc = { 1,2,3,4,5,6 };
    for (auto it= vc.begin(); it != vc.end(); it++) {
        cout << *it << " ";
    }
  return 0;
}

auto 与 const

先看一段代码:

int x = 0;
const auto n = x; 
auto f = n; 
const auto& r1 = x; 
auto& r2 = r1;

1.第 2 行代码中,n 为 const int,auto 被推导为 int。


2.第 3 行代码中,n 为 const int 类型,但是 auto 却被推导为 int 类型,这说明当=右边的表达式带有 const 属性时, auto 不会使用 const 属性,而是直接推导出 non-const 类型。


3.第 4 行代码中,auto 被推导为 int 类型,这个很容易理解,不再赘述。


4.第 5 行代码中,r1 是 const int & 类型,auto 也被推导为 const int 类型,这说明当 const 和引用结合时,auto 的推导将保留表达式的 const 类型。


总结:

  1. 1.当类型不为引用时,auto 的推导结果将不保留表达式的 const 属性;
  2. 2.当类型为引用时,auto 的推导结果将保留表达式的 const 属性。

auto 的高级用法

auto 除了可以独立使用,还可以和某些具体类型混合使用,这样 auto 表示的就是“半个”类型,而不是完整的类型:

int x = 0;
auto *pt1 = &x; //pt1 为 int *,auto 推导为 int
auto pt2 = &x; //pt2 为 int*,auto 推导为 int*
auto &r1 = x; //r1 为 int&,auto 推导为 int
auto r2 = r1; //r2 为 int,auto 推导为 int

auto 的限制

  1. 1.auto 不能在函数的参数中使用
auto funcName(int v1,int v2)
{
  return v1 + v2;
}
  1. 注意:C++11的时候不能,C++14开始可以让普通函数具备返回值推导
  2. 2.auto 不能作用于类的非静态成员变量**(就是没有static关键字修饰的成员变量)**
class Student {
protected:
    auto name;  //错误
    int age;  //正确
};

3.auto 不能作用于模板参数

template <typename T>
class Student{
  //dosomething:
};
int main(){
    Student<int> s1;
    Student<auto> s2 = s1; //错误
    return 0;
}
  1. 4.auto 不能用于推导数组类型

自动类型推导 decltype

decltype,在C++中,作为操作符,用于查询表达式的数据类型。


decltype在C++11标准制定时引入,主要是为泛型编程而设计,以解决泛型编程中,由于有些类型由模板参数决定,而难以(甚至不可能)表示的问题。


decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和 sizeof 很相似。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。有时候,我们可能需要计算某个表达式的类型。

lambda表达式如果我们想要使用它的类型我们就需要使用decltype。

auto num1 = 100;
auto num2 = 200;
decltype(num1 + num2) num3;

num3的类型就是num1 + num2 最终的结果的类型。

在泛型编程中,可能需要通过参数的运算来得到返回值的类型:

#include <iostream>
using namespace std;
template <typename R, typename T, typename U>
R add(T t, U u)
{
    return t + u;
}
int main() {
    int a = 1; float b = 2.0;
    auto c = add<decltype(a + b)>(a, b);
    cout << c << endl;
    return 0;
}

下面的代码正常吗?

template <typename T, typename U>
decltype(t + u) add(T t, U u) 
{
    return t + u;
}

C++ 的返回值是前置语法,在返回值定义的时候参数变量还不存在,所以报错!

正确写法:

template <typename T, typename U>
auto add(T t, U u)->decltype(t+u) //尾随返回类型
{
    return t + u;
}

C++14开始可以直接写成:

template <typename T, typename U>
auto add(T t, U u)
{
    return t + u;
}

右值引用

基本概念

C++98 中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以 提高程序的可读性。

比如交换两个变量的值,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。

void change(int& n1, int& n2)
{
    int temp = n1;
    n1 = n2;
    n2 = temp;
}

为了提高程序运行效率,C++11 中引入了右值引用,右值引用也是别名,但其只能对右值引用。

int func1(int v1, int v2){
     return v1 + v2;
}
int main()
{
     const int&& num1 = 10;//右值示例
     //引用函数返回值,返回值是一个临时变量,为右值
     int&& num2 = func1(10, 20);
     cout << num1 << endl;
     cout << num2 << endl;
     return 0;
}

右值与左值

  1. 1.一般认为:左值可放在赋值符号的左边,右值可以放在赋值符号 的右边;或者能够取地址的称为左值,不能取地址的称为右值
  2. 2.左值也能放在赋值符号的右边,右值只能放在赋值符号的右边
int num = 100;
//函数的返回值结果为引用
int& returnNum(){
    return num;
}
int main()
{
    int num2 = 10;
    int num3 = num2;
    int* p = new int(0);
    //num2和num3,p和*p都是左值,说明:左值既可放在=的左侧,也可放在=的右侧
    const int num4 = 30;
    //num4 = num1;
    //特例:num4 虽然是左值,但是为const常量,只读不允许被修改
    cout << &num4 << endl;
    //num4可以取地址,所以num4严格来看也是左值
    //num3 + 2 = 200;
    //编译失败:因为num3+2的结果是一个临时变量,没有具体名称,也不能取地址,因此为右值
    returnNum() = 111;
    return 0;
}

移动语义

转移语义可以将资源(堆、系统对象等)从一个对象转移到另一个对象,这样可以减少不必要的临时对象的创建、拷贝及销毁。移动语义与拷贝语义是相对的,可以类比文件的剪切和拷贝。在现有的 C++ 机制中,自定义的类要实现转移语义,需要定义移动构造函数,还可以定义转移赋值操作符。

以 string 类的移动构造函数为例:

MyString(MyString&& str) {
    cout << "move ctor source from " << str.data << endl;
    len = str.len;
    data = str.data;
    str.len = 0;
    str.data = NULL;
}

和拷贝构造函数类似,有几点需要注意:

  • 参数(右值)的符号必须是 &&
  • 参数(右值)不可以是常量,因为我们需要修改右值
  • 参数(右值)的资源链接和标记必须修改,否则,右值的析构函数就会释放资源。转移到新对象的资源也就无效了。

标准库函数 std::move 可以将左值变成一个右值。

编译器只对右值引用才能调用移动构造函数,那么如果已知一个命名对象不再被使用,此时仍然想调用它的移动构造函数,也就是把一个左值引用当做右值引用来使用,该怎么做呢?

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

列表初始化

在 C++98 中,标准允许使用花括号 {} 对数组元素进行统一的列表初始值设定。比如:

int arr1[] = {1,2,3,4,5};
int arr2[100] = {0};

但对于一些自定义类型却不行,例如:

vector<int> vc{1,2,3,4,5};

在 c++98 中是无法编译成功的,只能够定义 vector 对应之后通过循环进行插入元素达到这个目的。

C++11 扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加

#include<iostream>
#include<vector>
#include<map>
using namespace std;
class ClassNum {
public:
    ClassNum(int n1 = 0, int n2 = 0) : _x(n1), _y(n2)
    {}
private:
    int _x;
    int _y;
};
int main() {
    int num1 = { 100 };//定于内置类型
    int num2{ 3 };//也可以不加=
    //数组
    int arr1[5] = { 1,3,4,5,6 };
    int arr2[] = { 4,5,6,7,8 };
    //STL中的容器
    vector<int>v{ 12,2 };
    map<int, int>mp{ {1,2},{3,4} };
    //自定义类型初始化
    ClassNum p{ 1, 2 };
    return 0;
}

For each

C++11 引入了基于范围的迭代写法,拥有了能够写出像 Python 类似的简洁的循环语句。就拿最常用的 vector 遍历举例:

int main()
{
    vector<int> v1={ 1,2,3,4,5,6 };
    for(auto i : v1)//范围for
    {
        cout << i << “ ”;
    }
    return 0;
}

如果用 for each 则需要传入一个处理函数,下面案例用 lambda 实现该函数:

输入案例:

abcstringabc
a

输出案例:

2
#include <iostream>
#include <algorithm>
using namespace std;
int getCharNum(string s, char c) {
    int count = 0;
    for_each(s.begin(), s.end(), [&](char ch) {
        if (c == ch) {
            count++;
        }
    });
    return count;
}
int main() {
    string s1;
    char ch;
    cin >> s1;
    cin >> ch;
    cout << getCharNum(s1, ch) << endl;
    return 0;
}

Lambda表达式

lambda 表达式实际上是一个匿名类函数,在编译时会将表达式转换为匿名类函数。

语法

[capture-list] (parameters) mutable -> return-type { statement}
[捕获列表](参数)->返回值{ 函数体 };

1.[capture-list]: 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据 [] 来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用。(不能省略)

2.(parameters): 参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同 () 一起省略。(没有参数可以省略)

3.mutable: 默认情况下,lambda 函数总是一个 const 函数,mutable 可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空),mutable 放在参数列表和返回值之间。(可以省略)

4.->returntype: 返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。(有没有返回值都可以省略)

5.{statement}: 函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的 变量。(不能省略)

捕获列表说明

捕捉列表描述了上下文中那些数据可以被 lambda 使用,以及使用的方式传值还是传引用。

1.[a,&b]: 其中 a 以复制捕获而 b 以引用捕获。

2.[this]: 以引用捕获当前对象(*this)。

3.[&]: 若存在,以引用捕获所有用于 lambda 体内的自动变量,并以引用捕获当前对象。

4.[=]: 若存在,以复制捕获所有用于 lambda 体内的自动变量,并以引用捕获当前对象。

5.[]: 不捕获,大部分情况下不捕获就可以了。


在 lambda 函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。

C++11 中最简单的 lambda 函数为:[]{}; 该lambda函数不能做任何事情,没有意义!

#include<iostream>
using namespace std;
void(*FP)(); //函数指针
int main()
{
    //最简单的lambda表达式, 该lambda表达式没有任何意义
    [] {};
    //省略参数列表和返回值类型,返回值类型由编译器推导为int
    int num1 = 3, num2 = 4;
    //省略了返回值类型,无返回值类型
    auto fun1 = [&num1, &num2](int num3) {num2 = num1 + num3; };
    fun1(100);
    cout << num1 << " " << num2 << endl;
    //捕捉列表可以是lambda表达式
    auto fun = [fun1] {cout << "great" << endl; };
    fun();
    //各部分都很完善的lambda函数
    auto fun2 = [=, &num2](int num3)->int {return num2 += num1 + num3; };
    cout << fun2(10) << endl;
    //复制捕捉x
    int x = 10;
    auto add_x = [x](int a) mutable { x *= 2; return a + x; };
    cout << add_x(10) << endl;
    //编译失败--->提示找不到operator=() 
    //auto fun3 = [&num1,&num2](int num3) {num2 = num1 + num3;};
    //fun1 = fun3; 
    //允许使用一个lambda表达式拷贝构造一个新的副本
    auto fun3(fun);
    fun();
    //可以将lambda表达式赋值给相同类型的函数指针
    auto f2 = [] {};
    FP = f2;
    FP();
    return 0;
}

函数对象与lambda表达式

函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了 operator() 运算符的类对象。

从使用方式上来看,函数对象与 lambda 表达式完全一样。

#include <iostream>
using namespace std;
class Rate{
public:
    Rate(double rate) : _rate(rate){}
    double operator()(double money, int year) {
        return money * _rate * year;
    }
private:
    double _rate;
};
int main() {
    //函数对象
    double rate = 0.6;
    Rate r1(rate);
    double rd = r1(20000, 2);
    cout << rd << endl; 
    //lambda
    auto r2 = [=](double money, int year)->double {return money * rate * year;};
    double rd2 = r2(20000, 2);
    cout << rd2 << endl;
    return 0;
}

在 C++98 中,对一个数据集合中的元素进行排序,可以使用 sort 方法。

#include <iostream>
#include <algorithm>
#include <functional>
using namespace std;
int main()
{
    int array[] = { 3,6,9,5,4,7,0,8,2,1 };
    //默认按照小于比较,排出来结果是升序
    sort(array, array + sizeof(array) / sizeof(array[0]));
    //如果需要降序,需要改变元素的比较规则
    sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
    return 0;
}

如果待排序元素为自定义类型,需要用户定义排序时的比较规则。

#include <iostream>
#include <algorithm>
#include <functional>
using namespace std;
struct Goods{
    string name;
    double price;
};
struct Compare{
    bool operator()(const Goods& gl, const Goods& gr)
    {
        return gl.price <= gr.price;
    }
};
int main()
{
    Goods gds[] = { { "苹果", 5.1 }, { "橙子", 9.2 }, { "香蕉", 3.6 }, {"菠萝",9.6} };
    sort(gds, gds + sizeof(gds) / sizeof(gds[0]), Compare());
    for (int i = 0; i < 4; i++) {
        cout << gds[i].name << " " << gds[i].price << endl;
    }
    return 0;
}

有了 lambda 表示,代码就可以写成如下:

#include <iostream>
#include <algorithm>
#include <functional>
using namespace std;
struct Goods{
    string name;
    double price;
};
int main()
{
    Goods gds[] = { { "苹果", 5.1 }, { "橙子", 9.2 }, { "香蕉", 3.6 }, {"菠萝",9.6} };
    sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& l, const Goods& r)->bool
         {
             return l.price < r.price;
         });
    for (int i = 0; i < 4; i++) {
        cout << gds[i].name << " " << gds[i].price << endl;
    }
    return 0;
}
目录
相关文章
|
1月前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
108 59
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(三)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(二)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(一)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
1月前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
34 0
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析继承机制(三)
【C++】面向对象编程的三大特性:深入解析继承机制
|
1月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析继承机制(二)
【C++】面向对象编程的三大特性:深入解析继承机制
|
1月前
|
安全 程序员 编译器
【C++】面向对象编程的三大特性:深入解析继承机制(一)
【C++】面向对象编程的三大特性:深入解析继承机制
|
1月前
|
存储 编译器 程序员
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(二)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值