我一直在试图理解C++ vectors是如何操作的,但我就是不知道它们是如何做到这一点的:如何通过值传递它们?据我所知,vector使用动态数组进行操作,使它们既快速又易于使用。但是,如果它们使用动态数组,为什么我们可以通过值将它们传递给函数?传递给函数的值中的指针如何不指向与原始向量指针相同的位置?我已经在网上搜索了一段时间的答案,但我所能找到的是“我如何通过值传递向量”,而不是“如何通过值传递向量”:(
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个指针。第一个指向缓冲区的开始,第三个指向缓冲区的结束,第二个指向中间的某个地方。向量模板确保缓冲区的第一个和第二个(半开间隔)之间的所有部分都是用有效对象构造的。当你通过值传递一个向量时,它会创建一个新的“足够大”的缓冲区,并将有效对象(第一个和第二个指针之间)复制到新的缓冲区。当新向量超出作用域时,它会清理这个新缓冲区。
foo
instance
some_type instance
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"。如果我们将其改为通过引用传递:
"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)传递使代码更容易推理;所以一个解决方案是:
std::reference_wrapper
[[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,它混合了值和指针,甚至值和引用。
std::vector<std::shared_ptr<int>>
struct
struct bad { int x; double& y; };
思考bad的行为是一团乱。
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是指针:
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不允许它)。
std::vector
std::string
hfyxw5xn2#
简单来说,std::vector实现了一个复制构造函数和一个复制赋值操作符,它们可以进行“深度”复制,所以当你赋值或复制一个操作符时,它会创建一个vector内容的全新副本,而不仅仅是指向源vector内容的指针的副本。这意味着如果向量非常大,通过值传递它可能会非常慢,所以大多数代码将传递对常量向量(std::vector<T> const &foo)的引用或传递迭代器,而不是通过值传递向量,除非函数做了一些需要两个完全独立的数据副本的事情。
std::vector<T> const &foo
2条答案
按热度按时间mctunoxg1#
在考虑“按引用”和“按值”时,需要同时考虑实现细节和语义。
在您的例子中,您似乎认为“按值”意味着“在自动存储中”-在堆栈上,或存储在对象中。而“通过引用”意味着“在堆上,作为指针存储”。
他们是公平的模型。但从语义上讲,通过值只是意味着修改两个副本不会相互干扰。
即:
在这种情况下,如果修改
foo
内的值不会修改foo
外的值instance
,则使用值语义传递some_type instance
。对于一个vector,当它通过值传递时,它只是意味着在函数内部修改vector不会改变外部的vector。
当你有一个指针向量(或智能指针,或任何其他具有引用语义的东西)时,事情就变得有趣了。然后指针按值传递,但它们反过来是对公共数据的引用!
在vector的情况下,vector通常被实现为3个指针。第一个指向缓冲区的开始,第三个指向缓冲区的结束,第二个指向中间的某个地方。向量模板确保缓冲区的第一个和第二个(半开间隔)之间的所有部分都是用有效对象构造的。
当你通过值传递一个向量时,它会创建一个新的“足够大”的缓冲区,并将有效对象(第一个和第二个指针之间)复制到新的缓冲区。当新向量超出作用域时,它会清理这个新缓冲区。
在这个程序中,
instance
是一个包含1到5的向量。我们将它 * 通过值 * 传递给foo
,这将向量的 * 本地副本 * 元素增加7。变量
instance
没有被这个操作修改,当我们打印它时,我们得到"1,2,3,4,5,\n"
。如果我们将其改为通过引用传递:
我们现在打印
在这里,我使用了一个实际的C++参考。我们可以通过使用指针来“通过引用传递”,或者我们可以通过使用
std::reference_wrapper
来实现。在某些情况下,您可以通过使用在公共表中查找的对象的字符串名称来“按引用传递”;我在这里使用“引用”作为语义(代码的含义),而不是作为实现细节。通过保持语义清晰--通过值传递,而不是引用--您可以更容易地理解代码。通过copy(value)传递使代码更容易推理;所以一个解决方案是:
在这里,我们通过值获取一个向量,并通过值返回它。
来电者:
防止两者忘记存储返回值,并且永远不会意外修改它们的参数。
你有时会听到一条规则,你不应该混合引用和值语义。这可以以几种方式发生。最简单的是存储一个
std::vector<std::shared_ptr<int>>
--现在向量的值副本仍然有对共享数据的引用,函数可以改变什么真的很难描述。另一种简单的方法是使用一个
struct
,它混合了值和指针,甚至值和引用。思考
bad
的行为是一团乱。呃
如果
y
是指针:我们有另一种“什么是价值”。
y
是指针的值,还是被指向对象的值?如果是第一个,那么bad
可以具有值语义而没有问题;如果是第二个,它具有混合的值和引用语义。指针的值是外部引用的地址或位置。对既是值又是引用的东西进行推理通常很棘手,指针也不例外。
当您创建像
std::vector
这样的类时,虽然您在内部使用指针,但std::vector
通常会将其隐藏为实现细节。像所有抽象一样,它会泄漏,你可以在迭代器失效规则中看到它泄漏。但是
std::vector
并没有对一个指向内存缓冲区的指针进行建模。std::vector
为可变大小的内存缓冲区建模,其中包含数据;它使用值语义。这些语义被扩展为移动也是有效的(在某些情况下保证O(1)),并且某些移动操作保证迭代器和指针在其中是稳定的。我们可以将其与
std::string
进行对比,std::string
在相同的移动操作后不能保证指针的稳定性(这是因为std::string
允许在其内部存储字符的小缓冲区优化; vector不允许它)。hfyxw5xn2#
简单来说,
std::vector
实现了一个复制构造函数和一个复制赋值操作符,它们可以进行“深度”复制,所以当你赋值或复制一个操作符时,它会创建一个vector内容的全新副本,而不仅仅是指向源vector内容的指针的副本。这意味着如果向量非常大,通过值传递它可能会非常慢,所以大多数代码将传递对常量向量(
std::vector<T> const &foo
)的引用或传递迭代器,而不是通过值传递向量,除非函数做了一些需要两个完全独立的数据副本的事情。