将函数赋值给函数指针,常量参数正确性

Assigning function to function pointer, const argument correctness?

本文关键字:函数 常量 参数 正确性 指针 赋值      更新时间:2023-10-16

我现在正在大学学习C++和OOP的基础知识。我不是 100% 确定函数指针在为它们分配函数时是如何工作的。我遇到了以下代码:

void mystery7(int a, const double b) { cout << "mystery7" << endl; }
const int mystery8(int a, double b) { cout << "mystery8" << endl; }
int main() {
void(*p1)(int, double) = mystery7;            /* No error! */
void(*p2)(int, const double) = mystery7;
const int(*p3)(int, double) = mystery8;
const int(*p4)(const int, double) = mystery8; /* No error! */
}

根据我的理解,p2p3赋值很好,因为函数参数类型匹配并且恒常性是正确的。但是,为什么p1p4分配不失败呢?将常量双精度/整数与非常量双精度/整数匹配不应该是非法的吗?

根据C++标准(C++ 17,16.1 可重载声明)

(3.4) — 仅在存在或 没有常量和/或挥发性是等效的。也就是说,常量 并且每个参数类型的易失性类型说明符在以下情况下将被忽略 确定要声明、定义或调用的函数。

因此,在确定函数类型的过程中,限定符 const 例如下面函数声明的第二个参数被丢弃。

void mystery7(int a, const double b);

函数类型为void( int, double ).

还要考虑以下函数声明

void f( const int * const p );

它等效于以下声明

void f( const int * p );

第二个常量使参数常量(也就是说,它将指针本身声明为无法在函数内部重新赋值的常量对象)。第一个常量定义指针的类型。它不会被丢弃。

请注意,尽管在C++标准中使用了术语"常量引用"引用本身不能与指针相对的常量。这就是以下声明

int & const x = initializer;

是不正确的。

虽然这个声明

int * const x = initializer;

是正确的,并声明了一个常量指针。

对于按值传递的函数参数,有一个特殊的规则。

尽管它们上的const会影响它们在函数中的使用(以防止事故发生),但在签名上基本上被忽略了。这是因为按值传递的对象的const性对调用站点的原始复制对象没有任何影响。

这就是你所看到的。

(我个人认为这个设计决定是一个错误;这是令人困惑和不必要的!但它就是这样。请注意,它来自默默地将void foo(T arg[5]);更改为void foo(T* arg);的同一段落,因此其中已经有很多我们必须处理的胡说八道!

但是,请记住,这不仅仅是删除此类参数类型的任何const。在int* const中,指针是const的,但在int const*(或const int*)中,指针是非const的,而是指向一个const事物。只有第一个示例与指针本身const性有关,并且将被剥离。


[dcl.fct]/5函数的类型使用以下规则确定。每个参数(包括函数参数包)的类型由其自己的decl-specifier-seq和声明符确定。确定每个参数的类型后,任何类型为"T数组"或函数类型为T的参数都调整为"指向T的指针"。生成参数类型列表后,在形成函数类型时,将删除任何修改参数类型的顶级cv 限定符。转换后的参数类型的结果列表以及省略号或函数参数包的存在与否是函数的参数类型列表[ 注意:此转换不会影响参数的类型。例如,int(*)(const int p, decltype(p)*)int(*)(int, const int*)是相同的类型。— 尾注 ]

在某些情况下,向函数参数添加或删除const限定符是一个严重的错误。 当您通过指针传递参数时,它就会出现。

下面是一个可能出错的简单示例。 这段代码在 C 中被破坏了:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// char * strncpy ( char * destination, const char * source, size_t num );
/* Undeclare the macro required by the C standard, to get a function name that
* we can assign to a pointer:
*/
#undef strncpy
// The correct declaration:
char* (*const fp1)(char*, const char*, size_t) = strncpy;
// Changing const char* to char* will give a warning:
char* (*const fp2)(char*, char*, size_t) = strncpy;
// Adding a const qualifier is actually dangerous:
char* (*const fp3)(const char*, const char*, size_t) = strncpy;
const char* const unmodifiable = "hello, world!";
int main(void)
{
// This is undefined behavior:
fp3( unmodifiable, "Whoops!", sizeof(unmodifiable) );
fputs( unmodifiable, stdout );
return EXIT_SUCCESS;
}

这里的问题出在fp3. 这是指向接受两个const char*参数的函数的指针。 但是,它指向标准库调用strncpy()¹,其第一个参数是它修改的缓冲区。 也就是说,fp3( dest, src, length )有一个类型,它承诺不修改dest指向的数据,但随后它将参数传递给strncpy(),这会修改该数据! 这之所以成为可能,是因为我们更改了函数的类型签名。

尝试修改字符串常量是未定义的行为 - 我们有效地告诉程序调用strncpy( "hello, world!", "Whoops!", sizeof("hello, world!") )- 并且在我测试的几个不同的编译器上,它将在运行时静默失败。

任何现代 C 编译器都应该允许赋值fp1但警告您,您正在用fp2fp3搬起石头砸自己的脚。 在C++中,如果没有reinterpret_castfp2行和fp3行根本不会编译。 添加显式强制转换会使编译器假定您知道自己在做什么并静音警告,但程序仍然由于其未定义的行为而失败。

const auto fp2 =
reinterpret_cast<char*(*)(char*, char*, size_t)>(strncpy);
// Adding a const qualifier is actually dangerous:
const auto fp3 =
reinterpret_cast<char*(*)(const char*, const char*, size_t)>(strncpy);

按值传递的参数不会出现这种情况,因为编译器会复制这些参数。 标记按值传递的参数const只是意味着函数不需要修改其临时副本。 例如,如果标准库内部声明char* strncpy( char* const dest, const char* const src, const size_t n ),它将不能使用K&R习语*dest++ = *src++;。 这会修改函数的参数的临时副本,我们将其声明为const。 由于这不会影响程序的其余部分,因此 C 不介意是否在函数原型或函数指针中添加或删除像这样的const限定符。 通常,您不会将它们作为头文件中公共接口的一部分,因为它们是实现细节。

¹ 尽管我使用strncpy()作为具有正确签名的知名函数的示例,但它通常已被弃用。