C++ 字符串串讲

C++ 提供了以下两种类型的字符串表示形式:

    C 风格字符串
    C++ 引入的 string 类类型

C 风格字符串
    C 风格的字符串起源于 C 语言,并在 C++ 中继续得到支持。
    字符串实际上是使用 null 字符 \0 终止的一维字符数组。 
    因此,一个以 null 结尾的字符串,包含了组成字符串的字符。

    \0  -- >  "0"   ASCII  48   char a[]  -- 自动填充 \0 

    下面的声明和初始化创建了一个 RUNOOB 字符串。
    由于在数组的末尾存储了空字符,所以字符数组的大小比单词 RUNOOB 的字符数多一个。

    char site[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'};
    依据数组初始化规则,您可以把上面的语句写成以下语句:
        char site[] = "RUNOOB";

 图1 是 C/C++ 中定义的字符串的内存表示

C++ 编译器会在初始化数组时,自动把 \0 放在字符串的末尾。
尝试输出上面的字符串:

    #include <iostream>
    using namespace std;
    
    int main ()
    {
        char site[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'};
    
        cout << "菜鸟教程: ";
        cout << site << endl;
        
        return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:  \0 的作用在于标识 
    菜鸟教程: RUNOOB    

char 数据类型是最简单的字符数据类型。
    该类型的变量只能容纳 一个字符,而且在大多数系统上,只使用一个字节的内存。
以下示例即声明了一个名为 letter 的 char 变量。
请注意,这里的字符常数就是赋给变量的值,要用 单引号 括起来。

char letter ='A';

下面的程序使用了一个 char 变量和若干字符常数:
    #include <iostream>
    using namespace std;
    int main()  
    {
        char letter;
        letter = 'A';
        cout << letter << endl;
        letter = 'B';
        cout << letter << endl;
        return 0;
    }

    程序输出结果:
        A
        B

    有趣的是,字符与整数密切相关,因为它们在内部其实是被存储为整数。
    每个可打印的字符以及许多不可打印的字符都被分配一个唯一的数字。
    用于编码字符的最常见方法是 ASCII(美国信息交换标准代码的首字母简写)。
    当字符存储在内存中时,它实际上是存储的数字代码。
    当计算机被指示在屏幕上打印该值时,它将显示与数字代码对应的字符。

    例如,数字 65 对应大写字母 A,66 对应大写字母 B,等等。

下面的程序说明了字符之间的关系以及它们的存储方式:

    // This program demonstrates that characters are actually
    // stored internally by their ASCII integer value.
    #include <iostream>
    using namespace std;

    int main()
    {
        char letter;
        letter = 65; // 65 is the ASCII code for the character A
        cout << letter << endl;
        letter = 66; // 66 is the ASCII code for the character B
        cout << letter << endl;
        return 0;
    }

    程序输出结果:
        A
        B

图 2 进一步说明,
当你认为存储在内存中的是字符(如 A、B 和 C)时,实际上存储的是数字 65、66 和 67。


字符常数和字符串常数的区别
    字符常数和 char 变量只能保存 一个字符。
    如果要在常数或变量中存储多个字符,则需要使用更复杂的字符数据类型 string(字符串),
    字符串常数和变量可以包含一系列的字符。

先来认识一下字符串常数,并将它们与字符常数进行一些比较。

在以下示例中,'H' 是字符常数,"Hello" 是字符串常数。

请注意,字符常数用 单引号 括起来,而字符串常数则用 双引号 。

    cout << 'H' << endl;  //这显示的是字符常数
    cout << "Hello" << endl; //这显示的是字符串常数

因为字符串常数几乎可以是任意长度,所以程序必须有一些方法知道它有多长。
    在 C++ 中,这是通过在其末尾附加一个额外的字节并将数字 0 存储在其中来完成的。
    这被称为 null 终止符或 null 字符,它标记着字符串的结尾。

    不要将空终止符与字符 '0' 混淆。
    字符 '0' 的 ASCII 码是 48,而空终止符的 ASCII 码是 0。
    如果要在屏幕上打印字符 0,则显示的其实是 ASCII 码为 48 的字符;
    在使用字符串常数或给字符串变量赋值时,ASCII 代码为 0 的字符将自动附加在其末尾。

来看一个字符串常数存储在内存中的例子。图 3 描述了字符串 "Sebastian" 的存储方式。

    首先  可以发现字符串中的字符存储在连续的内存位置。
    其次,可以看到引号不与字符串一起存储,它们只是在源代码中标记字符串的开头和结尾。
    最后,请注意字符串的最后一个字节,它包含 null 终止符,由 \0 字符表示。

    最后一个字节意味着虽然字符串 "Sebastian" 的长度为 9 个字符,但它实际占用了 10 个字节的内存。

null 终止符是在后台默默奉献的又一个例子。
在显示字符串时,它不会在屏幕上打印,但是它始终在那里默默地做它的工作。

需要注意的是,C++ 会自动在字符串常数的末尾附加 null 终止符。


    现在来比较一下字符和字符串的存储方式。假设程序中有常数 'A' 和 "A"。
图 4 显示了其内部存储方式。

    如图 4 所示,'A' 是一个 1 字节的元素,而 "A" 是一个 2 字节的元素。


    由于字符实际上被存储为 ASCII 码,所以图 5显示了它们在内存中实际存储的内容。

    因为 char 变量只能保存一个字符,所以它可以被赋值为字符 'A',但不能被赋值为字符串 "A":
    char letterOne = 'A';   //该赋值语句正确
    char letterTwo = "A";  //该赋值语句错误

注意,重要的是不要把字符常数和字符串常数搞混。字符常数必须使用单引号括起来,而字符串常数则必须使用双引号。

    现在应该已经了解,有些字符串虽然看起来像是只有一个字符,但实际上并不是。
另外,也有一些字符看起来像是字符串,例如换行符 \n,虽然它由两个字符表示,一个反斜杠和一个控制字符 n,
但是它在内部其实只表示一个字符。

事实上,所有的转义序列在内部都只有 1 个字节。

下面的程序显示了 \n 作为字符常数的用法,用单引号括起来:

    //This program uses character literals.
    #include <iostream>
    using namespace std;

    int main()
    {
        char letter;
        letter = 'A';
        cout << letter << '\n';
        letter = 'B';
        cout << letter << '\n';
        return 0;
    }

    程序输出结果:
        A
        B

1 、定义的时候直接用字符串赋值
    char a[10]="hello";     //sizeof(a) 为 10
或  char a[]="hello";       //sizeof(a) 为 6

注意:不能先定义再给它赋值,如 char a[10]; a[10]="hello"; 这样是错误的!

2 、对数组中字符逐个赋值
    char a[10]={'h','e','l','l','o'};   //sizeof(a) 为 10
或  char a[]={'h','e','l','l','o'};     //sizeof(a) 为 5

3 、利用 strcpy
    char a[10]; 
    strcpy(a, "hello");



C++ 中有大量的函数用来操作以 null 结尾的字符串: --  char[]  

序号  函数 & 目的
1   strcpy(s1, s2);
    复制字符串 s2 到字符串 s1。
2   strcat(s1, s2);
    连接字符串 s2 到字符串 s1 的末尾。连接字符串也可以用 + 号,例如:
    string str1 = "runoob";
    string str2 = "google";
    string str = str1 + str2;
3   strlen(s1);
    返回字符串 s1 的长度。
4   strcmp(s1, s2);  // -- 基于 ASCII   abc  Abc
    如果 s1 和 s2 是相同的,则返回 0;如果 s1<s2 则返回值小于 0;如果 s1>s2 则返回值大于 0。
5   strchr(s1, ch);
    返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。
    (备注一下 -- 为实现)  
7   strupr(s);
    将字符串s转换为大写形式,只转换s中出现的小写字母,不改变其它字符。返回指向s的指针。
8   strlwr(s);
    将字符串s转换为小写形式,只转换s中出现的大写字母,不改变其它字符。返回指向s的指针。
9   strrev(s)
    把字符串s的所有字符的顺序颠倒过来(不包括空字符”\0”),返回指向颠倒顺序后的字符串指针。
10  strset(s,c)
    把字符串s中的所有字符都设置成字符c,返回指向s的指针。

//  实例

#include <iostream>
#include <cstring>
using namespace std;
 
int main ()
{
   char str1[13] = "runoob";
   char str2[13] = "google";
   char str3[13];
   int  len ;
 
   // 复制 str1 到 str3
   strcpy( str3, str1);
   cout << "strcpy( str3, str1) : " << str3 << endl;
 
   // 连接 str1 和 str2
   strcat( str1, str2);
   cout << "strcat( str1, str2): " << str1 << endl;
 
   // 连接后,str1 的总长度
   len = strlen(str1);
   cout << "strlen(str1) : " << len << endl;


    char s1[] = "My name is Mini.";
    strlwr(s1);
    cout<<"字符串中的大写字母转为小写字母后:"<<s1<<endl;

    char s1[] = "My name is Mini.";
    strupr(s1);
    cout<<"字符串中的小写字母转为大写字母后:"<<s1<<endl;

    char s1[] = "abcdefghijklmnopqrstuvwxyz";
    strrev(s1);
    cout<<"字符串导致后:"<<s1<<endl;

    char s1[] = "My name is Mini.";
    char dest='c';
    strset(s1, dest);
    cout<<"把字符串s中的所有字符都设置成字符c:"<<s1<<endl;
 
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

    strcpy( str3, str1) : runoob
    strcat( str1, str2): runoobgoogle
    strlen(str1) : 12



C++ 中的 String 类

C++ 标准库提供了 string 类类型,支持上述所有的操作,另外还增加了其他更多的功能。

先来看看下面这个实例:
#include <iostream>
#include <string>
using namespace std;
 
int main ()
{
   string str1 = "runoob";   // char str1[] = "runoob"  -- \0
   string str2 = "google";
   string str3;
   int  len ;
 
   // 复制 str1 到 str3
   str3 = str1;
   cout << "str3 : " << str3 << endl;
 
   // 连接 str1 和 str2
   str3 = str1 + str2;
   cout << "str1 + str2 : " << str3 << endl;
 
   // 连接后,str3 的总长度
   len = str3.size();
   cout << "str3.size() :  " << len << endl;
 
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:
    str3 : runoob
    str1 + str2 : runoobgoogle
    str3.size() :  12


使用 string 类需要包含头文件<string>,下面的例子介绍了几种定义 string 变量(对象)的方法:

#include <iostream>
#include <string>
using namespace std;

int main(){
    string s1;
    string s2 = "c plus plus";
    string s3 = s2;
    string s4 (5, 's');
    return 0;
}

    变量 s1 只是定义但没有初始化,编译器会将默认值赋给 s1,默认值是"",也即空字符串。
    变量 s2 在定义的同时被初始化为"c plus plus"。与C风格的字符串不同,string 的结尾没有结束标志'\0'。
    变量 s3 在定义的时候直接用 s2 进行初始化,因此 s3 的内容也是"c plus plus"。
    变量 s4 被初始化为由 5 个's'字符组成的字符串,也就是"sssss"。

    从上面的代码可以看出,string 变量可以直接通过 赋值操作符= 进行赋值。
    string 变量也可以用C风格的字符串进行赋值,例如,s2 是用一个字符串常量进行初始化的,而 s3 则是通过 s2 变量进行初始化的。

与C风格的字符串不同,当我们需要知道字符串长度时,可以调用 string 类提供的 length() 函数。
如下所示:

    string s = "http://c.biancheng.net";
    int len = s.length();   //  同意范围内 变量不允许重复定义
    cout<<len<<endl;

    输出结果为22。
由于 string 的末尾没有'\0'字符,所以 length() 返回的是字符串的真实长度,而不是长度 +1。

/*
转换为C风格的字符串
虽然 C++ 提供了 string 类来替代C语言中的字符串,但是在实际编程中,有时候必须要使用C风格的字符串
(例如打开文件时的路径),为此,string 类为我们提供了一个转换函数 c_str(),该函数能够将 string 
字符串转换为C风格的字符串,并返回该字符串的 const 指针(const char*)。请看下面的代码:

string path = "D:\\demo.txt";
FILE *fp = fopen(path.c_str(), "rt");

为了使用C语言中的 fopen() 函数打开文件,必须将 string 字符串转换为C风格的字符串。
*/

##  string 字符串的输入输出
    string 类重载了输入输出运算符,可以像对待普通变量那样对待 string 变量,也就是用>>进行输入,用<<进行输出。请看下面的代码:

    #include <iostream>
    #include <string>

    using namespace std;

    int main(){
        string s;
        cin>>s;  //输入字符串
        cout<<s<<endl;  //输出字符串
        return 0;
    }

    运行结果:
    http://c.biancheng.net  http://vip.biancheng.net↙
    http://c.biancheng.net


    虽然我们输入了两个由空格隔开的网址,但是只输出了一个,这是因为输入运算符>>默认会忽略空格,遇到空格就认为输入结束,
所以最后输入的 http://vip.biancheng.net  没有被存储到变量 s。

##  访问字符串中的字符
    string 字符串也可以像C风格的字符串一样按照下标来访问其中的每一个字符。
    string 字符串的起始下标仍是从 0 开始。请看下面的代码:

    #include <iostream>
    #include <string>
    using namespace std;

    int main(){
        string s = "1234567890";
        for(int i=0,len=s.length(); i<len; i++){
            cout<<s[i]<<" ";
        }
        cout<<endl;
        s[5] = '5';
        cout<<s<<endl;
        return 0;
    }

    运行结果:
    1 2 3 4 5 6 7 8 9 0
    1234557890

    本例定义了一个 string 变量 s,并赋值 "1234567890",之后用 for 循环遍历输出每一个字符。
    借助下标,除了能够访问每个字符,也可以修改每个字符,s[5] = '5';就将第6个字符修改为 '5',所以 s 最后为 "1234557890"。


##  字符串的拼接
    有了 string 类,我们可以使用+或+=运算符来直接拼接字符串,非常方便,
再也不需要使用C语言中的 strcat()、strcpy()、malloc() 等函数来拼接字符串了,再也不用担心空间不够会溢出了。
    用 + 来拼接字符串时,运算符的两边可以都是 string 字符串,也可以是一个 string 字符串和一个C风格的字符串,
还可以是一个 string 字符串和一个字符数组,或者是一个 string 字符串和一个单独的字符。请看下面的例子:

    #include <iostream>
    #include <string>
    using namespace std;

    int main(){
        string s1 = "first ";
        string s2 = "second ";
        char *s3 = "third ";
        char s4[] = "fourth ";
        char ch = '@';

        string s5 = s1 + s2;
        string s6 = s1 + s3;
        string s7 = s1 + s4;
        string s8 = s1 + ch;
        
        cout<<s5<<endl<<s6<<endl<<s7<<endl<<s8<<endl;

        return 0;
    }

    运行结果:
    first second
    first third
    first fourth
    first @


### string 字符串的增删改查
    C++ 提供的 string 类包含了若干实用的成员函数,大大方便了字符串的增加、删除、更改、查询等操作。

一. 插入字符串
    insert() 函数可以在 string 字符串中指定的位置插入另一个字符串,它的一种原型为:
    string& insert (size_t pos, const string& str);
        pos 表示要插入的位置,也就是下标;
        str 表示要插入的字符串,它可以是 string 字符串,也可以是C风格的字符串。

    请看下面的代码:
    #include <iostream>
    #include <string>
    using namespace std;

    int main(){
        string s1, s2, s3;
        s1 = s2 = "1234567890";
        s3 = "aaa";
        s1.insert(5, s3);
        cout<< s1 <<endl;
        s2.insert(5, "bbb");
        cout<< s2 <<endl;
        return 0;
    }

    运行结果:
    12345aaa67890
    12345bbb67890

    insert() 函数的第一个参数有越界的可能,如果越界,则会产生运行时异常.


二. 删除字符串
    erase() 函数可以删除 string 中的一个子字符串。它的一种原型为:
    string& erase (size_t pos = 0, size_t len = npos);
        pos 表示要删除的子字符串的起始下标,
        len 表示要删除子字符串的长度。如果不指明 len 的话,那么直接删除从 pos 到
    字符串结束处的所有字符(此时 len = str.length - pos)。

请看下面的代码:
    #include <iostream>
    #include <string>
    using namespace std;

    int main(){
        string s1, s2, s3;
        s1 = s2 = s3 = "1234567890";
        s2.erase(5);
        s3.erase(5, 3);
        cout<< s1 <<endl;
        cout<< s2 <<endl;
        cout<< s3 <<endl;
        return 0;
    }

    运行结果:
    1234567890
    12345
    1234590

    在 pos 参数没有越界的情况下, len 参数也可能会导致要删除的子字符串越界。但实际上这种情况不会发生,
erase() 函数会从以下两个值中取出最小的一个作为待删除子字符串的长度:
    1)  len 的值;
    2)  字符串长度减去 pos 的值。

    说得简单一些,待删除字符串最多只能删除到字符串结尾。

三. 提取子字符串
    substr() 函数用于从 string 字符串中提取子字符串,它的原型为:
    string substr (size_t pos = 0, size_t len = npos) const;
        pos 为要提取的子字符串的起始下标,
        len 为要提取的子字符串的长度。

    请看下面的代码:
    #include <iostream>
    #include <string>
    using namespace std;

    int main(){
        string s1 = "first second third";
        string s2;
        s2 = s1.substr(6, 6);
        cout<< s1 <<endl;
        cout<< s2 <<endl;
        return 0;
    }

    运行结果:
    first second third
    second

系统对 substr() 参数的处理和 erase() 类似:
    1)  如果 pos 越界,会抛出异常;
    2)  如果 len 越界,会提取从 pos 到字符串结尾处的所有字符

四. 字符串查找
    string 类提供了几个与字符串查找有关的函数,如下所示。
    1) find() 函数
    find() 函数用于在 string 字符串中查找子字符串出现的位置,它其中的两种原型为:
        size_t find (const string& str, size_t pos = 0) const;
        size_t find (const char* s, size_t pos = 0) const;

        第一个参数为待查找的子字符串,它可以是 string 字符串,也可以是C风格的字符串。
        第二个参数为开始查找的位置(下标);如果不指明,则从第0个字符开始查找。

    请看下面的代码:
    #include <iostream>
    #include <string>
    using namespace std;

    int main(){
        string s1 = "first second third";
        string s2 = "second";
        int index = s1.find(s2,5);
        if(index < s1.length())
            cout<<"Found at index : "<< index <<endl;
        else
            cout<<"Not found"<<endl;
        return 0;
    }

    运行结果:
    Found at index : 6

    find() 函数最终返回的是子字符串第一次出现在字符串中的起始下标。本例最终是在下标6处找到了 s2 字符串。
    如果没有查找到子字符串,那么会返回一个无穷大值 4294967295。

    2) rfind() 函数 
    rfind() 和 find() 很类似,同样是在字符串中查找子字符串,不同的是 find() 函数从第二个参数开始往后查找,
而 rfind() 函数则最多查找到第二个参数处,如果到了第二个参数所指定的下标还没有找到子字符串,则返回一个无
穷大值4294967295。

请看下面的例子:
    #include <iostream>
    #include <string>
    using namespace std;

    int main(){
        string s1 = "first second third";
        string s2 = "second";
        int index = s1.rfind(s2,6);
        if(index < s1.length())
            cout<<"Found at index : "<< index <<endl;
        else
            cout<<"Not found"<<endl;
        return 0;
    }

    运行结果:
    Found at index : 6

3) find_first_of() 函数   (函数概念模糊)
    find_first_of() 函数用于查找 子字符串和字符串 共同具有的字符 在字符串中首次出现的位置。

请看下面的代码:
    #include <iostream>
    #include <string>
    using namespace std;

    int main(){
        string s1 = "first second second third";
        string s2 = "asecond";
        int index = s1.find_first_of(s2);
        if(index < s1.length())
            cout<<"Found at index : "<< index <<endl;
        else
            cout<<"Not found"<<endl;
        return 0;
    }

    运行结果:
    Found at index : 3

    本例中 s1 和 s2 共同具有的字符是 ’s’,该字符在 s1 中首次出现的下标是3,故查找结果返回3。


补充 :

void *memset(void *s, int v, size_t n);
    s可以是数组名,也可以是指向某一内在空间的指针;
    v为要填充的值;
    n为要填充的字节数;

// memset将dest的全部元素用null填充 , 
memset(dest,0,7)    7为从dest起始地址开始前7个位置填充null
 
参考  菜鸟教材 , C编程