C++初阶(八)--内存管理

目录

引入:

一、C++中的内存布局

1.内存区域

2.示例变量存储位置说明

二、C语言中动态内存管理

三、C++内存管理方式 

1.new/delete操作内置类型

2.new和delete操作自定义类型

四、operator new与operator delete函数(重要点进行讲解)

五、new和delete的实现原理

六、定位 new 和定位 delete

七、面试题

1.malloc/free和new/delete的区别?

 2.内存泄漏


引入:

以下代码分别存储在哪个区域

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";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);
}

 图解:

一、C++中的内存布局

在了解具体的内存管理操作之前,先得清楚 C++ 程序的内存布局。一般来说,一个 C++ 程序的内存可以大致分为以下几个区域:

1.内存区域

  1. 内核空间

    • 用户代码不能读写。
    • 操作系统内核运行在此空间,负责管理系统资源和执行关键任务。
  2. 代码区(Text Segment)

    • 存放程序的可执行代码,即机器指令。
    • 通常是只读的,防止程序在运行过程中意外修改自身的代码。
  3. 全局 / 静态存储区(Data Segment)

    • 存储全局变量和静态变量。
    • 全局变量在整个程序的生命周期内都存在,而静态变量(包括局部静态变量)根据其定义的作用域有不同的可见性,但它们的生命周期都是从程序开始到结束。
  4. 栈区(Stack)

    • 用于存储函数调用时的局部变量、函数参数以及返回地址等信息。
    • 遵循后进先出(LIFO)的原则,每当一个函数被调用时,相关的信息就会被压入栈中,函数执行完毕后,这些信息又会被弹出栈。
    • 栈的大小通常是在程序启动时就确定好的,相对有限,如果在栈上分配过多的内存,可能会导致栈溢出的错误。
  5. 堆区(Heap)

    • 是一块相对较大且比较灵活的内存区域,用于动态分配内存。
    • 程序员可以在程序运行期间根据需要随时在堆上申请和释放内存。
    • 与栈不同,堆上的内存分配和释放需要程序员手动进行管理,这也正是内存管理容易出现问题的地方之一。如果忘记释放堆内存,会导致内存泄漏。
  6. 常量存储区

    • 存放常量数据,如字符串常量等。
    • 这些数据在程序运行期间不能被修改。
  7. 内存映射段

    • 可以用于文件映射、动态库加载、匿名映射等。
    • 提供了一种将文件或其他资源映射到进程内存空间的方式,以便更高效地访问这些资源。

2.示例变量存储位置说明

1、globalVar在哪里?

根据上面的代码可知,glovalVar是在main函数外创建的变量,即在全局创建的变量,全局变量存放在数据段(静态区)中。

2、staticGlobalVar在哪里?

staticGlobalVar是在main函数外创建的静态变量,即在全局创建的静态变量,全局静态变量存放在数据段(静态区)中。
3、staticVar在哪里?

staticVar是在main函数内部创建的静态变量,即在局部创建的静态变量,局部静态变量存放在数据段(静态区)中。

4、localVar在哪里?

localVar是在main函数内部创建的变量,即在局部创建的普通变量,局部创建的普通变量存放在栈区。
5、char2在哪里?

char2是在main函数内部创建的数组的数组名,即在局部创建的多个普通变量,局部创建的普通变量存放在栈区。

6、* char2在哪里?

*char2是对数组的的首元素进行解引用,解引用的值存放在哪个区域,*char2的则存放在哪个区域,*char2是数组的第一个字符,即字符变量中的第一个元素,字符变量存放在栈区,因此*char2存放在栈区。
7、pChar3在哪里?

pChar3是在main函数内部创建的const修饰的常指针变量,实质还是一个局部创建的变量,只是该变量的值不能修改,因此pChar3存放在栈区。

8、* pChar3在哪里?

*pChar3是对数组的的首元素进行解引用,解引用的值存放在哪个区域,*pChar3的则存放在哪个区域,*pChar3是常量字符串的第一个字符,字符常量存放在代码段(常量区),因此*pChar3存放在代码段(常量区)。
9、ptr1在哪里?

ptr1是在main函数内部创建的指针变量,实质还是一个局部创建的变量,因此pChar3存放在栈区。(ptr2、ptr3同理)

10、* ptr1在哪里?

*ptr1是对数组的的首元素进行解引用,解引用的值存放在哪个区域,*ptr1的则存放在哪个区域,*ptr1是通过动态开辟的空间,动态开辟的空间存放在堆区,因此*ptr1存放在堆区。(ptr2、ptr3同理)

顺便提一下:为什么说栈是向下增长的,而堆是向上增长的? 

简单来说,在一般情况下,在栈区开辟空间,先开辟的空间地址较高,而在堆区开辟空间,先开辟的空间地址较低。

例如,下面代码中,变量a和变量b存储在栈区,指针c和指针d指向堆区的内存空间:

#include <iostream>
using namespace std;
int main()
{//栈区开辟空间,先开辟的空间地址高int a = 10;int b = 20;cout << &a << endl;cout << &b << endl;//堆区开辟空间,先开辟的空间地址低int* c = (int*)malloc(sizeof(int)* 10);int* d = (int*)malloc(sizeof(int)* 10);cout << c << endl;cout << d << endl;return 0;
}

 

因为在栈区开辟空间,先开辟的空间地址较高,所以打印出来a的地址大于b的地址;在堆区开辟空间,先开辟的空间地址较低,所以c指向的空间地址小于d指向的空间地址。

注意:在堆区开辟空间,后开辟的空间地址不一定比先开辟的空间地址高。因为在堆区,后开辟的空间也有可能位于前面某一被释放的空间位置。

二、C语言中动态内存管理

1.malloc:

malloc函数的功能是开辟指定字节大小的内存空间,如果开辟成功就返回该空间的首地址,如果开辟失败就返回一个NULL。传参时只需传入需要开辟的字节个数。

2.calloc

calloc函数的功能也是开辟指定大小的内存空间,如果开辟成功就返回该空间的首地址,如果开辟失败就返回一个NULL。calloc函数传参时需要传入开辟的内存用于存放的元素个数和每个元素的大小。calloc函数开辟好内存后会将空间内容中的每一个字节都初始化为0。

3.realloc 

realloc函数可以调整已经开辟好的动态内存的大小,第一个参数是需要调整大小的动态内存的首地址,第二个参数是动态内存调整后的新大小。realloc函数与上面两个函数一样,如果开辟成功便返回开辟好的内存的首地址,开辟失败则返回NULL。

 4.free

free函数的作用就是将malloc、calloc以及realloc函数申请的动态内存空间释放,其释放空间的大小取决于之前申请的内存空间的大小。

这里只做简单的概述,若还想进一步了解malloc、calloc、realloc和free,请阅读动态内存管理。

三、C++内存管理方式 

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

1.new/delete操作内置类型

// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的空间
int* ptr6 = new int[3];
//动态申请10个int类型的空间并初始化为0到9
int* p7 = new int[10] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; //申请 + 赋值delete ptr4;//销毁
delete ptr5;
delete[] ptr6;
delete[] p7;

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

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()
{// 动态申请单个类的空间A* ptr4 = new A;// 动态申请一个A类的空间并初始化为10A* ptr5 = new A(10);// 动态申请10个A类的空间,创建 10 个对象A* ptr6 = new A[10];//动态申请10个A类的空间并初始化0到9A* ptr7 = new A[10]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; //申请 + 赋值delete ptr4;//销毁delete ptr5;delete[] ptr6;delete[] ptr7;}

 给一个malloc版本

int main()
{// 动态申请单个类的空间A* ptr4 = (A*)malloc(sizeof(A));// 动态申请一个 A 类的空间并初始化为 10A* ptr5 = (A*)malloc(sizeof(A));// 动态申请 10 个 A 类的空间A* ptr6 = (A*)malloc(sizeof(A) * 10);// 动态申请 10 个 A 类的空间并初始化 0 到 9A* ptr7 = (A*)malloc(sizeof(A) * 10);free(ptr4);free(ptr5);free(ptr6);free(ptr7);return 0;
}

 可以自己进行调试一下,会发现malloc,free和new,delete的区别。

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

总结一下:
 1、C++中如果是申请内置类型的对象或是数组,用new/delete和malloc/free没有什么区别。
 2、如果是自定义类型,区别很大,new和delete分别是开空间+构造函数、析构函数+释放空间,而malloc和free仅仅是开空间和释放空间。
 3、建议在C++中无论是内置类型还是自定义类型的申请和释放,尽量都使用new和delete。

 四、operator new与operator delete函数(重要点进行讲解)

new和delete在底层上就是调用operator newoperator delete的。

operator newoperator delete是 C++ 中用于动态内存分配和释放的操作符函数。它们可以被重载以实现自定义的内存分配策略。默认情况下,operator new会调用底层的操作系统函数来分配内存,而operator delete会释放由operator new分配的内存。

operator new和operator delete的用法和malloc和free的用法完全一样,其功能都是在堆上申请和释放空间。

	int* p1 = (int*)operator new(sizeof(int)* 10); //申请operator delete(p1); //销毁//-------------等价-----------------//int* p2 = (int*)malloc(sizeof(int)* 10); //申请free(p2); //销毁

默认行为:在 C++ 中,当我们使用new关键字来分配内存时,实际上会调用operator new函数。这个函数会尝试从堆中分配足够的内存来满足请求。如果分配成功,它会返回一个指向分配的内存的指针;如果分配失败,它会抛出一个std::bad_alloc异常。

实际上,operator new的底层是通过调用malloc函数来申请空间的,当malloc申请空间成功时直接返回;若申请空间失败,则尝试执行空间不足的应对措施,如果该应对措施用户设置了,则继续申请,否则抛异常。而operator delete的底层是通过调用free函数来释放空间的。

注意:虽然说operator new和operator delete是系统提供的全局函数,但是我们也可以针对某个类,重载其专属的operator new和operator delete函数,进而提高效率。 

例如,我们可以实现一个简单的内存池来提高内存分配的效率。

以下是一个简单的示例:

class MyClass
{
public:static void* operator new(size_t size){cout << "Custom operator new called." << endl;return malloc(size);}static void operator delete(void* ptr){cout << "Custom operator delete called." << endl;free(ptr);}
};int main()
{MyClass* obj = new MyClass();delete obj;return 0;
}

在这里,我们重载了MyClass类的operator newoperator delete函数。当我们创建一个MyClass对象时,会调用自定义的operator new函数,该函数使用malloc来分配内存。当我们释放一个MyClass对象时,会调用自定义的operator delete函数,该函数使用free来释放内存。

五、new和delete的实现原理


内置类型
 如果申请的是内置类型的空间,new/delete和malloc/free基本类似,不同的是,new/delete申请释放的是单个元素的空间,new[ ]/delete [ ]申请释放的是连续的空间,此外,malloc申请失败会返回NULL,而new申请失败会抛异常。

自定义类型
new的原理
 1、调用operator new函数申请空间。
 2、在申请的空间上执行构造函数,完成对象的构造。

delete的原理
 1、在空间上执行析构函数,完成对象中资源的清理工作。
 2、调用operator delete函数释放对象的空间。

new T[N]的原理
 1、调用operator new[ ]函数,在operator new[ ]函数中实际调用operator new函数完成N个对象空间的申请。
 2、在申请的空间上执行N次构造函数。

delete[ ] 的原理
 1、在空间上执行N次析构函数,完成N个对象中资源的清理。
 2、调用operator delete[ ]函数,在operator delete[ ]函数中实际调用operator delete函数完成N个对象空间的释放。

六、定位 new 和定位 delete

除了普通的operator newoperator delete,C++ 还提供了定位new和定位delete。定位new允许我们在已经分配的内存上构造对象,而定位delete允许我们在已经构造的对象上显式地调用析构函数并释放内存。

定位new(placement new)

定位new允许在已经分配好的内存地址上构造对象,而不是像普通的new操作符那样从堆上动态分配新的内存。它的语法形式是new (place_address) type或者new(place_address)type(initializer-list),其中place_address是一个已经分配好的内存地址,type是要构造的对象类型,initializer-list是类型的初始化列表

定位delete(placement delete)

定位delete通常与定位new配合使用,它允许在已经构造的对象上显式地调用析构函数并释放内存。一般情况下,不需要显式调用定位delete,只有在特定的情况下(比如异常处理中需要确保正确的析构函数调用)才可能会用到。

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

#include <iostream>
#include <new>class MyClass
{
public:MyClass(){std::cout << "MyClass constructor called." << std::endl;}~MyClass(){std::cout << "MyClass destructor called." << std::endl;}
};int main()
{//此时只是开辟空间,没有创建对象,因为构造函数没有调用char* buffer = new char[sizeof(MyClass)];MyClass* obj = new (buffer) MyClass();   //new(place_address)type 形式obj->~MyClass();delete[] buffer;return 0;
}

代码讲解:

在这个例子中,首先分配了一块足够大的内存空间(通过char* buffer = new char[sizeof(MyClass)];),然后使用定位new在这个已经分配好的内存地址buffer上构造了一个MyClass对象。这里先显式调用对象的析构函数(obj->~MyClass();),然后释放分配的内存块(delete[] buffer)。注意,这里并没有直接使用定位delete,而是通过先调用析构函数再释放内存块的方式来模拟定位delete的行为。

总的来说,定位new和定位delete提供了一种在特定内存位置上构造和销毁对象的机制,在一些特定的场景下可以提供更灵活的内存管理方式。但使用时需要非常小心,确保内存的正确分配和释放,以避免出现内存泄漏和未定义行为等问题。

七、面试题

1.malloc/free和new/delete的区别?

共同点:
 都是从堆上申请空间,并且需要用户手动释放。
不同点:

1、malloc和free是函数,new和delete是操作符。
2、malloc申请的空间不会初始化,new申请的空间会初始化。
3、malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即          可。
4、malloc的返回值是void*,在使用时必须强转,new不需要,因为new后跟的是空间的类         型。
5、malloc申请失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要         捕获异常。
6、申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数和析构函数,而          new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函        数完成空间中资源的清理。

 2.内存泄漏

什么是内存泄漏,内存泄漏的危害?

内存泄漏:

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

内存泄漏的危害:

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

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;
}

 内存泄漏分类

在C/C++中我们一般关心两种方面的内存泄漏:
1、堆内存泄漏(Heap Leak)

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

 2、系统资源泄漏

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

 如何避免内存泄漏?

   1、工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记住匹配的去释放。
 2、采用RAII思想或者智能指针来管理资源。
 3、有些公司内部规范使用内部实现的私有内存管理库,该库自带内存泄漏检测的功能选项。
 4、出问题了使用内存泄漏工具检测。

内存泄漏非常常见,解决方案分为两种:
 1、事前预防型。如智能指针等。
 2、事后查错型。如泄漏检测工具。

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

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

相关文章

基于vue框架的的驾校预约车辆管理系统设计与实现jwoqj(程序+源码+数据库+调试部署+开发环境)系统界面在最后面

系统程序文件列表 项目功能&#xff1a;学员,教练员,驾校车辆,车辆信息,车辆类型,预约信息,时间段,教学课程,上报维修,维修内容,练车记录,取消信息 开题报告内容 基于Vue框架的驾校预约车辆管理系统设计与实现开题报告 一、研究背景与意义 随着驾驶培训行业的快速发展&…

JVM结构图

JVM&#xff08;Java虚拟机&#xff09;是Java编程语言的核心组件之一&#xff0c;负责将Java字节码翻译成机器码并执行。JVM由多个子系统组成&#xff0c;包括类加载子系统、运行时数据区、执行引擎、Java本地接口和本地方法库。 类加载子系统&#xff08;Class Loading Subsy…

IDEA 打包首个java项目为jar包

新建java项目 创建一个java项目&#xff0c;使用Maven进行项目构建&#xff0c;高级配置方面主要设置了项目包版本等信息。 依照步骤生成相关的项目。 设置maven环境 从项目设置中查找maven相关配置 设置&#xff08;settings&#xff09;-》构建、执行、部署&#xff08;B…

【ARCGIS实验】地形特征线的提取

目录 一、提取不同位置的地形剖面线 二、将DEM转化为TIN 三、进行可视分析 四、进行山脊、山谷等特征线的提取 1、正负地形提取&#xff08;用于校正&#xff09; 2、山脊线提取 3、山谷线的提取 4、河网的提取 5、流域的分割 五、鞍部点的提取 1、背景 2、目的 3…

达梦数据库在终端/控制台交互查询SQL语句,查询结果导出excel

达梦数据库在终端/控制台交互查询SQL语句&#xff0c;查询结果导出excel 依赖 安装JDK&#xff0c;maven引入达梦包&#xff0c;maven打包主类改成查询工具类&#xff0c;即可放到linux平台运行 <dependency><groupId>com.dameng</groupId><artifactId…

【Linux】设备树

设备树简介 我们前面介绍过平台设备驱动&#xff0c;知道硬件资源信息可以放在设备中&#xff0c;然后在驱动的probe函数中从设备中获取资源信息。但是&#xff0c;Linux3.x以后的版本引入了设备树&#xff0c;设备树用于描述一个硬件平台的硬件资源&#xff0c;一般描述那些不…

node和npm版本冲突

问题描述&#xff1a; 解决办法&#xff1a; 一、 查看自己当前的node和npm版本 node -v npm -v 二、 登录node官网地址 node官网地址 https://nodejs.org/zh-cn/about/previous-releases 查看与自己node版本兼容的是哪一版本的npm,相对应进行更新即可。 三 升级node 下载最…

笑死人不偿命的联想:大象是什么?

element&#xff08;元素&#xff09;一词&#xff0c;起源不明。但是它长得很像elephant&#xff08;大象&#xff09;一词&#xff0c;其同通部分为ele-这一结构&#xff0c;因此我们很容易将两个单词进行拆分出来&#xff1a; element n.元素 // ele ment名缀elephant n.大…

书生-第四期闯关:完成SSH连接与端口映射并运行hello_world.py

端口映射完成后&#xff0c;访问127.0.0.1&#xff1a;7860成功展示如下界面&#xff1a; 书生浦语大模型实战营 项目地址&#xff1a;https://github.com/InternLM/Tutorial/

DBT踩坑第三弹

1. dbt在获取元数据信息的时候&#xff0c;底层使用pyHive的时候database信息没有传进去&#xff0c;pyHive默认又是会设置databasedefault&#xff0c;如果没有default库权限的&#xff0c;这个时候就会抛出Access异常。所以此时最好修改下 dbt-spark 的源码&#xff0c;把dat…

Codeforces Round 966 (Div. 3)

D. Right Left Wrong 题意 思路 我们可以先预处理前缀和&#xff0c;然后贪心每次找最左边的L和最右边的R&#xff0c;计算区间和&#xff0c;然后缩小区间重复操作即可 时间复杂度 O(N) void solve() {int n;cin >> n;vector<int> arr(n 1);vector<int>…

Qt 实战(10)模型视图 | 10.5、代理

文章目录 一、代理1、简介2、自定义代理 前言&#xff1a; 在Qt的模型/视图&#xff08;Model/View&#xff09;框架中&#xff0c;代理&#xff08;Delegate&#xff09;是一个非常重要的概念。它充当了模型和视图之间的桥梁&#xff0c;负责数据的显示和编辑。代理可以自定义…

NSSCTF-WEB-nizhuansiwei

前言 就直接上题目吧 这题有些意思 正文 <?php $text $_GET["text"]; $file $_GET["file"]; $password $_GET["password"];//定义三个变量 if(isset($text)&&(file_get_contents($text,r)"welcome to the zjctf"))…

无迹卡尔曼滤波器(UKF)

正如我们在前一章中所看到的&#xff0c;当状态转移模型f (x)和观测模型h (x)接近于线性时&#xff0c;EKF的性能是令人满意的。然而&#xff0c;当f (x)或h (x)模型是高度非线性的时&#xff0c;线性化误差会导致与状态的真实值显著不同的估计&#xff0c;以及不能捕获状态中的…

金蝶云星空与管易云的数据集成实战案例

金蝶云星空与管易云的数据集成案例分享 在企业信息化系统中&#xff0c;实现不同平台之间的数据无缝对接是提升业务效率的关键。本文将聚焦于一个具体的系统对接集成案例&#xff1a;如何将金蝶云星空中的调拨申请单数据集成到管易云的采购订单新增模块&#xff0c;特别是针对…

成本累计曲线:项目预算的秘密武器

在项目管理的过程中&#xff0c;成本控制是影响项目成败的关键因素之一&#xff0c;而其中“成本累计曲线”就像是一位财务导航员&#xff0c;为项目的成本控制和进度监控提供了极大的帮助。那么&#xff0c;什么是成本累计曲线&#xff1f;它包含哪些步骤&#xff1f;如何应用…

idea连接数据库出现错误的解决方式

在使用idea连接数据库时&#xff0c;出现错误&#xff1a; The server has terminated the handshake. The protocol list option (enabledTLSProtocols) is set, this option might cause connection issues with some versions of MySQL. Consider removing the protocol li…

C++朝花夕拾

目录 目录 函数分文件编写 野指针 const与指针 const修饰指针——常量指针 const修饰常量——指针常量 const既修饰指针,又修饰常量 const阻止函数修改 delete和delete[]的区别 内存四区&#xff08;面试会问&#xff1f;&#xff09; 程序运行前 代码区 全局区 程…

WPF中如何解决DataGrid的Header没有多余的一行

将最后一行设置DataGridTemplateColumn Width"*" 使其自适应

网站制作公司哪家比较靠谱?分享5家2024年口碑好的网站制作公司

想要分辨一家网站制作公司靠不靠谱并不简单&#xff0c;可能它流程透明&#xff0c;设计优秀。但这就一定是适合自己的吗&#xff1f;所以口碑这东西很重要。适合自己也很重要&#xff0c;要多方面去了解。 以下是五家在2024年口碑不错的网站制作公司&#xff0c;分享一下设计…