设计与声明
# 设计与声明, Design and Declarations
# Item 18:让接口容易被正确使用,不易被误用 Make interfaces easy to usecorrectly and hard to use incorrectly.
接口设计简明, 不要让用户对其有歧义
建立新类型,限制类型上的操作, 束缚对象值,消除客户的资源管理责任, 一个new type的案例,(enum会带来类型安全的问题? 什么是类型安全?)
class Month{ public: static Month Jan(){return Month(1);} //见下文为什么使用函数,不使用对象 static Month Feb(){return Month(2);} ... static Month Dec(){return Month(12);} ... private: explicit Month(int m); //explicit禁止参数隐式转换,private禁止用户生成自定义的月份 ... }; Date d(Month::Mar(), Day(30), Year(1995)); //正确
1
2
3
4
5
6
7
8
9
10
11
12
13好好设计程序的类型系统, 使用class, template, typedef, struct, enum等等(类型系统的C++最佳实践方式?)
# Item 19:设计class犹如设计type Treat class design as type design.
设计class的时候, 就好比设计type(一系列要注意的事项)
- 新类型的对象要如何创建和销毁?
这决定了要如何写构造函数和析构函数,包括要使用什么内存分配和释放函数,即new还是new[],delete还是delete[],见第16章 (opens new window)
- 对象初始化要如何区别于赋值?
这决定了你如何写,如何区别构造函数和赋值运算符,以及不要把初始化与赋值混淆,因为它们的语义不同,构造函数适用于未创建的对象,赋值适用于已创建的对象,这也是为什么我们要在构造函数中使用初始化列表而不使用赋值的原因,见第4章 (opens new window)和第12章 (opens new window)。
- 新类型的对象传值有什么意义?
要记住拷贝构造函数决定了你的类型是如何被传值的,因为传值会生成本地的拷贝。
- 新类型的合法数值有什么限制?
通常情况下,并不是成员的任何数值组合都是合法的。要让数据成员合法,我们需要根据合法的组合,在成员函数中对数值进行检测,尤其是构造函数,赋值运算符和setter。这也会影响到使用它的函数会抛出什么异常。
- 新类型属于某个继承层次吗?
如果你的新类型继承自某个已有的类,你的设计将被这些父类影响到,尤其是父类的某些函数是不是虚函数。如果你的新类型要作为一个父类,你将要决定把哪些函数声明为虚函数,尤其要注意析构函数,见第7章 (opens new window)。
- 新类型允许什么样的转换?
新类型的对象将会在程序的海洋中与其它各种各样的类型并用,这时你就要决定是否允许类型的转换。如果你希望把T1隐式转换为T2,你可以在T1中定义一个转换函数,例如operator T2,或者在T2中定义一个兼容T1的不加explicit修饰的构造函数。
如果希望使用显式转换,你要定义执行显示转换的函数,详见第15章 (opens new window)。
- 什么运算符和函数对于你的新类型是有意义的?
这决定了你要声明哪些函数,包括成员函数,非成员函数,友元函数等。
- 你要禁止哪些标准函数?
如果不希望使用编译器会自动生成的标准函数,把它们声明为私有,见第6章 (opens new window)
- 谁可以接触到成员?
这影响到哪些成员是公有的,哪些是保护的,哪些是私有的。这也能帮你决定哪些类和函数是友元的,以及要不要使用嵌套类(nested class)。
- 新类型的"隐藏接口"是什么?
新类型对于性能,异常安全性,资源管理(例如锁和内存)有什么保障? 哪些问题是自动解决不需要用户操心的? 要实现这些保障,自然会对这个类的实现产生限制,例如要使用智能指针而不要使用裸指针。
- 新类型有多通用?
如果想让你的新类型通用于许多类型,定义一个类模板(class template),而不是单个新类型。
新类型真的是你需要的吗?
如果定义一个子类只是为了给基类增加某些新功能,定义一些非成员的函数或者函数模板更加划算。
如何创建和销毁, operator new, operator new[]. operator delete, operator delete[],
对象初始化和对象的赋值有什么区别? copy constructor 和copy assignment之间有什么区别?
对一个新对象来说, pass by value意味着什么?因为要重载操作符, 函数, 和重载内存的分配和归还,
type cast的我呢提要怎么处理, 类型转换函数, operator T
# Item 20:宁以pass-by-reference-to-const替换pass-by-value Prefer pass-by-reference-to-const to pass-by-value.
函数的参数使用pass by reference to const替换pass by value,(内置类型, 其实pass by value也比较合适)
- 效率会比较高, 因为pass by value会产生临时对象, 对于非内置类型, 会调用copy constructor来进行构造
- pass by reference to const 实际上传递的是指针, 能够支持继承类的多态特性.
- 内置类型, STL迭代器和一些函数对象, 其实pass by value并不昂贵
# Item 21:必须返回对象时,别妄想返回其reference Don't try to return a reference when you must return an object.
如果必须返回一个对象, 不要返回他的reference, 因为可能会返回一个local的对象,local对象的renference是没有用的;
# Item 22:将成员变量声明为private Declare data members private.
成员变量声明为private
- public成员全部都是函数, 有利于语法一致性
- 使用函数来对成员变量进行精确的访问控制, 这样能够给类更好的封装性
- protect成员其实也是没有封装的, 因为如果在后续版本的代码里面,删除了这个protect成员, 其子类一样也需要修改
# Item 23:宁以non-member、non-friend替换member函数 Prefer non-member non-friend functions to member functions.
使用non member, non friend来替换member函数
- 对于private成员, 能够接触它的就是成员函数+友元函数
- 为的是增加封装性, 增加扩充的弹性,可以使用一个完全的第三方全局的function来进行, 称之为便携函数;
- 可以把这个全局函数放在一个命名空间里面, 稍微约束一下它的作用域
# Item 24:若所有参数皆需类型转换,请为此采用non-member函数 Declarenon-member functions when type conversions should apply to all parameters.
如果某个函数所有的参数都需要类型转换, 这个函数必须弄成non merber函数
隐式转换总体上会给程序带来隐患,因为如果出现了类型错误,编译器是不会报错的。
只有在参数表里出现的参数才可以进行隐式转换。例如operator*()这个函数, 局部的重载, 其实只能是X * 2这种形式, 但如果是要支持2 * x这种, 就需要全局的operator*()函数来支持;
class Rational{ public: ... const Rational operator*(const Rational& lhs) const; // 成员函数 ... }; result = oneHalf * 2; //编译通过 result = 2 * oneHalf; //编译错误
1
2
3
4
5
6
7
8class Rational{...}; const Rational operator*(const Rational& lhs, const Rational& rhs){ return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()) }//作为非成员函数, 两个参数都需要支持隐式转换 Rational oneFourth(1,4); Rational result; result = oneFourth * 2; //可以编译 result = 2 * oneFourth; //可以编译
1
2
3
4
5
6
7
8
9
# Item 25:考虑写出一个不抛异常的swap函数 Consider support for a non-throwing swap.
给自己的类写的swap函数,不要抛出异常, 这是为了和STL库一样支持swap的异常安全;
friend可以方位private变量和函数;
使用pimpl(the "pimpl" idiom,即"pointer to implementation"), 这样一来,要调换两个对象,直接交换指针就行了
//这个类包含Widget类的数据 class WidgetImpl{ public: ... private: int a,b,c; std::vector<double> v; //高成本拷贝警告! }; //使用pimpl手法的类 class Widget{ public: Widget(const Widget& rhs); //赋值运算符的实现见ch 10,11,12 Widget& operator=(const Widget& rhs){ ... *pImpl = *(rhs.pImpl); ... } ... private: WidgetImpl* pImpl; //使用pimpl指针来指向我们的数据 };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23如果默认的std::swap不会对效率产生比较大的影响,例如对象的成员数据不多,直接使用是没有问题的,就不用大费周章搞这些了
如果默认的std::swap会对你的函数/类模板产生效率影响: 给你的类使用pimpl手法,然后给它写一个只交换指针的swap成员函数,而且这个函数禁止抛出异常,然后:
对于类模板,要在类模板相同的名空间下写一个自定义的swap,在里面调用swap成员函数
对于类(不是类模板),还要给std::swap进行特殊化,也在它里面调用swap成员函数
调用swap的时候确保加上using语句来让std名空间里面的swap对编译器可见,然后swap函数前不要加任何名空间资格限制(qualification)
当默认的std::swap可能会拉低你自己的类的效率时,在自己的类里写一个swap成员函数,而且要保证它不会抛出异常
写了swap成员函数,按照编程惯例还要写一个非成员swap函数,放在类或者类模板的名空间下,用它来调用成员swap函数。对于类(非模板),还要特殊化std::swap
在调用swap时,要加上一句using std::swap,然后调用时不需要再加任何名空间资格限制, 否则可能会调用到私有的swap函数里面去
为了自定义的类而完全特殊化std模板是没问题的,但千万不要给std里添加任何东西。
再或者, 直接使用类自带的namespace, 来重写swap函数;
class Widget{ public: ... void swap(Widget& data){ using std::swap; // 这句稍后解释 swap(pImpl, other.pImpl); // 执行真正的swap,只交换指针 } ... }; namespace std{ template<> // 完全特殊化的std::swap void swap<Widget>(Widget& a, Widget& b){ a.swap(b); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16