C언어 포인터 연산
글쓴이: gurumong / 작성시간: 목, 2006/11/30 - 12:04오전
안녕하세요 ^^
책을 보다가 궁금한것이 생겼습니다
배열과 포인터의 관계에 관해서 설명하는 장에서..
포인터에서 정수를 더하거나 빼고, 포인터에서 포인터를 빼는 연산을 수행할 때 아무런 제한없이 임의의 포인터나 정수값을 사용할 수 있는 것은 아니다. 라고 책을 통해 배웠는데요
그러니까 표준은 진짜 배열만 포인터로써 포인터 연산이 사용될때에만 보장해주나요?
/* 실제 배열은 아니지만 배열에 대한 포인터로써 접근하여 포인터 연산을 하는 프로그램 */ #include <stdio.h> int main(void) { int (*a)[5][10]; a = malloc(sizeof(int) * 50); (*a)[2][3] = 3; printf("%d ", *(*(*a + 2)+3)); }
진단메세지가 나오지 않아서 저렇게도 표준이 허락하는구나 생각했는데
혹시나 해서 확인차 책 예제를 컴파일 해보았어요
배열이 아닌 서로 다른 대상체에 포인터 연산이 사용되어서 잘못된 사용이라 진단메세지를 기대했거든요
(컴파일러는 Dev-C++을 사용하고 "완전한 ANSI표준 C문법 지원"란에 "Yes"로 설정)
컴파일러 능력이 부족해서인지 진단메세지를 안뿌려주네요;;
#include <stddef.h> int main(void) { int a, b, ai[10]; ptrdiff_t data; data = &a - &b; /* wrong */ *(ai + 19) = 0; /* wrong */ return 0; }
실제 코드를 컴파일해서 확인하려 했지만 실패!
Forums:
확실히 잘못된 code 지만
Compiler 가 진단하기는 어렵죠.
우선 문법적으로는 문제가 없다는 것을 아실 겁니다.
그리고 전웅씨의 'C 언어 펀더멘탈'에는 분명 진단메시지는 compiler 의 품질의 문제이지
표준의 요구사항이 아니라는 이야기도 있구요.
분명 compile time 에 확인할 수는 있지만 그러기 위해서는 모든 pointer 연산에 있어
이것이 배열에서부터 왔는지를 추적해야 합니다.
물론 runtime 에 동적할당된 배열은 compile time 에 추적할 수는 없구요.
책에 말하길 interpreter 라면 배열경계검사가 가능하다라는 문구또한 역시 있죠.
써본 적은 없습니다만 gcc 의 공유 library 중 Electric Fence 라는 것이 있습니다.
Runtime 에 배열경계검사를 해주죠.
Debug mode 로 compile 을 했을 때 실행 도중에 check 해서 벗어날 때
message 를 주게 하는 compiler 도 있긴 합니다.
음 위의 질문은 배열경계검사와는 좀 차이가 있나요?...
하여간 단순한 compile model 에서는 불가능한 문제라고 생각합니다.
자신은 없지만 첫번째 source 역시 약간 문제가 있는 것 같습니다.
저도 한참 고민했던 문제인데 이 기회를 빌어 'C 언어 펀더멘탈'을 다시 꺼내보는군요.
즉 동적할당을 할 때
a = malloc(sizeof(int[10]) * 5); 나
a = malloc( sizeof(int[5][10]) ); 이 되어야 할 것 같습니다.
복사할 때도
memcpy(a, b, sizeof (int[10]) * 5); 나
memcpy(a, b, sizeof (int[5][10]); 가 되어야 할 것입니다.
물론 문법상 문제는 없기 때문에 compiler 는 진단 message 를 주지 않습니다.
라고는 생각하는데 솔직히 잘 모르겠네요.
device 씨가 쓰신 것처럼 이차원배열을 동적할당하는 것을 저도 많이 써봤었기에...
그나저나 다시 또 고민이네요.
예전에 C++ 에서 2차원배열은 동적할당이 불가능하다는 것을 얼핏 본 것 같은데
실제로 compile 해보니
int (*a)[3] = new int[2][3]; 이 제대로 compile 되니 원..
예전에 제가 잘못 알고 있었던 것인지... 골치야....
인용: 즉
원 코드와 다르지 않습니다.
a = malloc(sizeof(*a)); 와도 같고요.
아래처럼 생각하시면 쉽게 이해하실 것입니다.
C++ 에 대해서
그렇다면 delete 는 어떻게 되죠?
delete[][] 는 없고, delete 는 사용할 수 없으니
delete[] 밖에는 없는데...
제가 알기로 delete[] 의 mechanism 은 우선 소멸자를 각각 호출한 후
할당된 자유공간을 system 에게 되돌려줍니다.
Built-in type 은 소멸자를 호출하지 않는다고 보면 되지만
사용자정의형의 다차원배열의 경우 그 부분배열의 소멸자는
implementation 에 의해 자동정의가 되는 것인가요?
제가 알기로 배열의 typedef 은 피하자는 것이 일반적인 guideline 이고,
Array 를 delete 할 때에도 delete Arrary 가 아닌 delete[] Array 가
되어야 하는 것으로 알고 있습니다.
그렇다면 소멸자의 재귀적인 호출은 일어나지 않는 것 같군요.
어찌되었든 이부분은 C++ 가 직관적이지 못한 문법을 보이는 것 같습니다.
머리가 아픕니다.
저 때문에 문제가 집중이 안 되는 것 같아 device 씨에게 좀 죄송하네요.
연산자일 뿐입니다..
delete 와 delete[] 는 서로 다른 연산자입니다.
([] 가 delete 객체에 붙어있는 구조가 아닌 것이죠..)
그리고 delete[] 는 배열 즉 1개 이상의 객체가 연속적으로 있으므로 그걸 다 지우도록 구현되는 거죠.
(여기서 delete[] 가 1개 짜리에 대해 허용하는 지는 좀 애매하군요.. 에러가 났다가 안 났다가 했던 듯 해서...;)
1개있는 배열에 대해서 적용됩니다.
즉 new int[1]; 라면 delete[] 를 new int 라면 delete 를 써야만 합니다.
에러가 났다가 안 났다가 했다는 것은
new int 에 대해서 delete[] 를 썼다는 뜻인 것 같네요.
저기 궁금해서 그러는데요.
a = malloc(sizeof(int[10]) * 5);
a = malloc(sizeof(int) * 50);
위 두개가 서로 다르다는건가요? 위 두개가 다르다면
a = malloc(sizeof(int[2]) * 5);
a = malloc(sizeof(int[5]) * 2);
a = malloc(sizeof(int[10]));
이것도 다르다는건가요?
왜 다른지 이해가 안되네요. 설명좀 부탁드릴게요.
좀 잘못 이해하신 것 같은데..
다르다는 게 아니라 같다는 겁니다..
단지 배열에 메모리 연산을 강요할 때 제한 조건이 적용되느냐를 얘기하는 것이죠. 타입이나 경계 등에 대해서 말이죠.
특히나 쓰신 예제에서처럼 동적할당을 위해 단순히 숫자를 빼내는 거라면 다를 여지가 없죠..
무엇을 의도하신
무엇을 의도하신 건지는 잘 모르겠지만 첫번째 소스 코드에는 문제가 없습니다. a의 type 선언이 정수형 [5][10] 2차원 배열의 포인터이기 때문에 컴파일러는 a에 접근할 때 그 위치가 어떤 메모리 영역이든지 관계 없이 2차원 배열의 포인터로 해석합니다. 그리고 *(*(*a + 2)+3) 연산은 a의 형 선언에 맞추어 해석이 되므로 정상적으로 동작합니다. (만약 a의 선언에 2차원 배열의 포인터가 아닌 3차 포인터로 선언이 되어 있었다면 같은 소스로도 Segment fault를 유발할 겁니다)
질문을 다시 정리해서...
연산의 재료가 되는 포인터와 연산의 결과로 나오는 포인터가
반드시 같은 배열 대상체의 요소를 가르키는 포인터여야 한다고 표준에서 요구한다고 하는데요
위 코드는 배열하고는 아무 상관도 없는 동적으로 할당받은 대상체에다가
배열인것처럼 배열의 포인터로써 연산해서 사용하는데, 이것은 표준의 정의에는 맞지 않는데 어떻게 되는지 ^^;
잘못된 코드라고 말하기엔 현실적으로 실제 동적으로 할당받은 대상체를 2차원 배열로써 다뤄야 할 일이 많지 않나요?
현실과 상관없이 무조건적으로 표준의 정의에 부합되지 않으니 이식성이 없다 보장되지 않는것인지 궁금해서요
제가 책을 잘못 읽은것이 아닌가 생각되어서 몇 페이지 분량 부분을 집중적으로 읽고 읽고 했는데 ㅜ.ㅜ
그리고 윗분이 말씀하신 동적으로 할당받은 메모리의 경계에 대해서도 듣고보니 궁금하네요;;
배열이 아니더라도 일반 대상체에도 간접지정 연산자 사용은 안되지만 바로 뒷부분을 가르킬수 있다고 읽었는데
동적으로 할당받은 대상체에 대해서도 끝부분 하나를 더 가르킬수 있나요? 흠..
표준 잘 따르는 컴파일러 하나 만들기 굉장히 힘들꺼 같네요 --;;;;
컴파일러 만드는 분들이 존경스러워지는..
당연히 존경스러워야~~
그래서그 컴파일러를 공개하는 RMS 가 정말 존경스럽지요. (인간성은 별개로 두고...) gcc 이후 벤더에서 제공하던 컴파일러의 개발은 거의 중단되지 않았나 싶네요.
- 겨울아찌 -
ChangHyun Bang
winchild@kldp.org
- 겨울아찌 -
winchild@gmail.com
"연산의 재료가 되는
"연산의 재료가 되는 포인터와 연산의 결과로 나오는 포인터가 반드시 같은 배열 대상체의 요소를 가르키는 포인터여야 한다"라고 하셨는데요... 솔직히 한글로 번역된 문장인 듯해서 뜻을 이해하기 힘듭니다. C 언어에서 포인터에 대한 제한 사항은 포인터를 대입할 때 두 포인터의 type이 같을 것과 (예외가 있다면 pointer to void (void *)입니다. 이 녀석은 type이 달라도 대입할 수 있습니다.) 포인터의 차수가 같을 것입니다. (int **a를 int *a에 대입할 수 없다는 뜻입니다. type cast를 사용하면 얘기가 달라집니다만...) 배열은 단순히 연속된 메모리 공간에 index로 접근할 수 있게 가상의 구역을 나누어 놓은 것입니다. 따라서 malloc으로 같은 크기의 공간을 작성해 줄 수 있다면 문제가 없습니다. 또한 malloc의 반환형은 void *라서 1차 pointer에는 모두 대입할 수 있으므로 규칙에 위배되는 것은 없습니다.
지역변수로 선언된 int (*a)[5][10];는 실제 stack상에 1차 포인터를 위한 공간만 (32-bit addressing이라면 4 byte죠) 할당되고 *a를 통해 접근한 메모리 영역이 배열로 해석됩니다.
그리고 배열의 범위를 넘어가는 경우에 대해서는 정확히 알지는 못합니다만... 바로 뒤의 원소를 가리키는 경우를 제외하고는 undefined가 아닐까 합니다. (컴파일러 마음대로...) 그리고 동적 할당의 경우에도 마찬가지로 해석할 수 있습니다. (동적 할당에 대해 굳이 구별할 이유가 없음)
인용:포인터에서
정확히 말하면 T형 포인터 p에서 정수를 더하거나 뺄 때, p와 연산 결과로 나오는 포인터는
같은 배열의 원소 또는 마지막 원소의 하나 뒤를 가리킬 때만 연산 결과가 정의됩니다.
그리고 마지막 원소의 하나 뒤를 가리킬 때에는 역참조를 하면 안 됩니다.
그리고 배열이 아닌 개체를 가리킬 때는 원소 개수가 1인 배열과 마찬가지로 생각하면 됩니다.
확실히 제가 잘못 이해하고 있는 것 같은데
제가 고민한 것은 다차원배열의 부분배열에 padding bit 같은 것으로 인하여
정렬제한문제가 발생하지 않는가 하는 것이었습니다.
하지만 그런 일이 발생한다면
sizeof(int) * 5 != sizeof(int[5]) 가 될테이고,
실제로 위의 좌우변식은 같으니까 그런 문제는 없겠네요.
하지만 표준에서는
int a[5][3];
일 때
a[3][4] == a[4][0] 같이 부분배열의 경계를 넘어가는 것을
허용하지 않는 것으로 알고 있습니다.
이것이 허용된다면 이차원배열을 일차원배열처럼 다룰 수 있을 것인데...
이것을 허용하지 않은 것은 단지 논리적인 차원의 문제인가요?
인용: 하지만
예, 저는 논리의 문제라고 생각합니다.
("단지" 그뿐일 정도로 사소한 문제라고 생각하지는 않습니다만.)
표준을 그대로 적용하면 분명 a[3][4]는 잘못된 것입니다.
그러나 그렇다고 해서 2차원 배열을 1차원 배열로 다루는 방법이 없는 것은 아닙니다.
int* p = a[0]; 으로 해서 쓰면 되겠지요.
그게 그거지 뭐가 다르냐고 한다면 저도 딱히 반박하기 힘들지만
이렇게 하는 쪽이 1차원 배열로 다루겠다는 의도를 명확히 나타낼 수 있고
논리적으로도 옳다고 생각합니다.
뭔가 자꾸 어긋나는듯한 ㅜ.ㅜ
그러니까
malloc()함수로 할당받은 어떤 크기를 가지는 "배열"이 아닌 대상체를
포인터 연산을 사용해 배열 처럼 다루어 사용하는것은
표준이 정의하는 것과 어긋나는것인가요?
다시 설명하면
사용되는 포인터의 타입은 배열의 포인터이지만 이것으로 다루는 대상체가 실제 배열이 아니라면
그 포인터에 포인터 연산을 취한 결과는 어떤 배열의 원소를 가르키는것이 아니게 되는데
그럼 이식성이 없는것인가요?
또 한가지, 위의 winner님의 질문과 비슷한데요 (같은가!?)
malloc()함수로 할당받은 어떤 크기의 대상체를 일반대상체로 보고
그 다음 존재하지 않는 요소를 포인터가 가르키는것도 정상적인가요?
어떤 배열 int array[x][y]가있는데...
이것을 다른 포인터로써 또는 배열이 포인터로 변한 문맥하에서 어떤 요소를 참조할때
열에 해당하는 y가 마지막요소+1 큰것은 배열의 마지막 요소 바로 다음 가상의 요소(다음 바이트)를 가르키게 되는데
만약 행에 해당하는 x가 마지막요소+1 크다면 배열의 마지막 요소 다음 바이트가 아닌
"부분 배열"의 크기 만큼 "멀리" 떨어져 있는 다음 요소를 가르키게 되는데
이런 경우 문제를 일으킬수 있나요?
표준에서는 존재하지 않는 바로 다음 가상의 요소를 간접지정하는것은 안되지만 가르키는건 된다고 되어있는걸로 아는데
질문이 많네요;;
malloc으로 할당 받은
malloc으로 할당 받은 것은 연속된 메모리 공간입니다. 이것을 "배열이 아니다"라고 정의할 수 없습니다. malloc으로 할당받은 메모리 공간을 배열로 쓰든 구조체로 쓰든 그것은 프로그래머가 선언하는 것입니다. 따라서 malloc으로 할당받은 메모리 공간을 배열로 선언해서 쓰는 것은 표준에도 부합하며 아무런 문제를 일으키지 않습니다. (C 표준에서는 type에 대해서만 제약이 있을 뿐입니다.)
그리고 배열의 범위를 넘어서는 부분에 있어서는 배열의 바로 다음 위치를 가리키는 것까지는 허용하지만 그 값에 실제로 접근하지 말라고 되어 있습니다. 그리고 그 이후의 위치를 가리키는 것 자체는 c9x 표준을 보아도 undefined인 듯 합니다. (컴파일러 마음대로)
int a[5][3];
a[7][7] = 0;
을 컴파일 해보면 경고도 없고 컴파일도 됩니다만... 다른 변수 영역을 침범하고 있습니다. 그리고 저 코드의 행동은 컴파일러마다 다를 수 있습니다. (또한 그것 때문에 다른 기종이나 컴파일러에 이식했을 때 결과를 예측하기도 힘듭니다) 결론적으로 표준에 부합하든 하지 않든 사용하지 않는 것이 좋습니다.
시작은 미약하였으나
시작은 미약하였으나 그 끝은 창대하군요. 질문의 범위가 너무 방대해져서
어디서부터 말씀을 드릴 수 있을지 device 님께 어느 부분이 정말 문제가
되는지 알기 어렵네요.
제 책은 malloc 으로 할당된 대상체는 다루지 않습니다. 이는 좀 어중간
하게도 언어와 라이브러리 부분에 걸쳐 있기 때문에 엄밀한 의미에서 언어
만을 다룰 목적으로 쓰여진 책에서 모두 다루기에 어려운 부분이 있습니다.
예를 들어, malloc 으로 할당된 대상체는 기본적으로 type (표준 용어로는
effective type) 을 갖지 않고, 또 특정 type (혹은 그 배수) 에 딱 맞지
않는 크기를 가질 수도 있기 때문에 일반 선언을 통해 생성된 대상체와는
차이가 있습니다. 따라서 순수하게 제 책의 내용만으로 malloc 으로 할당된
대상체를 해석하려 하실 때에는 오해의 위험 부담이 있습니다.
요즘 같아서는 너무도 어려운 일처럼 보이지만, 만족스러운 수준의 개정판
이 나오면 그 이후 표준 라이브러리를 다룬 책을 하나 더 준비하려 합니다
- 꿈같은 얘기지만 이 두 권을 합하면 쓸만한 "표준 C 언어" 책이 되지
않을까 생각합니다. --;
device 님께서 주신 질문의 최종 목적지는 흔히 "object problem" 이라고
부르는 문제입니다. 익명손님께서는 번역된 문장 같다고 하신 문장(실제론
번역이 아니라 제가 직접 작성한 문장입니다 --;)이 말하는 것이 기본적인
원칙입니다. 즉, 어떤 포인터에 연산이 적용되어 결과로 포인터가 나왔다면
두 포인터가 모두 한 대상체의 유효한 범위 안을 가리키고 있어야 한다는
것입니다. 그렇게 되면 이제 문제는 어디서부터 어디까지를 유효한 대상체
의 범위로 보아야 하느냐가 됩니다 - 그래서 "object problem" 입니다.
이에 대한 정확한 해답은 표준에서 제공하지 못하고 있습니다. 표준이
의도만을 전달하는 수준으로 포인터 연산을 정의하고 있는 탓에 조금만
복잡한 예가 나오기 시작하면 논란이 시작됩니다. 단, 단순한 경우에 대해
서는 그 의도가 분명하기에 분명한 답을 내릴 수 있습니다.
일단, 논의 과정에서 언급된 것들을 살펴보면,
> 또한 malloc의 반환형은 void *라서 1차 pointer에는 모두 대입할 수 있으므로
>
그렇지는 않습니다. 아마도 포인터의 유도 횟수를 n차라고 부르시는 것
같은데, void * 는 모든 대상체 포인터를 위한 일반 포인터입니다. 메모리
할당이 항상 성공한다고 가정한다면,
도 가능합니다.
> 그리고 동적 할당의 경우에도 마찬가지로 해석할 수 있습니다. (동적 할당에 대해 굳이 구별할 이유가 없음)
>
대부분의 경우엔 그렇습니다만, 미묘한 예를 들기 시작하면 쉽게 확답을 할
수 없는 차이가 발생하기 시작합니다 - 예를 들어, 실제 접근하는 type
보다 작은 크기로 할당을 한다거나 특정 배열 대상체로 접근해 값을 저장한
후에 문자 포인터와 캐스트 변환으로 장난을 친다든가...
> 하지만 표준에서는
> int a[5][3];
> 일 때
>
> a[3][4] == a[4][0] 같이 부분배열의 경계를 넘어가는 것을
> 허용하지 않는 것으로 알고 있습니다.
> 이것이 허용된다면 이차원배열을 일차원배열처럼 다룰 수 있을 것인데...
>
> 이것을 허용하지 않은 것은 단지 논리적인 차원의 문제인가요?
>
에서 까다로운 컴파일러가 마지막 func(a[4][0]); 을 func(1); 로 최적화할
수 있음을 의미합니다. (아직 그 정도로 까다로운 컴파일러를 보지
못했습니다만, 그와 같은 최적화를 해도 표준에 부합하는 컴파일러임을
의미합니다) 확실히 "논리"적인 문제만은 아니죠?
> 그러나 그렇다고 해서 2차원 배열을 1차원 배열로 다루는 방법이 없는 것은 아닙니다.
> int* p = a[0]; 으로 해서 쓰면 되겠지요.
>
가 "의도된" 방법입니다. 위에서 설명드렸지만, 아직 "object problem" 에
대한 표준의 명확한 해답은 없습니다. 하지만, 상당히 그럴싸한 모델이
있습니다. 그 모델에 따르면 적어주신 방식으로는 p[2] 까지 뿐이 access
할 수 없으며, 위와 같이 해주어야 전체 배열을 1차원 배열로 접근할 수
있습니다.
> 다시 설명하면
> 사용되는 포인터의 타입은 배열의 포인터이지만 이것으로 다루는 대상체가 실제 배열이 아니라면
> 그 포인터에 포인터 연산을 취한 결과는 어떤 배열의 원소를 가르키는것이 아니게 되는데
> 그럼 이식성이 없는것인가요?
>
아닙니다. malloc() 도 할당 방법만 다를 뿐 배열로 선언한 대상체와
동일한 방법으로 사용할 수 있습니다. 문제가 되는 경우는 그렇게 할당한
메모리를 가지고 장난을 치기 시작할 때부터 입니다.
> 또 한가지, 위의 winner님의 질문과 비슷한데요 (같은가!?)
> malloc()함수로 할당받은 어떤 크기의 대상체를 일반대상체로 보고
> 그 다음 존재하지 않는 요소를 포인터가 가르키는것도 정상적인가요?
>
넵. 예를 들면,
와 같이 됩니다. 이런 식으로 "모범적으로" 사용한다면 선언해 사용하는
대상체와 크게 다르지 않습니다.
> 어떤 배열 int array[x][y]가있는데...
> 이것을 다른 포인터로써 또는 배열이 포인터로 변한 문맥하에서 어떤 요소를 참조할때
> 열에 해당하는 y가 마지막요소+1 큰것은 배열의 마지막 요소 바로 다음 가상의 요소(다음 바이트)를 가르키게 되는데
> 만약 행에 해당하는 x가 마지막요소+1 크다면 배열의 마지막 요소 다음 바이트가 아닌
> "부분 배열"의 크기 만큼 "멀리" 떨어져 있는 다음 요소를 가르키게 되는데
> 이런 경우 문제를 일으킬수 있나요?
>
아래 두 예를 비교해 보시기 바랍니다.
C 에는 다차원 배열이 따로 존재하지 않습니다. 배열은 항상 "무엇인가"의
배열이며, 그 "무엇인가"가 배열이 되어 "배열"의 배열이 되었을 때 사람이
편하게 이해하고자 다차원 배열이라 부르는 것 뿐입니다.
--
Jun, Woong (woong at icu.ac.kr)
Web: http://www.woong.org (서버 공사중)
--
Jun, Woong (woong at gmail.com)
http://www.woong.org
인용: 요즘
그러니까... 이걸 기대하고 있단 말입니다. H&S의 표준 라이브러리에 대한 설명은 기대했던 것보다 부족한 것 같아서...
大逆戰
大逆戰
ㅋㅋ 그러게요.
ㅋㅋ 그러게요. 부족할 뿐만 아니라 (사소하긴 하지만) 오류도 종종 있어서...
H&S 는 아무래도 상당히 실용적인 관점에서 레퍼런스를 제공하기 위한
목적인 것 같습니다.
--
Jun, Woong (woong at icu.ac.kr)
Web: http://www.woong.org (서버 공사중)
--
Jun, Woong (woong at gmail.com)
http://www.woong.org
잉 피곤할 때 글을
잉 피곤할 때 글을 적다보니 오류가 있네요. 마지막 예에서
를
로 수정합니다.
--
Jun, Woong (woong at icu.ac.kr)
Web: http://www.woong.org (서버 공사중)
--
Jun, Woong (woong at gmail.com)
http://www.woong.org
댓글 달기