顯示具有 C 語言 標籤的文章。 顯示所有文章
顯示具有 C 語言 標籤的文章。 顯示所有文章

2025年3月23日 星期日

參加小霸王樹莓派 Pico 線上課程 (免費)

前幾天在 FB 看到小霸王要在今天舉辦樹莓派 Pico 線上免費課程, 剛好今天不用去市圖還書, 冰箱食材也很充足不必上市場, 所以一整天就用來學習怎樣用 Arduino C 語言控制 RPi Pico 吧!

"rpi pico 樹莓派課程連結
免費課程,歡迎參加
請事先安裝Arduino IDE 2.x版喲
時間:2025/03/23 0900~1600
講義下載 : https://twgo.io/picow

PS : 有錄影, 沒聽到的話可在小霸王頻道回看. 

2023年11月23日 星期四

好站 : tutorialspoint.com 線上編譯器/編輯器

今天上完 Hahow 學校的 C 語言入門 (當作是複習), 課程中老師使用 tutorialspoint.com 的 C 語言線上編譯器, 其介面簡潔易用, 適合教學與快速測試 :





按 Execute 鈕會編譯並執行 C 程式碼, 結果會顯示在右邊的 Terminal 視窗; 按 Beautify 鈕會將 Source Code 內的程式碼整理成易讀格式; 按 Share 鈕會產生此程式原始碼的可分享超連結 :

 


tutorialspoint.com 也是是知名的線上教學文件網站, 除了 C 語言以外, 還提供其他語言的線上編譯器/編輯器, 例如我目前主要使用的 Python 與 Javascript 語言 :





已安裝大部分 Python 資料科學與爬蟲所需套件 (但 Sympy 沒有) :




HTML 編輯器網址如下, 可用 CDN 方式匯入 Javascript 與 CSS 樣式 : 




2023年6月12日 星期一

C/C++ 學習筆記 : 檔案處理

檔案以儲存形式來分有純文字檔 (text file) 與二進位檔 (binary file) 兩種, 文字檔以字元編碼 (ASCII 或 Unicode) 方式儲存資料, 可直接在螢幕輸出閱讀 (但比較占空間), 例如副檔名為 .txt, .log, .json, 或 .csv 者皆為文字檔, 也是程式專案中最常使用的檔案格式. 

而二進位檔是將記憶體中的資料以原始未經過編碼的 byte 形式原封不動儲存至檔案中, 需透過特定軟體解碼才能使用或閱讀, 例如編譯過的程式檔, 圖片, 影像, 以及音樂等均以二進位檔方式儲存 (因為這樣比較節省空間), 副檔名為 .dat, ,pdf, 或 .exe 等皆為二進位檔.  

本篇測試使用免費的線上開發平台 replit.com 執行程式碼, 此平台提供 Python, C, Java, C# 等程式語言的開發環境, 註冊使用者可將程式碼儲存在平台上或透過連結分享給別人, 參考 :



一. C 的檔案處理 : 

C 語言是以資料串流 (IO Stream) 方式來處理記憶體與周邊儲存裝置 (例如磁碟) 的資料傳輸, 但基於存取的效率考量, 檔案處理時程式並非直接對磁碟進行存取, 而是透過在記憶體中劃出的一塊緩衝區 (buffer) 當作串流的中介, 程式存取的是緩衝區存放的資料, 因此將資料寫入檔案時並非直接寫入磁碟, 而是先寫入緩衝區, 等關閉檔案時才真正寫入磁碟. C 語言標準函式庫中的 stdio 函式庫負責檔案的開啟, 讀寫, 以及關閉, 也會自動處理緩衝區的配置, 因此若程式要用到檔案輸出入時, 必須用 include 匯入 stdio.h 標頭檔.   


1. 開啟與關閉文字檔 :    

C 語言以文字串流 (text stream) 方式, 如同水管中的水流般一個字元一個字元地循序處理. C 語言的標準函式庫 stdio 提供檔案開啟, 讀寫, 與關閉等操作, 並會自動處理緩衝區. 使用標準函式庫處理檔案時必須先匯入 stdio.h 標頭檔 :

#include <stdio.h>

使用此函式庫開啟檔案時會傳回一個 FILE 型態的指標, 它會指向所開啟檔案的緩衝區, 因此處理檔案時要先宣告一個 FILE 型態的指標 :

FILE *fp;   

stdio 的檔案開啟與關閉函式 API 如下 :


 stdio 函式 說明
 FILE *fopen(char *filename, char *mode) 以指定模式開啟檔案, 成功傳回檔案指標, 失敗傳回 NULL.
 int fclose(FILE *fp) 傳入檔案指標關閉該檔案, 成功傳回 0, 失敗傳回 EOF


檔案操作結束務必呼叫 fclose() 並傳入檔案指標關閉檔案, 此函式會先將緩衝區內的資料寫回檔案後緩衝區記憶體. 

fclose(fp);     

fopen() 第二個參數為開啟模式字串, 可用的模式如下表 : 


 模式字串 說明
 r 以唯讀模式開啟已存在之文字檔, 成功傳回檔案指標, 失敗 (檔案不存在) 傳回 NULL
 w 以覆蓋寫入模式開啟文字檔, 若檔案存在內容會先被清空, 若檔案不存在就建立檔案
 a 以附加寫入模式開啟文字檔 (新增的資料附加在原資料後面), 若檔案不存在就建立檔案
 r+ 以讀寫模式開啟已存在之文字檔, 成功傳回檔案指標, 失敗 (檔案不存在) 傳回 NULL
 w+ 以讀寫模式開啟文字檔, 若檔案存在內容會先被清空, 若檔案不存在就建立檔案
 a+ 以附加讀寫模式開啟文字檔 (新增的資料附加在原資料後面), 若檔案不存在就建立檔案
 rb+ 以讀寫模式開啟已存在二進檔, 成功傳回檔案指標, 失敗 (檔案不存在) 傳回 NULL
 wb+ 以讀寫模式開啟二進檔, 若檔案存在內容會先被清空, 若檔案不存在就建立檔案
 ab+ 以附加讀寫模式開啟二進檔 (新增的資料附加在原資料後面), 若檔案不存在就建立檔案


文字檔案的基本操作有讀取 (read only), 寫入 (write), 以及檔尾附加寫入 (append) 三種模式, 模式字串後面添加一個 "+" 表示可以更新檔案內容, 例如 r+ 與 w+ 都表示可讀寫檔案內容; 但 w+ 會先清除原來的內容, 而 r+ 則不會. 

fopen() 函式的第一參數為檔案名稱字串, 可以包含路徑, 在 Windows 系統下路徑之倒斜線必須用 \ 跳脫, 以下是絕對路徑的用法 : 

FILE *fp;                                           // 宣告指向檔案緩衝區的指標
fp=fopen("test.txt");                          // 開啟工作目錄下的 text.txt 檔 (目前目錄=C:\myfolder\)
fp=fopen("C:\\myfolder\\test.txt");   // 開啟指定目錄下的 text.txt 檔 
fp=fopen("C:/myfolder/test.txt");     // 開啟指定目錄下的 text.txt 檔

也可以使用相對路徑 :

fp=fopen("./myfolder/test.txt");     //目前目錄的子目錄 myfolder 下的 test.txt
fp=fopen("../myfolder/test.txt");    //目前目錄的上一層目錄 myfolder 下的 test.txt

fopen() 開啟檔案成功會傳回 FILE 型態的指標 (指向緩衝器位址), 開啟失敗則傳回 NULL, 個可利用傳回值判斷是否開檔成功, 例如 : 

if (fp==NULL) {
    printf("檔案開啟失敗\n");
    }
else {
    printf("檔案開啟成功\n");
    }

完整範例如下 :

/* 開啟和關閉文字檔案 test01.c */
#include <stdio.h>

int main() {
  FILE *fp;
  fp=fopen("helloworld.txt", "r");
  if (fp==NULL) {printf("檔案開啟失敗\n");}
  else {printf("檔案開啟成功\n");}
  fclose(fp);
  }

先在工作目錄下製作一個 helloworld.txt 檔案 (裡面有無內容均可), 執行程式結果會顯示檔案開啟成功; 然後用 rm 指令刪除 helloworld.txt 再次執行程式則顯示島案開啟失敗. 以下是在 replit.com 底下用 a.replit 執行的結果 : 




在 replit.com 執行 C 程式的方法參考下面這篇 :



2. 讀寫文字檔 :   

stdio 函式庫內建 fgets() 與 fputs() 函式來讀寫文字檔 : 


 讀寫文字檔的函式 說明
 char *fgets(char *str, int n, FILE *fp) 從指標 fp 讀取 n-1 個字元, 成功傳回 str 指標, 至檔尾傳回 NULL
 int fputs(char *str, FILE *fp) 將指標 str 字串寫入檔案 fp, 成功傳回非負整數, 失敗傳回 EOF


首先來測試用 fputs() 將字串寫入文字檔, 寫入文字檔可用 "w" 或 "a" 模式開檔, 用 "w" 模式開啟時若此檔案已存在, 則開啟成功後原本的內容會被清空; 若檔案不存在就建立一個空檔案, 總之用 "w" 模式建立的檔案, 其內容都是我們用 fputs() 等函式寫入的; 而 "a" 模式則是會保留原檔案內容 (如果檔案存在的話), 用 puts() 寫入的內容會附加在檔尾, 例如 :

/* 寫入文字檔 test02.c */
#include <stdio.h>

int main() {
  FILE *fp;
  char *str1="Hello\n";       //用指標儲存字串
  char str2[10]="World\n";    //用陣列儲存字串
  fp=fopen("helloworld.txt", "w");  //以 w 模式開啟文字檔
  fputs(str1, fp);   //將字串寫入文字檔
  fputs(str2, fp);   //將字串寫入文字檔
  printf("文字檔寫入完成\n");
  fclose(fp);
  }

執行結果如下 : 




可見執行寫入後, helloworld.txt 內容為兩列的 Hello 與 Wordl. 如果將上面程式中的兩個 fputs() 註解掉重新執行程式, 則 helloworld.txt 檔案內容將變成空白 (開檔後沒有寫入). 

接下來可以用 fgets() 函式來讀取上面範例程式產生的 helloworld.txt 內容, 用 fgets() 讀檔首先必須宣告一個字元陣列來儲存讀到的內容, 其長度與 fgets() 的第二參數相同 :

char str[50];    //長度與 fgets() 的第二參數相同

然後使用 while 迴圈從緩衝區中固定讀取若干字元存到字元指標所指之位址, 直到 fgets() 傳回 NULL 表示已讀到檔尾結束迴圈, 語法如下 :

while (fgets(str, 50, fp) != NULL) {  //每次讀取 50-1=49 個字元 (扣掉字串尾 \0)
    .......
    }

完整範例如下 : 

/* 讀取文字檔 test03.c */
#include <stdio.h>

int main() {
  FILE *fp;
  char str[50];  //讀取字串的暫存區, 長度與 fgets() 第二參數相同即可
  int count=0;   //統計讀了幾列
  fp=fopen("helloworld.txt", "r");
  if (fp != NULL) {
    printf("文字檔內容 :\n");
    while (fgets(str, 50, fp) != NULL) {
      printf("%s", str);  //印出讀取到的字串 (列)
      count++;
      }
    printf("讀取了 %d 列\n", count);
    fclose(fp);
    }
  else {printf("檔案開啟失敗\n");}
  }

執行結果如下 :




下列範例使用 fgets() 與 fputs() 來複製檔案 : 

/* 複製文字檔 test04.c */
#include <stdio.h>

int main() {
  FILE *sfp, *dfp;  //宣告來源與目的檔案指標
  char str[20];   //暫存 fgets() 從緩衝區讀取之字串
  dfp=fopen("helloworld2.txt", "w");  //用 w 模式開啟目的檔
  sfp=fopen("helloworld.txt", "r");   //用 r 模式開啟來源檔
  if (sfp != NULL) {
    printf("文字檔內容 :\n");
    while (fgets(str, 20, sfp) != NULL) {
      printf("%s\n", str);  //印出讀取到的字串 (列)
      fputs(str, dfp);  //將讀取之資料寫入目的檔緩衝區
      }
    fclose(sfp);   //關閉目的檔
    fclose(dfp);   //關閉目的檔
    }
  else {printf("檔案開啟失敗\n");}
  }

此例以上面的 helloworld.txt 為來源檔, 用 r 模式開啟檔案後傳回檔案指標 sfp; 以尚未建立的 helloworld2.txt 為目的檔, 用 w 模式開啟檔案後傳回檔案指標 dfp, 然後用 while 迴圈讀取來源檔 sfp 並將讀取之資料寫入目的檔直到檔尾 EOF 出現為止, 執行結果如下 :




可見原本不存在的 helloworld2.txt 在執行完程式後被建立了, 且內容與來源檔案 helloworld.txt 完全相同, 亦即完成了檔案複製動作. 


3. 格式化讀寫文字檔 :   

上面的範例中寫入檔案的均為字串, 而 fputs() 是專門用來將字串寫入檔案中的函式 (因為其第一參數為 char 類型), 如果要將整數用 fputs() 寫入檔案, 必須先用 stdio 函式庫的 sprintf() 函式將整數轉成字串, 其呼叫方式如下 : 

sprintf(字串變數, "%d", 整數變數);         //將整數變數轉為字串變數

如果要將浮點數轉成字串, 則可用 stdlib 函式庫的 gcvt() 函式, 呼叫方法如下 : 

char *字串變數=gcvt(浮點數變數, 總位數, 暫存字串變數);    //將浮點數變數轉為字串變數

注意, gcvt() 標頭檔定義於 stdlib, 須用 include 匯入 :

#include <stdlib.h>

例如 :

/* 用 sprintf() 死 gcvt() 將數值轉成字串 test05.c */
#include <stdio.h>
#include <stdlib.h>

int main() {
  FILE *fp;
  fp=fopen("out.txt", "w");
  int i_price=123;       //整數
  float f_price=123.45;  //浮點數
  char i_price_str[10];  //用來儲存轉成字串的整數
  char c[10];            //gcvt() 用來暫存字串的字元陣列
  sprintf(i_price_str, "%d", i_price);    //將整數轉成字串
  char *f_price_str=gcvt(f_price, 5, c);  //將浮點數轉成字串
  fputs(i_price_str, fp); //將整數字串存入檔案
  fputs("\n", fp);        //存入換行字元
  fputs(f_price_str, fp); //將浮點數字串存入檔案
  fclose(fp);
  }

執行結果如下 : 




讀進儲存在檔案中的數值字串時也必須透過 strtoi(), atoi(), atof() 等函式轉成數值. 

參考 : 


另一個較簡單的方法是使用 fprintf() 與 fscanf() 函式, 如同 printf() 與 scanf() 為對標準 IO (螢幕與鍵盤) 進行格式化輸出入, C 語言的 stdio 也有對於檔案 IO 的格式化輸出入函式 fprintf() 與 fscanf(), 不論是獨或寫, 可以透過格式化字元自動轉換類型 :


 格式化讀寫文字檔函式 欄位2
 int fprintf(fp, 格式化字串, 變數群)  依格式化字串輸出到檔案, 成功傳回輸出字元, 失敗傳回 EOF
 int fscanf(fp, 格式化字串, 變數群)  依格式化字串從檔案讀取, 成功傳回讀取字元, 失敗傳回 EOF


格式化字串用法與 printf() 及 scanf() 相同, 常用的是 %c (字元), %s (字串), %d (整數), %f (浮點數) 等. 完整範例如下 : 

/* 格式化讀寫文字檔 test06.c */
#include <stdio.h>

int main() {
  FILE *fp;
  char str[20];
  char name[20]="USB 讀卡機";  
  int count=10;  
  float price=21.5;
  fp=fopen("product.txt", "w");
  fprintf(fp, "品名: %s\n", name);
  fprintf(fp, "數量: %d\n", count);
  fprintf(fp, "價格: %f\n", price);
  fclose(fp);
  fp=fopen("product.txt", "r");
  if (fp != NULL) {
    printf("文字檔內容 :\n");
    while (fscanf(fp, "%s", str) != EOF) {
      printf("%s\n", str);  //印出讀取到的字串 (列)
      }
    fclose(fp);
    }
  else {printf("檔案開啟失敗\n");}
  }

執行結果如下 : 




可見不論是字串或數值的檔案讀寫, 用 fprintf() 與 fscanf() 的格式化字串就能搞定, 不需要用其他各種函式轉來轉去. 

2023年6月8日 星期四

C 語言 CH12 檔案處理範例/練習/作業


範例 : 
練習題 : 
  1. 請建立C程式使用scanf()函數輸入檔案名稱後,讀取文字檔案內容來計算總共有幾行,程式在讀完後可以顯示檔案的總行數。
    https://replit.com/@tony1966/c-test#ch12-file-hadling/practice01.c
  2. 請建立C程式使用scanf()函數輸入程式檔名後,讀取程式碼檔案內容後,在每一行程式碼前加上行號(如同本書內容顯示的範例原始程式碼),可以輸出成名為output.txt的文字檔案。
    https://replit.com/@tony1966/c-test#ch12-file-hadling/practice02.c
作業 : 
  1. 設計一個程式讀取一個文字檔, 然後將其中的 "t" 字元改成 "*" 字元後儲存到另一個文字檔. 

2023年6月2日 星期五

使用 py2c 出現 Python.h : 沒有此一檔案或目錄的問題

最近在臉書看到有人提到 py2c 這個程式 (是一個 bash script), 它可以將 Python 程式翻成 C++ 程式, 然後就可以用 C++ 編譯器編譯成可執行檔, 其大小比用 pyinstaller 打包還要小得多, 且可以增加逆向工程還原的難度. 我根據下面這篇文章在 Mapleboard 的 Ubuntu Linux 作業系統下測試時卻出現 "Python.h : 沒有此一檔案或目錄的問題" 錯誤, 尚不知原因, 先把過程記下來. 



1. 安裝 Cython3 :  

one@LX2438:~$ sudo apt install cython3    
正在讀取套件清單... 完成
正在重建相依關係... 完成  
正在讀取狀態資料... 完成  
建議套件:
  cython-doc
下列【新】套件將會被安裝:
  cython3
升級 0 個,新安裝 1 個,移除 0 個,有 152 個未被升級。
需要下載 1,306 kB 的套件檔。
此操作完成之後,會多佔用 6,393 kB 的磁碟空間。
下載:1 http://ports.ubuntu.com jammy/universe arm64 cython3 arm64 0.29.28-1ubuntu3 [1,306 kB]
取得 1,306 kB 用了 3s (492 kB/s)   
選取了原先未選的套件 cython3。
(讀取資料庫 ... 目前共安裝了 288142 個檔案和目錄。)
正在準備解包 .../cython3_0.29.28-1ubuntu3_arm64.deb……
解開 cython3 (0.29.28-1ubuntu3) 中...
設定 cython3 (0.29.28-1ubuntu3) ...


2. 下載 py2c :  

到 GitHub 下載 py2c 這個 bash script :


此程式內容如下 : 

#!/bin/bash

if [ -z $1 ];then
printf "Usage: \n\t$0 [python source file][swtich] 1: To C code 2: to bytecode 3: to C and bytecode\n"
exit 0
fi

source_file=$1
tag=$2

fn_ext=$(echo ${source_file} | awk -F '.' '{print $2}'| tr -d '\r\n')
if [ -z ${fn_ext} ]; then
        fn=${source_file}
elif [ ${fn_ext} == "py" ]; then
fn=$(echo ${source_file} | awk -F '.' '{print $1}')
fi

if [ ${tag} -eq 1 ]; then
printf "Transfer python source to C.\n"
cython3 --embed -o ${fn}.c ${source_file}
elif [ ${tag} -eq 2 ]; then
printf "Transfer python source to bytecode.\n"
gcc -Os -I /usr/include/python3.6m -o ${fn} ${fn}.c -lpython3.6m -lpthread -lm -lutil -ldl
elif [ ${tag} -eq 3 ]; then
printf "Transfer python source to C sode and bytecode\n"
cython3 --embed -o ${fn}.c ${source_file}
gcc -Os -I /usr/include/python3.6m -o ${fn} ${fn}.c -lpython3.6m -lpthread -lm -lutil -ldl
fi
printf "Transfer End\n"

將此程式貼到 nano 存成 py2c 檔 : 

one@LX2438:~$ nano py2c    

用 chmod 指令將其權限改為可執行檔後複製到 /usr/bin/local 下 :

one@LX2438:~$ chmod 777 py2c   
one@LX2438:~$ cp py2c /usr/local/bin      

輸入 py2c 會顯示程式用法 :

one@LX2438:~$ py2c   
Usage: 
/usr/local/bin/py2c [python source file][swtich] 1: To C code 2: to bytecode 3: to C and bytecode

亦即指令的格式為 :

py2c 原始程式.py 選項  

選項的值有 1, 2, 3 三個, 1 是將 Python 程式轉成 C 程式; 2 是轉成可執行的 byte code; 3 是轉成 C 程式與可執行的 byte code.

所以我寫了一個 Python 測試程式 helloworld.py :

#helloworld.py
print('Hello World!)

但執行 py2c 卻出現錯誤 : 

one@LX2438:~$ py2c helloworld.py 3    
Transfer python source to C sode and bytecode
/home/one/.local/lib/python3.10/site-packages/Cython/Compiler/Main.py:369: FutureWarning: Cython directive 'language_level' not set, using 2 for now (Py2). This will change in a later release! File: /home/one/helloworld.py
  tree = Parsing.p_module(s, pxd, full_module_name)
helloworld.c:6:10: fatal error: Python.h: 沒有此一檔案或目錄   
    6 | #include "Python.h"
      |          ^~~~~~~~~~
compilation terminated.
Transfer End

我找到下面這篇文章 :


說是要安裝 python3-dev 才行, 但我安裝後還是一樣 :

one@LX2438:~$ sudo apt-get install python3-dev   
正在讀取套件清單... 完成
正在重建相依關係... 完成  
正在讀取狀態資料... 完成  
python3-dev 已是最新版本 (3.10.6-1~22.04)。
python3-dev 被設定為手動安裝。

目前暫時無解先擱著, 有空再研究. 



2023-06-04 補充 :

我今天用 ChatEverywhere 查詢 AI 對此問題的解法 : 

Q : 我使用 PY2C 時出現 "Python.h : 沒有此一檔案或目錄的問題" 錯誤, 這要如何解決?
A : 您好,這個錯誤通常是因為編譯器無法找到 Python.h 標頭文件所致。
為了解決這個問題,您可以嘗試以下幾個步驟:
確保您已經安裝了 Python 開發套件。在 Linux 上,您可以使用以下命令安裝:

sudo apt-get install python-dev

確保您的編譯器可以找到 Python.h 標頭文件。您可以在編譯器命令中添加以下選項:

-I/usr/include/python2.7/

請注意,上面的路徑可能因您的系統而異,請確保路徑正確。

Q : 請幫我解釋一下這個 bash shell 程式
A : 這個程式是一個 shell 腳本,用於將 Python 程式轉換成 C 語言代碼或字節碼。該腳本接受兩個參數:Python 源代碼文件的路徑和轉換類型。轉換類型可以是以下三種之一:

1. 將 Python 源代碼轉換為 C 語言代碼。
2. 將 Python 源代碼轉換為字節碼。
3. 同時將 Python 源代碼轉換為 C 語言代碼和字節碼。

該腳本使用了以下命令:

1. `cython3`:用於將 Python 代碼轉換為 C 語言代碼。
2. `gcc`:用於編譯 C 語言代碼並生成字節碼。

該腳本還使用了以下選項:

1. `--embed`:用於將 Python 解釋器嵌入到生成的 C 語言代碼中。
2. `-I`:用於指定 Python 開發套件的頭文件路徑。
3. `-o`:用於指定生成的 C 語言代碼或字節碼的輸出文件名。
4. `-l`:用於指定需要連接的庫文件。

該腳本的具體功能如下:

1. 檢查是否提供了 Python 源代碼文件的路徑,如果沒有,則輸出用法信息並退出。
2. 從提供的 Python 源代碼文件的路徑中獲取文件名和擴展名。
3. 如果文件擴展名為 `.py`,則使用文件名作為 C 語言代碼或字節碼的輸出文件名。
4. 根據提供的轉換類型,執行相應的轉換操作。
5. 輸出轉換結束信息。

希望這可以幫助您理解這個程式的功能。如果您有任何問題,請隨時提出。

Q : 編譯器命令中添加的選項怎是 python 2, 我用的是 python 3
A : 對於 Python 3,您可以使用以下命令安裝相應的開發套件:
在 Debian/Ubuntu 系統上:

sudo apt-get install python3-dev

在編譯器命令中,您需要將路徑 /usr/include/python3.6m 中的版本號替換為您系統上安裝的 Python 版本號。例如,如果您的系統上安裝的是 Python 3.8,則路徑應為 /usr/include/python3.8。

2023年5月31日 星期三

C/C++ 學習筆記 : 基本語法

今天繼續整理 C& C++ 的基本語法筆記. 本系列之前的文章參考 :


C++ 是 C 語言的延伸, 基本語法幾乎完全一樣, 可參考以前學 Arduino 時整理的筆記 :


本篇參考書籍如下 : 
  1. 快速學會 C++ 語言 (洪志維, 易習 2014)
  2. 第一次學 C++ 就上手第二版 (李啟龍, 碁峰 2017) 
  3. C++ 教學手冊第三版 (旗標 2012)


一. 程式基本架構 :

C 程式的基本架構從上至下依序分為下面四個區塊 : 
  1. 前置指令 : 主要是匯入函式庫標頭檔與定義常數等.
  2. 自訂函式之原型宣告 : 定義於主函式 main() 後面的其他函式都必須先做原型宣告. 
  3. main() 主函式 : 一個程式中只能有一個名稱為 main() 的主函式, 為程式執行入口. 
  4. 自訂函式 : 於 main() 中會呼叫的自訂函式.
C & C++ 程式的每個敘述結尾都必須以分號結束, 但下面兩個結尾不可用分號 :
  • 前置指令如 #include 或 #define 等.
  • 函式的結尾大括號.
其次, 如果函式定義於 main() 前面則不需要原型宣告. 

C 程式的基本範例如下 :

#include <stdio.h>
#include <stdlib.h>

int main() {
   printf("Hello, world!");    //在螢幕顯示字串
   system("PAUSE");           //暫停程式執行, 按 ENTER 繼續
   return 0;                            //傳回 0 表示程式正常結束
   }

C++ 程式的基本架構從上至下依序分為下面五個區塊 : 
  1. 前置指令 : 主要是匯入函式庫標頭檔與定義常數等.
  2. 自訂函式之原型宣告 : 定義於主函式 main() 後面的其他函式都必須先做原型宣告. 
  3. 命名空間宣告 : 可簡化函式的調用方式, 例如宣告標準函式庫 using namespace std. 
  4. main() 主函式 : 一個程式中只能有一個名稱為 main() 的主函式, 為程式執行入口. 
  5. 自訂函式 : 於 main() 中會呼叫的函式.
C++ 主要是多了一個可有可無的命名空間宣告, 基本範例如下 :

#include <iostream>
#include <cstdlib>

using namespace std;    //宣告命名空間 std (標準函式庫)
int main() {
  cout << "Hello, world!" << endl;    //在螢幕顯示字串
  return 0;                                           //傳回 0 表示程式正常結束
  }

由於 C++ 的標準輸出入函式定義在命名空間 std 下的 iostream 函式庫裡, 如果沒有用 using namspace 宣告命名空間, 則標準輸出必須把命名空間寫出來, 例如 std::cout 與 std::end1.


二. 標頭檔 :

C 與 C++ 遵循模組化設計精神, 提供了許多標準函式庫, C 語言函式庫的函式原型宣告都放在副檔名為 .h 的標頭檔 (header file), 只要在程式最前面用前置指令 #include 匯入這些標頭檔, 即可於程式中呼叫這些標準函式庫中的函式. 匯入函式庫標頭檔的語法如下 :

#include <標頭檔名稱.h>   

注意, 前置指令結尾不可加分號

C 語言最常用的標準函式庫標頭檔為 stdio.h 與 stdlib.h, 例如標準輸出入常用的 printf() 與 scanf() 函式的原型宣告放在 stdio.h 標頭檔內; 而呼叫作業系統的函式 system() 原型宣告與記憶體配置函式等則是放在 stdlib.h 標頭檔內,  故 C 程式的開頭通常都需要匯入這兩個標頭檔 :

#include <stdio.h>
#include <stdio.h>

常用的 C 語言標頭檔如下表 :

 常用的 C 語言標頭檔 說明
 stdio.h 標準輸出入 (螢幕與鍵盤) 函式原型宣告, 例如 printf(), scanf()
 stdlib.h 各種基本函式的標準函式庫, 例如 system()
 string.h 字串處理函式的原型宣告, 例如 strcpy()
 time.h 日期時間函式的原型宣告, 
 math.h 數學運算函式的原型宣告, 例如 sqrt()

C++ 最常用的標準函式庫標頭檔為 iostream 與 cstdlib (注意, 沒有 .h), 它們分別包含了 C 語言的 stdio.h 與 stdlib.h 標頭檔, 並且新增到 std 命名空間中 (因此不需要 .h). 除了這兩個函式庫在 C++ 中用法不同外, 其他函式庫例如 string, time 與 math 在 C++ 中用法與 C 程式完全相同.  


三. 註解 :

C 語言的註解必須放在 /* 與 */ 之間, 它原本是只能單行註解, 但 C++ 將其擴充為可單行也可以多行, 例如 :

/*  printf("這是單行註解");  */    
/* int i;
    for(i=0; i<5; i++) {
       printf("這是多行註解");
       }   */

C++ 另外新稱只能用於單行的 // 的註解方式, 例如 :

// printf("這是單行註解");

如果要用 // 註解一個區塊的程式碼, 則每一行都要用 // 註解掉 : 

// int i;
// for(i=0; i<5; i++) {
     //printf("這是多行註解");
     //}   

由於 C++ 編譯器都向下相容 C 語法, 因此這兩種註解方式都可以同時使用, 例如 : 

// printf("這是單行註解");
/* int i;
    for(i=0; i<5; i++) {
       printf("這是多行註解");
       }   */

亦即單行可使用 // 或 /* */, 而多行則使用 /* */.  但建議盡量都使用 C++ 的 // 來註解, 因為 /* */ 在註解中又有註解時會提前結束註解區域而導致編譯失敗, 例如 : 

/* int i;
    for(i=0; i<5; i++) {    /* 印五次的迴圈 */   
       printf("這是多行註解");
       }   */

上述程式碼在 for 迴圈那一行後面有 /* */ 註解, 由於中間的 */ 會被視為註解結束, 後面的程式碼脫離註解範圍而導致編譯失敗, 如果全部用 // 註解就無此問題, 例如 : 

// int i;
    //for(i=0; i<5; i++) {    // 印五次的迴圈
       //printf("這是多行註解");
       //}   


四. 變數, 常數, 與資料型態 :

變數與常數是程式儲存在記憶體中準備用來運算的資料, 變數的值在程式中可用指派運算子 = 更改, 而常數則不可更改. 


1. 資料型態 :

C & C++ 為強型態 (strong-typed) 的語言, 每一個變數都必須指定資料型別以便編譯器為其配置於記憶體空間. C 語言內建的基本資料型別有 4 種 (C 沒有字串型別, 字串使用以空字元 '\0' 結尾的字元陣列儲存) :
  • 字元 : char (1 byte), 必須用單引號括起來. 
  • 整數 : int (4 bytes)
  • 浮點數 : float (4 bytes) 與 double (8 bytes)
C++ 又新增了兩種基本型態 : 
  • 布林 : bool (1 byte), 只有 true(=1) 與 false(=0) 兩個值.
  • 無傳回值 : void, 用於宣告函式無傳回值. 
整數 int 前面可以用下列 4 個修飾詞 :
  • long : 與 int 相同為 4 bytes 整數.
  • short : 2 bytes 整數. 
  • signed : 有正數與負數. 
  • unsigned : 僅正數. 
其中 signed 與 unsigned 也用來修飾 char 類型. 如果整數變數均為正數, 則可宣告為 unsigned short 或 unsigned int.

字元資料必須用單引號括起來, 例如 'a' 與 'A' (單引號裡面只能放一個字元), 以一個 byte 的 ASCII 編碼 (0~127 的整數) 儲存, 例如 'A' 是以 65 這個整數儲存. 另外還有稱為脫逸字元的特殊字元, 常用的脫逸字元如下 (均占用 1 byte 的記憶體) : 
  • \0 : 空字元=NULL (用來標示字串的結束)
  • \n : 換行字元
  • \r : 回車字元
  • \t : 定位點字元
  • \\ : 倒斜線 \
  • \? : 問號 ?
  • \' : 單引號 '
  • \" : 雙引號


2. 變數與識別字 :

變數可以先宣告再賦值, 語法如下 : 

資料型態 識別字;    
識別字=值;    

int a;           // 先宣告
a=100;        // 再賦值

或者宣告同時賦值 :

資料型態 識別字=值;

例如 :

int a=100;     // 宣告同時賦值

可以同時宣告多個相同型別的變數, 各變數識別字以逗號隔開, 語法如下 : 

資料型態 識別字1, 識別字2, 識別字3, .... ; 

例如 :

int a, b, c;
int a=1, b=2, c=3; 

指定資料型態的目的是要讓編譯器為此變數配置所需的記憶體空間. 

識別字命名的規則如下 :
  • 字母有分大小寫, user 與 User 是不同的識別字. 
  • 只能使用英文字母, 數字, 與底線之組合, 但不可以用數字開頭.
  • 不可使用關鍵字或與內建函式同名
  • 長度不可超過 127 個字元.
注意, 這些命名規則不僅適用於變數名稱, 也適用於常數名稱, 函式名稱; 也適用於 C++ 中的物件與類別名稱.  

關鍵字是語言本身的保留字, 不可用作識別字.  C 的關鍵字如下表 : 


 auto break case char const
 continue default do double else
 enum extern float for goto
 if int long register return
 short signed sizeof static struct
 switch typedef union unsigned void
 volatile while   


C++ 又補充了如下關鍵字 : 


 bool catch explicit namespace delete
 dynamic_cast mutable protected friend class
 inline private template new operator
 static_cast try public reinterpret_case true
 virtual this throw typeid false
 type_name const_cast wchar_t asm using


3. 常數 :

常數的宣告有兩種方法 :
  • 使用前置指令 #define :
    語法 : #define 識別字 常數值  
    例如 : #define PI 3.14159
  • 使用 const 關鍵字 :
    語法 : const 資料型態 識別字=常數值;      
    例如 : const double PI=3.14159
注意 : 
  • 常數的識別字命名規則與變數一樣, 但習慣上全部使用大寫字母 (非強制). 
  • 使用前置指令時不需要指定資料型態 , 結尾也不可家分號; 而使用 const 時則要指定資料型態, 結尾必須用分號. 
  • 常數值若是字元要用單引號括起來; 若是字串則要用雙引號括起來.
C & C++ 中的字串常數以字元陣列 + '\0' 儲存, 故字串會比一般字元串列長度多出 1 個 byte, 字串常數以雙引號括起來, 例如 "A" 是字串, 長度為 2 個 byte; 而 'A' 則是字元, 長度為 1 個 byte. 

C++ 新增了 bool 布林型態, 其值只有 true 與 false, 等同於整數的 1 與 0.  

2023年5月29日 星期一

C 語言 CH11 結構範例/練習/作業


範例 : 
  1. https://replit.com/@tony1966/c-test#ch11-struct/Example01.c
  2. https://replit.com/@tony1966/c-test#ch11-struct/Example02.c
  3. https://replit.com/@tony1966/c-test#ch11-struct/Example03.c
  4. https://replit.com/@tony1966/c-test#ch11-struct/Example04.c
  5. https://replit.com/@tony1966/c-test#ch11-struct/test01.c
  6. https://replit.com/@tony1966/c-test#ch11-struct/test02.c
  7. https://replit.com/@tony1966/c-test#ch11-struct/test03.c
  8. https://replit.com/@tony1966/c-test#ch11-struct/test04.c
  9. https://replit.com/@tony1966/c-test#ch11-struct/test05.c
  10. https://replit.com/@tony1966/c-test#ch11-struct/test06.c
  11. https://replit.com/@tony1966/c-test#ch11-struct/test07.c
  12. https://replit.com/@tony1966/c-test#ch11-struct/test08.c
  13. https://replit.com/@tony1966/c-test#ch11-struct/test09.c
  14. https://replit.com/@tony1966/c-test#ch11-struct/test10.c
  15. https://replit.com/@tony1966/c-test#ch11-struct/test11.c
  16. https://replit.com/@tony1966/c-test#ch11-struct/test12.c
  17. https://replit.com/@tony1966/c-test#ch11-struct/test13.c
練習題 : 
  1. C程式宣告名為record的結構,內含2int型態的成員變數ab,和1float型態的成員變數c,然後建立結構變數test,指定float型態的成員值為123.6,和2int型態的成員值都為150,最後計算和顯示成員變數的總和。
    https://replit.com/@tony1966/c-test#ch11-struct/practice01.c
  2. 請繼續實作題1,宣告結構指標ptr指向test,然後使用兩種指標方式來分別指定int成員變數值為8867,和計算和顯示成員變數的平均。
    https://replit.com/@tony1966/c-test#ch11-struct/practice02.c
  3. 請建立C程式宣告employee結構來儲存員工資料,包含姓名、年齡和薪水,然後建立結構陣列儲存5位員工的基本資料。
    https://replit.com/@tony1966/c-test#ch11-struct/practice03.c
  4. 請建立C程式宣告item結構,擁有成員變數name字串(大小為20), 2個整數變數armslegs儲存有幾隻手和腳,然後使用結構陣列儲存下表的項目,最後一一顯示項目的成員變數值,如下所示:(Human 2 2) (Cat,0,4) (Dog,0,4) (Table,0,4)
    https://replit.com/@tony1966/c-test#ch11-struct/practice04.c
作業 :
  1. 下列程式碼會在哪一行出現編譯錯誤?
    struct user {
      char *name;
      };
    struct user users[5];
    users.name[0]="Tony";
  2. 下列的結構宣告哪裡有錯?
    struct member {
      char name[30];
      struct member no;
      }

2023年5月24日 星期三

C/C++ 學習筆記 : 結構

結構 (structure) 是將數個相關的資料類型 (稱為欄位或成員) 集結起來組成一個衍生的資料類型, 讓程式設計者可以依據需要自訂資料類型. 結構裡面的欄位可以是同質的 (全部欄位之類型相同) 或異質的 (包含不同類型), 而且欄位也可以是另一個結構 (即巢狀結構). 

例如公司的產品, 可以用包含型號 (字串), 品名 (字串), 與價格 (整數) 這三個欄位組成的結構來儲存; 好友通訊錄可以用包含姓名 (字串), 電話 (字串), 與生日 (字串) 等類型組成之結構來表示. 結構可讓資料的儲存更有系統. 


1. 定義結構 : 

C/C++ 使用 struct 關鍵字來定義結構, 語法如下 :

struct 結構名稱 {   
    資料類型 欄位名稱1;   
    資料類型 欄位名稱2;   
    ....   
    };    

注意, 每個欄位宣告後要用分號結束; 結尾大括號後面也要用分號結束. 定義一個結構就是在描述此新的資料型態是如何組成的, 它的成員可由既有的基本資料型態組成, 例如數值, 字串, 陣列, 指標, 甚至是另一個結構 (即巢狀結構, 結構裡面的成員是另一個結構). 其次, 結構的定義可以寫在 main() 裡面 (其他函式不可用), 也可以寫在 main() 外面 (其他函式可用).

例如定義一個記錄學生成績的結構 :

struct score {
    char name[10];
    int chinese;
    int english;
    int math;
    int nature;
    }

C++ 可以匯入 string 函式庫後使用 string 類型 : 

struct score {
    string name;
    int chinese;
    int english;
    int math;
    int nature;
    }


2. 結構變數的宣告與賦值 : 

定義一個結構後, 即可使用下列語法宣告結構變數 :

struct 結構名稱 結構變數名稱;      

以上面定義的 score 結構為例宣告兩個結構變數 : 

sctruc score score1, score2;       //宣告了兩個結構變數 score1, scroe2

但這兩個變數目前內容是空的, 賦值的方式是透過小數點符號取得欄位, 對於數值欄位 (整數與浮點數) 而言, 直接用 = 運算子將欄位值指派給該欄位即可, 但字串欄位則必須匯入 string 函式庫後使用 strcpy() 函式來賦值, 例如 :

strcpy(score1.name, "Tony");
score1.chinese=90;
score1.english=87;
score1.math=77;
score1.nature=92;

也可以用陣列的大括弧來賦值, 裡面的值一一對應到宣告結構時之順序, 例如 : 

struct score score2={"Amy", 98, 91, 56, 65}

使用此方式用不到 strcpy() 複製字串到欄位裏, 所以就不需要匯入 string 函式庫了. 結構成員的取值方式一律使用小數點符號與欄位名稱,  例如 score1.name, score2.nature 等. 如果要修改欄位內容, 除了字串需使用 strcpy() 外, 數值可直接用指派運算子 '=' 直接修改. 

下面範例中宣告了兩個結構變數 score1 與 score2, 分別用宣告後再賦值與宣告同時賦值兩種方式來儲存資料 : 

/* 建立結構和顯示結構內容 test01.c */
#include <stdio.h>
#include <string.h>

struct score {  //定義結構
  char name[10];
  int chinese;
  int english;
  int math;
  int nature;
  };

int main() {
  /* 宣告結構變數score1並使用.賦值 */
  struct score score1;   
  strcpy(score1.name, "Tony");
  score1.chinese=90;
  score1.english=87;
  score1.math=77;
  score1.nature=92;
  /* 顯示結構score1資料 */
  printf("結構score1的內容:\n");
  printf("姓名: %s\n", score1.name);
  printf("國文: %d\n", score1.chinese);
  printf("英文: %d\n", score1.english);
  printf("數學: %d\n", score1.math);
  printf("自然: %d\n", score1.nature);   
  /* 宣告結構變數score2並使用{}賦值 */
  struct score score2={"Amy", 98, 91, 56, 65); 
  /* 顯示結構score2資料 */
  printf("結構score2的內容:\n");
  printf("姓名: %s\n", score2.name);
  printf("國文: %d\n", score2.chinese);
  printf("英文: %d\n", score2.english);
  printf("數學: %d\n", score2.math);
  printf("自然: %d\n", score2.nature);   
  return 0;
  }

將此程式貼到線上編譯器 onlinGDB 執行結果如下 : 

結構score1的內容:
姓名: Tony
國文: 90
英文: 87
數學: 77
自然: 92
結構score2的內容:
姓名: Amy
國文: 98
英文: 91
數學: 56
自然: 65


3. 結構陣列 : 

陣列的元素為結構變數時, 此陣列變數稱為結構陣列, 其元素為在記憶體連續空間中的結構變數資料, 這與宣告一些相同定義之結構變數不同 (可能會放在不連續空間).  

結構陣列變數宣告方式如下 : 

struct 結構名稱 結構陣列名稱[長度];     

在此之前要先用 struct 關鍵字宣告一個結構, 例如 :

struct score {  //定義結構
  char name[10];
  int chinese;
  int english;
  int math;
  int nature;
  }; 

然後宣告含有兩個元素的結構陣列變數 scores[] : 

struct score scores[2];

注意, 給每一個元素賦值時不可以用 {}  指派給元素 : 

scores[0]={"Tony", 90, 87, 77, 92};
scores[1]={"Amy", 98, 91, 56, 65}; 

這種包含字串地內容要在宣告時同時賦值 :

struct score scores[2]={{"Tony", 90, 87, 77, 92}, {"Amy", 98, 91, 56, 65}}

完整範例程式如下 : 

/* 建立結構陣列和顯示其內容 : test02.c */
#include <stdio.h>
#include <string.h>

struct score {  //定義結構
  char name[10];
  int chinese;
  int english;
  int math;
  int nature;
  };

int main() {
  /* 宣告結構陣列變數scores並賦值 */
  struct score scores[2];   //宣告結構陣列變數
  strcpy(scores[0].name, "Tony");    //字串要用 strcpy() 賦值
  scores[0].chinese=90;
  scores[0].english=87;
  scores[0].math=77;
  scores[0].nature=92;
  strcpy(scores[0].name, "Amy");    //字串要用 strcpy() 賦值
  scores[1].chinese=98;
  scores[1].english=91;
  scores[1].math=56;
  scores[1].nature=65; 
  /* 顯示結構變數scores[0]資料 */
  printf("結構score[0]的內容:\n");
  printf("姓名: %s\n", scores[0].name);
  printf("國文: %d\n", scores[0].chinese);
  printf("英文: %d\n", scores[0].english);
  printf("數學: %d\n", scores[0].math);
  printf("自然: %d\n", scores[0].nature);   
  /* 顯示結構變數scores[1]資料 */
  printf("結構score[1]的內容:\n");
  printf("姓名: %s\n", scores[1].name);
  printf("國文: %d\n", scores[1].chinese);
  printf("英文: %d\n", scores[1].english);
  printf("數學: %d\n", scores[1].math);
  printf("自然: %d\n", scores[1].nature);  
  return 0;
  }

使用線上編譯器 onlinGDB 執行結果如下 :  

結構score[0]的內容:
姓名: Amy
國文: 90
英文: 87
數學: 77
自然: 92
結構score[1]的內容:
姓名: 
國文: 98
英文: 91
數學: 56
自然: 65

既然是陣列, 可以用迴圈讓程式更簡潔 : 

/* 建立結構陣列和顯示其內容 : test03.c */
#include <stdio.h>
#include <string.h>

struct score {  //定義結構
  char name[10];
  int chinese;
  int english;
  int math;
  int nature;
  };

int main() {
  /* 宣告結構陣列變數scores並賦值 */
  struct score scores[2];   //宣告結構陣列變數
  strcpy(scores[0].name, "Tony");    //字串要用 strcpy() 賦值
  scores[0].chinese=90;
  scores[0].english=87;
  scores[0].math=77;
  scores[0].nature=92;
  strcpy(scores[0].name, "Amy");    //字串要用 strcpy() 賦值
  scores[1].chinese=98;
  scores[1].english=91;
  scores[1].math=56;
  scores[1].nature=65; 
  /* 顯示結構變數資料 */
  int i;
  for (i=0; i<2; i++) {
    printf("結構score[%d]的內容:\n", i);
    printf("姓名: %s\n", scores[i].name);
    printf("國文: %d\n", scores[i].chinese);
    printf("英文: %d\n", scores[i].english);
    printf("數學: %d\n", scores[i].math);
    printf("自然: %d\n", scores[i].nature);   
    }  
  return 0;
  }


4. 結構指標 : 

指標可以指向任何類型的資料, 指向結構開始位址的指標稱為結構指標, 只要在宣告結構變數時, 在變數名稱前面加個星號, 此指標所存之位址即指向一個結構. 

結構指標語法如下 :

struct *結構指標名稱;    

首先宣告一個結構 score :

struct score {  //定義結構
  char name[10];
  int chinese;
  int english;
  int math;
  int nature;
  }; 

接著宣告一個結構變數 score1 :

struct score score1={"Tony", 90, 87, 77, 92};

然後宣告一個結構指標 ptr, 並將結構 score1 的位址指派給 *ptr :

struct score *ptr=&score1;

這樣結構指標 ptr 就指向結構 score1 了. 

存取結構指標所指向的內容有兩種做法 :
  • 使用指標所指內容搭配點運算子 : (*ptr).欄位名稱
    由於存取欄位的小數點運算子比指標運算子優先權高, 因此要先用括號將指標內容括起來, 例如要用 (*ptr).name 而非 *ptr.name (這會編譯錯誤).
  • 使用指標名稱搭配 -> 運算子 : ptr->欄位名稱
    注意, 使用 -> 時要用指標名稱, 不是所指向的內容 (*ptr).
下面範例指標內容搭配點運算子來存取結構之欄位 :

/* 建立結構陣列和顯示其內容 : test04.c */
#include <stdio.h>

struct score {  //定義結構
  char name[10];
  int chinese;
  int english;
  int math;
  int nature;
  };

int main() {
  /* 宣告結構變數score1並賦值 */
  struct score score1={"Tony", 90, 87, 77, 92}; 
  struct score *ptr=&score1;
  /* 顯示結構變數score1資料 */
  printf("結構score1的內容:\n");
  printf("姓名: %s\n", score1.name);
  printf("國文: %d\n", score1.chinese);
  printf("英文: %d\n", score1.english);
  printf("數學: %d\n", score1.math);
  printf("自然: %d\n", score1.nature); 
  /* 使用指標所指內容(*ptr)搭配點運算子來顯示結構資料 */
  printf("結構指標*ptr的內容:\n");  
  printf("姓名: %s\n", (*ptr).name);
  printf("國文: %d\n", (*ptr).chinese);
  printf("英文: %d\n", (*ptr).english);
  printf("數學: %d\n", (*ptr).math);
  printf("自然: %d\n", (*ptr).nature);      
  return 0; 
  }

此例使用指標內容 (*ptr) 搭配點運算子來存取結構欄位. 

用線上編譯器 onlinGDB 執行結果如下 :

結構score1的內容:
姓名: Tony
國文: 90
英文: 87
數學: 77
自然: 92
結構指標*ptr的內容:
姓名: Tony
國文: 90
英文: 87
數學: 77
自然: 92

可見由於 ptr 指向 score1, 因此兩個內容完全一樣, 更改 (*ptr) 的任一欄位同時也會改到 score1 的欄位內容. 

下面範例指標名稱搭配 -> 運算子來存取結構之欄位 :

/* 建立結構陣列和顯示其內容 : test05.c */
#include <stdio.h>

struct score {  //定義結構
  char name[10];
  int chinese;
  int english;
  int math;
  int nature;
  };

int main() {
  /* 宣告結構變數score1並賦值 */
  struct score score1={"Tony", 90, 87, 77, 92}; 
  struct score *ptr=&score1;
  /* 顯示結構變數score1資料 */
  printf("結構score1的內容:\n");
  printf("姓名: %s\n", score1.name);
  printf("國文: %d\n", score1.chinese);
  printf("英文: %d\n", score1.english);
  printf("數學: %d\n", score1.math);
  printf("自然: %d\n", score1.nature); 
  /* 使用指標名稱搭配 -> 運算子顯示結構資料 */
  printf("結構指標*ptr的內容:\n");  
  printf("姓名: %s\n", ptr->name);
  printf("國文: %d\n", ptr->chinese);
  printf("英文: %d\n", ptr->english);
  printf("數學: %d\n", ptr->math);
  printf("自然: %d\n", ptr->nature);       
  return 0;
  }

執行結果與前面範例一樣. 


5. 巢狀結構 : 

結構的成員是另一個結構稱為巢狀結構, 語法如下 : 

struct 結構名稱1 {   
    資料類型 欄位名稱1;   
    資料類型 欄位名稱2;   
    ....   
    };   
struc 結構名稱2 {   
    資料類型 欄位名稱1;   
    struct 結構名稱1 結構變數名稱;   
    ....   
    }; 

例如 : 

struct name {  //定義結構 name
  char first_name[10];
  char last_name[10];
  };

struct score {  //定義結構 score
  struct name student_name;    // 此欄位為另一個結構 name
  int chinese;
  int english;
  int math;
  int nature;
  };

此處定義了兩個結構 name 與 score, 其中 score 的第一個成員是 name 結構, 形成巢狀結構. 接著宣告兩個 score 結構變數 score1 與 score2 :

struct score score1={{"Tony", "Huang"}, 90, 87, 77, 92};
struct score score2={{"Amy", "Chen"}, 98, 91, 56, 65); 

因為第一個成員是另一個結構 name, 因此要用 {} 賦值. 存取內層結構 name 的成員時必須使用兩層的點運算子, 例如 :

score1.student_name.first_name  
score1.student_name.last_name 
score2.student_name.first_name  
score2.student_name.last_name 

完整範例如下 :

/* 建立巢狀結構和顯示其內容 : test06.c */
#include <stdio.h>

struct name {  //定義結構 name
  char first_name[10];
  char last_name[10];
  };

struct score {  //定義結構 score
  struct name student_name;  
  int chinese;
  int english;
  int math;
  int nature;
  };

int main() {
  /* 宣告結構變數score1並賦值 */
  struct score score1={{"Tony", "Huang"}, 90, 87, 77, 92};
  struct score score2={{"Amy", "Chen"}, 98, 91, 56, 65}; 
  /* 顯示結構變數score1資料 */
  printf("結構score1的內容:\n");
  printf("姓名: %s %s\n", score1.student_name.first_name, score1.student_name.last_name);
  printf("國文: %d\n", score1.chinese);
  printf("英文: %d\n", score1.english);
  printf("數學: %d\n", score1.math);
  printf("自然: %d\n", score1.nature); 
  /* 顯示結構變數score1資料 */
  printf("結構score2的內容:\n");
  printf("姓名: %s %s\n", score2.student_name.first_name, score1.student_name.last_name);
  printf("國文: %d\n", score2.chinese);
  printf("英文: %d\n", score2.english);
  printf("數學: %d\n", score2.math);
  printf("自然: %d\n", score2.nature);   
  return 0;
  }

使用線上編譯器 onlinGDB 執行結果如下 :  

結構score1的內容:
姓名: Tony Huang
國文: 90
英文: 87
數學: 77
自然: 92
結構score2的內容:
姓名: Amy Huang
國文: 98
英文: 91
數學: 56
自然: 65


6. 將結構傳入函式 : 

結構作為一種自訂的資料型態, 當然也可以當引數傳給函式, 但要注意, 直接將結構傳入函式中時使用的是傳值呼叫, 亦即函式接收到的參數只是一個副本, 在函式內對此副本的存取不會影響到外部的結構內容, 例如 :

/* 將結構傳入函式和顯示其內容 : test07.c */
#include <stdio.h>
#include <string.h>

struct score {  //定義結構
  char name[10];
  int chinese;
  int english;
  int math;
  int nature;
  };

void set_score(struct score s);     // 函式原型宣告必須在結構定義之後

int main() {
  /* 宣告結構變數score1並賦值 */
  struct score score1={"Tony", 90, 87, 77, 92};
  set_score(score1);   // 這是傳值呼叫
  /* 顯示函式外結構變數score1資料 */
  printf("函式外結構score1的內容:\n");
  printf("姓名: %s\n", score1.name);
  printf("國文: %d\n", score1.chinese);
  printf("英文: %d\n", score1.english);
  printf("數學: %d\n", score1.math);
  printf("自然: %d\n", score1.nature);   
  return 0;
  }

void set_score(struct score s) {
  /* 設定結構變數s資料 */
  strcpy(s.name, "Amy");
  s.chinese=98;
  s.english=91;
  s.math=56;
  s.nature=65;
  /* 顯示函式內結構變數s資料 */
  printf("函式內結構變數s的內容:\n");
  printf("姓名: %s\n", s.name);
  printf("國文: %d\n", s.chinese);
  printf("英文: %d\n", s.english);
  printf("數學: %d\n", s.math);
  printf("自然: %d\n", s.nature); 
  }

此例宣告了一個結構變數 score1, 然後將其作為引數傳入函式 set_score() 中, 由於直接將結構傳入函式為傳值呼叫, 傳入之參數為結構之副本, 故於函式內設定該結構副本之欄位對函式外部的結構毫無影響.

使用線上編譯器 onlinGDB 執行結果如下 : 

函式內結構變數s的內容:
姓名: Amy
國文: 98
英文: 91
數學: 56
自然: 65
函式外結構score1的內容:
姓名: Tony
國文: 90
英文: 87
數學: 77
自然: 92

可見函式內的結構雖然被改成 Amy 的成績, 但函式外的結構仍然是 Tony 的成績. 如果要在函式內更改函式外的結構變數內容, 必須使用結構指標, 例如 :

/* 將結構指標傳入函式和顯示其內容 : test08.c */
#include <stdio.h>
#include <string.h>

struct score {  //定義結構
  char name[10];
  int chinese;
  int english;
  int math;
  int nature;
  };

void set_score(struct score *ptr);

int main() {
  /* 宣告結構變數score1並賦值 */
  struct score score1={"Tony", 90, 87, 77, 92};
  struct score *ptr;   // 宣告結構指標變數
  ptr=&score1;         // 取得score1位址指定給指標ptr
  set_score(ptr);       // 這是傳址呼叫
  /* 顯示函式外結構變數score1資料 */
  printf("函式外結構score1的內容:\n");
  printf("姓名: %s\n", score1.name);
  printf("國文: %d\n", score1.chinese);
  printf("英文: %d\n", score1.english);
  printf("數學: %d\n", score1.math);
  printf("自然: %d\n", score1.nature);   
  return 0;
  }

void set_score(struct score *ptr) {
  /* 設定結構變數s資料 */
  strcpy((*ptr).name, "Amy");
  (*ptr).chinese=98;
  (*ptr).english=91;
  (*ptr).math=56;
  (*ptr).nature=65;
  /* 顯示函式內結構變數s資料 */
  printf("函式內結構變數s的內容:\n");
  printf("姓名: %s\n", (*ptr).name);
  printf("國文: %d\n", (*ptr).chinese);
  printf("英文: %d\n", (*ptr).english);
  printf("數學: %d\n", (*ptr).math);
  printf("自然: %d\n", (*ptr).nature); 
  }

用線上編譯器 onlinGDB 執行結果如下 : 

函式內結構變數s的內容:
姓名: Amy
國文: 98
英文: 91
數學: 56
自然: 65
函式外結構score1的內容:
姓名: Amy
國文: 98
英文: 91
數學: 56
自然: 65

可見由於傳入函式的引數是結構指標, 因此在函式內更改結構內容也就是更改函式外結構變數 score1 的內容, 所以列印的結果完全相同. 如上所述, 也可以使用結構專用的 -> 運算子來存取結構成員, 例如以 ptr->name 取代 (*ptr).name : 

/* 將結構指標傳入函式和顯示其內容 : test09.c */
#include <stdio.h>
#include <string.h>

struct score {  //定義結構
  char name[10];
  int chinese;
  int english;
  int math;
  int nature;
  };

void set_score(struct score *ptr);

int main() {
  /* 宣告結構變數score1並賦值 */
  struct score score1={"Tony", 90, 87, 77, 92};
  struct score *ptr;   // 宣告結構指標變數
  ptr=&score1;         // 取得score1位址指定給指標ptr
  set_score(ptr);      // 這是傳址呼叫
  /* 顯示函式外結構變數score1資料 */
  printf("函式外結構score1的內容:\n");
  printf("姓名: %s\n", score1.name);
  printf("國文: %d\n", score1.chinese);
  printf("英文: %d\n", score1.english);
  printf("數學: %d\n", score1.math);
  printf("自然: %d\n", score1.nature); 
  }

void set_score(struct score *ptr) {
  /* 設定結構變數s資料 */
  strcpy(ptr->name, "Amy");
  ptr->chinese=98;
  ptr->english=91;
  ptr->math=56;
  ptr->nature=65;
  /* 顯示函式內結構變數s資料 */
  printf("函式內結構變數s的內容:\n");
  printf("姓名: %s\n", ptr->name);
  printf("國文: %d\n", ptr->chinese);
  printf("英文: %d\n", ptr->english);
  printf("數學: %d\n", ptr->math);
  printf("自然: %d\n", ptr->nature); 
  }

結果與上面用 (*ptr). 作法結果相同.


7. 用 typedef 定義新資料型態 :    

型態定義關鍵字 typedef 用來為既有之資料型態取別名, 語法如下 :

typedef 原資料型態名稱 新資料型態名稱 

這經常被用在結構這種自訂資料型態上, 可使結構變數之宣告變得較簡短, 不必每次宣告一個結構變數時都要冠上 struct 關鍵字, 例如上面範例中所定義的結構 :

struct score {  //定義結構
  char name[10];
  int chinese;
  int english;
  int math;
  int nature;
  };

當宣告結構變數時必須使用 struct 關鍵字 :

struct score score1, score2;

如果用 typedef 為此結構取一個型態別名 :

typedef struct score myscore;   // 為結構 score 取一個型態別名 myscore

則宣告 score 結構變數時便可直接使用 myscore 型態 :

myscore score1, score2;  

測試範例如下 : 

/* 用typedef為結構取一個別名 : test10.c */
#include <stdio.h>

struct score {  //定義結構
  char name[10];
  int chinese;
  int english;
  int math;
  int nature;
  };

int main() {
  typedef struct score myscore;  //用typedef為結構score取別名myscore
  /* 宣告結構變數score1並賦值 */
  myscore score1={"Tony", 90, 87, 77, 92}; //用myscore宣告結構變數
  struct score score2={"Amy", 98, 91, 56, 65};  //用struct score宣告結構變數
  /* 顯示函式外結構變數score1資料 */
  printf("結構score1的內容:\n");
  printf("姓名: %s\n", score1.name);
  printf("國文: %d\n", score1.chinese);
  printf("英文: %d\n", score1.english);
  printf("數學: %d\n", score1.math);
  printf("自然: %d\n", score1.nature); 
  /* 顯示函式外結構變數score2資料 */
  printf("結構score2的內容:\n");
  printf("姓名: %s\n", score2.name);
  printf("國文: %d\n", score2.chinese);
  printf("英文: %d\n", score2.english);
  printf("數學: %d\n", score2.math);
  printf("自然: %d\n", score2.nature);  
  }

此例用 typedef 為結構 socre 取了一個型態別名 myscore, 然後分別用 myscore 與原始的 struct score 方式宣告了兩個結構變數 score1 與 score2, 顯然用新型態 myscore 較簡短, 以線上編譯器 onlinGDB 執行結果如下 : 

結構score1的內容:
姓名: Tony
國文: 90
英文: 87
數學: 77
自然: 92
結構score2的內容:
姓名: Amy
國文: 98
英文: 91
數學: 56
自然: 65

也可以在定義結構時就用 typedef 為結構取一個型態別名, 語法如下 : 

typedef struct 結構名稱 {   
    資料類型 欄位名稱1;   
    資料類型 欄位名稱2;   
    ....   
    } 新型態名稱;    

例如 : 

typedef struct score {  //定義結構並取一個新型態名稱 myscore
  char name[10];
  int chinese;
  int english;
  int math;
  int nature;
  } myscore;

這樣既可以用原始的 struct score 來宣告結構變數, 也可以用 myscore 來宣告 :

struct score score1;
myscore score2; 

可將上面範例修改為 :

/* 用typedef為結構取一個別名 : test11.c */
#include <stdio.h>

typedef struct score {  //定義結構並取一個新型態名稱
  char name[10];
  int chinese;
  int english;
  int math;
  int nature;
  } myscore;

int main() {
  /* 宣告結構變數score1並賦值 */
  myscore score1={"Tony", 90, 87, 77, 92}; //用myscore宣告結構變數
  struct score score2={"Amy", 98, 91, 56, 65};  //用struct score宣告結構變數
  /* 顯示函式外結構變數score1資料 */
  printf("結構score1的內容:\n");
  printf("姓名: %s\n", score1.name);
  printf("國文: %d\n", score1.chinese);
  printf("英文: %d\n", score1.english);
  printf("數學: %d\n", score1.math);
  printf("自然: %d\n", score1.nature); 
  /* 顯示函式外結構變數score2資料 */
  printf("結構score2的內容:\n");
  printf("姓名: %s\n", score2.name);
  printf("國文: %d\n", score2.chinese);
  printf("英文: %d\n", score2.english);
  printf("數學: %d\n", score2.math);
  printf("自然: %d\n", score2.nature);  
  }
 
執行節果與上一個範例相同.

事實上 typedef 可以用來為任何資料型態取別名, 例如為 int 型態取 integer 別名 : 

typedef int integer;
integer year=2023;

或為 char* 指標取 string 別名 :

typedef char* string; 
string="Hello World!" 

注意, 字串指標要用 char* 而非 *char. 

完整範例如下 : 

/* 用typedef為int與*char取一個別名 : test13.c */
#include <stdio.h>

int main() {
  typedef int integer;       // 為int型別取別名integer
  integer year=2023;         // 用integer宣告整數變數
  printf("%d\n", year);      
  typedef char* string;      // 為*char型別取別名string
  string str="Hello World!"; // 用string宣告字串變數
  printf("%s\n", str);
  return 0;
  }

執行結果如下 : 

2023
Hello World!