【编程语言】C#中字符串的内存分配

C++中的共享内存

作为一枚C++程序员,了解到在早期的版本中STL为了提高性能,在std::string中曾一度使用过共享内存的技术,在目前的版本中string已经不支持共享内存,其中一个原因是由于线程不安全。
有关copy-on-write和std::string的共享内存,陈皓大大已经有一篇很详尽的文章介绍过:
C++ STL STRING的COPY-ON-WRITE技术

本着负责任的态度,还是编写了如下测试代码,确认std::string当前对于共享内存的态度:

#include <iostream>
#include <string>

using namespace std;

int main()
{
    string str1 = "hello world";
    //1.用字面量构造str2
    string str2 = "hello world";

    //2.用str1构造str2,通过调用拷贝构造函数
    //string str2 = str1; //调用的是拷贝构造函数 等价于 string str2(str1);

    //3.通过赋值运算构造str2
    //string str2; //调用参数默认为空串的构造函数:string str2(“”);
    //str2 = str1; //调用str2的赋值操作:str2.operator=(str1);

    printf("Test std::string sharing memory: \n");
    printf("str1's address: %x\n", str1.c_str());
    printf("str2's address: %x\n", str2.c_str());

    getchar();
    return 0;
}

《【编程语言】C#中字符串的内存分配》

C#中的string分配在哪里?

要搞清楚C#中string的内存分配,首先要知道C#的string分配在哪里。string是引用类型的,而在C#引用类型的内存是分配在托管堆上的。尽管C#的string是引用类型的,但是编译器并不允许使用new根据一个文本变量来创建一个字符串变量,必须使用简明的声明语法来声明或是初始化。

        string str1 = "hello";
        string str2 = new string(str1);

这样的声明是不行的。

字符串的驻留

首先要明确一点,C#的string是一个不可变的(immutable)字符序列,所有对于字符串的操作都会返回一个新的字符串看,而原字符串则不受影响。
在实际的项目使用中,不可避免的会有大量的字符操作,这样会带来大量的字符串创建,内存分配,很有可能会引起垃圾回收,这样势必会影响到性能。CLR对于这样的情况,拥有自己的优化机制。类型本文一开始介绍的C++的内存共享技术,CLR中存在着一种叫做“驻留池”的概念。

使用字面量声明字符串时:

        string str1 = "hello world";
        string str2 = "hello world";
        Console.WriteLine(object.ReferenceEquals(str1, str2)); //True

使用字面量声明字符串时,驻留池可以保证在一个进程内的某个字符串只在内存中分配一次。

驻留池

参考文章:C#基础知识梳理系列七:字符串

明明声明了两个对象str1和str2,调用object.ReferenceEquals方法返回的是True,为什么它们指向的是同一个引用呢?这时由于相同的字符串在托管内存中只分配一次,再次通过字面量(literal string)声明相同的字符串对象时,会将后来一次的声明指向第一次声明所引用的对象。

那么CLR 如何保证做到的呢?在CLR初始化时创建一个内部的哈希表,这个表相当于一个字典表

Dictionary <TKey,TValue>

键就是字符串,值是指向托管堆中该字符串对象的引用。

不使用字面量声明的字符串:
原来是这样:C#中字符串的内存分配与驻留池

StringBuilder sb = new StringBuilder();
sb.Append("He").Append("llo");

string str1 = "Hello";
string str2 = sb.ToString();

Console.WriteLine(object.ReferenceEquals(str1, str2)); //False

调用object.ReferenceEquals方法返回的是False。

因为虽然str1,str2表示的是相同的字符串,但是由于str2不是通过字面量声明的,CLR在为sb.ToString()方法的返回值分配内存时,并不会到驻留池中去检查是否有值为“Hello”的字符串已经存在了,所以自然不会让s2指向驻留池内的对象。

显示的调用Intern:

StringBuilder sb = new StringBuilder();
sb.Append("He").Append("llo");

string str1 = "Hello";
string str2 = String.Intern(sb.ToString());

Console.WriteLine(object.ReferenceEquals(str1, str2)); //True

调用object.ReferenceEquals方法返回的是True。

Intern方法接受一个字符串作为参数,它会在驻留池中检查是否存在参数所表示的字符串。如果存在,则返回驻留池中找到的字符串的引用;否则向驻留池中加入一个新的表示相同值的字符串,并返回这个字符串的引用。

String.Intern Method (String) From MSDN

在MSDN上关于intern的使用,String.Intern Method (String)有个很容易混淆的例子:

The common language runtime conserves string storage by maintaining a table, called the intern pool, that contains a single reference to each unique literal string declared or created programmatically in your program. Consequently, an instance of a literal string with a particular value only exists once in the system.
For example, if you assign the same literal string to several variables, the runtime retrieves the same reference to the literal string from the intern pool and assigns it to each variable.
The Intern method uses the intern pool to search for a string equal to the value of str. If such a string exists, its reference in the intern pool is returned. If the string does not exist, a reference to str is added to the intern pool, then that reference is returned.
In the following example, the string s1, which has a value of “MyTest”, is already interned because it is a literal in the program. The System.Text.StringBuilder class generates a new string object that has the same value as s1. A reference to that string is assigned to s2. The Intern method searches for a string that has the same value as s2. Because such a string exists, the method returns the same reference that is assigned to s1. That reference is then assigned to s3. References s1 and s2 compare unequal because they refer to different objects; references s1 and s3 compare equal because they refer to the same string.

string s1 = "MyTest"; 
string s2 = new StringBuilder().Append("My").Append("Test").ToString(); 
string s3 = String.Intern(s2); 
Console.WriteLine((Object)s2==(Object)s1); // Different references.
Console.WriteLine((Object)s3==(Object)s1); // The same reference.

总结一下:

s1利用字面量声明(literal string),所以s1会进入到驻留池(intern pool);

s2利用StringBuilder来构建字符串,其内容s1一样,却没有保存在驻留池中;

String.Instern(s2)利用s2的内容在驻留池中进行查找,会返回s1在驻留池中的地址;

测试结果:

最后是我做的各种string操作测试,结果是打印在Unity中的,可以修改为Console,市面上应该找不到比我更完整的了吧:

    private void Test()
    {
        char[] tmp = "hello world".ToCharArray();
        //【共享内存】
        //利用字面量构造
        string str1 = "hello world";
        string str2 = "hello world";
        //Console.WriteLine(object.ReferenceEquals(str1, str2)); //True
        Debug.Log("利用字面量构造:");
        PrintLogs(str1, str2);

        //利用string(char* value)来构造
        str1 = new string(tmp);
        str2 = str1;
        Debug.Log("利用string(char* value)来构造:");
        PrintLogs(str1, str2);


        //【不共享内存】
        //ToString会重新分配内存,不会检查驻留池
        str1 = tmp.ToString();
        str2 = tmp.ToString();
        Debug.Log("ToString会重新分配内存,不会检查驻留池:");
        PrintLogs(str1, str2);
        //利用new分配新的内存
        str1 = new string(tmp);
        str2 = new string(tmp);
        Debug.Log("利用new分配新的内存:");
        PrintLogs(str1, str2);
        //换一种写法
        //str1 = new string(tmp);
        //str2 = new string(str1.ToCharArray());


        //【StringBuilder】
        StringBuilder sb = new StringBuilder();
        sb.Append("hello").Append(" world");
        //直接利用StringBuilder构造,不会共享
        str1 = "hello world";
        str2 = sb.ToString();
        Debug.Log("直接利用StringBuilder构造:");
        PrintLogs(str1, str2);
        //查询驻留池
        str1 = "hello world";
        str2 = string.Intern(sb.ToString());
        Debug.Log("StringBuilder 查询驻留池:");
        PrintLogs(str1, str2);
    }

    private void PrintLogs(string str1, string str2)
    {
        unsafe
        {
            fixed (char* p1 = str1)
            {
                Debug.Log("str1: " + Convert.ToString((int)p1, 16));
            }
            fixed (char* p2 = str2)
            {
                Debug.Log("str2: " + Convert.ToString((int)p2, 16));
            }
        }
    }

《【编程语言】C#中字符串的内存分配》

    原文作者:coffeecato
    原文地址: https://blog.csdn.net/u010832643/article/details/74248571
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞