首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 软件管理 > VSTS >

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

2013-10-18 
Effective C++ 第二版 22)传引用 23)返回对象 24)函数重载vs缺省值条款22 尽量用传引用而不用传C语言通过

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

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

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

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

12345678910111213141516171819class Person {public:    Person(); // 为简化,省略参数//    ~Person();...private:    string name, address;}; class Student: public Person {public:    Student(); // 为简化,省略参数//    ~Student();...private:    string schoolName, schoolAddress;};

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

1234Student 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: 避免潜在的开销, 通过引用传递对象;

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

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

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

123456789class 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. 写一个函数打印窗口的名字然后显示;

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

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

12WindowWithScrollBars wwsb;printNameAndDisplay(wwsb);

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

Solution: 通过引用来传递w;

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

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

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

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

 

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

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

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

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

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

12345678910111213class 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*之前有这样一个对象存在是没道理的:

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

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


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

在栈上创建局部对象:

123456// 写此函数的第一个错误方法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) 返回的是局部对象的引用;

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

123456// 写此函数的第二个错误方法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. 

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

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

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

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


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

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

12345678bool 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)

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

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

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

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

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


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

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

12345678void 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(), 作为缺省函数值:

1234567891011int 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的最小可能值;

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

12345template<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函数, 返回可表示为"实例化类型"的最小值;

12345template<class T>void printMinimumValue(){    cout << std::numeric_limits<T>::min();}

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

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

1234#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; 这种情况下必须重载函数:

1234double avg(int a);double avg(int a, int b);...double avg(int a, int b, int c, int d, int e);

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

12345678910111213141516171819// 一个表示自然数的类class Natural {public:    Natural(int initValue);    Natural(const Natural& rhs);private:    unsigned int value;    void init(int initValue);    void error(const string& msg);};inlinevoid Natural::init(int initValue) { value = initValue; }Natural::Natural(int initValue){    if (initValue > 0) init(initValue);    else error("Illegal initial value");}inlineNatural::Natural(const Natural& x) { init(x.value); }

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

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

热点排行