string的使用浅析

计算机如何表示字符

计算机使用数字编码来表示字符,最常见的编码方式是ASCII码(美国信息交换标准代码),通过将每个字符映射到8位二进制数字,共计128个字符,包含字母、数字、标点符号和一些控制字符。ASCII码表如下图所示:
ascii_Table2.png
图像来源于:http://www.asciima.com/ascii/12.html
随着计算机技术的发展,ASCII码已经逐渐被Unicode编码所取代。Unicode编码是一种用于表示所有字符的标准编码,包括ASCII码中的字符,以及世界上几乎所有语言的字符。Unicode编码使用32位二进制数字来表示每个字符,共计可以表示110万个字符
无论使用哪种编码方式,计算机都将字符转换为数字进行处理。

C语言中字符类型char

重复一下基本原理:在计算机下,都是将字符转换为数字进行处理。
在C语言中,char类型用于表示字符类型的数据。char类型的长度通常是一个字节,可以表示256个不同的字符,包含ASCII码和扩展ASCII码中的所有字符。

  1. 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;
}
  1. 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
//---------------------------------.//
  1. C语言的转义符
常见的转义符:
'\n': 换行符
'\\': 反斜杠
'\0': 空字符, ASCII码值为0
'0' : 字符0, ASCII码值为48

C++字符串类

在C++中,std::string是一个标准库中提供的字符串类,可以存储任意长度的字符串。

构造函数

C++98中提供7种方式来构建string对象,构建方式分别如下:

  1. 构建一个空字符串,长度为0;
string();
  1. 调用拷贝构造函数;
string (const string& str);
  1. 使用另一个string对象的部分构建string对象;
string (const string& str, size_t pos, size_t len = npos);
  1. 使用C语言字符串;
string (const char* s);
  1. 使用C语言字符串部分构建string对象;
string (const char* s, size_t n);
  1. 使用单个字符重复初始化;
string (size_t n, char c);
  1. 使用迭代器区间初始化;
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* ptrsize_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

字符串常用操作

容量操作

  1. 返回字符串的长度
size_t size() const;
size_t length() const;

上面两个函数的功能是相同的,返回字符串的长度。

  1. 将字符串resize到固定大小
void resize (size_t n);
void resize (size_t n, char c);
  1. 返回字符串对象分配的内存大小
size_t capacity() const;
  1. 修改字符串对象分配的内存带线啊哦
void reserve (size_t n = 0);
  1. 清空内容
void clear();
  1. 判断是否为空
bool empty() const;

访问元素

string对象有两种方式访问元素,operator[]at函数,区别在于at如果遇到下标i越界的情况,会抛出std::out_of_range异常终止程序。而operator[]不会抛出异常,知识简单地给字符串首地址指针和i做个加法,得到新的指针并解引用。如果i越界,则程序可能会崩溃,或者行为异常。

增删改查

  1. insert函数

insert函数支持将单个字符或者一个字符串插入到原字符串中第pos个字符后面。

string& insert (size_t pos, const char* s);
string& insert(size_t pos, char c)
  1. push_back函数

调用insert函数,在字符串的末尾插入单个字符。

void push_back (char c);
  1. append函数

调用insert函数,在字符串的末尾插入字符串内容。

string& append (const char* s);
  1. operator+=运算符重载

调用insert函数,完成在字符串的末尾插入单个字符和字符串内容。

string& operator+= (const char* s);
string& operator+= (char c);
  1. erase函数

erase函数用于删除从pos位置起,之后len个元素内容。

 string& erase (size_t pos = 0, size_t len = npos);
  1. 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,需要修改对象,则必须进行深拷贝;

因此,引用计数的写时拷贝的优点在于,如果对象不进行修改,则只需要增加计数即可,不用进行深拷贝,提高了效率。缺点:引用奇数存在线程安全问题,需要加锁,在多线程环境下要付出代价。

参考链接

猜你喜欢

转载自blog.csdn.net/hello_dear_you/article/details/129664013