Android NDK开发之旅3--C语言--指针

作者: 香沙小熊 | 来源:发表于2017-10-13 18:07 被阅读379次

    Android NDK开发之旅 目录

    前言

    学习 C 语言的指针既简单又有趣。通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。所以,想要成为一名优秀的 C 程序员,学习指针是很有必要的。
    正如您所知道的,每一个变量都有一个内存位置,每一个内存位置都定义了可使用连字号(&)运算符访问的地址,它表示了在内存中的一个地址。请看下面的实例,它将输出定义的变量地址:

    示例

    #include <stdio.h>
     
    int main ()
    {
       int  var1;
       char var2[10];
     
       printf("var1 变量的地址: %p\n", &var1  );
       printf("var2 变量的地址: %p\n", &var2  );
     
       return 0;
    }
    
    结果输出:
    var1 变量的地址: 0x7fff5cc109d4
    var2 变量的地址: 0x7fff5cc109de
    

    什么是指针?

    指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。就像其它变量或常量一样,您必须在使用指针存储其它变量地址之前,对其进行声明。指针变量声明的一般形式为:

    type *var-name;
    

    在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型,var-name 是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。以下是有效的指针声明:

    int    *ip;    /* 一个整型的指针 */
    double *dp;    /* 一个 double 型的指针 */
    float  *fp;    /* 一个浮点型的指针 */
    char   *ch;     /* 一个字符型的指针 */
    

    所有指针的值的实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,都是一样的,都是一个代表内存地址的长的十六进制数。不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。

    如何使用指针?

    使用指针时会频繁进行以下几个操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。这些是通过使用一元运算符 * 来返回位于操作数所指定地址的变量的值。下面的实例涉及到了这些操作:

    #include <stdio.h>
    #include <stdlib.h>
    
    void main() {
    
        int  var = 20;   /* 实际变量的声明 */
        int  *ip;        /* 指针变量的声明 */
        ip = &var;  /* 在指针变量中存储 var 的地址 */
    
         /* 变量 var 的地址 */
         printf(" 变量 var 的地址: %p\n", &var);
    
         /* 在指针变量中存储的地址 */
         printf(" 在指针变量中存储的地址: %p\n", ip);
    
         /* 使用指针访问值 */
         printf(" 指针访问的变量值: %d\n", *ip);
    
         /*对ip存的地址指向的变量进行操作 */
         *ip = 66;
         printf(" 对ip存的地址指向的变量,即对var进行操作后的值: %d\n", var);
    
         system("pause");
    
    }
    
    结果输出:
    变量 var 的地址: 004FFE3C
    在指针变量中存储的地址: 004FFE3C
    指针访问的变量值: 20
    对ip存的地址指向的变量,即对var进行操作后的值: 66
    

    C 中的 NULL 指针

    在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。
    NULL 指针是一个定义在标准库中的值为零的常量。请看下面的程序:

    #include <stdio.h>
     
    int main ()
    {
       int  *ptr = NULL;
     
       printf("ptr 的值是 %p\n", ptr  );
     
       return 0;
    }
    
    结果输出:
    ptr 的值是 00000000
    

    在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。
    如需检查一个空指针,您可以使用 if 语句,如下所示:

    if(ptr)     /* 如果 p 非空,则完成 */
    if(!ptr)    /* 如果 p 为空,则完成 */
    

    利于指针做简单的游戏外挂(DLL注入方式)

    1.游戏程序和外挂程序

    先写一个游戏.exe,运行,设定一个游戏时间。

    #include <stdlib.h>
    #include <stdio.h>
    #include <Windows.h>
    
    void main(){
        //游戏时间
        int time = 600;
    
        //打印出time的地址
        printf("%#x\n",&time);
    
        while(time>0){
            time--;
            printf("游戏时间剩余%d秒\n",time);
            Sleep(1000);//#include <Windows.h>
        }
        system("pause");
    }
    

    再创建一个工程(注:生产dll 格式文件),作为外挂,去修改游戏的时间:

    #include <stdlib.h>
    #include <stdio.h>
    
    __declspec(dllexport) void go(){
        //修改游戏时间
        int* p =(int*)0xdcf8a4;//注意:这个地址是游戏打印出来的
        *p = 99999;
    }
    
    

    注意:

    • 添加动态库DLL的输出声明:__declspec(dllexport)。
    • 要保证外挂程序的地址与游戏程序打印出来的地址一致。
    • 把项目的输出改为DLL,而不是EXE。(VS里面:解决方案--属性--常规)。
    • 通过DllInject.exe软件把DLL注入到游戏中,就可以发现游戏时间修改为99999了。

    2.使用DllInject工具注入外挂程序

    首先下载 DllInject.

    (1)点击刷新出现,加载正在运行游戏程序
    列出正在运行的进程
    (2)选择目标游戏程序,点击注入按钮,然后注入外挂程序
    注入外挂程序

    3.注入结果

    游戏程序正常运行 注入外挂程序

    指针类型

    指针有类型,地址没有类型
    地址只是开始的位置,类型大小决定读取到什么位置结束

    示例

    #include<stdio.h>
    #include<stdlib.h>
    
    void main() {
    
        int i = 89;
        int *p = &i; //声明int 类型的指针
        double j = 78.9;
        
        printf("int size:%d\n", sizeof(int));
        printf("double size:%d\n", sizeof(double));
    
        //打印相同类型变量的值
        printf("指针p指向地址 %#x, %d\n", p, *p);
    
        p = &j;
        printf("指针p指向地址 %#x, %lf\n", p, *p); //想通过4字节读取8字节变量的值,是不行的
    
        getchar();
    
    }
    
    结果输出:
    int size:4
    double size:8
    指针p指向地址 0xb7f7dc,89
    指针p指向地址 0xb7f7c0,0.000000
    
    

    只有“int类型的指针”才能用来指向“int类型的值”;其他不同长度类型的指针不行。
    指针是指向内存种的一块内存空间,而这块空间的大小要根据指针指向的数据的类型的长度来分配。
    比如:int型需要4个字节的空间,double 需要8个字节的空间。
    所以在定义指针的时候要指明指针的类型,这样程序才知道应该在内存中保留多大的空间给这个指针

    多级指针(主要讨论二级)

    多级指针的意义:

    动态内存分配二维数组,操作数组的时候。
    在jni.h中的struct JNIEnv结构体等有用到。

    例子:

    #include<stdio.h>
    #include<stdlib.h>
    //多级指针(二级指针)
    void main() {
    
        int a = -50;
        //p1上保存的是a的地址
        int*p1 = &a;
    
        //p2上保存的是p1的地址
        int** p2 = &p1;
    
        printf("p1:%#x,p2:%#x\n",p1,p2);
    
        printf("*p1:%d,**p2:%d\n", *p1, **p2);
    
        printf("%d\n", a);
    
        **p2 = 90;
    
        printf("*p1:%d,**p2:%d\n", *p1, **p2);
    
        printf(" a:%d\n", a);
        getchar();
    }
    
    结果输出:
    
    p1:0xddfc50,p2:0xddfc44
    *p1:-50,**p2:-50
    -50
    *p1:90,**p2:90
     a:90
    
    

    指针运算(加减法)(与数组的操作相结合)

    指针运算一般在数组的遍历才有意义,基于数组在内存中线性排列方式

    #include<stdio.h>
    #include<stdlib.h>
    void main() {
        int ids[] = { 78,90,23,65,19 };
    
        //数组的变量名:ids就是数组首地址 3种方式意义一样
        printf("%#x\n", ids);
        printf("%#x\n", &ids);
        printf("%#x\n", &ids[0]);
    
        //指针变量
        int *p = ids;
        printf("%#x\n", p);
    
        //p++向前移动sizeof(数据类型)个字节
        p++; 
    
        //指向数组的下一个元素地址 =  数组首地址值+4;
        printf("%#x\n", p);
    
        //指向数组的下一个元素
        printf("%d\n", *p);
    
        getchar();
    
    }
    
    结果输出:
    0x11afc7c
    0x11afc7c
    0x11afc7c
    0x11afc7c
    0x11afc80
    90
    

    通过使用指针给数组赋值

    
    
    void main() {
    
        int uids[5];
        /*高级写法
        int i = 0;
        for (; i < 5; i++) {
            uids[i] = i;
        }
    */
        //早些版本的写法
        int* p = uids;
        printf("%#x\n", p);
        int i = 0;
    
        //uids + 5 = uids的首地址值+5*4
        printf("%#x\n", uids + 5);
    
        for (; p < uids + 5; p++)
        {
            *p = i;
            i++;
        }
    
        printf("%d\n", uids[0]);
        printf("%d\n", uids[2]);
        getchar();
    }
    
    结果输出:
    0xf3f888
    0xf3f89c
    0
    2
    
    

    函数指针(与Java中的回调类似)

    函数指针的定义与基本使用

    #include<stdio.h>
    #include<stdlib.h>
    #include<Windows.h>
    
    void msg(char* msg,char* title) {
            //C语言里面字符串用char指针代替
        MessageBox(0, msg, title,0);
    
    }
    
    void main() {
        printf("%#x\n", msg);
        printf("%#x\n", &msg);
    
        //函数指针
        void(*fun_p)(char* msg, char* title) = msg;
        fun_p( "消息内容","标题");
    
        getchar();
    }
    
    

    结果输出:
    0x41140
    0x41140

    同时弹出window系统提示框


    window系统提示框

    在函数指针中调用另一个函数

    int add(int a, int b) {
        return a + b;
    }
    
    int minus(int a, int b) {
        return a - b;
    }
    
    void msg(int(*function_p)(int a, int b), int a, int b) {
        //调用函数指针的函数
        int res = function_p(a, b);
        printf("%d\n", res);
    }
    
    void main() {
        msg(add, 1, 2);
        msg(minus, 1, 2);
    
        getchar();
    }
    
    结果输出:
    3
    -1
    
    案例:用随机数生成一个数组,写一个函数查找最小的值,并返回最小数的地址,在主函数中打印出来
    int* getMinPointer(int ids[], int len) {
        int i = 0;
        int* p = &ids[0];
        for (; i < len; i++) {
            if (ids[i] < *p) {
                p = &ids[i];
            }
        }
        return p;
    }
    
    void main() {
        int ids[10];
        int i = 0;
        //初始化随机数发生器,设置种子,种子不一样,随机数才不一样
        //当前时间作为种子 有符号 int -xx - > +xx
        srand((unsigned)time(NULL));
        for (; i < 10; i++) {
            //100范围内
            ids[i] = rand() % 100;
            printf("%d\n", ids[i]);
        }
    
        int* p = getMinPointer(ids, sizeof(ids) / sizeof(int));
        printf("%#x,%d\n", p, *p);
        getchar();
    }
    
    
    结果输出:
    93
    81
    23
    53
    55
    49
    70
    92
    65
    25
    0x10ff8d4,23
    
    注意:
    • 可以看到,msg函数调用的时候需要传一个函数指针,参数是两个整型,返回值是整型。msg函数的执行需要执行我们手动传入的代码(函数),从而实现了回调(注入代码)。
    • 函数指针的使用,Java中new 内部类,类似Java的回调(比回调更加强大)。
    • 函数指针,提高复用性,在C语音的回调机制里面非常重要。

    特别感谢:
    动脑学院Jason
    小楠总






    微信号kpioneer

    相关文章

      网友评论

      • 0f100e7189b2:指针函数,本质是一个函数;返回的是一个指针;
        函数指针,本质是一个指针,且指针指向的函数;而不是普通的基本数据类型或者类对象;
        香沙小熊:这一块有争议,但更倾向于有类型 https://www.zhihu.com/question/31022750
        香沙小熊:关于指针类型,指针就是一种类型,有且只有一种,所不同的只是指针所指向对象类型的不同。你可以指向一个int数据,也可以指向一个float数据,也可以是其他你自己定的数据结构。你在声明指针的时候就必须指定指针的类型,否则你如何编译通过?编译器分析代码就可以知道指针的类型了哦。类型决定行为,指针指向对象的类型决定了你可以写出什么样的代码,对上面提到的过程没有任何影响。

        作者:知乎用户
        链接:https://www.zhihu.com/question/24445764/answer/27835072
        来源:知乎
        著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
      • 灯等灯等灯_d0c8:有点理解指针,又有点蒙😁

      本文标题:Android NDK开发之旅3--C语言--指针

      本文链接:https://www.haomeiwen.com/subject/fpjwyxtx.html