OO를 연구하고 있던 젊은 동료는 왜 모든 객체가 기본 유형이나 구조체와 반대되는 참조로 전달되는지 물었습니다. Java 및 C #과 같은 언어의 일반적인 특성입니다.
나는 그에게 좋은 대답을 찾지 못했습니다.
이 디자인 결정의 동기는 무엇입니까? 이 언어의 개발자는 매번 포인터와 typedef를 작성해야하는 데 지치셨습니까?
OO를 연구하고 있던 젊은 동료는 왜 모든 객체가 기본 유형이나 구조체와 반대되는 참조로 전달되는지 물었습니다. Java 및 C #과 같은 언어의 일반적인 특성입니다.
나는 그에게 좋은 대답을 찾지 못했습니다.
이 디자인 결정의 동기는 무엇입니까? 이 언어의 개발자는 매번 포인터와 typedef를 작성해야하는 데 지치셨습니까?
답변:
간단한 답변 :
어딘가에 전달 된 모든 객체의 복제본을 만들고 복사 할 때 메모리 소비
및 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
객체 ( 객체 포함)를 복사 할 수 없었습니다 .
따라서 자연 복제가없는 것과 같은 이유로 사본으로 전달 된 객체는 광기를 생성 합니다. DB 연결을 "복제"할 수 있다고해도 어떻게 닫혀 있는지 어떻게 확인할 수 있습니까?
* 주석 참조-이 "never"문은 모든 속성을 복제 하는 자동 복제 를 의미합니다 . Java는 하나를 제공하지 않았으므로 여기에 나열된 이유로 언어 사용자로서 자신을 작성하는 것은 좋지 않습니다. 일시적이지 않은 필드 만 복제하는 것이 시작이지만 transient
, 적절한 위치 를 정의하는 데 부지런해야합니다 .
clone
것이 좋습니다. "willy-nilly"는 의도적 인 의도없이 모든 속성을 생각하지 않고 복사한다는 의미 입니다. Java 언어 디자이너는 사용자가 만든의 구현을 요구하여 이러한 의도를 강요했습니다 clone
.
객체는 항상 Java로 참조됩니다. 그들은 결코 자신을 지나치지 않습니다.
한 가지 장점은 언어를 단순화한다는 것입니다. 는 C ++ 객체 멤버를 액세스하는 두 다른 연산자를 사용 할 필요가 만드는 값 또는 기준으로서 표현 될 수있다 : .
및 ->
. (이를 통합 할 수없는 이유가 있습니다. 예를 들어, 스마트 포인터는 참조 값이며이를 구별해야 .
합니다 .) Java 만 필요합니다 .
또 다른 이유는 다형성이 가치가 아닌 참조로 이루어져야하기 때문입니다. 값으로 처리되는 객체는 바로 거기에 있으며 고정 된 유형을 갖습니다. C ++에서 이것을 망칠 수 있습니다.
또한 Java는 기본 할당 / 복사 / 무엇이든 전환 할 수 있습니다. C ++에서는 다소간 딥 카피이지만 Java에서는 복사 .clone()
해야 할 경우와 같이 간단한 포인터 할당 / 복사 / 무엇이든 됩니다.
const
다른 객체를 가리 키도록 값을 변경할 수 있습니다. 참조는 객체의 다른 이름이며 NULL 일 수 없으며 재 장착 할 수 없습니다. 일반적으로 간단한 포인터 사용으로 구현되지만 구현 세부 사항입니다.
참조로 전달되는 C # 객체에 대한 초기 설명이 올바르지 않습니다. C #에서 개체는 참조 형식이지만 기본적으로 값 형식과 마찬가지로 값으로 전달됩니다. 참조 유형의 경우 값별 메소드 매개 변수로 복사되는 "값"은 참조 자체이므로 메소드 내부의 특성 변경 사항은 메소드 범위 외부에 반영됩니다.
그러나 메소드 내부에서 매개 변수 변수 자체를 다시 지정하면이 변경 사항이 메소드 범위 외부에 반영되지 않음을 알 수 있습니다. 반대로 실제로 ref
키워드를 사용하여 참조로 매개 변수를 전달하면 이 동작이 예상대로 작동합니다.
빠른 답변
Java 및 유사 언어의 디자이너는 "모든 것이 객체"개념을 적용하기를 원했습니다. 그리고 데이터를 참조로 전달하는 것은 매우 빠르며 많은 메모리를 소비하지 않습니다.
추가 확장 보링 주석
Altougth에서 이러한 언어는 객체 참조 (Java, Delphi, C #, VB.NET, Vala, Scala, PHP)를 사용하지만, 객체 참조는 변장 된 객체를 가리키는 포인터입니다. null 값, 메모리 할당, 객체의 전체 데이터를 복사하지 않고 참조 사본. 모두 객체가 아닌 객체 포인터입니다 !!!
오브젝트 파스칼 (Delphi가 아님), anc C ++ (Java가 아님, C # 아님)에서 오브젝트를 정적 할당 변수로 선언 할 수 있으며 동적 할당 변수를 사용하여 포인터를 사용하여 ( " 설탕 구문 "). 각각의 경우 특정 구문을 사용하므로 Java "및 친구"와 같이 혼동 될 수있는 방법이 없습니다. 이러한 언어에서 객체는 값 또는 참조로 전달 될 수 있습니다.
프로그래머는 포인터 구문이 필요한시기와 필요하지 않은시기를 알고 있지만 Java와 같은 언어에서는 혼동됩니다.
Java가 존재하거나 주류가되기 전에 많은 프로그래머들은 포인터없이 C ++에서 OO를 배우고, 필요할 때 값이나 참조로 전달합니다. 학습에서 비즈니스 앱으로 전환하면 일반적으로 객체 포인터를 사용합니다. QT 라이브러리가 그 좋은 예입니다.
Java를 배웠을 때 모든 것이 객체 개념이라는 것을 따르려고했지만 코딩에 혼란스러워했습니다. 결국 저는 "정적으로 할당 된 객체의 구문을 가진 포인터로 동적으로 할당 된 객체"라고 말하고 다시 코딩하는 데 문제가 없었습니다.
Java와 C #은 저수준 메모리를 제어합니다. 당신이 창조 한 물건들이 상주하는 "힙"은 그 자체의 삶을 산다. 예를 들어, 가비지 수집기는 원할 때마다 개체를 가져옵니다.
프로그램과 "힙"사이에 별도의 간접 계층이 있기 때문에 값과 포인터 (C ++에서와 같이)로 객체를 참조하는 두 가지 방법은 구분할 수 없게됩니다 . 항상 "포인터로"객체를 참조합니다 힙 어딘가에. 그렇기 때문에 이러한 설계 방식은 참조 별 전달을 기본 할당 의미론으로 만듭니다. 자바, C #, 루비 등
위의 명령 언어에만 해당됩니다. 메모리에 대한 제어 위에서 언급 한 언어에서 런타임에 전달하지만, 언어 설계는 말한다 "이봐, 실제로, 거기 이다 메모리, 그리고 거기에 있는 객체는, 그들은 할 메모리를 차지합니다." 기능적 언어는 정의에서 "메모리"개념을 배제함으로써 더욱 추상화됩니다. 저수준 메모리를 제어하지 않는 모든 언어에 참조로 전달이 반드시 적용되는 것은 아닙니다.
몇 가지 이유를 생각할 수 있습니다.
프리미티브 유형을 복사하는 것은 쉽지 않으며 일반적으로 하나의 기계 명령어로 변환됩니다.
객체를 복사하는 것은 쉽지 않습니다. 객체는 객체 자체 인 멤버를 포함 할 수 있습니다. 객체를 복사하면 CPU 시간과 메모리가 비쌉니다. 상황에 따라 객체를 복사하는 방법은 여러 가지가 있습니다.
참조로 객체를 전달하는 것이 저렴하며 객체의 여러 클라이언트간에 객체 정보를 공유 / 업데이트 할 때 편리합니다.
복잡한 데이터 구조 (특히 재귀적인 구조)에는 포인터가 필요합니다. 참조로 객체를 전달하는 것은 포인터를 전달하는보다 안전한 방법입니다.
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 언어에서는 전혀 불가능합니다.
그렇지 않으면 다형성이 없기 때문입니다.
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
즉, 여러 클래스로 구성된 큰 계층 구조가있는 경우 여기에 선언 된대로 오브젝트의 총 크기는 해당 클래스 모두의 조합이됩니다. 분명히, 이러한 객체는 많은 경우에 상당히 클 것입니다.
해결책은 힙에 그것을 작성하고 포인터를 사용하는 것입니다. 이것은 여러 부모를 가진 클래스의 객체 크기가 어떤 의미에서 관리 가능하다는 것을 의미합니다.
이것이 참조를 사용하는 것이 더 바람직한 방법 인 이유입니다.