为什么在这个简单的函数中我不应该使用`const`?

15

当学习C++时,学习函数概念的第一步通常是类似以下内容的函数:

int add(int a, int b)
{
    return a+b;
}

现在我在想:我应该在这里使用const关键字,还是不用它,从而导致...

int add(const int a, const int b)
{
    return a+b;
}

但这样做有意义吗?它会加速我的程序,做其他重要的事情,还是只会增加混乱?


在函数声明中使用const会让人感到困惑(void foo(int)void foo(const int);完全相同)。但在定义中不是这样的。当然,这只是我的观点(因此是评论,而不是答案)。 - juanchopanza
2
可能是 使用 const 修饰函数参数 的重复问题。 - demonplus
1
更加主观的是,看到一个参数通过const值传递有助于衡量程序员的能力:我有时发现这很有用。 - Bathsheba
@Cheersandhth.-Alf Kerrek的选项1还是2?个人而言,我更喜欢3胜过2胜过1。 - Deduplicator
@Deduplicator:这将是2,不用于纯声明,除非我认为仅头模块很好。 :) 曾经有过关于接口与实现的讨论。显然,当纯声明与实现相同时,有些工具会更加愉快,否则会引起问题。不确定是哪些工具。我记不清了。 - Cheers and hth. - Alf
显示剩余2条评论
10个回答

16

调用者的角度来看,第一种和第二种形式是相同的。

由于整数是通过值传递的,即使函数修改了ab,修改后的值也是原始值的副本,对于调用者来说不可见。

然而,从函数实现者的角度来看,存在差异。实际上,在第二种形式中:

int add(const int a, const int b)
如果你尝试在函数体内修改标记为constab的值,你将会收到编译错误。
相反,如果省略const,则可以更改这些值。再次强调,这些修改不会对调用者可见。

2
这种情况下,你应该在这里添加const它不是函数签名的一部分。这就是我认为对于这些简单函数不包括它的主要原因,因为从调用者的角度来看,它并不存在,只是一个实现细节:对于这样一个简单的函数,它是一个不必要的复杂性。 - Martin Ba
@MartinBa:感谢您的评论。然而,在这种超级简单的情况下,函数定义中参数上的const可能是不必要的,但我认为对于更复杂的函数,通过在_实现位置_标记它们为const可以帮助捕获错误,例如确保这些参数不会被错误地修改并且修改不会丢失。我会省略声明位置(原型)上的const,因为对调用者来说可能会很嘈杂/令人困惑。 - Mr.C64

7
使用 int add(const int a, const int b) 表示在函数体内不能修改输入参数。 也许编译器可以基于此进行一些优化,因此它应该永远不会比非const等效版本更慢。但我从未在实践中观察到这种效果。
传递 const 参数还可以增加代码库的稳定性,特别是在协作项目中。
尽管如此,我认为这样做过于冗长,而且没有必要,我会使用 int add(int a, int b)。极少数情况下,对于特别长的函数,我会利用可以使用非const参数声明函数,并使用const参数定义函数的事实。

这与声明const局部变量相同,我们可以使用它们(不确定是否会抑制用户定义类型的复制省略)。 - juanchopanza
1
它们无论如何都会被复制,我在这里看不到任何优化可能性,但我会把这留给优化神。 - Hatted Rooster
1
在一个特别长的函数中,如果输入参数没有被搞乱,那么这可以让人放心。 - Bathsheba
2
@SomeWittyUsername 啊,这个问题是关于按值传递的。有关按引用传递的评论在这里是不相关的。 - juanchopanza
@juanchopanza 这个问题是为什么在传值时不使用const。并且这个问题已经被C64先生正确回答了。 - SomeWittyUsername
显示剩余3条评论

7
如果您在代码库中遇到了正确性错误,并且使用 const 可以解决问题,那么请添加 const。但是,请注意以下相关问题。函数参数的顶层限定符不是函数类型的一部分,因此您的函数类型仍然是 int(int, int)。这意味着任何函数的声明也会忽略限定符,因此 int add(int, int)、int add(const int, int) 和 int add(int,const int) 都声明了同一个函数。因此,您必须决定如何编写头文件的策略。现在您有三个必要的立场可以采取:
1. 在声明和定义中始终使用限定符。优点是使代码保持“一致”(创建实现时考虑复制/粘贴)。缺点是这些限定符与接口无关,而且根本不能执行(您稍后可以更改定义!),因此最多只是噪音,最坏的情况下是错误。 2. 在定义中使用限定符,但在其他声明中不使用。优点是正确地传达了接口,而在定义中仍然可以进行 const 检查。缺点是有些人可能会因拼写上的差异而感到困惑。(就像人们可能会因为 static constexpr T foo; 类成员可以用 const T C::foo; 定义而感到困惑一样。) 3. 两者都不使用。优点是它是一致的、简洁的、易于记忆的和最小的。缺点是您在定义中错过了正确性检查。
没有正确答案。如果您是代码库所有者或项目负责人,您应该根据代码库和团队的最大问题来决定。 (我个人的立场是坚持使用(3)直到有充分的理由改变。)

对于问题中的特定示例,使用const限定按值传递的参数实际上会让用户感到困惑和误导。这种限定的实际含义是副本是不可变的,这当然与原始对象的const性或缺乏关系无关。 - SomeWittyUsername

1

我感觉每个人都在绕过答案的这个部分...... 使用const确实可以防止函数在内部修改您的int ab的值。这非常有用,因此如果编译器允许,请随意使用它。但是,函数调用者一旦函数完成就无法知道ab的任何变化。因此,即使ab已更改,除了定义函数的人以外,没有人会知道它们的更新值。

int funcB(int a, int b)
{
    a = a+1;
    b = b*b;
    return a+b;
}

void funcA()
{
    int s = 5;
    int t = 6;
    int result = funcB(s, t);
    printf("%f + %f = %f", s,t, result);
}

funcA 打印出:"5 + 6 = 42"

Const 保护通常在通过引用传递值时使用,例如:

int function(const int &a, const int &b) {}

这将变量 ab 的引用传递给函数(即不复制 ab,而是仅传递该变量的内存地址,也称为句柄)。当通过引用传递变量时,对变量所做的任何更改都会在函数范围之外记忆,并且可能会改变程序运行的方式。这通常是不期望的行为。 因此,如果您重新设计上面的 funcB 并通过引用传递:

int funcB(int &a, int &b)
{
    a = a+1;
    b = b*b;
    return a+b;
}

funcA 输出:"6 + 36 = 42"

如果你在 funcB 中添加了 const 的正确性:

int funcB(const int &a, const int &b)
{
    a = a+1;
    b = b*b;
    return a+b;
}

我认为编译器不会允许你这样做,因为你试图显式地修改通过const保护的值。

另一个需要使用const的重要时刻是当你通过指针传递参数而不是引用或副本时...

int funcB(int *a, int *b)
{
    a = a+1;
    b = b*b;
    return a+b;
}

除非你是指针专家,否则避免在没有const修饰的情况下传递指针。这个函数可能会尝试迭代指针数组的索引,从而导致与内存越界相关的运行时错误。你可能会意外地看到来自完全不同程序的内存...但很可能不会。

最后,由于你只是传递int,没有实际需要通过引用传递(通常这样做是为了避免将复杂数据添加到内存中,因为每个非引用或非指针传递到函数中都会将值复制到被调用函数的内存中),因为int的内存占用非常小。除非你正在使用具有极限内存的专用硬件,否则它可能有用;这不适用于大多数标准计算机、桌面电脑和智能手机,这些设备在过去20年内制造。


0

如果你想真正地实现const正确性,那么你应该添加它,但实际上它只会让你输入和阅读更多。

没有任何东西会变得更快,虽然它可能意味着你的变量会进入内存中的另一个位置,但在大多数现代机器上这是不太可能的。

它将阻止你意外地给它们赋值,但由于它们是函数局部变量,所以相对不重要。如果它们是引用,那么这就说明了你向调用者表明不要更改它们的意图,这才是最重要的。


0

如果你喜欢的话,可以在那里使用const

这不太可能加速你的程序,因为任何合理的编译器都可以看到没有代码路径会改变ab的值,并进行任何所需的优化。

abint类型,它们是按值传递的,因此将它们设置为const对该函数的用户没有影响。

唯一可能的好处是当你的函数很长且更复杂时,你想避免可能的编程错误,即在函数期间更改ab的原始值。


0
如果您使用const,那么您就不能修改a,b的值。这就是为什么我们不使用const的原因。

因此,在上述两个函数中,我不会这样做。因此我在询问... - arc_lupus

0
const的主要目的是提供文档说明和防止编程错误。const允许您清楚地告诉自己和他人某些内容不应更改。此外,它还具有额外的好处,即您声明为const的任何内容实际上都将保持不变,除非使用强制方法。将函数的引用参数声明为const引用特别有用:
bool SomeFunction (const myObj& obj);

在这里,一个myObj对象通过引用传递到SomeFunction中。出于安全考虑,使用const确保SomeFunction不能更改对象--毕竟,它只是要确保对象处于有效状态。这可以防止愚蠢的编程错误,否则可能会导致损坏对象(例如,通过设置类的字段进行测试,这可能导致该字段永远不会重置)。此外,通过声明参数为const,函数的用户可以确信他们的对象不会被更改,并且不需要担心调用函数可能产生的副作用。
此外,原因是参数的const仅在函数内部局部应用,因为它正在处理数据的副本。这意味着函数签名实际上是相同的。尽管如此,这样做可能是不好的风格。我个人倾向于仅对引用和指针参数使用const。

0

作为惯例,我们被建议尽可能使用const,因为我们可以从编译器获得很大的帮助,因此每当我们尝试更改常量时,编译器都会捕捉到错误;这是一件好事,可以避免被错误包围。

第二件事:a和b以值传递,因此它们被创建为本地常量,但只要它们不可改变,为什么我们需要副本?

好处是传递一个常量引用,这意味着永远不会更改参数并避免副本(按值复制):
int add(const int& a, const int& b) //现在a和b没有副本,我们也不能修改a和b { return a+b; }

0
int add(const int a, const int b)
{
    return a+b;
}

在这里使用const,以确保函数不会无意中修改a和b的原始值。 在上面的示例中,这样做没有意义。但如果它是像下面这个例子:

int add(const int *a, const int *b)
{
    //*a = 10; //This will throw error. 
    return a+b;
}

在函数中,如果传递了对象、数组或类似的数据类型,使用const是一个很好的方法,可以避免原始数据结构的变异。

对于复合类型,使用const引用而不仅仅是const是一个好的方法。 - Markus Kull

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