天天看點

初識C++ 函數重載以及背後的原理

寫在前面

先說說我的狀态吧,五一假期五天假,這些天都在玩,很少學習,我不是後悔,也沒必要,本來假期就是為了讓自己放松.我唯一要反思看到别人在學,我心裡也想學但是卻做不到,這是我的缺點,後面我會克服的.盡量快點和大家分享知識.今天和大家分享的是C++關于函數重載的關系,我們不僅僅需要學會重載的使用,更要了解C++為什麼支援函數重載.

函數重載

我們可能對函數很是熟悉,但是重載又是什麼意思呢?我們先來用一個具體的場景來分享.

一天,張三的老闆要你寫一個兩位數相加的函數,張三心想這不很簡單嗎?手指一動,結果就出來了,挺簡單的嘛.

int add(int x, int y)
{
  return x + y;
}      

現在老闆看張三的代碼立馬火了,你是怎麼想的,要是我想12,10.9相加呢?你這個就隻能兩個整型相加,回去修改!!!張三聽到老闆的話不由得反駁道:這怎麼改,總不能再寫一個add1,add2…吧.老闆聽到了張三的嘟囔,生氣道,你沒有學過函數重載嗎?看看下面的代碼,回去好好學習學習,基礎都不紮實.

張三看到代碼,不由大吃一驚,C++還可以這麼寫?好神奇啊,我要好好看看書.

int add(int x, int y)
{
  return x + y;
}

double add(double x, int y)
{
  return x + y;
}

double add(int x, double y)
{
  return x + y;
}      

我們可不希望張三這種事發生在我們身上,先來看看函數重載的定義

函數重載:是函數的一種特殊情況,C++允許在同一作用域中聲明幾個功能類似的同名函數,這些同名函數的

形參清單(參數個數 或 類型 或 順序)必須不同,常用來處理實作功能類似資料類型不同的問題 .

可能大家不喜歡看定義,我這裡給一個總結.

函數重載要滿足下面的要求.

  • 函數名相同
  • 參數的類型,個數,順序有一個不同就可以了
  • 傳回類型不做要求

函數重載的原理

一般情況下,我們知道了函數重載到會應用就可以了,但是對于我們來說需要我們看看他們的原理,為什麼C語言不支援重載,C++支援重載?這些都是問題.

為何C++可以支援重載

我們先用C++的編譯器簡單的看看如何執行程式,下面是我在Linux環境下使用g++來完成的,大家要是不太懂,可以先不管,直接了解C++的原理.

我們先來看看現象,發現C++可以精準的找到需要比對的函數,這是我們所疑惑的.

// test.h
#pragma once 
#include <iostream>
#include <stdio.h>

using std::cout;
void func(int a, double b);
void func(double a, int b);
//test.cpp

#include "test.h"
//寫兩個函數   函數形成重載
void func(int a, double b)
{
  printf("%d %lf", a, b);
}

void func(double a, int b)
{
  printf("%lf %d", a, b);
}


//Mian.cpp

#include "test.h"
int main()
{
  func(10, 2.20);
  return 0;
}      
初識C++ 函數重載以及背後的原理
初識C++ 函數重載以及背後的原理

程式的編譯連結

關于這一點,我們先簡單的說說,之前我們詳細的談過.一個檔案變成一個可執行程式需要經過下面4個步驟.

  1. 預處理 宏替換 頭檔案展開 注釋替換 main.cpp -> main.i test.cpp -> test.i
  2. 編譯 檢查文法 ,代碼變換成彙編語言 main.i -> main.s test.i -> test.s
  3. 彙編 彙編語言變成二進制語言,各個檔案變成目标檔案 main.s -> main.o test.s -> test.o
  4. 連結 多個目标檔案+連結庫發生連結
這裡我們需要重點談談連結,這是我們今天最重要的一部分

連結就僅僅隻是目标檔案的合并嗎?不是的,它要完成的任務很多,其中最重要的就是找到函數的位址,連結對應上,合并到一起

當我們進行過頭檔案的展開後,Main.cpp中有func函數的聲明和調用.在編譯和彙編的過程中存在一個符号表,這個符号表記錄了函數的定義以及相應的映射.這是很重要的.符号表裡面包含了函數名和函數的位址.

每一個目标檔案(.o)都包含一個符号表和一系列指令,我們看看入和完成函數連結.

初識C++ 函數重載以及背後的原理

現在到mian.o的指令這裡了,前面的一些列指令都正常經行,直到它遇到了func這個點,要是看過C語言的彙編語言的朋友們可能對下面的比較熟悉.

到了func這裡,編譯器開始call (func: ?),編譯器不知道func的位址,但是前面頭檔案的的展開中func函數已經聲明了,是以編譯器知道了func是一個函數.就先給它一個無效的位址.當程式進行連結時,編譯器一看它是一個無效位址,會拿函數名和其他的.o檔案裡面的符号表去碰,碰到了就填上,找不到就會報連接配接錯誤.

C語言為何不支援重載

到這裡就可以明白了,當我們拿函數名去碰的時候,符号表裡面存在多個相同的函數名,編譯器就不會識别該用哪個.更何況存在相同函數名的.c檔案有時都不可能編譯過.

gcc對函數名都不會做任何處理,這也是C語言不支援函數重載的原因.

初識C++ 函數重載以及背後的原理

C++為何可以支援函數重載

到這裡我們就可以得到了結果,既然在連結的時候無效的函數會拿函數名去其他的符号表裡面去碰,那麼隻要我們看看重載的函數名像不像同就可以了,大家可能會有些疑惑,重載的函數名不是相同的嗎?是的,但是C++編譯器會做一定的處理.這裡每個編譯器都有自己的函數名修飾規則 這就是C++ 支援重載的原理.

這就是C++可以支援重載的原因,g++的函數修飾後變成【_Z+函數名長度+函數名+類型首字母1+類型首字母2…】,也是我們隻對參數清單做了要求,對傳回值不做要求的原因.

初識C++ 函數重載以及背後的原理

C++和C語言互相調用

我們都知道C++支援C語言的大部分文法,C++和C語言可以互相調用嗎?實際上是可以的,在一個大型程式中,有的部門可能使用的是C寫的的函數,有的部門可能用的C++,要是他們不能互相使用那就打臉了.

建立靜态庫

我們可以把自己寫的代碼編譯成一個靜态庫或者動态庫,這裡我以靜态庫舉例,看看如何在VS中中建立一個靜态庫.

初識C++ 函數重載以及背後的原理

C++調用C

我們已經有了一個C語言的靜态庫,現在有一個C++的項目需要使用這個靜态庫,我們該如何使用呢?需要分為下面幾個步驟

下面這兩張圖檔都是修改環境的設定,我使用的是VS2013,其他的大概應該差不多,大家依次來修改就可以了.

初識C++ 函數重載以及背後的原理
初識C++ 函數重載以及背後的原理

到這裡我們就可以調用C語言的靜态庫了,讓我們來看看結果吧.

#include "../../Heap/Heap/heap.h"  //相對路徑

int main()
{
  MyHeap myHeap;
  InitMyHeap(&myHeap);
  HeapPush(&myHeap, 1);
  HeapPush(&myHeap, 2);
  HeapPush(&myHeap, 3);
  Display(&myHeap);
  return 0;
}      
初識C++ 函數重載以及背後的原理
這為什麼報錯?我們不是已經設定好了靜态庫了嗎?實際上這種錯誤是很容易分析出來的,當C++去調用C語言的函數時,C++會自動修改函數名,當時C語言不會啊,是以他們就不會碰到一起,連結就會出錯.

extern “C”

既然編譯器不能自動識别C語言的函數名,我們告訴編譯器一下不就可以了嗎.extern “C” 就是這種作用.

有時候在C++工程中可能需要将某些函數按照 C 的風格來編譯,在函數前加 extern “C” ,意思是告訴編譯器,

将該函數按照 C 語言規則來編譯。比如:tcmalloc是google用C++實作的一個項目,他提供tcmallc()和tcfree

兩個接口來使用,但如果是C項目就沒辦法使用,那麼他就使用extern “C”來解決

extern "C"   // 告知這是C語言的函數聲明
{
  #include "../../Heap/Heap/heap.h"
}

int main()
{
  MyHeap myHeap;
  InitMyHeap(&myHeap);
  HeapPush(&myHeap, 1);
  HeapPush(&myHeap, 2);
  HeapPush(&myHeap, 3);
  Display(&myHeap);
  return 0;
}      
初識C++ 函數重載以及背後的原理

extern “C” 原理

我們需要來看看extern “C” 的原理,使用了extern “C” 後,在C++在進行編譯的時候函數名字就依據C語言的方法來修改了,不在變成C++ 的規則.extern "C"可以單獨修飾函數,也可以修飾一系列函數,使用代碼塊.

// test.h
#pragma once 
#include <iostream>
#include <stdio.h>

extern "C" void func(int a, double b);

//test.cpp

#include "test.h"
//寫兩個函數   函數形成重載
void func(int a, double b)
{
  printf("%d %lf", a, b);
}

//Mian.cpp

#include "test.h"
int main()
{
  func(10, 2.20);
  return 0;
}      
初識C++ 函數重載以及背後的原理

C語言調用C++

那麼C語言可以調用C++ 的嗎?可以了,不過也需要一些段來完成.如何讓C語言去識别C++的規則呢?這是我們需要考慮的.

我們已經把庫改成的了C++的靜态庫了.

#include "../../Heap/Heap/heap.h"


int main()
{
  MyHeap myHeap;
  InitMyHeap(&myHeap);
  HeapPush(&myHeap, 1);
  HeapPush(&myHeap, 2);
  HeapPush(&myHeap, 3);
  Display(&myHeap);
  return 0;
}      
初識C++ 函數重載以及背後的原理

我們無法讓C語言的編譯器去識别C++ 的函數的命名,那麼我們是不是可以在函數一編譯的時候就完成函數名依照C語言來說.這就很簡單了.

初識C++ 函數重載以及背後的原理

但是即使是這樣,C語言仍舊會報錯,原因在于在頭檔案展開的時候,C語言根本不識别extern “C”,是以我們就需要條件編譯了.

使用條件編譯來修改的靜态庫的方法如下,需要再次編譯.

//方法一
#ifdef __cplusplus    // C++獨有的
  #define EXTERNC extern "C"
#else 
  #define EXTERNC
#endif


EXTERNC extern void InitMyHeap(MyHeap * pHeap);

EXTERNC extern void HeapPush(MyHeap* pHeap, HPDataType x);
EXTERNC extern bool IsFull(MyHeap* pHeap);
EXTERNC extern bool IsEmpty(MyHeap* pHeap);
EXTERNC extern int HeapSize(MyHeap* pHeap);
EXTERNC extern void adjustDown(MyHeap* pHeap);
EXTERNC extern void adjustUp(MyHeap* pHeap);
EXTERNC extern void Display(MyHeap* pHeap);
EXTERNC extern HPDataType HeapTop(MyHeap* pHeap);
EXTERNC extern void HeapPop(MyHeap* pHeap);

//方法 二
#ifdef __cplusplus
extern "C"
{
#endif

  extern void InitMyHeap(MyHeap * pHeap);

  extern void HeapPush(MyHeap* pHeap, HPDataType x);
  extern bool IsFull(MyHeap* pHeap);
  extern bool IsEmpty(MyHeap* pHeap);
  extern int HeapSize(MyHeap* pHeap);
  extern void adjustDown(MyHeap* pHeap);
  extern void adjustUp(MyHeap* pHeap);
  extern void Display(MyHeap* pHeap);
  extern HPDataType HeapTop(MyHeap* pHeap);
  extern void HeapPop(MyHeap* pHeap);
#ifdef __cplusplus
}
#endif      

這樣就解決了.

初識C++ 函數重載以及背後的原理
注意,這裡有一點需要注意的,當我們C語言調用C++靜态庫的時候,最起碼我們實際需要的的那部分代碼在extern "C"修飾的函數中不能發生重載.

C++ 注意事項

這個注意事項主要是依據extern "C"來談的,有些比較偏僻的内容需要關注下.

## extern "C"修飾的函數和一個函數完全一樣

在extern "C"修飾的函數子產品外面存在了一個完全一摸一樣的的函數,這個編譯器不會給通過的.

#ifdef __cplusplus
extern "C"
{
#endif 

  void func(int a, int b)
  {
    printf("C : %d %d\n", a, b);
  }

#ifdef __cplusplus
}
#endif 
//完全一樣
void func(int a, int b)
{
  printf("C : %d %d\n", a, b);
}      
初識C++ 函數重載以及背後的原理

extern "C"修飾的函數和一個函數構成重載

在extern "C"修飾的函數子產品外面一個函數構成重載這種編譯器可以通過的,但是extern "C"修飾的命名方法仍舊還是按照C語言的方式,構成重載的是C++的方式.

#include <iostream>
using namespace std;

#ifdef __cplusplus
extern "C"
{
#endif 

  void func(int a, int b)
  {
    printf("C : %d %d\n", a, b);
  }

#ifdef __cplusplus
}
#endif 

void func(double a, int b)
{
  printf("C++: %lf %d\n", a, b);
}

int main()
{
  func(1, 2);
  func(1.11, 2);
  return 0;
}      
初識C++ 函數重載以及背後的原理

繼續閱讀