计算机如何表示字符
计算机使用数字编码来表示字符,最常见的编码方式是ASCII码(美国信息交换标准代码),通过将每个字符映射到8位二进制数字,共计128个字符,包含字母、数字、标点符号和一些控制字符。ASCII码表如下图所示:
图像来源于:http://www.asciima.com/ascii/12.html
随着计算机技术的发展,ASCII码已经逐渐被Unicode编码所取代。Unicode编码是一种用于表示所有字符的标准编码,包括ASCII码中的字符,以及世界上几乎所有语言的字符。Unicode编码使用32位二进制数字来表示每个字符,共计可以表示110万个字符。
无论使用哪种编码方式,计算机都将字符转换为数字进行处理。
C语言中字符类型char
重复一下基本原理:在计算机下,都是将字符转换为数字进行处理。
在C语言中,char
类型用于表示字符类型的数据。char
类型的长度通常是一个字节,可以表示256个不同的字符,包含ASCII
码和扩展ASCII
码中的所有字符。
- char即数字
在C语言中,字符常量在内存中以ASCII码的形式存储,因此可以将char类型的变量视为整数类型处理,直接使用ASCII码值来进行运算。
C语言中只规定了unsigned char是无符号8位整数,signed char是有符号8位整数,而char类型只需是8位整数即可,可以是有符号,也可以是无符号,全凭编译器决定。
int main()
{
char temp = 'A'; // int: 65
cout << (int)temp << endl;
temp += 32; // int: 97, char: a
cout << temp << endl;
return 0;
}
- char字符串
在C语言中,字符串是一组按照特定方式排列的字符数组,以\0
空字符结尾。C语言字符串为什么要以空字符\0
结尾:由于C语言字符串中的每个字符都是连续紧凑排列在一个数组中,为了让程序能够知道字符串的长度,字符串中必须包含一个特殊字符,用于标识字符串的结尾,这个特殊字符就是空字符\0
。
空字符的妙用,利用C语言字符串“以0结尾”这个特点,可以在一个本来非0的字符处写入0,来提前结束字符串。
int main()
{
char temp[12] = "hello c"; // 空格对应的ASCII码值为32
cout << "sizeof: " << sizeof(temp) << endl;
cout << "strlen: " << strlen(temp) << endl;
temp [4] = 0;
cout << "sizeof: " << sizeof(temp) << endl;
cout << "strlen: " << strlen(temp) << endl;
return 0;
}
//----------outputs-----------------//
// sizeof: 12
// strlen: 7
// sizeof: 12
// strlen: 4
//---------------------------------.//
- C语言的转义符
常见的转义符:
'\n': 换行符
'\\': 反斜杠
'\0': 空字符, ASCII码值为0
'0' : 字符0, ASCII码值为48
C++字符串类
在C++中,std::string是一个标准库中提供的字符串类,可以存储任意长度的字符串。
构造函数
C++98中提供7种方式来构建string对象,构建方式分别如下:
- 构建一个空字符串,长度为0;
string();
- 调用拷贝构造函数;
string (const string& str);
- 使用另一个string对象的部分构建string对象;
string (const string& str, size_t pos, size_t len = npos);
- 使用C语言字符串;
string (const char* s);
- 使用C语言字符串部分构建string对象;
string (const char* s, size_t n);
- 使用单个字符重复初始化;
string (size_t n, char c);
- 使用迭代器区间初始化;
template <class InputIterator>
string (InputIterator first, InputIterator last);
string对象初始化示例如下:
// string constructor
#include <iostream>
#include <string>
int main ()
{
std::string s0 ("Initial string");
// constructors used in the same order as described above:
std::string s1; \\ 1
std::string s2 (s0); \\ 2
std::string s3 (s0, 8, 3); \\ 3
std::string s4 ("A character sequence"); \\ 4
std::string s5 ("Another character sequence", 12); \\ 5
std::string s6a (10, 'x'); \\ 6
std::string s6b (10, 42); // 42 is the ASCII code for '*'
std::string s7 (s0.begin(), s0.begin()+7); \\ 7
std::cout << "s1: " << s1 << "\ns2: " << s2 << "\ns3: " << s3;
std::cout << "\ns4: " << s4 << "\ns5: " << s5 << "\ns6a: " << s6a;
std::cout << "\ns6b: " << s6b << "\ns7: " << s7 << '\n';
return 0;
}
C++字符串和C字符串的不同
C++字符串和C字符串的主要区别是C++字符串是一个类,而C字符串是一个字符数组。在表达方式上也存在如下区别:
- C语言字符串是单独的一个
char* ptr
,自动以\0
结尾; - C++字符串是string类,其成员有两个:
char* ptr
和size_t len
,其中第二个成员用来确定结尾的位置,不需要以\0
结尾;
char data[20] = "hello\0world";
cout << data << endl; // hello
string str1(data);
cout << str1 << endl; // hello
string str2(data, 11);
cout << str2 << endl; // helloworld
字符串常用操作
容量操作
- 返回字符串的长度
size_t size() const;
size_t length() const;
上面两个函数的功能是相同的,返回字符串的长度。
- 将字符串resize到固定大小
void resize (size_t n);
void resize (size_t n, char c);
- 返回字符串对象分配的内存大小
size_t capacity() const;
- 修改字符串对象分配的内存带线啊哦
void reserve (size_t n = 0);
- 清空内容
void clear();
- 判断是否为空
bool empty() const;
访问元素
string对象有两种方式访问元素,operator[]
和at
函数,区别在于at
如果遇到下标i越界的情况,会抛出std::out_of_range
异常终止程序。而operator[]
不会抛出异常,知识简单地给字符串首地址指针和i
做个加法,得到新的指针并解引用。如果i
越界,则程序可能会崩溃,或者行为异常。
增删改查
- insert函数
insert函数支持将单个字符或者一个字符串插入到原字符串中第pos个字符后面。
string& insert (size_t pos, const char* s);
string& insert(size_t pos, char c)
- push_back函数
调用insert函数,在字符串的末尾插入单个字符。
void push_back (char c);
- append函数
调用insert函数,在字符串的末尾插入字符串内容。
string& append (const char* s);
- operator+=运算符重载
调用insert函数,完成在字符串的末尾插入单个字符和字符串内容。
string& operator+= (const char* s);
string& operator+= (char c);
- erase函数
erase
函数用于删除从pos
位置起,之后len
个元素内容。
string& erase (size_t pos = 0, size_t len = npos);
- find函数
find
函数支持从指定位置pos
开始查找,是否包含单个字符c
或者字符串内容。
size_t find (const char* s, size_t pos = 0) const;
size_t find (char c, size_t pos = 0) const;
切下一段子字符串
substr
函数会从第pos
个字符开始,截取长度为len的子字符串,原字符串内容不会改变。
边界问题:
- 如果原字符串剩余部分长度不足len,则返回长度小于len的子字符串而不会出错。
- 如果pos超出了原字符串的范围,则抛出
std::out_of_range
异常
string substr (size_t pos = 0, size_t len = npos) const;
模拟实现
class string
{
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos;
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
const_iterator begin() const
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator end() const
{
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;
}
// 常用操作
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
char& operator[] (size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char&
operator[] (size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
// request a change in capacity
void reverse(size_t n=0)
{
if(n > _capacity)
{
// 重新分配内存, 并释放旧的内存
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete [] _str;
_str = tmp;
_capacity = n;
}
}
void resize(size_t n, char c)
{
if (n <= _size)
{
_size = n;
_str[n] = '\0';
}
else
{
if(n > _capacity)
{
reverse(n);
}
memset(_str + _size, c, n - _size);
_size = n;
_str[_size] = '\0';
}
}
// 在指定位置插入一个字符
string& insert(size_t pos, char c)
{
assert(pos <= _size); // 分配的内存大小为size + 1
if(_size == _capacity)
{
// 两倍扩容
reverse(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size + 1;
while(end > pos)
{
// 指定位置后面的数据向后移动一步
_str[end] = _str[end - 1];
--end;
}
_str[pos] = c;
++_size;
return *this;
}
// 在指定位置插入字符串
string& insert(size_t pos, const char* s)
{
assert( pos <= _size);
size_t len = strlen(s);
if(_size + len > _capacity)
{
reverse(_size + len);
}
size_t end = _size + len;
while (end >= pos +len)
{
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, s, len);
_size += len;
return *this;
}
void push_back(char ch)
{
insert(_size, ch);
}
void append(const char* str)
{
insert(_size, str);
}
string& operator+= (char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
// 删除
string& erase(size_t pos=0, size_t len=npos)
{
assert(pos < _size);
if(len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len); // 使用后面的内容覆盖前面的内容
_size -= len;
}
return *this;
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
// 查询
size_t find(char ch)
{
// 顺序查找
for(size_t i=0; i<_size; ++i)
{
if(ch == _str[i])
return i;
}
return npos; // -1
}
size_t find(const char* s, size_t pos = 0)
{
const char* ptr = strstr(_str+pos, s);
if(ptr == nullptr)
return npos;
else
return ptr - _str;
}
};
const size_t string::npos = -1;
常见的运算符重载实现:
// 常见运算符重载
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 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 !(s1 == s2);
}
// output stream
ostream& operator<< (ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
// input stream
istream& operator>>(istream& in, string& s)
{
char ch = in.get();
while(ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
浅拷贝和深拷贝
浅拷贝
浅拷贝:编译器只是将对象中的值拷贝过来,如果对象中有管理资源,最后会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另外的对象不知道该资源已经被释放,继续对资源进行操作,就会出现访问违规问题。
如下代码所示,定义了一个string类,只给出了构造函数和析构函数,而拷贝构造函数和赋值运算符重载由编译器默认生成。
#include <cstring>
class string
{
private:
char* _str;
public:
string(const char* str="")
: _str(new char[strlen(str) + 1]
{
if(str != nullptr)
strcpy(_str, str);
}
~string()
{
delete [] _str;
_str = nullptr;
}
};
如下代码,当进行拷贝和赋值初始化,会出现string对象都指向同一块空间,运行到return 0
时,会依次调用三个对象的析构函数来释放空间。
由于三个string对象所指的地址空间相同,当析构完str3对象之后,再释放str2时,由于空间已经被释放,导致释放失败,出现报错。
int main()
{
string str1("Hello C++");
string str2(str1); // 调用拷贝构造函数
string str3 = str2; // 调用赋值运算符重载
return 0;
}
深拷贝
为了避免浅拷贝问题,可以使用深拷贝。简单实现代码如下:
// 拷贝构造函数
string::string(const string& s)
: _str(new char[strlen(s._str) + 1)
{
if(s._str != nullptr)
strcpy(_str, s._str);
}
// 赋值运算符重载
string& string::operator=(const string& s)
{
if( nullptr != s._str)
{
delete[] _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
return *this;
}
引用计数
#include <cstring>
class string
{
private:
char* _str;
int* _pCount;
public:
string(const char* str="")
: _str(new char[strlen(str) + 1], _pCount(new int)
{
if(str != nullptr)
strcpy(_str, str);
}
// 拷贝构造函数
string(const string& s)
{
_str = s._str;
_pCount = s._pCount;
++(*_pCount);
}
// 赋值运算符重载
string& operator=(const string& s)
{
delete[] _str;
_str = s._str;
_pCount = s._pCount;
++(*pCount);
return *this;
}
~string()
{
if(--(*_pCount) == 0)
{
delete [] _str;
delete _pCount;
_str = nullptr;
_pCount = nullptr;
}
}
};
为什么使用指针做引用计数,而不使用成员变量和静态成员变量?
- 成员变量:当调用拷贝和赋值来初始化对象时,如果采用成员变量做引用计数,只有当前对象的计数值发生变化,而其他共享的对象没有更新;
- 静态成员变量
class String
{
private:
char* _str;
public:
static int _count;
String(const char* s="")
: _str(new char[strlen(s) + 1])
{
if(s != nullptr)
{
strcpy(_str, s);
}
}
String(const String& s)
{
_str = s._str;
++_count;
}
String& operator=(const String& s)
{
delete[] _str;
_str = s._str;
++_count;
return *this;
}
~String()
{
if(--_count == 0)
{
delete[] _str;
_str = nullptr;
}
}
};
int String::_count = 1;
int main()
{
String str1("Hello C++");
String str2(str1);
String str3 = str1;
cout << String::_count << endl; // 3
String str4("other data");
cout << String::_count << endl; // 3
return 0;
}
当使用静态成员变量来做计数,由于一个类只有一份该变量,当一个已有的对象调用拷贝和赋值,使得多个对象都指向相同的内存空间,即此时引用计数大于1。这时重新定义一个新的变量,而计数变量只有一个,内容不会发生变化,此时数据就乱了。
使用指针做引用变量不仅能达到要求,还能保证为每个不同对象创建一份引用计数,不会影响其他相同对象的引用计数。
写时拷贝cow
浅拷贝存在的问题:
- 析构多次
- 其中一个对象进行修改时会影响另一个对象
针对上面的问题,提出了引用计数的写时拷贝,具体的原理如下:
- 针对第一个问题,使用浅拷贝时,多个对象指向同一块区域。此时可以引入计数器,让计数器加一,当析构时,若计数器大于1,则让计数器减一,当计数器为1时,再调用析构函数;
- 针对第二个问题,如果计数器不是1,需要修改对象,则必须进行深拷贝;
因此,引用计数的写时拷贝的优点在于,如果对象不进行修改,则只需要增加计数即可,不用进行深拷贝,提高了效率。缺点:引用奇数存在线程安全问题,需要加锁,在多线程环境下要付出代价。