文章目录引言一、C 的手动挡init 和 destroy 的痛苦1.1 一个典型的 C 模式1.2 真实项目中更糟二、构造函数对象从诞生那一刻就是合法的2.1 第一个构造函数2.2 构造函数重载2.3 成员初始化列表比构造函数体更高效三、析构函数自动打扫战场3.1 基本语法3.2 析构的调用时机3.3 析构函数的保证即使出错也会运行四、编译器默认生成的构造函数和析构函数五、完整演进从 C 到 C 的生命周期管理阶段1C —— 处处手动阶段2C —— 构造和析构接管一切六、常见陷阱6.1 虚析构函数预告6.2 不要在析构函数中抛出异常6.3 构造函数中不要调用 virtual 函数总结本系列为《C深度修炼基础、STL源码与多线程实战》第3篇前置条件理解 C 语言手动 init/destroy 模式读过第2篇了解 class 与封装引言在 C 语言中创建一个结构体对象需要两步先声明变量再手动调用init函数。销毁时必须记得调用对应的destroy或free。忘掉任何一步就是 bug——未初始化的字段包含垃圾值未释放的内存就是泄漏。C 的构造函数和析构函数把这个模式变成了自动挡对象诞生时编译器自动调用构造函数来初始化对象消亡时编译器自动调用析构函数来打扫战场。你不需要记得——编译器替你记。这不是语法糖而是 C 整个资源管理体系RAII的基石。一、C 的手动挡init 和 destroy 的痛苦1.1 一个典型的 C 模式// file_buffer.c — C风格手动管理#includestdio.h#includestdlib.h#includestring.hstructFileBuffer{char*data;size_t size;FILE*file;};// 初始化函数——必须手动调用voidfile_buffer_init(structFileBuffer*fb,constchar*path){fb-filefopen(path,r);if(!fb-file){fb-dataNULL;fb-size0;return;}// 获取文件大小fseek(fb-file,0,SEEK_END);fb-sizeftell(fb-file);rewind(fb-file);// 分配内存fb-datamalloc(fb-size);if(fb-data){fread(fb-data,1,fb-size,fb-file);}}// 清理函数——必须手动调用voidfile_buffer_destroy(structFileBuffer*fb){free(fb-data);fb-dataNULL;// 避免悬空指针if(fb-file)fclose(fb-file);}intmain(){structFileBufferfb;// 问题1如果忘记 initfb.data 是垃圾值file_buffer_init(fb,data.txt);// 中间可能有多个 return、goto或者提前出错退出if(fb.size0){return1;// 问题2提前 return忘记 destroy内存泄漏}printf(Read %zu bytes\n,fb.size);file_buffer_destroy(fb);// 问题3必须记得调用return0;}这段代码有三个典型风险点风险场景后果未初始化声明后、init 前使用对象垃圾数据、段错误未清理提前 return / goto / 异常内存泄漏、文件句柄泄漏重复清理多次调用 destroy双重 free、未定义行为1.2 真实项目中更糟intprocess_files(constchar**paths,intcount){structFileBuffer*buffersmalloc(count*sizeof(structFileBuffer));for(inti0;icount;i){file_buffer_init(buffers[i],paths[i]);if(!buffers[i].data){// 出错了需要清理前 i 个已经初始化的对象for(intj0;ji;j)file_buffer_destroy(buffers[j]);free(buffers);return-1;}}// ... 处理数据 ...// 正常清理for(inti0;icount;i)file_buffer_destroy(buffers[i]);free(buffers);return0;}每增加一个错误路径就要多写一段清理代码。C 程序里清理代码的体量常常超过业务逻辑。二、构造函数对象从诞生那一刻就是合法的2.1 第一个构造函数// file_buffer_v1.cpp#includecstdio#includecstdlib#includecstringclassFileBuffer{public:// 构造函数名字与类名相同没有返回类型FileBuffer(constchar*path){file_fopen(path,r);if(!file_){data_nullptr;size_0;return;}fseek(file_,0,SEEK_END);size_ftell(file_);rewind(file_);data_malloc(size_);if(data_){fread(data_,1,size_,file_);}}// 普通成员函数size_tsize()const{returnsize_;}constchar*data()const{returnstatic_castconstchar*(data_);}private:void*data_;size_t size_;FILE*file_;};intmain(){FileBufferfb(data.txt);// 构造自动调用无需手动 initprintf(Read %zu bytes\n,fb.size());// 即使提前 return析构函数也会自动清理见下一节return0;}关键变化就一个地方FileBuffer fb(data.txt)声明的同时构造函数自动执行了。你不再需要也不能手动调用init。编译器保证只要对象存在构造函数就一定已经执行过。2.2 构造函数重载和普通函数一样构造函数可以重载——用不同的参数组合初始化对象#includeiostreamclassPoint{public:Point():x_(0),y_(0){// 默认构造std::coutPoint()\n;}Point(intx,inty):x_(x),y_(y){// 带参数构造std::coutPoint(x, y)\n;}Point(intv):x_(v),y_(v){// 单参数构造std::coutPoint(v)\n;}private:intx_,y_;};intmain(){Point a;// Point()Pointb(3,5);// Point(3, 5)Pointc(7);// Point(7)Point d7;// 同样调用 Point(7) —— 隐式转换}⚠️Point d 7这行会调用Point(7)因为单参数构造函数默认允许隐式类型转换。大多数时候你不希望这样——加explicit可以禁止explicitPoint(intv):x_(v),y_(v){}// Point d 7; // ❌ 现在不允许了// Point d(7); // ✅ 显式调用仍然可以2.3 成员初始化列表比构造函数体更高效构造函数体{}内部的是赋值不是初始化。真正的初始化发生在进入构造函数体之前。对于非基本类型成员这意味着先默认初始化再赋新值——多了一次操作#includestringclassUser{public:// 不好的写法先默认构造 name_空 string再在函数体内赋值User(conststd::stringname,intage){name_name;// 赋值不是初始化age_age;// 对 int 来说没区别}// 好的写法初始化列表直接调用 string 的拷贝构造函数User(conststd::stringname,intage):name_(name)// 初始化不是赋值,age_(age)// 对 int 来说等价但风格统一{}private:std::string name_;intage_;};核心规则初始化列表的执行顺序只看成员声明顺序与初始化列表中的书写顺序无关。为了避免混淆始终按声明顺序书写初始化列表。classWidget{public:Widget(intv):b_(v),a_(b_1){}// 危险a_ 先用 b_ 初始化但 b_ 还没初始化// 实际执行顺序先 a_(b_ 1)b_ 是垃圾值后 b_(v)// 因为 a_ 声明在 b_ 前面private:inta_;// 声明在前intb_;// 声明在后};三、析构函数自动打扫战场3.1 基本语法析构函数的名字是~类名()没有参数没有返回值一个类只有一个析构函数classFileBuffer{public:FileBuffer(constchar*path){// ... 构造逻辑 ...}~FileBuffer(){// 析构函数free(data_);if(file_)fclose(file_);}private:void*data_;size_t size_;FILE*file_;};析构函数在对象生命周期结束时自动被调用voidprocess(){FileBufferfb(data.txt);// 构造// ... 使用 fb ...if(some_error()){return;// 提前退出——fb 的析构自动运行}// 正常结束时——fb 的析构也会自动运行}// -- 无论从哪个出口离开析构都会执行3.2 析构的调用时机#includeiostreamstructTracer{constchar*name_;Tracer(constchar*name):name_(name){std::coutname_ 诞生\n;}~Tracer(){std::coutname_ 消亡\n;}};Tracerglobal(全局对象);intmain(){std::cout进入 main\n;Tracerlocal(局部对象);{Tracerblock(块作用域对象);std::cout离开块作用域\n;}// block 在此析构std::cout离开 main\n;return0;}// local 在此析构global 在程序退出时析构$ g -stdc17 tracer.cpp ./a.out 全局对象 诞生 进入 main 局部对象 诞生 块作用域对象 诞生 离开块作用域 块作用域对象 消亡 离开 main 局部对象 消亡 全局对象 消亡析构顺序 构造顺序的逆序。局部对象先于全局对象析构栈上的后构造先析构。3.3 析构函数的保证即使出错也会运行这是析构函数最核心的价值。看一个对比// C 版本intprocess(){FILE*ffopen(data.txt,r);if(!f)return-1;char*bufmalloc(1024);if(!buf){fclose(f);// 手动清理 freturn-1;}intresultread_and_process(f,buf);// 如果 read_and_process 内部有什么 goto 或者 longjmp……// 下面的清理根本执行不到free(buf);fclose(f);returnresult;}// C 版本intprocess(){FileBufferfb(data.txt);// 构造函数打开文件、分配内存if(fb.size()0)return-1;// 提前 returnfb 的析构自动运行returnprocess_contents(fb);// 无论发生什么fb 的析构都会运行} 这就是RAIIResource Acquisition Is Initialization的雏形在构造函数中获取资源在析构函数中释放资源。编译器保证析构一定执行从而保证资源一定释放。完整论述见第12篇。四、编译器默认生成的构造函数和析构函数如果你不写任何构造函数编译器会生成以下默认成员默认生成行为默认构造函数T()调用每个成员的默认构造函数基本类型不初始化析构函数~T()调用每个成员的析构函数拷贝构造函数T(const T)逐成员拷贝拷贝赋值T operator(const T)逐成员赋值structData{intx;// 基本类型默认构造函数不初始化它doubley;// 同上std::string s;// 类类型默认构造函数调用 string()};intmain(){Data d;// 调用编译器生成的默认构造函数// d.x 和 d.y 是未定义的垃圾值// d.s 是空 stringstring 的默认构造函数保证的}⚠️关键规则一旦你手动定义了任何构造函数编译器就不再生成默认构造函数。如果你还需要默认构造行为必须显式加上 default;classFoo{public:Foo()default;// 显式要求编译器生成Foo(intx):x_(x){}// 自定义构造函数private:intx_0;};五、完整演进从 C 到 C 的生命周期管理以一个简单的动态字符串为例展示 C 到 C 的完整演进阶段1C —— 处处手动// string_c.c#includestdlib.h#includestring.h#includestdio.hstructDynString{char*data;size_t len;};voidds_init(structDynString*s,constchar*init){s-lenstrlen(init);s-datamalloc(s-len1);if(s-data)strcpy(s-data,init);}voidds_destroy(structDynString*s){free(s-data);s-dataNULL;s-len0;}// 使用每个函数调用点都要保证配对voiddo_stuff(){structDynStrings;ds_init(s,hello);// ... 如果这里有 return/error就漏了 ds_destroyds_destroy(s);}阶段2C —— 构造和析构接管一切// string_cpp.cpp#includecstring#includecstdlib#includeiostreamclassDynString{public:DynString(constchar*init){len_strlen(init);data_static_castchar*(malloc(len_1));if(data_)strcpy(data_,init);std::cout构造: data_\n;}~DynString(){std::cout析构: (data_?data_:(null))\n;free(data_);}constchar*c_str()const{returndata_;}private:char*data_;size_t len_;};voiddo_stuff(){DynStrings(hello);// 构造自动调用printf(%s\n,s.c_str());if(s.c_str()[0]h){return;// 提前退出——析构自动运行 ✅}// 析构在这里也会自动运行}// 输出// 构造: hello// hello// 析构: hello六、常见陷阱6.1 虚析构函数预告如果类会被继承析构函数通常应该声明为virtual。这不是本篇的重点详见第36篇但先留下印象classBase{public:~Base(){}};// 非虚析构——危险classDerived:publicBase{/* ... */};Base*pnewDerived();deletep;// 只调用了 Base::~Base()Derived 的部分泄漏了// 解法Base 应该写 virtual ~Base() default;6.2 不要在析构函数中抛出异常析构函数如果抛出异常且未被捕获std::terminate会被调用——程序直接终止。C11 起析构函数默认是noexcept的。6.3 构造函数中不要调用 virtual 函数在构造函数执行期间对象还没有完全变成派生类virtual 函数的调度是基类版本不是派生类版本。这是第37篇的主题。总结构造与析构是 C 从 C 继承来的语法骨架上最重要的新人构造函数保证对象从诞生那一刻起就是合法可用的——不会有忘记 init的漏洞析构函数保证对象离开作用域时资源一定被回收——无论从哪个出口离开成员初始化列表是真正的初始化构造函数体内部是赋值——对非基本类型有性能影响这三个保证合在一起构成了 C 最核心的编程范式——RAII资源获取即初始化。有了它我们才能写出不需要手动close、不需要手动free、不需要在每一个错误路径上复制清理代码的程序。下一篇文章我们将深入this 指针和成员函数——理解成员函数在底层到底是怎么被调用的以及p.move(x, y)和point_move(p, x, y)本质上是同一件事。动手练习把第2篇练习中的Thermometer类加上构造函数接受摄氏度/华氏度/开尔文三种参数并转换为开尔文存储加上析构函数打印日志写一个Tracer类追踪对象生命周期在多个作用域中创建它验证析构顺序是否是构造逆序故意在一个类的构造函数中申请堆内存、在析构中释放然后在main中用return提前退出确认析构自动运行
构造与析构:对象生命周期的“自动挡“
发布时间:2026/5/16 0:59:42
文章目录引言一、C 的手动挡init 和 destroy 的痛苦1.1 一个典型的 C 模式1.2 真实项目中更糟二、构造函数对象从诞生那一刻就是合法的2.1 第一个构造函数2.2 构造函数重载2.3 成员初始化列表比构造函数体更高效三、析构函数自动打扫战场3.1 基本语法3.2 析构的调用时机3.3 析构函数的保证即使出错也会运行四、编译器默认生成的构造函数和析构函数五、完整演进从 C 到 C 的生命周期管理阶段1C —— 处处手动阶段2C —— 构造和析构接管一切六、常见陷阱6.1 虚析构函数预告6.2 不要在析构函数中抛出异常6.3 构造函数中不要调用 virtual 函数总结本系列为《C深度修炼基础、STL源码与多线程实战》第3篇前置条件理解 C 语言手动 init/destroy 模式读过第2篇了解 class 与封装引言在 C 语言中创建一个结构体对象需要两步先声明变量再手动调用init函数。销毁时必须记得调用对应的destroy或free。忘掉任何一步就是 bug——未初始化的字段包含垃圾值未释放的内存就是泄漏。C 的构造函数和析构函数把这个模式变成了自动挡对象诞生时编译器自动调用构造函数来初始化对象消亡时编译器自动调用析构函数来打扫战场。你不需要记得——编译器替你记。这不是语法糖而是 C 整个资源管理体系RAII的基石。一、C 的手动挡init 和 destroy 的痛苦1.1 一个典型的 C 模式// file_buffer.c — C风格手动管理#includestdio.h#includestdlib.h#includestring.hstructFileBuffer{char*data;size_t size;FILE*file;};// 初始化函数——必须手动调用voidfile_buffer_init(structFileBuffer*fb,constchar*path){fb-filefopen(path,r);if(!fb-file){fb-dataNULL;fb-size0;return;}// 获取文件大小fseek(fb-file,0,SEEK_END);fb-sizeftell(fb-file);rewind(fb-file);// 分配内存fb-datamalloc(fb-size);if(fb-data){fread(fb-data,1,fb-size,fb-file);}}// 清理函数——必须手动调用voidfile_buffer_destroy(structFileBuffer*fb){free(fb-data);fb-dataNULL;// 避免悬空指针if(fb-file)fclose(fb-file);}intmain(){structFileBufferfb;// 问题1如果忘记 initfb.data 是垃圾值file_buffer_init(fb,data.txt);// 中间可能有多个 return、goto或者提前出错退出if(fb.size0){return1;// 问题2提前 return忘记 destroy内存泄漏}printf(Read %zu bytes\n,fb.size);file_buffer_destroy(fb);// 问题3必须记得调用return0;}这段代码有三个典型风险点风险场景后果未初始化声明后、init 前使用对象垃圾数据、段错误未清理提前 return / goto / 异常内存泄漏、文件句柄泄漏重复清理多次调用 destroy双重 free、未定义行为1.2 真实项目中更糟intprocess_files(constchar**paths,intcount){structFileBuffer*buffersmalloc(count*sizeof(structFileBuffer));for(inti0;icount;i){file_buffer_init(buffers[i],paths[i]);if(!buffers[i].data){// 出错了需要清理前 i 个已经初始化的对象for(intj0;ji;j)file_buffer_destroy(buffers[j]);free(buffers);return-1;}}// ... 处理数据 ...// 正常清理for(inti0;icount;i)file_buffer_destroy(buffers[i]);free(buffers);return0;}每增加一个错误路径就要多写一段清理代码。C 程序里清理代码的体量常常超过业务逻辑。二、构造函数对象从诞生那一刻就是合法的2.1 第一个构造函数// file_buffer_v1.cpp#includecstdio#includecstdlib#includecstringclassFileBuffer{public:// 构造函数名字与类名相同没有返回类型FileBuffer(constchar*path){file_fopen(path,r);if(!file_){data_nullptr;size_0;return;}fseek(file_,0,SEEK_END);size_ftell(file_);rewind(file_);data_malloc(size_);if(data_){fread(data_,1,size_,file_);}}// 普通成员函数size_tsize()const{returnsize_;}constchar*data()const{returnstatic_castconstchar*(data_);}private:void*data_;size_t size_;FILE*file_;};intmain(){FileBufferfb(data.txt);// 构造自动调用无需手动 initprintf(Read %zu bytes\n,fb.size());// 即使提前 return析构函数也会自动清理见下一节return0;}关键变化就一个地方FileBuffer fb(data.txt)声明的同时构造函数自动执行了。你不再需要也不能手动调用init。编译器保证只要对象存在构造函数就一定已经执行过。2.2 构造函数重载和普通函数一样构造函数可以重载——用不同的参数组合初始化对象#includeiostreamclassPoint{public:Point():x_(0),y_(0){// 默认构造std::coutPoint()\n;}Point(intx,inty):x_(x),y_(y){// 带参数构造std::coutPoint(x, y)\n;}Point(intv):x_(v),y_(v){// 单参数构造std::coutPoint(v)\n;}private:intx_,y_;};intmain(){Point a;// Point()Pointb(3,5);// Point(3, 5)Pointc(7);// Point(7)Point d7;// 同样调用 Point(7) —— 隐式转换}⚠️Point d 7这行会调用Point(7)因为单参数构造函数默认允许隐式类型转换。大多数时候你不希望这样——加explicit可以禁止explicitPoint(intv):x_(v),y_(v){}// Point d 7; // ❌ 现在不允许了// Point d(7); // ✅ 显式调用仍然可以2.3 成员初始化列表比构造函数体更高效构造函数体{}内部的是赋值不是初始化。真正的初始化发生在进入构造函数体之前。对于非基本类型成员这意味着先默认初始化再赋新值——多了一次操作#includestringclassUser{public:// 不好的写法先默认构造 name_空 string再在函数体内赋值User(conststd::stringname,intage){name_name;// 赋值不是初始化age_age;// 对 int 来说没区别}// 好的写法初始化列表直接调用 string 的拷贝构造函数User(conststd::stringname,intage):name_(name)// 初始化不是赋值,age_(age)// 对 int 来说等价但风格统一{}private:std::string name_;intage_;};核心规则初始化列表的执行顺序只看成员声明顺序与初始化列表中的书写顺序无关。为了避免混淆始终按声明顺序书写初始化列表。classWidget{public:Widget(intv):b_(v),a_(b_1){}// 危险a_ 先用 b_ 初始化但 b_ 还没初始化// 实际执行顺序先 a_(b_ 1)b_ 是垃圾值后 b_(v)// 因为 a_ 声明在 b_ 前面private:inta_;// 声明在前intb_;// 声明在后};三、析构函数自动打扫战场3.1 基本语法析构函数的名字是~类名()没有参数没有返回值一个类只有一个析构函数classFileBuffer{public:FileBuffer(constchar*path){// ... 构造逻辑 ...}~FileBuffer(){// 析构函数free(data_);if(file_)fclose(file_);}private:void*data_;size_t size_;FILE*file_;};析构函数在对象生命周期结束时自动被调用voidprocess(){FileBufferfb(data.txt);// 构造// ... 使用 fb ...if(some_error()){return;// 提前退出——fb 的析构自动运行}// 正常结束时——fb 的析构也会自动运行}// -- 无论从哪个出口离开析构都会执行3.2 析构的调用时机#includeiostreamstructTracer{constchar*name_;Tracer(constchar*name):name_(name){std::coutname_ 诞生\n;}~Tracer(){std::coutname_ 消亡\n;}};Tracerglobal(全局对象);intmain(){std::cout进入 main\n;Tracerlocal(局部对象);{Tracerblock(块作用域对象);std::cout离开块作用域\n;}// block 在此析构std::cout离开 main\n;return0;}// local 在此析构global 在程序退出时析构$ g -stdc17 tracer.cpp ./a.out 全局对象 诞生 进入 main 局部对象 诞生 块作用域对象 诞生 离开块作用域 块作用域对象 消亡 离开 main 局部对象 消亡 全局对象 消亡析构顺序 构造顺序的逆序。局部对象先于全局对象析构栈上的后构造先析构。3.3 析构函数的保证即使出错也会运行这是析构函数最核心的价值。看一个对比// C 版本intprocess(){FILE*ffopen(data.txt,r);if(!f)return-1;char*bufmalloc(1024);if(!buf){fclose(f);// 手动清理 freturn-1;}intresultread_and_process(f,buf);// 如果 read_and_process 内部有什么 goto 或者 longjmp……// 下面的清理根本执行不到free(buf);fclose(f);returnresult;}// C 版本intprocess(){FileBufferfb(data.txt);// 构造函数打开文件、分配内存if(fb.size()0)return-1;// 提前 returnfb 的析构自动运行returnprocess_contents(fb);// 无论发生什么fb 的析构都会运行} 这就是RAIIResource Acquisition Is Initialization的雏形在构造函数中获取资源在析构函数中释放资源。编译器保证析构一定执行从而保证资源一定释放。完整论述见第12篇。四、编译器默认生成的构造函数和析构函数如果你不写任何构造函数编译器会生成以下默认成员默认生成行为默认构造函数T()调用每个成员的默认构造函数基本类型不初始化析构函数~T()调用每个成员的析构函数拷贝构造函数T(const T)逐成员拷贝拷贝赋值T operator(const T)逐成员赋值structData{intx;// 基本类型默认构造函数不初始化它doubley;// 同上std::string s;// 类类型默认构造函数调用 string()};intmain(){Data d;// 调用编译器生成的默认构造函数// d.x 和 d.y 是未定义的垃圾值// d.s 是空 stringstring 的默认构造函数保证的}⚠️关键规则一旦你手动定义了任何构造函数编译器就不再生成默认构造函数。如果你还需要默认构造行为必须显式加上 default;classFoo{public:Foo()default;// 显式要求编译器生成Foo(intx):x_(x){}// 自定义构造函数private:intx_0;};五、完整演进从 C 到 C 的生命周期管理以一个简单的动态字符串为例展示 C 到 C 的完整演进阶段1C —— 处处手动// string_c.c#includestdlib.h#includestring.h#includestdio.hstructDynString{char*data;size_t len;};voidds_init(structDynString*s,constchar*init){s-lenstrlen(init);s-datamalloc(s-len1);if(s-data)strcpy(s-data,init);}voidds_destroy(structDynString*s){free(s-data);s-dataNULL;s-len0;}// 使用每个函数调用点都要保证配对voiddo_stuff(){structDynStrings;ds_init(s,hello);// ... 如果这里有 return/error就漏了 ds_destroyds_destroy(s);}阶段2C —— 构造和析构接管一切// string_cpp.cpp#includecstring#includecstdlib#includeiostreamclassDynString{public:DynString(constchar*init){len_strlen(init);data_static_castchar*(malloc(len_1));if(data_)strcpy(data_,init);std::cout构造: data_\n;}~DynString(){std::cout析构: (data_?data_:(null))\n;free(data_);}constchar*c_str()const{returndata_;}private:char*data_;size_t len_;};voiddo_stuff(){DynStrings(hello);// 构造自动调用printf(%s\n,s.c_str());if(s.c_str()[0]h){return;// 提前退出——析构自动运行 ✅}// 析构在这里也会自动运行}// 输出// 构造: hello// hello// 析构: hello六、常见陷阱6.1 虚析构函数预告如果类会被继承析构函数通常应该声明为virtual。这不是本篇的重点详见第36篇但先留下印象classBase{public:~Base(){}};// 非虚析构——危险classDerived:publicBase{/* ... */};Base*pnewDerived();deletep;// 只调用了 Base::~Base()Derived 的部分泄漏了// 解法Base 应该写 virtual ~Base() default;6.2 不要在析构函数中抛出异常析构函数如果抛出异常且未被捕获std::terminate会被调用——程序直接终止。C11 起析构函数默认是noexcept的。6.3 构造函数中不要调用 virtual 函数在构造函数执行期间对象还没有完全变成派生类virtual 函数的调度是基类版本不是派生类版本。这是第37篇的主题。总结构造与析构是 C 从 C 继承来的语法骨架上最重要的新人构造函数保证对象从诞生那一刻起就是合法可用的——不会有忘记 init的漏洞析构函数保证对象离开作用域时资源一定被回收——无论从哪个出口离开成员初始化列表是真正的初始化构造函数体内部是赋值——对非基本类型有性能影响这三个保证合在一起构成了 C 最核心的编程范式——RAII资源获取即初始化。有了它我们才能写出不需要手动close、不需要手动free、不需要在每一个错误路径上复制清理代码的程序。下一篇文章我们将深入this 指针和成员函数——理解成员函数在底层到底是怎么被调用的以及p.move(x, y)和point_move(p, x, y)本质上是同一件事。动手练习把第2篇练习中的Thermometer类加上构造函数接受摄氏度/华氏度/开尔文三种参数并转换为开尔文存储加上析构函数打印日志写一个Tracer类追踪对象生命周期在多个作用域中创建它验证析构顺序是否是构造逆序故意在一个类的构造函数中申请堆内存、在析构中释放然后在main中用return提前退出确认析构自动运行