Effective C 条款37绝不重新定义继承而来的缺省参数值本篇为《Effective C改善程序与设计的 55 个具体做法》读书笔记系列第 37 篇。开篇引言在 C 中virtual函数支持动态绑定运行时多态而函数的缺省参数值却是静态绑定的。这种不一致性导致了一个极其隐蔽的陷阱当你通过基类指针调用派生类的virtual函数时使用的缺省参数值可能来自基类而非派生类。Scott Meyers 在条款 37 中警告我们绝不重新定义继承而来的缺省参数值。本文将深入剖析这一陷阱的本质并提供安全的替代方案。核心问题一个令人困惑的示例让我们从一个直观的例子开始#includeiostreamclassShape{public:enumShapeColor{Red,Green,Blue};// virtual 函数带有缺省参数virtualvoiddraw(ShapeColor colorRed)const{std::coutShape::draw with color colorstd::endl;}virtual~Shape()default;};classRectangle:publicShape{public:// 危险重新定义了继承而来的缺省参数值virtualvoiddraw(ShapeColor colorGreen)constoverride{std::coutRectangle::draw with color colorstd::endl;}};classCircle:publicShape{public:// 没有指定缺省参数virtualvoiddraw(ShapeColor color)constoverride{std::coutCircle::draw with color colorstd::endl;}};intmain(){Shape*psnewShape();Shape*prnewRectangle();Shape*pcnewCircle();ps-draw();// Shape::draw with color 0 (Red)pr-draw();// Rectangle::draw with color 0 (Red) —— 注意不是 Green// pc-draw(); // 编译错误Circle::draw 没有缺省参数deleteps;deletepr;deletepc;return0;}令人震惊的结果调用语句实际调用的函数实际使用的缺省参数预期参数ps-draw()Shape::drawRed(0)Redpr-draw()Rectangle::drawRed(0)Green通过Shape*指针调用Rectangle::draw()时函数体是Rectangle的但缺省参数却是Shape的这就是静态绑定与动态绑定的分裂行为。原理深度解析静态类型 vs 动态类型要理解这个问题我们需要明确两个核心概念1. 静态类型Static Type静态类型是变量在声明时的类型在编译期就已确定Shape*prnewRectangle();// pr 的静态类型是 Shape*Rectangle*pr2newRectangle();// pr2 的静态类型是 Rectangle*2. 动态类型Dynamic Type动态类型是变量实际指向的对象的类型在运行期才能确定Shape*prnewRectangle();// pr 的动态类型是 Rectangle*prnewCircle();// pr 的动态类型变为 Circle*绑定机制的分裂特性绑定方式决定因素virtual函数的调用动态绑定对象的动态类型缺省参数值静态绑定指针/引用的静态类型Shape*prnewRectangle();pr-draw();// 等价于// 1. 调用哪个 draw动态绑定 → Rectangle::draw// 2. 缺省参数是什么静态绑定 → Shape::draw 的缺省参数 Red为什么 C 这样设计你可能会问为什么 C 不让缺省参数也动态绑定呢答案是运行期效率。如果缺省参数是动态绑定的编译器必须在运行期为每次virtual函数调用决定适当的缺省参数值。这需要在虚函数表中额外存储缺省参数信息每次调用时进行额外的查找和解析增加编译器的复杂度和运行期开销C 的设计哲学倾向于零开销抽象zero-overhead abstraction。为了程序的执行速度和编译器实现的简易度C 选择了在编译期决定缺省参数值。// 编译器实际生成的代码概念上// pr-draw() 被编译为类似// pr-vptr[draw_index](pr, Shape::draw_default_color); // 缺省参数在编译期硬编码代码示例更复杂的场景场景 1多层继承体系#includeiostreamclassBase{public:virtualvoidfunc(intx10)const{std::coutBase::func(x)std::endl;}virtual~Base()default;};classMiddle:publicBase{public:virtualvoidfunc(intx20)constoverride{// 危险std::coutMiddle::func(x)std::endl;}};classDerived:publicMiddle{public:virtualvoidfunc(intx30)constoverride{// 更危险std::coutDerived::func(x)std::endl;}};voidtest(){Base*pbnewDerived();Middle*pmnewDerived();Derived*pdnewDerived();pb-func();// Derived::func(10) —— Base 的缺省值pm-func();// Derived::func(20) —— Middle 的缺省值pd-func();// Derived::func(30) —— Derived 的缺省值deletepb;deletepm;deletepd;}场景 2引用同样受影响voiddrawShape(constShapeshape){shape.draw();// 同样的问题}Rectangle rect;drawShape(rect);// 调用 Rectangle::draw但使用 Shape::Red 作为缺省值解决方案NVI 设计模式当你确实需要为virtual函数提供缺省参数时NVINon-Virtual Interface设计模式是最优雅的解决方案。NVI 模式的核心思想将virtual函数设为private通过一个public的non-virtual函数来调用它。non-virtual函数负责提供缺省参数virtual函数负责实际工作。#includeiostreamclassShape{public:enumShapeColor{Red,Green,Blue};// public non-virtual 接口指定缺省参数voiddraw(ShapeColor colorRed)const{doDraw(color);// 调用 private virtual 实现}virtual~Shape()default;private:// private virtual 实现派生类可自定义行为virtualvoiddoDraw(ShapeColor color)const{std::coutShape::doDraw with color colorstd::endl;}};classRectangle:publicShape{private:virtualvoiddoDraw(ShapeColor color)constoverride{std::coutRectangle::doDraw with color colorstd::endl;}// 不需要指定缺省参数};classCircle:publicShape{private:virtualvoiddoDraw(ShapeColor color)constoverride{std::coutCircle::doDraw with color colorstd::endl;}};intmain(){Shape*psnewShape();Shape*prnewRectangle();Shape*pcnewCircle();ps-draw();// Shape::doDraw with color 0 (Red)pr-draw();// Rectangle::doDraw with color 0 (Red) —— 一致且正确pc-draw();// Circle::doDraw with color 0 (Red)// 也可以显式指定参数pr-draw(Shape::Green);// Rectangle::doDraw with color 1 (Green)deleteps;deletepr;deletepc;return0;}NVI 模式的优势优势说明缺省参数一致性所有派生类共享相同的缺省参数值接口与实现分离public接口稳定private实现可扩展前置/后置处理可以在non-virtual函数中添加通用逻辑符合条款 36non-virtual函数不会被派生类重定义classShape{public:voiddraw(ShapeColor colorRed)const{// 前置处理所有派生类共享prepareForDrawing();doDraw(color);// 多态调用// 后置处理所有派生类共享cleanupAfterDrawing();}private:virtualvoiddoDraw(ShapeColor color)const0;voidprepareForDrawing()const{std::coutPreparing canvas...std::endl;}voidcleanupAfterDrawing()const{std::coutCleaning up...std::endl;}};实际应用场景场景 1GUI 框架中的绘图系统#includeiostream#includestringclassWidget{public:enumRenderMode{Normal,Highlighted,Disabled};// NVI 模式统一的缺省参数和前置/后置处理voidrender(RenderMode modeNormal)const{beginRender();doRender(mode);endRender();}virtual~Widget()default;protected:// 派生类可访问的辅助函数boolisVisible()const{returnvisible;}private:virtualvoiddoRender(RenderMode mode)const0;voidbeginRender()const{std::cout[Begin Render]std::endl;}voidendRender()const{std::cout[End Render]std::endl;}boolvisibletrue;};classButton:publicWidget{private:virtualvoiddoRender(RenderMode mode)constoverride{std::coutButton rendering in mode modestd::endl;}};classTextBox:publicWidget{private:virtualvoiddoRender(RenderMode mode)constoverride{std::coutTextBox rendering in mode modestd::endl;}};voidrenderUI(constWidgetwidget){widget.render();// 总是使用 Widget::Normal 作为缺省值}场景 2网络请求库#includeiostream#includestringclassHttpClient{public:enumTimeout{Default30,Long120,Short5};// 统一的缺省超时时间voidsendRequest(conststd::stringurl,Timeout timeoutDefault){setupConnection();doSendRequest(url,timeout);teardownConnection();}virtual~HttpClient()default;private:virtualvoiddoSendRequest(conststd::stringurl,Timeout timeout)0;voidsetupConnection(){std::coutSetting up connection...std::endl;}voidteardownConnection(){std::coutTearing down connection...std::endl;}};classSecureHttpClient:publicHttpClient{private:virtualvoiddoSendRequest(conststd::stringurl,Timeout timeout)override{std::coutSending HTTPS request to url with timeout timeoutsstd::endl;}};classProxyHttpClient:publicHttpClient{private:virtualvoiddoSendRequest(conststd::stringurl,Timeout timeout)override{std::coutSending HTTP request through proxy to url with timeout timeoutsstd::endl;}};常见误区与解决方案误区 1“我在派生类中重复相同的缺省参数就安全了”classBase{public:virtualvoidfunc(intx10){/* ... */}};classDerived:publicBase{public:virtualvoidfunc(intx10)override{/* ... */}// 危险代码重复};问题代码重复DRY 原则被破坏如果基类的缺省参数改变所有派生类都必须同步修改仍然可能产生不一致如果某个派生类忘记修改误区 2“我可以用宏来避免重复”#defineDEFAULT_PARAM10classBase{public:virtualvoidfunc(intxDEFAULT_PARAM){/* ... */}};classDerived:publicBase{public:virtualvoidfunc(intxDEFAULT_PARAM)override{/* ... */}};虽然这解决了代码重复问题但仍然违反了条款 37 的精神且宏在现代 C 中应该避免使用。正确做法总结场景推荐方案virtual函数需要缺省参数使用 NVI 模式所有派生类共享相同缺省参数在public non-virtual函数中指定派生类需要不同的缺省行为考虑使用策略模式或函数重载总结核心要点要点说明缺省参数是静态绑定的由指针/引用的声明类型决定virtual函数是动态绑定的由对象的实际类型决定这种分裂会导致意外行为调用派生类函数却使用基类缺省参数NVI 模式是最佳解决方案public non-virtual提供缺省参数private virtual负责实现记忆口诀Virtual 函数动态绑缺省参数静态定。两者混用出大坑NVI 模式来救场。接口非虚参数稳实现私有可扩展。条款 37 的核心建议绝不重新定义继承而来的缺省参数值。如果你需要为virtual函数提供缺省参数使用 NVI 设计模式在public non-virtual函数中指定缺省参数让private virtual函数负责实际的多态实现参考阅读《Effective C》Scott Meyers条款 37《C Primer》Stanley B. Lippman 等关于虚函数和缺省参数的章节《设计模式》GoFTemplate Method 模式系列预告下一篇将深入解析条款 38——通过复合塑模出 has-a 或 “根据某物实现出”探讨复合composition与继承的区别以及何时应该选择复合而非继承。如果本文对你有帮助欢迎点赞、收藏、转发有任何问题可以在评论区留言讨论。