构造函数的深度解析
目录摘要一概念二分类三使用1无参构造函数2含参构造函数3全缺省构造函数4多个构造函数5总结四默认生成的构造函数的作用1情况12情况23情况34C11补丁5总结五初始化列表1解决的问题2书写规则3初始化列表一直存在4初始化列表的4种应用场景5注意事项六总结1来龙去脉2构造函数细节3初始化列表细节摘要本文介绍构造函数的方方面面从为什么要有构造函数到构造函数的类型已经如果正确使用构造函数再到构造函数无法解决的问题从而引出初始化列表来解决最后总结二者性质.....一概念对于以下Date类class Date { public: void Init(int year, int month, int day) { year year; _month month; _day day; } void Print() { cout _year - _month - _day endl; } private: int _year; int _month; int _day; }; int main() { //创建一个d1 并且初始化 Date d1; d1.Init(2022, 7, 5); d1.Print(); //创建一个d2 并且初始化 Date d2; d2.Init(2022, 7, 6); d2.Print(); return 0; }❓️创建一个对象就得初始化一次 未免有点麻烦那能否在对象创建时就将信息设置进去呢所以就有了构造函数构造函数概念构造函数是类的默认成员函数是特殊的成员函数需要注意的是构造函数虽然名称叫构造但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时空间就开好了)而是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能构造函数自动调用的特点就完美的替代的了Init。我们再也不需要手动Init了~其书写特征如下①.函数名与类名相同②无返回值(void都不用写)③.对象实例化时编译器自动调用对应的构造函数④. 构造函数可以重载(可以多个构造函数)二分类构造函数可以分为两类①默认构造函数②含参构造函数而默认构造函数包括无参构造函数、全缺省构造函数、不写构造时编译器默认生成的无参构造此外须知的是不显式写构造函数C编译器⾃动⽣成的构造函数是无参的这是必然的程序员都没显式写构造你默认生成的构造难道还要带参数我也看不到你生成的函数长什么样子啊自然也不知道给你传什么参数~总结不传实参就可以调用的构造就叫默认构造常见误区❌️❗️不写构造函数编译器默认生成的构造函数才叫默认构造函数错三使用1无参构造函数#define _CRT_SECURE_NO_WARNINGS 1 #includeiostream using namespace std; class Date { public: // 1.无参构造函数 Date() { _year 1; _month 1; _day 1; } void Print() { cout _year - _month - _day endl; } private: int _year; int _month; int _day; }; int main() { Date d1; // 调用无参构造函数 return 0; }2含参构造函数#define _CRT_SECURE_NO_WARNINGS 1 #includeiostream using namespace std; class Date { public: // 2.带参构造函数 Date(int year, int month, int day) { _year year; _month month; _day day; } void Print() { cout _year - _month - _day endl; } private: int _year; int _month; int _day; }; int main() { Date d1(1,1,1); // 调用无参构造函数 d1.Print(); return 0; }3全缺省构造函数#define _CRT_SECURE_NO_WARNINGS 1 #includeiostream using namespace std; class Date { public: // 2.带参构造函数 Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; } void Print() { cout _year - _month - _day endl; } private: int _year; int _month; int _day; }; int main() { Date d1;//触发缺省函数 d1.Print(); Date d2(1, 1, 1); //使用自带参数 d2.Print(); return 0; }①②③点的总结三种构造函数都可以单独使用但是需要注意的是如果构造函数可以无参不管这种无参是真正的无参构造还是全缺省不传参数形成的无参构造在用的时候都不能带括号否则会变成函数的声明-----类似 int Add( )容易混淆为参数为空的函数声明int main() { Date d1;//正确 Date d2();//错误成了函数声明 return 0; }而带参构造我们就可以带括号了比如Date d2(1,1,1)这是不会混淆为函数声明的因为函数声明的参数是类型而我们(1,1,1)直接传递了确切的值4多个构造函数class Date { public: //无参构造函数 Date() { _year 1; _month 1; _day 1; } //带参构造函数 Date(int year, int month, int day) { _year year; _month month; _day day; } void Print() { cout _year - _month - _day endl; } private: int _year; int _month; int _day; }; int main(void) { Date d1;// 调用无参构造函数 d1.Print(); Date d2(2024, 10, 14);// 调用带参构造函数 d2.Print(); return 0; }④的总结构造函数正如其特征中的第4点是可以重载但是需要注意的是默认构造函数全缺省构造函数无参构造函数只能存在一个二者语法上形成重载可以共存但是实际编译起来会因为二义性不能共存而报错因为Date d1;根本不知道调哪个编译器默认生成的构造也和上述两者不能共存但是编译器默认生成的构造肯定是单独存在的因为但凡有其他的构造函数都不会让编译器默认生成构造~二义性代码展示class Date { public: Date() { _year 1; _month 1; _day 1; } Date(int year 2024, int month 10, int day 14) { _year year; _month month; _day day; } private: int _year; int _month; int _day; }; int main(void) { //无参那到底是走无参还是全缺省 Date d1; return 0; }5总结我们在C中一般使用全缺省的构造函数因为真的很好用你可以不传参可以传部分参数也可以传全部参数可谓灵活多变代码如下#include iostream class Date { public: /* 全缺省 */ Date(int year 0, int month 1, int day 1) { _year year; _month month; _day day; } void Print() { printf(%d-%d-%d\n, _year, _month, _day); } private: int _year; int _month; int _day; }; int main(void) { Date d1; // 不传就是缺省值 Date d2(2024, 10, 14); Date d3(2024); Date d4(2024, 10); d1.Print(); // 0-1-1 d2.Print(); // 2024-10-14 d3.Print(); // 2024-1-1 d4.Print(); // 2024-10-1 return 0; }四默认生成的构造函数的作用再次强调不写默认生成的构造函数是默认构造函数的一种而默认构造函数又包括三种①无参构造函数、②全缺省构造函数、③不写默认生成的构造函数Q如果我们不在类中显式的写构造函数呢系统会做什么吗A会的如上文所述C编译器会默认自动生成的无参的构造函数而至C编译器默认生成的构造函数会对成员变量做什么呢看例子无构造函数代码class Date { public: private: int _year; int _month; int _day; }; int main(void) { //C编译器生成的是无参构造函数 //所以也不能带()否则成为函数声明 Date d1; return 0; }解释发现成员变量还是随机值那岂不是什么都没做C把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型如int/char/指针类型等...自定义类型就是我们使用class/struct/union等自己定义的类型编译器默认生成的构造函数会对其内置类型的变量不作初始化对其自定类型成员调用的它的默认构造函数所以默认构造只是对内置类型不做初始化但对自定义类型成员会调用默认构造下面我们用例子验证默认生成的构造函数的作用我们让Date类自动生成构造函数然后Date类对象有一个Time类对象_t我们让Time类型对象的构造函数分别为①无参构造函数、②全缺省构造函数、③不写构造时编译器默认生成的构造函数也就是默认构造函数的3种情况1情况1Date类中没有显式定义构造函数所以编译器默认自动生成无参构造函数成员变量含有自定义变量Time类的对象_t并且Time类中有默认构造函数(无参)class Time { public: Time() { cout Time() endl;//打印了Time()就代表这个构造函数被调用了 _hour 0; _minute 0; _second 0; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year; int _month; int _day; // 自定义类型 Time _t; }; int main() { Date d; return 0; }解释正如前文所说 Date类默认生产的构造函数会对其内置类型的变量不作初始化所以监视窗口中的3个成员变量都是随机值而Date类默认生产的构造函数会对其自定类型成员_t调用的它的默认构造函数所以_t对象中的成员变量都被它的默认构造函数初始化了为02情况2Date类中没有显式定义构造函数所以编译器默认自动生成无参构造函数成员变量含有自定义变量Time类的对象_t并且Time类中有默认构造函数(全缺省)class Time { public: Time(int hour 8, int minute 24, int sconde 5) { cout Time() endl;//打印了Time()就代表这个构造函数被调用了 _hour hour; _minute minute; _second sconde; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year; int _month; int _day; // 自定义类型 Time _t; }; int main() { Date d; return 0; }解释同理_t的成员变量会被默认构造函数初始化为缺省值3情况3Date类中没有显式定义构造函数所以编译器默认自动生成无参构造函数成员变量含有自定义变量Time类的对象_t并且Time类中有默认构造函数(编译器默认生成的无参构造)class Time { public: //无构造函数 private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year; int _month; int _day; // 自定义类型 Time _t; }; int main() { Date d; return 0; }结果解释同理_t会被自己的默认构造函数初始化这里Time类也没有显示写所以编译器也会给Time默认生成构造函数该构造函数同样不对内置类型进行任何赋值④情况4我们上面三种情况验证了该观点编译器默认生成的构造函数会对其内置类型的变量不作初始化对其自定类型成员调用的它的默认构造函数❓️现在假设我们让这个自定类型成员_t的构造函数不是一个默认构造函数了我们让Time类的构造函数变成带参的构造函数会发生什么呢class Time { public: Time(int hour, int minute, int seconf) { cout Time() endl; _hour 0; _minute 0; _second 0; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year; int _month; int _day; // 自定义类型 Time _t; }; int main() { Date d; return 0; }解释必然是报错的①报错就是指_t所对应的Time类中没有默认构造函数所以无法去调用该构造函数对_t的成员变量进行初始化因为带参构造不是默认构造~②虽然解决这个问题需要用到后文学习的初始化列表但是现在就可以解释为什么会报错因为如果_t对应的Time类的构造函数是带参的那你main中的Date d;这行代码是没有提供任何参数的d的成员变量_t是需要参数的所以Date d;必然报错4C11补丁我们知道编译器默认生成的构造函数会对其内置类型的变量不作初始化对其自定类型成员调用的它的默认构造函数开发者认为编译器默认生成的构造函数不对内置类型成员初始化是一种缺陷所以在C11版本中又打了补丁允许内置类型成员变量在类中声明时可以给默认值从侧面完成了内置类型的初始化成员变量在类中声明时可以给默认值的代码如下class Time { public: Time() { cout Time() endl; _hour 0; _minute 0; _second 0; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year 2024; int _month 10; int _day 14; // 自定义类型 Time _t; }; int main() { Date d; return 0; }结果解释相较于之前弥补了编译器默认生成的构造函数不对内置类型初始化的缺点d的成员变量也有默认值了5总结C编译器生成默认的构造函数会对其内置类型的变量不作初始化对其自定类型成员_t调用的它的默认构造函数无参的构造就叫作默认构造函数根据默认构造函数来初始化若自定类型成员_t没有默认构造函数而是含参的构造函数则会报错。五初始化列表1解决的问题所以怎么解决若自定类型成员_t没有默认构造函数而是含参的构造函数则会报错这一点呢换句话说就是你怎么给_t传参C发明了初始化列表来解决这个问题需要注意的是初始化列表不仅仅可以解决这种自定类型成员_t无默认构造函数的问题还可以解决需要在声明就初始化的成员变量这个问题比如引用变量const修饰的变量引用和const修饰的变量都是在声明的时候就初始化了这是语法规定的不需要质疑可以理解为出生就必须有值的成员变量所以如果成员变量是一个引用变量或者const修饰的变量此时在构造函数体中再给引用或const修饰的变量赋值就已经错了已经来不及了在构造函数中不叫初始化而叫赋值赋值变量已经诞生了你再去修改它。初始化变量诞生的那一刻就赋予初值。例子int main() { int a;//错❌️ const int b;//错❌️ return 0; }解释成员变量如果是引用或const修饰的在声明不给初始化就是上面这种错误所以初始化列表可以解决三种问题①自定类型成员_t无默认构造函数②引用变量③const修饰的变量2书写规则初始化列表书写规则以一个冒号开始接着以逗号分隔的数据成员列表每个成员变量后面跟一个放在括号中的初始值或表达式如图所示class Example { public: // 初始化列表解决所有“出生就必须有值”的成员 Example(int a, int b, int ref, int constVal) : _ref(ref) // 引用必须初始化 , _constVal(constVal) // const 变量必须初始化 , _obj(b) // 没有默认构造函数的自定义类型必须初始化 , _normal(a) // 普通成员也可以在这里初始化推荐效率高 { // 构造函数体内只能做赋值操作 // _ref ...; // 错误引用不能重新绑定 // _constVal ...; // 错误const 变量不能赋值 } private: int _ref; // 引用成员 const int _constVal; // const 成员 NoDefaultConstructor _obj; // 无默认构造的自定义类型 int _normal; // 普通成员 };解释三种问题都得到了解决并且不需要在声明时就初始化的成员变量也可以在写进初始化列表中百利而无一害提高了效率3初始化列表一直存在其实初始化列表不是新的东西其一直都存在当我们不显式地使用初始化列表的时候成员变量也会先去使用初始化列表的只不过既然没写那初始化就是空的然后再执行构造函数体内的代码只是先去使用初始化列表这一步我们看不见所以构造函数只有存在那么初始化列表也一定存在二者只会同时存在而不写构造编译器默认生成的构造函数不对内置类型处理对自定义类型调用他的默认构造这件事情本身这一系列的操作就是初始化列表做的初始化列表是每个构造函数与生俱来的“第一道工序”你写不写它都在那。你写了就按你的指令干你不写编译器就按它的默认方式干。对于内置类型和有默认构造的自定义类型它的默认方式勉强能工作对于引用、const、无默认构造的类型它的默认方式直接罢工出错必须手写初始化列表所以C11的补丁允许内置类型成员变量在类中声明时可以给默认值从侧面完成了内置类型的初始化这也是在初始化列表中完成的class Date { public: // 编译器默认生成的构造函数会使用这些默认值来初始化 // _year 1900, _month 1, _day 1 private: int _year 1900; // 声明时给默认值 int _month 1; int _day 1; };你写的int _year 1900;不是直接赋值而是告诉编译器“如果你要生成默认构造函数请在初始化列表里把_year初始化为 1900。”所以编译器生成的默认构造函数等价于Date() : _year(1900), _month(1), _day(1) { }4初始化列表的4种应用场景以下四点需要显式地写初始化列表①引用②const③无默认的构造函数的自定义类型成员变量④想自己控制有默认构造函数的自定义变量其中①②③是必须使用初始化列表解决的而④是程序员自我的需求我们知道成员变量是自定义类型但是自定义类型有默认构造函数是不需要初始化列表的但是现在有两种情况、情况1该自定义类型不仅有默认构造函数并且该默认构造函数是全缺省类型情况2该自定义类型不仅有默认构造函数并且还有带参构造函数在情况1中因为自定义类型的默认构造函数是全缺省的所以你也可以进行传参就需要初始化列表在情况2中如果你想调用自定义类型的另一种带参构造函数就需要初始化列表其实本质就是自定义类型的构造函数需要传参就需要初始化列表来完成下面我们举例代码对这四种情况都使用初始化列表①引用②const③无默认的构造函数的自定义类型成员变量④想自己控制有默认构造函数的自定义变量注其中④可以分为两种情况来演示所以共5种例子#include string using namespace std; // 无默认构造函数的自定义类型 class NoDefault { public: NoDefault(int x) : _x(x) //构造函数是带参构造 {} private: int _x; }; // 有默认构造 带参构造 class HasDefault { public: HasDefault() : _name(default), _val(0) //默认构造 无参构造 {} HasDefault(const string name, int val) : _name(name), _val(val) //带参构造 {} private: string _name; int _val; }; // 只有全缺省构造函数这就是默认构造函数 class AllDefault { public: AllDefault(const string name default, int val 0)//默认构造 全缺省构造函数 : _name(name), _val(val) {} private: string _name; int _val; }; class Demo { public: Demo(int ref, int constVal, int noDefVal, const string name1, int val1, const string name2, int val2) : _ref(ref) // ① 引用必须初始化 , _constVal(constVal) // ② const必须初始化 , _noDef(noDefVal) // ③ 无默认构造必须传参 , _hasDef(name1, val1) // ④ 有默认构造但想自己传参控制 调用带参构造 , _allDef(name2, val2) // ⑤ 全缺省构造但想自己传参不用默认值 {} private: int _ref; const int _constVal; NoDefault _noDef; HasDefault _hasDef; AllDefault _allDef; }; int main() { int a 10; Demo d(a, 100, 200, hello, 42, world, 99); return 0; }解释成员类型初始化列表写法不用初始化列表传参的后果①引用int_ref(ref)编译报错引用必须出生就绑定②const变量_constVal(constVal)编译报错const 必须出生就有值③无默认构造_noDef(noDefVal)编译报错编译器不知道怎么初始化④有默认构造但想传参_hasDef(name1, val1)编译通过但先默认构造再赋值浪费效率⑤全缺省但想传参不用默认值_allDef(name2, val2)编译通过但先用默认值构造再赋值浪费效率⑤ 和 ④ 的本质区别④ HasDefault有两个独立的构造函数一个是无参默认一个是带参。你不写初始化列表编译器调无参默认那个你写了编译器调带参那个。⑤ AllDefault只有一个全缺省构造函数它既是默认构造也可以带参调用。你不写初始化列表编译器用默认参数调它你写了编译器用你传的参数调它。但本质完全一样只要你想传参就必须在初始化列表里传。进了函数体就只能赋值已经来不及了。5注意事项①每个成员变量在初始化列表中只能出现一次即初始化只能初始化一次MyClass(int x, int r, int m) : a(x), a(x),ref(r), member(m) //只能出现一次 { }解释因为始化列表是成员变量诞生的地方。一个变量不可能诞生两次所以它在初始化列表中只能出现一次在构造函数体内就可以多次赋值~②成员变量在类中的声明顺序就是在初始化列表中的初始化顺序与其在初始化列表中出现的顺序无关。class A { public: A(int a) :_a1(a) , _a2(_a1) // 先执行它 {} void Print() { cout _a1 _a2 endl; } private: int _a2; // _a2 先声明 int _a1; }; int main() { A aa(1); aa.Print(); }解释因为我们先声明的是 _a2所以在初始化列表里我们先初始化的是 _a2所以先执行_a2这一句 _a2(_a1) _a1 此时还是没有得到传过去的 1所以此时_a1还是随机值所以 _a2 被初始化成随机值了。按照声明顺序然后是 _a1 _a1 接收到了1自然会初始化成 1最后按顺序打印c 1 和 随机值。六总结1来龙去脉①构造函数的由来与本质在 C 中我们以前写类时经常需要手动写一个Init函数来初始化对象但每次都显式调用它太麻烦也容易忘记。于是 C 引入了构造函数这个机制把它作为一种特殊的“默认成员函数”。构造函数在对象创建时会由编译器自动调用它有着独特的语法规则函数名与类名相同没有返回值。根据参数不同构造函数又分为多种类型各自有不同的用途。②默认构造函数的三种形态与二义性默认构造函数指的是“调用时不需要传任何参数”的构造函数它包含三种形态①无参构造函数、②全缺省构造函数、③我们不写时编译器自动生成的默认构造函数。这三种都可以支持Date d;这样的无参调用所以任何两者都不能同时存在否则编译器在遇到无参调用时就会产生二义性不知道该选哪一个。需要注意的是很多人误以为“默认构造函数”只指编译器自动生成的那一种其实不是上面三种都叫默认构造函数。③编译器默认生成构造函数的缺陷与初始化列表的诞生当我们不写任何构造函数时编译器会为我们自动生成一个默认构造函数。但这个默认构造函数存在明显缺陷它对内置类型成员变量如int、char不作初始化放任它们成为内存中的随机垃圾值而对自定义类型成员它会去调用该类型的默认构造函数。这导致一个问题如果某个自定义类型成员根本没有默认构造函数编译器就不知道该拿它怎么办了编译直接报错。为了解决这类问题C 提供了初始化列表机制让你能够在成员变量“诞生”的那一刻就精确指定它的初始化方式。④初始化列表解决的四种典型情况初始化列表不仅能解决“无默认构造函数的自定义类型成员”的问题还能解决另外三种必须用初始化列表的情况。第一种是引用成员变量引用必须在诞生时就绑定到一个对象无法在构造函数体内重新绑定。第二种是 const 成员变量它必须在诞生时就拥有一个值此后终身不可修改。第三种是没有默认构造函数的自定义类型成员编译器不知道如何初始化它必须你手动在初始化列表中传参。第四种是你想自己控制有默认构造函数的自定义类型成员虽然不写初始化列表编译器也能自动调用它的默认构造但你想传参以获取不同的初始状态这也必须通过初始化列表来完成。⑤初始化列表与构造函数体各司其职、混着用才是常态尽管我们强调尽量使用初始化列表但这并不意味着构造函数可以只要初始化列表而不要函数体。初始化列表能做的是在成员变量“诞生”的那一刻赋予它们初始值解决引用、const、无默认构造的自定义类型等必须在出生时就确定状态的问题。但初始化列表也有它做不到的事它写不了复杂的逻辑判断做不了参数合法性校验也处理不了文件打开、内存申请、网络连接、日志记录这类带有副作用或需要错误处理的初始化工作。这些必须放在构造函数体内部完成。因此实际开发中初始化列表和构造函数体从来都是搭配使用的初始化列表负责让成员变量高效、正确地“出生”构造函数体则负责在成员诞生之后执行那些初始化列表无法承担的复杂初始化逻辑。两者不是二选一的关系而是各司其职、协同作战。❓️什么情况不需要构造函数呢也就是不用显式的写呢你的成员变量情况能接受不写构造函数吗全是内置类型能接受随机值✅ 能全是内置类型想要初始值❌ 不能必须自己写有自定义类型且都能默认构造你能接受默认行为✅ 能有自定义类型想传参控制❌ 不能必须自己写有引用、const、无默认构造的类❌ 绝对不能编译都过不了2构造函数细节①函数名与类名相同。②无返回值。 (返回值啥都不需要给也不需要写void不要纠结C规定如此)③对象实例化时系统会⾃动调用对应的构造函数。④构造函数可以重载。⑤如果类中没有显式定义构造函数则C编译器会自动⽣成⼀个无参的默认构造函数⼀旦用户显式定义编译器将不再生成。⑥无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数都叫做默认构造函数。但是这三个函数有且只有⼀个存在不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载但是调用时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认生成那个叫默认构造实际上无参构造函数、全缺省构造函数也是默认构造总结⼀下就是不传实参就可以调用的构造就叫默认构造。⑦我们不写编译器默认生成的构造对内置类型成员变量的初始化没有要求也就是说是是否初始化是不确定的看编译器。对于自定义类型成员变量要求调用这个成员变量的默认构造函数初始化。如果这个成员变量没有默认构造函数那么就会报错我们要初始化这个成员变量需要用初始化列表才能解决初始化列表我们下个章节再细细讲解。⑧说明C把类型分成内置类型(基本类型)和自定义类型。内置类型就是语⾔提供的原⽣数据类型 如int/char/double/指针等⾃定义类型就是我们使用class/struct等关键字⾃⼰定义的类型。3初始化列表细节①每个成员变量在初始化列表中只能出现⼀次语法理解上初始化列表可以认为是每个成员变量定义初始化的地⽅。②引用成员变量const成员变量没有默认构造的类类型变量必须放在初始化列表位置进⾏初始化否则会编译报错。③尽量使用初始化列表初始化因为不管你有没有写初始化列表都必然会走一遍初始化列表那些你不在初始化列表里初始化的成员编译器也会在初始化列表阶段自动处理它们内置类型不管、自定义类型调默认构造。既然无论如何都要走一遍初始化列表那不如你自己来控制初始化列表一步到位省去多余开销。④C11支持后如果这个成员在声明位置给了缺省值初始化列表会用这个缺省值初始化。如果你没有给缺省值对于没有显⽰在初始化列表初始化的内置类型成员是否初始化取决于编译器C并没有规定。对于没有显⽰在初始化列表初始化的⾃定义类型成员会调⽤这个成员类型的默认构造函数如果没有默认构造会编译错误。⑥ 初始化列表中按照成员变量在类中声明顺序进⾏初始化跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持⼀致 [ 作者 ] shylyly [ 首次发布 ] 2024.10.14❌ [ 最新修改 ] 2026.7.17 [ 声明 ] 由于笔者水平有限文中难免有疏漏或不妥之处还望读者不吝赐教