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

使用 Python 调用 GitHub REST API 爬取 GitHub 仓库信息(完整指南) GitHub 提供了功能强大的 REST API,允许开发者通过编程方式访问 GitHub 上的仓库、用户、提交记录、PR(Pull Request)等信息。GitHub REST API 是 GitHub 官方提供的 API 接口,支持通过 HTTP 请求来操作 GitHub 上的资源。在本文中,我们将使用 Python 编写一个完整的爬虫程序,调用 GitHub 的 REST API 来爬取 GitHub 仓库的信息。目前 GitHub 使用的主要是 v3 版本的 REST API。 阅读详情

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种错误;在最后介绍了柔性数组;

GEE:计算非水体概率影像(水体淹没次数) 本文记录了在GEE平台上使用哨兵数据计算沿海地区非水体概率图像的计算方法和代码。基于像素,概率范围0-100。首先使用哨兵数据(s2)和大津算法(OTSU)计算的水体指数阈值来提取水体,然后计算研究区非水体像素在时间段内出现的概率。 阅读详情

相关推荐

HDMI2.1接口保护指南:从浪涌损坏案例看RK3588板子的ESD设计要点

本文深入探讨了HDMI2.1接口在RK3588板子上的ESD防护设计要点,通过真实案例分析了浪涌损坏的原因及解决方案。重点介绍了TMDS信号链路的四级防护体系、TVS管选型误区以及连接器与PCB的协同设计策略,帮助工程师有效避免静电放电导致的HDMI无输出故障。

weixin_29305337的博客 446

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

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

PLKDTHFS的博客 1518

海康/大华设备如何接入GB28181平台?最新2022版互联避坑指南

本文详细解析了海康/大华设备接入GB28181平台的最新2022版互联避坑指南,涵盖参数配置、实时视音频点播协议交互及典型故障排查。重点介绍了GB/T28181-2022标准下的设备编码规范、SIP服务器配置及媒体流保活机制,帮助工程团队高效解决设备离线和媒体流中断等问题。

weixin_30733003的博客 686

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

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

PLKDTHFS的博客 1487

SECS/GEM协议中的关键指令详解:从S1F1到S9F9的完整指南

本文详细解析了SECS/GEM协议中从S1F1到S9F9的关键指令,涵盖设备状态管理、控制诊断、报警处理等核心功能。通过实际案例和调试技巧,帮助开发者掌握半导体制造设备通信标准,解决SECS/GEM协议在实际工程应用中的通讯难题,提升生产线智能化水平。

weixin_26899659的博客 397

地理空间坐标系统-不同椭球基准间的坐标转换

基于同一个椭球基准,可以构建三类地理空间坐标系:大地球面坐标系、大地空间直角坐标系、大地投影坐标系。由于坐标原点和椭球长短半径等都相同,这三类坐标系之间可以通过无损的数学方法相互之间进行转换。那么现在问题来了,不同椭球基准(在全球范围来看存在成百上千的椭球基准)之间的坐标怎么转换?比如,北京54、西安80、国家2000,特别是在我国,现在主推国家2000坐标系,那么如何将历史的测绘成果(北京54和...

技术改变世界 8336

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

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

PLKDTHFS的博客 1226

UG二次开发避坑指南:如何正确配置Python环境让NXOpen脚本跑起来?

本文详细介绍了UG/NX二次开发中Python环境配置的全流程与NXOpen脚本调试技巧,帮助开发者避开常见陷阱。从NXOpen架构解析到Python解释器版本匹配,再到环境变量设置与路径问题处理,提供了一套经过验证的配置方案,确保脚本顺利运行。

weixin_33859135的博客 448

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

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

PLKDTHFS的博客 1195

Codex入门00-初识神器(小白入门:到底什么是Codex CLI,和ChatGPT写代码有什么不同?)

OpenAI Codex CLI 是 OpenAI 于2025年推出的终端原生AI编程代理,不是在网页里聊天写代码,而是直接在你的终端里自主读代码、改文件、跑命令。它拥有三种安全模式、/goal 持久化任务、云端沙箱执行等独特能力。本文从零介绍 Codex CLI 的核心概念、与 ChatGPT 写代码的本质区别、典型使用场景和费用说明,让你全面认识这个改变开发方式的神器。

yueyeguzhuo的专栏 302

Python FastAPI 面试题及参考答案

FastAPI 的路由机制是其核心功能之一,用于将不同的 URL 路径映射到相应的处理函数。通过定义路由,可以构建出功能丰富的 API。在 FastAPI 中,使用装饰器来定义路由。在这个示例中,是一个装饰器,它将根路径"/"的 GET 请求映射到read_root函数。当客户端发送一个 GET 请求到根路径时,FastAPI 会调用read_root函数并返回结果。

大模型大数据攻城狮的专栏 2247

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

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

PLKDTHFS的博客 926

高空抛物监控系统:基于YOLOv8的轨迹追踪与可视化系统

本文详细介绍了一种基于YOLOv8深度学习模型的高空抛物监控系统,该系统能够实时检测并追踪高层建筑抛掷物的运动轨迹。文章包含完整的系统设计思路、算法实现细节、数据集构建方法以及基于PyQt5的可视化界面开发过程。我们提供了完整的Python实现代码,并讨论了系统的性能优化策略和实际部署考量。此外,我们还推荐了几个公开可用的高空抛物数据集,并详细分析了系统的应用前景和技术挑战。

YOLO 446

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

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

PLKDTHFS的博客 989

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

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

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

2301_80029060的博客 1241

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

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

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

PLKDTHFS的博客 1763

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的博客 1740

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语言小设计之——成绩统计

余从开始决定自学嵌入式到现在已经一年了,回顾一路走来学了linux系统管理、ARM编程、系统移植、驱动一系列课程,但是现在仍然感觉动手写代码的能力差之甚远,痛定思痛,于是重新开始修炼C语言,以求达到深入理解C语言并能写出高质量C代码的层次。我相信很多同学肯定和我一样,虽然了解C语言的基本语法,能写单片机和ARM程序或者解一些简单的题目,但都只停留在了解阶段,对C语言的精髓及其重点知之甚少。我决定一

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

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值