一、函數概念?
1、 在linux C語言中,什麼是函數?
C語言是一種面向過程的語言,C語言稱之爲函數式語言,可以將任何功能都封裝成一個函數接口。
2、 在C語言中,封裝函數意義?
在某些功能比較複雜,往往用戶需要將其封裝成一個函數接口,調用者使用該函數時,不需要關注過程,只需要知道傳遞什麼參數和函數有什麼返回值即可。
play_music(“xxx.mp3”); -> 參數就是這首歌名字。
聽到這首歌 -> 返回值就是聽到聲音。
二、函數的書寫規則?
1、 在linux C語言中,函數有哪些種類?
main函數: 主函數,在C語言中有且僅有一個,一般都在main函數開始執行,然後調用別的函數。
系統函數(system calls):man手冊的2手冊中,這些函數不需要用戶寫實現過程。
庫函數(library calls):man手冊的3手冊中,這些函數不需要用戶寫實現過程。
自定義函數:不能在man手冊查詢得到,用戶必須寫清楚實現過程。
例子:
#include <stdio.h>
//1. main函數
int main(int argc,char *argv[])
{
//2. 調用printf()庫函數
printf("helloworld!\n");
//3. 調用自定義函數
func(); //undefined reference to `func' -> func()實現過程沒有定義!
return 0;
}
2、自定義函數的書寫規則?
1)確定函數的函數名 -> 函數名字最好體現函數的功能
命名規則: 與C語言中變量一樣。 例子: my_fun
2)確定函數的形式參數列表
有形式參數:my_fun(形式參數1,形式參數2, … ,形式參數n) -> 有多少個形參,將來就需要傳遞多少個實參。
無形式參數:my_fun()
什麼是形式參數?
形式參數在函數中作用,首先聲明好,要調用該函數,首先需要什麼參數。
例子: 露營(一張大被子,一張厚席子,一個高枕頭,一個綠色桶)
例子:my_fun(int a,int b) -> 說明調用該函數時,需要傳遞二個整型數據給這個函數!
3)確定返回值類型 -> 寫在名字前面
有返回值 -> 確定返回值類型
int -> 無論這個函數是調用成功還是失敗,都會返回一個int類型。
char -> 無論這個函數是調用成功還是失敗,都會返回一個char類型。
float -> 無論這個函數是調用成功還是失敗,都會返回一個float類型。
例子:
int my_fun(int a,int b)
return 0;
無返回值 -> void -> 代表該函數沒有返回值
例子: void my_fun(int a,int b)
返回值類型 函數名字(形式參數列表) -> 自定義函數頭
4)函數體 -> {}
例子:
int my_fun(int a,int b)
{ -> 功能就寫在函數體中
}
3、如何調用函數?
1)確保調用函數實現過程已經寫好了!
例子:
int my_fun(int a,int b)
{
printf("a = %d\n",a);
printf("b = %d\n",b);
return 0;
}
2)直接寫函數名字,後面緊跟一個圓括號(),圓括號中存放東西就是實際參數(實參)。
例子:
my_fun(5,10);
結論: 形式參數值由實際參數直接初始化。 -> 這是不可逆的過程。
上述例子: a=5,b=10;
4、函數返回值情況
int my_fun(int a,int b) -> 需要接受兩個參數,返回一個int類型數據。
int ret;
ret = my_fun(5,10); -> 將my_fun函數的返回值結果賦值給ret。
練習3:寫一個函數實現接收三個int參數,經過約定的運算後,得到結果並打印出來。
前面兩個參數是計算,最後參數計算方式。
計算方式:
1 -> +
2 -> -
3 -> *
4 -> /
fun(3,4,2) -> 結果: 3-4=-1
fun(3,4,3) -> 結果: 3*4=12
#include <stdio.h>
int my_fun(int x,int y,int z) // x=a y=b z=c
{
int ret;
switch(z)
{
case 1:
ret=x+y;
break;
case 2:
ret=x-y;
break;
case 3:
ret=x*y;
break;
case 4:
ret=x/y;
break;
default:
printf("enter error!\n");
break;
}
return ret;
}
int main(int argc,char *argv[])
{
int a,b,c,result;
while(1)
{
scanf("%d %d %d",&a,&b,&c);
result = my_fun(a,b,c);
printf("result = %d\n",result);
}
return 0;
}
練習4:以下程序運行結果是?
int fun(int a,int b)
{
if(a > b) return a;
else return b;
}
int main(int argc,char *argv[])
{
int x=3,y=8,z=6,r;
r = fun(fun(x,y),2*z);
printf("%d\n",r); //12
}
三、函數的聲明?
1、 函數的定義與函數的聲明有什麼區別?
函數的定義包含了函數的名字,返回值類型,形式參數列表,具體實現過程。
例子:
int my_fun(int a)
{
printf("a = %d\n",a);
return 0;
}
函數的聲明包含了名字,返回值類型,形式參數列表,函數聲明必須寫在函數調用之前。
用通俗的話來講,函數的聲明意思是: 在你調用某個函數之前你必須要告訴我,這個函數長什麼樣子。
例子:
int my_fun(int a);
2、 函數聲明需要注意的點
1)如果函數定義寫在函數調用之前,就不要聲明瞭。
例子:
int my_fun2(); -> 需要聲明
int my_fun1() -> 不需要聲明
{
}
int main()
{
my_fun1();
my_fun2();
}
int my_fun2() -> 需要聲明
{
}
2)一般庫函數/系統函數聲明都是放在頭文件中
例子:
printf()函數聲明寫在#include <stdio.h>裏面.
所以當我們調用printf()函數時,不需要自己聲明,只需要包含對應的頭文件即可。
四、從內存的角度分析自定義函數運行過程
1、 回顧棧區以及函數特徵
1)函數內部申請的變量都是棧區申請。
2)函數返回時,在函數內部申請過的全部棧區的內存空間都要全部釋放。
3)函數返回時,會返回到函數調用的地方。
例題1: 請從內存角度分析以下代碼,求出結果?
int swap(int x,int y)
{
int t;
t = x;
x = y;
y = t;
return 0;
}
int main(int argc,char *argv[])
{
int a,b;
scanf("%d %d",&a,&b); //5 3
swap(a,b);
printf("a = %d,b = %d\n",a,b); //5 3
return 0;
}
例題2: 請從內存角度分析以下代碼,求出結果?
int a,b;
int swap()
{
int t;
t = a;
a = b;
b = t;
return 0;
}
int main(int argc,char *argv[])
{
scanf("%d %d",&a,&b); //5 3
swap();
printf("a = %d,b = %d\n",a,b); //3 5
return 0;
}
結論:
1)形式參數與實際參數佔用不同的儲存單位。
2)形式參數值由實際參數直接初始化。
五、函數嵌套?
1、 什麼是函數嵌套?
函數嵌套就是調用某個函數內部再調用另外一個函數。
2、 有函數嵌套程序在內存有什麼特點?
如果嵌套的函數很多,就會在棧區累積非常多空間沒有被釋放。
3、 函數嵌套與遞歸函數有什麼區別?
函數嵌套:自己調用別人的函數。
例子:
void fun()
{
my_fun();
}
遞歸函數:自己調用自己的函數。
例子:
void fun()
{
fun();
}
六、遞歸函數?
1、 特點?
遞歸函數特點自身調用自身,無限遞歸,沒有終止的時刻。
例子:
void fun()
{
int a;
fun(); -> 無限遞歸,沒有終止條件 -> 導致棧空間溢出!
return;
}
int main()
{
fun();
return 0;
}
結論: 爲了防止棧空間溢出,所以遞歸函數一般都會攜帶終止條件,也就是說到達某個條件時,函數返回!
2、題型1: 給程序,求結果。
例題,求出下面程序的結果?
void fun(int n)
{
if(n > 1)
{
fun(n-1);
}
printf("n = %d\n",n);
return;
}
int main()
{
fun(100);
return 0;
}
答案:1~100
思路: 找出終止條件,列出幾項,找規律,再畫圖分析!
3、 題型2: 給規則,寫程序。
例題:寫出下列程序。
有5個學生坐在一起,問第5個學生多少歲,他說比第4個學生大2歲。問第4個學生多少歲,他說比第3個學生大2歲。問第3個學生多少歲,他說比第2個學生大2歲。問第2個學生多少歲,他說比第1個學生大2歲。最後問第1個學生多少歲,他說他是10歲,請問第5個同學多少歲?使用遞歸函數來完成。
#include <stdio.h>
int get_child_age(int n)
{
int age;
if(n <= 0) // -> 搞事情
return -1;
if(n == 1) // -> 終止條件
age = 10;
if(n > 1) // -> 正常
age = get_child_age(n-1) + 2;
return age;
}
int main(int argc,char *argv[])
{
int ret;
ret = get_child_age(5);
printf("ret = %d\n",ret);
return 0;
}
思路: 寫框架,分析情況 -> 最終目標與終止條件之間的關係。
練習1: 畫出以上例子的內存圖。
練習2: 有以下程序,求出結果?
int fun(int n) 10 9
{
if(n==1) return 1;
else return (n+fun(n-1)); 10+9+8+7+6+5+4+3+2+1
}
int main()
{
int x;
scanf("%d",&x); //若輸入一個數字10。
x = fun(x); 10
printf("%d\n",x); //55
}
練習3:第1天存100塊,後面每一天都比昨天多存10塊,第312天存了多少?
#include <stdio.h>
int get_money(int n)
{
int money;
if(n <= 0)
return -1;
if(n == 1)
money = 100;
if(n > 1)
money = 10 + get_money(n-1);
return money;
}
int main()
{
int ret;
ret = get_money(10);
printf("ret = %d\n",ret);
return 0;
}
練習4: 使用循環方式完成練習3,畫內存圖分析兩種方式差別。
循環 -> 多次使用一個變量
遞歸 -> 每次都開闢一個新的變量空間 --> 容易造成棧空間溢出!
七、回調函數
1、 什麼是回調函數?
先定義好函數A(喝水),再將這個函數A作爲另外一個函數B(爬山)的參數,在函數B(爬山)可以回過頭調用這個參數(喝水),這個函數A就稱之爲回調函數。
2、 基本框架?
void funB(funA)
{
funA..... 在函數B中調用函數A
}
void funA() -> 回調函數
{
}
int main()
{
funB(funA);
}
例題: 從鍵盤中獲取兩個數值,求出兩個值的和,要求使用回調函數完成。
int get_result(int a,int b,int(*p)(int,int)) -> 就把這個p當成函數名字
{
int ret;
ret = p(a,b);
return ret;
}
int add_fun(int x,int y)
{
int z;
z = x + y;
return z;
}
int main()
{
int a,b,ret;
scanf("%d %d",&a,&b);
ret = get_result(a,b,add_fun); -> 函數名字作爲參數,其實是傳遞了函數的地址過來。
printf("ret = %d\n",ret);
}
補充:
函數指針參數怎麼寫?
1)先寫一個 *
2)在*後面寫一個變量名,使用圓括號括住 (*p)
3)確定函數是哪個? int add_fun(int x,int y)
4)把第3步函數名與形參的變量名都去掉 int (int,int)
5)把第2步結果寫在第4步結果的返回值類型與形式參數列表之間 int(*p)(int,int)
3、 回調函數使用場景?
在數據庫、系統編程信號通信中常常看到回調函數。
練習4: 連續從鍵盤中獲取4個數字,求出其中的最大值,要求回調函數來完成。
int max4(int a,int b,int c,int d,int(*p)(int,int))
{
int m;
m = p(a,b);
m = p(m,c);
m = p(m,d);
return m;
}
int max2(int x,int y)
{
int z;
z = (x > y ? x : y);
return z;
}
int main()
{
int a,b,c,d,ret;
ret = max4(a,b,c,d,max2);
return 0;
}
八、變參函數
1、 什麼是變參函數?
變參函數是指該函數的參數不固定,例如:printf()函數。
printf("helloworld!\n"); -> 參數爲1
printf("a = %d\n",a); -> 參數爲2
printf("(%d %d)\n",x,y); -> 參數爲3
結論: printf()參數爲"1+x"
2、如何判斷一個函數是不是變參函數?
#include <stdio.h>
int printf(const char *format, ...);
const char *format: 輸出字符串格式
... : 參數不定
3、 類似的變參函數有很多: printf() scanf() ioctl() fcntl()
九、內聯函數?
1、 什麼是內聯函數?
在程序中調用函數時,需要花費一定的時間進行保護現場與恢復現場,但是使用了內聯函數,就既可以使用函數,又不需要花費時間來進行保護現場與恢復現場。
封裝:test.c
#include <stdio.h>
void fun()
{
printf("hello!\n");
}
int main()
{
fun();
fun();
fun();
return 0;
}
不封裝:test.c
#include <stdio.h>
int main()
{
printf("hello!\n");
printf("hello!\n");
printf("hello!\n");
return 0;
}
2、 如何實現內聯函數?
test.c
int main()
{
fun();
fun();
fun();
return 0;
}
head.h
#include <stdio.h>
inline void fun()
{
printf("hello!\n");
}
3、如何解決保護恢復現場問題?
1)使用內聯函數 ->inline
2)不要過多封裝成函數,當某些表達式組成一個功能時,才封裝爲一個函數。
4、在嵌入式中哪裏看到內聯函數?
在內核鏈表時看到內聯函數。