带你学C带你飞 | 指针 | 指针和数组 | 指针数组与数组指针 | 指针和二维数组 | void指针与NULL指针 | 指向指针的指针 | 常量和指针

一、指针

1.指针

  指针对于C语言非常重要!可以说:没有指针,就没有现在简洁、高效的C语言。要想明白指针的原理,就需要了解数据在内存中是如何存储和读取的!由于内存的最小索引单元是1个字节,因此可以将内存想象成一个超级大的字符数组。数组有索引下标,内存有地址,每一个地址可以存储一个字节的数据。如果想要存储一个整型变量,就需要4个存储单元。通常我们说的指针就是地址的意思。C语言中有专门的指针变量来存放指针。与普通变量不同的是,指针变量存放的是地址,普通变量存放的是数据。指针变量也有类型,它的类型就是存放的地址指向的数据类型。
在这里插入图片描述
  如何定义指针变量?

类型名 *指针变量名
char *pa; //定义一个指向字符型的指针变量
int *pf;  //定义一个指向整型的指针变量

  取地址运算符和取值运算符介绍:

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

    char *pa = &a;
    int *pf = &f;
    
  • 如果需要访问指针变量指向的数据(间接访问),可以使用取值运算符(*):

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

举个栗子:

#include <stdio.h>

int main()
{
        char a = 'F';
        int f = 123;

        char *pa = &a;
        int *pf = &f;

        printf("a = %c\n",*pa);
        printf("f = %d\n",*pf);

        //利用指针重新赋值
        *pa = 'C';
        *pf += 1;

        printf("a = %c\n",*pa);
        printf("f = %d\n",*pf);
        
        //查询指针变量的空间
        printf("sizeof pa = %d\n",sizeof(pa));
        printf("sizeof pf = %d\n",sizeof(pf));

        return 0;
}
[liujie@localhost sle19]$ gcc test.c && ./a.out
a = F
f = 123
a = C
f = 124
sizeof pa = 8
sizeof pf = 8

虽然指针变量指向的数据不同,但是指针变量的空间大小不一样的,这是因为指针变量中存放的是地址。
  最后,需要注意:避免访问未初始化的指针。在对指针进行间接访问的时候,必须确保已经被正确的初始化了。如下图,往栈中随机地址赋值,容易覆盖关键代码。
在这里插入图片描述

2.指针的课后作业

  1. 通常程序猿口中的“指针”,指的是什么东西?
    答:内存的地址

  2. 指针变量只能存放地址吗?
    答:是的

  3. 请问 int * a, b, c; 定义了多少个指针变量?
    答:一个。只有 a 是指针变量,b 和 c 是普通整型变量。

  4. 请问 int *(a, b, c); 定义了多少个指针变量?
    答:语法错误。学 C 语言可不能偷懒,int *a, *b, *c; 才是定义三个指针变量。

  5. 你觉得将取址运算符(&)作用于一个常数,然后试图打印该常数在内存中的地址,这样做可取吗?

    include <stdio.h>
    
    int main()
    {
            printf("%p\n", &110);
    
            return 0;
    }
    

    答:这样做不可取!报错信息已经提示你了:

    test.c:5: error: lvalue required as unary ‘&’ operand
    

    意思是:取址操作符(&)的作用对象应该是一个左值,而常数是右值。

  6. 请问下边代码是否可以成功执行呢?为什么?

    #include <stdio.h>
    
    int main()
    {
            int a, b;
    
            b = 110;
            a = &b;
    
            printf("%d\n", *a);
    
            return 0;
    }
    

    答:咋的一看,这没毛病……事实上只要你仔细推敲,这问题可大了!虽然说在我们的操作系统里:sizeof(int) == sizeof(*int) 说明存放指针变量和存放整型变量所需的存储空间是一样的。但这并不说明他们就可以互相取代。
    这种做法编译器并不会认可,它会毫不犹豫给你直接报错:
    在这里插入图片描述

  7. 请问为什么每次执行下边代码都会得到不同的结果?

    #include <stdio.h>
    
    int main()
    {
            int *a;
    
            printf("%p\n", a);
    
            return 0;
    }
    

    答:这里我们声明了一个指针变量 a,但并未对它进行初始化,这是非常危险的行为!因为我们没办法预测这个指针变量的值会被初始化为什么,它只是恰好内存中存在的“随机值”。

  8. 请问下边代码执行后,打印机的结果是什么?另外,*b 是左值(l-value)还是右值(r-value)?

    #include <stdio.h>
    
    int main()
    {
            int a = 110;
            int *b = &a;
    
            *b = *b - 10;
    
            printf("a = %d\n", a);
    
            return 0;
    }
    

    答:打印结果是 a = 100
    第一个问题:定义指针变量 b 的时候,存放的是变量 a 的地址。在此之后,*b 即对变量 a 的间接访问(通过地址访问 a 变量)。所以 *b = *b - 10; 相当于 a = a - 10; 也就是说,通过指针对一个变量间接访问,你可以理解为把它作为那个变量本身使唤(即 *b == a)。
    第二个问题:指针变量 b 既是左值,也是右值。看 *b = *b - 10; 这个语句,赋值号右边,*b 间接访问变量 a 的值,因为用的是它的值,所以是右值;赋值号左边,*b 用于定位变量 a 的存储位置,然后将右边表达式的值存放进去,所以此时为左值。

  9. 输入3个整数并排列大小

    #include <stdio.h>
    
    int main(void)
    {
            int a, b, c, t;
            int *pa, *pb, *pc;
            
            printf("请输入三个数:");
            scanf("%d%d%d", &a, &b, &c);
            
            pa = &a;
            pb = &b;
            pc = &c;
            
            if (a > b)
            {
                    t = *pa;
                    *pa = *pb;
                    *pb = t;
            }
            
            if (a > c)
            {
                    t = *pa;
                    *pa = *pc;
                    *pc = t;
            }
            
            if (b > c)
            {
                    t = *pb;
                    *pb = *pc;
                    *pc = t;
            }
            
            printf("%d <= %d <= %d\n", *pa, *pb, *pc);
            printf("%d <= %d <= %d\n", a, b, c);
            
            return 0;
    }
    

二、指针和数组

1.指针和数组

  scanf函数中第二个参数通常要在变量前面加上取址操作符(&),但是如果存储的位置是指针变量,则不需要。
举个栗子:

#include <stdio.h>

int main()
{       
        int a;
        int *p = &a;
        
        printf("请输入一个整数:");
        scanf("%d",&a);
        printf("a = %d\n",a);

        printf("请重新输入一个整数:");
        scanf("%d",p);
        printf("a = %d\n",a);

        return 0;
}
[liujie@localhost sle21]$ gcc test.c && ./a.out
请输入一个整数:7
a = 7
请重新输入一个整数:6
a = 6

  虽然数组和指针关系密切,但数组绝不是指针。数组名其实是数组第一个元素的地址

#include <stdio.h>

int main()
{       
        char str[128];
        
        printf("请输入鱼C的域名:");
        scanf("%s",str);
        
        printf("str 的地址是:%p\n",str);
        printf("str 的地址是:%p\n",&str[0]);
        
        return 0;
} 
[liujie@localhost sle21]$ gcc test1.c && ./a.out
请输入鱼C的域名:FishC.com
str 的地址是:0x7fff9524dad0
str 的地址是:0x7fff9524dad0

  如果用一个指针指向数组,应该怎么做呢?
将指针指向数组的第一个地址(数组名)就可以了,接下来再进行指针的运算。

char a[8] = "FishC";
char *p;

//下面两句话等价
p = a; //语句1
p = &a[0]; //语句2

  指针的运算:当指针指向数组元素的时候,我们可以对指针变量进行加减运算,这样做的意义相当于指向距离指针所在位置向前或向后第n个元素。对比标准的下标法访问数组元素,这种使用指针进行间接访问的方法叫做指针法

举个栗子:

#include <stdio.h>

int main()
{
        char a[] = "FishC";
        int b[5] = {1,2,3,4,5};
        char *pa = a;
        int *pb = b;

        printf("*pa = %c,*(pa+1)=%c,*(pa+2)=%c\n",*pa,*(pa+1),*(pa+2));
        printf("*pb = %d,*(pb+1)=%d,*(pb+2)=%d\n",*pb,*(pb+1),*(pb+2));
        return 0;
}
[liujie@localhost sle21]$ gcc test2.c && ./a.out
*pa = F,*(pa+1)=i,*(pa+2)=s
*pb = 1,*(pb+1)=2,*(pb+2)=3

需要强调的是:p+1并不是简单地将地址加1,而是指向数组的下一个元素。
  甚至可以用指针定义字符串,用下标法读取每一个元素。
举个栗子:

#include <stdio.h>
#include <string.h>

int main()
{       
        char *str = "I love FishC.com!";
        int i,length;
        
        length = strlen(str);

        for (i=0;i<length;i++)
        {
                printf("%c",str[i]);
        }
        printf("\n");

        return 0;
} 
[liujie@localhost sle21]$ gcc test3.c && ./a.out
I love FishC.com!

2.课后作业

  1. str[3] 用指针法如何表示?
    答:*(str + 3)

  2. 假设整型指针变量 p 存放的地址值是 0x11008888,那么请问 p + 1,p + 2,p + 4 和 p + 8 的地址分别是?
    答:由于在我们的操作系统中 sizeof(int) == 4,0x 开头表示该地址是 16 进制表示。

    p + 1 == 0x11008888 + 4 == 0x1100888C
    p + 2 == 0x11008888 + 8 == 0x11008890
    p + 4 == 0x11008888 + 16 == 0x11008898
    p + 8 == 0x11008888 + 32 == 0x110088A8
    
  3. 请问 str[20] 是否可以写成 20[str]?
    答:可以。
    因为在访问数组的元素的时候,数组名被解释为数组第一个元素的地址。所以 str[20] == *(str + 20) == *(20 + str) == [20]str

  4. 你能猜出下边关键代码段是用于干啥的吗?

    ……
    while (n-- && (*target2++ = *target1++) != '\0')             
    	 ;
    ……
    

    答:实现 strncpy 函数的功能

  5. 接上一题,请问代码写成下方形式,能否正确实现要求?

    ……
    while ((*target2++ = *target1++) != '\0' && n--)
                    ;
    ……
    

    答:不能。因为这么做会超出要求 1 个字符(比如要求拷贝 5 个字符,实际拷贝了 6 个),因为代码的逻辑是 *target2++ = *target1++ 先赋值,再判断 n–。

  6. 获取字符串的长度 —— strlen 函数
    基础要求:使用 fgets 函数读取用户输入的字符串(英文),并用指针法来计算字符串的字符个数。

    #include <stdio.h>
    
    #define MAX 1024
    
    int main()
    {
            char str[MAX];
            char *target = str;
            int length = 0;
    
            printf("请输入一个字符串:");
            fgets(str, MAX, stdin);
    
            while (*target++ != '\0')
            {
                    length++;
            }
    
            printf("您总共输入了 %d 个字符!\n", length - 1);
    
            return 0;
    }
    

    你们可能对 target++ != ‘\0’ 这一行代码有疑问,这里我给大家解释下。首先在“运算符的优先级和结合性”(http://bbs.fishc.com/thread-67664-1-1.html)可以查到自增运算符(++)的优先级比取值运算符()要高,所以 *target++ 相当于 *(target++),先执行自增运算符,再取值。但由于这是一个后缀的自增运算符,所以自增的效果要在下一条语句才会生效,因此这里取出来的依然是 target 地址自增前指向的数组元素的值。

    进阶要求:你可能发现写出来的代码只能统计英文字符的个数,遇到中文字符结果就会出错。请自行观察你当前系统对中文字符的处理方式,并设计一个可以统计中文字符以及中英文混合字符的程序。

    #include <stdio.h>
    
    #define MAX 1024
    
    int main()
    {
            char str[MAX];
            char *target = str;
            char ch;
            int length = 0;
    
            printf("请输入一个字符串:");
            fgets(str, MAX, stdin);
    
            while (1)
            {
                    ch = *target++;
                    if (ch == '\0')
                    {
                            break;
                    }
                    //我们只要检测一个字符对应的整型值是否为负数,如果是(中文字符),则将指针往后移动两个字节。
                    if ((int)ch < 0)
                    {
                            target += 2;
                    }
                    length++;
            }
    
            printf("您总共输入了 %d 个字符!\n", length - 1);
    
            return 0;
    }
    
  7. 拷贝字符串 —— strcpy 和 strncpy 函数
    基础要求:使用 fgets 函数读取用户输入的字符串(英文)并存储到字符数组 str1 中,并利用指针将 str1 中的字符串拷贝到字符数组 str2 中。

    #include <stdio.h>
    
    #define MAX 1024
    
    int main()
    {
            char str1[MAX];
            char str2[MAX];
    
            char *target1 = str1;
            char *target2 = str2;
    
            printf("请输入一个字符串到 str1 中:");
            fgets(str1, MAX, stdin);
    
            printf("开始拷贝 str1 的内容到 str2 中...\n");
            while ((*target2++ = *target1++) != '\0')
                    ;
    
            printf("拷贝完毕!");
            printf("现在,str2 中的内容是:%s", str2);
    
            return 0;
    }
    

    进阶要求:实现 strncpy 函数,让用户输入需要拷贝的字符个数(注意:该程序需要能够正确拷贝中英混合的字符串)。

    #include <stdio.h>
    
    #define MAX 1024
    
    int main()
    {
            char str1[MAX];
            char str2[MAX];
    
            char *target1 = str1;
            char *target2 = str2;
    
            char ch;
            int n;
    
            printf("请输入一个字符串到 str1 中:");
            fgets(str1, MAX, stdin);
    
            printf("请输入需要拷贝的字符个数:");
            scanf("%d", &n);
    
            printf("开始拷贝 str1 的内容到 str2 中...\n");
            while (n--)
            {
                   ch = *target2++ = *target1++;
                   if (ch == '\0')
                   {
                           break;
                   }
                   if ((int)ch < 0)
                   {
                           *target2++ = *target1++;
                           *target2++ = *target1++;
                   }
            }
    
            *target2 = '\0';
    
            printf("拷贝完毕!\n");
            printf("现在,str2 中的内容是:%s\n", str2);
    
            return 0;
    }
    
  8. 连接字符串 —— strcat 和 strncat 函数
    基础要求:使用 fgets 函数接收用户输入的两个字符串到 str1 和 str2 中,将 str2 连接到 str1 后边,并打印出来。

    #include <stdio.h>
    
    #define MAX 1024
    
    int main()
    {
            char str1[2 * MAX]; // 确保连接后不越界
            char str2[MAX];
    
            char *target1 = str1;
            char *target2 = str2;
    
            printf("请输入第一个字符串:");
            fgets(str1, MAX, stdin);
    
            printf("请输入第二格字符串:");
            fgets(str2, MAX, stdin);
    
            // 将指针指向 str1 的末尾处
            while (*target1++ != '\0')
                    ;
    
            // 我们希望 str1 最后边的 '\0' 和 '\n' 都被覆盖掉
            target1 -= 2;
    
            // 连接字符串
            while ((*target1++ = *target2++) != '\0')
                    ;
    
            printf("连接后的结果是:%s", str1);
    
            return 0;
    }
    

    进阶要求:实现 strncat 函数,让用户输入需要连接到 str1 后边的字符个数(注意:该程序需要能够正确拷贝中英混合的字符串)。

    #include <stdio.h>
    
    #define MAX 1024
    
    int main()
    {
            char str1[2 * MAX]; // 确保连接后不越界
            char str2[MAX];
    
            char *target1 = str1;
            char *target2 = str2;
    
            char ch;
            int n;
    
            printf("请输入第一个字符串:");
            fgets(str1, MAX, stdin);
    
            printf("请输入第二格字符串:");
            fgets(str2, MAX, stdin);
    
            printf("请输入需要连接的字符个数:");
            scanf("%d", &n);
    
            // 将指针指向 str1 的末尾处
            while (*target1++ != '\0')
                    ;
            // 我们希望 str1 最后边的 '\0' 和 '\n' 都被覆盖掉
            target1 -= 2;
    
            while (n--)
            {
                    ch = *target1++ = *target2++;
                    if (ch == '\0')
                    {
                            break;
                    }
                    if ((int)ch < 0)
                    {
                            *target1++ = *target2++;
                            *target1++ = *target2++;
                    }
            }
    
            *target1 = '\0';
    
            printf("连接后的结果是:%s\n", str1);
    
            return 0;
    }
    
  9. 比较字符串 —— strcmp 和 strncmp 函数
    基础要求: 使用 fgets 函数接收用户输入的两个字符串(仅支持英文)到 str1 和 str2 中,对比 str1 和 str2,如果两个字符串完全一致,打印“完全一致”;如果存在不同,打印第一处不同的位置(索引下标)。

    #include <stdio.h>
    
    #define MAX 1024
    
    int main()
    {
            char str1[MAX];
            char str2[MAX];
    
            char *target1 = str1;
            char *target2 = str2;
    
            int index = 1;
    
            printf("请输入第一个字符串:");
            fgets(str1, MAX, stdin);
    
            printf("请输入第二个字符串:");
            fgets(str2, MAX, stdin);
    
            while (*target1 != '\0' && *target2 != '\0')
            {
                    if (*target1++ != *target2++)
                    {
                           break;
                    }
                    index++;
            }
    
            if (*target1 == '\0' && *target2 == '\0')
            {
                    printf("两个字符串完全一致!\n");
            }
            else
            {
                    printf("两个字符串不完全相同,第 %d 个字符出现不同!\n", index);
            }
    
            return 0;
    }
    

    进阶要求:实现 strncmp 函数,允许用户指定前 n 个字符进行对比,这一次要求支持中英文混合的字符串比较噢!

    #include <stdio.h>
    
    #define MAX 1024
    
    int main()
    {
            char str1[MAX];
            char str2[MAX];
    
            char *target1 = str1;
            char *target2 = str2;
    
            char ch;
            int index = 1, n;
    
            printf("请输入第一个字符串:");
            fgets(str1, MAX, stdin);
    
            printf("请输入第二个字符串:");
            fgets(str2, MAX, stdin);
    
            printf("请输入需要对比的字符个数:");
            scanf("%d", &n);
    
            while (n && *target1 != '\0' && *target2 != '\0')
            {
                    ch = *target1;
                    if (ch < 0)
                    {
                            if (*target1++ != *target2++ || *target1++ != *target2++)
                            {
                                    break;
                            }
                    }
                    if (*target1++ != *target2++)
                    {
                           break;
                    }
                    index++;
                    n--;
            }
    
            if ((n == 0) || (*target1 == '\0' && *target2 == '\0'))
            {
                    printf("两个字符串的前 %d 个字符完全相同!\n", index);
            }
            else
            {
                    printf("两个字符串不完全相同,第 %d 个字符出现不同!\n", index);
            }
    
            return 0;
    }
    

三、指针数组和数组指针

1.指针数组和数组指针

  指针与数组的区别:它们虽然同样指向地址,但是指针变量是一个左值,而数组名是一个地址常量不是一个左值。

C 语言的术语 lvalue 指用于识别或定位一个存储位置的标识符。(注意:左值同时还必须是可改变的

  下面介绍指针数组与数组指针。

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

    #include <stdio.h>
    
    int main()
    {       
            int a = 1;
            int b = 2;
            int c = 3;
            int d = 4;
            int e = 5;
            
            int *p1[5] = {&a,&b,&c,&d,&e};
            int i;
            
            for (i = 0;i < 5;i++)
            {       
                    printf("%d\n",*p1[i]);
            }       
            
            return 0;
    }  
    
    [liujie@localhost sle22]$ gcc test.c && ./a.out
    1
    2
    3
    4
    5
    

    在这里插入图片描述
    指针数组常用于指向字符指针
    举个栗子:

    #include <stdio.h>
    
    int main()
    {       
            char *p1[3] = {
                    "让编程改变世界---鱼C工作室",
                    "一切皆有可能---李宁",
                    "永不知步---安踏"
            };              
            
            int i;
            for (i = 0;i < 3;i++)
            {
                    printf("%s\n",p1[i]);
            }       
            
            return 0;
    } 
    
    [liujie@localhost sle22]$ gcc test1.c && ./a.out
    让编程改变世界---鱼C工作室
    一切皆有可能---李宁
    永不知步---安踏
    
  • 数组指针
    数组指针其实是一个指向数组的指针。
    在这里插入图片描述
    举个栗子:

    #include <stdio.h>
    
    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;
    } 
    
    [liujie@localhost sle22]$ gcc test2.c && ./a.out
    1
    2
    3
    4
    5
    

2.课后作业

  1. 请问 str[3] 和 *(str + 3) 是否完全等价?
    答:完全等价。
    解析:在 C 语言中,数组名是被作为指针来处理的。更确切的说,数组名就是指向数组第一个元素的指针,而数组索引就是距离第一个元素的偏移量。这也解释了为什么在 C 语言中数组下标是从 0 开始计数的,因为这样它的索引可以对应到偏移量上。因此,str[3] 和 3[str] 是相同的,因为它们在编译器的内部都会被解释为 *(str + 3)。

  2. 请问下边代码是否可以正常执行?如果可以,会打印什么值?如果不行,请说明原因?

    #include <stdio.h>
    
    int main()
    {
            int a[5] = {1, 2, 3, 4, 5};
            int *b;
    
            b = &a[3];
            printf("%d\n", b[-2]);
    
            return 0;
    }
    

    答:可以正常执行。
    解析:由于数组的下标法跟指针法访问是等价的,所以 b[-2] 相当于 *(b - 2)

  3. 为什么不能使用 if (str1 == str2) 这样的形式来比较两个字符串?
    答:因为这样比较的是指向两个字符串的指针,而不是字符串本身。

  4. 通常我们交换两个变量的值需要使用到一个临时变量,代码如下:

    ……
    temp = a;
    a = b;
    b = temp;
    ……
    

    小明童鞋说其实大可不必使用临时变量,他这么写:

    ……
    a += b;
    b = a - b;
    a -= b;
    ……
    

    请问小明的办法可行吗?
    答:在大部分情况下,小明的方案是奏效的。不过有一种情况需要担心,就是在颠倒同一个变量时,这个代码是无法正常运行的。
    比如:

    ……
    #define SWAP(a, b) (a += b, b = a - b, a -= b)
    ……
    int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    int i, j;
    ……
    SWAP(array[i], array[j]);  // 当 i == j 时,触发 Bug
    ……
    
  5. 如果数组 array 的定义如下:

    int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    

    请问 array 和 &array 有区别吗?
    答:有。
    解析:虽然 array 和 &array 的值相同,但含义是不一样的。array 表示数组第一个元素的位置,而 &array 表示的是整个数组的位置(这里将数组看做一个整体)。

  6. 如果不上机,你能看出下边代码将打印什么值吗?

    #include <stdio.h>
    
    int main()
    {
            int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
            int *p = (int *)(&array + 1);
    
            printf("%d\n", *(p - 6));
    
            return 0;
    }
    

    答:4
    解析:首先,你要明白虽然 array 和 &array 的值相同,但含义是不一样的。array 表示数组第一个元素的位置,而 &array 表示的是整个数组的位置(这里将数组看做一个整体)。因此,&array + 1 指向的就是整个数组最后的位置(第二个 array 数组的起始位置),然后 (int *) 将其强制转换为一个整型地址(指针),所以指针变量 p 初始化后,指向的地址应该是 array[10](第 11 个元素),所以 *(p - 6) == array[10 - 6] == array[4] == 4。
    重点强调:array 是数组第一个元素的地址,所以 array + 1 指向数组第二个元素;&array 是整个数组的地址,所以 &array + 1 指向整个数组最后的位置。

  7. 如果不上机,你能看出下边代码将打印什么值吗?

    #include <stdio.h>
    
    int main()
    {
            int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
            int (*p)[10] = &array;
    
            printf("%d\n", *(*(p+1)-6));
    
            return 0;
    }
    

    答:4
    解析:指针 p 指向数组 array 的地址,p + 1 便是指向整个数组最后的位置(第二个 array 数组的起始位置),于是 *(p+1) 即下一个数组 array 数组的起始位置,即 &array[11],所以 ((p+1)-6) == array[11 - 6] = array[5]。

  8. 你如何理解“指针的类型决定了指针的视野,指针的视野决定了指针的步长”这句话?
    答:你必须清楚一个指针变量将告诉我们两个信息:某个数据结构的起始地址,以及该结构的跨度。比如 int p = &a; 说明该指针指向变量 a 的起始地址,以及它的跨度为 sizeof(int)。所以 p + 1 == p + sizeof(int)。

    int (*p)[10] 虽然是定义一个整型指针,但不要忘了它后边还有一个数组,所以它的跨度应该是 sizeof(int) * 10,而 array 作为数组名,它的含义是“指向数组第一个元素的地址”,所以 array 的跨度是 sizeof(array[0]),因此编译系统遇到 int (*p)[10] = array; 这样的定义就会果断报错:“左右类型不一致”。
    那 &array 怎么可以?
    因为 &array 是整个数组的地址,所以它的跨度自然而然就是整个数组啦。

  9. 写一个叫 sum 的程序,计算后边紧跟着的所有整型参数的和。

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(int argc, char *argv[])
    {
            int result = 0;
    
            while (argc-- != 1)
            {
                    result += atoi(argv[argc]);
            }
    
            printf("sum = %d\n", result);
    
            return 0;
    }
    
  10. 填充空白部分的代码:
    在这里插入图片描述
    要求实现结果如下:
    在这里插入图片描述

    #include <stdio.h>
    
    int main()
    {
            char *array[5] = {"FishC", "Five", "Star", "Good", "WoW"};
            char *(*p)[5] = &array;
            int i, j;
    
            for (i = 0; i < 5; i++)
            {
                    for (j = 0; (*p)[i][j] != '\0'; j++)
                    {
                            printf("%c ", (*p)[i][j]);
                    }
                    printf("\n");
            }
    
            return 0;
    }
    

    当然,根据下标法和指针法可以互换的原理,代码也可以这么写:

    #include <stdio.h>
    
    int main()
    {
            char *array[5] = {"FishC", "Five", "Star", "Good", "WoW"};
            char *(*p)[5] = &array;
            int i, j;
    
            for (i = 0; i < 5; i++)
            {
                    for (j = 0; *(*(*p + i) + j) != '\0'; j++)
                    {
                            printf("%c ", *(*(*p + i) + j));
                    }
                    printf("\n");
            }
    
            return 0;
    }
    
  11. 修改上一题的代码,要求按下边格式输出:
    在这里插入图片描述

    #include <stdio.h>
    #include <string.h>
    
    int main()
    {
            char *array[5] = {"FishC", "Five", "Star", "Good", "Wow"};
            char *(*p)[5] = &array;
            int i, j;
    
            for (i = 0; i < 5; i++)
            {
                    for (j = 0; j < 5; j++)
                    {
                            if (i > strlen((*p)[j]) - 1)
                            {
                                    break;
                            }
                            printf("%c ", (*p)[j][i]);
                    }
                    printf("\n");
            }
    
            return 0;
    }
    

四、指针和二维数组

1.指针和二维数组

  C语言中,二维数组在内存中是通过线性的方式来存储的。在一维数组中,我们知道数组名代表数组中第一个元素的地址。由于二维数组是一维数组的拓展,所以,在二维数组中,数组名(array)代表指向包含多个元素的数组的指针。
举个栗子:

#include <stdio.h>

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;
} 
[liujie@localhost sle24]$ gcc test.c && ./a.out
sizeof int:4
array:0x7fffdc09dc70
array + 1:0x7fffdc09dc84

  那么,*(array+1)表示的什么呢(array+1地址的解引用)?表示的是指向第二行子数组的第一个元素的地址。
在这里插入图片描述

//语法糖
*(array+1) == array[1];

举个栗子:

#include <stdio.h>

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):0x7ffea7298bf4
array[1]:0x7ffea7298bf4
&array[1][0]:0x7ffea7298bf4
**(array+1):5

  那么,*(*(array+1)+3)表示的是什么?

*(array+1)+3 == &array[1][3]
*(*(array+1)+3) == array[1][3]

举个栗子:

#include <stdio.h>

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;
}
[liujie@localhost sle24]$ gcc test2.c && ./a.out
*(*(array+1)+3):8
array[1][3]:8

结论:

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

  数组指针和二维数组之间的关系:

  • 初始化二维数组是可以偷懒的

    int array[][3] = {{0,1,2},{3,4,5}};
    
  • 定义一个数组指针是这样的

    int (*p)[3];
    
  • 那么如何解释下边的语句:

    int (*p)[3] = array;
    

举个栗子:

#include <stdio.h>


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]);

        return 0;
}
[liujie@localhost sle24]$ gcc test3.c && ./a.out
**(p+1):3
**(array+1):3
array[1][0]:3

2.课后作业

  1. 如果不上机,你能看出下边代码将打印什么值吗?

    #include <stdio.h>
    
    int main()
    {
            char matrix[3][5] = {
                    'I', 'l', 'o', 'v', 'e',
                    'F', 'i', 's', 'h', 'C',
                    '.', 'c', 'o', 'm', '!'
            };
            char *p;
    
            p = &matrix[0][3];
    
            printf("%c", *p);
            printf("%c", *p++);
            printf("%c", *++p);
            printf("\n");
    
            return 0;
    }
    

    答:vvF
    解析:

    • 字符指针 p 指向二维数组 matrix 第一行第四个元素(‘v’)
    • 由于 p 是一个字符指针(其跨度为一个字符),所以 p++ 指向 matrix 第一行第五个元素(‘e’),但加加运算符(++)作为后缀使用时,是先使用原来的值再进行加一运算,因此 *p++ 仍然打印 ‘v’,随后指针指向 ‘e’
    • 加加运算符(++)作为前缀使用时,则先让指针(p)指向下一个元素,再间接取出里边的值。由于二维数组实质上是一维数组的线性扩展,所以 ++p 指向的是第二行第一个元素(‘F’)
  2. 假如定义了二维数组 int matrix[4][5] = {0};,请问 matrix 和 matrix + 0 的含义一样吗?
    答:一样啊

  3. 假设有二维数组如下,请问 *(matrix + 1) + 2 的含义是?

    char matrix[3][5] = {
            'I', 'l', 'o', 'v', 'e',
            'F', 'i', 's', 'h', 'C',
            '.', 'c', 'o', 'm', '!'
    };
    

    答:*(matrix + 1) + 2 的含义是一个指向字符变量的指针,其值是二维数组 matrix 第二行第三个元素的地址(即 &matrix[1][2])。

  4. 请问 array[x][y][z] 用指针法如何表示?
    答:((*(array+i)+j)+k)

  5. 请问下边代码将打印什么值?

    #include <stdio.h>
    
    int main()
    {
            char array[2][3][5] = {
                    {
                            {'x', 'x', 'x', 'x', 'x'},
                            {'x', 'x', 'o', 'x', 'x'},
                            {'x', 'x', 'x', 'x', 'x'}
                    },
                    {
                            {'x', 'x', 'x', 'x', 'x'},
                            {'x', 'x', 'o', 'x', 'x'},
                            {'x', 'x', 'x', 'x', 'x'}
                    }
            };
    
            printf("%c%c%c%c\n", *(*(*array + 1) + 2), *(*(*(array + 1) + 1) + 2), ***array, *(**array + 1));
    
            return 0;
    }
    

    答:ooxx

  6. 如果不上机,你能看出下边代码将打印什么值吗?

    #include <stdio.h>
    
    int main()
    {
            int array[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
            int (*p)[3] = (int (*)[3])&array;
    
            printf("%d\n", p[2][2]);
    
            return 0;
    }
    

    答:9
    分析:对于初学者来说,这道题的难度级别很高,但当你尝试去理解它其中的原理之后,你会发现对指针的认识又深入了一点儿呢!
    等号右边强制将 array 这个一维数组重新划分成 3 * 3 的二维数组,然后用数组指针指向它(如果要使用指针来指向二维数组,只能使用数组指针)。
    So,p 指向的应该是这么一个二维数组:

    1 2 3
    4 5 6
    7 8 9
    

    所以,p[2][2] 的结果是 9。

  7. 编写一个程序,接收用户的输入,并将前 9 个字符以正方形矩阵(3 * 3)的方式输出。

    #include <stdio.h>
    
    int main()
    {
            int matrix[3][3] = {0};
            int i, j;
    
            for (i = 0; i < 3; i++)
            {
                    for (j = 0; j < 3; j++)
                    {
                            matrix[i][j] = getchar();
                    }
            }
    
            for (i = 0; i < 3; i++)
            {
                    for (j = 0; j < 3; j++)
                    {
                            printf("%c ", matrix[i][j]);
                    }
                    printf("\n");
            }
    
    
            return 0;
    }
    
  8. 这次不限制正方形矩阵的尺寸,要求程序自动计算用户输入的字符,并以最大的正方形矩阵输出(比如用户输入 17 个字符,输出 4 * 4 矩阵)。

    #include <stdio.h>
    #include <string.h>
    #include <math.h>
    
    #define MAX 1024
    
    int main()
    {
            int length, aver;
            int i, j;
            char str[MAX];
    
            scanf("%s", str);
    
            length = strlen(str);
            aver = sqrt(length);
    
            for (i = 0; i < aver; i++)
            {
                    for (j = 0; j < aver; j++)
                    {
                            printf("%c ", str[i * aver + j]);
                    }
                    printf("\n");
            }
    
            return 0;
    }
    
  9. 下表为广州市最近两年(2014年8月份 ~ 2016年8月份)的 PM2.5 检测数据表,请按要求编程。
    要求A:编写一个程序,用户输入待查询年月份(输入格式:2015-03),输出该月份的 PM2.5 值。

    #include <stdio.h>
    
    int main()
    {
            float pm25[3][12] = {
                    {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 31.3, 35.5, 58.7, 49.6, 55.5},
                    {59.8, 54.9, 33.1, 38.2, 26.6, 20.5, 27.8, 38.5, 41.5, 44.7, 38.1, 41.5},
                    {34.9, 36.4, 47.5, 37.9, 30.6, 23.4, 26.6, 34.3, 0.0, 0.0, 0.0, 0.0}
            };
            int year, month;
    
            printf("请输入待查询年月分(年-月): ");
            scanf("%d-%d", &year, &month);
    
            if (year < 2014 || year > 2016 || month < 1 || month > 12)
            {
                    printf("输入数据错误!\n");
            }
            else
            {
                    year -= 2014;
                    month -= 1;
                    if (pm25[year][month])
                    {
                            printf("%d年%d月广州的PM2.5值是: %.2f\n", year + 2014, month + 1, pm25[year][month]);
                    }
                    else
                    {
                            printf("抱歉,该月份未收录数据!\n");
                    }
            }
    
            return 0;
    }
    

五、void指针和NULL指针

1.void指针

  定义变量时,我们通过定义变量类型来定义变量所占的内存大小。void是无类型,定义变量会报错。void指针我们把它称之为通用指针,就是可以指向任意类型的数据。也就是说,任何类型的指针都可以赋值给void指针。
举个栗子:

#include <stdio.h>

int main ()
{
        int num = 1024;
        int *pi = &num;
        char *ps = "FishC";
        void *pv;

        printf("将整形指针赋值给void指针\n");
        pv = pi;
        printf("pi:%p,pv:%p\n",pi,pv);

        printf("将字符指针赋值给void指针\n");
        pv = ps;
        printf("ps:%p,pv:%p\n",ps,pv);

        return 0;
}
[liujie@localhost sle25]$ gcc test.c && ./a.out
将整形指针赋值给void指针
pi:0x7ffe14804024,pv:0x7ffe14804024
将字符指针赋值给void指针
ps:0x400690,pv:0x400690

  上面表明:其他类型指针可以转为void指针。将void指针转为其他类型指针就需要加上强制类型转换。另外,不要直接对void指针进行解引用,因为编译器搞不懂指针指向的数据的数据类型。
举个栗子:

#include <stdio.h>

int main ()
{
        int num = 1024;
        int *pi = &num;
        char *ps = "FishC";
        void *pv;

        printf("将整形指针赋值给void指针\n");
        pv = pi;
        printf("pi:%p,pv:%p\n",pi,pv);

        printf("将void指针强制转换为整形指针再解引用\n");
        printf("*pv:%d\n",*(int *)pv);

        return 0;
}
[liujie@localhost sle25]$ gcc test.c && ./a.out
将整形指针赋值给void指针
pi:0x7ffcf1146bb4,pv:0x7ffcf1146bb4void指针强制转换为整形指针再解引用
*pv:1024

2.NULL指针(空指针)

  大部分操作系统中,地址0是一个不被使用的地址,所以,如果一个指针指向NULL,则表明该指针不指向任何东西。NULL 的宏定义如下:

#define NULL ((void *)0)

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

3.课后作业

  1. 你猜下边代码会打印多少?

    #include <stdio.h>
    
    int main()
    {
            void a;
    
            printf("%d\n", sizeof(a));
    
            return 0;
    }
    

    答:会报错!那 void 既然是无类型,我们就不应该用它来定义一个变量,如果你一定要这么做,那么程序就会给你报错

  2. sizeof(void ) 呢?
    答:如果你回答是 4 个字节或 8 个字节,那么本题不能算你答对。因为指针的尺寸是与编译器的目标平台相关的。比如目标平台是 32 位的,那么 sizeof(void
    ) 就是 4,如果是 64 位的,那么 sizeof(void *) 就是 8,如果是 16 位的,那么就是 2 啦。

  3. 如何有效地避免出现悬空指针?
    注:悬空指针就是指向了不确定的内存区域的指针,通常对这种指针进行操作会使程序发生不可预知的错误。
    答:当你的指针不知道指向哪儿的时候,那么将它指向 NULL,以后就不会有太多的麻烦。比如定义一个指针变量的时候,你可以把它初始化为 NULL,这样至少可以确保它不是一个垂悬指针。

  4. 对 NULL 指针进行解引用,结果是什么?
    答:报错。无论什么操作系统,对空指针进行解引用都是非法的。

  5. 请问下边定义有没有问题?

    ……
    int *p = void *0;
    ……
    

    答:报错。
    这里是将 0 强制转换成 void 指针,所以要这么写才能通过编译:

    int *p = (void *)0;
    
  6. 请问下边代码会打印什么?

    #include <stdio.h>
    
    int main()
    {
            int array[5] = {1, 2, 3, 4, 5};
            int *pi = &array[2];
            void *pv;
    
            pv = pi;
            pv++;
            pi = pv;
    
            printf("%d\n", *pi);
    
            return 0;
    }
    

    由于 pv 是 void 类型指针,所以编译器并不知道其“跨度”是多少,因此 pv++ 只是简单的将地址加 1。

六、指向指针的指针

1.指向指针的指针

  指向指针的指针就是一个指针,其指向的是指针。如下:**pp
举个栗子:

#include <stdio.h>

int main()
{       
        int num = 520;
        int *p = &num;
        int **pp = &p;
        
        printf("num:%d,*p:%d,**p:%d\n",num,*p,**pp);
        printf("&p:%p,pp:%p\n",&p,pp);
        printf("&num:%p,p:%p,*p:%p\n",&num,p,*pp);
        
        return 0;
} 
[liujie@localhost sle26]$ gcc test.c && ./a.out
num:520,*p:520,**p:520
&p:0x7ffda8f2a618,pp:0x7ffda8f2a618
&num:0x7ffda8f2a624,p:0x7ffda8f2a624,*p:0x7ffda8f2a624

解析:

num -> 520
p -> &num
*p -> num -> 520
pp -> &p
*pp -> p -> &num
**pp -> *p -> num -> 520

  指针数组与指向指针的指针:可以用指向指针的指针指向指针数组。

举个栗子:

#include <stdio.h>

int main()
{       
        char *cBooks[] = {
                "(C程序设计语言)",
                "(C专家编程)",
                "(C和指针)",
                "(C陷阱与缺陷)",
                "(C Primer PLus)",
                "(带你学C带你飞)"
                };      
        //用指向指针的指针指向指针数组
        char **byFishC;
        char **jiayuLoves[4];
        
        byFishC = &cBooks[5];
        jiayuLOves[0] = &cBooks[0];
        jiayuLOves[1] = &cBooks[1];
        jiayuLOves[2] = &cBooks[2];
        jiayuLOves[3] = &cBooks[3];
        
        printf("FishC出版的图书有:%s\n",*byFishC);
        printf("小甲鱼喜欢的图书有:\n");


        for (i=0;i<4;i++)
        {
                printf("%s\n",*jiayuLoves[i]);
        }


        return 0;
}

[liujie@localhost sle26]$ gcc test2.c && ./a.out
FishC出版的图书有:(带你学C带你飞)
小甲鱼喜欢的图书有:
(C程序设计语言)
(C专家编程)
(C和指针)
(C陷阱与缺陷)

  用指向指针的指针指向指针数组有如下两个优势:

  • 避免重复分配内存
  • 只需要进行一处修改

使得代码的灵活性和安全性有了显著的提高!
  数组指针和二维数组:可以用数组指针来访问二维数组。
举个栗子:

#include <stdio.h>

int main()
{       
        int array[3][4] = {
                {0,1,2,3},
                {4,5,6,7},
                {8,9,10,11}};
        
        //数组指针--利用数组指针指向二维数组
        //利用指向指针的指针指向二维数组会造成尺寸不一致的情况
        int (*p)[4] = 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;
}
[liujie@localhost sle26]$ gcc test3.c && ./a.out
 0  1  2  3 
 4  5  6  7 
 8  9 10 11 

指针p指向的是一维数组,指针p指向的数组的跨度为4 * sizeof(int),将二维数组的第一个地址给了指针p,此时,p+1代表的是二维数组第二行的第一个地址。

2.课后作业

  1. 你有听说过二级指针和三级指针吗?它们是什么?
    答:二级指针其实就是指向指针的指针,而三级指针当然就是指向指针的指针的指针啦。

  2. 请按下图写出每个变量的定义过程。
    在这里插入图片描述

    int num = 520;
    int *p1 = &num;
    int *p2 = &num;
    int **pp1 = &p1;
    int ***p3 = &pp1;
    
  3. 请问什么时候 *&p 和 p 等价,什么时候 &p 和 p 等价?
    & 运算符的操作数必须是左值,因为只有左值才表示一个内存单元,才会有地址,运算结果是指针类型(地址);
    运算符的操作数必须是指针类型(地址),运算结果可以做左值。
    结论:如果表达式 p 可以做左值,那么 *&p 和 p 等价;如果表达式 p 是指针类型(地址),那么 &*p 和 p 等价

  4. 如果有 int a[10];,请问 &a 和 &a[0] 表达式的类型有何不同?
    答:a 是一个数组,在 &a 这个表达式中,数组类型做左值,&a 表示取整个数组的首地址,类型是 int (*)[10];&a[0] 则表示数组 a 的第一个元素的首地址,虽然两个地址的数值相同,但后者的类型是 int *

  5. 假如定义了一个指针数组 pArray 如下,请定义一个指向它的数组指针 p,让下边程序可以顺利执行。
    在这里插入图片描述
    程序实现如下:
    在这里插入图片描述
    答:

    char *(*p)[4] = &pArray;
    
  6. 定义以下变量:

    char a[4][3][2] = {
            {
                    {'a', 'b'}, {'c', 'd'}, {'e', 'f'}
            },
            {
                    {'g', 'h'}, {'i', 'j'}, {'k', 'l'}
            },
            {
                    {'m', 'n'}, {'o', 'p'}, {'q', 'r'}
            },
            {
                    {'s', 't'}, {'u', 'v'}, {'w', 'x'}
            }
    };
    char (*pa)[2] = &a[1][0];
    char (*ppa)[3][2] = &a[1];
    

    想要通过指针 pa 和 ppa 访问数组 a 中的 ‘x’ 元素,请问表达式应该怎么写?
    答:

    *(*(pa+8)+1)*(*(*(ppa+2)+2)+1))
    

    解析:
    在这里插入图片描述
    如上图所示,*pa 被定义为一个一维数组,其跨度为 2 个字节,所以 *(pa + 8) 指向的是 {‘w’, ‘x’} 这个数组,所以 ((pa + 8) + 1) 取出 ‘x’ 元素;*ppa 被定义为一个二维数组,其跨度为 6 个字节,所以 (ppa + 2) 指向的是 {{‘s’, ‘t’}, {‘u’, ‘v’}, {‘w’, ‘x’}} 这个二维数组,所以 (((ppa + 2) + 2) + 1) 取出 ‘x’ 元素。

  7. 分割字符串。用户输入一个英文句子,你的程序将这个字符串按空格进行分割,返回由单词组成的二维数组。
    要求:

    • 返回的二维数组必须尽可能地节省空间(利用C语言的变长数组来实现)
    • 不能使用现成的函数帮忙(你只能 #include <stdio.h>)
    #include <stdio.h>
    
    int main()
    {
            char str[1024];
            char *p = str;          // 用于间接寻址
            char *pos[1024] = {0};  // 记录每个单词的地址
            int len = 0;
            int cChar = 0, cWord = 0; // cChar 统计字符数, cWord 统计单词数
            int max = 0, i = 0, j;
    
            printf("请输入一个英文句子:");
            // 接收输入,顺带统计用户实际输入了多少个字符
            while ((str[len++] = getchar()) != '\n' && len + 1 < 1024)
                    ;
            str[len-1] = '\0'; // str[len]存放的是'\n',将其替换为'\0'
    
            if (*p != ' ')
            {
                    pos[i++] = p; // 记录第一个单词的地址
                    cWord++;
            }
    
            while (len--)
            {
                    if (*p++ == ' ')
                    {
                            // 判断最大字符数
                            max = cChar > max ? cChar : max;
                            cChar = 0;
    
                            // 到底了,退出循环
                            if (*p == '\0')
                            {
                                    break;
                            }
    
                            // 单词数加一
                            if (*p != ' ')
                            {
                                    pos[i++] = p;
                                    cWord++;
                            }
                    }
                    else // 没有else会把空格统计进去
                    {
                            cChar++;
                    }
            }
            max = --cChar > max ? cChar : max; // 最后会算多一个'\0',所以减去
    
            // 申请可变长数组,max+1,否则'\0'放不下
            char result[cWord][max+1];
    
            // 将分割好的单词放进二维数组里
            for (i = 0; i < cWord; i++)
            {
                    for (j = 0; *(pos[i]+j) != ' ' && *(pos[i]+j) != '\0'; j++)
                    {
                            result[i][j] = *(pos[i]+j);
                    }
                    result[i][j] = '\0';
            }
    
            // 打印结果
            printf("分割结果已存放到result[%d][%d]的二维数组中...\n", cWord, max+1);
            printf("现在依次打印每个单词:\n");
            for (i = 0; i < cWord; i++)
            {
                    printf("%s\n", result[i]);
            }
    
            return 0;
    }
    

七、常量和指针

1.常量和指针

  在C语言中,使用const关键字修饰,可以使变量具有常量一样的特性——只读,不可修改。如果视图修改,程序就会报错。

const int price = 520;
const char a = 'a';
const float pi = 3.14;

  指向常量的指针:指针可以指向被const修饰过的变量,这就意味着:不能通过指针来修改所引用的值。但是,可以通过修改引用来修改值。
举个栗子:

#include <stdio.h>

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);
        
        //直接修改引用
        pc = &num;
        printf("num:%d,&num:%p\n",num,&num);
        printf("*pc:%d,pc:%p\n",*pc,pc);

        return 0;
}
[liujie@localhost sle27]$ gcc test.c && ./a.out
cnum:880,&cnum:0x7ffd550e52b0
*pc:880,pc:0x7ffd550e52b0
num:520,&num:0x7ffd550e52b4
*pc:520,pc:0x7ffd550e52b4

结论:

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

  常量指针:指向常量的指针不能改变的是指向指针的值,但指针本身可以被修改,如果想要指针本身也不可变,就需要用到常量指针(同样适用const关键字,只是位置发生变化)。常量指针又分为指向非常量的常量指针和指向常量的常量指针。
总结:

  • 指向非常量的常量指针
    • 指针自身不可以被修改
    • 指针指向的值可以被修改
  • 指向常量的常量指针
    • 指针自身不可以被修改
    • 指针指向的值也不可以被修改

举个栗子:指向非常量的常量指针

#include <stdio.h>

int main()
{       
        int num = 520;
        const int cnum = 880;
        //常量指针
        int * const p = &num;
        
        //常量指针指向的值可以改变
        *p = 1024;
        printf("*p:%d\n",*p);
        
        //常量指针本身不可改变
        //*p = &cnum;
        
        return 0;
} 
[liujie@localhost sle27]$ gcc test1.c && ./a.out
*p:1024

指向常量的常量指针:

#include <stdio.h>

int main()
{
        int num = 520;
        const int cnum = 880;
        //如果是&num,则const起不到限制的作用
        const int * const p = &cnum;

        return 0;
}

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

#include <stdio.h>

int main()
{
        int num = 520;
        const int cnum = 880;
        const int * const p = &cnum;
        //指向“指向常量的常量指针”的指针
        const int * const *pp = &p;

        return 0;
}

2.课后作业

  1. const 修饰的只读变量必须在定义的同时初始化,想想为什么?
    答:因为 const 修饰的变量具有只读的特性,一旦生成变无法被改变,所以如果没有在定义的时候对它进行初始化,那它就失去了存在的意义。

  2. 请问 const int *a; 和 int const *a; 两种写法表示的含义一样吗?
    答:因为没有用 const 修饰 * 这样的解释,所以这两种写法表示的含义其实是一样的,都是表示 const int * a;(一个指向 const int 类型的指针)。a 所指向的内存单元为只读,所以 (*a)++ 是不允许的;但指针 a 本身可以修改,即 a++ 是允许的。

  3. 请问下边代码为什么在小甲鱼的编译系统中不能通过编译?如果不改变 p 变量的类型,应该如何改正?

    #include <stdio.h>
    
    int main()
    {
            int num = 520;
            void *p;
    
            p = &num;
            printf("%d\n", *p);
    
            return 0;
    }
    

    答:因为 void 指针是可以指向任何类型,所以从另一个角度来看,void 指针“只保存地址,而没有记录跨度“。应该将 p 先强制转换成 int * 类型,再对其进行解引用:

    #include <stdio.h>
    
    int main()
    {
            int num = 520;
            void *p;
    
            p = &num;
            printf("%d\n", *(int *)p);
    
            return 0;
    }
    
  4. 请问下面代码可以成功通过编译并运行吗?

    #include <stdio.h>
    
    int main()
    {
            const int num = 520;
            int *p = &num;
    
            printf("num = %d\n", num);
    
            *p = 1024;
            printf("num = %d\n", num);
    
            return 0;
    }
    

    答:虽然会“友情提示”,但代码还是可以通过编译并运行的
    在这里插入图片描述
    因为 const 其实只是对变量名(num)起到一个限制作用,也就是说你不可以通过这个变量名(num)修改它所在的内存通过指针进行间接修改。但是,这并不是说这块内存就不可以修改了,如果你可以通过其他形式访问到这块内存,还是可以进行修改的。所以,尽管编译器发现苗头不对,但它也只能义务地提醒你而已。

    在赋值、初始化或参数传参的过程中,赋值号左边的类型应该比右边的类型限定更为严格,或至少是同样严格。C 语言是一门充分相信程序员的编程语言,所以一切靠自觉!

  5. 请问在下边声明中,const 限制的是 q、*q 还是 **q?

    #include <stdio.h>
    
    int main(void)
    {
            const int num = 520;
            const int * const p = &num;
            const int * const *q = &p;
    
            ……
    
            return 0;
    }
    

    答:const 限制的是 *q 和 **q。
    分析:千万不要给乱七八糟的 const 给弄晕了,记住一点:const 永远限制紧随着它的标识符。const int * const *q = &p; 相当于 (const int) * (const *q) = &p;,即第一个 const 限制的是 **q 的指向,第二个 const 限制的是 *q 的指向,唯有一个漏网之鱼 —— q 没有被限制。

  6. 如果想要使用 const 同时限制 q、*q 和 **q,应该怎么做?
    答:声明应该写成:const int * const * const q = &p;。

  7. 请问 const int * const *q; 和 cosnt int const **q; 有何区别?
    答:const int * const *q; 限制了 *q 和 **q 的指向,而 cosnt int const **q; 只限制了 **q 的指向。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
三维数组指针指向一个三维数组指针。在C语言中,可以使用指针来访问和操作多维数组指针的声明和使用可以通过使用括号来确保优先级的正确性。例如,对于一个三维数组`int arr[4]`,可以通过`int (*p)`来声明一个指向数组指针。这样的指针可以用来访问和操作三维数组的元素。 四维数组指针指向一个四维数组指针。类似地,可以使用指针来访问和操作多维数组。对于一个四维数组`int arr`,可以通过`int (*p)`来声明一个指向数组指针。这样的指针可以用来访问和操作四维数组的元素。 在使用指针访问多维数组时,可以使用多层的循环来遍历数组的元素。通过指针的间接引用,可以获取和修改数组的元素值。请参考以下代码片段作为示例: ```c #include<stdio.h> #define N 3 void show(int (*p)[N][N][N]); int main(void) { int arr = { { {{1,2,3}, {4,5,6}, {7,8,9}}, {{10,11,12}, {13,14,15}, {16,17,18}}, {{19,20,21}, {22,23,24}, {25,26,27}} }, { {{28,29,30}, {31,32,33}, {34,35,36}}, {{37,38,39}, {40,41,42}, {43,44,45}}, {{46,47,48}, {49,50,51}, {52,53,54}} } }; int (*p)[N][N][N] = arr; // 指向一个四维数组指针 show(p); } void show(int (*p)[N][N][N]) { for(int i=0; i<2; i++) { for(int j=0; j<3; j++) { for(int k=0; k<3; k++) { for(int l=0; l<3; l++) { printf("%d ", (*p)[i][j][k][l]); } printf("\n"); } printf("\n"); } printf("\n"); } } ``` 以上代码示例演示了如何使用四维数组指针访问和打印数组中的元素。在函数`show`中,通过使用指针的间接引用,可以访问和打印出数组中的每个元素的值。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [指针和三维数组](https://blog.csdn.net/preserveXing/article/details/127530224)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [关于二维数组二维数组指针](https://blog.csdn.net/LuHe123_ye/article/details/121287079)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值