本博客将详细介绍如何在C++中实现一个自定义的字符串类 string
,这个类模仿了标准库中 std::string
的关键功能。这个过程将涵盖从声明到定义的每一步,重点介绍内存管理、操作符重载以及提供一些关键的实现细节。
首先:我们采用函数的声明与定义分离
目的是为了增加代码的可维护性以及提高效率:
1.在vs中,如果我们不实现声明与定义分离,那么编译器会默认认为你当前的函数是内联函数
:内联函数是一种编译器指令,它告诉编译器尝试在每个调用点展开函数体,而不是进行常规的函数调用。
也就是说每次调用时会直接展开代码,这只适合一些代码量很小但调用机器频繁的函数。所以我们采用函数的声明与定义分离也可以进一步的优化效率。
2.函数声明与定义分离的注意事项和特点
-
改善编译依赖性:将函数声明放在头文件中,而将实现(定义)放在源文件中,可以减少编译时的依赖关系。当实现改变时,只需重新编译该源文件及其直接依赖,而不需要重新编译包含头文件的所有文件。
-
封装:通过隐藏实现细节,可以保护数据和实现,减少模块间的耦合。这符合封装的面向对象原则,有助于维护和扩展。
-
链接考虑:在多个源文件中分散定义的函数只有在最终链接时才会解析。这种分离确保了更好的模块化和错误隔离,尤其是在大型项目中。
-
避免多重定义:如果在多个源文件中包含相同的函数定义而未进行适当的静态或内联标记,将导致链接错误。正确的声明和定义分离有助于避免这种问题。
类声明和成员变量
首先,我们需要在 my_string
命名空间中定义字符串类的结构。这包括成员变量和函数的声明
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <assert.h>
#include <string.h>
namespace my_string {
class string {
public:
typedef char* iterator;
typedef const char* const_iterator;
public:
const_iterator begin() const;
const_iterator end() const;
iterator begin();
iterator end();
const char* c_str() const;
void earse(size_t pos, size_t len = npos);
void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);
void swap(string& s);
void clear();
void pushback(char ch);
void append(const char* str);
void reserve(size_t n);
void resize(size_t n, char ch = '\0');
size_t size()const;
size_t capacity()const;
size_t find(char ch, size_t pos = 0)const;
size_t find(char* sub, size_t pos = 0)const;
string(const char* str = "");
string(const string& str);
~string();
string& operator=(string tmp);
string& operator+=(const char* str);
string& operator+=(char ch);
bool empty()const;
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
public:
static const int npos;
};
在定义成员变量时我们采用缺省值来增加安全性。
经典的string类问题
浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来 。如果 对象中管理资源 ,最后就会 导致多个对象共 享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为为 有效,所以当继续对资源进项操作时,就会发生发生了访问违规
深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情 况都是按照深拷贝方式提供。
也就是说我们如果不显示构造就会用默认的拷贝构造函数,就会出错!
构造函数和析构函数实现
构造函数初始化字符串,并为字符数组分配内存。我们同时提供了拷贝构造函数和析构函数以处理资源管理:
string::string(const char* str )
:_size(strlen(str))
{
_capacity = _size ;
_str = new char[_size + 1];
strcpy(_str, str);
}
string::string(const string& str)
{
string tmp(str._str);
swap(tmp);
}
string::~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
动态内存管理实现
对于内存管理,我们实现了 reserve
和 resize
方法来处理内存分配和调整大小的需求:
void string::reserve(size_t n) {
if (n > _capacity) {
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void string::resize(size_t n, char ch) {
if (n > _size) {
reserve(n);
std::fill(_str + _size, _str + n, ch);
_size = n;
}
_str[_size] = '\0';
}
操作符重载
操作符重载使得字符串类的对象能以类似于原生数据类型的方式使用:
string& string::operator=(string tmp) {
swap(tmp);
return *this;
}
bool operator==(const string& s1, const string& s2) {
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
std::ostream& operator<<(std::ostream& out, const string& s) {
out << s._str;
return out;
}
实现自定义迭代器
迭代器提供了几种操作,如递增 (++
), 递减 (--
), 解引用 (*
), 等。在我们的例子中,由于使用了原始指针作为迭代器,这些操作已经由指针的自然行为直接支持
//这里我们手搓一个迭代器
typedef char* iterator;
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
支持范围 for
循环
在C++定义中,范围for的底层实现是迭代器,所以哪怕我们要自定义使用范围for
我们就必须手动实现一个一模一样的迭代器这样就可以使用了
实现流插入和输出操作
在自定义实现流插入时,我们发现只能从右边传递给左边,但左边默认是*this,此时如果还是右边传递给左边,那不就成立 cout 传递给 *this 了,道反天罡!所以我们采取定义全局函数,将参数变成两个自定义的就可以解决,由于cout 是ostream流里的 cin是istream流里的所以返回类型也是对应的流类型
std::ostream& operator<<(std::ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
std::istream& operator>>(std::istream& in, string s)
{
s.clear();
char ch;
//in >> ch; //不能用下标 还没开孔加呢
//
ch = in.get();
char buff[128];
size_t i = 0;
//cin 和 scanf读不到空格
//C语言用getchar
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127)
{
buff[127] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
总结
以上就是如何在C++中从头到尾实现一个自定义的 string
类。我们不仅关注了如何实现基础功能,还涉及了如何通过操作符重载提高类的可用性,以及如何确保类在处理动态内存时的安全和效率。通过这种方式,我们能更深入地理解标准 std::string
类的内部工作机制,并能在需要时为自己的应用定制特定的行为。