剑指Offer
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.3.2 字符串

字符串是由若干字符组成的序列。由于字符串在编程时使用的频率非常高,为了优化,很多语言都对字符串做了特殊的规定。下面分别讨论C/C++和C#中字符串的特性。

C/C++ 中每个字符串都以字符'\0'作为结尾,这样我们就能很方便地找到字符串的最后尾部。但由于这个特点,每个字符串中都有一个额外字符的开销,稍不留神就会造成字符串的越界。比如下面的代码:

    char str[10];
    strcpy(str, "0123456789");

我们先声明一个长度为10的字符数组,然后把字符串"0123456789"复制到数组中。"0123456789"这个字符串看起来只有10个字符,但实际上它的末尾还有一个'\0'字符,因此它的实际长度为11个字节。要正确地复制该字符串,至少需要一个长度为11个字节的数组。

为了节省内存,C/C++把常量字符串放到单独的一个内存区域。当几个指针赋值给相同的常量字符串时,它们实际上会指向相同的内存地址。但用常量内存初始化数组,情况却有所不同。下面通过一个面试题来学习这一知识点。运行下面的代码,得到的结果是什么?

    int _tmain(int argc, _TCHAR* argv[])
    {
        char str1[] = "hello world";
        char str2[] = "hello world";

        char* str3 = "hello world";
        char* str4 = "hello world";

        if(str1 == str2)
            printf("str1 and str2 are same.\n");
        else
            printf("str1 and str2 are not same.\n");

        if(str3 == str4)
            printf("str3 and str4 are same.\n");
        else
            printf("str3 and str4 are not same.\n");

        return 0;
    }

str1和str2是两个字符串数组,我们会为它们分配两个长度为12个字节的空间,并把"hello world"的内容分别复制到数组中去。这是两个初始地址不同的数组,因此str1和str2的值也不相同,所以输出的第一行是”str1and str2are not same”。

str3和str4是两个指针,我们无须为它们分配内存以存储字符串的内容,而只需要把它们指向"hello world”在内存中的地址就可以了。由于"hello world”是常量字符串,它在内存中只有一个拷贝,因此str3和str4指向的是同一个地址。所以比较str3 和str4 的值得到的结果是相同的,输出的第二行是”str3and str4are same”。

在 C#中,封装字符串的类型 System.String 有一个非常特殊的性质:String中的内容是不能改变的。一旦试图改变String的内容,就会产生一个新的实例。请看下面的C#代码:

    String str = "hello";
    str.ToUpper();
    str.Insert(0, " WORLD");

虽然我们对str做了ToUpper和Insert两个操作,但操作的结果都是生成一个新的String实例并在返回值中返回,str本身的内容都不会发生改变,因此最终str的值仍然是"hello"。由此可见,如果试图改变String的内容,改变之后的值只可以通过返回值得到。用 String 作连续多次修改,每一次修改都会产生一个临时对象,这样开销太大会影响效率。为此C#定义了一个新的与字符串相关的类型StringBuilder,它能容纳修改后的结果。因此如果要连续多次修改字符串内容,用StringBuilder是更好的选择。

和修改 String 内容类似,如果我们试图把一个常量字符串赋值给一个String实例,也不是把String的内容改成赋值的字符串,而是生成一个新的String实例。请看下面的代码:

    class Program
    {
        internal static void ValueOrReference(Type type)
        {
            String result = "The type " + type.Name;
            if (type.IsValueType)
                Console.WriteLine(result + " is a value type.");
            else
                Console.WriteLine(result + " is a reference type.");
        }
        internal static void ModifyString(String text)
        {
            text = "world";
        }

        static void Main(string[] args)
        {
            String text = "hello";

            ValueOrReference(text.GetType());
            ModifyString(text);

            Console.WriteLine(text);
        }
    }

在上面的代码中,我们先判断String是值类型还是引用类型。类型String的定义是public sealed class String{...}。既然是class,那么String自然就是引用类型。接下来在方法 ModifyString 里,对 text 赋值一个新的字符串。我们要记得 text 的内容是不能被修改的。此时会先生成一个新的内容是"world"的String实例,然后把text指向这个新的实例。由于参数text没有加ref或者out,出了方法ModifyString之后,text还是指向原来的字符串,因此输出仍然是"hello"。要想实现出了函数之后 text 变成"world"的效果,我们必须把参数text标记ref或者out。