2015年1月25日
周日
阴雨
昨天因为评测了win10就没有更新笔记,今天刚好周日时间比较多,就好好来说下“
引用”吧。
个人觉得引用还是有很多知识的,也有很多值得注意的地方。
——————————————————————————华丽的分割线——————————————————————————————————
一、什么是引用?
引用,即
别名。
什么是别名?举例:关云长、关二爷都是关羽的别名;9527、华安都是唐伯虎的别名等等。
虽然是别名,但最终都是同一个东西。如上左图,b和c都是a的引用,也即是别名(同右图)。
形如:int& b = a,c = a;
b 和 c就是a的引用。也就是在变量名前加上小麻花一样的符号“&”。
二、引用的使用
1.引用的定义:
格式:数据类型 & 对象名 = 目标对象;
如:int & a = 100; const string & c = "hello world";
2.引用必须
初始化!
int& b; //
error!!!千万不要这么写
int& b = 20;
3.引用不能为
空!
int& a = NULL; // error!!!
4.引用不能
更换目标!
int& a = 10;
引用就是一个对象的别名,所以在内存中只有一份,所以引用是不占内存的。不信?看程序:
#include <iostream>
using namespace std;
int main()
{
int a = 2;
int& b = a;
cout<<&a<<" "<<&b<<endl;//打印a和b的地址
b = 100;<span style="white-space:pre"> </span> //通过b改变a的值
cout<<a<<" "<<b<<endl; //打印a和b的值
return 0;
}
由程序我们很好的证明了这一点:引用只是一个变量的别名,本质上是同一个东西,是没有自己的内存空间的。
三、引用类型的参数
1.引用型参数
让你写一个最简单的程序,交换a和b的程序。你肯定马上就能写出来,那让你用调用子函数的方法交换呢?你说,简单,看程序:
#include <iostream>
using namespace std;
void swap1(int a,int b){
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
int main()
{
int a = 100,b = 20;
swap1(a,b);
cout<<a<<" "<<b<<endl;
return 0;
}
结果一运行就傻眼了,怎么没有交换呢?如果你有这样的疑问,说明你对函数还不够理解,你还不能很好的区分实参和形参的区别(建议回去好好补补课本吧)。懂的人应该知道,swap1接受的两个形参a和b的值的确是交换了。可是形参只是实参的 副本,并不是实参本身,就算他们的值发生了改变也不会影响实参。
可我们上面说的引用就不一样了。如果我们使用引用类型的参数,那我们操作的形参,实际就是在操作实参!所以这里引入一个概念:
引用型参数。
我来帮大家写一下吧:
#include <iostream>
using namespace std;
void swap1(int& a,int& b){
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
int main()
{
int a = 100,b = 20;
swap1(a,b);
cout<<a<<" "<<b<<endl;
return 0;
}
我们注意看swap1这个子函数。他的两个参数都是
引用类型的参数。仅仅只是在形参前加上了&号就达到了我们的预期效果
在函数中改变实参的值。这只是引用型参数的一个好处,他还有一个好处:
避免对象复制带来的开销!看程序:
#include <iostream>
using namespace std;
void acc(int a,int& b){
cout<<&a<<" "<<&b<<endl;
}
int main()
{
int a = 100,b = 200;
cout<<&a<<" "<<&b<<endl;
acc(a,b);
return 0;
}
子函数中的a是形参,是主函数中a的一个副本,所以两个变量的地址不一样。既然是副本,就会发生对象的复制,开辟新的内存空间并且赋值,由此可见开销如何。但是子函数中的b是引用型的形参,实际就是主函数中b的别名,本质上是同一个东西,所以他们的地址一样。说明程序直接拿b过来用,这就省去了上面所说的复制带来的开销啦。
2.常引用型参数
但是我们还得提一个概念:
常引用型参数。(const修饰的引用型参数)
格式:const 类型& 变量名= 值;
有时候你可能想把实参的值传入函数但不改变实参的值,又能避免复制带来的开销,你就可以选用常引用型参数。例如:
#include <iostream>
using namespace std;
void acc(int& a,const int& b){
a = a+b;
// b = 100;//error,对b的修改是非法的
}
int main()
{
int a = 100,b = 200;
acc(a,b);
cout<<a<<" "<<b<<endl;
return 0;
}
acc函数中的形参b就是:常引用型参数。既可以不改变b的值,也可以省去复制带来的开销。
当然,常引用型参数的第二个作用是:
接受常量型的实参。这也是一种兼容性的考虑,我们来举例说明:
#include <iostream>
using namespace std;
void acc(int& a){}
int main()
{
const int a = 100;
acc(a);
return 0;
}
acc函数的形参a只是一个普通的引用,但是主函数中的a却是const int类型的,编译器不干了,它说:你不能引用const int的实参!这里我们只要把形参改为:const int & a编译器就会放过你了。在实际编程中,你可能会接受的参数const类型的变量,使用 常引用型参数就既可以接收普通变量和常属性的变量,这其实是出于一种兼容性的考虑。
四、引用类型的返回值
在说这个知识点之前,我们先来回顾一下c语言的函数返回值类型能干嘛:c语言中
非指针类型的函数返回值
只能作为
右值,不能作为
左值。
不知道大家了不了解左值和右值,我还是简单的讲下吧。说简单点,能放在等号左边的就叫左值,只能放在等号右边的就叫右值。
比如:我们可以写a=123;但是不能写123=a。这里,123就是一个右值,a就是一个左值。一般的,不能改变的都是右值,一般的变量都能做左值。我们还是继续体会第一句话吧:
我们可以看见只有13行报错,因为add函数返回的是int类型而不是指针类型,所以编译器报错了,它说add(2,3)不是一个左值。
但fun函数返回的是int*类型的指针。这就证明了那就话:c语言中
非指针类型的函数返回值
只能作为
右值,不能作为
左值。
我们回到正题,先给出一个定论吧:c++ 中如果一个函数
返回引用类型,则代表这个函数的返回值可以作为
左值。
我先来考你一个题目吧,写出下列函数的输出结果:
#include <iostream>
using namespace std;
int& imax(int& a,int& b){
return a>b?a:b;
}
int main()
{
int x = 1,y = 2;
imax(x,y) = 100;
cout<<x<<" "<<y<<endl;
return 0;
}
我们来看看你的答案对不对吧:
这段程序的功能就是把两个数中大的那个数字的值变成100。因为,imax函数返回的是引用型形参a和b中值最大的那个变量的引用。上面那句话说了,引用型的返回值可以作为左值,所以将这个值修改为100。懂了不?
但是,一定要注意这个但是!!!并不是所有类型都能作为引用型返回值的!!!!
为什么呢?因为引用型的返回值,从函数中返回的目标的引用,一定要保证在函数返回以后,该引用的目标依然有效。换句话说,就是你返回的这个东西,要在函数结束之后仍然存在(不会因为生命周期结束而在内存中被释放了)。
因此,
千万不要返回局部变量的引用!
因为,局部变量在函数结束之后因为生命周期结束,而被内存释放了,内存中已经没有这个变量了。在前面我们就说过了引用本身没有内存,不能为空。局部变量本身都被消灭了,这个引用也跟着没有了。
那么,我们能返回哪些类型的变量的引用呢?
- 全局、静态、成员变量的引用
- 在堆中动态创建的对象的引用
- 引用型参数本身
我们把他们三个都写出来你就明白了:
#include <iostream>
using namespace std;
int g_int = 1024; //全局变量
struct Count{
int num;
int& add(){
return ++num; //返回成员变量的引用
}
}; //结构体对象
int& g_fun(){
return g_int; //返回全局变量的引用
}
int& s_fun(){
static int a = 2048;
return a; //返回静态变量的引用
}
int& n_fun(){
return * new int(512);
}
int& r_fun(int& num){
return num;
}
int main()
{
int& a = g_fun(); //接受返回的全局变量的引用
cout<<a<<endl;
int& b = s_fun(); //接受返回的静态变量的引用
cout<<b<<endl;
int& c = n_fun(); //接受返回的动态建立的变量的引用
cout<<c<<endl;
int tmp = 256;
int& d = r_fun(tmp);//接受返回的引用变量本身的引用
Count cn;
cn.num = 0;
++cn.add();
cout<<cn.num<<endl;
return 0;
}
还有不懂的,欢迎评论留言,我尽量解答。
五、引用与指针:
1.在实现层面,引用就是指针。
引用之所以拥有这么强大的功能,都是因为
引用是通过指针实现的。
你可以自己想一想,引用是不是一种 可以修改所指向的值 但是不能修改所指对象 的指针?其实我们可以运用 * 和 const,模拟一下引用的实现。
你能说出const int* 和 int* const的 区别吗?
你还天真的以为他们是一样的?那你就错了。看程序:
我们可以观察一下编译器报错的位置:分别是第8行和第12行。
编译器说,*a这个指针是只读(read-only)的,b这个变量是只读的。
仔细观察一下:对a的操作,第8行是改变它所指向的值(报错),第9行是改变它的指向(没报错)。说明如果const在*之前的话,我们不可以改变指针所指向的变量的值但是能改变指针的指向。
对b的操作,第11行是改变它所指向的值(通过),第12行是改变它的指向(报错)。说明如果const在*之后的话,可以改变指针指向的值但是不能改变指针的指向。是不是很像我们所说的引用?
其实,引用就类似于
Type* const
PS:学了编译原理的话我们知道,最右推导是规范推导,所以编译器判别一个变量的类型的时候是从右往左看到的。
比如:const int* a:编译器首先知道这个变量叫做a,然后读到”*“说明这是个指针,再读到”int“说明这是个整型的指针,最后读到”const“就下结论:这是一个常量型的指针,到头来还是个指针,只不过指向的是常量。所以地址可以改变,指向的值不能改变。
又如:int* const a:编译器首先知道这个变量叫做a,然后读到”const“知道这个变量是个常量,再读到”*“说明这是个指针型的常量,最后读到”int“就知道了,a是一个指针型的常量,说到底这是一个常量。所以这个指针的值(指针的地址,即指针的指向)不能变,但是指针所指向的值随你便。
2.在语言层面,引用不是实体类型,因此与指针存在明显的差别。
这里我们就要谈谈引用和指针的区别了:
(1)指针可以不初始化,其目标可在初始化后随意变更 (除非是指针常量),而引用必须初始化,且一旦初始化就无法变更其目标
这个我们在最开始就强调了,就不在过多的解释了。
(2)存在空指针,不存在空引用
最开始我们也说明了这一点。
(3)存在指向指针的指针,不存在引用引用的引用。
看看程序你就懂了:
(4)存在引用指针的引用,不存在指向引用的指针
依然来看程序:
编译器说了:不能定义指向int& 的指针。
(5)存在指针数组,不存在引用数组,但存在数组引用。
一切代码说了算!
编译器又说了,你定义了引用数组是不对滴!
关于数组引用,我想多说几句,因为这个很实用的哦。
比如,给你一个数组,怎么求数组的大小呢?
你肯定会想到这么做:sizeof arr / sizeof arr[0]。当然这么做没错,但是把数组传到子函数中能这么做吗?且看代码:
为什么在主函数里面显示的大小是3,在子函数里面显示的就是1呢?那是因为你在主函数定义的,arr数组名代表的是数组整体,而你把arr作为参数传到子函数中之后arr数组名仅仅代表数组的首地址。那我们如何在子函数中也能求数组大小呢?这就需要用到数组引用,把数组作为整体传入到子函数中去。且看代码:
#include <iostream>
using namespace std;
void ssize(int (&arr)[3]){
int len = sizeof arr / sizeof arr[0];
cout<<len<<endl;
}
int main()
{
int arr[3] = {1,2,3};
int len = sizeof arr / sizeof arr[0];
cout<<len<<endl;
ssize(arr);
return 0;
}
我们将sszie子函数中的形参改为int (&arr)[3],这是个数组引用,本质上是个引用,所以可以代表数组整体。
————————————————————————————————华丽的分割线———————————————————————————————————
不知道看完这篇之后你有什么收获呢?还有什么疑问呢?都欢迎评论给我留言。
如有不正确的地方还望各位不吝赐教啊!
引用还是很实用的,以后编程会经常用到,所以掌握它是很有必要的。