1、(1)字符串:字符的集合,以‘\0'结尾。
(2)字符数组:存放字符的数组。
2、封装一个字符串,即封装string类:
(1)
# define _CRT_SECURE_NO_WARNINGS
# include<iostream>
# include<string.h>
using namespace std;
//string类管理字符串,空间一般为一个字符数组
class String
{
public:
/*String()//构造函数
:_pStr(new char[1])//该字符串中只有'\0',没有其他内容,把'\0'给到引用空间内
{
*_pStr = '\0';//正确
//*_pStr = 0;//此方法可取
//_pStr = 0;//把指针置空,存在内存泄漏
//_pStr = '\0';//把指针置空
//*_pStr = '0';//'\0'和字符0的含义不同
}
String(const char *pStr)//给出空间的首地址(该字符串除了'\0',还有其他的内容
{
//对指针进行判空
if (NULL == pStr)
{
//把空指针当成空串进行处理,空串里面有一个'\0'
_pStr = new char('\0');
}
//把字符串里面的内容存到空间里面
//1、开辟空间
_pStr = new char[strlen(pStr) + 1];
//2、把内容拷贝到新空间里面
strcpy(_pStr, pStr);
}*/
//将上述两中方式合为一个,给定空字符串为一个缺省值
String(const char *pStr=" ")
{
if (NULL == pStr)
{
_pStr = new char('\0');
}
else{
//1、开辟空间
_pStr = new char[strlen(pStr) + 1];
//2、把内容拷贝到新空间里面
strcpy(_pStr, pStr);
}
}
String(const String& s)
:_pStr(s._pStr)
{}
//赋值运算符重载
String& operator=(const String& s)
{
if (this != &s)//若不是自己给自己赋值,字符指针和s中的内容相同
{
_pStr = s._pStr;
}
return *this;
}
~String()
{
if (_pStr)
{
//空间存在,就释放空间
delete[] _pStr;
_pStr = NULL;
}
}
private:
char* _pStr;//给成动态数组,外部用户给与几个字符,即管理几个字符
};
void FuncTest()
{
String s1("haha");
String s2;
}
int main()
{
FuncTest();
return 0;
}
当s2调用析构函数,释放空间后,s1并不知情,s1成为一个野指针;当s1会尝试把s2已经释放的空间再释放一次,就出现问题。
(2)使用测试代码:如下,也会出现问题,程序会崩溃:
void FuncTest()
{
String s1("haha");
String s3;
s3 = s1;
}
出现此问题是因为赋值运算符里面出现问题:
以上(1),(2)即为浅拷贝,(只把对象里面的东西原样放到另一个对象里面,导致多个对象共用同一块空间,导致一块空间被释放多次,引起程序崩溃或内存泄漏。
(3)上述问题的解决方式之一:深拷贝
A、不要公用同一块空间,给每一个对象给一个空间
a、此种方式会出现错误:
String(const String& s)
{
if (_pStr)
delete[] _pStr;
_pStr = new char[strlen(s._pStr) + 1];
strcpy(_pStr, s._pStr);
}
错误原因:此时当前对象还不存在,不用考虑是否为空,若是释放,会导致程序崩溃。
改正:先初始化为空,此时就不用判空和释放
String(const String& s)
:_pStr(NULL)
{
_pStr = new char[strlen(s._pStr) + 1];
strcpy(_pStr, s._pStr);
}
或者:
String(const String& s)
:_pStr(new char[strlen(s._pStr)+1])
{
strcpy(_pStr, s._pStr);
}
B、对于
void FuncTest()
{
String s1("haha");
String s3;
s3 = s1;
}
a、方式1,可以采用:申请新空间、拷贝元素、释放旧空间、更改指向(指向新空间)
String& operator=(const String& s)
{
if (this != &s)
{
char *pTmp = new char[strlen(s._pStr) + 1];
strcpy(pTmp, s._pStr);
delete[] _pStr;
_pStr = pTmp;//让当前对象指向新空间
}
return *this;
}
b、方式2
String& operator=(const String& s)
{
delete[] _pStr;//释放旧空间
_pStr = new char[strlen(s._pStr) + 1];//开辟和s大小相同新空间
strcpy(_pStr, s._pStr);//把s中的内容拷贝过来
}
若是开辟空间失败,使用方式1不会对原来的空间造成影响;但是对于方式2开辟空间失败,会使原空间的内容不存在。
深拷贝:让每一个对象都拥有自己的空间,让处理的更深。
C、检测一下浅拷贝的方式有什么错误(实现一个简洁版本的浅拷贝):
class String
{
public:
String(const char *pStr = " ")
{
if (NULL == pStr)
{
_pStr = new char('\0');
}
else{
//1、开辟空间
_pStr = new char[strlen(pStr) + 1];
//2、把内容拷贝到新空间里面
strcpy(_pStr, pStr);
}
}
String(const String& s)
{ //当前对象,每个对象得String类里面都有一个_pStr指针,当前对象得指针没有赋初始值,所以是一个野指针,空间中最后存的得什么值,就指向以什么为地址得指针。
String strTmp(s._pStr);//创建临时对象,临时对象也有一块空间、有一个指针_pStr,通过s里面得字符串构造出来的,指针调用的是构造函数而不是拷贝构造函数。临时变量的指针指向tmp空间,该空间与s里面放的内容一模一样。
Swap(_pStr, strTmp._pStr);//交换两个对象中的内容,临时对象的指针变成一个野指针。出了函数的作用域要销毁,但是该空间不是从堆上创建的空间,释放会出错。
}
//赋值运算符重载
/*
String& operator=(const String& s)
{
if (this != &s)
{
String strTmp(s);
Swap(_pStr, strTmp._pStr);
}
return *this;
}
*/
/* //不用自己给自己赋值,直接交换
String& operator=(const String& s)
{
String strTmp(s);
Swap(_pStr, strTmp._pStr);
return *this;
}
*/
/*//不传引用,直接用s里面的值进行交换
String& operator=(String s)
{
Swap(_pStr, strTmp._pStr);
return *this;
}
*/
~String()
{
if (_pStr)
{
delete[] _pStr;
_pStr = NULL;
}
}
private:
char* _pStr;
};
void FuncTest()
{
String s1("haha");
String s2(s1);
}
int main()
{
FuncTest();
return 0;
}
此时程序崩溃,说明拷贝构造函数有问题。
改正拷贝构造函数:
//拷贝构造函数的解决方案:给定一个初始值,赋为空
String(const String& s)
:_pStr(NULL)
{
String strTmp(s._pStr);
Swap(_pStr, strTmp._pStr);
}
b、使用测试函数:
void FuncTest()
{
String s1("haha");
String s2;
s2 = s1;
}
说明赋值运算符的方式出现问题,创建临时变量,也会调用拷贝构造函数。此时不能判断是哪个函数出现问题(拷贝构造函数还是赋值运算符)。
判断下列赋值运算符的函数是否出现问题:
A、
String& operator=(const String& s)
{
if (this != &s)
{
String strTmp(s);
Swap(_pStr, strTmp._pStr);
}
return *this;
}
没有问题:直接拷贝拷贝构造函数的内容,用临时对象与当前对象交换当前对象和参数都已经存在,指针指向的空间一定是合法的,不用赋空,直接交换。出了if判断句,即释放this返回临时对象里面的空间。
B、
String& operator=(const String& s)
{
String strTmp(s);
Swap(_pStr, strTmp._pStr);
return *this;
}
没有问题:只是少了一步检测是否为自己给自己赋值的操作。如果不是自己给自己赋值,没有问题;如果是自己给自己赋值,出了函数的作用域,销毁原来的空间,留下的是临时空间,自己给自己赋值时,相当于临时空间没有含义,但是不会出现问题。但是不如A的方式好。创建临时对象是在函数体里面由用户自己创建。
C、
String& operator=(String s)
{
Swap(_pStr, strTmp._pStr);
return *this;
}
不会出现问题。传值,传参时相当于生成了一个临时变量,此时相当于当前对象和临时变量交换空间,出了作用域临时变量的空间会销毁,临时变量销毁的空间即为当前对象的旧空间。不论是不是自己给自己赋值都要创建临时对象(传参期间由编译器创建),原理与B类似。此种方式也不如A好。
(3)为了解决浅拷贝的问题,还可以采用:同一块空间进行计数,销毁一次即减一,如果计数为1则s2不能销毁,当s1减一后为0,s1必须进行释放。就不会存在一个空间被释放两次的问题。
a、把计数给成普通的类成员:不行
class String
{
public:
String(const char *pStr = " ")
{
if (NULL == pStr)
{
_pStr = new char('\0');
}
else{
//1、开辟空间
_pStr = new char[strlen(pStr) + 1];
//2、把内容拷贝到新空间里面
strcpy(_pStr, pStr);
}
_count = 1;//构造函数标记被几个函数在使用
}
//拷贝构造函数
String(const String& s)
:_pStr(s._pStr)
, _count(++s._count)
{
}
~String()
{
if (_pStr && 0 == --_count)
{
delete[] _pStr;
}
}
private:
char* _pStr;
mutable int _count;//给定一块整型的计数空间
};
void FunTest()
{
String s1("haha");
String s2(s1);
}
int main()
{
FunTest();
return 0;
}
但此时也会出现问题,每个对象都包含一份s2把自己计数减1,变为1;但是s1并不知道,此时也用2减,也为1,都不会释放,导致内存空间不能释放。
b、让计数的变量共享,给成static。解决方式:s2释放后让s1知道s2释放了一个。
class String
{
public:
String(const char *pStr = " ")
{
if (NULL == pStr)
{
_pStr = new char('\0');
}
else{
//1、开辟空间
_pStr = new char[strlen(pStr) + 1];
//2、把内容拷贝到新空间里面
strcpy(_pStr, pStr);
}
_count = 1;//构造函数标记被几个函数在使用
}
//拷贝构造函数
String(const String& s)
:_pStr(s._pStr)
{
_count++;//静态成员变量不能出现在函数体里
}
~String()
{
if (_pStr && 0 == --_count)
{
delete[] _pStr;
}
}
private:
char* _pStr;
static int _count;//给定一块整型的计数空间
};
int String::_count = 0;//刚开始没有空间,将初始值给为0
void FunTest()
{
String s1("haha");
String s2(s1);
}
int main()
{
FunTest();
return 0;
}
但是若是此时测试代码:
void FunTest()
{
String s1("haha");
String s2(s1);
String s3;
}
对于s3出了作用域需要销毁,计数变量变为0。但是此时s1和s2的计数变量也是0,再销毁就会变成负数,出现错误。
c、有几个空间,计数给几个;独立的用独立的空间,给定独立的计数,共享的给出一个计数空间。销毁空间时,若是一个计数已经用了,则减一,用另一个变量继续判定。直到计数为0,则释放。释放字符串空间和存放引用计数的空间。
class String
{
public:
String(const char *pStr = " ")
:_pcount(new int(1))//引用计数初始值给为1
{
//存放字符串的空间
if (NULL == pStr)
{
_pStr = new char('\0');
}
else{
//1、开辟空间
_pStr = new char[strlen(pStr) + 1];
//2、把内容拷贝到新空间里面
strcpy(_pStr, pStr);
}
}
//拷贝构造函数
String(const String& s)
:_pStr(s._pStr)
, _pcount(s._pcount)//共享计数空间
{
++(*_pcount);
}
~String()
{
if (_pStr && 0 == --*_pcount)
{
delete[] _pStr;//销毁字符串的空间
_pStr = NULL;
delete _pcount;//销毁引用变量的空间
_pcount = NULL;
}
}
private:
char* _pStr;
int* _pcount;//几个空间就有几个计数
};
void FunTest()
{
String s1("haha");
String s2(s1);
String s3;
}
int main()
{
FunTest();
return 0;
}
(4)
class String
{
public:
String(const char *pStr = " ")
:_pcount(new int(1))//引用计数初始值给为1
{
//存放字符串的空间
if (NULL == pStr)
{
_pStr = new char[1];
_pStr = new char('\0');
}
else{
//1、开辟空间
_pStr = new char[strlen(pStr) + 1];
//2、把内容拷贝到新空间里面
strcpy(_pStr, pStr);
}
}
//拷贝构造函数
String(const String& s)
:_pStr(s._pStr)
, _pcount(s._pcount)//共享计数空间
{
++(*_pcount);
}
//赋值运算符的重载
String& operator=(const String&s)
{
//考虑是否为当前对象,若是共用同一块空间,改变引用计数的值;
//没有共用一块空间,则将引用计数减1,减1后若是为0,则释放原空间。,指向新空间,将新空间里面的计数加1
//如果两个对象共用一块空间,可以认为是自己给自己赋值。
//如果地址不相同,则当前对象和s没有共用同一块空间
if (_pStr != s._pStr)
{
if (_pStr && 0 == --*_pcount)
{
delete[] _pStr;
delete _pcount;
}
//共用同一块空间、共用同一个引用计数
_pStr = s._pStr;
_pcount = s._pcount;
++*_pcount;//引用计数增加
}
return *this;
}
~String()
{
if (_pStr && 0 == --*_pcount)
{
delete[] _pStr;//销毁字符串的空间
_pStr = NULL;
delete _pcount;//销毁引用变量的空间
_pcount = NULL;
}
}
private:
char* _pStr;
int* _pcount;//几个空间就有几个计数
};
void FunTest()
{
String s1("haha");
String s2(s1);
String s3;
}
int main()
{
FunTest();
return 0;
}
若是少释放一个空间会导致错误,而且会存在空间的浪费。解决方案:把两个空间给成一个连续的空间,上面保存引用计数,下面保存字符串。指针向前倒4个字节即可以释放。代码如下:
class String
{
public:
String(const char *pStr = " ")
{
//存放字符串的空间
if (NULL == pStr)
{
_pStr = new char[1+4];
_pStr += 4;//引用计数占前四个字节,真正开始,得从加上四个字节之后。
_pStr = new char('\0');
}
else{
//1、开辟空间
_pStr = new char[strlen(pStr) + 1+4];
_pStr += 4;
//2、把内容拷贝到新空间里面
strcpy(_pStr, pStr);
}
//放引用计数,空间首地址得位置,_pStr向前偏移四个字节
GetRef() = 1;
}
//拷贝构造函数
String(const String& s)
:_pStr(s._pStr)
{
GetRef()++;
}
//赋值运算符的重载
String& operator=(const String&s)
{
//考虑是否为当前对象,若是共用同一块空间,改变引用计数的值;
//没有共用一块空间,则将引用计数减1,减1后若是为0,则释放原空间。,指向新空间,将新空间里面的计数加1
//如果两个对象共用一块空间,可以认为是自己给自己赋值。
//如果地址不相同,则当前对象和s没有共用同一块空间
if (_pStr != s._pStr)
{
Release();
//共用同一块空间、共用同一个引用计数
_pStr = s._pStr;
++GetRef();//引用计数增加
}
return *this;
}
~String()
{
Release();
}
private:
//获取引用计数
int &GetRef()
{
return *((int *)_pStr - 4);
}
//释放得函数
void Release()
{
if (_pStr && 0 == --GetRef())
{
_pStr -= 4;
delete[] (_pStr-4);//销毁字符串的空间
_pStr = NULL;
}
}
private:
char* _pStr;
};
void FunTest()
{
String s1("haha");
String s2(s1);
String s3;
s3 = s2;
}
int main()
{
FunTest();
return 0;
}
(6)写时拷贝(COW):
class String
{
public:
String(const char *pStr = " ")
{
//存放字符串的空间
if (NULL == pStr)
{
_pStr = new char[1+4];
_pStr += 4;//引用计数占前四个字节,真正开始,得从加上四个字节之后。
_pStr = new char('\0');
}
else{
//1、开辟空间
_pStr = new char[strlen(pStr) + 1+4];
_pStr += 4;
//2、把内容拷贝到新空间里面
strcpy(_pStr, pStr);
}
//放引用计数,空间首地址得位置,_pStr向前偏移四个字节
GetRef() = 1;
}
//拷贝构造函数
String(const String& s)
:_pStr(s._pStr)
{
GetRef()++;
}
//普通成员函数,不能调用const修饰得
char &operator[](size_t index)
{
return _pStr[index];
}
const char &operator[](size_t index)const
{
return _pStr[index];
}
//赋值运算符的重载
String& operator=(const String&s)
{
//考虑是否为当前对象,若是共用同一块空间,改变引用计数的值;
//没有共用一块空间,则将引用计数减1,减1后若是为0,则释放原空间。,指向新空间,将新空间里面的计数加1
//如果两个对象共用一块空间,可以认为是自己给自己赋值。
//如果地址不相同,则当前对象和s没有共用同一块空间
if (_pStr != s._pStr)
{
Release();
//共用同一块空间、共用同一个引用计数
_pStr = s._pStr;
++GetRef();//引用计数增加
}
return *this;
}
~String()
{
Release();
}
private:
//获取引用计数
int &GetRef()
{
return *((int *)_pStr - 4);
}
//释放得函数
void Release()
{
if (_pStr && 0 == --GetRef())
{
_pStr -= 4;
delete[] _pStr;//销毁字符串的空间
_pStr = NULL;
}
}
private:
char* _pStr;
};
void FunTest()
{
String s1("haha");
String s2(s1);
String s3;
s3 = s2;
const String s4("world");
s4[0];
}
int main()
{
FunTest();
return 0;
}
但是此程序存在问题,如果s1和s2和S3共用同一个空间,修改一个三个都会发生改变,仍然为一个浅拷贝。如下测试代码,会导致s1和s2,S3同时发生改变。
void FunTest()
{
String s1("haha");
String s2(s1);
String s3;
String s4(s3);
s3=s1;
s1[0] = 'w';
}
为了优化上述代码,我们要改变哪一个,则将该成员分离出来。我们改变重载下标函数中的内容如下:
char &operator[](size_t index)
{
//引用计数大于1,当前对象一定和别人共用同一块引用空间,
if (GetRef()>1)
{
//分离当前对象,即引用计数减1
--GetRef();
//给当前对象重新开辟空间,用当前对象构造
String strTmp(_pStr);
//把当前对象的字符串赋值为空
_pStr = NULL;
Swap(_pStr, strTmp._pStr);
}
return _pStr[index];
}
采用测试代码如下,会发生错误:
void FunTest()
{
String s1("haha");
String s2(s1);
String s3;
String s4(s3);
s3 = s1;
s1[0] = 'w';
const String s5(s4);
s5[0];
}
这是因为const类型的函数不能调用普通类型的成员函数。解决方法,再重载一个const类型的下标运算符
char &operator[](size_t index)
{
//引用计数大于1,当前对象一定和别人共用同一块引用空间,
if (GetRef()>1)
{
//分离当前对象,即引用计数减1
--GetRef();
//给当前对象重新开辟空间,用当前对象构造
String strTmp(_pStr);
//把当前对象的字符串赋值为空
_pStr = NULL;
Swap(_pStr, strTmp._pStr);
}
return _pStr[index];
}
//const类型的引用的返回值一般都为const,因为当前对象不能改变
const char &operator[](size_t index)const
{
return _pStr[index];
}
此String类不适用于多线程
总结:
1、浅拷贝:位拷贝、拷贝构造函数、赋值运算符重载
多个对象共用同一块资源,当对象被销毁时,同一块资源会被释放
多次引起程序崩溃,或者内存泄漏
解决方式:
(1)深拷贝---每个对象共同拥有自己的资源,必须程序员显示提供拷贝构造函数以及赋值运算符提供:A、普通版本深拷贝 B、简洁版深拷贝---拷贝(调用构造函数创建临时变量,用临时变量和当前对象交换成员变量)。这种方式不易出错,建议使用。
---赋值
(2)引用计数---普通成员变量(每个对象中各有一份,改变自己的,不能改变其他人的 不行)
----静态成员变量(所有的对象所共享,只要调用构造函数,计数置为1;被当前对象一个人使用,不行)
----整型的指针
2、一些常见的String类的功能:
(1)构造空的字符串
string ( );
(2)拷贝构造函数(类类型对象的引用)
string ( const string& str );
(3)构造函数(不是用字符串中所有的内容构造当前对象,而是pos表示从pos位置开始向后取几个字符;npos取得字符得个数。
string ( const string& str, size_t pos, size_t n = npos );
(4)构造函数(用几个字符来构造,n代表有效字符得个数)
string ( const char * s, size_t n );
(5)用字符串直接构造当前对象
string ( const char * s );
(6)字符串构造好了,里面放了n个字符c
string ( size_t n, char c );
(7)用一段区间构造,
template<class InputIterator> string (InputIterator begin, InputIterator end);
(8)获取字符串得长度:size或者length
(9)resize:把当前字符串中有效元素得个数改变成n个字符,char c说明把n个字符中多出来得用c补齐。
void resize ( size_t n, char c );
void resize ( size_t n );相当于调用上一个函数,少传递了第二个参数
(10)capacty:底层空间得实际大小
(11)reserve:把当前字符串底层空间得容量改变,容量既可以增大,又可以减小
(12)clear:清空;调用后成为空字符串
(13)下标运算符得重载:传递要获取地方得字符
const char& operator[] ( size_t pos ) const;
char& operator[] ( size_t pos );
(14)at:返回这个位置得字符
(15)append:拼接
(16)push_back:尾插
(17)assign:给字符串重新赋值(可以用一个字符串给另一个字符串赋值哦)【还可以用c格式得字符串进行赋值,相当于重写了一遍】
string& assign ( size_t n, char c );把一个字符串赋值为c
(18)insert:任意位置得插入字符或者字符串
string& insert ( size_t pos1, const string& str );
从当前位置pos1开始插入一个字符串;
string& insert ( size_t pos1, size_t n, char c );
从当前位置开始插入n个字符c
(19)erase:删除
string& erase ( size_t pos = 0, size_t n = npos );
从当前位置开始删除多少字符
如果两个参数都没有传,相当于清空,把所有得元素都删除
(20)replace:替换某一个位置得字符
(21)swap:对两个字符串进行交换
(22)c_str:C语言格式得字符串
const char* c_str ( ) const;
(23)copy:拷贝
find:查找
rfind:反响查找:从后往前查找
find_first_of:查找第一次出现得
find_last_of:查找最后一次出现得
substr:子串
strcmp:比较
此后就不一一重复介绍了,详细可以查看资料