前言
昨天我们学习了指针的基础知识以及基本的指针使用方法。指针相关知识内容庞大,今天我们来到第二部分:指针与数组。
这是指针专项学习第二篇,之后的博客中还会有指针专项学习的更深入的内容。
与昨天所述同理:指针学习需要有一定的C基础,如果有基础不足的情况,可以参考我之前的博客或自行学习补充。
另外,需要说明的是:由于指针理论的复杂,本博客对理论的解释其实并不够详实,主要原因在于个人认为编程语言需要更多地基于代码去学习。如果前面有未理解的地方,可以通过后文的代码进行理解。
概述
1.基础
2.基础代码
3.练习题
1.基础
这一部分内容文字较为枯燥,后面有手写笔记以及思维导图
1.1基础理论
- 内存相关
- 虚拟内存与物理内存
- 虚拟内存(内核:用户 = 1:3),物理内存包括固态硬盘(ARM)和虚拟内存映射(./a.out)
- 虚拟内存映射通过
/hello
和./a.out
实现
- 内核与用户空间
- 内核:内含计算机系统。
- 用户空间:包括栈区、堆区和静态区,涉及手动申请、释放内存
- 虚拟内存与物理内存
- C语言代码示例及说明
- 字符指针、字符串
char str = "hello"; char* p = str; // 指针可改变(段错误) char* p = "hello"; // 指针不可变(段错误)
- 数组与指针
int a[2][3]; &arr[0][0] ↔ a[0] // 一级地址 列偏移 arr ↔ &arr[0] //二级地址 行偏移 &arr // 三级地址 页偏移
特别注意多级指针与偏移的对应关系,这在代码解读题中会有体现。
...图片较小,请下载查看
-
指针偏移:&p
另外:a[i][j] = *((p+i)+j)
- 字符指针、字符串
1.2数组指针:
int arr[2][3];
int (*p)[3] = arr;
//指针的第一行.
1. ( )
> [ ]
> *
:小括号不可省略.
2. 数组指针本质上是特殊的二级指针:即将二维数组一维化,再指向第一行或某一行。
3. 数组指针类型:int (*)[]
:占8 byte(64位系统)
另外:指针数组:
int * p[4];
int * p[4] = {&a, &b, &c, &d}; //具体操作等同一维数组
1.3外部参数:
int argc
:外部传入参数个数const char *argv[]
:在命令行部传入的字符串.注意乘法要用“\*”。(eg:./a.out 8 \* 4)
1.4手写笔记:
1.5思维导图
2.基础代码
2.1 strcmp的重写:指针的使用
2.2 杨氏三角:数组指针的使用
2.3 求最大值:数组指针的使用
2.4 打印多个数组:指针数组:
2.5:外部传参的使用:
3.练习题
3.1:3个例题:
例题1:求输出结果
#include <stdio.h>
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int *p;
p = a[0];
printf("%d", p[0]);
return 0;
}
答案:1
解析:
-
代码中使用的是逗号运算符
(0, 1)
,不是{1,0};-
(0, 1)
的结果是1
(逗号运算符返回最后一个表达式的值) -
因此数组实际初始化为:
{1, 3, 5}。
-
输出1。
-
例题2:求输出结果
#include<stdio.h>
int main()
{
int a[5][5];
int (*p)[4];
p = a;
printf("%p, %d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
答案:0xfffffffffffffffc,-4
解析:
这段代码涉及指针运算和数组内存布局的关键概念。我们需要逐步分析 a
、p
的内存关系,以及 &p[4][2] - &a[4][2]
的计算过程。
3.2.1. 变量定义
int a[5][5]; // 5x5 的二维数组(共 25 个 int)
int (*p)[4]; // p 是指向长度为 4 的 int 数组的指针
p = a; // 将 a 的首地址赋给 p(类型不匹配,但 C 允许)
关键点:
-
a
是int[5][5]
,每行有 5 个int
。 -
p
是int(*)[4]
,它认为每行只有 4 个int
(尽管a
的实际行宽是 5)。 -
p = a
会导致指针p
以“每行 4 个int
”的方式解释a
的内存。
3.2.2. 内存布局
假设 a
的起始地址为 0x1000
,则内存布局如下(每个 int
占 4 字节):
地址 | 对应 a 的元素 | 对应 p 的行划分 |
---|---|---|
0x1000 | a[0][0] | p[0][0] |
0x1004 | a[0][1] | p[0][1] |
0x1008 | a[0][2] | p[0][2] |
0x100C | a[0][3] | p[0][3] |
0x1010 | a[0][4] | 不属于 p[0] |
0x1014 | a[1][0] | p[1][0] |
... | ... | ... |
0x10F0 | a[4][4] | 超出 p 的映射范围 |
p
的行划分:
-
p
认为每行只有 4 个int
(16 字节),而a
的实际行宽是 5 个int
(20 字节)。 -
因此,
p
的行会逐渐与a
的行错位。
3.2.3. p[4][2]
和 a[4][2]
的地址计算
(1) a[4][2]
的地址:
-
a
是int[5][5]
,a[4][2]
的偏移量为4 * 5 * sizeof(int) + 2 * sizeof(int) = 22 * 4 = 88
字节。 -
地址 =
0x1000 + 88 = 0x1058
。
(2) p[4][2]
的地址:
-
p
是int(*)[4]
,p[4]
的偏移量为4 * 4 * sizeof(int) = 64
字节。 -
p[4][2]
的偏移量是64 + 2 * 4 = 72
字节。 -
地址 =
0x1000 + 72 = 0x1048
。
地址关系:
&p[4][2] = 0x1048
&a[4][2] = 0x1058
-
&p[4][2] - &a[4][2]
是两个指针的减法运算,结果是它们之间的 元素个数差(以int
为单位)。 -
0x1048 - 0x1058 = -0x10
(即 -16 字节),转换为int
的个数是-16 / 4 = -4
。
3.2.4. 输出结果
-
%p
输出指针差值的十六进制形式:-
-4
的补码表示(64 位系统)是0xFFFFFFFFFFFFFFFC
,所以输出0xfffffffffffffffc
。
-
-
%d
直接输出-4
。
3.2.5关键结论
-
指针减法的结果是两个地址之间的 元素个数差(不是字节差)。
-
由于
p
的错误类型(int(*)[4]
),p[4][2]
的地址比a[4][2]
的地址 小 4 个int
,因此差值为-4
。 -
%p
打印的是地址差的补码形式(-4
的补码是0xFFFFFFFC
)。
例题3:求输出结果
#include <stdio.h>
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* ptr1 = (int*)(&aa + 1);
int* ptr2 = (int*)(*(aa + 1));
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}
答案:10,5
解析:
由于与昨天的题目类似,这里的解析相对简洁。
-
数组初始化:
-
aa[2][5]
是一个 2 行 5 列的二维数组,初始化为{ {1, 2, 3, 4, 5}, {6, 7, 8, 9, 10} }
。
-
-
指针操作:
-
&aa + 1
:&aa
是整个数组的地址,+1
会跳过整个数组(即 2x5=10 个int
),指向数组末尾的下一个位置。 -
*(aa + 1)
:aa + 1
是第二行的地址,*
解引用后得到第二行的首元素地址(即&aa[1][0]
)。
-
-
指针转换:
-
ptr1 = (int*)(&aa + 1)
:将数组末尾的地址转换为int*
类型。 -
ptr2 = (int*)(*(aa + 1))
:将第二行的首地址转换为int*
类型。
-
-
指针运算:
-
ptr1 - 1
:指向数组的最后一个元素aa[1][4]
(即10
)。 -
ptr2 - 1
:指向第一行的最后一个元素aa[0][4]
(即5
)。
-
输出结果
-
*(ptr1 - 1)
输出10
。 -
*(ptr2 - 1)
输出5
。
3.2:练习题:
题目:
1.设有说明语句:char a[]=”It is mine”;char *p=”It is mine”;则以下不正确的叙述是 。
A)a+1表示的是字符t的地址
B)p指向另外的字符串时,字符串的长度不受限制
C)p变量中存放的地址值可以改变
D)a中只能存放10个字符
2.若有定义:int a[2][3];则对a数组的第i行第j列元素值的正确引用是 。
A)*(*(a+i)+j) B)(a+i)[j] C)*(a+i+j) D)*(a+i)+j
3.若有定义:int a[2][3];则对a数组的第i行第j列元素地址的正确引用是 。
A)*(a[i]+j) B)(a+i) C)*(a+j) D)a[i]+j
4.若有程序段:int a[2][3],(*p)[3]; p=a;则对a数组元素地址的正确引用是 。
A)*(p+2) B)p[2] C)p[1]+1 D)(p+1)+2
5.若有程序段:int a[2][3],(*p)[3]; p=a;则对a数组元素的正确引用是 。
A)(p+1)[0] B)*(*(p+2)+1) C)*(p[1]+1) D)p[1]+2
6.若有定义:int (*p)[4];则标识符p 。
A)是一个指向整型变量的指针
B)是一个指针数组名
C)是一个指针,它指向一个含有四个整型元素的一维数组
D)定义不合法
7.以下与int *q[5];等价的定义语句是 。
A)int q[5] B)int *q C)int *(q[5]) D)int (*q)[5]
8.以下正确的说明语句是 。
A)int *b[]={1,3,5,7,9} ;
B)int a[5],*num[5]={&a[0],&a[1],&a[2],&a[3],&a[4]};
C)int a[]={1,3,5,7,9}; int *num[5]={a[0],a[1],a[2],a[3],a[4]};
D)int a[3][4],(*num)[4] ; num[1]=&a[1][3];
9.若有定义:int b[4][6],*p,*q[4];且0≤i<4,则不正确的赋值语句是 。
A)q[i]=b[i]; B)p=b; C)p=b[i]; D)q[i]=&b[0][0]
10.下面程序段的输出是 。
int a[ ]={2,4,6,8,10,12,14,16,18,20,22,24},*q[4],k;
for (k=0; k<4; k++) q[k]=&a[k*3];
printf(“%d\n”,q[3][0]);
A)22 B)16 C)20 D)输出不合法
11.若有定义int a[4][6];则能正确表示a数组中任一元素a[i][j](i,j均在有效范围内)地址的表达式 。
A)&a[0][0]+6*i+j B)&a[0][0]+4*j+i
C)&a[0][0]+4*i+j D)&a[0][0]+6*j+I
12.下面程序的运行结果是 。
main ( )
{ int x[5]={2,4,6,8,10}, *p, **pp ;
p=x , pp = &p ;
printf(“%d”,*(p++));
printf(“%3d”,**pp);
}
A)4 4 B)2 4 C)2 2 D)4 6
13.若有定义int x[4][3]={1,2,3,4,5,6,7,8,9,10,11,12}; int (*p)[3]=x ; 则能够正确表示数组元素x[1][2]的表达式是 。
A)*((*p+1)[2]) B)(*p+1)+2
C)*(*(p+5)) D)*(*(p+1)+2)
练习题参考答案与部分解析
1 | 2 | 3 | 4 | 5 |
D | A | D | C | C |
6 | 7 | 8 | 9 | 10 |
C | A | B | B | C |
11 | 12 | 13 | ||
A | B | D |
1.设有说明语句:char a[]=”It is mine”;char *p=”It is mine”;则以下不正确的叙述是 。
A)a+1表示的是字符t的地址
B)p指向另外的字符串时,字符串的长度不受限制
C)p变量中存放的地址值可以改变
D)a中只能存放10个字符
易错:C(地址可变,但字符串不可变)
D
2.若有定义:int a[2][3];则对a数组的第i行第j列元素值的正确引用是 。
A)*(*(a+i)+j) B)(a+i)[j] C)*(a+i+j) D)*(a+i)+j
A
3.若有定义:int a[2][3];则对a数组的第i行第j列元素地址的正确引用是 。
A)*(a[i]+j) B)(a+i) C)*(a+j) D)a[i]+j
易错:A(问的是地址,A是值)
D
4.若有程序段:int a[2][3],(*p)[3]; p=a;则对a数组元素地址的正确引用是 。
A)*(p+2) B)p[2] C)p[1]+1 D)(p+1)+2
C
5.若有程序段:int a[2][3],(*p)[3]; p=a;则对a数组元素的正确引用是 。
A)(p+1)[0] B)*(*(p+2)+1) C)*(p[1]+1) D)p[1]+2
C
6.若有定义:int (*p)[4];则标识符p 。
A)是一个指向整型变量的指针
B)是一个指针数组名
C)是一个指针,它指向一个含有四个整型元素的一维数组
D)定义不合法
C
7.以下与int *q[5];等价的定义语句是 。
A)int q[5] B)int *q C)int *(q[5]) D)int (*q)[5]
C
注意:D是数组指针,而题目是指针数组。
8.以下正确的说明语句是 。
A)int *b[]={1,3,5,7,9} ;//错
B)int a[5],*num[5]={&a[0],&a[1],&a[2],&a[3],&a[4]};//对
C)int a[]={1,3,5,7,9}; int *num[5]={a[0],a[1],a[2],a[3],a[4]};//错
D)int a[3][4],(*num)[4] ; num[1]=&a[1][3];//错
B
9.若有定义:int b[4][6],*p,*q[4];且0≤i<4,则不正确的赋值语句是 。
A)q[i]=b[i]; B)p=b; C)p=b[i]; D)q[i]=&b[0][0]
B
注意指针的等级
10.下面程序段的输出是 。
int a[ ]={2,4,6,8,10,12,14,16,18,20,22,24},*q[4],k;
for (k=0; k<4; k++) q[k]=&a[k*3];
printf(“%d\n”,q[3][0]);
A)22 B)16 C)20 D)输出不合法
C
11.若有定义int a[4][6];则能正确表示a数组中任一元素a[i][j](i,j均在有效范围内)地址的表达式 。
A)&a[0][0]+6*i+j B)&a[0][0]+4*j+i
C)&a[0][0]+4*i+j D)&a[0][0]+6*j+I
A
12.下面程序的运行结果是 。
main ( )
{ int x[5]={2,4,6,8,10}, *p, **pp ;
p=x , pp = &p ;
printf(“%d”,*(p++));
printf(“%3d”,**pp);
}
A)4 4 B)2 4 C)2 2 D)4 6
B
13.若有定义int x[4][3]={1,2,3,4,5,6,7,8,9,10,11,12}; int (*p)[3]=x ; 则能够正确表示数组元素x[1][2]的表达式是 。
A)*((*p+1)[2]) B)(*p+1)+2
C)*(*(p+5)) D)*(*(p+1)+2)
D
结语
指针学习属实需要耐力,再接再厉!