文章目录
- 一、引用
- 引用的概念和定义
- 引用的功能
- 引用的特性
- const引用
- const用法回顾
- 权限的放大缩小
- const引用的功能
- 指针和引用的关系
- 二、内联函数
- 三、nullptr
- 补充
- 结构体指针变量类型重定义
一、引用
引用的概念和定义
C++祖师爷为了优化在部分场景中使用指针会出现的效率较低和比较复杂的情况,引入了一个新概念——引用。
引用不是新定义一个变量,而是为已经存在的变量取别名,编译器不会为引用变量开辟空间,它和它引用的变量共用一块空间,通俗来说就是为一个变量取别名,虽然叫法不同,变量的不同的别名还是指代这个变量本身,也就是你的正式名字和你的外号都是你本身,引用用法如下:
类型& 引用别名 = 引用的对象;
#include <iostream>
using namespace std;
int main()
{int a = 0;//引用:b和c是a的别名int& b = a;int& c = a;//也可以为别名b取别名,d相当于还是a的别名int& d = b;//这里我们可以看到abcd的地址都是一样的cout << &a << endl;cout << &b << endl;cout << &c << endl;cout << &d << endl;return 0;
}
下面代码不是让d变成x的别名,只是赋值修改d指向的这块空间的值。
引用的功能
一、
其实引用在功能上和指针是有部分重叠的,至于怎么个重叠法呢,跟随小编的脚步一起来看看吧。
在C语言阶段我们想要实现一个交换变量值的函数时是需要传变量的地址的,这样形参的改变才会影响实参,现在我们了解了引用后其实就可以把它用上了,如果我们传引用调用函数,那么函数的形参名就是实参的别名,形参发生了什么变化实参也会跟着变。
补充:变量引用的生命周期一定和变量本身一样或者比变量本身小,比如下面x是a的引用,x出了swap这个函数后就销毁了,但是a还存在。
void swap(int* x, int* y)
{int tmp = *x;*x = *y;*y = tmp;
}void swap(int& x, int& y)
{int tmp = x;x = y;y = tmp;
}int main()
{int a = 10;int b = 20;swap(&a, &b);cout << a << " " << b << endl;swap(a, b);cout << a << " " << b << endl;return 0;
}
这里我们可以总结出引用的第一个功能:
引用做函数形参,修改形参影响实参
二、
在调用函数传参的时候当参数很大时,比如下面这个结构体变量A,如果传参数本身会把这个参数拷贝过去(因为形参是实参的临时拷贝),这样空间开销就太大了,所以我们通常会传它的指针,指针最大也就8个字节。我们想想,引用在这里依旧可以实现同样的功能,前面介绍到引用是不会为变量开辟空间的,所以这里也可以采用传引用的方式。
struct A
{int a[100];int b;
};void Func(struct A* a)
{ }void Func(struct A& aa)
{ }int main()
{struct A a;Func(&a);Func(a);
}
引用做函数形参,减少拷贝,提高效率
三、
引用不仅可以作为函数形参,还可作为函数返回值返回,在介绍引用做函数返回值有哪些功能之前,还请允许小编科普一些有关函数返回值的知识点。
上面小编实现了一个简单的传值返回,其中变量ret在出了Func这个函数作用域后就销毁了,所以我们可以确定x接受的返回值一定不会是ret本身。这里返回的其实是ret的一份临时拷贝变量,并且语法规定了这个临时变量是具有常性的,这里可以简单理解成它被const修饰过,并且语法规定就算返回值出了作用域还存在它返回的也是临时变量,因为编译器不会去识别返回值出作用域是否销毁。
有了上面的铺垫过后接下来小编就要介绍今天的主角了。
typedef struct SeqList
{int* arr;int size;int capacity;
}SL;void SLInit(SL& sl, int n = 4)
{sl.arr = (int*)malloc(n * sizeof(SL));sl.size = 0;sl.capacity = n;
}void SLPushBack(SL& sl, int x)
{sl.arr[sl.size] = x;sl.size++;
}//该函数表示返回顺序表第i个位置的数据
int& SLAt(SL& sl, int i)
{//断言检查i是否越界assert(i < sl.size);return sl.arr[i];
}int main()
{SL s;SLInit(s);SLPushBack(s, 1);SLPushBack(s, 2);SLPushBack(s, 3);SLPushBack(s, 4);for (int i = 0; i < s.size; i++){SLAt(s, i) += 1;}for (int i = 0; i < s.size; i++){cout << SLAt(s, i) << endl;}return 0;
}
上面是小编实现的一个简单顺序表,我们可以看到顺序表尾插了四个数字1234,SLAt函数实现的是返回顺序表第i个位置的数据,如果我们想要在主函数中修改该函数的返回值该怎么做呢?读者朋友应该可以发现SLAt的返回类型是int&,这不就是才介绍的引用吗?没错,这就是传引用返回。
上图我们可以直观看到传值返回和传引用返回的区别,传值返回产生了临时变量,所以修改临时变量不会改变返回值,直观来讲就是顺序表的值不会发生改变(并且这种操作会编译报错,因为临时变量具有常性,具有常性的变量无法被修改),但是传引用返回就不一样了,它返回的是返回值的别名,所以不会产生临时变量,编译就不会报错,我们可以简单理解别名就是它本身,所以修改返回值的别名就是修改它自己,所以我们可以看到顺序表的值确实被修改了。
注意:传引用返回只适用于返回对象出了函数作用域还存在的情况,例如上面这个例子返回值对象是在堆上申请的。
这里我们可以类比引用的前两个功能,总结出另外两个功能:
引用做函数返回类型,修改返回对象
引用做函数返回类型,减少拷贝,提高效率
引用的特性
- 引用在定义时必须初始化,变量和指针都可以先定义后赋值。
//变量可以先初始化再赋值int x;x = 10;//引用不能先初始化再赋值int& r;r = x;//错误用法
- 一个变量可以有多个引用
//变量x可以有多个引用int x = 10;int& a = x;int& b = x;int& c = x;
- 引用一但引用一个实体,就不能引用其他实体。
就是平时所说的引用不能改变指向。(可以形象理解引用是非常忠贞的,一个别名只能对应一个对象)
所以引用是不能完全替代指针的,有些情况比如数据结构中的链式结构或者树形结构我们有修改指向的需求,所以这类情况只能使用指针,指针可以修改指向。
const引用
const用法回顾
在介绍const引用之前,我们先回顾一下C语言阶段我们掌握的有关const的用法:
- const修饰变量:
- const修饰的变量无法直接修改,但是可以通过它的指针绕过他,使用它的地址修改,虽然这样做打破了语法的规则。
//const修饰变量const int n = 10;n = 20; //无法实现int* pn = &n;*pn = 20; //可以实现,但是破坏了语法规则
- const修饰指针变量:
1、const在*左边,修饰指针指向的对象,保证指针指向的对象本身无法通过指针修改,但是指针变量本身可变。
//const在*左边,修饰指针指向的内容,const int* p = &n;*p = m; //无法实现p = &m; //可以实现
2、const在*右边,修饰指针变量本身,保证指针变量本身无法被修改,但是指针指向的对象可以通过指针改变。
//const在*左边,修饰指针变量本身int* const p = &n;*p = m; //可以实现p = &m; //无法实现
权限的放大缩小
const int a = 10;//权限不能放大int& r1 = a;//权限可以平移const int& r2 = a;int b = 20;//权限可以缩小const int& r3 = b;
上面我们定义了一个const修饰的int类型变量a,它具有常性无法被修改。下面我们用没被const修饰的r1做a的别名,编译器会报错,因为a本身不能被修改,r1为a的别名却没有任何限制,可以随意修改,这里就会涉及到权限的放大。但是权限是可以平移和缩小的,下面两种引用编译器都不会报错。
注意:指针和引用才会涉及权限的放大和缩小,因为指针和引用对象的改变会影响原对象。
const引用的功能
int a = 10;double d = 1.1;//以下两种情况都是引用的临时对象const int& r1 = d; //1const int& r2 = a * 10; //2
我们先看第一个引用,double类型的d被const修饰的int类型的r1引用,这里会发生隐式类型转换(C语言规定相关类型可以进行隐式转换),这里C++语法还规定类型转换会产生临时对象,这里r1引用的其实是那个临时对象,临时对象是是编译器需要一个空间来暂存表达式的求值结果或者隐式转化时产生的中间对象时临时创建的一个未命名对象,它的特点是具有常性,不能修改。
所以这里r1需要加const修饰,防止权限的放大。
有了上面临时对象的概念后第二个引用为什么要加const就很好理解了,直接上图。
总结:C++引入const引用的目的就是方便传参,后续函数形参部分若用const引用来接受,即可适配大部分传参的情况。
指针和引用的关系
在C++中,引用和指针功能有重叠性,它们各自有各自的特点,在实践中相辅相成,互相不可替代,所以C++从某种程度上会比C语言更便捷,能实现更多功能。
1、指针存储一个变量地址需要开空间,引用是为一个变量取别名不用开空间。
2、引用在定义时必须初始化,指针可以先定义再初始化。
3、指针可以更改指向的对象,引用一旦引用一个实体后就不能再引用其他对象。
4、引用可以直接访问指向对象,而指针必须解引用后访问指向对象。
5、sizeof中含义不同,引用结果为引用类型大小,而指针始终是地址空间所占字节数(32位为4字节,64位为8字节)。
6、指针容易出现空指针和野指针的问题,引用很少出现,所以引用更安全一些。
7、在指令汇编角度,引用也是用指针实现的,但是一般我们都以语法层面为准。
二、内联函数
1、我们在C语言阶段学习的宏函数会在预处理时替换展开,但是宏函数是很容易出错的,比如要注意运算符优先级,并且不能调试,但是函数会自动处理运算符优先级所以C++设计内联函数就是为了替代C语言的宏函数。
2、内联函数的关键字是inline,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不会创建栈帧了,提高了时间效率(创建栈帧消耗时间),这里我给大家演示一下普通函数和内联函数在汇编指令层面的区别:
inline int Add(int x, int y)
{int ret = x + y;return ret;
}
int main()
{int ret = Add(1, 2);cout << ret << endl;return 0;
}
不是内联:
是内联:
3、既然内联函数有这么多优点,那么我们就可以无脑用它了吗?肯定不行,因为内联函数调用过多会造成程序变大,会占用很多空间,比如内联函数转化为指令有50行,调用10000次就是500000行,我们不内联的话函数就一直待在一块公共空间,每一次调用的时候会call指令跳转到函数所在位置,那么就是程序指令一共就是10000+50行。世界上没有什么是完美的,内联函数可以简单理解成是用空间换时间。
4、inline对于编译器而言只是一个建议,也就是说就算你加了inline编译器也可以在调用的地方不展开,比如说递归函数或者代码相对多一点的函数加了inline也会被编译器忽略,所以短小并且需要频繁调用的函数适合用内联。5、内联函数不能声明和定义分离,因为内联函数会直接展开,所以在编译阶段不会进符号表,那么在另一个文件调用内联函数时,因为文件里只有内联函数的声明,(声明只有无效地址,只有在函数定义才会分配确切的地址)所以在编译阶段找不到函数的地址,那么编译器只有寄希望于链接的时候合并符号表时得到函数的地址,但是内联函数不会进符号表,所以编译器就会报链接错误。
所以内联函数的定义和声明只能在一起,简单理解就是内联函数只能在有它的定义的文件里展开,若内联函数定义在头文件里,其他文件包了这个头文件,调用这个内联函数就可以展开。6、普通函数必须声明和定义分离,如果普通函数定义在头文件里,那么如果有两个文件都包含了这个头文件,最后两个文件编译后生成的目标文件都有这个函数的确切地址,后面链接这两个目标文件时两个文件的符号表都有这个函数的确切地址,就会链接报错。
与前面知识的联系:前面我们知道有static关键字,它修饰全局变量和函数后使得它们之只能在当前源文件里使用,之前我们的解释是static让它的外部链接属性变成了内部链接属性,其实本质就是让他不进符号表,和inline类似。
三、nullptr
NULL实际上是一个宏,在传统C头文件中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
C++中NULL被定义为字面常量0,C中被定义为无类型指针(void*)的常量,不论采用何种定义,在使用空值的指针时,都不可避免会遇到一些麻烦,比如下面:
本来想通过f(NULL)调用指针版本的Func(int*)函数,但是由于NULL被定义成0,调用了f(int x),因此与程序初衷相悖。
所以C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字面量,它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。
补充
结构体指针变量类型重定义
下面代码上面的写法等价于下面
typedef struct ListNode
{int val;struct ListNode* next;
}ListNode, *pnode;typedef struct ListNode ListNode;
typedef struct ListNode* *pnode;
以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~