约 4748 个字 预计阅读时间 24 分钟 共被读过 次
lec03-C基础2¶
理论框架¶
-
分层解析核心模型 (以指针为例)
- 基础假设:
- 内存可以被视为一个巨大的字节数组,每个字节都有一个唯一的地址。
- 指针是一个变量,其值是另一个变量的地址。
- 类型决定了指针所指向的内存区域的大小和如何解释。
- 数学表达:
- 地址是无符号整数。
- 指针运算基于指针指向的数据类型的大小进行。例如,
int *p; p++;
将使p
的值增加sizeof(int)
的大小。 sizeof()
操作符返回给定类型或变量的字节大小。
- 应用场景:
- 动态内存分配:使用
malloc()
和free()
来管理堆上的内存。 - 数据结构: 用于创建链表、树等复杂的数据结构。
- 函数参数传递:通过传递指针来修改函数外部的变量。
- 数组访问: 数组名可以被视为指向数组首元素的指针,从而可以使用指针进行数组访问.
- 动态内存分配:使用
教师强调:指针是 C 语言中最大的错误来源,必须谨慎使用。
- 基础假设:
学术图谱¶
- 领域发展树形图
- 奠基理论:
- 冯·诺依曼架构 (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 字符
\0
或0
用于标记字符串的结束。 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()
重新分配内存大小。
- C 语言提供了
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 代码中最大的错误来源 。
思辨空间¶
-
课堂讨论中的关键辩题
| 辩题 | 正方观点 | 反方观点 | | :--------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------- | | C 的手动内存管理 vs. 自动内存管理 | C 的手动内存管理提供了更高的性能和控制,可以进行更精细的内存优化。 | 手动内存管理容易导致内存泄漏和悬挂指针等错误,难以调试,增加开发难度。 | | 指针的必要性 vs. 安全性风险 | 指针是 C 语言的核心特性,允许直接操作内存,提高效率,适用于系统级编程和嵌入式开发。 | 指针容易引入错误,难以调试,现代编程语言使用更安全的引用和抽象来替代指针。 | | 数组边界检查的必要性 vs. 性能开销 | 数组边界检查可以防止越界访问导致的错误,提高程序的安全性。 | 数组边界检查会增加程序的运行开销,在性能敏感的应用中可能会成为瓶颈。 C 语言为了效率牺牲了安全性。 | | 使用 Valgrind 等工具进行调试的必要性 | Valgrind 等工具可以检测内存错误,提高代码的健壮性,是调试 C 代码不可或缺的工具。 | 使用 Valgrind 会显著降低程序的运行速度,会增加调试时间,但对于 C 程序的正确性来说是必要的。 | | C 作为系统级编程语言的价值 vs. 其他语言 | C 语言可以进行底层硬件操作,具有高性能,适用于开发操作系统、驱动程序等系统软件,且可以最大程度控制硬件。 | 现代语言(如 Rust, Go 等)在保证性能的同时,提供了更高级的抽象和更安全的内存管理机制,更适合用于系统级编程,可以避免 C 语言的常见问题。 |
增值模块¶
-
认知脚手架
-
知识迁移地图:
- 指针: 理解指针有助于理解动态内存分配、数据结构、函数参数传递等重要概念,为编写高效、灵活的 C 代码打下基础。
- 内存管理: 理解 C 语言的内存管理机制有助于理解操作系统和计算机体系结构,为后续学习操作系统和编译原理打下基础。
- 数组和指针的关系: 理解数组和指针的联系可以帮助理解内存布局和访问方式,为高效处理数据提供基础。
- 动态内存分配: 理解动态内存分配的原理可以帮助创建动态数据结构,提高程序的灵活性和效率。
2. 学术预警系统
-
高频考点:
- ★ 指针的概念和用法。
- ★★ 指针运算和类型转换。
- ★★ 动态内存分配和释放。
- ★★★ 内存泄漏、悬挂指针、重复释放等内存错误。
- ★★★★ 如何使用 Valgrind 进行内存调试。
-
常见论证误区:
- 未初始化指针: 使用未初始化的指针会导致未定义行为。
- 忘记释放内存: 忘记释放
malloc()
分配的内存会导致内存泄漏。 - 使用释放后的内存: 访问已通过
free()
释放的内存会导致未定义行为。 - 数组越界: 访问超出数组边界的内存会导致未定义行为,可能导致数据损坏或程序崩溃。
- 指针类型转换: 不恰当的指针类型转换可能导致未定义的行为。
3. 教授思维透视
-
论证偏好:
- 强调 C 语言的底层特性,以及对内存管理的精细控制。
- 重视动手实践,强调通过代码理解概念,而非死记硬背。
-
强调调试的重要性,提倡使用 Valgrind 等工具来检测内存错误。
- 学术倾向:
-
认为 C 语言是学习计算机系统和底层原理的重要工具。
- 认为虽然 C 语言的内存管理容易出错,但是仍然值得学习,因为它提供了对硬件的直接控制。
- 强调学习 C 语言内存管理中遇到的各种陷阱,以避免未来出现类似错误。
-