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

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

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
• 3

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

例如:

int a = 10;
int& b = a;
int& c = a;
int& d = a;
• 1
• 2
• 3
• 4

此时,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 的拷贝。而这其中会有一个临时变量,返回的是临时变量bac9390ef6bf4628b2db97ecb59bc715.png

反向证明:如果返回的是 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修饰了的类型理解为安全的类型,因为其不能被修改。我们若将一个安全的类型交给一个不安全的类型(可被修改),那么将不会成功。113b0067f2314bc68ce6fe15a9364581.pngconst 修饰的 a 不能修改,b 为 a 的引用。a 是只读,但是引用 b 具有 可读可写 的权利,该情况为 权限放大 ,所以错误了。下面没有错误是因为是一个拷贝,d的改变不影响c


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

7664c13a076f4437a7a15abbfde2c68a.png权限可以缩小,此时++x可以,因为x本身有可以修改的权限且y、z的值同时也会变,因为本来就是同一个空间,x的改变就是y、z的改变。只是作为z时,由于权限限制,++z不行5b998678a3ad46908212e60b6563ce57.png对于函数的返回值来说,也不能权限放大,例如:

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

202349abcb4a40fb8da549a5818eb0b7.png这样也是不行的,因为返回方式为 传值返回 ,返回的是临时变量,具有 常性 ,是不可改的;而引用放大了权限,所以是错误的

这时加 const 修饰,权限平移,就没问题了:const int& ret = func1()e537dccd641949c1916ddeaf3929ff7a.png同理,这里错误的原因:发生类型转换(提升、截断)的时候会产生一个临时变量bc6a68b675c449dcb2d188436144d17d.png对于类型转换来说,在转换的过程中会产生一个临时变量,例如 int ii =dd,把dd转换后的值放到临时变量中,把临时变量给接收的值ii,而临时变量具有常性,不可修改,引用就加了写权限,就错了,因为 权限被放大了 。


而下图由于返回的是x的别名,不是x,不会产生临时变量了,再传给int& ret,为权限平移

3fceaa057b7a4e23b9c520ddffb3a018.png总结:对于引用,引用后的变量所具权限可以缩小或不变,但是不能放大(指针也适用这个说法)。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;
}

9e566b03f48f447cae95b83e3053df38.png但通过上图可以看出写宏时很容易出错(下次再错就挨打吧[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++ 区域常规部分,在调试信息一栏设置格式为程序数据库:

916763fec380482b87532e14ad207adc.png在 C/C++ 优化一栏,将内联函数扩展部分选中只适用于 _inline :image.png设置完毕后,点击应用。

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

修改前:image.png两段反汇编代码最大的区别就是 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。

image.png注意:

 1、在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为关键字引入的。

 2、在C++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同。

 3、为了提高代码的健壮性,在后序表示指针空值时建议最好使用nullptr。

6.总结:

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

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

c3ad96b16d2e46119dd2b9357f295e3f.jpg

相关文章
|
12天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
35 4
|
27天前
|
存储 安全 编译器
在 C++中,引用和指针的区别
在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
|
1月前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
38 1
|
10天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
39 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
24 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
1月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
53 1
下一篇
无影云桌面