1 引言
在C语言中,字符串可以通过两种方式定义:
c
char str1[] = "Hello"; /* 字符数组 */
char *str2 = "Hello"; /* 字符指针 */
这两种方式看起来都能表示字符串 "Hello",但它们有着本质的区别。下面的代码就能揭示差异:
c
#include <stdio.h>
int main(void)
{
char str1[] = "Hello";
char *str2 = "Hello";
str1[0] = 'h'; /* 可以修改 */
printf("str1: %s\n", str1);
/* str2[0] = 'h'; */ /* 危险!可能导致程序崩溃 */
printf("str1 大小: %zu\n", sizeof(str1)); /* 6(包含'\0') */
printf("str2 大小: %zu\n", sizeof(str2)); /* 8(指针大小) */
return 0;
}
本章我们将深入探讨这两种方式的区别,以及它们在实践中的应用。
2 字符数组与字符指针的本质区别
2.1 存储位置不同
字符数组 :在栈上分配内存,数组的内容可以修改。
字符指针:指针变量本身在栈上,但它指向的字符串常量通常在只读数据区。
c
#include <stdio.h>
int main(void)
{
char str1[] = "Hello"; /* 栈上的数组,包含副本 */
char *str2 = "Hello"; /* 栈上的指针,指向只读区 */
printf("str1 地址: %p\n", str1); /* 栈地址 */
printf("str2 地址: %p\n", str2); /* 只读区地址 */
printf("&str2 : %p\n", &str2); /* 指针本身的地址(栈上) */
return 0;
}
内存布局示意图:
text
栈区:
str1: [H][e][l][l][o][\0] (可修改)
str2: [0x400600] (指针变量)
只读数据区:
0x400600: "Hello" (不可修改)
2.2 可修改性不同
c
char str1[] = "Hello";
str1[0] = 'h'; /* 合法:修改数组中的字符 */
str1 = "World"; /* 非法:数组名是常量,不能赋值 */
char *str2 = "Hello";
str2[0] = 'h'; /* 危险!试图修改只读区,可能崩溃 */
str2 = "World"; /* 合法:修改指针指向 */
总结:
-
字符数组:内容可改,但数组名本身不能改
-
字符指针:指针本身可改(可指向别处),但指向的内容是否可改取决于指向哪里
2.3 sizeof 的结果不同
c
char str1[] = "Hello";
char *str2 = "Hello";
printf("%zu\n", sizeof(str1)); /* 6:整个数组的大小(包含'\0') */
printf("%zu\n", sizeof(str2)); /* 8:指针本身的大小(64位系统) */
2.4 取地址的结果不同
c
char str1[] = "Hello";
char *str2 = "Hello";
printf("%p\n", str1); /* 数组首元素地址 */
printf("%p\n", &str1); /* 也是数组首地址,但类型是 char (*)[6] */
printf("%p\n", str2); /* 指向的字符串常量的地址 */
printf("%p\n", &str2); /* 指针变量本身的地址 */
3 字符串常量
3.1 字符串常量的本质
字符串常量是用双引号括起来的字符序列,如 "Hello"。它的本质是一个无名的字符数组 ,存储在程序的只读数据区(某些平台也可能在代码段)。
c
const char *p = "Hello"; /* 指向只读区的字符串常量 */
3.2 字符串常量的共享
编译器通常会优化,相同的字符串常量可能共享同一份存储:
c
#include <stdio.h>
int main(void)
{
char *p1 = "Hello";
char *p2 = "Hello";
printf("p1 = %p\n", p1);
printf("p2 = %p\n", p2); /* 可能和 p1 相同 */
return 0;
}
3.3 试图修改字符串常量
c
char *p = "Hello";
p[0] = 'h'; /* 未定义行为!可能导致程序崩溃 */
在不同的平台上结果可能不同:
-
某些平台:程序崩溃(段错误)
-
某些平台:修改成功(如果字符串常量在可写段)
-
某些平台:修改看起来成功,但可能影响其他指向同一常量的指针
绝对不要修改字符串常量。
3.4 用 const 保护字符串常量
为了明确表达意图,应该用 const 修饰指向字符串常量的指针:
c
const char *p = "Hello"; /* 表明不能通过 p 修改指向的内容 */
/* p[0] = 'h'; */ /* 编译器会报错,而不是运行时崩溃 */
4 字符串的初始化
4.1 字符数组的初始化
c
/* 方式1:直接用字符串初始化 */
char s1[6] = "Hello"; /* 显式指定大小 */
char s2[] = "Hello"; /* 编译器自动计算大小(6) */
/* 方式2:字符数组初始化 */
char s3[] = {'H', 'e', 'l', 'l', 'o', '\0'}; /* 手动添加'\0' */
/* 注意:没有'\0'就不是字符串 */
char s4[] = {'H', 'e', 'l', 'l', 'o'}; /* 长度5,没有'\0',不是字符串 */
4.2 字符指针的初始化
c
/* 指向字符串常量 */
const char *p1 = "Hello"; /* 推荐:明确是只读 */
char *p2 = "Hello"; /* 不推荐:容易误以为可修改 */
/* 指向栈上的数组 */
char arr[] = "Hello";
char *p3 = arr; /* p3 指向可修改的数组 */
/* 动态分配内存 */
char *p4 = malloc(10);
strcpy(p4, "Hello"); /* p4 指向堆上的字符串 */
4.3 区别总结
| 特性 | 字符数组 | 字符指针 |
|---|---|---|
| 存储位置 | 栈(或静态区) | 指针在栈,指向只读区或堆 |
| 内容是否可改 | 可修改 | 取决于指向哪里 |
| 指针本身是否可改 | 否(数组名是常量) | 是 |
| sizeof | 数组大小 | 指针大小 |
| 初始化来源 | 拷贝字符串内容 | 存储字符串地址 |
5 指针数组处理字符串表
5.1 字符串表的概念
在实际编程中,经常需要处理一组字符串,如菜单选项、命令列表、星期名称等。指针数组是存储这类字符串表的理想方式。
c
/* 定义字符串表 */
const char *weekdays[] = {
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
};
/* 访问 */
for (int i = 0; i < 7; i++) {
printf("%s\n", weekdays[i]);
}
5.2 指针数组 vs 二维字符数组
方式1:指针数组
c
const char *days1[] = {
"Monday", "Tuesday", "Wednesday"
};
-
每个字符串长度可以不同
-
字符串存储在只读区,不可修改
-
指针数组本身占用的空间较小(每个指针8字节)
方式2:二维字符数组
c
char days2[][10] = {
"Monday", "Tuesday", "Wednesday"
};
-
每个字符串长度固定(10字节),浪费空间
-
字符串存储在数组内,可以修改
-
占用连续空间,访问可能更快
内存布局对比:
text
指针数组 days1:
[ptr] → "Monday"
[ptr] → "Tuesday"
[ptr] → "Wednesday"
二维数组 days2:
[Monday\0 ][Tuesday\0 ][Wednesday\0]
5.3 命令行参数 argv
main 函数的第二个参数 argv 就是一个经典的字符串表:
c
int main(int argc, char *argv[])
{
printf("程序名:%s\n", argv[0]);
for (int i = 1; i < argc; i++) {
printf("参数 %d:%s\n", i, argv[i]);
}
return 0;
}
argv 是一个指针数组,每个元素指向一个命令行参数字符串。
5.4 字符串表的遍历
c
#include <stdio.h>
int main(void)
{
const char *colors[] = {
"Red", "Green", "Blue", "Yellow", "Cyan", "Magenta", NULL
}; /* 用 NULL 作为结束标志 */
/* 方法1:通过下标遍历 */
for (int i = 0; colors[i] != NULL; i++) {
printf("%s ", colors[i]);
}
printf("\n");
/* 方法2:通过指针遍历 */
const char **p = colors;
while (*p != NULL) {
printf("%s ", *p);
p++;
}
printf("\n");
return 0;
}
6 字符串处理函数中的指针
6.1 标准库函数的指针参数
理解指针与字符串的关系,有助于理解标准库函数的设计:
c
size_t strlen(const char *s); /* 不修改字符串,用 const */
char *strcpy(char *dest, const char *src); /* dest 可修改,src 只读 */
char *strcat(char *dest, const char *src); /* 同上 */
int strcmp(const char *s1, const char *s2); /* 两者都只读 */
6.2 实现简单的字符串函数
c
#include <stdio.h>
/* 实现 strlen */
size_t my_strlen(const char *s)
{
const char *p = s;
while (*p != '\0') {
p++;
}
return p - s; /* 指针相减得到长度 */
}
/* 实现 strcpy */
char *my_strcpy(char *dest, const char *src)
{
char *ret = dest;
while ((*dest++ = *src++) != '\0') {
;
}
return ret;
}
/* 实现 strcmp */
int my_strcmp(const char *s1, const char *s2)
{
while (*s1 && *s2 && *s1 == *s2) {
s1++;
s2++;
}
return *s1 - *s2;
}
6.3 字符串指针作为返回值
c
/* 返回指向字符串常量的指针(安全) */
const char* get_weekday(int n)
{
static const char *weekdays[] = {
"Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday", "Sunday"
};
if (n >= 1 && n <= 7) {
return weekdays[n-1];
}
return "Invalid";
}
/* 返回指向局部数组的指针(危险!) */
char* get_string_bad(void)
{
char str[] = "Hello"; /* 局部数组 */
return str; /* 函数返回后数组被销毁 */
}
7 常见错误与注意事项
7.1 试图修改字符串常量
c
char *p = "Hello";
p[0] = 'h'; /* 未定义行为,可能导致崩溃 */
7.2 混淆字符数组和字符指针的初始化
c
char str1[10];
str1 = "Hello"; /* 错误!数组名不能被赋值 */
char str2[10];
strcpy(str2, "Hello"); /* 正确 */
char *p;
p = "Hello"; /* 正确:p 指向字符串常量 */
7.3 忘记为 '\0' 分配空间
c
char str[5] = "Hello"; /* "Hello" 需要6个字符(包含'\0')! */
/* 实际只存储了 'H','e','l','l','o',没有 '\0',不是有效字符串 */
7.4 返回局部字符数组的地址
c
char* get_name(void)
{
char name[20] = "Alice";
return name; /* 危险!name 是局部变量 */
}
7.5 用 == 比较字符串内容
c
char *p1 = "Hello";
char *p2 = "Hello";
if (p1 == p2) { /* 比较的是地址,不是内容!可能为真,但不能依赖 */
/* ... */
}
/* 应该用 strcmp(p1, p2) == 0 */
7.6 字符串常量不可修改,但指针本身可改
c
const char *p = "Hello";
p = "World"; /* 合法:修改指针指向 */
/* p[0] = 'h'; */ /* 非法:不能修改内容 */
8 本章小结
本章系统介绍了指针与字符串的关系:
1. 字符数组 vs 字符指针
| 特性 | 字符数组 | 字符指针 |
|---|---|---|
| 存储 | 栈/静态区 | 指针在栈,指向只读区或堆 |
| 内容可改 | 是 | 取决于指向 |
| 自身可改 | 否(常量) | 是 |
| sizeof | 数组大小 | 指针大小 |
2. 字符串常量
-
存储在只读数据区
-
相同的字符串常量可能共享
-
绝对不能修改
-
应该用
const char*指向
3. 字符串表的两种方式
-
指针数组:每个字符串长度可不同,不可修改,节省空间
-
二维数组:固定长度,可修改,占用连续空间
4. 实际应用
-
命令行参数
argv是指针数组 -
标准库字符串函数大量使用指针
-
实现自己的字符串函数需要理解指针操作
5. 常见错误
-
修改字符串常量
-
忘记数组和指针的区别
-
返回局部字符数组
-
用
==比较字符串内容 -
忘记为
'\0'留空间