Linux的基礎I/O

目錄

1、理解“文件”

1.1 狹義理解

1.2 廣義理解

1.3 文件操作的歸類認知

1.4?系統角度

2、回顧C文件接口

2.1 文件的打開與關閉

2.2 文件的讀寫函數

2.3?stdin & stdout & stderr

3、系統文件I/O

3.1 一種傳標志位的方式

3.2 文件的系統調用接口

3.2.1 open()

3.2.2 read()?& write()?& close()

3.3 庫函數和系統調用

3.4?文件描述符fd

3.4.1 0 & 1 & 2

3.4.2 文件描述符的分配規則

3.4.3 重定向

3.4.4 重定向系統調用dup2()

4、理解一切皆文件

5、緩沖區

5.1 緩沖區的定義

5.2 緩沖區的作用

5.3 緩沖區的機制

現象1:

現象2:


1、理解“文件”

1.1 狹義理解

  • 文件在磁盤里
  • 磁盤是永久性存儲介質,因此文件在磁盤上永久性存儲
  • 磁盤是外設(即是輸出設備也是輸入設備)。
  • 對磁盤文件的所有操作(如讀取、寫入)本質上都是對外設的輸入/輸出,簡稱I/O(Input/Output)。

1.2 廣義理解

  • Linux中,一切皆文件(鍵盤、顯示器、網卡、磁盤……這些都是抽象化的過程)(后面會深入理解)。

1.3 文件操作的歸類認知

  • 文件 = 屬性(元數據)+ 內容
  • 對于0KB的空文件是占用磁盤空間的,有文件屬性。
  • 所有的文件操作本質文件內容操作文件屬性操作

1.4?系統角度

  • 對文件的操作本質進程對文件的操作
  • 磁盤管理者操作系統
  • 文件的讀寫本質不是通過C語言/C++的庫函數來操作的(這些庫函數只是為用戶提供方便),而是通過文件相關的系統調用接口來實現的。

文件分為“內存級(被打開)”文件“磁盤級(未打開)”文件

本節主講“內存級(被打開)”文件

2、回顧C文件接口

2.1 文件的打開與關閉

FILE *fopen(const char *path, const char *mode);

mode含義文件不存在時文件存在時寫入方式
"r"只讀返回?NULL正常打開不可寫入
"r+"讀寫返回?NULL正常打開從當前位置覆蓋
"w"只寫(新建)新建文件清空原內容從頭寫入
"w+"讀寫(新建)新建文件清空原內容從頭寫入
"a"追加(只寫)新建文件保留內容,追加到末尾只能末尾追加
"a+"追加(讀寫)新建文件保留內容,可讀/追加寫入可讀,但寫入僅限末尾

int fclose(FILE *fp);

注意:

ls /proc/[ 進程 id] -l 命令,查看當前正在運行進程的信息。

  • cwd:指向進程的當前工作目錄創建文件和打開文件的默認路徑
  • exe:指向啟動當前進程的可執行文件的路徑

2.2 文件的讀寫函數

函數名功能描述適用流類型參數說明返回值備注
fgetc從流中讀取單個字符所有輸入流
(如stdin、文件)
FILE *stream(文件指針)讀取的字符(int
失敗返回EOF
通常用于逐字符處理
fputc向流寫入單個字符所有輸出流
(如stdout、文件)
int char(字符)
FILE *stream
寫入的字符(int
失敗返回EOF
fgets從流中讀取一行文本所有輸入流char *str(緩沖區)
int n(最大長度)
FILE *stream
成功返回str
失敗返回NULL
保留換行符\n
fputs向流寫入一行文本所有輸出流const char *str(字符串)
FILE *stream
成功返回非負值
失敗返回EOF
不自動添加換行符
fscanf格式化輸入(類似scanf所有輸入流FILE *stream
const char *format(格式字符串)
...(變量地址)
成功匹配的參數數量
失敗返回EOF
需注意緩沖區溢出風險
fprintf格式化輸出(類似printf所有輸出流FILE *stream
const char *format
...(變量值)
成功返回寫入字符數
失敗返回負值
fread二進制輸入(塊讀取)文件流void *ptr(緩沖區)
size_t size(每塊大小)
size_t nmemb(塊數)
FILE *stream
實際讀取的塊數用于結構體等二進制數據
fwrite二進制輸出(塊寫入)文件流const void *ptr(數據地址)
size_t size
size_t nmemb
FILE *stream
實際寫入的塊數

注意:

寫字符串,不用寫\0,因為這是C語言的規定,不是文件的規定,寫進去會亂碼。?

2.3?stdin & stdout & stderr

C程序啟動默認打開三個輸入輸出流,分別是stdin,stdout,stderr

#include <stdio.h>extern FILE *stdin;  // 標準輸入,鍵盤文件
extern FILE *stdout; // 標準輸出,顯示器文件
extern FILE *stderr; // 標準錯誤,顯示器文件

3、系統文件I/O

3.1 一種傳標志位的方式

使用位圖,用比特位作為標志位。

#include <stdio.h>#define ONE     (1 << 0)  // 0000 0001 (二進制)
#define TWO     (1 << 1)  // 0000 0010 (二進制)
#define THREE   (1 << 2)  // 0000 0100 (二進制)void func(int flags) {if (flags & ONE)   printf("flags has ONE! ");if (flags & TWO)   printf("flags has TWO! ");if (flags & THREE) printf("flags has THREE! ");printf("\n");
}int main() {func(ONE);                // 輸出: flags has ONE!func(THREE);              // 輸出: flags has THREE!func(ONE | TWO);          // 輸出: flags has ONE! flags has TWO!func(ONE | TWO | THREE);  // 輸出: flags has ONE! flags has TWO! flags has THREE!return 0;
}

3.2 文件的系統調用接口

man 2 系統調用,有具體說明。

3.2.1 open()
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

pathname:要打開或創建的目標文件路徑。

flags:打開文件時的選項標志,可以使用以下常量通過"或"運算(|)組合:

必須指定且只能指定一個的選項:

  • O_RDONLY只讀打開。

  • O_WRONLY只寫打開。

  • O_RDWR讀寫打開。

可選標志:

  • O_CREAT:若文件不存在則創建它(需要mode參數,設置新文件的訪問權限)。

  • O_APPEND追加寫模式。

  • O_TRUNC:如果文件已存在且為普通文件,打開時會將其長度截斷為0邏輯上的清空(類似與vector的size)

return value:

  • 成功:返回新打開的文件描述符fd非負整數

  • 失敗:返回-1

注意:

那么C語言的fopen的flag就是:

“r” = O_RDONLY;

“w” = O_CREAT | O_WRONLY | O_TRUNC;

“a” = O_CREAT | O_WRONLY | O_APPEND;

“r+” = O_RDWR;

“w+” =?O_CREAT | O_RDWR?| O_TRUNC;

“a+” = O_CREAT | O_RDWR?| O_APPEND。

3.2.2 read()?& write()?& close()

類比C文件相關接口。

#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);
fd:文件描述符
buf:存儲讀取數據的緩沖區
count:請求讀取的字節數
#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
fd:文件描述符
buf:包含待寫入數據的緩沖區
count:請求寫入的字節數
#include <unistd.h>int close(int fd);
fd:要關閉的文件描述符

注意:

read()和write()的buf都是void*,不關心數據格式,以二進制流輸入輸出。

那么為什么語言層,有字符流的輸入輸出?

  • 首先,底層都是二進制流的輸入輸出。
  • 字符按ASCII輸入(讀出),按ASCII輸出(寫入)。對于字符設備,字符通過ASCII轉化成二進制寫到里面,然后通過ASCII解釋,以字符的形式顯示。

字符流的輸入輸出,是因為,我們輸入輸出的是字符串

3.3 庫函數和系統調用

類型示例函數所屬層級特點
庫函數fopen,?fclose,?fread,?fwriteC標準庫(libc)1. 提供更高級的抽象
2. 帶緩沖區
3. 可移植性更好
4. 最終會調用系統調用
系統調用open,?close,?read,?write,?lseek操作系統接口1. 直接與內核交互
2. 無緩沖區
3. 效率更高但更底層
4. 與具體操作系統相關

3.4?文件描述符fd

3.4.1 0 & 1 & 2

Linux 進程默認情況下會有 3 個缺省打開的文件描述符,分別是

標準輸入 0標準輸出 1標準錯誤 2

0,1,2 對應的物理設備一般是:鍵盤顯示器顯示器

所以輸入輸出還可以采用如下方式:

0,1,2是自動打開的

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>  // 添加read/write所需的頭文件int main()
{char buf[1024];// 從標準輸入(文件描述符0)讀取數據ssize_t s = read(0, buf, sizeof(buf) - 1); // 保留1字節給結尾的\0if(s > 0) {buf[s] = '\0';  // 添加字符串結束符// 將輸入內容同時輸出到標準輸出(1)和標準錯誤(2)write(1, buf, s);write(2, buf, s);}return 0;
}

而現在知道,文件描述符就是從 0 開始的小整數。當我們打開文件時,操作系統在內存中要創建相應的數據結構來描述目標文件。于是就有了 file 結構體,表示一個已經打開的文件對象。而進程執行 open 系統調用,所以必須讓進程和文件關聯起來。每個進程都有一個指針 * files指向一張表 files_struct,該表最重要的部分就是包含一個指針數組每個元素都是一個指向打開文件的指針!所以,本質上,文件描述符就是該數組的下標。所以,只要拿著文件描述符,就可以找到對應的文件。

注意:

C語言的stdin(fd = 0),stdout(fd = 1),stderr(fd = 2),是一個FILE結構體的指針,FILE結構體里面封裝了文件描述符fd,其他語言也一樣。

3.4.2 文件描述符的分配規則

直接看代碼:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>  // 添加 close 函數所需的頭文件int main()
{int fd = open("myfile", O_RDONLY);if (fd < 0) {perror("open");return 1;}printf("fd: %d\n", fd);close(fd);  // 正確的關閉位置return 0;
}

輸出:?fd: 3

關閉 fd = 0 或者 fd = 2,再看

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>  // 添加 close() 所需的頭文件int main()
{close(0);  // 關閉標準輸入(文件描述符 0)// close(2);  // 注釋掉的關閉標準錯誤(文件描述符 2)int fd = open("myfile", O_RDONLY);if (fd < 0) {perror("open");return 1;}printf("fd: %d\n", fd);close(fd);               // 關閉文件描述符return 0;
}

?輸出:?fd: 0或?fd: 2

結論:

在 Linux 系統中,文件描述符的分配原則最小的沒有被使用下標,作為fd,給新打開的文件

3.4.3 重定向

那如果關閉 fd = 1 呢?看代碼:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>int main()
{close(1);int fd = open("myfile", O_CREAT | O_WRONLY | O_TRUNC, 0644);if(fd < 0) {perror("open");return 1;}printf("fd: %d\n", fd);fflush(stdout);close(fd);exit(0);
}

因為語言層只認stdout中的fd = 1,此時下標為1的指針指向myfile,所以

本來應該輸出到顯示器上的內容,輸出到了myfile文件中。

這種現象叫做輸出重定向

常見的重定向有: >,>>,<。

輸出重定向的本質:

注意:

cat log.txt > myfile,實際上是cat log.txt 1>myfile只重定向了標準輸出

cat log.txt 1>myfile 2>&1重定向了標準輸出和標準錯誤

3.4.4 重定向系統調用dup2()
#include <unistd.h>int dup2(int oldfd, int newfd);

oldfd的指針 覆蓋 ?newfd的指針 。

如:dup2(fd,0),實現輸入重定向,dup2(fd,1),實現輸出重定向。

所以,重定向 = 文件打開方式 + dup2()

4、理解一切皆文件

首先,在 Windows 中是文件的東西,它們在 Linux 中也是文件;其次一些在 Windows 中不是文件的東西,比如進程、磁盤、顯示器、鍵盤這樣的硬件設備也被抽象成了文件,你可以使用訪問文件的方法訪問它們獲得信息;甚至管道,也是文件;將來我們要學習網絡編程中的 socket(套接字)這樣的東西,使用的接口跟文件接口也是一致的。

這樣做最明顯的好處是,開發者僅需要使用一套 API ,即可調取 Linux 系統中絕大部分的資源。舉個簡單的例子,Linux 中幾乎所有讀(讀文件,讀系統狀態,讀 PIPE)的操作都可以用 read 函數來進行;幾乎所有更改(更改文件,更改系統參數,寫 PIPE)的操作都可以用 write 函數來進行。

上圖中的外設,每個設備都可以有自己的 read、write,但一定是對應著不同的操作方法!!但通過 struct file 下的 struct file_operations 中的各種函數回調,讓我們開發者只用 file 便可調取 Linux 系統中絕大部分的資源!!這便是 "Linux 下一切皆文件" 的核心理解。

封裝+多態的體現。?

5、緩沖區

5.1 緩沖區的定義

臨時存儲數據的內存區域。

5.2 緩沖區的作用

提高使用者的效率。

5.3 緩沖區的機制

  • 用戶級語言層緩沖區,避免頻繁調用系統調用(成本高),提高C語言接口的效率。
  • 文件內核緩沖區,提高系統調用的效率。
  • 可以通過fsync(),將文件內核緩沖區的數據刷新到硬件。
  • 一般認為數據交給OS,就相當于交給硬件。

基于上面的機制,可以理解下面的現象:

現象1:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main() {// 關閉標準輸出(文件描述符1)close(1);int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0664);if (fd < 0) {perror("open");return 1;}printf("hello world: %d\n", fd);  // 注意:這里打印的fd值應該是1close(fd);return 0;
}

這個時候,對于普通文件,應該是滿了刷新,可是沒滿,也沒有強制刷新,然后關閉了fd,在程序退出時,刷新,但fd已經關閉了,刷新不了,所以log.txt中不會有數據。

可以使用 fflush()?強制刷新下緩沖區。

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main() {// 關閉標準輸出(文件描述符1)close(1);int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0664);if (fd < 0) {perror("open");return 1;}printf("hello world: %d\n", fd);  // 注意:這里打印的fd值應該是1fflush(stdout); // 強制刷新close(fd);return 0;
}

注意:stderr是不帶緩沖區,即立即刷新

現象2:
#include <stdio.h>
#include <string.h>
#include <unistd.h>  // 添加 write() 和 fork() 所需的頭文件int main() {const char *msg0 = "hello printf\n";const char *msg1 = "hello fwrite\n";const char *msg2 = "hello write\n";printf("%s", msg0);fwrite(msg1, 1, strlen(msg1), stdout);write(1, msg2, strlen(msg2));fork();return 0;
}

結果:

hello printf
hello fwrite
hello write

顯示器,行刷新;

系統調用write(),直接寫入內核。

但是重定向一下 ./hello > file,結果:

hello write
hello printf
hello fwrite
hello printf
hello fwrite

系統調用write(),直接寫入內核;

重定向,改變了刷新方式,普通文件,滿了刷新,可是沒慢,也沒有強制刷新,程序退出時,刷新,父子進程各刷新一份。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/88546.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/88546.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/88546.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

廣告匹配策略的智能化之路:人工智能大模型的方法和步驟

摘要 廣告匹配策略是指根據用戶的需求和偏好&#xff0c;向用戶推薦最合適的廣告的方法。廣告匹配策略的優化是數字化營銷的核心問題之一&#xff0c;也是提升廣告效果和收益的關鍵因素。本文介紹了如何利用人工智能大模型&#xff0c;從數據分析、廣告推薦、策略優化、效果評…

飛算JavaAI:重塑Java開發的“人機協同“新模式

引言 在Java開發領域&#xff0c;“效率"與"質量"的平衡始終是開發者面臨的核心挑戰——重復編碼消耗精力、復雜業務易出漏洞、老系統重構舉步維艱。飛算JavaAI的出現&#xff0c;并非簡單地用AI替代人工&#xff0c;而是構建了一套"AI處理機械勞動&#…

運行ssh -T git@github.com報錯

運行ssh -T gitgithub.com報錯 no such identity: /root/.ssh/id_rsa: No such file or directory gitssh.github.com: Permission denied (publickey). 如果我用的是ed25519而非rsa&#xff0c;有id_ed25519 則需要打開~/.ssh/config檢查一下是否寫錯了 vim ~/.ssh/config 然后…

20250710-2-Kubernetes 集群部署、配置和驗證-網絡組件存在的意義?_筆記

一、網絡組件的作用&#xfeff;1. 部署網絡組件的目的&#xfeff;核心功能&#xff1a;執行kubectl apply -f calico.yaml命令的主要目的是為Kubernetes集群部署網絡組件必要性&#xff1a;解決Pod間的跨節點通信問題建立集群范圍的網絡平面&#xff0c;使所有Pod處于同一網絡…

【牛客刷題】dd愛科學1.0

文章目錄 一、題目介紹1.1 題目描述1.2 輸入描述:1.3 輸出描述:1.4 示例1二、解題思路2.1 核心策略2.2 算法流程2.3 正確性證明三、算法實現四、關鍵步驟解析五、復雜度分析六、正確性驗證七、算法對比7.1 暴力搜索法7.2 動態規劃7.3 三種解法對比分析一、題目介紹 1.1 題目描…

跑步-Java刷題 藍橋云課

目錄 題目鏈接 題目 解題思路 代碼 題目鏈接 競賽中心 - 藍橋云課 題目 解題思路 用數組記錄每個月有多少天,再使用一個int型變量記錄是星期幾,遍歷即可 代碼 import java.util.Scanner; // 1:無需package // 2: 類名必須Main, 不可修改public class Main {public stat…

Qt常用控件之QWidget(二)

Qt常用控件&#xff08;二&#xff09;1.window frame2.windowTitle3.windowIcon&#x1f31f;&#x1f31f;hello&#xff0c;各位讀者大大們你們好呀&#x1f31f;&#x1f31f; &#x1f680;&#x1f680;系列專欄&#xff1a;【Qt的學習】 &#x1f4dd;&#x1f4dd;本篇…

飛算Java AI:專為 Java 開發者打造的智能開發引擎

目錄 一&#xff0c;核心功能 1&#xff0c;智能編碼&#xff08;AI Coding&#xff09; 2&#xff0c;AI 驅動測試&#xff08;AI Testing&#xff09; 3&#xff0c;智能運維&#xff08;AIOps&#xff09; 4&#xff0c;工程化支持 二、注冊與上手&#xff1a;3 分鐘快…

基于開源AI大模型AI智能名片S2B2C商城小程序源碼的私域流量新生態構建

摘要&#xff1a;私域流量并非新生概念&#xff0c;企業持續構建和經營“企業 - 客戶”關系是其持續存在的關鍵&#xff0c;且會隨時代發展自我完善迭代。本文探討了開源AI大模型AI智能名片S2B2C商城小程序源碼在私域流量領域的應用價值。通過分析私域流量發展現狀與挑戰&#…

用 ELK+Filebeat 提高50%問題排查效率,這套方案實測有效!

摘要 在中大型系統中&#xff0c;日志的分布常常讓問題排查變得異常痛苦&#xff1a;每次出錯都要登錄一堆服務器、翻一堆文本&#xff0c;還不一定能找到關鍵線索。為了解決這個問題&#xff0c;ELK&#xff08;Elasticsearch、Logstash、Kibana&#xff09;日志聚合平臺應運而…

數據治理到底是什么?搞清這四件事,你就徹底明白了!

目錄 第一件事&#xff1a;數據治理不是做“數據”&#xff0c;是做“管” 第二件事&#xff1a;治理的核心&#xff0c;是“數、責、權”的三角綁定 一是“數”&#xff1a;你到底有哪些數據&#xff1f; 二是“責”&#xff1a;每張表、每個字段是誰負責&#xff1f; 三…

Spring的事務控制——學習歷程

思考&#xff1a;1. 事務是干什么的&#xff1f;2. 事務的特性&#xff1f;3. 事務控制的傳播方式&#xff08;傳播行為&#xff09;4. 事務的隔離級別5. 事務是如何實現的&#xff1f;6. 事務的回滾方式7. 事務失效場景回答&#xff1a;1. 事務和鎖&#xff0c;還有版本控制 …

鴻蒙 Secure Boot 全流程解析:從 BootROM 到內核簽名驗證的實戰指南

摘要 隨著智能設備應用的深入&#xff0c;操作系統安全成為設備可信運行的基礎。在物聯網和多終端場景中&#xff0c;一旦系統被惡意篡改&#xff0c;將帶來數據泄露、設備被控等嚴重后果。鴻蒙系統在安全啟動方面設計了完整的機制&#xff0c;從最底層的 Boot ROM 開始逐級校驗…

tailwindCSS === 使用插件自動類名排序

目錄 類如何排序 自定義 實戰應用 .prettierrc package .eslintrc 人們一直在討論在 Tailwind 項目中對實用程序類進行排序的最佳方法。今天&#xff0c;我們很高興地宣布&#xff0c;隨著我們官方 prettier-plugin-tailwindcss 的發布&#xff0c;您終于可以不用為此擔…

數據結構 —— 鍵值對 map

目錄 map的若干操作 1、emplace() 2、find(key) 3、count(key) 4、lower_bound 和 upper_bound 5、erase() 6、empty() 7、降序的map 計蒜客T3603 叫號系統 題意&#xff1a; 解題思路&#xff1a; Code: Leetcode1309 解碼字母到整數映射 題意&#xff1a; 解題…

C++ 性能優化指南

C 性能優化指南&#xff08;針對 GCC 編譯器&#xff0c;面向高級工程師面試&#xff09; 代碼優化面試常問點&#xff1a; 如何避免不必要的對象拷貝&#xff1f;為什么要用引用或 std::move&#xff1f;虛函數調用有什么性能開銷&#xff1f;原理解釋&#xff1a; 傳遞對象時…

拼數(字符串排序)

題目描述設有 n 個正整數 a1?…an?&#xff0c;將它們聯接成一排&#xff0c;相鄰數字首尾相接&#xff0c;組成一個最大的整數。輸入格式第一行有一個整數&#xff0c;表示數字個數 n。第二行有 n 個整數&#xff0c;表示給出的 n 個整數 ai?。輸出格式一個正整數&#xff…

【MySQL】函數學習-字符串函數

一、MySQL字符串函數基礎回顧 在MySQL中&#xff0c;字符串函數用于處理文本數據&#xff0c;常見場景包括數據拼接、格式轉換、清洗等。以下是核心函數速覽&#xff1a;函數名作用說明基礎示例&#xff08;獨立運行&#xff09;CONCAT(s1,s2)拼接多個字符串SELECT CONCAT(heel…

AI不是“心智的蒸汽機“:重新理解人工智能的本質

當我們談論人工智能時&#xff0c;最常聽到的比喻是"心智的蒸汽機"——一個能夠自動化認知任務的強大工具。但這個比喻可能從根本上誤導了我們對AI真正潛力的理解。 最近&#xff0c;來自科羅拉多大學丹佛分校和肯尼索州立大學的研究團隊發表了一篇論文[1]&#xff0…

免費的AI Logo工具生成的Logo質量怎么樣?我對比了7個AI Logo生成器,設計必備

你嘗試過用 AI 生成 Logo 嗎&#xff1f;在 AI 巨火的今天&#xff0c;什么事情都可以嘗試用 AI 去做。在品牌設計上也是如此&#xff0c;用 AI 做品牌設計、用 AI 做電商海報、用 AI 做包裝設計等等。不知道你用過哪些 AI 工具&#xff0c;哪些是你覺得好用的。今天我們就來研…