字符串简介
字符串变量与常量
字符串变量需要满足两个条件:
1.一个数组,其元素均是字符;
2.最后一个元素以空字符'\0'
结束(C++中单引号内是字符,双引号内是字符串);
需要注意,定义字符串变量时,应当留意为空字符预留空间,使空字符自动加入到分配的空间中:
//注意必须是双引号
char str[11]={"helloworld"};
//或者简写
char str[]={"helloworld"};
如果写成char str[10]={"helloworld"};
,在visual studio编辑窗口中,会自动提示错误:const char[11]类型的值不能用于初始化char [10]类型的实体;
字符串常量是一对双引号括起来的字符序列,字符串中每个字符作为一个数组元素存储,比如字符串:"helloworld"
;
字符串常量同样以空字符结尾:
关于字符表示的说明
对于0 '\0' '0'
:
在机器中,表示为:
char c1=0; //0x00
char c2='\0'; //0x00
char c3='0'; //0x30
虽然0是int类型的数字,然而在内存中,由于char的限制,依然只使用一字节:00
,另外,观察机器码可以发现
c
1
c_{1}
c1和
c
2
c_{2}
c2是一样的;
对于
c
3
c_{3}
c3,保存的是字符0,字符0的ascii码为48,十六进制为30,注意ascii码用于字符编码,补码专用于数值编码,两者并没有关系;
ascii码是基于拉丁字母的编码方式,主要用于英语,使用指定的7位或8位二进制数组合表示128或256种可能字符;
'0':0x30
NULL:0x00
'A':0x41
'a':0x61
DEL:0x7F
现在看来,ascii已经不能满足全球使用,所以诞生了Unicode编码,Unicode编码的初衷就是把世界上的文字都映射到一套字符空间;
Unicode编码
为了表示Unicode字符集合,诞生了3种编码方式:
- 1.UTF-8:
一个字节(byte)表示字符,兼容ascii码;存储效率高,为了表示其他国家的语言,则将英语对应的utf-8重新组合去编码,比如汉语一个汉字由两个byte组合表示,UTF-8也是python的默认编码; - 2.UTF-16:
UTF-8为了表示其他语言,是不定长的,当汉语英语混合时,机器需消耗资源先判断字符是什么类别,所以需要定长的UTF-16编码,UTF-16是定长的2byte;
需要注意一个问题,当使用大于等于两个byte的编码时,需要考虑字节序,当两个机器进行交互(比如网络传输)时,如果不说明字节序,将会出现乱码;
编码错误的根本在于编码与解码方式不统一
- 3.UTF-32:
用4个byte定长表示字符,与UTF-16相比,扩充了表达范围,但依然面临字节序问题,事实上,UTF-32使用并不广泛;
由于需要解决字节序问题,Windows的文件会携带BOM(byte order mark),如果要在其他平台正确读该文件,需要去除BOM,比如Linux下:
dos2unix
编码实例
首先基于Notepad++指定编码:
ANSI就是最经典的ascii编码,UCS-2实际上相当于UTF-16;
为了便于读取文件,将文件保存到工程所在目录,Visual Studio中查看方式为:
右击文件,选择文件所在目录:
文件内容为,编码方式为UTF-16大端法:
helloworld
在解决方案资源管理器"添加"->“现有项”,加入之前保存好的文本文件,加入之后在解决方案资源管理器中右击文件,选择打开方式为二进制编辑器:
编码看似复杂,其实仔细观察会发现,0x68正是h的ascii码,由于是UTF-16,所以扩充为两字节0x0068,大端法的高位在最前,如果是小端法则为0x6800,FE FF也是告知系统,编码为大端法,同理,FF FE则代表小端法;
(00000000和00000010是Visual Studio的原因,可以忽略)如果文件携带BOM,则最开头会出现EF BB BF之类的3个连续字节,以告知系统编码方式与字节序的选择;
可见,去除BOM的方法也很简单,只需要写一个程序删除前3个字节就行;
html,xml等文件可以用于浏览器,因为它们自身就携带编码信息,从而便于解码,便得以在不同平台间广泛使用
字符串的指针表示
这里补充两个与指针相关的单目运算符:*
(定义指针变量)和&
(取地址),指针中存储的内容本质就是地址,指针变量用于存储其他变量的地址;
字符串的指针表示方法为:
char* pst="helloworld";
指针变量pstr指向字符类型,"helloworld"是一个字符串常量,即pstr指向内存中的字符串常量"helloworld"所在的地址,字符串常量"helloworld"以空字符结尾(保存在数组中),pstr指向首字符’h’,存储了’h’的地址:
回顾本篇开始的字符串变量定义,使用了数组char […]的方式,char [ ]与char*有以下例子;
比如 char [ ] 方式:
char strhello[11] = { "helloworld" };
//注意不要将10写成11,虽然不报错,但字符串的结束符默认不要操作
for (int index = 0; index < 10; ++index)
{
strhello[index] += 1;
cout << strhello[index] << endl;
}
当使用 char* 方式时:
char* pstrhello = "helloworld";
for (int index = 0; index < 10; ++index)
{
pstrhello[index] += 1;
cout << pstrhello[index] << endl;
}
此时,会发生中断,因为pstrhello指向的是没有写权限的区域,因为指针变量指向的是字符串常量,常量是不能修改的,如果让指针指向字符串变量(数组形式),则可以进行修改:
char* pstrhello = strhello;
for (int index = 0; index < 10; ++index)
{
pstrhello[index] += 1;
cout << pstrhello[index] << endl;
}
可见,指针变量指向的内容是否可变取决于该内容是常量还是变量;
指针变量在运行过程中,可以改变指向:
char strhello[11]={"helloworld"};
char* pstrhello="helloworld";
pstrhello=strhello
而数组char [ ]在运行中,不能改变地址(指向),如果写strhello=pstrhello
就会出错;
数组一旦声明,地址就不能再改变,指针可以改变指向是因为指针变量是一个变量而不是地址
字符串的基本操作(一)
获取字符串的长度:
#include<string.h>
char* s="helloworld";
strlen(s);
//返回字符串s的长度,10,不包含'\0'
字符串比较,两个字符串从左向右逐个字符按照ascii的值比较,直到出现不同的字符或遇到'\0'
为止:
strcmp(s1,s2);
/*
如果s1和s2相同,返回0;
如果s1<s2,返回值小于0;
如果s1>s2,返回值大于0
*/
char* s1 = "Apple";
char* s2 = "Bank";
cout << strcmp(s1, s2) << endl; //-1
比如s1="Apple"与s2=“Bank”,字符A与B相比,已经不同且A小于B,所以直接返回小于0的数;
字符串拷贝与拼接(个人认为这是早期C++遗留下来的不太好的函数):
strcpy(s1,s2);
//将s2拷贝到s1,(从s1的首字符开始),注意s1数组的空间要大于等于s2
strcat(s1,s2);
//需要确保s1数组的空余空间可以容纳s2
查找字符串:
strchr(s1,ch);
//ch在s1中首次出现的位置
cout << strchr(s1, 'l') << endl;
// le
strstr(s1,s2);
//s2在s1中首次出现的位置
cout << strstr(s1, "pp") << endl;
// pple
字符串的基本操作(二)
在字符串的基本操作(一)中,strcat实际上是一个很危险的函数,虽然说对于strcat(s1,s2);
需要确保s1数组的空余空间可以容纳s2,但这需要靠开发者自觉,如果在s1的结束符后开始拼接也是可以的,不注意这个问题的话,可能会导致内存中紧跟着s1后的内容被s2覆盖,这会暗中修改了机器的逻辑,极其危险;
这是真实发生过无数次的教训,曾经一个黑客就是利用strcat的方式将机器中用作判断权限的语句完整覆盖,直接获得权限操控机器,盗取了管理员看守的秘密信息;这种方式也就是计算机历史上第一个病毒:蠕虫病毒;其本质就是缓冲区溢出,暗中覆盖原有的逻辑;
从这个角度看,能很好表现出C++的特点,开发者必须时刻关注内存的管理,尤其注意边界检查;
为了修正安全问题,C++已经有了安全的api,只需要在以前的函数名后加_s()
,_s()
的意思是security,比如strcat_s(),通过观察其实现,其实就是在原有基础上增加了边界检查功能;
使用strlen_s,strcpy_s,strcat_s才更安全
安全版本的api在调用上略有不同,以strcat_s为例,在visual studio下,点击函数名后按F1可以自动跳转api的网页文档,阅读文档有助于我有效调用函数:
errno_t strcat_s(
char *strDestination,
size_t numberOfElements,
const char *strSource
);
参数说明为:
实例如下:
const int STR_LEN = 16;
char cat1[STR_LEN] = { "hello" };
char* cat2 = "world";
strcat_s(cat1, STR_LEN, cat2);
cout << cat1 << endl; //helloworld
如果将STR_LEN改为5,将会中断报错,报错信息为Buffer is too small;
字符串操作的开源库Redis,高效是基于空间换时间,Redis下的字符串设计:
Redis链接
sdshdr是一个动态的字符串对象
C++的新型字符串string
C++标准库提供了string类型专门处理字符串:
#include<string>
使用string可以更方便和安全地管理字符串;
定义字符串变量与初始化方式:
string s; //定义空字符串
string s="hello"; //定义并初始化
string s("hello");
string s=string("hello");
相关函数
获取字符串长度:
s.length();
s.size(); //等价于s.length()
s.capacity(); //返回的是这个容器的真正长度
字符串的比较:
string s1="hello";
string s2="world";
cout<<(s1==s2)<<endl;
cout<<(s1!=s2)<<endl;
实例如下:
string mys = "hello";
cout << mys.size() << endl; //5
cout << mys.length() << endl; //5
cout << mys.capacity() << endl; //15
查看变量mys,包含有以下对象:
常用操作
string类型转为传统C风格:
string s="hello";
const char* cpp_str=s.c_str();
关键字const
距离char
近,代表定义一个指向字符常量的指针;
随机访问:
string s="hello";
s[0]='m';
s; //mello
字符串拷贝(方便且安全):
string s1="hello";
string s2=s1;
字符串连接:
string s1="hello",s2="world";
string s3=s1+s2; //拼接结果在s3,即s3:helloworld
s1+=s2; //拼接结果在s1,即s1:helloworld
string的出现极大方便了字符串的使用,但没有传统C语言的字符串精简高效,适用于对性能要求不高的情况