C语言再学习—内存,链表
一、变量与指针
1.volatile
由于待会检查内存地址时候用到volatile,故在此介绍一下。
volatile
是 C/C++ 中的一个类型修饰符,用于告诉编译器该变量的值可能以编译器无法预测的方式被改变(如硬件自动更新、多线程共享等),从而禁止编译器对该变量进行优化。
核心作用
-
禁止编译器优化:
编译器通常会对变量访问进行优化(如缓存到寄存器、重排序),但volatile
会强制每次访问都直接读写内存。 -
保证内存可见性:
确保每次读取变量时都获取内存中的最新值,而非缓存副本。
2.测试程序:
在文件中查找.map文件查看到一下地址:
a 0x20000000 Data 4 main.o(.data)
c 0x20000004 Data 1 main.o(.data)
p1 0x20000008 Data 4 main.o(.data)
p2 0x2000000c Data 4 main.o(.data)
buf 0x20000010 Data 100 main.o(.bss)
结论:变量,能读能写,在内存里面
指针,保存的是地址,在32位处理器中,地址都是32位,无论什么类型的指针变量,都是4字节
3.malloc
malloc
(memory allocation 的缩写 ,即内存分配)是一个用于动态内存分配的标准库函数,其函数原型为:void* malloc(size_t size);
malloc
函数用于在堆(heap)内存中分配指定大小的连续内存块,并返回一个指向该内存块起始地址的指针。它主要用于在程序运行时,根据实际需求动态地申请内存,而不是在编译时就确定好内存大小,这在处理一些大小不确定的数据(如根据用户输入决定长度的数组、链表节点等)时非常有用。
例如,要分配一个能存储 10 个 int
类型数据的内存块,因为在常见系统中 int
类型占 4 个字节,所以可以这样调用:malloc(10 * sizeof(int));
,使用 sizeof(int)
而不是直接写 4,是为了增强代码的可移植性,因为不同系统中 int
类型的字节数可能不同。
返回值
malloc
函数返回一个 void*
类型的指针,指向分配好的内存块的起始地址。如果内存分配失败(比如系统没有足够的空闲内存 ),则返回 NULL
。因此在使用返回的指针前,通常需要检查它是否为 NULL
,以避免出现野指针引用导致程序崩溃。
使用完内存后,通过 free
函数释放该内存块(free
是与 malloc
配套使用的函数,用于释放 malloc
分配的内存 ),并将指针赋值为 NULL
,防止成为野指针。
示例代码:
#include <stdio.h>
#include <stdlib.h>int main() {// 分配一个能存储5个int类型数据的内存块int *ptr = (int *)malloc(5 * sizeof(int)); if (ptr == NULL) {printf("内存分配失败\n");return 1;}// 使用分配的内存for (int i = 0; i < 5; i++) {ptr[i] = i + 1;printf("%d ", ptr[i]);}// 释放内存,防止内存泄漏free(ptr); ptr = NULL; return 0;
}
4.结构体里的字节
当在结构体当中定义这些变量时 ,按常理来说person3所占字节为1+4=5,person4所占字节为1+1+4=6。但是现实不是,如上图,为了效率,在person3时候sex前面三个字节是浪费掉,空出来的。在person4定义的时候,既然有high了,那么就少浪费一个。使得整个sex\high\age打包起来成为一个整体。这样person4所占字节为8,person3也是8。
接着来看下图:
在结构体里面定义了两个char还有一个int(如表格中所画),当有一个int类型的指针给c赋值时。d原本应该显示的B被吞没了,这是因为 int *p是一个4字节的大小,不能因为只存一个字符'C'就不用四个字节的空间了。
二、链表
定义链表:
#include <stdio.h>
// 正确定义结构体
typedef struct _node {char value;struct _node *next;
} Node;
定义了一个名为spy的结构体类型,并使用typedef为它创建了两个别名。
char value;
:存储一个字符串struct _node *next
:指向同类型结构体的指针,用于链表结构中连接下一个节点
typedef ... spy; // 为struct spy创建别名spy typedef ...
头部插入链表:
假设原链表为 1 -> 2 -> NULL,头指针 head 指向节点 1。现在要在头部插入一个节点0,
最终链表变为:0 -> 1 -> 2 -> NULL。
分析:1.首先需要建立一个新节点。总不能凭空出现节点吧,所以就写一个创建节点的程序,哪怕后面要用到,直接拿过来用。
// 创建新节点
Node* createNode(int data) {Node* newNode = (Node*)malloc(sizeof(Node));//malloc返回一个sizeof大小的指针,//所以newNode是一个Node复制粘贴类型的指针 if (newNode == NULL) { //检测是否分配成功,如果newNode的地址值为NULL,则没有分配成功 printf("内存分配失败!\n");exit(EXIT_FAILURE); //直接终止程序 }newNode->data = data; //填入数据 newNode->next = NULL; //暂时设置没有下个节点 return newNode;
}
(注释已经很详细,在此不做分析)
2.插入的时候,首先定义新的节点,传入数据。再将0节点的末尾指向1,再更新头节点,让它指向0
// 在链表头部插入节点
void insertAtHead(Node** head_ref, int data) { //Node**head_ref代表是指向头指针的指针。//因此传入头指针的地址(二级指针)。int data:要插入的新节点的数据值。Node* newNode = createNode(data); //定义新节点 newNode->next = *head_ref; //*head_ref 目前是原始头指针的值(即原头节点的地址)//此行将新节点的 next 指针指向原头节点,从而将新节点插入到链表头部。 *head_ref = newNode; //修改原始头指针的值,使其指向新节点。此时新节点成为链表的第一个节点。
}
/* 示例:链表插入过程
假设原链表为 1 -> 2 -> NULL,头指针 head 指向节点 1。
1.调用 insertAtHead(&head, 0):head_ref 是 &head(即头指针的地址)。创建新节点 0,其 next 初始为 NULL。
2.执行 newNode->next = *head_ref:*head_ref 是 head 的值(即节点 1 的地址)。新节点 0 的 next 指向节点 1。
3.执行 *head_ref = newNode:head 的值被更新为新节点 0 的地址。最终链表变为:0 -> 1 -> 2 -> NULL。*/
若是不添加*head_ref = newNode; ,则状态如下图,head还是指向1。
尾部插入链表:
// 在链表尾部插入节点
void insertAtTail(Node** head_ref, int data) {Node* newNode = createNode(data); //定义新节点 if (*head_ref == NULL) { //处理链表为空的情况 ,newNode就是唯一节点 *head_ref = newNode;return;}Node* current = *head_ref; // 定义头节点,然后用while来实现遍历,找到NULL while (current->next != NULL) { //判断本节点是否不指向NULL current = current->next; //若是节点不指向NULL,则找到下一个节点 }current->next = newNode; //若是指向NULL则说明现在的current->next是原来的最后一个节点。//因为在原先创建新节点的时候,指向地址就是NULL所以这里的newNode不需要再指向NULL
}
删除指定值的第一个节点:
// 删除指定值的第一个节点
void deleteNode(Node** head_ref, int key) { //参数1:链表头节点的指针的指针 参数2:需要删除的节点的值 Node* temp = *head_ref;Node* prev;if (temp != NULL && temp->data == key) { //如果一上来就发现temp非空并且它被指定到了 *head_ref = temp->next; //那么指定头节点为temp的下一位,相当于吧temp孤立在最前面了 free(temp); //释放temp return; //直接返回 }while (temp != NULL && temp->data != key) { //若是一开始temp不是目标值 prev = temp; //prev记录下当前temp temp = temp->next; //temp指向下一个地址,直到是目标值之后跳出循环 }if (temp == NULL) return; //如果为NULL说明不存在目标节点,直接返回 prev->next = temp->next; //先把前一个的next指向下下个的地址 free(temp); //释放temp
}
分为两个部分:
1.头地址temp就是目标值
2.目标值在中间
整体代码:
#include <stdio.h>
#include <stdlib.h>// 定义链表节点结构
typedef struct Node {int data; // 数据域struct Node* next; // 指针域,指向下一个节点
} Node;// 创建新节点
Node* createNode(int data) {Node* newNode = (Node*)malloc(sizeof(Node));//malloc返回一个sizeof大小的指针,//所以newNode是一个Node复制粘贴类型的指针 if (newNode == NULL) { //检测是否分配成功,如果newNode的地址值为NULL,则没有分配成功 printf("内存分配失败!\n");exit(EXIT_FAILURE); //直接终止程序 }newNode->data = data; //填入数据 newNode->next = NULL; //此节点没有下个个节点 return newNode;
}// 在链表头部插入节点
void insertAtHead(Node** head_ref, int data) { //Node**head_ref代表是指向头指针的指针。//因此传入头指针的地址(二级指针)。int data:要插入的新节点的数据值。Node* newNode = createNode(data); //定义新节点 newNode->next = *head_ref; //*head_ref 目前是原始头指针的值(即原头节点的地址)//此行将新节点的 next 指针指向原头节点,从而将新节点插入到链表头部。 *head_ref = newNode; //修改原始头指针的值,使其指向新节点。此时新节点成为链表的第一个节点。
}
/* 示例:链表插入过程
假设原链表为 1 -> 2 -> NULL,头指针 head 指向节点 1。
1.调用 insertAtHead(&head, 0):head_ref 是 &head(即头指针的地址)。创建新节点 0,其 next 初始为 NULL。
2.执行 newNode->next = *head_ref:*head_ref 是 head 的值(即节点 1 的地址)。新节点 0 的 next 指向节点 1。
3.执行 *head_ref = newNode:head 的值被更新为新节点 0 的地址。最终链表变为:0 -> 1 -> 2 -> NULL。*/ // 在链表尾部插入节点
void insertAtTail(Node** head_ref, int data) {Node* newNode = createNode(data); //定义新节点 if (*head_ref == NULL) { //处理链表为空的情况 ,newNode就是唯一节点 *head_ref = newNode;return;}Node* current = *head_ref; // 定义头节点,然后用while来实现遍历,找到NULL while (current->next != NULL) { //判断本节点是否不指向NULL current = current->next; //若是节点不指向NULL,则找到下一个节点 }current->next = newNode; //若是指向NULL则说明现在的current->next是原来的最后一个节点。//因为在原先创建新节点的时候,指向地址就是NULL所以这里的newNode不需要再指向NULL
}// 删除指定值的第一个节点
void deleteNode(Node** head_ref, int key) { //参数1:链表头节点的指针的指针 参数2:需要删除的节点的值 Node* temp = *head_ref;Node* prev;if (temp != NULL && temp->data == key) { //如果一上来就发现temp非空并且它被指定到了 *head_ref = temp->next; //那么指定头节点为temp的下一位,相当于吧temp孤立在最前面了 free(temp); //释放temp return; //直接返回 }while (temp != NULL && temp->data != key) { //若是一开始temp不是目标值 prev = temp; //prev记录下当前temp temp = temp->next; //temp指向下一个地址,直到是目标值之后跳出循环 }if (temp == NULL) return; //如果为NULL说明不存在目标节点,直接返回 prev->next = temp->next; //先把前一个的next指向下下个的地址 free(temp); //释放temp
}// 遍历链表并打印节点值
void printList(Node* head) {Node* current = head;while (current != NULL) {printf("%d -> ", current->data);current = current->next;}printf("NULL\n");
}// 释放链表内存
void freeList(Node* head) {Node* temp;while (head != NULL) {temp = head;head = head->next;free(temp);}
}int main() {Node* head = NULL; // 初始化空链表// 插入节点insertAtTail(&head, 1); // 链表: 1 -> NULLinsertAtTail(&head, 2); // 链表: 1 -> 2 -> NULLinsertAtHead(&head, 0); // 链表: 0 -> 1 -> 2 -> NULLinsertAtTail(&head, 3); // 链表: 0 -> 1 -> 2 -> 3 -> NULLprintf("初始链表: ");printList(head);// 删除节点deleteNode(&head, 1); // 链表: 0 -> 2 -> 3 -> NULLprintf("删除节点1后: ");printList(head);// 释放链表freeList(head);head = NULL; // 防止野指针return 0;
}