C++中提供了一种新的数据类型——字符串类型(string)。实际上string并不是C++的基本类型,它是在C++标准库中声明的一个字符串类,用这种数据类型可以定义对象,每一个字符串变量都是string类的一个对象。标准库类型string表示可变长的字符序列,使用string类型必须首先包含它的头文件。
作为标准库的一部分,string定义在命名空间std中。
【例】
#include<string>//注意这里没有.h
using namespace std;
string类的意义有两个:第一个是为了处理char类型的数组,并封装了标准C中的一些字符串处理的函数。而当string类进入了C++标准后,它的第二个意义就是一个容器。
string类有106个成员接口函数。C++ 的一个常见面试题是让你实现一个 String 类,限于时间,不可能要求具备 std::string 的功能,但至少要求能正确管理资源。具体来说:
1)能像 int 类型那样定义变量,并且支持赋值、复制。
2)能用作函数的参数类型及返回类型。
3)能用作标准库容器的元素类型,即 vector/list/deque 的 value_type。(用作 std::map 的 key_type 是更进一步的要求,本文从略)。
下面是模拟实现string类的几个重要函数:
1、构造函数
【例】
#define _CRT_SECURE_NO_WARNINGS
#include<string>
using namespace std;
class String
{
public:
String(char *str = "")
{
if (str == NULL)
{
_str = new char[1];//为了与delete[]配合使用
*_str = '/0';
size = 0;
}
else
{
int length = strlen(str);
_str = new char[length + 1];
strcpy(_str, str);
size = length;
}
}
~String()
{
if (NULL != _str)
{
delete[] _str;//delete[]释放的空间必须是动态分配的
}
}
private:
char *_str;//指向字符串的指针
size_t size;//保存当前字符串长度
};
void Test2()
{
String s1("hello");
String s2(new char[3]);
}
int main()
{
Test2();
system("pause");
return 0;
}
释:在构造函数中,_str被初始化为空字符串(只有‘/0’)而不是NULL。因为C++中的任何字符串的长度至少为1(即至少包含一个结束符‘/0’)。孔字符串也是有效的字符串,它的长度为1,因此他代表一块合法的内存单元而不是NULL。
2、拷贝构造函数
1)浅拷贝
【例】
String::String(const String& s)//浅拷贝
:_str(s._str)
{}
void Test2()
{
String s1("hello");
String s2(s1);
}
以上代码会使系统崩溃。因为s1只是将_str的地址传给s2中的_str,即浅拷贝(位拷贝),如下图所示:
由图可知:s1和s2指向同一块内存,析构时先对s2释放“hello”这块空间,当要再析构s1时,系统将崩溃。
释:浅拷贝又称为位拷贝,编译器只是将指针的内容拷贝过来,导致多个对象共用一块内存空间,当其中任意对象将这块空间释放之后,当再次访问时将出错。
解决方法:(深拷贝)给要拷贝构造的对象重新分配空间。
【例】
String::String(const String& s)//深拷贝
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
size = strlen(s._str);
}
void Test2()
{
String s1("hello");
String s2(s1);
}
其运行的状态如图所示:
由图可知:拷贝的对象s2中_str的值(字符串的地址)和s1对象中的_str的值不同,“hello”保存在地址不同的两个空间里,说明了系统为对象重新开辟了空间——深拷贝。
其工作原理如图所示:
3、拷贝赋值函数
【例】
//方法一:
String& String::operator=(const String& s)
{
if (this != &s)//检查自赋值
{
delete[]_str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
size = strlen(s._str);
}
return *this;//为了支持链式访问
}
//方法二:
String& String:: operator=(const String& s)
{
if (this != &s)//1)检查自赋值
{
//2)创建临时变量获得分配的内存空间,并复制原来的内容
char *tmp = new char[strlen(s._str) + 1];
strcpy(tmp, s._str);
delete[]_str;//3)释放原有的内存
_str = s._str;
size = strlen(s._str);
}
return *this;//4)返回本对象引用
}
其运行状态如图所示:
分析:一般情况下,上面的两种写法都可以,但是相对而言,第二种更优一点。
方法一,先释放原有的空间,但是如果下面用new开辟新空间时失败了,而这时将s2赋值给s3,不仅不能成功赋值(空间开辟失败),还破坏了原有的s3对象。
方法二,先开辟新空间,将新空间的地址赋给一个临时变量,就算这时空间开辟失败,也不会影响原本s3对象。
综上所述:第二种方法更优一点。
注意:最后的返回值是为了支持链式访问。
例如:s3 = s2 = s1;
4、拷贝构造函数的现代写法:
【例】
String::String(const String& s)
:_str(NULL)//一定要对_str初始化
{
String tmp(s._str);
std::swap(tmp._str, _str);
}
释:如果没有初始化,_str的值很可能是一个随机值,其指向的内存空间是不合法的。当tmp._str和_str交换后析构tmp就会出错。
5、赋值运算符重载函数的两种现代写法
【例】
//第一种:
String& String::operator=(String s)
{
std::swap(_str, s._str);
return *this;
}
//第二种:
String& String::operator=(const String& s)
{
if (this->_str != s._str)
{
String tmp(s);
std::swap(tmp._str, _str);
}
return *this;
}
在面试时,一般写出上面四个string类的成员函数即可,除非面试官特别要求。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/18192.html