
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
[2.1 表达式的引用](#2.1 表达式的引用)
[2.2 类型转换的引用](#2.2 类型转换的引用)
前言
在上一篇文章C++入门基础指南:输入输出、缺省参数与函数重载,我们主要是讲解了C++入门基础中的输入输出、缺省参数以及函数重载,今天我们会结束C++的入门基础,将最后剩下的入门基础知识全部讲解完。
一、引用
1、引用的概念和定义
概念:引用不是新定义一个变量,而是给已存在变量取了一个别名 ,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
定义:类型& 引用别名=引用对象;
C++中为了避免引入太多的运算符,会复用C语言的一些符号,比如前面的 << 和 >> ,这里引用也和取地址使用了同一个符号 & ,大家注意使用方法角度区分就可以。
cpp
//Test.cpp
#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;
// 这里取地址我们会发现是一样的,这也印证了a和其引用的变量共用同一块内存空间
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
cout << a << endl;
cout << b << endl;
cout << c << endl;
cout << d << endl;
return 0;
}

并且我们会发现:我们对 d 进行 ++d 时,a、b、c、d均变成了1,这不由让我们想到了在C语言中学习到的一个比较难以理解的东西:地址传参。
二、引用的特性
引用在定义时必须初始化
cpp
//Test.cpp
int main()
{
int a = 10;
int& ra;
return 0;
}

一个变量可以有多个引用
这个特性在上面已经提到过了,相当于就是一个变量可以有多个别名。
引用一旦引用一个实体,再不能引用其他实体
cpp
//Test.cpp
int main()
{
int a = 10;
int& b = a;
int c = 20;
// 这里并非让b引用c,因为 C++ 引用不能改变指向,
// 这里是一个赋值
b = c;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl; //a与b的地址仍然是相同的,而c是另一个变量
return 0;
}

三、引用的使用
引用传参跟指针传参功能是非常类似的,只是引用传参会相对更方便一些
之前第一次接触学习地址这个东西的时候想必大家或多或少都有点难以理解,甚至有时分不清到底什么时候用值传参什么时候用地址传参,归根结底我们就是要搞清楚所用的变量需不需要进行修改,如果需要进行修改则就要传其地址。
但是引用这个东西相当于直接和地址联系在一起 ,由于别名和变量的地址 自动就是相同的,所以在一些函数调用中我们如果想要变量进行修改则无需再利用地址传参了,引用就可以解决:
cpp
void Swap(int& rx, int& ry) //引用传参:rx相当于就是变量x的别名,ry相当于就是变量y的别名
{
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;
}

由于形参 rx 和 ry 分别就是实参 x 和 y 的别名,而两者本来地址就是相同的,所以在函数内部对形参进行修改,外部的实参也会发生变化。
说到底,引用传参跟指针传参功能是非常类似的,只是引用传参会相对更方便一些。
引用在实践中第一 主要是于引用传参 中能减少拷贝提高效率
不仅在函数调用中可以使用引用,之前我们所学的数据结构中,对于栈的相关方法实现,前面我们都是利用指针来实现代码,引用也能解决:
cpp
//Stack.h
#include<iostream>
#include<assert.h>
#include<stdlib.h>
using namespace std;
typedef int STDataType;
typedef struct Stack
{
STDataType* arr;
int top;
int capacity;
}ST;
//栈的初始化
void STInit(ST& rs, int n = 4); //可以将缺省参数和引用同时使用,将大大提高代码效率
//入栈
void STPush(ST& rs, STDataType x);
//Stack.cpp
#include "Stack.h"
//栈的初始化
void STInit(ST& rs, int n)
{
rs.arr = (STDataType*)malloc(sizeof(STDataType) * n);
rs.top = 0;
rs.capacity = n;
}
//入栈
void STPush(ST& rs, STDataType x)
{
assert(&rs);
if (rs.capacity == rs.top) //栈满了
{
int newcapacity = rs.capacity == 0 ? 4 : 2 * rs.capacity;
STDataType* tmp = (STDataType*)realloc(rs.arr, sizeof(STDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
rs.capacity = newcapacity;
rs.arr = tmp;
}
rs.arr[rs.top] = x;
rs.top++;
}
//Test.cpp
#include "Stack.h"
int main()
{
//调用全局的
ST st1;
STInit(st1);
STPush(st1, 1);
STPush(st1, 2);
return 0;
}
引用在实践中第二 主要是于引用做返回值 中能改变引用对象时同时改变被引用对象
在之前我们学习栈的一个方法出栈时,我们只是知道这个函数能让我们得到栈顶的数据,但是当我们想要对栈顶数据修改时,我们其实并不能直接通过这个函数来实现:
cpp
//Stack.h
//出栈
int STTop(ST& rs);
//Stack.cpp
int STTop(ST& rs)
{
assert(rs.top > 0);
return rs.arr[rs.top - 1];
}
//Test.cpp
#include "Stack.h"
int main()
{
//调用全局的
ST st1;
STInit(st1);
STPush(st1, 1);
STPush(st1, 2);
STTop(st1) = 3;
return 0;
}

当我们函数调用后得到的值进行修改则会报这样的错误,原因是:当这个函数执行完代码 return 返回时,会有一个类似临时变量 的东西先把返回值存起来 ,再通过这个临时变量把数据作为 STTop(st1) 这个表达式的返回值,而这个临时变量有一个特性就是:临时变量具有常性 。可以理解为类似我们C语言中学习到的一个关键字const ,由于具有常性,所以是不能对其进行修改的。如果我们想要对栈顶数据进行修改则需要再定义一个新函数来实现,这样肯定相对麻烦点。
所以此时,引用做返回值 就能解决这个问题:
由于是引用做返回值,则函数执行完后传的是返回值的别名 ,相当于就是没有了临时变量 这个东西,也就使其不具有常性 了,这也就是说为什么引用做返回值能改变引用对象时同时改变被引用对象 ,引用对象 就是函数的返回值 ,而被引用对象 就是栈。
cpp
//Stack.h
//出栈
int& STTop(ST& rs);
//Stack.cpp
int& STTop(ST& rs)
{
assert(rs.top > 0);
return rs.arr[rs.top - 1];
}
//Test.cpp
#include "Stack.h"
int main()
{
//调用全局的
ST st1;
STInit(st1);
STPush(st1, 1);
STPush(st1, 2);
cout << STTop(st1) << endl;
STTop(st1) = 3; //如果不是引用做返回值则不能对其进行修改值,因为表达式是不可修改的左值
//引用做返回值则可以直接对其进行修改
cout << STTop(st1) << endl;
return 0;
}

引用返回值的场景相对比较复杂,我们在这里简单讲了一下场景,还有一些内容后续类和对象章节中会继续深入讲解。
引用和指针 在实践中相辅相成 ,功能有重叠性,但是各有特点 ,互相不可替代 。C++的引用跟其他语言的引用(如Java)是有很大的区别的,除了用法,最大的点,C++引用定义后不能改变指向 ,Java的引用可以改变指向。
四、const引用
1、const对象的引用
可以引用一个 const 对象,但是必须用 const 引用。const 引用也可以引用普通对象 ,因为对象的访问权限 在引用过程中可以缩小 ,但是不能放大。
在C语言的学习中我们就接触了这个关键字 const,const可以使一个变量只可读不可写 ,也就是不可被修改。那权限放大和权限缩小又是什么意思呢?
cpp
//Test.cpp
int main()
{
const int a = 10;
int& ra = a;
return 0;
}

这个报错的原因说简单点就是权限放大,首先我们知道引用对象和被引用对象两者没有区别,只是引用对象是被引用对象的别名罢了,所以两者其实是平起平坐的。
而上述代码中 a 变量 是只可读不可写 的,但是 ra 作为 变量 a 的别名,却没有 const 的约束,也就是说 ra 是既可以读也可以写 的,这样不就出现问题了吗,本来 ra 只是 a 的一个别名,却把权限从只可读不可写 变成了既可以读也可以写 ,这就叫做权限放大。
而如果想要让别名 ra 和 a 平起平坐,就需要利用const引用了:const int& ra = a;
cpp
//Test.cpp
int main()
{
int b = 20;
const int& rb = b;
b = 10;
rb = 10;
return 0;
}

当一个变量没有 const 约束也就是说既可以读也可以写时,而被 const 引用 了,也就使其别名 rb 变成了只可读不可写,这就叫做权限缩小 。
但需要注意的是,由上述代码我们发现只有引用对象 rb 被限制为只可读不可写 ,但被引用对象 b仍然可以进行修改。
2、普通对象的引用
2.1 表达式的引用
需要注意的是类似 int& rb = a*3; 这样一些场景下也就是表达式 时,a * 3 的结果会先保存在一个临时对象 中,而C++规定临时对象具有常性 ,所以这里就触发了权限放大 ,必须要用const引用才可以。
错误示范:
cpp
//Test.cpp
int main()
{
int a = 10;
int& r = 30; //30为常量,也具有常性,引用时如果不用const也相当于权限放大,所以需要const引用
int& rb = a * 3;
//a * 3 为表达式,表达式的引用会存在临时对象,C++规定:临时对象为常性,所以也需要const引用
return 0;
}

正确示范:
cpp
//Test.cpp
int main()
{
int a = 10;
const int& r = 30;
const int& rb = a * 3;
return 0;
}
2.2 类型转换的引用
和上面表达式的引用一样,类似 double d = 12.34; int& rd = d; 这样一些场景下在类型转换 中也会产生临时对象 存储中间值,临时对象具有常性,所以这里也触发了权限放大 ,必须要用const引用才可以。
错误示范:
cpp
//Test.cpp
int main()
{
double d = 12.34;
int& rd = d;
return 0;
}

正确示范:
cpp
//Test.cpp
int main()
{
double d = 12.34;
const int& rd = d;
return 0;
}
并且如果存在临时对象,则rd 为临时对象的别名而并非 d 的别名 ,因为我们知道引用的两者只是名字上的不同,类型和地址都是完全一致的,而d 和 rd 本身的类型就不同,所以 rd 不是 d 的别名,并且通过地址打印我们也可以证明:
cpp
//Test.cpp
int main()
{
double d = 12.34;
const int& rd = d;
cout << &d << endl;
cout << &rd << endl;
return 0;
}

所谓临时对象 就是编译器需要一个空间暂存表达式的求值结果 时临时创建 的一个未命名的对象,C++中把这个未命名对象叫做临时对象。
所以到现在能存在临时对象的情况我们知道了三种:(1)引用做函数的返回值;(2)表达式的引用;(3)类型转换的引用。
五、指针和引用的关系
C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。
语法概念上引用是一个变量的取别名不开空间,指针是存储一个变量地址,要开空间。
引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
引用可以直接访问指向对象,指针需要解引用才是访问指向对象。
sizeof 中含义不同:引用结果为引用类型的大小;但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8个字节)
指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全一些。
六、nullptr
NULL 实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
cpp
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
在C++ 中NULL被定义为字面常量0 ,而在C 中被定义为无类型指针 (void*) 的常量 。不论采取何种定义,在使用空值的指针 时,都不可避免的会遇到一些麻烦,本想通过 f(NULL) 调用指针版本的 f(int*) 函数,但是由于NULL被定义成0,变成了调用了 f(int x) ,因此与程序的初衷相悖。而只有将 NULL 类型强转成 int* 才可实现。
cpp
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
// 本想通过 f(NULL) 调用指针版本的f(int*)函数,但是由于在C++中NULL被定义成0,调用了f(int x),因此与程序的初衷相悖
f(NULL);
f((int*)NULL);
//f((void*)NULL); //编译报错:error C2665: "f": 2 个重载中没有一个可以转换所有参数类型
return 0;
}

而到了C++11 后就引入了nullptr ,nullptr 是一个特殊的关键字 ,nullptr 是一种特殊类型的字面量 ,它可以转换成任意其他类型的指针类型 。使用 nullptr 定义空指针可以避免类型转换的问题 ,因为 nullptr 只能被隐式地转换为指针类型,而不能被转换为整数类型。
cpp
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
f(NULL);
f(nullptr);
return 0;
}

就是说以后C++学习中如果我们需要用到空指针则不再是用 NULL 而是用 nullptr 了。
结束语
到此,C++的入门基础知识的讲解就完成了,内容量还是不少的,主要还是为C++后续的学习做铺垫。希望这篇文章对大家学习C++有所帮助!