1.经典的string类实现
最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。
(1) String.h
为了与库里的string进行区分我们使用String:
// String.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
#include<string.h>
#include<assert.h>
using namespace std;
namespace zyt
{
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()// 无参
// :_str(new char[1] {'\0'})//开一个空间存储'\0',不能直接给nullptr
// // 如果对无参对象应用c_str,接口会持续对空指针解引用直到遇到'\0'为止
// , _size(0)
// , _capacity(0)
//{}
//string(const char* str) // 有参
//{
// _size = strlen(str);
// _capacity = _size; // 容量不包含‘\0’
// _str = new char[_capacity + 1]; // 空间要多开一个
// strcpy(_str, str);
//}
// 短小频繁调用的函数可以直接定义在类里,默认是inline
// 或者直接写一个全缺省的构造
String(const char* str = "")//C语言规定常量字符串后面自带一个‘\0’
{//单参数构造函数支持隐式类型转换
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 深拷贝
//显示拷贝构造,否则其他函数(substr)会默认调用浅拷贝
String(const String& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
//显示赋值重载,否则也会默认调用浅拷贝,导致内存泄漏
String& operator=(const String& s)
{
if (this != &s)
{// 避免自己给自己赋值时,将原有空间释放
delete[] _str;// 把原有的空间直接释放掉
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
~String()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
const char* c_str()const
{
return _str;
}
// 清空所有数据但不销毁空间
void clear()
{
_str[0] = '\0';
_size = 0;
}
size_t size()const
{
return _size;
}
size_t capacity()const
{
return _capacity;
}
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);
void push_back(char ch);
void append(const char* str);
String& operator+=(char ch);
String& operator+=(const char* str);
void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);
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;
size_t _size;
size_t _capacity;
static const size_t npos;
//static const size_t npos = -1;
// static const 整形 的类型可以直接定义,其余static类型都要声明和定义分离
};
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 test1();
void test2();
void test3();
void test4();
}
(2) String .cpp
// String.cpp
#include"String.h"
namespace zyt
{
const size_t String::npos = -1;//声明和定义分离
void String::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
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';// 一定要加终止符
}
String& String::operator+=(char ch)
{
push_back(ch);
return *this;
}
void String::append(const char* str)
{
size_t len = strlen(str);
if (len + _size > _capacity)
{
//大于2倍,需要多少开多少,小于2倍按2倍扩
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
strcpy(_str + _size, str);
_size += len;
}
String& String::operator+=(const char* str)
{
append(str);
return *this;
}
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)
//{
// _str[end + 1] = _str[end];
// --end;
//}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
}
void String::insert(size_t pos, const char* str)
{
assert(pos < _size);
size_t len = strlen(str);
if (len + _size > _capacity)
{
//大于2倍,需要多少开多少,小于2倍按2倍扩
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;
}
void String::erase(size_t pos, size_t len)// 缺省参数只能声明的时候给
{
assert(pos < _size);
if (len >= _size - pos)// 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)
{
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
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;
}
else
{
return ptr - _str;// 两个指针相减就是要返回的下标
}
}
String String::substr(size_t pos, size_t len)
{
assert(pos < _size);
if (len > _size - pos)
{ // 更新len
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 || s1 == s2;
}
bool operator>(const String& s1, const String& s2)
{
return !(s1 <= s2);
}
bool operator>=(const String& s1, const String& s2)
{
return !(s1 < s2);
}
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);
}
ostream& operator<<(ostream& out, const String& s)
{ // 不一定是友元(没有访问类的私有),但必须是全局
for (auto ch : s)
{
out << ch;// 挨个取出来再插入
}
return out;
}
istream& operator>>(istream& in, String& s)
{
s.clear();//将s原有的数据清空
const int N = 256;
char buff[N];//空间为256的数组用来存储字符串,避免多次扩容和大量空间浪费的问题
int i = 0;
char ch;
//流提取遇到' '和'\0'无法读取
//in >> ch;
ch = in.get();//只获取一个字符
while (ch != ' ' && ch != '\0')
{
buff[i++] = ch;//先存储在数组里
if (i == N - 1)// i = 255
{
buff[i] = '\0';//存满了
s += buff;// +=会给s扩容
i = 0;
}
//in >> ch;
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
void test1()
{
String s1;
String s2("hello world");
cout << s2.c_str() << endl;
for (size_t i = 0; i < s2.size(); i++)
{
s2[i] += 2;
}
cout << s2.c_str() << endl;
String::iterator it = s2.begin();
while (it != s2.end())
{
*it -= 2;
cout << *it << " ";
++it;
}
cout << endl;
for (auto ch : s2)
{
cout << ch << " ";
}
}
void test2()
{
String s1("hello world");
s1 += 'a';
s1 += 'b';
cout << s1.c_str() << endl;
s1 += "123";
cout << s1.c_str() << endl;
s1.insert(5, '*');
cout << s1.c_str() << endl;
String s2("hello world");
s2.insert(5, "%%%");
cout << s2.c_str() << endl;
s2.insert(0, "%%%");
cout << s2.c_str() << endl;
s2.erase(5, 3);
cout << s2.c_str() << endl;
s2.erase(5, 100);
cout << s2.c_str() << endl;
}
void test3()
{
String s1("hello world");
size_t pos = s1.find(' ');
String last = s1.substr(pos);
cout << last.c_str() << endl;
String s2(s1);
cout << s2.c_str() << endl;
s1 = s2;
cout << s1.c_str() << endl;
s2 = s2;
cout << s2.c_str() << endl;
}
void test4()
{
String s1("hello world");
String s2("hello world");
cout << (s1 < s2) << endl;
cout << (s1 == s2) << endl;
//单参数构造函数支持隐式类型转换,所以没有函数实现也支持字符串和string比较
cout << ("hello world" < s2) << endl;
cout << (s1 == "hello world") << endl;
//运算符重载必须有个类类型的参数,所以下面的会被识别成指针的比较
cout << ("hello world" == "hello world") << endl;
cout << s1 << " " << s2 << endl;
String s3;
cin >> s3;
cout << s3 << endl;
}
}
(3)Test.cpp
// Test,cpp
#include"String.h"
int main()
{
zyt::test4();
return 0;
}
(4)注意
1. 【static const 整形】的类型可以直接定义,其余static类型都要声明和定义分离。
static const size_t npos = -1;
2. 短小频繁调用的函数(如类的构造、拷贝构造、赋值运算符重载以及析构函数等)可以直接定义在类里,默认是inline
3.全缺省构造函数用的是:String(const char* str = "")
C语言规定常量字符串后面自带一个‘\0’,
//String(const char* str = "\0") 错误示范,因为常量字符串后面自带一个'\0',所以这里重复写了。//String(const char* str = nullptr) 错误示范,如果对无参对象应用c_str,接口会持续对空指针解引用直到遇到'\0'为止。
4. String类如果没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是, s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃, 这种拷贝方式,称为浅拷贝。
5. 显示赋值重载,否则也会默认调用浅拷贝,导致内存泄漏
6. 封装:屏蔽了底层实现细节,提供了统一的类似访问容器的方式,不需要关心容器底部结构和实现细节
7. 当操作符两端数据类型不同会发生类型提升(提升成范围较大的),int类型和size_t类型比较会发生整形提升,两边都是正数不会有影响,但当int类型的数是负数(-1)时,就会出问题。
8. 在实现流输入时,为避免多次扩容和预留空间浪费过多,可以开一个空间为256的数组用来先存储字符串。
9. 实现流输入中,流提取遇到' '空格会被自动跳过,直到遇到下一个非空白字符,遇到'\0'结束读取,所以用到
get()
函数,get()
函数是istream
类的一个成员函数,用于从输入流中读取字符,get()
函数不会自动跳过任何空白字符,包括空格和制表符
10. 单参数构造函数支持隐式类型转换,所以没有函数实现也支持字符串和string比较:
String s1("hello world");
cout << (s1 == "hello world") << endl;
11. 运算符重载必须有个类类型的参数,所以下面的会被识别成指针的比较
cout << ("hello world" == "hello world") << endl;
2.浅拷贝
浅拷贝:也称位拷贝, 编译器只是将对象中的值拷贝过来 。如果对象中管理资源,最后就会导致 多个对象共享同一份资源 ,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。或者修改一个对象资源,会影响另外一个对象。
采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。
具体介绍可以看类和对象(中)的拷贝构造函数部分中有介绍:http://t.csdnimg.cn/2xIJH
3.深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
每个String类对象都要用空间来存放字符串,而s2要用s1拷贝构造出来,因此
深拷贝:给每个对象独立分配资源,保证多个对象之间不会因共享资源造成多次释放导致程序崩溃问题
(1)传统版String的写法
class String
{
public:
// 全缺省的构造
String(const char* str = "")//C语言规定常量字符串后面自带一个‘\0’
{//单参数构造函数支持隐式类型转换
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 深拷贝传统写法
//显示拷贝构造,否则其他函数(substr)会默认调用浅拷贝
String(const String& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
//传统写法
String& operator=(const String& s)
{
if (this != &s)
{// 避免自己给自己赋值时,将原有空间释放
delete[] _str;// 把原有的空间直接释放掉
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
~String()
{ //delete nullptr 也不会报错,直接返回
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
//...
private:
char* _str;
size_t _size;
size_t _capacity;
};
(2)现代版String的写法
class String
{
public:
String(const char* str = "")
{
if (nullptr == str)
{
assert(false);
return;
}
_str = new char[strlen(str) + 1];
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)
{// 不能将随机值交换给tmp,释放时就释放成野指针了,
// 内置类型在定义时初始化成nullptr
String tmp(s._str);
swap(tmp);// this 和 tmp交换(调的是命名空间的交换函数)
}
//现代版赋值重载= (s1 = s3)
//String& operator=(const String& s)
//{
// if (this != &s)
// {
// String tmp(s._str);
// swap(tmp);//tmp指向s3空间,出作用域tmp自动销毁
// }
// return *this;
//}
//s1 = s3 现代版赋值重载=(简化版)
String& operator=(String tmp)//这里直接触发拷贝构造tmp(局部变量)
{
swap(tmp);
return *this;
}
~String()
{ //delete nullptr 也不会报错,直接返回
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
//...
private://内置类型未初始化(初始化列表)编译器默认是随机值
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
}
● 实现现代版拷贝构造:
1. 内置类型未初始化(初始化列表)编译器默认是随机值,调用变现代拷贝构造使用swap函数,不能将随机值交换给tmp,释放局部变量tmp时,就释放成野指针!
2. 内置类型在定义时初始化,指针给缺省值nullptr,其余给缺省值0。
3. 实现s2(s1):
用s1创造局部变量tmp,再将s2与局部变量tmp交换,tmp指向nullptr,s2指向tmp原有资源,tmp出作用域销毁。
● 实现现代版赋值重载=:
类似于拷贝构造用s1创造局部变量tmp,再将s3与局部变量tmp交换,tmp指向s3原有资源,s3指向tmp原有资源,函数返回所需要的s3指向资源,tmp出作用域销毁。
(3)思考(swap)
C++98,算法库里面的swap和我们String类里实现的swap有什么区别?
// swap函数模板,具体实现调用的是实例化版本
template <class T> void swap ( T& a, T& b )
{
T c(a); a=b; b=c;
}
算法库里面的swap调用了拷贝构造,赋值;总共实现3次深拷贝,多次开空间和拷贝数据,效率低。
void swap(String& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
类里实现的swap只实现了1次深拷贝,内置类型的交换不用开空间,大大提高了效率。
(4)写实拷贝
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。引用计数 :用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源, 如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有 其他对象在使用该资源。
使用写实拷贝优点:
Func()函数返回值可以不用调用深拷贝,而是调用浅拷贝,让ret指向str的原有空间,出了作用域str销毁,引用计数-1,这种方式提高效率。
vs一般默认是深拷贝,有的的编译器可能用的是写实拷贝。
//如果用写实拷贝,要在每个修改对象的接口中都插入该函数
void copy_on_write()
{
if (count > 1)
{
//深拷贝
}
}
4.vs和g++下string结构的说明
注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。
(1)vs下string结构的说明
string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字符串的存储空间:● 当字符串长度小于16时,使用内部固定的字符数组来存放● 当字符串长度大于等于16时,从堆上开辟空间union _Bxty { // storage for small buffer or pointer to larger one value_type _Buf[_BUF_SIZE]; pointer _Ptr; char _Alias[_BUF_SIZE]; // to permit aliasing } _Bx;
这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量最后:还有一个指针做一些其他事情。故总共占16+4+4+4=28个字节。
(2)g++下string的结构
G++下,string是通过写时拷贝实现的,string对象总共占4个字节(32位)/ 8字节(64位),内部只包含了一个 指针,该指针将来指向一块堆空间,内部包含了如下字段:● 空间总大小● 字符串有效长度● 引用计数● 指向堆空间的指针,用来存储字符串。struct _Rep_base { size_type _M_length; size_type _M_capacity; _Atomic_word _M_refcount; };