找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2582

积分

0

好友

357

主题
发表于 5 天前 | 查看: 17| 回复: 0

如果让我选一个最适合检验 C++ 基本功的练手题目,我大概率会选:手写一个简化版的 std::string

它不需要你写复杂算法,也不涉及晦涩的模板元编程,但几乎把 C++ 里最容易“踩坑”的基础点,全都串了一遍:资源管理、拷贝控制、异常安全、移动语义、接口设计

更重要的是,你写出来的代码,跑不跑、稳不稳、好不好维护,自己一眼就能看出来。

为什么是 string,而不是 vector 或智能指针?

std::string 的好处在于:它足够小,但不简单

  • 内部一定有 动态内存
  • 一定会涉及 深拷贝 / 浅拷贝
  • 一定要考虑 异常安全
  • 一定绕不开 Rule of Three / Five
  • 稍微写完整一点,就会自然引出 移动语义

而且你平时用得越多,越容易低估它的复杂度。 真正动手写一遍,才会发现标准库的设计并不“理所当然”。

先明确一个目标:我们要实现到什么程度?

这里不是造一个“能替代标准库”的怪物,而是一个教学级、但设计正确String

目标功能

  • 管理一段以 \0 结尾的字符数组
  • 支持:
    • 默认构造
    • const char* 构造
    • 拷贝构造 / 拷贝赋值
    • 移动构造 / 移动赋值
    • 析构
  • 提供最基本的接口:size()c_str()

不做的事情

  • 不实现 SSO
  • 不考虑编码
  • 不追求极致性能

语义必须正确

第一版:最朴素的资源管理模型

先把类的骨架搭出来:

class String {
public:
    String() : data_(nullptr), size_(0) {}

    explicit String(const char* s) {
        size_ = std::strlen(s);
        data_ = new char[size_ + 1];
        std::memcpy(data_, s, size_ + 1);
    }

    ~String() {
        delete[] data_;
    }

    size_t size() const { return size_; }
    const char* c_str() const { return data_; }

private:
    char* data_;
    size_t size_;
};

到这里为止,一切都看起来很合理。但这只是第一步

此时的 String 有一个致命问题:一旦发生拷贝,就会炸。

拷贝构造:C++ 新手的第一个大坑

如果你现在这样用:

String a(“hello”);
String b = a;

编译能过,运行未定义行为。

原因很简单:默认拷贝构造是“逐成员拷贝”data_ 被两个对象指向同一块内存,析构时必然 double free。

正确的拷贝构造必须是深拷贝

String(const String& other) : size_(other.size_) {
    if (other.data_) {
        data_ = new char[size_ + 1];
        std::memcpy(data_, other.data_, size_ + 1);
    } else {
        data_ = nullptr;
    }
}

写到这里,其实已经在无形中用到了一个非常重要的设计原则:

谁申请资源,谁负责拷贝它。

拷贝赋值:真正考验基本功的地方

拷贝赋值比拷贝构造更容易写错。

一个常见但危险的版本是:

String& operator=(const String& other) {
    delete[] data_;
    size_ = other.size_;
    data_ = new char[size_ + 1];
    std::memcpy(data_, other.data_, size_ + 1);
    return *this;
}

问题在于:一旦 new 抛异常,对象已经被破坏。

标准库采用的思路是 copy-and-swap,这里我们也照着做:

String& operator=(String other) {
    swap(other);
    return *this;
}

void swap(String& other) noexcept {
    std::swap(data_, other.data_);
    std::swap(size_, other.size_);
}

这段代码背后,其实同时解决了三个问题:

  • 自赋值安全
  • 异常安全
  • 逻辑清晰

很多人第一次见到这种写法会觉得“绕”,但真正理解之后,反而很难再写回去。

移动语义:不是为了快,而是为了“对”

既然已经实现了拷贝控制,那再加上移动语义,几乎是顺手的事。

String(String&& other) noexcept
    : data_(other.data_), size_(other.size_) {
    other.data_ = nullptr;
    other.size_ = 0;
}

String& operator=(String&& other) noexcept {
    if (this != &other) {
        delete[] data_;
        data_ = other.data_;
        size_ = other.size_;
        other.data_ = nullptr;
        other.size_ = 0;
    }
    return *this;
}

这里有两个细节非常关键:

  1. 必须把源对象置为可析构状态
  2. noexcept 不是装饰品,容器会依赖它做优化

很多“看起来写了移动构造”的代码,真正用进 vector 里时,却完全没触发移动,原因往往就在这里。

这个小类到底考了你什么?

一个不到百行的 String,实际上串起了 C++ 的一整条主线:

  • RAII 是否理解到位
  • 拷贝和赋值是否分得清
  • 异常安全有没有概念
  • 移动语义是不是只停留在语法层面
  • 接口设计有没有保持对象不变式

如果这些点你都能写对、讲清楚,那C++ 的地基已经很稳了。这不仅是对内存管理的深刻理解,也是对RAII、移动语义等现代C++核心特性的实践检验。

写在最后

很多人刷题、刷八股,但对自己每天在用的 stringvector 却从没真正“拆开看过”。

手写一次,不是为了造轮子,而是为了知道:标准库到底替你扛走了多少复杂度。

如果你最近在补 C++ 基础、准备面试,或者只是想验证一下自己的理解是否扎实——这个练习,非常值得。如果你想与更多开发者交流此类基础但至关重要的技术话题,欢迎到云栈社区参与讨论。




上一篇:10个Agent Skills实战指南:提升AI工作效率与自动化流程
下一篇:系统分析师备考指南:计算机系统核心考点与RAID技术解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 02:48 , Processed in 0.297035 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表