当前位置: 首页 > article >正文

初识C++(二)

六、引用

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

通俗地讲,可以理解为一个人能够拥有多个称呼,这些所有的称呼都是表示这一个人的。

用法:类型& 引用变量名(对象名) = 引用实体;

注意:引用类型与引用实体的类型必须一致!

引用使用的注意事项

①引用在定义时必须初始化

②引用初始化后,无法再改变指向

③一个引用实体可以拥有多个引用/别名

下面是关于注意事项的图例:

引用使用举例

交换函数Swap:

那么问题来了:既然能够使用引用来替换指针的一些用途,引用能否完全替换掉指针呢?

不能的!C++设计出引用,是对指针使用复杂的场景进行一些替换,让代码简单易懂,并不能完全替换指针,引用不能够完全替换指针的根本原因在于引用不能够改变其指向。

对于Java/Python而言,是不存在指针的,这两门语言的引用能够改变指向,因此替换掉了指针。

也正是因为C++的引用不能够改变指向,对于链表、二叉树等基于指针链接前后物理不连续的空间的数据结构,也是不能使用引用替换掉指针的!

引用的使用场景

1.做参数

①输出型参数;②对象比较大时,使用引用能够减少拷贝、提高效率。

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

2.做返回值

①修改返回对象;②减少拷贝,提高效率。

错误示例

首先,C语言中学到过,当我们在函数中返回临时变量/局部变量的值时,由于函数栈帧的回收,会导致返回的该值是一个随机值,指针中讲到的野指针也是这样类似的情况。

以下是错误的示例

上图就返回了函数中创建的一个临时变量的值,这样做是错误的。

那么同样的,我们也不能够在上图的返回类型使用引用类型,同样为错误示例,如下所示:

上图函数Func返回类型int&,引用类型的返回,同样不能够作用在临时变量/局部变量上!否则相当于对一块未经允许访问的空间进行了访问,本质是野引用!将这个未经访问的空间a中的值赋给ret,那么ret就是随机值。

同样,ret是别名的错误示例

返回变量出了函数作用域,生命周期到头,需要销毁(栈帧回收) ,因此不能够使用引用返回!

如局部变量与临时变量。

正确示例

引用不能作用于临时变量、局部变量,那么引用一般作用于什么呢?

引用能够作用于静态变量(如static修饰)、全局变量、堆区开辟空间的变量。

首先在C++中,变量一般被称之为对象,同时C++将结构体升级成了类:结构体中不再只能够定义变量/对象,还能够定义函数。这意味着对于一些数据结构的实现,不再需要采取结构定义与实现函数分离的形式,而是可以直接在类中定义结构和函数进行操作

类中定义的函数名也较C语言简洁,因为该函数存在于哪个类,就属于哪个类的函数,不需要用复杂的名称来与其他的结构中的函数进行区分!

拿顺序表简单地举个例子: 

顺序表的类:

//正确示范
//C++中的结构体中能够定义对象和函数,结构体一般称之为类
//同时C++不需要使用typedef去除struct,允许直接使用struct后名称
struct SeqList
{
	int* a;
	int size;
	int capacity;

	//相较于C语言的函数操作更为简便
	void Init()//初始化
	{
		int* tmp = (int*)malloc(sizeof(int) * 4);
		if (tmp == NULL)
		{
			perror("malloc fail");
			return;
		}
		a = tmp;

		size = 0;
		capacity = 4;
	}
	void PushBack(int x)//尾插
	{
		//CheckMemorySize(省略)
		a[size++] = x;
	}
	int& Get(int pos)//获取pos下标的数据、使用int&返回别名,也能够修改pos处的数据
	{
		assert(pos >= 0);
		assert(pos < size);

		return a[pos];
	}
	void Destroy()//销毁
	{
		free(a);
		a = NULL;
	}
};

如上我们可以看见,在SeqList类中直接定义初始化、尾插、获取任意位置数据并修改等函数,在形参部分和整体代码量相较于C语言简洁便利许多。同时C++不需要使用typedef去除struct,直接使用类型名即可。

同时,对于获取pos下标处数据与修改,在C语言中本是两个函数分别完成,但是现在可以使用int&的引用类型作为返回值,一个函数就能够搞定,因为它返回的是该下标处数据的别名,对其的修改能够直接体现在数据的修改上。

传值返回,返回的是变量的临时拷贝;传引用返回,返回的是变量的别名。

正因如此,传值无法做到直接改变目标变量,但是传引用却能够轻松完成。

简单插入4个数据对其进行一些数据操作如下图:

如果我们将Get函数的返回类型改为int,那么就会返回目标下标数据的拷贝,即一份临时变量。

临时变量具有常性,即常量性,无法被修改!

如上图,使用引用作为返回值是能够起到很重要的作用的。

引用与指针的区别

引用在底层转换为指针的解释如下图: 

我们对比引用与指针在汇编层面上的指令,发现引用经过编译过程后也被转换为了指针,那么就说明,在底层上,引用是会开空间的,语法层面上引用是别名,不开空间。

引用底层使用指针实现的,引用的语法含义与底层实现是背离的!

通俗点,就像鱼香肉丝中没有鱼,老婆饼也不是老婆做的一样。

七、内联函数

如果调用函数次数过多、需要创建的函数栈帧过多,那么为了提高效率,C语言中通过宏函数的方式来替换函数;而C++通过内联函数的方式使函数在外部直接展开而不用创建栈帧。

对于宏而言,需要注意:1.宏并不是函数;2.宏属于预处理指令,末尾不需要分号,本质是一种替换;3.额外重视括号!括号控制优先级。

宏在预处理阶段就会被替换!

对于一个加法的Add函数,写成宏函数如下:

为什么x、y要单独括号括起来?

因为x、y不一定代表一个值,可能代表一个表达式!如果x、y是表达式,且表达式中的运算符优先级低于+号,那么替换进去就会出现问题!举例如下:

因此我们需要加括号确保绝对的正确顺序。

宏的优缺点?

优点:1.增强代码的复用性;2、提高效率/性能

缺点:1、宏在预处理阶段就被替换,不方便调试;2、可读性差,可维护性差,容易误用;3、没有类型安全的检查。

C++替代宏的技术?

1、常量定义---换用const enum;2、短小函数定义,换用内联函数。

内联函数的概念

inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开没有函数调
用建立栈帧的开销,内联函数提升程序运行的效率。

未使用inline修饰时,调用Add函数的反汇编代码如下图:

在汇编层面上,我们看到编译器会去找函数Add的指令首地址,从而调用函数。

如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用,不调用函数,也就不用创建函数栈帧,消除了函数栈帧创建的开销:

release下:直接查看反汇编下汇编代码是否存在call Add

debug下:我们需要将属性-常规-调试信息格式改为程序数据库(/Zi),属性-优化-内联函数扩展改为只适用于_inline(/Ob1),这样就能够调试-反汇编查看了

那么使用inline修饰Add函数,使其成为内联函数后,内联函数展开的反汇编如下:

通俗来讲,内联函数就是将函数里面的运算逻辑灵活地在外面实现,而不去创建调用函数所需要的栈帧空间。

内联特点

内联本质上是向编译器发出请求,当函数较大,编译器就会忽略内联的请求,较小函数能够正常内联。

因此较小函数的多次调用,我们可以使用内联inline,较大函数则使用静态static。

1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。

2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。

3. inline不建议声明与定义分离,分离的情况会导致链接错误,inline展开函数,就不会在符号表中生成该函数的地址,链接时就无法找到对应函数。

#pragma once解决头文件重复引入的问题,但是并不能解决两个.cpp文件中都包含相同头文件,该头文件又包含某个函数的定义,如下图所示,那么就会存在函数的重定义问题。

有三种方式能够解决这一问题: 

①声明与定义分离

如果不采用声明与定义分离,只通过定义,那么可以使用static或者inline进行操作。 

②采用static静态链接,只在当前文件可见 

③采用inline内联,同理只在当前文件展开

八、auto关键字---C++11

auto:自动推导类型。

对于auto而言,必须要初始化,不然怎么自动推导呢?

实际意义:对于较长类型,可以使用auto简便替代:

如下图的函数指针类型:

再如其他较长的类型:

auto的利弊

优点:简化较长的类型。

缺点:对不熟悉当前代码的人而言,一定程度上影响代码可读性。

注:①C++规定auto不能定义数组。②慎用auto,C++目前允许auto作返回值,但是不建议。

③同一行使用auto声明多个变量,这些变量必须同类型,否则编译器不会通过,编译器只对第一个类型进行推导,然后用该类型定义后面的变量。

上图中auto a = 9 , k = 'c' ;就无法编译通过,因为a与k类型不同但是他俩想用一个auto。

auto不能推导的场景

①auto不能作为函数的参数

②auto不能直接用来声明数组

九、范围for循环---C++11

范围for循环的使用

for(auto e : array)
{
    cout << e ;
}
cout << end;

auto自动推导数组中数据类型,也可以自己填入数组数据类型;e是数组数据的依次的临时拷贝。

范围for循环会自动迭代、自动判断结束。

范围for循环可以使我们的数组遍历过程变得非常简便:

范围for循环的使用条件

①for循环迭代的范围必须确定。

数组即第一个元素到最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

②迭代的对象要实现++和==的操作

十、指针空值nullptr---C++11

在C++中,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。

因此C++11中创建了nullptr关键字,用于表示指针空值,就不用NULL来表示指针空值了,以避免可能出现的差错。

注:

①在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的    ②在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。                                        ③为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。


http://www.kler.cn/a/502799.html

相关文章:

  • 前端开发:表格、列表、表单
  • 计算机网络速成
  • unity打包sdk热更新笔记
  • 【华为OD-E卷 - 求字符串中所有整数的最小和 100分(python、java、c++、js、c)】
  • IMX6ULL的IOMUXC寄存器和SNVS复用寄存器似乎都是对引脚指定复用功能的,那二者有何区别?
  • 【文件锁】多进程线程安全访问文件demo
  • windows和linux的抓包方式
  • C# Winform:项目引入SunnyUI后,显示模糊
  • Unknown Kotlin JVM target: 21
  • 如何创建一个数组并指定初始大小?
  • MATLAB学习笔记目录
  • 高性能多链 Layer2 基础设施 Cartesi:2024 生态发展回顾
  • Three.js 用户交互:构建沉浸式3D体验的关键
  • 透明部署、旁路逻辑串联的区别
  • 【数据结构-堆】力扣1792. 最大平均通过率
  • go中协程的生命周期
  • OpenCV实现Kuwahara滤波
  • Redis优化建议详解
  • UE5 使用内置组件进行网格切割
  • 【 PID 算法 】PID 算法基础
  • 云计算的环保性分析:真相与误区
  • 嵌入式岗位面试八股文(篇四 网络编程)
  • 20道Vue.js常见面试题
  • mysql set age=‘0‘ 和 set age=0的区别?
  • 【21天学习AI底层概念】day11 (kaggle新手入门教程)Your First Machine Learning Model
  • qt设置qwidget背景色无效