2017年9月25日 星期一

使用 Arduino IDE 開發 ESP8266 應用 (一) : 環境設定與韌體上傳

我以前剛開始接觸 ESP8266 時 Arduino IDE 尚未支援 ESP8266, 因此是透過軟體序列埠連接 Arduino Nano 板與 ESP-01 模組, 從 Arduino Nano 發出 AT 指令控制 ESP8266 的網路連線. 這種做法是以 Arduino 開發板當微控器, ESP8266 只是一個透過序列埠通訊的 WiFi 模組而已, 即 Arduino 是主, ESP8266 是僕, 但缺點是透過序列埠通訊傳遞 AT 指令再從回應中解讀 TCP/IP 協定, 操作速度受到序列埠的限制.

事實上 ESP8266 本身就是一個 32 位元處理器, 比起 Arduino 那 8 位元的 ATMEGA328P 處理器要強多了. 當 Arduino IDE 支援 ESP8266 開發板後, 就可以直接用 Arduino 語法 (C 語言系列) 開發 ESP8266 物聯網應用程式了. 不過 Arduino IDE 預設並未含有 ESP8266 開發板函式庫與驅動程式, 必須在 "檔案/偏好設定" 的 "額外的開發板管理員位址" 欄位輸入下列網址 :

http://arduino.esp8266.com/stable/package_esp8266com_index.json




然後在 "工具/開發板/開發板管理員" 中, 搜尋 "ESP8266" 會看到 "esp8266 by ESP8266 Community" 這一項, 按底下的 "安裝" 鈕進行下載安裝 :




安裝完成後會顯示 "Installed" :




這時再切到 "工具/開發板" 就可以看到 "ESP8266 Modules" 選單了. 如果是使用 ESP-01/ESP-12 等模組應選 "Generic ESP8266 Module" :




事實上往下拉還支援很多 ESP8266 開發板, 我手上有的板子是 NodeMCU 與 WeMOS D1 Mini :




選取板子類型後再切到 "工具", 就可看到此板子的資訊, 例如 Flash 大小與 baud rate (預設 115200 bps) 等 (這裡我用的是 ESP-01 模組) :




這樣就可以開始撰寫 Arduino 程式上傳 ESP8266 了. 注意, 燒錄韌體時必須先將 GPIO 0 接地 , 燒錄完之後再拔掉. 如果沒有將 GPIO 0, 會出現如下的 "espcomm_sync failed" 與 "error: espcomm_upload_mem failed" 錯誤訊息 :

warning: espcomm_sync failed
error: espcomm_open failed
error: espcomm_upload_mem failed
error: espcomm_upload_mem failed

參考 :

warning: espcomm_sync failed error: espcomm_open failed


我這次是使用手邊之前玩 Arduino+ESP8266 時剩下的 ESP-01 512K Flash 模組進行測試, 我把它插在好用的 ESP-01 專用轉接板上, 配合一個小麵包板與 2 個 LED + 220 歐姆電阻, 複製雙 LED 交互閃爍實驗 :




我是從加工後的背板引出 ESP-01 模組的 GND, GPIO 0, GPIO 2 三支腳到麵包板上, GPIO 0 與 GPIO 2 分別驅動兩個 LED, 此兩個 LED 各串接 200 歐姆電阻後共接 ESP-01 的 GND.

關於 ESP-01 專用轉接板參考 :

ESP-01 專用轉接板加工

我把上面這篇文章末尾的 MircoPython 程式改寫為 Arduino 程式如下 :


const int LED0=0;     //GPIO 0
const int LED2=2;     //GPIO 2
void setup() {
    pinMode(LED0,OUTPUT);
    pinMode(LED2,OUTPUT);
    }

void loop() {
    digitalWrite(LED0,HIGH);
    digitalWrite(LED2,LOW);
    delay(1000);
    digitalWrite(LED0,LOW);
    digitalWrite(LED2,HIGH);
    delay(1000);  
    }


草稿碼使用了 222229 bytes (51%) 的程式儲存空間。上限為 434160 bytes。
全域變數使用了 31572 bytes (38%) 的動態記憶體,剩餘 50348 bytes 給區域變數。上限為 81920 bytes 。
Uploading 226384 bytes from C:\Users\cht\AppData\Local\Temp\arduino_build_695446/arduesp_1.ino.bin to flash at 0x00000000
................................................................................ [ 36% ]
................................................................................ [ 72% ]
..............................................................                   [ 100% ]

天啊! 短短的程式編譯後竟然高達 222KB! 已經把 512K Flash (實際上只有 434KB 可用) 占掉一半了 (51%)! 反觀 Arduino Nano 才 30KB Flash, 同樣程式碼應該不到 10%. 韌體上傳完成後, 拔掉 GPIO 0 的接地線, 將 ESP8266 重開機即可看到兩個 LED 交互閃爍了.




參考 :

http://ruten-proteus.blogspot.tw/2015/09/esp8266-kits-support-arduino-ide.html

2017年9月24日 星期日

2017 年第 38 周記事

週六早上二哥他們學校開班親會, 剛好導師安排的 APCS 班第一次上課, 所以就載他一起去. 初次見到新導師感覺非常年輕有活力, 但他說已任教超過十年了, 孩子才三歲, 那就跟我碩士班那些小我一輪的同學差不多. 聽老師說他原先學 Javascript, 後來覺得弄錯方向, 就改學 C 了. 其實 Javascript 是前端 Only One, 也很重要啊, 但對他要提倡 Coding 風氣, 用 C 確實是正確的. 畢竟它是許多語言之母呀!

週三 9/20 後陽台防漏工程開工, 順便也把主臥室膨起來的部分磁磚敲掉重鋪, 後陽台估 2.8 萬, 臥室磁磚估 1.2 萬, 合計 4 萬. 師傅研判後陽台防漏功能漸失導致濕氣浸潤到樓下是可能的, 但也可能是中間水管有裂縫所致. 總之我先把後陽台防漏重作, 費用我全部支付, 只希望樓下那個張小姐不要再來找麻煩, 以後若還來說浸潤之事, 那就可能是中間水管, 得兩方各付一半. 現在我只要一聽到樓上晚上在敲敲打打, 我就會神經緊張, 樓下總是把所有噪音賴給我們, 說誰叫我住她們樓上, 如果不是我, 那查出誰製造噪音就是我的責任, 天啊!

小舅小舅媽與小阿姨本周去九州玩, 所以番茄苗要下周才種下去, 種了兩年的棗樹今年開花了, 花絮是細細碎碎那種, 因為很多刺本來想砍掉不種了, 但砍到只剩樹幹竟然在半年內又枝葉茂盛還開花了, 難怪每年附近鄰居的棗園一摘完就砍到只剩樹幹, 原來這樣才會開花啊!




週四 (9/21) 是母親三周年忌日, 哀傷雖然隨著時間過去漸漸撫平, 但想起過去回到老家媽都把一切弄得好好的, 回鄉下好像是度假, 現在換我操持, 才知是那麼不容易. 這周在 Youtube 上把 "花甲男孩轉大人" 看完, 看到花甲的阿嬤, 就彷彿看到母親類似的身影, 也讓我停下來想一想, 我是否有失去人生的方向呢?

2017年9月21日 星期四

C 語言測試 : 指標

暑假期間我家二哥把 C 語言最重要的陣列與指標看過一遍, 對於指標的意義與用法有了基本了解, 但指標究竟用來幹嘛卻沒有具體概念. 老實說我也沒有, 因為除了寫 Arduino 有用到基本的 C 語言技巧外, 我從來沒用 C 寫過像樣的東西. 所以就趁這個機會把 C 語言測試一下, 就從最難處理的指標開始吧!

C 語言雖然是高階語言, 但指標功能卻讓 C 語言也擁有低階的記憶體操作能力. 許多高階語言也有支援指標, 例如 C++, FORTRAN, PASCAL, BASIC, Perl, C# 等等, 其中 C++ 除了完整支援 C 的指標功能外, 也新增了 Smart Pointers 以提供較原始指標更安全的功能. 部分支援指標的高階語言因為安全性等原因而對指標做了限制, 例如 PASCAL 與 C#, 而 Java 則完全不支援指標, 參考 :

# Support_in_various_programming_languages

指標的主要用途如下 :
  1. 函式之間若要傳遞字串或陣列使用指標做傳址呼叫可避免資料的複製與搬移
  2. 需要從函式中傳回一個以上的值時必須使用指標, 因為 return 只能傳回一個一般變數值.
  3. 較複雜之資料結構需要利用指標以鏈結串列方式實作.
  4. 使用指標處理字串較方便. 
事實上, 許多 C 語言內建函式內部都是使用指標實作的.

以下的測試程式我參考了下列幾本書的範例加以改編 :
  1. C 語言初學指引第四版 (博碩, 陳錦輝)
  2. C 語言程式設計剖析 (全華, 簡聰海)
  3. C 語言從零開始 (博碩, 資訊教育研究室)
  4. C 語言入門 (易習, 丁國棟)
  5. C 語言程式設計實例入門第四版 (博碩, 高橋麻奈)
  6. C 語言程式設計與應用 (全華, 陳會安)
使用工具包括 Windows 上的 Dev C++, 樹莓派的 gcc, 以及線上 C 語言編譯器 TutorialsPoint (此為 Linux 主機, 使用 gcc 編譯, 不須存檔現打現譯) :

https://www.tutorialspoint.com/compile_c_online.php


一. 指標變數的宣告與賦值

1. 指標是甚麼?

指標是 C 語言中一種特別的資料型態, 專門用來儲存某個資料的記憶體位址, 其內容用來指向該資料. 編譯器使用 CPU 的間接定址法來存取指標變數所指之記憶體位址. 指標最早出現在 PL/I 語言中, 參考 :

https://zh.wikipedia.org/wiki/指標_(電腦科學)

指標變數與一般變數不同, 其儲存的內容是記憶體位址, 因此占用的記憶體大小是固定的, 即等於 CPU 的位址寬度, 例如 32 位元系統之位址匯流排寬度是 32 位元 (4 個 bytes), 可定址 2^32=4G Bytes 的 DRAM, 其指標變數長度為 4 個 Bytes; 而 64 位元系統位址匯流排 64 位元 (8 個 bytes), 定址能力為 2^64=16GB 之 DRAM,  其指標變數長度為 8 個 Bytes.


2. 指標變數的宣告

指標變數宣告方式是在變數名稱前面加 * 號 (ANSI C) 或是在資料型態後面加 * (C++ 新增方式), 例如 :

int *ptr;     //ANSI C 的宣告方式
int* ptr;     //C++ 新增的宣告方式

這都是宣告指向一個整數資料的指標變數 ptr. 指標變數宣告之後, 編譯器會在記憶體中指配記憶位址來儲存所指之位址. 指標變數在賦值之前其內容為記憶體之前的殘留值或隨機初始值, 此未初始化的指標變數是懸空的狀態 (dangling), 不可拿來使用, 因為它可能指向不允許存取的系統保留區或其他程序正在使用的記憶體位址, 可能導致運算結果錯誤或系統當機.

指標變數除了指向一般變數與陣列外, 也可以指向函數, 稱為函數指標, 其宣告方式例如 :

int (*ftpr) (int x,int y);

這是宣告一個指向函數的指標 fptr, 詳如後述.


3. 指標變數的賦值 (初始化) :

指標變數在使用前必須 "初始化 (賦值)" 以指向某一個記憶體位址; 亦即透過指定運算將一個記憶體位址存入指標內. 指標賦值有兩種方式 :
  1.  使用取址運算子 & (reference operator) 
  2.  將本身就是位址的陣列名稱函數名稱指派給指標 
取址運算子 & 可以取得任意變數 (包含指標變數) 之記憶體位址, 例如 :

int a=100;                      //宣告一般變數 a 並賦值 100
int *ptr;                          //宣告指向整數資料的指標
ptr=&a;                 //取得 a 的記憶體位址並賦值給指標 ptr

也可以在宣告指標的同時予以賦值, 上面的程式可改為 :

int a=100;
int *ptr=&a;          //取得 a 的記憶體位址並賦值給指標 ptr


4. 陣列指標 :

指標也可以指向陣列元素, 這種指標稱為陣列指標. 陣列的每一個元素相當於一般的變數, 因此可以用取址運算子 & 取得任一元素之記憶體位址, 然後指派給指標, 例如 :

int a[]={0,1,2,3,4,5};
int *ptr=&a[0];              //宣告整數指標 ptr 指向整數陣列 a 的開頭位址
ptr=&a[3];                      //指標改為指向陣列 a 的第四個元素

將指標指向陣列開頭除了用 &a[0] 取得第一個元素的開頭位址外賦值外, 也可以直接用陣列名稱賦值, 因為陣列名稱本身就是指向其第一個元素的開始位址, 例如 :

int *ptr=a;           //宣告整數指標 ptr 指向整數陣列 a 的開頭位址 (等於 &a[0])

從下面的範例可知, &a[0] 與 a 的內容是一樣的, 都是陣列 a 的開始位址 :

#include <stdio.h>

int main() {
    int a[]={0,1,2,3,4,5};
    printf("a=%p\n",a);
    printf("&a[0]=%p\n",&a[0]);
    int *ptr=a;
    printf("ptr=%p\n",ptr);
    return 0;
    }

結果如下 :

a=0x7ffff331aa90       (陣列開始位址)
&a[0]=0x7ffff331aa90   (陣列開始位址)
ptr=0x7ffff331aa90     (指向陣列開始位址)

陣列其實是一種特殊的指標, 在編譯器的符號表中, a 儲存了第一個元素的開頭位址 &a[0], 其內容是不可更改的 (否則會找不到陣列的開頭), 因此陣列又稱為常數指標. 一般的指標是變數, 其所儲存的內容 (記憶體位址) 是可以改變的, 可以做遞增遞減等算術運算, 例如 ++ptr 或 ptr-- 等等, 但是陣列 a 是常數指標, 不允許做 a++ 或 --a 等運算.

下列程式碼將編譯失敗, 因為陣列的起始位址是不能更改的 :

    int x=5;
    int a[]={0,1,2,3,4,5};
    int a=&x;      //不可更改陣列位址

在 Dev C++ 會出現如下錯誤訊息, 表示 a 已經被宣告為陣列, 不能更改 a 的位址 :

[Error] conflicting declaration 'int a'
[Note] previous declaration as 'int a [6]'

陣列指標除了可使用 *(ptr+i) 來存取陣列元素外, 也可以把指標當成陣列名稱, 使用索引 ptr[i] 來存取陣列元素, 例如 :

#include <stdio.h>

int main() {
    int a[]={0,1,2,3,4,5};
    int *ptr=a;
    printf("a[2]=*(ptr+2)=%d\n",*(ptr+2));    //使用取值運算子
    printf("a[2]=ptr[2]=%d\n",ptr[2]);             //陣列指標也可以使用索引存取陣列元素
    return 0;
    }

執行結果如下 :

a[2]=*(ptr+2)=2
a[2]=ptr[2]=2

指標既然可以指向陣列, 當然也可以在函數中當虛引數 (參數) 接收呼叫者傳遞之陣列. 在函數之間傳遞陣列是以傳址呼叫方式將陣列名稱 (即起始位址) 傳入函數, 例如下列求陣列元素和的程式, 被呼叫的函式必須宣告一個同型態陣列來接收引數 :

int sum(int a[], int len) {    //宣告一個整數陣列接收引數
    int s=0;
    for (int i=0; i<len; i++) {
        s=s + a[i];
        }
    return s;
    }

int main() {
    int len=6;
    int a[]={0,1,2,3,4,5};
    int s=sum(a,len);          //將陣列起始位址傳給函數 sum()
    printf("sum=%d",s);
    }

但是也可以改用指標來接收傳入之引數 :

int sum(int *ptr, int len) {   //宣告一個整數指標接收引數
    int s=0;
    for (int i=0; i<len; i++) {
        s=s + *(ptr + i);     //或者 ptr[i] 亦可
        }
    return s;
    }

int main() {
    int len=6;
    int a[]={0,1,2,3,4,5};
    int s=sum(a,len);         //將陣列起始位址傳給函數 sum()
    printf("sum=%d",s);
    }


5. 字串指標 :

由於 C 語言沒有字串資料類型, 因此字串是利用陣列來儲存, 但與字元陣列不同之處是結尾必須加上一個 ASCII 的 NULL 字元 '\0', 例如 :

char cha[5]={'H','E','L','L','O'};       //一般的字元陣列
char str[6]={'H','E','L','L','O','\0'};   //字串

也可以用雙引號給字元陣列賦值, 這時編譯器會自動在結尾處加上 NULL 字元 :

char str[]="HELLO";   //用雙引號賦值

在 printf() 中輸出字串必須使用 %s 格式, 對應的變數就是陣列名稱; 如果是輸出其中的某個字元則要用 %c 格式, 例如 :

int main() {
    char str[]="HELLO";
    printf("str=%s\n",str);             //輸出整個字串
    printf("str[1]=%c\n",str[1]);    //輸出單一字元
    return 0;
    }

既然字串是一種字元陣列, 指標可以指向陣列, 指向字串的指標稱為字串指標, 此指標必須宣告為 char 類型, 例如

char *ptr="HELLO";       //指向字串的指標
printf("*ptr=%s\n",ptr);   //輸出字串

也可以在迴圈中用 putchar() 輸出字串, 例如 :

int main() {
    char *ptr="HELLO";
    for (; *ptr != '\0'; *ptr++) {    //或 while(*ptr != '\0')  亦可
        putchar(*ptr);
        }
    return 0;
    }

如果是用陣列的話必須用一個索引計數器, 上面的指標就不用 :

int main() {
    char str[]="HELLO";
    int i=0;    //索引計數器
    while(str[i] != '\0') {
        putchar(str[i]);
        ++i;
        }
    return 0;
    }

雖然陣列與指標都能用來處理字串, 但它們最大的不同是, 陣列字串一經賦值即不可再指派新值, 因為陣列是常數指標, 其內容是不可變的, 例如下面的程式碼無法通過編譯 :

    char str[]="HELLO";
    str="WORLD";     //無法通過編譯

因為陣列 str 已經指向 "HELLO" 的開頭, 不能再指向 "WORLD".

而指標是變數, 可隨時指向任何字串, 上面的字串改用指標就可以通過編譯了, 例如 :

    char *ptr="HELLO";   //原指向 "HELLO"
    ptr="WORLD";           //改為指向 "WORLD"

指標除了可以指向陣列外, 也可以指向函數, 與陣列名稱代表陣列的開始位址一樣, 函數名稱代表函數的開始位址, 因此可以將函數名稱指派給指標變數, 使其指向該函數, 留待後面再行測試.


6. 不可指派常數值給指標 : 

直接對指標變數本身或其所指位址指派一個整數常數或字面值 (Literal) 是非常危險與錯誤的用法, 例如 :

int *ptr=1000;  



int *ptr;
ptr=1000;  

此指令在宣告指標變數 ptr 的同時也對其賦值 1000, 由於 ptr 在宣告後, 這位址 1000 可能指向系統或其他程序所使用的記憶體位址, 輕則破壞其他程式之運算結果, 重則導致使保護較不周全之作業系統當機.

事實上這些錯誤用法都無法通過編譯, 如下測試 1 所示 :


測試 1 : 錯誤的指標賦值方式 

#include <stdio.h>

int main() {
    int a=2;
    int *ptr=1000;  
    return 0;
  }

此程式在 TutorialsPoint 編譯會得到下列錯誤訊息 :

$gcc -o main *.c
main.c: In function ‘main’:
main.c:3:14: warning: initialization makes pointer from integer 
                      without a cast [-Wint-conversion]
     int *ptr=1000;
              ^~~~
$main

若在 Dev C++ 編譯則是 :

[Error] invalid conversion from 'int' to 'int*' [-fpermissive]


測試 2 : 顯示未初始化之指標內容

#include <stdio.h>

int main() {
    int *ptr;
    printf("未初始化指標 ptr 位址=%p\n", &ptr);
    printf("未初始化指標 ptr 內容=%p", ptr);
    return 0;
   }

此程式第一個 printf() 顯示用 & 取址運算子取得之指標變數 ptr 本身的位址, 在 64 位元系統為 8 個 bytes; 第二個 printf() 則顯示指標變數之內容. 注意, 雖然指標的內容是整數 (記憶體位址), 但是在 printf() 中顯示指標內容必須使用 %p, 不是 %d.

上面程式在 TutorialsPoint 執行結果未初始化指標內容是 nil (無值) :

$gcc -o main *.c
$main
未初始化指標 ptr 位址=0x7ffedc8ffa08
未初始化指標 ptr 內容=(nil)

而在 Dev C++ 結果是未初始化指標內容是 1, 這是記憶體殘留值 :

未初始化指標 ptr 位址=000000000022FE48   
未初始化指標 ptr 內容=0000000000000001  


可見指標只要一經宣告, 編譯器就會指派一個位址給它, 例如上面的 0x7ffedc8ffa08 與0x22FE48, 此位址每次執行都可能會不同.

存取指標所指之資料須使用取值運算子 * (dereference operator), 例如 :

int a=100;             //宣告一般變數 a 並賦值 100
int *ptr;                 //宣告指向整數資料的指標
ptr=&a;                 //取得 a 的記憶體位址並賦值給指標 ptr (指向 a)
*ptr=50;                //將 a 的值被改為 50 了

這裡 ptr 指標指向一般變數 a, 因此 *ptr=50 就會把 a 的內容改為 50, 例如 :


測試 3 : 利用指標更改所指位址內容

#include <stdio.h>

int main() {
    int a=100;
    printf("a 的位址=%p\n",&a);
    printf("a 的初始值=%d\n",a);
    int *ptr;                        //宣告指標 ptr
    ptr=&a;               //指標 ptr 指向 a
    printf("ptr 的內容=%p\n", ptr);      //ptr 內容=a 的位址
    printf("*ptr 的初始值=%d\n", *ptr);
    *ptr=50;
    printf("*ptr 的新值=%d\n",*ptr);
    printf("a 的新值=%d",a);
    return 0;
   }

執行結果如下 :

a 的位址=0x7ffd5899e0d4
a 的初始值=100
ptr 的內容=0x7ffd5899e0d4 (與 a 的位址一樣)
*ptr 的初始值=100
*ptr 的新值=50
a 的新值=50  (a 的值被改了)


7. 用指標當函數參數進行傳址呼叫

C 語言函數的 return 只能傳回一個值, 如果要傳回多個值必須利用指標或陣列的傳址呼叫才能達成, 在 "C 語言從零開始" 這本書的 11-7 節以及 "最新 C 語言程式設計實例入門" 的 9-3 節提到的兩個變數數值交換函數 swap() 唯一極佳範例, 改寫如下 :

測試 4 : 利用指標傳址呼叫交換兩個變數之數值

#include <stdio.h>

int swap(int *x,int *y) {  //函數的參數為指標 (不需傳回值)
   int tmp=*x;
   *x=*y;
   *y=*x;
   }

int main() {
    int a=5,b=15;
    printf("交換前 : a=%d b=%d\n",a,b);
    swap(&a,&b);    //以傳址呼叫將 a, b 之位址傳給函數
    printf("交換後 : a=%d b=%d\n",a,b);
    return 0;
    }

由於要傳回的值有兩個 (即交換後的 a, b), 因此將 a, b 的位址傳給 swap() 去運算, 因為指標指向了變數的位址, 直接在變數上進行交換動作, 因此也用不到 return 將值傳回來. 執行結果如下 :

交換前 : a=5 b=15
交換後 : a=15 b=15

如果將 swap() 改成如下用傳值呼叫的話, 由於傳進去的是資料的副本, 如果不將資料傳回來的話就是做白工 :

int swap(int x,int y) {  //傳值呼叫 : 無效的數值交換
   int tmp=x;
   x=y;
   y=x;
   }

交換後無法將兩個值同時傳回來等於做白工, 一定要用指標才行.


二. 指標的長度 :

指標變數與一般變數不同, 其儲存的內容是記憶體位址 (整數), 因此不論指標指向哪一種型態的資料, 其所占記憶體大小是固定的, , 即等於 CPU 的位址寬度, 例如 32 位元系統之位址匯流排寬度是 32 位元 (4 個 bytes), 可定址 2^32=4G Bytes 的 DRAM, 其指標變數長度為 4 個 Bytes; 而 64 位元系統位址匯流排為 64 位元 (8 個 bytes), 定址能力為 2^64=16GB 之 DRAM,  其指標變數長度為 8 個 Bytes. 例如 :


測試 5 : 指向各種型態的指標都占用相同大小的記憶體  


#include <stdio.h>

int main() {
    char c;
    int i;
    float f;
    double d;
    char *cptr=&c;
    int *iptr=&i;
    float *fptr=&f;
    double *dptr=&d;
    printf("變數名稱     記憶體位址          占用記憶體 (bytes)\n");
    printf("========    ================   ==================\n");
    printf("   c  \t    %p \t  %d\n", &c, sizeof(c));
    printf("   i  \t    %p \t  %d\n", &i, sizeof(i));
    printf("   f  \t    %p \t  %d\n", &f, sizeof(f));
    printf("   d  \t    %p \t  %d\n", &d, sizeof(d));
    printf(" cptr  \t    %p  \t  %d\n", &cptr, sizeof(cptr));
    printf(" iptr  \t    %p  \t  %d\n", &iptr, sizeof(iptr));
    printf(" fptr  \t    %p  \t  %d\n", &fptr, sizeof(fptr));
    printf(" dptr  \t    %p  \t  %d\n", &dptr, sizeof(dptr));
    return 0;
   }

在 Win10 64 位元系統的 Dev C++ 執行結果如下 :



可見指標內容不管所指資料類型為何都是 8 bytes (64 位元). 在 TutorialsPoint 上也是 8 bytes :




而在樹莓派 B 上則是 4 個 bytes, 因為其 CPU 是 32 位元的.





三. 指標的運算

指標儲存的是記憶體位址, 事實上也就是整數, 若呼叫 printf() 輸出時用 %d 會顯示整數值, 而用 %p 才會顯示位址值, 例如 :

int main() {
    int a=5;
    int *ptr=&a;
    printf("ptr=%d\n",ptr);   //以整數格式輸出指標內容
    printf("ptr=%p\n",ptr);   //以位址格式輸出指標內容
    return 0;
    }

執行結果如下 :

ptr=1065793828
ptr=0x7fff3f86b924

因此除了上述使用 "=" 運算子進行指定 (賦值) 運算外, 指標還可以進行算術運算, 但只限於與整數之加減運算以及遞增遞減運算, 不允許乘除等運算, 因為指標運算之用途只是為了用來計算記憶體位址之位移以存取資料而已, 指標乘除運算並無意義.


1. 指標加減運算 : 

指標的值可以與整數進行加減來改變指標所指向之位址, 但因為指標儲存的是記憶體位址, 不是一般的整數, 因此加減的整數其單位並非 bytes, 而是指標所指之資料型態, 例如當指標指向陣列時, ptr + i 中的 i 是指陣列元素移位個數 :

int a[]={0,1,2,3,4,5};    //宣告一個整數陣列 a
int *ptr=&a[0];      //宣告指標 ptr 指向陣列開頭元素 a[0]
ptr=ptr + 2;                   //指標向下移動 2 個 int 單位, 指向 a[2]

指標 ptr 原先指向 a[0], 指標加 2 並非 ptr 所儲存的位址加 2 個 bytes, 而是加 2 個整數單位, 例如 Dev C++ 中整數是 4 個 bytes, 則 ptr+2 就是指標移位 2*4=8 bytes; 而在樹莓派的 gcc 編譯器, 整數占 2 個 bytes, 則 ptr + 2 就是移位 2*2=4 個 bytes.


測試 6 : 指標加減指向陣列元素

#include <stdio.h>

int main() {
    int a[]={0,1,2,3,4,5};
    printf("元素\t位址\n");
    for (int i=0; i<5; i++) {
        printf("a[%d]\t%p\n",i,&a[i]);
        }
    int *ptr=&a[0];    //指標指向 a[0]
    printf("ptr=%p *ptr=%d\n",ptr,*ptr);
    ptr=ptr + 2;          //指標指向 a[2]
    printf("ptr=%p *ptr=%d\n",ptr,*ptr);
    ptr=ptr - 1;           //指標指向 a[1]
    printf("ptr=%p *ptr=%d",ptr,*ptr);
    return 0;
    }

在 TutorialsPoint 測試結果如下 :

元素 位址
a[0] 0x7fff4003fa10 
a[1] 0x7fff4003fa14
a[2] 0x7fff4003fa18 
a[3] 0x7fff4003fa1c
a[4] 0x7fff4003fa20
ptr=0x7fff4003fa10 *ptr=0
ptr=0x7fff4003fa18 *ptr=2
ptr=0x7fff4003fa14 *ptr=1

TutorialsPoint 的 int 占 4 個 bytes, 陣列的每個元素在記憶體內會在儲存在連續位址中, 因此指標 ptr 向前移動 2 個單位, 實際上位址是由 0x7fff4003fa10 向前移動 8 個 bytes 到 0x7fff4003fa18.

下面範例是參考 "C 語言初學指引第四版" 加以改編 :

測試 7  :  指標加減在各類型資料的位址的移位距離 

#include <stdio.h>

int main() {
    int a;
    short int *p;
    int *q;
    float *r;
    double *s;
    char *t;
    printf("指標移位前位址\n");
    p=(short int*) &a;    //強制轉型為 short int
    q=&a;
    r=(float*) &a;          //強制轉型為 float
    s=(double*) &a;      //強制轉型為 double
    t=(char*) &a;           //強制轉型為 char
    printf("=============\n");
    printf("p=%p\n",p);
    printf("q=%p\n",q);
    printf("r=%p\n",r);
    printf("s=%p\n",s);
    printf("t=%p\n",t);
    printf("指標移位後位址\n");
    p=p+1;
    q=q+1;
    r=r+1;
    s=s+1;
    t=t+1;
    printf("=============\n");
    printf("p=%p\n",p);
    printf("q=%p\n",q);
    printf("r=%p\n",r);
    printf("s=%p\n",s);
    printf("t=%p\n",t);  
    return 0;
    }

這裡用 p, q, r, s, t 五個指標分別指向五個不同的資料類型, 先印出移位前所指向之位址, 再讓指標向前移動一個單位, 然後印出所指向的新位址, 這會因不同資料型態而有所不同. 注意, 因為 a 是 int 類型, 因此除了 q 以外, 其他指標都必須取址後予以強制轉型.

在 TutorialsPoint 測試結果如下 :

指標移位前位址
=============
p=0x7ffd2bd96354
q=0x7ffd2bd96354
r=0x7ffd2bd96354
s=0x7ffd2bd96354
t=0x7ffd2bd96354
指標移位後位址
=============
p=0x7ffd2bd96356  (short int 占 2 個 bytes)
q=0x7ffd2bd96358  (int 占 4 個 bytes)
r=0x7ffd2bd96358  (float 占 4 個 bytes)
s=0x7ffd2bd9635c  (double 占 8 個 bytes)
t=0x7ffd2bd96355  (char 占 1 個 bytes)

可見在 TutorialsPoint 的主機上短整數 short int 類型移位一個單位為 2 個 bytes; 整數 int 為 4 個 bytes; 浮點數 float 為 4 個 bytes; 倍準數 double 為 8 個 bytes; 字元 char 為 1 個 byte.


3. 指標差值運算 :

兩個指向相同資料類型的指標變數可以相減, 得到的差是一個整數, 表示兩個指標間之距離, 其單位是該資料型別的長度. 如果這兩個指標指向同一陣列, 則差值表示兩個指標之間隔了多少元素.

測試 8 : 兩個指標間之距離

#include <stdio.h>

int main() {
    int a[]={0,1,2,3,4,5};
    printf("元素\t位址\n");
    for (int i=0; i<5; i++) {
        printf("a[%d]\t%p\n",i,&a[i]);
        }
    int *ptr1=&a[1];
    int *ptr2=&a[2];
    printf("ptr1=%p *ptr1=%d\n",ptr1,*ptr1);
    printf("ptr2=%p *ptr2=%d\n",ptr2,*ptr2);
    printf("ptr2-ptr1=%d\n",ptr2-ptr1);
    printf("ptr1-ptr2=%d",ptr1-ptr2);
    return 0;
    }

TutorialsPoint 執行結果 :

元素 位址
a[0] 0x7ffdcf7a8540
a[1] 0x7ffdcf7a8544
a[2] 0x7ffdcf7a8548
a[3] 0x7ffdcf7a854c
a[4] 0x7ffdcf7a8550
ptr1=0x7ffdcf7a8544 *ptr1=1
ptr2=0x7ffdcf7a8548 *ptr2=2
ptr2-ptr1=1  
ptr1-ptr2=-1  

可見 ptr2 與 ptr1 位址差距為 4 bytes, 由於 int 長度為 4 bytesm, 所以 ptr2-ptr1 為 1 個整數單位.

注意, 兩個相減的指標必須是指向同類型, 否則無法通過編譯. 其次, 兩個指標相減是允許的, 但兩個指標的相加運算卻是不允許的, 因為兩個位址相加沒有意義. 例如將上面的 ptr2-ptr1 改為 ptr2+ptr1 將無法通過編譯.


4. 遞增 (++) 與遞減 --) 運算 :

遞增與遞減運算就是指標加 1 或減 1 運算, 各有前置 (先做運算) 與後置 (後做運算) 之分 :

  1. 前遞增 ++ptr : 先加 1 再處理
  2. 後遞增 ptr++ : 先處理再加 1
  3. 前遞減 --ptr : 先減 1 再處理
  4. 後遞減 ptr-- : 先處理再減 1

++ 與 -- 是單元運算子, 如果只是做變數的單元運算, 前置或後置對運算結果相同. 例如 :

#include <stdio.h>

int main() {
    int a=1;
    ++a;   //前遞增單元運算 a=2
    printf("a=%d\n",a);
    a=1;
    a++;   //後遞增單元運算 a=2
    printf("a=%d\n",a);
    return 0;
    }

不論前遞增或後遞增, 單元運算的結果都是 a=2 :

$gcc -o main *.c
$main
a=2
a=2

結果無差別的原因是上述單元運算沒有再進行指定處理.

有差異的情況是單元運算後還進一步再做處理 (即指定運算), 例如 :

#include <stdio.h>

int main() {
    int a=1;
    int b=++a;    //前遞增後指定給 b, 故 b=2
    printf("b=%d\n",b);
    a=1;
    b=a++;         //指定給 b 後再做後遞增, 故 b=1
    printf("b=%d\n",b);
    return 0;
    }

結果 :

b=2
b=1

參考 :

30天C語言巔峰之路(Day13:運算子-遞增與遞減運算子)

指標變數做遞增遞減運算時, 由於指標的取址 & 與取值 * 運算子與遞增 ++ 遞減 -- 運算子優先等級一樣, 都是第二級 (第一級是括號), 因此當 &, *, ++, --, () 混合運算時需仔細判斷, 例如 *ptr++ 是先取 ptr 所指之值後, 再將 ptr 指向下一個資料下列; 而 *++ptr 則是先將指標增量指向下一個資料後, 再取出其值.

下面的範例使用一個陣列 a 與指標 ptr 來測試 *, ++, () 的 7 種混合運算 :

int a[5]={0,1,2,3,4};


測試 9 : 指標 *, ++,  () 的混合運算


#include <stdio.h>

int main() {
    int a[5]={0,1,2,3,4};
    printf("元素\t值\t位址\n");
    for (int i=0; i<5; i++) {
        printf("a[%d]\t%d\t%p\n",i,a[i],&a[i]);
        }  
    int *ptr=a;
    printf("ptr=a=%p\n",ptr);
    int x=*(ptr++);
    printf("x=*(ptr++) x=%d ptr=%p *ptr=%d\n",x,ptr,*ptr);
    x=*(++ptr);
    printf("x=*(++ptr) x=%d ptr=%p *ptr=%d\n",x,ptr,*ptr);
    x=(*ptr)++;
    printf("x=(*ptr)++ x=%d ptr=%p *ptr=%d\n",x,ptr,*ptr);
    x=++(*ptr);
    printf("x=++(*ptr) x=%d ptr=%p *ptr=%d\n",x,ptr,*ptr);
    x=*ptr++;
    printf("x=*ptr++ x=%d ptr=%p *ptr=%d\n",x,ptr,*ptr);
    x=*++ptr;
    printf("x=*++ptr x=%d ptr=%p *ptr=%d\n",x,ptr,*ptr);
    x=++*ptr;
    printf("x=++*ptr x=%d ptr=%p *ptr=%d\n",x,ptr,*ptr);
    return 0;
    }

在 TutorialsPoint 執行結果如下 :

元素 值 位址
a[0] 0 0x7ffe4aa8a9e0
a[1] 1 0x7ffe4aa8a9e4
a[2] 2 0x7ffe4aa8a9e8
a[3] 3 0x7ffe4aa8a9ec
a[4] 4 0x7ffe4aa8a9f0
ptr=a=0x7ffe4aa8a9e0
x=*(ptr++) x=0 ptr=0x7ffe4aa8a9e4 *ptr=1
x=*(++ptr) x=2 ptr=0x7ffe4aa8a9e8 *ptr=2
x=(*ptr)++ x=2 ptr=0x7ffe4aa8a9e8 *ptr=3
x=++(*ptr) x=4 ptr=0x7ffe4aa8a9e8 *ptr=4
x=*ptr++ x=4 ptr=0x7ffe4aa8a9ec *ptr=3
x=*++ptr x=4 ptr=0x7ffe4aa8a9f0 *ptr=4
x=++*ptr x=5 ptr=0x7ffe4aa8a9f0 *ptr=5

結果摘要如下表 :

 運算式 相當於 執行結果 說明
 x=*(ptr++); x=*ptr;
 ptr=ptr+1;
 x=0
 *ptr=1
 先取值 *ptr 給 x, 再遞增 ptr
 x=*(++ptr); ptr=ptr+1;
 x=*ptr;
 x=2
 *ptr=2
 先遞增 ptr 再取值給 x
 x=(*ptr)++; x=*ptr;
 *ptr=*ptr+1;
 x=2
 *ptr=3
 先取值 *ptr 給 x, 再把 *ptr 加 1
 x=++(*ptr); *ptr=*ptr+1;
 x=*ptr; 
 x=4
 *ptr=4
 把 *ptr 加 1 同時設給 x
 x=*ptr++; x=*ptr;
 ptr=ptr+1;
 x=4
 *ptr=3
 先取值 *ptr 給 x, 再把 *ptr 加 1
 x=*++ptr; ptr=ptr+1;
 x=*ptr;
 x=4
 *ptr=4
 先遞增 ptr 再取值給 x,  與 x=*(++ptr) 同
 x=++*ptr x=*ptr+1; x=5
 *ptr=5
 把 *ptr 加 1 同時設給 x, 與 x=++(*ptr) 同

可知 *(++ptr) 與 *++ptr 作用相同, 而 ++(*ptr) 與 ++*ptr 作用相同, 因為 ++, --, * 這些第二優先等級的運算子, 運算順序為由右向左之故.


5. 指標比較運算 :

兩個指標可以在 if 敘述中使用 ==, >, <, >=, <=, 或 != 等關係運算子進行比較運算, 用來判斷所儲存的記憶體位址是否相等, 或何者之記憶體位址較高或較低.


#include <stdio.h>

int main() {
    int a,b;
    int *ptr1=&a;
    int *ptr2=&b;
    printf("ptr1=%p\n",ptr1);
    printf("ptr2=%p\n",ptr2);
    if (ptr2 > ptr1) {
        printf("變數 b 位址高於變數 a 位址\n");
        }
    else {
        printf("變數 a 位址高於變數 b 位址\n");
        }
    return 0;
    }

在 Win10 上的 Dev C++ 執行結果如下 :

ptr1=000000000022FE3C
ptr2=000000000022FE38
變數 a 位址高於變數 b 位址

可見 Dev C++ 在編譯時, 變數位址是依據宣告先後由高位址往低位址指派. 在 TutorialsPoint 上執行的結果也是如此 :

$gcc -o main *.c
$main
ptr1=0x7ffe5964392c
ptr2=0x7ffe59643928
變數 a 位址高於變數 b 位址

在 Arduino Nano 編譯也是高位址先指派 :

void setup() {
    Serial.begin(9600);
    int a,b;
    int *ptr1=&a;
    int *ptr2=&b;
    Serial.println((long)ptr1);
    Serial.println((long)ptr2);
    if (ptr2 > ptr1) {
        Serial.println("變數 b 位址高於變數 a 位址\n");
        }
    else {
        Serial.println("變數 a 位址高於變數 b 位址\n");
        }
    }
void loop() {}

結果如下 :

2298
2296
變數 a 位址高於變數 b 位址

注意, 必須指向相同資料類型的指標才可以互相比較, 若將上面程式中的 b 與 ptr2 改為 float 類型, 則編譯時將會失敗, 出現如下錯誤訊息 (Dev C++) :

comparison between distinct pointer types 'float*' and 'int*' lacks a cast [-fpermissive]


四. 指標的指標 :  

指標變數用來儲存資料的記憶體位址, 指標除了用來指向一般變數, 也可以用來指向另一個指標變數, 稱為雙重指標. C 語言允許多重指標, 但因較複雜而用得不多. 宣告多重指標的方式是使用連續的 "*", 例如 :

char ***ppp;  //宣告三重指標 ppp

從下面範例可以觀察三重指標儲存的位址之關係 :

測試 10 : 三重指標

#include <stdio.h>

int main() {
    int a=100;
    int *ptr1=&a;             //指向整數的指標
    int **ptr2=&ptr1;      //指向整數指標的指標
    int ***ptr3=&ptr2;    //指向整數指標的指標的指標
    printf("&a=%p a=%d\n",&a,*ptr1);
    printf("&ptr1=%p ptr1=%p *ptr1=%d\n",&ptr1,ptr1,*ptr1);
    printf("&ptr2=%p ptr2=%p *ptr2=%p\n",&ptr2,ptr2,*ptr2);
    printf("&ptr3=%p ptr3=%p *ptr3=%p\n",&ptr3,ptr3,*ptr3);
    return 0;
    }

TutorialsPoint 執行結果如下 :

$gcc -o main *.c
$main
&a=0x7fff5e45bb2c a=100
&ptr1=0x7fff5e45bb20 ptr1=0x7fff5e45bb2c *ptr1=100
&ptr2=0x7fff5e45bb18 ptr2=0x7fff5e45bb20 *ptr2=0x7fff5e45bb2c
&ptr3=0x7fff5e45bb10 ptr3=0x7fff5e45bb18 *ptr3=0x7fff5e45bb20
在上面的範例中, 指標 ptr1 指向一個整數變數 a, 儲存的是 a 的位址 7fff5e45bb2c, 而指標 ptr2 又指向 ptr1; 儲存的是ptr1 的位址 7fff5e45bb20; 指標 ptr3 又指向 ptr2, 儲存的是 ptr2 的位址 7fff5e45bb18, 形成三重指標. 由於最終是指向一個整數, 因此這三個指標都必須宣告為 int.



五. 函數指標 : 

上面測試指標賦值時提到, 指標也可以指向函數,  稱為函數指標. 因為函數名稱在符號表中儲存的是函數在記憶體中的開始位址, 如同陣列名稱是陣列的開始位址一樣, 可以直接將函數名稱指派給指標, 使其指向該函數. 函數指標可以讓我們在程式中動態地呼叫不同的函數.

1. 函數指標的宣告與呼叫方式  :

函數指標的宣告方式如下 :

傳回值類型 (*指標名稱)([參數]);     //參數可有可無

函數指標的賦值只要將函數名稱指派給指標即可 (不必用 &) :

指標名稱=函數名稱;  

利用函數指標呼叫函數有兩種方式, 如果有用取值運算子 * 一定要用括弧將其與指標名稱一起括起來, 否則指標名稱會先跟後面的參數群結合  :

(*指標名稱)([參數]);  或   指標名稱([參數]);         


例如 :

int (*fptr)(int x,int y);     //宣告具有兩個 int 參數, 傳回值為 int 的函數指標 fptr

注意, *fptr 外面的括弧是必須的, *fptr 要先結合, 表示 fptr 是一個指標變數; 然後再與後面的括號結合, 表示它指向一個函數.


測試 11 : 函數指標與函數位址

#include <stdio.h>

int add(int x,int y) {
   return x+y;
   }
int sub(int x,int y) {
   return x-y;
   }

int main() {
    printf("add=%p\n",add);
    printf("sub=%p\n",sub);
    int (*fptr)(int x,int y);
    fptr=add;      //指標 fptr 指向 add() 函數
    printf("fptr=add=%p\n",fptr);
    printf("ftpr(8,3)=add(8,3)=8+3=%d\n",fptr(8,3));   //用函數指標名稱呼叫函數
    fptr=sub;      //指標 fptr 指向 sub() 函數
    printf("fptr=add=%p\n",fptr);
    printf("ftpr(8,3)=sub(8,3)=8-3=%d\n",fptr(8,3));
    return 0;
    }

在 TutorialsPoint 執行結果如下 :

add=0x4004d7
sub=0x4004eb
fptr=add=0x4004d7
ftpr(8,3)=add(8,3)=8+3=11
fptr=add=0x4004eb
ftpr(8,3)=sub(8,3)=8-3=5
可見透過指派函數名稱可以用同一名稱 fptr() 呼叫不同的函數, 雖然這跟直接呼叫 add() 與 sub() 沒兩樣, 但若配合條件判斷可達到動態呼叫不同函數之效.

如同陣列名稱代表陣列開始位址, 函數名稱也是代表函數開頭位址, 也可以當作引數傳遞給另一個函數, 這時接收此引數的參數就是要宣告為函數指標. 下列範例是我參考 "易習 C 語言入門" 第 8-5 節範例改寫而來 :

測試 12 : 用函數指標接收函數參數

#include <stdio.h>

int add(int x,int y) {
   return x+y;
   }
int sub(int x,int y) {
   return x-y;
   }
int compute(int x,int y,int (*fptr)(int x,int y)) {   //以函數指標接收函數引數
    return (*fptr)(x,y);
    }

int main() {
    printf("compute(8,3,add)=%d\n",compute(8,3,add));   //以函數 add 當引數
    printf("compute(8,3,sub)=%d\n",compute(8,3,sub));    //以函數 sub 當引數
    return 0;
    }

在 TutorialsPoint 執行結果如下 :

compute(8,3,add)=11
compute(8,3,sub)=5

結果是一樣的. 注意, 呼叫函數指標不一定要用 * 運算子, 可以直用函數指標的名稱, 所以上面的 compute() 也可以改為 :

int compute(int x,int y,int fptr(int x,int y)) {   //以函數指標接收函數引數
    return (*fptr)(x,y);
    }

2. 函數指標陣列 : 

函數指標也可以組成一個陣列, 稱為函數指標陣列, 這樣可以透過索引在迴圈中呼叫不同函數, 其宣告方式如下, 其實只是將單一函數指標改成陣列而已 :

傳回值類型 (*指標名稱[長度])([參數]);     //參數可有可無


例如 :

測試 13 : 函數指標陣列

#include <stdio.h>

int add(int x,int y) {
   return x+y;
   }
int sub(int x,int y) {
   return x-y;
   }
int mul(int x,int y) {
   return x*y;
   }

int main() {
    int (*ptr[3])(int x,int y);   //宣告一個函數指標陣列
    ptr[0]=add;   //函數指標賦值為函數名稱以指向函數
    ptr[1]=sub;
    ptr[2]=mul;
    char op[]={'+','-','*'};
    int opr1=15;
    int opr2=6;
    for (int i=0; i<3; i++) {
        printf("%d%c%d=%d\n",opr1,op[i],opr2,ptr[i](opr1,opr2));    //透過函數指標呼叫函數
        }
     return 0;
    }

在 TutorialsPoint 執行結果如下 :

15+6=21
15-6=9
15*6=90

3. 泛型 void :



int compare(void *a,void *b,int (*comp)(void *,void *)) {
    return (*comp)(a,b);
    }

int main() {
    int i1=50; i2=100;
    char c1='C',c2='C';
    int *iptr1=&i1;
    int *iptr2=&i2;
    char *cptr1=&c1;
    char *cptr2=&c2;
 

    return 0;
    }

~未完待續

參考 :

指標與動態記憶體配置介紹
为什么说指针是 C 语言的精髓?

母親三周年忌日

今天是 921 大地震 18 周年, 也是母親三周年忌日. 時間真的過得好快.

2017年9月20日 星期三

高師大新版校友借書證

這幾天收到母校高師大圖書館的來信, 說現行押金制校友借書證 (條碼式) 到年底就不能用了, 明年起須改用年費制新版 IC 卡感應式借書證, 半年 600 元, 一年 1000 元, 亦即以後校友押金免費借書的時代即將結束了 :

親愛的校友,您好:圖書資訊處依據105學年度第2學期圖書資訊委員會會議決議通過,本處校友服務要點(內容請詳圖資處首頁相關法規連結https://lis.nknu.edu.tw/zh/about/related_rules/606-2017-06-21-15-25-00)。本處推出全新校友服務,全面開放校友只需經首次身分認證後,即可免費入館閱覽並大幅提升校友借書冊數及相關使用權益,歡迎校友們踴躍利用。此外,同步發送新服務啟用通知給歷年已申辦校友借書證的校友們知悉。

新式校友借書證相關作法如下:
1. 原舊版紙卡校友借書證,依圖委會決議通過,即日起使用期限至106年12月31日止,校友們可於圖書館服務櫃台申辦保證金退費程序。欲持續使用圖書與空間借用服務之校友們,歡迎您線上申辦新版感應式校友借書證。相關連結請參考高師大校首頁>公用系統>校友借書證,網址:https://sso.nknu.edu.tw/userLogin/login.aspx?cUrl=/default.aspx

2. 免費開放所有持高師大正式畢業學位證書之校友們,首次於圖書館服務櫃檯綁訂自備之悠遊卡或一卡通後,即可持卡入館閱覽利用,無需排隊等候。

3. 新版感應式校友借書證推出利多項新服務:
(1)全新推出年費制校友借書證服務,提升借書權益:
A.半年期繳交費用600元,借書證自領證日起半年內有效。
B.年費校友借書證:每年繳年費1,000元整,借書證自領證日起一年內有效。
C.尊榮終身校友借書證:一次繳交費用8,000元整。
(2)大幅提升校友館藏借書冊數與各項資源利用:可借閱20冊圖書資料、2件多媒體視聽資料及開放圖書續借服務、提供空間借用服務等。


考慮使用率不高, 決定年底申請退還押金, 以後就不再向母校借書了.

2017年9月19日 星期二

2017 年第 37 周記事

這幾天忙著整理 C 語言筆記, 每天晚上二哥自修回來要教他一點 C 語言語法; 菁菁自覺有點胖了, 晚上拉我去小學操場跑幾圈, 晚上能用的時間很少. 但今天已週二了, 上周的周記必須寫一寫.

本周回鄉下發現小舅已在菜園搭起種小番茄的 M 形架, 據他說第二天手痠死了, 早知道這麼累, 花幾千塊就可以買到很多小番茄, 何必自己種. 如果我有時間種的話, 只要用竹竿綁一綁就好了, 不需要這麼專業啦 :




百香果棚旁邊那棵母親種植多年的四季芒果, 今年終於開花結果了, 但可惜她已無法親嚐. 上週成熟一個, 爸說味道不錯, 與海頓相比略為失色而已, 總之還是海頓好吃. 我仔細翻找, 發現還有兩三顆尚未成熟 :


周日把鄉下書房稍事整理, 主要是將二哥的書桌清理一番, 桌下的舊雜誌搬到樓上, 以前學機器人用的零件箱現在也沒再用了, 通通移到樓上去. 下午小舅打電話來說他家的監視器還是無法連線, 所以清到一半又趕去協助, 剩下的只好本周回去再整理.

Processing 入門 :互動式圖形實作介紹

2017年9月16日 星期六

鄉村基礎木作第七課 : 完成框架組裝

上週六木樂園休息, 我周一到周四晚上又在忙, 已經兩周沒去做木工了. 早上還沒九點就到了, 剛好老師正在開大門. 今天換回劉老師指導, 進度是組裝整個收納櫃框架. 在那之前先裝上支撐活動層板的螺絲孔, 用塑膠槌將螺母敲進之前鑽好的側板內側的四個孔.

接下來是用夾板從左右兩側夾住立起來的側板, 先將底板上膠沿著畫好的線黏在側板上, 喬好位置後用夾子夾緊, 然後用電動起子上螺絲. 然後依序向上鎖兩個抽屜要的底板, 並在抽屜底板上方用蚊子釘固定抽屜導軌.


用四個 L 形板左右夾住側板以便固定底板


鎖好底板左右各四顆螺絲後, 鬆開固定夾, 將內側 2 個 L 形板上移到抽屜底板下緣位置用固定夾夾住, 開始時不用夾太緊, 因為要微調對準抽屜底板下緣線, 定位完後再夾緊 :


定位抽屜底板只要內側 2 個 L 形板


定位好後抽屜底板兩側上膠放在 L 形板上, 用木槌稍微調整定位後用大型固定夾將抽屜底板前後都夾緊, 再用電動起子上螺絲鎖緊 :


抽屜底板 (下) 定位固定


抽屜底板 (下) 定位好後上膠鎖螺絲


不要急著再往上鎖抽屜底板 (上), 因為抽屜底板 (下) 的上方還要裝上抽屜導軌木條, 注意, 前方木條要留抽屜前板空間, 這樣抽屜才能在收進去時剛好與前緣切齊 :


抽屜底板 (下) 膠黏木條導軌後打蚊子釘固定

這樣就可以裝抽屜底板 (上) 了, 由於兩側板會夾緊, 所以應該就不再需要 L 形板幫忙了, 定位夾緊鎖螺絲後同樣要裝導軌木條 :

固定抽屜底板 (上)

接著是裝底板下方的裝飾板, 使用固定夾夾住底板與側板, 定位後上膠鎖螺絲 :


鎖裝飾板


好啦! (今天進度)

組起來後原來放的地方擺不下了, 只好放在工作台旁邊 :




最後是用木塞沾膠後將鎖螺絲的孔塞住, 用鐵鎚敲進螺絲孔內, 凸出來的部分用鋸子彎曲鋸片小心鋸掉, 這我倒是第一次學到. 剩下的就是用砂紙磨平即可. 今天時間不夠, 剩下天板與背板下次上課再裝了. 大概在兩堂課就可以完成課程了.