Effective C++ 第二版 22)传引用 23)返回对象 24)函数重载vs缺省值

56 篇文章 0 订阅

条款22 尽量用传引用而不用传值

C语言通过传值实现, C++继承传统把它作为默认方式, 除非明确指定, 函数的形参总会通过"实参的拷贝"来初始化, 函数的调用者得到的也是函数返回值的拷贝;

"通过值传递对象"的含义是由对象的拷贝构造函数定义的, 这使得传值操作变得很费事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class  Person {
public :
     Person();  // 为简化,省略参数
//
     ~Person();
...
private :
     string name, address;
};
 
class  Student:  public  Person {
public :
     Student();  // 为简化,省略参数
//
     ~Student();
...
private :
     string schoolName, schoolAddress;
};

定义一个returnStudent, 参数Student, 返回Student:

1
2
3
4
Student returnStudent(Student s) {  return  s; }
//...
Student plato;  // Plato(柏拉图)在Socrates(苏格拉底)门下学习
returnStudent(plato);  // 调用 returnStudent

首先, 调用了Student的拷贝构造函数将s初始化为plato; 然后再次调用拷贝构造将函数返回值对象初始化为s; 接着s的析构函数被调用; 最后returnStudent返回值对象的析构函数被调用; 这个什么也没做的函数的成本是两个Student的拷贝加析构;

Student对象中有两个string, 每次构造一个Student就必须构造两个string对象; Student是从Person继承的, 每次构造一个Student对象也必须构造一个Person对象; Person内部还有两个string对象...[ - -!], 传值的开销是: 6个构造和6个析构; 两次传值(参数+返回值)就是12个构造, 12个析构;


有些编译器能优化拷贝构造函数的调用, 但还是要对传值造成的开销有所警惕;

Solution: 避免潜在的开销, 通过引用传递对象;

1
const  Student& returnStudent( const  Student& s){  return  s; }

>没有新对象被创建, 没有构造或析构被调用;

另外一个优点: 避免了'切割问题' slicing problem; 当一个派生类对象作为基类的对象被传递时, 派生类对象的新特性会被切割掉, 变成一个简单的基类对象, 和预期的不符;

1
2
3
4
5
6
7
8
9
class  Window {
public :
     string name()  const // 返回窗口名
     virtual  void  display()  const // 绘制窗口内容
};
class  WindowWithScrollBars:  public  Window {
public :
     virtual  void  display()  const ;
};

>每个Window对象可以得到自己的名字-name(); 每个窗口可以被显示-display(); display()是virtual的, 意味着简单的Window基类对象display的方式和WindowWithScrollBar不同;

e.g. 写一个函数打印窗口的名字然后显示;

1
2
3
4
5
6
// 一个受“切割问题”困扰的函数
void  printNameAndDisplay(Window w)
{
     cout << w.name();
     w.display();
}

当用WindowWithScrollBars对象来调用这个函数时:

1
2
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);

参数w将会作为一个Window对象被创建(传值), 所有wwsb具有的作为WindowWithScrollBars对象的行为特性都被"切割"掉了; 在printNameAndDisplay内部, w的行为和Window对象一样, 不管当初传导函数的对象类型是什么, 对display的调用总是Window::display而不是WindowWithScrollBars::display;

Solution: 通过引用来传递w;

1
2
3
4
5
6
// 一个不受“切割问题”困扰的函数
void  printNameAndDisplay( const  Window& w)
{
     cout << w.name();
     w.display();
}

>w的行为和传到函数的类型一致, const使得w在函数内部不能修改;

传递引用也会增加复杂性, 最大的一个问题就是别名(条款17); 条款23: 有时不能用引用传递对象;

引用几乎都是通过指针来实现的, 所以通过引用传递对象实际上是传递指针; 如果是一个很小的对象--固定类型e.g. int ---这时传值比传引用更高效;

 

条款23 必须返回一个对象时不要试图返回一个引用

尽可能让事情简单, 但不要太简单 --- 爱因斯坦(据说是 - -!)

C++: 尽可能让程序高效, 但不要过于高效;

Note 传引用可能犯的严重错误: 传递一个并不存在的对象的引用;

e.g. 有理数类, 包含友元函数, 用两个有理数相乘:

1
2
3
4
5
6
7
8
9
10
11
12
13
class  Rational {
public :
     Rational( int  numerator = 0,  int  denominator = 1);
...
private :
     int  n, d;  // 分子和分母
friend  const  Rational operator*( const  Rational& lhs,  const  Rational& rhs)  // 参见条款21:为什么返回值是const
};
//...
inline  const  Rational operator*( const  Rational& lhs,  const  Rational& rhs)
{
     return  Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

>这个operator*是通过传值返回对象结果;

引用是一个名字, 一个对已经存在的对象起的名字; 无论何时看到一样引用的声明, 就要问自己: 他的另一个名字是什么? operator*要返回一个引用, 那他返回的必然是某个已经存在的Rational对象的引用, 这个对象包含了两个对象相乘的结果;

在期望调用operator*之前有这样一个对象存在是没道理的:

1
2
3
Rational a(1, 2);  // a = 1/2
Rational b(3, 5);  // b = 3/5
Rational c = a * b;  // c 为 3/10

>对于这样的代码, 期待已经存在一个值为3/10的有理数是不现实的; 如果operator*要返回这样一个数的引用, 就必须自己创建这个数的对象;


一个函数有两种方法创建新对象: 栈stack或堆heap;

在栈上创建局部对象:

1
2
3
4
5
6
// 写此函数的第一个错误方法
inline  const  Rational& operator*( const  Rational& lhs,  const  Rational& rhs)
{
     Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
     return  result;
}

否决的原因: 1) result对象增加了一次构造; 2) 返回的是局部对象的引用;

在堆上创建对象返回引用:

1
2
3
4
5
6
// 写此函数的第二个错误方法
inline  const  Rational& operator*( const  Rational& lhs,  const  Rational& rhs)
{
     Rational *result =  new  Rational(lhs.n * rhs.n, lhs.d * rhs.d);
     return  *result;
}

1) 构造函数的开销; 2)  创建的对象无法delete; 实际上这是一个内存泄露, 即使要求operator*的调用者去取得函数返回地址delete(条款31), 但有些复杂表达式会产生没有名字的临时值: e.g. 

1
Rational w, x, y, z;  w = x * y * z;

>有两个对operator*的调用产生了没名字的临时值, 无法删除;

在函数内部定义静态Rational对象:

1
2
3
4
5
6
7
// 写此函数的第三个错误方法
inline  const  Rational& operator*( const  Rational& lhs,  const  Rational& rhs)
{
     static  Rational result;
// 将要作为引用返回的 静态对象 lhs 和rhs 相乘,结果放进result;
     return  result;
}


>实际实现上面的伪代码时会发现, 不调用一个Rational的构造函数的话, 是不可能给出result的正确值的; [对于现有的接口而言]

即使实现了上面的代码, 这个错误的设计导致的结果:

1
2
3
4
5
6
7
8
bool  operator==( const  Rational& lhs,   const  Rational& rhs);   // Rationals 的operator==...
Rational a, b, c, d;
...
if  ((a * b) == (c * d)) {
     //处理相等的情况;
else  {
     //处理不相等的情况;
}

>((a*b) == (c*d))会yon永远为true, 不管a b c d是什么值 [最后比较的是static变量自己]

等价函数形式: if (operator==(operator*(a, b), operator*(c, d))); 当operator==被调用时, 有两个operator*被调用, 都返回operator*内部的静态Rational对象的引用; 上面的语句实际上是请求operator==对operator*内部的静态对象的值和自己比较; (停止思考静态数组这样的方式, 数组会增加实例开销, 降低程序性能, 在operator*这样的函数思考返回引用是浪费时间, 本来想优化optimeization, 反而变成差化pessimization)

所以, 写一个必须返回新对象的函数的正确方法就是让函数返回对象;

1
2
3
4
inline  const  Rational operator*( const  Rational& lhs,  const  Rational& rhs)
{
     return  Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

>用"operator*返回值构造和析构带来的开销"的代价换来正确的程序运行; [正确性是第一位的]

C++允许编译器采用优化措施来提高代码性能, 所以在某些场合operator*的返回值会被安全地除去; 当编译器(当前大多数支持)优化时, 程序运行速度会比你预计的要快;

Note 当需要在返回引用和返回对象间做决定时, 选择正确的那个, 开销由编译器去优化;


条款24 在函数重载和设定参数缺省值间慎重选择

会对函数重载和设定参数缺省值产生混淆的原因在于, 他们都允许一个函数以多种方式被调用:

1
2
3
4
5
6
7
8
void  f();  // f 被重载
void  f( int  x);
f();  // 调用 f()
f(10);  // 调用f(int)
void  g( int  x = 0);  // g 有一个
// 缺省参数值
g();  // 调用 g(0)
g(10);  // 调用 g(10)

一般来说, 如果可以选择一个合适的缺省值并且只用到一种算法, 就使用缺省参数; 否则使用函数重载;

e.g. 计算5个int最大值的函数, 使用了std::numeric_limits<int>::min(), 作为缺省函数值:

1
2
3
4
5
6
7
8
9
10
11
int  max( int  a,
int  b = std::numeric_limits< int >::min(),
int  c = std::numeric_limits< int >::min(),
int  d = std::numeric_limits< int >::min(),
int  e = std::numeric_limits< int >::min())
{
     int  temp = a > b ? a : b;
     temp = temp > c ? temp : c;
     temp = temp > d ? temp : d;
     return  temp > e ? temp : e;
}

std::numeric_limits<int>::min()是C++标准库中的方法, 表示在C中已经定义的INT_MIN宏(<limits.h>), 处理C++源代码的编译器所产生的int的最小可能值;

假设写一个函数模板, 参数固定为数字类型, 模板产生的函数可以打印用"实例化类型"表示的最小值:

1
2
3
4
5
template < class  T>
void  printMinimumValue()
{
     cout << 表示为T 类型的最小值;
}

如果只是使用<limits.h>和<float.h>会比较困难, 因为不知道T是什么, 所以不知道该打印INT_MIN还是DBL_MIN或者其他类型的值;

为了避开这类困难, 标准C++库在<limits>中定义了类模板numeric_limits, 这个类模板本身定义了一些静态成员函数; 每个函数返回的是"实例化这个模板的类型"的信息; numeric_limits<int>中函数返回的信息是关于int类型的, numeric_limits<double>中函数返回的信息是关于double类型的; numeric_limits中有min函数, 返回可表示为"实例化类型"的最小值;

1
2
3
4
5
template < class  T>
void  printMinimumValue()
{
     cout << std::numeric_limits<T>::min();
}

>看似numeric_limits的方法表示"类型相关常量"开销大, 其实源代码的冗长语句不会产生带目标代码[库]中;

实际上numeric_limits的调用不产生任何指令, 查看numeric_limits<int>min的简单实现:

1
2
3
4
#include <limits.h>
namespace  std {
     inline  int  numeric_limits< int >::min()  throw  () {  return  INT_MIN; }
}

>函数声明为inline, 调用时函数体代替函数(条款33); 它只是个INT_MIN, 本身仅仅是个简单的#define, 是"实现时定义的常量"; 

因此max函数看起来对每个缺省参数进行了函数调用, 其实只不过是用了简单的方法来表示类型相关常量; C++标准库中有很多这样的高效巧妙的应用(条款49);

max函数的关键是: 不管调用者提供几个参数, max计算采用相同(低效率)的算法; 函数内部不必在意哪些参数是外部输入的, 哪些是缺省的; 而且所选用的缺省值不影响算法的正确性; 所以这里缺省方案可行;


对很多函数来说, 找不到合适的缺省值; e.g. 写一个函数计算可多达5个int的平均值; 这里无法使用缺省函数, 因为函数的结果取决于传入的参数个数: 传入n个值, 总数要处以n; 这种情况下必须重载函数:

1
2
3
4
double  avg( int  a);
double  avg( int  a,  int  b);
...
double  avg( int  a,  int  b,  int  c,  int  d,  int  e);

另一种必须使用重载函数的情况是: 完成一项特殊的任务, 但算法取决于给定的输入值; 这种情况对于构造函数很常见: "缺省"构造函数是凭空(无输入)构造对象, 拷贝构造函数是根据一个已存在的对象构造一个对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 一个表示自然数的类
class  Natural {
public :
     Natural( int  initValue);
     Natural( const  Natural& rhs);
private :
     unsigned  int  value;
     void  init( int  initValue);
     void  error( const  string& msg);
};
inline
void  Natural::init( int  initValue) { value = initValue; }
Natural::Natural( int  initValue)
{
     if  (initValue > 0) init(initValue);
     else  error( "Illegal initial value" );
}
inline
Natural::Natural( const  Natural& x) { init(x.value); }

>输入为int的构造必须执行错误检查, 拷贝构造不需要, 因此需要2个不同的函数重载; 两个函数都必须对新对象赋初始值; 

写一个包含两个构造函数公共代码的私有成员函数init来解决重复代码的问题; 在重载函数中调用一个"为重载函数完成某些功能"的公共的底层函数的方法很常用(条款12);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值