• 中文
    • English
  • 注册
  • 查看作者
  • C语言指针所有知识点整理,彻底搞懂指针

    一.  前言

    上个星期学校突然开了数据结构这门课,还是用C++讲,学校又不给补,只好自学C++了

    于是先把C语言中的指针这个难点整理复习一下:

    二.  指针简介

    • 指针类型的变量,简称指针

    • 指针是一个数值为地址的变量,例如int类型变量的数值是整数,而指针中所存放的数值是地址,即内存单元的编号

    • 假设指针p中存放了一个浮点型变量a的地址,通常描述为:p指向a

    • 指针变量的赋值只能赋予地址, 决不能赋予任何其它数据,否则将引起错误

    • ……

    三.  指针的定义和初始化

    方法一:指针变量初始化的方法

    int a;

    int *p = &a;   或者int a,*p = &a;

    方法二:赋值语句的方法

    int a;

    int *p;

    p=&a;//被赋值的指针变量前不能再加“*”说明符,如写为*p=&a 也是错误的

    四.  指针的运算

    许多运算对于地址进行操作是没有意义的,所以C语言对指针仅支持基本运算和赋值运算以及加减算术运算

    1.  基本运算

    1.1  取地址运算符&

    取地址运算符&其功能是取得操作数的地址,且是单目运算符,其结合性为自右至左。&不能作用在常量和表达式上,比如&22,&(a+b)都是错误的

    举例:

    #include <stdio.h>
    int main() {
        int a;
        float b;
        char c;
        scanf("%d%f%c",&a,&b,&c);//调用scanf函数时,用&分别得到变量a,b,c的地址
        return 0;
    }

    1.2  取内容运算符*

    取内容运算符*其功能是获取指定地址中的数据,且是单目运算符,其结合性为自右至左,在*运算符之后跟的变量必须是指针变量。需要注意的是指针运算符*和指针变量说明中的指针说明符* 并不是一回事。在指针变量说明中,“*”是类型说明符,表示其后的变量是指针类型。而表达式中出现的“*”则是一个运算符用以表示指针变量所指的变量,表达式中,一般*p指具体的值,而p指地址

    #include <stdio.h>
    int main() {
        int a = 3, *p = &a;
        printf("a = %d\n",a);//通过变量名直接访问该变量的存储空间
        printf("p = %d\n",*p);//通过指针p间接访问运算
        return 0;
        //第三行代码中的*p是指定义一个指针型的变量p,
        //第五行代码中的*p是指指针变量p所指向的目标变量,即变量a
        
    }

    2.  赋值运算:

    指针变量同普通变量一样,使用之前不仅要定义,而且必须赋值(大概有以下几种格式:)

    • 赋值为0
      int *p;p = 0;//p可以使用,但是它不指向具体的变量

    • 赋值为NUll

      int *p;

      p = NULL;//变量未赋值时,可以是任意值,是不能使用的。否则将造成意外错误,注意和赋值为0区分

    • 赋值为同类型的地址

      int a = 10,*p;

      p = &a;

    • 赋值为指向相同类型变量的另一个指针变量

      int a,*p = &a,*q;

      q=b;//把a的地址赋予指针变量q

    • 赋值为数组的首地址

      int a[10],*p;

      p = a;//指向数组的首地址,等同于p = &a[0];

    • 赋值为字符串的首地址
      char *p;
      p=”zhangjia.tv”;//并不是把整个字符串装入指针变量, 而是把存放该字符串的字符数组的首地址装入指针变量

    • 赋值为把函数的入口地址
      int (*p)();
      p=f; //f为函数名

    3.  加减算术运算:

    对于指向数组的指针变量,可以加上或减去一个整数n:指针变量加或减一个整数n的意义是把指针指向的当前位置(指向某数组元素)向前或向后移动n个位置

    应该注意,数组指针变量向前或向后移动一个位置

    和地址加1或减1 在概念上是不同的

    因为数组可以有不同的类型, 各种类型的数组元素所占的字节长度是不同的。

    如指针变量加1,即向后移动1 个位置表示指针变量指向下一个数据元素的首地址。

    而不是在原地址基础上加1。

    例如:

    int a[5],*p;

    p=a; //p指向数组a,也就是指向a[0]

    p=p+2; //p指向a[2]注意:指针变量的加减运算只能对数组指针变量进行, 对指向其它类型变量的指针变量(如int,double等)作加减运算是毫无意义的。两个指针变量之间的运算只有指向同一数组的两个指针变量之间才能进行运算:1 :两指针变量相减:两指针变量相减所得之差是两个指针所指数组元素之间相差的元素个数也就是指是两个指针值(地址) 相减之差再除以该数组元素的长度(字节数)

    例如pf1和pf2 是指向同一浮点数组的两个指针变量,设pf1的值为2010H,pf2的值为2000H

    而浮点数组每个元素占4个字节,所以pf1-pf2的结果为(2000H-2010H)/4=4

    表示pf1和 pf2之间相差4个元素。但是两个指针变量不能进行加法运算2 :两指针变量进行关系运算, 指向同一数组的两指针变量进行关系运算可表示它们所指数组元素之间的关系。例如:

    pf1==pf2表示pf1和pf2指向同一数组元素

    pf1>pf2表示pf1处于高地址位置

    pf1<pf2表示pf2处于低地址位置

    3 :指针与0比较:
    p为指针变量,则p==0表明p是空指针,它不指向任何变量;p!=0表示p不是空指针

    五.  指针与函数:

    1.  指针作为函数的参数:

    函数的参数可以用指针类型,向函数传递的是地址值

    指针作为函数参数可以把实参的地址传入到被调函数中,被调函数对形参修改时,会影响到实参

    (形参出现在函数定义中,实参出现在主调函数中)

    #include <stdio.h>
    void Modify(int *a,int *b) {//形参
         a = 10;//不会修改形参的值,而且这里的a是地址值
        *b = 12;//会修改形参的值
    
    }
    int main() {
        int a = 1, b = 2;
        printf("a = %d, b = %d\n",a,b);
        Modify(&a,&b);//实参,不能写Modify(a,b);
        printf("a = %d, b = %d",a,b);
        return 0;
    }
    输出:
    a = 1, b = 2
    a = 1, b = 12

    指针做函数的参数,不仅能保留函数中对实参的修改,而且由于传递的是地址,不需要生成实参的副本,所以参数传递的效率非常高,特别是传递数组,结构体等

    举例:

    #include <stdio.h>
    #define M 100
    void arrout(int *, int);
    int arrin(int *);//这里可以只写*,而不写变量名
    void main() {
        int s[M],k;
        k = arrin(s);
        arrout(s,k);
    }
    int arrin(int *a) {
        int i, x;
        i = 0;
        scanf("%d",&x);
        while(x >= 0) {
            *(a + i) = x;//等于a[i] = x;
            i++;
            scanf("%d",&x);
        }
        return i;
    }
    
    void arrout(int *a, int n) {
        int i ;
        for(i = 0; i < n; i++) {
            printf(((i + 1) % 5 == 0) ? "%4d\n" :"%4d",*(a + i));//每输出五个数字一换行
    
        }
        printf("\n");
    }
    /*
    输入:
    4 5 6 7 5 45 65 -5
    输出:
       4   5   6   7   5
      45  65
    */

    值得注意的是,在第七行代码中,这里k = arrin(s);和k = arrin(&s);的结果是一样的,因为:

    在C语言中,若函数调用时实参是数组名,则传递给对应形参的是实参数组的首地址

    2.  函数返回指针

    C语言中,函数的返回值不仅可以是整型、字符型、实型等数据,还可以是指针类型,返回指针的函数被称为指针函数,即返回值为存储某种数据的内存地址

    举例:

    int *day(int x, int y) {
    int *p;
    ......
    return p;
    }

    在指针函数中,返回的地址值可以是变量的地址、指针变量或者数组的首地址,还可以是结构体,联合体等构造数据类型的首地址

    #include <stdio.h>
    int g = 1;//定义全局变量
    
    int * gf() {
        return &g; //返回全局变量地址
    }
    
    int *sf() {
        static int s = 2;//返回局部变量地址
        return &s;
    }
    
    void main() {
        int *p;
        p = sf();//p被赋值为局部静态变量的地址,注意是地址,不是值
        *p = 4;//修改局部静态变量的值
        printf("*p = %d\t*sf = %d",*p,*sf());
    }
    输出:*p = 4  *sf = 4

    注意*sf() 由于()的优先级比*高,所以该表达式等同于*(sf()),即先算sf()得到返回的地址,再算*,得到该地址所指向的内容

    3.  指向函数的指针

    C语言中,函数的函数名表示该函数的存储首地址,如果把函数名赋予一个指针变量,就可以用该指针来调用函数,这种指针变量称为指向函数的指针,简称为:函数指针例如:int(*p)();定义了一个指向函数的指针变量p,指针p所指向的函数应有int型返回值,没有形参注意这里的(*p)里的()不能省略,以为*p的优先级低于()

     

    当给函数指针赋值后,在进行函数调用时既可以通过函数名,也可以通过函数指针,

    对函数指针进行间接访问*运算时,其结果是使程序控制流程转移到函数指针所指向的函数入口地址执行该函数。

    而数据指针进行间接访问*运算时,访问的是指定地址的数据

    #include <stdio.h>
    float max(float, float , float);//函数声明
    void main() {
        float(*p)(float, float , float);
        float a, b , c, big;//定义  函数指针
        p = max; 
        /*使函数指针p指向max()函数,作用是将max()函数入口地址赋值给函数指针p,
        此时函数指针p和函数名max都能代表函数的入口地址,
        在给函数指针赋值的时候,只要给出函数名即可,不用给出参数*/
        
        scanf("%f%f%f",&a,&b,&c);
        big = (*p)(a,b,c);//通过函数指针调用函数,与big = max(a,b,c);等价
        printf("a = %.2f\tb = %.2f\tc = %.2f\nbig = %.2f\n",a,b,c,big);
    
    }
    
    float max(float x, float y, float z) {
        float temp = x;
        if(temp < y) temp = y;
        if(temp < z) temp = z;
        return temp;
    
    }
    
    输入:
    4 6 9
    输出:
    a = 4.00        b = 6.00        c = 9.00
    big = 9.00

    在C语言中,函数指针的主要作用是作为参数在函数间传递参数,实际上传递的是函数的执行地址,或者说是传递的是函数的调用控制。

    当函数在两个函数间传递时,主调函数的实参应该是被传递函数的函数名

    而被调函数的形参是接受函数地址的函数指针

    可以给函数指针赋予不同的函数名(函数的入口地址)而调用不用的函数

    #include <stdio.h>
    int minus(int, int);//求差函数声明
    int add(int, int);//求和函数声明
    int multiply(int, int);//求积函数声明
    void process(int x, int y, int(*fun)(int, int));//处理函数声明
    
    void main() {
        int a, b;
        printf("请输入a和b:\n");
        scanf("%d,%d",&a,&b);
    
        printf("a - b = ");
        process(a,b,minus);
    
        printf("a + b = ");
        process(a,b,add);
    
        printf("a * b = ");
        process(a,b,multiply);
    }
    
    int minus(int x, int y) {
        int z;
        z = x - y;
        return z;
    }
    
    int add(int x, int y) {
        int z;
        z = x + y;
        return z;
    }
    
    int multiply(int x, int y) {
        int z;
        z = x * y;
        return z;
    }
    
    process(int x, int y, int(*fun)(int , int)) {
        int result;
        result = (*fun)(x,y);//用函数指针调用函数
        printf("%d\n",result);
    }
    输出:
    请输入a和b:
    7,8
    a - b = -1
    a + b = 15
    a * b = 56

    ps.因为时间的原因,以下内容只整理了知识点,用代码实现的并不多,以后应用到的时候,会补齐

    六.  指针和数组

    1.  指针对数组元素的访问

    当指针p指向数组a的首地址时:

    指针名p和数组名a能够相互表示,互换使用,如下表:

    C语言指针所有知识点整理,彻底搞懂指针

    在上面的知识点中,我们已经提到,数组名表示数组首地址,这个地址是在数组定义的时候就已经确定的,而且不能更改

    所以数组名(比如a)可以看成一个常量指针,任何赋值、自增等企图修改它的操作都是非法的

    而指针p可以任意赋值

    但是指针的算术运算仅支持加减整数运算(不支持乘除以及求余运算)和指针的自增/自减运算

    以及同类型指针之间的减法运算(但是不支持同类型指针之间的加法运算)

    1.1  指针的加减整数运算:

    加/减一个整数n,就是将指针的地址量加/减去(数据长度(字节数)*n)个位置

    比如int型的p指针地址为:13ff78

    p-2 = p – (4 * 2) = 13ff70

    举例:

    #include <stdio.h>
    void main() {
        int a[] = {2,4,6,8,10,12,14,16,18,20};
        int *p = &a[5];
        printf("*(p - 2) = %d\n*(p - 1) = %d\n",*(p - 2), *(p - 1));
        printf("*p = %d\n",*p);
        printf("*(p + 1) = %d\n*(p + 2) = %d\n",*(p + 1), *(p + 2));
    }
    
    输出:
    *(p - 2) = 8
    *(p - 1) = 10
    *p = 12
    *(p + 1) = 14
    *(p + 2) = 16

    上例中值得注意的是,*p+n和*(p+n)是完全不同的

    *p+n是先间接访问指针p所指对象取值后再加上n这个数字

    *(p+n)是指针p作为地址量先加上整数n,得到一个新的地址量,再通过这个地址访问所指对象

    1.2  指针自增/自减运算

    很多人将指针的自增/自减和指针的加减混为一谈,其实这是两个完全不同的概念

    在上面的例子中,我们可以发现,第二次输出的时候*(p – 1) = 10,而不是6,这就说名p本身的地址值是并没有改变的,仍指向原来的对象

    而自增/自减运算会直接改变p本身的值,我们用一个表格总结:

    C语言指针所有知识点整理,彻底搞懂指针

    举例:

    #include <stdio.h>
    void main() {
        int a[] = {2,4,6,9,11};
        int *p = a;
        
        printf("*p = %d\n",*p);
        printf("*p++ = %d\n",*p++);//此时p的值已经被改变,但是表达式仍是原对象地址,等价于*(p++);
        printf("*++p = %d\n",*++p);//*和++运算符优先级相等,但是从右向左结合
        printf("(*p)++ = %d\n",(*p)++);//先进行*运算,访问到a[2]的值,再使a[2]的值加上数字1,而表达式仍是原对象a[2]的值
        printf("++(*p) = %d\n",++(*p));/先进行*运算,访问到a[2]的值(此时a[2] = 7)再使a[2]的值加上数字1,a[2] = 8
    }
    
    输出:
    *p = 2
    *p++ = 2
    *++p = 6
    (*p)++ = 6
    ++(*p) = 8

    1.3  两个同类型指针相减

    两个同类型指针相减,结果为两个指针所指向的的地址位置之间所包含对象的个数,其结果不是地址量,而是整数计算方式: (p中的地址值 – q中的地址值) /  数据长度(字节数)

    1.4  指针的关系运算

    若p和q为同类型的指针,则若指针p和指针q同时指向同一个位置,则 p == q若指针p指向的位置在q前方,则p < q若指针q指向的位置在q后方,则p > q

    #include <stdio.h>
    void main() {
        int a[] = {2,4,6,9,11};
        int *p = a;
        int *q = a;
        printf("*p == q ? %d\n",p == q);
    
        p = &a[2];//p在q后方
        printf("*p > q ? %d\n",p > q);
    
        q = &a[4];//p在q前方
        printf("*p > q ? %d\n",p > q);
    }
    输出:
    *p == q ? 1
    *p > q ? 1
    *p > q ? 0

    2.  字符指针

    字符指针和字符串数组都能实现对字符串的处理 字符数组由元素组成,每个元素存放一个字符,而字符指针中存放的是字符串的地址 只能对字符数组中的各个元素赋值,不能通过赋值语句对整个字符数组赋值,例如:char s[20]; s= “aaaaa”;是不允许的但是可以通过赋值语句对字符指针变量赋值,赋的是字符串的首地址:例如:char *p; p = “aaaa”; 是可以的

    字符数组名虽然代表地址,但是数组名的值不能改变,例如:char s[] = “aaaa”; s = s + 4;是不允许的

    但是字符指针变量的值可以改变,例如:char *p= “aaaaa”; p = p + 4;是可以的

    可以用下标形式引用指针所指向的字符串中的字符,例如:char *p; p = “aaaa”; printf(“%c\n”,p[4]);

    可以通过键盘输入字符串的方式为字符数组输入字符元素,但是不能通过输入函数让字符指针变量指向另一个字符串,因为键盘输入的字符串,系统是不分配存储空间的

    char s[100] ,*p; 
    scanf("%s",s);//可以
    scanf("%s",p);//不可以,指针p未指向明确地址
    strcpy(p,"Hello");//不可以,指针p未指向明确地址
    
    p = s;//指针p指向数组s
    scanf("%s",p);//可以,通过指针访问数组s
    strcpy(p,"Hello");//可以,通过指针修改数组s

    将字符串常量通过赋值语句赋予字符指针后,其中的字符不能被修改

    char s[100] = "zhangjia.tv";
    char *p;
    p = "https://zhangjia.pro";
    s[3] = "z";//可以,访问数组元素并修改
    p[3] = "z";//不可以,指针p所指向的是字符串常量,不可以修改其元素
    strcpy(p,"Hello");//不可以,指针p所指向的是字符串常量,不能修改

    3.  指向数组的指针

    C语言中二维数组可以看成元素是一维数组的一个一维数组,因此,对二维数组的访问可以通过指向数组的指针来实现。

    例如:

    int a[3][4];
    int (*p)[4]; //注意:这里[]的优先级比*高,所以一定要加()
    p = a;

    4.  指针数组

    一个数组的所有元素均为指针类型,称为指针数组。指针数组的每个元素都是指针变量

    例如:int *p[4];

    在二维数组中,有以下四种方式可以使用

    int a[2][3],*p[2];
    p[0] = a[0]; //或者p[0] = &[0][0];
    p[1] = a[1]; //或者p[1] = &[1][0];

    则:

    a[i][j]

    p[i][j]

    *(a[i] + j)

    *(p[i] + j)

    都是意义相同的表示方法

    5.  命令行参数

    指针数组的一个重要应用是作为main函数的形参,格式如下:

    main(int argc, char *argv[]) {
    ......
    }

    其中

    argc是命令行中参数的个数(可执行文件名本身也算一个)

    argv是一个字符指针数组,元素为指向实参字符串的指针argv[0]指向第一个实参字符串“文件名”。argv[1]指向第二个实参字符串。argv[2]指向第三个实参字符串

    运行带形参的主函数,必须在操作系统状态(一般DOS)下,输入带形参的主函数所在的可执行文件名,以及所需的实参(字符串),然后回车即可

    例如:

    #include <stdio.h>
    void main(int argc, char *argv[]) {
    while(argc-- > 0)
        printf("%s\n",*++argv); 
    }

    将上例保存文件名为:test.c,然后编译链接生成可执行文件:test.exe

    然后再DOS状态下中,打开test.exe所在的目录,输入:

    test.exe  Hello World

    程序将输出命令行中包括可执行文件在内的以空格分隔的所有字符串

    输出:
    test.exe
    Hello
    World

    6.  指向指针的指针

    如果在一个指针变量中存放的是一个目标变量的地址叫做一级指针,也就是通常咱们说的指针

    如果在一个指针变量中存放的是指向目标变量的地址的指针变量的地址,那么这个就叫做二级指针,也就是指向指针的指针

    例如:

    void main()
    {
        int a =99;
        int *pa =&a;//一级指针
        int **ppa =&pa;//二级指针
    }

    七.  动态内存分配

    当程序中定义了变量或者数组的时候,系统会在程序编译的时候分配给变量或者数组相应大小的内存单元,比如float a[5];

    这种分配固定大小的内存分配方法称为静态内存分配,经常会根据程序的需要手动修改数组的存储空间

    为了解决这个问题,C语言提供了动态分配内存的方法解决这个问题

    所谓动态分配内存,就是指在程序执行的过程中,动态地分配或者回收存储空间的内存分配方法

    动态内存分配不需要和静态内存分配一样,需要预先分配存储空间,而是由系统判断程序的需要及时分配

    1.  动态内存分配的步骤

    1. 要确切知道需要多少内存空间,避免空间的浪费

    2. 利用C标准库提供的动态分配函数来分配所需要的存储空间

    3. 使用指针指向获得的内存空间,并通过指针在该空间内实施运算或者操作

    4. 当对动态分配的内存操作完之后,一定要释放这一空间

    常用的动态内存管理函数:(都需要包含头文件stdlib.h)

    2.  带计数和清零的动态内存分配函数calloc()

    函数原型:

    void *calloc(unsigned n, unsigned size)

    n和size 都是无符号整型,n表示要存放数据元素的个数,size代表存放数据元素的大小

    它的功能是动态的分配n个size字节大小的连续存储空间,并且把存储空间全部清零

    若分配成功,则函数返回一个指向分配存储空间起始地址的指针,由于该地址内的数据类型无法确定,故为一个void型指针

    若没有足够的内存满足要求,则返回空指针NULLL,所以在使用内存前,最好判断一下,一般用sizeof确定该类型数据所占字节

    例如:

    int n, *p;
    scanf("%d",&n);
    p = (int *)calloc(n,sizeof(int));//分配n个连续的整型单元,首地址赋给p
    
    if(p == NULL) {//如果分配失败,退出
    printf("失败");
    exit(0);
    }

     3.  按照指定的字节分配内存的函数malloc()

    函数原型:

    void *malloc(unsigned size)

    size无符号整型,它的功能是动态分配size个字节的连续存储空间

    例如:

    int *p1, *p2,n;
    scanf("%d",&n);
    if((p1 = (int *)malloc(80)) == NULL) exit(1);
    if((p2 = (int *)malloc(n * sizeof(int))) == NULL) exit(1);
    如果成功,分别得到80和n * sizeof(int)字节的存储空间

    强调几点:

    1. malloc()函数前面必须要加上一个指针类型的转换符,因为该函数的返回值是空类型的指针,一般应该与右边的指针变量类型一致

    2. malloc()函数所带的一个参数是指需分配的内存单元字节数

    3. malloc()函数可能返回NULL,表示内存分配失败,因此也要检查分配的内存指针是否为空

    4.  动态重分配函数realloc()

    函数原型:void *realloc(void *p,unsigned size)p是以前通过动态分配得到的存储空间起始地址

    其功能是对指针p所指向的已动态分配的存储空间重新进行分配,新分配的大小为size字节

    该函数的返回值是新分配内存区的起始地址

    char *p;
    if((p = (char *)malloc(17) == NULL)  exit(1);
    strcpy(p,"this is 16 chars");
    p = (char *)realloc(p,18);
    if(p == NULL) exit(1);
    strcat(p,".");

    先用malloc()函数申请17个字节的内存,存放16个字符的字符串又用realloc()函数重新申请18个字节的内存,用于末尾添加“.”在使用realloc()函数时,若p = NULL,他就当当与malloc(size)的功能若size = 0,则相当于下面的free()功能

    5.  释放动态内存的函数free()

    函数原型:void free (void *p)p是指向待释放存储空间首地址的指针,其功能是释放不再使用的动态内存该函数没有返回值,free()

  • 0
  • 0
  • 0
  • 6k
  • zjmarina

    请登录之后再进行评论

    登录
    单栏布局 侧栏位置: