Effective C++笔记(7)—实现

尽可能延后变量定义式出现的时间

构造和析构一个对象需要有一定的代价,延后变量定义,可以改善效率。
比如说:

string func(string &s)
{
    string t;//方式1.
    if(s.size()==0)
        return string();
    string t;//方式2.
    for(int i=0;i<s.size();++i)
        t+=(s[i]+1);
    return t;
}

如果我们传入一个空串s,在方式1中,定义变量t,那么变量t完全没有被用到,然而却要经历一次构造和析构。

这里只是用到string,如果考虑一个复杂的class类型,默认构造的代价可能更大。

这里面有一个有意思的问题:
变量的定义究竟是放在循环外,还是循环内。
考虑下述两种情况:

//情况A
Foo f;
for(int i=0;i<n;++i)
{
    f=取决于i的某个值;
    //...
}

//情况B
for(int i=0;i<n;++i)
{
    Foo f(取决于i的某个值);
    //...
}

情况A和情况B究竟哪个更高效?
A:经历一次构造析构,但有n次赋值
B:经历n次构造n次析构

这就要具体考虑赋值成本和构造+析构的成本了。


尽量少做转型动作

C风格的转型动作看起来如下:

(T)expression
T(expression)

C++提供了四种新式转型:

const_cast<T>();
dynamic_cast<T>();
reinterpret_cast<T>();
static_cast<T>();

1.const_cast

将对象的常量性移除,即去掉const限定。
详情参考:C++新式转型之const_cast


2.dynamic_cast

主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。
详情参考:C++新式转型之dynamic_cast


3.reinterpret_cast

意图执行低级转型实际动作可能取决于编译器。

reinterpret_cast<new_type>(expression);

这是一个完全的编译器行为,它将expression的二进制序列解释成new_type。正因为他是从二进制的角度做转型,所以他可以“随便搞”,但也比较危险。
详情参考C++新式转型之reinterpret_cast


4.static_cast

用来强迫隐式转换(implict conversions),例如将non-const对象转为const对象,将int转为double。他也可以用来执行上述多种转换的反向转换,例如将void*转换为typed指针,将pointer-to-base转为pointer-to-derived(downcast)。但他无法将const转为non-const(const_cast)
详情参考:C++新式转型之static_cast

本条款中提到尽量少做转型操作举了几个例子:
1.使用static_cast作用于this往往不是作用于真正调用成员函数的对象,而是作用于该对象base域的一个副本。
2.使用dynamic_cast效率较低,之所以要用dynamic_cast因为我们手上有一个base指针,却想调用Derived的操作函数。作者提供了两种思路:一是使用容器并存储指向derived class对象的指针,二是使用虚函数。


避免返回handles指向对象内部成分

书中完整例子:

#include <iostream>
#include <memory>
using namespace std;
class Point{
public:
    Point(int x, int y) :x_(x), y_(y){}
    Point() :x_(0), y_(0){}
    void setX(int x){ x_ = x; }
    void setY(int y){ y_ = y; }
    int & getX(){ return x_; }
    int & getY(){ return y_; }
private:
    int x_;
    int y_;
};

struct RectPoint{

    Point left_up;
    Point right_down;
};

class Rect{
public:
    Rect(Point p1, Point p2) :pdata(new RectPoint())
    {
        pdata->left_up.setX(p1.getX());
        pdata->left_up.setY(p1.getY());
        pdata->right_down.setX(p2.getX());
        pdata->right_down.setY(p2.getY());
    }
    Point &upperLeft()const { return pdata->left_up; }
    Point&lowerRight()const{ return pdata->right_down; }
private:
    shared_ptr<RectPoint>pdata;
};
int main()
{
    Point p1(0, 0);
    Point p2(100, 100);
    const Rect rec(p1, p2);
    rec.upperLeft().setX(10);
    getchar();
    return 0;
}

当返回一个对象引用的时候,尽管成员函数const限定符限定了不能通过this指针修改成员函数,但返回的引用把数据成员暴露出来,也就可以直接调用setX接口,解决的办法是返回一个const reference

避免返回handles指向对象内部,他可以减少空悬指针发生的概率,也可以让const看起来行为就是const。


透彻了解inlining的里里外外

inline看起来像函数,动作像函数,比宏好得多,又可以不需要蒙受调用函数所招致的额外开销。

1.大量使用inline将增加目标码(object code)的大小。

2.inline只是对编译器的申请,并不是强制命令,这种命令可以隐式也可以是显示,其中隐式是在class内部定义函数,显示则是加上关键字inline

class A
{
public:
    void hello(){cout<<"hello"<<endl;}//隐式
};

//显示,you
template<typename T>
inline const T&std::max(const T&a,const T&b)
{return a<b?b:a;}

3.大部分编译器会拒绝太过复杂的函数inline,且所有的virtual函数的调用也不能inline,这很容易理解,virtual表示运行期在决定调用哪个函数,而inline是在编译器确定的。

4.构造函数可以inline,但编译器很可能不进行inline,即使你写了个空的构造函数,这个函数被处理这之后也未必是空的,编译器会在此构造函数中填写有一些内容,所有的基类的初始化,成员变量也要在此初始化如string。你写了空的构造函数,但也许这个构造函数会很大。析构同理。

5.大部分调试器对inline束手无策,因为无法在一个不存在的函数里面设置断点。

6.大多说inline限制在小型的被频繁调用的函数身上。

7.inline解决了宏定义的一些缺点:

用它替代宏定义,消除宏定义的缺点。宏定义使用预处理器实现,做一些简单的字符替换因此不能进行参数有效性的检测。另外它的返回值不能被强制转换为可转换的合适类型,且C++中引入了类及类的访问控制,在涉及到类的保护成员和私有成员就不能用宏定义来操作。

8.内联函数和宏定义的区别:

(1)内联函数在编译时展开,宏在预编译时展开;
(2)内联函数直接嵌入到目标代码中,宏是简单的做文本替换;
(3)内联函数有类型检测、语法判断等功能,而宏没有;
(4)inline函数是函数,宏不是;
(5)宏定义时要注意书写(参数要括起来)否则容易出现歧义,内联函数不会产生歧义;


将文件间的编译依存关系降到最低

接口和实现分离的方式,提供了两个办法:
1.Handle class
2.Interface class
前者就是Pimpl编程技法
后者就是利用纯虚函数来作为抽象接口的方式。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值