揭示C++中看似简单的 min 和 max 函数隐藏的细节

本文详细探讨了C++标准库中的min、max函数及其相关算法,包括基本用法、C++11和C++14的新特性,以及std::max的一个bug。通过学习,读者将理解这些函数的深层次原理和使用注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、简介

最小值和最大值是非常简单的函数,没有太多可说的,真的是这样吗?

最小值和最大值是非常基本的概念,但也可能存在一些细节上的问题和需要注意的地方。本文将深入探讨C++标准库里的std::minstd::max等相关函数的用法和注意事项。

二、基本算法

2.1、min 和 max

从功能上讲,std::minstd::max确实在做很简单的事情:它们接受两个值,std::min返回两个值中较小的那个,std::max返回两个值中较大的那个。

函数原型:

template<typename T>
const T& min(const T& a, const T& b);
template<typename T>
const T& max(const T& a, const T& b);

注意返回类型是一个引用(指向const)。即可以实例化指向较小或较大值的引用,从而避免进行复制。因为它是const,所以不能通过这个引用修改原始值。

int a = 2;
int b = 3;
const int& minValue = std::min(a, b);

这个示例中取最小值时不会复制任何对象。对于整型类型来说,这有点过分,但对于一般情况来说,知道这一点很有用。

不过要小心引用的生命周期!如果将作为最小值的值销毁了,那么引用将指向一个不再存在的对象。即使 minValue 是一个指向常量的引用,情况也是如此。实际上,由常量引用保存的临时对象是来自 std::min 的对象,而不是传递进来的那个对象。为了说明这一点,可以看一下下面代码:

#include<iostream>
#include <algorithm>
int get2()
{
    return 2;
}
int get3()
{
    return 3;
}
int main(){
	const int& minValue = std::min(get2(), get3());
	std::cout << minValue << '\n';
	return 0;
}

我们可能期望这段代码会显示 2,但实际上它会进入未定义行为。确实,std::min产生的临时变量被const引用保留了下来,但get2产生的临时变量在std::min执行后就被销毁了。

2.2、min_element 和 max_element

std::minstd::max有对应的*_element来操作值的范围:std::min_elementstd::max_element

std::minstd::max返回的是较大和较小的值,而std::min_elementstd::max_element以迭代器的形式返回较大和较小元素的位置:

template<typename ForwardIt>
ForwardIt min_element(ForwardIt first, ForwardIt last);

这种需求在代码中偶尔会出现。当出现这种情况时,不需要使用for循环重新实现它:它就存在于STL中,等待被使用。所有这些算法都有接受自定义比较函数(或函数对象)的重载版本,以便将值与小于运算符以外的其他内容进行比较。

三、现代C++的min 和 max 特性

3.1、C++ 11中的 std::initializer_list

在c++ 11中,std::min和std::max出现了新的重载。前面的代码只使用了两个(引用)值,为什么只局限于此呢?范围的情况由std::min_element和std::max_element覆盖,但是取不属于范围的几个值中的最小值也是有意义的。

在C++11中,std::minstd::max出现了新的重载函数。之前的函数只接受两个值(引用),为什么不扩展到接受更多值呢?对于一个范围的值,std::min_elementstd::max_element可以处理,但是对于不在一个范围内的多个值的最小值也是有意义的。

新的重载通过接受std::initializer_list来实现这一点:

template<typename T>
T min(std::initializer_list<T> ilist);

它可以这样用:

int minValue = std::min({4, 1, 5, 5, 8, 3, 7});

这个例子直接使用明文数字,std::initializer_list中也接受变量。

注意,与std::minstd::max的基本重载相反,返回的是较小(或较大)值的副本,而不再是引用。实际上,在基本重载中,引用绑定到实参,这对于初始化列表没有意义。

3.2、C++ 14中的 constexpr

C++ 14为目前看到的所有算法带来了新的constexpr重载。例如:

template<typename ForwardIt>
constexpr ForwardIt max_element(ForwardIt first, ForwardIt last);

这有两个显著的影响:

  • 所有与minmax相关的算法都可用于计算编译时值,可用于模板参数。
  • 编译器能够进行令人印象深刻的优化,并且在某些情况下能够完全消除与查找最小值或最大值相关的代码。

四、std::max的一个bug

std::max中有一个bug,并且这个bug正变得越来越明显。有人可能会想,这怎么可能呢?这样一个简单而广泛使用的函数怎么会有bug呢?

在特定情况下,当两个元素比较相等时(通过使用operator<或自定义比较器),就会发生这种情况。然后min返回对第一个的引用,这是可以的,但是max也返回对第一个的引用。这很奇怪。因为期望是在一对元素中,最大值总是小于最小值。

这个 bug 的出现是因为 std::max 函数的实现使用了 operator< 来进行比较。当两个元素相等时,operator< 返回 false,而 std::max 会错误地将第一个元素视为最大值。

用一个示例来验证这个 bug:

#include <iostream>
#include <algorithm>

int main() {
    int a = 5;
    int b = 5;
    
    int& maxRef = std::max(a, b);
    
    // 修改 a 的值
    a = 10;
    
    std::cout << "Max value: " << maxRef << std::endl;
    
    return 0;
}

预期输出应该是 “Max value: 10”,因为在代码中将 a 的值修改为 10。然而,由于 std::max 的 bug,实际输出会是 “Max value: 5”,因为 maxRef 仍然指向初始值较小的元素。

为了解决这个 bug,可以使用 std::max 的另一个重载版本,接受一个自定义的比较器作为参数,或者使用条件表达式 a > b ? a : b 来获取正确的最大值。

C++ 11中引入的新算法std::minmax纠正了这一点。minmax返回一个包含它接收到的两个值的最小值和最大值的值对(pair )。如果这些值相等,那么最小值是第一个,最大值是第二个。

template<typename T>
std::pair<const T&,const T&> minmax(const T& a, const T& b);

std::minmax具有std::minstd::max的所有技术特性:

  • 返回引用;
  • 自定义比较;
  • std::minmax_elementinitializer_listconstexpr的支持。

五、总结

那么,读完这篇文章,还认为最小值和最大值真的那么简单吗?

最小值和最大值函数看似简单,但在实际使用中还是需要注意一些细节问题。通过深入了解C++标准库中相关函数的特性和注意事项,可以更好地掌握它们的使用方法,提高编程效率和代码质量。

在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion 莱恩呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值