typedef函数指针和extern关键字

6

我在理解使用typedef定义函数指针的语法上遇到了问题。我已经阅读了很多答案,但仍然无法理解某些内容。我会尝试解释一下我的想法,以便你了解我的思路。

所以,我们使用typedef为现有的类型创建别名,例如:

typedef int number;

将使我们能够使用与整数相同的数字(类似于预处理器指令 - 我知道有一些差异,比如制作指针的typedef)。

另一个例子:

typedef struct
{
    int num;
} MyStruct;

将未命名的结构体赋予一个名为 MyStruct 的别名。

下面是指向函数的 typedef 的语法:

typedef int (*pFunc)(int, int);

也许我很难理解这个,因为typedef就像给类型取别名一样,而函数并不完全是类型,但从我的理解来看,这更像是指向某种函数签名的指针,所以第一个int是返回类型,第二个括号是用来表示传递给函数的参数类型。 现在我不太理解的是这部分内容:
(*pFunc)
  • 我认为,我们可以使用typedef创建一个名为pFunc的新类型,它是一个指针,这就是*的作用。现在,我们可以创建这种类型的变量,它将指向任何具有我们描述的签名的函数。我正确吗?

好的,假设我是正确的,通常声明指向某些内存的指针如下:

int *p;
double *p;
.
.
.

那么按照以下方式做不是更合理吗:

(pFunc*)

因为在我的看法中,如果星号在名称之前,那么pFunc就是某种类型的指针变量名,而不是实际的指针类型。

  • 我们可以这样做吗?如果可以,将星号放在后面而不是前面是否常用?如果将星号放在前面更常见,那么为什么?因为像我说的那样,当我们定义指针类型时,我们总是将星号放在名称本身之后,就像上面的例子一样,那么这究竟是为什么呢?
  • 关于这个问题的另一个问题,我不太明白括号周围的*pFunc的作用。我认为它们用于表示pFunc是某种类型的指针,如果我们不使用括号,那么签名的返回类型将是int*而不仅仅是int,我在这里正确吗?

好的,还有一件事让我困扰,就是语法的顺序。到目前为止,在所有typedef定义中,我们都将类型放在左边,将别名放在右边。

typedef int number;

typedef struct
{
    int num;
} MyStruct;

我们可以看到,int和struct是左边的类型,我们给它们起的别名是右边的。在指向函数的typedef中,它不遵循这个约定。我们将函数返回的类型放在右边,然后是括号中的类型名称,接着是参数的类型,这个顺序让我感到困惑,因为其他typedef的工作方式都是按照相同的顺序进行的。难道不应该像这样做吗?:typedef int (int,int) Func;因此,我们首先有一个typedef,我们要给它一个别名,这种情况下是一个函数签名,它需要两个整数并返回一个整数,然后在右边我们有别名。这样做更有意义吗?这遵循了其他typedef的顺序,我只是不太理解函数指针的顺序...另一个问题:当我们创建一个指向函数的指针时,它实际上是什么意思?我知道我们可以使用别名调用这个函数,但是像变量一样,指针存储在内存地址中?对于一个函数来说,有什么需要存储的吗?最后,我读到关键字extern与指向函数的指针有关,但是无法理解这个关键字的作用,请有人解释一下它的作用是什么?

2
如果你有一系列相关的问题,请将它们组织成一个列表,使得所有的问题都靠近彼此。 - n. m.
@n.m. 尝试将问题部分放在项目符号中。由于我的这些问题后面跟着一些我对事物看法的解释和一些示例,以帮助大家了解我的思维方式,因此将它们一个接一个地放置会很困难。希望现在更清楚了。 - Tugal.44
你自己已经给出了例子:int x 定义了一个变量。typedef int x 定义了一个类型;struct {...} x 定义了一个结构体变量,typedef struct {...} x 定义了一个类型。函数也是一样的。 - M Oehm
@MOehm 是的,我知道。对于其他所有示例,类型都在左边,别名都在右边,而对于函数指针,别名位于返回类型和参数之间,这是我的问题。 - Tugal.44
在C语言中,函数的类型是指其签名,即其返回类型和参数及其类型。 - Cholthi Paul Ttiopic
显示剩余3条评论
3个回答

6

typedef使用与声明值相同的语法来声明类型。

例如,如果我们声明一个名为myIntint,我们这样做:

int myInt;

如果我们想要声明一个名为myIntType的类型为int,我们只需添加typedef

typedef int myIntType;

我们可以定义一个函数myFunc,如下所示:
int myFunc(int a, int b);

这告诉编译器实际上有一个带有该名称和签名的函数,我们可以调用它。

我们还可以通过以下方式声明函数类型myFuncType:

typedef int myFuncType(int a, int b);

我们可以这样做:

myFuncType myFunc;

这相当于先前声明的myFunc(虽然这种形式很少使用)。
函数不是常规值;它代表一个带有入口地址的代码块。像上面那样的函数声明隐含地是extern;它们告诉编译器所命名的东西存在于其他地方。但是,您可以获取函数的地址,这称为函数指针。函数指针可以指向具有正确签名的任何函数。指针通过在类型/值名称前加上*来声明,因此我们可以尝试:
int *myFuncPtr(int a, int b);

但这是不正确的,因为*int绑定更紧密,所以我们声明myFuncPtr是一个返回指向int的指针的函数。我们必须在指针和名称周围加上括号以改变绑定顺序:

int (*myFuncPtr)(int a, int b);

为了声明一个类型,我们只需要在前面加上typedef

typedef int (*myFuncPtrType)(int a, int b);

在上面的myInt声明中,编译器为该变量分配了一些内存。然而,如果我们在不同的编译单元中编写一些代码,并想要引用myInt,我们需要在引用的编译单元中将其声明为extern,以便我们引用相同的内存。如果没有extern,编译器将分配第二个myInt,这将导致链接器错误(事实并非如此,因为C允许试探性定义,不应使用)。
正如上面所述,函数不是普通值,总是隐式extern。然而,函数指针是普通值,如果您尝试从单独的编译单元引用全局函数指针,则需要extern
通常,您会将全局变量(和函数)的extern放入头文件中。然后,您将包含头文件到包含这些变量和函数定义的编译单元中,以便编译器可以确保类型匹配。

关于extern关键字。如果存在同一变量分配两次的问题,为什么不使用#ifndef XXX #define XXX <code> #endif指令呢? - Tugal.44
你可以使用 ifdef 来使变量的 extern 在除一个编译单元之外的所有编译单元中出现,但你还必须为任何初始化器添加 ifdef。更好的方法是在头文件中放置一个 extern,并在 .c 文件中定义它。 - pat

2
变量定义或声明的语法是类型后面跟着一个或多个变量,可能带有修饰符。一些简单的例子如下:
 int  a, b;    // two int variables a and b
 int  *a, b;   // pointer to an int variable a and an int variable b
 int  a, *b, **c;   // int variable a, pointer to an int variable b, and pointer to a pointer to an int variable c

注意,这些中所有的星号都修改了星号右侧的变量,将其从一个int转换为指向int或指向int的指针。定义的变量可能会像这样使用:
int a, *b, **c, d;

a = 5;     // set a == 5
b = &a;    // set b == address of a
c = &b;    // set c == address of b which in this case has the address of int variable a

d = **c;   // put value of a into d using pointer to point to an int variable a
d = *b;    // put value of a into d using pointer to an int variable a
d = a;     // put value of a into d using the variable a directly

外部声明语句

extern语句用于指示变量的定义位于其他文件中,并且该变量具有全局可见性。因此,您可以使用extern关键字声明变量,以明确变量,这样C编译器在编译时就会拥有所需的信息进行良好的检查。 extern表示该变量实际上是在使用变量的源文件所在位置之外的某个地方定义其内存分配。

使用typedef

typedef是现代C语言的一个非常好的特性,因为它允许您创建一种类似于半新类型的别名。要完全具备创建新类型的功能,实际上需要C ++的类类型特征,这允许为新类型定义运算符。但是,typedef确实提供了一种允许程序员为类型创建别名的良好方法。

大多数typedef的用途是提供一种使变量定义更短、更简洁的方法。出于这个原因,它经常与struct定义一起使用。因此,您可能会有以下struct定义:

typdef struct {
    int iA;
    int iB;
} MyStruct, *PMyStruct;

这将为struct创建两个新的别名,一个用于struct本身,另一个用于指向struct的指针。这些别名可能会被使用,例如:
MyStruct  exampleStruct;
PMyStruct pExampleStrut;

pExampleStruct = &exampleStruct;

这个例子展示了typedef关键字的基本结构,即通过现有类型定义新类型并为其命名。 typedef之前的旧式C语言 在早期的C编译器中,人们经常使用C预处理器来定义宏,以创建复杂类型的别名。而typedef则是更为简洁的方法!
typedef被添加到C标准之前,你需要为结构体指定一个标签,代码看起来会像这样:
struct myTagStruct {     // create a struct declaration with the tag of myTagStruct
    int a;
    int b;
};

struct myTagStruct myStruct;      // create a variable myStruct of the struct

通常情况下,人们会在哪个指针处添加一个C预处理器定义,以便更容易地编写,例如:
#define MYTAGSTRUCT  struct myTagStruct

然后像这样使用它:

MYTAGSTRUCT myStruct;

然而,在使用首选的typedef语法和使用预处理器define方法之间有一个主要区别。预处理器使用C源代码文件的文本来生成修改后的C源代码版本,然后由C编译器进行编译。而typedef关键字是由C编译器编译的C源代码的一部分,因此C编译器知道所定义的类型别名。
为了展示区别,请看下面的源代码。
#define PMYSTRUCT MyStruct *

typedef struct {
    int a1;
    int b1;
} MyStruct, *PMyStruct;

MyStruct  sA, sB;      //
PMyStruct psA, psB;    // compiler sees this as MyStruct  *psA, *psB;
PMYSTRUCT psxA, psxB;  // Preprocessor generates MyStruct * psxA, psxB;

psA = &sA;
psB = &sB;
psxA = &sA;
psxB = &sB;   // compiler error - psxB is not a pointer variable 

使用typedef与函数指针

typedef用于函数指针的语法有些不寻常。它看起来有点像函数声明,但在指针语法上有一些微妙的变化。

typedef int (*pFunc)(int a1, int b1);

这段话的意思是:
  • 创建一个名为pFunc的变量类型typedef
  • 定义为pFunc类型的变量是一个指针
  • 该变量所指向的是一个带有两个int参数并返回一个int类型的函数
括号很重要,因为它们强制编译器以与默认规则不同的方式解释源文本。C编译器有一些解析源文本的规则,您可以通过使用括号来更改C编译器解释源文本的方式。这些规则涉及解析和如何定位变量名称,然后通过使用左结合和右结合的规则确定变量的类型。
a = 5 * b + 1;     // 5 times b then add 1
a = 5 * (b + 1);   // 5 times the sum of b and 1

int *pFunc(int a1, int b1);      // function prototype for function pFunc which returns a pointer to an int
int **pFunct(int a1, int b1);    // function prototype for function pFunc which returns a pointer to a pointer to an int
int (*pfunc)(int a1, int b1);    // function pointer variable for pointer to a function which returns an int
int *(*pFunc)(int a1, int b1);  // function pointer variable for pointer to a function which returns a pointer to an int

函数原型不是函数指针变量。 typedef 的语法类似于未使用 typedef 的变量定义的语法。

typedef  int * pInt;    // create typedef for pointer to an int
int *a;                 // create a variable that is a pointer to an int
pInt b;                 // create a variable that is a pointer to an int
typedef int (*pIntFunc)(int a1, int b1); // create typedef for pointer to a function
typedef int *pFuncWhat(int a1, int b1);  // create a typedef for a function that returns a pointer to an int. seems to be legal but useful? doubt it.
int (*pFuncA)(int a1, int b1);           // create a variable pFuncA that is a pointer to a function
int *FuncDecl(int a1, int b1);           // declare a function that returns a pointer to an int
pIntFunc  pFuncB;                        // create a variable pFuncB that is a pointer to a function

那么什么是函数指针呢?函数入口点有一个地址,因为函数是位于特定内存区域的机器代码。函数的地址是执行函数机器代码应该开始的地方。
当C源代码被编译时,函数调用被转换为一系列跳转到函数地址的机器指令。实际的机器指令并不是真正的跳转,而是一个调用指令,在进行跳转之前保存返回地址,以便在被调用的函数完成后可以返回到调用它的位置。
函数指针变量类似于函数声明。两者之间的区别类似于数组变量和指针变量之间的区别。大多数C编译器将数组变量视为指向变量的常量指针。大多数C编译器将函数名称视为指向函数的常量指针。
使用函数指针可以给你带来灵活性,但这种灵活性就像任何强大的力量一样,也可能导致巨大的破坏。
函数指针变量的一个用途是将函数地址作为参数传递给另一个函数。例如,C标准库有几个排序函数,需要一个比较函数的参数,用于比较正在排序的两个元素。另一个例子是线程库,在创建线程时,您需要指定要执行的函数的地址。
由于函数指针是一个变量,因此如果您有一个需要全局可见性的函数指针,当您在除定义它并分配其内存的源文件之外的文件中声明变量时,您将使用extern关键字作为函数指针变量声明的一部分。但是,如果它是在函数内分配的堆栈上的变量,或者如果它在struct中被用来创建struct的成员,则不会在变量上使用extern修饰符。 file1.c
// define a function that we are going to make available
// through a function pointer. a function will have global
// visibility unless we use the static keyword to reduce the
// visibility to this source file.
static int myfunc(int a, float b)
{
    return (a + (int) (b * 100.0));
}

// define a function pointer that contains the address of the
// function above that we are exporting from this file.
// this function pointer variable automatically has global visibility
// due to where the statement is located in the source file.
int(*pmyfunc)(int, float) = myfunc;

file1.h

// declare the function pointer, which has global visibility
// due to where it was defined in the source file. we declare
// the function pointer in an extern in order to make the
// function prototype with argument types available to the compiler
// when using this variable in other source files.
extern int(*pmyfunc)(int, float);

文件 2.c

#include "file1.h"

int iifunc (int a, int b)
{
    return (a + b/10 + 5);
}

// define a function that has as an argument a function pointer
// variable. this allows the caller to inject into the processing
// of this function a function to be used in the function.
int jjfunc (int a, int (*pf)(int, float))
{
    return ((a / 10) + pf(a, 2000.0));
}

int kkfunc (int a, char *pName)
{
    // an example of a local definition of a function pointer.
    // we could have used pmyfunc directly.
    int(*plocalfunc)(int, float) = pmyfunc;

    // following two statements show difference between calling a
    // function with a function pointer argument and calling a
    // function with a function call in the argument list.
    int k = jjfunc(a, plocalfunc);
    int l = iifunc(a, pmyfunc(a, 3000.0));

    printf ("%s - %d\n", pName, k);
    return k;
}

另一个情况是提供某种接口来隐藏实现细节。假设您有一个打印函数,您想要将其用于多个不同的输出位置或输出目标,例如文件、打印机和终端窗口。这与C++编译器实现虚拟函数或通过COM接口实现COM对象的方式类似。因此,您可以执行以下操作,这是一个非常简单的示例,缺少细节:

typedef struct {
    int  (*pOpenSink) (void);
    int  (*pPrintLine) (char *aszLine);
    int  (*pCloseSink) (void);
} DeviceOpsStruct;

DeviceOpsStruct DeviceOps [] = {
   {PrinterOpen, PrinterLine, PrinterClose},
   {FileOpen, FileLine, FileClose},
   {TermOpen, TermLine, TermClose}
};

int OpenDevice (int iDev)
{
    return DeviceOps[iDev].pOpenSink();
}

int LineDevice (int iDev, char *aszLine)
{
    return DeviceOps[iDev].pPrintLine (aszLine);
}
int CloseDevice (int iDev)
{
    return DeviceOps[iDev].pCloseSink();
}

2

为了明确其他人提供的解释,在C/C++中,括号是右结合的,因此以下声明:

typedef int *pFunc(int, int);

等价于:

typedef int *(pFunc(int, int));

这是一个返回整数指针的函数声明原型,而不是返回整数的函数指针声明。因此,您需要在(*pFunc)周围加括号来打破右结合,并告诉编译器pFunc是一个函数指针,而不仅仅是一个函数。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接