目录
6.引用
6.1引用概念
6.2引用的写法
6.3引用的特性
6.4常引用
6.5引用的使用场景
6.5.1引用做参数
6.5.2引用做返回值❗❗
🎇值做返回值
🎇引用做返回值
🎇引用在顺序表做返回值
6.5.3传值、传引用效率比较(参数)
6.5.4值和引用的作为返回值类型的性能比较(返回值)
6.6引用和指针
6.6.1引用替换指针的场景
6.6.2引用和指针的区别
6.引用
6.1引用概念
- 引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空 间,它和它引用的变量共用同一块内存空间。
- 引用就是别名。(引用类型)
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。
6.2引用的写法
引用的写法格式:
- 类型& 引用变量名(对象名) = 引用实体;
- 可以给变量的别名取别名,所有别名都是公用同一块内存空间。
- 注意:引用类型必须和引用实体是同种类型的
//类型& 引用变量名(对象名) = 引用实体;
void TestRef()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
【验证变量和变量的引用(别名)是否是同一块内存空间】
#include<iostream>
using namespace std;
int main()
{
int a = 0;
int& b = a;
int c = a;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << b << endl;
cout << c << endl;
b++;//b++的同时a++了吗?c++了吗?
cout << a << endl;
cout << b << endl;
cout << c << endl;
return 0;
}
【别名的别名&类型一致原则】
int main()
{
int a = 0;
int& b = a;
int& c = b;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
//❌double& b=a;类型必须一致
return 0;
}
6.3引用的特性
- 1. 引用在定义时必须初始化
- 2. 一个变量可以有多个引用
- 3. 引用一旦引用一个实体,再不能引用其他实体
大家可以自己动手验证下。
int main()
{
int a = 0;
//1.引用必须初始化
//int& b;
//b = a;
//2.引用定义后,不能改变指向
int& b = a;
int c = 2;
b = c;//这是不是改变指向,而是赋值
cout << a << endl;//2
cout << b << endl;//2
//3.一个变量可以有多个引用,多个别名
//定义别名的别名
int& x = a;
int& d = c;
return 0;
}
6.4常引用
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
}
6.5引用的使用场景
6.5.1引用做参数
引用做参数:
- 输出型参数
- 输入型参数
- 对象比较大,减少拷贝,提高效率
- 指针可以做,但是引用的更方便,更有性价比
//输出型参数
int* preorderTraversal(struct TreeNode* root, int* returnSize)
int* preorderTraversal(struct TreeNode* root, int& returnSize)
//交换两个变量
#include<iostream>
using namespace std;
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int main()
{
int x = 10;
int y = 20;
cout << "x=" << x << endl;
cout << "y=" << y << endl;
Swap(&x, &y);
cout << "x=" << x << endl;
cout << "y=" << y << endl;
return 0;
}
void Swap(int& p1, int& p2)
{
int tmp = p1;
p1 = p2;
p2 = tmp;
}
int main()
{
int x = 10;
int y = 20;
cout << "x=" << x << endl;
cout << "y=" << y << endl;
Swap(x, y);
cout << "x=" << x << endl;
cout << "y=" << y << endl;
return 0;
}
//不能交换地址
6.5.2引用做返回值❗❗
🎇值做返回值
请问return a意味着把a的值传给func吗?回顾:函数栈帧的创建与销毁-CSDN博客
- 函数调用结束,函数栈帧就销毁了,return需要返回值,会再函数栈帧销毁之前拷贝放到临时空间里面。给ret接收。
- 数据小,临时空间是寄存器。数据大。
#include<iostream>
using namespace std;
int func()
{
int a = 0;
return a;
}
int main()
{
int ret = func();
return 0;
}
🎇引用做返回值
- 函数调用结束,函数栈帧就销毁了,return返回a的别名,给ret接收。
- ret接收到了之后,后面访问a的别名,也就是访问a的空间,但是a的空间已被销毁,就会造成非法访问,野引用的问题。非法访问!!!特别注意
- 如果a的内存空间在函数调用之后没有被清理,去访问可能访问到原值。但是如果后序定义其他的变量,这个内存位置可能就被操作系统分配给其他的变量。
- 如果a的内存空间在函数调用之后被清理了,访问到了随机值。
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。- 返回变量出了函数作用域就生命周期(局部变量),不能用引用返回。返回变量出了函数作用域就生命周期到了就销毁,不能用引用返回。
- 非法访问!!野引用!!对程序存在安全隐患。
- C++中临时变量具有常量(不可被修改)
全局变量/静态变量static/堆上变量等就可以引用做返回值。
【示例一】
#include<iostream>
using namespace std;
int& func()
{
int a = 0;
//✔static int a=0;
return a;
}
int main()
{
int ret = func();
return 0;
}
【示例二】
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;
return 0;
}
🎇引用在顺序表做返回值
- 不需要typedef 就可以直接使用SeqList
- 结构体(类)中可以包含成员函数
- 域不同,不用再区分名称
- 开辟的空间在堆上,出了作用域,函数栈帧没有销毁(可以用引用做返回值)
返回引用
- 引用具有了读写双重功能
- 引用中间没有产生临时变量
引用做返回值
- 修改返回对象
- 减少拷贝提高效率
这个部分到后面还会细讲
//C++
#include<iostream>
#include<assert.h>
using namespace std;
struct SeqList
{
//成员变量
int* a;
int size;
int capacity;
//成员函数
void Init()
{
a = (int*)malloc(sizeof(int) * 4);
// ...
size = 0;
capacity = 4;
}
void PushBack(int x)
{
//... 扩容
a[size++] = x;
}
//读写返回变量
int& Get(int pos)
{
assert(pos >= 0);
assert(pos < size);
return a[pos];
}
int& operator[](int pos)
{
assert(pos >= 0);
assert(pos < size);
return a[pos];
}
};
int main()
{
SeqList s;
s.Init();
s.PushBack(1);
s.PushBack(2);
s.PushBack(3);
s.PushBack(4);
for (int i = 0; i < s.size; i++)
{
//cout << s.Get(i)<< " ";
cout << s[i] << " ";
//cout << s.operator[](i) << " ";
}
cout << endl;
for (int i = 0; i < s.size; i++)
{
/*if (s.Get(i) % 2 == 0)
{
s.Get(i) *= 2;
}*/
if (s[i] % 2 == 0)
{
s[i] *= 2;
}
}
cout << endl;
for (int i = 0; i < s.size; i++)
{
cout << s.Get(i) << " ";
}
cout << endl;
return 0;
}
6.5.3传值、传引用效率比较(参数)
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
- clock只能计算出整数位。0ms表示小于1ms,不代表没有,代表时间极其短。
#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void main()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
6.5.4值和引用的作为返回值类型的性能比较(返回值)
通过上述下述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。
#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void main()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
6.6引用和指针
6.6.1引用替换指针的场景
- 指针和引用的功能是类似的,重叠的。
- C++的引用,对指针使用比较复杂的场景进行一些替换【场景替换】,让代码更简单易懂,但是不能完全替代指针。
- C++的引用不能完全替代指针的元婴就有:引用定义之后,不能改变指向,但是指针可以改变指向。
- 在Java python 等其他语言里没有指正,只有引用,且其他语言的引用是可以改变指向的实现链表等。注意区别C++的引用是不能改变指向的。
- C++中90%的场景还是使用引用,但是在一些特殊场景,我们还是使用指针。(例如:链表,二叉树,指向一段动态内存开辟的空间)
【链表中二级指针】
//双向无头链表
typedef struct Node
{
struct Node* next;
struct Node* prev;
int val;
}LNode,*PNode;
//void PushBack(PNode* phead, int x)
void PushBack(PNode& phead, int x)
{
//
}
int main()
{
PNode pphead;
PushBack(pphead, 1);
}
【输出型参数】
OJ输入型参数:直接给数据使用。
OJ输出型参数:给参数需要修改这个参数(指针/别名)
int* preorderTraversal(struct TreeNode* root, int* returnSize)
int* preorderTraversal(struct TreeNode* root, int& returnSize)
6.6.2引用和指针的区别
引用和指针的不同点:
- 1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 2. 引用在定义时必须初始化,指针没有要求。
- 3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
- 4. 没有NULL引用,但有NULL指针。
- 5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
- 6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
- 7. 有多级指针,但是没有多级引用。
- 8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
- 9. 引用比指针使用起来相对更安全 。
- 底层是底层,语法是语法,二者不可混为一谈。
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
int main()
{
int a = 10;
int& ra = a;
cout << "&a = " << &a << endl;
cout << "&ra = " << &ra << endl;
return 0;
}
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
引用和指针的区别:
【语法】
- 引用是别名,不开空间;指针是地址,需要开空间存地址。
- 引用必须初始化,指针可以初始化可以不用初始化。
- 引用不能改变指向,指针可以改变指向。
- 引用相对更加安全,没有空引用;但有空指针。
- 容易出现野指针;但不容易出现野引用。
- 其他方面。
【底层】
- 汇编层面上,没有引用,只有指针,引用编译后也转换成指针了。
🙂感谢大家的阅读,若有错误和不足,欢迎指正。