初谈指针(1)

初谈指针(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 = &num;

    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 = &num;

    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版的,所谓的“挂”了。

是不是很像杰伦的“土味情歌”。(逃)

猜你喜欢

转载自blog.csdn.net/qq_41359051/article/details/81070556