目錄
一.iostream文件
二.命名空間
2.1.命名空間的定義
2.2.命名空間的使用
三.C++的輸入輸出
四.缺省參數(shù)
4.1.缺省參數(shù)概念
4.2.缺省參數(shù)分類
4.3.缺省參數(shù)注意事項
4.4.缺省參數(shù)用途
五.函數(shù)重載
5.1.重載函數(shù)概念
5.2.C++支持函數(shù)重載的原理--名字修飾(name Mangling)
5.3.extern "C"
六.引用
6.1.引用的概念
6.2.引用的特性
6.3.引用的使用場景
6.3.1.引用作為函數(shù)參數(shù)
6.3.2.引用作為函數(shù)返回值
6.4.傳值和傳引用效率比較
值和引用的作為參數(shù)的性能比較
值和引用的作為返回值類型的性能比較?
6.5.常引用
6.6.引用和指針的區(qū)別
七.內(nèi)聯(lián)函數(shù)
7.1.內(nèi)聯(lián)函數(shù)定義
7.2.內(nèi)聯(lián)函數(shù)特性
7.3.內(nèi)聯(lián)函數(shù)與宏函數(shù)的區(qū)別
八.auto關(guān)鍵字
8.1.auto簡介
8.2.auto的使用細則
8.3.auto不能推導的場景
九.基于范圍的for循環(huán)
9.1.范圍for的語法
9.2.范圍for的使用條件
十.指針空值nullptr
前言:
C++是在C的基礎(chǔ)之上,容納進去了面向?qū)ο缶幊趟枷?,并增加了許多有用的庫,以及編程范式
等。熟悉C語言之后,對C++學習有一定的幫助,本章節(jié)主要目標:
- 補充C語言語法的不足,以及C++是如何對C語言設(shè)計不合理的地方進行優(yōu)化的,比如:作用
- 域方面、IO方面、函數(shù)方面、指針方面、宏方面等;
- 為后續(xù)類和對象學習打基礎(chǔ)。
首先,我們先來編寫一個簡單的C++程序:
#include<iostream>
using namespace std;
int main()
{
cout << "hello C++" << endl;
return 0;
}
接下來針對該程序中的主要語法進行詳細講解。
一.iostream文件
iostream是標準的C++頭文件,在舊的標準C++中,使用的是iostream.h,實際上這兩個文件是不同的,在編譯器include文件夾里它是兩個文件,并且內(nèi)容不同?,F(xiàn)在C++標準明確提出不支持后綴為.h的頭文件,為了和C語言區(qū)分開,C++標準規(guī)定不使用后綴.h的頭文件。這不只是形式上的改變,其實現(xiàn)也有所不同。
二.命名空間
using namespace std:該段代碼是引用全局命名空間,在講解全局命名空間之前,先來學習一下什么是命名空間。命名空間實際上是由程序設(shè)計者命名的內(nèi)存區(qū)域,程序設(shè)計者可以根據(jù)需要指定一些有名字的空間區(qū)域,把一些自己定義的變量,函數(shù)等標識符存放在這個空間中,從而與其他實體定義分隔開來。
案例分析:
#include <stdio.h>
int rand = 0;
int main()
{
printf("%d\n", rand);
return 0;
}
運行結(jié)果:
?
但是,當我們加上頭文件:#include<stdlib.h>
#include <stdio.h>
#include <stdlib.h>
int rand = 0;
int main()
{
printf("%d\n", rand);
return 0;
}
運行結(jié)果:
?
可以看出, 在加上頭文件stdlib之后,程序卻運行出錯。究其原因可以發(fā)現(xiàn):在頭文件stdlib中已經(jīng)定義了名為rand的函數(shù),而編譯器又無法區(qū)分所打印的rand是函數(shù)還是變量,所以編譯器在運行程序的過程中會提示“rand”重定義,最終導致程序運行出錯。
在C/C++中,變量、函數(shù)和后面要學到的類都是大量存在的,這些變量、函數(shù)和類的名稱將都存
在于全局作用域中,可能會導致很多沖突。使用命名空間的目的是對標識符的名稱進行本地化,
以避免命名沖突或名字污染,namespace關(guān)鍵字的出現(xiàn)就是針對這種問題的。
2.1.命名空間的定義
namespace 空間名 {......}
namespace是定義命名空間的關(guān)鍵字,空間名可以用任意合法的標識符,在{ }內(nèi)聲明空間成員,例如定義一個命名空間A1,代碼如下所示:
namespace A1
{
int a = 10;
}
則變量a只在A1空間內(nèi)({ }作用域)有效,命名空間的作用就是建立一些互相分隔的作用域,把一些實體定義分隔開來。
正常的命名空間定義:
namespace N
{
//命名空間中可以定義變量/函數(shù)/類型
//定義變量
int rand = 10;
//定義函數(shù)
int Add(int left, int right)
{
return left + right;
}
//定義類型
struct Node
{
struct Node* next;
int val;
};
}
嵌套的命名空間定義
namespace N1
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N2
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
同一個工程中允許存在多個相同名稱的命名空間,編譯器最后會合成同一個命名空間中
注意:一個工程中的test.h和上面test.cpp中兩個N1會被合并成一個
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
注意:
一個命名空間就定義了一個新的作用域,命名空間中的所有內(nèi)容都局限于該命名空間中。
2.2.命名空間的使用
當命名空間外的作用域要使用空間內(nèi)定義的標識符時,有三種方法可以使用:
a.用空間名加上作用域標識符“::” 來標識要引用的實體
namespace sql
{
namespace A
{
int rand = 0;
//定義函數(shù)
void func()
{
printf("func()\n");
}
}
}
int main()
{
printf("%d\n", sql::A::rand);
return 0;
}
在引用處指明變量所屬的空間,就可以對變量進行操作了。
b.使用using關(guān)鍵字,在要引用空間實體的上面,使用using關(guān)鍵字引入要使用的空間變量
namespace sql
{
namespace A
{
int sum = 0;
//定義函數(shù)
void func()
{
printf("func()\n");
}
}
}
int main()
{
printf("%d\n",sql::A::sum);
return 0;
}
?這種情況下,只能使用using引入的標識符,如以上代碼中只引入了sum,如果sql空間里還有標識符b,則b不能被使用,但可以使用sql::A::b的形式。
c.使用using關(guān)鍵字直接引入要使用的變量所屬的空間
namespace sql
{
namespace A
{
int sum = 0;
//定義函數(shù)
void func()
{
printf("func()\n");
}
}
}
using namespace sql::A;
int main()
{
printf("%d\n",sum);
return 0;
}
但這種情況如果引入多個命名空間往往容易出錯,例如,定義了兩個命名空間,兩個空間都定義了變量a,如下所示:
namespace A1
{
int a = 10;
}
namespace A2
{
int a = 20;
}
using namespace A1;
using namespace A2;
int main()
{
printf("%d\n",a);//引起編譯錯誤
}
這樣在輸出a時就會出錯,因為A1和A2空間都定義了a變量,引入不明確,編譯出錯。因此只有在使用命名空間數(shù)量很少,以及確保這些命名空間中沒有同名成員時才使用using namespace語句。
在編寫C++程序時,由于C++標準庫中的所有標識符都被定義于一個名為std的namespace中,所以std又叫作標準命名空間,要使用其中定義的標識符就要引入std空間。
三.C++的輸入輸出
當我們在屏幕上輸出“hello C++”時,讀者或許會吃驚,為什么不是printf()。其實printf()函數(shù)也可以,但它是C語言的標準輸出函數(shù)。在C++中輸入輸出都是以“流”的形式實現(xiàn)的,C++定義了iostream流類庫,它包含兩個基礎(chǔ)類istream和ostream,用于表示輸入流和輸出流,并在庫中定義了標準輸入流對象cin和標準輸出流對象cout,分別用于處理輸入和輸出。
cin與提取運算符“>>”結(jié)合使用,用于讀入用戶輸入,以空白(包括空格,回車,TAB)為分隔符。
cout與插入運算符“<<”結(jié)合使用,用于打印消息。通常它還會與操作符endl使用,endl的效果是結(jié)束當前行,并將與設(shè)備關(guān)聯(lián)的緩沖區(qū)(buffer)中的數(shù)據(jù)刷新到設(shè)備中,保證程序所產(chǎn)生的的所有輸出都被寫入輸出流,而不是僅停留在內(nèi)存中。
注意:
使用cout標準輸出對象(控制臺)和cin標準輸入對象(鍵盤)時,必須包含< iostream >頭文件
以及按命名空間使用方法使用std。
案例:
#include<iostream>
using std::cout;
using std::endl;
int main()
{
//<<:流插入運算符
std::cout << "hello world!\n" << std::endl;
//等價于
//std::cout << "hello world!\n" << "\n";
cout << "hello world!\n" << "\n";
int i = 11;
double d = 11.11;
printf("%d %lf\n", i, d);
//自動識別類型
//std::cout << i << " " << d << std::endl;
cout << i << " " << d << std::endl;
//>>:流提取
scanf("%d%lf",&i,&d);
std::cin >> i >> d;
std::cout << i << " " << d << std::endl;
return 0;
}
運行結(jié)果:
?
std命名空間的使用慣例:
std是C++標準庫的命名空間,如何展開std使用更合理呢?
- 在日常練習中,建議直接using namespace std即可,這樣就很方便;
- using namespace std展開,標準庫就全部暴露出來了,如果我們定義跟庫重名的類型/對象/函數(shù),就存在沖突問題。該問題在日常練習中很少出現(xiàn),但是項目開發(fā)中代碼較多、規(guī)模大,就很容易出現(xiàn)。所以建議在項目開發(fā)中使用,像std::cout這樣使用時指定命名空間 +using std::cout展開常用的庫對象/類型等方式。
四.缺省參數(shù)
C++的函數(shù)也支持默認參數(shù)機制,即在定義定義或聲明函數(shù)時給形參一個初始值,在調(diào)用函數(shù)時,如果不傳遞實參就使用默認參數(shù)數(shù)值。
4.1.缺省參數(shù)概念
缺省參數(shù)是聲明或定義函數(shù)時為函數(shù)的參數(shù)指定一個缺省值。在調(diào)用該函數(shù)時,如果沒有指定實
參則采用該形參的缺省值,否則使用指定的實參。
案例:
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(1);
Func(2);
Func(3);
//當不傳實際參數(shù)時,則用缺省值
Func();
return 0;
}
運行結(jié)果:
?
4.2.缺省參數(shù)分類
全缺省參數(shù)
全缺省參數(shù)是聲明或定義函數(shù)時為函數(shù)的參數(shù)全都指定一個缺省值。
void TestFunc(int a = 10, int b = 20, int c = 30)
{
cout << "a= " << a << endl;
cout << "b= " << b << endl;
cout << "c= " << c << endl << endl;
}
int main()
{
//有參數(shù)傳入它會先從左向右依次匹配
TestFunc();//a,b,c使用默認形參
TestFunc(1);//只傳遞1給形參a,b,c使用默認形參值
TestFunc(1, 2);//傳遞1給a,2給b,c使用默認形參
TestFunc(1, 2, 3);//傳遞三個參數(shù),不使用默認形參值
//TestFunc(,1,);//不可以
return 0;
}
運行結(jié)果:
?
半缺省參數(shù)
半缺省參數(shù)是聲明或定義函數(shù)時為函數(shù)的部分參數(shù)自右向左連續(xù)指定缺省值,且中間不能有間隔。
void TestFunc(int a, int b = 20, int c = 30)
{
cout << "a= " << a << endl;
cout << "b= " << b << endl;
cout << "c= " << c << endl << endl;
}
int main()
{
//TestFunc();//必須傳入一個值
TestFunc(1);//只傳遞1給形參a,b,c使用默認形參值
TestFunc(1, 2);//傳遞1給a,2給b,c使用默認形參
TestFunc(1, 2, 3);//傳遞三個參數(shù),不使用默認形參值
return 0;
}
運行結(jié)果:
?
4.3.缺省參數(shù)注意事項
a.默認參數(shù)只可在函數(shù)聲明中出現(xiàn)一次,如果沒有函數(shù)聲明,只有函數(shù)定義,才可以在函數(shù)定義中設(shè)定。
b.默認參數(shù)定義的順序是自右向左,即如果一個參數(shù)設(shè)定了默認參數(shù),則其右邊不能再有普通參數(shù)。
c.默認參數(shù)調(diào)用時,遵循參數(shù)調(diào)用順序,即有參數(shù)傳入它會先從左向右依次匹配。
d.默認參數(shù)值可以是全局變量、全局常量,甚至可以是一個函數(shù),但不可以是局部變量,因為默認參數(shù)的值是在編譯時確定的,而局部變量位置與默認值在編譯時無法確定。
4.4.缺省參數(shù)用途
在學習數(shù)據(jù)結(jié)構(gòu)中的棧時,當我們在對棧進行初始化過程中并不知道要為該棧開辟多少字節(jié)的內(nèi)存空間,起始狀態(tài)我們都是為該棧開辟4個int類型的空間,當棧滿時,再將??臻g擴容至原來的2倍。但是,如果我們使用缺省參數(shù),當明確知道不需要太大空間時就使用默認的空間大小,當明確知道需要很大空間時就使用缺省參數(shù)。
需要注意的是,默認參數(shù)只可在函數(shù)聲明中出現(xiàn)一次,如果沒有函數(shù)聲明,只有函數(shù)定義,才可以在函數(shù)定義中設(shè)定。
案例:
#include<iostream>
using namespace std;
struct Stack
{
int* a;
int top;
int capacity;
};
//聲明
//缺省參數(shù)不能在函數(shù)聲明和定義中同時出現(xiàn)
//默認參數(shù)只可在函數(shù)聲明中出現(xiàn)一次,如果沒有函數(shù)聲明,只有函數(shù)定義,才可以在函數(shù)定義中設(shè)定
void StackInit(struct Stack* ps, int capacity = 4);
int main()
{
struct Stack st1;
//知道我一定會插入100個數(shù)據(jù),就可以顯示地傳參數(shù)100,這樣就提前開好空間,插入數(shù)據(jù)避免擴容
StackInit(&st1, 100);
struct Stack st2;
StackInit(&st2);
return 0;
}
//定義
void StackInit(struct Stack* ps, int capacity)
{
ps->a = (int*)malloc(sizeof(int) * capacity);
//...
ps->top = 0;
ps->capacity = capacity;
}
運行結(jié)果:
?
五.函數(shù)重載
所謂重載(overload)函數(shù)就是在同一個作用域內(nèi)函數(shù)名字相同但形參列表不同的函數(shù)。
5.1.重載函數(shù)概念
函數(shù)重載:是函數(shù)的一種特殊情況,C++允許在同一作用域中聲明幾個功能類似的同名函數(shù),這
些同名函數(shù)的形參列表(參數(shù)個數(shù)或類型或類型順序)不同,常用來處理實現(xiàn)功能類似數(shù)據(jù)類型
不同的問題。
它們的函數(shù)名相同但參數(shù)列表卻不同,參數(shù)列表的不同有三種含義:參數(shù)個數(shù)不同,或者參數(shù)類型不同或者參數(shù)個數(shù)和類型都不同。
參數(shù)類型不同:
//參數(shù)類型不同
int Add(int left, int right)
{
return left + right;
}
double Add(double left, double right)
{
return left + right;
}
int main()
{
cout << Add(1, 2) << endl;
cout << Add(1.1, 2.2) << endl;
return 0;
}
運行結(jié)果:
?
參數(shù)個數(shù)不同:
//參數(shù)個數(shù)不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a):" << a << endl;
}
int main()
{
f();
f(1);
return 0;
}
運行結(jié)果:
?
參數(shù)類型順序不同:
//參數(shù)類型順序不同
void func(int i, char ch)
{
cout << "void func(int i,char ch):" << i << " " << ch << endl;
}
void func(char ch, int i)
{
cout << "void func(char ch,int i):" << ch << " " << i << endl;
}
int main()
{
func(1, 'a');
func('a', 1);
return 0;
}
運行結(jié)果:
?
注意:
1.返回值不同,不能構(gòu)成重載,只有涉及到參數(shù)不同,才會構(gòu)成重載。
案例:
//返回值不同,不構(gòu)成重載,只有涉及到參數(shù)不同,才會構(gòu)成重載
short Add(short left, short right)
{
return left + right;
}
int Add(short left, short right)
{
return left + right;
}
int main()
{
Add(1, 3);
return 0;
}
運行結(jié)果:
?
2.當使用具有默認參數(shù)的函數(shù)重載形式時須注意防止調(diào)用的二義性,例如下面的兩個函數(shù):
int add(int x, int y = 1);
void add(int x);
當使用函數(shù)調(diào)用語句“add(10);”時會產(chǎn)生歧義,因為它既可以調(diào)用第一個add()函數(shù)也可以調(diào)用第二個add()函數(shù),編譯器無法確認到底要調(diào)用哪個重載函數(shù),這就產(chǎn)生了調(diào)用的二義性。在使用時要防止這種情況的發(fā)生。
5.2.C++支持函數(shù)重載的原理--名字修飾(name Mangling)
為什么C++支持函數(shù)重載,而C語言不支持函數(shù)重載呢?
在C/C++中,一個程序要運行起來,需要經(jīng)歷以下幾個階段:預處理、編譯、匯編、鏈接。假設(shè)在Linux環(huán)境下,要處理的程序為:func.h? func.c? test.c,則在每個階段對應(yīng)的執(zhí)行操作分別為:
- 預處理:頭文件展開,宏替換,條件編譯,去掉注釋? func.i main.i
- 編譯:語法檢查,生成匯編代碼? func.s main.s
- 匯編:把匯編代碼轉(zhuǎn)換成二進制機器碼? func.o main.o
- 鏈接:將.o的目標文件合并到一起,其次還需要找一些只給聲明的函數(shù)變量的地址,合并段表,符號表的合并和符號表的重定位? a.out
?
實際項目通常是由多個頭文件和多個源文件構(gòu)成,而通過C語言階段學習的編譯鏈接,我們可以知道,當前test.cpp中調(diào)用了func.cpp中定義的Add函數(shù)時,編譯后鏈接前,test.o的目標文件中沒有Add的函數(shù)地址,因為Add是在func.cpp中定義的,所以Add的地址在func.o中。那么怎么辦呢??
所以鏈接階段就是專門處理這種問題,鏈接器看到test.o調(diào)用Add,但是沒有Add的地址,就會到func.o的符號表中找Add的地址,然后鏈接到一起。
那么鏈接時,面對Add函數(shù),鏈接接器會使用哪個名字去找呢?這里每個編譯器都有自己的函數(shù)名修飾規(guī)則。
由于Windows下vs的修飾規(guī)則過于復雜,而Linux下g++的修飾規(guī)則簡單易懂,下面我們使用g++演示這個修飾后的名字。
通過編譯我們可以看出gcc的函數(shù)修飾后名字不變。而g++的函數(shù)修飾后變成【_Z+函數(shù)長度+函數(shù)名+類型首字母】。可以得出,在Linux下,采用gcc編譯完成后,函數(shù)名字的修飾沒有發(fā)生改變;而采用g++編譯完成后,函數(shù)名字的修飾發(fā)生改變,編譯器將函數(shù)參數(shù)類型信息添加到修改后的名字中。
因此,可以得出:C語言是沒辦法支持重載的,因為同名函數(shù)沒辦法區(qū)分;而C++是通過函數(shù)修飾規(guī)則來區(qū)分,只要參數(shù)不同,修飾出來的名字就不一樣,就支持了重載。
5.3.extern "C"
extern "C"的主要作用是為了能夠正確實現(xiàn)C++代碼調(diào)用其他C語言代碼。加上extern "C"后,會指示編譯器這部分代碼按C語言的進行編譯,而不是C++的。由于C++支持函數(shù)重載,因此編譯器編譯函數(shù)的過程中會將函數(shù)的參數(shù)類型也加到編譯后的代碼中,而不僅僅是函數(shù)名;而C語言并不支持函數(shù)重載,因此編譯C語言代碼的函數(shù)時不會帶上函數(shù)的參數(shù)類型,一般只包括函數(shù)名。
六.引用
引用不是新定義一個變量,而是給已存在變量取了一個別名,編譯器不會為引用變量開辟內(nèi)存空
間,它和它引用的變量共用同一塊內(nèi)存空間。
6.1.引用的概念
引用就是給一個變量起一個別名,用“&”標識符來標識,其格式如下所示:
數(shù)據(jù)類型 &引用名=變量名;
上述格式中,“&”并不是取地址操作符,而是起標識作用,標識所定義的標識符是一個引用。引用聲明完成以后相當于目標變量有兩個名稱,如下面代碼所示:
int a = 0;
int& b = a;
在上述代碼中,b就是變量a的引用,b和a標識的是同一塊內(nèi)存,相當于一個人有兩個名字。對a與b進行操作,都會更改內(nèi)存中的數(shù)據(jù)。
6.2.引用的特性
a.引用在定義時必須初始化,如“int &b;”語句是錯誤的。
案例:
int main()
{
int a = 10;
int& b = a;
int& c;
}
運行結(jié)果:
b.引用在初始化時只能綁定左值,不能綁定常量值,如“int &b=10;”語句是錯誤的;
案例:
int main()
{
int a = 10;
int& b = a;
int& c = 100;
}
運行結(jié)果:
c.引用一旦初始化,其值就不能再更改,即不能再做別的變量的引用,代碼如下所示:
int a = 10;
int b = 20;
int& p = a;
p = b;//為p賦值
p為變量a的引用,當p=b執(zhí)行時,并不是把p指向了變量b,而是使用變量b給變量a賦值;
d.數(shù)組不能定義引用,因為數(shù)組是一組數(shù)據(jù),無法定義其別名;
e.一個變量可以有多個引用。
案例:
int main()
{
int a = 0;
//引用
int& b = a;
int& c = a;
int& d = c;
//取地址
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}
運行結(jié)果:
6.3.引用的使用場景
6.3.1.引用作為函數(shù)參數(shù)
C++增加引用的類型,主要的應(yīng)用就是把它作為函數(shù)的參數(shù),以擴充函數(shù)傳遞數(shù)據(jù)的功能,引用作函數(shù)參數(shù)時區(qū)別于值傳遞與地址傳遞。我們以交換兩個數(shù)據(jù)的值為例來分析引用作為函數(shù)參數(shù)的用法。
案例:
//地址傳遞
void Swap2(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
//引用傳遞
void Swap3(int& rx, int& ry)
{
int temp = rx;
rx = ry;
ry = temp;
}
int main()
{
int x = 3, y = 5;
Swap1(x, y);
cout << x << " " << y << endl;
int m = 3, n = 5;
Swap2(&m, &n);
cout << m << " " << n << endl;
int i = 3, j = 5;
Swap3(i, j);
cout << i << " " << j << endl;
return 0;
}
運行結(jié)果:
分析:
這是一個典型的區(qū)分值傳遞與址傳遞的函數(shù),如果是值傳遞,由于副本機制無法實現(xiàn)兩個數(shù)據(jù)的交換;址傳遞則可以完成兩個數(shù)據(jù)的交換,但也需要為形參(指針)分配存儲單元,在調(diào)用時要反復使用“*指針名”,且實參傳遞時要取地址,這樣很容易出現(xiàn)錯誤且程序的可讀性也會下降。而引用傳遞就完全克服了它們的缺點,使用引用就是直接操作變量,簡單高效可讀性好。
6.3.2.引用作為函數(shù)返回值
案例1:
int Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int ret = Add(1, 2);
cout << ret << endl;
return 0;
}
運行結(jié)果:
分析:
返回普通類型對象其實是返回這個對象的拷貝,c++其實會創(chuàng)建一個臨時變量,這個臨時變量被隱藏了,它會把c的值拷貝給這個臨時變量,當執(zhí)行語句“int ret = Add(1, 2);”的時候就會把臨時變量的值再拷貝給ret,假設(shè)這個臨時變量是t,相當于做了這兩個賦值的步驟:t = c;? ret = t。
函數(shù)中的普通變量是存放在當前所開辟函數(shù)的棧幀中的,即存放在內(nèi)存中的棧區(qū);而存放在棧區(qū)中的臨時變量當函數(shù)調(diào)用結(jié)束后整個函數(shù)棧幀就會被銷毀,那么存放在這個棧幀中的臨時變量也隨之消亡,不復存在。
案例2:
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
cout << ret << endl;
return 0;
}
運行結(jié)果:
分析:
返回引用實際返回的是一個指向返回值的隱式指針,在內(nèi)存中不會產(chǎn)生副本,是直接將c拷貝給ret,這樣就避免產(chǎn)生臨時變量,相比返回普通類型的執(zhí)行效率更高,而且這個返回引用的函數(shù)也可以作為賦值運算符的左操作數(shù)。
案例3:
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
運行結(jié)果:
分析:
在Add函數(shù)調(diào)用結(jié)束后,為add函數(shù)創(chuàng)建的棧幀會被銷毀,這塊??臻g會還給操作系統(tǒng)。此時再使用Add函數(shù)的返回值,就會造成對內(nèi)存空間的非法訪問,而大部分情況下,編譯器不會對非法訪問內(nèi)存報錯。下一次的函數(shù)調(diào)用可能還是在這塊空間上建立棧幀,但是上一次的棧幀是否清理取決于編譯器,可能清理了,也可能沒清理:
- 如果編譯器沒有清理這個棧幀的話,那么這個c就還是3
- 如果編譯器清理了這個棧幀的話,這個c就有可能是個隨機值。
小結(jié):
引用返回的語法含義就是返回返回對象的別名,使用引用返回本質(zhì)是不對的,因為結(jié)果是沒有保障的。
出了函數(shù)作用域,返回對象就銷毀了,那么一定不能用引用返回(使用static時,可以使用引用返回),一定要用傳值返回。
不要將局部變量作為返回值:因為局部變量存放在棧區(qū),函數(shù)調(diào)用結(jié)束之后就釋放;第一次結(jié)果正確,是因為編譯器做了保留,第二次結(jié)果錯誤,是因為局部變量被釋放了。
函數(shù)的返回值可以左值存在:靜態(tài)變量存放在全局區(qū),是在整個程序運行結(jié)束才釋放。
引用作為返回的情況:
- 使用static修飾的靜態(tài)變量作為返回對象;
- 返回對象為調(diào)用函數(shù)中開辟的一塊內(nèi)存空間中的內(nèi)容(調(diào)用函數(shù)中開辟的空間是用malloc開辟的,存放在堆上,所以可以引用返回)。
案例:
int& Count()
{
static int n=0;//可以使用引用
//int n = 0;//不可以使用引用
n++;
//...
return n;
}
char& func2(char* str, int i)
{
return str[i];
}
int main()
{
//int ret = Count();
//ret的結(jié)果是未定義的,如果棧幀結(jié)束時,系統(tǒng)會清理棧幀并置成隨機值,那么這里ret的結(jié)果就是隨機值
int& ret = Count();
Count() = 10;//如果函數(shù)的返回值作為左值,必須使用引用
cout << ret << endl;
cout << ret << endl;//返回一個隨機值
char ch[] = "abcdef";
for (int i = 0; i < strlen(ch); ++i)
{
func2(ch, i) = '0' + i;
}
cout << ch << endl; //012345
return 0;
}
運行結(jié)果:
總結(jié):
引用作為函數(shù)參數(shù):
- 輸出型參數(shù);
- 大對象傳參,提高效率。
引用作為函數(shù)返回值:
- 輸出型返回對象,調(diào)用者可以修改返回對象;
- 減少拷貝,提高效率。
6.4.傳值和傳引用效率比較
值和引用的作為參數(shù)的性能比較
案例:
#include <time.h>
struct A
{
int a[10000];
};
void TestFunc1(A a)
{
}
void TestFunc2(A& a)
{
}
void TestRefAndValue()
{
A a;
//以值作為函數(shù)參數(shù)
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作為函數(shù)參數(shù)
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分別計算兩個函數(shù)運行結(jié)束后的時間
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
TestRefAndValue();
return 0;
}
運行結(jié)果:
值和引用的作為返回值類型的性能比較?
案例:
#include <time.h>
struct A
{
int a[10000];
};
A a;
//值返回
A TestFunc1()
{
return a;
}
//引用返回
A& TestFunc2()
{
return a;
}
void TestReturnByRefOrValue()
{
//以值作為函數(shù)的返回值類型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
//以引用作為函數(shù)的返回值類型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 計算兩個函數(shù)運算完成之后的時間
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
TestReturnByRefOrValue();
return 0;
}
運行結(jié)果:
小結(jié):
以值作為參數(shù)或者返回值類型,在傳參和返回期間,函數(shù)不會直接傳遞實參或者將變量本身直接返回,而是傳遞實參或者返回變量的一份臨時的拷貝,因此用值作為參數(shù)或者返回值類型,效率是非常低下的,尤其是當參數(shù)或者返回值類型非常大時,效率就更低。
通過上述代碼的比較,發(fā)現(xiàn)值和引用在作為傳參以及返回值類型上效率相差很大。
6.5.常引用
我們知道引用不能綁定常量值,如果想要用常量值去初始化引用,則引用必須用const來修飾,這樣的引用我們稱之為const引用。
const引用可以用cons對象和常量值來初始化,例如:
const int& a = 10;//常量值初始化const引用
const int a = 10;
const int& b = a;//const對象初始化const引用
一般來說,對于const對象而言,只能采用const引用,如果沒有對引用進行限定,那么就可以通過引用對數(shù)據(jù)進行修改,這是不允許的。但const引用不一定都得用const對象初始化,還可以用非const對象來初始化,例如:
int a = 10;
const int& b = a;
用非const對象初始化const引用,只是不允許通過該引用修改變量值。除此之外,const引用甚至可以用不同類型的變量來初始化const引用,例如:
double d = 1.2;
const int& b = d;
這是連指針都沒有的優(yōu)越性,此處b引用了一個double類型的數(shù)值,編譯器在編譯這兩行代碼時,先把d進行了一下轉(zhuǎn)換,轉(zhuǎn)換為int類型數(shù)據(jù),然后又賦值給了引用b,其轉(zhuǎn)換過程如下面代碼所示:
double d = 1.2;
const int temp = (int)d;
const int& b = temp;
在這種情況下,b綁定的是一個臨時變量。而當非const引用時,如果綁定到臨時變量,那么可以通過引用修改臨時變量的值,修改一個臨時變量的值是沒有任何一樣的,因此編譯器把這種行為定位非法的,那么用不同類型的變量初始化一個普通引用自然也是非法的。
案例:
int main()
{
int a = 10;
int& b = a;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
//權(quán)限不能放大
const int c = 20;
//int& d = c;//權(quán)限放大,從const變?yōu)榉莄onst,不合法
const int& d = c;
//權(quán)限能夠縮小
int e = 30;
const int& f = e;//權(quán)限縮小,從非const變?yōu)閏onst,合法
int ii = 1;
//強制類型轉(zhuǎn)換,并不會改變原變量類型,中間會產(chǎn)生一個臨時變量
double dd = ii;//ii會生成一個臨時變量,然后dd會拷貝這個臨時變量,而臨時變量具有常性
//double& rdd = ii;//會造成權(quán)限的放大,ii生成的臨時變量是const類型,而rdd是非const類型,不能從const變?yōu)榉莄onst,是不合法的
const double& rdd = ii;
const int& x = 10;//可以為常量
return 0;
}
6.6.引用和指針的區(qū)別
語法概念:
引用就是一個別名,沒有獨立空間,和其引用實體共用同一塊空間;而指針開辟了4字節(jié)或者8字節(jié)的空間,存儲變量的地址。
底層實現(xiàn):
在底層實現(xiàn)上,引用實際上是有空間的,因為引用在底層是按照指針方式來實現(xiàn)的。
使用場景:
指針更強大,更危險,更復雜;而引用相對局限一些,更安全,更簡單。
二者不同:
- 引用概念上定義一個變量的別名,指針存儲一個變量地址;
- 引用在定義時必須初始化,指針沒有要求;
- 引用在初始化時引用一個實體后,就不能再引用其他實體,而指針可以在任何時候指向任何一個同類型實體;
- 沒有NULL引用,但有NULL指針;
- 在sizeof中含義不同:引用結(jié)果為引用類型的大小,但指針始終是地址空間所占字節(jié)個數(shù)(32位平臺下占4個字節(jié));
- 引用自加即引用的實體增加1,指針自加即指針向后偏移一個類型的大??;
- 有多級指針,但是沒有多級引用;
- 訪問實體方式不同,指針需要顯式解引用,引用編譯器自己處理;
- 引用比指針使用起來相對更安全。
案例:
int main()
{
//語法的角度,ra沒有開空間
int a = 10;
int& ra = a;
ra = 20;
cout << a << endl;
//語法的角度,pa沒有開辟4或8字節(jié)的空間
//底層實現(xiàn)角度,引用底層是用指針實現(xiàn)的
int b = 20;
int* pa = &b;
*pa = 20;
cout << b << endl;
return 0;
}
運行結(jié)果:
七.內(nèi)聯(lián)函數(shù)
我們都直到函數(shù)的調(diào)用有利于代碼重用,提高效率,但有時頻繁的函數(shù)調(diào)用也會增加時間與空間的開銷反而造成效率低下。因為調(diào)用函數(shù)實際上是將程序執(zhí)行順序從函數(shù)調(diào)用處跳轉(zhuǎn)到函數(shù)所存放在內(nèi)存中的某個地址,將調(diào)用現(xiàn)場保留,跳轉(zhuǎn)到那個地址將函數(shù)執(zhí)行,執(zhí)行完畢后再回到調(diào)用現(xiàn)場,所以頻繁的函數(shù)調(diào)用會帶來很大開銷。為了解決這個問題,C++提供了內(nèi)聯(lián)(inline)函數(shù),在編譯時將函數(shù)體嵌入到調(diào)用處。
7.1.內(nèi)聯(lián)函數(shù)定義
以inline修飾的函數(shù)叫作內(nèi)聯(lián)函數(shù),編譯時C++編譯器會在調(diào)用內(nèi)聯(lián)函數(shù)的地方展開,沒有函數(shù)調(diào)
用建立棧幀的開銷,內(nèi)聯(lián)函數(shù)提升程序運行的效率。其格式如下:
inline 返回值類型 函數(shù)名(參數(shù)列表)
{
函數(shù)體;
}
案例:
#include<iostream>
using namespace std;
//Add就會在調(diào)用的地方展開
inline int Add(int x, int y)
{
return x + y;
}
int main()
{
int ret = Add(10, 20);
cout << ret << endl;
return 0;
}
如果在上述函數(shù)前增加inline關(guān)鍵字會將其改成內(nèi)聯(lián)函數(shù),在編譯期間編譯器會用函數(shù)體替換函數(shù)的調(diào)用。
查看方式:
在release模式下,查看編譯器生成的匯編代碼中是否存在call Add;
- 使用內(nèi)聯(lián)函數(shù)時,匯編語言中不再有call Add指令,函數(shù)指令直接在主函數(shù)中展開。
- 不使用內(nèi)聯(lián)函數(shù)時,要先通過call指令調(diào)用Add函數(shù),然后建立函數(shù)棧幀并執(zhí)行函數(shù)指令。
在debug模式下,需要對編譯器進行設(shè)置,否則不會展開(因為debug模式下,編譯器默認不會對代碼進行優(yōu)化,以下給出vs2019的設(shè)置方式)。在Debug版本下內(nèi)聯(lián)函數(shù)展開的方法:
- 打開屬性設(shè)置,選擇C/C++ ->?常規(guī),將調(diào)試信息格式改為程序數(shù)據(jù)庫;
- 選擇C/C++ ->?優(yōu)化,將內(nèi)聯(lián)函數(shù)擴展改為:只適用于_inline (Ob1)。
7.2.內(nèi)聯(lián)函數(shù)特性
a.inline是一種以空間換時間的做法,如果編譯器將函數(shù)當成內(nèi)聯(lián)函數(shù)處理,在編譯階段,會用函數(shù)體替換函數(shù)調(diào)用。缺陷:可能會使目標文件變大,優(yōu)勢:少了調(diào)用開銷,提高程序運行效率;
b.inline對于編譯器而言只是一個建議,不同編譯器關(guān)于inline實現(xiàn)機制可能不同,一般建議:將函數(shù)規(guī)模較小(即函數(shù)不是很長,具體沒有準確的說法,取決于編譯器內(nèi)部實現(xiàn))、不是遞歸、且頻繁調(diào)用的函數(shù)采用inline修飾,否則編譯器會忽略inline特性;
c.inline不建議聲明和定義分離,分離會導致鏈接錯誤。因為inline被展開,就沒有函數(shù)地址了,鏈接就會找不到。
7.3.內(nèi)聯(lián)函數(shù)與宏函數(shù)的區(qū)別
宏函數(shù):使用宏函數(shù),在預處理階段進行替換 。
- 宏的缺點:可讀性差,較為復雜;沒有類型安全檢查;不方便調(diào)試;?
- 宏的優(yōu)點:復用性變強;宏函數(shù)提高效率,減少棧幀建立。
C++中基本不再建議使用宏,盡量使用const,enum,inline去替代宏。inline幾乎解決了宏函數(shù)的缺點,同時兼具了它的缺點 。
八.auto關(guān)鍵字
C++11之前,auto默認修飾函數(shù)的局部變量,限定變量的作用域及存儲期。C++11中,auto稱為類型說明符,使用它可以讓編譯器根據(jù)初始化代碼推斷出所聲明變量的真實類型。
8.1.auto簡介
在早期C/C++中auto的含義是:使用auto修飾的變量,是具有自動存儲器的局部變量,但遺憾的是一直沒有人去使用它。C++11中,標準委員會賦予了auto全新的含義即:auto不再是一個存儲類型指示符,而是作為一個新的類型指示符來指示編譯器,auto聲明的變量必須由編譯器在編譯時期推導而得。
案例:
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;//自動推導類型
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
return 0;
}
運行結(jié)果:
注意:
使用auto定義變量時必須對其進行初始化,在編譯階段編譯器需要根據(jù)初始化表達式來推導auto的實際類型。因此auto并非是一種“類型”的聲明,而是一個類型聲明時的“占位符”,編譯器在編譯期會將auto替換為變量實際的類型。
8.2.auto的使用細則
auto與指針和引用結(jié)合起來使用???????????????????????????????????????????????????????????????????????????????????????????????????????????????? 用auto聲明指針類型時,用auto和auto*沒有任何區(qū)別,但用auto聲明引用類型時則必須加&。
案例:
int main()
{
//用auto聲明指針類型時,用auto和auto*沒有任何區(qū)別
int x = 10;
auto a = &x;//a的類型是:int*
auto* b = &x;//顯示地加*,表示用于接收一個指針類型的數(shù)據(jù)
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
//用auto聲明引用類型時則必須加&
auto& c = x;//顯示地加&,表示用于接收一個引用類型的數(shù)據(jù)
cout << typeid(c).name() << endl;
}
運行結(jié)果:
在同一行定義多個變量???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????? 當在同一行聲明多個變量時,這些變量必須是相同的類型,否則編譯器將會報錯,因為編譯器實際只對第一個類型進行推導,然后用推導出來的類型定義其他變量。
案例:
int main()
{
//在同一行定義多個變量
auto a = 1, b = 2;
auto c = 3, d = 4.0;//該行代碼會編譯失敗,因為c和d的初始化表達式類型不同
return 0;
}
運行結(jié)果:
8.3.auto不能推導的場景
auto不能作為函數(shù)參數(shù)?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????? auto不能作為形參類型,因為編譯器無法對其的實際類型進行推導。
案例:
void func(auto x)
{
cout << x << endl;
}
int main()
{
func(10);
return 0;
}
?運行結(jié)果:
auto不能直接用來聲明數(shù)組
案例:
int main()
{
int a[] = { 1,2,3 };
auto b[] = { 1,2,3 };
return 0;
}
運行結(jié)果:
注意:
為了避免與C++98中的auto發(fā)生混淆,C++11只保留了auto作為類型指示符的用法;
auto在實際中最常見的優(yōu)勢用法就是跟以后會講到的C++11提供的新式for循環(huán),還有l(wèi)ambda表達式等進行配合使用。
九.基于范圍的for循環(huán)
9.1.范圍for的語法
在C++98中如果要遍歷一個數(shù)組,可以按照以下方式進行:
int main()
{
int a[] = { 1,2,3,4,5,6 };
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
a[i]++;
}
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
cout << a[i] << " ";
}
cout << endl;
return 0;
}
對于一個有范圍的集合而言,由程序員來說明循環(huán)的范圍是多余的,有時候還會容易犯錯誤。因此C++11中引入了基于范圍的for循環(huán)。for循環(huán)后的括號由冒號“ :”分為兩部分:第一部分是范圍內(nèi)用于迭代的變量,第二部分則表示被迭代的范圍。?
int main()
{
int a[] = { 1,2,3,4,5,6 };
//范圍for
//自動地依次取a的數(shù)據(jù),賦值給e
//自動迭代,自動判斷結(jié)束
for (auto& e : a)//加&,可以對數(shù)組進行更改;加*,不可以對數(shù)組進行修改,因為無法從“int”轉(zhuǎn)換為“int *”
{
e--;
}
for (auto e : a)
{
cout << e << " ";
}
cout << endl;
return 0;
}
注意:
與普通循環(huán)類似,可以用continue來結(jié)束本次循環(huán),也可以用break來跳出整個循環(huán)。
9.2.范圍for的使用條件
for循環(huán)迭代的范圍必須是確定的??????????????????????????????????????????????????????????????????????????????????????????????????????????????????? 對于數(shù)組而言,就是數(shù)組中第一個元素和最后一個元素的范圍;對于類而言,應(yīng)該提供begin和end的方法,begin和end就是for循環(huán)迭代的范圍。
void TestFor(int array[])
{
for (auto& e : array)//for的范圍不確定
cout << e << endl;
}
int main()
{
int a[] = { 1,2,3,4,5,6 };
TestFor(a);
}
迭代的對象要實現(xiàn)++和==的操作
十.指針空值nullptr
在良好的C/C++編程習慣中,聲明一個變量時最好給該變量一個合適的初始值,否則可能會出現(xiàn)
不可預料的錯誤,比如未初始化的指針。如果一個指針沒有合法的指向,我們基本都是按照如下
方式對其進行初始化:
void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
// ……
}
NULL實際是一個宏,在傳統(tǒng)的C頭文件(stddef.h)中,可以看到如下代碼:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定義為字面常量0,或者被定義為無類型指針(void*)的常量。不論采取何
種定義,在使用空值的指針時,都不可避免的會遇到一些麻煩,比如:
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
//NULL實際是一個宏,在傳統(tǒng)的C頭文件(stddef.h)中,NULL可能被定義為字面常量0,或者被定義為無類型指針(void*)的常量。
int* p = NULL;
f(0);//f(int)
f(NULL);//f(int)
f(p);//f(int*)
//C++11 nullptr
//在使用nullptr表示指針空值時,不需要包含頭文件,因為nullptr是C++11作為新關(guān)鍵字引入的。
//在C++11中,sizeof(nullptr) 與 sizeof((void*)0)所占的字節(jié)數(shù)相同
f(nullptr);
int* ptr = nullptr;
return 0;
}
程序本意是想通過f(NULL)調(diào)用指針版本的f(int*)函數(shù),但是由于NULL被定義成0,因此與程序的初衷相悖。
在C++98中,字面常量0既可以是一個整形數(shù)字,也可以是無類型的指針(void*)常量,但是編譯器默認情況下將其看成是一個整形常量,如果要將其按照指針方式來使用,必須對其進行強轉(zhuǎn)(void
*)0。文章來源:http://www.zghlxwxcb.cn/news/detail-697293.html
注意:
1. 在使用nullptr表示指針空值時,不需要包含頭文件,因為nullptr是C++11作為新關(guān)鍵字引入的;? 2. 在C++11中,sizeof(nullptr) 與 sizeof((void*)0)所占的字節(jié)數(shù)相同;
3. 為了提高代碼的健壯性,在后續(xù)表示指針空值時建議最好使用nullptr。文章來源地址http://www.zghlxwxcb.cn/news/detail-697293.html
到了這里,關(guān)于C++初階:C++入門的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!