前言
众所周知,C/C++的知识的精髓是指针,指针的本质是内存地址,可无论是普通变量,还是类的成员变量,谁还能没有一个内存地址呢,既然普通变量有内存地址,那我们也能像对指针变量那样对普通变量进行操作,也就是指针的 * 操作和 -> 操作。
测试
打开compiler explorer,其网址如下:https://gcc.godbolt.org/
定义一个普通的变量a,写一个func_1函数,借用指针变量p,通过 * 操作,来给变量a赋值,再写一个函数func_2,不借助指针变量,直接对变量a的地址进行 * 操作来给变量赋值
int a = 0;
void func_1()
{
int* p = &a;
*p = 1;
}
void func_2()
{
*(int*)&a =1;
}
对比一下汇编,func_1函数 用了三条指令,func_2就简洁多了,直接对变量a的内存地址写:1,仅一条指令就完成了赋值,如你所见,普通变量也能做指针 * 操作,只要知道变量a的地址,就可以进行指针的 * 操作,还省掉了指针的开销,更简单直接。
当然还可以把func_2写的更极端一些,假设a的内存地址是:ox1234我们就可以把func_2函数改写成这样子
我们再写一个函数func_3,用最常规的方式给变量a赋值
int a = 0; //a address = 0x1234
void func_1()
{
int* p = &a;
*p = 1;
}
void func_2()
{
*(int*)&a =1;
}
void func_3()
{
a =1;
}
可以发现func_2函数和func_3函数汇编指令完全相同
所以如你所见,你最熟悉的变量读写都等同于对变量地址的指针 * 操作,正如变量的定义所言,变量不过是内存地址的别名,同样的道理,不借助指针变量,我们也可以通过指针的 -> 操作,对类的成员变量赋值
class A{
public:
int x;
} a;
void func_c1()
{
a.x = 1;
}
void func_c2()
{
(&a)->x = 1;
}
总结
1、指针操作不是指针变量的专利,普通变量甚至立即数也可以做指针(* 、->)操作
注意不能出现下面的表达式,因为&a在这里是常量,不能单独出现在等式左边,同时要注意引用&与取地址&的区别,参考:指针变量的传值、传址和传引用
&a = (int*)0x1234
也不要出现下面的表达式,很显然 " * " 的操作数必须是指针
* 0x1234 = 1
2、计算机的世界里面,万物皆有地址,所以万物皆可指针,你既可以循规蹈矩通过变量名或规定的函数接口读写变量,也可能无视规则,通过指针操作,随意随时随地的读写变量
3、除了0x1234,指针操作可以读写任意的内存地址,除了内存管理单元MMU,没人能制止这种读写行为,这也是早期 “游戏修改器” 的工作原理
最后
正是指针操作的灵活性,让他成为大神和黑客的最爱,很多大神仅仅通过一个栈变量(a)的地址,配合指针操作,就可以试探、回溯出整个函数的调用轨迹。同时指针操作的不可控性也是大规模编程的噩梦,试想一下,你得到了某个“私有变量”的内存地址,就意味着你可以通过指针操作不受任何限制的读写这个变量,这时,你就不会再理会它“禁止访问”的私有属性了,你也不再愿意循规蹈矩的通过成员函数来读写它。
所以暴露任何数据、函数的内存地址都是巨大的风险,因为这些地址都可以用来做违规、不受控的指针操作。