首页 > C++中的拷贝初始化

C++中的拷贝初始化

<C++ primer>里面有这么一段话

拷贝初始化不仅在我们用=定义变量时会发生, 在下列情况下也会发生:

  1. 将一个对象作为实参传递给一个非引用的形参

  2. 从一个返回类型为非引用类型的函数返回一个对象

  3. 从花括号列表....

    我为了验证这上面的第二条, 写了如下代码:

#ifndef GEEK_SOLUTION_H
#define GEEK_SOLUTION_H


class A {
public:
    A oneMore(){
        A b;
        return b;
    }

    void test(A a){
        printf("!!!\n");
    }

    A(A& x){
        printf("Copy!\n");
    }

    A(){
        printf("chuangjianle\n");
    }

    ~A(){
        printf("xiaohuile\n");
    }
    A operator=(const A& k){
        printf("aaaa\n");
        return *this;
    }
};
#endif //GEEK_SOLUTION_H

//-----------------------------------------------------

#include <iostream>
#include "Solution.h"

using namespace std;


int main(int argc, char* argv[]) {
    A a;
    A b = a.oneMore();

    printf("1314\n");
    return 0;
}

按理来说输出中应该有copy的, 结果报错, 报错信息如下 :

/Users/zhangzhimin/ClionProjects/geek/main.cpp:9:7: error: no matching constructor for initialization of 'A'
    A b = a.oneMore();
      ^   ~~~~~~~~~~~
/Users/zhangzhimin/ClionProjects/geek/Solution.h:16:5: note: candidate constructor not viable: expects an l-value for 1st argument
    A(A& x){
    ^
/Users/zhangzhimin/ClionProjects/geek/Solution.h:20:5: note: candidate constructor not viable: requires 0 arguments, but 1 was provided
    A(){
    ^

求解答..

新的进展

真是无语了, 按照1L的回复, 我拿vs2015试了下, 居然正常, 真是日了狗了, 不知道这怎么回事...


0x00 总体分析

编译错误因为

  1. 没有添加 <cstdio>,就使用 printf()

  2. 左值右值,const 概念理解不清,写出 A(A& x) 这样的 copy constructor

按道理从oneMore里面出来里面所有的分配的栈上的空间都要被释放掉的... 搞不懂- -, 难道是编译器优化的缘故? 直接就跳过了赋值初始化, 同时不释放那一块在oneMore中指向b的空间, 达到初始化main里面这个b的效果吗?

是因为你对对于编译器优化了解较少,而且 c++ 标准对此没有明确规定。

0x01 左值、右值、const 与 non-const

首先,你去搜索一什么是左值、右值。这是一个首先出现于编译领域的概念,很容易理解。

然后,我们来看下面这两段你写的代码:

    A oneMore(){
        A b;
        return b;
    }
    A b = a.oneMore();

从一个返回类型为非引用类型的函数返回一个对象并用这个对象对另一个同类型对象赋值初始化,确实应该发生两次 copy constructor 的调用,返回对象的时候一次,=初始化的时候一次(不过不一定,见 0x02)。

但是函数oneMore()返回类型是一个右值。右值一般被认为是一个不可被用户代码显式改变的值(除了 c++11 中的右值引用和 move 构造函数机制)仅可以传递给一个左值对象或者const 左值引用,这样才可以保证其内容不被改变。

一个右值如果传递给一个 non-const 左值引用了,那就意味着用户有可能去改变这个右值的值。所以你的编译器提示你 candidate constructor not viable: expects an l-value for 1st argument A(A& x) 。这里 l-value 就是左值的意思。你应该改写成A(const A& a)

如我没记错的话,C++ Primer 应该也有建议创建 copy constructor 的时候一律使用 A(const A& rhs);的形式,避免使用 A(A& rhs)。因为前者能涵盖后者。

具体的左值右值,const 与 non-const 规则,你可以 Search The Fucking Web.

0x02 编译器优化

A b = a.oneMore();,这一个表达式要是不优化,要经历如下步骤:

  1. 进入 oneMore() 函数体,创建要返回的对象 oneMore()::b。(automatic storage, default constructor)

  2. 函数返回,将对象 oneMore()::b 复制初始化给 main()::xxx。xxx是一个临时匿名对象,存储a.oneMore()的返回值。(automatic storage, copy constructor)

  3. A b = xxx 用 xxx 赋值初始化 main()::b (automatic storage, copy constructor)

正好,上面这些 C++ primer 也有讲。
我们来算一算,这短短的一个表达式在不优化的情况下我们消耗了多少计算量:三个 constructor,两个 destructor,这要是一个保存数千 string 的 vector。。。

我们先对变量进行拓扑排序:
oneMore()::b -> xxx -> main()::b
可以很明显地看到(即使编译器也能发现):使用 xxx 的时候 oneMore()::b 就没用了,使用 main()::b 的时候 xxx 就没用了。所以编译器会做一些优化来消除一些不必要的中间步骤计算。

优化的过程和算法就不说了,我们来看优化的结果:最后表达式 A b = a.oneMore()整个计算过程中只有一个变量 main()::b, 所有的操作都作用在了这个变量上。oneMore()::b, xxx 可以说根本没有创建,没创建就意味着不用销毁,这就可以省下两个 constructor 和两个 desctructor。最后这个表达式需要一个 constructor,零个 destructor

既然所有操作都作用在同一个对象上了,那么无论在何处打印这个对象(虽然名字不同)的地址也就是一样了。都是 0x7fff52bfd9a8


您好,您的代码在我的机器上,运行如下,并没有大的问题。

您的第二个关于地址的疑问,见下图。我笑了....

本人的编译环境主要是VC++的,如下

本人再次使用linux g++编译器,对您的程序做了测试。
一点,程序的代码风格出现严重的错误。我修改了不规范的地方,然后才编译通过。
g++ xxxx.cpp
程序运行的结果显示,地址是正确的。只是没有调用拷贝构造函数。具体原因不详。
建议:即使再小的程序,请把代码格式规范一下。


a.oneMore()函数返回的是右值,你需要声明一个这样的拷贝构造函数

    A(const A& x){
        printf("Copy!\n");
    }

谢邀。
实际上我已经回答过好几次楼主的问题了,可能是sf上c++的人不多的原因。。
这里我要说些题外话,关于楼主的学习方法,对于c++来说,如果想要快速理解,写程序是必须的,但分析问题不是这样分析的。
c++是一门相对偏底层的语言,编译结果直接就是二进制,所以我们一般通过汇编了解原理是最快速的方法。
实际上这个问题,几个月我也刚刚研究过。

注:以下代码均处于debug模式下,未经过优化。(优化的情况下,编译器会根据类型的复杂程度,进行参数拆分,堆上的指针不经复制直接返回等,这属于编译器行为,和c++语言本身无关。)

std::string test2()
{
    std::string s="222";
    return s;
}
void main()
{
    std::string s1=test2();
    std::cout<<s1<<std::endl;
}

我们知道,栈上申请的内存一定为随着函数的返回被释放(因为要平栈),所以上面的函数用汇编来分析,伪代码大概如下:

std::string test2()
{
    //里面执行stirng的构造函数
    auto s = new stringObject("123123");
    //dosomething;
    
    //返回前要平栈,但是这个复杂对象又是一个返回值
    //所以要先复制到返回值的地方
    auto s1=new stringObjectForm(s);
    //复制完成后要对本栈内的string析构,进行内存的释放
    delete s;
    return s1;
}

然后返回值这边的s1其实是在里面申请的内存。
至于引用参数传值和上述过程类似,但不同的是,其是先调用方申请内存后传递入被调用方供复制。


另外我还发现了一个奇怪的特性, 就是我尝试分别在oneMore和main里面打印这个b的地址 :

    A oneMore(){
        A b("lisi");
        printf("%p\n", &b);
        return b;
    }
int main(int argc, char* argv[]) {
    A a("zhangsan");
    A b = a.oneMore();
    printf("%p\n", &b);


    printf("1314\n");
    return 0;
}

结果是, 这两个结果一样... 也就是说他们居然指向同一块内存 :

/Users/zhangzhimin/Library/Caches/CLion2016.2/cmake/generated/geek-ef0ba4bc/ef0ba4bc/Debug/geek -wall
zhangsanchuangjianle
lisichuangjianle
0x7fff52bfd9a8
0x7fff52bfd9a8
1314
lisixiaohuile
zhangsanxiaohuile

Process finished with exit code 0

按道理从oneMore里面出来里面所有的分配的栈上的空间都要被释放掉的... 搞不懂- -, 难道是编译器优化的缘故? 直接就跳过了赋值初始化, 同时不释放那一块在oneMore中指向b的空间, 达到初始化main里面这个b的效果吗?

【热门文章】
【热门文章】