C++String详解(下)
2.string模拟实现
2.1string模拟实现的注意事项
错误警示1
在
string.h中定义了一个非内联的全局函数test_string1()
这里容易犯一个错误,这里注意,类里面的成员函数定义默认是内联函数,不会放在符号表,不会冲突。而在类外面定义全局函数或者全局变量,在Test.cpp和string.cpp都包含了string.h文件,于是test_string1()函数就会各自在两个文件中有一份,在链接时,有两份test_string1()于是就引发冲突的问题。
这里有三种解决方案
- 函数声明成static静态(
static用于修饰全局函数或变量时,会改变其链接属性。它将符号的链接属性从外部链接改为内部链接,从而该函数/变量的名字仅在当前编译单元(即当前源文件) 内可见。每个包含了该头文件的源文件都会获得一份属于自己的、独一无二的test_string1()副本)- 设计成inline内联函数(在 C++17 之前,
inline的主要功能是给编译器一个“建议”,建议将函数调用处用函数体替换,以优化性能。但更重要的是,它赋予了函数豁免ODR约束的能力。一个被定义为inline的函数或变量(C++17起),允许多个编译单元中存在其定义。链接器会确保所有这些相同的定义最终只保留一份)只在头文件中声明,在一个源文件中定义()
前两者本质上就是只在当前文件可见,不进入符号表
错误警示2
在构造函数中使用
_str = nullptr初始化然后在string.cpp中打印空对象会导致程序崩溃
C++ 的
std::cout和 C 的字符串函数(如strlen,printf等)都要求传入的char*是一个有效的以空字符结尾的字符串,或者是nullptr(但需要特殊处理)。nullptr表示"空指针",不是一个有效的字符串地址。cout会尝试访问nullptr指向的内存来读取字符,这是非法内存访问,导致段错误。正确做法:应该将空字符串初始化为一个有效的空字符串
错误警示3
wihle循环中整型提升的问题
我们在模拟实现insert函数时,在测试头部插入,也就是insert(,'&')时,程序会崩。这里原因就是在挪动数据时,当end=0时,将[0]挪动到[1]位置时,end--变成-1,原本我们思考的是end>=0时循环继续,end=-1时跳出循环,但是调试时循环没有结束,这里的原因是一个操作符两边的操作数类型不同时,编译器会进行整型提升,一般会从范围小的提升至范围大的类型,由于pos是size_t类型,比较时(循环条件)编译器会将int提升至size_t类型,从而将-1的补码(全是1)直接当作size_t类型,解析出来就是整型的最大值(4,294,967,295)
修正方案1:改变循环条件和方向(推荐)
void string::insert(size_t pos, char ch) { assert(pos <= _size); if (_size == _capacity) { reserve(_capacity == 0 ? 4 : _capacity * 2); } // 挪数据 size_t end = _size + 1; // 从末尾的下一个位置开始(包括'\0') // 循环条件:当 end 的位置大于目标位置 pos 时,就一直往前挪 while (end > pos) { _str[end] = _str[end - 1]; end--; } _str[pos] = ch; _size++; }修正方案2:强制转换(不推荐,但要知道)
在比较时,将无符号数强制转换为有符号数,但这可能会在
pos很大时引入新的问题。while (end >= (int)pos) // 强制将pos转为int,使比较在有符号数之间进行 //后续代码不变
错误警示4(重要)
substr中构造子字符串的缺少深拷贝构造函数问题
默认拷贝构造函数:编译器生成的是“浅拷贝”或“按成员拷贝”,它只是简单地复制指针
_str的值。这导致两个string对象(sub和suffix)的_str成员指向同一块堆内存。当其中一个对象(函数内的局部对象sub)先被销毁时,它会释放那块共享的内存。另一个对象(如suffix)的_str指针就变成了野指针。后续任何对该对象的访问(如cout << suffix.c_str())或它自己的析构函数被调用时,都会导致未定义行为,通常是程序崩溃。注意在不同平台下的不同模式会进行优化(前面章节有非常详细介绍),可能会“合三为一”(非常激进的优化)就是将临时对象和sub对象优化,直接将suffix充当sub对象,也就是整个过程中压根没有拷贝构造,程序也就正常运行了,并不是程序将bug修复了,而是没有触发bug。
解决方案:自己实现深拷贝构造函数
//s2(s1); string(const string& s) { _str=new char[s._capacity+1];//这里+1是多分配一个空间,用来存放\0 strcpy(_str,s._str);//拷贝数据 _size = s._size; _capacity = s._capacity; }
2.2iterator
iterator
iterator都是typedef来的只不过底层可能是指针有可能不是指针,模拟的是指针的行为。
这里可以使用原生指针来做迭代器,因为底层结构是数组。如果如后续所要介绍的链表就不是原生指针来实现迭代器,而是自定义类型,后续会详细了解。
迭代器其实就是封装的一种体现,迭代器通过提供一个统一的、简单的接口来遍历各种不同的数据结构,从而将遍历操作与数据结构的底层实现细节分离开来。这种“隐藏实现细节,暴露统一接口”的行为,正是封装的核心思想。
封装的特点:
隐藏内部实现细节:将对象的状态(数据)和行为(方法)包装在一起,并尽可能隐藏内部的复杂实现。
暴露公开的接口:只提供一个清晰、简洁的接口与外部交互。
目的:降低系统的耦合度,提高代码的模块化、可维护性和安全性。使用者不需要知道内部是如何工作的,只需要知道如何使用接口即可。
2.3传统拷贝和现代拷贝
传统拷贝和现代拷贝
简单来说传统拷贝和现代拷贝的最主要的区别就是,谁负责管理拷贝过程中的资源,效率上没有区别。
传统拷贝:程序员手动管理。你需要自己写代码
new出新内存,再把数据复制过去,费时费力且容易出错。现代拷贝:编译器或标准库工具自动管理。
拷贝构造函数
void swap(string&s) { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); } //s2(s1); // 传统版本 //string(const string& s) //{ // _str=new char[s._capacity+1];//这里+1是多分配一个空间,用来存放\0 // strcpy(_str,s._str); // _size = s._size; // _capacity = s._capacity; //} //现代版本 string(const string& s) { string tmp(s._str);//用s1的_str构造tmp swap(tmp);//等价于 this->swap(tmp); }
注意:
- 这里s就是s1,this指向s2。
- 如果我们没有在成员变量声明的地方给缺省值,对于内置类型不一定会初始化,如果把随机值交换给tmp,当tmp这个局部对象出作用域后,会调用析构进行释放资源,这时候如果_str未初始化就释放野指针了。
如果在swap函数中没有使用std::swap而直接使用swap,编译器会报参数不匹配的错误,这是因为我们希望调用算法库里面的swap,而不是我们写的这个swap。
赋值也是同理
//s2=s1;//赋值运算符重载 //在此函数中:this 指针指向 s2,参数 s 是 s1 的引用 //传统方法 //string& operator=(const string& s) //{ // if (this != &s)//防止自己给自己赋值 // { // delete[] _str;//为了防止空间不够,或者空间过大浪费,重新分配s2空间 // _str = new char[s._capacity + 1]; // strcpy(_str, s._str); // _size = s._size; // _capacity = s._capacity; // } // return *this;// 返回s2的引用 //} //现代方法 string& operator=(const string& s) { if (this != &s)//防止自己给自己赋值 { string tmp(s._str);// 1. 利用s1的字符串构造一个临时副本tmp swap(tmp); // 2. 交换s2(this)和tmp的内容。 //现在s2拥有了tmp新分配的资源,tmp则拿到了s2的旧资源。 } return *this; // 3. 函数结束,局部对象tmp析构,自动释放了s2原来的旧资源。 }
与上面不同的是,
s2是已经存在的对象,用s1(s)的_str构造tmp后,用swap交换,s2(this)的旧资源就给了tmp,然后新构造的tmp的新资源换给了s2(this)。由于tmp是局部对象,于是tmp出作用域后调用析构函数就清理了s2的旧资源//一步到位s1=s2// 调用赋值运算符 string& operator=(string tmp)// 传值!tmp 是 s2 的副本 { swap(tmp); // this->swap(tmp); 交换当前对象(s1)和tmp的资源 return *this; }// tmp 析构,释放掉 s1 原来的旧资源这里没有使用引用传参,而是传值传参,传值传参会调用拷贝构造,s2传给tmp对象,交换后,依然是走上面分析的流程。
这里有个问题,之前我们自己实现的swap和算法库里面的swap有什么区别int main() { string s1("hello world"); string s2("xxxxxxxxxxxxxx"); std::swap(s1,s2); s1.swap(s2); return 0; }
两个swap都可以实现交换,但是这里的差别就是深拷贝的问题,在这个例子中,算法库里面会有三次深拷贝(效率很低):
- 用a(s1)深拷贝一个c(tmp)(一次拷贝构造)
- 再把b(s2)赋值给a(s1)(一次拷贝赋值,深拷贝)
- 再将c(tmp)赋值给b(s2)(一次拷贝赋值,深拷贝)
当然为了防止这一情况出现,库里面还提供了全局的swap函数
之前有提到过,当函数模板和全局函数同时存在时,会优先使用现成的全局函数,也就是说不会调用深拷贝的版本。
2.4string模拟实现
string.h
#include<iostream> #include<assert.h> using namespace std; namespace name { 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; } //短小频繁的函数,直接在类内定义(默认是inline) //string() // : _str(new char[1] {'\0'})//这里不能写成_str = nullptr // , _size(0) // , _capacity(0) //{} string(const char* str="")//常量字符串默认有一个\0,如果给"\0"就有两个\0 {//这里不建议用初始化列表,存在初始化顺序问题,推荐函数体内初始化 _str = new char[strlen(str)+1];//这里+1是多分配一个空间,用来存放\0 _size= strlen(str); _capacity = _size;//capacity不包含\0 strcpy(_str,str); } void swap(string&s) { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); } //s2(s1); // 传统版本 //string(const string& s) //{ // _str=new char[s._capacity+1];//这里+1是多分配一个空间,用来存放\0 // strcpy(_str,s._str); // _size = s._size; // _capacity = s._capacity; //} //现代版本 string(const string& s) { string tmp(s._str);//用s1的_str构造tmp swap(tmp); //等价于 this->swap(tmp); } ~string() { delete[] _str; _str = nullptr; _capacity=_size=0; } void clear() { _str[0]='\0'; _size = 0;//不动capacity } const char* c_str() const { return _str; } size_t size() const { return _size; } size_t capacity() const { return _capacity; } char& operator[](size_t pos) { assert(pos <_size); return _str[pos]; } //s2=s1;//赋值运算符重载 //在此函数中:this 指针指向 s2,参数 s 是 s1 的引用 //传统方法 //string& operator=(const string& s) //{ // if (this != &s)//防止自己给自己赋值 // { // delete[] _str;//为了防止空间不够,或者空间过大浪费,重新分配s2空间 // _str = new char[s._capacity + 1]; // strcpy(_str, s._str); // _size = s._size; // _capacity = s._capacity; // } // return *this;// 返回s2的引用 //} //现代方法 string& operator=(const string& s) { if (this != &s)//防止自己给自己赋值 { string tmp(s._str);// 1. 利用s1的字符串构造一个临时副本tmp swap(tmp); // 2. 交换s2(this)和tmp的内容。 //现在s2拥有了tmp新分配的资源,tmp则拿到了s2的旧资源。 } return *this; // 3. 函数结束,局部对象tmp析构,自动释放了s2原来的旧资源。 } ////一步到位s1=s2// 调用赋值运算符 //string& operator=(string tmp)// 传值!tmp 是 s2 的副本 //{ // swap(tmp); // this->swap(tmp); 交换当前对象(s1)和tmp的资源 // return *this; //}// tmp 析构,释放掉 s1 原来的旧资源 const char& operator[](size_t pos) const { assert(pos < _size); return _str[pos]; } void reserve(size_t n); void push_back(char ch);//声明定义分离 void append(const char* str); string& operator+=(char ch); string& operator+=(const char* str); void insert(size_t pos,const char* str); void insert(size_t pos,char ch); void erase(size_t pos, size_t len= npos); size_t find(char ch,size_t pos=0); size_t find(const char*str, size_t pos=0); string substr(size_t pos = 0,size_t len=npos); private: char* _str=nullptr; //char _buff[16]; size_t _size=0; size_t _capacity=0; static const size_t npos; //static const size_t npos=-1;//ok,但是不推荐,可以理解为编译器特殊处理 //static const double N=1.2;//error }; bool operator==(const string& s1,const string& s2); bool operator!=(const string& s1,const string& s2); bool operator<(const string& s1,const string& s2); bool operator>(const string& s1,const string& s2); bool operator<=(const string& s1,const string& s2); bool operator>=(const string& s1,const string& s2); ostream & operator<<(ostream& out,const string& s); istream & operator>>(istream& in,string& s); //void test_string1()//Test.cpp和string.cpp都包含string.h头文件,编译器会报错 //{ // string s1("hello"); // string s2; //} void test_string1();//声明 void test_string2();//声明 void test_string3();//声明 void test_string4();//声明 void test_string5();//声明 void test_string6();//声明 void test_string7();//声明 }
String.cpp
#include"string.h" namespace name { const size_t string::npos = -1;//静态成员变量要在类外初始化 void string::reserve(size_t n) { if (n > _capacity) { char* tmp = new char[n+1];//多一个空间放'\0' strcpy(tmp,_str); delete[] _str; _str = tmp; _capacity = n; } } void string::push_back(char ch) { if (_size == _capacity) { reserve(_capacity==0?4:_capacity*2); } _str[_size] = ch; _size++; _str[_size] = '\0';//字符串最后要加'\0' } void string::append(const char* str) { size_t len= strlen(str); if (_size + len > _capacity) { //大于2倍,需要多少就申请多少,否则申请2倍 reserve(len+_size>2*_capacity?len+_size:2*_capacity); } strcpy(_str+_size,str); _size += len; } string& string::operator+=(char ch) { push_back(ch); return *this; } string& string::operator+=(const char* str) { append(str); return *this; } void string::insert(size_t pos, const char* str) { assert(pos<=_size); size_t len = strlen(str); if (str == nullptr) return; // 检查空指针 if (len == 0) return; // 空字符串不需要操作 if (len + _size > _capacity)//\0都单独开了,这里不用管 { reserve(len + _size > 2 * _capacity ? len + _size : 2 * _capacity); } //挪数据 int end = _size; while (end >= (int)pos) { _str[end+len]= _str[end]; end--; } memmove(_str+pos,str,len); _size += len; } void string::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)//为了防止当end=0时,end--变成-1,却被整型提升成size_t,所以pos要强制转换成int { _str[end + 1] = _str[end]; end--; } _str[pos] = ch; _size++; } void string::erase(size_t pos, size_t len) { assert(pos<_size); //全部删除 if (len >= _size - pos)//一闭一开相减就是长度 { _str[pos] = '\0'; _size = pos; } else { for (size_t i = pos + len; i <= _size; i++) { _str[i - len] = _str[i]; } _size -= len; } } size_t string::find(char ch, size_t pos) { assert(pos < _size); for (size_t i = pos; i < _size; i++) { if (ch == _str[i]) return i; } return npos; } size_t string::find(const char* str, size_t pos) { assert(pos<_size); const char* ptr=strstr(_str+pos,str); if (ptr == nullptr) return npos; return ptr-_str;//两个指针相减,就是下标 } string string::substr(size_t pos, size_t len)//传值返回 { assert(pos<_size); //更新len成有效长度 if (len > _size - pos) len= _size - pos; string sub; sub.reserve(len); for (size_t i = 0; i < len; i++) { sub+= _str[pos+i]; } return sub; } //实现成全局函数 bool operator==(const string& s1, const string& s2) { return strcmp(s1.c_str(), s2.c_str()) == 0; } bool operator!=(const string& s1, const string& s2) { return !(s1 == s2); } bool operator<(const string& s1, const string& s2) { //int strcmp(const char* str1, const char* str2); //Compares the C string str1 to the C string str2. //This function starts comparing the first character of each string. If they are equal to each other, //it continues with the following pairs until the characters differ or until a terminating null-character is reached. return strcmp(s1.c_str(), s2.c_str()) < 0; } bool operator>(const string& s1, const string& s2) { return !(s1 <= s2); } bool operator<=(const string& s1, const string& s2) { return s1<s2 || s1 == s2; } bool operator>=(const string& s1, const string& s2) { return !(s1 < s2); } ostream& operator<<(ostream& out, const string& s) { for (auto ch : s) { out << ch; } return out; } istream& operator>>(istream& in, string& s) { s.clear();//清空 const int N = 512; char buff[N]; int i = 0; char ch; //in >> ch;//读到空格或者换行了,但是会被认为分隔符,直接被忽略,也就不能结束循环 in.get(ch); while (ch != '\n' && ch != ' ') { buff[i++] = ch;//性能优化,减少扩容次数 if (i==N-1)//满了 { buff[i]='\0'; s+=buff; i = 0; } //s += ch; //in >> ch; ch=in.get(); } if (i > 0) { buff[i] = '\0'; s += buff; } return in; } }
Test.cpp
#include"string.h" namespace name { //测试 void test_string1() { string s1; string s2("hello world"); cout << s1.c_str() << endl; cout << s2.c_str() << endl; for (size_t i = 0; i < s2.size(); i++) { s2[i] += 2; } cout << s2.c_str() << endl; for (auto& ch : s2)//范围for底层就是迭代器 { ch -= 2; } cout << s2.c_str() << endl; string::iterator it = s2.begin(); while (it != s2.end()) { cout << *it << "-"; it++; } cout << endl; } void test_string2() { string s1("hello world"); s1 += 'd'; s1 += '!'; cout << s1.c_str() << endl; s1 += "1234567890"; cout << s1.c_str() << endl; s1.insert(5,'#'); cout << s1.c_str() << endl; s1.insert(0, '#'); cout << s1.c_str() << endl; string s2("hello world"); s2.insert(1, "1234567890"); cout << "s2:" << s2.c_str() << endl; } void test_string3() { string s1("hello world"); s1.erase(5); cout << s1.c_str() << endl; string s2("hello world"); s2.erase(5,100); cout << s2.c_str() << endl; string s3("hello world"); s3.erase(5, 2); cout << s3.c_str() << endl; } void test_string4() { string s("test.cpp.zip"); size_t pos = s.find('.'); string suffix = s.substr(pos); cout << suffix.c_str()<< endl; /*string copy(s);//要自己写拷贝构造函数,否则会调用系统默认的浅拷贝构造函数,于是两个指针指向同一块空间,析构的时候会出错 cout<<copy.c_str()<<endl;*/ s = suffix;//默认生成的赋值也是浅拷贝, cout << s.c_str() << endl; cout << suffix.c_str() << endl; s = s; cout << "self" << s.c_str() << endl; } void test_string5() { string s1("hello world"); string s2("hello world"); cout << (s1 == s2) << endl; cout << (s1 != s2) << endl; cout << (s1 > s2) << endl; cout << (s1 < s2) << endl; cout << (s1 >= s2) << endl; cout << (s1 <= s2) << endl; cout << (s1 == "hello world") << endl;//隐式类型转换,先调用构造函数,再调用运算符重载 cout << ("hello world" == "hello world") << endl;//不会隐式类型转换,因为运算符重载必需要有一个参数是类类型 } void test_string6() { string s1("hello world"); cout<< s1.c_str()<<endl; string s2(s1); /*cin >> s1; cout << s1.c_str() << endl; cin >> s2;*/ cout << s2.c_str() << endl; } void test_string7() { string s1("hello world"); string s2("xxxxxxxxxxx"); //std::swap(s1,s2); s1.swap(s2); cout << s1 << endl; cout << s2 << endl; } } int main() { //name::test_string1(); //name::test_string2(); //name::test_string3(); //name::test_string4(); //name::test_string6(); name::test_string7(); return 0; }
更多推荐







注意:





所有评论(0)