函数声明
函数声明引入函数名和它的类型。函数定义将函数名/类型与函数体关联起来。
函数声明
函数声明可以在任何作用域出现。类作用域中的函数声明引入成员函数(除非使用 friend 说明符),细节见成员函数和友元函数。
非指针声明符 ( 形参列表 ) cv限定符 (可选) 引用限定符 (可选) 异常说明 (可选) 属性 (可选)
|
(1) | ||||||||
非指针声明符 ( 形参列表 ) cv限定符 (可选) 引用限定符 (可选) 异常说明 (可选) 属性 (可选)-> 尾随返回类型
|
(2) | (C++11 起) | |||||||
(声明符 语法的其他形式见声明页面)
auto。| 非指针声明符 | - | 任何合法的声明符,但如果它以 *、& 或 && 开始,那么它必须被括号环绕。
| ||||||
| 形参列表 | - | 函数形参的逗号分隔列表,可以为空(细节见下文) | ||||||
| 属性 | - | (C++11 起) 属性的列表。这些属性应用于函数的类型,而非函数自身。声明符中标识符之后出现的属性与声明开端出现的属性合并到一起,如果存在。 | ||||||
| cv限定符 | - | const/volatile 限定,只能在非静态成员函数中使用 | ||||||
| 引用限定符 | - | (C++11 起) 引用限定,只能在非静态成员函数中使用 | ||||||
| 异常说明 | - |
| ||||||
| 尾随返回类型 | - | (C++11 起) 尾随返回类型,当返回类型取决于实参名时,例如 template<class T, class U> auto add(T t, U u) -> decltype(t + u);,或当返回类型复杂时,例如在 auto fpif(int)->int(*)(int) 中,尾随返回类型很有用
|
|
如声明页面所示,声明符可以后随 |
(C++20 起) |
只要声明说明符序列允许,函数声明符就可以和其他声明符混合:
// 声明一个 int、一个 int*、一个函数,及一个函数指针
int a = 1, *p = NULL, f(), (*pf)(double);
// 声明说明符序列 是 int
// 声明符 f() 声明(但不定义)一个不接受实参并返回 int 的函数
struct S
{
virtual int f(char) const, g(int) &&; // 声明两个非静态成员函数
virtual int f(char), x; // 编译时错误:(声明说明符序列中的)virtual
// 只能声明非静态成员函数
};
|
以 volatile 限定的对象类型作为形参类型或返回类型是被弃用的。 |
(C++20 起) |
函数的返回类型不能是函数类型或数组类型(但可以是到它们的指针或引用)。
|
与任何声明相同,声明前出现的属性和声明符中直接跟在标识符之后的属性都会应用到所声明或定义的实体(在这个例子中,应用到函数): 然而,(按上述语法)在声明符后出现的属性会应用到函数类型而非函数自身: |
(C++11 起) |
返回类型推导如果函数声明的声明说明符序列 包含关键词 如果返回类型是 (注意:“ 如果有多条返回语句,那么它们必须推导出相同的类型: 如果没有返回语句或返回语句的操作数是 void 表达式,那么所声明的返回类型,必须要么是 一旦在函数中见到一条返回语句,那么从该语句推导的返回类型就可以用于函数的剩余部分,包括其他返回语句: 如果返回语句使用花括号包围的初始化器列表,那么就不能推导: 除了用户定义转换函数以外的函数模板可以使用返回类型推导。即使返回语句中的表达式并非待决,推导也在实例化时发生。这种实例化并不处于 SFINAE 的目的的立即语境中。 使用返回类型推导的函数或函数模板的重声明或特化必须使用同一返回类型占位符: 反过来也一样:不使用返回类型推导的函数或函数模板的重声明或特化不能使用返回类型占位符: 显式实例化声明本身并不会实例化使用返回类型推导的函数模板: |
(C++14 起) |
形参列表
形参列表决定调用函数时所能指定的实参。它是形参声明 的逗号分隔列表,其中每一项拥有下列语法:
| 属性 (可选) 声明说明符序列 声明符 | (1) | ||||||||
|
属性 (可选) |
(2) | (C++23 起) | |||||||
属性 (可选) 声明说明符序列 声明符 = 初始化器
|
(3) | ||||||||
| 属性 (可选) 声明说明符序列 抽象声明符 (可选) | (4) | ||||||||
|
属性 (可选) |
(5) | (C++23 起) | |||||||
属性 (可选) 声明说明符序列 抽象声明符 (可选) = 初始化器
|
(6) | ||||||||
void
|
(7) | ||||||||
int f(int a, int* p, int (*(*x)(double))[3]);int f(int a = 7, int* p = nullptr, int (*(*x)(double))[3] = nullptr);int f(int, int*, int (*(*)(double))[3]);int f(int = 7, int* = nullptr, int (*(*)(double))[3] = nullptr);int f(void); 和 int f(); 声明同一函数。void 这一种形式与空形参列表等价,void 形参的其他用法非良构:
| 错误用法 | 示例 |
|---|---|
| 存在多个形参 | int f1(void, int);
|
void 形参具名
|
inf f2(void param);
|
void 具有 cv 限定
|
int f3(const void);
|
void 是待决的
|
int f4(T); (where T is void)
|
void 形参是显式对象形参 (C++23 起)
|
int f5(this void);
|
|
尽管声明说明符序列 暗示可以存在类型说明符之外的说明符,但可用的其他说明符只有 |
(C++17 前) |
|
如果任何函数形参使用了占位符( |
(C++20 起) |
|
带说明符 显式对象形参不能是函数形参包,并且只能作为以下声明中的形参列表的首个形参: 有显式对象形参的成员函数具有以下限制: |
(C++23 起) |
函数声明中声明的形参名通常只用作以自身为文档。它们在函数定义中被使用(但仍不强制)。
当类型名称被圆括号包围时(包括 lambda 表达式)(C++11 起)会产生歧义。此时可以解析成类型是函数指针的形参的声明以及声明符 中的标识符被额外的圆括号包围的形参的声明。解决方案是将该类型名称视为简单类型说明符(此时它即是函数指针类型):
class C {};
void f(int(C)) {} // void f(int(*fp)(C param)) {}
// 不是 void f(int C) {}
void g(int *(C[10])); // void g(int *(*fp)(C param[10]));
// 不是 void g(int *C[10]);
形参类型不能是含有到未知边界数组的引用或指针的类型,含有这种类型的多级指针/数组,或含有指向以这些类型为形参的函数的指针。
使用省略号
形参列表的最后一个形参可以是省略号(...);这会声明一个变参函数位于省略号前的逗号可以省略(C++26 弃用):
int printf(const char* fmt, ...); // 变参函数
int printf(const char* fmt...); // 同上,但 C++26 起被弃用
template<typename... Args>
void f(Args..., ...); // 带形参包的变参函数模板
template<typename... Args>
void f(Args... ...); // 同上,但 C++26 起被弃用
template<typename... Args>
void f(Args......); // 同上,但 C++26 起被弃用
函数类型
形参类型列表
函数的形参类型列表 按以下方式确定:
- 每个形参(包括形参包)(C++11 起)的类型通过形参自身的声明确定。
- 在确定每个形参的类型后,类型是 “
T的数组”或某个函数类型T的形参会调整为具有类型“指向T的指针”。 - 在产生形参类型的列表后,在组成函数类型时会移除所有修改了形参类型的顶层 cv 限定符。
- 转换后的形参类型的列表,加上是否有出现省略号或者形参包(C++11 起)的特征共同组成了函数的形参类型列表。
void f(char*); // #1
void f(char[]) {} // 定义了 #1
void f(const char*) {} // OK,另一重载
void f(char* const) {} // 错误:重定义了 #1
void g(char(*)[2]); // #2
void g(char[3][2]) {} // 定义了 #2
void g(char[3][3]) {} // OK,另一重载
void h(int x(const int)); // #3
void h(int (*)(int)) {} // 定义了 #3
确定函数类型
在语法 (1) 中,在假设非指针声明符 是独立声明的情况下,给定非指针声明符 中有限定标识 或无限定标识 的类型为“T 派生的声明符类型列表”:
|
(C++17 起) |
- 否则,(C++17 起)声明的函数的类型是
“返回T的 cv限定符 (可选) 引用限定符 (可选)(C++11 起)接受形参类型列表的函数 派生的声明符类型列表”。
|
在语法 (2) 中,在假设非指针声明符 是独立声明的情况下,给定非指针声明符 中有限定标识 或无限定标识 的类型为“ |
(C++11 起) |
|
(C++17 起) |
存在属性 时,它会应用到函数类型。 |
(C++11 起) |
// f1 的类型是“返回 void 的接受 int 的函数,带有属性 noreturn”
void f1(int a) [[noreturn]];
// f2 的类型是“返回 int 的接受指向 int 的指针的 constexpr noexcept 函数”
constexpr auto f2(int[] b) noexcept -> int;
struct X
{
// f3 的类型是“返回 const int 的 const 的不接受参数的函数”
const int f3() const;
};
尾随限定符
有 cv限定符 或引用限定符 (C++11 起)的函数类型(包括以 typedef 名命名的类型)只能作为以下类型出现:
typedef int FIC(int) const;
FIC f; // 错误:声明的不是成员函数
struct S
{
FIC f; // OK
};
FIC S::*pm = &S::f; // OK
函数签名
每个函数都有一个签名。
函数签名包含函数的名字和形参类型列表。除以下情况外,函数签名也包含函数的外围命名空间:
- 如果函数是成员函数,那么它的签名会包含以该函数为成员的类而非它的外围命名空间。它的签名也包含以下组分(如果存在):
- cv限定符
|
(C++11 起) |
|
(C++20 起) |
异常说明 和属性 (C++11 起)不是函数签名的一部分,尽管 noexcept 说明会影响函数类型(C++17 起)。
函数定义
非成员函数的定义只能在命名空间作用域中出现(不存在嵌套函数)。成员函数的定义也可以在类定义的体内出现。它们拥有下列语法:
| 属性 (可选) 声明说明符序列 (可选) 声明符 虚说明 (可选) 契约说明 (可选) 函数体 | (1) | ||||||||
| 属性 (可选) 声明说明符序列 (可选) 声明符 requires子句 契约说明 (可选) 函数体 | (2) | (C++20 起) | |||||||
| 属性 | - | (C++11 起) 属性的列表。这些属性与可能出现在声明符 中标识符之后的属性(见本页顶部)合并到一起。 |
| 声明说明符序列 | - | 带有说明符的返回类型,与声明文法相同 |
| 声明符 | - | 函数声明符,与上述函数声明语法相同(可以被圆括号包围) |
| 虚说明 | - | (C++11 起) override、final,或它们任意顺序的组合(只能用于非静态成员函数)
|
| requires子句 | - | requires 子句
|
| 契约说明 | - | (C++26 起) 函数契约说明符的列表 |
| 函数体 | - | 函数体(见下文) |
函数体 是下列之一:
| 构造函数初始化器 (可选) 复合语句 | (1) | ||||||||
| 函数try块 | (2) | ||||||||
= default ;
|
(3) | (C++11 起) | |||||||
= delete ;
|
(4) | (C++11 起) | |||||||
= delete ( 字符串字面量 );
|
(5) | (C++26 起) | |||||||
try 块。| 构造函数初始化器 | - | 成员初始化器列表,只能用于构造函数 |
| 复合语句 | - | 花括号环绕的语句序列,它们构成函数体 |
| 函数try块 | - | 函数 try 块
|
| 字符串字面量 | - | 不求值字符串字面量,可以用于解释函数为何被弃置的理由 |
int max(int a, int b, int c)
{
int m = (a > b) ? a : b;
return (m > c) ? m : c;
}
// 声明说明符序列是“int”
// 声明符是“max(int a, int b, int c)”
// 函数体是 { ... }
函数体是一条复合语句(由一对花括号环绕的零或多条语句),它们在函数调用时被执行。构造函数的函数体还包括以下内容:
- 对于所有标识符未在该构造函数的成员初始化器列表中出现的非静态数据成员,用于初始化对应成员子对象的默认成员初始化式或(C++11 起)默认初始化。
- 对于所有类型名未在该构造函数的成员初始化器列表中出现的基类,用于初始化对应基类子对象的默认初始化。
|
如果函数定义包含了虚说明,那么定义的必须是成员函数。 |
(C++11 起) |
|
如果函数定义包含了 requires子句,那么定义的必须是模板化函数。 |
(C++20 起) |
void f() override {} // 错误:不是成员函数
void g() requires (sizeof(int) == 4) {} // 错误:不是模板化函数
函数的各个形参类型和返回类型不能是(可有 cv 限定的)不完整的类类型,除非函数已显式定义为被弃置(C++11 起)。完整性检查只会在函数体中进行,因此成员函数可以返回在其中定义它们的类(或它的外围类),尽管在定义点它可能不完整(它在函数体内完整)。
在函数定义的声明符 中声明的形参在函数体内处于作用域中。如果某个形参没有在函数体中使用,那么它不需要具名(只需要使用抽象声明符):
void print(int a, int) // 没有使用第二个形参
{
std::printf("a = %d\n", a);
}
尽管形参上的顶层 cv 限定符在函数声明中被忽略,它们仍然会修饰形参的类型,这在函数体中可见:
void f(const int n) // 声明 void(int) 类型的函数
{
// 但在体内,n 的类型是 const int
}
预置函数如果函数定义具有语法 (3),那么该函数被定义为显式预置的。 显式预置的函数必须是特殊成员函数或比较运算符函数(C++20 起),并且不能有默认实参。 显式预置的特殊成员函数
如果
在首个声明被显式预置的函数隐式内联,并且在它可以是 constexpr 函数的情况下隐式具有 constexpr。 显式预置的函数和隐式声明的函数统称为预置 函数。它们的实际定义会隐式提供,详情见这些函数各自对应的页面。 弃置函数如果函数定义具有语法 (4) 或 (5)(C++26 起),那么该函数被定义为显式弃置的。 任何弃置函数的使用都是非良构的(程序无法编译)。这包含调用,包括显式(以函数调用运算符)及隐式(对弃置的重载运算符、特殊成员函数、分配函数等的调用),构成指向弃置函数的指针或成员指针,甚至是在不潜在求值的表达式中使用弃置函数。 非纯虚成员函数可以被定义为弃置,即便它被隐式 ODR 使用。弃置函数只能被弃置函数覆写,而非弃置函数只能被非弃置函数覆写。
如果函数被重载,那么首先进行重载决议,且只有在选择了弃置函数时程序才非良构: 函数的弃置定义必须是翻译单元中的首条声明:已经声明过的函数不能声明为弃置的: 由用户提供的函数如果一个函数由用户声明且没有在它的首个声明被显式预置或显式弃置,那么它由用户提供。由用户提供的显式预置的函数(即在它的首个声明后被显式预置)在它被显式预置的地方定义;如果该函数被隐式定义为弃置的,那么程序非良构。需要为不断变化的代码库提供稳定的二进制接口的情况下,在函数的首个声明后再定义为预置可以保证执行效率,也能提供简明的定义。 解决歧义在以
__func__在函数体内,如同以如下方式定义了函数局部的预定义变量 __func__: 此变量具有块作用域及静态存储期: 运行此代码 可能的输出: |
(C++11 起) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
函数契约说明符函数声明和 lambda 表达式可以包含函数契约说明符 的序列,说明符语法如下:
1) 引入前条件断言。
2,3) 引入后条件断言。
2) 断言没有绑定到结果。
3) 断言有绑定到结果。
函数契约断言是与函数关联的契约断言。函数契约断言的谓词是它的按语境转换到 以下函数不能声明有函数契约说明符: 前条件断言前条件断言与进入函数关联: 后条件断言后条件断言与正常退出函数关联。 如果后条件断言有标识符,那么函数契约说明符会引入标识符 作为关联函数的结果绑定。结果绑定表示对该函数的调用返回的对象或引用。结果绑定的类型是关联函数的返回类型。 如果后条件断言有标识符,而关联函数的返回类型是(可有 cv 限定的 当非模板化函数声明的返回类型包含占位类型时,有标识符 的后条件断言只能在函数定义中出现: 契约一致性函数或函数模板 如果声明 两个契约说明 在以相同顺序包含相同的函数契约说明符时相同。 如果满足以下所有条件,那么函数声明
如果只是因为比较谓词 中的两个 lambda 表达式而不满足以上条件,那么不要求诊断。 |
(C++26 起) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
注解
在使用直接初始化语法的变量声明和函数声明之间有歧义的情况下,编译器选择函数声明;见直接初始化页面。
| 功能特性测试宏 | 值 | 标准 | 功能特性 |
|---|---|---|---|
__cpp_decltype_auto |
201304L |
(C++14) | decltype(auto)
|
__cpp_return_type_deduction |
201304L |
(C++14) | 普通函数的返回类型推导 |
__cpp_explicit_this_parameter |
202110L |
(C++23) | 显式对象形参(推导 this)
|
__cpp_deleted_function |
202403L |
(C++26) | 带有理由的弃置函数 |
关键词
示例
#include <iostream>
#include <string>
// 拥有默认实参的简单函数,不返回内容
void f0(const std::string& arg = "world!")
{
std::cout << "Hello, " << arg << '\n';
}
// 命名空间(文件)作用域中的声明
// (定义在后面提供)
int f1();
// 返回指向 f0 的指针的函数,C++11 前的风格
void (*fp03())(const std::string&)
{
return f0;
}
// 返回指向 f0 的指针的函数
auto fp11() -> void(*)(const std::string&)
{
return f0;
}
int main()
{
f0();
fp03()("test!");
fp11()("again!");
int f2(std::string) noexcept; // 函数作用域中的声明
std::cout << "f2(\"bad\"):" << f2("bad") << '\n';
std::cout << "f2(\"42\"):" << f2("42") << '\n';
}
// 简单的非成员函数,返回 int
int f1()
{
return 007;
}
// 拥有异常说明和函数 try 块的函数
int f2(std::string str) noexcept
try
{
return std::stoi(str);
}
catch (const std::exception& e)
{
std::cerr << "stoi() 失败!\n";
return 0;
}
// 弃置函数,尝试调用它会造成编译错误
void bar() = delete
# if __cpp_deleted_function
("理由")
# endif
;可能的输出:
stoi() 失败!
Hello, world!
Hello, test!
Hello, again!
f2("bad"):0
f2("42"):42缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
| 缺陷报告 | 应用于 | 出版时的行为 | 正确行为 |
|---|---|---|---|
| CWG 135 | C++98 | 类内的成员函数定义的形参和返回值不能是类本身,因为它还不完整 | 允许此类定义 |
| CWG 332 | C++98 | 函数形参可以是有 cv 限定的 void 类型
|
已禁止 |
| CWG 393 | C++98 | 含有到未知边界数组的指针/引用的类型不能作为形参 | 允许这些类型 |
| CWG 452 | C++98 | 成员初始化器列表不是函数体的一部分 | 是函数体的一部分 |
| CWG 577 | C++98 | 待决 void 类型可以用来声明无形参函数
|
只有非待决的 void 可以
|
| CWG 1327 | C++11 | 显式预置或弃置的函数定义不能带有 override 或 final 说明符
|
可以带有这些说明符 |
| CWG 1355 | C++11 | 只有特殊成员函数能由用户提供 | 拓展到所有函数 |
| CWG 1394 | C++11 | 弃置函数不能有不完整类型的形参或返回不完整类型 | 允许这些地方有不完整的返回类型 |
| CWG 1824 | C++98 | 函数定义的返回类型和形参类型的完整性检查也会在函数定义的语境外进行 | 只能在函数定义的语境中检查 |
| CWG 1877 | C++14 | return; 在返回类型推导中被视为 return void();
|
此时直接将返回类型推导成 void
|
| CWG 2015 | C++11 | 虚弃置函数的隐式 ODR 使用非良构 | 将此类 ODR 使用从使用禁止豁免 |
| CWG 2044 | C++14 | 返回 void 但声明的返回类型是decltype(auto) 的函数的返回类型推导会失败
|
更新推导规则以处理这种情况 |
| CWG 2081 | C++14 | 即使函数的最初声明没有使用返回类型推导,它的重声明也可以使用 | 不能使用 |
| CWG 2144 | C++11 | {} 在同一个地方既可以是函数体也可以是初始化器
|
通过声明符标识符的类型来区分 |
| CWG 2145 | C++98 | 函数定义中声明符 不能被圆括号包围 | 可以包围 |
| CWG 2259 | C++11 | 圆括号包围类型名称时产生的歧义的解决规则未覆盖 lambda 表达式 | 已覆盖 |
| CWG 2430 | C++98 | CWG 问题 1824 的解决方案导致在类定义中的 函数定义中不能将该类作为返回类型形参类型 |
只能在函数体中检查 |
| CWG 2760 | C++98 | 构造函数的函数体不包括未在常规函数体中指定的初始化 | 也包括这些初始化 |
| CWG 2831 | C++20 | 带有 requires子句 的函数定义可以定义非模板化函数 | 已禁止 |
| CWG 2846 | C++23 | 显式对象成员函数不能有类外定义 | 可以有 |
| CWG 2915 | C++23 | 无名显式对象形参可以具有 void 类型
|
已禁止 |
参阅
声明函数的 C 文档
|