STL——string类的模拟实现

news2024/9/30 7:15:45

0.关注博主有更多知识

C++知识合集

目录

1.编码问题

2.string类概述

2.6习题练习

3.string类的模拟实现

3.1成员变量

3.2迭代器部分

3.3类的默认成员部分

3.4容量接口

3.5增删查改接口

3.6通用接口

3.7输入与输出

3.8完整代码

1.编码问题

实际上在我们接触C++之前就已经接触过一种编码,即ASCII编码。ASCII编码的主要目的就是为了建立计算机存储的值与英文字符之间的映射关系,因为计算机根本不认识'a'、'b'、'c'等等类似这样的字符,它只认识0、1这样的二进制序列。那么计算机是美国人发明的,那么他们必然想要计算机能够认识自己的文字,即英文字母和一些英文标点符号、数字等等,所以就诞生出了ASCII编码这样的东西,每一个值都确定一个英文符号

但是ASCII编码仅限于英文,而当今计算机流行于全世界,ASCII必然是不够用的,所以又诞生出了Unicode编码,即统一码,也称万国码。Unicode编码的主要用途就是建立计算机与非英文之间的映射,当然,Unicode也是可以支持ASCII的。那么对Unicode编码的详细概述由官方介绍将会更好:统一码——百度百科。就拿中文来说,Unicode编码使用两个字节来表示一个中文汉字,所以当前最流行的编码就是UTF-8,它既能够支持中文,也能够兼容ASCII。

那么还有一种编码名为GBK编码,这种GBK编码是专门用来支持中文的编码,貌似是国人造的(我也不确定)。但是GBK编码与UTF-8编码存在差异,所以在很多的是不方便的。例如,Windows操作系统的默认编码为GBK编码,而Linux操作系统的默认编码为UTF-8编码,那么在Windows写的程序和在Linux写的程序进行网络套接字收发数据时,就有可能产生乱码。

2.string类概述

事实上string类严格意义上将它不属于STL,因为它的出现要比STL早,所以它的接口设计与其他STL容器比起来显得"不伦不类",因为这是C++后期对string类的升级,目的是为了向STL靠齐,所以添加了类似于size()、迭代器这样的东西。但是本篇博客着重在于模拟实现而不在于介绍接口,因为接口的介绍实在是太麻烦了(string类的接口有100多个),所以建议读者直接去查阅文档:string类接口。

现在要介绍的是,string类是什么。string类是一个专门管理字符串的类,与其说是管理字符串,不如说是管理字符数组的类。就像数据结构当中的顺序表一样,string类的本质就是一个存放char类型元素的顺序表,需要支持插入、删除、扩容、查找等等操作,非常幸运,string类直接提供了这类接口,并且它能够自动扩容,也就是说我们在使用string类的时候根本不需要关心底层的空间大小够不够。

为了向STL靠齐,string类引入了迭代器的概念。迭代器就是一个像指针一样的东西,但是它不完全是指针,迭代器具有指向底层数据结构当中存储的元素的能力,因此,我们可以使用迭代器完成插入、修改、查找、删除和遍历的操作。但是刚才也说了,string类是专门管理字符数组的类,既然是数组,那么我们更倾向于使用下标的方式来完成增删查改,为什么又要引入迭代器呢?原因在于STL的容器当中,每个容器的底层的数据结构都不同,有顺序表、链表、堆、红黑树、哈希、AVL树等等,但是我们使用迭代器的话,就能用统一的操作实现对不同数据结构的控制。通常使用的迭代有begin()和end()两个,其中begin()接口返回的迭代器指向的是第一个有效元素的位置,end()接口返回的迭代器指向是最后一个有效元素的下一个位置,即末尾

  在string类当中,end()指向的位置为'\0'。

那么string类提供的一个接口当中有多个重载,其中不乏const成员函数和非const成员函数,这是因为这些接口(成员函数)要遵循下面的三个原则

  1.如果提供的接口仅仅是只读接口,例如size()、c_str()这种,不会修改底层数据结构的任何属性,那么只需要提供const版本的即可,因为const版本的成员函数无论是const对象还是非const对象都能调用

  2.如果提供的接口仅仅是只写接口,例如resize()、reverse()这种,它们直接修改了底层数据结构的大小和容量,那么只需要提供非const版本的即可,因为const对象修改底层属性是一种不合理操作

  3.如果提供的接口是可读可写接口,例如operator[]这种,那么它们必须实现const版本和非const版本的两个重载,const对象调用const版本的接口,非const对象调用非const版本的接口

  介绍这些的目的是为了做后面模拟实现string类的铺垫。

有一个细节需要注意,那便是string类不以'\0'作为结束标志,我们可以举一个简单的例子验证我们的说法:

int main()
{
	string s = "hello";
	s.resize(10);/*多的内容以'\0'填充*/
	s += "world";
	cout << s;
	return 0;
}

  在监视窗口观察:

  原因在于string类(即basic_string<T>)类有两个成员,一个成员用来表示有效数据的个数,另一个用来表示容量大小。调用operator<<()时,以有效数据的个数输出结果:

  但如果我们以C风格的字符串形式输出的话,结果就会不同:

int main()
{
	string s = "hello";
	s.resize(10);/*多的内容以'\0'填充*/
	s += "world";
	cout << s.c_str();
	return 0;
}

再者也需要注意一点,也是对以前类和对象的知识作补充,即运算符重载不能为静态成员函数。所以我们可以很明显的看到string类当中没有一个静态成员函数,原因具体我也不清楚(反正就是编译器报错了),我的猜测是因为static修饰的函数会修改其链接数据,使得该函数只能在本文件当中使用,而在编译的时候会连同其他文件一起编译,但是我们将函数声明为静态,所以会造成"找不到"函数的现象(但是它编译器不给链接错误的信息,所以这个猜测大家可以忽略):

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year), _month(month), _day(day)
	{}
	
	static bool operator==(const Date &d1, const Date &d2)
	{
		return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2023, 5, 16);
	Date d2(2023, 5, 16);

	Date::operator==(d1, d2);/*这样的写法完全没有问题,但是编译器报错*/
	return 0;
}

2.6习题练习

917. 仅仅反转字母

class Solution {
public:
    string reverseOnlyLetters(string s) {
        int begin = 0,end = s.size()-1;
        while(begin < end)
        {
            /*如果碰到了非字母,就跳过*/
            if(!isalpha(s[begin])) ++begin;
            if(!isalpha(s[end])) --end;

            /*反转的本质就是头尾交换
             *但是该题交换有条件,必须都是字母*/
            if(begin < end && ((isalpha(s[begin]) && isalpha(s[end]))))
                swap(s[begin++],s[end--]);
        }
        return s;
    }
};

415. 字符串相加

class Solution {
public:
    string addStrings(string num1, string num2) {
        int end1 = num1.size()-1,end2 = num2.size()-1;
        int carry = 0;/*进位*/
        
        string resStr;
        resStr.reserve(max(num1.size(),num2.size())+1);
        while(end1 >= 0 || end2 >= 0)
        {/*只要有一个字符串没有遍历完,都要进入循环*/
            int operNum1 = end1 < 0? 0 : num1[end1] - '0';
            int operNum2 = end2 < 0? 0 : num2[end2] - '0';

            /*两个字符串的每一位的和再加上进位,就等于当前的和
             *但是每一位的范围在0~9,所以大于9时需要进位*/
            int tmpSum = operNum1 + operNum2 + carry;
            carry = tmpSum / 10;
            tmpSum %= 10;

            resStr += tmpSum + '0';

            --end1,--end2;
        }
        if(carry == 1) resStr += '1';/*如果还有进位*/
        reverse(resStr.begin(),resStr.end());
        return resStr;
    }
};

387. 字符串中的第一个唯一字符

class Solution {
public:
    int firstUniqChar(string s) {
        /*字符串中只有小写字母,并且小写字母只有26个
         *所以使用数组用来计数,然后再遍历一遍字符串
         *找出只出现一次的字符下标即可*/
        int count[26] = {0};
        for(auto &e:s)
        {
            count[e - 'a']++;
        }
        for(int i=0;i<s.size();i++)
        {
            if(count[s[i] - 'a'] == 1) return i;
        }
        return -1;
    }
};

HJ1 字符串最后一个单词的长度

#include <iostream>
using namespace std;

int main()
{
    string str;
    /*一定是使用getline读取一整行字符串
     *因为流提取(>>)碰到空格即截止*/
    getline(cin,str);
    cout << str.size() - (str.rfind(' ') + 1) << endl;
    return 0;
}

125. 验证回文串

class Solution {
public:
    bool isPalindrome(string s) {
        /*大写转小写*/
        for(auto &e:s) if(isupper(e)) e = tolower(e);

        int begin = 0,end = s.size()-1;/*双指针*/
        while(begin < end)
        {
            if(!isalnum(s[begin]))
            {
                ++begin;
                continue;
            }
            if(!isalnum(s[end]))
            {
                --end;
                continue;
            }
            /*上面两个if是移除所有非字母、数字字符的*/
            
            if(s[begin] != s[end]) return false;
            ++begin;
            --end;
        }
        return true;
    }
};

541. 反转字符串 II

class Solution {
public:
    string reverseStr(string s, int k) {
        for(int i=0;i<s.size();i+=2*k)
        {
            /*如果从当前位置开始,还剩余2k个字符,翻转前k个
             *从当前位置开始,还剩余k个或k个以上字符,翻转前k个*/
            if(i+2*k <= s.size() || i+k<=s.size())
            {
                reverse(s.begin()+i,s.begin()+i+k);
            }
            else
            {/*全部翻转*/
                reverse(s.begin()+i,s.end());
            }
        }
        return s;
    }
};

557. 反转字符串中的单词 III

class Solution {
public:
    string reverseWords(string s) {
        /*思路就是找到一个空格就逆序
         *使用STL的算法find(),不使用string类的find()成员
         *因为STL的find()返回的是通用迭代器*/
        auto itBegin = s.begin();
        string::iterator itEnd;
        while((itEnd = find(itBegin,s.end(),' ')) != s.end())
        {
            reverse(itBegin,itEnd++);
            itBegin = itEnd;
        }
        reverse(itBegin,s.end());
        return s;
    }
};

43. 字符串相乘

class Solution {
public:
    string multiply(string num1, string num2) {
        if(num1 == "0" || num2 == "0") return "0";

        /*思路就是将乘法拆成加法,例如123*456,就相当于:
         *123*456 = 123*6 + 123*50 + 123*400 
         *按照这个思路分析下面的代码,不会很难*/
        int end2 = num2.size()-1;
        int zeroCnt = 0;
        string res = "0";
        while(end2 >= 0)
        {
            int end1 = num1.size()-1;
            int cnt = zeroCnt;
            int carry = 0;
            string onceProduct;/*乘法拆开后的每一次乘积*/
            while(end1 >= 0)
            {
                int n1 = num1[end1] - '0';
                int n2 = num2[end2] - '0';
                int product = n1 * n2 + carry;
                carry = product / 10;/*两个数的乘积最多为81*/
                product %= 10;
                onceProduct += (product + '0');
                --end1;
            }
            if(carry > 0) onceProduct += (carry + '0');
            reverse(onceProduct.begin(),onceProduct.end());
            while(cnt--) onceProduct += '0';
            /*到这里onceProduct可能是很多个0组成的字符串
             *但是并不影响,因为紧接着调用addStrings()函数*/
             
            res = addStrings(res,onceProduct);
            ++zeroCnt;
            --end2;
        }
        return res;
    }
    string addStrings(string num1, string num2) {
        int end1 = num1.size()-1,end2 = num2.size()-1;
        int carry = 0;/*进位*/
        
        string resStr;
        resStr.reserve(max(num1.size(),num2.size())+1);
        while(end1 >= 0 || end2 >= 0)
        {/*只要有一个字符串没有遍历完,都要进入循环*/
            int operNum1 = end1 < 0? 0 : num1[end1] - '0';
            int operNum2 = end2 < 0? 0 : num2[end2] - '0';

            /*两个字符串的每一位的和再加上进位,就等于当前的和
             *但是每一位的范围在0~9,所以大于9时需要进位*/
            int tmpSum = operNum1 + operNum2 + carry;
            carry = tmpSum / 10;
            tmpSum %= 10;

            resStr += tmpSum + '0';

            --end1,--end2;
        }
        if(carry == 1) resStr += '1';/*如果还有进位*/
        reverse(resStr.begin(),resStr.end());
        return resStr;
    }
};

3.string类的模拟实现

我们先看string类是如何定义的,以及需要什么成员变量和成员函数声明,然后对每一个接口的实现进行讲解:

class String
{/*毕竟是模拟实现,所以就不用模板模拟basic_string<char>了*/
    public:
    /*原生指针就可以充当迭代器
		 *原生指针具有指向、修改、插入...的能力
		 *只要自定义类型有了迭代器,在外部就可以使用范围for遍历了
		 *但是一定要注意,使用范围for的类型,其迭代器一定要配套*/
    typedef char * iterator;
    typedef const char * const_iterator;
    iterator begin() {return _str;}
    iterator end() {return _str + _size;}
    const_iterator begin() const{return _str;}
    const_iterator end() const{return _str + _size;}

    public:
    /*构造、拷贝、赋值、析构*/
    String(const char *str = "");
    String(const String &str);
    //String &operator=(const String &str);
    String &operator=(String str);/*现代写法*/
    ~String();

    /*有关容量的接口*/
    void reserve(int capacity);
    void resize(size_t n, const char &ch = '\0');

    /*增、删、查、改*/
    void push_back(const char &ch);
    String &insert(size_t pos, size_t n, const char &ch);
    String &insert(size_t pos, const String &s);
    String &append(size_t n, const char &ch);
    String &append(const String &s);
    String &operator+=(const char &ch);
    String &operator+=(const String &s);
    String &erase(size_t pos = 0, size_t len = npos);
    size_t find(const char &ch, size_t pos = 0) const;
    size_t find(const String &str, size_t pos = 0) const;
    String operator+(const String &str);

    /*通用接口*/
    char &operator[](size_t pos);
    const char &operator[](size_t pos) const;/*可读可写接口,要重载两个版本*/
    size_t size() const;
    size_t capacity() const;
    void clear();
    const char *c_str() const;/*只读接口*/
    void swap(String &str);

    /*输入与输出*/
    friend ostream &operator<<(ostream &out, const String &str);
    friend istream &operator>>(istream &in, String &str);

    private:
    char *_str;
    size_t _size;
    size_t _capacity;

    static const size_t npos = -1;/*常量整数可以在类内直接初始化*/
};

以上就是我们要模拟实现的接口,当然了,我们不可能将所有接口原模原样的复现出来,模拟实现的重点在于复习类和对象的知识,以及更加熟悉STL。

3.1成员变量

我们即使不清楚string类底层到底是怎么实现的,但我们可以大概猜测它就是一个顺序表,所以可以看到上面的成员变量有四个。其中"char *_str"用来指向字符数组;"size_t _size"用来表示有效元素个数,如果将_size作为下标,那么它指向的位置就是'\0';"size_t _capacity"用来表示容量,容量表示当前还能装下多少字符,注意,我模拟实现的_capacity是包括了'\0'了,即当_capacity=1时,表示当前容量为1,即已经存了一个'\0'。

3.2迭代器部分

对于string类来说,它的底层实际上就是一个字符数组,也就是顺序表,既然是顺序表那么就说明元素的指向、遍历、增删查改都可以通过指针去完成,所以string类的迭代器就是一个原生指针。那么有关于迭代器的接口上面已经实现好了:

iterator begin() {return _str;}
iterator end() {return _str + _size;}
const_iterator begin() const{return _str;}
const_iterator end() const{return _str + _size;}

很显然,begin()要返回起始位置,直接将_str返回即可;end()要返回末尾位置,直接将'\0'的位置返回即可。

3.3类的默认成员部分

这里的默认成员指的是构造函数、拷贝构造、析构函数、赋值运算符重载。那么对于它们的实现是这样的:

String::String(const char *str) /*缺省值给到声明处,定义不要再给了*/
{
    _capacity = strlen(str) + 1;
    _size = _capacity - 1;
    _str = new char[_capacity];
    strcpy(_str, str);
}

String::String(const String &str)
{
    _capacity = str._capacity;
    _size = str._size;
    _str = new char[_capacity];
    memcpy(_str, str._str,str._capacity);
}


/*传统写法*/
//String &String::operator=(const String &str)
//{
//	/*统一异地扩容*/
//	char *newStr = new char[str._capacity];
//	memcpy(newStr, str._str, str._capacity);
//	delete[] _str;
//	_str = newStr;
//	_capacity = str._capacity;
//	_size = str._size;
//	return *this;
//}

String &String::operator=(String str)
{
    swap(str);
    return *this;
}

String::~String()
{
    delete[] _str;
    _str = nullptr;
    _size = _capacity = 0;
}

对于构造函数来说,当参数为字符串时,那么它就将该字符串拷贝到自己底层的字符数组当中即可;但是当参数为当前对象的引用时(拷贝构造),记得前面说过,string类不以'\0'结尾,所以拷贝过去的不应该是字符串,而是底层的整个字符数组,这就是为什么我会写一个memcpy的原因。

对于赋值运算符重载来说,注释部分就是标准的、教科书式的写法,但是这样写太麻烦了,因为代码行数比较多;非注释部分是一种现代写法,函数的参数为形参,也就是说调用该函数时参数部分会发生一次拷贝构造,然后在函数体内直接调用了swap()函数交换底层的成员变量。最后当该函数退出时,形参会自动调用析构,此时就会将不需要的空间释放掉。我们以逻辑图来理解这种现代写法

注意我们调用的swap()是自己写swap():

void String::swap(String &str)
{
    std::swap(_str, str._str);
    std::swap(_size, str._size);
    std::swap(_capacity, str._capacity);
}

不直接调用库当中swap()的原因在于,如果我们直接将对象作为库当中swap()的参数,那么在交换过程中会发生三次深拷贝,这个过程体现在源码当中:

// STL的swap()实现
template <class T> void swap ( T& a, T& b )
{
  T c(a); a=b; b=c;
}

那么我们自己写的swap()就是将深拷贝的对象尽量变小,最终变成了内置类型。这里就需要注意了,STL当中swap()函数的函数体内有一"T c(a)"写法,我们知道,如果T是自定义类型,那么这就是在调用拷贝构造,那如果T是内置类型,也能这么写吗?答案是可以这么写,原因就出现在模板上,严格意义上来说,内置类型没有拷贝构造、析构函数这些概念,但是因为有了模板,而模板参数不知道是不是内置类型,所以就允许内置类型采用自定义类型的写法(支持一部分):

int main()
{
	int a(10);
	//int a = 10;

	int b(a);
	//int b = a;
	return 0;
}

3.4容量接口

容量接口无非就是reserve()和resize(),它们的实现是这样的:

void String::reserve(int capacity)
{
    if (capacity > _capacity)/*扩容只有往大了扩*/
    {
        /*统一异地扩容*/
        char *newStr = new char[capacity];

        /*这里不能用strcpy,因为strcpy遇到'\0'截止
			*我们说过string类不以'\0'结尾*/
        memcpy(newStr, _str, _capacity);

        delete[] _str;
        _str = newStr;
        _capacity = capacity;
    }

}

void String::resize(size_t n, const char &ch)
{
    /*resize有三种情况:小于当前有效个数->删除
		*大于当前有效个数&&小于当前容量->扩展
		*大于等于当前容量->扩容+扩展*/
    if (n <= _size)
    {
        _size = n;
        _str[_size] = '\0';
    }
    else
    {
        for (int i = _size; i < n; i++)
        {
            push_back(ch);/*复用,push_back()自动检查是否需要扩容*/
        }
    }
}

对于reserve()来说,只有扩容没有缩容,所以扩容之后的容量一定要大于当前容量。

对于resize()来说,分成三种情况,但大体上可以分为两种,也就是说,如果resize()之后的有效个数小于等于当前字符个数,那就与删除无异;其他情况就相当于尾插。我们的尾插直接复用push_back()接口(后面有它的实现),push_back()的内部会自动检测是否需要扩容并且还会自动添加'\0'。

3.5增删查改接口

这一部分接口是核心接口,它们是这样实现的:

void String::push_back(const char &ch)
{
    if (_size + 1 == _capacity)
    {
        /*扩容*/
        reserve(_capacity * 2);
    }
    _str[_size++] = ch;
    _str[_size] = '\0';
}

String &String::insert(size_t pos, size_t n, const char &ch)
{
    assert(pos < _size);
    if (_size + n + 1 >= _capacity)
    {
        reserve(_size + n + 1);
    }
    size_t end = _size + n;
    while (end >= pos + n)
    {
        _str[end] = _str[end - n];
        --end;
    }
    while (pos <= end)
    {
        _str[pos++] = ch;
    }
    _size += n;
    return *this;
}

String &String::insert(size_t pos, const String &s)
{
    assert(pos < _size);
    if (_size + s._size + 1 >= _capacity)
    {
        reserve(_size + s._size + 1);
    }
    size_t end = _size + s._size;
    while (end >= pos + s._size)
    {
        _str[end] = _str[end - s._size];
        --end;
    }

    /*插入字符串时不需要插入'\0'*/
    strncpy(_str + pos, s._str, s._size);

    _size += s._size;
    return *this;
}

String &String::append(size_t n, const char &ch)
{
    if (_size + n + 1 >= _capacity)
    {
        reserve(_size + n + 1);
    }
    while (n--) push_back(ch);
    return *this;
}

String &String::append(const String &s)
{
    if (_size + s._size + 1 >= _capacity)
    {
        reserve(_size + s._size + 1);
    }
    memcpy(_str + _size, s._str, s._size + 1);
    _size += s._size;/*最后不忘修改_size*/
    return *this;
}


String &String::operator+=(const char &ch)
{
    push_back(ch);
    return *this;
}

String &String::operator+=(const String &s)
{
    append(s);
    return *this;
}

String &String::erase(size_t pos, size_t len)
{
    assert(pos < _size);
    if (len == npos || pos + len >= _size)
    {
        _str[pos] = '\0';
        _size -= len;
        return *this;
    }
    memcpy(_str + pos, _str + pos + len, _size - (pos + len) + 1);
    _size -= len;
    return *this;
}

它们的实现逻辑都非常简单,勤画图就可以搞定。需要注意的是,表示位置的类型为size_t,即无符号整数,这就意味着它们没有负数。

3.6通用接口

/*可读可写接口,要重载两个版本*/
char &String::operator[](size_t pos)
{
    assert(pos < _size);
    return _str[pos];
}

const char &String::operator[](size_t pos) const
{
    assert(pos < _size);
    return _str[pos];
}

size_t String::size() const
{
    return _size;
}

size_t String::capacity() const
{
    return _capacity;
}

void String::clear()
{
    _size = 0;
    _str[0] = '\0';
}

size_t String::find(const char &ch, size_t pos) const
{
    assert(pos < _size);
    for (size_t i = 0; i < _size; i++)
    {
        if (_str[i] == ch) return i;
    }
    return npos;
}

size_t String::find(const String &str, size_t pos) const
{
    assert(pos < _size);
    char *findStr = strstr(_str, str._str);
    if (findStr == nullptr) return npos;
    return findStr - _str;
}

String String::operator+(const String &str)
{
    String tmp(*this);
    tmp += str;
    return tmp;
}

这里需要注意string类当中的find()和算法库当中的find()的差别,string类当中的find()的返回值是下标,算法库的find()的返回值是迭代器。

3.7输入与输出

ostream &operator<<(ostream &out, const String &str)
{
    for (auto &e : str) out << e;
    return out;
}

istream &operator>>(istream &in, String &str)
{
    str.clear();
    char buffer[128];
    size_t i = 0;
    char ch = in.get();
    while (ch != ' ' && ch != '\n')
    {
        if (i == 127)
        {
            str += buffer;
            i = 0;
        }
        buffer[i++] = ch;
        buffer[i] = '\0';

        ch = in.get();
    }
    if (i >= 0)
    {
        buffer[i] = '\0';
        str += buffer;
    }
    return in;
}

CPP 复制 全屏

我们之前多次强调string类不以'\0'结尾,所以在实现流插入运算符重载时不应该站在字符串的角度去实现;对于流提取运算符重载来说,我们的思路是利用ostream类提供的get()方法一个一个地读取字符,并先将这些字符送入缓冲区buffer当中,待缓冲区满时,再尾插到String对象当中。这样地做法能够减少频繁扩容

3.8完整代码

模拟实现string类——String.hpp完整代码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/545409.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot入门(构建、打包、启动、起步依赖starter)

文章目录 1 SpringBoot快速入门1.1 开发步骤步骤1 创建新模块步骤2 创建 Controller步骤3 启动服务器步骤4 进行测试 1.2 对比1.3 官网构建工程步骤1 进入SpringBoot官网步骤2 选择依赖步骤3 生成工程 1.4 SpringBoot工程快速启动1.4.1 问题导入1.4.2 打包1.4.3 启动 2 SpringB…

OverTheWireBandit教程(1-10)

这个网站还挺好玩的于是我就抽点时间做了一下 OverTheWire的登录网址&#xff1a;OverTheWire: Bandit 本人用的是远程连接软件mobaxterm&#xff0c;windows自带的ssh版本不对用不了 Bandit Level 0 Level Goal The goal of this level is for you to log into the game usi…

使用ASM直接生成字节码的方法

ASM是一套java字节码分析/生成/修改的工具&#xff0c;它能够在java程序运行时直接修改java字节码文件&#xff0c;换句话说它能够直接修改java的二进制文件&#xff1b;也能够跳过编译直接生成字节码文件。所以ASM功能非常强大&#xff0c;对于代码性能提升、代码问题定位都非…

【技术】《Netty》从零开始学netty源码(六十)之ByteToMessageDecoder

ByteToMessageDecoder 在Netty中用于拆包的解码器都继承了抽象类ByteToMessageDecoder&#xff0c;它的类结构如下&#xff1a; 从中可以看出它其实就是一个handler&#xff0c;只要添加到pipeline中channel每次读取数据的时候都会得到解析&#xff0c;它的数据结构如下&#…

业绩涨,股价不涨,蓝思科技深陷「果链困局」

作者 | 辰纹 来源 | 洞见新研社 曾经的女首富&#xff0c;蓝思科技董事长周群飞很困惑&#xff0c;公司业绩明明还算不错&#xff0c;可股价却怎么也涨不起来&#xff0c;距离市值2000亿的顶点更是遥遥无望。 根据不久前&#xff08;4月23日&#xff09;蓝思科技发布的2022年…

Mybatis中处理特殊SQL处理逻辑

文章目录 0、前言1、模糊查询2、动态表名3、获取自增的组件4、批量删除 0、前言 在MyBatis中可能会有一些特殊的SQL需要去执行&#xff0c;一般就是模糊查询、批量删除、动态设置表名、添加功能获取自增的主键这几种&#xff0c;现在分别来进行说明。 为了方便演示 &#xff0…

强化学习路线规划之深度学习代码练习预备

前面已经练习过神经网络的相关代码&#xff0c;其实弄明白了你会发现深度学习其实是个黑盒&#xff0c;不论是TensorFlow还是pytorch都已经为我们封装好了&#xff0c;我们不需要理解深度学习如何实现&#xff0c;神经网络如何计算&#xff0c;这些都不用我们管&#xff0c;可能…

一看就懂之与栈结构(FILO)相对的——队列结构(FLFO)

文章目录 一、什么是队列&#xff0c;什么是FIFO二、使用C模拟实现以及解析队列1.结构体的定义2.队列的创建及销毁3.实现插入操作4.队列删除操作5.获取栈中有效元素个数以及头元素尾元素 源代码分享 一、什么是队列&#xff0c;什么是FIFO ​ 队列允许在一端进行插入操作&…

微服务之以nacos注册中心,以gateway路由转发服务调用实例(第一篇)

实现以nacos为注册中心,网关路由转发调用 项目版本汇总项目初始化新建仓库拉取仓库项目父工程pom初始化依赖版本选择pom文件如下 网关服务构建pom文件启动类配置文件YMLnacos启动新建命名空间配置网关yml(nacos)网关服务启动 用户服务构建pom文件启动类配置文件YML新增url接口配…

[网鼎杯 2020 青龙组]jocker 题解

32位无壳 堆栈有问题 先修堆栈在反编译 查看关键函数 对输入的字符串进行了加密 加密之后omg函数中与存储的字符串进行比较 我们先解密这个 提取数据 解密脚本 data[0x66,0x6b,0x63,0x64,0x7f,0x61,0x67,0x64,0x3b,0x56,0x6b,0x61,0x7b,0x26,0x3b,0x50,0x63,0x5f,0x4d,0x5…

javascript基础二:Javscript字符串的常用方法有哪些?

在日常开发中&#xff0c;我们对字符串也是操作蛮多&#xff0c;这里我们来整理下字符串的一下最常用的方法 一、操作方法 字符串常用的操作方法归纳为增、删、改、查 增 这里增的意思并不是说直接增添内容&#xff0c;而是创建字符串的一个副本&#xff0c;再进行操作 除了…

Python实战基础9-元组、字典、集合

一、元组 Python的元组与列表类似&#xff0c;不同职称在于元组的元素不能修改。元组使用&#xff08;&#xff09;&#xff0c;列表使用[]。 Python的元组与列表类似&#xff0c;不同之处在于元组的元素不能修改&#xff08;增删改&#xff09;&#xff0c; 元组使用小括号()…

VuePress 1.x 踩坑记录

文章目录 前言1.Node.js 版本问题2.侧边栏3.添加页面目录导航4.非首页 footer 不生效5.部署到 Github 的错误vuepress 的 docs 与 Github Pages 的 docs 目录冲突样式丢失 7.资源引用问题本地图片找不到引用 CSDN 图片报 403 错误 参考文献 前言 我的第二本开源电子书《后台开…

被问了100遍的 堆的基本功能如何实现? 绝了!!!

文章目录 堆的介绍堆的概念堆的结构 堆的向下调整算法建堆的时间复杂度 堆的向上调整算法堆的基本功能实现初始化堆打印堆堆的插入堆的删除获取堆顶的数据获取堆的数据个数堆的判空销毁堆 堆的介绍 堆的概念 堆&#xff1a;如果有一个关键码的集合K{k0,k1,k2,…,kn-1}&#x…

计算机图形学-GAMES101-9

前言 材质和光的相互作用很重要。VertexShader和FragmentShader。纹理贴图Texture mapping。 一、在三角形中插值 为什么要在三角形内部插值&#xff1f;虽然我们的操作很多是在三角形顶点上进行计算的&#xff0c;但是对于三角形内部我们也希望每个像素点能得到一个值&…

FLASH锁死,STLink烧程序烧完一次无法再烧?

ST烧程序烧完一次无法再烧&#xff0c;因为把烧录引脚占用&#xff0c;所以可以再配置一下。 &#xff08;平时不勾PA13和PA14&#xff0c;也是会通过PA13和PA14烤录&#xff0c;勾上是为了防止锁死FLASH&#xff09; 如果锁住&#xff0c;再烧烧不进去 卡点&#xff0c;按住复…

【踩坑无数终极0错版】mac-Parallels Desktop的windwos虚拟机安装最新夜神模拟器+burpsuite证书安装+app渗透

文章目录 前言一、安装夜神模拟器二、夜神模拟器配置三、安装证书与所需软件四、测试抓包总结 前言 不想说了&#xff0c;反正我吐了&#xff0c;直接看正文吧。 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、安装夜神模拟器 mac上是安装不成功…

Spring6新特性来了!便捷替代Feign封装RPC接口

spring6的新特性笔者最近也有在研究&#xff0c;其中在HttpServiceProxyFactory服务代理工厂的使用方式体验上&#xff0c;笔者认为极其像是在用Feign编写RPC接口&#xff0c;使用服务代理工厂我们只要在全局配置单例的服务代里工厂bean再维护一个http interface接口就能统一的…

跨域跨网访问延迟高?中科三方云解析智能线路提供最优解析方案

在日常工作生活中&#xff0c;大多数人都是直接通过域名访问web服务器&#xff0c;但计算机并不能直接识别域名&#xff0c;因此需要域名系统&#xff08;DNS&#xff0c;Domain Name System&#xff09;将域名翻译成可由计算机直接识别的IP地址&#xff0c;这个环节就是域名解…

MOSFET开关:电源变换器基础知识及应用

​MOSFET是一种常用的场效应晶体管&#xff0c;广泛应用于电源变换器中。电源变换器是一种将输入电源转换为输出电源的电路&#xff0c;通常用于电子设备中。在本文中&#xff0c;我们将介绍MOSFET开关及其在电源变换器中的基础知识和应用。 一、MOSFET开关的基础知识 MOSFET…