目錄
- 一、缺省參數
- 二、函數重載
- 1. 函數類型不同
- 2. 參數個數不同
- 3、函數類型順序不同
- 三、引用
- 1、引用的概念和定義
- 2、引用的功能
- 2.1 功能1: 做函數形參,修改形參影響實參
- 2.2 功能2: 做函數形參,減少拷貝,提高效率
- 2.3 功能3:引用做返回值類型,修改返回對象
- 2.4 功能4: 引用做返回值類型,減少拷貝,提高效率
- 3、引用的特性
- 4、`const`引用
- 5、指針和引用的關系
- 6、`inline`
- 7、`nullptr`
【C++】入門基礎【上】<–請點擊
個人主頁<—請點擊
C++專欄<—請點擊
一、缺省參數
- 缺省參數是聲明或定義函數時為函數的參數指定?個缺省值。在調用該函數時,如果沒有指定實參則采用該形參的缺省值,否則使用指定的實參,缺省參數分為全缺省和半缺省參數。(有些地方把缺省參數也叫默認參數)。
下面我們通過代碼來認識一下缺省參數。
void func(int a = 10)
{cout << a << endl;
}
我們知道,在C語言中函數定義中變量是不能給值的,但在C++
中卻可以,這時候,給定的這個值就是缺省值,那這個參數就是缺省參數。
那了解了缺省參數是什么,那么它在使用的時候又該怎么用能?請看代碼:
func();
func(5);
此時我們要調用我們定義的函數,并且一個不給實參,一個給實參,讓我們一起看一下代碼的運行情況。
可以看到,沒給實參的,函數默認使用了缺省值,而給定實參的,函數使用的是給定的實參,而在C語言中,如果我們不給實參但函數又需要實參時,此時程序就會報錯,所以C++的缺省參數優化了這一問題。
- 全缺省就是全部形參給缺省值,半缺省就是部分形參給缺省值。
C++
規定半缺省參數必須從右往左依次連續缺省,不能間隔跳躍給缺省值。
如果你這樣定義代碼:
void func1(int a = 100, int b)
{cout << a<< " "<< b << endl;
}
會報錯:
因為半缺省參數必須從右往左依次連續缺省。
-
帶缺省參數的函數調用,C++規定必須從左到右依次給實參,不能跳躍給實參。
像上圖打算只給第二個傳實參,跳過第一個時就會報錯。 -
函數聲明和定義分離時,缺省參數不能在函數聲明和定義中同時出現,規定必須函數聲明給缺省值。
void func2(int a = 100);
void func2(int a = 100)
{cout << a << endl;
}
應改為:
void func2(int a = 100);
void func2(int a)
{cout << a << endl;
}
二、函數重載
我們知道C語言中不支持在同一作用域
中出現同名函數
的,它會報錯,但在C++
中是被允許的。C++
支持在同?作用域中出現同名函數,但是要求這些同名函數的形參不同
,可以是參數個數不同
或者類型不同
。如果全部相同那在C++
中也是不被允許的。
1. 函數類型不同
void func(int x, int y)
{cout << x + y << endl;
}void func(double x, double y)
{cout << x + y << endl;
}
測試結果:
從上面的測試結果,可以看出函數的調用成功而且正確。
2. 參數個數不同
void f()
{cout << "hello world!" << endl;
}void f(int a)
{cout << a << endl;
}
測試結果:
注意
:這里的第二個函數的參數不能帶缺省值,如果帶缺省值的話,當我們調用f()
時,第一個函數和第二個函數都滿足,此時程序會報出下面的錯誤:
這樣編譯器就會不知道調用誰,所以我們寫重載函數的時候一定要注意區分它們,不能讓它們存在歧義。
3、函數類型順序不同
void fd(int a, double b)
{cout << "fd(int a, double b)" << endl;
}void fd(double b, int a)
{cout << "fd(double b, int a)" << endl;
}
測試結果:
三、引用
1、引用的概念和定義
引用不是新定義?個變量,而是給已存在變量取了?個別名,編譯器不會為引用變量開辟內存空間,它和它引用的變量共用同?塊內存空間。
引用的用法:類型& 引?別名 = 引?對象;
C++
中為了避免引入太多的運算符,會復用C語言
的?些符號,引用和取地址使用了同?個符號&
,大家注意使用方法角度區分就可以。
#include<iostream>
using namespace std;int main()
{int a = 10;//b和c是a的別名int& b = a;int& c = a;cout << a << endl;b++;cout << a << endl;c++;cout << a << endl;return 0;
}
結果:
#include<iostream>
using namespace std;int main()
{int a = 10;//b和c是a的別名int& b = a;int& c = a;//d也是a的別名int& d = b;cout << &a << endl;cout << &b << endl;cout << &c << endl;cout << &d << endl;return 0;
}
它們的地址都是一模一樣的。
2、引用的功能
2.1 功能1: 做函數形參,修改形參影響實參
我們在C語言階段實現交換函數的時候是用指針來實現的,現在引用也可以起到這樣的作用。
#include<iostream>
using namespace std;void Swap(int& a, int& b)
{int tmp = a;a = b;b = tmp;
}int main()
{int x = 10;int y = 20;Swap(x, y);cout << "x=" << x << endl;cout << "y=" << y << endl;return 0;
}
交換結果:
2.2 功能2: 做函數形參,減少拷貝,提高效率
不使用引用時,值傳遞會觸發原對象的拷貝,如果對象比較大(如包含復雜成員,動態內存或嵌套結構),拷貝操作的時間和空間開銷顯著。
而當使用引用時,引用就是原對象的別名,傳遞時不會產生拷貝,直接操作原始對象,省去了拷貝構造的開銷,尤其對大型對象效果顯著。
2.3 功能3:引用做返回值類型,修改返回對象
#include <iostream>
using namespace std;int& func(int* a, int n)
{return a[n];
}void print(int* a,int n)
{for (int i = 0;i < n;i++){cout << a[i]<<" ";}cout << endl;
}int main()
{int a[11];for (int i = 0;i < 11;i++){a[i] = i + 1;}print(a, 11);func(a, 2)+=10;print(a, 11);
}
從上面代碼中可以看出,func
函數的返回值類型是引用充當的,這樣的好處是可以更改返回對象。
運行結果:
從運行結果可以看出數組中下標為2
的位置被更改了。
2.4 功能4: 引用做返回值類型,減少拷貝,提高效率
我們知道當函數調用結束時函數棧幀會被銷毀,那么其中定義的局部變量的生命周期也就結束了,自然也會被銷毀,當函數的返回值是函數中定義的局部變量時,編譯器會將返回值拷貝下來,然后儲存在臨時變量中作為返回值。
假設有如下函數:
int func()
{int set = 10;return set;
}
那既然傳引用返回可以更改返回對象,那傳值返回可以嗎?下面我們來試一下。我們還是使用傳引用返回的代碼,但是將傳引用返回改為傳值返回。
發現程序會報出以下錯誤:
這是因為函數返回的是值,而在函數返回值之前同樣會將返回的值拷貝下來,儲存在臨時變量中進行返回,而臨時變量它具有常性,是不可修改的,所以才會報出以上錯誤。
下圖是它們三者之間的區別:
所以說引用做返回值類型,減少了拷貝,提高了效率。
產生臨時變量的情況:
當出現類型轉換的時候也會產生臨時變量,像
double
類型d=1.5
,轉化為int
類型的x
這種情況,會產生一個臨時變量存儲轉換結果,然后再將臨時變量賦值給x
。
產生臨時變量的情況有以下幾種:
類型轉換、值傳遞、表達式求值
等等。
其中值傳遞就是我們上圖展示的情況,表達式求值例如:
int a = 1;
int b = 9;
int c = a + b * 10;
計算
b*10
時生成臨時int
,再與a
相加生成臨時int
,最后賦值給c
。
不安全的引用寫法:
int& func()
{int set = 10;return set;
}
原因:set
是局部變量,func
結束后,set
就銷毀了,返回它的別名本質也是一種類似野指針的行為。
3、引用的特性
- 引用在定義時必須初始化;
- ?個變量可以有多個引用;
- 引用?旦引用?個實體,再不能引用其他實體。
引用無法改變指向所以在鏈式結構中無法替代指針,這樣的場景下必須使用指針。
4、const
引用
可以引用?個const
對象,但是必須用const
引用。const
引用也可以引用普通對象,因為對象的訪問權限在引用過程中可以縮小,而不能放大。
權限放大的錯誤樣例:
const int x = 10;
int& y = x;
最初定義的x
的本意就是x
不可更改,但卻使用int&
引用x
這就導致了沖突,它的權限被放大了。
正確的使用:
const int x = 10;
const int& y = x;
注意
:以下這種情況中沒有權限的放大。
const int x = 10;
const int& y = x;
int z = y;
有人在學習完成引用后他們會認為上面這段代碼涉及到了權限的放大,但上面代碼的意圖是定義一個變量z
,并將y
值賦值給z
,只是一個簡單的賦值操作,注意不要混淆了。
權限的縮小是被允許的:
就像老師對你說,下課不準出校門,但你連教室門都不出這種情況一樣。
int x = 10;
const int& y = x;
5、指針和引用的關系
- 語法概念上引用是?個變量的取別名,不開空間,指針是存儲?個變量地址,要開空間。
- 引用在定義時必須初始化,指針建議初始化,但是語法上不是必須的。
- 引用在初始化時引用?個對象后,就不能再引用其他對象;而指針可以在不斷地改變指向對象。
- 引用可以直接訪問指向對象,指針需要解引用才能訪問指向對象。
sizeof
中含義不同,引用結果為引用類型的大小,但指針始終是地址空間所占字節個數(32
位平臺下占4
個字節,64
位下是8
個字節)- 指針很容易出現空指針和野指針的問題,引用很少出現,引用使用起來相對更安全?些。
指令匯編角度:引用是使用指針實現的。
int x = 10;
int& y = x;
int* py = &x;
轉到指令匯編:
引用的下面兩行是引用語句的匯編代碼,而指針下面兩行是指針語句的匯編代碼。我們從上圖可以看出兩者一模一樣,所以進一步印證了引用是使用指針實現的。
6、inline
用inline
修飾的函數叫做內聯函數,編譯時C++
編譯器會在調用的地方展開內聯函數,這樣調用內聯函數就不需要建立函數棧幀了,就可以提高效率。
C語言實現的宏函數也會在預處理時替換展開**,但是宏函數實現很復雜且很容易出錯,還不能調試,C++
設計inline
目的就是替代C語言的宏函數。**
正常的函數:
int add(int x, int y)
{int sum=x + y;return sum;
}
執行語句:int ret = add(2, 3);
時,它的反匯編代碼是這樣的:
圖片中有一個call
指令,這個指令是調用add
函數,說明函數沒有在預處理時展開。
inline
修飾的函數:
inline int add(int x, int y)
{int sum=x + y;return sum;
}
執行語句:int ret = add(2, 3);
時,它的反匯編代碼是這樣的:
從上圖可以看出它沒有調用函數,也就是沒有創建函數棧幀,而是在預處理階段
就展開了,像C語言的宏函數一樣。這樣就可以提高效率。
inline
對于編譯器而言只是?個建議,也就是說,你加了inline
,編譯器也可以選擇在調用的地方不展開,不同編譯器關于inline
什么情況展開各不相同,因為C++
標準沒有規定這個。inline
適用于頻繁調用的短小函數,對于遞歸函數,代碼相對多?些的函數,加上inline
也會被編譯器忽略。
inline
不建議聲明和定義分離到兩個文件,分離會導致鏈接錯誤。因為inline
被展開,就沒有函數地址,鏈接時會出現報錯。我們知道編譯器生成可執行程序會經過預處理、編譯、匯編、鏈接
等過程,而在鏈接過程代碼中的普通函數的地址需要到XXX.o
符號表中去尋找,因為他們不會展開所以它們的函數地址會進入符號表中,當你正確使用inline
修飾函數,即聲明和定義不分離時,inline
修飾的函數調用的地方已經正常展開了,而當你聲明和定義分離時,由于inline
修飾的函數的地址它本身不會進入XXX.o
符號表,又因為你沒有inline
修飾的函數定義,此時函數調用的地方就沒有正常展開,編譯器尋找地址的時候又找不到,此時就會報鏈接錯誤。
聲明和定義分離的錯誤情況:
F.h
#include <iostream>
using namespace std;inline int add(int x, int y);
F.cpp
#include "F.h"inline int add(int x, int y)
{int sum = x + y;return sum;
}
test.cpp
#include"F.h"int main()
{int ret = add(2, 3);return 0;
}
鏈接錯誤:
而當你聲明和定義不分離,再將F.cpp
中的定義刪去,(因為此時F.h
中已經有了,如果你這里不刪除的話它依舊會報錯,因為出現了兩個一摸一樣的函數主體),即:
F.h
#include <iostream>
using namespace std;inline int add(int x, int y)
{int sum = x + y;return sum;
}
這樣就可以正常展開了。
拓展
:inline
與static
所修飾的函數都具有內部鏈接屬性,不會進入XXX.o
符號表中,所以不會造成C2084
類型錯誤:
7、nullptr
NULL
實際是?個宏,在傳統的C頭文件stddef.h
中,可以看到如下代碼:
#ifndef NULL#ifdef __cplusplus#define NULL 0#else#define NULL ((void *)0)#endif
#endif
上面這段代碼是條件編譯指令,感興趣的小伙伴點擊–>【C語言】編譯和鏈接、預處理詳解
C++
中NULL
可能被定義為字面常量0
,或者C
中被定義為無類型指針(void*)
的常量。不論采取何種定義,在使用空值的指針時,都不可避免的會遇到一些麻煩,例如:
#include <iostream>
using namespace std;void f(int n)
{cout << "f(int n)" << endl;
}
void f(int* ptr)
{cout << "f(int* ptr)" << endl;
}int main()
{f(0);f(NULL);return 0;
}
這段代碼的執行結果是:
本想通過
f(NULL)
調?指針版本的f(int*)
函數,但是由于NULL
被定義成0
,調用了f(int x)
,因此與程序的初衷相悖。f((void*)NULL);
調用會報錯。
為了解決這個問題,C++11
中引入了nullptr
,nullptr
是?個特殊的關鍵字,nullptr
是?種特殊類型的字面量,它可以轉換成任意其他類型的指針類型。使用nullptr
定義空指針可以避免類型轉換的問題,因為nullptr
只能被隱式地轉換為指針類型,而不能被轉換為整數類型。
#include <iostream>
using namespace std;void f(int n)
{cout << "f(int n)" << endl;
}
void f(int* ptr)
{cout << "f(int* ptr)" << endl;
}int main()
{f(0);f(NULL);f(nullptr);return 0;
}
這樣就解決了這個問題。所以在C++
中初始化指針為空,會用nullptr
這個關鍵字初始化。
總結:
以上就是本期博客分享的全部內容啦!如果覺得文章還不錯的話可以三連支持一下,你的支持就是我前進最大的動力!
技術的探索永無止境! 道阻且長,行則將至!后續我會給大家帶來更多優質博客內容,歡迎關注我的CSDN賬號,我們一同成長!
(~ ̄▽ ̄)~