effective c++:引用传递与值传递,成员函数与非成员函数

时间:2023-03-08 15:50:45

以pass-by-reference-to-const 替换pass-by-value

考虑以下class继承体系

class Person {
public:
  Person(); // parameters omitted for simplicity
  virtual ~Person(); // see Item 7 for why this is virtual
  ...
private:
  std::string name;
  std::string address;
};
class Student: public Person {
public:
  Student(); // parameters again omitted
  virtual ~Student();
  ...
private:
  std::string schoolName;
  std::string schoolAddress;
};
bool validateStudent(Student s); // function taking a Student
// by value
Student plato; // Plato studied under Socrates
bool platoIsOK = validateStudent(plato); // call the function

本次以by value方式传递一个Student对象会导致一次Student 构造函数、一次Person 构造函数、四次string构造函数,共六次构造函数。当销毁时,同样学要六次的析构函数,可见其效率是如此的低,而使用pass-by-reference-to-const可以有效地回避原本需要的构造,析构带来的性能损耗。

bool validateStudent(const Student& s);

这种方式来传递参数效率高很多,因为没有新的对象被创建,其中的const的作用是保证传入参数在执行时不被修改,而使用by value传递参数要达到这种效果是通过对实参做一个副本,然后在副本上做修改,效率上的优劣是显而易见的。

通过by reference 传递还可以避免对象被切割的问题,当一个派生类对象以By value 传递时被视为了一个基类对象,派生类特有的特性被舍弃掉了

class Window {
public:
  ...
  std::string name() const; // return name of window
  virtual void display() const; // draw window and contents
};
class WindowWithScrollBars: public Window {
public:
  ...
  virtual void display() const;
};
void printNameAndDisplay(Window w) // incorrect! parameter
{ // may be sliced!
  std::cout << w.name();
  w.display();
}
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);

以上代码的本意是在printNameAndDisplay中传递一个wwsb的对象,调用派生类版本的display函数,但由于是值传递,代码执行时实际上运行的是基类的display,解决方法时改为引用传递避免对象被切割:

void printNameAndDisplay(const Window& w) // fine, parameter won’t
{ // be sliced
  std::cout << w.name();
  w.display();
}

pass-by-reference确实在很多时候扮演提高效率,减少错误的角色,但它也不是万能的,以下就举个有理数乘机的例子来说明

class Rational {
public:
  Rational(int numerator = , // see Item 24 for why this
  int denominator = ); // ctor isn’t declared explicit
  ...
private:
  int n, d; // numerator and denominator
  friend
  const Rational // see Item 3 for why the
  operator*(const Rational& lhs, // return type is const
  const Rational& rhs);
};

这个例子中并没有使用引用类型返回是有道理的,引用只是个名称,它代表这一个已经存在的对象,如果我们在上面代码中把返回类型Rational改为Rational&,就相当于我在两个有理数乘积之前已经存在了一个两数相乘的结果,我返回的只是这个结果变量的一个别名,这听起来很傻比,但如果有些情况下返回引用,它的意思就是这样。所以在选择使用引用传递和值传递时要仔细考虑我们到底需要表达的是什么意思。

成员变量应声明为private

如果成员变量都声明为public,会造成每个人都可以读写它,而实际应用中我们并不希望对用户开放所有权限,所以将变量声明为private可以控制每个变量的读写权限,这样也提高了类的封装性

class AccessLevels {
public:
  ...
  int getReadOnly() const { return readOnly; }
  void setReadWrite(int value) { readWrite = value; }
  int getReadWrite() const { return readWrite; }
  void setWriteOnly(int value) { writeOnly = value; }
private:
  int noAccess; // no access to this int
  int readOnly; // read-only access to this int
  int readWrite; // read-write access to this int
  int writeOnly; // write-only access to this int
};

使用非成员函数替代成员函数

假设有个class来表示浏览器,它其中需要实现清除缓存,历史记录,cookies.

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

用户需要整个执行所有动作,因此浏览器也提供这样的功能

class WebBrowser {
public:
  ...
  void clearEverything(); // calls clearCache, clearHistory,
  // and removeCookies
  ...
};

以上是成员函数的实现方案,非成员函数则可以写成这样。

void clearBrowser(WebBrowser& wb)
{
  wb.clearCache();
  wb.clearHistory();
  wb.removeCookies();
}

直觉告诉我们成员函数的实现更好些,但可惜的是这个直觉是错误的,面向对象要求数据尽可能的被封装,即越少的人可以看到内部数据,封装性越好,所以显然在这里我们选择非成员函数。

像WebBrowser这样的类所提供的功能肯定有很多,我们让整个核心机能组成一个类,而对于另外一些功能就没有必要放在同一个文件中,没有理由cookies管理与书签管理产生编译相依的关系

,分离他们的做法就是将各个辅助功能各自声明于不同的头文件,像stl那样<vector>,<algorithm>,<memory>只是属于同一个命名空间,我们可以根据需要来增加头文件,没有必要把整个stl增加进来。

// header “webbrowser.h” — header for class WebBrowser itself
// as well as “core” WebBrowser-related functionality
namespace WebBrowserStuff {
class WebBrowser { ... };
  ... // “core” related functionality, e.g.
  // non-member functions almost
  // all clients need
}
// header “webbrowserbookmarks.h”
namespace WebBrowserStuff {
... // bookmark-related convenience
} // functions
// header “webbrowsercookies.h”
namespace WebBrowserStuff {
... // cookie-related convenience
} // functions
...

非成员函数除了提高封装性外,当遇到参数需要隐式的类型转换,也需要使用非成员函数

class Rational {
public:
  Rational(int numerator = , // ctor is deliberately not explicit;
  int denominator = ); // allows implicit int-to-Rational
  // conversions
  int numerator() const; // accessors for numerator and
  int denominator() const; // denominator — see Item22   const Rational operator*(const Rational& rhs) const;
private:
...
};

以上是个关于有理数操作类,处理两个有理数乘积,看上去设计很合理,但遇到下面代码时就出现了问题。

Rational oneEighth(, );
Rational oneHalf(, );
Rational result = oneHalf * oneEighth; // fine
result = result * oneEighth; // fine
result = oneHalf * ; // fine
result = * oneHalf; // error!

在执行result = oneHalf * 2;这条语句时,onehalf是一个有opeator*函数的对象,"2"则是作为参数传递,编译器直接理解为

const Rational temp(); // create a temporary
// Rational object from 2
result = oneHalf * temp; // same as oneHalf.operator*(temp);

即把‘2’隐式转换成了Rational对象,而当执行result = 2 * oneHalf;时出现错误是"2"不能隐式转换引起的,隐式转换产生的条件是参数必须列于参数列表中,这条规则导致了第二条语句执行错误而第一条语句正确。找到原因之后,我们需要做的是把参与乘法的两个数都作为参数放到参数列表中。

class Rational {
  ... // contains no operator*
};
const Rational operator*(const Rational& lhs, // now a non-member
const Rational& rhs) // function
{
  return Rational(lhs.numerator() * rhs.numerator(),
  lhs.denominator() * rhs.denominator());
}
Rational oneFourth(, );
Rational result;
result = oneFourth * ; // fine
result = * oneFourth; // hooray, it works!