[TOC]
简介
串(string)是由零个或多个字符组成的有限序列,又名字符串。
上述描述是对字符串的定义,将其使用数学方法进行描述,一般记为:,其中:
- 是字符串的名称,用双引号括起来的是字符串的内容(值),注意双引号本身不属于字符串内容。
- 表示字符串中第 个位置的字符。
- 表示字符串的长度,当 时,表示字符串内容为空,即 “空串”,可以直接使用两个双引号
""
表示。
从上述字符串的定义中,还可以看到以下几方面内容:
- 零个或多个字符组成:说明字符串的内部元素类型为字符。
注:关于字符的更加详细释义,请参考 附录。 - 有限:说明字符串的内容长度是有一定限制的,小于一个最大范围,但是在该范围内,实际的长度是不确定的。
- 序列:说明字符串中的相邻字符存在前驱和后继的关系。
一些名词释义
空串:表示字符串没有内容,可用两个双引号
""
进行表示。空格串:表示只包含一个或多个空格的字符串。注意其与 空串 的区别,空格串 是有内容的,其内容为一个或多个空格。空串 没有内容,且其长度为
0
。主串:具备全部内容的字符串。
子串:字符串中任意个数的连续字符组成的子串称为该串的子串。
串的比较:两个串的大小比较方法是依次比较相同位置上的字符码值,首次不同时则比较完成。
字符串的存储结构
字符串的逻辑结构和线性表很相似,因此这里可以借助线性表的典型结构,即数组和链表来实现字符串结构:
串的顺序存储结构:即基于数组实现字符串结构。首先分配一个足够大的数组作为字符串的底层数据结构,然后依次将字符存储到数组对应位置上,完成存储。
串的链式存储结构:即基于链表实现字符串结构。由于字符串的数据元素为字符,如果采用链表作为字符串的底层数据结构,则需要为每一个字符创建一个链表结点,链表结点最少还应当具备一个后继指针域,如果要求具备往前回溯功能,则还应当创建一个前驱指针域,相对来说,基于链表的字符串在空间上存在资源使用过大问题。
当然,可以通过其他方式提高串的链式存储结构效率,但是总体来说,使用数组作为字符串的底层结构更加简单直接,性能上也更加优越。
因此,本文主要介绍基于数组的方式实现字符串数据结构。
字符串的基本操作
字符串的基本操作包含如下内容:
// 头文件
#ifndef __STRING_H__
#define __STRING_H__
#include
#define DEFAULT_LENGTH 100 // 底层数组默认长度
class String {
private:
char* pstr = nullptr; // 字符串指针
std::size_t capacity = DEFAULT_LENGTH; // 底层数组最大容量
std::size_t length = 0; // 字符串长度
private:
void init();
void checkIfNeedEnlarge(); // 长度超过默认长度,则动态扩展
public:
String(); // 空串
String(char c); // 一个字符串
String(const char* str); // 字符串指针
String(const String& str); // 字符串引用
~String(); // 析构函数
// 获取字符串长度
size_t size() const;
// 清空
void clear();
// 判空
bool isEmpty() const;
// 子串开头
bool startsWith(const char* str) const;
bool startsWith(const String& str) const;
// 子串结尾
bool endsWith(const char* str) const;
bool endsWith(const String& str) const;
// 删除字符串前后空白符
String trim();
// 获取索引 i 的字符
char& operator[](std::size_t i);
// 比较
bool operator==(const char* str) const;
bool operator==(const String& str) const;
bool operator!=(const String& str) const;
bool operator!=(const char* str) const;
// 字符串拼接 concat
String& operator+=(const String& str);
String& operator+=(const char* str);
String& operator+=(const char c);
String operator+(const String& str) const;
String operator+(const char* str) const;
String operator+(const char c) const;
// 支持输出操作
public:
friend std::ostream& operator<<(std::ostream& os, const String& str);
};
#endif
各个操作对应的具体实现如下所示:
#include "String.h"
void String::init() {
// 动态开辟一块内存,并初始化为 0
this->pstr = new char[DEFAULT_LENGTH] {0};
if (this->pstr == nullptr) {
throw "No extra memory to create String object!";
}
}
void String::checkIfNeedEnlarge() {
if (this->length >= this->capacity) {
// 开启一块更大的内存
char* p = new char[this->capacity += DEFAULT_LENGTH];
// 复制源字符串数组
std::memcpy(p, this->pstr, this->length);
// 释放源数组内存
free(this->pstr);
// 指向新字符串数组内存空间
this->pstr = p;
}
}
String::String() {
init();
}
String::String(char c) {
init();
(*this) += c;
}
String::String(const char* str) {
init();
(*this) += str;
}
String::String(const String& str) {
init();
(*this) += str;
}
String::~String() {
if (this->pstr) {
delete[] this->pstr;
}
}
size_t String::size() const {
return this->length;
}
void String::clear() {
std::memset(this->pstr, 0, this->length);
this->length = 0;
}
bool String::isEmpty() const {
return this->length == 0;
}
bool String::startsWith(const char* str) const {
bool ret = (str != nullptr);
do {
// str 为空
if (!ret) {
ret = false;
break;
}
// 子串长度大于主串,直接返回 false
size_t len = strlen(str);
if (len > this->length) {
ret = false;
break;
}
ret = true;
for (size_t i = 0; i < len; ++i) {
if (str[i] != this->pstr[i]) {
ret = false;
break;
}
}
} while (false);
return ret;
}
bool String::startsWith(const String& str) const {
return this->startsWith(str.pstr);
}
bool String::endsWith(const char* str) const {
bool ret = (str != nullptr);
do {
if (!ret) {
ret = false;
break;
}
size_t len = strlen(str);
if (len > this->length) {
ret = false;
break;
}
ret = true;
for (size_t i = 0; i < len; ++i) {
if (str[len - 1 - i] != this->pstr[this->length - 1 - i]) {
ret = false;
break;
}
}
} while (false);
return ret;
}
bool String::endsWith(const String& str) const {
return this->endsWith(str.pstr);
}
String String::trim() {
size_t begin = 0;
size_t end = this->length - 1;
while (this->pstr[begin] == ' ') {
++begin;
}
while (this->pstr[end] == ' ') {
--end;
}
String trimStr;
while (begin <= end) {
trimStr += this->pstr[begin++];
}
return trimStr;
}
char& String::operator[](std::size_t i) {
return this->pstr[i];
}
bool String::operator==(const char* str) const {
size_t len = strlen(str);
if (this->length != len) {
return false;
}
for (size_t i = 0; i < len; ++i) {
if (str[i] != this->pstr[i]) {
return false;
}
}
return true;
}
bool String::operator==(const String& str) const {
if (this->length != str.size()) {
return false;
}
for (size_t i = 0; i < this->length; ++i) {
if (str.pstr[i] != this->pstr[i]) {
return false;
}
}
return true;
}
bool String::operator!=(const String& str) const {
return !(*this == str);
}
bool String::operator!=(const char* str) const {
return !(*this == str);
}
String& String::operator+=(const String& str) {
size_t len = str.size();
// 先增加长度
this->length += len;
// 在检查
this->checkIfNeedEnlarge();
// 最后复制
for (size_t i = 0; i < len; ++i) {
this->pstr[this->length - len + i] = str.pstr[i];
}
return *this;
}
String& String::operator+=(const char* str) {
size_t len = strlen(str);
for (size_t i = 0, len = strlen(str); i < len; ++i) {
(*this) += str[i];
}
return *this;
}
String& String::operator+=(const char c) {
// 先检查边界
this->checkIfNeedEnlarge();
this->pstr[this->length++] = c;
return *this;
}
String String::operator+(const String& str) const {
String newStr(*this);
return newStr += str;
}
String String::operator+(const char* str) const {
String newStr(*this);
return newStr += str;
}
String String::operator+(const char c) const {
String newStr(*this);
return newStr += c;
}
std::ostream& operator<<(std::ostream& os, const String& str) {
os << str.pstr;
return os;
}
注:以上实现更多的在于展现思路,具体代码未经严格测试,可能存在错误,请知悉。
附录
-
字符:百度百科对字符的定义如下
字符指类字形单位或符号,包括字母、数字、运算符号、标点符号和其他符号,以及一些功能性符号。字符是电子计算机或无线电通信中字母、数字、符号的统称,其是数据结构中最小的数据存取单位...
可以简单将字符视作为一种符号,但是这里就存在一种问题,因为计算机只能处理
0
、1
这样的数值类型数据,所以对于字符这种符号类型数据,计算机不能直接进行处理,因此,这里存在一个转换/映射过程。这个映射过程就叫 编码,比如,当计算机采用 ASCII 编码时,大写字符
A
就会被映射为数值65
,而进行打印等操作的时候,计算机会通过查询 ASCII 编码表,就可以将65
转换为A
进行输出,这样就完成了字符类型的输入输出操作。但是,ASCII 码采用 7 位二进制数表示一个字符,因此它最多能表示 128 个字符,这样有一些特殊符号就无法进行表示,后来就采用 8 位二进制数表示一个字符,称为扩展 ASCII 码,它最多能够表示 256 个字符。扩展 ASCII 码对于以英语为母语的国家已经足够使用了,但是对于亚洲等国家,扩展 ASCII 码还远远无法满足这些国家的文字映射。
最初的一段时间,各个国家为了能让计算机支持显示自己的文字符号,就自定义了一套满足自己国家需求的编码表,比如中国制定的
GB2312
编码,就可以编码简体中文字符,比如日本的日文编码表Shift_JIS
,比如韩国的韩文编码表Euc-kr
...这种每个国家都自定义一套编码表存在的问题就是编码冲突,比如当不同国家相互通信的时候,就会由于解码方式不同导致乱码产生。因此,国际间需要一套标准编码表,以支持所有国家的文字符号。而这套编码表就是 Unicode。
Unicode 本身是一种规定,它规定了每个字符对应的数字编码(即码值),但是没有规定这个码值如何存储(简而言之,Unicode 是一种字符集)。比如,对于字符
A
,Unicode 只规定了它对应的数字编码为65
,但是是使用 1 字节进行存储,还是使用 2 字节进行存储,由不同的编码形式决定。Unicode 的编码形式一般称为 UTF-* 编码,常见的有 UTF-8、UTF-16 和 UTF-32。这里需要注意一下,早期的 Unicode 采用两字节编码方案,该方案称为 "Unicode",后来又改名为 UCS-2,但是后来发现 16 位编码无法囊括所有字符,然后引入了一种采用四字节编码的 UCS-4 方案,但是该编码方案太浪费空间了,最终为了平衡 UCS-2 和 UCS-4 两套编码之间的僵局,采用了新的编码方案 UTF-16。所以 UTF-16 其实是一种变长编码,每个字符编码为 16 位或者 32 位,对于码值在65535
之内的字符,采用两字节编码,超出的字符则采用四字节编码。Unicode 虽然统一了编码字符集,解决了多语言乱码问题,但是它本身在空间利用率上(早期 Unicode 使用两字节方案),稍显不足,比如对于字符
A
,使用 ASCII 码编码的话,其二进制为01000001
,而采用 Unicode 编码,则为00000000 01000001
,可以看到,Unicode 对于字符A
的编码,其实就是在 ASCII 编码的前面补0
,数值完全一样,但是 Unicode 比 ASCII 编码多占用了一倍的存储空间,存在资源浪费问题,并且对于文本的存储与传输会带来较大的性能损耗。同时,除了在空间利用率效率低之外,Unicode 还存在无法识别问题,因为虽然大部分编码采用两个字节,但是有些偏僻字符会采用三字节或者四字节...计算机无法区分当前是两字节编码字符还是三字节编码字符...因此,为了解决以上问题,又出现了 UTF-8 编码,它是对 Unicode 的一种优化,将 Unicode 编码转化为可变长编码,即 UTF-8 编码对于一个 Unicode 字符,会根据其数字编码大小编码成 1~6 个字节,常用的英文字母被编码成 1 个字节,汉字通常使用 3 个字节,其他比较偏僻的字符才会被编码为 4~6 个字节,这样,就可以节省空间。同时,UTF-8 会给每个 Unicode 字符编码进行标记,使得计算机通过该标记就知道当前 Unicode 字符占据的字节数,增加容错性。
综上所述,Unicode 是一种包含了全世界所有字符的编码集,而 UTF-* 是对 Unicode 的具体实现,其中,UTF-8 是当前使用最多的 Unicode 实现。
在计算机内存中,统一使用 Unicode 编码,当需要保存到磁盘或者进行传输的时候,就转换为 UTF-8 编码。比如,对于以 UTF-8 保存的文本文件,程序进行读取时,指定编码格式为 UTF-8,这样就能正确识别文本字符对应的字节数并将其转换为相应的 Unicode 码值,存储到内存中。当保存数据时,程序将内存中的字符码值转换为 UTF-8 的编码格式,即将一个数值以 UTF-8 的编码格式转换为另一个数值(二进制表示),存储到文本文件中即可。
参考
- 《大话数据结构》
- 数据结构--字符串类String(一)
- 廖雪峰 - 字符串和编码
- 字符串,那些你不知道的事