STL中string类的实现

news2024/12/24 3:28:16

目录

引入

构造 | 析构函数

构造函数

析构函数

返回指针的c_str()

求字符大小的size()

operator[]

普通对象调用:

const对象调用:

迭代器的实现

范围for

深浅拷贝

浅拷贝的不足

实现深拷贝

赋值的深拷贝

传统写法与现代写法

传统写法

现代写法

练习

增删查改

增容reserve()

尾插字符push_back()

追加字符串append()

贼好用的operator+=

指定位置插 insert()

删除erase()

查找find()


你知道在STL库中,string是怎么实现的吗?其实我们也能写!

本篇将手把手实现string类。通过自己的实现,我们能更好地去理解string类的底层原理。

引入

首先,在string.h里把string的框架搭好:

#pragma once
namespace jzy    //为了和STL里的string区分,我们把string放进自定义的命名空间里
{
    class string
    {
    public:
​
    private:
        char* _str;
        size_t _size;   //也可以用int,但库里面一般用size_t
        size_t _capacity;  //_capacity是有效字符的空间数,不包括\0
    };
}

构造 | 析构函数

构造函数

构造函数的形参有两种情况,一是有参,二是无参,这两个都要实现。

➡️有参:

string(char* str)      //有参:传字符串首元素的地址过来
            :_str(new char[strlen(str) + 1])    //记得为末尾的\0开一份空间
            , _size(strlen(str))
            , _capacity(strlen(str))
        {
            strcpy(_str, str);
        }

然而,这个函数在测试时却报错了:

#include"string.h"
using namespace jzy;
void test1()
{
    string s1("abc");
}
int main()
{
    test1();
    return 0;
}

报错:

这其实是因为:

abc是位于常量区的常量字符串,被const修饰,不可修改。这就是说,权限比较小。

而它要调用的string函数,形参未被const修饰,是可修改的,权限较大。

就像公司的上下级关系,权限小的不可以调用权限大的,它只能调用平级。

所以,string的形参也要被const修饰。

这个故事告诉我们,能用const就尽量用。

➡️修改后的有参:

class string
    {
    public:
        string(const char* str)  
            :_str(new char[strlen(str) + 1])   
            , _size(strlen(str))
            , _capacity(strlen(str))
        {
            strcpy(_str, str);
        }

➡️无参

string()
            :_str(new char[1])  //尽管无参,仍要为\0开空间
            ,_size(0)
            ,_capacity(0)
        {
            _str[0] = '\0';
        }

或者,用缺省值将有参/无参 合二为一:

string(const char* str = "\0")   //注:这里不能用'\0',得用"\0",//单引号表示的是单个字符,类型为char而非char*         
            :_str(new char[strlen(str) + 1])    
            , _size(strlen(str))
            , _capacity(strlen(str))
        {
            strcpy(_str, str);
        }

补:这里的"\0"其实有点画蛇添足。用""就可以了,里面隐含了\0。只要是常量字符串。都暗含了\0,只是看不见。

那这里的"\0"能不能换成nullptr呢?

不能!因为strlen不会检查判空,而是直接访问字符串,直到找到'\0'才会结束。

如果不传参,那默认为nullptr的话,strlen就会访问空指针,使程序崩溃。

析构函数

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

返回指针的c_str()

函数c_str()的作用?

"在C语言中,使用printf直接输出string类型的字符串可能会出现乱码。这是因为printf函数的%s格式化符号期望传入一个char类型的参数,而string类型的字符串实际上是一个对象,不是一个字符指针。所以在使用printf输出string类型的字符串时,应该使用s.c_str()方法将string类型转换为char类型。

而在C++中,使用cout输出string类型的字符串是没有问题的。cout对string类型有特殊的处理方式,可以直接输出string类型的字符串。

此外,也可以通过循环遍历string的每个字符,使用printf逐个输出字符,或者使用cout逐个输出字符,都可以得到相同的结果。

需要注意的是,如果没有包含<string>头文件,那么默认情况下是不能使用cout输出string类型的字符串的,此时需要使用c_str()方法将string类型转化为char*类型。"

(源自 c知道)

简单来说,string类是无法直接被cout或者printf输出的,它需要被转化成char类型才可以。那c_str()做的就是这样一个转化的工作。

c_str()返回的是字符串的首字母地址,此地址只读不写,因此要用const来修饰:

char* c_str()const  
    {
        return _str;
    }

有了字符串的首元素地址,我们就可以cout输出stirng字符串了。

测试一下:

void test2()
{
    string s1("abcdef");
    cout << s1.c_str() << endl;
}

求字符大小的size()

size_t size()const   //只读不写 就加上const保护
        {
            return _size;
        }

operator[]

operator[]是非常好用的接口,它能把字符串当数组一样使用。这么好用的接口,实现起来其实很简单。

普通对象调用:

char operator[](size_t pos)
        {
            return *(_str + pos);
        }

🤔等等……直接传值返回的话,会有什么弊端吗?

有的!我们无法直接修改字符,来测试一下:

void test3()
{
    string s1("abcdef");
    cout << ++s1[0] << endl;
}

 

传值返回s1[0],我们得到的并不是s1里的'a',而是它的拷贝,这导致我们无法修改真正的a。

如果想要对a做修改,那就要传引用返回。

更新版的operator[]:

char& operator[](size_t pos)
        {
            return *(_str + pos);
        }

const对象调用:

我们知道,const权限小,不能调用权限大的函数。因此,要再写一份const版的operator[]函数:

const char& operator[](size_t pos) const
        {
            return *(_str + pos);
        }

经过const的保护,这个版本的operator[]是只读不写的。

迭代器的实现

之前我们对迭代器的认识为“像指针一样的东西”,那它究竟是不是指针呢?两者又有什么关联呢?别急,实现一遍我们就知道了。

iterator:

begin():返回第一个字符的位置。

end():返回最后一个字符的下一个位置。

typedef char* iterator;
        iterator& begin()  //&可加,也可不加
        {
            return _str;
        }
        iterator end()   //注意:这里不能加&!
        {
            return _str + _size;
        }

这里说明一下,end()为什么不能加&。因为end()指向的不是最后一个字符,而是它的后一个,也就是\0,所以end()处是开区间,是不能取到的。

测试一下:

void test4()
{               
    string s1("hello");
    string::iterator it = s1.begin();
    while (it != s1.end())
    {
        cout << *it << " ";   //可读
        it++;
    }
    cout << endl;
    for (auto ch : s1)
    {
        cout << ++ch << " ";  //可写
    }
}

上面实现的是普通的迭代器,是可读可写的。

现在再实现const_iterator,只读不写的:

const_iterator:

typedef const char* const_iterator;
        const_iterator begin()const
        {
            return _str;
        }
        const_iterator end()const
        {
            return _str + _size;
        }

范围for

之前我们说过,范围for看起来很高级,实际上底层原理很简单,现在我们就来揭秘一下。

其实,只要写了迭代器,那直接就能用范围for,它甚至不需要你去实现。

现在,我们直接在刚刚实现的迭代器后面,使用范围for:

void test4()
{
    string s1("hello");
    string::iterator it = s1.begin();  
    while (it != s1.end())
    {
        cout <<*it<< " ";
        it++;
    }
    
    cout << endl;       
    for (auto ch : s1)  //再遍历一遍
    {
        cout << ch << " ";
    }
}

这时因为:范围for语句的底层原理是通过迭代器来实现的。编译器会用迭代器 来替换范围for。

范围for会自动调用 对象的begin()和end()方法 来获取迭代器的起始、结束位置,然后通过迭代器来遍历。

迭代器是一个对象,用于遍历和访问元素。范围for通过迭代器来遍历集合,不用再显式地操作指针,使代码更简洁易读。

可见,范围for的确没啥含金量……

深浅拷贝

浅拷贝的不足

之前我们了解过,浅拷贝就是值拷贝,对于内置类型,是按字节的方式直接拷贝的。对于自定义类型,是调用其拷贝构造函数完成拷贝的。

浅拷贝真的够用吗?

答案当然是否定的。如果有成员变量是指针,那拷贝时,仅仅是复制了指针的值而不复制指针指向的空间。

如图,string的浅拷贝:

可见,俩指针指向同一片空间。这就导致,当其中一个指针释放空间时,另一个指针也受到影响。

所以说,有指针成员时,就需要进行深拷贝了。

实现深拷贝

深拷贝是由我们自己实现的,拷贝指针时,不仅仅是复制值,更是要复制一份空间。

现在我们来实现下string的深拷贝:

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

可以看到,深拷贝的确是新开了空间:

赋值的深拷贝

其实赋值和刚刚说的拷贝构造是一个道理。很多时候,默认的赋值运算符就够用了,

但当涉及资源管理,如指针,就会出现两个指针指向同一块空间的情况。

来看string赋值的崩溃现场:

void test5()
{
    string s1("hello");
    string s2("111111111111111111111111111");
    s1 = s2;
}

 

那这种场景就需要实现赋值的深拷贝。

我们来实现一下:

string& operator=(const string& s)
        {
            delete[] _str;                       //先释放旧空间
            _str = new char[strlen(s._str) + 1];   //再开新空间
            strcpy(_str, s._str);
            _size = s._size;
            _capacity = s._capacity;
            return *this;
        }

测试下:

void test5()
{
    string s1("hello");
    string s2("111111111111111111111111111");
    s1 = s2;
}

 

可见,赋值成功。

❗但是,这并非 赋值运算符 的最终形态。因为还没检测 是否自己给自己赋值。

🚩这里要注意一个点:在实现赋值的深拷贝时,需要检测是否自己给自己赋值。

先来看看如果给自己赋值,会发生什么,

void test5()
{
    string s1("hello");
    string s2("111111111111111111111111111");
    s2 = s2;     
}

 

s2中的字符居然无效了!(被置成了随机值)

这是因为,this 和 形参s 都是s2。一上来this的_str空间就被释放,所以,此时s的 _str空间也被释放了,这俩现在都是随机值。所以,再把s拷给this,就会出现随机值的状况。

所以说,需要检查 是否自己给自己赋值 的情况。

赋值深拷贝的最终形态:

string& operator=(const string& s)
        {
            if (this != &s)
            {
                delete[] _str;                       //先释放旧空间
                _str = new char[strlen(s._str) + 1];   //再开新空间
                strcpy(_str, s._str);
                _size = s._size;
                _capacity = s._capacity;
                return *this;
            }
        }

传统写法与现代写法

在stirng这里,我们就要学会写同一个功能的两种写法,即传统写法与现代写法。

现代写法 较传统写法的优势目前可能表现不出来,但等我们学到了vector、list时,现代写法就方便多了。

所以,这两种写法,都是有必要掌握的!

下面就用拷贝构造来举例:

传统写法

传统写法,就是老老实实地打工搬砖:开空间、初始化、拷贝:

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

现代写法

现代写法,则精明多了。这些累活我不自己干,我雇打工人tmp来干:

string(const string& s)
        {
            string tmp(s._str);     //先构造个tmp
            swap(_str, tmp._str);    //把tmp交换给我
            swap(_size, tmp._size);
            swap(_capacity, tmp._capacity);
        }

但是这样写,有一个隐患:

this._str未经初始化,里面是随机值。经过swap,把随机值给了tmp. _str。

在delete tmp时,对随机值指向的空间进行释放,可能会引发崩溃。

如果 _str置空的话,delete就不会释放空指针。所以,要给this. _str初始化。

经过改造:

void swap(string&tmp)       //这是写在jzy类域里的swap
        {
            ::swap(_str, tmp._str);    //用::调用全局的swap函数
            ::swap(_size, tmp._size);
            ::swap(_capacity, tmp._capacity);
        }
​
        string(const string& s)
            :_str(nullptr)    //初始化,更安全
            ,_size(0)
            ,_capacity(0)
        {
            string tmp(s._str);
            swap(tmp);     
        }

注:这俩swap不一样,不是函数重载。函数重载的前提是在同一作用域。而这俩swap,一个是类里面的,一个是全局的。

所以说,全局的swap在调用时要加::,不然它会优先去局部域找,找到我们写的那个swap(string&tmp)之后,会认为参数不匹配。

总结一下,现代写法的本质是”拷贝构造新对象+将自己和新对象进行交换“。

练习

现在,在学习了现代写法的思想之后,我们来练习写operator=的现代写法:

void swap(string& tmp)   
        {
            ::swap(_str, tmp._str);  
            ::swap(_size, tmp._size);
            ::swap(_capacity, tmp._capacity);
        }
string& operator=(const string& s)
        {
            if (this != &s)
            {
                string tmp(s._str);
                swap(tmp);
                return *this;
            }
        }

增删查改

增容reserve()

reserve”保留“:开若干个空间,先保留在那里,即增容。

void reserve(size_t size)
        {
            if (size > _capacity)  //先检查下要不要增容
            {
                char* p = new char[size + 1];  //开新空间(为\0多开一个空间)
                strcpy(p, _str);               //拷数据
                delete[] _str;                 //释放旧空间
​
                _str = p;  
                _capacity = size;
            }
        }

尾插字符push_back()

void push_back(const char c)
        {
            if (_size == _capacity)   //先检查要不要扩容
            {
                reserve(_capacity == 0 ? 4 : 2 * _capacity);
            }
            _str[_size++] = c;
            _str[_size] = '\0';   //别忘了最后得加上'\0'
        }

追加字符串append()

void append(const char* ch)
        {
            //考虑扩容
            size_t total_size = _size + strlen(ch);   //先判断容量
            if(_capacity < total_size)
            {
                reserve(total_size);
            }
            strcpy(_str + _size, ch);   //直接把ch拷到\0的位置
            _size += strlen(ch);
        }

贼好用的operator+=

在了解stirng类的方法时,我们就惊叹过,operator+=真的好好用,既能追加字符,又能加字符串。

实际上,追加字符 和 追加字符串 是构成重载的两个函数,现在我们来实现下。

//追加字符
string& operator+=(const char c)
{
    push_back(c);    //复用了push_back()
    return *this;
}
​
//追加字符串
string& operator+=(const char* ch)
{
    append(ch);    //复用了append()
    return *this;
}

指定位置插 insert()

插字符:

string& insert(size_t pos, char c)
        {
            assert(pos <= _size);
​
            if (_size == _capacity)
            {
                reserve(_capacity == 0 ? 4 : 2 * _capacity);
            }
​
            //先把后面的数据往后挪
            size_t end = _size - 1;
            while (end > pos)
            {
                _str[end] = _str[end - 1];
                end--;
            }
            //再插入
            _str[pos] = c;
            _size++;
            _str[_size] = '\0';
​
            return *this;  //其实这个返回值意义不大
        }

插字符串:

string& insert(size_t pos, const char* ch)
        {
            assert(pos <= _size);
            
            size_t total_size = _size + strlen(ch); 
            if (_capacity < total_size)   //先算出一共需要多少空间,不够就开
            {
                reserve(total_size);
            }
​
            size_t end = _size ;   //挪数据
            size_t span = strlen(ch);
            while (end > pos)        //这个循环很容易写错! 不能写end>=pos,减成负数也就是无穷大,很容易越界!
            {
                _str[end + span] = _str[end];
                end--;
            }
            _str[pos+span] = _str[pos];
​
            strncpy(_str+pos, ch, strlen(ch));
            _size += strlen(ch);
            return *this;
        }

删除erase()

void erase(size_t pos, size_t len = npos)
        {
            assert(len < _size);
            if (len == npos||len>_size-pos)  //当删到末尾或者不够删时
            {
                _str[pos] = '\0';
            }
            else
            {
                strcpy(_str + pos, _str + pos + len);
                _str[_size - len] = '\0';
            }
            _size -= len;
        }

关于npos:

npos的类型为size_t,它被设为-1,因为size_t表示无符号数,-1在无符号数中表示 最大值。

npos在字符串中,意味着直到字符串的末尾。在容器中,表示不存在的位置。

我们要在类里声明静态的npos,在类外定义:

//类里
private:
        char* _str;
        size_t _size;   
        size_t _capacity;  
​
        static size_t npos; 
//类外
size_t string::npos = -1;

或者,在类里这样写:

private:
        char* _str;
        size_t _size;   
        size_t _capacity;  
​
        const static size_t npos = -1;   //const static在C++中是语法的特殊处理,直接可以当成定义初始化

查找find()

查找字符:

size_t find(char c, size_t pos = 0)const   //从pos位置开始找c,找到返回下标
        {
            assert(pos < _size);
            for (; pos < _size; pos++)
            {
                if (_str[pos] == c)
                {
                    return pos;
                }
            }
            return npos;
        }

查找字符串:

size_t find(const char* s, size_t pos)const   //第一个const一定要加!!
        {
            assert(pos < _size);
            const char* ret = strstr(_str + pos, s);
            if (ret == nullptr)
            {
                return npos;
            }
            return ret-_str;
        }

这里说明一下,关于左操作数的const为什么一定要加。

在测试时,我们给的例子为:s1.find("day", 0),这里的”day“是常量字符串,类型为const char*,而不是char *,

所以左操作数的类型也要严格为const char*,才能与常量字符串匹配。

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

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

相关文章

前端开发之服务器的基本概念与初识Ajax

1&#xff0c;服务器的基本概念与初识Ajax 1.1 URL地址的组成部分 1.2 客户端与服务器的通信过程 1.3 网页中如何请求数据 1.4 $.get()函数 1.4.1 $.get()函数的语法 // jQuery 中 $.get() 函数的功能单一&#xff0c;专门用来发起 get 请求&#xff0c;从而将服务器上的资源…

Cartesi Rollups在主网上正式启用,推出首个DApp,名为Honeypot

Cartesi的贡献者呼吁所有Web3开发者测试并加固Cartesi Rollups的安全性&#xff0c;因为Honeypot的资金每周以复利增长8% 2023年9月26日&#xff0c;在今天的美国纽约&#xff0c;Cartesi&#xff08;CTSI&#xff09;, 一种具有能够运行Linux发行版的虚拟机的特定应用程序的R…

​cannot import name ‘container_abcs’ from ‘torch._six’​

因为1.8版本之后container_abcs就已经被移除了。 cannot import name ‘container_abcs’ from ‘torch._six’ 解决办法: 改成以下写法&#xff1a; import collections.abc as container_abcs int_classes int

怎么写一个可以拖拽缩放的div?

说在前面 元素拖拽和缩放现在也是一个很常见的功能&#xff0c;让我们从实现div元素的拖拽缩放开始来了解元素拖拽缩放的具体原理和实现方法吧。 效果展示 实现步骤 画一个div 首先我们需要先画一个div&#xff0c;并给它8个方位&#xff08;上、下、左、右、左上、右上、右下…

Linux学习之HIS部署(5)

MySQL部署 #安装MySQL服务 [rootServices ~]# yum clean all; yum repolist -v ... Total packages: 8,265 [rootServices ~]# yum -y install mysql.x86_64 mysql-server.x86_64 mysql-devel.x86_64 ... Complete! [rootServices ~]# #启动MySQL服务 [rootServices ~]# syst…

【数据库——MySQL】(8)表数据插入、修改和删除练习及讲解

目录 1. 题目2. 解答 1. 题目 建立的数据库 YGGL&#xff0c;向库中的 3 个表中插入多行数据记录&#xff0c;然后修改和删除一些记录。 根据下表的样本数据&#xff0c;使用 SQL 语句向 Departments 表中插入数据。 使用 SQL 语句向 Employees 表中插入前 6 条数据。 使用…

Android SeekBar使用避坑指南

这里写自定义目录标题 SeekBar简介问题1.纵向进度条问题2.SeekBar间隙问题3.SeekBar高度设置不生效 SeekBar简介 SeekBar是Android原生UI组件&#xff0c;可以用来调节进度&#xff0c;广泛应用于音乐、视频进度展示调控、音量、亮度调节等功能里。 SeekBar的使用很简单&…

【MySQL】数据类型(一)

文章目录 前言一. tinyint等整型二. bit位字段类型三. float浮点型四. decimal浮点型结束语 前言 MySQL也有数据类型&#xff0c;其中一些与C/C/Java是一样的&#xff0c;但也有一些数据类型不同&#xff0c;更有新的独有的数据类型 一. tinyint等整型 MySQL将整型按照字节分成…

leetcode 33. 搜索旋转排序数组

2023.9.26 本题暴力法可以直接A&#xff0c;但是题目要求用log n的解法。 可以想到二分法&#xff0c;但是一般二分法适用于有序数组的&#xff0c;这里的数组只是部分有序&#xff0c;还能用二分法吗&#xff1f; 答案是可以的。因为数组是经过有序数组旋转得来的&#xff0c;…

Ae 效果:CC Split、CC Split 2

扭曲/CC Split Distort/CC Split CC Split &#xff08;CC 分割&#xff09;主要用于将图像在指定的两点之间进行分割&#xff0c;可以创造出独特的图像分割效果。 ◆ ◆ ◆ CC Split 效果属性说明 Point A 点 A 用于设置分割线的起始点位置。 可手动在查看器面板上拖动来改变…

Linux 线程同步(重要) 互斥量

/*三个窗口卖一百张票 */#include<stdio.h> #include<unistd.h> #include<pthread.h> #include<string.h> int tickets 0; void * sellticket(void * arg) {//卖票usleep(7000);while(tickets < 100) {printf("%ld 正在卖第 %d 张票\n",…

linux使用操作[3]

文章目录 版权声明环境变量$符号自行设置环境变量 上传、下载rz、sz命令 压缩、解压tar命令压缩tar解压zip 命令压缩文件unzip 命令解压文件 版权声明 本博客的内容基于我个人学习黑马程序员课程的学习笔记整理而成。我特此声明&#xff0c;所有版权属于黑马程序员或相关权利人…

什么是物联网智慧公厕?

在当今科技快速发展的背景下&#xff0c;具备全感知、可靠传输、智能处理三大特点的物联网技术&#xff0c;正逐渐渗透到各个领域。而智慧公厕作为其中的一个创新应用&#xff0c;正逐渐受到市场的关注和重视。 什么是物联网智慧公厕&#xff1f;物联网智慧公厕是指通过物联网…

图像采集卡在应用程序的重要性概述

达到最大吞吐量是工业和工厂自动化的关键标准之一。提高传感器分辨率和帧速率有助于实现目标&#xff0c;但会限制带宽&#xff0c;并带来新的传输问题。图像采集卡是将相机连接到PC的最方便、最可靠的方式&#xff0c;在工业环境中使用图像采集卡为高速应用带来了特定的好处&a…

MySQL 内部组件结构以及SQL执行逻辑

目录 一、MySQL的的内部组件结构二、连接器三、查询缓存四、分析器五、优化器六、执行器 一、MySQL的的内部组件结构 Server层 主要包括连接器、查询缓存、分析器、优化器、执行器等&#xff0c;涵盖 MySQL 的大多数核心服务功能&#xff0c;以及所有的内置函数 &#xff08;如…

接口自动化测试思路和实战(4):数据驱动测试框架

目录 数据驱动测试框架 步骤1、在项目根目录下新建conf文件夹&#xff0c;在下面新建config.ini文件 步骤2、在common的py文件夹下新建ini_file_utils.py文件 步骤3&#xff0c;重写local_config.py文件&#xff0c;封装读取ini文件中的值 步骤4、修改common_function.py文…

基于微信小程序的火锅店点餐订餐系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言系统主要功能&#xff1a;具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计…

MySQL基础篇-约束

目录 1.约束概述 2.分类 3.测试user表的约束情况 主键约束 非空约束及唯一约束 检查约束 默认约束 4.外键约束 外键约束的语法 外键约束的删除/更新行为 小结 1.约束概述 MySQL约束&#xff08;Constraints&#xff09;是用于确保表中数据完整性和一致性的规则。它们定…

“童”趣迎国庆 安全“童”行-柿铺梁坡社区开展迎国庆活动

“金秋十月好心境&#xff0c;举国欢腾迎国庆。”国庆节来临之际&#xff0c;为进一步加强梁坡社区未成年人爱国主义教育&#xff0c;丰富文化生活&#xff0c;营造热烈喜庆、文明和谐的节日氛围。9月24日上午&#xff0c;樊城区柿铺街道梁坡社区新时代文明实践站联合襄阳市和时…

借助 ControlNet 生成艺术二维码 – 基于 Stable Diffusion 的 AI 绘画方案

背景介绍 在过去的数月中&#xff0c;亚马逊云科技已经推出了多篇博文&#xff0c;来介绍如何在亚马逊云科技上部署 Stable Diffusion&#xff0c;或是如何结合 Amazon SageMaker 与 Stable Diffusion 进行模型训练和推理任务。 为了帮助客户快速、安全地在亚马逊云科技上构建、…