84669인 학습
152542인 학습
20005인 학습
5487인 학습
7821인 학습
359900인 학습
3350인 학습
180660인 학습
48569인 학습
18603인 학습
40936인 학습
1549인 학습
1183인 학습
32909인 학습
认证高级PHP讲师
楼上解释了为什么会调用拷贝构造函数,我再给你解释一下为什么会乱入。
首先本质原因是vector扩容,一开始容量是0,第一次操作扩容到1,第二次是翻倍为2。
你是用mac下的clang++的,它调用的stl实现应该是libcxx,我们可以通过libcxx里vector的push_back实现源码看出来。
源码可以在这里查看 https://github.com/llvm-mirro...
if (this->__end_ != this->__end_cap()) { } else __push_back_slow_path(__x);
__push_back_slow_path的实现是这样的
allocator_type& __a = this->__alloc(); __split_buffer<value_type, allocator_type&> __v(__recommend(size() + 1), size(), __a); __alloc_traits::construct(__a, _VSTD::__to_raw_pointer(__v.__end_), _VSTD::forward<_Up>(__x)); __v.__end_++; __swap_out_circular_buffer(__v);
可以看出是先扩容,__recommend就是做扩容的工作,随后把新的内容构造出来放在__a的后半段的首位,注意有forward,所以可移动也可拷贝,最后再执行__swap_out_circular_buffer函数。__swap_out_circular_buffer的实现是这样的:
template <class _Tp, class _Allocator> void vector<_Tp, _Allocator>::__swap_out_circular_buffer(__split_buffer<value_type, allocator_type&>& __v) { __annotate_delete(); __alloc_traits::__construct_backward(this->__alloc(), this->__begin_, this->__end_, __v.__begin_); _VSTD::swap(this->__begin_, __v.__begin_); _VSTD::swap(this->__end_, __v.__end_); _VSTD::swap(this->__end_cap(), __v.__end_cap()); __v.__first_ = __v.__begin_; __annotate_new(size()); __invalidate_all_iterators(); }
对于迭代器的处理和对于size的处理可以不用看,重点是__alloc_traits::__construct_backward,它负责把之前vector的数据拷贝或者移动到新vector内存区。__construct_backward的实现是这样的
while (__end1 != __begin1) { construct(__a, _VSTD::__to_raw_pointer(__end2-1), _VSTD::move_if_noexcept(*--__end1)); --__end2; }
可以从move_if_noexcept看出它要执行移动操作必须保证是noexcept的,而且这个操作是从end开始的,迭代器一直递减到begin,所以是逆序的。
所以在你执行第一次push_back时,检查了容量不够,执行__push_back_slow_path函数,vector扩容到1,并且执行了一次移动构造函数,因为原来的vector是空的,所以不需要进一步处理。
此时就是你的第一次打印 Move constructor is called. source: hello
Move constructor is called. source: hello
在你执行第二次push_back时,检查了容量不够,执行__push_back_slow_path函数,vector扩容到2,并且执行了一次移动构造函数。
此时就是你的第二次打印 Move constructor is called. source: world
Move constructor is called. source: world
随后它执行__swap_out_circular_buffer,并调用__alloc_traits::__construct_backward,由于你的移动构造函数不是noexcept的,所以它调用了一次你的拷贝构造函数。
__swap_out_circular_buffer
__alloc_traits::__construct_backward
此时就是你的第三次打印 Copy constructor is called. source: hello
Copy constructor is called. source: hello
如果你有更多的元素,你就会发现后续的拷贝构造函数执行顺序是和原vector的顺序反着的,原因上面也说了,操作是从end开始的,迭代器一直递减到begin。
因为你的移动构造函数不是noexcept的。将移动构造函数声明为noexcept(true),vector就不会调用拷贝构造函数了。
vector的push_back可能会需要扩展存储区。这一过程要将原有数据从原存储区拷贝到新申请的存储区。同时push_back需要保证在添加元素的过程中,若有操作抛出异常,容器保持push_back前的状态。因此,它不能调用可能会抛出异常的移动构造函数。因为从语义上来说,移动构造操作中断(抛出异常)会导致数据损坏。
它会调用可能抛异常的拷贝构造函数,是因为从语义上来说这么做不会导致数据损坏。当然你也可以在拷贝构造函数里搞事情。
楼上解释了为什么会调用拷贝构造函数,我再给你解释一下为什么会乱入。
首先本质原因是vector扩容,一开始容量是0,第一次操作扩容到1,第二次是翻倍为2。
你是用mac下的clang++的,它调用的stl实现应该是libcxx,我们可以通过libcxx里vector的push_back实现源码看出来。
源码可以在这里查看 https://github.com/llvm-mirro...
__push_back_slow_path的实现是这样的
可以看出是先扩容,__recommend就是做扩容的工作,随后把新的内容构造出来放在__a的后半段的首位,注意有forward,所以可移动也可拷贝,最后再执行__swap_out_circular_buffer函数。
__swap_out_circular_buffer的实现是这样的:
对于迭代器的处理和对于size的处理可以不用看,重点是__alloc_traits::__construct_backward,它负责把之前vector的数据拷贝或者移动到新vector内存区。
__construct_backward的实现是这样的
可以从move_if_noexcept看出它要执行移动操作必须保证是noexcept的,而且这个操作是从end开始的,迭代器一直递减到begin,所以是逆序的。
所以在你执行第一次push_back时,检查了容量不够,执行__push_back_slow_path函数,vector扩容到1,并且执行了一次移动构造函数,因为原来的vector是空的,所以不需要进一步处理。
此时就是你的第一次打印
Move constructor is called. source: hello
在你执行第二次push_back时,检查了容量不够,执行__push_back_slow_path函数,vector扩容到2,并且执行了一次移动构造函数。
此时就是你的第二次打印
Move constructor is called. source: world
随后它执行
__swap_out_circular_buffer
,并调用__alloc_traits::__construct_backward
,由于你的移动构造函数不是noexcept的,所以它调用了一次你的拷贝构造函数。此时就是你的第三次打印
Copy constructor is called. source: hello
如果你有更多的元素,你就会发现后续的拷贝构造函数执行顺序是和原vector的顺序反着的,原因上面也说了,操作是从end开始的,迭代器一直递减到begin。
因为你的移动构造函数不是noexcept的。将移动构造函数声明为noexcept(true),vector就不会调用拷贝构造函数了。
vector的push_back可能会需要扩展存储区。这一过程要将原有数据从原存储区拷贝到新申请的存储区。同时push_back需要保证在添加元素的过程中,若有操作抛出异常,容器保持push_back前的状态。因此,它不能调用可能会抛出异常的移动构造函数。因为从语义上来说,移动构造操作中断(抛出异常)会导致数据损坏。
它会调用可能抛异常的拷贝构造函数,是因为从语义上来说这么做不会导致数据损坏。当然你也可以在拷贝构造函数里搞事情。