effective c++第四章(条款18-25)

5 篇文章 0 订阅
本文探讨了C++编程中的接口设计原则,如使接口易于正确使用且难以误用,通过工厂函数防止错误参数传递,利用智能指针避免内存管理错误。此外,强调了类设计应视为类型设计,推荐使用引用传递而非值传递以提高效率并避免切割问题。文章还指出,应优先考虑非成员函数而非成员函数,并考虑支持不抛异常的swap函数以增强代码的健壮性。
摘要由CSDN通过智能技术生成

第四章:设计与声明

设计与声明

条款18:让接口容易被正确使用,不易被使用(Make interfaces easy to use correctly and hard to use incorrectly)

假设有如下代码

class Date{
public:
   Date(int month,int day,int year);
   ...
};

看似这个接口合理,但是客户很容易犯下错误

Date(30,3,1995);    //错误
Date(2,30,1995);    //错误

都以错误的次序传递参数,所以以函数替换对象,表示某个特定的月份

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);           //阻止生成新的月份
   ...
};
class Day{...};
class Year{...};

Date d(Month::Mar(),Day(30),Year(1995));

当我们使用一个factory函数时,第一种用法可能对开启错误机会:没有删除指针,或删除同一个指针超过一次

Investment* creatInvestment();         //可能开启客户错误机会
std::trl::shared_ptr<Investment> creatInvestment();

第二种强迫客户将返回值存储于一个trl::shared_ptr内,从而消弭了客户忘记删除Investment对象的可能性。
现在我们使用trl::shared_ptr接受两个实参,当第一个指针的引用次数为0时,调用第二个实参“删除器”:

std::trl::shared_ptr<Investment> pInv(0,getRidPfInvestment);     //错误

然而这无法通过编译。因为trl::shared_ptr构造函数第一个参数必须是指针,而0不是指针(int),通过转型(cast)static_cast

std::trl::shared_ptr<Investment> pInv(static_cast(Investment*)(0),getRidPfInvestment);        //可以

请记住:
-好的接口很容易被正确使用,不容易被误用。应该在所以接口中努力达成这些性质
-“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容
-“阻止误用”的办法包括建立新的类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任
-trl::shared_ptr支持定制型删除器。可防范DLL问题,可被用来自动解除互斥锁等等

条款19:设计class犹如设计type(Treat class design as type design)

当我们用c++这种oop(面对对象编程)语言设计class时应该仔细考虑:

1.新的type的对象应该如何被创建和销毁
2.对象的初始化和对象的赋值该有什么的差别
3.新的type的对象如果被pass by value(以值传递),意味着什么?
4.什么是新的type的"合法值"?
5.你的新的type需要配合某个继承图系吗?
6.你的新的type需要什么样的转换?
7.什么样的操作符和函数对此新的type而言是合理的?
8.什么样的标准函数应该被驳回?
9.谁该取用新的type的成员?
10.什么是新type的“为声明接口”?
11.你的新type有多么一般化?
12.你真的需要一个新的type吗?

请记住:
-Class的设计就是type的设计。在定义一个新type之前,请确定你已经考虑过本条款覆盖的所讨论主题

条款20:宁以pass-by-reference-to-const替换pass-by-value(Prefer pass-by reference-to-const to pass-by-value)

考虑如下class代码:

class Person{
public:
   Person();
   virtual ~Person();
   ...
private:
   std::string name;
   std::string address;
};
calss Student:public Person{
public:
   Student();
   ~Student();
   ...
private:
   std::string schoolName;
   std::string schoolAddress;
};
bool validateStudent(Student s);            //by value
Student plato;
bool platoIsok = validateStudent(plato);

当发生上述函数通过pass-by-value调用时会发生什么?首先Student 的copy构造函数被调用,而Student对象内又有两个string的对象,又要调用两次string的copy构造函数,同样的Student对象继承Person对象,又必须要构造一个Person对象,Person对象又要承担两个string构造动作。因此,以by value方式传递一个Student对象总体成本需要六次构造函数和六次析构函数

虽然这是正确的,也是我们使用这种方法所必要的成本,但是是否可以回避那些构造和析构动作呢?就是pass by reference-to-const:

bool validateStudent(const Student& s);    //by reference

这样就没有构造和析构函数被调用(const 使用见条款03).

by reference方式传递参数还可以避免切割(slicing)问题.
切割问题:derived class对象以by balue传递被视为了base class对象,造成base class的copy构造函数被调用,而derived class的性质被切割掉,进而只留下一个base class对象)

class Windoe{
public:
   ...
   std::string name() const;
   virtual void display() const;
};
class WindowWithScrollBars:public Window{
public:
   ...
   virtual void display() const;
};
void printNameAndDisplay(Window w){      //by value-不正确,参数别切割
   std::cout << w.name();
   w.display();
}
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);

我们传递一个WindowWithScrollBars 对象,而printNameAndDisplay以passed by value接受一个Window 对象,从而wwsb的WindowWithScrollBars性质被切割掉,所以printNameAndDisplay内调用的display总是Window::display,绝不会是WindowWithScrollBars ::display.

所以我们以by reference-to-const方式传递w

void printNameAndDisplay(const Window& w){      //by reference,不被切割
   std::cout << w.name();
   w.display();
}

这里说一下,通过底层发现reference往往以指针实现出来,因此pass by reference通常意味着传递的是指针。对于内置型对象,pass by value往往比pass by reference效率高点,这点也适用与STL的迭代器和函数对象,习惯上它们都被设计为pass by value。
但是并不是因为内置型小而选择pass by value,这是不对的,对象小并不意味着其构造函数昂贵,这点我们需要记住!!

请记住:
-尽量以pass by reference-to-const替换pass by value。前者通常比较高效,也可以避免切割问题
-以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass by value往往比较合适

条款21:必须返回对象时,别妄想返回其reference(Don’t try to return a reference when you must return an object)

pass by reference却是有很多优点,但是有时候却不能用,是一定不能用!!

class Rational{
public:
   Rational(int numerator = 0,int denominator = 1);
   ...
private:
   int n,d;                  //分子(numerator)和分母(denominator)
   friend const Rational& operator* (const Rational& lhs,const Rational& rhs);
};

返回一个reference,必须自己创建个Rational对象,函数创建新对象有两个途径:在stack空间或在heap空间

stack空间上

const Rational& operator* (const Rational& lhs,const Rational& rhs){
   Rational result(lhs.n * rhs.n,lhs.d * rhs.d);     //警告!!很糟糕!
   return result;
}

函数返回的reference指向result,而result是一个local对象,local对象在函数退出时提前被销毁,这将很糟糕!!!(对于指针返回一个local对象也是如此)

heap空间上(Heap-based对象由new创建):

const Rational& operator* (const Rational& lhs,const Rational& rhs){
   Rational* result = new Rational(lhs.n * rhs.n,lhs.d * rhs.d);  //警告!!更糟糕!
   return *result;
}

我们不仅需要代价用构造函数来完成初始化,同时还要面临new出来的对象谁来实施delete(这是必须的!!,条款16)
所以不论是on-the-stack还是on-the-heap,都是糟糕的。如果我们使用一个static Rational对象会是如何呢?

const Rational& operator* (const Rational& lhs,const Rational& rhs){
static Rational result;                   //警告!!
result=...;
return result;
bool operator==(const Rational& lhs,const Rational& rhs);
Rational a,b,c,d;
...
if((a*b)==(c*d)){
   ...
}else{
   ...
}

这时(ab)==(cd)总被核算为true,不论a,b,c,d为何值!!

if(operator==(operator*(a,b),operator*(c,d)))      //if((a*b)==(c*d))

operator==调用前,先调用operator*,而operator*调用确实改变了值,但是static 返回的都是reference,使用调用端看到的永远是static后面的值

一个"必须返回新对象"的函数正确写法是(或者其他本质等价的代码):

inline const Rational operator* (const Rational& lhs,const Rational& rhs){   //by value
   return Rational(lhs.n * rhs.n,lhs.d * rhs.d);
}

请记住:
-绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象

条款22:将成员变量声明为private(Declare data members private)

一个最主要的原因就是–>封装

那为什么不可以声明为protected?因为protected虽然对于"用户"来说是透明的,但是对derived class而言却是public,这样还是没有达到封装的效果。
假设我们有一个public成员变量,我们如果取消了它,那是使用它的客户码都将被破坏,那是一个不可知的大量;而我们取消一个protected成员变量,所以使用它的derived class都将被破坏,相比于public成员变量破坏的代码量的确小点,但那还是一个不可知的大量,所以从封装的角度观之只有两种:private(提供封装)和其他(不提供封装)

请记住:
-切记将成员变量声明为private,这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性
-protected并不比public更具封装性

条款23:宁以non-member、non-friend替换member函数(Prefer non-member non-friend functions to member functions)

class WebBrowser{
public:
   void clearCache();
   void clearHistory();
   void removeCookies();
   ...
};

调用三个函数比较繁琐,我们用一个函数来调用这三个函数,这里有两个选择

class WebBrowser{
public:
   void clearEverything();       //调用clearCache();clearHistory();removeCookies();
   ...
};
void clearBrowser(WebBrowser& wb){
  wb.clearCache();
  wb.clearHistory();
  wb.removeCookies();
}

现在问题是member函数clearEverythingnon-member函数clearBrowser哪个好?
封装角度看:愈多东西被封住,愈少有人看到它,我们就有愈大弹性去变化它。因此,愈多东西被封装,我们改变那些东西的能力也就愈大。所以non-member函数clearBrowser会是更好的选择。

注意:friend函数对class private成员的访问和member函数相同。

还有一点"成为class的non-member"并不意味着它"不可以是另一个class的member",所以较佳的做法是让其都放在一个namespace内:

namespace WebBrowserStuff{
   class WebBrowser{...};
   void clearBrowser(WebBrowser& wb);
   ...
}

这个做法的优点是:namespace和class不同,namespace可以跨越多个源码文件,而class不能。这样我们不同文件中使用同一个namespace可以设计不同的函数功能

//在webbrowser.h
namespace WebBrowserStuff{
    class WebBrowser{...};
    ...    //核心功能
}
//在webbrowserbookmarks.h
namespace WebBrowserStuff{
   ...     //与书签相关函数
}
...

所以便利函数放在多个头文件中但隶属同一命名空间,客户可以轻松扩展这一组便利函数,这是class无法提供的。

请记住:
-宁可拿non-member non-friend函数替换member函数。这样可以增加封装性、包裹弹性和机能扩充性

条款24:若所有参数皆需类型转换,请为此采用non-member函数(Declare non-member functions when type conversions should apply to all parameters)

class Rational{
public:
   Rational(int numerator=0,int denominator=1);
   int numerator() const;
   int denominator() const;
   const Rational operator* (const Rational& rhs) const;
   ...
};
Rational onrEighth(1,8);
Rational oneHalf(1,2);
Rational result=oneHalf * onrEighth;
result = result * onrEighth;

这些都可以通过,但是出现下面的时可能会出现意外:

result  = oneHalf * 2;            //很好
result  = 2 * oneHalf;            //错误!

我们以函数形式来看:

result  = oneHalf.operator*(2);      //很好   ()
result  = 2.operator*(oneHalf);      //错误!

如果Rational构造函数为explicit,则两个都不能通过!!
我们让operator*成为一个non-member函数:

class Rational{
   ...
};
const Rational operator*(const Rational& lhs,const Rational& rhs){
  return Rational(lhs.numerator()*rhs.numerator(),lhs.denominator()*rhs.denomirator());
}
Rational oneFourth(1,4);
Rational result;
result = oneFourth * 2;
result = 2 * oneFourth;      //很好

我们不可以让operator*成为一个friend函数!!
请记住:
-如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member

条款25:考虑写出一个不抛异常的swap函数(Consider support for a non-throwing swap)

缺省行为下的swap动作由标准程序库提供:

namespace std{
   template<typename T>
   void swap(T& a,T& b){
        T temp(a);
        a=b;
        b=temp;
   }
}

只要类型T支持copying(copy构造函数和copy assignment操作符)就可完成缺省的swap,然而,现在我们选择使用指针去完成–>pimpl手法(pointer to implementation)

class WidgetImpl{
public:
   ...
private:
   int a,b,c;
   std::vector<double> v;
   ...
};
class Widget{       //使用pimpl手法
public:
   Widget(const Widget& rhs);
   Widget& operator=(const Widget& rhs){
      ...
      *pImpl=*(rhs.pImpl);
      ...
   }
private:
   WidgetImpl* pImpl;              //指针,所指对象内含Widget数据
};

通过一个public成员函数,并且将std::swap特化,令它调用该函数

class Widget{
public:
   ...
   void swap(Widget& other){
      using std::swap;               //必须声明
      swap(pImpl,other.pImpl);       //若要置换Widget就置换pImpl指针
   }
   ...
};
namespace std{
   template<>
   void swap<Widget>(Widget& a,Widget& b){
      a.swap(b);
   }
}

现在可以通过编译了。当Widget和WidgetImpl都是class template如下:

template<typename T>
class WidgetImpl{...};

template<typename T>
class Widget{...};

如果我们这样写的话

namespace std{
   template<typename T>
   void swap< Widget<T> >(Widget& a,Widget& b){    //错误!
      a.swap(b);
   }
}

这会造成对function template的偏特化,而c++只允许对class template偏特化,所以这样写无法通过编译,正确写法是重载

namespace std{
   template<typename T>
   void swap(Widget& a,Widget& b){
      a.swap(b);
   }
}

重载function template没有问题,但是std比较特殊,可以全特化std内的template,但不可以添加新的template,所以我们创建一个新的namespace:

namespace WidgetStuff{
   ...
   template<typename T>
   class Widget{...};
   ...
   template<typename T>
   void swap(Widget<T>& a,Widget<T>& b){
      s.swap(b);
   }
}

现在我们有三个版本的swap。一般化版,特化版,和专属版,那么我们使用时到底是使用哪个版本的呢?

template<typename T>
void doSomething(T& obj1,T& obj2){
   using std::swap;         //令std::swap在此函数内可用
   ...
   swap(obj1,obj2);         //为T型对象调用最佳swap版本
   ...
}

当编译器看到对swap的调用时,它们便查找适当的swap调用,首先查找global作用域或T所在namespace内的任何T专属swap,如果T是Widget并位于命名空间WidgetStuff内,编译器会使用专属版,没有就使用std内的swap。
请记住:
-当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常
-如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于class(而非template),也请特化std::swap
-调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”
-为“用户定义类型”进行std template全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值