☰
彻底搞懂char字符、char*指针与char**二级指针的本质区别与实战应用
2026/10/1 1:02:48 网站建设 项目流程

1. 三种类型的第一次正面交锋

先看一段最直白的代码,看完你就知道这三种类型在内存里长什么样。

char c = 'A'; // 一个字符,占用1个字节 char* p = &c; // 指向字符的指针,存的是c的地址 char** pp = &p; // 指向字符指针的指针,存的是p的地址

很多新手看到第三行就懵了:指针还能指向指针?没错,而且在实际工程中,char**出现的频率比想象中高得多。比如你写的main函数:

int main(int argc, char** argv)

这个argv就是char**类型。你每次写的命令行参数,本质上就是通过它传进来的。

咱们先把话放这儿:char存的是真实的数据,char*存的是数据在哪,char**存的是"数据在哪"这个信息本身在哪。这句话你品一品,后面两个小时你就不会白费。

1.1 从内存布局讲清楚三者的本质区别

要理解这三者的关系,你必须站在内存的角度去看。

假设内存地址0x100处存了一个字符'A'。那么:

  • char c = 'A',意思是地址0x100这个字节里直接存的就是0x41('A'的ASCII码)。
  • char* p = &c,意思是p这个变量本身在另一个地址,比如0x200,0x200这个地址里存的值是0x100。
  • char** pp = &p,意思是pp这个变量在0x300,0x300这个地址里存的值是0x200。

用表格表示就是:

变量变量自身的地址存储的内容解释
c0x1000x41直接就是字符数据
p0x2000x100指向c的地址
pp0x3000x200指向p的地址

这个关系捋顺了,剩下的事情都好办。

1.2 为什么说char*不止是“字符指针”

严格来说,char*是指向char类型变量的指针。但在C语言的实际使用中,char*通常被用来表示一个字符串。为什么?因为C语言没有专门的字符串类型,字符串就是连续存放的一串字符,以\0结尾,而这个连续空间的起点用char*来记录。

char* str = "hello";

这个str指向的是'h'的地址,后面依次是'e'、'l'、'l'、'o'、'\0'。你拿str做加减运算,就能逐个访问字符串里的字符。

但你千万不要认为char*只能存字符串。它也可以指向单个字符,就像刚才代码里的char* p = &c。你甚至可以拿char*去遍历一个char数组里的任意一个元素。char*的本质是"指向char的指针","字符串"只是它的常见用法,不是它的全部。

2. 为什么需要char**:一级指针解决不了的场景

我在带新人的时候,经常有人问:既然char*已经能干这么多事了,为什么还要char**?答案其实就一句话:当你需要在函数里修改一个指针的指向时,你就需要指针的指针。

2.1 函数参数传递的经典陷阱

新手最容易踩的坑就是这个。看代码:

void modify(char* p) { p = (char*)malloc(100); } int main() { char* str = NULL; modify(str); // str还是NULL,你分配的100个字节丢了 }

为什么会这样?因为函数参数是值传递。modify函数里的p是str的一个副本,你在副本上改指向,原来的str根本不受影响。这就好比你让同事帮你拿快递,结果同事把快递放自己工位上了,你以为快递到了你桌上。

这时候char**就派上用场了:

void modify(char** p) { *p = (char*)malloc(100); } int main() { char* str = NULL; modify(&str); // 这次str指向了那100个字节 }

注意看区别:这次传的是str的地址,也就是char**。函数里*p = malloc(100)的意思是:在p所指向的内存里,写入malloc返回的地址。而p指向的正好是str,这就等于把str的值改了。

2.2 argv参数到底是怎么来的

再回到main函数的char** argv。你执行一行命令:

./program -a -b file.txt

操作系统会把这一行拆成4个字符串:./program、-a、-b、file.txt,然后把这4个字符串的首地址放进一个数组,再把这个数组的首地址传给argv。

所以argv其实是指向一个指针数组的指针。每个元素都是char*,分别指向一个字符串。argv[0]是程序名,argv[1]是第一个参数,以此类推。

这就是char**最常见的工程场景:表示一个字符串数组。

3. 三种类型的实际使用场景拆解

3.1 char:单字符,别小看它

char在绝大多数平台上占1个字节。你可能觉得char只能存字母,但实际情况是,char在底层就是个整数。你可以拿它做算术运算:

char c = 'A'; char d = c + 1; // d是'B',因为'A'的ASCII是65

有时候你会看到有人用char存小整数,比如传感器返回的0到127的值。这在嵌入式领域非常常见,因为省内存。但要注意,char可能是signed也可能是unsigned,取决于编译器实现。如果要做位运算,我建议直接指定signed char或unsigned char,不然可移植性是个坑。

3.2 char*:字符串操作的主力军

几乎所有的C语言字符串操作,输入输出都是char*。比如:

char buffer[128]; strcpy(buffer, "hello"); printf("%s", buffer);

这里的buffer在表达式里退化成指向数组首元素的char*指针。

日常工程中,char*常用来做:

  • 拼接字符串(strcat)
  • 格式化输出(sprintf)
  • 解析命令行参数
  • 读写文件的行缓冲

实际操作的时候,我强烈建议你搞清楚自己手里拿的char*到底指向哪块内存。是栈上的数组?堆上malloc出来的?还是字符串字面量?这三种情况对你能不能修改、要不要free,影响非常大。

3.3 char**:字符串数组与动态分配

char**最常见的两种用途:

第一种,表示字符串数组。用一个指针的指针,指向一个char*数组。数组里的每一个char*,各自指向一个字符串。

char* fruits[3] = {"apple", "banana", "cherry"}; char** pp = fruits; printf("%s\n", pp[0]); // apple printf("%s\n", pp[1]); // banana

第二种,在函数里分配并返回字符串数组。比如你要写一个函数,把一个文本文件的所有行读出来,行的数量提前不知道,那么就适合用char**:

int read_lines(char*** lines_out, int* count_out);

这里甚至出现了char***,但原理是一样的:你想让函数直接修改调用者的char**变量,就要传char***。不过到了这个层级,代码的可读性就开始下降了,工程上建议用结构体包一层。

3.4 动态分配二维字符串结构的注意事项

如果要用char**动态创建一个字符串数组,通常分两步:先分配指针数组,再分配每个字符串。

char** arr = malloc(3 * sizeof(char*)); for (int i = 0; i < 3; i++) { arr[i] = malloc(32); sprintf(arr[i], "row %d", i); } // 释放顺序:先释放每个字符串,再释放指针数组 for (int i = 0; i < 3; i++) { free(arr[i]); } free(arr);

释放的顺序很重要。你想想,先释放指针数组的话,字符串本身还在堆上没人管,就泄漏了。先释放每个字符串释放,再释放外层数组,内存就干净了。

4. 同名关键字在不同语言里的讲究

4.1 Java里的char不是C语言里的char

搜索引擎热词里有"java char是什么数据类型",这个必须单独拉出来说。Java的char是16位无符号整数,范围是0到65535,它用来表示Unicode码元。一个关键区别是:Java中char占2个字节,不是1个字节。

正因为占2个字节,Java的char可以直接存放中文:

char c = '中';

这在C语言里做不到。C语言的char只有1个字节,存不了中文字符。想存中文只能用char数组配合UTF-8编码,或者用wchar_t之类的宽字符类型。

还有一个容易混淆的地方:Java没有char*和char**这种东西。Java的字符串是String对象,字符串数组是String[],你根本不用关心指针怎么指来指去。这也是很多人从Java转C之后,碰到的第一个认知冲击:Java里字符串是引用类型,传参传的是引用(类似共享地址),但引用本身还是值传递。

4.2 GIS领域里char、float、int、time的数据类型区别

搜索引擎热词里还有"gis里面char float int time",这个跟本文主题有点关联,我稍微展开说。

在GIS(地理信息系统)的矢量数据中,一个要素的属性表里每一列都有自己的数据类型。常用的有这么几种:

类型典型用途存储特征
char / text地块名称、行政区划代码、道路名称字符串,需要指定长度,如char(20)
float / double经纬度坐标、面积、长度浮点数,有精度问题
int人口数量、楼层数、年份整数,存储紧凑
time / date数据采集时间、更新日期时间戳,数据库里有专门的类型

在GIS二次开发中,比如写一个Shapefile读取器,你从字段定义里解析出来的往往是"字段类型是字符型还是数值型"这样的信息,然后在内存里用char**来统一表达所有字段值的字符串形式,遇到numeric字段再转成float或int。这算是char**在GIS应用场景里的一个比较实际的用法了。

我见过不少搞GIS的新手,写属性查询的时候把numeric字段直接当字符串做了拼接,导致排序错误。比如"10"比"9"小,按字符串排序就会出问题。这时候正确的做法是先转成float或者int再比大小。说到底,理解char只是认识数据类型的起点,不同领域的数据类型差异,坑藏在细节里。

5. 一步一步教你搭建自己的测试环境

光看不练是学不会指针的。我建议你亲手写个程序验证一下。用VS Code或者任意文本编辑器都行,装一个gcc编译器。

第一步,创建test.c文件,敲入一段验证代码:

#include <stdio.h> int main() { char c = 'A'; char* p = &c; char** pp = &p; printf("c的值: %c\n", c); printf("c的地址: %p\n", &c); printf("p存的值: %p\n", (void*)p); printf("p的地址: %p\n", (void*)&p); printf("pp存的值: %p\n", (void*)pp); printf("pp解引用后: %p\n", (void*)*pp); printf("pp双重解引用: %c\n", **pp); return 0; }

第二步,编译运行:

gcc test.c -o test ./test

你会看到c的地址、p存的值完全一样,pp存的值和p的地址完全一样。这就把前面那张内存布局表实打实验证了一遍。

第三步,你自己动手改一改。弄个字符串试试:

char* str = "hello"; char** pstr = &str; printf("%s\n", *pstr); // hello

再试试修改指针指向的经典例子,把函数传参的问题也复现一遍。这样一套下来,你对三种类型的记忆就不是死记硬背,而是真理解了。

6. 新手最容易犯的四个错误

6.1 混淆声明与解引用

char* p代表p是一个指针,p表示地址,*p才表示一个char。很多刚入门的朋友写代码时搞不清楚:

char* p = "hello"; char c = *p; // 正确,c = 'h' char d = p; // 错误,p是地址不是字符

编译器大概率会报警告或者错误。但有些时候是隐式的错误,比如把指针直接赋给整数,不报错但结果根本没意义。

6.2 用char**存单个字符

有人会写:

char c = 'A'; char** pp = &c; // 编译报错

这里编译一定报错,因为&c的类型是char*,不是char**。你得先有一个char*变量,再把它的地址取出来赋给char**。这个关系必须一级一级来,不能跳级。

6.3 以为char*指向的内容一定能改

字符串字面量通常存储在只读区。你写:

char* p = "hello"; p[0] = 'H'; // 运行时会崩溃(未定义行为)

如果确实需要修改,你得用一个可写的副本:

char buf[] = "hello"; buf[0] = 'H'; // 没问题

这个坑在很多有Java背景的人身上特别常见,因为在Java里字符串是对象,修改字符串实际上是创建新对象,不会让你直接改底层的字符数组。到了C语言这里,你得自己管好每一块内存的可写性。

6.4 忘记释放动态分配的内存

只要用了malloc,最好提前想好谁负责释放。在复杂的项目中,这往往不是技术问题,而是规范问题。项目越大,内存泄漏越难排查。我的经验是:写malloc的时候就把free写好,别等程序跑挂了再回来补。

7. 一句话总结:什么时候用哪种

给你的选择标准其实很简单:

  • 只要处理一个字符,就用char。
  • 要处理字符串,或者需要间接访问一个字符,用char*。
  • 需要在一个函数里修改char*的指向,或者要表示字符串数组,用char**。
  • 如果再往上层走,函数里需要修改char**本身,那就会用到char***,但这时候建议用结构体替代,可读性好得多。

最后再分享一个我在面试新人时必问的题:给你一段代码,输出里有**、*的混用,你数不清关系怎么办。我通常建议他们先画格子。把内存想象成一列带编号的格子,每个变量是一格,格子里存的内容就写进去。只要画出来,指针关系一目了然。这个习惯养成了,后面遇到再复杂的多级指针都不怕。

到目前为止,把ch、char*和char**的关系理清楚,C语言入门最绕的一道坎就算过了。剩下的无非是多写多调试,脑子里时刻有一幅内存地图。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询