3.2 string
字符串处理问题是C++语言编程中经常遇到的问题,熟练地掌握字符串处理的方法,可以增强对字符串的存储和其处理方法的理解,从而写出高效的C++程序。
在前面第1章讲到,字符串可以用字符指针char*、字符数组等来表示,先来回顾一下字符指针和字符数组的使用注意点。
比如下面这几行代码:
char str[12] =
"Hello";
char *p = str;
*p = 'h'; // 改变第一个字母
再看这几行代码:
char *ptr =
"Hello";
*ptr = 'h'; // 错误
第一个字符串时用数组开辟的,它是可以改变的变量。而第二个字符串则是一个常量,也就是不可改变的值。ptr只是指向它的指针而已,而不能改变指向的内容。这部分区别看两者的汇编语言即可明了:
char p[] =
"Hello";的汇编代码如下:
004114B8 mov
eax,dword ptr [string "Hello" (4166FCh)]
004114BD mov
dword ptr [ebp-10h],eax
004114C0 mov
cx,word ptr ds:[416700h]
004114C7 mov
word ptr [ebp-0Ch],cx
char *ptr =
"Hello";的汇编代码如下:
004114CB mov
dword ptr [ebp-1Ch],offset string "Hello" (4166FCh)
可见用数组和用指针是完全不相同的。
要想通过指针来改变常量是错误,正确的写法应该是用const指针,如下所示:
const char *ptr
= "Hello";
除了以上限制外,字符数组、字符指针的字符串会有要考虑内存释放是否足够、字符串长度等的问题,因此本章主要讲解string。它是一个字符串的类,它集成的操作函数足以完成大多数情况下的需要。可以用“=”进行赋值操作,“==”进行比较,“+”做串联,使用非常简单,甚至可直接把它看作C++的基本数据类型。
为了在程序中使用string类型,必须包含头文件<string>,如下所示:
#include<string>
string.h和cstring都不是string类的头文件,这两个头文件主要定义C语言风格字符串操作的一些方法,譬如strlen()、strcpy()等。string.h是C语言的头文件格式,而cstring是C++风格的头文件,但是和<string.h>是一样的,它的目的是为了和C语言兼容。
1.?string类的实现
实现string类是一道考验C++基础知识的好题,接下来先看这样的一道题目,了解string类的内部实现。
已知类string的原型代码如下所示,请编写类string的7个类。
class String{
public:
String(const char *str = NULL); // 普通构造函数
String(const String &other); // 拷贝构造函数
~ String(); //
析构函数
String & operator =(const String
&other); // 赋值函数
String & operator +(const String
&other); // 字符串连接
bool operator ==(const String &other); // 判断相等
int getLength(); // 返回长度
private:
char *m_data; // 私有变量保存字符串
};
从上述程序可以看到,string类其实是一个对字符串指针有一系列操作动作的类,也就是说,string类的底层是一个字符串指针。这一题的参考答案以及注意点如下所示。
(1)普通构造函数。
String::String(const
char *str){
if(str==NULL){
m_data = new char[1];
*m_data = '\0';
// 对空字符串自动申请存放结束标志'\0'的加分点:对m_data加NULL 判断
}
else{
int length = strlen(str);
m_data = new char[length+1];
strcpy(m_data, str);
}
}
普通构造函数里需要注意的是,传入的是个char*类型的字符串。如果传入的str是个空的字符串,那这个string就也是一个空的字符串,直接用\0赋值。如果传入的str是非空字符串,私有变量m_data就需要预留length+1的长度,其中“+1”是用来放最后的'\0'的,因为strlen计算字符串长度时,没把'\0'算进去。
(2)String的析构函数。
String::~String(){
if(m_data){
delete[] m_data; // 或delete m_data;
m_data=0;
}
}
析构函数的主要功能主要是删除成员变量,需要先判断字符指针是否为空,如果不为空,再将其删除,并将其指向NULL。
(3)拷贝构造函数。
String::String(const
String &other){ // 输入参数为const型
if(!other.m_data){ // 对m_data加NULL 判断
m_data=0;
}
m_data = new char[strlen(other.m_data)+1];
strcpy(m_data, other.m_data);
}
拷贝构造函数里需要注意的是,传入的参数是个常引用,这样可以不用新增一个栈变量和参数内容可以保持不变,不被修改。
(4)赋值函数。
String &
String::operator =(const String &other){
// 输入参数为const型
if(this != &other){ // 检查是否自赋值
delete[] m_data; // 释放原有的内存资源
if(!other.m_data){ // 对m_data作NULL 判断
m_data=0;
}
else{
m_data=new
char[strlen(other.m_data)+1];
strcpy(m_data, other.m_data);
}
}
return *this; // 返回本对象的引用
}
赋值函数里需要注意的是,如果传入的参数内容已与本身的内容一致,则不需要赋值。如果传入的参数内容与本身内容不一致,需要先清空本身的内容。
(5)字符串连接。
String &
String::operator +(const String &other){
String newString;
if(!other.m_data){
newString=*this;
}
else if(!m_data){
newString=other;
}
else{
newString.m_data=new
char[strlen(m_data)+strlen(other.m_data)+1];
strcpy(newString.m_data,m_data);
strcat(newString.m_data,other.m_data);
}
return newString;
}
字符串连接函数里需要注意的分3种情况:传入的参数内容为空、本身内容为空或两者内容都不为空。
(6)判断相等。
bool
String::operator= =(const String &other){
if(strlen(m_data)!=strlen(other.m_data)){
return false;
}
else{
return
strcmp(m_data,other.m_data)?false:true;
}
}
判断相等函数,返回值只有true和false,先判断长度是否一致,再判断内容是否一致。
(7)返回长度。
int
String::getLength(){
return strlen(m_data);
}
返回长度函数,只需用strlen直接计算char*的长度即可。
2.?string声明方式
字符串变量的声明形式如下:
string Str;
这样就声明了一个字符串变量,但既然是一个类,就有构造函数和析构函数。上面的声明没有传入参数,所以就直接使用了string的默认的构造函数,这个函数所做的就是把Str初始化为一个空字符串。string类的构造函数和析构函数如下所示:
string s; // 生成一个空字符串s
string s(string
str) // 拷贝构造函数 生成str的复制品
string s(string
str,int stridx) // 将字符串str内“始于位置stridx”的部分当作
// 字符串的初值
string s(char
*str,int stridx,int strlen) // 将字符串str内“始于stridx且长度顶多strlen”
// 的部分作为字符串的初值
string s(char
*cstr) // 将C字符串作为s的初值
string s(char
*chars,int chars_len) // 将C字符串前chars_len个字符作为字符串s的
// 初值
string s(int
num,char c) // 生成一个字符串,包含num个c字符
string s(char
*beg,char *end) // 以区间beg;end(不包含end)内的字符作为字符
// 串s的初值
s.~string() // 销毁s字符,释放内存
string类的声明如例3.2所示。
【例3.2】 string的声明。
#include<iostream>
#include<string>
using namespace
std;
int main(){
string str1="Spend all your time
waiting.";
string str2="For that second
chance.";
string str3(str1,6); // "all your time waiting."
string str4(str1,6,3); // "all"
char ch_music[]={"Sarah
McLachlan"};
string str5=ch_music;
string str6(ch_music);
string str7(ch_music,5); // "Sarah"
string str8(4,'a'); // aaaa
string str9(ch_music+6,ch_music+14); // " McLachlan"
cout<<"str1:"<<str1<<endl;
cout<<"str2:"<<str2<<endl;
cout<<"str3:"<<str3<<endl;
cout<<"str4:"<<str4<<endl;
cout<<"str5:"<<str5<<endl;
cout<<"str6:"<<str6<<endl;
cout<<"str7:"<<str7<<endl;
cout<<"str8:"<<str8<<endl;
cout<<"str9:"<<str9<<endl;
return 0;
}
程序的执行结果是:
str1:Spend all
your time waiting.
str2:For that
second chance.
str3:all your
time waiting.
str4:all
str5:Sarah
McLachlan
str6:Sarah
McLachlan
str7:Sarah
str8:aaaa
str9:McLachla
例3.2中展示了声明一个string字符串的各种方式。
3.?C++字符串和C字符串的转换
C++提供的由C++字符串转换成对应的C字符串的方法是使用data()、c_str()和copy()来实现。其中,data()以字符数组的形式返回字符串内容,但并不添加'\0';c_str()返回一个以'\0'结尾的字符数组;而copy()则把字符串的内容复制或写入既有的c_string或字符数组内。需要注意的是,C++字符串并不以'\0'结尾。
c_str()语句可以生成一个const char *指针,并指向空字符的数组。这个数组的数据是临时的,当有一个改变这些数据的成员函数被调用后,其中的数据就会失效。因此要么现用现转换,要么把它的数据复制到用户自己可以管理的内存中后再转换。
【例3.3】 c_str()使用方法举例。
#include<iostream>
#include<string>
using namespace
std;
int main(){
string str="Hello world.";
const char * cstr=str.c_str();
cout<<cstr<<endl;
str="Abcd.";
cout<<cstr<<endl;
return 0;
}
程序的执行结果是:
Hello world.
Abcd.
如例3.3所示,改变了str的内容,cstr的内容也会随着改变。所以上面如果继续使用C指针的话,导致的错误将是不可想象的。既然C指针指向的内容容易失效,就可以考虑把数据复制出来解决问题。
【例3.4】 将c_str()里的内容复制出来以保持有效性。
#include<iostream>
#include<string>
#include<string.h>
using namespace
std;
int main(){
char * cstr=new char[20];
string str="Hello world.";
strncpy(cstr,str.c_str(),str.size());
cout<<cstr<<endl;
str="Abcd.";
cout<<cstr<<endl;
return 0;
}
程序的执行结果:
Hello world.
Hello world.
例3.4中用strcpy函数将str.c_str()的内容复制到cstr里了,这样就能保证cstr里的内容不随着str的内容改变而改变了。
copy(p,n,size_type
_Off = 0)这句表明从string类型对象中至多复制n个字符到字符指针p指向的空间中,并且默认从首字符开始,也可以指定开始的位置(从0开始计数),返回真正从对象中复制的字符。不过用户要确保p指向的空间足够来保存n个字符。
【例3.5】 string.copy用法的详细举例。
#include<iostream>
#include<string>
using namespace
std;
int main (){
size_t length;
char buffer[8];
string str("Test string......");
cout<<"str:"<<str<<endl;
length=str.copy(buffer,7,5);
buffer[length]='\0';
cout<<"str.copy(buffer,7,5),buffer contains:
"<<buffer<<endl;
length=str.copy(buffer,str.size(),5);
buffer[length]='\0';
cout<<"str.copy(buffer,str.size(),5),buffer
contains:"<<buffer<<endl;
length=str.copy(buffer,7,0);
buffer[length]='\0';
cout<<
"str.copy(buffer,7,0),buffer contains:"<<buffer<<endl;
length=str.copy(buffer,7); // 缺省参数pos,默认pos=0;
buffer[length]='\0';
cout<<"str.copy(buffer,7),buffer
contains:"<<buffer<<endl;
length=str.copy(buffer,string::npos,5);
buffer[length]='\0';
cout<<"string::npos:"<<(int)(string::npos)<<endl;
cout<<"buffer[string::npos]:"<<buffer[string::npos]<<endl;
cout<<"buffer[length-1]:"<<buffer[length-1]<<endl;
cout<<"str.copy(buffer,string::npos,5),buffer
contains:"<<buffer<<endl;
length=str.copy(buffer,string::npos);
buffer[length]='\0';
cout<<"str.copy(buffer,string::npos),buffer
contains:"<<buffer<<endl;
cout<<"buffer[string::npos]:"<<buffer[string::npos]<<endl;
cout<<"buffer[length-1]:"<<buffer[length-1]<<endl;
return 0;
}
程序的执行结果是:
str:Test
string......
str.copy(buffer,7,5),buffer
contains: string.
str.copy(buffer,str.size(),5),buffer
contains:string......
str.copy(buffer,7,0),buffer
contains:Test st
str.copy(buffer,7),buffer
contains:Test st
string::npos:-1
buffer[string::npos]:
buffer[length-1]:.
str.copy(buffer,string::npos,5),buffer
contains:string......
str.copy(buffer,string::npos),buffer
contains:Test string......
buffer[string::npos]:
buffer[length-1]:.
例3.5中展示了通过不同的方法用string的copy方法进行字符串的复制,常见的api的参数一般是把起始点信息放在前面,长度信息放在后面,如string的构造函数string s(char *str,int stridx,int strlen);而copy方法却是把长度放在起始点前面,这个是需要小心使用的。另外,copy函数的第二个参数,除了可以是长度,也可以是一个位置,如string::npos。
string::npos是一个机器最大的正整数,不同机器不一样,如64位机器是184467440
73709551615,而32位机器则是4294967295,类型是std::container_type::size_type。可以将其强制转换成int,就是-1,这样就不会存在移植的问题。一般用npos表示string的结束位置。
copy函数的第三个参数不填时则默认为0,即从第一个字符开始。
综上,可以使用string的c_str()、data()、copy(p,n),从一个string类型得到一个C类型的字符数组。
4.?string和int类型的转换
(1)int转string的方法。
这里需要用到snprintf(),函数原型为:
int
snprintf(char *str, size_t size, const char *format, ...)
它的功能主要是将可变个参数(...)按照format格式化成字符串,然后将其复制到str中,具体如下所述。
1)如果格式化后的字符串长度小于size,则将此字符串全部复制到str中,并给其后添加一个字符串结束符('\0')。
2)如果格式化后的字符串长度不小于size,则只将其中的(size-1)个字符复制到str中,并给其后添加一个字符串结束符('\0'),返回值为欲写入的字符串长度。
3)函数的返回值是若成功,则返回欲写入的字符串长度,若出错则返回负值。
4)如果原始参数为“…”,那么这是个可变参数。
【例3.6】 snprintf的使用举例。
#include<stdio.h>
int main (){
char a[20];
int i = snprintf(a, 9, "%012d",
12345);
printf("i = %d, a = %s", i, a);
return 0;
}
程序的执行结果是:
i = 12, a =
00000001
例3.6中,%012d的格式是指使输出的int型的数值以12位的固定位宽输出,如果不足12位,则在前面补0;如果超过12位,则按实际位数输出。如果输出的数值不是int型,则进行强制类型转换为int,之后按前面的格式输出。那就是先得到了000000012345,再取前面(9-1)位,即8位,则是00000001。
与此类似的,将int转换为string,代码通常可以这么写:
static inline
std::string i64tostr(long long a){
char buf[32];
snprintf(buf, sizeof(buf), "%lld", a);
return std::string(buf);
}
(2)string转int的方法。
这里需要用到strtol,strtoll,strtoul或strtoull函数,它们的函数原型分别如下所示:
long int
strtol(const char *nptr, char **endptr, int base);
long long int
strtoll(const char *nptr, char **endptr, int base);
unsigned long
int strtoul(const char *nptr, char **endptr, int base);
unsigned long
long int strtoull(const char *nptr, char **endptr, int base);
它们的功能是将参数nptr字符串根据参数base来转换成有符号的整型数、有符号的长整型数,无符号的整型数、无符号的长整型数。参数base范围从2~36,或0。参数base代表采用的进制方式,如base值为10则采用10进制;若base值为16则采用16进制数等;当base值为0时会根据情况选择用哪种进制:如果第一个字符是0,就判断第二字符如果是x则用16进制,否则用8进制,第一个字符不是0,则用10进制。一开始strtoul()会扫描参数nptr字符串,跳过前面的空格字符串,直到遇上数字或正负符号才开始做转换,再遇到非数字或字符串结束时(' ')结束转换,并将结果返回。若参数endptr不为NULL,则会将遇到不合条件而终止的nptr中的字符指针由endptr返回。
strtol使用如例3.7所示。
【例3.7】 strtol的使用举例。
#include<iostream>
#include<stdlib.h>
#include<string>
using namespace
std;
int main(){
char *endptr;
char nptr[]="123abc";
int ret = strtol(nptr, &endptr, 10 );
cout<<"ret:"<<ret<<endl;
cout<<"endptr:"<<endptr<<endl;
char *endptr2;
char nptr2[]=" \n\t abc";
ret = strtol(nptr2, &endptr2, 10 );
cout<<"ret:"<<ret<<endl;
cout<<"endptr2:"<<endptr2<<endl;
char *endptr8;
char nptr8[]="0123";
ret = strtol(nptr8, &endptr8,0);
cout<<"ret:"<<ret<<endl;
cout<<"endptr8:"<<endptr8<<endl;
char *endptr16;
char nptr16[]="0x123";
ret = strtol(nptr16, &endptr16,0);
cout<<"ret:"<<ret<<endl;
cout<<"endptr16:"<<endptr16<<endl;
return 0;
}
程序的执行结果是:
ret:123
endptr:abc
ret:0
endptr2:
abc
ret:83
endptr8:
ret:291
endptr16:
例3.7中主要是使用strtol函数对不同的字符串取出整数。
char
nptr[]="123abc";
int ret =
strtol(nptr, &endptr, 10 );
十进制里没有“数字”a,所以扫描到a就结束,因此ret值是123,即endptr是abc。
char *endptr2;
char
nptr2[]=" \n\t abc";
ret =
strtol(nptr2, &endptr2, 10 );
由于函数会忽略nptr前面的空格,所以从字符a开始扫描,但是遇见的“第一个”即是不合法字符,所以函数结束,此时ret=0; endptr =
nptr;
char *endptr8;
char
nptr8[]="0123";
ret =
strtol(nptr8, &endptr8,0);
char *endptr16;
char
nptr16[]="0x123";
ret =
strtol(nptr16, &endptr16,0);
当第三个参数为0时,则分以下3种情况。
1)如果nptr以0x开头,则把nptr当成16进制处理。
2)如果npstr以0开头,则把nptr当成8进制处理。
3)否则,把nptr当成10进制。
nptr8是以0开头的,所以将nptr8当成8进制处理,再将8进制的0123转换为10进制的,得到了1*8^2+2*8+3=83。nptr16是以0x开头的,将nptr16当成16进制处理,再将16进制的0x123转换为10进制的,得到了1*16^2+2*16+3=291。
因此,将字符串转换为10进制的数字,可以这么写:
static inline
int64_t strtoint(const std::string& s){
return strtoll(s.c_str(), 0, 10);
}
5.?string的其他常用成员函数
string的其他常用成员函数有以下几个:
int
capacity()const; // 返回当前容量(即string中不必增加内存即可存放的元素个数)
int
max_size()const; // 返回string对象中可存放的最大字符串的长度
int size()const; // 返回当前字符串的大小
int
length()const; // 返回当前字符串的长度
bool
empty()const; // 当前字符串是否为空
void resize(int
len,char c); // 把字符串当前大小置为len,并用字符c填充不足的部分