目录
当我们学数据结构的时候一定会写单链表,但是很多同学在学习时很难理解为何要用二级指针,什么时候需要用二级指针,因此C++为了简化这种痛苦,提出了引用的概念,并用这个概念扩充出了很多十分方便的使用技巧。
概念
引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟新的内存空间,它和它引用的变量公用同一块内存空间,符号上是使用的 &
特点
1、别名与原变量地址相同
int main()
{
int k = 0;
int& rk = k;
cout << &k << endl;// &k是取地址 0x0012ZZ60
cout << &rk << endl;// &rk是取地址 0x0012ZZ60
}
注意
我们区分取地址和引用是用&跟着的内容,如果 & 是在定义变量时,在类型名后面,那就是引用,如果是&紧跟着变量,那么就是取地址
return &k;//取地址
void Swap(int& a, int& b);//引用
int& ri = &i;//ri前面的是&引用,i前面的&是取地址,这里的含义是为i的地址取别名
2、别名和原变量的值相同
int main()
{
int i = 0;
int j = i;//赋值
int& ri = i;//取别名
cout << &i << &j << &ri << endl;// j地址不同于i和ri,i和ri地址相同
printf("i = %d j = %d ri = &d\n", i, ++j, ri);// 0 1 0
printf("i = %d j = %d ri = &d\n", ++i, j, ri);// 1 1 1
printf("i = %d j = %d ri = &d\n", i, j, ++ri);// 2 1 2
//对i加ri变化,对ri加i变化
return 0;
}
3、一块空间可以有多个别名
int i = 0;// 原变量
int& ri = i;// 取别名1
int& k = i;// 取别名2
int& rri = ri;// 对别名取别名
他们的所有性质都相同,但使用场景不多(目前我所接触到的,如果以后学习、项目中发现使用挺多的话,我会修改博客的)
4、引用必须初始化
引用在第一次定义时,必须初始化,也就是说必须说明是引用的谁,这个特性也就保证了引用不会出现“空引用”这样类似于指针上的问题。
调用的情况形参的引用也就对应于实参,也就是类似于给到了一个初始化
int a = 0;
int& ra = a;//correct
int& rb;//err
5、引用对象不能改变
在我们定义引用变量后,引用的对象就确定下来,无法再重新引用其他的变量,这个特性与JAVA不同,这个特性使得,在C++中指针还有用武之地,比如改变节点的指向等情况。而在JAVA中规定,初始化后可以再引用其他变量,使得JAVA完全抛弃了指针。
//博主这里还没有想到什么例子能够用错,因为 & 被认为是引用的场景
//就只有是在类型名后面,也就是定义的时候会用到,是否有其他场景来造出这个错误,我还没想到
具体用法
一般引用都是用在函数传值以及作返回值上,想想一下以前C语言很多需要传递指针的地方都可以用引用来替代,并且在使用别名的时候我们也不需要进行取地址等固定操作,简单来说int a; int& ra = a
a是ra,ra也是a,下面我们来简单看几个例子
简单实例
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
void PushFront(Node* &phead, int x)//单链表
{
Node* newnode = (Node*)malloc(sizeof(Node));
if (phead == nullptr)
{
phead = newnode;
return;
}
newnode->data = x;
newnode->next = phead;//C语言用**pphead 那么这里就需要*pphead
phead = newnode;//同上注释
}
作参数
第一种常用情况:作函数的参数,这种情况其实就是上面的简单例子,这些例子里都有个共同特点,是输出型参数,也就是说这些参数在函数外面还需要使用,需要通过这个函数来改变这些参数。
除此之外,和指针相同,都可以提高效率,主要是针对大对象(占用空间很大的struct class),深拷贝的类对象(博主暂时还没学到,后面学到了会进行补充)。
我对后面这种情况补充一个例子:
#include<time.h>
struct BIG
{
int score[10000];
char name[20000];
};
void Test(BIG x)
{ ; }
void Test_R(BIG& x)
{ ; }
void Test_P(BIG* x)
{ ; }
int main()
{
BIG data;
int begin1 = clock();
for (int i = 0; i < 500000; i++)
{
Test(data);
}
int end1 = clock();
int begin2 = clock();
for (int i = 0; i < 500000; i++)
{
Test_R(data);
}
int end2 = clock();
int begin3 = clock();
for (int i = 0; i < 500000; i++)
{
Test_P(data);
}
int end3 = clock();
printf("Test: %ds %dms\n", (end1 - begin1) / 1000, (end1 - begin1) % 1000);
printf("Test_R: %ds %dms\n", (end2 - begin2) / 1000, (end2 - begin2) % 1000);
printf("Test_P: %ds %dms\n", (end3 - begin3) / 1000, (end3 - begin3) % 1000);
return 0;
}
因此引用也可以极大的提高效率,其效率和指针几乎相同
在C++中偏向于用引用,代码看着会更简洁,让程序员更加的关注逻辑,而不是在语法上思考良久。
作返回值
前提知识——编译器创建的临时变量
C++在很多情况下编译器会自动产生临时变量,这些临时变量的值都具有常性(诱值),常性可以理解为临时变量是用const修饰过的,而这个特性会在常引用与权限部分用到
并且产生临时变量的阶段是在编译阶段,通过查看汇编可以看到这些临时变量
C++产生临时变量
情况列举:
1、函数的返回值是非引用类型(不管变量是全局的、局部的、静态的)时,会产生一个临时变量,把这个临时变量传值给函数外面进行接收
int Add(int a, int b){return a + b;} int ret = Add(1, 2);
2、显式类型转换:强制类型转换,用临时变量存储强制转换后的值,然后把临时变量用来赋值int a = (int)dd;或者使用printf(“%d”, (int)dd);
3、隐式类型转换:算数转换(int + double -> double + double)、整形提升(char + int -> int + int)、类型提升( float a = 1.2f; double b = a; )都会创建临时变量
4、截断,临时变量会存储截断后的值,然后把截断后的值进行赋值或者使用
5、初始化操作:创建一个默认值为0的临时对象,int value = int();
用法细则
在函数返回值时,如果我们不想生成临时变量,就用引用做返回值(一般需要是全局、静态、malloc、new、常量 或者 输入型参数,这些情况下,离开函数后,变量地址还是我们的,生命周期还没有结束,还没有还给操作系统,还能够正常使用)
因此引用的作用一:减少拷贝、提高效率
#include<time.h>
typedef struct BIG
{
int score[10000];
char name[20000];
}BIG;
struct BIG Test(BIG a)
{
return a;
}
struct BIG& Test_R(BIG& a)
{
return a;
}
struct BIG* Test_P(BIG* a)
{
return a;
}
int main()
{
BIG data;
int begin1 = clock();
for (int i = 0; i < 500000; i++)
{
Test(data);
}
int end1 = clock();
int begin2 = clock();
for (int i = 0; i < 500000; i++)
{
Test_R(data);
}
int end2 = clock();
int begin3 = clock();
for (int i = 0; i < 500000; i++)
{
Test_P(&data);
}
int end3 = clock();
printf("Test: %ds %dms\n", (end1 - begin1) / 1000, (end1 - begin1) % 1000);
printf("Test_R: %ds %dms\n", (end2 - begin2) / 1000, (end2 - begin2) % 1000);
printf("Test_P: %ds %dms\n", (end3 - begin3) / 1000, (end3 - begin3) % 1000);
return 0;
}
但如果返回值是局部变量时,在离开函数后,局部变量被销毁,此时函数外部接收到的引用值不确定,所以我们需要避免这种情况,以下为一些错误用例及分析:
int& Count()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = Count();//ret和n是拷贝关系
//这里打印ret的值不确定:
//如果Count函数结束后,栈帧销毁,编译器未清理栈帧,那么ret就是n
//但是编译器清理了栈帧,那么ret是随机值
cout << ret << endl;
return 0;
}
int& Count(int x)
{
int n = x;
n++;
return n;
}
int main()
{
int& ret = Count(10);//ret和n是引用关系
cout << ret << endl;//11/随机值,理由同上
cout << ret << endl;//21/随机值
return 0;
}
也就是说主要看编译器平台的处理
实例
//任意插
int& Insert(SeqList& ps, int pos)
{
assert(pos < MAX && pos >= 0);
return ps->a[pos];//返回位置
}
int main()
{
SeqList s;
Insert(&s, 0) = 1;//给第一个位置赋值1
cout << Insert(&s, 0) << endl;
Insert(s, 1) += 1;//给第二个位置加1
return 0;
}
常引用和权限
C++中的常引用是指被声明为const的引用类型。在函数参数传递和返回值时,使用常引用可以避免不必要的内存复制和修改操作,同时也能保证函数不会对原始数据进行修改。常引用通常用于访问非本地变量或临时对象,并将其传递给函数。与普通引用相比,常引用被声明成const,不能用于修改所引用的对象的值
在引用中,会涉及到:权限放大、权限平移、权限缩小三种情况,只有后两者被允许,而放大是被禁止的,其原因可以理解为,别名后的变量天然的权限低于原始变量。
int main()
{
int a = 1;
int& b = a;//权限平移,a可以影响b,b可以影响a
return 0;
}
int main()
{
const int a = 0;
int& b = a;//err, 权限放大,不被允许
return 0;
}
int main()
{
int x = 0;
int& y = x;//权限平移,x影响y,y影响x
const int& z = x;//权限缩小,x可以影响z,z不能影响x (x是管理员 z是普通用户)
x++;
z++;//err
return 0;
}
int main()
{
const int& x = 10;//权限平移,10是常量,无法改变,x是const修饰的,也无法改变
int& y = 20;//err,权限放大
return 0;
}
int main()
{
double d = 1.1;
int i = d;//隐式类型转换
int& ri = d;//err,这里发生了隐式类型转换,编译器会创建一个具有常性的临时变量,让这个临时变量传值给ri,因此这里就是权限放大
const int& rri = d;//权限平移
return 0;
}
int test()//注意这里返回值不是引用
{
static int x = 0;
return x;
}
int main()
{
int& ret1 = test();//err,权限放大,test返回的是非引用类型,所以会创建临时变量,临时变量具有常性,常性变量放到可以变化的ret1中,err
const int& ret2 = test();//权限平移,常性到常性
int ret3 = test();//普通的拷贝,没有权限问题
return 0;
}
int& test()
{
static int n = 0;
return n;
}
int main()
{
int& ret1 = test();//权限平移
const int& ret2 = test();//权限缩小
}