程式之美-微軟技術面試心得1. 第1章
遊戲之樂
——遊戲中碰到的題目
研究院舉辦過幾屆手足球(foosball)公開賽,第一屆的冠軍是一位文靜的女實習生
2. 程式之美-微軟技術面試心得
這一章的題目原本預計叫做「Problem Solving」——運用所學的知識解決問題,直譯為
「解決問題」,但似乎不太理想。事實上,這裡面大部分題目都是和遊戲相關的,因此,
本章改名為「遊戲之樂」。這些題目從遊戲和作者平時遇到的有趣問題出發,向程式設
計師提出挑戰。
個人電腦(PC)在蹣跚起步的時候,就被當時的主流觀點視為玩具。PC 的確有各種各
樣的遊戲,電腦上的遊戲是給人玩的,如果你願意,CPU 也可以讓人「玩」。
筆者曾經用「CPU 使用率」這個問題問了十幾個應聘者,一個典型的模式是:
我: 筆試考得怎麼樣?發揮了多少實力?
答: 我不習慣在紙上寫程式,平時都在電腦上寫……
我: 那你對 Windows、作業系統這些東西熟悉嗎?
答: 還算是相當熟悉……
我: 好,那你是否能在這台筆記型電腦上幫我解決一個問題——讓 CPU 的使用率劃出
一條水平線,比如就在 50%的地方。
這個時候可以觀察應聘者如下好幾個方面:
應聘者面對這個陌生問題時,如何開始分析。
有人知道觀察「工作管理員」如何執行,有人在紙上寫寫畫畫,有人明顯沒有
什麼想法。
當提示可以在網路上搜尋資料時,應聘者如何尋找資料,如何學習。
比如,有一位學生很快地用快捷鍵在 IE 中開啟了幾個 Tab 視窗,然後在每個視
窗輸入不同的搜尋關鍵字。當我提示在 MSDN 上找一些函數的時候,有些人根
本不知道 MSDN 網站應該怎麼用。有些人反覆讀了函數的說明,仍不知如何下
手。
在電腦上是如何寫程式,如何進行除錯的。
有人能很嫺熟地使用 C/C#的各種語言特性,很快地寫出程式,有人寫的程式編
譯了好幾次都不能通過,對編譯錯誤束手無策。程式第一次執行的時候,工作
管理員的 CPU 使用率不如預期,這時候有人就十分慌亂,在程式中瞎改一通,
希望能「矇」對。有人則有條理地分析,最後找到並解決問題。
2
3. 第 1 章:遊戲之樂
我想,45 分鐘下來,應聘者的思考能力、學習能力、技術能力如何,應該都很清楚了。
行還是不行,雙方也都明白了。
這一章的其他題目大多和遊戲有關,同學們在玩《接龍》、《俄羅斯方塊》,甚至《魔
獸世界》的時候,有沒有產生好奇心——這個程式為什麼這麼酷,如果是我來寫,應該
怎麼做?有沒有把好奇心轉化為行動?
喜歡玩電腦、會玩電腦的人,也會運用電腦解決實際問題,這也是我們要找的人才。
3
4. 程式之美-微軟技術面試心得
1.1 ★★★
讓 CPU 使用率曲線聽你指揮
寫一個程式,讓使用者決定 Windows 工作管理員(Task Manager)的 CPU 使用率。
程式越精簡越好,電腦語言不限。例如,可以實現下列三種情況:
CPU 的使用率固定在 50%,為一條直線。
CPU 的使用率為一條直線,但是實際使用率由命令行參數決定(參數範圍 1~
100)。
CPU 的使用率狀態是一條正弦曲線。
4
5. 第 1 章:遊戲之樂
分析與解法1
有一名學生寫了如下的程式碼:
while(true)
{
if(busy)
i++;
else
}
然後她就陷入了苦苦思索:else 要做什麼呢?怎樣才能讓電腦不做事情呢?CPU 使用
率為 0 的時候,到底是什麼東西在用 CPU?另一名學生花了很多時間構想如何「深入
核心,以控制 CPU 使用率」——可是事情真的有這麼複雜嗎?
MSRA IEG(Microsoft Research Asia, Innovation Engineering Group)的一些實習生寫了
各種解法,他們寫的簡單程式可以達到如圖 1-1 所示的效果。
圖 1-1:寫程式控制 CPU 使用率呈現正弦曲線
作者注:當面試的同學聽到這個問題的時候,很多人都有點意外。我把我的筆記型電腦交給他們說,這是開卷考
試,你可以上網查資料,做什麼都可以。大部分面試者在電腦上的第一個動作就是上網搜尋「CPU 使用率 50%」
這樣的關鍵字,當然沒有找到什麼直接的結果。不過本書出版以後,情況可能就不一樣了。
5
6. 程式之美-微軟技術面試心得
看來這並不是不可能完成的任務。讓我們仔細地回想一下寫程式時曾經碰到的問題,
如果我們不小心寫了一個無窮迴圈,CPU 使用率就會跳到最高,而且一直保持在
100%。我們也可以開啟工作管理員 ,實際觀察一下它是如何變動的。憑肉眼觀察,
2
它大約是 1 秒鐘更新一次。一般情況下,CPU 使用率會很低。但是,當使用者執行一
個程式,進行一些複雜操作的時候,CPU 的使用率會急遽升高。當使用者移動滑鼠時,
CPU 的使用率也有小幅度的變化。
當工作管理員報告 CPU 使用率為 0 的時候,誰在使用 CPU 呢?透過工作管理員的「處
理程序(Process)
」一頁可以看到,System Idle Process 佔用了 CPU 空閒的時間——這
時候大家該回憶起在「作業系統」這門課上所學到的一些知識了吧。系統中有那麼多
,它們什麼時候能「閒下來」呢?答案很簡單,這些程式或者在等待使
process(行程)
用者的輸入,或者在等待某些事件的發生 ,或者主動進入休眠狀態 。 3 4
在工作管理員的一個更新週期內,CPU 忙(執行應用程式)的時間和更新週期總時間
的比率,就是 CPU 的使用率,也就是說,工作管理員中顯示的是每個更新週期內 CPU
使用率的統計平均值。因此,我們可以寫一個程式,讓它在工作管理員的更新期間內
一會兒忙,一會兒閒,然後透過調整忙/閒的比例,就可以控制工作管理員中顯示的
CPU 使用率。
解法一:簡單的解法
要控制 CPU 的使用率曲線,就需要讓 CPU 在一段時間內(根據 Task Manager 的採樣率)
跑 busy 和 idle 兩個不同的迴圈(loop)
,從而透過不同的時間比例,來調節 CPU 使用率。
Busy loop 可以透過執行空迴圈來實現,idle 可以透過 Sleep()來實現。
問題的關鍵在於如何控制兩個 loop 的時間,我們先試驗一下 Sleep 一段時間,然後迴
圈 n 次,計算 n 的值。
那麼,對於一個空迴圈 for(i = 0; i < n; i++);又該如何計算這個最合適的 n 值
呢?我們都知道 CPU 執行的是機器指令,而最接近於機器指令的語言是組合語言,所
以我們可以先把這個空迴圈簡單地寫成如下組合語言程式碼後,再進行分析:
如果應聘者從來沒有研究過工作管理員,最好還是不要在簡歷上寫「精通 Windows」比較好。
例如 WaitForSingleObject()。
可以透過 Sleep()來實現。
6
7. 第 1 章:遊戲之樂
loop:
mov dx i ;將 i 置入 dx 暫存器
inc dx ;將 dx 暫存器加 1
mov i dx ;將 dx 中的值放回 i
cmp i n ;比較 i 和 n
jl loop ;i 小於 n 時則重複迴圈
假設這段程式碼要執行的 CPU 是 P4 2.4Ghz(2.4 * 10 的 9 次方個時鐘週期/每秒)。
現在 CPU 每個時鐘週期可以執行兩個以上的程式碼,那麼我們就取平均值兩個,於是
讓(2,400,000,000 * 2)/5=960,000,000(迴圈/秒),也就是說,CPU 1 秒鐘可以執行這
個空迴圈 960,000,000 次 不過 我們還是不能單純地將 n = 960,000,000 然後 Sleep(1000)
。 , ,
了事。如果我們讓 CPU 工作 1 秒鐘,然後休息 1 秒鐘,波形很有可能就是鋸齒狀的——
先達到一個峰值(>50%),然後跌到一個很低的使用率。
我 們 嘗 試 著 降 低 兩 個 數 量 級 , 令 n = 9,600,000 , 而 睡 眠 時 間 對 應 改 為 10 毫 秒
(Sleep(10))
。選擇 10 毫秒是因為它不大也不小,比較接近 Windows 排程時間的間
隔。如果選得太小(如 1 毫秒)
,則會造成執行緒頻繁地被喚醒和暫停,無形中又增加
了核心時間的不確定性。最後我們可以得到程式碼清單 1-1。
程式碼清單 1-1
int main()
{
for(; ; )
{
for(int i = 0; i < 9600000; i++)
;
Sleep(10);
}
return 0;
}
在不斷調整 9,600,000 的參數後,我們就可以在一台指定的機器上獲得一條大致穩定的
50% CPU 使用率直線。
使用這種方法要注意兩點影響:
儘量減少 sleep/awake 的頻率,減少作業系統核心排程程式的干擾。
儘量不要呼叫系統函數(比如 I/O 這些 privilege instruction)
,因為它也會導致很
多不可控制的核心執行時間。
7
8. 程式之美-微軟技術面試心得
此方法的缺點也很明顯:不能在不同的機器上通用。一旦換了一個 CPU,我們又得重
新計算 n 值。有沒有辦法動態地瞭解 CPU 的運算能力,然後自動調節忙/閒的時間比
呢?請看下一個解法。
解法二:使用 GetTickCount()和 Sleep()
我們知道 GetTickCount()可以得到「系統啟動到現在」所經歷時間的毫秒值,最多能
夠統計到 49.7 天。我們可以利用 GetTickCount()來判斷 busy loop 要迴圈多久,程式碼
如清單 1-2 所示。
程式碼清單 1-2
int busyTime = 10; // 10 ms
int idleTime = busyTime; // same ratio will lead to 50% cpu usage
Int64 startTime = 0;
while(true)
{
startTime = GetTickCount();
// busy loop
while((GetTickCount() - startTime) <= busyTime)
;
// idle loop
Sleep(idleTime);
}
這兩種解法都是假設目前系統上只有當前程式在執行,但實際上,作業系統中有很多
程式會同時執行各式各樣的任務,如果此刻其他行程(process)使用了 10% 的 CPU,
那我們的程式應該只能使用 40%的 CPU,這樣才能達到 50%的效果。
怎麼做呢?這就要用到另一個工具來幫忙——Perfmon.exe。
Perfmon 是從 Windows NT 開始就包含在 Windows 管理工具組中的專業檢測工具之一
(如
圖 1-2 所示)。Perfmon 可獲取有關作業系統、應用程式和硬體的各種效能計數器(perf
counter)。Perfmon 的用法相當直接,只要選擇你所要檢測的物件(如處理器、RAM 或
硬碟),然後選擇效能計數器(如監視磁片的平均佇列長度)即可。
8
9. 第 1 章:遊戲之樂
圖1-2:系統監視器(Perfmon)
我 們 可 以 寫 程 式 來 查 詢 Perfmon 的 值 , Microsoft .Net Framework 提 供 了
PerformanceCounter 這一物件,可以方便地得到各種效能資料,包括 CPU 的使用率。
例如下面這個程式(見程式碼清單 1-3)。
解法三:能動態調整的解法
程式碼清單 1-3
// C# code
static void MakeUsage(float level)
{
PerformanceCounter p = new PerformanceCounter(quot;Processorquot;,
quot;%Processor Timequot;, quot;_Totalquot;);
if(p==NULL)
{
return
}
while(true)
{
if(p.NextValue() > level)
System.Threading.Thread.Sleep(10);
9
10. 程式之美-微軟技術面試心得
}
}
上面的解法能方便地處理各種 CPU 使用率參數 這個程式可以解答前面提到的問題 2。
,
有了前面的知識累積,我們應該可以讓工作管理員畫出優美的正弦曲線了,見程式碼清
單 1-4。
解法四:正弦曲線
程式碼清單 1-4
// C++ code to make task manager generate sine graph
#include quot;Windows.hquot;
#include quot;stdlib.hquot;
#include quot;math.hquot;
const double SPLIT = 0.01;
const int COUNT = 200;
const double PI = 3.14159265;
const int INTERVAL = 300;
int _tmain(int argc, _TCHAR* argv[])
{
DWORD busySpan[COUNT]; // array of busy times
DWORD idleSpan[COUNT]; // array of idle times
int half = INTERVAL / 2;
double radian = 0.0;
for(int i = 0; i < COUNT; i++)
{
busySpan[i] = (DWORD)(half + (sin(PI * radian) * half));
idleSpan[i] = INTERVAL - busySpan[i];
radian += SPLIT;
}
DWORD startTime = 0;
int j = 0;
while(true)
{
j = j % COUNT;
startTime = GetTickCount();
while((GetTickCount() - startTime) <= busySpan[j])
;
Sleep(idleSpan[j]);
10
11. 第 1 章:遊戲之樂
j++;
}
return 0;
}
討論
如果機器是多 CPU,上面的程式會出現什麼結果?如何在多個 CPU 時顯示同樣的狀
態?例如,在雙核心的機器上,如果讓一個單執行緒的程式進行無窮迴圈,能讓兩個
CPU 的使用率達到 50%的水準嗎?為什麼?
多 CPU 的問題首先需要獲得系統的 CPU 資訊 可以使用 GetProcessorInfo()獲得多
。
處理器的資訊,然後指定行程在哪一個處理器上執行。其中指定執行使用的 CPU 是透
過 SetThreadAffinityMask()函數。
另外,還可以使用 RDTSC 指令獲取當前 CPU 核心執行週期數。
在 x86 平臺上定義函數:
inline __int64 GetCPUTickCount()
{
__asm
{
rdtsc;
}
}
在 x64 平臺上定義:
#define GetCPUTickCount() __rdtsc()
使用 CallNtPowerInformation API 得到 CPU 頻率,從而將週期數轉化為毫秒數,例如程
式碼清單 1-5 所示。
程式碼清單 1-5
_PROCESSOR_POWER_INFORMATION info;
CallNTPowerInformation(11, // query processor power information
NULL, // no input buffer
0, // input buffer size is zero
11
12. 程式之美-微軟技術面試心得
&info, // output buffer
Sizeof(info)); // outbuf size
__int64 t_begin = GetCPUTickCount();
// do something
__int64 t_end = GetCPUTickCount();
double millisec = ((double)t_end –(double)t_begin)
/(double)info.CurrentMhz;
RDTSC 指令讀取目前 CPU 的週期數,在多 CPU 系統中,這個週期數在不同的 CPU 之
間基數不同,頻率也有可能不同。利用從兩個不同的 CPU 得到的週期數來計算,會得
出沒有意義的值。如果執行緒在執行中被調動到了不同的 CPU,就會出現上述情況。
可用 SetThreadAffinityMask 避免執行緒遷移。另外,CPU 的頻率會隨系統供電及負荷
情況有所調整。
總結
能幫助你瞭解當前執行緒/行程/系統效能的 API 大致有以下這些。
Sleep():這個方法能讓當前執行緒「停」下來。
WaitForSingleObject():自己停下來,等待某個事件發生。
GetTickCount():有人把 Tick 翻譯成「滴答」,很傳神。
QueryPerformanceFrequency()、QueryPerformanceCounter():讓你取得更
精細的 CPU 資料。
timeGetSystemTime():是另一個得到高精度時間的方法。
PerformanceCounter:效能計數器。
GetProcessorInfo()/SetThreadAffinityMask() :遇到多核心的問題怎麼辦
呢?這兩個方法能夠幫你更有效地控制 CPU。
GetCPUTickCount():想拿到 CPU 核心執行週期數嗎?試試這個方法吧。
瞭解並應用了上面的 API,就可以考慮在履歷表中寫上「精通 Windows」了。
12
13. 第 1 章:遊戲之樂
1.2 ★★★
中國象棋將帥問題
下過象棋的人都知道,雙方的「將」和「帥」相隔遙遠,而且它們不能碰面。在象棋
殘局中,許多高手能利用這一規則走出巧妙的必殺招數。假設棋盤上只有「將」 「帥」
和
二子(如圖 1-3 所示)(為了下面敘述方便,我們約定用 A 表示「將」 表示
,B 「帥」 。
)
將
帥
圖 1-3:只有將帥的棋盤
A、B 二子被限制在己方 3×3 的格子裡移動。例如,在如上的表格裡,A 被正方形{d10,
f10, d8, f8}包圍,而 B 被正方形{d3, f3, d1, f1}包圍。每一步,A、B 分別可以橫向或縱向移
動一格,但不能沿對角線移動。另外,A 不能面對 B,也就是說,A 和 B 不能處於同一
縱向直線上(如 A 在 d10 的位置,那麼 B 就不能在 d1、d2 以及 d3 的位置上)。
請寫一個程式,輸出 A、B 所有合法位置,並要求在程式碼中只能使用一個變數。
13
14. 程式之美-微軟技術面試心得
分析與解法
問題的本身並不複雜,只要把所有 A、B 互相排斥的條件列舉出來就可以完成本題的要
求。由於本題要求只能使用一個變數,所以必須先想清楚在寫程式時,有哪些資訊需
要儲存,並且儘量高效率地儲存資訊。稍微思考一下,可以知道這個程式的框架大致
上是:
走遍 A 的位置
走遍 B 的位置
判斷 A、B 的位置組合是否滿足要求。
如果滿足,則輸出。
因此,需要儲存的是 A、B 的位置資訊,並且每次迴圈都要更新。為了能夠進行判斷,
首先需要建立一個邏輯的座標系統,以便檢測 A 何時會面對 B。這裡我們想到的方法
是用 1~9 的數字,按照行的優先順序來表示每個格點的位置(如圖 1-4 所示)。這樣,
只需要用「求餘數運算」就可以得到當前的列號,從而判斷 A、B 是否互斥。
圖 1-4:用 1~9 的數字表示 A、B 的座標
第二,題目要求只用一個變數,但是我們卻要儲存 A 和 B 兩個子的位置資訊,該怎麼
辦呢?
可以先把已知變數類型列舉一下,然後做些分析。
對於 bool 類型,是沒有辦法做任何擴展的,因為它只能表示 true 和 false 兩個值;而
byte 或 int 類型,它們能夠表達的資訊則更多。事實上,對本題來說,每個子都只需要
9 種可能值,就可以表達它的全部位置。
一個 8 位元的 byte 類型能夠表達 28=256 個值,所以用它來表示 A、B 的位置資訊綽綽有
餘,因此,可以把這個位元組的變數(設為 b)分成兩部分。用前面的 4 bit 表示 A 的位
置,用後面的 4 bit 表示 B 的位置,那麼 4 個 bit 可以表示 16 種可能值,這已經足夠了。
14
15. 第 1 章:遊戲之樂
問題在於如何使用 bit 級的運算,將資料從這一 byte 變數的左邊和右邊分別存入和讀出。
下面是做法:
將 byte b(10100101)的右邊 4 bit(0101)設為 n(0011):
首先清除 b 右邊的 bits,同時保持左邊的 bits:
11110000(LMASK)
& 10100101(b)
---------------
10100000
然後將上一步得到的結果與 n 做「或」運算
10100000(LMASK & b)
^ 00000011(n)
------------
10100011
將 byte b(10100101)左邊的 4 bit(1010)設為 n(0011):
首先,清除 b 左邊的 bits,同時保持右邊的 bits:
00001111(RMASK)
& 10100101(b)
-----------
00000101
現在,把 n 移動到 byte 資料的左邊
n << 4 = 00110000
然後對以上兩步得到的結果做「或」運算,即可得到最終結果。
00000101(RMASK & b)
^ 00110000(n << 4)
-----------
00110101
得到 byte 資料的右邊 4 bits 或左邊 4 bits(e.g. 10100101 中的 1010 以及 0101):
清除 b 左邊的 bits,同時保持右邊的 bits:
00001111(RMASK)
& 10100101(b)
-----------
00000101
15
16. 程式之美-微軟技術面試心得
清除 b 右邊的 bits,同時保持左邊的 bits:
11110000(LMASK)
& 10100101(b)
-----------
10100000
將結果右移 4 bits
10100000 >> 4 = 00001010
最後的挑戰是如何在不宣告其他變數條件的前提下,建立一個 for 迴圈。可以重複利
用 1byte 的儲存單元,把它作為迴圈計數器,並用前面提到的存取和讀入技術進行操
作。還可以用巨集來抽象化程式碼,例如:
for (LSET(b, 1); LGET(b) <= GRIDW * GRIDW; LSET(b, (LGET(b) + 1)))
解法一
程式碼清單 1-6
#define HALF_BITS_LENGTH 4
// 這個值是記憶儲存單元長度的一半,在這道題裡是 4bit
#define FULLMASK 255
// 這個數字表示一個全部 bit 的 mask,以二進位表示是 11111111。
#define LMASK (FULLMASK << HALF_BITS_LENGTH)
// 這個巨集表示左 bits 的 mask,以二進位表示是 11110000。
#define RMASK (FULLMASK >> HALF_BITS_LENGTH)
// 這個數字表示右 bits 的 mask,以二進位表示是 00001111。
#define RSET(b, n) (b = ((LMASK & b) ^ n))
// 這個巨集將 b 的右邊設置成 n
#define LSET(b, n) (b = ((RMASK & b) ^ (n << HALF_BITS_LENGTH)))
// 這個巨集,將 b 的左邊設置成 n
#define RGET(b) (RMASK & b)
// 這個巨集得到 b 的右邊的值
#define LGET(b) ((LMASK & b) >> HALF_BITS_LENGTH)
// 這個巨集得到 b 的左邊的值
#define GRIDW 3
// 這個數字表示將帥移動範圍的行寬度。
#include <stdio.h>
#define HALF_BITS_LENGTH 4
#define FULLMASK 255
#define LMASK (FULLMASK << HALF_BITS_LENGTH)
#define RMASK (FULLMASK >> HALF_BITS_LENGTH)
#define RSET(b, n) (b = ((LMASK & b) ^ n))
16
17. 第 1 章:遊戲之樂
#define LSET(b, n) (b = ((RMASK & b) ^ (n << HALF_BITS_LENGTH)))
#define RGET(b) (RMASK & b)
#define LGET(b) ((LMASK & b) >> HALF_BITS_LENGTH)
#define GRIDW 3
int main()
{
unsigned char b;
for(LSET(b, 1); LGET(b) <= GRIDW * GRIDW; LSET(b, (LGET(b) + 1)))
for(RSET(b, 1); RGET(b) <= GRIDW * GRIDW; RSET(b, (RGET(b) + 1)))
if(LGET(b) % GRIDW != RGET(b) % GRIDW)
printf(quot;A = %d, B = %dnquot;, LGET(b), RGET(b));
return 0;
}
輸出
格子的位置用 N 來表示,N = 1, 2, …, 8, 9,依照行的優先順序,如圖 1-5 所示。
「將」(A)的格子
「帥」(B)的格子
圖 1-5:「將」與「帥」的格子位置
A = 1, B = 2 A = 4, B = 2 A = 7, B = 2
A = 1, B = 3 A = 4, B = 3 A = 7, B = 3
A = 1, B = 5 A = 4, B = 5 A = 7, B = 5
A = 1, B = 6 A = 4, B = 6 A = 7, B = 6
A = 1, B = 8 A = 4, B = 8 A = 7, B = 8
A = 1, B = 9 A = 4, B = 9 A = 7, B = 9
17
18. 程式之美-微軟技術面試心得
A = 2, B = 1 A = 5, B = 1 A = 8, B = 1
A = 2, B = 3 A = 5, B = 3 A = 8, B = 3
A = 2, B = 4 A = 5, B = 4 A = 8, B = 4
A = 2, B = 6 A = 5, B = 6 A = 8, B = 6
A = 2, B = 7 A = 5, B = 7 A = 8, B = 7
A = 2, B = 9 A = 5, B = 9 A = 8, B = 9
A = 3, B = 1 A = 6, B = 1 A = 9, B = 1
A = 3, B = 2 A = 6, B = 2 A = 9, B = 2
A = 3, B = 4 A = 6, B = 4 A = 9, B = 4
A = 3, B = 5 A = 6, B = 5 A = 9, B = 5
A = 3, B = 7 A = 6, B = 7 A = 9, B = 7
A = 3, B = 8 A = 6, B = 8 A = 9, B = 8
考慮了這麼多因素,總算得到了本題的一個解法,但是 MSRA 裡卻有人說,下面的一
小段程式碼也能達到同樣的目的:
BYTE i = 81;
while(i--)
{
if(i / 9 % 3 == i % 9 % 3)
continue;
printf(「A = %d, B = %dn」, i / 9 + 1, i % 9 + 1);
}
但是很快又有另一個人說他的解法才是效率最高的:
程式碼清單 1-7
struct {
unsigned char a:4;
unsigned char b:4;
} i;
for(i.a = 1; i.a <= 9; i.a++)
for(i.b = 1; i.b <= 9; i.b++)
if(i.a % 3 != i.b % 3)
printf(「A = %d, B = %dn」, i.a, i.b);
讀者能自己證明一下嗎? 5
這一題目由微軟亞洲研究院工程師 Matt Scott 提供,他在學習象棋時想出了這個題目,後來一位應聘者給了比他
的「正解」簡明很多的答案,他們現在成了同事。
18