7

编辑:这个问题更多的是关于语言工程而不是 C++ 本身。我以 C++ 为例来展示我想要的东西,主要是因为我每天都在使用它。我不想知道它在 C++ 上是如何工作的,但我想就如何完成它展开讨论

这不是它现在的工作方式,这是我希望它可以完成的方式,这肯定会破坏 C 的兼容性,但这就是我认为 extern "C" 的全部意义所在。

我的意思是,在您现在声明的每个函数或方法中,您必须明确写入该对象将通过在其上加上引用运算符的引用发送。我希望每一个非 POD 类型都可以通过引用自动发送,因为我经常使用它,实际上对于大小超过 32 位的每个对象,这几乎是我的每个类。

让我们举例说明一下现在的情况,假设abc是类:

类示例{
    上市:
        int just_use_a(const a &object);
        int use_and_mess_with_b(b &object);
        无效 do_nothing_on_c(c 对象);
};

现在我希望:

类示例{
    上市:
        int just_use_a(const a object);
        int use_and_mess_with_b(b 对象);
        extern "C" void do_nothing_on_c(c object);
};

现在,do_nothing_on_c() 可以像今天一样运行。

这至少对我来说会很有趣,感觉更清楚,而且如果你知道每个非 POD 参数都是通过引用来的,我相信如果你必须明确声明它,错误将是相同的。

这种变化的另一个观点,来自 C 的人,在我看来,引用运算符是一种获取变量address的方法,这就是我用来获取指针的方式。我的意思是,它是同一个运算符,但在不同的上下文中具有不同的语义,这对您来说是不是也有点不对劲?

4

9 回答 9

15

我猜你错过了 C++ 和 C++ 语义的重点。您错过了C++ 通过值传递(几乎)所有内容是正确的事实,因为它是在 C 中完成的方式。 Always。但不仅在 C 中,我将在下面向您展示...

C上的参数语义

在 C 中,一切都是按值传递的。“primitives”和“PODs”通过复制它们的值来传递。在你的函数中修改它们,原来的不会被修改。尽管如此,复制一些 POD 的成本可能并不小。

当您使用指针表示法(*)时,您不会通过引用传递。您正在传递地址的副本。这或多或少是相同的,只有一个细微的区别:

typedef struct { int value ; } P ;

/* p is a pointer to P */
void doSomethingElse(P * p)
{
   p->value = 32 ;
   p = malloc(sizeof(P)) ; /* Don't bother with the leak */
   p->value = 45 ;
}

void doSomething()
{
   P * p = malloc(sizeof(P)) ;
   p->value = 25 ;

   doSomethingElse(p) ;

     int i = p->value ;
   /* Value of p ? 25 ? 32 ? 42 ? */
}

p->value 的最终值是 32。因为 p 是通过复制地址的值来传递的。所以原来的 p 没有被修改(并且新的 p 被泄露了)。

Java 和 C Sharp 上的参数语义

有些人可能会感到惊讶,但在 Java 中,所有内容也是按值复制的。上面的 C 示例将在 Java 中给出完全相同的结果。这几乎是您想要的,但您无法像在 C 中那样轻松地“通过引用/指针”传递原始数据。

在 C# 中,他们添加了“ref”关键字。它的工作方式或多或少类似于 C++ 中的参考。关键是,在 C# 中,您必须在函数声明和每次调用中都提及它。我想这又不是你想要的。

C++ 上的参数语义

在 C++ 中,几乎所有内容都是通过复制值来传递的。当您只使用符号的类型时,您正在复制符号(就像在 C 中所做的那样)。这就是为什么当您使用 * 时,您传递的是符号地址的副本。

但是,当您使用 & 时,假设您传递的是真实对象(无论是结构、int、指针等):引用。

很容易将其误认为是语法糖(即,在幕后,它像指针一样工作,生成的代码与用于指针的代码相同)。但...

事实是,引用不仅仅是语法糖。

  • 与指针不同,它授权像在堆栈上一样操作对象。
  • Unline 指针,当与 const 关键字相关联时,它授权从一种类型到另一种类型的隐式提升(主要通过构造函数)。
  • 与指针不同,符号不应为 NULL/无效。
  • 与“按副本”不同,您不会花费无用的时间来复制对象
  • 与“by-copy”不同,您可以将其用作 [out] 参数
  • 与“by-copy”不同,您可以在 C++ 中使用全方位的 OOP(即,您将完整的对象传递给等待接口的函数)。

因此,引用具有两全其美的优点。

让我们看看 C 示例,但在 doSomethingElse 函数上有一个 C++ 变体:

struct P { int value ; } ;

// p is a reference to a pointer to P
void doSomethingElse(P * & p)
{
   p->value = 32 ;
   p = (P *) malloc(sizeof(P)) ; // Don't bother with the leak
   p->value = 45 ;
}

void doSomething()
{
   P * p = (P *) malloc(sizeof(P)) ;
   p->value = 25 ;

   doSomethingElse(p) ;

     int i = p->value ;
   // Value of p ? 25 ? 32 ? 42 ?
}

结果是 42,旧的 p 被泄露,被新的 p 代替。因为,与 C 代码不同,我们传递的不是指针的副本,而是指针的引用,即指针本身。

使用 C++ 时,上面的示例必须非常清楚。如果不是,那么你错过了一些东西。

结论

C++ 是按复制/值传递的,因为它是一切工作的方式,无论是在 C、C# 还是在 Java 中(甚至在 JavaScript 中...... :-p ...)。和 C# 一样,C++ 有一个引用运算符/关键字,作为奖励

现在,据我了解,您可能正在做我所说的半开玩笑的C+,即具有一些有限 C++ 功能的 C。

也许您的解决方案是使用 typedefs(不过,它会激怒您的 C++ 同事,看到代码被无用的 typedefs 污染......),但这样做只会混淆您确实在那里缺少 C++ 的事实。

正如在另一篇文章中所说,您应该将您的思维方式从 C 开发(无论什么)转变为 C++ 开发,或者您可能应该转向另一种语言。但是不要继续使用 C++ 特性以 C 方式编程,因为通过有意识地忽略/混淆您使用的习语的力量,您将生成次优代码。

注意:并且不要通过复制除原语之外的任何其他内容。你会从它的面向对象能力中阉割你的函数,而在 C++ 中,这不是你想要的。

编辑

问题有所修改(请参阅https://stackoverflow.com/revisions/146271/list)。我让我原来的答案,并回答下面的新问题。

您如何看待 C++ 上的默认传递引用语义?就像您说的那样,它会破坏兼容性,并且您将对原语(即内置类型,仍将通过副本传递)和结构/对象(将作为引用传递)进行不同的传递。您必须添加另一个运算符来表示“按值传递”(外部“C”非常糟糕,并且已经用于其他完全不同的东西)。不,我真的很喜欢今天用 C++ 完成的方式。

[...] 在我看来,引用运算符是一种获取变量地址的方法,这就是我用来获取指针的方法。我的意思是,它是同一个运算符,但在不同的上下文中具有不同的语义,这对您来说是不是也有点不对劲?是和不是。运算符 >> 在与 C++ 流一起使用时也改变了它的语义。然后,您可以使用运算符 += 替换 strcat。我猜运算符 & 之所以被使用是因为它的含义是“与指针相反”,并且因为他们不想使用另一个符号(ASCII 是有限的,范围运算符 :: 以及指针 -> 表明很少有其他符号可用)。但是现在,如果 & 困扰你,&& 真的会让你感到不安,因为他们在 C++0x 中添加了一个一元 && (一种超级引用......)。我自己还没消化...

于 2008-09-28T18:41:07.163 回答
10

完全改变一段代码含义的编译器选项对我来说听起来是个坏主意。要么习惯 C++ 语法,要么找到不同的语言。

于 2008-09-28T17:31:27.457 回答
2

我宁愿不再通过使每个(非限定)参数成为引用来滥用引用。

将引用添加到 C++ 的主要原因是支持运算符重载;如果你想要“按引用传递”语义,C 有一个非常合理的方法:指针。

使用指针可以清楚地表明您更改指向对象的值的意图,并且只需查看函数调用就可以看到这一点,您不必查看函数声明来查看它是否使用引用。

另见

我确实想更改参数,我应该使用指针还是应该使用引用?我不知道一个强有力的逻辑原因。如果传递“不是对象”(例如空指针)是可以接受的,那么使用指针是有意义的。我个人的风格是在我想修改对象时使用指针,因为在某些情况下,这样更容易发现可以进行修改。

来自同一个常见问题解答

于 2008-09-28T17:45:02.790 回答
1

老实说,我认为整个 C++ 中的值传递/引用传递思想具有误导性。 一切都是价值传递。你有三种情况:

  1. 在哪里传递变量的本地副本

    void myFunct(int cantChangeMyValue)
    
  2. 将指针的本地副本传递给变量的位置

    void myFunct(int* cantChangeMyAddress) {
        *cantChangeMyAddress = 10;
    }
    
  3. 您传递“引用”的地方,但是通过编译器魔术,就好像您传递了一个指针并且每次都简单地取消引用它。

    void myFunct(int & hereBeMagic) {
        hereBeMagic = 10; // same as 2, without the dereference
    }
    

我个人觉得记住一切都是按价值传递的,这不会令人困惑。在某些情况下,该值可能是一个地址,它允许您更改函数之外的内容。

您的建议不允许程序员做第一件事。我个人认为取消该选项是个坏主意。C/C++ 的一个主要优点是具有细粒度的内存管理。让一切都通过引用传递只是试图让 C++ 更像 Java。

于 2008-09-28T17:20:22.677 回答
1

是的,我认为这是一个非常令人困惑的超载。

这是微软不得不说的情况:

不要将引用声明与地址操作符的使用混淆。当 & identifier 前面有一个类型(例如 int 或 char)时,则 identifier 被声明为对该类型的引用。当 & 标识符前面没有类型时,用法是地址运算符的用法。

我对 C 或 C++ 不是很擅长,但我更头疼的是在两种语言中整理 * 和 & 的各种用途,而不是在汇编程序中编码。

于 2008-09-28T17:21:58.743 回答
1

最好的建议是养成思考你真正想要发生的事情的习惯。当您没有复制构造函数(或不想使用它)时,通过引用传递很好,并且对于大型对象来说更便宜。但是,然后在类之外感觉到参数的突变。您可以改为通过const引用传递 - 然后没有突变,但您不能进行本地修改。为在函数中应该是只读的廉价对象传递 const 按值,当您想要一个可以进行本地修改的副本时,传递非 const 按值。

每个排列(按值/按引用和 const/non-const)都具有绝对不等价的重要差异。

于 2008-09-28T17:34:57.527 回答
1

当您按值传递时,您正在将数据复制到堆栈中。如果您为要传递的结构或类定义了 operator=,它将被执行。我知道没有编译器指令可以消除所提议的更改固有地导致的隐式语言混乱的繁琐。

一个常见的最佳实践是通过 const 引用传递值,而不仅仅是通过引用。这确保了在调用函数中不能更改值。这是 const 正确代码库的元素之一。

一个完全 const 正确的代码库更进一步,将 const 添加到原型的末尾。考虑:

void Foo::PrintStats( void ) const {
   /* Cannot modify Foo member variables */
}

void Foo::ChangeStats( void ) {
   /* Can modify foo member variables */
}

如果要将 Foo 对象传递给以 const 为前缀的函数,则可以调用 PrintStats()。编译器会在调用 ChangeStats() 时出错。

void ManipulateFoo( const Foo &foo )
{
    foo.PrintStats();  // Works
    foo.ChangeStats(); // Oops; compile error
}
于 2008-09-28T17:42:51.080 回答
0

有什么不清楚的。当你说:

int b(b ¶m);

你对第二个“b”有什么打算?你忘了介绍类型吗?您是否忘记对第一个“b”进行不同的书写?你不认为写这样的东西很清楚:

class B{/*something...*/};
int b(B& param);

从现在开始,我想你的意思是我写的。

现在,您的问题是“您不认为编译器将非 POD 的每个按值传递视为传递引用会更好吗?”。第一个问题是它会破坏你的合同。我想你的意思是通过 CONST 引用,而不仅仅是通过引用。

您现在的问题已简化为:“您知道是否有一些编译器指令可以按值优化函数调用吗?”

现在的答案是“我不知道”。

于 2008-09-28T18:36:01.310 回答
0

我认为如果你开始混合所有类型的可用参数以及它们的 const 变化,c++ 会变得非常混乱。

跟踪所有复制构造函数调用、所有重载的取消引用等等,它很快就会失控。

于 2008-09-29T19:35:11.457 回答