Skip to content

约 4748 个字 预计阅读时间 24 分钟 共被读过

lec03-C基础2

理论框架

  1. 分层解析核心模型 (以指针为例)

    • 基础假设:
      • 内存可以被视为一个巨大的字节数组,每个字节都有一个唯一的地址
      • 指针是一个变量,其值是另一个变量的地址。
      • 类型决定了指针所指向的内存区域的大小和如何解释。
    • 数学表达:
      • 地址是无符号整数。
      • 指针运算基于指针指向的数据类型的大小进行。例如,int *p; p++; 将使 p 的值增加 sizeof(int) 的大小。
      • sizeof() 操作符返回给定类型或变量的字节大小。
    • 应用场景:
      • 动态内存分配:使用 malloc()free() 来管理堆上的内存。
      • 数据结构: 用于创建链表、树等复杂的数据结构。
      • 函数参数传递:通过传递指针来修改函数外部的变量。
      • 数组访问: 数组名可以被视为指向数组首元素的指针,从而可以使用指针进行数组访问.

    教师强调:指针是 C 语言中最大的错误来源,必须谨慎使用。

学术图谱

  1. 领域发展树形图
    • 奠基理论:
      • 冯·诺依曼架构 (Von Neumann Architecture):现代计算机的基本架构,内存统一编址。
    • 分支演进:
      • 内存管理:
        • 静态内存分配:在编译时分配,例如全局变量和静态变量。
        • 栈内存分配:在函数调用时分配,用于局部变量和函数参数。
        • 动态内存分配:在运行时使用 malloc()free() 来分配和释放堆上的内存。
      • 数据结构:
        • 数组: 连续存储的相同类型的元素集合。
        • 链表: 使用指针连接的节点序列。
        • : 使用指针连接的节点层次结构。
    • 最新突破:
      • 自动内存管理: 垃圾回收机制,如 Java 和 Python 中使用的机制,用于自动回收不再使用的内存。
      • 内存安全: 新的编程语言如 Rust 提供了更安全的内存管理机制,减少了指针错误。
    • 学派争议点:
      • 手动内存管理 vs. 自动内存管理: C 的手动内存管理提供了更高的性能和控制,但也更容易出错。自动内存管理更安全,但可能会牺牲一些性能。
      • 指针的必要性 vs. 安全性风险: 指针是 C 语言的核心特性,但也是许多错误的根源,新的编程语言倾向于使用引用或更安全的抽象来替代指针。

章节精析

指针 (Pointers)

  • 核心内容
    • 内存地址的概念以及地址和存储在地址中的值的区别。
    • 指针是一种存储内存地址的变量。
    • 不同类型的指针指向不同类型的数据,void * 是通用指针。
    • NULL 指针是一个特殊的指针值,表示指针不指向任何有效的内存位置。
    • 指针可以指向任何类型的数据,包括函数。

C 指针的危险性 (C Pointer Dangers)

  • 核心内容
    • 指针在声明时只分配了存储地址的空间,并没有分配存储数据的空间。
    • 局部变量不会被自动初始化,可能包含垃圾值,指针也一样,使用未初始化的指针是危险的。
    • 必须小心地管理指针,以避免程序错误。

指针与结构体 (Pointers and Structures)

  • 核心内容
    • 指针可以指向结构体,可以使用 . 运算符访问结构体的成员,或使用 -> 运算符通过指针访问结构体的成员。
    • 可以使用指针复制结构体,也可以使用 * 解引用指针来访问结构体。

C 中使用指针的原因 (Why use Pointers in C)

  • 核心内容
    • 可以通过传递指针来避免复制大型数据,从而提高效率。
    • C 语言中的指针类似于 Java 中的引用。
    • 指针可以使代码更简洁。
    • 指针是 C 语言中最大的错误来源,必须谨慎使用。

指向不同大小的对象 (Pointing to Different Size Objects)

  • 核心内容
    • 现代计算机是字节寻址的。
    • 指针的类型声明告诉编译器如何通过指针访问内存。
    • 为了提高内存访问效率,通常需要进行“字对齐”。

sizeof() 操作符 (sizeof() operator)

  • 核心内容
    • sizeof() 操作符返回给定类型或变量的字节大小。
    • sizeof() 包括为了对齐而添加的填充字节。
    • sizeof() 不是一个函数,而是一个编译时操作符。

指针运算 (Pointer Arithmetic)

  • 核心内容
    • 指针运算会根据指针指向的数据类型的大小进行调整。
    • 指针运算应该谨慎使用,可能导致未定义的行为。
    • 编译器在处理指针运算时,会考虑指针指向的数据类型的大小。

修改指针参数 (Changing a Pointer Argument)

  • 核心内容
    • 要修改函数外部的指针,必须传递一个指向指针的指针 (即二级指针)。
    • 在函数内部修改一级指针形参不会影响函数外部实参指针的值。

指针的结论 (Conclusion on Pointers)

  • 核心内容
    • 指针是 C 语言中对内存地址的抽象。
    • * 操作符用于解引用指针,& 操作符用于获取变量的地址。
    • C 语言高效但不安全,需要程序员自己负责内存管理。

结构体再探讨 (Structures Revisited)

  • 核心内容
    • 结构体是 C 语言中组织数据的一种方式。
    • 结构体在内存中会进行对齐和填充,以满足硬件的访问要求。
    • sizeof(struct) 会返回结构体的总字节大小,包括填充字节。

联合体 (Unions)

  • 核心内容
    • 联合体是一种允许在相同的内存位置存储不同类型数据的结构。
    • 联合体的大小取决于其最大的成员的大小。
    • 对联合体成员的赋值会覆盖之前的值。

C 数组 (C Arrays)

  • 核心内容
    • 数组是相同类型的元素的连续存储的集合。
    • 数组的大小在声明时就必须是静态的。
    • 数组变量可以看作是指向数组首元素的指针。

数组名称/指针二重性 (Array Name / Pointer Duality)

  • 核心内容
    • 数组变量可以看作是指向数组首元素的指针。
    • 数组名和指针在大部分情况下可以互换使用。
    • 可以使用指针运算来访问数组元素。

数组和指针 (Arrays and Pointers)

  • 核心内容
    • 数组可以作为指向首元素的指针传递给函数。
    • 当数组作为参数传递给函数时,数组的大小会丢失。
    • 应该尽量避免指针运算,避免使用 ar++ 这样的代码。

C 数组的局限性 (C Arrays are Very Primitive)

  • 核心内容
    • C 数组不记录自身的长度。
    • C 数组不会进行边界检查,可能导致越界访问。
    • 必须将数组和数组的大小传递给需要操作数组的函数。

C 字符串 (C Strings)

  • 核心内容
    • C 字符串是以 null 结尾的字符数组。
    • null 字符 \00 用于标记字符串的结束。
    • strlen() 函数不计算 null 字符。

使用定义常量 (Use Defined Constants)

  • 核心内容
    • 应该使用常量来定义数组的大小,避免重复使用字面值。
    • 使用常量可以提高代码的可读性和可维护性。
    • 应该使用 < 而不是 <= 来进行循环条件判断。

数组和指针的 sizeof() (Arrays and Pointers sizeof())

  • 核心内容
    • 当数组作为参数传递给函数时,sizeof() 返回指针的大小而不是数组的大小。
    • 在函数内部,数组名实际上是指针,因此 sizeof(array) 返回指针的大小。

数组、结构体和指针 (Arrays and Structures and Pointers)

  • 核心内容
    • 结构体可以包含指针和数组。
    • 需要使用 malloc() 为结构体和结构体中的指针分配内存。

代码示例 (Some Code Examples)

  • 核心内容
    • 展示了如何通过指针访问结构体成员和数组元素。
    • 展示了如何通过指针赋值来修改结构体成员。

数组越界案例:Heartbleed漏洞 (When Arrays Go Bad: Heartbleed)

  • 核心内容
    • Heartbleed 漏洞是由于未检查数组长度而导致的内存泄漏。
    • 该漏洞允许攻击者读取服务器内存中的敏感信息。

strlen() 的简洁实现 (Concise strlen())

  • 核心内容
    • 展示了一个简洁的 strlen() 函数实现。
    • 如果字符串没有 null 终止符,该函数将无法正常工作。

main() 函数中的参数 (Arguments in main())

  • 核心内容
    • main() 函数可以接受命令行参数。
    • argc 表示命令行参数的个数,argv 是一个指向命令行参数字符串的指针数组。

Endianness (字节序)

  • 核心内容
    • 字节序是指多字节数据在内存中的存储顺序,分为大端和小端。
    • 大端序将高位字节存储在低地址,小端序将低位字节存储在低地址。
    • 字节序依赖于硬件架构。

Endianness 的应用 (Endianness and You)

  • 核心内容
    • 在网络编程中,需要考虑字节序,因为网络字节序是大端序。
    • 可以使用 ntohs()htons()ntohl()htonl() 等函数进行字节序转换。

C 内存管理 (C Memory Management)

  • 核心内容
    • C 语言中的内存分配分为静态分配、栈分配和堆分配。
    • 为了讨论的简单化,假设程序可以访问所有内存。

C 内存管理的区域 (C Memory Management)

  • 核心内容
    • 程序地址空间包含栈、堆、静态数据和代码段。
    • 栈向下增长,堆向上增长。
    • 静态数据和代码段在程序启动时加载,大小固定。

变量的分配位置 (Where are Variables Allocated?)

  • 核心内容
    • 在函数外部声明的变量存储在静态存储区。
    • 在函数内部声明的变量存储在栈上。
    • 当函数返回时,栈上的局部变量会被释放。

栈 (The Stack)

  • 核心内容
    • 每次函数调用都会在栈上分配新的栈帧。
    • 栈帧包含返回地址、参数和局部变量。
    • 栈使用连续的内存块,栈指针指向栈帧的起始位置。

栈的动画演示 (Stack Animation)

  • 核心内容
    • 栈是一种后进先出 (LIFO) 的数据结构。
    • 栈向下增长,栈指针指向栈顶。

堆的管理 (Managing the Heap)

  • 核心内容
    • C 语言提供了 malloc()calloc()free()realloc() 等函数来管理堆上的内存。
    • malloc() 分配未初始化的内存,calloc() 分配并初始化为零的内存。
    • free() 释放之前分配的内存,realloc() 重新分配内存大小。

malloc() 函数详解 (Malloc())

  • 核心内容
    • malloc() 函数用于分配指定大小的未初始化内存。
    • malloc() 返回一个指向分配内存的 void * 指针,如果分配失败则返回 NULL
    • 必须使用 sizeof() 操作符来确保代码的跨平台可移植性。
    • 必须使用类型转换将 void * 转换为适当的指针类型。

free() 函数详解 (And then free())

  • 核心内容
    • free() 函数用于释放之前由 malloc() 分配的内存。
    • 必须将原始的 malloc() 返回的地址传递给 free() 函数。
    • 释放内存之后,不要再使用该指针,否则会导致错误。

动态内存的使用 (Using Dynamic Memory)

  • 核心内容
    • 展示了如何使用 malloc()free() 创建和管理动态链表节点。
    • 展示了如何使用递归函数来创建二叉树。

总结 (Observations)

  • 核心内容
    • 静态存储和代码段易于管理,因为它们的大小固定。
    • 栈内存管理相对简单,因为它是 LIFO 的。
    • 堆内存管理复杂,需要手动分配和释放,容易出现内存泄漏、重复释放、使用已释放的内存等问题。

内存错误案例:忘记释放 (When Memory Goes Bad: Failure To Free)

  • 核心内容
    • 内存泄漏是由于忘记释放已分配的内存而导致的。
    • 内存泄漏的早期症状不明显,但最终会导致性能下降和程序崩溃。

内存错误案例:数组越界 (When Memory Goes Bad: Writing off the end of arrays...)

  • 核心内容
    • 数组越界写入会覆盖其他内存区域,导致数据损坏或程序崩溃。
    • 数组越界写入会破坏 malloc() 函数使用的内部数据结构,导致后续内存分配错误。

内存错误案例:返回栈中指针 (When Memory Goes Bad: Returning Pointers into the Stack)

  • 核心内容
    • 返回指向栈内存的指针是危险的,因为栈内存会在函数返回后被释放。
    • 对返回的栈内存的访问会导致未定义的行为,包括数据损坏和程序崩溃。

内存错误案例:使用已释放内存 (When Memory Goes Bad: Use After Free)

  • 核心内容
    • 使用已释放的指针会导致未定义的行为,因为该内存可能已被分配给其他用途。
    • 对已释放内存的读取可能导致获取错误的数据,而对已释放内存的写入可能导致程序崩溃。

内存错误案例:忘记 realloc 可能移动数据 (When Memory Goes Bad: Forgetting Realloc Can Move Data...)

  • 核心内容
    • realloc() 函数可能会移动内存块,导致之前的指针失效。
    • 必须更新所有指向该内存块的指针,否则会导致数据损坏和程序崩溃。

内存错误案例:释放错误内存 (When Memory Goes Bad: Freeing the Wrong Stuff...)

  • 核心内容
    • 释放未通过 malloc() 分配的内存会导致 malloc/free 内部数据结构混乱,导致程序崩溃。
    • 释放错误的内存会导致内存损坏和程序崩溃。

内存错误案例:重复释放 (When Memory Goes Bad: Double-Free...)

  • 核心内容
    • 重复释放同一块内存会导致内存损坏和程序崩溃。
    • 重复释放的内存可能已被分配给其他用途,导致程序行为异常。

Valgrind 工具介绍 (And Valgrind...)

  • 核心内容
    • Valgrind 是一个用于检测内存错误的工具。
    • Valgrind 可以检测内存泄漏、重复释放、数组越界写入等错误。
    • 必须在调试 C 代码时使用 Valgrind 。

结论 (And In Conclusion, …)

  • 核心内容
    • C 语言使用静态数据区、栈和堆来管理内存 。
    • 堆内存是 C 代码中最大的错误来源 。

思辨空间

  1. 课堂讨论中的关键辩题

    | 辩题 | 正方观点 | 反方观点 | | :--------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------- | | C 的手动内存管理 vs. 自动内存管理 | C 的手动内存管理提供了更高的性能和控制,可以进行更精细的内存优化。 | 手动内存管理容易导致内存泄漏和悬挂指针等错误,难以调试,增加开发难度。 | | 指针的必要性 vs. 安全性风险 | 指针是 C 语言的核心特性,允许直接操作内存,提高效率,适用于系统级编程和嵌入式开发。 | 指针容易引入错误,难以调试,现代编程语言使用更安全的引用和抽象来替代指针。 | | 数组边界检查的必要性 vs. 性能开销 | 数组边界检查可以防止越界访问导致的错误,提高程序的安全性。 | 数组边界检查会增加程序的运行开销,在性能敏感的应用中可能会成为瓶颈。 C 语言为了效率牺牲了安全性。 | | 使用 Valgrind 等工具进行调试的必要性 | Valgrind 等工具可以检测内存错误,提高代码的健壮性,是调试 C 代码不可或缺的工具。 | 使用 Valgrind 会显著降低程序的运行速度,会增加调试时间,但对于 C 程序的正确性来说是必要的。 | | C 作为系统级编程语言的价值 vs. 其他语言 | C 语言可以进行底层硬件操作,具有高性能,适用于开发操作系统、驱动程序等系统软件,且可以最大程度控制硬件。 | 现代语言(如 Rust, Go 等)在保证性能的同时,提供了更高级的抽象和更安全的内存管理机制,更适合用于系统级编程,可以避免 C 语言的常见问题。 |

增值模块

  1. 认知脚手架

    • 知识迁移地图:

      • 指针: 理解指针有助于理解动态内存分配、数据结构、函数参数传递等重要概念,为编写高效、灵活的 C 代码打下基础。
      • 内存管理: 理解 C 语言的内存管理机制有助于理解操作系统和计算机体系结构,为后续学习操作系统和编译原理打下基础。
      • 数组和指针的关系: 理解数组和指针的联系可以帮助理解内存布局和访问方式,为高效处理数据提供基础。
      • 动态内存分配: 理解动态内存分配的原理可以帮助创建动态数据结构,提高程序的灵活性和效率。
        2. 学术预警系统
    • 高频考点:

      • 指针的概念和用法。
      • ★★ 指针运算和类型转换。
      • ★★ 动态内存分配和释放。
      • ★★★ 内存泄漏、悬挂指针、重复释放等内存错误。
      • ★★★★ 如何使用 Valgrind 进行内存调试。
    • 常见论证误区:

      • 未初始化指针: 使用未初始化的指针会导致未定义行为。
      • 忘记释放内存: 忘记释放 malloc() 分配的内存会导致内存泄漏。
      • 使用释放后的内存: 访问已通过 free() 释放的内存会导致未定义行为。
      • 数组越界: 访问超出数组边界的内存会导致未定义行为,可能导致数据损坏或程序崩溃。
      • 指针类型转换: 不恰当的指针类型转换可能导致未定义的行为。
        3. 教授思维透视
    • 论证偏好:

      • 强调 C 语言的底层特性,以及对内存管理的精细控制。
      • 重视动手实践,强调通过代码理解概念,而非死记硬背。
      • 强调调试的重要性,提倡使用 Valgrind 等工具来检测内存错误。

        • 学术倾向:
      • 认为 C 语言是学习计算机系统和底层原理的重要工具。

      • 认为虽然 C 语言的内存管理容易出错,但是仍然值得学习,因为它提供了对硬件的直接控制。
      • 强调学习 C 语言内存管理中遇到的各种陷阱,以避免未来出现类似错误。