函數指針及extern "c"

C/C 中函數指針的含義

分類:學習

函數存放在內存的代碼區域內,它們同樣有地址,我們如何能獲得函數的地址呢?

  如果我們有一個int test(int a)的函數,那麼,它的地址就是函數的名字,這一點如同數組一樣,數組的名字就是數組的起始地址。

 定義一個指向函數的指針用如下的形式,以上面的test()爲例:

int (*fp)(int a);//這裏就定義了一個指向函數的指針

  函數指針不能絕對不能指向不同類型,或者是帶不同形參的函數,在定義函數指針的時候我們很容易犯如下的錯誤。

int *fp(int a);//這裏是錯誤的,因爲按照結合性和優先級來看就是先和()結合,然後變成了一個返回整形指針的函數了,而不是函數指針,這一點尤其需要注意!

  下面我們來看一個具體的例子:

#include <iostream
#include <string
using namespace std; 
 
int test(int a); 
 
void main(int argc,charargv[])   

    cout<<test<<endl;//顯示函數地址 
    int (*fp)(int a); 
    fp=test;//將函數test的地址賦給函數學指針fp 
    cout<<fp(5)<<"|"<<(*fp)(10)<<endl; 
//上面的輸出fp(5),這是標準c++的寫法,(*fp)(10)這是兼容c語言的標準寫法,兩種同意,但注意區分,避免寫的程序產生移植性問題! 
    cin.get(); 

 
int test(int a) 

    return a; 
}

  typedef定義可以簡化函數指針的定義,在定義一個的時候感覺不出來,但定義多了就知道方便了,上面的代碼改寫成如下的形式:

#include <iostream
#include <string
using namespace std; 
 
int test(int a); 
 
void main(int argc,charargv[])   

    cout<<test<<endl; 
    typedef int (*fp)(int a);//注意,這裏不是生命函數指針,而是定義一個函數指針的類型,這個類型是自己定義的,類型名爲fp 
    fp fpi;//這裏利用自己定義的類型名fp定義了一個fpi的函數指針! 
    fpi=test; 
    cout<<fpi(5)<<"|"<<(*fpi)(10)<<endl; 
    cin.get(); 

 
int test(int a) 

    return a; 
}



函數指針同樣是可以作爲參數傳遞給函數的,下面我們看個例子,仔細閱讀你將會發現它的用處,稍加推理可以很方便我們進行一些複雜的編程工作。

//-------------------該例以上一個例子作爲基礎稍加了修改----------------------------- 
#include <iostream>   
#include <string>   
using namespace std;   
   
int test(int);   
 
int test2(int (*ra)(int),int); 
 
void main(int argc,charargv[])     
{   
    cout<<test<<endl; 
    typedef int (*fp)(int);   
    fp fpi; 
    fpi=test;//fpi賦予test 函數的內存地址 
 
    cout<<test2(fpi,1)<<endl;//這裏調用test2函數的時候,這裏把fpi所存儲的函數地址(test的函數地址)傳遞了給test2的第一個形參 
    cin.get(); 
}   
   
int test(int a) 
{   
    return a-1; 

 
int test2(int (*ra)(int),int b)//這裏定義了一個名字爲ra的函數指針 

    int c=ra(10)+b;//在調用之後,ra已經指向fpi所指向的函數地址即test函數 
    return c; 
}

  利用函數指針,我們可以構成指針數組,更明確點的說法是構成指向函數的指針數組,這麼說可能就容易理解的多了。

#include <iostream>   
#include <string>   
using namespace std; 
 
void t1(){cout<<"test1";} 
void t2(){cout<<"test2";} 
void t3(){cout<<"test3";} 
void main(int argc,charargv[])     

    void* a[]={t1,t2,t3}; 
    cout<<"比較t1()的內存地址和數組a[0]所存儲的地址是否一致"<<t1<<"|"<<a[0]<<endl; 
 
    cout<<a[0]();//錯誤!指針數組是不能利用數組下標操作調用函數的 
 
    typedef void (*fp)();//自定義一個函數指針類型 
    fp b[]={t1,t2,t3}; //利用自定義類型fp把b[]定義趁一個指向函數的指針數組 
    b[0]();//現在利用指向函數的指針數組進行下標操作就可以進行函數的間接調用了; 
    cin.get(); 
}

  仔細看上面的例子可能不用我多說大家也會知道是怎麼一會事情了,最後我們做一個重點小結,只要記住這一點,對於理解利用函數指針構成數組進行函數間接調用就很容易了!

void* a[]={t1,t2,t3};
cout<<"比較t1()的內存地址和數組a[0]所存儲的地址是否一致"<<t1<<"|"<<a[0]<<endl;

cout<<a[0]();//錯誤!指針數組是不能利用數組下標操作調用函數的

  上面的這一小段中的錯誤行,爲什麼不能這麼調用呢?

  前一篇教程我們已經說的很清楚了,不過在這裏我們還是複習一下概念,指針數組元素所保存的只是一個內存地址,既然只是個內存地址就不可能進行a[0] ()這樣地址帶括號的操作,而函數指針不同它是一個例外,函數指針只所以這麼叫它就是因爲它是指向函數指向內存的代碼區的指針,它被系統授予允許與()括號操作的權利,進行間接的函數調用,既然函數指針允許這麼操作,那麼被定義成函數指針的數組就一定是可以一樣的操作的。

 

[軟件園地]typedef struct 用法詳解

分類:學習

1. 基本解釋

  typedef爲C語言的關鍵字,作用是爲一種數據類型定義一個新名字。這裏的數據類型包括內部數據類型(int,char等)和自定義的數據類型(struct等)。

  在編程中使用typedef目的一般有兩個,一個是給變量一個易記且意義明確的新名字,另一個是簡化一些比較複雜的類型聲明。

  至於typedef有什麼微妙之處,請你接着看下面對幾個問題的具體闡述。


 


  2. typedef & 結構的問題

  當用下面的代碼定義一個結構時,編譯器報了一個錯誤,爲什麼呢?莫非C語言不允許在結構中包含指向它自己的指針嗎?請你先猜想一下,然後看下文說明:

typedef struct tagNode
{
 char *pItem;
 pNode pNext;
} *pNode; 

  答案與分析:

  1、typedef的最簡單使用

typedef long byte_4;

  給已知數據類型long起個新名字,叫byte_4。

  2、 typedef與結構結合使用

typedef struct tagMyStruct
{
 int iNum;
 long lLength;
} MyStruct;

  這語句實際上完成兩個操作:

  1) 定義一個新的結構類型

struct tagMyStruct
{
 int iNum;
 long lLength;
};

  分析:tagMyStruct稱爲“tag”,即“標籤”,實際上是一個臨時名字,struct 關鍵字和tagMyStruct一起,構成了這個結構類型,不論是否有typedef,這個結構都存在。

  我們可以用struct tagMyStruct varName來定義變量,但要注意,使用tagMyStruct varName來定義變量是不對的,因爲struct 和tagMyStruct合在一起才能表示一個結構類型。

  2) typedef爲這個新的結構起了一個名字,叫MyStruct

typedef struct tagMyStruct MyStruct;

  因此,MyStruct實際上相當於struct tagMyStruct,我們可以使用MyStruct varName來定義變量。

  答案與分析

  C語言當然允許在結構中包含指向它自己的指針,我們可以在建立鏈表等數據結構的實現上看到無數這樣的例子,上述代碼的根本問題在於typedef的應用。

  根據我們上面的闡述可以知道:新結構建立的過程中遇到了pNext域的聲明,類型是pNode,要知道pNode表示的是類型的新名字,那麼在類型本身還沒有建立完成的時候,這個類型的新名字也還不存在,也就是說這個時候編譯器根本不認識pNode。

  解決這個問題的方法有多種:

  1)、

typedef struct tagNode
{
 char *pItem;
 struct tagNode *pNext;
} *pNode;

  2)、

typedef struct tagNode *pNode;
struct tagNode
{
 char *pItem;
 pNode pNext;
};

  注意:在這個例子中,你用typedef給一個還未完全聲明的類型起新名字。C語言編譯器支持這種做法。

  3)、規範做法:

typedef uint32 (* ADM_READDATA_PFUNC)(  uint16*,  uint32  );

這個以前沒有看到過,個人認爲是宇定義一個uint32的指針函數,uint16*,  uint32  爲函數裏的兩個參數; 應該相當於#define  uint32 (* ADM_READDATA_PFUNC)(  uint16*,  uint32  ); 

struct在代碼中常見兩種形式:
struct A
{
//...
};

struct
{
//...
} A;
這其實是兩個完全不同的用法:
前者叫做“結構體類型定義”,意思是:定義{}中的結構爲一個名稱是“A”的結構體。
這種用法在typedef中一般是:
typedef struct tagA //故意給一個不同的名字,作爲結構體的實名
{
//...
} A; //結構體的別名。

後者是結構體變量定義,意思是:以{}中的結構,定義一個名稱爲"A"的變量。這裏的結構體稱爲匿名結構體,是無法被直接引用的。
也可以通過typedef爲匿名結構體創建一個別名,從而使得它可以被引用:
typedef struct
{
//...
} A; //定義匿名結構體的別名爲A

C 中extern “C”含義深層探索

分類:學習

1.引言

  C++語言的創建初衷是“a better C”,但是這並不意味着C++中類似C語言的全局變量和函數所採用的編譯和連接方式與C語言完全相同。作爲一種欲與C兼容的語言,C++保留了一部分過程式語言的特點(被世人稱爲“不徹底地面向對象”),因而它可以定義不屬於任何類的全局變量和函數。但是,C++畢竟是一種面向對象的程序設計語言,爲了支持函數的重載,C++對全局函數的處理方式與C有明顯的不同。

2.從標準頭文件說起
某企業曾經給出如下的一道面試題:爲什麼標準頭文件都有類似以下的結構?

#ifndef __INCvxWorksh
#define __INCvxWorksh
#ifdef __cplusplus
extern "C" {
#endif
/*...*/
#ifdef __cplusplus
}
#endif
#endif /* __INCvxWorksh */

顯然,頭文件中的編譯宏“#ifndef __INCvxWorksh、#define __INCvxWorksh、#endif” 的作用是防止該頭文件被重複引用。那麼


#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif

的作用又是什麼呢?我們將在下文一一道來。

3.深層揭密extern "C"

  extern "C" 包含雙重含義,從字面上即可得到:首先,被它修飾的目標是“extern”的;其次,被它修飾的目標是“C”的。讓我們來詳細解讀這兩重含義。

  被extern "C"限定的函數或變量是extern類型的;

  extern是C/C++語言中表明函數和全局變量作用範圍(可見性)的關鍵字,該關鍵字告訴編譯器,其聲明的函數和變量可以在本模塊或其它模塊中使用。記住,下列語句:

  extern int a;


  僅僅是一個變量的聲明,其並不是在定義變量a,並未爲a分配內存空間。變量a在所有模塊中作爲一種全局變量只能被定義一次,否則會出現連接錯誤。

  通常,在模塊的頭文件中對本模塊提供給其它模塊引用的函數和全局變量以關鍵字extern聲明。例如,如果模塊B欲引用該模塊A中定義的全局變量和函數時只需包含模塊A的頭文件即可。這樣,模塊B中調用模塊A中的函數時,在編譯階段,模塊B雖然找不到該函數,但是並不會報錯;它會在連接階段中從模塊A編譯生成的目標代碼中找到此函數。

  與extern對應的關鍵字是static,被它修飾的全局變量和函數只能在本模塊中使用。因此,一個函數或變量只可能被本模塊使用時,其不可能被extern “C”修飾。

  被extern "C"修飾的變量和函數是按照C語言方式編譯和連接的;

  未加extern “C”聲明時的編譯方式

  首先看看C++中對類似C的函數是怎樣編譯的。

  作爲一種面向對象的語言,C++支持函數重載,而過程式語言C則不支持。函數被C++編譯後在符號庫中的名字與C語言的不同。例如,假設某個函數的原型爲:

void foo( int x, int y );


  該函數被C編譯器編譯後在符號庫中的名字爲_foo,而C++編譯器則會產生像_foo_int_int之類的名字(不同的編譯器可能生成的名字不同,但是都採用了相同的機制,生成的新名字稱爲“mangled name”)。

  _foo_int_int這樣的名字包含了函數名、函數參數數量及類型信息,C++就是靠這種機制來實現函數重載的。例如,在C++中,函數void foo( int x, int y )與void foo( int x, float y )編譯生成的符號是不相同的,後者爲_foo_int_float。

同樣地,C++中的變量除支持局部變量外,還支持類成員變量和全局變量。用戶所編寫程序的類成員變量可能與全局變量同名,我們以"."來區分。而本質上,編譯器在進行編譯時,與函數的處理相似,也爲類中的變量取了一個獨一無二的名字,這個名字與用戶程序中同名的全局變量名字不同。

  未加extern "C"聲明時的連接方式

  假設在C++中,模塊A的頭文件如下:

// 模塊A頭文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo( int x, int y );
#endif


  在模塊B中引用該函數:

// 模塊B實現文件 moduleB.cpp
#include "moduleA.h"
foo(2,3);


  實際上,在連接階段,連接器會從模塊A生成的目標文件moduleA.obj中尋找_foo_int_int這樣的符號!

  加extern "C"聲明後的編譯和連接方式

  加extern "C"聲明後,模塊A的頭文件變爲:

// 模塊A頭文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
extern "C" int foo( int x, int y );
#endif


  在模塊B的實現文件中仍然調用foo( 2,3 ),其結果是:

  (1)模塊A編譯生成foo的目標代碼時,沒有對其名字進行特殊處理,採用了C語言的方式;

  (2)連接器在爲模塊B的目標代碼尋找foo(2,3)調用時,尋找的是未經修改的符號名_foo。

  如果在模塊A中函數聲明瞭foo爲extern "C"類型,而模塊B中包含的是extern int foo( int x, int y ) ,則模塊B找不到模塊A中的函數;反之亦然。

  所以,可以用一句話概括extern “C”這個聲明的真實目的(任何語言中的任何語法特性的誕生都不是隨意而爲的,來源於真實世界的需求驅動。我們在思考問題時,不能只停留在這個語言是怎麼做的,還要問一問它爲什麼要這麼做,動機是什麼,這樣我們可以更深入地理解許多問題):
  實現C++與C及其它語言的混合編程。
  明白了C++中extern "C"的設立動機,我們下面來具體分析extern "C"通常的使用技巧。

4.extern "C"的慣用法

  (1)在C++中引用C語言中的函數和變量,在包含C語言頭文件(假設爲cExample.h)時,需進行下列處理:

extern "C"
{
#include "cExample.h"
}


  而在C語言的頭文件中,對其外部函數只能指定爲extern類型,C語言中不支持extern "C"聲明,在.c文件中包含了extern "C"時會出現編譯語法錯誤。

  筆者編寫的C++引用C函數例子工程中包含的三個文件的源代碼如下:

/* c語言頭文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);
#endif
/* c語言實現文件:cExample.c */
#include "cExample.h"
int add( int x, int y )
{
return x + y;
}
// c++實現文件,調用add:cppFile.cpp
extern "C"
{
#include "cExample.h"
}
int main(int argc, char* argv[])
{
add(2,3);
return 0;
}


  如果C++調用一個C語言編寫的.DLL時,當包括.DLL的頭文件或聲明接口函數時,應加extern "C" { }。

  (2)在C中引用C++語言中的函數和變量時,C++的頭文件需添加extern "C",但是在C語言中不能直接引用聲明瞭extern "C"的該頭文件,應該僅將C文件中將C++中定義的extern "C"函數聲明爲extern類型。
  筆者編寫的C引用C++函數例子工程中包含的三個文件的源代碼如下:

//C++頭文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif
//C++實現文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
return x + y;
}
/* C實現文件 cFile.c
/* 這樣會編譯出錯:#include "cExample.h" */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
add( 2, 3 );
return 0;
}


  如果深入理解了第3節中所闡述的extern "C"在編譯和連接階段發揮的作用,就能真正理解本節所闡述的從C++引用C函數和C引用C++函數的慣用法。對第4節給出的示例代碼,需要特別留意各個細節。

extern "C" 的用意

分類:學習

前些天,編程序是用到了很久以前寫的C程序,想把裏面的函數利用起來,連接發現出現了找不到具體函數的錯誤:

以下是假設舊的C程序庫

C的頭文件

 add x y

C的源文件

 add x y xy

C++的調用

	add

這樣編譯會產生錯誤cpp.obj : error LNK2001: unresolved external symbol "int __cdecl add(int,int)" (?add@@YAHHH@Z),原因是找不到add的目標模塊

這才令我想起C++重載的函數命名方式和C函數的命名方式,讓我們回顧一下:C中函數編譯後命名會在函數名前加以"_",比如add函數編譯成obj文件時的實際命名爲_add,而c++命名則不同,爲了實現函數重載同樣的函數名add因參數的不同會被編譯成不同的名字

例如

int add(int , int)==>add@@YAHHH@Z,

float add(float , float )==>add@@YAMMM@Z,

以上是VC6的命名方式,不同的編譯器會不同,總之不同的參數同樣的函數名將編譯成不同目標名,以便於函數重載是調用具體的函數。

編譯cpp.cpp中編譯器在cpp文件中發現add(1, 0);的調用而函數聲明爲extern int add(int x, int y);編譯器就決定去找add@@YAHHH@Z,可惜他找不到,因爲C的源文件把extern int add(int x, int y);編譯成_add了;

爲了解決這個問題C++採用了extern "C",這就是我們的主題,想要利用以前的C程序庫,那麼你就要學會它,我們可以看以下標準頭文件你會發現,很多頭文件都有以下的結構

 f1 f2 f3

如果我們仿製該頭文件可以得到

 add

這樣編譯

/*-----------c.c--------------*/
int
add(int x, int y){
return
x+y;
}

這時源文件爲*.c,__cplusplus沒有被定義,extern "C" {}這時沒有生效對於C他看到只是extern int add(int, int);
add函數編譯成_add(int, int);

而編譯c++源文件

/*-----------cpp.cpp--------------*/
#include "c.h"
void main()
{

add(1, 0);
}

這時源文件爲*.cpp,__cplusplus被定義,對於C++他看到的是extern "C" {extern int add(int, int);}編譯器就會知道 add(1, 0);調用的C風格的函數,就會知道去c.obj中找_add(int, int)而不是add@@YAHHH@Z

這也就爲什麼DLL中常看見extern "C" {},windows是採用C語言編制他首先要考慮到C可以正確調用這些DLL,而用戶可能會使用C++而extern "C" {}就會發生作用

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章