C++初阶 - 5.C/C++内存管理

news2024/12/24 22:02:09

目录

1.C/C++的内存分布

2.C语言中动态内存管理方式:malloc、calloc、realloc、free

3.C++内存管理方式

3.1 new/delete操作内置类型

3.2 new 和 delete操作自定义类型

4.operator new 与 operator delete 函数(重要点)

4.1 operator new 与 operator delete函数(重点)

4.2 重载 operator new 与operator delete(了解)

5.new 和 delete 的实现原理

5.1 内置类型

5.2 自定义类型

6. 定位 new 表达式(placement-new)(了解) 

7.常见面试题

7.1 malloc / free 和 new / delete 的区别

7.2 内存泄漏

7.2.1什么式内存泄漏,内存泄漏的危害

7.2.2 内存泄漏的危害

7.2.3 如何检测内存泄漏(了解)

7.2.4 如何避免内存泄漏


1.C/C++的内存分布

我们先来看一下下面的代码和相关问题

int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
    static int staticVar = 1;
    int localVar = 1;
    int num1[10] = { 1, 2, 3, 4 };
    char char2[] = "abcd";
    const char* pChar3 = "abcd";
    int* ptr1 = (int*)malloc(sizeof(int) * 4);
    int* ptr2 = (int*)calloc(4, sizeof(int));
    int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
    free(ptr1);
    free(ptr3);
}

1. 选择题:
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar在哪里?__C__   // 全局变量,全局变量和静态数据存放在 数据段(静态区) 
staticGlobalVar在哪里?__C__  // 静态数据,全局变量和静态数据段存放在 数据段(静态区) 
staticVar在哪里?__C__  // 静态数据,全局变量和静态数据段存放在 数据段(静态区)
localVar在哪里?__A__   //局部变量, 局部变量和函数存放在栈
num1 在哪里?__A__      //局部变量, 局部变量和函数存放在栈
char2在哪里?__A__      //局部变量, 局部变量和函数存放在栈
*char2在哪里?__A_      //局部变量, 局部变量和函数存放在栈
pChar3在哪里?__A__     //pChar3是一个指针变量,存储'abcd'的地址,属于局部变量, 局部变量和函数存放在栈
*pChar3在哪里?__D__    //pChar3是一个指针变量,存储'abcd'的地址,解引用后找到'abcd','abcd\0'存储在常量区。
ptr1在哪里?__A__       //ptr1是一个指针变量,存储malloc的地址,属于局部变量, 局部变量和函数存放在栈
*ptr1在哪里?__B__      //ptr1是一个指针变量,存储malloc的地址,解引用后拿到malloc的内容,该内容存放在堆区。

2. 填空题:
sizeof(num1) = __40__;   //sizeof(数组名),求该数组的大小,4 * 10 = 40
sizeof(char2) = __5__;   //sizeof(数组名),求该数组的大小,"abcd" = "abcd\0" 一共5个元素,1 * 5 = 5
strlen(char2) = __4__;   //strlen()是库函数,求字符串长度,"abcd"一共4个元素。
sizeof(pChar3) = _4/8___;//pChar3是一个指针变量,指针变量的大小受操作系统影响,4/8
strlen(pChar3) = __4__;  //strlen()是库函数,求字符串长度,"abcd"一共4个元素。
sizeof(ptr1) = __4/8__;    //ptr1是一个指针变量,指针变量的大小受操作系统影响,4/8

3. sizeof 和 strlen 区别?
sizeof()是操作符,求取变量的大小,
strlen()是函数,求的是字符串的长度,需要注意字符串一般末尾有'\0'作为结束标识符。

 【说明】

  1. 又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  2. 内存映射段--高效的IO映射方式,用于装载一个共享的动态内存库,用户可使用系统接口创建共享内存,做进程间通信,(Linux部分会详细讲解)
  3. --用于程序运行时动态内存分配,堆是向上增长。
  4. 数据段--存储全局变量和静态数据。
  5. 代码段--可执行的代码/只读常量。

2.C语言中动态内存管理方式:malloc、calloc、realloc、free

void Test ()
{
    int* p1 = (int*) malloc(sizeof(int));
    free(p1);
    // 1.malloc/calloc/realloc的区别是什么?
    int* p2 = (int*)calloc(4, sizeof (int));
    int* p3 = (int*)realloc(p2, sizeof(int)*10);
    // 这里需要free(p2)吗?
    /*
    不需要,realloc扩容方式分为两种:
    原地扩容,地址不改变。
    新位置扩容,copy原位置的内容,给到新位置,返回新位置的地址。
    */
    free(p3 );
}

面试题:

1.malloc/calloc/realloc的区别?

malloc申请的空间没有初始化,直接返回起始地址。

calloc申请的空间,会把空间初始化为0,然后返回起始地址。

realloc是对已经开辟好的空间进行扩容,扩容方式分为两种:
            原地扩容,地址不改变。
            新位置扩容,copy原位置的内容,给到新位置,返回新位置的地址。

2.malloc的实现原理?

【CTF】GLibc堆利用入门-机制介绍_哔哩哔哩_bilibili

3.C++内存管理方式

C语言内存管理方式在C++中可以继续使用,但是有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。

3.1 new/delete操作内置类型

#include<iostream>
using namespace std;
int main()
{
	//动态申请一个int类型的空间
	//C    
	int* p1 = (int*)malloc(sizeof(int));
	free(p1);
	//CPP
	int* p2 = new int;
	delete p2;

	//动态申请10个int类型的空间
	//C
	int* p3 = (int*)malloc(sizeof(int) * 10);
	free(p3);
	//CPP
	int* p4 = new int[10];
	delete[] p4;

	//动态申请一个int类型的空间并初始化为10
	//CPP
	int* p5 = new int(10);
	cout << "p5;" << *p5 << endl;
	delete p5;

	//动态申请一个int类型的数组空间并初始化为{1,2,3,0}
	//CPP
	int* p6 = new int[10] {1,2,3,0};
	for (int i = 0; i < 10; i++)
	{
		cout << "p6;" << *(p6+i) << endl;
	}
	delete[] p6;
	return 0;
}

注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[] 和 delete[]。注意:匹配起来使用。

3.2 new 和 delete操作自定义类型

class A
{
public:
    A(int a = 0)
    :_a(a)
    {
        cout << "A()" << this << endl;
    }
    ~A()
    {
        cout << "~A()" << this << endl;
    }
private:
    int _a;
};

int main()
{
    //new / delete 和 malloc / free最大的区别是 new /delete 对于【自定义类型还】除了开辟空间还会调用构造函数和析构函数
    A* p1 = (A*)malloc(sizeof(A));
    A* p2 = new A(1);
    free(p1);
    delete p2;

    //内置类型几乎是一样的
    int* p3 = (int*)malloc(sizeof(int));//C
    int* p4 = new int;
    free(p3);
    delete p4;

    A* p5 = (A*)malloc(sizeof(A)*10);
    A* p6 = new A[10];
    free(p5);
    delete[] p6;

    return 0;
}

注意在申请自定义类型的空间时,new会调用构造函数,delete 会调用析构函数,而malloc和free不会。

【面试题】

1.C语言malloc\free 与C++new、delete 的区别?

动态申请内置类型的数据:new / malloc 除了用法上面,其他没有什么区别。

动态申请自定义类型的数据:new / malloc 除了用法上面,new会调用构造函数初始化,delete会调用析构函数清理。

4.operator new 与 operator delete 函数(重要点)

4.1 operator new 与 operator delete函数(重点)

new 和 delete 是用户进行动态内存申请和释放的操作符operator new 和 operator delete 是系统提供的全局函数new在底层调用了operator new 全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。

/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间
失败,尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否
则抛异常。
*/
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
    // try to allocate size bytes
    void *p;
    while ((p = malloc(size)) == 0)
    if (_callnewh(size) == 0)
    {
        // report no memory
        // 如果申请内存失败了,这里会抛出bad_alloc 类型异常
        static const std::bad_alloc nomem;
        _RAISE(nomem);
    }
    return (p);
}

/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void *pUserData)
{
    _CrtMemBlockHeader * pHead;
    RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
    if (pUserData == NULL)
        return;
    _mlock(_HEAP_LOCK); /* block other threads */
    __TRY
    /* get a pointer to memory block header */
    pHead = pHdr(pUserData);
    /* verify block type */
    _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
    _free_dbg( pUserData, pHead->nBlockUse );//------------------------------
    __FINALLY
    _munlock(_HEAP_LOCK); /* release other threads */
    __END_TRY_FINALLY
    return;
}

/*
free的实现
*/
#define free(p)   _free_dbg(p, _NORMAL_BLOCK)//-----------------------------

通过上述两个全局变量的实现,operator new 实际也是通过malloc来申请空间的,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的

4.2 重载 operator new 与operator delete(了解)

注意:一般情况下不需要对operator new 和 operator delete进行重载,除非在申请和释放空间的时候有某些特殊的需求。比如:在使用new和delete申请空间和释放空间时,打印一些日志信息,可以帮助用户来检测时候存在内存泄漏。

// 重载operator delete,在申请空间时:打印在哪个文件、哪个函数、第多少行,申请了多少个
字节
void* operator new(size_t size, const char* fileName, const char* funcName,
size_t lineNo)
{
    void* p = ::operator new(size);
    cout << fileName << "-" << funcName << "-" << lineNo << "-" << p << "-"
    << size << endl;
    return p;
}
// 重载operator delete,在释放空间时:打印再那个文件、哪个函数、第多少行释放
void operator delete(void* p, const char* fileName, const char* funcName,
size_t lineNo)
{
    cout << fileName << "-" << funcName << "-" << lineNo << "-" << p <<
    endl;
    ::operator delete(p);
}
int main()
{
    // 对重载的operator new 和 operator delete进行调用
    int* p = new(__FILE__, __FUNCTION__, __LINE__) int;
    operator delete(p, __FILE__, __FUNCTION__, __LINE__);
    return 0;
}
// 上述调用显然太麻烦了,可以使用宏对调用进行简化
// 只有在Debug方式下,才调用用户重载的 operator new 和 operator delete
#ifdef _DEBUG
#define new new(__FILE__, __FUNCTION__, __LINE__)
#define delete(p) operator delete(p, __FILE__, __FUNCTION__, __LINE__)
#endif
int main()
{
    int* p = new int;
    delete(p);
    return 0;
}

5.new 和 delete 的实现原理

5.1 内置类型

如果申请的是内置类型的空间,new 和 malloc,delete 和free 基本相似,不同的地方是:new / delete 申请和释放的是首个元素的空间,new[] 和 delete[] 申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL;

5.2 自定义类型

  • new的原理
    • 调用operator new 函数申请空间
    • 在申请的空间上执行构造函数,完成对象的构造
  • delete 的原理
    • 在空间上执行析构函数,完成对象中资源的清理工作
    • 调用operator delete 函数释放对象的空间
  • new T[N]的原理
    • 调用operator new[] 函数,在operator new[] 中实际调用operator new 函数完成对N 个对象空间的申请
    • 在盛情的空间上执行N次构造函数
  • delete[] 的原理
    • 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
    • 调用operator delete[]  释放空间。实际在operator delete[] 中调用 operator delete 来释放空间。

6. 定位 new 表达式(placement-new)(了解) 

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。

使用格式:

new(place_address)type 或者 new(place_address)type(initializer-list)

place_address  必须是一个指针,initializer-list是类型的初始化列表

使用场景:

定位 new 表达式在实际中一般是配和内存池使用,因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调用构造函数进行初始化。

class A
{
public:
    A(int a = 0)
    : _a(a)
    {
        cout << "A():" << this << endl;
    }
~A()
    {
        cout << "~A():" << this << endl;
    }
private:
    int _a;
};
// 定位new/replacement new
int main()
{
    // p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
    A* p1 = (A*)malloc(sizeof(A));
    new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参
    p1->~A();
    free(p1);
    A* p2 = (A*)operator new(sizeof(A));
    new(p2)A(10);
    p2->~A();
    operator delete(p2);
    return 0;
}

7.常见面试题

7.1 malloc / free 和 new / delete 的区别

7.2 内存泄漏

7.2.1什么式内存泄漏,内存泄漏的危害

什么式内存泄偶?

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况,内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计失误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害?

长期运行的程序出现内存泄漏影响很大,如操作系统,后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。 

void MemoryLeaks()
{
    // 1.内存申请了忘记释放
    int* p1 = (int*)malloc(sizeof(int));
    int* p2 = new int;
    // 2.异常安全问题
    int* p3 = new int[10];
    Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
    delete[] p3;
}

7.2.2 内存泄漏的危害

C/C++程序中一般我们关心两种方面的内存泄漏:

堆内存泄漏:

堆内存指的是程序执行中依据要分配通过malloc / calloc / realloc / new 等从堆中分配的一块内存,用完后必须通过调用相应的 free 或者 delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法被使用,就会产生Heap Leak。

系统资源泄漏

指程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

7.2.3 如何检测内存泄漏(了解)

在vs下可以使用windows操作系统提供的 _CrtDumpMemoryLeaks() 函数进行简单检测,该函数只报出了大概泄漏了多少个字节,没有其他更准确的位置信息。

int main()
{
    int* p = new int[10];
    // 将该函数放在main函数之后,每次程序退出的时候就会检测是否存在内存泄漏
    _CrtDumpMemoryLeaks();
    return 0;
}

// 程序退出后,在输出窗口中可以检测到泄漏了多少字节,但是没有具体的位置
Detected memory leaks!
Dumping objects ->
{79} normal block at 0x00EC5FB8, 40 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.

因此写代码时一定要小心,尤其是动态内存操作时,一定要记着释放。但有些情况下总是防不胜防,简单的可以采用上述方式快速定位下。如果工程比较大,内存泄漏位置比较多,不太好查时一般都是借助第三方内存泄漏检测工具处理的。

  • 在linux下内存泄漏检测:Linux下几款C++程序中的内存泄露检查工具
  • 在windows下使用第三方工具:VS编程内存泄漏
  • 其他工具:https://www.cnblogs.com/liangxiaofeng/p/4318499.html

7.2.4 如何避免内存泄漏

  1.  工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记者匹配去释放。ps:
    这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智
    能指针来管理才有保证。
  2. 采用RAII思想或者智能指针来管理资源。
  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

总结一下:内存泄漏非常常见,解决方案分为两种:

1、事前预防型。如智能指针等。

2、事后查错型。如泄漏检测工具。

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

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

相关文章

Go语言开发者的Apache Arrow使用指南:扩展compute包

在本系列文章的第4篇《Go语言开发者的Apache Arrow使用指南&#xff1a;数据操作》[1]中我们遇到了大麻烦&#xff1a;Go的Arrow实现居然不支持像max、min、sum这样的简单聚合计算函数:(&#xff0c;分组聚合(grouped aggregation)就更是“遥不可期”。要想对从CSV读取的数据[2…

如何在代码中用宏定义确定VS编译器版本

我们经常需要将同一个代码文件在不同的编译器下编译&#xff0c;实际上对应着不同的代码&#xff0c;这个时候就可以使用宏定义来进行判断&#xff0c;比如如下代码&#xff1a; #if _MSC_VER >1900 // VS2015或者以上 #ifndef _CRT_SECURE_NO_WARNINGS #define _CRT_SECUR…

第十四届蓝桥杯大赛青少年省赛C++组试题真题 2023年5月

一、选择题 第 1 题 单选题 C中&#xff0c;bool类型的变量占用字节数为 ( )。 A. 1 B. 2 C. 3 D. 4 第 2 题 单选题 以下关于C结构体的说法&#xff0c;正确的是 ( )。 A. 结构体中只能包含成员变量&#xff0c;不能包含成员函数 B. 结构体不能从另一个结构体继承 …

发布npm包流程

发布npm包的步骤如下&#xff1a; 在终端中通过 npm init 命令创建一个新的npm包&#xff0c;按照提示填写包的信息&#xff0c;如包名称、版本、描述、作者、许可证等。 在包的根目录下创建一个 index.js 文件&#xff0c;编写你的代码。 确认你已经注册了npm账号&#xff0…

Vue二次封装axios为插件使用

基本的封装要求&#xff1a;统一 url 配置 统一 api 请求 request (请求)拦截器&#xff0c;例如&#xff1a;带上token等&#xff0c;设置请求头 response (响应)拦截器&#xff0c;例如&#xff1a;错误处理&#xff0c;页面重定向等 根据需要&#xff0c;结合 Vuex 做全局的…

【三维点云处理】顶点、面片、邻接矩阵、邻接距离矩阵以及稀疏存储概念

文章目录 vts和faces基础知识vertices-节点&#xff08;3是点的三维坐标&#xff09;faces-面片&#xff08;3是构成三角形面片的3个点&#xff09; 邻接矩阵邻接距离矩阵&#xff08;NN500&#xff09;稀疏矩阵 vts和faces基础知识 vertices-节点&#xff08;3是点的三维坐标…

git 实操

首先有安装好的git,安装好后,会在任一目录下右键出现git bash和git gui两个选项 打开git bash,设置好全局变量,用户名和邮箱,设置方法为: git config -- global user.name "xxx" git config --global user.email "xxxxxx.com" 1.创建版本库 git init 命…

Photoshop2023beta常见问题|ps 2023测试版智能AI功能不能用如何解决?

PS beta ai创成式填充用不了怎么办 生成图像出错解决方法&#xff1f;PS 2023最新版本更新了超强大的AI功能&#xff0c;可以一键生成或删除用户选中的内容&#xff0c;这可大大提高了生成图片的效率。生成出来的图片也被公认为质量超高&#xff0c;虽然偶尔可能有点小瑕疵&…

ajax/axios访问后端测试方法

文章目录 1、浏览器执行javascript方法GET请求POST请求 2、Postman测试工具GET请求POST请求 3、idea IDE提供的httpclient4、Apache JMeter 1、浏览器执行javascript方法 GET请求 http://localhost:6060/admin/get/123 POST请求 技巧&#xff1a;打开谷歌浏览器&#xff0c…

混战源起 ChatGPT,中国的 AI 产业怎么样了?

文章目录 前言一、回顾上半年的“百模大战”1.1 什么是“百模大战”1.2 AI大模型对我们的影响 二、中国AI产业现状三、我国在AI产业的前景与挑战结语 前言 北京时间7月13日凌晨&#xff0c;马斯克在Twitter上宣布&#xff1a;“xAI正式成立&#xff0c;去了解现实。”马斯克表示…

【Linux Shell】基础知识

Linux Shell基础知识 一、Linux Shell基础概念1.1 Shell定义1.2 命令行提示符 二、初识Shell2.1 Shell定义2.2 登录Shell相关文件2.3 Shell中的变量变量类型变量的引用单引号\ 与双引号\" \"变量的删除与检查 2.4 Shell中的扩展大括号扩展{ }其他扩展 一、Linux Shel…

STM32MX配置EEPROM(AT24C02)------保姆级教程

———————————————————————————————————— ⏩ 大家好哇&#xff01;我是小光&#xff0c;嵌入式爱好者&#xff0c;一个想要成为系统架构师的大三学生。 ⏩最近在开发一个STM32H723ZGT6的板子&#xff0c;使用STM32CUBEMX做了很多驱动&#x…

临床检验系统LIS

临床检验系统(Laboratory Information System)是一个能实现检验信息电子化、检验信息管理自动化的网络系统&#xff0c;其主要功能是将检验的实验仪器传出的检验数据经分析后&#xff0c;生成检验报告&#xff0c;通过网络存储在数据库中&#xff0c;使医生能够方便、及时的看到…

助力中小企业数字化改造 象过河把中小企业老板当成“自己人”

燥热的夏日午后&#xff0c;在郑州市国家大学科技园附近的一家咖啡店里约见了象过河软件研发的总负责人象海先生。 温文儒雅、润物无声的象海先生自信豁达、成熟冷静。软件开发技术出身的他&#xff0c;善于通过软件开发定制来帮助中小企业解决各种错综复杂的业务、仓储以及财…

Could not resolve placeholder

本质原因&#xff1a;项目启动未扫描到该配置&#xff0c;一般来说是配置不对 检查方向 1、检查编译后的target包里是否有该配置所在的文件 如果不在就clear&#xff0c;重新编译启动再去检查 2、检查启动的环境是否匹配 编译后的target包下的配置文件名称是否跟启动类的环境…

app流量变现流量分发策略——waterfall技术

waterfall&#xff0c;中文翻译为“瀑布流”&#xff0c;字面意思理解就是“从上往下流”&#xff0c;但“从上到下”这四个字该如何理解&#xff1f; 在广告行业中&#xff0c;waterfall指的是“在无法实时评估每次流量的价值时&#xff0c;基于历史eCPM数据&#xff0c;从上…

Docker OOM处理方式

优质博文&#xff1a;IT-BLOG-CN cicode xxx k8s docker events oom occured告警原因 特别需要注意的是&#xff1a;docker events oom的处理方法不同于java.lang.OutOfMemoryError。 当我们在PAAS/Captain上申请容器实例时&#xff0c;会设置内存Limit。比如容器Flavor为2C4…

【Nodejs】npm的使用

1.包和npm 1.1 什么是包 由于 Node 是一套轻内核的平台&#xff0c;虽然提供了一系列的内置模块&#xff0c;但是不足以满足开发者的需求&#xff0c;于是乎出现了包&#xff08;package&#xff09;的概念&#xff1a; 与核心模块类似&#xff0c;就是将一些预先设计好的功能…

20.0 HTTP通信

1. web开发 1.1 web开发介绍 Web指的是World Wide Web(万维网), 是一种基于互联网的信息系统. 万维网由一系列通过超文本链接相互连接的页面组成, 这些页面中包含了文本, 图像, 音频, 视频等多媒体内容. 用户可以通过浏览器访问万维网上的网页, 并通过超链接在不同页面之间导…

网页布局元素填充思路 ———— 先布局,再局部

【引言】 今天来分享一下在JS练习项目中学习到的网页布局思路&#xff0c;适合刚接触JS的小白在做练习项目&#xff0c;不知所措的时候看喔~ 简单来说就是 ———— 先布局&#xff0c;再局部。 在初次做练习项目的时候&#xff0c;大多都会先仿照一个网站先进行一个试练&…