C语言详解(动态内存管理)2

news2024/11/27 0:48:19

Hi~!这里是奋斗的小羊,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~
💥💥个人主页:奋斗的小羊
💥💥所属专栏:C语言

🚀本系列文章为个人学习笔记,在这里撰写成文一为巩固知识,二为展示我的学习过程及理解。文笔、排版拙劣,望见谅。


目录

  • 前言
  • 1、常见动态内存错误
      • 1.1 对NULL指针的解引用操作
      • 1.2 对动态内存空间的越界访问
      • 1.3 对非动态开辟内存使用free释放
      • 1.4 使用free释放动态内存的一部分
      • 1.5 对同一快动态内存多次释放
      • 1.6 动态开辟内存忘记释放(内存泄漏)
  • 2、动态内存经典笔试题分析
      • 2.1 题目一
      • 2.2 题目二
      • 2.3 题目三
      • 2.4 题目四
  • 3、柔性数组
      • 3.1 什么是柔性数组
      • 3.2 柔性数组的特点
      • 3.3 柔性数组的使用
      • 3.4 柔性数组的优势
  • 总结

前言

总的来说,动态内存管理为我们提供了更加灵活、高效和可扩展的内存管理方式,但动态内存管理函数可能会带来一些风险,主要包括内存泄漏、内存溢出和野指针等问题,我们在使用动态内存管理函数时要多留心,避免风险的出现


1、常见动态内存错误

1.1 对NULL指针的解引用操作

如果我们写的代码不严谨,没有考虑到动态内存分配失败的可能,就会写出类似于下面的代码:

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

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	
	//直接使用指针p
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p[i] = i + 1;
	}
	return 0;
}

这样的代码可能并没有什么问题,但是存在很大的隐患,因为动态内存函数是有可能开辟内存空间失败的,当开辟失败时会返回NULL,而NULL指针是不能解引用的
像VS这样比较强大的编译器会立马检测到并提示你
在这里插入图片描述

为了避免这种错误,我们需要对指针p进行判断,再决定是否使用

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

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	//判断p是否为空指针
	if (p == NULL)
	{
		//打印出错误信息
		perror("malloc");
		//终止程序
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p[i] = i + 1;
	}
	return 0;
}

1.2 对动态内存空间的越界访问

我们用动态内存函数开辟多大的空间,我们就使用多大的空间,不能越界访问,例如:

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

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	//判断p是否为空指针
	if (p == NULL)
	{
		//打印出错误信息
		perror("malloc");
		//终止程序
		return 1;
	}
	int i = 0;
	//p+1跳过1个整型,p+10就会越界
	for (i = 0; i <= 10; i++)
	{
		p[i] = i + 1;
	}
	return 0;
}

聪明的VS也会检测出错误提示你
在这里插入图片描述


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

free函数是用来释放由动态内存函数开辟的空间的,不能释放普通内存

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

int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	free(p);
	p = NULL;
	return 0;
}

当我们运行起来后就出问题了
在这里插入图片描述


1.4 使用free释放动态内存的一部分

上面我们用malloc函数申请了10个整型空间,然后通过for循环给这10个整型空间内放1~10的整数,有些同学可能会为了方便这样写代码:

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

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	//判断p是否为空指针
	if (p == NULL)
	{
		//打印出错误信息
		perror("malloc");
		//终止程序
		return 1;
	}
	//给申请的动态空间内存1~10
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*p++ = i;
	}
	//释放动态内存空间
	free(p);
	p = NULLreturn 0;
}

当我们运行起来才发现写出了BUG
在这里插入图片描述
这又是为什么呢?
事实上此时free(p)中的p指针已经不再指向malloc开辟的动态内存的起始地址了,因为*p++这里对p的指向不断递增

free操作的指针必须指向要被释放的动态内存的起始地址


1.5 对同一快动态内存多次释放

当我们用完一块动态内存空间后不再使用对其释放后,可能会因为忘记而重复释放一次,并且如果第一次释放时忘记给p指针赋NULL,那么程序就会出错

	//使用...
	
	//释放动态空间
	free(p);

	//...

	free(p);
	p = NULL;
	return 0;

但是如果我们两次释放时都给p指针赋了NULL,那基本不会发生什么事,相当于没有错,只是逻辑上讲不通
所以,在我们用free释放完动态内存空间后,紧跟着对指针赋NULL是很有必要的


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

动态开辟的空间一定要释放,并且正确释放

当我们写代码的时候,存在这样一种可能会出现的错误,那就是动态开辟的内存忘记释放或者因为某些原因还没有到free语句就提前终止代码,这里举个简单的例子

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

void text()
{
	int flag = 1;
	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		return 1;
	}

	//使用
	
	//因为某些原因函数提前返回了
	if (flag == 1)
	{
		return;
	}

	//free函数
	free(p);
	p = NULL;
}

int main()
{
	//自定义函数
	text();

	//后面还有大量代码
	//....

	return 0;
}

虽然我们确实用了free函数释放空间,但是当代码量较大时可能会因为某些原因还没到free函数就提前终止了,而我们还没意识到,就算后面我们意识到了这个问题这块内存我们也找不到了
只有整个程序结束后这块内存才能被释放,如果程序一直不结束这块空间就再也找不到了,这就叫内存泄漏

所以,就算动态内存申请使用后用了free,也是有可能犯内存泄漏的错误,我们要多加小心

内存泄漏是比较可怕的,尤其是某些24小时不断运行的服务器程序,如果存在内存泄漏,内存被耗干也只是时间的问题


2、动态内存经典笔试题分析

2.1 题目一

请问运行下面 text函数会有什么后果?

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

void get_memory(char* p)
{
	p = (char*)malloc(100);
}

void text(void)
{
	char* str = NULL;
	get_memory(str);
	strcpy(str, "hello world");
	printf(str);
}

int main()
{
	text();
	return 0;
}

上面的代码一共有两个问题
第一个问题:malloc申请动态内存空间后没有使用free函数释放,这可能会导致内存泄漏

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

void get_memory(char* p)
{
	p = (char*)malloc(100);
}

void text(void)
{
	char* str = NULL;
	get_memory(str);
	strcpy(str, "hello world");
	printf(str);

	free(str);
	str = NULL;
}

int main()
{
	text();
	return 0;
}

第二个问题: 函数传参传值调用和传址调用使用错误

这个代码的意思是申请一块动态内存空间地址交给指针p,通过指针p再交给指针str,再使用strcpy函数将字符串拷贝到动态内存空间内,最后打印出字符串
但是get_memory函数传参的时候使用的是传值调用,所以指针p跟指针str没有关系

有两种纠错方法
方法一: 将传值调用改为传址调用,此时p为二级指针

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

void get_memory(char** p)
{
	*p = (char*)malloc(100);
}

void text(void)
{
	char* str = NULL;
	get_memory(&str);
	strcpy(str, "hello world");
	printf(str);

	free(str);
	str = NULL;
}

int main()
{
	text();
	return 0;
}

在这里插入图片描述

方法二: 直接返回指针p的地址,不需要传参

char* get_memory()
{
	char* p = (char*)malloc(100);
	return p;
}

void text(void)
{
	char* str = NULL;
	str = get_memory();
	strcpy(str, "hello world");
	printf(str);

	free(str);
	str = NULL;
}

int main()
{
	text();
	return 0;
}

在这里插入图片描述


2.2 题目二

请问运行下面 text函数会有什么后果?

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

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

void text(void)
{
	char* str = NULL;
	str = get_memory();
	printf(str);
}

int main()
{
	text();
	return 0;
}

上面的代码是一个非常经典的例子,之前在C语言(指针)3中野指针一小节介绍过类似的例子

上面代码的问题:
我们在自定义函数get_memory中创建了一个局部临时数组存入字符串“hello world”,再将字符串的首地址返回用指针str接收,虽然此时指针str确实指向字符串“hello world”的首地址,但是此时str是没有权限访问这块空间的

因为在局部数组p在出了get_memory函数后就销毁了,它申请的空间会被收回,即使指针str能找到这块空间,但是它已经没有权限使用了,此时str就是一个野指针

在这里插入图片描述

所以我们应该避免返回栈空间地址

想要改正上面的代码也很简单,我们申请一块动态内存就行,同时也别忘了释放

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

char* get_memory(void)
{
	char* p = (char*)malloc(20);
	strcpy(p, "hello world");
	return p;
}

void text(void)
{
	char* str = NULL;
	str = get_memory();
	printf(str);
	
	free(str);
	str = NULL;
}

int main()
{
	text();
	return 0;
}

2.3 题目三

请问运行下面 text函数会有什么后果?

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

void get_memory(char** p, size_t num)
{
	*p = (char*)malloc(num);
}

void test(void)
{
	char* str = NULL;
	get_memory(&str, 100);
	strcpy(str, "hello world");
	printf(str);
}

int main()
{
	test();
	return 0;
}

上面的代码是可以打印出“hello world”的,但是遗憾的是上面的代码中使用了动态内存函数malloc,但是没有使用free函数释放动态内存空间
虽然上面的代码可以实现我们想要的效果,但这样的代码是存在安全隐患的

动态内存开辟函数malloccallocrealloc和动态内存释放函数free必须成对出现

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

void get_memory(char** p, size_t num)
{
	*p = (char*)malloc(num);
}

void test(void)
{
	char* str = NULL;
	get_memory(&str, 100);
	strcpy(str, "hello");
	printf(str);
	free(str);
	str = NULL;
}

int main()
{
	test();
	return 0;
}

2.4 题目四

请问运行下面 text函数会有什么后果?

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

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

int main()
{
	test();
	return 0;
}

使用malloc函数申请一块100个字节大小的动态内存空间放入字符串“hello”,然后使用free函数释放这一动态内存空间
但是此时指针str中还存着我们开辟的动态内存空间的地址,正确的写法free函数后应紧跟str = NULL;,但是上面的代码并没有这一条语句
if语句判断的时候指针str确实是不为空指针的,进入if语句后执行strcpy(str, "world");这条代码,根据我们对strcpy函数的了解,这里还要对指针str解引用,但是指针str我们之前已经用free函数释放过了,并且没有赋NULL所以str此时是野指针不能解引用,运行起来程序就会出错
在这里插入图片描述

这道题考察的还是free函数后紧跟p = NULL的问题


3、柔性数组

3.1 什么是柔性数组

C99中,结构体中的最后一个成员允许是未知大小的数组,这就叫柔性数组成员

  • 在结构体中
  • 最后一个成员
  • 未知大小的数组
struct S1
{
	int n;
	char c;
	double d;
	int arr[];//未知大小的数组
};
struct S2
{
	int n;
	char c;
	double d;
	int arr[0];//未知大小的数组
};

上面两种写法中arr都是柔性数组成员
有些编译器可能只支持其中的一种写法,VS中两种写法都支持


3.2 柔性数组的特点

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

在这里插入图片描述

正是因为sizeof返回的这种结构大小不包含柔性数组的内存,所以结构中的柔性数组成员前面必须至少有一个其他成员,否则结构体的大小没法计算


3.3 柔性数组的使用

包含柔性数组的结构怎么使用呢?
包含柔性数组的结构创建变量不会像一般结构那样创建,而是使用malloc函数进行内存的动态分配

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

struct S
{
	int n;
	int arr[];
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
	if (ps == NULL)
	{
		perror("malloc");
		//终止程序
		return;
	}
	//使用空间
	ps->n = 100;
	int i = 0;
	for (i = 0; i < 20; i++)
	{
		ps->arr[i] = i + 1;
	}
	
	//...
	free(ps);
	ps = NULL;
	return 0;
}

在这里插入图片描述

柔性数组的柔性怎么体现呢?
因为上面包含柔性数组的结构是由malloc函数进行内存的动态分配,所以我们可以使用realloc函数进行动态内存的调整,那这个数组的大小就可大可小

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

struct S
{
	int n;
	int arr[];
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
	if (ps == NULL)
	{
		perror("malloc");
		//终止程序
		return 1;
	}
	//使用空间
	ps->n = 100;
	int i = 0;
	for (i = 0; i < 20; i++)
	{
		ps->arr[i] = i + 1;
	}
	//调整ps指向的空间大小
	struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 40 * sizeof(int));
	//进行指针的非空判断,保护原地址
	if (ptr != NULL)
	{
		ps = ptr;
		//防止ptr变成野指针
		ptr = NULL;
	}
	else
	{
		perror("realloc");
		//终止程序
		return 1;
	}

	for (i = 0; i < 40; i++)
	{
		printf("%d ", ps->arr[i]);
	}

	//...
	free(ps);
	ps = NULL;
	return 0;
}

在这里插入图片描述

如果不使用柔性数组,还有一种办法能实现上面的效果

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

struct S
{
	int n;
	int* arr;
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	int* tmp = (int*)malloc(20 * sizeof(int));
	if (tmp == NULL)
	{
		perror("malloc");
		return 1;
	}
	else
	{
		ps->arr = tmp;
		tmp = NULL;
	}
	ps->n = 100;
	int i = 0;
	//给指针arr指向的20个整型空间赋值
	for (i = 0; i < 20; i++)
	{
		ps->arr[i] = i + 1;
	}

	//调整指针arr指向的空间大小
	tmp = (int*)realloc(ps->arr, 40 * sizeof(int));
	if (tmp != NULL)
	{
		ps->arr = tmp;
		tmp = NULL;
	}
	else
	{
		perror("realloc");
		return 1;
	}
	for (i = 0; i < 40; i++)
	{
		printf("%d ", ps->arr[i]);
	}

	//...
	free(ps->arr);
	ps->arr = NULL;
	free(ps);
	ps = NULL;
	return 0;
}

在这里插入图片描述

结构struct S中有一个指针成员,我们的想法是用malloc函数申请一块动态内存空间,再让结构中的这个指针指向这块动态分配的内存,然后这块由指针指向的动态内存空间就可以用realloc函数进行大小的调整了
可以看到这样实现的效果和柔性数组相似,那柔性数组为什么还要存在呢?
其实相比之下柔性数组还是有它的优势的


3.4 柔性数组的优势

  • 方便内存释放

如果我们的代码是在一个给别人用的函数中,你在里面做了两次内存分配,并把整个结构体返回给用户,用户调佣free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事
所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存释放

  • 这样有利于访问速度

连续的内存有利于提高访问速度,也有利于减少内存碎片
因为malloc等动态内存函数在申请空间时会在堆区允许的地方申请一块连续的空间,但是动态内存函数申请的多个动态内存空间之间并不是连续的,这些空间之间就形成了内存碎片


总结

  • 动态内存管理是一把双刃剑,它能给我们提供灵活的内存管理方式,但同样也会带来风险
  • 检查动态内存分配是否成功:在使用动态内存管理函数时,应该检查分配内存是否成功,以确保程序正常运行,这是比较容易忽略的点

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1803136.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

零空间(Null Space)控制例子

零空间(Null Space)控制是一种用于多任务控制系统的技术,特别适用于机器人和多自由度系统。其基本原理是将控制任务分解为不同的优先级,其中高优先级任务在主空间(Task Space)中执行,而低优先级任务在零空间(Null Space)中执行。这样可以保证在完成主要任务的同时,次…

VScode的插件使用

1、正则插件-1 2、AI助手工具-1-fittentech 3、画图工具-1 4、GitHub的查看工具 5、shell测试工具

vuInhub靶场实战系列--Kioptrix Level #1

免责声明 本文档仅供学习和研究使用,请勿使用文中的技术源码用于非法用途,任何人造成的任何负面影响,与本人无关。 目录 免责声明前言一、环境配置1.1 靶机信息1.2 靶场配置 二、信息收集2.1 主机发现2.2 端口扫描2.2.1 masscan2.2.2 nmap 2.3 指纹识别2.4 目录扫描2.4.1 dirb…

Python在股票交易分析中的应用:布林带与K线图的实战回测

引言 在股票交易的世界中&#xff0c;技术分析是投资者们用来预测市场动向的重要工具。布林带&#xff08;Bollinger Bands&#xff09;作为一种动态波动范围指标&#xff0c;因其直观性和实用性而广受欢迎。本文将通过Python代码&#xff0c;展示如何使用布林带结合K线图来分…

【动态规划-BM79 打家劫舍(二)】

题目 BM79 打家劫舍(二) 描述 你是一个经验丰富的小偷&#xff0c;准备偷沿湖的一排房间&#xff0c;每个房间都存有一定的现金&#xff0c;为了防止被发现&#xff0c;你不能偷相邻的两家&#xff0c;即&#xff0c;如果偷了第一家&#xff0c;就不能再偷第二家&#xff0c;如…

鸿蒙低代码开发一个高频问题

在版本是DevEco Studio 3.1.1 Release&#xff0c;SDK是3.1.0(API9)。 创建和设计的visual文件经常会遇到无法渲染的情况&#xff0c;或者自定义组件在Custom列表中突然不见了的情况。 有以下报错信息的&#xff1a; JSON schema validation error: data/visualModel/value/…

【python报错】TypeError: dict.get() takes no keyword arguments

【Python报错】TypeError: dict.get() takes no keyword arguments 在Python中&#xff0c;字典&#xff08;dict&#xff09;是一种非常灵活的数据结构&#xff0c;用于存储键值对。dict.get()方法是用来从字典中获取与给定键&#xff08;key&#xff09;相关联的值&#xff0…

opera打不开网页最简单的解决办法

如果以上为解决问题&#xff0c;继续下面操作 检查网络连接&#xff1a; 确认您的电脑已连接到互联网。 检查网络连接是否稳定&#xff0c;网络速度慢或链路拥堵可能会导致网页加载失败。 修改Local State文件&#xff1a; 关闭Opera浏览器。 定位到Opera浏览器的配置…

Acrobat Pro DC 2023 for Mac/Win:全平台PDF编辑器的终极解决方案

对于需要处理PDF文档的个人和企业用户来说&#xff0c;Adobe Acrobat Pro DC 2023是一款不可或缺的工具。作为全球领先的PDF编辑器&#xff0c;Acrobat Pro DC 2023在Mac和Windows平台上提供了丰富的功能和令人印象深刻的性能&#xff0c;使其成为用户编辑、转换和管理PDF文档的…

idea debug时提示”Method breakpoints may dramatically slow down debugging“的解决办法

问题现象 今天同事喊我过去看一个问题&#xff0c;项目正常启动的时候没问题&#xff0c;debug模式就卡住了&#xff0c;很久不动。我推测是哪个断点导致的&#xff0c;一看断点果然有情况。在方法上打了断点。 解决方式(Android Studio一样的解决&#xff09; 1、View Brea…

分享一个用python的本地WIFI密码查看器

本章教程&#xff0c;主要分享一个本地wifi密码查看器&#xff0c;用python实现的&#xff0c;感兴趣的可以试一试。 具体代码 import subprocess # 导入 subprocess 模块&#xff0c;用于执行系统命令 import tkinter as tk # 导入 tkinter 模块&#xff0c;用于创建图形用…

Typescript 中 tsconfig.json 无法写入文件,因为它会覆盖输入文件

先来看看问题 在开发ts项目的时候&#xff0c;包错提示无法写入文件 tsconfig.json无法写入文件"url"因为它会覆盖输入文件 这是tsconfig.json文件配置问题&#xff0c;需要加入下面的配置就好了&#xff1a; {"compilerOptions": {"outDir": …

Android开机动画关闭流程

一步一图项目上要加一个开机动画结束的回调&#xff0c;我这边看下如何加 好&#xff0c;老规矩&#xff0c;如何启动动画&#xff1f;动画是谁启动的&#xff1f;怎么关闭的&#xff1f;谁通知关闭的 带着问题看源码 动画的启动流程 开机动画的主入口在哪&#xff1f; 这个…

ChatGPT-4o体验demo

OpenAI 最近推出了其最新的人工智能语言模型——GPT-4O。该模型是在原有 GPT-4 的基础上进行优化而成&#xff0c;旨在提升生成质量和响应速度。GPT-4O 采用了更加高效的架构设计&#xff0c;使其在处理复杂文本时表现出更快的速度和更高的准确性。GPT-4O 在训练过程中融入了最…

【ARM Cache 及 MMU 系列文章 6 -- Cache 寄存器 CTR_EL0 | CLIDR | CCSIDR | CSSELR 使用详解 1】

请阅读【ARM Cache 及 MMU/MPU 系列文章专栏导读】 及【嵌入式开发学习必备专栏】 文章目录 Cache 常用寄存器Cache CSSELR 寄存器Cache CSSELR 使用场景Cache CSSELR 操作示例 Cache CLIDR 寄存器LoUU 介绍LoUU 使用 LoUIS 介绍CLIDR 使用 Cache CCSIDR 寄存器Cache CTR_EL0 C…

手把手教你实现条纹结构光三维重建(1)——多频条纹生成

关于条纹结构光三维重建的多频相移、格雷码、格雷码相移、互补格雷码等等编码方法&#xff0c;我们在大多数平台上&#xff0c;包括现在使用语言大模型提问&#xff0c;都可以搜到相关的理论&#xff0c;本人重点是想教会你怎么快速用代码实现。 首先说下硬件要求&#xff0c;…

【NI国产替代】500 MSPS 采样率,14 bit 分辨率数据采集盒子

• 双高速高精度数据采集通道 • 支持内外精准触发采样模式 • 丰富的总线控制接口 • 抗干扰能力强 高速采集盒子是一款双通道&#xff0c;具有 500 MSPS 采样率&#xff0c;14 bit 分辨率的高速高精度数据采集设备&#xff0c;其模拟输入带宽为 200 MHz&#xff0c;…

【MySQL】常见可执行程序

本文使用的版本是MySQL8&#xff0c;5.7可能会有所不同。 MySQL提供了一些重要的程序用来管理和操作数据库。这里会介绍一些常用的程序及其使用。对于MySQL程序的使用&#xff0c;可以查看官方帮助手册来学习。 MySQL :: MySQL 8.0 Reference Manual :: 6 MySQL Programs 程序…

SM201,SM203主控模块备件

SM201,SM203主控模块备件。MACSV软件安装&#xff1b;二、软件组成及各部分功能&#xff1b;三、组态流程&#xff1b;四、组态详解SM201,SM203主控模块备件&#xff08;组态各部分的操作过程及基本原理&#xff09;。一、MACSV系统软件安装软件安装——计算机角色在每台计算机…

基于STM32智能小车

一、前置准备 前置知识&#xff1a;需要学习stm32&#xff0c;建议去b站看江科大的视频&#xff0c;讲的很详细&#xff0c;学完串口那一块就可以制作了&#xff0c;软件用的是Keil5&#xff0c;开发语言C语言&#xff0c;手机连接蓝牙模块软件是蓝牙调试器。 需要准备的器件…