自学内容网 自学内容网

C++之String类模拟实现(上)

片头

嗨~小伙伴们,在前2篇文章中,我们介绍了关于C++String类的用法。今天,我们将学习如何模拟实现string类,准备好了吗?咱们开始咯!


一、基本框架

我们stl库中的string类是在std命名空间的,这里我们自定义一个命名空间bit,包含string类和简单的成员变量

namespace bit 
{
class string 
{
private:
char* _str;
size_t _size;
size_t _capacity;
};
}

二、构造函数和析构函数

在string.h中,声明构造函数和析构函数;在string.cpp中,实现构造函数和析构函数

string.h文件 

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<string.h>

namespace bit 
{
class string 
{
public:
string(const char* str);//构造函数
~string();//析构函数
private:
char* _str;
size_t _size;
size_t _capacity;
};
}

string.cpp文件

#include"string.h"
//多个文件,相同的命名空间,执行代码的时候,会合并
namespace bit {
//指定类域,在类里面声明,在类外面定义

//构造函数
string::string(const char* str)
:_str(new char[strlen(str)+1])
,_size(strlen(str))
,_capacity(strlen(str))
{
//拷贝数据到_str中
strcpy(_str, str);
}

//析构函数
string::~string() {
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}

运行一下:

但是有一个问题:strlen函数的效率比较低,运行时计算字符串的长度,3个strlen函数重复计算。

有的小伙伴可能会说,那简单,调换一下初始化列表的顺序不就行了?

小伙伴可能会说:那我改变声明顺序呢?emmm,可以倒是可以,但是习惯本身不容易改变。所以不推荐这种方法。

为了避免这种情况,我们使用函数体来初始化。

namespace bit {
//指定类域,在类里面声明,在类外面定义

//构造函数
string::string(const char* str)
//将_size放在初始化列表里面
:_size(strlen(str))
{
//_str和_capacity放到函数体里面
_str = (new char[_size + 1]);
_capacity = _size;
strcpy(_str, str);
}

//析构函数
string::~string() {
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}

此外,我们也可以提供string类的无参构造

 有的小伙伴可能会说:这还不简单~

错误写法:

//无参构造
string::string() {
_str = nullptr;
_size = 0;
_capacity = 0;
}
//这种写法是不可以的

这种写法看似正确,其实是错误的,因为如果我后面需要打印字符串,需要对空指针解引用,编译器会崩溃。

使用c_str函数时,获取到了一个空指针,但是并不会打印。因为c_str函数返回的是const char* 类型,const char* 不会按指针打印,而是按照字符串来打印,会去解引用,找到'\0'为止。这就出现了空指针解引用问题,程序会崩溃。

但是std库里面就不会崩溃。库里面,严格来说,它开了1个空间,存放’\0'。

所以,为了和std库保持一致,我们这样修改:

//无参构造
string::string() {
_str = new char[1]{'\0'};
_size = 0;
_capacity = 0;
}

 new一个数组,数组的元素类型为char,数组里面只有1个值'\0'

还可以再优化一下,无参函数和带参函数可以优化为全缺省函数

全缺省的函数声明:

string(const char* str = "");//全缺省的构造函数

全缺省的函数定义: 

        //全缺省的构造函数
string::string(const char* str)
//将_size放在初始化列表里面
:_size(strlen(str))
{
//_str和_capacity放到函数体里面
_str = (new char[_size + 1]);
_capacity = _size;
strcpy(_str, str);
}

缺省参数不能在函数声明和定义中同时出现。那我们把缺省参数写到函数声明中 


三、拷贝构造函数

函数声明

string(const string& s);//拷贝构造函数

函数定义

//拷贝构造函数
string::string(const string& s) {
_str = new char[s._capacity + 1];//多开1个空间,存放'\0'
strcpy(_str, s._str);//拷贝数据
_capacity = s._capacity;//设置容量
_size = s._size;//设置有效数据的个数
}

运行一下

以上是传统写法,接下来我们将要学习现代写法。

打个比方:我想吃烤鸡、烤鸭,传统写法就是自己买一只小鸡、小鸭,慢慢养大,最后吃掉。现代写法就是自己花钱买别人养大的小鸡、小鸭,最后吃掉。很显然,买别人养大的小鸡、小鸭肯定比自己养一只小鸡、小鸭,再慢慢养大更加省时省力。

拷贝构造函数也可以采用类似的思想。我们可以先调用构造函数,构造string类型的temp对象。然后再用temp对象和this对象进行交换

//现代写法(让别人干活,交换)
//s2(s1)
string::string(const string& s) {
string temp(s._str);                    //调用构造函数
std::swap(temp._str, _str);             //将temp._str和_str进行交换
std::swap(temp._size, _size);           //将temp._size和_size进行交换
std::swap(temp._capacity, _capacity);   //将temp._capacity和_capacity进行交换
}

 同时,我们还可以进一步简化

//拷贝构造函数
string::string(const string& s) {
string temp(s._str); //调用构造函数
swap(temp);          //直接调用我们自己定义的swap函数
}

四、遍历字符串

首先,我们用三个函数来获取三个成员变量

string.h中的函数声明

const char* c_str() const;//c_str函数
size_t size() const;//size()函数
size_t capacity() const;//capacity()函数

string.cpp中的函数定义

//c_str函数
const char* string::c_str() const {
return _str;
}

//size()函数
size_t string::size() const {
return _size;
}

//capacity()函数
size_t string::capacity() const {
return _capacity;
}

我们这里设置为const成员函数,使string类对象和const string类对象都可以调用这几个函数

①operator[]函数

operator[]函数的功能:返回pos位置的字符

函数声明:

//非const版本
char& operator[](size_t pos);//operator[]函数

//const版本
const char& operator[](size_t pos)const;

函数定义(非const版本):

//operator[]函数(非const版本)
char& string::operator[](size_t pos) {
assert(pos < _size);//严格控制pos的有效区间
return _str[pos];
}

函数定义(const版本):

//operator[]函数(const版本)
const char& string::operator[](size_t pos) const{
assert(pos < _size);//严格控制pos的有效区间
return _str[pos];
}

测试一下:

#include"string.h"

int main() {
bit::string s1("hello world!");
cout << s1.c_str() << endl;
bit::string s2;//我现在没有给s2传参
cout << s2.c_str() << endl;//以c语言的格式打印字符串

//因为返回类型是char&,可以对s1字符串进行修改
for (size_t i = 0; i < s1.size(); i++) {
s1[i]++;
}
//使用operator[]函数打印
for (size_t i = 0; i < s1.size(); i++) {
cout << s1[i] << " ";
}
cout << endl;

return 0;
}


②iterator迭代器

迭代器是一个类似于指针却不是指针的东西,实际上迭代器很复杂,但是我们简单的模拟实现就认为它是指针。

我们知道,范围for的底层就是iterator迭代器。现在我们使用范围for来遍历字符串试试~

我们需要自己提供一个iterator迭代器来实现范围for的遍历~

string.h中函数的声明

typedef char* iterator;//将char*重命名为iterator
        typedef const char* const_iterator;
        
        //非const版本
iterator begin();  
iterator end();   
        
        //const版本
        const_iterator begin() const;
const_iterator end() const;

        

string.cpp函数的定义

//非const版本
        //begin()函数返回的是第一个元素
string::iterator string::begin() {
return _str;
}
    //end()函数返回的是最后一个元素的下一个位置
string::iterator string::end() {
return _str + _size;
}

//const版本
    string::const_iterator string::begin() const {
return _str;
}
string::const_iterator string::end() const {
return _str + _size;
}

现在我们运行一下:

它底层的逻辑大概是这个样子:

string::iterator it1 = s1.begin();
while (it1 != s1.end()) {
cout << *it1 << " ";
it1++;
}
cout << endl;

测试一下:

void test_string2() {
string s1("hello world!");
string::iterator it1 = s1.begin();
while (it1 != s1.end()) {
cout << *it1 << " ";
it1++;
}
cout << endl;

const string s2("cccccc");//调用const版本
for (auto e: s2) {
cout << e << " ";
}
cout << endl;
}

运行结果:

注意,const_iterator指向的值不能被修改! 


五、对内容进行修改

①reserve函数

函数定义

//预留空间
void reserve(size_t n);

函数定义

void string::reserve(size_t n) {
//如果n大于当前的_capacity,需要扩容
if (n > _capacity) {
//开n+1个空间,多开1个空间预留给'\0'
//'\0'是不包括在_capacity里面的
char* temp = new char[n + 1];
strcpy(temp, _str);//拷贝数据
delete[] _str;//释放旧空间

_str = temp;//将新的地址赋给_str
_capacity = n;//_capacity为n,代表n个有效数据
}
}

只有当新的容量大于当前容量时才会执行

new char[n+1]分配足够的内存来存储n个字符和一个额外的空字符(存储'\0'),再将原来的字符串拷贝到新的空间中,并使原来的指针指向新空间,最后修改capacity为n。这里capacity含义是能存储有效字符个数的空间


②push_back函数

函数声明

//尾插一个字符
void push_back(char ch);

函数定义

//尾插一个字符
void string::push_back(char ch) {
//如果_capacity==_size,说明空间为0或者空间满了,需要扩容
if (_capacity == _size) {
size_t newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newCapacity);
}
_str[_size] = ch;//新的字符ch插入到原来存放'\0'的位置
_str[_size + 1] = '\0';//'\0'就存放到下一个位置
_size++;//_size更新
}

在原来字符串的基础上尾插1个字符,首先判断空间大小。如果起始空间为0,则赋值为4;若不为0,则扩容2倍,最后加上'\0'


③append函数

函数定义

//尾插一个字符串
void append(const char* str);

如果,我们这样实现append函数,是否可行呢?

//尾插一个字符串
void string::append(const char* str) {
size_t len = strlen(str);
if (_size + len > _capacity) {
reserve(_size + len);
}
strcat(_str, str);//在原来的基础上,尾插str新字符串
_size += len;
}

strcat函数是直接覆盖'\0',在原字符串的后面追加新的字符串,同时把'\0'也添加上去。

咱们可以尝试运行一下:

void test_string3() {
bit::string s1("hello world!");
cout << s1.c_str() << endl;

s1.push_back('x');
cout << s1.c_str() << endl;

s1.append("yyyyy");
cout << s1.c_str() << endl;
}

 运行结果没有问题,但是使用strcat函数效率不高,会从头到尾遍历一遍。我们可以使用strcpy函数。

//尾插一个字符串
void string::append(const char* str) {
//获取str新字符串的长度
size_t len = strlen(str);

//如果_size+len大于原有的capacity,扩容
if (_size + len > _capacity) {
reserve(_size + len);
}
strcpy(_str+_size, str);//自定义起始位置,从'\0'的位置开始
_size += len;//_size更新
}

首先,获取新字符串的有效长度,再判断空间是否足够,最后用字符串函数strcpy从尾端('\0'的位置)进行插入,更新成员变量_size的值。


④operator+=函数

函数声明

//operator+=函数可以构成重载,函数名相同,参数不同
string& operator+=(char ch);
string& operator +=(const char* str);

函数定义

    string& string::operator+=(char ch) {
push_back(ch);//调用push_back函数
return *this;
}
string& string::operator +=(const char* str) {
append(str);//调用append函数
return *this;
}

我们可以复用push_back函数和append函数

测试一下:

void test_string3() {
bit::string s1("hello world!");
cout << s1.c_str() << endl;

s1.push_back('x');
cout << s1.c_str() << endl;

s1.append("yyyyy");
cout << s1.c_str() << endl;

s1 += 'Q';
s1 += "国庆节快乐!";
s1 += "happy!";
cout << s1.c_str() << endl;
}

operator+=函数返回的就是对象本身。内置类型用运算符,返回的是什么,自定义类型返回的就是什么。内置类型用运算符,返回的是它本身。比如:int i = 0;i+=10;return i;返回的就是i本身。string+=“xxxxx”;return string;返回的就是string,就是这个对象本身,*this就是这个对象本身。

这个地方采用传引用&返回,因为出了作用域,这个对象还在。


片尾

今天,我们学习了模拟实现C++的string类函数,下一篇文章中,我们将继续介绍相关函数。希望看完这篇文章能对友友们有所帮助!!!

点赞收藏加关注!!!

谢谢大家!!!


原文地址:https://blog.csdn.net/qq_74336191/article/details/142703654

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!