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