Linux 环境下 C 程序设计 —— 黄继海 石彦华

30 阅读1小时+

目录

IMG_6491.HEIC

IMG_5646.HEIC

IMG_5647.HEIC

IMG_5648.HEIC

第1章 Linux操作系统基础 …… 1

1.1 GNU简介 …… 2

GNU是GNU's Not Unix的递归缩写,是一个由Richard Stallman发起的自由软件项目,旨在创建一个完全自由的类Unix操作系统。GNU项目的目标是开发一套完整的自由软件系统,包括编译器、编辑器、操作系统内核等。

1.1.1 GNU的历史

  • 1983年,Richard Stallman发起GNU项目
  • 1985年,成立自由软件基金会(FSF)
  • 1989年,发布GNU通用公共许可证(GPL)

1.1.2 GNU的核心组件

  • GCC:GNU编译器集合,支持多种编程语言
  • GNU Emacs:功能强大的文本编辑器
  • GNU Bash:命令行解释器
  • GNU Coreutils:核心工具集,包括ls、cp、mv等命令

1.2 Linux简介 …… 2

Linux是一种自由和开放源码的类Unix操作系统,由Linus Torvalds于1991年创建。Linux内核是Linux操作系统的核心,负责管理硬件资源和提供系统服务。

1.2.1 Linux的历史

  • 1991年,Linus Torvalds发布Linux 0.01版本
  • 1993年,Linux 1.0版本发布
  • 1994年,Linux内核采用GPL许可证
  • 2001年,Linux 2.4内核发布
  • 2003年,Linux 2.6内核发布
  • 2011年,Linux 3.0内核发布
  • 2015年,Linux 4.0内核发布
  • 2020年,Linux 5.0内核发布

1.2.2 Linux的特点

  • 自由开源:遵循GPL许可证,允许自由修改和分发
  • 多用户:支持多个用户同时使用系统
  • 多任务:支持多个程序同时运行
  • 稳定性:系统稳定可靠,适合长时间运行
  • 安全性:内置多种安全机制
  • 可移植性:支持多种硬件平台

1.2.3 Linux的发行版

  • Ubuntu:基于Debian,用户友好
  • CentOS:基于Red Hat,适合服务器
  • Fedora:由Red Hat赞助,更新频繁
  • Debian:以稳定性著称
  • Arch Linux:滚动更新,适合高级用户
  • openSUSE:由Novell赞助,企业级支持

1.3 Shell命令概述 …… 4

Shell是用户与Linux内核之间的接口,是一种命令解释器,负责执行用户输入的命令。在Linux系统中,常用的Shell包括Bash、Zsh、Fish等。

1.3.1 Shell的基本概念

  • 命令:用户输入的指令,如ls、cd等
  • 选项:修改命令行为的参数,通常以-开头,如-l、-a等
  • 参数:命令的操作对象,如文件名、目录名等
  • 管道:将一个命令的输出作为另一个命令的输入,使用|符号
  • 重定向:改变命令的输入/输出方向,使用<、>、>>等符号

1.3.2 Shell的基本操作

  • 命令补全:按下Tab键自动补全命令或文件名
  • 命令历史:使用上下箭头键查看历史命令
  • 命令别名:使用alias命令创建命令别名
  • 后台执行:在命令末尾添加&符号,使命令在后台执行

1.3.3 环境变量

  • 查看环境变量echo $变量名
  • 设置环境变量export 变量名=值
  • 常用环境变量
    • PATH:命令搜索路径
    • HOME:用户主目录
    • SHELL:当前使用的Shell
    • USER:当前用户名
    • PWD:当前工作目录

1.4 文件与目录操作 …… 7

文件和目录是Linux系统中最基本的存储单位,掌握文件和目录操作命令是使用Linux的基础。

1.4.1 目录操作命令

  • pwd(Print Working Directory):显示当前工作目录

    pwd
    
  • cd(Change Directory):切换目录

    cd /path/to/directory  # 切换到指定目录
    cd ..                # 切换到上级目录
    cd ~                # 切换到用户主目录
    cd -                # 切换到上一个工作目录
    
  • mkdir:创建目录

    mkdir directory      # 创建单个目录
    mkdir -p dir1/dir2   # 创建嵌套目录
    
  • rmdir:删除空目录

    rmdir directory      # 删除空目录
    

1.4.2 文件操作命令

  • ls(list):列出文件和目录

    ls                  # 列出当前目录中的文件和目录
    ls -l               # 详细列出,包括权限、大小、修改时间等
    ls -a               # 显示所有文件,包括隐藏文件
    ls -la              # 详细显示所有文件
    
  • cp:复制文件或目录

    cp source destination  # 复制文件
    cp -r source_dir destination_dir  # 复制目录
    
  • mv:移动或重命名文件或目录

    mv old_name new_name  # 重命名文件或目录
    mv file directory     # 移动文件到目录
    
  • rm:删除文件或目录

    rm file              # 删除文件
    rm -r directory      # 删除目录及其内容
    rm -f file           # 强制删除文件
    rm -rf directory     # 强制删除目录及其内容
    
  • touch:创建空文件或更新文件时间戳

    touch file           # 创建空文件
    
  • cat(catenate):查看文件内容

    cat file             # 查看文件内容
    cat file1 file2 > file3  # 合并文件内容
    
  • more:分页查看文件内容

    more file            # 分页查看文件内容
    
  • less:交互式分页查看文件内容

    less file            # 交互式分页查看文件内容
    
  • head:查看文件开头部分

    head file            # 查看文件前10行
    head -n 5 file       # 查看文件前5行
    
  • tail:查看文件结尾部分

    tail file            # 查看文件后10行
    tail -n 5 file       # 查看文件后5行
    tail -f file         # 实时查看文件内容
    

1.4.3 文件权限

  • chmod:修改文件权限

    chmod 755 file       # 设置文件权限为rwxr-xr-x
    chmod u+x file       # 给文件所有者添加执行权限
    chmod g-w file       # 移除文件所属组的写权限
    
  • chown:修改文件所有者

    chown user file      # 修改文件所有者为user
    chown user:group file  # 修改文件所有者和所属组
    
  • chgrp:修改文件所属组

    chgrp group file     # 修改文件所属组为group
    

1.5 系统运行常用命令 …… 19

系统运行命令用于管理Linux系统的运行状态,包括进程管理、系统信息查看等。

1.5.1 进程管理命令

  • ps(Process Status):查看进程状态

    ps                  # 查看当前终端的进程
    ps aux              # 查看所有进程
    ps -ef              # 查看所有进程,显示父进程ID
    
  • top(Table Of Processes):实时查看进程状态

    top                 # 实时查看进程状态
    
  • kill:终止进程

    kill PID            # 终止指定PID的进程
    kill -9 PID         # 强制终止指定PID的进程
    
  • pkill:根据进程名终止进程

    pkill process_name   # 终止指定名称的进程
    
  • **pgrep(process + grep

• grep 全称:Global Regular Expression Print)**:根据进程名查找进程 bash pgrep process_name # 查找指定名称的进程

1.5.2 系统信息命令

  • uname:查看系统信息

    uname -a            # 查看所有系统信息
    uname -r            # 查看内核版本
    
  • hostname:查看或设置主机名

    hostname            # 查看主机名
    hostname new_name    # 设置主机名
    
  • uptime():查看系统运行时间

    uptime              # 查看系统运行时间和负载
    
  • free:查看内存使用情况

    free                # 查看内存使用情况
    free -h             # 以人类可读的格式查看内存使用情况
    
  • df(Disk Free):查看磁盘使用情况

    df                  # 查看磁盘使用情况
    df -h               # 以人类可读的格式查看磁盘使用情况
    
  • du(Disk Usage):查看目录或文件大小

    du                  # 查看当前目录下各文件和目录的大小
    du -h               # 以人类可读的格式查看
    du -s               # 查看总大小
    

1.5.3 系统服务管理

  • systemctl(system + control):管理系统服务(systemd系统)

    systemctl status service_name  # 查看服务状态
    systemctl start service_name   # 启动服务
    systemctl stop service_name    # 停止服务
    systemctl restart service_name # 重启服务
    systemctl enable service_name  # 设置服务开机自启
    systemctl disable service_name # 禁止服务开机自启
    
  • service:管理系统服务(SysV系统)

    service service_name status    # 查看服务状态
    service service_name start     # 启动服务
    service service_name stop      # 停止服务
    service service_name restart   # 重启服务
    

1.6 查找操作命令 …… 22

查找命令用于在系统中查找文件、目录或内容。

1.6.1 文件查找命令

  • find:根据条件查找文件

    find /path -name "*.txt"    # 查找指定路径下所有.txt文件
    find /path -type f          # 查找指定路径下所有普通文件
    find /path -type d          # 查找指定路径下所有目录
    find /path -size +1M        # 查找指定路径下大小大于1MB的文件
    find /path -mtime -7        # 查找指定路径下7天内修改的文件
    
  • locate:快速查找文件(基于数据库)

    locate file_name         # 查找文件
    updatedb               # 更新数据库
    

1.6.2 内容查找命令

  • grep:在文件中查找指定内容

    grep "pattern" file      # 在文件中查找指定模式
    grep -r "pattern" /path  # 递归查找指定路径下的文件
    grep -i "pattern" file    # 忽略大小写查找
    grep -n "pattern" file    # 显示行号
    
  • egrep(Extended Global Regular Expression Print):使用扩展正则表达式查找

    egrep "pattern1|pattern2" file  # 查找多个模式
    
  • fgrep(Fixed Global Regular Expression Print):固定字符串查找,不解析正则表达式

    fgrep "pattern" file      # 固定字符串查找
    

1.7 其他常用命令 …… 23

1.7.1 网络命令

  • ifconfig(Interface Configuration):查看网络接口信息

    ifconfig             # 查看网络接口信息
    
  • ip:查看和配置网络接口

    ip addr             # 查看网络接口地址
    ip route            # 查看路由表
    
  • ping:测试网络连接

    ping hostname       # 测试与主机的连接
    
  • netstat(Network Statistics):查看网络状态

    netstat -tuln       # 查看所有监听端口
    
  • ss(Socket Statistics):查看网络状态(替代netstat)

    ss -tuln            # 查看所有监听端口
    
  • curl(Client URL):发送HTTP请求

    curl http://example.com  # 发送GET请求
    curl -X POST -d "data" http://example.com  # 发送POST请求
    
  • wget(World Wide Web Get):下载文件

    wget http://example.com/file  # 下载文件
    

1.7.2 压缩和解压缩命令

  • tar:打包和压缩文件

    tar -cvf archive.tar files   # 打包文件
    tar -xvf archive.tar         # 解包文件
    tar -czvf archive.tar.gz files  # 打包并压缩为gzip格式
    tar -xzvf archive.tar.gz     # 解压gzip格式
    tar -cjvf archive.tar.bz2 files  # 打包并压缩为bzip2格式
    tar -xjvf archive.tar.bz2     # 解压bzip2格式
    
  • gzip:压缩文件

    gzip file            # 压缩文件
    gunzip file.gz       # 解压文件
    
  • zip:压缩文件

    zip archive.zip files  # 压缩文件
    unzip archive.zip      # 解压文件
    

1.7.3 文本处理命令

  • sed:流编辑器

    sed 's/old/new/g' file  # 替换文件中的文本
    sed -i 's/old/new/g' file  # 直接修改文件
    
  • awk:文本处理工具

    awk '{print $1}' file   # 打印文件的第一列
    awk '$3 > 100 {print $0}' file  # 打印第三列大于100的行
    
  • cut:剪切文件内容

    cut -d ':' -f 1 /etc/passwd  # 以:为分隔符,提取第一列
    
  • sort:排序文件内容

    sort file            # 排序文件
    sort -n file         # 按数字排序
    sort -r file         # 反向排序
    
  • uniq:去重

    uniq file            # 去除连续重复行
    sort file | uniq     # 去除所有重复行
    
  • wc:统计文件内容

    wc -l file           # 统计行数
    wc -w file           # 统计单词数
    wc -c file           # 统计字节数
    

1.8 Linux应用软件包管理 …… 37

Linux系统使用软件包管理器来管理软件的安装、更新和卸载。不同的Linux发行版使用不同的软件包管理器。

1.8.1 Debian/Ubuntu系列(apt)

  • apt-get:管理软件包

    sudo apt-get update     # 更新软件包列表
    sudo apt-get upgrade    # 升级已安装的软件包
    sudo apt-get install package_name  # 安装软件包
    sudo apt-get remove package_name   # 卸载软件包
    sudo apt-get purge package_name    # 卸载软件包并删除配置文件
    sudo apt-get autoremove            # 自动删除不再需要的依赖包
    
  • apt:apt-get的改进版本

    sudo apt update         # 更新软件包列表
    sudo apt upgrade        # 升级已安装的软件包
    sudo apt install package_name  # 安装软件包
    sudo apt remove package_name   # 卸载软件包
    sudo apt purge package_name    # 卸载软件包并删除配置文件
    sudo apt autoremove            # 自动删除不再需要的依赖包
    

1.8.2 Red Hat/CentOS系列(yum/dnf)

  • yum:管理软件包(CentOS 7及以下)

    sudo yum update         # 更新软件包
    sudo yum install package_name  # 安装软件包
    sudo yum remove package_name   # 卸载软件包
    sudo yum search package_name   # 搜索软件包
    
  • dnf:管理软件包(CentOS 8及以上)

    sudo dnf update         # 更新软件包
    sudo dnf install package_name  # 安装软件包
    sudo dnf remove package_name   # 卸载软件包
    sudo dnf search package_name   # 搜索软件包
    

1.8.3 Arch Linux系列(pacman)

  • pacman:管理软件包
    sudo pacman -Sy         # 更新软件包数据库
    sudo pacman -Syu        # 更新系统
    sudo pacman -S package_name  # 安装软件包
    sudo pacman -R package_name  # 卸载软件包
    sudo pacman -Rs package_name # 卸载软件包及其依赖
    

1.8.4 源码安装

有些软件可能没有提供预编译的软件包,需要从源码编译安装。

# 1. 下载源码包
wget http://example.com/software.tar.gz

# 2. 解压源码包
tar -xzvf software.tar.gz

# 3. 进入源码目录
cd software

# 4. 配置
./configure

# 5. 编译
make

# 6. 安装
sudo make install

1.9 项目实训:Linux基本命令 …… 39

1.9.1 实训目标

  • 掌握Linux基本命令的使用
  • 熟悉文件和目录操作
  • 了解系统运行管理
  • 学会使用软件包管理器

1.9.2 实训内容

  1. 文件和目录操作

    • 创建目录结构:mkdir -p project/src project/include project/lib
    • 创建文件:touch project/src/main.c project/include/header.h
    • 复制文件:cp project/src/main.c project/src/main_backup.c
    • 移动文件:mv project/src/main_backup.c project/
    • 删除文件:rm project/main_backup.c
  2. 文件内容操作

    • 查看文件内容:cat project/src/main.c
    • 编辑文件:使用vim或nano编辑文件
    • 搜索内容:grep "main" project/src/main.c
  3. 系统信息查看

    • 查看系统版本:uname -a
    • 查看内存使用情况:free -h
    • 查看磁盘使用情况:df -h
    • 查看进程状态:ps aux | head -10
  4. 软件包管理

    • 更新软件包列表:sudo apt update(Ubuntu/Debian)或 sudo yum update(CentOS)
    • 安装软件包:sudo apt install tree(Ubuntu/Debian)或 sudo yum install tree(CentOS)
    • 查看已安装的软件包:dpkg -l(Ubuntu/Debian)或 rpm -qa(CentOS)

1.9.3 实训要求

  • 完成所有实训内容
  • 记录每个命令的执行结果
  • 撰写实训报告,总结Linux基本命令的使用方法

1.10 本章小结 …… 41

本章介绍了Linux操作系统的基础知识,包括:

  • GNU简介:GNU项目的历史和核心组件
  • Linux简介:Linux的历史、特点和发行版
  • Shell命令概述:Shell的基本概念和操作
  • 文件与目录操作:目录操作、文件操作和文件权限
  • 系统运行常用命令:进程管理、系统信息和系统服务管理
  • 查找操作命令:文件查找和内容查找
  • 其他常用命令:网络命令、压缩和解压缩命令、文本处理命令
  • Linux应用软件包管理:不同发行版的软件包管理器
  • 项目实训:Linux基本命令的使用

通过本章的学习,读者应该能够掌握Linux的基本操作,为后续的编程学习打下基础。

第2章 常用的C语言库函数 …… 44

2.1 字符和字符串操作函数 …… 45

2.1.1 字符操作函数(<ctype.h>

这类函数主要用于判断字符类型(如字母、数字、空格)或转换字符大小写,参数通常是int类型(实际传入char,会被提升为int)。

函数原型功能说明示例
int isalpha(int c)判断字符是否为字母(a-z/A-Z),是则返回非 0isalpha('A') → 非0
int isdigit(int c)判断字符是否为数字(0-9),是则返回非 0isdigit('5') → 非0
int isalnum(int c)判断字符是否为字母或数字,是则返回非 0isalnum('8') → 非0
int isspace(int c)判断字符是否为空白符(空格、换行、制表符)isspace('\n') → 非0
int isupper(int c)判断字符是否为大写字母,是则返回非 0isupper('B') → 非0
int islower(int c)判断字符是否为小写字母,是则返回非 0islower('b') → 非0
int toupper(int c)将小写字母转为大写,非小写字母返回原值toupper('a') → 'A'
int tolower(int c)将大写字母转为小写,非大写字母返回原值tolower('A') → 'a'

示例代码

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

int main() {
    char ch = '7';
    if (isdigit(ch)) {
        printf("%c 是数字\n", ch); // 输出:7 是数字
    }
    
    ch = 'm';
    printf("转换为大写:%c\n", toupper(ch)); // 输出:转换为大写:M
    return 0;
}

2.1.2 字符串操作函数(<string.h>

这类函数是字符串处理的核心,涵盖长度计算、拷贝、拼接、比较、查找等功能,使用时务必注意字符串以 \0 结尾,避免越界。

1. 基础操作(长度、拷贝、拼接)
函数原型功能说明
size_t strlen(const char *s)计算字符串长度(不包含\0
char *strcpy(char *dest, const char *src)src字符串拷贝到dest(包含\0),注意 dest 空间足够
char *strncpy(char *dest, const char *src, size_t n)拷贝src的前n个字符到dest,更安全(避免越界)
char *strcat(char *dest, const char *src)src拼接到dest末尾(覆盖dest\0),注意 dest 空间足够
char *strncat(char *dest, const char *src, size_t n)拼接src的前n个字符到dest末尾,更安全

示例代码

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

int main() {
    char str1[20] = "Hello";
    char str2[] = " World";
    
    // 字符串长度
    printf("str1长度:%zu\n", strlen(str1)); // 输出:5(不含\0)
    
    // 字符串拼接
    strcat(str1, str2);
    printf("拼接后:%s\n", str1); // 输出:Hello World
    
    // 字符串拷贝(注意str3空间足够)
    char str3[20];
    strcpy(str3, str1);
    printf("拷贝后:%s\n", str3); // 输出:Hello World
    return 0;
}
2. 字符串比较
函数原型功能说明
int strcmp(const char *s1, const char *s2)比较s1s2的 ASCII 值:- 相等返回 0- s1<s2 返回负数- s1>s2 返回正数
int strncmp(const char *s1, const char *s2, size_t n)仅比较前n个字符,规则同strcmp

示例代码

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

int main() {
    char *s1 = "apple";
    char *s2 = "banana";
    char *s3 = "apple";
    
    int res1 = strcmp(s1, s2); // s1 < s2,返回负数
    int res2 = strcmp(s1, s3); // 相等,返回0
    
    printf("s1 vs s2:%d\n", res1); // 输出:-1(具体值依编译器)
    printf("s1 vs s3:%d\n", res2); // 输出:0
    return 0;
}
3. 字符串查找
函数原型功能说明
char *strchr(const char *s, int c)s中查找字符c第一次出现的位置,返回指向该位置的指针,找不到返回NULL
char *strrchr(const char *s, int c)s中查找字符c最后一次出现的位置,返回指针,找不到返回NULL
char *strstr(const char *haystack, const char *needle)haystack中查找子串needle第一次出现的位置,找不到返回NULL

示例代码

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

int main() {
    char *str = "hello world";
    
    // 查找字符 'o' 第一次出现的位置
    char *pos1 = strchr(str, 'o');
    if (pos1) {
        printf("'o'第一次出现的位置:%ld\n", pos1 - str); // 输出:4
    }
    
    // 查找子串 "world"
    char *pos2 = strstr(str, "world");
    if (pos2) {
        printf("子串位置:%ld,子串内容:%s\n", pos2 - str, pos2);
        // 输出:子串位置:6,子串内容:world
    }
    return 0;
}

2.1.3 注意事项

  1. 使用<string.h>的函数时,目标数组必须有足够的空间,否则会导致内存越界(缓冲区溢出),推荐使用strncpy/strncat替代strcpy/strcat
  2. 字符串操作函数的参数如果是字符串常量(如"abc"),不能修改(如strcpy("abc", "def")会崩溃)。
  3. strlen返回size_t类型(无符号整数),避免用它做减法(如strlen(s1) - strlen(s2)可能得到非预期值)。

总结

  1. 字符操作函数(<ctype.h>):核心是判断字符类型isalpha/isdigit)和转换大小写toupper/tolower)。
  2. 字符串操作函数(<string.h>):核心包括长度计算strlen)、拷贝 / 拼接strcpy/strcat,优先用strncpy/strncat)、比较strcmp)、查找strchr/strstr)。
  3. 使用字符串函数时,务必保证目标缓冲区空间足够,避免内存越界问题。

2.2 内存管理函数 …… 55

2.2.1 内存分配函数

  • stdlib.h头文件中的函数:
    • malloc(size_t size):分配指定大小的内存
    • calloc(size_t nmemb, size_t size):分配指定数量和大小的内存,并初始化为0
    • realloc(void *ptr, size_t size):重新分配内存
    • free(void *ptr):释放内存

2.2.2 内存分配注意事项

  • 内存分配失败时,函数返回NULL
  • 分配的内存使用完毕后必须释放,否则会导致内存泄漏
  • 不要释放已经释放的内存,否则会导致错误
  • 不要在释放内存后继续使用该内存,否则会导致悬空指针

2.2.3 内存分配示例

// 分配一个整型数组
int *arr = (int *)malloc(10 * sizeof(int));
if (arr == NULL) {
    printf("内存分配失败\n");
    return 1;
}

// 使用数组
for (int i = 0; i < 10; i++) {
    arr[i] = i;
}

// 重新分配内存
arr = (int *)realloc(arr, 20 * sizeof(int));
if (arr == NULL) {
    printf("内存分配失败\n");
    return 1;
}

// 释放内存
free(arr);
arr = NULL; // 避免悬空指针

2.3 日期与时间函数 …… 63

2.3.1 时间类型

  • time_t:时间戳类型,通常是一个整数,表示从1970年1月1日00:00:00 UTC到当前时间的秒数
  • struct tm:分解时间结构,包含年、月、日、时、分、秒等信息

2.3.2 时间函数

  • time.h头文件中的函数:
    • time(time_t *t):获取当前时间戳
    • localtime(const time_t *timep):将时间戳转换为本地时间结构
    • gmtime(const time_t *timep):将时间戳转换为UTC时间结构
    • mktime(struct tm *tm):将时间结构转换为时间戳
    • asctime(const struct tm *tm):将时间结构转换为字符串
    • ctime(const time_t *timep):将时间戳转换为字符串
    • difftime(time_t time1, time_t time0):计算两个时间戳之间的差值
    • strftime(char *s, size_t max, const char *format, const struct tm *tm):格式化时间为字符串

2.3.3 时间函数示例

// 获取当前时间戳
time_t now = time(NULL);
printf("当前时间戳:%ld\n", now);

// 转换为本地时间结构
struct tm *local_time = localtime(&now);
printf("本地时间:%s", asctime(local_time));

// 格式化时间
char buffer[100];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", local_time);
printf("格式化时间:%s\n", buffer);

2.4 随机函数 …… 65

2.4.1 随机数生成函数

  • stdlib.h头文件中的函数:
    • rand(void):生成0到RAND_MAX之间的随机整数
    • srand(unsigned int seed):设置随机数种子

2.4.2 随机数生成注意事项

  • 如果不设置种子,rand()会使用默认种子,每次运行程序生成的随机数序列相同
  • 通常使用时间作为种子,确保每次运行生成不同的随机数序列

2.4.3 随机函数示例

// 设置种子
srand(time(NULL));

// 生成0-99之间的随机数
int random_num = rand() % 100;
printf("随机数:%d\n", random_num);

// 生成指定范围的随机数
int min = 10, max = 20;
int range_random = min + rand() % (max - min + 1);
printf("10-20之间的随机数:%d\n", range_random);

2.5 项目实训:C语言常用库函数调用 …… 68

2.5.1 实训目标

  • 掌握C语言常用库函数的使用
  • 熟悉字符和字符串操作
  • 了解内存管理函数的使用
  • 掌握日期与时间函数的使用
  • 熟悉随机函数的使用

2.5.2 实训内容

  1. 字符处理

    • 编写程序,输入一个字符,判断其类型(字母、数字、空白字符等)
    • 编写程序,将字符串转换为大写或小写
  2. 字符串操作

    • 编写程序,计算字符串长度
    • 编写程序,复制和拼接字符串
    • 编写程序,比较两个字符串
    • 编写程序,查找子字符串
  3. 内存管理

    • 编写程序,使用malloc分配内存,存储一个整型数组
    • 编写程序,使用realloc调整内存大小
    • 编写程序,释放分配的内存
  4. 日期与时间

    • 编写程序,获取当前时间并显示
    • 编写程序,格式化时间为指定格式
    • 编写程序,计算两个时间之间的差值
  5. 随机数生成

    • 编写程序,生成指定范围的随机数
    • 编写程序,模拟掷骰子

2.5.3 实训要求

  • 完成所有实训内容
  • 编写完整的C程序
  • 测试程序的正确性
  • 撰写实训报告

2.6 本章小结 …… 72

本章介绍了C语言常用的库函数,包括:

  • 字符和字符串操作函数:字符处理、字符串操作、字符串转换
  • 内存管理函数:内存分配、释放和注意事项
  • 日期与时间函数:时间类型、时间函数和示例
  • 随机函数:随机数生成和种子设置
  • 项目实训:C语言常用库函数调用

通过本章的学习,读者应该能够熟练使用C语言的常用库函数,为后续的编程学习打下基础。

第3章 编程环境 …… 74

3.1 概述 …… 75

编程环境是指用于编写、编译、调试和运行程序的工具集合。在Linux环境下,常用的编程工具包括:

  • 文本编辑器:VIM、Emacs、VS Code等
  • 编译器:GCC、Clang等
  • 调试器:GDB等
  • 工程管理工具:make等

选择合适的编程环境可以提高开发效率,本章将介绍Linux下常用的编程工具。

3.2 VIM编辑器 …… 75

3.2.1 VIM简介

VIM是Vi Improved的缩写,是一款功能强大的文本编辑器,广泛应用于Linux和Unix系统中。VIM具有以下特点:

  • 模式化编辑
  • 强大的命令集
  • 可扩展性
  • 跨平台支持

3.2.2 VIM的模式

  • 普通模式:默认模式,用于导航和执行命令
  • 插入模式:用于输入文本
  • 可视模式:用于选择文本
  • 命令行模式:用于执行命令

3.2.3 VIM的基本操作

  • 进入VIMvim filename
  • 进入插入模式i(在光标前插入)、a(在光标后插入)、o(在光标下一行插入)
  • 退出插入模式Esc
  • 保存文件:w
  • 退出VIM:q
  • 保存并退出:wqZZ
  • 强制退出:q!

3.2.4 VIM的常用命令

  • 导航命令

    • h:向左移动
    • j:向下移动
    • k:向上移动
    • l:向右移动
    • w:移动到下一个单词开头
    • b:移动到上一个单词开头
    • 0:移动到行首
    • $:移动到行尾
    • gg:移动到文件开头
    • G:移动到文件结尾
  • 删除命令

    • x:删除光标所在字符
    • dd:删除当前行
    • dw:删除当前单词
    • d$:删除从光标到行尾的内容
  • 复制和粘贴命令

    • yy:复制当前行
    • yw:复制当前单词
    • p:粘贴到光标后
    • P:粘贴到光标前
  • 搜索和替换命令

    • /pattern:向下搜索pattern
    • ?pattern:向上搜索pattern
    • n:继续搜索下一个
    • N:继续搜索上一个
    • :%s/old/new/g:全局替换

3.2.5 VIM的配置

VIM的配置文件是~/.vimrc,可以在其中设置各种选项,例如:

" 设置行号
set number

" 设置自动缩进
set autoindent

" 设置Tab键宽度
set tabstop=4
set shiftwidth=4

" 设置搜索忽略大小写
set ignorecase

" 设置高亮搜索
set hlsearch

3.3 GCC编译器 …… 79

3.3.1 GCC简介

GCC(GNU Compiler Collection)是一套功能强大的编译器集合,支持多种编程语言,包括C、C++、Objective-C、Fortran、Ada等。GCC是Linux下最常用的编译器。

3.3.2 GCC的基本使用

  • 编译单个文件gcc source.c -o output
  • 编译多个文件gcc file1.c file2.c -o output
  • 指定警告级别gcc -Wall source.c -o output
  • 指定优化级别gcc -O2 source.c -o output
  • 生成调试信息gcc -g source.c -o output

3.3.3 GCC的编译过程

  1. 预处理:处理头文件和宏定义,生成.i文件
    • gcc -E source.c -o source.i
  2. 编译:将预处理后的文件编译为汇编代码,生成.s文件
    • gcc -S source.i -o source.s
  3. 汇编:将汇编代码转换为目标代码,生成.o文件
    • gcc -c source.s -o source.o
  4. 链接:将目标代码与库文件链接,生成可执行文件
    • gcc source.o -o output

3.3.4 GCC的常用选项

  • -c:只编译不链接,生成目标文件
  • -o:指定输出文件
  • -Wall:开启所有警告
  • -Werror:将警告视为错误
  • -g:生成调试信息
  • -O0-O1-O2-O3:优化级别
  • -I:指定头文件搜索路径
  • -L:指定库文件搜索路径
  • -l:指定要链接的库

3.4 GDB程序调试器 …… 84

3.4.1 GDB简介

GDB(GNU Debugger)是一款功能强大的程序调试器,可以用于调试C、C++等语言的程序。GDB可以帮助开发者定位程序中的错误。

3.4.2 GDB的基本使用

  • 启动GDBgdb program
  • 运行程序runr
  • 设置断点break filename:lineb filename:line
  • 查看断点info breakpoints
  • 删除断点delete breakpoint_numberd breakpoint_number
  • 继续执行continuec
  • 单步执行steps(进入函数)、nextn(不进入函数)
  • 查看变量print variablep variable
  • 查看内存x/[format] address
  • 查看堆栈backtracebt
  • 退出GDBquitq

3.4.3 GDB的高级功能

  • 条件断点break filename:line if condition
  • 观察点watch variable
  • 命令断点break filename:line command
  • 线程调试info threadsthread thread_number

3.5 make工程管理器 …… 87

3.5.1 make简介

make是一款工程管理工具,用于自动化编译过程。通过编写makefile文件,可以指定文件之间的依赖关系和编译规则,使编译过程更加高效。

3.5.2 makefile的基本结构

makefile由一系列规则组成,每个规则的格式如下:

target: dependencies
	commands
  • target:目标文件
  • dependencies:依赖文件
  • commands:编译命令(必须以Tab开头)

3.5.3 makefile的变量

  • 内置变量
    • $@:目标文件
    • $^:所有依赖文件
    • $<:第一个依赖文件
  • 自定义变量
    CC = gcc
    CFLAGS = -Wall -g
    

3.5.4 makefile的示例

CC = gcc
CFLAGS = -Wall -g
TARGET = program

SRCS = main.c utils.c
OBJS = $(SRCS:.c=.o)

$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) $(OBJS) -o $(TARGET)

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

clean:
	rm -f $(OBJS) $(TARGET)

3.5.5 make的常用命令

  • 执行makemake
  • 指定目标make target
  • 清理生成的文件make clean
  • 查看执行过程make -n
  • 强制重新编译make -B

3.6 项目实训:makefile的编写 …… 93

3.6.1 实训目标

  • 掌握makefile的编写方法
  • 熟悉make的基本使用
  • 了解工程管理的基本概念

3.6.2 实训内容

  1. 创建项目结构

    • 创建项目目录:mkdir -p project/src project/include project/lib
    • 创建源文件:project/src/main.cproject/src/utils.c
    • 创建头文件:project/include/utils.h
  2. 编写源代码

    • project/include/utils.h

      #ifndef UTILS_H
      #define UTILS_H
      
      int add(int a, int b);
      int subtract(int a, int b);
      
      #endif
      
    • project/src/utils.c

      #include "utils.h"
      
      int add(int a, int b) {
          return a + b;
      }
      
      int subtract(int a, int b) {
          return a - b;
      }
      
    • project/src/main.c

      #include <stdio.h>
      #include "utils.h"
      
      int main() {
          int a = 10, b = 5;
          printf("%d + %d = %d\n", a, b, add(a, b));
          printf("%d - %d = %d\n", a, b, subtract(a, b));
          return 0;
      }
      
  3. 编写makefile

    • 创建project/Makefile

      CC = gcc
      CFLAGS = -Wall -g
      INCLUDE = -Iinclude
      TARGET = bin/program
      
      SRCS = src/main.c src/utils.c
      OBJS = $(SRCS:.c=.o)
      
      all: $(TARGET)
      
      $(TARGET): $(OBJS)
        mkdir -p bin
        $(CC) $(CFLAGS) $(OBJS) -o $(TARGET)
      
      %.o: %.c
        $(CC) $(CFLAGS) $(INCLUDE) -c $< -o $@
      
      clean:
        rm -f $(OBJS) $(TARGET)
        rm -rf bin
      
  4. 编译和运行

    • 进入项目目录:cd project
    • 编译项目:make
    • 运行程序:./bin/program
    • 清理项目:make clean

3.6.3 实训要求

  • 完成所有实训内容
  • 编写完整的makefile
  • 测试程序的正确性
  • 撰写实训报告

3.7 本章小结 …… 96

本章介绍了Linux下的编程环境,包括:

  • 概述:编程环境的基本组成
  • VIM编辑器:模式化编辑、基本操作、常用命令和配置
  • GCC编译器:基本使用、编译过程和常用选项
  • GDB程序调试器:基本使用和高级功能
  • make工程管理器:makefile的编写、变量和示例
  • 项目实训:makefile的编写

通过本章的学习,读者应该能够熟练使用Linux下的编程工具,为后续的编程学习打下基础。

第4章 文件操作 …… 98

4.1 文件系统 …… 99

4.1.1 文件系统的概念

文件系统是操作系统用于管理和存储文件的一种机制,它负责文件的组织、存储、检索和保护。在Linux系统中,文件系统采用树形结构,从根目录(/)开始,向下延伸出各种子目录和文件。

4.1.2 Linux文件系统类型

  • ext2/ext3/ext4:Linux系统的传统文件系统
  • XFS:高性能文件系统,适用于大型文件
  • Btrfs:现代文件系统,支持快照、压缩等功能
  • FAT32/NTFS:Windows文件系统,Linux也支持
  • tmpfs:临时文件系统,存储在内存中

4.1.3 文件系统的基本概念

  • 文件:存储数据的基本单位
  • 目录:存储文件和子目录的容器
  • inode:存储文件的元数据,如权限、大小、修改时间等
  • block:存储文件数据的物理块
  • 硬链接:指向同一个inode的多个文件名
  • 符号链接:指向另一个文件的路径的特殊文件

4.1.4 文件系统的操作命令

  • df:查看文件系统使用情况
  • du:查看目录或文件的大小
  • mount:挂载文件系统
  • umount:卸载文件系统
  • fsck:检查和修复文件系统

4.2 文件描述符的I/O操作 …… 100

4.2.1 文件描述符

文件描述符是一个非负整数,用于标识一个打开的文件。在Linux系统中,每个进程都有一个文件描述符表,用于管理该进程打开的文件。

4.2.2 常用的文件描述符

  • 0:标准输入(stdin)
  • 1:标准输出(stdout)
  • 2:标准错误(stderr)

4.2.3 文件操作函数

  • 打开文件open(const char *pathname, int flags, mode_t mode)

    • flags:打开方式,如O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)、O_CREAT(创建)、O_TRUNC(截断)等
    • mode:文件权限,如0644(所有者读写,其他用户只读)
  • 关闭文件close(int fd)

  • 读取文件read(int fd, void *buf, size_t count)

    • 返回值:实际读取的字节数,0表示文件结束,-1表示出错
  • 写入文件write(int fd, const void *buf, size_t count)

    • 返回值:实际写入的字节数,-1表示出错
  • 定位文件lseek(int fd, off_t offset, int whence)

    • whence:定位基准,如SEEK_SET(文件开头)、SEEK_CUR(当前位置)、SEEK_END(文件结尾)
  • 文件状态fstat(int fd, struct stat *statbuf)

    • 获取文件的元数据

4.2.4 文件操作示例

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

int main() {
    int fd;
    char buffer[1024];
    ssize_t nread, nwrite;

    // 打开文件
    fd = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open");
        exit(1);
    }

    // 写入文件
    const char *text = "Hello, World!\n";
    nwrite = write(fd, text, strlen(text));
    if (nwrite == -1) {
        perror("write");
        close(fd);
        exit(1);
    }
    printf("写入了 %zd 字节\n", nwrite);

    // 定位到文件开头
    lseek(fd, 0, SEEK_SET);

    // 读取文件
    nread = read(fd, buffer, sizeof(buffer));
    if (nread == -1) {
        perror("read");
        close(fd);
        exit(1);
    }
    printf("读取了 %zd 字节:%s", nread, buffer);

    // 关闭文件
    close(fd);

    return 0;
}

4.2.5 文件权限

  • 文件权限由9个字符表示,分为3组,每组3个字符:
    • 第一组:所有者权限
    • 第二组:所属组权限
    • 第三组:其他用户权限
  • 每个字符的含义:
    • r:读权限
    • w:写权限
    • x:执行权限
    • -:无权限
  • 权限的数字表示:
    • r = 4
    • w = 2
    • x = 1
    • 例如:0644 表示 rw-r--r--

4.3 项目实训:日志管理功能 …… 109

4.3.1 实训目标

  • 掌握文件描述符的I/O操作
  • 熟悉文件的打开、读写和关闭操作
  • 了解日志管理的基本概念
  • 学会编写简单的日志管理功能

4.3.2 实训内容

  1. 日志管理功能设计

    • 日志文件的创建和打开
    • 日志的写入(包括时间戳、日志级别、日志内容)
    • 日志文件的关闭
  2. 编写日志管理函数

    • log_open(const char *filename):打开日志文件
    • log_write(int level, const char *format, ...):写入日志
    • log_close():关闭日志文件
  3. 日志级别定义

    • 0:DEBUG
    • 1:INFO
    • 2:WARN
    • 3:ERROR
    • 4:FATAL
  4. 编写测试程序

    • 测试不同级别的日志写入
    • 验证日志文件的内容

4.3.3 实训代码

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdarg.h>
#include <time.h>

#define DEBUG 0
#define INFO 1
#define WARN 2
#define ERROR 3
#define FATAL 4

static int log_fd = -1;

const char *level_str[] = {
    "DEBUG",
    "INFO",
    "WARN",
    "ERROR",
    "FATAL"
};

int log_open(const char *filename) {
    log_fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0644);
    if (log_fd == -1) {
        perror("open");
        return -1;
    }
    return 0;
}

void log_write(int level, const char *format, ...) {
    if (log_fd == -1) {
        return;
    }

    // 获取当前时间
    time_t now = time(NULL);
    struct tm *tm_now = localtime(&now);
    char time_buf[64];
    strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", tm_now);

    // 格式化日志头部
    char log_buf[1024];
    int n = snprintf(log_buf, sizeof(log_buf), "[%s] [%s] ", time_buf, level_str[level]);

    // 格式化日志内容
    va_list args;
    va_start(args, format);
    vsnprintf(log_buf + n, sizeof(log_buf) - n, format, args);
    va_end(args);

    // 写入日志
    write(log_fd, log_buf, strlen(log_buf));
    write(log_fd, "\n", 1);
}

void log_close() {
    if (log_fd != -1) {
        close(log_fd);
        log_fd = -1;
    }
}

int main() {
    // 打开日志文件
    if (log_open("app.log") == -1) {
        return 1;
    }

    // 写入不同级别的日志
    log_write(DEBUG, "这是一条调试日志");
    log_write(INFO, "这是一条信息日志");
    log_write(WARN, "这是一条警告日志");
    log_write(ERROR, "这是一条错误日志");
    log_write(FATAL, "这是一条致命错误日志");

    // 关闭日志文件
    log_close();

    return 0;
}

4.3.4 实训要求

  • 完成所有实训内容
  • 编写完整的C程序
  • 测试程序的正确性
  • 查看日志文件的内容
  • 撰写实训报告

第5章 标准I/O库 …… 111

5.1 标准流的I/O操作 …… 114

5.1.1 标准流的概念

标准I/O库是C语言提供的一组用于输入输出操作的函数,它是对底层I/O操作的封装,提供了更高级、更方便的接口。标准I/O库使用流(stream)作为操作的对象。

5.1.2 标准流

在C语言中,有三个标准流:

  • stdin:标准输入流,通常对应键盘输入
  • stdout:标准输出流,通常对应屏幕输出
  • stderr:标准错误流,通常对应屏幕输出

这些标准流在程序启动时自动打开,不需要手动打开。

5.1.3 标准流的操作函数

  • 输入函数scanf(), getchar(), gets()
  • 输出函数printf(), putchar(), puts()

5.2 流的打开和关闭 …… 114

5.2.1 文件流

文件流是标准I/O库中用于操作文件的对象,用 FILE 类型表示。

5.2.2 打开文件

  • fopen(const char *pathname, const char *mode):打开文件并返回文件流指针
    • mode:打开模式,如"r"(只读)、"w"(只写,创建或截断)、"a"(追加)、"r+"(读写)、"w+"(读写,创建或截断)、"a+"(读写,追加)等

5.2.3 关闭文件

  • fclose(FILE *stream):关闭文件流
    • 关闭文件时会刷新缓冲区

5.2.4 打开和关闭文件的示例

#include <stdio.h>

int main() {
    FILE *fp;

    // 打开文件
    fp = fopen("test.txt", "w");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }

    // 写入数据
    fprintf(fp, "Hello, World!\n");

    // 关闭文件
    fclose(fp);

    return 0;
}

5.3 缓冲区的操作 …… 115

5.3.1 缓冲区的概念

缓冲区是标准I/O库为了提高I/O效率而设置的一块内存区域,用于暂时存储数据。当缓冲区满或手动刷新时,数据才会被写入实际的文件或设备。

5.3.2 缓冲区的类型

  • 全缓冲:当缓冲区满时才进行I/O操作,通常用于磁盘文件
  • 行缓冲:当遇到换行符时进行I/O操作,通常用于终端
  • 无缓冲:立即进行I/O操作,通常用于标准错误流

5.3.3 缓冲区的操作函数

  • fflush(FILE *stream):刷新缓冲区,将缓冲区中的数据写入实际的文件或设备
  • setbuf(FILE *stream, char *buf):设置缓冲区
  • setvbuf(FILE *stream, char *buf, int mode, size_t size):设置缓冲区的模式和大小
    • mode:_IOFBF(全缓冲)、_IOLBF(行缓冲)、_IONBF(无缓冲)

5.3.4 缓冲区操作示例

#include <stdio.h>

int main() {
    FILE *fp;
    char buffer[1024];

    // 打开文件
    fp = fopen("test.txt", "w");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }

    // 设置缓冲区
    setbuf(fp, buffer);

    // 写入数据
    fprintf(fp, "Hello, ");
    fprintf(fp, "World!\n");

    // 刷新缓冲区
    fflush(fp);

    // 关闭文件
    fclose(fp);

    return 0;
}

5.4 直接输入/输出 …… 118

5.4.1 直接输入/输出函数

  • fread(void *ptr, size_t size, size_t nmemb, FILE *stream):从文件流中读取数据

    • ptr:存储数据的指针
    • size:每个数据项的大小
    • nmemb:数据项的数量
    • 返回值:实际读取的数据项数量
  • fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream):向文件流中写入数据

    • ptr:数据的指针
    • size:每个数据项的大小
    • nmemb:数据项的数量
    • 返回值:实际写入的数据项数量

5.4.2 直接输入/输出示例

#include <stdio.h>

struct Student {
    char name[50];
    int age;
    float score;
};

int main() {
    FILE *fp;
    struct Student students[2] = {
        {"Alice", 18, 95.5},
        {"Bob", 19, 88.0}
    };
    struct Student read_students[2];

    // 写入数据
    fp = fopen("students.dat", "wb");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }
    fwrite(students, sizeof(struct Student), 2, fp);
    fclose(fp);

    // 读取数据
    fp = fopen("students.dat", "rb");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }
    fread(read_students, sizeof(struct Student), 2, fp);
    fclose(fp);

    // 输出数据
    for (int i = 0; i < 2; i++) {
        printf("Name: %s, Age: %d, Score: %.1f\n",
               read_students[i].name, read_students[i].age, read_students[i].score);
    }

    return 0;
}

5.5 格式化输入/输出 …… 119

5.5.1 格式化输出函数

  • printf(const char *format, ...):向标准输出流输出格式化数据
  • fprintf(FILE *stream, const char *format, ...):向指定文件流输出格式化数据
  • sprintf(char *str, const char *format, ...):向字符串输出格式化数据
  • snprintf(char *str, size_t size, const char *format, ...):向字符串输出指定长度的格式化数据

5.5.2 格式化输入函数

  • scanf(const char *format, ...):从标准输入流读取格式化数据
  • fscanf(FILE *stream, const char *format, ...):从指定文件流读取格式化数据
  • sscanf(const char *str, const char *format, ...):从字符串读取格式化数据

5.5.3 格式化输入/输出示例

#include <stdio.h>

int main() {
    FILE *fp;
    int a, b;
    float c;

    // 写入格式化数据
    fp = fopen("data.txt", "w");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }
    fprintf(fp, "%d %d %.2f\n", 10, 20, 3.14);
    fclose(fp);

    // 读取格式化数据
    fp = fopen("data.txt", "r");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }
    fscanf(fp, "%d %d %f", &a, &b, &c);
    fclose(fp);

    // 输出数据
    printf("a = %d, b = %d, c = %.2f\n", a, b, c);

    return 0;
}

5.6 基于字符和行的输入/输出 …… 121

5.6.1 基于字符的输入/输出函数

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

    • 返回值:读取的字符,若到达文件结尾或出错则返回EOF
  • fputc(int c, FILE *stream):向文件流中写入一个字符

    • 返回值:写入的字符,若出错则返回EOF
  • getchar(void):从标准输入流中读取一个字符

    • 相当于 fgetc(stdin)
  • putchar(int c):向标准输出流中写入一个字符

    • 相当于 fputc(c, stdout)

5.6.2 基于行的输入/输出函数

  • fgets(char *s, int size, FILE *stream):从文件流中读取一行数据

    • s:存储数据的缓冲区
    • size:缓冲区的大小
    • 返回值:成功返回s,若到达文件结尾或出错则返回NULL
  • fputs(const char *s, FILE *stream):向文件流中写入一行数据

    • 返回值:成功返回非负值,若出错则返回EOF
  • gets(char *s):从标准输入流中读取一行数据(已废弃)

    • 相当于 fgets(s, INT_MAX, stdin)
  • puts(const char *s):向标准输出流中写入一行数据

    • 相当于 fputs(s, stdout) 后跟 putchar('\n')

5.6.3 基于字符和行的输入/输出示例

#include <stdio.h>

int main() {
    FILE *fp;
    char buffer[1024];
    int ch;

    // 写入字符
    fp = fopen("test.txt", "w");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }
    fputc('H', fp);
    fputc('e', fp);
    fputc('l', fp);
    fputc('l', fp);
    fputc('o', fp);
    fputc('\n', fp);
    fclose(fp);

    // 读取字符
    fp = fopen("test.txt", "r");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }
    fclose(fp);

    // 写入行
    fp = fopen("test.txt", "a");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }
    fputs("World!\n", fp);
    fclose(fp);

    // 读取行
    fp = fopen("test.txt", "r");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        fputs(buffer, stdout);
    }
    fclose(fp);

    return 0;
}

5.7 项目实训:出错管理功能 …… 124

5.7.1 实训目标

  • 掌握标准I/O库的使用
  • 熟悉文件的打开、读写和关闭操作
  • 了解出错管理的基本概念
  • 学会编写简单的出错管理功能

5.7.2 实训内容

  1. 出错管理功能设计

    • 错误信息的定义和存储
    • 错误的记录和报告
    • 错误处理策略
  2. 编写出错管理函数

    • error_init():初始化错误管理模块
    • error_set(int code, const char *message):设置错误信息
    • error_get(int *code, char *message, size_t size):获取错误信息
    • error_print():打印错误信息
  3. 错误码定义

    • 0:成功
    • 1:文件打开失败
    • 2:文件读取失败
    • 3:文件写入失败
    • 4:内存分配失败
  4. 编写测试程序

    • 测试文件操作中的错误处理
    • 验证错误信息的正确性

5.7.3 实训代码

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

#define MAX_ERROR_MSG 256

static int error_code = 0;
static char error_message[MAX_ERROR_MSG];

void error_init() {
    error_code = 0;
    error_message[0] = '\0';
}

void error_set(int code, const char *message) {
    error_code = code;
    strncpy(error_message, message, MAX_ERROR_MSG - 1);
    error_message[MAX_ERROR_MSG - 1] = '\0';
}

void error_get(int *code, char *message, size_t size) {
    if (code != NULL) {
        *code = error_code;
    }
    if (message != NULL && size > 0) {
        strncpy(message, error_message, size - 1);
        message[size - 1] = '\0';
    }
}

void error_print() {
    if (error_code != 0) {
        fprintf(stderr, "Error %d: %s\n", error_code, error_message);
    }
}

FILE *safe_fopen(const char *pathname, const char *mode) {
    FILE *fp = fopen(pathname, mode);
    if (fp == NULL) {
        char msg[MAX_ERROR_MSG];
        snprintf(msg, sizeof(msg), "Failed to open file: %s", pathname);
        error_set(1, msg);
    }
    return fp;
}

size_t safe_fread(void *ptr, size_t size, size_t nmemb, FILE *stream) {
    size_t n = fread(ptr, size, nmemb, stream);
    if (n != nmemb) {
        error_set(2, "Failed to read file");
    }
    return n;
}

size_t safe_fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) {
    size_t n = fwrite(ptr, size, nmemb, stream);
    if (n != nmemb) {
        error_set(3, "Failed to write file");
    }
    return n;
}

void *safe_malloc(size_t size) {
    void *ptr = malloc(size);
    if (ptr == NULL) {
        error_set(4, "Failed to allocate memory");
    }
    return ptr;
}

int main() {
    FILE *fp;
    char *buffer;
    size_t n;

    // 初始化错误管理
    error_init();

    // 测试文件打开失败
    fp = safe_fopen("nonexistent.txt", "r");
    if (fp == NULL) {
        error_print();
        error_init();
    }

    // 测试文件写入
    fp = safe_fopen("test.txt", "w");
    if (fp == NULL) {
        error_print();
        return 1;
    }

    const char *text = "Hello, World!\n";
    safe_fwrite(text, 1, strlen(text), fp);
    if (error_code != 0) {
        error_print();
        fclose(fp);
        return 1;
    }
    fclose(fp);

    // 测试文件读取
    fp = safe_fopen("test.txt", "r");
    if (fp == NULL) {
        error_print();
        return 1;
    }

    // 获取文件大小
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    // 分配内存
    buffer = safe_malloc(size + 1);
    if (buffer == NULL) {
        error_print();
        fclose(fp);
        return 1;
    }

    // 读取文件
    n = safe_fread(buffer, 1, size, fp);
    if (error_code != 0) {
        error_print();
        free(buffer);
        fclose(fp);
        return 1;
    }
    buffer[n] = '\0';

    // 输出内容
    printf("File content: %s", buffer);

    // 清理资源
    free(buffer);
    fclose(fp);

    return 0;
}

5.7.4 实训要求

  • 完成所有实训内容
  • 编写完整的C程序
  • 测试程序的正确性
  • 验证错误处理的有效性
  • 撰写实训报告

5.8 本章小结 …… 125

本章介绍了C语言的标准I/O库,包括:

  • 标准流的I/O操作:标准流的概念和操作函数
  • 流的打开和关闭:文件流的打开和关闭操作
  • 缓冲区的操作:缓冲区的概念、类型和操作函数
  • 直接输入/输出:fread和fwrite函数的使用
  • 格式化输入/输出:printf、scanf等函数的使用
  • 基于字符和行的输入/输出:fgetc、fputc、fgets、fputs等函数的使用
  • 项目实训:出错管理功能

通过本章的学习,读者应该能够熟练使用标准I/O库进行文件操作,为后续的编程学习打下基础。

第6章 进程控制 …… 125

6.1 进程概述 …… 125

6.1.1 进程的概念

进程是程序的一次执行过程,是操作系统进行资源分配和调度的基本单位。每个进程都有自己的内存空间、文件描述符、环境变量等资源。

6.1.2 进程的状态

  • 运行状态(Running):进程正在CPU上执行
  • 就绪状态(Ready):进程已经准备好执行,等待CPU分配
  • 阻塞状态(Blocked):进程等待某个事件的发生,如I/O操作完成
  • 终止状态(Terminated):进程已经结束执行

6.1.3 进程的标识符

  • 进程ID(PID):每个进程都有一个唯一的ID
  • 父进程ID(PPID):创建该进程的进程的ID
  • 组ID(GID):进程所属的组的ID

6.1.4 进程的层次结构

在Linux系统中,进程形成一个树状结构,init进程(PID为1)是所有进程的祖先。

6.2 进程控制 …… 131

6.2.1 进程创建

  • fork():创建一个新进程

    • 返回值:在父进程中返回子进程的PID,在子进程中返回0,出错返回-1
    • 子进程是父进程的一个副本,继承父进程的内存空间、文件描述符等
  • vfork():创建一个新进程,与fork()类似,但有一些差异

    • 子进程共享父进程的内存空间
    • 父进程会被阻塞,直到子进程执行exec()或exit()

6.2.2 进程执行

  • exec系列函数:在当前进程中执行一个新程序
    • execl(const char *path, const char *arg, ...)
    • execle(const char *path, const char *arg, ..., char *const envp[])
    • execlp(const char *file, const char *arg, ...)
    • execv(const char *path, char *const argv[])
    • execve(const char *path, char *const argv[], char *const envp[])
    • execvp(const char *file, char *const argv[])

6.2.3 进程等待

  • wait(int *status):等待子进程结束

    • 返回值:结束的子进程的PID,出错返回-1
    • status:存储子进程的退出状态
  • waitpid(pid_t pid, int *status, int options):等待指定的子进程结束

    • pid:要等待的子进程的PID,-1表示等待任意子进程
    • options:选项,如WNOHANG(非阻塞)、WUNTRACED(跟踪停止的进程)

6.2.4 进程终止

  • exit(int status):终止进程

    • status:退出状态,0表示正常终止,非0表示异常终止
  • _exit(int status):终止进程,不执行清理操作

  • abort():异常终止进程,生成SIGABRT信号

6.2.5 进程控制示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid;
    int status;

    // 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(1);
    }

    if (pid == 0) {
        // 子进程
        printf("子进程 PID: %d\n", getpid());
        printf("子进程 PPID: %d\n", getppid());

        // 执行新程序
        execl("/bin/ls", "ls", "-l", NULL);

        // 如果exec失败
        perror("execl");
        exit(1);
    } else {
        // 父进程
        printf("父进程 PID: %d\n", getpid());
        printf("父进程 PPID: %d\n", getppid());
        printf("子进程 PID: %d\n", pid);

        // 等待子进程结束
        wait(&status);
        printf("子进程结束,退出状态: %d\n", WEXITSTATUS(status));
    }

    return 0;
}

6.2.6 进程的环境变量

  • getenv(const char *name):获取环境变量的值
  • setenv(const char *name, const char *value, int overwrite):设置环境变量
  • unsetenv(const char *name):删除环境变量
  • environ:全局变量,指向环境变量数组

6.3 项目实训:进程的实现 …… 139

6.3.1 实训目标

  • 掌握进程的创建和管理
  • 熟悉fork、exec、wait等函数的使用
  • 了解进程的生命周期
  • 学会编写简单的进程管理程序

6.3.2 实训内容

  1. 创建子进程

    • 使用fork()创建子进程
    • 区分父进程和子进程
    • 查看进程的PID和PPID
  2. 执行新程序

    • 使用exec系列函数执行新程序
    • 处理exec失败的情况
  3. 进程等待

    • 使用wait()或waitpid()等待子进程结束
    • 获取子进程的退出状态
  4. 进程管理

    • 编写一个简单的进程管理程序,创建多个子进程并等待它们结束

6.3.3 实训代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid;
    int status;
    int i;

    // 创建3个子进程
    for (i = 0; i < 3; i++) {
        pid = fork();
        if (pid == -1) {
            perror("fork");
            exit(1);
        }

        if (pid == 0) {
            // 子进程
            printf("子进程 %d PID: %d\n", i+1, getpid());

            // 模拟不同的执行时间
            sleep(i+1);

            // 退出状态为i+1
            exit(i+1);
        }
    }

    // 父进程等待所有子进程结束
    for (i = 0; i < 3; i++) {
        pid = wait(&status);
        if (pid == -1) {
            perror("wait");
            exit(1);
        }
        printf("子进程 %d 结束,退出状态: %d\n", pid, WEXITSTATUS(status));
    }

    printf("所有子进程都已结束\n");

    return 0;
}

6.3.4 实训要求

  • 完成所有实训内容
  • 编写完整的C程序
  • 测试程序的正确性
  • 观察进程的创建和结束过程
  • 撰写实训报告

6.4 本章小结 …… 140

本章介绍了进程控制的相关知识,包括:

  • 进程概述:进程的概念、状态、标识符和层次结构
  • 进程控制:进程的创建、执行、等待和终止
  • 项目实训:进程的实现

通过本章的学习,读者应该能够掌握进程的基本概念和控制方法,为后续的线程控制和进程间通信学习打下基础。

第7章 线程控制 …… 142

7.1 线程控制 …… 145

7.1.1 线程的概念

线程是进程内的一个执行单元,是CPU调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的内存空间、文件描述符等资源,但每个线程有自己的栈空间和程序计数器。

7.1.2 线程的特点

  • 轻量级:线程的创建和切换比进程快
  • 共享资源:线程共享进程的资源,减少了资源开销
  • 并发执行:多个线程可以并发执行,提高程序的执行效率
  • 独立性:每个线程有自己的执行路径和状态

7.1.3 线程的创建

  • pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg):创建一个新线程
    • thread:存储新线程的ID
    • attr:线程属性,NULL表示使用默认属性
    • start_routine:线程的入口函数
    • arg:传递给线程入口函数的参数
    • 返回值:成功返回0,失败返回错误码

7.1.4 线程的终止

  • pthread_exit(void *retval):终止当前线程

    • retval:线程的退出状态
  • pthread_cancel(pthread_t thread):取消指定的线程

    • thread:要取消的线程的ID

7.1.5 线程的等待

  • **pthread_join(pthread_t thread, void **retval)**:等待指定的线程结束
    • thread:要等待的线程的ID
    • retval:存储线程的退出状态
    • 返回值:成功返回0,失败返回错误码

7.1.6 线程的属性

  • pthread_attr_init(pthread_attr_t *attr):初始化线程属性
  • pthread_attr_destroy(pthread_attr_t *attr):销毁线程属性
  • pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate):设置线程的分离状态
    • detachstate:PTHREAD_CREATE_DETACHED(分离状态)或 PTHREAD_CREATE_JOINABLE(可连接状态)

7.1.7 线程同步

  • 互斥锁(Mutex)

    • pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr):初始化互斥锁
    • pthread_mutex_lock(pthread_mutex_t *mutex):获取互斥锁
    • pthread_mutex_unlock(pthread_mutex_t *mutex):释放互斥锁
    • pthread_mutex_destroy(pthread_mutex_t *mutex):销毁互斥锁
  • 条件变量(Condition Variable)

    • pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr):初始化条件变量
    • pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex):等待条件变量
    • pthread_cond_signal(pthread_cond_t *cond):唤醒一个等待条件变量的线程
    • pthread_cond_broadcast(pthread_cond_t *cond):唤醒所有等待条件变量的线程
    • pthread_cond_destroy(pthread_cond_t *cond):销毁条件变量
  • 信号量(Semaphore)

    • sem_init(sem_t *sem, int pshared, unsigned int value):初始化信号量
    • sem_wait(sem_t *sem):获取信号量
    • sem_post(sem_t *sem):释放信号量
    • sem_destroy(sem_t *sem):销毁信号量

7.1.8 线程控制示例

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

#define NUM_THREADS 5

void *thread_function(void *arg) {
    int thread_id = *((int *)arg);
    printf("线程 %d 开始执行\n", thread_id);

    // 模拟工作
    sleep(1);

    printf("线程 %d 执行完毕\n", thread_id);
    pthread_exit(NULL);
}

int main() {
    pthread_t threads[NUM_THREADS];
    int thread_ids[NUM_THREADS];
    int i, ret;

    // 创建线程
    for (i = 0; i < NUM_THREADS; i++) {
        thread_ids[i] = i;
        ret = pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]);
        if (ret != 0) {
            fprintf(stderr, "创建线程失败: %d\n", ret);
            exit(1);
        }
        printf("创建线程 %d\n", i);
    }

    // 等待线程结束
    for (i = 0; i < NUM_THREADS; i++) {
        ret = pthread_join(threads[i], NULL);
        if (ret != 0) {
            fprintf(stderr, "等待线程失败: %d\n", ret);
            exit(1);
        }
        printf("线程 %d 已结束\n", i);
    }

    printf("所有线程都已结束\n");

    return 0;
}

7.1.9 线程同步示例

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

#define NUM_THREADS 2
#define MAX_COUNT 10

int count = 0;
pthread_mutex_t mutex;

void *increment_function(void *arg) {
    int i;
    for (i = 0; i < MAX_COUNT; i++) {
        pthread_mutex_lock(&mutex);
        count++;
        printf("线程 %d: count = %d\n", *((int *)arg), count);
        pthread_mutex_unlock(&mutex);
        sleep(0.1);
    }
    pthread_exit(NULL);
}

int main() {
    pthread_t threads[NUM_THREADS];
    int thread_ids[NUM_THREADS];
    int i, ret;

    // 初始化互斥锁
    ret = pthread_mutex_init(&mutex, NULL);
    if (ret != 0) {
        fprintf(stderr, "初始化互斥锁失败: %d\n", ret);
        exit(1);
    }

    // 创建线程
    for (i = 0; i < NUM_THREADS; i++) {
        thread_ids[i] = i;
        ret = pthread_create(&threads[i], NULL, increment_function, &thread_ids[i]);
        if (ret != 0) {
            fprintf(stderr, "创建线程失败: %d\n", ret);
            exit(1);
        }
    }

    // 等待线程结束
    for (i = 0; i < NUM_THREADS; i++) {
        ret = pthread_join(threads[i], NULL);
        if (ret != 0) {
            fprintf(stderr, "等待线程失败: %d\n", ret);
            exit(1);
        }
    }

    // 销毁互斥锁
    pthread_mutex_destroy(&mutex);

    printf("最终 count = %d\n", count);

    return 0;
}

7.2 项目实训:线程的实现 …… 154

7.2.1 实训目标

  • 掌握线程的创建和管理
  • 熟悉线程同步机制
  • 了解线程的生命周期
  • 学会编写简单的多线程程序

7.2.2 实训内容

  1. 创建线程

    • 使用pthread_create()创建多个线程
    • 为每个线程传递参数
    • 区分主线程和子线程
  2. 线程同步

    • 使用互斥锁保护共享资源
    • 使用条件变量实现线程间通信
    • 使用信号量控制线程的执行顺序
  3. 线程管理

    • 使用pthread_join()等待线程结束
    • 使用pthread_exit()终止线程
    • 设置线程的属性
  4. 多线程应用

    • 编写一个多线程程序,模拟生产者-消费者问题
    • 编写一个多线程程序,计算数组的和

7.2.3 实训代码

示例1:生产者-消费者问题

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

#define BUFFER_SIZE 5
#define MAX_ITEMS 10

int buffer[BUFFER_SIZE];
int in = 0;
int out = 0;
pthread_mutex_t mutex;
pthread_cond_t not_full;
pthread_cond_t not_empty;

void *producer(void *arg) {
    int i, item;
    for (i = 0; i < MAX_ITEMS; i++) {
        item = i;

        pthread_mutex_lock(&mutex);
        while ((in + 1) % BUFFER_SIZE == out) {
            pthread_cond_wait(&not_full, &mutex);
        }

        buffer[in] = item;
        in = (in + 1) % BUFFER_SIZE;
        printf("生产者生产: %d\n", item);

        pthread_cond_signal(&not_empty);
        pthread_mutex_unlock(&mutex);

        sleep(1);
    }
    pthread_exit(NULL);
}

void *consumer(void *arg) {
    int i, item;
    for (i = 0; i < MAX_ITEMS; i++) {
        pthread_mutex_lock(&mutex);
        while (in == out) {
            pthread_cond_wait(&not_empty, &mutex);
        }

        item = buffer[out];
        out = (out + 1) % BUFFER_SIZE;
        printf("消费者消费: %d\n", item);

        pthread_cond_signal(&not_full);
        pthread_mutex_unlock(&mutex);

        sleep(2);
    }
    pthread_exit(NULL);
}

int main() {
    pthread_t producer_thread, consumer_thread;
    int ret;

    // 初始化互斥锁和条件变量
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&not_full, NULL);
    pthread_cond_init(&not_empty, NULL);

    // 创建生产者和消费者线程
    ret = pthread_create(&producer_thread, NULL, producer, NULL);
    if (ret != 0) {
        fprintf(stderr, "创建生产者线程失败: %d\n", ret);
        exit(1);
    }

    ret = pthread_create(&consumer_thread, NULL, consumer, NULL);
    if (ret != 0) {
        fprintf(stderr, "创建消费者线程失败: %d\n", ret);
        exit(1);
    }

    // 等待线程结束
    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);

    // 销毁互斥锁和条件变量
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&not_full);
    pthread_cond_destroy(&not_empty);

    return 0;
}

示例2:计算数组的和

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

#define NUM_THREADS 4
#define ARRAY_SIZE 1000

int array[ARRAY_SIZE];
long sum = 0;
pthread_mutex_t mutex;

void *sum_function(void *arg) {
    int thread_id = *((int *)arg);
    int start = thread_id * (ARRAY_SIZE / NUM_THREADS);
    int end = (thread_id + 1) * (ARRAY_SIZE / NUM_THREADS);
    long local_sum = 0;
    int i;

    for (i = start; i < end; i++) {
        local_sum += array[i];
    }

    pthread_mutex_lock(&mutex);
    sum += local_sum;
    pthread_mutex_unlock(&mutex);

    printf("线程 %d 计算的和: %ld\n", thread_id, local_sum);
    pthread_exit(NULL);
}

int main() {
    pthread_t threads[NUM_THREADS];
    int thread_ids[NUM_THREADS];
    int i, ret;

    // 初始化数组
    for (i = 0; i < ARRAY_SIZE; i++) {
        array[i] = i + 1;
    }

    // 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);

    // 创建线程
    for (i = 0; i < NUM_THREADS; i++) {
        thread_ids[i] = i;
        ret = pthread_create(&threads[i], NULL, sum_function, &thread_ids[i]);
        if (ret != 0) {
            fprintf(stderr, "创建线程失败: %d\n", ret);
            exit(1);
        }
    }

    // 等待线程结束
    for (i = 0; i < NUM_THREADS; i++) {
        ret = pthread_join(threads[i], NULL);
        if (ret != 0) {
            fprintf(stderr, "等待线程失败: %d\n", ret);
            exit(1);
        }
    }

    // 销毁互斥锁
    pthread_mutex_destroy(&mutex);

    printf("数组的总和: %ld\n", sum);
    printf("预期的总和: %ld\n", (long)ARRAY_SIZE * (ARRAY_SIZE + 1) / 2);

    return 0;
}

7.2.4 实训要求

  • 完成所有实训内容
  • 编写完整的C程序
  • 测试程序的正确性
  • 观察线程的执行过程
  • 撰写实训报告

7.3 本章小结 …… 155

本章介绍了线程控制的相关知识,包括:

  • 线程的概念和特点
  • 线程的创建、终止和等待
  • 线程的属性设置
  • 线程同步机制:互斥锁、条件变量、信号量
  • 项目实训:线程的实现

通过本章的学习,读者应该能够掌握线程的基本概念和控制方法,为后续的进程间通信学习打下基础。

第8章 进程间通信 …… 156

8.1 概述 …… 157

8.1.1 进程间通信的概念

进程间通信(IPC,Inter-Process Communication)是指不同进程之间传递信息的机制。由于进程的地址空间是相互独立的,进程之间不能直接访问对方的内存,因此需要使用专门的IPC机制来实现进程间的通信。

8.1.2 进程间通信的方式

在Linux系统中,常用的IPC机制包括:

  • 共享内存:多个进程共享同一块内存区域
  • 信号量:用于同步进程的执行
  • 管道:用于进程间的单向通信
  • 命名管道:用于无亲缘关系进程间的通信
  • 消息队列:用于进程间传递消息
  • 信号:用于进程间的异步通信
  • 套接字:用于网络通信

8.2 共享内存 …… 157

8.2.1 共享内存的概念

共享内存是一种高效的IPC机制,它允许多个进程共享同一块内存区域,进程可以直接读写该内存区域,而不需要进行数据拷贝。

8.2.2 共享内存的操作函数

  • shmget(key_t key, size_t size, int shmflg):创建或获取共享内存

    • key:共享内存的键值
    • size:共享内存的大小
    • shmflg:标志位,如IPC_CREAT(创建)、IPC_EXCL(排他)、权限位等
    • 返回值:共享内存的ID,出错返回-1
  • shmat(int shmid, const void *shmaddr, int shmflg):将共享内存附加到进程的地址空间

    • shmid:共享内存的ID
    • shmaddr:附加的地址,NULL表示由系统自动分配
    • shmflg:标志位,如SHM_RDONLY(只读)
    • 返回值:附加的内存地址,出错返回-1
  • shmdt(const void *shmaddr):将共享内存从进程的地址空间分离

    • shmaddr:附加的内存地址
    • 返回值:成功返回0,出错返回-1
  • shmctl(int shmid, int cmd, struct shmid_ds *buf):控制共享内存

    • shmid:共享内存的ID
    • cmd:命令,如IPC_STAT(获取状态)、IPC_SET(设置状态)、IPC_RMID(删除)
    • buf:状态缓冲区
    • 返回值:成功返回0,出错返回-1

8.2.3 共享内存的示例

// 写入进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_SIZE 1024
#define SHM_KEY 1234

int main() {
    int shmid;
    char *shmaddr;

    // 创建共享内存
    shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    // 附加共享内存
    shmaddr = (char *)shmat(shmid, NULL, 0);
    if (shmaddr == (char *)-1) {
        perror("shmat");
        exit(1);
    }

    // 写入数据
    strcpy(shmaddr, "Hello, Shared Memory!");
    printf("写入共享内存: %s\n", shmaddr);

    // 等待读取
    printf("按任意键继续...\n");
    getchar();

    // 分离共享内存
    shmdt(shmaddr);

    // 删除共享内存
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

// 读取进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_SIZE 1024
#define SHM_KEY 1234

int main() {
    int shmid;
    char *shmaddr;

    // 获取共享内存
    shmid = shmget(SHM_KEY, SHM_SIZE, 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    // 附加共享内存
    shmaddr = (char *)shmat(shmid, NULL, 0);
    if (shmaddr == (char *)-1) {
        perror("shmat");
        exit(1);
    }

    // 读取数据
    printf("读取共享内存: %s\n", shmaddr);

    // 分离共享内存
    shmdt(shmaddr);

    return 0;
}

8.3 信号量 …… 162

8.3.1 信号量的概念

信号量是一种用于同步进程执行的IPC机制,它可以用来保护共享资源,防止多个进程同时访问共享资源。

8.3.2 信号量的操作函数

  • semget(key_t key, int nsems, int semflg):创建或获取信号量集

    • key:信号量集的键值
    • nsems:信号量的数量
    • semflg:标志位,如IPC_CREAT(创建)、IPC_EXCL(排他)、权限位等
    • 返回值:信号量集的ID,出错返回-1
  • semop(int semid, struct sembuf *sops, size_t nsops):执行信号量操作

    • semid:信号量集的ID
    • sops:信号量操作数组
    • nsops:操作的数量
    • 返回值:成功返回0,出错返回-1
  • semctl(int semid, int semnum, int cmd, ...):控制信号量

    • semid:信号量集的ID
    • semnum:信号量的编号
    • cmd:命令,如SETVAL(设置值)、GETVAL(获取值)、IPC_RMID(删除)
    • 返回值:根据命令不同而不同,出错返回-1

8.3.3 信号量的示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/wait.h>

#define SEM_KEY 1234

// 信号量操作函数
void sem_wait(int semid) {
    struct sembuf sops;
    sops.sem_num = 0;
    sops.sem_op = -1;
    sops.sem_flg = 0;
    semop(semid, &sops, 1);
}

void sem_post(int semid) {
    struct sembuf sops;
    sops.sem_num = 0;
    sops.sem_op = 1;
    sops.sem_flg = 0;
    semop(semid, &sops, 1);
}

int main() {
    int semid;
    pid_t pid;

    // 创建信号量
    semid = semget(SEM_KEY, 1, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget");
        exit(1);
    }

    // 初始化信号量为1
    semctl(semid, 0, SETVAL, 1);

    // 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(1);
    }

    if (pid == 0) {
        // 子进程
        printf("子进程等待信号量...\n");
        sem_wait(semid);
        printf("子进程获取信号量\n");
        sleep(2);
        printf("子进程释放信号量\n");
        sem_post(semid);
        exit(0);
    } else {
        // 父进程
        sleep(1);
        printf("父进程等待信号量...\n");
        sem_wait(semid);
        printf("父进程获取信号量\n");
        sleep(2);
        printf("父进程释放信号量\n");
        sem_post(semid);
        wait(NULL);
    }

    // 删除信号量
    semctl(semid, 0, IPC_RMID);

    return 0;
}

8.4 管道通信 …… 168

8.4.1 管道的概念

管道是一种用于进程间单向通信的IPC机制,它只能在有亲缘关系的进程之间使用(如父进程和子进程)。

8.4.2 管道的操作函数

  • pipe(int pipefd[2]):创建管道
    • pipefd:管道的文件描述符数组,pipefd[0]用于读取,pipefd[1]用于写入
    • 返回值:成功返回0,出错返回-1

8.4.3 管道的示例

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

int main() {
    int pipefd[2];
    pid_t pid;
    char buffer[1024];

    // 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(1);
    }

    // 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(1);
    }

    if (pid == 0) {
        // 子进程:读取管道
        close(pipefd[1]); // 关闭写入端
        read(pipefd[0], buffer, sizeof(buffer));
        printf("子进程读取到: %s\n", buffer);
        close(pipefd[0]); // 关闭读取端
        exit(0);
    } else {
        // 父进程:写入管道
        close(pipefd[0]); // 关闭读取端
        const char *message = "Hello, Pipe!";
        write(pipefd[1], message, strlen(message) + 1);
        printf("父进程写入: %s\n", message);
        close(pipefd[1]); // 关闭写入端
        wait(NULL);
    }

    return 0;
}

8.5 命名管道 …… 171

8.5.1 命名管道的概念

命名管道(FIFO)是一种特殊的文件,它可以在无亲缘关系的进程之间使用,通过文件系统路径来标识。

8.5.2 命名管道的操作函数

  • mkfifo(const char *pathname, mode_t mode):创建命名管道
    • pathname:命名管道的路径
    • mode:权限位
    • 返回值:成功返回0,出错返回-1

8.5.3 命名管道的示例

// 写入进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>

#define FIFO_PATH "/tmp/myfifo"

int main() {
    int fd;
    const char *message = "Hello, Named Pipe!";

    // 创建命名管道
    mkfifo(FIFO_PATH, 0666);

    // 打开命名管道
    fd = open(FIFO_PATH, O_WRONLY);
    if (fd == -1) {
        perror("open");
        exit(1);
    }

    // 写入数据
    write(fd, message, strlen(message) + 1);
    printf("写入命名管道: %s\n", message);

    // 关闭命名管道
    close(fd);

    return 0;
}

// 读取进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

#define FIFO_PATH "/tmp/myfifo"

int main() {
    int fd;
    char buffer[1024];

    // 打开命名管道
    fd = open(FIFO_PATH, O_RDONLY);
    if (fd == -1) {
        perror("open");
        exit(1);
    }

    // 读取数据
    read(fd, buffer, sizeof(buffer));
    printf("读取命名管道: %s\n", buffer);

    // 关闭命名管道
    close(fd);

    // 删除命名管道
    unlink(FIFO_PATH);

    return 0;
}

8.6 消息队列 …… 176

8.6.1 消息队列的概念

消息队列是一种用于进程间传递消息的IPC机制,它可以存储消息,进程可以从消息队列中读取消息。

8.6.2 消息队列的操作函数

  • msgget(key_t key, int msgflg):创建或获取消息队列

    • key:消息队列的键值
    • msgflg:标志位,如IPC_CREAT(创建)、IPC_EXCL(排他)、权限位等
    • 返回值:消息队列的ID,出错返回-1
  • msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg):发送消息

    • msqid:消息队列的ID
    • msgp:消息指针
    • msgsz:消息大小
    • msgflg:标志位,如IPC_NOWAIT(非阻塞)
    • 返回值:成功返回0,出错返回-1
  • msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg):接收消息

    • msqid:消息队列的ID
    • msgp:消息指针
    • msgsz:消息大小
    • msgtyp:消息类型
    • msgflg:标志位,如IPC_NOWAIT(非阻塞)、MSG_NOERROR(截断消息)
    • 返回值:成功返回接收到的消息大小,出错返回-1
  • msgctl(int msqid, int cmd, struct msqid_ds *buf):控制消息队列

    • msqid:消息队列的ID
    • cmd:命令,如IPC_STAT(获取状态)、IPC_SET(设置状态)、IPC_RMID(删除)
    • buf:状态缓冲区
    • 返回值:成功返回0,出错返回-1

8.6.3 消息队列的示例

// 发送进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define MSG_KEY 1234

struct message {
    long mtype;
    char mtext[1024];
};

int main() {
    int msqid;
    struct message msg;

    // 创建消息队列
    msqid = msgget(MSG_KEY, IPC_CREAT | 0666);
    if (msqid == -1) {
        perror("msgget");
        exit(1);
    }

    // 设置消息类型和内容
    msg.mtype = 1;
    strcpy(msg.mtext, "Hello, Message Queue!");

    // 发送消息
    msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0);
    printf("发送消息: %s\n", msg.mtext);

    // 删除消息队列
    msgctl(msqid, IPC_RMID, NULL);

    return 0;
}

// 接收进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define MSG_KEY 1234

struct message {
    long mtype;
    char mtext[1024];
};

int main() {
    int msqid;
    struct message msg;

    // 获取消息队列
    msqid = msgget(MSG_KEY, 0666);
    if (msqid == -1) {
        perror("msgget");
        exit(1);
    }

    // 接收消息
    msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0);
    printf("接收消息: %s\n", msg.mtext);

    return 0;
}

8.7 项目实训:进程之间通信功能的实现 …… 183

8.7.1 实训目标

  • 掌握进程间通信的基本概念
  • 熟悉各种IPC机制的使用
  • 学会编写简单的进程间通信程序
  • 了解进程间通信的应用场景

8.7.2 实训内容

  1. 共享内存通信

    • 编写两个进程,使用共享内存传递数据
    • 一个进程写入数据,另一个进程读取数据
  2. 信号量同步

    • 编写两个进程,使用信号量同步它们的执行
    • 实现生产者-消费者模式
  3. 管道通信

    • 编写父进程和子进程,使用管道传递数据
  4. 命名管道通信

    • 编写两个无亲缘关系的进程,使用命名管道传递数据
  5. 消息队列通信

    • 编写两个进程,使用消息队列传递消息

8.7.3 实训代码

示例:使用共享内存和信号量实现生产者-消费者模式

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/wait.h>

#define SHM_KEY 1234
#define SEM_KEY 5678
#define SHM_SIZE 1024

// 信号量操作函数
void sem_wait(int semid) {
    struct sembuf sops;
    sops.sem_num = 0;
    sops.sem_op = -1;
    sops.sem_flg = 0;
    semop(semid, &sops, 1);
}

void sem_post(int semid) {
    struct sembuf sops;
    sops.sem_num = 0;
    sops.sem_op = 1;
    sops.sem_flg = 0;
    semop(semid, &sops, 1);
}

int main() {
    int shmid, semid;
    char *shmaddr;
    pid_t pid;

    // 创建共享内存
    shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    // 附加共享内存
    shmaddr = (char *)shmat(shmid, NULL, 0);
    if (shmaddr == (char *)-1) {
        perror("shmat");
        exit(1);
    }

    // 创建信号量
    semid = semget(SEM_KEY, 1, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget");
        exit(1);
    }

    // 初始化信号量为1
    semctl(semid, 0, SETVAL, 1);

    // 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(1);
    }

    if (pid == 0) {
        // 子进程:消费者
        while (1) {
            sem_wait(semid);
            if (strlen(shmaddr) > 0) {
                printf("消费者读取: %s\n", shmaddr);
                shmaddr[0] = '\0';
            }
            sem_post(semid);
            sleep(1);
        }
    } else {
        // 父进程:生产者
        char message[1024];
        while (1) {
            printf("请输入消息(输入exit退出): ");
            fgets(message, sizeof(message), stdin);
            message[strlen(message) - 1] = '\0'; // 去除换行符

            if (strcmp(message, "exit") == 0) {
                break;
            }

            sem_wait(semid);
            strcpy(shmaddr, message);
            printf("生产者写入: %s\n", message);
            sem_post(semid);
        }

        // 等待子进程结束
        kill(pid, SIGTERM);
        wait(NULL);

        // 清理资源
        shmdt(shmaddr);
        shmctl(shmid, IPC_RMID, NULL);
        semctl(semid, 0, IPC_RMID);
    }

    return 0;
}

8.7.4 实训要求

  • 完成所有实训内容
  • 编写完整的C程序
  • 测试程序的正确性
  • 观察进程间通信的过程
  • 撰写实训报告

8.8 本章小结 …… 184

本章介绍了进程间通信的相关知识,包括:

  • 概述:进程间通信的概念和方式
  • 共享内存:高效的内存共享机制
  • 信号量:用于同步进程的执行
  • 管道通信:有亲缘关系进程间的单向通信
  • 命名管道:无亲缘关系进程间的通信
  • 消息队列:用于传递消息的机制
  • 项目实训:进程之间通信功能的实现

通过本章的学习,读者应该能够掌握各种IPC机制的使用方法,为后续的网络编程学习打下基础。

第9章 信号及信号处理 …… 186

9.1 信号及其使用 …… 187

9.1.1 信号的概念

信号是一种用于进程间通信的机制,它是由操作系统或其他进程发送给目标进程的一种异步通知,用于通知目标进程发生了某个事件。

9.1.2 常用的信号

  • SIGINT (2):中断信号,通常由Ctrl+C产生
  • SIGTERM (15):终止信号,默认的终止进程信号
  • SIGKILL (9):强制终止信号,无法被捕获或忽略
  • SIGSEGV (11):段错误信号,当进程访问无效内存时产生
  • SIGFPE (8):浮点异常信号,当发生浮点运算错误时产生
  • SIGALRM (14):闹钟信号,由alarm()函数设置
  • SIGCHLD (17):子进程状态改变信号,当子进程终止或停止时产生
  • SIGUSR1 (10) 和 SIGUSR2 (12):用户自定义信号

9.1.3 信号的默认处理方式

  • Term:终止进程
  • Ign:忽略信号
  • Core:终止进程并生成core文件
  • Stop:停止进程
  • Cont:继续进程

9.1.4 信号的发送

  • kill(pid_t pid, int sig):向指定进程发送信号

    • pid:进程ID,-1表示向所有进程发送
    • sig:信号编号
    • 返回值:成功返回0,出错返回-1
  • raise(int sig):向当前进程发送信号

    • sig:信号编号
    • 返回值:成功返回0,出错返回-1
  • alarm(unsigned int seconds):设置闹钟,在指定秒数后发送SIGALRM信号

    • seconds:秒数
    • 返回值:之前设置的闹钟剩余秒数
  • pause(void):暂停进程,直到收到信号

    • 返回值:-1,并设置errno为EINTR

9.2 信号处理 …… 189

9.2.1 信号处理函数

  • signal(int signum, sighandler_t handler):设置信号处理函数

    • signum:信号编号
    • handler:信号处理函数,或SIG_DFL(默认处理)、SIG_IGN(忽略)
    • 返回值:之前的信号处理函数
  • sigaction(int signum, const struct sigaction *act, struct sigaction *oldact):设置信号处理函数(更灵活)

    • signum:信号编号
    • act:新的信号处理配置
    • oldact:保存旧的信号处理配置
    • 返回值:成功返回0,出错返回-1

9.2.2 信号处理函数的编写

信号处理函数的原型为:

void handler(int signum);
  • signum:接收到的信号编号

9.2.3 信号集操作

  • sigemptyset(sigset_t *set):初始化信号集为空
  • sigfillset(sigset_t *set):初始化信号集为包含所有信号
  • sigaddset(sigset_t *set, int signum):向信号集添加信号
  • sigdelset(sigset_t *set, int signum):从信号集删除信号
  • sigismember(const sigset_t *set, int signum):检查信号是否在信号集中

9.2.4 信号屏蔽

  • sigprocmask(int how, const sigset_t *set, sigset_t *oldset):设置进程的信号屏蔽字

    • how:SIG_BLOCK(添加屏蔽)、SIG_UNBLOCK(解除屏蔽)、SIG_SETMASK(设置屏蔽)
    • set:要屏蔽的信号集
    • oldset:保存旧的信号屏蔽字
    • 返回值:成功返回0,出错返回-1
  • sigsuspend(const sigset_t *mask):临时替换信号屏蔽字并暂停进程

    • mask:临时的信号屏蔽字
    • 返回值:-1,并设置errno为EINTR

9.2.5 信号处理示例

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

void sigint_handler(int signum) {
    printf("接收到SIGINT信号 (%d)\n", signum);
    printf("正在清理资源...\n");
    sleep(1);
    printf("资源清理完成,程序退出\n");
    exit(0);
}

int main() {
    // 设置SIGINT信号的处理函数
    signal(SIGINT, sigint_handler);

    printf("程序运行中,按Ctrl+C发送SIGINT信号\n");
    printf("等待信号...\n");

    // 无限循环
    while (1) {
        sleep(1);
    }

    return 0;
}

9.3 项目实训:信号的处理 …… 194

9.3.1 实训目标

  • 掌握信号的基本概念
  • 熟悉常用信号的使用
  • 学会编写信号处理函数
  • 了解信号屏蔽和信号集的操作

9.3.2 实训内容

  1. 信号处理函数的编写

    • 编写SIGINT信号的处理函数
    • 编写SIGTERM信号的处理函数
  2. 信号发送

    • 使用kill命令发送信号
    • 使用kill()函数发送信号
  3. 信号屏蔽

    • 使用sigprocmask()函数屏蔽信号
    • 使用sigsuspend()函数临时解除屏蔽
  4. 闹钟信号

    • 使用alarm()函数设置闹钟
    • 编写SIGALRM信号的处理函数

9.3.3 实训代码

示例1:信号处理函数

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

void sigint_handler(int signum) {
    printf("接收到SIGINT信号 (%d)\n", signum);
    printf("正在退出...\n");
    exit(0);
}

void sigterm_handler(int signum) {
    printf("接收到SIGTERM信号 (%d)\n", signum);
    printf("正在退出...\n");
    exit(0);
}

int main() {
    // 设置信号处理函数
    signal(SIGINT, sigint_handler);
    signal(SIGTERM, sigterm_handler);

    printf("程序运行中...\n");
    printf("使用 'kill %d' 发送SIGTERM信号\n", getpid());
    printf("按Ctrl+C发送SIGINT信号\n");

    // 无限循环
    while (1) {
        printf("运行中...\n");
        sleep(1);
    }

    return 0;
}

示例2:闹钟信号

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

int count = 0;

void alarm_handler(int signum) {
    printf("接收到SIGALRM信号 (%d)\n", signum);
    count++;
    if (count < 5) {
        // 重新设置闹钟
        alarm(1);
    } else {
        printf("闹钟响了5次,程序退出\n");
        exit(0);
    }
}

int main() {
    // 设置SIGALRM信号的处理函数
    signal(SIGALRM, alarm_handler);

    // 设置闹钟,1秒后发送SIGALRM信号
    alarm(1);

    printf("程序运行中,等待闹钟信号...\n");

    // 无限循环
    while (1) {
        pause(); // 暂停进程,直到收到信号
    }

    return 0;
}

9.3.4 实训要求

  • 完成所有实训内容
  • 编写完整的C程序
  • 测试程序的正确性
  • 观察信号的处理过程
  • 撰写实训报告

9.4 本章小结 …… 196

本章介绍了信号及信号处理的相关知识,包括:

  • 信号的概念和常用信号
  • 信号的默认处理方式
  • 信号的发送和接收
  • 信号处理函数的编写
  • 信号集操作和信号屏蔽
  • 项目实训:信号的处理

通过本章的学习,读者应该能够掌握信号的基本概念和处理方法,为后续的网络编程学习打下基础。

第10章 网络编程 …… 197

10.1 网络编程的基本概念 …… 198

10.1.1 网络协议

网络协议是计算机网络中通信双方必须遵守的规则和约定,它规定了数据的格式、传输方式、错误处理等。常用的网络协议包括:

  • TCP/IP:传输控制协议/网际协议,是互联网的基础协议
  • HTTP:超文本传输协议,用于Web通信
  • FTP:文件传输协议,用于文件传输
  • SMTP:简单邮件传输协议,用于邮件发送
  • DNS:域名系统,用于域名解析

10.1.2 网络模型

  • OSI七层模型:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层
  • TCP/IP四层模型:网络接口层、网络层、传输层、应用层

10.1.3 IP地址和端口

  • IP地址:用于标识网络中的设备,IPv4地址为32位,IPv6地址为128位
  • 端口:用于标识设备上的进程,范围为0-65535,其中0-1023为知名端口

10.1.4 套接字

套接字(Socket)是网络通信的端点,是应用程序与网络协议栈之间的接口。套接字分为:

  • 流套接字(SOCK_STREAM):基于TCP协议,提供可靠的、面向连接的通信
  • 数据报套接字(SOCK_DGRAM):基于UDP协议,提供不可靠的、无连接的通信

10.2 网络编程基础 …… 199

10.2.1 套接字函数

  • socket(int domain, int type, int protocol):创建套接字

    • domain:地址族,如AF_INET(IPv4)、AF_INET6(IPv6)
    • type:套接字类型,如SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)
    • protocol:协议,通常为0,表示使用默认协议
    • 返回值:成功返回套接字文件描述符,出错返回-1
  • bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen):绑定套接字到地址

    • sockfd:套接字文件描述符
    • addr:地址结构体
    • addrlen:地址结构体的长度
    • 返回值:成功返回0,出错返回-1
  • listen(int sockfd, int backlog):设置套接字为监听状态

    • sockfd:套接字文件描述符
    • backlog:等待连接的最大队列长度
    • 返回值:成功返回0,出错返回-1
  • accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen):接受连接

    • sockfd:监听套接字文件描述符
    • addr:存储客户端地址
    • addrlen:地址结构体的长度
    • 返回值:成功返回新的套接字文件描述符,出错返回-1
  • connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen):连接到服务器

    • sockfd:套接字文件描述符
    • addr:服务器地址
    • addrlen:地址结构体的长度
    • 返回值:成功返回0,出错返回-1
  • send(int sockfd, const void *buf, size_t len, int flags):发送数据

    • sockfd:套接字文件描述符
    • buf:数据缓冲区
    • len:数据长度
    • flags:标志,通常为0
    • 返回值:成功返回发送的字节数,出错返回-1
  • recv(int sockfd, void *buf, size_t len, int flags):接收数据

    • sockfd:套接字文件描述符
    • buf:数据缓冲区
    • len:缓冲区长度
    • flags:标志,通常为0
    • 返回值:成功返回接收的字节数,0表示连接关闭,出错返回-1
  • close(int sockfd):关闭套接字

    • sockfd:套接字文件描述符
    • 返回值:成功返回0,出错返回-1

10.2.2 地址结构体

  • IPv4地址结构体

    struct sockaddr_in {
        sa_family_t sin_family;    // 地址族(AF_INET)
        in_port_t sin_port;        // 端口号(网络字节序)
        struct in_addr sin_addr;   // IP地址
    };
    
  • 通用地址结构体

    struct sockaddr {
        sa_family_t sa_family;    // 地址族
        char sa_data[14];         // 地址数据
    };
    

10.2.3 字节序转换

  • htons(uint16_t hostshort):主机字节序转网络字节序(16位)
  • ntohs(uint16_t netshort):网络字节序转主机字节序(16位)
  • htonl(uint32_t hostlong):主机字节序转网络字节序(32位)
  • ntohl(uint32_t netlong):网络字节序转主机字节序(32位)

10.2.4 IP地址转换

  • inet_addr(const char *cp):字符串转IPv4地址(网络字节序)
  • inet_ntoa(struct in_addr in):IPv4地址转字符串
  • inet_pton(int af, const char *src, void *dst):字符串转IP地址(支持IPv4和IPv6)
  • inet_ntop(int af, const void *src, char *dst, socklen_t size):IP地址转字符串(支持IPv4和IPv6)

10.3 TCP通信编程 …… 203

10.3.1 TCP的特点

  • 面向连接:通信前需要建立连接
  • 可靠传输:使用确认机制和重传机制
  • 流量控制:使用滑动窗口机制
  • 拥塞控制:适应网络状况

10.3.2 TCP服务器的实现步骤

  1. 创建套接字
  2. 绑定地址
  3. 设置监听状态
  4. 接受连接
  5. 收发数据
  6. 关闭连接

10.3.3 TCP客户端的实现步骤

  1. 创建套接字
  2. 连接到服务器
  3. 收发数据
  4. 关闭连接

10.3.4 TCP服务器示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    char *hello = "Hello from server";

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置套接字选项
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    // 绑定地址
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 设置监听状态
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    // 接受连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }

    // 接收数据
    read(new_socket, buffer, BUFFER_SIZE);
    printf("客户端消息: %s\n", buffer);

    // 发送数据
    send(new_socket, hello, strlen(hello), 0);
    printf("已发送消息\n");

    // 关闭连接
    close(new_socket);
    close(server_fd);

    return 0;
}

10.3.5 TCP客户端示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE] = {0};
    char *hello = "Hello from client";

    // 创建套接字
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("套接字创建失败\n");
        return -1;
    }

    // 设置服务器地址
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    // 转换IP地址
    if(inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr)<=0) {
        printf("无效的地址\n");
        return -1;
    }

    // 连接到服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("连接失败\n");
        return -1;
    }

    // 发送数据
    send(sock, hello, strlen(hello), 0);
    printf("已发送消息\n");

    // 接收数据
    read(sock, buffer, BUFFER_SIZE);
    printf("服务器消息: %s\n", buffer);

    // 关闭连接
    close(sock);

    return 0;
}

10.4 UDP通信编程 …… 211

10.4.1 UDP的特点

  • 无连接:通信前不需要建立连接
  • 不可靠传输:不保证数据的可靠交付
  • 面向报文:以报文为单位传输数据
  • 速度快:没有确认和重传机制

10.4.2 UDP服务器的实现步骤

  1. 创建套接字
  2. 绑定地址
  3. 收发数据
  4. 关闭套接字

10.4.3 UDP客户端的实现步骤

  1. 创建套接字
  2. 收发数据
  3. 关闭套接字

10.4.4 UDP服务器示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;
    char buffer[BUFFER_SIZE];
    int len, n;

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 清空地址结构体
    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    // 设置服务器地址
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);

    // 绑定地址
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    len = sizeof(cliaddr);

    // 接收数据
    n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, MSG_WAITALL, (struct sockaddr *)&cliaddr, &len);
    buffer[n] = '\0';
    printf("客户端消息: %s\n", buffer);

    // 发送数据
    sendto(sockfd, "Hello from server", strlen("Hello from server"), MSG_CONFIRM, (const struct sockaddr *)&cliaddr, len);
    printf("已发送消息\n");

    // 关闭套接字
    close(sockfd);

    return 0;
}

10.4.5 UDP客户端示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in servaddr;
    char buffer[BUFFER_SIZE];
    int n, len;

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 清空地址结构体
    memset(&servaddr, 0, sizeof(servaddr));

    // 设置服务器地址
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 发送数据
    sendto(sockfd, "Hello from client", strlen("Hello from client"), MSG_CONFIRM, (const struct sockaddr *)&servaddr, sizeof(servaddr));
    printf("已发送消息\n");

    // 接收数据
    len = sizeof(servaddr);
    n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, MSG_WAITALL, (struct sockaddr *)&servaddr, &len);
    buffer[n] = '\0';
    printf("服务器消息: %s\n", buffer);

    // 关闭套接字
    close(sockfd);

    return 0;
}

10.5 项目实训:局域网文件下载 …… 216

10.5.1 实训目标

  • 掌握网络编程的基本概念
  • 熟悉TCP通信编程
  • 学会编写文件传输程序
  • 了解局域网通信的实现

10.5.2 实训内容

  1. 服务器端

    • 创建TCP服务器
    • 接收客户端的文件请求
    • 读取文件并发送给客户端
  2. 客户端

    • 创建TCP客户端
    • 连接到服务器
    • 发送文件请求
    • 接收文件并保存

10.5.3 实训代码

服务器端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include <fcntl.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    int file_fd;
    ssize_t bytes_read, bytes_sent;

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置套接字选项
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    // 绑定地址
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 设置监听状态
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    printf("服务器启动,等待客户端连接...\n");

    // 接受连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }

    // 接收文件名
    read(new_socket, buffer, BUFFER_SIZE);
    printf("客户端请求文件: %s\n", buffer);

    // 打开文件
    file_fd = open(buffer, O_RDONLY);
    if (file_fd < 0) {
        perror("open");
        send(new_socket, "文件不存在", strlen("文件不存在"), 0);
        close(new_socket);
        close(server_fd);
        return 1;
    }

    // 发送文件内容
    while ((bytes_read = read(file_fd, buffer, BUFFER_SIZE)) > 0) {
        bytes_sent = send(new_socket, buffer, bytes_read, 0);
        if (bytes_sent < 0) {
            perror("send");
            break;
        }
    }

    // 关闭文件和连接
    close(file_fd);
    close(new_socket);
    close(server_fd);

    printf("文件传输完成\n");

    return 0;
}

客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE] = {0};
    char filename[100];
    int file_fd;
    ssize_t bytes_read, bytes_written;

    // 创建套接字
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("套接字创建失败\n");
        return -1;
    }

    // 设置服务器地址
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    // 转换IP地址
    if(inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr)<=0) {
        printf("无效的地址\n");
        return -1;
    }

    // 连接到服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("连接失败\n");
        return -1;
    }

    // 输入文件名
    printf("请输入要下载的文件名: ");
    scanf("%s", filename);

    // 发送文件名
    send(sock, filename, strlen(filename), 0);
    printf("请求文件: %s\n", filename);

    // 打开文件
    file_fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (file_fd < 0) {
        perror("open");
        close(sock);
        return -1;
    }

    // 接收文件内容
    while ((bytes_read = read(sock, buffer, BUFFER_SIZE)) > 0) {
        bytes_written = write(file_fd, buffer, bytes_read);
        if (bytes_written < 0) {
            perror("write");
            break;
        }
    }

    // 关闭文件和连接
    close(file_fd);
    close(sock);

    printf("文件下载完成\n");

    return 0;
}

10.5.4 实训要求

  • 完成所有实训内容
  • 编写完整的C程序
  • 测试程序的正确性
  • 观察文件传输的过程
  • 撰写实训报告

10.6 本章小结 …… 221

本章介绍了网络编程的相关知识,包括:

  • 网络编程的基本概念:网络协议、网络模型、IP地址和端口、套接字
  • 网络编程基础:套接字函数、地址结构体、字节序转换、IP地址转换
  • TCP通信编程:TCP的特点、服务器和客户端的实现
  • UDP通信编程:UDP的特点、服务器和客户端的实现
  • 项目实训:局域网文件下载

通过本章的学习,读者应该能够掌握网络编程的基本概念和实现方法,为后续的网络应用开发打下基础。

第11章 GTK+图形界面编程 …… 222

11.1 Linux图形界面开发 …… 223

11.1.1 Linux图形界面概述

Linux系统的图形界面是基于X Window System(X11)构建的,它提供了一个基础的图形窗口系统,允许应用程序创建和管理窗口、处理用户输入等。

11.1.2 常见的Linux桌面环境

  • GNOME:使用GTK+作为工具包
  • KDE:使用Qt作为工具包
  • Xfce:轻量级桌面环境,使用GTK+
  • LXDE:轻量级桌面环境,使用GTK+

11.1.3 GTK+简介

GTK+(GIMP Toolkit)是一个跨平台的图形用户界面工具包,最初为GIMP(GNU Image Manipulation Program)开发,现在被广泛应用于Linux桌面环境和应用程序。

11.2 GTK+程序结构 …… 224

11.2.1 GTK+程序的基本结构

  1. 初始化GTK+
  2. 创建主窗口
  3. 创建和添加构件
  4. 连接信号和回调函数
  5. 显示窗口
  6. 进入主事件循环

11.2.2 GTK+程序示例

#include <gtk/gtk.h>

// 回调函数
void on_button_clicked(GtkWidget *widget, gpointer data) {
    g_print("按钮被点击了!\n");
}

int main(int argc, char *argv[]) {
    GtkWidget *window;
    GtkWidget *button;
    GtkWidget *box;

    // 初始化GTK+
    gtk_init(&argc, &argv);

    // 创建主窗口
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(window), "GTK+示例");
    gtk_window_set_default_size(GTK_WINDOW(window), 300, 200);
    g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);

    // 创建垂直布局盒
    box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
    gtk_container_add(GTK_CONTAINER(window), box);

    // 创建按钮
    button = gtk_button_new_with_label("点击我");
    g_signal_connect(button, "clicked", G_CALLBACK(on_button_clicked), NULL);
    gtk_box_pack_start(GTK_BOX(box), button, TRUE, TRUE, 0);

    // 显示所有构件
    gtk_widget_show_all(window);

    // 进入主事件循环
    gtk_main();

    return 0;
}

11.2.3 编译和运行GTK+程序

编译命令:

gcc `pkg-config --cflags gtk+-3.0` -o hello hello.c `pkg-config --libs gtk+-3.0`

运行命令:

./hello

11.3 基本构件 …… 226

11.3.1 窗口(Window)

  • gtk_window_new(GtkWindowType type):创建窗口

    • type:GTK_WINDOW_TOPLEVEL(顶级窗口)或 GTK_WINDOW_POPUP(弹出窗口)
  • 常用属性

    • title:窗口标题
    • default-width:默认宽度
    • default-height:默认高度
    • resizable:是否可调整大小
  • 常用信号

    • destroy:窗口被销毁时触发
    • delete-event:窗口关闭按钮被点击时触发

11.3.2 按钮(Button)

  • gtk_button_new():创建按钮

  • gtk_button_new_with_label(const gchar *label):创建带标签的按钮

  • gtk_button_new_with_mnemonic(const gchar *label):创建带助记符的按钮

  • 常用信号

    • clicked:按钮被点击时触发

11.3.3 标签(Label)

  • gtk_label_new(const gchar *str):创建标签
  • gtk_label_set_text(GtkLabel *label, const gchar *str):设置标签文本
  • gtk_label_set_markup(GtkLabel *label, const gchar *str):设置带标记的文本

11.3.4 输入框(Entry)

  • gtk_entry_new():创建输入框

  • gtk_entry_set_text(GtkEntry *entry, const gchar *text):设置输入框文本

  • gtk_entry_get_text(GtkEntry *entry):获取输入框文本

  • 常用信号

    • changed:输入框内容改变时触发
    • activate:按下Enter键时触发

11.3.5 布局构件

  • Box:水平或垂直布局
    • gtk_box_new(GtkOrientation orientation, gint spacing)
  • Grid:网格布局
    • gtk_grid_new()
  • Frame:带边框的容器
    • gtk_frame_new(const gchar *label)
  • Notebook:标签页容器
    • gtk_notebook_new()

11.5 信号与事件 …… 237

11.5.1 信号的概念

信号是GTK+中用于处理用户交互的机制,当用户执行某个操作(如点击按钮)时,会触发相应的信号,程序可以通过连接回调函数来响应这些信号。

11.5.2 信号连接

  • g_signal_connect(gpointer instance, const gchar *signal_name, GCallback c_handler, gpointer data):连接信号和回调函数
    • instance:发出信号的构件
    • signal_name:信号名称
    • c_handler:回调函数
    • data:传递给回调函数的数据

11.5.3 回调函数

回调函数的一般形式:

void callback_function(GtkWidget *widget, gpointer data) {
    // 处理信号
}

11.5.4 信号与事件示例

#include <gtk/gtk.h>

// 按钮点击回调函数
void on_button_clicked(GtkWidget *widget, gpointer data) {
    GtkEntry *entry = GTK_ENTRY(data);
    const gchar *text = gtk_entry_get_text(entry);
    g_print("输入的文本:%s\n", text);
}

// 窗口销毁回调函数
void on_window_destroy(GtkWidget *widget, gpointer data) {
    gtk_main_quit();
}

int main(int argc, char *argv[]) {
    GtkWidget *window;
    GtkWidget *box;
    GtkWidget *entry;
    GtkWidget *button;

    // 初始化GTK+
    gtk_init(&argc, &argv);

    // 创建窗口
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(window), "信号与事件示例");
    gtk_window_set_default_size(GTK_WINDOW(window), 300, 100);
    g_signal_connect(window, "destroy", G_CALLBACK(on_window_destroy), NULL);

    // 创建布局
    box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
    gtk_container_add(GTK_CONTAINER(window), box);

    // 创建输入框
    entry = gtk_entry_new();
    gtk_box_pack_start(GTK_BOX(box), entry, TRUE, TRUE, 0);

    // 创建按钮
    button = gtk_button_new_with_label("获取文本");
    g_signal_connect(button, "clicked", G_CALLBACK(on_button_clicked), entry);
    gtk_box_pack_start(GTK_BOX(box), button, TRUE, TRUE, 0);

    // 显示所有构件
    gtk_widget_show_all(window);

    // 进入主事件循环
    gtk_main();

    return 0;
}

11.6 常用构件 …… 239

11.6.1 复选框(CheckButton)

  • gtk_check_button_new():创建复选框
  • gtk_check_button_new_with_label(const gchar *label):创建带标签的复选框
  • gtk_toggle_button_get_active(GtkToggleButton *toggle_button):获取复选框状态
  • gtk_toggle_button_set_active(GtkToggleButton *toggle_button, gboolean is_active):设置复选框状态

11.6.2 单选按钮(RadioButton)

  • gtk_radio_button_new(GSList *group):创建单选按钮
  • gtk_radio_button_new_with_label(GSList *group, const gchar *label):创建带标签的单选按钮
  • gtk_radio_button_get_group(GtkRadioButton *radio_button):获取单选按钮组
  • gtk_radio_button_join_group(GtkRadioButton *radio_button, GtkRadioButton *group_source):将单选按钮加入组

11.6.3 组合框(ComboBox)

  • gtk_combo_box_text_new():创建文本组合框
  • gtk_combo_box_text_append_text(GtkComboBoxText *combo_box, const gchar *text):添加选项
  • gtk_combo_box_text_get_active_text(GtkComboBoxText *combo_box):获取当前选中的文本

11.6.4 进度条(ProgressBar)

  • gtk_progress_bar_new():创建进度条
  • gtk_progress_bar_set_fraction(GtkProgressBar *progress_bar, gdouble fraction):设置进度(0.0-1.0)
  • gtk_progress_bar_pulse(GtkProgressBar *progress_bar):脉冲模式

11.6.5 滚动窗口(ScrolledWindow)

  • gtk_scrolled_window_new(GtkAdjustment *hadjustment, GtkAdjustment *vadjustment):创建滚动窗口
  • gtk_scrolled_window_set_policy(GtkScrolledWindow *scrolled_window, GtkPolicyType hscrollbar_policy, GtkPolicyType vscrollbar_policy):设置滚动条策略

11.7 项目实训:贪吃蛇游戏 …… 243

11.7.1 实训目标

  • 掌握GTK+图形界面编程
  • 熟悉GTK+的基本构件和信号处理
  • 学会编写简单的图形界面游戏
  • 了解游戏开发的基本思路

11.7.2 实训内容

  1. 游戏设计

    • 游戏界面:包含游戏区域、分数显示、控制按钮
    • 游戏逻辑:蛇的移动、食物的生成、碰撞检测、得分计算
  2. 游戏实现

    • 创建主窗口
    • 绘制游戏区域
    • 实现蛇的移动控制
    • 实现食物的生成和碰撞检测
    • 实现得分系统

11.7.3 实训代码

#include <gtk/gtk.h>
#include <stdlib.h>
#include <time.h>

#define WIDTH 400
#define HEIGHT 400
#define GRID_SIZE 20
#define MAX_LENGTH 100

typedef struct {
    int x;
    int y;
} Position;

typedef struct {
    Position body[MAX_LENGTH];
    int length;
    int direction;
    Position food;
    int score;
    gboolean running;
    guint timer;
} SnakeGame;

SnakeGame game;
GtkWidget *drawing_area;
GtkWidget *score_label;

// 方向常量
#define UP 0
#define DOWN 1
#define LEFT 2
#define RIGHT 3

// 初始化游戏
void init_game() {
    // 初始化蛇
    game.length = 3;
    game.body[0].x = WIDTH / 2;
    game.body[0].y = HEIGHT / 2;
    game.body[1].x = game.body[0].x - GRID_SIZE;
    game.body[1].y = game.body[0].y;
    game.body[2].x = game.body[1].x - GRID_SIZE;
    game.body[2].y = game.body[1].y;
    game.direction = RIGHT;
    game.score = 0;
    game.running = TRUE;

    // 生成食物
    srand(time(NULL));
    game.food.x = (rand() % (WIDTH / GRID_SIZE)) * GRID_SIZE;
    game.food.y = (rand() % (HEIGHT / GRID_SIZE)) * GRID_SIZE;
}

// 绘制游戏
gboolean draw_game(GtkWidget *widget, cairo_t *cr, gpointer data) {
    int i;

    // 绘制背景
    cairo_set_source_rgb(cr, 0.9, 0.9, 0.9);
    cairo_rectangle(cr, 0, 0, WIDTH, HEIGHT);
    cairo_fill(cr);

    // 绘制蛇
    cairo_set_source_rgb(cr, 0.2, 0.6, 0.2);
    for (i = 0; i < game.length; i++) {
        cairo_rectangle(cr, game.body[i].x, game.body[i].y, GRID_SIZE - 1, GRID_SIZE - 1);
        cairo_fill(cr);
    }

    // 绘制食物
    cairo_set_source_rgb(cr, 0.8, 0.2, 0.2);
    cairo_rectangle(cr, game.food.x, game.food.y, GRID_SIZE - 1, GRID_SIZE - 1);
    cairo_fill(cr);

    return FALSE;
}

// 移动蛇
void move_snake() {
    int i;
    Position head;

    // 计算新头部位置
    head = game.body[0];
    switch (game.direction) {
        case UP:
            head.y -= GRID_SIZE;
            break;
        case DOWN:
            head.y += GRID_SIZE;
            break;
        case LEFT:
            head.x -= GRID_SIZE;
            break;
        case RIGHT:
            head.x += GRID_SIZE;
            break;
    }

    // 检查边界碰撞
    if (head.x < 0 || head.x >= WIDTH || head.y < 0 || head.y >= HEIGHT) {
        game.running = FALSE;
        return;
    }

    // 检查自身碰撞
    for (i = 1; i < game.length; i++) {
        if (head.x == game.body[i].x && head.y == game.body[i].y) {
            game.running = FALSE;
            return;
        }
    }

    // 检查食物碰撞
    if (head.x == game.food.x && head.y == game.food.y) {
        // 增加长度
        if (game.length < MAX_LENGTH) {
            game.length++;
        }
        // 增加分数
        game.score += 10;
        // 更新分数显示
        char score_text[50];
        sprintf(score_text, "分数: %d", game.score);
        gtk_label_set_text(GTK_LABEL(score_label), score_text);
        // 生成新食物
        game.food.x = (rand() % (WIDTH / GRID_SIZE)) * GRID_SIZE;
        game.food.y = (rand() % (HEIGHT / GRID_SIZE)) * GRID_SIZE;
    }

    // 移动身体
    for (i = game.length - 1; i > 0; i--) {
        game.body[i] = game.body[i - 1];
    }
    // 设置新头部
    game.body[0] = head;
}

// 定时器回调
gboolean on_timer(gpointer data) {
    if (game.running) {
        move_snake();
        gtk_widget_queue_draw(drawing_area);
        return TRUE;
    } else {
        // 游戏结束
        GtkWidget *dialog;
        char message[100];
        sprintf(message, "游戏结束!最终分数: %d", game.score);
        dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, "%s", message);
        gtk_dialog_run(GTK_DIALOG(dialog));
        gtk_widget_destroy(dialog);
        return FALSE;
    }
}

// 键盘事件处理
gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer data) {
    switch (event->keyval) {
        case GDK_KEY_Up:
            if (game.direction != DOWN) {
                game.direction = UP;
            }
            break;
        case GDK_KEY_Down:
            if (game.direction != UP) {
                game.direction = DOWN;
            }
            break;
        case GDK_KEY_Left:
            if (game.direction != RIGHT) {
                game.direction = LEFT;
            }
            break;
        case GDK_KEY_Right:
            if (game.direction != LEFT) {
                game.direction = RIGHT;
            }
            break;
    }
    return FALSE;
}

// 开始游戏
void on_start_game(GtkWidget *widget, gpointer data) {
    // 取消之前的定时器
    if (game.timer > 0) {
        g_source_remove(game.timer);
    }
    // 初始化游戏
    init_game();
    // 更新分数显示
    char score_text[50];
    sprintf(score_text, "分数: %d", game.score);
    gtk_label_set_text(GTK_LABEL(score_label), score_text);
    // 启动定时器
    game.timer = g_timeout_add(150, on_timer, NULL);
    // 重绘
    gtk_widget_queue_draw(drawing_area);
}

int main(int argc, char *argv[]) {
    GtkWidget *window;
    GtkWidget *vbox;
    GtkWidget *hbox;
    GtkWidget *start_button;

    // 初始化GTK+
    gtk_init(&argc, &argv);

    // 创建主窗口
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(window), "贪吃蛇游戏");
    gtk_window_set_default_size(GTK_WINDOW(window), WIDTH, HEIGHT + 50);
    g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);

    // 创建垂直布局
    vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
    gtk_container_add(GTK_CONTAINER(window), vbox);

    // 创建绘制区域
    drawing_area = gtk_drawing_area_new();
    gtk_widget_set_size_request(drawing_area, WIDTH, HEIGHT);
    g_signal_connect(drawing_area, "draw", G_CALLBACK(draw_game), NULL);
    gtk_box_pack_start(GTK_BOX(vbox), drawing_area, TRUE, TRUE, 0);

    // 创建水平布局
    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
    gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);

    // 创建分数标签
    score_label = gtk_label_new("分数: 0");
    gtk_box_pack_start(GTK_BOX(hbox), score_label, TRUE, TRUE, 0);

    // 创建开始按钮
    start_button = gtk_button_new_with_label("开始游戏");
    g_signal_connect(start_button, "clicked", G_CALLBACK(on_start_game), NULL);
    gtk_box_pack_start(GTK_BOX(hbox), start_button, FALSE, FALSE, 0);

    // 连接键盘事件
    gtk_widget_add_events(window, GDK_KEY_PRESS_MASK);
    g_signal_connect(window, "key-press-event", G_CALLBACK(on_key_press), NULL);

    // 初始化游戏
    init_game();

    // 显示所有构件
    gtk_widget_show_all(window);

    // 进入主事件循环
    gtk_main();

    return 0;
}

11.7.4 实训要求

  • 完成所有实训内容
  • 编写完整的C程序
  • 测试程序的正确性
  • 观察游戏的运行过程
  • 撰写实训报告

11.8 本章小结 …… 249

本章介绍了GTK+图形界面编程的相关知识,包括:

  • Linux图形界面开发:Linux图形界面概述、常见的桌面环境、GTK+简介
  • GTK+程序结构:基本结构、示例代码、编译和运行
  • 基本构件:窗口、按钮、标签、输入框、布局构件
  • 信号与事件:信号的概念、信号连接、回调函数
  • 常用构件:复选框、单选按钮、组合框、进度条、滚动窗口
  • 项目实训:贪吃蛇游戏

通过本章的学习,读者应该能够掌握GTK+图形界面编程的基本概念和实现方法,为后续的图形界面应用开发打下基础。

结语

呕心沥血 , 啊啊啊 , 我要歇一会了 !😱😱😱😱😱😱