C语言指针操作

server/2025/3/6 3:35:46/

1、指针基础

1.1、指针的声明

        指针变量用于存储内存地址,声明时需要指定指向的数据类型:

/* 指针的声明 */
char* c;		/* 指向字符的指针 */
int* p;			/* 指向整型的指针 */
float* f;		/* 指向浮点数的指针 */

         指针只能指向某种特定类型的对象,即每个指针都必须指向某种特定的数据类型。例如上面示例中,“int *p;”就是说明p是一个指向整型对象的指针。但是有一个例外,指向void类型的指针可以存放指向任何类型的指针,但是它不能间接引用其自身。

        指向任何对象的指针都可以转换为void *类型,且不会丢失信息。如果将结果再转换为初始指针类型,则可以恢复初始指针。指针可以被赋值为void *类型的指针,也可以赋值给void *类型的指针,并可与void *类型的指针进行比较。

        但是要注意一个问题,上面的三个指针仅是作为声明,一般情况下不要这样写,是比较危险的,因为三个指针变量指向哪里我们无法确定。如果覆盖到其他的内存区域,甚至是系统正在使用的关键区域,十分危险,但是这种情况系统一般会驳回程序的运行,此时程序会被中止并报错。万一覆盖到一个合法的地址,那么接下来的赋值就会导致一些有用的数据被莫名其妙地修改,此类bug是十分不好排查的,所以使用指针的时候一定要注意初始化。

int a = 10;
int *p = &a;    /* 声明指针时立即初始化 */// 或者将指针指向NULL
int *pa = NULL;    /* 暂时不知道该指向哪的时候先指向NULL */

         在C语言中,NULL被称为空指针,该指针不指向任何数据。本身是一个宏定义:

#define NULL ((void *)0)

         在大部分操作系统中,地址0通常都是一个不被使用的地址,如果一个指针指向NULL,意味着不指向任何东西。

1.2、取地址操作符(&)

        通过 & 获取变量的内存地址:

/* 通过 & 获取变量的内存地址 */
int a = 10;
int* p = &a;                    /* p存储变量a的地址 */
printf("address = %p\n", p);	/* address = 0000008655AFF774 */
printf("address = %p\n", &a);	/* address = 0000008655AFF774 */

1.3、解引用操作符(*)

        通过 * 访问指针指向的内存内容:

/* 通过 * 访问指针指向的内存内容 */
int a = 10;
int* p = &a;
printf("a = *p = %d\n", *p);	/* 输出10(通过p访问a的值) */

        直接通过变量名来访问变量的值称为直接访问,而通过指针这样的形式访问变量值称之为间接访问,因此解引用操作符也可称为间接运算符。

2、指针的常见操作

2.1、指针赋值

        指针可以直接指向另一变量的地址:

int a = 10, b = 20;
int* p = &a;
printf("a = *p = %d\n", *p);	/* 输出10(通过p访问a的值) */
p = &b;
printf("*p = %d\n", *p);		/* 输出20(通过p访问b的值) */

2.2、指针算术运算

        指针支持加减运算(按数据类型大小移动地址):

/* 指针算术运算 */
int arr[3] = { 10, 20, 30 };
int* p = arr;					/* p指向数组首元素 */
p++;							/* p指向数组第二个元素arr[1] */
printf("%d\n", *p);				/* 输出20,对应arr[1]的值 */

         上述示例中,p 是指向数组首元素的指针,那么 p++ 将对 p 进行自增运算并指向下一个元素arr[1];而 p += i 将对p进行加i的增量运算,使其指向指针 p 当前所指向的元素之后的第 i 个元素。

         指针的减法运算的意义:如果 p1 和 p2 指向相同数组中的不同元素,且 p1 < p2,那么 p2-p1+1 指向的元素之间的元素数目。可以编写一个获取字符串的长度的自定义函数strlen()。

int strlen(char* s)
{char* p = s;while (*p != '\0')p++;return p - s;
}int main()
{char s[] = "Hello World!";				/* 定义一个数组 */char* p = "Hello World!";				/* 定义一个指针 */printf("strlen(s) = %d\n", strlen(s));	/* 12 */printf("strlen(p) = %d\n", strlen(p));	/* 12 */return 0;
}

         上述示例中,需要特别注意 s 和 p 的区别,尽管它们的长度是一样的。但是 s 只是一个存放初始化字符串以及空字符 '\0' 的一维数组,只是 s 作为数组首元素的地址进行传递,数组中的单个字符可以进行修改。但是 p 是一个指针,始终指向同一个存储位置,其初值指向一个字符串常量,之后它可以被修改以指向其他的地址,但不允许修改字符串的内容。

2.3、指针比较

        在某些情况下指针可以通过“==”、“=”、“>”、“<”来比较地址的大小:

/* 指针比较 */
int arr[5];
int* p1 = &arr[0];
int* p2 = &arr[4];
if (p1 < p2) {printf("p1的地址在p2之前!\n");
}

         上述指针进行比较的前提是,指针 p1 和 p2 是指向同一数组中不同元素的指针。任何指针与 0 进行相等或不等的比较运算都有意义。但是指向不同数组元素的指针之间的算术或比较运算没有定义。有一个特例:指针的算术运算中可使用数组最后一个元素的下一个元素的地址。

3、常量指针和指针常量

        常量指针和指针常量的记忆技巧:

  • 常量指针:const在 * 左侧,如const int *p,表示“指向常量的指针”。记忆口诀:内容不可变,指针可变。
  • 指针常量:const在 * 右侧,如int* const p,表示“指针本身是常量”。记忆口诀:指针不可变,内容可变。 

3.1、常量指针(Pointer to Constant)

        常量指针表示指针指向的内容是常量,不能通过该指针修改指向的值,但指针本身可以指向其他地址。其声明语法为:

const 数据类型 *指针名;

// 或

数据类型 const *指针名;

/* 常量指针 */
int a = 10;
const int* p = &a;			/* p是常量指针,指向的int不可变 */// *p = 20;					/* 错误:不能通过p修改a的值 */
a = 20;						/* 正确:直接修改变量a的值是允许的 */
printf("*p = %d\n", *p);	/* *p = 20 */int b = 30;
p = &b;						/* 正确:指针可以指向其他变量 */
printf("*p = %d\n", *p);	/* *p = 30 */

        特点:

  • 指向的值不可通过指针修改。
  • 指针本身可以重新指向其他地址。
  • 常用于函数参数,避免意外修改外部数据。 
void printData(const int* arr, int size)
{for (int i = 0; i < size; i++) {printf("%d ", arr[i]);        /* 确保不会修改数组内容 */}
}

 3.2、指针常量(Constant Pointer)

        指针常量表示指针本身是常量,即指针的指向不可变(必须初始化),但可以通过指针修改指向的值。其声明语法为:

数据类型 *const 指针名 = 初始地址;

/* 指针常量 */
int a = 10;
int* const p = &a;				/* p是指针常量,指向不可变 */
printf("p = %p\n", (void*)p);*p = 20;						/* 正确:可以通过p修改a的值 */
printf("a = %d\n", a);			/* a = 20 */
printf("p = %p\n", (void*)p);int b = 30;
// p = &b;						/* 错误:指针的指向不可变 */

        特点:

  • 指针的指向固定,不可修改。
  • 可以通过指针修改指向的值。
  • 常用于固定访问某个内存地址的场景(如硬件寄存器)。
int reg = 0x1000;
int* const REG_ADDR = (int*)reg;    /* 指向固定地址 */*REG_ADDR = 1;                      /* 修改寄存器值 */

 3.3、指向常量的指针常量(Constant Pointer to Constant)

        指针和指向的内容都不可修改。其声明语法为:

const 数据类型 *const 指针名 = 初始地址;

/* 指向常量的指针常量 */
const int a = 10;
const int* const p = &a;			/* 指针和指向的内容均不可变 */// *p = 20;							/* 错误:不可修改值 */
int b = 30;
// p = &b;							/* 错误:不可修改指向 */

         特点:

  • 指针的指向和指向的值均不可变。
  • 适用于完全只读的场景(如全局配置数据)。 
const float* const PI = &3.1415926;

4、指针与数组

4.1、数组名的指针特性

        数组名就是数组首元素的地址:

	/* 数组名的指针特性 */int arr[5] = { 10, 20, 30, 40, 50 };int* p = arr;				/* p指向arr[0] */printf("*p = %d\n", *p);	/* *p = 10 */printf("arr = %p\n", arr);	/* arr = 00000016BAEFF7D8 */printf("p = %p\n", p);		/* p = 00000016BAEFF7D8 */

        也就是说,“int *p = arr;”和“int *p = &arr[0];”是等价的。 

4.2、通过指针遍历数组

         如果指针变量p指向数组中的某个特定元素,可根据指针运算的定义,p+1 将转向下一个元素,p+i 将指向数组元素之后的第 i 个元素,而 p-i 将指向所致数组元素之前的第 i 个元素。

/* 通过指针遍历数组 */
int arr[5] = { 10, 20, 30, 40, 50 };
int* p = arr;
for (int i = 0; i < 5; i++) {printf("%d ", *(p + i));    /* 10 20 30 40 50 */
}

         上述示例中,指针p指向arr,也就是arr[0],所以解引用*(p+i)得到的是数组元素a[i]的内容。

         还有另一等价的遍历数组元素写法:

int arr[5] = { 10, 20, 30, 40, 50 };
for (int* p = arr; p < arr + 5; p++) {printf("%d ", *p);		/* 输出10 20 30 40 50 */
}

        上述示例中使用了指向数组最后一个元素的下一个元素的地址 arr+5,这是被允许使用的。 

5、指针与函数

5.1、指针作为函数参数(传址调用)

       由于C语言中是以传值的方式将参数值传递给被调用函数。也就是说,被调用函数不能直接修改主调函数中变量的值。 但是可以通过指针修改函数外部的变量:

void swap(int* a, int* b)
{int temp = *a;*a = *b;*b = temp;
}int main()
{int x = 10, y = 20;swap(&x, &y);		printf("x = %d, y = %d\n", x, y);	/* x=20, y=10 */return 0;
}

         上述示例是将swap()函数的所有参数声明为指针,并且通过指针来间接地访问它们所指向的操作数,指针参数使得被调用函数能够访问和修改主调函数中对象的值。

        如果上述示例中的swap()函数的形式为swap(int a, int b),这样是无法达到目的的,此形式的swap()函数仅仅交换了a和b的副本的值。

void swap(int a, int b)
{int temp = a;a = b;b = temp;
}int main()
{int x = 10, y = 20;swap(x, y);printf("x = %d, y = %d\n", x, y);	/* x = 10, y = 20 */return 0;
}

5.2、返回指针的函数(指针函数)

        指针作为函数的返回值,是指函数的返回类型是一个指针类型,即函数返回某个数据的内存地址。这种函数通常用于动态分配内存或返回数据结构的地址。函数可以返回指针,但需要确保指向的内存有效:

int* findMin(int* arr, int size)
{int* min = arr;for (int i = 0; i < size; i++) {if (arr[i] < *min)min = &arr[i];}return min;				/* 返回最小值的地址 */
}int main()
{int arr[5] = { 3, 8, 6, 2, 4 };int* p = findMin(arr, 5);printf("p = %p\n", p);					/* p = 0000009D194FF724 */printf("&arr[3] = %p\n", &arr[3]);		/* &arr[3] = 0000009D194FF724 */return 0;
}

  注意事项:

  • 内存管理:如果返回动态分配的指针,一定要注意,调用者需要负责释放内存(如free)。
  • 局部变量:不要返回局部变量的地址,因为函数结束后内存失效。
  • 野指针:一定要确保返回的指针是指向的有效内存。

6、动态内存管理

6.1、malloc和free

        动态分配和释放内存:

/* 动态分配和释放内存 */
int* p = (int*)malloc(5 * sizeof(int));		/* 分配5个int的空间 */
if (p != NULL) {for (int i = 0; i < 5; i++) {p[i] = i + 1;printf("p[%d] = %d\n", i, p[i]);	/* 1 2 3 4 5 */}free(p);								/* 释放内存 */
}

         malloc并不是从一个在编译是就确定的固定大小的而数组中分配存储空间,而是在需要时向操作系统申请空间。所以,malloc管理的空间不一定时连续的。这样,空闲存储空间以空闲块链表的方式组织,每个块包含一个长度、一个指向下一块的指针以及指向自身存储空间的指针。当有申请请求时,malloc将扫描空闲块链表,直到找到一个足够大的块为止。如果块太大,则将它分成两部分:大小合适的块返回给用户,剩下的部分留在空闲块链表中。如果找不到一个足够大的块,则向操作系统申请一个大块并加入到空闲块链表中。

        释放过程也是首先搜索空闲块链表,以找到可以插入被释放块的合适位置。如果与被释放块相邻的任一边是一个空闲块,则将这两个块合成一个更大的块,这样存储空间就不会有太多的碎片。

6.2、避免内存泄漏

        确保每次malloc都有对应的free:

	/* 确保每次malloc都有对应的free */int* p = malloc(sizeof(int));int a = 10;p = &a;free(p);		/* 释放后,p变为野指针 */p = NULL;		/* 推荐置空 */

7、高级指针操作

7.1、指针数组

        由于指针也是变量,所以它们也可以像其他变量一样存储在一个数组中。下面示例中arr是一个存储指针的数组:

/* 指针数组 */
int a = 1, b = 2, c = 3;
int* arr[3] = { &a, &b, &c };    /* arr是一个指针数组,存储3个int指针 */

         内存布局:指针数组的每个元素独立指向内存中的某个地址。

arr[0] --> &a

arr[1] --> &b

arr[2] --> &c

        典型应用——存储多个字符串(字符串数组):

char* names[] = { "Alice", "Bob", "Klein" };
printf("names[0] = %s\n", names[0]);	/* names[0]指向"Alice" */
printf("names[1] = %s\n", names[1]);	/* names[1]指向"Bob" */
printf("names[2] = %s\n", names[2]);	/* names[2]指向"Klein" */

7.2、数组指针

        数组指针是一个指针 ,指向一个完整的数组。它保存的是整个数组的起始地址。下面示例中的 p 是一个数组指针。

int nums = { 10, 20, 30 };
int (*p)[3] = &nums;        /* p是一个数组指针,指向包含3个int的数组 */

        内存布局:数组指针指向整个数组的起始地址:

p --> nums[0](整个数组的首地址)

        典型应用1、操作二维数组。

/* 数组指针的应用 -- 操作二维数组 */
int matrix[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };
int (*p)[4] = matrix;			/* p指向二维数组的第一行(即matrix[0]) */
printf("%d\n", (*p)[2]);		/* 输出3(matrix[0][2]) */
printf("%d\n", (*(p+1))[3]);	/* 输出8(matrix[1][3] */
printf("%d\n", (*p)[10]);		/* 输出11(matrix[2][2],即二维数组第11个元素的值 */

        典型应用2、传递二维数组到函数。

/* 数组指针的应用 -- 传递二维数组到函数 */
void printMatrix(int (*mat)[4], int rows)
{for (int i = 0; i < rows; i++) {for (int j = 0; j < 4; j++) {printf("%d ", mat[i][j]);}printf("\n");}
}int main()
{int matrix[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };printMatrix(matrix, 3);return 0;
}

        指针数组和数组指针的关键区别:

特性指针数组数组指针
本质数组,元素是指针指针,指向一个数组
声明语法int *arr[5];int (*p)[5];
内存占用多个指针的空间(如5个指针)单个指针的空间
用途存储多个独立指针(如字符串数组)操作整个数组(如二维数组的行指针)
访问方式arr[i]访问第 i 个指针(*p)[i]访问数组的第 i+1 个元素

         运算符优先级:

  • int *arr[5]:[ ]优先级高于*,因此是指针数组。
  • int (*p)[5]:()强制*优先结合,因此是数组指针。

         常见错误:

  • 错误赋值:
int nums[3] = { 1, 2, 3 };
int (*p)[3] = nums;        /* 错误,nums是首元素地址,类型为int* */
int (*p)[3] = &nums;       /* 正确,&nums是整个数组的地址 */ 
  • 越界访问: 
int nums[3] = { 1, 2, 3 };
int (*p)[3] = &nums;
printf("%d\n", (*p)[3]);    /* 越界访问(数组索引为0~2) */

7.3、多级指针(指针的指针)

         在C语言中,指针的指针(即二级指针)是一种指向指针的指针变量,常用于处理多级间接访问和动态内存管理。

/* 多级指针(指针的指针) */
int a = 10;
int* p = &a;
int** pp = &p;							/* pp指向指针p */
printf("p的地址:%p\n", (void*)pp);		/* 输出p的地址 */
printf("a的地址:%p\n", (void*)*pp);		/* 输出a的地址 */
printf("a = **pp = %d", **pp);			/* 输出10 */

         指针的指针存储的是另一个指针的地址,声明时使用两个星号(**)。示例中,pp 是 int 型指针的指针,指向 p。在解引用的时候,*pp 获取的是 p 的值(即a的地址),而 **pp 是获取 a 的值。

        指针的指针的应用场景: 

  • 动态修改指针的值

         若需要在函数中修改指针指向的地址,需要传递指针的指针:

void allocate(int** ptr)
{*ptr = malloc(sizeof(int));		/* 修改外部指针指向新内存 */**ptr = 100;					/* 设置值 */
}int main()
{int* p = NULL;allocate(&p);		/* 传递指针的地址 */printf("%d\n", *p);	/* 输出100 */free(p);return 0;
}
  • 字符串数组(命令行参数) 

        main函数的参数char **argv是典型的指针的指针用法:

int main(int argc, char **argv)
{for (int i = 0; i < argc; i++) {printf("参数 %d:%s\n", i, argv[i]);     /* argv[i]是char*类型 */}return 0;
}

        常见错误:

  • 野指针:在动态申请后,释放内存后未置空指针的指针。
free(*pp);
*pp = NULL;    /* 避免野指针 */
  •  错误的解引用层级:
int **pp = NULL;
*pp = 10;        /* 错误:*pp是int*类型,不能直接赋值int */
**pp = 10;       /* 正确:需先确保*pp指向有效内存 */ 
  •  类型不匹配:
int a = 10;
int *p = &a;
char **pp = &p;    /* 错误:类型不兼容 */

7.4、函数指针

        在C语言中,函数本身不是变量,但可以定义指向函数的指针。函数指针是指向函数的指针变量,存储函数的入口地址,允许通过指针调用函数。这种类型的指针可以被赋值、存放在数组中、传递给函数以及作为函数的返回值等等。如下是一个指向函数的指针的示例。

int add(int a, int b)
{return a + b;
}
int (*funcPtr)(int, int) = add;	    /* 声明函数指针 */int main()
{printf("%d\n", funcPtr(2, 3));	/* 输出5 */return 0;
}

        注意:函数指针的返回类型和参数列表必须与目标函数一致,并且函数指针需要指向实际存在的函数。

        实现一个回调函数:

int add(int a, int b)
{return a + b;
}void process(int a, int b, int (*callback)(int, int))
{printf("结果:%d\n", callback(a, b));
}int main()
{/* 回调函数 */process(5, 3, add);		/* 输出:8 */return 0;
}


http://www.ppmy.cn/server/172767.html

相关文章

【芯片设计】AI芯片前端设计工程师面试记录·20250303

【芯片前端设计面试经验专栏介绍】 专栏聚焦数字芯片前端设计核心技术与面试方法论,涵盖架构设计、RTL开发、验证方法学、低功耗设计、时序收敛等高频考点,深入解析行业头部企业的面试真题与设计场景。内容包含但不限于: 知识点系统梳理 :从Verilog/SV语法陷阱、FSM设计模式…

Ollama进行DeepSeek本地部署存在安全风险解决方案,nginx反向代理配置

文章目录 概要整体架构流程技术细节**## 1.下载nginx [https://nginx.org/en/download.html](https://nginx.org/en/download.html),推荐Stable version稳定版**2.下载完成解压文件,打开conf文件夹下的nginx.conf,贴上反向代理配置3.然后点击解压文件夹下的nginx.exe,启动成…

LangGraph实战:构建智能文本分析流水线

LangGraph实战:构建智能文本分析流水线 1. 智能文本分析 LangGraph是基于图结构的工作流开发框架,通过节点函数和条件流转实现复杂业务逻辑。四大核心能力: 1.1 状态容器 统一管理流程执行上下文,支持JSON序列化存储 1.2 智能路由 基于条件判断实现动态分支跳转 1.3 可…

【Java 后端 Web 应用安全事件响应与溯源流程(Linux)】

Java 后端 Web 应用安全事件响应与溯源流程 一、事件响应核心流程&#xff08;Java 后端聚焦&#xff09;1. 快速隔离与现场保护2. 关键日志收集与备份 二、Java 应用攻击痕迹分析1. 应用服务器日志深度检测2. 应用层代码与依赖分析3. 内存与线程取证 三、工具链与自动化分析1.…

多空狙击线-新指标-图文教程,多空分界买点以及强弱操盘技术教程,通达信炒股软件指标

“多空狙击线”指标 “多空狙击线”特色指标是量能型技术指标&#xff0c;主要用于分析股票市场中机构做多/做空力量的强程度。该指标的构成、定义与原理如下: “多空狙击线”指标&#xff0c;又称机构做多/做空能量线&#xff0c;通过计算和分析股票市场中机构做多/做空力量…

Windows上使用go-ios实现iOS17自动化

前言 在Windows上运行iOS的自动化&#xff0c;tidevice对于iOS17以上并不支持&#xff0c;原因是iOS 17 引入新通信协议 ‌RemoteXPCQUIC‌&#xff0c;改变了 XCUITest 的启动方式。 一、go-ios的安装 1、安装命令&#xff1a;npm i go-ios 2、安装完成后输入命令which io…

【显示】3.1 Android 从Activity到Display链路概括

目录 一,Activity上屏Flow总结 二,链路拆解 2.1 Activity 的创建和 UI 初始化 2.2 Window 和 DecorView 的创建 2.3 Surface 的创建 2.4 View 的绘制流程 2.5 Surface 的提交和合成 2.6 上屏显示 三,多个Activity的处理方式 一,Activity上屏Flow总结 Activity → s…

vue3:初学 vue-router 路由配置

承上一篇&#xff1a;nodejs&#xff1a;express js-mdict 作为后端&#xff0c;vue 3 vite 作为前端&#xff0c;在线查询英汉词典 安装 cnpm install vue-router -S 现在讲一讲 vue3&#xff1a;vue-router 路由配置 cd \js\mydict-web\src mkdir router cd router 我还…