Metaprogramming

Metaprogramming

設計一支程式來製造程式碼,令該程式碼充分運用程式語言自身擁有的能力,輕鬆地、更有效率地完成更多事情。

舉個例子。例如現在有一個四則運算式需要計算:

5 + 8 * (2 - 3) + 7 * -6 / (2 - 1) + 1

這是一個經典的問題,大家應該不陌生。身經百戰的演算法設計高手,必然不假思索說出:stack,把所有符號依序放入stack,依照運算符號的優先次序push和pop,一下子就算完了。聽來簡單,但實做起來還是頗麻煩。

這裡要介紹的是更輕鬆、更強悍的方法:寫程式製造一支會進行四則運算的程式。大家都知道C/C++的語法當中,就有四則運算的語法了。現在來設計一支程式,製作出四則運算的程式碼吧!

如果輸入方才的四則運算式,就會產生如下程式碼,檔名為temp.cpp。

然後編譯temp.cpp、執行一下,就有答案了。您甚至可以把編譯、執行的指令,統統寫進程式碼當中:

程式設計師藉由Metaprogramming能做更多事情!現在的資訊領域當中,應用到Metaprogramming的也相當多,各位可以再去蒐集相關資料來閱讀。:)

Quine

寫一支程式,其功能是輸出本身程式碼。

給一題尚未有測試資料的題目,目前還不能送。

UVa 724

Array Indexing

Array Indexing

「索引」可說是電腦的奇技!一個元素存放到陣列之後,不論是在陣列的哪個地方,只要利用索引值(index),就能在一瞬間找到元素。

大多數的演算法都運用了「索引」的技巧,讓程式執行速度更快。

以下介紹索引的幾種運用方式。是我自己歸類整理的,並不是標準。

一、定位

概念為:將物件放入陣列中,array[位置] = 物件。

當元素很多時,我們可以放到陣列裡。我們只要記錄索引值,依舊可以常數時間得到元素。

範例:求最大值。將元素連續地放入陣列,若想紀錄一元素,僅需一索引值。

範例:求子字串。將元素連續地放入陣列,若想紀錄一區間,僅需頭尾的索引值。

範例:連續數字和。將元素連續地放入陣列,利用問題本身的數學性質以及索引值,快速得到答案。

範例:求中位數。將元素依照大小順序並連續地放入陣列,利用索引值得到位於中間的元素。

範例:二分搜尋法(Binary Search)。將元素依照大小順序並連續地放入陣列,然後夾擠索引值。如果使用的是鏈接串列,因為元素們都沒有索引值,就無法使用二分搜尋法。

範例:二元樹(Binary Tree)。元素的索引值對應到樹的結構,是一種特殊的定位。

範例:堆疊(stack)、佇列(queue)。元素連續地放入陣列,然後以改變索引值的方式,來動態增減堆疊及佇列的元素。

二、歸類並標記

概念為:物件直接作為陣列的索引值,array[物件] = 物件的屬性。

範例:正整數集合。物件是:正整數,物件的屬性是:是否在集合裡頭出現。

範例:統計英文字母出現次數。物件是英文字母,物件的屬性是英文字母的出現次數。

範例:計數排序法(Counting Sort)。索引值的大小順序,剛好也是元素的大小順序,故可用於排序。

範例:雜湊表(hash table)。元素的索引值由特殊方法決定,是一種特殊的歸類。

三、轉換

概念為:array[物件] = 另一個物件。類似函數的概念。

範例:取代(Substitution)、移位(Transposition)。取代和移位是密碼學的基礎概念。取代是文字的轉換,移位則是位置的轉換。

範例:page table。作業系統的機制,可將虛擬位址轉換成實體位址,是位址的轉換。

附錄:定址的時間複雜度

當索引值大小為N時,有人認為定址的時間複雜度是O(log2N),也有人認為是O(1)。這兩種說法都是有其依據的。

以數學的觀點來看:N共有log2N個位元,用二元樹的概念,依照各個位元的數值是0或是1進行分支,分支到底後就完成定址了。所以定址的時間複雜度是O(log2N)。

以電路的觀點來看:一顆中央處理器可以平行處理32位元(現在已有64位元),只要是介於0到2^32-1的索引值,都可以在1單位時間完成定址,而不必用32單位時間來完成定址。所以定址的時間複雜度是O(1)。

討論演算法的時間複雜度時,我們傾向假設定址的時間複雜度是O(1)。

附錄:定址的範圍

方才提到一顆中央處理器可以平行處理32位元,理論上可以定址到2^32以內的位址。一個位址一般擁有1byte的記憶體大小,所以我們利用定址方式,可以運用的記憶體就有2^32 * 1byte = 4GB 這麼多。

但是作業系統會保留一些位址、預留一些記憶體空間以維持系統運作,所以使用者實際可以運用的記憶體其實不到4GB。

當記憶體沒有插到4GB的時候,作業系統利用一種叫做virtual memory的技術,以硬碟空間補足記憶體不足4GB的部份。

位址是連續不斷的,我們寫程式也都直接假設位址對應到的記憶體空間是連續不斷的,然而實際上並不是連續的。作業系統運用一種叫做paging的技術,藉由page table,讓記憶體看起來是連續的。

Bitwise Operation

Bitwise Operator

歡迎來到二進位的世界。電腦資料都是以二進位儲存,想當然程式語言的變數也都是以二進位儲存。在C/C++當中有幾個位元運算子:<< SHIFT LEFT、>> SHIFT RIGHT、& AND、| OR、^ XOR、~ NOT,可以對變數進行位元運算。接下來要介紹位元運算的一些用途。

UVa 10469 10264

<< SHIFT LEFT
>> SHIFT RIGHT

這兩個運算子的功能主要是移動一個變數中的所有位元,位元向左/向右移動之後,最高位/最低位的位元會消失,最低位/最高位的位元補0:

5 << 1 = 10	// 00101 的全部位元向左移動一位數變成 01010。
5 << 2 = 20	// 00101 的全部位元向左移動兩位數變成 10100。
5 >> 1 = 2	// 00101 的全部位元向右移動一位數變成 00010。
5 >> 2 = 1	// 00101 的全部位元向右移動一位數變成 00001。

在十進位當中,當全部位數向左移動一位時,數值大小會變成原來的十倍,向左移動兩位時,會變成原來的百倍。這種情形在二進位也是成立的,當全部位元向左移動一位時,會變成原來的兩倍,向左移動兩位時,會變成原來的四倍。至於往右移動也是類似道理,變成了除法而已。

由於電腦進行位元運算比乘法、除法運算快上許多,所以有很多專業的程式設計師,會利用位元運算來取代乘法、除法運算。優點是程式執行效率增加,缺點是程式碼可讀性變低:

& AND

0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1

&的功能是將兩個變數對應的位元進行AND邏輯運算,然後產生新變數。

   00000000000000000000000001110100 -> 116
 & 00000000000000000000000000101001 -> 41
 ----------------------------------
   00000000000000000000000000100000 -> 32

&的特色,就是可以判斷出位元是不是1。例如我們想要數一個變數有幾個位元是1:

| OR

0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1

|的功能是將兩個變數對應的位元進行OR邏輯運算,然後產生新變數。

   00000000000000000000000001110100 -> 116
 | 00000000000000000000000000101001 -> 41
 ----------------------------------
   00000000000000000000000001111101 -> 125

|的特色,就是把位元強制標記成1。例如我們想要把五位數標成1:

^ XOR

0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0

^的功能是將兩個變數對應的位元進行XOR邏輯運算,然後產生新變數。

   00000000000000000000000001110100 -> 116
 ^ 00000000000000000000000000101001 -> 41
 ----------------------------------
   00000000000000000000000001011101 -> 93

^的特色,就是把位元的0和1顛倒。例如我們想要顛倒第五位數:

~ NOT

~ 0 = 1
~ 1 = 0

~的功能是顛倒一個變數每一個位元的0和1。

 ~ 00000000000000000000000000000011 -> 3
 ----------------------------------
   11111111111111111111111111111100 -> -4

延伸閱讀:>>的陷阱

當變數是正值或零時,>>會在左端最高位補零;當變數是負值時,>>會在左端最高位補一,讓變數保持負數。

當用到最高位元、也用到>>的時候,必須改用unsigned int、unsigned long long int等變數型態,才會得到正確結果。

延伸閱讀:unsigned的陷阱

一般來說,實施位元運算時,程式設計師喜歡採用unsigned變數型態,避免>>的陷阱。

此時要特別小心,勿將unsigned與signed型態的變數一起進行計算,很容易出現意想不到的事情。例如拿unsigned與signed變數進行加減法、比大小;例如把unsigned變數傳入函數,但是函數參數是signed變數。

Bitwise Trick

Bitwise Trick

http://kuoe0.ch/1436/

http://kuoe0.ch/1503/

http://www.aggregate.org/MAGIC/

http://graphics.stanford.edu/~seander/bithacks.html

實作時要特別小心運算子優先次序。沒有特地括括號的情況下,次序高的先結合。

交換兩個int變數

計算有幾個位元是1(32位元整數)

顛倒位元順序(32位元整數)

窮舉所有子集合

一個變數代表一個集合(即是bitset),每個位元分別代表每個集合元素。

檢查缺少的正整數

陣列裡放入1到10的正整數,最後發現少了一個,請找出少了哪一個。

使用Counting Sort雖然時間複雜度低,但是空間複雜度高。

8 Queen Problem(八皇后問題)

http://www.matrix67.com/blog/archives/266

使用回溯法。變數代替陣列,位元代替陣列元素。

UVa 11195

Nim(捻)

這是兩個人玩的小遊戲。桌面上有N堆石子,兩個人輪流從桌上取走石子,每人每次只能取其中一堆石子,至少取一顆,至多搬走整堆。最後淨空桌面的人勝利,請判斷誰會勝利。

這個問題的解答,跟每堆石子的數量多寡有關。神奇的是,竟然跟二進位表示法有很大關連: http://oddest.nc.hcc.edu.tw/math152.htm

UVa 10165

Gray Code

Josephus Problem

模數為二的時候,答案為:去除n的最高位元,然後整體左移一位,最後加上一。

Fast Inverse Square Root(平方根倒數)

原理是牛頓法,避開了開根號和除法運算,節省了很多計算時間。除此之外還使用了很多神奇的技巧,包括電腦結構和程式語言的冷知識。

3D繪圖經常要計算平方根倒數。相傳是原作者在開發電腦遊戲「雷神之鎚」時,所發明的方法。

http://en.wikipedia.org/wiki/Fast_inverse_square_root

http://www.lomont.org/Math/Papers/2003/InvSqrt.pdf

Pointer

前言

我自己有一套簡單的解讀方式,儘管這不一定是C/C++設計者的原意,但是應該能讓大家確實的掌握指標的概念。

記憶體與位址

談指標之前,必須簡述一下記憶體與位址的概念。我們可以把記憶體簡單地想成一條很長很長的0與1字串,總長度是固定的。

接著我們要替這條長長的字串訂立位址,就像是替一條公路設立里程碑。一開始是位址是零,然後每八個字,就讓位址增加一單位。

註:早期的CPU一次可讀八個字,所以設定八個字為一單位。而現在的CPU一次可讀三十二字,更新穎的CPU一次可讀六十四字,但是都仍然維持八個字為一單位。

位址通常都會用16進位表示,數值開頭再加個0x,表示是16進位數字。

變數

程式當中所有宣告的變數均會存放到記憶體當中的某一段,變數的數值會轉換成二進位,以0與1字串形式儲存。

編譯器和作業系統會幫我們安排適當的地點,讓這些變數不會重疊在一起。另外,如果是宣告陣列,在記憶體當中一定是連續的一段,中間不會被隔開。

沒有變數的地方,我們不能隨意取值和設值,否則會讓程式立刻中止。

至於有變數但是沒有設定初始值的地方,無法預測是0或1,我們不能隨意取值,否則會產生不可預期的結果,甚至讓程式當掉。

想要取得變數儲存的數值,只要直接用變數名稱就行了。

想要獲得變數所在的位址,只要在變數名稱前面加&,就能算出變數的位址。

儲存位址的變數型態

我們假設C/C++有一種變數型態叫做address,此變數型態儲存的數值,是一個位址。

C/C++的指標,概念上等同於address變數型態。無論是哪一種型態的指標,儲存的數值,都是一個位址,沒有差別。

現在我們可以把任何一個變數的位址,儲存在指標之中了。這時候指標的型態發揮用處了:編譯器會判斷「指標型態」與「變數型態多一個*」兩者一不一致,當不一致時會告知我們錯誤訊息。

儲存位址的變數,本身也是一個變數呀,當然它也有位址了。我們也可以把這個位址,用另一個位址變數存起來。

位址進行比大小運算

我們可以讓兩個位址比大小,比較原則就跟一般的整數比大小一樣。

位址進行減法運算

我們可以讓兩個位址相減,相減的結果是一個int變數。

註:我們無法直接讓兩個位址相加,除非自行將位址轉型成整數。

位址進行加整數、減整數運算

我們可以讓位址跟整數相加,讓位址減掉整數(但是不能讓整數減掉位址),以及使用加加與減減運算。

這時候指標的型態總算是有用處了,C/C++很聰明,會根據「指標型態少一個*」的型態所占的記憶體大小,來決定加與減的基本倍率。

位址進行反運算

我們可讓位址進行NOT運算,規則與一般整數進行NOT運算一樣,結果是一個int變數。

位址進行解參考運算

我們可以從一個位址開始,抓一段連續的記憶體,當作一個變數。只要在一個位址前面加上*就行了。(這個*與宣告變數時用到的*意義不同,也與乘法運算的*意義不同。)

這個時候指標型態再度發揮功用,C/C++會根據「指標型態少一個*」的型態,從該位址開始,截取該型態大小的記憶體當作一個變數,並自動設定好變數型態。

重點回顧

1. 在變數前面加&,就能得到變數所在位址。變數型態多一個*。
2. 不管是哪種指標,其實都是儲存位址的變數型態。
3. 位址的運算(指標的運算)
	1. 設值運算(assign):=
	2. 比較運算(compare):< > == >= <= !=
	3. 減法運算(subtract):-
	4. 加整數、減整數:+ - 前++ 後++ 前-- 後--。不能整數減位址。
	5. 反運算(not):前面加!
	6. 解參考(dereference):前面加*
	7. 參考(reference):前面加&。存位址的變數也是個變數,既是變數就有位址。
	8. 索引(index): [],請見下面介紹。
	其他運算,必須自行轉型成整數後,才有辦法算。
4. 編譯器會做型態檢查,參考與解參考時,會拿「指標型態少一個*」的型態做判斷。

附錄:Endianness

下面的程式碼的執行結果,跟0與1在記憶體內排放的順序很有關係,看你使用的電腦系統是使用Big-Endian還是Little-Endian,印出來的結果會不一樣。

Endianness的詳細介紹:http://libai.math.ncu.edu.tw/bcc16/pool/1.33.shtml

註:一般家用電腦都是採用Little-Endian。這裡所畫的圖都是Big-Endian,是為了讓各位比較容易理解。

附錄:memset

我們都知道C的標準函式庫有一個函式叫做memset,不管要被設值的資料是什麼型態,都是以1 byte為單位來設值。寫成程式碼大概是這樣:

附錄:void*

C語言有一種特殊的指標是void*。其實他也是儲存一個位址,唯一的差別是它不能進行解參考。C++則已經捨棄亦沒有必要使用void*。

附錄:同一行宣告許多指標

如果想在同一行宣告許多指標,則從第二個變數名稱開始,都要額外加上星號才行。這是一個不太直覺的規定。既然古人發明C時就這樣規定,我們只得依從。

附錄:指標的指標的指標,解參考再解參考再解參考,取位址。

各位讀過上面文章之後,應該可以輕易弄清楚這些事情背後的原理,而不是去背記*和&可以相互抵銷這種東西。

註:有些面試主考官最喜歡問這樣的問題,好似懂了這些東西就是會寫程式。這個問題不是在考驗你會不會寫程式,這是在考驗你眼睛好不好。

附錄:動態記憶體

宣告動態記憶體,由於實用性與技術上的考量,編譯器沒辦法直接給個變數名字,所以只好給變數的位址,湊合著用。

位址加整數、減整數後,進行解參考運算

a[x]這個語法,表面上看起來是得到a陣列第x格。事實上[]的意思是「指標加減整數後,進行解參考運算」,a[x]其實是*(a+x)的意義,a和x其中之一為整數、另外之一為位址變數。會有這種語法是因為易讀、方便。

Array

陣列變數型態

陣列是一種變數型態,要宣告一個陣列變數並不困難。

C/C++宣告陣列變數的語法非常不直覺,造就許多亂象。既然事實已經無法改變,也只能接受它了。

比較特別的是,一般的變數我們都能知道變數裡所存放的值,只要使用變數的名稱就能得到變數值。然而我們無法得知陣列變數裡所存放的值,事實上我們也不需要用到此值。

陣列變數的指標

陣列變數既然是一個變數,當然也擁有記憶體位址了。同樣地,使用&就可以取得記憶體位址。

陣列變數會自動改變變數型態

http://c-faq.com/aryptr/aryptrequiv.html

使用陣列變數時,編譯器會自動幫忙把陣列變數改變成另一種型態的變數,並且設定其數值為陣列第零格所在的記憶體位址。

只有三種情況,編譯器會保留陣列變數的原本模樣。

搞得這麼複雜,是因為編譯器想盡辦法讓我們不能更動陣列變數儲存的數值。畢竟陣列是用來規劃大量變數的儲存位置,要是隨便更動陣列變數的話,牽一髮動全身,記憶體就大亂了。

另一方面,改變成指標形態之後,要存取陣列元素就方便多了。請參考前面Pointer章節提到的各種運算。

存取陣列元素

初始化陣列變數、初始化陣列元素

我們不能更動、也看不到陣列變數儲存的數值!只有編譯器和作業系統有權力初始化陣列變數,而我們只能初始化陣列元素。

附帶一提,在新版本的C/C++當中,struct、vector也都可以使用大括號初始化元素,相當方便。這就留給讀者自行研究吧。

陣列元素是字元(也就是字串)

字串是一個char陣列。

前面提到,陣列變數會自動改變變數型態。凡是使用char[]型態的變數時,編譯器一樣會把變數型態改變為char*。

另外一定要提的是,C++ I/O遇到char*變數時,會雞婆地處理後續字元;而不是當作一個普通的記憶體位址。

附錄:string literal

說到了字串,就不得不提一下string literal。雙引號括起的字串,稱做string literal,是一個陣列變數,而且編譯器會把陣列元素設定成常數,我們不能更動其值。

http://c-faq.com/aryptr/aryptr2.html

陣列元素是指標

我們也可以宣告一個存位址的陣列。

這樣的陣列變數,也可以用&得到記憶體位址。然而變數型態相當複雜,不實用,又難背,這裡就略過不提了。

陣列元素是陣列

這個太瘋狂了。我一點也不想介紹。反正就是多維陣列。

得到陣列大小

使用sizeof關鍵字就可以得到陣列大小。注意到,回傳的變數型態是size_t,一般來說是一個無號整數;回傳的變數型態絕對不是int。

有一種情況要特別小心:函數的參數寫成陣列變數的模樣,但是編譯器事實上會幫忙改成指標變數。此時用sizeof得到的不是陣列大小而是指標大小。

附錄:動態記憶體

上個章節提到,宣告動態記憶體,編譯器給不出變數名稱,只能給個指標將就著用。不過在新版本的C/C++當中,已經可以給出變數名稱了。

附錄:heap與stack

編譯器進行記憶體管理,有兩個概念稱做heap和stack,分別是指整個記憶體的前端、後端這兩塊區域。heap通常用來存放程式當中一開始就建立好的變數,以及存放動態記憶體;stack用來存放執行過程中新建立的變數。

然而heap和stack的實際運作情形,在不同的編譯器和作業系統可能有一些差異,似乎沒有一個絕對的說法。

當記憶體用量過大,造成heap與stack兩塊區域相撞,此時稱做stack overflow,程式會當機。

Function

http://www.newty.de/fpt/index.html

C/C++函式庫的運用

sstream - 讀取一行不知道有多少個的數字

sstream當中的istringstream物件,可以以近似於cin的方式來讀取一個string變數內含的資料。

sstream的意義為string stream,也就是說,把string變數看待成stream。至於istringstream的i,應該就是指input之意吧。

下面的程式可以讀入一行不知道個數為多少個的數字,並輸出這些數字的總和。

sstream - 讀取不知道有多少行的輸入、並且做tokenize

UVa 483

cstdio - 數字轉字串,字串轉數字

C的標準函式庫提供了兩個非常好用的函式,可以快速的轉換字串成為數值。

UVa 10427

iostream iomaip - 八、十、十六進位數的輸出入

就算使用者輸入ABC或abc(十六進位表示法),compiler還是可以將之轉換成十進位數字,存到num裡面。

十六進位時,輸入的數字有0x或0X開頭也可以(不要把0打成英文字母o或O了)。

附帶一提,因為iomanip已經建好了hex oct dec等關鍵字,所以用setiosflags(ios::hex)是沒有任何效果的。【有待商榷】

就算使用者輸入2e3(科學記號表示法),compiler還是可以將之轉換成十進位數字,存到num裡面。

UVa 537

string cstring - 字串運算

一、讀字串,直到遇見空白、換行、檔案結束為止。

二、讀字串,直到一定數量,或者遇見空白、換行、檔案結束為止。

三、讀一行。

四、讀全部。

五、讀到特定字元為止。

六、交換。

七、長度。

八、比大小。

九、字串後面接字串。

set - 儲存字串

algorithm - 排序

sort()為Quick Sort,stable_sort()為Merge Sort。

排序基本資料型態的方法。

排序自訂資料型態的方法有兩種寫法。

ctime - 計時

ctime - 亂數

climits - 變數型態的極值

CHAR_BIT	char變數的記憶體大小(bits)    8
MB_LEN_MAX	一個字元的記憶體大小(byte)    1(英文系統) 2(中文系統)
SCHAR_MIN	有號char變數的下限值          -128
SCHAR_MAX	有號char變數的上限值          127
UCHAR_MAX	無號char變數的上限值          255
CHAR_MIN	char變數的下限值              -128 或 0
CHAR_MAX	char變數的上限值              127 或 255
SHRT_MIN	short int變數的下限值         -32768
SHRT_MAX	short int變數的上限值         32767
USHRT_MAX	無號short int變數的上限值     65535
INT_MIN		int變數的下限                 -2147483648
INT_MAX		int變數的上限值               2147483647
UINT_MAX	無號int變數的上限值           4294967295
LONG_MIN	long int變數的上限值          -2147483648
LONG_MAX	long int變數的下限值          2147483647
ULONG_MAX	無號long int變數的下限值      4294967295

附帶一提< limits.h >和< climits >是屬於C的函式庫,C++另有推出< limits >。

UVa 465

cassert - 檢查程式有沒有問題

assert()可以用來檢查程式中的變數數值正不正確。

在程式執行的期間,一旦執行至assert()的地方,若是assert()括號之中的敘述句不成立,就會跳出程式有問題的視窗。若沒有跳出任何程式有問題的視窗,就意味著程式成功的通過了所有assert()的檢查。

下面的這段程式碼利用了assert(),藉以檢查queue的運算是否如預期所料。

typeinfo - 印出變數型態的名稱

利用typeid(變數).name()這個語法,可以得到該變數的變數型態。如果該變數是一個物件,則會得到該物件所屬的class名稱。範例程式碼如下所示:

VC++和gcc同時能使用long long的方法