【C】指针

指针

变量在内存中的存放

在内存中,字节是最小的存储单元。对于一个内存单元来说,指针就是单元的地址,每个地址可以存放一个字节的数据。存放一个整形变量(int)就需要动用到四个存储单元。

在内存中完全没有存储变量名的必要。

因为编译器知道具体每一个变量名对应的存放地址,所以当你读取某个变量的时候,编译器就会找到变量名所在的地址,并根据变量的类型读取相应范围的数据

地址 存放的值
...
10000 'H'
10001 'e'
10002 'l'
10003 'l'
10004 'o'
10005 '123'
10006
10007
10008
10009 'W'
...

指针和指针变量

通常我们所说的指针,就是地址的意思。C 语言中有专门的指针变量用于存放指针,跟普通变量不同,指针变量存储的是一个地址。即,指针是变量的地址,而指针变量是存放指针的地址

定义指针变量

  • 类型名 *指针变量名

    char *pa; //定义一个指向字符型的指针变量

    int *pb; //定义一个指向整形的指针变量

  • 指针变量的数据类型就是指针变量中存放的地址所指向的内存单元的数据类型

取地址运算符和取值运算符

  • 如果需要获取某个变量的地址,可以使用取地址运算符(&):

    char *pa = &a;

    int *pb = &f;

  • 如果需要访问指针变量指向的数据,可以使用取值运算符(*):

    printf("%c, %d\n", *pa, *pb);

    注意:取值运算符跟定义指针用的都是星号(*),这属于符号的重用,在不同的地方有不同的意义:在定义时表示定义一个指针变量;在其他位置表示获取指针变量指向的变量的值。

  • 直接通过变量名来访问变量的值,我们称之为直接访问;通过指针变量这样的形式来访问变量的值,我们称之为间接访问,所以取值运算符有时候也叫间接运算符

    int main()
    {
        char a = 'F';
        int f = 123;
        char *pa = &a; //获取变量a的地址然后存放到指针变量pa中
        int *pb = &f;
        
        printf("a = %c\n", *pa); 
        printf("f = %d\n", *pb);
        
        *pa = 'C';
        *pb += 1;
        
        printf("now, a = %c\n", *pa);
        printf("now, f = %d\n", *pb);
        printf("sizeof pa = %d\n", sizeof(pa));
        printf("sizeof pb = %d\n", sizeof(pb));
        printf("the add of a is: %p\n",pa); //%p 地址输出
        printf("the add of f is: %p\n",pb);
        return 0;
    }
    

    运行结果

    a = F
    f = 123
    now, a = C
    now, f = 124
    sizeof pa = 8 //此处64位系统是8,32位系统则是4
    sizeof pb = 8 //同上
    the add of a is: 000000c48e9ffabf  
    the add of f is: 000000c48e9ffab8
    
    

避免访问未初始化的指针

int main()
{
    int *a;
	*a = 123;

	return 0;
}

类似于上边这样的代码是很危险的,因为指针变量 a 到底指向哪里,我们没办法知道。这个道理就跟访问未初始化的变量一样,它的值是随机的。

这在指针变量里会很危险,因为后边代码对一个未知地址进行赋值,那么你可能会覆盖到系统的一些关键代码。不过你也别高兴得太早,因为系统通常都不会允许你这么干,程序这时候会被终止并报错。

更危险的是,偶尔这个指针变量里随机存放的是一个合法的地址,那么接下来的赋值就会导致那个位置的值莫名其妙地被修改。这种类型的 Bug 是非常难以排查的

所以,在对指针进行间接访问时,必须确保它们已经被正确地初始化。

指针和数组

scanf函数用于从标准输入流中读取一个数据到变量中。scanf函数中通常要在变量前加上取值操作符(&)。但如果存储的位置是一个指针变量,则不用加上取值操作符。

int main()
{
    int a;
    int *p = &a;
    printf("请输入一个整数:");
    scanf("%d", &a); //a为变量,需要加取值操作符
    printf("a = %d\n", a);
    
    printf("请重新输入一个整数:");
    scanf("%d", p); //p为指针变量,无需加取值操作符
    printf("a = %d\n", a);
    
    return 0;
}

运行结果

请输入一个整数:3
a = 3
请重新输入一个整数:5
a = 5

数组名的真实身份

  • 数组名是数组第一个元素的地址,也是数组的首地址
int main()
{
    char str[128];
    printf("请输入域名:");
    scanf("%s",str); //数组无需使用取值操作符
    printf("你输入的域名是:%s\n",str);
    printf("str的地址是:%p\n",str); //%p地址输出str的地址
    printf("str的地址是:%p\n",&str[0]); //输出数组str里第一个元素的地址,需用取值操作符
    return 0;
}
  • 由于数组是一堆类型一致的变量挨个排放到一起的,所以第二个元素的地址就是第一个元素的地址加上每个元素所占的空间

    int main()
    {
        char a[] = "Hello"; //字符型1个字节
        int b[5] = {1,2,3,4,5}; //整形4个字节
        float c[5] = {1.1,2.2,3.3,4.4,5.5}; //float4个字节
        double d[5] = {1.1,2.2,3.3,4.4,5.5}; //double8个字节
        printf("a[0] -> %p, a[1] -> %p, a[2] -> %p\n", &a[0], &a[1], &a[2]);
        printf("b[0] -> %p, b[1] -> %p, b[2] -> %p\n", &b[0], &b[1], &b[2]);
        printf("c[0] -> %p, c[1] -> %p, c[2] -> %p\n", &c[0], &c[1], &c[2]);
        printf("d[0] -> %p, d[1] -> %p, d[2] -> %p\n", &d[0], &d[1], &d[2]);
        return 0;
    }
    

    运行结果

    //注: 每个人运行出来的地址并不完全相同
    a[0] -> 00000076bb5ffaca, a[1] -> 00000076bb5ffacb, a[2] -> 00000076bb5ffacc //16进制 aca -> aca+1=acb -> acb+1=acc
    b[0] -> 00000076bb5ffab0, b[1] -> 00000076bb5ffab4, b[2] -> 00000076bb5ffab8 //ab0 -> ab0+4=ab4 -> ab4+4=ab8
    c[0] -> 00000076bb5ffa90, c[1] -> 00000076bb5ffa94, c[2] -> 00000076bb5ffa98 //a90 ->a90+4=a94 -> a94+4=a98
    d[0] -> 00000076bb5ffa60, d[1] -> 00000076bb5ffa68, d[2] -> 00000076bb5ffa70  //a60 -> a60+8=a68 -> a68+8=a70(16进制:8+8=16进一)
    

指向数组的指针

  • 如果用一个指针指向数组,应该:

    char a[] = "Hello"; //定义数组
    char *p;  
    p = a; //语句1
    p = &a[0];  //语句2,指向数组首元素, 与语句1等值
    

指针的运算

  • 当指针指向数组元素的时候,我们可以对指针变量进行加减运算,这样做的意义相当于指向距离指针所在位置向前向后第n个元素。

    int main()
    {
        char a[] = "Hello"; 
        char *p = a;
        printf("*p = %c, *(p+1) = %c, *(p+2) = %c\n", *p, *(p+1), *(p+2)); //打印指针p,指针p+1,指针p+2,指向的元素
        return 0;
    }
    

    运行结果

    *p = H, *(p+1) = e, *(p+2) = l
    
  • 对比标准的下标访问数组元素,这种使用指针进行间接访问的方法叫指针法

  • 需要郑重强调的是:p+1并不是简单地将地址加1,而是指向数组的下一个元素。

    int main()
    {
        int b[5] = {1,2,3,4,5}; 
        int *p = b;
        printf("*p = %d, *(p+1) = %d, *(p+2) = %d\n", *p, *(p+1), *(p+2));
        return 0;
    }
    

    运行结果:

    *p = 1, *(p+1) = 2, *(p+2) = 3
    
  • 事实上,通过指针法访问数组的元素也不一定要定义一个指向数组的指针,因为数组名本身也是指向数组第一个元素的地址,所以我们也可以直接将指针法作用与地址

    int main()
    {
        int b[5] = {1,2,3,4,5}; 
        int *p = b;
        printf("*b = %d, *(b+1) = %d, *(b+2) = %d\n", *b, *(b+1), *(b+2));
        return 0;
    }
    

    运行结果:

    *b = 1, *(b+1) = 2, *(b+2) = 3
    
  • 我们也可以直接用指针定义字符串,然后用下标法读取每一个元素

    #include<stdio.h>
    #include<string.h>
    int main()
    {
        char *str = "Hello World!"; //定义字符指针变量,并初始化为字符串
        int i, length;
        length = strlen(str); //获取字符串长度
        for (i = 0; i < length; i++)
        {
            printf("%c",str[i]);
        }
        printf("\n");
        return 0;
    }
    

    运行结果

    Hello World!
    

指针和数组指针

指针和数组的区别

  • 写一个程序,计算一个字符串里字符个数(不使用函数和sizeof运算符)

    int main()
    {
        char str[] = "Hello World!";
        int count = 0;
        while (*str++ != '\0') //判断首元素是否为空字符,并自加。
        {
            count++;
        }
        printf("总共有%d个字符!\n",count);
        return 0;
    }
    

    运行结果

    error: lvalue required as increment operand  //报错,提示表达式必须是可修改的左值
    

    参考什么是 lvalue,什么是 rvalue?,我们知道C 语言的术语 lvalue 指用于识别或定位一个存储位置的标识符。(注意:左值同时还必须是可改变的)

    左值是可以改变的,指针就是左值,而数组名其实是一个地址常量,是不可以改变的,所以它不是一个左值

    修改:

    int main()
    {
        char str[] = "Hello World!";
        char *target = str;
        int count = 0;
        while (*target++ != '\0') //判断首元素是否为空字符,并自加。
        {
            count++;
        }
        printf("总共有%d个字符!\n",count);
        return 0;
    }
    
  • 结论:数组名只是一个地址,而指针是一个左值

指针数组和数组指针

  • **指针数组: int *p1[5]; **
  • 数组指针: int (*p2)[5];

指针数组

  • **指针数组: int *p1[5]; **

  • 优先级(高到低): [] -> *

  • p1[5] 先定义为一个有5个元素的数组, *表示它们应该是一个指向整形变量的指针

    下标 0 1 2 3 4
    元素 int * int * int * int * int *

    每一个数组元素都是一个整形指针

  • 结论:指针数组是一个数组,每一个数组元素存放一个指针变量

指针数组的初始化

int main()
{
    char *p1[5] = {
        "让编程改变世界 -- 鱼C工作室",
        "Just do it -- NIKE",
        "一切皆有可能 -- 李宁",
        "永不止步 -- 安踏",
        "One more thing... -- 苹果"
    };
    int i;

    for (i = 0; i < 5; i++)
    {
        printf("%s\n", p1[i]);
    }
    return 0;
}

运行结果

让编程改变世界 -- 鱼C工作室
Just do it -- NIKE
一切皆有可能 -- 李宁
永不止步 -- 安踏
One more thing... -- 苹果

数组指针

  • 数组指针: int (*p2)[5];

  • 优先级: [] = ()

  • 结合性: 左到右

  • p2先被定义为一个指针变量,指向有5个元素的整形数组

    下标 0 1 2 3 4
    元素 int int int int int
  • 结论:数组指针是一个指针,它指向的是一个数组

数组指针的初始化

int main()
{
    int temp[5] = {1, 2, 3, 4, 5};
    int (*p2)[5] = &temp; //取出数值的地址赋值给数组指针
    int i;

    for (i = 0; i < 5; i++)
    {
        printf("%d\n", *(*p2 + i));
    }
    return 0;
}

指针和二维数组

二维数组概念性问题

假设我们定义了二维数组array[4][5]

  • array表示的是什么?

    array是二维数组的首地址。在一维数组中,数组名相当于数组第一个元素的地址。由于二维数组实际上是一维数组的拓展,所以这里array应该理解为指向包含5个元素的数组的指针

    int main()
    {
        int array[4][5] = {0};
        printf("sizeof int: %d\n",sizeof(int));
        printf("array: %p\n",array);
        printf("array + 1: %p\n",array+1);
        return 0;
    }
    

    运行结果

    sizeof int: 4
    array: 000000aa36dff790  
    array + 1: 000000aa36dff7a4   //16进制: 790 + 14 = 7a4
    //16进制:14 -> 10进制:20    
    //20 / 4 = 5
    
  • *(array+1)表示的是什么

    解引用:将取值运算符('''')作用于一个地址之上,即把一个地址的值取出来,我们称之为解引用*。

    我们可以得出结论,array+1同样是指向包含五个元素的数组的地址,这时它指向的是第二行的首地址,这时将其解引用*(array+1)得到的结果就相当于array[1]

    int main()
    {
        int array[4][5] = {0};
        int i, j, k = 0;
        for (i=0; i<4;i++)
        {
            for (j=0;j<5;j++)
            {
                array[i][j] = k++;
            }
        }
        printf("*(array+1): %p\n",*(array+1));
        printf("array[1]: %p\n",array[1]);
        printf("&array[1][0]: %p\n",&array[1][0]);
        printf("**(array+1): %d\n",**(array+1));
        return 0;
    }
    

    运行结果

    *(array+1): 000000e854bffd84
    array[1]: 000000e854bffd84
    &array[1][0]: 000000e854bffd84
    **(array+1): 5
    
  • *(*(array+1)+3)表示的是什么

    同样我们可以得出,*(array+1)+3是将指针指向第2行第4个元素,再对其解引用*(*(array+1)+3)得到的值就是二维数组中第2行第4列的值,相当于array[1][3]

    int main()
    {
        int array[4][5] = {0};
        int i, j, k = 0;
        for (i=0; i<4;i++)
        {
            for (j=0;j<5;j++)
            {
                array[i][j] = k++;
            }
        }
        printf("*(*(array+1)+3): %d\n",*(*(array+1)+3));
        printf("array[1][3]: %d\n",array[1][3]);
        return 0;
    }
    

    运行结果

    *(*(array+1)+3): 8
    array[1][3]: 8
    
  • 经过以上3问,我们可以得出结论

    • *(array+i) == array[i]
    • *(*(array+i)+j) == array[i][j]
    • *(*(*(array+i)+j)+k) == array[i][j][k]

数组指针和二维数组

  • 我们知道,初始化二维数组是可以偷懒的:

    int array[2][3] = {{0, 1, 2}, {3, 4, 5}};

    可以写成

    int array[][3] = {{0, 1, 2},{3, 4, 5}};

  • 定义一个数组指针是这样的:

    int (*p)[3];

  • 那么如何解释下边语句:

    int (*p)[3] = array;

    int main()
    {
        int array[2][3] = {{0, 1, 2}, {3, 4, 5}};
        int (*p)[3] = array;
        printf("**(p+1): %d\n", **(p+1));
        printf("**(array+1): %d\n", **(array+1));
        printf("array[1][0]: %d\n", array[1][0]);
        
        printf("*(*(p+1)+2): %d\n", *(*(p+1)+2));
        printf("*(*(array+1)+2): %d\n", *(*(array+1)+2));
        printf("array[1][2]: %d\n", array[1][2]);
        return 0;
    }
    

    运行结果:

    **(p+1): 3
    **(array+1): 3
    array[1][0]: 3
    *(*(p+1)+2): 5
    *(*(array+1)+2): 5
    array[1][2]: 5
    

void指针和NULL指针

void 类型

  • void的字面意思是“无类型”,定义变量的时候,我们通过类型来决定该变量所占的内存空间。比如在我们的系统中,char 类型占 1 个字节,int 类型占 4 个字节,double 类型占 8 个字节等。
  • void 既然是无类型,就不应该用它来定义一个变量,否则程序就会报错!

void指针

  • void指针我们把它称之为通用指针,就是可以指向任意类型的数据,也就是说,任何类型的指针都可以赋值给void指针。(如果反过来将void指针赋给其他类型指针,需对void指针进行强制类型转换)

    int main()
    {
        int num = 1024;
        int *pi = &num;
        char *ps = "Hello";
        void *pv;
        pv = pi;
        printf("pi:%p,pv:%p\n",pi,pv);
        pv = ps;
        printf("ps:%p,pv:%p\n",ps,pv);
        return 0;
    }
    

    运行结果:

    pi:0000001a8cdff694,pv:0000001a8cdff694
    ps:00007ff6a41c9000,pv:00007ff6a41c9000
    
  • 注意

    • 不要直接对 void 指针进行解引用,因为编译器不知道它所指向的数据类型。如果实在需要,需要使用强制类型转换

      pv = pi;
      printf("*pv:%d\n",*(int *)pv); //将void指针转换为int指针后取值输出
      pv = ps;
      printf("*pv:%s\n",(char *)pv);
      
    • 使用 void 指针一定要小心,由于 void 指针可以包罗万象的特性,间接使得不同类型的指针转换变为合法

      int main()
      {
          int num = 1024;
          int *pi = &num;
          char *ps = "Hello";
          void *pv;
          pv = pi;
          printf("pi:%p,pv:%p\n",pi,pv);
          printf("*pv:%d\n",*(int *)pv);
          ps = (char *)pv; //将前面定义为int类型的pv强制转换成char类型后变成空的再赋值给ps
          pv = ps;
          printf("ps:%p,pv:%p\n",ps,pv);
          printf("*pv:%s\n",(char *)pv);
          return 0;
      }
      

      运行结果:

      pi:000000834dfffd74,pv:000000834dfffd74
      *pv:1024
      ps:000000834dfffd74,pv:000000834dfffd74
      *pv:  //输出出问题
      

NULL指针

  • 在 C 语言中,如果一个指针不指向任何数据,我们就称之为空指针,用 NULL 表示。

  • NULL 其实是一个宏定义: #define NULL ((void *)0)

    因为在大部分系统中,地址 0 通常是一个不被使用的地址。所以,如果一个指针指向 NULL,那么就意味着该指针不指向任何东西。

  • 当你还不清楚要将指针初始化为什么地址时,请将它初始化 NULL;在对指针进行解引用时,先检查该指针是否为 NULL。这种策略可以为你今后编写大型程序节省大量的调试时间。

    int main()
    {
        int *p1; //定义一个指针变量p1,不赋值,默认值为随机
        //这类指针称为野指针,迷途指针...
        int *p2 = NULL; //初始化为NULL,指向空指针
        printf("%d\n", *p1);
        printf("%d\n", *p2);
        return 0;
    }
    

    运行结果

    -1480124848  //得到一个随机的值
    Segmentation fault //对NULL指针进行解引用,程序出现异常
    

NULL不是NUL

  • NULL用于指针和对象,表示控制,指向一个不被使用的地址;而'\0'表示字符串的结尾

指向指针的指针

int main()
{
    int num = 520;
    int *p = &num; 
    int **pp = &p;
    printf("num: %d\n", num);
    printf("*p: %d\n", *p);
    printf("**p: %d\n", **pp);
    printf("&p: %p, pp: %p\n", &p, pp); //打印p指针地址和pp指针存放的值
    printf("&num: %p, p: %p, *pp: %p\n", &num, p, *pp);//打印num的地址,p指针存放的值和对pp进行一次解引用得到的值
    return 0;
}

上边代码中,p 定义的是指向整型的指针,也就是说指针变量 p 里边存放的是整型变量 num 的地址。

所以,*p 就是对指针进行解引用,得到了 num 的值。

而 pp 就是指向指针的指针,pp 存放的是指针变量 p 的地址。

所以,对 pp 进行一层解引用(*pp)得到的是 p 的值(num 的地址),对 pp 进行两层解引用(**pp)相当于对 p 进行一层解引用(*p),得到整型变量 num 的值

运行结果

num: 520
*p: 520
**p: 520
&p: 0000005fa17ff9a8, pp: 0000005fa17ff9a8
&num: 0000005fa17ff9b4, p: 0000005fa17ff9b4, *pp: 0000009fcd3ffd74

指针数组和指向指针的指针

int main()
{
    char *cBooks[] = {
        "《C程序设计语言》",
        "《C专家编程》",
        "《C和指针》",
        "《C陷阱与缺陷》",
        "《C Primer Plus》",
        "《带你学C带你飞》"}; //定义一个指针数组存放C语言书籍(一级指针)
    char **byFishC;    //定义一个指针存放由鱼C工作室出品的书(二级指针)
    char **myLoves[4]; //定义一个指针数组存放4本喜欢的书(二级指针)
    int i;
    byFishC = &cBooks[5]; //byFishC定义为指向字符指针的指针变量
    myLoves[0] = &cBooks[0];  //定义为指向字符指针的指针数组
    myLoves[1] = &cBooks[1];  //注意:赋值号两边的值需要是一个级别的
    myLoves[2] = &cBooks[2];
    myLoves[3] = &cBooks[3];
    printf("FishC出版的图书由: %s\n", *byFishC);
    printf("我喜欢的图书有: \n", *myLoves);
    for (i=0;i<4;i++)
    {
        printf("%s\n",*myLoves[i]);
    }
    return 0;
}

运行结果

FishC出版的图书由: 《带你学C带你飞》
我喜欢的图书有:
《C程序设计语言》
《C专家编程》
《C和指针》
《C陷阱与缺陷》

使用指向指针的指针来指向数组指针,至少有两个优势:

  • 避免重复分配内存(虽然我们进行了多个分类,但每本书的书名只占据一个存储位置,没有浪费)
  • 只需要进行一处修改(如果其中一个书名需要修改,我们只需要修改一个地方即可)

这样,代码的灵活性和安全性都有了显著地提高。

数组指针和二维数组

分析以下代码

int main()
{
    int array[3][4] = {
        {0,1,2,3},
        {4,5,6,7},
        {8,9,10,11}}; //定义一个3行4列的二维数组
    int **p = array; //定义一个指向指针的指针来指向array
    int i, j;
    for (i=0;i<3;i++)
    {
        for (j=0;j<4;j++)
        {
            printf("%2d ", *(*(p+i)+j));
        }
        printf("\n");
    }
    return 0;
}

运行结果

error:Segmentation fault  //报错

原因:

​ 二级指针p只是一个指向指针的指针,它指向一个整形变量指针,跨度仍然为4。而不是二维数组中的一行( 4*sizeof(int) ),所以一行中有多少个元素,指针p是不知道的。二级指针的缺点就是不清楚数组的列数,指针跨度与数组不一致,跨度太细。

​ 这相当于我们在定义二维数组时可以忽略,但却不能忽略,编译器可以根据你有多少列计算出有多少行,而不能根据行计算出列。

如何利用指针来指向二维数组?

利用指针来指向二维数组,需要用到数组指针

int main()
{
    int array[3][4] = {
        {0,1,2,3},
        {4,5,6,7},
        {8,9,10,11}}; //定义一个3行4列的二维数组
    int (*p)[4] = array; 
    //①定义一个数组指针指向array,跨度为4*sizeof(int)
    //②将二维数组首地址(第一行)赋值给指针p
    int i, j;
    for (i=0;i<3;i++)
    {
        for (j=0;j<4;j++)
        {
            printf("%2d ", *(*(p+i)+j)); 
        }
        printf("\n");
    }
    return 0;
}

也可以用这种写法

int main()
{
    int array[3][4] = {
        {0,1,2,3},
        {4,5,6,7},
        {8,9,10,11}}; //定义一个3行4列的二维数组
    int (*p)[3][4] = &array; 
    //①定义一个二维数组指针p,p的行数和列数与数组array一一对应
    //②要用&取整个二维数组的地址赋值给p
    int i, j;
    for (i=0;i<3;i++)
    {
        for (j=0;j<4;j++)
        {
            printf("%2d ", *(*(*p+i)+j)); 
            //因为指针有二级,所以解引用要有三级
        }
        printf("\n");
    }
    return 0;
}

常量和指针

常量

  • 常量应该是这样的: 520,'a',3.14
  • 或者这样:
    • #define PRICE 520
    • #define A 'a'
    • #define PI 3.14
  • 还可以用const关键字修饰变量
    • const关键字可以让变量拥有像常量一样的特性—只读不可修改
    • const int price = 520;
    • const char a = 'a';
    • const float pi = 3.14;

指向常量的指针

指针也可以指向被const修饰过的变量,但不能通过指针来修改它所引用的值

int main()
{
    int num = 520;
    const int cnum = 880;
    const int *pc = &cnum;
    printf("cnum: %d, &cnum: %p\n", cnum, &cnum);
    printf("*pc: %d, pc: %p\n", *pc, pc);
    return 0;
}

程序可以正常运行,运行结果

cnum: 880, &cnum: 0000008acb3ffe0c
*pc: 880, pc: 0000008acb3ffe0c

尝试一下修改指针指向的值,也就是cnum的值

int num = 520;
const int cnum = 880;
const int *pc = &cnum;
printf("cnum: %d, &cnum: %p\n", cnum, &cnum);
printf("*pc: %d, pc: %p\n", *pc, pc);
*pc = 1024;  // 尝试修改 *pc 的值,报错
printf("*pc: %d\n", *pc);

运行结果

error:assignment of read-only location '*pc'

可以看到,修改指针指向的值,程序会报错。再尝试一下修改指针的指向

int num = 520;
const int cnum = 880;
const int *pc = &cnum;
printf("cnum: %d, &cnum: %p\n", cnum, &cnum);
printf("*pc: %d, pc: %p\n", *pc, pc);
pc = &num;  // 让指针指向num
printf("num: %d, &num: %p\n", num, &num);
printf("*pc: %d, pc: %p\n", *pc, pc);

运行结果

cnum: 880, &cnum: 000000c47c1ff9e0
*pc: 880, pc: 000000c47c1ff9e0
num: 520, &num: 000000c47c1ff9e4
*pc: 520, pc: 000000c47c1ff9e4

程序正常运行

再尝试通过修改变量num的值来修改指针pc指向的值

int num = 520;
const int cnum = 880;
const int *pc = &cnum;
pc = &num;  // 让指针指向num
printf("num: %d, &num: %p\n", num, &num);
printf("*pc: %d, pc: %p\n", *pc, pc);
num = 1024;
printf("*pc: %d, pc: %p\n", *pc, pc);

运行结果

num: 520, &num: 000000edb7bffca4
*pc: 520, pc: 000000edb7bffca4
*pc: 1024, pc: 000000edb7bffca4

总结

  • 指针可以修改为指向不同的常量
  • 指针可以修改为指向不同的变量
  • 可以通过解引用来读取指针指向的数据
  • 不可以通过解引用修改指针指向的数据

常量指针

  • 在上面的例子中,指向常量的指针,不能改变的是指针指向的值,但指针本身是可以被修改的。如果要让指针也不可变,那么可以使用常量指针

  • 使用的同样是const关键字,只是位置稍微发生了变化:

    const *是指针常量,* const是常量指针

指向非常量常量指针

int main()
{
    int num = 520; //变量
    const int cnum = 880;  //常量
    int * const p = &num; //定义一个常量指针指向变量num,注意const的位置
    //const在*左边,则是指向常量的指针
    //const在*右边,则是常量指针
    *p = 1024; //尝试修改p指向的值
    printf("*p: %d\n",*p);
    return 0;
}

运行结果

*p: 1024

程序正常运行,常量指针指向的值可以改变,再尝试修改常量指针p自身的值

int num = 520;
const int cnum = 880;
int * const p = &num; 
p = &cnum; //修改常量指针p的值
printf("*p: %d\n",*p);

运行结果

error:assignment of read-only variable 'p'

程序报错,常量指针p自身的值不能修改

指向常量常量指针

在刚才的基础上进一步限制,让常量指针指向的值也是常量

int main()
{
    int num = 520;
    const int cnum = 880;
    const int * const p = &cnum; 
    //常量指针p指向的是有const修饰的cnum
    //所以常量指针前要加const'
    //也可以写成int const * const p = &cnum;
    // *前是指向的对象的类型, *后是指针的类型
    *p = 1024;
    printf("*p: %d\n", *p);
    p = &cnum;
    printf("*p: %d\n", *p);
    return 0;
}

运行结果:

error:assignment of read-only location '*(const int *)p'
error:assignment of read-only variable 'p'

这样指针自身不能被改变,它所指向的数据也不能通过对指针进行解引用来修改。其实这种限制在平时很少派上用场。

并且我们发现:如果初始化时,常量指针指向的对象不是const修饰的变量,那么我们仍然可以通过变量名直接修改它的值:

int num = 520;
const int cnum = 880;
const int * const p = &num; //p指向常量num
num = 1024;
printf("*p: %d\n",*p);
return 0;

运行结果

*p: 1024

指向“指向常量常量指针”的指针

标题看起来似乎挺恐怖的,但其实你只要仔细思考,就不难发现:关于指针的那点事儿,永远都是换汤不换药的把戏。

int main()
{
    int num = 520;
    const int * const p = &num; //指向常量的常量指针
    const int * const *pp = &p; //指向“指向常量的常量指针”的指针
    //指针pp指向指针p, 指针p指向变量num
    printf("pp: %p, &P: %p\n", pp, &p); 
    //打印“指针pp的值”和“存放指针p的地址”
    printf("*pp: %p, p: %p, &num: %p\n",*pp, p, &num);
    //打印“指针pp一层解引用得到的值”,“指针p的值”和“存放变量num的地址”
    printf("**pp: %d, *p: %d, num: %d\n",**pp, *p, num);
    //打印“指针pp两层解引用得到的值”,“指针p一层解引用得到的值”和“变量num的值”
    return 0;
}

运行结果:

pp: 000000d1e39ffd80, &P: 000000d1e39ffd80
*pp: 000000d1e39ffd8c, p: 000000d1e39ffd8c, &num: 000000d1e39ffd8c
**pp: 520, *p: 520, num: 520

总结

  • 指向非常量常量指针有如下特性:
    • 指针自身不可以被修改
    • 指针指向的值可以被修改
  • 指向常量常量指针有如下特性:
    • 指针自身不可以被修改
    • 指针指向的值也不可以被修改
posted @ 2025-02-14 21:18  芝麻凛  阅读(39)  评论(0)    收藏  举报