張皓凱、沈奕呈Nov 12,2021
tcirc
社課
C++
台中一中電研社
講義都會公告在社網上
社網:tcirc.tw
IG:tcirc_39th
online judge:judge.tcirc.tw
函式是執行某項作業的一段程式碼區塊,常用於需要大量在無法使用迴圈的地方重複使用某一段程式碼時。在我們之前的社課其實已經出現過各種不同的函式,例如:.size()
、.append()
、getline()
。
函式(function)的概念類似於數學中的函數(function),一樣都是有輸入、輸出,之中的運作也有一定的關係。
(圖片取自維基百科)
定宣告的方式如下:
輸出值資料型態 函式名稱(輸入值1資料型態 輸入值1名稱, 輸入值2資料型態 輸入值2名稱, ...){ return 回傳值; }
輸入值(input,參數parameter)的部分可以有0
到無限多個,而回傳值(盡量不要用輸出值因為容易跟cout
搞混,output)只可以沒有或一個。如果沒有輸出值則「輸出值資料型態」的部分用void
代替。(注意:void
不是一種資料型態)。輸入值的變數寫在小括號中就是宣告,不需要在宣告函式前先宣告變數,而輸入值變數的有效範圍是整個函式。
直接看範例
有回傳值:
int add(int a, int b){ return a+b; }
函式會在執行完return
後直接停止。
無回傳值:
void printmessage(){//沒有輸入小括號還是要留著 cout << "I'm a function!"; }
函式宣告的位置分成兩種:在main()
前面、在main()
後面,兩者沒有任何功能性上的差異,只是美觀而已。在main()
之前的宣告方法與上面相同,之後的話只需要在main()
前面加上這一行:
輸出值資料型態 函式名稱(輸入值1資料型態 輸入值1名稱, 輸入值2資料型態 輸入值2名稱, ...) //必須與宣告函式的部分相同
這個動作稱之為prototype
使用函式的動作我們稱為呼叫,一個函式被呼叫後電腦會執行函式內的程式碼。呼叫方法如下:
函式名稱(輸入值1,輸入值2,...)//輸入值的數量需要與定義時的輸入值數量相同
如果是有回傳值的函式,可以將變數值設為函式的回傳值。
#include <iostream> using namespace std; int f(int a, int b){ return a*a+5*b+2; cout << a; } void g(int a,int b){ cout << "a=" << a << " " << "b=" << b << endl; } int main(){ int a=3,b=7; g(a,b); int c=f(a, b); cout << c; }
/*---output
a=3 b=7
46
---------*/
如果函式中有某個輸入值常常為同樣的值,我們可以在宣告函式時給予參數一個預設值,則在之後呼叫時就可以不用輸入該參數。但若要給定每一值則必須輸入以前的所有參數。
#include <iostream> using namespace std; int power(int a, int b=2){ int re=1; for(int i=0;i<b;i++){ re*=a; } return re; } int main(){ cout << power(3) << " " << power(2,3); }
/*---output
9 8
---------*/
若要讓一個函式的參數為陣列,在宣告時可不指定大小,若有指定大小,在輸入時也不必給一個相同大小的陣列。另外,在傳入時只需要輸入陣列名稱即可。
#include <iostream> using namespace std; int countsum(int arr[], int n){ int sum=0; for(int i=0;i<n;i++){ sum+=arr[i]; } return sum; } int main(){ int arr[5]={2,5,9,7,3}; cout << countsum(arr,5); } //26
main()
其實也是一個函式,有沒有發現到沒有main()
的回傳型態是int
,但卻沒有return
,那是因為C++
會自動讓main()
回傳0。
當在一個函式中使用自己就稱為遞迴,遞迴最重要的就是設定停止條件(base case),否則你會把你的電腦給炸了(其實不會,電腦會有一個最大遞迴上限,超過了會自動終止你的程式)。
一樣,直接看範例
沒有base case:
#include <iostream> using namespace std; int f(int a){ cout << a; return f(a); } int main(){ cout << f(4); cout << "end"; }
一個正確的遞迴(費波那契數):
#include <iostream> using namespace std; long long c=0; int fib(int a){ if(a==1||a==2){ return 1; } return fib(a-1)+fib(a-2); } int main(){ cout << fib(6); } //8
我們知道「相同型態的變數們」可以一格一格的放進「陣列」這個資料型態
可是…有些情況是不能(或不適合)用陣列把多個變數綁在一起的
比如說…你沒辦法用一個 2*x 的陣列把不同資料型態的兩個變數綁在一起(例如: int+char)
或者…當兩種以上資料型態相同但「性質不同」的變數綁在一起時,這個情況也不適合用陣列當「複合型別的資料型態」(例如:xyz座標.身高+體重+視力)
你說這些你都可以用(一或多個)一維或二維陣列解決!?
那你就用5*n的(或5個)陣列記錄每個人的身高.體重.視力.年齡.體指率阿😑😑
保證你被一堆索引或零散的陣列搞瘋(╯°□°)╯︵ ┻━┻
是一種複合型別 (derived data type),在寫程式時可以大幅增加程式的結構與可讀性,減少冗餘,是個很棒的東西。
簡單來說它是一種自創的資料型態(可以將相同或不同資料型態的變數綁在一起)
還可以將不直覺的索引編號用直覺的名字代替(如:x.y)
用法: struct 自訂資料型態{成員1;成員2;...函式1{ }...};
說明:
-資料成員(data member):在某一個struct裡面宣告一些資料型態,而這些宣告的資料型態就是資料成員(類似於陣列中的索引)
-成員函式(member function):可以想成放在某struct目錄內的函式
struct people{ int height,weight; double bmi; double get_bmi(){ return bmi=weight/(height*height/100.0/100);} };
用法: 先宣告一個資料型態為【struct名稱】的變數,再利用 變數名稱.某成員或函式
呼叫該變數的指定成員或函式
int main() { people room[50]; while(cin>>n){ for(int i=0;i<n;i+=1){ cin>>room[i].height>>room[i].weight; cout<<room[i].get_bmi()<<' '; } } }
/*---input
5
160 45
180 70
173 75
164 55
158 63
---------*/
/*---output
17.5781 21.6049 25.0593 20.4491 25.2363
---------*/
class也是一種複合型別,而struct跟class是實現物件導向(Object Oriented Programming/OOP)的重要角色,
class和struct類似,但相較於struct,class多了存取標籤的功能,標籤可以拿來設定存取權限,避免因為撞名而誤用了class裡面的某些成員或函式
另外,我們可以用class自創標頭檔,自己寫一個好用的函式工具給其他程式使用
如果class只是單支程式的一部分的話,它就是能分類成員存取權限的struct
權限分為public、private和protected(這個我們不討論)
透過private可以把資料封在class內(封裝),讓外界不要隨便存取class的資料
class division{ public: void set_a(int n);//set=賦值 void set_b(int n); int get_a();//get=取值 int get_b(); double do_div();//相除 private: double a; double b; };
在class外寫函式的內容,記得在函式名稱前面加上所屬class的名稱,並接上 ::
,不然會被認定為一般的function
void division::set_a(int n){ a=double(n); } void division::set_b(int n){ b=double(n); } double division::get_a(){ return a; } double division::get_b(){ return b; } double division::do_div(){ return get_a()/get_b(); }
呼叫就跟struct的用法一樣啦–
先宣告一個資料型態為【class名稱】的變數,再利用 變數名稱.某成員或函式
呼叫該變數的指定成員或函式
int main() { int x,y; while(cin>>x>>y){ division ans; ans.set_a(x); ans.set_b(y); cout<<ans.do_div()<<'\n'; } }
/*---input
180 70
164 55
158 63
---------*/
/*---output
2.57143
2.98182
2.50794
---------*/
不常用的零件在程式內額外寫ok
但常用的零件如果每次要用的時候都得花時間寫一次,是不是很麻煩?
就像我們只需要知道電腦要怎麼用就好,不需要知道怎麼裝電腦,更不會在每次開電腦前,就組裝一次電腦
我們可以自己寫一個工具箱,做成.h的標頭檔
實作方式就是–
<>
檔案名稱:division4.h
class division{ public: void set_a(int n);//set=賦值 void set_b(int n); int get_a();//get=取值 int get_b(); double do_div();//相除 private: double a; double b; };
檔案名稱:division4.cpp
include"division4" void division::set_a(int n){ a=double(n); } void division::set_b(int n){ b=double(n); } double division::get_a(){ return a; } double division::get_b(){ return b; } double division::do_div(){ return get_a()/get_b(); }
檔案名稱:main555.cpp
include<iostream> incude"division4.h" using namespace std; int main() { int x,y; while(cin>>x>>y){ division ans; ans.set_a(x); ans.set_b(y); cout<<ans.do_div()<<'\n'; } }
最後在terminal同時編譯 division4.cpp main555.cpp 就可以了
剛剛的例子中,每當我們需要將兩個數相除,就得在main裡面用兩個函式來初始化a、b的值,難道我們不能在宣告變數型態時就初始化兩數的值?
可以的,這個方法叫建構函式
建構函式就是寫個與 struct 或 class 同名的函式,在宣告變數的時候就能順便進行這個函式
建構函式:
division::division(int n1,int n2){ set_a(n1); set_b(n2); }
額外加上函式這個即可
呼叫:
int main() { int x,y; while(cin>>x>>y){ division ans(x,y); cout<<ans.do_div()<<'\n'; } }
少了兩行是不是簡潔許多阿
我們還可以根據輸入資料型態的不同,對應不同的建構函式,這個方法叫多載
建構函式:
division::division(int n1,int n2){ set_a(n1); set_b(n2); } division::division(int n1,double n2){ set_a(n1); set_b(n2); } division::division(double n1,int n2){ set_a(n1); set_b(n2); } division::division(double n1,double n2){ set_a(n1); set_b(n2); }
呼叫:
int main() { division ans(7,2); division ans2(6.25,3.11); cout<<ans.do_div()<<'\n'; cout<<ans2.do_div()<<'\n'; } }
這樣我們就不用限定輸入的資料型態必須是整數,才能使用這個函式了
我們能方便的呼叫的函式庫裡的函式,正是因為那些函式有經過「建構」與「多載」的處理😁。
在第三次社課時有出現過這張圖,當時是在講變數儲存的方法,而變數所處存的位置(地址)就稱作為指標,指標可以直接讀取記憶體,利用這個特性除了可以讓程式進行得更快速,還可以達成區塊外讀取或更改變數值。
想要取得一個變數的指標可以透過取址運算子(&
)來達成,這裡與位元運算子的AND不同,AND是用來運算兩個數字,而取址運算子是加在變數前的。
#include <iostream> using namespace std; int main(){ int a=7; cout << "a=" << a << "\na的位置:" << &a; }
/*----output
a=7
a的位置:0x61fe1c
----------*/
指標是由一串16進位的數字組成,這串數字所代表的就是記憶體的位置代碼,沒有其他意義。
可以透過指標變數來儲存一個變數的指標,宣告方法如下:
要儲存的變數資料型態 * 指標變數名稱 = &變數; int num = 100; int *ptr = #
指標前的變數型態並不代表指標的變數型態,只是用與變數相同的資料型態會更方便識別。
如果想要得知這個指標位置所存的資料值就需要用到取值運算子(*
),用法一樣是加在變數前。
#include <iostream> using namespace std; int main(){ int a=7; int *ptr=&a; cout << *ptr; } //7
在宣告時的*
與取值運算子是不一樣的,宣告時的*
代表我要宣告的是一個指標變數,而取值運算子代表的是取得該記憶體位置的值。
#include <iostream> using namespace std; int main(){ int a=7; int *ptr=&a; cout << "The value of a is " << a << endl; cout << "The address of a is " << ptr << endl; cout << "The value in " << ptr << " is " << *ptr << endl; cout << "The address of ptr is " << &ptr << endl; }
/*----------output
The value of a is 7
The address of a is 0x61fe1c
The value in 0x61fe1c is 7
The address of ptr is 0x61fe10
----------------*/
直接上範例,不然很難講
#include <iostream> using namespace std; int main(){ int arr[5]={10,15,20,25,30}; int *ptr=arr; for(int i=0;i<5;i++){ cout << arr[i] << " " << &arr[i] << " " << ptr+i << endl; } }
/*---output----------
10 0x61fdf0 0x61fdf0
15 0x61fdf4 0x61fdf4
20 0x61fdf8 0x61fdf8
25 0x61fdfc 0x61fdfc
30 0x61fe00 0x61fe00
--------------------*/
陣列的名稱本身就是一個指標,等於&arr[0]
,且&arr[i]
等同於arr+i
。
因為C++無法直接寫出回傳值為陣列的函式,所以只能夠用指標來間接達成。
#include <iostream> using namespace std; for(int i=0;i<n;i++){ arr[i]*=arr[i]; } return arr; } int main(){ int arr1[5] = {3,6,7,9,12}; int *arr2 = square(arr1,5); for(int i=0;i<5;i++){ cout << arr2[i] << " "; } }
其實大部分的題目都可以用函式寫,很少有專門的題目練習。