4、复杂表达式和指针高级应用

指针数组与数组指针

  1. 指针数组的实质是一个数组,这个数组中存储的内容全部是指针变量;
  2. 数值指针的实质是一个指针,这个指针指向一个数组;
  • 分析指针数组与数组指针的表达式一般规律:int *p;(p是一个指针); int p[5];(p是一个数组);
    • (1) int *p[5]; (2) int (*p)[5]; (3) int *(p[5]);
  • 总结:我们在定义一个符号时,关键在于:
  1. 第一步:找核心,首先要搞清楚定义的符号时谁;
  2. 第二步:找结合,看谁跟核心最近,谁跟核心结合;
  3. 第三步,继续向外找结合,直到符号完;
  • 如果核心和 * 结合,表示核心是指针;
  • 如果核心和 [] 结合,表示核心是数组;
  • 如果核心和 () 结合,表示核心是函数;
  • 使用一般规律分析3个符号:([] 的优先级高于 *)
  1. int *p[5]:  p是一个数组,数组中的5个元素都是指针,指针指向int型,所以 *p[5] 是一个指针数组;
  2. int (*p)[5]: p是一个指针,指向一个数组,数组有5个元素都是int类型,所以 (*p)[5]是一个数组指针;
  3. int *(p[5]): 是一个指针数组,结合方式同第一个一样;
 

函数指针与typedef

  • 函数指针的实质还是一个指针,还是指针变量。本身占4个字节(在32位系统中,所有的指针都是4个字节);
  • 函数指针、指针数组、普通指针之间并没有本质的区别,区别在于:指针指向的东西是什么;
  • 函数指针的实质是一段在内存中连续分布的代码,函数的第一句代码的地址,就是这个函数的地址;
  • 结合函数的实质,函数指针其实就是一个普通的变量,这个普通的变量的类型是函数指针变量类型,他的值是
  • 函数指针的书写和分析方法:
  • C语言是一个强类型语言(每一个变量都有自己的变量类型),编译器可以帮我们做严格的类型检查;
  • 所有的指针变量类型的本质是一样的,知识写法不一样,如:int型指针 *p ;数组指针 int (*p)[5];
  • 假设有函数:void func(void); 对应的函数指针:void (*p)(void); 类型是:void (*)(void);
  • 函数名和数组名最大的区别:函数名做右值时加不加 & ,效果和意思是一样的;但是数组名做右值时加不加 & 意义就不一样了。
  • 写一个复杂的函数指针的示例:如函数strcpy: char *strcpy(char *dest, const char *src);对应的函数指针是: char *strcpy(char *pFunc)(char *dest, const char *src);
 

typedef关键字的用法

  • typedef是C语言中的一个关键字,用来定义(或者叫重命名类型);
  • C语言中的类型一共有2种:一种是编译器定义的原生类型(基础数据类型,如int、double之类的);第二种是用户自定义类型,不是语言自带的是程序员自己定义的(譬如数组类型、结构体类型、函数类型·····);
  • 数组指针、指针数组、函数指针等都属于用户自定义类型;
  • 有时候自定义的类型太长了,使用不方便,就可以用typedef给他重命名一个短一点的名字;
  • 注意:typedef是给类型重命名,也就是说,typedef定义出来的都是类型,而不是变量;
 

指针实战1

  • 如果学过C++或者Java或者C#等面向对象的语言,就会知道面向对象三大特征中有一个多态。多态就是同一个执行实际结果不一样,跟我们这里看到的现象其实是一样的;
  • #include <stdio.h>
    
    int add(int a, int b);
    int sub(int a, int b);
    int multiply(int a, int b);
    int divide(int a, int b);
    
    typedef int (*pFunc)(int, int);
    
    int main(void)
    {
        pFunc p1 = NULL;
        int a, b, d;
        char c;
    
        printf("please enter two number:
    ");
        scanf("%d %d",&a ,&b);
        printf("please enter flag: + - * / 
    ");
        do
        {
            scanf("%c",&c);
        }while(c == '
    ');
        switch(c)
        {
            case '+':
                p1 = add; break;
            case '-':
                p1 = sub; break;
            case '*':
                p1 = multiply; break;
            case '/':
                p1 = divide; break;
        }
        d = p1(a,b);
        printf("%d %c %d = %d
    ", a, c, b, d);
    }
  • 上述程序运行时出现断错误,第一步先定位段错误。定位的方法就是在可疑处加打印信息,从而锁定导致段错误的语句,然后集中分析这句为什么会段错误;
  • Linux中命令行默认是行缓冲,就是说程序在printf输出的时候,Linux不会一个字一个字的输出我们的内容,而是将其缓冲起来放在缓冲区,等第一行准备完了再一次性把一行全部输出来(为了效率)。Linux判断一行有没有完的依据就是换行符 ' ' (Windows中换行符是 ' ' ,Linux中是 ' ' ,iOS中是 ' ' )。就是说printf再多,只要没有遇到 (或者程序终止,或者缓冲区满)都不会输出而会不断缓冲,这时候是看不到输出内容的。因此,在每个printf打印语句(特别是用来做调试的printf语句)后面一定要加 ,否则可能导致误判。
  • 关于在linux命令行下用scanf写交互性代码的问题:
  • scanf是和系统的标准输入打交道,printf和标准输出打交道。要完全搞清楚这些东西得把标准输入标准输出搞清楚。
  • 我们用户在输入内容时结尾都会以 结尾,但是程序中scanf的时候都不会去接收最后的 ,导致这个回车符还存留在标准输入中。下次再scanf时就会先被拿出来,这就导致你真正想拿的那个数反而没机会拿,导致错误

 

指针实战2

  1. 本程序要完成一个计算器,我们设计了2个层次:上层是framework.c,实现应用程序框架;下层是cal.c,实现计算器。实际工作时cal.c是直接完成工作的,但是cal.c中的关键部分是调用的framework.c中的函数来完成的。
  2. 先写framework.c,由一个人来完成。这个人在framework.c中需要完成计算器的业务逻辑,并且把相应的接口写在对应的头文件中发出来,将来别的层次的人用这个头文件来协同工作。
  3. 另一个人来完成cal.c,实现具体的计算器;这个人需要framework层的工作人员提供头文件来工作(但是不需要framework.c)
  4. #ifndef __CAL_H
    #define __CAL_H
    
    typedef int (*pFunc)(int, int);
    
    // 结构体用来做计算器
    struct cal_t
    {
        int a;
        int b;
        pFunc pc;
    };
    
    // 函数声明放在最后面靠近endif的地方
    int calculator(const struct cal_t *p);
    
    #endif
    int calculator(const struct cal_t *p)
    {
        return p->pc(p->a, p->b);
    }
    #include <stdio.h>
    #include "cal.h"
    #include "framwork.h"
    
    int main(void)
    {
        int ret = 0;
        struct cal_t myCal;
        myCal.a = 12;
        myCal.b = 13;
        myCal.pc = sub;
        
        ret = calculator(&myCal);
        printf("ret = %d.
    ", ret);
        return 0 ;
    }
 
总结:
  • 本节和上节实际完成的是同一个习题,但是采用了不同的程序架构。
  • 对于简单问题来说,上节的不分层反而容易理解,反而简单;本节的分层代码不好理解,看起来有点把简单问题复杂化的意思。原因在于我们这个问题本身确实是简单问题,而简单问题就应该用简单方法处理。
  • 分层写代码的思路是:有多个层次结合来完成任务,每个层次专注各自不同的领域和任务;不同层次之间用头文件来交互。
  • 分层之后上层为下层提供服务,上层写的代码是为了在下层中被调用。
  • 上层注重业务逻辑,与我们最终的目标相直接关联,而没有具体干活的函数。
  • 下层注重实际干活的函数,注重为上层填充变量,并且将变量传递给上层中的函数(其实就是调用上层提供的接口函数)来完成任务。
  • 下层代码中其实核心是一个结构体变量(譬如本例中的struct cal_t),写下层代码的逻辑其实很简单:第一步先定义结构体变量;第二步填充结构体变量;第三步调用上层写好的接口函数,把结构体变量传给它既可。
 

再论typdef

  • C语言的2种类型:内建类型(ADT)、用户自定义类型(UDT);
  • typedef定义的是类型,而不是变量;
  • 类型是一个数据模板,变量是一个实在的数据;类型时不占内存的,而变量是占内存的;
  • 面向对象的语言中:类型是class,变量就是对象;
  •     typedef 和 #define宏 的区别:
  • typedef char *pChar; 
  • #define pChar char *;
 

typedef 与 struct结构体:

  • 结构体在使用时都是先定义结构体类型,再用结构体类型去定义变量;
  • C语言规定,结构体类型在使用时,必须是“ struct 结构体类型名 结构体变量名 ”,这样的方式来定义;如 struct student zhangsan;
  • #include <stdio.h>
    #include <string.h>
    
    typedef struct student
    {
        char name[12];
        int age;
        char mager[12];
    }s1;
    
    //定义两个类型,第一个是结构体类型,有两个名字,第二个是结构体指针类型
    typedef struct teater
    {
        char name[20];
        int age;
        char mager;
    }teater,*pTeater;
    
    int main(void)
    {
        s1 s1;
        s1.name = "sdfs";
        s1.age = 12;
        s1.mager = "gsfs";
    
        printf("%c,%d,%c
    ",s1.name,s1.age,s1.mager);
    
        return 0
    }
 

typedef 与 const

  • typedef int *PINT; const PINT p2; //相当于 int *const p2;
  • typedef int *PINT;  PINT cost p2;  //相当于 int *const p2;
  • 如果想要得到 const int *p;这种效果,只能 typedef const int *CPINT; CPINT p1;

使用typedef的意义:

  • 简化数据类型:char *(*)(char *, char *);   typedef char *(*pFunc)(char *, char *);    
  • 很多编程体系下,人们倾向于不使用int、double等C语言内建类型,因为这些类型本身和平台有关譬如int在16位机器上是16位的,在32位机器上就是32位的)。为了解决这个问题,很多程序使用自定义的中间类型来做缓冲。譬如linux内核中大量使用了这种技术;内核中先定义:typedef int size_t; 然后在特定的编码需要下用size_t来替代int(譬如可能还有typedef int len_t)
  • STM32的库中全部使用了自定义类型,譬如:typedef volatile unsigned int vu32;
 

二重指针

二重指针和一重指针的区别:
  • 二重指针和一重指针的本质都是指针变量,指针变量的本质就是变量;
  • 一重指针变量的 二重指针变量本身都占4个字节的内存空间;
 二重指针的本质:
  • 二重指针的本质就是指针变量,和普通指针的差别就是:他指向的变量类型必须是一个指针。二重指针其实也是一种数据类型,编译器在编译时会根据二重指针的数据类型来做静态类型检查,一旦发现运算时数据类型不匹配,编译器就会报错;
  • 二重指针(函数指针、数组指针)就是为了让编译器了解这个指针被定义它的程序员希望这个指针指向的东西,(定义指针时用数据类型来标记,譬如int *p,就表示p要指向int型数据),编译器知道指针类型之后可以帮我们做静态类型检查。编译器的这种静态类型检查可以辅助程序员发现一些隐含性的编程错误,这是C语言给程序员提供的一种编译时的查错机制;
  • 二重指针被发明的原因和函数指针、数组指针、结构体指针等是一样的;     
  • #include <stdio.h>
    void func(int **p)
    {
        *p = (int *)0x12345678;
    }
    
    int main(void)
    {
        int a = 4;
        int *p = NULL;
        p = &a;                // p指向a
        printf("p = %p.
    ", p);        // p打印出来就是a的内存地址
        func(&p);                    // 在func内部将p指向了别的地方
        printf("p = %p.
    ", p);        // p已经不指向a了,所以打印出来不是a的地址
        *p = 23;                    // 因为此时p指向0x12345678,但是这个地址是不
                        // 允许访问的,因此会段错误。
      
 
二重指针的用法:
  • 二重指针指向一重指针的地址;
  • 二重指针指向指针数组;
  • 实践编程中,二重指针用的比较少,大部分时候就是和指针数组纠结起来用的;
  • 编程中,有时在函数传参时为了通过函数内部改变外部的一个指针变量,会传这个指针变量的地址(就是二重指针)进去。
二重指针和数组指针:
  • 二重指针、数组指针、结构体指针、一重指针、普通变量的本质都是相同的,都是变量。
  • 所有的指针变量本质都是相同的,都是4个字节,都是用来指向别的东西的,不同类型的指针变量只是可以指向的(编译器允许你指向的)变量类型不同。
  • 二重指针就是:指针数组指针 如:int (*p)[2]; 
 

二维数组

二维数组的内存印象:
  • 一维数组在内存中是连续分布的多个内存单元组成的,而二维数组在内存中也是连续分布的多个内存单元组成的。
  • 从内存角度来看,一维数组和二维数组没有本质差别。
  • 二维数组int a[2][5]和一维数组int b[10]其实没有任何本质差别。我们可以把两者的同一单元的对应关系写下来。
  • a[0][0]     a[0][1]   a[0][4]     a[1][0]    a[1][1]      a[1][4]    
  • b[0]     b[1]       b[4]         b[5]        b[6]      b[9]
  • 既然二维数组都可以用一维数组来表示,那二维数组存在的意义和价值在哪里?明确告诉大家:二维数组a和一维数组b在内存使用效率、访问效率上是完全一样的(或者说差异是忽略不计的)。在某种情况下用二维数组而不用一维数组,原因在于二维数组好理解、代码好写、利于组织
  • 总结:我们使用二维数组(C语言提供二维数组),并不是必须,而是一种简化编程的方式。想一下,一维数组的出现其实也不是必然的,也是为了简化编程。
 
二维数组的下表式访问和指针访问:
  • 一维数组的两种访问方式:如:int b[10],  int *p = b; b[i] 等价于 *(p+i);
  • 二维数组的两种访问方式:如:int b[2][5], a[0][0] 等价于 *(*(p+0)+0); a[i][j] 等价于  *(*(p+i)+j);    
  • #include <stdio.h>
    
    int main(void)
    {
        int a[2][3] = {{1,2,3},{4,5,6}};
        int (*p)[3] = NULL;
        p = a;
        printf("a[1][2] = %d
    ", a[1][2]);
        printf("a[1][2] = %d
    ", *(*(a+1)+2));
        printf("a[1][2] = %d
    ", *(*(p+1)+2));
        return 0
    }
 
二维数组的应用和更多维数组
  • 最简单情况,有10个学生成绩要统计;如果这10个学生没有差别的一组,就用b[10];如果这10个学生天然就分为2组,每组5个,就适合用int a[2][5]来管理。
  • 最常用情况:一维数组用来表示直线,二维数组用来描述平面。数学上,用平面直角坐标系来比拟二维数组就很好理解了。
  • 三维数组和三维坐标系来比拟理解。三维数组其实就是立体空间。
  • 四维数组也是可以存在的,但是数学上有意义,现在空间中没有对应(因为人类生存的宇宙是三维的)。
  • 总结:一般常用最多就到二维数组,三维数组除了做一些特殊与数学运算有关的之外基本用不到。(四轴飞行器中运算飞行器角度、姿态时就要用到三维数组)
 

二位数组的运算和指针

指针指向二维数组的数组名
  • 二维数组的数组名表示二维数组的第一维数组中首元素的首地址(也就是第二维的数组)
  • 二维数组的数组名a等同于&a[0],这个和一维数组的符号含义是相符的。
  • 用数组指针来指向二维数组的数组名是类型匹配的。
指针指向二维数组的第一维
  • 用int *p来指向二维数组的第一维a[i];
  • 用 *(*(p+i)+j) 来指向以为数组的二维数组
指针指向二维数组的第二维
  • 二维数组的第二维元素其实就是普通变量了(a[1][1]其实就是int类型的7),已经不能用指针类型和它相互赋值了。
  • 除非int *p = &a[i][j];,类似于指针指向二维数组的第一维。 
  •     int aa[3][3] = {{1,2,3},{4,5,6},{7,8,9}};
        int (*p2)[3] = NULL;                    //定义一个数组指针,数组中有3个元素,都是int型
        int *p3 = NULL;
    
        printf("a[1][2] = %d
    ", *(*(aa+1)+2)); //*(*(aa+1)+2)<==> a[1][2]
    
        // 指针指向二维数组的数组名
        p2 = &aa[0];                            //表示二维数组第一维数组的首元素首地址
        printf("a[1][2] = %d
    ", *(*(p2+1)+2)); //*(*(p2+1)+2)<==> a[1][2]
        printf("a[2][0] = %d
    ", *(*(p2+2)+0)); //*(*(p2+2)+0)<==> a[2][0]
        
        // 指针指向二维数组的第一维
        p3 = aa[0];                         
        printf("a[0][1] = %d
    ", *(p3+1));      //*(p3+1)<==> a[0][1]
        printf("a[1][1] = %d
    ", *(p3+4));      //*(p3+4)<==> a[1][1]

     

  • 总结:二维数组和指针的纠葛,关键就是2点:
  • 数组中各个符号的含义。
  • 数组的指针式访问,尤其是二维数组的指针式访问。
 
原文地址:https://www.cnblogs.com/icefree/p/8537029.html