尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
C++类与对象收官:初始化列表、深拷贝与运算符重载实战
在 C 的学习路线上类和对象就像是一道分水岭。很多人在这里第一次感受到这门语言的重量也是从这一章开始程序不再是一行行顺序执行的指令而是变成了对象之间的协作与博弈。标题里写了“收官之战”这四个字很贴切——当你走到类与对象的下半程意味着你已经熬过了语法和直觉的磨合期接下来要面对的是真正能拉开差距的东西内存、权限、构造与析构的细枝末节以及在实战中怎么把这些机制组合起来不出事。这篇文章是系列终章聚焦那些教科书往往讲得比较含蓄、但写起来又绕不开的内容初始化列表、拷贝构造与赋值重载的深坑、static 和友元的真实用途以及 operator 重载里面的返回值陷阱。配的是完整可编译的代码段和我在实际项目里踩过的坑。不管你是在校准备期末考试还是自学转方向如果你已经知道什么是类、什么是对象但写出来的代码偶尔会崩、偶尔会编译不过这篇就是给你兜底的。1. 内容整体设计与思路拆解1.1 为什么“收官之战”收的是这几个点类和对象这一章知识点多且碎。构造函数、析构函数、访问限定符、this 指针、拷贝、赋值、静态成员、友元、运算符重载……如果只是应试背一背概念好像也能应付。但真到动手写的时候你会发现真正决定一段代码能不能跑的往往是那些“概念之外的东西”。我自己的体会是类和对象的难点分三个梯度。第一梯度是语法层比如怎么定义一个类、怎么实例化对象、怎么写构造函数。这部分跟着书敲一遍基本就会了。第二梯度是机制层你得理解编译器在你写的代码背后替你做了什么比如默认构造函数在什么情况下会被生成、什么时候又会消失拷贝的时候到底是浅拷贝还是深拷贝return 一个对象时走的是哪个函数。第三梯度才是真正的能力层把这些机制组合起来设计出一个内存安全、语义清晰、可维护的类。大多数人的差距不是第一梯度而是第二梯度到第三梯度之间的这段路。这篇文章的位置就是把第二梯度的问题掰开揉碎再往第三梯度推一把。1.2 一条主线贯穿对象生命周期我备课和写代码时习惯用一个逻辑串起所有知识点就是“对象的生命周期”。一个对象从出生到消亡它经历了什么出生阶段要分配内存、调构造函数、走初始化列表。存活阶段对象会被拷贝、被赋值、被作为参数传来传去、被放进容器里这个过程涉及拷贝构造和赋值重载。消亡阶段析构函数被调用资源被释放。而 static 和友元呢它们是这个生命周期之外的补充机制。static 成员属于类而不是属于某个具体对象友元则打破了类的封装边界让外部函数或类能访问私有成员。把类和对象的所有机制都挂在这条主线上你会发现很多问题其实是相通的。比如崩溃发生在析构阶段根源往往是存活阶段的浅拷贝赋值重载写不好表现又是运行时数据错乱。这篇文章就按这条主线去拆先把生命周期里的关键环节讲透再补充 static 和友元这两个外部视角最后落到一堆真实问题和排查思路上。2. 核心细节解析与实操要点2.1 初始化列表它不是“另一种写法”那么简单很多人第一次接触初始化列表以为它只是把构造函数里的赋值语句换了个位置。这是理解上最危险的一个误区。初始化列表和函数体内的赋值之间存在本质差异。先记住一个基本规则对象的成员变量初始化发生在进入构造函数函数体之前。也就是说你写在函数体里的m_age 18;根本不是初始化而是赋值。对于内置类型int、double、指针这两个区别看着不明显。但对于带构造函数的成员对象区别就大了没有在初始化列表中显式初始化的成员对象会先被默认构造函数“初始化”一遍然后你又在函数体里赋值一次等于白做了一次无用功甚至可能直接把刚构造好的对象又覆盖掉。更要命的是有三种成员你只能在初始化列表中初始化想靠函数体赋值就是在故意绕远路const 成员const 意味着一旦初始化不可修改所以必须直接在初始化列表中给初值。引用成员引用必须在定义时绑定这同样只能在初始化列表中完成。没有默认构造函数的成员对象如果成员对象只提供了带参构造函数那么你不把它写进初始化列表编译器就不知道该怎么“默认初始化”它直接报错。我拿一个实际场景给大家看class ConfigFile { public: ConfigFile(const std::string path, int timeout_ms) : m_path(path), // 普通成员但推荐放这里 m_timeout_ms(timeout_ms), m_default_name(unknown) // const 成员必须在这里 { // 函数体内可以做点校验或预处理 if (m_timeout_ms 0) { m_timeout_ms 1000; // 这里可以重新赋值注意m_timeout_ms不是const } } private: const std::string m_default_name; // const 成员 std::string m_path; int m_timeout_ms; };在实际开发里我习惯把能放进初始化列表的都放进去而不是留到函数体里赋值。理由很简单初始化列表的顺序是确定的按成员声明顺序不是按你的书写顺序语义也清晰——看一眼就明白这个对象构造时都干了什么。同时还能省下一次不必要的成员对象默认构造。这里有一个非常经典的坑初始化列表的成员初始化顺序跟你在列表中写的顺序无关只跟它们在类中的声明顺序有关。如果你写成了这样class Demo { public: Demo(int a) : m_b(a), m_a(m_b) {} // 看起来 m_a 用了 m_b private: int m_a; // 先声明先初始化 int m_b; // 后声明后初始化 };实际执行顺序是先初始化 m_a此时 m_b 还没初始化m_a 拿到的是一个未定义值。接着才初始化 m_b。所以编译开-Wreorder警告是值得养成的习惯它能在编译时就告诉你哪里可能出问题。2.2 拷贝构造与赋值重载浅拷贝是绝大多数崩溃的源头如果说初始化列表是构造阶段的暗坑那拷贝构造和赋值重载就是整个类和对象章节里最“要命”的战场。如果你写的类里有指针成员并且没有自己实现深拷贝逻辑那这个类就是一颗定时炸弹。默认拷贝构造函数做的是按字节拷贝也就是浅拷贝。按字节拷贝的意思是两个不同的对象它们的指针成员指向了同一块堆内存。当这两个对象离开作用域时析构函数会被调用两次同一块动态内存被 delete 两次程序直接崩给你看。更隐蔽一点的情况是你只释放了第一个对象的资源第二个对象再去访问那块内存时已经是悬空指针读出来的数据可能是乱的也可能是段错误。我给大家写一个自己动手实现深拷贝的场景。先看反例class MyString { public: MyString(const char* src) { m_size strlen(src) 1; m_data new char[m_size]; strcpy(m_data, src); } ~MyString() { delete[] m_data; } // 没有写拷贝构造和赋值重载 private: char* m_data; size_t m_size; }; MyString a(hello); MyString b(a); // 崩溃预定了这段代码只要运行到 b 和 a 都析构的时候必然崩。修复它你需要实现“深拷贝”拷贝时重新分配一块内存把数据复制过去让每个对象都拥有一份独立的资源。MyString(const MyString other) { m_size other.m_size; m_data new char[m_size]; strcpy(m_data, other.m_data); } MyString operator(const MyString other) { if (this ! other) { // 第一步自赋值检查 delete[] m_data; // 第二步释放旧资源 m_size other.m_size; m_data new char[m_size]; // 第三步分配新资源 strcpy(m_data, other.m_data); } return *this; // 第四步返回自身引用 }先说自赋值检查是为什么。如果写a a不检查的话会先把自己的 m_data 释放掉然后再去拷贝一个已经失效的内存区域结果自然是崩。然后是三步法释放、分配、拷贝。这三步顺序不能错。我见过有人先分配再释放结果旧数据还没来得及拷到新内存就没了也有人忘了释放旧资源造成内存泄漏。赋值重载返回引用这一点也容易出错。返回MyString对象按值返回在逻辑上没错但每赋值一次就会多产生一次临时对象的构造和析构性能白白损失。而且按你的直觉看a b c这种连续赋值需要赋值运算返回“对象本身”引用最适合。我在实际项目里对这类问题的态度是如果类里有指针、文件句柄、网络连接等资源必须三件套一起上——析构函数、拷贝构造、赋值重载。少一个就是你程序后期的隐患。如果类里全是内置类型或者没有资源的成员那默认生成的就够用不用自己折腾。2.3 static 成员与友元体制外的两个特殊角色static 成员和友元是类和对象里两个比较特殊的机制。它们都不属于“对象生命周期”这条主线但在设计时常常会用到所以我放到一起讲。static 成员变量属于类本身全类只有一个实例被所有对象共享。它的典型用途包括统计一个类创建了多少个对象、实现单例模式的实例指针、管理全局级的配置参数等。static 成员变量不能在构造函数里初始化必须在类外面单独定义一次这点几乎每个新手都会卡一次。class Logger { public: Logger() { s_instance_count; } static int instance_count() { return s_instance_count; } private: static int s_instance_count; // 声明 }; int Logger::s_instance_count 0; // 定义必须出现在类外static 成员函数也挺有意思。它没有 this 指针所以不能访问非 static 的成员变量。这意味着你只要不依赖对象状态逻辑就放在 static 函数里。这在工具类、全局计数、工厂模式中非常常用。友情提示一个容易混淆的点static 成员函数不能声明为 const因为它本身就没有 this 指针“const 修饰一个不存在的对象”是没有意义的语法错。再说友元。友元函数和友元类可以让外部函数或类访问当前类的私有成员这看起来像是对封装的破坏但实际使用时它能解决一些调度上的问题。最典型的场景是运算符重载比如你要实现cout 对象左操作数是 ostream 类型你没法给标准库的 ostream 类增加成员函数这时把输出运算符重载声明为这个类的友元函数就可以在外面使用对象的私有成员完成输出。友元的使用原则是能用公共接口解决的不用友元确有必要时才用而且尽量只友元一个函数不要动不动就友元整个类。滥用友元会让类的封装边界变得形同虚设。3. 实操过程与核心环节实现3.1 手把手写一个日期类构造函数与操作符重载的综合排雷我们先定一个目标写一个日期类支持初始化、自增增加一天、比较大小、输出到标准流。这个任务麻雀虽小五脏俱全几乎涵盖了类和对象章节的所有核心机制。代码细节我一步步拆开讲。实现前的设计思路是这样的日期合法性校验交给构造函数处理不合法就直接断言或抛异常这是“在对象出生阶段就阻止无效状态”的做法。运算符需要区分前置和后置C 用 “多写一个 int 参数” 来区分后置版本。这是语言的一个历史包袱但你必须记住。比较运算符重载成组成员函数比较顺手但operator必须写成友元函数。下面是核心代码#include iostream #include cassert class Date { friend std::ostream operator(std::ostream os, const Date d); public: Date(int year, int month, int day) : m_year(year), m_month(month), m_day(day) { assert(month 1 month 12); assert(day 1 day days_in_month(year, month)); } // 前置 Date operator() { add_one_day(); return *this; } // 后置注意假的int占位参数 Date operator(int) { Date tmp(*this); add_one_day(); return tmp; } bool operator(const Date other) const { return to_integer() other.to_integer(); } bool operator(const Date other) const { return m_year other.m_year m_month other.m_month m_day other.m_day; } int to_integer() const { return m_year * 10000 m_month * 100 m_day; } private: static int days_in_month(int year, int month) { static int days[] {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (month 2 is_leap(year)) { return 29; } return days[month]; } static bool is_leap(int year) { return (year % 4 0 year % 100 ! 0) || (year % 400 0); } void add_one_day() { if (m_day days_in_month(m_year, m_month)) { m_day; } else { m_day 1; if (m_month 12) { m_month; } else { m_month 1; m_year; } } } int m_year; int m_month; int m_day; }; std::ostream operator(std::ostream os, const Date d) { os d.m_year - d.m_month - d.m_day; return os; }这段代码包含了几个值得特别说明的细节第一后置返回的是旧值所以它的实现套路是先拷贝一个临时对象把自己加一天再把那个临时对象按值返回。前置返回的是自身所以返回引用。你要是把这两个返回方式搞反了轻则语义错误重则产生不必要的临时对象性能受损。第二operator和operator都加了const修饰符在函数末尾声明函数不会修改对象状态。这样才允许 const 对象或 const 引用去调用这些比较函数。不加 const会出现非常微妙的编译错误——比如把对象放进std::set时容器要求比较操作是 const 可调用的不写 const 直接编译不过。第三我把days_in_month和is_leap设为 static 私有成员函数。它们不依赖任何对象实例属于“纯粹的计算工具”放成 private 是为了不让外部随意调用避免把不该暴露的内部实现细节漏出去。调用方写起来会很顺手Date d1(2024, 2, 28); // 闰年所以下一个日会是2月29日 Date d2(2024, 3, 1); if (d1 d2) { std::cout 闰年判断正确2月28日加一天是3月1日 std::endl; }当你自己写完这个日期类再去看很多开源项目的代码就会发现运算符重载、const 成员函数、友元、static 全部是连在一起用的根本不分家。3.2 实现一个简单 String 类从资源管理到内存安全日期类没有指针成员所以不涉及深拷贝。但实际工程里大量崩溃都来自动态内存。我自己练习时会用一个精简版 String 类来固化深拷贝能力。这个练习比日期类更贴近真实项目。设计目标支持从const char*构造支持拷贝构造和赋值重载且为深拷贝支持输出析构函数正确释放内存完整代码如下class String { friend std::ostream operator(std::ostream os, const String s); public: String() : m_size(1), m_data(new char[1]) { m_data[0] \0; } String(const char* src) { m_size strlen(src) 1; m_data new char[m_size]; strcpy(m_data, src); } String(const String other) : m_size(other.m_size), m_data(new char[other.m_size]) { strcpy(m_data, other.m_data); } String operator(const String other) { if (this ! other) { delete[] m_data; m_size other.m_size; m_data new char[m_size]; strcpy(m_data, other.m_data); } return *this; } ~String() { delete[] m_data; } const char* c_str() const { return m_data; } private: char* m_data; size_t m_size; }; std::ostream operator(std::ostream os, const String s) { os s.m_data; return os; }写这个类时我要特别提醒你注意空字符串处理。默认构造函数里我给m_data分配了一个字符的空间并放了一个\0。为什么要这样因为如果你让空串的m_data是nullptr那c_str()返回空指针用户调用printf(%s, s.c_str())这类操作时可能直接引爆。而分配一个字节的成本极低换来的却是空串对象也能像合法字符串一样使用这个设计习惯很值得养成。还要注意的是赋值重载里我在分配新内存前delete[] m_data。如果你把顺序反过来先 new 新内存再去 delete 旧内存一旦 new 抛异常旧内存还没释放就形成资源泄漏。先释放后分配的这个顺序本身也意味着你必须先做自赋值检查。这两步是配套的缺一不可。我自己在测试这个 String 类时通常会用三种方式来验证直接构造、拷贝构造、连续赋值。特别是在连续赋值场景比如String s1(hello); String s2(world); String s3(!); s1 s2 s3;这个链式赋值会执行s2 s3先把 s3 的数据交给 s2然后s1 (s2 s3 的结果)也就是把 s3 的数据再交给 s1。返回引用让整个过程不需要产生任何临时对象和深拷贝如果你不小心把返回值写成了按值返回这里就会额外多出一次深拷贝开销对象一大性能差距立刻就出来了。3.3 编译器“悄悄”做了什么那些你没写但存在的东西很多新手会困惑我没写拷贝构造函数为什么代码能编译我不写构造函数为什么程序不报错这些都得归功于编译器自动生成的特殊成员函数。具体来说编译器会在以下情况自动生成默认构造函数当你的类没有任何用户自定义构造函数时。同理如果你没有写拷贝构造、赋值重载、析构函数编译器也会自动生成。但这里有个关键规则一旦你自定义了任何一个构造函数编译器就不会再自动生成默认构造函数。这意味着你的类如果需要一个无参构造版本你得自己补上。还有一个特别容易把人绕晕的规则如果你声明了一个带参数的构造函数又没有写拷贝构造函数编译器仍然会生成一个默认的拷贝构造。这个默认拷贝构造是一个浅拷贝。这在最前面已经说过浅拷贝本身不可怕可怕的是它配合析构函数一起出现时。所以业界有一个心法“如果你写了析构函数那几乎一定需要拷贝构造和赋值重载”——反过来如果类没有资源需要释放析构函数都不用写干脆全默认。移动构造和移动赋值是 C11 引入的另一个大话题这里点到为止。简单理解当临时对象要被“转移”而不是“拷贝”时移动构造可以偷取资源而不是重新分配。如果你的类有动态资源建议也把移动构造补上它的形参是右值引用核心操作是转移指针并把源对象的指针置空避免源析构时释放掉被转移的内存。对于追求性能和资源所有权控制的项目这一步比深拷贝优化还要明显。4. 常见问题与排查技巧实录4.1 崩溃与乱码从现象倒推本质我在带人的时候经常看到有人照着书敲代码编译过了运行时崩了第一反应是到处乱改。这里我给一个排查方法从现象倒推源头比盲目试错高效得多。先给一张速查表遇到对应现象直接按图索骥现象大概率原因排查思路程序崩溃报 “double free” 或 “pointer being freed was not allocated”浅拷贝导致同一块内存被释放两次检查是否自定义了拷贝构造和赋值重载是否处理深拷贝对象的成员变量值是随机数或乱码初始化列表顺序错误或成员变量未初始化检查成员变量声明顺序确保所有成员都在初始化列表中编译报错no matching function for call to ...()带了参数的构造函数存在但没有默认构造函数补写一个无参构造函数或者给所有形参加默认值把对象放进容器后行为诡异缺拷贝构造/赋值或应写移动构造却写了不必要的深拷贝检查是否为容器里的元素提供了正确的拷贝语义const 对象调用成员函数报错成员函数没加 const 修饰在只读成员函数末尾加conststatic 成员变量链接错误undefined reference类外缺少定义语句在类外写int ClassName::s_member 0;这张表是我在实际调试中总结出来最常碰到的几类。你项目里如果出现其他怪现象先问自己三个问题这个对象是怎么创建的它在哪儿被拷贝/赋值过它在哪儿析构把生命周期画出来十有八九能找到问题。4.2 我踩过的几个经典坑第一个坑是自赋值检查。我最早写赋值重载时没写if (this ! other)结果就是s s这种看似人畜无害的表达式直接把对象搞崩了。当时我还不信想着谁会写a a这种代码啊。但实际项目里有很多隐式的自赋值比如数组里把元素赋值给自己、容器迭代器指向同一个对象时互相赋值都会触发。谨记自赋值检查不是洁癖是必须。第二个坑是缺默认构造函数导致容器编译失败。比如std::vectorDate v(10);这种代码要求 Date 必须有一个默认构造函数因为 vector 得“无中生有”地构造 10 个元素。如果你的类没有默认构造函数这种写法就直接编译不过。当时我的解决方法很简单给 Date 的构造函数参数全部设默认值。// 老版本 Date(int year, int month, int day); // 新版本 Date(int year 1970, int month 1, int day 1);这样既保留了带参构造的功能又等价于提供了一个默认构造函数兼容性一下就上去了。第三个坑是 this 指针为空的情况。成员函数在通过 this 访问成员变量时编译器生成的代码其实是“去某个偏移量取内存”如果 this 是nullptr你不会立刻崩而是访问到非法地址才崩。所以如果代码里出现“调用了实例方法但成员变量全乱”的情况想一想调用方是否把空指针当成了正常对象来调用。第四个坑是 static 变量定义漏写。声明写在类里定义必须放在类外C17 以后可以用inline static简化。漏写定义编译器报的链接错误往往让人摸不着头脑尤其是undefined reference toClassName::s_member看着像是函数没实现其实是静态成员没定义。遇到这种错先检查类外有没有对应的定义。4.3 设计上的几个小建议代码正确运行只是底线。在正确之上类的设计质量远比你想象的更重要。这里分享几个我自己的习惯。第一尽量不写返回“内部资源”的接口。什么意思就是不要随意暴露get_data()返回裸指针让外部去改。一旦外部拿到了裸指针它就像一把钥匙能绕开你的封装直接改内部状态你的类的所有不变量都无法保证。如果非要返回只读数据返回const char*或const std::string这是常见做法。第二构造函数的参数别搞太多。四五个以上的参数基本属于设计信号说明这个类在承担太多职责。可以考虑用一组配置结构体或者拆成几个类否则调用方要传的参数自己都记不住。这一点在工程评审中经常被拿出来讨论。第三能不改对象状态的函数就加const。这不仅仅是为了编译器允许 const 对象调用更深层的原因是你是在向阅读代码的人传达“这个函数不会改变对象状态”的设计意图。加const的成本几乎为零收益却很高。第四多用“组合”而非“继承”。继承是整个面向对象里最容易被滥用的机制。我在类与对象收尾阶段会反复强调一句话优先考虑让一个类持有另一个类的对象而不是继承它。继承意味着强耦合、子类和父类的生死绑定组合则更灵活可以在运行时替换或复用。如果你不是确定“子类就是父类的一种”就不要用继承。4.4 一份自查清单最后我整理了一份写完一个类之后的自查清单。不用全做完但如果你的类要用于实际项目建议逐条过一遍类里有动态资源吗有的话析构、拷贝构造、赋值重载三件套齐全了吗初始化列表覆盖了所有成员吗有没有依赖未初始化成员的写法所有只读成员函数加const了吗需要默认构造吗如果编译器不自动生成我补了吗静态成员在类外定义了吗运算符重载的返回值是否遵循了“前置返回引用、后置返回旧值”的规则有没有把本可以设为 private 的东西暴露在 public 里4.5 对照测试怎么验证你的类没有问题写完一个类测试怎么测初学者最容易犯的错是“只测 main 函数里能跑通”。但类和对象的很多问题不会在单次顺序执行中暴露而是藏在对象拷贝、赋值、容器交互这些边界场景里。我常用的测试脚本很小但包含的用例很关键int main() { // 1. 正常构造 Date d1(2024, 1, 1); // 2. 拷贝构造 Date d2(d1); assert(d2.to_integer() d1.to_integer()); // 3. 赋值重载 Date d3(1999, 12, 31); d3 d2; assert(d3.to_integer() d2.to_integer()); // 4. 自赋值 d3 d3; // 触发自赋值检查 // 5. 连续赋值 Date d4(2000, 1, 1); d3 d4 d1; // 6. 前置和后置 Date d5(2024, 2, 28); d5; assert(d5.to_integer() 20240229); d5; assert(d5.to_integer() 20240301); // 7. 容器拷贝 std::vectorDate vec; vec.push_back(d1); vec.push_back(d5); // 触发多次拷贝检验正确性 std::cout 全部通过 std::endl; return 0; }对 String 类类似的测试要覆盖空字符串、长字符串、拷贝后修改原对象副本数据互不影响。用断言把每个关键行为钉死就是最简单的单元测试——不需要额外框架只是为了让你在修改代码后有个“跑一遍就知道有没有坏”的安全网。这套习惯一旦养成后面写再复杂的类也不会心里没底。5. 收尾前再塞两个小技巧技巧一编译时用-Wall -Wextra它能在很多问题爆发前就拉响警报。比如成员初始化顺序和声明顺序不一致、非虚析构函数、类型转换潜在风险都会在编译时告诉你。我见过太多“运行时 bug”其实编译期加两个选项就能提前发现。对 C 来说编译器是第一个也是最重要的审查员别关了它的眼睛。技巧二自己写完类以后用std::is_copy_constructible和std::is_copy_assignable这类类型特征去断言一下类的可拷贝性。这在模板代码里很常用但即使不用模板这种明确的静态断言也能防止未来有人不小心把拷贝构造删掉或搞坏。比如你写了一个理论上有动态资源、需要深拷贝的类你可以在类尾部加上static_assert(std::is_copy_constructibleMyClass::value, 这个类必须支持拷贝构造);这样一旦代码后续修改导致深拷贝功能丢失编译期立即报错而不是上线后崩溃。回到标题上说的“收官之战”。类与对象走到这一步你手里的工具已经不少了构造与析构、拷贝与赋值、static、友元、运算符重载。它们每一个单独拿出来都不难理解但它们组合在一起时代码的复杂度和微妙程度会指数上升。这个阶段最忌讳的就是“看起来懂了”。我见过太多人在考试里能默写出概念一动手写代码就崩了。区别不在于智商而在于有没有做足够多的“写出完整类”的练习有没有在调试中真正体会过浅拷贝的代价。如果你正好卡在这里我建议你做的事很简单把文中的 Date 类和 String 类亲手写一遍然后把测试用例跑起来再去修改其中几个实现比如把深拷贝注释掉、把赋值顺序调换一下观察会发生什么。这种主动制造错误的方式往往比反复看文档更容易建立直觉。等你真的把那些坑跳了一遍你才敢说自己真正学完了类和对象这一章。
RELATED

相关推荐

Android OpenGL ES进阶:自建EGL渲染线程替代GLSurfaceView

Android OpenGL ES进阶:自建EGL渲染线程替代GLSurfaceView

在 Android 上用 OpenGL ES 渲染图像,很多人第一反应就是GLSurfaceView。我刚开始接触图形开发时也一样,new 一个MyRenderer,在onDrawFrame里画个三角形,清屏、画图,一套流程非常顺手。但后来做自定义滤镜、离屏截图、…

📅 2026/10/21 6:51:02
2025届AI学术平台实测:哪些论文环节真正值得用?

2025届AI学术平台实测:哪些论文环节真正值得用?

1. 先用一句话回答:2025届AI学术平台的“实际效果”落在哪个环节最近后台留言里,出现频率最高的一句话是:2025届是不是真的离了AI学术平台就毕不了业?我连续两周,拿同一个课题去测了市面上一批AI学术工具,从…

📅 2026/10/21 6:51:02
真空烘箱精密控温:防氧化与数据追溯全解析

真空烘箱精密控温:防氧化与数据追溯全解析

摘要:新一代工业级真空烘箱如何实现精密控温?本文围绕三大机制展开:负压辐射加热实现深层脱水与防氧化、干式螺杆泵与闭环保压杜绝油污返吸、PLC 可编程控温与数据导出构建完整数据追溯链,并剖析其适用边界,助你科学选…

📅 2026/10/21 6:51:02
MORE NEWS

更多资讯

📰

专科生论文冲刺:千笔与Checkjie实测对比与组合使用指南

说实话,每年到这个节点,我后台私信里堆得最多的就是论文求助。尤其专科阶段,实习、备考、毕业设计全挤在一起,真正留给论文的时间可能就一两周。2026年这一批同学压力更大,很多学校把论文提交时间前移了,查…

📰

Formbook多波次钓鱼攻击的技术演化与纵深防御实践

Formbook这个样本家族,在钓鱼邮件攻击里算是老面孔了,但从我近几年处置的几起事件来看,它从来没打算退出舞台,反而一直在悄悄换打法。过去我们对付它的思路,基本是抓样本、提哈希、封域名,一套组合拳打完就…

📰

ASM磁盘组不完整导致spfile创建失败:ORA-15040排查与恢复

1. 故障现场:一条spfile命令引发的连环报错做DBA这行,几乎每个人早晚都会撞上“只是想建一个spfile,结果数据库却连环报错”的场景。上周在某个生产环境的备库切换演练里,我执行一条CREATE SPFILE命令,控制台连续弹出了…

📰

STM32智能小车实战:PWM调速、循迹、避障、跟随与红外遥控设计

简介:这是一份基于STM32F103ZET6单片机的智能小车系统设计论文,系统讲解PWM调速、红外循迹、红外避障、超声波避障、障碍物跟随以及红外遥控等功能的实现方法,适合单片机、嵌入式方向的在校学生,以及需要完成课程设计或毕业设计的…

📰

波形和文稿对不齐,播客剪辑台怎样让字幕跟着声音走

编辑 EP.028 那期《城市夜航》的时候,我把播放头拖到 08:24,“智能文稿"里显示的是"我们开始意识到,声音不只是内容的载体……”,可波形上对应的却是两秒前嘉宾的尾音。字幕和声音之间始终差着一小段,而且越…

📰

C#火车订票系统:WinForm+SQL Server全链路实战指南

简介:这是一套基于C#与SQL Server开发的完整火车订票系统实战项目资源,面向C#初学者及.NET桌面应用入门学习者,聚焦GUI开发、数据库交互与多模块业务逻辑实现。资源包含128个文件,主体为50个C#源码文件(.cs&#xff09…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬