c++ 如何通过值传递向量?

8ehkhllq  于 2023-10-21  发布在  其他
关注(0)|答案(2)|浏览(174)

我一直在试图理解C++ vectors是如何操作的,但我就是不知道它们是如何做到这一点的:如何通过值传递它们?
据我所知,vector使用动态数组进行操作,使它们既快速又易于使用。但是,如果它们使用动态数组,为什么我们可以通过值将它们传递给函数?传递给函数的值中的指针如何不指向与原始向量指针相同的位置?
我已经在网上搜索了一段时间的答案,但我所能找到的是“我如何通过值传递向量”,而不是“如何通过值传递向量”:(

mctunoxg

mctunoxg1#

在考虑“按引用”和“按值”时,需要同时考虑实现细节和语义。
在您的例子中,您似乎认为“按值”意味着“在自动存储中”-在堆栈上,或存储在对象中。而“通过引用”意味着“在堆上,作为指针存储”。
他们是公平的模型。但从语义上讲,通过值只是意味着修改两个副本不会相互干扰。
即:

void foo( some_type x ) {
  modify(x);
}
some_type instance;
foo(instance);
// foo did not modify instance!

在这种情况下,如果修改foo内的值不会修改foo外的值instance,则使用值语义传递some_type instance
对于一个vector,当它通过值传递时,它只是意味着在函数内部修改vector不会改变外部的vector。
当你有一个指针向量(或智能指针,或任何其他具有引用语义的东西)时,事情就变得有趣了。然后指针按值传递,但它们反过来是对公共数据的引用!
在vector的情况下,vector通常被实现为3个指针。第一个指向缓冲区的开始,第三个指向缓冲区的结束,第二个指向中间的某个地方。向量模板确保缓冲区的第一个和第二个(半开间隔)之间的所有部分都是用有效对象构造的。
当你通过值传递一个向量时,它会创建一个新的“足够大”的缓冲区,并将有效对象(第一个和第二个指针之间)复制到新的缓冲区。当新向量超出作用域时,它会清理这个新缓冲区。

void foo(std::vector<int> x) {
  for (auto& elem:x)
    elem+=7;
}
std::vector<int> instance{1,2,3,4,5};
foo(instance);
for (auto& elem:instance)
  std::cout << elem << ",";
std::cout << "\n";

在这个程序中,instance是一个包含1到5的向量。我们将它 * 通过值 * 传递给foo,这将向量的 * 本地副本 * 元素增加7。
变量instance没有被这个操作修改,当我们打印它时,我们得到"1,2,3,4,5,\n"
如果我们将其改为通过引用传递:

void foo(std::vector<int>& x) {
  for (auto& elem:x)
    elem+=7;
}
std::vector<int> instance{1,2,3,4,5};
foo(instance);
for (auto& elem:instance)
  std::cout << elem << ",";
std::cout << "\n";

我们现在打印

8,9,10,11,12,

在这里,我使用了一个实际的C++参考。我们可以通过使用指针来“通过引用传递”,或者我们可以通过使用std::reference_wrapper来实现。在某些情况下,您可以通过使用在公共表中查找的对象的字符串名称来“按引用传递”;我在这里使用“引用”作为语义(代码的含义),而不是作为实现细节。
通过保持语义清晰--通过值传递,而不是引用--您可以更容易地理解代码。通过copy(value)传递使代码更容易推理;所以一个解决方案是:

[[nodiscard]] std::vector<int> foo(std::vector<int> x) {
  for (auto& elem:x)
    elem+=7;
  return x;
}

在这里,我们通过值获取一个向量,并通过值返回它。
来电者:

std::vector<int> instance{1,2,3,4,5};
instance = foo(std::move(instance));
for (auto& elem:instance)
  std::cout << elem << ",";
std::cout << "\n";

防止两者忘记存储返回值,并且永远不会意外修改它们的参数。
你有时会听到一条规则,你不应该混合引用和值语义。这可以以几种方式发生。最简单的是存储一个std::vector<std::shared_ptr<int>>--现在向量的值副本仍然有对共享数据的引用,函数可以改变什么真的很难描述。
另一种简单的方法是使用一个struct,它混合了值和指针,甚至值和引用。

struct bad {
  int x;
  double& y;
};

思考bad的行为是一团乱。

int ione = 1, itwo = 2;
double done = 1., dtwo = 2.;
bad a {ione, done};
bad b {itwo, dtwo};
bad c = a; // c.y refers to a.y which is done
c = b; // c.y still refers to done, but now has the value 2.


如果y是指针:

struct bad {
  int x;
  double* y;
};

我们有另一种“什么是价值”。y是指针的值,还是被指向对象的值?如果是第一个,那么bad可以具有值语义而没有问题;如果是第二个,它具有混合的值和引用语义。
指针的值是外部引用的地址或位置。对既是值又是引用的东西进行推理通常很棘手,指针也不例外。
当您创建像std::vector这样的类时,虽然您在内部使用指针,但std::vector通常会将其隐藏为实现细节。像所有抽象一样,它会泄漏,你可以在迭代器失效规则中看到它泄漏。
但是std::vector并没有对一个指向内存缓冲区的指针进行建模。std::vector为可变大小的内存缓冲区建模,其中包含数据;它使用值语义。
这些语义被扩展为移动也是有效的(在某些情况下保证O(1)),并且某些移动操作保证迭代器和指针在其中是稳定的。我们可以将其与std::string进行对比,std::string在相同的移动操作后不能保证指针的稳定性(这是因为std::string允许在其内部存储字符的小缓冲区优化; vector不允许它)。

hfyxw5xn

hfyxw5xn2#

简单来说,std::vector实现了一个复制构造函数和一个复制赋值操作符,它们可以进行“深度”复制,所以当你赋值或复制一个操作符时,它会创建一个vector内容的全新副本,而不仅仅是指向源vector内容的指针的副本。
这意味着如果向量非常大,通过值传递它可能会非常慢,所以大多数代码将传递对常量向量(std::vector<T> const &foo)的引用或传递迭代器,而不是通过值传递向量,除非函数做了一些需要两个完全独立的数据副本的事情。

相关问题