指针和引用,一文即可知
1.指针
-
定义:存放
内存地址
的变量,内存地址是内存单元的编号。 -
指针大小
-
同一编译程序中,不同类型的指针变量
大小相同
-
指针的逻辑大小应由
编译器位数
决定,因为通常由sizeof测得,而sizeof是在编译期执行的
-
同一编译程序中,不同类型的指针变量
- 指针的类型
int *p; // 指向int类型的指针,值为int类型数据在内存中的地址
int *p[10]; // 指针数组,是一个数组,数组中每个数据元素是指向int类型的指针
int (*p)[10]; // 数组指针,是一个指针,指向数据元素为int类型,大小为10的数组
int *p(int); // 指针函数,函数名是p,参数是int类型的,返回值是int*类型的
int (*p)(int); // 函数指针,指向参数为int类型,返回值也为int类型的函数
int *(*p(int))[3];
// 1. p先与()结合,说明P 是一个函数,形参为int
// 2. 与外面的*结合,说明函数返回的是一个指针
// 3. 与[]结合,说明返回的指针指向的是一个数组
// 4. 再与*结合,说明数组里的元素是指针,然后再与int 结合,说明指针指向的内容是整型数据
// 5. 所以P 是一个参数为一个整型数据且返回一个指向由整型指针变量组成的数组的指针变量的函数
-
指针运算
-
指针加减:相当于
指针地址±加减值*sizeof(指针类型)
-
指针求差:通常是在同一个数组中,
差值 = 地址差值 / sizeof(数据类型)
-
指针加减:相当于
-
指针与const
-
顶层const/指针常量
int *const p
:指针类型的常量,指向不可变,但指向的值可变(
声明同时必须初始化
,且在程序声明周期内不可变) -
底层const/常量指针
const int *p, int const *p
:指向常量的指针,指向可变,但不可通过该指针改变指向的值(可以通过其他指针更改) -
指向常量的常指针
const int * const p
:指向常量的指针常量就是一个常量,并且它指向的对象也是一个常量 - 顶层const和底层const都是相对于指针而言的。
-
顶层const/指针常量
-
int a[10]
的数组名与数组首地址的差别-
a是数组名,也是数组首元素地址。+1表示
偏移一个数组元素
-
&a表示整个数组的首地址,+1表示
偏移一整个数组
,即数组末尾下一个元素的地址
-
a是数组名,也是数组首元素地址。+1表示
-
非法指针
-
野指针:未被初始化的指针,访问行为不可控。指针声明应赋值为
nullptr
,这样在使用时编译器会报错,避免非法访问 - 悬空指针:指针仍然指向已被释放的内存。C++的智能指针可以避免悬空指针的出现
-
野指针:未被初始化的指针,访问行为不可控。指针声明应赋值为
-
指针与函数
-
指针函数
int *p();
- 定义:指针类型的函数,返回值为指针
- 注意:在使用时,避免返回指向局部变量的指针,因为函数堆栈释放后,返回指针会变成悬空指针。也可以给局部变量加static,使其存储在堆区,但是不推荐。
-
函数指针
int (*p)()
- 定义:指向代码段中函数入口地址的指针
-
初始化:
函数指针变量 = 函数名
,函数名即是函数入口地址。 - 作用:1. 实现回调函数:通过函数指针调用的函数被称为回调函数,回调函数作为主调函数的形参,可以通过传入不同的函数指针作为实参,从而实现“动作/规则”的抽象,提供更强的灵活性
-
指针函数
2.引用
- 定义:临时使用的内存空间的别名(内存空间代表变量、类对象···)
-
特点
- 非空性:引用声明时必须初始化,并且生命周期内引用指向的对象不会改变,避免野指针
- 多个别名:一个变量可以有多个引用
- 类型安全:增加类型安全的检查,提供了更完善的内存使用方式
3.指针和引用的区别
-
本质上
- 程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是标号及其对应内存地址
-
指针变量在符号表上对应的地址值为
指针变量的地址值
-
引用在符号表上对应的地址值为
引用对象的地址值
-
符号表
是编译器生成的,
一般不能修改
,因此指针可以通过改变指针变量的值而改变指向,而引用则不能修改
-
层级上
- 指针可以有多级,引用只能有一级
-
独立性
- 指针是逻辑独立的,可以为空,但不能引用临时对象。
- 引用是一个别名,依附于具体对象,定义时必须初始化,并且在整个生命周期中引用关系不可改变
-
sizeof
- 指针是指针本身的大小,通常和寻址位数有关。sizeof引用得到的是其对应的变量大小
-
类型安全
- 引用通过抛弃多余的灵活性,增加类型安全的检查,提供了更完善的内存使用方式
- 指针不进行类型检查
-
形参规范
- 如果需要修改参数对应值,则使用指针。如果传递只读参数对应值,则使用const &
4.三种传递方式
-
值传递
- 原理:将函数实参拷贝到堆栈局部变量的过程
-
范围:适合基本数据类型和小结构体的传递,编译器可以进行
寄存器操作优化
(占用代码段空间,执行时直接加载到寄存器上),访问速度快,相比于指针和引用减少了访存次数。
-
指针传递
- 原理:本质也是传值,但是拷贝固定为指针大小
- 范围:适合于传递大结构体和类对象,可以修改指向变量的值
-
引用传递
- 原理:相当于实参的别名,传递的是地址
-
范围:适合于传递大结构体和类对象,如果形参只读,可以使用
const &
。
-
汇编示例
- 指针和引用在汇编层次上都是通过地址进行处理的,而值传递确实通过值进行的。
// **********汇编代码除去了堆栈调用过程和非必要部分**************
// 值传递
void val(int a) {
a = 1;
00EC19C1 mov dword ptr [a],1
}
// 指针传递
void ptr(int *a) {
*a = 1;
00EC18A1 mov eax,dword ptr [a] // 向寄存器中存放地址
00EC18A4 mov dword ptr [eax],1
}
// 引用传递
void ref(int& a) {
a = 1;
00EC17F1 mov eax,dword ptr [a]
00EC17F4 mov dword ptr [eax],1
}
// 主函数
int main() {
int c = 0;// 定义一个变量
00EC4A8F mov dword ptr [c],0
val(c);// 调用值传递
00EC4A96 mov eax,dword ptr [c] // 将c的值赋值给eax
00EC4A99 push eax
00EC4A9A call val (0EC13D4h)
00EC4A9F add esp,4
ptr(&c);
00EC4AA2 lea eax,[c] // 将c的地址赋值给eax
00EC4AA5 push eax
00EC4AA6 call ptr (0EC13DEh)
00EC4AAB add esp,4
ref(c);
00EC4AAE lea eax,[c] // 将c的地址赋值给eax
00EC4AB1 push eax
00EC4AB2 call ref (0EC13D9h)
00EC4AB7 add esp,4
return 0;
00EC4AD3 xor eax,eax
}
5.右值引用
-
C++11的表达式属性描述
- 数据类型:int、float、int*···
-
值类型
- 左值(lvalue):能取址且不能转移所有权。eg:变量、函数名、++i、字符串(本质是字符数组)
- 将亡值(xvalue) :能取址且可以转移所有权。eg:std::move(a)
- 纯右值(prvalue):无法取址且可以转移所有权。eg:常量及结果为常量的表达式,真假及结果为真假的表达式、nullptr、i++(先拷贝值副本返回,再+1)
- 注意:临时变量只有在被引用的时候才会拥有变量的属性,即内存空间和名字,否则可能就是一个寄存器或者一个没名字的临时栈内存区域
- 将亡值是C++11新增的,作用是将左值强制转换成右值,从而使拷贝变成移动,提高效率
-
作用
- 移动语义:具备对象资源所有权转移的能力,而不是先拷贝后销毁。避免了浅拷贝的悬空指针风险和深拷贝的开销
- 完美转发(顺便解决):将函数实参以其原本的值类别转发出去,避免实参与候选函数误绑定或编译错误
-
减少开销:临时对象构造使用后即放弃,通过右值引用的方式赋予临时对象可寻址访问的属性,从而减少临时对象析构和copy到新对象的构造开销。
foreach(auto &&i : array){ doing();// 操作 }
-
转换
- std::move:无条件的右值转换。右值直接返回,左值通过static_cast强制转换后返回
- std::forward:将函数参数的左右值类型,原封不动地传递到下一个函数中
- std::move和std::forward在运行时不做任何事情。
-
语法示例
// 右值引用必须定义时初始化 int num = 10; int && a = num;// × 不能使用左值进行右值初始化 int && a = 10; // √ // 只能通过右值引用修改右值 int &&a = 10; // 这里的a是右值引用,其实是10 a = 100; // move函数是通过static_cast强制将传入值转换为值原类型的右值引用 int a = 3; int &&t = std::move(a); int &&t2 = std::move(3); // 完美转发 string A("abc"); string&& Rval = std::move(A); string B(Rval); // 参数Rval在B的构造函数中转换为左值调用,进行拷贝 string C(std::forward<string>(Rval)); // 保证参数Rval的类型
-
原理
-
左值引用变量:存放
值
-
右值引用变量:存放
临时对象的地址
-
左值引用变量:存放
// 左值引用
int c = 10;
00E81842 mov dword ptr [c],0Ah // 将10的值放入双字空间b中
int* c1 = &c;
00E81849 lea eax,[c]
00E8184C mov dword ptr [c1],eax
// 定义右值引用变量:变量存放的是临时对象的地址
int&& a = 10;
00E8183F mov dword ptr [ebp-18h],0Ah // 将10放入[ebp-18h]开始的双字空间
00E81846 lea eax,[ebp-18h] // 将地址ebp-18h这个值放入eax中
00E81849 mov dword ptr [a],eax // 将地址ebp-18h放入双字空间a中
无法用指针而用引用的原因?
首先构造临时变量,即首先在当前函数的栈帧中留出一块空间(编译器负责),将临时对象构造到这个空间中,然后再将临时对象的值,复制给形式参数,然后临时变量就不需要了。其中,构造临时变量的过程是必须的!但是copy临时变量的过程是多余的,如果调用的函数能够直接使用临时变量就好了?怎么做到呢,比如将临时变量的地址传给调用的函数?好方法,怎么实现呢,指针可以吗?不行,因为指针无法获取临时变量的地址,那怎么办呢?引用!
在执行上述引用时,都会在栈帧中分配空间来存放临时变量,使之不再临时,而引用就是他们的名字,这样就让临时变量和普通变量一样,有自己的名字和内存空间,可以通过引用来赋值和取址。
std::move()作用是基于当前的左值创建一个可引用的临时变量来处理
std::move()返回参数对应的临时变量,但原变量依然不变
作用:实现移动构造函数
类的运算符重载
void func(int &&a, int &&b) {
int tmp = a;
a = b;
b = tmp;
}
int main() {
int a = 1;
long b = 2;
func(std::move(a), std::move(b));
printf("%d %ld", a, b);
}
因为右值引用,引用的是临时变量,因此我们完全可以“剥夺其资源”,从而大大的加快了构造函数的执行效率,这一过程也是引用真正的能区别与指针,且发挥其作用的地方,诠释了为什么引用是为类的对象而生
复制完即弃用的空间使用move变为引用再将其剥夺
buff本身就是一个在执行完构造函数就会被抛弃的,那使用std::move(),将其变成临时变量,然后再由Buff()的移动(复制)构造函数剥夺其内存空间,完美!!!!
指针本身就是一种数据类型,其已经内置了*、[]、+、++等操作。
因此在实现对象的运算符重载时,指针是不能使用的,而且使用值传递显然会增加一次内存copy。因此引用是最佳的选择!
指针和引用,核心区别就是指针不能引用临时变量
一个好的语言应该具有准确的语法描述,而避免过多的程序员未知的编译映射。
少年,我观你骨骼清奇,颖悟绝伦,必成人中龙凤。
秘籍(点击图中书籍)·有缘·赠予你
参考博客