簡體   English   中英

C ++函數指針,再次。關於語法的困惑

[英]C++ function pointers, again. Confusion regarding syntax

這個頁面上,我找到了一個很好的C ++函數指針示例(以及仿函數,但這個問題與仿函數無關)。 下面是該頁面的一些copypasta。

#include <iostream>

double add(double left, double right) {
  return left + right;
}

double multiply(double left, double right) {
  return left * right;
}

double binary_op(double left, double right, double (*f)(double, double)) {
  return (*f)(left, right);
}

int main( ) {
  double a = 5.0;
  double b = 10.0;

  std::cout << "Add: " << binary_op(a, b, add) << std::endl;
  std::cout << "Multiply: " << binary_op(a, b, multiply) << std::endl;

  return 0;
}

我總體上理解了代碼,但有一些事情我總是感到困惑。 函數binary_op()接受函數指針*f ,但是當它被使用時,例如在第19行binary_op(a, b, add) ,函數符號add被傳入,而不是人們會想到它的指針&add 現在你可能會說這是因為符號add 一個指針; 它是與add()函數對應的代碼位的地址。 很好,但是這里似乎仍然存在類型差異。 函數binary_op()接受*f ,這意味着f是指向某事物的指針。 我傳入add ,它本身就是代碼的指針。 (對吧?)那么f賦值為add ,這使得f成為代碼的指針,這意味着f就像add一樣,這意味着f應該被調成f(left, right) ,究竟如何應該調用add ,但在第12行,它被調用為(*f)(left, right) ,這對我來說似乎不對,因為它就像寫(*add)(left, right)*add不是函數,它是add點的代碼的第一個字符。 (對?)

我知道用以下代替binary_op()的原始定義也有效。

double binary_op(double left, double right, double f(double, double)) {
  return f(left, right);
}

事實上,這對我來說更有意義,但正如我上面解釋的那樣,原始語法沒有意義。

那么,為什么在語法上使用(*f)而不僅僅是f 如果符號func本身就是一個指針,那么短語“函數指針”或“指向函數的指針”究竟是什么意思? 由於原來的代碼目前維持,當我們寫double (*f)(double, double) ,是什么樣的事情f呢? 指向指針的指針(因為(*f)本身就是指向一些代碼的指針)? 是符號add同一類的事情(*f)或同一類的事情f

現在,如果所有這些的答案是“是的C ++語法很奇怪,只需記住函數指針語法,不要質疑它。” 那么我會不情願地接受它,但我真的想要正確解釋我在這里想錯了什么。

我已經讀過這個問題了 ,我想我明白這一點,但是沒有發現它有助於解決我的困惑。 我也讀過這個問題 ,這也沒有幫助,因為它沒有直接解決我的類型差異問題。 我可以繼續閱讀互聯網上的信息海洋以找到我的答案,但是嘿,這就是Stack Overflow的用途嗎?

這是因為C函數指針是特殊的。

首先,表達式add將衰減為指針。 就像引用數組會衰減成指針一樣,對函數的引用會衰減為指向函數的指針。

然后,那里有奇怪的東西:

return (*f)(left, right);

那么,為什么在語法上使用(*f)而不僅僅是f

兩者都有效,你可以像這樣重寫代碼:

return f(left, right);

這是因為解除引用操作符將返回對函數的引用,並且對函數或函數指針的引用都被認為是可調用的。

有趣的是,函數引用很容易衰減,在調用dereference運算符時會衰減回指針,允許根據需要多次取消引用函數:

return (*******f)(left, right); // ah! still works

由於原來的代碼目前維持,當我們寫double (*f)(double, double) ,是什么樣的事情f呢?

f的類型是double (*)(double, double)即它是指向double(double,double)類型函數的指針。

因為(*f)本身就是一個指針

它不是。

問:通過指針(例如*f )間接得到什么? 答:你得到左值參考。 例如,給定一個對象指針int* ptr ,表達式*ptr的類型是int&即lvalue對int引用。

對於函數指針也是如此:當您通過函數指針間接時,您將獲得指向該函數的左值引用。 *f的情況下,類型是double (&)(double, double)即對double(double,double)類型函數的引用。

是符號add同一類的事情(*f)或同一類的事情f

不合格的id表達式add*f相同,即它是一個左值:

標准草案[expr.prim.id.unqual]

...如果實體是一個函數,表達式是一個左值...


函數符號add是傳入的,而不是人們會想到的指針&add 現在你可能會說這是因為符號add是一個指針;

不,那不是原因。

add不是指針。 這是一個左值。 但是函數類型的左值隱式轉換為指針(這稱為衰減):

標准草案[conv.func]

函數類型T的左值可以轉換為“指向T的指針”的prvalue。 結果是指向函數的指針。

因此,以下在語義上是等效的:

binary_op(a, b,  add); // implicit function-to-pointer conversion
binary_op(a, b, &add); // explicit use of addressof operator

那么,為什么在語法上使用(*f)而不僅僅是f

事實證明,調用函數lvalue與調用函數指針具有相同的語法:

標准草案[expr.call]

函數調用是后綴表達式,后跟括號,其中包含可能為空的逗號分隔的initializer-clause列表,這些子句構成函數的參數。 后綴表達式應具有函數類型或函數指針類型。 對於非成員函數或靜態成員函數的調用,后綴表達式應該是引用函數左值 (在這種情況下,函數到指針標准轉換([conv.func])被抑制在postfix表達式上), 或者有函數指針類型

這些都是相同的函數調用:

add(parameter_list);    // lvalue
(*f)(parameter_list);   // lvalue

(&add)(parameter_list); // pointer
f(parameter_list);      // pointer

PS這兩個聲明是等價的:

double binary_op(double, double, double (*)(double, double))
double binary_op(double, double, double    (double, double))

這是因為以下規則,它與函數指針的隱式衰減互補:

標准草案[dcl.fct]

使用以下規則確定函數的類型。 每個參數的類型(包括函數參數包)由其自己的decl-specifier-seq和聲明符確定。 在確定每個參數的類型之后,將“T數組”或函數類型T的 任何參數 調整為“指向T的指針” ......

首先,當編譯器確定參數的類型時,將指定為函數聲明的函數參數調整為指向函數的指針。 例如,遵循函數聲明

void f( void h() );
void f( void ( *h )() );

等價並聲明相同的一個函數。

考慮以下示范程序

#include <iostream>

void f( void h() );
void f( void ( *h )() );

void h() { std::cout << "Hello Ray\n"; }

void f( void h() ) { h(); }

int main()
{
    f( h );
}

從c ++ 17標准(11.3.5函數):

5使用以下規則確定函數的類型。 每個參數的類型(包括函數參數包)由其自己的decl-specifier-seq和聲明符確定。 在確定每個參數的類型之后,將“T數組”或函數類型T的任何參數調整為“指向T的指針 ”。

另一方面,根據C ++ 17標准

9當給定參數沒有參數時,參數的傳遞方式使得接收函數可以通過調用va_arg(21.11)來獲取參數的值。 [注意:此段落不適用於傳遞給函數參數包的參數。 函數參數包在模板實例化期間(17.6.3)進行了擴展,因此當實際調用函數模板特化時,每個這樣的參數都有一個相應的參數。 - 結束注釋]對參數表達式執行左值到右值(7.1),數組到指針(7.2)和函數到指針(7.3)標准轉換

那么這兩個聲明之間有什么區別呢

void f( void h() );
void f( void ( *h )() );

對於第一個聲明,您可以將函數體中的參數h視為函數指針的typedef。

typedef void ( *H )();

例如

#include <iostream>

void f( void h() );
void f( void ( *h )() );

void h() { std::cout << "Hello Ray\n"; }


typedef void ( *H )();

void f( H h ) { h(); }

int main()
{
    f( h );
}

根據C ++ 17標准(8.5.1.2函數調用)

1函數調用是一個后綴表達式,后跟括號,其中包含一個可能為空的逗號分隔的initializer-clause列表,這些子句構成函數的參數。 后綴表達式應具有函數類型或函數指針類型

所以你也可以定義像這樣的函數

void f( void h() ) { ( *h )(); }

甚至喜歡

void f( void h() ) { ( ******h )(); }

因為當operator *應用於函數名時,函數名被隱含地賦予函數pijnter。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM