trinity C programming 8,9,10
1. C语言字符串的存储
- 字符串在内存中的存储:C语言中,字符串以字符数组的形式存储在内存中,以\0(空字符)表示字符串的结束。
- 字符的存储方式:字符串中的每个字符都存储为其对应的ASCII码。例如,字符'1'的ASCII码是49。
- 字符串初始化:可以通过直接赋值的方式初始化字符串,例如 char mystr[] = "Hello World";。
- 直接赋值限制: 不可以直接对字符数组赋值,比如char mystr[11]; mystr = "Hello World";是无效的。然而,可以通过逐个字符赋值的方式实现,例如:char mystr[11]; mystr[0] = 'H'; mystr[1] = 'e';等。
- 字符串的输入输出:可以使用sprintf函数将数据格式化后存入字符串;使用sscanf函数从字符串中读取数据。
2. 从字符串中读取数字
2.1 简单介绍读取数字 及其 解析浮点数的函数
字符与整数的区别:字符 '1' 与整数 1 是不同的。当从标准输入(stdin)或文件中读取字符串时,如果需要将字符串解释为数字,就需要从字符串中解析出数字。
读取和解析方法:可以使用 fscanf 和 scanf 进行格式化输入,或使用 getline 读取到C字符串后再解析数字。
解析浮点数的函数:
atof:没有错误检查,但经常使用。sscanf:带有一些错误检查。strtof:错误检查最为健全,但可能需要额外的代码才能使用。
这三个解析浮点数的函数(atof、sscanf、strtof)在C语言中各有特点,具体介绍如下:
-
atof(ASCII to float):-
功能:将字符串转换为浮点数。
-
用法:
float num = atof(str); -
优点:使用简单,直接将字符串中的数字转换为浮点数。
-
缺点:没有错误检查。如果字符串不是合法的数字(例如包含非数字字符),
atof不会报错,可能返回0.0,这可能导致错误。 -
示例:
char str[] = "3.14"; float num = atof(str); printf("%f", num); // 输出:3.140000
-
-
sscanf:-
功能:可以将字符串格式化并解析为各种数据类型(包括浮点数)。
-
用法:
int result = sscanf(str, "%f", &num); -
优点:带有基本的错误检查功能。返回值为成功匹配的项数(例如成功解析一个浮点数会返回1),这可以用来判断转换是否成功。
-
缺点:相比
atof稍微复杂一点,需要指定格式符,但在需要格式化解析多个变量时更灵活。 -
示例:
char str[] = "3.14"; float num; int result = sscanf(str, "%f", &num); if (result == 1) { printf("%f", num); // 输出:3.140000 } else { printf("转换失败"); }
-
-
strtof(string to float):-
功能:将字符串转换为浮点数,并提供更强的错误检查功能。
-
用法:
float num = strtof(str, &endptr); -
参数说明:
str为要转换的字符串,endptr是一个指针,用于存储第一个非数字字符的地址。 -
优点:最健全的错误检查功能。通过
endptr指针可以检测是否整个字符串都被成功解析,以及是否有无效字符。 -
缺点:稍微复杂,需要使用
endptr检查结果,但也因此提供了更详细的错误信息。 -
示例:
char str[] = "3.14abc"; char *endptr; float num = strtof(str, &endptr); if (*endptr == '\0') { printf("转换成功:%f", num); // 输出:3.140000 } else { printf("部分转换,剩余内容:%s", endptr); // 输出:部分转换,剩余内容:abc }
-
总结:
atof:简单快速,但没有错误检查。sscanf:带有基本错误检查,适合简单场景。strtof:错误检查最强,适合需要高可靠性的场景。
2.2 课堂代码解析
课堂代码
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char *line = NULL;
size_t n = 0;
printf("Enter a floating point number\n");
// 读取输入行,分配内存并存储为字符串
if (!(getline(&line, &n, stdin) > 0)) {
perror("Error reading input");
return (-1);
}
// 第一种方法:使用 atof
float f1 = atof(line);
// 第二种方法:使用 sscanf
float f2;
if ((sscanf(line, "%f", &f2)) != 1) {
fprintf(stderr, "Error using sscanf to read float from string %s", line);
exit(EXIT_FAILURE);
}
// 第三种方法:使用 strtof
char *next = line;
float f3 = strtof(line, &next); // 第二个参数用于存储第一个非数字字符的地址
if (line == next) {
fprintf(stderr, "Error using strtof to read float from string %s", line);
exit(EXIT_FAILURE);
}
printf("atoi: %f\n", f1);
printf("sscanf: %f\n", f2);
printf("strtof: %f\n", f3);
free(line); // 释放动态分配的内存
return 0;
}
分析:
- 变量声明与初始化:
char *line = NULL;:声明一个字符指针用于存储输入的字符串。 size_t n = 0;:声明变量n,用于存储输入行的长度(为getline函数使用)。
- 读取用户输入:
getline(&line, &n, stdin):使用getline从标准输入读取一行字符串,并将其存储在line指针指向的内存中。 如果读取失败,程序会打印错误信息并返回-1。
- 解析浮点数:
第一种方法:atof将字符串转换为浮点数并存储在f1中。 第二种方法:sscanf使用格式说明符"%f"将字符串解析为浮点数,并存储在f2中。如果解析失败,程序会打印错误信息并终止。 第三种方法:strtof将字符串转换为浮点数并存储在f3中,同时将指向第一个非数字字符的地址存储在next中。如果转换失败(即字符串中没有有效数字),next指针会与line指针相同,程序会打印错误信息并终止。
- 打印结果:
使用printf函数将三个方法解析的浮点数分别输出,以验证不同方法的效果。
- 释放内存:
free(line);释放getline动态分配的内存,防止内存泄漏。
2.3 getline
getline 是一个 C 标准库函数,用于从指定的输入流(如标准输入 stdin)读取一行字符。它会自动分配或调整内存大小,以确保读取的行可以完全存储。以下是 getline 的详细介绍:
函数原型
ssize_t getline(char **lineptr, size_t *n, FILE *stream);
参数说明
char **lineptr:指向字符指针的指针,指向存储读取行的缓冲区。初始值可以为NULL,此时getline将自动分配适当大小的内存;否则,它会尝试将读取的内容放入*lineptr指向的内存中。size_t *n:指向存储缓冲区大小的变量。如果*lineptr是NULL,getline会自动分配一个足够大的缓冲区,并将大小存储在*n中。如果缓冲区不够大,getline会自动调整缓冲区大小,并更新*n的值。FILE *stream:输入流,通常是stdin(标准输入)或一个文件指针。
返回值
- 成功时:返回读取的字符数(包括换行符
\n,如果有)。 - 失败时:返回
-1,例如遇到文件结尾(EOF)或发生错误。
使用示例
以下是一个使用 getline 从标准输入读取一行的示例:
#include <stdio.h>
#include <stdlib.h>
int main() {
char *line = NULL;
size_t len = 0;
ssize_t nread;
printf("Please enter a line of text:\n");
// 使用 getline 从标准输入读取一行
nread = getline(&line, &len, stdin);
if (nread == -1) {
perror("Error reading line");
free(line); // 确保释放内存
return 1;
}
printf("You entered: %s", line);
free(line); // 释放 getline 动态分配的内存
return 0;
}
解释
getline(&line, &len, stdin);:从标准输入读取一行并将其存储在line中。如果line是NULL,getline会自动分配内存并将大小更新到len中。- 如果读取成功,
nread会是读取的字符数;否则返回-1。 free(line);:释放getline分配的内存,以防止内存泄漏。
优点
- 自动分配和调整内存:
getline可以自动处理输入行的长度,不需要提前知道行的长度。 - 便于读取完整行:
getline会在读取行的末尾添加一个\0作为字符串结束符,适合处理任意长度的行。
缺点
- 可移植性问题:
getline并不是 C89/C90 标准的一部分,它是在 POSIX 标准(例如 Linux)中引入的。因此,某些非 POSIX 环境(例如 Windows)可能不支持getline。在这些环境下,可以通过自己实现一个类似的函数来替代。
总结
getline 是一个功能强大的函数,特别适用于读取不定长度的行。通过 getline,开发者可以减少手动管理缓冲区的麻烦,简化了读取输入的过程。
2.4 sscanf
sscanf 是 C 标准库中的一个函数,用于从字符串中提取格式化数据。它是 scanf 的字符串版本,允许我们从给定的字符串中按照指定的格式读取数据,并将读取的值存储到相应的变量中。
函数原型
int sscanf(const char *str, const char *format, ...);
参数说明
const char *str:要解析的输入字符串。const char *format:格式字符串,用于指定要解析的数据类型和格式。格式符与printf、scanf等函数相同,例如%d表示整数,%f表示浮点数,%s表示字符串等。...(可变参数) :一个或多个指针,指向用于存储解析结果的变量。
返回值
- 成功时:返回成功读取并匹配的项数。例如,如果成功读取了两个变量,则返回
2。 - 失败时:如果没有匹配项,或者发生错误,返回
0或EOF。
常用格式符
%d:整数%f:浮点数%s:字符串(读取到空白字符或字符串结尾)%c:单个字符%x:十六进制整数
使用示例
以下是使用 sscanf 从字符串中读取整数和浮点数的示例:
#include <stdio.h>
int main() {
const char *str = "123 45.67";
int int_val;
float float_val;
// 使用 sscanf 从字符串中提取一个整数和一个浮点数
int result = sscanf(str, "%d %f", &int_val, &float_val);
if (result == 2) {
printf("整数值:%d\n", int_val);
printf("浮点值:%f\n", float_val);
} else {
printf("解析失败\n");
}
return 0;
}
输出
整数值:123
浮点值:45.670000
使用场景
sscanf 通常用于从字符串中提取数据,例如:
- 处理输入数据,例如从用户输入或文件中读取字符串并解析为多个数据类型。
- 将格式化的字符串解析为结构化数据,便于处理和分析。
- 用于数据验证,例如验证字符串是否符合特定格式。
注意事项
- 输入验证:
sscanf不会自动检测错误输入(例如超过缓冲区大小、非法字符等),因此需要开发者自己检查返回值,确保数据正确解析。 - 格式字符串的准确性:格式字符串必须与输入字符串的结构匹配,否则
sscanf将无法正确解析数据。 - 逐字符读取:如果格式字符串包含
%c(用于读取单个字符),则不会跳过空白字符;%s(用于读取字符串)会自动跳过空白字符。
另一个示例
以下是一个更复杂的示例,从字符串中提取多个数据:
#include <stdio.h>
int main() {
const char *str = "Name: Alice, Age: 25, GPA: 3.8";
char name[20];
int age;
float gpa;
int result = sscanf(str, "Name: %s, Age: %d, GPA: %f", name, &age, &gpa);
if (result == 3) {
printf("姓名:%s\n", name);
printf("年龄:%d\n", age);
printf("GPA:%f\n", gpa);
} else {
printf("解析失败\n");
}
return 0;
}
输出
姓名:Alice
年龄:25
GPA:3.800000
总结
sscanf用于从字符串中解析数据,支持多种格式符。- 通过
format参数指定解析格式,返回成功解析的项数。 - 常用于数据解析和验证,可用于从复杂的字符串中提取结构化数据。
2.5 strtof
strtof 是 C 标准库中的一个函数,用于将字符串转换为浮点数(float 类型)。与 atof 不同,strtof 提供了更健全的错误检查机制,因此更适合在需要精确解析和错误检查的场景中使用。
函数原型
float strtof(const char *str, char **endptr);
参数说明
const char *str:指向要转换的字符串的指针。char **endptr:指向字符指针的指针,用于指向转换结束后的第一个无效字符的地址。如果endptr指向的值不为NULL,转换函数将其设置为该字符串中第一个非数字字符的地址。可以通过此指针检测转换是否成功。
返回值
- 成功时:返回字符串中的浮点数值(
float)。 - 失败时:如果
str中没有可以转换的浮点数,则返回0.0。在这种情况下,endptr的值将与str相同,这样可以检测到转换失败。
使用示例
以下是一个使用 strtof 转换字符串为浮点数的示例,并演示如何使用 endptr 检查是否成功转换。
#include <stdio.h>
#include <stdlib.h>
int main() {
const char *str = "3.14159abc";
char *endptr;
// 使用 strtof 将字符串转换为浮点数
float num = strtof(str, &endptr);
// 检查转换是否成功
if (endptr == str) {
printf("转换失败:无法解析浮点数\n");
} else {
printf("转换成功:%f\n", num);
printf("未转换部分:%s\n", endptr);
}
return 0;
}
输出
复制代码转换成功:3.141590
未转换部分:abc
解释
strtof将字符串"3.14159abc"转换为浮点数3.14159,并将endptr指向字符串中的第一个非数字字符'a'。- 通过检查
endptr是否与str相同,可以确定是否有有效的浮点数被转换。如果endptr等于str,则表示字符串中没有有效的浮点数。
使用场景
- 输入验证:使用
strtof可以检测字符串中是否包含有效的浮点数,并识别无法解析的部分,因此非常适合用于输入验证。 - 部分解析:如果需要从字符串中解析部分数据(例如数字后跟其他字符),
strtof可以帮助分离数字和非数字部分。 - 处理复杂字符串:适合用于解析包含混合数据的复杂字符串,并允许检查转换后的剩余字符。
注意事项
- 错误检查:与
atof不同,strtof提供了更可靠的错误检查,可以避免直接使用atof时的潜在问题。 - 依赖
locale设置:strtof的行为可能会受到locale设置的影响,例如小数点符号的处理(在某些地区可能是逗号,而不是点)。 - 转换范围:如果字符串表示的数字超出了
float的范围,strtof会返回HUGE_VALF(正无穷)或-HUGE_VALF(负无穷),并设置errno为ERANGE,表示超出范围。
另一个示例:处理无效输入
以下示例展示了如何使用 strtof 检测超出范围的输入和无效输入
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <float.h>
int main() {
const char *str = "1e40"; // 超出 float 范围的数
char *endptr;
errno = 0; // 清除之前的错误
float num = strtof(str, &endptr);
if (endptr == str) {
printf("转换失败:无法解析浮点数\n");
} else if (errno == ERANGE) {
printf("转换超出范围,返回无穷大\n");
if (num == HUGE_VALF) {
printf("正无穷\n");
} else if (num == -HUGE_VALF) {
printf("负无穷\n");
}
} else {
printf("转换成功:%f\n", num);
}
return 0;
}
总结
strtof是一个将字符串转换为浮点数的函数,提供了强健的错误检查。endptr参数允许检查转换是否成功,并定位未转换的字符。- 适合用于需要精确和可靠地解析浮点数的场景,例如输入验证和复杂字符串解析
2.6 三函数对比总结
总结
| 函数 | 功能 | 适用场景 | 返回值 | 错误处理 |
|---|---|---|---|---|
getline | 从输入流读取一整行字符串 | 动态输入读取,不解析内容 | 读取字符数或 -1 | 返回 -1 表示失败 |
sscanf | 从字符串中提取多个格式化数据 | 从结构化字符串中提取数据 | 成功匹配项数或 EOF | 返回值检查是否匹配 |
strtof | 从字符串中解析浮点数 | 解析浮点数并进行错误检查 | 转换的浮点数或 0.0 | errno 或 endptr 检查 |
总体而言
getline用于读取一整行文本。sscanf用于从字符串中提取多个不同类型的数据。strtof用于将字符串中的前部分转换为浮点数,并进行更严格的错误检查。
3. easy algorithm
3.1 各种sort
nsertion sort
BubbleSort
Quick Sort
3.2 Towers of Hanoi game
汉诺塔(Towers of Hanoi)是一种经典的递归算法问题,常用于学习递归和分治思想。游戏的目标是将一组不同大小的圆盘从一个柱子移动到另一个柱子,规则如下:
- 共有三个柱子:源柱(起点)、辅助柱和目标柱。
- 初始时,所有圆盘按大小从上到下排列在源柱上,最小的圆盘在最上方。
- 每次只能移动一个圆盘,且必须是柱子顶部的圆盘。
- 圆盘只能放在空柱或比它大的圆盘之上。
目标是在遵守规则的前提下,将所有圆盘从源柱移动到目标柱。
算法思路:
假设有 nnn 个圆盘在源柱上,要将其移动到目标柱上,分步如下:
- 先将前 n−1n-1n−1 个圆盘从源柱移动到辅助柱(递归调用)。
- 将第 nnn 个圆盘(即最大的圆盘)从源柱直接移动到目标柱。
- 最后,将辅助柱上的 n−1n-1n−1 个圆盘移动到目标柱(再次递归调用)。
复杂度
汉诺塔问题的移动次数是 2n−12^n - 12n−1,其中 nnn 是圆盘数,因此随着圆盘数增加,移动次数会呈指数增长。
#include <stdio.h>
// 汉诺塔函数
void hanoi(int n, char source, char target, char auxiliary) {
if (n == 1) {
printf("Move disk 1 from %c to %c\n", source, target);
} else {
// 将前 n-1 个圆盘从源柱移动到辅助柱
hanoi(n - 1, source, auxiliary, target);
// 将第 n 个圆盘从源柱移动到目标柱
printf("Move disk %d from %c to %c\n", n, source, target);
// 将 n-1 个圆盘从辅助柱移动到目标柱
hanoi(n - 1, auxiliary, target, source);
}
}
int main() {
int n;
printf("Enter the number of disks: ");
scanf("%d", &n);
// 调用汉诺塔函数,将 n 个圆盘从 A 移动到 C,使用 B 作为辅助柱
hanoi(n, 'A', 'C', 'B');
return 0;
}
推导可以通过观察法和数字归纳法
4. deep copy and shallow copy
将浅拷贝和深拷贝的示例代码写成一个完整的、可以运行的 C 程序,包含浅拷贝和深拷贝的实现:
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *A; // 指针成员
double B;
} mystruct2;
// 浅拷贝函数:返回结构体的浅拷贝
mystruct2 shallow_copy_inc_members(mystruct2 in) {
in.A[0]++;
in.A[1]++;
in.B += 1;
return in;
}
// 深拷贝函数:返回结构体的深拷贝
mystruct2 deep_copy_inc_members(mystruct2 in) {
mystruct2 copy;
// 分配内存并复制数据
copy.A = malloc(2 * sizeof(int));
copy.A[0] = in.A[0] + 1;
copy.A[1] = in.A[1] + 1;
copy.B = in.B + 1;
return copy;
}
int main(void) {
mystruct2 S1, S2, S3;
// 初始化 S1 的成员并赋值
S1.A = malloc(2 * sizeof(int));
S1.A[0] = 10;
S1.A[1] = 20;
S1.B = 2.0;
// 浅拷贝
S2 = shallow_copy_inc_members(S1);
printf("浅拷贝后:\n");
printf("S1: %d %d %lf\n", S1.A[0], S1.A[1], S1.B);
printf("S2: %d %d %lf\n", S2.A[0], S2.A[1], S2.B);
// 复原 S1 的值以测试深拷贝
S1.A[0] = 10;
S1.A[1] = 20;
S1.B = 2.0;
// 深拷贝
S3 = deep_copy_inc_members(S1);
printf("\n深拷贝后:\n");
printf("S1: %d %d %lf\n", S1.A[0], S1.A[1], S1.B);
printf("S3: %d %d %lf\n", S3.A[0], S3.A[1], S3.B);
// 释放分配的内存
free(S1.A);
free(S2.A);
free(S3.A);
return 0;
}
代码解释
-
结构体定义:定义了一个结构体
mystruct2,包含一个指针成员A和一个浮点数成员B。 -
浅拷贝函数:
shallow_copy_inc_members函数对传入的结构体的成员进行自增操作并返回。因为只是浅拷贝,返回的结构体指针指向与原结构体相同的内存。 -
深拷贝函数:
deep_copy_inc_members函数在返回结构体前,分配新的内存并复制数据,从而实现深拷贝。 -
主函数:
- 初始化
S1的值。 - 调用浅拷贝函数,生成
S2并打印S1和S2的值,演示浅拷贝导致的相同内存引用问题。 - 恢复
S1的初始值。 - 调用深拷贝函数,生成
S3并打印S1和S3的值,验证深拷贝创建了独立的内存。 - 释放
S1、S2和S3中分配的动态内存,防止内存泄漏。
- 初始化
输出示例
浅拷贝后:
S1: 11 21 2.000000
S2: 11 21 3.000000
深拷贝后:
S1: 10 20 2.000000
S3: 11 21 3.000000
结果分析
- 浅拷贝:
S2的指针A指向与S1相同的内存,因此对S2.A的修改也影响了S1.A。 - 深拷贝:
S3的指针A指向一块独立的内存,因此S3的修改不影响S1。
when we need to copy the objects manually, then it is going to be deep copy.
deep copy
5. 内存拷贝的一些函数
5.1 memcpy
memcpy 是 C 标准库中的一个函数,用于在内存中进行数据块的复制操作。它可以将一块内存区域的内容复制到另一块内存区域中,常用于数组、结构体等数据的复制。
函数原型
void *memcpy(void *dest, const void *src, size_t n);
参数说明
void *dest:目标内存区域的指针,表示数据要复制到的地址。const void *src:源内存区域的指针,表示要复制的数据的地址。size_t n:要复制的字节数(大小),即需要从源地址复制多少字节到目标地址。
返回值
- 返回
dest指针,即指向目标内存区域的指针。
使用场景
- 复制数组:将一个数组的数据复制到另一个数组。
- 复制结构体:在内存中将一个结构体的内容快速复制到另一个结构体。
- 复制大数据块:
memcpy常用于在内存中移动较大块的数据,效率高于逐字节复制。
注意事项
- 重叠问题:
memcpy不支持源内存和目标内存重叠(即两块内存区域有部分相同)。如果源和目标内存区域有重叠,应使用memmove,它可以正确处理重叠情况。 - 类型安全:
memcpy的源和目标是void *类型,可以是任何数据类型,但开发者需要确保源和目标的大小匹配。 - 复制精确字节数:
memcpy只按字节数复制,不会终止于'\0',所以复制字符串时需要指定正确的字节数,以避免复制多余数据。
示例代码
以下是 memcpy 的一些常见用法示例:
- 复制数组
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Hello, World!";
char dest[20];
// 使用 memcpy 将 src 复制到 dest
memcpy(dest, src, strlen(src) + 1); // 包括 '\0' 字符
printf("Source: %s\n", src);
printf("Destination: %s\n", dest);
return 0;
}
输出:
Source: Hello, World!
Destination: Hello, World!
2. 复制结构体
假设我们有一个 Person 结构体,可以用 memcpy 将一个结构体的内容复制到另一个结构体。
#include <stdio.h>
#include <string.h>
typedef struct {
int id;
char name[20];
} Person;
int main() {
Person person1 = {1, "Alice"};
Person person2;
// 使用 memcpy 将 person1 的内容复制到 person2
memcpy(&person2, &person1, sizeof(Person));
printf("Person2 ID: %d\n", person2.id);
printf("Person2 Name: %s\n", person2.name);
return 0;
}
输出:
Person2 ID: 1
Person2 Name: Alice
3. 复制部分数据
有时,我们只想复制一部分数据,可以用 memcpy 的第三个参数 n 指定字节数。
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Hello, World!";
char dest[10];
// 只复制前 5 个字符
memcpy(dest, src, 5);
dest[5] = '\0'; // 手动添加字符串结束符
printf("Destination: %s\n", dest);
return 0;
}
输出:
Destination: Hello
memcpy 的原理
memcpy 是通过逐字节地复制源内存区域的数据到目标内存区域实现的。对于大数据块复制,memcpy 使用了优化的低级操作以提升效率。
使用 memcpy 的注意点
- 确保源和目标大小匹配:
memcpy是按字节复制,不会进行数据转换,所以确保源和目标数据大小一致非常重要。 - 避免重叠:如果源和目标内存区域重叠,建议使用
memmove,它会确保数据正确复制。 - 正确的字节数:在字符串复制时需要注意指定正确的字节数,因为
memcpy不会自动检测字符串结束符'\0'。
memcpy 与 strcpy 的区别
memcpy:按字节复制指定大小的数据,适用于任何数据类型,可以处理非字符串数据。strcpy:只用于字符串复制,会自动复制到'\0'(字符串结束符),不适合非字符串数据。
总结
memcpy是一个通用的内存复制函数,可以快速高效地在内存中复制数据。- 使用时需要确保源和目标内存区域大小匹配,并且源和目标区域没有重叠。
memcpy可以用于数组、结构体等多种数据类型的复制。
5.2 strcpy
strcpy 是 C 标准库中的一个字符串复制函数,用于将一个字符串复制到另一个字符串中。它的工作原理是将源字符串的内容逐字符复制到目标字符串中,直到遇到字符串结束符 \0(空字符)为止。
函数原型
char *strcpy(char *dest, const char *src);
参数说明
char *dest:目标字符串的指针,表示复制后的字符串将被存储的地址。需要确保dest指向的内存空间足够大,能够容纳源字符串的内容。const char *src:源字符串的指针,即要被复制的字符串。
返回值
- 返回目标字符串
dest的指针。
使用场景
- 字符串复制:将一个字符串的内容复制到另一个字符串变量中。
- 初始化字符串:可以用来初始化空字符串,例如将源字符串复制到动态分配的字符串缓冲区中。
注意事项
- 目标空间大小:
strcpy不会检查目标空间dest是否足够大。因此,在复制前要确保dest的大小至少等于src的长度加 1(+1是为了存储字符串结束符\0)。如果目标空间不够大,可能会导致缓冲区溢出,造成程序崩溃或安全漏洞。 - 字符串结束符:
strcpy会自动复制字符串结束符\0,确保目标字符串也是以\0结尾。这个特性让strcpy在处理 C 风格字符串时更加方便,但它不适用于包含\0的非字符串数据。 - 不支持部分复制:
strcpy总是复制整个字符串,无法指定只复制字符串的某一部分。如果需要复制部分字符串,可以使用strncpy。
示例代码
以下是一些使用 strcpy 的基本示例:
- 复制字符串
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Hello, World!";
char dest[20];
// 使用 strcpy 将 src 复制到 dest
strcpy(dest, src);
printf("Source: %s\n", src);
printf("Destination: %s\n", dest);
return 0;
}
输出:
Source: Hello, World!
Destination: Hello, World!
2. 目标字符串空间不足的风险
如果目标字符串空间不足,可能会导致程序崩溃或数据溢出。因此,确保 dest 的大小足够是很重要的。
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Hello, World!";
char dest[5]; // 空间不足,可能会导致溢出
strcpy(dest, src); // 这是不安全的操作,可能会导致崩溃
printf("Destination: %s\n", dest); // 可能无法正确输出
return 0;
}
在这个例子中,dest 的空间不足以容纳 src 字符串,strcpy 会导致缓冲区溢出,程序可能崩溃。
- 动态分配目标字符串的空间
为了避免空间不足的风险,可以动态分配目标字符串的空间。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main() {
char src[] = "Hello, World!";
char *dest = malloc(strlen(src) + 1); // 动态分配目标空间
if (dest == NULL) {
perror("Failed to allocate memory");
return 1;
}
strcpy(dest, src);
printf("Source: %s\n", src);
printf("Destination: %s\n", dest);
free(dest); // 释放分配的内存
return 0;
}
在这个例子中,我们动态分配了足够的内存来存储 src 字符串,这样可以安全地复制字符串而不会导致溢出。
strcpy 与 memcpy 的区别
-
用途:
strcpy:专门用于字符串复制,会自动识别并复制\0结束符,适合用于 C 风格字符串。memcpy:按字节数复制数据,适合用于任意数据块(不仅限于字符串),不自动处理\0。
-
复制长度:
strcpy会一直复制到\0字符。memcpy按指定的字节数复制,不关心数据内容。
strcpy 的安全替代方法
由于 strcpy 不会检查目标空间的大小,容易导致缓冲区溢出。C11 标准引入了 strcpy_s,它会在复制前检查目标空间大小,使用更安全:
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Hello, World!";
char dest[20];
// 使用 strcpy_s (在支持 C11 的编译器中可用)
strcpy_s(dest, sizeof(dest), src);
printf("Source: %s\n", src);
printf("Destination: %s\n", dest);
return 0;
}
在不支持 strcpy_s 的编译器中,可以使用 strncpy 作为替代,指定要复制的最大字符数,但需要手动处理字符串结束符:
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0'; // 确保目标字符串以 '\0' 结束
总结
strcpy是一个用于复制字符串的函数,复制到\0结束符,适用于 C 风格字符串。- 复制时需要确保目标字符串空间足够,否则可能导致缓冲区溢出。
- 如果需要复制部分字符串,可以使用
strncpy或更安全的strcpy_s(在支持的编译器上)。
5.3 sizeof 和 strlen
sizeof 和 strlen 是 C 语言中两个常用的函数/操作符,用于获取数据的大小或长度,但它们的作用和使用场景不同。以下是它们的区别:
- 基本定义
sizeof:这是一个编译时操作符,用于计算变量、数据类型或数据结构在内存中占用的字节数。它的结果在编译阶段就已确定,和数据的实际内容无关。strlen:这是一个库函数,用于计算以'\0'结尾的字符串的长度,即字符串中字符的数量(不包括'\0')。它的结果在运行时计算,必须在字符串已经初始化后才能使用。
- 用法与返回值
-
sizeof- 用法:
sizeof(variable)或sizeof(type) - 返回值:返回数据类型或变量所占的内存字节数,包括所有字符和
'\0'。
- 用法:
-
strlen- 用法:
strlen(string) - 返回值:返回字符串中字符的数量,不包括字符串结尾的
'\0'。
- 用法:
示例
示例 1:数组和字符串的区别
#include <stdio.h>
#include <string.h>
int main() {
char str[20] = "Hello";
printf("sizeof(str): %zu\n", sizeof(str)); // 输出:20
printf("strlen(str): %zu\n", strlen(str)); // 输出:5
return 0;
}
解释:
sizeof(str)返回的是数组str的大小,因为str是一个长度为 20 的字符数组,所以sizeof(str)的结果是 20。strlen(str)返回的是字符串的长度,即"Hello"的字符数为 5,不包括'\0'。
示例 2:指针与数组
#include <stdio.h>
#include <string.h>
int main() {
char *str = "Hello";
printf("sizeof(str): %zu\n", sizeof(str)); // 输出:8(在 64 位系统上指针大小为 8 字节)
printf("strlen(str): %zu\n", strlen(str)); // 输出:5
return 0;
}
解释:
sizeof(str)返回的是指针str的大小,因为str是一个指向字符串的指针。在 64 位系统上,指针大小通常是 8 字节,因此sizeof(str)的结果是 8。strlen(str)返回字符串的实际长度,即"Hello"的字符数为 5。
- 主要区别
| 特性 | sizeof | strlen |
|---|---|---|
| 功能 | 计算变量、类型或数组的总字节大小 | 计算字符串的长度(字符数,不包含 '\0') |
| 计算时机 | 编译时 | 运行时 |
| 返回值 | 返回变量、类型的总字节数,包括所有字符和 '\0' | 返回字符串的字符数,不包含 '\0' |
| 适用场景 | 可用于任意数据类型,包括数组、结构体等 | 只能用于以 '\0' 结尾的字符串 |
| 需要的头文件 | 无需头文件 | 需要包含 <string.h> |
- 注意事项
-
字符串数组与字符串指针的区别:
sizeof可以正确计算字符数组的大小,但如果是字符串指针,sizeof返回的是指针的大小,而不是字符串的长度。strlen用于以'\0'结尾的字符串,如果数组没有'\0'结尾,strlen可能导致未定义行为。
-
计算数组长度时的误用:
- 如果试图用
sizeof计算动态分配的数组长度(例如通过malloc分配的数组),sizeof只返回指针的大小,而不是数组的总大小。 - 在这种情况下,需要记录数组的长度,或者手动维护元素数量。
- 如果试图用
示例 3:动态分配数组的大小
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char *str = malloc(20 * sizeof(char));
strcpy(str, "Hello");
printf("sizeof(str): %zu\n", sizeof(str)); // 输出:8(在 64 位系统上指针大小为 8 字节)
printf("strlen(str): %zu\n", strlen(str)); // 输出:5
free(str);
return 0;
}
解释:
sizeof(str)返回的是指针str的大小,而不是动态分配内存的大小(因为str是一个指针)。strlen(str)返回的是字符串"Hello"的长度,即 5。
总结
sizeof用于获取变量、数组或数据类型的总字节数,适用于所有数据类型。strlen用于获取以'\0'结尾的字符串的字符数,只能用于字符串。- 使用时需要根据具体数据类型和需求选择适合的函数/操作符,避免误用导致错误。
5.4 C语言字符串 and 字符数组的区别
在 C 语言中,字符串和字符数组虽然看起来相似,但它们之间有一些重要的区别。以下是它们的区别及一些示例说明。
- 定义与概念
-
字符串:
- 在 C 语言中,字符串是一种特殊的字符数组,以
'\0'(空字符)结尾。 - 字符串是通过
char *指针或字符数组来表示的。例如:char *str = "Hello";或char str[] = "Hello";。 '\0'的结尾字符是字符串的一部分,用来标识字符串的结束。
- 在 C 语言中,字符串是一种特殊的字符数组,以
-
字符数组:
- 字符数组是一个普通的数组,存储一系列字符,可以用来存储字符串,但不一定需要
'\0'结尾。 - 字符数组的大小是固定的,存储的是单个字符的序列。
- 字符数组是一个普通的数组,存储一系列字符,可以用来存储字符串,但不一定需要
- 初始化方式
字符串初始化
char *str = "Hello";
- 这种方式会把字符串常量
"Hello"存储在内存的只读区域(一般是程序的常量区),str是指向这个字符串常量的指针,不能对其内容进行修改。
char str[] = "Hello";
- 这种方式会将字符串
"Hello"复制到字符数组str中,str是一个独立的字符数组,可以对其内容进行修改。
字符数组初始化
char arr[5] = {'H', 'e', 'l', 'l', 'o'};
- 这种方式初始化的字符数组没有
'\0'结尾,所以不是一个真正的 C 字符串。 - 可以显式添加
'\0'结尾,变成字符串。
- 内存分配与可修改性
-
字符串(指针方式) :如果使用指针方式
char *str = "Hello";,str指向的是字符串常量区,通常是只读的,不能修改。如果尝试修改会导致未定义行为或程序崩溃。char *str = "Hello"; str[0] = 'h'; // 错误,试图修改只读区域 -
字符数组:使用字符数组初始化的字符串
char str[] = "Hello";,数组内容是可修改的,因为它在栈或堆内存中存储了字符串的副本。char str[] = "Hello"; str[0] = 'h'; // 正确,可以修改数组中的字符
- 大小与长度
-
字符串:字符串的长度由
strlen函数计算,它计算的是从第一个字符开始直到遇到'\0'之前的字符数,不包括'\0'本身。char *str = "Hello"; printf("%zu\n", strlen(str)); // 输出 5 -
字符数组:字符数组的大小由
sizeof操作符计算,返回的是整个数组占用的字节数。如果字符数组包含'\0',则sizeof结果会比strlen大 1。char str[] = "Hello"; printf("%zu\n", sizeof(str)); // 输出 6,因为包括 '\0'
- 内存布局
- 字符串:字符串常量(如
"Hello")通常存储在只读数据段。使用指针指向该常量区域时,不会分配额外内存。 - 字符数组:字符数组是实际的内存分配,可以存储在栈上或堆上,数组本身占用实际空间。定义字符数组时,可以指定数组大小,并且该内存可以自由读写。
- 示例对比
字符串(指针方式)
#include <stdio.h>
int main() {
char *str = "Hello";
printf("String: %s\n", str);
// str[0] = 'h'; // 错误,试图修改只读内存
return 0;
}
字符数组
#include <stdio.h>
int main() {
char str[] = "Hello";
printf("Character Array: %s\n", str);
str[0] = 'h'; // 正确,字符数组内容可修改
printf("Modified Array: %s\n", str);
return 0;
}
输出:
Character Array: Hello
Modified Array: hello
7. 使用场景
- 字符串指针:适用于只读的字符串,不需要对字符串内容进行修改的场景,如输出固定的文本。
- 字符数组:适用于需要对字符串内容进行修改的场景,如拼接、追加字符等操作。
总结
| 特性 | 字符串(指针方式) | 字符数组 |
|---|---|---|
| 定义方式 | char *str = "Hello"; | char str[] = "Hello"; |
| 可修改性 | 通常不可修改(只读内存) | 可修改 |
| 内存分配 | 指向常量区的内存,通常是只读区域 | 实际分配数组大小的可写内存 |
| 大小 | 不包括 '\0' 的长度,由 strlen 计算 | 整个数组大小,由 sizeof 计算 |
| 使用场景 | 用于只读的、固定的字符串 | 用于需要修改的字符串 |
理解这两者的区别,可以帮助你在 C 语言中更有效地管理字符串和字符数据。
6. Compilation process again
Preprocessing
Compiling
Assembling
Linking
7. 练习题
根据分析,代码会将 S1.name 修改为 "Xohn",S1.score 保持不变,而 S2.name 修改为 "Xary",S2.score 被修改为 0。
#include <stdio.h>
#include <stdlib.h>
struct Student {
char *name;
int score;
};
// 传递结构体的副本(值传递)
void modify_Student(struct Student in) {
in.name[0] = 'X'; // 修改字符串的第一个字符
in.score = 0; // 修改 score,但不影响原结构体
}
// 传递结构体指针(地址传递)
void modify_Student_pointer(struct Student *in_p) {
in_p->name[0] = 'X'; // 修改字符串的第一个字符
in_p->score = 0; // 修改原结构体的 score
}
int main(void) {
struct Student S1, S2;
// 初始化 S1
S1.score = 1;
S1.name = malloc(32 * sizeof(char));
sprintf(S1.name, "John");
// 初始化 S2
S2.score = 2;
S2.name = malloc(32 * sizeof(char));
sprintf(S2.name, "Mary");
// 调用修改函数
modify_Student(S1); // 值传递
modify_Student_pointer(&S2); // 指针传递
// 打印结果
printf("%s \t %d\n", S1.name, S1.score); // 预期输出:Xohn 1
printf("%s \t %d\n", S2.name, S2.score); // 预期输出:Xary 0
// 释放内存
free(S1.name);
free(S2.name);
return 0;
}
代码解释
-
结构体
Student:包含name和score两个成员。 -
modify_Student函数:使用值传递,不会修改S1.score的值,但会影响S1.name指向的字符串的第一个字符。 -
modify_Student_pointer函数:使用指针传递,会修改S2.score和S2.name。 -
主函数
main:- 初始化
S1和S2的score和name。 - 调用两个函数分别修改
S1和S2。 - 输出
S1和S2的name和score。 - 释放分配的内存。
- 初始化
输出
Xohn 1
Xary 0
这就是完整的代码和预期的输出。
8. 动静态库
在 C 语言中,库是一组编译好的函数和数据的集合,可以被多个程序复用。根据链接方式的不同,库可以分为静态库和动态库。它们的主要区别在于链接的时间和运行时的加载方式。以下是对 C 语言静态库和动态库的介绍。
- 静态库(Static Library)
定义
- 静态库是一种在编译时被复制到可执行文件中的库。链接静态库的代码会在编译阶段直接嵌入到生成的可执行文件中,因此程序运行时不再需要该库的存在。
- 静态库的文件扩展名通常为
.a(在 Unix/Linux 系统)或.lib(在 Windows 系统)。
创建和使用静态库
创建静态库
假设有两个源文件 math.c 和 utils.c,其中包含一些函数实现。
-
编译源文件:
gcc -c math.c utils.c这会生成
math.o和utils.o两个目标文件。 -
创建静态库:
ar rcs libmylib.a math.o utils.o这会将目标文件打包成静态库
libmylib.a。
使用静态库
在编译时链接静态库:
gcc main.c -L. -lmylib -o myprogram
-L.:表示在当前目录查找库。-lmylib:链接名为libmylib.a的静态库(省略了lib和.a)。
如果静态库成功链接,那么生成的可执行文件 myprogram 将包含库中的所有代码,在运行时不需要再依赖库文件。
静态库的优缺点
-
优点
:
- 独立性:生成的可执行文件不依赖外部库,可以在没有库文件的系统上运行。
- 加载速度快:不需要在运行时加载外部库,启动速度较快。
-
缺点
:
- 占用空间:由于每个使用静态库的程序都会包含库的副本,会增加可执行文件的大小。
- 更新不便:如果库代码有更新,所有使用该库的程序都需要重新编译。
- 动态库(Dynamic Library)
定义
- 动态库是一种在程序运行时加载的库,库文件本身不会嵌入可执行文件中。程序在运行时通过链接器动态加载库文件。
- 动态库的文件扩展名通常为
.so(在 Unix/Linux 系统)或.dll(在 Windows 系统)。
创建和使用动态库
创建动态库
假设有两个源文件 math.c 和 utils.c。
编译源文件并生成动态库:
gcc -fPIC -c math.c utils.c
gcc -shared -o libmylib.so math.o utils.o
-fPIC:生成与位置无关的代码(Position Independent Code),用于共享库。-shared:指示编译器创建共享库(动态库)。
这会生成 libmylib.so 动态库。
使用动态库
在编译时指定动态库:
gcc main.c -L. -lmylib -o myprogram
在运行时,系统会根据环境变量(如 LD_LIBRARY_PATH 或 /etc/ld.so.conf)找到动态库 libmylib.so。
-
如果动态库文件位于当前目录,则可以设置
LD_LIBRARY_PATH环境变量:
export LD_LIBRARY_PATH=. ./myprogram
动态库的优缺点
-
优点:
- 节省空间:动态库在运行时加载,不会嵌入可执行文件,多个程序可以共享同一个动态库,减少内存和磁盘空间的占用。
- 更新方便:如果库文件更新,无需重新编译使用该库的程序,只需要替换动态库文件。
-
缺点:
- 依赖性:运行时必须确保动态库文件存在,否则程序无法启动。
- 启动稍慢:在程序运行时需要加载动态库,加载过程可能稍微增加启动时间。
- 静态库和动态库的对比
| 特性 | 静态库 | 动态库 |
|---|---|---|
| 文件扩展名 | .a(Linux),.lib(Windows) | .so(Linux),.dll(Windows) |
| 链接方式 | 编译时链接 | 运行时动态链接 |
| 文件大小 | 可执行文件较大 | 可执行文件较小 |
| 共享性 | 不支持共享,每个程序独立复制 | 支持共享,多个程序可共享同一库文件 |
| 运行时依赖 | 无需库文件 | 需要库文件 |
| 更新 | 需重新编译程序 | 只需更新库文件 |
| 性能 | 启动快,占用内存多 | 启动稍慢,内存占用少 |
- 示例代码
下面是一个简单的例子,演示如何使用静态库和动态库。
例子:数学库
创建数学函数 math.c
// math.c
#include <stdio.h>
void add(int a, int b) {
printf("Addition: %d\n", a + b);
}
void subtract(int a, int b) {
printf("Subtraction: %d\n", a - b);
}
创建主程序 main.c
// main.c
#include <stdio.h>
void add(int a, int b);
void subtract(int a, int b);
int main() {
add(10, 5);
subtract(10, 5);
return 0;
}
创建和使用静态库
-
编译数学库:
gcc -c math.c ar rcs libmath.a math.o -
编译主程序并链接静态库:
gcc main.c -L. -lmath -o static_program -
运行:
./static_program
创建和使用动态库
-
编译数学库为动态库:
gcc -fPIC -c math.c gcc -shared -o libmath.so math.o -
编译主程序并链接动态库:
gcc main.c -L. -lmath -o dynamic_program -
设置环境变量并运行:
export LD_LIBRARY_PATH=. ./dynamic_program
总结
- 静态库:在编译时直接嵌入到程序中,适合无需频繁更新的库,但生成的可执行文件较大。
- 动态库:在程序运行时加载,适合需要节省空间和易于更新的场景,但运行时需要依赖库文件。
根据程序需求选择适合的库类型,可以提高程序的性能和可维护性。