数组和指针

指针

什么是指针?

基本概念

指针就是内存地址。指针变量是存储内存地址的变量。

想象计算机内存就像一个大酒店:

  • 房间号 = 内存地址(指针)
  • 房间里的客人 = 存储在内存中的数据
  • 房间号登记簿 = 指针变量

指针变量的定义

1
2
3
4
5
6
7
8
9
10
11
12
类型 *指针变量名;

#include <stdio.h>

int main() {
// 定义不同类型的指针
int *p_i; // 指向整型的指针,变量名为p_i
char *p_c; // 指向字符的指针,变量名为p_c
float *p_f; // 指向浮点数的指针,变量名为p_f

return 0;
}

指针变量的引用

取址运算符 &

取址运算符&用来取得其操作数的地址。

1
2
3
4
5
6
7
8
9
10
//例如,如果 num 是一个整型变量,则 &num 是它的地址
int main() {
int num = 3;
int* p = &num; // 将变量num的地址取出来,存到指针p中
printf("%d 的地址是 %p\n", num, p);
return 0;
}

//输出:3 的地址是 0x7ffeefbff3ac

解引用运算符 *

解引用运算符(间接访问运算符)*用来获取指针所指向地址的值,它返回指针所指的对象的值

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

int main() {
// 1. 定义一个普通变量
int num = 42;

// 2. 定义一个指针,指向num的地址
int *p = &num;

// 3. 使用解引用运算符 * 获取指针指向的值
int value = *p; // *p 就是 num 的值 42

printf("num = %d\n", num); // 输出: 42
printf("*p = %d\n", *p); // 输出: 42
printf("value = %d\n", value); // 输出: 42

return 0;
}

补充说明

  1. & 和 * 的关系
1
2
3
对于任意变量 a:
1. *(&a) == a // & 和 * 相互抵消
2. &(*p) == p // * 和 & 相互抵消(p 是指向有效内存的指针)
  1. 优先级和结合性
  • &*都是单目运算符(仅对一个操作数进行运算的运算符类型)
  • 优先级:2级(较高)
  • 结合性:自右向左
  • &*p 等价于 &(*p)
  • *&a 等价于 *(&a)
  1. (p_a)++与a++等价(注意,括号是必需的)。
    这是因为
    和+的优先级别相同,都是自右向左结合,若没有括号,就相当于*(p_a++)其执行过程是先取指针变量p_a所指向的值,再使指针变量p_a的值加1(指向下一个存储单元),而不是使所指向的变量a的值加1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

int main() {
// 示例1: (*p)++
int a = 10;
int *p = &a;

printf("a=%d\n", a); // 输出: a=10
(*p)++; // a的值加1,p不变
printf("a=%d\n", a); // 输出: a=11

// 示例2: *p++
int arr[] = {100, 200, 300};
int *q = arr;

printf("\n*q=%d\n", *q); // 输出: *q=100
int value = *q++; // 先取arr[0]的值,然后q指向arr[1]
printf("value=%d\n", value); // 输出: value=100
printf("*q=%d\n", *q); // 输出: *q=200

return 0;
}

数组

一维数组

一维数组就是一排整齐的盒子,每个盒子可以放同样类型的东西,每个盒子都有编号(从0开始)。

一维数组的定义与初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
数据类型 数组名[大小];

// 方法1:声明时初始化
int numbers[5] = {1, 2, 3, 4, 5};

// 方法2:部分初始化(剩余元素自动初始化为0)
int nums[5] = {1, 2, 3}; // nums[3]和nums[4]自动为0

// 方法3:不指定大小,编译器自动计算
int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

// 方法4:全部初始化为0
int zeros[10] = {0};

一维数组的引用

1
数组名[下标]

下标引用法

1
2
3
4
5
6
7
8
9
int arr[5] = {10, 20, 30, 40, 50};

// 读取元素
int a = arr[0]; // a = 10
int b = arr[2]; // b = 30

// 修改元素
arr[1] = 25; // 第二个元素变为25
arr[3] = arr[2] * 2; // 第四个元素 = 30 * 2 = 60

指针引用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int arr[5] = {1, 2, 3, 4, 5};

// 方法1:数组名是指向首元素的指针
int *p = arr; // p指向arr[0]
int x = *p; // x = 1
int y = *(p + 2); // y = 3

// 方法2:直接使用数组名
int z = *(arr + 3); // z = 4

// 方法3:指针自增
int *p = arr;
printf("%d ", *p); // 1
p++; // 指向下一个元素
printf("%d ", *p); // 2

注:arr 本身就是一个指针,可以直接赋值给指针变量 parr 是数组第 0 个元素的地址,所以int *p = arr;也可以写作int *p = &arr[0];。也就是说,arrp&arr[0] 这三种写法都是等价的,它们都指向数组第 0 个元素,或者说指向数组的开头。

指针法示例:

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
#include <stdio.h>

int main() {
int i; // 循环变量
int a[10]; // 定义长度为10的整型数组,存储10个整数
int *p = a; // 指针p初始指向数组a的首地址(a是数组名,等价于&a[0])

// 循环输入10个整数到数组a中
for (i = 0; i < 10; i++) {
/*
* scanf("%d", p++) 原理拆解:
* 1. scanf的第二个参数要求是「内存地址」,用于接收输入的整数;
* 2. p++ 是「后置自增运算符」,执行规则为「先使用,后自增」:
* - 第一步(使用):将p当前指向的地址(如第一次循环是&a[0])传给scanf,
* scanf拿到地址后,把用户输入的整数写入该地址对应的内存(即a[0]);
* - 第二步(自增):scanf完成地址传递和数据写入后,p自动自增(p = p + 1),
* 由于p是int*类型,「+1」实际是移动4字节(int占4字节),刚好指向数组下一个元素(如a[1]);
* 3. 循环10次后,p会从&a[0]逐步移动到数组外的地址(&a[10]),因此后续输出前必须重置p。
*/
scanf("%d", p++);
}
p = a; // 重置指针p到数组首地址!若不重置,p当前指向数组外,输出会越界
// 循环输出数组的10个元素
for (i = 0; i < 10; i++) {
// *p++ :先取p当前指向的元素值(如a[0])输出,再让p自增指向下一个元素
printf("%d ", *p++);
}

return 0;
}

重要指针运算表达式

假设 p 是指向数组 arr 中第 n 个元素的指针,那么 *p++*++p(*p)++ 分别是什么意思呢?

  • *p++ 等价于 *(p++),表示先取得第 n 个元素的值,再将 p 指向下一个元素。

  • *++p 等价于 *(++p),会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会获得第 n+1 个数组元素的值。

  • (*p)++ 就非常简单了,会先取得第 n 个元素的值,再对该元素的值加 1。假设 p 指向第 0 个元素,并且第 0 个元素的值为 99,执行完该语句后,第 0 个元素的值就会变为 100

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
示例:

#include <stdio.h>

int main() {
int arr[] = {1, 2, 3, 4, 5};
int *p;

// *p++ - 先取值后指针+1
p = &arr[2];
int a = *p++; // a = 3, p指向arr[3]

// *++p - 先指针+1后取值
p = &arr[2];
int b = *++p; // b = 4, p指向arr[3]

// (*p)++ - 先取值后元素值+1
p = &arr[2];
int c = (*p)++; // c = 3, arr[2]变为4

// ++(*p) - 元素值+1后取值
p = &arr[2];
int d = ++(*p); // d = 5, arr[2]变为5

return 0;
}

二维数组

二维数组的定义与初始化

二维数组是一个数组的数组。一个二维数组,在本质上是由多个一维数组构成。

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
数据类型 数组名[行数][列数];

#include <stdio.h>

int main() {
// 1. 分行初始化(推荐)
int matrix1[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};

// 2. 连续初始化
int matrix2[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};

// 3. 部分初始化(未指定的自动为0)
int matrix3[3][4] = {
{1, 2}, // 第0行:1, 2, 0, 0
{5, 6, 7}, // 第1行:5, 6, 7, 0
{9} // 第2行:9, 0, 0, 0
};

// 4. 省略行数(编译器自动计算)
int matrix4[][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
}; // 自动为3行

// 5. 全零初始化
int matrix5[3][4] = {0}; // 所有元素为0

return 0;
}

二维数组的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
数组名[行下标][列下标]

#include <stdio.h>
#define N 3 // 定义行数
#define M 4 // 定义列数
int main()
{
int a[N][M], i, j; // 定义N行M列的二维数组
printf("Input 3*4 data for the array:\n");
for(i=0; i<N; i++) // 外循环:控制行
for(j=0; j<M; j++) // 内循环:控制列
scanf("%d", &a[i][j]); // 输入每个元素
printf("Output the array:\n");
for(i=0; i<N; i++)
{
for(j=0; j<M; j++)
printf("%5d", a[i][j]); // 输出每个元素(占5个字符宽度)
printf("\n"); // 每行结束后换行
}
return 0;
}
1
2
3
4
5
输入1 2 3 4 5 6 7 8 9 10 11 12后,输出为:
Output the array:
1 2 3 4
5 6 7 8
9 10 11 12

二维数组的指针

在 C 语言中,二维数组本质是 “数组的数组”—— 它在内存中连续存储。二维数组的地址分两种类型:行地址(指向整行) 和 列地址(指向单个元素)。

列指针

列指针本质是指向 “单个元素” 的普通指针,对应二维数组的 “列地址”,利用二维数组在内存中 “连续存储” 的特性,可将二维数组当作 “超长一维数组” 遍历。
什么是列指针?
列指针是指向数组单个元素的普通指针(比如int *p),对应二维数组的 “列地址”:

  • 它指向的是二维数组中的某一个具体元素;
  • 列指针+1会直接跳转到当前行的下一个元素(仅偏移 1 个元素,而非整行)。

列指针的定义与用法

列指针的定义是普通指针的写法,无需额外语法,直接指向二维数组的首元素即可。示例:

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 main()
{
// 定义3行4列的二维数组并初始化
int a[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
// 定义列指针p,指向数组首元素(第0行第0列)
int *p = &a[0][0];

printf("二维数组的元素输出:\n");
// 遍历所有元素(3行×4列=12个)
for(int i = 0; i < 12; i++)
{
// 输出当前元素,再将指针移动到下一个元素
printf("%5d", *p++);
// 每输出4个元素(对应一行)后换行
if((i + 1) % 4 == 0)
{
printf("\n");
}
}

return 0;
}

输出:

1
2
3
4
二维数组的元素输出:
1 2 3 4
5 6 7 8
9 10 11 12

列指针的等价访问方式
由于二维数组内存连续,列指针可通过 “一维下标” 访问任意元素(需结合列数计算位置),等价写法如下(以列数为 4 为例):

1
2
// 访问第i行第j列的元素
*(p + i*4 + j) == p[i*4 + j] == a[i][j]

行指针

行指针对应二维数组的 “行地址”,能直接按 “行 - 列” 结构访问数组,比列指针更贴合二维数组的逻辑结构。
什么是行指针?
行指针是指向 “固定长度一维数组” 的指针,对应二维数组的 “行地址”:

  • 它指向的是 “由 m 个元素组成的一维数组”(m 是二维数组的列数);
  • 行指针+1会直接跳过一整行(即 m 个元素),比如指向含 4 个 int 的数组时,p+1会跳过 4 个 int,直接到下一行首地址。

行指针的定义与用法

1
数据类型 (*行指针变量)[一维数组长度];

例如,定义一个指向 “含 4 个 int 的一维数组” 的行指针:

1
2
3
int (*p)[4];

//注意:*p两侧的括号不能省略—— 因为方括号[]的优先级高于*,若写成int *p[4],会被解析为 “指针数组”(含 4 个 int 指针的数组),而非行指针。

示例:

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
#include <stdio.h>

int main()
{
// 定义3行4列的二维数组并初始化
int a[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
// 定义行指针p,指向“含4个int的一维数组”
int (*p)[4];
// 行指针指向二维数组a(a是行地址,与p类型匹配)
p = a;

printf("二维数组的元素输出:\n");
// 外循环控制行
for(int i = 0; i < 3; i++)
{
// 内循环控制列
for(int j = 0; j < 4; j++)
{
// 通过行指针访问元素p[i][j]
printf("%5d", p[i][j]);
}
// 每输出一行后换行
printf("\n");
}

return 0;
}

输出:

1
2
3
4
二维数组的元素输出:
1 2 3 4
5 6 7 8
9 10 11 12

二维数组元素的等价访问方式
行指针访问二维数组元素时,存在多种等价写法(以p指向a为例),本质都是通过 “行地址 + 列偏移” 定位元素:

1
2
3
4
5
p[i][j]          // 直观的行-列形式
== *(p[i] + j) // 行指针取当前行,再偏移列下标
== *(*(p + i) + j) // 行指针偏移行下标,再偏移列下标
==
a[i][j] // 与原数组的访问形式完全等价

二维数组的地址含义(以int a[3][4]为例)

表示形式 含义 说明(地址类型/特性)
a 二维数组名;数组首地址;第0行的首地址 行地址,+1 跳转至下一行首地址(偏移4个int)
a+1 / &a[1] 第1行的首地址 行地址,+1 跳转至第2行首地址
a[0] / *(a+0) / *a / &a[0][0] 第0行首地址;第0行第0列元素的地址 列地址,+1 跳转至当前行下一个元素地址(偏移1个int)
a[1]+2 / *(a+1)+2 / &a[1][2] 第1行第2列元素a[1][2]的地址 列地址,指向具体元素的地址
*(a[1]+2) / *(*(a+1)+2) / a[1][2] 第1行第2列元素a[1][2]的值 整型值,无地址属性

字符数组和指针

字符数组是 C 语言中存储字符 / 字符串的核心工具,它既可以存储单个字符的集合,也能通过\0(字符串结束符)存储完整字符串。

字符数组的定义

1
2
3
4
5
// 定义长度为n的字符数组(n为正整数)
char 数组名[n];

// 示例:定义长度为10的字符数组
char arr[10];

字符数组初始化方式

初始化方式 示例 说明(是否含\0
逐个字符初始化(无\0 char arr1[5] = {'h','e','l','l','o'}; 纯字符数组,长度5,无\0,不能当作字符串使用
逐个字符初始化(加\0 char arr2[6] = {'h','e','l','l','o','\0'}; 字符串数组,长度6,手动加\0
字符串常量初始化(推荐) char arr3[6] = "hello"; 等价于arr2,编译器自动在末尾加\0
省略长度初始化 char arr4[] = "hello"; 长度自动为6(5个字符+1个\0),最简洁
部分初始化 char arr5[10] = "hi"; 前2位为'h''i',剩余位置自动补\0

字符数组的输入输出

输入

方式 示例 特点/注意事项
scanf(“%s”, arr) scanf("%s", arr); 不读取空格/换行,自动加\0;注意数组长度,避免溢出
gets() gets(arr); 读取空格/换行,自动加\0;存在缓冲区溢出风险
fgets() fgets(arr, 10, stdin); 读取空格/换行,指定最大读取长度;会保留换行符,需手动处理

输出

方式 示例 适用场景
单个字符输出 printf("%c", arr[i]); 逐个输出字符
整串输出 printf("%s", arr); \0的字符串数组
整串输出(puts()) puts(arr); 输出字符串并自动换行

字符串处理函数(需包含<string.h>)

函数名 功能 示例 返回值/说明
strlen() 计算字符串长度(不含\0 strlen("hello") 返回5(仅统计有效字符,不含结束符)
strcpy() 字符串拷贝 strcpy(arr, "hello") 将”hello”拷贝到arr,需保证arr长度足够
strcat() 字符串拼接 strcat(arr, " world") 将” world”拼接到arr末尾,需保证arr长度足够
strcmp() 字符串比较(按ASCII) strcmp("a", "b") a<b返回-1,a==b返回0,a>b返回1
strlwr() 大写字母转小写字母 strlwr("Abcd") 将”Abcd”转为”abcd”
strupr() 小写字母转大写字母 strupr("Abcd") 将”Abcd”转为”ABCD”

多级指针和指针数组

多级指针

多级指针的本质是指针变量,只是它指向的不是普通变量,而是另一个指针变量的地址。即指向指针的指针
示例:

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

int main() {
char *str_arr[3] = {"C语言", "指针数组", "多级指针"};
char **p = str_arr; // 二级指针指向指针数组的首地址(数组名是首元素地址)

// 遍历3个字符串
for (int i = 0; i < 3; i++) {
printf("第%d个字符串:%s\n", i+1, *p);//%s的工作逻辑:从传入的地址开始,逐字符输出直到\0 —— 它不需要 “具体字符”,只需要 “字符串的起始地址”。因此用*p
p++; // 指针后移,指向指针数组的下一个元素
}
return 0;
}

输出:

1
2
3
1个字符串:C语言
2个字符串:指针数组
3个字符串:多级指针

多级指针的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
int main() {
int x; // 定义变量x
int *p = &x; // p指向x(一级指针)
int **q = &p; // q指向p(二级指针)
int ***t = &q; // t指向q(三级指针)

// 按顺序赋值,观察x的变化
x = 10; // 第一步:x被赋值为10 → x=10
*p = 1; // 第二步:*p指向x → x被覆盖为1 → x=1
**q = 2; // 第三步:**q指向x → x被覆盖为2 → x=2
***t = 3; // 第四步:***t指向x → x被覆盖为3 → x=3

// 验证最终结果
printf("x = %d\n", x); // 输出 3(最后一次赋值的结果)
printf("*p = %d\n", *p); // 输出 3(p指向x)
printf("**q = %d\n", **q); // 输出 3(q→p→x)
printf("***t = %d\n", ***t); // 输出 3(t→q→p→x)
return 0;
}

指针数组

指针数组的本质是数组(不是指针),只是数组的每个元素都是同类型的指针变量

  • 普通数组:元素是int/char等基础类型(如int arr[5],元素是 5 个int);
  • 指针数组:元素是int*/char*等指针类型(如int *p[5],元素是 5 个int*)。

指针数组的定义与用法

1
数据类型 *数组名[数组长度];

例如:

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
#include<stdio.h>
#include<string.h>
#define N 100
int main()
{
char country[N][20], *p[N]; // country:存储字符串的二维数组;p:指针数组(每个元素指向一个字符串)
char *t; // 交换指针的临时变量
int i,j,k,n;

// 指针数组初始化:让p的每个元素指向country的对应行(即每个国家名称的首地址)
for(i=0;i<N;i++)
p[i] = country[i];

printf("Input n:");
scanf("%d", &n);
printf("Input country name:\n");
// 输入n个国家名称,存入country,同时p的元素已指向对应字符串
for(i=0;i<n;i++)
scanf("%s", p[i]);

// 选择法排序(控制轮数)
for(i=0;i<n-1;i++)
{
k = i; // 假设当前轮的第i个元素是最小的
// 查找当前轮的最小字符串对应的指针下标k
for(j=i+1;j<n;j++)
{
// strcmp(a,b):a>b返回正数,a=b返回0,a<b返回负数
if(strcmp(p[k], p[j]) > 0)
k = j;
}
// 若最小元素不是当前i位置,交换p[i]和p[k]的指针(仅交换地址,不移动字符串)
if(k!=i)
{
t = p[k];
p[k] = p[i];
p[i] = t;
}
}

// 输出排序后的字符串(通过指针数组p访问)
printf("Sorted country name:\n");
for(i=0;i<n;i++)
printf("%s ", p[i]);

return 0;
}
/* 程序核心逻辑
1.指针数组的作用:p[N]的每个元素是指针,指向country二维数组的一行(即一个国家名称的首地址)。
2.排序的高效性:选择排序时,仅交换p数组中指针的指向(地址),无需移动country中存储的字符串本身,避免了大量字符拷贝,效率更高。
3.字符串比较:通过strcmp函数按字典序比较两个字符串的大小,决定排序的交换逻辑。*/

数组和指针
https://linsport.github.io/2025/12/21/c语言编程基础/数组和指针/
作者
sport lin
发布于
2025年12月21日
许可协议