C语言高级特性

自定义数据类型

C 语言允许用户根据需要定义自己的数据类型,以更好地组织和表示数据。常见的自定义数据类型包括枚举、结构体、共用体,以及使用 typedef 创建类型别名。

枚举类型 (enum)

枚举类型用于定义一组命名的整数常量,使代码更具可读性。

基本语法

c
enum Color { RED, GREEN, BLUE }; // 默认从 0 开始赋值,RED=0, GREEN=1, BLUE=2

enum Status { OK = 0, WARNING = 1, ERROR = 2 }; // 显式赋值

使用示例

c
#include <stdio.h>

enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY };

int main() {
    enum Day today = WEDNESDAY;

    if (today == WEDNESDAY) {
        printf("Today is Wednesday.\n"); // 输出:Today is Wednesday.
    }

    printf("Value of FRIDAY: %d\n", FRIDAY); // 输出:Value of FRIDAY: 4
    return 0;
}

与 C++ 的区别: C++11 引入了作用域枚举 (enum class),解决了传统 C 语言枚举的一些问题(如命名冲突、隐式转换为整数)。C 语言的枚举常量属于全局作用域(或定义它们的作用域),并且可以隐式转换为 int

c
enum Color { RED_C, GREEN_C, BLUE_C };
enum State { ACTIVE, INACTIVE };

int main() {
    enum Color c = RED_C;
    // enum State s = RED_C; // 在 C 中通常不会报错,但逻辑上不推荐
    int val = GREEN_C; // C 语言中枚举可以隐式转换为 int
    printf("Value: %d\n", val);
    return 0;
}

结构体 (struct)

结构体允许将不同类型的数据项组合成一个单一的逻辑单元。

基本语法

c
struct Point {
    int x;
    double y;
    char label;
};

初始化与访问

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

struct Person {
    char name[50];
    int age;
};

int main() {
    // 初始化 (C99 及以后支持指定初始化器)
    struct Person p1 = {"Alice", 30};
    struct Person p2;
    strcpy(p2.name, "Bob");
    p2.age = 25;

    // 访问成员
    printf("Person 1: Name=%s, Age=%d\n", p1.name, p1.age);
    printf("Person 2: Name=%s, Age=%d\n", p2.name, p2.age);

    // 结构体指针访问
    struct Person* ptr_p = &p1;
    printf("Pointer Access: Name=%s, Age=%d\n", ptr_p->name, ptr_p->age);

    // C 语言中声明结构体变量需要带 struct 关键字,除非使用了 typedef
    // Point pt; // 错误,除非 Point 是 typedef 定义的别名
    struct Point pt_var;
    pt_var.x = 10;

    return 0;
}

与 C++ 的区别

  • struct vs class: C 语言没有 class 关键字。C++ 中 structclass 主要区别在于默认访问权限(struct 默认 publicclass 默认 private),并且 class 更常用于包含成员函数(方法)。
  • 声明: 在 C++ 中,定义结构体后可以直接使用结构体名声明变量(如 Point pt;),而在 C 语言中通常需要带 struct 关键字(如 struct Point pt;),除非使用了 typedef

共用体 (union)

共用体允许在相同的内存位置存储不同的数据类型,但一次只能有效使用其中一种类型。

基本语法

c
union Data {
    int i;
    float f;
    char str[20];
};

使用示例

c
#include <stdio.h>
#include <string.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.i: 10

    data.f = 220.5f;
    printf("data.f: %f\n", data.f); // 输出:data.f: 220.500000
    // 此时访问 data.i 的值是未定义的 (取决于内存表示)
    // printf("data.i after float assignment: %d\n", data.i);

    strcpy(data.str, "C Programming");
    printf("data.str: %s\n", data.str); // 输出:data.str: C Programming
    // 此时访问 data.i 或 data.f 的值是未定义的

    printf("Size of union Data: %zu\n", sizeof(union Data)); // 输出:Size of union Data: 20 (或更大,取决于对齐)

    return 0;
}

别名 (typedef)

typedef 用于为已有的数据类型创建一个新的名称(别名),以提高代码的可读性和简化复杂类型声明。

基本语法

c
typedef existing_type new_type_name;

使用示例

c
#include <stdio.h>
#include <stdlib.h> // for malloc

// 为基本类型创建别名
typedef unsigned long ulong;
typedef const char* CString;

// 为结构体创建别名 (常用方式)
struct Point {
    int x, y;
};
typedef struct Point Vec2D;
// 或者合并定义和 typedef
typedef struct Vector {
    double x, y, z;
} Vector3D;

// 为数组类型创建别名
typedef int IntArray[10];

// 为函数指针创建别名
typedef void (*FuncPtr)(int);

void printNumber(int n) {
    printf("Number: %d\n", n);
}

int main() {
    ulong largeNum = 1234567890UL;
    CString message = "Hello Typedef";
    Vec2D v = {10, 20}; // 使用别名声明,无需 struct 关键字
    Vector3D vec3 = {1.0, 2.0, 3.0};
    IntArray numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
    FuncPtr myFunc = printNumber;

    printf("ulong: %lu\n", largeNum);
    printf("CString: %s\n", message);
    printf("Vec2D: (%d, %d)\n", v.x, v.y);
    printf("Vector3D: (%f, %f, %f)\n", vec3.x, vec3.y, vec3.z);
    printf("IntArray element: %d\n", numbers[2]);
    myFunc(42); // 调用函数指针

    return 0;
}

与 C++ 的区别: C++11 引入了 using 关键字作为 typedef 的现代替代方案,语法更清晰,并且能更好地处理模板别名。C 语言没有 using 别名声明。

cpp
// C++ using example
using ulong_alias = unsigned long;
template<typename T>
using Matrix = std::vector<std::vector<T>>; // C typedef 无法直接定义模板别名

预处理器Preprocessor

C语言中的预处理器(Preprocessor)是编译器的一个重要组成部分,它在实际编译之前对源代码进行处理。预处理器的主要作用是对源代码进行宏替换、文件包含、条件编译等操作。

预处理器指令以 # 开头,常见的预处理指令包括: (1) 宏定义 (#define) (2) 文件包含 (#include) (3) 条件编译 (#if, #ifdef, #ifndef, #elif, #else, #endif) (4) 其他指令 (如 #error, #warning, #pragma)

宏定义 (#define)

宏定义用于创建符号常量或简单的代码片段替换。

对象式宏

c
#include <stdio.h>

#define PI 3.14159
#define BUFFER_SIZE 1024

int main() {
    double radius = 5.0;
    double area = PI * radius * radius;
    char buffer[BUFFER_SIZE];
    printf("Area: %f\n", area);
    printf("Buffer size: %d\n", BUFFER_SIZE);
    return 0;
}

函数式宏

c
#include <stdio.h>

#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))

int main() {
    int max_val = MAX(10, 20); // 展开为 ((10) > (20) ? (10) : (20))
    int sq_val = SQUARE(5 + 2); // 展开为 ((5 + 2) * (5 + 2))
    printf("Max: %d\n", max_val); // 输出:Max: 20
    printf("Square: %d\n", sq_val); // 输出:Square: 49
    return 0;
}

与 C++ 的区别: C++ 提供了更好的替代方案:

  • 常量: 使用 constconstexpr 定义常量,它们具有类型安全,作用域规则更清晰。
  • 函数: 使用内联函数 (inline) 或普通函数替代函数式宏,它们类型安全,行为更可预测,调试更方便。

文件包含 (#include)

#include 指令用于将其他文件的内容插入到当前文件中,通常用于包含头文件。

预处理器通常首先在当前源文件所在的目录查找,如果找不到,再按查找系统头文件的方式查找。


两种形式

  • #include <filename.h>:用于包含标准库头文件或系统提供的头文件。预处理器通常在系统指定的包含路径中查找。
    c
    #include <stdio.h>   // 标准输入输出库
    #include <stdlib.h>  // 标准库函数 (malloc, exit等)
    #include <string.h>  // 字符串处理函数
    #include <math.h>    // 数学函数
  • #include "filename.h":用于包含用户自定义的头文件。
    c
    #include "myheader.h" // 包含同目录下的自定义头文件
    #include "utils/helpers.h" // 包含相对路径下的头文件

头文件守卫 (Header Guards):为了防止同一个头文件被多次包含导致重定义错误,必须使用头文件守卫。

c
// myheader.h
#ifndef MYHEADER_H // 如果 MYHEADER_H 未定义
#define MYHEADER_H // 则定义 MYHEADER_H

// --- 头文件内容开始 ---
struct MyStruct { 
    int data;
};

void myFunction(int x);

// --- 头文件内容结束 ---

#endif // MYHEADER_H

#pragma once:许多现代 C/C++ 编译器支持 #pragma once 指令,它具有与头文件守卫相同的功能,但语法更简洁,且可能编译更快。但它不是 C/C++ 标准的一部分,可移植性略差于传统头文件守卫。

c
// myheader.h
#pragma once

// 头文件内容
struct MyStruct { 
    int data;
};
void myFunction(int x);

条件编译

条件编译指令允许根据预处理时定义的条件,选择性地编译代码块。

常用指令

  • #ifdef MACRO_NAME:如果 MACRO_NAME 已通过 #define 定义,则编译后续代码。
  • #ifndef MACRO_NAME:如果 MACRO_NAME 未被定义,则编译后续代码。
  • #if constant_expression:如果 constant_expression 求值为非零(真),则编译后续代码。表达式必须在预处理时就能求值。
  • #elif constant_expressionelse if 的形式,在前一个 #if#elif 条件为假时检查此条件。
  • #else:如果前面的 #if/#ifdef/#ifndef/#elif 条件都为假,则编译此部分代码。
  • #endif:标记条件编译块的结束。

使用场景

  • 平台特定代码:根据不同的操作系统或编译器编译不同的代码。
    c
    #include <stdio.h>
    
    #ifdef _WIN32
        // Windows 特定代码
        #include <windows.h>
        const char* os_name = "Windows";
    #elif defined(__linux__)
        // Linux 特定代码
        #include <unistd.h>
        const char* os_name = "Linux";
    #elif defined(__APPLE__)
        // macOS 特定代码
        const char* os_name = "macOS";
    #else
        // 其他平台代码
        const char* os_name = "Unknown OS";
    #endif
    
    int main() {
        printf("Running on: %s\n", os_name);
        return 0;
    }
  • 调试代码:只在调试模式下编译某些代码。
    c
    #include <stdio.h>
    
    // 在编译时通过 -DDEBUG 或在代码中定义
    // #define DEBUG 1 
    
    int main() {
        int x = 10;
        #ifdef DEBUG
            fprintf(stderr, "Debug: Variable x = %d at line %d\n", x, __LINE__);
        #endif
        printf("Program continues...\n");
        return 0;
    }
  • 功能开关:根据宏定义启用或禁用某些功能。
    c
    #include <stdio.h>
    
    #define ENABLE_FEATURE_X 1 // 设为 0 或注释掉则禁用
    
    #if ENABLE_FEATURE_X
        void featureX() { printf("Feature X is enabled.\n"); }
    #else
        void featureX_disabled() { printf("Feature X is disabled.\n"); }
    #endif
    
    int main() {
        #if ENABLE_FEATURE_X
            featureX();
        #else
            featureX_disabled();
        #endif
        return 0;
    }

defined 运算符:可以在 #if#elif 中使用 defined(MACRO_NAME)defined MACRO_NAME 来检查宏是否已定义,这比 #ifdef 更灵活,允许组合条件。

c
#define OPTION_A
// #define OPTION_B 10

#if defined(OPTION_A) && (!defined(OPTION_B) || OPTION_B < 5)
    printf("Condition met.\n");
#else
    printf("Condition not met.\n");
#endif

文件I/O及相关函数

在C语言中,所有文件操作都是基于流(stream)的概念进行的。流是一个抽象的概念,代表一个数据序列,无论是从文件读取还是向文件写入。每个流都与一个FILE*类型的指针相关联,这个指针指向内部的文件控制块,包含了文件的状态和位置等信息。

C语言提供了一套完整的文件操作函数,用于文件的创建、打开、读写、定位和关闭。这些函数定义在标准库头文件 <stdio.h> 中。

文件打开与关闭

  • fopen 函数用于打开文件,接受两个参数:文件名打开模式

  • fclose 当不再需要访问文件时,应该关闭文件以释放资源, 接受一个参数:文件指针

c
FILE *fopen(const char *filename, const char *mode);
int fclose(FILE *stream);
模式 描述
"r" 只读模式。打开一个已存在的文本文件用于读取数据,文件指针位于文件开头。如果文件不存在,则函数返回 NULL 并设置错误标志。适用于只读操作。
"w" 写模式。创建一个新的文本文件或覆盖一个已存在的文件进行写入操作,文件指针位于文件开头。如果文件存在,其内容会被清空。如果文件不存在,则创建新文件。
"a" 追加模式。打开一个文本文件用于在文件末尾追加数据,文件指针位于文件末尾。如果文件不存在,则创建新文件。尝试在其他位置写入会导致数据追加到文件末尾。
"r+" 读写模式。打开一个已存在的文本文件用于读取和写入数据,文件指针位于文件开头。如果文件不存在,则函数返回 NULL 并设置错误标志。适合需要同时读取和修改文件的应用。
"w+" 读写模式(覆盖)。创建一个新的文本文件或覆盖一个已存在的文件进行读取和写入操作,文件指针位于文件开头。如果文件存在,其内容会被清空。如果文件不存在,则创建新文件。
"a+" 读写模式(追加)。打开一个文本文件用于在文件末尾追加数据并允许读取,文件指针位于文件末尾。即使使用 fseek() 移动文件指针,所有写操作仍然会在文件末尾进行。如果文件不存在,则创建新文件。
c
#include <stdio.h>

int main() {
    FILE *fp = fopen("example.txt", "w");
    if (fp == NULL) {
        printf("Failed to open file!\n");
        return 1;
    }
    
    // 文件操作...
    
    fclose(fp); // 关闭文件
    return 0;
}

文本文件读写

C语言提供了多种文件读写函数,适用于不同场景:

1. 单个字符读写 (fgetc/fputc)

  • fgetc(FILE *stream) : 从文件中读取一个字符

  • fputc(int c, FILE *stream): 向文件中写入一个字符

c
#include <stdio.h>

int main() {
    FILE *fp = fopen("example.txt", "w+");
    if (fp == NULL) return 1;
    
    // 写入单个字符
    fputc('A', fp);
    
    // 重置文件指针到开头
    rewind(fp);
    
    // 读取单个字符
    int ch = fgetc(fp);
    printf("Read character: %c\n", ch);
    
    fclose(fp);
    return 0;
}

2. 行读写 (fgets/fputs)

  • fgets(char *str, int n, FILE *stream) 用于从文件中读取一行文本,最多读取n-1个字符。

  • fputs(const char *str, FILE *stream) 用于向文件写入一个字符串,不自动添加换行符。

c
#include <stdio.h>

int main() {
    FILE *fp = fopen("example.txt", "w+");
    if (fp == NULL) return 1;
    
    // 写入一行文本
    fputs("Hello, World!\n", fp);
    
    rewind(fp);
    
    // 读取一行(最多读取99个字符)
    char buffer[100];
    fgets(buffer, sizeof(buffer), fp);
    printf("Read line: %s", buffer);
    
    fclose(fp);
    return 0;
}

3. 格式化读写 (fscanf/fprintf)

c
#include <stdio.h>

int main() {
    FILE *fp = fopen("data.txt", "w+");
    if (fp == NULL) return 1;
    
    // 格式化写入
    fprintf(fp, "%d %f %s\n", 42, 3.14, "Pi");
    
    rewind(fp);
    
    // 格式化读取
    int i;
    float f;
    char str[20];
    fscanf(fp, "%d %f %s", &i, &f, str);
    printf("Read: %d, %f, %s\n", i, f, str);
    
    fclose(fp);
    return 0;
}

二进制文件读写

二进制文件读写 (fread/fwrite):

  • fread(void *ptr, size_t size, size_t nmemb, FILE *stream):从文件中读取数据到内存块中。

  • fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream):将内存块中的数据写入文件。

参数名 含义
ptr 指向内存块的指针:
- 对于 fread,数据从文件读取到该内存块中。
- 对于 fwrite,数据从该内存块写入文件。
size 每个元素的大小(以字节为单位)。
nmemb 要读取或写入的元素数量。
stream 文件流指针,指向一个已打开的文件(通过 fopen 打开)。
c
#include <stdio.h>

struct Data {
    int id;
    double value;
};

int main() {
    FILE *fp = fopen("data.bin", "wb+");
    if (fp == NULL) return 1;
    
    struct Data data = {1, 3.14};
    
    // 写入二进制数据
    fwrite(&data, sizeof(struct Data), 1, fp);
    
    rewind(fp);
    
    // 读取二进制数据
    struct Data read_data;
    fread(&read_data, sizeof(struct Data), 1, fp);
    printf("Read: id=%d, value=%f\n", read_data.id, read_data.value);
    
    fclose(fp);
    return 0;
}

文件定位函数

文件位置指针指示文件中当前读写位置,可以通过相关函数进行控制和查询

  • ftell(FILE *stream):返回当前文件位置
  • fseek(FILE *stream, long offset, int whence):设置文件位置
  • rewind(FILE *stream):重置文件位置到开头
c
#include <stdio.h>

int main() {
    FILE *fp = fopen("example.txt", "w+");
    if (fp == NULL) return 1;
    
    fputs("This is a test file.", fp);
    
    // 获取文件大小
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    printf("File size: %ld bytes\n", size);
    
    // 跳转到第5个字节
    fseek(fp, 5, SEEK_SET);
    
    // 读取从第5个字节开始的内容
    char buffer[100];
    fgets(buffer, sizeof(buffer), fp);
    printf("Content from position 5: %s\n", buffer);
    
    fclose(fp);
    return 0;
}

错误处理 (feof/ferror)

  • feof(FILE *stream):检测是否到达文件末尾。

  • ferror(FILE *stream): 检测是否存在文件错误。

c
#include <stdio.h>

int main() {
    FILE *fp = fopen("nonexistent.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }
    
    // 读取文件内容
    char ch;
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }
    
    // 检查是否到达文件末尾
    if (feof(fp)) {
        printf("\nReached end of file\n");
    }
    
    // 检查是否发生错误
    if (ferror(fp)) {
        printf("Error reading file\n");
    }
    
    fclose(fp);
    return 0;
}