Effective C++之四:设计与声明
2017-12-21 15:56
429 查看
条款18 让接口容易被正确使用,不易被误用
(1)“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
客户可能输入错误的日子
class Date{
pubilc:
Date(int month,int day,
int year);
...
};
Date(30,3,1995);
可以通过导入新的类型进行预防:
class Date{
pubilc:
Date(int month,int day,
int year);
...
};
struct Day{
explicit Day(int d):val(d){}
int val;
};
class Month{
//一年只有12个月份,不能出现无效月份
public:
staticMonth Jan() {return Month(1)};
//返回有效月份
staticMonth Feb() {return
Month(2)}; //以函数替换对象,表现某个特定月份。non-local-static的使用!
...
static Month Dec() {return Month(12)};
...
private:
explicit Month(int m); //阻止生成新的月份
... //这是月份专属数据
};
struct Year{
explicit Year(int y):val(y){}
int val;
};
class Date{
pubilc:
Date(const Month& m,const Day& d,
const Year& y);
...
};
int main()
{
Date d(30,3,
1995); //错误
Date d(Day(30), Month(3), Year(1995));
//错误
Date d(Month(3), Day(30), Year(1995));
//错误
Date d(Month::Mar(), Day(30), Year(1995));//正确。
return0;
}
(2)
sharp_ptr支持定制型删除器。这可防范DLL(动态链接库)问题,可被用来自动解除互斥锁(条款14)等。
DLL问题是:对象在动态链接库中被new创建,却在另一个DLL被delete,这会导致运行期错误。
shared_ptr没有这问题,因为它缺省的删除器是来自它诞生的那个DLL的delete。
shared_prt<Investment> createInvestment()
{
shared_prt<Investment> retVal(static_cast<Investment*>(0), //建立一个空指针。需要static_cast转换。直接0编译不过。
getRidOfInvestment);
//为shared_prt设立一个删除器
retVal = ...;
return retVal;
}
条款19 设计class犹如设计type
class设计就是type的设计。
(1)新type的对象应该如何被创建和销毁(条款49-52)
(2)对象的初始化和对象的赋值该有什么样的差别(条款4)
(3)
新type的对象如果被passed by value意味着什么? 会调用构造函数
记住copy构造函数用来定义一个type的pass-by-value该如何实现?如果有继承,还要先调用基类的构造函数和自己成员变量的构造函数
(4)什么是新type的合法值?(条款18)
(5)新的type需要配合某个继承图系么?基类是virtual与否(条款34、36);是否允许其他类继承你,会影响你的析构函数是不是virtual(条款7)
(6)你的新type需要什么样的转换?(条款15有隐式显式转换的范例)
(7)什么样的操作符和函数对此type而言是合理的。这个问题答案决定将声明哪些函数,其中哪些是member函数(条款23/24/46)
(8)什么样的标准函数应该驳回? 那就把这些声明为private (条款6)
(9)谁该取用新type的成员?哪些是public、private、friends
(10)什么是新type的未声明接口?(条款29)
(11)你的新type有多么一般化?
(12)你真的需要一个新type么?还是说定义一个派生类就可以。
条款20 宁以pass-by-reference-const替换pass-by-value
尽量以pass-by-reference-const替换pass-by-value,前者比较高效并且可以解决切割问题。
(1)高效
bool vaildstudent(student s);//这个方式被淘汰!
student plato;
bool platoisok = vaildstudent(plato); //会构造一个新的对象s,然后函数完毕再销毁。其中student里面的基类和string成员变量的构造函数和析构函数都要被调用。浪费资源。
bool vaildstudent(const student& s); //const避免传入的对象被改变。
(2)解决切割问题
void printNameAndDisplay(Window w) //会发生切割。
{//如果传递派生类,会被认为是基类对象,只会调用基类的构造函数。丧失了多态的特性。
cout<<w.name();
w.display();
}
void printNameAndDisplay(const Window& w) //如果传进来是Window的派生类,就不会发生切割。
{ //因为引用是用指针实现出来的
cout<<w.name();
w.display();
}
(3)
但是以上的规则并不适用于内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value比较合适。
条款21 必须返回对象时,别妄想返回reference
绝不要返回pointer或reference指向一个local stack对象(会被销毁),或返回reference指向一个heap-allocated对象(可能忘记手动delete),或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
(1)
//有理数相乘的函数
const Rational& operator*(const Rational& lhs,
const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result; //错!返回的引用指向的东西在函数完毕后就销毁了
}
(2)
//有理数相乘的函数
const Rational& operator*(const Rational& lhs,
const Rational& rhs)
{
Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return result; //更错!谁来delete??肯定会内存泄露
}
(3)
//有理数相乘的函数
const Rational& operator*(const Rational& lhs,
const Rational& rhs)
{
static Rational result; //错错错!每一个返回的引用都是指向同一个内部定义的static
Rational对象
result = ...;
return result;
}
(4)
//有理数相乘的函数
inline const Rationaloperator*(const
Rational& lhs,
const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d); //正确
}
(5)单例模式
条款4已经为“在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例。
条款22 将成员变量声明为private
(1)
切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,方便函数内部实现变化。并提供class作者以充分的实现弹性。
访问数据的一致性:所有访问成员变量都需要成员函数来访问,那就一致了。
class AccessLevels{ //可细微划分访问控制
public:
int getReadOnly()const {returnreadonly};
void setReadWrite(int value) {readwrite = value;}
int getReadWrite()const {returnreadwrite};
void setWriteOnly(int value) {writeonly = value;}
private:
int noAccess; //禁用
int readonly; //只读
int readwrite; //只写
int writeonly; //读写
};
(2)
protected 并不比 public更具封装性。
条款23 宁以non-menber、non-friend替换 member函数
(1)
宁以non-menber、non-friend替换 member函数。 这样做可以增加封装性、包裹弹性和机能扩充性。
class WebBrowser{
public:
void func1();
void func2();
void func3();
...
//方式一:使用成员函数
void doeverything();//里面调用 func1、func2、func3
};
//方式二:使用non-member、non-friend函数。我们选择使用这一种方法!
void outsizefunc(WebBrowser& wb)
{
wb.func1();
wb.func2();
wb.func3();
}
1.增加代码的封装性:有多少函数可以访问变量。
2.提高代码的扩展性
3.降低代码的编译依存性。
(2)
可以把不同功能的便利函数放在不同的头文件里面,但是放在同一个namespace(命名空间)里面就可。
使用的时候只需要#include相对应功能的头文件就可以。允许客户只对他们所用的部分系统形成编译相依。
要想实现这种切割机能的方式,就不适用于class成员函数,因为一个class必须整体定义,不能被分割为片段。
//头文件 "webbrowser.h",针对class本身及WebBrowser核心机能
namespace WebBrowserStuff
{
class WebBrowser{...};
...
//核心机能。几乎所有客户都需要的
//non-member函数
}
//头文件 "webbrowserbookmarks.h"
namespace WebBrowserStuff//将所有便利函数放在多个头文件内但隶属于同一个命名空间,意味着客户可以轻松扩展这一组遍历函数。
{ //他们需要做的就是直接添加更多的non-member、non-friend函数到此命名空间中
... //与书签相关的便利函数
}
//头文件 "webbrowsercookies.h"
namespace WebBrowserStuff
{
... //与cookie相关的便利函数
}
条款24 若所有参数皆需类型转换,请为此采用non-member函数
(1)
如果需要为某个函数的所有参数(包括this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是non-member。
class Rational{
public:
Rational(int numerator =0,
int denominator =1);
//没有explicit,支持隐式转换
int numerator()const;
int denominator()const;
private:
...
};
//必须写为non-member函数,不然 ret = 2 * onefour;就会错。因为只有当
参数 被列进参数列,这个参数才是隐式转换的合格参与者
const Rationaloperator*(constRational&
lhs,
constRational& rhs)
{
returnRational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
int main()
{
Rational onefour(1,4);
Rational ret;
ret = onefour * 2; //可以,2被隐式转换了
ret = 2 * onefour; //可以,2被隐式转换了
return0;
}
(2)
无论何时,如果你可以避免friend函数就该避免。
条款25 考虑写出一个不抛异常的swap函数
(0)
如果swap的缺省代码对你的class或class template提供可接受的效率,你不需要额外做任何事。
(1)正在编写class的 swap
当std::swap对你的效率不高时,做下面几件事:
1、提供一个public swap成员函数,并确定这个成员函数不抛出异常。
2、在你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述的swap成员函数。
3、如果你正在编写一个class(而非class template),为你的class特化std::swap。并令她调用你的swap函数。
4、最后,如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap。
class Widget
{
public:
Widget(const Widget&);
Widget& operator=(const Widget&
rhs)
{ ...
*pImpl = *(rhs.pImpl);
...
}
void swap(Widget& rhs)
//public swap成员函数
{
using std::swap;
//包含using声明式
swap(pImpl, rhs.pImpl); //赤裸裸调用
}
private:
WidgetImpl *pImpl;
};
//如果你正在编写一个class(而非class template),为你的class特化std::swap。并令她调用你的swap函数。
namespace std
{
template <>
void swap<Widget>(Widget& a,
Widget& b) //修订后的std::swap特化版本
{
a.swap(b);
}
}
(2)如果我们的类是一个模板类,但是c++并不允许对函数偏特化,所以我们在类的命名空间中重载swap函数就行,这是个基本套路!
namespace WidgetStuff
{
template<typename T>
class Widget
{
public:
Widget(const Widget&);
Widget& operator=(const Widget&
rhs)
{ ...
*pImpl = *(rhs.pImpl);
...
}
void swap(Widget& rhs)
//public swap成员函数
{
using std::swap;
//包含using声明式
swap(pImpl, rhs.pImpl); //赤裸裸调用
}
private:
WidgetImpl *pImpl;
};
template<typename T>
void swap(Widget<T> &lhs,
Widget<T> &rhs)
//non-member swap函数。这里并不属于std命名空间
{
lhs.swap(rhs);
}
}
//调用swap时应该针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
template <typename T>
void doSomthing(T& lhs, T& rhs)
{
using
std::swap;
//令std::swap在此函数内可用
swap(lhs, rhs); //为T类型对象调用最佳swap版本
}
(3)
为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
(1)“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
客户可能输入错误的日子
class Date{
pubilc:
Date(int month,int day,
int year);
...
};
Date(30,3,1995);
可以通过导入新的类型进行预防:
class Date{
pubilc:
Date(int month,int day,
int year);
...
};
struct Day{
explicit Day(int d):val(d){}
int val;
};
class Month{
//一年只有12个月份,不能出现无效月份
public:
staticMonth Jan() {return Month(1)};
//返回有效月份
staticMonth Feb() {return
Month(2)}; //以函数替换对象,表现某个特定月份。non-local-static的使用!
...
static Month Dec() {return Month(12)};
...
private:
explicit Month(int m); //阻止生成新的月份
... //这是月份专属数据
};
struct Year{
explicit Year(int y):val(y){}
int val;
};
class Date{
pubilc:
Date(const Month& m,const Day& d,
const Year& y);
...
};
int main()
{
Date d(30,3,
1995); //错误
Date d(Day(30), Month(3), Year(1995));
//错误
Date d(Month(3), Day(30), Year(1995));
//错误
Date d(Month::Mar(), Day(30), Year(1995));//正确。
return0;
}
(2)
sharp_ptr支持定制型删除器。这可防范DLL(动态链接库)问题,可被用来自动解除互斥锁(条款14)等。
DLL问题是:对象在动态链接库中被new创建,却在另一个DLL被delete,这会导致运行期错误。
shared_ptr没有这问题,因为它缺省的删除器是来自它诞生的那个DLL的delete。
shared_prt<Investment> createInvestment()
{
shared_prt<Investment> retVal(static_cast<Investment*>(0), //建立一个空指针。需要static_cast转换。直接0编译不过。
getRidOfInvestment);
//为shared_prt设立一个删除器
retVal = ...;
return retVal;
}
条款19 设计class犹如设计type
class设计就是type的设计。
(1)新type的对象应该如何被创建和销毁(条款49-52)
(2)对象的初始化和对象的赋值该有什么样的差别(条款4)
(3)
新type的对象如果被passed by value意味着什么? 会调用构造函数
记住copy构造函数用来定义一个type的pass-by-value该如何实现?如果有继承,还要先调用基类的构造函数和自己成员变量的构造函数
(4)什么是新type的合法值?(条款18)
(5)新的type需要配合某个继承图系么?基类是virtual与否(条款34、36);是否允许其他类继承你,会影响你的析构函数是不是virtual(条款7)
(6)你的新type需要什么样的转换?(条款15有隐式显式转换的范例)
(7)什么样的操作符和函数对此type而言是合理的。这个问题答案决定将声明哪些函数,其中哪些是member函数(条款23/24/46)
(8)什么样的标准函数应该驳回? 那就把这些声明为private (条款6)
(9)谁该取用新type的成员?哪些是public、private、friends
(10)什么是新type的未声明接口?(条款29)
(11)你的新type有多么一般化?
(12)你真的需要一个新type么?还是说定义一个派生类就可以。
条款20 宁以pass-by-reference-const替换pass-by-value
尽量以pass-by-reference-const替换pass-by-value,前者比较高效并且可以解决切割问题。
(1)高效
bool vaildstudent(student s);//这个方式被淘汰!
student plato;
bool platoisok = vaildstudent(plato); //会构造一个新的对象s,然后函数完毕再销毁。其中student里面的基类和string成员变量的构造函数和析构函数都要被调用。浪费资源。
bool vaildstudent(const student& s); //const避免传入的对象被改变。
(2)解决切割问题
void printNameAndDisplay(Window w) //会发生切割。
{//如果传递派生类,会被认为是基类对象,只会调用基类的构造函数。丧失了多态的特性。
cout<<w.name();
w.display();
}
void printNameAndDisplay(const Window& w) //如果传进来是Window的派生类,就不会发生切割。
{ //因为引用是用指针实现出来的
cout<<w.name();
w.display();
}
(3)
但是以上的规则并不适用于内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value比较合适。
条款21 必须返回对象时,别妄想返回reference
绝不要返回pointer或reference指向一个local stack对象(会被销毁),或返回reference指向一个heap-allocated对象(可能忘记手动delete),或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
(1)
//有理数相乘的函数
const Rational& operator*(const Rational& lhs,
const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result; //错!返回的引用指向的东西在函数完毕后就销毁了
}
(2)
//有理数相乘的函数
const Rational& operator*(const Rational& lhs,
const Rational& rhs)
{
Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return result; //更错!谁来delete??肯定会内存泄露
}
(3)
//有理数相乘的函数
const Rational& operator*(const Rational& lhs,
const Rational& rhs)
{
static Rational result; //错错错!每一个返回的引用都是指向同一个内部定义的static
Rational对象
result = ...;
return result;
}
(4)
//有理数相乘的函数
inline const Rationaloperator*(const
Rational& lhs,
const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d); //正确
}
(5)单例模式
条款4已经为“在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例。
条款22 将成员变量声明为private
(1)
切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,方便函数内部实现变化。并提供class作者以充分的实现弹性。
访问数据的一致性:所有访问成员变量都需要成员函数来访问,那就一致了。
class AccessLevels{ //可细微划分访问控制
public:
int getReadOnly()const {returnreadonly};
void setReadWrite(int value) {readwrite = value;}
int getReadWrite()const {returnreadwrite};
void setWriteOnly(int value) {writeonly = value;}
private:
int noAccess; //禁用
int readonly; //只读
int readwrite; //只写
int writeonly; //读写
};
(2)
protected 并不比 public更具封装性。
条款23 宁以non-menber、non-friend替换 member函数
(1)
宁以non-menber、non-friend替换 member函数。 这样做可以增加封装性、包裹弹性和机能扩充性。
class WebBrowser{
public:
void func1();
void func2();
void func3();
...
//方式一:使用成员函数
void doeverything();//里面调用 func1、func2、func3
};
//方式二:使用non-member、non-friend函数。我们选择使用这一种方法!
void outsizefunc(WebBrowser& wb)
{
wb.func1();
wb.func2();
wb.func3();
}
1.增加代码的封装性:有多少函数可以访问变量。
2.提高代码的扩展性
3.降低代码的编译依存性。
(2)
可以把不同功能的便利函数放在不同的头文件里面,但是放在同一个namespace(命名空间)里面就可。
使用的时候只需要#include相对应功能的头文件就可以。允许客户只对他们所用的部分系统形成编译相依。
要想实现这种切割机能的方式,就不适用于class成员函数,因为一个class必须整体定义,不能被分割为片段。
//头文件 "webbrowser.h",针对class本身及WebBrowser核心机能
namespace WebBrowserStuff
{
class WebBrowser{...};
...
//核心机能。几乎所有客户都需要的
//non-member函数
}
//头文件 "webbrowserbookmarks.h"
namespace WebBrowserStuff//将所有便利函数放在多个头文件内但隶属于同一个命名空间,意味着客户可以轻松扩展这一组遍历函数。
{ //他们需要做的就是直接添加更多的non-member、non-friend函数到此命名空间中
... //与书签相关的便利函数
}
//头文件 "webbrowsercookies.h"
namespace WebBrowserStuff
{
... //与cookie相关的便利函数
}
条款24 若所有参数皆需类型转换,请为此采用non-member函数
(1)
如果需要为某个函数的所有参数(包括this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是non-member。
class Rational{
public:
Rational(int numerator =0,
int denominator =1);
//没有explicit,支持隐式转换
int numerator()const;
int denominator()const;
private:
...
};
//必须写为non-member函数,不然 ret = 2 * onefour;就会错。因为只有当
参数 被列进参数列,这个参数才是隐式转换的合格参与者
const Rationaloperator*(constRational&
lhs,
constRational& rhs)
{
returnRational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
int main()
{
Rational onefour(1,4);
Rational ret;
ret = onefour * 2; //可以,2被隐式转换了
ret = 2 * onefour; //可以,2被隐式转换了
return0;
}
(2)
无论何时,如果你可以避免friend函数就该避免。
条款25 考虑写出一个不抛异常的swap函数
(0)
如果swap的缺省代码对你的class或class template提供可接受的效率,你不需要额外做任何事。
(1)正在编写class的 swap
当std::swap对你的效率不高时,做下面几件事:
1、提供一个public swap成员函数,并确定这个成员函数不抛出异常。
2、在你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述的swap成员函数。
3、如果你正在编写一个class(而非class template),为你的class特化std::swap。并令她调用你的swap函数。
4、最后,如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap。
class Widget
{
public:
Widget(const Widget&);
Widget& operator=(const Widget&
rhs)
{ ...
*pImpl = *(rhs.pImpl);
...
}
void swap(Widget& rhs)
//public swap成员函数
{
using std::swap;
//包含using声明式
swap(pImpl, rhs.pImpl); //赤裸裸调用
}
private:
WidgetImpl *pImpl;
};
//如果你正在编写一个class(而非class template),为你的class特化std::swap。并令她调用你的swap函数。
namespace std
{
template <>
void swap<Widget>(Widget& a,
Widget& b) //修订后的std::swap特化版本
{
a.swap(b);
}
}
(2)如果我们的类是一个模板类,但是c++并不允许对函数偏特化,所以我们在类的命名空间中重载swap函数就行,这是个基本套路!
namespace WidgetStuff
{
template<typename T>
class Widget
{
public:
Widget(const Widget&);
Widget& operator=(const Widget&
rhs)
{ ...
*pImpl = *(rhs.pImpl);
...
}
void swap(Widget& rhs)
//public swap成员函数
{
using std::swap;
//包含using声明式
swap(pImpl, rhs.pImpl); //赤裸裸调用
}
private:
WidgetImpl *pImpl;
};
template<typename T>
void swap(Widget<T> &lhs,
Widget<T> &rhs)
//non-member swap函数。这里并不属于std命名空间
{
lhs.swap(rhs);
}
}
//调用swap时应该针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
template <typename T>
void doSomthing(T& lhs, T& rhs)
{
using
std::swap;
//令std::swap在此函数内可用
swap(lhs, rhs); //为T类型对象调用最佳swap版本
}
(3)
为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
相关文章推荐
- 《Effective C++》第4章 设计与声明(1)-读书笔记
- Effective C++ 读书笔记(18-28):类与函数之设计和声明
- 《Effective C++》设计与声明:条款18-条款19
- Effective C++之设计与声明
- Effective C++ 精要(第四部分:设计与声明)
- Effective C++ 小笔记:条款18-25(第四章 设计与声明)
- Effective C++ —— 设计与声明(四)
- 《Effective C++》第四章:设计与声明
- Effective C++ -- 设计与声明
- Effective C++ --4 设计与声明
- effective C++ 读书笔记四 —— 设计与声明
- Effective C++ ——设计与声明
- Effective C++ ——设计与声明
- Effective C++ — 类的设计与声明
- Effective C++ ——设计与声明
- effective C++: 4.设计与声明
- Effective C++ 笔记:4设计与声明
- 《Effective C++ 3》04 设计与声明 条款:18-25
- Effective C++第四章-设计与声明-2
- Effective c++ 18~ 25 设计与声明