星驰编程网

免费编程资源分享平台_编程教程_代码示例_开发技术文章

从深拷贝到 COW:C++ 字符串复制的内存栈堆变化明细

C++ 字符串复制:栈堆之间的 "数据搬家" 大戏

一、字符串复制就像 "搬家":栈是地址牌,堆是仓库

想象你有一个仓库(堆)里放着一箱子文件(字符串数据),箱子上有个地址牌(栈上的指针)。复制字符串就像搬家:



  • 深拷贝:雇个搬家公司,把箱子里的文件全抄一份,放进新仓库,再做个新地址牌。
  • 写时复制:先共用一个仓库,地址牌指向同一个地方,等有人要改文件了再抄一份。



C++ 里复制字符串时,栈和堆的变化就像这场搬家大戏,每个步骤都藏着内存的小秘密。今天咱们就当回 "内存侦探",一步步拆解!

二、主角登场:std::string 的 "复制工具箱"

C++ 的std::string是个 "智能箱子",里面藏着三个关键信息(都在栈上):



  • 指向堆内存的指针(_M_p):仓库地址
  • 字符串长度(_M_len):箱子里有多少文件
  • 容量(_M_cap):箱子能装多少文件



复制时,这三个信息和堆里的 "文件" 都会发生变化,咱们逐个看。

案例 1:深拷贝(现代编译器主流)—— 彻底的搬家

代码(string_copy.cpp):



cpp

运行

#include <iostream>
#include <string>

int main() {
    // 原始字符串:栈上有地址牌,堆里有数据
    std::string src = "hello";
    std::cout << "原始字符串(src):\n";
    std::cout << "  栈地址:" << &src << "\n";
    std::cout << "  堆地址(数据):" << static_cast<void*>(src.data()) << "\n";
    std::cout << "  长度:" << src.size() << ", 容量:" << src.capacity() << "\n\n";

    // 拷贝构造:深拷贝
    std::string dest = src;  // 核心操作
    std::cout << "复制后(dest):\n";
    std::cout << "  栈地址:" << &dest << "\n";  // 新栈地址
    std::cout << "  堆地址(数据):" << static_cast<void*>(dest.data()) << "\n";  // 新堆地址
    std::cout << "  长度:" << dest.size() << ", 容量:" << dest.capacity() << "\n";

    return 0;
}



编译运行



bash

g++ string_copy.cpp -o demo && ./demo  # Linux/macOS
# Windows: g++ string_copy.cpp -o demo.exe && demo.exe



输出



plaintext

原始字符串(src):
  栈地址:0x7ffd9a5b9a70
  堆地址(数据):0x55f8d9a52b60
  长度:5, 容量:5

复制后(dest):
  栈地址:0x7ffd9a5b9a90  # 栈上新建了string对象
  堆地址(数据):0x55f8d9a52ba0  # 堆上新建了数据
  长度:5, 容量:5



内存变化 step by step



  1. 原始字符串(src):栈:src 占一块内存(比如 0x7ffd9a5b9a70),里面存三个值:指针:0x55f8d9a52b60(指向堆)长度:5("hello" 有 5 个字符)容量:5(刚好装下)堆:0x55f8d9a52b60 处存着 h e l l o \0(6 字节,含结束符)。
  2. 复制过程(dest = src):栈:新建 dest 对象(地址 0x7ffd9a5b9a90),复制 src 的长度和容量(都是 5)。堆:新分配一块内存(0x55f8d9a52ba0),把 src 堆里的 h e l l o \0 完整复制过来。栈上的 dest 指针更新为新堆地址 0x55f8d9a52ba0。
  3. 最终状态:栈上两个独立对象,堆上两份相同数据 —— 改 dest 完全不影响 src(就像两家各有一箱子相同文件)。

案例 2:写时复制(COW,旧编译器)—— 先合租再分家

老版本 GCC(比如 4.8)会用 "写时复制" 优化,刚开始不真复制,等修改时才动手。



代码



cpp

运行

#include <iostream>
#include <string>

int main() {
    std::string src = "shared";
    std::string dest = src;  // 先不真复制

    std::cout << "复制后(未修改):\n";
    std::cout << "  src堆地址:" << static_cast<void*>(src.data()) << "\n";
    std::cout << "  dest堆地址:" << static_cast<void*>(dest.data()) << "\n";  // 地址相同

    // 修改dest,触发真正复制
    dest += " modified";
    std::cout << "\n修改dest后:\n";
    std::cout << "  src堆地址:" << static_cast<void*>(src.data()) << "\n";
    std::cout << "  dest堆地址:" << static_cast<void*>(dest.data()) << "\n";  // 地址不同

    return 0;
}



输出(旧编译器):



plaintext

复制后(未修改):
  src堆地址:0x55f8d9a52b60
  dest堆地址:0x55f8d9a52b60  # 共用堆内存

修改dest后:
  src堆地址:0x55f8d9a52b60
  dest堆地址:0x55f8d9a52ba0  # 分家了,dest有了新堆内存



内存变化 step by step



  1. 复制后(未修改):栈:src 和 dest 是两个对象,但指针都指向同一堆地址 0x55f8d9a52b60。堆:数据只有一份,但多了个 "引用计数"(记录有 2 个对象在用)。
  2. 修改 dest 时:堆:检测到引用计数 > 1,给 dest 新分配堆内存 0x55f8d9a52ba0,复制原数据并添加 " modified"。栈:dest 的指针更新为新堆地址,引用计数减为 1(只剩 src 在用旧地址)。



就像两个人先合租一个仓库,其中一个要重新装修时,就自己租个新仓库并复制东西 —— 前期省空间,后期不打扰。

案例 3:C 风格字符串复制(strcpy)—— 手动搬家

C 语言的strcpy更原始,全程手动操作,栈堆变化更 "赤裸"。



代码



cpp

运行

#include <iostream>
#include <cstring>

int main() {
    // 源字符串(常量区)
    const char* src = "manual copy";
    std::cout << "源字符串(常量区)地址:" << static_cast<void*>(const_cast<char*>(src)) << "\n";

    // 栈上的目标指针(还没指向有效内存)
    char* dest;

    // 1. 计算长度(确定要搬多少东西)
    size_t len = std::strlen(src);
    std::cout << "字符串长度:" << len << "字节\n";

    // 2. 堆上分配内存(租新仓库)
    dest = new char[len + 1];  // +1留结束符'\0'
    std::cout << "目标堆地址:" << static_cast<void*>(dest) << "\n";

    // 3. 复制数据(手动搬家)
    std::strcpy(dest, src);
    std::cout << "复制后内容:" << dest << "\n";

    // 4. 手动释放(退租仓库)
    delete[] dest;

    return 0;
}



输出



plaintext

源字符串(常量区)地址:0x55f8d9a3e0c0
字符串长度:10字节
目标堆地址:0x55f8d9a52b60
复制后内容:manual copy



内存变化 step by step



  1. 源字符串:常量区:"manual copy\0" 存在只读内存(0x55f8d9a3e0c0)。栈:src 指针指向这个地址。
  2. 复制过程:栈:dest 指针先无意义,计算长度 len=10 后,new char[11] 分配堆内存 0x55f8d9a52b60,dest 指向这里。堆:strcpy 逐字节把常量区的数据复制到 0x55f8d9a52b60,最后加 '\0'。
  3. 风险点:如果忘了 delete[] dest,堆内存就永远 "租" 着不还(内存泄漏)—— 就像搬家后不退租,白白浪费钱。

三、三种复制方式的栈堆变化对比表

复制方式

栈变化

堆变化

典型场景

std::string 深拷贝

新建对象,复制长度 / 容量,指针指向新堆地址

新分配内存,复制全部数据

现代 C++(C++11 及以后)

std::string COW

新建对象,复制长度 / 容量,指针指向原堆地址

初始共享,修改时才新分配并复制

旧编译器(如 GCC 4.8 前)

strcpy(C 风格)

指针从无效变为指向新堆地址

手动分配内存,逐字节复制

兼容 C 代码

四、避坑指南:别让内存 "搬家" 变 "翻车"

  1. 深拷贝的性能坑:频繁深拷贝大字符串会浪费内存和时间(就像天天搬大箱子)。解决:用std::move转移所有权(直接把仓库地址牌给别人,自己不用了)。
  2. cpp
  3. 运行
  4. std::string a = "very long string..."; std::string b = std::move(a); // 转移所有权,a变空,无堆复制

  5. COW 的多线程坑:多线程同时修改共享字符串,可能导致复制混乱(就像两人同时装修合租仓库)。现代编译器淘汰 COW 就是因为这个。
  6. strcpy 的越界坑:目标内存不够大时,strcpy会写到界外(就像搬家时东西堆到邻居家)。解决:用strncpy(dest, src, max_len)限制长度。

总结:复制的本质是 "数据主权转移"

C++ 字符串复制时,栈和堆的变化围绕一个核心:数据的主权归属



  • 深拷贝:立刻创建独立主权,栈堆都独立。
  • COW:暂时共享主权,修改时再分割。
  • C 风格:手动管理主权,容易出纰漏。



理解了栈上的 "地址牌" 和堆里的 "数据仓库" 如何配合,你就能轻松掌控字符串复制的内存奥秘~


标题

  1. 《C++ 字符串复制:栈堆内存的 "搬家" 全过程解析》
  2. 《从深拷贝到 COW:C++ 字符串复制的栈堆变化明细》

简介

本文用 "搬家" 的趣味类比,详细解析 C++ 字符串复制时栈和堆的变化过程。通过std::string的深拷贝、写时复制(COW)以及 C 风格strcpy的案例,展示栈上指针 / 长度 / 容量与堆上数据的具体变化,对比不同复制方式的内存行为与适用场景,附完整代码和编译步骤,帮你彻底理解字符串复制的底层逻辑。

关键词

#C++ #字符串复制 #栈堆变化 #深拷贝 #内存管理

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言