后台开发:核心技术与应用实践3.2 string

简介:

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填充不足的部分

相关文章
|
缓存 安全 Java
Java核心技术之核心类的使用(Spring Guava String JDK工具包)
Google开源Java工具库Guava+Apache Commons的核心类剖析
Java核心技术之核心类的使用(Spring Guava String JDK工具包)
|
设计模式 弹性计算 缓存
C++ STL STRING的COPY-ON-WRITE技术详解
Scott Meyers在《More Effective C++》中举了个例子,不知你是否还记得?在你还在上学的时候,你的父母要你不要看电视,而去复习功课,于是你把自己关在房间里,做出一副正在复习功课的样子,其实你在干着别的诸如给班上的某位女生写情书之类的事,而一旦你的父母出来在你房间要检查你是否在复习时,你才真正捡起课本看书。这就是“拖延战术”,直到你非要做的时候才去做。
274 0
一起谈.NET技术,用lambda去除Magic-String
string是我们的朋友。我们离不开string,但是有时候string也挺烦人的。 比如说,下面的代码,根据方法名来获取MethodInfo: var info = typeof (DateTime).GetMethod("ToShortDateSting");Console.WriteLine(info.Name); 进行“Rename Method”重构时,重构工具是不会去对string进行重命名的。
737 0
|
Java
一起谈.NET技术,深入理解string和如何高效地使用string
  一个月以前我写了一篇讨论字符串的驻留(string interning)的文章,我今天将会以字符串的驻留为基础,进一步来讨论.NET中的string。string interning的基本前提是string的恒定性(immutability),即string一旦被创建将不会改变。
775 0
|
2天前
|
安全 Java 编译器
Java中String、StringBuilder和StringBuffer的区别
Java中String、StringBuilder和StringBuffer的区别