跳转至

C语言过渡C++基础知识

约 5945 个字 1349 行代码 1 张图片 预计阅读时间 37 分钟

C++关键字

在C++中,有63个关键字,而C语言只有32个关键字

C++
1
2
3
4
5
asm do if return try continue auto double inline short typedef for
bool dynamic_cast int signed typeid public break else long sizeof typename
throw case enum mutable static union wchar_t catch explicit namespace         
static_cast unsigned default char export new struct using friend class extern operator switch virtual register const false private template void true
const_cast float protected this volatile while delete goto reinterpret_cast

Note

这些关键字不需要死记硬背

C++命名空间

命名空间的介绍

在C++中,变量、函数和类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。而使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的,而定义一个命名空间就相当于定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中

例如,在C语言中,对于下面的程序

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>
#include <stdlib.h>

int rand = 10;

int main()
{
    printf("%d\n", rand);
    return 0;
}
报错内容
rand: 重定义以前的定义是函数

因为在C语言中,存在一个名为rand()的函数,此时若将变量名定义为rand会与标准库中rand()函数名产生冲突,而C语言能解决这个问题方法只有更改变量名,否则没有其他办法

但是在C++中,可以使用命名空间解决这个问题

所谓命名空间指当前命名的变量所处的空间,在命名空间中,可以声明类、变量、函数、模版和其他命名空间,例如

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//命名空间1
namespace test1
{
    //变量
    int i = 0;

    //类型
    struct Student
    {
        int age;
        char name[20];
    };

    //函数
    int add(int x, int y)
    {
        return x + y;
    }
}

在C++中,使用namespace关键字创建命名空间,语法如下:

C++
1
2
3
4
namespace 命名空间名
{
    //变量/函数/类型
}//注意最后一行不需要分号,不同于结构体

在C++中,命名空间可以嵌套定义,例如:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//命名空间2
namespace test2
{
    int num = 0;
    //命名空间3
    namespace test3
    {
        int num = 0;
    }
}

对于上面的代码,在命名空间test2中嵌套定义了一个命名空间test3

有了命名空间,就可以解决上面C语言出现的问题,解决方法如下:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
//头文件NameSpace.h中的命名空间4
//将rand变量放入命名空间test4中
namespace test4
{
    int rand = 10;
}

//测试文件
//C++主函数
#include <iostream>
#include <stdlib.h>
#include "NameSpace.h"

int main()
{
    //调用命名空间4中的rand变量,而不是标准库中的rand()函数
    printf("%d\n", test4::rand);
    return 0;
}
输出结果
10

作用域限定符

在C++中,::表示作用域限定符,使用方法如下:

C++
1
空间 :: 变量/类型/对象名

::左侧空间为空时,默认在全局中寻找::右侧的内容,例如:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//NameSpace.h文件中的命名空间2与嵌套的命名空间3
//命名空间2
namespace test2
{
    int num = 20;
    //命名空间3
    namespace test3
    {
        int num = 30;
    }
}

//C++主函数
#include <iostream>
#include <stdlib.h>
#include "NameSpace.h"

//全局变量
int num = 10;

int main()
{
    //作用域限定符
    //局部变量
    int num = 0;
    printf("%d\n", num);
    //全局变量
    printf("%d\n", ::num);
    //指定命名空间的变量
    printf("%d\n", test2::num);
    //嵌套的命名空间的变量
    printf("%d\n", test2::test3::num);
    return 0;
}
输出结果
0
10
20
30

命名空间的使用

在C++中,有三种使用命名空间的方法:

  1. 加命名空间名称及作用域限定符,例如N::a
  2. 使用using将命名空间中某个成员引入,也称部分展开,例如using N::a
  3. 使用using将命名空间整体引入,也称全局展开,例如using namespace N

测试实例:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//NameSpace.h中的命名空间1
namespace test1
{
    //变量
    int i = 10;

    //类型
    struct Student
    {
        int age;
        char name[20];
    };

    //函数
    int add(int x, int y)
    {
        return x + y;
    }
}

//加命名空间名称及作用域限定符
#include <iostream>
#include "NameSpace.h"

int main()
{
    printf("%d", test1::i);

    return 0;
}
输出结果
10

//部分展开
#include <iostream>
#include "NameSpace.h"
using test1::i;// 部分展开

int main()
{
    printf("%d", i);
    return 0;
}
输出结果
10

//全局展开
#include <iostream>
#include "NameSpace.h"
using namespace test1;

int main()
{
    printf("%d\n", i);
    return 0;
}
输出结果
10

Tip

在实际使用过程中,更推荐指定以及部分展开,如果只是自己练习时,更推荐使用全局展开 注意,使用using namespace N并不代表在函数中不可以指定,部分展开和全局展开都只是改变寻找方式,并不是使用了其中一种方式其他方式不可以再使用,而如果已经全局展开,那么再指定将直接去指定的命名空间找,而不是在全局展开的命名空间中找

当存在两个相同的命名空间时,会被合并成一个命名空间,而不是直接覆盖,例如

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//NameSpace.h中的两个重名的命名空间
//两个同名的命名空间
namespace test5
{
    int num1 = 10;
}

namespace test5
{
    int num2 = 20;
}

//测试文件
//同名命名空间合并
#include <iostream>
#include "NameSpace.h"
using namespace std;
using namespace test5;

int main()
{
    cout << test5::num1 << endl;
    cout << test5::num2 << endl;
    return 0;
}
输出结果
10
20

注意,如果是同一文件夹下有多个.cpp的源文件,那么多个源文件中只能有一个源文件包含定义在头文件的命名空间,否则将会出现命名空间中的变量多次定义的问题,例如

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//NameSpace.h头文件
namespace test5
{
    int num1 = 10;
}

//test.cpp中包含头文件
#include "NameSpace.h"

//test1.cpp中包含头文件
#include "NameSpace.h"

//此时会提示报错,因为test.cpp和test1.cpp两个文件中都出现了相同的命名空间中的变量,导致变量多次定义

正确的解决方案如下:

  1. 只有一个源文件,命名空间放置在头文件,包括声明和定义

    C++
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    //如果没有多个源文件,那么就将命名空间放在头文件中,源文件引入即可
    //NameSpace.h
    namespace test5
    {
        int num1 = 10;
        //使用static修饰的变量
        static int num2 = 10;
    }
    
    //只有一个源文件
    #include <iostream>
    #include "NameSpace.h"
    using namespace std;
    using namespace test5;
    
    int main()
    {
        cout << test5::num1 << endl;
        cout << test5::num2 << endl;
        return 0;
    }
    输出结果
    10
    10
    
  2. 多个源文件时,声明和定义分离,将命名空间放置在其中一个源文件中,其中的变量只需要声明,不需要定义,其余源文件只需要使用extern引入即可,引入的位置需要对变量进行定义

    C++
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //test.cpp
    namespace  test5
    {
        int num1;// 只有声明
    }
    
    //test1.cpp
    #include <iostream>
    #include "NameSpace.h"
    using namespace std;
    extern int num1 = 1;// 其余文件定义
    
    int main()
    {
        cout << num1 << endl;
        return 0;
    }
    输出结果
    1
    
  3. 使用static防止变量多次定义,一般用于需要将命名空间放置在头文件,并且声明和定义不分离

    C++
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    //NameSpace.h
    namespace test5
    {
        static int num1 = 10;
        struct test
        {
            int i;
        };
        static int add(int i)
        {
            return i;
        }
    }
    
    //test.cpp
    #include "NameSpace.h"
    
    //test1.cpp
    #include <iostream>
    #include "NameSpace.h"
    using namespace std;
    int main()
    {
        test5::test p1 = {10};
        cout << test5::num1 << endl;
        cout << p1.i << endl;
        cout << test5::add(1) << endl;
        return 0;
    }
    输出结果
    10
    10
    1
    

因为C++中结构体是需要创建对象后才分配空间,所以当只要结构体对象没有重定义,命名空间的结构体就不会发生冲突

C++的输入以及输出

在C++中,可以使用coutcin配合流插入运算符<<和流提取运算符>>使用,例如:

Tip

使用coutcin时需要包含头文件iostream,注意C++的标准库头文件不带有.h,并且需要引入命名空间std(C++中的标准命名空间)或者直接指定。因此推荐使用<iostream>+std的方式

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//C++的输入和输出
#include <iostream>
using namespace std;

int main()
{
    int num = 0;
    cout << "请输入数值:";
    cin >> num;
    cout << num << endl;

    return 0;
}
输入
10
输出结果
请输入数值10
10

在上面的代码中,定义了一个num变量,通过标准输入对象cin和流提取运算符>>控制变量num的输入,不同于C语言,此处输入可以不需要取地址运算符&,对变量内容的输出使用标准输出对象cout和流插入运算符<<控制内容的输出,而endl表示换行符,作用效果类似'\n'

Note

对于上面代码中的

C++
1
cout << num << endl;

可以连续使用<<,上面的代码相当于

C++
1
(cout << num) << endl;

对于<<运算符来说,执行完一次后会返回该运算符左侧的对象cout,此时就相当于

C++
1
2
cout << num;
cout << endl;

对于>>运算符也是如此

在C++中,输入和输出可以自动识别变量类型,故输出和输入不需要占位符

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

int main()
{
    int num = 0;
    double num1 = 0;
    char c = 0;
    cin >> num >> num1 >> c;//多组内容的输入,相当于scanf("%d%lf%c", &num, &num1, &c);
    cout << num << ' ' << num1 << ' ' << c;//多组内容输出,相当于printf("%d %f %c", num, num1, c);

    return 0;
}
输入
1 2.5 c
输出结果
1 2.5 c

Note

scanf一样,cin会自动忽略空白字符

  • 也可以使用指定的方式使用coutcin
C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//C++的输入和输出
#include <iostream>

int main()
{
    int num = 0;
    std::cin >> num;
    std::cout << num << std::endl;

    return 0;
}
输入
10
输出
10

如果需要输入连续的字符到string对象中,此时可以使用istream中的getline()方法读取包含空格在内的字符串直到遇到\ngetline()函数原型如下

C++
1
istream& getline (istream& is, string& str);

Quote

如果需要正常结束程序时,在Windows下可以使用快捷键Ctrl+D

对于使用cout输出字符串时,可以使用空白字符串作为两个独立的字符串拼接,例如下面的代码:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream>
#include <string>

int main()
{
    std::cout << "我是一个"
                 ""
                 "大聪明"
              << std::endl;
    return 0;
}

输出结果
我是一个大聪明

Note

需要注意,用作拼接作用的字符串一定不能含有非空白内容(即除空格、制表符和换行符以外)

C++中的缺省参数

缺省参数的介绍

在C++中,缺省参数是指在函数的定义以及声明中可以为形式参数赋值,例如

C++
1
2
3
4
int add(int x = 0, int y = 0)
{
    return (x + y);
}

在上面的代码中,变量x和变量y赋值为0,称xy缺省参数,两个0为两个缺省参数的缺省值,缺省值必须是常量或者全局变量

如果调用add函数时不传入实际参数时,则add函数直接使用缺省参数的数值进行计算,例如

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

int add(int x = 0, int y = 0)
{
    return (x + y);
}

int main()
{
    //调用add函数不传参数
    cout << add() << endl;
    //调用add函数传一个参数,使用右侧第一个缺省值
    cout << add(1) << endl;
    //调用add函数传两个参数
    cout << add(1, 2) << endl;

    return 0;
}
输出结果
0
1
3

缺省参数的使用

在使用缺省参数时,必须遵循从右往左连续使用,不可以跳跃式使用

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//缺省参数
#include <iostream>
using namespace std;

int add1(int x = 0, int y = 0, int z = 0)
{
    return (x + y + z);
}

int main()
{
    //使用缺省参数时必须从右往左连续使用

    //正确使用方法
    add1(1, 2);//使用第三个缺省参数,使用右侧第一个缺省参数
    add1(1);//使用右侧第二个和第三个缺省参数
    add1();//使用全部缺省参数
    add1(1, 2, 3);//不使用缺省参数

    //错误使用方法
    add1(1, , 2);//不可以直接使用第二个缺省参数
    add1(, 1, 2);//不可以直接使用第一个缺省参数
    add1(, , 1);//不可以直接使用第一个和第二个缺省参数
    add1(, 1, );//不可以直接使用第一个和第三个缺省参数

    return 0;
}

Abstract

从右往左使用意思是,当调用函数给函数传递实际参数时,第一个实参对应着第一个第一个形参,第二个实参对应着第二个形参,第三个实参对应着第三个形参。使用缺省参数时,当只传递一个实际参数时,该实际参数对应着第一个形参,此时使用第二个和第三个缺省参数,同理,当传递两个实际参数时,两个实际参数对应的前两个形参,使用第三个缺省参数。总是满足最右边的缺省参数是第一个使用的
连续使用意思是,给函数传递实际参数到形参时,必须满足从左往右传递实际参数给形参,从而满足从左往右使用缺省参数

缺省参数的分类

在C++中,缺省参数分为

  1. 全缺省参数
  2. 半缺省参数

全缺省参数

全缺省参数是指在函数声明或定义时,所有变量都是缺省参数,例如

C++
1
2
3
4
int add1(int x = 0, int y = 0, int z = 0)
{
    return (x + y);
}

半缺省参数

半缺省参数是指在函数声明或定义中,部分变量是缺省参数,例如

C++
1
2
3
4
int add1(int x, int y = 0, int z = 0)//只有y和z是缺省变量
{
    return (x + y + z);
}

半缺省参数中,必须满足从右往左依次连续给出,不可以跳跃式给缺省值(由于从右往左使用的原则),不是缺省参数的需要在调用时传递实际参数,例如

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//半缺省参数
int add2(int x, int y = 0, int z = 0)
{
    return (x + y + z);
}

int add3(int x, int y, int z = 0)
{
    return (x + y + z);
}

//不可以跳跃式给缺省值
int add4(int x = 0, int y, int z)// 跳过右侧开始的第一个和第二个参数,给第三个参数x缺省值
{
    return (x + y + z);
}

int add5(int x, int y = 0, int z)// 跳过右侧开始的第一个参数给第二个参数y缺省值
{
    return (x + y + z);
}

int main()
{
    add2(1);//只有一个不是缺省参数时,必须为该形参传递实参
    add3(1, 2);//有两个不是缺省参数时,必须为两个形参传递实参

    add4(, 2, 3);//不可以使用,需要满足从右往左使用缺省参数的原则
    add5(1, , 3);//不可以使用,需要满足从右往左使用缺省参数的原则
    return 0;
}

声明和定义函数缺省参数时,不能同时出现缺省参数和缺省值,一般建议声明时给缺省参数和缺省值,而定义时不给缺省值,此时编译器识别使用声明中的缺省值,例如

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
//NameSpace.h中的函数声明
int add6(int a = 20);

//测试文件
//定义不给缺省值
int add6(int a)
{
    return a;
}

#include <iostream>
#include "NameSpace.h"
using namespace std;

int main()
{
    cout<<add6(2)<<' ';
    cout<<add6()<<'\n';// 使用声明中的缺省值
    return 0;
}
输出结果:
2 20

C++中的函数重载

函数重载介绍

在C语言中,不能出现名字相同的函数,因为C语言中的编译过程函数是通过函数名以及对应的地址来找到函数并调用的,如果名字相同,那么编译器将无法分辨是哪一个函数

在C++中,允许在同一作用域下使用相同的函数名结合参数不同、参数类型和不同参数类型的不同顺序构成函数重载

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//参数类型不同的重载
//整型参数
int add(int a, int b)
{
    return a + b;
}
//浮点型参数
double add(double a, double b)
{
    return a + b;
}

//参数个数不同的重载
//两个参数
int add(int a, int b)
{
    return a + b;
}
//三个参数
int add(int a, int b, int c)
{
    return a + b + c;
}

//不同类型的但数量相同情况下的不同参数顺序的重载
//注意变量名不需要更改顺序
//int类型在前,char类型在后
int getcharacter(int a, char b)
{
    return b;
}
//char类型在前,int类型在后
int getcharacter(char a, int b)
{
    return a;
}

Note

注意返回类型不可以作为判断函数是否重载的条件,函数重载只有三种特点:

  1. 参数个数不同
  2. 参数类型不同
  3. 相同个数相互类型不同情况下,不同的参数顺序

函数重载一般用于针对不同类型实现相同的功能,例如

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//函数重载
//针对整型的加法函数
int add(int a, int b)
{
    return a + b;
}

//针对浮点型的加法函数
double add(double a, double b)
{
    return a + b;
}

重载函数的选择

在没有进入类之前,对于全局域的函数重载来说,正常调用对应函数名,但是需要传递正确的实参,例如

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//整型加法
int add(int a, int b)
{
    return a + b;
}
//浮点型加法
double add(double a, double b)
{
    return a + b;
}
#include <iostream>
using namespace std;

int main()
{    
    //传哪种类型值就调用哪种类型的函数
    cout <<"整型加法:" << add(1, 2) << endl;
    cout <<"浮点型加法:"<< add(1.1, 2.2) << endl;
    return 0;
}
输出结果
整型加法3
浮点型加法3.3

如果使用命名空间,则需要满足需要重载的函数在同一个命名空间,并且使用作用域限定符调用函数,传递实参时依旧是满足传哪种类型的值就调用哪种类型的函数

注意,含有缺省参数的函数和无缺省参数的函数无法构成函数重载,例如

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//缺省参数与函数重载
int add(int a = 10, int b = 20)
{
    return a + b;
}

int add(int a, int b = 30)
{
    return a + b;
}
//将会报错为add函数重定义

Info

了解:

C++是通过函数名修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载

例如在Linux平台下用g++编译的函数重载后的函数名为_Z3addii(对应两个int类型参数的add函数),_Z3adddd(对应两个double类型参数的add函数),两个函数名中的3为函数本身(不包括类型)的名字长度,例如add函数名字是3个字符的长度

C++中的引用

引用的介绍

在C语言中,为了可以通过其他变量直接改变某一个变量的值由此引出了指针的概念,通过指针类型可以改变该指针指向的空间的内容

在C++中,使用引用来辅助指针的使用,例如:

Note

注意引用的底层依旧是指针实现

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//指针中的swap函数
void swap(int* num1, int* num2)
{
    int tmp = *num1;
    *num1 = *num2;
    *num2 = tmp;
}

//使用引用的函数
void swap(int& num1, int& num2)
{
    int tmp = num1;
    num1 = num2;
    num2 = tmp;
}

#include <iostream>
using namespace std;

int main()
{

    //C语言中的swap函数
    int a = 1;
    int b = 0;
    cout << "指针交换" << endl;
    cout << "交换前:" << endl;
    cout << a << " " << b << endl;
    swap(&a, &b);
    cout << "交换后:" << endl;
    cout << a << " " << b << endl;

    //C++中使用引用的swap函数
    a = 1;
    b = 0;
    cout << endl;
    cout << "引用交换" << endl;
    cout << "交换前:" << endl;
    cout << a << " " << b << endl;
    swap(a, b);
    cout << "交换后:" << endl;
    cout << a << " " << b << endl;

    return 0;
}
输出结果
指针交换
交换前
1 0
交换后
0 1

引用交换
交换前
1 0
交换后
0 1

在C++中,引用相当于为变量取一个别名(引用变量),这个别名和其对应的变量(引用实体)共用一个地址,所以可以通过引用直接改变对应的变量所在空间的内容

所以在上面的代码中,对于引用变量的函数void swap(int& num1, int& num2)num1num2为实参ab的别名,所以此时num1num2ab的地址相同,直接在swap函数改变num1num2的值即可改变实参ab中的值

引用的使用

在C++中,使用下面的语法创建引用变量

C++
1
引用实体的类型 &引用变量名 = 引用实体;

Note

此处的引用实体可以是任何类型,包括指针类型以及引用类型

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//引用与指针
#include <iostream>
using namespace std;

int main()
{
    int a = 0;
    //指针类型
    int* p = &a;
    cout << "指针修改前:" << endl;
    cout << a << endl;
    *p = 1;
    cout << "修改后:" << endl;
    cout << a << endl;

    //引用类型
    a = 0;
    int& rp = a;
    cout << "引用修改前:" << endl;
    cout << a << endl;
    rp = 1;
    cout << "修改后:" << endl;
    cout << a << endl;

    //引用实体为指针类型
    a = 0;
    int*& rpp = p;
    cout << "引用类型为指针修改前:" << endl;
    cout << a << endl;
    *rpp = 1;
    cout << "修改后:" << endl;
    cout << a << endl;

    //引用变量作为引用实体
    a = 0;
    int& r = rp;
    cout << "引用类型为引用变量修改前:" << endl;
    cout << a << endl;
    r = 1;
    cout << "修改后:" << endl;
    cout << a << endl;
    return 0;
}
输出结果
指针修改前
0
修改后
1
引用修改前
0
修改后
1
引用类型为指针修改前
0
修改后
1
引用类型为引用变量修改前
0
修改后
1

引用与指针的对比

在C++中,引用有以下的特点:

  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,就不可以更改为其他实体,哪怕显式让其指向其他实体,本质还是改变初始化时的实体
  4. 没有NULL引用
  5. 使用sizeof关键字计算某个引用的大小时,计算的是引用实体的类型大小,而不是一个固定值
  6. 引用变量自加1相当于对应实体内容加1
  7. 引用没有多级引用,即没有二级及以上引用
  8. 引用在访问指向的实体时不需要解引用,由编译器处理,除非实体是指针类型
  9. 引用在概念上没有实际地址

对比指针的特点:

  1. 指针在使用之前可以不初始化,但是在VS上面必须初始化
  2. 一个变量可以有多个指针
  3. 一个指针指向一个变量时,可以改变其指向,使其指向下一个变量
  4. NULL指针
  5. 使用sizeof关键字计算某个指针的大小时,计算的是指针变量的大小,是一个固定值,在32位机上值为4,在64位机上是8
  6. 指针变量自加1相当于跳过指向的类型占用的内存大小个字节
  7. 指针存在二级及以上的引用
  8. 指针不论指向的类型如何,都必须显式解引用
  9. 指针在概念上和实际上都有自己的实际地址
C++
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
//指针可以更改指向,引用不可以
#include <iostream>
using namespace std;

int main()
{
    int a = 0;
    //变量a给引用变量b
    int& b = a;
    cout << "初始a中的值:" << a << endl;
    //通过引用b改变a的值
    b = 20;
    cout << "通过引用改变a中的值:" << a << endl;

    //改变引用a实体的引用变量b使其指向c
    cout << "初始a中的值:" << a << endl;
    int c = 10;
    cout << "初始c中的值" << c << endl;
    b = c;
    cout << "现在a中的值:" << a << endl;
    //通过引用b改变c的值
    b = 30;
    cout << "当前c中的值" << c << endl;
    cout << "当前a中的值:" << a << endl;

    return 0;
}
输出结果
初始a中的值0
通过引用改变a中的值20
初始a中的值20
初始c中的值10
现在a中的值10
当前c中的值10
当前a中的值30
//从上面的结果中可以看到,尽管b引用显式得改变了指向,使其指向c,但是实际上只是将c中的值给了b引用,而因为引用b的指向不可以改变,故依旧改变的是a中的值

//sizeof
#include <iostream>
using namespace std;

int main()
{
    //一个变量可以有多个引用和指针
    int a = 0;
    //引用指向变量a
    int& r = a;
    //创建指针和指针的引用
    int* p = &a;
    int*& r2 = p;

    cout << "计算引用变量大小:(引用int类型)" << sizeof(r) << endl;
    cout << "计算引用变量大小:(引用int*类型)" << sizeof(r2) << endl;
    cout << "计算引用指针大小:(64位机,指向int类型)" << sizeof(p) << endl;

    return 0;
}
输出结果
计算引用变量大小(引用int类型)4
计算引用变量大小(引用int*类型)8
计算引用指针大小(64位机指向int类型)8

//引用变量和指针+1操作
#include <iostream>
using namespace std;

int main()
{
    int arr[4] = { 1,2,3,4 };
    int& a1= arr[0];
    int* a2 = &arr[0];
    //const int*& a4 = &arr[0];//无法初始化
    cout << "计算引用实体类型为int类型+1跳过的大小" << endl;
    cout << "开始位置:" << &arr[0] << " " << "对应数值:" << arr[0] << endl;
    cout << "结束位置对应数值:" << a1 + 1 << endl;
    cout << "计算指针+1跳过的大小" << endl;
    cout << "开始位置:" << &arr[0] << " " << "对应数值:" << arr[0] << endl;
    cout << "结束位置:" << a2 + 1 << endl;
    cout << "计算引用实体类型为int*类型+1跳过的大小" << endl;
    a2 = arr;
    int*& a3 = a2;
    // const int*& a4 = &arr[0];//注意不可以使用这种写法,因为当前编译器不知道到底右侧值是否可以修改
    cout << "开始位置:" << &arr[0] << " " << "对应数值:" << arr[0] << endl;
    cout << "结束位置:" << a3 + 1 << endl;

    return 0;
}
输出结果
计算引用实体类型为int类型+1跳过的大小
开始位置000000970C4FF7B8 对应数值1
结束位置对应数值2
计算指针+1跳过的大小
开始位置000000970C4FF7B8 对应数值1
结束位置000000970C4FF7BC
计算引用实体类型为int*类型+1跳过的大小
开始位置000000970C4FF7B8 对应数值1
结束位置000000970C4FF7BC

//引用本身的地址和指针本身的地址
#include <iostream>
using namespace std;

int main()
{
    //一个变量可以有多个引用和指针
    int a = 0;
    //两个引用指向同一个变量a
    int& r = a;
    //两个指针指向同一个变量a
    int* p = &a;
    int*& r2 = p;

    cout << "引用实体为int类型变量的地址:" << &r << endl;
    cout << "引用实体为int*类型变量的地址" << &r2 << endl;
    cout << "指针变量指向的int类型变量的地址" << p << endl;
    cout << "指针变量自身的地址" << &p << endl;
    return 0;
}
输出结果
引用实体为int类型变量的地址0000000C97EFF5A4
引用实体为int*类型变量的地址0000000C97EFF608
指针变量指向的int类型变量的地址0000000C97EFF5A4
指针变量自身的地址0000000C97EFF608

const的引用(常量引用)

在C语言中,可以使用const指针修饰指针变量,并且const可以修饰指针变量整体使得无法通过解引用该指针修改指向空间的内容,也可以放在变量名前方限制指针无法指向其他对象

在C++中,也存在const修饰的引用,但是const只能放在引用变量整体的前面,即const int& a对于引用的const来说都是底层const,例如

Note

在指针/引用中,指针和引用的权限只可以缩小不可以放大

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//常量引用
#include <iostream>
using namespace std;

int main()
{
    //对于常量来说,需要使用常量引用变量才能引用常量实体
    const int a = 0;
    const int& a1 = a;// a为const修饰的常量
    const int& a2 = 10;//10为常量值
    double d = 2.34;
    //int& a3 = d;//无法编译,但不是类型不同的原因,而是因为强制转换产生的临时变量具有常性
    const int& a3 = d;//可以编译
    //const int* p = d;//但是指针无法做到同样的实现,原因是类型不同
    return 0;
}

Question

在强制类型转换的过程中,实际上是创建了一个临时变量,例如

C++
1
2
double b = 2.34;
int a = (int)b;

在此处的强制转换过程中,a被赋值为强制转换为int类型的b,但是b本身的值并没有改变,b中存储的值依旧是2.34,但是a中存的值为2,所以强制转换过程不会直接对需要强制转换的对象b进行改变,而是取出该变量b中的数据放到临时变量中并转换为对应类型再给目标对象a,上面的过程可以大致写成:

C++
1
2
3
double b = 2.34;
const int temp = b; // 截断
int a = temp;

此时可以解释为什么int& a3 = d需要加const才可以编译通过

在上面的代码中ddouble类型的变量,而需要强制转换为int类型的变量给int类型的引用,但是在强制转换过程中,是先创建了临时变量存储d中的值,而临时变量中的值为常量值不可以被改变,所以需要给int类型的引用加const才可以成功编译,这个过程可以大致写成:

C++
1
2
3
double b = 2.34;
const int temp = b; // 截断
const int& a3 = temp;

引用在实际中的使用

函数返回值

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//返回值
int add(int a, int b)
{
    static int z = a + b;
    return z;
}

#include <iostream>
using namespace std;

int main()
{
    int ret = add(1, 2);
    cout << ret << endl;
    return 0;
}
输出结果
3

//返回引用
int& add(int a, int b)
{
    static int z = a + b;
    return z;// 离开add作用域不会销毁,所以可以使用引用返回
}

#include <iostream>
using namespace std;

int main()
{
    int ret = add(1, 2);
    cout << ret << endl;
    return 0;
}
输出结果
3

在上面的代码中,add函数中有一个static类型的变量,直接返回值时,需要将该变量的值存入寄存器中,再由寄存器带回给add函数被ret接收,而使用引用返回该类型的变量可以在一定程度上减少前面的步骤消耗

但是,注意下面的代码可能会出现问题

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

int& add(int a, int b)
{
    int z = a + b;
    return z;// 局部变量在出了add函数会销毁
}

int main()
{
    int& ret = add(1, 2);
    //int ret = add(1, 2);//依旧会有问题
    cout << ret << endl;
    cout << "输出结果:" << ret << endl;
    return 0;
}
输出结果
3
输出结果-858993460
//编译器警告:返回局部变量或临时变量的地址: z

因为zadd函数中的局部变量,出了add函数将会被销毁,此时返回z是返回变量z的引用,被ret接收后,ret指向z的位置,当z销毁后,ret依旧指向该位置,但是该位置的值已经不一定是刚才计算出来的结果,因为add函数栈帧空间可能会被覆盖,没被覆盖时该地址的值依旧是计算出来的值,否则就是随机值,具体取决于编译器如何分配空间

Note

在使用引用作为函数返回值时,注意引用的实体尽量不是临时变量或者局部变量,除非返回作为形式参数的引用变量

所以,函数返回时,出了函数作用域如果返回对象还在(没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回

返回值和返回引用时间消耗对比
C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
using namespace std;

#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
    // 以值作为函数的返回值类型
    size_t begin1 = clock();
    for (size_t i = 0; i < 100000; ++i)
        TestFunc1();
    size_t end1 = clock();
    // 以引用作为函数的返回值类型
    size_t begin2 = clock();
    for (size_t i = 0; i < 100000; ++i)
        TestFunc2();
    size_t end2 = clock();
    // 计算两个函数运算完成之后的时间
    cout << "TestFunc1 time:" << end1 - begin1 << endl;
    cout << "TestFunc2 time:" << end2 - begin2 << endl;
}

int main()
{
    TestReturnByRefOrValue();
    return 0;
}
输出结果
TestFunc1 time:161
TestFunc2 time:1

函数形式参数

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

void swap(int& num1, int& num2)
{
    int tmp = num1;
    num1 = num2;
    num2 = tmp;
}

int main()
{
    int a = 1;
    int b = 0;
    cout << "交换前的a和b:" << "a = " << a << " " << "b = " << b << endl;
    swap(a, b);
    cout << "交换后的a和b:" << "a = " << a << " " << "b = " << b << endl;
    return 0;
}
输出结果
交换前的a和ba = 1 b = 0
交换后的a和ba = 0 b = 1
形式参数为值与形式参数为引用时间消耗对比
C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
using namespace std;
#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
    A a;
    // 以值作为函数参数
    size_t begin1 = clock();
    for (size_t i = 0; i < 10000; ++i)
        TestFunc1(a);
    size_t end1 = clock();
    // 以引用作为函数参数
    size_t begin2 = clock();
    for (size_t i = 0; i < 10000; ++i)
        TestFunc2(a);
    size_t end2 = clock();
    // 分别计算两个函数运行结束后的时间
    cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
    cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}

int main()
{
    TestRefAndValue();
    return 0;
}
输出结果
TestFunc1(A)-time:15
TestFunc2(A&)-time:0

内联函数

在C语言中,常量和宏函数一般用#define来定义,优点是:增强代码的复用性以及提高性能,但是#define定义的二者在预处理时将会替换其在函数出现的位置,从而导致下面的三个重要的问题

  1. #define定义的宏函数和常量无法被调试
  2. 代码可读性差,可维护性差,并且容易误用
  3. #define定义的宏函数没有类型的检查

在C++中,为了避免上述问题,经常使用constenum来表示常量,而使用内联函数代替#define定义的宏函数

所谓内联函数,即当函数代码量较小时,在汇编代码中直接将函数代码替换掉原来的call+函数名指令,减少函数栈帧开辟时的开销,从而实现直接执行函数,但是缺点也就比较明显,如果代码量太大,将会导致替换后的函数指令很多,为此C++中的内联函数到底是真正的函数还是类似于C语言的宏函数取决于函数代码规模的大小,如果函数代码量大,那么尽管定义为内联函数,还是会被编译器认作普通函数

Note

需要注意的是,尽管声明为内联函数,具体是否会被当做内联函数取决于编译器,不一定代码量小的就是内联函数,因为inline对于编译器来说只是一个建议,而不是命令

在C++中,使用inline关键字修饰定义的函数,语法如下

C++
1
inline + 普通函数定义

代码实例

C++
1
2
3
4
5
6
7
8
//C语言中#define定义的ADD宏函数
#define ADD(x, y) ((x) + (y))

//C++中内联函数
inline int add(int a, int b)
{
    return a + b;
}

Note

不建议将内联函数的声明和定义分开写,二者分离会导致链接错误。因为inline会展开函数,导致函数失去本身的地址,声明的函数就无法被找到

C++
1
2
3
4
5
6
7
8
9
//函数声明
inline int add(int a, int b);

//函数定义
int add(int a, int b)
{
    return a + b;
}
//编译报错为链接错误

内联函数的特点

inline是一种以存储空间(不是内存空间)换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大(因为函数展开之后汇编代码量增多),优势:少了调用开销,提高程序运行效率

auto类型变量(C++11标准支持)

auto关键字介绍

随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:

  1. 类型难于拼写
  2. 含义不明确导致容易出错

例如下面的代码中

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <string>
#include <map>
int main()
{
    std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange", "橙子" }, {"pear","梨"} };
    std::map<std::string, std::string>::iterator it = m.begin();
    while (it != m.end())
    {
        //....
    }
    return 0;
}

可以用typedef来处理上面的问题

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <string>
#include <map>
typedef std::map<std::string, std::string> Map;
int main()
{
    Map m{ { "apple", "苹果" },{ "orange", "橙子" }, {"pear","梨"} };
    Map::iterator it = m.begin();
    while (it != m.end())
    {
        //....
    }
    return 0;
}

但是typedef也有别的问题,考虑下面的问题

C++
1
2
3
4
5
6
7
typedef char* pstring;
int main()
{
    const pstring p1;    // 编译失败
    const pstring* p2;   // 编译通过
    return 0;
}
在上面的代码中,p1中因为const修饰的p1本身,所以展开后为char* const p1,作为const修饰的变量为常量,只有一次赋值的机会,故需要初始化,而const pstring* p2展开后为const (char*)* p2,所以本质p2就是一个指向char*的二级指针,其本身并不是常量,所以编译不会报错

所以上面的代码可以理解为如下:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;
typedef char* pstring;

int main()
{
    char i = 0;
    char* p = &i; // char* const
    printf("%p\n", p);

    const pstring p1 = nullptr; // char* const
    const pstring* p2 = &p; // const (char*)* p2

    printf("%p\n", *(p2)); // p2指向的地址与i地址相同
    *(*p2)= 20;
    cout << (int)(*(*p2)) << endl;

    return 0;
}

auto关键字的使用

auto关键字基本使用

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量

在C++11标准中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得

使用auto关键字创建变量的语法如下:

C++
1
auto 变量名;

auto创建的变量有自动推导类型的特点,例如下面的代码中

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//auto关键字
#include <iostream>
using namespace std;

int main()
{
    int a = 0;
    auto b = a;//b自动推导为int类型
    double c = 0.0;
    auto d = c;//c自动推导为double类型

    cout << "a变量的类型为:" << typeid(a).name() << endl;
    cout << "b变量的类型为:" << typeid(b).name() << endl;
    cout << "c变量的类型为:" << typeid(c).name() << endl;
    cout << "d变量的类型为:" << typeid(d).name() << endl;

    return 0;
}
输出结果
a变量的类型为int
b变量的类型为int
c变量的类型为double
d变量的类型为double

在上面的代码中,创建了两个普通对象ac,使用auto关键字创建了两个对象bd,分别自动匹配了ac的类型为intdouble,使用typeid.name()查看变量类型

需要注意的是,使用auto创建的变量一定要初始化,否则编译器无法推导该变量的类型从而报错

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//auto关键字
#include <iostream>
using namespace std;

int main()
{
    auto e;// 报错

    return 0;
}
报错信息
e: 类型包含auto的符号必须具有初始值设定项

所以,auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型

使用auto关键字可以同时创建多个变量,但是必须保证一个auto关键字创建的多个变量匹配一种类型,不可以auto匹配多个类型

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <iostream>
using namespace std;

int main()
{
    auto a = 1, b = 2, c = 3, d = 4;//所有变量匹配为int类型
    auto f = 2.0, g = 2;//不可以同时匹配多个类型

    return 0;
}
报错信息
在声明符列表中,“auto必须始终推导为同一类型

auto关键字配合指针和引用

auto关键字创建的变量也可以自动推导为指针和引用类型,也可以直接将auto关键字创建的变量指定为指针和引用类型,用auto声明指针类型时,用autoauto*没有任何区别,但用auto声明引用类型时则必须加&,如下

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//auto关键字配合指针和引用
#include <iostream>
using namespace std;

int main()
{
    int a = 0;
    int* p = &a;
    auto p1 = p;//自动推导为int*类型
    auto* p2 = p;//指定为int*类型
    //auto* p3 = a;//不可以推导
    auto r1 = a;//自动推导为int类型
    auto& r2 = a;//指定为引用类型

    return 0;
}

编译器指示如下:

auto关键字不可以推导的场景

  1. auto关键字创建的变量不可以作为函数形参

    C++
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    void test(auto a)
    {
    
    }
    
    #include <iostream>
    using namespace std;
    
    int main()
    {
        test(1);
        return 0;
    }
    报错信息
    参数不能为包含auto的类型
    
  2. auto不能直接用于声明数组

    C++
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    #include <iostream>
    using namespace std;
    
    int main()
    {
        int arr[] = { 1,2,3,4,5 };
        auto arr1[] = { 1,2,3,4,5 };//不可以直接用来创建数组
    
        return 0;
    }
    报错信息
    auto类型不能出现在顶级数组类型中
    

但是可以获取到数组的地址

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

int main()
{
    int arr[] = { 1,2,3,4,5 };
    auto arr1 = arr;//可以将已经创建的数组给auto创建的变量,自动匹配为int*
    cout << "arr1:" << typeid(arr1).name() << endl;
    for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
    {
        cout << "arr1[i]:" << arr1[i] << endl;
    }

    return 0;
}
输出结果
arr1int * __ptr64
arr1[i]1
arr1[i]2
arr1[i]3
arr1[i]4
arr1[i]5

基于范围的for循环(C++11标准支持)

基于范围的for循环基础使用

在C++98中需要按照常规方式进行数组的遍历,例如

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>
using namespace std;

int main()
{
    int arr[] = { 1,2,3,4,5 };
    for (int i = 0; i < sizeof(arr)/sizeof(int); i++)
    {
        cout << arr[i] << " ";
    }

    return 0;
}
输出结果
1 2 3 4 5

但是在C++11中可以按照下面的方式进行遍历

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream>
using namespace std;

int main()
{
    int arr[] = { 1,2,3,4,5 };

    for (int num : arr)
    {
        cout << num << " ";
    }

    return 0;
}

在上面的for循环中,创建了一个num变量,将数组中的元素给num变量,再打印num变量

鉴于上面的过程说明,如果想用上面的方式修改数组中的元素则不会成功

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

int main()
{
    int arr[] = { 1,2,3,4,5 };

    for (int num : arr)
    {
        num *= 2;
        cout << num << " ";
    }
    cout << endl;
    for (int num : arr)
    {
        cout << num << " ";
    }

    return 0;
}
输出结果
2 4 6 8 10
1 2 3 4 5

此时可以配合引用进行原数组内容的修改

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

int main()
{
    int arr[] = { 1,2,3,4,5 };

    for (int& num : arr)
    {
        num *= 2;
        cout << num << " ";
    }
    cout << endl;
    for (int num : arr)
    {
        cout << num << " ";
    }

    return 0;
}
输出结果
2 4 6 8 10
2 4 6 8 10

Note

与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环

基于范围的for循环也可以使用auto创建循环变量

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

int main()
{
    int arr[] = { 1,2,3,4,5 };

    for (auto num : arr)
    {
        cout << num << " ";
    }
    cout << endl;
    for (auto& num : arr)
    {
        num *= 2;
        cout << num << " ";
    }

    return 0;
}
输出结果
1 2 3 4 5
2 4 6 8 10

基于范围的for循环使用条件

for循环迭代的范围必须是确定的

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;
void test(int* arr)
{
    for (auto num : arr)
    {
        cout << num << " ";//不可以遍历(数组名不再是整个数组,而是第一个元素的地址指针),因为无法确定范围,数组在传参时只会传递第一个元素的地址
    }
}

int main()
{
    int arr[] = { 1,2,3,4,5 };
    test(arr);
    return 0;
}

空指针值(C++11标准支持)

在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,基本都是按照如下方式对其进行初始化:

C++
1
2
3
4
5
6
7
void TestPtr()
{
 int* p1 = NULL;
 int* p2 = 0;

 // ……
}

NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

C++
1
2
3
4
5
6
7
#ifndef NULL
#ifdef __cplusplus
#define NULL    0
#else
#define NULL    ((void *)0)
#endif
#endif

在上面的宏定义中,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量,不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如在函数重载中可能会使编译器匹配参数为整型的函数,而不是参数为指针类型的函数:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

void test(int a)
{
    cout << "匹配int类型形参" << endl;
}

void test(int* a)
{
    cout << "匹配int*类型形参" << endl;
}

int main()
{
    test(0);
    test(NULL);
    return 0;
}
输出结果
匹配int类型形参
匹配int类型形参

此时为了使形参匹配到指针类型,必须将NULL或者0强制转换为(void*)类型

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void test(int a)
{
    cout << "匹配int类型形参" << endl;
}

void test(void* a)
{
    cout << "匹配int*类型形参" << endl;
}

int main()
{
    test(0);
    test((void*)NULL);
    return 0;
}

为了解决上面的问题,C++11标准中提出了nullptr用于指定指针空值

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

void test(int a)
{
    cout << "匹配int类型形参" << endl;
}

void test(int* a)
{
    cout << "匹配int*类型形参" << endl;
}

int main()
{
    test(0);
    //test((void*)NULL);
    test(nullptr);
    return 0;
}
输出结果
匹配int类型形参
匹配int*类型形参

在使用nullptr时注意下面的三点:

  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
  2. 在C++11中,sizeof(nullptr)sizeof((void*)0)所占的字节数相同。
  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr
C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream>
using namespace std;

int main()
{
    cout << "sizeof(NULL):" << sizeof(NULL) << endl;
    cout << "sizeof(nullptr):" << sizeof(nullptr) << endl;
    cout << "sizeof((void*)0):" << sizeof((void*)0) << endl;
    return 0;
}
输出结果
sizeof(NULL)4
sizeof(nullptr)8
sizeof((void*)0)8