一個C++
程式中,往往需要包含若干個函數
,可以說函數
是C++
程式的基礎組成元件,是程式中的頭等公民。
如果要理解程式中使用函數
的具體意義,則需要了解語言發展過程中致力要解決的 2
問題:
一是完善語言的內建功能庫(API
),讓開發者不為通用功能所幹擾。
另就是通過特定的程式碼組織方案提升程式的可伸縮性、可維護性、可複用性以及安全性。
隨著專案規模的增大,分離程式碼,重構整體結構尤為重要。
函數的出現,從某種意義上講,其首要任務便是分離主函數中的程式碼,通過構建有層次性的程式碼,從而提升程式的健壯性。當然,通過函數分離程式碼是有準則的,其準則便是以重用邏輯為核心。
分離是一個大前提,有了這個大前提,便是分離的方案。
如函數設計理念、類設計理念、微服務設計理念……都是分離的思路,只是各自面對的是不同規模的專案。
C++
中使用函數分 2
步:
C++
底層執行時系統並不會為函數中的區域性變數分配空間。C++
對定義
和呼叫
這 2
個過程有順序要求,也就是必須先義再呼叫。
#include <iostream>
using namespace std;
/*
* 定義函數:
* 側重設計理念:此函數的功能是什麼?或者說,通過使用此函數能獲取到什麼樣的幫助
* 如下設計一個顯示個體資訊的函數
* 當然在設計過程時,需遵循函數的語法要求
*/
void showInfo(char names[10]){
cout<<"你好:"<<names<<endl;
}
int main(int argc, char** argv) {
char myNames[10]="果殼";
//呼叫時,函數中的程式碼方被啟用
showInfo(myNames);
return 0;
}
如上程式碼,當在main
函數中呼叫showInfo
函數時,showInfo
需要在主函數之前定義,否則編譯器會丟擲錯誤。
如果非要把函數的定義放在呼叫語法之後,也不是不可以。可通過把函數的設計過程再分拆成 2
個步驟實施:
如下所示:
#include <iostream>
using namespace std;
//宣告函數原型
void showInfo(char names[10]);
int main(int argc, char** argv) {
char myNames[10]="果殼";
//呼叫函數
showInfo(myNames);
return 0;
}
//函數定義可以放在函數呼叫之後
void showInfo(char names[10]){
cout<<"你好:"<<names<<endl;
}
原型宣告語句只要是在呼叫之前就可以。
函數和變數的區別:
stack area
)、堆中(heap area
)或全域性資料區域(data area
)。code area
)。函數的作用域與變數的作用域不同,變數因宣告位置和儲存位置不同,其作用域則會有多種情況。而函數只可能儲存存在程式碼區,C++
不允許函數巢狀定義,且只能在檔案中定義。從某種意義上講,函數的定義只有全域性概念,而無區域性概念。
如上文所述,如果函數是定義在呼叫之前,不存在呼叫不到問題,但如果定義是在呼叫之後,則需要宣告原型後才能呼叫到,宣告位置不同時,此函數的可見性也不一樣。如下述程式碼
#include <iostream>
using namespace std;
int main(int argc, char** argv) {
char myNames[10]="果殼";
//呼叫不到
showInfo(myNames );
//宣告函數原型
void showInfo(char names[10]);
//可呼叫到函數
showInfo(myNames);
return 0;
}
//函數定義在呼叫之後,呼叫時,則需要先宣告
void showInfo(char names[10]){
cout<<"你好:"<<names<<endl;
}
通過這種機制,可以限制函數的使用位置。
本文是從廣義角度討論函數,並不涉及類中函數的作用域問題。因類可以對函數進一步封裝,可以限制函數的使用範圍。
一個函數被定義後,此函數除了能在定義它的檔案中使用外,在其它檔案中同樣能使用。如下在函數01.cpp
檔案中有 showInfo
函數。
此函數可以在函數02.cpp
中使用,但是需要有提前宣告語句。
以函數
為基礎單元組織程式程式碼的方案,稱為程式導向程式設計。
程式導向指通過複用、嚴格規定函數的呼叫流程達到精簡程式碼的目的。程式導向的特點是:輕資料,重邏輯
,會導致各種不同語意的資料混亂在一起。
其原罪在於函數設計時的基本思想:
如下呼叫函數時,傳遞給函數的無論是一名學生或一隻小狗的姓名,函數沒有標準機制對資料語意加以區分。函數把資料和邏輯徹底分開,一個函數並不刻意關聯(服務於)某種特定語意的資料。
int main(int argc, char** argv) {
//顯示學生資訊
char names[10]="張三";
showInfo(names);
//顯示小狗的資訊
char dogNames[10]="小花" ;
showInfo(dogNames);
return 0;
}
C++
中給函數傳遞引數有 3
種方案。
如下定義了一個交換 2
個變數中資料的函數。
#include <iostream>
using namespace std;
//交換函數
void swap(int num1,int num2){
int tmp=num1;
num1=num2;
num2=tmp;
}
int main(int argc, char** argv) {
int num1=45;
int num2=23;
cout<<"交換之前:"<<num1<<":"<<num2<<endl;
swap(num1,num2);
cout<<"交換之前:"<<num1<<":"<<num2<<endl;
}
在主函數中呼叫swap
函數時,引數傳遞採用值傳遞方案。執行程式後,主函數中的 2
個變數的值沒有得到交換。
為什麼沒有交換成功?得先從值傳遞的特點說起:
呼叫函數時,底層執行時系統會給函數在棧中分配一個執行空間,此空間稱為棧幀。棧幀與棧幀之間是隔離的。如下圖所示:
swap
函數和main
有各自的棧幀,main
把自己的資料複製一份後交給swap
函數,swap
的邏輯操作的是自己內部變數中的資料。
對於上述程式碼,能否做到通過呼叫swap
函數達到交換主函數中變數的效果?
可以通過傳遞指標的方案,傳遞指標的特點:
傳遞指標的優點:
傳遞指標的缺點:
#include <iostream>
using namespace std;
//指標型別做為形參
void swap(int* num1,int* num2){
int tmp=*num1;
*num1=*num2;
*num2=tmp;
}
int main(int argc, char** argv) {
int num1=45;
int num2=23;
cout<<"交換之前:"<<num1<<":"<<num2<<endl;
//傳遞變數的地址(指標)
swap(&num1,&num2);
cout<<"交換之前:"<<num1<<":"<<num2<<endl;
}
下面通過圖示解釋指標傳遞的過程。
主函數中的num1
和num2
儲存的是具體的資料:45
和23
。是int
資料型別。
swap
函數中的num1
和num2
的型別是指標型別,分別儲存了主函數中num1
和num2
的記憶體地址,或者說是擁有造訪主函數中num1
和num2
變數的另一種途徑。
本質上講,變數名和變數的地址是
C++
提供的2
種存取變數的方案。變數名存取可認為是間接存取,指標存取可認為是直接存取。
理論上講,指標存取要快於變數名存取。
指標是一類資料,除了可以作為函數的引數,也可以作為函數的返回值。但在使用時,請注意如下的問題。
#include <iostream>
using namespace std;
//函數返回指標型別
int * f(){
int tmp=20;
return &tmp;
}
int ma3222in(int argc, char** argv) {
int* p=f();
cout<<*p<<endl;
}
如上程式碼,呼叫 f
函數時,返回 f
函數中 tmp
的地址。因 tmp
是區域性變數,其作用域只在函數內部有效,通過指標繞開了編譯器檢查,雖然能得到結果是 20
,但務必不要這麼使用。執行時系統也會顯示警告。
[Warning] address of local variable 'tmp' returned [-Wreturn-local-addr]
如下程式碼,可能就得不到函數呼叫後想要的結果:
#include <iostream>
using namespace std;
int * f(){
int tmp=20;
return &tmp;
}
int f1(){
int num=50;
return num;
}
int main(int argc, char** argv) {
int* p=f();
f1();
cout<<*p<<endl;
}
如上輸出結果是 50
,而不是 20
。原因是 f
函數執行完畢後,其空間被回收,且分配給 f1
繼續使用,這時p
所指向位置的資料是 f1
執行後的結果。
所以,切記不要返回區域性變數的地址。
除了通過傳遞指標,C++
還有一個傳遞參照的方案,同樣可實現傳遞指標所能達到的效果。
使用指標有很多優勢,也有明顯的缺陷,指標有自己的記憶體空間,會給理解指標以及運用指標帶來了難度。參照
是C++
的新概念,能提供指標能實現的效果,但比指標更輕量級。
參照和指標的區別:
參照和指標一樣,都會打破函數的封裝性。如下程式碼,使用參照作為函數的引數。
#include <iostream>
using namespace std;
//參照做為形參
void swap(int& num1,int& num2){
int tmp=num1;
num1=num2;
num2=tmp;
}
int main(int argc, char** argv) {
int num1=45;
int num2=23;
cout<<"交換之前:"<<num1<<":"<<num2<<endl;
//呼叫時
swap(num1,num2);
cout<<"交換之前:"<<num1<<":"<<num2<<endl;
}
呼叫時,和值傳遞是一樣的,所以,在沒有看到函數原型時,具體是參照、還是值傳遞會讓人誤判。
有一點要注意,參照不是資料型別,所以,下面的程式碼是有錯誤的。
#include <iostream>
using namespace std;
void swap(int& num1,int& num2){
int tmp=num1;
num1=num2;
num2=tmp;
}
void swap(int num1,int num2){
int tmp=num1;
num1=num2;
num2=tmp;
}
int main(int argc, char** argv) {
int num1=45;
int num2=23;
cout<<"交換之前:"<<num1<<":"<<num2<<endl;
swap(num1,num2);
cout<<"交換之前:"<<num1<<":"<<num2<<endl;
}
對於編譯器而講,認為這 2
個函數是一樣的,不會當成函數過載。
可以使用參照作為函數的返回值。
當使用參照作為返回值時,不能返回區域性變數,參照必須是建立在變數存在的基礎之上。變數都不存在,給它起一個別名,是沒有任何實際意義的。
#include <iostream>
using namespace std;
//返回變數的參照
int& f2(){
int tmp=20;
return tmp;
}
int main(int argc, char** argv) {
//參照失敗,函數執行完畢後,其區域性變數也回收
int & ref= f2();
print(ref);
}
如果不希望函數內部修改參照所指向的變數中的值,可以使用 const
限定參照。如下程式碼會提示,不能通過參照修改變數。
使用函數名呼叫函數,是常規呼叫方式。函數儲存在程式碼區,也有其記憶體地址,函數名儲存的就是函數在記憶體中的地址,也就是函數的指標。
#include <iostream>
using namespace std;
int f1(int a){
return a+1;
}
int main(int argc, char** argv) {
//f1 中儲存是函數地址
cout<<f1<<endl;
// f1()才是呼叫函數
cout<<f1(7)<<endl;
return 0;
}
所以,如上主函數中的兩行程式碼本質上是不同的。
f1
中儲存的是函數指標。f1()
表示通過 f1
找到函數在記憶體的位置,並執行其程式碼。可以宣告一個函數指標變數,用來儲存函數的記憶體地址。
#include <iostream>
using namespace std;
int f1(int a) {
return a+1;
}
int main(int argc, char** argv) {
//函數指標變數
int (* f)(int);
//儲存函數地址
f=f1;
//通過函數指標呼叫函數
cout<<f(10)<<endl;
// f(10)和(*f)(10)是兩種合法的呼叫方式
cout<<(*f)(10)<<endl;
return 0;
}
以上程式碼如果只用於普通函數呼叫,意義並不大。函數指標的意義可以讓函數作為引數、作為函數的返回值。可以認為函數在C++
中是一類特殊型別,可以如資料一樣進行傳遞。
如下程式碼,讓一個函數作為另一個函數的引數。
#include <iostream>
using namespace std;
//f1 有 3 個引數,一個是函數指標,2 個int 型別
int f1(int (*f)(int ,int), int a,int b) {
int res= f(a,b);
return res;
}
int f2(int a,int b){
return a+b;
}
int main(int argc, char** argv) {
int num1=10;
int num2=20;
//把 f2 作為 f1 的引數
int res= f1(f2,num1,num2);
cout<<res<<endl;
return 0;
}
輸出結果:30
。
把函數作為另一函數的返回型別,需要提前使用 typedef
定義函數型別 。
#include <iostream>
using namespace std;
int f2(int a,int b){
return a+b;
}
//定義函數型別
typedef int (*ftype)(int ,int);
//返回函數型別
ftype f3(){
return f2;
}
int main(int argc, char** argv) {
int num1=10;
int num2=20;
int (*f)(int ,int);
f= f3();
cout<<f(num1,num2)<<endl;
return 0;
}
輸出結果:30
。
本文主要講解函數的引數傳遞問題和函數指標。
引數傳遞有 3 種方案:
只要涉及到記憶體儲存,就會有地址一說,函數指標指函數在記憶體中的儲存位置,C++
允許開發者使用函數指標傳遞函數本身。這是非常了不起的特性。