欢迎来到ZyyOvO的博客✨,一个关于探索技术的角落,记录学习的点滴📖,分享实用的技巧🛠️,偶尔还有一些奇思妙想💡
本文由ZyyOvO原创✍️,感谢支持❤️!请尊重原创📩!欢迎评论区留言交流🌟
个人主页 👉 ZyyOvO
本文专栏➡️C++ 进阶之路
各位于晏,亦菲们请看
- 引言
- 函数模板的概念
- 函数模板的匹配原则
- 函数模板的底层原理
- 模板的编译阶段
- 模板实例化
- 编译器与链接器的协作
- 编译器的工作流程
- 前端编译阶段
- 模板实例化阶段
- 后端编译阶段
- 函数模板总结
- 写在最后
引言
点击快速复习 👉:【C++ 函数重载】—— 现代编译技术下的多态表达与性能优化
上篇文章我们讲到C++的函数重载,包括函数重载的条件,原理以及一些易错事项,那么本文我们为大家介绍C++中泛型编程的主要方式——模板。
在 C++ 中,模板(Template
)是一种强大的编程特性,它允许程序员编写与类型无关的代码,实现代码的复用和泛型编程。
如同模具一样,C++中的模板也是同样的道理!
函数模板的概念
模板是 C++泛型编程的基础,它提供了一种将类型参数化的机制。模板分为类模板和函数模板,通过模板,我们可以定义通用的类或函数,这些类或函数可以处理多种不同的数据类型,而不需要为每种数据类型都编写一套单独的代码。这样可以提高代码的复用性和可维护性。
函数模板定义了一系列具有相似功能但可以处理不同数据类型的函数。通过使用模板,你无需为每种数据类型都编写一个单独的函数,而是可以定义一个通用的函数,让编译器根据实际使用的参数类型自动生成相应的具体函数。
定义
- 函数模板的定义通常包含
模板声明
和函数定义
两部分。模板声明使用template
关键字,后跟一个或多个模板参数列表,函数定义则使用这些模板参数来实现通用的逻辑。
语法:
template <typename T,typename T2,...typename Tn>
返回类型 函数名(参数列表){// 函数体
}
template
:这是定义模板的关键字,表明接下来要定义一个模板。typename
(也可以用class
):用于声明一个类型参数,它告诉编译器 T 是一个代表任意类型的占位符。T
:类型参数的名称,你可以根据需要自定义,但通常使用单个大写字母,如 T、U 等。- 返回类型:函数的返回类型,可以是模板参数 T 或其他类型。
- 参数列表:函数的参数列表,可以包含模板参数 T。
示例:
template<typename T>
void Swap( T& left, T& right)
{T temp = left;left = right;right = temp;
}
使用
隐式实例化
- 让编译器根据实参自动推演模板参数的实际类型
使用函数模板时,你可以像调用普通函数一样调用它,编译器会根据传递的实参类型自动推导模板参数的类型。
声明一个模板:
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
使用模板:
int main()
{int a1 = 10, a2 = 20;double d1 = 10.0, d2 = 20.0;int intResult = Add(a1, a2);double doubleResult = Add(d1, d2);return 0;
}
Add(a1, a2);
:调用 Add 函数模板,编译器会根据传入的参数 a1 和 a2 的类型(int)自动推导模板参数 T 为 int,然后实例化出一个处理 int 类型的 Add 函数。Add(d1, d2);
:同理,调用 Add 函数模板时,编译器根据 d1 和 d2 的类型(double)推导模板参数 T 为 double,并实例化出一个处理 double 类型的 Add 函数。
下面这条语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型通过实参a1将
T
推演为int
,通过实参d1将T推演为double
类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T
确定为int
或者double
类型而报错.
Add(a1, d1);
注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅,对于语句Add(a1, d1);
此时有两种处理方式:
- 用户自己来强制转化
Add(a, (int)d);
- 使用显式实例化
显示实例化
模板的显示实例化(Explicit Instantiation
)是一种手动告诉编译器生成特定模板实例代码的机制.当你有函数模板时,编译器通常会在代码中第一次使用到特定模板实例时才生成对应的代码,但有时候你可能希望提前显式地让编译器生成特定类型的模板实例,这就需要用到显式实例化。
还是之前那个例子:
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
使用显示实例化
int main(void)
{int a = 10;double b = 20.0;// 显式实例化Add<int>(a, b);return 0;
}
- 在这个例子中,
Add<int>(a, b);
这行代码显式地告诉编译器生成 Add 函数模板针对 int 类型的实例代码。 - 如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
- 此时编译器会将
double
类型的b转换为int
类型来完成函数调用!
函数模板的匹配原则
函数模板的匹配原则是 C++ 中重载决议的核心规则之一,决定了编译器在多个候选函数(包括模板和非模板函数)中选择最合适版本的优先级顺序。
非模板函数优先
- 如果存在非模板函数与调用参数完全匹配(无需隐式转换),则优先选择非模板函数,而非实例化模板。
#include <iostream>
// 模板函数
template <typename T>
void print(T a) {std::cout << "Template: " << a << std::endl;
}// 非模板函数(参数类型为 int)
void print(int a) {std::cout << "Non-template: " << a << std::endl;
}int main() {print(42); // 调用非模板函数(精确匹配)print(3.14); // 调用模板函数(生成 print<double>)
}
输出:
Non-template: 42
Template: 3.14
更特化的的模板函数优先
- 当多个模板都能匹配时,编译器选择参数范围更狭窄(更特化)的模板。
#include <iostream>// 通用模板
template <typename T>
void show(T a) {std::cout << "Generic: " << a << std::endl;
}// 更特化的模板(针对指针)
template <typename T>
void show(T* a) {std::cout << "Specialized (pointer): " << *a << std::endl;
}int main() {int x = 10;show(x); // 调用通用模板(T=int)show(&x); // 调用指针特化版本(T=int)
}
- 通用模板 T 可以匹配任何类型。
- 指针特化模板 T* 只能匹配指针类型,因此更特化。
输出:
Generic: 10
Specialized (pointer): 10
精确匹配优先于隐式转换
- 如果模板生成的实例化版本与非模板函数相比,参数匹配更精确(无需转换),优先选择模板。
#include <iostream>// 模板函数
template <typename T>
void log(T a) {std::cout << "Template log: " << a << std::endl;
}// 非模板函数(参数类型为 double)
void log(double a) {std::cout << "Non-template log: " << a << std::endl;
}int main() {log(42); // 调用模板生成的 log<int>(精确匹配)log(3.14); // 调用非模板函数(精确匹配)
}
- 模板实例化版本:完全匹配
int
- 非模板函数:需要
int → double
隐式转换
输出:
Template log: 42
Non-template log: 3.14
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
// 专门处理int的加法函数
int Add(int left, int right)
{return left + right;}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{return left + right;
}
void Test()
{Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}
显式指定模板参数
- 显式指定模板参数时,编译器直接实例化模板,不参与与非模板函数的优先级比较。
#include <iostream>template <typename T>
void report(T a) {std::cout << "Template report: " << a << std::endl;
}void report(double a) {std::cout << "Non-template report: " << a << std::endl;
}int main() {report(42); // 调用模板生成的 report<int>report<double>(3.14); // 强制调用模板生成的 report<double>report(3.14); // 调用非模板函数(精确匹配 double)
}
显式指定模板参数
- 用户强制要求实例化
report<double>
。
跳过非模板函数检查
- 显式指定模板参数时,编译器直接生成
report<double>
,不再考虑非模板函数。
结果
- 调用模板实例化的
report<double>
,而非 非模板函数report(double a)
。
输出:
Template report: 42
Template report: 3.14
Non-template report: 3.14
优先级总结
#include <iostream>// 非模板函数
void test(int a) {std::cout << "Non-template: " << a << std::endl;
}// 通用模板
template <typename T>
void test(T a) {std::cout << "Generic template: " << a << std::endl;
}// 更特化的模板(针对 double)
template <>
void test(double a) {std::cout << "Specialized template: " << a << std::endl;
}int main() {test(42); // 非模板函数(精确匹配)test(3.14); // 更特化的模板(double 特化)test("Hi"); // 通用模板(T=const char*)
}
- 非模板函数 > 特化模板 > 通用模板
- 精确匹配(无需类型转换)优先于需要隐式转换的函数。
- 显式指定模板参数时,直接实例化模板(跳过非模板函数)。
输出:
Non-template: 42
Specialized template: 3.14
Generic template: Hi
函数模板的底层原理
模板的编译阶段
- 抽象语法树(AST)
存储:
编译器将模板的语法结构(如函数参数、返回类型、操作逻辑)转换为AST保存,但不生成任何机器码。
template<typename T>
T max(T a, T b) { return (a > b) ? a : b; }
// 仅保存AST,无代码生成
实例化触发
- 隐式实例化:当代码中首次使用模板时触发。
int main() {max(3, 5); // 触发 max<int> 的实例化max(3.0, 5.0); // 触发 max<double> 的实例化
}
实例化位置:编译器在调用点的作用域内生成实例化代码,通常位于当前编译单元(.cpp
文件)内。
- 显式实例化控制
手动指定实例化:通过 template
关键字强制生成特定类型的实例。
template int max<int>(int, int); // 显式实例化 int 版本
类型推导的底层逻辑
推导规则
- 按值传递:编译器执行类型退化(
Decay
),移除引用、const/volatile
修饰符,数组退化为指针。
template<typename T>
void f(T t) {}const int a = 10;
int arr[3] = {1, 2, 3};
f(a); // T = int(移除const)
f(arr); // T = int*(数组退化为指针)
- 按引用传递:保留原始类型信息。
template<typename T>
void f(T& t) {}const int a = 10;
f(a); // T = const int(保留const)
- 万能引用(
Forwarding Reference
)
引用折叠规则:T&&
根据实参的左右值推导不同结果。
引用折叠规则:
T& & → T&
T&& & → T&
T& && → T&
T&& && → T&&
template<typename T>
void f(T&& t) {}int a = 10;
f(a); // T = int&(左值 → T& && → T&)
f(10); // T = int(右值 → T&&)
模板实例化
1、语法检查
- 两阶段名称查找(
Two-Phase Lookup
):
第1阶段:解析模板定义时检查非依赖名称(如全局函数、字面量类型)。
template<typename T>
void func(T t) {int x = 10; // 非依赖名称,立即检查std::cout << x; // 依赖名称,延迟检查
}
第2阶段:实例化时检查依赖名称(如 T::member
、模板参数相关的表达式)。
2、生成机器码
- 符号生成与名称修饰:为每个实例生成唯一的符号名。
GCC/Clang:_Z3maxIiET_S0_S0_(max<int>)。MSVC:??$max@H@@YAHHH@Z(max<int>)。
符号组成:
- 函数名、模板参数、命名空间、参数类型等编码。
示例:void ns::foo<int, double>(int*)
可能被编码为:
GCC:_ZN2ns3fooIiJdEEEvPi
MSVC:??$foo@H$0A@@ns@@YAXPAH@Z
反编译工具:
- GCC:c++filt _ZN2ns3fooIiJdEEEvPi → ns::foo<int, double>(int*)
- MSVC:undname.exe 工具可解析修饰名。
3、 实例化重复与优化
- 代码膨胀示例:
max(1, 2); // 生成 int 版本
max(1L, 2L); // 生成 long 版本
max(1.0f, 2.0f); // 生成 float 版本
每个实例独立生成代码,导致二进制文件增大。
- 显式实例化优化:
// 在某个.cpp文件中集中实例化
template int max<int>(int, int);
template double max<double>(double, double);
模板特化的底层实现
- 全特化(
Full Specialization
) 直接覆盖通用模板:生成特定类型的独立实现。
template<>
int max<int>(int a, int b) {// 定制化的int版本实现return (a > b) ? a : b;
}
编译器直接使用全特化版本,跳过通用模板逻辑。
- 偏特化模拟(通过重载) 函数模板不支持偏特化,但可通过重载实现类似效果。
template<typename T> void process(T) {} // 通用版本
template<typename T> void process(T*) {} // 指针特化版本
template<typename T> void process(T[], int size) {} // 数组特化版本
SFINAE的底层机制
- 替换失败:
在推导阶段,若替换模板参数导致非法表达式或类型,候选被静默排除。
示例:
template<typename T>
auto f(T t) -> decltype(t.size()) { ... } // 仅当 T 有 size() 时有效
编译器行为:
- 尝试所有候选函数,排除无效替换的模板,保留有效候选参与重载。
编译器与链接器的协作
实例化重复问题
- 多个编译单元实例化相同模板:每个.cpp文件独立生成
max<int>
,导致重复代码。 - 链接器合并:最终链接时保留一份
max<int>
的副本,其余被丢弃。
显式实例化声明
- 减少重复实例化:在头文件中声明
extern template
,在某个.cpp文件中集中定义。
// header.h
extern template int max<int>(int, int); // 声明不实例化
// source.cpp
template int max<int>(int, int); // 实际实例化
底层示例:从代码到汇编
- 代码示例:
template<typename T>
T add(T a, T b) { return a + b; }int main() {add(1, 2); // 实例化 add<int>add(3.0, 4.0); // 实例化 add<double>
}
- GCC生成的汇编代码(简化版)
; add<int> 的实例化
_Z3addIiET_S0_S0_:lea eax, [rdi + rsi] ; 整数加法(寄存器操作)ret; add<double> 的实例化
_Z3addIdET_S0_S0_:addsd xmm0, xmm1 ; 浮点数加法(SSE指令)retmain:mov edi, 1 ; 传递参数1到edi(int调用约定)mov esi, 2 ; 传递参数2到esicall _Z3addIiET_S0_S0_ ; 调用 add<int>movsd xmm0, [rip + .LC0] ; 加载3.0到xmm0movsd xmm1, [rip + .LC1] ; 加载4.0到xmm1call _Z3addIdET_S0_S0_ ; 调用 add<double>xor eax, eax ; 返回0ret
编译器的工作流程
- 编译器的整体结构
编译器通常可以分为前端(Front - End
)、中端(Middle - End
)和后端(Back - End
)三个主要部分:
- 前端:负责处理与源语言相关的分析工作,包括词法分析、语法分析、语义分析等,将源代码转换为一种中间表示形式(IR)。
- 中端:对中间表示形式进行优化,提高代码的性能和效率,不依赖于具体的源语言和目标机器。
- 后端:将优化后的中间表示形式转换为目标机器的机器语言代码,处理与目标机器相关的问题,如寄存器分配、指令选择等。
对于函数模板,编译器会经过如下几个阶段处理:
前端编译阶段
- 词法分析(
Lexical Analysis
) - 工作原理:
编译器的词法分析器会按字符逐个读取源代码,将其拆分成一个个词法单元(Token)。例如,对于代码
template <typename T> T add(T a, T b) { return a + b; }
词法分析器会识别出 template、<、typename、T
等词法单元。
- 实现方式:
通常使用有限状态自动机(Finite State Automaton, FSA
)来实现。编译器会预先定义好各种词法单元的模式,当读取字符时,根据当前状态和输入字符进行状态转移,最终识别出对应的词法单元。例如,使用正则表达式来描述标识符、关键字等的模式,再将正则表达式转换为有限状态自动机进行匹配。
- 语法分析(
Syntax Analysis
) - 工作原理:
语法分析器根据词法分析器输出的词法单元序列,依据 C++ 的语法规则构建抽象语法树(Abstract Syntax Tree, AST
)。AST 是一种树形结构,它以一种更结构化的方式表示源代码的语法结构。例如,对于上述 add 函数模板,AST 会包含模板声明、函数定义、参数列表、函数体等节点。
- 实现方式:
常见的实现方法有递归下降分析法、算符优先分析法和 LR 分析法等。递归下降分析法是一种自顶向下的分析方法,它为每个非终结符编写一个递归函数,通过递归调用这些函数来构建 AST。例如,对于函数定义,会有一个函数来处理函数头,另一个函数来处理函数体。
- 语义分析 - 模板定义检查
- 工作原理:
语义分析器对 AST 进行检查,确保模板定义符合 C++ 的语义规则。例如,检查模板参数是否合法、模板函数体中的语句是否符合语法和语义要求等。对于 会检查 T 是否为合法的模板参数类型。
template <typename T> T add(T a, T b)
- 实现方式:
通过遍历 AST,对每个节点进行语义检查。编译器会维护一些符号表和类型系统,用于记录和检查标识符的作用域、类型信息等。例如,当遇到一个变量时,会在符号表中查找其定义,并检查其类型是否与使用处匹配。
- 模板定义符号表构建
- 工作原理:
符号表是编译器用于记录标识符信息的数据结构。在模板定义阶段,编译器会为模板及其相关的标识符(如模板参数、函数名等)建立符号表项。例如,对于 add 函数模板,会在符号表中记录模板名 add、模板参数 T 以及它们的作用域等信息。
- 实现方式:
通常使用哈希表或树形结构来实现符号表。当遇到一个新的标识符时,会在符号表中插入一个新的表项;当使用一个标识符时,会在符号表中查找对应的表项。
模板实例化阶段
- 模板实例化请求
- 工作原理
:当代码中使用模板函数并指定具体类型时,会触发模板实例化请求。
例如:
int result = add<int>(1, 2);
会触发 add 函数模板针对 int 类型的实例化请求。
- 实现方式:
编译器在编译过程中遇到模板函数调用时,会记录调用的位置和提供的模板参数类型,然后发起实例化请求。
- 显式与隐式实例化判断
- 工作原理:
编译器会根据代码中是否使用 template
关键字明确指定实例化来判断是显式实例化还是隐式实例化。
例如:
template int add<int>(int, int);
是显式实例化
add<int>(1, 2);
是隐式实例化。
- 实现方式:
在处理模板实例化请求时,检查代码中是否存在显式实例化的语法结构。
- 实例化上下文确定
- 工作原理:
确定实例化所需的环境和信息,包括命名空间、作用域等。例如,在不同的命名空间中使用同一个模板函数,实例化时需要考虑命名空间的影响。
- 实现方式:
通过维护作用域栈和命名空间信息,在实例化时根据当前的作用域和命名空间来确定实例化上下文。
- 模板参数推导
- 工作原理:
当调用模板函数时没有显式指定模板参数类型,编译器会根据调用时提供的实参类型推导出模板参数的具体类型。例如,add(1, 2);
编译器会根据实参 1 和 2 的类型 int 推导出模板参数 T 为 int。
- 实现方式:
编译器会根据实参的类型和模板参数的匹配规则进行推导。匹配规则包括类型转换、引用折叠等。例如,如果实参是 const int 类型,而模板参数是 T,则会推导出 T 为 int。
- 模板参数替换
- 工作原理:
将模板代码中的模板参数替换为具体类型。例如,对于 add 函数模板,当 T 被推导为 int 后,会将函数体中的 T 都替换为 int。
- 实现方式:
通过遍历 AST,将所有与模板参数相关的节点替换为具体类型的节点。
- 重写模板代码
- 工作原理:
根据替换后的类型,对模板代码进行重写,生成具体的函数代码。例如,将 add
函数模板重写为
int add(int a, int b) { return a + b; }。
实现方式:在 AST 上进行修改和生成新的代码节点,然后将修改后的 AST 转换为具体的源代码。
- 模板特化检查
- 工作原理:
查看是否存在针对当前类型的特化版本。如果有特化版本,则使用特化版本;否则,使用通用模板。例如,对于 add 函数模板,如果存在针对 double 类型的特化版本,当调用 add<double>(1.0, 2.0)
时,会使用特化版本。
- 实现方式:
在符号表中查找是否存在针对当前类型的特化模板定义,如果存在,则进行合法性检查并使用该特化版本。
后端编译阶段
- 具体代码生成
- 工作原理:
根据重写后的模板代码生成具体的目标代码。编译器会将抽象的代码结构转换为具体的机器指令。例如,将 int add(int a, int b) { return a + b; }
转换为对应的汇编指令。
- 实现方式:
使用代码生成器,根据目标机器的指令集和架构,将 AST 或中间表示转换为汇编代码。
- 中间代码生成
- 工作原理:
将具体代码转换为中间表示形式(IR),便于后续优化。IR
是一种独立于目标机器的代码表示,具有更高的抽象层次。例如,将汇编代码转换为 LLVM IR
。
- 实现方式:
通过对 AST 进行分析和转换,生成中间代码。中间代码通常具有更简单的结构和更统一的表示,便于进行各种优化操作。
- 代码优化
- 工作原理:
对中间代码进行优化,提高代码的性能。优化包括局部优化和全局优化。局部优化主要针对单个函数或代码块内的代码进行优化,如常量折叠、死代码消除等;全局优化则考虑整个程序的上下文进行优化,如函数内联、循环展开等。
- 实现方式:
使用各种优化算法和技术,如数据流分析、控制流分析等。例如,常量折叠是通过在编译时计算常量表达式的值,将表达式替换为计算结果;函数内联是将函数调用替换为函数体的代码。
- 目标代码生成
- 工作原理:
将优化后的中间代码转换为目标机器的汇编代码。编译器会根据目标机器的指令集和架构,将中间代码中的操作转换为具体的机器指令。
- 实现方式:
使用目标代码生成器,根据中间代码和目标机器的信息生成汇编代码。
- 符号解析
- 工作原理:
解析代码中的符号引用,确定符号的实际地址。在编译过程中,不同的源文件可能会引用相同的符号,符号解析的目的是将这些引用与实际的定义关联起来。例如,在一个源文件中调用另一个源文件中定义的函数,需要通过符号解析确定函数的实际地址。
- 实现方式:
链接器会维护一个符号表,记录所有符号的定义和引用信息。在链接过程中,会根据符号表中的信息将符号引用与实际的定义进行匹配。
- 链接
- 工作原理:
将多个目标文件链接成一个可执行文件。不同的源文件会被编译成不同的目标文件,链接器会将这些目标文件合并,并处理符号引用和重定位等问题。
- 实现方式:
链接器会读取所有的目标文件和库文件,将它们的代码和数据段合并,处理符号引用和重定位信息,最终生成一个可执行文件。
函数模板总结
延迟编译与模板存储
- 蓝图存储:模板定义时,编译器仅保存其语法结构(如AST),不生成机器码。
- 触发实例化:首次调用时(如 max(3,5))才根据具体类型生成实际函数。
类型推导规则
- 按值传递:退化类型(移除引用、const,数组/函数转指针)。
template<typename T> void f(T t);
f("Hello"); // T推导为 `const char*`(数组退化为指针)
- 按引用传递:保留原始类型修饰符。
template<typename T> void f(T& t);
const int a = 10;
f(a); // T推导为 `const int`
- 万能引用(
T&&
):根据实参左右值推导不同引用类型(引用折叠规则)。
实例化过程
- 类型推导:确定模板参数 T。
- 语法检查:验证 T 是否支持模板内所有操作(如 operator+)。
- 生成机器码:将模板中的 T 替换为具体类型,生成独立函数。
- 名称修饰:生成唯一符号名(如 Z3maxIiET_S0_S0 表示 max)。
符号管理与链接优化
- 代码膨胀:每个类型生成独立实例(如 max 和 max)。
- 显式实例化:通过 template int max(int, int); 集中生成代码,减少重复。
- 链接器合并:多个编译单元中的相同实例在链接时仅保留一份。
模板特化与优先级
- 全特化:直接覆盖通用模板,生成特定类型优化版本。
template<> int max<int>(int a, int b) { ... }
- 偏特化模拟:通过重载实现(如针对指针或容器的特化版本)。
核心代价与优化策略
问题 | 优化手段 |
---|---|
代码膨胀 | 显式实例化、类型擦除(如 std::function) |
编译时间增长 | 前置声明、extern template 声明 |
二义性错误 | 明确模板参数、避免重载冲突 |
写在最后
本文到这里就结束了,有关C++更深入的讲解,如类模板,继承和多态,C++11新语法新特性等高级话题,后面会发布专门的文章为大家讲解。感谢您的观看!
如果你觉得这篇文章对你有所帮助,请为我的博客 点赞👍收藏⭐️ 评论💬或 分享🔗 支持一下!你的每一个支持都是我继续创作的动力✨!🙏
如果你有任何问题或想法,也欢迎 留言💬 交流,一起进步📚!❤️ 感谢你的阅读和支持🌟!🎉
祝各位大佬吃得饱🍖,睡得好🛌,日有所得📈,逐梦扬帆⛵!