初谈指针(1)
前言
每一门语言都有其特性,说到C,就一定绕不过指针。
指针“随意”“奔放”,穿梭在内存地址之间,用得好就恣意潇洒。然而相伴的危害也大,使许多程序员“成也指针,败也指针”。要想熟练掌握指针,其难度系数不可谓之小。所以高校老师不爱讲,连给我们实训的老师都说尽量少用指针。What The Fuck?那还是C吗?
不可否认我在很长一段时间里对指针是惧怕的。摸不着头脑的“段错误”让我一度沦陷“一朝敲代码,十年改BUG”的窘境。可过去终究得过去。我有一个大胆的想法:要用最简单的方式讲述如何使用指针,让每个学C的人乐于使用指针。只是此时此刻的自己还能力有限,只好说“任重而道远”。
什么是指针
什么是指针?答:保存地址值的变量。
所以首先可以得到的信息是,指针是一个变量。用来干嘛呢?用来保存地址。
一定要注意,如果这样定义:int *p = NULL
,指针是p,而不是*p。
一般来说,在32位机上,用四个字节来表示地址,所以无论是int *还是char *的指针,都只占四个字节。对应的,64位机占8个字节。
#include <stdio.h>
int main(int agrc, char **argv)
{
int *p = NULL;
char *c = NULL;
printf("p占%ld个字节\n", sizeof(p));
printf("c占%ld个字节\n", sizeof(c));
return 0;
}
// 32位机上输出
>>p占4个字节
>>c占4个字节
// 同样的代码,在64位上输出
>>p占8个字节
>>c占8个字节
32位
64位
所以指针只用于存放变量的地址,而不是变量本身。这里可以利用VS的调试器来验证
#include <stdio.h>
int main(int agrc, char **argv)
{
int num = 512;
int *p = #
char *str = "youguanxinqing";
printf("p的地址是%p\n", &p);
printf("str的地址是%p\n", &str);
return 0;
可见p地址是001AFB88;str地址是001AFB7C。我们可以在内存中看一下,这两个地址里面都存放的什么
根据入栈“先进后出”的原理,我们要对这串数据反向读,所以地址中存放的是001afb94
p作为指针,存放的自然是地址,所以我们在内存中搜索这个地址
可见00000200,这是一串16进制表示的东西,我们转换成10进制看看
这样就可以得到结果了:p地址上存放的是num的地址,而num地址上存放的是512的16进制表现形式
对于指针变量str也是同理
需要区别的是,对字符串、数组来说,指针存放的是它们的首地址,所以“ouguanxinqing”存放在后面地址中
指针传递
首先我们说说什么是值传递。
值传递:指在调用函数时,将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
在C语言中,默认传递以值传递为主。如下代码
#include <stdio.h>
void swap(int x, int y)
{
x = x + y;
y = x - y;
x = x - y;
}
int main(int agrc, char **argv)
{
int x = 1;
int y = 2;
swap(x, y); // 值传递
printf("x = %d\n", x);
printf("y = %d\n", y);
return 0;
}
// 输出
>>x = 1
>>y = 2
调用了swap函数,但main函数中的x、y值并没有发生交换。这是因为在swap函数中,操纵的是x、y的复制本,并非main函数中的x、y。
而如果我们通过指针传递的方式,就可以直接对main中的x、y做修改了。
#include <stdio.h>
void swap(int *x, int *y)
{
*x = *x + *y;
*y = *x - *y;
*x = *x - *y;
}
int main(int agrc, char **argv)
{
int x = 1;
int y = 2;
swap(&x, &y); // 指针传递
printf("x = %d\n", x);
printf("y = %d\n", y);
return 0;
}
// 输出
>>x = 2
>>y = 1
其实指针传递是很好理解的。因它传给函数的是地址,所以函数的参数要用指针来接收,这就是为什么指针传递时,swap函数的参数定义是int *x, int *y
了
多级指针
在实际开发中,多级指针并不常用,因为无限的增加指针级别,只会让代码更加混乱。指针的存在是为了方便与人,而非制造混乱。
如何理解***p3呢?
1. p3这个变量,存放的是p2的地址,所以*p3的意思是从p2的地址上取出存放的数据;
2. 而p2这个变量里边存放的是p1的地址,那么*p3就等于p1的地址;
3. 既然*p3等于p1的地址,那么**p3的意思就是从p1的地址上取出存放的数据,而p1也是个指针,存放的是变量num的地址 ,也就是说:**p3等于num的地址;
4. 既然**p3是num的地址,那么***p的意思就是从num的地址上取出存放的数据,所以***p3就等于512了;
可能有点绕,我们打印一下,验证看是不是如此
而在参数传递中,如果需要传递指针的地址,那么函数必须用指针的指针来接收;依次类推,如果需要传递二级指针的地址,函数便得用三级指针来接收……
#include <stdio.h>
void swap(int **pointer)
{
**pointer = 2 * 512;
}
int main(int agrc, char **argv)
{
int num = 512;
int *p = #
printf("调用swap函数前,num = %d \n", num);
swap(&p);
printf("调用swap函数后,num = %d \n", num);
return 0;
}
// 输出
>>调用swap函数前,num = 512
>>调用swap函数后,num = 1024
指针函数
说到指针函数,我突然想起“指针数组”和“数组指针”,一不小心就会绕进去。
C中存在指针函数以及函数指针两个概念:
1. 指针函数,其根本就是一个函数,只不过这个函数的返回值是指针;
2. 函数指针,其根本就是一个指针,它能够指向一个函数。
首先我们看一个简单的,可以有返回值的函数
#include <stdio.h>
int result()
{
int num = 512;
return num;
}
int main(int argc, char **argv)
{
int digit;
digit = result();
printf("%d\n", digit);
return 0;
}
// 输出
>>512
事实上,每个变量都有其对应的作用域。num作为局部变量,在调用result()函数的时候被创建,等到result()函数运行完毕之后,内存被释放,num被系统回收。其回收的意思就是:num的地址上存放的数据不再是512。是什么呢?这个就不确定了。
在main函数中,之所以digit能接收到512,实际是系统复制了一份num变量的值,而后赋给digit。有何依据呢?很简单。我们定义一个指针函数,用来返回num的地址,在main()函数中接收一下,看能不能取出这个地址中的数据。
#include <stdio.h>
int *result()
{
int num = 512;
return &num ;
}
int main(int argc, char **argv)
{
int *digit;
digit = result();
printf("%d\n", *digit);
return 0;
}
// 输出
>>Segmentation fault //段错误
报段错误的原因是:访问了不该或不存的内存(当然,这并非造成段错误的唯一原因)。因为num在result()运行结束之后就被系统回收掉了。我们可以用static
关键字,让num在函数结束之后不被系统回收,这样一来,我们就可以返回num的地址了。
#include <stdio.h>
int *result()
{
static int num = 512; // 增加关键字static
return &num ;
}
int main(int argc, char **argv)
{
int *digit;
digit = result();
printf("%d\n", *digit);
return 0;
}
// 输出
>>512
对于字符串
#include <stdio.h>
char *result()
{
char *str = "hello world";
printf("str中存放的地址:%p\n", str);
printf("str自己的地址:%p\n", &str);
return str;
}
int main(int argc, char **argv)
{
char *p = result();
printf("p中存放的地址:%p\n", p);
printf("p自己的地址:%p\n", &p);
printf("打印p的内容:%s\n", p);
return 0;
}
能够看到,main函数中的指针p接受到的是字符串的真实地址,而不是局部变量str的地址,所以p可以正常访问,并且打印出“hello world”
接下来我们再看一个代码
#include <stdio.h>
int *result()
{
int x = 2;
int *p = &x;
printf("x的地址是:%p\n", &x);
printf("p存储的地址是:%p\n", p);
printf("p自己的地址是:%p\n", &p);
return p;
}
int main(int agrc, char **argv)
{
int *digit = result();
printf("digit中存放的地址是:%p\n", digit);
printf("digit自己的地址是:%p\n", &digit);
printf("digit的值是:%d\n", *digit);
return 0;
}
这段代码能够正常运行,并且输出如下:
其实应该是系统来不及对x的内存进行释放,所以能够拿到正确值。我们加个延时,再行输出试试
能看到两次结果不再一样,所以在指针函数中,一定要注意,返回的不应该是局部变量的地址
一个不一样的收尾
我一直说指针强大,如何强大法呢?不妨用一个小小的实例,来展现它访问地址的能力吧!
使用工具:
植物大战僵尸
别怀疑,就是当年大火的pc端游戏
Cheat Engine
用于搜索地址
Visual Studio 2013
编写代码,生成dll动态库文件
DllInject
dll注入工具
开始动手!
第一步:开启植物大战僵尸,利用【阳光】值动态改变进行搜索,直到获取到存放【阳光】的地址
第二步:编写代码如下
/* 增加关键字,代表add_sunshine函数是其他程序可以调用的dll函数 */
__declspec(dllexport)
void add_sunshine()
{
int *p = 0x092F7C18; /* 定义一个指针p,指向【阳光】的地址 */
while (1){
if (*p > 1000){
*p = 0; /* 如果阳光>1000,置0 */
}
else{
*p += 1; /* 开启循环,阳光一直+1 */
}
}
}
第三步:生成dll文件
第四步:向程序注入dll文件
最后实现效果,能看到【阳光】从0-1000循环
嗯,这就是最LOW版的,所谓的“挂”了。
…
是不是很像杰伦的“土味情歌”。(逃)