从C语言到C++②(第一章_C++入门_中篇)缺省参数+函数重载+引用(上):https://developer.aliyun.com/article/1513635
3.引用
3.1引用的概念
引用 不是新定义一个变量,而 是 给已存在变量取了一个别名,
编译器不会为引用变量开辟内存空间 ,它和它引用的变量 共用同一块内存空间。
比如你的真实姓名已存在, 你的小名或者外号就是一个别名。
语法:数据类型 & 引用名 = 引用实体;
这里的&可不是取地址啊!它是放在数据类型后面的 &,一定要区分开来!
代码演示:
#include <iostream> using namespace std; int main() { int a = 10; int& ra = a;//<====定义引用类型 //(这里取名为 ra,因为引用的英文是 reference,所以后面命名变量时会简写为 r,或者 ref 来代表引用) printf("%p\n", &a); printf("%p\n", &ra); cout << a << endl; cout << ra << endl; return 0; }
引用在语法层,我们要理解这里没有开新空间,就是对原来的取了一个新名称而已。
再次注意:
① 引用并不是新定义一个变量,只是给一个变量取别名。
② 编译器不会为引用的变量开辟内存空间,它和它引用的变量会共用同一块内存空间。
3.2引用的特性
1. 引用在定义时必须初始化
2. 一个变量可以有多个引用
3. 引用一旦引用一个实体,再不能引用其他实体
前两个好理解,第三个给出代码演示:
#include <iostream> using namespace std; int main() { int a = 10; int& ra = a; int b = 20; ra = b; // ? cout << a << endl; cout << ra << endl; cout << b << endl; cout << ra << endl; return 0; }
问号处是什么意思呢?这里是让 ra 变成 b 的别名,还是把 b 的值赋值给 ra 呢?
这里打印了四个20,所以是把 b 的值赋值给 ra 。
引用是不会变的,我们定义它的时候它是谁的别名,就是谁的别名了。
以后就不会改了,它是从一而终的!!!
引用和指针是截然不同的,指针是可以改变指向的:
平常这么写其实没什么意义:
int a = 10;
int& ra = a;
它真正有用的地方在于它能够做参数和做返回值。
3.3引用做参数
我们在C语言教学中讲过 Swap 两数交换的三种方式。
我们当时用的最多的就是利用临时变量去进行交换。
如果把它写成函数形式就是这样:
#include <iostream> using namespace std; void Swap(int* px, int* py) { int tmp = *px; *px = *py; *py = tmp; } int main() { int a = 10; int b = 20; cout << a << ' ' << b << endl; Swap(&a, &b); // 传址 cout << a << ' ' << b << endl; return 0; }
这里我们调用 Swap 函数需要传地址,
因为形参是实参的一份临时拷贝,改变形参并不会对实参产生实质性的影响。
但是,我们学了引用之后我们就可以这么玩:
#include <iostream> using namespace std; void Swap(int& ra, int& rb) { int tmp = ra; ra = rb; rb = tmp; } int main() { int a = 10; int b = 20; cout << a << ' ' << b << endl; Swap(a, b); // 这里既没有传值,也没有传地址,而是传引用 cout << a << ' ' << b << endl; return 0; }
是怎么做到交换的?
我们知道,形参是定义在栈帧里面的。
实际调用这个函数的时候,才会给 ra 和 rb 开空间。调用这个函数的时候,把实参传给形参。
那什么时候开始定义的?实参传给形参的时候开始定义的。
ra 是 a 的别名,rb 是 b 的别名,所以 ra 和 rb 的交换,就是 a 和 b 的交换。
因此,我们利用这一特点,就可以轻松实现两数的交换。
我们来梳理一下,顺带复习一下之前讲的函数重载。
在我们一共学了三种传参方式:传值、传地址、传引用。
#include<iostream> using namespace std; void Swap(int x, int y) { int tmp = x; x = y; y = tmp; cout << 1 << endl; } void Swap(int* px, int* py) { int tmp = *px; *px = *py; *py = tmp; cout << 2 << endl; } void Swap(int& rx, int& ry) { int tmp = rx; rx = ry; ry = tmp; cout << 3 << endl; } int main() { int a = 10; int b = 20; Swap(&a, &b); //Swap(a, b); // 报错 return 0; }
这里 Swap(a,b) 为什么会报错呢?
这三个 Swap 是可以构成函数重载的,
只要不影响它的函数名修饰规则,就不会构影响!
换言之,修饰出来的函数名不一样,就支持重载!
但是 Swap(a,b) 调用时存在歧义。调用不明确!
编译器不知道调用哪一个,是传值还是传引用,所以会报错。
当时再讲数据结构单链表的时候用的是二级指针,当时没有采用头结点的方式。
那么要传指针的地址,自然要用二级指针的方式接收。
现在我们学了引用,我们就可以试着用引用的方法来解决了(这里我们把 .c 改为 .cpp)
任何类型都是可以取别名的,指针也不例外:
int a = 10; int& ra = a; int* pa = &a; int*& rpa = pa
我们来看如何用引用的方法来实现!
#include <stdio.h> #include <stdlib.h> #include <assert.h> typedef int SLNodeDataType; typedef struct SingleListNode { SLNodeDataType data; // 用来存放节点的数据 struct SingleListNode* next; // 指向后继节点的指针 } SLNode; void SListPrint(SLNode* pHead); void SListPushBack(SLNode*& rpHead, SLNodeDataType x); // ... 略
SList.cpp:
#include "SList.h" /* 打印 */ void SListPrint(SLNode* pHead) { SLNode* cur = pHead; while (cur != NULL) { printf("%d -> ", cur->data); cur = cur->next; } printf("NULL\n"); } /* 创建新节点 */ SLNode* CreateNewNode(SLNodeDataType x) { //创建,开辟空间 SLNode* new_node = (SLNode*)malloc(sizeof(SLNode)); //malloc检查 if (new_node == NULL) { printf("malloc failed!\n"); exit(-1); } //放置 new_node->data = x; //存传入的数据 new_node->next = NULL; //next默认置空 return new_node; //递交新节点 } /* 尾插(指针的引用) */ void SListPushBack(SLNode*& rpHead, SLNodeDataType x) { //创建新节点 SLNode* new_node = CreateNewNode(x); //如果链表是空的 if (rpHead == NULL) { //直接插入即可 rpHead = new_node; } else { //找到尾结点 SLNode* end = rpHead; while (end->next != NULL) { end = end->next; //令end指向后继节点 } //插入 end->next = new_node; } }
解读: 这里的 SLNode* & rpHead 就是 pHead 的一个别名。
Test.cpp:
#include "SList.h" // 这里我们不传二级指针了。 //void TestSList1() //{ // SLNode* pList = NULL; // SListPushBack(&pList, 1); // SListPushBack(&pList, 2); // SListPushBack(&pList, 3); // SListPushBack(&pList, 4); // // SListPrint(pList); //} // 使用引用的方法: // 我们传 指针的 引用! void TestSList2() { SLNode* pList = NULL; SListPushBack(pList, 1); SListPushBack(pList, 2); SListPushBack(pList, 3); SListPushBack(pList, 4); SListPrint(pList); } int main() { TestSList2(); return 0; }
3.4 传值返回
这是我们以前的传值返回:
int Add(int a, int b) { int c = a + b; return c; } int main() { int ret = Add(1, 2); cout << ret << endl; return 0; }
这里 return 的时候会生成一个临时变量(c 为 3)
将 3 复制给这个临时变量,然后返回给 ret
如果我们直接把 c 交给 ret,就会出现一些问题。
如果直接取 c 给 ret,取到的是 3 还是 随机值,就要取决于栈帧是否销毁空间!
这个时候严格来说,其实都是非法访问了。
因为这块空间已经还给操作系统了,这就取决于编译器了。
有的编译器会清,有的编译器不会清,这就太玄学了!
所以,在这中间会生成一个临时变量,来递交给 ret 。
而不是直接用 c 作为返回值,造成非法访问。
所以这里不会直接用 c 作为返回值,而是生成一个临时变量。
那么问题来了,这个临时变量是存在哪里的呢?
① 如果 c 比较小(4或8),一般是寄存器来干存储临时变量的活。
② 如果 c 比较大,临时变量就会放在调用 Add 函数的栈帧中。
总结:所有的传值返回都会生成一个拷贝
(这是编译器的机制,就像传值传参会生成一份拷贝一样)
从C语言到C++②(第一章_C++入门_中篇)缺省参数+函数重载+引用(下):https://developer.aliyun.com/article/1513637?spm=a2c6h.13148508.setting.21.5e0d4f0emCh6wU