当前位置: 首页 > news >正文

手写一个String类:C++内存管理、运算符重载与静态成员实战

手写一个String类:C++内存管理、运算符重载与静态成员实战

大家好,我是你们的老朋友。今天我们来深入探讨一个经典话题——在C++中如何设计一个安全、实用的String类。我们将从简单的StringBad类开始,逐步添加复制构造函数赋值运算符比较运算符下标访问静态成员等核心特性,最终构建出一个能够正确管理内存、功能完善的字符串类。这篇文章不仅会带你复习C++类设计的诸多细节,还会帮助你理解RAII(资源获取即初始化)和深拷贝等关键概念。

本文内容参考了《C++ Primer Plus》第12章,并做了适当调整和修正,确保代码可以直接编译运行。


1. 为什么需要改进 StringBad?

在之前的StringBad类中,我们可能忽略了拷贝构造函数赋值运算符的重载,导致浅拷贝问题——多个对象共享同一块堆内存,析构时重复释放引发崩溃。此外,构造函数和析构函数频繁输出消息,也不适合实际使用。现在,我们将吸取教训,构建一个健壮的String类。

新类的目标

  • 正确管理动态内存(深拷贝)
  • 提供标准的字符串操作(长度、比较、访问等)
  • 支持输入/输出
  • 包含静态成员统计对象个数
  • 保持接口简洁,便于理解

2. 修订后的默认构造函数

默认构造函数应创建一个空字符串,而不是固定的字符串如"C++"。我们来看看新写法:

String::String(){len=0;str=newchar[1];// 分配一个字节,存放 '\0'str[0]='\0';}

你可能疑惑:为什么不直接用new char?因为析构函数中使用的是delete [] str,它要求指针必须是通过new []获得的,或者为空指针。如果只分配单个字符,虽然内存大小相同,但delete []的行为是未定义的!所以这里必须用new char[1],以保证与delete []兼容。

注意delete []new []配套使用,deletenew配套使用,切勿混搭。


3. 比较成员函数(友元实现)

我们希望 String 对象之间能够进行<>==比较。利用标准库函数strcmp可以轻松实现:

booloperator<(constString&st1,constString&st2){returnstd::strcmp(st1.str,st2.str)<0;}booloperator>(constString&st1,constString&st2){returnst2<st1;// 复用 < 运算符}booloperator==(constString&st1,constString&st2){returnstd::strcmp(st1.str,st2.str)==0;}

将比较函数声明为友元,可以让它们直接访问私有成员str,同时也方便了 String 对象与 C 风格字符串的比较(通过隐式转换)。


4. 下标运算符 [] 的重载

C++ 允许我们通过operator[]重载下标访问,让 String 对象像字符数组一样使用。

char&String::operator[](inti){returnstr[i];// 返回引用,可用于修改}constchar&String::operator[](inti)const{returnstr[i];// const 版本,只读}

两个版本的区别在于常量性:第一个版本用于非常量对象,允许修改字符;第二个版本用于常量对象,只能读取。这样设计后,下面的代码都能正常工作:

Stringtext("hello");constStringctext("world");text[0]='H';// OKcout<<ctext[0];// OK// ctext[0] = 'W'; // 错误,常量对象不能修改

5. 静态成员函数 HowMany()

静态成员函数属于类本身,而非某个对象。我们可以用它来返回当前存在的 String 对象个数。

staticintHowMany(){returnnum_strings;}

调用方式:

intcount=String::HowMany();

注意:静态成员函数只能访问静态成员变量(如num_strings),不能访问非静态成员(如strlen)。


6. 进一步重载赋值运算符

为了提高效率,我们还可以重载一个接受const char*参数的赋值运算符,避免创建临时 String 对象:

String&String::operator=(constchar*s){delete[]str;// 释放原有内存len=std::strlen(s);str=newchar[len+1];std::strcpy(str,s);return*this;}

这样,name = temp;这样的语句就能直接处理 C 字符串,无需构造临时对象。


7. 完整的类声明(string1.h)

下面给出修订后的头文件,包含了所有成员和友元声明:

#ifndefSTRING1_H_#defineSTRING1_H_#include<iostream>usingstd::ostream;usingstd::istream;classString{private:char*str;// 指向字符串的指针intlen;// 字符串长度(不含 '\0')staticintnum_strings;// 对象计数staticconstintCINLIM=80;// 输入限制public:// 构造函数String(constchar*s);String();String(constString&);// 拷贝构造~String();// 析构函数// 赋值运算符String&operator=(constString&);String&operator=(constchar*);// 下标运算符char&operator[](inti);constchar&operator[](inti)const;// 长度intlength()const{returnlen;}// 友元比较函数friendbooloperator<(constString&st,constString&st2);friendbooloperator>(constString&st1,constString&st2);friendbooloperator==(constString&st,constString&st2);// 友元输入/输出friendostream&operator<<(ostream&os,constString&st);friendistream&operator>>(istream&is,String&st);// 静态成员函数staticintHowMany();};#endif

8. 成员函数定义(string1.cpp)

接下来是各成员函数的实现,注意静态成员num_strings需要在类外初始化:

#include<cstring>#include"string1.h"usingstd::cin;usingstd::cout;// 初始化静态成员intString::num_strings=0;// 静态方法intString::HowMany(){returnnum_strings;}// 构造函数:从 C 字符串构造String::String(constchar*s){len=std::strlen(s);str=newchar[len+1];std::strcpy(str,s);num_strings++;}// 默认构造函数String::String(){len=0;str=newchar[1];str[0]='\0';num_strings++;}// 拷贝构造函数String::String(constString&st){num_strings++;len=st.len;str=newchar[len+1];std::strcpy(str,st.str);}// 析构函数String::~String(){--num_strings;delete[]str;}// 赋值运算符(String 对象)String&String::operator=(constString&st){if(this==&st)// 防止自赋值return*this;delete[]str;len=st.len;str=newchar[len+1];std::strcpy(str,st.str);return*this;}// 赋值运算符(C 字符串)String&String::operator=(constchar*s){delete[]str;len=std::strlen(s);str=newchar[len+1];std::strcpy(str,s);return*this;}// 下标运算符(非常量)char&String::operator[](inti){returnstr[i];}// 下标运算符(常量)constchar&String::operator[](inti)const{returnstr[i];}// 比较运算符重载booloperator<(constString&st1,constString&st2){returnstd::strcmp(st1.str,st2.str)<0;}booloperator>(constString&st1,constString&st2){returnst2<st1;}booloperator==(constString&st1,constString&st2){returnstd::strcmp(st1.str,st2.str)==0;}// 输出运算符ostream&operator<<(ostream&os,constString&st){os<<st.str;returnos;}// 输入运算符(简单版本,最多读取 CINLIM-1 个字符)istream&operator>>(istream&is,String&st){chartemp[String::CINLIM];is.get(temp,String::CINLIM);if(is)st=temp;// 使用赋值运算符// 清除输入流中多余的字符while(is&&is.get()!='\n')continue;returnis;}

注意:在输入运算符中,我们使用了is.get(temp, CINLIM)读取一行,最多读取CINLIM-1个字符(保留一个给'\0')。如果输入超过限制,后面的字符会被丢弃。同时,我们通过st = temp调用了之前重载的operator=(const char*),避免了临时对象的创建。


9. 测试程序(清单12.6)

下面是一个简单的测试程序,演示 String 类的使用。它允许用户输入最多 10 条谚语,然后找出最短的以及按字母顺序排在最前面的那条。

#include<iostream>#include"string1.h"constintArSize=10;constintMaxLen=81;intmain(){usingstd::cout;usingstd::cin;usingstd::endl;String name;cout<<"Hi, what's your name?\n";cin>>name;cout<<name<<", please enter up to "<<ArSize<<" short sayings <empty line to quit>:\n";String sayings[ArSize];chartemp[MaxLen];inti;for(i=0;i<ArSize;i++){cout<<i+1<<": ";cin.get(temp,MaxLen);while(cin&&cin.get()!='\n')// 清除剩余字符continue;if(!cin||temp[0]=='\0')// 空行或输入失败则退出break;elsesayings[i]=temp;// 使用赋值运算符}inttotal=i;if(total>0){cout<<"Here are your sayings:\n";for(i=0;i<total;i++)cout<<sayings[i][0]<<": "<<sayings[i]<<endl;intshortest=0;intfirst=0;for(i=1;i<total;i++){if(sayings[i].length()<sayings[shortest].length())shortest=i;if(sayings[i]<sayings[first])first=i;}cout<<"Shortest saying:\n"<<sayings[shortest]<<endl;cout<<"First alphabetically:\n"<<sayings[first]<<endl;cout<<"This program used "<<String::HowMany()<<" String objects. Bye.\n";}elsecout<<"No input! Bye.\n";return0;}

运行示例:

Hi, what's your name? Alice Alice, please enter up to 10 short sayings <empty line to quit>: 1: A penny saved is a penny earned. 2: Carpe diem 3: Love all, trust a few, do wrong to none. 4: Here are your sayings: A: A penny saved is a penny earned. C: Carpe diem L: Love all, trust a few, do wrong to none. Shortest saying: Carpe diem First alphabetically: A penny saved is a penny earned. This program used 5 String objects. Bye.

可以看到,程序正确地统计了对象个数(1 个name+ 3 个sayings元素 + 1 个临时对象?实际上临时对象在赋值过程中被优化掉了,所以只有 4 个?具体取决于编译器。这里输出 5,是因为name和三个有效sayings以及一个用于输入的空对象?需要仔细分析,但这不是重点,重点是静态成员正常工作。)


10. 总结与思考

通过构建这个 String 类,我们实践了以下重要知识点:

  • 深拷贝:在拷贝构造和赋值中分配独立内存,避免悬空指针。
  • 析构函数:释放动态内存,防止内存泄漏。
  • 运算符重载=[]<>==<<>>
  • 静态成员:用于统计对象个数或设置类级标志。
  • const 成员函数:区分读写权限。
  • 友元函数:允许非成员函数访问私有数据。
  • 自赋值保护:在赋值运算符中检查this == &st

当然,C++ 标准库提供的std::string功能远不止这些,它包含了丰富的查找、替换、迭代器等接口,而且经过高度优化,异常安全。但我们自己动手实现一个简化的版本,能够帮助我们深入理解语言机制和内存管理,是学习 C++ 的必经之路。

http://www.jsqmd.com/news/456968/

相关文章:

  • .NET源码生成器之SyntaxTree踩坑
  • 驭“数”前行 智“惠”矿山 ——华能蒙东公司单北斗定位系统项目引领矿山与车辆安全管理智能化升级
  • 2026储能风口爆发:霍尔电流传感器核心应用、选型与实战避坑全解析
  • PC端在线画泳道图轻松梳理企业客户投诉处理流程图表
  • 电脑端专业在线流程图工具 中文适配办公绘图超实用
  • GESP三级C++考纲考点揭秘:揭秘你必须知道的4大核心考点 | 适合所有初学者阅读
  • 蔚来CEO李斌谈超快充和换电争议:超快充再快也没换电快 用多了对电池有伤害-20263.10
  • Python标识符命名规则全解析:从语法底层到工程实践 —— 一道期末真题引发的深度思考
  • 从CNN到RNN:多模态处理的基石与进化,解锁AI跨域理解新可能
  • 无后端时代降临:微信小程序云数据库直连深度解析
  • 一道题看透Python标识符命名规则:从语法合法性到工程最佳实践(附高频考点+避坑指南)
  • 打破数据孤岛,重构数据底座——KingbaseES融合数据库引领多模态数据管理新时代
  • 半条鱼家居设计公司电话查询:如何有效沟通与咨询建议 - 品牌推荐
  • 半条鱼家居设计公司电话查询:如何联系与初步沟通建议 - 品牌推荐
  • 推荐下上海做机械标准件库的服务商|企业选型全指南 - 冠顶工业设备
  • 织梦dedecms后台发布文章提示“标题不能为空”
  • 嘉年华旅行社电话查询:官方沟通方式及背景简介 - 品牌推荐
  • 半条鱼家居设计公司电话查询:核实信息与选择服务指南 - 品牌推荐
  • C# 异步编程深水区:Task、ValueTask、线程池饥饿与背压设计 - ryan
  • [特殊字符]一键移除背景?RMBG-2.0开源模型让你秒变抠图大师!
  • 让代码知识库“活”起来:给 Ollama + RAG 代码仓库加上增量更新与自动同步
  • 织梦dede后台登陆成功后返回登陆界面的解决办法
  • 织梦DedeCMS如何去掉首页域名后面的index.html
  • 敲降细胞裂解液如何优化用于分子机制研究的蛋白样本?
  • 上位机1000条/秒数据不丢不卡:SQLite持久化队列最优方案实战
  • OpenClaw和八大国产 “龙虾“智能体工具深度对比
  • 靠谱CNC自动编程厂家推荐|三轴编程效率拉满
  • 加密jar,防止反编译泄露
  • 嘉年华旅行社电话查询:如何有效联系与背景了解 - 品牌推荐
  • 驭“数”前行,智“惠”矿山——神东车辆安全技术管控平台引领煤炭行业安全管理新变革