一个正在研究面向对象的年轻同事问我为什么每个对象都通过引用传递,这与原始类型或结构相反。它是Java和C#等语言的共同特征。
我找不到适合他的答案。
此设计决定的动机是什么?这些语言的开发人员是否厌倦了每次都必须创建指针和typedef的麻烦?
一个正在研究面向对象的年轻同事问我为什么每个对象都通过引用传递,这与原始类型或结构相反。它是Java和C#等语言的共同特征。
我找不到适合他的答案。
此设计决定的动机是什么?这些语言的开发人员是否厌倦了每次都必须创建指针和typedef的麻烦?
Answers:
基本原因可以归结为:
因此,参考。
简单答案:
在重新创建并对传递到某处的每个对象进行深层复制时,最大程度地减少内存消耗
和
CPU时间。
在C ++中,有两个主要选项:按值返回或按指针返回。让我们看第一个:
MyClass getNewObject() {
MyClass newObj;
return newObj;
}
假设您的编译器不够聪明,无法使用返回值优化,那么这是怎么回事:
我们毫无意义地制作了对象的副本。这浪费了处理时间。
让我们看一下按指针返回:
MyClass* getNewObject() {
MyClass newObj = new MyClass();
return newObj;
}
我们已经消除了冗余副本,但是现在又引入了另一个问题:我们在堆上创建了一个不会自动销毁的对象。我们必须自己处理:
MyClass someObj = getNewObject();
delete someObj;
知道谁负责删除以这种方式分配的对象是只能通过注释或约定来传达的信息。它很容易导致内存泄漏。
建议了许多解决方法来解决这两个问题-返回值优化(在这种情况下,编译器足够聪明,不能在按值返回的情况下创建冗余副本),将引用传递给方法(因此函数会注入到现有对象,而不是创建一个新对象),智能指针(这样,所有权问题就没有意义了)。
Java / C#创建者意识到,始终通过引用返回对象是一个更好的解决方案,特别是如果该语言本身支持该对象。它与语言所具有的许多其他功能相关联,例如垃圾收集等。
许多其他答案都有很好的信息。我想补充一点关于克隆的要点的但这仅部分得到解决。
使用引用很聪明。复制东西很危险。
正如其他人所说,在Java中,没有自然的“克隆”。 这不仅仅是缺少的功能。 您永远都不想只随意地复制对象中的每个属性(无论是浅层还是深层)。如果该属性是数据库连接怎么办?您不仅可以克隆一个数据库连接,还可以克隆一个人。 存在初始化是有原因的。
深拷贝是他们自己的问题-您真正走了多深?您绝对不能复制任何静态的内容(包括任何Class
对象)。
因此,出于同样的原因,也就没有自然的克隆,作为副本传递的对象会造成精神错乱。即使您可以“克隆”数据库连接-现在如何确保它已关闭?
*请参阅注释-通过此“从不”语句,我的意思是自动克隆每个属性。Java没有提供Java语言,对于您作为使用该语言的用户来说,创建Java语言可能不是一个好主意。仅克隆非瞬态字段将是一个开始,但是即使那样,您也需要认真地定义transient
适当的位置。
clone
是可以的。“故意”意味着复制所有属性而不考虑它-没有故意的意图。Java语言设计人员通过要求用户创建的实现来强制实现这一意图clone
。
对象始终以Java引用。他们永远不会绕过自己。
一个优点是,这简化了语言。C ++对象可以表示为值或引用,这就需要使用两个不同的运算符来访问成员: .
和->
。(有一些原因不能将其合并;例如,智能指针是作为引用的值,并且必须使其与众不同。)Java仅需要.
。
另一个原因是多态性必须通过引用而不是值来完成;按值处理的对象就在那里,并且具有固定的类型。可以用C ++搞定。
而且,Java可以切换默认的分配/复制/任何内容。在C ++中,它是一个或多或少的深层副本,而在Java中,它是一个简单的指针赋值/复制/任何操作,并带有.clone()
诸如此类,以防您需要复制。
const
,否则可以将其值更改为指向其他对象。引用是对象的另一个名称,不能为NULL,也不能重新放置。它通常是通过简单使用指针来实现的,但这是实现的细节。
快速回答
Java和类似语言的设计人员希望应用“一切都是对象”的概念。而且,将数据作为参考传递非常快,并且不会占用太多内存。
额外的无聊评论
通俗地说,这些语言使用对象引用(Java,Delphi,C#,VB.NET,Vala,Scala,PHP),事实是对象引用是伪装成对象的指针。空值,内存分配,引用的副本而不复制对象的全部数据,所有这些都是对象指针,而不是普通对象!
在对象Pascal(不是Delphi)和c C ++(不是Java,不是C#)中,可以通过使用指针将对象声明为静态分配的变量,也可以将其声明为动态分配的变量(“糖语法”)。每个案例都使用特定的语法,并且没有像Java“和朋友”中那样令人困惑的方法。在这些语言中,对象既可以作为值也可以作为引用传递。
程序员知道什么时候需要指针语法,什么时候不需要指针语法,但是在Java和类似语言中,这很令人困惑。
在Java出现或成为主流之前,许多程序员在C ++中没有指针地学习了面向对象,而是在需要时通过值或引用进行传递。从学习切换到业务应用程序时,它们通常使用对象指针。QT库就是一个很好的例子。
当我学习Java时,我试图遵循一切都是对象的概念,但是对编码感到困惑。最终,我说:“好,这是一个使用静态分配的对象的语法动态分配了指针的对象”,并且再次编写代码没有麻烦。
Java和C#确实可以控制您的低级内存。您创建的对象所在的“堆”拥有自己的生活;例如,垃圾收集器会根据需要收获对象。
由于您的程序和该“堆”之间存在单独的间接层,因此通过值和指针(如C ++)引用对象的两种方式变得难以区分。:您始终通过“指针”引用对象。在堆的某个地方。这就是为什么这种设计方法将按引用传递作为赋值的默认语义的原因。Java,C#,Ruby等。
以上仅涉及命令式语言。在对内存的控制上面提到的语言传递给运行,但语言的设计也说:“哎,但实际上,还有就是内存,并且有是的对象,他们做占用内存”。通过从其定义中排除“内存”的概念,功能语言甚至可以进一步抽象。这就是为什么按引用传递不一定适用于您不控制低级内存的所有语言的原因。
因为否则,该函数应该能够自动创建传递给它的任何类型的对象的(显然很深的)副本。通常情况下,它无法猜测出来。因此,您将必须为所有对象/类定义复制/克隆方法的实现。
因为Java被设计为更好的C ++,而C#被设计为更好的Java,并且这些语言的开发人员对根本破坏了C ++对象模型(对象是值类型)感到厌倦。
面向对象编程的三个基本原理中的两个是继承和多态性,将对象视为值类型而不是引用类型会给这两者造成严重破坏。当您将对象作为参数传递给函数时,编译器需要知道要传递多少字节。当您的对象是引用类型时,答案很简单:指针的大小,所有对象都相同。但是,当您的对象是值类型时,它必须传递值的实际大小。由于派生类可以添加新字段,因此这意味着sizeof(派生)!= sizeof(基数),多态性超出了人们的视野。
这是一个演示问题的普通C ++程序:
#include <iostream>
class Parent
{
public:
int a;
int b;
int c;
Parent(int ia, int ib, int ic) {
a = ia; b = ib; c = ic;
};
virtual void doSomething(void) {
std::cout << "Parent doSomething" << std::endl;
}
};
class Child : public Parent {
public:
int d;
int e;
Child(int id, int ie) : Parent(1,2,3) {
d = id; e = ie;
};
virtual void doSomething(void) {
std::cout << "Child doSomething : D = " << d << std::endl;
}
};
void foo(Parent a) {
a.doSomething();
}
int main(void)
{
Child c(4, 5);
foo(c);
return 0;
}
该程序的输出不同于任何理智的OO语言中的等效程序的输出,因为您无法将按值派生的对象传递给需要基础对象的函数,因此编译器将创建一个隐藏的副本构造函数并传递Child对象的Parent部分的副本,而不是像您告诉的那样传递Child对象。像这样的隐藏语义陷阱就是为什么在C ++中应避免按值传递对象,而在几乎所有其他OO语言中根本不可能实现。
因为否则就不会有多态性。
在OO编程中,您可以Derived
从一个类中创建一个更大的类Base
,然后将其传递给期望一个类的函数Base
。太琐碎了吧?
函数的参数大小是固定的,并且在编译时确定。您可以争论所有您想要的,可执行代码是这样,并且语言需要在某一点或另一点执行(纯解释语言不受此限制...)
现在,计算机上已经定义了一个数据:存储单元的地址,通常表示为一个或两个“字”。它可以作为指针或引用在编程语言中看到。
因此,为了传递任意长度的对象,最简单的方法是将指针/引用传递给该对象。
这是OO编程的技术限制。
但是由于对于大型类型,您通常还是喜欢传递引用以避免复制,因此通常不认为这是一个重大打击:)
但是,在Java或C#中,有一个重要的后果,就是将对象传递给方法时,您不知道对象是否会被该方法修改。这使得调试/并行化变得更加困难,这就是功能语言和透明引用正在尝试解决的问题->复制并不是那么糟糕(在有意义的时候)。
好吧,所以我并不是要说这就是为什么对象是引用类型或通过引用传递的原因,但是我可以举一个例子说明为什么从长远来看这是一个非常好的主意。
如果我没记错的话,当您在C ++中继承一个类时,该类的所有方法和属性实际上都会复制到子类中。就像在子类中再次编写该类的内容一样。
因此,这意味着子类中数据的总大小是父类和派生类中内容的组合。
EG:#include
class Top
{
int arrTop[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};
class Middle : Top
{
int arrMiddle[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};
class Bottom : Middle
{
int arrBottom[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};
int main()
{
using namespace std;
int arr[20];
cout << "Size of array of 20 ints: " << sizeof(arr) << endl;
Top top;
Middle middle;
Bottom bottom;
cout << "Size of Top Class: " << sizeof(top) << endl;
cout << "Size of middle Class: " << sizeof(middle) << endl;
cout << "Size of bottom Class: " << sizeof(bottom) << endl;
}
这将向您显示:
Size of array of 20 ints: 80
Size of Top Class: 80
Size of middle Class: 160
Size of bottom Class: 240
这意味着,如果您具有多个类的大型层次结构,则此处声明的对象的总大小将是所有这些类的组合。显然,这些对象在很多情况下会很大。
我认为,解决方案是在堆上创建它并使用指针。从某种意义上讲,这意味着具有几个父类的类的对象大小是可以控制的。
这就是为什么使用引用将是更可取的方法。