C++ 波澜壮阔 40 年:引用、内联函数与现代空指针,效率跃升三基石

C++ 波澜壮阔 40 年:引用、内联函数与现代空指针,效率跃升三基石


在这里插入图片描述


🔥@晨非辰Tong: 个人主页
👀专栏:《数据结构与算法入门指南》《C++学习之旅》
💪学习阶段:C语言、数据结构与算法初学者
⏳“人理解迭代,神理解递归。”


文章目录


引言

C++的演进之路,是不断在性能与安全、灵活与严谨之间寻求平衡的艺术。

本文将深入剖析三大特性:“引用”、“内联函数”、“nullptr”。

理解它们,不仅是掌握语法,更是洞察C++设计哲学,书写更高效、更健壮代码的关键一步。

一、引用:C++前期重难点

1.1 一览:引用的方方面面

  • 概念及定义:

引用不是重新定义变量,而是给已经定义的变量起一个别名(编译器不会为引用变量开辟内存空间,共用同一块)。

形式:类型& 引用别名 = 引用对象;

C++中为了避免引入太多的运算符,会复用C语言的⼀些符号,比如<<,>>,引用也和取地址使用了同⼀个符号&,注意使用方法角度来区分。
  • 引用的特性:
  1. 引用在定义时必须初始化。
  2. ⼀个变量可以有多个引用。
  3. 引用一旦引用一个实体,再不能引用其他实体。
#include<iostream>usingnamespace std;intmain(){int i =10;//引用:j是i的别名int& j = i;//多个引用int& k = i;//给别名取别名int& a = j;//看地址 cout <<&i <<'\n'; cout <<&j <<'\n'; cout <<&k <<'\n'; cout <<&a << endl;return0;}
在这里插入图片描述

(观察地址都是一个变量!)

#include<iostream>usingnamespace std;intmain(){//未初始化引用int& j; cout << j << endl;return0;}
在这里插入图片描述
#include<iostream>usingnamespace std;intmain(){int a =10;int& b = a;//再次引用其他变量相当于赋值int c =20; b = c; cout <<"b="<< b << endl;return0;}
在这里插入图片描述

1.2 划重点:引用的正确使用

  • 引用的主要实践用途是通过引用传参和引用返回来减少数据拷贝提高效率,以及在修改引用对象时同步改变被引用的原对象。(引用返回值相对复杂,先简单了解。)
  • 引用传参跟指针传参功能是类似的,引用传参相对更方便⼀些。
  • 引用和指针相辅相成,并不能完全替代。
  1. 引用传参——>代替指针(大部分情况引用可以代替指针)
voidSwap(int* a,int* b){if(*a >*b){int tmp =*a;*a =*b;*b = tmp;}}voidSwap(int& rx,int& ry){if(rx > ry){int tmp = rx; rx = ry; ry = tmp;}}intmain(){int x =2;int y =1;Swap(&x,&y); cout << x <<' '<< y <<'\n';Swap(x, y); cout << x <<' '<< y <<'\n';return0;}
在这里插入图片描述
在逻辑上,rx,ryx,y的别名,本质上就是x,y,所以交换rx,ry就是交换x,y
对于函数传参时,引用的初始化:因为只有调用函数才会定义引用,传参就相当于赋值int& rx = x, int& ry = y
  • 一级指针的引用:
#include<iostream>usingnamespace std;voidswap(int** x,int** y){int* tmp =*x;*x =*y;*y = tmp;}voidswap(int*& x,int*& y){int* tmp = x; x = y; y = tmp;}intmain(){//swap函数交换指针int a =10;int b =17;int* pa =&a;int* pb =&b;swap(&pa,&pb);//用指针 cout <<*pa <<" "<<*pb <<'\n';swap(pa, pb);//用引用 cout <<*pa <<" "<<*pb <<'\n';return0;}
在这里插入图片描述
对于链表、树等,节点定义位置,只能使用指针。因为C++的引用无法改变指针指向,但是节点一定存在改变指向的情况。
  1. 引用返回值:
  • 先看传值返回
#include<iostream>usingnamespace std;//传值返回intfunc(){int ret =0;//...return ret;}intmain(){int x =func();//func() += 1;return0;}
在这里插入图片描述

看传值返回,func函数返回的其实ret的一个拷贝(相当于临时变量),调用结束,函数销毁,看下面的func() += 1;就会报错。

在这里插入图片描述
  • 对比看传引用返回
#include<iostream>usingnamespace std;//传引用返回int&func(){int ret =0;//...return ret;}intmain(){int x =func(); cout << x << endl;return0;}
在这里插入图片描述

看传引用返回,实际上函数返回的是ret的别名(比如tmp)。与传值返回不同的是,函数销毁后,将空间返回操作系统(但仍然指向这块空间),如果别人对空间进行操作,就会改变,这是就相当于野指针的访问!很危险!!

在这里插入图片描述
  • 验证上面说法:
    一个很奇怪的结果:
#include<iostream>usingnamespace std;int&func1(){int ret =0;//...return ret;}int&func2(){int y =123;//...return y;}intmain(){int& x =func1(); cout << x << endl;func2(); cout << x << endl;return0;}
在这里插入图片描述

可以看到,我们并没有修改x的值,为什么再次输出x确是y的数值?

在这里插入图片描述

已经知道,函数销毁后,将空间返回给操作系统(但是别名x仍然指向这块空间),意味着这块空间可以分配给其他操作。那么新创建的函数就会在这块空间上,又因为故意的将两个函数结构写的类似,代表二者的栈帧一样大
既然栈帧一样大,x就会接收func2返回的别名,也就是y的值。

1.3 存疑的地方

  1. 传引用返回相当于野指针访问,为什么不报错?
    要知道,越界不一定报错!数组的越界存在越界抽查。越界抽查:在临近数组结束的位置进行检查。
  • 越界读:没事
intmain(){int a[10];//越界读:没事 a[10];return0;}
在这里插入图片描述
  • 越界写:有事
intmain(){int a[10];//越界写:有事 a[11]=1; a[15]=1;return0;}
在这里插入图片描述

1.4 const引用

  1. 可以对const对象进行引用,但是必须在类型前加constconst引用也可以引用普通对象,因为对象的访问权限在引用过程中能够减小但是不能放大。
  2. 类似int& rb = a*3; (表达式计算值会存到临时对象中)double d = 12.34; int& rd = d;(类型转换过程将中间值存在临时对象)C++规定这类对象具有常性(只读),也就是说需要常引用(避免权限放大)
  3. 临时对象:编译器需要一个空间暂存表达式的计算结果时创建的一个未命名的对象,C++规定为临时对象。
  • 解释第1条:
#include<iostream>usingnamespace std;intmain(){//const对象constint a =10;//权限放大:不能int& ra = a;return0;//权限缩小:可以int b =10;constint& rb = b;//不属于权限放大constint c =1;int rc = c;//权限放大缩小,对const 指针&引用//权限放大constint* p1 =&a;int* p2 = p1;//权限缩小int e =1;int* p3 =&e;constint* p4 = p3;}

不能权限放大: 变量aconst修饰,代表只能读不饿能写,但是后面的引用仿佛在说“可以通过别名对变量进行读写”。但是本体都只能读,一个别名带还想翻天?!,这肯定是错的!(指针同理。但不是别名)

在这里插入图片描述


在这里插入图片描述

注意下面不属于权限放大:属于拷贝复制

#include<iostream>usingnamespace std;int(){constint c =1;int rc = c;return0;}

可以权限缩小: 本体b可以读写,对于别名rb只进行读是允许的。

函数传参使用const

以后函数传参都会使用引用,一方面是减少拷贝提高效率,另一方面则是形参会影响实参(少部分)。既然如此,传参以后建议const修饰,这样就可以传普通对象、const对象、常量。

voidfunc(constint& x){}intmain(){//const int& a = 10;int y =0;func(y);constint z =1;func(z);func(2);return0;}
  • 解释第2条:

类型转换有:隐式类型转换、显式类型转换(强制类型转换)。

#include<iostream>usingnamespace std;intmain(){//C语言隐式转换int i =0;double d = i;//强制转换:整型、指针int p =(int)&i;return0;}

对于引用的类型转换:必须用常引用!

#include<iostream>usingnamespace std;intmain(){int i =1;//double& ri = i;//不行constdouble rd = i;//可以//int& pi = (int)&i;//不行constint& rp =(int)&i;//可以return0;}
在这里插入图片描述


在这里插入图片描述

这就是因为上面说的:会产生临时对象(只读属性),需要对应常引用。

在这里插入图片描述

1.5 引用和指针的关系(面试必看)

通常从语法层面来区分,底层层面只是在特定情况下辅助了解

  • 语法概念上,引用是变量的别名,不需要另开空间;指针存储一个变量的地址,需要额外空间。
  • 引用在定义时必须初始化;指针建议初始化,语法上不必须。
  • 引用只能引用一个实体,不能改变;指针可以不断改变指向对象。
  • 指针容易出现空指针、野指针的情况;引用很少。
  • 引用可以直接访问引用对象;指针需要解引用。
  • sizeof中,引用结果为引用类型的大小;指针始终是地址空间所占的字节数(32位- -4字节,64位- -8字。

二、inline内联函数

  1. inline修饰的函数称为内联函数,编译时C++编译器会在调用函数的位置展开函数,这样就不需要建立栈帧,提高效率
  2. 我使用的是VS编译器,debug版本默认不展开inline便于调试。若需要展开,请看下面如何设置。
  3. inline 对于编译器只是一个建议,编译器可以选择执行与否(不同编译器也不同)。inline适用于频繁调用的小函数,对于递归函数、代码多的函数,编译器会忽略inline
  4. C语言实现的宏函数会在预处理时展开,但实现复杂,易出错,不方便调试。C++设计inline就为了替换宏函数。
  5. inline函数的声明、定义不能分到两个文件(放在同一头文件)。分离会导致内联无法展开,也找不到有效的函数定义,导致连接错误。

2.1 对要点的详细解释

  1. 解释第2条: 如何设置?

找到解决方案资源管理器,鼠标右键你的项目:(红框框)

在这里插入图片描述

在弹出的小界面选择属性后:看图示


  1. 解释第3条:inline只是建议

从汇编辅助理解:以简单的函数为例,得出编译器的选择应该有临界值,超过就不展开。

inlineintAdd(int a,int b){ a++; a++; a++; a++; a++;return a + b;}intmain(){int ret =Add(1,2)*3; cout << ret << endl;return0;}
  • 5个a++,选择展开:
在这里插入图片描述
  • 10个a++就不展开了:看框框,call就代表调用函数,创建栈帧
在这里插入图片描述
  • 划重点,划重点:面试问题:为什么只是”建议“??!
要完全将选择权交给程序员的话,那么就会发生代码指令恶行膨胀问题,导致可执行程序(安装包)过大!!
参照上后面的5个a++,指令变多了,要是调用的多,局都要展开,更多了。但是不展开,就是单次展开 + 次数 * 函数体,这就少多了。
为了避免问题,就将权力给了编译器。
  1. 解释第4条: 替代宏函数

(C++通过const,enum,inline替代宏。)

为什么C语言的宏函数很坑?
拿ADD宏函数为例:先记住宏就是替换
//ADD宏函数:第1种#include<iostream>#defineADD(int a,int b)return a+b;

第1种错误:根据宏的形式,后面多';',在后面替换时会导致有两个分号。

#include<iostream>usingnamespace std;#defineADD(a,b) a + bintmain(){int ret =ADD(1,2)*3; cout << ret << endl;return0;}

第2种错误:替换后看似正确,期望输出9,但是由于优先级输出7。

在这里插入图片描述
#include<iostream>#defineADD(a, b)(a + b)intmain()

第3种错误(最接近正确):改进了第2种,确保输出9。但在a,b都是表达式时,就又会发生优先级的问题。

#include<iostream>#defineADD(a, b)((a)+(b))

第4种:考虑了参数是表达式的问题,最终正确。

从上面来看,用宏实现简单的加法函数就这么麻烦,要考虑很多,但是人有 存在的意义:将高频调用的小函数写成宏函数,可以提高效率,预处理阶段宏会替换,提高效率,不建立栈帧。
  • inline内联函数代替宏函数:
inlineintAdd(int a,int b){return a + b;}intmain(){int ret =Add(1,2)*3; cout << ret << endl;return0;}

正常定义函数,只需要前面加上关键字inline。使得函数象宏函数一样,不会再创建栈帧。


三、宏:nullptr

nullptr是一个宏,在传统的C头文件stddef.h

#ifndefNULL#ifdef__cpulscpuls#defineNULL0#else#defineNULL((void*)0)#endif#endif
  • C++中NULL可能被定义为字面常量0,或者C中被定义为无类型指针(void*)的常量。不论如何定义,在使用空值的指针时,都会遇到麻烦,本想通过f(NULL)调用指针版本的f(int*)函数,但由于NULL被定义成0,调用了f(int x),因此与程序的初衷相悖。f((void*)NULL);调用会报错。
  • C++11中引入nullptrnullptr是⼀个特殊的关键字,是⼀种特殊类型的字面量,它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。
#include<iostream>usingnamespace std;voidf(int x){ cout <<"f(int x)"<< endl;}voidf(int* ptr){ cout <<"f(int* ptr)"<< endl;}intmain(){f(0);//调用 f(int x)f(NULL);//想调用第2个函数,但是NULL被定义为0/(void*)0,导致调用了第1个函数f((int*)NULL);//调用 f(int* ptr)//f((void*)NULL);//void*不能隐式转换为int*,需要额外的类型转换f(nullptr);//调用 f(int* ptr)return0;}
// 不要这样写int* p1 =NULL;int* p2 =0;//应该这样写int* p3 =nullptr;f(nullptr);// 明确调用指针版本的重载函数

总结

🍓 我是晨非辰Tong!若这篇技术干货帮你打通了学习中的卡点: 👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用 💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑 🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解 技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标! 

结语:

引用解决了指针传参的繁琐与风险,提供了更安全的别名机制;内联函数在编译时权衡空间与时间,取代了宏函数的不可预测性;nullptr则以类型安全的方式终结了空指针的歧义。
它们共同展现了一个理念:在保持C语言效率同时,通过类型系统和语言机制提供更多安全保障。这正是C++能够在系统编程领域保持四十年前沿地位的重要原因。

【源码参考:https://gitee.com/tian-aochen/c-learning-record/tree/master/test_11.29_%E5%85%A5%E9%97%A82/Basics

Read more

C++量子模拟内存管理:90%开发者忽略的5个关键布局技巧

第一章:C++量子模拟内存管理的核心挑战 在C++开发的量子模拟器中,内存管理是决定系统性能与稳定性的关键环节。由于量子态的叠加性和纠缠特性,模拟n个量子比特需要维护一个大小为2^n的复数向量空间,导致内存消耗呈指数级增长。这不仅对堆内存分配策略提出了极高要求,也加剧了缓存局部性、内存泄漏和生命周期控制等问题。 动态内存分配的性能瓶颈 量子态演化过程中频繁调用矩阵运算和张量积操作,通常依赖new和delete进行动态内存管理。然而,频繁的堆操作会引发内存碎片并降低缓存命中率。 // 分配2^n维复数向量表示量子态 std::complex* state = new std::complex[1 << n]; for (int i = 0; i < (1 << n); ++i) { state[i] = (i == 0) ? std::complex(1.0, 0.

By Ne0inhk
C++ 类和对象(二):默认成员函数详解

C++ 类和对象(二):默认成员函数详解

在 C++ 面向对象编程中,类的默认成员函数是非常重要的概念。当我们没有显式实现某些成员函数时,编译器会自动生成它们,这些函数被称为默认成员函数。本文将详细介绍 C++ 类的 6 个默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载以及取地址运算符重载。 一、默认成员函数概述 默认成员函数是指用户没有显式实现,编译器会自动生成的成员函数。一个类在我们不写任何成员函数的情况下,编译器会默认生成以下 6 个默认成员函数:构造函数析构函数拷贝构造函数赋值运算符重载普通取地址运算符重载const 取地址运算符重载         其中前 4 个是我们需要重点掌握的,后两个在大多数情况下使用编译器自动生成的即可。另外,C++11 以后还增加了两个默认成员函数:移动构造和移动赋值,本文暂不讨论。 二、构造函数         构造函数是一种特殊的成员函数,其作用是在对象实例化时初始化对象,替代了我们以前手动调用的Init函数,并且会自动调用。 构造函数的特点:函数名与类名相同无返回值(不需要写void)对象实例化时系统会自动调用对应的构造函数可以重载

By Ne0inhk

跨平台宏定义的陷阱与优化:从C/C++到HarmonyOS的实战解析

跨平台宏定义的陷阱与优化:从C/C++到HarmonyOS的实战解析 1. 跨平台开发的宏定义挑战 在当今多平台并存的开发环境中,C/C++开发者经常需要面对一个核心问题:如何让同一份代码在不同操作系统上正确编译和运行。宏定义作为C/C++预处理器的重要功能,成为解决平台差异的首选工具,但同时也带来了诸多陷阱。 平台识别宏的混乱现状是开发者面临的首要问题。不同操作系统和编译器定义了各自的一套宏,比如: * Windows平台常见的_WIN32和_WIN64 * Linux平台的__linux__ * macOS的__APPLE__和__MACH__ * HarmonyOS的__harmony__ 更复杂的是,这些宏定义之间存在层级关系和互斥性。例如,在64位Windows系统中,_WIN64和_WIN32会同时被定义,而在32位系统中只有_WIN32被定义。这种复杂性容易导致条件编译的逻辑错误。 宏定义的常见陷阱包括: 1. 宏覆盖问题:不同平台的头文件可能定义了相同名称但含义不同的宏 2. 顺序依赖:宏定义的检测顺序可能影响编译结果 3. 未定义行为:忘

By Ne0inhk
CCF-GESP 等级考试 2025年12月认证C++二级真题解析

CCF-GESP 等级考试 2025年12月认证C++二级真题解析

1 单选题(每题 2 分,共 30 分) 第1题 近日,空中客车公司表示,约6000架空客A320系列飞机需要紧急更换一种易受太阳辐射影响的飞行控制软件。空客表示,在对一起飞行事故分析后的结果显示,强烈的太阳辐射可能会损坏飞行控制系统所需的关键数据,导致判断失误,进而引发飞行异常。那这里的飞行控制系统执行判断的部件最可能是下面的(   )。 A. 辐射传感器                  B. 处理器                        C. 内存单元                           D. 输出设备 解析:答案B。飞行控制系统的核心是处理器,它负责接收传感器数据、执行控制算法并输出指令,是判断和决策的关键部件。辐射传感器用于检测辐射,只提供检测数据,不直接参与判断,内存单元用于存储数据不直接参与判断,输出设备用于执行动作更不直接参与判断。因此,处理器最可能是执行判断的部件。故选B。 第2题 小明最近为了备考GESP,开始看B站上关于网络知识的视频。其中提到计算机网络系统有不同的划分标准,那他平时上学所在的教学楼内的网络是一个(   )。 A.

By Ne0inhk