C语言基础知识

C语言基础知识

main函数

main函数介绍

在C语言中,main函数是程序的入口点,从该函数开始执行程序代码,并通常返回一个整数来指示程序的执行结果。main函数前的int表示该函数的返回类型是整数,通常用于返回程序的执行状态码,其中0表示成功执行,非0表示错误或异常。

在一个C程序中只能有一个main函数,因为main函数是程序的入口点,编译器和链接器会寻找唯一的main函数来开始执行程序。如果有多个main函数,编译器会报错。

在一个工程内也只能有一个main函数。

main函数多种写法

  1. 基本形式
int main() {
    // 程序代码
    return 0;
}
  1. 带命令行参数的形式
int main(int argc, char *argv[]) {
    // 程序代码
    return 0;
}
  • argc 表示传递给程序的命令行参数数量。
  • argv 是一个字符串数组,包含了传递给程序的命令行参数。

数据类型

以下是C语言中所有数据类型的详细列表,以表格形式呈现:

类型说明占用字节数示例
整型(Integer Types)
char字符型,通常用于存储单个字符1char c = 'A';
signed char有符号字符型,范围从 -128 到 1271signed char sc = -10;
unsigned char无符号字符型,范围从 0 到 2551unsigned char uc = 255;
short短整型,通常用于存储较小范围的整数2short s = 32767;
unsigned short无符号短整型,范围从 0 到 655352unsigned short us = 65535;
int整型,通常用于存储标准整数值4int i = 100;
unsigned int无符号整型,范围从 0 到 42949672954unsigned int ui = 4294967295;
long长整型,通常用于存储更大范围的整数4 或 8long l = 2147483647;
unsigned long无符号长整型,范围从 0 到 184467440737095516154 或 8unsigned long ul = 18446744073709551615;
long long长长整型,通常用于存储更大范围的整数8long long ll = 9223372036854775807;
unsigned long long无符号长长整型,范围从 0 到 184467440737095516158unsigned long long ull = 18446744073709551615;
浮点型(Floating-point Types)
float单精度浮点型,通常用于存储浮点数4float f = 3.14f;
double双精度浮点型,通常用于存储更精确的浮点数8double d = 3.141592653589793;
long double扩展精度浮点型,提供更高的精度12 或 16long double ld = 3.141592653589793238;
派生数据类型(Derived Types)
数组(Array)一组相同类型的数据集合根据元素类型和数量决定int arr[5] = {1, 2, 3, 4, 5};
指针(Pointer)存储另一个变量的内存地址4 或 8int *p = &i;
结构体(Structure)自定义的数据类型,包含不同类型的数据成员根据成员类型和数量决定struct Point { int x; int y; };
共用体(Union)自定义的数据类型,所有成员共享同一内存空间根据最大成员类型决定union Data { int i; float f; char str[20]; };
枚举(Enumeration)自定义的数据类型,包含命名的整型常量根据具体实现决定enum Day {Sunday, Monday, Tuesday};
用户定义数据类型(User-defined Types)
typedef为已有类型创建新的名称typedef unsigned int uint;
布尔类型(Bool Types)
booltrue:表示真,其值为1, false:表示假,其值为0。1bool isTrue = true;

使用 bool 类型

在C语言中,bool 类型用于表示布尔值,即逻辑上的“真”(true)或“假”(false)。虽然C语言在其早期版本中没有内建的 bool 类型,但从C99标准开始,引入了 <stdbool.h> 头文件来提供对布尔类型的支持。

要使用 bool 类型,你需要包含 <stdbool.h> 头文件,该头文件定义了 bool 类型及其两个值 truefalse

变量

变量声明

变量声明时需要指定变量的类型和名称。声明告诉编译器要为该变量分配内存,并为其指定类型。

语法

类型 变量名;

示例

int age;          // 声明一个整数类型的变量 age
float height;     // 声明一个浮点类型的变量 height
char grade;       // 声明一个字符类型的变量 grade

在C语言中,变量的命名规则如下:

  1. 变量名只能包含字母、数字和下划线。变量名必须以字母或下划线开头,不能以数字开头。
    • 合法例子:var_name, count1, _total
    • 不合法例子:1count, total-amount
  2. 变量名区分大小写VarName, varname, 和 VARNAME 是不同的变量名。
  3. 变量名不能是C语言的关键字。例如,int, return, if 是关键字,不能用作变量名。
  4. 变量名应具有描述性。选择能够反映变量用途的名称,可以提高代码的可读性。例如,temperaturetemp 更具描述性。
  5. 避免使用过于简单或含糊的名字。例如,xy 可能不如 widthheight 有意义,特别是在复杂的程序中。

变量初始化

变量初始化是给变量赋初值。变量可以在声明时进行初始化,也可以在声明后单独赋值。

语法

变量名 = 初值;

示例

int age = 25;            // 声明并初始化变量 age
float height = 5.9;      // 声明并初始化变量 height
char grade = 'A';        // 声明并初始化变量 grade

变量的作用域

变量的作用域决定了变量的可见范围。C语言中常见的作用域包括局部作用域和全局作用域。

  • 局部变量:在函数或块内声明,只在该函数或块内有效。

    示例

    void func() {
        int localVar = 10;   // 局部变量 localVar,只在 func 函数内有效
        printf("%d\n", localVar);
    }
    
  • 全局变量:在所有函数外部声明,整个程序都可以访问。

    示例

    int globalVar = 100;    // 全局变量 globalVar,整个程序都可以访问
    
    void func() {
        printf("%d\n", globalVar);
    }
    

变量的存储类别

C语言中的变量具有不同的存储类别,如 autostaticexternregister。这些存储类别影响变量的生命周期和存储位置。

  • auto:局部变量的默认存储类别,生命周期仅在函数调用期间有效。可以省略 auto 关键字。

    示例

    void func() {
        auto int a = 5;   // 默认存储类别
    }
    
  • static:变量的值在函数调用之间保持不变,生命周期为整个程序运行期间。

    示例

    void func() {
        static int count = 0;   // static 变量,值在函数调用之间保持
        count++;
        printf("%d\n", count);
    }
    
  • extern:声明一个变量在其他文件中定义,用于跨文件共享变量。

    示例

    extern int sharedVar;   // 声明一个在其他文件中定义的变量
    
  • register:建议编译器将变量存储在寄存器中以提高访问速度,但编译器可能会忽略这个建议。

    示例

    void func() {
        register int counter = 0;   // 尝试将变量存储在寄存器中
    }
    

示例程序

#include <stdio.h>

int globalVar = 100;    // 全局变量

void func() {
    int localVar = 10;   // 局部变量
    static int staticVar = 5; // 静态变量
    printf("Local variable: %d\n", localVar);
    printf("Static variable: %d\n", staticVar);
    staticVar++;
}

int main() {
    func();
    func();  // 调用 func 函数两次,观察 staticVar 的变化
    printf("Global variable: %d\n", globalVar);
    return 0;
}

在这个示例中:

  • globalVar 是全局变量。
  • localVar 是局部变量。
  • staticVar 是静态变量,其值在函数调用之间保持。

基本函数

printf()

printf()是 C 语言中一个非常重要的标准库函数,用于格式化输出数据到标准输出(通常是控制台)。它定义在 <stdio.h> 头文件中。printf 允许你以特定的格式输出不同类型的数据,并且提供了丰富的格式控制功能。

语法
int printf(const char *format, ...);
  • format:格式控制字符串,指定了如何格式化后续的参数。
  • ...:可变参数列表,按照 format 字符串中的格式指定输出的内容。

printf 是 C 标准库中的一个函数,用于格式化输出。它使用格式说明符(占位符)来指定如何格式化不同类型的数据。以下是常用的 printf 格式说明符及其用途:

常用格式说明符
占位符数据类型说明
%dint以十进制格式输出带符号整数
%iint以十进制格式输出带符号整数
%uunsigned int以十进制格式输出无符号整数
%ounsigned int以八进制格式输出无符号整数
%xunsigned int以小写十六进制格式输出无符号整数
%Xunsigned int以大写十六进制格式输出无符号整数
%ffloatdouble以小数点格式输出浮点数
%efloatdouble以科学计数法格式输出浮点数
%Efloatdouble以科学计数法格式输出浮点数
%gfloatdouble以 %f 或 %e 格式输出浮点数(自动选择较短的形式)
%Gfloatdouble以 %f 或 %E 格式输出浮点数(自动选择较短的形式)
%cchar输出单个字符
%schar *输出字符串
%pvoid *输出指针的值
%nint *存储已输出字符的数量
%%输出百分号符号 %
%ldlong int以十进制格式输出长整数
%lilong int以十进制格式输出长整数
%luunsigned long int以十进制格式输出无符号长整数
%lounsigned long int以八进制格式输出无符号长整数
%lxunsigned long int以小写十六进制格式输出无符号长整数
%lXunsigned long int以大写十六进制格式输出无符号长整数
%lldlong long int以十进制格式输出长长整数
%llilong long int以十进制格式输出长长整数
%lluunsigned long long int以十进制格式输出无符号长长整数
%lounsigned long long int以八进制格式输出无符号长长整数
%llxunsigned long long int以小写十六进制格式输出无符号长长整数
%llXunsigned long long int以大写十六进制格式输出无符号长长整数
%zusize_t以十进制格式输出 size_t 类型
格式控制
格式描述示例
%5d输出宽度为5的整数,不足的地方填充空格printf("%5d", 42);
%05d输出宽度为5的整数,不足的地方填充零printf("%05d", 42);
%-5d输出宽度为5的整数,左对齐printf("%-5d", 42);
%.2f输出浮点数,小数部分保留两位printf("%.2f", 3.141);
%10s输出宽度为10的字符串,不足的地方填充空格printf("%10s", "Hello");

这些格式说明符和格式控制选项帮助你灵活地格式化和输出不同类型的数据。

scanf()

scanf() 是 C 语言中的标准输入函数,用于从标准输入(通常是键盘)读取格式化数据。它定义在 <stdio.h> 头文件中。与 printf 类似,scanf 也使用格式说明符来指定输入数据的类型和格式。

语法
int scanf(const char *format, ...);
  • format:格式控制字符串,用于指定输入的数据类型。
  • ...:可变参数列表,指定要存储输入数据的变量的地址。
常见格式说明符

以下是 scanf 函数中常见的格式说明符,用于读取不同类型的数据:

说明符描述示例
%d读取一个有符号十进制整数scanf("%d", &age);
%i读取一个有符号整数,可以是十进制、八进制或十六进制scanf("%i", &value);
%u读取一个无符号十进制整数scanf("%u", &count);
%x读取一个无符号十六进制整数scanf("%x", &hexValue);
%X读取一个无符号十六进制整数(大写字母)scanf("%X", &hexValue);
%f读取一个浮点数scanf("%f", &height);
%e读取一个浮点数,以科学记数法格式输入scanf("%e", &expValue);
%g读取一个浮点数,自动选择 %f%e 格式scanf("%g", &num);
%c读取一个字符scanf("%c", &ch);
%s读取一个字符串,直到遇到空白字符scanf("%s", str);
%p读取一个指针地址scanf("%p", &ptr);
示例代码

以下是一个使用 scanf 的示例,展示了如何从用户输入中读取不同类型的数据:

#include <stdio.h>

int main() {
    int age;
    float height;
    char grade;
    char name[50];

    // 读取整数
    printf("Enter your age: ");
    scanf("%d", &age);

    // 读取浮点数
    printf("Enter your height: ");
    scanf("%f", &height);

    // 读取字符
    printf("Enter your grade: ");
    scanf(" %c", &grade); // 注意前面的空格,用于忽略前一个输入后的换行符

    // 读取字符串
    printf("Enter your name: ");
    scanf("%s", name);

    // 打印输入值
    printf("Age: %d\n", age);
    printf("Height: %.2f\n", height);
    printf("Grade: %c\n", grade);
    printf("Name: %s\n", name);

    return 0;
}
scanf()读取多个值

在C语言中,scanf 函数可以一次性读取多个值。你可以在格式控制字符串中指定多个格式说明符,并将对应的变量地址作为参数传递给 scanf。这样,scanf 会按照格式说明符的顺序读取输入并将结果存储到相应的变量中。

以下是一个使用 scanf 输入多个值的示例:

#include <stdio.h>

int main() {
    int age;
    float height;
    char grade;
    char name[50];

    // 提示用户输入
    printf("Enter your age, height (in meters), grade, and name: ");
    
    // 使用scanf读取多个值
    scanf("%d %f %c %s", &age, &height, &grade, name);

    // 打印输入值
    printf("Age: %d\n", age);
    printf("Height: %.2f meters\n", height);
    printf("Grade: %c\n", grade);
    printf("Name: %s\n", name);

    return 0;
}

getchar()putchar()

getcharputchar 是 C 语言中用于处理字符输入和输出的标准库函数,分别定义在 <stdio.h> 头文件中。它们用于处理单个字符的读取和写入,相比于 scanfprintf,它们更为简单和直接。

getchar()

功能:从标准输入(通常是键盘)读取一个字符。

原型

int getchar(void);

返回值

  • 成功:返回读取的字符(以 int 类型表示)。通常是 ASCII 值。
  • 失败:返回 EOF(通常是 -1),表示输入结束或出错。

示例代码

#include <stdio.h>

int main() {
    int c;
    
    printf("Enter a character: ");
    c = getchar(); // 读取一个字符

    printf("You entered: ");
    putchar(c);    // 输出读取的字符
    putchar('\n'); // 输出换行符

    return 0;
}
putchar()

功能:将一个字符写入到标准输出(通常是屏幕)。

原型

int putchar(int char);

参数

  • char:要输出的字符(以 int 类型传递,通常是 ASCII 值)。

返回值

  • 成功:返回输出的字符(通常是 ASCII 值)。
  • 失败:返回 EOF(通常是 -1),表示输出错误。

示例代码

#include <stdio.h>

int main() {
    char c = 'A';

    printf("Character to be output: ");
    putchar(c);    // 输出字符
    putchar('\n'); // 输出换行符

    return 0;
}

qsort()

qsort 是 C 标准库中的一个函数,用于对数组进行快速排序(Quick Sort)。它的定义在 stdlib.h 头文件中。qsort 函数非常灵活,可以对任何类型的数组进行排序,只要你提供适当的比较函数。

qsort 函数原型
void qsort(void *base, size_t num, size_t size, int (*compar)(const void *, const void *));
  • void *base:指向要排序的数组的起始地址。
  • size_t num:数组中的元素数量。
  • size_t size:每个元素的大小(以字节为单位)。
  • int (*compar)(const void *, const void *):指向比较函数的指针。
比较函数

比较函数用于确定数组元素的顺序。它接收两个 const void * 类型的参数,分别指向要比较的两个元素。比较函数应返回如下值:

  • 如果第一个元素小于第二个元素,返回负值。
  • 如果第一个元素等于第二个元素,返回零。
  • 如果第一个元素大于第二个元素,返回正值。
对整数数组进行排序
#include <stdio.h>
#include <stdlib.h>

// 比较函数:用于比较两个整数
int compareInts(const void *a, const void *b) {
    return (*(int*)a - *(int*)b);
}

int main() {
    int arr[] = {5, 2, 9, 1, 5, 6};
    size_t arrSize = sizeof(arr) / sizeof(arr[0]);

    // 使用 qsort 对数组进行排序
    qsort(arr, arrSize, sizeof(int), compareInts);

    // 输出排序后的数组
    for (size_t i = 0; i < arrSize; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}
对结构体数组进行排序
#include <stdio.h>
#include <stdlib.h>

// 定义结构体
typedef struct {
    char name[20];
    int age;
} Person;

// 比较函数:用于比较两个结构体(按年龄排序)
int comparePersons(const void *a, const void *b) {
    Person *pa = (Person *)a;
    Person *pb = (Person *)b;
    return pa->age - pb->age;
}

int main() {
    Person people[] = {
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35}
    };
    size_t peopleSize = sizeof(people) / sizeof(people[0]);

    // 使用 qsort 对结构体数组进行排序
    qsort(people, peopleSize, sizeof(Person), comparePersons);

    // 输出排序后的结构体数组
    for (size_t i = 0; i < peopleSize; i++) {
        printf("%s: %d\n", people[i].name, people[i].age);
    }

    return 0;
}
  1. 整数数组排序
    • 定义比较函数 compareInts,比较两个整数。
    • 调用 qsort(arr, arrSize, sizeof(int), compareInts) 对数组进行排序。
    • 输出排序后的数组。
  2. 结构体数组排序
    • 定义结构体 Person
    • 定义比较函数 comparePersons,比较两个结构体的年龄。
    • 调用 qsort(people, peopleSize, sizeof(Person), comparePersons) 对结构体数组进行排序。
    • 输出排序后的结构体数组。

字符分类函数

C 标准库提供了一组字符分类函数,用于检查单个字符的特性(如字母、数字、空白等)。这些函数在 ctype.h 头文件中定义,并接受一个 int 类型的参数,通常是 unsigned char,表示要检查的字符。

字符分类函数列表
函数说明
isalnum检查字符是否是字母或数字
isalpha检查字符是否是字母
iscntrl检查字符是否是控制字符
isdigit检查字符是否是十进制数字
isgraph检查字符是否是除空格外的可打印字符
islower检查字符是否是小写字母
isprint检查字符是否是可打印字符,包括空格
ispunct检查字符是否是标点符号(不包括字母和数字)
isspace检查字符是否是空白字符(如空格、换行、制表符)
isupper检查字符是否是大写字母
isxdigit检查字符是否是十六进制数字
示例代码

以下是这些字符分类函数的示例代码,展示了如何使用它们来检查字符的不同特性:

#include <stdio.h>
#include <ctype.h>

int main() {
    char ch;

    // 示例字符
    ch = 'A';
    printf("Character: %c\n", ch);
    printf("isalnum: %d\n", isalnum(ch));
    printf("isalpha: %d\n", isalpha(ch));
    printf("isdigit: %d\n", isdigit(ch));
    printf("islower: %d\n", islower(ch));
    printf("isupper: %d\n", isupper(ch));
    printf("isspace: %d\n", isspace(ch));
    printf("ispunct: %d\n", ispunct(ch));
    printf("isprint: %d\n", isprint(ch));
    printf("isgraph: %d\n", isgraph(ch));
    printf("iscntrl: %d\n", iscntrl(ch));
    printf("isxdigit: %d\n\n", isxdigit(ch));

    return 0;
}

字符转换函数

C 标准库提供了一组字符转换函数,用于将字符转换为大写、小写或其他特定格式。这些函数定义在 ctype.h 头文件中。常见的字符转换函数包括 touppertolower,它们分别用于将字符转换为大写和小写。

字符转换函数列表
函数说明
toupper将字符转换为大写
tolower将字符转换为小写
函数原型
int toupper(int c);
int tolower(int c);

参数说明

  • int c:要转换的字符。通常是一个 unsigned char 类型的值,或 EOF

返回值

  • toupper:如果 c 是小写字母,则返回其对应的大写字母;否则返回 c
  • tolower:如果 c 是大写字母,则返回其对应的小写字母;否则返回 c
示例代码

以下是使用 touppertolower 函数的示例代码,展示了如何将字符转换为大写和小写:

#include <stdio.h>
#include <ctype.h>

int main() {
    char ch1 = 'a';
    char ch2 = 'B';
    char ch3 = '1';

    // 使用 toupper 将字符转换为大写
    printf("toupper('%c') = '%c'\n", ch1, toupper(ch1));
    printf("toupper('%c') = '%c'\n", ch2, toupper(ch2));
    printf("toupper('%c') = '%c'\n", ch3, toupper(ch3));

    // 使用 tolower 将字符转换为小写
    printf("tolower('%c') = '%c'\n", ch1, tolower(ch1));
    printf("tolower('%c') = '%c'\n", ch2, tolower(ch2));
    printf("tolower('%c') = '%c'\n", ch3, tolower(ch3));

    return 0;
}
  1. 定义要转换的字符

    • char ch1 = 'a'; 定义一个小写字母 a
    • char ch2 = 'B'; 定义一个大写字母 B
    • char ch3 = '1'; 定义一个数字字符 1
  2. 使用 toupper 函数

    • toupper(ch1) 将小写字母 a 转换为大写字母 A
    • toupper(ch2) 将大写字母 B 保持不变。
    • toupper(ch3) 对于非字母字符 1,保持不变。
  3. 使用 tolower 函数

    • tolower(ch1) 将小写字母 a 保持不变。
    • tolower(ch2) 将大写字母 B 转换为小写字母 b
    • tolower(ch3) 对于非字母字符 1,保持不变。

strlen()

strlen 是 C 标准库中的一个函数,用于计算以空字符(‘\0’)结尾的字符串的长度。该函数定义在 string.h 头文件中。

strlen 函数原型
size_t strlen(const char *str);

参数说明

  • const char *str:指向要计算长度的字符串的指针。

返回值

  • 返回字符串的长度,即字符串中字符的数量,不包括终止的空字符 \0
示例代码

以下是使用 strlen 函数的示例代码,展示了如何计算字符串的长度:

#include <stdio.h>
#include <string.h>

int main() {
    const char *str1 = "Hello, World!";
    const char *str2 = "";
    const char *str3 = "C programming";

    // 使用 strlen 计算字符串的长度
    printf("Length of \"%s\" is %zu\n", str1, strlen(str1));
    printf("Length of \"%s\" is %zu\n", str2, strlen(str2));
    printf("Length of \"%s\" is %zu\n", str3, strlen(str3));

    return 0;
}
  1. 定义字符串

    • const char *str1 = "Hello, World!"; 定义一个普通字符串。
    • const char *str2 = ""; 定义一个空字符串。
    • const char *str3 = "C programming"; 定义另一个字符串。
  2. 计算字符串的长度

    • strlen(str1) 返回字符串 str1 的长度,不包括末尾的空字符 \0
    • strlen(str2) 返回空字符串 str2 的长度,为 0。
    • strlen(str3) 返回字符串 str3 的长度。
  3. 打印结果

    • 使用 printf 打印每个字符串的长度。
注意事项
  • strlen 只计算字符串中的字符数量,不包括末尾的空字符 \0
  • 确保传递给 strlen 的字符串是以空字符结尾的有效字符串,否则可能会导致未定义行为。
  • strlen 的时间复杂度为 O(n),其中 n 是字符串的长度,因为它需要遍历整个字符串来计算长度。

strcpy()

strcpy 是 C 标准库中的一个函数,用于将源字符串复制到目标字符串。它定义在 string.h 头文件中。该函数将源字符串的内容复制到目标字符串,包括末尾的空字符 \0

strcpy 函数原型
char *strcpy(char *dest, const char *src);

参数说明

  • char *dest:指向目标字符串的指针。
  • const char *src:指向源字符串的指针。

返回值

  • 返回 dest 的指针。

使用场景

  • 将一个字符串复制到另一个字符串。
  • 初始化字符串数组。
示例代码

以下是使用 strcpy 函数的示例代码:

#include <stdio.h>
#include <string.h>

int main() {
    // 源字符串
    const char *src = "Hello, World!";
    
    // 目标字符串,确保空间足够
    char dest[50];
    
    // 复制字符串
    strcpy(dest, src);
    
    // 打印结果
    printf("Source: %s\n", src);
    printf("Destination: %s\n", dest);
    
    return 0;
}

代码解释

  1. 定义源字符串

    • const char *src = "Hello, World!"; 定义一个源字符串 src
  2. 定义目标字符串

    • char dest[50]; 定义一个目标字符串 dest,并确保它有足够的空间来存储源字符串和末尾的空字符。
  3. 复制字符串

    • strcpy(dest, src); 使用 strcpy 函数将源字符串 src 复制到目标字符串 dest
  4. 打印结果

    • printf("Source: %s\n", src); 打印源字符串。
    • printf("Destination: %s\n", dest); 打印目标字符串。
注意事项
  • 目标字符串 dest 必须有足够的空间来存储源字符串 src 及其末尾的空字符 \0
  • 如果目标字符串 dest 的空间不足,会导致缓冲区溢出,可能引发未定义行为或安全漏洞。
使用 strncpy

由于 strcpy 不检查目标缓冲区的大小,使用时需要特别小心,确保目标缓冲区足够大,以避免缓冲区溢出。为了提高安全性,建议使用更安全的字符串复制函数,如 strncpy 或更现代的 strcpy_s(在支持的编译器和平台上)。

以下是使用 strncpy 的示例代码,它可以指定复制的最大字符数,从而提高安全性:

#include <stdio.h>
#include <string.h>

int main() {
    // 源字符串
    const char *src = "Hello, World!";
    
    // 目标字符串,确保空间足够
    char dest[50];
    
    // 复制字符串,最多复制 49 个字符,以确保空间留给末尾的空字符
    strncpy(dest, src, sizeof(dest) - 1);
    
    // 确保目标字符串以空字符结尾
    dest[sizeof(dest) - 1] = '\0';
    
    // 打印结果
    printf("Source: %s\n", src);
    printf("Destination: %s\n", dest);
    
    return 0;
}

strcat()

strcat 是 C 标准库中的一个函数,用于将一个源字符串追加到目标字符串的末尾。它定义在 string.h 头文件中。该函数将源字符串的内容复制到目标字符串的末尾,并自动添加终止符 \0

strcat 函数原型
char *strcat(char *dest, const char *src);

参数说明

  • char *dest:指向目标字符串的指针。该字符串必须有足够的空间来存储源字符串及其末尾的空字符。
  • const char *src:指向源字符串的指针。

返回值

  • 返回 dest 的指针。

使用场景

  • 将一个字符串追加到另一个字符串的末尾。
  • 构建由多个子字符串组成的长字符串。
示例代码

以下是使用 strcat 函数的示例代码:

#include <stdio.h>
#include <string.h>

int main() {
    // 目标字符串,确保空间足够
    char dest[50] = "Hello, ";
    
    // 源字符串
    const char *src = "World!";
    
    // 追加字符串
    strcat(dest, src);
    
    // 打印结果
    printf("Result: %s\n", dest);
    
    return 0;
}

代码解释

  1. 定义目标字符串

    • char dest[50] = "Hello, "; 定义一个目标字符串 dest,并确保它有足够的空间来存储源字符串和末尾的空字符。
  2. 定义源字符串

    • const char *src = "World!"; 定义一个源字符串 src
  3. 追加字符串

    • strcat(dest, src); 使用 strcat 函数将源字符串 src 追加到目标字符串 dest 的末尾。
  4. 打印结果

    • printf("Result: %s\n", dest); 打印结果字符串。
注意事项
  • 目标字符串 dest 必须有足够的空间来存储源字符串 src 及其末尾的空字符 \0,以及原目标字符串的内容。
  • 如果目标字符串 dest 的空间不足,会导致缓冲区溢出,可能引发未定义行为或安全漏洞。
使用 strncat

由于 strcat 不检查目标缓冲区的大小,使用时需要特别小心,确保目标缓冲区有足够的空间以避免缓冲区溢出。为了提高安全性,建议使用更安全的字符串追加函数,如 strncat 或更现代的 strcat_s(在支持的编译器和平台上)。

以下是使用 strncat 的示例代码,它可以指定追加的最大字符数,从而提高安全性:

#include <stdio.h>
#include <string.h>

int main() {
    // 目标字符串,确保空间足够
    char dest[50] = "Hello, ";
    
    // 源字符串
    const char *src = "World!";
    
    // 追加字符串,最多追加 49 - strlen(dest) 个字符,以确保空间留给末尾的空字符
    strncat(dest, src, sizeof(dest) - strlen(dest) - 1);
    
    // 打印结果
    printf("Result: %s\n", dest);
    
    return 0;
}

代码解释

  1. 定义目标字符串
    • char dest[50] = "Hello, "; 定义一个目标字符串 dest,并确保它有足够的空间来存储源字符串和末尾的空字符。
  2. 定义源字符串
    • const char *src = "World!"; 定义一个源字符串 src
  3. 追加字符串
    • strncat(dest, src, sizeof(dest) - strlen(dest) - 1); 使用 strncat 函数将源字符串 src 追加到目标字符串 dest 的末尾,最多追加 sizeof(dest) - strlen(dest) - 1 个字符,以确保目标字符串有足够的空间。
  4. 打印结果
    • printf("Result: %s\n", dest); 打印结果字符串。

strcmp()

strcmp 是 C 标准库中的一个函数,用于比较两个字符串的大小。它定义在 string.h 头文件中。该函数按字典顺序比较两个字符串的每个字符,直到找到不同的字符或达到字符串的结尾。

strcmp 函数原型
int strcmp(const char *str1, const char *str2);

参数说明

  • const char *str1:指向第一个字符串的指针。
  • const char *str2:指向第二个字符串的指针。

返回值

strcmp 返回一个整数值,根据比较结果返回以下值之一:

  • 小于零:如果 str1 小于 str2
  • 等于零:如果 str1 等于 str2
  • 大于零:如果 str1 大于 str2

使用场景

  • 判断两个字符串是否相等。
  • 比较字符串的字典顺序。
  • 在排序算法中使用字符串比较。
示例代码

以下是使用 strcmp 函数的示例代码:

#include <stdio.h>
#include <string.h>

int main() {
    const char *str1 = "Hello";
    const char *str2 = "World";
    const char *str3 = "Hello";

    int result1 = strcmp(str1, str2);
    int result2 = strcmp(str1, str3);

    // 打印比较结果
    printf("Comparing '%s' and '%s': %d\n", str1, str2, result1);
    printf("Comparing '%s' and '%s': %d\n", str1, str3, result2);

    return 0;
}

代码解释

  1. 定义字符串

    • const char *str1 = "Hello"; 定义字符串 str1
    • const char *str2 = "World"; 定义字符串 str2
    • const char *str3 = "Hello"; 定义字符串 str3
  2. 比较字符串

    • int result1 = strcmp(str1, str2); 比较字符串 str1str2,将结果存储在 result1 中。
    • int result2 = strcmp(str1, str3); 比较字符串 str1str3,将结果存储在 result2 中。
  3. 打印比较结果

    • printf("Comparing '%s' and '%s': %d\n", str1, str2, result1); 打印 str1str2 的比较结果。
    • printf("Comparing '%s' and '%s': %d\n", str1, str3, result2); 打印 str1str3 的比较结果。

输出结果

Comparing 'Hello' and 'World': -15
Comparing 'Hello' and 'Hello': 0

结果分析

  • result1 为 -15,表示 str1 小于 str2
  • result2 为 0,表示 str1 等于 str3
注意事项
  • strcmp 函数区分大小写,即大写字母和小写字母被视为不同的字符。例如,"Hello""hello" 被认为是不同的字符串。
  • strcmp 函数在比较时会检查每个字符的 ASCII 值,直到遇到不同的字符或达到字符串的结尾。
strncmp 示例

在某些情况下,为了避免缓冲区溢出或其他安全问题,可以使用更安全的字符串比较函数,如 strncmpstrncmp 函数允许指定比较的最大字符数,从而提供更好的安全性。

以下是使用 strncmp 的示例代码:

#include <stdio.h>
#include <string.h>

int main() {
    const char *str1 = "Hello";
    const char *str2 = "Helium";

    int result = strncmp(str1, str2, 3);

    // 打印比较结果
    printf("Comparing first 3 characters of '%s' and '%s': %d\n", str1, str2, result);

    return 0;
}

代码解释

  1. 定义字符串
    • const char *str1 = "Hello"; 定义字符串 str1
    • const char *str2 = "Helium"; 定义字符串 str2
  2. 比较字符串的前 3 个字符
    • int result = strncmp(str1, str2, 3); 比较 str1str2 的前 3 个字符,将结果存储在 result 中。
  3. 打印比较结果
    • printf("Comparing first 3 characters of '%s' and '%s': %d\n", str1, str2, result); 打印 str1str2 的前 3 个字符的比较结果。

strstr()

strstr 是 C 标准库中的一个函数,用于在一个字符串中查找另一个子字符串的首次出现。它定义在 string.h 头文件中。该函数返回一个指向子字符串首次出现位置的指针,如果未找到子字符串,则返回 NULL

strstr 函数原型
char *strstr(const char *haystack, const char *needle);

参数说明

  • const char *haystack:指向要搜索的目标字符串(大字符串)的指针。
  • const char *needle:指向要查找的子字符串(小字符串)的指针。

返回值

  • 返回 needlehaystack 中首次出现的位置的指针。
  • 如果 needle 不在 haystack 中,则返回 NULL

使用场景

  • 查找字符串中的子字符串。
  • 在实现字符串处理函数时进行子字符串搜索。
示例代码

以下是使用 strstr 函数的示例代码:

#include <stdio.h>
#include <string.h>

int main() {
    const char *haystack = "Hello, World!";
    const char *needle = "World";
    
    // 查找子字符串
    char *result = strstr(haystack, needle);
    
    if (result) {
        printf("Found '%s' in '%s' at position: %ld\n", needle, haystack, result - haystack);
    } else {
        printf("'%s' not found in '%s'\n", needle, haystack);
    }
    
    return 0;
}

代码解释

  1. 定义目标字符串和子字符串

    • const char *haystack = "Hello, World!"; 定义一个目标字符串 haystack
    • const char *needle = "World"; 定义一个要查找的子字符串 needle
  2. 查找子字符串

    • char *result = strstr(haystack, needle); 使用 strstr 函数查找 needlehaystack 中的首次出现位置,并将结果存储在 result 中。
  3. 打印结果

    • if (result) 判断是否找到了子字符串。
    • printf("Found '%s' in '%s' at position: %ld\n", needle, haystack, result - haystack); 打印找到子字符串的位置。如果未找到,则打印未找到信息。

输出结果

Found 'World' in 'Hello, World!' at position: 7

结果分析

  • strstr 函数找到子字符串 "World" 在目标字符串 "Hello, World!" 中首次出现的位置。
  • result - haystack 计算出子字符串的起始位置,结果为 7
注意事项
  • strstr 函数区分大小写,即大写字母和小写字母被视为不同的字符。
  • 如果 needle 是一个空字符串,strstr 函数将返回 haystack 指针,即目标字符串的起始位置。

strerror()

strerror 是 C 标准库中的一个函数,用于将错误码转换为描述错误的字符串。它定义在 string.h 头文件中。该函数对于处理系统调用或库函数返回的错误码非常有用,可以将错误码转换为更易读的错误消息。

strerror 函数原型
char *strerror(int errnum);

strerror 是 C 标准库中的一个函数,用于将错误码转换为描述错误的字符串。它定义在 string.h 头文件中。该函数对于处理系统调用或库函数返回的错误码非常有用,可以将错误码转换为更易读的错误消息。

strerror 函数原型
char *strerror(int errnum);

参数说明

  • int errnum:错误码,即需要转换为描述性错误消息的错误代码。

返回值

  • 返回一个指向静态字符串的指针,该字符串描述了 errnum 指定的错误。

使用场景

  • 将错误码转换为人类可读的错误消息。
  • 调试和错误处理,提供更详细的错误信息。
示例代码

以下是使用 strerror 函数的示例代码:

#include <stdio.h>
#include <string.h>
#include <errno.h>

int main() {
    FILE *file = fopen("nonexistentfile.txt", "r");
    
    if (file == NULL) {
        int errnum = errno;
        printf("Error opening file: %s\n", strerror(errnum));
    } else {
        // 如果文件成功打开,进行相关操作
        fclose(file);
    }
    
    return 0;
}

代码解释

  1. 尝试打开一个不存在的文件

    • FILE *file = fopen("nonexistentfile.txt", "r"); 尝试以只读模式打开一个不存在的文件 nonexistentfile.txt
  2. 检查文件指针是否为空

    • if (file == NULL) 判断文件指针是否为空,即文件是否打开失败。
  3. 获取并打印错误信息

    • int errnum = errno; 获取错误码。
    • printf("Error opening file: %s\n", strerror(errnum)); 使用 strerror 函数将错误码转换为描述性错误消息并打印。
  4. 关闭文件

    • fclose(file); 如果文件成功打开,关闭文件。

输出结果

Error opening file: No such file or directory

结果分析

  • 由于文件 nonexistentfile.txt 不存在,fopen 函数返回 NULL,错误码 errno 被设置为 ENOENT(对应的值可能因系统不同而不同)。
  • strerror 函数将 errno 对应的错误码转换为描述性错误消息 “No such file or directory”,并打印出来。
注意事项
  • 返回的指针指向的是静态字符串,该字符串可能在后续的函数调用中被覆盖。因此,如果需要长期保存错误信息,应将其复制到另一个缓冲区中。
  • 不同平台上的错误码及其对应的错误消息可能会有所不同。
常见的错误码

以下是一些常见的错误码及其含义:

  • EACCES:权限被拒绝。
  • EEXIST:文件已存在。
  • EINVAL:无效的参数。
  • ENOMEM:内存不足。
  • ENOENT:没有找到该文件或目录。

memcpy()memmove()

memcpy 是 C 标准库中的一个函数,用于将内存区域中的数据从一个位置复制到另一个位置。它定义在 string.h 头文件中。该函数非常高效,通常用于数组或结构体的内存拷贝。

memcpy 函数原型
void *memcpy(void *dest, const void *src, size_t n);

参数说明

  • void *dest:指向目标内存区域的指针。
  • const void *src:指向源内存区域的指针。
  • size_t n:要复制的字节数。

返回值

  • 返回指向目标内存区域 dest 的指针。

使用场景

  • 在内存中复制数据,例如复制数组内容。
  • 在处理结构体时,复制结构体的数据。
  • 在低级别内存操作中进行数据移动。

示例代码

以下是使用 memcpy 函数的示例代码:

#include <stdio.h>
#include <string.h>

int main() {
    char src[50] = "Hello, World!";
    char dest[50];

    // 复制 src 到 dest
    memcpy(dest, src, strlen(src) + 1);

    // 打印复制后的结果
    printf("Source: %s\n", src);
    printf("Destination: %s\n", dest);

    return 0;
}

代码解释

  1. 定义源和目标数组

    • char src[50] = "Hello, World!"; 定义并初始化源数组 src
    • char dest[50]; 定义目标数组 dest,未初始化。
  2. 复制数据

    • memcpy(dest, src, strlen(src) + 1); 使用 memcpy 函数将 src 中的数据复制到 dest 中。strlen(src) + 1 用于包括字符串末尾的空字符 \0
  3. 打印结果

    • printf("Source: %s\n", src); 打印源数组 src
    • printf("Destination: %s\n", dest); 打印目标数组 dest

输出结果

Source: Hello, World!
Destination: Hello, World!

结果分析

  • memcpy 函数成功地将源数组 src 中的数据复制到目标数组 dest 中。

注意事项

  • 内存重叠问题memcpy 不处理内存区域重叠的情况。如果源和目标内存区域重叠,应使用 memmove 函数。
  • 安全性:确保目标内存区域 dest 足够大以容纳复制的数据,避免缓冲区溢出。
memmove 函数原型

memmove 函数与 memcpy 类似,但它能正确处理内存区域重叠的情况。其函数原型为:

void *memmove(void *dest, const void *src, size_t n);
memcpymemmove 的区别
  • memcpy:用于内存区域不重叠的情况,效率较高。
  • memmove:用于内存区域可能重叠的情况,安全性更高。

memmove 示例代码

以下是使用 memmove 函数的示例代码:

#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "Hello, World!";
    
    // 使用 memmove 在字符串内重叠区域进行复制
    memmove(str + 7, str, 5);
    str[12] = '\0'; // 添加字符串结束符

    // 打印结果
    printf("Result: %s\n", str);

    return 0;
}

代码解释

  1. 定义字符串

    • char str[] = "Hello, World!"; 定义并初始化字符串 str
  2. 使用 memmove 进行复制

    • memmove(str + 7, str, 5); 将字符串前 5 个字符复制到位置 7 开始的位置。
  3. 添加字符串结束符

    • str[12] = '\0'; 添加字符串结束符。
  4. 打印结果

    • printf("Result: %s\n", str); 打印最终字符串。

输出结果

Result: Hello, Hello!

总结

memcpy 是一个高效的内存拷贝函数,但在使用时要注意内存区域不应重叠。对于可能重叠的内存区域,应使用 memmove 函数。正确使用这些内存操作函数,可以在 C 程序中实现高效且安全的内存管理。

memset()

memset 是 C 标准库中的一个函数,用于将内存区域的所有字节设置为指定的值。它定义在 string.h 头文件中。该函数通常用于初始化数组或结构体,或者在处理内存之前将其重置为某个特定值。

memset 函数原型
void *memset(void *s, int c, size_t n);

参数说明

  • void *s:指向要填充的内存区域的指针。
  • int c:要设置的值,以无符号字符形式解释。
  • size_t n:要设置的字节数。

返回值

  • 返回指向目标内存区域 s 的指针。

使用场景

  • 初始化数组或结构体。
  • 重置内存区域。
  • 清除敏感数据(如密码)以防止被泄露。
示例代码

以下是使用 memset 函数的示例代码:

#include <stdio.h>
#include <string.h>

int main() {
    char buffer[50];

    // 使用 memset 初始化内存区域,将 buffer 中的前 50 个字节设置为 0
    memset(buffer, 0, sizeof(buffer));

    // 打印结果,检查内存区域是否成功设置
    for (int i = 0; i < 50; i++) {
        printf("%d ", buffer[i]);
    }
    
    return 0;
}

代码解释

  1. 定义缓冲区

    • char buffer[50]; 定义一个字符数组 buffer
  2. 使用 memset 初始化内存区域

    • memset(buffer, 0, sizeof(buffer)); 使用 memset 函数将 buffer 中的前 50 个字节设置为 0
  3. 打印结果

    • 使用循环打印 buffer 中的每个元素,以检查内存区域是否成功设置为 0

输出结果

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 

结果分析

  • memset 函数成功地将 buffer 数组的前 50 个字节设置为 0
常见用法
  1. 初始化数组

    int arr[10];
    memset(arr, 0, sizeof(arr)); // 将数组 arr 中的所有元素设置为 0
    
  2. 重置结构体

    struct MyStruct {
        int a;
        float b;
    };
    
    struct MyStruct obj;
    memset(&obj, 0, sizeof(obj)); // 将结构体 obj 的所有成员设置为 0
    
  3. 清除敏感数据

    char password[20] = "secretpassword";
    memset(password, 0, sizeof(password)); // 清除密码数据
    
注意事项
  • memset 设置的是字节值,因此对于多字节类型(如 intfloat),需要注意可能导致的数据不一致。例如,将 int 数组的所有元素设置为 0 并不会如预期那样设置每个元素为 0
  • memset 设置的是无符号字符值,即使传递的是负数值,也会被解释为无符号字符。

memcmp()

memcmp 是 C 标准库中的一个函数,用于比较两个内存区域的内容。它定义在 string.h 头文件中。该函数常用于在内存级别上检查两个数据块是否相等,或确定它们的字节顺序。

memcmp 函数原型
int memcmp(const void *s1, const void *s2, size_t n);

参数说明

  • const void *s1:指向第一个内存区域的指针。
  • const void *s2:指向第二个内存区域的指针。
  • size_t n:要比较的字节数。

返回值

  • 返回一个整数,根据比较结果不同而不同:
    • 如果 s1s2n 个字节完全相同,返回 0
    • 如果在第一个不同的字节位置 s1 的字节大于 s2 的字节,返回一个正整数。
    • 如果在第一个不同的字节位置 s1 的字节小于 s2 的字节,返回一个负整数。

使用场景

  • 比较两个数组或结构体的数据是否相同。
  • 在内存级别上排序或查找数据。
示例代码

以下是使用 memcmp 函数的示例代码:

#include <stdio.h>
#include <string.h>

int main() {
    char buffer1[] = "Hello, World!";
    char buffer2[] = "Hello, World!";
    char buffer3[] = "Hello, C Programming!";

    int result1 = memcmp(buffer1, buffer2, strlen(buffer1));
    int result2 = memcmp(buffer1, buffer3, strlen(buffer1));

    // 打印比较结果
    printf("Result1: %d\n", result1); // 预期结果 0,因为 buffer1 和 buffer2 相同
    printf("Result2: %d\n", result2); // 预期结果为负数,因为 buffer1 和 buffer3 不同

    return 0;
}

代码解释

  1. 定义三个缓冲区

    • char buffer1[] = "Hello, World!"; 定义并初始化第一个缓冲区 buffer1
    • char buffer2[] = "Hello, World!"; 定义并初始化第二个缓冲区 buffer2
    • char buffer3[] = "Hello, C Programming!"; 定义并初始化第三个缓冲区 buffer3
  2. 比较缓冲区

    • int result1 = memcmp(buffer1, buffer2, strlen(buffer1)); 比较 buffer1buffer2,比较长度为 strlen(buffer1)
    • int result2 = memcmp(buffer1, buffer3, strlen(buffer1)); 比较 buffer1buffer3,比较长度为 strlen(buffer1)
  3. 打印结果

    • printf("Result1: %d\n", result1); 打印 result1,预期结果为 0,因为 buffer1buffer2 相同。
    • printf("Result2: %d\n", result2); 打印 result2,预期结果为负数,因为 buffer1buffer3 不同。

输出结果

Result1: 0
Result2: -32

结果分析

  • result10,表明 buffer1buffer2 的前 13 个字节完全相同。
  • result2 为负数,表明 buffer1buffer3 的前 13 个字节不同,在比较过程中 buffer1 的一个字节小于 buffer3 的对应字节。
注意事项
  • memcmp 函数按字节进行比较,不考虑字符编码或其他数据格式。

  • 比较时,如果 n 大于实际数据长度,可能会导致未定义行为,应确保 n 不超过实际数据长度。

  • 在处理二进制数据或非字符串数据时,memcmp 比较有用。

  • 在比较结构体时,确保结构体没有未初始化的填充字节,以避免意外比较结果。

atoi()

atoi 是 C 标准库中的一个函数,用于将字符串转换为整数。它定义在 stdlib.h 头文件中。该函数通常用于从输入字符串中提取整数值,尤其在处理用户输入或读取文本数据时。

atoi 函数原型
int atoi(const char *str);

参数说明

  • const char *str:指向要转换的字符串。

返回值

  • 返回转换后的整数值。如果字符串中不包含有效的数字,则返回值为 0

使用场景

  • 从字符串中提取整数。
  • 处理用户输入,将字符串形式的数字转换为整数进行计算。
  • 解析配置文件或其他文本数据中的数值。
示例代码

以下是使用 atoi 函数的示例代码:

#include <stdio.h>
#include <stdlib.h>

int main() {
    char str1[] = "12345";
    char str2[] = "42";
    char str3[] = "abc123";
    char str4[] = "123abc";

    int num1 = atoi(str1);
    int num2 = atoi(str2);
    int num3 = atoi(str3);
    int num4 = atoi(str4);

    // 打印转换结果
    printf("String: %s, Integer: %d\n", str1, num1); // 预期输出 12345
    printf("String: %s, Integer: %d\n", str2, num2); // 预期输出 42
    printf("String: %s, Integer: %d\n", str3, num3); // 预期输出 0
    printf("String: %s, Integer: %d\n", str4, num4); // 预期输出 123

    return 0;
}

代码解释

  1. 定义字符串

    • char str1[] = "12345"; 定义并初始化字符串 str1
    • char str2[] = "42"; 定义并初始化字符串 str2
    • char str3[] = "abc123"; 定义并初始化字符串 str3
    • char str4[] = "123abc"; 定义并初始化字符串 str4
  2. 使用 atoi 函数转换字符串

    • int num1 = atoi(str1); 将字符串 str1 转换为整数 num1
    • int num2 = atoi(str2); 将字符串 str2 转换为整数 num2
    • int num3 = atoi(str3); 将字符串 str3 转换为整数 num3
    • int num4 = atoi(str4); 将字符串 str4 转换为整数 num4
  3. 打印转换结果

    • 使用 printf 打印每个字符串及其对应的整数值。

输出结果

String: 12345, Integer: 12345
String: 42, Integer: 42
String: abc123, Integer: 0
String: 123abc, Integer: 123

结果分析

  • 对于纯数字字符串,atoi 能够正确转换为整数值。
  • 对于不以数字开头的字符串,atoi 返回 0
  • 对于以数字开头但包含非数字字符的字符串,atoi 只转换前面的数字部分
注意事项
  • atoi 不会检测错误,对于无效输入(如全非数字字符的字符串),返回值为 0。这种情况无法区分合法的 0 和无效输入。
  • atoi 不处理整数溢出情况,对于超出 int 范围的值,行为未定义。

atof()

atof 是 C 标准库中的一个函数,用于将字符串转换为浮点数。它定义在 stdlib.h 头文件中。该函数通常用于从输入字符串中提取浮点数值,尤其在处理用户输入或读取文本数据时。

atof 函数原型
double atof(const char *str);

参数说明

  • const char *str:指向要转换的字符串。

返回值

  • 返回转换后的浮点数值。如果字符串中不包含有效的浮点数,则返回值为 0.0

使用场景

  • 从字符串中提取浮点数。
  • 处理用户输入,将字符串形式的数字转换为浮点数进行计算。
  • 解析配置文件或其他文本数据中的数值。
示例代码

以下是使用 atof 函数的示例代码:

#include <stdio.h>
#include <stdlib.h>

int main() {
    char str1[] = "123.45";
    char str2[] = "42.0";
    char str3[] = "abc123.45";
    char str4[] = "123.45abc";

    double num1 = atof(str1);
    double num2 = atof(str2);
    double num3 = atof(str3);
    double num4 = atof(str4);

    // 打印转换结果
    printf("String: %s, Double: %f\n", str1, num1); // 预期输出 123.450000
    printf("String: %s, Double: %f\n", str2, num2); // 预期输出 42.000000
    printf("String: %s, Double: %f\n", str3, num3); // 预期输出 0.000000
    printf("String: %s, Double: %f\n", str4, num4); // 预期输出 123.450000

    return 0;
}

代码解释

  1. 定义字符串

    • char str1[] = "123.45"; 定义并初始化字符串 str1
    • char str2[] = "42.0"; 定义并初始化字符串 str2
    • char str3[] = "abc123.45"; 定义并初始化字符串 str3
    • char str4[] = "123.45abc"; 定义并初始化字符串 str4
  2. 使用 atof 函数转换字符串

    • double num1 = atof(str1); 将字符串 str1 转换为浮点数 num1
    • double num2 = atof(str2); 将字符串 str2 转换为浮点数 num2
    • double num3 = atof(str3); 将字符串 str3 转换为浮点数 num3
    • double num4 = atof(str4); 将字符串 str4 转换为浮点数 num4
  3. 打印转换结果

    • 使用 printf 打印每个字符串及其对应的浮点数值。

输出结果

String: 123.45, Double: 123.450000
String: 42.0, Double: 42.000000
String: abc123.45, Double: 0.000000
String: 123.45abc, Double: 123.450000

结果分析

  • 对于纯浮点数字字符串,atof 能够正确转换为浮点数值。
  • 对于不以数字开头的字符串,atof 返回 0.0
  • 对于以数字开头但包含非数字字符的字符串,atof 只转换前面的数字部分。
注意事项
  • atof 不会检测错误,对于无效输入(如全非数字字符的字符串),返回值为 0.0。这种情况无法区分合法的 0.0 和无效输入。
  • atof 不处理浮点数溢出情况,对于超出 double 范围的值,行为未定义。

操作符

算术操作符

以下是 C 语言中常见的算术操作符及其描述的表格:

操作符描述示例示例结果
+加法a + b7 (如果 a3b4)
-减法a - b-1 (如果 a3b4)
*乘法a * b12 (如果 a3b4)
/除法a / b0 (如果 a3b4,整数除法)
%取余(模运算)a % b3 (如果 a11b4)
++自增(增加1)a++5 (如果 a4,在自增后 a5)
--自减(减少1)a--3 (如果 a4,在自减后 a3)
说明
  • 加法 (+):将两个操作数相加。
  • 减法 (-):从第一个操作数中减去第二个操作数。
  • 乘法 (*):将两个操作数相乘。
  • 除法 (/):将第一个操作数除以第二个操作数。对于整数,结果是整数部分(舍去小数)。
  • 取余 (%):返回第一个操作数除以第二个操作数的余数。
  • 自增 (++):将变量的值增加1。可以前置(++a)或后置(a++)。前置自增会先增加值再使用,后置自增会先使用值再增加。
  • 自减 (--):将变量的值减少1。可以前置(--a)或后置(a--)。前置自减会先减少值再使用,后置自减会先使用值再减少。

在 C 语言中,整数除法和小数除法(浮点数除法)的行为有所不同。下面详细解释这两种除法及其处理方式:

整数除法和小数除法

整数除法

定义

  • 当两个操作数都是整数时,进行整数除法。结果也是一个整数,任何小数部分都会被丢弃(即向零舍入)。

示例

#include <stdio.h>

int main() {
    int a = 7;
    int b = 3;
    int result = a / b; // 整数除法

    printf("Integer division result: %d\n", result); // 输出结果是2
    return 0;
}

解释

  • 在上面的示例中,7 / 3 的结果是 2,因为小数部分 .333... 被丢弃。

小数除法

定义

  • 当至少一个操作数是浮点数(floatdouble),进行小数除法。结果是浮点数,保留小数部分。

示例

#include <stdio.h>

int main() {
    float a = 7.0;
    float b = 3.0;
    float result = a / b; // 小数除法

    printf("Floating-point division result: %.2f\n", result); // 输出结果是2.33
    return 0;
}
前置++和后置++区别

在 C 语言中,前置自增(++a)和后置自增(a++)操作符用于将变量的值增加 1。尽管它们的基本作用是相同的,但在表达式中的行为和计算顺序有所不同。下面详细解释前置自增和后置自增的区别。

  • 前置自增 (++a):先增加变量的值,然后使用增加后的值。
  • 后置自增 (a++):先使用变量的当前值,然后增加变量的值。

赋值操作符在 C 语言中用于将一个值赋给一个变量。基本的赋值操作符是 =,但 C 语言还提供了多种复合赋值操作符来简化常见的操作。以下是 C 语言中常见的赋值操作符及其功能:

赋值操作符

基本赋值操作符
操作符描述示例解释
=赋值操作符,将右侧的值赋给左侧的变量a = 5;5 赋值给变量 a
复合赋值操作符

这些操作符结合了基本赋值操作和其他算术运算,简化了代码书写。

操作符描述示例解释
+=加法赋值,将右侧的值加到左侧变量上a += 3;等同于 a = a + 3;
-=减法赋值,将右侧的值从左侧变量中减去a -= 2;等同于 a = a - 2;
*=乘法赋值,将右侧的值乘以左侧变量a *= 4;等同于 a = a * 4;
/=除法赋值,将左侧变量除以右侧的值a /= 5;等同于 a = a / 5;
%=取余赋值,将左侧变量对右侧的值取余a %= 3;等同于 a = a % 3;
示例代码
#include <stdio.h>

int main() {
    int a = 10;
    int b = 5;

    // 使用基本赋值操作符
    a = b; // a 现在等于 5

    // 使用复合赋值操作符
    a += 3;  // a 现在等于 8(原值 5 加上 3)
    a -= 2;  // a 现在等于 6(原值 8 减去 2)
    a *= 4;  // a 现在等于 24(原值 6 乘以 4)
    a /= 3;  // a 现在等于 8(原值 24 除以 3)
    a %= 3;  // a 现在等于 2(原值 8 对 3 取余)

    printf("a = %d\n", a); // 输出结果是 2

    return 0;
}

位操作符

操作符名称描述示例结果
&按位与对应位都是 1 时结果为 1,否则为 05 & 31 (二进制:0000 0001)
``按位或只要对应位有一个是 1,结果为 1`5
^按位异或对应位不同则结果为 1,否则为 05 ^ 36 (二进制:0000 0110)
~按位取反将每个位取反(0 变 1,1 变 0)~5-6 (二进制:1111 1010)
<<左移将操作数的位向左移动指定的位数,右边用零填充5 << 110 (二进制:0000 1010)
>>右移将操作数的位向右移动指定的位数,左边用符号位或零填充20 >> 110 (二进制:0000 1010)

关系操作符

操作符名称描述示例结果
==等于比较两个值是否相等a == b真或假
!=不等于比较两个值是否不相等a != b真或假
>大于比较左边的值是否大于右边的值a > b真或假
<小于比较左边的值是否小于右边的值a < b真或假
>=大于或等于比较左边的值是否大于或等于右边的值a >= b真或假
<=小于或等于比较左边的值是否小于或等于右边的值a <= b真或假

逻辑操作符

操作符名称描述示例结果
&&逻辑与当且仅当两个操作数都为真时,结果为真a && b真或假
``逻辑或当且仅当两个操作数都为假时,结果为假
!逻辑非将操作数的布尔值取反!a真或假

条件操作符

在 C 语言中,条件操作符(?:),也称为三元操作符,是一个用于简化条件判断的操作符。它允许你在单行代码中进行条件判断,并返回两个可能结果中的一个。条件操作符是一个简写形式的 if-else 语句,通常用于需要根据条件选择值的场景。

条件操作符的基本语法如下:

condition ? expression1 : expression2;
  • condition:一个布尔表达式。如果 condition 为真(非零),则计算并返回 expression1 的值。
  • expression1:当 condition 为真时计算并返回的值。
  • expression2:当 condition 为假(零)时计算并返回的值。
#include <stdio.h>

int main() {
    int a = 10;
    int b = 20;
    int max;

    max = (a > b) ? a : b; // 如果 a > b,则 max = a,否则 max = b

    printf("The maximum value is %d\n", max); // 输出: The maximum value is 20

    return 0;
}

在这个例子中,条件操作符用来判断 ab 的大小,并将较大的值赋给 max

  • 条件操作符(?::用于简化 if-else 语句,允许在单行代码中进行条件判断。
  • 语法condition ? expression1 : expression2;,根据 condition 的值返回 expression1expression2
  • 优先级和结合性:优先级较低,结合性从左到右。嵌套使用时需注意可读性。

操作符优先级表

以下是 C 语言中常见操作符的优先级和结合性表格(从高到低):

优先级操作符描述结合性
1() [] . ->圆括号、数组下标、成员访问左结合
2++ -- ! ~ + -自增、自减、逻辑非、按位取反、正负号右结合
3* / %乘法、除法、取余左结合
4+ -加法、减法左结合
5<< >>左移、右移左结合
6< <= > >=小于、小于等于、大于、大于等于左结合
7== !=等于、不等于左结合
8&按位与左结合
9^按位异或左结合
10``按位或
11&&逻辑与左结合
12``
13? :条件运算符左结合
14= += -= *= /= %= <<= >>= &= ^= `=`赋值操作符
15,逗号操作符左结合

作用域

在 C 语言中,作用域(scope)指的是程序中变量和函数名称可以被访问和使用的范围。主要有两种常见的作用域:块作用域(block scope)文件作用域(file scope)。这两种作用域在变量和函数的声明和使用上有不同的规则和影响。

块作用域(Block Scope)

定义

  • 块作用域是指在代码块(通常是由花括号 {} 包围的区域)内定义的变量或函数的作用范围。每个代码块都是一个新的作用域。

特点

  • 局部变量:在代码块内定义的变量只有在该块内可见,块外无法访问这些变量。
  • 嵌套作用域:一个代码块可以嵌套在另一个代码块内,内层代码块可以访问外层代码块中的变量,但外层代码块无法访问内层代码块中的局部变量。

示例

#include <stdio.h>

int main() {
    int x = 10; // x 的作用域是 main 函数的整个代码块

    {
        int y = 20; // y 的作用域是这个内嵌的代码块
        printf("Inside inner block: x = %d, y = %d\n", x, y);
    }

    // printf("Outside inner block: y = %d\n", y); // 错误:y 在这里不可见
    printf("Outside inner block: x = %d\n", x);

    return 0;
}

解释

  • 在内嵌的代码块中,y 只在该块内可见,外层代码块无法访问 y
  • 变量 x 在整个 main 函数的作用域内可见。

文件作用域(File Scope)

定义

  • 文件作用域是指在整个源文件中有效的作用域。变量或函数在文件的任何地方声明,只要它们没有被局部作用域限制,就可以在整个文件内访问。

特点

  • 全局变量:在文件顶部定义的变量,默认具有文件作用域,文件内的所有函数都可以访问这些变量。
  • 全局函数:在文件内定义的函数,默认也具有文件作用域,可以被该文件中的任何代码调用。

示例

#include <stdio.h>

int global_var = 100; // 文件作用域

void print_global_var() {
    printf("Global variable: %d\n", global_var);
}

int main() {
    printf("Main function: %d\n", global_var);
    print_global_var();
    return 0;
}

解释

  • global_var 在整个源文件中有效,可以在 main 函数和 print_global_var 函数中访问。

关键字

C 语言中的关键字是语言预定义的保留字,它们具有特殊的意义和用途,不能用作变量名、函数名或其他标识符。关键字定义了 C 语言的基本语法和控制结构,帮助程序员控制程序的流程和数据处理。

以下是 C 语言中的所有关键字及其简要说明:

关键字说明
auto自动变量,默认存储类别。
break退出当前的循环或 switch 语句。
caseswitch 语句中用于标识不同的分支。
char用于声明字符类型变量。
const指定变量的值在初始化后不可更改。
continue跳过当前循环的剩余部分,继续下一次循环。
defaultswitch 语句中定义默认的分支。
do定义一个 do-while 循环的开始。
double用于声明双精度浮点型变量。
else用于定义 if 语句的另一条分支。
enum定义枚举类型。
extern声明一个外部变量或函数。
float用于声明单精度浮点型变量。
for定义一个 for 循环。
goto跳转到程序中的某个标号。
if定义一个条件语句。
int用于声明整数类型变量。
long用于声明长整型变量。
register建议编译器将变量存储在寄存器中,以提高访问速度。
return从函数中返回一个值。
short用于声明短整型变量。
signed指定整数变量为有符号类型(默认)。
sizeof返回数据类型或变量的大小(以字节为单位)。
static指定变量的存储类别为静态,作用域为函数内。
struct定义一个结构体类型。
switch定义一个多分支的选择结构。
typedef定义新类型名。
union定义一个共用体类型。
unsigned指定整数变量为无符号类型。
void指示函数不返回值或指针不指向任何类型。
volatile指定变量的值可能会被意外改变,禁止优化。
while定义一个 while 循环。

关键字的用途

  • 控制结构:如 ifelseforwhileswitch 用于控制程序的执行流程。
  • 数据类型:如 intcharfloatdoublevoid 用于定义变量和函数的类型。
  • 存储类别:如 autostaticexternregister 用于定义变量的存储方式和作用域。
  • 函数和结构:如 structuniontypedef 用于定义复杂数据类型和简化类型名。

sizeof 是 C 语言中的一个关键字,用于获取数据类型或变量所占的字节数。在程序中使用 sizeof 可以帮助确定内存布局和数据结构的大小,确保程序的内存管理和操作的正确性。

sizeof 操作符的用法

sizeof 操作符可以用于以下几种情况:

  1. 数据类型:获取特定数据类型的大小。
  2. 变量:获取特定变量的大小。
  3. 表达式:获取表达式结果类型的大小。
sizeof(type)
sizeof(expression)
sizeof variable
获取数据类型的大小
#include <stdio.h>

int main() {
    printf("Size of int: %zu bytes\n", sizeof(int));
    printf("Size of char: %zu bytes\n", sizeof(char));
    printf("Size of float: %zu bytes\n", sizeof(float));
    printf("Size of double: %zu bytes\n", sizeof(double));
    return 0;
}
获取变量的大小
#include <stdio.h>

int main() {
    int a = 10;
    char b = 'A';
    float c = 3.14;
    double d = 5.12;

    printf("Size of a: %zu bytes\n", sizeof(a));
    printf("Size of b: %zu bytes\n", sizeof(b));
    printf("Size of c: %zu bytes\n", sizeof(c));
    printf("Size of d: %zu bytes\n", sizeof(d));
    return 0;
}
获取数组的大小
#include <stdio.h>

int main() {
    int arr[10];

    printf("Size of arr: %zu bytes\n", sizeof(arr));
    printf("Number of elements in arr: %zu\n", sizeof(arr) / sizeof(arr[0]));
    return 0;
}
获取结构体的大小
#include <stdio.h>

struct Point {
    int x;
    int y;
};

int main() {
    struct Point p;

    printf("Size of struct Point: %zu bytes\n", sizeof(struct Point));
    printf("Size of p: %zu bytes\n", sizeof(p));
    return 0;
}

signed和unsigned关键字

在 C 语言中,signedunsigned 关键字用于声明整数类型变量的符号属性。理解这两个关键字的作用和区别对于正确处理整数运算和避免潜在的错误至关重要。

signed 关键字
  • 描述:表示带符号整数,可以存储正数、负数和零。
  • 默认值:如果不指定 signedunsigned,整数类型默认是 signed
  • 取值范围:范围取决于具体的整数类型(charshortintlong)。对于 32 位系统,signed int 的取值范围通常是 -2147483648 到 2147483647。
#include <stdio.h>

int main() {
    signed int a = -10;
    signed int b = 20;
    printf("a = %d, b = %d\n", a, b);
    return 0;
}
unsigned 关键字
  • 描述:表示无符号整数,只能存储非负数(正数和零)。
  • 取值范围:范围取决于具体的整数类型(charshortintlong)。对于 32 位系统,unsigned int 的取值范围通常是 0 到 4294967295。
#include <stdio.h>

int main() {
    unsigned int a = 10;
    unsigned int b = 20;
    printf("a = %u, b = %u\n", a, b);
    return 0;
}
signedunsigned 的区别
  1. 取值范围

    • signed 整数类型可以存储负数、零和正数。
    • unsigned 整数类型只能存储零和正数。
  2. 内存分配

    • signedunsigned 类型在内存中占用的字节数相同,但它们解释这些字节的方式不同。
    • 例如,对于 32 位系统,signed intunsigned int 都占用 4 个字节。
  3. 使用场景

    • signed:当需要表示负数时使用。
    • unsigned:当只需要表示非负数时使用,通常用于位操作、无符号计数器和内存地址等。
混合使用 signedunsigned

在混合使用 signedunsigned 类型时需要小心,因为它们之间的运算可能导致意外的结果。例如,将一个 signed 整数与一个 unsigned 整数进行比较或运算时,signed 整数会被隐式转换为 unsigned 整数,这可能导致负数被解释为一个非常大的正数。

#include <stdio.h>

int main() {
    int a = -1;
    unsigned int b = 1;

    if (a < b) {
        printf("a is less than b\n");
    } else {
        printf("a is not less than b\n");
    }

    return 0;
}

在上述示例中,输出将是 “a is not less than b”,因为 a 被隐式转换为 unsigned,导致 -1 变成一个非常大的正数。

extern关键字

extern 关键字在 C 语言中用于声明变量或函数在其他文件中定义。这种声明告诉编译器变量或函数的存在和其类型,但不定义其实际存储或实现。使用 extern 可以实现跨文件的代码组织和模块化。

  1. 跨文件共享变量

    当多个源文件需要访问同一个全局变量时,可以使用 extern 关键字来声明该变量。变量的实际定义(存储)应该在一个源文件中完成。

    // file1.c
    #include <stdio.h>
    
    int global_var = 10;  // 变量定义
    
    void print_global() {
        printf("Global variable: %d\n", global_var);
    }
    
    // file2.c
    #include <stdio.h>
    
    extern int global_var;  // 变量声明
    
    void modify_global() {
        global_var = 20;
    }
    
    // main.c
    #include <stdio.h>
    
    extern void print_global();
    extern void modify_global();
    
    int main() {
        print_global();       // 输出: Global variable: 10
        modify_global();
        print_global();       // 输出: Global variable: 20
        return 0;
    }
    
  2. 跨文件共享函数

    函数的声明通常会放在头文件中,而在源文件中定义函数的具体实现。

    // math_utils.h
    #ifndef MATH_UTILS_H
    #define MATH_UTILS_H
    
    extern int add(int a, int b);  // 函数声明
    
    #endif
    
    // math_utils.c
    #include "math_utils.h"
    
    int add(int a, int b) {  // 函数定义
        return a + b;
    }
    
    // main.c
    #include <stdio.h>
    #include "math_utils.h"
    
    int main() {
        int result = add(5, 3);
        printf("Result: %d\n", result);
        return 0;
    }
    

全局变量共享

  • file1.c:定义全局变量 global_var
  • file2.c:使用 extern 关键字声明 global_var,并修改其值。
  • main.c:使用 extern 关键字声明 print_globalmodify_global 函数,并调用它们。

函数共享

  • math_utils.h:声明 add 函数。
  • math_utils.c:定义 add 函数。
  • main.c:包含头文件 math_utils.h,并调用 add 函数。
extern 关键字的特点
  1. 声明而不定义
    • extern 声明的变量或函数在其他源文件中定义,编译器知道其类型和名称,但不分配存储或实现。
  2. 全局作用域
    • extern 声明通常用于全局变量和函数,跨多个源文件共享数据和功能。
  3. 在头文件中的使用
    • extern 声明可以放在头文件中,以便在多个源文件中包含和使用。
  4. 可选的 extern
    • 在函数声明中,extern 关键字是可选的,因为函数的默认声明就是 extern。例如,extern int add(int a, int b);int add(int a, int b); 是等价的。

static关键字

static 关键字在 C 语言中有多种用途,主要用于控制变量或函数的存储期和链接性。具体来说,static 可以用于局部变量、全局变量和函数。下面是对 static 关键字在这三种场景中的详细介绍。

局部变量

static 用于局部变量时,该变量的存储期为整个程序的运行期,而不是通常的局部变量的自动存储期(即函数调用期间)。这意味着局部 static 变量在函数调用结束后不会被销毁,它会保留其值直到下一次函数调用。

#include <stdio.h>

void counter() {
    static int count = 0;  // 静态局部变量,只初始化一次
    count++;
    printf("Count: %d\n", count);
}

int main() {
    counter();  // 输出: Count: 1
    counter();  // 输出: Count: 2
    counter();  // 输出: Count: 3
    return 0;
}
全局变量

static 用于全局变量时,该变量的作用域仅限于定义它的源文件。这意味着该变量不能被其他源文件访问,从而实现变量的封装。

  • file1.c:定义了一个静态全局变量。
// file1.c
#include <stdio.h>

static int global_var = 10;  // 静态全局变量

void print_global_var() {
    printf("Global variable in file1.c: %d\n", global_var);
}
  • file2.c:尝试访问 file1.c 中的静态全局变量会导致编译错误。
// file2.c
#include <stdio.h>

extern void print_global_var();

int main() {
    // 访问 file1.c 中的静态全局变量会导致编译错误
    // printf("Global variable in file2.c: %d\n", global_var);

    print_global_var();
    return 0;
}
函数

static 用于函数时,该函数的作用域仅限于定义它的源文件。这意味着该函数不能被其他源文件调用,从而实现函数的封装。

  • file1.c:定义了一个静态函数。
// file1.c
#include <stdio.h>

static void static_function() {  // 静态函数
    printf("This is a static function.\n");
}

void call_static_function() {
    static_function();
}
  • file2.c:尝试调用 file1.c 中的静态函数会导致编译错误。
// file2.c
#include <stdio.h>

extern void call_static_function();

int main() {
    // 调用 file1.c 中的静态函数会导致编译错误
    // static_function();

    call_static_function();
    return 0;
}

总结:

所以如果希望全局变量和函数不被他人使用的话,可以在前面加入static关键字

const关键字

在C语言中,const 关键字用于定义常量,这意味着被修饰的变量的值在其生命周期内不能被修改。const 关键字的使用可以增强代码的可读性和安全性,防止意外修改变量的值。以下是 const 关键字的详细介绍及其各种用法。

定义一个不可修改的变量:

const int a = 10;

任何试图修改 a 的操作都会导致编译错误:

a = 20; // 错误,不能修改 const 变量

typedef关键字

typedef 是 C 语言中的一个关键字,用于创建类型别名。通过 typedef,你可以为已有的数据类型定义一个新的名字,从而简化代码的书写和提高代码的可读性。

typedef 的用途
  1. 简化复杂类型的表示

    • 例如,可以用 typedef 给复杂的结构体或指针类型定义简短的别名。
  2. 增加代码可读性

    • 通过定义有意义的别名,可以使代码更具可读性,特别是在处理复杂的数据结构时。
  3. 提高代码可移植性

    • 如果需要改变某个类型,可以只修改 typedef 声明,代码的其他部分无需改变。
语法
typedef 原始类型 新类型名;

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

  1. 基本类型别名
#include <stdio.h>

// 为 int 类型定义一个新的别名
typedef int Integer;

int main() {
    Integer a = 10;
    Integer b = 20;
    printf("Sum: %d\n", a + b);
    return 0;
}
  1. 结构体类型别名
#include <stdio.h>

// 定义一个结构体类型
typedef struct {
    char name[50];
    int age;
} Person;

int main() {
    Person p;
    p.age = 25;
    snprintf(p.name, sizeof(p.name), "Alice");
    printf("Name: %s, Age: %d\n", p.name, p.age);
    return 0;
}
  1. 指针类型别名
#include <stdio.h>

// 为指向 int 的指针定义一个别名
typedef int* IntPtr;

int main() {
    int value = 42;
    IntPtr ptr = &value;
    printf("Value: %d\n", *ptr);
    return 0;
}
  1. 函数指针类型别名
#include <stdio.h>

// 为一个特定类型的函数指针定义一个别名
typedef int (*FuncPtr)(int, int);

// 定义一个函数
int add(int a, int b) {
    return a + b;
}

int main() {
    FuncPtr func = add;
    printf("Sum: %d\n", func(10, 20));
    return 0;
}
  1. 定义指向一维数组的指针
#include <stdio.h>

// 定义指向长度为10的整型数组的指针类型
typedef int (*Array10Ptr)[10];

int main() {
    int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    Array10Ptr ptr = &arr;
    
	// 通过指针访问数组元素
	for (int i = 0; i < 10; i++) {
    	printf("%d ", (*ptr)[i]);
	}
	printf("\n");

	return 0;
}

语句

if语句

在 C 语言中,if 语句用于执行条件判断,并根据判断结果决定是否执行某些代码块。if 语句是控制流语句的核心之一,它使程序能够根据不同的条件执行不同的代码路径。

if 语句的基本语法如下:

if (condition) {
    // 当 condition 为真(非零)时执行的代码块
} else {
    // 当 condition 为假(零)时执行的代码块(可选)
}
  • condition 是一个布尔表达式。如果 condition 为真(非零),则执行 if 语句块中的代码;否则,执行 else 语句块中的代码(如果存在)。
简单 if 语句
#include <stdio.h>

int main() {
    int number = 10;

    if (number > 0) {
        printf("The number is positive.\n");
    }

    return 0;
}

在这个例子中,if 语句检查 number 是否大于零。如果条件为真(即 number 为正数),则执行 printf 语句。

elseif 语句
#include <stdio.h>

int main() {
    int number = -5;

    if (number > 0) {
        printf("The number is positive.\n");
    } else {
        printf("The number is not positive.\n");
    }

    return 0;
}

这里,if 语句检查 number 是否大于零。如果条件为假(即 number 为负数或零),则执行 else 语句块中的代码。

if-else if-else 语句
#include <stdio.h>

int main() {
    int number = 0;

    if (number > 0) {
        printf("The number is positive.\n");
    } else if (number < 0) {
        printf("The number is negative.\n");
    } else {
        printf("The number is zero.\n");
    }

    return 0;
}

在这个例子中,if-else if-else 语句用于检查 number 的不同值,并根据不同的条件执行不同的代码块。

注意事项
  1. 条件表达式if 语句的条件表达式可以是任何返回布尔值的表达式。只要表达式的结果为非零值,条件就为真;如果结果为零,条件就为假。
  2. 代码块if 语句块可以包含一个或多个语句。多个语句需要用花括号 {} 括起来,以形成一个代码块。如果只有一条语句,可以省略花括号。
  3. 嵌套 if 语句if 语句可以嵌套在其他 if 语句中,以处理更复杂的条件。
  4. 短路特性:在复合条件中,&&(逻辑与)和 ||(逻辑或)具有短路特性。当第一个条件足以确定结果时,第二个条件不会被计算。

switch语句

在 C 语言中,switch 语句是一种多分支选择控制结构,用于根据表达式的值选择不同的代码块执行。switch 语句特别适用于需要对一个变量的多个可能值进行不同处理的情况,通常用来替代多个 if-else if 语句。

switch (expression) {
    case value1:
        // 当 expression 等于 value1 时执行的代码
        break;
    case value2:
        // 当 expression 等于 value2 时执行的代码
        break;
    // 可以有多个 case
    default:
        // 当 expression 不等于任何 case 值时执行的代码(可选)
}
  • expression:要进行判断的表达式,通常是一个整数类型的变量或常量。
  • case valueN:定义了一个可能的值,如果 expression 的值等于 valueN,则执行该 case 下的代码。
  • break:用于终止 switch 语句。如果省略 break,程序会继续执行后续的 case 语句,直到遇到 breakswitch 语句结束。
  • default:可选的部分,用于处理所有 case 语句没有覆盖到的值。如果没有匹配的 case,则执行 default 中的代码。
基本示例
#include <stdio.h>

int main() {
    int day = 3;

    switch (day) {
        case 1:
            printf("Monday\n");
            break;
        case 2:
            printf("Tuesday\n");
            break;
        case 3:
            printf("Wednesday\n");
            break;
        case 4:
            printf("Thursday\n");
            break;
        case 5:
            printf("Friday\n");
            break;
        case 6:
            printf("Saturday\n");
            break;
        case 7:
            printf("Sunday\n");
            break;
        default:
            printf("Invalid day\n");
    }

    return 0;
}

在这个示例中,根据变量 day 的值打印对应的星期几。如果 day 的值不在 17 之间,则打印 “Invalid day”。

case 穿透(fall-through)
#include <stdio.h>

int main() {
    int grade = 85;

    switch (grade / 10) {
        case 10:
        case 9:
            printf("Excellent\n");
            break;
        case 8:
            printf("Good\n");
            break;
        case 7:
            printf("Average\n");
            break;
        default:
            printf("Needs Improvement\n");
    }

    return 0;
}

在这个例子中,case 10case 9 共享相同的代码块,这是一种 case 穿透(fall-through)现象。由于没有 breakgrade 为 90 时,case 9 也会执行 Excellent 的代码块。

注意事项
  1. expression 类型switch 语句中的 expression 必须是整型、字符型或者枚举类型。不能使用浮点型、字符串等其他类型。
  2. case 值的唯一性:每个 case 标签的值必须是唯一的,并且在同一个 switch 语句中,case 标签的值不能重复。
  3. break 语句的作用break 语句用于跳出 switch 语句。如果没有 break,程序会继续执行后续的 case 语句,这种现象称为 case 穿透(fall-through)。
  4. default 部分default 是可选的,通常用于处理所有 case 值未覆盖的情况。如果没有 default,并且没有匹配的 case,则 switch 语句什么也不做。
  5. 避免 case 穿透:如果不需要 case 穿透,确保每个 case 语句块最后有 break 语句,或者使用空语句 ; 来显式表示结束。

while语句

在 C 语言中,while 语句是一种控制流结构,用于在给定条件为真的情况下重复执行某段代码。while 语句非常适合用于需要重复执行操作直到条件不满足为止的场景。

while 语句的基本语法如下:

while (condition) {
    // 当 condition 为真(非零)时执行的代码块
}
  • condition:一个布尔表达式。如果 condition 为真(非零),则执行 while 语句块中的代码。否则,退出 while 循环。

    image-20240819205932430
基本示例
#include <stdio.h>

int main() {
    int count = 0;

    while (count < 5) {
        printf("Count is %d\n", count);
        count++; // 更新计数器,防止无限循环
    }

    return 0;
}

在这个例子中,while 语句会在 count 小于 5 的情况下重复执行 printf 语句。每次循环结束后,count 增加 1。当 count 达到 5 时,条件不再满足,循环终止。

无限循环

如果 while 语句的条件始终为真,循环将会无限执行下去,除非在循环体内部使用 break 语句来退出循环,或进行其他操作使条件变为假。

#include <stdio.h>

int main() {
    while (1) { // 无限循环
        printf("This is an infinite loop.\n");
        // 可以在这里使用 break 语句来退出循环
        break; // 退出循环
    }

    return 0;
}

在这个示例中,while (1) 表示无限循环。break 语句会在第一次迭代中退出循环。

使用 breakcontinue
#include <stdio.h>

int main() {
    int i = 0;

    while (i < 10) {
        i++;
        if (i == 5) {
            continue; // 跳过 i 为 5 时的打印
        }
        printf("i is %d\n", i);
        if (i == 8) {
            break; // 当 i 为 8 时退出循环
        }
    }

    return 0;
}

在这个例子中:

  • i 等于 5 时,使用 continue 语句跳过当前迭代的 printf 语句。
  • i 等于 8 时,使用 break 语句退出循环。

for语句

在 C 语言中,for 语句是一种用于控制循环的结构,它允许你在一个循环中同时初始化循环变量、设定循环条件和更新循环变量。for 语句通常用于当你知道循环的次数或者需要一个计数器来控制循环的场景。

for 语句的基本语法如下:

for (initialization; condition; update) {
    // 循环体代码
}
  • initialization:初始化语句,在循环开始时执行一次。通常用于设置循环变量的初始值。

  • condition:循环条件,是一个布尔表达式。在每次循环迭代前检查。如果条件为真(非零),执行循环体代码;否则,退出循环。

  • update:更新语句,在每次循环迭代后执行。通常用于更新循环变量的值。

    image-20240819205942438
基本示例
#include <stdio.h>

int main() {
    for (int i = 0; i < 5; i++) {
        printf("i is %d\n", i);
    }

    return 0;
}

在这个示例中,for 循环初始化 i0,然后在每次循环中检查 i < 5。如果条件为真,执行 printf 语句,并在每次迭代后增加 i 的值。循环在 i 达到 5 时退出。

多个初始化和更新

for 语句允许多个初始化和更新表达式,使用逗号分隔。

#include <stdio.h>

int main() {
    for (int i = 0, j = 10; i < 5; i++, j -= 2) {
        printf("i is %d, j is %d\n", i, j);
    }

    return 0;
}

在这个例子中,ij 分别被初始化为 010。每次循环中,i 增加 1j 减少 2

for 循环中的空语句

for 循环中的三个部分(初始化、条件和更新)都可以是空的,这样的循环会变成无限循环,直到在循环体内使用 break 语句来终止循环。

#include <stdio.h>

int main() {
    int i = 0;

    for (; i < 5; ) {
        printf("i is %d\n", i);
        i++;
    }

    return 0;
}

在这个例子中,for 语句没有初始化和更新部分,只有条件部分。循环会在 i 小于 5 时执行,并在每次循环体内更新 i 的值。

嵌套 for 循环
#include <stdio.h>

int main() {
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            printf("i: %d, j: %d\n", i, j);
        }
    }

    return 0;
}

在这个例子中,for 循环嵌套在另一个 for 循环内部。外层循环控制 i 的值,内层循环控制 j 的值。每次外层循环迭代时,内层循环会完整地执行一遍。

在 C 语言中的 for 循环结构中,breakcontinue 语句用于控制循环的流。它们分别用于提前退出循环和跳过当前循环的剩余部分。以下是它们的详细介绍:

使用breakcontinue

break 语句用于立即退出包含它的循环或 switch 语句。使用 break 语句可以提前终止循环,不论循环条件是否仍为真。

continue 语句用于跳过当前循环的剩余部分,直接进入下一次循环的迭代。它不会退出循环,只是跳过当前迭代的剩余代码。

#include <stdio.h>

int main() {
    for (int i = 0; i < 10; i++) {
        if (i % 2 == 0) {
            continue; // 跳过偶数
        }
        if (i == 7) {
            break; // 当 i 为 7 时,退出循环
        }
        printf("i is %d\n", i);
    }

    return 0;
}

在这个例子中:

  • continue 语句用于跳过所有偶数(不会打印偶数)。
  • break 语句用于在 i 等于 7 时退出循环。

do while语句

在 C 语言中,do-while 语句是一种循环控制结构,它类似于 while 语句,但有一个关键的不同点:do-while 循环至少会执行一次循环体代码,然后再检查循环条件。这是因为条件检查是在循环体代码执行之后进行的。

do-while 语句的基本语法如下:

do {
    // 循环体代码
} while (condition);
  • 循环体代码:在每次循环中执行的代码块。
  • condition:一个布尔表达式,循环体代码执行后检查。如果 condition 为真(非零),则继续执行循环体;如果为假(零),则退出循环。
  • image-20240727095129349
基本示例
#include <stdio.h>

int main() {
    int count = 0;

    do {
        printf("Count is %d\n", count);
        count++;
    } while (count < 5);

    return 0;
}

在这个示例中,do-while 循环会至少执行一次循环体代码,然后检查 count < 5 条件。如果条件为真,继续执行;如果条件为假,循环结束。结果是 printf 语句会输出 count04 的值。

while 循环的比较

do-while 循环中,无论条件是否为真,循环体都会至少执行一次。这与 while 循环不同,while 循环会在每次循环之前检查条件。

#include <stdio.h>

int main() {
    int count = 5;

    // 使用 while 循环
    while (count < 5) {
        printf("Count is %d\n", count);
        count++;
    }

    count = 5;

    // 使用 do-while 循环
    do {
        printf("Count is %d\n", count);
        count++;
    } while (count < 5);

    return 0;
}

在这个例子中:

  • 使用 while 循环时,条件 count < 5 为假,因此循环体不会执行。
  • 使用 do-while 循环时,循环体会执行一次,即使 count 的初始值不满足条件。

goto语句

在 C 语言中,goto 语句是一种跳转控制结构,用于将程序的控制流转移到同一函数中的指定标签处。goto 语句可以跳过某些代码块,或者在特定条件下跳转到函数中的其他位置。尽管 goto 语句提供了一种直接的控制流方式,但它的使用通常被认为是不推荐的,因为它会使代码难以理解和维护。

goto 语句的基本语法如下:

goto label;
  • label:是一个标识符,后跟冒号(:)。它标识了程序中 goto 语句要跳转到的位置。
基本示例
#include <stdio.h>

int main() {
    int x = 0;

    if (x == 0) {
        goto skip; // 跳转到标签 skip
    }

    printf("This will not be printed.\n");

skip:
    printf("This will be printed.\n");

    return 0;
}

在这个示例中,当 x 等于 0 时,goto skip 语句会使控制流跳转到 skip 标签的位置。printf("This will not be printed."); 语句被跳过。

示例:用于错误处理
#include <stdio.h>

int main() {
    FILE *file;
    file = fopen("example.txt", "r");
    
    if (file == NULL) {
        goto error; // 如果打开文件失败,跳转到错误处理
    }

    // 执行文件操作
    printf("File opened successfully.\n");

    fclose(file); // 关闭文件

    return 0;

error:
    printf("Error opening file.\n");
    return 1;
}

在这个例子中,goto 语句用于在文件打开失败时跳转到错误处理部分。

注意事项
  1. 代码可读性:频繁使用 goto 语句会导致代码逻辑不清晰,使代码难以理解和维护。它打破了程序结构的正常流程,导致“跳跃式”逻辑。

  2. 使用场景goto 语句可以用于错误处理、跳出深层嵌套的循环或 switch 语句等特殊场景,但应谨慎使用,优先考虑其他结构,如 breakcontinue 或函数调用。

  3. 跳转限制goto 语句只能跳转到同一函数内的标签。它不能跳转到函数外部,也不能跨越函数边界。

  4. 标签位置:标签必须在 goto 语句之前定义,标签的位置可以在函数的任何地方,只要在跳转之前可见。

数组

在 C 语言中,数组是一种数据结构,用于存储一组相同类型的元素。每个元素在数组中都有一个索引,通过索引可以访问数组中的具体元素。数组是静态数据结构,大小在编译时确定,并且一旦定义后,其大小无法改变。

数组的基本特性

  1. 相同类型的元素:数组中的所有元素必须是相同的数据类型,如整型、浮点型、字符型等。
  2. 索引访问:数组元素通过索引访问。索引从 0 开始,范围是 0n-1,其中 n 是数组的大小。
  3. 固定大小:数组的大小在声明时确定,并且在程序运行过程中不可改变。
  4. 内存连续:数组元素在内存中是连续存储的,这使得通过索引访问数组元素非常高效。

数组的声明和初始化

声明
type arrayName[size];
  • type:数组元素的数据类型。
  • arrayName:数组的名称。
  • size:数组的大小,表示可以存储的元素数量。
示例:声明一个整数数组
int numbers[5]; // 声明一个可以存储 5 个整数的数组
初始化

可以在声明数组时进行初始化,也可以在之后的代码中初始化。

声明时初始化

int numbers[5] = {1, 2, 3, 4, 5}; // 初始化数组元素

如果初始化列表的元素个数少于数组的大小,其余元素将被自动初始化为 0

int numbers[5] = {1, 2}; // 数组内容为 {1, 2, 0, 0, 0}

部分初始化

如果初始化列表包含的元素个数大于数组的大小,编译器会报错。

int numbers[5] = {1, 2, 3}; // 合法,数组内容为 {1, 2, 3, 0, 0}

如果没有指定大小,数组的大小会根据初始化列表的元素个数自动确定。

int numbers[] = {1, 2, 3, 4, 5}; // 编译器会自动将数组大小设为 5

数组元素访问

数组元素通过索引访问,索引从 0 开始。例如,numbers[0] 访问数组中的第一个元素,numbers[1] 访问第二个元素,以此类推。

示例:访问和修改数组元素
#include <stdio.h>

int main() {
    int numbers[5] = {1, 2, 3, 4, 5};

    // 访问数组元素
    printf("First element: %d\n", numbers[0]); // 输出 1
    printf("Second element: %d\n", numbers[1]); // 输出 2

    // 修改数组元素
    numbers[2] = 10;
    printf("Modified third element: %d\n", numbers[2]); // 输出 10

    return 0;
}

多维数组

C 语言支持多维数组,如二维数组。多维数组是一个数组的数组。

声明二维数组
type arrayName[rows][columns];
  • rows:数组的行数。
  • columns:数组的列数。
示例:声明和初始化二维数组
int matrix[3][3] = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};
访问二维数组元素
#include <stdio.h>

int main() {
    int matrix[3][3] = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };

    // 访问和打印二维数组的元素
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }

    return 0;
}

变长数组

在 C99 标准中引入了变长数组(Variable Length Arrays,VLA),允许在运行时动态确定数组的大小。与静态数组不同,静态数组的大小必须在编译时确定。变长数组的大小可以通过在数组声明中使用变量来指定,而不是常量。

声明变长数组的语法类似于静态数组,但是可以使用运行时确定的变量作为数组的大小。

type arrayName[variableSize];
  • type:数组元素的数据类型。
  • arrayName:数组的名称。
  • variableSize:一个在运行时确定的表达式,用于指定数组的大小。
#include <stdio.h>

void printArray(int size, int array[size]) {
    for (int i = 0; i < size; i++) {
        printf("%d ", array[i]);
    }
    printf("\n");
}

int main() {
    int n = 5;
    int numbers[n]; // 变长数组

    // 动态初始化数组
    for (int i = 0; i < n; i++) {
        numbers[i] = i + 1;
    }

    // 调用函数打印数组
    printf("Array contents: ");
    printArray(n, numbers);

    return 0;
}

在这个示例中,numbers 是一个变长数组,其大小由变量 n 决定。在函数 printArray 中,将 size 参数用作数组的大小。这种用法使得我们可以在运行时根据需要动态地调整数组的大小。

字符和ASCII码

在 C 语言中,字符(char 类型)是一种基本的数据类型,用于存储单个字符。字符在计算机中通常以 ASCII 码(American Standard Code for Information Interchange)形式存储和处理。ASCII 码是一种字符编码标准,用于表示文本中的字符。每个 ASCII 字符对应一个唯一的整数值,从而使得字符可以被计算机以数字的形式处理。

字符

转义字符概述

在 C 语言中,转义字符用于表示一些特殊字符,这些字符在文本中通常难以直接输入或无法直接表示。转义字符通常由一个反斜杠 (\) 后跟一个特定的字符组成。它们在字符串和字符常量中用来插入控制字符、换行符、引号等特殊字符。

常见的转义字符

以下是一些常见的转义字符及其用途:

转义字符描述示例
\n换行符printf("Hello\nWorld"); 输出 HelloWorld 在不同的行
\r回车符printf("Hello\rWorld"); 输出 World 取代 Hello
\t水平制表符(Tab)printf("Hello\tWorld"); 输出 HelloWorld 之间有一个制表符的间隔
\\反斜杠字符printf("Path: C:\\Program Files"); 输出 Path: C:\Program Files
\"双引号字符printf("He said, \"Hello\""); 输出 He said, "Hello"
\'单引号字符printf("It\'s a test"); 输出 It's a test
\a响铃符(警告音)printf("Warning\a"); 可能会发出声音(视系统而定)
\b退格符printf("Hello\bWorld"); 输出 HellWorldo 被退格删除)
\f换页符printf("Page break\fHere"); 输出 Page break 后换页(在文本输出中效果有限)
\v垂直制表符printf("Hello\vWorld"); 输出 HelloWorld 之间有一个垂直制表符的间隔(在文本输出中效果有限)
\xhh十六进制表示的字符printf("\x41"); 输出 A,其中 41 是字符 A 的十六进制 ASCII 码
\ooo八进制表示的字符printf("\101"); 输出 A,其中 101 是字符 A 的八进制 ASCII 码

ASCII码

ASCII 码概述

ASCII 码是一种字符编码标准,用于表示英文字符、数字、标点符号和一些控制字符。ASCII 码使用 7 位二进制数来表示字符,因此可以表示 128 个字符(包括控制字符和可打印字符)。

  • 控制字符(0-31 和 127):这些字符用于控制文本的显示(例如换行符、回车符)。
  • 可打印字符(32-126):包括数字(0-9)、字母(A-Z、a-z)和标点符号。
ASCII 码表
img
ASCII 码与字符的关系
  • 字符到 ASCII 码:可以通过将字符类型转换为 int 来获得其 ASCII 码。
  • ASCII 码到字符:可以通过将整数类型转换为 char 来获得对应的字符。

示例:字符与 ASCII 码的转换

#include <stdio.h>

int main() {
    char ch = 'A'; // 字符常量
    int asciiValue = (int)ch; // 转换为 ASCII 码值

    printf("Character: %c\n", ch);
    printf("ASCII value: %d\n", asciiValue);

    return 0;
}

在这个示例中,字符 'A' 被转换为其对应的 ASCII 码值 65,并输出到屏幕上。

字符串和字符数组

在 C 语言中,字符串和字符数组是紧密相关的概念。理解它们的关系和差异对于有效处理文本数据非常重要。以下是对字符串和字符数组的详细介绍。

字符数组

字符数组是一个包含 char 类型元素的数组。它的每个元素都是一个字符。字符数组的大小在声明时必须指定(或根据初始化列表自动确定)。

#include <stdio.h>

int main() {
    // 声明并初始化字符数组
    char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 显式指定结束符
    char str2[] = "Hello"; // 自动包含结束符 '\0'

    printf("str1: %s\n", str1);
    printf("str2: %s\n", str2);

    return 0;
}

在这个示例中,str1 是一个字符数组,其中的每个字符都是显式指定的,包括字符串结束符 \0str2 使用字符串字面量初始化,编译器会自动添加结束符 \0

字符串

在 C 语言中,字符串是以 \0(空字符)结尾的字符数组。这个特殊字符用于标识字符串的结束。字符串可以通过字符数组来表示,也可以通过字符串字面量(如 "Hello")来初始化字符数组。

字符串的性质

  1. 终结符 \0:C 语言字符串的末尾是一个特殊的字符 \0,它标识字符串的结束。在处理字符串时,标准库函数会依赖这个终结符来判断字符串的结束位置。

  2. 字符数组作为字符串:一个字符数组可以被当作字符串处理,只要它以 \0 结束。例如,char str[6] = "Hello"; 中的 str 被视为一个字符串,其中 \0 是自动添加的。

常用字符串函数

C 语言标准库提供了许多函数来操作字符串,这些函数通常在 <string.h> 头文件中声明。例如:

  • strlen():计算字符串的长度(不包括 \0)。
  • strcmp():比较两个字符串。
  • strcpy():复制字符串。
  • strcat():连接两个字符串。

示例:使用字符串函数

#include <stdio.h>
#include <string.h>

int main() {
    char str1[20] = "Hello";
    char str2[20] = "World";
    char str3[40];

    // 计算长度
    printf("Length of str1: %zu\n", strlen(str1));

    // 连接字符串
    strcat(str1, " ");
    strcat(str1, str2);
    printf("Concatenated str1: %s\n", str1);

    // 复制字符串
    strcpy(str3, str1);
    printf("Copied str3: %s\n", str3);

    // 比较字符串
    int cmp = strcmp(str1, str2);
    if (cmp == 0) {
        printf("str1 is equal to str2\n");
    } else if (cmp < 0) {
        printf("str1 is less than str2\n");
    } else {
        printf("str1 is greater than str2\n");
    }

    return 0;
}

使用gets和puts函数

getsputs 是 C 语言中的两个函数,用于输入和输出字符串。然而,gets 是不推荐使用的函数,因为它存在安全漏洞。相反,fgets 是一个更安全的替代函数。

gets 函数

gets 函数从标准输入读取一行字符,直到遇到换行符或文件结束符,然后将读取的字符存储在字符串中,并在末尾添加一个空字符 \0

gets 示例
#include <stdio.h>

int main() {
    char buffer[100];

    // 使用 gets 读取字符串
    printf("Enter a string: ");
    gets(buffer);  // 不安全,不推荐使用

    printf("You entered: %s\n", buffer);

    return 0;
}

gets 不检查输入的长度,如果输入的字符超过了缓冲区的长度,会导致缓冲区溢出,从而可能引发程序崩溃或安全漏洞。因此,gets 函数在 C11 标准中被废弃。

安全替代函数 fgets

fgets 函数可以指定读取的最大字符数,从而避免缓冲区溢出问题。它从指定的流(通常是标准输入)读取一行,并在读取的字符串末尾添加一个空字符 \0

fgets 示例
#include <stdio.h>

int main() {
    char buffer[100];

    // 使用 fgets 读取字符串
    printf("Enter a string: ");
    fgets(buffer, sizeof(buffer), stdin);

    // 移除换行符(如果有)
    size_t len = strlen(buffer);
    if (len > 0 && buffer[len-1] == '\n') {
        buffer[len-1] = '\0';
    }

    printf("You entered: %s\n", buffer);

    return 0;
}
puts 函数

puts 函数用于将字符串输出到标准输出,并在字符串末尾自动添加一个换行符。

puts 示例
#include <stdio.h>

int main() {
    char str[] = "Hello, world!";

    // 使用 puts 输出字符串
    puts(str);

    return 0;
}

函数

库函数

库函数是编程语言提供的预定义函数,用于执行常见的操作以简化开发。

C语言标准库函数大全(ctype、time 、stdio、stdlib、math、string)_c语言库函数查询手册-CSDN博客

自定义函数

在 C 语言中,自定义函数是指由程序员定义的函数,用于实现特定的功能。自定义函数可以提高代码的可读性、重用性和组织性。以下是关于自定义函数的详细介绍。

自定义函数的基本结构包括函数的声明和定义:

  1. 函数声明(也称为函数原型):在函数调用之前声明函数,以告知编译器函数的名称、返回类型和参数类型。
  2. 函数定义:函数的实际实现,包括函数体内的代码。
#include <stdio.h>

// 函数声明
返回类型 函数名(参数类型 参数名, ...);

int add(int a, int b); // 函数声明

int main() {
    int result;

    // 调用自定义函数
    result = add(5, 3);
    printf("The result is: %d\n", result);

    return 0;
}

// 函数定义
int add(int a, int b) {
    return a + b;
}

函数的形参和实参

在 C 语言中,函数参数可以分为形参(形式参数)和实参(实际参数)。

形参(形式参数)

形参是在函数定义时声明的参数,它们作为占位符,用于接收函数调用时传递的值。形参的作用域仅限于函数内部。

#include <stdio.h>

// 函数定义,带有形参 a 和 b
int add(int a, int b) {
    return a + b;
}

int main() {
    int result = add(3, 4);  // 传递实际参数 3 和 4
    printf("The sum is: %d\n", result);

    return 0;
}

在这个示例中,add 函数的形参是 ab

实参(实际参数)

实参是在函数调用时传递给函数的值。实参可以是常量、变量或表达式。

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main() {
    int x = 3;
    int y = 4;
    int result = add(x, y);  // 传递实际参数 x 和 y
    printf("The sum is: %d\n", result);

    return 0;
}

在这个示例中,add 函数的实参是变量 xy

形参和实参的关系
  • 传值:在 C 语言中,实参的值被传递给形参。形参接收实参的副本,函数内部对形参的修改不会影响实参。这种传递方式称为按值传递(pass by value)。

函数的链式访问

在 C 语言中,链式访问(chaining)通常是指通过连续调用函数来实现某种功能,每个函数调用返回一个值,供下一个函数调用使用。这种方式在处理字符串、数据结构(如链表)或构建复杂对象时尤其有用。

链式访问可以通过以下几种方式实现:

  1. 返回指针或引用:函数返回指向自身结构或对象的指针或引用,从而允许后续的函数调用。
  2. 返回结构体:函数返回一个结构体,后续函数调用可以继续使用这个结构体。
int main() {
    char str[100] = "  Hello, world!  ";

    // 链式调用字符串处理函数
    printf("%s\n", to_upper(replace_spaces(trim(str))));

    return 0;
}

main 函数中,我们定义了一个字符串 str,并通过链式调用对其进行处理。链式调用的顺序如下:

  1. trim(str):移除字符串两端的空格。
  2. replace_spaces(trim(str)):将处理后的字符串中的空格替换为下划线。
  3. to_upper(replace_spaces(trim(str))):将最终处理后的字符串转换为大写。

函数的嵌套调用

函数的嵌套调用是指一个函数在其函数体内调用另一个函数。通过嵌套调用,可以将复杂的任务分解为多个简单的任务,每个任务由一个独立的函数完成。这种方法不仅提高了代码的可读性和可维护性,还使得代码的逻辑更加清晰。

代码示例
#include <stdio.h>

// 定义一个函数,用于计算两个数的和
int add(int a, int b) {
    return a + b;
}

// 定义一个函数,用于计算两个数的乘积
int multiply(int a, int b) {
    return a * b;
}

// 定义一个函数,用于计算两个数的和与另一个数的乘积
int complex_operation(int x, int y, int z) {
    int sum = add(x, y);       // 调用 add 函数
    int result = multiply(sum, z);  // 调用 multiply 函数
    return result;
}

int main() {
    int x = 2, y = 3, z = 4;

    // 调用 complex_operation 函数
    int result = complex_operation(x, y, z);
    printf("The result is: %d\n", result);

    return 0;
}
解释
  1. add 函数:计算两个整数的和。
  2. multiply 函数:计算两个整数的乘积。
  3. complex_operation 函数:计算两个整数的和,然后将和与另一个整数相乘。该函数先调用 add 函数,再调用 multiply 函数。
  4. main 函数:调用 complex_operation 函数,并输出结果。

输出结果为:

The result is: 20

函数中的return语句

return 语句在 C 语言中用于终止函数的执行并返回一个值(如果有指定返回值类型)。它不仅可以在函数的任何地方结束函数,还可以将结果返回给调用者。理解 return 语句对于编写有效的函数至关重要。

return expression;
  • expression 是要返回的值,可以是一个变量、常量、表达式或函数调用。
  • 如果函数的返回类型是 void,则 return 语句可以省略 expression

函数递归

函数递归是指在函数内部调用其自身的一种编程技巧。递归通常用于解决问题的一部分可以用更小规模的同类问题来解决的情况。递归函数必须具备两个关键部分:基准情形(base case)和递归情形(recursive case)。

  1. 基准情形:递归的终止条件,当满足该条件时,递归不再进行。
  2. 递归情形:函数调用自身的部分,在每次调用时问题的规模逐渐减小。
递归函数的示例
  1. 计算阶乘

阶乘是一个经典的递归问题,n 的阶乘(n!)等于 n 乘以 (n-1) 的阶乘,

递归公式为: n ! = n × ( n − 1 ) ! n! = n \times (n-1)! n!=n×(n1)!
基准情形为: 0 ! = 1 0! = 1 0!=1

#include <stdio.h>

int factorial(int n) {
    if (n == 0) {
        return 1;  // 基准情形
    } else {
        return n * factorial(n - 1);  // 递归情形
    }
}

int main() {
    int number = 5;
    printf("Factorial of %d is %d\n", number, factorial(number));
    return 0;
}
  1. 斐波那契数列

斐波那契数列也是一个典型的递归问题,第 n 个斐波那契数等于前两个斐波那契数之和:
递归公式为: F ( n ) = F ( n − 1 ) + F ( n − 2 ) F(n) = F(n-1) + F(n-2) F(n)=F(n1)+F(n2)
基准情形为: F ( 0 ) = 0 F(0) = 0 F(0)=0 F ( 1 ) = 1 F(1) = 1 F(1)=1

#include <stdio.h>

int fibonacci(int n) {
    if (n == 0) {
        return 0;  // 基准情形
    } else if (n == 1) {
        return 1;  // 基准情形
    } else {
        return fibonacci(n - 1) + fibonacci(n - 2);  // 递归情形
    }
}

int main() {
    int number = 10;
    printf("Fibonacci of %d is %d\n", number, fibonacci(number));
    return 0;
}
递归与迭代

递归和迭代是解决问题的两种不同方法。递归通过函数调用自身来实现循环,而迭代通过循环结构(如 forwhile)来重复执行代码。对于一些问题,递归实现更直观,但迭代实现通常更高效。

  1. 迭代实现阶乘
#include <stdio.h>

int factorial(int n) {
    int result = 1;
    for (int i = 1; i <= n; ++i) {
        result *= i;
    }
    return result;
}

int main() {
    int number = 5;
    printf("Factorial of %d is %d\n", number, factorial(number));
    return 0;
}
  1. 迭代实现斐波那契数列
#include <stdio.h>

int fibonacci(int n) {
    if (n == 0) return 0;
    if (n == 1) return 1;
    int a = 0, b = 1, temp;
    for (int i = 2; i <= n; ++i) {
        temp = a + b;
        a = b;
        b = temp;
    }
    return b;
}

int main() {
    int number = 10;
    printf("Fibonacci of %d is %d\n", number, fibonacci(number));
    return 0;
}

传值调用和传址调用

在C语言中,函数参数的传递方式主要有两种:传值调用(Call by Value)和传址调用(Call by Reference)。这两种方式在参数传递和函数内部对参数的操作上有很大的不同。

传值调用 (Call by Value)

在传值调用中,函数接收的是实际参数的副本。对参数的任何修改都不会影响原始数据。传值调用是C语言中函数参数的默认传递方式。

  • 函数接收参数的副本。
  • 对参数的修改不会影响实际参数的值。

示例代码:

#include <stdio.h>

void modifyValue(int x) {
    x = 10; // 修改的是副本,不会影响实际参数
    printf("Inside modifyValue: x = %d\n", x);
}

int main() {
    int a = 5;
    printf("Before modifyValue: a = %d\n", a);
    modifyValue(a);
    printf("After modifyValue: a = %d\n", a); // a 的值未变
    return 0;
}

输出:

Before modifyValue: a = 5
Inside modifyValue: x = 10
After modifyValue: a = 5
传址调用 (Call by Reference)

在传址调用中,函数接收的是实际参数的地址。通过地址,函数可以直接修改实际参数的值。C语言通过指针来实现传址调用。

  • 函数接收参数的地址(指针)。
  • 对参数的修改会直接影响实际参数的值。

示例代码:

#include <stdio.h>

void modifyValue(int *x) {
    *x = 10; // 修改的是实际参数的值
    printf("Inside modifyValue: *x = %d\n", *x);
}

int main() {
    int a = 5;
    printf("Before modifyValue: a = %d\n", a);
    modifyValue(&a); // 传递 a 的地址
    printf("After modifyValue: a = %d\n", a); // a 的值被修改
    return 0;
}

输出:

Before modifyValue: a = 5
Inside modifyValue: *x = 10
After modifyValue: a = 10
比较
特性传值调用 (Call by Value)传址调用 (Call by Reference)
传递内容参数的副本参数的地址(指针)
影响实际参数否,对参数的修改不会影响实际参数是,对参数的修改会直接影响实际参数
实现方式默认方式通过指针实现
适用场景适用于不需要修改实际参数的情况适用于需要修改实际参数的情况
性能副本可能消耗更多内存(尤其是大数据结构)通常更高效,尤其是对于大数据结构,但可能增加指针操作的复杂性
  • 传值调用:函数接收参数的副本,对参数的修改不会影响实际参数。适用于不需要修改实际参数的情况。
  • 传址调用:函数接收参数的地址(指针),对参数的修改会直接影响实际参数。适用于需要修改实际参数的情况,通过指针实现。

指针

指针变量是C语言中的一种特殊变量,它存储的是另一个变量的内存地址。指针在C语言中具有强大的功能,可以用于动态内存分配、数组和字符串操作、函数参数传递等。

定义指针变量的语法如下:

type *pointer_name;
  • type 是指针指向的变量的数据类型。
  • * 表示这是一个指针变量。
  • pointer_name 是指针变量的名称。

初始化指针

指针变量通常通过取地址运算符 & 初始化,示例如下:

int a = 10;
int *p = &a; // p 是指向整数变量 a 的指针

指针的操作

取地址运算符 &

取地址运算符 & 用于获取变量的内存地址

int a = 10;
int *p = &a; // p 存储 a 的地址
解引用运算符 *

解引用运算符 * 用于访问指针指向的变量的值

int a = 10;
int *p = &a;
int b = *p; // b 的值为 10,等同于 a 的值
修改指针指向的值

通过指针可以修改其指向的变量的值

int a = 10;
int *p = &a;
*p = 20; // 现在 a 的值变为 20
指针和数组

指针和数组在许多情况下是可以互换使用的。

int arr[3] = {1, 2, 3};
int *p = arr; // p 指向数组的第一个元素

您可以使用指针遍历数组:

for(int i = 0; i < 3; i++) {
    printf("%d ", *(p + i)); // 输出数组元素
}
指针和字符串

在 C 语言中,字符串通常表示为字符数组或字符指针。

char str[] = "Hello";
char *p = str; // p 指向字符串的第一个字符
指针和动态内存分配

指针常用于动态内存分配。例如,使用 malloc 分配内存:

int *p = (int *)malloc(5 * sizeof(int)); // 分配存储 5 个 int 类型变量的内存
if (p == NULL) {
    // 内存分配失败处理
}
指针作为函数参数

指针可以作为函数参数,允许函数修改传入的变量

void increment(int *p) {
    (*p)++;
}

int main() {
    int a = 10;
    increment(&a); // a 的值变为 11
    return 0;
}

指针的类型和指针运算

指针的类型决定了指针解引用时读取的内存块大小。例如,int * 类型的指针解引用时读取 4 字节(在大多数系统上),而 char * 类型的指针解引用时读取 1 字节。

指针运算包括加减整数,这会根据指针类型移动指针。

int arr[3] = {1, 2, 3};
int *p = arr;
p++; // p 现在指向 arr[1]

void* 指针

void* 指针是C语言中一种通用指针类型,它可以指向任何类型的数据。由于 void* 指针没有具体的类型信息,所以它无法直接解引用,也无法进行指针运算void* 指针的主要用途是实现通用数据类型操作,例如动态内存分配、数据传递和函数参数。

const修饰指针

在C语言中,const 关键字用于定义常量,这意味着被修饰的变量的值在其生命周期内不能被修改。const 关键字的使用可以增强代码的可读性和安全性,防止意外修改变量的值。以下是 const 关键字的详细介绍及其各种用法。

const 关键字可以用在指针类型中,以不同的方式修饰指针和指针指向的内容。

const 关键字在指针中的用法
  1. 指向常量的指针

指针本身可以改变,但不能通过指针修改指向的内容:

int x = 10;
const int *p = &x;
*p = 20; // 错误,不能通过 p 修改 x 的值
p = &y; // 正确,可以改变 p 的指向
  1. 常量指针

指针本身是常量,但可以通过指针修改指向的内容:

int x = 10;
int *const p = &x;
*p = 20; // 正确,可以通过 p 修改 x 的值
p = &y; // 错误,不能改变 p 的指向
  1. 指向常量的常量指针

指针本身和指针指向的内容都不能修改:

int x = 10;
const int *const p = &x;
*p = 20; // 错误,不能通过 p 修改 x 的值
p = &y; // 错误,不能改变 p 的指向
const 关键字在函数中的用法
  1. 修饰函数参数

避免函数修改传递给它的参数值:

void foo(const int x) {
    x = 20; // 错误,不能修改 const 参数
}
  1. 修饰指针参数

防止函数修改指针指向的内容:

void foo(const int *p) {
    *p = 20; // 错误,不能通过 p 修改指向内容
}

允许函数修改指针本身的值:

void foo(const int *p) {
    p = &y; // 正确,可以改变 p 的指向
}
  1. 修饰返回值

函数返回一个指向常量的指针,防止调用者修改返回的值:

const char* getString() {
    return "Hello";
}

const char *str = getString();
str[0] = 'h'; // 错误,不能修改返回的常量字符串
const 在数组中的使用

定义一个不可修改的数组:

const int arr[3] = {1, 2, 3};
arr[0] = 10; // 错误,不能修改数组元素
const 在结构体中的使用

定义结构体中的不可修改成员:

struct Point {
    const int x;
    int y;
};

struct Point p = {10, 20};
p.x = 30; // 错误,不能修改 const 成员
p.y = 40; // 正确,可以修改非 const 成员
示例代码

以下是一个完整的示例,展示了 const 关键字的各种用法:

#include <stdio.h>

void printArray(const int *arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

const char* getString() {
    return "Hello, World!";
}

int main() {
    // 常量变量
    const int a = 10;
    // a = 20; // 错误

    // 指向常量的指针
    int x = 10;
    const int *p1 = &x;
    // *p1 = 20; // 错误
    p1 = &a; // 正确

    // 常量指针
    int y = 20;
    int *const p2 = &y;
    *p2 = 30; // 正确
    // p2 = &x; // 错误

    // 指向常量的常量指针
    const int *const p3 = &x;
    // *p3 = 40; // 错误
    // p3 = &y; // 错误

    // 常量数组
    const int arr[] = {1, 2, 3};
    // arr[0] = 10; // 错误
    printArray(arr, 3);

    // 常量字符串
    const char *str = getString();
    // str[0] = 'h'; // 错误
    printf("%s\n", str);

    return 0;
}

指针运算

在C语言中,指针运算是指针操作的重要部分,涉及指针的加减法、指针的比较和指针的差值计算。这些运算在处理数组、动态内存和复杂数据结构时非常有用。以下是指针运算的详细介绍。

指针的加减运算
  1. 指针加法

指针加法是将指针向前移动若干个元素的位置。例如,对于 int 类型的指针,每次加1都会移动指针到下一个整型元素的位置。

int arr[] = {10, 20, 30, 40, 50};
int *ptr = arr;
ptr = ptr + 1; // 或者 ptr++
printf("%d\n", *ptr); // 输出 20

指针的加法会根据指针的类型自动调整步长。例如,int 指针每次加1,移动4个字节(假设 int 占4个字节)。

  1. 指针减法

指针减法是将指针向后移动若干个元素的位置。类似于指针加法,减法也会根据指针的类型自动调整步长。

int arr[] = {10, 20, 30, 40, 50};
int *ptr = arr + 4; // 指向最后一个元素
ptr = ptr - 1; // 或者 ptr--
printf("%d\n", *ptr); // 输出 40
指针的差值运算

指针的差值运算用于计算两个指针之间的元素个数。这在处理数组时非常有用。

int arr[] = {10, 20, 30, 40, 50};
int *ptr1 = arr;
int *ptr2 = arr + 4;
ptrdiff_t diff = ptr2 - ptr1;
printf("%td\n", diff); // 输出 4
指针的比较运算

指针可以使用关系运算符进行比较,例如 ==, !=, <, >, <=, >=。指针比较通常用于遍历数组或检查是否到达数组的末尾。

int arr[] = {10, 20, 30, 40, 50};
int *ptr1 = arr;
int *ptr2 = arr + 4;

if (ptr1 < ptr2) {
    printf("ptr1 指向的地址小于 ptr2\n");
}

野指针

野指针(Dangling Pointer)是指向已释放或未分配内存的指针。使用野指针会导致不可预测的行为和严重的错误,如程序崩溃、数据损坏或安全漏洞。野指针通常由以下几种情况引起:

  1. 使用未初始化的指针

    int *ptr; // 未初始化的指针
    *ptr = 10; // 未定义行为
    
  2. 使用已释放的指针

    int *ptr = (int *)malloc(sizeof(int));
    *ptr = 10;
    free(ptr); // 释放内存
    *ptr = 20; // 未定义行为
    
  3. 超出变量作用域

    int* func() {
        int a = 10;
        return &a; // 返回局部变量的地址
    } 
    //局部变量指针会被销毁
    int *ptr = func(); // ptr 成为野指针
    
  4. 指针越界:

    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr + 5; // 错误,指向数组范围之外
    int value = *ptr; // 未定义行为
    
如何避免野指针
  1. 初始化指针

    int *ptr = NULL; // 将指针初始化为 NULL
    
  2. 释放后将指针置为空

    int *ptr = (int *)malloc(sizeof(int));
    *ptr = 10;
    free(ptr);
    ptr = NULL; // 将指针置为空
    
  3. 谨慎使用局部变量的地址

    • 避免返回局部变量的地址,考虑使用动态分配或传递变量的副本。

数组名

在C语言中,数组名是一个特殊的指针常量,它指向数组的第一个元素。

数组名的特点
  1. 地址常量

    • 数组名表示数组的首地址,即第一个元素的地址。
    • 数组名的值是常量,不能修改。
  2. 隐式转换

    • 在表达式中,数组名会被隐式地转换为指向其第一个元素的指针。
  3. 大小计算

    • sizeof(array) 返回整个数组的字节大小。
    • sizeof(array[0]) 返回数组第一个元素的字节大小。
数组名和数组地址的区别

&arr指的是数组的地址

arr是指向数组首元素的常量指针,表示数组首元素的地址。

&arr+1时跳过整个数组,指向数组之后的地址。

arr+1指向下一个元素的地址。

一维数组传参

在C语言中,一维数组的传参主要是通过指针来实现的。当一个数组作为函数参数传递时,实际上传递的是数组首元素的地址,即指向数组的指针。这种传递方式也被称为传址调用。

  1. 传递方式

    • 当将一维数组作为函数参数传递时,函数接收到的是一个指向数组首元素的指针
    • 这意味着函数可以通过指针访问和修改数组的元素。
  2. 数组与指针的关系

    • 数组名在函数调用时被隐式地转换为指向数组首元素的指针。
    • 函数内部可以通过这个指针访问数组的所有元素。

以下是一个示例,展示了如何将一维数组作为参数传递给函数,并在函数内访问和修改数组的内容:

#include <stdio.h>

// 函数声明:传递一维数组的指针和数组的大小
void printArray(int arr[], int size);
void modifyArray(int arr[], int size);

int main() {
    int myArray[5] = {1, 2, 3, 4, 5};
    
    // 打印数组内容
    printArray(myArray, 5);

    // 修改数组内容
    modifyArray(myArray, 5);

    // 打印修改后的数组内容
    printArray(myArray, 5);

    return 0;
}

// 打印数组的函数
void printArray(int arr[], int size) {
    printf("Array elements: ");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

// 修改数组的函数
void modifyArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        arr[i] *= 2; // 将每个元素乘以2
    }
}
  1. 函数参数
    • printArraymodifyArray 函数的参数是 int arr[],这是一个指向整型数组首元素的指针
    • size 参数表示数组的大小,以便函数能够知道处理多少元素。
  2. 访问数组元素
    • printArraymodifyArray 函数中,通过 arr[i] 可以访问数组的每个元素。arr[i] 实际上是 *(arr + i) 的简写形式,即指针操作。
  3. 数组修改
    • modifyArray 函数中,修改了数组的内容,所有元素都被乘以2。由于传递的是指针,原始数组的内容在 main 函数中也被改变了。

指针数组与数组指针

在C语言中,指针数组(Array of Pointers)和数组指针(Pointer to an Array)是两种不同的概念。理解它们的区别对于有效地使用指针和数组非常重要。

指针数组(Array of Pointers)

定义

  • 指针数组是一个数组,其中的每个元素都是一个指针。

声明

  • 例如,int *arr[5]; 表示定义了一个包含5个 int * 类型元素的数组。

初始化

  • 通常使用动态内存分配或将每个指针指向特定数据来初始化指针数组。

应用场景

  • 用于存储多个指针,例如,处理字符串数组(每个字符串是一个字符指针)。
  • 用于实现动态数据结构,如链表。

示例代码

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptrArray[3];

    for (int i = 0; i < 3; i++) {
        ptrArray[i] = (int *)malloc(sizeof(int));
        if (ptrArray[i] == NULL) {
            printf("Memory allocation failed\n");
            return 1;
        }
        *ptrArray[i] = i * 10;
    }

    printf("Pointer array values:\n");
    for (int i = 0; i < 3; i++) {
        printf("ptrArray[%d] points to value: %d\n", i, *ptrArray[i]);
    }

    for (int i = 0; i < 3; i++) {
        free(ptrArray[i]);
    }

    return 0;
}
数组指针(Pointer to an Array)

定义

  • 数组指针是指向整个数组的指针。它可以用于处理多维数组或传递数组给函数时,数组的指针。

声明

  • 例如,int (*arr)[5]; 表示定义了一个指向包含5个 int 元素的数组的指针。

初始化

  • 通常在声明时将其指向一个实际的数组。

应用场景

  • 用于处理多维数组。
  • 传递数组给函数时,通常会使用数组指针,以便函数能够访问整个数组。

示例代码

#include <stdio.h>

void printArray(int (*arr)[5], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", (*arr)[i]);
    }
    printf("\n");
}

int main() {
    int myArray[5] = {1, 2, 3, 4, 5};
    int (*arrayPointer)[5] = &myArray;

    printf("Array values:\n");
    printArray(arrayPointer, 5);

    return 0;
}
区别总结
特性指针数组(Array of Pointers)数组指针(Pointer to an Array)
定义数组中的每个元素都是一个指针指向整个数组的指针
声明int *arr[5]; // 包含5个 int * 指针元素int (*arr)[5]; // 指向包含5个 int 元素的数组
使用场景存储多个指针,例如字符串数组或动态数据结构处理多维数组或传递整个数组给函数
初始化使用动态内存分配或将指针指向特定数据将指针指向实际数组
示例代码int *ptrArray[3]; // 指针数组的例子int (*arrayPointer)[5] = &myArray; // 数组指针的例子

函数指针

函数指针变量是指向函数的指针,它允许你通过指针调用函数。这种机制在C语言中非常有用,特别是在需要动态决定函数调用或实现回调功能时。

函数指针的定义和使用
  1. 定义函数指针

    • 函数指针的定义格式是 返回类型 (*指针变量名)(参数类型1, 参数类型2, ...)。例如,int (*funcPtr)(int, int); 定义了一个指向接受两个 int 参数并返回 int 的函数的指针。
  2. 初始化函数指针

    • 函数指针可以被初始化为指向具体的函数。初始化时,函数名(没有括号)作为函数指针的值。
  3. 调用函数

    • 通过函数指针调用函数时,使用 (*指针变量名)(参数) 语法,或者直接使用 指针变量名(参数) 语法。

以下是一个简单的示例,展示了如何定义、初始化和使用函数指针:

#include <stdio.h>

// 函数声明
int add(int a, int b);
int multiply(int a, int b);

int main() {
    // 定义函数指针
    int (*operation)(int, int);

    // 初始化函数指针为 add 函数
    operation = add;
    printf("Addition: %d\n", operation(5, 3)); // 使用函数指针调用 add 函数

    // 重新初始化函数指针为 multiply 函数
    operation = multiply;
    printf("Multiplication: %d\n", operation(5, 3)); // 使用函数指针调用 multiply 函数

    return 0;
}

// 函数定义
int add(int a, int b) {
    return a + b;
}

int multiply(int a, int b) {
    return a * b;
}
  1. 函数指针定义

    • int (*operation)(int, int); 定义了一个函数指针 operation,它指向接受两个 int 参数并返回 int 的函数。
  2. 初始化函数指针

    • operation = add;operation 指向 add 函数。
    • operation = multiply;operation 重新指向 multiply 函数。
  3. 调用函数

    • operation(5, 3); 使用函数指针调用函数,结果取决于指针当前指向的函数。
函数指针的应用
  1. 回调函数

    • 可以将函数指针作为参数传递给其他函数,实现回调功能。例如,排序函数可以接收一个比较函数的指针。
  2. 动态函数调用

    • 允许在运行时决定调用哪个函数,增加程序的灵活性。
  3. 表驱动法

    • 通过函数指针数组实现表驱动的编程模式,处理不同操作的情况。
函数指针数组

一个函数指针数组是一个数组,每个元素都是一个函数指针。它可以用于实现多种操作的选择。例如:

#include <stdio.h>

// 函数声明
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);

// 函数指针数组
int (*operations[])(int, int) = {add, subtract, multiply};

int main() {
    int a = 10, b = 5;
    
    printf("Addition: %d\n", operations[0](a, b));   // 调用 add
    printf("Subtraction: %d\n", operations[1](a, b)); // 调用 subtract
    printf("Multiplication: %d\n", operations[2](a, b)); // 调用 multiply

    return 0;
}

// 函数定义
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }

回调函数

回调函数是一种编程模式,其中一个函数(称为回调函数)作为参数传递给另一个函数,后者在适当的时候调用这个回调函数。这种机制允许你动态地指定要执行的操作,使得程序更加灵活和可扩展。

回调函数的工作原理

  1. 定义回调函数

    • 回调函数是一个符合特定函数签名的函数,可以被作为参数传递给另一个函数。
  2. 定义接收回调函数的函数

    • 这个函数接受一个函数指针作为参数,并在适当的时候调用这个函数指针所指向的回调函数。
  3. 调用回调函数

    • 在接收回调函数的函数内部,使用函数指针调用回调函数。

以下是回调函数的一个简单示例,展示了如何定义和使用回调函数:

#include <stdio.h>

// 回调函数类型定义
typedef void (*Callback)(int);

// 接受回调函数的函数
void process(int x, Callback cb) {
    printf("Processing value: %d\n", x);
    // 调用回调函数
    cb(x);
}

// 实际的回调函数
void myCallback(int value) {
    printf("Callback function called with value: %d\n", value);
}

int main() {
    // 调用 process 函数,并传递 myCallback 作为回调函数
    process(42, myCallback);
    return 0;
}

代码解释:

  1. 定义回调函数类型
    • typedef void (*Callback)(int); 定义了一个名为 Callback 的函数指针类型,表示指向一个接受 int 参数并返回 void 的函数。
  2. 定义接受回调函数的函数
    • void process(int x, Callback cb) 是一个接受一个 int 和一个回调函数指针的函数。在函数内部,cb(x); 被用来调用回调函数。
  3. 定义回调函数
    • void myCallback(int value) 是一个实际的回调函数,它符合 Callback 类型的函数签名。
  4. 使用回调函数
    • process(42, myCallback); 调用 process 函数,并将 myCallback 作为回调函数传递给它。

结构体

结构体的声明和初始化

结构体的声明

结构体的声明定义了结构体的类型和它包含的成员。使用 struct 关键字来声明一个结构体。

struct Tag {
    type member1;
    type member2;
    ...
    type memberN;
};

示例

// 声明一个结构体类型 Student
struct Student {
    int id;         // 学号
    char name[50];  // 姓名
    float grade;    // 成绩
};

在上述示例中,Student 是结构体的标签,表示一种新类型。这种新类型包含了三个成员:idnamegrade

结构体的初始化

结构体的初始化是在声明结构体变量时给它的成员赋初值。

初始化语法

struct Tag variableName = {value1, value2, ..., valueN};

示例

#include <stdio.h>
#include <string.h>

// 声明结构体类型 Student
struct Student {
    int id;         // 学号
    char name[50];  // 姓名
    float grade;    // 成绩
};

int main() {
    // 初始化结构体变量
    struct Student student1 = {1, "John Doe", 88.5};

    // 使用点运算符访问并打印结构体成员
    printf("ID: %d\n", student1.id);
    printf("Name: %s\n", student1.name);
    printf("Grade: %.2f\n", student1.grade);

    return 0;
}
结构体的其他初始化方式
  1. 逐个成员初始化

    在声明结构体变量后,可以逐个成员进行初始化。

    struct Student student2;
    student2.id = 2;
    strcpy(student2.name, "Jane Smith");
    student2.grade = 92.0;
    
  2. 在定义结构体时声明和初始化变量

    struct Student {
        int id;
        char name[50];
        float grade;
    } student3 = {3, "Alice Johnson", 85.0};
    
  3. 使用指针初始化结构体成员

    struct Student student4;
    struct Student *ptr = &student4;
    ptr->id = 4;
    strcpy(ptr->name, "Bob Brown");
    ptr->grade = 78.5;
    

结构体访问操作符

在 C 语言中,结构体访问操作符用于访问结构体中的成员变量。主要有两种操作符:

  1. 点运算符 (.)
  2. 箭头运算符 (->)
点运算符 (.)

点运算符用于访问结构体变量的成员。它需要一个结构体变量,并通过该变量访问其中的成员。

structVariable.memberName;

示例

#include <stdio.h>

struct Student {
    int id;
    char name[50];
    float grade;
};

int main() {
    struct Student student1 = {1, "John Doe", 88.5};

    // 使用点运算符访问结构体成员
    printf("ID: %d\n", student1.id);
    printf("Name: %s\n", student1.name);
    printf("Grade: %.2f\n", student1.grade);

    return 0;
}

在上述示例中,student1 是一个 Student 结构体变量,点运算符用于访问其成员 idnamegrade

箭头运算符 (->)

箭头运算符用于通过结构体指针访问结构体的成员。它需要一个指向结构体的指针,并通过该指针访问结构体成员。

structPointer->memberName;

示例

#include <stdio.h>

struct Student {
    int id;
    char name[50];
    float grade;
};

int main() {
    struct Student student2 = {2, "Jane Smith", 92.0};
    struct Student *ptr = &student2;

    // 使用箭头运算符访问结构体成员
    printf("ID: %d\n", ptr->id);
    printf("Name: %s\n", ptr->name);
    printf("Grade: %.2f\n", ptr->grade);

    return 0;
}

在上述示例中,ptr 是指向 Student 结构体的指针,箭头运算符用于访问 student2 的成员 idnamegrade

结构体内存对齐

结构体内存对齐是指在内存中存储结构体成员时,为了提高数据访问效率,编译器可能会在成员之间添加填充字节(padding),使得结构体的成员按一定的规则对齐到特定的内存边界。

内存对齐主要是为了提高数据访问的效率。大多数处理器在内存访问时,要求数据位于特定的地址边界上(例如 2 字节、4 字节或 8 字节边界)。未对齐的数据访问会导致处理器需要执行额外的操作,如分两次读取数据,从而降低性能。因此,编译器会自动对结构体进行内存对齐,以满足处理器的要求。

内存对齐规则

  1. 每个成员按照自己的类型大小对齐

    • 对于基本类型的成员,编译器会按照该类型的大小(例如,int 类型通常对齐到 4 字节边界)来对齐。
    • 编译器在需要时会插入填充字节,使得下一个成员按其对齐要求开始。
  2. 结构体的总大小为最宽成员的大小的倍数

    • 结构体的总大小也可能会被填充,使其大小是结构体内最大对齐成员的大小的倍数

示例

#include <stdio.h>

struct Example {
    char a;    // 1 字节
    int b;     // 4 字节
    short c;   // 2 字节
};

int main() {
    struct Example ex;

    printf("Size of struct Example: %lu\n", sizeof(ex));
    printf("Offset of a: %lu\n", offsetof(struct Example, a));
    printf("Offset of b: %lu\n", offsetof(struct Example, b));
    printf("Offset of c: %lu\n", offsetof(struct Example, c));

    return 0;
}

假设 int 对齐到 4 字节边界,short 对齐到 2 字节边界,char 对齐到 1 字节边界。

  • a:占用 1 字节,位于偏移量 0。
  • b:需要对齐到 4 字节边界,因此它在内存中的偏移量为 4,前面插入了 3 个填充字节。
  • c:需要对齐到 2 字节边界,位于偏移量 8,因此 b 之后没有额外的填充字节。

因此,struct Example 的大小将是 12 字节,其中 3 字节是填充字节。

typedef对结构体重命名

在 C 语言中,typedef 可以用来为数据类型定义一个新的名字,这包括基本类型、指针类型以及用户自定义的类型,如结构体。通过 typedef,你可以给结构体类型一个简短而清晰的别名,从而简化代码的书写和提高可读性。

语法
typedef struct {
    type member1;
    type member2;
    ...
} AliasName;

或者

typedef struct Tag {
    type member1;
    type member2;
    ...
} AliasName;
示例 1:匿名结构体
#include <stdio.h>

typedef struct {
    int id;
    char name[50];
    float grade;
} Student;

int main() {
    Student s1 = {1, "John Doe", 88.5};

    printf("ID: %d\n", s1.id);
    printf("Name: %s\n", s1.name);
    printf("Grade: %.2f\n", s1.grade);

    return 0;
}

在这个例子中,typedef 创建了一个名为 Student 的新类型,该类型表示一个结构体。这样,在声明结构体变量时,无需再使用 struct 关键字,直接使用 Student 即可。

示例 2:带标签的结构体
#include <stdio.h>

typedef struct Student {
    int id;
    char name[50];
    float grade;
} Student;

int main() {
    Student s2 = {2, "Jane Smith", 92.0};

    printf("ID: %d\n", s2.id);
    printf("Name: %s\n", s2.name);
    printf("Grade: %.2f\n", s2.grade);

    return 0;
}

在这个例子中,typedef 依然给结构体定义了一个新名字 Student,但结构体本身还有一个标签 Student。在这种情况下,你可以用 Student 代替 struct Student 来声明结构体变量。

位段

位段(Bit Field)是 C 语言中的一种特殊结构体成员,它允许程序员在结构体中定义以位为单位的字段。这些字段可以用来存储小范围的整数值,或者用来表示一组紧密关联的布尔标志。位段的使用有助于节省内存空间,特别是在需要存储多个布尔值或小范围整数的情况下。

位段的定义

位段是定义在结构体中的,使用格式如下:

struct {
    type memberName : numberOfBits;
    ...
};
  • type:通常是 intunsigned int 或其他整数类型,表示位段的底层存储类型。
  • memberName:位段的名字。
  • numberOfBits该成员占用的位数(bit 数)。
示例
#include <stdio.h>

struct Status {
    unsigned int is_on : 1;    // 1 位,表示布尔值
    unsigned int mode : 2;     // 2 位,表示模式(最多可存储 0-3 的值)
    unsigned int value : 5;    // 5 位,表示值(最多可存储 0-31 的值)
};

int main() {
    struct Status s;

    s.is_on = 1;   // 设置 is_on 为 1
    s.mode = 2;    // 设置 mode 为 2
    s.value = 25;  // 设置 value 为 25

    printf("Status:\n");
    printf("is_on: %u\n", s.is_on);
    printf("mode: %u\n", s.mode);
    printf("value: %u\n", s.value);

    return 0;
}

运行结果

Status:
is_on: 1
mode: 2
value: 25

在这个例子中,Status 结构体包含三个位段成员:

  • is_on 占用 1 位,用来存储布尔值(0 或 1)。
  • mode 占用 2 位,可以存储 0 到 3 之间的整数值。
  • value 占用 5 位,可以存储 0 到 31 之间的整数值。

位段的使用注意事项

  1. 对齐和大小:位段的分配通常受到底层存储类型(如 int)对齐要求的影响。不同编译器可能会在位段之间插入填充位以保证对齐。

  2. 访问效率:虽然位段可以节省内存空间,但它们的访问效率可能不如普通结构体成员,因为处理器需要执行额外的操作来提取位段中的位。

  3. 可移植性:不同编译器处理位段的方式可能不同,特别是关于对齐和字节顺序,因此位段在跨平台时可能会存在可移植性问题。

  4. 类型大小:虽然位段的定义允许使用 charint 等类型,但编译器通常将位段成员存储在机器字大小的存储单元中(例如 32 位或 64 位)。

联合体

联合体(Union)是C语言中的一种数据结构,它允许多个不同类型的变量共享同一块内存空间。这意味着在一个联合体中,所有成员变量共用相同的存储空间,因此在任一时刻,联合体中只能有一个成员有效存储值。

联合体的定义

联合体的定义方式与结构体类似,但使用 union 关键字:

union UnionName {
    type1 member1;
    type2 member2;
    ...
};
  • UnionName 是联合体的名字。
  • member1member2 等是联合体的成员,它们可以是不同的类型。

示例

#include <stdio.h>

union Data {
    int i;
    float f;
    char str[20];
};

int main() {
    union Data data;

    data.i = 10;
    printf("data.i : %d\n", data.i);

    data.f = 220.5;
    printf("data.f : %f\n", data.f);

    // 尝试输出 data.i,这时它已经被 data.f 覆盖了
    printf("data.i after assigning float: %d\n", data.i);

    // 现在为字符串赋值
    snprintf(data.str, sizeof(data.str), "Hello");
    printf("data.str : %s\n", data.str);

    // 再次尝试输出之前的整数和浮点数
    printf("data.i after assigning string: %d\n", data.i);
    printf("data.f after assigning string: %f\n", data.f);

    return 0;
}

运行结果

data.i : 10
data.f : 220.500000
data.i after assigning float: 1132396544
data.str : Hello
data.i after assigning string: 1819043144
data.f after assigning string: 0.000000

在这个例子中,union Data 包含一个 int、一个 float 和一个字符数组 str。由于联合体的所有成员共享相同的内存区域,给联合体的某个成员赋值会覆盖其他成员的值。

联合体的特性

  1. 共享内存:联合体的所有成员共用相同的内存区域。联合体的大小等于它最大成员的大小,而不是所有成员大小之和。

  2. 只能存储一个有效值:在任一时刻,联合体中只能有一个成员存储有效的值。如果给联合体的某个成员赋值,其他成员的值将被覆盖。

  3. 用途:联合体常用于需要在多个数据类型之间共享内存的情况下。例如,一个变量可能需要在不同的情况下表示不同类型的数据。

联合体的内存分布

假设一个联合体定义如下:

union Example {
    char c;
    int i;
    double d;
};

在这个联合体中,char c 占用 1 字节,int i 通常占用 4 字节,double d 通常占用 8 字节。那么这个联合体的大小为 8 字节,因为 double 是其中最大的数据类型。

枚举

枚举(Enumeration)是C语言中的一种用户自定义数据类型,用于定义一组具名的整数常量。通过枚举,可以为一组相关的常量赋予更具描述性的名字,从而提高代码的可读性和可维护性。

枚举的定义

使用 enum 关键字来定义一个枚举类型,格式如下:

enum EnumName {
    constant1,
    constant2,
    ...
};
  • EnumName 是枚举类型的名字。
  • constant1constant2 等是枚举常量,它们默认从 0 开始递增。

示例

#include <stdio.h>

enum Weekday {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
};

int main() {
    enum Weekday today;

    today = WEDNESDAY;

    if (today == WEDNESDAY) {
        printf("Today is Wednesday.\n");
    }

    return 0;
}

运行结果

Today is Wednesday.

在这个例子中,enum Weekday 定义了一个枚举类型,其中包含一周七天的常量。从 SUNDAY 开始,每个常量默认对应一个整数值,依次递增。SUNDAY 的值为 0,MONDAY 为 1,以此类推。

自定义枚举值

你可以显式地为枚举常量赋值,并且后续未赋值的枚举常量将基于前一个常量递增。例如:

enum Weekday {
    SUNDAY = 7,
    MONDAY,
    TUESDAY = 10,
    WEDNESDAY,
    THURSDAY = 5,
    FRIDAY,
    SATURDAY
};

在这个例子中:

  • SUNDAY 的值为 7。
  • MONDAY 自动递增为 8。
  • TUESDAY 显式设为 10。
  • WEDNESDAY 自动递增为 11。
  • THURSDAY 显式设为 5。
  • FRIDAY 自动递增为 6。
  • SATURDAY 自动递增为 7。

枚举的用途

  • 状态管理:枚举常用于表示程序中的各种状态,例如文件操作状态、程序运行模式等。

    enum Status {
        OK,
        ERROR,
        UNKNOWN
    };
    
  • 选项标记:可以用枚举来表示一组可能的选项或标记,便于条件判断。

    enum Color {
        RED,
        GREEN,
        BLUE
    };
    
  • 代码可读性:枚举能够使代码更加直观易懂,避免直接使用“魔术数字”(hard-coded numbers)。

枚举的底层类型

枚举类型的底层表示为整数类型,通常是 int。在某些编译器中,你可以指定枚举类型的底层表示为其他整数类型,但在C标准中,枚举通常与 int 类型关联。

枚举的优点

  • 提高代码可读性:使用枚举可以避免使用难以理解的数字,使代码更具表达力。
  • 方便维护:如果某些常量的值需要改变,只需在枚举定义中修改,而无需遍历整个代码。
  • 减少错误:枚举定义了一组有效值,使用这些具名常量可以避免输入错误的数字。

枚举的缺点

  • 类型安全性较弱:C语言中的枚举类型没有严格的类型检查,枚举变量可以直接赋值为整数,这可能导致逻辑错误。

    enum Weekday day;
    day = 10; // 虽然不合逻辑,但编译器不会报错
    

动态内存管理

动态内存管理是C语言中的一种技术,允许程序在运行时动态分配、使用和释放内存。与静态内存分配不同,动态内存分配可以灵活地管理内存资源,使程序能够适应不同的需求和数据大小。

动态内存管理函数

C标准库提供了一组函数用于动态分配和管理内存,主要包括以下几种:

  1. malloc(memory allocation)

    • 功能:分配一块指定大小的内存,并返回指向该内存的指针。
    • 返回值:成功时返回分配的内存的指针,失败时返回 NULL
    • 语法:
      void* malloc(size_t size);
      
    • 示例:
      int* ptr = (int*)malloc(10 * sizeof(int));
      if (ptr == NULL) {
          // 处理内存分配失败的情况
      }
      
  2. calloc(contiguous allocation)

    • 功能:分配指定数量的内存块,每个内存块的大小相同,并初始化所有内存块为零。
    • 返回值:成功时返回分配的内存的指针,失败时返回 NULL
    • 语法:
      void* calloc(size_t num, size_t size);
      
    • 示例:
      int* ptr = (int*)calloc(10, sizeof(int));
      if (ptr == NULL) {
          // 处理内存分配失败的情况
      }
      
  3. realloc(reallocation)

    • 功能:调整先前分配的内存块的大小。它可以增加或减少内存块的大小,并可能将内存块移动到新的位置。
    • 返回值:成功时返回指向新内存的指针,失败时返回 NULL
    • 语法:
      void* realloc(void* ptr, size_t new_size);
      
    • 示例:
      ptr = (int*)realloc(ptr, 20 * sizeof(int));
      if (ptr == NULL) {
          // 处理内存重新分配失败的情况
      }
      
  4. free

    • 功能:释放先前使用 malloccallocrealloc 分配的内存块,避免内存泄漏。
    • 语法:
      void free(void* ptr);
      
    • 示例:
      free(ptr);
      

动态内存管理的风险

  • 内存泄漏:如果忘记释放不再使用的内存,程序会耗尽可用内存,导致性能下降或程序崩溃。
  • 野指针:在释放内存后继续使用该内存的指针可能会导致未定义行为。
  • 碎片化:频繁的分配和释放操作可能会导致内存碎片化,从而降低内存分配的效率。
  • 双重释放:重复调用 free 释放同一块内存可能会导致程序崩溃或未定义行为。

示例:动态分配数组

以下是使用动态内存管理创建和使用动态数组的简单示例:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int n;
    printf("Enter the number of elements: ");
    scanf("%d", &n);

    // 动态分配数组
    int* array = (int*)malloc(n * sizeof(int));
    if (array == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }

    // 初始化并打印数组
    for (int i = 0; i < n; i++) {
        array[i] = i + 1;
        printf("%d ", array[i]);
    }
    printf("\n");

    // 释放内存
    free(array);

    return 0;
}

链表

链表(Linked List)是一种常见的数据结构,用于存储一组数据元素。与数组不同,链表中的数据元素(节点)在内存中不必是连续的。每个节点包含数据和一个指向下一个节点的指针,链表通过这些指针将所有节点链接在一起。

链表的基本结构

链表由一系列节点(Node)组成,每个节点包含两个部分:

  1. 数据域(Data Field):存储实际的数据。
  2. 指针域(Pointer Field):存储指向下一个节点的指针。

最常见的链表类型是单向链表(Singly Linked List),其结构定义如下:

struct Node {
    int data;           // 数据域
    struct Node* next;  // 指针域
};

链表的类型

  1. 单向链表(Singly Linked List)

    • 每个节点只包含一个指向下一个节点的指针。
    • 链表的最后一个节点的指针指向 NULL,表示链表的结束。
  2. 双向链表(Doubly Linked List)

    • 每个节点包含两个指针,一个指向下一个节点,一个指向前一个节点。
    • 可以从任意节点向前或向后遍历链表。
  3. 循环链表(Circular Linked List)

    • 单向或双向链表的变种。
    • 最后一个节点的指针指向链表的第一个节点,形成一个环状结构。

链表的基本操作

  1. 创建链表

    • 通过动态内存分配为节点分配内存,然后将节点连接起来。
  2. 插入节点

    • 可以在链表的头部、尾部或中间插入新节点。
  3. 删除节点

    • 可以删除头节点、尾节点或特定位置的节点。
  4. 遍历链表

    • 从头节点开始,依次访问链表中的每个节点。
  5. 查找节点

    • 查找链表中包含特定数据的节点。

示例:单向链表的基本操作

以下是使用C语言实现的一个简单的单向链表的示例,包括创建链表、插入节点、遍历链表和删除节点的功能。

#include <stdio.h>
#include <stdlib.h>

// 定义链表节点结构
struct Node {
    int data;
    struct Node* next;
};

// 创建新节点
struct Node* createNode(int data) {
    struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

// 在链表的头部插入新节点
void insertAtHead(struct Node** head, int data) {
    struct Node* newNode = createNode(data);
    newNode->next = *head;
    *head = newNode;
}

// 在链表中打印所有节点
void printList(struct Node* head) {
    struct Node* current = head;
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

// 删除链表中的一个节点
void deleteNode(struct Node** head, int key) {
    struct Node* temp = *head;
    struct Node* prev = NULL;

    // 如果头节点是要删除的节点
    if (temp != NULL && temp->data == key) {
        *head = temp->next;
        free(temp);
        return;
    }

    // 搜索要删除的节点
    while (temp != NULL && temp->data != key) {
        prev = temp;
        temp = temp->next;
    }

    // 如果找不到节点
    if (temp == NULL) return;

    // 取消链接并释放内存
    prev->next = temp->next;
    free(temp);
}

int main() {
    struct Node* head = NULL;

    // 插入节点
    insertAtHead(&head, 1);
    insertAtHead(&head, 2);
    insertAtHead(&head, 3);

    // 打印链表
    printf("Linked List: ");
    printList(head);

    // 删除节点
    deleteNode(&head, 2);
    printf("After Deletion: ");
    printList(head);

    return 0;
}

输出结果

Linked List: 3 -> 2 -> 1 -> NULL
After Deletion: 3 -> 1 -> NULL

文件信息区和文件指针

在C语言中,文件信息区和文件指针是文件操作的两个重要概念。理解这两个概念对于文件的读取、写入和管理至关重要。

文件信息区

文件信息区是操作系统为每个打开的文件在内存中分配的一个数据结构,用于存储与该文件相关的状态信息和控制信息。这些信息通常包括:

  1. 文件描述符:唯一标识一个打开文件的整数,用于区分不同的文件。
  2. 文件当前位置:当前读取或写入文件的位置(即文件指针的位置)。
  3. 文件的打开模式:文件是以只读、写入还是追加的方式打开。
  4. 缓冲区:用于存储从磁盘读入或准备写入磁盘的数据,提升文件操作的效率。
  5. 错误状态:用于记录文件操作中的错误信息。

文件信息区在文件打开时创建,并在文件关闭时释放。每个文件操作函数都需要访问文件信息区,以便正确地执行读写操作。

FILE类型

FILE 类型是C标准库定义的一个结构体类型,用于表示文件流(file stream)。它封装了文件操作所需的各种信息和状态,用于管理文件的读取、写入、定位等操作。

在C语言中,文件操作涉及到对文件的打开、关闭、读写等一系列操作。为了简化这些操作,C标准库定义了 FILE 结构体,它内部保存了文件的相关信息,如文件指针、文件状态、缓冲区等。用户在使用文件操作函数(如 fopenfreadfwrite 等)时,只需要操作这个 FILE 类型的指针,而无需关心文件的具体实现细节。

文件指针

文件指针是一个指向文件信息区的指针,它实际上是一个 FILE* 类型的指针。通过文件指针,程序可以访问和操作与特定文件关联的文件信息区。

文件指针主要用于以下操作:

  1. 打开文件:通过 fopen() 函数打开文件时,返回一个 FILE* 类型的文件指针。
  2. 读取文件:通过 fread()fgets() 等函数使用文件指针从文件中读取数据。
  3. 写入文件:通过 fwrite()fputs() 等函数使用文件指针向文件中写入数据。
  4. 移动文件指针:通过 fseek() 函数移动文件指针,以便在文件的不同位置进行读写操作。
  5. 关闭文件:通过 fclose() 函数关闭文件并释放文件信息区。

fopen()fcolse()

fopenfclose 是C语言中用于文件操作的两个常用函数。fopen 用于打开文件,而 fclose 用于关闭文件。

fopen 函数

fopen 函数用于打开一个文件,并返回一个指向 FILE 类型的指针。这个指针可以用于后续的文件读写操作。如果文件打开失败,fopen 会返回 NULL

语法

FILE *fopen(const char *filename, const char *mode);

参数

  • filename:要打开的文件的路径和名称。
  • mode:文件的打开模式,用于指定文件的读写方式。

常见的文件打开模式

模式描述
"r"只读方式打开文件。文件必须存在。
"w"写入方式打开文件。如果文件存在,则清空文件内容;如果文件不存在,则创建新文件。
"a"追加方式打开文件。如果文件存在,写入的数据会追加到文件末尾;如果文件不存在,则创建新文件。
"r+"读写方式打开文件。文件必须存在。
"w+"读写方式打开文件。如果文件存在,则清空文件内容;如果文件不存在,则创建新文件。
"a+"读写方式打开文件。如果文件存在,写入的数据会追加到文件末尾;如果文件不存在,则创建新文件。
"rb"二进制模式只读打开文件。文件必须存在。
"wb"二进制模式写入打开文件。如果文件存在,则清空文件内容;如果文件不存在,则创建新文件。
"ab"二进制模式追加打开文件。如果文件存在,写入的数据会追加到文件末尾;如果文件不存在,则创建新文件。

示例

#include <stdio.h>

int main() {
    FILE *fp;

    // 以写入模式打开文件
    fp = fopen("example.txt", "w");
    if (fp == NULL) {
        printf("Error opening file!\n");
        return 1;
    }

    // 写入数据到文件
    fprintf(fp, "Hello, World!\n");

    // 关闭文件
    fclose(fp);
    fp = NULL;

    return 0;
}
fclose 函数

fclose 函数用于关闭一个由 fopen 或其他打开文件的函数(如 freopen)打开的文件,并释放与该文件相关的资源。如果 fclose 成功执行,返回值为 0;如果失败,返回值为 EOF(通常为 -1)。

语法

int fclose(FILE *stream);

参数

  • stream:要关闭的文件指针,即 fopen 返回的 FILE* 指针。

示例

#include <stdio.h>

int main() {
    FILE *fp;

    // 打开文件进行写入
    fp = fopen("example.txt", "w");
    if (fp == NULL) {
        printf("Error opening file!\n");
        return 1;
    }

    // 写入数据
    fprintf(fp, "Hello, World!\n");

    // 关闭文件
    if (fclose(fp) != 0) {
        printf("Error closing file!\n");
        return 1;
    }
    
    fp = NULL;

    return 0;
}

fgetc()和fputc()

fgetcfputc 是C标准库中用于处理单个字符的文件输入输出函数。fgetc 用于从文件中读取一个字符,fputc 用于向文件中写入一个字符。

fgetc 函数

fgetc 函数用于从指定的文件流中读取一个字符,并将其返回为 int 类型。如果到达文件末尾或读取出错,返回 EOF

语法

int fgetc(FILE *stream);

参数

  • stream:指向 FILE 类型的文件指针,表示要读取的文件。

返回值

  • 返回读取的字符(作为 unsigned char 提升后的 int 类型)。
  • 如果到达文件末尾或发生错误,返回 EOF(通常为 -1)。

示例

#include <stdio.h>

int main() {
    FILE *fp;
    int ch;

    // 打开文件进行读取
    fp = fopen("example.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 使用 fgetc 逐个读取字符并输出到控制台
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }

    // 关闭文件
    fclose(fp);

    return 0;
}
fputc 函数

fputc 函数用于将一个字符写入指定的文件流,并返回写入的字符作为 unsigned char 提升后的 int 类型。如果写入出错,返回 EOF

语法

int fputc(int char, FILE *stream);

参数

  • char:要写入的字符,传入时为 int 类型,但实际写入时为 unsigned char 类型。
  • stream:指向 FILE 类型的文件指针,表示要写入的文件。

返回值

  • 返回写入的字符(作为 unsigned char 提升后的 int 类型)。
  • 如果发生错误,返回 EOF

示例

#include <stdio.h>

int main() {
    FILE *fp;
    int ch;

    // 打开文件进行写入
    fp = fopen("output.txt", "w");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 使用 fputc 写入字符到文件
    for (ch = 'A'; ch <= 'Z'; ch++) {
        fputc(ch, fp);
    }

    // 关闭文件
    fclose(fp);

    return 0;
}

fgets()和fputs()

fgets()fputs() 是 C 语言中用于处理字符串的文件输入输出函数。fgets() 用于从文件中读取一行字符串,fputs() 用于向文件中写入一行字符串。

fgets() 函数

fgets() 用于从指定的文件流中读取一行字符,并将其存储在指定的字符串数组中。读取的字符串以 null 字符 '\0' 结尾。

语法

char *fgets(char *str, int n, FILE *stream);

参数

  • str:指向用于存储读取字符串的字符数组。
  • n:要读取的最大字符数,包括字符串末尾的 '\0'
  • stream:指向 FILE 类型的文件指针,表示要读取的文件。

返回值

  • 返回指向字符串 str 的指针。
  • 如果读取到文件末尾或发生错误,返回 NULL

使用示例

#include <stdio.h>

int main() {
    FILE *fp;
    char buffer[100];

    // 打开文件进行读取
    fp = fopen("example.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 使用 fgets 读取文件中的一行
    while (fgets(buffer, 100, fp) != NULL) {
        printf("%s", buffer);
    }

    // 关闭文件
    fclose(fp);

    return 0;
}
fputs() 函数

fputs() 用于将一个字符串写入指定的文件流。字符串不包括末尾的 '\0' 字符。

语法

int fputs(const char *str, FILE *stream);

参数

  • str:要写入的字符串。
  • stream:指向 FILE 类型的文件指针,表示要写入的文件。

返回值

  • 成功时返回一个非负值。
  • 如果发生错误,返回 EOF

使用示例

#include <stdio.h>

int main() {
    FILE *fp;
    const char *text = "Hello, World!\n";

    // 打开文件进行写入
    fp = fopen("output.txt", "w");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 使用 fputs 写入字符串到文件
    fputs(text, fp);

    // 关闭文件
    fclose(fp);

    return 0;
}
总结
  • fgets():从文件中读取一行字符串,并存储在给定的字符数组中,读取的字符串包括换行符 '\n',以 '\0' 结尾。
  • fputs():将一个字符串写入文件,不写入字符串末尾的 '\0',但可以包含换行符 '\n'

这两个函数常用于文件的逐行读取和写入操作。

fscanf()fprintf()

fscanf()fprintf() 是 C 语言中的标准输入输出函数,分别用于从文件中读取格式化数据和向文件中写入格式化数据。

fscanf() 函数

fscanf() 函数用于从指定的文件流中读取格式化输入,并将其存储在指定的变量中。它的功能类似于 scanf(),但目标是文件而非标准输入。

语法

int fscanf(FILE *stream, const char *format, ...);

参数

  • stream:指向 FILE 类型的文件指针,表示要读取的文件。
  • format:格式字符串,指定要读取的数据类型和格式。
  • ...:变量的指针,用于存储读取的数据。

返回值

  • 成功读取并匹配的输入项的数量。
  • 如果达到文件末尾或发生读取错误,返回 EOF

使用示例

#include <stdio.h>

int main() {
    FILE *fp;
    int num;
    char str[50];

    // 打开文件进行读取
    fp = fopen("data.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 使用 fscanf 读取格式化数据
    while (fscanf(fp, "%d %s", &num, str) != EOF) {
        printf("Number: %d, String: %s\n", num, str);
    }

    // 关闭文件
    fclose(fp);

    return 0;
}
fprintf() 函数

fprintf() 函数用于将格式化输出写入指定的文件流。它的功能类似于 printf(),但输出目标是文件而非标准输出。

语法

int fprintf(FILE *stream, const char *format, ...);

参数

  • stream:指向 FILE 类型的文件指针,表示要写入的文件。
  • format:格式字符串,指定要写入的数据类型和格式。
  • ...:要写入的数据。

返回值

  • 成功写入的字符数(不包括终止的 null 字符 '\0')。
  • 如果发生错误,返回一个负值。

使用示例

#include <stdio.h>

int main() {
    FILE *fp;
    int num = 42;
    const char *str = "Hello, World!";

    // 打开文件进行写入
    fp = fopen("output.txt", "w");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 使用 fprintf 写入格式化数据
    fprintf(fp, "Number: %d, String: %s\n", num, str);

    // 关闭文件
    fclose(fp);

    return 0;
}

fread()fwrite()

fread()fwrite() 是 C 语言中用于二进制文件处理的标准输入输出函数。fread() 用于从文件中读取二进制数据,而 fwrite() 用于向文件中写入二进制数据。

fread() 函数

fread() 函数用于从指定的文件流中读取数据到数组中。它通常用于读取二进制文件。

语法

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

参数

  • ptr:指向接收读取数据的数组的指针。
  • size:每个元素的字节大小。
  • nmemb:要读取的元素数量。
  • stream:指向 FILE 类型的文件指针,表示要读取的文件。

返回值

  • 成功时,返回实际读取的元素数量。
  • 如果读取的元素数少于请求的数量,或者到达文件末尾,返回值可能小于 nmemb

使用示例

#include <stdio.h>

int main() {
    FILE *fp;
    int buffer[10];

    // 打开文件进行读取
    fp = fopen("data.bin", "rb");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 使用 fread 读取二进制数据
    size_t read_count = fread(buffer, sizeof(int), 10, fp);
    printf("Elements read: %zu\n", read_count);

    // 关闭文件
    fclose(fp);

    return 0;
}
fwrite() 函数

fwrite() 函数用于将数据从数组写入到指定的文件流中。它通常用于写入二进制文件。

语法

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

参数

  • ptr:指向要写入数据的数组的指针。
  • size:每个元素的字节大小。
  • nmemb:要写入的元素数量。
  • stream:指向 FILE 类型的文件指针,表示要写入的文件。

返回值

  • 成功时,返回实际写入的元素数量。
  • 如果写入的元素数少于请求的数量,可能发生写入错误。

使用示例

#include <stdio.h>

int main() {
    FILE *fp;
    int buffer[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

    // 打开文件进行写入
    fp = fopen("output.bin", "wb");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 使用 fwrite 写入二进制数据
    size_t write_count = fwrite(buffer, sizeof(int), 10, fp);
    printf("Elements written: %zu\n", write_count);

    // 关闭文件
    fclose(fp);

    return 0;
}

ftell()fseek()

ftell()fseek() 是 C 语言中常用的文件操作函数,通常一起使用来获取和设置文件指针的位置。

ftell() 函数

ftell() 用于获取文件指针在文件中的当前位置,返回的是从文件开始到当前文件指针位置的字节数。

语法

long ftell(FILE *stream);

参数

  • stream:指向 FILE 类型的文件指针,表示要获取当前位置的文件。

返回值

  • 成功时,返回当前文件指针的位置(从文件开头到当前指针位置的字节数)。
  • 失败时,返回 -1L,并设置错误标志,可以通过 ferror() 函数检查错误。

示例

long position = ftell(fp);
fseek() 函数

fseek() 用于移动文件指针到文件中的指定位置。它可以相对于文件的开头、当前指针位置或文件末尾进行移动。

语法

int fseek(FILE *stream, long offset, int whence);

参数

  • stream:指向 FILE 类型的文件指针,表示要操作的文件。
  • offset:偏移量,表示要移动的字节数。
  • whence:位置常量,决定偏移量的参考点。
    • SEEK_SET:文件开头。
    • SEEK_CUR:当前文件指针位置。
    • SEEK_END:文件末尾。

返回值

  • 成功时,返回 0
  • 失败时,返回非零值,并设置错误标志。

示例

// 移动文件指针到文件的第三个字节
fseek(fp, 2, SEEK_SET);
综合示例

以下是一个同时使用 ftell()fseek() 的示例,演示如何获取和设置文件指针位置:

#include <stdio.h>

int main() {
    FILE *fp;
    long position;

    // 打开文件
    fp = fopen("example.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 移动文件指针到文件的第三个字节
    fseek(fp, 2, SEEK_SET);

    // 获取当前文件指针位置
    position = ftell(fp);
    if (position != -1L) {
        printf("Current file pointer position: %ld\n", position);
    } else {
        perror("ftell failed");
    }

    // 关闭文件
    fclose(fp);

    return 0;
}

feof()ferror()

feof()ferror() 是 C 语言中的标准库函数,用于检测文件操作过程中是否到达文件末尾或发生错误。它们通常与其他文件操作函数(如 fread()fwrite()fgetc() 等)一起使用,以确保文件操作的正确性。

feof() 函数

feof() 用于检查文件指针是否到达文件末尾。

语法

int feof(FILE *stream);

参数

  • stream:指向 FILE 类型的文件指针,表示要检查的文件。

返回值

  • 如果文件指针到达文件末尾,则返回非零值(通常为 1)。
  • 如果未到达文件末尾,则返回 0

使用示例

#include <stdio.h>

int main() {
    FILE *fp;
    int c;

    // 打开文件
    fp = fopen("example.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 读取文件内容
    while ((c = fgetc(fp)) != EOF) {
        putchar(c);
    }

    // 检查是否到达文件末尾
    if (feof(fp)) {
        printf("\nEnd of file reached.\n");
    } else {
        printf("\nNot end of file.\n");
    }

    // 关闭文件
    fclose(fp);

    return 0;
}
ferror() 函数

ferror() 用于检查文件操作是否发生错误。

语法

int ferror(FILE *stream);

参数

  • stream:指向 FILE 类型的文件指针,表示要检查的文件。

返回值

  • 如果发生错误,则返回非零值。
  • 如果未发生错误,则返回 0

使用示例

#include <stdio.h>

int main() {
    FILE *fp;
    int c;

    // 打开文件
    fp = fopen("example.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 读取文件内容
    while ((c = fgetc(fp)) != EOF) {
        putchar(c);
    }

    // 检查是否发生错误
    if (ferror(fp)) {
        printf("\nAn error occurred while reading the file.\n");
    } else {
        printf("\nNo errors occurred during file read.\n");
    }

    // 关闭文件
    fclose(fp);

    return 0;
}

其他知识

Debug和Release的区别

比较
特性Debug 模式Release 模式
调试信息生成详细的调试信息通常不生成调试信息
代码优化不进行优化进行多种优化
执行速度较慢更快
内存占用较大较小
断言启用断言禁用断言
可执行文件大小较大较小
使用场景
  • Debug 模式
    • 开发和调试阶段,用于检测和修复错误。
    • 需要详细的调试信息和原始代码行为。
  • Release 模式
    • 软件发布阶段,用于生成高效的可执行程序。
    • 需要更快的执行速度和更小的文件大小。

调试快捷键

以下是 VS Code 中调试 C 语言程序常用的快捷键及其功能,列成表格:

快捷键功能描述
F9创建或取消断点
F5启动调试,运行程序并在断点处暂停
Ctrl+F5开始执行但不调试,直接运行程序
F10逐过程(Step Over),跳过函数调用
F11逐语句(Step Into),进入函数内部
Shift+F11逐出(Step Out),跳出当前函数,返回上一级调用处
  • 创建和取消断点
    • F9:在光标所在行创建或取消断点,控制程序在指定位置暂停。
  • 启动和控制调试
    • F5:启动调试,程序将运行并在第一个断点处暂停,经常与 F9 配合使用。
    • Ctrl+F5:开始执行但不调试,直接运行程序而不会暂停在断点处。
  • 控制程序执行
    • F10:逐过程执行代码,跳过函数调用的具体实现,只执行到函数调用的下一行,适用于处理一次函数调用或一条语句。
    • F11:逐语句执行代码,进入函数内部,在函数调用处使用 F11 可以观察函数的详细执行过程。
    • Shift+F11:逐出当前函数,快速跳出当前函数,返回上一级调用处。

asset()断言

assert 是C语言中的一个宏,用于在程序中进行断言。断言是一种调试工具,用来在程序运行时检查某个条件是否为真。如果条件为假,assert 会打印错误信息并终止程序执行。这有助于在开发和测试阶段发现程序中的逻辑错误和不一致。

要使用 assert,需要包含头文件 <assert.h>

#include <assert.h>

asset语法

assert(expression);
  • expression:这是一个逻辑表达式。如果 expression 的值为假(即为0),assert 会终止程序并输出错误信息。如果 expression 的值为真(非0),程序继续执行。
示例代码

以下是一个使用 assert 的简单示例:

#include <stdio.h>
#include <assert.h>

void test_positive(int num) {
    assert(num > 0); // 断言 num 必须为正数
    printf("num is positive: %d\n", num);
}

int main() {
    int a = 10;
    int b = -5;

    test_positive(a); // 正确,程序继续执行
    test_positive(b); // 错误,程序中止并输出断言失败信息

    return 0;
}

在上述示例中,如果传递给 test_positive 函数的参数为负数,assert 会使程序终止,并输出如下错误信息:

Assertion failed: (num > 0), function test_positive, file example.c, line 6.
断言的输出

当断言失败时,assert 宏会输出以下信息:

  • 失败的表达式
  • 包含该 assert 语句的文件名
  • 包含该 assert 语句的行号
  • 包含该 assert 语句的函数名(如果支持)
禁用assert

在发布版本中,通常会禁用断言以提高性能。可以通过定义 NDEBUG 宏来禁用 assert

#define NDEBUG
#include <assert.h>

定义 NDEBUG 宏后,所有的 assert 语句都会被预处理器移除,不再执行。

程序的编译和链接

在 C 语言(以及其他许多编程语言)中,程序的构建过程通常包括两个关键步骤:编译和链接。这两个步骤将源代码转换为可执行的程序。下面是对这两个过程的详细介绍。

编译(Compilation)

编译是将人类可读的源代码(如 .c 文件)转换为机器可以理解的二进制代码(目标代码或中间代码)的过程。

编译过程的主要步骤:

  • 预处理(Preprocessing):
    • 预处理器(Preprocessor)首先处理源代码中的预处理指令(如 #include#define 等)。它会进行宏替换、文件包含、条件编译等操作,生成预处理后的代码。
  • 词法分析(Lexical Analysis):
    • 编译器分析源代码,将其分解为标记(Tokens),这些标记是代码的基本组成单元,如关键字、变量名、操作符等。
  • 语法分析(Syntax Analysis):
    • 编译器检查标记序列是否符合语言的语法规则,构建语法树(Syntax Tree)。
  • 语义分析(Semantic Analysis):
    • 编译器进一步检查程序是否符合语义规则,例如变量类型是否正确、函数调用是否匹配等。
  • 中间代码生成(Intermediate Code Generation):
    • 编译器将语法树转换为中间代码,这是一种独立于具体机器的代码形式。
  • 优化(Optimization):
    • 编译器可以对中间代码进行优化,以提高程序的执行效率或减少内存占用。
  • 目标代码生成(Code Generation):
    • 最后,编译器将中间代码转换为特定平台的目标代码,通常是机器代码或汇编代码。
  • 汇编(Assembly):
    • 如果生成的是汇编代码,还需要汇编器将其转换为二进制的目标文件(.o.obj 文件)。
链接(Linking)

链接是将编译生成的目标文件与程序依赖的库文件组合起来,生成最终的可执行文件的过程。

链接过程的主要步骤:

  • 符号解析(Symbol Resolution):
    • 链接器(Linker)解析目标文件中使用的符号(如函数和变量名),确保每个符号都有唯一的定义。如果一个符号在多个目标文件中出现,链接器必须处理这些重复定义。
  • 地址分配(Address Allocation):
    • 链接器为每个目标文件的代码和数据段分配内存地址,使得各个目标文件可以协同工作。
  • 重定位(Relocation):
    • 由于目标文件中的代码和数据段还未分配最终的内存地址,链接器会调整这些段的地址,并修改代码中的相应指令,使得它们可以正确地访问内存。
  • 库的链接(Library Linking):
    • 链接器将程序中引用的库文件(如标准库或第三方库)与目标文件链接在一起。如果是静态链接,库代码会被复制到最终的可执行文件中;如果是动态链接,程序运行时会从共享库加载所需的函数。
  • 生成可执行文件(Generating Executable File):
    • 最后,链接器将所有的目标文件和库组合成一个可执行文件。这个文件包含了程序运行所需的所有机器代码和数据。
编译和链接的示例

假设有两个源文件 main.chelper.c,它们分别定义了主函数和辅助函数。

  1. 编译:

    gcc -c main.c -o main.o
    gcc -c helper.c -o helper.o
    

    这里的 -c 选项告诉编译器只进行编译而不进行链接,生成目标文件 main.ohelper.o

  2. 链接:

    gcc main.o helper.o -o my_program
    

    这一步链接生成了最终的可执行文件 my_program

预定义符号

预定义符号(Predefined Macros)是由编译器在编译过程中自动定义的一组宏。它们用于提供关于编译器、操作系统、代码所在文件、当前行号等信息。预定义符号在代码中常用于调试、条件编译以及获取编译环境的相关信息。

常见的预定义符号

以下是一些常见的预定义符号及其功能:

预定义符号说明
__FILE__当前文件的名称(包含路径)。
__LINE__当前行号。
__DATE__编译的日期,格式为 “Mmm dd yyyy”(如 “Aug 12 2024”)。
__TIME__编译的时间,格式为 “hh:mm:ss”。
__func__当前所在的函数名。
__STDC__如果定义为 1,表示程序严格遵循 ANSI C 标准。
__cplusplus表示正在使用 C++ 编译器进行编译,通常用于在 C++ 代码中进行条件编译。
__GNUC__定义 GCC 编译器的版本号。例如,__GNUC__ 为 9 表示 GCC 9.x。
__STDC_VERSION__定义 C 标准的版本号。如果定义为 199901L,则表示支持 C99 标准。
使用示例

下面是一个简单的示例,演示如何使用这些预定义符号:

#include <stdio.h>

int main() {
    printf("File: %s\n", __FILE__);
    printf("Line: %d\n", __LINE__);
    printf("Date: %s\n", __DATE__);
    printf("Time: %s\n", __TIME__);
    printf("Function: %s\n", __func__);

#ifdef __GNUC__
    printf("Compiled with GCC version %d.%d\n", __GNUC__, __GNUC_MINOR__);
#endif

#ifdef __cplusplus
    printf("C++ mode\n");
#else
    printf("C mode\n");
#endif

    return 0;
}
输出示例

假设这个代码文件名为 example.c,并使用 GCC 编译器进行编译,可能的输出如下:

File: example.c
Line: 6
Date: Aug 12 2024
Time: 14:32:10
Function: main
Compiled with GCC version 9.3
C mode
主要用途
  • 调试信息: __FILE____LINE____func__ 常用于在调试和错误处理时输出有用的信息,帮助开发者定位问题。
  • 条件编译: 预定义符号可用于检查编译器类型、标准版本等,从而在不同的编译环境下执行不同的代码。
  • 编译时间信息: __DATE____TIME__ 提供了编译时的日期和时间信息,可以用于生成编译日志或显示程序的编译时间。

预定义符号在 C/C++ 编程中是非常有用的工具,可以帮助程序员更好地控制代码的编译过程,并提供有用的上下文信息。

#define

#define 是 C 语言和 C++ 语言中的一个预处理指令,用于定义宏。宏是由预处理器在编译前进行替换的文本块。#define 可以用来定义常量、宏函数、条件编译等,帮助简化代码、提高可读性和可维护性。

语法
#define MACRO_NAME replacement_text
使用方法
  1. 常量定义

    #define PI 3.14159
    

    在代码中使用 PI 时,预处理器会将其替换为 3.14159。例如:

    double area = PI * radius * radius;
    
  2. 宏函数

    #define SQUARE(x) ((x) * (x))
    

    这定义了一个宏函数 SQUARE,它接受一个参数 x,并计算 x 的平方。在代码中使用时:

    int result = SQUARE(5);  // 结果是 25
    

    注意在宏函数中使用括号来确保运算的优先级正确。

  3. 条件编译

    #define DEBUG
    
    #ifdef DEBUG
    printf("Debug mode is enabled\n");
    #endif
    

    这里 DEBUG 是一个标志宏,在代码中可以用 #ifdef#endif 来有条件地编译代码块。#ifdef 检查宏是否被定义,如果定义了就编译相应的代码。

宏和函数的区别
image-20240817224919393

预处理符号

预处理操作符 # 是 C 语言和 C++ 语言中的一种预处理指令,用于处理宏定义。它们在宏定义和宏展开中起着关键作用。# 操作符用于生成字符串化的宏参数和进行宏替换。

字符串化操作符 #

字符串化操作符 # 用于将宏参数转换为字符串。它在宏定义中使用,可以将参数变成一个字符串字面量。这个操作符在宏展开时会将参数周围的内容转换为字符串。

语法:

#define STRINGIFY(x) #x

示例:

#include <stdio.h>

#define TO_STRING(x) #x

int main() {
    printf("%s\n", TO_STRING(Hello World));  // 输出 "Hello World"
    return 0;
}

在这个例子中,TO_STRING 宏将 Hello World 变成了字符串 "Hello World"

拼接操作符 ##

拼接操作符 ## 用于将宏参数拼接成一个新的标识符。在宏定义中,它可以用来组合两个宏参数,形成新的宏名或代码片段。

语法:

#define CONCAT(x, y) x##y

示例:

#include <stdio.h>

#define CONCAT(x, y) x##y
#define NAME(a) CONCAT(a, _suffix)

int main() {
    int my_suffix = 10;
    printf("%d\n", NAME(my));  // 输出 10
    return 0;
}

在这个例子中,NAME(my) 被展开为 my_suffix,而 my_suffix 是一个已定义的变量。

#include

在 C 语言和 C++ 语言中,#include 指令用于包含头文件,允许程序使用头文件中定义的函数、宏、数据结构等。#include 指令可以使用两种不同的语法:尖括号 (<>) 和双引号 ("")。它们的主要区别在于文件的查找路径。

#include <>
  • 语法: #include <header.h>

  • 用途: 用于包含系统头文件或标准库头文件。

  • 查找路径: 编译器首先在预定义的系统目录中查找头文件,这些目录通常包括标准库的位置和编译器提供的系统头文件路径。

  • 示例:

    #include <stdio.h>
    #include <stdlib.h>
    

    在这个示例中,stdio.hstdlib.h 是标准库头文件,通常位于编译器预定义的系统路径中。

#include ""
  • 语法: #include "header.h"

  • 用途: 用于包含用户自定义的头文件或项目中的头文件。

  • 查找路径: 编译器首先在当前源文件所在的目录中查找头文件。如果在当前目录中找不到,编译器才会在系统目录中查找。

  • 示例:

    #include "myheader.h"
    

    在这个示例中,myheader.h 是项目中的自定义头文件,通常放在项目的源代码目录中。

总结比较
特性#include <>#include ""
主要用途系统头文件、标准库头文件用户自定义头文件、项目特定头文件
查找路径先在系统目录查找先在当前源文件所在目录查找
查找顺序系统路径中查找当前目录查找,找不到时再查找系统目录
示例#include <stdio.h>#include "myheader.h"

条件编译

条件编译是 C 语言和 C++ 语言中的一个预处理功能,用于根据特定条件编译代码的不同部分。这种机制可以在编译阶段控制代码的编译,常用于跨平台开发、调试和配置不同的编译选项。

主要条件编译指令
  1. #ifdef#ifndef

    • #ifdef(如果已定义)指令检查宏是否已被定义。
    • #ifndef(如果未定义)指令检查宏是否未被定义。

    语法:

    #ifdef MACRO_NAME
    // 如果 MACRO_NAME 已定义,则编译这部分代码
    #endif
    
    #ifndef MACRO_NAME
    // 如果 MACRO_NAME 未定义,则编译这部分代码
    #endif
    

    示例:

    #ifdef DEBUG
    printf("Debug mode is enabled\n");
    #endif
    

    如果 DEBUG 宏已定义,这段代码会被编译并执行。

  2. #if#elif

    • #if 用于基于一个表达式的值进行条件编译。
    • #elif(否则如果)用于在 #if 失败时,检查其他条件。

    语法:

    #if CONDITION
    // 如果 CONDITION 为真,则编译这部分代码
    #elif ANOTHER_CONDITION
    // 如果 CONDITION 为假,且 ANOTHER_CONDITION 为真,则编译这部分代码
    #else
    // 如果以上条件都不满足,则编译这部分代码
    #endif
    

    示例:

    #if PLATFORM == WINDOWS
    printf("Running on Windows\n");
    #elif PLATFORM == LINUX
    printf("Running on Linux\n");
    #else
    printf("Unknown platform\n");
    #endif
    

    根据 PLATFORM 的不同值,编译器会选择不同的代码块。

  3. #define#undef

    • #define 用于定义一个宏。
    • #undef 用于取消对一个宏的定义。

    语法:

    #define MACRO_NAME value
    #undef MACRO_NAME
    

    示例:

    #define VERSION 1
    
    #ifdef VERSION
    printf("Version: %d\n", VERSION);
    #endif
    
    #undef VERSION
    

    这里 VERSION 宏被定义后,会执行相应的代码,之后用 #undef 取消宏的定义。

  4. #error#warning

    • #error 用于在编译时生成错误消息并终止编译。
    • #warning 用于在编译时生成警告消息。

    语法:

    #error "Error message"
    #warning "Warning message"
    

    示例:

    #ifndef REQUIRED_MACRO
    #error "REQUIRED_MACRO must be defined"
    #endif
    

    如果 REQUIRED_MACRO 未定义,编译器会生成一个错误消息并停止编译。

防止头文件重复包含

在 C 语言和 C++ 语言中,防止头文件重复包含是一个重要的编程实践,尤其是在大型项目中。重复包含同一个头文件可能导致编译错误和代码膨胀。为了解决这个问题,通常使用预处理指令来确保每个头文件只被包含一次。

方法
  1. 使用宏定义(Include Guards)

    这种方法通过定义一个唯一的宏来防止头文件被重复包含。在头文件开始时定义一个宏,在文件结束时取消定义这个宏。如果该宏已经定义,则说明头文件已经被包含过,编译器将忽略后续的包含。

    示例代码:

    // myheader.h
    
    #ifndef MYHEADER_H
    #define MYHEADER_H
    
    // 头文件内容
    
    #endif // MYHEADER_H
    

    解释:

    • #ifndef MYHEADER_H:检查 MYHEADER_H 是否未定义。
    • #define MYHEADER_H:定义 MYHEADER_H
    • #endif:结束条件编译。

    这段代码确保了 myheader.h 只会被包含一次。如果在同一编译单元中再次包含该头文件,MYHEADER_H 已经被定义,所以编译器会跳过该头文件的内容。

  2. 使用 #pragma once

    #pragma once 是一种编译器特定的指令,告诉编译器在编译过程中只包含一次该头文件。这种方法通常被认为更简洁,但它不是所有编译器都支持的标准。

    示例代码:

    // myheader.h
    
    #pragma once
    
    // 头文件内容
    

    解释:

    • #pragma once:指示编译器只包含该头文件一次,避免重复包含。
比较
特性Include Guards#pragma once
实现方式使用宏定义和条件编译编译器特定的指令
支持性标准支持,所有编译器都支持并非所有编译器都支持
简洁性需要多行代码更简洁,只需一行
兼容性高,确保兼容所有编译器可能存在一些兼容性问题
  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值