C PRIMER PLUS——第7节:指针
目录
1. 指针的概念
(1)定义
(2)格式
(3)作用
2.指针形参和实参
3. 指针运算
(1)步长
(2)有意义的操作
(3)无意义的操作
(4)基本运算
4.野指针和悬空指针
5.void 类型指针
6.二级指针和多级指针
7.数组指针
8.利用索引遍历的二种格式的二维数组
(1)行优先遍历(Row-Major Order Traversal)
(2)列优先遍历(Column-Major Order Traversal)
(3)两种遍历方式的对比
(4)总结
9.利用指针遍历的二种格式的二维数组
(1)行指针遍历(Row Pointer Traversal)
(2)列指针遍历(Column Pointer Traversal)
(3)两种指针遍历方式的对比
(4)总结
10.数组指针,指针数组和函数指针
(1)数组指针
①定义
②格式
③示例
④内存分析
(2)指针数组
①定义
②格式
③示例
④内存分析
(3)函数指针
①定义
②格式
③示例
④内存分析
(4)易混淆点
(5)对比总结
1. 指针的概念
(1)定义
指针属于一种特殊的变量,其存储的数值是内存地址,并非普通的数据。借助指针,能够对内存进行直接操作,还能高效地处理数组和函数。指针的变量=存储着内存地址的变量
(2)格式
type *pointer_name;// 数据类型 * 变量名
type
代表指针所指向变量的数据类型(要跟指向变量的类型保持一致),*
是指针声明符,pointer_name
是指针变量的名称(自己起的名字)。
例如:
int *p; // 该指针指向int类型的数据
char *str; // 此指针指向char类型的数据(可用于表示字符串)
(3)作用
1.能够直接对内存进行操作,在动态内存分配时会用到,像malloc
、free
函数的使用就离不开指针。
2.可以高效地传递参数,避免数据的复制,减少内存开销。
3.能操作数组,实现数组元素的快速访问。
4.支持函数指针,可用于实现回调函数和动态调用函数。
5.可操作字符串,实现字符串的高效处理。
6.查询数据:利用*可查询数据,存储数据
int a = 10;
int * p = &a;
printf ( " %d\n ", *p ); //10
7. 操作其他函数中的变量
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void swap(int* p1, int* p2);
int main()
{int a = 10;int b = 20;printf("调用前:%d,%d\n", a, b);// 调用前:10, 20swap(&a, &b);printf("调用后:%d,%d\n", a, b);// 调用后:20, 10return 0;
}
void swap(int* p1, int* p2)
{int temp = *p1;*p1 = *p2;*p2 = temp;
}
8.函数返回多个值
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void getmaxandmin(int arr[], int len, int* max, int* min);
int main()
{int arr[] = { 1,2,3,4,5,6,7,8,9,10 };int len = sizeof(arr) / sizeof(int);//调用getmaxandmin函数求最大最小值int max = arr[0];int min = arr[0];getmaxandmin(arr, len, &max, &min);printf("数组中最大值为:%d\n", max);// 数组中最大值为:10printf("数组中最小值为:%d\n", min);// 数组中最小值为:1return 0;
}
void getmaxandmin(int arr[], int len, int* max, int* min)
{//求数组最大值*max = arr[0];for (int i = 1; i < len; i++){if (arr[i] > *max){*max = arr[i];}}
//求数组最小值*min = arr[0];for (int i = 1; i < len; i++){if (arr[i] < *min){*min = arr[i];}}
}
9.函数的结果和计算状态分开
#define _CRT_SECURE_NO_WARNINGS
//定义一个函数,将两数相除,获取他们余数
int getremainder(int num1, int num2, int* res);
#include <stdio.h>
int main()
{int a = 10;int b = 3;int res = 0;//调用函数获取余数int flag = getremainder(a, b, &res);//对状态进行判断if (!flag){printf("获取到的余数为:%d\n", res);// 获取到的余数为:1}return 0;
}
//返回值:表示计算状态:0正常,1不正常
int getremainder(int num1, int num2, int* res)
{if (num2 == 0){//停止return 1;}*res = num1 % num2;return 0;
}
2.指针形参和实参
在 C 语言中,函数参数的传递方式是值传递,当传递指针时,传递的是内存地址的值。
void swap(int* a, int* b)
{int temp = *a;*a = *b;*b = temp;
}
int main()
{int x = 10, y = 20;swap(&x, &y); // 传递变量x和y的地址return 0;
}
在这个例子中,swap
函数的形参a
和b
是指针,接收的是实参x
和y
的地址,通过解引用操作就可以修改实参的值。
3. 指针运算
(1)步长
指针移动一次的字节个数——→和数据类型有关
(2)有意义的操作
- 指针跟整数进行加减操作(每次移动N个步长)
- 指针跟指针进行减操作(间隔步长)
(3)无意义的操作
- 指针跟整数进行乘除操作(此时指针指向不明)
- 指针跟指针进行加,乘,除操作
(4)基本运算
- 递增 / 递减运算:
p++
、p--
,指针会根据所指向数据类型的大小移动相应的字节数。 - 加减整数运算:
p + n
、p - n
,指针会向前或向后移动n
个元素的位置。 - 指针相减:
p1 - p2
,得到的结果是两个指针之间的元素个数(前提是这两个指针指向同一个数组)。 - 比较运算:
==
、!=
、<
、>
等,可用于判断指针是否指向同一个位置或者判断指针的位置关系。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{int arr[] = { 1,2,3,4,5,6,7,8,9,10 };//获取0索引的内存地址int* p1 = &arr[0]; //通过内存地址获取数据printf("%d\n", *p1); //1printf("%d\n", *(p1+1)); //2//获取5索引的内存地址int* p2 = &arr[5];//p2-p1间隔了多少步长printf("%d\n", p2 - p1); //5printf("%p\n", p1); //000000209FAFF658printf("%p\n", p2); //000000209FAFF66Creturn 0;
}
4.野指针和悬空指针
- 野指针:指的是未被初始化的指针,其指向的是随机的内存地址。例如:
int *p; // 未初始化
*p = 10; // 错误,这是野指针操作
- 悬空指针:指针原本指向的内存已经被释放,但指针仍然指向该地址。例如:
int *p = (int *)malloc(sizeof(int));
free(p);
*p = 10; // 错误,这是悬空指针操作
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int* method();
int main()
{int a = 10;int* p1 = &a;printf("%p\n", p1); //000000EE32AFF5D4printf("%d\n", *p1);//10//野指针int* p2 = p1 + 10;printf("%p\n", p2); //000000EE32AFF5FCprintf("%d\n", *p2);//238//悬空指针int* p3 = method();printf("拖点时间\n");printf("%p\n", p3); //000000EE32AFF494printf("%d\n", *p3);//17return 0;
}
int* method()
{int num = 10;int* p = #return p;
}
5.void 类型指针
void*
是一种通用指针类型,可以指向任意类型的数据,但在使用时需要进行显式类型转换。void*
常用于函数参数和返回值,像malloc
函数的返回值就是void*
类型。例如:
void *p;
int x = 10;
p = &x; // 无需类型转换
int *q = (int*)p; // 需要显式类型转换
特点:无法获取数据,无法计算,但可接受任意地址。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void swap(void* p1, void* p2, int len);
int main()
{int a = 10;short b = 20;int* p1 = &a;short* p2 = &b;printf("%d\n", *p1); //10printf("%d\n", *p2); //20
/* 不同类型的指针不同,是不能互相赋值的void类型的指针打破了上面观点void没有任何类型,好处可以接受任意类型指针记录的内存地址缺点:void类型的指针,无法获取变量的数据,也不能进行加减计算 */void* p3 = p1;void* p4 = p2;//调用函数交换数据int c = 100;int d = 200;swap(&c, &d, 4);printf("c= %d,d= %d\n", c, d);// c = 200, d = 100return 0;
}
//函数:用来交换两个变量记录的数据,修改一下更具有通用性
void swap(void* p1, void* p2, int len)
{//把void类型的指针转成char类型指针char* pc1 = p1;char* pc2 = p2;char temp = 0;//以字节为单位,一个字母一个字节进行交换for (int i = 0; i < len; i++){temp = *pc1;*pc1 = *pc2;*pc2 = temp;pc1++;pc2++;}
}
6.二级指针和多级指针
- 二级指针:指的是指向指针的指针,其定义格式为
type **pointer
。例如:
int x = 10;
int *p = &x;
int **pp = &p; // pp是二级指针,指向指针p
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{int a = 10;int b = 20;int* p = &a;int** pp = &p;//作用1:利用二级指针修改一级指针里面记录的内存地址*pp = &b;//作用2:利用二级指针获取到变量中记录的数据printf("%p\n", &a); //0000009A2031F524printf("%p\n", &b); //0000009A2031F544printf("%p\n", p); //0000009A2031F544printf("%d\n", **pp); //20
}
- 多级指针:以此类推,还可以有三级指针、四级指针等,不过在实际编程中很少会用到超过二级的指针。
7.数组指针
(1)概念:数组指针是指向数组的指针,其定义格式为type (*pointer)[size]
。例如:
int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &arr; // p是指向包含5个int元素的数组的指针
(2)作用:方便的操作数组中各种数据
(3)arr参与计算的时候,会退化为第一个元素的指针,记录的内存地址第一个元素首地址也是数组的首地址,步长:数据类型 int 4
- sizeof运算时,不会退化,arr还是整体。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{//定义数组int arr[] = { 1,2,3,4,5,6,7,8,9,10 };//sizeof运算时,不会退化,arr还是整体printf("%zu\n", sizeof(arr)); //40printf("%p\n", arr); //0000006404AFF888printf("%p\n", &arr); //0000006404AFF888printf("%p\n", arr+1); //0000006404AFF88Cprintf("%p\n", &arr + 1); //0000006404AFF8B0
}
- &arr获取地址时,不会退化,记录的内存地址第一个元素的首地址,也是数组首地址,步长:数据类型 * 数组长度
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{//定义数组int arr[] = { 1,2,3,4,5,6,7,8,9,10 };int len = sizeof(arr) / sizeof(int);//获取数组指针,实际上获取的是数组首地址int* p1 = arr;//利用循环和指针遍历数组获取里面每一个元素for (int i = 0; i < len; i++){printf("%d,", *p1++);//1,2,3,4,5,6,7,8,9,10,}
}
8.利用索引遍历的二种格式的二维数组
在 C 语言中,遍历二维数组主要有两种方式:行优先遍历和列优先遍历。这两种方式的选择取决于数组在内存中的存储方式(C 语言采用行优先存储)以及具体的应用场景。下面详细介绍这两种遍历方式的定义、格式和示例。
(1)行优先遍历(Row-Major Order Traversal)
定义:行优先遍历按照二维数组在内存中的存储顺序依次访问元素。C 语言中,二维数组是按行连续存储的,即先存储第一行的所有元素,再存储第二行,依此类推。因此,行优先遍历是最自然的遍历方式,效率也更高。
格式:
for (int i = 0; i < rows; i++) { // 外层循环控制行
for (int j = 0; j < cols; j++) { // 内层循环控制列
// 访问 array[i][j]
}
}
- 外层循环:遍历每一行(
i
表示行索引)。 - 内层循环:遍历当前行的每一列(
j
表示列索引)。
#include <stdio.h>int main() {int array[3][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};int rows = 3;int cols = 4;// 行优先遍历printf("行优先遍历结果:\n");for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {printf("%d ", array[i][j]);}printf("\n"); // 换行以显示每行元素}return 0;
}
行优先遍历结果:
1 2 3 4
5 6 7 8
9 10 11 12
(2)列优先遍历(Column-Major Order Traversal)
定义:列优先遍历是按列的顺序访问二维数组的元素,即先访问第一列的所有元素,再访问第二列,依此类推。虽然在 C 语言中二维数组在内存中是按行存储的,但列优先遍历在某些算法(如矩阵转置)中非常有用。
格式:
for (int j = 0; j < cols; j++) { // 外层循环控制列
for (int i = 0; i < rows; i++) { // 内层循环控制行
// 访问 array[i][j]
}
}
- 外层循环:遍历每一列(
j
表示列索引)。 - 内层循环:遍历当前列的每一行(
i
表示行索引)。
#include <stdio.h>int main() {int array[3][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};int rows = 3;int cols = 4;// 列优先遍历printf("列优先遍历结果:\n");for (int j = 0; j < cols; j++) {for (int i = 0; i < rows; i++) {printf("%d ", array[i][j]);}printf("\n"); // 换行以显示每列元素}return 0;
}
列优先遍历结果:
1 5 9
2 6 10
3 7 11
4 8 12
(3)两种遍历方式的对比
(4)总结
-
行优先遍历是 C 语言中遍历二维数组的首选方式,因为它符合数组在内存中的存储顺序,能充分利用缓存机制,提高访问效率。
-
列优先遍历虽然在内存访问上不连续,但在某些算法(如矩阵转置、按列求和)中是必要的。
-
无论使用哪种遍历方式,都要注意循环变量的嵌套顺序:行优先是外层行、内层列,列优先是外层列、内层行。
9.利用指针遍历的二种格式的二维数组
在 C 语言中,利用指针遍历二维数组有两种主要方式:行指针遍历和列指针遍历。这两种方式基于不同的指针类型和内存访问模式,下面详细讲解它们的定义、格式和示例。
(1)行指针遍历(Row Pointer Traversal)
定义:行指针遍历使用指向一维数组的指针(即行指针)来遍历二维数组。通过行指针可以按行访问二维数组,每次移动一行的位置。这种方式符合 C 语言中二维数组的内存布局(行优先存储),效率较高。
格式:
type (*row_ptr)[cols] = array; // 定义行指针,指向包含cols个元素的一维数组
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
// 通过行指针访问元素:*(*(row_ptr + i) + j) 或 row_ptr[i][j]
}
}
-
row_ptr
是指向一维数组的指针,每次移动sizeof(type) * cols
字节(即一行的大小)。 -
*(row_ptr + i)
等价于row_ptr[i]
,表示第i
行的首地址。 -
*(*(row_ptr + i) + j)
等价于row_ptr[i][j]
,表示第i
行第j
列的元素。
#include <stdio.h>int main() {int array[3][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};int rows = 3;int cols = 4;// 行指针遍历int (*row_ptr)[4] = array; // 指向包含4个int的一维数组printf("行指针遍历结果:\n");for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {printf("%d ", *(*(row_ptr + i) + j)); // 等价于 row_ptr[i][j]}printf("\n");}return 0;
}
行指针遍历结果:
1 2 3 4
5 6 7 8
9 10 11 12
(2)列指针遍历(Column Pointer Traversal)
定义:列指针遍历使用普通指针(即列指针)直接遍历二维数组的所有元素,将二维数组视为一维连续内存块。这种方式跳过了行的概念,直接按内存顺序访问每个元素,适用于需要连续处理所有元素的场景。
格式:
type *col_ptr = &array[0][0]; // 定义列指针,指向数组首元素
for (int i = 0; i < rows * cols; i++) {
// 通过列指针访问元素:*(col_ptr + i)
}
col_ptr
是普通指针,每次移动sizeof(type)
字节(即一个元素的大小)。*(col_ptr + i)
表示数组的第i
个元素(按内存顺序)。
#include <stdio.h>int main() {int array[3][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};int rows = 3;int cols = 4;// 列指针遍历int *col_ptr = &array[0][0]; // 指向首元素printf("列指针遍历结果:\n");for (int i = 0; i < rows * cols; i++) {printf("%d ", *(col_ptr + i)); // 等价于 array[i/cols][i%cols]if ((i + 1) % cols == 0) {printf("\n"); // 每行结束后换行}}return 0;
}
列指针遍历结果:
1 2 3 4
5 6 7 8
9 10 11 12
(3)两种指针遍历方式的对比
(4)总结
-
行指针遍历通过指向一维数组的指针按行访问二维数组,保持了行列结构的直观性,适合需要逐行处理的场景。
-
列指针遍历通过普通指针直接遍历内存中的所有元素,效率更高(减少了指针运算),适合需要连续处理所有元素的场景。
-
无论使用哪种方式,都要注意指针类型与数组维度的匹配,避免越界访问。
10.数组指针,指针数组和函数指针
(1)数组指针
①定义
数组指针是指向整个数组的指针,它保存的是数组的起始地址,但类型是指向数组的指针。通过数组指针可以按数组维度访问内存,常用于处理多维数组。
②格式
type (*ptr)[size]; // ptr是指向包含size个type元素的数组的指针
-
type
:数组元素的类型。 -
size
:数组的长度。 -
(*ptr)
:括号确保ptr
先与*
结合,成为指针,再指向数组。
③示例
#include <stdio.h>
int main() {int arr[3][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};// 定义数组指针,指向包含4个int的一维数组int (*ptr)[4] = arr; // ptr指向arr的第0行// 通过数组指针访问元素printf("%d\n", *(*(ptr + 1) + 2)); // 输出7(第1行第2列)printf("%d\n", ptr[2][3]); // 输出12(第2行第3列)return 0;
}
④内存分析
ptr
指向整个一维数组arr[0]
(长度为 4)。ptr + 1
移动4 * sizeof(int)
字节,指向下一行。*(ptr + 1)
解引用得到第 1 行的首地址,类型为int*
。*(*(ptr + 1) + 2)
等价于ptr[1][2]
。
(2)指针数组
①定义
指针数组是一个数组,其元素都是指针。每个元素可以指向不同的内存地址,常用于存储多个字符串或动态分配的内存块。
②格式
type *arr[size]; // arr是包含size个type*指针的数组
type
:指针指向的数据类型。arr[size]
:数组长度为size
,每个元素是type*
类型。
③示例
#include <stdio.h>
int main() {// 指针数组:每个元素是char*,指向字符串常量char *fruits[3] = {"Apple","Banana","Cherry"};// 访问指针数组for (int i = 0; i < 3; i++) {printf("%s\n", fruits[i]); // fruits[i]是char*,指向字符串首字符}return 0;
}
④内存分析
-
fruits
是长度为 3 的数组,每个元素是char*
类型。 -
fruits[0]
指向字符串"Apple"
的首字符'A'
。 -
字符串常量存储在只读内存区,指针数组保存它们的地址。
(3)函数指针
①定义
函数指针是指向函数的指针,它保存的是函数的入口地址。通过函数指针可以动态调用不同的函数,常用于实现回调函数、函数表或动态链接。
②格式
return_type (*ptr)(parameter_types); // ptr是指向函数的指针
return_type
:函数的返回类型。(parameter_types)
:函数的参数类型列表。(*ptr)
:括号确保ptr
先与*
结合,成为指针,再指向函数。
③示例
#include <stdio.h>
// 两个函数,具有相同的参数和返回类型
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int main() {// 定义函数指针,指向返回int、接受两个int参数的函数int (*op)(int, int); // 让函数指针指向add函数op = add;printf("3 + 2 = %d\n", op(3, 2)); // 输出5// 让函数指针指向sub函数op = sub;printf("3 - 2 = %d\n", op(3, 2)); // 输出1return 0;
}
④内存分析
-
op
保存的是函数的入口地址(代码段中的位置)。 -
op = add
将add
函数的地址赋给op
。 -
op(3, 2)
等价于调用add(3, 2)
或sub(3, 2)
。
(4)易混淆点
int (*ptr)[4]
:数组指针,ptr
指向包含 4 个int
的数组。int *ptr[4]
:指针数组,ptr
是包含 4 个int*
的数组。- 括号的重要性 :括号改变优先级,决定是指针还是数组。