· CSDN的uu们,大家好。这里是C++入门的第五讲。
· 座右铭:前路坎坷,披荆斩棘,扶摇直上。
· 博客主页: @姬如祎
· 收录专栏:C++专题
目录
1. 知识引入
2. 引用的特性
2.1 引用在定义时必须初始化
2.2 一个变量可以有多个引用
3. 引用一旦引用一个实体,再不能引用其他实体
3. 常引用
编辑
4. 引用的使用场景
4.1 做参数
4.2 做返回值
5. 引用的底层实现
6. 引用与指针的区别
1. 知识引入
想必大家都还记得在用C语言实现单链表的时候的痛苦吧,什么二级指针,太难了!我们来看看当时的代码:
typedef struct ListNode
{struct ListNode* next;int val;
} ListNode;void ListPushHead(ListNode** pphead)
{//代码实现省略
}int main()
{struct ListNode* phead = NULL;ListPushHead(&phead);return 0;
}
因为形参只是实参的临时拷贝,形参的改变不影响实参。因此想要改变 phead 就必须要传 phead的地址过去。phead又是指针,所以就要传二级指针。真令人头大!
于是C++引入了新语法:引用。我们来看看引用的定义。
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。(这是在引用理解层面的定义,至于引用到底开不开空间,后面会讲解的)
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。
语法:
类型& 引用变量名(对象名) = 引用实体;
注意:引用类型必须和引用实体是同种类型的。
我们来看看有了C++的引用,上面的代码可以怎么写呢?
你可以将形参定义为这个结构体指针的引用,因为引用是变量的别名,结构体指针的引用那么就是这个结构体指针的别名,通过改变这个别名指向的内容就能改变结构体指针指向的内容。
typedef struct ListNode
{struct ListNode* next;int val;
} ListNode;void ListPushHead(ListNode*& pphead)
{//代码实现省略
}int main()
{struct ListNode* phead = NULL;ListPushHead(phead);return 0;
}
或者你还可以这么写:
将结构体 typedef 为 *ListNode,那么 ListNode 就是结构体指针,这样在形参的书写上会更加简洁。但是理解起来就有一点困难了。
typedef struct ListNode
{struct ListNode* next;int val;
} *ListNode;void ListPushHead(ListNode& pphead)
{//代码实现省略
}int main()
{struct ListNode* phead = NULL;ListPushHead(phead);return 0;
}
2. 引用的特性
我们再来看看引用的定义:
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
在理解引用时,你可以直接在脑海中呈现一张图:
有了这个图,我们来理解引用的特性。
2.1 引用在定义时必须初始化
我们可以看到下面的代码是编译失败的,原因就是引用在定义的时候没有初始化。至于为什么必须在定义的时候初始化,后面会讲解。
2.2 一个变量可以有多个引用
这个很好理解,一个变量当然可以有多个别名啦!就像一个人,他可能会有乳名,小名,大名等等。同样地,你可以在脑海中呈现那个图:
最后那条语句,我们让定义了一个变量 b 的引用,实际上 d 也指向 a 所指向的空间,因为 b 是 a 的引用嘛。既然多个变量指向可同一块空间那么,我们改变其中一个变量的值,其他的变量的值肯定也会发生相应的变化。上面的图可以很好地帮助大家理解。
int main()
{int a = 20;int& b = a;int& c = a;int& d = b;d = 30;cout << "a: " << a << endl;cout << "b: " << b << endl;cout << "c: " << c << endl;cout << "d: " << d << endl;return 0;
}
3. 引用一旦引用一个实体,再不能引用其他实体
这里可以先尝试记忆,原因后面才会提到。
我们来看下面的代码,c = b 看上去好像是让 c 指向 b 这个变量,但实际上只是将 b 的值赋值给了 c ,并不是改变了引用的实体。我们可以通过打印值来证明。
int main()
{int a = 20;int b = 30;int& c = a;c = b;return 0;
}
我们可以看到将 b 赋值给 c ,改变了 c 的值,也改变了 a 的值,这说明 c 还是变量 a 的引用,c = b 只是将 b 的值赋值给 c。
3. 常引用
我们来看上面的代码为什么会报错:const 修饰了变量 a,代表 a 指向的空间的内容不允许被改变。我们将变量 a 取别名为 ra,这个时候我们的操作权限就被放大了。因为引用变量没有加任何的限定符,表示通过引用变量 ra 能够修改 ra 指向的内容,而 ra 又是 a 的别名,则可推出 通过 ra能够改变 a 指向的内容。这就会与 const int a = 20;相冲突。
因此对于常量的引用,我们必须加上 const 限定符。不能让操作权限放大。
int main()
{const int a = 20;const int& ra = a; //加上 const 限定符,进制权限的放大return 0;
}
有人就可能会说了,既然不允许权限的放大,那允不允许权限的缩小呢?当然是可以的啦。
int main()
{int a = 20;const int& ra = a;return 0;
}
通过 const 限定符使得 ra 的权限缩小,只能读 ,不能修改。
4. 引用的使用场景
4.1 做参数
在知识引用里面就是引用做参数的例子呀!
下面的知识点会涉及函数栈帧的相关知识,不懂的老铁可以先瞅瞅:详解函数栈帧的创建和销毁。
我们知道在函数栈帧创建的过程中,实参向形参的传递会拷贝实参,但是拷贝实参必然会有时间的消耗。但是引用就比较特殊啦,因为引用是变量的别名,我们在理解的层次上,可以认为引用并不会开辟空间,所以在传递参数的时候,效率就会比不传引用高。下面的代码会一定程度上证明只一点:
struct A { int a[10000]; }; void Func1(A a) {}void Func2(A& a) {}int main()
{A a;int begin1 = clock();for (int i = 0; i < 100000; i++)Func1(a);int end1 = clock();int begin2 = clock();for (int i = 0; i < 100000; i++)Func2(a);int end2 = clock();cout << "传值花费的时间:" << end1 - begin1 << endl;cout << "传引用花费的时间:" << end2 - begin2 << endl;return 0;
}
4.2 做返回值
做返回值就和普通的变量没什么大的区别,但有一点值得注意:因为引用是变量的别名,将引用作为函数的返回值,我们必须确保该变量不能在这个函数调用完毕后就被销毁了,即是:不可返回局部变量的引用。
大家能猜到下面的代码的输出结果吗?(IDE:VS2019)
int Func(int a, int b)
{int d = 0;d = a + b;return d;
}int& Add(int a, int b)
{int c = 0;c = a + b;return c;
}int main()
{int& a = Add(10, 20);cout << a << endl;Func(20, 30);cout << a << endl;return 0;
}
下面来解释原因:
我们来看看Linux下的g++编辑器的结果:segmentation fault,段错误,非法访问内存。
切记:不可返回局部变量的引用。
还有一点就是:在返回值上引用也是不需要拷贝的,效率也是比直接返回值高好吧!这里就不再写代码演示了,代码和上面测试传参的相差不大。
5. 引用的底层实现
我们可以在调试的过程中查看汇编代码来观察引用的底层实现:
我们看到引用的底层实现还是C语言的指针啊!那么我们就可以解释为什么引用定义出来就必须初始化,引用的实体不能改变了!原因就是这个指针经过了 const 修饰,类似于这样:
int* const reference
因为 cosnt 修饰所以我们必须在引用定义的时候初始化。因为 const 修饰所以我们不能更改引用的实体。
但是,我们在平时的应用时,将引用理解为别名,不开辟空间即可,当有人问你引用的底层实现时,你知道就行了!这样能简化理解的难度。
6. 引用与指针的区别
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全
上面的这些东东在理解的基础上记忆即可!切不可死记硬背。
好了,引用就差不多讲完了,有什么不正确的地方欢迎大家的指正。