一 引子
相信很多铁子们在学习C语言的时候有一个概念很是令各位感到头疼,那就是指针。可能学了一个学期指针是什么也还搞不清楚,那么学习C++就是你的福音,因为C++给你提供了一个新的类型,那就是引用。
1:引用的基本概念
首先,我们可以简单粗暴的理解,引用就是一个数据的别名。
他的定义形式与指针类似,使用&(我们的老朋友,取地址符),上代码。
#include<iostream>
using namespace std;
int main()
{
int a=10;
int &y=a;
cout<<y<<endl;
return 0;
}
这段代码的意思其实就是定义了一个引用指向了a变量,那么我们就可理解为此时y是a的一个别名,对y操作就是对a操作。
2:引用的注意事项
(1)与指针一样,在定义阶段,&是声明一个引用的意思而不是取地址的含义,&前面没有数据类型的时候是取地址的意思。
(2)引用一经声明必须初始化(就是必须指明他是谁的别名)
(3)引用一经声明不可改变,上代码。
#include<iostream>
using namespace std;
int main()
{
int b=20,a=10;
int &y=a;
cout<<y<<endl;
y=b;
cout<<y<<endl;
return 0;
}
这里的y=b实质上是a=b的意思, 并不是改变y的引用为b 。故结果是10 20。
(4)可对引用再次引用。也就是说,一个变量的别名可以再有自己的别名,这样套娃下去(有点像指针里面的二级指针,三级指针)。
(5)引用是一种关系型声明,声明他和某一原有变量(实体)的关系,故而类型与原类型一致,并且不分配内存,与被引用的变量有相同的地址。
3:引用作为函数参数
1:交换数值
说了大半天引用的基本内容,我们来说一说为什么说引用可以让你对指针不再烦恼。
我们首先回忆我们在学习C语言的时候有一个经典的问题,就是如何实现两个数据数值的交换。
#include<iostream>
using namespace std;
void swap(int *a,int *b)
{
int t;
t=*a;
*a=*b;
*b=t;
}
void swap1(int &a,int &b)
{
int t;
t=a;
a=b;
b=t;
}
int main()
{
int a = 10;
int b = 20;
swap(&a,&b);
cout<<"a = "<<a<<' '<<"b = "<<b<<endl;
cout<<"-----------"<<endl;
a=10;
b=20;
swap1(a,b);
cout<<"a = "<<a<<' '<<"b = "<<b<<endl;
swap函数是我们熟知的函数的传址调用,而swap1是将a,b传递过去,用两个引用接收,操作起来看起来比指针简单多了。
看到这里不知你是否会有一个疑问,为什么不在主函数中声明两个引用,将引用当作参数传入函数,而是直接将a,b直接传递。
或者是说为什么swap1中函数参数是两个引用但是他们并没有初始化,为什么能够使用。
这是因为在调用这个函数的时候,函数接收到实参的传递后,形参就会初始化。
(2)结构体传参
#include<iostream>
using namespace std;
struct stu
{
int age;
char name[20];
};
void prints(stu &s)//stu &s == s1 引用的函数传参
{
cout<<s.age<<" "<<s.name<<endl;
}
int main()
{
stu s1={10,"zhangsan"};
prints(s1);
return 0;
}
这里注意对结构体的操作是.操作符而不是->操作符。因为s是s1的一个别名。
在这里也没有发生值拷贝的动作。
4:引用的实质
在上文中我们不断提到指针与引用有很多相似的地方,那么他们之间是否有某些特殊的关系呢
为了容易理解和印象深刻我们先看一段代码
#include<iostream>
using namespace std;
struct a
{
int *a;
};
struct b
{
int &a;
};
int main()
{
cout<<sizeof(struct a)<<endl;
cout<<sizeof(struct b)<<endl;
}
我们会发现,引用与指针所占用的大小是一样的。又结合引用必须初始化这与经const修饰的值也一样。所以我们可以大胆的推测,引用是一个常指针。
当我们将引用作为函数参数传递的时候,编译器会替我们将实参取地址给引用接收。
同样对一个引用操作赋值的时候,编译器会替我们隐藏进行取*操作。
我们在研究引用的时候可以当作一个常量指针这样去研究,但是编程的时候就当作一个值的别名就行。
常量指针
简单的举一个例子,我们定义一个数组,我们知道数组名其实就是一个指针。但是有没有想过他在哪里储存。比如int arr[10];真正在栈上开辟的是是个整形,而数组名其实在常量区的一个指针,指向这部分区域。
5:引用作为函数的返回值
既然引用的本质是一个指针,那么他跟指针在作为函数返回值的时候就有类似的特征。
引用作为返回值不要返回局部变量的引用,会出现栈上数据被释放的情况,而因返回一个不随函数结束生命结束的变量(堆上的变量 静态变量 全局变量)。
#include<iostream>
using namespace std;
int& geta()
{
int a=10;
return a;
}
int main()
{
int &main_a_re=geta();
cout<<main_a_re<<endl;
cout<<main_a_re<<endl;
return 0;
}
就像这个代码,编译器会报警告,提醒我们不应该返回一个局部变量a,因为geta中的a随着函数的结束而被销毁(因为是在栈区上开辟的,函数结束时会被操作系统自动回收)。故而第一次可以打印出来,正常出栈。而第二次打印,我们已经失去了对这块区域的访问权限,故而拿不到正确的a值。所以我们在用引用作为返回值的时候应该使用上文提到的变量形式。
正确操作如下,当然使用malloc等动态开辟的内存也是可以的,不过要记得释放。
#include<iostream>
using namespace std;
int& geta()
{
static int a=10;
return a;
}
int main()
{
int &main_a_re=geta()
cout<<main_a_re<<endl;
cout<<main_a_re<<endl;
return 0;
}
最后一点,既然返回值是一个引用(一个数据的别名)而不是一个数值,那么返回值是引用的函数是可以作为左值。而且引用返回一个函数值的最大好处是,在内存中不产生被返回值的值拷贝。
6:指针引用
现在有个问题,如何通过指针来改变此指针指向区域的值,要求用函数实现。
#include<iostream>
using namespace std;
//二级指针版
void change_a_1(int** p)
{
int a = 1000;
*p = &a;
}
//指针引用版
void change_a_2(int* &p)//拆开理解 int*类型的引用p
{
int a = 1000;
p=&a;
}
int main()
{
int* p = NULL;
change_a_1(&p);
cout << *p << endl;
change_a_2(p);
cout << *p << endl;
return 0;
}
首先我们定义一个指针变量,第一种方法是取指针p的地址当作参数,用二级指针接收。可这样的话对于指针掌握不熟练的铁子们就不太友好。所以我们选择对指针进行引用。
7:const+引用
1:如果想对一个常量进行引用, 必须是一个const引用。
2:相反如果一个普通变量,用一个const引用接收是可以的。
3:const 修饰引用,一般跟const修饰指针的用途是一样的。都是作为函数参数保证该参数是输入参数,是只读的,在函数内部该表不了外部的值。