書名: 51單片機C語言開發教程作者名: 劉理云本章字數: 9230字更新時間: 2019-01-04 20:13:41
2.9 函數
一個較大的程序一般應分為若干個程序模塊,每一個模塊用來實現一個特定的功能。所有的高級語言中都有子程序這個概念,用子程序來實現模塊的功能。在C語言中,子程序的作用是由函數來完成的。一個C程序可由一個主函數和若干個子函數構成。由主函數調用其他函數,其他函數也可以互相調用。同一個函數可以被一個或多個函數調用任意多次。圖2-16是一個程序中函數調用的示意圖。

圖2-16 函數調用示意圖
在程序設計中,常將一些常用的功能模塊編寫成函數,放在函數庫中供公共選用。要善于利用函數,以減少重復編寫程序段的工作量。
printstar和print_message都是用戶定義的函數名,分別用來輸出一排星號和一行信息。
說明:
①一個源程序文件由一個或多個函數組成。一個源程序文件是一個編譯單位,即以源文件為單位進行編譯,而不是以函數為單位進行編譯。
②一個C程序由一個或多個源程序文件組成。對較大的程序,一般不希望全放在一個文件中,而將函數和其他內容(如預定義)分別放到若干個源文件中,再由若干源文件組成一個C程序。這樣可以分別編寫、分別編譯,提高調試效率。一個源文件可以為多個C程序公用。
③C程序的執行從main函數開始,調用其他函數后流程回到main函數,在main函數中結束整個程序的運行。
④所有函數都是平行的,即在定義函數時是互相獨立的,一個函數并不屬于另一函數,即函數不能嵌套定義,但可以互相調用,但不能調用main函數。
⑤從用戶使用的角度看,函數有兩種:
a.標準函數,即函數庫。這是由系統提供的,用戶不必自己定義這些函數,可以直接使用它們。應該說明,每個系統提供的庫函數的數量和功能不同,當然有一些基本的函數是共用的。
b.用戶自己定義的函數,以解決用戶的專門需要。
⑥從函數的形式看,函數分兩類:
a.無參函數。如例2-22中的printstar和print_message就是無參函數。在調用無參函數時,主調函數并不將數據傳送給被調用函數,一般用來執行指定的一組操作。無參函數可以返回或不返回函數值,但一般以不返回函數值的居多。
b.有參函數。在調用函數時,在主調函數和被調用函數之間有參數傳遞,也就是說,主調函數可以將數據傳給被調用函數使用,被調用函數中的數據也可以返回來供主調函數使用。
2.9.1 函數定義的一般形式
①無參函數的定義形式。
類型標識符 函數名( )
例2-22中的printstar和print_message函數都是無參函數。用“類型標識符”指定函數值的類型,即函數返回來的值的類型。無參函數一般不需要返回函數值,因此可以不定類型標識符,例2-22就如此。
②有參函數定義的一般形式。
類型標識符 函數名(形式參數表列)
形式參數說明
例如:
這是一個求x和y二者中大者的函數,x和y為形式參數,從主調函數的實際參數把參數值傳遞給被調用函數中的形式參數x和y。第二行“int x,y;”是對形式參數作類型說明,指定x和y為整型。大括號內是函數體,它包括說明部分和語句部分。請注意,“int z;”必須寫在大括號內,而不能寫在大括號外,也不能將第二、三行合并寫成“int x,y,z;”。形式參數的說明應在函數體外。在函數體的語句中求出z的值(為x與y中較大者),return語句的作用是將z的值作為函數值帶回到主調函數中。return后面的括號中的值作為函數帶回的值(或稱函數返回值)。在函數定義時已指定max函數為整型,在函數體中定義z為整型,二者是一致的,將z作為函數max的值返回調用函數(見例2-23)。
③可以有“空函數”,它的形式為:
類型說明符 函數名(?。﹞ }
例如
調用此函數時,什么工作也不做,沒有任何實際作用。在主調函數中寫上“dummy(?。?;”表明“這里要調用一個函數”,而現在這個函數沒有起作用,等以后擴充函數功能時補充上。
2.9.2 函數參數和函數的值
2.9.2.1 形式參數和實際參數
在調用函數時,大多數情況下,主調函數和被調用函數之間有數據傳遞關系。這就是前面提到的有參函數。在定義函數名后面括號中的變量名稱為“形式參數”(簡稱“形參”),在調用函數時,函數名后面括號中的表達式稱為“實際參數”(簡稱“實參”)。
程序中第8~14行是一個函數定義(注意第8行的末尾沒有分號)。第8行定義了一個函數名max和指定兩個形參x、y。程序第5行是一個調用函數語句,max后面括號內的a,b是實參。a和b是main函數中定義的變量,x和y是函數max中的形式參數變量。通過函數調用,使兩個函數中的數據發生聯系。見圖2-17。

圖2-17 例2-23程序中的函數定義
關于形參與實參的說明:
①在定義函數中指定的形參變量,在未出現函數調用時,它們并不占內存中的存儲單元。只有在發生函數調用時函數max中的形參才被分配內存單元。在調用結束后,形參所占的內存單元也被釋放。
②實參可以是常量、變量或表達式,如:
但要求它們有確定的值。在調用時將實參的值賦給形參變量(如果形參是數組名,則傳遞的是數組首地址而不是變量的值)。
③在被定義的函數中,必須指定形參的類型。
④實參與形參的類型一致。例2-23中實參與形參都是整型,這是合法的、正確的。如果實參為整型而形參為實型,或者相反,則發生“類型不匹配”的錯誤。字符型與整型可以互相通用。
⑤C語言規定,實參變量對形參變量的數據傳遞是“值傳遞”,即單向傳遞,只由實參傳給形參,而不能由形參傳回來給實參。在內存中,實參單元與形參單元是不同的單元。如圖2-18所示。

圖2-18 形參和實參(1)
在調用函數時,給形參分配存儲單元,并將實參對應的值傳遞給形參,調用結束后,形參單元被釋放,實參單元仍保留并維持原值。因此,在執行一個被調用函數時,形參的值如果發生改變,并不會改變主調函數的實參的值。例如,若在執行函數過程中x和y的值變為10和15,而a和b仍為2和3,見圖2-19。

圖2-19 形參和實參(2)
⑥允許在列出“形參表列”時,同時說明形參類型。如:
相當于:
又如:
可以寫成
2.9.2.2 函數的返回值
通常,希望通過函數調用使主調函數能得到一個確定的值,這就是函數的返回值。例如,例2-23中,max(2,3)的值是3,max(5,2)的值是5。賦值語句將這個函數值賦給變量C。下面對函數值作一些說明:
①函數的返回值是通過函數中的return語句獲得的。return語句將被調用函數中的一個確定值帶回主調函數中去。見圖2-17中從return語句返回的箭頭。
如果需要從被調用函數返回一個函數值(供主調函數使用),被調用函數中必須包含return語句。如果不需要從被調用函數返回函數返可以不要return語句。
一個函數中可以有一個以上return語句,執行到哪一個return語句,哪一個語句起作用。
return語句后面的括號也可以不要,如
它與“return(z);”等價。
return后面的值可以是一個表達式。例如,例2-23中的函數max可以改寫如下:
這樣的函數體更為簡短,只用一個return語句就把求值和返回都解決了。
②函數值的類型。既然函數有返回值,這個值當然應屬于某一個確定的類型,應當在定義函數時指定函數值的類型。例如:
int max(x,y) 函數值為整型
char letter(c1,c2) 函數值為字符型
double min(x,y) 函數值為雙精度型
讀者會問:例2-23中的函數定義并沒有說明類型,為什么?C語言規定,凡不加類型說明的函數,一律自動按整型處理。例2-23中的max函數返回值為整型,因此可以不必說明。
在定義函數時對函數值說明的類型一般應該和return語句中的表達式類型一致。例如,例2-23中用隱含方式指定max函數值為整型,而變量z也被指定為整型,通過return語句把z的值作為max的函數值,由max帶回主調函數。z的類型與max函數的類型是一致的,是正確的。
③如果函數值的類型和return語句中表達式的值不一致,則以函數類型為準。對數值型數據,可以自動進行轉換。即函數類型決定返回值的類型。
【例2-24】將例2-23稍作改動(注意是變量的類型改動)。
函數max定義為整型,而return語句中的z為實型,二者不一致,按上述規定,先將z轉換為整型,然后max(x,y)帶回一個整型值2給主調函數main。如果將main函數中的c定義為實型,用%f格式符輸出,也是輸出2.000000。
有時,可以利用這一特點進行類型轉換,如在函數中進行實型運算,希望返回的是整型量,可讓系統去自動完成類型轉換。但這種做法往往使程序不清晰,可讀性降低,容易弄錯,而且并不是所有的類型都能互相轉換的(如實數與字符類型數據之間),因此建議初學者不要采用這種方法,而應做到使函數類型與return返回值的類型一致。
④如果被調用函數中沒有return語句,并不返回一個確定的、用戶所希望得到的函數值,但實際上,函數并不是不返回值,而只是不返回有用的值,返回的是一個不確定的值。例如,在例2-22程序中,盡管沒有要求printstar和print_message函數返回值,但是如果在程序中出現下面的語句也是合法的:
運行時除了得到例2-22一樣的結果外,還可以輸出a,b,c的值(今為21、20、21)。a,b,c的值不一定有實際意義(今printstar函數輸出21個字符,今返回值為21,print_message輸出20個字符,返回值為20)。
⑤為了明確表示“不返回值”,可以用“void”定義“無類型”(或稱“空類型”)。例如,例2-22中的定義可以改為:
這樣,系統就保證不使函數返回任何值,即禁止在調用函數中使用被調用函數的返回值。如果已將printstar和print_message函數定義為void類型,則下面的用法就是錯誤的:
編譯時會給出出錯信息。
為使程序減少出錯,保證正確調用,凡不要求返回函數值的函數,一般應定義為“void”類型。許多C語言書的程序中都大量用到void類型函數,讀者應對此有一定了解。
2.9.3 函數的調用
(1)函數調用的一般形式
函數調用的一般形式為
函數名(實參列表);
如果是調用無參函數,則實參表列可以沒有,但括號不能省略,見例2-22。如果實參表列包含多個實參,則各參數間用逗號隔開。實參與形參的個數應相等,類型一致。實參與形參按順序對應,一一傳遞數據。
(2)函數調用的方式
按函數在程序中出現的位置來分,可以有以下三種函數調用方式:
①函數語句。把函數調用作為一個語句。如例2-22中
這時不要求函數返回值,只要求函數完成一定的操作。
②函數表達式。函數出現在一個表達式中,這種表達式稱為函數表達式。這時要求函數返回一個確定的值以參加表達式的運算。例如
函數max是表達式的一部分,它的值乘2再賦給c。
③函數參數。函數調用作為一個函數的實參。例如
其中max(b,c)是一次函數調用,它的值作為max另一次調用的實參。m的值是a、b、c三者最大的。又如
也是把max(a,b)作為printf函數的一個參數。
函數調用作為函數的參數,實質上也是函數表達式形成調用的一種,因為函數的參數本來就是要求是表達式形式。
(3)對被調用函數的說明
在一個函數中調用另一函數(即被調用函數)需要具備哪些條件呢?
①首先被調用的函數必須是已經存在的函數(是庫函數或用戶自己定義的函數)。
②如果使用庫函數,一般還應該在本文件開頭用#include命令將調用有關庫函數時所需用到的信息包含到本文件來。例如,前幾章中已經用過的:
#include <reg52.h>(或寫成#include“reg52.h”)
其中“reg52.h”是一個“頭文件”,.h是頭文件所用的后綴,標志頭文件。
③如果使用用戶自己定義的函數,而且該函數與調用它的函數(即主調函數)在同一個文件中,一般還應在主調函數中對被調用函數的返回值的類型作說明。這種類型說明的一般形式為
類型標識符 被調用函數的函數名(?。?;
這是一個很簡單的函數調用,函數add的作用是求兩個實數之和,得到的函數值也是實型。請注意程序第二行:“float add(?。?;”,是對被調用的函數add的返回值作類型說明。注意:對函數的“定義”和“說明”不是一回事?!岸x”是指對函數功能的確立,包括指定函數名,函數值類型、形參及類型、函數體等,它是一個完整的、獨立的函數單位。而“說明”則是對已定義的函數的返回值進行類型說明(或稱“聲明”),它只包括函數名、函數類型以及一個空的括號,不包括形參和函數體。對被調用函數進行說明的作用是告訴系統:在本函數中將要用到的某函數是××類型,也就是說明該函數的返回值的類型,以便在主調函數中按此類型對函數值作相應的處理。
讀者可能會提出疑問:在例2-22、例2-23中的main函數中并未出現類似的對被調用函數的類型說明。C語言規定,在以下幾種情況下可以不在調用函數前對被調用函數作類型說明。
①如果函數的值(函數的返回值)是整型或字符型,可以不必進行說明,系統對它們自動按整型說明。如例2-23、例2-24、例2-25都是如此。
②如果被調用函數的定義出現在主調函數之前,可以不必加以說明。因為編譯系統已經先知道了已定義的函數類型,會自動處理的。
如果把例2-26改寫如下(即把main函數放在add函數的下面),就不必在main函數中對add進行說明。
也就是說,將被調用的函數的定義放在主調函數之前,就可以不必另加類型說明。
③如果已在所有函數定義之前,在文件的開頭,在函數的外部已說明了函數類型,則在各個主調函數中不必對所調用的函數再作類型說明。例如:
除了以上三種情況外,都應該按上述介紹的方法對所調用函數的返回值作類型說明,否則編譯時就會出現錯誤。
(4)函數的嵌套調用
C語言的函數定義都是互相平行、獨立的,也就是說在定義函數時,一個函數內不能包含另一個函數。
C語句不能嵌套定義函數,但可以嵌套調用函數,也就是說,在調用一個函數的過程中,又調用另一個函數。見圖2-20。

圖2-20 函數的嵌套調用
圖2-20表示的是兩層嵌套(連main函數共3層函數),其執行過程是:①執行main函數的開頭部分;②遇調用a函數的操作語句,流程轉去a函數;③執行a函數的開頭部分;④遇調用b函數的操作語句,流程轉去b函數;⑤執行b函數,如果再無其他嵌套的函數,則完成b函數的全部操作;⑥返回調用b函數處,即返回a函數;⑦繼續執行a函數中尚未執行的部分,直到a函數結束;⑧返回main函數中調用a函數處;⑨繼續執行main函數的剩余部分直到結束。
(5)函數的遞歸調用
在調用一個函數的過程中又出現直接或間接地調用該函數本身,稱為函數的遞歸調用。C語言的特點之一就在于允許函數的遞歸調用。例如:
在調用函數f的過程中,又要調用f函數,這是直接調用本函數,見圖2-21。下面是間接調用本函數:

圖2-21 函數的遞歸調用(1)
在調用f1函數過程中要調用f2函數,而在調用f2函數過程中又要調用f1函數,見圖2-22。

圖2-22 函數的遞歸調用(2)
從圖上可以看到,這兩種遞歸調用都是無終止的自身調用。顯然,程序中不應出現這種無終止的遞歸調用,而只應出現有限次數的、有終止的遞歸調用,這可以用if語句來控制,只有在某一條件成立時才繼續執行遞歸調用,否則就不再繼續。
(6)數組作為函數參數
前面已經介紹了可以用變量作函數參數,此外,數組元素也可以作函數實參,其用法與變量相同。數組名也可以作實參和形參,傳遞的是整個數組。
①數組元素做函數實參 由于實參可以是表達式形式,數組元素可以是表達式的組成部分,因此數組元素當然可以作為函數的實參,與變量做實參一樣,是單向傳遞,即“值傳送”方式。
【例2-27】有兩個數組a、b。各有10個元素,將它們對應地逐個相比(即a[0]與b[0]比,a[1]與b[1]比,……)。如果a數組中的元素大于b數組中的相應元素的數目多于b數組中元素大于a數組中相應元素的數目(例如,a[i]>b[i]6次,b[i]>a[i]3次,其中i每次為不同的值),則認為a數組大于b數組,并分別統計出兩個數組相應元素大于、等于、小于的次數。
程序如下:
②可以用數組名作函數參數 此時實參與形參都應用數組名(或用數組指針)。
【例2-28】有一個一維數組score,內放10個學生成績,求平均成績。
程序如下:
說明:
①用數組名作函數參數,應該在主調函數和被調函數分別定義數組,例中array是形參數組名,score是實參數名,分別在其所在函數中定義,不能只在一方定義。
②實參數組與形參數組類型應一致,如不一致,結果將出錯。
③實參數組和形參數組大小可以一致也可以不一致,C編譯對形參數組大小不作檢查,只是將實參數組的首地址傳給形參數組。如果要求形參數組得到實參數組全部的元素值,則應當指定形參數組與實參數組大小一致。
形參數組也可以指定大小,在定義數組時在數組名后面跟一個空的中括號,為了在被調用函數中處理數組元素的需要,可以另設一個參數,傳遞數組元素的個數,例2-28可以改寫為例2-29形式。
④最后應當強調一點:數組名作函數參數時,不是“值傳送”,不是單向傳遞,而是把實參數組的起始地址傳遞給形參數組,這樣兩個數組就共占同一段內存單元。見圖2-23。假若a的起始地址為1000,則b數組的起始地址也是1000,顯然,a和b同占一段內存單元,a[0]與b[0]同占一個單元……。這種傳遞方式叫“地址傳送”。由此可以看到,形參數組中各元素的值如發生變化會使實參數元素的值同時發生變化,從圖2-23很容易理解。這點是與變量做函數參數的情況不相同的,務請注意。在程序設計中可以有意識地利用這一特點改變實參數組元素的值。

圖2-23 例2-29圖
【例2-30】用選擇法對數組中10個整數按由小到大排序。所謂選擇法就是:先將10個數中最小的數與a[0]對換;再將a[1]~a[9]中最小的數與a[1]對換;……,每比較一輪,找出一個未經排序的數中最小的一個。共比較9輪。
程序如下:
可以看到在執行函數調用語句“sort(a,10);”之前和之后,a數組中各元素的值是不同的。原來是無序的,執行“sort(a,10);”后,a數組已經排好序了,這是由于形參數組array已用選擇法進行排序了,形參數組改變也使實參數組隨之改變。
2.9.4 局部變量和全局變量
(1)局部變量
在一個函數內部定義的變量是內部變量,它只在本函數范圍內有效,也就是說只有在本函數內才能使用它們,在此函數以外是不能使用這些變量的。這稱為“局部變量”。如:
說明:
①主函數main中定義的變量(m,n)也只在主函數中有效,而不因為在主函數中定義而在整個文件或程序中有效。主函數也不能使用其他函數中定義的變量。
②不同函數中可以使用相同名字的變量,它們代表不同的對象,互不干擾。例如,在f1函數中定義了變量b、c,倘若在f2函數中也定義變量b和c,它們在內存中占不同的單元,互不混淆。
③形式參數也是局部變量。例如f1函數中的形參a,也只在f1函數中有效。其他函數不能調用。
④在一個函數內部,可以在復合語句中定義變量,這些變量只在本復合語句中有效,這種復合語句也可稱為“分程序”或“程序塊”。
變量c只在復合語句(分程序)內有效,離開該復合語句該變量就無效,釋放內存單元。
(2)全局變量
前已介紹,程序的編譯單位是源程序文件,一個源文件可以包含一個或若干個函數。在函數內定義的變量是局部變量,而在函數之外定義的變量稱為外部變量,外部變量是全局變量。全局變量可以為本文件中其他函數所共用。它的有效范圍為:從定義變量的位置開始到本源文件結束。如:
p、q、c1、c2都是全局變量,但它們的作用范圍不同,在main函數和函數f2中可以使用全局變量p、q、c1、c2,但在函數f1中只能使用全局變量p、q,而不能使用c1和c2。
在一個函數中既可以使用本函數中的局部變量,又可以使用有效的全局變量。
說明:
①設全局變量的作用是:增加了函數間數據聯系的渠道。由于同一文件中的所有函數都能引用全局變量的值,因此如果在一個函數中改變了全局變量的值,就能影響到其他函數,相當于各個函數間有直接的傳遞通道。由于函數的調用只能帶回一個返回值,因此有時可以利用全局變量增加與函數聯系的渠道,從函數得到一個以上的函數值。
【例2-31】有一個一維數組,內放10個學生成績,寫一函數,求出平均分、最高分和最低分。
此外,C語言規定,外部數組可以賦初值,而局部數組不能賦初值。
②建議不在必要時不要使用全局變量。因為全局變量在程序的全部執行過程中都占用存儲單元,而不是僅在需要時才開辟單元;它使函數的通用性降低了,因為函數在執行時要依賴于其所在的外部變量,如果將一個函數移到另一個文件中,還要將有關的外部變量及其值一起移過去,但若該外部變量與其他文件的變量同名時,就會出現問題,降低了程序的可靠性和通用性;使用全局變量過多,會降低程序的清晰性,人們往往難以清楚地判斷出每個瞬時各個外部變量的值,在各個函數執行時都可能改變外部變量的值,程序容易出錯。因此要限制使用全局變量。
③如果外部變量在文件開頭定義,則在整個文件范圍內都可以使用該外部變量,如果不在文件開頭定義,按上面規定作用范圍只限于定義點到文件終點。如果在定義點之前的函數想引用該外部變量,則應該在該函數中用關鍵字extern作“外部變量說明”,表示該變量在函數的外部定義在函數內部可以使用它們。
運行結果為13。
由于外部變量定義在函數main之后,因此在main函數引用外部變量a和b之前,應該用extern進行外部變量說明,說明a和b是外部變量。如果不作extern說明,編譯時出錯,系統不會認為a、b是已定義的外部變量。一般做法是外部變量的定義放在引用它的所有函數之前,這樣可以避免在函數中多加一個extern說明。
外部變量定義和外部變量說明并不是同一回事。外部變量的定義只能有一次,它的位置在所有函數之外,而同一文件中的外部變量的說明可以有多次,它的位置在函數之內(哪個函數要用就在哪個函數中說明)。系統根據外部變量的定義(而不是根據外部變量的說明)分配存儲單元。對外部變量的初始化只能在“定義”時進行,而不能在“說明”中進行。所謂“說明”,其作用是:聲明該變量是一個已在外部定義過的變量,僅僅是為了引用該變量而作的“聲明”。原則上,所有函數都應當對所用的外部變量作說明(用extern),只是為簡化起見,允許在外部變量的定義點之后的函數可以省寫這個“說明”。
④如果在同一個源文件中,外部變量與局部變量同名,則在局部變量的作用范圍內,外部變量不起作用。
運行結果為8。
這里故意重復使用a、b作變量名,請讀者區別不同的a、b的含義和作用范圍。第一行定義了外部變量a、b,并使之初始化。第二行開始定義函數max,a、b是形參,形參也是局部變量。函數max中的a、b不是外部變量a、b,它們的值是由實參傳給形參的,外部變量a、b在max函數范圍內不起作用。最后4行是main函數,它定義了一個局部變量a,因此全局變量a在main函數范圍內不起作用,而全局變量b在此范圍內有效。因此printf函數中的max(a,b)相當于max(8,5),程序運行后得到結果為8。
2.9.5 內部函數和外部函數
函數本質上是全局的,因為一個函數要被另外的函數調用,但是,根據函數能否被其他源文件調用,將函數區分為內部函數和外部函數。
(1)內部函數
如果一個函數只能被本文件中其他函數所調用,它稱為內部函數。在定義內部函數時,在函數名和函數類型前面加static。即
static類型標識符函數名(形參表)
內部函數又稱靜態函數。使用內部函數,可以使函數只局限于所在文件,如在不同的文件中有同名的內部函數,互不干擾。這樣不同的人可以分別編寫不同的函數,而不必擔心所用函數是否會與其他文件中函數同名,通常把只由同一文件使用的函數和外部變量放在一個文件中,冠以static使之局部化,其他文件不能引用。
(2)外部函數
在定義函數時,如果冠以關鍵字extern,表示此函數是外部函數。如
函數fun可以為其他文件調用,如果在定義函數時省略extern,則隱含為外部函數。本書前面所用的函數都作為外部函數。
在需用調用此函數文件中,一般要用extern說明所用的函數是外部函數。也可用“#include”命令將外部函數包含到某文件中。如在某文件開頭加上以下指令:
這時,在編譯時,系統自動將這2個文件放到main函數的前頭,作為一整體編譯,而不是分3個文件編譯。這時這些函數被認為是在同一文件中,不再是外部函數被其他文件調用了。main函數中原有的extern說明可以不要。
- C++ Primer習題集(第5版)
- 計算機圖形學編程(使用OpenGL和C++)(第2版)
- PyTorch自動駕駛視覺感知算法實戰
- Java 9 Programming Blueprints
- Getting Started with PowerShell
- Elasticsearch for Hadoop
- 數據結構習題解析與實驗指導
- ExtJS高級程序設計
- Mastering Web Application Development with AngularJS
- Python+Tableau數據可視化之美
- Learning Material Design
- 微前端設計與實現
- Node.js應用開發
- 一步一步學Spring Boot:微服務項目實戰(第2版)
- Spring MVC Cookbook