???????個人主頁:簡 料
????所屬專欄:C++
????個人社區(qū):越努力越幸運(yùn)社區(qū)
????簡? ? ?? 介:簡料簡料,簡單有料~在校大學(xué)生一枚,專注C/C++/GO的干貨分享,立志成為您的好幫手 ~
C/C++學(xué)習(xí)路線 (點擊解鎖) |
---|
??C語言 |
??初階數(shù)據(jù)結(jié)構(gòu)與算法 |
??C++ |
??高階數(shù)據(jù)結(jié)構(gòu) |
??Linux系統(tǒng)編程與網(wǎng)絡(luò)編程 |
??前言
?? 由
C語言
到C++
,這是一個即刺激又艱難的過程,說他刺激,是因為學(xué)會用C++
后就不會再想用C語言
,說他艱難,是因為C++
是一門古老復(fù)雜的語言,很多工作多年的C++
程序員都不敢說他精通C++
(哈哈哈)。當(dāng)然了,也別聽到這些就被勸退了,C++
經(jīng)過了這么多年的發(fā)展,其語言的成熟度自然是有的,并且,如今世界主流的計算機(jī)語言當(dāng)中依然有C++
的身影,這就說明,C++
這門語言是有他獨特的風(fēng)格來吸引人們的。
??C++
的運(yùn)行速度很快,是因為C++
比較接近底層。這也是C++
受歡迎的優(yōu)點之一。有人說,學(xué)好C++
再去學(xué)其他任何語言都很容易上手,但學(xué)好其他語言就不一定了。
C++
是在C
的基礎(chǔ)之上,容納進(jìn)去了面向?qū)ο缶幊趟枷?,并增加了許多有用的庫,以及編程范式等。熟悉C語言
之后,對C++
學(xué)習(xí)有一定的幫助,本章節(jié)主要目標(biāo):
- 補(bǔ)充C語言語法的不足,以及
C++
是如何對C語言
設(shè)計不合理的地方進(jìn)行優(yōu)化的,比如:作用域方面、IO
方面、函數(shù)方面、指針方面、宏方面等。- 為后續(xù)類和對象學(xué)習(xí)打基礎(chǔ)。
接下來就開始學(xué)習(xí)
C++
吧!
一. 淺看【C++】關(guān)鍵字
-
C++
總計63
個關(guān)鍵字,C
語言32
個關(guān)鍵字。幾乎擴(kuò)充了兩倍,不過不要驚呼,后面復(fù)雜的還沒來呢。
下面我們只是看一下C++
有多少關(guān)鍵字,不對關(guān)鍵字進(jìn)行具體的講解。后面學(xué)到哪,哪有出現(xiàn)以后再細(xì)講。
二. 命名空間
在C/C++
中,變量、函數(shù)和后面要學(xué)到的類都是大量存在的,這些變量、函數(shù)和類的名稱將都存在于全局作用域中,可能會導(dǎo)致很多命名沖突。使用命名空間的目的是對標(biāo)識符的名稱進(jìn)行本地化,以避免命名沖突或名字污染,namespace
關(guān)鍵字的出現(xiàn)就是針對這種問題的。
問題案例:
#include <stdio.h>
#include <stdlib.h>
// 這個自己定義的 rand 與庫里面的沖突
int rand = 10;
// C語言沒辦法解決類似這樣的命名沖突問題,所以 C++ 提出了 namespace 來解決
int main()
{
printf("%d\n", rand);
return 0;
}
// 編譯后報錯:error: “rand”: 重定義;以前的定義是“函數(shù)”
1. 命名空間的定義??
-> 定義命名空間,需要使用到namespace
關(guān)鍵字,后面跟命名空間的名字,然后接一對{}
即可,{}
中即為命名空間的成員。
// 1. my_space 是命名空間的名字,一般開發(fā)中是用項目名字做命名空間名。
namespace my_space
{
// 命名空間中可以定義變量/函數(shù)/類型
// 變量
int a = 100;
int b = 200;
int rand = 0;
// 函數(shù)
void swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
// 類型
struct student
{
char name[20];
int age;
......
};
}
// 2. 命名空間可以嵌套
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;
}
}
}
// 3. 同一個工程中允許存在多個相同名稱的命名空間,編譯器最后會合成同一個命名空間中。
// 一個工程中(可以在不同文件)的兩個 my_space 會被合并成一個,例如下面的 my_space 命名空間會與第一個命名空間合并
namespace my_space
{
int a = 100;
int b = 200;
void print(int a, int b)
{
printf("%d-%d\n", a, b);
//cout << a << ' ' << b << endl;
}
}
注意: 一個命名空間就定義了一個新的作用域,命名空間中的所有內(nèi)容都局限于該命名空間中
2. 命名空間的使用??
那么命名空間中成員該如何使用呢?
例如:
namespace my_space
{
// 命名空間中可以定義變量/函數(shù)/類型
// 變量
int a = 100;
int b = 200;
int rand = 0;
// 函數(shù)
void swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
// 類型
struct student
{
char name[20];
int age;
......
};
}
int main()
{
// 這樣是不行的,因為 a 處在 my_space 命名空間中,被圍起來了
//printf("%d\n", a);
// 需要這樣
printf("%d\n", my_space::a); // 表示 a 是屬于 my_space 這個命名空間里面的變量
return 0;
}
【命名空間使用的三種方式】
- 首先認(rèn)識一下作用域限定符:
?作用域限定符【::
】:表示指定作用域,例如my_space::a
表明a
是my_space
這個作用域當(dāng)中的,在my_space
這個作用域里面去找a
。默認(rèn)::a
指的是在全局域去找a
。
- 加命名空間名稱及作用域限定符
使用上面my_space
命名空間為例。
int main()
{
//
printf("%d-%d\n", my_space::a, my_space::b);
return 0;
}
- 使用
using
將命名空間中某個成員引入
using my_space::b;
int main()
{
printf("%d\n", my_space::a);
// 可直接使用 b ,因為 b 在全局域被展開
printf("%d\n", b);
return 0;
}
- 使用
using namespace
命名空間名稱 引入
// 直接將 my_space 命名空間在全局域展開,相當(dāng)于推翻了 my_space 命名空間域的圍墻
using namespace my_space;
int main()
{
// 當(dāng)然還是可以 my_space::a 這樣寫
printf("%d-%d\n", a, b);
struct student s1;
......
return 0;
}
三. 【C++】輸入輸出(IO)
C++
在輸入輸出上獨自弄了自己的一套,可能是祖師爺覺得C語言
的輸入輸出寫起來不方便,又或是其他原因,反正C++
的輸入輸出是一個類,對其進(jìn)行了封裝,重載等等(這里不講解)。
那么如何來使用呢?如下:
#include<iostream>
// std 是 C++ 標(biāo)準(zhǔn)庫的命名空間名,C++ 將標(biāo)準(zhǔn)庫的定義實現(xiàn)都放到這個命名空間中
using namespace std;
int main()
{
cout << "Hello world!!!" << endl;
return 0;
}
說明:
- 使用
cout
標(biāo)準(zhǔn)輸出對象(控制臺)和cin
標(biāo)準(zhǔn)輸入對象(鍵盤)時,必須包含<iostream>
頭文件以及按命名空間使用方法使用std
。 -
cout
和cin
是全局的流對象,endl
是特殊的C++
符號,表示換行輸出,他們都包含在包含<iostream>
頭文件中。 -
<<
是流插入運(yùn)算符,>>
是流提取運(yùn)算符。 - 使用
C++
輸入輸出更方便,不需要像printf / scanf
輸入輸出時那樣,需要手動控制格式。C++
的輸入輸出可以自動識別變量類型。 - 實際上
cout
和cin
分別是ostream
和istream
類型的對象,>>
和<<
也涉及運(yùn)算符重載等知識,這些知識博主后續(xù)給大家講解學(xué)習(xí),所以這里只需要簡單學(xué)習(xí)他們的使用。后面本小白還有一個章節(jié)更深入的學(xué)習(xí)IO
流用法及原理。
【注意】:早期標(biāo)準(zhǔn)庫將所有功能在全局域中實現(xiàn),聲明在.h
后綴的頭文件中,使用時只需包含對應(yīng)頭文件即可,后來將其實現(xiàn)在std
命名空間下,為了和C
頭文件區(qū)分,也為了正確使用命名空間,規(guī)定C++
頭文件不帶.h
;舊編譯器(vc 6.0)
中還支持<iostream.h>
格式,后續(xù)編譯器已不支持,因此推薦使用<iostream> + std
的方式。
對于輸入輸出自動識別類型,這可以說設(shè)計的非常棒,如下:
#include <iostream>
using namespace std;
int main()
{
int a;
double b;
char c;
// 可以自動識別變量的類型
cin >> a;
cin >> b >> c;
// 打印只需寫變量名即可,cout 會自動推導(dǎo)類型并對應(yīng)打印在顯示器上
cout << a << endl;
cout << b << ' ' << c << endl;
return 0;
}
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
int main()
{
int a = 100;
double f = 3.14;
char ch = 'A';
cout << a << ' ' << f << ' ' << ch << endl;
return 0;
}
運(yùn)行結(jié)果:
【看這里】:關(guān)于 cout
和cin
還有很多更復(fù)雜的用法,比如控制浮點數(shù)輸出精度,控制整形輸出進(jìn)制格式等等。因為C++
兼容C語言
的用法,這些又用得不是很多,這里就不展開講解了。后續(xù)有需要,大家可以配合文檔學(xué)習(xí)。
? std
命名空間的使用慣例:
std
是C++
標(biāo)準(zhǔn)庫的命名空間,如何展開std
使用更合理呢?
- 在日常練習(xí)中,建議直接
using namespace std
即可,這樣就很方便。 -
using namespace std
展開,標(biāo)準(zhǔn)庫就全部暴露出來了,如果我們定義跟庫重名的類型 / 對象 / 函數(shù),就存在沖突問題。該問題在日常練習(xí)中很少出現(xiàn),但是項目開發(fā)中代碼較多、規(guī)模大,就很容易出現(xiàn)。所以建議在項目開發(fā)中,使用像std::cout
這樣指定命名空間+ using std::cout
展開常用的庫對象 / 類型等方式。
例如:
#include <iostream>
// using std::(......)
using std::cout;
using std::endl;
int main()
{
int a = 100;
double f = 3.14;
char ch = 'A';
int b = 0, c = 0;
// std::(......)
std::cin >> b >> c;
cout << a << ' ' << f << ' ' << ch << endl;
return 0;
}
四. 缺省參數(shù)
1. 缺省參數(shù)的概念??
【概念】:缺省參數(shù)是聲明或定義函數(shù)時為函數(shù)的參數(shù)指定一個缺省值。在調(diào)用該函數(shù)時,如果沒有指定實參則采用該形參的缺省值,否則使用指定的實參。
例如:
#include <iostream>
using std::cout;
using std::endl;
// a 的缺省值為 0,如果沒有顯示傳參,那么 a 就是 0
// 顯示傳參了 a 就是那個傳過來的參數(shù)的值
// 相當(dāng)于 0 是 a 的備胎!
void func(int a = 0)
{
cout << a << endl;
}
int main()
{
func(); // 打印 0 // 沒有傳參
func(10); // 打印 10 // 顯示傳遞 10 給 a
return 0;
}
2. 缺省參數(shù)的分類??
- 全缺省參數(shù)
全缺省參數(shù)就是函數(shù)的每個形參都給一個缺省值,例如:
#include <iostream>
using std::cout;
using std::endl;
// 全缺省,每個形參都給個缺省參數(shù)
void func(int a = 1, int b = 2, int c = 3)
{
cout << a << ' ' << b << ' ' << c << endl;
}
int main()
{
func(); // 打印 1 2 3
func(9, 99, 999); // 打印 9 99 999
// 當(dāng)然也可以這樣傳遞
func(11);
// 這個 11 是形參 a 的實參 ,所以此時打印 11 2 3,
// b 和 c 沒有顯示傳遞就使用缺省值
// 值得注意的是,實參傳遞規(guī)定是從左到右傳遞,不能像這樣傳遞(規(guī)定不行):func(11, , 33);
// 這樣也行
func(11, 22); // 道理相同,打印 11 22 3
return 0;
}
- 半缺省參數(shù)
半缺省就是函數(shù)形參的缺省值從右到左連續(xù)不給完,例如:
#include <iostream>
using std::cout;
using std::endl;
// 半缺省,缺省值從右到左連續(xù)不給完,a 沒有缺省值,所以一定要顯示傳遞一個參數(shù)給 a
void func(int a, int b = 10, int c = 20)
{
cout << a << ' ' << b << ' ' << c << endl;
}
// 這樣也是半缺省
void func1(int a, int b, int c = 10)
{
cout << a << ' ' << b << ' ' << c;
}
// 為什么要從右到左連續(xù)設(shè)置缺省值呢?(規(guī)定)
// 如果不是從右到左,如下:
// 這樣使用 func2 時,調(diào)用就會有歧義.............
/*void func2(int a = 10, int b, int c = 20)
{
}*/
int main()
{
// 至少傳遞一個參數(shù),第一個參數(shù)是給 a 的。
// 后面不傳使用缺省值
func(10);
// 打印 10 10 20
// 這樣也可以
// b 也顯示傳了,使用顯示傳的值,c 照樣使用缺省值
func(10, 200);
// 打印 10 200 20
// 這樣一樣可以
// 相當(dāng)于都顯示傳了
func(9, 99, 999);
// 打印 9 99 999
return 0;
}
- 注意
- 半缺省參數(shù)必須從右往左依次來給出,不能間隔著給。
- 缺省參數(shù)不能在函數(shù)聲明和定義中同時出現(xiàn),如下會出現(xiàn)的問題:
// test.h
void Func(int a = 10);
// test.cpp
void Func(int a = 20)
{}
// 如果聲明與定義位置同時出現(xiàn),恰巧兩個位置提供的值不同,那編譯器就無法確定到底該用那個缺省值。
// 一般,只在聲明給缺省參數(shù)值,在聲明給缺省值后,定義就不能給缺省值了。
- 缺省值必須是常量或者全局變量。
- C語言不支持(編譯器不支持)。
五. 函數(shù)重載
自然語言中,一個詞可以有多重含義,人們可以通過上下文來判斷該詞真實的含義,即該詞被重載了。
比如: 以前有一個笑話,國有兩個體育項目大家根本不用看,也不用擔(dān)心。一個是乒乓球,一個是男足。前者是 “ 誰也贏不了!”,后者是 “ 誰也贏不了!“。
1. 函數(shù)重載的概念??
【函數(shù)重載】:是函數(shù)的一種特殊情況,C++
允許在同一作用域中聲明幾個功能類似的同名函數(shù),這些同名函數(shù)的形參列表(參數(shù)個數(shù) 或 類型 或 類型順序)不同,常用來處理實現(xiàn)功能類似數(shù)據(jù)類型不同的問題。
函數(shù)重載可以提高代碼的可讀性和復(fù)用性,讓程序員在保持函數(shù)名稱不變的前提下,根據(jù)不同的參數(shù)類型和個數(shù),實現(xiàn)不同的功能,避免了函數(shù)名稱過長或重復(fù)的問題。
例如:
- 在下面的代碼中,定義了兩個名為
Add
的函數(shù),但他們有不同的參數(shù)類型:
#include <iostream>
using namespace std;
// 參數(shù)類型不同
int Add(int a, int b)
{
return a + b;
}
double Add(double a, double b)
{
return a + b;
}
int main()
{
int a = 10, b = 20;
double d1 = 3.14, d2 = 13.14;
cout << Add(a, b) << endl;
cout << Add(d1, d2) << endl;
return 0;
}
第一個函數(shù)接受兩個整數(shù)參數(shù)并返回它們的和。第二個函數(shù)接受兩個double
類型參數(shù)并返回它們的和。雖然這兩個函數(shù)都叫做Add
,但由于它們帶有不同類型的參數(shù),因此編譯器可以區(qū)分它們,并在調(diào)用時根據(jù)傳遞給它們的參數(shù)類型選擇合適的函數(shù)。例如:
上面的Add(a, b)
調(diào)用第一個Add
函數(shù),Add(d1, d2)
調(diào)用第二個Add
函數(shù)。
【注意】:在C++
中,僅僅依據(jù)返回值類型無法實現(xiàn)重載。例如下面這段代碼是非法的:
int Add(int a, int b)
{
return a + b;
}
double Add(int a, int b) // 編譯錯誤! 與前面已有定義沖突
{
return (double)(a + b);
}
因為函數(shù)重載必須依據(jù)參數(shù)列表的不同來實現(xiàn)。
- 參數(shù)個數(shù)和參數(shù)類型順序不同:
// 參數(shù)個數(shù)不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int)" << endl;
}
// 參數(shù)類型順序不同
void f(int a, char b)
{
cout << "f(int a, char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
int main()
{
f(); // 打印 f()
f(10); // 打印 f(int)
f(10, 'a'); // 打印 f(int a, char b)
f('a', 10); // 打印 f(char b, int a)
return 0;
}
2. 【C++】支持函數(shù)重載的原理—名字修飾【了解即可】??
為什么C++
支持函數(shù)重載,而C語言
不支持函數(shù)重載呢?
在C/C++
中,一個程序要運(yùn)行起來,需要經(jīng)歷以下幾個階段:預(yù)處理、編譯、匯編、鏈接。
- 實際項目通常是由多個頭文件和多個源文件構(gòu)成,而通過
C
語言階段學(xué)習(xí)的編譯鏈接,我們可以知道,【當(dāng)a.cpp
中調(diào)用了b.cpp
中定義的Add
函數(shù)時】,編譯后鏈接前,a.o
的目標(biāo)文件中沒有Add
的函數(shù)地址,因為Add
是在b.cpp
中定義的,所以Add
的地址在b.o
中。那么怎么辦呢? - 所以鏈接階段就是專門處理這種問題,鏈接器看到
a.o
調(diào)用Add
,但是沒有Add
的地址,就會到b.o
的符號表中找Add
的地址,然后鏈接到一起。 - 那么鏈接時,面對
Add
函數(shù),鏈接接器會使用哪個名字去找呢?這里每個編譯器都有自己的函數(shù)名修飾規(guī)則。 - 由于
Windows
下vs
的修飾規(guī)則過于復(fù)雜,而Linux
下g++
的修飾規(guī)則簡單易懂,下面我們使用g++
演示這個修飾后的名字。 - 通過下面我們可以看出
gcc
的函數(shù)修飾后名字不變。而g++
的函數(shù)修飾后變成【_Z+函數(shù)長度
+函數(shù)名+類型首字母】:
- 采用
C
語言編譯器編譯后結(jié)果:
【結(jié)論】:在linux
下,采用gcc
編譯完成后,函數(shù)名字的修飾沒有發(fā)生改變。
- 采用
C++
編譯器編譯后結(jié)果:
【結(jié)論】:在Linux
下,采用g++
編譯完成后,函數(shù)名字的修飾發(fā)生改變,編譯器將函數(shù)參數(shù)類型信息添加到修改后的名字中。
Windows
下名字修飾規(guī)則
對比Linux
會發(fā)現(xiàn),windows
下vs
編譯器對函數(shù)名字修飾規(guī)則相對復(fù)雜難懂,但道理都是類似的,這里就不做細(xì)致的研究了。
【擴(kuò)展學(xué)習(xí)】:C/C++
函數(shù)調(diào)用約定和名字修飾規(guī)則–有興趣好奇的同學(xué)可以看看,里面有對vs
下函數(shù)名修飾規(guī)則講解。【點此深入了解】
- 通過以上就理解了
C
語言沒辦法支持重載,因為同名函數(shù)沒辦法區(qū)分。而C++
是通過函數(shù)修飾規(guī)則來區(qū)分,只要參數(shù)不同,修飾出來的名字就不一樣,就支持了重載。 - 如果兩個函數(shù)函數(shù)名和參數(shù)是一樣的,返回值不同是不構(gòu)成重載的,因為調(diào)用時編譯器沒辦法區(qū)分。
六. 引用
1. 引用的概念??
引用不是新定義一個變量,而是給已存在變量取了一個別名,編譯器不會為引用變量開辟內(nèi)存空間,它和它引用的變量共用同一塊內(nèi)存空間。
比如:李逵,在家稱為 “鐵牛” ,江湖上人稱 “黑旋風(fēng)” 。
【格式】:類型& 引用變量名(對象名) = 引用實體;
void test()
{
int a = 10;
int& ra = a; // 定義引用類型
cout << a << endl;
cout << ra << endl;
}
// a 和 ra 打印結(jié)果相同
int a = 10;
int &ref = a;
ref
是a
的引用,兩個變量共享同一個存儲空間。因此,對a
的修改會同步體現(xiàn)在ref
上,反之也是如此,例如:
a = 20; // 修改 a 的值
cout << ref; // 輸出 20
【注意】:引用類型必須和引用實體是同種類型的。
2. 引用的特性??
引用(Reference)指的是一種特殊的變量類型,它是一種為已有變量起別名的方式。引用相當(dāng)于一個變量的別名,與原變量共享同一個存儲空間,對它的操作就等同于對原變量進(jìn)行操作。引用除了一開始必須初始化指向某個對象外,不能再指向其它對象,也不能被重新賦值。
【特性】:
- 引用在定義時必須初始化
- 一個變量可以有多個引用
- 引用一旦引用一個實體,再不能引用其他實體
void test()
{
int a = 10;
// int& ra; // 該條語句編譯時會出錯 // 必須要初始化
// a 可以有多個引用
int& ra = a;
int& rra = a;
// 三個地址打印相同
printf("%p %p %p\n", &a, &ra, &rra);
}
3. 常引用問題??
關(guān)于常引用問題,可以看我之前對這點寫過的文章:【傳送門】。
void test()
{
const int a = 10;
//int& ra = a; // 該語句編譯時會出錯,a為常量
const int& ra = a;
// int& b = 10; // 該語句編譯時會出錯,b為常量
const int& b = 10;
double d = 12.34;
//int& rd = d;
// 該語句編譯時會出錯,類型不同
// d 隱式類型轉(zhuǎn)換生成臨時變量,臨時變量具有常性,因此需要 const 引用
const int& rd = d;
}
4. 使用的場景??
- 對于函數(shù)的參數(shù)傳遞,引用可以用來在函數(shù)內(nèi)部操作外部變量。使用引用可以不用傳指針,避免了指針操作的延遲以及很多指針引發(fā)的問題。
例如,定義一個函數(shù),使用引用作為參數(shù)來交換兩個變量的值:
// 傳統(tǒng)傳址方式
// void swap(int *a, int *b) {
// int temp = *a;
// *a = *b;
// *b = temp;
// }
// 傳引用
void swap(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 1, y = 2;
swap(x, y);
cout << x << " " << y; // 輸出 2 1
return 0;
}
在函數(shù)中, a
和b
分別是x
和y
的引用,它們實際上就是x
和y
本身。通過引用,在函數(shù)內(nèi)直接對原變量進(jìn)行操作,實現(xiàn)了兩個變量的交換。上面兩個函數(shù)都可以實現(xiàn)兩個整數(shù)的交換,但使用引用方式可以讓代碼更加簡潔易讀。
-
引用做返回值
-
返回靜態(tài)變量的引用
int& Count()
{
// 靜態(tài)變量
static int n = 0;
n++;
// ...
// 靜態(tài)變量可以做引用返回
// 因為靜態(tài)變量存在靜態(tài)區(qū),函數(shù)結(jié)束后不銷毀
return n;
}
- 下面代碼輸出什么結(jié)果?為什么?
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
// ret 是函數(shù)中 c 的引用,但是函數(shù)結(jié)束后,c 就銷毀了
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;
return 0;
}
運(yùn)行結(jié)果:
【注意】:如果函數(shù)返回時,出了函數(shù)作用域,如果返回對象還在(還沒還給系統(tǒng)),則可以使用引用返回,如果已經(jīng)還給系統(tǒng)了,則必須使用傳值返回。
5. 引用的好處??
以值作為參數(shù)或者返回值類型,在傳參和返回期間,函數(shù)不會直接傳遞實參或者將變量本身直接返回,而是傳遞實參或者返回變量的一份臨時的拷貝,因此用值作為參數(shù)或者返回值類型,效率是非常低下的,尤其是當(dāng)參數(shù)或者返回值類型非常大時,效率就更低。
5.1. 傳值與傳引用性能比較??
#include <iostream>
#include <time.h>
using namespace std;
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ù)運(yùn)行結(jié)束后的時間
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
TestRefAndValue();
return 0;
}
5.2. 值返回和引用返回性能比較??
#include <iostream>
#include <time.h>
using namespace std;
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ù)運(yùn)算完成之后的時間
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
TestReturnByRefOrValue();
return 0;
}
運(yùn)行結(jié)果:
通過上述代碼的比較,發(fā)現(xiàn)傳值和引用在作為傳參以及返回值類型上效率相差很大。因此,我們在后面寫代碼的過程中,能用引用就用引用。
6. 指針和引用的區(qū)別??
在語法概念上引用就是一個別名,沒有獨立空間,和其引用實體共用同一塊空間。
int main()
{
int a = 10;
int& ra = a;
// 打印相同
cout<<"&a = "<< &a <<endl;
cout<<"&ra = "<< &ra <<endl;
return 0;
}
在底層實現(xiàn)上實際是有空間的,因為引用是按照指針方式來實現(xiàn)的。
int main()
{
int a = 10;
int& ra = a;
// 引用改值
ra = 20;
// 指針改值
int* pa = &a;
*pa = 20;
return 0;
}
我們來看下引用和指針的匯編代碼對比:
可以看到,引用和指針的匯編都是一樣的,所以引用的底層就是相當(dāng)于一個指針。
引用和指針的不同點:
- 引用概念上定義一個變量的別名,指針存儲一個變量地址;
- 引用在定義時必須初始化,指針沒有要求;
- 引用在初始化時引用一個實體后,就不能再引用其他實體,而指針可以在任何時候指向任何一個同類型實體;
- 沒有
NULL
引用,但有NULL
指針; - 在
sizeof
中含義不同:引用結(jié)果為引用類型的大小,但指針始終是地址空間所占字節(jié)個數(shù)(32
位平臺下占4
個字節(jié)); - 引用自加即引用的實體增加
1
,指針自加即指針向后偏移一個類型的大??; - 有多級指針,但是沒有多級引用;
- 訪問實體方式不同,指針需要顯式解引用,引用編譯器自己處理;
- 引用比指針使用起來相對更安全。
七. 內(nèi)聯(lián)函數(shù)
1. 內(nèi)聯(lián)函數(shù)的概念?????
【概念】:內(nèi)聯(lián)函數(shù)是一種函數(shù)調(diào)用方式,它的目的是提高程序的執(zhí)行速度。內(nèi)聯(lián)函數(shù)的特點是在編譯器的編譯過程中將函數(shù)的代碼拷貝到調(diào)用處,而不是通過函數(shù)調(diào)用的方式來執(zhí)行。這種方式可以避免函數(shù)調(diào)用的時間和資源開銷,提高程序的效率。
內(nèi)聯(lián)函數(shù)在 C++
中可以通過在函數(shù)聲明前加上關(guān)鍵字 inline
來定義。在定義內(nèi)聯(lián)函數(shù)的時候需要注意以下幾點:
- 內(nèi)聯(lián)函數(shù)通常用于執(zhí)行簡單的任務(wù),如果函數(shù)體過長,編譯器可能會忽略
inline
關(guān)鍵字。 - 內(nèi)聯(lián)函數(shù)不能被遞歸調(diào)用。
- 內(nèi)聯(lián)函數(shù)通常包含在頭文件中,這樣可以在多個源文件中使用。
總體來說,內(nèi)聯(lián)函數(shù)在應(yīng)對一些簡單的操作時非常有用,能夠提高程序的運(yùn)行效率,但也需要注意函數(shù)體的長度以及適用場景。
從匯編代碼的角度來看,正常的函數(shù):
如果在上述函數(shù)前增加inline
關(guān)鍵字將其改成內(nèi)聯(lián)函數(shù),在編譯期間編譯器會用函數(shù)體替換函數(shù)的調(diào)用。
【查看方式】:
- 在
release
模式下,查看編譯器生成的匯編代碼中是否存在call Add
。 - 在
debug
模式下,需要對編譯器進(jìn)行設(shè)置,否則不會展開(因為debug
模式下,編譯器默認(rèn)不會對代碼進(jìn)行優(yōu)化,以下給出vs
的設(shè)置方式)。
改為內(nèi)聯(lián)函數(shù)匯編代碼如下:
(可以看到?jīng)]有call ......
,而是直接在調(diào)用位置展開)
2. 內(nèi)聯(lián)函數(shù)的特性?????
-
inline
是一種以空間換時間的做法,如果編譯器將函數(shù)當(dāng)成內(nèi)聯(lián)函數(shù)處理,在編譯階段,會用函數(shù)體替換函數(shù)調(diào)用,缺陷:可能會使目標(biāo)文件變大,優(yōu)勢:少了調(diào)用開銷,提高程序運(yùn)行效率。 -
inline
對于編譯器而言只是一個建議,不同編譯器關(guān)于inline
實現(xiàn)機(jī)制可能不同,一般建議:將函數(shù)規(guī)模較小(即函數(shù)不是很長,具體沒有準(zhǔn)確的說法,取決于編譯器內(nèi)部實現(xiàn))、不是遞歸、且頻繁調(diào)用的函數(shù)采用inline
修飾,否則編譯器會忽略inline
特性。 -
inline
不建議聲明和定義分離,分離會導(dǎo)致鏈接錯誤。因為inline
被展開,就沒有函數(shù)地址了,鏈接就會找不到。
// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
f(10);
return 0;
}
// 程序運(yùn)行會有類似以下的錯誤:
// 鏈接錯誤:main.obj : error LNK2019: 無法解析的外部符號 "void __cdecl
// f(int)" (?f@@YAXH@Z),該符號在函數(shù) _main 中被引用
有關(guān)宏的相關(guān)面試題(宏與內(nèi)聯(lián)相像):
【宏的優(yōu)缺點】?
【優(yōu)點】:
- 增強(qiáng)代碼的復(fù)用性。
- 提高性能。
【缺點】:
- 不方便調(diào)試宏。(因為預(yù)編譯階段進(jìn)行了替換)
- 導(dǎo)致代碼可讀性差,可維護(hù)性差,容易誤用。
- 沒有類型安全的檢查 。
【C++有哪些技術(shù)替代宏】?
- 常量定義 換用
const enum
。 - 短小函數(shù)定義 換用內(nèi)聯(lián)函數(shù)。
八. 【C++11】auto 關(guān)鍵字
1. 類型別名思考?
隨著程序越來越復(fù)雜,程序中用到的類型也越來越復(fù)雜,經(jīng)常體現(xiàn)在:
- 類型難于拼寫。
- 含義不明確導(dǎo)致容易出錯。
例如下面代碼:
#include <iostream>
#include <string>
#include <map>
using namespace std;
int main()
{
std::map<std::string, std::string> m{ { "apple", "蘋果" }, { "orange",
"橙子" }, {"pear","梨"} };
std::map<std::string, std::string>::iterator it = m.begin();
while (it != m.end())
{
//....
}
return 0;
}
std::map<std::string, std::string>::iterator
是一個類型,但是該類型太長了,特別容易寫錯。聰明的同學(xué)可能已經(jīng)想到:可以通過typedef
給類型取別名,比如:
#include <iostream>
#include <string>
#include <map>
using namespace std;
typedef std::map<std::string, std::string> Map;
int main()
{
Map m{ { "apple", "蘋果" },{ "orange", "橙子" }, {"pear","梨"} };
Map::iterator it = m.begin();
while (it != m.end())
{
//....
}
return 0;
}
使用typedef
給類型取別名確實可以簡化代碼,但是typedef
又會遇到新的難題:
typedef char* pstring;
int main()
{
const pstring p1; // 編譯成功還是失?。?/span>
const pstring* p2; // 編譯成功還是失?。?/span>
return 0;
}
在編程時,常常需要把表達(dá)式的值賦值給變量,這就要求在聲明變量的時候清楚地知道表達(dá)式的類型。然而有時候要做到這點并非那么容易,因此C++11
給auto
賦予了新的含義。
2. auto 簡介?
在早期C/C++
中auto
的含義是:使用auto
修飾的變量,是具有自動存儲器的局部變量,但遺憾的是一直沒有人去使用它,大家可思考下為什么?
C++11
中,標(biāo)準(zhǔn)委員會賦予了auto
全新的含義即:auto
不再是一個存儲類型指示符,而是作為一個新的類型指示符來指示編譯器,auto
聲明的變量必須由編譯器在編譯時期推導(dǎo)而得。
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
// typeid(變量名).name(): 得出變量類型
cout << typeid(b).name() << endl; // int
cout << typeid(c).name() << endl; // char
cout << typeid(d).name() << endl; // int
//auto e; 無法通過編譯,使用auto定義變量時必須對其進(jìn)行初始化
return 0;
}
【注意】:使用auto
定義變量時必須對其進(jìn)行初始化,在編譯階段編譯器需要根據(jù)初始化表達(dá)式來推導(dǎo)auto
的實際類型。因此auto
并非是一種 “類型” 的聲明,而是一個類型聲明時的 “占位符” ,編譯器在編譯期會將auto
替換為變量實際的類型。
3. auto 的使用規(guī)則?
auto
與指針和引用結(jié)合起來使用:用auto
聲明指針類型時,用auto
和auto*
沒有任何區(qū)別,但用auto
聲明引用類型時則必須加&
。
如下使用:
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl; // int * __ptr64
cout << typeid(b).name() << endl; // int * __ptr64
cout << typeid(c).name() << endl; // int
*a = 20;
cout << x << endl; // 20
*b = 30;
cout << x << endl; // 30
c = 40;
cout << x << endl; // 40
return 0;
}
- 在同一行定義多個變量:當(dāng)在同一行聲明多個變量時,這些變量必須是相同的類型,否則編譯器將會報錯,因為編譯器實際只對第一個類型進(jìn)行推導(dǎo),然后用推導(dǎo)出來的類型定義其他變量。
void test()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 該行代碼會編譯失敗,因為c和d的初始化表達(dá)式類型不同
}
4. auto 不能推導(dǎo)的場景?
-
auto
不能作為函數(shù)的參數(shù):
// 此處代碼編譯失敗,auto不能作為形參類型,因為編譯器無法對a的實際類型進(jìn)行推導(dǎo)
void test(auto a)
{}
-
auto
不能直接用來聲明數(shù)組:
void test()
{
int a[] = {1,2,3};
auto b[] = {4,5,6}; // 這是不行的
}
- 為了避免與
C++98
中的auto
發(fā)生混淆,C++11
只保留了auto
作為類型指示符的用法。 -
auto
在實際中最常見的優(yōu)勢用法就是跟后面的C++11
提供的新式for
循環(huán),還有lambda
表達(dá)式等進(jìn)行配合使用。
九. 【C++11】基于范圍的 for 循環(huán)
1. 范圍 for 的語法??
在C++98
中如果要遍歷一個數(shù)組,可以按照以下方式進(jìn)行:
void test()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
cout << *p << endl;
}
對于一個有范圍的集合而言,由程序員來說明循環(huán)的范圍是多余的,有時候還會容易犯錯誤。因此C++11
中引入了基于范圍的for
循環(huán)。for
循環(huán)后的括號由冒號“ :”
分為兩部分:第一部分是范圍內(nèi)用于迭代的變量,第二部分則表示被迭代的范圍。
void test()
{
int array[] = { 1, 2, 3, 4, 5 };
// auto自動推到類型為 int ,然后 & 引用改數(shù)組的元素
// e 就是數(shù)組元素的引用
// 自動遍歷數(shù)組取值
for(auto& e : array)
e *= 2; // 如果是引用就可以修改數(shù)組的值,不是引用的修改影響不了數(shù)組
// 打印
for(auto e : array)
cout << e << " ";
return 0;
}
【注意】:與普通循環(huán)類似,可以用continue
來結(jié)束本次循環(huán),也可以用break
來跳出整個循環(huán)。
2. 范圍 for 的使用條件??
-
for
循環(huán)迭代的范圍必須是確定的:對于數(shù)組而言,就是數(shù)組中第一個元素和最后一個元素的范圍;對于類而言,應(yīng)該提供begin
和end
的方法,begin
和end
就是for
循環(huán)迭代的范圍。
【注意】:以下代碼就有問題,因為for
的范圍不確定
void test(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
- 迭代的對象要實現(xiàn)
++
和==
的操作。(關(guān)于迭代器這個問題,后面再涉及,這里不做講解,現(xiàn)在知道這個不行就ok
啦)。
十. 【C++11】指針空值 nullptr
C++98
中的指針空值
在良好的C/C++
編程習(xí)慣中,聲明一個變量時最好給該變量一個合適的初始值,否則可能會出現(xiàn)不可預(yù)料的錯誤,比如未初始化的指針。如果一個指針沒有合法的指向,我們基本都是按照如下方式對其進(jìn)行初始化:
void test()
{
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()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
程序本意是想通過f(NULL)
調(diào)用指針版本的f(int*)
函數(shù),但是由于NULL
被定義成0
,因此與程序的初衷相悖。
在C++98
中,字面常量0
既可以是一個整形數(shù)字,也可以是無類型的指針(void*)
常量,但是編譯器默認(rèn)情況下將其看成是一個整形常量,如果要將其按照指針方式來使用,必須對其進(jìn)行強(qiáng)轉(zhuǎn)(void*) 0
,所以C++11
引入了一個新關(guān)鍵字表示空:nullptr
。
-
C++11
中的指針空值
- 在使用
nullptr
表示指針空值時,不需要包含頭文件,因為nullptr
是C++11
作為新關(guān)鍵字引入的。 - 在
C++11
中,sizeof(nullptr)
與sizeof((void*)0)
所占的字節(jié)數(shù)相同。 - 為了提高代碼的健壯性,在后續(xù)表示指針空值時建議最好使用
nullptr
。
??寫在最后
??本章介紹的知識有:關(guān)鍵字概述,命名空間,輸入輸出,缺省參數(shù),引用,內(nèi)聯(lián)函數(shù),
auto
關(guān)鍵字,范圍for
,nullptr
空值。這些知識大多是補(bǔ)充了一些C
語言的缺陷,讓我們在寫代碼的時候能夠更加輕松的應(yīng)對一些不同的代碼需求,當(dāng)然,這些知識在后面也會反復(fù)的用到,馬上就要進(jìn)入的類和對象就需要大量使用到本章的知識。因此,學(xué)好本章的知識是進(jìn)入C++
不可或缺的一步。
?????后續(xù)將會繼續(xù)輸出有關(guān)
C++
的文章,你們的支持就是我寫作的最大動力!
感謝閱讀本小白的博客,錯誤的地方請嚴(yán)厲指出噢~文章來源:http://www.zghlxwxcb.cn/news/detail-438822.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-438822.html
到了這里,關(guān)于【C++】打開C++大門,踏入C++世界的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!