初识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等你来看

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

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

相关文章

记录 AI绘图 Stable Diffusion的本地安装使用,可搭建画图服务端

开头 最近刷短视频看到了很多关于AI绘图&#xff0c;Midjourney&#xff0c;gittimg.ai&#xff0c;Stable Diffusion等一些绘图AI工具&#xff0c;感受到了AI绘画的魅力。通过chatGPT生成关键词再加上绘图工具&#xff0c;真是完美&#xff0c;文末教大家如何用gpt提词 Midj…

浮动布局与定位布局

目录 前言: 浮动布局&#xff08;Float Layout&#xff09;: 定位布局&#xff08;Positioning Layout&#xff09;: 1.传统布局: 1.1文档流布局: 1.1.1基本的布局方式: 1.1.2 块级元素: 1.1.3调整元素: 1.2浮动布局: 1.2.1浮动布局允许元素向左或向右浮动&#xff0c;使…

docker启动卡死问题排查

问题&#xff1a;输入docker ps 或则vession 卡死&#xff0c;无任何输出 排查思路如下&#xff1a; 1、查看docker状态或者日志 journalctl -u docker.service 或者 systemctl status docker 3月 20 18:23:06 dfbpmyy2 dockerd[1114]: time"2024-03-20T18:23:06.7449…

SpringBoot整合Redis:缓存击穿--互斥锁解决

&#x1f389;&#x1f389;欢迎光临&#xff0c;终于等到你啦&#x1f389;&#x1f389; &#x1f3c5;我是苏泽&#xff0c;一位对技术充满热情的探索者和分享者。&#x1f680;&#x1f680; &#x1f31f;持续更新的专栏Redis实战与进阶 本专栏讲解Redis从原理到实践 …

Python算法100例-4.5 最小公倍数

完整源代码项目地址&#xff0c;关注博主私信源代码后可获取 1.问题描述2.问题分析3.算法设计4.确定程序框架5.完整的程序 1&#xff0e;问题描述 求任意两个正整数的最小公倍数&#xff08;Least Common Multiple&#xff0c;LCM&#xff09;。 2&#xff0e;问题分析 如…

开源博客项目Blog .NET Core源码学习(12:App.Application项目结构分析)

开源博客项目Blog的App.Application项目主要定义网站页面使用的数据类&#xff0c;同时定义各类数据的增删改查操作接口和实现类。App.Application项目未安装Nuget包&#xff0c;主要引用App.Core项目的类型。   App.Application项目的顶层文件夹如下图所示&#xff0c;下面逐…

【Java程序设计】【C00352】基于Springboot的疫情隔壁酒店管理系统(有论文)

基于Springboot的疫情隔壁酒店管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 项目获取 &#x1f345;文末点击卡片获取源码&#x1f345; 开发环境 运行环境&#xff1a;推荐jdk1.8&#xff1b; 开发工具&#xff1a;eclipse以…

大学里女生适合做什么副业?这几个小副业最适合女生

大学里女生应该做什么样的副业合适&#xff1f;对于这个问题没办法给大家确定的答案&#xff0c;毕竟每个人能做什么&#xff0c;适合什么&#xff0c;要根据每个人的性格特点特长来看。 但是女生们不管做什么&#xff0c;尽量都不要选择做廉价劳动力的赚钱方式&#xff0c;因为…

STM32串口收发单字节数据原理及程序实现

线路连接&#xff1a; 显示屏的SCA接在B11&#xff0c;SCL接在B10&#xff0c;串口的RX连接A9&#xff0c;TX连接A10。 程序编写&#xff1a; 在上一个博客中实现了串口的发送代码&#xff0c;这里实现串口的接收代码&#xff0c;在上一个代码的基础上增加程序功能。 Seiral.…

【搞不明白】redis和mysql、mybatisplus有啥关系

一个解决三天的bug&#xff0c;到现在也没有搞明白&#xff0c;如题&#xff0c;到底redis和mysql、mybatisplus能有啥关系。三个不相关的嘛 记录下问题&#xff1a; (框架用的Jeecgboot 3.6.0) – 学习使用 看下面的一段代码&#xff0c;是一个分页查询&#xff1a;注意引用的…

mysql数据库表字段使用desc等关键字报错及解决方法

建议尽量避开关键字&#xff01;&#xff01;&#xff01; 如果实在必须用&#xff0c;那参考下面解决办法&#xff1a; 在要用到关键字的地方加反引号标识&#xff1a;

@EnableWebMvc 导致自定义序列化器失效

目录 前言 一. 自定义序列化器失效 1.1 EnableWebMvc 的作用 1.2 EnableWebMvc 带来了什么后果 1.3 原理分析 1.4 问题解决 二. 总结 前言 在使用Swagger的时候用 到了EnableWebMvc&#xff0c;发现之前为了解决Long类型、日期类型等自定义序列化器失效了 Configurati…

JavaScript、ES6与微信小程序:工具箱、升级与新房子

JavaScript、ES6和微信小程序三者之间有什么联系&#xff1f;我想&#xff0c;作为初学者还是有点蒙。下面作一个简单的分析&#xff0c;供大家参考。 首先,我们可以把JavaScript想象成一个非常强大的工具箱,里面装满了各种各样的工具。这些工具可以帮助我们完成各种任务,比如…

vivado 使用远程主机和计算群集

使用远程主机和计算群集 概述 AMD Vivado™集成设计环境&#xff08;IDE&#xff09;支持同时并行合成和实现的执行在多个Linux主机上运行。你可以做到这一点通过配置单个主机或指定要在其上启动作业的命令手动执行现有的计算集群。目前&#xff0c;Linux是Vivado唯一支持远程主…

书生浦语大模型实战营第一课笔记

书生浦语大模型全链路开源体系 课程笔记大模型的发展趋势InternLM2的主要亮点模型到应用的典型流程全链路的开源工具 InternLM2技术报告笔记大型语言模型的发展InternEvoModel Structure训练数据 课程笔记 第一节课主要对大模型进行介绍&#xff0c;特别是书生浦语大模型的发展…

【读书微言】The first summary

系列文章目录 文章目录 系列文章目录前言一、读书微言总结 前言 一、读书微言 强大的内心催生信念我们的生活是否幸福并不是依靠外在的环境&#xff0c;而是依靠我们内在的信念。要想成为自己命运的主宰&#xff0c;我们就必须形成自己的信念。只要我们能坚定自己的内在信念&…

认识线程(Thread)

目录 一、概念 1、 线程是什么 2、为啥要有线程&#xff1f; 3、进程和线程的区别 4、Java 的线程 和 操作系统线程 的关系 二、第⼀个多线程程序 三、创建线程 ⽅法1&#xff1a;继承 Thread 类 ⽅法2&#xff1a;实现 Runnable 接⼝ 对⽐上⾯两种⽅法: 其他变形 四、…

鸿蒙应用开发学习:用Marquee组件做个跑马灯

一、前言 鸿蒙应用的学习持续进行中&#xff0c;这两天阅读官方的API参考文档&#xff0c;发现一个有趣的组件——Marquee&#xff0c;用它做了个跑马灯&#xff0c;做个学习记录。 二、参考资料 官网文档链接如下&#xff1a; https://developer.huawei.com/consumer/cn/d…

【JVM】Java八股文之JVM篇

目录 一、JVM类加载与垃圾回收加载过程加载机制优点图解加载机制 分代回收分代垃圾回收新生代垃圾回收老年代垃圾回收 回收算法 一、JVM类加载与垃圾回收 面试过程中最经典的一题&#xff1a; 请你讲讲在JVM中类的加载过程以及垃圾回收&#xff1f; 加载过程 当Java虚拟机&…

双纤SFP光模块和单纤SFP光模块之间的区别

双纤SFP光模块和单纤SFP光模块是两种不同的光模块类型。对于网络部署而言&#xff0c;了解它们之间的区别至关重要。本文将深入探讨这两种光模块之间的差异&#xff0c;并介绍其特性和适用场景。 双纤与单纤SFP光模块&#xff1a;它们是什么&#xff1f; 双纤SFP光模块是常用…