쉽게 풀어쓴 C언어 Express(이승재, 이기정)-7주차

Jaylee
Quantum Ant
Published in
29 min readAug 22, 2019

스트림과 파일입출력

스트림이란?

입출력 장치들은 상당히 다양한 방식으로 데이터를 주고받지만 C에서는 스트림이라는 개념을 사용하여서 일관된 방법으로 입출력을 수행할 수 있다. 스트림이란 모든 입력과 출력을 바이트들의 흐름으로 생각하는 것이다. 어떤 입출력 장치던 상관없이 바이트 단위로 입출력이 이루어진다.

Fig1. 입출력스트림

스트림의 장점 첫 번째는 장치 독립성이다. 입출력 장치에 상관없이 프로그램을 작성할 수 있다. 입력과 출력은 무조건 연속된 바이트의 스트림이다. 따라서 입력 장치와 출력 장치가 무엇이던 간에 동일한 방식으로 출력과 입력을 할 수 있다.

스트림의 장점 두 번째는 버퍼를 사용한다는 점이다. 일반적으로 입출력장치보다 CPU가 훨씬 빠르기 때문에 CPU가 하나의 바이트가 입출력되기를 기다리는 것은 매우 비효율적이다. 따라서 CPU와 입출력장치 중간에 버퍼가 있다. 입력의 경우에는 버퍼가 어느 정도 쌓이면 한꺼번에 CPU가 한꺼번에 데이터를 가져간다. 출력의 경우에는 CPU가 버퍼로 대량의 데이터를 전송하면, 출력장치가 시간이 날 때마다 버퍼에서 데이터를 가져가게 된다.

표준 입출력 스트림

몇 개의 기본적인 스트림은 프로그래머가 생성하지 않아도 자동으로 생성한다. 이것을 표준 입출력 스트림이라고 한다. 이들 스트림은 프로그램이 시작될 때 자동으로 만들어지고, 프로그램이 끝날 때 자동으로 소멸된다. 표준 입출력 스트림은 다음과 같다.

Fig2. 표준 입출력 스트림

stdin은 표준 입력 스트림을 의미한다. scanf()를 사용하면 stdin 스트림을 사용하는 것이다. stdout은 표준 출력 스트림이다. printf()를 사용하면 stdout스트림을 사용하는 것이다. stderr은 프로게이머가 다르게 정의할 수도 있으나 보통은 stdout과 같이 모니터의 화면을 의미한다.

입출력 함수의 분류

입출력 함수들은 스트림과 입출력 형식이 지정되느냐 않느냐에 따라 다음과 같이 분류할 수 있다.

Fig3. 입출력함수

형식이 없는 입출력이란 주로 문자열 형태의 입출력을 의미한다. 형식이 있는 입출력은 형식 지정자로 받아들이는 데이터의 형식을 지정해 데이터를 받아들이는 것을 말한다.

printf() 출력

printf()에서 정밀하게 출력을 제어할 수 있다.printf()의 첫 번째 매개변수인 format(형식 지정자)는 형식 제어 문자열이라고 한다. 형식 제어 문자열은 변수나 수식의 값을 출력하는 형식을 지정한다. 형식 제어 문자열에서 필수적으로 있어야하는 것은 ‘형식’뿐이나 다른 것들로 출력을 제어할 수 있다.

-문법

printf(“%플래그① 필드폭② .정밀도③ 형식④”,출력하려는 값);

ex) printf(“%-10.3f”);

①플래그

플래그는 하나의 문자로서 다음과 같은 사항을 지시한다.

Fig4. 플래그와 사용예

② 필드폭

데이터가 출력되는 필드의 크기를 지정할 수 있다. 필드폭은 %와 형식 지정자 사이에 들어간다. 예를 들어 %10d라고 하면 필드폭은 10문자 크기가 된다. 만약 필드폭이 출력되는 데이터 크기보다 크면 데이터는 오른쪽 정렬되어 출력된다. 만약 데이터가 필드보다 크면 필드폭은 자동으로 넓어진다. +,-같은 부호도 필드에서 1개의 크기를 차지한다.

Fig5. 필드 폭의 사용예

③정밀도

%e,%E,%f를 통해서 실수를 출력할 때 정밀도는 소수점 이하의 자릿수의 개수가 된다. 즉 %10.3이라고 하면 필드폭은 10이고 그중에서 소수점 이하 자릿수가 3이라는 의미가 된다. 필드폭을 지정하지 않고 %.3이라고도 할 수 있는데, 이 때는 소수점 이하 자릿수만 지정하게 된다. 정밀도를 지정하지 않으면 소수점 이하 6자리로 출력된다.

④형식

출력값을 어떤 형식으로 변환하여 출력할 것인지를 지정한다.

Fig6–1. 형식 지정자
Fig6–2. 형식지정자(계속)

scanf()를 이용한 입력

scanf()는 표준 입력에서 형식을 지정하여 데이터를 받아들이는 함수이다. scanf()는 입력받는 문자열을 자동으로 숫자의 형태로 변환시켜 준다. 예를 들어 123을 입력할 때 사용자는 문자1,2,3을 차례대로 입력한다. scanf()는 1,2,3을 모아서 123이라는 하나의 정수로 변환한다. 여러 가지 scanf()의 기능을 알아보자.

필드폭을 지정하여 읽기

scanf()의 형식 제어 문자열에 필드폭이 지정되었으면 필드폭만큼의 문자를 읽어서 값으로 변환한다. 이것을 사용하면 공백 문자로 입력값을 분리하지 않고도 여러 개의 값을 읽을 수 있다.

ex)

scanf(“%3d%3d”,&a,&b);

입력:123456

위에 문장에서는 123이 a에 저장되고 456이 b에 저장된다.

8진수,16진수 입력

정수는 10진수 뿐만 아니라 8진수나 16진수로도 입력이 가능하다.

ex)

scanf(“%d %o %x”,&a,&b,&c);

printf(“d=%d o=%o x=%d”, a,b,c);

입력: 10 10 10

출력: d=10 o=8 x=16

10을 똑같이 입력하였지만 입력받은 형식지정자가 서로 다르다. 따라서 서로 다른 값이 변수 a,b,c에 저장된다.

문자와 문자열 읽기

Fig7. scanf()의 문자와 문자열 읽기

scanf()로도 문자를 읽을 수 있다. 다만 문자를 받아들이는 형식 지정자 %c는 공백 문자도 하나의 문자로 인식해 입력한다. %c 형식 지정자 사이에 공백 문자를 두는 경우(%c %c)에는 공백 문자를 이용하여 문자들을 분리한다. 다만 형식지정자가 붙어있으면(%c%c) 공백문자도 하나의 문자로 취급되어 입력된다.

Fig8. scanf()와 공백

문자 집합으로 읽기

scanf()에는 문자 집합을 기호로 표시하고 이 문자 집합에 포함된 문자만을 읽을 수 있는 기능이 들어 있다. 읽고 싶은 문자 집합은 문자들을 대괄호([])로 묶어서 표시한다. 예를 들어 %[abc]라고 지정하면 a,b,c로만 이루어진 문자열만 읽다가 a,b,c 외의 문자가 나오면 입력이 중지된다. 따라서 만약 첫 번째 문자가 a,b,c가 아니라면 문자 배열에는 아무 것도 저장되지 않는다.

scanf()의 반환값

scanf()가 반환하는 값은 읽은 항목의 개수이다. 이를 이용하면 값이 모두 성공적으로 입력되었는 지, 사용자가 몇 개나 성공적으로 입력하였는지를 검사할 수 있다.

파일의 기초

변수는 메모리에 저장된다. 그런데 메모리는 영구적인 저장장치가 아니다. 따라서 데이터를 영구적으로 저장하려면 디스크와 같은 보조 기억 장치에 저장해야 한다. C에서는 디스크에 파일을 생성시켜서 데이터를 보관할 수 있다.

파일의 개념

파일도 스트림으로 취급되기 때문에 파일도 일련의 연속된 바이트라고 생각하면 된다. 따라서 파일에 대한 입출력도 표준 입출력과 동일한 함수들로 이루어진다. 파일 입출력을 위해서는 프로그래머가 파일 이름을 직접 결정하여 파일 스트림을 생성하여야 한다.

모든 파일은 입출력 동작이 발생하는 현재 위치를 나타내는 파일 포인터를 가진다. 파일을 열면 파일 포인터는 파일의 첫 번째 바이트를 가리킨다. 입출력 연산이 진행되면 파일 포인터는 자동적으로 이동된다.

파일의 유형

C에서는 텍스트 파일과 이진 파일(바이너리 파일)이라는 2가지 파일 형식을 지원한다. 텍스트 파일은 말 그대로 텍스트가 들어있는 파일이다. 우리가 흔히 사용하는 메모장 파일등이 예이다. 텍스트 파일은 연속적인 줄로 이루어져 있다. 각 줄에서는 여러 개의 문자를 포함할 수 있으며 각 줄의 마지막에는 줄의 끝을 알리는 문자가 있다.

이진 파일은 사람은 읽을 수 없으나 컴퓨터는 읽을 수 있다. 문자가 저장된 것이 아닌 데이터가 저장된 파일이다. 이진 파일의 데이터는 이 진수 형태로 저장된다. 이진 파일은 텍스트가 저장된 것이 아니기 때문에 각 줄을 표시할 필요가 없고 각 줄의 끝을 알리는 문자가 필요 없다. 따라서 각 줄의 끝을 알리는 문자, NULL은 단순히 데이터로 취급된다. 실행파일, 사운드파일, 이미지 파일등이 이진 파일의 예이다.

파일 열기(fopen())

-문법

①FILE *포인터변수;

포인터변수=②fopen(“③파일이름”,“④파일모드”);

ex)

FILE *fp;

fp=fopen(“test.txt”,“w”);

① FLIE은 stdio.h에 선언된 구조체 자료형이다. 각각의 파일에 대하여 FILE 구조체가 하나씩 필요하다.

② fopen()은 주어진 파일 이름을 가지고 파일을 생성하여 FILE 포인터를 반환한다. 만약 fopen()이 실패하면 NULL포인터가 반환된다.

③ fopen()의 첫 번째 매개변수인 name은 파일의 이름을 나타내는 문자열이다. “”로 둘러싸서 문자열 상수로 전달할 수도 있고, 배열에 담아서 전달할 수도 있다.

④ fopen()의 두 번째 매개변수인 mode는 파일을 여는 모드를 의미한다. 파일이 이진파일인지, 텍스트 파일인지 파일의 유형을 나타내기도 하고, 파일을 쓸 것인지, 읽을 것인지를 나타내는데 사용한다.

Fig9. 파일의 모드

기본적인 파일모드에 “t”나 “b”를 붙일 수 있다. 각각 텍스트 파일을 열 때, 이진 파일을 열 때 붙인다. 예를 들어 이진 파일을 읽기 모드로 열 때는 “rb”로 모드값을 주어야 한다. 만약 “t”,“b”를 붙이지 않았으면 텍스트 파일로 간주한다.

“a”나 “a+”모드는 추가 모드라고 한다. 추가 모드로 파일이 열리면 파일의 끝에서 쓰기 동작이 이루어진다. 파일 안의 기존의 데이터들은 절대 지워지지 않는다.

“r+”,“w+”,“a+” 파일모드가 지정되면 읽고 쓰기가 모두 가능하다. 이러한 모드를 수정모드라고 한다. 그러나 읽기에서 쓰기, 쓰기에서 읽기로 모드를 전환하려면 fflush(), fsetpos(), fseek() 중의 하나를 호출해야 한다.

파일닫기(fclose())

열린 파일을 닫는 함수는 fclose()이다. fclose()는 stdio.h에 정의되어 있다.

-문법

fclose(FILE 포인터);

ex)

fclose(fp);

성공적으로 파일을 닫는 경우에는 0이 반환된다. 실패한 경우에는 –1이 반환된다.

ex) 파일 열고 닫기

#include <stdio.h>

int main()

{

FILE *fp=NULL;

fp=fopen(“sample.txt”,“w”)

//만약 같은 이름의 파일이 있다면 기존의 내용이 지워진다.

//파일이 없다면 새로 만들어진다.

if(fp==NULL)

printf(“파일 열기 실패”);

else

printf(“파일 열기 성공”);

fclose(fp);

return 0;

}

파일의 삭제(remove())

파일을 삭제하는 함수는 remove()이다. remove()는 stdio.h에 정의되어 있다.

-문법

remove(“파일이름”);

ex)

remove(“sample.txt”);

성공적으로 파일을 삭제한 경우에는 0이 반환된다. 실패한 경우에는 –1이 반환된다.

기타 유용한 함수들

Fig10. 자주 쓰는 함수들

텍스트 파일 읽기와 쓰기

파일을 열었으면 읽고 쓰기가 가능하다. 다음은 파일 입출력 함수들을 입출력 종류에 따라 분류해놓은 것이다.

Fig11. 파일을 읽고 쓰는 함수들

문자와 문자열 단위 입출력의 주된 용도는 사람이 읽을 수 있는 텍스트 파일로 데이터를 저장하는 것이다. 이들 함수는 scanf()와 같이 성공적으로 읽은 항목의 개수를 반환한다. 반환되는 값이 0이면 입출력이 실패했다는 것을 의미한다. 이때 오류의 종류를 자세히 알려면 feof()나 ferror()를 호출하면 된다.

서식화된 입출력은 fprintf()와 fscanf()를 사용한다. 정수를 텍스트로 변환해 파일에 저장하거나 파일에 텍스트를 읽어서 정수로 변환할 때 사용한다.

이진 데이터 입출력은 이진 파일에서만 가능하다. 이것은 메모리에 있는 데이터를 직접 디스크에 그대로 저장하는 것이다. 이것은 주로 전용 프로그램에서 나중에 사용하기 위한 데이터를 저장하기 위해서 사용된다.

문자단위 입출력

하나의 문자를 파일에 쓰는 fpuc()를 사용해보자.

ex)

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

#include <string.h>

int main() {

FILE* fp = NULL;

fp = fopen(“sample.txt”, “w”);

if (fp == NULL)

printf(“파일 열기 실패”);

else

printf(“파일 열기 성공”);

fputc(‘a’, fp);

fputc(‘b’, fp);

fputc(‘c’, fp);

fclose(fp);

return 0;

}

fgetc()를 이용해서 이전에 저장한 파일의 내용을 표시해보자

ex)

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

#include <string.h>

int main() {

FILE* fp = NULL;

int c;

fp = fopen(“sample.txt”, “r”);

if (fp == NULL)

printf(“파일 열기 실패\n”);

else

printf(“파일 열기 성공\n”);

while ((c = fgetc(fp)) != EOF) {

//EOF는 파일의 끝이라는 뜻으로오류가 발생하거나, 파일의 끝에 도달했을 때 fgetc()가 반환한다.

putchar(c);

}

fclose(fp);

printf(“\n”);

return 0;

}

문자열 단위 입출력

문자로 이루어진 한 줄을 출력받으려면 fgets()를 사용해야 한다. 한 줄의 텍스트를 파일에 쓸때는 fputs()를 사용한다.

문자열 입출력 함수를 이용해 텍스트 파일을 복사해보자

ex)#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

int main() {

FILE *fp1;

FILE *fp2;

char file1[100], file2[100];

char buffer[100];

printf(“원본파일 이름: “);

scanf(“%s”, file1);

printf(“복사파일 이름: “);

scanf(“%s”, file2);

if ((fp1 = fopen(file1, “r”)) == NULL) {

fprintf(stderr, “원본파일을 열수 없습니다.\n”,file1);

exit(1);

}

if ((fp2 = fopen(file2, “w”)) == NULL) {

fprintf(stderr, “복사파일을 열수 없습니다.\n”, file2);

exit(1);

}

while (fgets(buffer, 100, fp1) != NULL)

fputs(buffer, fp2);

fclose(fp1);

fclose(fp2);

return 0;

}

형식화된 입출력

정수나 실수 데이터를 기록하는 경우에는 모니터에 출력할 때와 마찬가지로, 정수, 실수 데이터를 문자열로 바꿔 저장한다. 이러한 종류의 입출력을 형식화된 입출력이라고 한다. 특정 형식을 지정하고 이 형식으로 파일에 입출력을 하는 것이다. 형식화된 입출력은 fprintf()와 fscanf()을 이용하여 이뤄진다.

fprintf()와 fscanf()의 문법은 printf()와 scanf()와 비슷하다.

ex)

fprintf(fp,“%d %s %f”,number,name,score) //컴퓨터에서 파일로 데이터를 출력할 때

fscanf(fp,“%d %s %f”,&number,&name,&score) //파일에서 데이터를 입력받을 때

이진 파일 읽기와 쓰기

텍스트 파일에서는 모든 정보가 문자열로 바뀌어서 파일에 출력된다. 다만 이진파일에서는 데이터가 직접 저장되어있는 파일이다. 즉 모든 정보가 이진수의 형태로 파일에 저장된다. 하나의 정수는 4바이트로 표현되므로 4파이트가 파일에 저장된다. 이진파일은 숫자 데이터를 변환과정 없이 바로 읽을 수 있으며 텍스트 파일에 비하여 저장공간도 적게 든다.

하지만 이진 파일은 사람이 파일의 내용을 확인할 수 없다. 텍스트 데이터가 아니기 때문에 모니터에 출력하는 것이 불가능하다.

이진 파일 쓰기(fwrite())

이진 파일은 대량의 데이터를 한 번에 기록할 때 매우 편리하다. 이진 파일에 데이터를 기록할 때는 fwrite()를 이용한다.

-문법

fwrite(메모리블록의 주소, 항목(요소)의 크기, 항목의 개수,FILE 포인터);

ex)

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

int main() {

int buffer[5] = { 1,2,3,4,5 };

FILE* fp = NULL;

fp = fopen(“binary.bin”, “wb”);

if (fp == NULL) {

fprintf(stderr, “파일을 열수 없습니다.”);

return 1;

}

fwrite(buffer, sizeof(int), 5, fp);

fclose(fp);

return 0;

}

이진 파일 읽기(fread())

이진 파일은 텍스트 에디터로 읽을 수 없다. 이진 파일을 읽기 위해서는 fread()을 이용해야 한다. fread()의 형식은 fwrite와 동일하다.

-문법

fread(메모리블록의 주소, 항목(요소)의 크기, 항목의 개수,FILE 포인터);

ex)

fread(buffer, sizeof(int), 5, fp);

다만 여기서는 fp가 가리키는 주소의 파일을 읽어 buffer에 저장한다.

버퍼링

스트림에는 기본적으로 버퍼가 포함되어있다. 버퍼가 있다면 입력, 출력된 데이터는 일단 버퍼에 써진다. 버퍼가 다 채워지면 디스크 파일에 버퍼의 내용을 기록한다.

하지만 프로그램에 따라서 버퍼를 비워야하는 경우도 자주 발생한다. 즉 프로그램이 쓴 데이터가 즉시 하드웨어 장치에 써지기를 원한다면 버퍼를 비워야한다. 버퍼는 fflush()를 호출하면 비워진다.

fflush(fp);

인수는 FILE에 대한 포인터이다. 파일에서 읽을 때도 마찬가지이다. 기존에 버퍼에 있던 것을 무시하고 새로 읽으려면 fflush()를 호출한다. fflush()거 호출되면 이후의 동작은 파일이 열려있는 모드에 따라 달라진다. 만약 읽기 모드라면 버퍼는 단순히 비워지고, 쓰기 모드라면 버퍼의 내용이 디스크에 기록된다.

버퍼를 아예 없애야할 필요가 있을 때는 setbuf()함수를 사용한다. setbuf()는 스트림의 버퍼를 직접 지정하는 함수로서 버퍼자리의 인수에 NULL을 써주면 버퍼가 없어진다.

setbuf(fp,NULL);

임의 접근

지금까지의 입출력 방법은 파일의 처음부터 읽는 순차 접근 방법이었다. 순차 접근은 한 번읽은 데이터를 다시 읽으려면 파일을 닫고 다시 열어야 한다. 하지만 또 다른 입출력 방법이 있다. 임의 접근 방법이라고 한다. 임의 접근 방법은 파일의 어느 위치에서든지 읽기와 쓰기가 가능하다.

임의 접근의 원리

모든 파일에는 파일 포인터가 존재한다. 파일 포인터는 읽기와 쓰기 동작이 현재 어느 위치에서 이루어지는 지를 존재한다. 새 파일이 만들어지게 되면 파일 포인터는 0이고 이것은 파일의 시작 부분을 가리킨다. 기존의 파일의 경우 추가 모드에서 열렸을 때는 파일의 끝이 되고, 다른 모드인 경우에는 파일의 시작부분을 가리킨다.

파일에서 읽기나 쓰기가 수행되면 파일 포인터가 갱신된다. 만약 10바이트 읽었다면 파일포인터는 파일의 위치+10바이트를 가리킨다. 우리가 입출력 함수를 사용하면 파일포인터 위치도 그에 맞게 자동으로 변경된다.

임의 접근을 할 때는 파일 포인터를 움직여서 임의 파일 액세스를 할 수 있다. 파일 포인터가 파일의 위치+10바이트에 있는데 파일의 위치+5바이트의의 값을 읽어야 할 때, 파일 포인터를 움직여서 임의 접근을 할 수 있다는 것이다.

파일포인터 관련 함수

fseek()함수를 이용하면 파일 포인터를 정밀하게 제어할 수 있다. fseek()는 위치 표시자를 원하는 값으로 변경한다.

-문법

fseek(FILE 포인터, 거리, 기준위치);

거리는 기준 위치로부터 위치 표시자가 이동하는 거리를 나타낸다. 거리가 양수이면 앞으로 가고 음수이면 뒤로 간다. 기준 위치는 위치 표시자를 이동시키는 기준 위치를 나타낸다. 기준 위치는 다음과 같은 값 중에서 하나를 사용할 수 있다.

Fig12. 파일포인터의 기준 위치

rewind()를 호출하면 위치 표시자가 0으로 설정된다.

-문법

rewind(FILE 포인터);

위치 표시자의 현재 위치를 알아낼려면 ftell()을 사용한다. ftell()은 현재의 위치 표시자의 값을 long형으로 반환한다. 만약 오류가 발생하면 –1L을 반환한다.

-문법

ftell(FILE 포인터);

파일의 끝을 알아내는 함수는 feof()이다. feof()는 현재 위치가 파일의 끝인지를 알려준다. 이 함수가 필요한 이유는 이진 파일인 경우 EOF를 나타내는 –1도 데이터의 값으로 쓰일 수 있기 때문이다.

-문법

feof(FILE 포인터);

전처리 및 다중소스 파일

전처리기란?

전처리기는 컴파일하기 전에 소스파일을 처리하는 컴파일러의 한 부분이다. 전처리기는 보통 컴파일러에 포함되어 있고, 자동으로 실행되며 컴파일러의 하나의 요소로 취급된다.

전처리기는 소스파일을 처리하여 수정된 소스파일을 생성한다. 이 수정된 소스 파일은 다음 단계의 컴파일러에 의해 컴파일된다. 전처리기에 의해 수정된 소스파일은 컴파일 과정이 끝난 후에 삭제되기 때문에 사용자는 볼 수 없다.

전처리기 지시자는 #로 시작한다. 다음의 표에서 전처리기에서 사용하는 지시자들이 설명되어 있다.

Fig13. 전처리기 지시자

단순매크로

#define 지시자를 사용하면 숫자 상수에 이름을 부여할 수 있다. #define 문을 이용해 숫자 상수를 기호 상수로 만든 것을 단순 매크로라고 한다.

-문법

#define 기호상수이름 값

ex)

#define MIN_SIZE 10

상수를 기호 상수로 표시하는 이유는, 첫 번째로 기호상수는 상수에 비해 프로그램의 가독성을 높여주기 때문이다. 상수보다 기호상수가 더 많은 정보를 준다. 두 번째로 값의 변경이 용이 하다는 점이다. 상수를 사용하면 값을 일일이 찾아 변경해야 하지만, 기호상수는 맨 위의 단순매크로의 값만 변경하면 되기 때문에 훨씬 편하다.

#define을 사용한 매크로의 예이다.

#define PI 3.141592 //원주율

#define EOF (-1) //파일의 끝

#define EPS 1,0e-9 //실수의 계산한계

#define DIGITS “0123456789” //문자상수

#define getchar() getc(stdin) //stdio.h에 정의

함수 매크로

함수 매크로란 매크로가 함수처럼 매개변수를 가지는 것이다. 매크로를 이용하면 복잡한 계산을 숨기고 보다 간단하게 나타낼 수 있다. 즉, 함수를 사용하는 것과 유사한 효과를 낼 수 있다.

-문법

#define 매크로이름(인수) (작동문장)

ex)

#define SQUARE(x) ((x)*(x)) //제곱계산

위의 예제에서는 주어진 수의 제곱을 구하는 SQUARE라는 매크로를 정의하고 있다. 다만 작동문장에서의 모든 인수는 ()로 감싸져 있어야 한다. SQUARE는 x라는 매개변수를 가진다. 전처리기가 SQUARE를 발견하게 되면 정의된 코드로 변환하고 x를 매크로 호출 시 주어지는 인수로 치환한다.

매크로 사용 시 주의할 점

함수 매크로에서는 매개변수의 자료형을 써주지 않는다. 따라서 어떠한 자료형도 사용이 가능하다. SQUARE(a+b)와 같이 변수를 포함한 수식도 가능하다.

함수 매크로를 정의할 때는 매크로의 매개변수를 꼭 ()로 감싸주어야 한다. 함수 매크로에서는 단순히 매개변수를 기계적으로 대치해주기 때문이다.

만약 감싸주지 않으면 사용자가 원하는 매개변수가 전달이 안될 수 있다. 함수 매크로를 정의할 때 매개변수를 ()로 감싸주지 않았을 경우, SQUARE(a+b)으로 (a+b)의 제곱을 계산한다고 하자. 사용자가 의도한 바는(a+b)*(a+b)였으나 함수 매크로는 a+b를 x의 자리에 단순히 대치하기 때문에 실행결과는 a+b*a+b가 된다. 이처럼 원치않는 결과를 방지하기 위해서는 매개변수를 ()로 꼭 감싸주어야 한다.

또한 매크로를 정의할 때 정의한 매개변수는 모두 사용되어야한다.

ex)#define HALF(x,y) ((x)/2) //오류!

또한 매크로이름과 인수 사이에 공백이 있으면 안된다.

ex)#define ADD (x,y) ((x)+(y)) //전처리기는 기호상수 정의라고 정의함

#연산자

함수 매크로를 사용하다보면 매크로의 인수를 문자열로 변경하고 싶은 경우가 있다. 예를 들어서 “x=5”와 같이 변수의 이름과 변수의 값을 동시에 출력해야 한다고 하자. 따라서 변수를 출력하는 다음과 같은 매크로를 이용한다고 하자.

#define PRINT(exp) printf(“(exp)=%d\n”,exp)

위의 매크로는 제대로 동작하지 않는다. 문자열 안에 있는 매개변수는 치환하지 않기 때문이다.

전달된 실제 인수를 문자열 형태로 출력하려면 #연산자를 사용해야 한다.

#define PRINT(exp) printf(#exp“ =%d\n”,exp);

#은 문자열 변환 연산자라고 불린다. 매크로 정의에서 매개 변수 앞에 #가 위치하면 매크로 호출에 의하여 전달되는 인수는 큰따옴표로 감싸지고 문자열로 변환된다. #연산자는 매개변수를 가지는 매크로에서만 사용할 수 있다.

내장매크로

내장 매크로란 컴파일러가 프로그래머들이 유용하게 사용하도록 제공하는 몇 개의 미리 정의되어있는 매크로이다.

Fig14. 내장 매크로

사용자가 이들 매크로를 사용하면 전처리기에 의하여 정의된 코드로 치환된다. __LINE__ 매크로는 정수를 반환하므로 %d로 출력해야 하고, 그 외의 매크로는 문자열을 반환하므로 %s를 이용해 출력해야 한다.

ex)

printf(“컴파일 날짜: %s”,__DATE__);

printf(“오류 발생=%s 라인발생=%d”,__FILE__,__LINE__);

함수 매크로와 함수

함수 매크로는 함수에 비해 빠르다는 장점이 있다. 함수를 호출하기 위해서는 인수와 복귀 주소를 시스템 스택에 저장해야 한다. 다만 함수 매크로는 단순히 코드가 그 위치에 삽입되는 것이기 때문에 함수 호출의 복잡한 단계를 거칠 필요가 없다.

다만 함수 매크로는 함수에 비해 복잡한 작업을 할 수 없다는 단점이 있다. 함수 매크로는 함수의 길이를 어느 한도 이상 길게 할 수 없다. 또한 함수는 어느 한 곳에 저장해두고 호출하는데 반해, 함수 매크로는 호출할 때마다 코드를 삽입하기 때문에 소스 코드의 길이가 커진다.

#ifdef, #endif

#ifdef는 조건부 컴파일을 지시하는 전처리 지시자이다. 조건부 컴파일이란 어떤 조건이 만족되는 경우에만 지정된 소스 코드 블록을 컴파일하는 것이다. #ifdef는 #ifdef 다음에 있는 매크로를 검사하여 매크로가 정의되어 있으면 #ifdef와 #endif사이에 있는 모든 문장을 컴파일한다. 그렇지 않으면 문장들은 컴파일되지 않아 실행코드에 포함되지 않는다.

-문법

#ifdef 매크로

실행문장

#endif

ex)

#ifdef DEBUG

printf(“value=%d\n”);

#endif

위의 예에서 DEBUG라는 매크로가 정의되어있으면 컴파일된다. DEBUG 매크로가 선언되어있지 않으면 아예 컴파일에 포함되지 않는다. 매크로 선언은 #define을 사용해 해주면 된다.

#ifndef

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

문법은 #ifdef와 동일하다.

#undef

#undef는 매크로의 정의를 취소해준다. #undef는 주로 이전의 정의를 무효화하고 새로 정의하고 싶을 때 사용한다.

-문법

#undef 매크로이름

ex)

#define SIZE 100

#undef SIZE

#define SIZE 100

#if, #else, #endif

#if는 #if다음의 있는 기호를 검사하여 기호가 참으로 계산되면 #if와 #endif 사이에 있는 모든 코드를 컴파일한다. 조건은 상수 수식이어야 하고, 관계연산자나 논리연산자는 사용할 수 있다.

-문법

#if 수식

실행문장;

#endif

ex)

#if DEBUG==1

printf(“value=%d\n”);

#endif

앞에서 학습한 #ifdef는 매크로의 값에는 상관하지 않는다. 단순히 매크로가 정의만 되어있으면 다음 문장을 실행한다. 하지만 #if는 매크로의 값에 따라서 컴파일 여부를 결정한다. 위의 예에서 DEBUG가 1이면 실행문장이 실행된다. 만약 DEBUG가 1로 정의되지 않거나 아예 정의되어 있지 않으면 실행하지 않는다. 매크로가 정의되어 있는 지 검사하려면 defined()를 사용한다.

다중 소스 파일

C에서는 하나의 프로그램이 여러 소스 파일로 이루어질 수 있다. 프로그램을 여러 개의 소스 파일로 만드는 이유는 서로 밀접하게 관련된 함수들을 모아 독립적인 파일에 저장시켜 두면 나중에 다시 사용할 수 있기 때문이다.

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

Fig16. 헤더 파일과 여러개의 소스파일을 통해 프로그램을 작성한 모습

+관습적인 의미로, 컴파일러가 제공하는 헤더파일을 포함할 때는 <>를 사용하고 사용자가 만든 헤더파일을 포함할 때는 “”를 사용한다.

비주얼 스튜디오에서 다중 소스 파일을 사용하려면 오른쪽의 솔루션 탐색기의 소스파일에서 오른쪽 마우스 버튼을 누르고 “추가->새항목->c++파일”을 선택하고 이름을 적어주면 된다. 헤더 파일도 같은 방식으로 추가한다.

헤더 파일에 들어가는 것

헤더 파일을 사용하지 않으면 다른 소스 파일에서 제공하는 함수를 사용하기 전에 함수 원형을 소스 파일 첫 부분에 선언해야 한다. 사용하는 함수의 모든 원형을 입력해야하고, 입력한 함수 원형을 모든 소스 파일에 입력해야 한다.

따라서 헤더파일을 작성하여서 여기에 함수의 원형을 넣어두고 다른 소스 파일에서는 이 헤더 파일을 포함하는 것이 좋다. 보통 헤더 파일에는 함수의 원형, 구조체 정의, 매크로 정의, typedef의 정의를 넣어주면 좋다.

Fig16. 헤더파일을 포함시킨 코드

다중 소스 파일과 외부 변수

다중 소스 파일의 경우, 하나의 프로그램에 여러 개의 소스 파일이 존재한다. 외부 변수를 사용하려면 사용하려는 외부 변수가 전역 변수여야 한다. 전역 변수가 정의된 소스 파일 외의 다른 소스 파일에서도 사용하려면 외부 변수로 선언해야 한다. 예를 들어 main.c에 다음과 같은 전역 변수가 선언되어 있다고 가정하자.

double gx,gy;

만약 power.c에서 이 변수를 사용하려면 먼저 다음과 같이 소스 파일 처음부분에서 외부 변수로 선언을 해야 한다.

extern double gx,gy;

extern 키워드는 변수가 외부에서 선언되었다는 것을 컴파일러에게 알려주는 역할을 한다. extern으로 선언된 변수는 전역 변수처럼 소스파일의 모든 함수에서 사용할 수 있다.

구조체, typedef, 매크로 정의

구조체, typedef, 매크로 등을 여러 소스 파일에 걸쳐서 사용하려면 이들을 헤더파일에 넣어주는 것이 좋다.

비트 필드 구조체

비트 필드 구조체는 구조체의 일종으로서 멤버들의 크기가 비트 단위로 나누어져 있는 구조체를 의미한다. 즉 비트들을 멤버로 가지는 구조체이다. 비트 필드를 사용하면 꼭 필요한 만큼의 비트만을 사용할 수 있어서 메모리를 효율적으로 사용하는 것이 가능하다.

다음의 문장은 상품정보를 저장하는 비트 필드 구조체이다.

struct product{

unsigned style : 3;

unsigned size : 2;

unsigned color : 1;

};

product 구조체에는 style,size,color의 3개의 필드가 정의되어 있다. 각각 3,2,1 비트를 사용한다. 그러므로 style은 0~7까지의 값, size는 0~3까지의 값, color은 0~1까지의 값을 나타낼 수 있다. 이들 비트 필드의 크기는 멤버 이름 다음 :과 숫자를 통해 나타낸다. 비트 필드는 메모리에 선언된 순서로 순차적으로 저장된다.

비트 필드를 사용할 때 주의할 점

비트 필드를 사용할 때 비트 필드 멤버의 이름은 생략이 가능하다. 이 때에는 해당 비트를 사용할 수 없으며 단지 자리만 차지하게 된다.

struct product{

unsigned style : 3;

unsigned size : 2;

unsigned color : 1;

unsigned : 2;

};

위의 마지막 선언에서 마지막 비트 필드는 이름이 없다. 따라서 2비트는 사용되지 않으면서 자리만 차지하게된다. 이러한 비트 필드가 필요한 이유는 워드(컴퓨터가 한번에 처리하는 크기)의 경계에 비트 필드가 걸치게 되면 입출력 속도가 상당히 늦어지기 때문에 다음 멤버가 워드의 처음에서 시작할 수 있도록 이러한 필드를 두는 것이다.

이름이 없는 비트 필드 중, 크기가 0인 비트 필드를 둘 수 있다. 이 때에는 현재의 워드의 남아있는 비트들을 모두 버린다. 이는 다음 비트 필드가 워드의 처음에서 시작하도록 만들기 위함이다.

struct product{

unsigned style : 3;

unsigned : 0;

unsigned size : 2;

unsigned color : 1;

};

여기서 첫 번째 워드의 하위 3비트를 style이 차지하고 첫 번째 워드의 나머지 비트들은 버려진다. 다음 워드에서 size,color가 할당된다.

또한 비트 필드의 크기를 지정할 때는 워드의 크기를 넘어설 수 없다. 비주얼 c++에서 워드의 크기는 32비트이다. 따라서 32비트를 넘는 비트 필드의 크기를 지정하면 오류가 발생한다.

비트 필드 구조체도 구조체이기 때문에 int나 char같은 일반 멤버도 같이 선언할 수 있다.

비트 필드의 응용 분야

비트 필드를 사용하는 첫 번째 이유는 데이터를 저장할 때 메모리를 절약하기 위함이다. ON, OFF의 상태를 가지는 변수를 저장할 때 int형 변수를 사용하는 것보다 1비트의 비트 필드를 사용하는 것이 메모리를 절약할 수 있다.

비트 필드가 많이 사용되는 분야는 하드웨어 제어이다. 컴퓨터에 연결되는 하드웨어 장치들은 메모리 주소에 매핑되는 하드웨어 포트를 이용하여 제어하는 경우가 많다. 이런 하드웨어 장치들은 비트단위로 제어하도록 되어 있다. 따라서 하드웨어 장치를 비트 필드로 정의하여 사용한다.

--

--