【C++初阶】C++入门(二):引用内联函数auto关键字范围for循环(C++11)指针空值nullptr

news2024/11/17 17:26:32

在这里插入图片描述

​📝个人主页:@Sherry的成长之路
🏠学习社区:Sherry的成长之路(个人社区)
📖专栏链接:C++初阶
🎯长路漫漫浩浩,万事皆有期待

上一篇博客:【C++初阶】C++入门(一):命名空间&&C++的输入输出&&缺省参数&&函数重载

文章目录

  • 1.引用
    • 1.1引用的概念
    • 1.2引用的特性
      • 1.2.1引用在定义时必须初始化
      • 1.2.2一个变量可以有多个引用
      • 1.2.3引用一旦引用了一个实体,就不能再引用其他实体
    • 1.3引用的使用场景
      • 1.3.1引用做参数(输出型参数)
      • 1.3.2解决二级指针难懂的问题 :
      • 1.3.3引用做返回值
    • 1.4常引用
    • 1.5引用和指针的区别
  • 2.内联函数
    • 2.1内联函数的概念
    • 2.2 特性
  • 3.auto关键字(C++11)
    • 3.1auto简介
    • 3.2 auto的使用细则
      • 3.2.1 auto与指针和引用结合起来使用
      • 3.2.2在同一行定义多个变量
    • 3.3 auto不能推导的场景
      • 3.3.1auto不能作为函数的参数
      • 3.3.2auto不能直接用来声明数组
  • 4.基于范围的for循环(C++11)
    • 4.1范围for的使用条件
      • 4.1.1for循环迭代的范围必须是确定的
      • 4.1.2迭代的对象要实现++和==操作
  • 5.指针空值nullptr
    • 5.1 C++98中的指针空值
    • 5.2 C++11中的指针空值
  • 6.总结:

1.引用

1.1引用的概念

引用不是定义一个变量,而是已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

其使用的基本形式为:类型& 引用变量名(对象名) = 引用实体。

#include <iostream>
using namespace std;
int main()
{
	int a = 10;
	int& b = a;//给变量a去了一个别名,叫 b
	cout << "a = " << a << endl;//a打印结果为10
	cout << "b = " << b << endl;//b打印结果也是10
	b = 20;//改变b也就是改变了a
	cout << "a = " << a << endl;//a打印结果为20
	cout << "b = " << b << endl;//b打印结果也是为20
	return 0;
}

:引用类型必须和引用实体是同种类型。

1.2引用的特性

1.2.1引用在定义时必须初始化

正确示例:

int a = 10;
int& b = a;//引用在定义时必须初始化

错误示例:

int c = 10;
int &d;//定义时未初始化
d = c;

1.2.2一个变量可以有多个引用

例如:

int a = 10;
int& b = a;
int& c = a;
int& d = a;

此时,b、c、d都是变量a的引用。

1.2.3引用一旦引用了一个实体,就不能再引用其他实体

例如:

int a = 10;
int& b = a;

此时,b已经是a的引用了,b不能再引用其他实体。如果写下以下代码,想让b引用另一个变量c:

int a = 10;
int& b = a;
int c = 20;
b = c;//错误想法:让b转而引用c

但该代码的意思是:将b引用的实体赋值为c,也就是将变量a的内容改成了20。

1.3引用的使用场景

1.3.1引用做参数(输出型参数)

形参的改变影响实参的参数叫做输出型参数,对于输出型参数,使用引用十分方便。
C语言中的交换函数,学习C语言的时候经常用交换函数来说明传值和传址的区别。现在我们学习了引用,可以不用指针作为形参了:

//交换函数
void Swap(int& a, int& b)
{
	int tmp = a;
	a = b;
	b = tmp;
}

因为在这里a和b是传入实参的引用,我们将a和b的值交换,就相当于将传入的两个实参交换了。

1.3.2解决二级指针难懂的问题 :

在单链表的C语言实现的这篇博客里,由于是没有头结点的链表,所以修改时,需要二级指针,刚开始学习的时候可能比较难理解。但是学了引用,就可以解决这个问题:

结构定义:

typedef struct SListNode
{
	int data;
	struct SListNode* next;
}SLTNode;

原代码:

void SListPushFront(SLTNode** pphead, SLTDateType x)
{
	SLTNode* newnode = BuyListNode(x);
	newnode->next = *pphead; 
	*pphead = newnode;
}

// 调用
SLTNode* pilst = NULL;
SListPushFront(&plist);

修改后的二级指针被替换成了引用:

void SListPushFront(SLTNode*& pphead, SLTDateType x) // 修改
{
	SLTNode* newnode = BuyListNode(x);
	newnode->next = *pphead; 
	*pphead = newnode;
}

// 调用
SLTNode* pilst = NULL;
SListPushFront(plist); // 修改

这里的意思是给一级指针取了一个别名,传过来的是plist,而plist 是一个一级指针,所以会出现 * 。相当于 pphead 是 plist 的别名,这里修改 pphead ,也就可以对 plist 完成修改。

也可以这么写 :

typedef struct SListNode
{
	int data;
	struct SListNode* next;
}SLTNode, *PSLTNode;

意思就是将 struct SListNode* 类型重命名为 PSLTNode

void SListPushFront(PSLTNode& pphead, SLTDateType x) // 改
{
	PSLTNode newnode = BuyListNode(x);
	newnode->next = pphead; 
	pphead = newnode;
}

// 调用 
PSLTNode plist = NULL;
SListPushFront(plist);

在 typedef 之后,PSLTNode 就是结构体指针,所以传参过去,只需要在形参那边用引用接收,随后进行操作,就可以达成目的。

总结:引用做参数优点 1.作输出型参数 2.提高效率(大对象/深拷贝对象–之后学习)

1.3.3引用做返回值

引用也可以做返回值,但要注意一些问题。

int Count()
{
	int n = 0;
	n++;
	
	return n;
}

int main()
{
	int ret = Count();
	cout << ret << endl;
	return 0;
}

这里看似很简单,就是把Count函数计算结束的结果返回,但是这里包含了 传值返回

若从栈帧角度看,会先创建 main 函数的栈帧,里面就会有 call指令,开始调用Count 函数。 Count 函数也会形成栈帧,而栈帧中也有空间,用来接受参数,里面的 n 则用来计算结果并返回。

对于传值返回,返回的并不是 n ,而是返回的是 n 的拷贝。而这其中会有一个临时变量,返回的是临时变量
在这里插入图片描述
反向证明:如果返回的是 n 的话,由于Count 的函数栈帧已经销毁了,这里打印的ret的值是不确定的。因为空间已经归还给操作系统了,这时都是非法访问,所以必定是n拷贝后的数据被返回。

1.如果Count 函数结束,栈帧销毁,没有清理栈帧,那么ret的结果侥幸正确
2.如果Count 函数结束,栈帧销毁,清理栈帧,那么ret的结果是随机值

但是临时变量在哪?
如果 n 比较小(4/8 byte),一般是寄存器充当临时变量,例如eax
如果 n 比较大,临时变量放在调用 add 函数的栈帧中

最后将临时变量中的值赋值给ret

不论这个函数结束,返回的那个值会不会被销毁,都会创建临时变量返回,例如 :

int Count()
{
	static int n = 0;
    n++;
    return n;
}

int main()
{
	int ret = Count();
	cout << ret << endl;

	return 0;
}

对于该函数,编译器仍然是创建临时变量返回;因为编译器不会对其进行特殊处理,仍然是放到 eax 寄存器中返回的。

但这个临时变量创建的有点多余,明明这块空间一直存在,却依然创建临时变量返回

那如果改成引用返回会修改这个缺陷吗?

int& Count()
{
	int n = 0;
    n++;
    return n;
}

int main()
{
	int& ret = Count();
	cout << ret << endl;

	return 0;
}

引用返回就是不生成临时变量,直接返回 n 的引用。而这里产生的问题就是 非法访问

造成的问题:
1.存在非法访问,因为Count 的返回值是 n 的引用, Count 栈帧销毁后,访问变量 n 的空间,此时n的空间已经还给操作系统了,由于这是读操作,编译器不一定检查出来,但是本质是错的,类似野指针访问。
2.如果 Count 函数栈帧销毁,空间被清理,那么取 n 值时取到的就可能是随机值,取决于编译器的决策。
eg:调用Count函数后再调用其他函数后会再次建立栈帧,后面的栈帧会覆盖前面的栈帧,恰好出现随机值

引用返回的原则:如果函数返回时,出了函数作用域,返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。区别就是传值返回生成拷贝,引用返回不生成拷贝

比如 static 修饰的静态变量就没有缺陷

int& c()
{
	static int n = 0;
    n++;
    return n;
}
int main()
{
	int& ret = Count();
	cout << ret << endl;

	return 0;
}

因为 static 修饰的变量在静态区,出了作用域也存在,这时就可以引用返回。

我们可以理解引用返回也有一个返回值,但是这个返回值的类型是 int& ,中间并不产生拷贝,因为返回的是别名。这就相当于返回的就是它本身。

引用返回还可以方便查找和修改->读写功能同在:

#include <cassert>
#define N 10

typedef struct Array
{
	int a[N];
	int size;
}AY;

int& PostAt(AY& ay, int i)
{
	assert(i < N);

	return ay.a[i];
}

int main()
{
	AY ay;
	PostAt(ay, 1);	
    // 修改返回值
	for (int i = 0; i < N; i++)
	{
		PostAt(ay, i) = i * 3;
	}
	
	for (int i = 0; i < N; i++)
	{
		cout << PostAt(ay, i) << ' ';
	}

	return 0;
}

由于PostAt 的形参 ay 为 main 中 局部变量 ay的别名,所以 ay 一直存在;这时可以使用引用返回。

引用返回 减少了值拷贝 ,不用将其拷贝到临时变量中返回;并且由于是引用返回,所以也可以 修改返回对象 。

总结:如果出了作用域,返回变量(静态static,全局变量,上一层栈帧,动态开辟malloc等不会随着函数调用的结束而被销毁的数据)仍然存在,则可以使用引用返回,不能是函数内部创建的普通局部变量。

引用做返回值优点 1.修改+获取返回值 2.减少拷贝,提高效率(大对象/深拷贝对象–之后学习)

1.4常引用

上面提到,引用类型必须和引用实体是同种类型的。但是仅仅是同种类型,还不能保证能够引用成功,我们若用一个普通引用类型去引用其对应的类型,但该类型被const所修饰,那么引用将不会成功。

int main()
{
	const int a = 10;
	//int& ra = a;    //该语句编译时会出错,a为常量
	const int& ra = a;//正确
	
	//int& b = 10;    //该语句编译时会出错,10为常量
	const int& b = 10;//正确
	return 0;
}

我们可以将被const修饰了的类型理解为安全的类型,因为其不能被修改。我们若将一个安全的类型交给一个不安全的类型(可被修改),那么将不会成功。

10d44a64d5bb45f20606d9b47d9.png)

const 修饰的 a 不能修改,b 为 a 的引用。a 是只读,但是引用 b 具有 可读可写 的权利,该情况为 权限放大 ,所以错误了。下面没有错误是因为是一个拷贝,d的改变不影响c

这时,只要加 const 修饰 b ,让 b 的权限也只有只读,使得 权限不变 ,就没问题了:

在这里插入图片描述

权限可以缩小,此时++x可以,因为x本身有可以修改的权限且y、z的值同时也会变,因为本来就是同一个空间,x的改变就是y、z的改变。只是作为z时,由于权限限制,++z不行
在这里插入图片描述

对于函数的返回值来说,也不能权限放大,例如:

int func1()
{
	static int x = 0;
    return x;
}

int main()
{
    int& ret = func1(); // error  
    return 0;
}

在这里插入图片描述
这样也是不行的,因为返回方式为 传值返回 ,返回的是临时变量,具有 常性 ,是不可改的;而引用放大了权限,所以是错误的

这时加 const 修饰,权限平移,就没问题了:const int& ret = func1()
在这里插入图片描述

同理,这里错误的原因:发生类型转换(提升、截断)的时候会产生一个临时变量
在这里插入图片描述对于类型转换来说,在转换的过程中会产生一个临时变量,例如 int ii =dd,把dd转换后的值放到临时变量中,把临时变量给接收的值ii,而临时变量具有常性,不可修改,引用就加了写权限,就错了,因为 权限被放大了 。

而下图由于返回的是x的别名,不是x,不会产生临时变量了,再传给int& ret,为权限平移在这里插入图片描述总结:对于引用,引用后的变量所具权限可以缩小或不变,但是不能放大(指针也适用这个说法)。const type& 可以接收各种类型的对象(变量、常量、隐式转换)。对于输出型参数可以用引用,反之用 const type& 更加安全。

1.5引用和指针的区别

在语法概念上,引用就是一个别名,没有独立的空间,其和引用实体共用同一块空间。

int main()
{
	int a = 10;
	//在语法上,这里给a这块空间取了一个别名,没有新开空间
	int& ra = a;
	ra = 20;

	//在语法上,这里定义了一个pa指针,开辟了4个字节(32位平台)的空间,用于存储a的地址
	int* pa = &a;
	*pa = 20;
	return 0;
}

但是在底层实现上,引用实际是有空间的:从汇编角度来看,引用的底层实现也是类似指针存地址的方式来处理的。

引用和指针的区别面试常考点,强烈建议理解
1、引用在定义时必须初始化,指针没有要求。
2、引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
3、没有NULL引用,但有NULL指针。
4、在sizeof中的含义不同:引用的结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
5、引用进行自增操作就相当于实体增加1,指针进行自增操作是指针向后偏移一个类型的大小。
6、有多级指针,但是没有多级引用。
7、访问实体的方式不同,指针需要显示解引用,而引用是编译器自己处理。
8、引用比指针使用起来相对更安全。

2.内联函数

调用函数需要建立栈帧,栈帧中要保存寄存器,结束后就要恢复,这其中都是有 消耗 的:

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

int main()
{
	add(1, 2);
	add(1, 2);
	add(1, 2);
	add(1, 2);
	add(1, 2);

	return 0;
}

而针对 频繁调用 的 小函数,可以用 宏 优化,因为宏是在预处理阶段完成替换的,并没有执行时的开销,并且因为代码量小,也不会造成代码堆积。

例如,代码就可以写成这样:

#define ADD(x, y) ((x) + (y)) 

int main()
{
	cout << ADD(1, 2) << endl;

	return 0;
}

在这里插入图片描述

但通过上图可以看出写宏时很容易出错(下次再错就挨打吧[bushi]),要么是替换出错,要么是优先级出错,所以宏并不友好。

为了减少函数调用开销,还可以在一定程度上替代宏,避免宏的出错, C++ 设计出了内联函数,关键字为 inline :

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

int main()
{
	int ret = add(1, 2);
	cout << ret << endl;

	return 0;
}

2.1内联函数的概念

在 release 版本下,inline 内联函数会直接在函数调用部分展开;对于 debug 则需要 主动设置 (debug 下编译器默认不对代码做优化);但是 release 版本下其他版本优化的太多,可能就不太好观察,所以我们设置一下编译器,在 debug 下看:

打开解决方案资源管理器,右击项目名称,选中属性并打开,在 C/C++ 区域常规部分,在调试信息一栏设置格式为程序数据库:
在这里插入图片描述

在 C/C++ 优化一栏,将内联函数扩展部分选中只适用于 _inline :
在这里插入图片描述

设置完毕后,点击应用。

在设置前、后,分别启动调试,查看反汇编代码:

修改前:
在这里插入图片描述

修改后:

在这里插入图片描述

两段反汇编代码最大的区别就是 call 消失了 ,call 就是函数调用的指令,它的消失就说明第二段代码没有进行调用。内联函数直接在局部展开了,在 main 函数中完成了操作。有了内联,我们就不需要去用 c语言 的宏了,因为宏很容易出错。

2.2 特性

inline是一种以 空间换时间 的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。
缺陷:可能会使目标文件变大;优势:少了调用开销,提高程序运行效率。

inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。

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

注意
​ 1)空间换时间是因为反复调用内联函数,导致编译出来的可执行程序变大

inline void func()
{
    // 编译完成为 10 条指令
    cout<<"111111"<<endl;
    cout<<"111111"<<endl;
    cout<<"111111"<<endl;
    cout<<"111111"<<endl;
    cout<<"111111"<<endl;
    cout<<"111111"<<endl;
    cout<<"111111"<<endl;
    cout<<"111111"<<endl;
    cout<<"111111"<<endl;
    cout<<"111111"<<endl;
}

若不用内联函数,不展开,若10000次调用 func,每次调用的地方为 call 指令的形式,总计 10010 行指令。若用内联函数,则展开,若一千次调用,每次调用的地方为都会展开为 10 条指令,总计 10 * 10000 行指令。

展开会让编译后的程序变大,如果递归函数作内联,后果可想而知。所以长函数和递归函数不适合展开。

​ 2)编译器可以忽略内联请求,内联函数被忽略的界限没有被规定,一般10行以上就被认为是长函数,当然不同的编译器不同

因此编译器会决策是否使用内联函数,如果函数太大会造成代码膨胀。

​ 3)内联函数声明和定义不可分离

// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
	cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
	f(10);
	return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl
f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用

由于内联函数在调用的地方展开,所以内联函数无地址(这里的地址指的是call 指令调用函数的地址,通过这个地址会跳到 jmp 指令处,再根据 jmp 处指令跳转到函数执行的部分) ,即 f.cpp->f.o 的符号表中,不会生成 f 的地址。

当编译时,由于头文件要被包含,但是这时只有函数声明,但是没有函数的定义,所以只能在链接时展开,这里只能变为 call + 地址的指令,但是内联函数并没有地址,链接不到,就报错了。

所以当声明和定义分离,调用函数时,由于内联函数无地址,编译器链接不到,就会报错,为链接错误。

// F.h
#include <iostream>
using namespace std;
inline void f(int i)
{
	cout << i << endl;
}

// main.cpp
#include "F.h"
int main()
{
	f(10);
	return 0;
}

因此。申明和定义不要分离,直接在.h 文件中定义,所有包含.h 的地方不需要链接,直接展开

总结:简短,频繁调用的小函数建议定义成 inline 内联函数 .
 1、inline是一种以空间换时间的做法,省了去调用函数的额外开销。由于内联函数会在调用的位置展开,所以代码很长或者有递归的函数不适宜作为内联函数。频繁调用的小函数建议定义成内联函数。
 2、inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有递归等,编译器优化时会忽略掉内联。
 3、inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了链接就会找不到。

3.auto关键字(C++11)

3.1auto简介

在早期的C/C++中auto的含义是:使用auto修饰的变量是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。
 在C++11中,标准委员会赋予了auto全新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

#include <iostream>
using namespace std;
double Fun()
{
	return 3.14;
}
int main()
{
	int a = 10;
	auto b = a;
	auto c = 'A';
	auto d = Fun();
	//打印变量b,c,d的类型
	cout << typeid(b).name() << endl;//打印结果为int
	cout << typeid(c).name() << endl;//打印结果为char
	cout << typeid(d).name() << endl;//打印结果为double
	return 0;
}

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

3.2 auto的使用细则

3.2.1 auto与指针和引用结合起来使用

用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时必须加&

#include <iostream>
using namespace std;
int main()
{
	int a = 10;
	auto b = &a;   //自动推导出b的类型为int*
	auto* c = &a;  //自动推导出c的类型为int*
	auto& d = a;   //自动推导出d的类型为int
	//打印变量b,c,d的类型
	cout << typeid(b).name() << endl;//打印结果为int*
	cout << typeid(c).name() << endl;//打印结果为int*
	cout << typeid(d).name() << endl;//打印结果为int
	return 0;
}

注意:用auto声明引用时必须加&,否则创建的只是与实体类型相同的普通变量。

3.2.2在同一行定义多个变量

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

int main()
{
	auto a = 1, b = 2; //正确
	auto c = 3, d = 4.0; //编译器报错:“auto”必须始终推导为同一类型
	return 0;
}

3.3 auto不能推导的场景

3.3.1auto不能作为函数的参数

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

void TestAuto(auto x)
{}

3.3.2auto不能直接用来声明数组

int main()
{
	int a[] = { 1, 2, 3 };
	auto b[] = { 4, 5, 6 };//error
	return 0;
}

4.基于范围的for循环(C++11)

范围for的语法糖
若是在C++98中我们要遍历一个数组,可以按照以下方式:

int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//将数组元素值全部乘以2
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
	arr[i] *= 2;
}
//打印数组中的所有元素
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
	cout << arr[i] << " ";
}
cout << endl;

以上方式也是我们C语言中所用的遍历数组的方式,但对于一个有范围的集合而言,循环是多余的,有时还容易犯错。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//将数组元素值全部乘以2
for (auto& e : arr)
{
	e *= 2;
}
//打印数组中的所有元素
for (auto e : arr)
{
	cout << e << " ";
}
cout << endl;

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

4.1范围for的使用条件

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

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

以下代码就有问题,因为for的范围不确定,因为函数传参,数组就会退化为指针:

void TestFor(int array[])
{
	for (auto& e : array)
    {
        cout << e << endl;
    }
}

4.1.2迭代的对象要实现++和==操作

这是关于迭代器的问题,先了解一下。

5.指针空值nullptr

5.1 C++98中的指针空值

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

int* p1 = NULL;
int* p2 = 0;

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

/* Define NULL pointer value */
#ifndef NULL
#ifdef __cplusplus
#define NULL    0
#else  /* __cplusplus */
#define NULL    ((void *)0)
#endif  /* __cplusplus */
#endif  /* NULL */

可以看到,NULL可能被定义为字面常量0,也可能被定义为无类型指针(void*)的常量。但是不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,例如:

#include <iostream>
using namespace std;
void Fun(int p)
{
	cout << "Fun(int)" << endl;
}
void Fun(int* p)
{
	cout << "Fun(int*)" << endl;
}
int main()
{
	Fun(0);           //打印结果为 Fun(int)
	Fun(NULL);        //打印结果为 Fun(int)
	Fun((int*)NULL);  //打印结果为 Fun(int*)
	return 0;
}

程序本意本意是想通过Fun(NULL)调用指针版本的Fun(int* p)函数,但是由于NULL被定义为0,Fun(NULL)最终调用的是Fun(int p)函数。

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

5.2 C++11中的指针空值

对于C++98中的问题,C++11引入了关键字nullptr。
在这里插入图片描述

注意
 1、在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为关键字引入的。
 2、在C++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同。
 3、为了提高代码的健壮性,在后序表示指针空值时建议最好使用nullptr。

6.总结:

今天我们认识并具体学习了有关引用、内联函数、auto关键字、范围for循环(C++11)、指针空值nullptr的知识。接下来,我们将继续学习C++中类和对象的相关知识。希望我的文章和讲解能对大家的学习提供一些帮助。

当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/442168.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【pinia】新一代更好用的状态管理器Pinia

目录 一&#xff0c;Pinia状态管理库 1.Pinia介绍 2.Pinia的核心特性 3.核心概念 4.Pinia vs Vuex 5.Pinia & Vuex的不同 6.Pinia名字 二&#xff0c;Pinia基本使用 1.安装Pinia 2.配置main.ts文件 3.创建store/index.ts文件 4.使用数据 三&#xff0c;状态更新…

再获CSA大奖!顺丰科技腾讯安全iOA零信任联合方案获认可

随着千行百业数字化转型的加速&#xff0c;远程办公、业务协同、分支互联等需求涌现&#xff0c;传统的基于边界的网络安全防护理念难以有效抵挡层出不穷的威胁攻击&#xff0c;基于“无边界安全”理念的零信任技术模型逐渐成为企业关注的重点。 近日&#xff0c;第六届云安全…

千云物流 -车辆智能监控调度(一)-技术选型

技术选型 消息队列&#xff1a;rabbitMq 时序数据库&#xff1a; TDengine kv存储&#xff1a;redis 时序数据库 https://db-engines.com/en/ranking/timeseriesdbms 选择范围&#xff1a;Apache IoTDB&#xff0c;TDengine&#xff0c;OpenTSDB 对于存储车辆位置数据的时序数…

LabelImg安装记录

一 安装anaconda 安装conda主要是为了方便环境管理&#xff0c;避免软件版本冲突&#xff0c;安装简单&#xff0c;教程也很多&#xff0c;不做赘述 二 创建虚拟环境 在这里&#xff0c;我们创建一个专门用于标注数据的虚拟环境&#xff0c;取名为labelImg # 第一条命令c…

特斯拉 Tesla 热管理系统技术迭代分析(Model S/X/3/Y热管理系统介绍)

摘要&#xff1a; 特斯拉第三代热管理系统 为了更好地了解特斯拉的技术迭代以及集成度较高的热管理技术&#xff0c;今天我们针对特斯拉初代和第二代热管理系统做简单介绍。 特斯拉第一代热管理系统 系统架构原理图 第一代热管理系统应用在Model S和Model X上&#xff0c;共有…

4月14号软件资讯更新合集.....

PlayEdu v1.0-beta.2 版本发布&#xff0c;企业培训解决方案 PlayEdu 是基于 SpringBoot3 Java17 React18 开发的企业内部培训系统。它专注于提供私有化部署方案&#xff0c;包括视频&#xff0c;图片等资源的内网部署。目前主要支持有本地视频上传播放、学员邮箱登录、无限…

MIT6.824 Lecture18 Fork Consistency

Background 拜占庭问题&#xff08;Byzantine Generals Problem&#xff09;得名于一个古老的传说&#xff0c;讲述了拜占庭帝国在战争中的一个失败策略。在这个故事中&#xff0c;多名拜占庭将军要协调进攻或撤退的行动&#xff0c;但是其中一些将军可能会向其他帝国泄露假消…

在 Rocky linux 8.5 使用 Kubespray v2.21.0 离线部署 kubernetes v1.25.6 集群(草稿)

文章目录 前言创建7台虚拟机要求配置代理下载介质部署前准备安装部署工具配置 python venv配置部署容器 配置互信编写 inventory.ini创建 offline.yml部署 offline repokubespray v2.21.1 部署 kubernetes 失败报错1&#xff1a;Install packages requirements报错2&#xff1a…

各主流图床经历-尝试gitee,七牛云,smms,阿里云

目录 结论&#xff1a;都试过之后我还推荐用aliyun&#xff0c;反正不太贵 目的&#xff1a; 经历&#xff1a; typora用阿里云作图床的流程 结论&#xff1a;都试过之后我还推荐用aliyun&#xff0c;反正不太贵 目的&#xff1a; 想要让md文件中的本地相对链接转为网络图…

使用chatgpt一分钟帮你实现思维导图

前言 本篇基础篇课程&#xff0c;实操起来很简单&#xff0c;但却非常的实用。利用好这个功能&#xff0c;工作效率或能提升10倍&#xff01; 本篇内容的主题&#xff1a;利用ChatGPT&#xff0c;一分钟帮你实现详尽的思维导图。 创作内容大纲 格式转化 结合Xmind 创作内容…

2021地理设计组二等奖:城市三维空间格局对城市内涝的影响研究——以深圳市为例

作品简介 一、设计思想 内涝是指由于连续性降雨或强降雨导致城市地表径流超过地下管网排水能力从而引发的积水现象。内涝的发生会严重破坏城市基础服务设施&#xff08;如交通运输、通讯以及水、电、气的供应&#xff09;&#xff0c;甚至严重影响人民的财产和生命安全。为了降…

【Git基础】常用git命令(二)

文章目录 1. 合并commit为一个1.1 git commit --amend1.2 git rebase -i1.3 git reset1.4 示例 2. 修改commit的内容2.1 git commit --amend2.2 git rebase -i2.3 git cherry-pick2.4 git reflog和git reset 3. 查看commit内容3.1 git log3.2 git log --oneline3.3 git log -[l…

全网多种方法解决[rejected] master -> master (fetch first)的错误

文章目录 1. 复现错误2. 分析错误3. 解决错误4. 解决该错误的其他方法 1. 复现错误 今天使用git status查看文件状态&#xff0c;发现有一个文件未提交&#xff0c;如下代码所示&#xff1a; D:\project\test>git status On branch master Your branch is up to date with …

基于高德导航的大作业

绪 论 课题的建设背景 移动互联网 如今手机的发展非常迅速&#xff0c;手机越来越成为人们不可缺少的东西。手机从最初功能简单的功能机&#xff0c;发展到如今几乎无所不能的智能机&#xff0c;满足了人们的日常需求&#xff0c;手机不再是简单的通讯工具&#xff0c;还可以通…

Spring请求与响应——请求

Spring请求与响应——请求 请求映射路径RequestMapping() 请求参数GetGet发请求参数接收Get请求参数 POSTPOST发送参数 参数种类POJO类型参数数组类型类型参数集合类型参数时间参数演示 JSON数据传输参数发送请求接收请求集合参数与pojo参数演示 RequestBody与RequestParam区别…

手机信号不好?真不是套餐原因!4招教你搞定!

不少小伙伴跟搜卡之家小编抱怨手机信号突然不好&#xff0c;比如&#xff1a; 打着电话突然听不见对方的声音! 手机玩着玩着刷不出来页面和图片&#xff01; 手机信号满格但上不了网&#xff01; 这手机是不是坏了&#xff01; 其实这样的情况大部分都是因为手机信号的问题…

【Arduino】舵机的连线和基本操作

&#x1f38a;专栏【Arduino】 &#x1f354;喜欢的诗句&#xff1a;更喜岷山千里雪 三军过后尽开颜。 &#x1f386;音乐分享【勋章】 大一同学小吉&#xff0c;欢迎并且感谢大家指出我的问题&#x1f970; 目录 &#x1f6a5;舵机图片 &#x1f6a5;舵机连接方式 &#x1…

字典树(Trie/前缀树)详解

目录 字典树的概念 字典树的逻辑 字典树的实现 字典树小结 例题强化 字典树的概念 字典树&#xff08;Trie&#xff09;是一种空间换时间的数据结构&#xff0c;是一棵关于“字典”的树&#xff0c;主要用于统计、排序和保存大量的字符串。字典树是通过利用字符串的公共前…

【unity实战】随机地下城生成2——绘制地图Tilemap的使用及一些技巧的使用(含源码)

绘制房间 修改素材配置 切割图片 绘制瓦片地图 先新建我们的调色盘,保存好位置 拖入我们刚才切片好的素材 在房间预设体创建我们的瓦片地图 绘制地图的小技巧 点选移动适合的瓦片移动到位置上绘画 框选复制已经绘制好的地图,快捷键i

【C# .NET】chapter 13 使用多任务改进性能和可扩展性

目录 一、物理内存和虚拟内存使用&#xff08;Recorder 类&#xff09; 二、 对比 string的“”操作与stringbuilder 操作 的处理效率&#xff0c;内存消耗情况&#xff0c; 三、异步运行任务、三种启动任务方法、将上一任务方法处理结果作为参数传给下一任务方法 四、嵌套…