본문 바로가기
카테고리 없음

C 언어 고급편]🚀19 전처리와 분할 컴파일

by redcubes 2024. 4. 17.

전처리 지시자

소스파일→ 전처리된 소스파일 → 목적파일 → 실행파일

전처리기가 소프코드를 컴파일하기 좋게 다듬는 과정.

#include 파일 포함.

<> 컴파일러 설정 디렉터리에서 파일 찾음.

“” 소스파일이 저장된 디렉터리에서 먼저 찾음. 사용자 정의 헤더.

프로젝트 디렉터리가 이닌 곳에서 찾으려면 경로면 포함. 이스케이프 필요 없음.(전처리기가 하는 일이라서 컴파일러처럼 할 필요 없음. 전처리기가 배치 파일이나 배시 스크립트가 간단한 명령을 처리하는 방식과 유사한 느낌이 있다.)

students.h 헤더 파일에서 구조체 정의하고 main.c에서 #include “students.h”하면 헤더 파일 포함됨.

메인 함수 중간에서도 인클루드는 가능하지만….

특별한 이유 없으면 하지 말 것

전처리 후에도 여전히 텍스트 파일임.

매크로명 만들기 #define

#define 매크로명 치환값

매크로 함수

#define 매크로함수명(인수) 치환될 부분

#include <stdio.h>
#define PI 3.14159                        // 상수를 매크로명으로 정의
#define LIMIT 100.0                       // 상수를 매크로명으로 정의
#define MSG "passed!"                     // 문자열을 매크로명으로 정의
#define ERR_PRN printf("허용 범위를 벗어났습니다!\\n")  // 출력문을 매크로명으로 정의

int main(void)
{
	double radius, area;                  // 반지름과 면적 변수

	printf("반지름을 입력하세요(10 이하) : ");
	scanf("%lf", &radius);                // 반지름 입력
	area = PI * radius * radius;          // 면적 계산
	if (area > LIMIT) ERR_PRN;            // 면적이 100을 초과하면 오류 메시지 출력
	else printf("원의 면적 : %.2lf(%s)\\n", area, MSG);  // 면적과 메시지 출력

	return 0;
}

상수 대신에 쓰이는 매크로명은 매크로 상수

치환될 부분이 길어 여러 줄에 걸쳐 쓰려면 \로 연결

#define INTRO "Perfect C Language \\
& Basic Data structure"

#define을 사용한 매크로 함수

#include <stdio.h>
#define SUM(a, b) ((a) + (b))   // 두 값을 더하는 매크로 함수
#define MUL(a, b) ((a) * (b))   // 두 값을 곱하는 매크로 함수

int main(void)
{
	int a = 10, b = 20;
	int x = 30, y = 40;
	int res;

	printf("a + b = %d\\n", SUM(a, b));  // a와 b의 합
	printf("x + y = %d\\n", SUM(x, y));  // x와 y의 합
	res = 30 / MUL(2, 5);               // 30을 2와 5의 곱으로 나눔
	printf("res : %d\\n", res);

	return 0;
}

치환된 후의 상호작용의 불안정성을 방지하기 위해 수식을 괄호로 감싸 둔다.(연산자 우선순위에 의한 오류 방지)

이미 정의된 매크로

__FILE__    //전체 디렉터리 경로를 포함한 파일명
__FUNCTION__ //매크로명이 사용된 함수이름
__LINE__   // 매크로명이 사용된 해 번호
__DATE__   // 컴파일을 시작한 날짜
__TIME__  //컴파일을 시작한 시간


#include <stdio.h>

void func(void);

int main(void)
{
	printf("컴파일 날짜와 시간 : %s, %s\\n\\n", __DATE__, __TIME__);
	printf("파일명 : %s\\n", __FILE__);
	printf("함수명 : %s\\n", __FUNCTION__);
	printf("행번호 : %d\\n", __LINE__);

#line 100 "macro.c"   // 행 번호를 100부터 시작, 파일명은 macro.c로 표시
	func();           // 여기부터 행 번호는 100으로 시작

	return 0;
}

void func(void)
{
	printf("\\n");
	printf("파일명 : %s\\n", __FILE__);
	printf("함수명 : %s\\n", __FUNCTION__);
	printf("행번호 : %d\\n", __LINE__);
}

__FILE____LINE__#line지시자로 정의를 바꿀 수 있음.

 

프로그램 디버깅에 활용 가능

매크로 연산자 #과

#은 매크로 함수의 인수를 문자열로 치환

##은 두 인수를 붙임. cocatenate

#include <stdio.h>
#define PRINT_EXPR(x) printf(#x " = %d\\n", x)
#define NAME_CAT(x, y) (x ## y)

int main(void)
{
	int a1, a2;

	NAME_CAT(a, 1) = 10;     // (a1) = 10;
	NAME_CAT(a, 2) = 20;     // (a2) = 20;
	PRINT_EXPR(a1 + a2);     // printf("a1 + a2" " = %d\\n", a1 + a2);
	PRINT_EXPR(a2 - a1);     // printf("a2 - a1" " = %d\\n", a2 - a1);

	return 0;
}

조건부 컴파일 지시자

#include <stdio.h>
#define VER 7          // 치환될 부분이 있는 매크로명 정의
#define BIT16          // 치환될 부분이 없는 매크로명 정의

int main(void)
{
	int max;

#if VER >= 6           // 매크로명 VER이 6 이상이면
	printf("버전 %d입니다.\\n", VER);    // 이 문장 컴파일
#endif                 // #if의 끝

#ifdef BIT16           // 매크로명 BIT16이 정의되어 있으면
	max = 32767;       // 이 문장 컴파일
#else                  // BIT16이 정의되어 있지 않으면
	max = 2147483647;  // 이 문장 컴파일
#endif                 // #ifdef의 끝

	printf("int형 변수의 최댓값 : %d\\n", max);   // max 출력

	return 0;
}

#ifdef#if + defined

#ifndef#if + !defined

다른 연산자와 함께 쓰려면 분리된 형태 사용.

#pragma 지시자

컴파일 방법을 세부지시. 지시명으로 어떤 기능을 제어할 것인지 알려줌.

pack은 구조체의 패딩바이트 크기 결정

warning은 경고 메시지 관리

#include <stdio.h>
#pragma pack(push, 1)    // 바이트 얼라인먼트를 1로 바꿈
typedef struct
{
	char ch;
	int in;
} Sample1;

#pragma pack(pop)        // 바꾸기 전의 바이트 얼라인먼트 적용
typedef struct
{
	char ch;
	int in;
} Sample2;

int main(void)
{
	printf("Sample1 구조체의 크기 : %d바이트\\n", sizeof(Sample1));
	printf("Sample2 구조체의 크기 : %d바이트\\n", sizeof(Sample2));

	return 0;
}

바이크 얼라인먼트 크기를 조절

분할 컴파일

방법

  • 두 수의 평균을 구하는 프로그램

main.c

#include <stdio.h>

void input_data(int*, int*);  // 두 정수를 입력하는 함수 선언
double average(int, int);     // 평균을 구하는 함수 선언

int main(void)
{
	int a, b;
	double avg;

	input_data(&a, &b);       // 두 정수 입력
	avg = average(a, b);      // 평균 계산
	printf("%d와 %d의 평균 : %.1lf\\n", a, b, avg);  // 입력값과 평균 출력

	return 0;
}

sub.c

#define _CRT_SECURE_NO_WARNINGS     // scanf 함수 사용을 위한 매크로 
#include <stdio.h>                  // printf, scanf 함수 사용을 위해 필요

void input_data(int* pa, int* pb)   // 두 정수 입력 함수
{
	printf("두 정수 입력 : ");
	scanf("%d%d", pa, pb);
}

double average(int a, int b)        // 평균을 구하는 함수
{
	int tot;
	double avg;

	tot = a + b;
	avg = tot / 2.0;

	return avg;
}

  1. 프로젝트에 main.c추가하고 컴파일-디버그폴더에 목적파일 생성
  2. 프로젝트에 sub.c추가하고 컴파일-역시 목적파일 생성.
  3. 링크([빌드]-[솔루션 빌드])-실행파일 생김.

주의사항

  • 각 파일을 독립적으로 컴파일 할 수 있도록 필요한 선언을 포함해야 함.

분할 컴파일에서 extern과 static의 용도

extern다른 파일에 선언된 전역 변수 사용.

static다른 파일이 사용하지 못하게 함.

extern

  • 용도: extern 키워드는 다른 파일에서 정의된 전역 변수나 함수를 참조할 때 사용합니다. 즉, 변수나 함수가 외부에 선언되어 있음을 명시하고, 링크 시점에 해당 심볼의 정의를 찾아 연결합니다.
  • 사용 예:
  • // file1.c에서 정의 int global_var = 10; // file2.c에서 사용 extern int global_var;
  • 주의사항:
    1. extern 변수를 사용할 때는 실제로 해당 변수가 다른 파일에 정의되어 있어야 합니다. 정의되지 않은 상태에서 사용하면 링커 오류가 발생할 수 있습니다.
    2. 초기화: extern으로 선언된 변수는 초기화하면 안 됩니다. 초기화는 정의와 함께 이루어져야 하며, extern은 정의가 아닌 선언에 사용됩니다.
    3. 헤더 파일: extern 변수는 헤더 파일에 선언하고, 한 소스 파일에서만 정의하는 것이 일반적입니다. 이렇게 하면 여러 소스 파일에서 같은 전역 변수를 쉽게 공유할 수 있습니다.

static

    • 용도: static 키워드는 변수나 함수의 범위를 해당 파일 내로 제한합니다. 즉, static으로 선언된 변수나 함수는 다른 파일에서 접근할 수 없게 됩니다.
    • 사용 예:
    • // file1.c 내에서만 접근 가능 static int file1_local_var = 20; static void file1_function() { // 내용 }
      1. 범위 제한: static으로 선언된 변수나 함수는 다른 파일에서 접근할 수 없습니다. 이는 의도적으로 정보 은닉을 할 때 유용합니다.
      2. 초기화: 전역 static 변수는 자동으로 0으로 초기화됩니다. 로컬 static 변수도 선언 시 명시적으로 초기화하지 않으면 0으로 초기화됩니다.
      3. 메모리: static 변수는 프로그램의 생명 주기 동안 계속 존재하므로, 프로그램이 종료될 때까지 메모리를 차지하게 됩니다.주의사항:

헤더 파일의 필요성과 중복 문제 해결 방법

헤더 파일이 여러 번 포함될 때 중복 정의 문제가 발생할 수 있습니다. 예를 들어, 두 개의 서로 다른 헤더 파일이 서로를 포함하고 있다면, 컴파일러는 같은 선언을 여러 번 처리하게 되어 문제를 일으킬 수 있습니다. 이러한 문제를 해결하기 위한 주요 방법은 다음과 같습니다.

  1. 함수 선언 공유: 헤더 파일을 통해 여러 소스 파일에서 사용할 함수의 선언을 공유할 수 있습니다. 이를 통해 함수가 정의된 소스 파일과 다른 소스 파일에서도 해당 함수를 사용할 수 있게 됩니다.
  2. 타입 정의 공유: 사용자 정의 타입(예: 구조체, 열거형, 타입 별칭)을 헤더 파일에 선언하여 여러 소스 파일에서 이를 공유할 수 있습니다.
  3. 매크로 정의: 매크로 상수나 매크로 함수를 헤더 파일에 정의함으로써 다양한 소스 파일에서 이를 재사용할 수 있습니다.
  4. 모듈화 및 재사용: 헤더 파일은 코드의 모듈화를 촉진하고, 쉽게 재사용할 수 있게 해줍니다. 이는 프로젝트의 유지보수성을 높이고, 오류 가능성을 줄입니다.
  • 중복 포함 문제 및 해결 방법
    • Include Guards: 인클루드 가드는 헤더 파일이 한 번만 포함되도록 보장합니다. 이는 전처리기 지시어를 사용하여 구현됩니다. 가장 일반적인 형태는 #ifndef, #define, #endif를 사용하는 것입니다.
      • 예시:
      • // file.h #ifndef FILE_H #define FILE_H void function(); // 다른 선언들 #endif
    • Pragma Once: 일부 컴파일러에서는 #pragma once 지시어를 제공하여 동일한 헤더 파일이 여러 번 포함되는 것을 방지합니다. 이 지시어는 파일 전체에 대한 유일한 포함을 보장합니다.
      • 예시:
      • // file.h #pragma once void function(); // 다른 선언들
    #pragma once는 간단하고 효율적이지만, 모든 컴파일러에서 지원되는 것은 아니므로, 포터블 코드를 작성하려면 인클루드 가드를 사용하는 것이 더 일반적입니다. 각 방법은 코드의 중복 포함을 방지하고, 프로젝트의 관리를 더욱 효율적으로 만들어 줍니다.