| Severus-Yuan的专栏 | 链接🔗 |
|---|---|
| C语言 | C语言 |
| 初阶数据结构与算法 | 初阶数据结构与算法 |
| 数据结构算法题 | 数据结构算法题 |
| C++ | C++ |
文章目录
- (一)编码(了解即可)
- (二)深浅拷贝的传统现代写法
- (1)传统写法
- (2)现代写法
- 1.拷贝构造
- 继续优化:
- 2.赋值
- 继续优化:
- 3.测试
- (3)全局swap和类成员swap
- (三)引用计数的写时拷贝(了解即可)
- vs和g++下string结构的说明:
(一)编码(了解即可)
内存和磁盘中只有0/1,如何用0/1去表示世界上的文字呢?
在C语言中我们了解了ASCII码,ASCII码就是一种编码,编码是值和符号的一种映射关系。
ASCII值在计算机中存的实际就是八个比特位表示的值。(这八个比特位先换成整型,再通过整型去ASCII表找对应的字符。)
但很显然,只有ASCII码是不够的,全世界有这么多文字,又该怎么办呢?这时候就出现了unicode(编码国际所有文字)。
这也能说明为啥string是模板了,因为字符的编码方案有多种,那么对应的类的实例化就有多种。
还有一种编码是GBK(编码中日韩文字)。
(二)深浅拷贝的传统现代写法
(1)传统写法
在博客【C++初阶】string类的模拟实现,近三万字呕心沥血详细分析!中,深拷贝的拷贝函数、赋值运算符重载函数都是自己手动开辟的内存空间,自己完成拷贝或赋值操作。
//string.h//拷贝构造函数string(conststring&s){_ptr=newchar[strlen(s._ptr)+1];strcpy(_ptr,s._ptr);//会拷贝'\0'_capacity=s._capacity;_size=s._size;}//赋值string&operator=(conststring&s){if(s!=*this){delete[]_ptr;_ptr=newchar[strlen(s._ptr+1)];strcpy(_ptr,s._ptr);_size=s._size;_capacity=s._capacity;}return*this;}(2)现代写法
现代写法下的深拷贝倾向于说把活外包出去,让别人帮我们干活。
1.拷贝构造
//string.h//*this(s)string(conststring&s){stringtmp(s._ptr);//交换指针、_size和_capacityswap(_ptr,tmp._ptr);swap(_size,tmp._size);swap(_capacity,tmp._capacity);}分析一下为什么要这么写:
此处拷贝函数的目的是将s的内容拷贝给this指向的内容。首先,我们用s的_ptr指针构造一个tmp的string类对象(有单参数构造函数,此处为隐式类型转换),tmp和s的_ptr指针都指向相同的内容,但_ptr不同,_size相同但是_capacity不一定相同。然后再交换this->_ptr和tmp._ptr(地址交换,_ptr指向的内容没变)、this->_size和tmp._size、this->_capacity和tmp._capacity,这样s的内容就通过tmp间接拷贝给了this。
注意:拷贝构造本质上就是构造,这里新对象的构造调用的只有拷贝构造,进入现代写法的拷贝构造函数之后,并未对*this对象new空间,所以_ptr/_size/_capacity都是随机值。在交换之后,tmp得到的就是这三个随机值。_size和_capacity倒是问题不大,但tmp出函数了是要析构的,而且还是调用手动实现的析构函数(手动实现的析构函数才会对_ptr指向的堆内存进行释放)。delete[]不能是随机值,但可以是nullptr,因为对nullptr的情况做了特殊处理。
所以我们需要在声明处给缺省值。
//string.hnamespaceyuan{classstring{private:char*_ptr=nullptr;size_t _capacity=0;size_t _size=0;conststaticsize_t npos;}};图示分析:
继续优化:
实现现代写法的拷贝构造函数时连着使用了三个swap函数,不如我们直接实现一个专门用来交换string类对象的_ptr/_size/_capacity的swap函数。
//string.hvoidswap(string&s){std::swap(_ptr,s._ptr);std::swap(_size,s._size);std::swap(_capacity,s._capacity);}//this(s)string(conststring&s){stringtmp(s._ptr);//交换指针、_size和_capacityswap(tmp);}2.赋值
//string.hvoidswap(string&s){std::swap(_ptr,s._ptr);std::swap(_size,s._size);std::swap(_capacity,s._capacity);}//*this = sstring&operator=(conststring&s){if(&s!=this){//构造stringtmp(s._ptr);//拷贝构造//string tmp(s);swap(tmp);}return*this;}现代写法下的赋值运算符重载函数和现代写法下的拷贝构造的逻辑简直一模一样,只是说拷贝构造tmp出函数之后析构的是空,但赋值tmp出函数之后是把s的_ptr原来指向的空间真的释放了。
继续优化:
//string.hstring&operator=(string tmp){//不用再考虑自己给自己赋值了,等进入函数内部,早拷贝完了swap(tmp);return*this;}3.测试
//test.cppnamespaceyuan{voidtest15(){strings15("hello");strings16(s15);cout<<s16<<endl;strings17("bank");strings18("yuan");cout<<s18<<" ";s18=s17;cout<<s18<<endl;}}intmain(){yuan::test15();return0;}(3)全局swap和类成员swap
全局:
string类对象的全局swap是由库里的swap模板实例化而来的,模板如下:
类成员:
类成员的swap函数就和我们在这手动实现的swap函数逻辑一样,交换两个string类对象的_ptr、_size和_capacity。
但全局swap函数代价就太大了,根据模板可知,会进行三次深拷贝。
不过其实调用哪个没差,根据string类的全局swap可知,其实最后也会调用string类的成员函数swap,可以理解成在模板和现成的成员函数之间,那肯定还是调现成的,除非模板能生成更匹配的函数。
(三)引用计数的写时拷贝(了解即可)
浅拷贝会带来改值、覆盖、析构两次或内存泄漏等多个问题,所以我们选择深拷贝,但是深拷贝的代价是要申请一块新空间,我们不禁想:所有情况下都必须要进行深拷贝吗?有没有即使有_ptr但也可以浅拷贝的情况呢?
其实是有的。
对于析构两次或内存泄漏的问题,使用浅拷贝完全没问题。但需要额外加一个引用计数,用于记录有多少个对象指向了同一块内存空间。
- 析构两次:譬如此时有n个对象指向了同一块内存空间,那么引用计数就是n,每当一个对象不指向这块空间时,就n–,只有当n减到0时的那个对象才会调用析构函数,完成对内存空间的回收释放;
- 内存泄漏:进行赋值前的赋值空间失去了唯一的指向对象,那n–得0,会调用析构函数完成对内存空间的回收释放,这样就不会造成内存泄漏的问题了。
对于改值、覆盖的问题,如果引用计数为1,那就可以放心修改,但是如果引用计数>1就不能原空间更改了,还是要老老实实地创建空间进行深拷贝。这也就是为啥叫做引用计数的写时拷贝——等要写值(修改)的时候才进行深拷贝。
既然有时还是得深拷贝,那为啥还费这么大的劲搞一个引用计数的写时拷贝出来?可以理解成博弈思想吧,但凡出现不用改值的情况可以用浅拷贝的情况,那就是赚了。
vs和g++下string结构的说明:
初学C++,如有错误,欢迎支持。
——end——