首页 > 解决方案 > 为什么按值传递时复制c ++向量内部元素/数组

问题描述

为什么向量传值时会复制向量的内部元素?

#include<vector>
using namespace std;

// this func won't modify v[2]. 
// Meaning v[2] (and the whole inner array) was copied 
// when v is passed to the func?
void modify(vector<int> v) {
    v[2] = 100;
}

// this func modify v[2]
void modify(vector<int>& v) {
    v[2] = 100;
}

int main() {
    vector<int> v = {1,2,3,4,5,6};

    // still same
    modify(v);

    // modified
    modified2(v);
}

我发现当向量按值传递时,向量的实际内容被复制,这很奇怪。我认为 std::vector 实现必须有一个指针字段,该指针字段映射到实际数组所在的堆上的地址。所以当向量被传递时,即使是通过值,地址也应该保持不变,指向相同的内容。像这样的东西:

#include<iostream>

using namespace std;

// a dummy wrapper of an array
// trying to mock vector<int>
class vector_int {
    public:
    int* inner_array; // the actual array 
    vector_int(int *a) {
        inner_array = a;
    }
    int* at(int pos) {
        return inner_array+pos;
    }
};

// this passes the "mocked vector" by value
// but 'inner_array' is not copied
void modify(vector_int v) {
    *(v.at(2)) = 10;
}

int main() {
    int* a = new int[3] {1,2,3};
    vector_int v = vector_int(a);
    modify(v); // v[2] is modified
}

这个关于 std::vector 实现的假设是否正确?是什么让向量内容在按值传递时被复制?


编辑

感谢改变 igel 的回答和 UnholySheep 的评论,我弄清楚了 std::vector 具有价值语义的原因(或者内部数组被复制的原因)。

如果复制构造函数类在类定义中显式定义,则复制构造函数将确定在函数调用中传递变量时如何复制结构/类实例。所以我可以为 my 定义一个复制构造函数vector_int,在其中我复制整个inner_array,比如

#include<iostream>

using namespace std;

class vector_int {
    public:
    int* inner_array;
    int len;
    vector_int(int *a, int len) {
        inner_array = a;
        this->len = len;
    }
    int* at(int pos) {
        return inner_array+pos;
    }
    // this is the copy constructor
    vector_int(const vector_int &v2) {
        inner_array = new int;
        for (int i =0; i < v2.len; i++) {
            *(inner_array+i) = *(v2.inner_array+i);
        }
    } 
};

// Yay, the vector_int's inner_array is copied
// when this function is called
// and no modification of the original vector is done
void modify(vector_int v) {
    *(v.at(2)) = 10;
}

int main() {
    int* a = new int[3] {1,2,3};
    vector_int v = vector_int(a,3);
    // 
    modify(v);
}

我在本地计算机上检查了 stdlib 实现的源代码(g++ Apple LLVM 版本 10.0.0)。std::vector 定义了一个复制构造函数,看起来像这样

template <class _Tp, class _Allocator>
vector<_Tp, _Allocator>::vector(const vector& __x)
    : __base(__alloc_traits::select_on_container_copy_construction(__x.__alloc()))
{
    size_type __n = __x.size();
    if (__n > 0)
    {
        allocate(__n);
        __construct_at_end(__x.__begin_, __x.__end_, __n);
    }
}

看起来它为实际复制的数组+复制数组做了一个malloc。

标签: c++arraysvector

解决方案


C++ 允许类类型为创建、复制、移动和销毁的含义提供自己的代码,并且该代码被隐式调用,没有任何明显的函数调用。这被称为值语义,它是 C++ 在其他语言诉诸诸如create_foo(foo)foo.clone()或.destroy_foo(foo)foo.dispose()

每个类都可以定义以下特殊成员函数

  • 构造函数,用于将对象置于有效的初始状态
  • 一个析构函数,负责清理
  • 复制构造函数,用于创建与另一个对象重复的新对象
  • 移动构造函数,用于通过传输另一个对象的数据来创建新对象
  • 复制赋值运算符,用于将对象复制到现有对象中
  • 移动赋值运算符,用于在两个现有对象之间传输数据

这些都是您可以定义来做任何您想做的事情的功能。但是它们被隐式调用,这意味着此类的用户在他们的代码中看不到这些函数调用,并且他们希望它们做可预测的事情。您应该通过遵循三/五/零规则来确保您的类的行为是可预测的。

当然,还有其他用于共享数据的工具,例如您已经知道的传递引用。

标准库中的许多类都使用这些特殊的成员函数来实现非常有用的特殊行为,并帮助用户编写安全、正确的代码。例如:

  • std::vector复制时,将始终具有相同的元素,尽管底层数组和包含的对象将是分开的。
  • std::unique_ptr包装只有一个所有者的资源。为了强制执行,它不能被复制
  • std::shared_ptr包装具有许多所有者的资源。何时清理此类资源并不完全清楚,因此复制 ashared_ptr会执行自动引用计数,并且只有在最后一个所有者完成资源后才会清理资源。

推荐阅读