从C语言到C++②(第一章_C++入门_中篇)缺省参数+函数重载+引用(下)

简介: 从C语言到C++②(第一章_C++入门_中篇)缺省参数+函数重载+引用

从C语言到C++②(第一章_C++入门_中篇)缺省参数+函数重载+引用(中):https://developer.aliyun.com/article/1513636

3.5 引用做传值返回

我们已经知道,普通的传值返回会生成一个临时变量了。

我们来试试引用的返回。

引用返回的意思就是,不会生成临时变量,直接返回 c 的别名。

这段代码存在的问题:


① 存在非法访问,因为 Add(1, 2) 的返回值是 c 的引用,所以 Add 栈帧销毁后,


会去访问 c 位置空间。


② 如果 Add 函数栈帧销毁,清理空间,那么取 c 值的时候取到的就是随机值,


给 ret 就是随机值,当前这个取决于编译器实现了。VS 下销毁栈帧,是不清空间数据的。


栈帧:C语言中,每个栈帧对应着一个未运行完的函数。

栈帧中保存了该函数的返回地址和局部变量。

既然不清空间数据,那还担心什么呢?


我们来看看下面这种情况:

#include <iostream>
using namespace std;
 
int& Add(int a, int b) 
{
    int c = a + b;
    return c;
}
 
int main()
{
    int& ret = Add(1, 2);
    cout << ret << endl;
    Add(10, 20);
    cout << ret << endl;  // 这里ret变成30了
 
    return 0;
}

解读:我们并没有动 ret,但是 ret 的结果变成了 30,因为栈帧被改了。


当再次调用 Add 时,这块栈帧的 "所有权" 就不是你的了。


我函数销毁了,栈帧就空出来了,新的函数覆盖了之前那个已经销毁的栈帧,


所以 ret 的结果变成 30 了。


结论就是:不要轻易使用引用返回!


那引用返回有什么存在的意义呢?等我们后面讲完类和对象后再细说。


总结:


日常当中是不建议用引用返回的,如果函数返回时,出了函数的作用域,


如果返回对象还未还给操作系统,则可以使用引用返回,如果已经还给操作系统了,


就不要用引用返回了,老老实实传值返回就行了。


通俗点说就是 —— 看返回对象还在不在栈帧内,在的话就可以使用引用返回。


举个例子:静态变量,全局变量,出了作用域不会销毁

int& Count() 
{
    static int n = 0;
    n++;
    // ...
    return n;
}

注意事项:临时变量具有常性

临时变量是右值(不可被修改),可以读但不能修改。

3.6关于引用的探讨

3.6.1比较传值和传引用的效率

那传值返回和传引用返回的区别是什么呢?


传引用返回速度更快。


以值作为参数或者返回值类型,在传参和返回期间,


函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时拷贝。


因此值作为参数或者返回值类型,效率是非常低下的,


尤其是当参数或者返回值类型(比如一些结构体)非常大时,效率就更低。


传值和传指针在作为传参以及返回值类型上效率相差十分悬殊。


引用的作用主要体现在传参和传返回值:


① 引用传参和传返回值,有些场景下面,可以提高性能(大对象 + 深拷贝对象)。


② 引用传参和传返回值,输出型参数和输出型返回值。


有些场景下面,形参的改变可以改变实参。


有些场景下面,引用返回,可以减少拷贝、改变返回对象。(了解一下,后面会学)


引用后面用的非常的多!非常重要!

3.6.2 引用和指针的区别

在语法概念上:引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

但是在底层的实现上:实际上是有空间的,因为引用是按照指针方式来实现的。

① 引用是在概念上定义一个变量的别名,而指针是存储一个变量的地址。

② 引用在定义时必须初始化,而指针是最好初始化,不初始化也不会报错。

③ 引用在初始化时引用一个实体后,就不能再引用其他实体,

而指针可以在任何时候指向任何一个同类型的实体。

④ 有空指针,但是没有空引用。

⑤ 在 sizeof 中含义不同,引用结果为引用类型的大小,

但指针始终是地址空间所占字节数(64位平台下占8个字节)

⑥ 引用++即引用的实体增加1,指针++即指针向后偏移一个类型的大小。

⑦ 有多级指针,但是没有多级引用。

⑧ 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。

⑨ 引用比指针使用起来相对更加安全。

总结:指针使用起来更复杂一些,更容易出错一些。(指针和引用的区别,面试经常考察)

使用指针有考虑空指针,野指针等等问题,指针太灵活了,所以相对而言没有引用安全!

3.7常引用

如果既要利用引用来提高程序的效率,又想要保护传递给函数的数据不能在函数中被改变,

就应使用常引用。常引用就是在前面引用的语法前+const

语法:const 数据类型&  引用名 =  引用实体;

一共有三种情况:分别是权限的放大、保持权限不变、权限的缩小。

3.7.1 权限的放大

下面是一个引用的例子:

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

如果对引用实体使用 const 修饰,直接引用会导致报错:

int main()
{
    const int a = 10;
    int& ra = a;
 
    return 0;
}

分析:导致这种问题的原因是,我本身标明了 const,这块空间上的值不能被修改。

我自己都不能修改,你 ra 变成我 a 的引用,意味着你修改 ra 可以修改我的 a,

这就是属于权限的放大问题,a 是可读的,你 ra 要变成可读可写的,当然不行。

那么如何解决这样的问题,我们继续往下看。

3.7.2 保持权限的一致

既然引用实体用了 const 进行修饰,我直接引用的话属于权限的放大,

我们可以给引用前面也加上 const,让他们的权限保持不变。

给引用前面加上 const:

int main()
{
    const int a = 10;
    const int& ra = a;
 
    return 0;
}

解读:const int& ra = a 的意思就是,我变成你的别名,但是我不能修改你。

这样 a 是可读不可写的,ra 也是可读不可写的,这样就保持了权限的不变。

如果我们想使用引用,但是不希望它被修改,我们就可以使用常引用来解决。

3.7.3 权限的缩小

如果引用实体并没有被 const 修饰,是可读可写的,

但是我希望它的引用不能修改它,我们可以用常引用来解决。

a 是可读可写的,但是我限制 ra 是可读单不可写:

int main()
{
    int a = 10;
    const int& ra = a;
 
    return 0;
}

解读:这当然是可以的,这就是权限的缩小。

举个例子,就好比你办身份证,你的本名是可以印在身份证上的,

但是你的绰号可以印在身份证上吗?

所以就需要加以限制,你的绰号可以被人喊,但是不能写在身份证上。

所以,权限的缩小,你可以理解为是一种自我约束

3.7.4 常引用的应用

举个例子:

假设 x 是一个大对象,或者是后面学的深拷贝的对象

那么尽量用引用传参,以减少拷贝。

如果 Func 函数中不需要改变 x,那么这里就尽量使用 const 引用传参。

void Func(int& x) 
{
    cout << x << endl;
}
 
int main()
{
    const int a = 10;
    int b = 20;  
 
    Func(a);  // 报错,涉及权限的放大
    Func(b);  // 权限是一致的,没问题
 
    return 0;
}

加 const 后,让权限保持一致:

// "加上保持权限的一致"
void Func(const int& x) 
{
    cout << x << endl;
}
 
int main()
{
    const int a = 10;
    int b = 20;  
 
    Func(a);  // 权限是一致的
    Func(b);  // 权限的缩小
 
    return 0;
}


解读:如此一来,a 是可读不可写的,传进 Func 函数中也是可读不可写的,


就保持了权限的一致了。b 是可读可写的,刚才形参还没使用 const 修饰之前,


x是可读可写的,但是加上 const 后,属于权限的缩小,x 就是可读但不可写的了。


所以说引用做参数时和以前一样(甚至更建议)函数中不改变参数的值时,在前面+const


常引用后期会用的比较多,现在理解的不深刻也没关系,早晚的事情。


后面讲类和对象的时候会反复讲的,印象会不断加深的。

3.7.5 带常性的变量的引用

先看代码:

int main()
{
    double d = 3.14;
    int i = d;
 
    cout << d << "  " << i << endl;//输出了3.14  3
    return 0;
}

这里的 d 是可以给 i 的,这个在C语言里面叫做 隐式类型转换

它会把 d 的整型部分给 i,浮点数部分直接丢掉。

但是我在这里加一个引用呢?

int main()
{
    double d = 3.14;
    int& i = d;  // 我能不能用i去引用d呢?
 
    return 0;
}

运行结果:(报错)

直接用 i 去引用 d 是会报错的,思考下是为什么?

这里可能有的朋友要说,d 是浮点型,i 是整型啊,会不会是因为类型不同导致的?

但是奇葩的是 —— 如果我在它前面加上一个 const 修饰,

却又不报错了,这又是为什么?

int main()
{
    double d = 3.14;
    const int& i = d;  // ??? 又可以了
 
    cout << d << "  " << i << endl;//输出了3.14  3
    return 0;
}

解析:因为 内置类型产生的临时变量具有常性,不能被修改。

隐式类型转换不是直接发生的,而是现在中间产生一个临时变量。

是先把 d 给了临时变量,然后再把东西交给 i 的:

如果这里用了引用,生成的是临时变量的别名,

又因为临时变量是一个右值,是不可以被修改的,所以导致了报错。

结论:如果引用的是一个带有常性的变量,就要用带 const 的引用。

3.7.6 常引用做参数

前面提到过:使用引用传参,如果函数中不改变参数的值,建议使用 const 引用

举个例子:

一般栈的打印,是不需要改变参数的值的,这里就可以加上 const

void StackPrint(const struct Stack& st) {...}

const 数据类型&  可以接收各种类型的对象。


使用 const 的引用好处有很多,如果传入的是 const 对象,就是权限保持不变;


普通的对象,就是权限的缩小;中间产生临时变量,也可以解决。


因为 const 引用的通吃的,它的价值就在这个地方,如果不加 const 就只能传普通对象。


又到了枯燥的学习知识点的阶段,如果想深入学习的话要学的东西还是多啊。


修炼内功,修炼内功

本篇完。

目录
相关文章
|
1月前
|
存储 安全 编译器
c++入门
c++作为面向对象的语言与c的简单区别:c语言作为面向过程的语言还是跟c++有很大的区别的,比如说一个简单的五子棋的实现对于c语言面向过程的设计思路是首先分析解决这个问题的步骤:(1)开始游戏(2)黑子先走(3)绘制画面(4)判断输赢(5)轮到白子(6)绘制画面(7)判断输赢(8)返回步骤(2) (9)输出最后结果。但对于c++就不一样了,在下五子棋的例子中,用面向对象的方法来解决的话,首先将整个五子棋游戏分为三个对象:(1)黑白双方,这两方的行为是一样的。(2)棋盘系统,负责绘制画面。
26 0
|
4月前
|
存储 分布式计算 编译器
C++入门基础2
本内容主要讲解C++中的引用、inline函数和nullptr。引用是变量的别名,与原变量共享内存,定义时需初始化且不可更改指向对象,适用于传参和返回值以提高效率;const引用可增强代码灵活性。Inline函数通过展开提高效率,但是否展开由编译器决定,不建议分离声明与定义。Nullptr用于指针赋空,取代C语言中的NULL。最后鼓励持续学习,精进技能,提升竞争力。
|
5月前
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
6月前
|
存储 编译器 C语言
【C语言程序设计——入门】C语言入门与基础语法(头歌实践教学平台习题)【合集】
本文档介绍了C语言环境配置和编程任务,主要内容包括: - **C语言环境配置**:详细讲解了在Windows系统上配置C语言开发环境的步骤。 - **第1关:程序改错**:包含任务描述、相关知识(如头文件引用、基本语法规则)、编程要求、测试说明及通关代码。 - **第2关:scanf函数**:涉及`scanf`和`printf`函数的格式与使用方法,提供编程要求、测试说明及通关代码。 文档结构清晰,涵盖从环境搭建到具体编程任务的完整流程,适合初学者学习和实践。
127 4
|
7月前
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
277 7
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
6月前
|
C语言
【C语言程序设计——入门】基本数据类型与表达式(头歌实践教学平台习题)【合集】
这份文档详细介绍了编程任务的多个关卡,涵盖C语言的基础知识和应用。主要内容包括: 1. **目录**:列出所有关卡,如`print函数操作`、`转义字符使用`、`数的向上取整`等。 2. **各关卡的任务描述**:明确每关的具体编程任务,例如使用`printf`函数输出特定字符串、实现向上取整功能等。 3. **相关知识**:提供完成任务所需的背景知识,如格式化输出、算术运算符、关系运算符等。 4. **编程要求**:给出具体的代码编写提示。 5. **测试说明**:包含预期输入输出,帮助验证程序正确性。 6. 文档通过逐步引导学习者掌握C语言的基本语法和常用函数,适合初学者练习编程技能。
182 1
|
9月前
|
存储 安全 编译器
【C++打怪之路Lv1】-- 入门二级
【C++打怪之路Lv1】-- 入门二级
81 0
|
5月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
1月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
46 0