C++基礎總結:變量和基本類型
文章目錄
一、函數基礎
- 函數調用過程:
- 將控制權轉移給調用函數,函數隱式地定義並初始化它的形參;
- 當執行到return語句或者執行完函數內全部語句後返回結果。
- 實參和形參:實參是形參的初始值
- 函數的返回類型
- 大多數類型都可以作爲函數的返回類型。void也可以,它表示不返回任何值。
- 函數的返回類型不能是數組類型或函數類型,但可以是指向數組或函數的指針。
- 局部靜態對象
- 局部靜態對象在程序的執行路徑第一次經過對象定義語句時初始化,並且直到程序最終才被銷燬。
- 局部靜態對象只能被初始化一次,不會被多次初始化!如果沒有顯示初始值,它將執行值默認初始化,內置類型的局部靜態變量初始化爲0
- 示例:
輸出結果:int count_calls() { static int count = 0; return ++count; } int main() { for(int i = 0; i < 6; ++i) std::cout <<count_calls() << ", "; }
1, 2, 3, 4, 5,
二、參數傳遞
- 形參初始化的原理和變量初始化一樣。
- 實參可以通過引用傳遞或者值傳遞給形參初始化。
1. 傳值參數
- 通過值傳遞的形參是實參的拷貝,兩者獨立,不會相互影響
- 指針形參
- 當執行指針拷貝操作時,拷貝的是指針的值。拷貝後,兩個指針是不同的指針。所以,我們可以通過指針形參訪問或修改實參指向的值,如果修改形參的指向,實參指向並不會因此改變。
2. 傳引用參數
- 拷貝大的類類型對象或者容器對象時效率低,甚至有的類類型根本不支持拷貝操作,此時我們可以使用引用形參訪問該類型的對象。
- 如果函數無須改變引用形參的值,最好將其聲明爲常量引用。
- 內置類型初始化一般會比引用效率高,所以內置類型無須使用常量引用,可以使用引用或傳值參數。
- 使用引用形參是方便返回多個結果的有效途徑。
3. const形參和實參
- 某變量或字面值是否可以用作參數傳遞,可以考慮其是否可以用於變量初始化的值。
- 我們要儘可能的使用常量引用代替普通引用。因爲普通引用會給使用不需要修改參數函數的使用者一種誤導,並且普通引用會限制可傳入參數類型。我們不能把const對象、字面值或者需要類型轉換的對象傳遞給普通的引用形參。
4. 數組形參
- 數組有連個特性,分別是:不允許拷貝數組以及使用數組時(通常)會將其轉換成指針。
- 因爲不能拷貝數組,所以無法使用值傳遞的方式使用數組參數。
- 因爲數組會被轉換成指針,所以當我們爲函數傳遞一個數組時,實際上傳遞的是指向數組首元素的指針。
- 儘管不能以值傳遞的方式傳遞數組,但我們可以把形參寫成類似數組的形式:
儘管表現形式不同,但三個函數等價:每個函數的唯一形參都是const int*類型。//儘管形式不同,但是下面三個函數是等價的 void print(const int*); void print(const int[]); //函數的用途是作用於一個數組 void print(const int[10]); //這裏的維度表示我們期望數組含有多少元素,實際並不一定
int i =0, j[2] = {0, 1}; print(&i); //正確:&i的類型是int* print(j); //正確:j轉換成int*並指向j[0]
- 爲防止數組指針越界,管理指針形參有三種常用技術:
- 使用標記指定數組長度
這種方法是在數組結尾有一個結束標誌,典型就是C風格字符數組。字符數組最後跟着一個空字符,函數在處理C風格字符串時遇到空字符停止:
這種方式適用於有明顯結束標記的數組,但對於int這種數組就不太有效了。void print(const char* cp) { if(cp) //若cp不是空指針 while(*cp) //若指針所指字符不是空字符 cout << *cp++; //輸出當前字符並指向下一個字符 }
- 使用標準庫規範
第二種方法來自標準庫技術,是傳入數組首元素和尾後元素的指針。
可以通過調用標準庫的begin和end函數,代碼安全地獲取數組首指針和尾後指針void print(const int* beg, const int* end) { //輸出beg和end之間(不含end)的所有元素 while(beg != end) cout << *beg++ << endl; //輸出當前元素並將指針向前移動一個位置 }
int j[2] = {0, 1}; print(std::begin(j), std::end(j));
- 顯式傳遞一個表示數組大小的形參
第三種方法是專門定義一個表示數組大小的形參,在C程序和舊的C++程序中常常使用這個方法。//const int ia[]等價於const int* ia //size表示數組的大小,將它顯式地傳給函數用於控制對ia元素的訪問 void print(const int ia[], size_t size) { for(size_t i = 0; i != size; ++i) { cout << ia[i] << endl; } } int j[] = {0, 1}; print(j, end(j)-begin(j));
- 使用標記指定數組長度
- 數組引用形參
C++語言允許將變量定義成數組的引用,所以,形參也可以是數組的引用。引用形參綁定到對應的實參上,也就是綁定到數組上:void print(int (&arr)[10]) //&arr兩端的括號必不可少,後邊的10與傳入數組指針形參不同,這個具有實際意義,即數組長度必須爲10 { for(auto elem : arr) cout << elem << endl; }
- 傳遞多維數組
- 和所有數組一樣,當將多爲數組傳遞給函數時,真正傳遞的是指向數組首元素的指針。
//matrix指向數組的首元素,該數組的元素是由10個整數構成的數組 void print(int (*matrix)[10], int rowSize){/* ... */} 等價於 void print(int matrix[][10], int rowSize){/* ... */} matrix看起來是一個二維數組,實際上形參是指向含有10個整數的數組的指針 int *matrix[10]; //10個指針構成的數組 int (*matrix)[10]; //指向含有10個整數的數組的指針
5. main:處理命令行選項
- 通常,我們使用空參主函數,但我們也可以給主函數傳入參數
int main(int argc, char* argv[]){...}
第一個形參是數組長度,第二個形參是C風格字符串數組,argv指向char*,所以也可以寫成:
int main(int argc, char** argv){...}
當我們使用argv中的參數時,一定要記得可選的實參從argv[1]開始,argv[0]保存程序的名字,而非用戶輸入
6. 含有可變形參的函數
C++11標準提供了兩種主要方法:如果所有實參類型相同,可以傳遞一個名爲initializer_list的標準庫類型;如果實參的類型不同,我們可以使用可變參數模板,此方法在後續的模板章節中總結。除此之外,C++還提供了一種特殊的形參類型,即省略符形參。
initializer_list形參
-
initializer_list是一種標準庫類型,用於表示某種特定類型的值的數組。initializer_list類型定義在同名的頭文件中。
-
和vector一樣,initializer_list也是一種模板類型,定義對象時,必須說明列表中所含元素的類型。
-
和vector不同的是,initializer_list對象中的元素永遠是常量值,無法改變initializer_list對象中元素的值
-
示例:
void error_msg(initializer_list<string> il) { for(auto beg = il.begin(); beg != il.end(); ++beg) cout << *beg << " "; } error_msg({"functionx", "okay"}); //通過一對花括號傳入序列
-
initializer_list提供的操作
操作 說明 initializer_list lst; 默認初始化;T類型元素的空列表 initializer_list lst{a, b, c…}; lst的元素數量和初始值一樣多;lst的元素是對應初始值的副本;列表中的元素是const lst2(lst) 拷貝或賦值一個initializer_list對象不會拷貝列表中的元素;拷貝後,原始列表和副本共享元素 lst2 = lst 拷貝或賦值一個initializer_list對象不會拷貝列表中的元素;拷貝後,原始列表和副本共享元素 lst.size() 類表中的元素數量 lst.begin() 返回指向lst中首元素的指針 lst.end() 返回指向lst中尾後元素的指針
省略符形參
省略符形參是爲C++便於訪問某些特殊的C代碼而設置的,這些代碼使用了名爲varargs的C標準庫功能。通常,省略符形參不應用於其他目的。
省略符形參應該僅僅用於C和C++通用的類型。特別注意的是,大多數類類型的對象在傳遞給省略符形參時都無法正確拷貝。
省略符形參只能出現在形參列表的最後一個位置
void foo(parm_list, ...);
void foo(...);
我們可以添加cstdarg文件,使用其中定義的函數方法訪問省略符形參
#include <cstdarg>
void VarArgFunc(int i, ...)
{
va_list pArg = nullptr; //創建參數指針
va_start(pArg, i) //初始化參數指針
int a = va_arg(pArg, int); //獲取第一個參數
char b = va_arg(pArg, char); //獲取第二個參數
va_end(pArg); //清空參數指針
}
三、返回類型和return語句
1. 有返回值函數
- 返回一個值的方式和初始化一個變量和形參的方式完全一樣:返回的值用於初始化調用點的一個臨時變量,該臨時量就是函數調用的結果。
- 不要返回局部對象的引用或指針!所以,我們返回的引用或指針需要是函數調用之前就已經存在的變量。
- 調用一個返回引用的函數會得到左值,其它返回類型得到右值。
- C++11新標準規定,函數可以返回花括號包圍的值的列表。
- main函數可以沒有顯式返回值,main函數的返回值可以看作是狀態指示器。返回0表示執行成功,返回其他值表示執行失敗。爲了使返回值與機器無關,cstdlib頭文件定義了兩個預處理變量,我們可以使用這兩個變量表示成功與失敗:EXIT_FAILURE和EXIT_SUCCESS
- 如果一個函數調用了自身,無論是直接還是間接,都稱該函數爲遞歸函數。如果函數出現不斷調用自身直到程序棧空間耗盡爲止,我們稱其爲循環遞歸。另外,main函數不能調用自己。
2. 返回數組指針
- 因爲數組不能拷貝,所以函數不能返回數組。但是,函數可以返回數組的指針或引用。
使用類型別名
typedef int arrT[10]; //arrT是一個類型別名,表示的類型是含有10個整數的數組
using arrT = int[10]; //與上條等價
arrT* func(int i); //func返回一個指向含有10個整數的數組的指針
聲明一個返回數組指針的函數
int arr[10]; //arr是一個含有10個整數的數組
int* p1[10]; //p1是一個含有10個指針的數組
int (*p2)[10] = &arr; //p2是一個指針,它指向含有10個整數的數組
下面的函數聲明:
int (*func(int i))[10];
- func(int i)表示調用func函數時需要一個int類型的實參
- (*func(int i))意味着我們可以對函數調用的結果執行解引用操作
- (*func(int i))[10]表示解引用func的調用將得到一個大小是10的數組
- int (*func(int i))[10]表示數組中的元素是int類型
使用尾置返回類型
尾置返回類型是C++11的新標準,常用語比較複雜的返回類型,比如數組的指針或數組的引用。
下例爲函數的聲明:
auto func(int i) -> int(*)[10]; //返回類型放到形參之後,原來放置返回類型的地方使用auto
四、函數重載
同一作用域內,函數名相同,形參列表不同的函數稱之爲重載函數。
main函數不能重載。
重載和const形參
擁有頂層const的形參無法與另一個沒有頂層const的形參區分:
Record lookup(Phone);
Record lookup(const Phone); //重複聲明瞭Record lookup(Phone)
Record lookup(Phone*);
Record lookup(Phone* const); //重複申明瞭Record lookup(Phone*)
底層const可以重載:
Record lookup(Account&); //函數作用於Account引用
Record lookup(const Account&); //新函數,作用於常量引用
Record lookup(Account*); //新函數,作用於指向Account的指針
Record lookup(const Account*); //新函數,作用於指向常量的指針
const_cast和重載
string& shorterString(string& s1, string& s2)
{
auto& r = shorterString(const_cast<const string&>(s1),
const_cast<const string&>(s2));
return const_cast<string&>(r);
}
五、特殊用途語言特性
1. 默認實參
- 局部變量不能作爲默認實參
2. 內聯函數和constexpr函數
調用函數一般比求等價表達式的值慢一些。大多數機器上,函數調用過程爲:調用前要先保存寄存器,並在返回時恢復;可能需要拷貝實參;程序轉向一個新的位置繼續執行。
內聯函數可避免函數調用的開銷
在函數聲明前加inline,可以將其聲明成內聯函數。內聯函數是向編譯器發送請求,將其在編譯時直接展開到函數調用處,當然,編譯器也可以忽略這個請求。
很多編譯器都不支持內聯遞歸調用。
constexpr函數
constexpr函數是指能用於常量表達式的函數。定義constexpr函數需要遵循約定:
- 函數的返回類型及所有形參的類型都得是字面值類型
- 函數體中必須有且只有一條return語句:
constexpr int new_sz() {return 42;}
constexpr int foo = new_sz(); //正確:foo是一個常量表達式
如果arg是常量表達式,則scale(arg)也是常量表達式:
constexpr size_t scale(size_t cnt){return new_sz() * cnt;}
int arr[scale(2)]; //正確:scale(2)是常量表達式
int i = 2;
int a2[scale(i)]; //錯誤:scale(i)不是常量表達式
constexpr函數不一定返回常量表達式。
內聯函數和constexpr函數通常定義在頭文件中。
3. 調試幫助
程序可以包含一些用於調試的代碼,但是這些代碼只在開發程序時使用,當應用程序開發完成後,要先屏蔽調試代碼。這種方法用到兩項預處理功能:assert和NDEBUG。
assert預處理宏
- assert是一種預處理宏,預處理宏是一個預處理變量,行爲類似於內聯函數。
- assert宏定義在cassert頭文件中。
- 可以像下面代碼一樣,檢查表達式輸出結果,如果表達式爲假(0),assert輸出信息並中止程序執行;如果爲真(非0),程序繼續執行:assert(expr);
NDEBUG預處理變量
assert的行爲依賴於一個名爲NDEBUG的預處理變量的狀態。如果定義了NDEBUG,則assert什麼也不做。默認狀態下,沒有定義NDEBUG,此時assert將檢查運行時檢查。
六、函數指針
函數指針指向的是函數,而非對象。函數的類型由他的返回類型和形參類型共同決定,與函數名無關。要想聲明一個可以指向函數的指針,只需要用指針替代函數名即可:
bool (*pf)(const string&, const string&); //未初始化
*pf兩端的括號必不可少。如果不寫括號,則pf是一個返回值爲bool*的函數
使用函數指針
當函數名作爲值使用時,該函數自動地轉換成指針。
pf = lengthCompare; //pf指向名爲lengthCompare的函數
pf = &lengthCompare; //等價的賦值語句:取地址符可選
我們還可以直接使用指向函數的指針調用該函數,無須提前解引用指針:
bool b1 = pf("hello", "goodbye"); //調用lengthCompare函數
bool b2 = (*pf)("hello", "goodbye"); //一個等價的調用
bool b3 = lengthCompare("hello", "goodbye");//另一個等價的調用
函數指針形參
和數組類似,雖然不能定義函數類型的形參,但是形參可以是指向函數的指針。此時,形參看起來是函數類型,實際卻是指針:
void useBigger(const string& s1, const string& s2, bool pf(const string&, const string&));
等價於
void useBigger(const string& s1, const string& s2, bool(*pf)(const string&, const string&));
也可以直接把函數作爲實參使用,此時他會自動轉換成指針
useBigger(s1, s2, lengthCompare);
類型別名和decltype可以簡化函數指針的代碼:
//Func和Func2是函數類型
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2; //等價的類型
//FuncP和FuncP2是指向函數的指針
typedef bool(*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2; //等價的類型
可以使用以下形式聲明useBigger:
void useBigger(const string&, const string&, Func);
void useBigger(const string&, const string&, Func2);
返回指向函數的指針
和數組類似,雖然不能返回一個函數,但是可以返回指向函數類型的指針。
要想聲明一個返回函數指針的函數,最簡單的方法是使用類型別名:
using F = int(int*, int); //F是函數類型,不是指針
using PF = int(*)(int*, int); //PF是指針類型
我們必須顯式地將返回類型指定爲指針:
PF f1(int); //正確:PF是指向函數的指針,f1返回指向函數的指針
F f1(int); //錯誤:F是函數類型,f1不能返回一個函數
F* f1(int); //正確:顯式地指定返回類型時指向函數的指針
我們也能直接聲明f1:
int (*f1(int))(int*, int);
也可以使用尾置返回類型的方式:
auto f1(int) -> int (*)(int*, int);
將auto和decltype用於函數指針類型
string::size_type sumLength(const string&, const string&);
string::size_type largerLength(const string&, const string&);
//getFcn函數返回指向sumLength或者largerLength的指針
decltype(sumLength)* getFcn(const string&);
當我們將decltype作用於某個函數時,它返回函數類型而非指針類型。因此,我們顯式地加上*以表明我們需要返回指針,而非函數本身。