目录
一、经典的string类问题
1.出现的问题
2.浅拷贝
3.深拷贝
二、string类的模拟实现
1.传统版的string类
2.现代版的string类(采用移动语义)
3.相关习题*
习题一
习题二
4.写时拷贝
5.完整版string类的模拟实现[注意重定义]
MyString.h
MyString.c
test.c
一、经典的string类问题
1.出现的问题
模拟实现string容器的 构造、拷贝构造、赋值运算符重载以及析构函数
出错代码如下:
// 类的定义namespace imitate{class string{public: //string()//无参构造函数//:_str(nullptr)//{}//string(char* str)//有参构造函数//:_str(str)//{}string():_str(new char[1]){_str[0] = '\0';}string(char* str) //构造函数在堆上开辟一段strlen+1的空间+1是c_str:_str(new char[strlen(str)+1]){strcpy(_str, str); //strcpy会拷贝\0过去}//string(char* str="") //构造函数在堆上开辟一段strlen+1的空间+1是c_str//:_str(new char[strlen(str) + 1])//{//strcpy(_str, str); //strcpy会拷贝\0过去//}size_t size() const{return strlen(_str);}bool empty(){return _str == nullptr;}char& operator[](size_t i)//用引用返回不仅可以读字符,还可以修改字符{return _str[i];}~string()//析构函数{if (_str){delete[] _str;_str = nullptr;}}const char* c_str() //返回C的格式字符串{return _str;}private:char* _str;};}
// 测试using namespace imitate;void TestString1(){string s1("hello");string s2;for (size_t i = 0; i < s1.size(); i++){s1[i] += 1;std::cout << s1[i] << " ";}std::cout << std::endl;for (size_t i = 0; i < s2.size(); i++){s2[i] += 1;std::cout << s2[i] << " ";}std::cout << std::endl;}void TestString2(){string s1("hello");//采用默认的构造函数string s2(s1); // 出错点 1std::cout << s1.c_str() << std::endl;std::cout << s2.c_str() << std::endl;string s3("world");//采用s1 = s3; // 出错点 2std::cout << s1.c_str() << std::endl;std::cout << s3.c_str() << std::endl;}int main(){TestString1();TestString2();return 0;}
出错点1:
string s2(s1); // 出错点 1
采用默认的拷贝构造函数,导致s2._str拷贝了s1._str所指向的地址,导致s1,s2的_str都指向同一块空间。测试函数2结束后,s2、s1指向相同的地址会被释放两次,会导致程序崩溃。
出错点2:
s1 = s3; // 出错点 2
采用默认赋值运算符重载,s3._str被赋予了s1._str所指向的地址,导致s1,s2的_str都指向同一块空间。测试函数2结束后,s3、s1指向相同的地址会被释放两次,会导致程序崩溃。
总结:采用 默认拷贝构造函数 和 默认赋值运算符重载 会导致一块空间会被多个类对象指向,导致多次释放同一空间,致使程序崩溃。这种拷贝方式,称为浅拷贝。
2.浅拷贝
浅拷贝指创建了一个新的对象,其中包含了原始对象的所有属性和值,但是对于原始对象中引用的其他对象,浅拷贝只是复制了其引用,而不是复制其实际值。
在浅拷贝中,新对象和原始对象之间共享同一个内存地址,所以如果修改其中一个对象的属性,则另一个对象的属性也会随之改变,因为它们引用同一个对象。浅拷贝通常比深拷贝更快,因为它只需要复制对象的引用,而不需要递归地复制对象的所有子对象。
3.深拷贝
深拷贝指创建一个新对象,该对象是原始对象的完全副本,包括原始对象的所有属性和嵌套对象的属性。深拷贝会递归地复制所有嵌套的对象,而不仅仅是复制其引用。因此,原始对象和副本对象之间没有共享任何内存地址。
深拷贝可以防止副本对象的修改影响原始对象,因为它们是完全独立的。但是,深拷贝可能比浅拷贝更耗时,因为它需要递归地复制所有嵌套的对象。
如果类中有指向堆内存的指针或者使用了动态内存分配函数(如new、malloc等),则需要手动编写拷贝构造函数或赋值操作符来实现深拷贝。
二、string类的模拟实现
1.传统版的string类
C++ 的一个常见面试题是让你实现一个 String 类,限于时间,不可能要求具备 std::string 的功能,但至少要求能正确管理资源。
具体来说:能像 int 类型那样定义变量,并且支持赋值、复制。能用作函数的参数类型及返回类型。能用作标准库容器的元素类型,即 vector / list / deque 的 value_type。(用作 std::map 的 key_type 是更进一步的要求,本文从略)。换言之,你的 String 能让以下代码编译运行通过,并且没有内存方面的错误。
添加了自定义的拷贝的构造函数赋值运算符重载
代码如下:
// 传统版的string类的模拟————类的定义// ...namespace imitate{class string{public:string():_str(new char[1]){_str[0] = '\0';}string(const char* str):_str(new char[strlen(str) + 1]){strcpy(_str, str);}string(const string& s):_str(new char[s.size() + 1]){strcpy(_str, s._str);}size_t size() const{return strlen(_str);}char& operator[](size_t i){return _str[i];}~string(){if (_str){delete[] _str;_str = nullptr;}}const char* c_str(){return _str;}// const 是因为对 str 不需要修改,安全性更高// 参数 & 是因为不需要传值拷贝、效率高// 返回值 & 是为了连续赋值(效率高)string& operator=(const string& str){// 检查是否自己给自己赋值if (this != &str){// 因为str为const修饰,所以要调用size()函数的话应给函数加上const修饰char* tmp = new char[str.size() + 1];strcpy(tmp, str._str);delete[] _str;_str = tmp;}return *this;}private:char* _str;};}
// 测试代码// ...using namespace imitate;void TestString(){string s1;string s2("hello");for (size_t i = 0; i < s2.size(); i++){std::cout << s2[i] << " ";}std::cout << std::endl;s1 = s2;for (size_t i = 0; i < s1.size(); i++){std::cout << s1[i] << " ";}}int main(){TestString();return 0;}
2.现代版的string类(采用移动语义)
现代版的string类采用swap
函数的原因:实现C++11中的移动语义。
C++的移动语义
可以在不进行任何拷贝操作的情况下,将一个对象的所有权从一个对象转移到另一个对象。这个过程使用右值引用(rvalue references)实现。
移动语义的主要目的是优化C++代码的性能,减少不必要的内存分配和拷贝操作,提高程序的效率。当一个对象被移动而不是拷贝时,可以避免拷贝构造函数和析构函数的调用,从而提高程序的性能。
代码如下:
// 现代版的string类模拟————类的定义// ...namespace imitate{class string{public:string():_str(new char[1]){_str[0] = '\0';}string(const char* str):_str(new char[strlen(str) + 1]){strcpy(_str, str);}string(const string& s) // 使用swap函数,进行直接把所有权交予另一个对象:_str(nullptr){string tmp(s._str);std::swap(_str, tmp._str);}size_t size() const{return strlen(_str);}char& operator[](size_t i){return _str[i];}~string(){if (_str){delete[] _str;_str = nullptr;}}const char* c_str(){return _str;}string& operator=(const string& str){if (this != &str){//tmp 会在函数结束时自动调用其析构函数进行资源的释放string tmp(str._str);std::swap(_str, tmp._str);}return *this;}private:char* _str;};}
// 测试代码// ...void TestString1(){string s1;string s2("hello");string s3(s2);s1 = s3;}int main(){TestString1();return 0;}
3.相关习题*
习题一
习题二
注意:用一个对象初始化另一个对象时,例如
MyClass obj1; MyClass obj2 = obj1;
采用的是 拷贝构造函数 而非使用 重载运算符
4.写时拷贝
写时拷贝(Copy-On-Write,简称COW)是一种优化技术,它可以避免在数据复制时不必要的内存开销。写时拷贝的实现方式是,在数据需要被修改时,才会复制数据,而在数据未被修改时,共享同一份数据。
写时拷贝通常是通过使用智能指针实现的。智能指针可以跟踪指向的对象的引用计数,并在需要时进行复制。具体来说,当一个智能指针被拷贝时,它的引用计数会增加,而指向的对象不会被复制。当一个指向对象的智能指针需要修改对象时,它会先检查引用计数是否为1,如果是,说明这个对象没有被其他智能指针共享,可以直接修改,否则,它会先复制一份对象,并把引用计数减1,然后对复制后的对象进行修改。
5.完整版string类的模拟实现[注意重定义]
注意:
1.size_t imitate::string::npos = -1;不能放到头文件中,避免重定义问题。
2.将函数标记为”inline”可以消除重定义错误。
3.在头文件中定义了这些函数需要将它们标记为 inline,否则每个包含该头文件的源文件都会生成该函数的定义,从而导致链接错误。
MyString.h
#pragma once#define _CRT_SECURE_NO_WARNINGS#include#includenamespace imitate{class string{public:typedef char* iterator;static size_t npos; //insert用的位置public:string(const char* str = " "){_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, str);}string(const string& s): _size(0), _capacity(0), _str(nullptr){string tmp(s._str);this->swap(tmp);}~string(){if (_str){delete[] _str;_size = 0;_capacity = 0;_str = nullptr;}}string& operator=(string& s);void swap(string& s);void push_back(char c);void append(const char* str);void append(const string& s);string& operator+=(char c);string& operator+=(const char* str);string& operator+=(const string& s);string& insert(size_t pos, char c);string& insert(size_t pos, const char* str);string& insert(size_t pos, const string& s);string& erase(size_t pos, size_t len = npos);size_t find(char c, size_t pos = 0);size_t find(const char* str, size_t pos = 0);size_t find(const string& s, size_t pos = 0);void resize(size_t newsize, char c = '\0');iterator begin()//iterator迭代器的原理{return _str;}iterator end(){return (_str + _size);}const char* c_str(){return _str;}char& operator[](size_t i) const{return _str[i];}bool operator<(const string& s){return strcmp(_str, s._str);}bool operator(const string& s){return strcmp(_str, s._str);}bool operator>=(const string& s){return (strcmp(_str, s._str) == 1) || (strcmp(_str, s._str) == 0);}bool operator=(const string& s){return strcmp(_str, s._str) == 0;}bool operator!=(const string& s){return strcmp(_str, s._str);}bool empty(){return _size == 0;}size_t size() const{return _size;}size_t capacity(){return _capacity;}private:char* _str;size_t _size; //已经有多少个有效字符个数size_t _capacity; //能存多少个有效字符个数 \0不是有效字符,\0是标识结束的字符void CheckFull(){if (_size == _capacity){size_t newcapacity = _capacity == 0 " />>(std::istream& _in, string& s){while (1){char c;c = _in.get();if (c == ' ' || c == '\n'){break;}else{s += c;}}return _in;}}
MyString.c
#include"MyString.h"size_t imitate::string::npos = -1;imitate::string& imitate::string::operator=(string& s){string tmp(s);string::swap(tmp);return *this;}void imitate::string::swap(string& s){std::swap(_size,s._size);std::swap(_capacity ,s._capacity);std::swap(_str, s._str);}void imitate::string::push_back(char c){this->CheckFull();_str[_size] = c;_size++;_str[_size] = '\0';}void imitate::string::append(const char* str){size_t len = strlen(str);if ((_size + len) > _capacity){size_t newcapacity = _size + len;char* tmp = new char[newcapacity + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = newcapacity;}strcpy(_str + _size, str);_size += len;}void imitate::string::append(const string& s){if ((_size + s._size) > _capacity){size_t newcapacity = _size + s._size;char* tmp = new char[newcapacity + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = newcapacity;}strcpy(_str + _size, s._str);_size += s._size;}imitate::string& imitate::string::operator+=(char c){this->push_back(c);return *this;}imitate::string& imitate::string::operator+=(const char* str){this->append(str);return *this;}imitate::string& imitate::string::operator+=(const string& s){this->append(s);return *this;}imitate::string& imitate::string::insert(size_t pos, char c){assert(pos CheckFull();size_t end = _size;while (end >= pos){_str[end + 1] = _str[end];end--;}_str[pos] = c;_size++;return *this;}imitate::string& imitate::string::insert(size_t pos, const char* str){assert(pos _capacity){size_t newcapacity = _size + len;char* tmp = new char[newcapacity + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = newcapacity;}size_t end = _size;while (end >= pos){_str[end + len] = _str[end];end--;}for (size_t i = 0; i < len; ++i, ++pos){_str[pos] = str[i];}_size += len;return *this;}imitate::string& imitate::string::insert(size_t pos, const string& s){assert(pos _capacity){size_t newcapacity = _size + len;char* tmp = new char[newcapacity + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = newcapacity;}size_t end = _size;while (end >= pos){_str[end + len] = _str[end];end--;}for (size_t i = 0; i < len; ++i, ++pos){_str[pos] = s._str[i];}_size += len;return *this;}imitate::string& imitate::string::erase(size_t pos, size_t len){assert(pos = _size-pos){_str[pos] = '\0';_size = pos;}else{for (size_t i = pos + len; i <= _size; ++i){_str[i - len] = _str[i];}_size = _size - len;}return *this;}size_t imitate::string::find(char c, size_t pos){iterator it = begin();while (it != end()){if (*it == c){return it - begin();}it++;}return npos;}size_t imitate::string::find(const char* str, size_t pos){char* p = strstr(_str + pos, str);if (!p){return npos;}return p - _str;}size_t imitate::string::find(const string& s, size_t pos){char* p = strstr(_str + pos, s._str);if (!p){return npos;}return p - _str;}void imitate::string::resize(size_t newsize, char c){if (_size _capacity){size_t newcapacity = newsize;char* tmp = new char[newcapacity + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = newcapacity;}for (size_t i = _size; i < newsize; ++i){_str[i] = c;}_size = newsize;_str[_size] = '\0';}}
test.c
#include"MyString.h"using namespace imitate;void test1(){string s1;string s2("Hello");string s3(s2);string s4(" World!");s2.push_back('!');s2.append(" World!");s3.append(s4);s4 += s2;s2 += "Hi! Hi! Hi!";}void test2(){string s2("Hello");string s3(s2);string s4(" World!");s2.insert(2, s4);}void test3(){string s1("Hello");string s2(" World!");s1.append(s2);s1.find('a', 2);s1.find("World!", 7);s1.find(s2);std::cin >> s1;std::cout << s1;}int main(){test3();std::cout << string::npos;return 0;}