前面我们搞定了C++类与对象的基础语法、内存模型和this指针,今天进入面向对象核心实战:手写一个简易版 MyString 字符串类

本文不使用STL的std::string,全程手动管理堆内存,通过实战彻底吃透C++六大默认成员函数、深浅拷贝、内存泄漏、自赋值判断等难点,是入门C++面向对象的必经实操。

一、场景需求:为什么要手写 MyString?

C语言字符串是裸露的 char*,需要手动管理长度、内存释放,极易出错;而C++的std::string封装完善,但我们不知道底层实现原理。

我们自定义 MyString 类,核心需求:

  • 手动维护堆区字符数组,存储字符串内容

  • 记录字符串有效长度,兼容空串、普通字符串场景

  • 完整实现对象初始化、拷贝、赋值、销毁全生命周期管理

基础类结构定义:

class MyString {
private:
    char* _data;       // 指向堆区动态字符串
    size_t _size;      // 有效字符长度(不包含 '\0')
};

二、C++六大默认成员函数

如果我们不手动实现,编译器会自动生成6个默认成员函数,但默认生成的函数只适用于静态栈内存类,完全不适用需要手动管理堆内存的MyString,会引发双重释放、内存泄漏等严重问题。

六大默认成员函数分别是:

  1. 默认构造函数

  2. 析构函数

  3. 拷贝构造函数

  4. 赋值运算符重载

  5. 取地址运算符重载

  6. const取地址运算符重载

下面我们结合MyString实战,逐个实现、逐个拆解。

三、构造函数:对象的初始化(对象诞生)

1. 核心作用

对象创建时自动调用,负责初始化成员变量、分配堆内存资源。支持函数重载,可实现多种初始化方式。

若未手动实现,编译器生成的默认构造函数不会初始化堆内存,野指针直接导致程序crash。

2. 实战实现(无参 + 带参构造)

// 1. 无参默认构造:初始化空字符串
MyString() 
    : _data(new char[1]), _size(0) 
{
    _data[0] = '\0'; // 空串必须手动补结束符
    cout << "默认构造函数调用" << endl;
}

// 2. 带参构造:通过C风格字符串初始化
MyString(const char* str) 
{
    // 兼容空指针入参,防止strlen空指针崩溃
    if (nullptr == str) {
        _size = 0;
        _data = new char[1];
        _data[0] = '\0';
        return;
    }

    _size = strlen(str);
    _data = new char[_size + 1]; // 多开1字节存储 '\0'
    strcpy(_data, str);
    cout << "带参构造函数调用" << endl;
}

3. 关键踩坑点

  • 空串不能直接置空指针,必须开辟1字节内存存放 \0,避免后续拷贝、打印崩溃

  • 必须兼容 nullptr 入参,防御性编程杜绝空指针异常

  • 堆内存长度必须 _size + 1,预留结束符位置

四、析构函数:资源释放(对象消亡)

1. 核心作用

对象生命周期结束时自动调用,专门用于释放类中 new/new[] 申请的堆内存,杜绝内存泄漏。

2. 核心特性

  • 无返回值、无参数、不能重载(一个类仅有一个析构函数)

  • 编译器默认析构函数只会释放栈成员,不会主动释放堆内存

  • 局部对象出作用域、堆对象delete时,触发析构

3. 实战实现

~MyString() {
    // 防御性判空,避免重复释放
    if (_data != nullptr) {
        delete[] _data; // 释放堆区字符数组
        _data = nullptr; // 置空野指针,杜绝二次释放
    }
    cout << "析构函数调用,释放内存" << endl;
}

五、拷贝构造函数:深拷贝解决双重释放

1. 触发场景

用已有对象初始化新对象时自动调用:

  • MyString s2(s1);

  • MyString s3 = s1;

  • 对象以值传递传参、值返回

2. 深浅拷贝痛点

编译器默认拷贝 = 浅拷贝:直接逐字节拷贝指针地址,最终导致:

  1. 两个对象的 _data 指向同一块堆内存

  2. 对象析构时,同一块内存被释放两次 → Double Free 崩溃

因此,堆内存类必须手动实现深拷贝:开辟全新堆内存,拷贝内容,不共享地址。

3. 实战深拷贝实现

// 拷贝构造:必须const引用传参,避免无限递归
MyString(const MyString& other) 
    : _size(other._size) 
{
    // 1. 开辟全新堆内存(核心:不共享内存)
    _data = new char[_size + 1];
    // 2. 拷贝字符串内容
    strcpy(_data, other._data);
    cout << "拷贝构造函数调用(深拷贝)" << endl;
}

4. 关键考点

拷贝构造参数必须是 const 引用:如果传值,会触发新的拷贝构造,造成无限递归死循环

六、赋值运算符重载:已存在对象的赋值更新

1. 触发场景

两个已经存在的对象相互赋值:s1 = s2;(区别于拷贝构造的「新对象初始化」)。

2. 核心难点(三大必备步骤)

赋值重载如果不严谨,会出现自赋值崩溃、内存泄漏、数据覆盖问题,必须严格遵循三步逻辑:

  1. 判断自赋值:防止 s1 = s1 时,先释放内存导致数据丢失

  2. 释放旧资源:清空当前对象原有堆内存,避免内存泄漏

  3. 开辟新内存 + 拷贝数据:完成赋值更新

3. 实战实现(支持链式赋值)

MyString& operator=(const MyString& other) {
    // 1. 杜绝自赋值:自己给自己赋值直接返回
    if (this == &other) {
        return *this;
    }

    // 2. 释放当前对象旧堆内存
    delete[] _data;

    // 3. 更新长度、开辟新内存、拷贝内容
    _size = other._size;
    _data = new char[_size + 1];
    strcpy(_data, other._data);

    cout << "赋值运算符重载调用" << endl;
    return *this; // 返回自身引用,支持链式赋值 a=b=c
}

七、const成员函数:只读接口,保证数据安全

1. 核心作用

在成员函数末尾加 const,声明该函数为只读函数

  • 编译器强制校验:函数内禁止修改任何成员变量

  • 常量对象(const MyString)只能调用const成员函数

2. 实战工具函数实现

// 打印字符串:只读操作,加const
void print() const {
    cout << "MyString内容:" << _data << endl;
}

// 获取字符串长度:只读操作,加const
size_t size() const {
    return _size;
}

八、取地址运算符重载

取地址运算符是六大默认成员函数之一,编译器默认实现。

// 普通对象取地址
MyString* operator&() {
    return this;
}

// const对象取地址
const MyString* operator&() const {
    return this;
}

九、完整可运行 MyString 源码

#include <iostream>
#include <cstring>
using namespace std;

class MyString {
private:
    char* _data;
    size_t _size;

public:
    // 1. 默认构造
    MyString() 
        : _data(new char[1]), _size(0) 
    {
        _data[0] = '\0';
        cout << "默认构造函数调用" << endl;
    }

    // 2. 带参构造
    MyString(const char* str) 
    {
        if (nullptr == str) {
            _size = 0;
            _data = new char[1];
            _data[0] = '\0';
            return;
        }
        _size = strlen(str);
        _data = new char[_size + 1];
        strcpy(_data, str);
        cout << "带参构造函数调用" << endl;
    }

    // 3. 拷贝构造(深拷贝)
    MyString(const MyString& other) 
        : _size(other._size) 
    {
        _data = new char[_size + 1];
        strcpy(_data, other._data);
        cout << "拷贝构造函数调用(深拷贝)" << endl;
    }

    // 4. 赋值运算符重载
    MyString& operator=(const MyString& other) {
        if (this == &other) {
            return *this;
        }
        delete[] _data;
        _size = other._size;
        _data = new char[_size + 1];
        strcpy(_data, other._data);
        cout << "赋值运算符重载调用" << endl;
        return *this;
    }

    // 5. 析构函数
    ~MyString() {
        if (_data != nullptr) {
            delete[] _data;
            _data = nullptr;
        }
        cout << "析构函数调用,释放内存" << endl;
    }

    // const成员函数
    void print() const {
        cout << "MyString内容:" << _data << endl;
    }

    size_t size() const {
        return _size;
    }

    // 取地址重载(默认实现即可)
    MyString* operator&() { return this; }
    const MyString* operator&() const { return this; }
};

// 测试代码
int main() {
    MyString s1("Hello C++"); // 带参构造
    s1.print();

    MyString s2 = s1; // 拷贝构造
    s2.print();

    MyString s3; // 默认构造
    s3 = s1;     // 赋值重载
    s3.print();

    return 0;
}

十、知识点总结

1. 三大特殊函数使用场景

  • 构造函数:对象初始化、堆内存分配

  • 析构函数:对象销毁、堆内存释放,防止泄漏

  • 拷贝/赋值:堆内存类必须手动实现深拷贝

2. 浅拷贝 VS 深拷贝

  • 浅拷贝:仅拷贝指针地址,多对象共享堆内存 → 双重释放崩溃

  • 深拷贝:开辟新内存、拷贝数据,对象内存独立、安全无冲突

3. 赋值重载三大铁律

自赋值判断 → 释放旧资源 → 开辟新资源拷贝数据

4. const成员函数意义

只读防护、兼容常量对象调用,提升代码健壮性与安全性