c++之引用详解
引用概念:引用不是新定义一个变量,而是给==已存在变量==取了一个别名,编译器不会为引用变量开辟内存空 间,它和它引用的变量==共用同一块内存空间==。
#include <iostream>
using namespace std;
int main()
{
int a = 0;
int& b = a;//引用
a++;
printf("&a = %p,a = %d\n", &a, a);
b++;
printf("&b = %p,b = %d\n", &b, b);
return 0;
}
可以看出b就是a,他们公用一块内存空间!
- ==但是引用是要初始化的!==
#include <iostream> using namespace std; int main() { int&a;//这样是会直接报错! }
一旦引用了某个实体后就不能再修改了!
#include <iostream> using namespace std; int main() { int a =10; int b = 100; int&c = a; c = b;//这个是赋值,因为一旦引用一个实体,就再也不能引用其他实体 //所以c的地址仍然与a相同! printf("a = %p\n",&a); printf("a = %p\n",&b); printf("a = %p\n",&c); }
==因为引用无法替换!这就导致了引用在c++中是无法完全代替指针的!==
引用在实际当中的价值
做参数!
//可用于减少拷贝次数!
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}//这种就叫做输出型参数!就是说我的参数在里面进行修改,然后还要传回来!
BTNode* BinarYTreeCreat(BTDataType* a,int* pi);//此处的pi就可以使用 引用来代替!
//可以用于简化二级指针!
typedef struct ListNode
{
int val;
struct ListNode* next;
}LTNode,*PTNode;
void SlistPushBack(LTNode** phead, int val);//这是最原始的链表插入,因为要改变头所以要引入二级指针!
void SlistPushBack(LTNode*& phead, int val);//这是对原本的基础上进行改进,使用引用代替二级指针!
void SlistPushBack(PTNode& phead, int val);//这是最后的优化版本!
用引用参数的好处!
- 引用做参数,可以减少拷贝次数,正常情况下,我们无论传什么类型的变量,都一定要进行一份拷贝,无非是拷贝大小的区别而已!即使是指针类型,也要对该指针进行一次临时拷贝!然后通过该指针找到我们要的变量的内存位置!但是我们如果使用引用作为参数,就相当于直接对于我们要操作的变量本身进行读写操作!不需要进行任何的拷贝操作!可以更好的节约性能!
- 增加代码可读性!一般我们要对二级,乃至更高级的指针本身进行操作我们就要使用比它高一级的指针存储它的地址!这样其实会增加对代码阅读的难度!使用引用我们就可以直观的知道,我们到底操作的是什么类型!
做返回值
int& Count()
{
static int n = 0;
n++;
return n ;
}//着叫做引用返回
int Count()
{
static int n = 0;
return n;
}//这叫做传值引用
这两个返回方式有极大的差别
传值返回
int Count() { static int n = 0; return n; } int main() { int a = Count(); }
传值返回的流程为
首先在内存的栈区建立main函数的栈帧
mian的栈帧中创建a的空间
创建Count函数的栈帧
在静态区创建 n的空间
然后创建一个临时变量
为什么要创建临时变量呢?因为如果n不在静态区,一旦函数结束变量就会被销毁!重新访问的时候值可能是要的,也可能是不要的!也就是发生了越界访问!所以不可以直接将n给ret!
有这个临时变量在,就不用担心是否在静态区中!
临时变量一般是在寄存器中!但是寄存器一般比较小,只能存比较小的变量!
大的变量会在上一层栈帧的某个地方创建一个临时变量!
讲n的数据拷贝到临时变量
最后临时变量拷贝到变量a
为了直观理解我将count的栈帧画在了main的上方,从物理上面看,main的栈帧是在上面的,因为栈帧是向下生长的!
引用返回
int& Count() { static int n = 0; return n; } int main() { int a = Count(); }
引用返回的流程为
首先在内存的栈区建立main函数的栈帧
mian的栈帧中创建a的空间
创建Count函数的栈帧
在静态区创建 n的空间
此时因为是引用返回,返回的是n的别名,也可以认为n本身就是那个临时变量!
直接从n将数据拷贝到变量a
对比传值返回和引用返回的效率
以值作为参数或者返回值类型,在传参和返回期间,函数不会==直接传递实参或者将变量本身直接返回==,而是传递实参或者返回变量的==一份临时的拷贝==,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是==当参数或者返回值类型非常大时,效率就更低!==
#include <time.h> struct A{ int a[10000]; }; void TestFunc1(A a){} void TestFunc2(A& a){} void TestRefAndValue() { 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; } int main() { TestRefAndValue(); }
关于static在引用中的作用!
int& Count() { static int n = 0; n++; return n ; }
从上面的例子中我们可以看出引用返回本质就是不通过临时变量,直接将变量本身进行返回!
但是这是有前提的!就是变量在函数结束后不可被销毁!这个例子中因为变量n==存在于静态区不会因为函数的结束而销毁==!
假如我们使用的是 int类型的n进行返回,我们无法保证返回回来的向量n是否是我们需要的数据!因为当出来函数域后,函数的栈帧也被销毁,也就是失去了使用权!然后我们在这个情况下进行拷贝==这就相当于越界访问!==
内存空间的销毁并不是字面意义上的销毁,而是使用权的失去,但是物理空间还是在哪里的!没有了使用权,无法保证里面是否使我们存储的一个数据!
在这种状态下进行访问,我们可能会得到我们想要的数据,也可能能得不到我们想要的数据,都有可能!
关于引用返回的结论
出了函数域,返回的变量被销毁,就不可以使用引用返回,因为返回的结果是未知的!
出了函数域,返回的变量保留,则可以使用引用返回!
关于临时向量
==实际上,临时向量不止存在于这种情况!不同类型赋值,强制类型转换,传值返回的时候都会产生临时变量!==
#include <iostream> using namespace std; int main() { int a = 0; double b = 0; int c = 0; a = c;//同类型的赋值不会产生临时变量! c = b;//会发生隐性强制类型转换 a = (int)b;//无论是隐形强制类型转换还是显性强制类型转换,都是要通过一个临时变量 cout << (int)b << endl;//打印的也不是b变量本身,而是它产生的为int类型的临时变量!b本身是没有任何修改的! }
我们可以看出,只要涉及强制类型转换,就会产生临时变量!强制类型转换并不是将原本的变量进行修改!而是产生一个要转换类型的临时变量,然后通过这个变量进行操作!
最重要的一点临时变量具有常性!(意味着临时变量的类型为 const int/char.....)
引用返回的意义
从上面的列子我们可以看出,引用返回的意义无非就是两点
- 减少拷贝次数,调高性能效率!
- 修改返回值!
//代码演示 typedef int SLDataType; typedef struct SeqList//这是一个顺序表的结构体 { SLDataType a*; int size; int capacity; }SL; //我们假设存怎么一个顺序表 1 2 3 4 5 6 //那么我们想要让其的下标为偶数的位置*2 SLDataType& SLAt(SL* psl,size_t pos)//使用这个是为了更加方便的进行封装! { assert(psl); assert(pos < psl->size); return pos; } int main() { SL s1; //......为了方便笔者就认定已经存在了一个顺序表为 1 2 3 4 5 6 for(int i = 0; i < SLsize(&s1);i++) { if(SLAt(psl,i) % 2 == 0) { SLAt(psl,i) *=2;//可以直接对返回值进行修改! } } }
关于引用和重载
#include <iostream>
using namespace std;
void swap(int& a,int& b)
{
int temp = a;
a = b;
b = temp;
}
void swap(int a,int b)
{
int temp = a;
a = b;
b = temp;
}
int maim()
{
int a =1;
int b = 1;
swap(a,b); //程序会报错!因为虽然可以构成重载!但是编译器不知道应该调用那个函数!
return 0;
}
常引用
常引用 就是==const 类型& 引用名 = 目标变量名==
常引用往往涉及权限的平移,放大和缩小!
我们要明确一点!引用时候权限可以平移,可以缩小,但是不能放大!
int a = 10;
这个变量a的类型为int ,它的权限能读能写,我们既可以访问,也可以修改
const int a = 10;
这个变量的类型为 const int ,它的权限只能读不能写,我们只能对它进行访问,但是不能对该变量进行修改!
权限的赋予
当我们将一个引用进行初始化的时候就是将权限进行了一次赋予
int a = 10;
int& test = a;
==这时候 变量test和变量a对于同一块内存空间都有着相同的权限!==因为两者同为int类型!
int a = 10;
const int& test = a;
将int类型的变量,对常引用进行初始化,这就是典型的权限缩小
因为常引用初始化后就可以当成一般变量进行使用,但是const 前缀的变量不可以对内存数据进行修改,而原本的变量仍然可以,这就是权限缩小!
const int a = 10;
int& test = a;
**这就是权限放大!原本的变量为const int类型,初始化后只能对该变量进行访问但是不能修改!我们使用该变量对int&类型的引用进行初始化!就会发生权限放大!因为int类型的变量是被允许对内存进行修改和读取的!**这是不被允许的!会导致编译报错!
int a = 10;
const int& b = a;
b++;//报错!
a++;//可执行!
b++会报错是因为变量b的权限仅仅只有读,不能写!但是a的变量权限可读可写,所以a++这行代码是可执行的!虽然它们同属一块内存空间,但是权限的不同!而且变量b的权限虽然缩小,但是变量a的权限仍然是不变的!
常引用的实际价值
那么常引用究竟在实际的编程中右什么实际的价值呢?
==答案是当不涉及对于函数参数的修改的时候,常引用可以使该函数有更大的适用范围!更少的拷贝次数!==
void func_A(int& a, int& b) { //.... } void func_B(const int& a, const int& b) { //.... } int main() { int a = 0; int b = 0; const int c = 0; const int d = 0; func_A(a,b);//通过 func_A(c,d);//会发生报错 func_B(a,b);//通过 func_B(c,d);//通过! }
func_A会报错的原因也很简单,就是我们上面所说的权限放大,const int类型的权限小于int类型,所以不能接收!
但是我们也说过权限可以平移,可以缩小!所以使用更小权限的由const修饰的类型反倒拥有了更广泛的适用范围!所以func_B函数可以两者都适用!
同时也保留了引用具有更少的拷贝次数的优点!
常引用与临时变量
double a = 1.00;
int& b = (int*)a;
当我们第一次看到这个代码的时候,我们可能会觉得这没有问题啊?double强制转成int然后作为目标变量名将b初始化。
但是这个是个错误的代码!因为我们上文说过强制类型转换必产生==临时变量==,而临时变量具有==常性!==也就是说类型是const开头的!这就犯了权限放大的错误!
正确的代码应该是
double a = 1.00;
const int& b = (int*)a;
//下面这样也行
//const int&b = a;
//因为发生了隐形的强制类型转换!上面的是显性的强制类型转换!
//但是无论是隐形还是显性都会产生临时变量!
为了避免忘记,也请记住上文提到过的传值返回的是临时变量!具有常性!
int Count()
{
int n = 0;
n++;
return n;
}
int main()
{
int& a = Count();//显然这也是错误的!返回值的实际真正类型为const int!
return 0;
}
正确的代码为
int Count()
{
int n = 0;
n++;
return n;
}
int main()
{
const int& a = Count();
return 0;
}
常引用的常量初始化
我们已知引用必须初始化!但是是否能使用常量对于引用进行初始化呢?
int& a = 1;
上面的代码是错误的!为什么?
因为在c++中常量的类型为const int!还是老样子犯了权限放大的错误!所以解决办法也是一样的!
const int& a = 1;//这样就可以对使用常量对a进行初始化了!
常引用与缺省参数
void func(int& N = 10)
{
//...
}
由上面的结论我相信,读者们也可以看出了吧,我们已知缺省参数一定要是全局变量或者常数,当我们缺省值使用常数的时候,类型为 const int,该代码又发生了权限放大!所以还是一样的修改!
void func(const int& N = 10)
{
//...
}
结论
引用和指针的不同点
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- . 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何 一个同类型实体
- 没有NULL引用,但有NULL指针(就是说引用不可以使用NULL进行初始化!)
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
常引用的使用注意
- 注意权限的放大和缩小
- 注意强制类型转换,传值返回产生了临时变量!
- 注意常数的类型!
一点补充
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
int main()
{
int a = 10;
int& ra = a;
cout << "&a = " << &a <<endl;
cout << "&ra = " << &ra <<endl;
}
但是在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
int a = 10;
int& ra = a;
ra = 20;
return 0;
}
int main()
{
int a = 10;
int* pa = a;
*pa = 20;
return 0;
}