1.空指针,野指针,悬挂指针
空指针:定义:int *p=NULL;
未分配内存空间,用NULL表示。
野指针:定义:int *p;
没有被初始化,或者指向未知的内存位置,或者指向无效的内存地址。野指针可能包含任意的内存地址,可能指向堆、栈或任何其他地方。对野指针进行解引用操作会导致未定义的行为。
悬挂指针:指在程序中仍然存在,但指向的内存地址已经无效或不再可用的指针。
有几种情况会导致悬挂指针的出现:
- 释放内存后未置空指针
- 超出作用域的指针
- 使用已被释放的内存
所以通常在释放内存后,要将悬挂指针置空
2. 引用参数:形参写成&L,即为引用参数,为C++中的用法。
C语言中,通过传递指针来模拟引用参数的行为。
3.函数做形参(函数指针)
函数指针的声明形式为 int (*func)(int),其中 int 表示返回值的类型, func 是函数指针的名称, (int) 是参数列表的声明。
4.bool类型
C语言中没有bool类型,可借助宏定义来完成。函数返回宏时,返回值的类型就是宏展开后的类型。
5.malloc
int *p = (int*)malloc(sizeof(int));
蓝色的(int*)是强制类型转换,因为只能给指针分配内存,所以必须转换为指针类型
6.形参是指针类型,如:SqList *L, 则对应的实参应定义为:SqList L, 并将&L传入。
若实参定义为:SqList *L,则L为野指针,直接传入会导致不可预料的后果,将L初始化为NULL也不行。而且定义成指针没有意义,因为数据存储在结构体成员数组或指针内。若定义成指针,不仅要给L->elem分配内存,还要给L本身这个指针分配内存。
给L本身这个指针分配内存:
L = (SqList *)malloc(sizeof(SqList));
7. LinkList L
L 确实是一个指针,但在 C 语言中,函数参数仍然是按值传递的。当你传递L作为参数时,函数得到的是L的拷贝,而不是原始指针本身。所以必须传入一个指向L的指针,此时指向L的指针本身被拷贝,但修改此拷贝时,修改的是此拷贝指向的内容,也就是L。
关键:修改指针,修改的是指针指向的内容
8. LinkList *L
L是个二重指针,访问结构体成员时,应:(*L)->data
9.单链表插入节点
当使用 LNode p; 来表示当前要插入的节点时,每次循环迭代都会创建一个新的 p,但由于 p 是局部变量,它的生命周期仅限于循环块内。当每次迭代结束时,p 将被销毁(p 实际上是在下一次迭代开始时被销毁的,而不是在当前迭代结束后被销毁)这意味着它的内存空间会被释放。然后,你将 (*L)->next 设置为 &p,这意味着 (*L)->next 现在指向了一个已被销毁的内存地址,这个地址实际上不再有效。
这种情况下,链表的每个节点都指向了同一个内存地址,即被销毁的 p 的地址。因为 p 在每次迭代结束时被销毁,所以链表中的所有节点的 next 指针实际上都指向相同的地址。这导致链表的每个节点都指向了相同的数据,最终链表中只包含最后一次迭代的输入。
要解决这个问题,你应该为每个新节点使用动态内存分配,确保每个节点都有独立的内存,而不是共享同一个局部变量 p 的内存。这样,你可以正确构建一个包含多个节点的链表,并确保数据的完整性和正确性。
10.尾插法建表
错误示范:通过移动指针 p 来进行链表的构建
LNode *p = (*L)->next;
for (i = 1; i <= n; i++) {
p = (LNode*)malloc(sizeof(LNode));
if (!p) exit(ERROR);
scanf("%d",&(p->data));
p = p->next;
}
标蓝的两句:在循环中,执行 p = (LNode*)malloc(sizeof(LNode));为 p 分配了一个新的节点。然而并没有将这个新节点连接到链表中,而是直接将 p 更新为指向新分配的节点。这将导致原始链表的第一个节点 (*L)->next 丢失,因为 p 不再指向它。
更正代码:给p->next分配内存,不会丢失与链表的关系。然后将 p 移动到新节点,以确保它指向链表的最后一个节点。
LNode *p = (*L); // 指向链表的头节点
for (i = 1; i <= n; i++) {
p->next = (LNode*)malloc(sizeof(LNode));
if (!(p->next)) exit(ERROR);
p = p->next;
scanf("%d", &(p->data));
p->next = NULL; // 设置新节点为链表的尾节点
}
标蓝的三句:按逻辑应该是以下这个顺序,但上面的更简洁
scanf("%d", &(p->next->data ));
p->next->next = NULL;
p = p->next;
10. ListInsert(LinkList L, int i, ElemType e)
int ListInsert(LinkList L, int i, ElemType e) {
// 修改 L 不会影响外部链表头指针,一般用于有头结点的链表}
LinkList *L: 传递的是链表头指针的地址。在函数内部对 *L 的修改将直接影响外部链表的头指针。通过修改 *L 可以改变外部链表的结构。
int ListInsert(LinkList *L, int i, ElemType e) {
// 修改 *L 会影响外部链表头指针,更多用于无头结点的链表,栈就是个典型例子}
故用LinkList L和LinkList *L做形参均可,使用 LinkList L 作为形参在特定情况(Init和Create不行,Insert和Delete可以。因为前两个牵扯到分配内存)下是可以正常工作的,这是因为在 C 语言中,参数传递是通过值传递进行的,但对于指针类型的参数,传递的是指针的副本,仍然可以通过这个副本修改原始链表。
不能修改L对空间的指向关系,即不能在函数内对L重新分配内存,但可以对L指向的内存进行修改。
11.内存特点
1、栈内存特点
数据一执行完毕,变量会立即释放,节约内存空间。
优势:存取速度比堆要快,仅次于直接位于CPU中的寄存器。
缺点:存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
一般直接创建的变量
具体代表:
函数调用栈: 栈内存主要用于存储函数调用的局部变量、函数参数、返回地址等。每次函数调用时,系统都会在栈上分配一块内存用于存储当前函数的数据。
局部变量: 函数中声明的局部变量通常存储在栈上。这些变量的生命周期与函数的调用周期相对应。
函数参数: 被传递给函数的参数也存储在栈上。它们在函数调用时被推入栈,函数结束时从栈中弹出。
2、堆内存特点
堆内存中所有的实体都有内存地址值,内存释放靠垃圾回收机制不定时的收取。
堆的优势:可以动态地分配内存大小。
缺点:由于要在运行时动态分配内存,存取速度较慢。
具体代表:动态分配的数组: 使用 malloc、calloc 或 new 在堆上分配的动态数组是堆内存的具体代表。例如,int* arr = (int*)malloc(10 * sizeof(int)); 分配了包含 10 个整数的数组。
动态分配的对象: 对象的实例,特别是在面向对象编程中,通常通过 new 运算符在堆上分配内存。例如,SomeClass* obj = new SomeClass();。
链表和树节点: 动态数据结构,如链表、树等,通常在堆上分配内存。新的节点可以在运行时动态添加,而不需要预先知道其数量。
全局变量: 全局变量和静态变量的存储位置通常在程序启动时就分配在堆上。它们的生命周期贯穿整个程序的执行过程。
字符串: 动态分配的字符串,如使用 malloc 或 new 分配的字符数组,也是堆内存的典型例子。
3、栈内存的数据共享:
栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:
int a = 3;
int b = 3;
编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b = 3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。
这时,如果再令a=4;那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并令a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。