引言:C程序设计实验的重要性与挑战

C语言作为计算机科学与技术的基础编程语言,在上海交通大学的计算机相关专业课程中占据核心地位。C程序设计实验不仅是理论知识的实践检验,更是培养编程思维、调试能力和工程素养的关键环节。然而,许多学生在实验过程中常常遇到各种难题:语法错误难以定位、逻辑错误难以排查、算法设计思路不清等。本文将基于上海交大C程序设计实验的典型题目,提供详细的答案解析和编程技巧指导,帮助学生高效掌握编程方法,解决实验中的实际问题。

一、基础语法实验:从入门到熟练

1.1 实验题目:计算两个整数的和、差、积、商和余数

题目要求:编写程序,输入两个整数a和b,计算并输出它们的和、差、积、商(整数除法)和余数。

解题思路

  • 首先需要包含标准输入输出头文件stdio.h
  • 使用scanf函数读取用户输入的两个整数
  • 分别使用+-*/%运算符进行计算
  • 使用printf函数格式化输出结果

参考代码

#include <stdio.h>

int main() {
    int a, b;
    
    // 输入提示
    printf("请输入两个整数(用空格分隔):");
    
    // 读取输入
    scanf("%d %d", &a, &b);
    
    // 计算并输出结果
    printf("和:%d\n", a + b);
    printf("差:%d\n", a - b);
    printf("积:%d\n", a * b);
    printf("商:%d\n", a / b);  // 整数除法,自动舍去小数部分
    printf("余数:%d\n", a % b);
    
    return 0;
}

代码解析

  1. #include <stdio.h>:包含标准输入输出库,这是使用printfscanf函数的前提
  2. int a, b;:声明两个整型变量用于存储输入的数值
  3. scanf("%d %d", &a, &b):从标准输入读取两个整数,%d是格式说明符,&是取地址运算符
  4. a / b:整数除法,当a=7、b=3时,结果为2(不是2.333…)
  5. a % b:取模运算,计算a除以b的余数,7%3=1

常见错误与调试技巧

  • 错误1:忘记包含stdio.h,导致编译错误”implicit declaration of function”
  • 错误2scanf中忘记使用&,导致运行时错误(段错误)
  • 错误3:除数为0导致运行时错误,应添加检查:
if (b == 0) {
    printf("错误:除数不能为0!\n");
    return 1;
}

1.2 实验题目:判断素数

题目要求:编写程序,输入一个正整数n,判断是否为素数(质数),输出”Yes”或”No”。

解题思路

  • 素数定义:大于1的自然数,除了1和它本身外,不能被其他自然数整除
  • 优化算法:只需检查到√n即可,因为如果n有大于√n的因子,必然有小于√n的对应因子
  • 特殊情况:1不是素数

参考代码

#include <stdio.h>
#include <math.h>  // 用于sqrt函数

int main() {
    int n, i;
    int isPrime = 1;  // 标记是否为素数,1表示是,0表示否
    
    printf("请输入一个正整数:");
    scanf("%d", &n);
    
    // 处理特殊情况
    if (n <= 1) {
        isPrime = 0;
    } else {
        // 只需检查到√n
        int limit = (int)sqrt(n);
        for (i = 2; i <= limit; i++) {
            if (n % i == 0) {
                isPrime = 0;
                break;  // 找到因子,立即退出循环
            }
        }
    }
    
    // 输出结果
    if (isPrime) {
        printf("Yes\n");
    } else {
        printf("No\n");
    }
    
    return 0;
}

代码解析

  1. #include <math.h>:使用sqrt函数需要包含数学库
  2. int isPrime = 1;:使用标志变量记录结果,初始假设是素数
  3. int limit = (int)sqrt(n);:计算√n并取整,优化循环次数
  4. for (i = 2; i <= limit; i++):从2开始检查到√n
  5. if (n % i == 0):如果能被整除,说明不是素数
  6. break;:找到因子后立即退出,提高效率

优化版本(不使用sqrt)

#include <stdio.h>

int main() {
    int n, i;
    int isPrime = 1;
    
    printf("请输入一个正整数:");
    scanf("%d", &n);
    
    if (n <= 1) {
        isPrime = 0;
    } else {
        // 优化:i*i <= n 等价于 i <= sqrt(n),避免浮点运算
        for (i = 2; i * i <= n; i++) {
            if (n % i == 0) {
                isPrime = 0;
                break;
            }
        }
    }
    
    printf(isPrime ? "Yes\n" : "No\n");
    
    return 0;
}

二、数组与循环实验:数据处理的核心

2.1 实验题目:数组元素排序

题目要求:编写程序,输入10个整数,使用冒泡排序法从小到大排序后输出。

解题思路

  • 冒泡排序:重复遍历数组,比较相邻元素,如果顺序错误则交换
  • 外层循环控制排序轮数,内层循环控制每轮比较次数
  • 每轮结束后,最大的元素”冒泡”到末尾

参考代码

#include <stdio.h>

int main() {
    int arr[10];
    int i, j, temp;
    
    // 输入10个整数
    printf("请输入10个整数:\n");
    for (i = 0; i < 10; i++) {
        printf("arr[%d] = ", i);
        scanf("%d", &arr[i]);
    }
    
    // 冒泡排序
    for (i = 0; i < 9; i++) {  // 外层循环:9轮比较
        for (j = 0; j < 9 - i; j++) {  // 内层循环:每轮比较次数递减
            if (arr[j] > arr[j + 1]) {
                // 交换相邻元素
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
    
    // 输出排序结果
    printf("排序后的数组:\n");
    for (i = 0; i < 10; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    
    return 0;
}

代码解析

  1. int arr[10];:声明长度为10的整型数组
  2. for (i = 0; i < 9; i++):外层循环9次,因为10个元素需要9轮比较
  3. for (j = 0; j < 9 - i; j++):每轮比较次数递减,因为每轮结束后末尾元素已有序
  4. if (arr[j] > arr[j + 1]):比较相邻元素,如果前一个大于后一个则交换
  5. 三变量交换技巧:使用temp临时存储,完成交换
  6. 时间复杂度:O(n²),对于10个元素是可行的,但对于大量数据效率较低

执行过程示例(输入:5, 3, 8, 1, 2):

  • 第1轮:比较9次,最大值8移到末尾 → [3, 5, 1, 2, 8]
  • 第2轮:比较8次,次大值5移到倒数第二 → [3, 1, 2, 5, 8]
  • 第3轮:比较7次 → [1, 2, 3, 5, 8]
  • 第4轮:比较6次 → [1, 2, 3, 5, 8]
  • 完成排序

2.2 实验题目:矩阵转置

题目要求:编写程序,输入一个3×3矩阵,输出其转置矩阵。

解题思路

  • 矩阵转置:将矩阵的行和列互换,即a[i][j]变成a[j][i]
  • 使用二维数组存储矩阵
  • 注意:原地转置时,只需遍历上三角或下三角,避免重复交换

参考代码

#include <stdio.h>

int main() {
    int matrix[3][3];
    int i, j, temp;

    // 输入矩阵
    printf("请输入3×3矩阵(按行输入):\n");
    for (i = 0; i < 3; i++) {
        printf("第%d行:", i + 1);
        for (j = 0; j < 3; j++) {
            scanf("%d", &matrix[i][j]);
        }
    }

    // 输出原矩阵
    printf("\n原矩阵:\n");
    for (i = 0; i < 3; i++) {
        for (j = 0; j < 3; j++) {
            printf("%d\t", matrix[i][j]);
        }
        printf("\n");
    }

    // 矩阵转置(原地转置)
    for (i = 0; i < 3; i++) {
        for (j = i + 1; j < 3; j++) {  // 只遍历上三角
            // 交换matrix[i][j]和matrix[j][i]
            temp = matrix[i][j];
            matrix[i][j] = matrix[j][i];
            matrix[j][i] = temp;
        }
    }

    // 输出转置矩阵
    printf("\n转置矩阵:\n");
    for (i = 0; i < 3; i++) {
        for (j = 0; j < 3; j++) {
            printf("%d\t", matrix[i][j]);
        }
        printf("\n");
    }

    return 0;
}

代码解析

  1. int matrix[3][3];:声明3×3二维数组
  2. 原地转置优化for (j = i + 1; j < 3; j++)只遍历上三角(对角线以上),避免重复交换
  3. 交换逻辑matrix[i][j]matrix[j][i]互换,实现行列互换
  4. 输出格式:使用\t(制表符)对齐,使矩阵显示更美观
  5. 时间复杂度:O(n²/2) ≈ O(n²),但常数因子减半

错误示范(常见错误):

// 错误:遍历整个矩阵,导致重复交换,最终转置失败
for (i = 0; i < 3; i++) {
    for (j = 0; j < 3; j++) {
        temp = matrix[i][j];
        matrix[i][j] = matrix[j][i];  // 第一次交换正确,第二次交换又换回去了
        matrix[j][i] = temp;
    }
}

三、指针与函数实验:C语言的精髓

3.1 实验题目:使用指针实现字符串反转

题目要求:编写函数,使用指针实现字符串反转功能。

解题思路

  • 指针操作:通过指针直接访问和修改内存中的数据
  • 字符串反转:首尾字符交换,向中间靠拢
  • 边界条件:空字符串、单字符字符串的处理

参考代码

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

// 函数声明
void reverse_string(char *str);

int main() {
    char str[100];
    
    printf("请输入一个字符串:");
    gets(str);  // 注意:gets不安全,实际应用中应使用fgets
    
    reverse_string(str);
    
    printf("反转后的字符串:%s\n", str);
    
    return 0;
}

// 使用指针反转字符串
void reverse_string(char *str) {
    if (str == NULL || *str == '\0') {
        return;  // 空指针或空字符串直接返回
    }
    
    char *start = str;           // 指向字符串开头
    char *end = str + strlen(str) - 1;  // 指向字符串末尾(最后一个字符)
    
    // 当start < end时,交换字符并移动指针
    while (start < end) {
        // 交换字符
        char temp = *start;
        *start = *end;
        *end = temp;
        
        // 移动指针
        start++;
        end--;
    }
}

代码解析

  1. char *str:函数参数是指向字符的指针,即字符串的首地址
  2. char *start = str;:start指针指向字符串开头
  3. char *end = str + strlen(str) - 1;:end指针指向字符串末尾
  4. while (start < end):当两个指针未相遇时继续交换
  5. *start*end:解引用操作,获取指针指向的字符值
  6. 指针移动start++end--使指针向中间移动

执行过程示例(输入”hello”):

  • 初始:start=‘h’, end=‘o’
  • 第1轮:交换’h’和’o’ → “oellh”,start++, end–
  • 第2轮:交换’e’和’l’ → “olleh”,start++, end–
  • 此时start > end,循环结束

安全版本(使用fgets):

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

void reverse_string(char *str);

int main() {
    char str[100];
    
    printf("请输入一个字符串:");
    // fgets会读取换行符,需要处理
    if (fgets(str, sizeof(str), stdin) != NULL) {
        // 去除可能的换行符
        size_t len = strlen(str);
        if (len > 0 && str[len - 1] == '\n') {
            str[len - 1] = '\0';
        }
        
        reverse_string(str);
        printf("反转后的字符串:%s\n", str);
    }
    
    return 0;
}

3.2 实验题目:使用函数指针实现回调

题目要求:编写程序,定义一个函数用于计算两个数的运算,使用函数指针作为参数,实现加法、减法、乘法的灵活调用。

解题思路

  • 函数指针:指向函数的指针变量,可以像调用函数一样使用
  • 回调机制:将函数作为参数传递,提高代码的灵活性和复用性
  • 定义统一的函数接口,通过不同的函数指针实现不同的功能

参考代码

#include <stdio.h>

// 定义函数指针类型:指向返回int、接受两个int参数的函数
typedef int (*operation_func)(int, int);

// 具体的运算函数
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }

// 统一的计算函数,接受函数指针作为参数
int calculate(int x, int y, operation_func op) {
    return op(x, y);  // 通过函数指针调用具体函数
}

int main() {
    int a = 10, b = 5;
    
    printf("%d + %d = %d\n", a, b, calculate(a, b, add));
    printf("%d - %d = %d\n", a, b, calculate(a, b, subtract));
    printf("%d * %d = %d\n", a, b, calculate(a, b, multiply));
    
    // 也可以直接传递函数地址
    operation_func func = add;
    printf("直接调用:10 + 5 = %d\n", calculate(10, 5, func));
    
    return 0;
}

代码解析

  1. typedef int (*operation_func)(int, int);:定义函数指针类型
    • operation_func是一种类型,可以声明函数指针变量
    • 指向的函数返回int,接受两个int参数
  2. int add(int a, int b) { return a + b; }:具体函数实现
  3. int calculate(int x, inti y, operation_func op):统一接口函数
    • op是函数指针参数,可以接收add、subtract等函数
  4. op(x, y):通过函数指针调用函数,等价于add(x, y)subtract(x, y)
  5. 函数指针的赋值operation_func func = add;,函数名就是函数地址

函数指针的调用方式

// 方式1:通过函数指针变量调用
operation_func func = add;
int result1 = func(10, 5);

// 方式2:直接通过类型转换调用
int result2 = ((operation_func)add)(10, 5);

// 方式3:在calculate函数内部调用
int result3 = calculate(10, 5, add);

四、结构体与文件操作实验:工程化编程基础

4.1 实验题目:学生信息管理系统

题目要求:定义学生结构体,包含学号、姓名、成绩,编写函数实现录入、显示、查找功能,并将数据保存到文件。

解题思路

  • 结构体设计:合理组织相关数据
  • 文件操作:使用fopen、fwrite、fread等函数实现数据持久化
  • 模块化设计:将不同功能封装成函数

参考代码

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

#define MAX_STUDENTS 50
#define FILENAME "students.dat"

// 学生结构体
typedef struct {
    char id[20];
    char name[30];
    float score;
} Student;

// 函数声明
void inputStudent(Student *s);
void displayStudent(Student *s);
void saveToFile(Student students[], int count);
int readFromFile(Student students[]);
void findStudent(Student students[], int count, char *id);

int main() {
    Student students[MAX_STUDENTS];
    int count = 0;
    char searchId[20];
    int choice;
    
    // 从文件读取已有数据
    count = readFromFile(students);
    
    while (1) {
        printf("\n=== 学生信息管理系统 ===\n");
        printf("1. 录入学生信息\n");
        printf("2. 显示所有学生\n");
        printf("3. 查找学生\n");
        printf("4. 保存并退出\n");
        printf("请选择:");
        scanf("%d", &choice);
        getchar();  // 清除缓冲区的换行符
        
        switch (choice) {
            case 1:
                if (count >= MAX_STUDENTS) {
                    printf("已达到最大人数限制!\n");
                    break;
                }
                inputStudent(&students[count]);
                count++;
                break;
            case 2:
                printf("\n所有学生信息:\n");
                for (int i = 0; i < count; i++) {
                    displayStudent(&students[i]);
                }
                break;
            case 3:
                printf("请输入要查找的学号:");
                scanf("%s", searchId);
                findStudent(students, count, searchId);
                break;
            case 4:
                saveToFile(students, count);
                printf("数据已保存,程序退出。\n");
                return 0;
            default:
                printf("无效选择!\n");
        }
    }
    
    return 0;
}

// 录入学生信息
void inputStudent(Student *s) {
    printf("请输入学号:");
    scanf("%s", s->id);
    printf("请输入姓名:");
    scanf("%s", s->name);
    printf("请输入成绩:");
    scanf("%f", &s->score);
}

// 显示学生信息
void inputStudent(Student *s) {
    printf("请输入学号:");
    scanf("%s", s->id);
    printf("请输入姓名:");
    scanf("%s", s->name);
    printf("请输入成绩:");
    scanf("%f", &s->score);
}

// 显示学生信息
void displayStudent(Student *s) {
    printf("学号:%s | 姓名:%s | 成绩:%.1f\n", s->id, s->name, s->score);
}

// 保存到文件
void saveToFile(Student students[], int count) {
    FILE *fp = fopen(FILENAME, "wb");
    if (fp == NULL) {
        printf("无法打开文件!\n");
        return;
    }
    
    // 写入数据
    fwrite(students, sizeof(Student), count, fp);
    fclose(fp);
    printf("数据已保存到 %s\n", FILENAME);
}

// 从文件读取
int readFromFile(Student students[]) {
    FILE *fp = fopen(FILENAME, "rb");
    if (fp == NULL) {
        return 0;  // 文件不存在,返回0
    }
    
    int count = fread(students, sizeof(Student), MAX_STUDENTS, fp);
    fclose(fp);
    printf("已从文件加载 %d 条记录\n", count);
    return count;
}

// 查找学生
void findStudent(Student students[], int count, char *id) {
    for (int i = 0; i < count; i++) {
        if (strcmp(students[i].id, id) == 0) {
            printf("找到学生:");
            displayStudent(&students[i]);
            return;
        }
    }
    printf("未找到学号为 %s 的学生\n", id);
}

代码解析

  1. 结构体定义

    typedef struct {
       char id[20];
       char name[30];
       float score;
    } Student;
    
    • 使用typedef简化类型名称
    • 包含学号、姓名、成绩三个字段
  2. 文件操作函数

    • fopen(FILENAME, "wb"):以二进制写模式打开文件
    • fwrite(students, sizeof(Student), count, fp):批量写入结构体数据
    • fread(students, sizeof(Student), MAX_STUDENTS, fp):批量读取数据
  3. 指针使用

    • inputStudent(&students[count]):传递结构体指针,避免复制开销
    • s->id:使用箭头运算符访问结构体成员
  4. 字符串比较

    • strcmp(students[i].id, id) == 0:比较字符串是否相等
  5. 缓冲区处理

    • getchar():清除scanf留下的换行符,防止影响后续输入

4.2 实验题目:链表操作

题目要求:实现单向链表的创建、插入、删除和遍历操作。

解题思路

  • 链表节点结构:包含数据和指向下一个节点的指针
  • 动态内存分配:使用malloc和free管理内存
  • 边界情况处理:空链表、头节点操作、尾节点操作

参考代码

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

// 链表节点结构
typedef struct Node {
    int data;
    struct Node *next;
} Node;

// 函数声明
Node* createNode(int data);
void insertAtHead(Node **head, int data);
void insertAtTail(Node **head, int data);
void deleteNode(Node **head, int data);
void displayList(Node *head);
void freeList(Node **head);

int main() {
    Node *head = NULL;  // 链表头指针
    
    // 创建链表:10 -> 20 -> 30
    insertAtTail(&head, 10);
    insertAtTail(&head, 20);
    insertAtTail(&head, 30);
    
    printf("初始链表:");
    displayList(head);  // 输出:10 -> 20 -> 30 -> NULL
    
    // 在头部插入5
    insertAtHead(&head, 5);
    printf("头部插入5后:");
    displayList(head);  // 输出:5 -> 10 -> 20 -> 30 -> NULL
    
    // 删除节点20
    deleteNode(&head, 20);
    printf("删除20后:");
    displayList(head);  // 输出:5 -> 10 -> 30 -> NULL
    
    // 释放链表内存
    freeList(&head);
    
    return 0;
}

// 创建新节点
Node* createNode(int data) {
    Node *newNode = (Node*)malloc(sizeof(Node));
    if (newNode == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

// 头部插入
void insertAtHead(Node **head, int data) {
    Node *newNode = createNode(data);
    newNode->next = *head;  // 新节点指向原头节点
    *head = newNode;        // 更新头指针
}

// 尾部插入
void insertAtTail(Node **head, int data) {
    Node *newNode = createNode(data);
    
    if (*head == NULL) {
        *head = newNode;
        return;
    }
    
    Node *current = *head;
    while (current->next != NULL) {
        current = current->next;
    }
    current->next = newNode;
}

// 删除节点
void deleteNode(Node **head, int data) {
    Node *current = *head;
    Node *prev = NULL;
    
    while (current != NULL && current->data != data) {
        prev = current;
        current = current->next;
    }
    
    if (current == NULL) {
        printf("未找到数据 %d\n", data);
        return;
    }
    
    if (prev == NULL) {
        // 删除头节点
        *head = current->next;
    } else {
        // 删除中间或尾节点
        prev->next = current->next;
    }
    
    free(current);
}

// 遍历链表
void displayList(Node *head) {
    Node *current = head;
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

// 释放链表内存
void freeList(Node **head) {
    Node *current = *head;
    Node *next;
    
    while (current != NULL) {
        next = current->next;
        free(current);
        current = next;
    }
    
    *head = NULL;
}

代码解析

  1. 二级指针的使用

    • void insertAtHead(Node **head, int data):使用二级指针是因为需要修改头指针本身
    • *head = newNode;:通过解引用修改外部头指针
  2. 内存管理

    • malloc(sizeof(Node)):动态分配节点内存
    • free(current):删除节点时释放内存,防止内存泄漏
  3. 链表遍历

    • while (current->next != NULL):找到尾节点
    • while (current != NULL && current->data != data):查找特定节点
  4. 边界情况处理

    • 空链表:if (*head == NULL)
    • 删除头节点:if (prev == NULL)
    • 未找到节点:if (current == NULL)

五、高级算法实验:提升编程思维

5.1 实验题目:递归实现汉诺塔

题目要求:使用递归方法解决汉诺塔问题,打印移动步骤。

解题思路

  • 递归思想:将复杂问题分解为相似的子问题
  • 汉诺塔规律:将n个盘子从A移到C,需要先将n-1个盘子移到B,再将第n个盘子移到C,最后将n-1个盘子移到C
  • 基准情况:n=1时直接移动

参考代码

#include <stdio.h>

// 函数声明
void hanoi(int n, char source, char auxiliary, char target);

int main() {
    int n;
    
    printf("请输入汉诺塔的层数:");
    scanf("%d", &n);
    
    printf("\n移动步骤:\n");
    hanoi(n, 'A', 'B', 'C');
    
    return 0;
}

// 汉诺塔递归函数
void hanoi(int n, char source, char auxiliary, char target) {
    // 基准情况:只有一个盘子
    if (n == 1) {
        printf("%c -> %c\n", source, target);
        return;
    }
    
    // 步骤1:将n-1个盘子从source移到auxiliary(借助target)
    hanoi(n - 1, source, target, auxiliary);
    
    // 步骤2:将第n个盘子从source移到target
    printf("%c -> %c\n", source, target);
    
    // 步骤3:将n-1个盘子从auxiliary移到target(借助source)
    hanoi(n - 1, auxiliary, source, target);
}

代码解析

  1. 递归三要素

    • 基准情况:if (n == 1)
    • 递归调用:hanoi(n - 1, ...)
    • 问题规模缩小:n每次减1
  2. 参数含义

    • source:源柱
    • auxiliary:辅助柱
    • target:目标柱
  3. 执行过程示例(n=3):

    hanoi(3, A, B, C)
    ├── hanoi(2, A, C, B)
    │   ├── hanoi(1, A, B, C) → A -> C
    │   ├── A -> B
    │   └── hanoi(1, C, A, B) → C -> B
    ├── A -> C
    └── hanoi(2, B, A, C)
       ├── hanoi(1, B, C, A) → B -> A
       ├── B -> C
       └── hanoi(1, A, B, C) → A -> C
    
  4. 移动次数:2^n - 1次,时间复杂度O(2^n)

5.2 实验题目:动态规划解决背包问题

题目要求:0-1背包问题,给定物品重量和价值,以及背包容量,求最大价值。

解题思路

  • 动态规划:将问题分解为子问题,存储中间结果避免重复计算
  • 状态定义:dp[i][w]表示前i个物品在容量w下的最大价值
  • 状态转移方程:dp[i][w] = max(dp[i-1][w], dp[i-1][w-weight[i]] + value[i])

参考代码

#include <stdio.h>
#include <algorithm>

// 物品结构体
typedef struct {
    int weight;
    int value;
} Item;

// 动态规划求解背包问题
int knapsack(Item items[], int n, int capacity) {
    // dp[i][w]:前i个物品在容量w下的最大价值
    int dp[n + 1][capacity + 1];
    
    // 初始化第0行(没有物品)
    for (int w = 0; w <= capacity; w++) {
        dp[0][w] = 0;
    }
    
    // 填充DP表
    for (int i = 1; i <= n; i++) {
        for (int w = 0; w <= capacity; w++) {
            // 不选当前物品
            dp[i][w] = dp[i - 1][w];
            
            // 如果能装下当前物品,尝试选择
            if (items[i - 1].weight <= w) {
                int include = dp[i - 1][w - items[i - 1].weight] + items[i - 1].value;
                if (include > dp[i][w]) {
                    dp[i][w] = include;
                }
            }
        }
    }
    
    return dp[n][capacity];
}

int main() {
    Item items[] = {
        {2, 3},  // 重量2,价值3
        {3, 4},  // 重量3,价值4
        {4, 5},  // 重量4,价值5
        {5, 6}   // 重量5,价值6
    };
    int n = sizeof(items) / sizeof(items[0]);
    int capacity = 8;
    
    int maxValue = knapsack(items, n, capacity);
    printf("背包容量:%d\n", capacity);
    printf("最大价值:%d\n", maxValue);
    
    return 0;
}

代码解析

  1. DP表定义int dp[n + 1][capacity + 1]

    • 行:物品数量(0到n)
    • 列:背包容量(0到capacity)
  2. 初始化

    • dp[0][w] = 0:没有物品时价值为0
  3. 状态转移

    • dp[i][w] = dp[i - 1][w]:不选当前物品
    • include = dp[i - 1][w - weight[i]] + value[i]:选择当前物品
  4. 空间优化(一维数组):

int knapsack(Item items[], int n, int capacity) {
    int dp[capacity + 1] = {0};
    
    for (int i = 0; i < n; i++) {
        // 逆序遍历,防止重复选择
        for (int w = capacity; w >= items[i].weight; w--) {
            dp[w] = max(dp[w], dp[w - items[i].weight] + items[i].value);
        }
    }
    
    return dp[capacity];
}

六、调试技巧与最佳实践

6.1 常见编译错误及解决方案

错误类型1:语法错误

// 错误:缺少分号
int a = 10
printf("%d", a);  // 错误:expected ';' before 'printf'

// 解决方案:仔细检查每行末尾的分号

错误类型2:未声明标识符

// 错误:未包含头文件
printf("%d", sqrt(16));  // 错误:implicit declaration of function 'sqrt'

// 解决方案:添加 #include <math.h>

错误类型3:类型不匹配

// 错误:指针类型不匹配
int *p;
scanf("%d", p);  // 错误:未给指针分配内存

// 解决方案:使用取地址运算符或分配内存
scanf("%d", &a);
p = &a;

6.2 运行时错误调试

使用gdb调试

# 编译时加入调试信息
gcc -g program.c -o program

# 启动gdb
gdb ./program

# 常用命令
(gdb) break main          # 在main函数设置断点
(gdb) run                 # 运行程序
(gdb) next                # 单步执行
(gdb) print variable      # 查看变量值
(gdb) backtrace           # 查看调用栈
(gdb) quit                # 退出

使用Valgrind检测内存泄漏

valgrind --leak-check=full ./program

6.3 代码规范建议

  1. 命名规范

    • 变量名:小驼峰(studentName)或下划线(student_name)
    • 常量名:全大写(MAX_SIZE)
    • 函数名:小写加下划线(calculate_sum)
  2. 注释规范: “`c /*

    • 函数功能:计算两个整数的和
    • 参数:a - 第一个整数
    • b - 第二个整数
    • 返回值:两个整数的和 */ int add(int a, int b) { return a + b; }

    ”`

  3. 错误处理

    FILE *fp = fopen("file.txt", "r");
    if (fp == NULL) {
       perror("打开文件失败");
       return -1;
    }
    

七、总结与学习建议

7.1 核心知识点回顾

  1. 基础语法:变量、数据类型、运算符、输入输出
  2. 流程控制:if-else、switch、for、while、do-while
  3. 数组与字符串:一维/二维数组、字符串处理函数
  4. 指针:指针变量、指针运算、指针与数组、函数指针
  5. 函数:函数定义、参数传递、递归、回调
  6. 结构体:自定义数据类型、结构体指针
  7. 动态内存:malloc、free、内存泄漏预防
  8. 文件操作:fopen、fclose、fread、fwrite
  9. 高级算法:递归、动态规划

7.2 高效学习路径

阶段1:基础语法(1-2周)

  • 掌握基本数据类型和运算符
  • 熟练使用if、for、while
  • 理解数组和字符串操作

阶段2:指针与内存(2-3周)

  • 理解指针的本质(地址)
  • 掌握指针与数组的关系
  • 学会动态内存管理

阶段3:函数与模块化(1-2周)

  • 理解函数调用栈
  • 掌握参数传递机制(值传递 vs 地址传递)
  • 学会编写可复用的函数

阶段4:数据结构(2-3周)

  • 实现链表、栈、队列
  • 理解结构体的应用
  • 掌握基本算法

阶段5:综合应用(持续练习)

  • 完成实验指导书中的综合实验
  • 尝试解决实际问题
  • 阅读优秀开源代码

7.3 实验技巧总结

  1. 先设计后编码:画流程图、写伪代码
  2. 分模块测试:先测试单个函数,再整合
  3. 善用调试工具:gdb、printf调试法
  4. 阅读编译器错误信息:从第一个错误开始解决
  5. 代码复用:将常用功能封装成函数
  6. 版本控制:使用git管理代码版本

7.4 推荐练习题目

  1. 基础:计算器、成绩统计、素数判断
  2. 中级:学生管理系统、通讯录、贪吃蛇游戏
  3. 高级:迷宫求解、排序算法可视化、简单Shell

通过系统学习和大量实践,你一定能够掌握C程序设计的精髓,顺利完成上海交大的实验要求,并为后续的计算机专业课程打下坚实基础。记住,编程能力的提升来自于不断的编码、调试和思考,祝你学习顺利!