CDay09

动态内存分配


在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。

相关函数

malloc函数

//原型:
void* malloc( size_t size );

//功能:
分配size字节未初始化的内存

//参数:
 size  - 要分配的字节数

//返回值:
成功时,返回指向新分配内存的指针。为避免内存泄漏,必须用 free() 或 realloc() 解分配返回的指针。
失败时,返回空指针。

calloc函数

//原型:
void* calloc( size_t num, size_t size );

//功能:
为 num 个对象的数组分配内存,并初始化所有分配存储中的字节为零。(申请size * num字节内存)

//参数:
 num  -  对象数目
 size -  每个对象的大小

//返回值:
成功时,返回指向新分配内存的指针。为避免内存泄漏,必须用 free() 或 realloc() 解分配返回的指针。
失败时,返回空指针。

realloc函数

//原型:
void *realloc( void *ptr, size_t new_size );

//功能:
重新分配给定的内存区域。它必须是之前为 malloc() 、 calloc() 或 realloc() 所分配,并且仍未被 free 或 realloc 的调用所释放。

//参数:
ptr       -   指向需要重新分配的内存区域的指针
new_size  -    数组的新大小(字节数)
//返回值:
成功时,返回指向新分配内存的指针。返回的指针必须用 free() 或 realloc() 归还。原指针 ptr 被非法化,而且任何通过它的访问是未定义行为(即使重分配是就地的)。
失败时,返回空指针。原指针 ptr 保持有效,并需要通过 free() 或 realloc() 归还。

注意事项

1、ptr必须是以前通过malloc、realloc、calloc函数返回的指针

2、若重新分配内存在原有内存基础上缩小,则直接截断

3、若重新分配内存在原有内存基础上扩大,则优先尽力在原地扩容,如无法原地扩容,则重新在堆里找一块内存进行分配,并将原有数据复制过来后,将原来那片内存free掉。

有关内存的释放(free函数)

如果申请的内存没有被释放,会造成内存泄漏

//例子:
p = malloc(...);
q = malloc(...);
p = q;

内存泄漏:如果程序中存在垃圾,这种现象就叫做内存泄漏。

Q:如何避免内存泄漏? ---及时释放不再用到的内存空间

//原型:
void free( void *ptr );

//功能:
解分配之前由 malloc() 、 calloc()或 realloc() 分配的空间。

//参数:
ptr   -   指向要解分配的内存的指针(malloc() 、 calloc()或 realloc()的返回值)

//返回值:
无

Q:free函数如何知道该释放多大的内存空间?

A:free释放的时候会根据传入的地址向前偏移4个字节 从这4字节获取具体的内存块大小并释放。 (实际上的实现很可能使用8字节做为头部:其中每四个字节分别标记大小和是否正在使用)

引入free函数后,可以减少内存泄漏,但是同时也引入了一个新的问题:悬空指针

p = malloc(...);
q = p;
free(p);

注意:

  • 若 ptr 为空指针,则函数不进行操作。

  • 若 ptr 的值不等于之前从 malloc() 、 calloc() 、 realloc() 或 aligned_alloc() (C11 起) 返回的值,则行为未定义。

  • 若 ptr 所指代的内存区域已经被解分配,则行为未定义,即是说已经以ptr 为参数调用 free() 或 realloc() ,而且没有后继的 malloc() 、 calloc() 或 realloc() 调用以 ptr 为结果。

  • 若在 free() 返回后通过指针 ptr 访问内存,则行为未定义(除非另一个分配函数恰好返回等于 ptr 的值)。

有关通用指针与空指针

void*:通用指针,可以指向任意类型的对象,同时也可以转换为其他类型的指针。

空指针:不能指向任何对象的指针(用宏NULL表示空指针,其值为0)。所以不能对空指针进行解引用,否则会报NULLPointer Exception(空指针异常)

#ifndef NULL
    #ifdef __cplusplus
        #define NULL 0
    #else
        #define NULL ((void *)0)
    #endif
#endif

示例

//拼接两个字符串,但是不改变其中任何一个字符串

#define _CRT_SECURE_NO_WARNINGS

#include<stdio.h>
#include<string.h>
#include<stdlib.h>

char* myStrcat(const char*, const char*);

int main(void)
{
	char str1[] = "Hello ";
	char str2[] = "World!";

	char* resultStr = myStrcat(str1, str2);

	puts(resultStr);
	free(resultStr);

	return 0;
}

char* myStrcat(const char* str1, const char* str2)
{
	char* resultStr = malloc(strlen(str1) + strlen(str2) + 1);		//1是'\0'
	if (resultStr == NULL)
	{
		printf("Error: malloc failed in myStrcat!");
		exit(1);
	}

	//内存分配成功
	strcpy(resultStr, str1);
	strcat(resultStr, str2);

	return resultStr;
}

动态内存分配是链式结构的基础

链表:用一条 “链” 将所有结点串联起来的结构。
结点:数据域、指针域(存放另一个结点的地址)

链表的分类:

示例:

#include<stdio.h>
#include<stdlib.h>

typedef struct node {
	int data;
	struct node* next;
}Node;

Node* add_to_list(Node* list, int data);

int main(void)
{
	Node* list = NULL;
	list = add_to_list(list, 3);			// 3
	list = add_to_list(list, 2);			// 2 --> 3
	list = add_to_list(list, 1);			// 1 --> 2 --> 3

	while (list)
	{
		printf("%d  ", list->data);
		list = list->next;
	}

	return 0;
}

// 头插法
Node* add_to_list(Node* list, int data)
{
	//申请空间
	Node* newNode = (Node*)malloc(sizeof(Node));
	if (newNode == NULL)
	{
		printf("malloc failed in add_to_list function!\n");
		exit(1);
	}
	newNode->data = data;
	newNode->next = list;

	return newNode;
}

二级指针

原则:
如果想修改 指针变量 的值 --> 传递二级指针
如果想修改 指针变量指向的对象 的值 --> 传递一级指针

示例:

#include<stdio.h>
#include<stdlib.h>

typedef struct node {
	int data;
	struct node* next;
}Node;

void add_to_list(Node** list, int data);

int main(void)
{
	Node* list = NULL;
	add_to_list(&list, 3);			// 3
	add_to_list(&list, 2);			// 2 --> 3
	add_to_list(&list, 1);			// 1 --> 2 --> 3

	while (list)
	{
		printf("%d  ", list->data);
		list = list->next;
	}

	return 0;
}

// 头插法
void add_to_list(Node** plist, int data)
{
	//申请空间
	Node* newNode = (Node*)malloc(sizeof(Node));
	if (newNode == NULL)
	{
		printf("malloc failed in add_to_list function!");
		exit(1);
	}
	newNode->data = data;
	newNode->next = *plist;

	*plist = newNode;
}

函数指针

指向函数的指针

#include<stdio.h>
#include<math.h>

#define PI 3.1415926

double average(double (*f)(double), double a, double b);

int main(void)
{
	double result = average(&sin, 0, PI);		// 等价于 average(sin, 0, PI)
	printf("%lf", result);

	return 0;
}

double average(double (*f)(double), double a, double b)
{
	return (*f)((a + b) / 2);					// 等价于 f((a + b) / 2)
}

/*
* 注:函数指针与数组类似,函数名可以作为指针使用。
*/

qsort函数

  • 如果comp返回值小于0(< 0),那么p1所指向元素会被排在p2所指向元素的左面;

  • 如果comp返回值等于0(= 0),那么p1所指向元素与p2所指向元素的顺序不确定;

  • 如果comp返回值大于0(> 0),那么p1所指向元素会被排在p2所指向元素的右面。

#include<stdio.h>

#define SIZE(a) (sizeof(a)/sizeof(a[0]))

typedef struct student {
	int no;
	char name[25];
	int chinese;
	int math;
	int english;
}Student;

typedef struct node {
	int data;
	struct node* next;
} Node;

int cmp(const void*, const void*);		//比较函数

int main(void)
{
	Student students[5] = {
		{3, "wangyuyan", 100, 90, 90},
		{1, "liuyifei", 100, 100, 100},
		{2, "zhaolinger", 90, 100, 100},
		{5, "xiaolongnv", 90, 90, 100},
		{4, "xixi", 90, 100, 90} };
	qsort(students, SIZE(students), sizeof(students[0]), cmp);

	return 0;
}

// 比较规则:先比较总分大小(逆序), 再比较语文成绩(逆序),再比较数学(逆序),比较英语成绩(逆序),比较学号(顺序)
int cmp(const void* p1, const void* p2) {
	Student* s1 = p1;
	Student* s2 = p2;
	int total1 = s1->chinese + s1->math + s1->english;
	int total2 = s2->chinese + s2->math + s2->english;

	if (total1 != total2) {
		return total2 - total1;
	}

	if (s1->chinese != s2->chinese) {
		return s2->chinese - s1->chinese;
	}

	if (s1->math != s2->math) {
		return s2->math - s1->math;
	}

	if (s1->english != s2->english) {
		return s2->english - s1->english;
	}

	return s1->no - s2->no;
}

链表

单链表的基本操作

1、增(在某个结点的后面添加)--------------O(1)

newNode->next = cur->next;
cur->nex = newNode;

2、删(在某个结点后删除)--------------O(1)

cur->next = cur->next->next;

3、查

  • 根据索引查找元素 ----------O(n)

  • 查找链表与特定值相等的元素 ----------O(n)

    • 元素大小有序 ----------O(n)

    • 元素大小无序 ----------O(n)

双向链表的基本操作

1、增加(在某个结点前面添加) ----------O(1)

2、删除(删除该结点) ----------O(1)

3、查找

  • 查找前驱结点 ----------O(1),单链表O(n)

  • 根据索引查找值 ----------O(n)

    • 单链表:平均遍历 n/2
    • 双向链表:平均遍历 n/4
  • 查找链表中与特定值相等的元素,有序无序都是O(n)

比较单链表与双向链表

  • 双向链表会占用更多的内存空间
  • 双向链表的性能会优于单链表

在实际工程中,一般会使用双向链表

posted @ 2023-02-06 21:09  MyXjl  阅读(10)  评论(0编辑  收藏  举报