string 模拟实现

string的数据结构

char* _str;
size_t _size;
size_t _capacity;

_str 是用来存储字符串的数组,采用new在堆上开辟空间;

_size 是用来表示字符串的长度,数组大小strlen(_str);

_capacity 是用来表示_str的空间大小, _capacity 不包括字符串中 '\0' 所占的空间。。

string的构造、析构函数和operator=

在STL中这个构造函数有很多的版本,但是这里只实现常用的几个版本

string s1;
string s2("hello world");
string s3(s2);

1)缺省构造函数

        string(const char* str = "")
			: _size(strlen(str))
		{
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

下面有几个错误版本:

a) 构造函数的类型转换错误

string(const char* str)
	:_str(str), _size(strlen(str)), _capacity(strlen(str))
{

}
string s2("hello world");

如果这样设计就会出现下面的报错信息:

str 是一个const char* 类型的数据,_str 是一个char* 数据类型,在初始化列表中将str 初始化_str,会导致权限放大的问题,同时还有浅拷贝的问题。

如果我们将string数据结构中的 _str 类型改为const char* 可以解决这个问题,但是又带来了新的问题 :string的内容是不能被修改的;

所以我们不能将数据结构中的_str类型改变为const char*.我们只能修改构造函数。

改进:

在初始化_str时,我们可以申请一块大小为 strlen(str)+1 的空间(要为\0也开辟一个字节的空间),将str中的值拷贝到这块空间中,再让_str指向这块空间(这也是深拷贝的过程)。在初始化列表中,我们可以只初始化 _size(strlen(str)) ,因为对于这种用一个常量字符串进行初始化的情况,我们通常会开辟大小为_size的空间,所以_capacity = _size,如果将_capacity的初始化也写在初始化列表中,就要保证_size初始化的顺序在_capcity初始化之前,而这个初始化顺序取决于两者变量声明的顺序,与初始化列表中的顺序无关。

b)对str空指针解引用问题

string(const char* str)
	: _size(strlen(str))
{
	_capacity = _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}
string s1;

我们在使用cout打印 s1._str 时,流插入自动识别类型,识别到const char* ,但是不打印这个指针,它是通过解引用打印这个字符串,这样就会产生对空指针的解引用 (因为s1._str 被初始化为NULL)。

改进:
1. 单独处理

而STL库中对于这种情况,就直接打印空。为了解决这个问题,我们就需要设计一个无参的构造函数,当创建对象时,如果没有传递值,我们就将这个对象的内容设置成\0。

string()
    :_str(new char), _size(0), _capacity(0) 
{
	_str[0] = '\0';
}
string()
	:_str(new char[1]), _size(0), _capacity(0)
{
	_str[0] = '\0';
}

想一下这两中方式哪一个比较好呢?答案是第二种。

虽然这两种写法都是申请一个字节的空间用来存储\0,但是我们要考虑一下析构函数,如果写成第一种情况,它的析构函数就要写成以下形式:

~string()
{
    delete _str;  // 释放动态分配的内存
    _str = nullptr;
    _size = 0;
    _capacity = 0;
}

但是对于其他有初始值创建对象来说,它们申请的往往是一个数组,这样析构函数就要写成这种形式:

~string()
{
    delete[] _str;  // 释放动态分配的内存
    _str = nullptr;
    _size = 0;
    _capacity = 0;
}

所以为了统一析构函数的写法,我们通常采用第一种方法进行申请空间。

2. 利用缺省值

设置缺省值有以下四种方法:

string(const char* str)
	: _size(strlen(str))
{
	_capacity = _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}
string(const char* str = nullptr)
string(const char* str = '\0')
string(const char* str = "\0")
string(const char* str = "")

第一种会导致strlen求size时,对空指针进行解引用。

第二种会导致强制类型转换,左边是const char* 类型,右边是char类型;

第三种:strlen在求长度时遇到\0就会停止,所以_size = 0-->_capacity = 1,_str中拷贝进入一个\0;

第四种:当然第三种方法中的'\0'也可以不写,因为一个常量字符串默认会以\0作为结尾;

这几种方法中,第四种方法是比较合适的。

c)最终版本

string(const char* str = "")
	: _size(strlen(str))
{
	_capacity = _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}

2)拷贝构造函数

这个函数同样要注意的是,拷贝的时候要进行深拷贝。

string(const string& s)
	:_capacity(s._capacity), _size(s._size)
{
	_str = new char[s._capacity + 1];
	strcpy(_str, s._str);
}

3)析构函数

~string()
{
    delete[] _str;  // 释放动态分配的内存
    _str = nullptr;
    _size = 0;
    _capacity = 0;
}

4)operator=

operate= 的使用对象是两个已经存在的对象:用一个对象赋值给另一个对象。

而拷贝构造是用一个已经存在的对象去初始化另一个对象。

实现这个操作符需要考虑三种情况:主要是空间大小关系

为了避免讨论过多的情况(目的地空间不足还需要进行扩容;如果扩容的空间过大会导致空间浪费),较好的实现方法就是将目的地空间原始空间先释放掉,然后再开辟一个块与起始空间相等的空间,最后在进行值拷贝,所以这种拷贝也叫做深拷贝

// 这里设置返回值,主要是为了支持连续赋值的使用场景
string& operator=(const string& s)
{
	delete[] _str;
	_str = new char[s._capacity + 1];
	strcpy(_str, s._str);
	_capacity = s._capacity;
	_size = s._size;
	return *this;
}

但是这种实现方式还存在几个问题:

a)自己给自己赋值时,会出现问题,当执行 delete[] _str 后,_str 不再指向有效的内存。若之后仍然进行 strcpy(_str, s._str) 操作 (s._str) ,就会导致非法访问已释放的内存,可能会引发程序崩溃或未定义行为;

b)当先给原始空间释放过后,开辟空间失败后会回到main函数中抛异常,此时目的地空间已将被释放了。

解决方法:

a)遇到自己给自己拷贝时,不进行操作直接返回。

b)我们可以先用一个临时变量将起始内容拷贝到这块临时空间后,再释放起始空间,最后将目的地指针指向这块临时空间即可。

// 这里设置返回值,主要是为了支持连续赋值的使用场景
string& operator=(const string& s)
{
	/*delete[] _str;
	_str = new char[s._capacity + 1];
	strcpy(_str, s._str);
	_capacity = s._capacity;
	_size = s._size;
	return *this;*/

	if (this != &s)
	{
		char* temp = new char[s._capacity + 1];
		strcpy(temp, s._str);
		delete[] _str;
		_str = temp;
		_capacity = s._capacity;
		_size = s._size;
		return *this;
	}
	return *this;
}

Iterators:

1)begin、end

Return iterator to beginning / end

Returns an iterator pointing to the first / past-the-end character of the string.

上面的 iterator 是一个宏定义:

typedef char* iterator;
iterator begin()
{
	return _str;
}

iterator end()
{
	return _str + _size;
}

其实迭代器可以理解成指针,但是迭代器的实现不一定是使用指针实现的。

迭代器是一种用于访问容器中元素的对象,它通常是指向容器中某个元素的指针或对象。迭代器的底层实现可以使用指针、类或模板等多种方式。

对于C++ STL中的标准容器,其迭代器一般采用指针来实现。例如,对于 vector 容器,其迭代器类型为指向元素类型的指针。对于 list 容器,其迭代器类型为双向链表节点指针。而对于 mapset 容器,其迭代器类型为指向关键字和值类型的指针。

除了使用指针实现迭代器外,还可以使用类或模板等方式实现。例如,迭代器可以作为容器的内部类来实现,或者可以使用模板来实现通用迭代器。

迭代器还有const类型的。

const_iterator begin() const
{
	return _str;
}

const_iterator end() const
{
	return _str + _size;
}

迭代器的用法我会在下面遍历string时讲到,这里先简单说一下:const类型的迭代器,只能查看数据,不能修改数据;而没有const修饰的迭代器既可以查看也可以修改。

2)rbegin、rend

Return reverse iterator to reverse beginning

Returns a reverse iterator pointing to the last character of the string。

这个迭代器和上面一个是相反的,rend返回指向第一个字符的迭代器,rbegin返回指向最后一个字符的迭代器。

需要注意的是,rbegin在从后向前移动时,用的是rbegin++,而不是rbegin--。

总结一下,在STL中迭代器的区间都是左闭右开的。

Element access:

1)operator[ ]

这种操作符的作用就相当于我们访问数组时的 [ ],所以两者的用法是一样的。

char& operator[](size_t pos)
{
	assert(pos >= 0 && pos < _size);
	return _str[pos];
}

但是对于下面这种情况会出现错误:

char& operator[](size_t pos)
{
	assert(pos >= 0 && pos < _size);
	return _str[pos];
}

void func(const string& s1)
{
    cout << s1[1] << endl;
}

int main()
{
    string s("Hello world");
    func(s);
    return 0;
}

通常我们在函数不改变实参时,会将接收参数的类型设置成 const string& (因为函数形参是实参的一个临时拷贝,这个过程需要调用拷贝构造函数,如果函数参数用引用接收,就不会调用拷贝构造函数,减少了一定的消耗),但是,这样之后我们会发现这样就使用不了 [ ] 操作符了,这是因为 const对象s1不能调用非const成员函数 char& operator[](size_t pos)  ,所以我们需要将 char& operator[](size_t pos) 函数设置成为const类型的。

但是如果只将该操作符重载为const成员函数,这样做就会导致我们不能通过 [ ] 操作符进行更改string对象的内容了,所以我们需要有这两种函数构成重载,在需要时调用合适的函数。

// const对象不能调用非const成员函数,
// 但是对于[]操作符在有些情况下是需要进行更改的:s[pos]++
// 所以这里就需要有两个[]操作符构成重载
const char& operator[](size_t pos) const
{
	assert(pos >= 0 && pos < _size);
	return _str[pos];
}

char& operator[](size_t pos)
{
	assert(pos >= 0 && pos < _size);
	return _str[pos];
}

遍历

1)[ ]+下标

// 保证const对象能够调用这个函数
size_t size() const
{
	return _size;
}

// 这里需要有两个[]操作符构成重载
const char& operator[](size_t pos) const
{
	assert(pos >= 0 && pos < _size);
	return _str[pos];
}

char& operator[](size_t pos)
{
	assert(pos >= 0 && pos < _size);
	return _str[pos];
}

2)迭代器:iterator (用指针模拟实现)

typedef char* iterator;
iterator begin()
{
	return _str;
}

iterator end()
{
	return _str + _size;
}

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

3)范围for

底层使用迭代器实现的,所以只要有迭代器,范围for语法就能使用。

for (auto ch : s1)
{
	cout << ch << " ";
}

这种写法的底层会被替换成上面迭代器的写法。

所以对于下面这种情况就会出错:

typedef char* iterator;
iterator begin()
{
	return _str;
}

iterator end()
{
	return _str + _size;
}

void print(const string& s)
{
	for (auto ch : s)
	{
		cout << ch << " ";
	}
}

s对象为const类型的,所以需要调用const类型的迭代器。所以迭代器也需要实现两种类型的:一个const修饰的、一种没有const修饰的。

字符串比较

利用运算符重载和ASCII码进行比较字符串的大小,功能类似于strcmp函数。

bool operator==(const string& s)
{
	return (strcmp(_str, s._str) == 0);
}

bool operator!=(const string& s)
{
	return !(*this == s);
}

错误点:

需要注意的是,这里不是写成

bool operator!=(const string& s)
{
	return !(_str == s._str);
}

这是因为:如果你直接写成 _str == s._str,将无法调用 operator== 的重载函数。因为 _strs._str 都是指向字符串的指针,这时的==就是判断值是否相等的==,所以这个表达式比较的是两个指针的地址是否相等。

*this == s 这种写法,使用*this表示当前对象自身,表示==左边是一个对象,右边也是一个对象,这时就会调用operator==进行比较两对象的字符串是否相等了。

改进:

但是这样写还是有一点缺陷的:如果我将*this放在操作符==的右边时,会出现问题

bool operator!=(const string& s)
{
	return !(s == *this);
}

因为在这一步中 s == *this  ==左边对象类型为const类型,当它调用operator==函数时,会造成权限的放大(this没有被const关键字修饰,const对象不能调用非const成员函数),所以我们需要在operator==函数中用const修饰this,这样无论是const对象调用==,还是普通对象调用==都是没有问题的,因为权限是可以缩小的,而不能被放大。

同时在实现其他操作符时尽量进行操作符的复用前面我们实现过的操作符,这样有以下好处:

  1. 代码重用:通过复用已经实现过的操作符,可以减少代码的冗余和重复。这样可以提高代码的可维护性和可读性,同时也减少了错误的可能性。

  2. 一致性:通过复用已经实现过的操作符,可以保持代码的一致性。如果已经实现的操作符被正确测试和验证过,那么在其他操作符中复用它们可以确保整个代码逻辑的一致性。

  3. 减少错误:通过复用已经实现过的操作符,可以减少错误和漏洞的引入。如果一个操作符已经被正确实现和测试,并且在其他地方进行了广泛使用和验证,那么在其他操作符中复用它可以避免重新实现相同的逻辑并减少出错的可能性。

  4. 提高效率:复用已经实现过的操作符可以提高代码的执行效率。已经实现的操作符通常会经过优化和性能测试,因此复用它们可以避免重复的计算和处理,从而提高整体的执行效率。

最后实现的代码为:

bool operator==(const string& s) const
{
	return (strcmp(_str, s._str) == 0);
}

bool operator!=(const string& s) const
{
	return !(*this == s);
}

bool operator>(const string& s) const
{
	return (strcmp(_str, s._str) > 0);
}

bool operator>=(const string& s) const
{
	return (*this > s || *this == s);
}

bool operator<(const string& s) const
{
	return !(*this >= s);
}

bool operator<=(const string& s) const
{
	return (*this == s || *this < s);
}

总结一下:

1)对于不修改对象的成员的函数时,尽量使用const修饰该函数;

2)如果这个函数需要同时满足这两种情况时,就需要使用函数重载:

        a)在读取数据时我们不希望进行更改成员,要使用const进行修饰该函数;

        b)如果我们在读的过程中也想要修改时,就不能使用const进行修饰。

操作符优先级问题

同时在使用的过程中,还有一点细节需要注意:

cout << s1 > s2 << endl;

因为流插入<< 的运算优先级比较高,会先运算 cout << s1,它的返回值为ostream的一个流,左边s2 << endl 返回值也是一个流,最后在进行运算 ostream > ostream ,此时类型就不匹配了。

报错内容为:

二元“<<”: 没有找到接受“string”类型的右操作数的运算符(或没有可接受的转换)

所以在进行这个运算时,要加一个括号,以免出现错误。

Modifiers:

1)reserve

Request a change in capacity

Requests that the string capacity be adapted to a planned change in size to a length of up to n characters.

它不会改变string的length,只是进行扩容(分配更大的空间并修改capacity)。

void reserve(size_t n)
{
	char* temp = new char[n + 1]; //注意为\0开辟一个空间
	strcpy(temp, _str);
	delete[] _str;
	_str = temp;
	_capacity = n;
}

 当然这个reserve还有一点瑕疵,当n小于_capacity时,会进行缩容
 (缩容可能会带来一定的风险:如果进行插入操作还要再进行扩容,形成抖动--反复的释放和申请空间。因为C++不支持释放空间的一部分,要想达到缩容效果,只能开辟一块较小的空间,然后将内容拷贝到这块空间,并将_str指向这块空间)

并且当缩容的空间小于_str的字符长度时,通过strcpy拷贝还会产生越界的问题。

缩容指的是将动态数组或容器的内部数组大小减小,以释放多余的内存。虽然缩容可以减少内存的使用,但同时也可能带来以下问题:

  1. 内存分配和释放开销:缩容需要重新分配内存,并将原有数据复制到新的内存中。这一过程会产生一定的时间和空间开销,而且频繁进行缩容操作会加剧这一问题。

  2. 导致内存碎片:由于缩容会释放一部分内存,这些内存空间可能不能连续使用,从而导致内存碎片的产生。内存碎片会影响内存分配和释放的效率,甚至可能导致程序出现内存溢出等问题。

  3. 降低性能:缩容可能会导致性能下降。如果缩容频繁进行,那么每次重新分配内存和复制数据都会耗费时间和计算资源,从而降低程序的整体性能。

  4. 可能引发bug:在进行缩容操作时,如果没有正确处理好指针和迭代器等相关问题,就可能会引发程序崩溃、数据丢失等错误,甚至可能会破坏程序的正确性和稳定性。

所以库里的reserve是不支持缩容的。

void reserve(size_t n)
{
	if (n > _capacity)
	{
		char* temp = new char[n + 1]; //为\0开辟一个空间
		if (_str)
		{
			strcpy(temp, _str);
			delete[] _str;
		}
		_str = temp;
		_capacity = n;
	}
}

同时这里使用new开辟空间时,不用再向malloc一样进行检查temp是否为空,因为new如果空间开辟失败也不会返回NULL,而是抛异常。

我们还需要判断一下,_str是否为空,如果为空就不需要再将_str内容拷贝给temp中了,只进行扩容操作(会出现空指针解引用的问题),不为空时才进行拷贝。

2)resize

Resize string

Resizes the string to a length of n characters.

If n is smaller than the current string length, the current value is shortened to its first n character, removing the characters beyond the nth.

resize的功能与reserve的功能类似,resize不仅能够扩容还能够进行初始化。

注意库里面resize的细节:

同时resize在扩容时还存在一定的内存对齐,这导致有时候开辟的空间会大于我们的需求。(注意不同的编译器实现不同,在VS上会存在这种情况,但是在g++编译器上,它只会申请我们给定的空间大小)

示例一:

std::string s3;
s3.resize(10, 'x');
cout << s3.c_str() << endl;
s3.resize(20, 'y');
cout << s3.c_str() << endl;

我们发现对于已经初始化的部分,resize是不会再进行初始化了,它只会在新开辟的空间进行初始化。

示例二:

std::string s3;
s3.resize(10, 'x');
cout << s3.c_str() << endl;
s3.resize(20, 'y');
cout << s3.c_str() << endl;
s3.resize(5, 'y');
cout << s3.c_str() << endl;

  • resize的实现

主要进行空间三种空间大小的比较

void resize(size_t n, char ch = '\0') // 扩容+初始化
{
	// 三种情况进行讨论
	if (n <= _size)
	{
		// 容量不变,只改变内容
		_size = n;
		_str[_size] = '\0';
	}
	else
	{
		if (n > _capacity) // 需要的空间大于原本的空间时,进行扩容
		{
			reserve(n);
		}
		// 当需要的空间大于_size但小于_capacity时,不需要进行扩容直接进行初始化

		// 将新开辟的空间进行初始化
		size_t i = _size;
		while (i < n)
		{
			_str[i] = ch;
			i++;
		}
		_size = n;
		_str[_size] = '\0';
	}
}

3)insert

void insert(size_t pos, char ch)
{
	assert(pos >= 0 && pos <= _size);
	// 如果空间不足,需要进行扩容
	if (_size + 1 > _capacity)
	{
		reserve(_size + 1);
	}
	// 移动数据
	for (size_t i = _size; i > pos; i--)
	{
		_str[i] = _str[i - 1];
	}
	_str[pos] = ch;
	_size++;
	_str[_size] = '\0';
}

因为在插入一个字符之前需要将在指定位置后面的字符向后移动,将这个空间给空出来。

移动数据有两种方法:

 一种是将当前数据移动到后面;

另一种是将当前数据的前一个数据移动到当前位置。

第一种方法的最后一步是 end 移动到0时,进行最后一次移动数据,end--,结束条件是-1 <= 0,条件为假,退出循环。

第二种方法的最后一步是end移动到下标为1的位置上时,进行最后一次移动数据,end--,结束条件是0 < 0,条件为假,退出循环。

但是如果移动数据部分,是以下样式的写法在进行头插时会出现错误:

size_t end = _size;
while(end <= pos)
{
    _str[end+1] = _str[end];
    end--;
}

当pos为0,最后一次循环时end为0,0--,又因为end为size_t数据类型的,0--后是一个很大的数,而不是-1.

如果我们将end的数据类型改为int类型也不能解决这个问题。这是因为_size是一个size_t类型的,在定义int end = _size时,会进行类型提升,end最终还是size_t的类型。

所以不能使用这个方法挪动数据。

4)operator+=、append、push_back

void reserve(size_t n)
{
	char* temp = new char[n + 1]; //为\0开辟一个空间
	strcpy(temp, _str);
	delete[] _str;
	_str = temp;
	_capacity = n;
}

void push_back(char ch)
{
	// 判断容量
	if (_size + 1 > _capacity)
	{
		// 扩容(添加单个字符时,一次扩容二倍)
		if (_capacity == 0)
			reserve(2);
		else
		{
			reserve(_capacity * 2); // capacity不包括\0的空间
		}
	}
	// 添加
	_str[_size] = ch;
	_size++;
	_str[_size] = '\0';
}

void append(const char* str)
{
	// 判断容量
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		// 扩容(添加单个字符时,一次扩容需要的空间大小)
		reserve(_capacity + len);
	}
	strcpy(_str + _size, str);
	_size += len;
}

string& operator+=(char ch)
{
	push_back(ch);
	return *this;
}

string& operator+=(const char* str)
{
	append(str);
	return *this;
}

5)erase

void erase(size_t pos, size_t len = npos)
{
	assert(pos < _size && pos >= 0); // 条件为假进行断言
	size_t end = pos + len;
	size_t begin = pos;
	if (len == npos || pos + len > _size) // pos + len = 0
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		/*for (size_t i = end; i <= _size; i++)
		{
			_str[begin++] = _str[i];
		}*/
		strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}
}

细节:

1、npos

静态变量在声明时是不能给缺省值的,因为在声明时给的缺省值是被用来在构造函数初始化列表中初始化成员变量时使用的。

而静态变量是属于整个类的,不能在某一个对象中进行初始化。所以静态变量的初始化应该在类外进行,而不应该在构造函数中。

但是C++在这块语法上有一个槽点:

上面已经提到static修饰的变量是不能在类中进行初始化的,但是如果在之前的基础上加上一个const就可以在声明时进行初始化:

static const size_t npos = -1;

但是这种写法只针对于整形静态变量,对于其他数据类型的变量都不能这样使用。

否则会有以下报错信息:

static const double temp = 1.1;

2、

这里的第二种情况要考虑len==npos,因为npos是一个size_t最大的数,这个最大的数尽管加上一也会进行溢出,所以需要单独进行判断一下:

6)swap

交换有两种情况:

一种是直接内容不变,直接交换指向两个内容的指针

另一种是指向内容的指针不变,将两部分的内容进行交换。

显然第一种方式更为高效。(string类中使用的是第一种交换方法,而在算法模板中的交换函数用的是第二种方法)

void swap(string& s)
{
	// 交换容量
	size_t t = _size;
	_size = s._size;
	// 交换指针
	char* temp = _str;
	s._size = t;
	_str = s._str;
	s._str = temp;
	// 如果_size>_capacity则需要进行扩容
	s.reserve(s._size);
	reserve(_size);
}

Non-member function overloads

1)流插入

Ⅰ. 流插入的使用细节

int main()
{
    std::string s("0123456");
    s1 += '\0';
    s1 += "XXXXXX";
    
    cout << s << endl;
    cout << s.c_str() << endl;    
}

这两种打印方式不同主要在于:在打印 s 时是通过 s 的 size 决定打印的内容的;

在打印 s.c_str() 时,是通过 ‘\0’ 决定打印的内容的。

所以上面的打印结果为:

0123456\0XXXXXX 和 0123456

主要原因是,虽然两个都是使用  <<  进行打印,但是两者调用的函数是不一样的:

第一个,<< 操作符的右面数据类型为string,所以在打印的时候会调用:

ostream& operator<<(ostream& out, const string& s)
{
	for (auto ch : s)
	{
		out << ch;
	}
	return out;
}

这个循环是从string的开头打印到结尾,将所有的内容都打印出来。

iterator begin()
{
	return _str;
}

iterator end()
{
	return _str + _size;
}

第二个,<< 操作符右面的数据类型为const char* (因为s.c_str() 函数的返回值就是const char* ),所以它调用的函数就是C++自己实现的基本类型操作符,就相当于打印一个字符串,功能类似于puts、printf("%s")。所以遇到\0就会停止打印。

const char* c_str()
{
	return _str;
}

Ⅱ. 流插入的模拟实现

使用迭代器,流插入的一般实现方法,我已经在另一篇博客中详细介绍了,这里不做赘述:

链接:http://t.csdnimg.cn/eenhr

但是对于有迭代器的类来说,我们除了使用一般方法,还可以用迭代器帮助我们访问类的私有成员对象。

ostream& operator<<(ostream& out, const string& s)
{
	for (auto ch : s)
	{
		out << ch;
	}
	return out;
}

这个函数将放在全局中,函数内部使用迭代器和公有成员 <<,注意内部的 << 是C++基本类型的流提取操作符。

2)流提取

Ⅰ.流提取的使用细节

  • 将换行符和空格为两个字符的分隔符

cin 和 scanf 一样,会将换行符和空格为两个字符的分隔符。看一个实例:

istream& operator>>(istream& in, string& s)
{
	char ch;
	in >> ch;
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		in >> ch;
	}
	return in;
}

程序还没有结束,在这段代码中,我们认为当输入一个回车时这个“hello world”字符串就会结束。但是,默认情况下,会将  空格和回车  这两个字符作为两个字符之间的分隔符,不会作为一个有效字符从缓冲区中读取出来,所以ch永远读不到空格和换行符,也就不会跳出循环。

  •  缓冲区细节

cin和C语言中的scanf使用不是同一个缓冲区。所以我们在使用时要配套使用:使用cin从标准输入读取,使用cout从缓冲区读出;使用 scanf 从标准输入读取,使用 printf 从缓冲区读出。

在 C++ 中,cin 对象是 C++ 标准库中的输入流对象,它使用了独立于 C 语言的缓冲区。当你使用 cin 对象进行输入操作时,输入的数据首先会被存储在 cin 对象的缓冲区中,然后再根据需要从缓冲区中读取数据。

而在 C 语言中,scanf 函数使用的是标准输入流 stdin,它也有自己的缓冲区。当你使用 scanf 函数进行输入操作时,输入的数据会直接存储在 stdin 的缓冲区中。

尽管 cinscanf 使用了不同的缓冲区,但它们最终都会从标准输入中读取数据。因此,在 C++ 和 C 混合编程时,如果你先使用了 cin 进行输入操作,然后又使用了 scanf,你需要注意输入缓冲区的状态。由于 cinscanf 使用不同的缓冲区,可能会导致输入的数据不符合预期。

为了避免这种混乱,可以在使用 scanf 前先使用 cin.ignore() 清空 cin 缓冲区中的未读取字符,或者使用 fflush(stdin) 清空 stdin 缓冲区中的内容。这样可以确保下一个输入操作从一个干净的缓冲区开始。

需要注意的是,fflush(stdin) 并不是标准 C 的规定,它是一种常见的编译器扩展。在某些编译器中,fflush(stdin) 可能会导致未定义行为。因此,在使用 fflush(stdin) 时要谨慎,并根据具体的编译器和平台进行判断。

Ⅱ. 流提取的模拟实现

如果你希望将空格和换行符都作为字符串的一部分进行读取,可以使用 getline 函数来代替 >> 操作符。getline 函数可以读取一行完整的输入,包括其中的空格和换行符。此时我们再使用空格和换行符作为循环结束条件才可行。

#include <iostream>
using namespace std;
istream& operator>>(istream& in, string& s)
{
	char ch;
	ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		ch = in.get();
	}
	return in;
}

int main()
{
	string s2;
	cin >> s2;
	cout << s2 << endl;
	return 0;
}

在这段程序中,当读到空格时,认为这个字符串是一个有效字符并读取到ch中,然后进行循环条件的判断。读取到空格直接就跳出循环,剩余的内容没有被读取出来,world会被留到缓冲区中。

当然,库里面为了减少扩容的次数,会先设置一个预填空间,当这个空间满了之后,就将这个空间的内容添加到string中,然后string一次就会开辟预填空间的大小的空间,预填空间容量归零;同时再次输入进去的字符还是先添加到预填空间中,等预填空间满了之后再次添加到string中……

istream& operator>>(istream& in, string& s)
{
	s.clear();

	char ch = in.get();
	char buff[128];
	size_t i = 0;
	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;
}

其实就是以空间换时间的策略。


今天的分享就到这里了,如果,你感觉这篇博客对你有帮助的话,就点个赞吧!感谢感谢……

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/331708.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用 Postman 发送 get 请求的简易教程

在API开发与测试的场景中&#xff0c;Postman 是一种普遍应用的工具&#xff0c;它极大地简化了发送和接收HTTP请求的流程。要发出GET请求&#xff0c;用户只需设定正确的参数并点击发送即可。 如何使用 Postman 发送一个GET请求 创建一个新请求并将类型设为 GET 首先&#…

余承东发声!预测:2024年,鸿蒙OS将取代苹果iOS…

半导体行业观察机构Techinsights&#xff0c;1月3日发布报告预测&#xff1b;从2024年起&#xff0c;鸿蒙Harmony OS将取代苹果iOS&#xff0c;成为中国市场上第二大智能手机操作系统。 TechInsights预测&#xff0c;2024年全球智能手机销量将同比反弹3%。华为手机在2024年将坚…

基于Harris角点的多视角图像全景拼接算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1 Harris角点检测 4.2 图像配准 4.3 图像变换和拼接 4.4 全景图像优化 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 function [ImageB…

k8s---ingress对外服务(ingress-controller)

ingress 概念 k8s的对外服务&#xff0c;ingress service作用现在两个方面&#xff1a; 1、集群内部&#xff1a;不断跟踪的变化&#xff0c;更新endpoint中的pod对象&#xff0c;基于pod的ip地址不断变化的一种服务发现机制。 2、集群外部&#xff1a;类似于负载均衡器&a…

如何在Linux运行RStudio Server并实现Web浏览器远程访问

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” 文章目录 前言1. 安装RStudio Server2. 本地访问3. Linux 安装cpolar4. 配置RStudio server公网访问地址5. …

Linux系统编程(二)文件IO/系统调用IO

一、IO 简介 I/O 是一切实现的基础&#xff1a; 标准 IO&#xff08;stdio&#xff09;&#xff1b;系统调用 IO&#xff08;sysio&#xff0c;文件IO&#xff09;&#xff1b; 不同系统上的系统调用 IO 的使用方式可能不一样&#xff0c;为了隐藏不同系统上的细节&#xff…

mysql 为大表新增字段或索引

1 问题 mysql 为大表增加或增加索引等操作时&#xff0c;直接操作原表可能会因为执行超时而导致失败。解决办法如下。 2 解决办法 &#xff08;1&#xff09;建新表-复制表A 的数据结构&#xff0c;不复制数据 create table B like A; &#xff08;2&#xff09;加字段或索…

使用muduo库编写网络server端

muduo库源码编译安装和环境搭建 C muduo网络库知识分享01 - Linux平台下muduo网络库源码编译安装-CSDN博客 #include<iostream> #include<muduo/net/TcpServer.h> #include<muduo/net/EventLoop.h> using namespace std; using namespace muduo; using name…

两道有挑战的问题(算法村第九关黄金挑战)

将有序数组转换为二叉搜索树 108. 将有序数组转换为二叉搜索树 - 力扣&#xff08;LeetCode&#xff09; 给你一个整数数组 nums &#xff0c;其中元素已经按 升序 排列&#xff0c;请你将其转换为一棵 高度平衡 二叉搜索树。 高度平衡 二叉树是一棵满足「每个节点的左右两个…

作为班主任如何管理好班级

作为班主任&#xff0c;如何才能把班级管理得井井有条&#xff0c;让每个学生都能够得到全面的发展呢&#xff1f;这个问题一直困扰着许多班主任。接下来&#xff0c;我将从几个方面来分享一下自己的经验和看法。 建立良好的师生关系是班级管理的基石。作为班主任&#xff0c;…

【linux】粘滞位.yum

粘滞位 1.为什么我们普通用户可以删掉别人的文件&#xff08;包括root&#xff09;?合理吗&#xff1f; 2.删除一个文件和目标文件有关系吗&#xff1f; 没关系&#xff0c;和所处的目录有关系。 1.我们先以root身份创建一个目录&#xff0c;接着在这个目录下创建一个文件 2…

如何获取一个德国容器

1.注册discord账号 discord注册网址:https://discord.com/ 下面是容器的discord邀请链接 https://discord.com/Discord邀请链接:https://discord.com/invite/jVMSWrchC4 2.进入discord群聊点击link 在点击网址,这个网址每星期都会变就是图中的② 3.进入容器网址,进入界面…

POKT Network 开启周期性通缩,该计划将持续至 2025 年

POKT Network&#xff08;也被称为 Pocket Network&#xff09;在通证经济模型上完成了重大的改进&#xff0c;不仅将通货膨胀率降至 5% 以下&#xff0c;并使 POKT 通证在 2025 年走向通缩的轨迹上&#xff0c;预计到2024 年年底通货膨胀率将降至 2% 以下。POKT Network 的 “…

JVM工作原理与实战(十九):运行时数据区-方法区

专栏导航 JVM工作原理与实战 RabbitMQ入门指南 从零开始了解大数据 目录 专栏导航 前言 一、运行时数据区 二、方法区 1.方法区介绍 2.方法区在Java虚拟机的实现 3.类的元信息 4.运行时常量池 5.字符串常量池 6.静态变量的存储 总结 前言 JVM作为Java程序的运行环境…

什么是网络安全,如何防范?

网络安全&#xff08;Cyber Security&#xff09;是指网络系统的硬件、软件及其系统中的数据受到保护&#xff0c;不因偶然的或者恶意的原因而遭受到破坏、更改、泄露&#xff0c;系统连续可靠正常地运行&#xff0c;网络服务不中断。 网络安全涵盖了网络设备安全、网络信息安全…

canvas绘制不同样式的五角星(图文示例)

查看专栏目录 canvas实例应用100专栏&#xff0c;提供canvas的基础知识&#xff0c;高级动画&#xff0c;相关应用扩展等信息。canvas作为html的一部分&#xff0c;是图像图标地图可视化的一个重要的基础&#xff0c;学好了canvas&#xff0c;在其他的一些应用上将会起到非常重…

如何在MinIO存储服务中通过Buckets实现远程访问管理界面上传文件

文章目录 前言1. 创建Buckets和Access Keys2. Linux 安装Cpolar3. 创建连接MinIO服务公网地址4. 远程调用MinIO服务小结5. 固定连接TCP公网地址6. 固定地址连接测试 前言 MinIO是一款高性能、分布式的对象存储系统&#xff0c;它可以100%的运行在标准硬件上&#xff0c;即X86等…

真假转换之间 tr

文章目录 真假转换之间 tra-z小写全部转换为大写A-Z大写全部转换为小写貌似起名可以用这个移除文件中的所有空格更多信息 真假转换之间 tr Linux tr 命令用于转换或删除字符。 tr 命令可以从标准输入读取数据&#xff0c;经过字符串转译后&#xff0c;将结果输出到标准输出。…

线性回归理论+实战

线性回归 什么是线性回归 3.1. 线性回归 — 动手学深度学习 2.0.0 documentation (d2l.ai) 模型 损失函数 模型拟合&#xff08;fit&#xff09;数据之前&#xff0c;我们需要确定一个拟合程度的度量。 损失函数&#xff08;loss function&#xff09;能够量化目标的实际值…

[go语言]数据类型

目录 知识结构 整型、浮点型 1.整型 2.浮点型 复数、布尔类型 1.复数 2.布尔类型 字符与字符串 1.字符串的格式化 2.字符串的截取 3.格式化好的字符串赋值给量 4.字符串的转换 5.strings包 知识结构 整型、浮点型 1.整型 在Go语言中&#xff0c;整型数据是一种基…