TCIRC C++教學講義
===
>[name=張皓凱、沈奕呈][time=Nov 12,2021]
###### tags:`tcirc` `社課` `C++` `台中一中電研社`
---
第五節社課
===
[TOC]
---
## 電研
講義都會公告在社網上
[社網:tcirc.tw](https://tcirc.tw/)
[IG:tcirc_39th](https://www.instagram.com/tcirc_39th/)
![ig QRcode](https://i.imgur.com/4hyS6GM.png)
[online judge:judge.tcirc.tw](https://judge.tcirc.tw/)
---
## 函式(function)
函式是執行某項作業的一段程式碼區塊,常用於需要大量在無法使用迴圈的地方重複使用某一段程式碼時。在我們之前的社課其實已經出現過各種不同的函式,例如:`.size()`、`.append()`、`getline()`。
----
函式(function)的概念類似於數學中的函數(function),一樣都是有輸入、輸出,之中的運作也有一定的關係。
![](https://i.imgur.com/Kz0CPhu.png)
(圖片取自維基百科)
---
### 宣告
---
#### 方式
定宣告的方式如下:
```cpp=
輸出值資料型態 函式名稱(輸入值1資料型態 輸入值1名稱, 輸入值2資料型態 輸入值2名稱, ...){
return 回傳值;
}
```
輸入值(input,參數parameter)的部分可以有`0`到無限多個,而回傳值(盡量不要用輸出值因為容易跟`cout`搞混,output)只可以沒有或一個。如果沒有輸出值則「輸出值資料型態」的部分用`void`代替。(<font color='#ff0000'>注意</font>:`void`不是一種資料型態)。輸入值的變數寫在小括號中就是宣告,不需要在宣告函式前先宣告變數,而輸入值變數的有效範圍是整個函式。
----
直接看範例
有回傳值:
```cpp=
int add(int a, int b){
return a+b;
}
```
函式會在執行完`return`後直接停止。
無回傳值:
```cpp=
void printmessage(){//沒有輸入小括號還是要留著
cout << "I'm a function!";
}
```
---
#### 位置
函式宣告的位置分成兩種:在`main()`前面、在`main()`後面,兩者沒有任何功能性上的差異,只是美觀而已。在`main()`之前的宣告方法與上面相同,之後的話只需要在`main()`前面加上這一行:
```cpp=
輸出值資料型態 函式名稱(輸入值1資料型態 輸入值1名稱, 輸入值2資料型態 輸入值2名稱, ...)
//必須與宣告函式的部分相同
```
這個動作稱之為prototype
---
#### 呼叫
使用函式的動作我們稱為呼叫,一個函式被呼叫後電腦會執行函式內的程式碼。呼叫方法如下:
```cpp=
函式名稱(輸入值1,輸入值2,...)//輸入值的數量需要與定義時的輸入值數量相同
```
如果是有回傳值的函式,可以將變數值設為函式的回傳值。
----
```cpp=
#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
---------*/
```
---
#### 預設值
如果函式中有某個輸入值常常為同樣的值,我們可以在宣告函式時給予參數一個預設值,則在之後呼叫時就可以不用輸入該參數。但若要給定每一值則必須輸入以前的所有參數。
----
```cpp=
#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
---------*/
```
---
### 陣列為參數
若要讓一個函式的參數為陣列,在宣告時可不指定大小,若有指定大小,在輸入時也不必給一個相同大小的陣列。另外,在傳入時只需要輸入陣列名稱即可。
```cpp=
#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()`其實也是一個函式,有沒有發現到沒有`main()`的回傳型態是`int`,但卻沒有`return`,那是因為`C++`會自動讓`main()`回傳0。
---
## 遞迴
當在一個函式中使用自己就稱為遞迴,遞迴最重要的就是<font color="ff0000">設定停止條件(base case)</font>,否則你會把你的電腦給炸了(其實不會,電腦會有一個最大遞迴上限,超過了會自動終止你的程式)。
----
一樣,直接看範例
----
沒有base case:
```cpp=
#include <iostream>
using namespace std;
int f(int a){
cout << a;
return f(a);
}
int main(){
cout << f(4);
cout << "end";
}
```
----
一個正確的遞迴(費波那契數):
```cpp=
#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
```
---
## 簡單練習題:(用遞迴做做看吧~)
1. 請做出階乘函式。
2. 請做出[阿克曼函式](https://zh.wikipedia.org/wiki/%E9%98%BF%E5%85%8B%E6%9B%BC%E5%87%BD%E6%95%B8)。
3. 請做出[最大公因數函式](https://judge.tcirc.tw/ShowProblem?problemid=b001)。
4. 請做出根號函式(精度~1e-6)。
---
## struct
我們知道「相同型態的變數們」可以一格一格的放進「**陣列**」這個資料型態
可是...有些情況是不能(或不適合)用陣列把多個變數綁在一起的
----
比如說...你沒辦法用一個 2*x 的陣列把不同資料型態的兩個變數綁在一起(例如: int+char)
或者...當兩種以上資料型態相同但「性質不同」的變數綁在一起時,這個情況也不適合用陣列當「<font color='#ff0000'>複合型別</font>的資料型態」(例如:<font color='#87CEFA'>xyz座標.身高+體重+視力</font>)
----
你說這些你都可以用(一或多個)一維或二維陣列解決!?
那你就用5*n的(或5個)陣列記錄每個人的<font color='#87CEFA'>身高.體重.視力.年齡.體指率</font>阿😑😑
保證你被一堆索引或零散的陣列搞瘋(╯°□°)╯︵ ┻━┻
----
### struct
是一種<font color='#ff0000'>複合型別</font> (derived data type),在寫程式時可以大幅增加程式的結構與可讀性,減少冗餘,是個很棒的東西。
簡單來說它是一種自創的資料型態(可以將相同或不同資料型態的變數綁在一起)
還可以將不直覺的索引編號用直覺的名字代替(如:<font color='#87CEFA'>x.y</font>)
----
#### 宣告
- 用法: `struct 自訂資料型態{成員1;成員2;...函式1{ }...};`
- 說明:
-資料成員(data member):在某一個struct裡面宣告一些資料型態,而這些宣告的資料型態就是資料成員(類似於陣列中的索引)
-成員函式(member function):可以想成放在某struct目錄內的函式
----
- 寫法:等同於在struct區塊宣告變數和函式
- <font color='#ff0000'>注意:</font>struct 要在大括號後加分號,函式不用
```cpp=
struct people{
int height,weight;
double bmi;
double get_bmi(){
return bmi=weight/(height*height/100.0/100);}
};
```
----
#### 呼叫
用法: 先宣告一個資料型態為【struct名稱】的變數,再利用 `變數名稱.某成員或函式` 呼叫該變數的指定成員或函式
```cpp=
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
class也是一種複合型別,而struct跟class是實現物件導向(Object Oriented Programming/OOP)的重要角色,
- class和struct類似,但相較於struct,class多了存取標籤的功能,標籤可以拿來設定存取權限,避免因為撞名而誤用了class裡面的某些成員或函式
- 另外,我們可以用class自創標頭檔,自己寫一個好用的函式工具給其他程式使用
----
### 程式內
如果class只是單支程式的一部分的話,它就是能分類成員存取權限的struct
權限分為public、private和protected(這個我們不討論)
- public : 此成員或函式在class內、外都可以使用
- private : 只能在class內使用,或是透過public內的函數使用
透過private可以把資料封在class內(<font color='#ff0000'>封裝</font>),讓外界不要隨便存取class的資料
----
#### 宣告
- 列出成員及函式,為了方便尋找成員和函式,先不寫函式內容,以求簡短
- class中需頻繁使用的變數通常設在private,因為可以設成簡短的變數,不怕和class外的變數重複
```cpp=
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
```cpp=
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名稱】的變數,再利用 `變數名稱.某成員或函式` 呼叫該變數的指定成員或函式
```cpp=
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
---------*/
```
----
### .h
不常用的零件在程式內額外寫ok
但常用的零件如果每次要用的時候都得花時間寫一次,是不是很麻煩?
就像我們只需要知道電腦要怎麼用就好,不需要知道怎麼裝電腦,更不會在每次開電腦前,就組裝一次電腦
我們可以自己寫一個工具箱,做成.h的標頭檔
----
實作方式就是--
- 1.宣告 => 將class宣告的部分存成一個.h檔
- 2.實作 => 將實作的部分存成一個同名的.cpp檔
- 3.呼叫 => 再開一個檔案寫你的程式,就可以使用你自製的標頭檔了
- <font color='#ff0000'>注意:</font>2.3.的程式要引入第一個檔名做標頭檔,且此標頭檔要用雙引號括住而不是 `<>`
如:include"division4"
----
#### 範例
檔案名稱:division4.h
```cpp=
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
```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
```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 就可以了
----
### 建構子(constructor)
剛剛的例子中,每當我們需要將兩個數相除,就得在main裡面用兩個函式來初始化a、b的值,難道我們不能在宣告變數型態時就初始化兩數的值?
可以的,這個方法叫建構函式
----
建構函式就是寫個與 struct 或 class 同名的函式,在宣告變數的時候就能順便進行這個函式
----
建構函式:
```cpp=
division::division(int n1,int n2){
set_a(n1);
set_b(n2);
}
```
額外加上函式這個即可
----
呼叫:
```cpp=
int main() {
int x,y;
while(cin>>x>>y){
division ans(x,y);
cout<<ans.do_div()<<'\n';
}
}
```
少了兩行是不是簡潔許多阿
----
#### 多載
我們還可以根據輸入資料型態的不同,對應不同的建構函式,這個方法叫多載
---
建構函式:
```cpp=
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);
}
```
----
呼叫:
```cpp=
int main() {
division ans(7,2);
division ans2(6.25,3.11);
cout<<ans.do_div()<<'\n';
cout<<ans2.do_div()<<'\n';
}
}
```
這樣我們就不用限定輸入的資料型態必須是整數,才能使用這個函式了
----
我們能方便的呼叫的函式庫裡的函式,正是因為那些函式有經過「建構」與「多載」的處理😁。
---
## 指標
----
![](https://i.imgur.com/kvFr4cZ.png)
在第三次社課時有出現過這張圖,當時是在講變數儲存的方法,而變數所處存的位置(地址)就稱作為指標,指標可以直接讀取記憶體,利用這個特性除了可以讓程式進行得更快速,還可以達成區塊外讀取或更改變數值。
----
想要取得一個變數的指標可以透過取址運算子(`&`)來達成,這裡與位元運算子的AND不同,AND是用來運算兩個數字,而取址運算子是加在變數前的。
```cpp=
#include <iostream>
using namespace std;
int main(){
int a=7;
cout << "a=" << a << "\na的位置:" << &a;
}
```
```
/*----output
a=7
a的位置:0x61fe1c
----------*/
```
指標是由一串16進位的數字組成,這串數字所代表的就是記憶體的位置代碼,沒有其他意義。
---
### 宣告指標變數
可以透過指標變數來儲存一個變數的指標,宣告方法如下:
```cpp=
要儲存的變數資料型態 * 指標變數名稱 = &變數;
int num = 100;
int *ptr = #
```
指標前的變數型態並不代表指標的變數型態,只是用與變數相同的資料型態會更方便識別。
---
### 取值
如果想要得知這個指標位置所存的資料值就需要用到取值運算子(`*`),用法一樣是加在變數前。
```cpp=
#include <iostream>
using namespace std;
int main(){
int a=7;
int *ptr=&a;
cout << *ptr;
}
//7
```
在宣告時的`*`與取值運算子是不一樣的,宣告時的`*`代表我要宣告的是一個指標變數,而取值運算子代表的是取得該記憶體位置的值。
----
```cpp=
#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
----------------*/
```
---
### 陣列指標
直接上範例,不然很難講
```cpp=
#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++無法直接寫出回傳值為陣列的函式,所以只能夠用指標來間接達成。
```cpp=
#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] << " ";
}
}
```
---
## 練習題
其實大部分的題目都可以用函式寫,很少有專門的題目練習。
{"metaMigratedAt":"2023-06-16T15:19:50.253Z","metaMigratedFrom":"YAML","title":"C++教學講義-第五次","breaks":true,"slideOptions":"{\"theme\":\"blood\",\"transition\":\"slide\"}","contributors":"[{\"id\":\"a031de8f-38ef-4123-9d53-e13dd69cbbc3\",\"add\":5984,\"del\":993},{\"id\":\"6a5475c5-bfd3-428c-9219-c760b9000deb\",\"add\":5767,\"del\":108}]"}