此文编写参考狄泰软件学院唐佐林老师的视频课程
字符串操作详解可参考该链接
一、历史遗留问题
我们都知道c语言是通过字符数组来去模拟字符串的,也就是说c语言是不支持真正意义上的字符串的,还有很重要的一点是c语言不支持自定义类型,因此它也无法获得字符串类型,但是到了c++这里,这个状况就可以被改变了,在c++中,我们可以定义很多的类。所以在c++中,我们可以通过类完成字符串类型的定义。需要注意的是:c++语言直接支持c语言的所有概念,c++语言中没有原生的字符串类型,c++标准库提供了string类型。下面是string类型能够完成的一些功能:
字符串连接
字符串大小比较
子串查找和提取
字符串的插入和替换
以下通过一个实例来去理解字符串类字符串的连接功能有多么便捷和强大。
#include<iostream>
#include<string>
using namespace std;
void sort_string(string str[],int len);//排序
string add_string(string str[],int len);//字符串连接函数
int main()
{
string str[]=//声明了一个字符串数组
{
"hello world",
"c",
"c++",
"python",
"java"
};
sort_string(str,5);//对字符串数组中的字符串进行排序
for(int i=0;i<5;i++)
{
cout<<"str="<<str[i]<<endl;//对排序后的结果进行输出
}
cout<<add_string(str,5)<<endl;//字符串连接的结果
return 0;
}
void sort_string(string str[],int len)
{
for(int i=0;i<len;i++)
{
for(int j=i;j<len;j++)
{
if(str[i]>str[j])
{
swap(str[i],str[j]);
}
}
}
}
string add_string(string str[],int len)
{
string ret="";
for(int i=0;i<len;i++)
{
ret+=str[i]+";";
}
return ret;
}
例子中用到了C++中的swap交换函数函数,下面对这个函数做个简单的介绍
swap函数:不用担心交换变量精度的缺失,无需构造临时变量,不会增加空间复杂度
swap 包含在命名空间std 里面
swap(a,b);(交换两个数)
swap(a[i] = b[j]);(交换两个位置上的变量)
结果:
分析:结果显示这些单词的顺序就如同词典一样,故排序成功了,且字符串的连接工作也成功了,c++的标准库非常的强大,如swap函数直接将数组的两个元素的位置对换了,并且这两个元素还是对象,再如连接字符串数组中所有的字符串,用’+'就实现了字符串的连接,本质上这是通过对+这个操作符的重载实现的,这些工作如果用c语言来写是非常困难的,所以c++是对c的改进,两者并不是对立的关系。
二、字符串与数字的转换
在标准库中提供了相关的类对字符串和数字进行转换,字符串流类**(sstream)**就是其中的一个。
(1)<sstream>头文件
(2)istringstream----字符串输入流
特性:把字符串转化成数字
注意:当转换成功的时候会返回1,转换失败会返回0。
(3)ostringstream-----字符串输出流
特性:把数字转化成字符串
注意:左移操作符返回了左操作数本身,即oss对象本身。
对这些类的使用例程如下:
程序1:
#include<iostream>
#include<string>
#include<sstream>
using namespace std;
int main()
{
double a=0;
bool s=false;
istringstream iss("123.456");
s=iss>>a;
if(s)//若转化成功,则s=true
{
cout<<"a="<<a<<endl;
}
ostringstream oss;
oss<<a;//等价于oss<<123<<"."<<456
string str=oss.str();
cout<<"str="<<str<<endl;
return 0;
}
运行结果:
程序2:对程序1我们可以做个改进,也就是写成函数的形式来去实现。如下:
#include<iostream>
#include<string>
#include<sstream>
using namespace std;
bool to_number(const string& str,double& num)
{
istringstream iss(str);
return (iss>>num);
}
string to_string(double& num)
{
ostringstream oss;
oss<<num;
return oss.str();
}
int main()
{
double num=0;
cout<<to_number("123.456",num)<<endl;
cout<<"num="<<num<<endl;
string str=to_string(num);
cout<<"str="<<str<<endl;
return 0;
}
程序3:程序2写成函数的形式,但是仍旧有很大的缺陷,如字符串到数字的转换,有可能是转换成浮点型、整型等等,那么这样的话就要写很多的重载函数,这样就显得有点啰嗦了,那么是否有更好的解决方案呢?在还没有学到函数模板的时候,我们可以用宏的方法来去改进和实现。例程如下:
#include<iostream>
#include<string>
#include<sstream>
using namespace std;
/*利用了临时对象的特性,因为临时对象的生命周期只有一条语句的时间,
所以必须在一条语句的时间里完成宏定义*/
#define TO_NUM(str,num) (istringstream(str)>>num)
#define TO_str(num) (((ostringstream&)(ostringstream()<<num)).str())
int main()
{
double num=0;
TO_NUM("123.345",num);
cout<<"num="<<num<<endl;
string str=TO_str(num);
cout<<"str="<<str<<endl;
return 0;
}
运行结果:
分析:对于数字转换成字符串,我们因为需要一步就完成上一例的三个步骤,因此我们可以利用临时对象的特性,并且此处还用到了强制类型转换,此处的强制类型转换仍旧是使用c语言的方法。
三、实现字符串的循环右移
1、用c++来实现
在c语言中,我们是通过字符数组来实现字符串的,若要实现循环右移的功能,也只能站在字符数组的角度去实现,现在学了c++,我们就可以使用c++中的字符串类,从另外一个视角来去实现字符串的循环右移。例程如下:
#include<iostream>
#include<string>
using namespace std;
string right_move(const string& str,unsigned int n);
int main()
{
string str="abcdefg";
str=right_move(str,3);
cout<<"str="<<str<<endl;//efgabcd
return 0;
}
string right_move(const string& str,unsigned int n)
{
string ret="";
unsigned int pos=0;//定义一个变量,用于记录位置
n=n%str.length();//用于计算出真正的有效移动位数,如abc移动4位相当于移动1位,此处采用取余的方法
pos=str.length()-n;//计算出位置,分割好位置,为下面做字符串提取做准备,如abc移动1位,则abc=>ab c
ret=str.substr(pos);//将该位置后的子串提取出来
ret=ret+str.substr(0,pos);//将上面的子串和后面的子串换个位置
return ret;//返回修改后的字符串
}
运行结果:
事实上我们还可以对上面这个程序再做改进,对>>使用用操作符重载,这样功能实现起来更加的直观好用,改进后的例程如下:
#include<iostream>
#include<string>
using namespace std;
string operator >>(const string& str,unsigned int n)
{
string ret="";
unsigned int pos=0;//定义一个变量,用于记录位置
n=n%str.length();//用于计算出真正的有效移动位数,如abc移动4位相当于移动1位,此处采用取余的方法
pos=str.length()-n;//计算出位置,分割好位置,为下面做字符串提取做准备,如abc移动1位,则abc=>ab c
ret=str.substr(pos);//将该位置后的子串提取出来
ret=ret+str.substr(0,pos);//将上面的子串和后面的子串换个位置
return ret;//返回修改后的字符串
}
int main()
{
string str="abcdefg";
str=str>>3;
cout<<"str="<<str<<endl;//efgabcd
return 0;
}
运行结果:
分析:效果是和上面一个例子是一致的,但是操作起来更加的方便,和日常使用普通的右移操作符一般。
2、用c语言来实现
为了真正体会到c++的强大之处,我们可以看下如果用c语言来实现字符串的循环右移,它的实现过程是怎么样的,与c++有什么不同,例程如下:
#include<stdio.h>
#include<string.h>
void right_move(const char* src,char* result,unsigned int n);
int main()
{
char result[200]={0};
right_move("abcd",result,2);
printf("result=%s\n",result);
return 0;
}
void right_move(const char* src,char* result,unsigned int n)
{
int i=0;
int length=strlen(src);
for(i=0;i<length;i++)
{
result[(i+n)%length]=src[i];
}
result[length]='\0';
}
运行结果:
分析:因为在c中是用字符数组来模拟字符串的,所以这个时候就应该切换到数组的视角来解决问题了,这个例程中是采用移动前的字符和移动后的字符做相应的映射关系实现的,如字符串"abcd"存放在数组s中,那么s[0]对应的字符就是a,此时对应的下标为0,那么当我们要将这个字符数组中的字符右移2位的时候,这个时候a字符对应的下标就为2了,由这样的原理可以得出这样的一条公式result[(i+n)%length]=src[i],其中i为当前字符所在数组中的位置,n为偏移量,之所以要对数组长度取余,是为了计算真正的位置偏移量。对比在c++中用字符串类,显然在c语言中用字符数组的角度实现字符串的循环右移没有那么直观,你还需要再草稿图上画出每个字符所处的位置,然后进行分析,最后做映射关系,结束的时候还不能忘记加上字符串结束标志’\0’。
四、使用字符串类时的易错点
在日常编程中,很多人在进行c++编程时采用的却是c编程的思想,因此给程序带来bug,下面一些例子是这种思想在关于字符串操作方面所带来的一些常见影响。
例1:造成野指针问题
#include<iostream>
#include<string>
using namespace std;
int main()
{
string str="1234";
const char* p=str.c_str();//此处是字符串类的成员函数,返回的是指向字符串的指针
cout<<p<<endl;
str.append("abcd");//此处是字符串类的成员函数,是在原来的字符串中追加新的字符串
cout<<p<<endl;
return 0;
}
结果:
分析:看此处的运行结果好像没什么问题,达到了预期的效果,但是其实背后却隐藏了危险的,很可能会造成野指针的问题。因为string类内部维护了一个指向数据的const char*指针,这个指针是很有可能在程序运行过程中发生变化的,当然也有可能没发生变化,但是这种不确定的行为是会带来安全性问题的。
此例中,我们将这个指针的值赋给了p,那么此时两个指针指向了同一个空间,但是当后面用str.append(“abcd”)时,这个时候内部维护的指针完全是有可能指向新的空间的,假如当指向新的空间时,会将原来空间的值拷贝一份赋值到新的空间里,然后再追加上后来加上去的字符串,那么指向原来空间的指针p就成了野指针了,所以看似简单的问题却造成了野指针的问题,很大部分的原因就是用c++写程序却用着c的编程方法,所以这个应该是要去避免的。
例2:造成打印字符串为空的问题
#include<iostream>
#include<string>
using namespace std;
int main()
{
string str="";
str.reserve(10);
/*保留一定量内存以容纳一定数量的字符,这个函数为string重新分配内存。重新分配的大小由其参数决定,默认参数为0,这时候会对string进行非强制性缩减。此处是分配了10个字节的空间*/
const char* sa="abcde";
for(int i=0;i<sizeof(sa);i++)
{
str[i]=sa[i];
}
if(!str.empty())//若为空,则返回true,若为非空则返回false,故此处不会执行
{
for(int i=0;i<sizeof(sa);i++)
cout<<str[i]<<endl;
}
cout<<str<<endl;//正常情况下,此处输出为空
for(int i=0;i<sizeof(sa);i++)
cout<<str[i]<<endl;//此处有字母输出
return 0;
}
运行结果:
分析:按照预想本应该输出三次abcde,但是只输出了最后的for循环的内容,这是为什么呢?原因是字符串类内部同样维护了一个表示长度信息的成员变量m_length,在刚开始定义str的时候,该长度信息为0,当通过字符数组依次给字符串对象str赋值时,其实里面是存有内容的,但是输出str时却为空,也就是说字符串对象本身并不认为自己是有内容的,所以打印的结果仍旧为空。
总结:
string类通过一个数据空间保存数据
string类通过一个成员变量保存当前字符串的长度
c++开发时尽量避免c语言中惯用的编程思想。