.NET一个字符串到底分配了多少内存?

string abc = "aaa"+"bbb"+"ccc";

这是一个经典的基础知识题目,它涉及了字符串的类型、堆栈和堆的内存分配机制,因此被很多人拿来考核开发者的基础知识功底。.

首先,我们都知道,判断值类型的标准是查看该类型是否会继承自System.ValueType,通过查看和分析,string直接继承于System.Object,因此string是引用类型,其内存分配会遵照引用类型的规范,也就是说如下的代码将会在堆栈上分配一块存储引用的内存,然后再在堆上分配一块存储字符串实例对象的内存。

string a = "edison";

现在再来看看string abc="aaa"+"bbb"+"ccc"。

按照常规的思路,字符串具有不可变性,大部分人会认为这里的表达式会涉及很多临时变量的生成,可能C#编译器会先执行"aaa"+"bbb",并且把结果值赋给一个临时变量,再执行临时变量和"ccc"相加,最后把相加的结果再赋值给abc。

But,其实C#编译器比想象中要聪明得多,以下的C#代码和IL代码可以充分说明C#编译器的智能:

// The first format
string first = "aaa" + "bbb" + "ccc";
// The second format
string second = "aaabbbccc";
// Display string 
Console.WriteLine(first);
Console.WriteLine(second);

该C#代码的IL代码如下图所示:

.NET一个字符串到底分配了多少内存?

正如我们所看到的,string abc="aaa"+"bbb"+"ccc"; 这样的表达式被C#编译器看成一个完整的字符串"aaabbbccc",而不是执行某些拼接方法,可以将其看作是C#编译器的优化,所以在本次内存分配中只是在栈中分配了一个存储字符串引用的内存块,以及在托管堆分配了一块存储"aaabbbccc"字符串对象的内存块。

那么,我们的常规思路在.NET程序中又是怎么体现的呢?我们来看一下一段代码:

int num = 1;
string str = "aaa" + num.ToString();
Console.WriteLine(str);

这里我们首先初始化了一个int类型的变量,其次初始化了一个string类型的字符串,并执行“+”操作,这时我们来看看其对应的IL代码:

.NET一个字符串到底分配了多少内存?

如上图所示,在这段代码中执行“+”操作,会调用String的Concat方法,该方法需要传入两个string类型的参数,也就产生了另一个string类型的临时变量。换句话说,在此次内存分配中,堆栈中会分配一个存储字符串引用的内存块,在托管堆则分配了两块内存块,分别存储了存储"aaa"字符串对象和"1"字符串对象。

可能这段代码还是不熟悉,我们再来看看下面一段代码,我们就感觉十分亲切熟悉了:

string str = "aaa";
str += "bbb";
str += "ccc";
Console.WriteLine(str);

其对应的IL代码如下图所示:

.NET一个字符串到底分配了多少内存?

如图可以看出,在拼接过程中产生了两个临时字符串对象,并调用了两次String.Concat方法进行拼接,就不用多解释了。