LV01-06-C语言-指针

本文主要是C语言——指针相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
--- ---

一、指针

1.什么是指针?

计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节, char 占用 1 个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号唯一的,根据编号可以准确地找到某个字节。

内存中字节的编号称为地址( Address )指针( Pointer )。在C语言中,内存单元的地址称为指针,专门用来存放地址的变量,称为指针变量,在不影响理解的情况中,有时对地址、指针和指针变量不区分,通称指针。
$$
地址 = 指针 = 指针变量
$$
对于 32 位 环境,程序能够使用的内存为 4GB ,最小的地址为 0x00000000 ,最大的地址为 0xFFFFFFFF ,注意这里的地址都是用 8 个十六进制数表示的,一共是 32 位。

对于 64 位系统来说,我们打印地址的时候会发现,它的地址都是由 12 个十六进制数表示的,这样算下来,才 48 位,按理来说不应该是 64 位吗?通过查阅资料,发现 48 位其实只是表象,显示了 48 位是因为目前为止 64 位系统的地址线只有 48 条。地址中第 48 位到第 63 位由第 47 位扩展而来(全0 全1)。因此有两段合法的地址空间,分别是

1
2
0x0000 0000 0000 0000 - 0x0000 7FFF FFFF FFFF
0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF

两段加起来总共 2^48Byte = 256TB ,一段是 128TB ,然而现在的 PC 基本达不到 128 的内存,因此第二段地址一般是见不到的,全都存在了第一段当中,所以看到的 48 位地址应该在前边再加上 0000 这才是完整的 64 位地址。

2.一切皆地址?

C语言 用变量来存储数据,用函数来定义一段可以重复使用的代码,它们最终都要放到内存中才能供 CPU 使用。 CPU 只能通过地址来取得内存中的代码和数据,程序在执行过程中会告知 CPU 要执行的代码以及要读写的数据的地址。

CPU 访问内存时需要的是地址,而不是变量名和函数名。变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。

例如:

变量 a、b、c 在内存中的地址分别是 0X0000、0X0004、0X0008 ,那么加法运算 c = a + b; 将会被转换成类似下面的形式:

1
0X0008 = (0X0000) + (0X0004);

( ) 表示取值操作,整个表达式的意思是,取出地址 0X0000 和 0X0004 上的值,将它们相加,把相加的结果赋值给地址为 0X0008 的内存。

所以,从根本上来说,数据的运算其实都是通过地址来运算的。

3.为什么要用指针?

C程序设计中使用指针可以

  • 使程序简洁、紧凑、高效。

  • 有效地表示复杂的数据结构。

  • 动态分配内存。

  • 得到多于一个的函数返回值。

4.怎么使用指针?

4.1指针变量的定义

1
storage_type data_type *p_name;
storage_type 存储类型(指针变量本身的存储类型,可以说明也可以不说明)
data_type 任意有效的 C 数据类型(指针目标的数据类型,必须说明)
p_name 指针变量名

例如:

1
2
3
4
5
static int *p1;  /* p1是一个指向静态整型变量的指针变量 */
float *p2; /* p2是一个指向浮点型变量的指针变量 */
char *p3; /* p3是一个指向字符型变量的指针变量 */
int *a, *b, *c; /* a、b、c 的都是 int* 类型的指针变量 */
int *a, b, c; /* a 是 int* 类型的指针变量,b、c 都是类型为 int 的普通变量 */

【注意】指针说明时指定的数据类型不是指针变量本身的数据类型,而是指针目标的数据类型,简称为指针的数据类型。

【说明】引入指针要注意程序中的 p 、 *p 和 &p 三种表示方法的不同意义。设 p 为一个指针( int *p; )则:

p 指针变量, 它的内容是地址量
*p 指针所指向的对象, 它的内容是数据
&p 指针变量占用的存储区域的地址,是个常量

4.2指针变量的赋值

指针变量在使用前,不仅要定义说明,而且要赋予具体的值,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值(赋为 NULL 值的指针被称为空指针),未经赋值的指针不能随便使用,否则将导致程序的错误,并且指针变量的值只能是变量的地址,不能是其他数据,否则将会导致错误产生。

没有合法指向的指针称为“野”指针。“野”指针随机指向一块空间,该空间中存储的可能是其他程序的数据甚至是系统数据,故不能对“野”指针所指向的空间进行存取操作,否则轻者会引起程序崩溃,严重的可能导致整个系统崩溃。

1
2
3
4
5
6
/* 1. 定义时直接赋值 */
<存储类型> <数据类型> *<指针变量名> = <地址量>;

/* 2. 定义完成后再赋值 */
<存储类型> <数据类型> *<指针变量名>;
<指针变量名> = <地址量>;

例如:

1
2
3
4
5
6
7
int a = 10;        /* 定义一个整型变量 */

int *p = NULL; /* 定义一个空指针 */
int *p1 = &a; /* 定义一个指针变量 p1,同时赋初值,使其指向整型变量 a */

int *p2; /* 定义一个指针变量 p2,不进行初始化赋值 */
p2 = &a; /* 将变量 a 的地址赋给指针变量 p2 ,使其指向整型变量 a */

【注意】

(1)定义指针变量时必须带 * ,给指针变量赋值时**不能带 * **。

(2)和普通变量一样,指针变量也可以被多次修改,可以改变指针变量的值,使其指向不同的地址。

(3)地址量必须是一个地址,若是普通变量,要加上**取地址 & **符号。

4.3指针变量的引用

  • 获取数据

指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:

1
*<p_name>;

这里的 * 并不是乘号,而是被称为指针运算符,用来获取指针变量所指向地址中所存储的数据。

例如:

1
2
3
4
5
6
7
8
9
int main(int argc, char *argv[])
{
int a =10;
int *p1 = &a;
int *p2;
p2 = &a;
printf("a=%d, *p1=%d, *p2=%d\n", a, *p1, *p2);
return 0;
}
image-20220130130321406
  • 修改数据

通过指针可以直接修改指针所指向变量的数据,使用格式如下

1
*<p_name> = value;

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main(int argc, char *argv[])
{
int a =10;
int *p1 = &a;
int *p2;
p2 = &a;

printf("a=%d, *p1=%d, *p2=%d\n", a, *p1, *p2);
*p1 = 20;
printf("a=%d, *p1=%d, *p2=%d\n", a, *p1, *p2);
*p2 = 30;
printf("a=%d, *p1=%d, *p2=%d\n", a, *p1, *p2);
a = 40;
printf("a=%d, *p1=%d, *p2=%d\n", a, *p1, *p2);

return 0;
}
image-20220130141024744

4.4指针和变量在内存中的存放

要想更加清楚地理解指针变量,我们可以编写程序来进行测试,好清楚它在内存中的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main(int argc, char *argv[])
{
int a =10;
int *p1 = &a;
int *p2;
p2 = &a;

printf("a=%d, &a=%p\n", a, &a);
printf("*p1=%d, p1=%p, &p1=%p, &(*p1)=%p\n", *p1, p1, &p1, &(*p1));
printf("*p2=%d, p2=%p, &p2=%p, &(*p2)=%p\n", *p2, p2, &p2, &(*p2));

return 0;
}
image-20220130174049078

通过打印指针变量的地址以及普通变量的地址可以得到指针变量和普通变量在内存中的存放如下所示:

image-20220130180725096

5.指针占几个字节?

从上边对指针的概念的阐述中可以得出,指针也可以称之为地址,而地址的大小就与计算机多少位相关联起来。常见的就是 64 位系统和 32 位系统,接下来就来验证一下不同位数的系统中的指针大小。

  • 64 位系统
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(int argc, char *argv[])
{
int a =10;
int *p = &a;

printf("sizeof(p)=%ld\n",sizeof(p));

return 0;
}
image-20220130181429628

可以看出在 64 位系统下,指针变量 p 占据了 8Byte 的空间,一共就是 64bit 。

  • 32 位系统
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(int argc, char *argv[])
{
int a =10;
int *p = &a;

printf("sizeof(p)=%ld\n",sizeof(p));/* %ld 要改为 %d,否则就会有一个警告 */

return 0;
}
image-20220130182415239

可以看出在 32 位系统下,指针变量 p 占据了 4Byte 的空间,一共就是 32bit 。

6.指针的运算

指针运算是以指针变量所存放的地址量作为运算量而进行的运算,指针运算的实质就是地址的计算,指针运算的种类是有限的,它只能进行赋值运算算术运算关系运算

6.1指针的赋值运算

见 4.2指针变量的赋值 一节。

6.2指针的算术运算

6.2.1算术运算

先定义指针变量 px 和 py 。

运算符 计算形式 含义
+ px + n 指针向地址大的方向移动 n 个数据
实际位置的地址量: (px) + sizeof(px的指向的数据类型) * n
- px - n 指针向地址小的方向移动 n 个数据
实际位置的地址量是: (px) - sizeof(px的指向的数据类型) * n
++ px++ 指针向地址大的方向移动 n 个数据
++px
-- px-- 指针向地址小的方向移动 n 个数据
--px
- px - py 指针 px 与 py之间间隔元素的个数
px 和 py 必须是同意数据类型的指针变量,否则两者相减毫无意义。

【注意】

(1)不同数据类型的两个指针实行加减整数运算是无意义的。

(2) px - py 运算的结果不是地址量,而是一个整数值,意思就它们相减的结果是两指针指向的地址位置之间相隔数据的个数,而不是两指针持有的地址值相减的结果。(两指针数据类型要一致

6.2.2使用实例

这里需要提前用一下下边要说的指针与数组的知识,这样通过数组来进行验证,对于指针的算术运算更容易理解些。这里主要验证两指针相减的情况,其他的都比较简单,下一节介绍数组与指针的时候也会经常用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
int main(int argc, char *argv[])
{
int a[6] = {0, 1, 2, 3, 4, 5};
char b[6] = {0, 1, 2, 3, 4, 5};
int i;
int *px1 = &a[0];
int *py1 = &a[5];
char *px2 = &b[0];
char *py2 = &b[3];
for(i = 0; i < 6; i++)
{
printf("a[%d]=%d , &a[%d]=%p | b[%d]=%d , &b[%d]=%p\n", i, a[i], i, &a[i], i, b[i], i, &b[i]);
}
printf("%p - %p = %ld\n", py1, px1, py1 - px1);
printf("%p - %p = %ld\n", py2, px2, py2 - px2);

return 0;
}
image-20220130210001974

6.3指针的关系运算

  • 关系运算

两指针之间的关系运算表示它们指向的地址位置之间的关系。指向地址大的指针大于指向地址小的指针。

运算符 说明 举例
> 大于 px > py
< 小于 px < py
>= 大于等于 px >= py
<= 小于等于 px <= py
== 等于 px == py
!= 不等于 px != py

【注意】

(1)不同数据类型的指针之间的关系运算没有任何意义,指向不同数据区域的数据的两个指针之间的关系运算也没有意义。

(2)指针与一般整数变量之间的关系运算没有意义。但可以和 0(NULL) 进行等于或不等于的关系运算,判断指针是否为空。

7. void 指针

7.1 void 指针概念

void 指针是一种不确定数据类型的指针变量,它可以通过强制类型转换让该变量指向任何数据类型的变量。由于 void 指针没有特定的类型,因此它可以指向任何类型的数据

【注意】

(1)任何类型的指针都可以直接赋值给 void 指针,而无需进行其他相关的强制类型转换。

(2)要将void 指针 赋给其他类型的指针,则需要强制类型转换。

7.2一般形式

1
void *<指针变量名称>;

【注意】

在 ANSI C 标准中,对于 void 指针,虽然任何类型的指针都可以直接赋值给 void 指针,但是在没有强制类型转换之前,不能进行任何指针的算术运算,这是因为在引用指针目标值时, void * 相当于类型不确定,只知道指针指的起始地址,但是不知道占用的字节数,所以就没有颁发决定以什么单位来进行偏移,就会出现编译错误。

在 GNU 中则允许其进行算术运算,因为在默认情况下, GNU 认为 void * 和 char * 一样,既然是确定的,当然可以进行一些算术操作。

7.3使用规则

进行强制类型转换格式:

1
*(<目标数据类型> *)<指针变量名>
  • 对 void 指针赋值

void 指针可以指向任意类型的数据,就是说可以用任意类型的指针对 void 指针赋值。

例如:

1
2
3
int *a;  /* 定义一个整型指针变量 a */
void *p; /* 定义一个 void 型指针变量 p */
p = a; /* 将指针变量 a 指向的地址赋值给 p */
  • 将 void 指针赋给其他类型指针

“空类型”可以包容“有类型”,而“有类型”则不能包容“空类型”,要将 void 指针赋值给其他类型的指针,必须进行强制类型转换。

1
2
3
void *p1;     /* 定义一个 void 型指针变量 p1 */
int *p2; /* 定义一个整型指针变量 p2 */
p2 = (int*)p1;/* 将void指针变量p1赋值给int指针变量p2 */
  • 使用 void 指针

必须进行强制类型转换才可以使用。

1
2
3
4
5
6
int *a;  /* 定义一个整型指针变量 a */
void *p; /* 定义一个 void 型指针变量 p */

p = a; /* 将指针变量 a 指向的地址赋值给 p */

ptrintf("*p=%d\n", *(int *)p);

8. const 指针

8.1 const 变量

C语言 中,关键字 const 修饰变量,可以使变量常量化,这样就使得变量的值不能修改,从而达到保护变量的目的。一般的说明形式如下:

1
const <数据类型> 变量名 = [<表达式>] ;

那如果说当变量有 const 修饰时,想用指针间接访问变量,指针也要有 const 修饰。那么 const 放在指针声明的什么位置呢?请接着往下看。

【说明】修饰指针的时候可以简记为左数右指,意思就是const在*号左边,那么指针指向的数据不可改变,但是指针变量可以改变。const在*号右边,表示指针变量指向的数据可以改变,但是指针变量不可改变,需要注意的是const不可以放在指针变量后边

8.2常量化指针目标表达式

  • 一般形式

常量化指针目标是限制通过指针改变其目标的数值 ,但**<指针变量> 存储的地址值可以修改**。

1
const <数据类型> *<指针变量名称>[= <指针运算表达式>];

例如:

1
2
3
int a = 10;
int b = 20;
const int *p = &a;

【分析】上边定义了两个整型变量 a,b 和一个带有 const 修饰的指针变量,此时指针 p 指向的是 a ,我们可以改变 p 中的地址值,即通过 p = &b; 使其指向 b ;我们也可以通过 *p 来访问相应的目标值,但是,我们无法通过 *p = 30; 这样的赋值操作来改变目标值,若强行修改则会报以下错误:

1
error: assignment of read-only location ‘*p’

8.3常量化指针变量

  • 一般形式

常量化指针变量,使得** <指针变量> 存储的地址值不可被修改,但是可以通过 *<指针变量名称> 修改指针所指向变量的值**。

1
<数据类型> * const <指针变量名> = <指针运算表达式> ; 

例如:

1
2
3
int a = 10;
int b = 20;
int * const p = &a;

【分析】上边定义了两个整型变量 a,b 和一个带有 const 修饰的指针变量,此时指针 p 指向的是 a ,我们无法改变 p 中的地址值,即通过 p = &b; 使其指向 b ,若强行修改则会报以下错误:

1
error: assignment of read-only variable ‘p’

我们可以通过 *p 来访问相应的目标值,我们也可以通过 *p = 30; 这样的赋值操作来改变目标值。

8.4常量化指针变量及其目标表达式

  • 一般形式

常量化指针变量及其目标表达式,使得既不可以修改<指针变量>的地址,也不可以通过<指针变量名称>修改指针所指向变量的值*。

1
const <数据类型> *const <指针变量名> = <指针运算表达式> ; 

例如:

1
2
3
int a = 10;
int b = 20;
const int * const p = &a;

分析:上边定义了两个整型变量 a,b 和一个带有 const 修饰的指针变量,此时指针 p 指向的是 a ,我们无法改变 p 中的地址值,即通过 p = &b; 使其指向 b ;我们可以通过 *p 来访问相应的目标值,但是,我们无法通过 *p = 30; 这样的赋值操作来改变目标值。若强行修改地址值或者目标值,则会报以下错误:

1
2
error: assignment of read-only variable ‘p’  /* 通过p=&b;修改存储的地址值 */
error: assignment of read-only location ‘*p’ /* 通过*p=30;修改目标值 */

二、指针与数组

1.指针与一维数组

1.1数组的指针

数组元素的地址是指数组元素在内存中的起始地址,可以由各个元素加上取地址符号 & 组成,而数组名就代表了数组的起始地址。

数组指针就是指向数组起始地址的指针,其本质就是指针,一维数组名可以看做是一维数组的指针(但是一维数组名不能像指针变量那样做自 ++ 和 – 运算)

1.2数组元素的表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int main(int argc, char *argv[])
{
int a[3] = {0, 1, 2};
int *p = a;
int i;
printf("a[i]\t p[i]\t *(a+i)\t *(p+i)\t|");
printf("\t &a[i]\t &p[i]\t a+i\t p+i \n");
printf("---------------------------------------------------");
printf("---------------------------------------------------\n");
for(i = 0; i < 3; i++)
{
printf(" %d\t %d\t %d\t %d\t|", a[i], p[i], *(a+i), *(p+i));
printf("\t%p\t%p\t%p\t%p\n", &a[i], &p[i], a+i, p+i);
}

return 0;
}
image-20220204134546751

从上边实例可以看出:

  • a[i],p[i],*(p+i),*(a+i)四者等价,均可表 示数组元素;

  • &a[i],&p[i],p+i, a+i四者等价,均可表示相应数组元素地址;

元素表示 元素地址表示
a[i] ⇔ p[i] ⇔ *(p+i) ⇔ *(a+i) &a[i] ⇔ &p[i] ⇔ p+i ⇔ a+i
a[0] p[0] *(p+0) *(a+0) &a[0] &p[0] p+0 a+0
a[1] p[1] *(p+1) *(a+1) &a[1] &p[1] p+1 a+1
a[2] p[2] *(p+2) *(a+2) &a[2] &p[2] p+2 a+2

【注意】

(1)指针变量和数组在访问数组中元素时,一定条件下其使用方法具有相同的形式,因为指针变量和数组名都是地址量

(2)指针变量和数组的指针(或叫数组名)在本质上不同指针变量是地址变量,而数组的指针是地址常量。例如 p++, p– (正确); a++, a–(错误) 。

2.指针与二维数组

2.1列指针遍历二维数组

在 C语言中 二维数组的元素连续存储,按行优先存储,所以自然可以使用一级指针来进行访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(int argc, char *argv[])
{
int a[2][3] = {{0, 1, 2}, {3, 4, 5}};
int *p = a[0]; /* 不能直接写 int *p = a;这样类型不匹配,会有警告 */
int i, j;
for(i = 0;i < 2; i++)
{
for(j = 0;j < 3; j++)
{
printf("a[%d][%d]=%d,&a[%d][%d]=%p | ",i, j, a[i][j], i, j, &a[i][j]);
printf("*(p+%d)=%d,p+%d=%p\n",i * 3 + j, *(p + i * 3 + j), i * 3 + j, p + i * 3 + j);
}
}
return 0;
}
image-20220204155825156

从例子中可以看出,一级指针 p 移动了 6 个数,到达 p + 5 时,将 a[2][3] 中的 6 个数访问完毕,相当于移动了 6 列,因此一级指针 p 也叫作列指针

【注意】

1
int *p= a[0];  /* 列指针的定义法 */ 

2.2行指针遍历二维数组

  • 行指针的概念

在学习二维数组时,我们就知道二维数组可以看做是多个一维数组,对于 a[2][3] 来说,有如下表格中理解:

行名(代表了地址) 每行元素
a a[0] &a[0][0] a[0][0] a[0][1] a[0][2]
a + 1 a[1] &a[1][0] a[1][0] a[1][1] a[1][2]

在指针与一维数组中我们知道** a[i], p[i], *(p+i), *(a+i)四者等价;&a[i], &p[i], p+i, a+i四者等价 **,所以类似的有:

行名(代表了地址) 每行元素地址
第1行 a a[0] ⇔ *(a + 0) &a[0][0] a[0][0] a[0][1] a[0][2]
第2行 a + 1 a[1] ⇔ *(a + 1) &a[1][0] a[1][0] a[1][1] a[1][2]

所以,第2行第2列的元素就可以表示为:

a[1][1] ⇔ *(&a[1][0] + 1) ⇔ *(a[1] + 1) ⇔ *(*(a + 1) + 1)

上边的 a[0]、a[1] 代表了行地址,但是与一维数组名一样,是地址常量。指针变量存储的是地址,那么存储行地址的指针变量,被称之为行指针

  • 行指针的定义
1
<存储类型> <数据类型> (*<指针变量名>)[表达式] ; 

【注意】

(1)存储类型指的是 auto, register, static, extern ,若省略,则默认为 auto 。

(2)赋值时要注意定义为行指针的指针变量才可以直接将数组名赋值给指针变量,否则会有警告。

例如:

1
2
3
int a[2][3]; 
int *q = a; /* 会报警告*/
int (*p)[3] = a; /* 正确方式*/
  • 行指针使用

若有 int a[2][3]; int (*p) = a; 则有

a[i][j] ⇔ *(&a[i][0] + j) ⇔ *(a[i] + j) ⇔ *(*(a + i) + j)
p[i][j] ⇔ *(&p[i][0] + j) ⇔ *(p[i] + j) ⇔ *(*(p + i) + j)

2.3行指针与列指针

对于如下定义的二维数组:

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

有如下说明

指针类型 表示形式 说明
行指针 a 或 a + 0 指向第0行
a + 1 指向第1行
列指针 a[0] 是一维数组的名称,也是整个数组的首地址。
第0行第1个元素(a[0][0])的地址。
a[0] + 1 第0行第2个元素(a[0][1])的地址
a[0] + 2 第0行第3个元素(a[0][2])的地址
a[1] 第1行第1个元素(a[1][0])的地址
a[1] + 1 第1行第2个元素(a[1][1])的地址
a[1] + 2 第1行第3个元素(a[1][2])的地址

可以将列指针理解为行指针的具体元素,行指针理解为列指针的地址。那么他们的具体关系就可以这样表示:

*行指针 ⇒ 列指针
&列指针 ⇒ 行指针

一般来说我们都是定义行指针,再转换为列指针进而访问数组具体元素,所以这里只写一个行指针转换为列指针的列子,列指针转行指针应该属于一个逆运算,但是意义好像不大,也基本没有遇到过。

行指针⇒列指针 列指针等价表示 指向内容 指向内容的等价表示 数组表示
a ⇒ *a a[0] *a[0] *(*a) a[0][0]
a+1 ⇒ *(a+1) a[1] *a[1] *(*(a+1)) a[1][0]

3.指针数组

3.1指针数组的定义

指针数组是指由若干个具有相同存储类型和数据类型指针变量构成的集合,也就是说这个数组中的每个元素都是同类型的指针变量。

一般声明形式:

1
<存储类型>  <数据类型>  *<指针数组名>[<指针数组大小>];

【注意】指针数组名表示该指针数组的起始地址

例如:

1
2
int *pa[2]; /* 定义了一个长度为 2 的 int 型指针数组*/
char *pb[6];/* 定义了一个长度为 6 的 char 型指针数组*/

但是其实写成下边这种形式可能会更好理解一些,看个人吧。

1
2
int * pa[2]; /* 定义了一个长度为 2 的 int 型指针数组*/
char * pb[6];/* 定义了一个长度为 6 的 char 型指针数组*/

可以这样理解,对于 int * pa[2]; 来说,是声明了一个 int 类型的一维数组 pa[2] ,它包含了两个元素,每个元素都是一个 int 类型的指针。

3.2指针数组的初始化

初始化写法 等价写法(赋值)
int a = 10;
int b = 20;
int * p[2] = {&a, &b};
int a = 10;
int b = 20;
int * p[2];
p[0] = &a;
p[1] = &b;

3.3指针数组的存储空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(int argc, char *argv[])
{
int a = 10;
int b = 20;
int * p[2] = {&a, &b};

printf("p=%p,&p=%p,&p[0]=%p,&p[1]=%p\n", p, &p, &p[0],&p[1]);
printf("a=%d,&a=%p,p[0]=%p,*p[0]=%d\n", a, &a, p[0], *p[0]);
printf("b=%d,&b=%p,p[1]=%p,*p[1]=%d\n", b, &b, p[1], *p[1]);

return 0;
}
image-20220204190231704

由程序结果可知变量在内存中存储如下图所示:

image-20220204195558425

【注意】图中红色的两个地址之间是有 8Byte 的空间,类型明明是 int ,应该是 4 个字节才对吧,又怎么会这样呢?在介绍指针的的时候说过,指针变量所占空间是固定的,在 32 位系统中就是 4 个字节,而在 64 位系统中则是 8 个字节,由于这里是 64 位系统,所以指针数组名加 1 ,就会移动 8 个字节。

3.4指针数组的数组名

对于指针数组的数组名,它代表了指针数组的起始地址。由于数组元素已经是指针了,而数组名又是数组首元素的地址,因此指针数组名就是指针数组中首个指针元素的地址,所以指针数组名是一个多级指针,具体来说就是一个二级指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main(int argc, char *argv[])
{
int a = 10;
int b = 20;
int * p[2] = {&a, &b};
int * *q;

q = p;
printf("sizeof(p)=%ld,sizeof(q)=%ld\n",sizeof(p), sizeof(q));
printf("q=%p,&q=%p,&q[0]=%p,&q[1]=%p\n", q, &q, &p[0],&q[1]);
printf("p=%p,&p=%p,&p[0]=%p,&p[1]=%p\n", p, &p, &p[0],&p[1]);
printf("a=%d,&a=%p,p[0]=%p,*p[0]=%d,**p=%d\n", a, &a, p[0], *p[0], **p);
printf("b=%d,&b=%p,p[1]=%p,*p[1]=%d,**(p+1)=%d\n", b, &b, p[1], *p[1], **(p+1));

return 0;
}
image-20220204203431783

【注意】

(1)从另一个方面理解,在一维数组中我们知道** a[i], p[i], *(p+i), *(a+i)四者等价 *,所以直接将 p[i]替换为(p+i) 那么也可以得到上边的结果。

(2)通过 sizeof(指针数组名) 可以求得指针数组占据的总空间,而每个元素所占空间与系统相关。

3.5指针数组与二维数组

考虑到二维数组可以通过行指针来访问,这样的话通过指针数组来存储二维数组的行指针,那么这样就可以通过指针数组遍历二维数组了。

image-20220204204857551
  • 通过指针数组遍历二维数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main(int argc, char *argv[])
{
int a [2][3] = {{0, 1, 2}, {3, 4, 5}};
int * p[2] = {a[0], a[1]};
int i, j;
for(i = 0; i < 2; i++)
{
for(j = 0; j < 3; j++)
{
printf("a[%d][%d]=%d,p[%d][%d]=%d,", i, j, a[i][j], i, j, p[i][j]);
printf("*(p[%d] + %d)=%d,*(*(p + %d) + %d)=%d\n", i, j, *(p[i] + j), i, j, *(*(p + i) + j));
}
}

return 0;
}
image-20220204210115901

三、指针与字符串

1.字符指针变量的概念

C语言 通过使用字符数 组来处理字符串, char 数据类型的指针变量称为字符指针变量。字符指针变量与字符数组有着密切关系,它也被用来处理字符串。

2.字符指针变量的赋值

  • 指向字符数组
1
2
char str[] = "strings";
char *p = str;

这样的赋值方式是将字符指针变量 p 指向数组 str 的首地址,即将字符串的首地址赋予指针,并不是把该字符串复制到指针中。

  • 指向字符串常量
1
char *p = "string";

这样的赋值方式是将一个字符串常量的首地址赋给指针变量 p ,这样的赋值方式会使指针指向对象的值不能被修改,例如 *p = ‘S’; 这将导致程序报错,原因就在于字符串常量不能被修改。

3.字符串打印

由于指针变量指向的是字符串的首地址,所以我们可以这样来打印指针变量指向的字符串。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(int argc, char *argv[])
{
char str[] = "strings";
char *p = str;

printf("str[]=%s,p=%s\n", str, p);

return 0;
}

输出结果如下:

1
str[]=strings,p=strings

四、多级指针

1.多级指针的概念

  • 把一个指向指针变量指针变量,称为多级指针变量。

  • 对于指向处理数据指针变量称为一级指针变量,简称一级指针。

  • 指向一级指针变量指针变量称为二级指针变量,简称二级指针。

2.二(多)级指针的声明

1
<存储类型> <数据类型> ** <指针名>; /* 几级指针就写几个* 号*/

【注意】

(1)指针变量也是一种变量,也会占用存储空间,也可以使用 & 获取它的地址。

(2) C语言 不限制指针的级数,**每增加一级指针,在定义指针变量时就得增加一个星号 * **。实际开发中会经常使用一级指针和二级指针,几乎用不到高级指针。

例如:

1
2
3
4
5
int a = 10;  /* 定义了一个整型变量 a */
int *p1; /* 定义了一个一级整型指针变量 p1 */
int **p2; /* 定义了一个二级整型指针变量 p2 */
int ***p3; /* 定义了一个三级整型指针变量 p3 */
int ****p4; /* 定义了一个四级整型指针变量 p4 */

3.二(多)级指针的初始化

初始化写法 等价写法(赋值)
int a = 10;
int *p1 = &a;
int **p2 = &p1;
int ***p3 = &p2;
int ****p4 = &p3;
int a = 10;
int *p1;
int **p2;
int ***p3;
int ****p4;
p1 = &a;
p2 = &p1;
p3 = &p2;
p4 = &p3;

【注意】指针变量的初始化或者赋值,后边一定要是地址

【说明】

image-20220206154923623

上边初始化的部分,定义了一个整型变量 a ,然后定义了一个一级指针变量 p1 ,指针变量 p1 中存放的数据是整型变量 a 的地址;又定义了一个二级指针变量 p2 , p2 中存放的是 p1 的地址;又定义了一个指针变量 p3 ,里边存放的是 p2 的地址;再后面又定义了一个四级指针变量 p4 ,里边存放的是 p3 的地址。

4.二(多)级指针的引用

1
** <指针名>; /* 几级指针就写几个* 号*/

通过上一节的多级指针初始化的分析可知,多级指针存放的都是地址,而 * 号可以取出地址中的数据,详细的使用情况可看下一节多级指针的存储空间的介绍。

5.多级指针的存储空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main(int argc, char *argv[])
{
int a = 10;
int *p1 = &a;
int **p2 = &p1;
int ***p3 = &p2;
int ****p4 = &p3;

printf("a=%d,&a=%p\n", a, &a);
printf("*p1=%d,p1=%p,&p1=%p\n", *p1, p1, &p1);
printf("**p2=%d,*p2=%p,p2=%p,&p2=%p\n", **p2, *p2, p2, &p2);
printf("***p3=%d,**p3=%p,*p3=%p,p3=%p,&p3=%p\n", ***p3, **p3, *p3, p3, &p3);
printf("****p4=%d,***p4=%p,**p4=%p,*p4=%p,p4=%p,&p4=%p\n", ****p4, ***p4, **p4, *p4, p4, &p4);

return 0;
}
image-20220206165514337

由上图分析可得各级指针在内存中情况如下:

image-20220206172020146

【注意】

(1)相邻红色的地址编号相差都为 8 ,这是因为二级、三级和四级指针中存储的都是指针变量,而在 64 位系统中指针变量所占空间为 8 个字节,所以就出现了上边的情况。

(2) * 号是自右向左结合,所以引用多级指针的时候的时候可以这样理解:
$$
\begin{flalign}
****p4 = ***(*p4) = ***p3 = **(*p3) = **p2 = *(*p2) = *p1 = 10 \
***p3 = **(*p3) = **p2 = *(*p2) = *p1 = 10 \
**p2 = *(*p2) = *p1 = 10 \
*p1 = 10
\end{flalign}
$$

6.多级指针的运算

总的来说,多级指针也是指针,符合一级指针的运算规则。指针变量加1,是向地址大的方向移动一个目标数据。类似,多级指针运算也是以其目标变量为单位进行偏移。

比如, int **p;p++; 移动一个 int * 变量所占的内存空间。再比如 int ***p;p++ 移动一个 int ** 所占的内存空间。

其实多级指针中存放的都是指针变量,除了一级指针,多级指针移动时,移动的都是一个指针变量的大小,在 32 位系统中是 4 个字节,在 64 位系统中是 8 个字节。

五、函数指针

为了汇总,这里需要使用函数的概念,函数的详情可以看下一章。

1.函数指针的定义

如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址函数名表示的就是这个地址。通过指针的概念,我们知道既然是地址,那么就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针

2.函数指针的声明

1
<数据类型> (*<函数指针名称>) (<参数说明列表>);
数据类型 函数指针所指向的函数的返回值类型
(*<函数指针名称>) *说明为指针()不可缺省,表明为函数的指针
参数说明列表 应该与函数指针所指向的函数的形参说明保持一致

【注意】指向函数的指针变量没有 ++ 和 – 运算

例如:

1
int (*p)(int, int);

这个语句定义了一个指向函数的指针变量 p :

(1)它是一个指针变量,所以要有一个 * ,即( *p );

(2)前面的 int 表示这个指针变量可以指向返回值类型为 int 型的函数

(3)括号中的两个 int 表示这个指针变量可以指向有两个参数且都是 int 型的函数

所以总体来说,这个语句的意思就是:定义了一个指针变量 p ,该指针变量可以指向返回值类型为 int 型,且有两个整型参数的函数。 p 的类型为 int (*)(int,int) 。

3.函数指针的使用

3.1函数指针初始化

前边需要有一个函数的声明:

1
int sum(int a, int b); /* 函数声明 */
初始化写法 等价写法(赋值)
int (*p)(int, int) = sum; int (*p)(int, int);
p = sum;

3.2函数指针的调用

1
[ <变量名> = ] (*<函数指针名称>)(<实际传入参数说明列表>);

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 提前定义的函数 */
int sum(int a, int b); /* 函数声明 */
/* -------------------------------------------------- */
/* 主程序中函数指针的操作 */
int (*p)(int, int) = sum; /* 定义一个函数指针 */
printf("a=%d,b=%d,(*p)(a, b)=%d\n", a, b, (*p)(a, b));

/* -------------------------------------------------- */
/* 或者 */
int c = 0;
int (*p)(int, int) = sum; /* 定义一个函数指针 */
c = (*p)(a, b);
printf("a=%d,b=%d,c=%d\n", a, b, c);

4.函数指针的存储空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>

int sum(int a, int b); /* 函数声明 */

int main(int argc, char *argv[])
{
int a = 3;
int b = 2;
int c = 0;

int (*p)(int, int) = sum; /* 定义一个函数指针 */
c = (*p)(a, b);

printf("a=%d,b=%d,c=%d\n", a, b, c);
printf("&a=%p,&b=%p,&c=%p\n", &a, &b,&c);
printf("&sum=%p,p=%p,&p=%p\n", sum, p, &p);

return 0;
}

int sum(int a, int b)
{
return (a + b);
}
image-20220207104831017

由上图分析可以得到函数和函数指针在内存中的分布如下图:

image-20220207105826214

5.函数指针类型

5.1定义格式

上边我们已经知道了可以按照下边的形式定义一个函数指针:

1
<数据类型> (*<函数指针名称>) (<参数说明列表>);

可是当参数列表中参数很多的时候,就显得很麻烦,这个时候我们可以借助 typedef 来定义一个函数指针类型:

1
typedef <数据类型> (*<函数指针名称>) (<参数说明列表>);

5.2使用实例

点击查看实例
test.c
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
#include <stdio.h>

typedef int (*MyFunc)(int, char);

int func1(int a, char b);
int func2(int a, char b);

int main(int argc, char *argv[])
{
int (*p)(int, char);/* 定义一个可以指向带有int和char类型参数且返回值为int类型的函数指针变量 */
MyFunc pfunc; /* 与上边的含义相同 */

p = func1;
printf("func1 return:%d\n", (*p)(5, 'a'));
p = func2;
printf("func1 return:%d\n\n", (*p)(6, 'b'));

pfunc = func1;
printf("func1 return:%d\n", (*pfunc)(5, 'a'));
pfunc = func2;
printf("func1 return:%d\n", (*pfunc)(6, 'b'));
return 0;
}

int func1(int a, char b)
{
printf("func1:a = %d, b = %c\n", a, b);
return 1;
}

int func2(int a, char b)
{
printf("func2:a = %d, b = %c\n", a, b);
return 2;
}

在终端执行以下命令编译程序:

1
2
gcc test.c -Wall # 生成可执行文件 a.out 
./a.out # 执行可执行程序

然后,终端会有以下信息显示:

1
2
3
4
5
6
7
8
9
10
func1:a = 5, b = a
func1 return:1
func2:a = 6, b = b
func1 return:2

func1:a = 5, b = a
func1 return:1
func2:a = 6, b = b
func1 return:2

可见两种定义其实是完全一致的。

6.函数指针数组

6.1定义

函数指针数组是一个保存若干个函数名的数组,一般形式如下:

1
<数据类型> (*<函数指针数组名称> [<大小>] )(<参数说明列表> );

其中,<大小>是指函数指针数组元数的个数,其它同普通的函数指针。

例如:

1
int (*pFunction[3])(int, int);

以上语句定义了一个指针数组 pFunction ,该指针数组的元素都是 int 型函数指针,可以指向返回值类型为 int 型,且有两个整型参数的函数。

6.2使用

点击查看实例
test.c
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
#include <stdio.h>

typedef int (*MyFunc)(int, char);

int func1(int a, char b);
int func2(int a, char b);

int main(int argc, char *argv[])
{
int (*p[2])(int, char); /* 定义一个可以指向带有int和char类型参数且返回值为int类型的函数指针数组 */
MyFunc pfunc[2]; /* 定义一个可以指向带有int和char类型参数且返回值为int类型的函数指针数组 */

p[0] = func1;
p[1] = func2;
printf("func1 return:%d\n", (*p[0])(5, 'a'));

printf("func1 return:%d\n\n", (*p[1])(6, 'b'));

pfunc[0] = func1;
pfunc[1] = func2;
printf("func1 return:%d\n", (*pfunc[0])(7, 'c'));
printf("func1 return:%d\n", (*pfunc[1])(8, 'd'));
return 0;
}

int func1(int a, char b)
{
printf("func1:a = %d, b = %c\n", a, b);
return 1;
}

int func2(int a, char b)
{
printf("func2:a = %d, b = %c\n", a, b);
return 2;
}

在终端执行以下命令编译程序:

1
2
gcc test.c -Wall # 生成可执行文件 a.out 
./a.out # 执行可执行程序

然后,终端会有以下信息显示:

1
2
3
4
5
6
7
8
9
func1:a = 5, b = a
func1 return:1
func2:a = 6, b = b
func1 return:2

func1:a = 7, b = c
func1 return:1
func2:a = 8, b = d
func1 return:2

六、指针定义总结

1. int p;

这是一个普通的整型变量p,这个语句的含义就是定义了一个整型变量 p 。

2. int *p;

  • 首先从 p 处开始,先与 * 结合,所以说明 p 是一个指针;
  • 然后再与 int 结合, 说明指针所指向的内容的类型为 int 型;

所以 p 是一个可以指向整型数据的指针变量,这个语句的含义就是定义了一个指向整型数据的指针变量 p。

3. int **p;

  • 首先从 p 开始, 先与 * 结合, 说是 p 是一个指针;
  • 然后再与 * 结合, 说明指针所指向的元素是指针;
  • 然后再与 int 结合, 说明该指针所指向的元素是整型数据。

所以, p 是一个二级指针,指向整型数据,p中可以存放一个地址,这个地址对应的内存区域是一个整数,这个语句的含义就是定义了一个可以指向整型数据的二级指针变量 p 。

4. int p[3];

  • 首先从 p 处开始,先与 [] 结合,说明 p 是一个数组;
  • 然后与 int 结合, 说明数组里的元素是整型的。

所以 p 是一个由整型数据组成的数组,这个语句的含义就是定义了一个含有 3 个整型数据的数组 p 。

5. int *p[3];

  • 首先从 p 处开始,先与 [] 结合,因为其优先级比 * 高,所以 p 是一个数组;
  • 然后再与 * 结合,说明数组里的元素是指针类型;
  • 然后再与 int 结合,说明指针所指向的内容的类型是整型的。

所以 p 是一个由指向整型数据的指针所组成的数组,这个语句的含义就是,定义了一个含有 3 个元素的数组 p ,数组中每个元素都是一个指向整型数据的指针。

6. int (*p)[3];

  • 首先从 p 处开始,先与 * 结合,说明 p 是一个指针;
  • 然后再与 [] 结合(与”()”这步可以忽略,只是为了改变优先级), 说明指针所指向的内容是一个数组;
  • 然后再与 int 结合, 说明数组里的元素是整型的。

所以 p 是一个指向由整型数据组成的数组的指针,这个语句的含义就是,定义了一个指针变量 p ,它可以指向一个含有 3 个 int 类型元素的数组。

7. int p(int);

  • 首先,从 p 处起,先与 () 结合, 说明 p 是一个函数;
  • 然后进入 () 里分析,说明该函数有一个整型变量的参数;
  • 然后再与外面的 int 结合, 说明函数的返回值是一个整型数据

所以, p 是一个带有一个 int 形参且返回值为 int 类型的函数,这个语句的含义就是,定义了一个函数 p,这个函数有一个 int 类型的参数,返回值为 int 类型。

8. int *p(int);

  • 首先,从 p 处起,先与 () 结合,说明 p 是一个函数;
  • 然后进入 () 里分析,说明该函数有一个整型变量的参数;
  • 然后再与外面的 int *结合, 说明函数的返回值是一个整型指针

所以, p 是一个带有一个 int 形参且返回值为 int *类型的函数,这个语句的含义就是,定义了一个指针函数 p,这个函数有一个 int 类型的参数,返回值为 int *类型。

9. int (*p)(int);

  • 首先,从 p 处开始, 先与指针结合,说明 p 是一个指针;
  • 然后与 () 结合,说明指针指向的是一个函数;
  • 然后再与 () 里的 int 结合, 说明函数有一个 int 型的参数;
  • 再与最外层的 int 结合, 说明函数的返回类型是整型。

所以 p 是一个指向有一个整型参数且返回类型为整型的函数的指针,这个语句的含义就是,定义了一个函数指针 p,这个函数指针指向一个函数,这个函数有一个 int 类型参数,且返回值为 int 类型。

10. int (*p[3])(int, int);

  • 首先从 p 处开始, 先与 [] 结合,因为其优先级比 * 高,所以 p 是一个数组;
  • 然后再与 * 结合, 说明数组里的元素是指针类型;
  • 然后与 () 结合, 说明指针指向的是一个函数;
  • 然后再与 () 里的 int 结合, 说明函数有两个 int 型的参数;
  • 再与最外层的 int 结合, 说明函数的返回类型是整型。

所以 p 是一个指向有两个整型参数且返回类型为整型的函数的指针数组,这个语句的含义就是,定义了一个有 3 个元素的数组 p ,数组中每个元素都是指针,指针指向含有两个 int 类型参数,且返回值为 int 类型的函数。

11. int *(*p(int))[3];

  • 首先,从 p 开始,先与 () 结合, 说明 p 是一个函数;
  • 然后进入 () 里面,与 int 结合,说明函数有一个整型变量参数;
  • 然后再与外面的 * 结合, 说明函数返回的是一个指针;
  • 然后到最外面一层, 先与 [] 结合, 说明返回的指针指向的是一个数组,;
  • 然后再与 * 结合, 说明数组里的元素是指针;
  • 然后再与 int 结合, 说明指针指向的内容是整型数据。

所以 p 是一个参数为一个整型数据且返回一个指向由整型指针变量组成的数组的指针变量的函数