포인터는 왜 어려울까?

C/C 2011. 11. 3. 18:19

출처 : http://minjang.egloos.com/2291148

왜 C/C++를 가장 강력하고 또 가장 어려운 언어라고 할까? 아마 대부분 이 물음에 ‘포인터’로 답할 것이다. 포인터는 강력함과 동시에 수 많은 어려움을 선사하여 많은 C/C++ 개발자를 괴롭힌다. 포인터로 고생하는 초보자들은 어디서나 볼 수 있다. 심지어는 포인터만 다룬 책들도 많이 있다. 도대체 포인터는 왜 이렇게 어려울까?

먼저, C/C++의 포인터 문법 자체가 복잡하다. 대부분 주소 값을 얻거나(&) 그 주소 값의 내용을 읽는 것(*)만 알아도 되지만 포인터 형태가 조금만 복잡해져도 사실 쉽지 않다. int *a[3]은 매우 간단하지만 int (*a)[3]만 되어도 그리 쉽게 머리 속에 그림이 잘 안 그려진다. 게다가 C++로 가면 클래스 멤버 함수까지 생기니 더 복잡해진다.

그런데 이건 어디까지나 문법의 문제다. 초보 개발자가 포인터형 자료구조나 함수 포인터에 쩔쩔매는 이유는 사실 문법이 어려워서가 아니다. 아무리 포인터를 공식에 맞춰 해석해도 데이터나 코드를 ‘가리킨다’라는 개념이 명확하지 않아서 그렇다.

포인터가 어려운 이유는 사실 매우 간단하다. ‘컴퓨터’를 몰라서 벌어지는 비극이다. 좀 더 정확히 쓰면 C/C++ 코드 한 줄이 어떻게 컴파일러가 번역하고, 어떻게 운영체제가 실행을 시키고, 마지막으로 어떻게 프로세서 내부에서 처리되어 메모리를 변화 시키는지 감이 없기 때문에 이해가 안가는 것이다. 컴퓨터 시스템의 기본 원리를 모르는데 야리꾸리한 비유를 동원해 포인터를 배우려 하니 더 헷갈릴 뿐이다.

여기서 대충 C 수업 숙제만 해결하면 되는 친구라면 컴퓨터 시스템 따위 공부해봤자 인생에 도움 안되니 대충 대충 넘어가도 된다. 그런데 최소한 이 바닥에서 진짜 개발자로 먹고 살려면 기본적인 컴퓨터 시스템에 대한 이해는 필수다. 그러면 포인터는 문제 되지 않는다. 이중포인터니 뭐 그런 소리를 할 필요도 없다. 포인터가 그냥 데이터나 코드를 가리킨다는 기본적인 사실에 왜 컴퓨터가 이런 표현을 필요로 하는지만 이해하면 끝난다.

간단한 문제를 하나 생각해보자.

a = a + 1;

위 코드에서 변수 a는 지역 변수이다. 그럴 때 이 코드가 어떻게 컴파일러에 의해 번역되어 어떻게 메모리를 변화시키는지 10분 이상 설명해보라.

. . .

이 질문에 막힘이 없는 친구라면 포인터로 절대 고생하지 않을 것이다. 그런 친구가 설사 포인터로 삽질한다 하더라도 그건 단순히 문법에 대한 혼동일 뿐이다. 이 질문에 답할 자신이 없다면 지금 C/C++ 책이나 무슨 디자인 패턴 책을 봐야 할 때가 아니다. 기본적인 컴퓨터 시스템부터 공부해야 한다.

위 문제를 이제 좀 더 자세히 살펴보자. 질문을 던졌던 코드를 x86 32비트로 컴파일하면 보통 아래처럼 번역된다.

mov eax,dword ptr [ebp-4]
add eax,1
mov dword ptr [ebp-4],eax

아니면 바로 CISC 스타일로 표현되기도 한다.

inc  dword ptr [ebp-4]

여기서 내가 x86 기계어를 반드시 이해해야 한다고 말하는 것이 아니다. 내가 주장하는 것은 최소한 컴파일러가 어떻게 저 간단한 덧셈 문장을 기계어로 바꾸는지는 알아야 한다는 것이다. MIPS가 되었던 ARM이 되었던 상관없다. 살짝 형태만 다르지 기본 틀은 같다.

만약 위 기계어의 모든 부분을 하나도 빠짐 없이1 설명할 수 있다면 컴퓨터 시스템도 대부분 이해하고 있을 뿐만 아니라 포인터의 대부분도 이해한다는 뜻이다. 나머지는 함수 포인터에 대한 내용이긴 한데 역시 함수 호출이 스택에서 어떻게 이루어지는지만 알아도 문제가 되지 않는다.

이 기계어를 조금 설명하면, 변수 a의 값을 1을 증가시키려면 (1) 변수 a가 있는 주소의 내용을 임시 레지스터로 읽어오고, (2) 이 레지스터의 값을 하나 증가시키고, 마지막으로 (3) 변수 a가 있는 주소에 이 레지스터 내용을 써주는 것이다.

“a = a + 1”에는 어떠한 포인터 표현도 없다. 그런데 컴퓨터는 이걸 포인터로 해석해서 실행하는 것이다. 이렇게 포인터는 컴퓨터 기본 시스템 작동 원리만 알면 너무나 자연스러운 개념이 된다.

지금 저 무시하나효? 저 이 정도는 다 알아욤. 그래도 포인터는 어렵던데요?

라고 반문하시면 난 여전히 이 코드를 제대로 이해하지 못해서 벌어지는 일이라고 말하고 싶다. 정말 장담 하는데 저 간단한 덧셈 코드의 기계어 작동만 “다 알아도” 포인터는 진짜 아무것도 아니다. (추가: malloc/free, new/delete과 같은 힙 할당으로 인한 포인터 사용은 이 포스팅에서 다루는 내용이 아니다.)

더 나가 왜 지역 변수 a가 ‘ebp-4’ 같은 주소에 있는지도 알면 매우 좋다. 이걸 알면 자동으로 스택과 함수 호출 규약도 자연스레 알 것이고, 왜 함수 호출 비용이 있고, 왜 컴파일러가 함수를 인라인하는지 등등 알게 되는 사실이 눈덩이처럼 불어난다.

그럼 나무라지만 말고 좀 친절히 책도 추천해주세요. 21일 완성 같은 거 없어요?

사실 난 21일 완성 시리즈를 참 좋아했다. 농담 아니고 나의 첫 번째 C 공부 역시 21일 완성으로 했다. 그런데 아쉽게도 지금까지 내가 열심히 떠든 “컴퓨터 시스템”에 대해 가볍게 볼 수 있는 책은 그리 많지 않다. 보통 대학교에서 강의하는 시스템 프로그래밍 정도 수업 내용을 참고하면 된다. 몇몇 대학교의 시스템 프로그래밍 강좌 홈페이지에가서 강의 자료 내려 받아 보는 것도 매우 좋다. 좀 더 내공을 더 기르고 싶으면 특정 CPU 아키텍처를 섭렵하면 된다. 예를 들어, x86의 내용을 알고 싶으면 여기에 가서 Volume 1과 Volume 3A를 참고 하면 된다. 다 보는 건 사실 말도 안되고 중요 챕터만 봐도 정말 많은 걸 알 수 있다.

두 줄 요약:

아놔. 요즘 컴퓨터 빠른데 이런 거 진짜 알아야 해요? STL 쓰는 법도 알아야 하고 디자인 패턴도...

네, 개발자, 적어도 C/C++ 개발자라면 꼭 아셔야 합니다.

1 대충 열거하면: opcode, 레지스터, 데이터 정렬 문제, CISC/RISC의 차이(더 이상 이 논쟁은 유효하지 않음), 캐시/메모리, 가상메모리, 스택/힙/코드 등등

p.s. 이번 달 마소에 “개발자 업그레이드 시나리오: 수퍼 개발자의 꿈”이라는 코너에 민망한 글을 하나 썼다. 블로그에 올리려니 넘 남사시러워 요즘 출판 업계도 불황이고 하니 직접 책을 사보시던가 아니면 가까운 서점에 나가셔서 보시기를 바란다. 그 기사에서 내가 주장한 바가 위에서 떠든 이야기랑 비슷하다. 수퍼 개발자가 되려면 기초 체력, 그러니까 기초 전산 실력이 매우 중요함을 역설했다. 서두에 살짝 포인터 이야기를 꺼냈는데 이 포스팅에서는 포인터를 가지고 물고 늘어진 것임.

'C > C' 카테고리의 다른 글

헤더 파일의 중복 정의 막기  (0) 2011.10.28
C언어 - 긴 문자열 초기화  (0) 2011.10.28
c/c++ 2차원(이차원) 배열 동적할당 방법2  (0) 2011.10.23
C언어 할 때 알면 좋은 것  (0) 2011.10.20

설정

트랙백

댓글


출처 : http://elfinlas.tistory.com

#ifndef _MyLib_H_
#define _MyLib_H_
//Header File 기술
#endif


_MyLib_H_ 는 현재 헤더 파일에서 고유한 메크로 이름이구요.
처음 헤더 파일이 시작되면 이 메크로명이 정의되어 있지 않으므로 #ifndef 에서부터 #endif 까지의 내용이 정의되게 됩니다.


이후에도 이 파일이 인클루드 될 경우 이미 메크로가 정의되어 있기에 그 안에 들어가는 헤더파일의 정의를 무시하고 넘어가게 되는 원리입니다.

'C > C' 카테고리의 다른 글

포인터는 왜 어려울까?  (0) 2011.11.03
C언어 - 긴 문자열 초기화  (0) 2011.10.28
c/c++ 2차원(이차원) 배열 동적할당 방법2  (0) 2011.10.23
C언어 할 때 알면 좋은 것  (0) 2011.10.20

설정

트랙백

댓글


출처 : http://ksunghwank.blog.me/140033
(1) '\' 문자 사용(계속문자)

EXAMPLE
char str[]="행복이라함은 성공해서 풍요롭고 여유롭게 사는것이라 생각해 왔었다.\
돈 많이 벌고, 착한 마누라랑 멋들어지게 사는거 말이다.\
사실 이렇게 되어도 행복하게될지는 알 수 없다.";

(2) " " (큰따옴표)로 묶어주기

EXAMPLE
char str[]="행복이라함은 성공해서 풍요롭고 여유롭게 사는것이라 생각해 왔었다."
"돈 많이 벌고, 착한 마누라랑 멋들어지게 사는거 말이다."
"사실 이렇게 되어도 행복하게될지는 알 수 없다.";

EXAMPLE 2
char str[]="행복이라함은 성공해서 풍요롭고 여유롭게 사는것이라 생각해 왔었다."
"돈 많이 벌고, 착한 마누라랑 멋들어지게 사는거 말이다."
"사실 이렇게 되어도 행복하게될지는 알 수 없다.";

결론
(2) 의 방법이 코드를 정렬하기에 더 좋다.
만일 (1)의 방법으로 EXAMPLE 2 와같이한다면 사이에 공백(또는 TAB 문자)가 들어가게 된다.

'C > C' 카테고리의 다른 글

포인터는 왜 어려울까?  (0) 2011.11.03
헤더 파일의 중복 정의 막기  (0) 2011.10.28
c/c++ 2차원(이차원) 배열 동적할당 방법2  (0) 2011.10.23
C언어 할 때 알면 좋은 것  (0) 2011.10.20

설정

트랙백

댓글


출처 : http://coden.tistory.com/10


int **array = null;
intheight=8,width=6;
array = (int **) malloc( sizeof(int *)* height );
for( int i=0; i < height ; i++)
array[i] = (int *) malloc( sizeof(int)* width );


이런식으로 동적할당 하면 malloc를 height만큼 호출하기 때문에 행과 열이 나누어 진다.

memset으로 한번에 값을 지정할 수도 없고
할당된 메모리를 해제할경우 for문으로 height번만큼 해제해줘야 한다.

int **array = null;
int height=8,width=6;
array = (int **) malloc( sizeof(int *)* height);
array[0] = (int *) malloc( sizeof(int) * width*height );
for( int i=1; i< height ; i++)
array[i] = array[ 0 ] + i*width;



조금더 고쳐보면..

int **array = null;
int height=8,width=6;
array = (int **) malloc( sizeof(int *)* height);
array[0] = (int *) malloc( sizeof(int) * width*height );
for( int i=1; i< height ; i++)
array[i] = array[ i-1 ] + width;

malloc 호출을 줄일 수 있어서 시간 효율성이 좋을 듯 싶다

'C > C' 카테고리의 다른 글

포인터는 왜 어려울까?  (0) 2011.11.03
헤더 파일의 중복 정의 막기  (0) 2011.10.28
C언어 - 긴 문자열 초기화  (0) 2011.10.28
C언어 할 때 알면 좋은 것  (0) 2011.10.20

설정

트랙백

댓글


1 ( ) [ ] -> .
왼쪽 우선
2 ! ~ ++ -- + -(부호) *(포인터) & sizeof 캐스트
오른쪽 우선
3 *(곱셈) / %
왼쪽 우선
4 + -(덧셈, 뺄셈)
왼쪽 우선
5 << >>
왼쪽 우선
6 < <= > >=
왼쪽 우선
7 == !=
왼쪽 우선
8 &
왼쪽 우선
9 ^
왼쪽 우선
10 |
왼쪽 우선
11 &&
왼쪽 우선
12
||
왼쪽 우선
13
? :
오른쪽 우선
14
= 복합대입
오른쪽 우선
15
,
왼쪽 우선

(6)최대 혹은 최소값 구할 때 초기값 설정
int a;
a = 0x7FFFFFFF; // 가장 큰 수로 보초(sentinel)를 세울 때
int a;
a = 0x80000000; // 가장 작은 수로 보초(sentinel)를 세울 때

rand()%n을 사용하여 난수의 범위를 설정하면, 대부분의 난수 발생기에서 low-order 비트들이 random하지 않으므로,
난수의 분포가 이상적인 일양분포(uniform distribution)를 이루지 않습니다.

예를들어 난수의 범위를 0부터 10까지 지정했을 때 무수히 많은 시행을 한다면 0부터 10까지 각각 해당하는 숫자에 맞는 확률이 비슷하게 나와야 진정한 난수라고 볼 수 있습니다.
그래서 C Programming FAQs에서는 다음과 같은 방법을 권고하고 있습니다.

(int)((double)rand() / ((double)RAND_MAX + 1) * n)
또는,
rand() / (RAND_MAX / n + 1)

'C > C' 카테고리의 다른 글

포인터는 왜 어려울까?  (0) 2011.11.03
헤더 파일의 중복 정의 막기  (0) 2011.10.28
C언어 - 긴 문자열 초기화  (0) 2011.10.28
c/c++ 2차원(이차원) 배열 동적할당 방법2  (0) 2011.10.23

설정

트랙백

댓글