文章目录
- 一、引用概念及示例
- 二、引用做函数参数
- 二、引用做函数的返回值
- 四、常引用
- 五、引用和指针的区别
一、引用概念及示例
c语言指针存变量地址,然后通过解引用可以访问或者改变变量,且也可以改变指针变量里面存的地址
修改变量这样还需要对指针变量解引用,这样较为麻烦,且修改地址还有些不安全,所以c++中有了同样能修改变量且可以访问变量的名词
这被成为引用引用 引用:引用不是新定义的变量,而是给已存在的变量取一个别名,
不过编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间
引用操作符&它这与取地址操作符一样,不过引用在使用时引用是双目,&左边数据类型,右边为引用变量名,
而取地址操作符是单目操作符,右边为取地址变量名
引用就相当于给别人起外号,虽然一个人可能有很多外号,但是这些外号对应的还是那个人 而对其中一个外号进行改动,其它外号都是在变化的
如有一个人叫张三,别人又叫他法外狂徒,如今我们将法外狂徒判刑十年,是不是张三被判刑了10年虽然一个变量可能有不同的别名但是这些别名它的空间都为同一个空间,其中一个别名改变 也会影响到其它的别名甚至他本身。
#include<iostream>
//在引用时要初始化,要说明是谁的引用
using namespace std;
int main()
{
int a = 0;
int& b = a;//对a起别名
int& c = b;//对b起别名
c++;//b的别名改变
cout << a << endl;
cout << b << endl;
cout << c << endl;
b++;//a的别名改变
cout << a << endl;
cout << b << endl;
cout << c << endl;
//b是a的引用,c是b的引用,它们和它所引用的对象共用同一块空间,所以它们地址都相同
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
return 0;
}
二、引用做函数参数
引用作为函数参数的形参,在函数调用时,相当于形参是对实参的引用,所以在函数内部对实参的操作也就是对实参的操作。引用做参数,形参和实参共同有一块内存空间,没有为形参开辟新的内存空间,减少了调用时因为形参开辟内存的损耗,提高了效率
#include<iostream>
using namespace std;
//交换函数
//形参x是实参a的引用
//形参y是实参b的引用
void Swap(int& x, int& y)
{
//交换变量x和y的值就是交换变量a和b的值
int temp = x;
x = y;
y = temp;
//x与a的地址相同
cout <<"形参x地址:"<< &x << endl;
//y与b的地址相同
cout <<"形参y地址:" << &y << endl;
//编译器并没有为它们开辟临时空间
int main()
{
int a = 10;
int b = 20;
cout << "交换前:"<<endl;
cout << "a:"<<a << endl;
cout <<"b:"<< b << endl;
Swap(a, b);
cout << "交换后:"<<endl;
cout <<"a:"<< a << endl;
cout <<"b:"<< b << endl;
return 0;
}
而不是引用做参数的,要达到交换函数的话,在调用函数时,就需要传实参地址,形参为指针,且还要为这形参(指针变量)开辟临时空间。
//传址
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
cout <<"形参x地址:"<< &x << endl;//x与a的地址并不相同,即使x指向a的地址,但是只要它创建了一个变量,那么编译器就会为起分配内存空间
cout <<"形参y地址:"<< &y << endl;//y与b的地址并不相同
}
//传值
int add(int a, int b)
{
cout <<"形参a地址:"<< &a << endl;
cout <<"形参b地址:"<< &b << endl;
return a + b;
}
int main()
{
int a = 10;
int b = 20;
cout << "交换前:" << endl;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
cout <<"实参a地址:"<< &a << endl;
cout <<"实参b地址:"<< &b << endl;
Swap(&a, &b);
cout << "交换后:" << endl;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
int x = 1;
int y = 2;
cout <<"实参x地址:"<< &x << endl;
cout <<"实参y地址:"<< &b << endl;
int ret = add(x, y);
cout << ret << endl;
return 0;
}
不是引用型做参数的,在函数调用时实参无论是传值还是传址,形参变量在定义时编译器都会为其开辟一个临时内存空间,形参都有自己的地址,在调用时会为形参开临时空间有一定的损耗,降低效率,引用做参数时,没有为形参开临时空间,减少了因开临时空间的损耗,提高了效率。当参数是一个很大时,效率比引用做参数低很多
以形参为一个很大的结构体为例
struct A
{
int a[100000];
};
void f(A c)//c++可以直接用结构体名
{
}
//引用没有开临时空间,没有那些消耗
void f1(A&c){}
void TestValue()
{
A a;
size_t begin1 = clock();
for (int i = 0; i < 10000; i++)
{
f(a);
}
size_t end1 = clock();
size_t begin2 = clock();
for (int i = 0; i < 10000; i++)
{
f1(a);
}
size_t end2 = clock();
cout << "f_time:" << end1 - begin1 << endl;
cout << "f1_time:" << end2 - begin2 << endl;
}
int main()
{
TestValue();
return 0;
}
引用做参数极大的提高了效率
二、引用做函数的返回值
值返回和引用返回,引用返回时,返回的对象具有左值特性,可以被修改
值返回
int fun1()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = fun1();
cout << ret << endl;
return 0;
}
n并不是直接返回给ret,它中间会生成一个临时变量,一般是寄存器,然后作为返回值给ret,但是不一定是寄存器,寄存器只有四个字节或者8个字节,数据量大就放不下,因此有时候生成的临时变量不一定是寄存器
返回值拷贝给临时变量,临时变量拷贝给接收变量,两次拷贝
为什么会生成临时变量空间?
函数调用为函数开辟空间,在main函数中为变量ret申请了一块空间,存放ret的值,main函数调用了fun1函数,fun1函数中为n申请了一块空间。当fun1函数调用结束时,fun1空间销毁,但是需要将fun1函数中的n的值返回,n在局部域里面,不能用n作为返回值,fun1函数都已经调用结束如何能返回,通过将n的值放入寄存器中,通过寄存器将值带回给调用它的那个函数,而将值带回这个过程生成了临时变量,然后将值拷贝给这个临时变量,将返回值带回,同样有因开辟临时空间带来的消耗。
引用返回
如何避免因生成临时变量带来的消耗?
引用返回,不生成临时变量减少了拷贝带来的消耗,提高了效率,小对象影响不大,但是大对象时就极大的提高了效率
当大对象做返回时,引用和值返回效率有很大差异
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
TestReturnByRefOrValue();
return 0;
}
引用做返回能极大的提高效率
但是不是每个场景都能用引用返回
fun1函数结束,栈帧销毁,如果没有清理栈帧,那么ret的结果就会是正确的,
fun1函数结束,栈帧销毁,如果清理栈帧,那么ret的结果就会是随机值,他这块栈帧空间里面的内容都被清理了,此时再访问时就会出现随机值。
int& fun()
{
//static int n = 0;
int n = 0;
n++;
cout << &n << endl;
return n;
}
int main()
{
const int& ret = fun();//由于这个常量返回是通过寄存器保存,然后回来时再将寄存器里面的值赋值给ret,
cout << ret << endl;
cout << &ret << endl;
return 0;
}
它两个地址是相同的
fun函数栈帧没有清理,编译器一般是不会清理栈帧空间的内容,但是这个空间里的内容有可能为随机值,将这块空间重新赋值。
ret是n的别名,再调用fun函数,也就将上一个函数的栈帧空间覆盖了给新调用的fun分配栈帧空间,此时再访问ret,ret的值可能为随机值,访问销毁空间上的变量,像数组那样越界访问了。
函数栈帧没有清理,再调用另外一个函数,而这个函数又要建立栈帧,初始化,然后这个栈帧将上一个函数的栈帧空间覆盖了,此时再访问ret,就会是随机值
后面栈帧覆盖前一个栈帧,此时再访问就是一个随机值
用引用返回出了函数作用域再访问这样的操作就相当于野指针访问十分危险。所以要返回的值只能作用在该函数中,不可用引用返回,当函数栈帧销毁不会影响返回的值,可以用引用返回的,引用返回
适用于全局变量,静态变量(static),由malloc出来的
返回值出了作用域引用对象不在的不适用引用返回。
对顺序表赋某个位置的值赋值+修改
将pos位置值改为10,一般的话需要获得pos位置的值,然后再对这个位置的值修改为10再将10赋值给顺序表pos位置赋值
#include<assert.h>
struct SeqList
{
int a[100];
size_t size;
};
//将pos位置的值修改为x
void SLModify(SeqList* ps, int pos, int x)
{
assert(pos >= 0 && pos < 100);
ps->a[pos] = x;
}
//获取pos位置的值
int Get(SeqList* ps, int pos)
{
assert(pos >= 0 && pos < 100);
return ps->a[pos];
}
int main()
{
SeqList s;
SLModify(&s, 0, 1);
int ret = Get(&s, 0);
cout << ret << endl;
ret += 5;//只是得到pos位置的值修改并没有做到对顺序表进行修改
cout << ret << endl;
SLModify(&s, 0, ret);//要修改还需要重新调用修改顺序表的接口
}
这样很麻烦,明明都将值修改了结果还要再调用修改顺序表接口函数,因此用引用返回可以做到获取pos位置值时就将其修改
而引用做函数的返回值,它返回的对象具有左值的特性,可以被修改,一般函数的返回值对象是不可被修改的,所以可以直接获取pos位置的值的别名,然后对其修改其实也就是对顺序表修改
#include<assert.h>
struct SeqList
{
int a[100];
size_t size;
};
void SLModify(SeqList* ps, int pos, int x)
{
assert(pos >= 0 && pos < 100);
ps->a[pos] = x;
}
int& SLAt(SeqList* ps, int pos)
{
assert(pos >= 0 && pos < 100);
return ps->a[pos];
}
int main()
{
SeqList s;
SLModify(&s, 0, 1);
SLAt(&s, 0) = 10;//引用返回的具有左值特性,可修改
cout << SLAt(&s, 0) << endl;
cout << SLAt(&s, 0)+5 << endl;
return 0;
}
引用做返回值
- 减少拷贝提高效率
- 修改返回值和获取返回值
四、常引用
在定义引用变量时用const修饰,被定义的引用变量也就是常引用
一般引用
a可修改可读,可以对a引用,b对a的引用过程中为权限的平移
这里引用时,a变量被const修饰不可修改,只读,而如果对其引用就会变为可修改,修改了它的权限,是错误的。
在引用过程中权限不能放大
。
那么如何将其变为正确的,在
加const修饰,成为常引用
z为x的别名,但是现在它只有可读属性,不可被修改,原本变量的可改属性没有了,所以在引用过程中,权限可以缩小。以z访问时,不可以修改它也就不可以修改x,但是以x访问时,x改变,此时z也会改变,z和x共用空间,只是不能通过z改变,但可以通过x改变z
为何不可?
由于
不同类型转换才会产生临时变量,临时变量为右值,具有常性,相当于加了const修饰,不可被修改,因此可以常引用
引用时权限不可放大,需要加const修饰使其成为常引用
这是可以的,权限平移
常引用
五、引用和指针的区别
引用在定义时必须初始化,指针没有要求
引用是已知变量的别名,指针是地址
引用在引用一个实体后不能再引用其它实体,但是指针可以指向其它实体
没有NULL引用,但是有NULL指针
计算大小字节时,引用的大小为引用类型的大小,指针始终时地址空间所占字节个数4或者8字节
引用自增就是引用的实体加一,而指针自增是向后偏移一个类型的大小
引用比指针使用起来更安全(指针空指针,野指针)
语法上引用没有开空间,指针开空间
但是从底层汇编指令角度看,引用类似指针的方式实现的
指针汇编
引用汇编