10.C 语言内存划分,static,字符串
字符串
C语言中,字符串实际上是以空字符 \0
结尾的字符数组。这意味着一个字符串不仅仅包含可见字符,还必须包含一个终止符(\0
),这标志着字符串的结束。这种表示方法使得处理字符串既灵活又具有一定的复杂性。
字符串的定义
常量字符串:用双引号括起来的一组字符。
char *str = "Hello, World!";
这里
str
是一个指向字符串字面量的指针,该字符串存储在只读内存区域。字符数组:用于存储可修改的字符串。
char str[] = "Hello, World!";
或者明确指定大小:
char str[14] = "Hello, World!";
注意这里的大小为 14,因为包括了结尾的
\0
。
字符串的操作
C语言标准库提供了丰富的字符串处理函数,位于 <string.h>
头文件中:
strlen:计算字符串长度(不包括末尾的
\0
)。size_t len = strlen("Hello");
strcpy 和 strncpy:复制字符串。
strncpy
功能:与
strcpy
类似,但是增加了额外的参数n
,表示最多复制的字符数。如果源字符串长度小于n
,则在目标字符串后面填充足够的空字符\0
直到总共写了n
个字符;如果源字符串长度大于或等于n
,则不会在目标字符串末尾添加终止空字符\0
,这意味着结果可能不是一个有效的C字符串。char dst[20]; strcpy(dst, "Hello"); // 完全复制 strncpy(dst, "Hello", 5); // 最多复制前5个字符
strcat 和 strncat:连接字符串。
char greeting[20] = "Hello, "; strcat(greeting, "World!"); // 将"World!"追加到greeting后
strcmp 和 strncmp:比较两个字符串。
int result = strcmp("abc", "def"); // 如果第一个参数小于第二个参数,则返回负数
strstr:查找子串首次出现的位置。
char *pos = strstr("Hello, World!", "World");
C 语言内存的几个主要区域
栈 (Stack)
- 用途:存储函数的局部变量(包括基本数据类型和数组)、函数参数、函数调用的返回地址和控制信息。
- 管理:由编译器自动管理。当函数被调用时,其局部变量和相关信息被压入栈;当函数返回时,这些数据被自动弹出并释放。
- 特点:访问速度快,空间有限,遵循后进先出(LIFO)原则。
堆 (Heap)
- 用途:用于动态内存分配。程序员使用
malloc
、calloc
、realloc
等函数在此区域分配内存,并使用free
函数显式释放。 - 管理:由程序员手动管理。如果分配的内存没有被释放,就会导致内存泄漏。
- 特点:空间相对较大,分配和释放速度比栈慢。
- 用途:用于动态内存分配。程序员使用
全局/静态存储区 (Global/Static Storage Area)
- 这个区域通常进一步细分为两个部分:
- 已初始化的数据段 (Initialized Data Segment / .data):
- 用途:存储程序中已初始化的全局变量和静态变量(包括
static
全局变量和static
局部变量)。 - 特点:在程序启动时分配,在程序结束时释放。
- 用途:存储程序中已初始化的全局变量和静态变量(包括
- 未初始化的数据段 (Uninitialized Data Segment / .bss - Block Started by Symbol):
- 用途:存储程序中未初始化的全局变量和静态变量(编译器会自动将它们初始化为 0)。
- 特点:在程序启动时分配,在程序结束时释放。
- 已初始化的数据段 (Initialized Data Segment / .data):
- 这个区域通常进一步细分为两个部分:
常量存储区 (Constant Storage Area / .rodata - Read-Only Data)
- 用途:存储程序中的常量数据,例如字符串字面量(String Literals)、用
const
修饰的全局或静态常量(某些实现可能放在这里,也可能放在 .data 段,但字符串字面量几乎总是在 .rodata)。 - 特点:通常位于只读内存区域,程序试图修改这里的值会导致未定义行为(通常是运行时错误,如段错误)。
- 用途:存储程序中的常量数据,例如字符串字面量(String Literals)、用
代码区 (Code Segment / Text Segment)
- 用途:存储程序的可执行指令(机器码)。
- 特点:通常是只读的,以防止程序意外修改自身的指令。
字符串字面量存在哪个区域?
字符串字面量(例如 "Hello, World!"
)存储在常量存储区(.rodata 段)。
- 为什么? 字符串字面量是程序的一部分,在编译时就已经确定了内容。将它们放在只读的常量区可以防止程序意外修改它们,并且可以在多个地方安全地共享同一个字符串字面量的实例。
- 示例:
char *str1 = "Hello"; char *str2 = "Hello"; // str1 和 str2 可能指向同一个内存地址
"Hello"
这个字符串字面量被存储在.rodata
段。str1
和str2
都是指向这个只读内存区域的指针。尝试修改str1[0] = 'h';
会导致未定义行为。
动态分配字符串
动态分配字符串意味着你需要使用标准库函数 malloc
、calloc
或 realloc
来在运行时为字符串分配内存。这种方法允许你在程序执行期间根据实际需求调整字符串的大小,而不是在编译时就固定下来。
#include <stdio.h>
#include <stdlib.h>int main() {char *str;// 分配内存用于存储字符串 "Hello, World!"str = (char *) malloc(14 * sizeof(char)); // 13个字符 + 1个'\0'if (str == NULL) { // 检查是否分配成功fprintf(stderr, "内存分配失败\n");exit(1);}// 复制字符串到新分配的内存strcpy(str, "Hello, World!");printf("%s\n", str);// 释放内存free(str);return 0;
}
这里我们使用了 malloc
函数来分配足够的内存以容纳指定的字符串,并通过 strcpy
将字符串复制到新分配的内存位置。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>int main() {char *str = (char *) malloc(5 * sizeof(char)); // 初始分配5个字符的空间if (str == NULL) {fprintf(stderr, "内存分配失败\n");exit(1);}strncpy(str, "Hi!", 4); // 包含终止符'\0'printf("Initial string: %s\n", str);// 重新分配更多空间str = (char *) realloc(str, 14 * sizeof(char)); // 现在有足够的空间存储 "Hello, World!"if (str == NULL) {fprintf(stderr, "内存重新分配失败\n");exit(1);}strcpy(str, "Hello, World!");printf("Extended string: %s\n", str);free(str);return 0;
}
注意:当使用 realloc
时,如果它不能分配请求的额外内存,则返回 NULL
并保持原内存块不变。因此,在重新分配之后检查返回值是很重要的,以避免丢失原始数据。
注意事项
- 初始化:动态分配的内存不一定被初始化为零或空字符。如果你打算立即使用这块内存作为字符串,请确保它以
\0
结尾。 - 内存泄漏:每次调用
malloc
、calloc
或realloc
后,都应该有相应的free
调用来释放分配的内存,否则会导致内存泄漏。 - 越界访问:确保不会超出分配的内存范围写入数据,这可能会导致未定义行为或程序崩溃。
静态局部变量
当 static
关键字用于局部变量声明时,它改变了该变量的生命周期但不改变其作用域。具体来说:
- 生命周期:静态局部变量在整个程序运行期间都存在(即从程序开始到结束),而不是仅仅在其所在的代码块执行期间存在。
- 初始化:静态局部变量只会在第一次进入其所在的代码块时被初始化一次,并且如果没有显式初始化,会被自动初始化为0(对于数值类型)或NULL(对于指针类型)。
- 作用域:尽管其生命周期是整个程序运行期间,但它的作用域仍然局限于声明它的代码块内。
void func() {static int count = 0; // 只有第一次调用func时才会初始化为0count++;printf("%d\n", count);
}
每次调用 func()
函数时,count
的值都会增加,而不是重新从0开始计数。
int main() {func();func();return 0;
}
数字转换为字符串
使用 sprintf
或 snprintf
sprintf
和 snprintf
函数可以用来格式化输出到一个字符数组中,非常适合用于数字到字符串的转换。
sprintf
:不检查目标缓冲区大小,可能导致缓冲区溢出。snprintf
:更安全的选择,允许指定最大写入字符数(包括终止空字符)。
#include <stdio.h>int main() {char buffer[10]; // 确保有足够的空间int num = 12345;// 使用 sprintfsprintf(buffer, "%d", num);printf("Using sprintf: %s\n", buffer);// 使用 snprintf 更加安全snprintf(buffer, sizeof(buffer), "%d", num);printf("Using snprintf: %s\n", buffer);return 0;
}
使用 itoa
(非标准)
尽管一些编译器支持 itoa
函数用于整数到字符串的转换,但它不是标准 C 库的一部分,因此移植性较差。
#include <stdio.h>int main() {char buffer[10];int num = 12345;itoa(num, buffer, 10); // 将整数num转换为十进制字符串存入bufferprintf("Using itoa: %s\n", buffer);return 0;
}
注意:由于 itoa
不是标准函数,使用时需谨慎。
字符串转换为数字
C语言提供了几个函数用于将字符串转换为各种类型的数字值:
使用 atoi
, atol
, atof
atoi
: 将字符串转换为int
类型。atol
: 将字符串转换为long
类型。atof
: 将字符串转换为double
类型。
这些函数简单易用,但不会报告转换错误。
#include <stdio.h>
#include <stdlib.h>int main() {const char *strInt = "12345";const char *strFloat = "123.45";int intValue = atoi(strInt);double floatValue = atof(strFloat);printf("Integer value: %d\n", intValue);printf("Float value: %.2f\n", floatValue);return 0;
}
使用 strtol
, strtoul
, strtod
对于需要更多控制的情况,比如想要检测转换错误或处理基数不同的数字(如二进制、八进制、十六进制),可以使用以下函数:
strtol
: 将字符串转换为长整型,并提供更多的错误检查功能。strtoul
: 类似于strtol
,但是结果是无符号长整型。strtod
: 将字符串转换为双精度浮点数。
#include <stdio.h>
#include <stdlib.h>int main() {const char *strInt = "12345";char *end;long int li = strtol(strInt, &end, 10); // 10表示十进制if (*end != '\0') { // 检查是否全部转换成功printf("Conversion error!\n");} else {printf("Converted integer: %ld\n", li);}return 0;
}
这些函数不仅能够进行转换,还能通过 end
参数返回第一个无法被转换的字符位置,从而帮助判断转换是否完全成功。
总结
无论是将数字转换为字符串还是反之,选择合适的方法取决于你的具体需求。对于简单的场景,sprintf
/snprintf
和 atoi
系列函数通常足够了;而对于需要更多控制或者更高的安全性要求的场合,则应该考虑使用 strtol
、strtoul
和 strtod
等函数。