news 2026/6/4 1:31:55

C语言指针知识点

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言指针知识点

C语言指针知识点

前言

指针是C语言的灵魂,也是无数初学者心中难以逾越的高山。有人说“理解了指针,就理解了C语言的一半”,这话一点都不夸张。本文将从最基础的内存概念开始,循序渐进地讲解字符指针、指针数组、数组指针以及它们之间错综复杂的关系,配合大量的代码示例和内存图示,帮助你真正掌握C指针的核心知识。


第一章:内存与地址——指针的物理基础

1.1 内存就是一个个带编号的小格子

想象一下,计算机的内存就像一栋巨大的公寓楼,每个房间都有唯一的门牌号。这些“房间”就是内存单元,而“门牌号”就是地址。

text

内存示意图(每个格子1字节): 地址: 1000 1001 1002 1003 1004 1005 ... 内容: ??? ??? ??? ??? ??? ??? ...

当我们定义一个变量时,编译器会为这个变量分配一个或多个这样的“房间”。指针变量特殊的地方在于:它里面存放的不是普通数据,而是另一个房间的门牌号(地址)。

1.2 为什么需要指针?

没有指针时,函数只能通过返回值来传递数据,而且只能传递一份。有了指针,我们可以:

  • 在函数内部修改外部的变量

  • 动态申请内存(malloc)

  • 构建链表、树等复杂数据结构

  • 高效地操作数组和字符串

  • 实现回调函数

可以说,没有指针,C语言能做的工作会大打折扣。


第二章:字符指针——最简单的指针

2.1 字符指针的定义

字符指针就是指向字符类型数据的指针变量,用char*表示。它是最基础的指针类型之一,也是理解其他复杂指针的基石。

c

char ch = 'A'; char *p = &ch; // p 指向 ch,p 里存放的是 ch 的地址

2.2 字符指针的两种核心用法

用法一:指向单个字符

c

char ch = 'w'; char *pc = &ch; // pc 是指针变量,指向 ch *pc = 'W'; // 通过指针修改 ch 的值 printf("%c\n", ch); // 输出 W

关键点:普通变量不会自动变成指针,必须用&取地址。

c

char ch = 'A'; char *p1 = &ch; // ✅ 正确,必须用 & char *p2 = ch; // ❌ 错误!ch 的值是 'A'(ASCII 65),不是地址

用法二:指向字符串(最常见)

C语言中没有专门的字符串类型,字符串是以\0结尾的字符数组。字符指针可以指向这个数组的第一个字符。

c

// 方式A:指向可修改的字符数组 char arr[10] = "abcdef"; char *p1 = arr; // arr 是数组名,会退化为指针 *p1 = 'w'; // 修改第一个字符 printf("%s\n", p1); // 输出 wbcdef // 方式B:指向字符串常量(只读) const char *p2 = "abcdef"; // *p2 = 'w'; // 错误!不能修改常量区的内容 printf("%s\n", p2); // 输出 abcdef

2.3 指针的移动

指针变量本身的值(它所存储的地址)是可以改变的,这让我们可以遍历字符串中的每一个字符。

c

const char *p = "abc"; printf("%s\n", p); // 输出 abc printf("%s\n", p+1); // 输出 bc(从 b 开始输出) printf("%s\n", p+2); // 输出 c(从 c 开始输出)

注意:这里并没有修改字符串的内容,只是让 p 指向了不同的位置。字符串 "abc" 本身仍然完好无损地躺在内存中。

2.4 const 关键字的位置含义——一个常见的困惑点

很多初学者对const放在*的左边还是右边感到困惑。记住这个规律:

c

const char *p = "abc"; // const 在左边:指向的内容是常量,不能改内容 p = "def"; // ✅ 可以改变指向 // *p = 'x'; // ❌ 错误 char * const p = "abc"; // const 在右边:指针本身是常量,不能改变指向 // p = "def"; // ❌ 错误 *p = 'x'; // ❌ 危险(指向常量区) // 更常见且安全的写法: const char * const p = "abc"; // 内容和指向都不能改

2.5 字符指针 vs 字符数组——最本质的区别

这是面试中经常被问到的问题。两者最本质的区别在于内存位置可变性

特性char *str = "abc"char arr[] = "abc"
存储位置指针在栈/全局,字符串在常量区整个数组在栈/全局
是否可修改内容❌ 不可改(未定义行为,程序可能崩溃)✅ 可改
可重新指向其他字符串✅ 可以(指针本身是变量)❌ 不可以(数组名是常量地址)
大小指针占8字节(64位系统)数组占4字节("abc"+\0)

验证代码:

c

#include <stdio.h> int main() { char str1[] = "hello bit."; char str2[] = "hello bit."; const char *str3 = "hello bit."; const char *str4 = "hello bit."; if (str1 == str2) printf("str1 and str2 are same\n"); else printf("str1 and str2 are not same\n"); // 输出这个 if (str3 == str4) printf("str3 and str4 are same\n"); // 输出这个 else printf("str3 and str4 are not same\n"); return 0; }

为什么结果不同?因为str1str2是栈上两个不同的数组,各自占用独立的内存空间,地址自然不同。而str3str4指向的是常量区的同一块内存(编译器会优化,相同的字符串常量只存一份)。


第三章:深入理解 printf 的 %s——从地址到字符串

3.1 %s 的工作原理

printf的格式控制符决定了它怎么处理传入的参数:

格式符参数类型要求工作原理
%schar*从传入的地址开始,逐个字节读取,直到遇到\0停止
%pvoid*直接把地址值以十六进制形式打印出来
%cchar(值)打印单个字符
%dint(值)打印整数值

c

const char *p = "hello"; printf("%s\n", p); // 传入地址0x1000 → 去0x1000读内容 → 输出 hello printf("%c\n", *p); // 传入字符'h'(值) → 直接输出 h printf("%p\n", p); // 传入地址0x1000 → 输出 0x1000

3.2 什么类型可以用 %s?

✅ 可以用 %s 的情况:

c

// 1. 字符指针 char *p = "hello"; printf("%s", p); // ✅ // 2. 字符数组名(退化为指针) char arr[] = "hello"; printf("%s", arr); // ✅ // 3. 指针数组的单个元素 char *arr[] = {"abc", "def"}; printf("%s", arr[0]); // ✅ 输出 abc // 4. 数组指针的解引用 char arr[10] = "hello"; char (*ptr)[10] = &arr; printf("%s", *ptr); // ✅ *ptr 的类型是 char*

❌ 不能用 %s 的情况:

c

// 1. 指针数组名本身 char *arr[] = {"abc", "def"}; printf("%s", arr); // ❌ arr 退化后是 char**,类型不匹配 // 2. 数组指针本身 char (*ptr)[10] = &arr; printf("%s", ptr); // ❌ ptr 的类型是 char(*)[10] // 3. 整型数组 int intArr[] = {1,2,3}; printf("%s", intArr); // ❌ intArr 退化后是 int*

第四章:数组名与取地址——一个容易混淆的区别

4.1 三种地址的区别

对于char arr[10] = "abcdef",我们有三种方式获取地址:

表达式含义类型+1 后的偏移
arr数组首元素的地址char*0x1000x101(1字节)
&arr[0]首元素的地址(同上)char*0x1000x101(1字节)
&arr整个数组的地址char(*)[10]0x1000x10A(10字节)

4.2 为什么要用 &arr?

这是理解数组指针的关键问题。

核心原因:数组名在表达式中会自动退化为指向首元素的指针。如果你想要“指向整个数组的指针”,就必须用&来“阻止”退化,并提升类型。

c

char arr[10]; // 不用 &:类型是数组首元素的指针 char *p1 = arr; // arr 退化 → char*(指向第一个字符) // 用 &:类型是指向整个数组的指针 char (*p2)[10] = &arr; // &arr → char(*)[10](指向整个数组)

类比理解:

把数组想象成一栋10层楼(每层1个字符):

表达式含义类比
arr指向一楼的指针“这栋楼的一楼在哪儿?”
&arr指向整栋楼的指针“这栋楼整体在哪儿?”

两个指针的数值相同(都是楼的位置),但类型不同

  • arr是“楼层指针”,+1 到二楼

  • &arr是“整楼指针”,+1 到下一栋楼

4.3 代码验证

c

#include <stdio.h> int main() { char arr[10] = "abcdef"; printf("arr = %p\n", arr); // 0x100 printf("arr+1 = %p\n", arr+1); // 0x101 printf("&arr = %p\n", &arr); // 0x100 printf("&arr+1 = %p\n", &arr+1); // 0x10A return 0; }

4.4 普通变量 vs 数组:取地址的区别

重要澄清:只有数组才有“退化”,普通变量没有退化。

情况代码原因
普通变量char *p = &ch;变量不会退化,必须用&取地址
一维数组(退化)char *p = arr;数组名退化为指针,不用&
一维数组(取整个数组)char (*p)[10] = &arr;要用&,类型才是char(*)[10]

c

char ch = 'A'; char *p1 = &ch; // ✅ 普通变量,必须用 & char *p2 = ch; // ❌ 错误!ch 的值是 'A',不是地址 char arr[10]; char *p3 = arr; // ✅ 数组名退化,不需要 & char (*p4)[10] = &arr; // ✅ 取整个数组的地址,需要 &

第五章:指针数组——存放指针的数组

5.1 什么是指针数组

指针数组本质上是一个数组,只不过数组里存放的不是整数或字符,而是指针(地址)。

c

int *p[5]; // p 是一个数组,有5个元素,每个元素是 int* 类型 char *arr[3]; // arr 是一个数组,有3个元素,每个元素是 char* 类型

5.2 指针数组的核心作用

指针数组最大的优势是:让多个长度不同的字符串可以像普通数组一样被统一管理

c

char *fruits[] = {"apple", "banana", "cherry", "durian"}; // 这四个字符串长度分别为5、6、6、6(+1的\0) // 但每个指针只占8字节,总共32字节 // 如果使用二维数组 char[4][10],需要40字节,浪费空间

5.3 内存布局详解

c

char *arr[3] = {"hello", "world", "nice"};

text

内存布局: 指针数组 arr(位于栈或全局数据区): ┌─────────────────────────────────────────┐ │ 地址: 2000 2008 2016 │ │ 内容: 3000 3100 3200 │ │ 含义: 指向hello 指向world 指向nice │ └─────────────────────────────────────────┘ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ 实际字符串(位于常量区,位置不固定): ┌──────┐ │ 3000 │ h e l l o \0 ├──────┤ │ 3100 │ w o r l d \0 ├──────┤ │ 3200 │ n i c e \0 └──────┘

关键特点:

  • 指针数组本身在内存中连续存储(2000, 2008, 2016)

  • 字符串分散在内存的其他位置(3000, 3100, 3200,可能相隔很远)

  • 每个字符串只占用实际长度+1的空间,没有浪费

5.4 指针数组 vs 二维数组

特点指针数组char *a[]二维数组char a[3][10]
内存布局指针连续 + 字符串分散所有字符连续存储
字符串长度可以不同每行固定长度
内存利用率高(无浪费)低(预分配固定长度)
修改字符串可以指向新字符串或修改内容只能修改内容
常用场景字符串列表、命令行参数固定格式数据(如棋盘)

第六章:数组指针——指向数组的指针

6.1 什么是数组指针

数组指针本质上是一个指针,只不过它指向的是一个数组整体,而不是单个元素。

c

int (*p)[5]; // p 是一个指针,指向一个包含5个int元素的数组 char (*ptr)[10]; // ptr 是一个指针,指向一个包含10个char元素的数组

6.2 为什么需要数组指针?

数组指针的核心价值在于:能够以“整个数组”为单位进行移动或操作

c

int arr[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}}; int (*p)[4] = arr; // p 指向第一行 // p 是一个指针,指向一个长度为4的int数组 // p+1 会跳过一整行(16字节),而不是一个元素(4字节)

6.3 数组指针的三种核心用途

用途1:遍历二维数组的行

c

#include <stdio.h> int main() { int arr[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; int (*p)[4] = arr; // p 指向第一行 for (int i = 0; i < 3; i++) { for (int j = 0; j < 4; j++) { printf("%d ", p[i][j]); // 方式1 } printf("\n"); } return 0; }

用途2:函数参数传递二维数组(保留列信息)

c

void printMatrix(int (*arr)[5], int rows) { for (int i = 0; i < rows; i++) { for (int j = 0; j < 5; j++) { printf("%d ", arr[i][j]); } printf("\n"); } }

用途3:动态分配二维数组

c

int (*matrix)[5] = malloc(10 * sizeof(*matrix)); // 10行,每行5个int

6.4 指针运算演示

c

#include <stdio.h> int main() { int arr[10] = {0}; // 普通指针 int *p1 = arr; printf("p1 = %p\n", p1); // 0x100 printf("p1+1 = %p\n", p1+1); // 0x104(移动4字节) // 数组指针 int (*p2)[10] = &arr; printf("p2 = %p\n", p2); // 0x100 printf("p2+1 = %p\n", p2+1); // 0x128(移动40字节 = 10*4) return 0; }

第七章:深入理解“退化”——C语言最隐蔽的特性

7.1 什么是退化?

退化(Decay)是C语言中一个非常重要的概念:数组名在大多数情况下会自动转换为指向其首元素的指针。

7.2 什么时候不退化?

只有两种情况下数组名不会退化

c

int arr[10]; // 情况1:作为 sizeof 的操作数 size_t s = sizeof(arr); // s = 40(10 * 4),不是 8(指针大小) // 情况2:作为 & 的操作数 int (*p)[10] = &arr; // &arr 的类型是 int(*)[10],不是 int*

7.3 什么时候会退化?

几乎所有其他情况:

c

int arr[10]; int *p = arr; // ✅ arr 退化 arr[0]; // ✅ arr 退化(等价于 *(arr+0)) func(arr); // ✅ 传给函数时退化 arr + 1; // ✅ 运算时退化

7.4 不同维度数组的退化规律

数组定义完整类型退化后的类型匹配的指针类型
int a[5]int[5]int*int(*)[5]
int a[3][4]int[3][4]int(*)[4]int(*)[4]
int a[3][4][5]int[3][4][5]int(*)[4][5]int(*)[4][5]

7.5 为什么二维数组名可以直接赋值给数组指针?

c

int arr[3][5]; int (*ptr)[5] = arr; // ✅ 可以,不需要 &

原因:类型匹配!

  • arr的类型是int[3][5]

  • 在表达式中,arr退化为int(*)[5](指向有5个int的数组的指针)

  • ptr的类型正好是int(*)[5]

对比一维数组:

c

int a[5]; int (*ptr)[5] = &a; // ✅ 必须用 & // int (*ptr)[5] = a; // ❌ a 退化后是 int*,类型不匹配

第八章:指针数组与数组指针的终极对比

8.1 语法对比

c

// 指针数组:先看到 [],后看到 * int *p[5]; // p 是一个数组,包含5个 int* 类型的元素 // 数组指针:先看到 *,后看到 [] int (*p)[5]; // p 是一个指针,指向 int[5] 类型的数组

8.2 大小对比

c

printf("sizeof(p1) = %lu\n", sizeof(p1)); // 40 (5 * 8) printf("sizeof(p2) = %lu\n", sizeof(p2)); // 8 (指针大小)

8.3 用途对比

场景使用指针数组使用数组指针
存储多个字符串✅ 最佳选择❌ 不合适
函数传递二维数组❌ 不合适✅ 最佳选择
字符串排序✅ 高效❌ 不合适
动态分配二维数组❌ 需要多次分配✅ 一次分配

第九章:常见错误与调试技巧

9.1 常见错误清单

错误1:混淆普通变量和数组的取地址

c

char ch = 'A'; char *p1 = ch; // ❌ 错误!普通变量不会退化 char *p2 = &ch; // ✅ 正确 char arr[10]; char *p3 = arr; // ✅ 数组名退化,正确 char (*p4)[10] = arr; // ❌ 类型不匹配 char (*p5)[10] = &arr; // ✅ 正确

错误2:修改字符串常量

c

char *p = "hello"; p[0] = 'H'; // ❌ 危险!

错误3:返回局部数组的地址

c

char* getString() { char arr[] = "hello"; return arr; // ❌ 返回后 arr 被销毁 }

9.2 调试技巧

技巧1:使用 %p 打印地址

c

printf("arr = %p\n", arr); printf("arr+1 = %p\n", arr+1); printf("&arr = %p\n", &arr);

技巧2:使用 sizeof 区分数组和指针

c

int arr[10]; int *p = arr; printf("%lu, %lu\n", sizeof(arr), sizeof(p)); // 40, 8

第十章:记忆口诀与总结

10.1 核心记忆口诀

text

指针数组存指针,数组指针指数组。 左看右看定类型,括号位置分清楚。 普通变量取地址,必须加上 & 符号。 数组名会自己退,退成指针不需要。 要想取到整个数组,& 符号不能少。 一维数组要加 &,二维数组退化了。 数组名用 sizeof 和 & 是本体, 其他场合都退化莫迟疑。 const 在左内容不变, const 在右指向不变。 %s 要地址,%c 要字符, 传对类型才不会出错。

10.2 终极对比表

概念语法本质大小取地址方式
普通变量char ch变量1字节必须用&ch
字符指针char *p指针8字节本身已是地址
字符数组char arr[10]数组10字节arr退化,&arr取整体
指针数组int *p[5]数组40字节数组名退化
数组指针int (*p)[5]指针8字节指向数组

10.3 最后的话

指针是C语言的精髓,也是通向高级C编程的必经之路。理解指针不是一蹴而就的,需要在实践中反复练习、不断思考。

记住这几个最关键的要点:

  1. 普通变量不会退化,必须用&取地址

  2. 数组名会退化,变成指向首元素的指针

  3. 要取整个数组的地址,一维数组必须用&,二维及以上可以直接用数组名

  4. %s要求char*,传对类型才不会出错

建议你:

  1. 多画内存图:遇到指针问题,先在纸上画出内存布局

  2. 多用 printf 打印地址:验证自己的理解是否正确

  3. 多写测试代码:把本文的每个例子都亲手跑一遍

希望这篇指南能够帮助你彻底掌握C指针。如果你有任何疑问,欢迎继续探讨!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/4 1:24:21

openGSD安装与配置国产大模型

本次介绍的是最新版 https://github.com/open-gsd/gsd-pi 不是之前的gsd&#xff0c;也不是gsd-2。安装命令是 npm install -g opengsd/gsd-pilatest 第一步&#xff1a;前置条件是git和node环境&#xff0c;都安装最新稳定版就行。 第二步&#xff1a;执行安装命令 第三步…

作者头像 李华
网站建设 2026/6/4 1:22:18

毕业设计实战复盘:用DHT11/DHT12+51单片机+Zigbee,从零搭建一个低成本温湿度监测系统

低成本温湿度监测系统实战&#xff1a;从DHT传感器到Zigbee无线传输的完整设计在物联网技术快速发展的今天&#xff0c;环境监测系统已成为许多应用场景的基础设施。作为一名电子工程专业的毕业生&#xff0c;我曾花费数月时间完成了一个基于51单片机和DHT传感器的温湿度监测系…

作者头像 李华
网站建设 2026/6/4 1:09:40

现代Web开发:架构演进和前沿实践

深入探索现代Web应用技术开发&#xff1a;架构、实践与演进 在数字化浪潮席卷全球的今天&#xff0c;Web应用已成为连接用户与服务、驱动业务创新的核心载体。从简单的静态页面到复杂的交互式平台&#xff0c;Web开发技术经历了翻天覆地的变化。本文将深入探讨现代Web应用技术…

作者头像 李华