# 11.C File Processing
## 11.1 Introduction
儲存在變數和陣列裡的資料是暫時的,當程式結束後儲存的資料會全部遺失,所以程式會利用 ==**檔案(Files)**== 來長期保存大量資料。
> Files通常存放在輔助性儲存裝置
> ex:硬碟,固態磁碟(SSD),快閃儲存裝置和DVD
## 11.2 Files and Streams
C將每個檔案視為連續的位元組串流(a sequential stream of bytes),不同的作業系統會有不同判斷檔案結尾的方法,像 **end-of-file marker**(檔案結尾記號)或是在系統維護管理結構中新增該檔案的總位,這些動作對我們來說是隱藏的。

#### Standard Streams in Every Program:
開啟一個檔案後,就會有一個stream和這個檔案結合在一起,同時有三個stream也會自動開啟
:::warning
* standard input:從鍵盤讀取資料
* standard output:將資料印到螢幕上
* standard error:將錯誤訊息顯示到螢幕上
:::
#### Communication Channels(通訊通道):
stream提供程式與檔案之間的通訊管道,讓程式可以正確的讀取資料或印出資料
#### ==FILE Structure:==
開啟檔案會回傳一個指向 **FILE結構** (定義在<stdio.h>中)的 **指標** ,這個指標通常含有處理這個檔案所需的資訊,某些作業系統的FILE結構中包含了一個 **file descriptor** (檔案描述子)也就是指到作業系統內 **open file table**(開啟檔案表)陣列的索引值,這個陣列的每一個元素都含有一個**file control block (FCB)** (程式控制區塊)
作業系統便是由FCB來管理某個檔案。標準輸入、標準輸出、標準錯誤會以下列指標進行操作:**stdin、stdout、stderr**
> FCB:包含檔案位置+檔案的metadata->為了存取檔案所以需要FCB
> metadata:資料屬性
#### fgetc():
```c=
fgetc("檔案指標")
//如果打fgetc(stdin)會等同於getchar()
```
有點像getchar,一次從檔案讀取一個字元
file.txt裡面:
```
1234
555
000
```
```c=
#include<stdio.h>
int main(void){
FILE *ptr=fopen("file.txt","r");
char c;
while((c=fgetc(ptr))!=EOF){
printf("%c",c);
}
}
```
輸出:
```
1234
555
000
```
#### fputc():
```c=
fputc('要放進檔案的字元',"檔案指標")
//如果打fputc('a',stdout)會等同於putchar('a')
```
有點像putchar,一次寫入一個字元進檔案
```c=
#include<stdio.h>
#include<string.h>
int main(void){
FILE *ptr=fopen("file.txt","w");
char str[10]="1234567";
int i=strlen(str);
while(i--){
fputc(str[i],ptr);
}
}
```
file.txt裡面:
```
7654321
```
#### fgets()&fputs():
```c=
fgets('要儲存檔案內容的字串','讀入的字串長度',"檔案指標")
//fgets(str,10,stdin);
//不管設定的毒入長度多長讀到換行或EOF都會停止
fputs('放進檔案的字串',"檔案指標")
//fputs(str,fptr);
```
也跟一般使用的$gets()$跟$puts()$差不多都是讀入跟寫入一行字
>fgets()會讀進換行字元,fputs()如果輸出的字串沒有換行的話不會自動換行
>
>這兩個好像不能同時用在同一個檔案(?)我自己試失敗了,有人成功可以跟我說一下
>->應該是11.4提到的覆蓋資料問題
## 11.3 Creating a Sequential-Access File
```c=
// Fig. 11.2: fig11_02.c
// Creating a sequential file
#include <stdio.h>
int main(void)
{
FILE *cfPtr;// cfPtr = clients.txt file pointer
// fopen opens file. Exit program if unable to create file
if ((cfPtr = fopen("clients.txt","w")) == NULL) {
puts("File could not be opened");
}
else {
puts("Enter the account, name, and balance.");
puts("Enter EOF to end input.");
printf("%s", "? ");
unsigned int account; // account number
char name[30]; // account name
double balance; // account balance
scanf("%d%29s%lf", &account, name, &balance);
// write account, name and balance into file with fprintf
while (!feof(stdin)) {
fprintf(cfPtr,"%d %s %.2f\n", account, name, balence);
printf("%s", "? ");
scanf("%d%29s%lf", &account, name, &balance);
}
fclose(cfPtr);// fclose closes file
}
}
```
### 11.3.1 Pointer to a FILE
第7行的cfPtr表示指向FILE結構的指標。
### 11.3.2 Using fopen to Open the File
每個開啟的檔案必須要有個別的FILE型別的指標。第10行說明程式要使用"client.txt"這個檔案。$fopen()$的使用方式:
```c=
fopen("檔案名稱","檔案開啟模式")//檔案開啟模式在11.3.6
```
>如果沒有指定檔案路徑會直接程式所在的目錄尋找
>如果用"w"來開啟某個已經存在的檔案,但使用者實際上希望保存這個檔案。將會在沒有警告的情況下刪除這個檔案[name=小蜜蜂]
>在程式參考某個檔案之前,忘記先開啟這個檔案會發生邏輯錯誤[name=小蜜蜂]
### 11.3.3 Using feof to Check for the End-of-File Indicator
第22行的$feof()$是用來判斷**stdin**的檔案是否在檔案的結尾設定結尾信號。結尾信號會通知程式已經沒有資料要進行處理。$feof()$在遇到結尾信號$EOF$的時候會**傳回true(1),否則傳回flase(0)。**
```c=
feof("檔案指標");
```
|作業系統| EOF按鍵|
|:-|:-|
|Windows|Ctrl+z|
|Linux/Mac OS X/UNIX|Ctrl+d|
### 11.3.4 Using fprintf to Write to the File
第26行使用$fprintf()$用來將測資傳送到檔案中
```c=
fprintf("要寫進的檔案指標","輸出格式","要輸出的資料");
//fprintf(stdout,"%d",10)跟printf("%d",10)的意義是一樣的
```
### 11.3.5 Using fclose to Close the File
第31行中,當輸入檔案結尾之後(feof),程式會使用 $fclose()$來關閉檔案clients.txt,若沒有呼叫函數$fclose()$ 當程式停止執行後,作業系統通常會自己關閉檔案
```c=
fclose("檔案指標");
```
> 關閉一個檔案會釋放資源給其他正在等待這項資源的使用者或程式,因此應該確定檔案再也不使用就要關閉檔案,而非等到作業系統關閉[name=小蜜蜂]
### FILE指標&FILE結構&FCB之間的關係
當開啟一個檔案時,檔案的FCB會複製到記憶體中,程式中所使用的檔案必須具有唯一的名稱

上面的圖在說檔案指標跟FCB的連結關係,翻譯如下:
* cfPtr=fopen("clients.dat","w"); →fopen回傳指向FILE結構的指標
* "clients.dat"的FILE結構包含一個描述子,也就是一個小的整數,代表指向開啟檔案表的索引
* 當程式發出I/O呼叫,像fprintf(ctPtr,"%d",account); 的指令,程式會在FILE結構中找到描述子(7),並使用描述子在開啟檔案表中找到FCB
* 此程式會呼叫一個作業系統服務,此服務會使用FCB中的資料來控制所有到磁碟機真正檔案的輸入輸出(使用者不能直接存取FCB)
* 當檔案開啟時,此項目會從磁碟機上FCB被複製出來
### ==11.3.6 File Open Modes==
:::warning
|檔案開啟模式|說明|
|:-|:--|
|r|開啟用來讀取的檔案。(檔案不存在fopen()會回傳NULL)|
|w|建立一個用來寫入的檔案。如果檔案已經存在,就會刪除已經存在的內容。|
|a|附加;開啟或是建立一個用來將資料寫到檔案結尾的檔案。
|r+|開啟一個用來更新資料的檔案(可讀寫)。
|w+|建立一個用來更新資料的檔案。如果檔案已經存在,就會刪除已經存在的內容。
|a+|附加:開啟或是建立一個檔案,將資料寫到檔案結尾。
|rb|以二進位模式開啟一個用來讀取的檔案。
|wb|以二進位模式開啟一個用來寫入的檔案。如果檔案已經存在,程式就會刪除已經存在的內容。
|ab|以二進位模式附加、開啟或是建立一個用來將資料寫到檔案結尾的檔案。
|rb+|以二進位模式開啟一個用來更新資料的檔案(可讀寫)。
|wb+|以二進位模式開啟一個用來更新資料的檔案。如果檔案已經存在,程式就會刪除已經存在的內容。
|ab+|附加:以二進位模式開啟或是建立一個檔案,將資料寫到檔案結尾。
:::
當使用w, w+, wb, wb+模式時,C11提供獨佔寫入模式。如果檔案已經存在或無法建立fopen會失效。如果成功開啟或建立一個檔案時,將只會有你的程式能夠進行檔案存取。如果任何模式開啟檔案時產生錯誤,fopen將會傳回NULL。
>以讀取模式開啟一個不存在的檔案會發生錯誤[name=小蜜蜂]
>開啟一個用來讀取或寫入的檔案,但沒有設定正確的檔案存取權限(跟作業系統有關)會導致錯誤[name=小蜜蜂]
>當沒有足夠的磁碟空間時,開啟檔案會導致執行錯誤[name=小蜜蜂]
>用寫入模式("w")開啟一個必須使用更新模式("r+")開啟的檔案會導致原本檔案中的內容遺失[name=小蜜蜂]
>如果不需要修改檔案內容請以唯讀方式(不會更新檔案)開啟檔案,這可以防止不小心修改到檔案。這是最小權限原則的另一個例子[name=小蜜蜂]
## 11.4 Reading Data from a SequentialAccess File
這章在討論如何循序讀取一個檔案
下面程式碼的重點(?:
* fscanf()相當於scanf(),只是需要再加上一個檔案指標的引數
* 用fopen()的回傳值不等於NULL來判斷是否正確開啟這個檔案
* feof()判斷檔案是否結束


### 11.4.1 Resetting the File Position Pointer
#### rewind():
```c=
rewind("檔案指標")
```
將檔案位置指標 **(file postition pointer)** 重新指回檔案開頭,這樣就可以重複讀取檔案
* 檔案位置指標 **(file postition pointer)**
-- FILE結構中的一員,並不是真的指標而是一個整數值,代表檔案中下一個讀取或寫入的位元組,通常也稱**檔案位移(file offset)**
```c=
#include<stdio.h>
#include<string.h>
int main(void){
FILE *ptr=fopen("file.txt","r");
char str[100];
int i=2;//設定要讀兩次檔案
while(i){
fputs(str,stdout);//將讀到的結果放到標準輸出(=puts(str))
//如果檔案結束
if(fgets(str,100,ptr)==NULL){
fputs("\n",stdout); //排版(#
rewind(ptr); //重置檔案位置指標
fgets(str,100,ptr);//重新讀進一行字,這時候str會等於檔案一開頭的那行字
i--;
}
}
fclose(ptr); //關檔案
}
```
file.txt裡的東西:
```
12345678
6666666
helloworld!
5555555
```
輸出樣子(檔案重複輸出了兩次):
```
12345678
6666666
helloworld!
5555555
12345678
6666666
helloworld!
5555555
```
### 11.4.2 Credit Inquiry Program
下面的信用查詢程式有四種選項:
:::info
* 選項1:產生欠款餘額為零的帳戶清單
* 選項2:產生欠款餘額為負的帳戶清單
* 選項3:產生欠款餘額為正的帳戶清單
* 選項4:結束程式
:::



### Updating a Sequential File(更改循序檔案):
如果想要更改這種 **循序檔案(重複讀取的檔案)** 中的資料,可能會破壞檔案中的其他資料,像是如果<font color="3D88FA">新的紀錄比原本的紀錄長的話可能會導致新的資料覆蓋到下一個循序紀錄的開頭部分</font>。這個問題是因為在 **fprintf()&fscanf()的格式化輸入/輸出模式(formatted input/output model)** 中,欄位大小是可以改變的,像是100、14、2074這些數字都是儲存在相同位元組中的int,但在顯示到螢幕或是寫進檔案中他們的<font color="3D88FA">欄位寬度卻不一樣</font>。
因此,以 **fprintf()跟fscanf()** 來進行循序存取通常不會更改檔案中的紀錄,常見的更改方法就是將整個檔案重新寫過 <font color="9A9A9A"> →複製檔案並將更改的資料寫進新檔案中</font>
* **更改一筆資料則需要處理當案中的每一筆資料**
## 11.5 Random-Access Files
因為$fprintf()$的長度不一定會一樣,所以可以創造一個random-access file(隨機存取檔案)來讓每一個紀錄有固定的長度。通常用於需要直接進行存去的資料(不需要在記憶體中搜尋)
## 11.6 Creating a Random-Access File
### fwrite:
```c=
fwrite("要寫入資料的指標","要寫入的資料大小","寫入的資料個數","檔案指標");
//fwrite(&a,sizeof(int),1,ptr)有點像fprintf(ptr,"%d",a);
```
* $fwrite()$主要是以固定大小來存取資料,所以要修改資料時會較好修改
* 讀入資料大小通常以$sizeof()$來計算
* 檔案處理程式很少只寫一種資料進檔案中,通常會搭配$struct$來使用
* 第三個引數通常填1,若想要寫入一個陣列可以把第三個引數改成要寫入的陣列大小,前面則是一個指向陣列的指標
## 11.7 Writing Data Randomly to a Random-Access File
```c=
// Fig. 11.11: fig11_11.c
// Writing data randomly to a random-access file
#include <stdio.h>
// clientData structure definition
struct clientData{
unsigned int acctNum; // account number
char lastName[15]; // account last name
char firstName[10]; // account first name
double balance; // account balance
}; // end structure clientData
int main(void)
{
FILE *cfPtr; // accounts.dat file pointer
// fopen opens the file; exits if file cannot be opened
if ((cfPtr = fopen("accounts.dat","rb+")) == NULL) {
puts("File could not be opened.");
}
else {
// create clientData with default information
struct clientData client = {0, "", "", 0.0};
// require user to specify account number
printf("%s", "Enter account number"
" (1 to 100, 0 to end input): ");
scanf("%d", &client.acctNum);
// user enters information, which is copied into file
while (client.acctNum != 0) {
// user enters last name, first name and balance
printf("%s", "\nEnter lastname, firstname, balance: ");
// set record lastName, firstName and balance value
fscanf(stdin, "%14s%9s%lf", client.lastName,
client.firstName, &client.balance);
// seek position in file to user-specified record
fseek(cfPtr, (client.acctNum - 1) *
sizeof(struct clientData), SEEK_SET);
// write user-specified information in file
fwrite(&client, sizeof(struct clientData), 1, cfPtr);
// enable user to input another account number
printf("%s", "\nEnter account number: ");
scanf("%d", &client.acctNum);
}
fclose(cfPtr); // fclose closes the file
}
}
```
### 11.7.1 Positioning the File Position Pointer with fseek
40-41行的$fseek$中的$(client.accountNum-1)*sizeof(struct clientData)$所計算出的稱為**offset(偏移量)以及displacement(位移)**。與陣列相同,對第一個而言,會被視為檔案中的位元組0。
### fseek:
```c=
fseek("檔案指標","資料位移量","符號常數(位移開始的位置)");
//fseek(fptr,10*size(int),SEEK_SET)
```
* offset(偏移量)為正往後移,為負的往前移
* **符號常數(定義在$<stdio.h>$中):**
**SEEK_SET: 從檔案開頭開始位移**
**SEEK_CUT: 從檔案目前位置開始位移**
**SEEK_END: 從檔案結尾處開始位移**
### 11.7.2 Error Checking
可以藉由查看fseek、fscanf、fwrite的傳回值來判斷讀取資料時是否有出現錯誤
:::warning
* $fsacnf()$:會回傳成功讀取的資料個數,若讀取錯誤會回傳EOF
* $fseek()$:發生錯誤會回傳非0值
* $fwrite()$:回傳成功輸出的項目數量~(第三個引數)~
:::
## 11.8 Reading Data from a Random-Access File
### fread:
```c=
fread("要讀入資料的指標","讀入的資料大小","讀入的資料個數","檔案指標");
//fread(&a,sizeof(int),1,ptr)有點像fscanf(ptr,"%d",&a);
```
* 可參考$fwrite()$用法
* 會回傳成功讀入的資料數量,若發生錯誤則會傳值小於第三個引數
* 若要把資料讀入陣列可改第三個引數
## 11.9 Case Study: Transaction-Processing Program
```c=
// Fig. 11.15: fig11_15.c
// Transaction-processing program reads a random-access file sequentially,
// updates data already written to the file, creates new data to
// be placed in the file, and deletes data previously stored in the file.
#include <stdio.h>
// clientData structure definition
struct clientData{
unsigned int acctNum; // account number
char lastName[15]; // account last name
char firstName[10]; // account first name
double balance; // account balance
};
// prototypes
unsigned int enterChoice(void);
void textFile(FILE *readPtr);
void updateRecord(FILE *fPtr);
void newRecord(FILE *fPtr);
void deleteRecord(FILE *fPtr);
int main(void)
{
FILE *cfPtr; // accounts.dat file pointer
// fopen opens the file; exits if file cannot be opened
if ( ( cfPtr = fopen("accounts.dat","rb+"))== NULL) {
puts("File could not be opened.");
}
else {
unsigned int choice; // user's choice
// enable user to specify action
while ((choice = enterChoice()) != 5) {
switch (choice) {
// create text file from record file
case 1:
textFile(cfPtr);
break;
// update record
case 2:
updateRecord(cfPtr);
break;
// create record
case 3:
newRecord(cfPtr);
break;
// delete existing record
case 4:
deleteRecord(cfPtr);
break;
// display message if user does not select valid choice
default:
puts("Incorrect choice");
break;
}
}
fclose(cfPtr); // fclose closes the file
}
}
// create formatted text file for printing
void textFile(FILE *readPtr)
{
FILE *writePtr; // accounts.txt file pointer
// fopen opens the file; exits if file cannot be opened
if (( writePtr = fopen("accounts.txt","w")) == NULL) {
puts("File could not be opened.");
}
else {
rewind(readPtr); // sets pointer to beginning of file
fprintf(writePtr, "%-6s%-16s%-11s%10s\n",
"Acct", "Last Name", "First Name","Balance");
// copy all records from random-access file into text file
while (!feof(readPtr)) {
// create clientData with default information
struct clientData client = { 0, "", "", 0.0 };
int result =
fread(&client, sizeof(struct clientData), 1, readPtr);
// write single record to text file
if (result != 0 && client.acctNum != 0) {
fprintf(writePtr,"%-6d%-16s%-11s%10.2f\n"
client.accNum, client.lastName,
client.firstName, client.balance);
}
}
fclose(writePtr); // fclose closes the file
}
}
// update balance in record
void updateRecord(FILE *fPtr)
{
// obtain number of account to update
printf("%s", "Enter account to update (1 - 100): ");
unsigned int account; // account number
scanf("%d", &account);
//move file pointer to correct record in file
fseek(fPtr, (account-1) * sizeof(struct clientData),
SEEK_SET);
// create clientData with no information
struct clientData client = {0, "", "", 0.0};
// read record from file
fread(&client, sizeof(struct clientData),1,fPtr);
// display error if account does not exist
if (client.acctNum == 0) {
printf("Account #%d has no information.\n", account);
}
else { // update record
printf("%-6d%-16s%-11s%10.2f\n\n",
client.acctNum, client.lastName,
client.firstName, client.balance);
// request transaction amount from user
printf("%s", "Enter charge (+) or payment (-): ");
double transaction; // transaction amount
scanf("%lf", &transaction);
client.balance += transaction; // update record balance
printf("%-6d%-16s%-11s%10.2f\n",
client.acctNum, client.lastName,
client.firstName, client.balance);
//move file pointer to correct record in file
fseek(fPtr, (account - 1) * sizeof(struct clientData),
SEEK_SET)
//write updated record over old record in file
fwrite(&client, sizeof(struct clientData), 1, fPtr);
}
}
// delete an existing record
void deleteRecord(FILE *fPtr)
{
// obtain number of account to delete
printf("%s", "Enter account number to delete (1 - 100): ");
unsigned int accountNum; // account number
scanf("%d", &accountNum);
// move file pointer to correct record in file
fseek(fPtr, (accountNum - 1) * sizeof(struct clientData),
SEEK_SET);
struct clientData client; // stores record read from file
// read record from file
fread(&client, sizeof(struct clientData), 1, fPtr);
// display error if record does not exist
if (client.acctNum == 0) {
printf("Account %d does not exist.\n", accountNum);
}
else { // delete record
// move file pointer to correct record in file
fseek(fPtr, (accountNum - 1) * sizeof(struct clientData),
SEEK_SET);
struct clientData blankClient = {0, "", "", 0}; // blank client
// replace existing record with blank record
fwrite(&blankClient,
sizeof(struct clientData), 1, fPtr);
}
}
// create and insert record
void newRecord(FILE *fPtr)
{
// obtain number of account to create
printf("%s", "Enter new account number (1 - 100): ");
unsigned int accountNum; // account number
scanf("%d", &accountNum);
// move file pointer to correct record in file
fseek(fPtr, (accountNum - 1) * sizeof(struct clientData),
SEEK_SET);
// create clientData with default information
struct clientData client = { 0, "", "", 0.0 };
// read record from file
fread(&client, sizeof(struct clientData), 1, fPtr);
// display error if account already exists
if (client.acctNum != 0) {
printf("Account #%d already contains information.\n",
client.acctNum);
}
else { // create record
// user enters last name, first name and balance
printf("%s", "Enter lastname, firstname, balance\n? ");
scanf("%14s%9s%lf", &client.lastName, &client.firstName,
&client.balance);
client.acctNum = accountNum;
//move file pointer to correct record in file
fseek(fPtr, (client.accNum - 1) *
sizeof(struct clientData), 1, fPtr);
//insert record in file
fwrite(&client,
sizeof(struct clientData), 1, fPtr);
}
}
// enable user to input menu choice
unsigned int enterChoice(void)
{
// display available options
printf("%s", "\nEnter your choice\n"
"1 - store a formatted text file of accounts called\n"
" \"accounts.txt\" for printing\n"
"2 - update an account\n"
"3 - add a new account\n"
"4 - delete an account\n"
"5 - end program\n? ");
unsigned int menuChoice; // variable to store user's choice
scanf("%u", &menuChoice); // receive choice from user
return menuChoice;
}
```

* 選項一呼叫函式textFile,將所有的帳戶格式化清單存到一個稱為accounts.txt的文字檔當中。
* 選項二呼叫函式updatedRecord,可以將"已存在的帳戶"進行更改,再輸入帳號號碼的時候,城市會先使用fread來確定是否有此筆資料,如果此資料為0的話,就會跳出"此紀錄為空",如果有帳戶資料的話就會跳出欲修改的金額。
* 選項三呼叫函式newRecord,可以增加一個新的帳戶到這個檔案之中,若已有檔案的話,程式會提示已有資料。
* 選項四呼叫函式deletedRecord,可以在檔案當中刪除一筆資料,若本來沒有此資料,程式會提示沒有此筆資料。
* 選項五代表結束此程式
通常這種需要進行修改以及讀取的資料會使用"rb+"來開啟檔案
## 11.10 Secure C Programmin
### fprintf_s&fscanf_s:
除了指定FILE指標參數來進行處理,其他與printf_s&scanf_s並無不同,若c編譯器標準函式庫包含這些函式則應該使用這兩個函式更為安全
### CERT安全C撰寫標準的第9章:
第9章主要探討輸出入和規則,關於檔案處理的內容都在這章介紹
* FIO03-C:
當以非獨占檔案開啟模式來打開寫入檔案(沒有w的),若有此檔案,函式$fopen()$開啟檔案並擷取其內容,在$fopen()$呼叫前沒有跡象顯示檔案存在。要確認現有的檔案不適被開啟及擷取,可以使用C11新的獨佔模式(章節11.3),在檔案不存在時用$fopen()$開啟。
* FIO04-C:
一個好的程式碼必須確認檔案處裡函式的回傳值是否傳回錯誤代碼,這確認了函示是否被正確執行。
* FIO07-C:
函式$rewind()$不會回傳任何的值,因此無法確認運行是否正常,建議==使用$fseek()$替代==,因為發生錯誤後$fseek()$可回傳非0的值。
* FIO09-C:
本章介紹到文字檔案及二進制檔案,由於不同平台二進制資料的表示差異,以二進制寫入的檔案通常不可攜,若要更可攜的檔案描述,參考使用文字檔案和函式庫,可處理跨平台在二進制檔案表示上的差異。
* FIO14-C:
某些函式庫在文字檔案和二進制檔案的運作並非相同,像是在==函式$fseek()$中使用SEEK_END來位移的話不保證運行正確,建議使用SEEK_SET==。
* FIO42-C:
在許多平台一次只能開啟有限個檔案,所以在檔案不在被程式使用時就應該將其關閉。
簡單講就是用不到^^
###### tags: `程設好難` `學校門口大樹石頭下` `他的課就很好睡阿`
<style>
.navbar-brand::after { content: " × 老葉的程式設計"; }
</style>