Effective C++摘要《第5章:类和函数:实现》20090209

来源:互联网 发布:企业软件正版化 编辑:程序博客网 时间:2024/05/17 02:48

===条款29: 避免返回内部数据的句柄===
假设b是一个const string对象:
class string {
public:
  string(const char *value);        // 具体实现参见条款11
  ~string();                        // 构造函数的注解参见条款m5
  operator char *() const;          // 转换string -> char*;
  ...
private:
  char *data;
};

const string b("hello world");      // b是一个const对象
char *str = b;               // 调用b.operator char*()
strcpy(str, "hi mom");       // 修改str指向的值

通过str修改了const对象的值,问题的原因来自:
// 一个执行很快但不正确的实现
inline string::operator char*() const
{ return data; }

这个函数的缺陷在于它返回了一个"句柄",而这个句柄所指向的信息本来是应该隐藏在被调用函数所在的string对象的内部。
这样,这个句柄就给了调用者自由访问data所指的私有数据的机会

解决办法:
1、将string::operator char*()声明成非const型,这样const对象就不能调用它
但是这种解决方法似乎不合理,因为无论这个对象是否为const,将它转换成char*形式是很合理的事情
2、不返回内部数据句柄,而返回数据拷贝
// 一个执行慢但很安全的实现
inline string::operator char*() const
{
  char *copy = new char[strlen(data) + 1];
  strcpy(copy, data);
  return copy;
}
缺点:速度慢,而且要手工delete掉返回的指针
3、函数返回加const
inline string::operator const char*() const
{ return data; }
这个函数既快又安全,跟stl中string类型中的c_str()返回值一样const char*

===条款30: 避免这样的成员函数:其返回值是指向成员的非const指针或引用,但成员的访问级比这个函数要低===
class address { ... };           // 某人居住在此

class person {
public:
  address& personaddress() { return address; }
private:
  address address;
};

person scott(...);             // 为简化省略了参数
address& addr =                // 假设addr为全局变量
  scott.personaddress();
现在,全局对象addr成为了scott.address的另一个名字,利用它可以随意读写scott.address。
实际上,scott.address不再为private,而是public,访问级提升的根源在于成员函数personaddress。
这个成员函数的做法有违当初将person::address声明为private的初衷
===条款31: 千万不要返回局部对象的引用,也不要返回函数内部用new初始化的指针的引用===
class rational {          // 一个有理数类
public:
  rational(int numerator = 0, int denominator = 1);
  ~rational();
private:
  int n, d;               // 分子和分母
// 注意operator* (不正确地)返回了一个引用
friend const rational& operator*(const rational& lhs,
                                 const rational& rhs);
};
// operator*不正确的实现
inline const rational& operator*(const rational& lhs,
                                 const rational& rhs)
{
  rational result(lhs.n * rhs.n, lhs.d * rhs.d);
  return result;
}

//调用
rational two = 2;
rational four = two * two;         // 同operator*(two, two)
函数调用时将发生如下事件:
1. 局部对象result被创建。
2. 初始化一个引用,使之成为result的另一个名字;这个引用先放在另一边,留做operator*的返回值。
3. 局部对象result被销毁,它在堆栈所占的空间可被本程序其它部分或其他程序使用。
4. 用步骤2中的引用初始化对象four。

// operator*的另一个不正确的实现
inline const rational& operator*(const rational& lhs,
                                 const rational& rhs)
{
  // create a new object on the heap
  rational *result =
    new rational(lhs.n * rhs.n, lhs.d * rhs.d);

  // return it
  return *result;
}
这个方法的确避免了上面例子中的问题,但却引发了新的难题。
如何delete?必须要知道它返回的名字,然后手工去delete
但是下面的将无法delete
rational one(1), two(2), three(3), four(4);
rational product;
product = one * two * three * four;
===条款32: 尽可能地推迟变量的定义===
如果定义了一个有构造函数和析构函数的类型的变量,当程序运行到变量定义之处时,必然面临构造的开销;
当变量离开它的生命空间时,又要承担析构的开销。这意味着定义无用的变量必然伴随着不必要的开销,
所以只要可能,就要避免这种情况发生。
// 此函数太早定义了变量"encrypted"
string encryptPassword(const string& password)
{
  string encrypted;

  if (password.length() < MINIMUM_PASSWORD_LENGTH) {
     throw logic_error("Password is too short");
  }

  进行必要的操作,将口令的加密版本 
  放进encrypted之中;

  return encrypted;
}
改进:
// 这个函数推迟了encrypted的定义,
// 直到真正需要时才定义
string encryptPassword(const string& password)
{
  if (password.length() < MINIMUM_PASSWORD_LENGTH) {
    throw logic_error("Password is too short");
  }

  string encrypted;        // 缺省构造encrypted
  encrypted = password;    // 给encrypted赋值
  encrypt(encrypted);
  return encrypted;
}
改进:用password来初始化encrypted,从而绕过了对缺省构造函数不必要的调用
// 定义和初始化encrypted的最好方式
string encryptPassword(const string& password)
{
  ...                             // 检查长度

  string encrypted(password);     // 通过拷贝构造函数定义并初始化

  encrypt(encrypted);
  return encrypted;
}
你不仅要将变量的定义推迟到必须使用它的时候,还要尽量推迟到可以为它提供一个初始化参数为止。
===条款33: 明智地使用内联===
要牢记在心的一条是,inline指令就象register,它只是对编译器的一种提示,而不是命令。
也就是说,只要编译器愿意,它就可以随意地忽略掉你的指令,事实上编译器常常会这么做。

程序库的设计者必须预先估计到声明内联函数带来的负面影响。
因为想对程序库中的内联函数进行二进制代码升级是不可能的。换句话说,如果f是库中的一个内联函数,用户会将f的函数体编译到自己的程序中。
如果程序库的设计者后来要修改f,所有使用f的用户程序必须重新编译。
相反,如果f是非内联函数,对f的修改仅需要用户重新链接,这就比需要重新编译大大减轻了负担;如果包含这个函数的程序库是被动态链接的,程序库的修改对用户来说完全是透明的。
一般来说,实际编程时最初的原则是不要内联任何函数,除非函数确实很小很简单
===条款34: 将文件间的编译依赖性降至最低===
目的:减少程序重新编译时消耗的时间
将接口从实现分离
class Person {
public:
  Person(const string& name, const Date& birthday,
         const Address& addr, const Country& country);
  virtual ~Person();

  ...                      // 简化起见,省略了拷贝构造
                           // 函数和赋值运算符函数
  string name() const;
  string birthDate() const;
  string address() const;
  string nationality() const;

private:
  string name_;            // 实现细节
  Date birthDate_;         // 实现细节
  Address address_;        // 实现细节
  Country citizenship_;    // 实现细节
};
这里要注意到的重要一点是,Person的实现用到了一些类,即string, Date,Address和Country;
Person要想被编译,就得让编译器能够访问得到这些类的定义。这样的定义一般是通过#include指令来提供的,所以在定义Person类的文件头部,可以看到象下面这样的语句:
#include <string>
#include "date.h"
#include "address.h"
#include "country.h"
*******这样一来,定义Person的文件和这些头文件之间就建立了编译依赖关系!*******
如果任一个辅助类(即string, Date,Address和Country)改变了它的实现(这里指的是修改头文件中的任何地方,哪怕增加一个空行),或任一个辅助类所依赖的类改变了实现,包含Person类的文件以及任何使用了Person类的文件就必须重新编译。
解决办法:
使用类指针("将一个对象的实现隐藏在指针身后")实现Person接口和实现的分离
方法1:句柄类
// 编译器还是要知道这些类型名,
// 因为Person的构造函数要用到它们
class string;      // 对标准string来说这样做不对,
                   // 原因参见条款49
class Date;
class Address;
class Country;
// 类PersonImpl将包含Person对象的实
// 现细节,此处只是类名的提前声明
class PersonImpl;

class Person {
public:
  Person(const string& name, const Date& birthday,
         const Address& addr, const Country& country);
  virtual ~Person();
  ...                               // 拷贝构造函数, operator=
  string name() const;
  string birthDate() const;
  string address() const;
  string nationality() const;
private:
  PersonImpl *impl;                 // 指向具体的实现类
};
现在Person的用户程序完全和string,date,address,country以及person的实现细节分家了
修改string、date、address、country、PersonImpl都不会让包含Person类的文件以及任何使用了Person类的文件重新编译
分离的关键在于,"对类定义的依赖" 被 "对类声明的依赖" 取代了。
Person类仅仅用一个指针来指向某个不确定的实现,这样的类常常被称为句炳类(Handle class)或信封类(Envelope class)
你一定会好奇句炳类实际上都做了些什么。
答案很简单:它只是把所有的函数调用都转移到了对应的主体类中,主体类真正完成工作。例如,下面是Person的两个成员函数的实现:
#include "Person.h"          // 因为是在实现Person类,所以必须包含类的定义
#include "PersonImpl.h"      // 也必须包含PersonImpl类的定义,
                             // 否则不能调用它的成员函数。
                             // 注意PersonImpl和Person含有一样的
                             // 成员函数,它们的接口完全相同
Person::Person(const string& name, const Date& birthday,
               const Address& addr, const Country& country)
{
  impl = new PersonImpl(name, birthday, addr, country);
}
string Person::name() const
{
  return impl->name();
}
方法2:协议类
除了句柄类,另一选择是使Person成为一种特殊类型的抽象基类,称为协议类(Protocol class)。
class Person {
public:
  virtual ~Person();

  virtual string name() const = 0;
  virtual string birthDate() const = 0;
  virtual string address() const = 0;
  virtual string nationality() const = 0;
 
  static Person * makePerson(const string& name,
                             const Date& birthday,
                             const Address& addr,
                             const Country& country);
};
Person * Person::makePerson(const string& name,
                            const Date& birthday,
                            const Address& addr,
                            const Country& country)
{
  //RealPerson是Person的派生类
  return new RealPerson(name, birthday, addr, country);//这种设计类似与设计模式中的工厂方法模式
}
Person类的用户必须通过Person的指针和引用来使用它,因为实例化一个包含纯虚函数的类是不可能的。
//main.cpp
string name;
Date dateOfBirth;
Address address;
Country nation;
// 创建一个支持Person接口的对象
Person *pp = makePerson(name, dateOfBirth, address, nation);
cout  << pp->name()              // 通过Person接口使用对象
      << " was born on "        
      << pp->birthDate()
      << " and now lives at "
      << pp->address();
delete pp;                       // 删除对象

代价:时间+内存
句柄类和协议类分离了接口和实现,从而降低了文件间编译的依赖性。"但,所有这些把戏会带来多少代价呢?"
句柄类:
成员函数必须通过(指向实现的)指针来获得对象数据。这样,每次访问的间接性就多一层。
此外,计算每个对象所占用的内存大小时,还应该算上这个指针。
还有,指针本身还要被初始化(在句柄类的构造函数内),以使之指向被动态分配的实现对象,所以,还要承担动态内存分配(以及后续的内存释放)所带来的开销
协议类:
每个函数都是虚函数,所以每次调用函数时必须承担间接跳转的开销
每个从协议类派生而来的对象必然包含一个虚指针,这个指针可能会增加对象存储所需要的内存数量(具体取决于:对于对象的虚函数来说,此协议类是不是它们的唯一来源)。

最后一点,句柄类和协议类都不大会使用内联函数。使用任何内联函数时都要访问实现细节,而设计句柄类和协议类的初衷正是为了避免这种情况。

 

原创粉丝点击