迭代器
概念:在 C++ 中,迭代器是访问容器(如数组、列表、向量、字符串等)元素的一种方式。迭代器提供了一种统一的接口,使得你可以使用相同的代码来遍历不同类型的容器。迭代器本质上是一个指针或者指针的封装,它指向容器中的某个元素。
iterator:表示迭代器类型,通常用于声明迭代器变量
迭代器是通用的,能更好与算法配合
迭代器的分类
在C++中,迭代器根据功能和能力被分为 5种类型,支持的操作逐渐增强。
以 std::string 为例,它的迭代器属于 随机访问迭代器。
获取迭代器
迭代器操作
范围for循环
一个类只要支持迭代器就支持范围for,其底层实现就是迭代器。范围for存在局限性,只能顺着遍历。若是自定义类需要自己实现普通和const迭代器。
aoto关键字
核心作用:让编译器自动推导变量类型,避免显式写出冗长或复杂的类型名
推导规则:
1.忽略顶层 const 和引用,保留底层const:
什么是顶层引用,为什么忽略?
C++ 中并没有“顶层引用”这一标准术语,但可以通过对比 “顶层 const” 来类比理解:
1.顶层 const:表示对象本身是常量(直接修饰变量本身)。例如const int a = 10; 中的 a 是顶层 const。
底层 const:表示指针或引用指向的对象是常量。例如:const int* p = &a; 中的 p 是底层 const(指针指向的内容不可变)。
2.避免意外的引用绑定:如果 auto 自动保留引用,可能导致未预期的副作用(例如无意中修改原数据)。
简化代码逻辑:大多数情况下,开发者可能只需要操作副本而非用。
使用规则:
auto 会丢弃初始化表达式中的引用属性,推导出被引用对象的类型。若需要保留引用,必须显式添加 &。
2.数组退化为指针
注意事项:
1.必须初始化。auto 变量必须显式初始化,否则无法推导类型
2.类型可能不符合预期。推导结果可能因初始化表达式不同而变化
3.谨慎使用引用和 const。需显式指定 & 或 const 以保留引用或常量性
auto范围for的使用场景
auto加引用,ch 是字符串中字符的引用,直接绑定到原字符串的每个字符上。在循环体内对 ch 的修改(如 ch++)会直接影响原字符串。
不加引用auto ch,ch 是字符串中字符的副本,与原字符串完全独立。
对 ch 的修改(如 ch++)仅作用于副本,不会影响原字符串。原字符串保持不变。
1.为什么学习string类
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP(面向对象编程)的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
所以C++中专门在标准模板库中封装了一个string类,方便我们进行有关字符串的操作,在实现时不用自己再实现代码细节,只需正确调用函数即可
2.标准库中的string类
2.1了解string类
定义:std::string 是一个模板类,定义在 头文件中。它本质上是一个动态数组,用于存储字符序列。
- 字符串是表示字符序列的类
- 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
- string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)
- string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作basic_string的默认参数(更多的模板信息请参考basic_string)。
- 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。不能操作多字节或者变长字符的序列。
在使用string类时,必须包含#include头文件以及using namespace std。
2.2常用接口说明
构造函数
1.默认构造函数:创建一个空对象,不需要任何参数。
2.复制构造函数:通过已有的string对象来创建一个新的string对象,参数是对一个string对象的常量引用。
3.从子字符串构造:第一个参数是对原始字符串的引用,pos是子字符串开始的位置,len是子字符串的长度。
4.从C字符串构造:参数是一个指向字符数组的指针。
5.从字符序列构造:从给定的字符数组中复制前n个字符来创建对象,参数是一个指向字符数组的指针和要复制的字符数量。
6.填充构造函数:参数是字符串长度和填充字符
7.模板构造函数:允许使用任何类型的输入迭代器来构造一个字符串。参数确定序列的范围。
容量操作函数
size:返回字符串有效字符长度
与length底层实现原理一样,引入size是为了与其他接口保持一致,一般情况下都用size()。和strlen一样不计入’\0’
capacity:返回当前分配的存储空间大小,即字符串的容量。
empty:检查字符串是否为空。返回 true;否则返回 false
clear:将s中的字符串清空
注意清空时只是将size清0,不改变底层空间的大小
reserve:请求改变字符串的容量,只开空间。
如果 n 大于当前容量,字符串的容量会被增加到至少 n;如果 n 小于或等于当前容量,此调用可能不会改变容量,这是一个不具有约束力的请求。底层实现看是否还存有数据(是否使用clear+reserve(0)),若有则不缩,反之缩。
一般情况下不会缩容,因为系统不支持分段释放内存,缩容是以时间换空间的方式 ,将原空间需保留部分拷贝到新空间,再释放原空间。
n 是一个 size_t 类型的参数,表示要预留的最小字符数。如果省略此参数或其值为 0,则不会预留额外的内存。通过预留足够的内存,可以减少在字符串增长过程中因内存不足而进行的多次内存分配和数据复制,从而提高程序的性能
resize:预留足够的内存,开空间+填值初始化。
如果 n 小于当前长度,字符串会被截断;如果 n 大于当前长度,字符串会被扩展,并用空字符填充。
存在第二种带填充字符的重载。将字符串的大小调整为 n 个字符,如果需要扩展字符串,则使用字符 c 进行填充。如果当前字符串长度大于 n,则截断字符串,只保留前 n 个字符。
注意:resize 函数不会抛出异常,即使在内存分配失败的情况下,它也会静默地减少字符串的大小。
max_size:返回可容纳的最大字符数
由 std::string 所基于的 std::allocator 决定的,并且受到系统内存限制的影响。
类对象访问及遍历操作
是一个重载的运算符函数,它允许你使用方括号[]来访问字符串中的字符。这个运算符有两个版本:char& operator[] (size_t pos):
这是一个非常量版本,返回一个对字符串中指定位置pos的字符的引用。这意味着你可以通过这个引用来修改字符串中的字符。const char& operator[] (size_t pos) const:
这是一个常量版本,返回一个对字符串中指定位置pos的字符的常量引用。这意味着你可以读取字符串中的字符,但不能通过这个引用来修改它。
对比at,at是在还没[]运算符重载时出现的,同样具有查找和修改的功能。
区别于[],at会进行边界检查,通常慢一点,当出现边界问题时会抛异常。[]不会检查边界,出问题直接报错。at提供了更好的安全性和错误处理能力,可以帮助避免潜在的运行时错误。但在日常中索引值一般都确定,更加习惯于使用[]。
都返回一个迭代器
begin:指向容器的第一个元素
end:指向容器“结束”的位置,即最后一个元素之后的位置
rbegin:指向容器的最后一个元素,即反向遍历的开始位置
rend:指向容器第一个元素之前的位置,即反向遍历结束的位置
cbegin:返回一个常量迭代器,指向容器第一个元素,常量迭代器不允许修改容器中的元素
cend:返回一个常量迭代器,指向容器的“结束”位置。
crbegin:返回一个常量反向迭代器(const_reverse_iterator),其余和rbegin相同
crend:返回一个常量反向迭代器,其余和rend相同
string的三种遍历方式
void Teststring3()
{
string s("hello Bit");
// 3种遍历方式:
// 需要注意的以下三种方式除了遍历string对象,还可以遍历是修改string中的字符,
// 另外以下三种方式对于string而言,第一种使用最多
// 1. for+operator[]
for (size_t i = 0; i < s.size(); ++i)
cout << s[i] << " ";
cout << endl;
// 2.迭代器
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it <<" ";
++it;
}
cout << endl;
//string::reverse_iterator rit = s.rbegin();
//C++11之后,直接使用auto定义迭代器,让编译器推到迭代器的类型
auto rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit << " ";
rit++;
}
cout << endl;
// 3.范围for
for (auto& ch : s)
{
cout << ch << " ";
ch++;
}
cout << endl;
}
int main()
{
Teststring3();
return 0;
}
string类对象的修改操作
1.operator+=:重载的加法赋值运算符,用于将另一个字符串或字符串字面量追加到当前字符串的末尾。
2.append:将指定的字符串或字符数组追加到当前字符串的末尾
3.push_back:将单个字符追加到字符串的末尾
4.assign:将字符串的内容替换为指定的字符串或字符数组,会覆盖
5.insert:在字符串的指定位置插入另一个字符串或字符数组。头部插入存在数据挪动导致效率问题
6.erase:从字符串中删除指定范围的字符
7.replace:替换字符串中指定范围的字符为另一个字符串或字符数组
8.swap:交换当前字符串与另一个字符串的内容
9.pop_back删除字符串最后一个字符
各接口的具体实现细节可以在cplusplus.com官网上去查看。这里不一一列举,学习了解重点即可。
字符串操作
c_str() 函数用于获取一个 C++ 风格的字符串(C string),也就是以空字符(‘\0’)结尾的字符数组。返回一个指向内部数据的指针,该数据是一个以空字符终止的字符数组,即 C 风格的字符串。这个指针可以用来与需要 C 风格字符串的函数或库进行交互,使得可以在需要 C 风格字符串的上下文中使用 C++ 字符串。
重点分析:
1.查找字符串,pos参数指定开始查找位置,默认为0
2.查找C风格字符串
3.查找字符数组,pos为开始位置,n为要查找的字符数
4.查找单个字符,pos为开始位置默认为0,可以指定很方便
未找到都返回std::string::npos
用于从一个字符串对象中提取子字符串,并返回一个新的字符串对象,原始字符串不会被修改。
pos为开始查找位置,默认为0,len表示要提取的字串长度,默认为npos,是一个很大的值,若不自己给范围将提取到字符串末尾。
如果指定的 pos 超出了主字符串的有效范围,或者 len 为 0,则 substr 函数将返回一个空字符串。
这两个接口函数可以很好的实现查找和分割字符串,以获取一个网址的协议 域名 资源名为例。
string结构的说明
下述结构是在32位平台下进行验证,32位平台下指针占4个字节.
vs下string的结构
string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字符串的存储空间:
当字符串长度小于16时,使用内部固定的字符数组来存放
当字符串长度大于等于16时,从堆上开辟空间
这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量最后:还有一个指针做一些其他事情。故总共占16+4+4+4=28个字节。
g++下string的结构
G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:
空间总大小
字符串有效长度
引用计数
指向堆空间的指针,用来存储字符串
3.string类的模拟实现
实现细节已在代码中注释,主要实现常用接口以及相关操作
- string.h
#pragma once
#include<assert.h>
using namespace std;
namespace ee
{
class string
{
friend ostream& operator<<(ostream& out, const ee::string& s);
friend istream& operator>>(istream& in, ee::string& s);
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
//_str为指向字符串首元素地址的指针
return _str;
}
iterator end()
{
//_size隐式转换成指针类型
return _size+_str;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
//构造函数,从C风格字符串创建
string(const char* str="")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
//strcpy(_str, str);
//strcpy遇到空白符会终止拷贝,memcpy拷贝整个字符串包括'\0'
memcpy(_str, str, _size + 1);
}
//拷贝构造,接收一个对象的引用,复制现有来创建
string(const string&s)
{
_size = s._size;
_capacity = _size;
_str = new char[_size + 1];
//strcpy(_str, s._str);
memcpy(_str, s._str, _size + 1);
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//赋值运算符重载,传统写法
//string& operator=(const string& s)
//{
// if (this != &s)
// {
// char*tmp= new char[s._capacity + 1];
// //加1是为了把'\0'带上
// memcpy(tmp, s._str, s._size+1);
// delete[]_str;
// _str = tmp;
// _size = s._size;
// _capacity = s._capacity;
// }
// return *this;
//}
//过渡写法,在函数体内调用拷贝构造进行交换
//string& operator=(const string& s)
//{
// if (this != &s)
// {
// string tmp(s);
// //this->swap(tmp);
// swap(tmp);
// }
// return *this;
//}
//现代写法,运用std库中swap函数进行交换
string& operator=(string s)//传值传参
{//传参过程中s已经进行一次深拷贝
swap( s);
return*this;
}
//析构函数
~string()
{
_size = _capacity = 0;
delete[] _str;
_str = nullptr;
}
//将自定义字符串类对象转化为C风格字符串
const char* c_str()const
{
return _str;
}
//有效数据个数
size_t size()const
{
return _size;
}
//可读写
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
//只读
const char& operator[](size_t pos)const
{
assert(pos < _size);
return _str[pos];
}
//预存空间
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
//strcpy(tmp, _str);
memcpy(tmp, _str, _size + 1);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
//调整容量
void resize(size_t n,char ch='\0')
{
//分三种情况,n小于容量,等于和大于,统一开辟一块新空间扩容
if (n < _size)
{
_size = n;
_str[n] = '\0';
}
else
{
reserve(n+_size);
//拷贝原数据
for (size_t i = _size; i < _size+n; i++)
{
_str[i] = ch;
}
_size += n;
//最后一个位置手动赋值
_str[_size] = '\0';
}
}
void push_back(char ch)
{
//先检查容量
if (_size == _capacity)
{
//采取二倍扩容
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
//追加函数
void append(const char*str)
{
int len = strlen(str);
if (_size + len > _capacity)
{
//_size+len可能比二倍容量还大,至少扩这么多
reserve(_size + len);
}
//拷贝时从_size下一个位置开始,避免前面数据被覆盖
//strcpy(_str+_size, str);
memcpy(_str + _size, str, len + 1);
_size+= len;
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
void insert(size_t pos, size_t n, char ch)
{
assert(pos <= _size);
if (_size + n > _capacity)
{
reserve(_size + n);
}
//挪动数据
size_t end = _size;
while (end >= pos && end != npos)
{
_str[end + n] = _str[end];
end--;
}
for (size_t i = 0; i < n; i++)
{
_str[pos] = ch;
pos++;
}
_size += n;
}
void insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
//挪动数据
size_t end = _size;
while (end >= pos && end != npos)
{
_str[end + len] = _str[end];
end--;
}
//for (size_t i = 0; i < len; i++)
//{
//_str[pos + i] = str[i];
//}
//strcpy(_str + pos, str);
memcpy(_str + pos, str, len + 1);
_size += len;
}
void erase(size_t pos, size_t len = npos)
{//len给一个缺省值,代表默认完全删除
assert(pos <= _size);
//完全删除情况
if (npos == len || len + pos >= _size)
{
_str[pos] = '\0';
_size = pos;
}
//部分删除情况(取中间一段删除)
else
{
size_t end = len + pos;
while (end <= _size)
{//挪动的数据
_str[pos++] = _str[end++];
}
_size -= len;
}
}
//从pos位置开始查找
size_t find(char ch, size_t pos = 0)
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
//查找字符串
size_t find(const char* str, size_t pos=0)
{
assert(pos < _size);
//运用字符串中查找子字符串的函数
const char* ptr = strstr(_str + pos, str);
if (ptr)
{
//返回子字符串在原始字符串中的位置
//若直接返回ptr是指向子字符串的一个指针,而不是索引
return ptr - _str;
}
else
{
return npos;
}
}
string substr(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
size_t n = len;
//要查找的子字符串就是原字符串的情况
if (pos + len > _size|| len == npos)
{//第一个条件判断输入len是否超出长度,第二个条件判断是否输入len
//为避免栈溢出应将len==npos放到第一个判断条件,若没有传入len的话就不用判断后条件
n = _size - pos;
}
//返回一个新字符串
string tmp;
tmp.reserve(n);
for (size_t i = pos; i < pos+n; i++)
{
tmp += _str[i];
}
return tmp;
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
//手撕实现
//bool operator<(const string& s)
//{
// size_t i1 = 0;
// size_t i2 = 0;
// while (i1 < _size && i2 < _size)
// {
// if (_str[i1] <s. _str[i2])
// {
// return true;
// }
// else
// {
// return false;
// }
// }
// // "hello" "hello" false
// // "helloxx" "hello" false
// // "hello" "helloxx" true
// //存在三种情况需要判断
// if (i1 == _size && i2 != _size)
// {
// return true;
// }
// else
// {
// return false;
// }
//}
//运用库函数实现
bool operator<(const string& s)const
{//memcpy不检查空字符,strcmp遇到'\0'就停止比较
//指定比较小字符串的长度,指定大的会越界
int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
return ret == 0 ? _size < s._size : ret < 0;
//当ret==0时,比较部分相等,长的大
//当memcpy返回负值ret<0,返回true,memcpy返回正值,
// 与条件判断语句第三部分ret<0相反,返回false
}
bool operator ==(const string&s)const
{
return _size == s._size
&& memcmp(_str, s._str, _size) == 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);
}
bool operator!=(const string& s) const
{
return !(*this == s);
}
private:
size_t _size;
size_t _capacity;
char* _str;
public:
const static size_t npos;
};
const size_t string::npos = -1;
//istream和ostream都是std库中的成员,使用需要加上命名空间(若没展开)
ostream& operator << (ostream& out, const string& s)
{//ostream在库中定义是防拷贝的,得用引用
/*for (size_t i = 0; i < s.size(); i++)
{
//这里单个字符的打印就不需要友元去访问类的私有成员
out << s[i];
}*/
for ( auto ch :s)//auto出来没有const属性,需手动添加
{
out << ch;
}
return out;
}
istream& operator >> (istream& in, string& s)
{
s.clear();
//>>流提取运算符和scanf一样,会跳过前导空白符,读取过程中
// 遇到空格和换行符会终止
char ch = in.get();//get每次读取一个字符
while (ch == ' ' || ch == '\n')//循环条件为了跳过前导空白字符
{
ch = in.get();
}
//创建临时数组来接收字符,避免频繁开辟空间
char buff[128];
int i = 0;
while (ch != ' ' && ch != '\n')
{//改变条件可以跳过作为结束标志的字符
buff[i++] = ch;
if (i == 127)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
//索引i没到127不需要扩容直接跳出循环
//没有将buff中临时变量提取到s中去
if (i != 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
};
- 测试test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
#include"string.h"
using namespace std;
//测试:构造+迭代器
void teststring1()
{
ee::string s1("keep going");
cout << s1.c_str() << endl;
ee::string s2;//为空即\0,遇到\0直接不打印
cout << s2.c_str() << endl;
//遍历方式
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1[i] << " ";
}
cout << endl;
//修改值
/*for (size_t i = 0; i < s1.size(); i++)
{
s1[i]++;
}
cout<< endl;*///还没重载<<运算符
ee::string s3("keep going");
auto it = s3.begin();
while (it != s3.end())
{
(*it)++;
cout << *it << " ";
it++;
}
cout << endl;
//范围for
for(auto ch:s3)
{
cout << ch << " ";
}
cout << endl;
}
//测试:push_back/append
void teststring2()
{
ee::string s1("keep going");
cout << s1.c_str() << endl;
s1.push_back('!');
s1.append(" move on");
cout << s1.c_str() << endl;
}
//测试:insert/erase
void teststring3()
{
ee::string s1("keepgoing");
s1.insert(9, "****");//字符串末尾默认为'\0',*后面的字符无法打印
cout << s1.c_str() << endl;
s1.insert(4, 6, '!');
cout << s1.c_str() << endl;
//s1.erase(s1.begin(), s1.end());
s1.erase(4,6);
cout << s1.c_str() << endl;
s1.erase(1, 30);
cout << s1.c_str() << endl;
}
//测试:substr
void teststring4()
{
ee::string url = "ftp://www.baidu.com/?tn=65081411_1_oem_dg";
size_t pos1 = url.find("://");
if (pos1 != ee::string::npos)
{
ee::string protocol = url.substr(0, pos1);
cout << protocol.c_str() << endl;
}
size_t pos2 = url.find('/', pos1 + 3);
if (pos2 != ee::string::npos)
{
ee::string domain = url.substr(pos1 + 3, pos2 - (pos1 + 3));
ee::string uri = url.substr(pos2 + 1);
cout << domain.c_str() << endl;
cout << uri.c_str() << endl;
}
}
//测试:resize
void teststring5()
{
ee::string s("hellow world");
s.resize(8);
cout << s.c_str() << endl;
s.resize(15, 'i');
cout << s.c_str() << endl;
s.resize(11);
cout << s.c_str() << endl;
}
//测试:+=运算符
void teststring6()
{
// c的字符数组,以\0为终止算长度
// string不看\0,以size为终止算长度
ee::string s("hello world");
s += '\0';
s += "6666666";
cout << s.c_str() << endl;
cout << s << endl;
}
//测试:流插入,流提取
void teststring7()
{
ee::string s;
cin >> s;
cout << s << endl;
}
//测试:关系运算符
void teststring8()
{
ee::string s1("hello");
ee::string s2("hello");
cout << (s1 < s2) << endl;
cout << (s1 > s2) << endl;
cout << (s1 == s2) << endl<<endl;
ee::string s3("hello");
ee::string s4("helloxxx");
cout << (s3 < s4) << endl;
cout << (s3 > s4) << endl;
cout << (s3 == s4) << endl<<endl;
ee::string s5("helloxxx");
ee::string s6("hello");
cout << (s5 < s6) << endl;
cout << (s5 > s6) << endl;
cout << (s5 == s6) << endl << endl;
}
//测试:赋值运算符重载
void teststring9()
{
ee::string s1("hello");
ee::string s2(s1);
cout << s1 << endl;
cout << s2 << endl;
ee::string s3("xxxxxxxxxxxxx");
s1 = s3;
cout << s1 << endl;
cout << s3 << endl;
}
int main()
{
teststring9();
return 0;
}