在C和C++中,字符串的创建本质上是 为一串字符分配内存。 内存的来源主要有四个地方:

  1. 静态存储区:用于存放全局变量、静态变量和字符串常量。这部分内存在程序编译的时候就已经分配好了,程序启动后加载,结束后释放,也就是整个运行期间都存在。
  2. 栈:用于存放函数的参数值、局部变量等。由编译器自动分配和释放,在函数创建时创建,函数返回时销毁。
  3. 堆:用于程序动态分配的内存,可以通过 malloc/free 或 new/delete 来管理。
  4. std::string:C++的抽象,底层会利用 栈 和 堆 来管理字符数据。

下面我们来分析对于这些不同内存来源的字符串的区别。

1. C风格字符串 char* or char[]

1.1 字符串字面量

void func(){
	const char* str = "xxx"; // str是一个指向只读内存区域的指针
}

内存占用: 位于静态存储区的只读数据段,在编译时被确定、在程序启动时被加载,直到程序结束才会被释放。无论函数调用多少次,这个字符串常量在内存中都只有独一份。 如果你创建了一个占用空间100k的字符串字面量,那这100k的内存占用会持续整个程序的生命周期。这就意味着,如果程序对运行时内存占用敏感,却对性能要求不高,字符串字面量可能不是最优解。

性能: 性能极高,没有任何运行时的内存分配开销。

限制: 是只读数据,无法在程序中修改字符串内容(因此最好使用const char*来指向它)。

生命周期: 静态生命周期,贯穿整个程序。

适用场景: 函数中不会被修改的、固定的字符串。

1.2 栈上数组

在函数内部定义的局部字符数组,和其他函数局部数据一样会被存储到栈上。

void func(){
	char str[] = "xxx"; // 编译器会自动计算长度
	// or
	char buffer[32];
	strcpy(buffer, "xxx");
}

内存占用: 占用栈空间,当函数执行时,栈指针移动为数组分配空间;当函数返回值,栈指针移动回收空间。 需要注意的是,如果数组过大,可能会导致 栈溢出。

性能: 非常高,因为栈的分配和回收只需要移动栈指针,开销极小。

限制: 和函数执行一样的生命周期,函数进入前被创建,函数返回后被销毁。 所以绝对不能返回一个指向栈上数组的指针!

适用场景: 需要一个临时的、大小可以预估(不会太大)的字符串缓冲区时。

1.3 堆上分配

使用 malloc (C) 或 new[] (C++) 在堆上分配内存。

void func(){
	// C Style
	char* str = (char*)malloc(sizeof(char) * 4);
	if(str){
		strcpy(str, "xxx"); // 注意:这个"xxx"是被存储在静态存储区的,也就是程序启动的时候就占用了4Byte
		free(str);
	}

	// Cpp Style
	char* str = new char[4];
	strcpy(str, "xxx");
	delete[] str;
}

内存占用: 占用堆内存。堆的可用空间较大,适合保存超长的字符串。

性能: 较低,因为堆内存的分配开销很大,需要执行复杂的算法,没有栈空间那样只需要移动一个指针简单,此外还需要处理内存碎片问题。 释放操作 free / delete 同样有开销。

限制: 需要手动使用 malloc/new & free/delete 手动管理生命周期,这导致了更高的出错概率(比如内存泄漏或重复释放)。

适用场景: 字符串大小较大或者在运行时才能确定 or 字符串需要跨越函数(比如作为返回值)。

1.4 静态局部变量

使用 static 修饰的局部变量,存储在静态存储区。

void func(){
	static char str[] = "xxx"; // 首次调用函数被初始化
	str[0] = 'y'; // 可修改
}

内存占用: 占用静态存储区的.data或.bss段(这里和字面量不同,字面量是只读数据段)。 -> 静态存储区的内容整体遵循 “程序启动或首次使用时加载,程序结束时释放” 的规则

性能: 非常高,首次调用有初始化开销,后续调用没有任何内存分配开销。

限制: 如果多线程同时调用 func() 并修改 str,可能会产生数据竞争,需要加锁保护。

适用场景: 函数需要一个可以持久保存的、可修改的字符串时。

2. std::string

在C++中,多数情况下 std::string 是首选。

std::string func() {
	std::string str = "xxx";
	str += "yyy"; // 可以实现拼接等功能
	return str; // 可以安全返回
}

内存占用: std::string 对象本身可以存在 栈、堆或静态区,具体取决于如何被定义。它管理的字符数据通常在堆上。 -> 短字符串优化:许多std::string 实现会进行这个优化,如果字符串很短,字符数据会直接存储在std::string对象内部,从而避免了堆分配。这使得std::string 在处理大量短字符串时,性能可以媲美甚至超过C风格的栈上数组。 -> 如果因为频繁的分配遇到了性能瓶颈,可以使用 std::string::reserve() 提前预留空间,避免多次堆分配。

性能: 创建性能:对于短字符串,因为有优化,性能极高。对于长字符串,因为涉及堆分配,性能与 malloc/new 类似,但经过高度优化。 操作性能:各种操作都有优化,但如果字符串长度增加导致内容不足时,会触发重新分配,会有分配+复制开销(一般是空间*2)。

限制:无

简要总结

字面量

const char* str = “xxx”; 静态变量区中的只读数据段,程序启动就加载,程序退出才释放,性能极高,但无法修改。

栈上数组

char[] str = “xxx”; 保存在栈上,进入函数时分配内存,分配内存性能极高,可修改,退出函数后会释放。

malloc/free & new/delete

char* str = malloc(sizeof(char) * 3) free(str);

char* str = new char[4]; delete[] str;

内存在堆上分配,分配性能消耗较高,但是灵活,可用空间大,可修改,分配释放由程序员手动控制。

std::string

std::string str = “xxx”; str += “yyy”; 性能高、易用性强、大多数情况下是最优解,遵循RAII。