• C++11核心知识点(长期更新) —— 六个类方法、常量、右值和move语义相关...


    起源:重载引起的问题

    春节没事闲下来记录一下,对问题做一个汇总

    普通const和non-const的重载选择

     如图所示,遇到类似问题,编译器会做出重载选择。const接收的范围比non-const范围大。

    临时对象的重载选择

    标题一 测试

    问题提出:string的引用

    对于函数形参,可以使用引用或者const引用,这两者应该耳熟能详,一般来说,const引用可接受的范围更大,包括临时对象字面量。参考下这个例子:

    一般来说,"hello c++"这种字面量值被编译成一个常量,在调用print_value()时,错误明确提示。因此std::string &v需要改成:const std::string &v,这说明一个事情,类似这种处理字面量或者临时对象时,需要改成const引用接收。
    问题似乎解决了,但是我们可以看出,这里存在一个问题:如果想对原值进行修改,但是由于是const引用,因此需要对原值进行复制一份才能修改。

    在解决上面的问题之前,再探究下几个小问题:
    一、"hello c++"这个串是存在于内存的,但是很明显,编译器是不想让用户来操作。我们也认为它是一个常量,现在一个问题是能不能将这个"hello c++"找出来重新打印一次呢? 下面是设计的一个代码:

    如图所示,这里试图使用v.c_str()来取内存中的地址,然后再返回给temp_ptr,设计的好像很精巧没问题。但,结果却大跌眼镜,printf什么也没打印出来,这是为啥呢?下面将debug运行起来,查看图示如下:

    如图所示,此时得到字符串地址,内存显示的也很正确,继续往下运行。

     

    运行到此处时,出现了一个非常大的问题,编译器将字符串地址向前移动了一字节,虽然temp_ptr和s的值都未改变,但是已经不再指向字符串首地址!
    这说明,编译器在背后想尽办法,就是不让用户得到这个地址对字符串进行操作,怎么办呢?

    简化问题:使用int右值引用

    下面先把问题简化下,使用一个int的引用试下。

    这个代码的设计就是达成一个目的,把内存中的右值20修改成21,同时出现一个新符号 int &&v表示是右值引用。根据一般认知,右值究竟在内存哪里是神秘未知的,而且很多情况下不需要关心,因为它的存在多半是因为字面量或临时值。对了更清楚对照,下面贴一张汇编的图示:

    这个图更清楚的说明了情况,return v由于是左值引用,因此执行的汇编为:mov eax,dword ptr[v],这个v因为是对20的引用(右值),我们不管它是左值引用右值引用,反正它是一个引用,这个引用本身也是占空的,类比于int *p =xx中的p
    因此,汇编将[v]中的值返回,其实这个值是一个地址,也正是20的地址0x0081fc4c。由于得到地址,因此后面可以对temp_ptr进行加减操作,从而改变了内存中20的值。

    标题三:

    简化问题:使用普通类右值引用

    下面再使用一个普通类进行测试,代码如下:

    这次和int一样,也是可以正确得到结果。测试到这里只能给出一个结论:
    编译器对string进行特殊对待了,即进行特殊处理。接收的左边只能是string而不是string &,这样产生一份复制。(关于这个问题以后再细致测试,暂时写到这)

    【重点】函数返回值

    函数返回值是众多问题最集中的地方,虽然实际用处不大,但是这里做个纯理论的研究,模型如下:

    形式如下:

    T为对象,S为引用,不产生临时对象

    注意的问题

    一、由于对象是上分配的,因此没有任何安全,引用返回也无意义可言,编译器警告:返回局部变量或地址。
    二、接收者也有两种情况,如果是引用则继续引用栈上对象;如果是值,等价于值 <- 引用,因此会执行复制构造

    总体概括

    一、在栈上分配一个对象
    二、函数结束,对象执行析构
    三、将该对象的地址返回

    从这三步执行流程可以看出,该操作是极度危险的!

    T为对象,S为对象,产生临时对象

    需要注意的问题如下:

    一、test_user()中产生的临时对象会执行析构操作
    二、临时对象属于右值,这里就出现了右值的概念,那么它有什么特别之处呢?精彩的问题出现在这里。

    如果一个右值没有执行特别操作,那么右值就会在它产生的地方之后消失。如何理解,参考下面的代码,假如:

    int age =test.user().m_nAge;  //此处右值产生
    ... //此处右值结束

    这个点很难注意到,test_user()产生了一个右值(临时)对象,但是并没有对这个对象做特别处理,因此在后面一句执行之前,这个右值消亡了,因此会执行析构操作。完整如下:

    需要注意的问题:

    一、右值对象究竟能不能存活,主要看左值是否对其进行操作。如果想延长右值的存活期,可以用const左值引用const &,或者右值引用&&。

    T为引用,S为引用,不产生临时对象

    和情形一性质等同,过程如下:

    一、栈上产生对象
    二、函数结束,栈上局部对象无效,产生析构操作
    三、将对象(无效)地址返回

    T为引用,S为对象,产生临时对象

     值 < - 引用,会执行复制构造产生临时对象

    再论右值

    从上面四种情形可以看出,在函数执行中,右值产生的情况就在于产生了临时对象,也可以说这个临时对象就是右值。
    如果这个临时对象被调用者进行了相关操作,比如引用等,右值就延续存活期,如果没有操作,则右值就会消亡,在上面例子中就是执行析构操作

    ◆构造

    类型T包含类型S的指针*S

    对于这个问题,设计的一个简单的模型如下:

    我们把这个模型描述下:类型T包含一个自定义类型*S,参考代码如下:

    /*
        @ 地址:Address类
    */
    class Address {
    public:
        char* val; //资源
        Address() {
            std::cout << "Address构造执行" << std::endl;
        }
        Address(const char* address) {
            this->val = new char[100];
            strcpy(val, address);
        }
        ~Address() {
            std::cout << "Address析构执行" << std::endl;
        }
    };
    /*
        @工人:Worker类
    */
    class Worker {
    public:
        int test_val; //常规类型的测试值
        Address* address;//指针类型
        //Address address;//普通类型
        std::string name;
        double salary;
        Worker(double salary, const char* address) {
            this->address = new Address(address);
            this->salary = salary;
            std::cout << "worker构造执行..." << std::endl;
        }
        ~Worker() {
            std::cout << "Worker析构执行..." << std::endl;
            //delete address;
        }
    };
    
    int main() {
        auto worker = new Worker(2000.45, "广州市天河区");
        std::cout << worker->address->val << std::endl;
        delete worker;
    }

    这段代码主要想说明下面几个问题:
    一、类型包含有指针,指针可以认为就是一个普通值,类比于int,char等,worker使用delete析构之后,内部的成员指针值仅仅是无效的(堆栈退出),所以Address类型根本也不会执行析构!
           除非用delete显式的进行删除,文中注释掉
    二、同理,Address类型也包含一个char*指针,如果不显式的进行delete,实例生命周期结束,内部资源也一样泄漏!

    写这么多,总结成一句话就是:指针仅仅是一种普通值,想自动指望它来删除资源是不可能的,除非显式指定。这似乎是一种常识,但是如果类型T包含的是类型S呢?请看下文 

    类型T包含类型S

    如果T包含类型S(自定义或其它),这里主要说明是类,而不是常规的char,int等,下面设计一个极简的例子说明:

    /*
        @ 地址:Address类
    */
    class Address {
    public:
        Address() { std::cout << "Address构造执行..." << std::endl; }
        ~Address() { std::cout << "Address析构执行..." << std::endl; }
    };
    /*
        @工人:Worker类
    */
    class Worker {
    public:
        Address address; //自动构造和析构
        Worker() {
            std::cout << "worker构造执行..." << std::endl;
        }
        ~Worker() {
            std::cout << "Worker析构执行..." << std::endl;
        }
    };
    
    int main() {
        auto worker = Worker();
    }

    分析:Woker类包含一个成员变量为类类型Address,在执行构造时,会先自动构造address,在析构时会执行address的析构。这种情形已经司空见惯,但还是要认真的提下。为什么要说这个,是因为它给我们一个启示:
    如果一个成员变量为类类型(不是指针),在外层对象析构时,就会自动执行它的析构(普通类型没有这个待遇)
    于是,智能指针就粉墨登场! 假如我们把这个address换成一个智能指针类型,那么就可以实现用它来管理资源

    用智能指针重构

    /*
        @ 使用智能指针重构的代码
    */
    /*
        @ 地址:Address类
    */
    class Address {
    public:
        std::unique_ptr<char> ptr_val; //资源
        Address() { std::cout << "Address构造执行" << std::endl; }
        Address(const char* address) {
            std::cout << "重载参数构造执行..." << std::endl;
            this->ptr_val.reset(new char[100]);
        }
        ~Address() { std::cout << "Address析构执行" << std::endl; }
    };
    /*
        @工人:Worker类
    */
    class Worker {
    public:
        int test_val; //常规类型的测试值
        std::unique_ptr<Address> ptr_address;//地址
        std::string name;//姓名
        double salary;//薪资
        Worker(double salary, const char* address) {
            ptr_address.reset(new Address(address));
            this->salary = salary;
            std::cout << "worker构造执行..." << std::endl;
        }
        ~Worker() {
            std::cout << "Worker析构执行..." << std::endl;
        }
    };
    
    int main() {
        std::unique_ptr<Worker> ptr_worker(new Worker(2000.45,"广州市天河区"));
        /*
            1. ptr_worker生命周期结束
            2. 执行Worker的析构
            3. Worker析构导致ptr_address进行析构
            3. ptr_address析构导致Address()的析构
            4. Address()的析构导致ptr_val的析构
            5. ptr_val最终释放char*指向的堆资源
            6. 所有资源得到释放
        */
    }

    分析:这个代码通过一路使用智能指针进行实例管理,我们可以看到,代码得到很大简化
    指针只能管的到它指向的这一层,对于包含的下一层是无能为力的,也就是说,智能指针能保证它直接管理的那个对象在自己的生命周期结束时,让对象产生析构!这里就显示了智能提针的意义所在!

    尤其对于指针两个字需要特别理解,对于一个普通的栈对象来说,退栈就会销毁,也会执行它的析构方法,但是指针不同,必须要手动进行delete,因此智能指针就管理new出来的对象!看起来是多么朴素的废话,
    可是简单的知识点也需要认真的思考....

    析构

    ◆复制构造

    复制构造也可以重载,一般有下面两种形式

     但是一般不去修改右侧对象,没有理由做这个事儿,因此多使用第一种const的形式。

    通过T(O)的构造形式得到对象

    通过F()函数返回的形式得到对象

    ◆赋值构造

    ◆移动构造

    ◆析构

     

    000

  • 相关阅读:
    jquery获取input的checked属性
    归并排序法
    Pascal's Triangle II —LeetCode
    01背包之求第K优解——Bone Collector II
    Intellij IDEA 14隐藏被排除的文件夹
    LeetCode——Majority Element
    多线程爬虫Java调用wget下载文件,独立线程读取输出缓冲区
    LeetCode——Restore IP Addresses
    LeetCode——Pascal's Triangle
    LeetCode——Permutations
  • 原文地址:https://www.cnblogs.com/tinaluo/p/14399368.html
Copyright © 2020-2023  润新知