将函数分配给函数指针,常量参数的正确性?

18

我现在正在大学里学习C++和面向对象编程的基础知识。 我不确定当将函数赋给函数指针时,函数指针是如何工作的。我遇到了以下代码:

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的赋值是正确的,因为它们的函数参数类型匹配且const正确。但是为什么p1p4的赋值不会失败呢?将const double/int与非const double/int匹配不应该是非法的吗?


2
也许使用const mon const引用会使示例更有趣。由于mystery 7和8是按值调用的,从技术上讲,它们始终是const(或更精确地说:不可更改的)。 - lalala
@lalala 这是一个真正的术语/概念混淆。不可变性和身份是不相关的概念。 - Lightness Races in Orbit
如果您按照您的建议更改示例,那么就不再有问题了(因为这种行为特别涉及传值方式)。 - Lightness Races in Orbit
3个回答

19
根据C++标准(C++ 17,16.1 Overloadable declarations)
(3.4) - 只有在const和/或volatile的存在或不存在上不同的参数声明是等效的。也就是说,在确定正在声明、定义或调用哪个函数时,将忽略每个参数类型的const和volatile类型说明符。
因此,在下面的函数声明中,用于第二个参数的限定符const在确定函数类型的过程中被忽略。
void mystery7(int a, const double b);

函数类型为void( int, double )

同时考虑以下函数声明。

void f( const int * const p );

它相当于以下声明

void f( const int * p );

第二个const是使参数常量的关键(它将指针本身声明为无法在函数内重新分配的常量对象)。第一个const定义了指针的类型。它并未被丢弃。

请注意,虽然C++标准中使用了“const引用”这一术语,但与指针不同,引用本身不能是常量。也就是说,以下声明:

int & const x = initializer;

不正确。

尽管这个声明

int * const x = initializer;

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


感谢您的快速回复。这确实是一些新概念,我现在已经掌握了。但是当我尝试将(const int,double)传递给*p4时会发生什么?这是否会导致编译错误,因为指针指向具有(int,double)参数的函数? - edwardlam0328
1
@edwardlam0328 参数是函数的本地变量,使用复制初始化方法初始化参数。你可以这样想象它。void f( int x ); const int i = 10; f( i ); void f( /int x/ ) { int x = i; /.../ } 这个声明 int x = i; 是有效的,常量可以用作初始化器。 - Vlad from Moscow
感谢大家提醒我关于传值的问题!我忘记了这个重要概念。所以如果我理解正确的话,因为传值参数在函数体内创建了一个本地副本,所以它不会影响传递的原始const类型变量,这种方式我们就可以绕过const限制了吗?这就是为什么将函数指针声明为(const int、double)或(int、double)没有区别的原因,对吗? - edwardlam0328
1
或多或少是的。语言设计者不一定非得这样做(毕竟,在函数体内使用 const 仍然具有目的和意义!)。但他们这样做是为了让调用者不必关心,我想。 @edwardlam0328 - Lightness Races in Orbit
const 只有在函数体内才对参数的值有意义,因此问题不是“为什么这些兼容”,而是“为什么允许限定参数”。虽然从技术上讲,参数名称也可以说同样适用,并且对于文档/可读性很有用,因此有争议是否应该允许。 - Alex Celeste
显示剩余2条评论

3
函数参数按值传递有一个特殊的规则。
虽然使用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-qualifiers。转换后的参数类型列表、省略号或函数参数包的存在与否是函数的parameter-type-list[ 注意:此转换不影响参数的类型。例如,int(*)(const int p, decltype(p)*)int(*)(int, const int*)是相同的类型。——end note——]


2
在我看来,这是个好决定——无论函数实现是否想修改已复制的对象都不是调用者需要知道的事情。 - M.M
@M.M 这只是一个“最好有”的东西,而且会在类型系统中创建一个特殊情况,导致像 OP 这样的混淆。 - Lightness Races in Orbit

1

有一种情况,当你通过指针传递参数时,添加或删除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_castfp2fp3 行根本无法编译。添加显式转换使编译器假定你知道自己在做什么并消除了警告,但由于其未定义的行为,程序仍然会失败。
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()作为具有正确签名的众所周知的函数的例子,但它已被弃用。


请注意,通常情况下使用 strncpy(buf, src, sizeof(buf)); fputs(buf, stdout); 是不正确的,因为如果 src 不适合 buf,则 strncpy 的输出将不以空字符结尾。 - M.M
@M.M 我本来可以用memcpy()写这个例子,但我认为它在这里已经很好地发挥了作用。 - Davislor
@M.M 添加了一个脚注。 - Davislor

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