Engineering/C/C++2009/08/16 10:55


디버깅이 어려운 이유는 간단하다.

사람들은 자기가 보고자 하는 것만 보기때문이다. 아는 것만 보려하기 때문이다. 코드의 에러를 예측할 때는 자기가 아는 만큼 안에서 예측을 하게 된다. 고로 경험과 지식, 그리고 이 것들을 지혜로 흡수한 사람은 당연히 디버깅을 잘한다.

결론 : 무식이 죄다. ㅋ



어제 런타임 에러가 난 버그가 포함된 코드이다. 분명히 delete [] buf;  이 구문에서 런타임 오류가 나는 걸 바로 잡아냈다. 처음엔 후~ 뻔하지 동적으로 할당한 메모리를 초과해서 내가 무슨 짓을 하였구나~. 그런데 난 절대로 14바이트 이상 초과하여 무슨 짓을 하지 않았다. 그런데 이놈의 코드가 계속 런타임 에러를 내는 것이었다. 20분 정도를 들여다 본 후에야 나는 어디가 문제인지 그제야 알아차렸다. 후 -_-

대부분 C/C++를 배운 사람들은 위의 코드를 보자마자 어디가 잘못되었는지 바로 알 것이다. 심지어 10분전에 new연산자에 대해 배운 C++ 초봉들도 어라 저거 맞는 코드인가? 라고 생각할 것이다. 

   1. BYTE* buf = new BYTE(14);
   2. BYTE* buf = new BYTE[14];

그렇다. [] 대신에 ()를 써서 생긴 런.타.임(run-time) 오류이다. 컴파일 과정에서 전혀 무리가 없는 코드이다. 처음에 저 버그를 발견하고 나서 아니 이놈의 컴파일러가 왜 에러를 안내지? 라고 생각하며 컴파일러에게 분풀이를 하였다.  하지만 조금만 생각을 해봐도 "에러를 낼 수가 없군~" 이라는 결론이 난다. 당연한거지만 바로 생성자 때문이다.

즉 컴파일러는 1번 코드에는 BYTE만큼의 크기(1바이트)로 동적으로 메모리를 할당한 다음, ()을 보고 아~ 생성자를 호출하여야 겠군! 이러면서 1바이트의 메모리에 14라는 값을 넣어주고 그 주소값을 buf에 저장하였다. 2번 코드는 []를 보고 14바이트 만큼 동적으로 메모리를 할당하고, 첫번째 주소를 buf에 저장한 것이다.

1번 코드에서는 동적으로 할당된 메모리는 1바이트인데, 14바이트인줄 알고 이것저것 해댔으니 당연히 delete 할 때 에러가 날 수 밖에 ... 그런데 BYTE라고 해봐야 unsigned char인데 저건 분명히 C/C++의 기본 자료형이다. int, char 같은 놈들에게도 생성자가 있단 말인가? 테스트를 해보았다.

int a(5);   과연 이 코드가 C 컴파일러는 어떻게  처리하고, C++에서는 어떻게 처리할 것인가?

C와 C++ 컴파일러는 과연 어떻게 처리할 것인가. 얼마전에 소개한 http://www.codepad.org 에서 코드를 돌려보았다. 역시나~ 결과는 예상대로 나왔다. 링크를 따라가보면 C컴파일러는 에러를 내고,  C++ 컴파일러는 정상적으로 컴파일을 하고 실행하여 5라는 결과를 보여준다.

C 코드 : http://codepad.org/g0nB7w2B
C++ 코드 : http://codepad.org/0FhQsQu2


결론 : C에 객체지향이라는 개념이 추가되면서 C++가 나왔고, 그러면서 생성자라는 개념이 생기면서 새로 생긴 문법이었다.

어처구니 없는 삽질을 하며 얻어낸 작은 지식이자 작은 재미이다. ㅎ


Posted by Humaneer
Engineering/C/C++2009/07/24 13:02

회사에 아는 선임분이 ARM 교육을 가셔서 부트 로더쪽 코딩으로 뭔가를 실습하는데, 잘 모르겠다고 메신저로 물어보시길

"C에서 특정 주소로 점프 하려면 우찌해야 하냐?"

참고로 부트로더의 역할을 내가 아는데로 간단하게 정리를 해보면

1. 뭔가를 한다 -_-   (보통 시리얼 통신(rs-232,)을 통해 커널이미지를 정해진 주소에 다운로드 시키게 된다)
2. 특정 주소(커널 이미지의 시작점)로 점프를 한다.
3. 끝 -_-

역시 .. 내가 아는데로 정리하니 ... 간단하다 -_- . 다시 말해 모르는게 속편하다는 거

아무튼 질문을 받고 내가 처음에 바로 떠올린건 인라인 어셈블리였지만, '어셈은 안돼~' 라는 말에 함수포인터를 써보기로 하였다. 커널(커널함수)의 리턴이나 인자는 없다고 가정하였다. 쉽게 말해 kernel의 코드가 다음과 같은 함수로 선언되어 있다고 한 것이다.  void kernel(void); 고로 요런 함수의 주소에 매핑되는 함수포인터를 사용하여 아래와 같은 코드가 나오게 되었다.


내가 짜놓고도 잘 될려나 하고 반신반의 했었는데, 잘 된다고 한다. ㅋ역시나 C로 못하는 것은 없다. C만세. -_-;


Posted by Humaneer
Human Life/Book2009/07/03 10:46

GREAT CODE(하드웨어의 이해)
카테고리 컴퓨터/IT
지은이 랜달 하이드 (에이콘출판, 2007년)
상세보기


최근에 질러서 보고 있는 책이다. 필자가 말하고자 하는 바는 다음과 같다.

HLL(High Level Language)의 컴파일러가 아무리 똑똑하더라도 H/W(혹은 컴퓨터 아키텍처)에 대한 이해도가 부족한 사람이 작성한 코드는 어찌할 수가 없다. 고로 H/W에 대해 이해를 해야 멋진 코드가 나온다.

나같이 무지한 S/W쟁이가 필수적으로 봐야할 책이 아닌가 싶다. 현재는 6장 메모리 구조와 접근이라는 챕터를 읽고 있다. 폰 노이만 아키텍처의 기본적인 컨셉과, 시스템 버스, 클럭, 메모리 구조, 접근에 관한 문제를 얕지 않으면서도 알기 쉽게 설명하고 있다.

후다닥 독파해버려야지. +_+;


Posted by Humaneer
Engineering/C/C++2009/05/25 15:58

눈컴파일 및 실시간 두뇌 실행을 해봅시다.



거두절미 하고 아래의 소스는 과연 실행이 잘 될까?


당연히 잘 된다. Abc라는 문자열이 화면에 출력된다.



그렇다면 아래의 소스는 실행이 잘 될까? ㅋ


당연히 잘 될까? 컴파일, 링킹까지는 된다. 하지만 실행시 오류가 난다. 즉, 우리 프로그래머들이 가장 싫어하는 Runtime Error(혹은 Segmentation Fault)가 뜬다는 말이다. -_-;



그렇다면 아래의 소스는?


실행된다. abc라는 문자열이 화면에 출력된다.


도대체 뭐가 문제일까?


나도 명확하게는 잘 모르겠다. 위의 사례로 보아 배열로 선언하여 정적으로 할당받은 메모리 영역은 read/write 모두 가능하다. 하지만 포인터로 선언하여 정적으로 할당받은 메모리 영역은 read only이다. 이게 당최 뭐람? ㅋ

게다가 두번째 소스를 visual studio에서 debug mode로 컴파일을 하면 런타임 에러가 뜬다. 그런데 release mode로 컴파일을 하면 런타임 에러가 뜨지 않는다. 그렇다면 debug/release의 선택에 따라 할당하는 메모리의 영역에 차이가 있는 것인가?

정답은 '아니오'이다. release mode로 컴파일 하고 실행하면 에러는 뜨지 않는다. 즉 char* str = "abc"; str[0] = 'A' 또는 *(str + 0) = 'A'를 수행할 때 에러를 내지 않는다는 것이다. 하지만 출력해보면 그 결과가 매우 신기하다. Abc가 아니라 abc가 출력된다는 것. 즉 포인터로 할당한 문자열의 내용을 바꾸는 명령이 반영이 되지 않은 것이다.

그렇다면 결론은 이러한 것인가? ㅋ (내멋데로 결론)
char* str = "ABC"; 이렇게 포인터를 통해 정적으로 할당받은 메모리 영역은 read only이다.(배열은 read/write가능) 또한 debug mode로 컴파일을 하면 포인터를 통해 할당받은 메모리 영역에 쓰기를 시도할 때 런타임 에러를 내지만, release mode로 컴파일을 하면 에러를 내지는 않지만, write하는 명령을 무시한다.

gcc, bcc, turbo c 같은 컴파일러는 어떠한지 궁금하다. 혹시 이 글 보고 해보실 분은 해보시고 결과를 피드백해주시면 매우 감사하겠습니다 ;). 어쨌거나 오묘한 포인터와 배열의 세계. ㅋ

Posted by Humaneer
Engineering/C/C++2009/04/10 08:47

왜 C++ 와 C 가 함께 쓰기가 어려울까요 ? C++야 C에서 나왔으니까 당연히 두 언어는 함께 섞어 써도 아무런 문제가 없어야 되는거 아냐 ? 라고 생각하실지도 모르겠습니다. 그렇지만  C++와 C 를 섞어 쓰는 게 생각만큼 그리 쉽진 않습니다. 개선된 C로서 C++ 특징 중 하나가 함수 재정의(function override) 가 가능하다는 것이고, 이것을 처리하기 위해서는 함수 심볼명을 코드에 나온 그대로 생성하는 게 아니라 컴파일러가 조정할 필요가 있게 됩니다. 이렇게 컴파일러가 함수 심볼명을 재정의하는 것을 name mangling 이라고 하는데요, 이것 때문에 C와 C++를 섞어 쓰는게 쉽지 않습니다. 어떻게 name mangling 이루어 지는지 아래 코드를 통해 눈으로 확인해 보겠습니다.

Name Mangling

다음 코드를 한 번 컴파일 해 보시겠어요 ? max 라는 함수를 재정의 하고 있습니다.


실행해 본 결과는 다음과 같습니다.


 

$ g++ -o mixed mixed.cpp

$ ./mixed
double max(double, double) called
int max(int, int) called
double max = 35.1
int max = 10


실행한 결과가 위와 같이 나온다는 건 다 이해하실테구요. name mangling 이 어떤 식으로 이루어지는지 확인해 보기 위해 함수 심볼 테이블을 살펴 보겠습니다. 제가 이 글을 쓰면서 사용하고 있는 컴파일러는 g++(4.1.0 20060304) 이고, 실행파일에서  함수 심볼명을 보기 위해 nm 유틸리티를 활용했습니다.


$ g++ --version
g++ (GCC) 4.1.0 20060304 (Red Hat 4.1.0-3)
Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ nm mixed
08049ac0 d _DYNAMIC
08049ba4 d _GLOBAL_OFFSET_TABLE_
080486ea t _GLOBAL__I__Z3maxdd
0804892c R _IO_stdin_used
         w _Jv_RegisterClasses
0804875c T _Z3maxdd
08048716 T _Z3maxii

080486a4 t _Z41__static_initialization_and_destruction_0ii
......



제가 진하게 표시한 부분에 주목해 보시기 바랍니다. 소스 코드에서는 max 라고 이름지어진 함수 두 개의 컴파일 후 심볼명은 _Z3maxdd, _Z3maxii와 같이 생성되어 있는 걸 확인하실 수 있습니다. 제가 추측하건데, Z 다음 숫자(3)는 함수 본래 이름 길이를 뜻하는 것 같고, 그 다음에 오는 dd, ii 는 함수 인자의 타입을 뜻하는 것 같습니다. 그러니까 double max(double, double) 에 대해서는  _Z3maxdd 라는 심볼명이 생성되고, int max(int, int)에 대해서는 _Z3maxii 라는 심볼명이 생성된 것으로 보입니다. 컴파일러 입장에서는 당연히 두 함수를 구분해야 하므로 이렇게 서로 다른 심볼명을 생성하는 건 당연지사입니다. C++ 에서는 함수 재정의만 지원되는 게 아니라 namespace 도 지원됩니다. test 라는 namespace 에 max 라는 함수를 정의하면 어떻게 될까요 ?





새롭게 컴파일한 실행 파일의 심볼 테이블을 보면 다음과 같이 나오네요.

$  nm mixed | grep max
0804875a t _GLOBAL__I__Z3maxdd
08048820 T _Z3maxdd
080487da T _Z3maxii
08048786 T _ZN4test3maxEcc


앞의 코드를 컴파일했을 때는 나타나지  않던 _ZN4test3maxEcc 가 새로 생긴 걸 보니 test 라는 namespace 내의 max 함수인가 봅니다. 이상과 같이 함수 재정의나 네임스페이스 같은 것들을 지원하기 위해서  C++ 컴파일러는 name mangling 을 하게 됩니다. 그럼 어떻게 이런 문제를 피하면서 C 와 C++ 를 섞어서 프로그래밍할 수 있을까요 ?

지금까지 장황하게 얘기를 해서 그렇지 섞어 프로그래밍하기를 위한 방법은 알고 보면 그리 복잡하지 않습니다. name mangling 때문에 섞어 프로그래밍하기가 어려워진 것이므로 C++ 코드를 컴파일할 때 name mangling 을 하지 않도록 하면 되는 것이겠죠. 그렇게 되면 C 코드에서도 C++ 코드를 불러 쓸 수 있고, C++ 코드에서도 C 코드를 불러 쓸 수 있게 될 것입니다. 그럼 C/C++ 섞어 프로그래밍하기 문제는 C++ name mangling 을 어떻게 막느냐의 문제로 환원될 것입니다. 어떻게 name mangling을 막느냐 ? 간단합니다. C++ 코드 컴파일 시에 extern "C" 라는 호출 규약을 쓰도록 하면 됩니다. 자~ 다음 코드를 한 번 컴파일해 보신 후에 심볼 테이블을 살펴 보시겠어요 ?


 


$ g++ -o externc externc.cpp
$ nm externc | grep max
080486dc t _GLOBAL__I_max
08048674 T max


name mangling 이 일어나지 않은 걸 확인하실 수 있습니다. 이 지식을 바탕으로 실제 대규모 프로젝트에서는 어떻게 C/C++ 섞어 프로그래밍하기를 할 수 있는지 풀어가 보겠습니다.

C 라이브러리를 C++에서 사용하기

그럼 첫번째로 기존에 이미 존재하는 C 로 작성된 라이브러리가 있는데, 그걸 C++ 에서 사용하고 싶은 경우 어떻게 해야할지 알아 보겠습니다.

솔직히 C++ 가 아무리 언어적으로 뛰어 나다고 하더라도 기존에 C 로 이미 구현되어 있는 것들에 대응하는 C++ 라이브러리가 나오기는 힘들 것입니다. 그러니 현실적으로 C++ 코드에서 C로 구현된 라이브러리를 쓰는 건 매우 일상적인 개발 현실이라고 할 수 있습니다. 예를 들어, 우리가 거의 매일 같이 쓰는 문자열 관련 C 라이브러리는 C 코드, C++ 코드 상관 없이 일상적으로 쓰이고 있죠.

아까 예제 코드가 다음과 같이 구성되어 있다고 상상해 보시죠.


자~ 그럼 이 코드를 컴파일하고 링크시켜 볼까요 ?


$ gcc -c max.c                    # object 파일만 만듭니다
$ g++ -c cppmain.cpp              # object 파일만 만듭니다
$ g++ -o cppmax cppmain.o max.o   # 링크합니다.


그랬더니 다음과 같은 링크 에러가 발생하네요.


cppmain.o:cppmain.cpp:(.text+0x13a): undefined reference to `max(int, int)'
collect2: ld returned 1 exit status


당연하겠지요 ? cppmain.o 에서는 name mangling 된 max(g++ 에서는 _Z3maxii)를 찾는데, C에 의해 컴파일된 max.o 에는 그냥 max 만 정의되어 있을 것이기 때문입니다. 이 문제를 해결하는 게 extern "C" 였구요, 이걸 max.h 에 적용하면 문제가 해결됩니다.

/* max.h */
extern "C" int max(int a, int b);

근데, 보통은 이렇게 문제가 간단하질 않죠 ? 라이브러리 헤더 파일에 수많은 함수들이 정의되어 있는데, 그 함수 선언 하나 하나에 extern "C"를 붙일 생각을 하시니 갑갑하시죠. 그럴 경우 쓸 수 있는 방법이 물론 있습니다. 이런식으로 하면 됩니다.

/* max.h - max() 함수 말고도 상당히 많은 함수가 선언되어 있다고 가정해 보겠습니다 */
extern "C" {
int max(int a, int b);

...... /* 다른 함수 정의 */

}

이제 다 문제가 해결됐나요 ? 아니죠~ 한가지 문제가 더 있습니다. 말 그대로 우리가 목표로 하는 것이 C/C++ 섞어 프로그래밍하기 인데... C++ 쪽 문제를 해결하다 보니 C 쪽에서 문제가 발생합니다. 바로 extern "C" 라는 건 C++ 컴파일러만 이해할 수 있는 키워드입니다. C 컴파일러는 이해하지 못하기 때문에 max.h 를 컴파일할 수 없습니다. 어디 한 번 확인해 보시죠.


 


$ gcc -c cmain.c
In file included from cmain.c:2:
max.h:2: error: parse error before string constant



그럼 또 이 문제를 어떻게 해결해야 하나요 ? 음... 골치 아프군요. 골치 아프더라도 한 번 곰곰히 생각해 보겠습니다. C 컴파일러용과 C++ 컴파일러용 헤더 파일을 따로 만들까요 ? C 컴파일러는 max.h 를 include 하고, C++ 컴파일러는 max.hpp 를 include 하라고 하는 거죠. extern "C" 가 있고 없고 차이밖에 없는 데 그렇게 한다는 건 왠지 무식해 보이네요 그죠~? 그렇담 좀 더 우아한 해결책이 없을까요 ?

C 컴파일러에게는 extern "C" { } 이 안 보이게 하고, C++ 컴파일러에게만 보이게 하는 방법이 없을까요 ? 만약 헤더 파일을 컴파일하고 있는 컴파일러가 C 컴파일러인지 C++ 컴파일러인지 구분할 수 있다면 이런 방법이 가능하지 않을까요 ? 그러니까 다음과 같은 조건부 컴파일이 가능할 것 같은데 말이죠.

#if (C++ 컴파일러라면)
extern "C" {
#endif

/* 헤더 파일 본 내용 */

#if (C++ 컴파일러라면)
}
#endif

그럼 우리의 문제는 어떻게 하면 컴파일러 종류를 알아낼 수 있느냐 하는 문제로 귀착됐네요. 뭐, 가장 쉬운 방법은 컴파일을 하는 사람이 C++ 컴파일러를 뜻하는 매크로를 정의하는 것이겠죠. 그런데, 이 C/C++ 섞어 프로그래밍하기는 워낙 근본적인 문제이기 때문에 개발자가 일일이 매번 나름의 C++ 컴파일러 매크로를 정의할 필요가 없도록 모든 C++ 컴파일러는 __cplusplus 라는 매크로를 정의하도록 되어 있습니다. 그러니까 max.h 를 다음과 같이 수정하면 만사형통이라는 얘기입니다.

/* max.h - max() 함수 말고도 상당히 많은 함수가 선언되어 있다고 가정해
   보겠습니다 */
#ifdef __cplusplus
extern "C" {
#endif

int max(int a, int b);

...... /* 다른 함수 정의 */

#ifdef __cplusplus
}
#endif

자~ 그렇다면 여기서 한 가지 얻을 수 있는 포인트는

"C 라이브러리를 작성하려거든 나중에 C++ 에서 활용될 가능성을 미리 염두해 두고, 헤더 파일 처음과 마지막에 extern "C" {} 가 조건부 컴파일되게 하시라"

라는 것입니다. 시스템에 설치되어 있는 C 라이브러리 헤더 파일들은 죄다 위와 같은 조건부 컴파일 구문이 들어가 있는 걸 확인할 수 있습니다. 여기에 한 술 더 떠서 g++ 헤더 파일 중 _ansi.h 에는 다음과 같이 정의되어 있네요.(이해를 돕기 위해 상당히 간소화시킨 것입니다)

#ifdef __cplusplus
#  define _BEGIN_STD_C extern "C" {
#  define _END_STD_C  }
#else
#  define _BEGIN_STD_C
#  define _END_STD_C
#endif

위와 같이 정의하고 모든 헤더 파일의 처음과 끝에 _BEGIN_STD_C, _END_STD_C 와 같은 매크로를 붙이는 것이지요. 다음과 같이요.

/* max.h - max() 함수 말고도 상당히 많은 함수가 선언되어 있다고 가정해 보겠습니다 */
#include "_ansi.h"

_BEGIN_STD_C

int max(int a, int b);

...... /* 다른 함수 정의 */

_END_STD_C

이번 글에서는 주로 name mangling과 C++ 에서 C 코드를 호출하는 방법에 대해 주로 알아 봤는데요, 다음에는 거꾸로 C에서 C++ 를 호출하는 방법에 대해 알아보도록 하겠습니다.


제가 마지막에 정리하길...

C 라이브러리를 작성하려거든 나중에 C++ 에서 활용될 가능성을 미리 염두해 두고, 헤더 파일 처음과 마지막에 extern "C" {} 가 조건부 컴파일되게 하시라

라고 했던 것 기억하시죠 ? 근데 솔직히 세상이 우리 생각대로만 돌아간다면 얼마나 좋겠습니까 ? 다들 제가 말한 팁을 알고 있다면 처음 작성할 때부터 C++ 고려해서 프로그래밍했겠지만, 실제로는 그렇지 않은 경우가 더 많기 마련입니다.

지금 우리 수중에 C++를 고려하지 않은 괜찮은 C 라이브러리가 있는데, 이걸 C++ 에서 쓰고 싶을 경우는 어떻게 해야할까요 ? 그 C 라이브러리의 헤더 파일을 편집해서 함수 선언에다가 일일이 extern "C"를 붙여댈까요 ? 그렇게라도 해서 그 C 라이브러리를 쓰시겠다면 말리진 않겠지만, 머리가 똑바로 박힌 사람이라면 한 열개쯤 extern "C" 를 붙이다가 이걸 어떻게 쉽게 해결할 방법이 없을까 하고 고민하기 시작할 겁니다. 이른바 창조적인 귀차니즘이 시작되는 거죠. 이런 창조적인 귀차니즘은 매우 바람직한 현상이니, 개발하는 동안에는 반복적인 작업을 얼마든지 귀찮아 하시기 바랍니다.

저번 글에서 extern "C" { ... } 이런식으로 { ... } 내용 전체에 대해 extern "C"를 붙이는 방법이 있다는 걸 소개해 드렸습니다. 이걸 한 번 더 응용하시면 됩니다. 다음 C 라이브러리가 C++ 를 고려하지 않고 다음과 같이 작성되어 있다고 치겠습니다.

/* max.h */
int max(int a, int b);
그럼 이걸 include 하는 C++ 소스 코드에서는 다음과 같이 하시면 됩니다.

/* cppmain.cpp */
#include <iostream>
extern "C" {
#include "max.h"
}

using namespace std;
// 나머지 내용은 동일
컴파일 해 보시면 컴파일/링크도 잘 되고 예상대로 실행도 잘되는 걸 확인하실 수 있을 겁니다. 이번글은 두 번째 팁의 A/S 글이니 여기에서 짧게 마칩니다.


 

Posted by Humaneer
Engineering/System2009/04/07 17:05

시작하기 전에

이 글은 내가 가끔은 제대로 생각하기도, 때로는 잘못생각하기도 했던 endian에 대해 확실하게 개념을 잡기위함이다. -_-
나중에 또 헷갈리면 찾아보기 위해 ㅋ

Big/Little Endian

CPU가 multi byte의 데이터를 메모리에 저장할 때 리틀 엔디안(Little Endian)과 빅 엔디안(Big Endian)의 방식이 있다. 2학년 때 어깨너머로 누구에게 잠깐 듣고나서 '으음 그렇군' 하고 넘어갔다가, 3학년 때 컴퓨터 구조수업 때 배우면서 '아하 그랬었군~!' 했던게 기억이 난다.

0x01234567 이라는 4바이트의 데이터를 메모리시작 주소 0x00에 저장한다고 하였을 때, Big/Little Endian은 아래와 같이 저장할 것이다. 간단하다 simple!

Memory Address

Data

Big Endian

Little Endian

0x00000000

0x01

0x67

0x00000001

0x23

0x45

0x00000002

0x45

0x23

0x00000003

0x67

0x01


그런데 내가 헷갈린 것은 1바이트씩 메모리에 쓸 때도 big endian이냐 little endian이냐를 따지는가, 그러지 말아야 하는가였다. 예를들면 0x01(0 0 0 0  0 0 0 1) 을 쓸 때에도 0 0 0 0 0 0 0 1인지 1 0 0 0 0 0 0 0인지가 헷갈렸었던 것이다. 너무 오버해서 생각했다고나 할까? ㅋ

endian은 multi byte를 쓸 때 어떤 순서로 쓰느냐에 따라 분기가 나뉘는 것이지 bit stream의 순서까지를 결정하는 것은 아니기 때문이다. bit stream은 Big/Little Endian에 상관없이 무조건 Big Endian이다. -_-

Intel계열 CPU는 little endian이고, 모토로라 계열은 big endian이다. 그리고 내가 업무에 쓰는 processor인 BF537역시 little endian이다. 맨날 little endian만 보다 보니 이제 요놈이 더 편한 것 같다. ㅋ

Posted by Humaneer
Engineering/C/C++2008/12/22 08:53

통신관련 프로그래밍을 하다보면, 여러 장치간의 프로토콜을 정해야할 때가 있다.
패킷의 헤더를 정해야 할 것이고, 그 밖에 여러가지 것들, 그리고 데이터까지.
그런데 데이터의 길이가 가변적이라면?


다음과 같이 정의를 할 수 있을 것이다. 자 이렇게 하였을 때 이 패킷의 사이즈는 얼마가 될까?

4 + 1 + 1 + 4 = 10바이트가 될 것이다. 하지만 아래와 같은 기법도 있다.

배열의 크기를 0으로 하다니! 저게 말이 돼? 라고 나는 생각하였지만, 컴파일을 하고 실행을 해본 결과 내가 얼마나 무지한가를 또 한 번 절감하게 되었다. ㅋ

저렇게 패킷을 정의하고 사용은 아래와 비스무리하게, 물론 응용의 방법은 많다. 포인터의 세계에서 불가능은 없으니 +_+;


실행 결과는 아래와 같다. 패킷의 사이즈가 6이다. 데이터의 길이 역시 자기 마음데로 조절할 수가 있다.
Posted by Humaneer
Engineering/C/C++2008/10/10 11:38
 
종종 헷갈리는 포인터질 -_-;

typedef가 들어가니 더 헷갈린다 훗~

Posted by Humaneer
Engineering/Algorithm2007/10/13 03:55
그냥 심심해서 구현해본 Heap Sort다. 사실 이게 진짜 Heap Sort인지는 나도 모른다-_-. 그냥 힙에 원소를 모조리 집어넣었다가 다시 모조리 빼면 정렬된 값이 나오니까 그게 정렬아닌가? ㅋㅋ 사용한 자료구조는 배열을 이용한 이진 트리이며, 나중에 이것저것 정렬해보기 위해 템플릿으로 구현하였다.

이 알고리즘의 Time Complexity를 나름 분석해보겠다. 정렬을 할 때 걸리는 시간은 직관적으로 엔트리들을 힙에 삽입하는 시간 T(enQueue)과 힙에서 엔트리를 빼내는 시간 T(deQueue)의 합을 통해 알 수있다.

1. Enqueue
Heap에 엔트리를 추가할 때 사용되는 함수는 enQueue()안에서 호출된 upWard()이다. 부모와 자식을 비교해서 자식이 크면 값을 교환하고 부모의 위치에서 자기 자신을 재귀호출 하는 간단한 함수이다. 참고로 Heap에 새로운 엔트리를 추가할 때 가장 밑에서부터 우선순위를 따지며 위로 올라오며 위치가 바뀌기 때문에 이 과정을 Reheapification Upward라고 한다. (리히피피케이션 업워드 -_-)

upWard()

source code : upWard()


InputSize : Size of Array[N],  N
Basic Operation : number of call upWard() recursively
Time Complexity : O(N * Log N)
처음에 enQueue()를 통해 배열의 마지막 부분에 엔트리를 삽입하고 그 후부터 upWard()를 이용해서 새로 삽입한 엔트리를 부모와 값을 비교하여 재귀호출을 하게 되는데, worst case가 맨 마지막에서 부터 맨 앞(root)으로 이동하는 경우인데, 이 경우 2진 트리이기 때문에 재귀호출하는 횟수는 밑수가 2인 로그의 N이 된다. 이러한 과정을 N번 되풀이 하니까 O(N * Log N)이 된다.


2. Dequeue
Heap에서 엔트리를 뽑아낼 때(제거할 때) 사용되는 함수는 remove()안에서 호출된 downWard()이다. 가장 첫번째 엔트리(root)를 빼내고, Heap에 저장된 제일 뒤에 위치한 엔트리를 root자리에 복사한 다음 downWard()를 호출하여 자식들보다 작으면 값을 교환하고 자식의 위치에서 재귀호출 하는 함수이다. 이 과정은 Reheapification Downward라고 한다. (리히피피케이션 다운워드 -_-)

downWard

source code : downWard()


InputSize : Size of Array[N],  N
Basic Operation : number of call downWard() recursively
Time Complexity : O(N * Log N)
downWard()는 deQueue()함수 안에서 root의 위치에서 부터 시작된다. 즉 downWard(0)부터 호출된다는 말이다. worst case가 root에서부터 가장 아래로(배열의 마지막으로) 내려가는 경우인데, 2진 트리이므로 밑수가 2인 로그의 N이 된다. 이러한 과정을 N번 되풀이 하므로  O(N * Log N)

삽입할 때도 O(N * Log N)이고 빼낼 때도 O(N * Log N)이다. 
따라서 이 알고리즘의 최종 Time Complexity는 O(N * Log N)이 되겠다.

전체 소스파일 (왜 이따구로 허접하게 구현했냐? 라고 물어본다면 할 말 없음 -_-ㅗ)

heap.cpp

Implementation of Heap Sort with C++

Posted by 비회원
Engineering/Algorithm2007/10/13 03:11

[1편 원문보기: http://www.gpgstudy.com/forum/viewtopic.php?t=566]
[2편 원문보기: http://www.gpgstudy.com/forum/viewtopic.php?t=627]
[3편 원문보기: http://www.gpgstudy.com/forum/viewtopic.php?t=860]


프로그래밍과 영어 by 류광

한국의 프로그래머들이 흔히 듣는 조언 가운데, ‘영어는 필수입니다’라는 것이 있다. 영어가 필수인 이유로 흔히 말하는 것은 외국 책이나 자료를 빨리 제대로 읽을 수 있어야 한다는 것이다. 그러나 프로그래머가 영어를 잘해야 하는 또 다른, 그리고 좀 더 중요한 이유는, 코딩 자체가 일종의 영작문이라고 할 수 있다는 점이다.


거의 대부분의 프로그래밍 언어들은 영어를 기반으로 하고 있다. 특히 고수준 언어로 올라갈수록 자연 언어로서의 영어와 가까와지는 경향이 보인다. 또한 설계가 잘 된 코드일수록 마치 영어로 쓰여진 문서를 읽는 느낌이 드는데, 이는 프로그래밍 방법론의 발전과도 맞닿아 있다. 절차적 언어 시절에는 프로그램 소스 코드라는 것을 컴퓨터를 위한 작업 지시서로 생각하는 경향이 있었으나, 소프트웨어의 설계 및 유지·보수에 대한 문제의식이 대두되고 객체지향적 패러다임이 대세를 이루면서 프로그램 소스 코드는 기본적으로 사람을 위한 것이며 프로그래밍 언어는 사람의 생각을 좀 더 직접적으로 나타낼 수 있어야 한다는 생각이 널리 퍼진 것 같다. 객체 지향이라는 것, 특히 ‘모든 것은 객체이다’라는 말은 결국 우리가 다뤄야 할 문제에 관련된 사물들과 개념들을 그대로 소스 코드로 옮길 수 있어야 한다는 말과 일맥상통한다는 점에서, 소스 코드가 곧 하나의 문서(일상정 의미에서의)이며 프로그래머는 곧 작가 또는 저술가라는 점을 항상 염두에 두어야 할 것이다.


앞에서도 말했듯이 프로그램 소스 코드는 컴퓨터를 위한 작업 지시서가 아니라 프로그래머 스스로가 컴퓨터를 통해서 무엇을 어떻게 하고자 하는지를 적어놓은 것이다. 따라서 코딩은 프로그래머 스스로를 위한 서술 과정이기도 하다. 그러나 불행하게도 우리는 우리의 한글로 우리의 생각을 적을 수가 없다. 물론 C# 같은 일부 언어에서는 변수나 클래스, 함수 이름 같은 식별자들에 한글, 엄밀히 말하면 유니코드의 ‘글자(letter)’에 속하는 범위의 한글 글자들을 사용할 수 있으나, 어순 자체는 여전히 프로그래밍 언어의 정의에 따르며, 그 정의라는 것은 대부분 영문법을 따른다. 애초에 한글 언어를 목표로 만들어진 프로그래밍 언어들도 있으나 대부분 기존 언어에서 흔히 보이는 핵심 키워드들을 한글화한 것일 뿐 우리말글의 어순을 따르지는 않는다. 차후에 우리말글의 어순을 따르며 우리말글을 사용하는 사람의 생각을 그대로 자연스럽게 표현할 수 있는 언어가 나오기를 바라나, 어쨌든 지금은 영어에 익숙해질 수밖에 없다.


이 글에서는 영어에 관련해서 프로그래머가 좀 더 나은 소스 코드를 작성하는 데 도움이 될만한 몇 가지 주제들을 이야기하겠다.


그 전에 한가지 강조하고 싶은 것이 있다. 많은 프로그래머들이 고교 과정때까지 배운 수학과 물리학을 전혀 활용하지 못하고 모든 것들을 다시 시작하는 모습을 많이 보는데 무척 안타까운 일이다. 예를 들어 수학 시간에 배운 다각형과 프로그래밍 책에 나오는 폴리곤을 다른 것으로 생각하는(이는 저자나 번역가의 잘못이 크다) 사람은 그 수학 시간만큼의 시간을 낭비하는 것이다. 그와 마찬가지로, 고교 과정까지 배운 영어만으로도 커다란 재산이며, 프로그래머의 코딩 실력에 커다란 뒷받침이 된다. 품사니 태, 법, 3형식 4형식 같은 것들을 자꾸 떠올려보면서 이 글을 읽었으면 한다.


0. 주석


흔히 하는 이야기로, 주석은 없어서도 안 되지만 필요 이상으로 많아서도 안된다. Martin Fowler의 책 Refactoring 을 보면, ‘주석을 쓸 필요가 있다고 느낀다면, 먼저 주석이 불필요해질 때까지 코드를 리팩토링해보라’라는 말이 나올 정도이다. 이는, 역시 흔히 하는 이야기로 ‘잘 쓴 코드는 그 자체가 주석이다’라는 말과 일맥상통하는 말이다. 예를 들어, Refactoring의 Extract Method 항목을 보면 이런 예가 나온다.


코드:
void printOwing(double amount)
{
    printBanner();

    //print details
    System.out.println("name:" + _name);
    System.out.println("amount" + amount);
}


===>


void printOwing(double amount)
{
    printBanner();

    printDetails(amount);
}


void printDetails(double amount) {
    System.out.println("name:" + _name);
    System.out.println("amount" + amount);
}


이 예는 코드의 블럭에 주석을 붙여서 설명을 하는 대신, 블럭이 하는 일을 직접 알 수 있는 이름의 메서드를 추가함으로써 주석 자체가 필요없게 만든 것이다.
그러나, 주석에 관련된 이러한 지침들과 조언들은 대부분 프로그래머가 영어에 익숙하다는 전제를 깔고 있다. 아주 극단적인 예로, 다음 두 코드 조각들을 비교해보자.


코드:
1.
void printOwing(double amount)
{
    printBanner();

    printDetails(amount);
}


2.
// 고객의 미지불 정보를 출력. a는 미지불 총액
void print1(double a)
{
    // 배너를 출력한다
    print2();

    // 미지불 정보를 출력한다.
    print3(a);
}


영어에 익숙한 사람이라면 당연히 1번 예가 더 낫다고 생각할 것이며, 주석을 달 필요를 전혀 못느낄 것이다. 그러나 만일 owing이나 banner, amount 같은 단어를 모르는 사람이라면 1번은 주석이 전혀 없는 난해한 코드, 2번은 친절한 주석이 달린 알기 쉬운 코드라고 생각할 수 있다.

이 이야기의 교훈은 간단하다. 깔끔한 코드를 원한다면 영어에 더 익숙해져야 하며, 그게 싫으면 주석이라도 많이 집어넣으라는 것. 물론 선택은 여러분의 몫이다.


1. 객체 지향 언어의 표기법과 영어
객체 지향 언어들의 표기 방식에서 영어보다는 한글 어순에 가까운 용법들이 보인다는 점을 지적하는 사람들이 있다. 예를 들어 어떤 문을 연다고 하자. 절차적, 즉 함수와 자료구조 중심의 프로그래밍 언어라면,

open(theDoor);
이런 형태로 표기하게 될 것이다. 이는 영어의 명령문 어순(서술어-목적어)과 거의 일치한다.
Open the door.

반면 객체 지향적 프로그래밍 언어라면,
theDoor.open();
이는 차라리 한글 어순에 더 맞는 듯 보인다. 그 문(the door)을 열어라(open).

그러나 이는 단지 우연일 뿐인 것 같다. theDoor.open()을 일상 영어로 표시하자면 다음에 가까울 것이다.
Door, open yourself!

(yourself는 그냥 붙인 것이 아니다. 실제로, c++에서는 open()이 호출될 때 암묵적으로 this 포인터가 전달된다. this가 바로 yourself에 해당한다).

절차에서 객체로 오면서, 바뀐 것은 어순이 아니라 명령의 대상일 뿐이다. 즉 open(theDoor)는 컴퓨터 자체에게 문을 열라고 명령하는 것이라면, theDoor.open()은 ‘문’이라는 객체에게 명령을 내리는 것(객체 지향 세계의 용어로 말한다면 객체에 메시지를 전달)이다.

어쨌거나 중요한 것은, 객체 지향 언어를 만드는 사람들은 스스로 자각했든 그렇지 않든 여전히 영어를 염두에 두면서 언어를 설계했다는 점 - 객체 지향에서도 영어는 여전히, 아니 더욱더 중요하다.


2. 명사

영문법의 명사는 프로그램 소스 코드의 변수나 상수, 클래스 등의 이름에 해당한다. 명사는 행위의 주체(주어) 또는 행위의 대상(목적어)으로 쓰이곤 한다.

코딩 스타일 표준에 따라서는 변수 이름을 소문자로 시작할 수도 있고 대문자로 시작할 수도 있다. 물론 접두어가 붙는 경우 접두어는 소문자로 쓰는 것이 일반적인 관례이나, 변수의 의미를 알려주는 주된 단어 자체는 대문자로 하는 것이 바람직할 것 같다.

변수를 대문자로 시작하는 것이 좋은 첫 번째 이유는, 변수를 일종의 고유 명사로 볼 수 있기 때문이다. 알다시피 영어에서 고유 명사는 항상 대문자로 시작한다. 두 번째로, 특히 객체 지향적 언어에서는 문장 제일 처음에 나오는 단어가 변수(객체)인 경우가 많다. 절차적 언어에서는 배정문이나 기타 제어문이 아닌 나머지는 모두 함수 호출이며, 따라서 항상 동사가 문장 처음에 나오는 셈이 된다. 그러나 객체 지향 언어에서는 어떠한 동작을 수행할 때 우선 객체 변수가 나오고 그 다음에 그 객체의 메서드가 나오는 식이며, 따라서 문장 처음에 변수 즉 명사가 나오게 된다. 역시 알다시피 영어에서 문장 첫 단어는 대문자로 쓴다.


3. 관사

변수 이름에는 접두어가 붙기도 한다. C 중심 API에서는 흔히 변수의 자료형을 의미하는 접두어를 붙이곤 했으나, 엄격한 형검사를 수행하는 C++ 같은 언어에서는 굳이 변수의 이름에 자료형 정보를 추가할 필요는 없다고 생각한다. 굳이 접두어를 붙여야 한다면 자료형보다는 변수의 범위에 대한 정보를 제공하는 접두어가 더 유용할 것이다.

영어 문장에 좀 더 가까운 코드를 만들기 위해서라면, 변수의 범위를 ‘관사’로 표현하는 것이 바람직할 것이다. 예를 들어 함수의 지역 변수나 매개 변수에는 a(an)를, 전역 변수나 멤버 변수에는 the를 붙이는 식이다. 이는 일상 영어에서 a와 the의 일반적인 의미를 반영한 것이다. 예를 들어 an apple은 불특정한 하나의 사과를 뜻하나, the apple은 해당 맥락에서 이미 존재하고 있던 또는 화자들이 익히 알고 있는 특정한 사과를 뜻한다. 지역 변수는 해당 범위에 진입할 때 초기화되고 범위를 벗어나면서 사라진다는 점에서 a(an)를 붙이는 것이 의미가 있으며, 전역 변수는 전역 아래의 개별 범위들에서도 이미 존재하며 언제라도 접근할 수 있다는 점에서 the를 붙이는 것이 의미가 있다.

예:
코드:
int getCurrentDistanceFromOrigin(int aX, aY)
{
    return getDistanceBetween(
        Position(theOriginX, theOriginY), Position(aX, aY)
    );
}
 

4. 형용사

명사에는 형용사가 붙기도 한다. 형용사는 명사에 대한 추가적인 정보를 제공한다. 영어의 경우 형용사는 명사 앞에 붙는다. 예를 들어 a red apple. 그렇다면 프로그래밍에서 형용사는 어떻게 표현될까?
형용사는 명사에 대한 추가적인 정보를 제공하기 위한 것이라는 점을 생각하면, 형용사는 객체의 상태를 의미하는 멤버 변수 또는 속성에 해당한다고 할 수 있다. 예를 들어 a red apple을 프로그래밍적으로 표현한다면,

코드:
class Apple
{
private:
    Color theColor;
public:
    void setColor(Color aColor) { theColor = aColor; }
};

...


Apple aApple;
aApple.setColor( Color("Red") );

이는 결국, 뭔가를 코드로 표현하고자 할 때 어떤 형용사를 사용하려고 한다면, 그 형용사 하나만 추가할 수는 없다는 뜻이 된다. 형용사를 사용하려고 하면, 사용하고자 하는 형용사들의 범주를 의미하는 ‘명사’를 정하고 그 범주를 명사에 속하는 하나의 멤버로 만들어야 한다는 뜻이 된다. 위의 예에서 red라는 형용사를 사용하기 위해서는 color라는 일반화된 ‘명사’를 생각해 낼 수 있어야 한다 - 사실 이는 객체 지향적 모델링의 기본이라 할 수 있다. 주어진 대상에서 중요한 명사들을 뽑아내는 것은 가장 기본적인 객체 식별 방법이기 때문이다.




[1편 원문보기: http://www.gpgstudy.com/forum/viewtopic.php?t=566]
[2편 원문보기: http://www.gpgstudy.com/forum/viewtopic.php?t=627]
[3편 원문보기: http://www.gpgstudy.com/forum/viewtopic.php?t=860]

Posted by 비회원