Effective C++ 条款19:设计 class 犹如设计 type

发布时间:2026/6/11 12:18:42
Effective C++ 条款19:设计 class 犹如设计 type Effective C 条款19设计 class 犹如设计 type核心观点在 C 中设计 class 就是设计一个全新的 type。你应该带着和语言设计者当初设计语言内置类型时一样的谨慎来研讨 class 的设计。一、类 类型重新认识 class在 C 中class不仅仅是一种组织代码的方式——每一个 class 都是一个完整的类型type。当你定义一个int时你获得了什么明确的内存表示通常是 4 字节一组合法的操作、-、*、/、%等明确的初始化和赋值语义与上下文的转换规则在表达式中的行为定义当你定义一个class Widget时你也应该提供同样完整和精确的定义。用户对你的Widget的期望和他们对int的期望没有本质区别。关键洞察设计一个优秀的 class其难度不亚于设计一门语言中的内置类型。二、设计新 type 前必须回答的问题Scott Meyers 在本条款中提出了一系列关键问题。在写下class关键字之前请先认真思考这些问题。2.1 对象的创建与销毁// 你的类型支持哪些创建方式Widget w1;// 默认构造Widgetw2(w1);// 拷贝构造Widgetw3(std::move(w1));// 移动构造C11Widget w4Widget();// 拷贝/移动构造autow5std::make_uniqueWidget();// 工厂模式// 你的类型支持哪些销毁方式// - 栈上自动销毁// - delete 销毁// - 是否需要虚析构函数设计决策是否需要默认构造函数拷贝语义是什么深拷贝还是浅拷贝是否支持移动语义C11是否需要虚析构函数作为基类时2.2 初始化与赋值的区别Widget w1;Widget w2w1;// 初始化拷贝构造w2w1;// 赋值拷贝赋值运算符// 这两个操作语义上可能完全不同设计决策初始化和赋值是否应该产生相同的结果是否需要自定义拷贝赋值运算符是否需要自定义移动赋值运算符2.3 值传递的语义voidprocess(Widget w);// 按值传递voidprocess(Widgetw);// 按引用传递voidprocess(constWidgetw);// 按 const 引用传递voidprocess(Widgetw);// 按右值引用传递C11设计决策按值传递你的类型是否高效是否支持移动语义来优化值传递是否应该在某些情况下禁止拷贝2.4 合法值的约束classDate{public:Date(intyear,intmonth,intday);// month 必须是 1-12// day 必须对应该月的有效日期};设计决策什么样的值组合是合法的非法值应该在编译期还是运行期被拒绝使用异常、断言还是其他机制2.5 继承体系中的定位classBase{public:virtual~Base()default;virtualvoiddraw()0;};classDerived:publicBase{public:voiddraw()override;};设计决策你的类是被设计为基类吗如果是基类析构函数是否为virtual哪些函数应该是virtual是否允许进一步派生final2.6 类型转换classRational{public:Rational(intnumerator0,intdenominator1);// 隐式转换到 doubleoperatordouble()const;// 还是显式转换doubletoDouble()const;};Rationalr(3,5);doubledr;// 隐式转换可能意外doubled2r.toDouble();// 显式转换更安全设计决策是否允许隐式类型转换如果需要转换是隐式还是显式是否提供显式的转换函数2.7 操作符重载classComplex{public:Complex(doublereal,doubleimag);// 哪些操作符需要重载Complexoperator(constComplexrhs)const;Complexoperator-(constComplexrhs)const;Complexoperator*(constComplexrhs)const;// 复合赋值操作符Complexoperator(constComplexrhs);// 比较操作符booloperator(constComplexrhs)const;booloperator!(constComplexrhs)const;// 流输出friendstd::ostreamoperator(std::ostreamos,constComplexc);};设计决策哪些操作符对该类型有意义操作符的语义应该与内置类型一致吗是否需要提供非成员版本的操作符2.8 一般化模板参数// 你的类型是否应该是模板templatetypenameTclassStack{public:voidpush(constTitem);Tpop();boolempty()const;private:std::vectorTdata_;};// 还是非模板的具体类型classIntStack{// 只支持 int};设计决策你的类型是否适用于多种数据类型如果泛化有哪些约束条件C20 concepts泛化是否会带来不必要的复杂性三、完整的设计案例一个健壮的字符串包装类让我们综合运用上述原则设计一个简单的String类#includeiostream#includecstring#includealgorithm#includeutilityclassString{private:char*data_;size_t length_;// 辅助函数voidinitFromCString(constchar*str);public:// 创建与销毁 // 默认构造String():data_(newchar[1]{\0}),length_(0){}// 从 C 字符串构造explicitString(constchar*str);// 拷贝构造String(constStringother);// 移动构造C11String(Stringother)noexcept;// 析构~String();// 赋值 // 拷贝赋值Stringoperator(constStringother);// 移动赋值C11Stringoperator(Stringother)noexcept;// 访问器 size_tlength()const{returnlength_;}boolempty()const{returnlength_0;}constchar*c_str()const{returndata_;}charoperator[](size_t index)const;charoperator[](size_t index);// 比较 booloperator(constStringother)const;booloperator!(constStringother)const;booloperator(constStringother)const;// 操作 Stringoperator(constStringother);// 友元函数friendStringoperator(constStringlhs,constStringrhs);friendstd::ostreamoperator(std::ostreamos,constStringstr);};// 实现String::String(constchar*str){initFromCString(str);}String::String(constStringother):length_(other.length_){data_newchar[length_1];std::memcpy(data_,other.data_,length_1);}String::String(Stringother)noexcept:data_(other.data_),length_(other.length_){other.data_nullptr;other.length_0;}String::~String(){delete[]data_;}StringString::operator(constStringother){if(this!other){Stringtemp(other);// 拷贝构造临时对象std::swap(data_,temp.data_);std::swap(length_,temp.length_);}return*this;}StringString::operator(Stringother)noexcept{if(this!other){delete[]data_;data_other.data_;length_other.length_;other.data_nullptr;other.length_0;}return*this;}voidString::initFromCString(constchar*str){if(str){length_std::strlen(str);data_newchar[length_1];std::memcpy(data_,str,length_1);}else{data_newchar[1]{\0};length_0;}}charString::operator[](size_t index)const{if(indexlength_){throwstd::out_of_range(String index out of range);}returndata_[index];}charString::operator[](size_t index){if(indexlength_){throwstd::out_of_range(String index out of range);}returndata_[index];}boolString::operator(constStringother)const{if(length_!other.length_)returnfalse;returnstd::memcmp(data_,other.data_,length_)0;}StringString::operator(constStringother){char*newDatanewchar[length_other.length_1];std::memcpy(newData,data_,length_);std::memcpy(newDatalength_,other.data_,other.length_1);delete[]data_;data_newData;length_other.length_;return*this;}Stringoperator(constStringlhs,constStringrhs){Stringresult(lhs);resultrhs;returnresult;}std::ostreamoperator(std::ostreamos,constStringstr){osstr.data_;returnos;}这个设计考虑了方面设计决策创建默认构造、C字符串构造、拷贝构造、移动构造销毁析构函数释放动态内存初始化 vs 赋值区分拷贝构造和拷贝赋值值传递支持移动语义高效值传递合法值空字符串是合法的nullptr 被安全处理继承非基类设计析构函数非 virtual转换提供c_str()显式转换禁止隐式转换操作符支持[]、、、、四、现代 C 的简化Rule of Zero / Three / FiveRule of Zero如果你的类不需要手动管理资源就让编译器自动生成所有特殊成员函数// ✅ Rule of Zero所有成员都能自动管理资源classModernWidget{private:std::string name_;std::vectorintdata_;std::unique_ptrHelperhelper_;public:// 不需要定义任何特殊成员函数// 编译器生成的默认版本完全正确};Rule of FiveC11 起如果你的类需要自定义析构函数通常也需要自定义以下全部五个classResourceOwner{public:// 1. 析构函数~ResourceOwner();// 2. 拷贝构造函数ResourceOwner(constResourceOwnerother);// 3. 拷贝赋值运算符ResourceOwneroperator(constResourceOwnerother);// 4. 移动构造函数ResourceOwner(ResourceOwnerother)noexcept;// 5. 移动赋值运算符ResourceOwneroperator(ResourceOwnerother)noexcept;};现代建议优先遵循 Rule of Zero。只有当类确实拥有原始资源裸指针、文件句柄等时才考虑 Rule of Five。五、总结设计 class 就是设计 type。在定义一个新 class 之前请系统性地回答以下问题问题类别关键问题创建与销毁支持哪些构造函数析构函数需要什么特殊处理初始化与赋值初始化和赋值语义是否一致值传递按值传递是否高效是否需要移动语义合法值哪些值组合是合法的如何约束继承是否设计为基类虚函数策略转换支持哪些类型转换隐式还是显式操作符哪些操作符有意义语义如何定义一般化是否应该设计为模板设计哲学把每一个 class 都当作一门小型语言的类型来设计。你的用户包括未来的你自己会感谢你的严谨。六、延伸阅读Effective C 条款05了解 C 默默编写并调用哪些函数Effective C 条款06若不想使用编译器自动生成的函数就该明确拒绝Effective C 条款18让接口容易被正确使用不易被误用C Core GuidelinesC.21 - If you define ordeleteany default operation, define ordeletethem all《C Primer》第 13 章拷贝控制如果这篇文章对你有帮助欢迎点赞 、收藏 ⭐、评论 你的支持是我持续创作的动力