C++11 新特性 学习笔记

C++11 新特性 | 侯捷C++11学习笔记

笔者作为侯捷C++11新特性课程的笔记进行记录,供自己查阅方便

文章目录

  • C++11 新特性 | 侯捷C++11学习笔记
    • 1.Variadic Templates
        • C++11支持函数模板的默认模板参数
        • C++11在函数模板和类模板中使用可变参数
      • 可变参数模板
          • 1) 可变参数函数模板
          • 2) 可变参数类模板
      • 参数包扩展
    • 2.Spaces in Template Expressions && nullptr && auto
      • 2.1 Spaces in Template Expressions(>>的改进)
      • 2.2 nullptr and std:nullptr_t
        • C++11 nullptr:初始化空指针
      • 2.3 auto
        • C++ auto类型推导完全攻略
        • auto 类型推导的语法和规则
        • auto 的高级用法
        • auto 的限制
        • auto 的应用(重点)
          • 使用 auto 定义迭代器
          • auto 用于[泛型编程](https://so.****.net/so/search?q=泛型编程&spm=1001.2101.3001.7020)
    • 3.Unifrom Lnitialization(统一初始化)
      • C++11列表初始化(统一了初始化方式)
      • 统一的初始化
    • 4.Initializer_list
    • 5. std::array容器
    • 6.explicit
    • 8.for循环
      • C++11 for循环(基于范围的循环)详解
      • C++11 for循环使用注意事项
    • 9.=default、=delete
      • 9.1 default
      • 9.2 delete
    • 10.Alias Template(using 用法)
    • 11.template template parameters(双重模版参数)
      • 解释
      • 语法
      • 例子
      • 注意
      • Q1:
      • Q2:
    • 12.Type Alias(类型别名,using用法)
    • 13.noexcept
      • noexcept的用法
      • noexcept的用途
      • 示例
    • 14.override
    • 15.final
      • 1. 用于虚函数
      • 2. 用于类定义
      • 总结
    • 16.decltype
      • C++ decltype类型推导完全攻略
          • exp 注意事项
        • decltype 推导规则
        • decltype 的实际应用
        • 汇总auto和decltype的区别
          • 语法格式的区别
    • 17.Lambda 表达式
      • C++11 lambda匿名函数用法详解
        • lambda匿名函数的定义
          • lambda匿名函数中的[外部变量]
    • 18.Rvalue references(右值引用)
      • C++左值和右值
      • C++右值引用
    • 19.移动构造函数的功能和用法
      • 深浅拷贝效率问题
      • C++移动构造函数(移动语义的具体实现)
    • 20. move()函数:将左值强制转换为右值
    • 21.Perfect Forwarding 完美转发及其实现
    • 22.array容器
    • 23.hashtable 无序容器
    • 24.tuple
      • tuple对象的创建
          • 1) 类的构造函数
          • 2) make_tuple()函数
      • tuple常用函数
    • 25.union 非受限联合体
          • 1. C++11 允许非 POD 类型
          • 2. C++11 允许联合体有静态成员
      • 非受限联合体的赋值注意事项
          • placement new 是什么?
      • 非受限联合体的匿名声明和“枚举式类”
    • 26.constexpr:验证是否为常量表达式
      • constexpr修饰普通变量
      • constexpr修饰函数
      • constexpr修饰类的构造函数
      • constexpr修饰模板函数
      • constexpr和const的区别
    • 27.long long超长整形详解
    • 28.智能指针
      • 1.shared_ptr智能指针(超级详细)
        • C++11 shared_ptr智能指针
          • 1、shared_ptr智能指针的创建
          • 2、shared_ptr模板类提供的成员方法
      • 2.unique_ptr智能指针
        • unique_ptr智能指针的创建
        • unique_ptr模板类提供的成员方法
      • 3.weak_ptr智能指针
        • weak_ptr智能指针
          • 1、weak_ptr指针的创建
          • 2) weak_ptr模板类提供的成员方法

1.Variadic Templates

C++11支持函数模板的默认模板参数

在 C++98/03 标准中,类模板可以有默认的模板参数,如下:

template <typename T, typename U = int, U N = 0>
struct Foo
{
    // ...
};

但是却不支持函数的默认模板参数:

template <typename T = int>  // error in C++98/03: default template arguments
void func()
{
    // ...
}

现在这一限制在 C++11 中被解除了。上面的 func 函数在 C++11 中可以直接使用,代码如下:

int main(void)
{
    func();   //T = int
    return 0;
}

此时模板参数 T 的类型就为默认值 int。从上面的例子中可以看出,当所有模板参数都有默认参数时,函数模板的调用如同一个普通函数。但对于类模板而言,哪怕所有参数都有默认参数,在使用时也必须在模板名后跟随<>来实例化。

除了上面提到的部分之外,函数模板的默认模板参数在使用规则上和其他的默认参数也有一些不同,它没有必须写在参数表最后的限制。甚至于,根据实际场景中函数模板被调用的情形,编译器还可以自行推导出部分模板参数的类型。

这意味着,当默认模板参数和编译器自行推导出模板参数类型的能力一起结合使用时,代码的书写将变得异常灵活。我们可以指定函数中的一部分模板参数采用默认参数,而另一部分使用自动推导,比如下面的例子:

template <typename R = int, typename U>
R func(U val)
{
    return val;
}
int main()
{
    func(97);               // R=int, U=int
    func<char>(97);         // R=char, U=int
    func<double, int>(97);  // R=double, U=int
    return 0;
}

C++11 标准中,我们可以像 func(97) 这样调用模板函数,因为编译器可以根据实参 97 自行推导出模板参数 U 的类型为 int,并且根据返回值 val=97 推导出 R 的类型也为 int;而 func(97) 手动指定了模板参数 R 的类型为 char(默认模板参数将无效),并通过实参 97 推导出了 U = int;最后 func<double,int>(97) 手动指定的 R 和 U 的类型值,因此无需编译器自行推导。

再次强调,当默认模板参数和自行推导的模板参数同时使用时,若无法推导出函数模板参数的类型,编译器会选择使用默认模板参数;如果模板参数即无法推导出来,又未设置其默认值,则编译器直接报错。例如:

template <typename T, typename U = double>
void func(T val1 = 0, U val2 = 0)
{
    //...
}
int main()
{
    func('c'); //T=char, U=double
    func();    //编译报错
    return 0;
}

其中,func(‘c’) 的这种调用方式,编译器通过实参 ‘c’ 可以推导出 T=char,但由于未传递第 2 个实参,因此模板参数 U 使用的是默认参数 double;但 func() 的调用方式是不行的,虽然 val1 设置有默认值,但编译器无法通过该默认值推导出模板参数 T 的类型。由此不难看出,编译器的自动推导能力并没有想象的那么强大。

总的来说,C++11 支持为函数模板中的参数设置默认值,在实际使用过程中,我们可以选择使用默认值,也可以尝试由编译器自行推导得到,还可以亲自指定各个模板参数的类型。

C++11在函数模板和类模板中使用可变参数

所谓可变参数,指的是参数的个数和类型都可以是任意的。提到参数,大家会第一时间想到函数参数,除此之外 C++ 的模板(包括函数模板和类模板)也会用到参数。

对于函数参数而言,C++ 一直都支持为函数设置可变参数,最典型的代表就是 printf() 函数,它的语法格式为:

int printf ( const char * format, ... );

...就表示的是可变参数,即 printf() 函数可以接收任意个参数,且各个参数的类型可以不同,例如:

printf("%d", 10);printf("%d %c",10, 'A');printf("%d %c %f",10, 'A', 1.23);

我们通常将容纳多个参数的可变参数称为参数包。借助 format 字符串,printf() 函数可以轻松判断出参数包中的参数个数和类型。

下面的程序中,自定义了一个简单的可变参数函数:

#include <iostream>
#include <cstdarg>//可变参数的函数
void vair_fun(int count, ...)
{    
    va_list args;    
    va_start(args, count);    
    for (int i = 0; i < count; ++i)    
    {        
        int arg = va_arg(args, int);        
        std::cout << arg << " ";    
    }    
    va_end(args);
}
int main()
{    
    //可变参数有 4 个,分别为 10、20、30、40    
    vair_fun(4, 10, 20, 30,40);    
    return 0;
}

程序中的 vair_fun() 函数有 2 个参数,一个是 count,另一个就是 … 可变参数。我们可以很容易在函数内部使用 count 参数,但要想使用参数包中的参数,需要借助<cstdarg>头文件中的 va_start、va_arg 以及 va_end 这 3 个带参数的宏:

  • va_start(args, count):args 是 va_list 类型的变量,我们可以简单的将其视为 char * 类型。借助 count 参数,找到可变参数的起始位置并赋值给 args;
  • va_arg(args, int):调用 va_start 找到可变参数起始位置的前提下,通过指明参数类型为 int,va_arg 就可以将可变参数中的第一个参数返回;
  • va_end(args):不再使用 args 变量后,应及时调用 va_end 宏清理 args 变量。

注意,借助 va_arg 获取参数包中的参数时,va_arg 不具备自行终止的能力,所以程序中借助 count 参数控制 va_arg 的执行次数,继而将所有的参数读取出来。控制 va_arg 执行次数还有其他方法,比如读取到指定数据时终止。

使用 … 可变参数的过程中,需注意以下几点:

  1. … 可变参数必须作为函数的最后一个参数,且一个函数最多只能拥有 1 个可变参数。
  2. 可变参数的前面至少要有 1 个有名参数(例如上面例子中的 count 参数);
  3. 当可变参数中包含 char 类型的参数时,va_arg 宏要以 int 类型的方式读取;当可变参数中包含 short 类型的参数时,va_arg 宏要以 double 类型的方式读取。

**需要注意的是,… 可变参数的方法仅适用于函数参数,并不适用于模板参数。**C++11 标准中,提供了一种实现可变模板参数的方法。

可变参数模板

C++ 11 标准发布之前,函数模板和类模板只能设定固定数量的模板参数。C++11 标准对模板的功能进行了扩展,允许模板中包含任意数量的模板参数,这样的模板又称可变参数模板。

1) 可变参数函数模板

先讲解函数模板,如下定义了一个可变参数的函数模板:

template<typename... T>
void vair_fun(T...args) {
    //函数体
}

模板参数中, typename(或者 class)后跟 … 就表明 T 是一个可变模板参数,它可以接收多种数据类型,又称模板参数包。vair_fun() 函数中,args 参数的类型用 T… 表示,表示 args 参数可以接收任意个参数,又称函数参数包。

这也就意味着,此函数模板最终实例化出的 vair_fun() 函数可以指定任意类型、任意数量的参数。例如,我们可以这样使用这个函数模板:

vair_fun();
vair_fun(1, "abc");
vair_fun(1, "abc", 1.23);
123

使用可变参数模板的难点在于,如何在模板函数内部“解开”参数包(使用包内的数据),这里给大家介绍两种简单的方法。

【递归方式解包】
先看一个实例:

#include <iostream>
using namespace std;
//模板函数递归的出口
void vir_fun() {
}
template <typename T, typename... args>
void vir_fun(T argc, args... argv)
{
    cout << argc << endl;
    //开始递归,将第一个参数外的 argv 参数包重新传递给 vir_fun
    vir_fun(argv...);
}
int main()
{
    vir_fun(1, "http://www.biancheng.net", 2.34);
    return 0;
}

执行结果为:

1
http://www.biancheng.net
2.34

分析一个程序的执行流程:

  • 首先,main() 函数调用 vir_fun() 模板函数时,根据所传实参的值,可以很轻易地判断出模板参数 T 的类型为 int,函数参数 argc 的值为 1,剩余的模板参数和函数参数都分别位于 args 和 argv 中;
  • vir_fun() 函数中,首先输出了 argc 的值(为 1),然后重复调用自身,同时将函数参数包 argv 中的数据作为实参传递给形参 argc 和 argv;
  • 再次执行 vir_fun() 函数,此时模板参数 T 的类型为 char*,输出 argc 的值为 “http:www.biancheng.net”。再次调用自身,继续将 argv 包中的数据作为实参;
  • 再次执行 vir_fun() 函数,此时模板参数 T 的类型为 double,输出 argc 的值为 2.34。再次调用自身,将空的 argv 包作为实参;
  • 由于 argv 包没有数据,此时会调用无任何形参、函数体为空的 vir_fun() 函数,最终执行结束。

以递归方式解包,一定要设置递归结束的出口。例如本例中,无形参、函数体为空的 vir_fun() 函数就是递归结束的出口。

【非递归方法解包】
借助逗号表达式和初始化列表,也可以解开参数包。

以 vir_fun() 函数为例,下面程序演示了非递归方法解包的过程:

#include <iostream>
using namespace std;
template <typename T>
void dispaly(T t) {
    cout << t << endl;
}
template <typename... args>
void vir_fun(args... argv)
{
    //逗号表达式+初始化列表
    int arr[] = { (dispaly(argv),0)... };
}
int main()
{
    vir_fun(1, "http://www.biancheng.net", 2.34);
    return 0;
}

这里重点分析一下第 13 行代码,我们以{ }初始化列表的方式对数组 arr 进行了初始化, (display(argv),0)… 会依次展开为 (display(1),0)、(display(“http://www.biancheng.net”),0) 和 (display(2.34),0)。也就是说,第 13 行代码和如下代码是等价的:

 int arr[] = { (dispaly(1),0), (dispaly("http://www.biancheng.net"),0), (dispaly(2.34),0) };

可以看到,每个元素都是一个逗号表达式,以 (display(1), 0) 为例,它会先计算 display(1),然后将 0 作为整个表达式的值返回给数组,因此 arr 数组最终存储的都是 0。arr 数组纯粹是为了将参数包展开,没有发挥其它作用。

2) 可变参数类模板

image-20241010102752119

printf递归调用,解包,和上面一样。要注意函数1,算是一个终止条件吧。

Types可以理解为一个包类型,表示参数可以有很多类型

image-20241010204756484

image-20241010103142620

答:2比较特化(特别),3比较泛化

image-20241010103807043

tuple继承,(看右上角)一开始是int,float,string,继承为float,string,一层一层继承下去,下面的例子也是说的这个

C++11 标准中,类模板中的模板参数也可以是一个可变参数。C++ 11 标准提供的 typle 元组类就是一个典型的可变参数模板类,它的定义如下:

template <typename... Types>
class tuple;

和固定模板参数的类不同,tuple 模板类实例化时,可以接收任意数量、任意类型的模板参数,例如:

std:tuple<> tp0;
std::tuple<int> tp1 = std::make_tuple(1);
std::tuple<int, double> tp2 = std::make_tuple(1, 2.34);
std::tuple<int, double, string> tp3 = std::make_tuple(1, 2.34, "http://www.biancheng.net");

如下代码展示了一个支持可变参数的类模板:

#include <iostream>
//声明模板类demo
template<typename... Values> class demo;
//继承式递归的出口
template<> class demo<> {};
//以继承的方式解包
template<typename Head, typename... Tail>
class demo<Head, Tail...>
    : private demo<Tail...>
{
public:
    demo(Head v, Tail... vtail) : m_head(v), demo<Tail...>(vtail...) {
        dis_head();
    }
    void dis_head() { std::cout << m_head << std::endl; }
protected:
    Head m_head;
};
int main() {
    demo<int, float, std::string> t(1, 2.34, "http://www.biancheng.net");
    return 0;
}

程序中,demo 模板参数中的 Tail 就是一个参数包,解包的方式是以“递归+继承”的方式实现的。具体来讲,demo<Head, Tail…> 类实例化时,由于其继承自 demo<Tail…> 类,因此父类也会实例化,一直递归至 Tail 参数包为空,此时会调用模板参数列表为空的 demo 模板类。

程序的输出结果为:

http://www.biancheng.net
2.34
1

参数包扩展

参数包扩展是C++模板编程中的一个重要特性,它允许在模板中接受任意数量和类型的参数。参数包的扩展是在编译时由编译器自动处理的,其过程可以视为将参数包中的每个参数都作为独立的参数进行展开。

具体来说,参数包扩展的语法是在参数名后面加上省略号(…)。这个省略号表示该参数是一个参数包,可以包含零个或多个参数。在函数调用或模板实例化时,编译器会根据上下文自动将参数包中的每个参数展开为独立的参数。

以下是一些关于参数包扩展的详细解释和示例:

函数模板中的参数包扩展:
在函数模板中,参数包可以用于定义可变数量的函数参数。例如:

cpp
template<typename… Args>
void print(Args… args) {
// … 这里可以使用参数包展开来逐个打印参数
}
在这个例子中,Args… 是一个类型参数包,它表示函数可以接受任意数量和类型的参数。在函数体内,可以使用参数包展开来逐个处理这些参数。

参数包展开的方式:
参数包展开有多种方式,包括递归函数、逗号表达式、折叠表达式等。以下是几种常见的展开方式:

递归函数:通过定义一个递归函数来逐个处理参数包中的参数。这种方式需要定义一个递归终止条件,以避免无限递归。
逗号表达式:利用C++中的逗号表达式(,),它保证从左到右的顺序求值,可以在逗号表达式中逐个展开参数包。
折叠表达式(C++17引入):折叠表达式提供了一种简洁的方式来展开参数包,并可以对参数包中的元素进行折叠操作(如求和、求积等)。
示例:
以下是一个使用折叠表达式来展开参数包并打印每个参数的示例:

cpp
#include

template<typename… Args>
void print(Args… args) {
(std::cout << … << args) << std::endl; // 使用折叠表达式展开参数包
}

int main() {
print(1, 2, 3, “hello”); // 输出:123hello
return 0;
}
在这个例子中,折叠表达式 (std::cout << … << args) 将参数包 args 中的每个参数都展开为 std::cout << 操作的一部分,并依次打印出来。

需要注意的是,虽然参数包扩展提供了很大的灵活性,但也需要谨慎使用以避免编译错误或运行时错误。在使用参数包时,应确保传入的参数类型与函数模板或类模板中定义的参数包类型相匹配。

2.Spaces in Template Expressions && nullptr && auto

2.1 Spaces in Template Expressions(>>的改进)

image-20241010104301186

就是容器嵌套的时候不需要加空格了(之前害怕两个>>解析为输入)

2.2 nullptr and std:nullptr_t

  • nullptr的类型是std::nullptr_t
C++11 nullptr:初始化空指针

实际开发中,避免产生“野指针”最有效的方法,就是在定义指针的同时完成初始化操作,即便该指针的指向尚未明确,也要将其初始化为空指针。

所谓“野指针”,又称“悬挂指针”,指的是没有明确指向的指针。野指针往往指向的是那些不可用的内存区域,这就意味着像操作普通指针那样使用野指针(例如 &p),极可能导致程序发生异常。

C++98/03 标准中,将一个指针初始化为空指针的方式有 2 种:

int *p = 0;
int *p = NULL; //推荐使用

可以看到,我们可以将指针明确指向 0(0x0000 0000)这个内存空间。一方面,明确指针的指向可以避免其成为野指针;另一方面,大多数操作系统都不允许用户对地址为 0 的内存空间执行写操作,若用户在程序中尝试修改其内容,则程序运行会直接报错。

相比第一种方式,我们更习惯将指针初始化为 NULL。值得一提的是,NULL 并不是 C++ 的关键字,它是 C++ 为我们事先定义好的一个宏,并且它的值往往就是字面量 0(#define NULL 0)。

C++ 中将 NULL 定义为字面常量 0,虽然能满足大部分场景的需要,但个别情况下,它会导致程序的运行和我们的预期不符。例如:

#include <iostream>
using namespace std;
void isnull(void *c){
    cout << "void*c" << endl;
}
void isnull(int n){
    cout << "int n" << endl;
}
int main() {
    isnull(0);
    isnull(NULL);
    return 0;
}

程序执行结果为:

int n
int n

对于 isnull(0) 来说,显然它真正调用的是参数为整形的 isnull() 函数;而对于 isnull(NULL),我们期望它实际调用的是参数为 void*c 的 isnull() 函数,但观察程序的执行结果不难看出,并不符合我们的预期。

C++ 98/03 标准中,如果我们想令 isnull(NULL) 实际调用的是 isnull(void* c),就需要对 NULL(或者 0)进行强制类型转换:

isnull( (void*)NULL );
isnull( (void*)0 );

如此,才会成功调用我们预期的函数(读者可自行执行此代码,观察输出结果)。

由于 C++ 98 标准使用期间,NULL 已经得到了广泛的应用,出于兼容性的考虑,C++11 标准并没有对 NULL 的宏定义做任何修改。为了修正 C++ 存在的这一 BUG,C++ 标准委员会最终决定另其炉灶,在 C++11 标准中引入一个新关键字,即 nullptr。

在使用 nullptr 之前,读者需保证自己使用的编译器支持该关键字。以 Visual Studio 和 codeblocks 为例,前者早在 2010 版本就对 C++ 11 标准中的部分特性提供了支持,其中就包括 nullptr;如果使用后者,读者需将其 G++ 编译器版本至少升级至 4.6.1(同时开启 -std=c++0x 编译选项)。

nullptr 是 nullptr_t 类型的右值常量,专用于初始化空类型指针。nullptr_t 是 C++11 新增加的数据类型,可称为“指针空值类型”。也就是说,nullpter 仅是该类型的一个实例对象(已经定义好,可以直接使用),如果需要我们完全定义出多个同 nullptr 完全一样的实例对象。

值得一提的是,nullptr 可以被隐式转换成任意的指针类型。举个例子:

int * a1 = nullptr;
char * a2 = nullptr;
double * a3 = nullptr;

显然,不同类型的指针变量都可以使用 nullptr 来初始化,编译器分别将 nullptr 隐式转换成 int*、char* 以及 double* 指针类型。

另外,通过将指针初始化为 nullptr,可以很好地解决 NULL 遗留的问题,比如:

#include <iostream>
using namespace std;
void isnull(void *c){
    cout << "void*c" << endl;
}
void isnull(int n){
    cout << "int n" << endl;
}
int main() {
    isnull(NULL);
    isnull(nullptr);
    return 0;
}

程序执行结果为:

int n
void*c

借助执行结果不难看出,由于 nullptr 无法隐式转换为整形,而可以隐式匹配指针类型,因此执行结果和我们的预期相符。

总之在 C++11 标准下,相比 NULL 和 0,使用 nullptr 初始化空指针可以令我们编写的程序更加健壮。

2.3 auto

C++ auto类型推导完全攻略

C++11 之前的版本(C++98 和 C++ 03)中,定义变量或者声明变量之前都必须指明它的类型,比如 int、char 等;但是在一些比较灵活的语言中,比如 C#JavaScriptPHPPython 等,程序员在定义变量时可以不指明具体的类型,而是让编译器(或者解释器)自己去推导,这就让代码的编写更加方便。

C++11 为了顺应这种趋势也开始支持自动类型推导了!C++11 使用 auto 关键字来支持自动类型推导。

auto 类型推导的语法和规则

在之前的 C++ 版本中,auto 关键字用来指明变量的存储类型,它和 static 关键字是相对的。auto 表示变量是自动存储的,这也是编译器的默认规则,所以写不写都一样,一般我们也不写,这使得 auto 关键字的存在变得非常鸡肋。

C++11 赋予 auto 关键字新的含义,使用它来做自动类型推导。也就是说,使用了 auto 关键字以后,编译器会在编译期间自动推导出变量的类型,这样我们就不用手动指明变量的数据类型了。

auto 关键字基本的使用语法如下:

auto name = value;

name 是变量的名字,value 是变量的初始值。

注意:auto 仅仅是一个占位符,在编译器期间它会被真正的类型所替代。或者说,C++ 中的变量必须是有明确类型的,只是这个类型是由编译器自己推导出来的。

auto 类型推导的简单例子:

auto n = 10;
auto f = 12.8;
auto p = &n;
auto url = “http://c.biancheng.net/cplus/”;

下面我们来解释一下:

  • 第 1 行中,10 是一个整数,默认是 int 类型,所以推导出变量 n 的类型是 int。
  • 第 2 行中,12.8 是一个小数,默认是 double 类型,所以推导出变量 f 的类型是 double。
  • 第 3 行中,&n 的结果是一个 int* 类型的指针,所以推导出变量 p 的类型是 int*。
  • 第 4 行中,由双引号""包围起来的字符串是 const char* 类型,所以推导出变量 url 的类型是 const char*,也即一个常量指针。

我们也可以连续定义多个变量:

int n = 20;
auto *p = &n, m = 99;

先看前面的第一个子表达式,&n 的类型是 int*,编译器会根据 auto *p 推导出 auto 为 int。后面的 m 变量自然也为 int 类型,所以把 99 赋值给它也是正确的。

这里我们要注意,推导的时候不能有二义性。在本例中,编译器根据第一个子表达式已经推导出 auto 为 int 类型,那么后面的 m 也只能是 int 类型,如果写作m=12.5就是错误的,因为 12.5 是double 类型,这和 int 是冲突的。

还有一个值得注意的地方是:使用 auto 类型推导的变量必须马上初始化,这个很容易理解,因为 auto 在 C++11 中只是“占位符”,并非如 int 一样的真正的类型声明。

auto 的高级用法

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

int  x = 0;
auto *p1 = &x;   //p1 为 int *,auto 推导为 int
auto  p2 = &x;   //p2 为 int*,auto 推导为 int*
auto &r1  = x;   //r1 为 int&,auto 推导为 int
auto r2 = r1;    //r2 为  int,auto 推导为 int

下面我们来解释一下:

  • 第 2 行代码中,p1 为 int* 类型,也即 auto * 为 int *,所以 auto 被推导成了 int 类型。
  • 第 3 行代码中,auto 被推导为 int* 类型,前边的例子也已经演示过了。
  • 第 4 行代码中,r1 为 int & 类型,auto 被推导为 int 类型。
  • 第 5 行代码是需要重点说明的,r1 本来是 int& 类型,但是 auto 却被推导为 int 类型,这表明当=右边的表达式是一个引用类型时,auto 会把引用抛弃,直接推导出它的原始类型。

接下来,我们再来看一下 auto 和 const 的结合:

int  x = 0;
const  auto n = x;  //n 为 const int ,auto 被推导为 int
auto f = n;      //f 为 const int,auto 被推导为 int(const 属性被抛弃)
const auto &r1 = x;  //r1 为 const int& 类型,auto 被推导为 int
auto &r2 = r1;  //r1 为 const int& 类型,auto 被推导为 const int 类型`在这里插入代码片`

下面我们来解释一下:

  • 第 2 行代码中,n 为 const int,auto 被推导为 int。
  • 第 3 行代码中,n 为 const int 类型,但是 auto 却被推导为 int 类型,这说明当=右边的表达式带有 const 属性时, auto 不会使用 const 属性,而是直接推导出 non-const 类型。
  • 第 4 行代码中,auto 被推导为 int 类型,这个很容易理解,不再赘述。
  • 第 5 行代码中,r1 是 const int & 类型,auto 也被推导为 const int 类型,这说明当 const 和引用结合时,auto 的推导将保留表达式的 const 类型。

最后我们来简单总结一下 auto 与 const 结合的用法:

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

前面介绍推导规则的时候我们说过,使用 auto 的时候必须对变量进行初始化,这是 auto 的限制之一。那么,除此以外,auto 还有哪些其它的限制呢?

  1. auto 不能在函数的参数中使用。

这个应该很容易理解,我们在定义函数的时候只是对参数进行了声明,指明了参数的类型,但并没有给它赋值,只有在实际调用函数的时候才会给参数赋值;而 auto 要求必须对变量进行初始化,所以这是矛盾的。

  1. auto 不能作用于类的非静态成员变量(也就是没有 static 关键字修饰的成员变量)中。
  2. auto 关键字不能定义数组,比如下面的例子就是错误的:

char url[] = “http://c.biancheng.net/”;
auto str[] = url; //arr 为数组,所以不能使用 auto

  1. auto 不能作用于模板参数,请看下面的例子:
template <typename T>
class A{
    //TODO:
};
int  main(){
    A<int> C1;
    A<auto> C2 = C1;  //错误
    return 0;
}
auto 的应用(重点)

说了那么多 auto 的推导规则和一些注意事

上一篇:023 elasticsearch查询数据 高亮 分页 中文分词器 field的数据类型


下一篇:创业一年半,我的团队终于走向正轨了!-未来计划