本文主要介绍了string的成员变量、auto和范围for、string常用接口的说明、OJ题目、string的模拟实现,内容全由作者原创(无AI),并带有配图帮助博友们更好的理解,点个关注不迷路,下面进入正文~~
目录
[3.4.1下标 + 访问](#3.4.1下标 + [ ]访问)
前言:为什么要使用string?
C语言中,字符串是指'\0'结尾的一串字符,C语言标准库提供了一系列操作字符串的库函数,但是使用起来还是不方便,在操作时会存在越界的风险。为了更方便,更快捷的使用字符串,在C++引入了string类的概念,string本质上就是类,这个类里面存储了有关于字符串的成员变量和一系列操作字符串的函数,可以更好的使用。
1.string的成员变量
在了解一个类时,我们最先关注的应是这个类的成员变量。C++标准并没有强制规定成员变量,在VS和g++(Linux)下,string的成员变量有所不同
1.1VS下string的成员变量
cpp
struct _String_internal
{
// 1. 联合体:区分短串/长串
union _Union
{
char _local_buf[15]; // 短字符串缓冲区(15字节)
char* _ptr; // 长字符串:指向堆内存
} _u;
// 2. 字符串实际长度
size_t _length;
// 3. 总容量(包含结束符)
size_t _capacity;
};
组成string的成员变量一共有四个:
以下是在32位平台下验证
1.联合体(为了减小堆内存的使用,在使用短字符(字符串长度小于16)时,会存储在栈上的数据,当字符长度较大时,再在堆上申请空间)
2.length/size,当前有效字符的个数
3.capacity,已分配的内存容量
4.还有一个指针做一些其他事情
注意:'\0'不算有效字符的个数和已分配的内存容量。
1.2g++下string的成员变量
g++下,string是通过拷贝实现的,string对象总共占4个字节,内部只有一个指针,指针指向一块堆空间,里面存有如下字段:
1.已分配的内存容量
2.当前有效字符的个数
3.引用计数(后面会讲)
4.指向堆空间的指针,用来存储字符串
2.auto和范围for
2.1auto
在C++11中,auto是一个类型指示符,它可以通过传入的值来推导值的类型。例如auto a = 1;编译器就会自动判断a的类型是整形。
使用auto需要注意的事项:
1.当auto声明指针类型时,用auto和auto*没有区别,但是声明引用类型时,需要使用auto&。
2.当用auto在声明多个变量时,这多个变量必须要是同一类型,否则编译器会报错。因为auto只会推导第一个数据的类型,然后用推导出来的类型定义其他变量。
3.auto不能作为函数的参数,但是可以作为函数的返回值,不过要谨慎使用,因为频繁的使用auto做返回值会大大降低代码的可读性。
4.auto不能直接用来声明数组
使用auto的优势:在声明一个类型名很长的数据时,使用auto会很方便,后面会有例子展示。
下面时使用auto的代码:
cpp
int func1()
{
return 10;
}
// 不能做参数
//void func0(auto a = 0)
//{}
auto func2()
{
//...
return func1();
}
auto func3()
{
//...
return func2();
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = func1();
// 编译报错:rror C3531: "e": 类型包含"auto"的符号必须具有初始值设定项
//auto e;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto array[] = { 4, 5, 6 };
auto ret = func3();
//用auto作返回值会降低代码的可读性
int x = 10;
auto y = &x;
auto* z = &x;
auto& m = x;
cout << typeid(x).name() << endl;
cout << typeid(y).name() << endl;
cout << typeid(z).name() << endl;
auto aa = 1, bb = 2;
// 编译报错:error C3538: 在声明符列表中,"auto"必须始终推导为同一类型
//auto cc = 3, dd = 4.0;
return 0;
}
2.2范围for
对于一个有范围的集合而言,让程序员自己判断这个集合的范围是多余的,有时还很容易犯错误。因此,在C++11引入了基于范围的for循环。for循环的括号里由冒号" ;"分为两部分,第一部分是范围内用于迭代的变量,第二部分是表示迭代的范围。自动迭代,自动取数据,自动判断结束。
范围for可以作用到数组和容器对象上进行遍历。
范围for的底层就是用迭代器进行遍历。
下面是使用范围for的代码:
cpp
int array[] = { 1, 2, 3, 4, 5 };
// C++98的遍历
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
array[i] *= 2;
}
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
cout << array[i] << endl;
}
// 范围for适用于容器和数组
// C++11的遍历
//修改数组的值需要传引用,否则就是传值调用
for (auto& e : array)
e *= 2;
for (auto e : array)
cout << e << " " << endl;
这里就可以很好的使用auto判断容器数据的类型。
需要特别注意的是修改数组的值需要传引用,否则就是传值调用。
3.string常用接口的说明
3.1制胜法宝
请点击制胜法宝->制胜法宝
当我们了解完制胜法宝,我们就可以很好的掌握string。
3.2string类的常见构造
|------------------------------|-----------------------|
| construct函数名称 | 功能说明 |
| string() (重点) | 构造空的string类对象,即空字符串 |
| string(const char* s) (重点) | 用C-string来构造string类对象 |
| string(size_t n, char c) | string类对象中包含n个字符c |
| string(const string&s) (重点) | 拷贝构造函数 |

其实string的构造函数有很多种,但是常见就表格这几种,如果有需要使用到其他构造函数,再来查也不迟。
下面是常见构造函数的使用方法:
cpp
void Teststring()
{
string s1;//构造空的string类对象s1
string s2("hello");//用C格式字符串构造string类对象s2
string s3(s2);//拷贝构造s3
}
3.3string类对象的容量操作
|--------------|------------------------------|
| 函数名称 | 功能说明 |
| size(重点) | 返回字符串有效字符长度 |
| length | 返回字符串有效字符长度 |
| capacity | 返回空间总大小 |
| empty (重点) | 检测字符串释放为空串,是返回true,否则返回false |
| clear (重点) | 清空有效字符 |
| reserve (重点) | 为字符串预留空间 |
| resize (重点) | 将有效字符的个数该成n个,多出的空间用字符c填充 |
注意:
-
size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
-
clear()只是将string中有效字符清空,不改变底层空间大小。
-
resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用 0 来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
-
reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。
3.4.string类对象的访问及遍历操作
遍历string类一共有三种方法:
1.下标 +
2.迭代器
3.范围for
|-------------------|-------------------------------------------|
| 函数名称 | 功能说明 |
| operator\[\] (重点) | 返回pos位置的字符,const string类对象调用 |
| begin+ end | begin获取第一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
| rbegin + rend | rbegin获取最后一个字符的迭代器 + rend获取第一个字符前一个位置的迭代器 |
| 范围for | C++11支持更简洁的范围for的新遍历方式 |
3.4.1下标 + 访问
在我们以前学习数组的时候,经常会使用方括号访问,string也类似于数组,也可以使用方括号进行访问。但string的方括号并不是直接使用,而是运算符重载出来的。
cpp
// string::operator[]
#include <iostream>
#include <string>
int main ()
{
std::string str ("Test string");
for (int i=0; i<str.length(); ++i)
{
std::cout << str[i];
}
return 0;
3.4.2迭代器访问
在使用迭代器访问之前,我们先要了解迭代器是什么。
迭代器本质上是一个类,里面封装关于指针的操作,我们可以像指针一样使用迭代器。迭代器的关键字是iterator,在string访问iterator要写出string::iterator。
使用begin()可以获得指向首元素的迭代器,使用end()可以获得指向最后一个元素的下一个位置的迭代器,因为要保持数据的范围是作弊又开。
这样我们就可以写出用迭代器访问的代码:
cpp
// 2、迭代器
//string::iterator it = s2.begin();
auto it = s2.begin();
while (it != s2.end())
{
cout << *it;
it++;
}
cout << endl;
这里使用string::iterator有点麻烦,后面遇到名字更长的迭代器会更加麻烦,这时使用auto的好处就体现出来了,因为auto可以自动推导类型。
上面讲的是正向迭代器,除此之外还有一个反向迭代器,关键字是reverse_iterator。
使用rbegin()可以获得指向最后一个元素的迭代器,使用rend()可以获得指向第一个位置的前一个位置的迭代器。
在这里it++并不是物理空间意义上的向前移动,可以理解为是向后移动,因为要从后向前访问,这里是需要注意的地方。
这样我们就可以写出用反向迭代器访问的代码:
cpp
//string::reverse_iterator rit = s2.rbegin();
auto rit = s2.rbegin();
while (rit != s2.rend())
{
cout << *rit;
rit++;
}
cout << endl;
除了正向和反向迭代器之外,还有const_iterator和const_reverse_iterator,其作用是只能访问数据,不能修改数据。不能直接写const iterator,这样写是指针的指向不能改变,不符合我们的需求。
3.4.3范围for访问
范围for的底层其实就是用迭代器实现的,相比于迭代器访问,范围for的优势就是写起来更方便。
cpp
// 3、字符赋值,自动迭代,自动判断结束
// 底层就是迭代器
for (auto ch : s2)
{
cout << ch;
}
3.5string类对象的修改操作
|-----------------|----------------------------------|
| 函数名称 | 功能说明 |
| push_back | 在字符串后尾插字符c |
| append | 在字符串后追加一个字符串 |
| operator+= (重点) | 在字符串后追加字符串str |
| c_str(重点) | 返回C格式字符串 |
| find + npos(重点) | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的 位置 |
| rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的 位置 |
| substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
这里我们只需要掌握标有重点的知识点即可。
比如说operator+=已经可以实现追加字符和字符串的功能,push_back和append就不是很有必要。在实际使用中,也是operator+=使用的比较多。
c_str的作用就是返回底层数组C格式字符串,这样做是为了兼容C语言里的字符串操作函数,比说说strcmp...直接使用sting类是无法使用C语言里的字符串操作函数的。
下面是使用find的语法:

除了上面的函数外,还有insert和erase这样的修改操作函数,这里不做过多讲解,当然也比较重要。下面是使用了一些修改操作函数的代码:
cpp
void test_string2()
{
string s("hello world");
s.push_back(' ');
s.push_back('x');
s.append("yyyyyy");
cout << s << endl;
s += ' ';
s += "33333333333";
cout << s << endl;
s.insert(0, "hello bit ");
cout << s << endl;
s.insert(10, "zzzz");
cout << s << endl;
s.insert(0, "p");
cout << s << endl;
char ch = 't';
s.insert(0, 5, ch);
s.insert(s.begin(), ch);
cout << s << endl;
}
还需要注意的是,我们可以提前预估string需要存储字符串的大小,可以使用reserve提前把空间开好。
3.6string类非成员函数
|---------------------------|----------------------|
| 函数 | 功能说明 |
| operator+ | 尽量少用,因为传值返回,导致深拷贝效率低 |
| operator>> (重点) | 输入运算符重载 |
| operator<< (重点) | 输出运算符重载 |
| getline (重点) | 获取一行字符串 |
| relational operators (重点) | 大小比较 |
| swap | 交换两个字符串的值 |
这些几个接口大小可以了解一下。string类中还有 一些其他的操作,这里不一一列举,大家在需要用到时不明白了查文档即可。
4.小试牛刀
4.1找字符串中第一个只出现一次的字符

cpp
class Solution {
public:
int firstUniqChar(string s) {
int count[26] = {0};
for(auto ch:s)
{
count[ch-'a']++;
}
for(size_t i = 0;i <s.size();i++)
{
if(count[s[i]-'a'] == 1)
{
return i;
}
}
return -1;
}
};
4.2仅仅反转字母

cpp
class Solution {
public:
bool isletter(char ch)
{
if('a'<=ch&&ch<='z'||'A'<=ch&&ch<='Z')
{
return true;
}
return false;
}
string reverseOnlyLetters(string s) {
int left = 0;
int right = s.size()-1;
while(left<right)
{
while(left<right && !isletter(s[left]))
left++;
while(left<right && !isletter(s[right]))
right--;
swap(s[left++],s[right--]);
}
return s;
}
};
5.string的模拟实现
5.1string.h
cpp
#pragma once
#include <iostream>
#include <assert.h>
using namespace std;
namespace cyh
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
string(const char* str = "")
{
_size = strlen(str);
// _capacity不包含\0
_capacity = _size;
_str = new char[_size + 1];
strcpy(_str, str);
}
void swap(string& s)
{
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
std::swap(_str, s._str);
}
string(const string& s)
{
string tmp(s);
swap(tmp);
}
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
// 短小频繁调用的函数,可以直接定义到类里面,默认是inline
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
const iterator c_str() const
{
return _str;
}
void reserve(size_t n);
void push_back(char ch);
void append(const char* str);
string& operator+=(char ch);
string& operator+=(const char* str);
void insert(size_t pos, char ch);
void insert(size_t pos, const char* s);
void erase(size_t pos, size_t len = npos);
size_t find(char ch, size_t pos = 0);
size_t find(const char* str, size_t pos = 0);
string substr(size_t pos = 0, size_t len = npos);
private:
size_t _size = 0;
size_t _capacity = 0;
iterator _str = nullptr;
static const size_t npos;
};
bool operator<(const string& s1, const string& s2);
bool operator<=(const string& s1, const string& s2);
bool operator>(const string& s1, const string& s2);
bool operator>=(const string& s1, const string& s2);
bool operator==(const string& s1, const string& s2);
bool operator!=(const string& s1, const string& s2);
ostream& operator<<(ostream& out, const string& s);
istream& operator>>(istream& in, string& s);
}
5.2string.cpp
cpp
#define _CRT_SECURE_NO_WARNINGS
#include "string.h"
namespace cyh
{
const size_t string::npos = -1;
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::push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _size);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
strcpy(_str + _size, str);
_size += len;
}
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
void string::insert(size_t pos, char ch)
{
assert(pos < _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _size);
}
size_t end = _size + 1;
while (pos < end)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
}
void string::insert(size_t pos, const char* s)
{
assert(pos < _size);
size_t len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
size_t end = _size + len;
while (pos < end - len + 1)
{
_str[end] = _str[end - len];
--end;
}
//strcpy(_str + pos, s);
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = s[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
{
for (size_t i = pos + len; i <= _size; i++)
{
_str[i - len] = _str[i];
}
_size -= len;
}
}
size_t string::find(char ch, size_t pos)
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t string::find(const char* str, size_t pos)
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
if (len > _size - pos)
{
len = _size - pos;
}
string sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++)
{
sub += _str[pos + i];
}
return sub;
}
bool operator<(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) < 0;
}
bool operator<=(const string& s1, const string& s2)
{
return s1 < s2 || s1 == s2;
}
bool operator>(const string& s1, const string& s2)
{
return !(s1 <= s2);
}
bool operator>=(const string& s1, const string& s2)
{
return !(s1 < s2);
}
bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
cout << ch;
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
int i = 0;
char ch;
char buff[256];
ch = in.get();
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 255)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
}
结语:
这篇文章全文由作者手写,图片由画图软件所制,无AI制作,希望各位博友能有所收获
欢迎各位博友的讨论,觉得不错的小伙伴,别忘了点赞关注哦~
