C语言入坑记录

#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),而实参是普通变量时,需要用 & 取地址传递

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 语言只有值传递,但可以通过指针模拟引用传递的效果。

原理分析

  1. 值传递(Pass by Value)
  • 当传递结构体变量时(如 SeqList L),函数会创建该结构体的一个完整副本
  • 在函数内部修改的是副本,不会影响原始变量
  • 因此需要返回修改后的副本并赋值给原始变量
  1. 指针传递(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]

为什么推荐使用指针写法?

  1. 性能更好
  • 避免复制整个结构体(特别是当结构体很大时)
  • 只传递指针(4或8字节)而不是整个结构体(104字节:100元素+4长度)
  1. 内存效率更高
  • 不需要创建临时副本,节省栈空间
  1. 代码更简洁
  • 不需要返回值
  • 不需要赋值操作
  1. 一致性更好
  • 与插入(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 LSeqList *L
内存使用创建完整副本只传递指针(4/8字节)
修改对象修改副本直接修改原始数据
是否需要返回必须返回修改后的副本不需要返回
调用方式List = Converts(List);Converts(&List);
性能较差(大结构体复制开销大)更优
内存安全栈溢出风险(大结构体)更安全

在实际工程中,特别是当结构体较大时,指针传递是更优的选择。它避免了不必要的内存复制,提高了效率,并且代码更加简洁。这也是为什么你看到 InsertListDeleteList 函数使用指针的原因——它们需要修改原始顺序表。

在 C 语言中,函数返回类型的选择取决于函数的功能需求和设计意图。你提到的三种返回类型(voidDataTypeSeqList)各有特定的使用场景和区别:

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);  // 必须接收返回值并覆盖原变量

三者的关键区别

特性voidDataTypeSeqList
返回值单个元素整个结构体
内存开销最小大(复制整个结构体)
修改方式通过指针直接修改原数据通过指针修改原数据返回修改后的副本
调用处理直接调用可接收返回值必须接收并覆盖原变量
适用操作插入、修改等删除(返回被删元素)需要返回新表的状态操作
效率低(大结构体复制)

设计原则与最佳实践

  1. 优先使用指针和void
  • 当函数需要修改结构体时,使用指针参数和void返回类型
  • 避免不必要的数据复制
   // 推荐:使用指针直接修改
   void Converts(SeqList *L) {
       // 直接操作原表
   }
  1. 按需返回特定值
  • 当需要返回单个元素信息时使用DataType
   // 返回被删除的元素
   DataType DeleteList(SeqList *L, int i) {...}
  1. 避免返回整个结构体
  • SeqList返回类型应谨慎使用
  • 仅当无法使用指针且结构体较小时适用
  • 在嵌入式系统或内存受限环境中尤其要避免
  1. 一致性设计
  • 在整个程序中保持一致的参数传递风格
  • 推荐统一使用指针方式操作结构体

修改建议(统一使用指针风格)

// 插入: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) {...}

为什么这样设计更合理?

  1. 内存效率
  • 避免复制100个元素的数组(404字节 vs 8字节指针)
  • 对大型数据结构尤为重要
  1. 性能优化
  • 指针操作比复制整个结构体快得多
  • 减少函数调用开销
  1. 代码一致性
  • 所有修改操作都使用指针参数
  • 非修改操作(如打印)使用值传递
  1. 避免错误
  • 防止忘记接收返回值导致的错误
   // 错误示例:忘记接收返回值
   Converts(List);  // 无效操作!
   // 正确做法
   Converts(&List);  // 直接修改
  1. 真实世界实践
  • 标准库函数如qsort()使用指针修改数据
  • Linux内核等大型项目广泛使用指针操作数据结构

总之,函数返回类型的选择反映了函数的设计意图:是否修改原始数据?需要返回什么信息?理解这些区别有助于编写更高效、更健壮的代码。

  1. 单链表:为什么选择带头结点更好呢?

我们选择带头结点的方式,这样在插入和删除时对第一个结点的处理与其他结点相同。

主要问题:

1. 函数定义中的参数不完整(缺少类型声明)且参数传递方式不正确。

2. 在main函数中调用CreateListF时,传递的参数是head(未初始化的LinkList)、p的地址以及ch(未初始化的char)。

3. 函数CreateListF中,参数head和p的传递方式不正确,特别是p是一个指向结点的指针,而函数中却要修改指针本身(即让p指向新结点),所以应该传递指针的指针(或指针的引用)才能修改调用处的指针变量。

但是,观察函数内部,实际上p是在函数内部分配新结点,然后通过头插法插入链表,而head是链表的头指针,也需要被修改。所以,这两个参数都需要通过指针的指针(或指针的引用)来传递,以便在函数内部修改调用处的变量。

但是,我们注意到函数CreateListF返回的是head,所以我们可以通过返回值来返回头指针。

改进建议:

1. 将CreateListF函数重新设计,不需要传递head和p,而是通过返回值返回头指针。

2. 在函数内部,将p作为局部指针变量,ch也作为局部变量。

3. 函数只接收一个参数(如果需要指定输入来源,可以传递一个文件指针,但这里默认从stdin读取)或者无参数。

发表回复