C语言 - 指针

总结一句话:指针就是地址

指针的格式:

  1. 数据类型 *指针变量名 = 初始地址值;
  • 数据类型是指针变量所指向变量数据类型。可以是 int、char、float 等基本类型,也可以是数组等构造类型。

  • 字符 * 用于告知系统这里定义的是一个指针变量,通常跟在类型关键字的后面。比如, char * 表示一个指向字符的指针, float * 表示一个指向 float 类型的指针。此外,还有指向数组的指针、指向结构体的指针。

    1
    int *p;  //读作:指向int类型的指针”或简称“int指针”
  • 这是一个指针变量,用于存储int型的整数在内存空间中数据的地址。

  • 注意:

  • 1、指针变量的名字是 p,不是 *p。

  • 2、指针变量中只能存放地址,不要将一个整数(或任何其它非地址类型的数据)赋给一个指针变量。

取址运算符:&

  1. 作用:取出指定变量在内存中的地址

  2. 语法格式:&变量;(p=&a)

  3. 指针=&变量(只是表示这个指针指向这个变量的地址,变相说明这个地址存储的内容,也给了这个指针)

  4. 举例:

    1
    2
    3
    int num = 10; 
    printf("num = %d\n", num); // 输出变量的值。 num = 10
    printf("&num = %p\n", &num); // 输出变量的内存地址。&num = 00000050593ffbbc
    • 说明:

    • 1、在输出取址运算获得的地址时,需要使用“%p”作为格式输出符。

    • 2、这里num的4个字节,每个字节都有地址,取出的是第一个字节的地址(较小的地址)。

    • 将变量的地址赋值给指针变量:

    1
    2
    3
    int num = 10;
    int *p; //p为一个整型指针变量
    p = #
    1
    2
    3
    int a,*p;
    p=&a;
    以后p也就代表着&a;

指针变量的赋值:

  1. 指针变量中只能存放地址(指针),不要将一个整数(或任何其它非地址类型的数据)赋给一个指针变量。

  2. C语言中的地址包括位置信息(内存编号,或称纯地址)和它所指向的数据的类型信息,即它是“带类型的地址”。所以,一个指针变量只能指向同一个类型的变量,不能抛开类型随意赋值。

    • char* 类型的指针是为了存放 char 类型变量的地址。
    • short* 类型的指针是为了存放 short 类型变量的地址。
    • int* 类型的指针是为了存放 int 类型变量的地址。
  3. 在没有对指针变量赋值时,指针变量的值是不确定的,可能系统会分配一个未知的地址,此时使用此指针变量可能会导致不可预料的后果甚至是系统崩溃。为了避免这个问题,通常给指针变量赋初始值为0(或NULL),并把值为0的指针变量称为空指针变量(野指针)

    1
    2
    3
    4
    5
    6
    7
    8
    int main() {
    int num = 10, *ptr;
    ptr = #
    printf("%d\n",num);
    scanf("%d", ptr); //等价于scanf("%d", &num);
    printf("%d\n",num);
    return 0;
    }

取值运算符:*(就是解引用):

  1. 作用:根据一个给定的内存地址取出该地址对应变量的值

  2. 语法格式:* 指针表达式;(*p=5)

  3. “* 指针=指针指定的变量的值”(前面加上*表示这个指针等于这个变量的值)

  4. *”不同于定义指针变量的符号,这里是运算符。“指针表达式”用于得到一个内存地址,与“*”结合以获得该内存地址对应变量的值。

    1
    2
    3
    int a=5,*p;
    p=&a;
    printf("%d\n",*p);//等价于*(&a) 先得到地址,再进行求值
  • 举例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int main() {

    int a = 2024;
    int *p;
    p = &a;

    printf("%p\n",&a); //0000005cc43ff6d4
    printf("%p\n",p); //0000005cc43ff6d4
    printf("%d\n", *p); //2024

    return 0;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int main() {

    int num = 10; //这里定义一个整型变量num
    printf("num = %d\n", num); //输出变量num的值。输出:num = 10
    printf("&num = %p\n", &num); //输出变量num的地址。输出:&num = 000000e6a11ffa1c

    int *p = #
    printf("%p\n",p); //000000e6a11ffa1c
    printf("%d\n",*p);//10

    printf("*&num = %d\n", *&num);//通过num地址读取num中的数据。输出:*&num = 10

    return 0;
    }

    通过指针变量修改指向内存地址位置上的值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int main() {

    int num = 10;
    int *p = #
    *p = 20;
    printf("num = %d\n",num); //num = 20

    char ch = 'w';
    char* pc = &ch;
    *pc = 's';
    printf("ch = %c\n", ch); //ch = 's'

    return 0;
    }

& 运算符与 * 运算符:

  1. & 运算符与 * 运算符互为逆运算

    1
    2
    int i = 5;
    if (i == *(&i)) // 正确
  2. &*p的含义:

    • “&”“*”两个运算符的优先级别相同,但按自右而左方向运算。因此,&*p&a相同,即变量a的地址。
    • 如果有p1 = &*p; 它的作用是将&a (a的地址)赋给p1 ,如果p1原来指向 b,经过重新赋值后它已不再指向b了,而指向了a。
  3. *&a的含义:

    • 先进行&a运算,得a的地址,再进行*运算。*&a*p的作用是一样的,它们都等价于变量a。即*&a与 a 等价。

指针与数组:(解释)

  1. C语言规定:数组的名字是数组的首地址,即第0个元素的地址。如&a[0]

  2. **注意:***p和a[5],p和a不同的是:p是指针变量,a是个常量,所以可以用等号给p赋值,但是不能用等号给a赋值

  3. 如:p=&a[0]正确;a=&b[0];错误

  4. 对于长度是 N 的一维数组 a,当使用指针 p 指向其首元素后,即可通过指针 p 访问数组的各个元素

  5. &数组名[某数] 相当于 数组名+某数

  6. 数组名[某数] 相当于 *(数组名+某数)

    1
    2
    3
    4
    5
    6
    7
    8
    int a[5],*p;
    p=a;//数组的首地址给p
    p+1也就等与a[1]的地址
    *(p+1)也就等于a[1]的值

    a[0]用 *p 表示
    a[1]用*(p+1)表示
    a[i]用*(p+i)表示

    指针与数组

指针的运算:

  1. 指针本质上就是一个无符号整数,代表了内存地址。除了上面提到的取址运算外,指针还可以与整数加减、自增自减、同类指针相减运算等。

  2. 指针与整数值的加减运算:

    1. 格式:指针±整数

    2. 指针与整数值的加减运算,表示指针所指向的内存地址的移动(加,向后移动;减,向前移动)。指针移动的单位,与指针指向的数据类型有关。数据类型占据多少个字节,每单位就移动多少个字节。

    3. 比如:变量a、b、c、d和e都是整型数据int类型,它们在内存中占据一块连续的存储区域。指针变量p指向变量a,也就是p的值是0xFF12,则:

      指针的运算1

    4. 说明:指针p+1并不是地址+1,而是指针p指向数组中的下一个数据。比如,int *p,p+1表示当前地址+4,指向下一个整型数据。

  3. 指针的自增、自减运算:

    1. 针对指针的增加或减少指的是内存地址的向前或向后移动
    2. 当对指针进行++时,指针会按照它指向的数据类型字节数大小增加,比如 int * 指针,每 ++ 一次, 就增加4个字节。
    3. 当对指针进行--时,指针会按照它指向的数据类型字节数大小减少,比如 int * 指针,每--一次, 就减少4个字节。
  4. 同类指针相减运算:(相加是非法的)

    1. 格式:指针 - 指针

    2. 相同类型的指针允许进行减法运算,返回它们之间的距离,即相隔多少个数据单位(注意:非字节数)。高位地址减去低位地址,返回的是正值;低位地址减去高位地址,返回的是负值。

      1
      2
      3
      4
      5
      6
      7
      8
      int main() {

      short *ps1;
      short *ps2;
      ps1 = (short *) 0x1234;
      ps2 = (short *) 0x1236;
      ptrdiff_t dist = ps2 - ps1;
      printf("%d\n", dist); // 1 相差2个字节正好存放1个 short 类型的值。
    3. 两个指针相减,通常两个指针都是指向同一数组中的元素才有意义。结果是两个地址之差除以数组元素的长度。不相干的两个变量的地址,通常没有做减法的必要。

      指针的运算2

  5. 指针间的比较运算:

    1. 指针之间的比较运算,比如 ==、!= 、<、 <= 、 >、 >=。比较的是各自的内存****地址的大小,返回值是整数 1 (true)或 0 (false)。

      1
      2
      3
      4
      5
      6
      7
      8
      int arr[5] = {1, 2, 3, 4, 5};
      int *p1 = &arr[0];
      int *p2 = &arr[3];

      printf("%d\n",p1 > p2); //0
      printf("%d\n",p1 < p2); //1
      printf("%d\n",p1 == p2); //0
      printf("%d\n",p1 != p2); //1

野指针:

  1. 野指针:就是指针指向的位置是不可知(随机性不正确没有明确限制的)。

  2. 形成野指针的原因:

    1. 指针使用前未初始化:

      • 指针变量在定义时如果未初始化,其值是随机的,此时操作指针就是去访问一个不确定的地址,所以结果是不可知的。此时p就为野指针。

        1
        2
        3
        4
        5
        6
        int main() {
        int *p;
        printf("%d\n",*p);//错误 未初始化 直接访问

        return 0;
        }
    2. 指针越界访问:

      • 当i=10时,此时*p访问的内存空间不在数组有效范围内,此时*p就属于非法访问内存空间,p为野指针。

        野指针

    3. 指针指向已释放的空间

  3. 野指针的避免:

    1. 定义指针的时候,如果没有确切的地址赋值,为指针变量赋一个 NULL 值是好的编程习惯。即
    2. int *p = NULL;
    3. 赋为 NULL 值的指针被称为空指针,NULL 指针是一个定义在标准库 <stdio.h>中的值为零的常量 #define NULL 0

二级指针:(多重指针)

  1. 指针的指针:即指针的地址,先定义一个指针变量,指针变量本身也有自己的地址,从而拿另一个指针来存储这个指针的地址。

  2. 简单来说,二级指针就是一个指针变量存储的是另外一个指针变量的地址。通俗来说,二级指针就是指向指针的指针

  3. 一个指针p1记录一个变量的地址。由于指针p1也是变量,自然也有地址,那么p1变量的地址可以用另一个指针p2来记录。则p2就称为二级指针

  4. 格式:数据类型 **指针名; 如:int **p;

    二级指针

    • 进而推理,会有int ***pppa = &ppa; 等情况,但这些情况一般不会遇到。

指针与数组:(专题)

  • *“,称为解引用符号,其作用与&相反
  • *“,后面只能跟指针变量(即地址),”&“后面跟的是普通变量(包括指针变量)。
  1. 一维数组与指针:

    1. 可以用一个指针变量指向一个数组元素。

      指针与数组1

    2. 如果指针变量p的初值为&a[0],则:

      • p+ia+i就是数组元素a[i]的地址。或者说,它们指向a数组序号为i的元素。
      • *(p+i)*(a+i)p+ia+i所指向的数组元素的值,即a[i]的值。
    3. 指向数组元素的**指针变量**也可以带下标,如p[i]。p[i]被处理成*(p+i),如果p是指向一个整型数组元素a[0],则p[i]代表a[i]。但是必须弄清楚p的当前值是什么:如果当前p指向a[3],则p[2]并不代表a[2],而是a[3+2],即a[5]。

    4. &数组名:

      1
      2
      3
      int arr[5];
      printf("%p\n", arr); //000000000034fa50
      printf("%p\n", &arr); //000000000034fa50

      虽然输出结果是一样的,但是arr表示arr[0]元素的地址,而&arr则表示的是arr数组的地址

  2. 二维数组与指针:

    1. 数组名[数1][数2] 等价于 *(数组名[数1]+数2)

    2. 如:a[1][2]==*(a[1]+2)

    3. &数组名[数1][数2] 等价于 数组名[数1]+数2

    4. 如:&a[1][2]==a[1]+2

    5. 获取数组元素值的三种表示形式:

      • 1) a[i][j] 下标法
      • 2) *(a[i]+j) 用一维数组名
      • 3) ((a+i)+j) 用二维数组名
    6. 设有一个二维数组 a 定义为:

      1
      2
      3
      int a[3][4] = {{1, 2,  3,  4},
      {5, 6, 7, 8},
      {9, 10, 11, 12}};
    7. 二维数组 a,可视为三个一维数组:a[0]、a[1]、a[2];而每个一维数组又是分别由 4 个元素组成。首先,理解如下的操作:

      1
      2
      3
      4
      5
      printf("%d\n",a[0][0]);  //二维数组中元素a[0][0]的值
      printf("%p\n",&a[0][0]); //二维数组中元素a[0][0]的值对应的地址
      printf("%p\n",a[0]); //二维数组中a[0][0]的地址
      printf("%p\n",a); //二维数组中a[0]的地址
      printf("%p\n",&a); //二维数组a的地址

      指针与数组2

      指针与数组3

    • 总结:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      &a:二维数组a的地址
      a: 二维数组中a[0]的地址
      a[0]:二维数组中a[0][0]的地址

      讨论:a[0][0]相关的
      a[0][0]的地址:&a[0][0],a[0],*a,
      a[0][0]的值: a[0][0],*(a[0]),**a,

      讨论:a[1]相关的
      a[1]的地址:&a[1],a + 1

      讨论:a[1][0]相关的
      a[1][0]的地址:&a[1][0],a[1],*(a+1)
      a[1][0]的值:a[1][0],*a[1],*(*(a+1))

      讨论:a[1][2]相关的
      a[1][2]的地址:&a[1][2],a[1]+2,*(a+1)+2
      a[1][2]的值:a[1][2],*(a[1]+2),*(*(a+1)+2)
    • 注意:如果 a 是二维数组,则 a[i]代表一个数组名, a[i]并不占内存单元,也不能存放 a 数组元素值。它只是一个地址。所以:a、a+i、a[i]、(a+i)、(a+i)+j、a[i]+j 都是地址。

  3. 使用指针变量访问:

    1. 设 p 是指针变量,若p 指向数组首元素,即p = a[0]

    2. p+j 将指向 a[0] 数组中的元素 a[0][j]

    3. 对于二维数组a[M][N]来讲,由于 a[0]、a[1]、… 、a[M-1]等各行数组在内存中是依次连续存储,则对于 a 数组中的任一元素 a[i][j]

      1. 地址表示:p+i*N+j
      2. 值表示:*(p+i*N+j)p[i*N+j]
      3. 数组在内存当中是连续存放的,不论是几维数组。所以在公式二维数组当中 i*N 就是把前面的有几行,一行有几个元素都算上,再加上本行的第 j 个元素
    4. 举例:

      1
      2
      3
      4
      5
      6
      int b[4][3] = {{10, 20, 30},
      {40, 50, 60},
      {70, 80, 90},
      {100, 110, 120}};

      int *p = b[0];
      1. 则:元素 b[1][2]对应的地址/指针、元素值为:
      1
      2
      3
      printf("b[1][2]对应的地址/指针为:%p\n",p+1*3+2);
      printf("b[1][2]对应的值为:%d\n",*(p+1*3+2));
      printf("b[1][2]对应的值为:%d\n",p[1*3+2]);

数组指针:(是个指针)

  1. 当指针变量里存放一个数组的首地址时,此指针变量称为指向数组的指针变量,简称数组指针
  2. 二维数组a中。a+1指向下一个元素。即下一个一维数组,即下一行
  3. 整型指针: int * pint; 能够指向整型数据的指针。
  4. 浮点型指针: float * pf; 能够指向浮点型数据的指针。
  5. 数组指针:int (*p)[5] 能够指向数组的指针。

指针数组:(是个数组)

  1. 数组是用来存放一系列相同类型的数据,当然数组也可以用来存放指针,这种用来存放指针的数组被称为指针数组,它要求存放在数组中指针的数据类型必须一致

  2. 指针数组:是一个数组,其中的每个元素都是一个指针。也就是说,数组中的每个元素存储的是某个变量的地址。

  3. 格式:数据类型 *指针数组名[大小];

  4. 举例:int *arr[5];

  5. arr是一个数组,有5个元素,每个元素是一个整型指针,需要使用下标来区分。

    指针数组1

    指针数组2

字符指针:

  • 一个字符串,可以使用一维字符数组表示,也可以使用字符指针来表示。

  • 字符数组由若干个元素组成,每个元素放一个字符

  • 字符指针变量中存放的是地址(字符串/字符数组的首地址),绝不是将字符串放到字符指针变量中。

  • 举例:

    1
    2
    char str[] = "hello tom";  //使用字符数组
    char * pStr = "hello tom"; //使用字符指针
  • 图示:

    字符指针1

  • 两种方式对比:

    • 数组名是一个常量,在定义好数组以后,不可以给数组重新赋值为一个新的数组,但是可以通过角标的方式获取或修改指定索引位置上的元素值。

    • 指针是一个变量。可以多次重新赋值

    • 对已声明好的字符数组,只能一一对各个元素赋值,不能用以下错误方法对字符数组赋值,而对字符指针变量,采用如下方式赋值是可以的。

      1
      2
      3
      4
      5
      char str[14];
      str[0] = 'i'; //正确
      str = "hello Tom"; //错误
      char * pStr = "hel";
      pStr = "hello tom"; //正确
    • 一个字符数组,因为它有确定的内存地址,所以字符数组名是一个常量。而定义一个字符指针变量时,它在指向某个确定的字符串数据的情况下,也可以多次重新赋值

      字符指针2

字符串数组和字符串指针表示:

  1. 如果一个数组的每个成员都是一个字符串,则构成了字符串数组。字符串数组有两种表示方式:① 二维字符数组②字符指针数组

  2. 方式一:使用二维字符数组

    1
    char fruit[][7]={"Apple","Orange","Grape","Pear","Peach"};  //上一章5.6节举例4
    1
    2
    3
    4
    5
    6
    7
    8
    9
    char weekdays[7][10] = {   //行数7也可以省略
    "Monday",
    "Tuesday",
    "Wednesday",
    "Thursday",
    "Friday",
    "Saturday",
    "Sunday"
    };
    1. 字符串数组,一共包含7个字符串,所以第一维的长度是7。其中,最长的字符串的长度是10(含结尾的终止符 \0 ),所以第二维的长度统一设为10。
    2. 数组的第二维,长度统一定为10,有点浪费空间,因为大多数成员的长度都小于10。解决方法就是把数组的第二维,从字符数组改成字符指针。所以说使用字符指针数组比使用二维字符数组可读性更好
  3. 方式二:字符指针数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    char* weekdays[7] = {  //7也可以省略
    "Monday",
    "Tuesday",
    "Wednesday",
    "Thursday",
    "Friday",
    "Saturday",
    "Sunday"
    };
    1. 上面的字符串数组,其实是一个一维数组,成员就是7个字符指针,每个指针指向一个字符串(字符数组)。
    2. 类似的:定义表示颜色的指针数组 colors,存储“red”、“yellow”、“blue”、“white”、“black” 5 种颜色。
    1
    char *colors[5] = {"red", "yellow", "blue", "white", "black"};
  4. 两种方式的遍历是一样的:

    1
    2
    3
    for (int i = 0; i < 7; i++) {
    printf("%s\n", weekdays[i]);
    }

指向固定长度数组的指针变量:

  1. 定义格式:(*标识符)[一维数组元素个数];
  2. 例如:定义一个指针变量 p,它指向包含有 4 个元素的一维数组。
  • int (*p)[4];

说明:p先和*结合,说明p是一个指针变量,指向一个大小为4的整型数组。

注意:此时定义的是一个指针变量,并非是一个指针数组。(*p 必须放在括弧内,否则就变成了定义指针数组。)

  • 由于 p 是指向有 4 个整型元素的一维数组的指针变量,因此,p+1 是将地址值加上 4*4,即指向下一个一维数组。

  • 举例:

    1
    2
    3
    4
    5
    int a[3][4] = {{1, 2,  3,  4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}};
    int (*q)[4];
    q = a;

q 为二维数组第 0 行首地址,与 a 相同;

q+1 为二维数组第 1 行首地址,与 a+1相同;

q+2 为二维数组第 2 行首地址,与 a+2相同;

*(q+i)为二维数组第 i 行第 0 列元素的地址,与*(a+i)相同;

*(q+i)+j 为二维数组第 i 行第 j 列元素的地址,与*(a+i)+j 相同;

*(*(q+i)+j) 为二维数组第 i 行第 j 列元素值,与*(*(a+i)+j)相同,即 a[i][j]

容易混淆的概念:

  1. 指针数组:
    1. 定义:是个数组,有若干个相同类型的指针构成的集合
    2. int *p[5],表示数组p有5个int *类型的指针变量构成,分别是p[0]~p[4]
  2. 数组指针:
    1. 定义:是个指针,指向一个数组,加1跳一个数组
    2. int (*p)[5],p是一个指针,p是一个数组指针,p+1表示指向下个数组跳5个整形
  3. 指针的指针:(二级指针)
    1. int **p;p是指针的指针
    2. int *q;
    3. p=&q;用p来存储指针q的地址

参考链接: