向量如何按值传递?

Ami*_*ari 1 c++ vector

我一直试图理解 C++ 向量是如何操作的,但我就是不知道它们是如何做这一件事的:它们如何按值传递?

据我所知,向量使用动态数组进行操作,使得它们既快速又易于使用。但是,如果它们使用动态数组,为什么我们可以按值将它们传递给函数呢?传递给函数的值中的指针如何不指向与原始向量的指针相同的位置?

我在网上搜索答案已经有一段时间了,但我能找到的只是“如何按值传递向量”而不是“如何按值传递向量”:(

Yak*_*ont 5

当您考虑“按引用”和“按值”时,您需要考虑实现细节和语义。

就您而言,您似乎认为“按值”意味着“自动存储” - 在堆栈上或存储在对象内。而“通过引用”的意思是“在堆上,存储为指针”。

这些都是公平的模型。但从语义上讲,按值仅意味着修改两个副本不会相互干扰。

IE:

void foo( some_type x ) {
  modify(x);
}
some_type instance;
foo(instance);
// foo did not modify instance!
Run Code Online (Sandbox Code Playgroud)

在这种情况下,some_type instance如果对内部值的修改foo没有修改instance外部值,则使用值语义传递foo值,则使用值语义传递。

对于向量来说,当它按值传递时,仅意味着函数内向量的修改不会改变外部向量。

当您拥有诸如指针向量(或智能指针,或任何其他具有引用语义的东西)之类的东西时,事情就会变得有趣。然后指针按值传递,但它们反过来又是对公共数据的引用!

在向量的情况下,向量通常被实现为 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";
Run Code Online (Sandbox Code Playgroud)

在此程序中,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";
Run Code Online (Sandbox Code Playgroud)

我们现在打印

8,9,10,11,12,
Run Code Online (Sandbox Code Playgroud)

在这里,我使用实际的 C++ 参考。我们可以使用指针“按引用传递”,也可以使用std::reference_wrapper. 在某些情况下,您可以通过使用在公用表中查找的对象的字符串名称来“按引用传递”;我在这里使用“引用”作为语义(代码的含义),而不是作为实现细节。

通过保持语义清晰——您是按值传递,而不是引用——您可以更轻松地理解代码。通过副本(值)传递使代码非常容易推理;所以一种解决方案是:

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

在这里,我们按值获取一个向量,然后按值返回它。

呼叫者,召集者:

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

防止两者忘记存储返回值,并且永远不会意外修改它们的参数。

有时您会听到一条规则,即不应混合引用和值语义。这可以通过几种方式发生。最简单的方法是存储std::vector<std::shared_ptr<int>>- 现在向量的值副本仍然具有对共享数据的引用,并且函数可以更改的内容确实很难描述。

另一种简单的方法是混合struct值和指针,甚至混合值和引用以获得额外的疯狂。

struct bad {
  int x;
  double& y;
};
Run Code Online (Sandbox Code Playgroud)

想想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.
Run Code Online (Sandbox Code Playgroud)

嗯。

如果y是一个指针:

struct bad {
  int x;
  double* y;
};
Run Code Online (Sandbox Code Playgroud)

我们还有另一种“什么是价值”。我们认为y是指针的值,还是被指向的值?如果是第一个,则bad可以毫无问题地具有值语义;如果是第二种,它具有混合值和引用语义。

指针是其值是外部引用的地址或位置的东西。推理既是值又是引用的事物通常很棘手,指针也不例外。

当您创建像 之类的类时std::vector,当您在内部使用指针时,std::vector大多数情况下会将其隐藏为实现细节。像所有抽象一样,它会泄漏,您可以在迭代器失效规则中看到它泄漏。

std::vector不会对指向其中有数据的内存缓冲区的指针进行建模。 std::vector对其中包含数据的可变大小的内存缓冲区进行建模;它使用值语义。

这些语义通过它也可以有效地移动(在某些情况下保证 O(1))而扩展,并且某些移动操作保证其中的迭代器和指针稳定。我们可以将其与 进行对比std::string,后者在相同的移动操作后不保证指针稳定性(这是因为std::string允许在其自身内存储字符的小缓冲区优化;向量不允许这样做)。