一、乱码问题
#include <windows.h>
SetConsoleOutputCP(65001);
Windows 控制台默认是 GBK (936),但 VS Code 的终端如果用 UTF-8 输出就会显示乱码。
有两种处理方式:
方法 A:让终端用 UTF-8 显示
在你的 C 代码 main() 开头加:
c复制编辑#include <windows.h>
int main() {
SetConsoleOutputCP(65001); // 设置控制台输出为 UTF-8
printf("你好,世界!\n");
return 0;
}
注:这样做之后,控制台输出的汉字确实不乱码了,但当报错时,控制台给出的error信息中,代码中的中文注释仍显示为乱码,多次尝试之后解决了:
方法:在控制台运行以下命令:chcp 65001
二、形参、实参对应的问题
函数定义时的形参用的*L,调用时用的实参要用 &L ,而不是L
原因:在C语言中,形参使用 *L(指针)时,实参通常需要传递变量的地址(即 &L),但具体是否使用 & 取决于实参本身的类型。以下是详细说明:
1. 形参是指针(*L),实参用 &L 的情况
当函数形参是一个指针(例如 int *L),而实参是普通变量时,需要用 & 取地址传递
void modify(int *L) {
*L = 100; // 通过指针修改实参的值
}
int main() {
int L = 10;
modify(&L); // 传递L的地址
printf(“%d\n”, L); // 输出100
return 0;
}
2. 形参是指针(*L),实参直接传指针(无需 &)
如果实参本身已经是指针变量,则直接传递指针,无需再加 &:
void allocateMemory(int *L) {
*L = 200;
}
int main() {
int *ptr = malloc(sizeof(int)); // ptr本身是指针
allocateMemory(ptr); // 直接传递指针,无需&
printf("%d\n", *ptr); // 输出200
free(ptr);
return 0;
}
三、赋值与初始化的区别

为什么C不可以? 这说法好有说服力的感觉。

四、为什么有的函数需要返回结构体,有的函数不需要,值传递与引用传递的区别?

在 C 语言中,理解值传递(pass by value)和引用传递(pass by reference)是理解这个问题的关键。C 语言只有值传递,但可以通过指针模拟引用传递的效果。
原理分析
- 值传递(Pass by Value):
- 当传递结构体变量时(如
SeqList L),函数会创建该结构体的一个完整副本 - 在函数内部修改的是副本,不会影响原始变量
- 因此需要返回修改后的副本并赋值给原始变量
- 指针传递(Pass by Pointer):
- 当传递结构体指针时(如
SeqList *L),函数得到的是原始结构体的内存地址 - 在函数内部通过指针直接修改原始结构体的内容
- 不需要返回值,因为修改的是原始数据
具体比较
值传递写法(需要返回修改后的副本):
SeqList Converts(SeqList L) {
// 修改的是 L 的副本
return L; // 必须返回修改后的副本
}
// 调用时必须赋值
List = Converts(List);
内存图解:
调用前:
原始 List [data: 1,2,3,4,5 | length:5]
调用时:
创建副本 L [data: 1,2,3,4,5 | length:5]
函数内修改:
副本 L [data: 5,4,3,2,1 | length:5]
返回后:
原始 List = 副本 L
指针传递写法(直接修改原始数据):
void Converts(SeqList *L) {
// 通过指针直接修改原始 List
// 不需要返回值
}
// 调用时直接修改
Converts(&List);
内存图解:
调用前:
List [data: 1,2,3,4,5 | length:5]
调用时:
传递指针 &List(内存地址)
函数内修改:
通过指针直接修改原始 List [data: 5,4,3,2,1 | length:5]
为什么推荐使用指针写法?
- 性能更好:
- 避免复制整个结构体(特别是当结构体很大时)
- 只传递指针(4或8字节)而不是整个结构体(104字节:100元素+4长度)
- 内存效率更高:
- 不需要创建临时副本,节省栈空间
- 代码更简洁:
- 不需要返回值
- 不需要赋值操作
- 一致性更好:
- 与插入(InsertList)和删除(DeleteList)函数风格一致
修改建议(推荐使用指针写法):
// 使用指针直接修改原表
void Converts(SeqList *L) {
if (L == NULL || L->length <= 1) return;
DataType x;
int k = L->length / 2;
for (int i = 0; i < k; i++) {
x = L->data[i];
L->data[i] = L->data[L->length - i - 1];
L->data[L->length - i - 1] = x;
}
}
// 调用方式
Converts(&List); // 直接修改原始List
关键点总结
| 特性 | 值传递写法 | 指针传递写法 |
|---|---|---|
| 参数类型 | SeqList L | SeqList *L |
| 内存使用 | 创建完整副本 | 只传递指针(4/8字节) |
| 修改对象 | 修改副本 | 直接修改原始数据 |
| 是否需要返回 | 必须返回修改后的副本 | 不需要返回 |
| 调用方式 | List = Converts(List); | Converts(&List); |
| 性能 | 较差(大结构体复制开销大) | 更优 |
| 内存安全 | 栈溢出风险(大结构体) | 更安全 |
在实际工程中,特别是当结构体较大时,指针传递是更优的选择。它避免了不必要的内存复制,提高了效率,并且代码更加简洁。这也是为什么你看到 InsertList 和 DeleteList 函数使用指针的原因——它们需要修改原始顺序表。
五、为什么有的函数类型是void, 有的函数类型是DataType, 有的是SeqList? 有什么区别吗?

在 C 语言中,函数返回类型的选择取决于函数的功能需求和设计意图。你提到的三种返回类型(void、DataType、SeqList)各有特定的使用场景和区别:
1. void 返回类型(无返回值)
使用场景:当函数不需要返回任何数据,只执行某些操作时使用
特点:
- 函数执行操作但不产生结果值
- 通常用于修改传入的参数或执行副作用(如打印)
- 调用者不需要接收返回值
代码示例:
void InsertList(SeqList *L, int i, DataType x) {
// 通过指针直接修改传入的顺序表
// 不需要返回值
}
调用方式:
InsertList(&List, 2, 8); // 直接调用,不接收返回值
2. DataType 返回类型
使用场景:当函数需要返回单个数据元素时使用
特点:
- 返回顺序表中的特定元素(如被删除的元素)
- 返回类型与顺序表元素类型一致
- 调用者可以接收并使用这个返回值
代码示例:
DataType DeleteList(SeqList *L, int i) {
DataType x = L->data[i-1];
// 删除操作...
return x; // 返回被删除的元素
}
调用方式:
int deleted = DeleteList(&List, 2); // 接收返回值
printf("删除的元素:%d\n", deleted);
3. SeqList 返回类型
使用场景:当函数需要返回整个修改后的顺序表时使用
特点:
- 返回一个完整的数据结构副本
- 在值传递(非指针)方式下使用
- 调用者需要接收并覆盖原变量
- 内存开销大(复制整个结构体)
代码示例:
SeqList Converts(SeqList L) {
// 修改副本
return L; // 返回修改后的副本
}
调用方式:
List = Converts(List); // 必须接收返回值并覆盖原变量
三者的关键区别
| 特性 | void | DataType | SeqList |
|---|---|---|---|
| 返回值 | 无 | 单个元素 | 整个结构体 |
| 内存开销 | 最小 | 小 | 大(复制整个结构体) |
| 修改方式 | 通过指针直接修改原数据 | 通过指针修改原数据 | 返回修改后的副本 |
| 调用处理 | 直接调用 | 可接收返回值 | 必须接收并覆盖原变量 |
| 适用操作 | 插入、修改等 | 删除(返回被删元素) | 需要返回新表的状态操作 |
| 效率 | 高 | 高 | 低(大结构体复制) |
设计原则与最佳实践
- 优先使用指针和
void:
- 当函数需要修改结构体时,使用指针参数和
void返回类型 - 避免不必要的数据复制
// 推荐:使用指针直接修改
void Converts(SeqList *L) {
// 直接操作原表
}
- 按需返回特定值:
- 当需要返回单个元素信息时使用
DataType
// 返回被删除的元素
DataType DeleteList(SeqList *L, int i) {...}
- 避免返回整个结构体:
SeqList返回类型应谨慎使用- 仅当无法使用指针且结构体较小时适用
- 在嵌入式系统或内存受限环境中尤其要避免
- 一致性设计:
- 在整个程序中保持一致的参数传递风格
- 推荐统一使用指针方式操作结构体
修改建议(统一使用指针风格)
// 插入:void + 指针参数
void InsertList(SeqList *L, int i, DataType x) {...}
// 删除:DataType + 指针参数(返回被删元素)
DataType DeleteList(SeqList *L, int i) {...}
// 逆置:void + 指针参数
void Converts(SeqList *L) {
DataType x;
int k = L->length / 2;
for(int i=0; i<k; i++) {
x = L->data[i];
L->data[i] = L->data[L->length-i-1];
L->data[L->length-i-1] = x;
}
}
// 打印:void + 值传递(不修改原表)
void PrintSeqList(SeqList L) {...}为什么这样设计更合理?
- 内存效率:
- 避免复制100个元素的数组(404字节 vs 8字节指针)
- 对大型数据结构尤为重要
- 性能优化:
- 指针操作比复制整个结构体快得多
- 减少函数调用开销
- 代码一致性:
- 所有修改操作都使用指针参数
- 非修改操作(如打印)使用值传递
- 避免错误:
- 防止忘记接收返回值导致的错误
// 错误示例:忘记接收返回值
Converts(List); // 无效操作!
// 正确做法
Converts(&List); // 直接修改- 真实世界实践:
- 标准库函数如
qsort()使用指针修改数据 - Linux内核等大型项目广泛使用指针操作数据结构
总之,函数返回类型的选择反映了函数的设计意图:是否修改原始数据?需要返回什么信息?理解这些区别有助于编写更高效、更健壮的代码。
六、单链表
- 单链表:为什么选择带头结点更好呢?
我们选择带头结点的方式,这样在插入和删除时对第一个结点的处理与其他结点相同。
七、函数参数的确定思路
主要问题:
1. 函数定义中的参数不完整(缺少类型声明)且参数传递方式不正确。
2. 在main函数中调用CreateListF时,传递的参数是head(未初始化的LinkList)、p的地址以及ch(未初始化的char)。
3. 函数CreateListF中,参数head和p的传递方式不正确,特别是p是一个指向结点的指针,而函数中却要修改指针本身(即让p指向新结点),所以应该传递指针的指针(或指针的引用)才能修改调用处的指针变量。
但是,观察函数内部,实际上p是在函数内部分配新结点,然后通过头插法插入链表,而head是链表的头指针,也需要被修改。所以,这两个参数都需要通过指针的指针(或指针的引用)来传递,以便在函数内部修改调用处的变量。
但是,我们注意到函数CreateListF返回的是head,所以我们可以通过返回值来返回头指针。
而对于p,在函数内部是局部使用的,并不需要返回给调用者(因为p在函数中只是用来临时指向新结点的,调用者并不需要这个p)。
因此,我们可以将p设为函数内的局部变量,而不需要作为参数传递。
另外,参数ch是字符,在调用函数前我们并没有读入字符,而且函数内部已经调用了getchar,所以ch应该作为函数内的局部变量。
改进建议:
1. 将CreateListF函数重新设计,不需要传递head和p,而是通过返回值返回头指针。
2. 在函数内部,将p作为局部指针变量,ch也作为局部变量。
3. 函数只接收一个参数(如果需要指定输入来源,可以传递一个文件指针,但这里默认从stdin读取)或者无参数。