电商设计就是网站设计吗,品牌设计需要多少钱,没有网站的域名,公关公司职级在 C 中, 函数重载是一个非常强大的特性, 允许多个函数使用相同的名称, 但具有不同的参数类型. 重载解析决定了在给定的调用中, 编译器应选择哪个版本的重载函数. 本文将深入探讨 C 重载解析的工作原理, 帮助你在实际编程中更好地理解这一机制.
重载(Overload) vs 重写(Overri…在 C 中, 函数重载是一个非常强大的特性, 允许多个函数使用相同的名称, 但具有不同的参数类型. 重载解析决定了在给定的调用中, 编译器应选择哪个版本的重载函数. 本文将深入探讨 C 重载解析的工作原理, 帮助你在实际编程中更好地理解这一机制.
重载(Overload) vs 重写(Override)
多态(polymorphism)的定义
多态性是指一个实体能够表现为多种形式, 并存在两种或两种以上的可能性. 这种特性使得对象能够根据上下文以不同的方式运行.
编译时多态性: 编译时多态性主要通过函数, 方法或构造函数的重载实现. 这种重载机制允许程序根据数据类型选择合适的函数调用, 从而实现灵活性和代码的可重用性.运行时多态性: 运行时多态性通过方法重写(Override)实现. 程序在运行时根据对象的实际类型调用正确的方法, 从而支持动态行为.
什么是函数重载?
重载是一种允许为具有不同签名(Signature)的多个声明分配相同名称的机制. 常见的有函数重载和运算符重载.
运算符重载
运算符重载允许为特定运算符定义多种操作. 运算符 可以用于添加两个数字或连接两个字符串. 这种重载通过重载解析过程得以实现.
int a 1, b 2;
std::string sa 1, sb 2;int c a b; // 3
std::string sc sa sb; // 12什么是方法重写 方法重写通常与继承一起使用. 当子类的方法与父类的方法具有相同名称时, 子类的方法将覆盖父类的方法. 例如在Animal和Dog类中都声明了voice的方法 #include iostream
class Animal {
public:virtual void voice() 0;
};class Dog : public Animal {
public:void voice() override { std::cout wang wang\n; }
};需要注意, 方法重写与本文讨论的重载解析无关.
为什么需要重载解析
当一个函数调用在编译时包含多个候选函数时, 编译器需要通过重载解析机制来选择最匹配的版本. 重载解析不仅考虑函数的名称, 还需要匹配传递给函数的参数类型, 以确保调用的正确性. 这种机制避免了使用冗长的函数名称, 例如 funStr() 或 funInt(), 从而提高了代码的可读性和简洁性.
fun(mountain);
fun(17);重载声明(Declaring Overloads)
重载适用于自由函数, 类(class)的方法和构造函数. 当这些方法满足如下条件时被称为重载:
具有完全相同的名称从同一范围可见具有一组不同的参数类型
需要注意的是声明的顺序没有影响.
代码样例
// 重载函数 1
void fun(int) {}// 重载函数 2
void fun(int, double) {}int main() {fun(42); // 调用第一个重载fun(42, 3.14); // 调用第二个重载
}什么是重载解析(Overload Resolution)
重载解析是选择最合适重载的过程. 编译器在编译时决定调用哪个重载,
仅考虑(传递的)参数类型以及它们如何与(接收的)参数类型匹配, 从不考虑实际值如果编译器无法选择一个特定的重载, 则函数调用被视为不明确(ambiguous), 导致编译失败
模板函数或方法参与重载解析过程, 如果两个重载被视为相等, 则非模板函数将始终优先于模板函数.
哪些情况不能重载 两个函数仅在返回类型上有所不同. 由于使用返回值是可选的, 因此编译器将其视为定义同一函数两次. int add(int a, int b) {}
double add(int a, int b) {}两个函数仅在其默认参数上有所不同. 默认值不会使函数签名不同. void fun(int a, int b 10);
void fun(int a, int b 5);
void fun(int a 1, int b 10);两个具有相同签名的方法, 其中一个被标记为静态(static) void fun() {}
static void fun() {}重载解析过程概述
重载解析由编译器计算, 在通常情况下, 此过程会导致调用预期的重载. 但是如果存在如下的情况就会变得很复杂, 并且如果编译器选择了错误的重载函数, 这对用户来说就比较难觉察到这一点.
数据类型转换可能很混乱指针/引用数据类型可能无法按预期解析模板函数可以以意想不到的方式推断参数
何时使用重载而不是使用模板
应该编写重载集还是单个模板?当不同的类型需要不同的操作过程, 首选重载函数 例如: std::string 的构造函数 (const char *), (std::string ), (size_type, char) 等 当函数主体对所有数据类型执行相同的操作时, 模板是正确的选择. 例如: 对于std::sort(data.begin(), data.end())来说, data可以是任何类型, 例如 int, double, std::string 等
重载解析开始前
编译器必须首先运行一个名为名称查找的过程名称查找是查找当前范围内可见的每个函数声明的过程 名称查找可能需要参数相关查找模板函数可能需要模板参数推导 可见函数声明的完整列表称为重载集
重载解析细节
第一步, 将整个重载集放入候选列表中第二步, 删除所有无效候选, 根据 C 标准, 无效重载被称为not viable
什么因素导致候选函数不可行或无效 传递的参数数量与声明不匹配 传递太多参数始终被视为无效传递较少参数无效, 除非函数声明中存在默认参数 void doThing(); // candidate A
void doThing(int, bool true); // candidate B
doThing(38);传递参数的数据类型无法转换为与声明相匹配, 即使考虑隐式转换. void doThing(); // candidate A
void doThing(int); // candidate B
void doThing(std::string); // candidate C
doThing(38);查找最佳重载的过程
创建候选列表删除无效重载对剩余候选进行排序如果候选列表中恰好有一个函数的排名高于所有其他函数, 则它将赢得重载解析过程如果最高排名并列, 则使用决胜局
#include iostream
#include string// candidate A
int lookUp(const std::string* key) { return A; }// candidate B
int lookUp(std::string* key) { return B; }int main() {std::string* str new std::string(text);int value lookUp(str);std::cout value std::endl; // 调用哪个?
}答案:
str的类型为std::string*, 所以选择B
candidate B测试题
#include iostream
// overload A1
void fun(char value) { std::cout A1 std::endl; }// overload A2
void fun(long value) { std::cout A2 std::endl; }int main() {fun(42); // 调用哪个?
}答案:
ambiguous (编译失败)选择候选函数
决胜局是重载解析的最后一步, 用于确定哪个候选函数更匹配
当模板和非模板候选函数并列第一时, 将选择非模板函数需要较少步骤的隐式转换比需要更多步骤的候选函数更匹配如果没有最佳匹配或存在无法解析的平局, 则生成编译器错误
C20 新增的决胜局
C20 引入了与模板一起使用的概念(Concepts), 用于对 T 添加约束, 从而限制允许的类型集对模板施加约束不会改变模板在重载解析方面的含义如果传递的参数满足多个重载模板的概念, 则选择约束更严格的模板 此规则仅用作决胜局
测试题
#include iostream// overload B1
void fun(char value) { std::cout B1 std::endl; }// overload B2
template typename T
void fun(T value) {std::cout B2 std::endl;
}int main() {fun(42); // 调用哪个?
}答案
B2当候选集没有最佳匹配时
如何解决模糊函数调用
添加或删除重载将构造函数标记为显式以防止隐式转换可以通过 SFINAE 消除模板函数, 无法实例化的模板函数将不会放置在候选集中使用显式转换在调用之前转换参数, 比如static_cast 传递的参数显式构造对象, 比如使用 std::string(some text) 而不是传递字符串文字
当没有最佳匹配时, 编译会生成错误消息 - “没有匹配的函数可供调用”, 并列出可能的候选者, 即使没有可行的候选者
void fun(char value) {}int main() { fun(x, nullptr); }当最佳匹配不是您想要的
重载解析调试起来可能很复杂, 因为没有方法来询问编译器为什么选择特定的重载如果编译器提供详细模式会很有帮助通过故意将不明确的重载添加到候选列表中, 生成的错误消息可能有助于解释原因尝试更改某些传递参数的数据类型
做一些测试
下面的测试有些存在ambiguous的情况.
Question 1
对比着看下面的两个例子. 思考一下答案.
#include iostream// overload C1
void fun(double, int, int) { std::cout C1 std::endl; }// overload C2
void fun(int, double, double) { std::cout C2 std::endl; }int main() {fun(4, 5, 6); // 调用哪个?
}#include iostream
// overload D1
void fun(int, int, double) { std::cout D1 std::endl; }// overload D2
void fun(int, double, double) { std::cout D2 std::endl; }int main() {fun(4, 5, 6); // 调用哪个?
}答案:
C1/C2 ambiguous(编译错误)
D1为什么会是这种情况? 从参数匹配上来讲, 1的情况是有两个参数类型 int 匹配, 2只有一个参数类型匹配, 似乎是应该选C1和 D1, 但是实际情况则并非如此. 在对候选项进行排序的时候, 编译器会这样做
C的情况: 检查参数4: C1不匹配, C2匹配, C2 胜一局, 此时排序为 [C2, C1]检查参数5: C1匹配, C2不匹配, C1 胜一局, 此时造成排序顺序不确定, 产生ambiguous D的情况: 检查参数4: 两个都匹配, 并列第一,检查参数5: D1匹配, D2不匹配, 排序为[D1, D2]检查参数6: 均不匹配, 排序不变.没有更多的参数了, 当前排序为[D1, D2], 所以D1获胜, 被选择.
Question 3
#include iostream
// overload E1
void fun(int ) { std::cout E1 std::endl; }// overload E2
void fun(int) { std::cout E2 std::endl; }int main() {int x 42;fun(x); // 调用哪个?
}答案:
ambiguous(编译错误)Question 4
#include iostream
// overload F1
void fun(int ) { std::cout F1 std::endl; }// overload F2
void fun(int) { std::cout F2 std::endl; }int main() {fun(42); // 调用哪个?
}答案:
F2Question 5
#include iostream
// overload G1
void fun(int ) { std::cout G1 std::endl; }// overload G2
void fun(int ) { std::cout G2 std::endl; }int main() {int x 42;fun(x); // 调用哪个?
}答案:
G1Question 6
#include iostream
// overload H1
void fun(int ) { std::cout H1 std::endl; }// overload H2
void fun(int ) { std::cout H2 std::endl; }int main() {fun(42); // 调用哪个?
}答案:
H2总结
重载解析是 C 中一个非常强大但复杂的特性. 理解重载解析的细节, 尤其是在处理模棱两可的错误时, 将帮助你写出更高效, 可维护的代码. 在实际编程中, 尽量避免模板和非模板函数的重载冲突, 使用显式转换来消除模糊匹配的情况.
参考链接
Back To Basics: Overload Resolution - CppCon 2021