- 为什么要用string类代替char*?
1.为了字符串操作方便,C标准库中提供了一些库函数,但是这些库函数与字符串是分离的,不符合面向对象的思想,而且底层空间需要用户自己来管理,很有可能越界访问
2.2.在常规工作中,为了简单、方便、快捷,基本都使用string类 - string类的了解
- string是表示字符串的字符串类
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator> string;
- 不能操作多字节或者变长字符的序列。
- string类的常用接口介绍
1.string类对象的常见构造
string():构造空的string类对象,即空字符串
string(const char* s) : 用C-string来构造string类对象
string(size_t n, char c) :string类对象中包含n个字符c
string(const string&s) : 拷贝构造函数
例如:
string s1; // 构造空的string类对象s1
string s2("hello bit"); // 用C格式字符串构造string类对象s2
string s3(s2);//拷贝构造
2.string类对象的容量操作
size():返回字符串有效字符长度
capacity(): 返回空间总大小
empty ():检测字符串是否为空串,是返回true,否则返回false
clear(): 清空有效字符
reserve(): 即改变底层容量的大小,不会改变有效元素个数,当扩容时,新空间大小<=旧空间大小时,容量不改变,当新空间大小>旧空间大小时,扩容;当减小容量时,在vs下,若新空间大小>=15,不会缩小,若新空间大小<15时,缩小容量
resize() : 即改变有效元素个数,当增多有效元素到n个时,多出的位置用ch填充(可能会扩容);当减少有效元素到n个时,只修改有效元素个数,不会缩小空间
3.string类对象的访问及遍历操作
例如:
int main()
{
string s1;
string s2("hello");
string s3(10, 'a');
string s4(s2);//拷贝构造
//string类支持C++标准的输入输出
cin >> s1;
cout << s1 << endl;
for (auto e : s2)//遍历方式:范围for
cout << e << " ";//e表示s2字符串中每个字符
cout << endl;
for (size_t i = 0; i < s3.size(); ++i)
cout << s3[i];
cout << endl;
//迭代器的方式来遍历
//迭代器当成是指针
//string::iterator it = s4.begin();
auto it = s4.begin();
while (it != s4.end())//string:[begin,end)--->begin代表的是首字符的位置
//end代表最后一个有效字符的下一个位置
{
cout << *it;
++it;
}
cout << endl;
return 0;
}
begin指首元素的位置,end指最后一个有效字符的下一个位置
4. string类对象的修改操作
push_back(char c) 指字符串尾插字符c;append 指在字符串后追加一个字符串;operator+=str 指在字符串后追加字符串str;c_str()指返回C格式字符串;find + npos指从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置;rfind 指从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置;substr 指在str中从pos位置开始,截取n个字符,然后将其返回。
例如:
int main()
{
string s;
for (size_t i = 0; i < 100; ++i)
s.push_back('$');//不断地扩容了,成本比较大
return 0;
}
其中尾插操作会不断地扩容,成本比较大,因此可以为:
string s;
s.reserve(100);//直接将容量扩好,这样效率比较高
size_t sz = s.capacity();
cout << "s grow" << endl;
for (int i = 0; i < 100; ++i)
{
s.push_back('%');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed" << endl;
}
}
运用reserve直接将容量扩好,效率比较高
- 在线OJ笔试题
1、将指定字符后面的字符删除
例如:将空格后的字符删除
int main()
{
string s("hello world");
s.erase(s.begin() + s.find(' '), s.end());//将指定字符后面的删除
return 0;
}
2.拿到指定字符串
例如:拿到www.baidu.com这个网址
string strRet("https://www.baidu.com/s?wd=retriev");//要截取www.baidu.com
size_t start=strRet.find("://");
if (start != string::npos)
cout << "is in" << endl;
else
cout << "is not in" << endl;
start+=3;
size_t finish=strRet.find('/',start);
string ret=strRet.substr(start,finish-start);
cout<<ret<<endl;
3.求字符串最后一个单词的长度
int main()
{
string s;
getline(cin, s);//输入字符串,不能用cin,因为遇到空格就停止了
cout<<s.substr(s.rfind(' ') + 1).size() << endl;
return 0;
}
分析:先输入字符串,别使用输入函数,因为遇到空格就停止了,然后从后往前找到空格,截取空格+1后面的字符串,求长度。
4.寻找字符串中第一个只出现一次的字符,返回下标
例如:
class S
{
public:
S(const char* c)//构造函数
:s (c)
{}
int FirstUniqChar()
{
int count[256] = { 0 };
int size = s.size();
for (int i = 0; i < size; ++i)
{
count[s[i]] += 1;
}
for (int i = 0; i < size; ++i)
{
if (1 == count[s[i]])
return i;
}
return -1;
}
private:
string s;
};
int main()
{
S s1("aaacvccbnnmj");
int ret=s1.FirstUniqChar();
cout << ret << endl;
return 0;
}
5.翻转字符串
class S
{
public:
S(const char* c)
:s(c)
{}
string reverseString()
{
if (s.empty())
return s;
size_t start = 0;
size_t end = s.size() - 1;
while (start < end)
{
swap(s[start], s[end]);
++start;
--end;
}
return s;
}
private:
string s;
};
int main()
{
S s1("hello");
string s2=s1.reverseString();
cout << s2;
cout << endl;
return 0;
}
6.判断一个字符串是否是回文,只要数字或者字母,其他字符忽略
class S
{
public:
S(const char* c)
:s(c)
{}
bool LetterOrNumber(char ch)
{
return (ch >= '0'&&ch <= '9')
|| (ch >= 'a'&&ch <= 'z')
|| (ch >= 'A'&&ch <= 'Z');
}
bool isPalindrome()
{
for (auto& ch : s)//将小写字母转换为大写
{
if (ch >= 'a'&&ch <= 'z')
ch -= 32;
}
int begin = 0;
int end = s.size() - 1;
while (begin < end)
{
while (begin < end&&!LetterOrNumber(s[begin]))
{
++begin;
}
while (begin < end&&!LetterOrNumber(s[end]))
{
--end;
}
if (s[begin] != s[end])
return false;
else
{
++begin;
--end;
}
}
return true;
}
private:
string s;
};
int main()
{
S s1("a bba cddc");
if (s1.isPalindrome() == true)
cout << "是回文" << endl;
else
cout << "不是回文" << endl;
system("pause");
return 0;
}
7.给定两个字符串形式的非负整数 num1 和num2,计算它们的和
class Solution
{
public:
Solution(const char* c1,const char* c2)
: num1(c1)
, num2(c2)
{}
string addStrings()
{
int Lsize = num1.size();
int Rsize = num2.size();
if (Lsize<Rsize)//保证左操作数比右操作数位数多
{
num1.swap(num2);
swap(Lsize, Rsize);
}
char step = 0;//表示进位
string strRet(Lsize + 1, '0');//结果应该比长的左操作数多了一位
//以num1的字符个数作为循环上限
for (int idxL = Lsize - 1, idxR = Rsize - 1; idxL >= 0; --idxL, --idxR)
{
char cRet = num1[idxL] - '0' + step;
//每一位的相加,但是是字符,转换为数字,结果可能超过10,需要进位
if (idxR >= 0)
cRet += num2[idxR] - '0';
step = 0;//将上一次进位清0
if (cRet >= 10)
{
step = 1;
cRet -= 10;
}
strRet[idxL + 1] += cRet;//最低位加上cRet
}
if (step == 1)//如果最后一次要进位
strRet[0] += 1;
else//否则删除多出来的那一位
strRet.erase(strRet.begin());
return strRet;
}
private:
string num1;
string num2;
};
int main()
{
Solution s1("111","33");
string ret=s1.addStrings();
for (size_t i = 0; i < ret.size(); ++i)
{
cout << ret[i];
}
cout << endl;
system("pause");
return 0;
}
源代码(github):
https://github.com/wangbiy/C-/tree/master/test_2019_9_6_1/test_2019_9_6_1
- 深浅拷贝
1、浅拷贝问题
例如:
namespace Daisy
{
class string
{
public:
string();
string(const char* str = "")
{
/*
if (nullptr == str)
{
_str = new char[1];
*_str = '\0';
}
else
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
*/
if (nullptr == str)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~string()
{
if (_str)
{
delete[]_str;
_str = nullptr;
}
}
private:
char* _str;//动态空间
};
}
int main()
{
Daisy::string s1("hello");
Daisy::string s2(nullptr);
//Daisy::string s3(s1);
//拷贝构造函数没有显式定义,编译器自动生成一个,
//但是s3和s1指向同一块空间,最后释放时先释放s3,
//然后s1又释放了一次,一块空间被释放了两次,这就是浅拷贝
//s2 = s1;//编译器生成了默认的赋值运算符重载,也会发生浅拷贝
return 0;
}
定义了一个string类,并没有显式定义拷贝构造函数和赋值运算符重载,因此在调用拷贝构造函数和赋值运算符重载时,编译器自动生成,例如:
(1)定义s3对象,利用拷贝构造函数拷贝构造s1,由于对象中含有指针数据类型这时s3与s1指向同一块内存,出了作用域后,调用析构函数先清理s3,再清理s2,再清理s1,但是由于s3与s1指向同一块内存,释放的是同一块空间,会崩溃(同一块内存被释放2次),这就是浅拷贝
2.解决浅拷贝问题第一种方法(深拷贝)
释放内存的时候不会重现重复释放同一块内存的情况,因为深拷贝是给每个对象分配一个资源
例如:
(1)传统写法(老老实实开空间,再拷贝数据)
namespace Daisy
{
class string
{
public:
string(const char* str = "")
{
if (nullptr == str)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~string()
{
if (_str)
{
delete[]_str;
_str = nullptr;
}
}
//深拷贝,即给每个对象分配资源
string(const string& s)
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
string& operator=(const string& s)
{
if (this != &s)
{
//delete[]_str;//将原来的空间释放
//_str = new char[strlen(s._str) + 1];
//strcpy(_str, s._str);
char* ptr = new char[strlen(s._str) + 1];
strcpy(ptr, s._str);
delete[]_str;
_str = ptr;//这种方式比较好,因为上一种方法实现释放空间,后来再申请空间,可是万一后面申请空间失败,就会出错
}
return *this;
}
private:
char* _str;
};
}
int main()
{
Daisy::string s1("hello");
Daisy::string s2(nullptr);
Daisy::string s3(s1);
s3 = s1;
return 0;
}
进行拷贝构造和赋值运算符重载的显式定义,拷贝构造是给成员变量先分配一块对象资源大小的空间,然后将对象的内容拷贝给成员,而赋值运算符重载有两种方法,一种是将原来的空间释放,重新分配对象资源大小的空间,再拷贝进去对象的内容,另一种是定义一个临时指针变量,分配空间,将对象的内容拷贝进去,然后释放原空间,再将指针指向临时变量空间。第二种方法比较好,因为如果使用第一种方法,先释放了原空间,再重新申请空间,但是万一申请不成功(一般不会申请不成功,但要考虑到),就会造成问题
(2)现代版本(让别人拷贝好之后,再换过来)
例如:
namespace Daisy
{
class string
{
public:
string(const char* str = "")
{
if (nullptr == str)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~string()
{
if (_str)
{
delete[]_str;
_str = nullptr;
}
}
//深拷贝,即给每个对象分配资源
string(const string& s)//拷贝构造函数
: _str(nullptr)
//string s3(s1),因为pTemp创建了一个和s1相同的空间,相同的内容
//pTemp与s3交换后,s3指向了pTemp指向的内容,而pTemp指向的原本s3指向的内容
//也就是随机值,出了该作用域后,析构函数进行清理工作,pTemp的_str进行释放,
{ //此时是对随机值进行释放(也就是释放野指针,会崩溃),因此在这里给s3的_str初始化为nullptr
{
string pTemp(s._str);//构造一个临时对象
swap(_str, pTemp._str);
}
//string& operator=(const string& s)
//{
// if (this != &s)
// {
// string pTemp(s);//拷贝构造
// swap(_str, pTemp._str);
// }
// return *this;
//}
string& operator=(const string& s)
{
string pTemp(s);//拷贝构造
swap(_str, pTemp._str);
return *this;
}
//string& operator=(string s)
//{
// swap(_str, s._str);
// return *this;
//}
private:
char* _str;
};
}
int main()
{
Daisy::string s1("hello");
Daisy::string s2(nullptr);
Daisy::string s3(s1);
s2 = s1;
return 0;
}
分析:深拷贝就是利用临时空间来拷贝,要注意在构造拷贝构造函数时,将成员变量_str初始化为nullptr,因为在拷贝构造s3时,即string s3(s1),创建了一个临时空间pTemp,它创建了一个和s1相同的空间,具有相同的内容,在与s3交换后,s3指向pTemp的空间,pTemp指向s3的空间,也就是随机值,出了该作用域后,析构函数进行清理工作,释放pTemp的_str,也就是释放随机值,即对野指针进行释放,崩溃,因此将_str初始化为nullptr.
3.解决浅拷贝问题的第二种方法(使用计数的方式及写时拷贝)
计数的方式就是定义一个成员变量,调用构造函数构造对象时,这个变量等于1,调用拷贝构造函数时,这个变量++,因此可以用这个变量表示这时有几个对象,在释放时,释放一个,这个变量–,如果这个变量等于0,销毁这个空间。
(1)如果简单定义一个成员变量来计数,这样在拷贝构造时,两个对象指向同一块内存,在释放资源时,只有一个对象的计数发生改变,另一个并不能发生改变,无法解决
例如:
//使用计数的方式来解决浅拷贝,但是这样使用普通的成员变量,是每个对象有一份计数,并不是共有的,
//因此在销毁s2时,s2中的计数-1,但s1中的计数并没有改变
namespace Daisy
{
class string
{
public:
string(const char* str="")
{
if (str == nullptr)
str = "";
_str = new char[strlen(str)+1];
strcpy(_str, str);
_count = 1;
}
string(string& s)
:_str(s._str)
, _count(++s._count)
{}
~string()
{
if (0==--_count)
{
delete[]_str;
_str = nullptr;
}
}
private:
char* _str;
int _count;
};
}
int main()
{
Daisy::string s1("hello");
Daisy::string s2(s1);
return 0;
}
(2)这时我们想到了static修饰的成员变量,是对象公有的,但是使用这种方法,一旦重新调用构造函数,计数直接就变成了1,使得所有的对象的计数都变成了1,造成问题
例如:
//如果使用static修饰的成员变量作为计数,但是一旦重新调用构造函数,_count又会发生问题,直接变为1,它是公有的,使得三个对象计数都为1,无法解决
namespace Daisy
{
class string
{
public:
string(const char* str = "")
{
if (str == nullptr)
str = "";
else
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
_count = 1;
}
}
string(string& s)
:_str(s._str)
{
++_count;
}
~string()
{
if (0 == --_count)
{
delete[]_str;
_str = nullptr;
}
}
private:
char* _str;
static int _count;
};
}
int Daisy::string::_count = 0;
int main()
{
Daisy::string s1("hello");
Daisy::string s2(s1);
Daisy::string s3("world");//重新调用构造函数,所有对象的计数都变成了1,但是s1和s2共有的空间计数为2
return 0;
}
(3)因此我们可以使用指针解决
例如:
namespace Daisy
{
class string
{
public:
string(const char* str = "")
{
if (str == nullptr)
str = "";
else
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
_pCount=new int(1);
}
}
string(string& s)
:_str(s._str)
, _pCount(s._pCount)
{
++(*_pCount);
}
~string()
{
Release();
}
string& operator=(const string& s)
{
if (this != &s)
{
Release();
_str = s._str;
_pCount = s._pCount;
++(*_pCount);
}
return *this;
}
char& operator[](size_t index)
{
return _str[index];
}
const char& operator[](size_t index)const
{
return _str[index];
}
private:
void Release()
{
if (_str && 0 == --(*_pCount))
{
delete[]_str;
delete _pCount;
_str = nullptr;
_pCount = nullptr;
}
}
private:
char* _str;
int* _pCount;
};
}
int main()
{
Daisy::string s1("hello");
Daisy::string s2(s1);
//共用一份空间
Daisy::string s3("world");
Daisy::string s4(s3);
//共用一份空间
s1 = s3;
s2 = s4;
s1[0] = 'w';//想要改变s1中的内容,但是不能改其他3个对象的内容,所以要分离s1的内容,
//所以就有了写时拷贝
return 0;
}
利用指针来解决这个问题.这个就是重新分配一个整形的空间来存储某一空间的指针的个数,这里的赋值运算符重载就是先清理掉原来的资源(例如s1=s3,就是清理掉s1的资源,将s3的内容赋给s1,计数++,表示一个空间有两个对象),再进行赋值,但是这个仍不能解决改变一个对象的内容,其他与它指向同一块内存的对象的内容也会发生改变的问题,接下来我们采用写时拷贝的在修改一个对象时分离对象的思想来解决
(4)写时拷贝:浅拷贝+引用计数+在修改一个对象时分离对象的思想来解决(可能有线程安全的问题)
例如:
void Swap(string& s)
{
swap(_str, s._str);
swap(_pCount, s._pCount);
}
char& operator[](size_t index)
{
if (*_pCount > 1)//有对象共享一个空间
{
//分离
string strTemp(_str);
Swap(strTemp);
}
return _str[index];
}
如图:
最终的结果就是:
分析:构造一个新的临时对象strTemp,内容是s1的内容,因为当前对象是s1(s1[0]=‘w’),将s1与strTemp交换,即_str、_pCount交换,这样在改变s1中的内容时,不会改变其他对象的内容,这样就是写时拷贝的在修改对象分离对象。
- 深拷贝与写时拷贝的比较:
深拷贝实现简单,不需要考虑线程安全问题,但是在有些场景下写时拷贝的效率能高一些。
源代码(github)
https://github.com/wangbiy/C-/tree/master/test_2019_9_7_1/test_2019_9_7_1