effective c++(三)

面向对象守则要求数据应该尽可能被封装,东西被封装它就不再可见,越多东西被封装,越少人可以看到它,而越少人看到他,我们就有越大的弹性去变化它,它使我们能够改变事物而只影响有些客户。

只因在意封装性而让函数成为类的非成员函数并不意味着它不可以是另一个类的成员

在c++比较自然的做法是让那些作用函数成为一个非成员函数并且位于类所在的同一个命名空间内。命名空间与类不同,前者可以跨越多个源码文件而后者不能。并且没有理由让毫无相关性的函数产生编译相依关系,因此分离它们的最直接做法就是将不同的无关的函数声明于不同的头文件,都在同一个命名空间内。如:

头文件"webbrowser.h" 针对class WebBrowser自身
//及WebBrowser核心机能
namespace WebBrowserStuff{
 class WebBrowser{...}
 ...                     //核心机能,例如几乎所有客户都需要的
}                        //非成员函数
//头文件'webbrowserbookmarks.h"
namespace WebBrowserStuff{
 ...                      //与。。。相关的便利函数
}
//头文件"webbrowsercookies.h"
namesapce WebBrowserStuff{
 ...
}                         //与...相关的便利函数
...

将所有便利函数放在多个头文件内但隶属同一个命名空间,意味客户可以轻松扩展这一组便利函数,它们需要做的是添加更多非成员函数和非友元函数到此命名空间内。宁可拿非成员函数和非友元函数替代成员函数,这样做可以增加封装性,包裹弹性和机能扩充性。

考虑如下:

class Rational{
 public:
 Rational(int numerator=0,int denominator=1)
 const Rational operator* (const Rational& rhs) const;
 ...
};
result=oneHalf*2;//通过编译    其中有一个隐式转换将2转换为Rational对象
result=2*oneHalf;//编译失败

因为Rational是内含operator*函数的类,所以编译器调用该函数,但是整数2没有对应的operator*成员函数,编译器也会尝试寻找可被一下调用的non-member operator*函数   result=operator*(2,oneHalf);,但是并无这样的函数。因而,只有当参数被列于参数列内,这个参数才是隐式类型转换的合格参与者。

要让上述通过编译需要一个非成员函数:

const Rational operator*(const Rational& lhs,const Rational& rhs)
{
 return Rational(lhs.numerator()*rhs.numberator(),
         lhs.denominator()*rhs.denominator())
}

成员函数的反面是非成员函数,不是友元函数,无论何时如果你可以避免友元函数就该避免。

如果你需要为某个函数的所有参数进行类型转换,那么这个函数必须是个非成员函数。

标准程序库提供的swap算法为:

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

针对以指针指向一个对象,内含真正数据的那种类型,称为pimpl手法的类,传统的swap效率不高,例如:

class WidgetImpl{
 public:
  ...
 private:
 int a, b, c
 std::vector<double> v;
};
class Widget{
 public:
 Widget(const Widget& rhs);
 Widget& operator=(const Widget& rhs)
 {
   ...
   *pImpl=*(rhs.pImpl);
   ...
 }
 ...
 private:
 WidgetImpl* pImpl;
};

当置换两个Widget对象值时,唯一需要做的是置换其pImpl指针,因此应该:

class Widget{
 public:
 ...
 void swap(Widget& other)
 {
   using std::swap;
   swap(pImpl,other.pImpl);
 }
 ...
};
namespace std{
 template<>
 void swap<Widget>(Widget& a,Widget& b)
 {
  a.swap(b);
 }
}

这样不只能通过编译,而且与stl容器有一致性,因为所有stl容器也都提供有公有swap成员函数和std::swap特化版本

当我们企图偏特化一个函数模板时是错误的,因为c++只允许对类模板偏特化,在函数模板身上偏特化是行不通的,当你打算偏特化一个函数模板时,通常做法是为它添加一个重载版本:像这样:

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

但是这同样是不合法的,因为客户可以全特化std内的template,但不可以添加新的templates或classes或functions或其他任何东西到std里头,但是我们可以将上述代码放在另一个命名空间内使它合法。

当你写一个函数模板,其内需要置换两个对象值时,应该如此:

template<typename T>
void doSomething(T& obj1,T& obj2)
{
 using std::swap;     //意义在于没有专属的就调用std内的swap版本
 ...
 swap(obj1,obj2);
 ...
}

一旦编译器看到对swap的调用,c++名称查找法则确保将找到global作用域或T所在之命名空间内的任何T专属的swap,如果没有专属的swap存在,就会使用std内的swap,这也是让using让std的swap在函数内曝光,不过编译器是比较喜欢std::swap的T专属特化版,而非一般化的那个template,但是别为调用添加额外的修饰符,那会影响c++挑选适当函数,例如:

std::swap(obj1,obj2);  //错误的swap调用方式

这会强迫编译器只认std内的swap,不再可能调用一个定义于它处的适当T专属版本。

成员版swap绝不可能抛出异常,那是因为swap的一个最好的应用帮助classes提供强烈的异常安全机制保障,当你写下一个自定版本的swap,往往提供的不只是高效置换对象值的方法,而且不抛出异常,因为高效的swap几乎总是基于对内置类型的操作,而内置类型上的操作绝不会抛出异常。

避免函数过早定义变量如:

string encryptPassword(const std::string& passwdor)
{
 ...
 string encrypted;
 if(password.length()<MinimumPasswordLength)
{
 throw logic_error("Password is too short");
 }
 ..return encrypted;
}

如果抛出了异常,这样定义的变量并没有使用,我们还需要承担它的构造成本和析构成本,因此我们应该尽可能延后变量定义式的出现时间,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止(提高效率,避免使用默认的构造函数,然后再赋值)

dynamic_cast主要用来执行安全向下转型,也就是用来决定某对象是否归属继承体系中的某个类型,它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。之所以需要它,通常是因为你想在一个你认定为派生类对象身上执行派生类操作函数,但是你的手上却只有一个指向基类的指针或引用。

任何一个类型转换,不论是通过转型操作而进行的显式转换,或通过编译器完成的隐式转换,往往真的令编译器编译出运行期间执行的代码。

下面这种情况下会有个偏移量在运行期被施于Derived* 指针身上,用以取得正确的Base* 指针值,也说明单一对象可能拥有一个以上的地址,例如以Base* 指向它时的地址和以Derived* 指向它时的地址

class Base{...};
class Derived:public Base{...};
Derived d;
Base* pb=&d;                //隐喻地将Derived* 转换为Base*

对象的布局方式和它们的地址计算方式随编译器的不同而不同,那意味着由于知道对象如何布局而设计的转型,在某一平台行的通,在其他平台并不一定行得通。

考虑如下:

class Window{
 public:
 virtual void onResize(){...}
 ...
};
class SpecialWindow:public Window{
 public:
 virtual void onResize(){
   static_cast<Window>(*this).onResize();
 ...
 }
 ...
};

本来是想通过转型调用window的onResize函数,但是实际上它调用的并不是当前对象上的函数,而是稍早转型动作所建立的一个*this对象的base class成分,它是在当前对象的base class成分的副本上调用window::onResize,然后再当前对象身上执行specialwindow专属动作

为了避免使用dynamic_cast,通常有两种做法,第一种是使用容器并在其中存储直接指向派生类对象的指针,通常是智能指针,然后直接使用指针操作它,第二种是在基类提供纯虚函数然后再派生类中做你想对每个派生类做的事

绝对要避免的是所谓的连串转型,像这样

for(iter=winptrs.being();iter!=winptrs.end();++iter)
{
 if(...*psw1=dynamic_cast<...>(iter->get())
    ...
 else if (...*pws2=dynamic_cast<..>(iter->get()))
 else if(...psw3=dynamic_cast<...>(iter->get())...}

优良的c++代码很少使用转型,我们应该尽可能隔离转型动作,通常是把它隐藏在某个函数内,特别是在注重效率的代码中避免使用dynamic_casts。

考虑如下:

class Rectangle{
 public:
 Point& upperLeft() const{ return pData->ulhc;}
 Point& lowerRight() const{return pData->lrhc;}
..
};

一方面函数被声明为const,因为它们的目的只是为了提取数据,而不是修改,但另一方面两个函数却都返回了引用指向私有内部数据,调用者于是可通过这些引用更改内部数据。虽然ulhc和lrhc都被声明为private,但它们实际上却是public,因为公有函数传出了它们的引用。引用,指针,和迭代器统统都是所谓的handles(号码牌,用来取得某个对象),而返回一个代表对象内部数据的handle,随之而来的便是降低对象封装性的风险,并且,handle被传过去了,一旦如此我们就是暴露在handle比其所指对象更长寿的风险下了,因此避免返回handles指向对象内部。

带有异常安全性的函数需要满足两点,第一步泄露任何资源,第二步步允许数据败坏(数据不合理变化)。异常安全函数提供一下三个保证之1:

  • 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下,没有任何对象或数据结构会因此败坏,所有对象都处于一种内部前后一致的状态
  • 强烈保证:如果异常被抛出,程序状态不改变,如果函数成功,就是完全成功,如果函数失败,程序会回复到调用函数之前的状态。
  • 不抛掷保证:承诺绝不抛出异常,作用于内置类型身上的操作都提供不抛掷保证。

有种空白异常明细,如 int dosomething() throw(),并不是说它绝不会抛出异常,而是说如果抛出异常,将是严重错误,会有属于set_unxepected的函数被调用

shared_ptr::reset函数只有在其参数被成功生成之后才会被调用,而删除原来的对象在其内被使用,成功才删除

std::trl::shared_ptr<Image>bgImage;
bgImage.reset(new Image(imgSrc));

有个一般化的设计策略很典型地会导致强烈保证,很值得熟悉它,这个策略称为copy and swap,为你打算修改的对象作出一份副本,然后再那副本身上做一切必要修改,若有任何修改动作抛出异常,原对象仍保持未改变状态,待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换。

函数提供的异常安全保证通常最高只等于其所调用之各个函数的异常安全保证中的最弱者。

将函数在类声明中就完成定义会使函数成为内联函数,虚函数不可能成为内联函数,因为内联函数意味执行前,先将调用动作替换为被调用函数的本体,同时编译器通常也不对通过函数指针而进行调用实施内联。

如果有个异常在对象构造期间抛出,该对象已构造好的那一部分会被自动销毁

需要将文件间的编译依存关系降至最低,在下面代码中,person定义文件和其含入文件之间形成了一种编译依存关系,如果这些头文件中有任何一个被改变,或这些头文件所依赖的其他头文件有任何改变,那么每一个含入Person class的文库就需要重新编译。

class Person{
 public:
 Person(const string& name,const Date& birthday,const Address& addr);
 ...
 private:
 string theName;          //需要包含它们的头文件
 Date theBirthDate;
 Address theAddress;
};

分离的关键在于以声明的依存性替换定义的依存性,那正是编译依存性最小化的本质,现实中让头文件尽可能自我满足,万一做不到,则让它于其他文件内的声明式而非定义式相依。

#include <string>
#include <memory>
class PersonImpl;
class Date;
class Address;
class Person{                      //像这样使用pimpl idiom的类往往称为handle classes
 public:
 Person(const string& name,const Date& birthday,const Address& addr);
 ..
 private:
 std::trl::shared_ptr<PersonImpl> pImpl;//指针,指向实现物

如果使用对象的引用或对象的指针可以完成任务,就不要使用对象,如果能够,尽量以类声明式替换类的定义式

另外一种方法是使用lnterface class来解决,令Person成为一种特殊的抽象基类,目的是详细一一描述派生类接口

class Person{
 public:
 virtual ~Person();
 virtual string name() const=0;         //需要将接口纯虚化
 virtual string birthDate() const=0;
 virtual string address() const=0;
 static std::trl::shared_prt<Person> create(const string&name,const Date& birthday,const Address& addr);//工厂函数,真正具现化
...
};

从lnterface class 继承接口规格,然后实现接口所覆盖的函数

class RealPerson:public Person{
 public:
 RealPerson(const string& name,const Date& birthday,const Address& addr)
:theName(name),theBirthDate(birthday),theAddress(addr)
{}
private:
 string theName;
 Date theBirthDate;
 Address theAddress;
};
std::trl::shared_ptr<Person> Person::create(const string& name,const Date& birthday,const Address& addr)
{
 return 
    std::trl::shared_ptr<Person>(new RealPerson(name,birthday,addr));
}

Handle class和lnterface classes解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性,当它们导致速度或大小差异过于重大以至于类之间的耦合相形之下不成为关键,就正常操作。

如果你令class D以public形式继承class B,你便是告诉C++编译器,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立,也就是凡是B对象可派上用场的任何地方,D对象一样可以派上用场,类比于人和学生,人的概念比学生更一般化,基类,学生是人的一种特殊形式,派生类。

可以在派生类中实现虚有函数来实现派生类有而基类无的的特殊用途

class Bird{
 ...             //没有fly()函数
};
class FlyingBird:public Bird{
 public:
 virtual void fly();       //声明自己的fly函数
..
};

public继承意味着is-a,适用于基类身上的每一件事情一定也适用于派生类身上,因为每一个派生类对象也都是一个基类对象

派生类内的名称会遮掩基类内的名称,在public继承下从来没有人希望如此,为了让遮掩的名称再见天日,可使用using声明式或转交函数。编译器寻找函数对应的名称从最里面的作用域往外搜索

class Base{
 private:
 int x;
 public:
 virtual void mf1()=0;
 virtual void mf1(int);
 virtual void mf2();
 void mf3();
 void mf3(double);
 ...
};
class Derived:public Base{
 public:
 virtual void mf1();
 void mf3();
 void mf4();
 ..
};
Derived d;
int x;
d.mf1();      //  调用Derived::mf1
d.mf1(x);     //   错误,因为Derived::mf1遮掩了Base::mf1 ,要成功编译可用下面的代码做出改变
d.mf2();   
d.mf3();
d.mf3(x);     //   错误Derived::mf3遮掩了Base::mf3
class Derived:public Base{
 public:
 using Base::mf1;      //让Base class内名为mf1和mf3的所有东西在Derived作用域内都可见
 using Base::mf3;
 ...
 或者
 virtual void mf1(){Base::mf1();}   //转交函数,暗自成为inline
};

猜你喜欢

转载自blog.csdn.net/weixin_38893389/article/details/79503126