文章目录
一、指针
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.指针的课后作业
-
通常程序猿口中的“指针”,指的是什么东西?
答:内存的地址 -
指针变量只能存放地址吗?
答:是的 -
请问 int * a, b, c; 定义了多少个指针变量?
答:一个。只有 a 是指针变量,b 和 c 是普通整型变量。 -
请问 int *(a, b, c); 定义了多少个指针变量?
答:语法错误。学 C 语言可不能偷懒,int *a, *b, *c; 才是定义三个指针变量。 -
你觉得将取址运算符(&)作用于一个常数,然后试图打印该常数在内存中的地址,这样做可取吗?
include <stdio.h> int main() { printf("%p\n", &110); return 0; }
答:这样做不可取!报错信息已经提示你了:
test.c:5: error: lvalue required as unary ‘&’ operand
意思是:取址操作符(&)的作用对象应该是一个左值,而常数是右值。
-
请问下边代码是否可以成功执行呢?为什么?
#include <stdio.h> int main() { int a, b; b = 110; a = &b; printf("%d\n", *a); return 0; }
答:咋的一看,这没毛病……事实上只要你仔细推敲,这问题可大了!虽然说在我们的操作系统里:sizeof(int) == sizeof(*int) 说明存放指针变量和存放整型变量所需的存储空间是一样的。但这并不说明他们就可以互相取代。
这种做法编译器并不会认可,它会毫不犹豫给你直接报错:
-
请问为什么每次执行下边代码都会得到不同的结果?
#include <stdio.h> int main() { int *a; printf("%p\n", a); return 0; }
答:这里我们声明了一个指针变量 a,但并未对它进行初始化,这是非常危险的行为!因为我们没办法预测这个指针变量的值会被初始化为什么,它只是恰好内存中存在的“随机值”。
-
请问下边代码执行后,打印机的结果是什么?另外,*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 的存储位置,然后将右边表达式的值存放进去,所以此时为左值。 -
输入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.课后作业
-
str[3] 用指针法如何表示?
答:*(str + 3) -
假设整型指针变量 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
-
请问 str[20] 是否可以写成 20[str]?
答:可以。
因为在访问数组的元素的时候,数组名被解释为数组第一个元素的地址。所以 str[20] == *(str + 20) == *(20 + str) == [20]str -
你能猜出下边关键代码段是用于干啥的吗?
…… while (n-- && (*target2++ = *target1++) != '\0') ; ……
答:实现 strncpy 函数的功能
-
接上一题,请问代码写成下方形式,能否正确实现要求?
…… while ((*target2++ = *target1++) != '\0' && n--) ; ……
答:不能。因为这么做会超出要求 1 个字符(比如要求拷贝 5 个字符,实际拷贝了 6 个),因为代码的逻辑是 *target2++ = *target1++ 先赋值,再判断 n–。
-
获取字符串的长度 —— 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; }
-
拷贝字符串 —— 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; }
-
连接字符串 —— 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; }
-
比较字符串 —— 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.课后作业
-
请问 str[3] 和 *(str + 3) 是否完全等价?
答:完全等价。
解析:在 C 语言中,数组名是被作为指针来处理的。更确切的说,数组名就是指向数组第一个元素的指针,而数组索引就是距离第一个元素的偏移量。这也解释了为什么在 C 语言中数组下标是从 0 开始计数的,因为这样它的索引可以对应到偏移量上。因此,str[3] 和 3[str] 是相同的,因为它们在编译器的内部都会被解释为 *(str + 3)。 -
请问下边代码是否可以正常执行?如果可以,会打印什么值?如果不行,请说明原因?
#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) -
为什么不能使用 if (str1 == str2) 这样的形式来比较两个字符串?
答:因为这样比较的是指向两个字符串的指针,而不是字符串本身。 -
通常我们交换两个变量的值需要使用到一个临时变量,代码如下:
…… 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 ……
-
如果数组 array 的定义如下:
int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
请问 array 和 &array 有区别吗?
答:有。
解析:虽然 array 和 &array 的值相同,但含义是不一样的。array 表示数组第一个元素的位置,而 &array 表示的是整个数组的位置(这里将数组看做一个整体)。 -
如果不上机,你能看出下边代码将打印什么值吗?
#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 指向整个数组最后的位置。 -
如果不上机,你能看出下边代码将打印什么值吗?
#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]。 -
你如何理解“指针的类型决定了指针的视野,指针的视野决定了指针的步长”这句话?
答:你必须清楚一个指针变量将告诉我们两个信息:某个数据结构的起始地址,以及该结构的跨度。比如 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 是整个数组的地址,所以它的跨度自然而然就是整个数组啦。 -
写一个叫 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; }
-
填充空白部分的代码:
要求实现结果如下:
#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; }
-
修改上一题的代码,要求按下边格式输出:
#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.课后作业
-
如果不上机,你能看出下边代码将打印什么值吗?
#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’)
-
假如定义了二维数组 int matrix[4][5] = {0};,请问 matrix 和 matrix + 0 的含义一样吗?
答:一样啊 -
假设有二维数组如下,请问 *(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])。
-
请问 array[x][y][z] 用指针法如何表示?
答:((*(array+i)+j)+k) -
请问下边代码将打印什么值?
#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
-
如果不上机,你能看出下边代码将打印什么值吗?
#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。
-
编写一个程序,接收用户的输入,并将前 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; }
-
这次不限制正方形矩阵的尺寸,要求程序自动计算用户输入的字符,并以最大的正方形矩阵输出(比如用户输入 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; }
-
下表为广州市最近两年(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 = #
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 = #
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:0x7ffcf1146bb4
将void指针强制转换为整形指针再解引用
*pv:1024
2.NULL指针(空指针)
大部分操作系统中,地址0是一个不被使用的地址,所以,如果一个指针指向NULL,则表明该指针不指向任何东西。NULL 的宏定义如下:
#define NULL ((void *)0)
当你还不清楚要将指针初始化为什么地址时,请将它初始化NULL;在对指针进行解引用时,先检查该指针是否为NULL。这种策略可以为你今后编写大型程序节省大量的调试时间。NULL用于指针和对象,指向一个不被使用的地址。
3.课后作业
-
你猜下边代码会打印多少?
#include <stdio.h> int main() { void a; printf("%d\n", sizeof(a)); return 0; }
答:会报错!那 void 既然是无类型,我们就不应该用它来定义一个变量,如果你一定要这么做,那么程序就会给你报错
-
sizeof(void ) 呢?
答:如果你回答是 4 个字节或 8 个字节,那么本题不能算你答对。因为指针的尺寸是与编译器的目标平台相关的。比如目标平台是 32 位的,那么 sizeof(void) 就是 4,如果是 64 位的,那么 sizeof(void *) 就是 8,如果是 16 位的,那么就是 2 啦。 -
如何有效地避免出现悬空指针?
注:悬空指针就是指向了不确定的内存区域的指针,通常对这种指针进行操作会使程序发生不可预知的错误。
答:当你的指针不知道指向哪儿的时候,那么将它指向 NULL,以后就不会有太多的麻烦。比如定义一个指针变量的时候,你可以把它初始化为 NULL,这样至少可以确保它不是一个垂悬指针。 -
对 NULL 指针进行解引用,结果是什么?
答:报错。无论什么操作系统,对空指针进行解引用都是非法的。 -
请问下边定义有没有问题?
…… int *p = void *0; ……
答:报错。
这里是将 0 强制转换成 void 指针,所以要这么写才能通过编译:int *p = (void *)0;
-
请问下边代码会打印什么?
#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 = #
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.课后作业
-
你有听说过二级指针和三级指针吗?它们是什么?
答:二级指针其实就是指向指针的指针,而三级指针当然就是指向指针的指针的指针啦。 -
请按下图写出每个变量的定义过程。
int num = 520; int *p1 = # int *p2 = # int **pp1 = &p1; int ***p3 = &pp1;
-
请问什么时候 *&p 和 p 等价,什么时候 &p 和 p 等价?
& 运算符的操作数必须是左值,因为只有左值才表示一个内存单元,才会有地址,运算结果是指针类型(地址); 运算符的操作数必须是指针类型(地址),运算结果可以做左值。
结论:如果表达式 p 可以做左值,那么 *&p 和 p 等价;如果表达式 p 是指针类型(地址),那么 &*p 和 p 等价 -
如果有 int a[10];,请问 &a 和 &a[0] 表达式的类型有何不同?
答:a 是一个数组,在 &a 这个表达式中,数组类型做左值,&a 表示取整个数组的首地址,类型是int (*)[10]
;&a[0] 则表示数组 a 的第一个元素的首地址,虽然两个地址的数值相同,但后者的类型是int *
。 -
假如定义了一个指针数组 pArray 如下,请定义一个指向它的数组指针 p,让下边程序可以顺利执行。
程序实现如下:
答:char *(*p)[4] = &pArray;
-
定义以下变量:
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’ 元素。 -
分割字符串。用户输入一个英文句子,你的程序将这个字符串按空格进行分割,返回由单词组成的二维数组。
要求:- 返回的二维数组必须尽可能地节省空间(利用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 = #
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 = #
//常量指针指向的值可以改变
*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.课后作业
-
const 修饰的只读变量必须在定义的同时初始化,想想为什么?
答:因为 const 修饰的变量具有只读的特性,一旦生成变无法被改变,所以如果没有在定义的时候对它进行初始化,那它就失去了存在的意义。 -
请问 const int *a; 和 int const *a; 两种写法表示的含义一样吗?
答:因为没有用 const 修饰 * 这样的解释,所以这两种写法表示的含义其实是一样的,都是表示 const int * a;(一个指向 const int 类型的指针)。a 所指向的内存单元为只读,所以 (*a)++ 是不允许的;但指针 a 本身可以修改,即 a++ 是允许的。 -
请问下边代码为什么在小甲鱼的编译系统中不能通过编译?如果不改变 p 变量的类型,应该如何改正?
#include <stdio.h> int main() { int num = 520; void *p; p = # printf("%d\n", *p); return 0; }
答:因为 void 指针是可以指向任何类型,所以从另一个角度来看,void 指针“只保存地址,而没有记录跨度“。应该将 p 先强制转换成 int * 类型,再对其进行解引用:
#include <stdio.h> int main() { int num = 520; void *p; p = # printf("%d\n", *(int *)p); return 0; }
-
请问下面代码可以成功通过编译并运行吗?
#include <stdio.h> int main() { const int num = 520; int *p = # printf("num = %d\n", num); *p = 1024; printf("num = %d\n", num); return 0; }
答:虽然会“友情提示”,但代码还是可以通过编译并运行的
因为 const 其实只是对变量名(num)起到一个限制作用,也就是说你不可以通过这个变量名(num)修改它所在的内存通过指针进行间接修改。但是,这并不是说这块内存就不可以修改了,如果你可以通过其他形式访问到这块内存,还是可以进行修改的。所以,尽管编译器发现苗头不对,但它也只能义务地提醒你而已。在赋值、初始化或参数传参的过程中,赋值号左边的类型应该比右边的类型限定更为严格,或至少是同样严格。C 语言是一门充分相信程序员的编程语言,所以一切靠自觉!
-
请问在下边声明中,const 限制的是 q、*q 还是 **q?
#include <stdio.h> int main(void) { const int num = 520; const int * const p = # 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 没有被限制。 -
如果想要使用 const 同时限制 q、*q 和 **q,应该怎么做?
答:声明应该写成:const int * const * const q = &p;。 -
请问 const int * const *q; 和 cosnt int const **q; 有何区别?
答:const int * const *q; 限制了 *q 和 **q 的指向,而 cosnt int const **q; 只限制了 **q 的指向。