前面我们搞定了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,会引发双重释放、内存泄漏等严重问题。
六大默认成员函数分别是:
-
默认构造函数
-
析构函数
-
拷贝构造函数
-
赋值运算符重载
-
取地址运算符重载
-
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. 深浅拷贝痛点
编译器默认拷贝 = 浅拷贝:直接逐字节拷贝指针地址,最终导致:
-
两个对象的
_data指向同一块堆内存 -
对象析构时,同一块内存被释放两次 → 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. 核心难点(三大必备步骤)
赋值重载如果不严谨,会出现自赋值崩溃、内存泄漏、数据覆盖问题,必须严格遵循三步逻辑:
-
判断自赋值:防止 s1 = s1 时,先释放内存导致数据丢失
-
释放旧资源:清空当前对象原有堆内存,避免内存泄漏
-
开辟新内存 + 拷贝数据:完成赋值更新
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成员函数意义
只读防护、兼容常量对象调用,提升代码健壮性与安全性
浙公网安备 33010602011771号