初识C语言·动态内存开辟

目录

1 为什么要有动态内存开辟

2 malloc函数的使用

3 free函数的使用

4 calloc函数的使用

5 realloc函数的使用

6 常见的动态内存开辟的错误

1)对空指针的解引用

2)对动态内存开辟空间的越界访问

我们使用了calloc函数开辟了10个整型空间,使用的时候循环次数是11次,那么最后一次循环就会越界访问到未开辟的空间,系统就会报错。3)  对非动态内存开辟的空间释放

4)free释放一部分动态内存开辟的空间

5)对同一块空间多次释放

6)动态内存开辟的空间未释放

7 动态内存开辟函数题目解析

代码1:

代码2:

代码3:

 代码4:

8 柔性数组


1 为什么要有动态内存开辟

int a = 10;
int arr[10] = { 0 };

上述定义了一个整型,开辟了4个字节,定义了一个整型数组,开辟了40个字节,但是是固定开辟的,面对灵活多变的实际问题的时候可能就有点鸡肋,这种开辟空间的特点是:

i) 开辟好空间之后不能改变

ii) 开辟的空间大小是固定的

那么为了解决实际问题就引入了动态内存开辟,可以根据实际需要进行内存开辟。

那么引用了4个函数,分别是malloc calloc free realloc,而动态开辟函数开辟的空间都是在堆区开辟的,不是栈区!


2 malloc函数的使用

void* malloc (size_t size);

这是malloc函数的原型,需要引用的头文件是stdlib,返回的是void*指针,返回void*指针的原因是因为实际工作中开辟的空间类型是根据实际需要确定的,所以开辟好空间之后需要进行强制类型转化,开辟空间的单位是字节,参数表示的是开辟多少个字节,最后返回的地址是开辟的字节的首地址。

那么开辟空间的话也是分为是否开辟成功的,如果开辟成功了,返回的就是那块空间的首地址,如果开辟失败了,返回的就是空指针,比如我开辟几百亿个字节,一般情况下返回的就是NULL,所以我们用指针接收了地址后第一件事就是判断一下是不是空指针,不然是会有警告的。

初识C语言·动态内存开辟_第1张图片

 这是正常使用的情况,那如果size_ t size是0呢?这时候malloc的行为标准是未定义的,操作就取决于编译器了。

代码1:

int main()
{
	int* p = (int*)malloc(40);
    assert(p);
	for (int i = 0; i < 10; i++)
	{
		*p = i;
        p++;
	}	
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}

问运行结果是什么?
答案是报错,你可以把内存开辟的返回的地址理解成数组名,数组名哪里能自增自减呢?理解成数组名的缘由是因为free,动态内存函数开辟了空间之后,使用完空间是要被释放的,而free的参数是开辟的空间的首地址,所以p不能自增自减。防止后面释放空间释放错了。


3 free函数的使用

上面提到了返回的地址不能自增自减,因为free( 头文件依然是stdlib)要出场了,free,免费,释放,在C语言里面就是专门用来释放动态内存开辟的空间的,当使用完之后都是要free的。

int main()
{
	int* p = (int*)malloc(40);
	assert(p);
	for (int i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}	
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

free的参数是动态内存开辟的地址,可能让人疑问的点就是为什么最后要给一个NULL,这是因为我们传的是值,不是地址,free的形参是值,所以释放空间的时候,那块空间确实是释放了没错,但是p还仍然保留着原来的数据,所以需要手动置为空指针。

使用free的时候需要注意的:

·free的参数一定是动态开辟的地址,如果不是,那么free的行为是未定义的

·free的参数如果是NULL,那么该函数什么事都不做


4 calloc函数的使用

void* calloc (size_t num, size_t size);

calloc函数的原型如上,头文件依然是stdlib,作用是开辟num个大小为size字节的空间,它和malloc是极其相似的,唯一的不同就是calloc会自动初始化空间为0,malloc函数则不会初始化, 举个例子:

int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	assert(p);
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

最后的运行结果应该全是0。


5 realloc函数的使用

realloc函数才是动态内存开辟函数的老大,因为点啥呢,因为它可以扩大空间,比如你写代码到一半发现空间不够了,这时候就需要realloc函数来操作了,它可以扩大空间

void* realloc (void* ptr, size_t size);

realloc函数的原型如上,头文件依然是stdlib,第一个参数一般都是动态开辟的地址,第二个是表示扩大到多少字节,所以一般情况下,使用该函数之前一般都是已经动态开辟了空间的,那么什么是特殊情况呢?

如下:

int main()
{
	int* p = (int*)realloc(NULL, 40);
	assert(p);
	for (int i = 0; i < 10; i++) 
	{
		*(p + i) = i;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

这种情况realloc函数就是malloc函数,也就是说,如果realloc函数的第一个参数是空指针的话,那么就是从内存中随机开辟空间,此时的realloc函数就是malloc函数。

那么realloc函数一般用法就是用来扩大空间的,如果你想实验一下缩短空间也是可以试试的,只不过这个时候realloc函数的行为是未定义的。

realloc函数扩大空间有两种情况:

1 原地址的后面有足够的空间用于扩容 

2 原地址的后面没有足够的空间用于扩容

第一种情况没什么好说的,原有空间变大而已,第二种情况,realloc会在堆区重新找一个符合需要的空间,如果没有找到就会返回空指针,如果找到了,那么原有的数据会赋值到新空间,且原有空间会被释放,所以realloc函数开辟完空间之后,如果是重新找空间开辟的,就会释放原来的空间,那么实际写代码的时候,我们就会重新用一个指针来接收新空间,判断完是不是空指针后,再决定要不要赋给原来的地址,

realloc函数和其他函数最不一样的地方就是在于它会自己释放空间,这点需要注意,realloc函数举个例子:
 

int main()
{
	int* p = (int*)calloc(25, sizeof(int));
	assert(p);
	int* pa = realloc(p, 20 * sizeof(int));//两种情况 所以用另一个指针接收
	assert(pa);
	if (pa != NULL)
	{
		p = pa;
	}
	free(*p);
	free(*pa);
	p = NULL;
	pa = NULL;
	return 0;
}

Tips:所有动态内存开辟的空间是不会自己释放的,释放的空间都是需要自己释放的,要么就是程序结束由操作系统来释放。

int* Test()
{
	int* p = *(int*)malloc(40);
	return p;
}
int main()
{
	//操作
	return 0;
}

这样的p只要不释放,空间都是可以使用的。


6 常见的动态内存开辟的错误

1)对空指针的解引用

//对空指针的解引用
int main()
{
	int* p = (int*)malloc(INT_MAX * INT_MAX);;
	*p = 20;
	free(p);
    p = NULL;
	return 0;
}

这里内存是开辟不了这么多的空间的,所以p是空指针,那么解引用之后,自然就会报错

2)对动态内存开辟空间的越界访问

//对动态内存开辟空间的越界访问
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	assert(p);
	for (int i = 0; i <= 10; i++)
	{
		*(p + i) = i;
	}	
	for (int i = 0; i <= 10; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

我们使用了calloc函数开辟了10个整型空间,使用的时候循环次数是11次,那么最后一次循环就会越界访问到未开辟的空间,系统就会报错。
3)  对非动态内存开辟的空间释放

//对非动态开辟的空间进行释放
int main()
{
	int a = 10;
	int* pa = &a;
	free(pa);
	pa = NULL;
	return 0;
}

前面提到free函数只能释放动态内存开辟的空间,因为局部变量 全局变量是在栈区 静态区的,而free适用于堆区的动态内存开辟,所以使用free释放非动态内存开辟的空间的时候系统就会报错。

4)free释放一部分动态内存开辟的空间

//使用free释放一部分动态内存开辟的空间
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	assert(p);
	p++;
	free(p);
	p = NULL;
	return 0;
}

前面提及我们可以把动态内存开辟返回的地址当作数组名,这样可以避免我们给该地址自增自减,因为free释放都是释放的一整块空间,那么自增之后,free的参数不是起始地址,就会导致释放过多,也会导致越界访问,系统就会报错。

5)对同一块空间多次释放

//对同一块空间多次释放
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	assert(p);
	/* 
		操作
	*/
	free(p);
	//p = NULL;
	/*
		操作
	*/
	free(p);
	p = NULL;
	return 0;
}

对同一块空间多次释放后,free再去访问那个地址, 自然就会报错,但是如果前面释放了空间之后,并且置于0,是没有问题的,因为free的参数如果是空指针的话,就不会有任何行为。

6)动态内存开辟的空间未释放

//动态内存开辟的空间未释放
int main()
{
	int* p = (int*)malloc(40);
	/*
		操作
	*/
	return 0;
}

如果开辟的空间没有进行释放,那么内存中这块空间的状态就是一直被占用的情况,就会导致内存泄露,假如这种情况多了的话,说不定某一天你的系统内存就被占满了,然后你一重启,就会发现,欸对了,所以动态内存开辟的空间一定要正确释放。


7 动态内存开辟函数题目解析

代码1:

void GetMemory(char* p)
{
	p = (char*)malloc(100);
}
int main()
{
	char* str = NULL;
	Getmemory(str);
	strcpy(str, "hello world");
	printf(str);
	return 0;
}

程序运行到strcpy的时候就会报错,最开始str是空指针,那么这里的传参方式是传值调用,所以即使p的地址已经指向了malloc开辟的100字节,str仍然是空指针,还有一个问题是出了函数GeiMemory的时候,p就会被销毁了,也会导致内存泄漏,并且打印出错

Tips:printf这里是没有问题的,可以自行实验一下,比如pirntf("abcdefg\n"); 实际上传的也是该字符串的地址。

void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}
int main()
{
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");
	printf(str);
    free(str);
    str = NULL;
	return 0;
}

这是修正后的代码。

代码2:

char* GetMemory()
{
	char p[] = "Hello world";
	return p;
}
int main()
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
	return 0;
}

如果经过调试,我们会发现str指向的确实是常量字符串的地址,实际打印的效果却是一串乱码,其实这是因为出了Getmemory函数的作用域,导致常量字符串被销毁,但是返回的地址是没问题的,是数据没了,此时的p就是野指针了。

修改的方式也很简单,只需要加一个static就行了。

char* GetMemory()
{
	static char p[] = "Hello world";
	return p;
}
int main()
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
	return 0;
}

代码3:

void Getmemory(char** p, size_t num)
{
	*p = (char*)malloc(num);
}
int main()
{
	char* str = NULL;
	Getmemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
	return 0;
}

程序看起来是没有问题的,确实是打印了hello,但是还是存在问题,内存泄漏,没有释放空间。

 代码4:

int main()
{
	char* str = (char*)malloc(100);
	assert(str);
	strcpy(str, "hello");
	free(str);
	if (str != NULL);
	{
		strcpy(str, "world");
		printf(str);
	}
	return 0;
}

这串代码的运行结果是打印world,虽然free了动态开辟的空间,但是str仍然指向了那块空间,str先是赋值了hello,然后判断不是空指针,再次复制了world,所以最后的结果是world,这个str就是野指针了,虽然运行结果是对的,但是程序仍然是有问题的。


8 柔性数组

 在C99标准中,允许柔性数组的存在,比如:

struct St
{
	int i;
	int arr[0];
};

其中arr就是柔性数组,但有的编译器可能无法通过,所以有时候0是没有加的。

柔性数组的特点是:
i)柔性数组一定是最后一个成员

ii)sizeof计算大小的时候不包括柔性数组的大小

iii)使用malloc的时候开辟的空间应该大于结构体前面成员大小的总和,以此来符合预期

int main()
{
	struct St
	{
		char i;
		int j;
		int arr[0];
	};
	printf("%zd\n",sizeof(struct St));
	return 0;
}

结合内存对齐,柔性数组的特点,最后的结果是8。

柔性数组的使用:

struct St
{
	char i;
	int j;
	int arr[0];
};
int main()
{
	struct St* ps = (struct St*)malloc(sizeof(struct St) + 10 * sizeof(int));
	assert(ps);
	ps->i = 'w';
	ps->j = 520;
	//使用柔性数组
	for (int m = 0; m < 10; m++)
	{
		ps->arr[m] = m;
	}
	//空间不够
	struct St* pt = (struct St*)realloc(ps,sizeof(struct St) + 15 * sizeof(int));
	assert(pt);
	ps = pt;
	printf("%c\n", ps->i);
	printf("%d\n", ps->j);
	for (int n = 10; n < 15; n++)
	{
		ps->arr[n] = n;
	}
	for (int m = 0; m < 15; m++)
	{
		printf("%d ", ps->arr[m]);
	}
	free(pt);
	ps = NULL;
	pt = NULL;
	return 0;
}

常规结构体在栈区开辟的空间,有了柔性数组,我们就需要用到malloc函数,那么结构体就是在堆区开辟的空间,在使用的时候需要注意最后释放只需要释放一个指针,因为realloc函数是会自己释放上一块空间的,在开辟空间的时候:

struct St* ps = (struct St*)malloc(sizeof(struct St) + 10 * sizeof(int));

这种写法是为了更直观的看到开辟的空间比整个结构体都大,这样柔性数组才有自己的空间

那么还有一种模拟柔性数组的写法,如下:
 

struct St
{
	char i;
	int j;
	int* arr;
};
int main()
{
	struct St* ps = (struct St*)malloc(sizeof(struct St));
	assert(ps);
	ps->i = 'w';
	ps->j = 520;
	ps->arr = (int*)malloc(10*sizeof(int));
	assert(ps->arr);
	//使用柔性数组
	for (int i = 0; i < 10; i++)
	{
		*(ps->arr + i) = i;
	}
	//空间不够
	int* pt = (int*)realloc(ps->arr,15 * sizeof(int));
	assert(pt);
	ps->arr = pt;
	printf("%c\n", ps->i);
	printf("%d\n", ps->j);
	for (int n = 10; n < 15; n++)
	{
		*(ps->arr + n) = n;
	}
	for (int m = 0,n = 0; m < 15; m++,n++)
	{
		printf("%d ",*(ps->arr + m));
	}
	free(pt);
	free(ps);
	ps->arr = NULL;
	pt = NULL;
	ps = NULL;
	return 0;
}

结构体的最后一个成员是int*,也可以是其他类型的指针,这种写法是先为结构体的其他成员开辟空间,再给int*开一个单间,需要用的时候给个malloc,空间不够给个realloc,这样的话,也是类似于柔性数组的,它与上面的写法不同的是多次开辟空间,多次释放,略显繁琐,但是访问速度快点,上面的优点就是内存方便释放,两种写法各有优势。


感谢阅读!

你可能感兴趣的:(c语言,数据结构,开发语言)