왜 객체가 참조로 전달됩니까?


31

OO를 연구하고 있던 젊은 동료는 왜 모든 객체가 기본 유형이나 구조체와 반대되는 참조로 전달되는지 물었습니다. Java 및 C #과 같은 언어의 일반적인 특성입니다.

나는 그에게 좋은 대답을 찾지 못했습니다.

이 디자인 결정의 동기는 무엇입니까? 이 언어의 개발자는 매번 포인터와 typedef를 작성해야하는 데 지치셨습니까?


2
Java와 C #에서 왜 값 대신 참조로, 포인터 대신 참조로 매개 변수를 전달해야하는지 묻고 있습니까?
robert

@Robert, "포인터가 아닌 참조"사이에 차이점이 있습니까? 제목이 '왜 객체가 항상 참조인가?'와 같은 제목으로 변경해야한다고 생각하십니까?
Gustavo Cardoso

참조는 포인터입니다.
compman

2
@Anto : Java 참조는 모든면에서 적절하게 사용 된 C 포인터와 동일합니다 (적절하게 사용됨 : 유형 캐스트가 아니며 유효하지 않은 메모리로 설정되지 않고 리터럴로 설정되지 않음).
Zan Lynx

5
또한 실제로 pedantic하기 위해서는 제목이 잘못되었습니다 (적어도 .net에 관한 한). 객체는 참조로 전달되지 않고 참조는 값으로 전달됩니다. 객체를 메소드에 전달하면 참조 값이 메소드 본문 내의 새 참조에 복사됩니다. "객체가 참조로 전달된다"는 것이 부끄러 울 때 일반적인 프로그래머 인용문의 목록에 들어간 것은 부끄러운 일이며 새로운 프로그래머의 시작에 대한 참조에 대한 이해가 부족하다고 생각합니다.
SecretDeveloper

답변:


15

기본적인 이유는 다음과 같습니다.

  • 포인터는 올바른 기술입니다
  • 특정 데이터 구조를 구현하려면 포인터가 필요합니다
  • 메모리를 효율적으로 사용하려면 포인터가 필요합니다
  • 당신은 작업을 수동으로 메모리 색인 필요하지 않은 경우 직접 하드웨어를 사용하고 있지 않습니다.

따라서, 참조.


32

간단한 답변 :

어딘가에 전달 된 모든 객체의 복제본을 만들고 복사 할 때 메모리 소비
CPU 시간을 최소화합니다
.


나는 당신에게 동의하지만, 이것들과 함께 미적 또는 OO 디자인 동기가 있다고 생각합니다.
구스타보 카르도소

9
@ 구스타보 Cardoso : "일부 미적 또는 OO 디자인 동기 부여". 아니요. 단순히 최적화입니다.
S.Lott

6
@ S.Lott : 아니요, 의미 적으로 객체의 복사본을 만들고 싶지 않기 때문에 참조로 전달하는 것이 OO 용어로 의미가 있습니다. 사본이 아닌 오브젝트 를 전달하려고 합니다. 가치를 지날 경우, 객체의 모든 복제본이 더 높은 수준에서 의미가없는 곳에서 생성되어 OO 메타포를 어느 정도 깨뜨립니다.
intuited

@ 구스타보 : 우리는 같은 주장을하고 있다고 생각합니다. 당신은 OOP의 의미론을 언급하고 OOP의 은유를 내 자신의 추가 이유라고 언급합니다. OOP 제작자가 "메모리 소비를 최소화"하고 "CPU 시간을 절약"하는 방식으로 만든 것처럼 보입니다.
Tim

14

C ++에는 값으로 반환 또는 포인터로 반환의 두 가지 주요 옵션이 있습니다. 첫 번째를 보자.

MyClass getNewObject() {
    MyClass newObj;
    return newObj;
}

컴파일러가 반환 값 최적화를 사용하기에 충분히 영리하지 않다고 가정하면 다음과 같이됩니다.

  • newObj는 임시 객체로 구성되어 로컬 스택에 배치됩니다.
  • newObj의 사본이 작성되어 리턴됩니다.

우리는 객체의 사본을 무의미하게 만들었습니다. 이것은 처리 시간 낭비입니다.

포인터로 리턴을 보자.

MyClass* getNewObject() {
    MyClass newObj = new MyClass();
    return newObj;
}

중복 사본을 제거했지만 이제는 또 다른 문제점이 발생했습니다. 힙에 자동으로 파괴되지 않는 오브젝트를 작성했습니다. 우리는 스스로 그것을 처리해야합니다.

MyClass someObj = getNewObject();
delete someObj;

이런 방식으로 할당 된 객체를 삭제하는 책임을 누가 아는 것은 의견이나 규칙에 의해서만 전달 될 수있는 것입니다. 메모리 누수가 쉽게 발생합니다.

이 두 가지 문제를 해결하기위한 많은 해결 방법이 제안되었습니다. 즉, 반환 값 최적화 (컴파일러가 값을 기준으로 중복 복사본을 만들지 않을 정도로 똑똑한 컴파일러), 메서드에 대한 참조 전달 (함수를 기존 객체를 새로 만드는 것이 아니라 스마트 포인터 (소유권 문제를 다루기 위해).

Java / C # 제작자는 특히 언어가 기본적으로 지원하는 경우 항상 참조로 객체를 반환하는 것이 더 나은 솔루션임을 깨달았습니다. 가비지 수집 등과 같이 언어에있는 다른 많은 기능과 연결됩니다.


가치에 의한 반품은 충분히 나쁘지만, 객체에 관해서는 가치에 의한 가치가 훨씬 나쁘며, 이것이 그들이 피하려고했던 실제 문제라고 생각합니다.
메이슨 휠러

유효한 포인트가 있는지 확인하십시오. 그러나 @Mason이 지적한 OO 디자인 문제는 변화의 최종 동기였습니다. 참조를 사용하려는 경우 참조와 값의 차이를 유지할 의미가 없었습니다.
구스타보 카르도소

10

다른 많은 답변에는 좋은 정보가 있습니다. 부분적으로 만 해결 된 복제 에 대한 중요한 점 하나를 추가하고 싶습니다 .

참조를 사용하는 것이 현명합니다. 사물을 복사하는 것은 위험합니다.

다른 사람들이 말했듯이 Java에는 자연스러운 "복제본"이 없습니다. 이것은 단지 누락 된 기능이 아닙니다. 당신은 결코 그냥 다짜고짜 * 사본 (얕은 여부 깊이) 객체의 모든 속성에 원하는 없습니다. 해당 속성이 데이터베이스 연결 인 경우 어떻게됩니까? 사람을 복제 할 수있는 것보다 더 이상 데이터베이스 연결을 "복제"할 수 없습니다. 초기화 가 이유가 있습니다.

딥 카피는 그들 자신의 문제입니다-당신은 정말로 얼마나 깊 습니까? 정적 Class객체 ( 객체 포함)를 복사 할 수 없었습니다 .

따라서 자연 복제가없는 것과 같은 이유로 사본으로 전달 된 객체는 광기를 생성 합니다. DB 연결을 "복제"할 수 있다고해도 어떻게 닫혀 있는지 어떻게 확인할 수 있습니까?


* 주석 참조-이 "never"문은 모든 속성을 복제 하는 자동 복제 를 의미합니다 . Java는 하나를 제공하지 않았으므로 여기에 나열된 이유로 언어 사용자로서 자신을 작성하는 것은 좋지 않습니다. 일시적이지 않은 필드 만 복제하는 것이 시작이지만 transient, 적절한 위치 를 정의하는 데 부지런해야합니다 .


좋은 이의 제기 에서 특정 조건의 복제 의 점프 가 결코 필요 하지 않다는 진술로 점프하는 것을 이해하는 데 어려움이 있습니다. 관련 정적 기능, 아니 IO 또는 열려있는 연결이 문제에서있을 수있는 곳 정확한 중복이 필요했던 곳 내가 상황을 발생했습니다 ... 나는 복제의 위험을 이해하지만, 나는 담요를 볼 수 없습니다 않습니다 .
잉카

2
@ 잉카-당신은 나를 오해하고있을 수 있습니다. 의도적으로 구현하는 clone것이 좋습니다. "willy-nilly"는 의도적 인 의도없이 모든 속성을 생각하지 않고 복사한다는 의미 입니다. Java 언어 디자이너는 사용자가 만든의 구현을 요구하여 이러한 의도를 강요했습니다 clone.
Nicole

불변 개체에 대한 참조를 사용하는 것이 현명합니다. Date와 같은 간단한 값을 변경 가능하게 한 다음 여러 참조를 만드는 것은 아닙니다.
케빈 클라인

@NickC : "무엇을 복제하는 것이 위험한"주된 이유는 Java 및 .net과 같은 언어 / 프레임 워크에는 참조가 변경 가능한 상태, 신원 또는 둘 다를 캡슐화하는지 여부를 선언적으로 표시 할 수단이 없기 때문입니다. 필드에 변경 가능한 상태를 캡슐화하지만 동일성을 캡슐화하지 않는 객체 참조가 포함 된 경우, 객체를 복제하려면 상태를 유지하는 객체가 복제되고 필드에 저장된 해당 복제에 대한 참조가 필요합니다. 참조가 식별 가능하지만 변경 불가능한 상태를 캡슐화하는 경우 사본의 필드는 원본과 동일한 오브젝트를 참조해야합니다.
supercat

깊은 복사 측면이 중요한 포인트입니다. 다른 객체에 대한 참조가 포함 된 객체, 특히 객체 그래프에 변경 가능한 객체가 포함 된 경우 객체를 복사하는 것이 문제가됩니다.
Caleb

4

객체는 항상 Java로 참조됩니다. 그들은 결코 자신을 지나치지 않습니다.

한 가지 장점은 언어를 단순화한다는 것입니다. 는 C ++ 객체 멤버를 액세스하는 두 다른 연산자를 사용 할 필요가 만드는 값 또는 기준으로서 표현 될 수있다 : .->. (이를 통합 할 수없는 이유가 있습니다. 예를 들어, 스마트 포인터는 참조 값이며이를 구별해야 .합니다 .) Java 만 필요합니다 .

또 다른 이유는 다형성이 가치가 아닌 참조로 이루어져야하기 때문입니다. 값으로 처리되는 객체는 바로 거기에 있으며 고정 된 유형을 갖습니다. C ++에서 이것을 망칠 수 있습니다.

또한 Java는 기본 할당 / 복사 / 무엇이든 전환 할 수 있습니다. C ++에서는 다소간 딥 카피이지만 Java에서는 복사 .clone()해야 할 경우와 같이 간단한 포인터 할당 / 복사 / 무엇이든 됩니다.


'(* object)->'를 사용하면 때때로보기 흉한 상태가됩니다.
Gustavo Cardoso

1
C ++이 포인터, 참조 및 값을 구별한다는 점은 주목할 가치가 있습니다. SomeClass *는 객체에 대한 포인터입니다. SomeClass &는 객체에 대한 참조입니다. SomeClass는 값 유형입니다.
Ant

나는 초기 질문에 @Rober를 이미 요청했지만 여기서도 그렇게 할 것입니다 : C ++에서 *와 &의 차이점은 기술 수준이 낮습니다. 그것들은 높은 수준에서 의미 적으로 동일합니까?
구스타보 카르도소

3
@ 구스타보 카르도소 : 차이점은 의미 론적이다. 낮은 기술 수준에서는 일반적으로 동일합니다. 포인터가 객체를 가리 키거나 NULL (정의 된 잘못된 값)입니다. 그렇지 않으면 const다른 객체를 가리 키도록 값을 변경할 수 있습니다. 참조는 객체의 다른 이름이며 NULL 일 수 없으며 재 장착 할 수 없습니다. 일반적으로 간단한 포인터 사용으로 구현되지만 구현 세부 사항입니다.
David Thornley

"다형성은 참조로 수행해야합니다."+1 그것은 대부분의 다른 답변들이 무시했던 엄청나게 중요한 세부 사항입니다.
Doval

4

참조로 전달되는 C # 객체에 대한 초기 설명이 올바르지 않습니다. C #에서 개체는 참조 형식이지만 기본적으로 값 형식과 마찬가지로 값으로 전달됩니다. 참조 유형의 경우 값별 메소드 매개 변수로 복사되는 "값"은 참조 자체이므로 메소드 내부의 특성 변경 사항은 메소드 범위 외부에 반영됩니다.

그러나 메소드 내부에서 매개 변수 변수 자체를 다시 지정하면이 변경 사항이 메소드 범위 외부에 반영되지 않음을 알 수 있습니다. 반대로 실제로 ref키워드를 사용하여 참조로 매개 변수를 전달하면 이 동작이 예상대로 작동합니다.


3

빠른 답변

Java 및 유사 언어의 디자이너는 "모든 것이 객체"개념을 적용하기를 원했습니다. 그리고 데이터를 참조로 전달하는 것은 매우 빠르며 많은 메모리를 소비하지 않습니다.

추가 확장 보링 주석

Altougth에서 이러한 언어는 객체 참조 (Java, Delphi, C #, VB.NET, Vala, Scala, PHP)를 사용하지만, 객체 참조는 변장 된 객체를 가리키는 포인터입니다. null 값, 메모리 할당, 객체의 전체 데이터를 복사하지 않고 참조 사본. 모두 객체가 아닌 객체 포인터입니다 !!!

오브젝트 파스칼 (Delphi가 아님), anc C ++ (Java가 아님, C # 아님)에서 오브젝트를 정적 할당 변수로 선언 할 수 있으며 동적 할당 변수를 사용하여 포인터를 사용하여 ( " 설탕 구문 "). 각각의 경우 특정 구문을 사용하므로 Java "및 친구"와 같이 혼동 될 수있는 방법이 없습니다. 이러한 언어에서 객체는 값 또는 참조로 전달 될 수 있습니다.

프로그래머는 포인터 구문이 필요한시기와 필요하지 않은시기를 알고 있지만 Java와 같은 언어에서는 혼동됩니다.

Java가 존재하거나 주류가되기 전에 많은 프로그래머들은 포인터없이 C ++에서 OO를 배우고, 필요할 때 값이나 참조로 전달합니다. 학습에서 비즈니스 앱으로 전환하면 일반적으로 객체 포인터를 사용합니다. QT 라이브러리가 그 좋은 예입니다.

Java를 배웠을 때 모든 것이 객체 개념이라는 것을 따르려고했지만 코딩에 혼란스러워했습니다. 결국 저는 "정적으로 할당 된 객체의 구문을 가진 포인터로 동적으로 할당 된 객체"라고 말하고 다시 코딩하는 데 문제가 없었습니다.


3

Java와 C #은 저수준 메모리를 제어합니다. 당신이 창조 한 물건들이 상주하는 "힙"은 그 자체의 삶을 산다. 예를 들어, 가비지 수집기는 원할 때마다 개체를 가져옵니다.

프로그램과 "힙"사이에 별도의 간접 계층이 있기 때문에 값과 포인터 (C ++에서와 같이)로 객체를 참조하는 두 가지 방법은 구분할 수 없게됩니다 . 항상 "포인터로"객체를 참조합니다 힙 어딘가에. 그렇기 때문에 이러한 설계 방식은 참조 별 전달을 기본 할당 의미론으로 만듭니다. 자바, C #, 루비 등

위의 명령 언어에만 해당됩니다. 메모리에 대한 제어 위에서 언급 한 언어에서 런타임에 전달하지만, 언어 설계는 말한다 "이봐, 실제로, 거기 이다 메모리, 그리고 거기에 있는 객체는, 그들은 메모리를 차지합니다." 기능적 언어는 정의에서 "메모리"개념을 배제함으로써 더욱 추상화됩니다. 저수준 메모리를 제어하지 않는 모든 언어에 참조로 전달이 반드시 적용되는 것은 아닙니다.


2

몇 가지 이유를 생각할 수 있습니다.

  • 프리미티브 유형을 복사하는 것은 쉽지 않으며 일반적으로 하나의 기계 명령어로 변환됩니다.

  • 객체를 복사하는 것은 쉽지 않습니다. 객체는 객체 자체 인 멤버를 포함 할 수 있습니다. 객체를 복사하면 CPU 시간과 메모리가 비쌉니다. 상황에 따라 객체를 복사하는 방법은 여러 가지가 있습니다.

  • 참조로 객체를 전달하는 것이 저렴하며 객체의 여러 클라이언트간에 객체 정보를 공유 / 업데이트 할 때 편리합니다.

  • 복잡한 데이터 구조 (특히 재귀적인 구조)에는 포인터가 필요합니다. 참조로 객체를 전달하는 것은 포인터를 전달하는보다 안전한 방법입니다.


1

그렇지 않으면 함수는 전달 된 모든 종류의 객체에 대해 (분명히 깊은) 사본을 자동으로 만들 수 있어야합니다. 그리고 일반적으로 그것을 추측 할 수 없습니다. 따라서 모든 객체 / 클래스에 대한 복사 / 복제 방법 구현을 정의해야합니다.


얕은 사본을 만들고 실제 값과 다른 객체에 대한 포인터를 유지할 수 있습니까?
구스타보 카르도소

#Gustavo Cardoso,이 객체를 통해 다른 객체를 수정할 수 있다면 참조로 전달되지 않은 객체에서 기대할 수있는 것입니까?
David

0

Java는 더 나은 C ++로 설계되었고 C #은 더 나은 Java로 설계 되었기 때문에 이러한 언어의 개발자는 객체가 값 유형 인 근본적으로 손상된 C ++ 객체 모델에 지쳤습니다.

객체 지향 프로그래밍의 세 가지 기본 원칙 중 두 가지는 상속과 다형성이며 객체를 참조 유형 대신 값 유형으로 처리하면 둘 다 혼란을 겪습니다. 함수에 객체를 매개 변수로 전달할 때 컴파일러는 전달할 바이트 수를 알아야합니다. 객체가 참조 유형 인 경우 대답은 간단합니다. 포인터의 크기는 모든 객체에 동일합니다. 그러나 객체가 값 유형 인 경우 실제 크기 값을 전달해야합니다. 파생 클래스는 새로운 필드를 추가 할 수 있기 때문에 sizeof (derived)! = sizeof (base)를 의미하며 다형성이 창 밖으로 나옵니다.

다음은 문제를 보여주는 간단한 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 객체를 전달하는 대신 Child 객체의 Parent 부분 사본 . 이와 같이 숨겨진 의미 론적 문제는 C ++에서 값으로 객체를 전달하는 것을 피해야하며 거의 모든 다른 OO 언어에서는 전혀 불가능합니다.


아주 좋은 지적입니다. 그러나 문제를 해결하는 데 상당한 노력이 필요하기 때문에 반환 문제에 중점을 두었습니다. 이 프로그램은 단일 앰퍼샌드를 추가하여 수정 될 수 있습니다. void foo (Parent & a)
Ant

OO는 포인터없이 제대로 작동하지 않습니다
Gustavo Cardoso

-1.000000000000
P Shved

5
Java는 값에 따라 전달 된다는 것을 기억하는 것이 중요 합니다 ( 프리미티브는 순수하게 값으로 전달되는 반면 값 은 오브젝트 참조 를 전달합니다).
Nicole

3
@Pavel Shved- "둘이 하나보다 낫다!" 다시 말해서, 더 많은 밧줄이 당신을 만날 수 있습니다.
Nicole

0

그렇지 않으면 다형성이 없기 때문입니다.

OO 프로그래밍에서는 하나 Derived에서 더 큰 클래스를 만든 Base다음 Base하나를 기대하는 함수에 전달할 수 있습니다. 아주 사소한 어?

함수 인수의 크기는 고정되어 컴파일 타임에 결정됩니다. 당신은 당신이 원하는 모든 것을 주장 할 수 있습니다.

이제 컴퓨터에는 잘 정의 된 데이터가 있습니다. 메모리 셀의 주소는 일반적으로 하나 또는 두 개의 "워드"로 표시됩니다. 프로그래밍 언어에서 포인터 또는 참조로 볼 수 있습니다.

따라서 임의 길이의 객체를 전달하려면 가장 간단한 방법은이 객체에 대한 포인터 / 참조를 전달하는 것입니다.

이것은 OO 프로그래밍의 기술적 한계입니다.

그러나 큰 유형의 경우 일반적으로 복사를 피하기 위해 어쨌든 참조를 전달하는 것을 선호하므로 일반적으로 큰 타격으로 간주되지 않습니다. :)

Java 또는 C #에서는 객체를 메소드에 전달할 때 메소드가 객체를 수정할지 여부를 모릅니다. 디버깅 / 병렬화가 더 어려워지며 이는 기능 언어와 투명 참조가 해결하려고하는 문제입니다.


-1

대답은 이름에 있습니다 (거의 어쨌든). 참조 (주소와 같은)는 단지 다른 것을 의미하며, 값은 다른 것의 또 다른 사본입니다. 누군가 다음과 같은 효과에 대해 언급했을 것이라고 확신하지만 둘 중 하나가 적합하지 않은 상황이있을 것입니다 (메모리 보안 대 메모리 효율). 메모리, 메모리, 메모리 관리에 관한 모든 것입니다. MEMORY! :디


-2

자, 이것이 정확히 객체가 참조 유형이거나 참조로 전달되는 이유라고 말하지는 않지만 이것이 장기적으로 매우 좋은 아이디어의 예를 보여줄 수 있습니다.

내가 실수하지 않으면 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

즉, 여러 클래스로 구성된 큰 계층 구조가있는 경우 여기에 선언 된대로 오브젝트의 총 크기는 해당 클래스 모두의 조합이됩니다. 분명히, 이러한 객체는 많은 경우에 상당히 클 것입니다.

해결책은 힙에 그것을 작성하고 포인터를 사용하는 것입니다. 이것은 여러 부모를 가진 클래스의 객체 크기가 어떤 의미에서 관리 가능하다는 것을 의미합니다.

이것이 참조를 사용하는 것이 더 바람직한 방법 인 이유입니다.


1
이것은 이전의 13 가지 답변에서 제시하고 설명한 내용에 비해 실질적인 내용을 제공하지 않는 것 같습니다
gnat
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.