我现在正在我的大学学习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! */
}
根据我的理解,p2
和p3
分配很好,因为函数参数类型匹配并且const-ness是正确的。但是,为什么p1
和p4
分配不会失败?将const double / int与非const double / int匹配是否不合法?
答案 0 :(得分:18)
根据C ++标准(C ++ 17,16.1可重载声明)
(3.4)-参数声明仅在存在或不存在上有所不同 缺少const和/或volatile是等效的。也就是说,const 每种参数类型的volatile和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 ++标准中使用了术语“常量引用”,但引用本身不能与指针相反。那是下面的声明
int & const x = initializer;
不正确。
此声明
int * const x = initializer;
是正确的,并声明了一个常量指针。
答案 1 :(得分: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限定符 。结果转换后的参数类型列表以及是否存在省略号或功能参数包就是功能的 parameter-type-list 。 [注意:”此转换不影响参数的类型。 例如,int(*)(const int p, decltype(p)*)
和int(*)(int, const int*)
是相同的类型。 -注释]
答案 2 :(得分: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()
,从而修改该数据!这仅是因为我们更改了函数的类型签名。
尝试修改字符串常量是未定义的行为-我们有效地告诉程序调用{{1}}-并且在我测试过的几种不同的编译器上,它将在运行时无提示地失败。
任何现代C编译器都应允许分配给strncpy( "hello, world!", "Whoops!", sizeof("hello, world!") )
,但要警告您使用fp1
或fp2
可能会使自己陷入困境。在C ++中,如果没有fp3
,fp2
和fp3
行将根本无法编译。添加显式强制转换会使编译器假定您知道自己在做什么,并且会静默警告,但由于其不确定的行为,程序仍然会失败。
reinterpret_cast
按值传递参数不会出现这种情况,因为编译器会复制这些参数。标记由值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
,则它将无法使用K&R习惯用法char* strncpy( char* const dest, const char* const src, const size_t n )
。这会修改函数的参数临时副本,我们将其声明为*dest++ = *src++;
。由于这不会影响程序的其余部分,因此C不会介意是否像函数原型或函数指针中那样添加或删除const
限定符。通常,由于它们是实现细节,因此您不要将它们作为头文件中公共接口的一部分。
¹尽管我使用const
作为带有正确签名的著名函数的示例,但通常不推荐使用。