【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

感谢阅读!!!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

手撕算法-买卖股票的最佳时机(买卖一次)

描述 分析 只能买卖一次。希望在最低处买&#xff0c;最高处卖。 怎么判断最低处&#xff1f;遍历时存储已遍历的最小值。 怎么判断最高处&#xff1f;遍历时&#xff0c;比较当前位置和最小值的差&#xff0c;取较大的。 代码 class Solution {public int maxProfit(int…

Anaconda安装教程

简介 Anaconda是一个开源的Python发行版&#xff0c;专注于科学计算领域。它支持Linux&#xff0c;Mac&#xff0c;Windows系统&#xff0c;并提供了包管理与环境管理的功能。Anaconda利用工具conda来进行package和environment的管理&#xff0c;并且已经包含了Python和相关的…

SpringCloud从入门到精通速成(二)

文章目录 1.Nacos配置管理1.1.统一配置管理1.1.1.在nacos中添加配置文件1.1.2.从微服务拉取配置 1.2.配置热更新1.2.1.方式一1.2.2.方式二 1.3.配置共享1&#xff09;添加一个环境共享配置2&#xff09;在user-service中读取共享配置3&#xff09;运行两个UserApplication&…

若依用户信息数据导入时自定义密码

若依导入功能: 在使用若依脚手架时,用户信息管理是非常必要的一个部分,而面对大量数据时,使用excel批量导入数据可大大提高效率。若依脚手架也是提供了导入功能,如下图所示: 问题描述 虽然若依脚手架提供了批量导入功能,但其导入的密码总是123456,不仅不安全,而且在…

【Python + Django】静态文件的添加

前言&#xff1a; 前一篇文章我们已经学会了怎么用django写文本页面啦&#xff01;&#xff01;&#xff01; 有一说一&#xff0c;这个静态页面是真的丑。 我们总得用一些花花绿绿的东西把这个丑陋的网站给装饰一下吧&#xff01;&#xff01;&#xff01;&#xff01;&…

手撕算法-接雨水

描述 分析 i位置能积累的雨水量&#xff0c;等于其左右两边最大高度的最小值。为了能获取i位置左右两边的最大高度。使用动态规划。两个dp数组&#xff1a; leftMaxrightMax 其中 leftMax[i] 代表i位置左边的最大高度rightMax[i] 代表i位置右边的最大高度 初始状态&#x…

BEVFormer v2论文阅读

摘要 本文工作 提出了一种具有透视监督&#xff08;perspective supervision&#xff09;的新型鸟瞰(BEV)检测器&#xff0c;该检测器收敛速度更快&#xff0c;更适合现代图像骨干。现有的最先进的BEV检测器通常与VovNet等特定深度预训练的主干相连&#xff0c;阻碍了蓬勃发展…

SpringBoot整合ShardingSphere-JDBC 5.3.2 实现读写分离、分库分表。

&#x1f469;&#x1f3fd;‍&#x1f4bb;个人主页&#xff1a;阿木木AEcru &#x1f525; 系列专栏&#xff1a;《Docker容器化部署系列》 《Java每日面筋》 &#x1f4b9;每一次技术突破&#xff0c;都是对自我能力的挑战和超越。 Docker部署MYSQL主从详细教程-阿木木AEcru…

计算机网络:物理层下的传输媒体概览

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

秘钥缩写、全称和中文名

三级加密体系 第一级&#xff1a; LMK(LOCAL MAIN KEY)&#xff0c;存放于HSM机中&#xff0c;用于对所有存于本地的其他密钥和加密数据进行加密&#xff0c;是最重要的密钥。 第二级&#xff1a; 如ZMK&#xff08;即平时大家说的主密钥MK&#xff09;&#xff0c;存于本地或…

autorun 病毒清除工具 源码

** autorun 病毒清除工具 源码 ** 1、新建一个记事本&#xff1a;AutoRun病毒清理工具.txt&#xff0c;复制以下代码&#xff1a; Autorun 病毒清除工具 Echo Offcolor 2etitle Autorun 病毒清除工具-By 段子手168 2023-10-25Rem 杀进程taskkill /F /IM SocksA.exe /IM …

hyper-v虚拟机使用宿主机usb设备

文章目录 一、修改宿主机组策略二、使用 一、修改宿主机组策略 在宿主电脑上&#xff0c;按 winr 组合键打开运行窗口&#xff0c;输入 gpedit.msc 打开组策略编辑器&#xff0c;依次点击计算机配置- 管理模板- Windows 组件- 远程桌面服务- 远程桌面会话客户端- RemoteFX USB…

目标检测——PP-YOLOE算法解读

PP-YOLO系列&#xff0c;均是基于百度自研PaddlePaddle深度学习框架发布的算法&#xff0c;2020年基于YOLOv3改进发布PP-YOLO&#xff0c;2021年发布PP-YOLOv2和移动端检测算法PP-PicoDet&#xff0c;2022年发布PP-YOLOE和PP-YOLOE-R。由于均是一个系列&#xff0c;所以放一起解…

一键入门Ubuntu22!

目录 一、安装 二、常用目录 三、常用指令 四、用户指令 五、ssh与scp 六、服务相关 七、Python与Pycharm 八、Vim编辑器 九、Ubuntu22下使用Mysql 十、Ubuntu22下使用mongodb 十一、Ubuntu22下使用redis Ubuntu是一个基于Debian的开源操作系统&#xff0c;由Canoni…

基于霍夫检测(hough变换)的人眼瞳孔定位,Matlab实现

博主简介&#xff1a; 专注、专一于Matlab图像处理学习、交流&#xff0c;matlab图像代码代做/项目合作可以联系&#xff08;QQ:3249726188&#xff09; 个人主页&#xff1a;Matlab_ImagePro-CSDN博客 原则&#xff1a;代码均由本人编写完成&#xff0c;非中介&#xff0c;提供…

网络原理(4)——TCP协议的特性

目录 一、滑动窗口 1、ack丢了 2、数据丢了 二、流量控制&#xff08;流控&#xff09; 三、拥塞控制 拥塞窗口动态变化的规则 四、延时应答 五、捎带应答 六、面向字节流 七、异常情况 &#xff08;1&#xff09;进程崩溃了 &#xff08;2&#xff09;其中一方关机…

虚拟机开机启动失败,进入(initramfs)解决办法

虚拟机开机启动失败&#xff0c;进入&#xff08;initramfs&#xff09;解决办法 打开虚拟机中Ubuntu时进入(initramfs)&#xff0c;导致无法进入桌面。问题如下图显示&#xff1a; 命令行输入 fsck -y /dev/sda5输入 exit进入 选择root 后回车 输入虚拟机的密码 进入 root …

网络原理(5)——IP协议(网络层)

目录 一、IP协议报头介绍 1、4位版本 2、4位首部长度 3、8位服务器类型 4、16位总长度 5、16位标识位 6、3位标志位 7、13位偏移量 8、8位生存空间 9、8位协议 10、16位首部检验和 11、32位源IP地址 12、32位目的IP地址 二、IP协议如何管理地址&#xff1f; 1、动…

《剑指 Offer》专项突破版 - 面试题 88 : 动态规划的基础知识(C++ 实现)

目录 前言 面试题 88 : 爬楼梯的最少成本 一、分析确定状态转移方程 二、递归代码 三、使用缓存的递归代码 四、空间复杂度为 O(n) 的迭代代码 五、空间复杂度为 O(1) 的迭代代码 前言 动态规划是目前算法面试中的热门话题&#xff0c;应聘者经常在各大公司的面试中遇到…