一、認清函數的真相
1、函數的由來
程式 = 資料 + 算法
C程式 = 資料 + 函數
2、函數的意義
子產品化程式設計
C語言中的子產品化
3、面向過程的程式設計
# 面向過程是一種以過程為中心的程式設計思想
# 首先将複雜問題分解為一個個容易解決的問題
# 分解過後的問題可以按照步驟一步步完成
# 函數是面向過程在C語言中的展現
# 解決問題的每一個步驟可以用函數來實作
3、聲明和定義
# 程式中的聲明可以了解為預先告訴編譯器實體的存在,如:變量,函數,等等
# 程式中的定義明确訓示編譯器實體的意義
聲明和定義并不相同!!!
執行個體:
// global.c
// int g_var = 0; // 定義
#include <stdio.h>
extern int g_var; //聲明一個外部變量
void f(int i, int j); //聲明一個外部函數
int main()
{
int g(int x);
g_var = 10;
f(1, 2);
printf("%d\n", g(3));
return 0;
}
void f(int i, int j) // 定義
printf("i + j = %d\n", i + j);
int g(int x)
return 2 * x + g_var;
4、函數參數
# 函數參數在本質上與局部變量相同,都是在棧上配置設定空間
# 函數參數的初始值是函數調用時的實參值
int f(int i, int j)
printf("%d, %d\n", i, j);
int k = 1;
f(k, k++);
printf("%d\n", k);
函數參數的求值順序依賴于編譯器的實作!!!
C語言中大多數運算符對其操作數求值的順序都是依賴于編譯器的實作的!!!
int i = f() * g(); //這裡可不要盲目的認為先計算f()再計算g()???
5、程式中的順序點
# 程式中存在一定的順序點
# 順序點指的是執行過程中修改變量值的最晚時刻
# 在程式達到順序點的時候,之前所做的一切操作必須反映到後續的通路中
# 每個完整表達式結束時
# &&,||, ?:,以及逗号表達式的每個運算對象計算之後
# 函數調用中對所有實際參數的求值完成之後(進入函數體之前)
int k = 2;
int a = 1;
k = k++ + k++;
printf("k = %d\n", k);
if( a-- && a )
{
printf("a = %d\n", a);
}
函數的預設認定
# c語言會預設沒有類型的函數參數為int
f(i, j)
return i + j;
等價于
小結:
1、c語言是一種面向過程的語言
2、函數可了解為解決問題的步驟
3、函數的實參并沒有固定的計算次序
4、順序點是c語言中變量改變的最晚時機
5、函數定義時參數和傳回值得預設類型為int
三、可變參數分析與宏分析
問題:如何編寫一個可以計算n個數平均值的函數?
下面是大家都能想到的方法,其實c語言中還有另外一種實作方法。
float func(int array[], int size)
int i = 0;
float avr = 0;
for(i=0; i<size; i++)
avr += array[i];
return avr / size;
int array[] = {1, 2, 3, 4, 5};
printf("%f\n", func(array, 5));
1、可變參數
# c語言中可以定義參數可變的函數
# 參數可變函數的實作依賴于stdarg.h頭檔案
# va_list變量與va_start,va_end和va_arg配合使用能夠通路參數值
可變參數的定義與使用執行個體:
#include <stdarg.h> //可變參數頭檔案,一定要加上。
float average(int n, ...)
va_list args; //專用類型,可變清單
float sum = 0;
va_start(args, n);//初始化清單
for(i=0; i<n; i++)
sum += va_arg(args, int);
va_end(args); //結束清單
return sum / n;
printf("%f\n", average(5, 1, 2, 3, 4, 5));
printf("%f\n", average(4, 1, 2, 3, 4));
2、可變參數的限制
# 可變參數必須從頭到尾按照順序逐個通路
# 參數清單中至少要存在一個确定的命名參數
# 可變參數宏無法判斷實際存在的參數的數量
# 可變參數宏無法判斷參數的實際類型
warning:va_arg如果指定了錯誤的類型,那麼結果是不可預測的。
# 可變參數是c語言提供的一種函數設計技巧
# 可變參數的函數提供了一種更友善的函數調用方式
# 可變參數必須順序的通路
# 無法直接通路可變參數清單中間的參數值
3、李逵和李鬼
#define reset(p, len) while( len > 0) ((char*)p)[--len] = 0
void reset(void* p, int len)
while( len > 0 )
((char*)p)[--len] = 0;
int len = sizeof(array);
reset(array, len);
4、函數 vs 宏
# 宏是由預處理直接替換展開的,編譯器不知道宏的存在
# 函數是由編譯器直接編譯的實體,調用行為由編譯器決定
# 多次使用宏會導緻程式代碼量增加
# 函數是跳轉執行的,是以代碼量不會增加
# 宏的效率比函數要高,因為是直接展開,無調用開銷
#函數調用時會建立活動記錄,效率不如宏
5、宏的優點和缺點
# 宏的效率比函數稍高,但是其副作用巨大,容易出錯
#define add(a, b) a + b
#define mul(a, b) a * b
#define _min_(a, b) ((a) < (b) ? (a) : (b))
int i = 1;
int j = 10;
printf("%d\n", mul(add(1, 2), add(3, 4)));//宏的副作用
printf("%d\n", _min_(i++, j));
函數存在實參到形參的傳遞,是以無任何副作用,但是函數需要建立活動對象,效率受影響
int add(int a, int b)
return a + b;
int mul(int a, int b)
return a * b;
int _min_(int a, int b)
return a < b ? a : b;
printf("%d\n", mul(add(1, 2), add(3, 4)));
宏無可替代的優勢
宏參數可以是任何c語言實體
# 宏編寫的_min_參數類型可以是int,float等等
# 宏的參數可以是類型名
# define malloc(type, n) (type*)malloc(n * sizeof(type))
int *p = malloc(int, 5);//這是函數完成不了的
1、宏和函數并不是競争對手
2、宏能夠接受任何類型的參數,效率高,易出錯
3、函數的參數必須是固定類型,效率稍低,不易出錯
4、宏可以實作函數不能實作的功能
五、函數調用行為
1、活動記錄
# 活動記錄是函數調用時用于記錄一系列相關資訊的記錄
臨時變量域:用來存放臨時變量的值,如k++的中間結果
解釋:對于k++,函數是這樣來處理的
1、生産臨時變量temp
2、将k指派與temp
3、k = k+1
是以對于f(k, k++)中k++實際上取得是temp的值。
局部變量域:用來存放函數本次執行中的局部變量
機器狀态域:用來儲存調用函數之前有關機器狀态的資訊,包括各種寄存器的目前值和傳回位址等;
實參數域:用于存放函數的實參資訊
傳回值域:為調用者函數存放傳回值
2、參數入棧
既然函數參數的計算次序是依賴編譯器實作的,那麼函數參數的入棧次序是如何确定的呢?
那就是根據調用約定:
# 當一個函數被調用時,參數會傳遞給被調用的函數,而傳回值會被傳回給調用函數。函數的調用約定就是描述參數是怎麼傳遞到棧空間的,以及棧空間由誰維護。
# 參數傳遞順序
從右到左依次入棧:_stdcall, _cdecl, _thiscall
從左到右依次入棧:_pascal, _fastcall
# 調用堆棧清理
調用者清除棧
被調用函數傳回後清除棧
是以函數參數的入棧次序,可以有自己确定。還有函數的調用和傳回釋放都是有編譯器編譯好代碼有調用函數來操作,可以說函數的釋放依賴于編譯器。
那什麼時候會用到調用約定呢?
當我們使用别人給的庫函數,當然如果是别人給的源碼,那使用同一個編譯器進行編譯,自然就是是同樣的調用約定。而調用别人動态庫中的函數時就必須要約定好調用規則了。如果使用了不同的調用規則,就會發生莫名其妙的調用失敗,或者會産生荒謬的結果。
别人提供的動态庫,有可能不是用c語言編寫的,是以這個時候,我們調用動态庫時,就要小心定義調用約定了。
# 函數調用是c語言的核心機制
# 活動記錄中儲存了函數調用以及傳回所需要的一切資訊
# 調用約定是調用者和被調用者之間的調用協定,常用于不同開發者編寫的庫函數之間。
五、函數遞歸和函數設計技巧
1、遞歸概述
# 遞歸是數學領域中概念在程式設計中的應用
# 遞歸是一種強有力的程式設計方法
# 遞歸的本質為函數内部在适當的時候調用自身。
# c遞歸函數有兩個主要的組成部分:
遞歸點-以不同參數調用自身
出口-不在遞歸調用
f(x) = 1; (x = 1) x*f(x-1);(x>1)
利用遞歸函數求解n!
int func(int x)
if( x > 1 )
return x * func(x - 1);
else
return 1;
printf("x! = %d\n", func(4));
遞歸的一大優點就是它非常符合我們的思考方式。
缺點就是它占用較大的棧空間,如上程式中,如果n較大,可能會産生棧溢出的結果。
1、c語言中的遞歸函數必然會使用判斷語句
2、遞歸函數在需要編寫的時候定義函數的出口,否則棧會溢出
3、遞歸函數是一種分而治之的思想
思考:編寫一個函數列印一個字元數組的全排列
函數設計技巧
# 不要在函數中使用全局變量,盡量讓函數從意義上是一個獨立的功能子產品
# 參數名要能夠展現參數的意義
void str_copy(char *str1, char *str2);
void str_copy(char *str_dest, char *str_src);
# 如果參數是指針,且僅作輸入參數用,則應在類型前加const,以防止該指針在函數體内被意外修改
void str_copy(char *str_dest, const char *str_src);
# 不要省略傳回值的類型,如果函數沒有傳回值,那麼應聲明為void類型
# 在函數體的“入口處”,對參數的有效性進行檢查,對指針的檢查尤為重要
# 語句不可傳回指向“棧記憶體”的“指針”,因為該記憶體在函數體結束時被自動銷毀
# 函數體的規模要小,盡量控制在80行代碼之内
# 相同的輸入應當産生相同的輸出,盡量避免函數帶有“記憶性”功能
# 避免函數有太多的參數,參數個數盡量控制在4個以内
# 有時候函數不需要傳回值,但為了增加靈活性,如支援鍊式表達,可以附加傳回值。鍊式表達就是指函數的參數使用函數傳回值。
char s[54];
int len = strlen(strcpy(s, "android"));
# 函數名與傳回值類型在語義上不可沖突
char c;
c = getchar();//getchar的傳回值為int型
if(eof == c)
//...
六、進軍c++的世界
1、c語言已經很強大了,為什麼還要有c++?
答:c語言是面向過程的進階語言,但它有一個問題就是不能把生活中的問題直接映射到c語言程式中,是以用c開發應用程式的效率不高。而c++是在c語言的基礎上進行了兩個加強,一個是類型加強,一個是引進了面向對象的概念,是以c++更容易用來描述現實生活中的問題。
我們認識世界的方式就是通過分類;而c++同樣加強了類的概念,之前的c語言中也使用了類,但沒有具體提出。
類一直存在,但不一定有對象。c++中的類實際為struct結構的更新版,它在結構體的基礎上添加了函數,c++中稱為對象行為。
2.初識oop(object oriented programmingt)面向對象程式設計
# 類和對象是面向對象中的兩個基本概念
# “類”指的是一類事物,是一個抽象的概念
# “對象”指的是屬于某個類的一個實體,是一個具體存在的事物
類是一種“模闆”,可以通過這種模闆建立出不同的對象“執行個體”
對象“執行個體”是類“模闆”的一個具體實作
一個類可以有很多對象,而一個對象必然屬于某個類
3、生活中的類和對象
4、抽象
# 抽象的意義是觀察一群“事物”,并認識它們所具有的一些共同特性
# 抽象的本質是忽略不重要的差別,隻記錄能表現事物特征的關鍵資料項
# 類是抽象在程式設計領域的概念
# 類用于抽象的描述一類事物所特有的屬性和行為
如:電腦類的每個對象都有cpu,記憶體和硬碟,電腦類的每個對象都可以開機和運作程式
#對象時一個具體的事物,擁有其所屬類的所有屬性,并且每個屬性都是一個特有的值
如:老虎的每個對象(也就是每隻老虎),都有不同的體重,不同食量以及不同的性情
5、封裝
# 類中描述的事物屬性和行為往往是相關的
# 在c++中屬性通過變量來表示,行為通過函數來模拟
# 封裝指的是類中的變量隻能通過類的函數來通路
6、c++中的第一個類展示
struct student
const char* name;
int number;
void info()
printf("name = %s, number = %d\n", name, number);
};
student s;
s.name = "delphi";
s.number = 100;
s.info();
可以看出它的類就是c中的struct加上了一個函數(代碼)
7、c++類中有三種通路權限
public--類的外部可以自由的通路
protected--類自身和子類中可以通路
private--類自身中可以通路
protected:
public:
void set(const char* n, int i)
name = n;
number = i;
s.set("delphi", 100);
8、你也能做富二代
# 在c語言中struct有了自己的含義,雖然在c++中擴充成為了類,但一般情況還是遵循c中的用法
# c++一般情況下用class來做類的關鍵字聲明
# 繼承是c++中代碼複用的方式,通過繼承,在子類中可以使用父類中的代碼
# 子類可以完全繼承父類中所有的變量和函數,在可以使用父類的地方就可以用子類代替
# 子類從概念上而言是一種特殊的父類
class master : public student
const char* domain;
void setdomain(const char* d)
domain = d;
const char* getdomain()
return domain;
master s;
s.setdomain("software");
printf("domain = %s\n", s.getdomain());
1、面向對象是一種新型的軟體開發思想
2、面向對象将生活中的事物完全映射到程式中
3、抽象,封裝和繼承是面向對象程式設計的重要特征
4、繼承能夠很好的複用已有類的特性
5、子類是一種特殊化的父類