当前位置: 首页 > backend >正文

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.能够直接对内存进行操作,在动态内存分配时会用到,像mallocfree函数的使用就离不开指针。

        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函数的形参ab是指针,接收的是实参xy的地址,通过解引用操作就可以修改实参的值。


3. 指针运算

(1)步长

指针移动一次的字节个数——→和数据类型有关

(2)有意义的操作

  • 指针跟整数进行加减操作(每次移动N个步长)
  • 指针跟指针进行减操作(间隔步长)

(3)无意义的操作

  • 指针跟整数进行乘除操作(此时指针指向不明)
  • 指针跟指针进行加,乘,除操作

(4)基本运算

  • 递增 / 递减运算p++p--,指针会根据所指向数据类型的大小移动相应的字节数。
  • 加减整数运算p + np - 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 = &num;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)总结

  1. 行优先遍历是 C 语言中遍历二维数组的首选方式,因为它符合数组在内存中的存储顺序,能充分利用缓存机制,提高访问效率。

  2. 列优先遍历虽然在内存访问上不连续,但在某些算法(如矩阵转置、按列求和)中是必要的。

  3. 无论使用哪种遍历方式,都要注意循环变量的嵌套顺序:行优先是外层行、内层列,列优先是外层列、内层行


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)总结

  1. 行指针遍历通过指向一维数组的指针按行访问二维数组,保持了行列结构的直观性,适合需要逐行处理的场景。

  2. 列指针遍历通过普通指针直接遍历内存中的所有元素,效率更高(减少了指针运算),适合需要连续处理所有元素的场景。

  3. 无论使用哪种方式,都要注意指针类型与数组维度的匹配,避免越界访问。


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*的数组。
  • 括号的重要性  :括号改变优先级,决定是指针还是数组。

(5)对比总结

http://www.xdnf.cn/news/5302.html

相关文章:

  • Day 3:Warp协作功能深度实战
  • 运放OP方向技术要点和大厂题目解析
  • 文件IO之系统IO
  • dockerfile编写入门
  • 十六、统一建模语言 UML
  • 16前端项目----交易页
  • QT6 源(90):阅读与注释 LCD显示类 QLCDNumber ,源代码以及属性测试。该类继承于容器框架 QFrame
  • Windows报错:OSError: [WinError 1455] 页面文件太小,无法完成操作的问题
  • Redis能保证数据不丢失吗之RDB
  • 【Web】使用Vue3开发鸿蒙的HelloWorld!
  • 模拟太阳系(C#编写的maui跨平台项目源码)
  • Autoware message_filters::Synchronizer链接错误问题
  • Axure疑难杂症:统计分析页面引入Echarts示例动态效果
  • 目录粘滞位的使用
  • JDBC链接数据库
  • 【typenum】 0 配置文件(Cargo.toml)
  • 【MySQL】事务(重点)
  • 酒店洗护用品那些事儿:品牌选择及扬州卓韵用品介绍
  • 6. 存储池配置与CephFS创建 ceph version 14.2.22
  • muduo源码解析
  • 【第33节 数据库基础概念】
  • 游戏引擎学习第269天:清理菜单绘制
  • [模型选择与调优]机器学习-part4
  • PyTorch API 6 - 编译、fft、fx、函数转换、调试、符号追踪
  • HTTP 请求中 Content-Type 头部
  • 使用谱聚类将相似度矩阵分为2类
  • 2025年RAG技术有哪些创新点?
  • 海市蜃楼的形成原理
  • M0的基础篇之PWM学习
  • adb命令查询不到设备?