第三章 C++對C的擴展
3.1 ::作用域運算符
???????通常情況下,如果有兩個同名變量,一個是全局變量,另一個是局部變量,那么局部變量在其作用域內(nèi)具有較高的優(yōu)先權(quán),它將屏蔽全局變量。
//全局變量
int a = 10;
void test(){
//局部變量
int a = 20;
//全局a被隱藏
cout << "a:" << a << endl;
}
程序的輸出結(jié)果是a:20。
在test函數(shù)的輸出語句中,使用的變量a是test函數(shù)內(nèi)定義的局部變量,因此輸出的結(jié)果為局部變量a的值。
作用域運算符可以用來解決局部變量與全局變量的重名問題
//全局變量
int a = 10;
//1. 局部變量和全局變量同名
void test(){
int a = 20;
//打印局部變量a
cout << "局部變量a:" << a << endl;
//打印全局變量a
cout << "全局變量a:" << ::a << endl;
}
???????這個例子可以看出,作用域運算符可以用來解決局部變量與全局變量的重名問題,即在局部變量的作用域內(nèi),可用 :: 對被屏蔽的同名的全局變量進行訪問。
3.2 名字控制
???????創(chuàng)建名字是程序設(shè)計過程中一項最基本的活動,當(dāng)一個項目很大時,它會不可避免地包含大量名字。c++ 允許我們對名字的產(chǎn)生和名字的可見性進行控制。
???????我們之前在學(xué)習(xí)c語言可以通過static關(guān)鍵字來使得名字只得在本編譯單元內(nèi)可見,在c++ 中我們將通過一種通過命名空間來控制對名字的訪問。
3.2.1 C++命名空間(namespace)
???????在c++ 中,名稱(name)可以是符號常量、變量、函數(shù)、結(jié)構(gòu)、枚舉、類和對象等等。工程越大,名稱互相沖突性的可能性越大。另外使用多個廠商的類庫時,也可能導(dǎo)致名稱沖突。為了避免,在大規(guī)模程序的設(shè)計中,以及在程序員使用各種各樣的C++ 庫時,這些標(biāo)識符的命名發(fā)生沖突,標(biāo)準(zhǔn)C++ 引入關(guān)鍵字namespace(命名空間/名字空間/名稱空間),可以更好地控制標(biāo)識符的作用域。
3.2.2 命名空間使用語法
- 創(chuàng)建一個命名空間:
namespace A{
int a = 10;
}
namespace B{
int a = 20;
}
void test(){
cout << "A::a : " << A::a << endl;
cout << "B::a : " << B::a << endl;
}
- 命名空間只能命名空全局范圍內(nèi)定義(以下錯誤寫法)
void test(){
namespace A{
int a = 10;
}
namespace B{
int a = 20;
}
cout << "A::a : " << A::a << endl;
cout << "B::a : " << B::a << endl;
}
- 命名空間可嵌套命名空間
namespace A{
int a = 10;
namespace B{
int a = 20;
}
}
void test(){
cout << "A::a : " << A::a << endl;
cout << "A::B::a : " << A::B::a << endl;
}
- 命名空間是開放的,即可以隨時把新的成員加入已有的命名空間中
namespace A{
int a = 10;
}
namespace A{
void func(){
cout << "hello namespace!" << endl;
}
}
void test(){
cout << "A::a : " << A::a << endl;
A::func();
}
- 聲明和實現(xiàn)可分離
#pragma once
namespace MySpace{
void func1();
void func2(int param);
}
void MySpace::func1(){
cout << "MySpace::func1" << endl;
}
void MySpace::func2(int param){
cout << "MySpace::func2 : " << param << endl;
}
- 無名命名空間,意味著命名空間中的標(biāo)識符只能在本文件內(nèi)訪問,相當(dāng)于給這個標(biāo)識符加上了static,使得其可以作為內(nèi)部連接
namespace{
int a = 10;
void func(){ cout << "hello namespace" << endl; }
}
void test(){
cout << "a : " << a << endl;
func();
}
- 命名空間別名
namespace veryLongName{
int a = 10;
void func(){ cout << "hello namespace" << endl; }
}
void test(){
namespace shortName = veryLongName;
cout << "veryLongName::a : " << shortName::a << endl;
veryLongName::func();
shortName::func();
}
3.2.3 using聲明
???????using聲明可使得指定的標(biāo)識符可用。
namespace A{
int paramA = 20;
int paramB = 30;
void funcA(){ cout << "hello funcA" << endl; }
void funcB(){ cout << "hello funcA" << endl; }
}
void test(){
//1. 通過命名空間域運算符
cout << A::paramA << endl;
A::funcA();
//2. using聲明
using A::paramA;
using A::funcA;
cout << paramA << endl;
//cout << paramB << endl; //不可直接訪問
funcA();
//3. 同名沖突
//int paramA = 20; //相同作用域注意同名沖突
}
???????using聲明碰到函數(shù)重載
namespace A{
void func(){}
void func(int x){}
int func(int x,int y){}
}
void test(){
using A::func;
func();
func(10);
func(10, 20);
}
???????如果命名空間包含一組用相同名字重載的函數(shù),using聲明就聲明了這個重載函數(shù)的所有集合。
3.2.4 using編譯指令
???????using編譯指令使整個命名空間標(biāo)識符可用.
namespace A{
int paramA = 20;
int paramB = 30;
void funcA(){ cout << "hello funcA" << endl; }
void funcB(){ cout << "hello funcB" << endl; }
}
void test01(){
using namespace A;
cout << paramA << endl;
cout << paramB << endl;
funcA();
funcB();
//不會產(chǎn)生二義性
int paramA = 30;
cout << paramA << endl;
}
namespace B{
int paramA = 20;
int paramB = 30;
void funcA(){ cout << "hello funcA" << endl; }
void funcB(){ cout << "hello funcB" << endl; }
}
void test02(){
using namespace A;
using namespace B;
//二義性產(chǎn)生,不知道調(diào)用A還是B的paramA
//cout << paramA << endl;
}
注意:使用using聲明或using編譯指令會增加命名沖突的可能性。也就是說,如果有名稱空間,并在代碼中使用作用域解析運算符,則不會出現(xiàn)二義性。
3.2.5 命名空間使用
???????需要記住的關(guān)鍵問題是當(dāng)引入一個全局的using編譯指令時,就為該文件打開了該命名空間,它不會影響任何其他的文件,所以可以在每一個實現(xiàn)文件中調(diào)整對命名空間的控制。比如,如果發(fā)現(xiàn)某一個實現(xiàn)文件中有太多的using指令而產(chǎn)生的命名沖突,就要對該文件做個簡單的改變,通過明確的限定或者using聲明來消除名字沖突,這樣不需要修改其他的實現(xiàn)文件。
3.3 全局變量檢測增強
c語言代碼:
int a = 10; //賦值,當(dāng)做定義
int a; //沒有賦值,當(dāng)做聲明
int main(){
printf("a:%d\n",a);
return EXIT_SUCCESS;
}
此代碼在c++下編譯失敗,在c下編譯通過.
3.4 C++中所有的變量和函數(shù)都必須有類型
c語言代碼:
//i沒有寫類型,可以是任意類型
int fun1(i){
printf("%d\n", i);
return 0;
}
//i沒有寫類型,可以是任意類型
int fun2(i){
printf("%s\n", i);
return 0;
}
//沒有寫參數(shù),代表可以傳任何類型的實參
int fun3(){
printf("fun33333333333333333\n");
return 0;
}
//C語言,如果函數(shù)沒有參數(shù),建議寫void,代表沒有參數(shù)
int fun4(void){
printf("fun4444444444444\n");
return 0;
}
g(){
return 10;
}
int main(){
fun1(10);
fun2("abc");
fun3(1, 2, "abc");
printf("g = %d\n", g());
return 0;
}
以上c代碼c編譯器編譯可通過,c++ 編譯器無法編譯通過。
- 在C語言中,int fun() 表示返回值為int,接受任意參數(shù)的函數(shù),int fun(void) 表示返回值為int的無參函數(shù)。
- 在C++ 中,int fun() 和int fun(void) 具有相同的意義,都表示返回值為int的無參函數(shù)。
3.5 更嚴(yán)格的類型轉(zhuǎn)換
在C++,不同類型的變量一般是不能直接賦值的,需要相應(yīng)的強轉(zhuǎn)。
c語言代碼:
typedef enum COLOR{ GREEN, RED, YELLOW } color;
int main(){
color mycolor = GREEN;
mycolor = 10;
printf("mycolor:%d\n", mycolor);
char* p = malloc(10);
return EXIT_SUCCESS;
}
以上c代碼c編譯器編譯可通過,c++ 編譯器無法編譯通過。
3.6 struct類型加強
- c中定義結(jié)構(gòu)體變量需要加上struct關(guān)鍵字,c++ 不需要。
- c中的結(jié)構(gòu)體只能定義成員變量,不能定義成員函數(shù)。c++ 即可以定義成員變量,也可以定義成員函數(shù)。
//1. 結(jié)構(gòu)體中即可以定義成員變量,也可以定義成員函數(shù)
struct Student{
string mName;
int mAge;
void setName(string name){ mName = name; }
void setAge(int age){ mAge = age; }
void showStudent(){
cout << "Name:" << mName << " Age:" << mAge << endl;
}
};
//2. c++中定義結(jié)構(gòu)體變量不需要加struct關(guān)鍵字
void test01(){
Student student;
student.setName("John");
student.setAge(20);
student.showStudent();
}
3.7 新增bool類型關(guān)鍵字
???????標(biāo)準(zhǔn)c++ 的bool類型有兩種內(nèi)建的常量true(轉(zhuǎn)換為整數(shù)1)和false(轉(zhuǎn)換為整數(shù)0)表示狀態(tài)。這三個名字都是關(guān)鍵字。
- bool類型只有兩個值,true(1值),false(0值)
- bool類型占1個字節(jié)大小
- 給bool類型賦值時,非0值會自動轉(zhuǎn)換為true(1),0值會自動轉(zhuǎn)換false(0)
void test()
{ cout << sizeof(false) << endl; //為1,//bool類型占一個字節(jié)大小
bool flag = true; // c語言中沒有這種類型
flag = 100; //給bool類型賦值時,非0值會自動轉(zhuǎn)換為true(1),0值會自動轉(zhuǎn)換false(0)
}
c語言中的bool類型:
???????c語言中也有bool類型,在c99標(biāo)準(zhǔn)之前是沒有bool關(guān)鍵字,c99標(biāo)準(zhǔn)已經(jīng)有bool類型,包含頭文件stdbool.h,就可以使用和c++ 一樣的bool類型。
3.8 三目運算符功能增強
- c語言三目運算表達式返回值為數(shù)據(jù)值,為右值,不能賦值。
int a = 10;
int b = 20;
printf("ret:%d\n", a > b ? a : b);
//思考一個問題,(a > b ? a : b) 三目運算表達式返回的是什么?
//(a > b ? a : b) = 100;
//返回的是右值
- c++ 語言三目運算表達式返回值為變量本身(引用),為左值,可以賦值。
int a = 10;
int b = 20;
printf("ret:%d\n", a > b ? a : b);
//思考一個問題,(a > b ? a : b) 三目運算表達式返回的是什么?
cout << "b:" << b << endl;
//返回的是左值,變量的引用
(a > b ? a : b) = 100;//返回的是左值,變量的引用
cout << "b:" << b << endl;
左值和右值概念:
在c++ 中可以放在賦值操作符左邊的是左值,可以放到賦值操作符右面的是右值。
有些變量即可以當(dāng)左值,也可以當(dāng)右值。
左值為Lvalue,L代表Location,表示內(nèi)存可以尋址,可以賦值。
右值為Rvalue,R代表Read,就是可以知道它的值。
比如:int temp = 10;temp在內(nèi)存中有地址,10沒有,但是可以Read到它的值。
3.9 C/C++中的const
3.9.1 const概述
???????const單詞字面意思為常數(shù),不變的。它是c/c++ 中的一個關(guān)鍵字,是一個限定符,它用來限定一個變量不允許改變,它將一個對象轉(zhuǎn)換成一個常量。
const int a = 10;
A = 100; //編譯錯誤,const是一個常量,不可修改
3.9.2 C/C++中const的區(qū)別
3.9.2.1 C中的const
???????常量的引進是在c++ 早期版本中,當(dāng)時標(biāo)準(zhǔn)C規(guī)范正在制定。那時,盡管C委員會決定在C中引入const,但是,他們c中的const理解為”一個不能改變的普通變量”,也就是認為const應(yīng)該是一個只讀變量,既然是變量那么就會給const分配內(nèi)存,并且在c中const是一個全局只讀變量,c語言中const修飾的只讀變量是外部連接的。
???????如果這么寫:
const int arrSize = 10;
int arr[arrSize];
???????看似是一件合理的編碼,但是這將得出一個錯誤。 因為arrSize占用某塊內(nèi)存,所以C編譯器不知道它在編譯時的值是多少?
3.9.2.2 C++中的const
???????在c++ 中,一個const不必創(chuàng)建內(nèi)存空間,而在c中,一個const總是需要一塊內(nèi)存空間。在c++ 中,是否為const常量分配內(nèi)存空間依賴于如何使用。一般說來,如果一個const僅僅用來把一個名字用一個值代替(就像使用 #define 一樣),那么該存儲局空間就不必創(chuàng)建。
???????如果存儲空間沒有分配內(nèi)存的話,在進行完數(shù)據(jù)類型檢查后,為了代碼更加有效,值也許會折疊到代碼中。
???????不過,取一個const地址, 或者把它定義為extern,則會為該const創(chuàng)建內(nèi)存空間。
??????? 在c++ 中,出現(xiàn)在所有函數(shù)之外的const作用于整個文件(也就是說它在該文件外不可見),默認為內(nèi)部連接,c++ 中其他的標(biāo)識符一般默認為外部連接。
3.9.2.3 C/C++中const異同總結(jié)
- c語言全局const會被存儲到只讀數(shù)據(jù)段。c++ 中全局const當(dāng)聲明extern或者對變量取地址時,編譯器會分配存儲地址,變量存儲在只讀數(shù)據(jù)段。兩個都受到了只讀數(shù)據(jù)段的保護,不可修改。
const int constA = 10;
int main(){
int* p = (int*)&constA;
*p = 200;
}
??????? 以上代碼在c/c++ 中編譯通過,在運行期,修改constA的值時,發(fā)生寫入錯誤。原因是修改只讀數(shù)據(jù)段的數(shù)據(jù)。
- c語言中局部const存儲在堆棧區(qū),只是不能通過變量直接修改const只讀變量的值,但是可以跳過編譯器的檢查,通過指針間接修改const值。
const int constA = 10;
int* p = (int*)&constA;
*p = 300;
printf("constA:%d\n",constA);
printf("*p:%d\n", *p);
運行結(jié)果:
constA:300
*p:300
???????c語言中,通過指針間接賦值修改了constA的值。
c++中對于局部的const變量要區(qū)別對待:
???????1. 對于基礎(chǔ)數(shù)據(jù)類型,也就是const int a = 10這種,編譯器會把它放到符號表中,不分配內(nèi)存,當(dāng)對其取地址時,會分配內(nèi)存。
const int constA = 10;
int* p = (int*)&constA;
*p = 300;
cout << "constA:" << constA << endl;
cout << "*p:" << *p << endl;
運行結(jié)果:
constA:10
*p:300
constA在符號表中,當(dāng)我們對constA取地址,這個時候為constA分配了新的空間,*p操作的是分配的空間,而 constA 是從符號表獲得的值。
???????2. 對于基礎(chǔ)數(shù)據(jù)類型,如果用一個變量初始化const變量,如果const int a = b,那么也是會給a分配內(nèi)存。
int b = 10;
const int constA = b;
int* p = (int*)&constA;
*p = 300;
cout << "constA:" << constA << endl;
cout << "*p:" << *p << endl;
運行結(jié)果:
constA:300
*p:300
???????constA 分配了內(nèi)存,所以我們可以修改constA內(nèi)存中的值。
???????3. 對于自定數(shù)據(jù)類型,比如類對象,那么也會分配內(nèi)存。
const Person person; //未初始化age
//person.age = 50; //不可修改
Person* pPerson = (Person*)&person;
//指針間接修改
pPerson->age = 100;
cout << "pPerson->age:" << pPerson->age << endl;
pPerson->age = 200;
cout << "pPerson->age:" << pPerson->age << endl;
運行結(jié)果:
pPerson->age:100
pPerson->age:200
???????為person分配了內(nèi)存,所以我們可以通過指針的間接賦值修改person對象。
-
c中const默認為外部連接,c++ 中const默認為內(nèi)部連接.當(dāng)c語言兩個文件中都有
const int a的時候,編譯器會報重定義的錯誤。而在c++ 中,則不會,因為c++ 中的const默認是內(nèi)部連接的。如果想讓c++ 中的const具有外部連接,必須顯示聲明為:extern const int a = 10;
???????const由c++ 采用,并加進標(biāo)準(zhǔn)c中,盡管他們很不一樣。在c中,編譯器對待const如同對待變量一樣,只不過帶有一個特殊的標(biāo)記,意思是”你不能改變我”。在c++ 中定義const時,編譯器為它創(chuàng)建空間,所以如果在兩個不同文件定義多個同名的const,鏈接器將發(fā)生鏈接錯誤。簡而言之,const在c++ 中用的更好。
了解: 能否用變量定義數(shù)組:
在支持c99標(biāo)準(zhǔn)的編譯器中,可以使用變量定義數(shù)組。
- 微軟官方描述vs2013編譯器不支持c99.:
Microsoft C conforms to the standard for the C language as set forth in the 9899:1990 edition of the ANSI C standard.
- 以下代碼在Linux GCC支持c99編譯器編譯通過
int a = 10;
int arr[a];
int i = 0;
for(;i<10;i++)
arr[i] = i;
i = 0;
for(;i<10;i++)
printf("%d\n",arr[i]);
3.9.3 盡量以const替換#define
???????在舊版本C中,如果想建立一個常量,必須使用預(yù)處理器”
???????#define MAX 1024;
???????我們定義的宏MAX從未被編譯器看到過,因為在預(yù)處理階段,所有的MAX已經(jīng)被替換為了1024,于是MAX并沒有將其加入到符號表中。但我們使用這個常量獲得一個編譯錯誤信息時,可能會帶來一些困惑,因為這個信息可能會提到1024,但是并沒有提到MAX.如果MAX被定義在一個不是你寫的頭文件中,你可能并不知道1024代表什么,也許解決這個問題要花費很長時間。
???????解決辦法就是用一個常量替換上面的宏。
???????const int max= 1024;
const和#define區(qū)別總結(jié):
1.const有類型,可進行編譯器類型安全檢查。#define無類型,不可進行類型檢查.
2.const有作用域,而#define 不重視作用域,默認定義處到文件結(jié)尾.如果定義在指定作用域下有效的常量,那么 #define 就不能用。
- 宏常量沒有類型,所以調(diào)用了int類型重載的函數(shù)。const有類型,所以調(diào)用希望的short類型函數(shù)?
#define PARAM 128
const short param = 128;
void func(short a){
cout << "short!" << endl;
}
void func(int a){
cout << "int" << endl;
}
- 宏常量不重視作用域.
void func1(){
const int a = 10;
#define A 20
//#undef A //卸載宏常量A
}
void func2(){
//cout << "a:" << a << endl; //不可訪問,超出了const int a作用域
cout << "A:" << A << endl; //#define作用域從定義到文件結(jié)束或者到#undef,可訪問
}
int main(){
func2();
return EXIT_SUCCESS;
}
問題: 宏常量可以有命名空間嗎?
namespace MySpace{
#define num 1024
}
void test(){
//cout << MySpace::NUM << endl; //錯誤
//int num = 100; //命名沖突
cout << num << endl;
}
3.10 引用(reference)
3.10.1 引用基本用法
???????引用是c++ 對c的重要擴充。在c/c++ 中指針的作用基本都是一樣的,但是c++ 增加了另外一種給函數(shù)傳遞地址的途徑,這就是按引用傳遞(pass-by-reference),它也存在于其他一些編程語言中,并不是c++ 的發(fā)明。
- 變量名實質(zhì)上是一段連續(xù)內(nèi)存空間的別名,是一個標(biāo)號(門牌號)
- 程序中通過變量來申請并命名內(nèi)存空間
- 通過變量的名字可以使用存儲空間
???????對一段連續(xù)的內(nèi)存空間只能取一個別名嗎?
???????c++ 中新增了引用的概念,引用可以作為一個已定義變量的別名。
???????基本語法:
Type& ref = val;
???????注意事項:
- &在此不是求地址運算,而是起標(biāo)識作用。
- 類型標(biāo)識符是指目標(biāo)變量的類型
- 必須在聲明引用變量時進行初始化。
- 引用初始化之后不能改變。
- 不能有NULL引用。必須確保引用是和一塊合法的存儲單元關(guān)聯(lián)。
- 可以建立對數(shù)組的引用。
//1. 認識引用
void test01(){
int a = 10;
//給變量a取一個別名b
int& b = a;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
cout << "------------" << endl;
//操作b就相當(dāng)于操作a本身
b = 100;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
cout << "------------" << endl;
//一個變量可以有n個別名
int& c = a;
c = 200;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
cout << "c:" << c << endl;
cout << "------------" << endl;
//a,b,c的地址都是相同的
cout << "a:" << &a << endl;
cout << "b:" << &b << endl;
cout << "c:" << &c << endl;
}
//2. 使用引用注意事項
void test02(){
//1) 引用必須初始化
//int& ref; //報錯:必須初始化引用
//2) 引用一旦初始化,不能改變引用
int a = 10;
int b = 20;
int& ref = a;
ref = b; //不能改變引用
//3) 不能對數(shù)組建立引用
int arr[10];
//int& ref3[10] = arr;
}
//1. 建立數(shù)組引用方法一
typedef int ArrRef[10];
int arr[10];
ArrRef& aRef = arr;
for (int i = 0; i < 10;i ++){
aRef[i] = i+1;
}
for (int i = 0; i < 10;i++){
cout << arr[i] << " ";
}
cout << endl;
//2. 建立數(shù)組引用方法二
int(&f)[10] = arr;
for (int i = 0; i < 10; i++){
f[i] = i+10;
}
for (int i = 0; i < 10; i++){
cout << arr[i] << " ";
}
cout << endl;
3.10.2 函數(shù)中的引用
???????最常見看見引用的地方是在函數(shù)參數(shù)和返回值中。當(dāng)引用被用作函數(shù)參數(shù)的時,在函數(shù)內(nèi)對任何引用的修改,將對還函數(shù)外的參數(shù)產(chǎn)生改變。當(dāng)然,可以通過傳遞一個指針來做相同的事情,但引用具有更清晰的語法。
???????如果從函數(shù)中返回一個引用,必須像從函數(shù)中返回一個指針一樣對待。當(dāng)函數(shù)返回值時,引用關(guān)聯(lián)的內(nèi)存一定要存在。
//值傳遞
void ValueSwap(int m,int n){
int temp = m;
m = n;
n = temp;
}
//地址傳遞
void PointerSwap(int* m,int* n){
int temp = *m;
*m = *n;
*n = temp;
}
//引用傳遞
void ReferenceSwap(int& m,int& n){
int temp = m;
m = n;
n = temp;
}
void test(){
int a = 10;
int b = 20;
//值傳遞
ValueSwap(a, b);
cout << "a:" << a << " b:" << b << endl;
//地址傳遞
PointerSwap(&a, &b);
cout << "a:" << a << " b:" << b << endl;
//引用傳遞
ReferenceSwap(a, b);
cout << "a:" << a << " b:" << b << endl;
}
通過引用參數(shù)產(chǎn)生的效果同按地址傳遞是一樣的。引用的語法更清楚簡單:
???????1) 函數(shù)調(diào)用時傳遞的實參不必加“&”符
???????2) 在被調(diào)函數(shù)中不必在參數(shù)前加“ * ”符
???????引用作為其它變量的別名而存在,因此在一些場合可以代替指針。C++ 主張用引用傳遞取代地址傳遞的方式,因為引用語法容易且不易出錯。
//返回局部變量引用
int& TestFun01(){
int a = 10; //局部變量
return a;
}
//返回靜態(tài)變量引用
int& TestFunc02(){
static int a = 20;
cout << "static int a : " << a << endl;
return a;
}
int main(){
//不能返回局部變量的引用
int& ret01 = TestFun01();
//如果函數(shù)做左值,那么必須返回引用
TestFunc02();
TestFunc02() = 100;
TestFunc02();
return EXIT_SUCCESS;
}
- 不能返回局部變量的引用。
- 函數(shù)當(dāng)左值,必須返回引用。
3.10.3 引用的本質(zhì)
???????引用的本質(zhì)在c++內(nèi)部實現(xiàn)是一個指針常量.
Type& ref = val; // Type* const ref = &val;
???????c++ 編譯器在編譯過程中使用常指針作為引用的內(nèi)部實現(xiàn),因此引用所占用的空間大小與指針相同,只是這個過程是編譯器內(nèi)部實現(xiàn),用戶不可見。
//發(fā)現(xiàn)是引用,轉(zhuǎn)換為 int* const ref = &a;
void testFunc(int& ref){
ref = 100; // ref是引用,轉(zhuǎn)換為*ref = 100
}
int main(){
int a = 10;
int& aRef = a; //自動轉(zhuǎn)換為 int* const aRef = &a;這也能說明引用為什么必須初始化
aRef = 20; //內(nèi)部發(fā)現(xiàn)aRef是引用,自動幫我們轉(zhuǎn)換為: *aRef = 20;
cout << "a:" << a << endl;
cout << "aRef:" << aRef << endl;
testFunc(a);
return EXIT_SUCCESS;
}
3.10.4 指針引用
???????在c語言中如果想改變一個指針的指向而不是它所指向的內(nèi)容,函數(shù)聲明可能這樣:
void fun(int**);
???????給指針變量取一個別名。
Type* pointer = NULL;
Type*& = pointer;
???????Type pointer = NULL; Type& = pointer;**
struct Teacher{
int mAge;
};
//指針間接修改teacher的年齡
void AllocateAndInitByPointer(Teacher** teacher){
*teacher = (Teacher*)malloc(sizeof(Teacher));
(*teacher)->mAge = 200;
}
//引用修改teacher年齡
void AllocateAndInitByReference(Teacher*& teacher){
teacher->mAge = 300;
}
void test(){
//創(chuàng)建Teacher
Teacher* teacher = NULL;
//指針間接賦值
AllocateAndInitByPointer(&teacher);
cout << "AllocateAndInitByPointer:" << teacher->mAge << endl;
//引用賦值,將teacher本身傳到ChangeAgeByReference函數(shù)中
AllocateAndInitByReference(teacher);
cout << "AllocateAndInitByReference:" << teacher->mAge << endl;
free(teacher);
}
對于c++ 中的定義那個,語法清晰多了。函數(shù)參數(shù)變成指針的引用,用不著取得指針的地址。
3.10.5 常量引用
???????常量引用的定義格式:
const Type& ref = val;
???????常量引用注意:
- 字面量不能賦給引用,但是可以賦給const引用
- const修飾的引用,不能修改。
void test01(){
int a = 100;
const int& aRef = a; //此時aRef就是a
//aRef = 200; 不能通過aRef的值
a = 100; //OK
cout << "a:" << a << endl;
cout << "aRef:" << aRef << endl;
}
void test02(){
//不能把一個字面量賦給引用
//int& ref = 100;
//但是可以把一個字面量賦給常引用
const int& ref = 100; //int temp = 200; const int& ret = temp;
}
const引用使用場景:
???????常量引用主要用在函數(shù)的形參,尤其是類的拷貝/復(fù)制構(gòu)造函數(shù)。
???????將函數(shù)的形參定義為常量引用的好處:
引用不產(chǎn)生新的變量,減少形參與實參傳遞時的開銷。
-
由于引用可能導(dǎo)致實參隨形參改變而改變,將其定義為常量引用可以消除這種副作用。
如果希望實參隨著形參的改變而改變,那么使用一般的引用,如果不希望實參隨著形參改變,那么使用常引用。
//const int& param防止函數(shù)中意外修改數(shù)據(jù)
void ShowVal(const int& param){
cout << "param:" << param << endl;
}
3.11 練習(xí)作業(yè)
- 設(shè)計一個類,求圓的周長。
- 設(shè)計一個學(xué)生類,屬性有姓名和學(xué)號,可以給姓名和學(xué)號賦值,可以顯示學(xué)生的姓 名和學(xué)號
3.12 內(nèi)聯(lián)函數(shù)(inline function)
3.12.1 內(nèi)聯(lián)函數(shù)的引出
???????c++ 從c中繼承的一個重要特征就是效率。假如c++ 的效率明顯低于c的效率,那么就會有很大的一批程序員不去使用c++ 了。
???????在c中我們經(jīng)常把一些短并且執(zhí)行頻繁的計算寫成宏,而不是函數(shù),這樣做的理由是為了執(zhí)行效率,宏可以避免函數(shù)調(diào)用的開銷,這些都由預(yù)處理來完成。
???????但是在c++ 出現(xiàn)之后,使用預(yù)處理宏會出現(xiàn)兩個問題:
- 第一個在c中也會出現(xiàn),宏看起來像一個函數(shù)調(diào)用,但是會有隱藏一些難以發(fā)現(xiàn)的錯誤。
- 第二個問題是c++ 特有的,預(yù)處理器不允許訪問類的成員,也就是說預(yù)處理器宏不能用作類類的成員函數(shù)。
???????為了保持預(yù)處理宏的效率又增加安全性,而且還能像一般成員函數(shù)那樣可以在類里訪問自如,c++ 引入了內(nèi)聯(lián)函數(shù)(inline function).
???????內(nèi)聯(lián)函數(shù)為了繼承宏函數(shù)的效率,沒有函數(shù)調(diào)用時開銷,然后又可以像普通函數(shù)那樣,可以進行參數(shù),返回值類型的安全檢查,又可以作為成員函數(shù)。
3.12.2 預(yù)處理宏的缺陷
???????預(yù)處理器宏存在問題的關(guān)鍵是我們可能認為預(yù)處理器的行為和編譯器的行為是一樣的。當(dāng)然也是由于宏函數(shù)調(diào)用和函數(shù)調(diào)用在外表看起來是一樣的,因為也容易被混淆。但是其中也會有一些微妙的問題出現(xiàn):
問題一:
#define ADD(x,y) x+y
inline int Add(int x,int y){
return x + y;
}
void test(){
int ret1 = ADD(10, 20) * 10; //希望的結(jié)果是300
int ret2 = Add(10, 20) * 10; //希望結(jié)果也是300
cout << "ret1:" << ret1 << endl; //210
cout << "ret2:" << ret2 << endl; //300
}
問題二:
#define COMPARE(x,y) ((x) < (y) ? (x) : (y))
int Compare(int x,int y){
return x < y ? x : y;
}
void test02(){
int a = 1;
int b = 3;
//cout << "COMPARE(++a, b):" << COMPARE(++a, b) << endl; // 3
cout << "Compare(int x,int y):" << Compare(++a, b) << endl; //2
}
問題三:
???????預(yù)定義宏函數(shù)沒有作用域概念,無法作為一個類的成員函數(shù),也就是說預(yù)定義宏沒有辦法表示類的范圍。
3.12.3 內(nèi)聯(lián)函數(shù)
3.12.3.1 內(nèi)聯(lián)函數(shù)基本概念
???????在c++中,預(yù)定義宏的概念是用內(nèi)聯(lián)函數(shù)來實現(xiàn)的,而內(nèi)聯(lián)函數(shù)本身也是一個真正的函數(shù)。內(nèi)聯(lián)函數(shù)具有普通函數(shù)的所有行為。唯一不同之處在于內(nèi)聯(lián)函數(shù)會在適當(dāng)?shù)牡胤较耦A(yù)定義宏一樣展開,所以不需要函數(shù)調(diào)用的開銷。因此應(yīng)該不使用宏,使用內(nèi)聯(lián)函數(shù)。
- 在普通函數(shù)(非成員函數(shù))函數(shù)前面加上inline關(guān)鍵字使之成為內(nèi)聯(lián)函數(shù)。但是必須注意必須函數(shù)體和聲明結(jié)合在一起,否則編譯器將它作為普通函數(shù)來對待。
inline void func(int a);
???????以上寫法沒有任何效果,僅僅是聲明函數(shù),應(yīng)該如下方式來做:
inline int func(int a){return ++;}
???????注意: 編譯器將會檢查函數(shù)參數(shù)列表使用是否正確,并返回值(進行必要的轉(zhuǎn)換)。這些事預(yù)處理器無法完成的。
???????內(nèi)聯(lián)函數(shù)的確占用空間,但是內(nèi)聯(lián)函數(shù)相對于普通函數(shù)的優(yōu)勢只是省去了函數(shù)調(diào)用時候的壓棧,跳轉(zhuǎn),返回的開銷。我們可以理解為內(nèi)聯(lián)函數(shù)是以空間換時間。
3.12.3.2 類內(nèi)部的內(nèi)聯(lián)函數(shù)
???????為了定義內(nèi)聯(lián)函數(shù),通常必須在函數(shù)定義前面放一個inline關(guān)鍵字。但是在類內(nèi)部定義內(nèi)聯(lián)函數(shù)時并不是必須的。任何在類內(nèi)部定義的函數(shù)自動成為內(nèi)聯(lián)函數(shù)。
class Person{
public:
Person(){ cout << "構(gòu)造函數(shù)!" << endl; }
void PrintPerson(){ cout << "輸出Person!" << endl; }
}
???????構(gòu)造函數(shù)Person,成員函數(shù)PrintPerson在類的內(nèi)部定義,自動成為內(nèi)聯(lián)函數(shù)。
3.12.3.3 內(nèi)聯(lián)函數(shù)和編譯器
???????內(nèi)聯(lián)函數(shù)并不是何時何地都有效,為了理解內(nèi)聯(lián)函數(shù)何時有效,應(yīng)該要知道編譯器碰到內(nèi)聯(lián)函數(shù)會怎么處理?
???????對于任何類型的函數(shù),編譯器會將函數(shù)類型(包括函數(shù)名字,參數(shù)類型,返回值類型)放入到符號表中。同樣,當(dāng)編譯器看到內(nèi)聯(lián)函數(shù),并且對內(nèi)聯(lián)函數(shù)體進行分析沒有發(fā)現(xiàn)錯誤時,也會將內(nèi)聯(lián)函數(shù)放入符號表。
當(dāng)調(diào)用一個內(nèi)聯(lián)函數(shù)的時候,編譯器首先確保傳入?yún)?shù)類型是正確匹配的,或者如果類型不正完全匹配,但是可以將其轉(zhuǎn)換為正確類型,并且返回值在目標(biāo)表達式里匹配正確類型,或者可以轉(zhuǎn)換為目標(biāo)類型,內(nèi)聯(lián)函數(shù)就會直接替換函數(shù)調(diào)用,這就消除了函數(shù)調(diào)用的開銷。假如內(nèi)聯(lián)函數(shù)是成員函數(shù),對象this指針也會被放入合適位置。
???????類型檢查和類型轉(zhuǎn)換、包括在合適位置放入對象this指針這些都是預(yù)處理器不能完成的。
???????但是c++ 內(nèi)聯(lián)編譯會有一些限制,以下情況編譯器可能考慮不會將函數(shù)進行內(nèi)聯(lián)編譯:
- 不能存在任何形式的循環(huán)語句
- 不能存在過多的條件判斷語句
- 函數(shù)體不能過于龐大
- 不能對函數(shù)進行取址操作
內(nèi)聯(lián)僅僅只是給編譯器一個建議,編譯器不一定會接受這種建議,如果你沒有將函數(shù)聲明為內(nèi)聯(lián)函數(shù),那么編譯器也可能將此函數(shù)做內(nèi)聯(lián)編譯。一個好的編譯器將會內(nèi)聯(lián)小的、簡單的函數(shù)。
3.13 函數(shù)的默認參數(shù)
???????c++ 在聲明函數(shù)原型的時可為一個或者多個參數(shù)指定默認(缺省)的參數(shù)值,當(dāng)函數(shù)調(diào)用的時候如果沒有指定這個值,編譯器會自動用默認值代替。
void TestFunc01(int a = 10, int b = 20){
cout << "a + b = " << a + b << endl;
}
//注意點:
//1. 形參b設(shè)置默認參數(shù)值,那么后面位置的形參c也需要設(shè)置默認參數(shù)
void TestFunc02(int a,int b = 10,int c = 10){}
//2. 如果函數(shù)聲明和函數(shù)定義分開,函數(shù)聲明設(shè)置了默認參數(shù),函數(shù)定義不能再設(shè)置默認參數(shù)
void TestFunc03(int a = 0,int b = 0);
void TestFunc03(int a, int b){}
int main(){
//1.如果沒有傳參數(shù),那么使用默認參數(shù)
TestFunc01();
//2. 如果傳一個參數(shù),那么第二個參數(shù)使用默認參數(shù)
TestFunc01(100);
//3. 如果傳入兩個參數(shù),那么兩個參數(shù)都使用我們傳入的參數(shù)
TestFunc01(100, 200);
return EXIT_SUCCESS;
}
注意點:
- 函數(shù)的默認參數(shù)從左向右,如果一個參數(shù)設(shè)置了默認參數(shù),那么這個參數(shù)之后的參數(shù)都必須設(shè)置默認參數(shù)。
- 如果函數(shù)聲明和函數(shù)定義分開寫,函數(shù)聲明和函數(shù)定義不能同時設(shè)置默認參數(shù)。
3.14 函數(shù)的占位參數(shù)
???????c++ 在聲明函數(shù)時,可以設(shè)置占位參數(shù)。占位參數(shù)只有參數(shù)類型聲明,而沒有參數(shù)名聲明。一般情況下,在函數(shù)體內(nèi)部無法使用占位參數(shù)。
void TestFunc01(int a,int b,int){
//函數(shù)內(nèi)部無法使用占位參數(shù)
cout << "a + b = " << a + b << endl;
}
//占位參數(shù)也可以設(shè)置默認值
void TestFunc02(int a, int b, int = 20){
//函數(shù)內(nèi)部依舊無法使用占位參數(shù)
cout << "a + b = " << a + b << endl;
}
int main(){
//錯誤調(diào)用,占位參數(shù)也是參數(shù),必須傳參數(shù)
//TestFunc01(10,20);
//正確調(diào)用
TestFunc01(10,20,30);
//正確調(diào)用
TestFunc02(10,20);
//正確調(diào)用
TestFunc02(10, 20, 30);
return EXIT_SUCCESS;
}
什么時候用,在后面的操作符重載的后置++要用到這個.
3.15 函數(shù)重載(overload)
3.15.1 函數(shù)重載概述
能使名字方便使用,是任何程序設(shè)計語言的一個重要特征!
???????我們現(xiàn)實生活中經(jīng)常會碰到一些字在不同的場景下具有不同的意思,比如漢語中的多音字“重”。
???????當(dāng)我們說: “他好重啊,我都背不動!”我們根據(jù)上下文意思,知道“重”在此時此地表示重量的意思。
???????如果我們說“你怎么寫了那么多重復(fù)的代碼? 維護性太差了!”這個地方我們知道,“重”表示重復(fù)的意思。
???????同樣一個字在不同的場景下具有不同的含義。那么在c++ 中也有一種類似的現(xiàn)象出現(xiàn),同一個函數(shù)名在不同場景下可以具有不同的含義。
???????在傳統(tǒng)c語言中,函數(shù)名必須是唯一的,程序中不允許出現(xiàn)同名的函數(shù)。在c++ 中是允許出現(xiàn)同名的函數(shù),這種現(xiàn)象稱為函數(shù)重載。
???????函數(shù)重載的目的就是為了方便的使用函數(shù)名。
???????函數(shù)重載并不復(fù)雜,等大家學(xué)完就會明白什么時候需要用到他們,以及是如何編譯,鏈接的。
3.15.2 函數(shù)重載
3.15.2.1 函數(shù)重載基本語法
實現(xiàn)函數(shù)重載的條件:
- 同一個作用域
- 參數(shù)個數(shù)不同
- 參數(shù)類型不同
- 參數(shù)順序不同
//1. 函數(shù)重載條件
namespace A{
void MyFunc(){ cout << "無參數(shù)!" << endl; }
void MyFunc(int a){ cout << "a: " << a << endl; }
void MyFunc(string b){ cout << "b: " << b << endl; }
void MyFunc(int a, string b){ cout << "a: " << a << " b:" << b << endl;}
void MyFunc(string b, int a){cout << "a: " << a << " b:" << b << endl;}
}
//2.返回值不作為函數(shù)重載依據(jù)
namespace B{
void MyFunc(string b, int a){}
//int MyFunc(string b, int a){} //無法重載僅按返回值區(qū)分的函數(shù)
}
注意: 函數(shù)重載和默認參數(shù)一起使用,需要額外注意二義性問題的產(chǎn)生。
void MyFunc(string b){
cout << "b: " << b << endl;
}
//函數(shù)重載碰上默認參數(shù)
void MyFunc(string b, int a = 10){
cout << "a: " << a << " b:" << b << endl;
}
int main(){
MyFunc("hello"); //這時,兩個函數(shù)都能匹配調(diào)用,產(chǎn)生二義性
return 0;
}
思考:為什么函數(shù)返回值不作為重載條件呢?
??????? 當(dāng)編譯器能從上下文中確定唯一的函數(shù)的時,如int ret = func(),這個當(dāng)然是沒有問題的。然而,我們在編寫程序過程中可以忽略他的返回值。那么這個時候,一個函數(shù)為void func(int x);另一個為int func(int x); 當(dāng)我們直接調(diào)用func(10),這個時候編譯器就不確定調(diào)用那個函數(shù)。所以在c++ 中禁止使用返回值作為重載的條件。
3.15.2.2 函數(shù)重載實現(xiàn)原理
???????編譯器為了實現(xiàn)函數(shù)重載,也是默認為我們做了一些幕后的工作,編譯器用不同的參數(shù)類型來修飾不同的函數(shù)名,比如void func(); 編譯器可能會將函數(shù)名修飾成_func,當(dāng)編譯器碰到void func(int x),編譯器可能將函數(shù)名修飾為_func_int,當(dāng)編譯器碰到void func(int x,char c),編譯器可能會將函數(shù)名修飾為_func_int_char我這里使用”可能”這個字眼是因為編譯器如何修飾重載的函數(shù)名稱并沒有一個統(tǒng)一的標(biāo)準(zhǔn),所以不同的編譯器可能會產(chǎn)生不同的內(nèi)部名。
void func(){}
void func(int x){}
void func(int x,char y){}
以上三個函數(shù)在linux下生成的編譯之后的函數(shù)名為:
_Z4funcv //v 代表void,無參數(shù)
_Z4funci //i 代表參數(shù)為int類型
_Z4funcic //i 代表第一個參數(shù)為int類型,第二個參數(shù)為char類型
3.15.3 extern “C”淺析
以下在Linux下測試:
c函數(shù): void MyFunc(){} ,被編譯成函數(shù): MyFunc
c++函數(shù): void MyFunc(){},被編譯成函數(shù): _Z6Myfuncv
???????通過這個測試,由于c++ 中需要支持函數(shù)重載,所以c和c++ 中對同一個函數(shù)經(jīng)過編譯后生成的函數(shù)名是不相同的,這就導(dǎo)致了一個問題,如果在c++ 中調(diào)用一個使用c語言編寫模塊中的某個函數(shù),那么c++ 是根據(jù)c++ 的名稱修飾方式來查找并鏈接這個函數(shù),那么就會發(fā)生鏈接錯誤,以上例,c++ 中調(diào)用MyFunc函數(shù),在鏈接階段會去找Z6Myfuncv,結(jié)果是沒有找到的,因為這個MyFunc函數(shù)是c語言編寫的,生成的符號是MyFunc。
那么如果我想在c++ 調(diào)用c的函數(shù)怎么辦?
extern "C"的主要作用就是為了實現(xiàn)c++ 代碼能夠調(diào)用其他c語言代碼。加上extern "C"后,這部分代碼編譯器按c語言的方式進行編譯和鏈接,而不是按c++ 的方式。
MyModule.h
#ifndef MYMODULE_H
#define MYMODULE_H
#include<stdio.h>
#if __cplusplus
extern "C"{
#endif
void func1();
int func2(int a,int b);
#if __cplusplus
}
#endif
#endif
MyModule.c
#include"MyModule.h"
void func1(){
printf("hello world!");
}
int func2(int a, int b){
return a + b;
}
TestExternC.cpp
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#if 0
#ifdef __cplusplus
extern "C" {
#if 0
void func1();
int func2(int a, int b);
#else
#include"MyModule.h"
#endif
}
#endif
#else
extern "C" void func1();
extern "C" int func2(int a, int b);
#endif
int main(){
func1();
cout << func2(10, 20) << endl;
return EXIT_SUCCESS;
}