C语言再学习—内存,链表

发布于:2025-07-02 ⋅ 阅读:(16) ⋅ 点赞:(0)

一、变量与指针

1.volatile

由于待会检查内存地址时候用到volatile,故在此介绍一下。

volatile 是 C/C++ 中的一个类型修饰符,用于告诉编译器该变量的值可能以编译器无法预测的方式被改变(如硬件自动更新、多线程共享等),从而禁止编译器对该变量进行优化。

核心作用

  1. 禁止编译器优化
    编译器通常会对变量访问进行优化(如缓存到寄存器、重排序),但 volatile 会强制每次访问都直接读写内存。

  2. 保证内存可见性
    确保每次读取变量时都获取内存中的最新值,而非缓存副本。

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 -> NULL
    insertAtTail(&head, 2);  // 链表: 1 -> 2 -> NULL
    insertAtHead(&head, 0);  // 链表: 0 -> 1 -> 2 -> NULL
    insertAtTail(&head, 3);  // 链表: 0 -> 1 -> 2 -> 3 -> NULL

    printf("初始链表: ");
    printList(head);

    // 删除节点
    deleteNode(&head, 1);  // 链表: 0 -> 2 -> 3 -> NULL
    printf("删除节点1后: ");
    printList(head);

    // 释放链表
    freeList(head);
    head = NULL;  // 防止野指针

    return 0;
}


网站公告

今日签到

点亮在社区的每一天
去签到