第23课 对象的销毁

1. 有趣的问题

(1)程序意图:在Test()中以0作为参数调用Test(int i)来将成员变量mi初始值设置为0.

(2)运行结果:成员变量mi的值为随机值(没达到目的!)

【实例分析】有趣的问题

#include <stdio.h>

class Test
{
private:
    int mi;
public:

    //带参构造函数
    Test(int i)
    {
        mi = i;
     }

    //不带参构造函数
    Test()
    {
        Test(0);//程序的意图是把Test当成普通函数来使用以达到对mi赋值的目的但直接调用构造函数,会将产生临时对象。所以Test(0)相当于
                //对新的临时对象的mi赋初值为0,而不是对这个对象本身mi赋值
     }

    void print()
    {
        printf("mi = %d\n", mi);
    }
};

int main()
{
    Test t;
    t.print(); //mi并没被赋初始,这里会输出随机值

    return 0;
}

2. 临时对象

(1)构造函数是一个特殊的函数,调用构造函数将产生一个临时对象

(2)临时对象的生命期只有一条语句的时间

(3)临时对象的作用域只在一条语句中

(4)临时对象是C++中值得警惕的灰色地带

【编程实验】解决方案

#include <stdio.h>

class Test
{
private:
    int mi;
    //正确的做法,是提供一个用来初始化的普通函数
    void init(int i){ mi = i;}
public:

    //带参构造函数
    Test(int i)
    {
        init(i);
     }

    //不带参构造函数
    Test()
    {
        init(0);//调用普通的初始化函数,而不是带参的构造函数Test(int i);
     }

    void print()
    {
        printf("mi = %d\n", mi);
    }
};

int main()
{
    Test t;
    t.print(); //mi并没被赋初始,这里会输出随机值

    return 0;
}

3. 临时对象与返回值优化(RVO)

(1)现代C++编译器在不影响最终执行结果的前提下,会尽力减少临时对象的产生。

【编程实验】神秘的临时对象

#include <stdio.h>

class Test
{
private:
    int mi;

public:

    //带参构造函数
    Test(int i)
    {
        mi = i;
        printf("Test(int i): %d\n", i);
     }

    //不带参构造函数
    Test()
    {
        mi = 0;
        printf("Test()\n");

     }

    //拷贝构造函数
    Test(const Test& t)
    {
        mi = t.mi;
        printf("Test(cosnt Test& t): %d\n", t.mi);
    }

    void print()
    {
        printf("mi = %d\n", mi);
    }

    ~Test(){printf("~Test()\n");}
};

Test func()
{
    return Test(20);
}

int main()
{

    Test t = Test(10); //==> Test t = 10,临时对象被编译器给“优化”掉了说明:如果不优化,该行代码的行为:调用Test(10)
                       //将产生一个临时对象,并用这个对象去初始化t对象,会先调用Test(int i),再调用Test(const Test& t)

    Test tt = func();  //==> Test tt = Test(20);==>Test tt = 20;
                       //说明:如果不优化,该行代码的行为:在func内部调用Test(20),将产生一个临时对象,此时(Test(int i)被调用,然后按值返回,
                       //会调用拷贝构造函数Test(const Test&)产生第2个临时对象,最后用第2个临时对象去初始化tt对象,将再次调用Test(const Test& t)

    t.print();
    tt.print();

    return 0;
}

//实际输出(优化后)结果
//Test(int i): 10
//Test(int i): 20
//~Test()
//~Test()

(2)返回值优化(RVO)

//假设Test是一个类,构造函数为Test(int i);

Test func()
{
    return Test(2);//若不优化,将产生临时对象,并返回给调用者
}

①在没有任何“优化”之前,return Test(2)代码的行为这行代码中

  先构造了一个 Test 类的临时的无名对象(姑且叫它t1),接着把 t1 拷贝到另一块临时对象 t2(不在栈上),然后函数保存好 t2 的地址(放在 eax 寄存器中)后返回,Func的栈区间被“撤消”(这时 t1 也就“没有”了,t1 的生存期在Func中,所以被析构了),在 Test a = TestFun(); 这一句中,a利用t2的地址,可以找到t2,接着进行构造。这样a的构造过程就完成了。然后再把 t2 也“干掉”。

经过“优化”的结果

  可以看到,在这个过程中,t1和t2 这两个临时的对象的存在实在是很浪费的,占用空间不说,关键是他们都只是为a的构造而存在,a构造完了之后生命也就终结了。既然这两个临时的对象对于程序员来说根本就“看不到、摸不着”(匿名对象),于是编译器干脆在里面做点手脚,不生成它们!怎么做呢?很简单,编译器“偷偷地”在我们写的TestFun函数中增加一个参数 Test&,然后把a的地址传进去(注意,这个时候a的内存空间已经存在了,但对象还没有被“构造”,也就是构造函数还没有被调用),然后在函数体内部,直接用a来代替原来的“匿名对象”,在函数体内部就完成a的构造。这样,就省下了两个临时变量的开销。这就是所谓的“返回值优化”!

③编译器“优化”后的伪代码

//Test a = func(); 这行代码,经过编译优化后的等价伪代码:
//从中可以发现,优化后,减少了临时变量的产生

Test a;   //a只是一个占位符
func(a);  //传入a的引用

void func(Test& t) //优化时,编译器在func函数中增加一个引用的参数
{
      t.Test(2); //调用构造函数来构造t对象
}

4. 小结

(1)直接调用构造函数将产生一个临时对象

(2)临时对象是性能的瓶颈,也是bug的来源之一

(3)现代C++编译器会尽力避开临时对象

(4)实际工程开发中需要人为的避开临时对象

时间: 2024-12-26 20:28:31

第23课 对象的销毁的相关文章

第22课 对象的销毁

1. 析构函数 (1)C++的类中可以定义一个特殊的清理函数,叫析构函数 (2)析构函数的功能与构造函数相反 (3)定义:~ClassName();//注意,无参无返回值:对象销毁时会被自动调用 [编程实验]析构函数的使用初探 #include <stdio.h> class Test { private: int mi; public: Test(int i) { mi = i; printf("Test(): %d\n", mi); } //析构函数 ~Test() {

第19课 - 对象的构造(下)

第19课 - 对象的构造(下) 1. 特殊的构造函数 (1)无参构造函数 当类中没有定义构造函数时,编译器默认提供一个无参构造函数,并且其函数体为空. (2)拷贝构造函数 当类中没有定义拷贝构造函数时,编译器默认提供一个拷贝构造函数,简单的进行成员变量的值复制. 1 #include <stdio.h> 2 3 class Test 4 { 5 private: 6 int i; 7 int j; 8 public: 9 int getI() 10 { 11 return i; 12 } 13

第17课 - 对象的构造

第17课 - 对象的构造(上) 0. 问题 对象中成员变量的初始值是什么? 下面的类定义中成员变量 i 和 j 的初始值是什么?  对象定义在 全局空间.栈上.堆上,具有不同的属性. 1 #include <stdio.h> 2 3 class Test 4 { 5 private: 6 int i; 7 int j; 8 public: 9 int getI() { return i; } // 类成员函数,直接访问 10 int getJ() { return j; } 11 }; 12

[C++]对象的销毁机制

销毁时会按照从后向前的顺序销毁,也就是说,越在后面定义的对象会越早销毁.其中的原因就是函数是在栈中保存的,因此,先定义的对象先压栈,所以在退栈时就会后销毁.而如果参数有多个的话,大多数编译器是从右开始压栈的,也就是参数列表最右边的变量最先压栈,所以参数列表最右边的变量会在最后销毁. 代码如下: 1 #include<iostream> 2 3 using namespace std; 4 5 class Matter { 6 public: 7 Matter(int id) : _identi

QObject::deleteLater()并没有将对象立即销毁,而是向主消息循环发送了一个event,下一次主消息循环收到这个event之后才会销毁对象 good

程序编译运行过程很顺利,测试的时候也没发现什么问题.但后来我随手上传了一个1G大小的文件,发现每次文件上传到70%左右的时候程序就崩溃了,小文件就没这个问题.急忙打开任务管理器,这才发现上传文件的时候,程序内存占用会随着上传进度的增加而增加,上传1G文件的时候内存最多会吃到1.5G,这时候程序申请不到更多内存了,我又没做检查,当然就会崩溃掉. 限制上传文件大小这种事我是不会做的,毕竟一个上传工具占用内存比PS都高实在不科学.注意到文件上传完成之后内存会立即回到正常值,显然原因并不是我忘记释放内存

数据--第23课 - 队列的优化实现

第23课 - 队列的优化实现 1. 顺序队列的瓶颈 顺序队列 线性表的第一个元素作为队头. 线性表的最后一个元素作为队尾. 入队的新元素是线性表的最后,时间复杂度为O(1). 出队列时需要将后续的所有元素向前移动,时间复杂度是O(n). 2. 顺序队列的优化方案 (1)定义front使其始终代表队头的下标. 出队列时对头元素返回,且front++ (2)定义rear使其始终代表队尾下一个元素的下标. 出队时将新的元素插入,且rear++ (3)没有必要只将下标为0的位置定义为队头. (4)顺序队

C++--第23课 - STL简介

第23课 - STL简介 1. 标准模板SLT C++的作用就是提高程序书写的效率,那么就得代码复用. STL,即:Standard Template(样板) Library,是C++的一部分(常用的数据结构).STL是常用数据结构和算法的集合.STL的目标是标准化组件,提高开发效率和程序可靠性. STL库作为为C++的一部分与编译器一同被发布.STL主要由以下3个部分组成: 容器(Container):管理数据的集合 算法(Algorithm):处理集合内的元素 迭代器(Iterator):遍

第23课.神秘的临时对象

1.下面程序输出什么?为什么? #include <stdio.h> class Test { int mi; public: Test(int i) { mi = i; } Test() { Test(0); //直接调用了构造函数,会产生一个临时对象:Test(0)这个构造函数对这个临时对象进行初始化后.就被析构函数销毁了.所以对这个程序而言.这句话没有任何意义.才导致了,打印的随机值. } void print() { printf("mi = %d\n", mi);

第23课 可变参数模板(4)_Optional和Lazy类的实现

1. optional类的实现 (1)optional的功能 ①optional<T>的内部存储空间可能存储了T类型的值,也可能没有.只有当optional被T初始化之后,这个optional才是有效的.否则是无效的.它实现了未初始化的概念. ②optional可以用于解决函数返回无效值的问题.当函数返回一个未初始化的Optional对象时,表明函数正确执行了,只是结果不是有用的值. ③举例:optional<int> op; //未被初始化. optional<int>