当前位置: 首页 > news >正文

【C++入门(下篇)】C++引用,内联函数,auto关键字的学习

前言:

在上一期我们进行了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 引用特性

  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,再不能引用其他实体

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;
}

首先还是直接看我们的运行结果如下:
在这里插入图片描述

具体解析:

本例演示了三种交换变量的值的方法:

  1. swap1() 直接传递参数的内容,不能达到交换两个数的值的目的。对于 swap1() 来说,a、b 是形参,是作用范围仅限于函数内部的局部变量,它们有自己独立的内存,和 num1、num2 指代的数据不一样。调用函数时分别将 num1、num2 的值传递给 a、b,此后 num1、num2 和 a、b 再无任何关系,在 swap1() 内部修改 a、b 的值不会影响函数外部的 num1、num2,更不会改变 num1、num2 的值。

  2. swap2() 传递的是指针,能够达到交换两个数的值的目的。调用函数时,分别将 num1、num2 的指针传递给 c,d,此后c,d指向 a、b 所代表的数据,在函数内部可以通过指针间接地修改 a、b 的值。

  3. 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++有哪些技术替代宏?

  1. 常量定义 换用const enum
  2. 短小函数定义 换用内联函数

因此,咱们需要探究一下二者之间的具体关系有哪些!!!

首先咱们先回顾一下宏的优缺点有哪些:
优点:

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:变量的自动类型推断。

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

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

我们举例例子来说明:

#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不能推导的场景

  1. auto不能作为函数的参数

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

  1. auto不能直接用来声明数组
void TestAuto()
{int a[] = {1,2,3};auto b[] = {456};
}
  1. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
  2. 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的使用条件

  1. for循环迭代的范围必须是确定的
    对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
    注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{for(auto& e : array)cout<< e <<endl;
}
  1. 迭代的对象要实现++和==的操作。(关于迭代器这个问题,以后会讲,现在提一下,没办法讲清楚,现在大家了解一下就可以了)

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 】无法隐式转换为整形,而可以隐式匹配指针类型,因此执行结果和我们的预期相符。

注意:

  1. 在使用【nullptr 】表示指针空值时,不需要包含头文件,因为【nullptr 】是C++11作为新关键字引入的。
  2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用【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++的入门知识的学习便告一段落了。有了以上知识的铺垫,我们后面的学习才能开展下去。最后一定要认真总结哟!!!

相关文章:

【C++入门(下篇)】C++引用,内联函数,auto关键字的学习

前言&#xff1a; 在上一期我们进行了C的初步认识&#xff0c;了解了一下基本的概念还学习了包括&#xff1a;命名空间&#xff0c;输入输出以及缺省参数等相关的知识。今天我们将进一步对C入门知识进行学习&#xff0c;主要还需要大家掌握我们接下来要学习的——引用&#xf…...

基于合作型Stackerlberg博弈的考虑差别定价和风险管理的微网运行策略研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

2023年全国最新保安员精选真题及答案8

百分百题库提供保安员考试试题、保安职业资格考试预测题、保安员考试真题、保安职业资格证考试题库等&#xff0c;提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻松过关。 81.以下各组情形都属于区域巡逻中异常情况的是&#xff08;&#xff09;。 A&#x…...

JavaScript高级程序设计读书分享之6章——MapSet

JavaScript高级程序设计(第4版)读书分享笔记记录 适用于刚入门前端的同志 Map 作为 ECMAScript 6 的新增特性&#xff0c;Map 是一种新的集合类型&#xff0c;为这门语言带来了真正的键/值存储机制。Map 的大多数特性都可以通过 Object 类型实现&#xff0c;但二者之间还是存在…...

改进的 A*算法的路径规划(路径规划+代码+毕业设计)

引言 近年来&#xff0c;随着智能时代的到来&#xff0c;路径规划技术飞快发展&#xff0c;已经形成了一套较为成熟的理论体系。其经典规划算法包括 Dijkstra 算法、A算法、D算法、Field D算法等&#xff0c;然而传统的路径规划算法在复杂的场景的表现并不如人意&#xff0c;例…...

Tina_Linux存储性能参考指南

OpenRemoved_Tina_Linux_存储性能_参考指南 1 概述 1.1 编写目的 介绍TinaLinux 存储性能的测试方法和历史数据&#xff0c;提供参考。 1.2 适用范围 Tina V3.0 及其后续版本。 1.3 相关人员 适用于TinaLinux 平台的客户及相关技术人员。 2 经验性能值 Flash 性能与实…...

NCRE计算机等级考试Python真题(四)

第四套试题1、以下选项中&#xff0c;不属于需求分析阶段的任务是&#xff1a;A.需求规格说明书评审B.确定软件系统的性能需求C.确定软件系统的功能需求D.制定软件集成测试计划正确答案&#xff1a; D2、关于数据流图&#xff08;DFD&#xff09;的描述&#xff0c;以下选项中正…...

LeetCode每周刷题总结2.20-2.26

本栏目记录本人每周写的力扣题的相关学习总结。 虽然开新的栏目都没有完成 70.爬楼梯 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 解题思路&#xff1a; 斐波那契数列递归 class Solution {…...

u盘里删除的文件可以恢复吗?分享解决方法

u盘里删除的文件可以恢复吗?不知道使用过U盘的你&#xff0c;是否遇到过这样的问题呢?其实正常情况下&#xff0c;在电脑中操作u盘&#xff0c;并删除相关的文件&#xff0c;删除的文件是不会经过电脑回收站的。想要找回就需要借助相关的恢复工具才能实现。下面小编给大家分享…...

十、vben框架如何使用table来写报表

在项目开发的过程中&#xff0c;有很多特殊的table样式&#xff0c;有的时候后端会用帆软来写报表&#xff0c;但是有的特殊的报表后端就不能支持实现了&#xff0c;那么前端是如何实现的呢&#xff0c;今天我们就来讲讲。 先上效果图&#xff1a; 本次使用的tsx组件来写的报表…...

jQuery:入门

jQuery 入门 Date: January 19, 2023 目标&#xff1a; 能够说出什么是 jQuery 能够说出 jQuery 的优点 能够简单使用 jQuery 能够说出 DOM 对象和 jQuery 对象的区别 jQuery 概述 JavaScript 库 仓库&#xff1a; 可以把很多东西放到这个仓库里面。找东西只需要到仓库里…...

实例3:树莓派呼吸灯

实例3&#xff1a;树莓派呼吸灯 实验目的 通过背景知识学习&#xff0c;了解digital与analog的区别。通过GPIO对外部LED灯进行呼吸控制&#xff0c;熟悉PWM技术。 实验要求 通过python编程&#xff0c;用GPIO控制LED灯&#xff0c;使之亮度逐渐增大&#xff0c;随后减小&am…...

android适配ipv6,请求慢?

先贴一篇我们经常能搜索到的解决方案&#xff1a; Android 在 4G 下访问 IPV6 慢的解决方案 文章很有参考意义&#xff0c;但也并不是所有请求慢的的原因&#xff01; 本文是另一种原因,有兴趣就继续往下看一看. 使用的okhttp框架,模式支持ipv6和ipv4协议,但两种协议同时存在时…...

【LeetCode】剑指 Offer(10)

目录 题目&#xff1a;剑指 Offer 27. 二叉树的镜像 - 力扣&#xff08;Leetcode&#xff09; 题目的接口&#xff1a; 解题思路&#xff1a; 代码&#xff1a; 过啦&#xff01;&#xff01;&#xff01; 题目&#xff1a;剑指 Offer 28. 对称的二叉树 - 力扣&#xff0…...

学校AI视频行为分析监测系统 opencv

学校AI视频行为分析监测系统通过pythonopencv网络模型AI视频分析技术&#xff0c;学校AI视频行为分析监测算法对学校区域人员打架行为识别、跌倒行为识别、翻墙识别、人员聚众识别、攀高识别、抽烟行为等进行智能识别预警。OpenCV的全称是Open Source Computer Vision Library&…...

内存数据库的设计与实现(已在大型项目中应用)

一、概况 1、设计总图 组成,由Redis集群缓存,普通缓存,传统数据库,各类数据驱动 2、内存数据库的增删改查,分页查询 组成,由数据查询,分页查询,数据存储,数据修改,数据删除 3、内存数据库的驱动 组成,由驱动适配器,普通缓存驱动,Redis缓存驱动 4、内存数据库与…...

Linux基础命令-stat显示文件的状态信息

文章目录 stat 命令介绍 语法格式 基本参数 测试三个时间的变化过程 1&#xff09;使用cat命令 2&#xff09;使用echo命令 3&#xff09;使用chmod命令 4&#xff09;使用vim命令 参考实例 1&#xff09;显示文件的状态信息 2&#xff09;以简洁的形式显示状态信…...

SQL入门DEMO

单表查询 ● --查询订购日期在1996年7月1日至1996年7月15日之间的订单的订购日期、订单ID、客户ID和雇员ID等字段的值 ● --查询供应商的ID、公司名称、地区、城市和电话字段的值。条件是“地区等于华北”并且“联系人头衔等于销售代表”。 –查询供应商的ID、公司名称、地…...

辉光管时钟学习制作及开源软硬件工程

文章目录前言开源地址辉光管项目介绍辉光管的工作条件硬件部分部分介绍充电电路驱动电路不足之处软件部分总结前言 作为一个电子人&#xff0c;一直想做一个辉光管时钟&#xff0c;算是大学的一个心愿&#xff0c;终于在快要毕业前做了一个&#xff0c;下面把软件和硬件的部分…...

动手学深度学习(第二版)学习笔记 第三章

第三章 线性神经网络 代码&#xff1a;d2l-zh/pytorch/chapter_linear-networks 3.1 线性回归 3.1. 线性回归 — 动手学深度学习 2.0.0 documentation 解析解 线性回归的解可以用一个公式简单地表达出来&#xff0c;这类解叫作解析解&#xff08;analytical solution&…...

冯诺依曼体系结构与操作系统的概念及理解

一、 冯诺依曼体系结构1、概念2、内存的作用3、硬件原理解释软件行为二、操作系统的概念及基本作用1、概念2、设计操作系统的目的3、操作系统的主要作用4、什么是管理5、管理的目的6、操作系统如何为我们服务一、 冯诺依曼体系结构 我们常见的计算机&#xff0c;如笔记本。我们…...

【深度探讨】如何利用区块链改善公共服务

发表时间&#xff1a;2022年5月4日 信息来源&#xff1a;bsvblockchain.org BSV区块链协会全力支持符合企业和政府对于节能降耗和合法合规等相关要求的区块链生态系统。 然而&#xff0c;虽然监管机构负责其监管范围内的技术服务的性质、目的和影响&#xff0c;但他们并不是全…...

【打卡】图分析与节点嵌入

背景介绍 图&#xff08;Graphs&#xff09;是一种对物体&#xff08;objects&#xff09;和他们之间的关系&#xff08;relationships&#xff09;建模的数据结构&#xff0c;物体以结点&#xff08;nodes&#xff09;表示&#xff0c;关系以边&#xff08;edges&#xff09;…...

python元编程详解

什么是元编程 软件开发中很重要的一条原则就是“不要重复自己的工作&#xff08;Don’t repeat youself&#xff09;”&#xff0c;也就是说当我们需要复制粘贴代码时候&#xff0c;通常都需要寻找一个更加优雅的解决方案&#xff0c;在python中&#xff0c;这类问题常常会归类…...

为什么文档对 SaaS 公司至关重要?

在过去十年左右的时间里&#xff0c;SaaS的兴起使全球数百家公司成为家喻户晓的公司。但他们并不是仅仅依靠产品的力量到达那里的。客户服务和支持是使一切在幕后顺利进行的原因——其中很大一部分是文档。以正确的风格和正确的位置在您的网站上找到适当的用户文档对于将浏览器…...

Echarts 实现电池效果的柱状图

第022个点击查看专栏目录本示例是解决显示电池电量状态的柱状图&#xff0c;具体的核心代码请参考源代码。 文章目录示例效果示例源代码&#xff08;共102行&#xff09;相关资料参考专栏介绍示例效果 示例源代码&#xff08;共102行&#xff09; /* * Author: 还是大剑师兰特…...

计算机网络高频知识点(一)

目录 一、http状态码 二、浏览器怎么数据缓存 三、强缓存与协商缓存 1、强缓存 2、协商缓存 四、简单请求与复杂请求 五、PUT 请求类型 六、GET请求类型 七、GET 和 POST 的区别 八、跨域 1、什么时候会跨域 2、解决方式 九、计算机网络的七层协议与五层协议分别指…...

JavaScript split()方法

JavaScript split()方法 目录JavaScript split()方法一、定义和用法二、语法三、参数值四、返回值五、更多实例5.1 省略分割参数5.2 使用limit参数5.3 使用一个字符作为分割符一、定义和用法 split() 方法用于把一个字符串分割成字符串数组。 二、语法 string.split(separat…...

前端面试题 —— 性能优化

目录 一、CDN的作用 二、CDN的使用场景 三、懒加载的概念 四、懒加载与预加载的区别 五、documentFragment 是什么&#xff1f;用它跟直接操作 DOM 的区别是什么&#xff1f; 六、常见的图片格式及使用场景 七、懒加载的特点 八、如何优化动画&#xff1f; 九、如何提⾼…...

我的周刊(第080期)

我的信息周刊&#xff0c;记录这周我看到的有价值的信息&#xff0c;主要针对计算机领域&#xff0c;内容主题极大程度被我个人喜好主导。这个项目核心目的在于记录让自己有印象的信息做一个留存以及共享。&#x1f3af; 项目stable-diffusion-webui-docker[1]基于 Docker 的一…...

深圳广东网站建设套餐/郑州网站优化排名

01 listPython内置的一种数据类型是列表&#xff1a;list。list是一种有序的集合&#xff0c;可以随时添加和删除其中的元素。比如&#xff0c;列出班里所有同学的名字&#xff0c;就可以用一个list表示&#xff1a;classmates [Michael, Bob, Tracy]print(classmates)变量cla…...

网站制作公司哪儿济南兴田德润有活动吗/seo在线工具

题目链接 第一个差分约束的题目。看了好多的博客&#xff0c;差分约束就是根据不等式建好图&#xff0c;然后求最短路或者最长路即可&#xff0c;很明显建图是关键。 这个题的题意是&#xff0c;给很多的区间&#xff0c;每个区间上至少ci个数&#xff0c;问最这些数最少有多少…...

秦皇岛市住房公积金管理中心/优化大师班级优化大师

public V put(K key, V value): 把指定的键与指定的值添加到Map集合中。 说明&#xff1a; ​ 在使用put存储一对元素(key-value)对象时&#xff0c;会先拿key去判断Map集合中是否已经存在。 ​ 如果Map集合中没有相同的key存在&#xff1a;就把key-value存储到Map集合中&am…...

网站做显卡评测软件/google推广平台怎么做

Seems like a lot of my posts lately have started with something like "Heres a weird IE bug" or "Heres something odd in .NET" but... 似乎我最近的许多帖子都是以“这是一个奇怪的IE错误”或“这是.NET中的奇怪内容”开头的&#xff0c;但是... He…...

nginx wordpress 404/网络营销课程实训总结

我们已经学习了字符串和数字基础的处理方法和逻辑&#xff0c;大家有没有觉得使用起来很方便&#xff0c;编程的过程中也是很给力的呀&#xff01;其实Python还有更多字符串处理的方法&#xff0c;大家今天就一起来体验一下吧小朋友们可以先复习一下前一节课的知识哈&#xff0…...

做零售出口的网站/武汉seo优化

1,第一个问题---报错---现象&#xff0c;目标mysql库两个表&#xff0c;一个有数据&#xff0c;一个没有数据&#xff0c;切复制进程RBA为0查看源端ggserr.log 发现端倪2018-06-27 15:47:42 INFO OGG-00987 Oracle GoldenGate Command Interpreter for Oracle: GGSCI command (…...