trinity C programming 8,9,10

86 阅读37分钟

trinity C programming 8,9,10

1. C语言字符串的存储

  1. 字符串在内存中的存储:C语言中,字符串以字符数组的形式存储在内存中,以\0(空字符)表示字符串的结束。
  2. 字符的存储方式:字符串中的每个字符都存储为其对应的ASCII码。例如,字符'1'的ASCII码是49。
  3. 字符串初始化:可以通过直接赋值的方式初始化字符串,例如 char mystr[] = "Hello World";。
  4. 直接赋值限制: 不可以直接对字符数组赋值,比如char mystr[11]; mystr = "Hello World";是无效的。然而,可以通过逐个字符赋值的方式实现,例如:char mystr[11]; mystr[0] = 'H'; mystr[1] = 'e';等。
  5. 字符串的输入输出:可以使用sprintf函数将数据格式化后存入字符串;使用sscanf函数从字符串中读取数据。

2. 从字符串中读取数字

2.1 简单介绍读取数字 及其 解析浮点数的函数

字符与整数的区别:字符 '1' 与整数 1 是不同的。当从标准输入(stdin)或文件中读取字符串时,如果需要将字符串解释为数字,就需要从字符串中解析出数字。

读取和解析方法:可以使用 fscanfscanf 进行格式化输入,或使用 getline 读取到C字符串后再解析数字。

解析浮点数的函数

  • atof:没有错误检查,但经常使用。
  • sscanf:带有一些错误检查。
  • strtof:错误检查最为健全,但可能需要额外的代码才能使用。

这三个解析浮点数的函数(atofsscanfstrtof)在C语言中各有特点,具体介绍如下:

  1. atof(ASCII to float):

    • 功能:将字符串转换为浮点数。

    • 用法:float num = atof(str);

    • 优点:使用简单,直接将字符串中的数字转换为浮点数。

    • 缺点:没有错误检查。如果字符串不是合法的数字(例如包含非数字字符),atof不会报错,可能返回0.0,这可能导致错误。

    • 示例:

      char str[] = "3.14";
      float num = atof(str);
      printf("%f", num);  // 输出:3.140000
      
  2. 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("转换失败");
      }
      
  3. 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;
}
​

分析:

  1. 变量声明与初始化:

char *line = NULL;:声明一个字符指针用于存储输入的字符串。 size_t n = 0;:声明变量n,用于存储输入行的长度(为getline函数使用)。

  1. 读取用户输入:

getline(&line, &n, stdin):使用getline从标准输入读取一行字符串,并将其存储在line指针指向的内存中。 如果读取失败,程序会打印错误信息并返回-1。

  1. 解析浮点数:

第一种方法:atof将字符串转换为浮点数并存储在f1中。 第二种方法:sscanf使用格式说明符"%f"将字符串解析为浮点数,并存储在f2中。如果解析失败,程序会打印错误信息并终止。 第三种方法:strtof将字符串转换为浮点数并存储在f3中,同时将指向第一个非数字字符的地址存储在next中。如果转换失败(即字符串中没有有效数字),next指针会与line指针相同,程序会打印错误信息并终止。

  1. 打印结果:

使用printf函数将三个方法解析的浮点数分别输出,以验证不同方法的效果。

  1. 释放内存:

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:指向存储缓冲区大小的变量。如果 *lineptrNULLgetline 会自动分配一个足够大的缓冲区,并将大小存储在 *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 中。如果 lineNULLgetline 会自动分配内存并将大小更新到 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:格式字符串,用于指定要解析的数据类型和格式。格式符与 printfscanf 等函数相同,例如 %d 表示整数,%f 表示浮点数,%s 表示字符串等。
  • ...(可变参数) :一个或多个指针,指向用于存储解析结果的变量。

返回值

  • 成功时:返回成功读取并匹配的项数。例如,如果成功读取了两个变量,则返回 2
  • 失败时:如果没有匹配项,或者发生错误,返回 0EOF

常用格式符

  • %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(负无穷),并设置 errnoERANGE,表示超出范围。

另一个示例:处理无效输入

以下示例展示了如何使用 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.0errnoendptr 检查

总体而言

  • getline 用于读取一整行文本。
  • sscanf 用于从字符串中提取多个不同类型的数据。
  • strtof 用于将字符串中的前部分转换为浮点数,并进行更严格的错误检查。

3. easy algorithm

3.1 各种sort

nsertion sort

BubbleSort

Quick Sort

juejin.cn/post/722059…

3.2 Towers of Hanoi game

汉诺塔(Towers of Hanoi)是一种经典的递归算法问题,常用于学习递归和分治思想。游戏的目标是将一组不同大小的圆盘从一个柱子移动到另一个柱子,规则如下:

  1. 共有三个柱子:源柱(起点)、辅助柱和目标柱。
  2. 初始时,所有圆盘按大小从上到下排列在源柱上,最小的圆盘在最上方。
  3. 每次只能移动一个圆盘,且必须是柱子顶部的圆盘。
  4. 圆盘只能放在空柱或比它大的圆盘之上。

目标是在遵守规则的前提下,将所有圆盘从源柱移动到目标柱。

算法思路:

假设有 nnn 个圆盘在源柱上,要将其移动到目标柱上,分步如下:

  1. 先将前 n−1n-1n−1 个圆盘从源柱移动到辅助柱(递归调用)。
  2. 将第 nnn 个圆盘(即最大的圆盘)从源柱直接移动到目标柱。
  3. 最后,将辅助柱上的 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;
}

代码解释

  1. 结构体定义:定义了一个结构体 mystruct2,包含一个指针成员 A 和一个浮点数成员 B

  2. 浅拷贝函数shallow_copy_inc_members 函数对传入的结构体的成员进行自增操作并返回。因为只是浅拷贝,返回的结构体指针指向与原结构体相同的内存。

  3. 深拷贝函数deep_copy_inc_members 函数在返回结构体前,分配新的内存并复制数据,从而实现深拷贝。

  4. 主函数

    • 初始化 S1 的值。
    • 调用浅拷贝函数,生成 S2 并打印 S1S2 的值,演示浅拷贝导致的相同内存引用问题。
    • 恢复 S1 的初始值。
    • 调用深拷贝函数,生成 S3 并打印 S1S3 的值,验证深拷贝创建了独立的内存。
    • 释放 S1S2S3 中分配的动态内存,防止内存泄漏。

输出示例

浅拷贝后:
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.

image.png

deep copy

image.png

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 常用于在内存中移动较大块的数据,效率高于逐字节复制。

注意事项

  1. 重叠问题memcpy 不支持源内存和目标内存重叠(即两块内存区域有部分相同)。如果源和目标内存区域有重叠,应使用 memmove,它可以正确处理重叠情况。
  2. 类型安全memcpy 的源和目标是 void * 类型,可以是任何数据类型,但开发者需要确保源和目标的大小匹配。
  3. 复制精确字节数memcpy 只按字节数复制,不会终止于 '\0',所以复制字符串时需要指定正确的字节数,以避免复制多余数据。

示例代码

以下是 memcpy 的一些常见用法示例:

  1. 复制数组
#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 的注意点

  1. 确保源和目标大小匹配memcpy 是按字节复制,不会进行数据转换,所以确保源和目标数据大小一致非常重要。
  2. 避免重叠:如果源和目标内存区域重叠,建议使用 memmove,它会确保数据正确复制。
  3. 正确的字节数:在字符串复制时需要注意指定正确的字节数,因为 memcpy 不会自动检测字符串结束符 '\0'

memcpystrcpy 的区别

  • 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 的指针。

使用场景

  • 字符串复制:将一个字符串的内容复制到另一个字符串变量中。
  • 初始化字符串:可以用来初始化空字符串,例如将源字符串复制到动态分配的字符串缓冲区中。

注意事项

  1. 目标空间大小strcpy 不会检查目标空间 dest 是否足够大。因此,在复制前要确保 dest 的大小至少等于 src 的长度加 1(+1 是为了存储字符串结束符 \0)。如果目标空间不够大,可能会导致缓冲区溢出,造成程序崩溃或安全漏洞。
  2. 字符串结束符strcpy 会自动复制字符串结束符 \0,确保目标字符串也是以 \0 结尾。这个特性让 strcpy 在处理 C 风格字符串时更加方便,但它不适用于包含 \0 的非字符串数据。
  3. 不支持部分复制strcpy 总是复制整个字符串,无法指定只复制字符串的某一部分。如果需要复制部分字符串,可以使用 strncpy

示例代码

以下是一些使用 strcpy 的基本示例:

  1. 复制字符串
#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 会导致缓冲区溢出,程序可能崩溃。

  1. 动态分配目标字符串的空间

为了避免空间不足的风险,可以动态分配目标字符串的空间。

#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 字符串,这样可以安全地复制字符串而不会导致溢出。

strcpymemcpy 的区别

  • 用途:

    • 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

sizeofstrlen 是 C 语言中两个常用的函数/操作符,用于获取数据的大小或长度,但它们的作用和使用场景不同。以下是它们的区别:

  1. 基本定义
  • sizeof:这是一个编译时操作符,用于计算变量、数据类型或数据结构在内存中占用的字节数。它的结果在编译阶段就已确定,和数据的实际内容无关。
  • strlen:这是一个库函数,用于计算以 '\0' 结尾的字符串的长度,即字符串中字符的数量(不包括 '\0' )。它的结果在运行时计算,必须在字符串已经初始化后才能使用。
  1. 用法与返回值
  • 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。
  1. 主要区别
特性sizeofstrlen
功能计算变量、类型或数组的总字节大小计算字符串的长度(字符数,不包含 '\0'
计算时机编译时运行时
返回值返回变量、类型的总字节数,包括所有字符和 '\0'返回字符串的字符数,不包含 '\0'
适用场景可用于任意数据类型,包括数组、结构体等只能用于以 '\0' 结尾的字符串
需要的头文件无需头文件需要包含 <string.h>
  1. 注意事项
  • 字符串数组与字符串指针的区别

    • 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 语言中,字符串和字符数组虽然看起来相似,但它们之间有一些重要的区别。以下是它们的区别及一些示例说明。

  1. 定义与概念
  • 字符串

    • 在 C 语言中,字符串是一种特殊的字符数组,以 '\0'(空字符)结尾。
    • 字符串是通过 char * 指针或字符数组来表示的。例如:char *str = "Hello";char str[] = "Hello";
    • '\0' 的结尾字符是字符串的一部分,用来标识字符串的结束。
  • 字符数组

    • 字符数组是一个普通的数组,存储一系列字符,可以用来存储字符串,但不一定需要 '\0' 结尾。
    • 字符数组的大小是固定的,存储的是单个字符的序列。
  1. 初始化方式

字符串初始化

char *str = "Hello";
  • 这种方式会把字符串常量 "Hello" 存储在内存的只读区域(一般是程序的常量区),str 是指向这个字符串常量的指针,不能对其内容进行修改。
char str[] = "Hello";
  • 这种方式会将字符串 "Hello" 复制到字符数组 str 中,str 是一个独立的字符数组,可以对其内容进行修改。

字符数组初始化

char arr[5] = {'H', 'e', 'l', 'l', 'o'};
  • 这种方式初始化的字符数组没有 '\0' 结尾,所以不是一个真正的 C 字符串。
  • 可以显式添加 '\0' 结尾,变成字符串。
  1. 内存分配与可修改性
  • 字符串(指针方式) :如果使用指针方式 char *str = "Hello";str 指向的是字符串常量区,通常是只读的,不能修改。如果尝试修改会导致未定义行为或程序崩溃。

    char *str = "Hello";
    str[0] = 'h';  // 错误,试图修改只读区域
    
  • 字符数组:使用字符数组初始化的字符串 char str[] = "Hello";,数组内容是可修改的,因为它在栈或堆内存中存储了字符串的副本。

    char str[] = "Hello";
    str[0] = 'h';  // 正确,可以修改数组中的字符
    
  1. 大小与长度
  • 字符串:字符串的长度由 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'
    
  1. 内存布局
  • 字符串:字符串常量(如 "Hello")通常存储在只读数据段。使用指针指向该常量区域时,不会分配额外内存。
  • 字符数组:字符数组是实际的内存分配,可以存储在栈上或堆上,数组本身占用实际空间。定义字符数组时,可以指定数组大小,并且该内存可以自由读写。
  1. 示例对比

字符串(指针方式)

#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;
}

代码解释

  1. 结构体 Student:包含 namescore 两个成员。

  2. modify_Student 函数:使用值传递,不会修改 S1.score 的值,但会影响 S1.name 指向的字符串的第一个字符。

  3. modify_Student_pointer 函数:使用指针传递,会修改 S2.scoreS2.name

  4. 主函数 main

    • 初始化 S1S2scorename
    • 调用两个函数分别修改 S1S2
    • 输出 S1S2namescore
    • 释放分配的内存。

输出

Xohn    1
Xary    0

这就是完整的代码和预期的输出。

8. 动静态库

在 C 语言中,库是一组编译好的函数和数据的集合,可以被多个程序复用。根据链接方式的不同,库可以分为静态库动态库。它们的主要区别在于链接的时间和运行时的加载方式。以下是对 C 语言静态库和动态库的介绍。

  1. 静态库(Static Library)

定义

  • 静态库是一种在编译时被复制到可执行文件中的库。链接静态库的代码会在编译阶段直接嵌入到生成的可执行文件中,因此程序运行时不再需要该库的存在。
  • 静态库的文件扩展名通常为 .a(在 Unix/Linux 系统)或 .lib(在 Windows 系统)。

创建和使用静态库

创建静态库

假设有两个源文件 math.cutils.c,其中包含一些函数实现。

  1. 编译源文件

    gcc -c math.c utils.c
    

    这会生成 math.outils.o 两个目标文件。

  2. 创建静态库

    ar rcs libmylib.a math.o utils.o
    

    这会将目标文件打包成静态库 libmylib.a

使用静态库

在编译时链接静态库:

gcc main.c -L. -lmylib -o myprogram
  • -L.:表示在当前目录查找库。
  • -lmylib:链接名为 libmylib.a 的静态库(省略了 lib.a)。

如果静态库成功链接,那么生成的可执行文件 myprogram 将包含库中的所有代码,在运行时不需要再依赖库文件。

静态库的优缺点

  • 优点

    • 独立性:生成的可执行文件不依赖外部库,可以在没有库文件的系统上运行。
    • 加载速度快:不需要在运行时加载外部库,启动速度较快。
  • 缺点

    • 占用空间:由于每个使用静态库的程序都会包含库的副本,会增加可执行文件的大小。
    • 更新不便:如果库代码有更新,所有使用该库的程序都需要重新编译。
  1. 动态库(Dynamic Library)

定义

  • 动态库是一种在程序运行时加载的库,库文件本身不会嵌入可执行文件中。程序在运行时通过链接器动态加载库文件。
  • 动态库的文件扩展名通常为 .so(在 Unix/Linux 系统)或 .dll(在 Windows 系统)。

创建和使用动态库

创建动态库

假设有两个源文件 math.cutils.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
    

动态库的优缺点

  • 优点

    • 节省空间:动态库在运行时加载,不会嵌入可执行文件,多个程序可以共享同一个动态库,减少内存和磁盘空间的占用。
    • 更新方便:如果库文件更新,无需重新编译使用该库的程序,只需要替换动态库文件。
  • 缺点

    • 依赖性:运行时必须确保动态库文件存在,否则程序无法启动。
    • 启动稍慢:在程序运行时需要加载动态库,加载过程可能稍微增加启动时间。
  1. 静态库和动态库的对比
特性静态库动态库
文件扩展名.a(Linux),.lib(Windows).so(Linux),.dll(Windows)
链接方式编译时链接运行时动态链接
文件大小可执行文件较大可执行文件较小
共享性不支持共享,每个程序独立复制支持共享,多个程序可共享同一库文件
运行时依赖无需库文件需要库文件
更新需重新编译程序只需更新库文件
性能启动快,占用内存多启动稍慢,内存占用少
  1. 示例代码

下面是一个简单的例子,演示如何使用静态库和动态库。

例子:数学库

创建数学函数 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;
}

创建和使用静态库

  1. 编译数学库

    gcc -c math.c
    ar rcs libmath.a math.o
    
  2. 编译主程序并链接静态库

    gcc main.c -L. -lmath -o static_program
    
  3. 运行

    ./static_program
    

创建和使用动态库

  1. 编译数学库为动态库

    gcc -fPIC -c math.c
    gcc -shared -o libmath.so math.o
    
  2. 编译主程序并链接动态库

    gcc main.c -L. -lmath -o dynamic_program
    
  3. 设置环境变量并运行

    export LD_LIBRARY_PATH=.
    ./dynamic_program
    

总结

  • 静态库:在编译时直接嵌入到程序中,适合无需频繁更新的库,但生成的可执行文件较大。
  • 动态库:在程序运行时加载,适合需要节省空间和易于更新的场景,但运行时需要依赖库文件。

根据程序需求选择适合的库类型,可以提高程序的性能和可维护性。