ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • C언어 기초#12 전처리 및 비트 필드
    Programming 기초/C Language 2023. 4. 28. 17:04

    * 전처리기(preprocessor)

    • 본격적으로 컴파일하기에 앞서 소스 파일을 처리하는 컴파일러의 한 부분.
    • 전처리기는 보통 컴파일러에 포함되어 있고 자동으로 실행되며 컴파일러의 하나의 요소로 취급됨.
    • 전처리기는 소스 파일을 처리하여 수정된 소스 파일을 생산한다.
    • 전처리기는 몇 가지의 전처리기 지시자들을 처리한다. 이 지시자들은 #기호로 시작한다. e.g) #include

     

    쉽게 풀어쓴 C언어 Express, 2014

    하나의 프로그램이 만들어지기까지의 과정은 위의 그림과 같다. 두 번째 단계에 해당하는 컴파일러의 내부를 살펴보면 아래 그림과 같이 전처리기와 컴파일러가 있다. 

    쉽게 풀어쓴 C언어 Express, 2014

    (오브젝트 파일은 기계어로 번역된 파일이다.)

     

    * 단순 매크로(macro)

    • #define 지시자를 이용하면 숫자 상수에 의미 있는 이름을 부여할 수 있다. 
    • #define 문을 이용하여 숫자 상수를 기호 상수로 만든 것을 단순 매크로(macro)라고 한다.
    • 예를 들어 #define MAX_SIZE 100를 전처리기는 소스 파일에서 MAX_SIZE를 100으로 변경한다.
    • 이는 프로그램의 가독성과 소스 변경이 용이하다는 장점이 있다.

     

    * 함수 매크로(function-like macro)

    • 매크로가 함수처럼 매개 변수를 가지는 것이다. 
    • 함수 매크로를 사용하면 함수처럼 복잡한 계산을 숨기고 보다 간단하게 나타낼 수 있다.
    • 함수 매크로에서는 매개 변수의 자료형을 써주지 않는다. 어떠한 자료형에 대해서도 적용이 가능하다.
    #define MAX(x) (((x)>(y)?(x):(y))	// (조건)?(참):(거짓)는 조건이 참이면 (참)을 실행하고 거짓이면 (거짓)을 실행한다.

    함수매크로에서는 매개변수가 기계적으로 대치되기 때문에 매개 변수들을 괄호로 묶어줘야한다.

    #define SQUARE(x) x*x
    ...
    v=SQUARE(a+b);
    #define SQUARE(x) ((x)*(x)) // 올바른 방법

    위 SQUARE(x) 함수 매크로의 원래 의도는 (a+b)*(a+b)를 계산하게 하는 것이나, 괄호가 생략되어 a+b*a+b로 계산된다.

    // 매크로를 한 줄 이상으로 만드는 방법
    #define PRINT(x) if( debug==1 && \
    						mode==1) \
                            printf("%d", x);

    매크로를 한 줄 이상 만들고 싶을 때는 줄의 맨 끝에 \을 적어준다.

     

    * 함수 매크로와 함수

    • 매크로의 장점은 함수에 비하여 수행 속도가 빠르다는 것이다.  함수 호출을 하기 위한 인수와 복귀 주소를 시스템 스택에 저장해야하는 실제 함수와 달리 매크로는 호출이 아닌 코드가 그 위치에 삽입되는 것이기 때문에 수행 속도가 빠르다.
    • 매크로의 단점은 코드의 길이를 어느 한도 이상 길게 할 수 없다. 가능은 하나 복잡해진다. 30개의 매크로가 있다면 30개의 똑같은 코드가 프로그램에 존재하게 된다. 함수는 단 하나의 코드만을 가지고 있다. 따라서 매크로를 사용하면 소스 파일의 크기가 커진다.
    • 함수 매크로와 함수 중 무엇을 사용할 것이냐는 프로그램의 크기와 실행 속도 중에서 어떤 것이 더 중요한지를 따진다.

    * #연산자

    • 매크로의 인수를 문자열로 변경하고 싶을 때 사용한다.
    • #은 문자열 변환 연산자(Stringizing Operator)라고 불린다.
    // x=5를 출력하고 싶다. 하지만 실행결과는 exp=5이다.
    
    #include <stdio.h>
    #define PRINT(exp) printf("exp=%d\n", exp);
    
    int main(void)
    {
    	int x=5;
        
        PRINT(x);
        return 0;
    }
    -------------------------------------------
    exp=5

    실행결과를 "x=5"를 얻고 싶지만 다른 결과를 얻었다. 전처리기는 큰따옴표 안의 exp는 치환하지 않기 때문이다.

    또한 전달된 실제 인수인 x를 문자열로 만들어야 한다. 이때 #을 변수 앞에 붙이면 된다.

    #define PRINT(exp) printf(#exp"=%d\n", exp);
    // #exp는 "x"로 치환되고 printf("x"" = %d\n", x);와 같이 변경된다.

     

    * ##연산자

    • ##연산자는 토큰 병합 연산자(token-pasting operator)라고 불린다.
    • 2개의 토큰을 따로 따로 치환한 후에 2개의 토큰을 합하는 역할을 한다.
    #include <stdio.h>
    #define MAKE_NAME(n) v ## n
    #define PRINT(n) printf("v" #n " = %d\n", v ## n);
    
    int main(void)
    {
    	int MAKE_NAME(1) = 10;
        int MAKE_NAME(2) = 20;
        
        PRINT(1);	// printf("v1 = %d\n", v1);과 같다.
        PRINT(2);	// printf("v2 = %d\n", v2);과 같다.
        return 0;
    }
    ------------------------------------
    v1 = 10
    v2 = 20

     

    * 내장 매크로

    • 컴파일러가 프로그래머들이 유용하게 사용하도록 제공하는 몇 개의 미리 정의되어 있는 매크로이다. 많이 사용하는 것은 다음의 4가지이다.
    • 이들 매크로는 처음과 끝에 두 개의 밑줄이 있다. 이것은 포르그래머가 사용하는 기호 상수와 겹치지 않게 하기 위함.
    내장 매크로 설명
    _ _DATE_ _ 이 매크로를 만나면 소스가 컴파일된 날짜(월 일 년)로 치환된다.
    _ _TIME_ _ 이 매크로를 만나면 소스가 컴파일된 시간(시:분:초)으로 치환된다.
    _ _ LINE_ _ 이 매크로를 만나면 소스 파일에서의 현재의 라인 번호로 치환된다.
    _ _FILE_ _ 이 매크로를 만나면 소스 파일 이름으로 치환된다.

    _ _LINE_ _과 _ _FILE_ _은 주로 디버깅에 관한 정보를 출력할 때 쓰인다. 

    printf("치명적 오류 발생 파일 이름=%s 라인 번호= %d\n", __FILE__, __LINE__);

     

    * #ifdef, #endif

    • 조건부 컴파일을 지시하는 전처리 지시자이다.
    • 조건부 컴파일이란 어떤 조건이 만족되는 경우에만 지정된 소스 코드 블록을 컴파일하는 것이다.
    • #ifdef는 #ifdef 다음에 있는 매크로를 검사하여 매크로가 정의되어 있으면 #if와 #endif 사이에 있는 모든 문장들을 컴파일한다. 그렇지 않으면 문장들은 컴파일되지 않아서 실행코드에 포함되지 않는다.(아예 없는 것으로 취급)
    #define DEBUG	// DEBUG를 정의하면 아래 printf문장이 컴파일되고, 
    		// DEBUG를 정의하지 않으면 컴파일에 포함되지 않는다.
    
    int aberage(int x, int y)
    {
    #ifdef DEBUG
    	printf("x=%d, y=%d\n", x, y);
    #endif
    	return (x+y)/2;
    }
    • #else를 사용할 수도 있다. #else를 사용하면 매크로가 정의되지 않았을 경우 컴파일되는 문장들이 들어간다.
    • #ifedf의 조건으로 사용되는 매크로는 소스 파일의 시작 부분에서 정의하기도 하지만 보통은 대부분의 컴파일러에서 소스를 건드리지 않고 컴파일러의 대화 상자에서 변경할 수 있도록 한다. Visual Studio에서도 "프로젝트 -> test의 속성" 메뉴를 클릭하면 아래 대화 상자가 나오고 여기에서 프로그래머가 원하는 매크로를 정의할 수 있다.

    visual studio 2019버전

     

    * #ifndef

    #ifdef의 반대의 의미. 어떤 매크로가 정의되어 있지 않으면 #ifndef와 #endif 사이의 문장이 컴파일에 포함된다.

     

    * #undef

    매크로의 정의를 취소한다. 이전에 정의된 매크로를 다시 정의하고 싶은 경우에 사용한다. 이전의 정의를 무효화하고 새로 정의하고 싶은 경우에 사용한다.

    #define SIZE 100
    ..
    #undef SIZE
    #define SIZE 200

     

    * #if, #else, #endif

    • #if는 #if 다음에 있는 조건을 검사하여 조건이 참으로 계산되면 #if와 #endif 사이에 있는 모든 코드를 컴파일한다. 조건은 상수 수식이어야 하고 관계 연산자나 논리 연산자를 사용할 수 있다.
    • #ifdef는 매크로의 값에는 상관하지 않는데, #if는 매크로의 값에 따라서 컴파일 여부를 결정한다. 
    • else if와 같은 의미로 #elif도 사용할 수 있다.
    #define METHOD 1
    ...
    #if METHOD == 1
    	printf("방법 1이 선택되었습니다.\n");
    #end if

     

    * 다중 소스 파일 불러오기

    여러 개의 소스 파일로 만들어지는 프로그램에서 각각의 소스 파일을 모듈(module)이라고 한다. 보통 각가의 모듈은 하나의 소스 파일과 함수들의 원형이 정의되어 있는 헤더 파일을 가진다.

     

    e.g) 거듭 제곱을 구하는 함수 power()를 만들고 이것을 power.c에 저장한 뒤 호출하는 법

    //main.c
    #include <stdio.h>	// 헤더 파일을 포함할 때는 <...>를 사용한다.
    #include "power.h"	// 사용자가 만든 헤더 파일을 포함할 때는 "..."을 사용한다.
    ..
    int main(void)
    {
    	...
    	printf("%d의 %d 제곱값은 %f\n", x, y, power(x,y));
       	...
    }
    < 왜 헤더 파일을 사용해야하는가? >
    헤더 파일을 사용하지 않으려면 다른 소스 파일에서 제공하는 함수를 사용하기 전에 함수 원형을 소스 파일 첫부분에서 선언해야 한다. 상당히 번거로운 일이고 소스 파일이 많다면 같은 내용이 중복된다. 그래서 헤더 파일을 작성하여 헤더파일에 함수들의 원형을 넣어두고 다른 소스 파일에서는 이 헤더 파일을 포함하는 것이 좋다. 

    < 헤더 파일에는 어떤 내용들을 넣으면 좋을까? >
    일반적으로는 함수의 원형 또는 구조체 정의, 매크로 정의, typedef의 정의를 넣어주면 좋다.

     

    * 다중 소스 파일과 외부 변수

    외부 변수 선언은 다른 소스 파일에서 정의된 전역 변수를 사용하기 위하여 extern이라는 키워드를 사용하여 그 변수를 외부 변수로 선언하는 것이다. (이전 게시글 참고)

     

    * 비트 필드 구조체

    비트 필드 구조체는 구조체의 일종으로서 멤버들의 크기가 비트 단위로 나누어져 있는 구조체를 의미한다. 즉 비트들을 멤버로 가지는 구조체이다.

    // 상품 정보를 저장하는 비트 필드 구조체
    
    struct product {
    	int number;	// 구조체 안에 비트 필드 멤버와 일반 멤버를 동시에 둘 수 있다.
    	unsigned style : 3;	// unsigned를 붙이면 부호가 없는 자료형이 된다.
        unsigned size  : 2;
        unsigned color : 1;
        unsigned :2;	// 이름 생략이 가능하다. 해당 비트들은 사용할 수 없으며 단지 자리만 차지하게 된다.
    };
    • 비트 필드의 크기는 멤버 이름 다음에 콜론(:)을 사용하여 나타낸다.
    • unsigned style : 3;은 unsigned 중에서 3개의 비트만을 사용한다는 것을 나타낸다.
    • 비주얼 C++에서는 unsigned 자료형이 32비트로 표현되므로 0에서 32까지의 숫자를 사용할 수 있다.
    • 이름없는 비트 필드가 필요한 이유는 워드의 경계에 비트 필드가 걸치게 되면 입출력 속도가 상당히 늦어지기 때문에 다음 멤버가 워드의 처음에서 시작할 수 있도록 이러한 필드를 두는 것이다.
    struct product {
    	unsigned style : 3;
        unsinged :0;	// 현재 워드의 남아있는 비트를 버린다.
        unsigned size  : 2;	// 다음 워드에서 size와 color가 할당된다.
        unsigned color : 1;	
    };
    • 이름이 없는 비트 필드 중에서 크기가 0인 비트 필드는 사용하지 않은 현재 워드의 남아 있는 비트들을 모두 버린다. 이는 다음 비트 필드가 워드의 처음에서 시작하도록 만들기 위해서이다.

     

    댓글

Designed by Tistory.