zl程序教程

您现在的位置是:首页 >  其他

当前栏目

c++标准模板库

2023-02-18 16:46:44 时间

  ●既选择了远方,便只顾风雨兼程。

  ●欢迎大家有问题随时私信我!

  ●版权:本文由[你帅你先说.]原创,CSDN首发,侵权必究。

  为您导航

  1.STL简介 1.1什么是STL

  STL( -标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。

  1.2STL的版本 1.3STL的六大组件

  1.4STL的缺陷

  1.STL库的更新太慢了。这个得严重吐槽,上一版靠谱是C++98,中间的C++03基本一些修订。C++11出来已经相隔了13年,STL才进一步更新。

  2.STL现在都没有支持线程安全。并发环境下需要我们自己加锁。且锁的粒度是比较大的。

  3.STL极度的追求效率,导致内部比较复杂。比如类型萃取,迭代器萃取。

  4.STL的使用会有代码膨胀的问题,比如使用vector/vector/vector这样会生成多份代码,当然这是模板语法本身导致的。

  2.为什么学习string类?

  C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。

  3.标准库中的string类

  讲到string类就必须科普一些知识

  我们知道,数据是以二进制的形式在内存中存储的,因为计算机只认识0和1,比如我们存一个数字10,在计算中就会存入1010,除了数字,我们还可能会存入字母、符号等,计算机不会直接就把这些符号存进去,而是制定了一个规则,这些符号和字母对应一个值,这就是我们所熟知的ASCII码,比如存字符'A',对应65,计算机中就会存入,符号也是类似的原理,早期计算机是欧美那些国家发明出来的,但随着国家之间加强合作,最终每个国家都会使用计算机编程,所以每个国家都会制定一套各自语言的存储规则,所以就有人制定了一个表示全世界的编码表,叫做utf( Format),它有很多编码方式,例如utf-8、utf-16、utf-32。现在用的最多的是utf-8,所以编码实际上就是值和符号建立映射关系。当然,我们国家也有自己一套中文量身定制的编码表,叫gbk

  3.类常用接口 3.1.类对象的常见构造 函数名称功能说明

  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
    }

  3.1.类对象的容量操作 函数名称功能说明

  size

  返回字符串有效字符长度

  empty

  检测字符串释放为空串,是返回true,否则返回false

  clear

  清空有效字符

  为字符串预留空间

  resize

  将有效字符的个数该成n个,多出的空间用字符c填充

  返回空间总大小

  size&&clear

  resize

  resize和的区别在于resize不仅仅是开空间,还会对这些空间进行初始化(默认为0)。换句话说,中_size的值是不变的,变的只是,而resize则会改变_size的值,扩容多少_size的值就是多少。

  size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一 致,一般情况下基本都是用size()。clear()只是将string中有效字符清空,不改变底层空间大小。resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字 符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的 元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。(size_t =0):为string预留空间,不改变有效元素个数,当的参数小于 string的底层空间总大小时,不会改变容量大小。 3.1.类对象的访问及遍历操作 函数名称功能说明

  []

  返回pos位置的字符,const string类对象调用

  at

  获取字符串中的字符

  back

  访问最后一个字符

  front

  访问第一个字符

  begin/end

  begin获取第一个字符的迭代器/end获取最后一个字符下一个位置的迭代器

  rbegin/rend

  rend获取第一个字符前一个位置的迭代器/rbegin获取最后一个字符下一个位置的迭代器

  []

  at

  迭代器+begin/end

  我们发现,迭代器的访问方式有点像指针,那迭代器有什么意义呢?

  对于string,下标就足够好用了,确实可以不用迭代器,但是对于其它容器呢?迭代器的意义就在于所有的容器都可以使用迭代器这种方式去访问修改。

  3.1.类对象的修改操作 函数名称功能说明

  在字符串后尾插字符c

  append

  在字符串后追加一个字符串

  +=

  在字符串后追加字符串str

  c_str

  返回C格式字符串

  find/rfind + npos

  从字符串pos位置开始往后/前找字符c,返回该字符在字符串中的位置

  substr

  在str中从pos位置开始,截取n个字符,然后将其返回

  读入一行字符串,遇到’\n’结束

   && append

  在string尾部追加字符时,s.© / s.append(1, c) / s += 'c’三种的实现方式差不多,一般 情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符c++标准模板库,还可以连接字符串。对string操作时,如果能够大概预估到放多少字符,可以先通过把空间预留好。

  c_str

  我们发现fopen函数只支持const char*类型(即C语言格式的字符串)的数据,此时就可以用c_str来转换。

  find/rfind && npos && substr

  现在我们有一个需求,要求取出文件的后缀名,就需要用上这些函数了。

  npos是一个常数,用来表示不存在的位置,一般是取-1,转换成size_t类型后也是一个很大的数,可以认为不存在。

  接下来我们还有个需求,要把这条网址分成协议、域名、虚拟目录

  第一个参数需要传入输入流,第二个参数需要传一个字符串。

  3.1.5字符串和其它数据类型的转换

  4.string类的模拟实现

  为了避免与库里面的stirng类冲突,我们可以自己定义个命名空间。

   #pragma once

    #include
    namespace ljt
    {
        class string
        {
        public:
            string(const char* str) :_str(new char[strlen(str) + 1])
            {
                strcpy(_str, str);
            }
            ~string()
            {
                delete[] _str;
                _str = nullptr;
            }
        private:
            char* _str;
        };
    }

  这里说几个小细节,构造函数中不能写成_str(str),因为这样传过来的字符串是无法更改的,所以必须在堆上申请空间进行存储。strlen(str)+1这里面加1的原因是还有一个'\0'也占空间大小。

  此时我们测试一下程序,发现程序奔溃了。

  我们通过调试来看看是什么问题。

  这个时候我们就发现问题了,s1和s2在类里面定义的指针都指向了同一块空间,当程序结束时,s2析构函数释放空间,s1空间又释放了一次空间,一块空间被释放了两次,造成了程序崩溃。

  这里就涉及到了深浅拷贝问题,浅拷贝就是完全复制粘贴,就是上面这个例子。在这里用浅拷贝显然不行,所以我们可以使用深拷贝,刚刚我们发现程序崩溃的原因是一个空间释放多次,为了解决这个问题,我们可以自己写一个拷贝构造函数,且每次构造都开一个空间,这样就能避免重复释放相同空间。

   #pragma once

    #include
    namespace ljt
    {
        class string
        {
        public:
            string(const char* str) :_str(new char[strlen(str) + 1])
            {
                strcpy(_str, str);
            }
            string(const string& s):_str(new char[strlen(s._str)+1])
            {
                strcpy(_str, s._str);
            }
            ~string()
            {
                delete[] _str;
                _str = nullptr;
            }
        private:
            char* _str;
        };
    }

  此时通过调试发现就不是同一块空间了。

  刚刚我们写的深拷贝是传统写法,深拷贝还有一种现代写法。

   string(const string& s):_str(nullptr)

    {
        string tmp(s._str);
        swap(_str,tmp._str);
    }

  这种写法是这样的,首先让tmp开一块和s._str一样的空间,然后交换_str和tmp._str所指向的空间,然后出了作用域tmp会调用析构函数,释放空间。

  当然,深浅拷贝不仅仅这么简单。

  我们发现,这种情况下又崩溃了。

  图解原因

  此时s1指向了s3开的空间,而s1开的空间又没有释放,可我们刚刚不是已经解决这个问题了吗?在这里需要区分一下不同写法所调用的函数是哪些

   int main()

    {
        String s1("hello");    //调用构造函数
        String s2 = "world";   //调用构造函数
        String s3(s1);       //调用拷贝构造函数
        String s4 = s1;      //调用拷贝构造函数
        String s5;           //调用构造函数
        s5 = s1;             //调用拷贝赋值运算符
        return 0;
    }

  尤其要注意string s4 = s1和s5 = s1,一个是初始化,一个是赋值。

  所以要想解决刚刚那个问题,我们还得写一个拷贝赋值运算符,也就是重载=运算符。

   string& operator=(const string& s)

    {
        if (this != &s)
        {
            string tmp(s);
            swap(_str,tmp._str);
        }
        return *this;
    }

  这段代码一般来说大多数情况下是没有问题了,但有一种特殊情况,如果new失败呢?前面我们说过new失败会抛异常,那你失败就失败吧,但空间已经被你释放了,所以这段代码还要再优化一下。

   string& operator=(const string& s)

    {
        if (this != &s)
        {
            char* tmp = new char[strlen(s._str) + 1];
            strcpy(tmp, s._str);
            delete[] _str;
            _str = tmp;
        }
        return *this;
    }

  同样地,这段代码也是传统写法,它也有现代写法。

   string& operator=(const string& s)

    {
        if(this != &s)
        {
            string tmp(s);
            swap(_str,tmp._str);
        }
        return *this;
    }

  思想和上面深拷贝的类似。

  这段代码还可以写的更简洁

   string& operator=(string s)

    {
        swap(_str,s._str);
        return *this;
    }
    //这边不用判断自己给自己赋值,因为判断不了,因为传的不是引用,地址不一样了。

  接下来我们开始写string类的增删查改

  我们增加_size和两个变量,然后对构造函数进行完善

   namespace ljt

    {
        class string
        {
        public:
            string(const char* str = "") :_size(strlen(str)),_capacity(_size)
            {
                _str = new char[_capacity + 1];
                strcpy(_str, str);
            }
            string(const string& s):_str(nullptr) :_size(0), _capacity(0)
            {
                string tmp(s._str);
                
                swap(_str,tmp._str);
                swap(_size,tmp._size);
                swap(_capacity,tmp._capacity);
            }
            string& operator=(string s)
            {
                swap(_str,s._str);
                swap(_size,s._size);
                swap(_capacity,tmp._capacity);
                
                return *this;
            }
            ~string()
            {
                delete[] _str;
                _str = nullptr;
                _size = _capacity = 0;
            }
        private:
            char* _str;
            size_t _size;
            size_t _capacity;
        };
    }

  实现c_str()函数

   const char* c_str()const

    {
        return _str;
    }

  实现size()函数

   size_t size()

    {
        return _size;
    }

  实现[]重载

   char& operator[](size_t pos)

    {
        assert(pos  _capacity)
        {
            reserve(_size + len);
        }
        strcpy(_str + _size,str);
        _size += len;
    }

  string类里的swap()

  C++里除了标准库里有swap()函数,string类里面也有swap()函数,那为什么string类还要单独写一个swap()函数?相信大家可以猜到c++标准模板库,可能string类里的swap()函数是专门针对string类写的,所以效率可能会更高,确实是这样的。

  这是C++标准库里的swap()函数

  我们看到,标准库里的方法要进行三次拷贝构造,且都是深拷贝。

  而string类里的swap()函数只是简单进行值的交换,所以效率更高。

  string类+=运算符重载

   string& operator+=(char ch)

    {
        push_back(ch);
        return *this;
    }
    string& operator+=(const char* str)
    {
        append(str);
        return *this;
    }

  string类比大小运算符重载

   bool operator(istream& in,string& s)

    {
        s.clear();
        char ch = in.get();
        while(ch != ' ' && ch != '\n')
        {
            s += ch;
            ch = in.get()
        }
        return in;
    }

本文共 2602 个字数,平均阅读时长 ≈ 7分钟