数据结构(c++版):字符串类的实现

1 字符串的基本概念与重要性

字符串是由零个或多个字符组成的有限序列,是编程中最基本且常用的数据结构之一。在C++中,字符串处理不仅涉及到文本数据的存储和操作,更是许多算法和应用的核心组成部分。虽然C++标准库提供了功能强大的std::string类,但自己实现一个字符串类仍然是理解内存管理运算符重载面向对象编程概念的绝佳方式。

字符串在程序设计中的重要性不言而喻。它们用于存储和处理文本信息,从简单的用户输入到复杂的文档处理,字符串操作无处不在。一个良好的字符串类应当提供丰富的操作接口,包括创建、复制、连接、比较和修改等功能,同时还要保证高效的内存使用和安全性。通过自定义字符串类,开发者可以更深入地理解字符串操作的底层机制,提升编程能力

在C语言中,字符串通常通过字符数组和一系列标准库函数(如strlenstrcpystrcat等)来处理。然而,这种方式的缺点在于需要手动管理内存,且缺乏直接支持字符串高级操作的能力。C++通过类和运算符重载机制,使得字符串的处理更加直观和便捷,提高了代码的可读性和易用性

2 C++字符串类的设计思路

实现一个自定义字符串类需要考虑多个关键因素,这些因素直接影响类的性能、安全性和易用性。以下是设计字符串类时的核心考虑方面:

2.1 内存管理策略

字符串类需要动态管理内存,以适应不同长度的字符串。在我们的String类中,使用char* str指针来存储字符串内容,并在构造函数中动态分配内存,在析构函数中释放内存。这种设计允许字符串长度在运行时动态变化,但需要精心管理内存分配和释放,避免内存泄漏悬空指针问题

良好的内存管理策略还包括在分配内存时预留额外空间(如容量管理),以减少频繁内存重新分配的开销。虽然当前的String类实现中没有显式的容量管理,但在实际应用中,通常会维护一个容量值(capacity),当需要扩展字符串时,不是精确分配所需内存,而是分配更大的块以提高效率

2.2 运算符重载的应用

运算符重载是C++的一个重要特性,它允许我们为自定义类型定义运算符的行为。在字符串类中,通过重载运算符,可以使字符串操作更加直观和自然。我们的String类重载了多个运算符,包括:

  • 赋值运算符(=)​​:用于字符串的赋值操作
  • 下标运算符([])​​:用于访问字符串中的单个字符
  • 相等运算符(==!=)​​:用于字符串比较
  • 加法运算符(+)​​:用于字符串拼接
  • 输出运算符(<<)​​:用于字符串的输出

这些重载的运算符使得字符串对象可以像内置类型一样使用,大大提高了代码的可读性和简洁性。例如,我们可以使用s1 + s2来拼接两个字符串,使用s1 == s2来比较两个字符串是否相等,而不需要调用特定的成员函数

2.3 深拷贝与浅拷贝的选择

在字符串类的设计中,​拷贝处理是一个关键问题。浅拷贝只是复制指针值,导致多个对象共享同一内存块,这会在析构时引起重复释放内存的问题。深拷贝则是创建新的内存空间并复制内容,确保每个对象拥有独立的数据副本

我们的String类实现了深拷贝策略,这在拷贝构造函数和赋值运算符中体现得尤为明显。拷贝构造函数String(const String& s)会分配新内存并复制内容,而不是简单地复制指针。同样,赋值运算符也会先释放原有内存,再分配新内存并复制内容。这种策略虽然需要更多的内存和计算资源,但避免了多个对象共享同一内存带来的问题,提高了代码的安全性

2.4 常量正确性与异常安全

良好的C++代码应当注重常量正确性和异常安全性。在我们的String类中,所有不会修改对象状态的成员函数都被声明为const,如getlength()operator[]等。这确保了这些函数可以在常量对象上调用,同时向使用者明确表达了函数的行为

异常安全是指代码在面临异常情况时仍能保持一致性。在字符串类中,内存分配可能会失败(抛出std::bad_alloc异常),因此需要确保在异常发生时不会出现资源泄漏或数据不一致的情况。虽然当前的实现没有显式处理异常,但在生产环境中,需要确保即使发生异常,资源也能被正确释放,对象状态保持一致

3 String类的具体实现分析

3.1 默认构造函数实现

String::String()
{
    length = 0;
    str = new char[1];
    str[0] = '\0';
}

默认构造函数创建一个空字符串。它设置长度为0,并分配一个字符的内存空间,用于存储字符串结束符\0。这种实现确保了即使空字符串也能正确表示,并且与其他C字符串函数兼容。需要注意的是,即使字符串为空,也需要分配内存并正确设置结束符,这是为了与C风格字符串保持一致,并确保后续操作(如strcpystrcat)的正确性

3.2 参数化构造函数实现

String::String(const char* s)
{
    length = strlen(s);
    str = new char[length + 1];
    strcpy(str, s);
}

参数化构造函数接受一个C风格字符串作为参数,并基于它创建String对象。该函数首先使用strlen函数计算输入字符串的长度,然后分配足够的内存(长度+1,用于存储结束符\0),最后使用strcpy函数将输入字符串复制到新分配的内存中。这个构造函数允许从C风格字符串隐式或显式创建String对象,提供了与现有代码的互操作性

3.3 拷贝构造函数实现

String::String(const String& s)
{
    length = s.length;
    str = new char[length + 1];
    strcpy(str, s.str);
}

拷贝构造函数用于创建一个与已有String对象内容相同的新对象。它执行深拷贝,即分配新的内存空间并复制原始对象的内容,而不是简单地复制指针。这样确保了每个对象拥有独立的字符串数据,避免了多个对象共享同一内存可能带来的问题。特别是在对象传递和返回时,拷贝构造函数保证了每个对象的数据独立性

3.4 析构函数实现

String::~String()
{
    delete[] str;
}

析构函数负责释放对象所占用的资源,主要是动态分配的内存。通过使用delete[]操作符释放str指针所指向的内存,析构函数防止了内存泄漏的发生。需要注意的是,析构函数在对象生命周期结束时自动调用,无需手动调用,这简化了资源管理并提高了代码的可靠性

3.5 赋值运算符重载实现

String& String::operator=(const String& s)
{
    if (this != &s)
    {
        delete[] str;
        length = s.length;
        str = new char[length + 1];
        strcpy(str, s.str);
    }
    return *this;
}

赋值运算符重载允许将一个String对象的值赋给另一个已存在的对象。函数首先检查自我赋值的情况(this != &s),这是必要的,因为如果没有这个检查,自我赋值会导致先释放内存,然后访问已释放内存的内容。在排除自我赋值后,函数释放目标对象的原有内存,分配新内存,并复制源对象的内容。最后返回当前对象的引用,以支持链式赋值(如a = b = c

3.6 索引运算符重载实现

char String::operator[](size_t index) const
{
    return str[index];
}

索引运算符重载允许像访问数组一样访问字符串中的字符。通过重载[]运算符,我们可以直接使用s[index]的形式访问字符串中的特定字符,而不需要调用专门的成员函数。当前的实现提供了常量版本的索引运算符,它返回指定位置的字符但不允许修改。在实际应用中,通常还会提供非常量版本,返回字符的引用,允许修改指定位置的字符

3.7 相等与不等运算符重载实现

bool String::operator==(const String& s) const {
    return strcmp(str, s.str) == 0;
}

bool String::operator!=(const String& s) const {
    return strcmp(str, s.str) != 0;
}

相等和不等运算符重载使用C标准库函数strcmp来比较两个字符串的内容。strcmp函数逐字符比较两个字符串,如果相等返回0,否则返回非零值。这些重载的运算符使得字符串比较变得直观和简洁,我们可以直接使用s1 == s2s1 != s2来进行比较,而不需要调用专门的比较函数

3.8 拼接运算符重载实现

String String::operator+(const String& s) const
{
    String result;
    result.length = length + s.length;
    result.str = new char[result.length + 1];
    strcpy(result.str, str);
    strcat(result.str, s.str);
    return result;
}

拼接运算符重载实现了两个字符串的连接操作。它创建一个新的String对象,其长度为两个字符串长度之和,分配足够的内存,首先复制第一个字符串的内容,然后连接第二个字符串的内容。函数返回新创建的字符串对象,支持链式拼接操作。这种实现使得字符串拼接变得简单直观,如s3 = s1 + s2或更复杂的链式操作

3.9 输出运算符重载实现

ostream& operator<<(ostream& out, const String& s)
{
    out << s.str;
    return out;
}

输出运算符重载允许使用标准输出流(如cout)直接输出String对象。函数接受一个输出流引用和一个String对象引用,将对象的字符串内容输出到流中,然后返回流的引用以支持链式输出操作。这个运算符重载极大地简化了字符串的输出,使我们可以直接使用cout << s而不是cout << s.c_str()或类似的冗长语法

4 String类的功能测试与验证

为了验证String类的各项功能,我们使用提供的测试代码对类进行全面测试。测试代码涵盖了字符串的创建、输出、拼接、索引访问、比较和赋值等主要功能。

基本输出测试​:

String s("123456d");
cout << s << endl;

这段代码测试了参数化构造函数和输出运算符重载。它创建一个包含内容"123456d"的字符串对象,然后使用cout输出。预期输出为"123456d",验证了构造函数和输出运算符的正确性

字符串拼接测试

cout << s + "4455" << endl;

这行代码测试了字符串拼接功能。它将字符串s与C风格字符串"4455"拼接,然后输出结果。预期输出为"123456d4455",验证了加法运算符重载的正确性

索引访问测试​:

cout << s[5] << endl;

这行代码测试了索引运算符重载。它访问字符串s的第5个索引位置(0-based)的字符。对于字符串"123456d",索引5处的字符是'6',预期输出为'6',验证了索引运算符的正确性

字符串比较测试​:

cout << (s == "123456d") << endl;
cout << (s != "123456d") << endl;

这两行代码测试了相等和不等运算符重载。第一个比较预期结果为true(输出1),第二个比较预期结果为false(输出0),验证了比较运算符的正确性

链式赋值测试​:

String a, b, c;
a = b = c = s;
cout << s << endl;
cout << a << b << c << endl;

这段代码测试了赋值运算符重载的支持链式赋值的能力。它将字符串s的值依次赋给c、b和a,然后输出所有字符串。预期所有字符串都具有与s相同的内容,验证了赋值运算符的正确实现

复制功能测试​:

String x = s.copy();
cout << x << endl;

这段代码测试了copy函数的功能。它创建s的一个副本x,然后输出x的内容。预期输出与s相同,验证了copy函数的正确性

通过以上测试,我们验证了String类的各项功能均正常工作,包括构造、输出、拼接、索引访问、比较、赋值和复制等操作。这些测试确保了类实现的正确性和可靠性。

5 String类的扩展思路与优化建议

虽然当前实现的String类提供了基本功能,但仍有许多方面可以改进和扩展,以提高其性能、安全性和功能完整性。以下是一些可能的扩展思路和优化建议:

  • ​迭代器支持​:为String类添加迭代器支持,使其能够与标准库算法协同工作。可以定义begin()end()成员函数,返回指向字符串开头和结尾的迭代器。迭代器可以是简单的指针包装,因为字符串在内部使用字符数组存储

    。迭代器支持将允许使用范围-based for循环遍历字符串中的字符,以及使用标准库算法处理字符串。
  • ​移动语义支持​:在C++11及更高版本中,可以添加移动构造函数和移动赋值运算符,以提高性能。移动操作"窃取"临时对象的资源,而不是进行昂贵的深拷贝,这对于返回值和临时对象特别有用

    。移动构造函数和移动赋值运算符接受右值引用参数(String&&),并转移资源所有权,将源对象的指针设置为nullptr以防止重复释放。
  • ​容量管理机制​:当前实现每次拼接操作都会分配精确所需的内存,这在频繁操作时可能降低性能。可以添加容量管理机制,维护一个容量值(capacity),当需要扩展时不是精确分配所需内存,而是按某种策略(如倍增)分配更大内存

    。这需要添加reserve()函数来预留内存,以及capacity()函数来查询当前容量。同时,可以添加shrink_to_fit()函数来释放多余内存。
  • ​更多字符串操作功能​:可以添加更多实用的字符串操作功能,如子串提取(substr())、查找(find())、替换(replace())、插入(insert())和删除(erase())等

    。这些功能将大大提高类的实用价值,使其更接近标准库字符串类的功能。
  • ​异常安全增强​:增强类的异常安全性,确保在内存分配失败等异常情况下,对象仍处于有效状态。可以使用RAII(Resource Acquisition Is Initialization)技术,确保资源总是被正确管理

    。例如,在赋值运算符中,可以先分配新内存再释放旧内存,这样即使在分配过程中发生异常,原有数据仍然保持不变。
  • ​​ Unicode支持​:当前实现假设字符串使用单字节编码,对于多字节字符集(如UTF-8)可能处理不当。可以扩展类以支持Unicode字符串,添加编码转换和Unicode感知的操作功能。这需要更复杂的设计,但对于现代应用程序非常重要。

通过这些扩展和优化,String类将变得更加强大、高效和实用,更接近标准库字符串类的功能和性能。

源码区:

#define _CRT_SECURE_NO_WARNING
 #include<stdio.h>
#include<iostream>
#include<string>
#include<cstring>
using namespace std;
class String
{
public:
	String();//初始化字符串
	String(const char* s);
	String(const String& s);//拷贝构造函数
	~String();
	size_t getlength()const;//size_t是内置长度类型关于长度的都可以用这个
	char operator[](size_t index)const;//operator是运算符重载只要我用了【】运算符就是调用了这个函数
	String& operator=(const String& s);//对两个字符串进行操作,重载两个运算符以后相当于操作符后面那个字符串都是固定const+类型加引用+对象名
	bool operator==(const String& s)const;
	bool operator!=(const String& s)const;
	String copy()const;
	String operator+(const String& s)const;
		friend ostream & operator<<(ostream& out, const String& s);
	

private:
char* str;//数组动态分配
	size_t length;

};

String::String()
{
	length = 0;
	str = new char[1];
	str[0] = '/0';

}
String::String(const char* s)//不希望传进来的字符串有所改变所以加const
{
	length = strlen(s);
	str = new char[length + 1];
	strcpy(str, s);//将s的内容区别拷贝到string里面

}
String::String(const String& s)//拷贝构造函数防止拷贝函数指向同一地址实现两次析构
{
	length = s.length;
	str = new char[length + 1];
	strcpy(str, s.str);

}
String::~String()
{
	delete[]str;
}

size_t String::getlength()const
{
	return length;
}
char String::operator[](size_t index)const//例:string s;s【2】直接访问第三个字符不然就要调用函数才能访问更加方便
{
	return str[index];
}
String& String::operator=(const String& s)//因为是赋值运算要修改String 类的内容所以用引用更直接减少拷贝的内存数据
{
	if (this != &s)
	{
		delete[]str;
		length = s.length;
		str = new char[length + 1];
		strcpy(str, s.str);
}
	return *this;//等于返回string
}
bool String::operator==(const String& s)const {
	return strcmp(str, s.str)==0;
}
bool String::operator!=(const String& s)const {
	return strcmp(str, s.str) != 0;
}
String String::copy()const
{
	String s = *this;//调用赋值,解引用本身再返回,初始化的效果==(*this) s与x的str会指向同一块内存
	return s;
}
String String::operator+(const String& s)const
{
	String result;//返回拼接结果
	result.length = length + s.length;
	result.str = new  char[result.length + 1];
	strcpy(result.str, str);
	strcat(result.str, s.str);//result相当于一个中介,先接受str在接受s.str。strcarAPI是拷贝到结尾的接口
	return result;
}
ostream& operator<<(ostream& out, const String& s)//运算符重载声明   函数体中调用输出流对象重载流运算符,将string类型的对象输出到输出流中
{
	out << s.str;//返回输出的out 引用,实现连续输出的效果
	return out;
}

int main()
{
	String s("123456d");
	cout << s << endl;
	cout << s + "4455" << endl;
	cout << s[5] << endl;
	cout << (s == "123456d")<< endl;
	cout << (s != "123456d") << endl;
	s = s + "hhhh";
	cout << s << endl;
	String a, b, c;
	a = b = c= s;
	cout << s << endl;
	cout << a<<b <<c<< endl;
	String x = s.copy();
	cout << x << endl;
	return 0;
}

运行区:

自己实现字符串类 class CMStringImp; class CMstring { public: explicit CMstring(void); ~CMstring(void); CMstring(LPCTSTR lpszstr); CMstring(const CMstring& lpszstr); CMstring& operator = (const CMstring& lpszstr); operator LPCTSTR() const; bool operator == (const CMstring&) const; bool operator != (const CMstring&) const; bool operator < (const CMstring&) const; TCHAR operator[] (int nIndex) const; TCHAR& operator[] (int nIndex); CMstring& operator += (LPCTSTR pStr); CMstring& operator += (TCHAR ch); friend CMstring operator+(const CMstring& str1, const CMstring& str2); friend CMstring operator+(const CMstring& str1, LPCTSTR lpszstr); friend CMstring operator+(const CMstring& str1, TCHAR ch); friend CMstring operator+(TCHAR ch, const CMstring& str1); friend CMstring operator+(LPCTSTR lpszstr, const CMstring& str1); // friend wostream operator <<(wostream &is;,const CMstring &str;); public: LPCTSTR GetData() const; bool IsEmpty() const; TCHAR GetAt(int) const; TCHAR& GetAt(int); int GetLength() const; int Compare(const CMstring&) const; int CompareNoCase(const CMstring&) const; int Find(TCHAR ch, int nStart = 0) const; int Find(LPCTSTR pStr, int nStart = 0) const; int ReverseFind(TCHAR ch) const; int ReverseFind(LPCTSTR pStr) const; CMstring Right(int nCount) const; CMstring Left(int nCount ) const; public: CMstring& MakeLower(); CMstring& MakeUpper(); CMstring& MakeReverse(); int Replace(TCHAR chOld, TCHAR chNew); int Replace(LPCTSTR pszOld, LPCTSTR pszNew); int Insert(int iIndex, TCHAR ch); void Format(LPCTSTR lpszFormat, ...); private: CMStringImp* m_pImp; };
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值