コンピューターが文字を表現する仕組み
コンピュータは文字を表すためにデジタル エンコーディングを使用します。最も一般的なエンコーディング方法は ASCII (American Standard Code for Information Interchange) です。各文字を 8 ビットの2 進数にマッピングすることにより、文字、数字、句読点を含む合計 128 文字が存在します。およびいくつかの制御文字。ASCII コード表を以下の図に示します。
画像出典: http://www.asciima.com/ascii/12.html
コンピュータ技術の発展に伴い、ASCII コードは徐々に Unicode コードに置き換えられました。Unicode は、ASCII 文字を含むすべての文字と、世界中のほぼすべての言語の文字を表すために使用される標準エンコードです。Unicode エンコードでは、32 の 2 進数を使用して各文字を表現し、合計 110 万文字を表現できます。
使用されているエンコードに関係なく、コンピューターは文字を数値に変換して処理します。
C言語の文字型char
基本原理を繰り返します。コンピュータの下では、文字は処理のために数値に変換されます。
C 言語では、char
文字型のデータを表すために型が使用されます。char
型の長さは通常 1 バイトで、ASCII
包含コードと拡張コードASCII
のすべての文字を含む 256 個の異なる文字を表すことができます。
- 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;
}
- 文字列
C 言語では、文字列は特定の方法で配置された文字配列のセットであり、null 文字で終了します。C 言語の文字列が NULL 文字で終わるのはなぜですか: C 言語の文字列の各文字は連続してコンパクトに配列されているため、プログラムが文字列の長さを認識するには、文字列に特殊文字が含まれている必要があります。文字列の終わりを識別するために使用されるこの特殊文字は、ヌル文字です。ヌル文字の素晴らしい使い方は、C言語の文字列の「0で終わる」という特徴を利用して、本来ゼロでない文字に0を書いて文字列を早く終わらせることができます。\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 つの方法が提供されており、その構築方法は次のとおりです。
- 長さ 0 の空の文字列を構築します。
string();
- コピー コンストラクターを呼び出します。
string (const string& str);
- 別の文字列オブジェクトの一部を使用して文字列オブジェクトを構築します。
string (const string& str, size_t pos, size_t len = npos);
- C 言語の文字列を使用します。
string (const char* s);
- C 言語の文字列部分を使用して文字列オブジェクトを構築します。
string (const char* s, size_t n);
- 単一文字による初期化の繰り返し。
string (size_t n, char c);
- イテレータ範囲で初期化します。
template <class InputIterator>
string (InputIterator first, InputIterator last);
文字列オブジェクトの初期化の例は次のとおりです。
// 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++ 文字列は文字列クラスであり、そのメンバーには と の 2 つがあり、
char* ptr
2size_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;
上記 2 つの関数の機能は同じで、文字列の長さを返します。
- 文字列のサイズを固定サイズに変更する
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;
アクセス要素
文字列オブジェクトには要素operator[]
とat
関数にアクセスする 2 つの方法がありますが、違いは、at
添字 i が範囲外の場合、std::out_of_range
例外がスローされてプログラムが終了することです。operator[]
例外をスローする代わりに、ナレッジは単に文字列の最初のアドレス ポインタを追加してi
新しいポインタを取得し、それを逆参照します。i
境界を超えると、プログラムがクラッシュしたり、誤動作したりする可能性があります。
クラッド
- 挿入関数
挿入関数は、元の文字列内の文字 pos の後に単一の文字または文字列を挿入することをサポートします。
string& insert (size_t pos, const char* s);
string& insert(size_t pos, char c)
- プッシュバック関数
insert 関数を呼び出して、文字列の末尾に 1 文字を挿入します。
void push_back (char c);
- 追加関数
insert 関数を呼び出して、文字列の内容を文字列の末尾に挿入します。
string& append (const char* s);
- 演算子+= 演算子のオーバーロード
insert 関数を呼び出して、文字列の末尾に 1 つの文字と文字列コンテンツを挿入します。
string& operator+= (const char* s);
string& operator+= (char c);
- 消去機能
erase
この関数は、pos
position から次の要素の内容を削除するために使用されますlen
。
string& erase (size_t pos = 0, size_t len = npos);
- 検索関数
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;
}
浅いコピーと深いコピー
浅いコピー
浅いコピー: コンパイラはオブジェクト内の値をコピーするだけです。オブジェクト内に管理リソースがある場合、最終的には複数のオブジェクトが同じリソースを共有します。オブジェクトが破棄されると、リソースは解放され、今度は別のオブジェクトはリソースが解放されたことを認識していないため、リソース上で操作を続けるとアクセス違反の問題が発生します。
次のコードに示すように、文字列クラスが定義され、コンストラクターとデストラクターのみが指定されますが、コピー コンストラクターと代入演算子のオーバーロードはデフォルトでコンパイラーによって生成されます。
#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;
}
};
次のコードは、コピーして初期化を割り当てるときに、文字列オブジェクトが同じスペースを指しているように見えます。実行するとreturn 0
、スペースを解放するために 3 つのオブジェクトのデストラクターが順番に呼び出されます。
3 つの文字列オブジェクトが指すアドレス空間は同じであるため、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 つしかないため、既存のオブジェクトがコピーと代入を呼び出すと、複数のオブジェクトが同じメモリ空間を指します。つまり、この時点での参照カウントは 1 より大きくなります。 。このとき、新しい変数が再定義されますが、count変数は1つだけなので、内容は変わりませんし、このときデータがめちゃくちゃになってしまいます。
ポインターを参照変数として使用すると、要件を満たすだけでなく、他の同一オブジェクトの参照カウントに影響を与えることなく、異なるオブジェクトごとに参照カウントが作成されるようになります。
コピーオンライトの牛
浅いコピーの問題:
- 複数回の分割
- オブジェクトの 1 つによって行われた変更は、もう一方のオブジェクトに影響を与えます
上記の問題に対処するために、参照カウントのコピーオンライトが提案されており、その具体的な原理は次のとおりです。
- 最初の問題については、浅いコピーを使用すると、複数のオブジェクトが同じ領域を指します。このとき、カウンタに 1 を加算するカウンタを導入することができ、破棄する場合、カウンタが 1 より大きい場合はカウンタを 1 減算し、カウンタが 1 の場合は再度デストラクタを呼び出します。
- 2 番目の質問では、カウンターが 1 ではなく、オブジェクトを変更する必要がある場合は、ディープ コピーを実行する必要があります。
したがって、参照カウントのコピーオンライトの利点は、オブジェクトが変更されない場合、ディープコピーを行わずにカウントを増やすだけで済み、効率が向上することです。短所: 奇数を引用するとスレッドの安全性の問題があり、ロックが必要であり、マルチスレッド環境では代償が発生します。