第五章(循環和關系表達式)和第六章(分支語句和邏輯運算符)直接跳過,所有語言都一樣的,if/else/switch/while/for這種。直接進入第七章(函數)。
1.函數原型
以下是函數原型的例子
void cheers(int);//cheers方法的函數原型
int main()
{
using namespace std;
cheers(5);
return 0;
}
//函數的實際實現
void cheers(int n)
{
using namespace std;
for(int i=0;i<n;i++){
cout << "Cheers! ";
}
cout << endl;
}
避免使用函數原型的唯一方法是,在首次使用函數之前定義它。函數原型不要求提供變量名,有類型列表就足夠了。
函數原型的作用:
- 確保編譯器正確處理函數返回值;
- 編譯器檢查使用的參數數目是否正確;
- 編譯器檢查使用的參數類型是否正確。如果不正確,則轉換為正確的類型(如果可能的話)。
2.函數參數和按值傳遞
舉個栗子
double volume = cube(side);
cube的原型如下:
double cube(double x);
被調用時,該函數將創建一個新的名為x的double變量,并將其初始化。這樣cube執行的操作將不會影響原數據,因為cube()使用的是side的副本,而不是原來的數據(C/C+新手的話,一定要注意這塊)。
3.函數和數組
還是先舉個栗子
int sum_arr(int arr[],int n);// n是arr的size
表面上看arr是個數組,但是實際上arr是個指針。在C++中,當且僅當用于函數頭或函數原型中,int *arr 和 int arr[]的含義是相同的。它們都意味著arr是一個int指針。這塊是個知識點,面試題經常會問,數組在函數參數時是退化為指針的。不明白的同學可以嘗試理解下下面的代碼
#include <iostream>
void sizeOfArray(int arr[])
{
//函數中,arr從數組退化成指針
using namespace std;
cout << "in func arr size:" << sizeof(arr) << endl;
}
int main() {
using namespace std;
int arr[10];
cout << "arr size:" << sizeof(arr) << endl;//輸出的值為sizeof(int)*10
sizeOfArray(arr);//輸出的值為指針所占的字節數,64位mac為8
return 0;
}
將數組地址作為參數的好處是可以節省復制整個數組所需的時間和內存。如果數組很大,使用拷貝的系統開銷將非常大;程序不僅需要更多的計算機內存,還需要花時間復制大塊的數據。壞處是使用原數據增加了破壞數據的風險,可以使用const保護數組,如下:
void show_array(const int arr[],int size);//函數原型
如果嘗試在show_array的實現中嘗試修改arr,編譯器會報錯,如下
將const用于指針有一些很微妙的地方。可以用兩種不同的方式將const用于指針。第一種方法是讓指針指向一個常量對象,這樣可以防止使用該指針來修改所指向的值,第二種方法是將指針本身聲明為常量,這樣可以防止改變指針指向的位置。舉個栗子:
int age = 39;
const int *pt = &age;//一個指針,指向的是const int
*pt = 1;//不可以,因為指針指向的是const int
age = 20;//可以,因為age本身只是int,是可變的
const float g_earth = 9.80;
const float *pe = &g_earth;//可以,常量
const float g_moon = 9.99;
float * pm = &g_moon;//不可以,C++禁止這種情況
如果將指針指向指針,也是類似的規則,C++不允許如下的情況
const int **pp;
int *p;
pp = &p;//不可以,編譯器報錯
結論:如果數據類型本身并不是指針,則可以將const數據或非const數據的地址賦給const的指針,但只能將非const數據的地址賦給const指針。
并且建議盡可能將指針參數聲明為指向常量數據的指針,理由如下:
- 可以避免由于無意間修改數據而導致的編程錯誤;
- 使用const使得函數能夠處理const和非const實參,否則將只能接受非const數據。
再介紹下常量指針,如下
int a = 3;
int b = 4;
int * const pt = &a;//可以
pt = &b;//不可以
pt是一個常量指針,初始化后將不能再修改指向位置。
4.函數和二維數組
考慮如下情況:
int data[3][4];
int total = sum(data,3);
sum的函數原型應該是啥樣?答案是
int sum(int (*a)[4],int size);
這里要注意的是,多維數組的話,是有第一維會退化成指針,后面的維度都是還是數組。并且第一個參數應該是int (*a)[4],而不是int *a[4],因為int *a[4]表示一個由4個指向int的指針組成的數組。sum的另一種可讀性更強的原型是
int sum(int a[][4],int size);
如果對于上面的描述理解不上去的,可以結合下面的代碼感受下
int val = 20;
int valArray[3][4];
int *ptArray[4];//指針數組,也可以這么理解 (int *)ptArray[4],ptArray是一個數組,每個元素 int*
int *pt = &val;
ptArray[0] = pt;
int (*arrArray)[4];//arrArray是個指針,指針指向的每個元素是個int [4]類型
arrArray = valArray;//可以
ptArray = valArray;//不可以, 編譯器報錯,類型不對
5.函數和C-風格字符串
先溫習下C-風格字符串,表示的方式有三種:
- char數組;
- 用引號括起的字符串常量(也稱字符串字面值);
- 被設置為字符串的地址的char指針。
上述其實說的都是char指針(char*),因此函數原型參數都為如下
void processCStr(char *);
C風格字符串與常規char數組的一種重要區別是字符串有內置的結束字符。這意味著不必將字符串長度作為參數傳遞給函數,函數可以使用循環檢查字符串中的每個字符直到遇到結尾的空字符。
返回的字符串的方式如下:
char* returnStr(){
char *s = "string";
return s;
}
int main() {
using namespace std;
cout << "result:" << returnStr() << endl;
return 0;
}
6.函數和結構體
結構體和普通變量類似,函數都將創建參數對應的副本,函數內操作的其實是結構體變量的副本,所以在結構體變量包含的數據較多時,會有性能問題。有兩種方式可以提高效率,第一種是傳遞結構體的指針,第二種是傳遞結構體的引用(關于引用會在下一章講解),簡單舉個栗子:
#include <iostream>
struct Person {
int age;
char *name;
};
//在函數內的操作將不影響原值
void processStruct1(Person p) {
p.name = "mrlee1";
p.age = 20;
}
void processStruct2(Person *p) {
p->name = "mrlee2";
p->age = 21;
}
void processStruct3(Person &p) {
p.name = "mrlee3";
p.age = 22;
}
void printPerson(Person p) {
using namespace std;
cout << "age:" << p.age << ",name:" << p.name << endl;
}
int main() {
using namespace std;
Person originPerson = {18, "mrlee"};
//按值傳遞
processStruct1(originPerson);
printPerson(originPerson);//實際打印age:18,name:mrlee,沒有變化
//指針傳遞
processStruct2(&originPerson);
printPerson(originPerson);//實際打印age:21,name:mrlee2
//引用傳遞
processStruct3(originPerson);
printPerson(originPerson);//實際打印age:22,name:mrlee3
return 0;
}
7.函數和string對象
雖然C風格字符串和string對象的用途幾乎相同,但與數組相比,string對象與結構體更相似。例如,可以將一個結構體賦給另一個結構體,也可以將一個對象賦給結構體。如果需要多個字符串,可以聲明一個string對象數組,而且不是二維char數組。舉個栗子:
#include <iostream>
#include <sstream>
using namespace std;
void processSting(const std::string strings[], int size) {
for (int i = 0; i < size; i++) {
cout << "string[" << i << "]:" << strings[i] << endl;
}
}
/**
* 這個比較尷尬,因為C++里int轉string還是比較麻煩,目前先這么寫了
* @param n
* @return
*/
string intToString(int n) {
stringstream stream;
stream << n;
return stream.str();
}
int main() {
const int size = 5;
string strings[size];
for (int i = 0; i < size; i++) {
strings[i] = "mrlee" + intToString(i + 1);
}
processSting(strings, size);
return 0;
}
8.函數與array對象
沒啥好說的,看栗子吧:
#include <iostream>
#include <sstream>
#include <array>
#include <string>
using namespace std;
const int size = 4;
/**
* 這個比較尷尬,因為C++里int轉string還是比較麻煩,目前先這么寫了
* @param n
* @return
*/
string intToString(int n) {
stringstream stream;
stream << n;
return stream.str();
}
//按值傳遞,函數處理的是原始對象的副本
void wrongModifyArray(array<string, size> stringArray) {
for (int i = 0; i < stringArray.size(); i++) {
stringArray[i] = "modified1";
}
}
//按指針傳遞
void rightModifyArray(array<string, size> *stringArray) {
for (int i = 0; i < (*stringArray).size(); i++) {
(*stringArray)[i] = "modified" + intToString(i);
}
}
//按引用傳遞
void rightModifyArray2(array<string, size> &stringArray) {
for (int i = 0; i < stringArray.size(); i++) {
stringArray[i] = "modified2" + intToString(i);
}
}
void printArray(array<string, size> stringArray) {
for (int i = 0; i < stringArray.size(); i++) {
cout << "string" << i << ":" << stringArray[i] << endl;
}
cout << endl;
}
int main() {
array<string, size> originStringArray = {
"string1", "string2", "string3", "string4"
};
printArray(originStringArray);
wrongModifyArray(originStringArray);
printArray(originStringArray);
rightModifyArray(&originStringArray);
printArray(originStringArray);
rightModifyArray2(originStringArray);
printArray(originStringArray);
return 0;
}
打印結果如下
string0:string1
string1:string2
string2:string3
string3:string4
string0:string1
string1:string2
string2:string3
string3:string4
string0:modified0
string1:modified1
string2:modified2
string3:modified3
string0:modified20
string1:modified21
string2:modified22
string3:modified23
9.函數指針
函數這個話題比較大,這里只是簡單點一下。
與數據項相似,函數也有地址。函數的地址是存儲其機器語言代碼的內存的開始地址。還是看個栗子吧:
void func(string name) {
cout << "hello " << name << endl;
}
int main() {
//聲明函數指針
void (*funcPt)(string);
//獲取函數的地址,就是函數名其實
funcPt = func;
//使用指針調用函數
funcPt("mr.lee");
//下面方式也可以,個人傾向于下面這種,雖然寫的對了,但是表示的比較明確
(*funcPt)("mr.lau");
return 0;
}
通常,要聲明指向特定類型的函數的指針,可以首先編寫這個函數的原型,然后用形如(*pt)替換函數名即可。
下面解釋一個稍微復雜的栗子:
const double *(*pa[3])(const double *,int) = {f1,f2,f3};
在解釋這個復雜的栗子之前首先回顧一點東西
int *a[3];//a是一個數組,每個元素是int *;
int (*b)[3];//b是一個指針,指針指向的每個元素都是int[3]
f1是什么類型的?
來逐步解釋,首先運算符[]優先級高于,因此pa[3]表示p3是一個數組,這個數組包含三個元素,每個元素是指針類型。那是什么指針類型呢?是一個返回const double *,參數是const double *和int的函數,所以f1的聲明如下
const double * f1(const double *,int)
上面的代碼比較冗長,可以考慮使用typedef簡化,先看下簡單的typedef如何使用
typedef double real;
int main() {
real a = 5.4;
return 0;
}
再看如何簡化上面的函數指針
typedef double * (*p_fun)(const double *,int);//聲明別名
const double * f1(const double *,int);//聲明函數原型
p_fun func_pt = f1;//獲取指針