前言:
在上一期我们进行了C++的初步认识,了解了一下基本的概念还学习了包括:命名空间,输入输出以及缺省参数等相关的知识。今天我们将进一步对C++入门知识进行学习,主要还需要大家掌握我们接下来要学习的——引用,内联函数和关键字等。好了,废话不多说,我们直接进入今天的学习吧!!!
本文目录
- 前言:
- 1.引用(重点掌握)
- 1.1引用概念
- 1.2 引用特性
- 1.3 常引用
- 1.4使用场景
- C++引用作为函数参数
- C++引用作为函数返回值
- 1.5 引用和指针的区别
- 2. 内联函数
- 2.1 基本概念
- 2.2特性
- 3.auto关键字(C++11)
- 3.1 类型别名思考
- 3.2关键字简介
- 3.3 auto的使用细则
- 3.3 auto不能推导的场景
- 注意事项
- 4. 基于范围的for循环(C++11)
- 4.1 范围for的语法
- 4.2 范围for的使用条件
- 5.指针空值nullptr(C++11)
- 总结
1.引用(重点掌握)
引用是 C++ 的新增内容,在实际开发中会经常使用;C++ 用的引用就如同C语言的指针一样重要,但它比指针更加方便和易用,有时候甚至是不可或缺的。
同指针一样,引用能够减少数据的拷贝,提高数据的传递效率。因此,我们不仅仅从语法层面讲解 C++ 引用,而是深入 C++ 引用的本质,让大家不但知其然,而且知其所以然。
那么为什么C++会引入引用这一概念呢?在正式学习之前,我们先搞明白它的诞生原因。
在我们之前的学习中我们已经知道,参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。
对于像 char、bool、int、float 等基本类型的数据,它们占用的内存往往只有几个字节,对它们进行内存拷贝非常快速。而数组、结构体、对象是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行频繁的内存拷贝可能会消耗很多时间,拖慢程序的执行效率。
C/C++ 禁止在函数调用时直接传递数组的内容,而是强制传递数组指针,而对于结构体和对象没有这种限制,调用函数时既可以传递指针,也可以直接传递内容* 但是在 C++ 中,我们有了一种比指针更加便捷的传递聚合类型数据的方式,那就是引用(Reference)。有了以上内容做铺垫,接下来,我们正式进入引用的学习!!!
在 C/C++ 中,我们将 char、int、float等由语言本身支持的类型称为基本类型,将数组、结构体、类(对象)等由基本类型组合而成的类型称为聚合类型
1.1引用概念
引用是 C++ 相对于C语言的又一个扩充。引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
引用类似于 Windows 中的快捷方式,一个可执行程序可以有多个快捷方式,通过这些快捷方式和可执行程序本身都能够运行程序;引用还类似于人的绰号(笔名),使用绰号(笔名)和本名都能表示一个人。
例如:
在四大名著水浒传中,108好汉几乎都有“绰号”。像我们熟知的李逵,在家称为"铁牛",江湖上人称"黑旋风"。
引用的定义方式类似于指针,只是用&取代了*,类型& 引用变量名(对象名) = 引用实体
语法格式为:
type &name = data;
注意:
引用必须在定义的同时初始化,并且以后也要从一而终,不能再引用其它数据,这有点类似于常量(const 变量)
老规矩,我们还是通过代码来进行观察
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
int main()
{
//cout << "hello world" << endl;
int i = 0;
int& k = i; //定义引用类型
int j = i;
cout << &i << endl;
cout << &j << endl;
cout << &k << endl;
return 0;
}
运行结果:
接下来,我为大家一一讲解上述代码:
本例中,变量 【k】就是变量 【i】 的引用,它们用来指代同一份数据;也可以说变量 【k】是变量 【i】的另一个名字。从输出结果可以看出,【k】和 【i】的地址一样,都是00AFFA80;或者说地址为00AFFA80的内存有两个名字,【k】 和 【i】,想要访问该内存上的数据时,使用哪个名字都行。
注意,引用在定义时需要添加&,在使用时不能添加&,使用时添加&表示取地址。如上面代码所示,&表示引用,&也可以表示取地址。除了这两种用法,&还可以表示位运算中的与运算。
当我们对i进行【i++】的操作,k也进行相应的操作时,会发生什么呢?我们通过调试给大家展示。
当我们进行【k++】操作时,【i】理所当然会进行自增操作(【k】就是【i】,可以理解两个都是同一块空间)。然而对【j++】的时候,我们不难发现,并未【i】造成自增操作。
接下来就是大家可能会关心的“套娃”问题,那么我们是否还可以对【i】进行取别名操作呢?答案当然是可以的。
那么我们在对【k】进行相应的操作是否还可以呢?答案也是可以的,在进行下去也是可以的。
由于引用 【i】和原始变量 【k】都是指向同一地址,所以通过引用也可以修改原始变量中所存储的数据,请看下面的例子:
int main()
{
int i = 99;
int& k = i;
k = 10;
cout << i << ", " << k << endl;
return 0;
}
运行结果:
最终程序输出两个 10,可见原始变量【i】的值已经被引用变量 【k】所修改。
如果读者不希望通过引用来修改原始的数据,那么可以在定义时添加 const 限制,形式为:
const type &name = value;
1.2 引用特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
int main
{
int a = 10;
// int& ra; // 该条语句编译时会出错
int& ra = a;
int& rra = a;
cout << &a << endl;
cout << &ra << endl;
cout << &rra << endl;
return 0;
}
报错如下:
1.3 常引用
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
}
1.4使用场景
C++引用作为函数参数
在定义或声明函数时,我们可以将函数的形参指定为引用的形式,这样在调用函数时就会将实参和形参绑定在一起,让它们都指代同一份数据。如此一来,如果在函数体中修改了形参的数据,那么实参的数据也会被修改,从而拥有“在函数内部影响函数外部数据”的效果。
我们还是通过一段代码来进行理解记忆:
//直接传递参数内容
void swap1(int a, int b)
{
int temp = a;
a = b;
b = temp;
}
//传递指针
void swap2(int* c, int* d)
{
int temp = *c;
*c= *d;
*d = temp;
}
//按引用传参
void swap3(int& a1, int& b1)
{
int temp = a1;
a1= b1;
b1= temp;
}
int main()
{
int num1, num2;
cout << "Input two integers: ";
cin >> num1 >> num2;
swap1(num1, num2);
cout << num1 << " " << num2 << endl;
cout << "Input two integers: ";
cin >> num1 >> num2;
swap2(&num1, &num2);
cout << num1 << " " << num2 << endl;
cout << "Input two integers: ";
cin >> num1 >> num2;
swap3(num1, num2);
cout << num1 << " " << num2 << endl;
return 0;
}
首先还是直接看我们的运行结果如下:
具体解析:
本例演示了三种交换变量的值的方法:
-
swap1() 直接传递参数的内容,不能达到交换两个数的值的目的。对于 swap1() 来说,a、b 是形参,是作用范围仅限于函数内部的局部变量,它们有自己独立的内存,和 num1、num2 指代的数据不一样。调用函数时分别将 num1、num2 的值传递给 a、b,此后 num1、num2 和 a、b 再无任何关系,在 swap1() 内部修改 a、b 的值不会影响函数外部的 num1、num2,更不会改变 num1、num2 的值。
-
swap2() 传递的是指针,能够达到交换两个数的值的目的。调用函数时,分别将 num1、num2 的指针传递给 c,d,此后c,d指向 a、b 所代表的数据,在函数内部可以通过指针间接地修改 a、b 的值。
-
swap3() 是按引用传递,能够达到交换两个数的值的目的。调用函数时,分别将 a1,b1绑定到 num1、num2 所指代的数据,此后 a1和 num1、b1和 num2 就都代表同一份数据了,通过 a1修改数据后会影响 num1,通过 b1修改数据后也会影响 num2。
从以上代码的编写中可以发现,按引用传参在使用形式上比指针更加直观。在以后的 C++编程中,使用引用会方便许多,它一般可以代替指针(当然指针在C++中也不可或缺)
C++引用作为函数返回值
引用除了可以作为函数形参,还可以作为函数返回值。
1.第一个优点就是减少拷贝,提高效率:
当我们使用的是引用返回时,我们定义的【n】在静态区,当离开Count栈帧时,我们的【n】不会被销毁。那么值是如何返回回来的呢?当程序执行【return】之前,系统会自动的创建一个临时变量(操作系统赋予的,我们无法得知),如果是传引用的返回的话,临时变量就相当于【n】然后就把相应的要释放的值放到临时变量中,通过临时变量把值带回来。而传值返回则不行,以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
2、调用者可以修改返回对象
我们以代码为例进行了解:
int& Add(int& r)
{
r += 10;
return r;
}
int main()
{
int num1 = 10;
int num2 = Add(num1);
cout << num1 << " " << num2 << endl;
return 0;
}
运行结果如下:
在将引用作为函数返回值时应该注意一个小问题,就是不能返回局部数据(例如局部变量、局部对象、局部数组等)的引用,因为当函数调用完成后局部数据就会被销毁,有可能在下次使用时数据就不存在了,C++编译器检测到该行为时也会给出警告。
更改上面的例子,让 Add() 返回一个局部数据的引用:
int& Add(int& a)
{
int n = a + 10;
return n; //返回局部数据的引用
}
int main()
{
int num1 = 20;
int num2 = Add(num1);
cout << num2 << endl;
int& num3 = Add(num1);
int& num4 = Add(num3);
cout << num3 << endl;
cout << num4 << endl;
return 0;
}
在 Visual Studio 下的运行结果:
而我们期望的运行结果是:
1)30
2)30 40
Add() 返回一个对局部变量 【n】 的引用,这是导致运行结果非常怪异的根源,因为函数是在栈上运行的,并且运行结束后会放弃对所有局部数据的管理权,后面的函数调用会覆盖前面函数的局部数据。代码中,第二次调用 Add() 会覆盖第一次调用 Add() 所产生的局部数据,第三次调用 Add() 会覆盖第二次调用 Add() 所产生的局部数据。
1.5 引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
例如下图代码打印地址就可以发现,两个占用的都是同一块地址空间。
int main()
{
int a = 1;
int& b = a;
cout << "&a = " << &a << endl;
cout << "&ra = " << &b << endl;
return 0;
}
输出结果如下:
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
int a = 10;
int& ra = a;
cout << ra << endl;
ra = 20;
int* pa = &a;
*pa = 20;
cout << a << endl;
cout << ra << endl;
cout << pa << endl;
return 0;
}
我们通过汇编来进行观察了解:
因此,我们得出二者的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
10. 引用比指针使用起来相对更安全
2. 内联函数
引入:
一般来说,调用一个函数流程为:当前调用命令的地址被保存下来,程序流跳转到所调用的函数并执行该函数,最后跳转回之前所保存的命令地址。对于需要经常调用的小函数来说,这大大降低了程序运行效率。所以,C99 新增了内联函数
2.1 基本概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率
如果在上述展示的代码中函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
查看方式:
由于debug版本下我们要对代码进行调试,所以代码中不会展开内联函数体,我们需要对工程进行属性设置,具体如下:
最后展示如下:
2.2特性
a:
inline 修饰符并非强制性的:编译器有可能会置之不理。例如,递归函数通常不会被编译成内联函数。编译器有权自行决定是否要将有 inline 修饰符的函数编译成内联函数。
b:
和其他函数不同的是,在每个用到内联函数的翻译单元中,都必须重复定义这个内联函数。编译器必须时刻准备好该函数定义,以便在调用它时及时插入内联代码。因此,经常在头文件中定义内联函数。
c:
inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
d:
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
//test.h
#pragma once
#include <iostream>
using namespace std;
inline void f(int i);
//test.cpp
#include "test.h"
void f(int i)
{
cout << i << endl;
}
//main.cpp
#include "test.h"
int main()
{
f(10);
return 0;
}
此时,当我们运行我们的代码时就会出现报错的情况。
接下来我们讨论在C语言中宏和C++中内联函数的一些联系
在以往的面试中,会出现以下题目:
C++有哪些技术替代宏?
- 常量定义 换用const enum
- 短小函数定义 换用内联函数
因此,咱们需要探究一下二者之间的具体关系有哪些!!!
首先咱们先回顾一下宏的优缺点有哪些:
优点:
1.增强代码的复用性。
2.提高性能。
缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。
3.没有类型安全的检查 。
接下来我们在来聊聊宏和内联函数:
a:
C++ 语言支持函数内联,其目的是为了提高函数的执行效率(速度).在 C程序中,可以用宏代码提高执行效率。宏代码本身不是函数,但使用起来像函数,而inline是函数。
b:
内联函数是在编译时展开,而宏在预编译时展开;在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。预处理器用复制宏代码的方式代替函数调用,省去了参数压栈、生成汇编语言的 【call】调用、返回参数、执行return等过程,从而提高了速度。
c:
宏在定义时要小心处理宏参数,一般用括号括起来,否则容易出现二义性。而内联函数不会出现二义性。例如:
#define MAX(a, b) (a) > (b) ? (a) : (b)
int main()
{
result = MAX(i, j) + 2 ;
return 0;
}
由于运算符【+】比运算符【:】的优先级高,因此,上述代码将会被预处理器解释为:
result = (i) > (j) ? (i) : (j) + 2 ;
如果把宏代码改写为
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
则可以解决由优先级引起的错误。但是即使使用修改后的宏代码也不是万无一失的,例如语句
result = MAX(i++, j);
将被预处理器解释为
result = (i++) > (j) ? (i++) : (j);
对于C++ 而言,使用宏代码还有另一种缺点:无法操作类的私有数据成员。
d:
inline有点类似于宏定义,但是它和宏定义不同的是,宏定义只是简单的文本替换,是在预编译阶段进行的。而inline的引入正是为了取消这种复杂的宏定义的。
那么内联函数是否就一定很好呢?答案当然是否定的。
如果所有的函数都是内联函数,那么它还能叫做“内联函数”吗?
a.内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率;
b.如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率 的收获会很少;
c.另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
以下情况不宜使用内联:
如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
因此,有时不要仅是为了提高编程效率而使用这两种函数,要综合考虑后再使用,因为有时使用这两种函数可能会出现一些别的错误!!!
3.auto关键字(C++11)
3.1 类型别名思考
早在C++98标准中就存在了auto关键字,那时的auto用于声明变量为自动变量,自动变量意为拥有自动的生命期,这是多余的,因为就算不使用auto声明,变量依旧拥有自动的生命期:
int a =10 ; //拥有自动生命期
auto int b = 20 ;//拥有自动生命期
static int c = 30 ;//延长了生命期
C++98中的auto多余且极少使用,C++11已经删除了这一用法,取而代之的是全新的auto:变量的自动类型推断。
随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:
- 类型难于拼写
- 含义不明确导致容易出错
我们举例例子来说明:
#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;
}
std::map<std::string, std::string>::iterator 是一个类型,当我们看到这么一大串时,恐怕“人都麻了”吧。因为该类型太长了,特别容易写错。聪明的同学可能已经想到:可以通过typedef给类型取别名,比如:
#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给类型取别名确实可以简化代码,但是typedef有会遇到新的难题,例如当我们遇到以下代码时:
typedef char* pstring;
int main()
{
const pstring p1; // 编译成功还是失败?
const pstring* p2; // 编译成功还是失败?
return 0;
}
当我们编译以上代码就会发现出错了,那么具体什么原因呢?我给大家分析分析:
p1:
const直接修饰的是指针变量【p1】,因此指针变量【p1】本身不能修改,但是它指向的内容可以修改,但是【p1】现在由const修饰,所以如果我们不初始化时赋值的话,程序出现报错的情况。而如果把相应的初始化操作移到之后在进行的话则是不行的。
p2:
【p2】大家是否会认为是被当做二级指针来由【const】,然而却不是这样的,【const】修饰的是二级指针【p2】所指向的内容,因此指针变量【p2】是没有被const修饰的,所以p2可以不初始化。
因此,在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并非那么容易,因此C++11给auto赋予了新的含义。
3.2关键字简介
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
举个例子:
int main()
{
int a = 10;
auto ra = a; //自动类型推断,ra为int类型
cout << typeid(ra).name() << endl;
return 0;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
}
typeid运算符可以输出变量的类型。程序的运行输出结果为:
注意:
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。auto的自动类型推断发生在编译期,所以使用auto并不会造成程序运行时效率的降低。
3.3 auto的使用细则
1. auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;
return 0;
}
输出结果为:
2.在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
int main
{
auto a = 1, b = 2;
auto c = 3, d = 4.0;
// 该行代码会编译失败,因为c和d的初始化表达式类型不同
return 0;
}
代码会报错如下:
3.3 auto不能推导的场景
- auto不能作为函数的参数
此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
- auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
- 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
- auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。
注意事项
auto 变量必须在定义时初始化,这类似于const关键字。
定义在一个auto序列的变量必须始终推导成同一类型。例如:
auto a1 = 10, a2 = 20, a3 = 30;//正确
auto b1 = 10, b2 = 20.0, b3 = 'a';//错误,没有推导为同一类型
初始化表达式为数组时,auto关键字推导类型为指针。
int main()
{
int a[3] = { 1, 2, 3 };
auto b = a;
cout << typeid(b).name() << endl;
return 0;
}
输出结果为:
若表达式为数组且auto带上&,则推导类型为数组类型。
int main()
{
int a[3] = { 1, 2, 3 };
auto& b = a;
cout << typeid(b).name() << endl;
return 0;
}
输出结果为:
函数或者模板参数不能被声明为auto
void func(auto a) //错误
{
//...
}
时刻要注意auto并不是一个真正的类型。
auto仅仅是一个占位符,它并不是一个真正的类型,不能使用一些以类型为操作数的操作符,如sizeof或者typeid。
4. 基于范围的for循环(C++11)
4.1 范围for的语法
在C++98中如果要遍历一个数组,可以按照以下方式进行:
int main()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
cout << *p << endl;
return 0;
}
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
4.2 范围for的使用条件
- for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
- 迭代的对象要实现++和==的操作。(关于迭代器这个问题,以后会讲,现在提一下,没办法讲清楚,现在大家了解一下就可以了)
5.指针空值nullptr(C++11)
实际开发中,避免产生“野指针”最有效的方法,就是在定义指针的同时完成初始化操作,即便该指针的指向尚未明确,也要将其初始化为空指针。我们在复习下“野指针”的概念。
所谓“野指针”,又称“悬挂指针”,指的是没有明确指向的指针。野指针往往指向的是那些不可用的内存区域,这就意味着像操作普通指针那样使用野指针。
C++98标准中,将一个指针初始化为空指针的方式有 2 种:
void TestPtr()
{
int* p1 = NULL;//推荐使用
int* p2 = 0;
}
可以看到,我们可以将指针明确指向 0(0x0000 0000)这个内存空间。
一方面,明确指针的指向可以避免其成为野指针;
另一方面,大多数操作系统都不允许用户对地址为 0 的内存空间执行写操作,若用户在程序中尝试修改其内容,则程序运行会直接报错。
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,例如:
void isbool(void* c)
{
cout << "void*c" << endl;
}
void isbool(int n)
{
cout << "int n" << endl;
}
int main()
{
isbool(0);
isbool(NULL);
return 0;
}
程序执行结果为:
对于 isbool(0) 来说,显然它真正调用的是参数为整形的 isbool() 函数;而对于 isbool(NULL),我们期望它实际调用的是参数为 void*c 的 isbool() 函数,但观察程序的执行结果不难看出,并不符合我们的预期。
C++ 98 标准中,如果我们想令 isbool(NULL) 实际调用的是 isbool(void* c),就需要对 NULL(或者 0)进行强制类型转换:
isbool( (void*)NULL );
isbool( (void*)0 );
将指针初始化为 nullptr,可以很好地解决 NULL 遗留的问题:
void isbool(void* c)
{
cout << "void*c" << endl;
}
void isbool(int n)
{
cout << "int n" << endl;
}
int main()
{
isbool(nullptr);
isbool(NULL);
return 0;
}
输出结果为:
借助执行结果不难看出,由于 【nullptr 】无法隐式转换为整形,而可以隐式匹配指针类型,因此执行结果和我们的预期相符。
注意:
- 在使用【nullptr 】表示指针空值时,不需要包含头文件,因为【nullptr 】是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用【nullptr 】。
总之在 C++11 标准下,相比 NULL 和 0,使用 【nullptr 】初始化空指针可以令我们编写的程序更加健壮。
最后提出一点【nullptr 】可以被隐式转换成任意的指针类型。举个例子:
int main()
{
int* a = nullptr;
char* b = nullptr;
double* c = nullptr;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
return 0;
}
输出结果为:
显然,不同类型的指针变量都可以使用 【nullptr 】来初始化,编译器分别将 【nullptr 】隐式转换成 int*、char* 以及 double* 指针类型。
总结
学到此,我们对C++的入门知识的学习便告一段落了。有了以上知识的铺垫,我们后面的学习才能开展下去。最后一定要认真总结哟!!!