动态内存管理❀C

news2024/12/23 13:43:25

目录

  • ❀动态内存管理的意义
  • ❀动态内存管理函数
    • malloc - 申请空间
      • free - 释放空间
    • calloc - 申请空间
    • realloc - 调整空间大小
  • ❀常见的动态内存错误
    • 对NULL指针的解引用操作 - err
    • 对动态开辟空间的越界访问 - err
    • 对非动态开辟内存使用free释放 - err
    • 使用free释放一块动态开辟内存的一部分 - err
    • 对同一块动态内存多次释放 - err
    • 动态开辟内存忘记释放(内存泄漏)
  • ❀❀❀
  • ❀柔性数组
    • 柔性数组的特点
    • 柔性数组的优势

❀动态内存管理的意义

基本的内存开辟方式:

int val = 20;//在栈空间上开辟四个字节
int arr1[10];//在栈空间上开辟40个字节的连续空间

但是以上内存开辟的内存空间大小是固定的,不能后续自动改变空间大小。
当实际需要的空间大小比事先开辟的大小小就会造成空间浪费,当不够就需要再另外开辟空间。
但是对于空间的需求,不仅仅是上述的情况。
有时候我们需要的空间大小在程序运行的时候才能知道,编译时开辟空间的方式就不能满足了。
因此就需要动态内存管理的能力。且C语言提供了一些相关函数。

❀动态内存管理函数

在这里插入图片描述

malloc - 申请空间

在这里插入图片描述
在这里插入图片描述
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针。

因此对malloc的返回值一定要做检查。

  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者强制类型转换成需要的类型。
  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。

1、申请成功

//√
#include <stdlib.h>

int main()
{
	//申请空间
	//void* p = malloc(40);//在堆区开辟指定大小的空间后返回该空间的起始地址
	int* p = (int*)malloc(INT_MAX);//用void*指针接收不方便,而此时想存放整形型据就用整型指针接收
	//检查返回值
	if (p == NULL)
	{
		//申请失败
		perror("malloc");//打印malloc:错误信息
		return 1;
	}
	//申请成功
	
	//初始化
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p = i;
		p++;
	}
	
	return 0;
}

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2、申请失败

	int* ptr = (int*)malloc(INT_MAX);//申请的空间太大

在这里插入图片描述

free - 释放空间

C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的。
在这里插入图片描述

  • 如果指向的空间不是动态开辟的,那free函数的行为是未定义的。
int main()
{
	//申请空间
	int* ptr = (int*)malloc(40);//40B
	
	int* p = ptr;
	//检查返回值
	if (p == NULL)
	{
		//申请失败
		perror("malloc");
		return 1;
	}
	//申请成功
	
	//初始化
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p = i;
		p++;
	}

	释放空间
	//free(p);//× - p++后此时p指向的不再是起始位置了
	free(ptr);
	
	return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

	//释放空间
	free(ptr);//ptr还指向已经被释放的不属于自己的空间,此时ptr就是野指针,因此还要继续将ptr置成空指针
	*ptr = 100;//err非法访问
	//释放空间
	free(ptr);
	ptr = NULL;

	if (ptr != NULL)
	{
		*ptr = 100;
	}
//√
//写法1:
int main()
{
	//申请空间
	int* ptr = (int*)malloc(40);	
	int* p = ptr;
	if (p == NULL)
	{
		//申请失败
		perror("malloc");
		return 1;
	}
	//申请成功
	
	//初始化
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p = i;
		p++;
	}
	
	//释放空间
	free(ptr);
	ptr = NULL;
	
	return 0;
}
//√
//写法2:
int main()
{
	//申请空间
	int* ptr = (int*)malloc(40);
	if (ptr == NULL)
	{
		//申请失败
		perror("malloc");
		return 1;
	}
	//申请成功
	
	//初始化
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(ptr + i) = i;
	}
	
	//释放空间
	free(ptr);
	ptr = NULL;

	return 0;
}
  • 如果 memblock 是NULL指针,则函数什么事都不做。
int main()
{
	int* p = NULL;
	free(p);//什么事都不发生

	return 0;
}

必须释放空间吗?
当我们不释放动态申请的内存的时候,如果程序结束,动态申请的内存由操作系统自动回收,但是如果程序不结束,动态内存是不会自动回收的,就会形成内存泄露的问题。

int main()
{
	while (1)
	{
		malloc(1000);
	}

	return 0;
}
//会耗干内存,造成死机

在这里插入图片描述

calloc - 申请空间

1、

使用malloc开辟空间:

int main()
{
	int*p = (int*)malloc(40);

	return 0;
}

#
使用malloc申请的内存空间都是随机值。

使用calloc申请内存空间:

在这里插入图片描述
在这里插入图片描述

int main()
{
	//申请10个整形的空间
	int* p = (int*)calloc(10, sizeof(int));//sizeof(int) - 每个元素的大小

	return 0;
}

在这里插入图片描述
calloc申请的空间会被初始化为0。

喜欢初始化为0就可以选用calloc了。

2、

int main()
{
	//申请10个整形的空间
	int* p = (int*)calloc(10, sizeof(int));
	//开辟失败
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	printf("\n");

	free(p);
	p = NULL;

	return 0;
}

在这里插入图片描述
malloc的底层实现非常复杂。

realloc - 调整空间大小

1、使用场景:

在这里插入图片描述
在这里插入图片描述
有时会我们发现过去申请的空间太小了,有时候又会觉得申请的空间过大了,realloc 函数可以对动态开辟内存的大小做灵活的调整。

2、realloc在调整内存空间的存在两种情况:
(1)当原空间后有足够的未用空间时,直接追加开辟新的空间,返回整个空间的起始地址(同原起始地址);
(2)当没有足够未用的空间时,realloc会重新在空间中的其它位置找一块足够未用的空间,先将原空间的数据复制到新空间,然后再追加,realloc会将原空间主动free掉,返回新空间的起始地址;
在这里插入图片描述

int main()
{
	int*p = (int*)malloc(40);
	
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;//0 1 2 3 4 5 6 7 8 9
	}

	//空间仍不够,希望能放20个元素,考虑扩容 - realloc
	//int* p = (int*)realloc(p, 80);//当找不到足够的空间扩容失败返回空指针时,还会弄丢原空间
	int*ptr = (int*)realloc(p, 80);//80B
	if (ptr != NULL)
	{
		p = ptr;
	}
	//扩容成功了,开始使用

	//不再使用,就释放
	free(p);
	p = NULL;

	return 0;
}

❀常见的动态内存错误

对NULL指针的解引用操作 - err

开辟空间失败的时候,有可能出现对空指针解引用的操作。

int main()
{
	int* p = (int*)malloc(1000);
	
	//解决办法:对malloc函数的返回值进行判断,防止空指针
	int i = 0;
	if (p == NULL)
	{
		//....
		return 1;
	}
	//使用
	for (i = 0; i < 250; i++)
	{
		*(p + i) = i;
	}

	free(p);
	p = NULL;

	return 0;
}

对动态开辟空间的越界访问 - err

动态开辟空间的方式类似于数组,也是一片连续的内存空间。
解决:对内存边界要自行主动检查。

int main()
{
	int* p = (int*)malloc(100);//25个整型空间
	int i = 0;
	if (p == NULL)
	{
		//....
		return 1;
	}
	使用
	越界访问了
	//for (i = 0; i <= 25; i++)//访问到第26个整型空间
	for (i = 0; i < 25; i++)
	{
		*(p + i) = i;
	}

	return 0;
}

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

int main()
{
	int a = 10;

	int* p = &a;
	//.....

	free(p);//free不能释放不是动态开辟的空间,编译会报错
	p = NULL;
	return 0;
}

对于局部变量,出作用域时编译器会主动释放。而动态开辟的空间如果程序不结束,不会主动释放空间,需要free来帮助释放。

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

int main()
{
	int* p = (int*)malloc(100);
	if (p == null)
	{
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p = i;
		p++;
	}
	//释放空间
	free(p);//err - 此时p在++后指向的不是起始地址了
	p = null;

	return 0;
}

在这里插入图片描述

用free释放必须从起始位置开始释放。
必须提前记住起始地址。避免内存泄露。

对同一块动态内存多次释放 - err

int main()
{
	int* p = malloc(100);
	if (p == NULL)
		return 1;
	
	free(p);
	//....
	free(p);//err

	p = NULL;

	return 0;
}

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

1、

void test()
{
	int* p = malloc(100);
	使用
	
	忘记释放
	//free(p);
	//p = NULL;
}
int main()
{
	test();
	//.....
	while (1)
	{
		;
	}
	
    //已经无法释放
	return 0;
}

2、
即使成对使用 malloc 和 free ,仍可能内存泄漏。

void test()
{
	int* p = malloc(100);
	//使用
	if (1)
		return;

	free(p);
	p = NULL;
	//没机会执行free,仍然会内存泄漏
}

❀❀❀

1、

#include <string.h>

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

	return 0;
}

程序崩溃。
原因:(1)内存泄漏;(2)对NULL的解引用操作。
p是str的临时拷贝,函数调用结束后,形参变量p会被销毁,但p指向的动态空间并没有被释放,造成内存泄漏;而str仍指向空指针,strcpy造成对空指针的非法访问。

注意:

int main()
{
	char* p = "hehe\n";//p指向的是字符串的首地址 - h的地址
	printf("hehe\n");//传给printf函数的是字符串的首地址

	char arr[] = "hehe\n";
	printf(arr);

	return 0;
}

在这里插入图片描述

//√
#include <string.h>

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

2、

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

在这里插入图片描述
返回栈空间地址的问题。
局部变量数组p的内存空间在函数调用结束后会销毁,又返回了数组p的首地址,str将非法访问一块未知的空间,此时str就是野指针。

int* test()
{
	int a = 10;
	return &a;
}

int main()
{
	int* p = test();

	printf("%d\n", *p);
	printf("%d\n", *p);

	return 0;
}

在这里插入图片描述
如果空间释放后栈帧空间的数据没有被覆盖,则可能能够访问得到原数据,一旦被覆盖就找不到原数据了。

注意与以下代码作区分:以下是正确代码;

int test()
{
	int a = 10;
	return a;
}
int main()
{
	int n = test();

	return 0;
}

3、

#include <string.h>

void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
}
int main()
{
	Test();
	return 0;
}
//√
#include <string.h>

void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
	//判断是否申请成功
	//...
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
	//没有释放
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

4、

void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)//释放后使用造成str成野指针
	{
		strcpy(str, "world");
		printf(str);
	}
}
int main()
{
	Test();
	return 0;
}
//√
void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	str = NULL;
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}
int main()
{
	Test();
	return 0;
}

❀柔性数组

C99 中,结构中的最后一个元素允许是未知大小的数组,即 柔性数组成员。

柔性数组的特点

  • 结构中的柔性数组成员前面必须至少一个其他成员。
struct S1
{
	int num;
	double d;
	int arr[0];//柔性数组成员
};
struct S2
{
	int num;
	int arr[];//柔性数组成员
};
  • sizeof 返回的这种结构大小不包括柔性数组的内存。
struct S2
{
	int num;//4
	int arr[];//柔性数组成员
};


int main()
{
	printf("%d\n", sizeof(struct S2));//?
	
	return 0;
}

在这里插入图片描述

  • 包含柔性数组成员的结构要用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
#include <stdlib.h>

struct S2
{
	int num;//4
	int arr[];//柔性数组成员
};
int main()
{	
	//struct S2* ps = (struct S2*)malloc(sizeof(struct S2));//× - 只为num开辟了空间
	struct S2* ps = (struct S2*)malloc(sizeof(struct S2)+40);//根据情况为柔性数组分配预期空间,此时期望柔性数组内可以放10个整型
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	
	//访问num
	ps->num = 100;
	//访问柔性数组 - 同普通数组
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->arr[i]);
	}

	//释放
	free(ps);
	ps = NULL;

	return 0;
}

在这里插入图片描述

  • 方便对数组的扩容。
#include <stdlib.h>

struct S2
{
	int num;//4
	int arr[];//柔性数组成员
};
int main()
{
	struct S2* ps = (struct S2*)malloc(sizeof(struct S2)+40);//根据情况为柔性数组分配预期空间,此时期望柔性数组内可以放10个整型
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}

	//访问num
	ps->num = 100;
	//访问柔性数组 - 同普通数组
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	//for (i = 0; i < 10; i++)
	//{
	//	printf("%d ", ps->arr[i]);
	//}

	//扩容
	struct S2* ptr = (struct S2*)realloc(ps, sizeof(struct S2)+80);//为柔性数组扩容80B
	if (ptr == NULL)
	{
		perror("realloc\n");
		return 1;
	}
	else
	{
		ps = ptr;
	}

	//继续访问柔性数组
	for (i = 10; i < 20; i++)
	{
		ps->arr[i] = i;
	}
	for (i = 0; i < 20; i++)
	{
		printf("%d ", ps->arr[i]);
	}

	//释放
	free(ps);
	ps = NULL;

	return 0;
}

在这里插入图片描述

柔性数组的优势

struct S3
{
	int num;
	int* arr;
};

int main()
{
	struct S3* ps = (struct S3*)malloc(sizeof(struct S3));
	if (ps == NULL)
	{
		return 1;
	}
	ps->arr = (int*)malloc(40);
	if (ps->arr == NULL)
	{
		free(ps);
		ps = NULL;
		return 1;
	}

	//使用
	//...
	
	//释放 - 注意释放顺序
	free(ps->arr);
	ps->arr = NULL;

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

以上代码同样可以调节数组的大小,但在实现细节上会有所差异。

优势1: 方便内存释放。
柔性数组的空间是和其它变量一次申请的,是一片连续的内存空间,使用完通过一次就可以释放完。
而以上的方法需要多次申请,多次释放。

优势2: 有利于访问速度。
对在内存中连续存放的数据的访问速度比非连续存放的访问速度快。
连续的内存有益于提高访问速度,也有益于减少内存碎片的浪费。

高并发内存池!
在这里插入图片描述
其实实际上,两种动态开辟方式相差不大,都可以使用。

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

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

相关文章

Pycharm安装配置Pyside6

PySide6是在Python环境下的一套Qt6 API库。使用PySide6可以轻松创建基于Qt6的GUI程序&#xff1b;PySide6由Qt官方维护。 1. Pyside6的安装&#xff1a; 直接安装在原python上面&#xff0c;在cmd里运行&#xff1a;(网速慢使用阿里源源) pip3 install Pyside6 -i https://p…

网络安全——逻辑漏洞之越权漏洞

作者名&#xff1a;Demo不是emo 主页面链接&#xff1a;主页传送门创作初心&#xff1a;舞台再大&#xff0c;你不上台&#xff0c;永远是观众&#xff0c;没人会关心你努不努力&#xff0c;摔的痛不痛&#xff0c;他们只会看你最后站在什么位置&#xff0c;然后羡慕或鄙夷座右…

高通导航器软件开发包使用指南(8)

高通导航器软件开发包使用指南&#xff08;8&#xff09;7 电子速度控制器7.1 ESC固件更新7.1.1相关参数说明7.1.3在初始化期间启用更新7.1.4固件配置7.1.5固件从版本7.1.6更新程序7 电子速度控制器 7.1 ESC固件更新 高通公司Navigator支持ESC固件更新&#xff0c;无需连接或…

2022亚太C题详细思路

2022年亚太今日已经正式开赛&#xff0c;为了帮助大家更好的选题建模&#xff0c;这里首先对ABC三道题目进行浅要评析&#xff0c;以方便大家更好的择题。同时相关资料也会后续进行补充。预计明日公布各题统计选题人数以及较为完善的资料。今天作为第一天重要的是择好题&#x…

Tableau阈值设置及其使用

阈值又叫临界值&#xff0c;是指一个效应能够产生的最低值或最高值。 ——百度百科 文章目录前言一、案例中阈值的使用背景介绍二、设置阈值参数三、颜色区分四、可筛选设置总结前言 介绍Tableau阈值的设置&#xff0c;供各位小伙伴参考。本文案例来源于Tableau自带示例工作薄…

mysql 数据备份与恢复使用详解

一、前言 对一个运行中的线上系统来说&#xff0c;定期对数据库进行备份是非常重要的&#xff0c;备份不仅可以确保数据的局部完整性&#xff0c;一定程度上也为数据安全性提供了保障&#xff0c;设想如果某种极端的场景下&#xff0c;比如磁盘损坏导致某个时间段数据丢失&…

冒泡排序法

目录 一、问题 二、冒泡排序的思想 三、举例 四、算法分析 五、代码实现 一、问题 现有一个整型数组&#xff08;乱序&#xff09;&#xff0c;并且写一个函数&#xff08;Sort&#xff09;对数组进行排序&#xff0c;顺序要求升序。 二、冒泡排序的思想 两两相邻的元素…

【100个 Unity实用技能】 | Unity自定义脚本的初始模版

Unity 小科普 老规矩&#xff0c;先介绍一下 Unity 的科普小知识&#xff1a; Unity是 实时3D互动内容创作和运营平台 。包括游戏开发、美术、建筑、汽车设计、影视在内的所有创作者&#xff0c;借助 Unity 将创意变成现实。Unity 平台提供一整套完善的软件解决方案&#xff…

java每日一练(2)

java每日一练(2) 单选部分 1.A 派生出子类 B &#xff0c; B 派生出子类 C &#xff0c;并且在 java 源代码有如下声明&#xff1a; A a0new A();A a1new B();A a2new C(); 问以下哪个说法是正确的&#xff08;&#xff09; A 只有第一行能通过编译 B 第1、2行能通过编译&…

【Servlet】6:一篇文章搞懂Servlet对象的相互调用、数据共享

目录 | 请求对象和响应对象 生命周期 | Servlet之间的相互调用 Servlet调用 基本概述 重定向Servlet调用 请求转发Servlet调用 | Servlet之间的数据共享 Servlet数据共享 基本概述 ServletContext接口 数据共享 Cookie类 数据共享 HttpSession接口 数据共享 HttpServletRequest…

2022亚太A题赛题分享

序列图像特征提取及模具熔融结晶建模分析 连铸过程中的模具通量对钢半月板进行热绝缘&#xff0c;防止液态钢连铸过程中液态钢再氧化&#xff0c;控制传热&#xff0c;提供链润滑&#xff0c;吸收非金属夹杂物。模具通量的冶金功能主要由温度控制曲线下的熔化速率和结晶速率决定…

【论文简述及翻译】MVSNet:Depth Inference for Unstructured Multi-view Stereo(ECCV 2018)

一、论文简述 1. 第一作者&#xff1a;Yao Yao 2. 发表年份&#xff1a;2018 Oral 3. 发表期刊&#xff1a;ECCV 4. 关键词&#xff1a;MVS、端到端网络、代价体、深度图、可微分单应变换 5. 探索动机&#xff1a;传统方法存在一些常见的局限性&#xff0c;很难处理场景的…

1100亩烟台深耕水稻 国稻种芯·中国水稻节:山东盐碱地水稻

1100亩烟台深耕水稻 国稻种芯中国水稻节&#xff1a;山东盐碱地水稻 &#xff08;YMG全媒体记者 庞磊 通讯员 包刚先 李敏 摄影报道&#xff09;新闻中国采编网 中国新闻采编网 谋定研究中国智库网 中国农民丰收节国际贸易促进会 国稻种芯中国水稻节 中国三农智库网-功能性农业…

LinuxHadoop环境

Hadoop环境Hadoop集群拓扑1、集群拓扑2、角色分配一、虚拟机安装二、虚拟机克隆1、克隆类型&#xff08;1&#xff09;完整克隆&#xff08;2&#xff09;链接克隆2、克隆步骤&#xff08;1&#xff09;克隆出master虚拟机&#xff08;2&#xff09;克隆出slave1虚拟机&#xf…

线性回归实战---Abalone鲍鱼年龄预测

线性回归实现Abalone鲍鱼年龄预测 文章目录线性回归实现Abalone鲍鱼年龄预测一、环境准备数据集简介二、线性回归基础知识什么是线性回归?“最小二乘法” 求解线性回归问题三、Python代码四、结果分析前面我们使用手动编写,后面通过sklearn第三方库来与我们手写的模型进行对比…

Mysql——使用字符集以及校对

一、字符集 1、查看mysql支持的所有字符集 show character set; 2、查看指定数据库的字符集 show variables like ‘character%’; 这八种情况分别对应&#xff1a; 1&#xff09;设置客户端使用的字符集 2&#xff09;设置链接数据库时的字符集 3&#xff09;设置创建数据库…

2、Pinpoint-Server端安装

0、本章节简介 安装Pinpoint服务端 采用Docker安装所以需要提前安装 docker和 docker-compose 本文使用的版本是Pinpoint:2.1版本 ps 由于Pinpoint依赖了很多的基础镜像&#xff0c;所以推荐不要在已经部署了程序的机器上部署&#xff0c;以免造成端口号冲突&#xff0c;推荐使…

HTML5期末大作业——HTML+CSS+JavaScript平遥古城旅游景点介绍(6页)

&#x1f468;‍&#x1f393;学生HTML静态网页基础水平制作&#x1f469;‍&#x1f393;&#xff0c;页面排版干净简洁。使用HTMLCSS页面布局设计,web大学生网页设计作业源码&#xff0c;这是一个不错的旅游网页制作&#xff0c;画面精明&#xff0c;排版整洁&#xff0c;内容…

【笔试强训】Day2

&#x1f308;欢迎来到笔试强训专栏 (꒪ꇴ꒪(꒪ꇴ꒪ )&#x1f423;,我是Scort目前状态&#xff1a;大三非科班啃C中&#x1f30d;博客主页&#xff1a;张小姐的猫~江湖背景快上车&#x1f698;&#xff0c;握好方向盘跟我有一起打天下嘞&#xff01;送给自己的一句鸡汤&#x…