C++语法知识点合集:7.string类


一、标准库中的string类

1.string类

  • string是表示字符串的字符串类
  • 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
  • string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator> string;
  • 不能操作多字节或者变长字符的序列。
  • 在使用string类时,必须包含#include头文件以及using namespace std;

2.auto和范围for

  1. auto关键字:自动推断类型,作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期
    推导而得。
  • 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
  • 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
  • auto不能作为函数的参数,可以做返回值,但是谨慎使用。
  • auto不能直接用来声明数组。
#include <iostream>
using namespace std;
int func1()
{
    return 10;
}
// 不能做参数
// void func2(auto a)
//{
//} // 可以做返回值,但是建议谨慎使用
auto func3()
{
    return 3;
}
int main()
{
    int a = 10;
    auto b = a;
    auto c = 'a';
    auto d = func1();
    // 编译报错:rror C3531: “e”: 类型包含“auto”的符号必须具有初始值设定项
    // auto e;
    cout << typeid(b).name() << endl; // typeid(variable).name()返回的是类型名称的一个编码
    cout << typeid(c).name() << endl; // int-i char-c  int*-Pi
    cout << typeid(d).name() << endl;
    int x = 10;
    auto y = &x;
    auto *z = &x;
    auto &m = x;
    cout << typeid(x).name() << endl;
    cout << typeid(y).name() << endl;
    cout << typeid(z).name() << endl;
    auto aa = 1, bb = 2;
    // 编译报错:error C3538: 在声明符列表中,“auto”必须始终推导为同一类型
    // auto cc = 3, dd = 4.0;
    // 编译报错:error C3318: “auto []”: 数组不能具有其中包含“auto”的元素类型
    // auto array[] = {4, 5, 6};
    return 0;
}
#include <iostream>
#include <string>
#include <map> // 导入映射容器类 std::map 用于存储键值对
using namespace std;
int main()
{
    // 表示键和值都是std::string类型的映射(即字典)。
    std::map<std::string, std::string> dict = {{"apple", "苹果"}, {"orange", "橙子"}, {"pear", "梨"}};
    // auto的用武之地
    //::iterator 是 std::map 内部定义的一个类型,用来表示一个指向 std::map 容器元素的迭代器。
    // 迭代器可以看作是一个指针,它用于遍历容器中的元素。
    //  std::map<std::string, std::string>::iterator it = dict.begin();
    // it = dict.begin(); it 是声明的迭代器变量。
    // dict.begin() 是一个方法,它返回一个迭代器,指向 dict 容器中的第一个元素。即访问 dict 中的第一个键值对。
    auto it = dict.begin();
    while (it != dict.end())
    {
        cout << it->first << ":" << it->second << endl;
        ++it;
    }
    return 0;
}
  1. 范围for
  • 对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。
  • for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。
  • 范围for可以作用到数组和容器对象上进行遍历
  • 范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。
#include <iostream>
#include <string>
#include <map>
using namespace std;
int main()
{
    int array[] = {1, 2, 3, 4, 5};
    // C++11的遍历
    // 这相当于告诉编译器 请遍历 array 中的每一个元素,并在每次迭代时将当前元素的引用赋给 e。
    for (auto &e : array) //: 表示遍历的范围 左边是循环变量 右边是要遍历的容器
        e *= 2;
    for (auto e : array)
        cout << e << " " << endl;
    string str("hello world");
    for (auto ch : str)
    {
        cout << ch << " ";
    }
    cout << endl;
    return 0;
}

3.string类的常用接口说明

  1. string类对象的常见构造
  • string() 构造空的string类对象,即空字符串
  • string(const char* s) 用C-string来构造string类对象
  • string(size_t n, char c) string类对象中包含n个字符c
  • string(const string&s) 拷贝构造函数
void Teststring()
{
	string s1; // 构造空的string类对象s1
	string s2("hello bit"); // 用C格式字符串构造string类对象s2
	string s3(s2); // 拷贝构造s3
}
  1. string类对象的容量操作
  • size 返回字符串有效字符长度
// string::size
#include <iostream>
#include <string>

int main ()
{
  std::string str ("Test string");
  std::cout << "The size of str is " << str.size() << " bytes.\n";
  return 0;
}
  • length 返回字符串有效字符长度
  • capacity 返回空间总大小
  • empty 检测字符串释放为空串,是返回true,否则返回false
  • clear 清空有效字符
  • reserve 为字符串预留空间
// string::reserve
#include <iostream>
#include <fstream>
#include <string>

int main ()
{
  std::string str;

  std::ifstream file ("test.txt",std::ios::in|std::ios::ate);
  if (file) {
    std::ifstream::streampos filesize = file.tellg();
    str.reserve(filesize);

    file.seekg(0);
    while (!file.eof())
    {
      str += file.get();
    }
    std::cout << str;
  }
  return 0;
}
  • resize 将有效字符的个数改成n个,多出的空间用字符c填充
// resizing string
#include <iostream>
#include <string>

int main ()
{
  std::string str ("I like to code in C");
  std::cout << str << '\n';

  unsigned sz = str.size();

  str.resize (sz+2,'+');
  std::cout << str << '\n';

  str.resize (14);
  std::cout << str << '\n';
  return 0;
}

output

I like to code in C
I like to code in C++
I like to code

  1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况
    下基本都是用size()。
  2. clear()只是将string中有效字符清空,不改变底层空间大小。
  3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
  4. reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserve不会改变容量大小。
  1. string类对象的访问及遍历操作
  • operator[] 返回pos位置的字符,const string类对象调用
// string::operator[]
#include <iostream>
#include <string>
using namespace std;
int main()
{
    string str("Test string");
    for (int i = 0; i < str.length(); ++i)
    {
        cout << str[i];
    }
    return 0;
}
  • begin+ end begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
// string::begin/end
#include <iostream>
#include <string>
using namespace std;
int main()
{
    string str("Test string");
    for (string::iterator it = str.begin(); it != str.end(); ++it)
        cout << *it;
    cout << '\n';

    return 0;
}
  • rbegin + rend rbegin一个指向容器中最后一个元素的反向迭代器 + rend返回一个指向字符串开头之前位置的反向迭代器。
// string::rbegin/rend
#include <iostream>
#include <string>
using namespace std;
int main()
{
    string str("now step live...");
    // reverse_iterator C++ 标准库中提供的一种反向迭代器
    for (string::reverse_iterator rit = str.rbegin(); rit != str.rend(); ++rit)
        cout << *rit;
    return 0;
}
  1. string类对象的修改操作
  • push_back 在字符串后尾插字符c
// string::push_back
#include <iostream>
#include <fstream>
#include <string>
// 这段代码从 test.txt 文件中读取所有字符并将其存储在 str 字符串中,然后将 str 输出到控制台
int main()
{
    std::string str;
    // std::ios::in 表示以只读方式打开文件
    std::ifstream file("test.txt", std::ios::in);
    if (file)
    {
        while (!file.eof())            // file.get() 从文件中读取一个字符
            str.push_back(file.get()); // str.push_back() 将读取到的字符添加到字符串 str 的末尾
    }
    std::cout << str << '\n'; // 将读取到的完整字符串 str 输出到控制台。
    return 0;
}
  • append 在字符串后追加一个字符串
// appending to string
#include <iostream>
#include <string>

int main()
{
    std::string str;
    std::string str2 = "Writing ";
    std::string str3 = "print 10 and then 5 more";

    // used in the same order as described above:
    str.append(str2); // "Writing "
    // 将字符串 str3 中从第 6 个字符开始的 3 个字符(即 "10 ")添加到 str 末尾,结果为 "Writing 10 "。
    str.append(str3, 6, 3); // "10 "
    // str.append("dots are cool", 5);:将字符串 "dots are cool" 的前 5 个字符(即 "dots ")添加到 str 末尾
    str.append("dots are cool", 5); // "dots "
    str.append("here: ");           // "here: "
    // 在 str 末尾添加 10 个 . 字符
    str.append(10u, '.'); // ".........."
    // 将字符串 str3 从第 8 个字符到末尾的所有字符(即 " and then 5 more")添加到 str 末尾,
    str.append(str3.begin() + 8, str3.end()); // " and then 5 more"
                                              // str.append(5, 0x2E);:在 str 末尾再添加 5 个 . 字符
    str.append(5, 0x2E);                      // "....."

    std::cout << str << '\n';
    return 0;
}
  • operator+= 在字符串后追加字符串str
// string::operator+=
#include <iostream>
#include <string>

int main()
{
    std::string name("John");
    std::string family("Smith");
    name += " K. "; // c-string
    name += family; // string
    name += '\n';   // character

    std::cout << name;
    return 0;
}
  • c_str 返回C格式字符串
// strings and c-strings
#include <iostream>
#include <cstring>
#include <string>

int main()
{
    std::string str("Please split this sentence into tokens");

    char *cstr = new char[str.length() + 1];
    std::strcpy(cstr, str.c_str());
    char *p = std::strtok(cstr, " ");

    while (p != 0)
    {
        std::cout << p << '\n';
        p = std::strtok(NULL, " ");
    }

    delete[] cstr;
    return 0;
} // cstr now contains a c-string copy of str
  // strtok函数用于分割字符串,它通过修改原字符串来实现分割操作,
  // 即在找到分隔符后,会将其替换为'\0'(空字符),使得返回的子字符串是独立的。
  // strtok函数的第一个参数在第一次调用时是要分割的字符串,
  // 在后续调用时应传递NULL,以继续分割同一个字符串。
  // char *p = std::strtok(cstr, " ");
  • find + npos 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
// string::find
#include <iostream> // std::cout
#include <string>   // std::string

int main()
{
    std::string str("There are two needles in this haystack with needles.");
    std::string str2("needle");

    // different member versions of find in the same order as above:

    // 使用find函数在字符串str中查找str2(即"needle")首次出现的位置。
    // find返回的是子字符串的起始位置,如果未找到子字符串,则返回std::string::npos。
    std::size_t found = str.find(str2);
    // 如果find函数找到了str2,则found不等于std::string::npos,进入条件分支。
    if (found != std::string::npos)
        std::cout << "first 'needle' found at: " << found << '\n';

    // find函数这次使用的是三个参数的版本
    // needles are small":要查找的字符串。
    // found + 1:从上次找到的位置的下一个字符开始查找,即在字符串str中查找第二个"needle"的出现位置
    // 只使用前6个字符(即"needle")进行匹配查找。
    found = str.find("needles are small", found + 1, 6);
    if (found != std::string::npos)
        std::cout << "second 'needle' found at: " << found << '\n';

    // 查找字符串"haystack"的位置,并输出。
    found = str.find("haystack");
    if (found != std::string::npos)
        std::cout << "'haystack' also found at: " << found << '\n';

    // 查找第一个句号"."的位置,并输出。
    found = str.find('.');
    if (found != std::string::npos)
        std::cout << "Period found at: " << found << '\n';

    // let's replace the first needle:
    // 使用replace函数将字符串str中第一个出现的str2("needle")替换为"preposition"。
    // 1.str.find(str2):找到str2的起始位置。
    // 2.str2.length():获取str2的长度。
    // 3.preposition":用这个字符串替换找到的"needle"。
    str.replace(str.find(str2), str2.length(), "preposition");
    std::cout << str << '\n';

    return 0;
}
  • rfind 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置
// string::rfind
#include <iostream>
#include <string>
#include <cstddef>

int main()
{
    std::string str("The sixth sick sheik's sixth sheep's sick.");
    std::string key("sixth");
    // rfind函数用于在字符串str中从右到左查找key(即"sixth")最后一次出现的位置。
    // rfind返回的是子字符串的起始位置索引。如果未找到,返回std::string::npos
    std::size_t found = str.rfind(key);
    if (found != std::string::npos)
        // 第一个参数found是要替换的起始位置。
        // 第二个参数key.length()是要替换的长度(即"sixth"的长度)。
        str.replace(found, key.length(), "seventh");

    std::cout << str << '\n';

    return 0;
}
  • substr 在str中从pos位置开始,截取n个字符,然后将其返回
// string::substr
#include <iostream>
#include <string>

int main()
{
    std::string str = "We think in generalities, but we live in details.";
    // (quoting Alfred N. Whitehead)
    // 第一个参数3表示子字符串的起始位置(从0开始计数,所以位置3对应的是"think"中的't')。
    // 第二个参数5表示子字符串的长度,也就是从位置3开始,提取5个字符。
    std::string str2 = str.substr(3, 5); // "think"

    std::size_t pos = str.find("live"); // position of "live" in str
    // 再次使用substr函数,从位置pos开始提取子字符串,直到字符串str的末尾。
    std::string str3 = str.substr(pos); // get from "live" to the end

    std::cout << str2 << ' ' << str3 << '\n';

    return 0;
}
  1. 在string尾部追加字符时,s.push_back© / s.append(1, c) / s += 'c’三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
  2. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
  1. string类非成员函数
  • operator+ 尽量少用,因为传值返回,导致深拷贝效率低
  • operator>> 输入运算符重载
// extract to string
#include <iostream>
#include <string>

main()
{
    std::string name;

    std::cout << "Please, enter your name: ";
    std::cin >> name;
    std::cout << "Hello, " << name << "!\n";

    return 0;
}
  • operator<< 输出运算符重载
// inserting strings into output streams
#include <iostream>
#include <string>

main()
{
    std::string str = "Hello world!";
    std::cout << str << '\n';
    return 0;
}
  • getline 获取一行字符串
// extract to string
#include <iostream>
#include <string>

int main()
{
    std::string name;

    std::cout << "Please, enter your full name: ";
    std::getline(std::cin, name);  从标准输入流(键盘输入)中读取一整行字符并存储到 name 变量中
    std::cout << "Hello, " << name << "!\n";

    return 0;
}
  • relational operators 大小比较
// string comparisons
#include <iostream>
#include <vector>

int main()
{
    std::string foo = "alpha";
    std::string bar = "beta";

    if (foo == bar)
        std::cout << "foo and bar are equal\n";
    if (foo != bar)
        std::cout << "foo and bar are not equal\n";
    if (foo < bar)
        std::cout << "foo is less than bar\n";
    if (foo > bar)
        std::cout << "foo is greater than bar\n";
    if (foo <= bar)
        std::cout << "foo is less than or equal to bar\n";
    if (foo >= bar)
        std::cout << "foo is greater than or equal to bar\n";

    return 0;
}
  1. vs和g++下string结构的说明

vs下string的结构:
string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字符串的存储空间:
当字符串长度小于16时,使用内部固定的字符数组来存放;当字符串长度大于等于16时,从堆上开辟空间
还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量
最后还有一个指针做一些其他事情
故总共占16+4+4+4=28个字节

g++下string的结构
G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指针将来指向一块>堆空间,内部包含了如下字段:

  • 空间总大小
  • 字符串有效长度
  • 引用计数
  • 指向堆空间的指针,用来存储字符串。

二、string类的模拟实现

1. 经典的string类问题

#include <assert.h>
#include <string.h>
using namespace std;
// 为了和标准库区分,此处使用String
class String
{
public:
    /*String()
    :_str(new char[1])
    {*_str = '\0';}
    */
    // String(const char* str = "\0") 错误示范
    // String(const char* str = nullptr) 错误示范
    String(const char *str = "")
    {
        // 这个构造函数的参数 str 被赋予了一个默认值 "",
        // 这是一个指向空字符串的指针(即一个只包含终止符 \0 的字符数组)。
        // 如果在创建 String 对象时没有传递参数,那么 str 将自动被赋值为 "",即空字符串。
        if (nullptr == str)
        {
            assert(false);
            return;
        }
        _str = new char[strlen(str) + 1];
        strcpy(_str, str);
    }
    ~String()
    {
        if (_str)
        {
            delete[] _str;
            _str = nullptr;
        }
    }

private:
    char *_str;
};
// 测试
void TestString()
{
    String s1("hello bit!!!");
    String s2(s1);
}

上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。

2.浅拷贝

也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。

可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。

3.深拷贝

如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

在这里插入图片描述

  1. 对比下面的赋值运算符
String &operator=(String s)
{
        swap(_str, s._str);
        return *this;
}

传值方式的使用:
在赋值运算符中,使用传值意味着函数参数String s在调用时会创建一个副本。也就是说,当执行String a = b;时,会调用operator=(String s),而参数s是对象b的一个副本。即当operator=被调用时,编译器会生成一个新的String对象(s),它是被赋值对象的副本。
swap(_str, s._str);将当前对象的内部指针_str与副本对象s的内部指针进行交换。由于s是传值过来的副本对象,交换之后,当前对象_str指向了副本对象的内存,而副本对象s则指向了当前对象原来所指向的内存。
交换操作完成后,副本对象s会在函数结束时自动销毁,其析构函数会释放它持有的内存,即之前当前对象持有的内存。因此,这种方式下的赋值运算符不会导致内存泄漏。为更优写法。

 String& operator=(const String& s)
{
    if(this != &s)
    {
    String strTmp(s);
    swap(_str, strTmp._str);
    }
    return *this;
}

这个赋值运算符首先检查是否是自我赋值,然后为字符串分配新内存并将字符串内容复制过来,最后释放旧内存。这样做的优点是直接了当,但缺点是容易引入内存泄漏的风险,特别是在内存分配成功但复制内容失败时。

  1. 对比拷贝构造函数
String(const String &s)
        : _str(nullptr)
    {
        String strTmp(s._str);
        swap(_str, strTmp._str);
    }

由于在拷贝构造函数中,首先创建了一个临时对象 strTmp,即使在创建临时对象时发生异常,当前对象的状态也不会被改变。只有当 strTmp 构造成功后,才会进行交换操作。因此,当前对象总是保持在一种安全状态。为更优写法。

String(const String &s)
    : _str(new char[strlen(s._str) + 1])
{
    strcpy(_str, s._str);
}

这个拷贝构造函数使用了直接分配新内存的方式。它根据传入对象String中的字符串长度,分配相同长度的新内存,然后将字符串内容复制到新内存中。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值