模拟实现string类不是为了造一个更好的轮子,而是更加理解string类,从而来掌握string类的使用
string类的接口设计繁多,故而不会全部涵盖到,但是核心的会模拟实现
库中string类是封装在std的命名空间中的,所以在模拟实现中我们也可以用命名空间来封装
string类的实现可以在.h头文件中,.cpp文件中再来实际操作string类对象
完整的代码实现:
namespace djx
{class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin()const{return _str;}const_iterator end()const{return _str + _size;}string(const char* str = ""):_size(strlen(str)),_capacity(_size){_str = new char[_capacity + 1];strcpy(_str, str);}//传统写法/*string(const string& s){_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}*/void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//现代写法string(const string& s):_str(nullptr),_size(0),_capacity(0){string tmp(s._str);swap(tmp);}//传统写法/* string& operator=(const string& s){if (this != &s){char* tmp = new char[s._capacity + 1];strcpy(tmp, s._str);delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;}return *this;}*///现代写法1/*string& operator=(const string& s){if (this != &s){string tmp(s);swap(tmp);}return *this;}*///现代写法2string& operator=(string s){swap(s);return *this;}~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}const char* c_str()const{return _str;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}const char& operator[](size_t pos)const{assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;_size++;_str[_size] = '\0';}void append(const char* s){size_t len = strlen(s);if (_size + len > _capacity){reserve(_size + len);}strcpy(_str+_size, s);_size += len;}string& operator+=(char ch){push_back(ch);return *this;}string& operator+=(const char* s){append(s);return *this;}/*void insert(size_t pos, char ch){assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}int end = _size;while (end >=(int) pos){_str[end + 1] = _str[end];end--;}_str[pos] = ch;_size++;}*/void insert(size_t pos, char ch){assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}size_t end = _size+1;while (end >pos){_str[end] = _str[end-1];end--;}_str[pos] = ch;_size++;}void insert(size_t pos, const char* s){assert(pos <= _size);size_t len = strlen(s);if (_size + len > _capacity){reserve(_size + len);}int end = _size;while (end >=(int) pos){_str[end + len] = _str[end];end--;}strncpy(_str + pos, s, len);_size += len;}void erase(size_t pos, size_t len = npos){assert(pos < _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{size_t begin = pos + len;while (begin <= _size){_str[begin - len] = _str[begin];begin++;}_size -= len;}}bool operator<(const string& s)const{return strcmp(_str, s._str) < 0;}bool operator==(const string& s)const{return strcmp(_str, s._str) == 0;}bool operator<=(const string& s)const{return *this < s || *this == s;}bool operator>(const string& s)const{return !(*this <= s);}bool operator>=(const string& s)const{return !(*this < s);}bool operator!=(const string& s)const{return !(*this == s);}void resize(size_t n,char ch='\0'){if (n <= _size){_str[n] = '\0';_size = n;}else{reserve(n);while (_size < n){_str[_size] = ch;_size++;}_str[_size] = '\0';}}size_t find(char ch, size_t pos = 0){for (size_t i = pos; i < _size; i++){if (_str[i] == ch){return i;}}return npos;//没有找到}size_t find(const char* s, size_t pos=0){const char* p = strstr(_str+pos, s);if (p){return p - _str;}else{return npos;}}string substr(size_t pos, size_t len = npos){string s;size_t end = pos + len;if (len == npos || pos + len >= _size){len = _size - pos;end = _size;}s.reserve(len);for (size_t i = pos; i < end; i++){s += _str[i];}return s;}size_t size()const{return _size;}size_t capacity()const{return _capacity;}void clear(){_str[0] = '\0';_size = 0;}private:char* _str;size_t _size;size_t _capacity;public:const static size_t npos;//const static size_t npos=-1;//特例//const static double npos = 1.1; // 不支持};const size_t string::npos = -1;ostream& operator<<(ostream& out, const string& s){for (auto e : s){out << e;}return out;}istream& operator>>(istream& in, string& s){s.clear();char buff[129];size_t i = 0;char ch;ch = in.get();while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 128){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i != 0){buff[i] = '\0';s += buff;}return in;}
}
构造函数和析构函数:
namespace djx
{class string{public:string(const char* str = "")//构造函数:_size(strlen(str))//在对象中的成员都要走初始化列表(成员们定义的地方),_capacity(_size){_str = new char[_capacity + 1];//多开一个空间给'\0'strcpy(_str, str);}~string()//析构函数{delete[] _str;//释放空间+调用对象的析构函数(用于清理申请的资源)_str = nullptr;_size = _capacity = 0;}const char* c_str()const//返回c格式的字符串,加const用以修饰this指针,让const对象可以调用,非const对象也是可以调用的{return _str;}private:char* _str;//指向存储字符串的空间size_t _size;//有效字符的个数size_t _capacity;//存储有效字符的容量};
}
测试:
string类的对象可以重载流插入,流提取运算符,但现在我们还没有实现,又想查看string类中字符串的内容,可以用c_str 得到_str
void test1()
{djx::string s("hello");cout << s.c_str() << endl;djx::string s2;cout << s2.c_str() << endl;
}
构造函数:
1 库中string实现:对于无参的string,初始化为空字符串,对于有参的string,则初始化为参数内容
所以可以给构造函数缺省参数,缺省值是"",注意不能是" " 因为空格也是有效字符,也没必要是"\0",因为常量字符串自动会带有一个'\0'
2 _size 和_capacity是不算'\0'的大小的,它只是一个标识字符,因为c语言需要'\0'作为字符串的结束标志,c++不能抛弃c,所以'\0'被保留下来了
实际在开空间时也是需要为'\0'预留一个空间的
string的三种遍历方式:
operator[]重载:
#include<assert.h>
namespace djx
{class string{public:string(const char* str = ""):_size(strlen(str)),_capacity(_size){_str = new char[_capacity + 1];strcpy(_str, str);}~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}const char* c_str()const{return _str;}char& operator[](size_t pos)//非const对象调用,返回引用,可读可写{assert(pos < _size);return _str[pos];}const char& operator[](size_t pos)const//const对象调用,只读不可写{assert(pos < _size);return _str[pos];}size_t size()const//const对象可以调用,非const对象也可以调用{return _size;}size_t capacity()const{return _capacity;}private:char* _str;size_t _size;size_t _capacity;};
}
测试:
void test2()
{djx::string s("hello");for (size_t i = 0; i < s.size(); i++){cout << s[i];}cout << endl;
}
迭代器:
string类的迭代器可以看作是指针,因为它完美契合指针的行为
namespace djx
{class string{public:typedef char* iterator;//非const对象的迭代器typedef const char* const_iterator;//const对象的迭代器iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin()const{return _str;}const_iterator end()const{return _str + _size;}string(const char* str = ""):_size(strlen(str)),_capacity(_size){_str = new char[_capacity + 1];strcpy(_str, str);}~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}const char* c_str()const{return _str;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}const char& operator[](size_t pos)const{assert(pos < _size);return _str[pos];}size_t size()const{return _size;}size_t capacity()const{return _capacity;}private:char* _str;size_t _size;size_t _capacity;};
}
测试:
void test3()
{djx::string s("hello");cout << s.c_str() <<endl;djx::string::iterator it = s.begin();while (it != s.end()){(*it)++;//写cout << *it;//读it++;}cout << endl;
}
范围for:
范围for的底层原理是迭代器,所以有了迭代器就可以使用范围for
测试:
void test4()
{djx::string s("hello");cout << s.c_str() << endl;for (auto &e : s)//不加引用就是只读{e++;//写cout << e;//读}cout << endl;
}
插入和删除操作:
push_back:
尾插一个字符,插入之前需要检查是否需要扩容,扩容扩至原来的2倍
注意:_size==_capacity 可能是第一次插入,双方都是0,那么2*_capacity就是0,所以如果是第一次插入的扩容,可以扩4个空间
扩容可以使用reserve:
1 开n个空间,实际上reserve要开n+1个空间,因为要存'\0'
2 拷贝数据到新空间
3 释放旧空间
4 指向新空间
append:
插入之前要检查是否需要扩容:原有效字符个数+要插入的有效字符个数>_capacity则扩容
operator+=:
目前阶段的完整代码:
#include<assert.h>
namespace djx
{class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin()const{return _str;}const_iterator end()const{return _str + _size;}string(const char* str = ""):_size(strlen(str)),_capacity(_size){_str = new char[_capacity + 1];strcpy(_str, str);}~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}const char* c_str()const{return _str;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}const char& operator[](size_t pos)const{assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;_size++;_str[_size] = '\0';}void append(const char* s){size_t len = strlen(s);if (_size + len > _capacity){reserve(_size + len);}strcpy(_str+_size, s);_size += len;}string& operator+=(char ch){push_back(ch);return *this;}string& operator+=(const char* s){append(s);return *this;}size_t size()const{return _size;}size_t capacity()const{return _capacity;}private:char* _str;size_t _size;size_t _capacity;};
}
测试:
void test5()
{djx::string s("hello");cout << s.c_str() << endl;s.push_back(' ');s.append("world");cout << s.c_str() << endl;s += '#';s += "!!!!!!!!!";cout << s.c_str() << endl;djx::string s2;s2 += '#';s2 += "!!!!!!!!!!";cout << s2.c_str() << endl;
}
insert:
插入一个字符,版本1:
在pos位置插入一个字符,就需要从'\0'开始直到pos位置,将这些位置上的数据全部向后挪动一位
不要忘记把'\0'一并移走
注意:如果是头插,pos==0,且end是size_t类型,那么循环的结束条件是end<pos
即end<0,但由于end是size_t类型是不会<0的,让end写成int类型,那么end可以达到-1,为避开隐式类型转换,将end由int提升为size_t,所以pos也要强制为int类型,这样当pos为0时,end可以达到-1,-1<0结束循环
版本2:
版本1利用强转来解决循环条件的结束问题,版本2不强转:
将end作为最后一个要挪动的数据的最终位置,end==pos+1的位置时,pos位置上的值已经挪到pos+1上了,当end==pos位置时,结束循环
插入常量字符串:
可以尾插,所以pos可以是_size
1 检查原有效字符个数+要插入的有效字符格式是否>_capacity,大于则扩容
2 从'\0'位置开始,一直到pos位置上的数据,将它们全部向后挪动len步
3 将常量字符串拷贝到_str+pos的位置,只要拷贝len个,不要使用strcpy全部拷贝,因为会拷贝到'\0'
erase:
有两种情况:
一:从pos位置开始,有多少删多少
1 当没有给len传参时,len使用缺省值npos,(size_t类型的-1,很大的数字,但是一个字符串不会有那么大,所以npos通常可以理解为有多少要多少),即从pos位置开始全部删除
2 传参给len,但若pos+len>=_size (pos+len是要删除的最后一个数据的下一个位置),也是从pos位置开始全部删除
方法:在pos位置给'\0'
二:从pos位置开始,删除len个字符
pos+len是合法的,那么从pos+len位置开始一直到'\0',要将它们全部向前移动len步,覆盖效果
不要忘记移动'\0'
len给一个缺省值npos(const修饰的静态变量,值为-1)
静态成员变量不在对象中,不走初始化列表,在类外定义,给初始值-1
但是 const修饰的静态整型成员变量是一个特例,可以在声明的地方给缺省值
其实在常规情况下,成员变量声明的地方给缺省值,就代表它们在走初始化列表的时候可以被初始化
流插入和流提取重载:
流插入:
ostream& operator<<(ostream& out, const string& s){for (auto e : s){out << e;}return out;}
会有cout<<s<<s2的情况,所以有ostream类型的返回值 ,out出了作用域还在所以可以用传引用返回提高效率
流提取:
void clear(){_str[0] = '\0';_size = 0;}
istream& operator>>(istream& in, string& s){s.clear();//在向string类的对象输入内容时,要情况原有的所有数据char buff[129];size_t i = 0;char ch;ch = in.get();while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 128){buff[i] = '\0';s += buff;i = 0;//从头再来}ch = in.get();}if (i != 0)//可能字符串的长度没有达到128个{buff[i] = '\0';s += buff;}return in;}
会有cin>>s>>s2的情况,所以有istream类型的返回值
在输入的字符串很长的情况下,若是提取一个字符便+=到string类的对象中,难免会有多次扩容,
若以为了提高效率,可以开一个能存储129个字符的buff数组,提取的字符先存储到buff数组中
当有了128个字符时,加入'\0',再将buff数组中的所有字符以字符串的形式一并+=到string类的对象中
buff数组就像一个蓄水池,水满了就全部拿走,然后再接着蓄水,满了再全部拿走
注意:cin和scanf一样,不能提取到空格或者'\n',scanf中用getchar可以提取任意字符,那在cin中,有get可以提取任意字符
测试:
void test6()
{djx::string s("hello world");cout << s.c_str() << endl;//没有重载流插入运算符之前,打印string类对象的内容s.insert(5, '%');cout << s.c_str() << endl;s.insert(s.size(), '%');cout << s.c_str() << endl;s.insert(0, '%');cout << s.c_str() << endl;djx::string s2("hello world");s2.insert(5, "abc");cout << s2 << endl;//重载流插入运算符之后,打印string类对象的内容s2.insert(0, "xxx");cout << s2 << endl;s2.erase(0, 3);cout << s2 << endl;s2.erase(5, 100);cout << s2 << endl;s2.erase(2);cout << s2 << endl;}
void test7()
{djx::string s("hello world");cout << s << endl;cin >> s;cout << s << endl;
}
目前为止的完整代码:
#include<assert.h>
namespace djx
{class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin()const{return _str;}const_iterator end()const{return _str + _size;}string(const char* str = ""):_size(strlen(str)),_capacity(_size){_str = new char[_capacity + 1];strcpy(_str, str);}~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}const char* c_str()const{return _str;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}const char& operator[](size_t pos)const{assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;_size++;_str[_size] = '\0';}void append(const char* s){size_t len = strlen(s);if (_size + len > _capacity){reserve(_size + len);}strcpy(_str+_size, s);_size += len;}string& operator+=(char ch){push_back(ch);return *this;}string& operator+=(const char* s){append(s);return *this;}/*void insert(size_t pos, char ch){assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}int end = _size;while (end >=(int) pos){_str[end + 1] = _str[end];end--;}_str[pos] = ch;_size++;}*/void insert(size_t pos, char ch){assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}size_t end = _size+1;while (end >pos){_str[end] = _str[end-1];end--;}_str[pos] = ch;_size++;}void insert(size_t pos, const char* s){assert(pos <= _size);size_t len = strlen(s);if (_size + len > _capacity){reserve(_size + len);}int end = _size;while (end >=(int) pos){_str[end + len] = _str[end];end--;}strncpy(_str + pos, s, len);_size += len;}void erase(size_t pos, size_t len = npos){assert(pos < _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{size_t begin = pos + len;while (begin <= _size){_str[begin - len] = _str[begin];begin++;}_size -= len;}}size_t size()const{return _size;}size_t capacity()const{return _capacity;}void clear(){_str[0] = '\0';_size = 0;}private:char* _str;size_t _size;size_t _capacity;public:const static size_t npos;//const static size_t npos=-1;//特例//const static double npos = 1.1; // 不支持};const size_t string::npos = -1;ostream& operator<<(ostream& out, const string& s){for (auto e : s){out << e;}return out;}istream& operator>>(istream& in, string& s){s.clear();char buff[129];size_t i = 0;char ch;ch = in.get();while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 128){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i != 0){buff[i] = '\0';s += buff;}return in;}
}
关系运算符重载:
bool operator<(const string& s)const{return strcmp(_str, s._str) < 0;}bool operator==(const string& s)const{return strcmp(_str, s._str) == 0;}bool operator<=(const string& s)const{return *this < s || *this == s;}bool operator>(const string& s)const{return !(*this <= s);}bool operator>=(const string& s)const{return !(*this < s);}bool operator!=(const string& s)const{return !(*this == s);}
resize:
void resize(size_t n,char ch='\0'){if (n <= _size)//删除效果{_str[n] = '\0';_size = n;}else//插入效果{reserve(n);while (_size < n){_str[_size] = ch;_size++;}_str[_size] = '\0';}}
ch的缺省值给'\0',若是没有传参给ch,那么就用'\0'填充
分三种情况:
n<=_size:删除效果,保留前n个有效字符
_size<n<_capacity : 无需扩容,直接插入
n>_capacity :先扩容,再插入
测试:
void test8()
{djx::string s("hello world");cout << s<< endl;s.resize(5);cout << s << endl;s.resize(25, 'x');cout << s << endl;
}
find:
查找一个字符:
size_t find(char ch, size_t pos = 0){for (size_t i = pos; i < _size; i++){if (_str[i] == ch){return i;}}return npos;//没有找到}
pos为0,若没有传参给pos则默认从头开始找
查找字符串:
size_t find(const char* s, size_t pos=0){const char* p = strstr(_str+pos, s);//指向匹配字符串的首字符if (p){return p - _str;}else{return npos;//找不到}}
测试:
void test()
{djx::string s("hello world");size_t i = s.find("world");cout << i << endl;
}
substr:
string substr(size_t pos, size_t len = npos){string s;size_t end = pos + len;if (len == npos || pos + len >= _size)//从pos位置开始有多少取多少{len = _size - pos;end = _size;}s.reserve(len);//提前开空间,避免多次扩容for (size_t i = pos; i < end; i++){s += _str[i];}return s;//传值返回}
拷贝构造:
传统写法和现代写法在效率上区别不大,只是代码简洁性的区分
传统写法:
//传统写法string(const string& s){_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}
自己开和s一样大的空间,拷贝数据
现代写法:
void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//现代写法string(const string& s):_str(nullptr),_size(0),_capacity(0){string tmp(s._str);//调用构造函数swap(tmp);}
让构造函数去生成tmp,再与tmp交换
当tmp销毁时,调用tmp的析构函数会把this指向的对象,它申请的资源给释放掉
注意:this指向的对象中的成员变量都要走初始化列表(是它们定义的地方)
那么如果不给this指向对象的成员变量初始化,那么this指向的对象中的_str指向的是一块随机的空间,是野指针,不能随意释放这块不属于自己的空间
所以需要在初始胡列表的地方给this指向对象中的成员变量初始化
赋值重载:
传统写法:
//传统写法string& operator=(const string& s){if (this != &s){char* tmp = new char[s._capacity + 1];strcpy(tmp, s._str);delete[] _str;//释放旧空间_str = tmp;//指向新空间_size = s._size;_capacity = s._capacity;}return *this;}
1 tmp指向一块与s一模一样的空间
2 释放_str指向的空间
3 _str指向tmp指向的空间
现代写法:
版本1:
//现代写法1string& operator=(const string& s){if (this != &s){string tmp(s);//调用拷贝构造生成tmpswap(tmp);}return *this;}
版本2:
//现代写法2string& operator=(string s)//拷贝构造生成s{swap(s);return *this;}
s销毁,会释放原本由*this对象申请的空间
测试:
void test9()
{djx::string s("test.cpp.tar.zip");size_t i = s.find('.');cout << i << endl;djx::string sub = s.substr(i);cout << sub << endl;djx::string s2("https://legacy.cplusplus.com/reference/string/string/rfind/");//分割 协议、域名、资源名djx::string sub1;djx::string sub2;djx::string sub3;size_t i1 = s2.find(':');if (i1 != djx::string::npos){sub1 = s2.substr(0, i1);}else{cout << "找不到i1" << endl;}size_t i2 = s2.find('/', i1 + 3);if (i2 != djx::string::npos){sub2 = s2.substr(i1+3, i2-(i1+3));}else{cout << "找不到i2" << endl;}sub3 = s2.substr(i2 + 1);cout << sub1 << endl;cout << sub2 << endl;cout << sub3 << endl;
}
注意1:
substr是传值返回,s对象出了作用域之后就销毁了,会生成一个临时对象,由s拷贝构造生成
若是我们没有写拷贝构造函数,编译器默认生成的拷贝构造函数对于对象中内置类型的成员只会完成浅拷贝(值拷贝),即临时对象和s对象管理同一块空间,但是s出了作用域之后会调用析构函数,这块空间会被释放掉
substr返回的临时对象再拷贝构造给sub对象,没写拷贝构造函数,编译器生成的拷贝构造依然是值拷贝,那么sub对象和临时对象管理同一块空间,临时对象任务完成后,销毁,再次对已经释放的空间进行释放,导致程序崩溃
这里还涉及编译器优化的问题:
substr拷贝构造生成临时对象,再由临时对象拷贝构造生成sub,连续的拷贝构造动作,编译器直接一步优化为一个拷贝构造:让s在销毁前先拷贝构造生成sub,即使是优化了,也因为浅拷贝的问题导致程序崩溃-> sub 和s管理同一块资源空间,s销毁,释放一次这块空间,等sub销毁时,再一次释放这块空间
所以拷贝构造必须由我们来写,完成深拷贝,让每个对象有自己独立的资源空间
注意2 :
解决拷贝构造问题后,由原来的浅拷贝变为了深拷贝,那么substr返回的临时对象就有和s一模一样的独立资源空间
但是若是我们没有写赋值重载函数,那么编译器默认生成的赋值重载函数对于对象中的内置类型的成员只会浅拷贝,即sub1和临时对象管理同一块空间,且sub1原来管理的资源空间丢失,导致内存泄漏,且临时对象销毁,对它管理的资源空间释放一次,当sub1销毁,又对这块空间释放一次,程序崩溃
所以我们也要显示写赋值重载函数,完成深拷贝,让每个对象有自己独立的,与赋值对象一模一样的资源空间,而不是和其他对象共享同一块资源空间的管理
完结撒花~