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

在这里插入图片描述

​📝个人主页:@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++中类和对象的相关知识。希望我的文章和讲解能对大家的学习提供一些帮助。

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

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL数据库学习笔记之存储引擎

存储引擎 MySQL体系结构 连接层 最上层是一些客户端和连接服务&#xff0c;主要完成一些类似于连接处理、授权认证、以及相关的安全方案。服务器也会为安全接入的每个客户端验证它所具有的操作权限。 服务层 第二层架构主要完成大多数的核心服务功能&#xff0c;如SQL接口&am…

【Linux网络】部署YUM仓库及NFS服务

部署YUM仓库及NSF服务 一、YUM仓库1.1、YUM仓库概述1.2准备安装来源1.3在软件仓库加载非官方RPM包组1.4yum与apt 二、配置yam源与制作索引表2.1配置FTP源2.2配置国内在线yum源2.3在线源与本地源同时使用2.4建立软件包索引关系表的三种方法 三、nfs共享存储服务3.1安装软件&…

OpenAI-ChatGPT最新官方接口《微调ChatGPT模型》全网最详细中英文实用指南和教程,助你零基础快速轻松掌握全新技术(四)(附源码)

微调ChatGPT模型 前言Introduction 导言What models can be fine-tuned? 哪些模型可以微调&#xff1f;Installation 安装Prepare training data 准备训练数据CLI data preparation tool CLI数据准备工具Create a fine-tuned model 创建微调模型Use a fine-tuned model 使用微…

Windows下版本控制器(SVN) - 1、开发中的实际问题+2、版本控制简介

文章目录 基础知识-Windows下版本控制器(SVN)1、开发中的实际问题2、版本控制简介2.1 版本控制[Revision control]2.2 Subversion2.3 Subversion 的优良特性2.4 SVN 的工作原理&#xff1a;2.5 SVN 基本操作 本人其他相关文章链接 基础知识-Windows下版本控制器(SVN) 1、开发中…

docker容器:docker镜像的三种创建方法及dockerfile案例

目录 一、基于现有镜像创建 1、创建启动镜像 2、生成新镜像 二、基于本地模板创建 1、OPENVZ 下载模板 2、导入容器生成镜像 三、基于dockerfile创建 1、dockerfile结构及分层 2、联合文件系统 3、docker镜像加载原理 4、dockerfile操作常用的指令 (1)FROM指令 (…

倾斜摄影三维模型格式转换OSGB 到3Dtitles 实现的常用技术方法

倾斜摄影三维模型格式转换OSGB 到3Dtitles 实现的常用技术方法 倾斜摄影三维模型是一种用于建立真实世界三维场景的技术&#xff0c;常用于城市规划、土地管理、文化遗产保护等领域。在倾斜摄影模型中&#xff0c;OSGB格式和3Dtiles格式都是常见的数据格式。其中&#xff0c;OS…

ChatGPT 速通手册——连续提问和重新生成的作用

连续提问和重新生成的作用 和 ChatGPT 聊天&#xff0c;也是有套路的。我们把给 ChatGPT 输入的问题文本&#xff0c;叫 Prompt&#xff0c;提示词。实际上&#xff0c;传统搜索引擎也有比较相类似的功能。 在 Prompt Learning 提示学习之后&#xff0c;又总结出一种更好的聊…

如何高效提高倾斜摄影三维模型顶层合并的技术方法分析

如何高效提高倾斜摄影三维模型顶层合并的技术方法分析 1、倾斜摄影三维模型顶层合并 1.1倾斜摄影三维模型是一种基于倾斜摄影技术&#xff0c;通过多个角度拍摄同一区域的影像&#xff0c;利用计算机图像处理和三维重建技术生成的三维地理信息数据。由于一个大区域可能需要多块…

13-nginx

一 初始Nginx 1 Nginx概述 Nginx是一款轻量级的Web服务器、反向代理服务器&#xff0c;由于它的内存占用少&#xff0c;启动极快&#xff0c;高并发能力强&#xff0c;在互联网项目中广泛应用。Nginx 专为性能优化而开发&#xff0c;使用异步非阻塞事件驱动模型。 常见服务器 …

57 openEuler搭建Mariadb数据库服务器-管理数据库用户

文章目录 57 openEuler搭建Mariadb数据库服务器-管理数据库用户57.1 创建用户57.2 查看用户57.3 修改用户57.3.1 修改用户名57.3.2 修改用户示例57.3.3 修改用户密码57.3.4 修改用户密码示例 57.4 删除用户57.5 用户授权57.6 删除用户权限 57 openEuler搭建Mariadb数据库服务器…

(Ubuntu22.04 Jammy)安装ROS2 Humble

文章目录 (Ubuntu22.04 Jammy)安装ROS2 (Humble)版本一、设置本地区域二、设置源三、安装ROS2软件包四、环境设置五、测试用例Talker-listener 六、卸载ros2 (Ubuntu22.04 Jammy)安装ROS2 (Humble)版本 提示&#xff1a;以下内容是已经安装了ubuntu22.04 下进行安装ros2 一、设…

Java语法理论和面经杂疑篇《十二. JDK8 - 17新特性》

第18章_JDK8-17新特性&#xff08;下&#xff09; 6. 新语法结构 新的语法结构&#xff0c;为我们勾勒出了 Java 语法进化的一个趋势&#xff0c;将开发者从复杂、繁琐的低层次抽象中逐渐解放出来&#xff0c;以更高层次、更优雅的抽象&#xff0c;既降低代码量&#xff0c;又…

设计模式-结构型模式之享元模式

5. 享元模式 5.1. 模式动机 面向对象技术可以很好地解决一些灵活性或可扩展性问题&#xff0c;但在很多情况下需要在系统中增加类和对象的个数。当对象数量太多时&#xff0c;将导致运行代价过高&#xff0c;带来性能下降等问题。 享元模式正是为解决这一类问题而诞生的。享元模…

函数栈帧的创建和销毁【汇编语言理解】

&#x1f339;作者:云小逸 &#x1f4dd;个人主页:云小逸的主页 &#x1f4dd;Github:云小逸的Github &#x1f91f;motto:要敢于一个人默默的面对自己&#xff0c;强大自己才是核心。不要等到什么都没有了&#xff0c;才下定决心去做。种一颗树&#xff0c;最好的时间是十年前…

【C++的内联函数】

文章目录 一、什么是内联函数二、内联函数的优缺点三、使用内联函数的注意事项 一、什么是内联函数 用关键字inline修饰的函数叫做内联函数。 C编译器编译时会自动在被调用的地方展开。 二、内联函数的优缺点 内联函数的优点&#xff1a; 没有函数栈帧创建&#xff0c;提升…

WebRTC系列-Qos系列之AEC-可配置参数

文章目录 1. 简介2. 源码中相关参数WebRTC的自适应回声消除(AEC)是一个广泛使用的技术,用于在音频通信中消除扬声器输出产生的回声。在WebRTC中,有三种AEC算法可供选择,分别是 AECM、 AEC和 AEC3。本文将介绍WebRTC AEC 3算法的原理和应用场景。 在上图中可以看出AEC算…

应用于音箱领域中的音频功放IC型号推荐

音箱音频功放ic俗称“扩音机”又叫音频功率放大器IC&#xff1b;是各类音响器材中不可缺少的部分&#xff0c;其作用主要是将音源器材输入的较微弱信号进行放大后&#xff0c;产生足够大的电流去推动扬声器进行声音的重放。 现如今&#xff0c;音频功放芯片伴随着人工交互及智…

OpenShift 4 - 在 CI/CD Pipeline 中创建 KubeVirt 容器虚拟机 - 方法1+2 (视频)

《OpenShift / RHEL / DevSecOps 汇总目录》 说明&#xff1a;本文已经在支持 OpenShift 4.12 的 OpenShift 环境中验证 文章目录 准备环境安装可实现 KubeVirt 操作的 Tekton 资源创建密钥对 在 CI/CD 流水线管道中创建 VM方法1&#xff1a;通过 Manifest 任务创建 VM方法2&am…

自动驾驶企业面临哪些数据安全挑战?

近期&#xff0c;“特斯拉员工被曝私下分享用户隐私”不可避免地成了新闻热点&#xff0c;据说连马斯克也不能幸免。 据相关媒体报道&#xff0c;9名前特斯拉员工爆料在2019年至2022年期间&#xff0c;特斯拉员工通过内部消息系统私下分享了一些车主车载摄像头记录的隐私视频和…

spring框架注解

3.Spring有哪些常用注解呢&#xff1f; Spring常用注解 Web: Controller&#xff1a;组合注解&#xff08;组合了Component注解&#xff09;&#xff0c;应用在MVC层&#xff08;控制层&#xff09;。 RestController&#xff1a;该注解为一个组合注解&#xff0c;相当于Con…