C++ 写时拷贝 3

http://blog.csdn.net/ljianhui/article/details/22895505

字符串一种在程序中经常要使用到的数据结构,然而在C中却没有字符串这种类型。在C++中,为了方便字符串的使用,在STL中提供了一个string类。该类维护一个char指针,并封装和提供各种的字符串操作。

一、为什么要实现隐式公享写时拷贝
试想一下,如果我们要自己实现一个string类,最简单的方式是什么?就是让每一个string类的实例维护一个在内存中独立的字符数组,每个string对象各不相干。这样一个对象的任何变化都不会影响到其他的对象。这样做的好处就是处理简单,不易出错,但是这样做的缺点却是内存的使用量、程序的效率也低。
例如,对于如下的例子:
[cpp] view plain copy
  1. int swap(string &x, string &y)  
  2. {  
  3.     string tmp(x);  
  4.     x = y;  
  5.     y = tmp;  
  6. }  
在上面的代码中,我们需要做的事情非常简单,只是想交换一下两个string对象的值而已。然而,如果我们采用上面所说的实现方式,一共在内存上进行了三次的用new来创建一个新的数组并复制数据,同时还会调用两次的delete[]。我们花了这么大的力气才完成了这样一个简单的动作。而隐式共享写时复制的内存管理策略却可以解决这样的问题。虽然C++11用&&运算符解决了上述问题,但是在程序中使用大量的string的副本,而不改变其值的情况还是不少的。例如string数组,删除一个元素后的移动操作。

二、隐式共享写时复制的实现思想
什么是隐式共享写时拷贝呢?就是当用一个string对象初始化另一个string对象或把一个string对象赋值给另一个string对象时,它们内部维护的指针其实指向了内存中的同一个字符数组,这就是隐式共享。

使用这种方法,上述的代码就不需要调用new来创建数组也不需要复制,也不会调用delete[](如果不是很明白也不要紧,看完实现代码和后自然就明白了)。然后两个指针指向同一个对象很容易引发错误,当其中一个对象执行析构函数释放掉其内部指针指向的内存时,另一个对象却对此完全不知情,可能会引用一个不存在的内存,从而让程序崩溃。所以为了方便资源的管理,我们引用智能指针的思想,为每个内存中的字符数组(用new创建,存在于堆中)添加一个引用计数used,表示有多少个对象引用这个块内存(即字符数组)。当一个对象析构时,它会把引用计数used减1,当used为0时,表示没有对象引用这块内存,从而把这块内存释放掉。当然由于used也要在对象中共享,所以它也是一个堆中的数据,每个对象有一个指向它的指针。

而当一个string对象需要改变它的值时,例如
[cpp] view plain copy
  1. string s1("abc");  
  2. string s2(s1);  
  3. string s3("edf");  
  4. s2 += s3;  
此时,s1和s2指向了堆内存中的同一个字符数组,而当s2的值要改变时,因为如果直接在其指向的内存中修改,则会影响到对象s1,所以为了让s2的操作不影响到s1,s2会在重新new出一块内存,然后先把之前所引用的字符数组的数据复制到新的字符数组中,然后再把s3中的字符数据复制到新的字符数组中。这就是写时拷贝。注意,同时还要把之前指向的内存的引用计数减1(因为它指向了新的堆中的字符数组),并在堆中重新new一个块内存,用于保存新的引用计数,同时把新的字符数组的引用计数置为1。因为此时只有一个对象(就是改变值的对象)在使用这个内存。

三、代码实现及设计要点详解
说了这么多,还是来看看代码的实现吧,为了与标准C++的string类区别开来,这样采用第一个字母大写来表示自定义的字符串类String。

源代码可以点击下面的连接下载:
http://download.csdn.net/detail/ljianhui/7143351

其头文件_stringv2.h如下:
[cpp] view plain copy
  1. #ifndef _STRINGV2_H_INCLUDED  
  2. #define _STRINGV2_H_INCLUDED  
  3. /*** 
  4. String类的部分实现,采用的内存管理策略是:隐式共享,写时复制 
  5. 实现方法:与智能指针的实现类似 
  6. ***/  
  7. class String  
  8. {  
  9.     public:  
  10.         String();  
  11.         String(const String& s);  
  12.         String(const char *pc, size_t len);  
  13.         String(const char *pc);  
  14.         ~String();  
  15.         String& operator=(const String &s);  
  16.         String& operator=(const char *s);  
  17.         String& operator+=(const String &rhs);  
  18.         String& operator+=(const char *rhs);  
  19.         void clear();  
  20.         size_t getLength()const {return _length;}  
  21.         const char* cstr()const {return _cstr;}  
  22.     private://function  
  23.         void _initString(const char *cstr, size_t len);  
  24.         void _decUsed();  
  25.         char* _renewAndCat(const char *cstr, size_t len);  
  26.         void _addString(const char *cstr, size_t len);  
  27.         void _addAssignOpt(const char *cstr, size_t len);  
  28.     private://data  
  29.         char *_cstr;  
  30.         size_t *_used;  
  31.         size_t _length;  
  32.         size_t _capacity;  
  33. };  
  34. String operator+(const String &lhs, const String &rhs);  
  35. std::ostream& operator <<(std::ostream &os, const String &s);  
  36. std::istream& operator >>(std::istream &in, String &s);  
  37. #endif // _STRINGV2_H_INCLUDED  
从上面的String的数据成员,我们可以看到String在其内部维护一个指向堆内存的字符数组的char指针_cstr和一个指向堆内存中字符数组的引用计数的size_t指针_used。本类并没有实现String的所有操作,只是实现了大部分的初始化和String跟写操作有关的函数。

注意:为了说明的方便,我会使用s._cstr等方式来指明一个成员变量所属的对象,或使用*s._cstr等方式来引用一个对象的指针成员所指的内存。但这并不是说在类的外面访问成员变量,只是为了说明的方便和清晰而已。为了方便代码的阅读,类的成员变量或私有函数都以下划线“_”开头。

下面就来一个函数一个函数地解释其实现方式。
1)默认构造函数
[cpp] view plain copy
  1. String::String():  
  2.     _cstr(NULL),  
  3.     _used(new size_t(1)),  
  4.     _length(0),  
  5.     _capacity(0)  
  6. {  
  7. }  
这里需要注意的地方就是,在默认初始化中,我们并不使用new来申请内存,而是直接把_cstr置为NULL,这样做是因为我们不知道程序接下来会做什么动作,贸然为其分配内存是不合理的。例如,对于如下操作,则无需分配内存,
[cpp] view plain copy
  1. String s1("abc");  
  2. String s2;  
  3. s2 = s1;  
根据隐式共享的原则,只需要把s2._cstr的值赋为s1._cstr即可。而为什么没有为对象分配内存,而*_used的值却为1呢?这里只要是为了操作的统一,考虑上面的语句s2 = s1,其产生的操作应该是把s2._used所指向的内存数据(引用计数)的值减1,因为s2._cstr不再指向原先的字符数据。s1._used所指向的内存数据的值加1。若*s2._userd的值为0,就释放s2._userd和s2._cstr所指向的内存。而如果在这里,*s2._userd的初始值为0,0减1就会变成-1,而_userd是一个无符号整数的指针,它的值就会变成2^32-1,从而让程序运行的结果不符合我们的预想。而*s2._userd的初始值为1则可完美地避免这个问题。

2)复制构造函数
[cpp] view plain copy
  1. String::String(const String &s):  
  2.     _cstr(s._cstr),  
  3.     _used(s._used),  
  4.     _length(s._length),  
  5.     _capacity(s._capacity)  
  6. {  
  7.     ++*_used;  
  8. }  
本函数非常易懂,就是把s的成员的值全部复制给*this即可,但是由于多了*this这个对象引用s的字符数组,所以应该把该字符数组的引用计数加1。注意,此时this->_used和s._used指向了同一个对象。

3)带C字串参数的构造函数
[cpp] view plain copy
  1. String::String(const char *cstr, size_t len)  
  2. {  
  3.     if(cstr == NULL)  
  4.         return;  
  5.     size_t str_len = strlen(cstr);  
  6.     if(len <= str_len)  
  7.     {  
  8.         _initString(cstr, len);  
  9.     }  
  10. }  
  11. void String::_initString(const char *cstr, size_t len)  
  12. {  
  13.     if(cstr == NULL)  
  14.         return;  
  15.     _cstr = new char[len + 1];  
  16.     memcpy(_cstr, cstr, len);  
  17.     _cstr[len] = 0;  
  18.     _used = new size_t(1);  
  19.     _length = len;  
  20.     _capacity = len;  
  21. }  
该函数非常简单,由于是构造函数,而且使用的参数是C风格的字符串,所以默认为其字符串一定不是某个对象所引用的字符数组,所以直接为其分配内存,并复制字符。非常明显,因为是该对象第一个创建该字符数组的,所以其引用为1.

4)带C风格字符串的构造函数
[cpp] view plain copy
  1. String::String(const char *cstr)  
  2. {  
  3.     if(cstr == NULL)  
  4.         return;  
  5.     size_t len = strlen(cstr);  
  6.     _initString(cstr, len);  
  7. }  
其实现与上原理相同,只是参数不同,不再详述。

5)析构函数
[cpp] view plain copy
  1. String::~String()  
  2. {  
  3.     _decUsed();  
  4. }  
  5. void String::_decUsed()  
  6. {  
  7.     --*_used;  
  8.     if(*_used == 0)  
  9.     {  
  10.         if(_cstr != NULL)  
  11.         {  
  12.             delete[] _cstr;  
  13.             _cstr = NULL;  
  14.             _length = 0;  
  15.             _capacity = 0;  
  16.         }  
  17.         delete _used;  
  18.         _used = NULL;  
  19.     }  
  20. }  
_decUsed()函数可以说是该类内存释放的管理函数,可以看到,每当一个对象被析构时,其指向的堆中的字符数组的引用计数就会减1,当引用计数为0时,就释放字符数组和引用计数。

6)赋值操作函数
[cpp] view plain copy
  1. String& String::operator=(const String &s)  
  2. {  
  3.     ++*(s._used);  
  4.     _decUsed();  
  5.     _cstr = s._cstr;  
  6.     _length = s._length;  
  7.     _capacity = s._capacity;  
  8.     return *this;  
  9. }  
该赋值操作函数的参数一个本类的对象,该类赋值操作函数第一个要避免的就是自身赋值的问题,在有指针存在的类中是特别要重视这个问题,而在这个String类也不可例外。为什么这样说呢?因为我们调用赋值操作函数时,必须要减少左值的引用计数,增加右值的引用计数,这个在第1)点已经说过了,而如果是自身赋值的话,在减少其引用计数时,其引用计数可能为0,从而导致字符数组的释放,从而让_cstr指针悬空(delete[]掉了,却在赋值的过程中,重新赋为delete[]前的值,即_cstr的值没有在赋值过程中改变)。

一般的程序的做法是判断参数的地址与this是否相等来避免自身赋值,而这里却可以采用巧妙的策略来避免这个问题,可以看到上面的代码并没有if判断语句。我们首先对*s._used加1,这样*s._used至少为2,然后再对*(this->_used)减1,这样即使s与*this是同一个对象,也可以保证*(this->_used)的值至少为1,不会变为0,从而让字符数组不会被释放。因为复制是使用隐式共享的,所以直接复制指针,使指针_cstr其指向与s同一个存在中的字符数组并复制其他的数据成员即可。

同时,我们还要记得返回当前对象的引用。

7)重载的赋值操作函数
[cpp] view plain copy
  1. String& String::operator=(const char *cstr)  
  2. {  
  3.     if(cstr != NULL)  
  4.     {  
  5.         _decUsed();  
  6.         size_t len = strlen(cstr);  
  7.         _initString(cstr, len);  
  8.     }  
  9.     return *this;  
  10. }  
该赋值操作函数的参数一个C风格的字符串,因而不会发生自身赋值的问题。与String(const char *cstr)函数相似,唯一不同的是使用赋值操作函数时,对象已经存在,所以要调用_decUsed来减少该对象的_cstr原先指向的字符数组的引用计数。然后生成根据cstr创建一个全新的字符数组。并返回当前对象的引用。

8)重载+=操作符,实现字符串连接
[cpp] view plain copy
  1. String& String::operator+=(const String &s)  
  2. {  
  3.     _addAssignOpt(s._cstr, s._length);  
  4.     return *this;  
  5. }  
  6. String& String::operator+=(const char *cstr)  
  7. {  
  8.     if(cstr != NULL)  
  9.         _addAssignOpt(cstr, strlen(cstr));  
  10.     return *this;  
  11. }  
  12.   
  13. void String::_addAssignOpt(const char *cstr, size_t len)  
  14. {  
  15.     if(*_used == 1)  
  16.         _addString(cstr, len);  
  17.     else  
  18.     {  
  19.         _decUsed();  
  20.         _cstr = _renewAndCat(cstr, len);  
  21.         _used = new size_t(1);  
  22.     }  
  23. }  
  24. void String::_addString(const char *cstr, size_t len)  
  25. {  
  26.     //本函数,只有在引用计数为1时,才可用  
  27.     if(*_used != 1)  
  28.         return;  
  29.     if(len + _length > _capacity)  
  30.     {  
  31.         char *ptr = _renewAndCat(cstr, len);  
  32.         delete[] _cstr;  
  33.         _cstr = ptr;  
  34.     }  
  35.     else  
  36.     {  
  37.         strncat(_cstr, cstr, len);  
  38.         _length += len;  
  39.     }  
  40. }  
  41. char* String::_renewAndCat(const char *cstr, size_t len)  
  42. {  
  43.     size_t new_len = len + _length;  
  44.     size_t capacity = new_len;  
  45.     capacity += (capacity >> 1);  
  46.     char *ptr = new char[capacity+1];  
  47.     if(_cstr != NULL)  
  48.         memcpy(ptr, _cstr, _length);  
  49.     ptr[_length] = 0;  
  50.     _length = new_len;  
  51.     _capacity = capacity;  
  52.     strncat(ptr, cstr, len);  
  53.     return ptr;  
  54. }  
+=是一个复杂的操作,也是我实现时琢磨得最久的操作。因为它是写的操作,根据写时拷贝的原则,它需要减少其原先字符数组的引用计数,同时创建一个新的字符数组来储存增加长度后的字符串。并且该对象所引用的字符数组可能只有该对象自己在引用,也就是说其引用计数为1,此时减少其引用计数还可能导致原先字符数组的释放,从而丢失数据,并在使用指向原先字符数组的指针进行数据复制时发生错误。所以引用计数是否为1应该采用不同的策略。

而当引用计数为1时,我们可以认为该对象独立享有该字符数组,可以对其进行任何操作而不影响其他对象,这时,我们可以把字符串直接追加到已经的字符数组的后面,而这样做可能因为字符数组的容量不够而不能进行,这时为字符数组的重新分配合适的空间。

当引用计数不为1时,我们首先调用_decUsed()来减少原字符数组的引用计数,然后调用_renewAndCat来连接并产生新的字符数组,然后重置_cstr的指向,并new一个新的引用计数,初始值置为1.

上面的函数中,_renewAndCat的功能就是分配新的字符数组,同时把原先的字符数据复制到新的字符数组中,再在新的字符数组中追加字符串,返回新的字符数组的首地址。_addString是只有当引用计数为1时才能调用的函数,其字符数组足以容纳连接后的字符串,则直接连接,若不能,则调用_renewAndCat重新分配合适的字符数组,并进行复制,最后,把旧的字符数组delete[]掉,再把_cstr赋值为_renewAndCat创建的新字符数组的首地址。

注:本人认为,如果一个字符串对象做了一次+=运算,那么它很可能会很快做第二次,所以在分配内存时,我采用了预分配的策略,每次分配都分配连接完成后的字符串的长度的1.5倍。这样当下一次执行+=时,字符数组就可能有足够多的容量来保存连接后的字符串,而不用重新分配和复制。而且我们注意到,当调用+=一次之后,*_used肯定为1,即下次运行+=时,是极有可能直接加到字符数组的后面的。

为了提高程序的运行效率,在进行1.5倍的预分配时,没有使用浮点数乘法,更没有使用除法,而是采用了移位运算,如下:
int capacity = len;
capacity += (capacity >> 1)
其对应的数学表达式为:“x = capacity + capacity/2;capacity = x”。因为右移运算相当于除以2,这样就实现了乘以1.5的运算操作。

9)清空字符串
[cpp] view plain copy
  1. void String::clear()  
  2. {  
  3.     _decUsed();  
  4.     _cstr = NULL;  
  5.     _used = new size_t(1);  
  6.     _length = 0;  
  7.     _capacity = 0;  
  8. }  
该函数用于清除字符串对象引用的字符数组的数据,所以我们只需要调用_decUsed函数,减少对象所引用的字符数组的引用计数,并把其他成员变量设置为默认的值即可。即与默认构造函数所设置的值一致。


注:以下函数不是String的成员函数
10)重载+操作符
[cpp] view plain copy
  1. String operator +(const String &lhs, const String &rhs)  
  2. {  
  3.     String stemp(lhs);  
  4.     stemp += rhs;  
  5.     return stemp;  
  6. }  
该函数的实现可以借助上面实现的+=操作符,先用第一个对象rhs复制构造一个临时对象stemp,然后通过把第二个参数追加到临时对象stemp上,返回stemp即可简单轻松地实现+操作符的重载。

11)重载输出操作符
[cpp] view plain copy
  1. ostream& operator << (ostream &os, const String &s)  
  2. {  
  3.     os<<s.cstr();  
  4.     return os;  
  5. }  
12)重载输入操作符
[cpp] view plain copy
  1. istream& operator >> (istream &in, String &s)  
  2. {  
  3.     const int BUFFER_SIZE = 256;  
  4.     char buffer[BUFFER_SIZE];  
  5.     char *end = buffer + BUFFER_SIZE -1;  
  6.     s.clear();  
  7.     do  
  8.     {  
  9.         //用于判断是否读完输入内容,因为如果还未读取的输入字符数大于buffer  
  10.         //的容量,则buffer的最后一个字符会被get函数置为'\0'  
  11.         *end = '#';  
  12.         in.get(buffer, BUFFER_SIZE);  
  13.         s += buffer;  
  14.     }while(*end == '\0');  
  15.     in.get();  
  16.     return in;  
  17. }  
实现输入操作符的重载的一个困难之处就是我们不知道用户要输入的字符串的长度,也就不知道应该分配一个多大的缓冲区来接收输入的字符。所以在这里,设置一个一定大小的缓冲,采用循环读取,连续添加到字符串对象中的方法来实现。那么如何知道该循环读取输入多少次呢?也就是说,怎么知道已经把所有的输入字符读取完毕呢?在这里,我使用了一个标准输入流istream的get成员函数,该成员函数从输入流中读取指定个数的字符或遇到输入流结束而返回,注意最后它会自动加入一个空字符‘\0’作为结束。这个空字符也作为读入的字符数量的计数。例如,如果有一个大小为6的char型数组作为buffer,从标准输入流中读入6个字符,实际上只会从标准输入中读入最多5个字符(因为可能遇到流结束),并把空字符‘\0’加入到buffer的末尾。

所以我们可以把buffer的最后一个字节,设置成我们自己特定的一个字符(只是是非‘\0’即可),如这里的'#',然后读入buffer大小的字符数。若还没有读取完毕,我们设置的这个特殊的字符会被空字符'\0'覆盖,我们从而知道,还没读取完标准输入的数据。若我们设置的特殊字符没有被覆盖,就说明,读到的数据不足以填满buffer,也就是说,我们已经没有数据可读了,从而可以判断已经读取完所有输入的字符。

注:输入也是一个写的操作,并且会把对象之前的内容覆盖掉,所以在输入到对象之前,要先调用clear成员函数,把对象清空。

四、测试代码
[cpp] view plain copy
  1. #include <iostream>  
  2. #include "_stringv2.h"  
  3. using std::cin;  
  4. using std::cout;  
  5. using std::cin;  
  6. using std::endl;  
  7. int main()  
  8. {  
  9.     String s1;  
  10.     s1 = "abc";  
  11.     {  
  12.         String s2(s1);  
  13.         s2 += s1;  
  14.         cout << s2 << endl;  
  15.     }  
  16.     String s3(s1);  
  17.     cin >> s3;  
  18.     cout << s3 << endl;  
  19.     cout << s1 << endl;  
  20.     String s4 = s1 + s3;  
  21.     cout << s4 << endl;  
  22. }  
运行结果如下:

五、代码分析
首先定义一个String的对象s1,s1调用默认构造函数,生成一个默认的对象,然后调用赋值操作函数,为s1分配堆内存字符数组。对象s2是以对象s1的样本复制构造出来的对象,其作用域只在花括号内。我们可以看到s2的改变并没有影响到s1。其他的调用也一样,从而可以看到是实现了隐式共享,写时拷贝。从运行的结果可以看出,一切的运行都是没有问题的,与标准库的string的输出一致。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/383778.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++ String类写时拷贝 4

http://blog.51cto.com/zgw285763054/1839752 维基百科&#xff1a; 写入时复制&#xff08;英语&#xff1a;Copy-on-write&#xff0c;简称COW&#xff09;是一种计算机程序设计领域的优化策略。其核心思想是&#xff0c;如果有多个调用者&#xff08;callers&#xff09;同时…

C++笔试复习

基础知识点 C中对象数组在定义的时候全部进行实例化&#xff08;与Java不同&#xff0c;Java相当于只是定义了一个指针数组&#xff0c;没有进行实例化&#xff09; 程序的三种基本控制结构是&#xff1a;顺序结构、循环结构、选择结构 一个C程序开发步骤通常包括&#xff1a…

Java笔试复习

Java程序运行 Java程序的执行必须经过编辑、编译和运行三个步骤 编辑指编写代码&#xff0c;最终形成后缀名为.java的Java源文件编译指使用Java编译器&#xff08;javac指令&#xff09;将源文件翻译为二进制代码&#xff0c;编译后生成后缀名为.class的字节码文件&#xff0c…

Java类名与包名不区分大小写

刚才写了一个简单的Java程序&#xff0c;经过测试得到一个令人震惊的结论&#xff1a;Java类名和包名是不区分大小写的 可以看一下这个例子&#xff1a; package Test;class aBcdEfG {}class AbCdefg {}public class TTT {public static void main(String[] args){AbCdefg tm…

处理大并发之一 对epoll的理解,epoll客户端服务端代码

http://blog.csdn.net/zhuxiaoping54532/article/details/56494313处理大并发之一对epoll的理解&#xff0c;epoll客户端服务端代码序言&#xff1a;该博客是一系列的博客&#xff0c;首先从最基础的epoll说起&#xff0c;然后研究libevent源码及使用方法&#xff0c;最后研究n…

链栈基本操作

http://blog.csdn.net/jwentao01/article/details/46765517###;栈基本概念&#xff1a; 栈&#xff08;stack&#xff09;是限定在表尾进行插入和删除操作的线性表&#xff08;或单链表&#xff09;。 //只能在一段进行插入和删除&#xff0c;因此不存在&#xff0c;在中间进行…

Linux网络编程---I/O复用模型之select

https://blog.csdn.net/men_wen/article/details/53456435Linux网络编程—I/O复用模型之select 1. IO复用模型 IO复用能够预先告知内核&#xff0c;一旦发现进程指定的一个或者多个IO条件就绪&#xff0c;它就通知进程。IO复用阻塞在select或poll系统调用上&#xff0c;而不是阻…

Linux网络编程---I/O复用模型之poll

https://blog.csdn.net/men_wen/article/details/53456474Linux网络编程—I/O复用模型之poll 1.函数poll poll系统调用和select类似&#xff0c;也是在指定时间内轮询一定数量的文件描述符&#xff0c;以测试其中是否有就绪者。 #include <poll.h>int poll(struct pollfd…

Linux网络编程---I/O复用模型之epoll

https://blog.csdn.net/men_wen/article/details/53456474 Linux网络编程—I/O复用模型之epoll 1. epoll模型简介 epoll是Linux多路服用IO接口select/poll的加强版&#xff0c;e对应的英文单词就是enhancement&#xff0c;中文翻译为增强&#xff0c;加强&#xff0c;提高&…

POJ 1741tree-点分治入门

学习了一下点分治&#xff0c;如果理解有误还请不吝赐教。 为了快速求得树上任意两点之间距离满足某种关系的点对数&#xff0c;我们需要用到这种算法。 点分治是树上的一种分治算法&#xff0c;依靠树和子树之间的关系进行分治从而降低复杂度。 和其他树上的算法有一些区别…

基于单链表的生产者消费者问题

『生产者与消费者问题分析』「原理」生产者生产产品&#xff0c;消费者消费产品。产品如果被消费者消费完了&#xff0c;同时生产者又没有生产出产品&#xff0c;消费者 就必须等待。同样的&#xff0c;如果生产者生产了产品&#xff0c;而消费者没有去消费&#x…

C++智能指针(二)模拟实现三种智能指针

https://blog.csdn.net/nou_camp/article/details/70186721在上一篇博客中提到了Auto_ptr(C智能指针&#xff08;一&#xff09;)&#xff0c;下面进行模拟实现Auto_ptr 采用类模板实现 #include<iostream> using namespace std; template<class T> class Autoptr …

C++智能指针(三)总结

https://blog.csdn.net/nou_camp/article/details/70195795 在上一篇博客中&#xff08;C智能指针&#xff08;二&#xff09;&#xff09;模拟实现了三种智能指针。 其中最好的就是shared_ptr,但是这并不代表它就是最完美的&#xff0c;它也有问题&#xff0c;这个问题就是循环…

c++11 你需要知道这些就够了

https://blog.csdn.net/tangliguantou/article/details/50549751c11新特性举着火把寻找电灯今天我就权当抛砖引玉&#xff0c;如有不解大家一起探讨。有部分内容是引用自互联网上的内容&#xff0c;如有问题请联系我。T&& 右值引用 std::move 右值引用出现之前我们只能…

c++仿函数 functor

https://www.cnblogs.com/decade-dnbc66/p/5347088.html内容整理自国外C教材先考虑一个简单的例子&#xff1a;假设有一个vector<string>&#xff0c;你的任务是统计长度小于5的string的个数&#xff0c;如果使用count_if函数的话&#xff0c;你的代码可能长成这样&#…

Ubuntu软件更新失败

刚安装好Ubuntu以后需要将系统的软件都更新一下&#xff0c;但是遇到一个问题就是下载仓库信息失败&#xff0c;大概是这个样子的错误&#xff1a; 经国遇到这样的问题可以试一下下面这个命令&#xff1a; sudo rm -rf /var/lib/apt/lists/* sudo apt-get update参考网址&…

getsockname函数与getpeername函数的使用

https://www.tuicool.com/articles/V3Aveygetsockname和getpeername函数 getsockname函数用于获取与某个套接字关联的本地协议地址 getpeername函数用于获取与某个套接字关联的外地协议地址 定义如下&#xff1a;[cpp] view plaincopy#include<sys/socket.h> int gets…

Linux命令【一】基本命令

shell命令和bash命令相同&#xff0c;指的是命令解析器 快捷键 history 所有的历史命令ctrl P 向上滚动命令 ctrl N 向下滚动命令 ctrlB将光标向前移动 ctrlF将光标向后移动 ctrlA移动到命令行头部 ctrlE移动到命令行尾部 光标删除操作&#xff1a;删除光标前面字符ctrlh或…

剑指offer面试题:替换空格

https://blog.csdn.net/yanxiaolx/article/details/52235212题目&#xff1a;请实现一个函数&#xff0c;把字符串中的每个空格替换成“%20”。例如输入“We are happy.”&#xff0c;则输出“We%20are%20happy.”。解析&#xff1a;时间复杂度为O(n)的解法。完整代码及测试用例…

数据库原理及应用【一】引言

什么是数据库&#xff1a;一个大规模的集成的数据集合 作用&#xff1a;描述现实世界的实体(entities)以及实体之间的关系 管理数据库的系统软件&#xff1a;DBMS 文件是一个平滑的字符流&#xff0c;无法完成信息的检索和管理 数据&#xff08;data&#xff09;:用来描述现…