|
|
|
|
LIST BOX 생성 리스트 박스 리소스 에디터에서 생성하시구, 적당한 곳에 위치 시키세요. 그리구 옵션(Properties)에 보면 Owner Draw 항목이 있을껍니다. 그 부분을 Variable 로 선택하시면 됩니다. 그리구, No, Fixed, Variable 이렇게 3 항목에 부연설명을 좀 하자면... No는 시스템이 알아서 그리는거구요. Fixed는 항목 들이 각각 크기가 같은 오너 드로우 이구요. Variable은 항목 별로 각각 크기가 다른 오너 드로우 입니다. # 윈도우 메시지 오버라이딩 - 먼저 WM_MEASUREITEM 메시지를 오버라이딩 합니다.그리기 전에, 항목의 크기를 계산하여 넘겨줄때 쓰이지 메시지 입니다. 이부분에 줄내림을 생각해서 항목의 크기를 구하셔서 대입해주시면 됩니다. 참고로, 한번 불려서 그안에서 항목 갯수만큼 루프를 돌리는 것이 아니구요. 항목 갯수만큼 불립니다. 따라서... 그때 그때 오는 항목에 대해서만 처리를 해주면 됩니다. 그럼 예제 코드... (똑같이 하시면 안되구요. 원하시는대로 동작하게끔 다시 작성하세요) void Cmultiline_listboxDlg::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct) { if(m_listbox.GetDlgCtrlID() == nIDCtl) { // 리스트 박스의 크기 구하기 RECT rcListBox; m_listbox.GetClientRect(&rcListBox); CRect rc; rc.SetRect(rcListBox.left+10, rcListBox.top, rcListBox.right-10, rcListBox.bottom); // 화면에 찍었을때 글자의 RECT 구하기 (DrawText의 DT_CALCRECT 플래그) CPaintDC dc(this); dc.DrawText(reinterpret_cast<LPCTSTR>(lpMeasureItemStruct->itemData), -1, &rc, DT_CALCRECT | DT_WORDBREAK); // 구한 값을 리턴하기 위해 구조체 포인터 멤버에 대입 lpMeasureItemStruct->itemHeight = rc.Height(); lpMeasureItemStruct->itemWidth = rc.Width(); } //CDialog::OnMeasureItem(nIDCtl, lpMeasureItemStruct);} - 그리는 메시지 WM_DRAWITEM 오버라이딩아시겠지만... 넘어오는 구조체의 데이터로 화면에 그려주는 부분입니다. 그리고, 여기도 OnMeasureItem 처럼 항목 갯수 만큼 불리므로, 그때 그때 오는 것만 처리해주시면 됩니다. 특별히 어려운것은 없으니 예제 코드만 보여 드릴게요... void Cmultiline_listboxDlg::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct) { if(m_listbox.GetDlgCtrlID() == nIDCtl) { CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC); //SELECTED 아이템인가에 따라 색상 결정 COLORREF colText, colBack; colText = (lpDrawItemStruct->itemState & ODS_SELECTED) ? GetSysColor(COLOR_HIGHLIGHTTEXT) : GetSysColor(COLOR_WINDOWTEXT); colBack = (lpDrawItemStruct->itemState & ODS_SELECTED) ? GetSysColor(COLOR_HIGHLIGHT) : GetSysColor(COLOR_WINDOW); pDC->SetTextColor(colText); pDC->SetBkColor(colBack); // 배경 그리기 CRect rc(lpDrawItemStruct->rcItem); pDC->FillRect(&rc, &CBrush(colBack)); // 글자 찍기 rc.MoveToX(5); pDC->DrawText(reinterpret_cast<LPCTSTR>(lpDrawItemStruct->itemData), -1, &rc, DT_WORDBREAK); } //CDialog::OnDrawItem(nIDCtl, lpDrawItemStruct);}
출처 : http://blog.naver.com/doblex/71246764
일단 clip 배열은 rgb(8bit) 값의 table 이고, table 의 구조는 clip + 0 ~ clip + 319 까지는 0 , clip + 320 ~ clip + 320 + 255 까지는 rgb 값(0 ~ 255) clip + 320 + 256 ~ clip + end 까지는 255(0xff) 의 값으로 채워진다.
위와 같이 table을 구성 해놓고, clip 함수로 사용 시에 table index "x" 의 값이 음수이거나 255(0xff)보다 클 경우 0 또는 255를 return.
그런데, 정상 값 앞뒤로 왜 320 이냐...
//R = 1.164(Y - 16) + 1.596(Cr - 128) //G = 1.164(Y - 16) - 0.813(Cr - 128) - 0.391(Cb - 128) //B = 1.164(Y - 16) + 2.018(Cb - 128) 위 수식에서 R의 값은 0 <= Y, Cr <= 255 이므로, 1.164(0 - 16) + 1.596(0 - 128) = -222.912, 1.164(255 - 16) + 1.596(255 - 128) = 480.888
480 - 256... end point는 넘지 않음.. 일단 별로 의미 없음으로 결론.
unsigned char clip[896];
void InitClip(){ memset(clip, 0, 320); for (int i=0; i<256; ++i) clip[i+320] = i; memset(clip+320+256, 255, 320); }
inline unsigned char Clip(int x) { return clip[320 + ((x+0x8000) >> 16)]; }
inline unsigned char Clip2(int x) { return clip[320 + x]; }
//convert YUY2 to RGB24 //R = 1.164(Y - 16) + 1.596(Cr - 128) //G = 1.164(Y - 16) - 0.813(Cr - 128) - 0.391(Cb - 128) //B = 1.164(Y - 16) + 2.018(Cb - 128) const long cy = int(1.164*65536+0.5); const long cu1 = int(1.596*65536+0.5); const int cu2 = int(-0.813*65536+0.5); const int cv1 = int(-0.391*65536+0.5); const int cv2 = int(2.018*65536+0.5);
void ConvertYUY2toRGB24(const unsigned char* src, unsigned char* dst, int width, int height) { for (int row = 0; row < height; ++row) { const unsigned char* yuv = src + width * 2 * row; unsigned char* rgb; rgb = dst + width * 3 * (height-1-row);
for (int col = 0; col < width; col += 2) { long y1=yuv[0]; long y2=yuv[2]; long v1=yuv[1]; long u1=yuv[3]; rgb[0]=Clip2((cy*(y1-16)>>16) + (cv2*(v1-128)>>16)); rgb[1]=Clip2((cy*(y1-16)>>16) + (cu2*(u1-128)>>16) + (cv1*(v1-128)>>16)); rgb[2]=Clip2((cy*(y1-16)>>16) + (cu1*(u1-128)>>16)); rgb[3]=Clip2((cy*(y2-16)>>16) + (cv2*(v1-128)>>16)); rgb[4]=Clip2((cy*(y2-16)>>16) + (cu2*(u1-128)>>16) + (cv1*(v1-128)>>16)); rgb[5]=Clip2((cy*(y2-16)>>16) + (cu1*(u1-128)>>16)); yuv += 4; rgb += 6; } } }
YUV to RGB 변환 코드를 작성하시오. step1) #define CLIP(x) (((x) < 0) ? 0 : (((x) > 255) ? 255 : (x))) void Yuv2Rgb(int Y, int U, int V, int &R, int &G, int &B) { B = 1.164*(Y-16) + 2.018*(U-128) ; G = 1.164*(Y-16) - 0.813*(V-128) - 0.391*(U-128) ; R = 1.164*(Y-16) + 1.596*(V-128) ;
B = CLIP(B); G = CLIP(G); R = CLIP(R); } => YUV, RGB 변환 공식은 아래와 같다.
B = 1.164*(Y-16) + 2.018*(U-128) G = 1.164*(Y-16) - 0.813*(V-128) - 0.391*(U-128) R = 1.164*(Y-16) + 1.596*(V-128) (YUV 포멧 및 RGB 변환에 관해서는 http://www.fourcc.org/를 참조하라.) 위의 공식은 실제 몇몇 픽셀에서 overflow 가 발생할 것이다.
0~255의 범위를 벗어나는 값이 나온다. 이를 방지하기 위해, 0~ 255 값으로 clipping 이 필요하다. step2)
#define CLIP(x) (((x) < 0) ? 0 : (((x) > 255) ? 255 : (x))) void Yuv2Rgb(int Y, int U, int V, int &R, int &G, int &B) { B = ( 76284*(Y-16) + 132252*(U-128) ) >> 16; G = ( 76284*(Y-16) - 53281*(V-128) - 25625*(U-128) ) >> 16 ; R = ( 76284*(Y-16) + 104595*(V-128) ) >> 16 ; B = CLIP(B); G = CLIP(G); R = CLIP(R); } => YUV, RGB 변환 공식을 약간 변형하면 아래와 같다.
B = 65536*(1.164*(Y-16) + 2.018*(U-128) ) / 65536 G = 65536*(1.164*(Y-16) - 0.813*(V-128) - 0.391*(U-128) ) / 65536 R = 65536*(1.164*(Y-16) + 1.596*(V-128) ) / 65536 B = ( 76284*(Y-16) + 132252*(U-128) ) >> 16 G = ( 76284*(Y-16) - 53281*(V-128) - 25625*(U-128) ) >> 16 R = ( 76284*(Y-16) + 104595*(V-128) ) >> 16 변형된 공식을 사용함으로서, floating point 연산을 없엘 수 있다. var viewer_image_url = "http://blogimgs.naver.com/blog20/blog/layout_photo/viewer/"; var photo = new PhotoLayer(parent.parent.parent); photo.Initialized(); window.onunload = function() { photo.oPhotoFrame.doFrameMainClose(); }.bind(this);
잠이 오질 않는다..
"대통려의 죽움이 아닌 인간 노무현의 죽음이기에 너무 마음이 아프다" 라는 웹에서 본 문구가 가슴에 너무 와닿는 지금
" 겁이나는 것은 사람들의 시선이고 상처받는 것은 사람들의 말이고 가장 무서운 것은 인간이다." 라는 글귀가 오늘 따라 머리에서 지워지질 않는다. 나 자신의 간악한 마음을 들여다 보며, 늦으나마 참회의 시간을 갖어 본다.
부디... 노무현님 평온한 영면에 드시길 두손 모아 기원합니다...
분양소에 분양을 다녀오고나서... 영결식과 노제에 다녀오고나서... 마음이 더 무거운 건 왜 일까.......
다.매크로 분석가변 인수 함수는 만드는 방법과 주의 사항 등에 대해 알아 봤는데 사용만을 목적으로 한다면 여기까지만 이해해도 충분하다. 그러나 가변 인수를 읽어 내는 매크로들이 어떤 식으로 동작하는지, 포인터를 어떻게 조작하길레 임의 타입의 인수를 자유 자재로 읽을 수 있는지 호기심이 발동한다면 va_ 매크로를 분석해 보도록 하자. 이 매크로들은 길이가 짧지만 포인터와 sizeof 연산자, 그리고 비트 연산자들이 어떻게 절묘하게 동작하는지 감상해 볼 수 있는 좋은 예제이다. 가변 인수와 관련된 타입과 매크로는 표준 헤더 파일 stdarg.h에 정의되어 있다. 이 헤더 파일을 직접 열어 보면 플랫폼별로 va_ 매크로들이 각각 작성되어 있는데 대부분의 경우 인텔 계열의 CPU를 사용하고 있으므로 매킨토시나 알파, MIPS 같은 경우는 무시하고 X86 계열의 경우만 분석해 보도록 하자. 실제 매크로 구문은 컴파일러마다 조금씩 다른데 아래 코드는 가장 간략하게 잘 정리되어 있다고 생각되는 비주얼 C++ 6.0의 stdarg.h 헤더 파일에 기록된 내용이다. typedef char * va_list; #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) #define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) #define va_end(ap) ( ap = (va_list)0 ) 먼저 va_list에 대한 타입 정의를 볼 수 있는데 va_list는 단순한 char *형으로 정의되어 있다. 여기서 char에 대한 포인터라는 것은 별다른 의미는 없고 증감할 때 1바이트씩 증감하도록 하기 위해 char형 포인터로 선언된 것이다. 실제로 어떤 컴파일러는 va_list를 void *로 정의하는 것도 있다. 중요한 것은 va_list 타입이 포인터 타입이라는 것이다. _INTSIZEOF(n) 매크로는 인수로 전달된 타입 n의 길이를 계산하는데 n의 값에 따라 이 매크로의 계산 결과가 어떻게 되는지 조사해 보자. 매크로의 연산식을 엄밀하게 분석해 보면 각 타입의 크기가 얼마로 계산될 지 예측할 수 있지만 이럴 때는 그냥 프로그램을 하나 만들어 확인해 보는 것이 더 간편하고 확실하다. printf("char = %d\n",_INTSIZEOF(char)); printf("short = %d\n",_INTSIZEOF(short)); printf("int = %d\n",_INTSIZEOF(int)); printf("float = %d\n",_INTSIZEOF(float)); printf("double = %d\n",_INTSIZEOF(double)); 크기별로 각 타입에 대해 _INTSIZEOF 매크로가 어떤 값을 계산해 내는지 출력해 보았다. 결과는 다음과 같다. char = 4 short = 4 int = 4 float = 4 double = 8 char형의 크기는 1이지만 이 매크로에 의해 4로 계산되며 short, float도 4가 되며 double은 8이 된다. 이 매크로가 하는 일은 타입의 크기를 4의 배수로 올림해 준다고 할 수 있는데 좀 더 정확하게 표현하자면 정수형의 크기에 대한 배수로 올림한다. 알다시피 정수형의 크기는 시스템마다 다른데 16비트 환경에서는 2바이트이고 32비트 환경에서는 4바이트이며 이 크기는 또한 스택 하나의 크기이기도 하다. 결국 이 매크로는 각 타입의 변수가 스택을 통해 함수로 전달될 때 몇 바이트를 차지하는가를 계산해낸다. char형이 1바이트라도 함수의 인수로 전달될 때는 int형으로 확장되므로 스택에는 4바이트로 들어가며 _INTSIZEOF는 인수가 스택에 들어가 있을 때의 크기를 계산해 내는 것이다. 아주 간단한 동작을 하는 매크로이지만 플랫폼에 따른 스택의 크기까지 고려하여 이식성을 보장할 수 있도록 잘 작성되어 있다. 4바이트의 배수 타입에 대해서 _INTSIZEOF는 sizeof 연산자와 실질적으로 동일하다. va_start 매크로는 가변 인수의 위치를 가리키는 포인터 ap를 초기화하는데 이 초기화를 위해 마지막 고정 인수 v를 전달해 주어야 한다. ap는 마지막 고정 인수 v의 번지에 v의 크기를 더한 번지로 초기화된다. 스택에 인수가 들어갈 때는 전달된 역순으로 들어가므로 가변 인수들이 먼저 전달(높은 번지)되고 고정 인수가 제일 끝에 전달(낮은 번지)된다. 따라서 가변 인수 함수가 호출된 직후의 스택 모양은 다음과 같다.
이 상태에서 &v는 고정 인수의 번지를 가리키며 이 번지를 char *로 캐스팅한 후 고정 인수의 길이만큼 더하면 바로 아래에 있는 첫 번째 가변 인수의 번지를 구할 수 있다. va_start 매크로는 이 연산을 통해 ap에 가변 인수의 시작 번지를 초기화한다. 이후 ap에 있는 값을 읽으면 가변 인수의 값을 구할 수 있는데 이 동작을 하는 매크로가 바로 va_arg 매크로이다.
va_arg 함수는 ap를 일단 가변 인수의 길이만큼 더해 다음 가변 인수 번지로 이동시킨다. 그리고 다시 길이를 빼서 원래 자리로 돌아온 후 이 번지를 t타입의 포인터로 캐스팅하여 * 연산자로 그 값을 읽는다. 이 매크로는 ap의 값을 읽기만 하는 것이 아니라 다음번 va_arg 호출을 위해 ap를 방금 읽은 가변 인수 다음의 번지로 옮겨 주는 동작까지 해야 하기 때문에 길이를 더했다가 다시 뺀 후 그 위치를 읽도록 되어 있다. 이 매크로의 동작을 그림으로 그려 보면 다음과 같다.
중간 변수를 사용하지 않고 매크로 한 줄로 값을 읽기도 하고 ap를 다음 위치로 옮겨 놓기도 해야 하므로 조금 복잡하게 되어 있는데 매크로 구문의 연산 순서에 따라 어떤 동작들이 일어나는지를 보도록 하자.
va_arg(ap,t) 호출문은 ap 번지에 있는 가변 인수를 t 타입으로 읽고 그 길이만큼 ap를 증가시켜 다음 가변 인수를 읽을 수 있도록 한다. 그래서 va_arg를 계속 호출하면 가변 인수들을 연속적으로 액세스할 수 있다. 단, va_arg가 정확하게 읽고 길이만큼 다음 위치로 이동하기 위해서는 가변 인수의 타입을 반드시 알려 주어야 한다.
마지막으로 va_end 매크로는 가변 인수를 가리키던 ap 포인터를 NULL로 만들어 무효화시키는데 사실 이 동작은 굳이 필요치 않다. 어차피 ap는 지역 변수로 선언되었고 함수가 종료되면 사라지므로 어떤 값을 가지더라도 아무 문제가 없으며 실제로 va_end 호출을 빼도 별 문제없이 잘 동작한다. va_end 매크로는 미래의 플랫폼에서 가변 인수를 읽는 방법이 달라질 경우 뒷정리를 할 수 있는 위치를 확보하는 역할 이외에는 아무 의미가 없다. 가변 인수 함수의 예제로 최초 작성했던 GetSum 함수를 매크로를 쓰지 않고 전개해서 간략하게 다시 작성해 보면 다음과 같다. 동작은 완벽하게 동일하다. int GetSum(int num, ...) { int sum=0; int i; // va_list ap; char *ap; int arg; // va_start(ap,num); ap=(char *)&num+sizeof(num); for (i=0;i<num;i++) { // arg=va_arg(ap,int); arg=*(int *)ap; ap+=sizeof(int); sum+=arg; } // va_end(ap); return sum; } 보다시피 가변 인수 함수는 포인터 연산, sizeof 연산자, 캐스트 연산자들의 절묘한 조합에 의해 동작한다는 것을 알 수 있다. 이 동작을 좀 더 쓰기 쉽고 호환성과 이식성에 유리하도록 정리해 놓은 것이 바로 va_ 매크로이다. 라.가변 인수 함수의 활용가변 인수 함수는 한 줄에 여러 개의 정보를 다양한 방법으로 다룰 수 있다는 면에서 편리하다. printf 함수는 다양한 타입의 변수들을 한꺼번에 출력할 수 있어 변수값을 확인해 볼 때 아주 유용하다. 이런 함수를 직접 만들려면 독자적으로 서식을 정의하고 서식 문자열과 대응되는 가변 인수를 직접 읽는 복잡한 루틴을 만들어야 하는데 다행히 이런 일을 대신해 주는 함수들을 준비되어 있다. 대표적으로 다음 두 함수만 소개 한다. int vprintf( const char *format, va_list argptr ); int vsprintf( char *buffer, const char *format, va_list argptr ); 이외에 vscanf, vsscanf 등의 함수도 있는데 알파벳 v로 시작한다고 해서 이런 함수들을 v계열의 함수라고 한다. 위 두 함수들은 printf, sprintf와 동일한 기능을 수행하는데 가변 인수를 직접 나열하는 대신 가변 인수가 시작되는 번지만을 인수로 취한다는 점이 다르다. 즉 실제로 가변 인수를 취하지는 않으며 가변 인수를 취하는 다른 함수의 내부에서 printf 의 서식을 해석하고 적용하는 일을 대신해 준다. 이 두 함수를 사용하면 printf처럼 동작하는 비슷한 함수를 직접 만들어 쓸 수 있다. 다음 함수는 C/C++언어의 가변 인수 기능을 활용하여 실행중에 변수값을 디버거로 실시간 확인해 보는 기능을 제공한다. void CustomTrace(char *format, ...) { char buf[1024]; va_list marker; va_start( marker, format ); vsprintf(buf,format,marker); OutputDebugString(buf); } OutputDebugString이라는 API 함수가 사용되었는데 이 함수는 주어진 문자열을 디버깅 창으로 출력해 준다. 비주얼 C++의 경우 Output 윈도우에 이 함수의 출력 내용이 나타나므로 실행중에 변수값의 변화를 확인하거나 특정 함수의 호출 시점, 회수 등을 알고 싶을 때 중간 중간에 이 함수를 삽입해 주면 된다. 사용예를 들자면 다음과 같다. CustomTrace("변수 a=%d, 변수 f=%f\n",a,f); CustomTrace("함수 func가 %d번째 호출되었음",count++); CustomTrace 함수의 내부는 무척 간단하다. va_start로 첫 번째 가변 인수의 번지를 구한 후 그 번지를 서식 문자열과 함께 vsprintf 함수로 넘겨 주기만 하면 된다. OutputDebugString 함수를 직접 사용할 수 있지만 이 함수는 단순한 문자열만 출력할 수 있는데 비해 CustomTrace는 서식화된 문자열을 출력할 수 있어 훨씬 더 편리하다. 다음은 똑같은 목적의 좀 더 복잡한 함수를 소개한다. 이 함수는 Win32 파일 입출력 함수까지 사용하고 있기 때문에 현재 단계에서 분석해 보기는 어려우므로 차후에 API를 배운 후에 직접 분석해 보기 바란다. #define DEBUGLOGFILE "c:\\DebugLog.txt" void WriteLogFile(char *strLog,...) { HANDLE hLog; static int count=0; DWORD dwWritten; char szLog[1024]; char strLog2[1024]; va_list marker; SYSTEMTIME st; // 가변 인수를 조립한다. va_start( marker, strLog ); vsprintf(szLog,strLog,marker); // 처음 호출될 때 파일을 만들고 이후부터는 파일을 열기만 한다. if (count == 0) { hLog=CreateFile(DEBUGLOGFILE,GENERIC_WRITE,0,NULL, CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL); } else { hLog=CreateFile(DEBUGLOGFILE,GENERIC_WRITE,0,NULL, OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL); } // 로그에 현재 시간과 카운터를 기록한다. GetLocalTime(&st); wsprintf(strLog2,"카운터=%06d(%d:%d:%d:%d) %s\r\n",count++, st.wHour,st.wMinute,st.wSecond,st.wMilliseconds,szLog); SetFilePointer(hLog,0,NULL,FILE_END); WriteFile(hLog,strLog2,strlen(strLog2),&dwWritten,NULL); CloseHandle(hLog); } 사용하는 방법은 printf나 CustromTrace와 동일하다. 단, 차이점이라면 조립된 서식 문자열이 화면이나 디버깅 창으로 출력되는 것이 아니라 지정한 파일에 기록된다는 것과 카운트, 호출 시점의 시간 등을 같이 기록해 준다는 점이다. 멀티 스레드 환경이나 실시간으로 동작하는 프로그램을 디버깅할 때는 디버거를 쓰기 쉽지 않기 때문에 모든 디버깅 정보를 파일에 일단 기록한 후 파일에 남겨진 로그 정보를 분석하는 것이 더 효율적이다. 이럴 때 이 함수가 아주 유용하게 사용되며 실전에서 여러 번 활용했었는데 효과가 아주 좋았다. 릴리즈 모드에서만 증상이 나타날 때라든가 디버거를 쓸 수 없는 서비스류의 프로그램을 디버깅할 때 특히 많은 활약을 한다. 물론 이 함수는 더 개선해 볼 여지가 많은데 다음에 실력이 늘면 직접 개선해 보기 바란다.
나.가변 인수 함수의 조건가변 인수 함수는 인수의 개수와 타입에 대한 제약이 없지만 그렇다고 해서 아무 인수나 마음대로 전달할 수 있는 것은 아니다. 가변 인수 함수에도 지켜야 할 규칙들이 있는데 이 규칙에 대해 알아 보자. 가변 인수 함수는 반드시 하나 이상의 고정 인수를 가져야 한다. 첫 번째 인수부터 가변 인수일 수는 없는데 왜냐하면 가변 인수를 읽기 위한 포인터 ap를 초기화하기 위해서 마지막 고정 인수의 번지를 알아야 하기 때문이다. va_start 매크로는 마지막 고정 인수의 번지에 길이를 더해 가변 인수가 시작되는 번지를 계산하는데 고정 인수가 없으면 이 매크로가 동작하지 않는다. GetSum 함수는 인수의 개수를 전달하는 num 고정 인수를 가지며 printf 함수도 서식 문자열을 첫 번째 인수로 가진다. 만약 고정 인수를 가지지 않는 가변 인수 함수를 꼭 만들고 싶다면 va_ 매크로를 쓰는 대신 스택을 직접 뒤지는 방법을 사용할 수는 있다. 하지만 컴파일러마다 함수를 호출할 때 스택을 조작하는 방법이 다르고 어셈블리를 직접 사용해야 하기 때문에 일반적으로 불가능하다고 보는 편이 옳다. 또한 바로 다음의 2, 3번 규칙을 만족하기 위해서도 고정 인수가 필요하다. 가변 인수들을 일관된 방법으로 읽기 위해서는 반드시 하나 이상의 고정 인수가 있어야 한다. 함수 내부에서 자신에게 전달된 가변 인수의 개수를 알 수 있도록 해야 한다. 전달될 수 있는 인수의 개수에는 제한이 없으며 컴파일러는 함수가 호출될 때 인수의 개수를 점검하지도 않는다. 그래서 호출측에서 가변 인수가 몇개나 전달되었는지를 알려 주지 않으면 함수 내부에서 인수의 개수를 알 수 있는 방법이 전혀 없다. 함수 스스로 인수의 개수를 파악할 수 있도록 호출측이 정보를 제공해야 한다. GetSum 함수는 첫 번째 고정 인수 num을 통해 뒤쪽의 가변 인수가 몇개나 전달되었는지를 알려 주도록 되어 있으며 함수 내부에서는 num만큼 루프를 돌면서 va_arg로 인수들을 읽었다. 만약 num 인수가 없다면 GetSum 함수는 루프를 얼마만큼 돌아야 할 지 결정할 수 없을 것이다.
GetSum 함수의 예처럼 가변 인수의 개수를 고정 인수로 알려 주는 것은 가장 쉽기는 하지만 개수를 바꿀 때마다 고정 인수를 수정해야 하므로 불편할 수도 있다. 고정 인수로 개수를 전달하는 것이 귀찮다면 가변 인수의 목록 끝에 특이값을 전달하는 방법을 쓸 수도 있는데 예를 들어 인수값 중 0을 만나면 이 값을 가변 인수의 끝으로 인식하도록 약속을 하는 것이다. 이런 방법으로 GetSum 함수를 수정해 보았다.
#include <Turboc.h> void PrintSum(const char *msg, ...) { int sum=0; va_list ap; int arg; va_start(ap,msg); for (;;) { arg=va_arg(ap,int); if (arg == 0) { break; } sum+=arg; } va_end(ap); printf(msg,sum); } void main() { PrintSum("1+2=%d\n",1,2,0); PrintSum("3+4+5+6=%d\n",3,4,5,6,0); PrintSum("10~15=%d\n",10,11,12,13,14,15,0); } GetSum 함수를 수정한 PrintSum 함수는 서식 문자열과 여러 개의 정수값들을 인수로 전달받되 가변 인수의 끝에는 0을 두어 0을 만날 때까지 모든 인수의 값을 합해 메시지를 직접 출력한다. 함수 내부의 루프는 무한 루프로 수정되었으며 읽은 인수값이 0일 때까지 루프를 돌도록 했다. 실행 결과는 앞에서 만든 예제와 동일하다. GetSum 함수는 가변 인수의 개수를 고정 인수를 통해 직접적으로 알려 주도록 했으며 PrintSum 함수는 개수는 알려 주지 않되 가변 인수의 끝을 알려 주는 특별한 표지값을 전달함으로써 이 값이 나올 때까지 가변 인수를 취할 수 있도록 했다. 어떤 방법을 쓰든지 어쨋든 함수 내부에서 가변 인수의 개수를 알 수 있도록만 해 주면 된다. 그렇다면 표준 함수인 printf는 인수의 개수를 어떻게 파악할까? 개수를 전달해 주는 고정 인수도 없고 끝을 나타내는 특이값도 없어서 함수 내부에서 가변 인수의 개수를 알 수 없는 것 같다. 그러나 자세히 관찰해 보면 서식 문자열에 포함된 서식의 개수가 바로 가변 인수의 개수와 일치한다는 것을 알 수 있다. printf는 첫 번째 고정 인수로 전달되는 서식 문자열에서 %d, %f, %s 같은 서식의 개수만큼 가변 인수를 읽음으로써 사실상 가변 인수의 개수를 전달받는다. 개수와 마찬가지로 함수 내부에서 각각의 가변 인수 타입을 알 수 있어야 한다. GetSum이나 PrintSum 함수처럼 모든 인수를 정수형으로 고정하든가 아니면 첫 번째, 두 번째는 실수, 세 번째 이후는 모두 정수라는 식으로 미리 약속이 되어 있어야 한다. printf는 대응되는 서식으로부터 가변 인수의 타입을 판별하는데 %d가 제일 처음 나왔으면 첫 번째 가변 인수는 정수, 다음으로 %f가 나왔으면 두 번째 가변 인수는 실수라는 것을 알게 된다. 가변 인수들의 타입을 알아야 하는 이유는 va_arg 매크로가 ap번지에서 가변 인수를 읽을 때 얼마만큼 읽어서 어떤 타입으로 해석해야 할 지를 알아야 하기 때문이다. 가변 인수의 타입을 전달하는 방식도 여러 가지를 생각할 수 있는데 printf와 같이 하나의 고정 인수를 통해 모든 가변 인수의 타입을 판단할 수 있는 힌트를 제공하는 방식이 가장 좋다. 다음 예제의 GetSum2 함수는 types 고정 인수에 이후 전달되는 가변 인수들의 타입을 문자열로 전달한다. 정수형에 대해서는 i, 실수형에 대해서는 d라는 문자를 할당해서 이 문자들을 순서대로 죽 적어주는 것이다. 예를 들어 types가 "iidd"라면 앞 쪽 두 인수는 정수형이고 뒤쪽 두 인수는 실수형이며 총 가변 인수는 4개라는 정보가 전달된다. #include <Turboc.h> double GetSum2(const char *types, ...) { double sum=0; va_list ap; const char *p; va_start(ap,types); for (p=types;*p;p++) { switch (*p) { case 'i': sum+=va_arg(ap,int); break; case 'd': sum+=va_arg(ap,double); break; } } va_end(ap); return sum; } void main() { printf("1+2=%f\n",GetSum2("ii",1,2)); printf("2.5+3.8+4=%f\n",GetSum2("ddi",2.5,3.8,4)); printf("1+2.345+6+7.8901=%f\n",GetSum2("idid",1,2.345,6,7.8901)); } types 고정 인수를 통해 인수의 개수와 타입까지도 한꺼번에 전달할 수 있기 때문에 정수형, 실수형을 마구 섞어서 전달해도 함수 내부에서 다양한 타입의 인수들을 제대로 읽을 수 있을 것이다. 실행 결과는 다음과 같다. 1+2=3.000000 2.5+3.8+4=10.300000 1+2.345+6+7.8901=17.235100 GetSum2 함수에서는 types의 길이만큼 루프를 돌되 이 문자열의 처음부터 순서대로 문자를 읽으면서 i이면 va_arg(ap, int)로 인수를 읽고 d이면 va_arg(ap, double)로 인수를 읽었다. 정수, 실수 외에도 더 다양한 타입을 전달하고 싶다면 types의 의미를 확장하고 switch문의 case만 늘려 주면 된다. 규칙들이 다소 복잡하다고 느껴질 지 모르겠지만 잘 생각해 보면 지극히 당연한 규칙들 뿐이다. 함수 내부에서 전달된 인수의 개수나 타입을 전혀 알 수 없다면 값을 정확하게 읽지 못하므로 이런 규칙이 필요하다. 규칙만 지킨다면 인수에 대한 정보를 알려 주는 방법에 대해서는 자유를 누릴 수 있다. 가변 인수 함수는 인수의 개수나 타입에 대해 호출측에서 자유롭게 결정할 수 있는 편리한 함수이다. 그러나 자유에는 언제나 책임이 따르는 법이라 규칙을 제대로 지키지 않았을 때의 결과에 대해 컴파일러는 어떠한 책임도 지지 않는다. 가변 인수 함수를 잘못 호출했을 때 어떤 결과가 발생하는지 다음 예제를 실행해 보자. #include <Turboc.h> void main() { delay(1000);printf("%d%d\n",1,2); delay(1000);printf("%d%d%d\n",1,2,3,4,5); delay(1000);printf("%d%d\n",1,3.14); delay(1000);printf("%f%f\n",1,2); delay(1000);printf("%s\n",1); } 다섯 개의 printf 함수 호출문이 있고 결과를 천천히 감상할 수 있도록 1초씩 시간을 지연시키도록 했다. 순서대로 이 호출문들을 분석해 보자. 첫 번째 호출문은 두 개의 정수를 출력하되 서식과 인수의 개수, 타입이 정확하게 일치하며 따라서 이 호출문은 아주 정상적으로 처리될 것이다. 두 번째 호출문에는 서식이 세 개 밖에 없지만 인수는 다섯 개나 전달되었다. 이 경우도 정상 실행되는데 모자라는 것은 문제가 되지만 남는 것은 무시해 버리면 되므로 문제가 되지 않는다. 세 번째 호출문은 %d 서식 두 개를 가지고 있지만 실제 전달된 인수는 정수 상수 하나와 실수 상수 하나여서 개수는 맞지만 타입이 일치하지 않는다. 이 경우 printf는 두 번째 가변 인수를 정수형으로 읽기 때문에 3.14를 억지로 정수형으로 바꾸어 출력하게 된다. 이상 동작을 하지는 않지만 어쨋든 원하던 결과는 아닐 것이다. 네 번째 호출문은 두 개의 %f 서식을 가지고 있지만 가변 인수는 둘 다 정수형이다. 이 경우 정수형 값을 8바이트의 실수형으로 읽으려고 시도하게 되는데 원하는 결과도 나오지 않을 뿐더러 잘못하면 다운될 수도 있다. 이 코드가 다운될 것인가 아닌가는 순전히 운의 문제이되 요행스럽게도 스택에는 자유 공간이 많이 남아 있기 때문에 8바이틀 더 읽는다고 해서 쉽게 다운되지는 않는다. 마지막 호출문은 심각한데 서식은 %s로 되어 있어 가변 인수가 문자열인 것으로 전달되지만 실제 전달된 인수는 정수형이다. 따라서 불쌍한 printf는 정수 1을 포인터로 해석하여 이 위치의 문자열을 읽으려고 시도하는데 절대 번지 1은 시스템 영역이기 때문에 그 자리에서 즉사해버린다. 32비트의 보호된 환경에서는 허가되지 않은 영역을 읽으려고 할 때 운영체제가 강제로 프로세스를 종료해 버리기 때문이다. 보다시피 가변 인수 함수를 잘못 쓰면 이렇게 위험해질 수 있다. 그러나 이런 위험한 코드를 작성했음에도 불구하고 위 예제를 컴파일하면 컴파일러는 뻔뻔스럽게도 0 error 0 warning이라는 결과를 보여 준다. 컴파일러는 위 코드가 위험한지 아닌지를 판별할 능력도 없고 권한도 없다. printf 함수의 원형에는 ...이라고 되어 있어 인수에 대해서는 개수든 타입이든 간섭하지 말라고 했기 때문이다. 그러니 가변 인수 함수는 사용하는 사람이 주의하는 수밖에 없다.
|
|
|