전처리기

|

소스파일을 컴파일해 실행하는 과정은 다음과 같다 :

프로그램 작성 -> 전처리 -> 컴파일 -> 링크 -> 실행

컴파일 전에 하는 과정은 전처리라고 하고,이를 수행하는 장치를 전처리기라고 한다. # 문자로 시작하는 문장을 가리켜 전처리기 지시자라고 한다. 대표적으로 헤더 파일을 인클루드 하는 #include가 있다. 이번 포스팅에서는 이 전처리 문장들을 알아보기로 한다.

- 매크로

#define으로 시작되는 전처리 문장을 매크로라고 하며, 매크로는 크게 매크로 상수와 매크로 함수로 나뉘어진다.
첫번째로 매크로 상수의 정의는 다음과 같다 :

#define PI 3.14

PI는 매크로 상수 이름, 3.14는 치환될 값이다. 매크로 상수를 정의하면(참고로 전처리기 지시자 뒤에는 세미콜론을 붙이지 않는다.) 전처리기는 소스 파일에서 PI를 3.14로 인식하고 소스코드에 있는 PI를 3.14로 바꾸고 컴파일러에게 전달한다.
즉, area = PI * radius * radius 라는 문장이 있다면, 전처리기는 이를 area = 3.14 * radius * radius로 바꿔 컴파일러에 전달한다는 뜻이다.

이렇게 매크로 상수를 정의하면 프로그램을 쉽게 수정할 수 있고,큰 숫자들을 직관적인 문자열로 표현할 수 있다. 그리고 변수와 달리 매크로 상수는 별도의 메모리 공간을 필요로 하지 않는다. 보통 매크로 상수는 main 함수가 등장하기 전 최상단에 몰아서 정의한다.

매크로 상수를 해제하는 전처리기 지시자도 있다. #undef는 기존 매크로의 정의를 해제하고 이후부터 치환을 중지한다.

#include <stdio.h>

#define PI 3.14

int main()
{
 printf("%lf\n", PI);
#undef PI
#define PI 3.141592
 printf("%lf\n", PI);
 return 0;
}

예제는 PI를 3.14로 매크로 정의 했다가 해제하고 3.141592로 재정의해서 쓰고 있다.



매크로 "함수"는 함수처럼 인자를 설정할 수 있는 매크로다. 이름은 함수지만, 기능은 단순히 치환하는 것이므로 실제로 함수는 아니다.
매크로 함수의 정의는 다음과 같다 :

#define MUL(a,b) a*b

MUL(a,b)는 매크로 함수의 이름, a*b는 함수의 기능이다.
예를 들어 MUL(3,4)라고 쓴다면, 치환하여 3*4가 되는 식이다. 매크로 함수의 장점은 인자의 자료성을 가리지 않고, 속도가 빠르다. 하지만 매크로 함수의 내부에서 자기 자신을 호출할 수 없고, 한~두줄 정도의 간단한 내용만 매크로 함수로 정의할 수 있다는 점이다.

매크로 함수를 사용할 때 주의할 점은, 매크로 함수는 실제로 함수가 아니라 그저 문장을 치환해준다는 사실이다.


#include <stdio.h>

#define MUL(x,y) x*y
int mul(int x, int y)
{
 return x*y;
}
int main()
{
 int a = 3, b = 4;
 printf("%d\n",MUL(a + 1, b + 1));
 printf("%d\n", mul(a + 1, b + a));

 return 0;
}

위 예제를 보면 MUL은 단순히 치환되기 때문에, 인자로 들어온 a+1,b+1을 x와 y자리에 넣어 a+1 * b+1로 만든다. *가 연산자 우선순위에서 높기 때문에, 사용자가 바라는 (a+1)*(b+1)로는 기능하지 않는다.따라서,이를 고려하여 매크로 함수의 정의를 MUL(x,y) ((x)*(y)) 로 변경해야 한다.

매크로 함수를 정의할 때 사용되는 연산자들은 #, ##, 가 있다.

#는 매크로 함수의 인자를 문자열로 바꿔준다. #define OUTPUT(a) #a라고 기술하고,
printf("%s\n",OUTPUT(13579)); 를 해보면 "문자열로" 13579가 나온다. 즉 # 연산자는 매크로 함수의 인자를 문자열로 치환한다.(참고로 매크로 함수에서 =나 +같은 기호를 문자열로 쓰고 싶다면 " " 안에 넣으면 된다.)

##는 토큰 결합 연산자로서,매크로 함수 안에서 토큰을 결합한다. 토큰이란 문법 분석의 단위( int a = 3; 이라면 int , a , = , 3 , ; 가 모두 토큰이다)로서, 이런 토큰을 분석하는 프로그램을 파서라고 하고 이 파서는 컴파일러 안에 포함되어 있다.파서는 (더 자세히는 프로그래밍 언어론에서 다루지만) 코드의 문법을 파악하기 위해 코드를 토큰 단위로 분리하고 의미를 파악한다. 아무튼, 매크로 함수에서의 사용법은

#define OUTPUT(a,b,c) a ## b ## c 라고 기술하고 printf("%d \n",OUTPUT(a, = ,5)); 라고 매크로 함수를 호출하면
3개의 토큰이 결합하여 a=5라는 문장이 나오게 된다.


C에는 기본적으로 정의되어 있는 매크로 들도 있다. 주로 __XXXX__ 형식으로 되어 있는데 몇개만 소개해보자면,

__FILE__ 현재 소스 코드의 파일 이름을 나타냄.
__LINE__ 현재 위치의 소스 코드의 행 번호를 나타냄.
__DATE__ 현재 소스 코드의 컴파일 날짜를 나타냄.
__TIME__ 현재 소스 코드의 컴파일 시간을 나타냄.



- 조건부 컴파일

조건부 컴파일이란, 매크로 상수의 존재 유무 혹은 매크로 상수의 값을 보고 특정 조건이 만족될 때만 컴파일이 되도록 하는 기법이다.

#if ~ #endif

#if는 if와 비슷하게, 매크로 상수를 비교하는 산술,관계,논리 연산자를 이용해 조건을 확인한다. 차이점이라면 #if는 조건식의 결과에 따라 컴파일을 수행하고, if는 조건식의 결과에 따라 문장을 실행한다.

 

#include <stdio.h>

#define CODE 1
int main()
{
 int a = 3, b = 5;
 int result;
#if(CODE==1)
 result = a + b;
 printf("%d\n", result);
#endif

#if(CODE==2)
 result = a*b;
 printf("%d\n", result);
#endif
 return 0;
}

간단한 예제로 , 매크로 상수 CODE가 1이면 덧셈을, 2면 곱셈을 하는 예제다. #if를 사용하면 꼭 #endif로 조건부 컴파일 구간이 끝났음을 명시해야 한다. CODE가 1인 경우, #if(CODE==2) 이하부터 #endif까지는 컴파일이 되지 않는다.
if와 마찬가지로, #if에서도 else와 else if(단 여기서는 #elif)를 사용할 수 있다.

#if 조건식
1.컴파일 문장
#elif 조건식2
2.컴파일 문장
#else
3.컴파일 문장
#endif

이런식으로.

#if와 비슷한 전처리기 연산자로 #ifdef가 있다. 이 전처리기 연산자는 매크로 상수의 값이 아닌 매크로가 정의되어 있는지를 판단한다. 반대로,
#ifndef는 매크로가 정의되어 있지 않으면 하단을 실행한다. 이 명령어들은 헤더 파일의 중복 검사에 사용되는데, 자세한 설명은
http://mypensieve.tistory.com/85 여기서 다루기로 한다.





 

 

신고

'ComputerEngineering > C/C++' 카테고리의 다른 글

printf  (0) 2016.11.21
전처리기  (0) 2016.08.25
가변 인자  (0) 2016.08.18
동적 할당(C)  (0) 2016.08.17
memory.h  (0) 2016.08.13
time.h (c++의 경우 ctime)  (0) 2016.08.10
trackback 0 And comment 0

가변 인자

|

가변 인자는 함수 인자 수를 고정하지 않고 함수를 호출하는 방법이다.

 

#include <stdio.h>
#include <stdlib.h>

void add(int num, ...)
{
 int *p = &num + 1;

 for (int i = 0; i < num; i++)
  printf("%d\n", p[i]);
}

int main()
{
 int a, b, c;
 a = 10;
 b = 20;
 c = 30;

 add(3, a, b, c);
 
 return 0;
}


함수 add는 인자로 int num을 받고, 그 이후는 ... 으로 쓰여있다. num은 인자의 수를 저장하고, ...은 이 함수가 가변 인자 함수임을 알린다.

호출 시 add(3,a,b,c)는 인자가 3개 있고, 그 인자들이 a,b,c라는 뜻.
함수는 내부에서 포인터 p가 num 이후,즉 num 변수 바로 다음(+1은 int에서 4바이트씩 뛰는 것이므로)인자를 가리킨다.
a,b,c에 저장된 10,20,30은  &num+1,&num+2,&num+3에 각각 복사된다. p는 num+1, 즉 a의 주소를 가리키고 있으므로 반복문은 a부터 num만큼(3) 3개를 출력한다. 


가변 인자 함수는 인자가 여러개일 때, 함수 내부적으로 num의 값을 통해 인자의 개수에 따라 유동적으로 기능을 정할 수 있다는 장점이 있다.





#include <stdio.h>
#include <stdarg.h>

int add(int num, ...)
{
 va_list ptr;
 int sum = 0;
 va_start(ptr, num); // ptr을 초기화 한다. 고정 인자(num) 다음을 가리키게 함.
 // 이 때 두번쨰 인자는 함수에 있는 마지막 고정 인수가 되어야 한다.

 for (int i = 0; i < num; i++)
  sum += va_arg(ptr, int); // 인자 반환 - 반복 호출 시 다음 인자를 반환한다.

 va_end(ptr); // ptr에 NULL 저장.

 return sum;
}
int main()
{
 int sum;
 sum = add(10,1,2,3,4,5,6,7,8,9,10);
 printf("%d", sum);


 return 0;
}

위 예제는 함수의 가변 인자수와 인자를 넣으면, 고정 인자를 제외한 가변 인자들의 합을 구하는 예제다.

가변 인자의 사용을 위해서는 stdarg.h 를 include 해야 한다.
- void va_start(va_list ap,last)
- type va_arg(va_list,type)
- void va_end(va_list ap)

가변 인자를 가리키는 포인터인 va_list 타입 포인터를 정의하고, 그 포인터를 va_start롤 통해 초기화 시켜야 한다.
이 때 꼭 두번째 인자는 함수에 있는 마지막 고정 인수가 되어야 한다.(즉 가변 인자의 개수를 나타내는 값은 마지막 고정 인수가 되어야 함)
va_arg는 포인터가 가리키는 주소에서 두번째 인자 만큼 값을 읽어 반환하고,그 다음 인자로 이동한다.
사용을 마쳤으면 va_end로 포인터를 NULL로 만들어준다.


신고

'ComputerEngineering > C/C++' 카테고리의 다른 글

printf  (0) 2016.11.21
전처리기  (0) 2016.08.25
가변 인자  (0) 2016.08.18
동적 할당(C)  (0) 2016.08.17
memory.h  (0) 2016.08.13
time.h (c++의 경우 ctime)  (0) 2016.08.10
trackback 0 And comment 0

동적 할당(C)

|

 

프로그램에서 사용되는 메모리는 정적 메모리와 동적 메모리가 있다. 스택,코드,데이터는 정적 메모리에 저장되고, 힙은 동적 메모리에 저장된다.코드영역(실행 코드,함수),스택(지역,매개 변수),데이터 영역(전역,정적 변수)는 요구되는 메모리의 크기가 컴파일 때 결정된다. 따라서 이 크기는 런타임에 변경되는 것이 불가능하다. 하지만 동적 메모리에서는 가능하다.

다시 한번 프로세스(실행중인 프로그램)의 메모리 공간을 정의해보자면

코드 영역 - 프로그램 실행 코드 또는 함수들이 저장

스택 영역 - 함수 호출에 의핸 매개변수와 지역 변수 등 블록 내부에 정의된 변수. 잠깐 사용되고 메모리에서 소멸되는 데이터가 저장된다.

데이터 영역 - 전역 변수와 정적 변수들이 저장됨. 프로그램이 종료될 때까지 유지되어야 하는 데이터가 저장된다.

힙 영역 - 프로그램이 실행되는 동안에 프로그래머가 동적으로 메모리를 할당할 수 있는 영역.

이 영역들은 각기 주소가 다르고, 각자의 영역을 가지고 있다.

그렇다면 왜 동적 메모리가 필요한가?
예를 들어, 배열은 메모리를 할당하게 되면 배열이 선언된 함수나 프로그램이 종료될 때까지 그 영역을 해제할 수 없다. 즉 고정된 메모리를 할당받는다. 따라서, 메모리의 낭비나 부족이 발생할 수 있고, 배열 선언 시 크기 값을 무조건 상수로 설정하여야 한다.

이렇게 메모리 크기를 미리 예측할 수 없는 경우에 동적 메모리 할당이 사용된다. 힙 영역에 메모리 할당을 하게 되면 필요한 시점에 필요한 크기만큼 메로리를 할당할 수 있다. 동적 메모리 할당은 C에서 포인터를 기반으로 동작한다. 함수를 통해 더 자세히 알아보자.


메모리 할당 함수들 ( stdlib.h )

- void* malloc(size_t size)

- void* calloc(size_t num,size_t size)

- void* realloc(void* p,size_t size)

메모리 해제 함수

- void free(void* p)


보통 메모리 할당에는 malloc 함수가 자주 쓰인다. 이 함수는 성공 시 메모리의 시작 주소를 반환하고, 실패 시 NULL을 반환한다.
인자로 size_t 형식을 받는데,이는 메모리에 얼만큼 동적 할당할 지 바이트 수를 의미한다. 함수의 리턴 형태는  void* , 즉 보이드 포인터다.
사용자가 어떤 형식의 자료형을 할당할 지 알 수 없으므로, 함수는 void* 형식으로 리턴하고 사용자가 이를 형변환 해야 한다.
예를 들어

int *p =NULL;
p = (int*)malloc(4);

이 구문은,4byte만큼의 메모리를 동적 할당해 int형 포인터 p에  그 주소를 저장하겠다는 뜻이다. 자료형은 int이므로 malloc이 리턴하는 주소를 int* 형으로 형변환 해주어야 한다.

할당이 있다면 해제도 있어야 한다. 그렇지 않으면 메모리의 낭비가 발생한다. 해제는 free함수로 한다.
free(p);
이렇게 써주면 p에 동적할당했던 메모리가 해제된다.

 

#include <stdio.h>
#include <stdlib.h>

int main()
{
 int i;
 puts("input the size: ");
 scanf("%d", &i);
 puts("---------------------\n");
 int *p = (int*)malloc(sizeof(int)* i);

 for (int j = 0; j < i; j++)
 {
  p[j] = j;
  printf("%d\n", p[j]);
 }
 free(p);

 printf("%d\n", p[0]); // 쓰레기 값
p = NULL;
 return 0;
}

 

사용자로부터 원소의 개수를 입력받고,그 개수 * int의 크기 만큼(즉 배열의 크기)를 동적할당하는 예제다. 메모리에 int형의 메모리 블록을 생성하고, 그 "주소"를 int *p에 담는 것이므로, 여기에 동적할 크기는 (int*)*i가 아닌 (int)*i가 된다. 혼동하지 말자. 
이렇게 동적할당을 사용하면 프로그래머가 메모리의 크기를 유동적으로 지정할 수 있다.(참고로 동적할당은 sizeof로 배열 크기를 측정할 수 없다. 해당 함수를 실행하면 포인터의 크기인 4가 나오게 된다.)
free 함수를 통해 동적 할당을 해제하면 포인터가 가리키던 대상은 쓰레기 값이 나오게 된다. 혹시 모를 오용을 방지하기 위해 사용을 마친 포인터는 NULL로 초기화 해주는 것이 좋다.



calloc함수는 malloc 함수와 같은 기능을 하지만, 사용법이 약간 다르다.calloc함수의 인자는 2개인데, size_t num과 size_t size다. calloc 함수는 num과 size를 곱한 만큼을 동적할당하게 된다. 즉 int형 4칸이 필요하다면 malloc은 malloc(sizeof(int)*4) 였다면 calloc은 calloc(4,sizeof(int)) 다. 또 다른 차이는, malloc 함수는 메모리 할당된 영역을 초기화 하지 않아 쓰레기 값이 나오는 반면 calloc함수는 전부 0으로 초기화해준다. 편리하긴 하지만 따로 의도한 경우가 아니라면 시간 낭비를 유발할 수 있다.


malloc 함수와 calloc함수는 동적 메모리를 할당한 후 메모리의 크기를 변경할 수 없다는 단점이 있다. 이러한 문제를 realloc 함수가 해결해준다. realloc함수는 동적 메모리로 할당되어 있는 영역 p에서 size만큼을 다시 재할당 해주고(이 때 주소가 바뀔 수 있다), 그 재할당된 메모리의 시작 주소를 반환한다.



#include <stdio.h>
#include <stdlib.h>

int main()
{
 int *p = (int*)malloc(sizeof(int)* 2); // 8바이트 할당
 p[0] = 1;
 p[1] = 2;

 p = (int*)realloc(p, sizeof(int)* 4); // 16바이트로 확장
 p[2] = 30;
 p[3] = 40;

 for (int i = 0; i < 4; i++)
  printf("%d\n", p[i]);

 printf("----------------------\n");
 p = (int*)realloc(p, sizeof(int)* 2); // 8바이트로 다시 재할당
 
 for (int i = 0; i < 4; i++)
  printf("%d\n", p[i]);

 printf("----------------------\n");


 p[2] = 50;
 
 for (int i = 0; i < 4; i++)
  printf("%d\n", p[i]);

 return 0;
}

 

 

예제는 8바이트를 동적할당하고,  16바이트로 확장한 후 , 8바이트로 다시 재할당하고 있다. 16바이트까지 할당하면 p[3]까지 원소가 들어있다. 이때 다시 8바이트로 재할당하면 p[2]에 쓰레기 값이(원래는 30이 저장되어 있던) 저장되어 있는 것을 볼 수 있다. 즉 메모리를 줄이게 되면 메모리에 들어있던 데이터들이 보존되지 않는다. 할당을 해제했지만 p[2]에 다시 접근해 값을 저장하고 출력까지 할 수 있다. 이는 C에서 배열의 경계를 체크하지 않기 때문에 가능한 일이다.



 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char** arr(int num)
{
 char buffer[256];
 char **p = (char**)malloc(sizeof(char*)* num);
 for (int i = 0; i < num; i++)
 {
  puts("type the phrase : ");
  gets(buffer);
  p[i] = (char*)malloc(strlen(buffer) + 1);
  strcpy(p[i], buffer);
 }
 return p;
}

int main()
{
 int i;
 puts("How many ? : ");
 scanf("%d", &i);
 fflush(stdin);
 char **ptr = arr(i);
 puts("--------------------------------\n");
 for (int j = 0; j < i; j++)
 {
  puts(ptr[j]);
  free(ptr[j]);
 }
 free(ptr);
 return 0;
}

 

위 예제는 사용자가 입력한 숫자 만큼 배열을 생성하고,거기에 사용자가 입력한 문자열을 넣는 예제다. 여기서 살짝 복잡한 이차원(혹은 이중)포인터를 통한 동적 할당을 하고 있다.
함수 arr은 리턴 방식이 char**로, char형 2차원 포인터다. 이는 2차원 포인터의 원소에 다시 동적 메모리 할당을 통해 문자열을 저장하고,각각 1차원 포인터에 접근하기 위해 2차원 포인터의 시작 주소를 리턴하기 위함이다.
**p에 인자로 들어온 문자열의 개수 만큼 곱하기 char* 형으로 동적 할당을 한다.
그리고 반복문을 통해 문자열을 입력받고, 이를 버퍼에 넣은 후 2차원 포인터 각각의 원소(2차원 포인터는 1차원 포인터를 가리킨다. 즉 2차원 포인터 배열의 원소"값"은 1차원 포인터의 주소가 되는 것이다.)에 1차원 문자 포인터로 버퍼가 담고 있는 문자열의 크기만큼을 동적할당하고, strcpy 함수로 문자 포인터가 가리키고 있는 메모리에 문자열을 복사한다.
이 때,**p는 static으로 하지 않아도 상관없다. arr 함수가 종료되면 p는 소멸하지만, p에 동적 할당된 메모리는 사라지지 않으므로(별도로 free를 하지 않는 이상) 해당 주소를 return해  새 포인터인 **ptr이 받아 사용할 수 있다.

 

 


 

신고

'ComputerEngineering > C/C++' 카테고리의 다른 글

전처리기  (0) 2016.08.25
가변 인자  (0) 2016.08.18
동적 할당(C)  (0) 2016.08.17
memory.h  (0) 2016.08.13
time.h (c++의 경우 ctime)  (0) 2016.08.10
데이터 변환 표준 함수  (0) 2016.08.10
trackback 0 And comment 0
prev | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ··· | 43 | next