c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第五式】动态内存管理

C# 调用halcon中visualize_object_model_3d显示点云 3.1 判断线程是否为空,如果为空,则新创建,如果不为空,则中断线程,清空窗口后再次执行创建操作3.2 创建:调用show()方法实例化显示类的对象,传入显示参数,将线程testvisual 绑定 在 类对象的action方法上(此处action方法内部,只调用了visualize_object_model_3d算子)3.3 启动线程。 阅读详情

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第五式】动态内存管理

【心法】
【第零章】c语言概述
【第一章】分支与循环语句
【第二章】函数
【第三章】数组
【第四章】操作符
【第五章】指针
【第六章】结构体
【第七章】const与c语言中一些错误代码
【禁忌秘术】
【第一式】数据的存储
【第二式】指针
【第三式】字符函数和字符串函数
【第四式】自定义类型详解(结构体、枚举、联合)
【第五式】动态内存管理
【第六式】文件操作
【第七式】程序的编译



前言

大家在使用数组时肯定都会有过这样的疑问,如果一个数组的长度不足了,可以延长这个数组吗?使其能够满足存储的需求。又或者是现在不确定数组应该设为多长,先把它放着,等之后再设置它的长度。
上面的这些需求,数组显然是无法实现的,那么c语言中有什么东西能够做到这一点呢?
动态内存分配!你现在不知道需要多大的空间,没关系,之后你明确了的时候,使用动态内存分配,将你要的空间分配给即可。

本章重点:

  • 为什么存在动态内存分配
  • 动态内存函数的介绍
    • malloc
    • free
    • calloc
    • realloc
  • 常见的动态内存错误

一、为什么存在动态内存分配

此前我们掌握的内存开辟方式有:

int val = 20; // 在栈空间开辟4个字节的空间
char arr[10] = { 0 }; // 在栈空间上开辟10个字节的连续空间

但是上述的开辟空间的方式有两个特点:

  • 空间开辟大小的固定的;
  • 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

但正如前言中说的,在处理实际问题时,我们对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小只有在程序运行的时候才知道,这时候就只能试试动态内存开辟了。

二、动态内存函数的介绍

1. malloc和free

c语言提供了一个动态内存开辟的函数malloc:

void* malloc(size_t size);

在这里插入图片描述

  • 这个函数的功能是分配一块内存空间,并返回这片空间的起始地址;
  • 这片空间的大小由参数size指定,单位为字节;
  • 这片新分配的空间并未初始化,保存的数据未知;
  • 如果参数size为0时,这种行为未被定义,取决于编译器;
  • 如果该函数分配空间成功,则会返回这块空间的地址;这个指针的类型为void *是因为它可以强制转换为任何所需的类型以满足解引用的需求;如果分配空间失败则会返回一个NULL指针;

c语言还提供了另一个函数free

void free(void*)

在这里插入图片描述

  • 它的功能是回收一片动态分配的内存空间;
  • 它可以回收malloc,calloc或者是realloc函数分配的空间,使得这片空间又能被再次分配;
  • 如果`ptr``指向的空间不是动态分配的内存空间,这种行为是未定义的;
  • 如果ptr是一个NULL指针,这个函数什么都不会做(不是错误);
  • 注意,这个函数并不会改变ptr这个指针本身的内容,也就是说指针ptr仍指向这片空间(变成了野指针);

举个例子:

#include <stdio.h>
#include <stdlib.h> // 动态分配函数必须包含库

int main()
{
	// 代码1
	int num = 0;
	scanf("%d", &num);
	// 要在程序执行过程中,从用户处获得输入,设置一个用户指定大小的数组
	int arr[num] = { 0 }; // error,数组的定义[]中的值应该是一个常量

	// 代码2
	int* ptr = NULL;
	ptr = (int*)malloc(num * sizeof(int)); // malloc函数的单位为字节
	if (ptr == NULL) // 内存分配失败
	{
		perror("malloc"); // 输出错误信息,方便调试
		return;
	}
	// 内存分配成功后执行逻辑
	int i = 0;
	for (i = 0; i < num; i++)
	{
		*(ptr + i) = 0;
	}

	// 内存使用完毕
	// 回收空间
	free(ptr);
	ptr = NULL; // free函数并不会改变ptr本身的值,需要程序员手动将指针置空

	return 0;
}

在这里插入图片描述

注意
在最后的ptr = NULL这条语句是非常有必要的,因为没有这条语句,ptr此时已经变成了野指针,这是十分危险的;

2. calloc

除了能使用malloc函数进行内存分配之外,c语言还提供了calloc函数来进行内存分配;

void* calloc(size_t num, size_t size);

大家可能会疑惑,我们已经有malloc函数,为什么还要来一个calloc呢?
请接着往下看:
在这里插入图片描述

  • 这个函数的功能是分配一片空间,并对这片空间进行初始化(初始化为0);
  • calloc将会为num个元素分配一片内存空间,每个元素占size个字节,并在分配空间之后,以字节为单位将这片空间的值初始化为0;
  • 这个函数的有效结果就是分配一片初值为0的,有num * size这么多字节的空间;
  • 如果size为0,它的返回值依赖于特定的库实现(不一定是NULL指针),但这个指针一定不能被解引用;
  • 两个参数,num表示元素的个数,size表示一个元素占的字节数;
  • 如果分配成功,函数会返回一个指向这片空间的指针,指针的类型为void *,它可以根据需求,由使用者自由的强制转换成需要的类型,并进行解引用操作;
  • 如果分配失败,将会返回一个NULL指针;

从上面的解读中,我们就能知道,malloc函数仅仅只是分配了这么一片空间,但其中的值是没有被初始化的,calloc函数能够在分配空间的同时,将这片空间的每个字节都初始化为0;

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

int main()
{
	int *ptr1 = (int*)malloc(10 * sizeof(int));
	if (ptr1 == NULL)
	{
		perror("malloc");
		return;
	}

	int *ptr2 = (int*)calloc(10, sizeof(int));
	if (ptr2 == NULL)
	{
		perror("calloc");
	}

	free(ptr1);
	ptr1 == NULL;
	free(ptr2);
	ptr2 = NULL;

	return 0;
}

在这里插入图片描述
所以当我们对申请的空间的内容有初始化的要求时,就可以使用calloc函数很方便的完成了;

3. realloc

有了上面的两个内存分配函数,我们能够在程序执行中根据需求为变量分配内存空间;
但是仔细想想,当我们在分配完空间之后,这片空间仍不足以满足需求呢?这时应该怎么办?
是再使用malloc或calloc函数重新分配一片空间,并将原来的空间回收吗?
这样是否有点麻烦,有没有其他方法?
有的,兄弟有的。
使用realloc函数;

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

在这里插入图片描述

  • realloc函数可以重新分配动态分配的空间,改变ptr指向空间的大小;
  • 这个函数可能会分配一片新的内存空间(返回值指向的空间);
  • 这片空间的内容会保留到新大小和旧大小中较小的那个,即使这片空间是一片新分配的空间;如果新大小更大,新分配空间中的内容是未定义的;
  • 如果参数ptr是NULL指针,这个函数的行为和malloc相同,分配一片大小为size字节的空间,并返回一个指向它的指针;
  • 在C90标准中,如果参数size为0,调用该函数的行为就像调用了free一样,它会释放掉ptr指向的空间,并返回一个NULL指针;在C99/C11标准中,则取决于特定的库实现,该行为未定义;
  • 返回值有两种情况:
    1. 返回的指针和ptr相同;
    2. 返回的指针和ptr不同,是一个新的地址;
  • 返回指针的类型为void*,原因和前面的相同;
  • C90标准中,有两种情况会返回NULL指针:参数size为0;空间分配失败;
  • C99/C11标准中,只有空间分配失败会返回NULL指针。参数size为0的这种行为未定义;

在这里插入图片描述

看下面代码:

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

int main()
{
	int* ptr = (int*)malloc(100);
	if (ptr == NULL)
	{
		perror("malloc");
		return;
	}
	// 业务处理

	// 扩充容量
	// 代码1
	ptr = (int*)realloc(ptr, 1000); 

	// 代码2
	int* p = (int*)realloc(ptr, 1000);
	if (p == NULL)
	{
		perror("realloc");
		return;
	}
	ptr = p;
	
	// 业务处理

	free(ptr);
	ptr = NULL;

	return 0;
}

上面代码中代码1和代码2正确吗,应该用哪个?

根据上面的函数解读,我们知道realloc函数是可能出现分配失败的情况的。
在代码1中,如果realloc失败,它会返回NULL指针,此时直接将ptr设为了NULL指针,这就导致了内存泄漏,之后再也找不到ptr指向的空间了;
所以代码1是不可行的,应该使用代码2,在调用realloc之后,先判断它的返回值,看看空间是否分配成功,成功之后,才将分配的地址赋值给ptr;

三、常见的动态内存错误

1. 对NULL指针的解引用操作

#include <limits.h>

void test()
{
	int* p = (int *)malloc(INT_MAX / 4);
	*p = 20;
	free(p);
	p = NULL;
}

这段代码正确吗?
错误,因为malloc函数可能返回NULL指针,当空间分配失败时,ptr指针为NULL指针,对NULL指针解引用是错误的;

2. 对动态开辟空间的越界访问

#include <stdio.h>

void test()
{
	int i = 0;
	int *p = (int *)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return;
	}

	for (i = 0; i < 11; i++)
	{
		*(p + i) = 1;
	}

	free(p);
	p = NULL;
}

这段代码也是有问题的,
malloc函数只为p分配了10个int类型的空间,循环会访问到第十一个int类型的元素,此时产生了越界;

3. 对非动态开辟内存使用free释放

#include <stdlib.h>

void test()
{
	int arr[10] = { 0 };
	int *p = arr;
	free(p);
}

这段代码就是使用了free函数释放不是动态分配的内存空间,此时也会出错;

4. 使用free释放一块动态开辟内存的一部分

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

void test()
{
	int *p = (int *)malloc(10 * sizeof(int));
	if (p == NULL)
	{	
		perror("malloc");
		return ;
	}

	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p++) = i;
	}

	free(p);
	p = NULL;
}

这段代码同样是有问题的,在循环中,指针p指向的位置已经发生了变化,此时指针p并没有指向动态分配的内存空间的起始位置,此时使用free函数来释放动态分配的空间,仍有一部分没有释放,所以这个代码是错误的;

5. 对同一块动态内存多次释放

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

void test()
{
	int *p = (int *)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return ;
	}

	// 业务实现
	free(p);

	// 业务实现
	free(p);
}

这种代码也是错误的,它对同一片动态内存空间释放了两次;
虽然,上面的代码不太可能会出现(毕竟没有谁会在同一个函数中对一片动态分配的内存空间释放多次),但这样的错误的可能会出现的;
动态分配的空间,只有两种情况会释放,程序结束、手动free;有可能出现,你在这个函数中将这个空间释放了一次,在另一个函数中又对它释放了一次,这时就出现了多次释放的错误;

有方法可以规避这种错误:

free(p);
p = NULL;

上面的这两行代码就能规避多次释放的问题,还记得free函数的特点吗?
当free函数的参数为NULL指针时,调用这个函数什么也不会发生。
在每次释放指针指向的动态内存空间之后,紧接着将这个指针置为NULL指针,能够有效的规避这种错误的出现;

6. 动态开辟内存忘记释放(内存泄漏)

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

void test()
{
	int *p = (int *)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return ;
	}

	// 业务处理

	// 忘记释放动态内存空间
	// 出现内存泄漏
}

int main()
{
	test();
	// 业务处理

	return 0;
}

上述代码中,主函数调用了test函数,动态分配了一个内存空间给指针p,在test函数退出时,并没有释放这片空间,这片空间就一直留存在堆区中,且指针p是test函数的局部变量,在test弹出之后,这个变量就销毁了,这片动态分配的空间也就没有人能够找到了,所以这片空间在之后的程序中是无法再被使用的,只有在整个程序结束时,这片空间才会还给OS,这就产生了内存泄漏问题。
可能有人会这没什么,反正程序结束,这片空间会归还给OS的,我们不用管;
但是试想一下,这个程序如果是一个服务器程序呢?它需要长期的执行,每次调用test函数都会泄漏一部分的内存空间,硬件资源总是有限的,那么这个程序在运行一段时间之后就一定会因为内存资源不足而挂掉,如果2天这个程序就把内存消耗光了,就得2天重启一次程序,这会带来很大的损失;

忘记释放不再使用的动态开辟的空间会造成内存泄漏
切记
动态开辟的空间一定要释放,并且要正确的释放

四、使用动态内存修改通讯录

在之前的通讯录结构体类型中,不管什么情况,通讯录都能保存1000个人的信息,如果这个人他只有100个朋友的信息需要保存,他使用这个通讯录会浪费剩下的900个空间,这会造成内存资源很大的浪费;如果他需要保存的信息超过1000个,那么这个通讯录就无法满足他的需求;
这时我们就可以使用动态内存来替换掉之前使用的静态分配的空间,在程序执行过程中,根据用户的需要增加通讯录的空间,即满足了用户要保存人的信息的需求,又减少了系统资源的开销;

// 静态版本
// 定义结构体、标识符常量
#define MAXNUM 1000 // 通讯录中能保存的人数

typedef struct
{
	int age; // 年龄
	char name[20]; // 名字
	char sex[7]; // 性别
	char phone_number[20]; // 电话号码
	char address[30]; // 地址
} Peoinfo; // 保存人的信息

typedef struct
{
	Peoinfo data[MAXNUM]; // 通讯录可以保存1000个人的信息
	int count; // 当前通讯录中的人数
} Contact;

修改后的代码,有改动的代码

// 动态内存版本
#define INIT_COUNT 3 // 通讯录初始能保存3个人的信息
#define INCREASE_COUNT 2 // 每次扩容增加数量

typedef struct
{
	int age; // 年龄
	char name[20]; // 名字
	char sex[7]; // 性别
	char phone_number[20]; // 电话号码
	char address[30]; // 地址
} Peoinfo; // 保存人的信息

// 使用指针指向保存人的信息的空间
typedef struct
{
	Peoinfo* data; // 指向保存人信息的空间
	int count; // 当前通讯录中的人数
	int num; // 当前通讯录中能保存的最大人数
} Contact;
void initContact(Contact* contact)
{
	assert(contact);

	// 动态分配 INIT_COUNT * sizeof(Peoinfo) 个字节的空间
	contact->data = (Contact*)calloc(INIT_COUNT, sizeof(Peoinfo));
	contact->count = 0;
	contact->num = INIT_COUNT;
}

static void increase_Capacity(Contact* contact)
{
	// 为通讯录的保存人数扩容
	Contact* ptr = (Contact*)realloc(contact->data, ((contact->num) + INCREASE_COUNT) * sizeof(Peoinfo));
	// 空间分配失败
	if (ptr == NULL)
	{
		perror("checkCapacity, realloc");
		return;
	}
	contact->data = ptr; // 将重新分配的空间的地址赋值给contact
	contact->num += INCREASE_COUNT;
}

void Add(Contact* contact)
{
	assert(contact);
	if (isFull(contact))
	{
		increase_Capacity(contact);
		printf("已扩容\n");
	}

	getchar();
	// 没有做溢出检查
	printf("输入名字:>");
	gets((contact->data[contact->count]).name);

	printf("输入年龄:>");
	scanf("%d", &((contact->data[contact->count]).age));

	getchar();
	printf("输入性别:>");
	gets((contact->data[contact->count]).sex);

	//getchar();
	printf("输入电话:>");
	gets((contact->data[contact->count]).phone_number);

	//getchar();
	printf("输入地址:>");
	gets((contact->data[contact->count]).address);

	(contact->count)++;
}

别忘了要手动释放动态开辟的内存空间

case exit:
	free(contact.data);
	contact.data = NULL;
	printf("退出通讯录\n");
	break;

五、几个经典笔试题

1. 题目1

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

void GetMemory(char* p)
{
	p = (char*)malloc(100);
}
void Test()
{
	char *str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}

int main()
{
	// 调用Test函数会有什么样的结果?
	Test();

	return 0;
}

分析:
根据代码意思,这段代码的目的是在Test函数中定义了一个char *的变量,初始化为NULL指针,调用GetMemory为str动态分配一片空间,之后将“hello world”这个字符串复制到这片动态开辟的空间中;
但事实真的如此吗?
来看看运行结果:
在这里插入图片描述
为什么会这样呢?
我们不是将一个指针作为参数吗?这不是传址调用吗?
真的吗?真的是传址调用吗?
注意,虽然GetMemory的参数是指针,且传入的实参也是指针,但是GetMemory是对这个指针本身进行修改,并不是修改这个指针指向的内容;形参p是str的一份临时拷贝,这个函数是一个传值调用,函数内部的操作是不会对实参本身产生任何影响的;
注意不要看到函数的参数是指针就把它当成传址调用,应该结合这个指针在函数中执行什么样的操作来判断

2. 题目2

#include <stdio.h>

char* GetMemory()
{
	char p[] = "hello world";
	return p;
}

void Test()
{
	char *str = NULL;
	str = GetMemory();
	printf(str);
}

int main()
{
	Test();

	return 0;
}

分析:
上面代码的目的应该是,Test函数通过调用GetMemory函数来获得"hello world"这个字符串的地址,并在Test中输出它;
但结果真的如此吗?
并不是对吧,注意GetMemory函数中的字符数组p是一个局部变量,且没有static修饰,并不是静态变量,所以这个变量的生命周期只在GetMemory函数中,离开函数这个变量就销毁了,所以当Test函数使用这个地址时,其实它已经变成了一个野指针,这片空间指向的内容是什么完全确认;

运行结果:
在这里插入图片描述

可以看到结果和分析的相同,这个地址指向内容变成了随机值;

3. 题目3

#include <stdio.h>

void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}

void Test()
{
	char *str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
}

int main()
{
	Test();

	return 0;
}

分析:
GetMemory函数中动态开辟了一片空间,并通过指针操作,将Test函数传给GetMemory的参数str,此时是一个传址调用,此时的代码是符合设计的;
运行结果:
在这里插入图片描述

4. 题目4

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

void Test()
{
	char *str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

int main()
{
	Test();

	return 0;
}

分析:
这就是一个典型的野指针问题,str接收了动态开辟的空间,在调用了free函数之后,str指向的空间已经被系统回收,此时这片空间已经不属于str,但是并没有手动将str指向NULL,所以if语句块中的语句会被执行,会输出"world"(虽然这片空间已经不属于str,但strcpy这个函数不会管你这些,你给出指令,它就会执行对应的操作);

运行结果:
在这里插入图片描述
在这里插入图片描述

六、C/C++程序的内存开辟

我们曾经简单的介绍了C\C++中程序的内存区域划分,分为栈区、堆区、静态区三个区域,现在我们再对这部分进行详细说明;
在这里插入图片描述
C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元自动释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等;
  2. 堆区:一般由程序员分配释放,若程序员不释放,程序结束可能由OS回收。分配方式类似于链表;
  3. 数据段(静态区)(static):存放全局变量、静态数据。程序结束后由系统释放;
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码;

从这幅图中更能理解为什么用static修饰的局部变量生命周期会变长,因为静态变量存放在数据段,数据段的特点是在上面创建的变量会一直存在,直到程序结束才销毁,所以生命周期变长。

七、柔性数组

在C99中,结构体的最后一个元素允许是未知大小的数组,这就叫做【柔性数组】成员。
例如:

typedef struct st_type
{
	int i;
	int a[0]; // 柔性数组成员
} type_a;

有时上面代码编译器会报错,可以改成:

typedef struct st_type
{
	int i;
	int a[]; // 柔性数组成员
} type_a;

1. 柔性数组的特点

  • 结构中的柔性数组成员前面必须至少有一个其他成员;
  • sizeof返回这种结构大小不包括柔性数组的内存;
  • 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小;

例如:

#include <stdio.h>

typedef struct st_type
{
	int i;
	int a[]; // 柔性数组成员
} type_a;

int main()
{
	printf("%d\n", sizeof(type_a));

	return 0;
}

运行结果:
在这里插入图片描述

2. 柔性数组的使用

// 代码1
#include <stdio.h>
#include <stdlib.h>

typedef struct st_type
{
	int i;
	int a[]; // 柔性数组成员
} type_a;

int main()
{
	int i = 0;
	type_a *p = (type_a*)malloc(sizeof(type_a) + sizeof(int) * 100);
	if (p == NULL)
	{
		perror("malloc");
		return;
	}
	p->i = 100;
	for (i = 0; i < p->i; i++)
	{
		p->a[i] = i;
	}
	free(p);
	p = NULL;

	return 0;
}

上面的代码在堆区开辟了一片空间用于保存这个结构体的信息,这个结构体中有一个长度为100的int数组;
柔性数组a有100个int元素的连续空间;

3. 柔性数组的优势

上面的代码还有另一种写法:

// 代码2
#include <stdio.h>
#include <stdlib.h>

typedef struct
{
	int i;
	int *p_a;
} type_a;

int main()
{
	type_a *p = (type_a*)malloc(sizeof(type_a));
	if(p == NULL)
	{
		perror("malloc p");
		return ;
	}
	p->i = 100;
	p->p_a = (int*)malloc(p->i * sizeof(int));
	if(p->p_a == NULL)
	{
		perror("malloc P_a");
		return ;
	}

	int i = 0;
	for (i = 0; i < p->i; i++)
	{
		*(p->p_a + i) = i;
	}

	// 一定要按照顺序,否则会出错
	free(p->p_a);
	p->p_a = NULL;
	free(p);
	p = NULL;

	return 0;
}

代码1和代码2才能实现同样的功能,但是代码1有两个好处:

  1. 方便内存释放:

    可以看到,在代码2中进行了两次内存释放操作,因为结构体内部的成员也进行了动态内存开辟;
    当这个函数是我们封装好提供给他人使用的函数,别人是不知道,我们也不希望别人知道这个函数是怎样实现的,所以用户是不知道这个结构体内部的成员在使用结束后也需要释放内存,这就会产生问题;
    使用代码1的写法,直接释放结构体指针指向的内存空间,就能够完成动态开辟的内存空间的释放;

  2. 有利于访问速度

    代码1中整个结构体是在一片连续的堆区空间中,代码2则是数组和结构体的内存空间并不连续;连续的内存有益于提高访问速度,也有益于减少内存碎片;

所以上面修改后的通讯录还可以使用柔性数组来实现;

typedef struct
{
	int age; // 年龄
	char name[20]; // 名字
	char sex[7]; // 性别
	char phone_number[20]; // 电话号码
	char address[30]; // 地址
} Peoinfo; // 保存人的信息

typedef struct
{
	int count; // 当前通讯录中的人数
	int num; // 当前通讯录中能保存的最大人数
	Peoinfo data[0]; // 指向保存人信息的空间
} Contact;

在空间不足时使用realloc函数来扩容即可;


总结

本文介绍了动态内存的开辟方法,并结合这个技术修改了之前的通讯录程序,这个技术使得程序员可以在程序执行过程中动态的调整变量占据的内存空间大小,极大的扩展了程序的功能;并在之后详细解读了动态内存开辟的6种错误;在最后介绍了柔性数组;

Docker 实战:使用 Docker Desktop 在 MacOS 上安装 Docker 在这采用 Docker Desktop 在 Mac 上来安装 Docker。Docker Desktop 是 Docker 的管理桌面 IDE,提供了一个直观的GUI(图形用户界面),允许您直接在本机上管理容器、应用程序以及镜像。适用于 Mac、Linux 以及 Windows 环境,允许您构建、共享和运行容器化应用程序和微服务。Docker Desktop 可以大大降低在复杂配置上的时间,因此您可以专注于编写代码。它负责端口映射、文件系统问题和其他默认设置,并定期更新 Bug 修复和安全更新。 阅读详情

相关推荐

大模型应用开发面试全流程实录:RAG、上下文工程与多Agent协作技术深度解析

Transformer 依靠多头自注意力和位置编码实现高效序列建模。Prompt Engineering包含多种设计思路,如 Zero-shot、Few-shot、Chain-of-thought,提升模型泛化和推理。Token 和上下文窗口直接影响模型性能与成本,选择需结合实际业务场景(如客服对话需长窗口)。Embedding 技术是 RAG 的基础,决定了语义检索效果。向量数据库如 FAISS、Milvus等分别适用于本地/分布式检索。

m0_52114506的博客 828

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第二式】指针

字符指针数组指针指针数组数组传参和指针传参几种数组、指针之间的比较函数指针函数指针数组指向函数指针数组的指针回调函数指针和数组的笔试题指针是一个存储地址的变量,地址是一块内存空间的唯一标识;指针的大小是固定的(4字节或8字节);指针是有类型的,它的类型决定它 + - 整数时移动的步长、*解引用操作时的权限;指针是能够进行运算的,+ - 整数、指针相减、指针的关系运算;接下来,就让我们继续探讨指针的进阶主题。对于普通的指针,我们都已经很熟悉了。

PLKDTHFS的博客 1518

C++版Cesium实战:从OpenGL到3DTiles的全流程开发指南

本文提供了一份C++版Cesium全流程开发指南,详细阐述了如何利用OpenGL和Qt从零构建高性能、可离线部署的三维GIS应用。内容涵盖开发环境搭建、OpenGL渲染引擎初始化、Cesium核心坐标系与场景管理移植,并重点解析了3DTiles数据的解析、异步加载与渲染优化等硬核实战技术,旨在帮助开发者实现与现有C++工业软件的深度集成。

weixin_28077113的博客 164

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第一式】数据的存储

本章节的重点是数据类型详细介绍整型在内存中的的存储:原码、反码、补码大小端字节序介绍及判断浮点型在内存中的存储解析char // 字符数据类型short // 短整型int // 整型long // 长整型long long // 更长的整型float // 单精度浮点数double // 双精度浮点数// c语言有没有字符串类型?// 没有以及他们所占存储空间的大小。使用这个类型开辟内存空间的大小(大小决定了使用范围)如何看待内存空间的视角什么是大端小端?

PLKDTHFS的博客 1487

麒麟银河操作系统V10下ffmpeg的完整部署指南(兼容Linux系统)

本文提供在麒麟银河操作系统V10上从源码编译部署FFmpeg的完整指南,涵盖环境准备、核心依赖库(如x264)编译、FFmpeg本体配置安装、系统集成及常见问题排查。步骤详细,同样适用于Ubuntu、CentOS等主流Linux发行版,帮助用户打造功能强大的多媒体处理工具链。

weixin_29205061的博客 255

基于deepseek+dify+达梦实现自然语言查询数据库

自然语言查询数据库信息

weixin_43974943的博客 1593

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第六式】文件操作

为什么使用文件什么是文件文件的打开和关闭文件的顺序读写文件的随机读写文本文件和二进制文件文件读取结束的判定文件缓冲区磁盘上的文件就是文件。但是在程序设计上,我们谈到的文件一般有两种:程序文件、数据文件;本节详细的对c语言中数据文件的使用进行了详细的介绍,包括对文件读写操作使用的函数,以及通过文件位置指针的操作函数来对文件进行随机读写,并指出了feof函数可能出现的使用错误,在最后,介绍了文件缓冲区的概念;

PLKDTHFS的博客 1226

流体动力学仿真软件:XFlow_(20).案例研究:声学仿真

在流体动力学仿真软件中,声学仿真是一个重要的应用领域。通过数值模拟,工程师和科研人员可以预测和分析流体系统中的声学现象,如气动噪声、声波传播和声场分布。这些仿真结果对于优化设计、降低噪声水平和提高系统性能具有重要意义。本节将详细介绍如何在流体动力学仿真软件中进行声学仿真,包括声学模型的选择、参数设置、后处理分析等内容,并通过具体的案例来说明这些步骤。

kkchenjj的博客 902

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第七式】程序的编译

程序的翻译环境程序的执行环境c语言程序的编译+链接预定义符号介绍预处理指令#define宏和函数的对比预处理操作符#和##的介绍命令定义预处理指令#include预处理指令#undef条件编译__FILE__ // 进行编译的源文件__LINE__ // 文件当前行号__DATE__ // 文件被编译的日期__TIME__ // 文件被编译的时间__STDC__ // 如果编译器遵循ANSI C,其值为1,否则未定义。

PLKDTHFS的博客 1195

YOLO11改进|注意力机制篇|引入ELA注意力机制

YOLO11添加ELA注意力机制

A1983Z的博客 3647

101、LLC谐振变换器的传递函数推导

文章摘要 LLC谐振变换器的传递函数推导是电源设计的关键环节。作者通过一次炸管事故案例,揭示了忽视传递函数计算的严重后果。LLC传递函数复杂的根源在于其三个谐振元件(Lr、Cr、Lm)的变频工作特性,导致增益曲线非单调变化。文章详细介绍了采用扩展描述函数法的建模思路,指出Lm对增益曲线的重要影响,并给出了传递函数的数学表达式及频率响应特性。通过实际调试案例,作者对比了错误补偿器设计(Type II)与优化方案(Type III)的效果差异,强调了实测验证的重要性。最后总结了5条实用经验,包括留足裕度、关注启

Jack010169的博客 152

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第三式】字符函数和字符串函数

c语言中对字符和字符串的处理非常频繁,但c语言本身是没有字符串类型的,字符串通常存储在常量字符串或字符数组中。常量字符串适用于不需要对其进行修改的字符串函数。strlen函数的作用是获得一个字符串的长度;接收一个const修饰的字符指针变量,这个指针指向的字符串必须有结束符\0;返回一个无符号(易错点)的int类型的数值,表示这个字符串的长度(不包括结束符\0如果没有返回值,则表示出错;// 使用示例int main()printf("a的长度 = %d\n", strlen(a));

PLKDTHFS的博客 926

YOLOv11模型改进全攻略:利用轻量化模块实现精度与速度双跃升

如果你在实操中遇到模块替换导致的精度骤降或部署报错,不妨尝试调整轻量化模块的替换比例,或针对性地保留关键层的原始结构。记住,轻量化的核心是“在精度损失可接受的范围内最大化提升效率”,只要结合业务场景灵活取舍,你也能让YOLOv11在资源受限的环境中大放异彩。接下来,我们从模块选型到代码集成,一步步带你打造“轻量且能打”的YOLOv11,真正帮你解决工业落地中的效率与精度痛点。训练过程中关注模型的mAP和推理速度,若精度下降过多,可调整模块替换的比例(如仅替换Backbone的部分卷积层)。

m0_64661770的博客 501

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第四式】自定义类型详解(结构体、枚举、联合)

c语言中虽然定义得有许多的类型,如char、int、double等等,但是现实生活中需要描述的问题,仅仅使用这些已有类型是不够的,所以在c语言中,程序员可以根据自己的需要,定义出自己的类型结构,以满足需求。结构体:结构体类型的声明结构的自引用结构体变量的定义和初始化结构体内存对齐结构体传参结构体实现位段(位段的填充&可移植性)枚举枚举类型的定义枚举的优点枚举的使用联合联合类型的定义联合的特点联合的使用有了结构体类型,之后应该如何定义变量呢?

PLKDTHFS的博客 989

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

C语言修炼秘籍之——动态内存分配

客官可是在寻找动态内存分配秘方?快快请进,且听我娓娓道来~

2301_80029060的博客 1241

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

c语言修炼秘籍【第五章】指针

本文会从介绍指针是什么开始,循序渐进地介绍指针的使用、指针使用时会遇到的问题、指针能进行哪些运算、指针与数组之间的关系,以及指针数组这些内容。本文对c语言中使用的指针进行了较简略的介绍,包括什么是指针,指针类型有什么用,指针的一些运算,什么是野指针,如何利用指针访问数组,二级指针使用中会出现的问题,以及指针数组。

PLKDTHFS的博客 1763

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

C语言修炼秘籍【第零章】c语言概述

c语言是一门编程语言。语言的作用就是交流,人们可以利用它与计算机进行交流,让计算机知道你需要它完成什么工作。本系列文章是对笔者在c语言学习的总结记录。本文会先粗略的介绍c语言中需要学习的内容。

PLKDTHFS的博客 1664

【C++修炼之路】C++动态内存管理

C语言的malloc/free如同原始人的燧石,C++用new/delete将其淬炼为青铜利刃

m0_74779094的博客 1021

c语言修炼秘籍【第二章】函数

本文会从函数是什么开始,逐一介绍函数的语法结构,如何声明和定义一个函数,函数的参数类型,调用函数时的传参类型,函数的调用方式,函数递归,并在最后对汉诺塔、青蛙跳台阶和八皇后问题进行详细分析(包含全部代码)

PLKDTHFS的博客 1443

c语言修炼秘籍【第三章】数组

本文将会介绍常用到的一维数组和二维数组是如何使用及它们在内存中是如何存储,说明何为数组越界,并讨论数组在作为函数参数时应该如何使用。在最后附带三子棋和扫雷的代码实现详解。本文对一维和二维数组的初始化、创建、使用,作出的简要的介绍,并通过冒泡排序算法的实现,详细说明了当数组作为函数参数时的性质 - - 传递的是首元素地址。在最后,给出了两个数组的使用实例,附带了完整的实现代码,并配有详细注释。

PLKDTHFS的博客 1257

c语言修炼秘籍【第四章】操作符

本文会对c语言中使用到的操作符进行介绍,包括算术操作符移位操作符位操作符赋值操作符单目操作符关系操作符逻辑操作符条件操作符逗号表达式下标引用、函数调用和结构成员表达式求值单目操作符的种类!// 逻辑反操作- // 负值+ // 正值& // 取地址sizeof // 操作数的类型长度(以字节为单位)~ // 按二进制位取反++ // 前置、后置++-- // 前置、后置--* // 间接访问操作符(解引用操作符) -- 指针的使用。

PLKDTHFS的博客 1136

c语言修炼秘籍【第一章】分支与循环语句

在处理现实中的问题时,我们总会遇到需要作出选择或是需要重复多次的完成一种事件。你去图书馆学习,需要日复一日的坚持,每多学一天,你的代码功力就能+1,当你的代码功力,达到了某一程度,你就能够迎娶白富美,走上人生巅峰。为了实现这些目的,c语言提供了分支语句,用以实现选择;提供循环语句,用以实现重复多次的机械活动;表达式语句函数调用语句控制语句复合语句空语句分支、循环语句属于控制语句。控制语句用于控制程序的执行流程,以实现程序的各种结构方式,它们由各种特定的语句定义符构成;

PLKDTHFS的博客 1244

c语言修炼秘籍【第六章】结构体

将结构体与数组进行对比:数组是一组相同类型的元素集合;结构体是一些值的集合,它们类型可以不同,这些值被称为成员变量。使用结构体类型定义变量int x;int y;} p1;// 声明类型的同时定义变量p1// 定义结构体变量 p2// 初始化:定义变量的同时赋初值struct Stu// 名字int age;// 年龄int data;// 结构体嵌套的初始化// 结构体嵌套的初始化本文对c语言中使用到的结构体进行了简单介绍,更详细的会在后续攻略中详解。

PLKDTHFS的博客 761

C语言 | 进阶修炼

C语言进阶学习笔记

thoahz的博客 1741

c语言修炼秘籍【第七章】const与c语言中一些错误代码

而数组内部的内存使用分配是从低到高的,arr[0]的地址在最低位,当循环进行到第11次 - - arr[11]设置的其实是i的值,此时将 i 的值设为了0,循环继续。这是因为,程序要进行报错也是需要执行时间的,访问arr[11]时程序仍处于循环中,没有时间报错就又进入了下一次循环。test1()可以正常使用指针对n为进行修改,也可以对p指向的位置进行修改。test2()不能通过指针对n进行修改,可以对p指向的位置进行修改。,arr后分配,地址会比i的地址低,分配到的地址为。,i先分配空间,地址为。

PLKDTHFS的博客 674

C语言修炼手册(四万字)

持续更新中....

LYC_462857656的博客 1185
上一篇: c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第四式】自定义类型详解(结构体、枚举、联合)
下一篇: c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第六式】文件操作
YKPG
博客等级 码龄6年 344粉丝 37原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值