news 2026/10/13 18:55:40

C语言指针与字符串:为什么必须分配char[len+1]内存

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言指针与字符串:为什么必须分配char[len+1]内存

最近有个同学问我:“指针不是指向首地址,就可以输出字符串,为什么要分配内存 char[len+1]?”这个问题特别典型,几乎是每个学 C 语言的人都会撞上的墙。不是因为笨,而是因为指针和字符串组合之后,脑袋里很容易产生一个模糊的“指向首地址就行”的直觉。今天就用实际经验把这层纸捅破。

先说结论:指针指向首地址确实可以“让字符串看起来能输出”,但这个地址背后到底是不是真实、合法的内存,完全另说。如果不分配内存,指针就是一个野指针,它“恰好”保存了一个地址,但那个地址可能不属于你、可能随时被别的代码覆盖、甚至可能是无效地址。你只是赌它没崩而已。真正的字符串处理里,内存分配决定了数据能否安全存在,而char[len + 1]里的+1是为字符串终止符'\0'预留的位置,这一步少不得。

这篇文章适合所有被指针、字符串困扰的 C 语言初学者,也适合想彻底搞清楚“指针和数组到底差在哪”的开发者。我会从原理到实操,把为什么要分配内存、不分配会出什么问题、什么时候可以偷懒不分配,全给你捋清楚。


1. 指针与字符串:先分清“地址”和“内存所有权”

很多 C 语言教材上来就讲“指针是一个变量,用来存放地址”,然后画一个箭头从指针变量指向某个内存单元,学生就想当然以为:只要指针变量里有一个地址,就像有了一个遥控器,按下按钮就能获取对应的数据。这个理解没错,但漏了最关键的前提:地址必须指向一段真实存在、由你掌控的内存,才谈得上用指针访问那里面的数据。

想象一下,你拿到了一个房间的门牌号,但那个房间可能根本不存在,可能是别人的房子,可能是即将被拆掉的临时棚,你拿钥匙去开门自然是一脸懵。指针变量本身,就是一个存放“门牌号”的小纸条。纸条上写什么数字都行,问题是那个门牌号指向的地方,到底是不是“你的可访问内存”。

代码里最经典的问题就出在这:

char *str; printf("请输入字符串:"); scanf("%s", str);

这段代码里,str只是一个未初始化的指针变量,它里面装的是一个随机的“垃圾值”——可能是 0,可能是任意地址。你把这个垃圾值当作有效地址传入scanf,本质上是让操作系统把一个随机地址当成合法缓冲区来写。绝大多数情况会直接段错误(Segmentation fault),因为内核根本不允许你访问那个地址。运气不好也可能没崩,但那纯属侥幸,你正在踩踏别人的内存,数据早就被人改得面目全非了。

再退一步,就算你没有立刻段错误,后续输出这个字符串时,也完全不可控。比如:

char *p; p = "hello"; // 这又是另一回事,下面细说

这里p指向的是一个字符串字面量,也就是程序运行时放在只读数据段里的"hello\0"。这个地址是真实存在的,能输出,看起来“没有分配内存也成功了”。但如果你尝试修改p[0] = 'H',很多平台直接崩,因为那块内存是只读的。所以“能输出”不等于“可以安全读写”,这是两个维度的事。

关键点:

  • 指针变量里的地址,必须指向一块“合法、可访问、生命周期可控”的内存;
  • 指向只读字符串字面量时,能读不能写;
  • 指向未初始化的野指针,读和写都可能是灾难;
  • char[len + 1]分配的是“属于你的、可读可写、生命期由你控制”的内存。

1.1 “指向首地址”为什么往往不够

很多人会拿数组举反例:

char arr[6] = "hello"; char *p = arr; puts(p);

这里确实只把数组首地址给了p,没额外分配,也能正常输出。问题在于,arr本身就是一块已经分配的、有实际空间的内存。你是在“借用”已有数组的首地址,而不是“凭空让它成为一个地址”。

很多同学把这条经验错误推广了,以为“任何字符串只要给个首地址就能输出”。错在哪?错在模糊了**“空间的存在性”**。arr之所以能输出,是因为编译器已经为它在栈上分配了 6 个字节的内存空间,并且内容被初始化成"hello\0"。你要做的只是一种“地址拷贝”,把数组的首地址交给指针。而当你声明char *p;时,并没有任何与字符串相关的内存被创建。p这个概念本身只是“一个能保存地址的变量”,至于它保存的地址是谁的,需要你自己负责。

打个比方,数组相当于你已经租下了一个柜台,把柜台号抄给指针;而裸指针char *p;相当于你随手写了一个柜台号,但你根本没去租柜台,商场地下一堆号码牌,你随便抄了一个,后面能不能找到柜台全凭运气。

所以“指向首地址”在某些巧合下能输出字符串,是因为那个首地址背后恰好有真实且合法的字符串内容。但你需要区别的是:是你创建了那块内容,还是你从别处捡来一个地址?

1.2 输出字符串的本质:遍历到 '\0' 为止

再深挖一层:为什么 C 语言输出字符串时只靠一个“首地址”就够了?因为puts、printf("%s")、strlen这些函数,全部是按照约定从指针给的地址开始,逐个读取字节,直到遇到一个值为 0 的字节('\0')为止。它们不需要知道字符串长度,只需要你保证:

  1. 起始地址是合法的,可读的;
  2. 从起始地址开始,到'\0'之间的字节都是这个字符串的一部分;
  3. 这块内存从头到尾是连续可达的。

这个约定带来一个隐患:如果首地址合法,但后面没有'\0',函数会一直往前读,直到在某块内存中撞见一个碰巧为 0 的字节,或者非法访问导致崩溃。这就是为什么除了分配内存之外,还需要+1来专门放'\0'。分配 10 个字符的长度,你只能利用其中 9 个位置来放实际内容,最后一个必须是终止符。如果忘了+1,一旦你把第 10 个字符写进去,'\0'无处安放,就会越界写到相邻内存。缓冲区溢出的很多经典 bug 就是这么来的。


2. 为什么必须分配内存:三个无法回避的理由

理解了地址与内存所有权的区别,再看“为什么必须分配char[len + 1]”这个问题,就清晰了。这里我从三个角度详细拆解:生命周期、可写性、终止符空间。

2.1 生命周期:临时借用和永久居住的区别

考虑一个最常见的字符串拼接场景。你从输入里读到一个名字,长度未知,需要把"你好, "和名字拼起来输出。代码可能会这么写:

char *full; char name[20]; gets(name); // 先不讨论 gets 的安全问题 strcpy(full, "你好, "); // 全错 strcat(full, name);

full只是一个悬空的指针,strcpy要把"你好, "拷贝到full指向的内存里,但full压根没有指向任何有效内存。这就像一个快递员拿到一个收件人地址,但那个地址是空白的,他只能去碰运气。正确做法是:

int len = strlen(name); char *full = malloc(len + 5); // 至少 "你好, " 占 5 个字符(含逗号空格) + name长度 + 1

如果没有分配,拷贝进去的内容会随机落到某个地址。轻则数据损坏,重则段错误。分配了内存,你才能掌控这块区域的创建和释放时机,保证它在拼接、输出、后续处理过程中始终有效。

还有一个容易被忽视的点是函数返回字符串的场景。如果函数里定义一个局部数组:

char *get_message(void) { char buf[64]; sprintf(buf, "some data"); return buf; }

外表看起来挺对,函数内部有数组,返回数组首地址,调用方能继续输出。但问题是:buf是在栈上分配的,函数一返回,栈帧被回收,这块内存不再属于你。虽然那个地址可能还存着刚才的字符串,但下次任何函数调一进来,栈就会被覆盖,这个区域的字节分分钟变成乱码。这种“返回栈地址”的行为是未定义的。要安全返回,就得用malloc分配堆内存,或者由调用方传入缓冲区。

所以分配内存本质上是建立“所有权关系”:这块内存归你管,它的生命周期由你决定,什么时候释放也由你决定。不分配,你只是一个路过借光的指针,数据死活看运气。

2.2 可写性:只读区不能做拼接、修改

再看另一个常见场景,很多人初始化指针时直接指向字符串字面量:

char *p = "hello"; p[0] = 'H'; // 可能导致崩溃

"hello"在 C 标准里是字符串字面量,它属于只读区(实践中通常在只读数据段或者.rodata)。指针p只是拿到了它的首地址。编译器允许你用char *p = "hello"这种写法,纯粹是为了兼容历史,但并不意味着你可以通过p修改数组内容。标准里写明:如果试图修改由字符串字面量指向的数组,行为是未定义的。在 Windows / Linux 上,往往就是段错误;在一些嵌入式平台上,可能不报错但实际改的是 RAM 镜像,回了家数据又变回去,折腾半天。

如果你希望字符串是可以修改的,必须有一块可写内存。栈数组char arr[] = "hello";会复制一份到栈上,可写,但生命周期受限于作用域;堆内存char *s = malloc(6); strcpy(s, "hello");可写且生命周期较长。只有当你需要一块“借来的只读字符串”时才可以直接指向字面量,否则分配是硬需求。

2.3 终止符:char[len + 1]的+1为什么不能省

大多数新手不会漏掉“分配 len 个字符”,但容易漏掉+1。比如从输入中读取了 4 个字符abcd,你分配malloc(4)。然后把abcd拷进去。这时内存布局是:

a b c d ? ?

因为你没有写'\0',这块内存的第 5 个字节是什么完全未知。printf("%s", p)一开始读a b c d,然后继续读第 5 个字节、第 6 个字节……直到某处碰巧是 0 为止。实际输出就会变成abcd后面跟一长串垃圾字符,或者直接越界访问崩溃。

char[len + 1]里的+1就是为了强制在这个字符串末尾留出一个位置,写入'\0'。这是 C 字符串的基本契约:凡是使用%s、字符串函数操作的缓冲区,必须以'\0'结尾。漏掉它,就是拿程序的稳定性和安全性开玩笑。据我经验,很多缓冲区溢出漏洞的根源,就是开发者在计算长度时忘了给终止符留一个字节。

补充一个细节:如果你用calloc而不是malloc,分配出来的内存会初始化为 0,那么即使你只分配了len个字节,把abcd拷进去后,因为calloc会把多余区域清 0,第 5 个字节恰好是 0,暂时不会出问题。但这只是侥幸——如果你继续往里面写入更多数据,或者代码路径变了,0 就会被覆盖掉。所以我一直强调:不要把未定义行为的巧合当成正确写法。


3. 核心实操:什么时候需要分配、什么时候可以直接指向

很多人看完前面对比会觉得“搞指针真麻烦,干脆全部用数组算了”。其实没必要。我们把场景归类,就知道该怎么做。

3.1 可以用数组代替分配的常见情况

在栈上定义一个大小确定的数组,是最简单的方式:

char buf[32]; snprintf(buf, sizeof(buf), "hello, %s!", name);

只要你确认字符串长度不会超过31(留一个给'\0'),这种写法完全不需要手动分配内存。编译器自动维护生命期,自动回收,代码也短。缺点是大小固定:如果实际数据超长了,snprintf会截断,不会越界,但得到的结果不符合预期。

如果你需要经常修改字符串内容,或需要使用标准字符串函数,用栈数组挺好的。

3.2 需要堆内存(malloc)的场景清单

下面这四种情况必须用malloc/calloc分配,不能用裸指针瞎整:

  1. 输入长度未知,需要按实际长度动态分配:例如读取一行用户输入,长度可能几十也可能几千,栈上开固定数组要么浪费要么不够。正确做法是先读入一个临时大缓冲,得到长度后再精确分配malloc(len + 1)。

  2. 函数返回动态生成字符串:比如把两个字符串拼接成一个新字符串,返回给调用方使用。在函数内部用栈数组无法返回(栈内存被回收),必须用堆。

  3. 字符串大小随着循环或运行过程不断变化:比如逐步构建一个超长 XML,长度不确定,不可能提前开一个巨大栈数组。

  4. 需要长时间保持数据的缓冲区:某些数据要在程序运行期间一直存在,且可能被多个函数共享,堆内存可以绕过作用域限制。

3.3 字符串字面量到底算不算“分配”

有一种情况不需要调用malloc:char *p = "hello";。这其实也是“分配”了内存的——程序加载时,运行时环境把"hello\0"放到了只读数据段,你在概念上可以认为系统为你分配了一块静态内存,只是不给你修改权限。所以“不分配内存”的说法严格来说不准确。更准确的是你没有在栈或堆上显式分配一块可读写的内存,但你拥有的只是一个指向静态只读区的指针。

这样做的好处是简单高效,但它只适用于“读取、比较、传参”,不适用于“修改、拼接、覆盖”。如果后续要用strcat往这个指针后面追加内容,就傻了:那块只读区后面不一定是你的地盘,写了就会碰壁。

3.4 动态分配的正确姿势:一个可控标准流程

我平时处理动态字符串的基本套路是这样,代码不难,但每步都有讲究:

#include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { char tmp[256]; printf("请输入一段文字(不超过255字符): "); if (fgets(tmp, sizeof(tmp), stdin) == NULL) { return -1; } // 去掉可能残留的换行符 tmp[strcspn(tmp, "\n")] = '\0'; size_t len = strlen(tmp); // 关键:len + 1 留出终止符的位置 char *p = malloc(len + 1); if (p == NULL) { fprintf(stderr, "内存分配失败\n"); return -1; } strcpy(p, tmp); printf("动态字符串为: %s\n", p); free(p); // 用完释放,避免内存泄漏 return 0; }

解释几个点:

  • fgets会读取sizeof(tmp)-1个字符,并在末尾自动加'\0',所以我不会用gets,那是禁区。
  • strcspn(tmp, "\n")找到第一个换行的位置并替换成'\0',把末尾的换行符去掉。
  • 计算strlen(tmp)得到实际内容长度,然后malloc(len + 1),多一个字节给终止符。
  • strcpy(p, tmp)时会把tmp的内容连同终止符一起复制过去,安全性上没问题,因为空间足够。
  • 用完一定free(p),否则长期运行时内存泄漏。

这套流程简单,但背后藏着 C 语言关于字符串的完整约定。理解每一步为什么这样做,比背代码有用得多。


4. 探索一个容易混淆的问题:char * 到底和数组差在哪

很多新手会把char *p和char arr[]当成一回事,其实这两个东西有本质区别,而下面这段是理解“为什么必须分配内存”的最后一块拼图。

4.1 数组和指针的底层差异

数组是内存中的一个真实实体,它在声明时编译器就分配了连续空间,数组名本身是一个固定地址(不可重新赋值的常量指针概念)。而指针只是一个变量,它的值可以被随意更改为别的地址。看这两句:

char arr[10] = "hello"; char *p = "hello";

arr占 10 个字节,每一个字节都在栈上(若在函数外就是数据段),arr这个名字会被编译器转换为该数组首元素的地址,这个地址在生命周期内不变。p是什么?它本身有 8 字节(64 位系统上),存放的是指向字符串字面量的地址。如果你执行p = "world";,没问题,把p从指向"hello"的地址改为指向"world"的地址;但如果你执行arr = "world";,编译直接报错,因为arr是固定地址,不能重新赋值。

另外,sizeof(arr)返回整个数组的大小(这里就是 10),而sizeof(p)只返回一个指针的大小(通常是 8)。这直接解释了为什么数组和指针不能混着用还能全身而退。

4.2 数组自动分配 vs 指针需要手动分配

调用函数时,如果形参是char arr[],在函数内部本质上传的是指针(数组会“退化”为指针)。如果你在子函数里写了sizeof(arr),得到的是指针大小而不是数组大小。这也是为什么主函数里可以char buf[100];然后直接scanf("%99s", buf);,但如果你定义一个char *buf;,就必须先malloc(100)。前者的空间是编译器“顺手”给你分配的,后者是一张白纸,你要自己把“空间”画出来。

所以在讨论“指针指向首地址是否可以输出字符串”这个问题时,真正的答案是:

输出字符串只需要一个首地址,这个首地址本身不需要你用 malloc 来“制造”——但它所指向的字符串内容必须存在于某块合法的内存中。如果你的首地址指向一个自己创建的数组、或者指向一个动态分配的堆空间,那就是安全的。如果只是凭空声明一个指针变量就往里面填地址,那就完全不可靠。

换句话说,“分配内存”并不是为了“让指针能指”,而是为了“让被指的地址有东西,且那块东西归你支配”。


5. 从崩溃乱码到内存泄漏:五个真实排查经验与建议

这部分写点实战中经常踩的坑。我在帮别人调试 C 程序时,遇到的字符串问题翻来覆去就是那几类,这里整理成速查表,遇到相似问题可以逐条对照。

5.1 为什么scanf("%s", p)会段错误

这是最典型的错误。char *p; scanf("%s", p);崩溃,因为p没有初始化。解决办法要么char p[100];,要么char *p = malloc(100);然后scanf("%99s", p)。顺带提醒:scanf("%s", ...)本身就不安全,因为它不限制接收长度,如果用户输入超过 99 个字符,照样越界。推荐用fgets或scanf("%99s")。

5.2 输出乱码,末尾多了一堆奇怪字符

这种多半是缺'\0'。比如:

char *p = malloc(4); strncpy(p, "abcd", 4); printf("%s\n", p);

输出会变成abcd+ 垃圾。因为只拷了 4 个字节,把a b c d填进去,没有终止符。正确分配是malloc(5),然后strncpy(p, "abcd", 4); p[4] = '\0';。如果用的是strcpy,它会自动带上终止符,但你仍然需要多申请 1 个字节,否则写入终止符时就已经越界了。

5.3 函数返回局部数组地址,输出时正常,几分钟后就乱码

下面代码可能在某些教科书例题里见过:

char* get_str() { char s[] = "hello"; return s; // 错 }

s是函数内的局部数组,函数返回后这块栈空间已经无效。虽然第一次输出时可能还残留着数据,但只要再调用任何一个函数,栈帧重建,数据就会被覆盖。解决方法是让调用方传入缓冲区:

void get_str(char *dst, int size) { snprintf(dst, size, "hello"); }

或者函数内部用malloc,返回堆指针,由调用方释放。

5.4 申请内存但忘了释放

动态字符串用起来方便,但带来的代价就是你得记得free。在长时间运行的程序里,如果循环里疯狂malloc但不free,内存占用越拉越高,最后程序被系统杀掉。排查内存泄漏可以用一些工具,比如在开发环境开启 AddressSanitizer,或者在 Linux 下用 Valgrind(强调:这只是开发调试工具,不是代理)。写动态字符串时,我习惯在分配之后立刻想好释放时机,最好保持成对的malloc/free,不要中途到处return导致漏释放。

5.5char[len + 1]中的len到底怎么算

len不是用户肉眼可见字符数就够了,要区分字节数和字符数。如果处理的是多字节编码(比如 UTF-8 中文),一个汉字可能占 3 个字节,strlen返回的是字节数而不仅是字符个数。所以malloc(strlen(src) + 1)一定够用,因为strlen就是从头数到'\0'的字节长度,加 1 装上终止符正好。

如果要拼接两个字符串,新长度是strlen(s1) + strlen(s2) + 1,多出来的 1 是终止符位置。别写成分别各自加 1,否则就多分配了一个无用字节,造成内存浪费,虽然不大但养成好习惯更好。正确的计算方式:

char *s = malloc(strlen(a) + strlen(b) + 1); strcpy(s, a); strcat(s, b);

这里需要说清楚的是,strcat会从s的原终止符位置开始追加b,它要求s本身必须以'\0'结尾,而且空间足够装下b的内容加上新的终止符。所以strcpy之后,s已经有一个合法终止符了,strcat会找到它并覆盖,最后在b结束后写入新的'\0'。

5.6 再谈static数组返回值:一种折中方案

有人问过:是不是非得malloc?其实还有一种常见方案是使用static局部数组:

char* get_str() { static char s[64]; snprintf(s, sizeof(s), "hello"); return s; }

s是静态存储期,函数返回后依然有效,不需要malloc。代价是它不是线程安全的,而且每次调用都是同一块缓冲区,第二次调用会覆盖第一次返回的内容。作为轻量级场景的替代方案可以用,但不推荐在并发环境下用。


6. 最后的几条经验总结

从一个只会“指向首地址就输出”的坑里跳出来,我花了不少时间。现在回头看看,真正想通的是三件事:

第一,指针只是地址的载体,不是空间的提供者。你要的数据必须真实存在于某个地址里,且要保证访问权限和数据生命周期都正确,才能稳定输出。char[len + 1]的本质是亲手创建一块可控空间,把字符串数据放进去,顺便在末尾留一个'\0'的门牌标记。

第二,字符串是内存布局加上终止符约定的结果。一块内存不叫字符串,内存里连续字节并在末尾出现'\0',你看它才像一个字符串。所以字符串函数的输入参数必须指向这样一个“带终止符的缓冲区”。只有地址但没有内容、只有内容但没有终止符,都会出问题。

第三,能偷懒不分配内存的前提是,你清楚知道偷懒指向的内存真实存在。指向字符串字面量不叫做不安全,那是只读静态存储区;指向一个栈上的数组,也没问题,只要你保证作用域还在;指向另一个堆对象,同样可以,只要你保证不被提前释放。我在实际开发中最常用的判断标准是:你这个字符串数据需要存活多久?谁拥有它?中间你会不会修改它?问清这三个问题,选哪条路就顺理成章。

如果你还在初学阶段,建议亲手写一遍动态分配字符串、手动释放、再跑一下 Valgrind / AddressSanitizer 检查,多试试越界读、忘记释放等错误场景,踩过坑就知道这些约定不是绕弯子,而是帮你防未定义行为的安全带。

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

Java工程师24周转型Agent Engineer:完整学习路线与避坑指南

做Java后端做了六七年&#xff0c;本来觉得自己就在“写接口、调存储、薅中间件”这条路上走到黑了&#xff0c;结果这两年AI应用的需求一冒出来&#xff0c;身边好几个Java同事陆续转去做了Agent方向&#xff0c;薪资涨幅都不小。很多人问我&#xff1a;Java工程师转Agent Eng…

作者头像 李华
网站建设 2026/10/13 18:54:26

MATLAB中SVM图像分割实战:小样本高维特征下的像素级分类方法

简介&#xff1a;本资源是一份基于支持向量机&#xff08;SVM&#xff09;实现图像分割的MATLAB完整实践方案&#xff0c;面向数字图像处理初学者、机器学习入门者及计算机视觉课程设计学生&#xff0c;解决颜色对比明显场景下的目标区域自动提取与可视化抠图问题。压缩包共8个…

作者头像 李华
网站建设 2026/10/13 18:53:41

Qt/QML动态语言切换全指南:从ts/qm到界面实时刷新

简介&#xff1a;这是一份面向Qt/Qml开发者的多国语言动态翻译示例资源&#xff0c;重点解决跨地域应用中语言切换与汉化需求&#xff0c;覆盖Qt Widgets与Qml两大主流界面技术&#xff0c;适合需要为桌面应用或嵌入式界面增加多语言支持的开发者。压缩包共16个文件&#xff0c…

作者头像 李华
网站建设 2026/10/13 18:51:44

YOLOv3行人检测实战:VOC2012数据抽取、锚框聚类与训练调优

1. 项目缘起与整体设计思路行人检测这个方向&#xff0c;我在实际项目里接触过不少次&#xff0c;从最早的 HOGSVM 一路做到现在的深度学习方案&#xff0c;踩过的坑可以说能写满一个笔记本。这次要聊的是用 YOLOv3 来训练一个专门的行人检测模型&#xff0c;数据来源是从 VOC2…

作者头像 李华
网站建设 2026/10/13 18:49:02

litellm实战:开源大模型API网关如何统一供应商接入与成本管控

1. 为什么前端我都需要一个"大模型接入中间层"1.1 多个供应商接入的混乱现状先从我最近遇到的实际问题说起。我的一个后端服务原本只接了某一家模型API&#xff0c;代码里用一套SDK就能完成调用。后来需求变多&#xff0c;要同时接上开源模型托管平台、云厂商原生模型…

作者头像 李华
网站建设 2026/10/13 18:46:21

LiteLLM 大模型 API 网关实战:统一多模型接入、路由与成本管控

如果你同时对接过两家以上大模型厂商的 API&#xff0c;大概率撞见过同一个尴尬场景&#xff1a;相同的请求逻辑&#xff0c;换一家服务商就得重写一遍接入层。请求体格式不一样、鉴权方式不一样、返回结构也不一样&#xff0c;连报错信息都各说各话。我在某次内部项目改造里被…

作者头像 李华