目录
基础知识
常用接口
1>常见构造
2>容量操作
3>访问及遍历操作
1.迭代器
2.反向迭代器
3.范围for
4.auto
4>修改操作
5>非成员函数
其它接口
模拟实现
string.h
string.cpp
swap()
基础知识
string是一个管理字符的类,定义在std命名空间的。当我们使用using namespace std;后,就可以直接使用string类,而不用每次在string前加上std:: 前缀。这样能减少代码的冗余,提高代码的可读性,但是有可能会引起命名冲突
#include<string>
// 第一种
std::string mystr1;
// 第二种
using namespace std;
string mystr2;
所以我们在使用string类时,必须包含#include头文件以及using namespace std;(根据具体情况而定)
vs下的string结构:总共占28个字节,内部结构有
① 一个联合体,用来定义string中字符串的存储空间,当字符串长度小于16时,使用内部固定的字符数组来存放;当当字符串长度大于等于16时,从堆上开辟空间(大多数情况下字符串的长度都小于16,那么string对象创建好后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高)
② 一个size_t字段保存字符串长度
③ 一个size_t字段保存从堆上开辟空间总的容量
④ 一个指针
所以总共占16+4+4+4=28个字节
g++下string的结构:总共占4个字节,内部只包含一个指针,该指针将来指向一块堆空间,内部包含了三个字段:① 空间总大小 ② 字符串有效长度 ③ 引用计数
常用接口
string类的接口有很多,下面我就介绍一些常用的接口,大家可以配合C++官网的介绍来看下面的内容,因为C++官网只有英文,看起来会有点吃力
string - C++ Reference
1>常见构造
函数名称 | 功能说明 |
---|---|
string() | 构造空的string类对象,即空字符串 |
string(const char* s) | 用C-string来构造string类对象 |
string(const string& s) | 拷贝构造函数 |
string(size_t n, char c) | ring类对象中包含n个字符c |
int main()
{
// 重点
string s1;
string s2("hello tianci");
string s3(s2);
string s4(10, 'c');
string s5("aabb", 2);
string s5(s3, 2, 3);
string s6(s3, 4);
return 0;
}
2>容量操作
函数名称 | 功能说明 |
---|---|
size | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty | 检测字符串释放为空串,是返回true,否则返回false |
clear | 清空有效字符 |
reserve | 为字符串预留空间 |
resize | 将有效字符的个数改成n个,多出的空间用字符c填充 |
int main()
{
string s("hello tianci");
cout << s.size() << endl;
cout << s.length() << endl;
cout << s.capacity() << endl;
s.resize(10);
cout << s << endl;
s.resize(12, 'w');
cout << s << endl;
s.reserve(100);
cout << s.capacity() << endl;
s.clear();
cout << s.empty() << endl;
return 0;
}
这里有几点需要注意:
① size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,建议使用size()
② clear()只是将string中有效字符清空,不改变底层空间大小
③ resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间(resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变)
④ reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserve不会改变容量大小
3>访问及遍历操作
函数名称 | 功能说明 |
---|---|
operator[] | 返回pos位置的字符,const string类对象调用 |
begin+end | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
rbegin+rend | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
范围for | C++11支持更简洁的范围for的新遍历方式 |
int main()
{
string s("hello tianci");
for (int i = 0; i < s.size(); i++)
{
cout << s[i];
}
cout << endl;
string::iterator it1 = s.begin();
while (it1 != s.end())
{
cout << *it1;
++it1;
}
cout << endl;
string::reverse_iterator it2 = s.rbegin();
while (it2 != s.rend())
{
cout << *it2;
++it2;
}
cout << endl;
for (char ch : s)
{
cout << ch;
}
return 0;
}
1.迭代器
迭代器是一种用于遍历容器(如数组、链表、向量等)中元素的对象,就像是一个指向容器中某个元素的“指针”,还可以通过迭代器来读取、修改元素,以及在容器中移动位置
这里我就简单介绍一下迭代器的使用,因为底层比较复杂,往后再详细介绍
int main()
{
string s1("hello tianci");
string::iterator it1 = s1.begin();
while(it1 != s1.end())
{
cout << (*it1);
++it1;
}
const string s2("hello tianci");
string::const_iterator it2 = s2.begin();
return 0;
}
这里有几点需要注意的是:
① string::iterator是C++中string类的一个成员类型,并不是一个类内部的类,而是一个类型别名,用于表示string对象的迭代器
② 这里用 < 替换 != 也可以,但是建议使用 != ,因为例如链表的存储结构不是连续的,所以就不能用 < ,而 != 可以通用
③可以这样理解, s.begin()指向字符串的首个有效字符,即指向'h',s.end()指向字符串最后一位有效字符的下一位,即指向'\0',("hello tianci"在内存中是以'\0'结尾的),所以s.begin()到s.end()是一个左闭右开的区间
2.反向迭代器
顾名思义,就是指从后往前迭代,所以s.rbegin()指向'i',s.rend()指向'h'的前一位,依然是一个左闭右开的区间,只不过是方向相反
int main()
{
string s3("hello tianci");
string::reverse_iterator it3 = s3.rbegin();
while(it3 != s3.rend())
{
cout << (*it3);
++it3;
}
const string s4("hello tianci");
string::const_reverse_iterator it4 = s4.rbegin();
return 0;
}
迭代器共有四种类型,在使用的时候需要判断对象的类型以及顺序
3.范围for
范围for是C++11的一个小语法
① 原理:范围for底层是迭代器
② 适用于容器遍历和数组遍历
③ 自动取容器的数据赋值给左边的对象
④ 自动++,自动判断结束
int main()
{
int arr[] = {1,2,3,4};
string s("hello tianci");
for (int i : arr)
{
cout << i << ' ';
}
cout << endl;
for (char ch: s)
{
cout << ch;
}
return 0;
}
需要注意的是,范围for是自动取容器的数据赋值给左边的对象,那就意味着依次取右边的值拷贝给左边的对象,所以在左边的代码中,ch的改变并不会影响原来的数据,而使用引用才会改变原来的数据
总结:对于遍历string类对象有三种方式:① 下标 + [] 的形式 ② 迭代器(通用) ③ 范围for
在这里我要介绍另一个C++11的小语法
4.auto
① C++11中,auto作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得
② 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
③ 当在同一行声明多个变量时,这些变量必须是相同的类型,因为编译器实际只对第一个类型进行推导
④ auto可以作为函数的参数(C++20开始支持),也可以做返回值(谨慎使用,用不好很坑人)
⑤ auto不能直接用来声明数组
int main()
{
std::map<std::string, std::string> dict;
//std::map<std::string, std::string>::iterator dit = dict.begin();
auto dit = dict.begin();
return 0;
}
auto可以简化代码,替代写起来长的类型,所以auto使用起来会特别的香,但是使用时要注意一下细节
4>修改操作
函数名称 | 功能说明 |
---|---|
push_back | 在字符串后尾插字符c |
append | 在字符串后追加一个字符串 |
operator+= | 在字符串后追加字符串str |
c_str | 返回C格式字符串 |
find+npos | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
int main()
{
string s("hello");
s.push_back(',');
cout << s.c_str() << endl;
s.append("tianci");
cout << s.c_str() << endl;
s += '!';
cout << s.c_str() << endl;
s += "!!";
cout << s.c_str() << endl;
int pos1 = s.find('l');
int pos2 = s.find('l', 4);
int pos3 = s.rfind('o');
cout << pos1 << endl;
cout << pos2 << endl;
cout << pos3 << endl;
string sub1 = s.substr(6, 4);
cout << sub1.c_str() << endl;
string sub2 = s.substr(6);
cout << sub2.c_str() << endl;
return 0;
}
在这里我想提一点,在string类中,npos是一个静态常量成员,定义为static const size_t npos = -1; 通常用于表示一个“无效”或“最大可能”的位置值
① 在find()中,如果没有找到匹配的字符,通常会返回npos,此时npos作为一个无效的值,就可以让我们快速知道是否找到
② 在substr()中,npos赋值给缺省参数,此时npos作为一个最大可能的值,相当于截取到字符串的末尾
另外对string操作时,如果可以预估要放多少字符,可以先通过reserve预留空间,这样做可以提高效率。在string尾部追加字符时,建议用+=,它可以连接单个字符或字符串,看起来也比较简洁
5>非成员函数
函数名称 | 功能说明 |
---|---|
operator+ | 尽量少用,因为传值返回,导致深拷贝效率低 |
operator>> | 输入运算符重载 |
operator<< | 输出运算符重载 |
getline | 获取一行字符串 |
relational operators | 大小比较 |
int main()
{
string s1;
string s2("tianci");
s1 = "hello " + s2;
cout << s1.c_str() << endl;
cout << (s1 == s2) << endl;
cout << (s1 != s2) << endl;
cout << (s1 <= s2) << endl;
cout << (s1 >= s2) << endl;
cout << (s1 < s2) << endl;
cout << (s1 > s2) << endl;
string s3;
cin >> s3;
cout << s3 << endl;
string s4;
getline(cin, s4);
cout << s4 << endl;
string s5;
getline(cin, s4, '#');
cout << s5 << endl;
return 0;
}
这里有一个非常重要的点,那就是cin读取时,遇到空格或'\n'就会停止,因为它认为空格是分隔符的意思;而getline读取时,遇到空格并不会停止,而是遇到字符(不给的时候就用缺省值'\n')才会停止,如果给了字符,则遇到该字符才会停止读取
getline() 可能在一些OJ题要用到,比如要读取一个字符串,但不想遇到空格就停止,这时候getline() 就可以完美解决这个问题
另外我想介绍一个关于cout的点
int main()
{
const char* p1 = "aabb";
int* p2 = nullptr;
cout << p1 << endl; // aabb
cout << p2 << endl; // 00000000
return 0;
}
如果用cout打印p1,因为p1是常量,它会自动解引用从而打印字符串的内容,而不是打印它的地址;p2就不会这样
下面有两个方法可以解决
cout << (void*)p1 << endl; // 00FA9B30
printf("%p", p1); // 00FA9B30
其它接口
下面介绍一些可能会遇到的接口
int main()
{
string s1;
// 最多能放多少字符,不同平台结果可能不一样
cout << s1.max_size() << endl;
string s2("hello tianci");
// 正向查找在原字符串中第一个与指定字符串(或字符)中
// 的某个字符匹配的字符,返回它的位置
cout << s2.find_first_of("aoc") << endl; // 第一个出现的o,所以返回下标4
// 从第六个位置开始往后找
cout << s2.find_first_of("aoc", 6) << endl; // 第一个出现的是a,所以返回下标8
string s3("hello tianci");
// 反向查找
cout << s3.find_last_of("aoc") << endl; // 第一个出现的是c,所以返回下标10
// 从第六个位置开始往前找
cout << s3.find_last_of("aoc", 6) << endl; // 第一个出现的是o,所以返回下标4
// 在下标为0位置上插入字符c
string s4("hello");
s4.insert(0, "h"); // hhello
// 在下标为3位置上插入字符串str
string s5("hello");
s5.insert(3, "!!!"); // hel!!!lo
// 交换
s4.swap(s5);
// 删除下标为1位置开始的1个字符
string s6("hello");
s6.erase(1, 1); // hllo
// 翻转字符串
string s7("hello");
reverse(s7.begin(), s7.end()); // olleh
// 从下标为3位置开始的1个字符替换成%%
string s8("tianci");
s8.replace(3, 1, "%%"); // tia%%ci
// to_string的功能是将数据转换为字符串
return 0;
}
这里有几点需要注意的是:
① find_first_of() 是从pos位置往后找,find_last_of() 是从pos位置往前找,返回的是特定字符串第一个出现在原字符串的字符的位置
② insert() 和 erase() 一般不使用,因为插入后还要移动数据,效率很低
③ reverse就是实现顺序逆置(比如1234,reverse后4321),它是一个函数模版,所以vector和list等都可以用
④ replace() 建议平替的时候使用,如果一换多个字符意味着又要移动数据,效率比较低
关于接口的介绍到这里就结束啦,如果大家遇到其它接口,可以尝试在C++官网读懂这个接口的作用,可以先猜再配合翻译软件理解这个接口实现的功能
模拟实现
string.h
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace tianci
{
class string
{
public:
string(const char* str = "");
string(const string& s);
//string& operator=(const string& s);
string& operator=(string s);
~string();
// iterator
using iterator = char*;
using const_iterator = const char*;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
// access
char& operator[](size_t index)
{
assert(index < _size);
return _str[index];
}
const char& operator[](size_t index)const
{
assert(index < _size);
return _str[index];
}
// modify
void push_back(char c);
string& operator+=(char c);
void append(const char* str);
string& operator+=(const char* str);
void clear();
void swap(string& s);
const char* c_str()const;
// capacity
size_t size()const;
size_t capacity()const;
bool empty()const;
void resize(size_t n, char c = '\0');
void reserve(size_t n);
// 返回c在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const;
// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const;
// 在pos位置上插入字符c/字符串str,并返回该字符的位置
void insert(size_t pos, char c);
void insert(size_t pos, const char* str);
// 删除pos位置上的元素,并返回该元素的下一个位置
void erase(size_t pos, size_t len);
string substr(size_t pos, size_t len = npos);
void swap(string& s);
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
public:
static const size_t npos;
};
void swap(string& s1, string& s2);
bool operator== (const string& lhs, const string& rhs);
bool operator!= (const string& lhs, const string& rhs);
bool operator> (const string& lhs, const string& rhs);
bool operator< (const string& lhs, const string& rhs);
bool operator>= (const string& lhs, const string& rhs);
bool operator<= (const string& lhs, const string& rhs);
ostream& operator<<(ostream& os, const string& str);
istream& operator>>(istream& is, string& str);
istream& getline(istream& is, string& str, char delim = '\n');
}
关于string.h有一点就是static修饰的成员变量要在类外面定义,但如果是加了const 的成员变量可以在声明处给缺省值,就不用再到类外面定义(建议还是类内声明,类外定义,这个只是例外)
string.cpp
#include"string.h"
namespace tianci
{
const size_t string::npos = -1;
string::string(const char* str)
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_size + 1];
strcpy(_str, str);
}
// 传统写法
//string::string(const string& s)
//{
// _str = new char[s._capacity + 1];
// strcpy(_str, s._str);
// _size = s._size;
// _capacity = s._capacity;
//}
//
// 现代写法
string::string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
// 传统写法
//string& string::operator=(const string& s)
//{
// if (this != &s)
// {
// delete[] _str;
// _str = new char[s._capacity + 1];
// strcpy(_str, s._str);
// _size = s._size;
// _capacity = s._capacity;
// }
// return *this;
//}
//
string& string::operator=(string s)
{
swap(s);
return *this;
}
string::~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
void string::push_back(char c)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size++] = c;
}
string& string::operator+=(char c)
{
push_back(c);
return *this;
}
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
size_t newcapacity = 2 * _capacity;
if (_size + len > newcapacity)
newcapacity = _size + len;
reserve(newcapacity);
}
strcpy(_str + _size, str);
_size += len;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
void string::clear()
{
_str[0] = '\0';
_size = 0;
}
void string::swap(string& s)
{
tianci::string tmp = s;
s = *this;
*this = tmp;
}
const char* string::c_str()const
{
return _str;
}
size_t string::size()const
{
return _size;
}
size_t string::capacity()const
{
return _capacity;
}
bool string::empty()const
{
return _size == 0 ? true : false;
}
void string::resize(size_t n, char c)
{
if (n > _size)
{
if (n > _capacity)
{
reserve(n);
}
for (size_t i = 0; i < n - _size; i++)
{
_str[_size + i] = c;
}
_size = n;
}
else
{
_str[n] = c;
_size = n;
}
}
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
size_t string::find(char c, size_t pos) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == c)
return i;
}
return npos;
}
size_t string::find(const char* s, size_t pos) const
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, s);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
void string::insert(size_t pos, char c)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = c;
++_size;
}
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
size_t newcapacity = 2 * _capacity;
if (_size + len > newcapacity)
{
newcapacity = _size + len;
reserve(newcapacity);
}
_capacity = newcapacity;
}
size_t end = _size;
while (end > pos - 1)
{
_str[end + len] = _str[end];
--end;
}
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;
}
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
size_t begin = pos;
while (begin <= _size - len)
{
_str[begin] = _str[begin + len];
++begin;
}
_size -= len;
}
}
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
// 大于后面剩余串的长度,则直接取到结尾
if (len > (_size - pos))
{
len = _size - pos;
}
tianci::string sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++)
{
sub += _str[pos + i];
}
return sub;
}
void string::swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 成员函数
// 全局函数
void swap(string& s1, string& s2)
{
s1.swap(s2);
}
bool operator== (const string& lhs, const string& rhs)
{
return strcmp(lhs.c_str(), rhs.c_str()) == 0;
}
bool operator!= (const string& lhs, const string& rhs)
{
return !(lhs == rhs);
}
bool operator> (const string& lhs, const string& rhs)
{
return !(lhs <= rhs);
}
bool operator< (const string& lhs, const string& rhs)
{
return strcmp(lhs.c_str(), rhs.c_str()) < 0;
}
bool operator>= (const string& lhs, const string& rhs)
{
return !(lhs < rhs);
}
bool operator<= (const string& lhs, const string& rhs)
{
return lhs < rhs || lhs == rhs;
}
ostream& operator<<(ostream& os, const string& str)
{
for (size_t i = 0; i < str.size(); i++)
{
os << str[i];
}
return os;
}
istream& operator>>(istream& is, string& str)
{
str.clear();
int i = 0;
char buff[256];
char ch;
ch = is.get();
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 255)
{
buff[i] = '\0';
str += buff;
i = 0;
}
ch = is.get();
}
if (i > 0)
{
buff[i] = '\0';
str += buff;
}
return is;
}
istream& getline(istream& is, string& str, char delim)
{
str.clear();
int i = 0;
char buff[256];
char ch;
ch = is.get();
while (ch != delim)
{
buff[i++] = ch;
if (i == 255)
{
buff[i] = '\0';
str += buff;
i = 0;
}
ch = is.get();
}
if (i > 0)
{
buff[i] = '\0';
str += buff;
}
return is;
}
}
关于string.cpp的点就是,拷贝构造和赋值操作的现代写法巧妙地运用函数的this指针参数,还有就是出了作用域就会自动调用析构函数,不会担心内存泄漏,这些都是类和对象的知识点,其中一些函数实现成全局函数也是因为this指针,导致函数的参数固定,所以实现成全局函数会比较理想
swap()
最后再介绍一下swap函数,之所以我把它单拎出来,也是因为它比较重要
① 算法库swap:可以看到算法库中的swap是先拷贝构造一个对象,再去赋值,而赋值又要调用拷贝构造,后面还要调用析构函数,这样操作的代价有点大,效率特别低
② string类swap:string类的swap本质就是单纯调换指针的指向,并不需要再构造一个对象出来,代价相对小很多,效率也提高不少
之所以string类实现一个成员函数和一个全局函数,那是因为即使swap(a, b)这样写,也可以转为a.swap(b),所以全局函数的底层还是调用成员函数swap实现的,不仅string类,vector和list都实现了两个swap函数去交换,因为调用算法库中的swap函数代价实在是太大了
本篇文章到这里就结束啦,希望这些内容对大家有所帮助!
下篇文章见,希望大家多多来支持一下!
感谢大家的三连支持!