C++引用与指针的关系:
写在开篇:我们知道在C语言中最令人头疼的就是指针了,不仅有一级指针,还有二级指针,三级指针,数组指针,函数指针等等以及各种逆天的套娃操作,让很多小伙伴笑嘻了。进入C++之后,我们发现常用引用来代替指针进行操作,引用比较有人性化,简单易懂。那么它们之间有什么联系呢??? 快来一起看看吧~~
正文开始@Assassin
目录:
1. 二者的差异:
总论:
指针 | 引用 |
---|---|
可以不初始化 | 必须初始化 |
可以为空 | 不能为空 |
可以更换目标 | 不能更换目标 |
- 引用必须初始化,而指针可以不初始化
我们在定义一个引用的时候必须为其指定一个初始值,但是指针却不需要:
int &r; //不合法,没有初始化引用
int *p; //合法,但p为野指针,使用需要小心
- 引用不能为空,而指针可以为空
由于引用不能为空,所以我们在使用引用的时候不需要测试其合法性,而在使用指针的时候需要首先判断指针是否为空指针,否则可能会引起程序崩溃。
void test_p(int* p)
{
if(p != null_ptr) //对p所指对象赋值时需先判断p是否为空指针
*p = 3;
return;
}
void test_r(int& r)
{
r = 3; //由于引用不能为空,所以此处无需判断r的有效性就可以对r直接赋值
return;
}
- 引用不能更换目标
指针可以随时改变指向,但是引用只能指向初始化时指向的对象,无法改变:
int a = 1;
int b = 2;
int &r = a; //初始化引用r指向变量a
int *p = &a; //初始化指针p指向变量a
p = &b; //指针p指向了变量b
r = b; //引用r依然指向a,但a的值变成了b
2. 指针的基本使用:
2.1 指针是什么?
指针是个变量,存放内存单元的地址(编号)。
#include <stdio.h>
int main()
{
int a = 10; //在内存中开辟一块空间
int *p = &a; //这里我们对变量a,取出它的地址,可以使用&操作符。
//将a的地址存放在p变量中,p就是一个之指针变量。
return 0;
}
总结:指针就是变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
2.2 数组指针:
数组指针的定义:数组指针是指针?还是数组?
答案是:指针。我们已经熟悉: 整形指针: int* pint; 能够指向整形数据的指针。 浮点型指针: float * pf; 能够指向浮点型数据的指针。那数组指针应该是:能够指向数组的指针。
数组指针的写法:
int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
数组指针的使用:
#include <stdio.h>
#include <windows.h>
void print_arr1(int arr[3][5], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print_arr2(int (*arr)[5], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
print_arr1(arr, 3, 5);
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
print_arr2(arr, 3, 5);
system("pause");
return 0;
}
结果:
2.3 函数指针:
首先看一段代码:
#include <stdio.h>
#include <windows.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
system("pause");
return 0;
}
结果:
输出的是两个地址,这两个地址是 test 函数的地址。 那我们的函数的地址要想保存起来,怎么保存?
下面我们看代码:
void test()
{
printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();
首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针? 答案是:
pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。
2.4 指针的一些深入:
#include<cstdio>
#include<cstring>
int main(void)
{
//一维数组
int a[] = {1, 2, 3, 4};
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(a + 0));
printf("%d\n", sizeof(*a));
printf("%d\n", sizeof(a + 1));
printf("%d\n", sizeof(a[1]));
printf("%d\n", sizeof(&a));
printf("%d\n", sizeof(*&a));
printf("%d\n", sizeof(&a + 1));
printf("%d\n", sizeof(&a[0]));
printf("%d\n", sizeof(&a[0] + 1));
//字符数组
char arr[] = {'a', 'b', 'c', 'd', 'e', 'f'};
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr + 0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr + 1));
printf("%d\n", sizeof(&arr[0] + 1));
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr + 0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr + 1));
printf("%d\n", strlen(&arr[0] + 1));
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr + 0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr + 1));
printf("%d\n", sizeof(&arr[0] + 1));
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr + 0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr + 1));
printf("%d\n", strlen(&arr[0] + 1));
char *p = "abcdef";
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(p + 1));
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));
printf("%d\n", sizeof(&p));
printf("%d\n", sizeof(&p + 1));
printf("%d\n", sizeof(&p[0] + 1));
printf("%d\n", strlen(p));
printf("%d\n", strlen(p + 1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p + 1));
printf("%d\n", strlen(&p[0] + 1));
//二维数组
int a[3][4] = {0};
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(a[0][0]));
printf("%d\n", sizeof(a[0]));
printf("%d\n", sizeof(a[0] + 1));
printf("%d\n", sizeof(*(a[0] + 1)));
printf("%d\n", sizeof(a + 1));
printf("%d\n", sizeof(*(a + 1)));
printf("%d\n", sizeof(&a[0] + 1));
printf("%d\n", sizeof(*(&a[0] + 1)));
printf("%d\n", sizeof(*a));
printf("%d\n", sizeof(a[3]));
return 0;
}
自己跑跑看~~分析一哈有些为什么报错
3. 引用的基本使用:
3.1 引用的用法:
作用: 给变量起别名
语法: 数据类型 &别名 = 原名
示例:
int main() {
int a = 10;
int &b = a;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
b = 100;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
system("pause");
return 0;
}
3.2 深入了解引用:
左值引用¶:
常规引用,一般表示对象的身份。
右值引用¶:
右值引用就是必须绑定到右值(一个临时对象、将要销毁的对象)的引用,一般表示对象的值。
右值引用可实现转移语义(Move Sementics)和精确传递(Perfect Forwarding),它的主要目的有两个方面:
- 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
- 能够更简洁明确地定义泛型函数。
引用折叠¶:
- X& &、X& &&、X&& & 可折叠成 X&
- X&& && 可折叠成 X&&
C++的引用在减少了程序员自由度的同时提升了内存操作的安全性和语义的优美性。比如引用强制要求必须初始化,可以让我们在使用引用的时候不用再去判断引用是否为空,让代码更加简洁优美,避免了指针满天飞的情形。除了这种场景之外引用还用于如下两个场景:
- 引用型参数:
一般我们使用const reference参数作为只读形参,这种情况下既可以避免参数拷贝还可以获得与传值参数一样的调用方式。
void test(const vector<int> &data)
{
//...
}
int main()
{
vector<int> data{1,2,3,4,5,6,7,8};
test(data);
}
- 引用型返回值:
C++提供了重载运算符的功能,我们在重载某些操作符的时候,使用引用型返回值可以获得跟该操作符原来语法相同的调用方式,保持了操作符语义的一致性。一个例子就是operator[]操作符,这个操作符一般需要返回一个引用对象,才能正确的被修改。
vector<int> v(10);
v[5] = 10; //[]操作符返回引用,然后vector对应元素才能被修改
//如果[]操作符不返回引用而是指针的话,赋值语句则需要这样写
*v[5] = 10; //这种书写方式,完全不符合我们对[]调用的认知,容易产生误解
3.3 引用的本质:
本质: 引用的本质在c++内部实现是一个指针常量.
如何得出此结论?
//发现是引用,转换为 int* const ref = &a;
void func(int& ref){
ref = 100; // ref是引用,转换为*ref = 100
}
int main(){
int a = 10;
//自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改
int& ref = a;
ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20;
cout << "a:" << a << endl;
cout << "ref:" << ref << endl;
func(a);
return 0;
}
结论: C++推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了,编译器是很智能的。
4. 指针与引用的性能差距:
指针与引用之间有没有性能差距呢?这种问题就需要进入汇编层面去看一下。我们先写一个test1函数,参数传递使用指针:
void test1(int* p)
{
*p = 3; //此处应该首先判断p是否为空,为了测试的需要,此处我们没加。
return;
}
该代码段对应的汇编代码如下:
(gdb) disassemble
Dump of assembler code for function test1(int*):
0x0000000000400886 <+0>: push %rbp
0x0000000000400887 <+1>: mov %rsp,%rbp
0x000000000040088a <+4>: mov %rdi,-0x8(%rbp)
=> 0x000000000040088e <+8>: mov -0x8(%rbp),%rax
0x0000000000400892 <+12>: movl $0x3,(%rax)
0x0000000000400898 <+18>: nop
0x0000000000400899 <+19>: pop %rbp
0x000000000040089a <+20>: retq
End of assembler dump.
上述代码1、2行是参数调用保存现场操作;第3行是参数传递,函数调用第一个参数一般放在rdi寄存器,此行代码把rdi寄存器值(指针p的值)写入栈中;第4行是把栈中p的值写入rax寄存器;第5行是把立即数3写入到rax寄存器值所指向的内存中,此处要注意(%rax)两边的括号,这个括号并并不是可有可无的,(%rax)和%rax完全是两种意义,(%rax)代表rax寄存器中值所代表地址部分的内存,即相当于C++代码中的*p,而%rax代表rax寄存器,相当于C++代码中的p值,所以汇编这里使用了(%rax)而不是%rax。
我们再写出参数传递使用引用的C++代码段test2:
void test2(int& r)
{
r = 3; //赋值前无需判断reference是否为空
return;
}
这段代码对应的汇编代码如下:
(gdb) disassemble
Dump of assembler code for function test2(int&):
0x000000000040089b <+0>: push %rbp
0x000000000040089c <+1>: mov %rsp,%rbp
0x000000000040089f <+4>: mov %rdi,-0x8(%rbp)
=> 0x00000000004008a3 <+8>: mov -0x8(%rbp),%rax
0x00000000004008a7 <+12>: movl $0x3,(%rax)
0x00000000004008ad <+18>: nop
0x00000000004008ae <+19>: pop %rbp
0x00000000004008af <+20>: retq
End of assembler dump.
5. 总结:
C++中引入了引用操作,在对引用的使用加了更多限制条件的情况下,保证了引用使用的安全性和便捷性,还可以保持代码的优雅性。在适合的情况使用适合的操作,引用的使用可以一定程度避免“指针满天飞”的情况,对于提升程序稳定性也有一定的积极意义。最后,指针与引用底层实现都是一样的,不用担心两者的性能差距。
写在最后:
有任何问题请issue!谢谢—