赞
踩
C 语言中的字符串C 语言中,字符串是以 '\0' 结尾的一些字符的集合,为了操作方便, C 标准库中提供了一些 str 系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP 的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
1. 字符串是表示字符序列的类2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。3. string 类是使用 char( 即作为它的字符类型,使用它的默认 char_traits 和分配器类型 ( 关于模板的更多信息,请参阅basic_string) 。4. string 类是 basic_string 模板类的一个实例,它使用 char 来实例化 basic_string 模板类,并用 char_traits和allocator 作为 basic_string 的默认参数 ( 根于更多的模板信息请参考 basic_string) 。5. 注意,这个类独立于所使用的编码来处理字节 : 如果用来处理多字节或变长字符 ( 如 UTF-8) 的序列,这个类的所有成员( 如长度或大小 ) 以及它的迭代器,将仍然按照字节 ( 而不是实际编码的字符 ) 来操作。总结:1. string 是表示字符串的字符串类2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作 string 的常规操作。3. string 在底层实际是: basic_string 模板类的别名, typedef basic_string<char, char_traits, allocator>string;4. 不能操作多字节或者变长字符的序列
下来我们对其进行实现展示
- #include<iostream>
- #include<string>//含string的包
- #include<Windows.h>
- using namespace std;
- //4个默认的成员函数
- void test_string1()
- {
- string s1;//构造空的string类对象,即空字符串,s1
- string s2("hello");//用C-string来构造string类对象s2
- string s3("hello", 2);//拷贝前两个字符赋给s3
- string s4(s2);//拷贝构造函数
- string s5(s2, 1, 8);//提取s2从1到8位置的字符,字符小于8则输出剩下全不自负
- string s6(s2, 1, string::npos);//提取从1到字符末尾
- string s7(10,'a');//构造10个a
-
- cout << "s1: " << s1 << endl;
- cout << "s2: " << s2 << endl;
- cout << "s3: " << s3 << endl;
- cout << "s4: " << s4 << endl;
- cout << "s5: " << s5 << endl;
- cout << "s6: " << s6 << endl;
- s1 = s7;
- cout << "s7: " << s7 << endl;
- cout << "s1: " << s1 << endl;
- }
- int main()
- {
- test_string1();
- system("pause");
- return 0;
- }
string类对象的方式问方式大体可以分为3种operator[]+下标,我们使用最多的访问方式,迭代器访问方式,范围for()访问方式,下面我们来一一介绍
- void test_string2()//[]+下标的遍历方式,推荐用法
- {
- string s1("hello");
- s1 += ' ';
- s1 += "world";
- cout << s1 << endl;
- //写
- for (size_t i = 0; i < s1.size(); i++)//对字符+1后移1个字符
- {
- s1[i] += 1;
- }
- //读
- for (size_t i = 0; i < s1.size(); i++)
- {
- cout << s1[i] << " ";
- }
- cout << endl;
- }
这种方式基本就是我们在数组操作字符串时进行遍历读写的操作,这个操作输出了一个hello world,而后我们对于字符遍历进行了+1操作,再次遍历输出,得到的结果是i f m m p x p s m e对所有的字符进行了+1,在进行遍历输出
[]+下标的方式也是我们最为熟悉的
字符迭代器是一种相对模板化的一种字符在操作方式格式为 string::iterator it = s1.begin();类似与这样,当然,如果我们想调用其他的迭代器比如vector,只需要将string换为vector,而后后面再<类型>即可
- //迭代器
- //写
- string::iterator it = s1.begin();
- //auto it = s1.begin();也可以套用这种方式,之前学习的auto自动判断类型
- while (it != s1.end())
- {
- *it -= 1;
- ++it;
- }
- //读
- it = s1.begin();
- while (it != s1.end())
- {
- cout << *it << " ";
- ++it;
- }
- cout << endl;
- }
因为我们刚在上面改变了hello world的字符,所以我们在这里用迭代器先将其改回来--对*it-=1,对每个字符-1,而后++it遍历,完成整个字符的-1,而后遍历字符串,进行输出
我们可以看到迭代器中的操作,很像指针,暂时我们可以将其当做指针
不过这只是最普通最常用的一种迭代器,还有三种迭代器
- void test_string3()
- {
- string s1("hello world");
- //倒着遍历
- string::reverse_iterator rit = s1.rbegin();
- while (rit != s1.rend())
- {
- cout << *rit << " ";
- ++rit;
- }
- string num("12345");
- cout << string2int(num) << endl;
- }
这种是reverse_iterator倒序迭代器,不过我们操作还是++进行操作的,内部自动转化为倒序
这段代码利用倒序迭代器完成了将hello world,与12345倒序输出的功能
还有const修饰的迭代器,无法对原来的数据进行改变
- int string2int(const string& str)
- {
- int val = 0;
- //只能读,不能写
- string::const_iterator it = str.begin();
- while (it != str.end())
- {
- val *= 10;
- val += (*it - '0');
- ++it;
- }
- //val = 0;
- /*string::const_reverse_iterator rit = str.rbegin();
- while (rit != str.rend())
- {
- val *= 10;
- val += (*rit - '0');
- ++rit;
- }*/
- return val;
- int main()
- {
- string num("12345");
- //test_string2();
- //test_string3();
- cout << string2int(num) << endl;;
- system("pause");
- return 0;
- }
这段代码完成了字符串对于int型的转化,正着转化和倒着转化
从顺序看迭代器分为两种,正序和倒序,从属性来看,迭代器分为const和普通,自由组合一共4种
- 范围for
- C++11->原理其实是迭代器
- for (auto ch : s1)
- {
- cout << ch << " ";
- }
- cout << endl;
- }
范围for其实就是用我们从前的知识,对s1进行依次遍历,不过这种方式底层原理也是迭代器,这便是三种遍历string的方式
这些是基本的一些字符串操作函数,下面我们对capacity()进行剖析
我们可以看到,当我们不断地去加入字符时,容量的大小变化,从0开始,差不多是每次变化1.5倍左右进行扩大得到,我们还可以推出,到了后面增容会越来越慢,随着基数的增大每次扩大的容量也会增加,而且增容是有代价的,不能频繁去增加
下面我们再来了解一个关键字,reserve,这个关键字的含义是直接开辟定量的空间
此时我们会发现,就没有扩容了,直接将容量增到了100,其实我们在开辟时,显示的是开辟了100个空间,实际上是101个,因为还会腾出一个空间去给\0
在了解reserve后,我们有个与之相对应的resize()
这种是仅有一个int参数的情况,起到的作用就是开辟了100个字符串这种是reserve重载的方式,开辟了100个字符串并赋值
那么我们再来看下这种情况
当我们reserve了5个大小的空间,但是字符串不够放进去,所以就只有前五个放进去了,而后我们进行开辟20个,放入x,也不会再将world放入,全为x
这是我们的字符串插入操作,两种都可以完成操作,不过推荐第二种,方便可读
1. size() 与 length() 方法底层实现原理完全相同,引入 size() 的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size() 。2. clear() 只是将 string 中有效字符清空,不改变底层空间大小。3. resize(size_t n) 与 resize(size_t n, char c) 都是将字符串中有效字符个数改变到 n 个,不同的是当字符个数增多时:resize(n) 用 0 来填充多出的元素空间, resize(size_t n, char c) 用字符 c 来填充多出的元素空间。注意:resize 在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。4. reserve(size_t res_arg=0) :为 string 预留空间,不改变有效元素个数,当 reserve 的参数小于string的底层空间总大小时, reserver 不会改变容量大小。
这是我们的插入删除
还有一个小小的接口,c_str(),获取字符数组首地址,用C形式遍历,其实最主要的差距就是中间有无\0,对于C++的遍历方式而言,遇到\0并不会停止遍历,而对于C语言,碰到\0就停止了
还有一个substr(pos)函数,访问从pos位到结束的所有子串,以及我们的find('s')函数,找到第一个与s相同的字符
1. 在 string 尾部追加字符时, s.push_back(c) / s.append(1, c) / s += 'c' 三种的实现方式差不多,一般情况下string 类的 += 操作用的比较多, += 操作不仅可以连接单个字符,还可以连接字符串。2. 对 string 操作时,如果能够大概预估到放多少字符,可以先通过 reserve 把空间预留好。
- void Teststring()
- {
- string str;
- str.push_back(' '); // 在str后插入空格
- str.append("hello"); // 在str后追加一个字符"hello"
- str += 'b'; // 在str后追加一个字符'b'
- str += "it"; // 在str后追加一个字符串"it"
- cout << str << endl;
- cout << str.c_str() << endl; // 以C语言的方式打印字符串
-
- // 获取file的后缀
- string file1("string.cpp");
- size_t pos = file.rfind('.');
- string suffix(file.substr(pos, file.size() - pos));
- cout << suffix << endl;
-
- // npos是string里面的一个静态成员变量
- // static const size_t npos = -1;
-
- // 取出url中的域名
- sring url("http://www.cplusplus.com/reference/string/string/find/");
- cout << url << endl;
- size_t start = url.find("://");
- if (start == string::npos)
- {
- cout << "invalid url" << endl;
- return;
- }
- start += 3;
- size_t finish = url.find('/', start);
- string address = url.substr(start, finish - start);
- // 删除url的协议前缀
- pos = url.find("://");
- url.erase(0, pos + 3);
- cout << url << endl;
- }
这是string一些接口的常用用法
还有一个需要我们注意的点是cin与getline,cin遇到空格或者换行都会停下来,而我们的getline只遇到换行会停下,空格不会,这在需要输入一些带空格的字符串时很有用
在这里我们对String类进行模拟实现,加深我们对其的理解
我们之前在其它类中定义构造函数的时候是怎么定义的呢?我们在这里试一下
我们会发现,一般的写法对于读取字符串是可行的,但当我们想利用[ ]去改变它时,就会出现报错,原因是我们的s1存在栈中,去调用这个构造函数时会指向存在代码区的“hello”,而我们的operator[ ]返回的是字符串本身第i个位置的字符,因为存在代码段的字符串无法被修改,所以在我们想去修改字符串时会报错
所以我们就不能把字符串存储在常量区中,在这里我们选择堆,因为堆可以自行开辟大小
而我们若想建立一个空对象,也不能直接取用nullptr,因为strlen时出现了空指针,找不到\0,所以我们在建立空对象时需要在堆上开辟一个大小的空间,附上\0
下面我们展示全缺省合并写法
析构函数就正常的资源清理与指针指控即可
我们先来试试如果我们不写拷贝构造会发生什么呢?
我们先对c_str进行了重载,随后拷贝构造了s2,发现也可以实现拷贝构造,但是程序给崩溃了,这是什么原因呢,其实当我们没有重载拷贝构造时,系统自带的拷贝构造就会完成浅拷贝,也就是在栈中将s1中存的字符串地址按字节将值拷贝到同在栈中的s2,此时s1,s2指向堆中的同一个字符串,这里没有什么问题,但是最后在析构的时候,会对s1,s2分别进行析构,又因为他们指向同一地址空间,所以同一块空间被析构了两次,引起报错
接下来看看正确做法
正确的做法就是在堆中在开辟一块一模一样大的空间,再将值拷进去,进行深拷贝
我们用一般的方法进行赋值
发现同样的,会引起崩溃,这与上面的拷贝构造是一样的,系统自动浅拷贝,会在最后析构的时候出现问题,所以也需要我们对其进行重载
具体做法就是在堆中开辟一块与s3相同的tmp空间,将s3的拷贝给这个tmp,再将原来的s1空间释放掉,再将s1指向tmp,就完成了赋值
下面我们展示代码
- namespace wxy
- {
- class string
- {
- public:
- /*string()
- :_str(nullptr)//而我们在调用无参时也不能这样调,会有空指针的问题,无法找到\0结束
- {}*/
- /* string(char* str)
- :_str(str)//不能这么写,因为string对象中存的是指针,指针指向的数组中存储字符,字符无法修改
- {}*/
-
- //string()//可行方案
-
- // :_str(new char[1])
- //{
- // _str[0] = '0';
- //}
-
- //string(char* str)//将空间开辟在堆上,str+1是为了给\0空间
- // :_str(new char[strlen(str + 1)])//开辟一个str+1大小的新空间,而后用strcpy函数将在常量区的str拷到堆区的_str中
- //{
- // strcpy(_str, str);
- //}
-
- //将他们两个合并
- string(char* str = " ")
- :_str(new char[strlen(str) + 1])
- {
- strcpy(_str, str);
- }
- ~string()
- {
- delete[] _str;
- _str = nullptr;
- }
- //string s2(s1)
- string(const string& s)
- :_str(new char[strlen(s._str)+1])
- {
- strcpy(_str, s._str);
- }
- size_t size()
- {
- return strlen(_str);
- }
- char& operator[](size_t i)
- {
- return _str[i];
- }
- const char* c_str()
- {
- return _str;
- }
- //s1=s3
- //s1=s1
- string& operator=(const string& s)
- {
- if (this != &s)
- {
- char* tmp = new char[strlen(s._str) + 1];
- strcpy(tmp, s._str);
- delete[] _str;
- _str = tmp;
-
- return *this;
- }
- }
- private:
- char* _str;
- };
- void test_string2()
- {
- string s1("hello");
- string s2(s1);
- /*cout << s1.c_str() << endl;
- cout << s2.c_str() << endl;*/
- string s3("world");
- s1 = s3;
- cout << s1.c_str() << endl;
- cout << s3.c_str() << endl;
- }
- }
- void test_string1()
- {
- string s1("hello");
- string s2;
-
- for (size_t i = 0; i < s1.size(); ++i)
- {
- s1[i] += 1;//
- cout << s1[i] << " ";
- }
- cout << endl;
-
- for (size_t i = 0; i < s2.size(); ++i)
- {
- s1[i] += 1;//
- cout << s2[i] << " ";
- }
- cout << endl;
- }
我们的深拷贝其实可以有一种现代写法
- //深拷贝-现代写法
- string(const string& s)
- :_str(nullptr)
- {
- string tmp(s._str);
- swap(_str, tmp._str);
- }
- //s1=s2
- string& operator=(const string& s)
- {
- if (this != &s)
- {
- string tmp(s);
- swap(_str, tmp._str);
- }
- return *this;
- }
- //最简单版本
- string& operator=(const string& s)
- {
- swap(_str, s._str);
- return *this;
- }
说明:上述string类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
接下来我们想实现一个更加复杂的string类
- namespace wxy
- {
- class string
- {
- public:
- typedef char* iterator;//迭代器
- iterator begin()//迭代器起点
- {
- return _str;
- }
- iterator end()//迭代器终点
- {
- return _str + _size;
- }
- string(const char* str = " ")//构造函数,堆上开辟空间将字符串拷进去
- {
- _size = strlen(str);
- _capacity = _size;
- _str = new char[_capacity + 1];
- strcpy(_str, str);
- }
- ~string()//析构函数
- {
- delete[] _str;
- _str = nullptr;
- _size = _capacity = 0;
- }
- size_t size()const//计算有效字符个数
- {
- return _size;
- }
- size_t capacity()const//计算字符串容量,\0不算
- {
- return _capacity;
- }
- char& operator[](size_t i)//[]重载
- {
- assert(i < _size);
- return _str[i];
- }
- const char& operator[](size_t i)const
- {
- assert(i < _size);
- return _str[i];
- }
- const char* c_str()//c语言形式输出字符
- {
- return _str;
- }
- void reserve(size_t n)//开空间函数
- {
- if (n > _capacity)//当要开的空间大于当前总容量时
- {
- char* newstr = new char[n + 1];//堆上开辟n+1
- strcpy(newstr, _str);//原字符串拷进堆
- delete[] _str;//析构原字符串
- _str = newstr;//堆上开的新空间赋回字符串
- _capacity = n;//容量改为n
- }
- }
- void resize(size_t n, char ch = '\0')//改有效字符个数
- {
- if (n < _size)//当要改的有效字符个数小于当前有效字符个数
- {
- _str[n] = '\0';//将第n号下标元素改为\0,系统会自动识别到\0,剩下的就不识别了
- _size = n;//有效字符个数改为n
- }
- else
- {
- if (n > _capacity)//当有效字符个数大于整个容量
- {
- reserve(n);//开空间到n
- }
- for (size_t i = _size; i < n; ++i)
- {
- _str[i] = ch;//将剩下空出来的位置赋成字符ch,缺省时为\0
- }
- _size = n;//有效字符个数改为n
- _str[_size] = "\0";//最后一个位置上改为\0,好让系统识别结束
- }
- }
- void push_back(char ch)//尾插字符
- {
- //空间满,进行增容
- if (_size == _capacity)
- {
- size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
- reserve(newcapacity);
- }
- _str[_size] = ch;
- ++_size;
- _str[_size] = '\0';
- }
- void append(const char* str)//尾插字符串
- {
- size_t len = strlen(str);//计算字符串长度
- if (_size + len > _capacity)//当最终插入后的字符串长度大于容量
- {
- reserve(_size + len);//扩容到应该的长度
- }
- strcpy(_str + _size, str);//将字符拷贝到增容后的空间
- _size += len;//有效字符个数变为len
- }
- //s1+='a'
- string& operator+=(char ch)//+=重载
- {
- this->push_back(ch);
- return *this;
- }
- //s1+="aaaa"
- string& operator+=(const char* str)
- {
- this->append(str);
- return *this;
- }
- string& intsert(size_t pos, char ch)//插入字符
- {
- assert(pos < _size);//当插入位置小于有效字符个数断言
- if (_size == _capacity)//容量不够,扩容
- {
- size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
- reserve(newcapacity);
- }
- int end = _size;//size开始移动
- while (end >= pos)//从后往前循环全部后移1
- {
- _str[end + 1] = _str[end];
- --end;
- }
- _str[pos] = ch;//腾出来的位置插入ch
- ++_size;//有效字符个数+1
- }
- string& insert(size_t pos, const char* str)
- {
- assert(pos < _size);
- size_t len = strlen(str);//计算字符串长度
- if (_size + len > _capacity)
- {
- reserve(_size + len);
- }
- int end = _size;
- while (end >= pos)
- {
- _str[end + len] = _str[end];//全部后移len位
- --end;
- }
- /*for (size_t i = 0; i < len; ++i)
- {
- _str[pos] = str[i++];
- }*/
- strncpy(_str + pos, str, len);//拷贝len位
- _size += len;
- }
- void erase(size_t pos, size_t len = npos)//删除pos开始之后长为len的字符串
- {
- assert(pos < _size);
- if (len >= _size - pos)//字符串长度大于等于有效字符长度-删除的位置
- {
- _str[pos] = '\0';//删除的位置变为\0
- _size = pos;//有效长度变为前面没删的部分
- }
- else//当有效字符长度大于字符串长度+pos时
- {
- size_t i = pos + len;//删除位置+字符串长度=删后字符串长度i,i后数据要挪到前面
- while (i <= _size)
- {
- _str[i-len] = _str[i];//将i后数据挪到前len位置
- ++i;
- }
- _size -= len;//最终长度
- }
- }
- size_t find(char ch, size_t pos = 0)//从pos开始寻找ch
- {
- 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)
- {
- char* p = strstr(_str, str);//在_str中寻找str
- if (p == nullptr)//未找到
- {
- return npos;
- }
- else
- {
- return p-_str;//返回
- }
- }
-
- bool operator<(const string& s)
- {
- int ret = strcmp(_str, s._str);//s与_str比较,_str大返回>0,相等=0,小<0
- return ret < 0;
- }
-
- bool operator==(const string& s)
- {
- int ret = strcmp(_str, s._str);
- return ret == 0;
- }
- bool operator<=(const string& s)
- {
- return *this<s || *this == s;
- }
- bool operator>(const string& s)
- {
- return !(*this <= s);
- }
- bool operator>=(const string& s)
- {
- return !(*this < s);
- }
- bool operator!=(const string& s)
- {
- return !(*this == s);
- }
- private:
- char* _str;
- size_t _size;//已有多少个有效字符
- size_t _capacity;//可以存储多少个有效字符 \0不是有效字符
- static size_t npos;
- };
- size_t string::npos = -1;
- //getlin遇到空格不结束
- istream& operator>>(istream& in, string& s)
- {
- while (1)
- {
- char ch;
- //in >> ch;
- ch = in.get();
- if (ch == ' ' || ch == '\n')
- {
- break;
- }
- else{
- s += ch;
- }
- }
- }
- ostream& operator<<(ostream& out,const string& s)
- {
- for (size_t i = 0; i < s.size(); ++i)
- {
- cout << s[i];
- }
- return out;
- }
- void test_string1()
- {
- string s1;
- string s2("hello");
- string::iterator it2 = s2.begin();
- while (it2 != s2.end())
- {
- cout << *it2 << " ";
- ++it2;
- }
- cout << endl;
- //范围for,本质迭代器
- for (auto e : s2)
- {
- cout << e << " ";
- }
- cout << endl;
- }
- }
对于string的学习还需要在不断地练习中精进
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。