• C++入门——引用|内联函数|auto关键字|基于范围的for循环|指针空值


    前言

    C++入门专栏是为了补充C的不足,并为后面学习类和对象打基础。在前面我们已经讲解了命名空间、输入输出、缺省参数、重载函数等,今天我们将完结C++的入门。

    下面开始我们的学习吧!

    一、引用

    1、引用是什么呢?为什么C++添加了引用?

    (1)引用的概念: 引用是给已存在变量取了一个别名,不是重新定义一个新变量。编译器不会为引用变量开辟内存空间,它和它引用的变量一起用同一块内存空间。(就如西游记中孙悟空,也叫齐天大圣,你不管叫他那个名字,都是代表一个人。)

    (2) 在C语言中,我们要改变一个变量的值可以取一个变量的地址通过解引用来改变,但是这是一种间接的玩法。总结:简单的说就是指针是间接的,而引用相当于它的别名还是它自己,更直接,更方便了。 所以以后我们大多都是使用引用了。下面让我们详细了解引用的魅力!

    2、引用的语法

    引用的语法:

    类型& 引用变量名(对象名) = 引用实体。

    tip:

    (1)引用的理解:

    ①C++觉得添加太多新符号不太好,所以就会在某些地方共用一些符号。例如这里的‘&’。

    ②‘&’这个符号用法的区分:‘&’在变量名之前,代表取地址;‘&’在类型之后代表引用(取别名)。

    ③引用在语法逻辑上不会开辟新空间,它和它引用的实体共用一块内存空间。

    ④引用就是取别名,不管怎样还是共用一块内存空间。

    ⑤代码示例:

    //引用的理解:
    #include
    
    using namespace std;
    
    int main()
    {
    	int a = 10;
    	//b是a的引用(别名)
    	int& b = a;
    
    	//打印a与b的地址
    	cout << &a << endl;
    	cout << &b << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    运行结果:
    在这里插入图片描述

    (2)引用的特性:

    ①引用在定义的时候必须初始化(就如你给谁取别名,肯定是有一个明确的指向,是给谁取的)。

    ②一个变量可以有多个别名(就像孙悟空就有多个别名)。

    ③引用一旦引用了一个实体,就不能再引用其他实体了(从这里就能看出C++的革命是不彻底的,引用并没有将指针完全替代)。

    ④代码示例:

    //引用的特性:
    #include
    
    using namespace std;
    
    int main()
    {
    	int a = 10;
    	//①必须初始化;
    	//int& b;//编译报错,引用必须初始化
    
    	//②一个变量可以有多个引用
    	int& b = a;//b是a的引用(别名)
    	int& c = b;//c是b的引用(别名的别名也是可以的)
    
    	//③引用一旦引用了一个实体,就不能在引用其他实体了
    	int x = 9;
    	//int& b = x;//编译报错,"b"重定义,多次初始化
    	b = x;//注意这里b不是x的引用,而是x赋值给b,b仍是a的别名。
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    (3)常引用——引用的权限

    ①引用过程中,权限可以平移或者缩小,但是不可以放大。

    ②算术转化:如果操作符的操作数类型不一致,会发生类型转化,只有类型一致,才能进行运算。

    ③类型转化:生成一个临时变量,临时变量具有常性,即临时变量不可改变,是常变量。图示:

    在这里插入图片描述

    ④为什么类型转化会生成临时变量——因为变量a类型不会改变,所以需要生成一个中间变量来进行类型转化。

    ⑤当引用的实体是一个常变量的时候,我们就要使用常引用,因为权限不可以放大。

    ⑥常引用的应用:常引用做参数——如果函数中只是使用参数,不改变参数的值,建议使用常引用。

    ⑦代码示例:

    //常引用——引用的权限
    #include
    
    using namespace std;
    
    int main()
    {
    	//1、引用过程中,权限可以平移或者缩小
    	int a = 10;
    	int& b = a;//权限平移,a/b可读可写
    	//如a能++,b也可以
    	b++;
    	a++;
    	const int& c = a;//权限的缩小,a可读可写c可读不可写
    	//a能++,c不可以
    	a++;
    	//c++;//编译报错,c不能改变
    
    	//2、引用过程中,权限不可以放大
    	double x = 9;
    	//int& y = x;//因为类型不一致,x生成const int的临时变量可读不可写,而y可读可写,引用权限不可以放大,所以报错。
    	const int& y = x;//权限平移
    
    	return 0;
    }
    
    • 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

    3、引用的应用

    (1)引用做参数

    引用做参数的意义:

    ①做输出型参数:形参的改变要影响实参。

    代码示例:交换两个整数

    #include
    
    using namespace std;
    //使用引用做输出参数交换两个整数
    void Swap(int& x, int& y)
    {
    	int temp = x;
    	x = y;
    	y = temp;
    }
    
    int main()
    {
    	int a = 3;
    	int b = 5;
    	//调用函数交换a b
    	Swap(a, b);
    	cout << "交换后:a=" << a << " b=" << b << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    tip:在C语言的时候,我们交换两个整数,要使形参的改变影响实参,我们用的指针来实现,现在C++里面我们可以使用引用实现。

    ②引用做参数,减少拷贝提高了效率。(特别是对于大对象和深拷贝类对象)

    代码示例:

    //2.调高效率,建议大对象/深拷贝对象使用引用做参数
    
    #include
    #include
    
    using namespace std;
    
    //定义一个大结构体
    struct A
    {
    	int a[10000];
    };
    
    //定义函数:以值作为函数参数
    void Func1(A a){}
    //定义函数:以引用作为函数参数
    void Func2(A& a){}
    
    //定义函数:分别计算两个函数运行结束后的时间
    void TestRefAndValue()
    {
    	//struct A a;//C中定义结构体类型变量的方式,Cpp也可以这样(兼容C)
    	A a;//Cpp中定义类对象的方式,因为在Cpp中struct升级为类了
    
    	//以值作为函数参数
    	size_t begin1 = clock();//开始时间
    	for (size_t i = 0; i < 10000; ++i)
    	{
    		Func1(a);
    	}//调用10000次Func1函数
    	size_t end1 = clock();//结束时间
    
    	//以引用作为函数参数
    	size_t begin2 = clock();
    	for (size_t i = 0; i < 10000; ++i)
    	{
    		Func2(a);
    	}
    	size_t end2 = clock();
    
    	//分别输出两个函数运行结束后的时间
    	cout << "Func1(A)_time:" << end1 - begin1 << endl;
    	cout << "Func1(A&)_time:" << end2 - begin2 << endl;
    }
    
    int main()
    {
    	//调用函数TestRefAndValue分别计算以值为参数和以引用为参数的运行时间
    	TestRefAndValue();
    	return 0;
    }
    
    • 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

    运行结果:

    在这里插入图片描述

    (2)引用做返回值

    ①引用做返回值的第一个意义:减少拷贝提高效率。

    代码示例:

    //①减少拷贝提高效率
    #include
    #include
    
    using namespace std;
    
    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;
    }
    
    • 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

    运行结果:

    在这里插入图片描述

    ②为什么会减少拷贝提高效率呢?

    答案是:函数的返回类型是传值返回,就会将返回值拷贝到一个临时变量中,最后再拷贝回主调函数。当函数的返回类型是引用返回时,就不会生成临时变量通过拷贝返回,而是直接返回返回值的别名。图示:

    在这里插入图片描述

    ③局部变量与静态变量传引用返回。

    代码示例1:局部变量传引用返回

    //局部变量传引用返回
    #include
    
    using namespace std;
    
    int& Count()
    {
    	int n = 0;//局部变量
    	n++;
    	//……
    	return n;
    }
    
    int main()
    {
    	//ret也是n的别名
    	int& ret = Count();
    	//调用完Count直接输出ret
    	cout << ret << endl;
    	printf("sss\n");
    	//调用完printf,再次输出ret
    	cout << ret << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    运行结果:

    在这里插入图片描述

    为什么ret的值不一样呢——因为局部变量随栈帧销毁而销毁。如果Count函数结束,栈帧不清理,那么ret的结果侥幸正确;如果Count函数结束,栈帧清理,那么ret的结果是随机值。图示:

    在这里插入图片描述

    代码示例2:静态变量传引用返回

    //静态变量传引用返回
    #include
    
    using namespace std;
    
    int& Count()
    {
    	static int n = 0;//静态变量
    	n++;
    	//……
    	return n;
    }
    
    int main()
    {
    	//ret也是n的别名
    	int& ret = Count();
    	//调用完Count直接输出ret
    	cout << ret << endl;
    	printf("sss\n");
    	//调用完printf,再次输出ret
    	cout << ret << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    运行结果:

    在这里插入图片描述

    为什么这里ret的值一样呢——静态变量存储在静态区,不随栈帧的销毁而销毁。

    tip:谨慎用引用做返回值,出了函数作用域,对象不在了,就不能用引用返回,还在就可以用引用返回。

    ④引用做返回值的第二个意义:获取返回值与修改返回值。

    代码示例:静态顺序表获取pos位置值与修改pos位置值

    //代码示例:静态顺序表获取pos位置值与修改pos位置值
    #include
    #include
    
    using namespace std;
    
    //定义静态顺序表类型
    struct SeqList
    {
    	int a[100];//顺序表大小
    	int size;//有效数据个数
    };
    
    //C实现——获取pos位置值
    int SLGet(SeqList* ps, int pos)
    {
    	//断言pos位置是否合理
    	assert(pos >= 0 && pos < 100);
    	//返回pos位置值
    	return ps->a[pos];
    }
    
    //C实现——修改pos位置值
    void SLModify(SeqList* ps, int pos, int x)
    {
    	//断言pos位置是否合理
    	assert(pos >= 0 && pos < 100);
    	//修改pos位置值
    	ps->a[pos] = x;
    }
    
    //C++使用引用做返回值实现——修改&获取pos位置值
    int& SLAt(SeqList& ps, int pos)
    {
    	//断言pos位置是否合理
    	assert(pos >= 0 && pos < 100);
    	//返回pos位置的别名
    	return ps.a[pos];
    }
    
    int main()
    {
    	SeqList s;
    	//调用C的实现,来获取与修改pos位置
    	SLModify(&s, 1, 2);
    	cout << SLGet(&s, 1) << endl;
    	//调用C++的实现,来获取与修改pos位置
    	SLAt(s, 0) = 1;//修改
    	cout << SLAt(s, 0) << endl;//获取
    	return 0;
    }
    
    • 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

    总结

    1、引用做参数:①做输出型参数;②减少拷贝提高效率;③基本任何场景都可以用引用做参数。

    2、引用做返回值:①减少拷贝提高效率;②可以读写返回值;③谨慎用引用做返回值,出了函数作用域,对象不在了,就不能用引用返回,还在就可以用引用返回。

    4、引用与指针的区别

    (1)语法层面: 引用不开空间,是对实体取别名;指针开空间,是存储实体地址。

    代码示例:

    //引用与指针的区别
    #include
    
    using namespace std;
    
    int main()
    {
    	int a = 11;
    
    	//引用在语法层面:不开空间,是对a的别名
    	int& ra = a;
    	ra = 13;
    
    	//指针在语法层面:开空间,存储a的地址
    	int* pa = &a;
    	*pa = 14;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    F10调试观察:

    在这里插入图片描述

    (2)底层层面: 从底层汇编指令实现的角度看,引用是类似指针的方式实现的。即在底层实现上引用实际是开空间的。

    汇编代码图示:

    在这里插入图片描述

    (3)引用与指针不同点总结:

    ①在语法层面:引用不开空间是一个实体的别名,指针开空间,存储实体的地址。

    ②引用在定义时必须初始化,指针没有要求。

    ③引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。(即引用不可以修改指向,指针可以修改指向。)

    ④没有NULL引用,但有NULL指针。

    ⑤在sizeof中含义不同:引用结果为引用类型的大小,但是指针始终是地址空间所占字节个数。

    ⑥引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。

    ⑦有多级指针,但没有多级引用。

    ⑧访问实体方式不同,指针需要显示解引用,引用编译器自己处理。

    ⑨引用比指针使用起来相对更安全。(如上面第四点。)

    二、内联函数

    1、回顾宏函数

    我们都知道调用函数需要建立栈帧是有消耗的,所以对于一些代码少且频繁调用的函数,在C语言我们使用了宏函数优化。

    (1)代码示例:两数相加的宏

    错误形式1:

    #define Add(x,y) x+y
    
    • 1

    解读:Add(10,20) * 20,宏预编译进行替换为10 + 20 * 20,我们发现因为操作符优先级的问题,不是先加后乘。

    错误形式2:

    #define Add(x,y) (x+y)
    
    • 1

    解读:Add(1 | 2 , 1 & 2),宏替换后为:(1 | 2 + 1 & 2),位操作符的优先级低于算术操作符,所以错误。

    正确形式:

    #define Add(x,y) ((x) + (y))
    
    • 1

    tip:宏参数的求值是在所有周围表达式的上下环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多写括号。(可以替换看一看。)

    (2)宏的优缺点:

    优点: 不需要建立栈帧,提高效率。

    缺点: 因为宏在预编译阶段进行了替换,所以①不方便调试;②代码可读性差,可维护性差,容易出错;③没有类型的检查等等。

    (3)宏函数有这么多缺点,我们C++祖师爷就看不下去了,所以就有了inline内联函数。

    2、内联函数

    (1)概念: 以inline修饰的函数叫做内联函数,编译时C++编译器会在调用函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。

    代码示例:

    //内联函数
    #include
    using namespace std;
    
    //inline修饰的函数
    inline int Add(int x, int y)
    {
    	return x + y;
    }
    
    int main()
    {
    	int ret = 0;
    	ret = Add(1, 2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    tip: ①内联函数弥补了宏的缺点,继承了宏的优点。内联函数可读性高,可调试,不复杂等等。②在默认的debug模式下,inline不会起作用,否则不方便调试。

    (2)内联这么好能不能都写成内联呢?

    答案是: 不可以,因为inline的特性不支持所有函数都写成内联。

    (3)内联函数的特性:

    ①inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺点:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。例如Func编译后是50行指令,如果Func不是inline,调用10000次Func合计指令为:10000+50(调用即call Func(地址),跳转到Func。);如果Func是inline,调用10000次Func合计指令为:10000*50(假设inline只是单纯展开,实际不是)。指令越多,目标文件越大。

    inline对于编译器而言只是一个建议,最终是否成为inline,由编译器决定。一般来说,内联机制用于优化规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、流程直接(不递归)、频繁调用的函数。例如递归函数加了inline也会被编译器否决。

    inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

    代码示例:

    //F.h
    #include
    using namespace std;
    
    inline void f(int i);
    //F.cpp
    #include"F.h"
    
    void f(int i)
    {
    	cout << i << endl;
    }
    //Test.cpp
    #include"F.h"
    
    int main()
    {
    	f(10);
    	return 0;
    }
    
    //链接错误:Test.obj:LNK2019:无法解析的外部符号 "void __cdecl f(int)" (? f@@YAXH@Z),函数 _main 中引用了该符号	
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    tip:不建议声明与定义分开,所以inline直接在.h文件中定义实现。

    三、auto关键字(C++11)

    1、auto简介

    C++11之前: 在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量(限定变量的作用域及生命周期),但是没有人去使用,因为局部变量默认是auto修饰的。如下:

    int a = 10;//自动存储类型
    auto int b = 10;//自动存储类型
    
    • 1
    • 2

    这样的话,auto没有用了。

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

    简单来说,在C++11中auto可以根据右边的表达式自动推导变量的类型

    代码示例:

    #include
    using namespace std;
    
    int main()
    {
    	auto a = 13;
    	auto b = 0.14;
    
    	//打印类型
    	cout << typeid(a).name() << endl;
    	cout << typeid(b).name() << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    运行结果:

    在这里插入图片描述

    注意:使用auto定义变量时必须对其进行初始化,在编译阶段需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。

    为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法。

    2、auto的应用场景

    随着程序越来越复杂,程序中用到的类型也越来越复杂。类型复杂,我们不仅容易写错还难于拼写。所以这个时候使用auto来帮我们自动推导变量的类型,就非常方便了。

    代码示例:

    #include
    #include
    
    int main()
    {
    	std::map<std::string, std::string> dict;
    
    	//std::map::iterator it = dict.begin();
    	//等价于
    	auto it = dict.begin();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    tip:auto在实际中最常见的优势用法就是跟C++11提供的新式for循环,还有lambda表达式等进行配合使用。

    3、auto的使用细则

    (1)auto与指针和引用结合使用

    代码示例:

    #include
    using namespace std;
    
    int main()
    {
    	int x = 10;
    	auto a = &x;
    	auto* b = &x;//auto*指定必须是指针类型
    	auto& c = x;
    
    	cout << typeid(a).name() << endl;
    	cout << typeid(b).name() << endl;
    	cout << typeid(c).name() << endl;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    运行结果:

    在这里插入图片描述

    tip:使用auto声明指针时,用auto和auto没有任何区别(auto指定必须是指针),但用auto声明引用类型时必须加&。

    (2)auto在同一行定义多个变量

    当在同一行声明多个变量时,这些变量必须是相同类型,否则编译器将报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

    代码示例:

    int main()
    {
    	auto a = 1, b = 2;
    	auto c = 3, d = 13.14;//编译失败,因为c和d的初始化表达式类型不同
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    4、auto不能推导的场景

    (1)auto不能作为函数的参数

    void TestAuto(auto a){}
    
    • 1

    此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导。

    (2)auto不能直接用来声明数组

    void TestAuto()
    {
    	auto a[] = {1, 2};
    }
    
    • 1
    • 2
    • 3
    • 4

    编译失败,auto不能声明数组,因为auto类型不能出现在顶级数组类型中。

    四、基于范围的for循环(C++11)

    1、范围for的语法

    在以前(C++98),如果要遍历一个数组,我们是按照下面的方式实现:

    #include
    using namespace std;
    
    int main()
    {
    	int arr[] = { 1, 2, 3, 4, 5 };
    	//通过下标访问
    	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
    	{
    		arr[i] *= 2;
    	}
    	//通过指针访问
    	for (int* p = arr; p < arr + sizeof(arr) / sizeof(arr[0]); p++)
    	{
    		cout << *p << endl;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还容易犯错。因此C++11中引入了基于范围的for循环

    范围for的语法格式如下:

    for(auto e : array)
    
    • 1

    for循环后的括号由冒号“:”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

    现在我们使用范围for来遍历数组,修改数组:

    #include
    using namespace std;
    
    int main()
    {
    	int arr[] = { 1, 2, 3, 4, 5 };
    	//范围for与引用结合:修改数组
    	for (auto& e : arr)
    	{
    		e *= 2;
    	}
    	//范围for:打印数组
    	for (auto e : arr)
    	{
    		cout << e << endl;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    总结:

    ①使用范围for遍历数组与以前相比,用起来非常方便,不易出错。所以范围for是一个语法糖。

    ②适用于数组,依次取数组中的数据赋值给变量e,自动迭代,自动判断结束。

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

    2、范围for的使用条件

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

    对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

    错误演示:以下代码中for的范围是不确定的

    void TestFor(int arr[])
    {
    	for (auto e : arr)
    	{
    		cout << e << endl;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    解读:数组传参,实际只能接收数组的首元素地址,这里我们并不知道数组的范围,所以报错。

    (2)迭代的对象要实现++和==的操作。(后期讲解,大家先知道即可)

    五、指针空值nullptr(C++11)

    1、C++98中的指针空值

    在良好的C/C++编程习惯中,声明一个变量时最好给变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。

    如果一个指针没有合法的指向,我们就需要将其置为空。

    代码演示:

    int main()
    {
    	int* p1 = NULL;
    	int* p2 = 0;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    为什么0也可以将指针置为空呢?

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

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

    可以看到,NULL可能别被定义为字面常量0,或者被定义为无类型指针(void)的常量。

    不论采用哪种定义,在使用空值的指针时,都不可避免的遇到一些麻烦,如下:

    #include
    using namespace std;
    
    //参数类型是整形
    void f(int)
    {
    	cout << "f(int)" << endl;
    }
    //参数类型是整形指针
    void f(int*)
    {
    	cout << "f(int*)" << endl;
    }
    
    int main()
    {
    	//调用f函数,观察
    	f(0);
    	f(NULL);
    	f((int*)NULL);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    运行结果:

    在这里插入图片描述

    解读:

    ①程序的本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序初衷相孛。

    ②在C++98中字面常量0即可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对齐进行强转(void*)0。

    ③形参因为我们只是观察参数匹配规则,所以可以只写形参类型。

    NULL这样太尴尬,所以C++11引入了nullptr

    2、nullptr

    先看一段代码示例:

    #include
    using namespace std;
    
    //参数类型是整形
    void f(int)
    {
    	cout << "f(int)" << endl;
    }
    //参数类型是整形指针
    void f(int*)
    {
    	cout << "f(int*)" << endl;
    }
    
    int main()
    {
    	//调用f函数,观察
    	f(NULL);
    	f(nullptr);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    运行结果:

    在这里插入图片描述

    总结:

    ①为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。

    ②在使用nullptr表示空值时,不需要包含头文件,因为nullptr是C++11作为关键字引入的。

    ③在C++11中,sizeof(nullptr)与sizeof((void*)0)所占字节数相同。

  • 相关阅读:
    DelphiWebMVC对VUE导出包的支持
    【5G MAC】RA-RNTI的计算过程
    电输运性质测试系统
    自学Python 66 Tkinter开发基础
    Visio文件编辑查看工具Visio Viewer for Mac
    数据分析面试经验
    【Vue瀑布流布局】vue简单实现瀑布流布局,循环数据自动排列到最短的列上【详细注释,直接使用】
    七夕,用HTML+CSS动画实现旋转图片表白魔方感动她
    MySQL中对于事务的理解
    从0开始学习pyspark--pyspark中的Spark DataFrame, Spark SQL, Pandas on Spark[第3节]
  • 原文地址:https://blog.csdn.net/wangjiushun/article/details/133757002