C++幕后故事(三)--程序语义转化

392 阅读6分钟

读者如果觉得我文章还不错的,希望可以多多支持下我,文章可以转发,但是必须保留原出处和原作者署名。更多内容请关注我的微信公众号:cpp手艺人

先来看两段代码执行效率是一样?

//oa的一系列操作...
OptimizationA GetOpt()
{
    OptimizationA oa;
    //oa的一系列操作...
    return oa;
}

void GetOpt(OptimizationA &_result)
{
    // result的一系列操作...
    return;
}

思考:效率是一样的?如果是不一样的,那么又是如何不一样的?那我们如何做效率更好呢?

程序语义的转化

我们自己写的代码,自己看一回事,但是在编译器的角度来看又是一番风景。所以这次我们换个角度来看待问题,分别从初始化操作、优化、成员列表初始化三个方面探究下编译器会怎么翻译我们的代码。

1.初始化操作

A.显式初始化操作

OptimizationA oe;
OptimizationA of(oe);
OptimizationA og = oe;
OptimizationA oh = OptimizationA(oe);
// 编译器的角度看,分成两步走,
// 第一步:定义变量(不会调用初始化操作),第二步:调用拷贝构造
// 1.OptimizationA of (注意此时不会调用OptimizationA的默认构造函数)
// 2.of.OptimizationA::OptimizationA(oe) (调用拷贝构造函数)
// 3.og.OptimizationA::OptimizationA(oe) (调用拷贝构造函数)
// 4.oh.OptimizationA::OptimizationA(oe) (调用拷贝构造函数)

B.参数初始化

void Parameter(OptimizationA oa)
{
}

{
OptimizationA tempoa;
Parameter(tempoa);
} 

// 编译器生成的代码
OptimizationA _tempObj<font>;
// tempObj调用copy构造
tempObj.OptimizationA::OptimizationA(tempoa);
Parameter(tempObj);
// tempObj调用析构函数,销毁对象
tempObj.OptimizationA::~OptimizationA();

C.返回值初始化

OptimizationA GetOpt()
{
    OptimizationA oa;
    return oa;
}

// 此为编译器的生成的函数,分为两步操作
// 第一步:将上面的函数重写为下面的带引用参数的形式
void GetOpt(OptimizationA &_result)
{
    OptimizationA oa;
    //oa的一系列操作。。。。。。
// 第二步:在return返回之前,调用result的copy 构造函数
    result::OptimizationA::OptimizationA(oa);
    return;
}
// 下面是编译器生成的调用代码
// 1.形式转换成这样
OptimizationA result;
GetOpt(result);

// 2.如果用户调用了类成员函数
GetOpt().GetHello();
// 编译器则转换成这样
(GetOpt(result), result).GetHello();

// 3.如果是用户定义了函数指针
OptimizationA (*pf)();
pf = GetOpt; // 没有参数
// 编译器则转换成这样
void (*pf)(OptimizationA &);
(pf(result), result).GetHello();

2.优化

A.用户层面优化

// 程序员的未优化
OptimizationA GetOpt(const T &y, const T &x)
{
    OptimizationA oa(x, y);
    // oa其他操作
    return oa;
}
// 在linux上测试需要关闭优化选项
// 先是生成了一个临时对象tempobj,然后调用tempobj的拷贝构造函数,将oa的数据拷贝到
// tempobj中,然后在调用oa的析构函数。
// 这个过程中消耗了一个tempobj的拷贝构造和析构函数

// 程序员优化,这样做就少了一个临时对象的生成和销毁
OptimizationA GetOpt(const T &x, const T &y)
{
    return OptimizationA(x, y);
}
未优化代码 优化代码
Linux上关闭优化选项结果:
compiler:1 level:2 call ctor
compiler:2 level:3 call copy ctor
compiler:1 level:2 call dtor
compiler:3 level:4 call copy ctor
compiler:2 level:3 call dtor
compiler:3 level:4 call dtor
Linux不关闭优化选项:
compiler:1 level:2 call ctor
compiler:1 level:2 call dtor
windows上:
compiler:1 level:2 call ctor
compiler:2 level:3 call copy ctor
compiler:1 level:2 call dtor
compiler:2 level:3 call dtor
Linux:
compiler:1 level:2 call ctor
compiler:1 level:2 call dtor
在windows上:
compiler:1 level:2 call ctor
compiler:1 level:2 call dtor

B.编译器优化

// 程序员写的代码
OptimizationA GetOpt()
{
    OptimizationA oa;
    return oa;
}

// 编译器生成的代码:(named return value (NRV))
// 分为两步操作
// 第一步:将上面的函数重写为下面的带引用参数的形式
void GetOpt(OptimizationA &_result)
{
    OptimizationA oa;
    //oa的一系列操作...

    // 第二步:在return返回之前,调用__result的copy 构造函数
    __result::OptimizationA::OptimizationA(oa);

    return;
}

3.成员列表初始化

先来看段代码:

class InitialzationB
{
public:
//  InitialzationB()
//  {}

    InitialzationB(int value):  m_IA(value), m_a(value), m_b(value)
        /*
            放在初始化列中……
            1.如果是在成员列表初始化,站在编译器的角度看
            m_IA.InitialzationA::InitialzationA(value)
        */
    {
        /*
            放在构造函数中…..
            m_IA = value;
            2.如果是在函数内部初始化,站在编译器的角度看
            A.先是生成一个临时对象
            InitialzationA oc;
            oc.InitialzationA::InitialzationA(value);
            B.在m_IA的copy ctor
            m_IA.InitialzationA::InitialzationA(oc);    
            C.临时对象再去销毁
            oc.InitialzationA::~InitialzationA();
            所以成员变量初始化会提高效率,但只针对类类型变量,对基本类型无影响。
            在初始化列表中,不要用类成员变量去初始化另外一个成员变量
        */
    }

private:
    InitialzationA m_IA; // 自定义class
    int m_a;
    int m_b;
};

A.成员列表初始化含义

InitialzationB(int value): m_IA(value), m_a(value), m_b(value) 这就是初始化列表的调用方法

B.为什么需要初始化列表,以及初始化列表调用时机

简单来说为了初始化对象时的效率。看上面的代码第7行放在初始化列中,从编译器的角度看就是直接调用了InitialzationA的构造函数。但是你如果放在16行,那么在编译器的角度看就是先生成了一个InitialzationA临时对象,在调用m_IA的copy构造函数,然后临时对象的消亡调用析构函数。所以大费周章的构造对象造成效率的下降。 调用时机:编译器会在构造函数之前会插入一段额外的代码,这就是initialization list。然后在执行用户写的代码。

C.注意事项

A.有四种情况必须放到初始化列表中
1. 成员变量是个引用
2. 成员变量是const类型
3. 成员变量是带参数的构造函数类类型
4. 基类有带参数的构造函数
B.初始化列表的初始化顺序

初始化顺序是按照在类中的声明顺序的来决定。所以在类的初始化列表中还是严格按照类中声明的顺序来复制。

比如:

class InitialzationB
{
public:
//  InitialzationB()
//  {}

// InitialzationB(int value):  m_IA(value) , m_b(value), m_a(m_b)
// 正宗做法
InitialzationB(int value): m_IA(value), m_a(value), m_b(value)
{
}

private:
    InitialzationA m_IA; 
int m_b;
    int m_a;
};
C.在初始化列表中调用成员函数

不要在初始化列表中调用成员函数,因为你不知道这个函数以后会多么的依赖当前的对象。

总结:

现在我们开始回答上面提出的问题,第一个方法至少消耗了一个ctor,copy ctor, dtor,同时还要考虑编译器的实现,中间可能还会temp object的生成,又会增加一个copy ctor,dtor。反过来再看方法二只消耗了ctor,dtor。效率肯定比方法一高。 知道了编译器做了什么,和怎么做的。这将有助于对C++语言背后的实现细节更了若指掌,才能写出高效的程序。同时也看出来c++为了追求效率,背后做了很多我们不知道的事情。最后假如我们是编译器,我们会如何生成代码的?这是值得我们思考的地方。