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

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


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::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[] = {4,5,6};
}
  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++的入门知识的学习便告一段落了。有了以上知识的铺垫,我们后面的学习才能开展下去。最后一定要认真总结哟!!!

相关文章
|
1月前
|
存储 安全 编译器
【C++打怪之路Lv1】-- 入门二级
【C++打怪之路Lv1】-- 入门二级
23 0
|
1月前
|
自然语言处理 编译器 C语言
【C++打怪之路Lv1】-- C++开篇(入门)
【C++打怪之路Lv1】-- C++开篇(入门)
24 0
|
1月前
|
分布式计算 Java 编译器
【C++入门(下)】—— 我与C++的不解之缘(二)
【C++入门(下)】—— 我与C++的不解之缘(二)
|
1月前
|
编译器 Linux C语言
【C++入门(上)】—— 我与C++的不解之缘(一)
【C++入门(上)】—— 我与C++的不解之缘(一)
|
7天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
33 4
|
8天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
28 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
23 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)