Untitled
我的博客备份
2019.5.14
C++
- 深入理解多态
- GDB调试技巧-打印vector的元素值
- 两种构造方法的效率对比
- 如何实现一个min-stack
- 如何实现一个string类1
- 如何实现一个string类1
- 指针和引用
- 正确删除vector中的元素
- 深入理解static关键字1
- 深入理解static关键字2
- 自己动手实现vector
- 记一个bug
OS
Data Structure
Human Resouce Machine
Untitled
std::move
当你熟悉了move语义并开始使用move的时候,你就会发现有很多case,你希望对某个对象使用move,但是它却是一个左值,而不是右值。比如下面的swap函数。
1 | #include<iostream> |
这段代码仅仅为了交换两个string,就进行了三次copy,显然效率是比较低的,而且在这个case下,copy其实是完全不必要的。也许我们可以用三次move替代三次copy减少开销,但是该怎么做呢?根据之前所学的知识,我们知道,只有右值可以调用move,而swap函数传进来的参数很可能是左值的。
Untitled
右值引用
1. 什么是右值引用
右值引用是C++11新加的一种引用类型,是一种仅能绑定到右值上的引用。不同于左值引用仅用一个&表示,右值引用用两个&&表示。
1 | int x{ 5 }; |
右值引用有两个非常有用的性质: 1. 右值引用将初始化他们的对象的寿命延长到右值引用的寿命;2. 非常量的右值引用允许修改右值。
看一个实际例子:
1 | #include <iostream> |
这段代码打印出:
1 | 3/5 |
Fraction{ 3, 5 }是一个匿名对象(临时对象),在这行语句结束的时候就出了作用域,本来应该被销毁掉,但是我们用了一个右值引用来绑定它,因此延长了它的生命期,直到main函数结束,局部变量rref被销毁的时候,这个临时对象才会被销毁。
再看另外一个例子:
1 | #include<iostream> |
这段代码执行结果是:
1 | 10 |
这里用字面值初始化一个右值引用,会创建一个临时对象,我们可以通过右值引用来修改这个对象。
2. 右值引用作为函数参数
右值引用最有用的地方在于作为函数参数,尤其是在写重载函数时,希望对传入的左值和右值表现出不同的行为。
1 | void fun(const int& lref) |
这段代码打印出:
1 | l-value reference to const. |
可以看出,当传入的参数是左值时,调用的是左值版本的fun(), 当传入的参数是右值时,调用的是右值版本的fun()。可是这有什么用呢?这对于移动语义来说是一个非常重要的特性,后面会继续讨论。
再看一个有意思的例子:
1 | int &&ref{ 5 }; |
ref是一个右值引用,那么fun(ref)调用的是右值引用版本吗?事实上,这里调用的是左值版本的fun()函数。虽然ref是一个右值引用,但是这仅说明它绑定的对象是一个右值,它本身是一个局部变量,是一个左值,因此这行代码调用的是左值版本的fun()。
3. 不要返回右值引用
在绝大多数情况下,你都不应该返回右值引用。因为右值引用绑定的对象在出作用域之后就会被销毁,因此从函数返回,你智能得到一个”hanging reference”。
参考资料
[1].https://www.learncpp.com/cpp-tutorial/15-2-rvalue-references/
Untitled
移动构造和移动赋值
前面已经讨论过通过重载拷贝构造函数和赋值运算符来实现move导致的一系列问题。现在我们来看看C++11如何通过移动构造和移动赋值来解决这些问题。
1. 拷贝构造函数和拷贝赋值
首先来回顾一下copy语义。拷贝构造函数通过拷贝已经存在的对象来初始化一个新的对象。copy 赋值用于将一个对象拷贝给另外一个对象。如果没有定义,c++编译器会给一个默认的拷贝构造函数和拷贝赋值函数,这两个函数仅做浅拷贝,所以对于含有动态申请内存的类来说,这两个默认函数存在一些问题,因此,处理带有动态内存的类必须要重写这两个函数做深拷贝。
在之前的文章中,我们已经实现过两个版本的Auto_ptr,现在来看看第三个版本
1 | template<class T> |
在这个例子中,我们在main函数中调用generateResource函数得到一个智能指针,接着赋值给一个已经存在的智能指针mainres。这个程序的运行结果是:
1 | Resource acquired |
(如果编译器做了返回值优化(RVO)的话,也可能仅有四条输出)。
短短两行代码,却有这么多次资源创建和销毁,是怎么回事?我们来具体看一下到底发生了什么!
- 在generateResource函数中,局部变量res被创建出来,并用动态申请的Resource来初始化,因此打印出第一行Resource acquired。
- 局部变量res通过传值返回给main函数,这里通过调用拷贝构造函数将res拷贝到一个临时对象,因为我们的拷贝是深拷贝,因此会再次创建一个Resource,所以会打印出第二次
Resource acquired。 - generateResource函数返回,此时res出作用域被销毁,因此打印出第一个Resource destroyed。
- 刚才产生的临时对象被拷贝到mainres,这是通过拷贝赋值函数实现的,拷贝赋值也是深拷贝,因此会再次创建一个Resource,所以会打印出第三个Resource acquired。
- 拷贝赋值完成后,临时对象出作用域被销毁,因此打印出第二个Resource destroyed。
- main函数结束后,mailres出作用域被销毁,因此打印出第三个Resource destroyed。
可以看出来,因为我们调用了一次拷贝构造函数和一次拷贝赋值函数,因此多出来两次不必要的Resource构造和销毁。效率很低,但是至少没有crash。现在,有了c++11的move semantics,我们可以做的更高效。
2. 移动构造函数和移动赋值
C++11定义了两个新的函数以实现move semantics,分别是移动构造函数和移动赋值函数。不同于拷贝构造函数和拷贝赋值将一个对象拷贝到另外一个对象,移动构造函数和移动赋值函数将对象的ownership从一个对象移动到另一个对象,显然,移动的开销要比拷贝的开销小很多。
再来看拥有移动构造函数和移动赋值函数的auto_ptr版本。
1 | #include <iostream> |
移动构造和移动赋值是很简单的,我们这是简单的把新对象的指针指向了原有对象的资源,然后将原有对象的指针置为空, 这样就把资源从原有对象“移动”到了新对象。这段代码的执行结果是:
1 | Resource acquired |
看,这样好多了。这段代码的执行流程跟上面那个例子一样,只是在调用拷贝构造函数和拷贝赋值函数的时候,分别调用了移动构造函数和移动赋值函数。
- 在generateResource函数中,局部变量res被创建出来,并用动态申请的Resource来初始化,因此打印出第一行Resource acquired。
- 局部变量res通过传值返回给main函数,这里通过调用移动构造函数将res移动到一个临时对象。
- generateResource函数返回,此时res出作用域被销毁,因为此时res是一个空指针,因此什么也没有发生。
- 刚才产生的临时对象被移动到mainres,这是通过移动赋值函数实现的。
- 移动赋值完成后,临时对象出作用域被销毁,此时临时对象的指针为空,因此什么也没发生。
- main函数结束后,mailres出作用域被销毁,因此打印出第三个Resource destroyed。
这段代码仅有一次资源的构造和析构,但是有两次move。
3. 什么时候会调用移动构造和移动赋值呢?
如果一个类定义类移动构造函数和移动赋值函数,并且传参传的是右值的时候,会调用移动构造或移动赋值函数。一般情况下,这个右值要么是字面量,要么是临时对象。
在大多数情况下,编译器不会提供默认的移动构造和移动赋值函数,除非这个类没有定义拷贝构造函数,拷贝赋值函数,移动构造函数,移动赋值函数和析构函数。
而且,编译器提供的移动构造和移动赋值函数跟默认的拷贝构造,拷贝赋值函数做的事情一样。
Rule: 如果需要移动构造和移动赋值,你必须自己写。
4. move semantics的核心思想
如果我们在构造或者赋值时,传入的参数是一个左值,我们只能做copy,不能move,因为左值在后面还有可能会用到,我们不能认为修改左值是安全的。比如a=b这行代码,我们不能指望b能被改变。
如果调用构造或者赋值时,传入的参数是右值,我们知道右值只是某种临时对象,我们可以放心的move而不是copy。这样做是安全的,因为右值在语句结束就会被销毁,我们不可能在接下里的代码中继续使用它。
C++11通过右值引用给我们提供了根据不同参数(左值还是右值)调用不同构造或赋值函数的能力,让代码更高效。
5. move函数应该让两边的对象都处于定义良好的状态
在上面的例子中,移动构造和移动赋值函数最后都将原有对象的指针置为nullptr了,这看起来是不必要的,毕竟如果传入的对象是一个右值,那么在语句结束的时候总是要被销毁的,为什么我们要多此一举在函数里面做清理工作呢?
答案很简单,当传入的右值出了作用域的时候,它的析构函数会被调用,如果它的指针还指向原来申请的内存,那么这块内存会被释放,导致新对象的指针成为野指针。
另外,前面说到传入的参数是右值时,会调用移动构造或移动赋值函数,其实,传入左值也可以选择调用移动构造或移动赋值函数,这个在后面会继续讨论。
6. 函数按值返回的左值可以被move而不必copy
在auto_ptr4版本的generateResource函数,res是通过传值返回的,虽然它是一个左值,但是它调用的是move而不是copy。C++规范中有一个特殊的规定,从函数返回的automatic object如果是通过传值返回的话,可以被move而不需要copy,即便他们是一个左值。这个是合理的,既然generateResource函数中的res在函数结束的时候马上就要被销毁掉,那么我们“偷”一下它的资源也是合理的,这样就避免了昂贵又没有必要的copy开销。
尽管编译器可以move函数返回值,但是在某些case,还可以更进一步,在这些case,不管是move构造函数还是copy构造函数都不会被调用(即返回值优化RVO)。
7. 禁止拷贝
在上面的auto_ptr4版本中,我们保留了拷贝构造函数和拷贝赋值函数是为了和移动构造函数,移动赋值函数做比较。但是有时候我们需要禁止copy,因为copy的开销很大,而且有时候对象T也不支持copy。
下面这个版本的Auto_ptr支持move但是不支持copy。
1 | #include <iostream> |
在这个版本中,如果你尝试将一个Auto_ptr5左值通过值传递给一个函数,编译器会报错拷贝构造函数被删除了。这是很好的,因为我们无论如何都应该通过const左值引用来传递Auto_ptr。
Auto_ptr5是一个比较不错的智能指针,实际上标准库中的std::unique_ptr跟auto_ptr5非常类似,推荐使用。
Hello World
Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.
Quick Start
Create a new post
1 | $ hexo new "My New Post" |
More info: Writing
Run server
1 | $ hexo server |
More info: Server
Generate static files
1 | $ hexo generate |
More info: Generating
Deploy to remote sites
1 | $ hexo deploy |
More info: Deployment
smart pointer
intro to smart pointer and move semantics
(翻译改写自https://www.learncpp.com/cpp-tutorial/15-1-intro-to-smart-pointers-move-semantics/)
1. 裸指针导致的内存泄漏问题
考虑下面这个函数,在这个函数中我们动态申请了一片内存。
1 | void someFunction() |
这段代码看起来非常直白,但是存在一个问题:我们常常会忘了释放内存。即使我们始终记得释放内存,但是还存在一些case导致内存没有正确释放。
case1: 函数提前返回
1 | include <iostream> |
case2: 抛出异常
1 | #include <iostream> |
在上面两段代码中,由于函数提前返回或者抛出异常,导致内存泄漏,而且每一次这个函数被调用,都会导致新的内存泄漏。
导致以上问题的根本原因在于裸指针没有内在的内存清理机制。
2. 智能指针类可以解决这类问题吗?
类有一个很好的特性就是类有析构函数,当对象出作用域的时候,析构函数就会自动执行,释放其占有的内存。如果我们在构造函数中申请内存,并在析构函数中delete,那么就可以保证内存能够被正确释放。
那么我们可以用一个类来管理指针吗?答案是肯定的!
假设有一个类,它的唯一任务是持有并“拥有”一个传递给它的指针,然后在类对象超出作用域时释放该指针。只要类的对象仅作为局部变量创建,我们就可以保证类将正确地超出作用域(无论何时或如何终止函数),并且所拥有的指针将被销毁。
1 | #include <iostream> |
这段代码的执行结果:
1 | Resource acquired |
看一下这段程序是如何运行的。首先,我们新建了一个Resource对象,并把指针作为参数传递给模版类Auto_ptr1的构造函数,从此时起,res变量就拥有了Resource对象。因为res是一个局部变量,作用域是main函数的一对打括号,当出了大括号,res变量就会被销毁。只要Auto_ptr1对象被定义为一个局部变量,不管函数如何结束,都可以保证Resource类被正确的析构。
像Auto_ptr1这种类称为smart pointer,智能指针是一个组合类,它被设计用来管理动态分配的内存,并确保当智能指针对象超出范围时内存被删除。(对应的,内置指针有时被称为”dumb pointer”,因为它们不能自己清理)。
现在我们回到someFunction,看看智能指针如何解决内存泄漏的问题。
1 | #include <iostream> |
当用户输入0时, 程序会提前退出,打印出:
1 | Resource acquired |
因为ptr是一个局部变量,函数结束时会自动调用ptr的析构函数,正常释放掉resource占用的内存。
3. Auto_ptr1的一个严重缺陷
Auto_ptr解决了裸指针导致的内存泄漏问题,但是它还存在一个严重的缺陷,来看一段代码。
1 | #include <iostream> |
这段代码的执行结果是:
1 | Resource acquired |
程序大概率会在此时crash,看到问题所在了吗?因为我们没有提供拷贝构造函数,因此编译器给我们提供了一个默认的拷贝构造函数,这个默认的函数仅做浅拷贝。所以在main函数中,我们用res1来初始化res2之后,res1和res2指向同一个Resource对象。当res2出了作用域时,会释放掉resource对象占用的内存使res1称为一个野指针,当res1出了作用域时,它会尝试再次释放resource对象,导致程序crash。
下面这段代码也存在类似的问题
1 | void passByValue(Auto_ptr1<Resource> res) |
res1会传值给res,导致两个指针指向同一个资源,进而导致程序crash。
所以,如何修复这个问题呢?
有一个办法是我们可以显式定义并且将拷贝构造函数和赋值运算符置为delete。这样从一开始就阻止了任何拷贝,当然也阻止了函数调用时的参数传值。看起来似乎完美解决了问题,但是,如果我们向从一个函数返回Auto_ptr1呢?
1 | ??? generateResource() |
我们不能返回引用,因为Auto_ptr1是局部变量,出了作用域,就会被销毁掉。返回地址也是一样。看来我们只能通过传值返回了。
另外一个办法是自定义拷贝构造函数和赋值运算符,在这两个函数中进行深拷贝。这种方式至少可以保证不存在多个指针指向同一个资源的问题。但是拷贝是非常耗时的操作(),,不是我们想要的甚至是不可能的),我们也不想仅仅因为需要从函数返回Auto_ptr而做一些毫无必要的拷贝。
似乎所有的路都堵死了, 还有别的办法吗?
4. Move semantics
其实,设计C++的大牛们已经为我们准备好了解决方案。如果我们不做拷贝,只是将指针的所有权从一个对象移动到另外一个对象,那又如何呢?移动而非拷贝,这就是move semantics背后的核心思想。
我们看看Auto_ptr的第二个版本如何实现移动而非拷贝。
1 | #include <iostream> |
这段代码打印出:
1 | Resource acquired |
注意operator=函数将m_ptr的所有权从res1传递到res2,因此不会出现指针副本,内存也能够清理干净!
5. std::auto_ptr以及为什么要避免使用它
现在是时候讨论一下std::auto_ptr了。std::auto_ptr是c++98引入的,这是c++首次尝试引入的第一个智能指针。std::auto_ptr实现移动语义的方式跟上面介绍的Auto_ptr2一样。
然而,事实证明std::auto_ptr(以及我们的Auto_ptr2)存在一系列问题,使得使用std::auto_ptr变成一件很危险的事情。
(由此可见,即便是设计C++的大牛们也会有考虑不周的时候。:)
首先,std::auto_ptr是通过拷贝构造函数和赋值运算符重载实现移动语义的,把一个std::auto_ptr传值给一个函数,会造成auto_ptr指向的资源被转移给了函数的参数。函数参数是一个局部变量,在函数执行完成之后就会被销毁,其指向的资源也会被销毁。然后调用者如果继续使用auto_ptr就会得到一个空指针,造成程序crash。
其次,std::auto_ptr释放内存总是用delete xxx,而不是delete[] xxx, 这就意味着auto_ptr不能正确释放动态分配的数组。更糟糕的是,如果你把指向数组的指针传给auto_ptr,它不会报任何错误或警告,这样看下来,就会导致内存泄漏问题。
最后,auto_ptr不能处理C++标准库中的其他类,包括大多数容器和算法类。这是因为这些类在做copy的时候确实是做了copy而不是move。
基于上述原因,auto_ptr在C++11不推荐使用,到了C++17,auto_ptr已经从标准库中被删除了。
6. 更进一步
auto_ptr设计的核心问题在于C++11之前,C++语言没有move semantics。重载拷贝构造函数和赋值运算符来实现移动语义会导致很多奇怪的case和bug。比如res2 = res1这行代码,你不知道res1是否会改变。
因为这些原因,C++11正式定义了move semantics, 并提供了三种智能指针,std::unique_ptr, std::weak_ptr, std::shared_ptr。
7. 参考资料
[1]. https://www.learncpp.com/cpp-tutorial/15-1-intro-to-smart-pointers-move-semantics/
线程总结
如何正确删除vector中的元素
GDB调试技巧-打印vector的元素值
平常在使用GDB调试程序的时候,我们经常需要查看一个STL容器里面存储的元素的值是多少。但是用GDB的p命令打印容器,得到的却是一堆乱七八糟的东西。比如有一个vector<int> nums = {1,2,3}
,当我们使用p nums
命令时,我们得到的结果是: