【c++入门】引用,内联函数,auto

Alt

🔥个人主页:Quitecoder

🔥专栏:c++笔记仓
Alt

朋友们大家好,本节我们来到c++中一个重要的部分:引用

目录

  • 1.引用的基本概念与用法
    • 1.1引用特性
    • 1.2使用场景
    • 1.3传值、传引用效率比较
    • 1.4引用做返回值
    • 1.5引用和指针的对比
  • 2.内联函数
  • 3.auto关键字
  • 4. 基于范围的for循环(C++11)
  • 5.指针空值nullptr(C++11)

1.引用的基本概念与用法

引用是一个重要的概念,它提供了一种方式,通过它可以让两个不同的标识符(变量名、参数名等)引用同一个数据对象

在本质上,引用就像是数据对象的一个别名。使用引用时,对引用的任何操作都会直接反映到被引用的对象上。它允许程序员在不使用指针的情况下通过不同的名称访问同一数据块

void TestRef()
{int a = 10;int& b = a;cout << &a << endl;cout << &b << endl;return 0;
}

在变量类型前使用&来声明一个引用类型

在这个示例中,b是a的引用int& b = a;,我们对b修改,a的值也会改变,当打印a和b的地址时,会看到它们的地址是相同的
在这里插入图片描述
在这里插入图片描述
b就是a的别名

1.1引用特性

引用必须被初始化

在C++中,声明引用时必须同时进行初始化。这表明,引用一旦被创建,就必须立即指向一个已存在的变量。你不能像指针那样先声明一个引用,然后再让它指向一个变量

int x = 5;
int &b = x; // 正确,b被初始化为x的引用
int &c; // 错误,引用必须在声明时被初始化

引用本质上是所引用变量的别名

一旦引用被初始化为某个变量的引用,它就永远引用那个变量,不会像指针那样可以改变所指向的变量。这意味着通过引用对数据的任何操作都是直接作用于它所引用的那个变量上

int a =0;
int &b=a;
int c=2;
b=c;

b引用了a,则不会再改变,这里b=c则就是把c的值赋值给b
在这里插入图片描述
我们可以看到,a和b的地址是相同的

一个变量可以有多个引用

int a = 0;
int& b = a;
int& c = a;
int& d = a;

1.2使用场景

做参数

如果我们想用一个函数来实现两个数的交换,用我们学过的知识,这里我们使用指针:

void Swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}
int main()
{int x = 10;int y = 20;Swap(&x, &y);return 0;
}

如果我们不传地址,那么a就是x的临时拷贝,a的改变不会影响x的值

下面是引用的做法

void Swap(int& a, int& b)
{int tmp = a;a = b;b = tmp;
}
int main()
{int x = 10;int y = 20;Swap(x, y);return 0;
}

这里a就是x的别名,b就是y的别名,对ab进行修改同时就对xy进行修改

在后面我们会讲到这个部分的底层逻辑

这个版本的 Swap 函数展示了C++引用的强大用处和简洁语法。通过引用参数,可以直接修改传入的变量,而无需担心指针解引用和地址操作,这使得代码更加安全、清晰

但是由于引用定义后不能改变指向,引用不能替代指针

当然,这里swap函数取名字也可以取x,y,因为他们在不同作用域,对结果没有什么影响

void Swap(int& x, int& y)
{int tmp = x;x = y;y = tmp;
}

1.3传值、传引用效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低

#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void main()
{A a;// 以值作为函数参数size_t begin1 = clock();for (size_t i = 0; i < 10000; ++i)TestFunc1(a);size_t end1 = clock();// 以引用作为函数参数size_t begin2 = clock();for (size_t i = 0; i < 10000; ++i)TestFunc2(a);size_t end2 = clock();// 分别计算两个函数运行结束后的时间cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}

按值传递 (TestFunc1(A a))
当你以值传递一个对象给函数时,该函数获得的是原对象的一个完全独立的拷贝。这意味着函数中对该参数的任何修改都不会反映到原对象上。在这个具体的例子中,当TestFunc1被调用,结构体A中包含的数组将会被整个复制给函数内的一个新的局部变量a
对于大的结构体(如本例中定义的struct A { int a[10000]; };),这个拷贝操作将会非常昂贵,从时间和处理器资源的角度考虑都是如此。每次函数调用都会触发一个大数组的拷贝过程,这可能导致显著的性能下降

按引用传递 (TestFunc2(A& a))
与按值传递不同,按引用传递对象意味着函数接收的是原对象的一个引用(或者说是原对象的一个别名)。这意味着函数中对参数的任何修改都将反映到传入的原始对象上。关键点在于没有产生任何拷贝,函数直接在原对象上工作
在本例中,当TestFunc2被调用,并且以A&(结构体A的引用)作为参数时,它实际上是直接操作原有的对象a,而不是创建一个新的拷贝。这样就避免了昂贵的拷贝操作,极大地提高了效率

在上述代码示例中,TestFunc1(按值传递)会因为每次调用时都需要复制一个大数组而显得非常慢,而TestFunc2(按引用传递)则会因为避免了这种拷贝,运行时间将大大减少

在这里插入图片描述

1.4引用做返回值

首先我们来看这串代码

int func()
{int a = 0;return a;
}
int main()
{int ret = func();return 0;
}

在函数 func 中定义的变量 a 是一个局部变量它的生命周期仅限于函数 func 的执行期间。一旦 func 执行完毕,a所占用的内存就会被释放掉,该内存区域可以被其他函数或变量复用。这意味着,在函数 func 外部,我们无法安全地访问变量 a

当函数被调用时,一个栈帧(stack frame)就会被分配给这个调用。栈帧是存储函数局部变量、参数和其他信息(如返回地址)的内存块。对于 func 函数,它的栈帧将包含局部变量 a 的存储空间

值返回的基本原理
当 func 函数通过 return a; 返回 a 的值时,实际上返回的是 a 值的一个副本,而不是 a 自身。这个返回值副本通常是通过寄存器传递给函数的调用者,在 main 函数中, int ret = func(); 一句捕获了 func 返回的 a 的副本,并将其存储在 main 的局部变量 ret 中。值得注意的是,此时的 ret 和 func 中的 a 互不干扰,ret 拥有 a 的一个独立副本

再看这串代码:

int& func()
{int a = 0;return a;
}
int main()
{int ret = func();return 0;
}

这里func返回的是a的别名,但这里有很大问题

问题解释

func 函数中,a 是个局部变量,它的生命周期仅限于函数 func 的执行期间。当 func 函数执行完毕后,局部变量 a 的存储空间将被释放,此时返回给调用者的引用将指向一个已经被销毁的对象。尽管 main 函数中用 int ret = func(); 接收的是引用的返回值的拷贝,从而避免直接持有悬空引用,但 func 函数的设计本身是有问题的,因为它返回了对局部变量的引用

返回局部变量的引用导致了未定义行为,因为一旦 func 函数返回,a 的生命周期结束,其所占用的内存可能会被其他数据覆盖,或者其所在的栈帧空间可能被后续的函数调用复用。在这种情况下,通过悬空引用访问这块内存是非法的,这可能导致程序崩溃

替代方案

  • 返回静态局部变量的引用:静态局部变量的生命周期持续到程序结束,因此返回其引用是安全的
   int& func() {static int a = 0;return a;}
  • 使用动态内存分配:在一些必须返回复杂数据结构而又不希望拷贝它们的情况下,可以动态分配内存(例如,使用new),然后返回指向它的指针

总结:若返回变量出了函数作用域生命周期结束,不能用引用返回

1.5引用和指针的对比

语法层面

  1. 引用是别名,不开空间;指针是地址,需要开空间存地址
    在底层实现上实际是有空间的,因为引用是按照指针方式来实现的
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}

在这里插入图片描述
引用底层是用指针实现的

  1. 引用必须初始化,指针可以不初始化
  2. 引用不能改变指向,指针可以
  3. 引用相对更安全,没有空引用,但是有空指针
  4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  6. 有多级指针,但是没有多级引用

底层层面

在汇编层面,没有引用,都是指针,引用编译后也转换成指针了

2.内联函数

内联函数旨在减少函数调用的开销,通过在每个调用点将函数体展开来达到这一目的。这种方法适用于那些函数体较小、调用频繁的函数

比如,我要调用一万次Add函数:

int Add(int x,int y)
{return x+y;
}

如果我要调用一万次,意味着要建立一万个栈帧,消耗比较大

在c语言中,我们可以用来解决

#define Add(x,y) ((x)+(y)) 

在c++中,可以通过在函数声明前添加关键字inline来指示编译器将一个函数视为内联函数

inline int Add(int x,int y)
{return x+y;
}

当编译器处理到函数调用时,如果该函数被声明为内联,则编译器会尝试将该函数调用替换为函数体本身的代码。这样,当程序运行到那一点时,不再需要跳转到函数然后返回,而是直接执行了函数的代码。这样做的好处是减少了函数调用的消耗

#include <iostream>
using namespace std;
inline int Add(int x, int y) {return x + y;
}int main() {int result = Add(5, 3);cout << result << endl;return 0;
}

在这个简单的例子中,由于Add函数被声明为内联,编译器可能会将main函数中的Add(5, 3)调用直接替换为5 + 3,从而避免了函数调用的开销

在这里插入图片描述
在这里插入图片描述

  1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率
  2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性
  3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到

3.auto关键字

auto 关键字是 C++11 中引入的一个特性,它让编译器能够自动推导变量的类型。使用 auto 可以使代码更加简洁易读,特别是当处理复杂的类型

   int a = 0;int b = a;auto c = a;auto p = &a;auto *p = &a;auto& r = a;

它的推导是十分灵活的

用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

比如我们想创建一个函数指针:

void func(int a, int b)
{}
int main()
{void(*pf1)(int,int)=func;auto pf2=func;return 0;
}

在这里插入图片描述
其类型是相同的

使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量

void TestAuto()
{auto a = 1, b = 2; auto c = 3, d = 4.0;  // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

auto不能推导的场景

  1. auto不能作为函数的参数
void TestAuto(auto a)
{}

在这里插入图片描述

  1. auto不能直接用来声明数组

4. 基于范围的for循环(C++11)

在C++98中如果要遍历一个数组,可以按照以下方式进行:

void TestFor()
{
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;
}

C++11 引入了范围 for 循环(也称为基于范围的 for 循环),它使得遍历容器(例如数组、向量等)变得更加简单和直观。这个特性大大简化了对容器元素的访问和迭代操作

范围 for 循环的语法如下:

for (declaration : range) {// 循环体
}
  • declaration:声明一个变量,这个变量的类型应该与 range 中的元素类型相兼容。这个变量将在每次循环迭代时被初始化为序列中的当前元素。这里可以使用 auto 使编译器自动推断元素类型。
  • range:是您要遍历的序列或容器,可以是数组、向量、列表等。

示例

遍历数组:

int arr[] = {1, 2, 3, 4, 5};
for (int elem : arr) {std::cout << elem << ' ';
}

输出:

1 2 3 4 5

使用 auto 关键字:

int arr[] = { 1,2,3,4,5 };for (auto e : arr)
{std::cout << e << " ";
}

输出:

1 2 3 4 5

按引用遍历以修改元素

int array[] = { 1, 2, 3, 4, 5 };
for (auto& e : array)e *= 2;
for (auto e : array)cout << e << " ";

输出

2 4 6 8 10

范围for的使用条件

  1. for循环迭代的范围必须是确定的
    对于数组而言,就是数组中第一个元素和最后一个元素的范围

以下代码就有问题,因为for的范围不确定:

void TestFor(int array[])
{for(auto& e : array)cout<< e <<endl;
}

5.指针空值nullptr(C++11)

在 C++ 中,nullptr 是一个字面量,用于表示空指针。它在 C++11 标准中引入,用以替代 C 语言时代的 NULL 宏和 C++ 中的 0(零),以明确表示空指针的意图。这样的引入解决了旧有方法的一些类型安全性问题和模糊性,增强了代码的可读性和可维护性

如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化

int* p1 = NULL;
int* p2 = 0;

NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码

#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif

可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量,我们可能遇到下面的麻烦:

void f(int)
{cout<<"f(int)"<<endl;
}
void f(int*)
{cout<<"f(int*)"<<endl;
}
int main()
{f(0);f(NULL);f((int*)NULL);return 0;
}

在这里插入图片描述
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,与预期违背

nullptr 的类型是 nullptr_t,可以自动转换到任何其他指针类型,但不可以不经转换直接用于整数类型,这解决了原来使用 NULL 或 0 可能引起的一些类型混淆或过载解析问题nullptr 可用于任何需要空指针的地方,与所有指针类型兼容,包括 C++ 基本类型指针、对象指针、函数指针以及成员函数指针

在这里插入图片描述
由于 nullptr 有自己的类型 nullptr_t,所以它可以被用于函数重载的场景,这在使用 NULL(通常被定义为 0 或 ((void)0))时无法实现*

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

感谢阅读!!!

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

企业微信可以更换公司主体吗?

企业微信变更主体有什么作用&#xff1f;当我们的企业因为各种原因需要注销或已经注销&#xff0c;或者运营变更等情况&#xff0c;企业微信无法继续使用原主体继续使用时&#xff0c;可以申请企业主体变更&#xff0c;变更为新的主体。企业微信变更主体的条件有哪些&#xff1…

搭建基于 Snowflake 的 CI/CD 最佳实践!

Snowflake 提供了可扩展的计算和存储资源&#xff0c;和基于 SQL 的界面 Snowsight&#xff0c;方便用户进行数据操作和分析。然而&#xff0c;如果用户想将自己的 CI/CD 流程与 Snowflake 集成时&#xff0c;会发现一些不便之处&#xff08;尤其相比其 SnowSight 优秀的查询能…

x-zse-96安卓端纯算,魔改AES还原

两天前发了一个x-zse-96的文章,当时遇到了点问题,只分析到了最后一个白盒AES函数里面,并且当时用dfa攻击还原出了秘钥,IV也确定了,但是加密结果不对,本来打算把下文鸽掉的,因为当时unidbg没跑起来,用frida去hook白盒AES中的每一行汇编有点麻烦,没有unidbg方便.后来小白大佬说un…

初识C++(一)

目录 一、什么是C 二、关键字&#xff1a; 三、命名空间 &#xff1a; 1. C语言存在的问题&#xff1a; 2. namespace关键字&#xff1a; 3. 注意点&#xff1a; 4.使用命名空间分为三种&#xff1a; 四、输入输出&#xff1a; 五、缺省函数&#xff1a; 1. 什么是缺省…

ARM实验 LED流水灯

.text .global _start _start: 使能GPIOE GPIOF的外设时钟 RCC_MP_AHB4ENSETR的第[4][5]设置为1即可使能GPIOE GPIOF时钟 LDR R0,0X50000A28 指定寄存器地址 LDR R1,[R0] 将寄存器原来的数值读取出来&#xff0c;保存到R1中 ORR R1,R1,#(0x3<<4) 将第4位设置为1 S…

【STM32嵌入式系统设计与开发】——6矩阵按键应用(4x4)

这里写目录标题 一、任务描述二、任务实施1、SingleKey工程文件夹创建2、函数编辑&#xff08;1&#xff09;主函数编辑&#xff08;2&#xff09;LED IO初始化函数(LED_Init())&#xff08;3&#xff09;开发板矩阵键盘IO初始化&#xff08;ExpKeyBordInit()&#xff09;&…

如何使用 Elasticsearch 作为向量数据库

在今天的文章中&#xff0c;我们将很快地通过 Docker 来快速地设置 Elasticsearch 及 Kibana&#xff0c;并设置 Elasticsearch 为向量搜索。 拉取 Docker 镜像 docker pull docker.elastic.co/elasticsearch/elasticsearch:8.12.2 docker pull docker.elastic.co/kibana/kiba…

【Bug】记录2024年遇到的Bug以及修复方案

--------------------------------------------------------分割线 2024.3.22------------------------------------------------------- 1、load_sample_image raise AttributeError(“Cannot find sample image: %s” % image_name) AttributeError: Cannot find sample ima…

【wubuntu】披着Win11皮肤主题的Ubuntu系统

wubuntu - 一款外观类似于 Windows 的 Linux 操作系统&#xff0c;没有任何硬件限制。以下是官方的描述 Wubuntu is an operating system based on Ubuntu LTS that has a similar appearance to Windows using the open-source themes. Wubuntu also comes with a set of adva…

计算机网络——数据链路层(数据链路层功能概述)

计算机网络——数据链路层&#xff08;数据链路层功能概述&#xff09; 数据链路层的功能数据链路层的基本概念封装成帧和透明传输 我们之前已经学完了物理层的所有内容&#xff0c;今天开始我们要进入数据链路层的学习&#xff0c;如果有小伙伴对物理层的内容感兴趣的话&#…

MySQL数据库:索引

一、索引&#xff1a; 1. 索引的概念&#xff1a; 索引是一个排序的列表&#xff0c;在这个列表中存储着索引的值和包含这个值的数据所在行的物理地址。 使用索引后可以不用扫描全表来定位某行的数据&#xff0c;而是先通过索引表找到该行数据对应的物理地址然后访问相应的数据…

Kubernetes(k8s)集群健康检查常用的五种指标

文章目录 1、节点健康指标2、Pod健康指标3、服务健康指标4、网络健康指标5、存储健康指标 1、节点健康指标 节点状态&#xff1a;检查节点是否处于Ready状态&#xff0c;以及是否存在任何异常状态。 资源利用率&#xff1a;监控节点的CPU、内存、磁盘等资源的使用情况&#xf…

vue 安装脚手架报错 certificate has expired

vue 安装脚手架的时候报错&#xff0c;报错信息如下&#xff1a; 错误信息&#xff1a;npm ERR! request to https://registry.npm.taobao.org/vue%2fcli failed, reason: certificate has expired 翻译&#xff1a;npm ERR&#xff01;请求到https://registry.npm.taobao.org…

上位机图像处理和嵌入式模块部署(qmacvisual图像预处理)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 不管大家是在读书的时候学习的图像处理&#xff0c;还是在后来的工作中&#xff0c;重新学习了图像处理&#xff0c;相信大家对图像预处理的概念并…

【Linux实践室】Linux用户管理实战指南:新建与删除用户操作详解

&#x1f308;个人主页&#xff1a;聆风吟_ &#x1f525;系列专栏&#xff1a;Linux实践室、网络奇遇记 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 一. ⛳️任务描述二. ⛳️相关知识2.1 &#x1f514;Linux创建用户命令2.1.1 知识点讲解2.1.2 案…

1Panel应用推荐:DataEase开源数据可视化分析工具

1Panel&#xff08;github.com/1Panel-dev/1Panel&#xff09;是一款现代化、开源的Linux服务器运维管理面板&#xff0c;它致力于通过开源的方式&#xff0c;帮助用户简化建站与运维管理流程。为了方便广大用户快捷安装部署相关软件应用&#xff0c;1Panel特别开通应用商店&am…

html第一次作业

常用标签 0, 骨架&#xff08;&#xff01;tap&#xff09; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><t…

mysql字段多个值,mybatis/mybatis-plus匹配查询

mysql中有一个字段是字符串类型的&#xff0c;category字段值有多个用逗号分割的&#xff0c;例如&#xff1a;娱乐,时尚美妆,美食 。现在想实现这么一个功能&#xff0c; 前端传参 字符串&#xff0c;美食,娱乐。现在想在mybatis的xml中实现&#xff0c;查询&#xff0c;能查到…

vue3+element Plus form 作为子组件,从父组件如何赋值?

刚开始接触vue3时&#xff0c;碰到一个很low的问题&#xff0c;将form作为子组件&#xff0c;在页面中给form表单项输入内容&#xff0c;输入框不显示值&#xff0c;知道问题出在哪&#xff0c;但因为vue3组合式api不熟悉&#xff0c;不知从哪下手... 效果图&#xff1a; 父组…

华为中心AP 配置入侵防御实验

配置入侵防御示例 组网图形 图1 入侵防御组网图 组网需求配置思路操作步骤中心AP的配置文件 组网需求 如图1所示&#xff0c;某企业部署了WLAN网络&#xff0c;内网用户可以访问Internet的Web服务器。现需要在中心AP上配置入侵防御功能&#xff0c;具体要求如下&#xff1a; 保…