패스트캠퍼스에서 진행하고 있는 OpenCV 강의에서 사용되는 예제 프로그램은 100개가 훨씬 넘습니다. 매번 새로운 강의가 시작될 때마다 Visual Studio 버전이 바뀌거나 OpenCV 라이브러리 버전이 바뀌면 모든 프로젝트를 새로 빌드해서 테스트를 진행하는데요, 100개가 넘는 프로젝트를 Visual Studio에서 불러와서 하나씩 하나씩 컴파일하고 빌드하려고 하면 너무 번거롭습니다. 그래서 일괄 빌드 방법을 알아봤습니다.


일단 저의 OpenCV 강의는 8주 과정으로 이루어져있기 때문에 "1주차_예제_코드", "2주차_예제_코드", ... 같은 형태의 폴더가 있고, 그 아래에 여러 개의 예제 프로젝트 폴더를 만들어두었습니다. 그러므로 현재 폴더에서 "N주차_예제_코드" 폴더를 모두 찾고, 그 아래에 있는 폴더들에 각각 들어가서 예제 프로젝트를 빌드해야 합니다. 이 작업을 수행하는 배치 파일 build_all.bat 파일을 아래와 같이 작성했습니다.


@ECHO OFF

SET CURR=%CD%
ECHO Current dir: %CURR%

FOR /D %%i IN (%CURR%\*) DO (
  PUSHD %%i
  ECHO Check sub-dir in %%i

  FOR /D %%k IN (%%i\*) DO (
    PUSHD %%k

    ECHO Now in %%k

    IF EXIST "*.vcxproj" (
      ECHO Build the project in %%k
	  
      msbuild /t:Rebuild /p:Configuration=Release
      copy .\x64\release\*.exe .
      rmdir x64 /S /Q
	  
    ) ELSE (
      ECHO No project in %%k
    )	

    POPD 
  )

  POPD 
)


위 배치 파일은 현재 폴더에서 하위 폴더들을 모두 찾고, 각각의 하위 폴더에서 또 한번 하위 폴더들을 찾아서 방문하고, 해당 폴더에 *.vcxproj 파일이 있는 경우에는 해당 프로젝트를 Release 모드로 빌드합니다. 그리고 생성된 ./release/<proj_name>.exe 파일을 프로젝트 폴더쪽으로 복사한 후, release 폴더는 지워버립니다. 그러면 나중에 실행 파일을 쉽게 실행하여 결과를 볼 수 있기 때문에 유용하죠.


아래는 실제로 build_all.bat 파일을 실행했을 때의 결과입니다. (프로젝트 몇 개만 복사해서 테스트해봤습니다.)


위에 나타난 도스창은 CMD 또는 일반 명령 프롬프트가 아니라 시작 메뉴에서 Developer Command Prompt for VS 2017 라는 이름을 검색해서 실행한 창입니다. Visual Studio 프로젝트 빌드를 위한 환경 설정이 적용된 창이라서 msbuild라는 이름의 빌드 명령을 사용할 수 있습니다.


만약 현재 폴더 아래의 하위 폴더에 존재하는 모든 프로젝트를 빌드하려면 위 배치 파일을 아래처럼 변경하면 되겠죠?

@ECHO OFF

SET CURR=%CD%
ECHO Current dir: %CURR%

FOR /D %%i IN (%CURR%\*) DO (
  PUSHD %%i
  ECHO Check sub-dir in %%i

  IF EXIST "*.vcxproj" (
    ECHO Build the project in %%i
	  
    msbuild /t:Rebuild /p:Configuration=Release
    copy .\x64\release\*.exe .
    rmdir x64 /S /Q
	  
  ) ELSE (
    ECHO No project in %%i
  )	

  POPD 
)


한 가지 주의할 점은, 빌드하려고 하는 전체 경로 이름에 빈칸이 있으면 안됩니다. 배치 파일의 FOR 구문에서 빈칸에 대해서는 제대로 처리를 하지 못하네요.


Posted by kkokkal
:


C언어에서 실수값을 정수값으로 변환하는 방법, 이는 C언어에서 자주 듣게되는 질문중의 하나이며, 실제 프로그래밍에서 널리 사용되고 있다. 이 글에서는 실수형(float, double, long double) 타입에 저장된 값을 정수형(int, long)으로 변환하는 방법에 대하여 알아본다. 본문에서 설명하는 내용 중 일부는 구글링을 통해 발견한 Jukka Korpela 의 글(아래 링크)을 참조하였다.

http://www.cs.tut.fi/~jkorpela/round.html

그리고, 아래 링크에서 간단한 해답을 볼 수도 있다.

http://c-faq.com/fp/round.html

----------------------------------------------------------------------------------------------------------

실수값을 정수형으로 변환하기 위해 일반적으로 많이 사용하는 코드는 다음과 같다.

    (int)(x+0.5)

그러나 이 방법은 음수에 대해서는 동작하지 않는다. 또한 int 타입 대신에 long 타입을 사용하는 것이 바람직하다. 즉, 일반적으로 많이 사용하는 코드가 그리 썩 좋은 코드는 아니라는 것이다. 이 문제에 대하여 심도있게 알아보기 전에 먼저 이러한 변환에는 자름(truncating)과 반올림(rounding) 두 가지 방식의 변환이 존재한다는 것을 기억하자. 참고로, 어떠한 타입의 실수형(예를 들어, float, double, long double)을 정수형으로 변환할 것인지는 크게 중요하지 않다. 

■ 자름 변환 (truncating conversion)

자름 변환(또는 내림 변환)은 소수점 이하의 부분은 모두 무시하는 것을 의미한다. 즉, 실수 3.9는 정수 3으로 변환된다. 이 변환은 C언어에서 기본적으로 수행되는 형변환으로, 실수값(float, double, long double)을 정수형(int, long)으로 변환할 때 자동으로 수행되는 변환이다. 만약 i가 정수형이고 x는 실수형인 경우, 다음의 대입문은 이러한 변환을 발생시킨다.

    i = x

내림 변환은 다음과 같은 형변환(casting) 연산을 할 경우에도 발생한다.

    (int) x


■ 반올림 변환 (rounding conversion)

반올림 변환은 실수형 값에서 가장 가까운 정수값으로 변환하는 것을 의미한다. 예를 들어, 실수 3.9를 반올림 변환을 하면 정수 4가 된다. 반올림 변환은 사람들이 실제 생활에서 더욱 널리 사용하는 변환이다. 그러나, C언어에서는 반올림을 직접적으로 지원하는 함수나 연산자가 존재하지 않는다. (살짝 이해가 안되는 이야기이다)

0보다 큰 실수형에 대해서는 다음과 같은 간단한 방법으로 반올림 연산을 수행할 수 있다.

    (long) (x+0.5)

그러나, 위 식은 음수에 대해서는 정확하게 동작하지 않는다. 그러므로, 실수 x가 음수인 경우를 고려하여 다음과 같이 작성해야 한다.

    x >= 0 ? (long)(x+0.5) : (long)(x-0.5)

위 표현은 실수 x에 가장 가까운 정수를 표현하는 식이다.

위의 표현을 이용하여 다음과 같이 매크로(macro) 함수를 만들 수 있다.

    #define round(x) ((x)>=0?(long)((x)+0.5):(long)((x)-0.5))

위와 같이 매크로를 만들어놓으면 간단하게 반올림 연산을 수행할 수 있다. 이러한 반올림 연산은 1.5를 2로 변환하고, -1.5는 -2로 변환한다.

실수형 값을 정수형 값으로 변환할 때 오버플로우(overflow)가 발생할 수 있음을 인지해야한다. int 형 대신에 long 형을 사용하는 것은 보다 큰 범위의 정수형을 지원할 수 있다. (그러나, 32비트 컴퓨팅 환경에서는 int 나 long 이나 4바이트로 표현되므로 값의 범위는 같다. 그럼에도, 실수형은 정수형보다 더 큰 범위의 수를 표현할 수 있기 때문에 long 타입을 사용하는 것을 권장한다.)

만약 효율성이 크게 중요하지 않다면, #define 문을 사용하는 대신 아래와 같은 함수를 정의함으로써 프로그램을 보다 안정적으로 만들 수 있다.

    long round(double x) {
        assert(x >= LONG_MIN-0.5);
        assert(x <= LONG_MAX+0.5);
        if (x >= 0)
            return (long) (x+0.5);
        return (long) (x-0.5);
    }


만약 효율성이 중요시된다면 아래와 같이 매크로 형태로 만들 수도 있을 것이다.

    #define round(x) ((x) < LONG_MIN-0.5 || (x) > LONG_MAX+0.5 ?\
    error() : ((x)>=0?(long)((x)+0.5):(long)((x)-0.5))


위 문장은 #include <limits.h> 을 필요로 하고, 이는 long 타입을 반환하는 error 함수가 있어야 정상적으로 동작할 것이다.

Posted by kkokkal
:
공용체란 C언어 문법 책 뒷쪽에, 구조체(structure) 다음에 꼽사리로 나오는 데이터 구조.
예를 들어, 아래와 같은 공용체가 정의되어있다고 하자.

typedef union _data {
    char a;
    int b;
    float c;
} data;


이 경우, 이 공용체는 전체 8바이트의 크기를 갖는다. 이 8바이트 중에서 첫 번째 바이트는 a 변수가 사용하고, 처음 네 개의 바이트는 b가 사용하고, 처음부터 끝까지 여덟 바이트는 c가 사용한다. 즉, 세 멤버 변수 a, b, c 는 중첩된 메모리 공간을 같이 사용한다. 그러므로, a 변수에 값을 저장하면, b와 c에 저장된 값은 전혀 다른 값으로 바뀌게 된다.

"도대체 이런 구조를 왜 써야할까???" 하는 의문을 바로 몇 달전 C 프로그래밍을 가르치면서 다시금 떠올렸었다.

그러다가 OpenCV 책을 번역하면서 발견한, union의 효과적인 사용 예를 기록해보려고 한다.

OpenCV의 행렬 구조체의 정의는 다음과 같다.

typedef struct CvMat
{
	int type;
	int step;
	int* refcount;
	int hdr_refcount;
	union {
		uchar* ptr;
		short* s;
		int* i;
		float* fl;
		double* db;
	} data;
	union {
		int rows;
		int height;
	};
	union {
		int cols;
		int width;
	};
} CvMat;


CvMat 라는 구조체 내부에 union이 두루 사용된 것을 볼 수 있다. 먼저 아래쪽 두 개의 union을 보면 rows 와 height 두 개를 union으로 감싸고 있다. 이 경우, rows 와 height 는 같은 메모리 공간을 지칭하는 변수가 된다. 즉, 행렬의 행의 크기를 나타내는 값을 rows 라는 변수로도 접근할 수 있고, height 라는 변수로도 접근할 수가 있는 것이다. 이건, 꽤 괜찮다. ^^;

두 번째, 위 구조체 정의 중간에 있는 data 라는 이름을 할당한 union은 5 가지 종류의 포인터 변수를 가지고 있는데, 이 또한 꽤 그럴싸하다. 어차피 포인터 형 변수라면 항상 4바이트(win32 환경의 경우)를 갖게 되니까 data 변수는 결과적으로 4바이트를 갖는다. 그런데 data.ptr 을 사용할 수도 있고, data.s, data.i, data.fl, data.db 를 사용할 수도 있다. 즉, CvMat 행렬이 실제 저장하고 있는 값이 무슨 타입이냐에 따라 실제 데이터가 저장된 메모리 공간을 가리키는 변수를 선택적으로 사용할 수 있는 것이다. 그러면 뭐가 좋을까??? 포인터 연산을 통해 주변 원소로 이동할 때 캐스팅을 따로 하지 않아도 되는 장점이 생긴다. (물론 주의해서 사용해야 하는 점은 존재한다.) 음... 어려울려나?? 암튼 좋은거다. ^^;

암튼, union을 사용해서 도움이 되는 예제를 처음 보다시피하여 기록해보았다. 도움이 될라나.. 흠..


Posted by kkokkal
:
2차원 평면 상에서 어느 한 점의 위상(phase)을 알고 싶을 때, 쉽게 말해서 x축과 이루는 각을 알고 싶을 때 사용할 수 있는 함수가 atan2(아크 탄젠트 투) 이다. 이 함수의 원형은 다음과 같다.

double atan2(double y, double x)

이 함수는 atan(y/x) 결과 값을 반환하며, 반환값의 범위는 -pi 부터 +pi 까지이다. (즉, -3.14 ~ + 3.14)

이 함수를 사용할 때 주의할 점은 점의 y좌표를 먼저 써주어야 한다는 것이다. 너무나 당연스럽게 (x, y) 좌표 순서대로 써줄 경우 잘못된 결과를 얻게 될 수 있다.

몇몇 2차원 좌표에 대하여 atan2 함수를 적용하였을 때 결과값을 아래 그림에 정리하였다. 다시 한 번 말하지만, (x, y) 좌표가 서로 뒤바뀌어 써주어야 한다는 점에 주의하기 바란다.


Posted by kkokkal
:
본 게시글은 필자가 유니코드에 대해 그동안 궁금해했던 점들을 인터넷에서 찾아서 정리한 내용이다. 자료 조사에 반나절 이상이 소요되었고, 내용 정리에 또 반나절이 사용되었다. 나름 잘 정리하려고 애썼음을 밝히고 싶다. 쩝...

■ ASCII -> ANSI -> Unicode

C 언어를 배우면 나오는 용어 중에 ASCII 코드(아스키 코드) 라는 것이 있다. 영문자를 포함한 문자들을 32에서 127 사이의 숫자들을 사용하여 표현하는 방식이다. ASCII 코드의 32 미만에는 특수 코드(예를 들어, PC 스피커에서 '삑'소리를 나게 한다던가 탭, 줄바꿈 등)가 저장되었다. 즉, 7비트만 있으면 영문 + 숫자 + 특수 문자 등을 화면에 출력할 수 있다.

보통의 컴퓨터에서 8비트(=1바이트) 단위를 사용하기 때문에 1비트를 더 사용하여 추가적인 문자를 표현하기도 하였다. 예를 들어, 유럽식 특수 문자를 넣기도 하고, 또는 화면에 선을 그릴 수 있는 그래픽 문자를 넣어두기도 하였다.

[그림 1] ASCII 코드를 포함하는 IBM PC의 OEM 코드

그러나, 이 추가 비트를 이용하여 생긴 128~255 사이의 숫자에 매핍된 문자는 나라마다 또는 컴퓨터 회사마다 서로 다르게 되어있어서 혼란이 생기게 되었다. 그리하여 ANSI 표준 위원회에서 ANSI 표준을 정하였는데 128 아래의 숫자에는 모든 사람들이 ASCII 코드를 그대로 사용하는 것에 동의하였다. 128 이상의 숫자에서는 코드 페이지(code page)라는 것을 만들어서 각 나라마다 고유의 문자셋을 정하여 사용하도록 설정하였다. 다국어로 판매되던 MS-DOS 는 이 코드 페이지를 이용하여 영어부터 아이슬란드어까지의 다양한 나라의 언어를 처리할 수 있었다. 그러나, 하나의 컴퓨터에서 히브리어와 그리스어를 동시에 처리하는 것을 불가능했고, 아시아권의 문자를 표현하기에는 8비트는 턱없이 부족했다.

아시아권의 문자는 두 개의 바이트를 사용하여 표시하는 DBCS(Double Bytes Character Set)를 사용해야만 했다. 이 경우 16비트를 사용하고, 65536개의 서로 다른 문자의 표현이 가능해지는 것이다. (그러나, 실제로 6만여개의 코드를 모두 사용하는 것은 아니다.)

유니코드(unicode)는 전세계의 모든 문자를 컴퓨터에서 일관되게 표현하기 위해 만들어진 표준이다. 기존의 ASCII, ANSI 등의 인코딩 방식이 다국어 환경에서 서로 호환되지 않는 단점들이 있었기 때문에 이러한 문자 인코딩 방식을 모두 유일한 유니코드로 교체하고자 하는 것이 이 표준의 목적이다. 애플컴퓨터, IBM, 마이크로소프트 등이 컨소시엄으로 설립한 유니코드(Unicode)가 1990년에 첫 버전을 발표하였고, ISO/IEC JTC1에서 1995년 9월 국제표준으로 제정하였다. 유니코드의 공식 명칭은 ISO/IEC 10646-1(Universal Multiple-Octet Coded Character Set)이다. 2008년 현재 유니코드 5.1까지 발표되었다. 유니코드는 컴퓨터 소프트웨어의 국제화와 지역화에 널리 사용되게 되었으며, 최근의 기술인 XML, 자바, 그리고 Microsoft Windows 최신 운영 체제 등에서도 지원하고 있다.

■ 유니코드의 인코딩 방식

'일반 텍스트' 라는 개념은 존재하지 않습니다.

우리가 일반적으로 메모장 등의 텍스트 편집기를 이용하여 그 내용을 확인할 수 있는 문서를 '일반 텍스트'라고 말을 하는데 이는 올바른 표현이 아니다. ZIP, RAR 등의 압축 프로그램이 고유의 포맷 또는 인코딩 방식이 있는 것처럼 일반 문자열도 나름대로의 인코딩 방식을 가질 수 있다. 그 중, 가장 대중적이면서 널리 사용되던 인코딩 방식으로 볼 수 있었던 텍스트 파일을 '일반 텍스트'라고 표현을 한 것이라고 생각해야 한다. 이는 유니코드에 대한 설명을 하면서 보다 명확해질 수 있다.

다음과 같은 문자열이 있다고 하자.

Hello

이 문자열을 유니코드를 이용하여 표현하면 다음과 같이 5개의 코드 포인트를 사용하여 나타낼 수 있다.

U+0048 U+0065 U+006C U+006C U+006F

자, 그렇다면 이 코드를 실제로 어떻게 저장을 할까?? 메모장을 이용하여 실제 Hello 라는 문자열을 타이핑하고, 유니코드 방식으로 저장하여 그 결과를 살펴보자.

[그림 2]는 빅 인디언 방식으로 문자열을 저장한 결과를 보여준다. 처음에 나타나는 FE FF 의 두 바이트가 이 문서가 빅 인디언 방식으로 저장되어 있음을 나타낸다. 그 후에 나타나는 문자열의 바이트 순서는 위에 나타난 코드 포인트의 순서와 동일하다.

[그림 2] 빅 인디언 방식

[그림 3]은 리틀 인디언 방식으로 문자열을 저장한 결과이다. 빅 인디언 방식과 달리 FF FE 순서로 처음 두 바이트가 나타나고, 그 후에 5개의 코드 포인트는 바이트 순서가 서로 바뀌어서 저장되는 것을 볼 수 있다.

[그림 3] 리틀 인디언 방식

마지막으로 요새 널리 사용되는 UTF-8 방식의 인코딩 방식을 알아보자. 위의 Hello 문자열에서 각 문자는 상위 바이트에 00 이라는 비어있는 바이트를 가지고 있다. 즉, UTF-8에서는 0~127 사이에 존재하는 ASCII 코드에 대해서는 오직 하나의 바이트만을 사용하여 인코딩을 하는 방식이다. 128 이상의 코드 포인트는 2~6 바이트까지 확장하여 저장을 한다. (http://www.utf-8.com/ 참조) UTF-8에서 한글은 보통 3바이트를 차지한다. [그림 4]는 Hello 문자열을 UTF-8 방식으로 저장한 결과를 보여준다. 이 때, 처음 EF BB BF 는 UTF-8 인코딩의 시작을 의미한다. 참고로, 빅 인디언의 FE FF, 리틀 인디언의 FF FE, UTF-8의 EF BB BF 와 같이 시작되는 문자 코드를 BOM(Byte Order Mark)이라고 한다.

[그림 4] UTF-8 방식

■ Microsoft Windows 프로그래밍과 유니코드

Windows 2000은 유니코드 사용을 기본으로 하여 만들어졌다. 사용자로부터 ANSI 문자열이 넘어오면 이를 유니코드로 바꾸어 내부적으로 처리한다. 물론 이런 변환을 수행하기 때문에 시간과 메모리면에서의 소비가 존재한다. 그럼에도 유니코드를 사용하는 이유는 추후에 지속적으로 유니코드를 사용하는 컴퓨팅 환경에 미리 맞추어 나가려는 노력이라고 볼 수 있다.

참고로, Windows 2000은 유니코드와 ANSI를 지원한다. 즉 둘 중 하나로 개발할 수 있다. Windows 3.1 또는 Windows 98은 내부적으로 ANSI 문자열을 사용한다. Windows CE는 유니코드만을 지원하고, 유니코드로만 개발해야한다.

Visual C++ 2005를 이용하여 프로그램을 개발할 경우, 기본적으로 프로젝트의 문자집합 속성이 유니코드로 지정되어있다. 그러므로 WIN32 API 함수들도 유니코드 환경에 맞게 제공이 되는데, 하위호환성(win9x)을 위해 멀티바이트 입력도 받을 수 있도록 API 가 제공이 된다. 예를 들어, 기존의 SendMessage 함수는 SendMessageW(Wide, 유니코드) 와 SendMessageA(Ansi, 멀티바이트) 함수로 나누어져서 다음과 같은 전처리기에 의해 개발 환경에 맞게끔 자동 스위칭되어 호출된다.

#ifdef UNICODE
#define SendMessage  SendMessageW
#else
#define SendMessage  SendMessageA
#endif // !UNICODE

Visual C++ 2005 프로젝트 속성에서 문자집합 속성이 유니코드로 설정되어있으면 UNICODE 라는 것이 정의되는 것으로 인지하면 된다. 그러므로, WIN32 API를 사용할 때에는 대표적인 함수 이름을 사용하여 예전과 큰 변화없이 프로그램을 작성하면 된다.

프로그램 내부에서 유니코드로 문자열 상수를 사용하기 위해서는 문자열 앞에 대문자 L을 붙여서 사용해야 한다. 즉, "Hello" 라고 쓰면 이는 ANSI 형태의 문자열이고, L"Hello" 라고 써야 유니코드 문자열이 된다. 그러나, 실제 Visual C++ 2005 에서 프로그래밍을 할 때에는 L 매크로 대신에 _T() 매크로를 사용하는 것이 유리하다. 즉, _T("Hello") 형태로 작성하도록 한다. 이는 _T() 매크로가 유니코드일 때에는 L"Hello" 로, 멀티바이트 환경에서는 "Hello" 로 자동으로 변환시켜주기 때문이다. (내부적으로 UNICODE 가 정의되었는지를 판단하여 결정된다.)

유니코드는 2바이트를 차지하기 때문에 기존의 char 타입으로 문자열을 저장할 수 없고, wchar_t 타입을 사용해야 한다. wchar_t 타입은 실제로는 다음과 같이 unsigned short 타입과 동일하다.

typedef unsigned short wchar_t;
wchar_t szBuffer[100];

위 코드에서 szBuffer에는 100개의 유니코드형 문자를 저장할 수 있다. 그러나, 실제적으로 Visual C++ 2005에서 문자열 데이터를 저장하기 위해서는 TCHAR 타입을 사용하는 것이 바람직하다. TCHAR은 프로젝트 설정이 UNICODE로 되어있으면 wchar_t 로 변환되고, 멀티바이트 설정이면 char로 변환된다.

typedef  unsigned short    wchar_t;
typedef  wchar_t           WCHAR;

#ifdef  UNICODE
  typedef  WCHAR   TCHAR;
#else
  typedef  char    TCHAR;

문자열 버퍼를 다루기 위해서는 기존의 strcpy, strcat 같은 표준 C 런타임 문자열 함수도 유니코드에서는 사용할 수 없다. 대신 이에 대응되는 유니코드 문자열 함수를 사용해야 한다.

char* strcat(char*, const char*);
wchar_t* wcscat(schar_t*, const wchar_t*);

이처럼 유니코드 문자열 함수는 wcs(wide character string)로 시작한다. 그러므로 기존 문자열 함수 이름에서 str을 wcs로 변경하면 된다. 그러나, (위와 마찬가지로) Visual C++ 2005에서는 _tcscmp 함수를 사용하는 것이 바람직하다. TCHAR와 마찬가지로 _tcscmp 함수는 UNICODE 가 정의되었는지를 확인하여 strcmp 또는 wcscmp 함수를 알아서 변환해주기 때문이다. 아래는 MSDN에 나와있는 변환 방식을 보여준다.

 TCHAR.H routine _UNICODE & _MBCS not defined _MBCS defined _UNICODE defined
 _tcscmp strcmp
 _mbscmp  wcscmp


참고문헌: 조엘 온 소프트웨어 - 유쾌한 오프라인 블로그, 여러 블로그, MSDN, etc


Posted by kkokkal
:
Effective C++ 책 39항목에 보면 공백 클래스(empty class)라는 것이 나온다. 개념 자체는 매우 쉽다. 아무런 멤버 변수도 없고, 가상 함수도 없고, 가상 기본 클래스도 없는... 말 그대로 비어있는 클래스이다. 예를 들어, 다음과 같은 형태로 정의된 클래스이다.

class Empty {};

그런데, 이 클래스의 실제 크기는 얼마나 될까??? C++ 에서는 크기가 0인 독립 구조의 객체가 생기는 것을 금지하기 떄문에, 이 클래스의 크기(sizeof)는 0이 아니다. 컴파일러는 이러한 '공백' 객체에 char 한 개를 슬그머니 끼워넣는 식으로 처리하여, 크기는 1이 된다.

즉, sizeof(Empty) 의 값은 1이 된다.

이를 숙지하고 다음 코드를 살펴보자. 그리 어렵지 않는 문제이다.

#include <stdio.h>

class A {};

class B {
int x;
A e;
};

int main(void)
{
printf("sizeof(A) = %dn", sizeof(A));
printf("sizeof(B) = %dn", sizeof(B));

return 0;
}

자, 이 코드를 실행하면 과연 어떤 결과가 출력이 될까?? 뜸들이지 않고 답을 말하면 다음과 같다.


sizeof(A) = 1
sizeof(B) = 8

위 결과에서, 클래스 B의 크기가 8이 되는 것은 바이트 패딩(byte padding) 때문이며, 이는 클래스 내부 데이터의 바이트를 4바이트 단위로 할당하기 때문이다. 이는 CPU에서 연산의 속도를 향상시키기 위한 장치라고 보면 된다. 참고로, 이는 컴파일러 옵션 또는 #pragma 전처리기를 사용하여 변경할 수 있다. 그러나, 기본적으로는 4바이트(int 형의 크기) 단위를 사용하는 것이 일반적이다.



참고로, C 언어에서는 위와 같은 것이 어떻게 동작을 할까?? C 언어에는 class 가 없으므로, 이는 구조체로 변경하여 생각할 수 있을 것이다. C 언어에서는 비어있는 구조체를 만들 수 없으므로 char 타입 하나를 갖는 구조체를 만들어서 테스트를 해보자.

#include <stdio.h>

typedef struct _A {
char c;
} A;

typedef struct _B {
int x;
A e;
} B;

int main(void)
{
printf("sizeof(A) = %dn", sizeof(A));
printf("sizeof(B) = %dn", sizeof(B));

return 0;
}

이 코드의 실행 결과는 앞의 것과 동일하며, 이는 마찬가지로 바이트 패딩 때문이다.

sizeof(A) = 1
sizeof(B) = 8


0
Posted by kkokkal
:
Effective C++ 3판 중 20번째 항목을 보면 '값에 의한 전달' 보다는 '상수 객체 참조자에 의한 전달'을 권장하고 있다.

일단 새로 만든 자료형(클래스나 구조체)을 값에 의한 전달(pass-by-value)을 하게되면 당연히 다수의 생성자와 소멸자 호출이 있기 때문에 효율이 낮아진다는 것이다. 때문에 덩치가 큰 객체(클래스의 인스턴스)는 참조에 의한 전달(pass-by-reference)를 하는 것이 좋다는 것인데, 이는 C++ 을 어느정도 공부한 사람이라면 당연한 이야기로 여길 것이다.

상수 객체에 대한 참조자(reference-to-const)란 무엇일까? 이는 바로 다음과 같은 코드를 의미한다.


class Person { ... };
class Student : public Person { ... };

bool validateStudent(const Student& s);



위와 같이 상수 객체 참조자에 의한 전달을 하여야 효율적이라는 것인데, 여기에는 '복사 손실 문제(slicing problem)' 문제까지도 해결하고자 하는 의도가 숨겨져있다. 복사 손실 문제 또는 슬라이스 문제란 파생 클래스 객체가 기본 클래스 객체로서 전달되는 경우, 이 객체가 값으로 전달되면 기본 클래스의 복사 생성자가 호출되고, 파생 클래스 객체로 동작하게 하는 특징들이 사라져버리는 문제를 일컫는다. 복사 손실 문제를 해결한, 상수 객체 참조자를 이용하는, 다음의 프로그램 코드를 살펴보자.

#include <iostream>

using namespace std;

class AA
{
public:
    int a;

    virtual void pp() const { cout << "aa" << endl; }
};

class BB : public AA
{
public:
	int b;

	virtual void pp() const { cout << "bb" << endl; }
};

void func(const AA& aa)
{
	cout << "a = " << aa.a << endl;
	// cout << "b = " << aa.b << endl; // 에러 발생
	aa.pp();
}

int main(void)
{
	BB bb;
	bb.a = 1;
	bb.b = 2;
	bb.pp();

	func(bb);
}


위 프로그램을 컴파일하고 실행시키면 결과는 어떨까?? main 함수에서 자식 클래스 BB의 객체를 생성하고, func 함수는 부모 클래스의 상수 참조형으로 받는다. 그리고, 공통으로 존재하는 함수 pp 를 호출하는 것인데... 그 결과는 다음과 같다.

bb
a = 1
bb

만약 클래스 AA 와 BB 의 멤버 함수 pp() 에 virtual 지시자가 없으면 어떻게 될까? 이 경우 컴파일 에러는 발생하지 않는다. 다만, 함수 func 에서의 출력 결과는 조금 다르게 나타난다.

bb
a = 1
aa

이는 virtual 지시자의 원래 역할을 제대로 수행하고 있음을 의미한다. 사실 이러한 virtual 지시자에 대한 예제는 보통 객체를 포인터 타입으로 넘겨줄 때를 예제로 들어서 설명하는 것이 많은데, 실제 참조형으로 넘겨줄 때에도 이러한 점이 통한다는 것이 신기했다. 이런게 되나 싶던 것이 된다는 것이랄까?? 암튼, virtual 지시자의 역할에 대해서는 C++ 문법책을 참조하기 바란다.

암튼, Effective C++ 책은 잘못이해하기 쉬운 내용들을 콕! 찝어서 설명해주는 아주 영양가 만점의 책이다. 특히나 곽용재씨가 번역한 3판은 어찌나 번역이 아름다운지(단순히 번역을 잘했다가 아니라 아름다운거다), 2판 번역서를 보면서 답답했던 맘을 아주 잘 누그려뜨려주어서 맘에 든다.


0


Posted by kkokkal
:

#include 
#include 

#include 
#include 

using namespace std;

class POINT
{
public:
	int x;
	int y;
};

bool PointComp(const POINT* pa, const POINT* pb)
{
	return ((pa->x) < (pb->x));
}

void main()
{
	register int i;

	vector points;
	vector::iterator iter;

	//------------------------------------------------------------------------
	// 데이터 입력
	//------------------------------------------------------------------------

	// 랜덤 발생 초기화
	srand((unsigned int)time(0));

	for (i = 0; i < 5; i++)
	{
		POINT* pt = new POINT;

		pt->x = rand() % 100;
		pt->y = rand() % 100;

		points.push_back(pt);
	}

	//------------------------------------------------------------------------
	// 데이터 참조 (이터레이터 사용)
	//------------------------------------------------------------------------

	printf("Original data (Randomly generated):\n");
	for (i = 0, iter = points.begin(); iter != points.end(); ++iter, ++i)
	{
		printf("x[%d] = %2d, y[%d] = %2d\n", i, (*iter)->x, i, (*iter)->y);
	}

	//------------------------------------------------------------------------
	// 데이터 정렬 (배열처럼 중괄호 연산자 사용)
	//------------------------------------------------------------------------

	sort(points.begin(), points.end(), PointComp);

	printf("\nSorting:\n");
	for (i = 0; i < points.size(); i++)
	{
		printf("x[%d] = %2d, y[%d] = %2d\n", i, points[i]->x, i, points[i]->y);
	}

	//------------------------------------------------------------------------
	// 데이터 삭제
	//------------------------------------------------------------------------

	POINT* p;

	for (iter = points.begin(); iter != points.end(); ++iter)
	{
		p = *iter;
		if (p != NULL)
		{
			delete p;
			p = NULL;
		}
	}

	points.clear();

	printf("\nRemoving all the data:\n");
	printf("points.size() = %d\n", points.size());
}


아래는 실제 실행 결과의 예.

Original data (Randomly generated):

x[0] = 14, y[0] = 54
x[1] = 93, y[1] = 57
x[2] = 61, y[2] = 18
x[3] = 94, y[3] = 46
x[4] = 78, y[4] = 6

Sorting:

x[0] = 14, y[0] = 54
x[1] = 61, y[1] = 18
x[2] = 78, y[2] = 6
x[3] = 93, y[3] = 57
x[4] = 94, y[4] = 46

Removing all the data:

points.size() = 0


Posted by kkokkal
:

#include <functional> // definition of less
#include <queue>  // definition of priority_queue
#include <iostream>

using namespace std;

struct Task
{
        int priority;
        friend bool operator < (const Task& t1, const Task& t2);
        Task(int p=0) : priority(p) {}
};

bool operator < (const Task& t1, const Task& t2)
{
        return t1.priority < t2.priority;
}

int main()
{
        priority_queue<Task> scheduler;
        scheduler.push(Task(3));
        scheduler.push(Task(5));
        scheduler.push(Task(1));
        scheduler.push(Task(1));
        cout<< scheduler.top().priority <<endl;   // output 5
        return 0;
}


출처: http://www.milab.co.kr/


Posted by kkokkal
:
이 예제는 임의의 정수(0~999)를 N개 생성하여 정렬하는 예제이다.


실행 결과의 예는 아래와 같다.


Posted by kkokkal
: