初识C++(二)引用,内联函数,auto

目录

1.引用的概念与用法:

1.1引用特性:

1.2使用场景

    1.2.1做参数

1.3传值、传引用效率比较

1.4引用做返回值

1.5引用和指针的对比

2.内联函数

3.auto关键字

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

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


1.引用的概念与用法:

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

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

        当定义引用时,编译器会将引用和它所引用的变量绑定在一起,因此,引用并不需要额外的存储空间来存储数据,而是直接使用被引用变量的存储空间。

void Test()
{int a = 10;int& b = a;cout << &a << endl;cout << &b << endl;
}


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

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

1.1引用特性:

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

必须初始化的原因:当定义引用时,编译器会将引用和它所引用的变量绑定在一起,因此,引用并不需要额外的存储空间来存储数据,而是直接使用被引用变量的存储空间。

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

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

void Test()
{int a = 0;int& b = a;cout << a << endl;cout << b << endl;cout << &a << endl;cout << &b << endl;int c = 2;b = c;cout << b << endl;cout << &b << endl;cout << &c << endl;
}

b引用了a,则不会再改变,这里b=c则就是把c的值赋值给b

我们可以看到,a和b的地址是相同的

一个变量可以有多个引用

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

多个变量指向同一块空间。

1.2使用场景

    1.2.1做参数

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

void Swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}

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

        下面是引用的做法:

void Swap(int& a, int& b)
{int tmp = a;a = b;b = tmp;
}

这里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传值、传引用效率比较

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

会先在main栈帧/寄存器中存放临时变量,保存n值的一份临时拷贝,然后Count函数销毁,再把临时变量的值给ret

#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),然后返回指向它的指针
总结:若返回变量出了函数作用域生命周期结束,不能用引用返回

一般可以用引用做返回值的是:静态的,全局的,malloc等等

1.5引用和指针的对比

语法层面:

引用是别名,不开空间;指针是地址,需要开空间存地址
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的

int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}

引用底层是用指针实现的

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

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

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,从而避免了函数调用的开销

inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率
inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性
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)
{}

2) 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的使用条件:

for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围
以下代码就有问题,因为for的范围不确定:(array的形参只是一个指针,不是完整的数组)

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

可以看到,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

感谢阅读!!!


我的第二篇初始C++:http://t.csdnimg.cn/DMhzX

第三篇:初识C++(三)构造函数和析构函数http://t.csdnimg.cn/0C7oL等你来看

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

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

相关文章

腾讯云4核8G服务器支持多少人同时在线?

腾讯云4核8G服务器价格&#xff1a;轻量4核8G12M优惠价格646元15个月、CVM S5服务器4核8G配置1437元买1年送3个月。腾讯云4核8G服务器支持多少人同时在线&#xff1f;支持30个并发数&#xff0c;可容纳日均1万IP人数访问。腾讯云百科txybk.com整理4核8G服务器支持多少人同时在线…

独立看门狗

什么是独立看门狗&#xff1f;它有什么用&#xff1f;什么时候用&#xff1f;不用行不行&#xff1f; 独立看门狗&#xff08;Independent Watchdog&#xff0c;简称IWDG&#xff09;是一种微控制器中常见的硬件保护机制。它的主要作用是监视系统的运行状态&#xff0c;当系统出…

2024 年 15 个最佳自动化 UI 测试工具【建议收藏】

Web 开发行业正在不断发展&#xff0c;许多最佳自动化 UI 测试工具可用于测试基于 Web 的项目&#xff0c;以确保它没有错误并且每个用户都可以轻松访问。这些工具可帮助您测试您的 Web 项目并使其完全兼容用户端的要求和需求。 UI 自动化测试工具可测试基于 Web 的应用程序或软…

JRT菜单

上一章搭建了登录界面的雏形和抽取了登录接口。给多组使用登录和菜单功能提供预留&#xff0c;做到不强行入侵别人业务。任何产品只需要按自己表实现登录接口后配置到容器即可共用登录界面和菜单部分。最后自己的用户关联到JRT角色表即可。 登录效果 这次构建菜单体系 首先用…

Transformer的前世今生 day09(Transformer的框架概述)

前情提要 编码器-解码器结构 如果将一个模型分为两块&#xff1a;编码器和解码器那么编码器-解码器结构为&#xff1a;编码器负责处理输入&#xff0c;解码器负责生成输出流程&#xff1a;我们先将输入送入编码器层&#xff0c;得到一个中间状态state&#xff0c;并送入解码器…

号码采集协议讲解

仅供学习研究交流使用 需要的进去拿源码或者成品

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

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 qmacvisual本身提供了图像拼接的功能。功能本身比较有意思的。大家如果拍过毕业照&#xff0c;特别是那种几百人、上千人的合照&#xff0c;应该就…

Rust编程(二)语法和数据类型

编程规范 类C语法&#xff0c;函数需要定义&#xff0c;指令需要以&#xff1b;结尾。需要大括号{} 文件名&#xff0c;变量&#xff0c;函数命名使用snake case&#xff0c;eg&#xff1a;new_function() 结构体&#xff0c;特征命名&#xff0c;使用大驼峰命名&#xff0c;e…

微信小程序商城构建全栈应用

今天&#xff0c;将之前大学朋友分享给我的好几个小程序内容&#xff0c;简单的从百度网盘下载了一下&#xff0c;重新回顾小程序内容&#xff0c;重新构建融合一些不同语言的元素。下面是网盘的简单截图。 我先挑选了一个微信小程序商城项目&#xff0c;简单看了看&#xff0…

中霖教育:不是会计专业能参加24年的中级会计师考试吗?

经常有很多同学问&#xff1a;我不是会计专业的能报中级会计师考试吗? 先来看报名需要满足的必要条件&#xff1a; 1、大学专科学历&#xff0c;从事会计工作满5年。 2、大学本科学历或学士学位&#xff0c;从事会计工作满4年。 3、第二学士学位或研究生班毕业&#xff0c…

Rust编程(三)生命周期与异常处理

生命周期 生命周期&#xff0c;简而言之就是引用的有效作用域。在大多数时候&#xff0c;我们无需手动的声明生命周期&#xff0c;因为编译器可以自动进行推导。生命周期的主要作用是避免悬垂引用&#xff0c;它会导致程序引用了本不该引用的数据&#xff1a; {let r;{let x …

Partisia Blockchain:真正做到兼顾隐私、高性能和可拓展的公链

目前&#xff0c;包括 Secret Network、Oasis Protocol 等在内的绝大多数以隐私为特性的可编程公链&#xff0c;在兼顾隐私的同时&#xff0c;在可拓展以及性能上或多或少的有所牺牲&#xff0c;即难以对诸多实际应用场景进行支撑。这归咎于链的设计以及共识机制的不合理&#…

袁志佳:前端全栈工程师精英班

教程介绍 本套课程涵盖大前端的全部领域&#xff0c;并对传统的Web前端全栈深入教学。如利用前端知识开发 AI、VR、AR、iOS、Android、PC、Server、智能硬件。 同时我们将核心打造 JavaScript语言新发展、Vue源码分析、前端持续集成方案汇总、MV*框架深度分析 、前端图形学、N…

Python爬虫如何快速入门

写了几篇网络爬虫的博文后&#xff0c;有网友留言问Python爬虫如何入门&#xff1f;今天就来了解一下什么是爬虫&#xff0c;如何快速的上手Python爬虫。 一、什么是网络爬虫 网络爬虫&#xff0c;英文名称为Web Crawler或Spider&#xff0c;是一种通过程序在互联网上自动获取…

目前2024年腾讯云4核8G服务器租用优惠价格表

2024年腾讯云4核8G服务器租用优惠价格&#xff1a;轻量应用服务器4核8G12M带宽646元15个月&#xff0c;CVM云服务器S5实例优惠价格1437.24元买一年送3个月&#xff0c;腾讯云4核8G服务器活动页面 txybk.com/go/txy 活动链接打开如下图&#xff1a; 腾讯云4核8G服务器优惠价格 轻…

虚拟机如何在原有磁盘上扩容

虚拟机未开启状态–菜单栏–虚拟机–快照–拍摄快照–拍摄快照– 菜单栏–虚拟机–快照–快照管理器–点击刚刚的快照1–删除–是– 文件–新建或者打开–硬盘&#xff08;以本人Win 10.64.3GL为例&#xff09;–虚拟机设置–硬件– 硬盘&#xff08;SATA&#xff09;–磁盘实…

Git基础(24):分支回退

文章目录 前言放弃已修改的内容分支回退到指定commit 前言 将分支回退到之前的某个版本 开发中&#xff0c;可能开发某个功能不需要了&#xff0c;或者想要回退到之前历史的某个commit&#xff0c; 放弃后来修改的内容。 放弃已修改的内容 如果未提交&#xff0c;直接使用 …

二分算法的变种----查找递增可重复数组边界

public class test {//数组长度a,b,c为8,d为7&#xff1b;static int[] a {3,5,8,8,8,9,9,10};static int[] b {8,8,8,8,8,8,8,8};static int[] c {0,0,0,0,0,0,0,0};static int[] d {0,0,0,0,0,0,0};public static void main(String[] args) {int target 0;System.out.pr…

Windows下pycharm配置QtDesigner、PyUic、PyRcc

前言&#xff1a; 在配置QtDesigner、PyUic、PyRcc之前&#xff0c;咱们先了解一下这三个工具在PyQt5里面的作用 Qt Designer、PyUIC、PyRCC 在 PyQt5 开发中扮演着重要的角色&#xff0c;它们分别用于设计界面、将设计的界面转换为 Python 代码以及管理资源文件。下面将详细解…

Python性能测试框架Locust实战教程

01、认识Locust Locust是一个比较容易上手的分布式用户负载测试工具。它旨在对网站&#xff08;或其他系统&#xff09;进行负载测试&#xff0c;并确定系统可以处理多少个并发用户&#xff0c;Locust 在英文中是 蝗虫 的意思&#xff1a;作者的想法是在测试期间&#xff0c;放…