前言
接上篇:【C 篇】迈入新世界的大门——初识C (上篇)
引用
引用的概念和定义
引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间, 它和它引⽤的变量共⽤同⼀块内存空间。
⽐如:⽔壶传中李逵,宋江叫"铁⽜",江湖上⼈称"⿊旋⻛";林冲,外号豹⼦头;
引用就是取别名!!!
定义方式
类型&引⽤别名=引⽤对象
C 中为了避免引⼊太多的运算符,会复⽤C语⾔的⼀些符号,⽐如前⾯的<<
和>>
,这⾥引⽤也和取地址使⽤了同⼀个符号&
,注意使⽤⽅法⻆度区分就可以。
#include <iostream>
using namespace std;
int main()
{
int a = 0;
// 引⽤:b和c是a的别名
int& b = a;
int& c = a;
// 也可以给别名b取别名,d相当于还是a的别名
int& d = b;
d;
// 这⾥取地址我们看到是⼀样的
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}
其实就是一个变量所在的内存空间给它起了不同的名字
引用的特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再也不能引用其他实体
#include<iostream>
using namespace std;
int main()
{
int a = 10;
// 编译报错:“ra”: 必须初始化引⽤
//int& ra;
int& b = a;
int c = 20;
// 这⾥并⾮让b引⽤c,因为C 引⽤不能改变指向,
// 这⾥是⼀个赋值
b = c;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
return 0;
}
- 比如在链表等数据结构,就只能用指针,因为引用无法改变指向(但是在Java中就只使用引用,其引用就可以改变指向)
引用的使用
- 引⽤在实践中主要是于引⽤传参和引⽤做返回值中减少拷⻉提⾼效率和改变引⽤对象时同时改变被引⽤对象。
- 引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。
void Swap(int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
}
int main()
{
int x = 0, y = 1;
cout << x <<" " << y << endl;
Swap(x, y);
cout << x << " " << y << endl;
return 0;
}
- 引⽤返回值的场景相对⽐较复杂,我们在这⾥简单⼀下场景,还有⼀些内容后续类和对象章节中会继续深⼊讲解。
我们在数据结构介绍了栈,具体的可以看【初阶数据结构篇】栈的实现(附源码)这篇文章
我们这里要取栈顶数据,想直接改变栈顶数据的值,就可以用传引用返回
代码语言:javascript复制#include<iostream>
using namespace std;
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void STInit(ST& rs, int n = 4)
{
rs.a = (STDataType*)malloc(n * sizeof(STDataType));
rs.top = 0;
rs.capacity = n;
}
// 栈顶
void STPush(ST& rs, STDataType x)
{
assert(ps);
// 满了, 扩容
if (rs.top == rs.capacity)
{
printf("扩容n");
int newcapacity = rs.capacity == 0 ? 4 : rs.capacity * 2;
STDataType* tmp = (STDataType*)realloc(rs.a, newcapacity *
sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
rs.a = tmp;
rs.capacity = newcapacity;
}
rs.a[rs.top] = x;
rs.top ;
}
// int STTop(ST& rs)
int& STTop(ST& rs)
{
assert(rs.top > 0);
return rs.a[rs.top];
}
int main()
{
// 调⽤全局的
ST st1;
STInit(st1);
STPush(st1, 1);
STPush(st1, 2);
cout << STTop(st1) << endl;//2
STTop(st1) = 1;
cout << STTop(st1) << endl;//3
return 0;
}
- 当然也可以用指针,但是显然引用更方便
int* STTop(ST& rs)
{
assert(rs.top > 0);
//return &(rs.a[rs.top - 1]);
return rs.a (rs.top - 1);
}
补充:
如果是写成下图左边形式->传值返回,编译会直接报错,C 在传值返回时会先把数据拷贝到一份空间中,这个叫做临时对象,C 规定临时对象具有常性,所以不能进行 =
操作,会报错。而传引用返回就跳过了这一拷贝步骤,直接返回变量的别名,就可以对变量进行修改了
所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象, C 中把这个未命名对象叫做临时对象。
- ⼀些主要⽤C代码实现版本数据结构教材中,使⽤C 引⽤替代指针传参,⽬的是简化程序,避开复杂的指针。
#include<iostream>
using namespace std;
typedef struct SeqList
{
int a[10];
int size;
}SLT;
void SeqPushBack(SLT& sl, int x)
{
//...
}
- 指针变量也可以取别名,这⾥
LTNode*& phead
就是给指针变量取别名,这样就不需要⽤⼆级指针了,相对⽽⾔简化了程序
typedef struct ListNode
{
int val;
struct ListNode* next;
}LTNode, *PNode;
//void ListPushBack(LTNode** phead, int x)
//void ListPushBack(LTNode*& phead, int x)
void ListPushBack(PNode& phead, int x)
{
PNode newnode = (PNode)malloc(sizeof(LTNode));
newnode->val = x;
newnode->next = NULL;
if (phead == NULL)
{
phead = newnode;
}
else
{
//...
}
}
int main()
{
PNode plist = NULL;
ListPushBack(plist, 1);
return 0;
}
总之:引⽤和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C 的引⽤跟其他语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的特点就是:C 引⽤定义后不能改变指向, Java的引⽤可以改变指向。
const引用
在这之前先看我们在C语言指针里讲过的:
代码语言:javascript复制#include <stdio.h>
int main()
{
const int a = 5;
int* p = &a;
*p = 4;
printf("%d", a);
return 0;
}
- 在C语言里它不会报错,只会报警告,可以成功运行,这里我们通过指针修改了
const
修饰的变量,按理来说是不合法的,但由于C的语法检查不是那么严,所以仍然可以运行
- 但如果将它改为
.cpp
,那就会报错了,C 的语法检查更为严格,不允许我们修改const
修饰的变量
注意:
下面这种情况不存在权限放大,因为const
修饰的是p5
本身而不是指向的内容
int b = 5;
int* const p5 = &b;
int* p6 = p5;
*p6 = 15;
在引用中也是类似的,总结就是:权限可以缩小,不能放大
- 可以引⽤⼀个
const
对象,但是必须⽤const
引⽤。const
引⽤也可以引⽤普通对象,因为对象的访问权限在引⽤过程中可以缩⼩,但是不能放⼤。
int main()
{
const int a = 10;
// 编译报错: “初始化”: ⽆法从“const int”转换为“int &”
// 这⾥的引⽤是对a访问权限的放⼤
//int& ra = a;
// 这样才可以
const int& ra = a;
// 编译报错: “ra”: 不能给常量赋值
//ra ;
// 这⾥的引⽤是对b访问权限的缩⼩
int b = 20;
const int& rb = b;
// 编译报错: “rb”: 不能给常量赋值
//rb ;
b ;//这是可以的
return 0;
}
- 不过需要注意的是类似
int& rb = a*3
;double d = 12.34
;int& rd = d;
这样⼀些场景下例如a*3
的结果保存临时对象中,int& rd = d
也是类似,在类型转换
中也会产⽣临时对象存储中间值,也就是时,rb
和rd
引⽤的都是临时对象,⽽C 规定临时对象具有常性,所以这⾥就触发了权限放⼤,必须要⽤常引⽤才可以。
#include<iostream>
using namespace std;
int main()
{
int a = 10;
const int& ra = 30;
// 编译报错: “初始化”: ⽆法从“int”转换为“int &”
// int& rb = a * 3;
const int& rb = a*3;
double d = 12.34;
// 编译报错:“初始化”: ⽆法从“double”转换为“int &”
// int& rd = d;
const int& rd = d;
return 0;
}
const
引用在传参时很有效:
如下:如果不是const
传参,那除了f1(a)
都要报错
void f1(const int& rx)
{
//..
}
int main()
{
const int xx = 20;
int a = 10;
const int& rb = a * 3;
double d = 12.34;
const int& rd = d;
f1(xx);
f1(a);
f1(a*3);
f1(d);
return 0;
}
指针和引用的关系
C 中指针和引⽤就像两个性格迥异的亲兄弟,指针是哥哥,引⽤是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有⾃⼰的特点,互相不可替代。
- 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
- **引⽤在定义时必须初始化,指针建议初始化,但是语法不是必须的 **。
- 引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。
- 引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
sizeof
中含义不同,引⽤的结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8字节)
int main()
{
int& r;
int* p;
char x = 'a';
char& rx = x;
cout << sizeof(rx) << endl;
char* ptr = &x;
cout << sizeof(ptr) << endl;
return 0;
}
- 指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些。
但是引用也会存在“野引用” 如下:
代码语言:javascript复制int& func()
{
int ret = 10;
ret ;
//...
return ret;//函数返回后销毁,此时此块空间已经还给操作系统,不能使用
}
只是相对来说更安全一些
补充:
在第一条我们是这么说的:
语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
代码语言:javascript复制但在实际的底层汇编代码来看,引用也是通过指针实现的
int main()
{
int x = 0;
int& rx = x;
rx = 1;
int* ptr = &x;
*ptr = 1;
return 0;
}
知道一下就可以了