目录
一、经典的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); // 出错点 1
std::cout << s1.c_str() << std::endl;
std::cout << s2.c_str() << std::endl;
string s3("world");
//采用
s1 = s3; // 出错点 2
std::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<iostream>
#include<assert.h>
namespace 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) == -1) || (strcmp(_str, s._str) == 0);
}
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 ? 6 : _capacity * 2;
char* tmp = new char[newcapacity + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = newcapacity;
}
}
};
inline std::ostream& operator<<(std::ostream& _out, const string& s)
{
for (size_t i = 0; i < s.size(); i++)
{
_out << s[i];
}
return _out;
}
// 将函数标记为"inline"可以消除重定义错误
// 在头文件中定义了这些函数需要将它们标记为 inline
// 否则每个包含该头文件的源文件都会生成该函数的定义,从而导致链接错误
inline std::istream& operator>>(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 <= _size);
this->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 < _size);
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;
}
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 < _size);
size_t len = s._size;
if (_size + len > _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 < len);
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 = _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 < newsize)
{
_str[newsize] = '\n';
_size = newsize;
}
else
{
if (newsize > _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;
}