一、基本概念
char **str 在 C 语言中表示"指向字符指针的指针",通常被称为双重指针或指针的指针。
char **str;
这个声明可以理解为:
- str 是一个指针变量(它的值是地址)
- str 指向一个 char* 类型的指针变量
- 而char* 类型的变量指向一个字符(或字符串)
- str 的值是一系列指针数组的首地址
二、内存布局
内存地址 存储的内容 指向的地址 说明
0x1000 0x2000 str[0] 指向第一个字符串
0x1008 0x2006 str[1] 指向第二个字符串
0x1010 0x200c str[2] 指向第三个字符串
0x1018 0x2010 str[3] 指向第四个字符串
0x2000 'h' 'e' 'l' 'l' 'o' '\0' 字符串0 (索引0)
0x2006 'w' 'o' 'r' 'l' 'd' '\0' 字符串1 (索引1)
0x200c 'f' 'o' 'o' '\0' 字符串2 (索引2)
0x2010 'b' 'a' 'r' '\0' 字符串3 (索引3)
关键点:
- str 本身占用 8 字节(64位系统),存储的是指针数组的首地址即:0x1000
- 指针数组的每个元素(str[0], str[1]...)各占 8 字节,存储的是字符串的首地址
- 字符串存储在内存的其他位置(堆区或常量区)
指针数组是连续的 :0x1000、0x1008、0x1010、0x1018 每个相隔 8 字节
每个指针存储对应字符串的起始地址:
str[0] 存 0x2000("hello" 的地址)
str[1] 存 0x2006("world" 的地址)
str[2] 存 0x200c("foo" 的地址)
str[3] 存 0x2010("bar" 的地址)
字符串在内存中连续存放,但这不是必须的,只是为了图示方便
三、3种常见使用场景
场景1:指向指针数组(最常用)
// 方式1:指向栈上的指针数组
char *arr[] = {"hello", "world", "foo", "bar"};
char **str = arr; // str 指向数组的第一个元素
// 访问方式
printf("%s\n", str[0]); // 输出: hello
printf("%c\n", str[0][0]); // 输出: h
printf("%c\n", *str[0]); // 输出: h
场景2:动态二维数组
// 分配一个包含 3 个字符串指针的数组
char **str = malloc(3 * sizeof(char*));
// 为每个指针分配字符串空间
str[0] = malloc(6); // "hello" 需要 6 字节(含 '\0')
str[1] = malloc(6); // "world"
str[2] = malloc(4); // "foo"
// 复制字符串
strcpy(str[0], "hello");
strcpy(str[1], "world");
strcpy(str[2], "foo");
// 使用完后释放
for (int i = 0; i < 3; i++) {
free(str[i]); // 先释放每个字符串
}
free(str); // 再释放指针数组
场景3:函数参数(修改指针本身)
void allocate_string(char **str) {
// *str 就是调用者传入的指针变量
*str = malloc(100);
strcpy(*str, "Hello from function");
}
int main() {
char *s = NULL;
allocate_string(&s); // 传入指针的地址
printf("%s\n", s); // 输出: Hello from function
free(s);
return 0;
}
char *b[3] 和 char **c的区别:
- char *b[3]:编译时确定大小的指针数组,栈上分配 3 个指针的空间
- char **c:运行时动态分配的指针数组,堆上可以分配任意数量
例如:
char **c = malloc(10 * sizeof(char*)); 时来动态指定开辟多少个指针数组
关键区别在于内存分配时机和位置:
- b 的空间在编译时确定(sizeof(b) = 24);
- c 只是一个指针,空间在运行时通过 malloc 决定;
四、访问方式的等价写法
char **str;
char *arr[] = {"hello", "world", "foo"};
str = arr;
// 以下访问方式完全等价
str[0] // 第一个字符串的地址
*(str + 0) // 同上
str[0][0] // 第一个字符串的第一个字符
*(*(str + 0)) // 同上
*str[0] // 同上
str[1] // 第二个字符串的地址
str[1][2] // 第二个字符串的第三个字符
五、二维数组 vs 指针数组
char a[3][10] = {"hello", "world", "foo"}; // 二维数组:连续内存,每行固定10字节
char *b[3] = {"hello", "world", "foo"}; // 指针数组:指针连续,字符串不一定连续
char **c = b; // 双重指针 动态开辟
三种字符串数组的内存存储详解
- char a[3][10]------ 二维数组(连续内存块)
内存布局:
地址 内容
0x1000 'h' 'e' 'l' 'l' 'o' '\0' . . . . (10字节)
0x100a 'w' 'o' 'r' 'l' 'd' '\0' . . . . (10字节)
0x1014 'f' 'o' 'o' '\0' . . . . . . . . (10字节)
特点:
1.一整块连续内存:3行 × 10字节 = 30字节连续存储
2.每行固定 10 字节,未使用的位置填充为 0
3.字符串存储在数组自己的空间内(栈上或静态区)
4.浪费空间("foo" 只用 4 字节,却占 10 字节)
5.访问速度快(直接偏移计算:a[1][2] = *(0x1000 + 1×10 + 2))
printf("%c\n", a[1][2]); // 'r'(world 的第 3 个字符)
printf("%s\n", a[1]); // "world"
2. char *b[3] ------ 指针数组(指针连续,字符串分离)内存布局:
指针数组(连续): 字符串(可能不连续):
地址 内容(指向)
0x2000 0x3000 → 0x3000 'h' 'e' 'l' 'l' 'o' '\0'
0x2008 0x3006 → 0x3006 'w' 'o' 'r' 'l' 'd' '\0'
0x2010 0x300c → 0x300c 'f' 'o' 'o' '\0'
特点:
1.指针数组连续存储(3个指针 × 8字节 = 24字节)
2.字符串存储在其他地方(常量区,通常是只读的)
3.不浪费空间(每个字符串恰好占用所需长度 + 1)
4.可以指向不同长度的字符串
5.字符串常量通常不可修改(如果试图修改会崩溃)
printf("%c\n", b[1][2]); // 'r'(通过指针找到字符串)
b[1][0] = 'W'; // 可能崩溃(字符串常量只读)
3. char **c = b 双重指针(指向指针数组)内存布局:
c (char**) 指针数组 字符串
0x4000→ 0x2000 0x3000→ 'h' 'e' 'l' 'l' 'o' '\0'
0x2008 0x3006→ 'w' 'o' 'r' 'l' 'd' '\0'
0x2010 0x300c→ 'f' 'o' 'o' '\0'
特点:1.c 本身是一个指针变量(8字节),存储在某个地方
2.c 的值是 0x2000(指针数组的首地址)
3.通过 c 可以间接访问整个指针数组
4.常用于函数参数(传递字符串数组)
printf("%c\n", c[1][2]); // 'r'(和 b[1][2] 完全一样)
printf("%s\n", c[2]); // "foo"
六、常见错误
错误1:未初始化就使用
cchar **str;
*str = "hello"; // str 指向随机地址,危险!
错误2:混淆指针和内容
char **str = malloc(3 * sizeof(char*));
strcpy(str[0], "hello"); // str[0] 未分配空间!
错误3:内存泄漏
char **str = malloc(3 * sizeof(char*));
str[0] = malloc(10);
// 使用后只 free(str) str[0] 泄漏!