왜 C와 C ++는 구조체 내에서 멤버 별 배열 할당을 지원하지만 일반적으로 지원하지 않습니까?


87

멤버 별 배열 할당이 지원되지 않으므로 다음이 작동하지 않음을 이해합니다.

int num1[3] = {1,2,3};
int num2[3];
num2 = num1; // "error: invalid array assignment"

나는 이것을 사실로 받아 들였고, 언어의 목적은 개방형 프레임 워크를 제공하는 것이고 사용자가 배열 복사와 같은 것을 구현하는 방법을 결정하게하는 것이라고 생각했습니다.

그러나 다음은 작동합니다.

struct myStruct { int num[3]; };
struct myStruct struct1 = {{1,2,3}};
struct myStruct struct2;
struct2 = struct1;

배열 num[3]은의 인스턴스에서의 인스턴스로 멤버별로 할당 struct1됩니다 struct2.

구조체에 대해 멤버 별 배열 할당이 지원되지만 일반적으로 지원되지 않는 이유는 무엇입니까?

편집 : struct의 std :: string 스레드에서 Roger Pate 의 코멘트 -복사 / 할당 문제? 대답의 일반적인 방향을 가리키는 것 같지만 직접 확인할만큼 충분히 알지 못합니다.

편집 2 : 많은 훌륭한 응답. 나는 주로 행동의 철학적 또는 역사적 근거에 대해 궁금해했기 때문에 Luther Blissett을 선택 했지만 관련 사양 문서에 대한 James McNellis 의 참조도 유용했습니다.


6
이것은 C에서 비롯 되었기 때문에 C와 C ++를 모두 태그로 사용하고 있습니다. 또한 좋은 질문입니다.
GManNickG 2010 년

4
오래 전에 C에서 구조 할당이 일반적으로 가능하지 않았고 사용 memcpy()하거나 유사 하게 사용해야한다는 점에 주목할 가치가 있습니다 .
ggg

약간의 참고로 ... boost::array( boost.org/doc/libs/release/doc/html/array.html ) 그리고 지금 std::array( en.cppreference.com/w/cpp/container/array )은 STL 호환 대안입니다. 지저분한 오래된 C 배열. 복사 할당을 지원합니다.
Emile Cormier 2013

@EmileCormier 그리고 그들은-타다! -배열 주변의 구조.
Peter-Monica 복원

답변:


46

여기에 내 의견이 있습니다.

C 언어의 개발은 C 에서 배열 유형의 진화에 대한 통찰력을 제공합니다.

배열에 대해 간략히 설명하겠습니다.

C의 선구자 B와 BCPL에는 다음과 같은 선언과 같은 고유 한 배열 유형이 없습니다.

auto V[10] (B)
or 
let V = vec 10 (BCPL)

V는 메모리 10 "워드"의 사용되지 않는 영역을 가리 키도록 초기화되는 (유형이 지정되지 않은) 포인터로 선언됩니다. B는 이미 *포인터 역 참조에 사용 되었으며 오늘날 C / C ++에서 [] 와 *(V+i)같이을 의미 하는 약칭 표기법 V[i]을 사용했습니다. 그러나 V배열이 아니라 일부 메모리를 가리켜 야하는 포인터입니다. Dennis Ritchie가 구조체 유형으로 B를 확장하려고 할 때 문제가 발생했습니다. 그는 오늘날 C 에서처럼 배열이 구조체의 일부가되기를 원했습니다.

struct {
    int inumber;
    char name[14];
};

그러나 포인터로서 배열의 B, BCPL 개념을 사용하면 런타임 에 구조체 내 14 바이트의 메모리 영역에 대해 초기화 해야하는 name포인터를 필드에 포함해야했습니다 . 초기화 / 레이아웃 문제는 결국 배열에 특별한 처리를 제공함으로써 해결되었습니다. 컴파일러는 배열을 포함하는 표현식을 제외하고는 데이터에 대한 포인터를 실제로 요구하지 않고도 구조, 스택 등에서 배열의 위치를 ​​추적합니다. 이 처리를 통해 거의 모든 B 코드가 계속 실행될 수 있으며 "배열을 보면 포인터로 변환" 규칙 의 소스가 됩니다. 개방형 크기 등의 배열을 허용했기 때문에 매우 편리한 것으로 판명 된 호환성 해킹입니다.

그리고 배열을 할당 할 수없는 이유는 다음과 같습니다. 배열은 B의 포인터 였으므로 간단히 다음과 같이 작성할 수 있습니다.

auto V[10];
V=V+5;

"배열"을 리베이스합니다. 배열 변수의 기본이 더 이상 lvalue가 아니기 때문에 이것은 이제 의미가 없습니다. 따라서이 할당이 허용되지 않아 선언 된 배열을 기반으로 한 몇 가지 프로그램을 포착하는 데 도움이 되었습니다.. 그리고이 개념이 고착되었습니다. 배열은 C 유형 시스템에서 인용 된 일급 클래스로 설계되지 않았기 때문에 대부분 사용하면 포인터가되는 특수한 짐승으로 취급되었습니다. 그리고 특정 관점 (C- 어레이가 잘못된 해킹이라는 것을 무시 함)에서 배열 할당을 허용하지 않는 것은 여전히 ​​의미가 있습니다. 열린 배열 또는 배열 함수 매개 변수는 크기 정보가없는 포인터로 취급됩니다. 컴파일러에는 배열 할당을 생성 할 수있는 정보가 없으며 호환성을 위해 포인터 할당이 필요했습니다.

/* Example how array assignment void make things even weirder in C/C++, 
   if we don't want to break existing code.
   It's actually better to leave things as they are...
*/
typedef int vec[3];

void f(vec a, vec b) 
{
    vec x,y; 
    a=b; // pointer assignment
    x=y; // NEW! element-wise assignment
    a=x; // pointer assignment
    x=a; // NEW! element-wise assignment
}

1978 년 C의 개정판이 구조체 할당 ( http://cm.bell-labs.com/cm/cs/who/dmr/cchanges.pdf )을 추가했을 때 변경되지 않았습니다 . 레코드 C에서 고유 한 유형 이었지만 초기 K & R C에서는이를 할당 할 수 없었습니다. memcpy를 사용하여 멤버별로 복사해야했고 함수 매개 변수로 포인터 만 전달할 수있었습니다. 어시 그먼트 (및 매개 변수 전달)는 이제 단순히 구조체 원시 메모리의 memcpy로 정의되었으며 기존 코드를 깰 수 없었기 때문에 쉽게 배치되었습니다. 의도하지 않은 부작용으로 이것은 암시 적으로 일종의 배열 할당을 도입했지만 구조 내부 어딘가에서 발생했기 때문에 배열이 사용되는 방식에 실제로 문제를 일으킬 수 없었습니다.


C가 구문을 정의하지 않았다는 것이 너무 나쁩니다. 예 int[10] c;를 들어 lvalue c가 10 개 항목 배열의 첫 번째 항목에 대한 포인터가 아닌 10 개 항목의 배열처럼 동작 하도록 만드는 것입니다. 변수에 사용할 때 공간을 할당하지만 함수 인수로 사용할 때는 포인터를 전달하는 typedef를 만들 수있는 것이 유용한 몇 가지 상황이 있지만 배열 유형의 값을 가질 수 없다는 것은 의미 론적 약점입니다. 언어로.
supercat

"일부 메모리를 가리켜 야하는 포인터"라고 말하는 대신, 중요한 점은 포인터 자체가 일반 포인터처럼 메모리에 저장 되어야한다는 것 입니다. 이것은 이후의 설명에서 발견되지만 주요 차이점을 더 잘 강조한다고 생각합니다. (현대 C에서 배열 변수의 이름은 메모리 블록을 참조하므로 차이가 없습니다. 포인터 자체가 추상 기계의 어디에도 논리적으로 저장되지 않는다는 것입니다.)
Peter Cordes

역사에 대한 멋진 요약은 배열에 대한 C의 혐오감을 참조하십시오 .
Peter Cordes

31

할당 연산자와 관련하여 C ++ 표준은 다음과 같이 말합니다 (C ++ 03 §5.17 / 1).

여러 할당 연산자가 있습니다. 모두 왼쪽 피연산자로 수정 가능한 lvalue가 필요합니다.

배열은 수정 가능한 lvalue가 아닙니다.

그러나 클래스 유형 개체에 대한 할당은 특별히 정의됩니다 (§5.17 / 4).

클래스의 개체에 대한 할당은 복사 할당 연산자에 의해 정의됩니다.

따라서 클래스에 대해 암시 적으로 선언 된 복사 할당 연산자가 수행하는 작업을 살펴 ​​봅니다 (§12.8 / 13).

클래스 X에 대해 암시 적으로 정의 된 복사 할당 연산자는 하위 객체의 멤버 별 할당을 수행합니다. ... 각 하위 개체는 해당 유형에 적합한 방식으로 할당됩니다.
...-
하위 개체가 배열 인 경우 각 요소가 요소 유형에 적합한 방식으로 할당됩니다
.

따라서 클래스 유형 객체의 경우 배열이 올바르게 복사됩니다. 사용자 선언 복사 할당 연산자를 제공하면이를 활용할 수 없으며 배열을 요소별로 복사해야합니다.


이유는 C (C99 §6.5.16 / 2)에서 유사합니다.

할당 연산자는 왼쪽 피연산자로 수정 가능한 lvalue를 가져야합니다.

그리고 §6.3.2.1 / 1 :

수정 가능한 lvalue는 배열 유형이없는 lvalue입니다 ... [다른 제약 조건이 따릅니다]

C에서 할당은 C ++ (§6.5.16.1 / 2)보다 훨씬 간단합니다.

단순 대입 (=)에서는 오른쪽 피연산자의 값이 대입 식의 유형으로 변환되고 왼쪽 피연산자가 지정한 오브젝트에 저장된 값을 대체합니다.

구조체 유형 객체를 할당하려면 왼쪽 및 오른쪽 피연산자의 유형이 동일해야하므로 오른쪽 피연산자의 값이 왼쪽 피연산자로 간단히 복사됩니다.


1
배열이 변경 불가능한 이유는 무엇입니까? 아니면 클래스 유형에있을 때와 같이 배열에 대해 할당이 특별히 정의되지 않는 이유는 무엇입니까?
GManNickG 2010 년

1
@GMan : 그게 더 흥미로운 질문 이군요. C ++의 경우 대답은 아마도 "C에있는 방식이기 때문일 것입니다."이고 C의 경우 언어가 진화 한 방식 때문이라고 생각합니다 (즉, 그 이유는 기술적이지 않은 역사적 이유).하지만 나는 살아 있지 않았습니다. 대부분의 경우 그 부분에 대한 답을 좀 더 알고있는 사람에게 맡기겠습니다. -P (FWIW, C90 또는 C99 근거 문서에서 아무것도 찾을 수 없습니다).
James McNellis 2010 년

2
C ++ 03 표준에서 "수정 가능한 lvalue"의 정의가 어디에 있는지 아는 사람이 있습니까? 그것은 해야 §3.10에합니다. 색인은 해당 페이지에 정의되어 있다고 말하지만 그렇지 않습니다. §8.3.4 / 5의 (비 규범 적) 메모에는 "배열 유형의 개체는 수정할 수 없습니다. 3.10 참조"라고 나와 있지만 §3.10에서는 "배열"이라는 단어를 한 번 사용하지 않습니다.
James McNellis 2010 년

@James : 저도 똑같이하고있었습니다. 제거 된 정의를 참조하는 것 같습니다. 그리고 예, 나는 항상 그 모든 것의 진짜 이유를 알고 싶었지만 그것은 수수께끼처럼 보입니다. "실수로 배열을 할당하여 사람들이 비효율적이되는 것을 방지"와 같은 말을 들었지만 말도 안됩니다.
GManNickG 2010 년

1
@GMan, James : 최근 comp.lang.c ++ groups.google.com/group/comp.lang.c++/browse_frm/thread/… 에 대한 토론 이있었습니다. 배열이 아닌 수정 좌변 때문에 분명히 그렇지 않은 (배열 확실히 좌변 모든 const가 아닌 lvalues가 수정할 수 있습니다이다) 때문이 아니라 =요구 를 rvalueRHS을 하고 배열이 될 수 없습니다 를 rvalue ! lvalue-to-rvalue 변환은 배열에 대해 금지되며 lvalue-to-pointer로 대체됩니다. static_cast동일한 용어로 정의되기 때문에 rvalue를 만드는 데 더 좋지 않습니다.
Potatoswatter

2

이 링크 : http://www2.research.att.com/~bs/bs_faq2.html 배열 할당에 대한 섹션이 있습니다.

배열의 두 가지 근본적인 문제는

  • 배열은 자신의 크기를 모릅니다
  • 배열의 이름은 약간의 도발에서 첫 번째 요소에 대한 포인터로 변환됩니다.

그리고 이것이 배열과 구조체의 근본적인 차이점이라고 생각합니다. 배열 변수는 자기 지식이 제한된 저수준 데이터 요소입니다. 기본적으로 메모리 덩어리와 색인화 방법입니다.

따라서 컴파일러는 int a [10]과 int b [20]의 차이를 알 수 없습니다.

그러나 구조체에는 동일한 모호성이 없습니다.


3
이 페이지는 배열을 함수에 전달하는 방법에 대해 설명합니다 (이는 수행 할 수 없으므로 포인터 일 뿐이며 크기를 잃었다 고 말할 때 의미합니다). 그것은 배열에 배열을 할당하는 것과 관련이 없습니다. 그리고 아니요, 배열 변수는 첫 번째 요소에 대한 "정말"포인터가 아니라 배열입니다. 배열은 포인터가 아닙니다.
GManNickG 2010 년

의견을 보내 주셔서 감사합니다. 그러나 기사의 해당 섹션을 읽었을 때 그는 배열이 자체 크기를 알지 못한다고 말한 다음 그 사실을 설명하기 위해 배열이 인수로 전달되는 예제를 사용합니다. 따라서 배열이 인수로 전달 될 때 크기에 대한 정보를 잃었거나 시작할 정보가 없었습니까? 나는 후자를 가정했다.
Scott Turley

3
컴파일러는 두 개의 서로 다른 크기의 배열의 차이를 말할 수있다 - 인쇄 해보십시오 sizeof(a)대를 sizeof(b)하거나 통과 avoid f(int (&)[20]);.
Georg Fritzsche

각 배열 크기가 고유 한 유형을 구성한다는 것을 이해하는 것이 중요합니다. 매개 변수 전달 규칙은 크기를 개별적으로 전달해야하는 대신 모든 크기의 배열 인수를 사용하는 가난한 사람의 "일반"함수를 작성할 수 있도록합니다. 그렇지 않은 경우 (그리고 C ++에서 특정 크기의 배열에 대한 참조 매개 변수를 정의 할 수 있으며 반드시 정의해야합니다), 분명히 말도 안되는 각기 다른 크기에 대한 특정 함수가 필요합니다. 나는 그것에 대해 다른 포스트 에 썼다 .
Peter-Monica 복원

0

대답 한 모든 사람은 C / C ++ 전문가입니다. 하지만 이것이 주된 이유라고 생각했습니다.

num2 = num1;

여기서는 허용되지 않는 배열의 기본 주소를 변경하려고합니다.

그리고 물론, struct2 = struct1;

여기서 객체 struct1은 다른 객체에 할당됩니다.


그리고 구조체를 할당하면 결국 동일한 질문을 던지는 배열 구성원이 할당됩니다. 두 상황 모두에서 배열 인 경우 하나는 허용되고 다른 하나는 허용되지 않는 이유는 무엇입니까?
GManNickG 2010 년

1
동의합니다. 그러나 첫 번째는 컴파일러에 의해 방지됩니다 (num2 = num1). 두 번째는 컴파일러에 의해 방지되지 않습니다. 그것은 큰 차이를 만듭니다.
nsivakr 2010 년

배열을 할당 num2 = num1할 수 있다면 완벽하게 잘 작동합니다. 의 요소는 num2의 해당 요소와 동일한 값을 갖습니다 num1.
juanchopanza

0

더 이상의 노력이 C에서 배열을 강화하기 위해 만들어진 또 다른 이유는 배열 할당하지 않다고 아마 유용합니다. 구조체로 래핑하여 C에서 쉽게 얻을 수 있지만 (구조체의 주소는 추가 처리를 위해 단순히 배열의 주소 또는 배열의 첫 번째 요소 주소로 캐스팅 할 수 있음)이 기능은 거의 사용되지 않습니다. 한 가지 이유는 서로 다른 크기의 배열이 호환되지 않아 할당의 이점을 제한하거나 값에 따라 함수에 전달하기 때문입니다.

배열이 일급 유형 인 언어에서 배열 매개 변수가있는 대부분의 함수는 임의 크기의 배열에 대해 작성됩니다. 그런 다음 함수는 일반적으로 배열이 제공하는 정보 인 지정된 수의 요소를 반복합니다. (C에서 관용구는 물론 포인터와 별도의 요소 수를 전달하는 것입니다.) 하나의 특정 크기의 배열을 받아들이는 함수는 자주 필요하지 않으므로 많이 놓치지 않습니다. (이것은 C ++ 템플릿 에서처럼 발생하는 배열 크기에 대해 별도의 함수를 생성하기 위해 컴파일러에 맡길 수있을 때 변경됩니다. 이것이 std::array유용한 이유 입니다.)

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.