前言

我提到的这些部分,是我在自学C与C++中遇到的比较困难的点。因为初学者的编程,不太容易使用到这些点,所以很容易造成遗忘,并且自己写很容易出错。

最近在看标准C库的源码的时候遇到了这样的困惑,就是关于函数指针,或者说,把一个函数作为另一个函数的参数的这种行为。在读原码的函数原型的时候我发现我尽然读不懂这里的语法是什么。

所以特此写一篇文章来记录一下我认为比较难的知识点。

因为我觉得数组参数,或者说多维数组参数这个点可以作为函数参数,函数指针的铺垫知识,因为性质比较相似,又比较简单。所以就一起跟着讲了。

数组形参

  • 但是呢,数组作为形参,又和一些其他的形参有很大程度的不同。这是由数组的两个性质导致的。所以我们要从数组的两个性质开始讲起。

    1. 数组是不允许拷贝的:这意味着我们不能将一个数组直接赋值给另一个数组,也不能将一个数组直接传递给函数。所以,如果我们试图将数组作为函数的参数,实际上我们在传递的是数组的引用或者说是指向数组第一个元素的指针。

    2. 使用数组时,通常会将其转换成指针:当数组被用作函数的参数时,它会被自动转换为一个指向数组第一个元素的指针。这就是为什么函数内部看到的是一个指针,而不是实际的数组。

  • 这两个特性解释了为什么我们不能直接将数组作为参数传递给函数,而需要以指针的形式来传递。

现在让我们来看一下数组形参的三种主要写法,或者称为表示方式:

1、类型名后跟对应类型的空方括号:

void myFunction(int arr[]) {
    // ...
}

2、类型名后跟对应类型的非空方括号:

  • 注意这种写法其实是一种语法糖,编译器会自动忽略方括号中的数字,仍然将它视作指针。
void myFunction(int arr[10]) {
    // ...
}

3、指针形式:

void myFunction(int* arr) {
    // ...
}

以上三种形式在语义上是相同的,都表示一个指向整型的指针。但是,仅仅通过这个指针,函数是无法知道数组的大小的。因此,通常的做法是将数组的大小作为另一个参数传递给函数。

这就是为什么你通常会看到如下的函数定义:

void myFunction(int arr[], int size) {
    // ...
}

所以,在这三种情况下,我其实最推崇的就是类型名后跟对应类型的空方括号这种定义方式了,因为简单直接,容易阅读,也不容易出错。

再深入了解一下多维数组参数

以下是将二维数组作为函数参数的几种方式:

直接指定维度大小:

void myFunction(int arr[10][10]) {
    // ...
}
  • 在这个例子中,我们将一个10x10的二维整数数组作为参数传递给myFunction函数。

使用空的第一维度:

void myFunction(int arr[][10]) {
    // ...
}
  • 在这个例子中,我们不指定第一维度的大小。函数接受任意大小的第一维度,但是第二维度必须是10。

使用指针表示法:

void myFunction(int (*arr)[10]) {
    // ...
}
  • 在这个例子中,我们使用了一个指针arr,它指向一个包含10个整数的数组。这是一种更底层的方式,但是功能上等价于前两个例子。

再次强调,如果数组的大小不是已知的常量,那么你不能在函数参数中使用它。如果数组的大小在运行时才能确定,那么你需要考虑使用动态内存分配,例如,使用C++的new和delete操作符,或者使用vector之类的容器类。

当处理多维数组时,你需要特别小心地处理每一个维度,因为每一个维度都需要正确地计算内存位置。所以,一般推荐避免使用裸数组,而应使用C++的标准库容器,例如vector和array,它们对内存管理提供了更好的封装。

函数指针

下面来详细讲解一下函数指针的定义,性质,以及作为形参的情况:

函数指针的定义

ReturnType (*pointerName)(ParameterTypes);

这里,ReturnType是函数的返回类型ParameterTypes是函数的参数类型列表(用逗号分隔)pointerName是你选择的指针名称。

例如,以下代码定义了一个名为fp的函数指针,它指向一个没有参数并且返回类型为int的函数:

int (*fp)(void);

函数指针的性质:

函数指针的一些基本性质如下:

  • 函数指针可以像任何其他指针一样被赋值和解引用。当你解引用一个函数指针并以正确的参数列表调用它时,它会调用它所指向的函数。

  • 函数的名称其实就是一个指向该函数的指针,因此你可以直接将函数的名称赋值给函数指针。

例如:

void someFunction(int a) {
    // ...
}

void (*fp)(int) = someFunction;  // 将函数的名称赋值给函数指针
  • 函数指针可以作为参数传递给其他函数,也可以作为函数的返回值。这使得它们可以用来创建灵活的程序设计模式,如回调函数或工厂函数。

函数指针作为形参

以下是一个例子:

void callFunction(int (*fp)(void)) {
    int result = fp();  // 调用函数指针
    // ...
}
  • 在这个例子中,callFunction函数接受一个指向函数的指针作为参数。这个函数没有参数,并且返回一个int值。然后,callFunction函数调用这个函数并获取其返回值。

这是函数指针的基本概念和用法。通过使用函数指针,你可以创建更灵活和动态的代码,因为你可以根据需要在运行时更改或配置要调用的函数。

注意:当我们使用函数指针的时候,是可以省略取地址符的

void someFunction(int a) {
    // ...
}

void (*fp1)(int) = someFunction;  // 可以省略取地址符
void (*fp2)(int) = &someFunction; // 显式使用取地址符
  • 而当你想要通过函数指针调用其所指向的函数时,可以直接使用函数指针名称,也可以选择显式解引用。也就是说,以下两种调用方式也是等效的:
int a = 5;
fp1(a);   // 可以直接使用函数指针名调用
(*fp2)(a); // 显式解引用函数指针后再调用

在这两种情况下,虽然可以选择省略取地址符或者解引用符,但在某些情况下,明确地写出它们可以增加代码的清晰度,特别是对于不太熟悉函数指针的读者来说。

问题一:C语言的函数指针,函数指针形参与C++的有什么区别或者说不同吗?

  • C语言和C++语言在函数指针的使用上基本上是相同的。这包括函数指针的声明、初始化、作为函数参数、以及通过函数指针调用函数等方面。因为C++是在C语言的基础上发展出来的,所以在这个特性上它们是共享的。

  • 不过,值得注意的一点区别是,C++引入了对象和成员函数的概念,而C语言没有。在C++中,你可以使用指向成员函数的指针,但这需要使用特殊的语法,并且你需要一个对象实例来调用它。在C语言中,所有的函数都是全局的,所以没有这个特性。

  • 此外,C++还提供了其他一些功能,如函数对象(functors)和lambda表达式,这些可以在某些情况下替代函数指针。而C语言没有这些特性。

  • 在函数指针作为函数参数的使用上,两者也基本相同。在C和C++中,你都可以创建一个接受函数指针参数的函数,然后在这个函数内部通过这个函数指针调用其他函数。

综上,虽然C++提供了一些C语言没有的额外特性,但在函数指针的基本用法上,C语言和C++是非常相似的。

总结

我们讨论了几个关于函数形参以及函数指针的主题:

  • 数组形参:我们首先讨论了数组形参的特性,以及为什么它们在传递到函数时会被自动转换为指向数组首元素的指针。这是由于C++的两个规则导致的:一是数组是不允许拷贝的,二是在使用数组时通常会将其转换为指针。因此,我们传递数组给函数时,实际上是传递了一个指针。

    • 我们还讨论了传递数组形参的三种常见写法

      • 类型名后跟对应类型的空方括号
      • 类型名后跟对应类型的非空方括号(注意,方括号中的数字在这种情况下会被忽略)
      • 指针形式
    • 对于多维数组作为函数形参,你必须为除第一维度外的所有维度提供大小,因为编译器需要这些信息来正确计算内存中的元素位置。

  • 函数指针:我们讨论了函数指针,这是一个特殊类型的指针,它指向一个函数,而不是一个对象或变量。函数指针可以用来调用它所指向的函数,或者传递给其他能够调用这个函数的代码。

  • 我们讨论了如何定义和使用函数指针,包括函数指针的性质以及作为函数参数的情况。当函数名赋值给函数指针时,通常可以省略取地址运算符&,而当你想通过函数指针调用其所指向的函数时,可以直接使用函数指针名称,也可以选择显式解引用。

最后,我们比较了C和C++在使用函数指针上的相似性和区别。虽然C++在C的基础上提供了一些额外的特性,如成员函数指针、函数对象(functors)和lambda表达式,但在函数指针的基本用法上,C和C++是非常相似的。

最后的最后,如果你觉得我的这篇文章写的不错的话,请给我一个赞与收藏,关注我,我会继续给大家带来更多更优质的干货内容

06-01 13:43