'pc관련/C언어'에 해당되는 글 19건

  1. 2019.04.16 C언어_배열
  2. 2019.04.14 CreateWindow() - 10개의 인수
  3. 2019.04.14 API 윈도우 창띄우기
  4. 2019.04.11 C언어_디스크파일의 사용
  5. 2019.04.07 C언어_링크리스트
  6. 2019.04.07 C언어_포인터,고급기능
  7. 2019.04.03 고급 프로그램제어문
  8. 2019.03.28 C언어_변수에 범위
pc관련/C언어2019. 4. 16. 20:33

배열(array)은 동일한 하나의 데이터형을 가진 연속된 원소들로 구성된다. 배열을 원한다면, 선언(declaration)을 사용하여 이를 컴파일러에게 알려야 한다. 배열 선언(array declaration)은 그 배열이 몇개의 원소를 가지고 있으며, 원소들의 데이터형이 무엇인지 컴파일러에게 알려 준다. 이 정보가 있어야만, 컴파일러는 배열을 바르게 설정할 수 있다. 배열 원소들은 보통의 변수들이 가질 수 있는 것과 동일한 데이터형들을 가질 수 있다. 다음과 같은 배열 선언의 예를 살펴보자

/* 배열 선언의 몇 가지 예 */
int main(void)
{
float candy[365]; /* 465개의 float 형 값을 가지는 배열 */
char code[12]; /* 12개의 char 형 값을 가지는 배열 */
int states[50]; /*50개의 int형 값을 가지는 배열 */
......
}

각괄호([])가 candy, code, states가 배열이라는 것을 나타낸다. 각괄호 안에 있는 수는 그 배열 안에 있는 원소의 개수를 나타낸다.

배열 안에 있는 각 원소들은, 인덱스(index)라 부르는 첨자 번호를 사용함으로서 개별적으로 접근할 수 있다. 인덱스는 0부터 시작한다. 따라서, candy[0]은 candy 배열의 첫 번재 원소다. candy[364]는 365번째 원소, 즉 마지막 원소다.

이것은 우리가 이미 아는 내용이다. 이제 좀더 새로운 내용을 살펴보자.

초기화...
일반적으로, 배열은 프로그램에 필요한 데이터를 저장하는 데 사용된다. 예를 들면, 12개의 원소를 가지는 배열은 1년의 각 달의 날짜 수를 저장할 수 있다. 이러한 경우에는 프로그램의 시작 부분에서 배열을 초기화 시키는 것이 편리하다. 배열을 어떻게 초기화하는지 살펴보자

우리는, 단일 값을 가지는 변수를 -흔히 스칼라(scalar) 변수라고 부른다- 을 선언해서 다음과 같은 수직으로 초기화 시킬 수 있다는 것을 알고 있다.

int fix = 1;
float flax = PI * 2;

여기서 PI는 매크로로 이미 정의되어 있다고 가정했다. C는 다음과 같이, 새로운 신택스를 사용하여 초기화를 배열에 까지 확장한다.


int main(void)
{
int powers[8] = {1,2,4,6,8,16,32,64}; /* ANSI에서만 가능하다 */
........
}

여기서 볼 수 있듯이, 배열은 콤마로 분리된 값들의 리스트를 중괄호로 감싸서 초기화한다. 원한다면 값과 콤마 사이에 스페이스를 넣을 수도 있다. 첫 번째 원소 (powers[0])에 값 1이 대입되고, 나머지 원소들에도 차례로 값이 대입된다. (컴파일러가 이 형식의 초기화를 신택스 에러로 인식한다면, ANSI 이전의 컴파일러 이기 때문이다. 그러한 경우에는 배열 선언 앞에 키워드 static를 붙이면 해결된다. 이 키워드의 의미에 대해서는 '12장 : 기억부류, 연계, 메모리 관리;에서 설명한다.) 리스트 10.1 은 각 달의 수를 출력하는 짧은 프로그램이다.

리스트10.1

/* day_mon1.c -- 각 달의 날짜 수를 출력한다 */
#include
#define MONTHS 12
int main(void)
{
int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};
int index;

for (index = 0; index < MONTHS; index++)
printf("%2d월: 날짜 수 %2d\n", index+1, days[index]);

return 0;
}



출력 결과는 다음과 같다.
1월 : 날짜 수 31
2월 : 날짜 수 28
3월 : 날짜 수 31
4월 : 날짜 수 30
5월 : 날짜 수 31
6월 : 날짜 수 30
7월 : 날짜 수 31
8월 : 날짜 수 31
9월 : 날짜 수 30
10월 : 날짜 수 31
11월 : 날짜 수 30
12월 : 날짜 수 31

그다지 훌륭하지는 않지만, 이 프로그램은 4년에 딱 한달만 날짜 수가 틀린다. 이 프로그램은 콤마로 분리된 값들의 리스트를 중괄호({})로 감싸서 days[]를 초기화 한다.

이 예제는 배열 크기를 기호 상수 MONTHS를 사용하여 나타내고 있다. 이것은 일반적으로 추천할 만한 테크닉이다. 예를 들어, 갑자기 1년이 13개월로 바뀐다면, 해당하는 #defind 지시문만 수정하면 되기 때문에, 프로그램에서 배열 크기가 사용된 모든 곳을 일일이 찾아다니며 고칠 필요가 없다.

NOTE
배열에 const 사용하기
때로는 배열을 읽기 전용으로만 사용하고 싶을 때가 있다. 즉, 프로그램이 배열에서 값을 꺼내 오기는 하지만, 배열에 새로운 값을 써 넣지 않는 것이다. 이러한 경우에는, 배열을 선언하고 초기화할 때 const 키워드를 사용할 수 있고, 또한 사용해야 한다. 그러므로 리스트 10.1에서 배열을 다음과 같이 초기화하는 것이 더 나은 선택이다.

const int days [MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};

이것은, 프로그램이 배열에 있는 각 원소를 상수로 취급하게 만든다. 보통의 변수와 마찬가지로, 일단 const 로 선언되면 나중에 값들을 대입할 수 없기 때문에, const 데이터를 초기화하려면 선언을 사용해야 한다. 이것을 알게 되었으므로, 이제부터 나오는 예제에 const를 사용할 수 있다.


그런데 배열을 초기화 하는데 길패하면 어떻게 될까? 리스트 10.2는 그와 같은 경우에 무슨 일이 벌어지는지 보여준다.

리스트 10.2

/* no_data.c -- 초기화시키지 않은 배열 */
#include
#define SIZE 4
int main(void)
{
int no_data[SIZE]; /* 초기화시키지 않은 배열 */
int i;
printf("%2s%14s\n",
"i", "no_data[i]");
for (i = 0; i < SIZE; i++)
printf("%2d%14d\n", i, no_data[i]);

return 0;
}

 

다음은 프로그램의 실행 예다. (시스템에 따라 다른 결과가 나올 수 있다.)
i no_date[!]
0 -858993460
1 -858993460
2 -858993460
3 -858993460

배열 원소는 보통의 변수와 같다. 사용자가 이들을 초기화하지 않는다면 그들은 아무 값이나 가질 수 있다. 컴파일러는 우연히 그 메모리 위치에 놓여 있는 값들을 사용한다. 사용자의 시스템이 이 실행 예와 다를 결과가 나오는 것도 바로 이 때문이다.

NOTE
기억 부류에 대한 사전 통고
배열도, 다른 변수들과 마찬가지로, 여러 가지 기억 부류(storage class)를 사용하여 생성할 수 있다. 이 주제에 대해서는 12장에서 설명한다. 지금 당장은, 이 장에서 설명하는 배열들이 모두 자동 기억 부류에 속한다는 것만 알면 된다. 이것은 그 배열들이 static 키워드 없이 함수 안에서 선언되고 있다는 것을 의미한다. 지금까지 사용한 모든 변수들과 배열은 자동 기억 부류에 속한다.

이 시점에서 기억 부류를 언급하는 이유는, 서로 다른 기억 부류는 서로 다른특성을 가지고 있기 때문이다. 그래서 이 장에서 설명하는 모든 것을 다른 기억 부류로 일반화시킬 수 ㅇ없다. 특히, 일부 다른 기억부류에 속하는 변수와 배열들은 사용자가 초기화하지 않을 경우 그들의 내용을 0으로 설정한다.

초기화 리스트에 들어 있는 항목들의 개수는 배열의 크기와 일치해야 한다. 이것이 일치하지 않으면 무슨 일이 벌어질까? 초기값의 개수가 두 개 모자란 상태의 리스트를 가지고 앞의 예제를 다시 시도해 보자

리스트 10.3
/* some_data.c -- 일부만 초기화된 배열 */
#include
#define SIZE 4
int main(void)
{
int some_data[SIZE] = {1492, 1066};
int i;

printf("%2s%14s\n",
"i", "some_data[i]");
for (i = 0; i < SIZE; i++)
printf("%2d%14d\n", i, some_data[i]);

return 0;
}

다음은 프로그램의 실행 예다. (시스템에 따라 다른 결과가 나올 수 있다.)
i some_data[!]
0 1492
1 1066
2 0
3 0

여기서 볼 수 있듯이, 컴파일러는 문제를 알아채지 못했다. 컴파일러는, 초기값 리스트에 있는 값들을 다 사용하고 나서, 나머지 원소들을 0으로 초기화했다. 즉, 사용자가 배열을 전혀 초기화하지 않으면, 배열 원소들은, 초기화하지 않은 보통의 변수들처럼 쓰레기 값들을 갖게 된다. 그러나 배열을 일부분만 초기화하면, 나머지 원소들이 0으로 설정된다. 컴파일러는 이런 넉넉함을 에러로 간주한다. 그러나 컴파일러의 이런 변덕에 속앓이를 할 필요가 없다. 각괄호 안의 배열 크기를 생략하면 컴파일러가 스스로 초기값 리스트에 맞에 배열 크기를 설정한다.

리스트 10.4

/* day_mon2.c -- 컴파일러가 원소 개수를 카운트한다 */
#include
int main(void)
{
const int days[] = {31,28,31,30,31,30,31,31,30,31};
int index;
for (index = 0; index < sizeof days / sizeof days[0]; index++)
printf("%2d월: 날짜 수 %2d\n", index +1,
days[index]);

return 0;
}

리스트 10.4에서 주목할 점은 다음 두 가지다.
■ 빈 각괄호를 사용하여 배열을 초기화하면, 컴파일러는 초기값리스트에 있는 항목들의 개수를 카운트하여 그것을 배열 크기로 가지는 배열을 만든다.
■ for 루프 제어 명령문에서 우리가 무엇을 했는지 주목하라. 우리는 정확하게 카운트하는 능력이 (당연히) 부족하기 때문에, 컴퓨터가 배열 크기를 계산해서 우리에게 알려 주도록 부탁한다. sizeof 연산자는 객체 또는 데이터형(type)의 크기를 바이트 수로 알아낸다. 그러므로 sizeof days는 바이트 수로 배열 전체의 크기다. sizeof days[0]은 바이트 수로 배열 원소 하나의 크기다. 벼열 전체의 크기를 배열 원소 하나의 크기로 나누면, 그 배열에 몇 개의 원소가 있는지 알 수 있다.

프로그램의 실행 결과는 다음과 같다.
1월: 날짜 수 31
2월: 날짜 수 28
3월: 날짜 수 31
4월: 날짜 수 30
5월: 날짜 수 31
6월: 날짜 수 30
7월: 날짜 수 31
8월: 날짜 수 31
9월: 날짜 수 30
10월: 날짜 수 31

에라! 값을 10개만 입력했내 ㅡㅡ.. 배열 크기를 프로그램이 직접 알아내게 하는 이 방법은, 배열의 끝을 지나쳐서 출력하는 사태를 예방한다. 그러나 또한 이것은 자동 카운트의 잠재적인 단점을 경고한다. 원소의 개수가 틀렸더라도 컴파일러가 이 에러를 잡아내지 못한다는 것이다.

'pc관련 > C언어' 카테고리의 다른 글

CreateWindow() - 10개의 인수  (0) 2019.04.14
API 윈도우 창띄우기  (0) 2019.04.14
C언어_디스크파일의 사용  (0) 2019.04.11
C언어_링크리스트  (0) 2019.04.07
C언어_포인터,고급기능  (0) 2019.04.07
Posted by 둥이파파^^
pc관련/C언어2019. 4. 14. 13:17

윈도우를 생성할때 쓰는 CreateWindow 함수..

HWND CreateWindow(lpszClassName, lpszWindowName, dwStyle, x, y, nWidth,, nHeight, hwndParent, hmenu, hinst, lpvParam)

10개의 인수를 취하는데 순서에 맞추어 정확하게 인수를 전달해야 한다. 한개씩 살펴보면..

■ lpszClassName
: 생성하고자 하는 윈도우의 클래스를 지정. WndClass.lpszClassName 맴버에 대입했던 것을 같이 대입한다.

■ lpszWindowName
: 윈도우의 타이틀 바에 나타날 문자열

■ dwStyle
: 윈도우의 형태를 지정하는 인수
WS_CAPTION - 타이틀 바를 가진다.
WS_HSCROLL - 수평 스크롤바를 가진다.
WS_VSCROLL - 수직 스크롤바를 가진다.
WS_MAXIMIZEBOX - 최대화 버튼을 가진다.
WS_MINIMIZEBOX - 최소화 버튼을 가진다.
WS_SYSMENU - 시스템 메뉴를 가진다.
WS_THICKFRAME - 크기를 조절할 수 있는 경계선을 가진다.

WS_OVERLAPPEDWINDOW - 타이틀 바, 시스템 메뉴, 크기 조절, 최소 최대 버튼등이 한번에 정의 되어있다.

■ X, Y, nWidth, nHeight
: ,X, Y, 는 화면의 좌표값이고 nWidth, nHeight 는 윈도우의 높이와 넓이 이다. CW_USEDEFAULT 로 설정하면 운영체제가 적당한 크기를 찾게 된다.

■ hWndParent
: 부모윈도우가 있을경우 부모 윈도우의 핸들을 지정한다. 없거나 최상위 윈도우일경우 NULL로 지정

■ hmenu
: 윈도우에서 사용할 메뉴의 핸들. CreateWindows 함수로 만들어진 윈도우에만 적용되는 메뉴. 없으면 NULL

■ hinst
: 프로그램의 핸들을 지정한다. WinMain의 인수 hInstance.

■ lpvParam
: CREATESTRUCT라는 구조체의 번지. 보통은 NULL 값이며 잘 사용되지 않는다.

'pc관련 > C언어' 카테고리의 다른 글

C언어_배열  (0) 2019.04.16
API 윈도우 창띄우기  (0) 2019.04.14
C언어_디스크파일의 사용  (0) 2019.04.11
C언어_링크리스트  (0) 2019.04.07
C언어_포인터,고급기능  (0) 2019.04.07
Posted by 둥이파파^^
pc관련/C언어2019. 4. 14. 13:16

#include

LRESULT CALLBACK wndproc (HWND,UINT, WPARAM, LPARAM);
HINSTANCE ginst;
LPCTSTR lpct=TEXT("첫 번째 예재");

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
HWND hwnd;
MSG message;
WNDCLASS wndclass;
ginst=hInstance;

wndclass.cbClsExtra=0;
wndclass.cbWndExtra=0;
wndclass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.hCursor=LoadCursor(NULL,IDC_ARROW);
wndclass.hIcon=LoadIcon(NULL, IDI_APPLICATION);
wndclass.hInstance=hInstance;
wndclass.lpfnWndProc=wndproc;
wndclass.lpszClassName=lpct;
wndclass.lpszMenuName=NULL;
wndclass.style=CS_HREDRAW | CS_VREDRAW;
RegisterClass(&wndclass);

hwnd=CreateWindow(lpct,lpct, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, (HMENU)NULL,hInstance,NULL);

ShowWindow(hwnd,nShowCmd);

while (GetMessage(&message,NULL,0,0))
{
TranslateMessage(&message);
DispatchMessage(&message);
}
return (int)message.wParam;
}

LRESULT CALLBACK wndproc(HWND hWnd,UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_DESTROY : PostQuitMessage(0);
return 0;
}
return (DefWindowProc(hWnd, msg, wParam, lParam));
}

 

윈도우 창하나 띄우는 API 소스다
교제에 첫번째 예재가 이러한데 책이 바이블식으로 되있는데도 이해가 힘들다.

LRESULT CALLBACK wndproc (HWND,UINT, WPARAM, LPARAM);

이부분에 대해..
보면 LRESULT와 CALLBACK이라는 데이터형이 있습니다. LRESULT는 윈도우 프로시저에서 반환되는 데이터형이고 CALLBACK은 FAR PASCAL을 재정의한 것으로 콜백루틴이나 프로시저에서 사용한다.

먼저 LRESULT에 대해 말해 보면 LRESULT는 비주얼 C++에서 다음과 같이 선언되 있습니다.

#define LRESULT LONG

즉 LRESULT는 long 변수의 다른 이름일 뿐이다. long이라는 리턴값을 쓰지 않고 굳이 LRESULT라고 재선언한 것은 이 값이 리턴값임을 좀더 명확히 나타내기 위한 프로그래머의 의도라 생각하면 된다. 결국 LRESULT로 반환되는 값은 long값이라 생각하면 되는데 그렇다고 해서 꼭 숫자일 필요는 없다. long은 4 바이트 변수이므로 LRESULT에 포인터를 캐스팅해서 반환해도 무관하다. 포인터 역시 4바이트의 변수일 뿐이다. 대부분의 윈도우 프로그램에서는 LRESULT 값으로 객체의 포인터를 반환하는 것을 자주 볼 수 있을 것이다.

CALLBACK은 FAR PASCAL의 다른 이름이다. FAR라는 것은 원거리 포인터임을 나타내는 것이고 PASCAL이라는 것은 함수 호출규약을 나타내는 것이다. 프로그래밍 내에서 함수호출이라는 것은 그 함수의 시작포인터로 점프하는 것을 의미하고, 또 윈도우에서는 가상 메모리 주소를 사용하므로 대부분의 함수 진입부가 원거리 포인터라는 뜻.

하지만 윈도우 95이상에서는 원거리 포인터와 근거리 포인터의 구분이 없으므로 FAR라는 것은 무시해도 상관없다. PASCAL 호출규약이라는 것은 함수를 호출할 때 넘겨지는 인자가 스택에 어떠한 순서로 쌓일지에 대한 약이다. C에서는 인자를 왼쪽에서부터 오른쪽으로 차례로 인자 값을 스택에 넣어 넘겨주고 함수에 진입하면 이와는 반대순서로 스택에서 그 값을 받아와 사용한다. 하지만 C++에서는 이와 반대의 순서로 스택에 인자를 넣어 사용하게 된다. 또한 PASCAL 호출규약은 함수를 호출하면 이전에 사용되던 변수의 값 ax, bx, cx 등의 값을 먼저 스택에 넣어두었다가 함수가 리턴되기 전에 이 값을 복원시켜주지만 C에서는 함수를 호출하기 전 이러한 일들을 하고 함수가 끝나서 이전의 스텝으로 돌아오면 그제서야 변수값을 복원하는 일을 한다다.

이러한 것이 바로 FAR PASCAL이 의미하는 것이고 이것을 CALLBACK이라고 선언해 사용하는 것은 이 함수가 콜백함수로 사용됨을 프로그래머가 명시하기 위한 것이다. 즉 자신이 짠 이 함수는 윈도우에서 CALLBACK되어 불리어지는 함수라는 것을 자신이나, 다른 사람이 봐서 금방 알 수 있도록 이렇게 이름 붙여놓은 것이다. 이 두 가지 모두 사용 용도를 명확히 하는 역할을 할 뿐 다른 것과의 차이점은 없는데, 이렇게 사용하는 이유는 윈도우 프로그램의 크기가 예전에 비해 무척 커져가고 작업도 여러 사람이 같이 하는 경우가 많아 서로의 의도를 다른 사람에게 명확하게 설명하고자 하는 제작자의 의도가 들어있다고 보면 된다.

'pc관련 > C언어' 카테고리의 다른 글

C언어_배열  (0) 2019.04.16
CreateWindow() - 10개의 인수  (0) 2019.04.14
C언어_디스크파일의 사용  (0) 2019.04.11
C언어_링크리스트  (0) 2019.04.07
C언어_포인터,고급기능  (0) 2019.04.07
Posted by 둥이파파^^
pc관련/C언어2019. 4. 11. 21:33

* 디스크 파일의 사용
대부분의 프로그램에서는 데이터나 도는 사용 환경을 저장하는 등 여러 가지 목적으로 디스크 파이를 사용한다. 오늘은 다음과 같은 내용을 다룰 것이다.

·디스크 파일에 관련된 스트림
·C에서 사용되는 두 가지 형태의 디스크 파일
·파일에 데이터를 저장하는 방법
·파일에서 데이터를 읽어들이는 방법
·파일을 닫는 방법
·디스크 파일의 관리
·임시 파일의 사용

1. 스트림과 디스크 파일
: C는 디스크 파일을 포함하여 모든 입력과 출력을 스트림으로 수행한다. 앞에서는 키보드,화면, 그리고 DOS 시스템에서 프린터와 같이 특정 장치와 연결되어 있는 C의 미리 정의된 스트림을 사용하는 방법을 다루었다. 디스크 파일 스트림은 기본적으로 동일한 방법으로 사용된다. 이것은 스트림을 통한 입출력의 한 가지 장점이다. 한 가지 스트림을 사용하는 방법은 다른 스트림에서 변화가 없거나 약간만 다르게 해서 사용할 수 있다. 디스크 파일 스트림에서 중요한 차이점이 있다면, 프로그램이 특정 디스크 파일과 관련된 스트림을 반드시 생성해야 한다는 것이다.

2. 디스크 파일의 종류
: 14번째 강의에서 C의 스트림에 텍스트(text)와 이진(binary)의 두 가지 종류가 있다는 것을 설명했다. 이런 두 가지 형태의 스트림 중에서 어떤 것도 파일과 관련될 수 있는데, 파일을 적절한 모드로 사용하기 위해서는 각각의 특성을 이해할 필요가 있다.

텍스트 스트림(text stream)은 텍스트 모드 파일과 관련되어 있다. 텍스트 모드 파일은 일련의 문장들로 구성된다. 각각의 문장은 문자로 구성되고 문장의 마지막을 나타내는 하나 이상의 문자를 포함한다. 문장의 최대 길이는 255자이다. '문장'은 C의 문자열과 다르다는 것을 기억할 필요가 있다. 문장에는 널 문자(\0)가 포함되지 않는다. 텍스트 모드의 스트림을 사용할 때 C의 문장 진행 문자(\n)와 운영체제가 디스크 파일에서 문장의 마지막을 표시하기 위해서 사용하는 문자 사이에는 변환이 수행된다. DOS 환경에서는 개행 문자(CR : carriage return)와 다음 줄 문자(LF : line feed)로 변환된다. 데이터가 텍스트 모드의 파일에 저장될 때 각각의 \n은 CR-LF로 변환된다. 데이터가 디스크 파일에서 읽어들여질 때 DR-LF는 다시 \n으로 변환된다. UNIX 환경에서는 어떤 변환도 수행되지 않으므로 문장 진행 문자가 변경되지 않고 그대로 남는다.

이진 스트림(binary stream)은 이진 모드 파일과 관련되어 있다. 모든 데이터는 있는 그대로 저장되거나 읽어들여지고, 문장 내에서 구분이나 문장의 마지막을 표시하는 문자는 사용되지 않는다. NULL과 문장의 마지막을 표시하는 문자는 특별한 의미를 가지지 않으며 다른 어떤 데이터와 똑같이 취급된다.

어떤 파일 입출력 함수는 한 가지 파일 모드에서만 사용될 수 있고, 다른 어떤 함수는 두 가지 모드에서 사용될 수 있다. 이 장에서는 어떤 함수를 어떤 모드에서 사용해야 하는지 알려줄 것이다.

3. 파일 이름
: 모든 디스크 파일은 이름을 가지고 있고 디스크 파일을 다룰 때에는 반드시 파일 이름을 사용해야 한다. 파일 이름은 다른 텍스트 데이터와 마찬가지로 문자열에 저장된다. 파일 이름을 위해 적용되거나 제한되는 규칙은 운영체제마다 다른다. DOS와 윈도우 3.x에서 파일 이름은 1 ~ 8자까지의 이름, 선택적으로 사용되는 마침표(.), 3자까지의 확장자로 구성된다. 반면에, 윈도우 95(98)와 NT, 대부분의 UNIX 시스템에서는 256자까지의 파일 이름을 사용할 수 있다.

운영체제마다 다른 것으로는 파일명에 허용되는 문자도 포함된다. 예를 들어, 윈도우 95에서는 다음 문자들이 허용되지 않는다.

/ \ : * ? " < > |

여러분은 사용중인 운영체제에 따라 파일명 규칙을 지키고 주의해야 한다. 또한, C 프로그램에서의 파일명은 경로 정보를 가질 수 있다. 경로(path)는 파일이 위치된 드라이브와 디렉토리(또는 폴더)를 가리킨다. 만약 경로 없이 파일명을 지정하면 프로그램은 파일이 운영체제가 현재 기본적으로 사용중인 디렉토리에 있다고 가정할 것이다. 파일명의 일부분으로 경로 정보를 항상 지정하는 것은 좋은 프로그래밍 습관이다. PC에서 백슬래시 문자(\)는 경로의 디렉토리 이름을 구분하는 데 사용된다. 예를 들어, DOS와 윈도우에서 다음은

c:\data\list.txt

드라이브 C의 디렉토리 \DATA에 저장되어 있는 LIST.TXT라는 이름의 파일을 뜻한다. 백슬래시 문자가 문자열에서 사용될 때에는 C에 대해서 특수한 의미를 가진다는 것을 알고 있을 것이다. 그래서 백슬래시 문자 자체를 표현하려면 하나의 백슬래시를 추가해야 한다. 앞에 나타난 파일 이름은 C 프로그램에서 다음과 같이 표현될 것이다.

char *filename = "c:\\daa\\list.txt;

키보드에서 파일 이름을 입력한다면 하나의 백슬래시만을 사용할 수 있다. 모든 시스템에서 백슬래시가 디렉토리 구분자로 사용되는 것은 아니다. 예를 들어, UNIX에서 는 일반적인 슬래시(/)를 사용한다.

4. 파일 열기
: 디스크 파일에 관련된 스트림을 생성하는 과정을 파일 열기(opening)라고 한다. 파일을 열게 되면 파일에서 프로그램으로 데이터를 읽어들이는 읽기(reading) 동작이나 프로그램에서 파일로 데이터를 저장하는 쓰기(writing) 동작, 또는 두 가지 모두가 가능하게 된다. 파일의 사용을 마치게 되면 닫아야 한다. 파일을 닫는 것에 대해서는 이 장에서 나중에 설명할 것이다. 파일을 열기 위해서는 라이브러리 함수 fopen()을 사용한다. fopen()의 원형은 STDIO.H에 나타나 있으며 다음과 같다.

FILE *fopen(const char *filename, const char *mode);

이 원형은 fopen()이 STDIO.H에 선언된 구조체인 FILE형에 대한 포인터를 돌려준다는 사실을 알려준다. FILE 구조체의 멤버는 프로그램에서 여러 가지 파일 관리를 수행하기 위해 사용되지만, 여기에서는 자세한 내용을 알 필요가 없다. 그러나 열기 원하는 각각의 파일에 대해서는 FILE형에 대한 포인터를 선언해야 한다. fopen()을 호출하면 함수는 FILE 구조체형 변수를 생성하고, 생성된 구조체에 대한 포인터를 돌려준다. 파일을 사용하는 이후의 모든 동작에서는 이 포인터가 사용된다. 만약 fopen() 함수에서 문제가 발생하면 NULL을 돌려준다. 예를 들어, 이런 실패는 하드웨어 에러나 또는 초기화되지 않은 디스크에서 파일을 열려고 할 때 발생할 수 있다.

인수 filename은 열리는 파일의 이름이다. 앞에서도 언급했듯이 filename에는 경로 정보가 포함될 수 있고, 포함되는 것이 좋다. 인수 filename은 큰 따옴표 내에 포함된 일반적인 문자열이나 도는 문자열 변수에 대한 포인터가 될 수 있다. 인수 mode는 열릴 파일의 사용 모드를 지정하는 것이다. mode는 이진(binary)모드나 텍스트(text) 모드, 그리고 읽기(reading)나 쓰기(writing), 또는 두 가지 모두 중에서 어떤 상태로 열려야 하는지 제어한다. mode에 사용할 수 있는 값은 <표 16.1>에 나타나 있다.

<표 16.1> fopen() 함수의 mode의 값

모드 의미
r 읽기 상태로 파일을 연다. 지정된 이름의 파일이 존재하지 않으면 fopen()은  NULL을 돌려준다.
w 쓰기 상태로 파일을 연다. 지정된 이름의 파일이 존재하지 않으면 생성된다. 지정된 이름의 파일이 존재하면 경고 없이 삭제되고 새롭고 비어 있는 파일 이 생성된다.
a 데이터 추가 상태로 파일을 연다. 지정된 이름의 파일이 존재하지 않으면  생성된다. 파일이 이미 존재한다면 새로운 데이터는 파일의 마지막에  추가된다.
r++ 읽기와 쓰기 상태로 파일을 연다. 지정된 이름의 파일이 존재하지 않으면  생성된다. 파일이 이미 존재한다면 새로운 데이터는 이전의 데이터를  덮어쓰며 파일의 시작 부분에 위치된다.
w+ 읽기와 쓰기 상태로 파일을 연다. 지정된 이름의 파일이 존재하지 않으면 생성된다. 파일이 이미 존재한다면 덮어써진다.
a+ 읽기와 데이터 추가 상태로 파일을 연다. 지정된 이름의 파일이 존재하지  않으면 생성된다. 파일이 이미 존재한다면 새로운 데이터는 파일의 마지막에  추가된다.

기본적으로 설정되어 있는 파일의 모드는 텍스트(text)이다. 파일을 이진 모드로 열기 위해서는 인수 mode에 b를 추가해야 한다. 인수 mode에 a를 포함시키면 텍스트 모드의 파일을 데이터 추가가 가능한 상태로 열어주고, ab를 포함시키면 이진 모드의 파일을 데이터 추가가 가능한 상태로 열어줄 것이다. 만약 에러가 발생하면 fopen()은 NULL을 돌려준다는 사실을 기억하자. 다음과 같은 경우에는 NULL의 복귀값을 얻게 될 것이다.

·유효하지 않은 파일 이름을 사용한 경우

·준비되지 않은 디스크에서 파일을 열려고 할 때. 예를 들어, 드라이브가 닫히지 않았거나 디스크가 초기화되어 있지 않을 때

·존재하지 않는 디렉토리나 디스크 드라이브의 파일을 열려고 할 때

·존재하지 않는 파일을 'r' 모드로 열려고 할 때

fopen()을 사용할 때에는 에러가 발생했는지 확인할 필요가 있다. 구체적으로 어떤 에러가 발생했는지 정확하게 알 수 있는 방법은 없지만, 적절한 메시지를 출력하고 다시 파일을 열도록 해주거나 프로그램을 종료할 수 있다. 대부분의 C 컴파일러는 에러의 상태에 대한 정보를 알 수 있게 해주는 ANSI 비 호환 확장 기능을 포함하고 있다.

<리스트 16.1> 다양한 모드로 디스크 파일을 여는 fopen()의 사용

/* fopen() 함수의 사용 예 */

#include

 

main()

{

FILE *fp;

char ch, filename[40], mode[4];

 

while(1)

{

 

/* 파일명과 모드 입력 */

 

printf("\nEnter a filename: ");

gets(filename);

printf("\nEnter a mode (max 3 characters): ");

gets(mode);

 

/* 파일 열기를 시도함 */

 

if((fp = fopen(filename, mode)) != NULL)

{

printf("\nSuccessful opening %s in mode %s.\n,

filename, mode);

fclose(fp);

puts("Enter x to exit, any other to continue.");

if((ch = getc(stdin)) == 'x')

break;

else

continue;

}

else

{

fprintf(stderr, "\nError opening file %s in mode %s.\n",

filename, mode);

puts("Enter x to exit, any other to try again.");

if((ch = getc(stdin) == 'x')

break;

else

continue;

}

}

}

 

-> 입력 / 출력

Enter a filename : junk.txt

Enter a mode (max 3 characters): w

Successful opening junk.txt in mode w.
Enter x to exit, any other to continue.
j

Enter a filename: morejunk.txt

Enter a mode (max 3 characters): r

Error opening morejunk.txt in mode r.
Enter x to exit, any other to try again.
x

5. 파일에 데이터 기록하고 읽어들이기
: 디스크 파일을 사용하는 프로그램에서는 파일에 데이터를 기록하거나 파일에서 데이터를 읽어들이고 또는 두 가지를 모두 수행할 수 있다. 디스크 파일에 데이터를 저장하기 위해서 는 세가지 방법을 사용할 수 있다.

·형식화된 데이터를 파일에 저장하기 위해서 형식화된 출력을 사용할 수 있다. 형식화된 출력은 텍스트 모드의 파일에서만 사용해야 한다. 형식화된 출력의 기본적인 용도는 스프레드시트나 데이터베이스 등의 다른 프로그램에서 사용되는 텍스트와 숫자 데이터를 가지는 파일을 생성하는 것이다. 또한, 드물기는 하지만 C 프로그램에서 사용할 파일을 생성하기 위해서 형식화된 출력을 사용한다.

·한 문자나 문장을 파일에 저장하기 위해서 문자 출력을 수행할 수 있다. 기술적으로 이진 모드의 파일에 문자 출력을 수행하는 것은 불가능하지만 특수한 방법으로는 가능할 수도 있다. 문자 출력은 텍스트 파일에만 제한하여 사용해야 한다. 문자 출력은 문서 작성기와 같은 프로그램뿐 아니라 C 에서도 사용할 수 있는 형식으로, 숫자가 아닌 텍스트 데이터를 저장하기 위해서 주로 사용된다.

·메모리의 일부분에 저장된 내용을 디스크 파일에 저장하기 위해서 직접 출력을 사용할 수 있다. 이 방법은 이진 파일에서만 사용된다. 직접 출력은 나중에 C 프로그램에서 사용하기 위한 데이터를 저장하는 가장 좋은 방법이다. 파일에서 데이터를 읽어들이는 경우에도, 앞에서 설명한 것과 마찬가지로 형식화된 입력, 문자 입력, 또는 직접 입력의 세 가지 방법을 사용할 수 있다. 파일을 읽어들이는 경우에 사용하는 방법은 대개 파일의 특성에 따라 다르다. 일반적으로, 여러분은 파일을 저장했을 때와 같은 방법으로 데이터를 읽어들이게 되지만 반드시 지켜야 하는 규칙은 아니다. 그러나 파일을 저장했을 때와 다른 모드로 파일을 읽어들이려면 C와 파일 형식에 대한 충분한 지식이 필요하다.

앞에서 파일 입력과 출력의 세 가지 형태를 잘 이해했다면 각각의 형태가 어떤 경우에 가장 적합한지 알 수 있을 것이다. 그러나 이런 구분은 결코 사용을 제한하기 위한 것이 아니다. C 언어의 한 가지 장점은 융통성이므로, 숙련된 프로그래머는 파일 출력의 형태를 대부분 필요성에 맞추러 사용할 수 있다. 초보적인 프로그래머는 경우에는 다음에서 설명할 내용을 참고로 해서 프로그래밍에 적용할 수 있을 것이다.

5.1 형식화된 파일 입력과 출력
: 형식화된 파일 입/출력은 특정 방법으로 형식화된 텍스트와 숫자 데이터를 다룬다. 이것을 14번째 강의에서 설명한 printf()와 scanf() 함수를 통해서 키보드 입력과 화면 출력을 형식화하는 것에 비유할 수 있다. 우선, 형식화된 출력에 대해서 설명한 후에 입력을 다룰 것이다.

▶ 형식화된 파일 출력
: 형식화된 파일 출력은 라이브러리 함수 fprintf()를 통해서 수행된다. fprintf()의 원형은 헤더 파일 STDIO.H에 정의되어 있으며, 다음과 같다.

int fprintf(FILE *fp, char *fmt, ...);

첫 번째 인수는 FILE형에 대한 포인터이다. 데이터를 특정 디스크 파일에 기록하기 위해서는 fopen()을 사용하여 파일을 열었을 때 구해지는 포인터를 전달해야 한다. 두 번째 인수는 형식화 문자열이다. 형식화 문자열에 대한 내용은 14번째 강의에서 printf()를 설명할 때 다루었다. fprintf()에서 사용되는 형식화 문자열은 printf()에서 사용되는 것과 똑같은 규칙을 따른다. 상세한 내용은 14번깨 강의를 참조하기 바람.....^^;; 마지막 인수는 말줄임표(...)이다. 이것은 무슨 뜻일까? 함수 원형에서 사용되는 말줄임표(...)는 변칙적인 개수의 인수를 뜻한다. 즉, fprintf()는 파일 포인터와 형식화 문자열을 인수로 가지며, 추가로 필요한 만큼 많은 인수를 받아들일 수 있다. 이것은 printf()와 비슷하다. 추가로 사용되는 인수는 지정된 스트림으로 출력되는 변수의 이름이다. fprintf()는 인수 목록에서 지정된 스트림으로 출력 내용을 전달한다는 것을 제외하면 printf() 와 비슷하게 동작한다는 것을 기억하기 바란다. 사실, fprintf()에서 stdout을 스트림 인수로 지정한다면 fprintf()는 printf()와 동일하다. <리스트 16.2>에 있는 프로그램은 fprintf()를 사용하고 있다.

<리스트 16.2> fprintf()가 파일과 stdout으로 동일한 내용의 형식화된 출력을 수행한다는 것을 보여주는 예

/* fprintf() 함수의 사용 예 */

#include

#include

void clear_kb(void);

 

main()

{

FILE *fp;

float data[5];

int count;

char filename[20];

 

puts("Enter 5 floating point numerical values.");

 

for(count = 0; count < 5; count++)

scanf("%f", &data[count]);

 

/* 파일명을 구하고 파일을 연다. */

/* 우선 stdin에서 나머지 문자를 지운다. */

 

clear_kb();

 

puts("Enter a name for the file.");

gets(filename);

 

if((fp = fopen(filename, "w")) == NULL)

{

fprintf(stderr, "Error opening file %s.", filename);

exit(1);

}

 

/* 숫자 데이터를 파일과 stdout으로 기록한다. */

 

for(count = 0; count < 5; count++)

{

fprintf(fp, "\ndata[%d] = %f", count, data[count]);

fprintf(stdout, "\ndata[%d] = %f", count, data[count]);

}

fclose(fp);

printf("\n");

return(0);

}

 

void clear_kb(void)

/* stdin에서 나머지 문자를 지운다. */

{

char junk[80];

gets(junk);

}

 

▶ 형식화된 파일 입력
: 형식화된 파일 입력에서는 입력 동작이 stdin 대신에 지정된 스트림을 통해서 수행된다는 것을 제외하면 scanf()와 비슷하게 동작하는 라이브러리 함수 fscanf()를 사용한다. scanf()는 14번째 강의에서 설명했다. fcsanf()의 원형은 다음과 같다.

int fscanf(FILE *fp, const char *fmt, ...);

인수 fp는 fopen()이 돌려주는 FILE형에 대한 포인터이고, fmt는 fscanf()가 입력을 받아들이 는 방법을 지정하는 형식화 문자열에 대한 포인터이다. 형식화 문자열의 구성 요소는 scanf()에서 사용되는 것과 동일하다. 마지막으로, 말줄임표(...)는 fscanf()가 입력된 값을 할당하는 변수의 주소인 하나 이상의 추가적인 인수를 뜻한다. fscanf()를 사용하기 전에 14번째 강의에서 설명한 내용을 한 번 더 읽어볼 필요가 있을 것이다. 함수 fscanf()는 stdin 대신에 지정된 스트림에서 문자를 읽어들인다는 것을 제외하면 scanf()와 똑같은 방법으로 사용된다. fscanf()를 사용해보기 위해서는 함수가 읽어들일 수 있도록 형식화된 약간의 숫자나 문자열 이 저장된 텍스트 파일이 필요하다. 에디터를 사용하여 공백(빈칸이나 문장 진행 문자)으로 구분되는 5개의 부동 소수형 숫자를 가지는 INPUT.TXT라는 이름의 파일을 생성하자. 예를 들어, 다음과 같은 파일을 생성할 수 있다.

123.45 87.001
100.02
0.00456 1.0005

이제 <리스트 16.3>에 있는 프로그램을 컴파일하고 실행하자.]

<리스트 16.3> 디스크파일에서 형식화된 데이터를 읽어들이기 위해 fscanf()를 사용하는 예제

/* fscanf()로 형식화된 파일 데이터 읽어들이기 */

#include

#include

 

main()

{

float f1, f2, f3, f4, f5;

FILE *fp;

 

if((fp = fopen("INPUT.TXT", "r")) == NULL)

{

fprintf(stderr, "Error opening file.");

exit(1);

}

 

fscanf(fp, "%f %f %f %f %f", &f1, &f2, &f3, &f4, &f5);

printf("The values are %f, %f, %f, %f, and %f.", f1, f2, f3, f4, f5);

 

fclose(fp);

return(0);

}

 

5.2 문자 입력과 출력
: 디스크 파일에서 수행되는 문자 입출력(character I/O)은 한 문자뿐 아니라 문자들로 구성되는 문장을 대상으로 한다. 문장은 문장 진행(newline) 문자로 종료되는 일련의 문자들이라는 것을 기억하자. 문자 입출력은 텍스트 모드파일에서 수행해야 한다. 문자 입출력 함수에 대한 설명과 예제 프로그램을 살펴보도록 하자.

▶ 문자 입력
: 한 문자를 읽어들이기 위한 getc(), fgetc()와 문장을 읽어들이기 위한 fgets()의 세 가지 문자 입력 함수가 있다.

▶ getc()와 fgetc() 함수
: 함수 getc()와 fgetc()는 동일하므로 원하는 함수를 사용하면 된다. 두 함수는 지정된 스트림에서 한 문자를 읽어들인다. getc()의 원형은 STDIO.H에 정의되어 있다.

int getc(FILE *fp);

인수 fp는 파일이 열릴 때 fopen()이 돌려주는 포인터이다. 함수는 입력된 문자를 돌려주거나 또는 에러가 발생하면 EOF를 돌려준다.

여러분은 키보드에서 문자를 입력하기 위해 앞의 프로그램에서 getc()를 사용했다. 이 함수는 C의 스트림의 융툥성을 확인할 수 있는 또다른 예이다. 키보드나 파일 입력을 위해 이 함수를 사용할 수 있기 때문이다. 만약 getc()와 fgetc()가 한 문자를 돌려준다면 원형에 왜 int형의 복귀값을 가지는 것일까? 파일을 읽을 때 파일의 마지막을 표시하는 문자를 읽을 필요가 있는데, 시스템에 따라서 이 문자가 char형이 아닌 int형이 될 수 있으므로 int형의 복귀값을 가지는 것이다. <리스트 16.10>에서 getc()를 사용하는 예를 볼 것이다.

▶ fgets() 함수
: 파일에서 문장을 읽어들이기 위해서는 fgets() 라이브러리 함수를 사용하자. 원형은 다음과 같다.

char *fgets(char *str, int n, FILE *fp);

인수 str은 입력 내용이 저장되는 버퍼에 대한 포인터이고, n은 입력되는 문자의 최대 개수이며, fp는 파일이 열릴 때 fopen()이 돌려주는 FILE형에 대한 포인터이다.

fgets() 함수를 호출하면 fp에서 문자를 읽어들이고 str이 지적하는 메모리 영역에 읽어들인 문자를 저장한다. 함수는 문장 진행 문자가 나타나거나 또는 n-1자를 읽어들일 때까지 계속해서 문자를 읽어들인다. n의 값을 str에 할당된 메모리의 바이트 수와 같은 값으로 설정하면 입력 내용이 할당된 영역을 벗어나서 메모리를 겹쳐쓰지 않도록 방지할 수 있다. n-1은 fgets()가 문자열의 마지막에 추가하는 널 종료 문자(\0)을 위한 공간은 제외한 값이다. 입력이 성공적으로 끝나면 fgets()는 str을 돌려준다. 또한, 다음과 같이 두 가지 경우에 NULL값을 돌려주고 에러가 발생한다.

·str에 어떤 문자를 할당하기 전에 읽기 에러가 발생하거나 EOF 문자가 나타나면 함수는 NULL을 돌려주고, str이 지적하는 메모리의 내용은 변경되지 않는다.

·str에 하나 이상의 문자를 할당한 후에 읽기 에러가 발생하거나 EOF 문자가 나타나면 함수는 NULL을 돌려주고, str이 지적하는 메모리에는 쓸모없는 데이터가 저장된다. 여기서 fgets() 함수가 반드시 한 줄의 문장을 읽어들이지는 않는다는 것을 알 수 있을 것이다. 즉, 함수는 문장 진행 문자가 나타날 때까지 계속해서 문자를 읽어들이지 않는다. 만약 문장 진행 문자가 나타나기 전에 n-1자를 읽어들였다면 fgets()는 동작을 중단한다. 프로그램은 다시 입력 동작을 수행할 때 마지막으로 입력 동작을 중단한 곳에서부터 시작한다. fgets()가 문장 진행문자에서 중단할 때까지 전체 문자열을 읽어들이기 위해서는 입력 버퍼의 크기는 물론이고 fgets()에 전달되는 n의 값을 충분한 크기로 설정하자.

▶ 문자 출력
: 문자 출력 함수에는 putc()와 fputs()가 있다.

▶ putc() 함수
: 라이브러리 함수 putc()는 지정된 스트림에 한 문자를 출력한다. 함수의 원형은 STDIO.H에 정의되어 있으며, 다음과 같다.

int putc(int ch, FILE *fp);

인수 ch는 출력되는 문자이다. 다른 문자 함수에서와 마찬가지로 이 함수에서도 int형이 사용되고 있지만 실제로는 하위 바이트만 사용된다. 인수 fp는 파일에 대한 포인터이다. 즉, 파일을 열 때 fopen()이 돌려주는 포인터이다. 함수 putc()의 동작이 성공적이었다면 출력된 문자를 돌려주고 에러가 발생한 경우에는 EOF를 돌려준다. 기호 상수 EOf는 STDIO.H에 정의되어 있으며 -1의 값을 가진다. 실제로 '어떤' 문자도 -1이라는 값을 가지고 있지 않으므로 텍스트 모드의 파일에서는 에러를 표현하는 문자로 EOF를 사용할 수 있다.

▶ fputs() 함수
: 지정된 스트림에 문장을 출력하기 위해서는 라이브러리 함수 fputs()를 사용하지. 이 함수는 14번째 강의에서 다루어진 puts()와 같은 방법으로 사용된다. 유일한 차이는 fputs()를 사용할 경우 출력 스트림을 지정할 수 있다는 것이다. 또한, fputs()는 문자열의 마지막에 문장 진행 문자를 추가하지 않는다. 원한다면 문장 진행 문자를 직접 포함시키도록 하자. STDIO.H에 정의되어 있는 함수의 원형은 다음과 같다.

char fputs(char *str, FILE *fp);

인수 str은 스트림으로 출력되고 널 문자로 종료되는 문자열에 대한 포인터이고, fp는 파일을 열 때 fopen()이 돌려주는 FILE형에 대한 포인터이다. str이 지적하는 문자열은 마지막의 \0을 제거한 상태로 파일에 기록된다. 함수 fputs()의 동작이 성공적이라면 음수가 아닌 값을 돌려주고, 에러가 발생하면 EOF를 돌려준다.

5.3 직접 파일 입력과 출력
: 현재 사용 중인 C 프로그램이나 또는 다른 어떤 C 프로그램에서 나중에 사용하기 위한 데이터를 저장할 때에는 직접 파일 입출력을 가장 많이 사용한다. 직접 입출력은 이진 모드의 파일에서만 사용된다. 직접 출력을 수행할 때에는 데이터가 블록 단위로 메모리에서 디스크 파일로 저장된다. 직접 입력의 경우에는 이와 반대로 블록 단위의 데이터를 디스크 파일에서 메모리로 읽어들인다. 예를 들어, 직접 출력 함수를 한 번 호출하여 double형의 배열 전체를 디스크에 저장할 수 있고, 직접 입력 함수를 한 번 호출하여 다시 디스크에서 메모리로 전체 배열을 읽어들일 수 있다. 직접 입출력 함수는 fread()와 fwrite()이다.

▶ fwrite() 함수
: 라이브러리 함수 fwrite()는 메모리의 데이터를 블록 단위로 이진 모드의 파일에 기록한다. STDIO.H에 정의되어 있는 함수의 원형은 다음과 같다.

int fwrite(void *buf, int size, int count, FILE *fp);

인수 buf는 파일에 기록할 데이터가 저장되어 있는 메모리 영역에 대한 포인터이다. 포인터의 형은 void이므로 어떤 데이터형에 대한 포인터가 될 수 있다. 인수 size는 개별적인 데이터 항목의 크기를 바이트 단위로 지정하는 것이고, count는 기록할 항목의 수를 지정한다. 예를 들어, 100개의 요소를 가지는 정수형 배열을 저장하기 원한다면 각각의 int형은 2바이트를 차지하므로 size는 2가 될 것이고, 배열은 100개의 요소를 가지므로 count는 100이 될 것이다. size 인수를 계산하기 위해서 sizeof() 연산자를 사용할 수 있다. 인수 fp는 물론 파일을 열 때 fopen()이 돌려주는 FILE형에 대한 포인터이다. gwrite() 함수의 동작이 성공적이면 기록한 항목의 개수를 돌려준다. 만약 함수가 돌려주는 값이 count보다 작다면 어던 에러가 발생했다는 것을 알 수 있다. 에러를 확인하기 위해서는 대개 다음과 같이 fwrite()를 사용한다.

if((fwrite(buf, size, count, fp) != count)
fprintf(stderr, "Error writing to file.");

fwrite()를 사용하는 몇 가지 예를 살펴보자. 하나의 double형 변수 X를 파일에 기록하기 위해서 다음과 같이 한다.

fwrite(&x, sizeof(double), 1, fp);

50개의 address형 구조체를 가지는 배열 data[]를 파일에 기록하기 위해서는 두 가지 방법을 사용할 수 있다.

fwrite(data, sizeof(address), 50, fp);
fwrite(data, sizeof(data), 1, fp);

첫 번째 방법에서는 address형의 크기를 가지는 50개의 요소가 배열 포함되어 있는 것으로 계산하여 배열을 저장한다. 두 번째 방법에서는 배열을 하나의 '요소'로 취급한다. 두 가지 방법의 실행 결과는 동일하다. 다음 단원에서는 fread()를 설명하고 나서 fread()와 fwrite()를 사용하는 프로그램을 분석할 것이다.

▶ fread() 함수
: fread() 라이브러리 함수는 이진 모드의 파일에서 블록 단위의 데이터를 메모리로 읽어들인 다. STDIO.H에 정의되어 있는 함수의 원형은 다음과 같다.

int fread(void *buf, int size, int count, FILE *fp);

인수 buf는 파일에서 읽어들인 데이터를 저장할 메모리 영역에 대한 포인터이다. fwrite()에서와 마찬가지로 포인터형은 void이다. 인수 size는 읽어들일 개별적인 데이터 항목의 크기를 바이트 단위로 지정하는 것이고, count는 읽어들일 항목의 개수를 지정한다. 이런 인수들이 fwrite()에서 사용되는 인수들과 같다는 것에 주목하지 바란다. 여기에서도 size 인수를 계산하기 위해서 sizeof() 연산자를 자주 사용한다. 인수 fp는 항상 그렇듯이 파일을 열 때 fopen()이 돌려주는 FILE형에 대한 포인터이다. fread() 함수는 읽어들인 항목의 개수를 돌려준다. 그러나 파일의 마지막에 도달하거나 또는 에러가 발생한다면 이 값은 count보다 작을 수 있다. <리스트 16.4>에 있는 프로그램은 fwrite()와 fread()의 사용 예를 보여준다.

<리스트 16.4> 직접 파일 입출력을 위한 fwrite()와 fread()의 사용

/* fwrite()와 fread()를 사용한 직접 파일 입출력 */

#include

#include

 

#define SIZE 20

 

main(0

{

int count, array1[SIZE], array2[SIZE];

FILE *fp;

 

/* array1[] 초기화 */

 

for(count = 0; count < SIZE; count++)

array1[SIZE] = 2 * count;

 

/* 이진 모드 파일 열기 */

 

if((fp = fopen("direct.txt", "wb")) == NULL)

{

fprintf(stderr, "Error opening file.");

exit(1);

}

/* array1[]을 파일에 저장 */

 

if(fwrite(array1, sizeof(int), SIZE, fp) != SIZE)

{

fprintf(stderr, "Error writing to file.");

exit(1);

}

 

fclose(fp);

 

/* 같은 파일을 이진 모드 읽기 상태로 연다. */

 

if((fp = fopen("direct.txt", "rb")) == NULL)

{

fprintf(stderr, "Error epening file.");

exit(1);

}

 

/* 데이터를 array2[]로 읽어들인다. */

 

if(fread(array2, sizeof(int), SIZE, fp) != SIZE)

{

fprintf(stderr, "Error reading file.");

exit(1);

}

 

fclose(fp);

 

/* 두 배열이 같다는 것을 보여주기 위해 출력 */

 

for(count = 0; count < SIZE; count++)

printf("%d\t%d\n", array1[count], array2[count]);

return(0);

}

 

6. 파일 버퍼링 : 파일 닫기와 플러시
: 파일의 사용을 마칠 때에는 fclose() 함수로 파일을 닫아야 한다. 이 장에 나타난 프로그램 에서는 이미 fclose()를 사용했었다. 함수의 원형은 다음과 같다.

int fclose(FILE *fp);

인수 fp는 스트림에 관련된 FILE형 포인터이다. fclose()의 동작이 성공적으로 수행되면 함수는 0을 돌려주고 에러가 발생하면 -1을 돌려준다. 파일을 닫을 때에는 파일 버퍼가 플러시(flush)된다. 즉, 파일에 기록된다. 또한, fcloseall() 함수를 사용하여 표준으로 정의되어 있는 stdin, stdout, stdprn, stderr, stdaux를 제외하고 열려 있는 모드 스트림을 닫을 수도 있다. 이 함수의 원형은 다음과 같다.

int fcloseall(void);

이 함수는 모든 스트림의 버퍼를 플러시하고 나서 닫힌 스트림의 개수를 돌려준다.

main()의 마지막에 도달하거나 또는 exit() 함수를 실행하여 프로그램을 마칠 때에는 모든 스트림이 자동으로 플러시되고 닫힌다. 그러나 프로그래머가 스트림을 직접 닫는 것이 좋다. 특히, 디스크 파일과 관련된 스트림에서는 파일의 사용이 끝나는 즉시 닫아야 한다. 이것은 스트림 버퍼와 관련된 문제이다.

디스크 파일에 관련된 스트림을 생성할 때에는 자동으로 버퍼가 생성되고 스트림과 연결된다 버퍼(buffer)는 파일에 기록되거나 파일에서 읽어들여지는 데이터를 임시로 저장하기 위해서 사용되는 메모리 영역이다. 버퍼는 디스크 드라이브가 블록 단위를 기본으로 하는 장치이므 로 필요하다. 이런 장치들은 데이터를 블록 단위로 읽어들이거나 기록할 때 효과적이므로 블록 단위 장치라고 한다. 이상적인 블록의 크기는 사용중인 하드웨어에 따라 다르다. 대개 수백에서 수천 바이트 사이의 크기이다. 그러나 정확한 블록의 크기에 대해서는 신경 쓸 필요가 없다.

파일 스트림과 관련된 버퍼는 문자를 기본으로 하는 스트림과 블록을 기본으로 하는 디스크 장치 간의 중간 매체 역할을 한다. 프로그램에서 데이터를 스트림으로 기록할 때 데이터는 버퍼가 가득찰 대가지 저장되었다가 블록 단위로 디스크에 기록된다. 이것은 디스크 파일에서 데이터를 읽어들이는 경우에도 적용된다. 버퍼의 생성과 사용은 모두 운영체제에 의해서 자동으로 처리된다. 프로그래머가 신경 쓸 일은 없다. C는 버퍼를 관리하기 위한 몇 가지 함수를 제공하지만 자세한 내용은 생략한다. 그래서 실제로는 이런 버퍼의 동작이 이론과 다르게 이루어진다. 프로그램이 실행되는 동안 디스크에 '저장할' 데이터가 디스크에 기록되는 것이 아니라 버퍼 내에 존재한다는 뜻이다. 만약 전원이 차단되거나 다른 어떤 문제가 발생하여 프로그램이 '중단'되면 버퍼 내의 데이터는 사라질 것이고, 디스크 파일에 저장된 것을 정확히 파악할 수 없게 된다.

파일을 닫지 않고 스트림에 관련된 버퍼를 플러시(flush)하기 위해서는 fflush()나 flushall() 라이브러리 함수를 사용한다. 파일을 계속해서 사용하는 동안 버퍼를 디스크에 기록하기 원한다면 fflush()를 사용하자. 열려 있는 모든 스트림의 버퍼를 플러시 하기 위해서는 flushall()을 사용하자. 두 함수의 원형은 다음과 같다.

int fflush(FILE *fp);
int flushall(void);

인수 fp는 파일을 열 때 fopen()이 돌려주는 FILE 포인터이다. 만약 쓰기 가능한 상태로 파일을 열었다면 fflush()는 버퍼를 디스크에 저장한다. 그러나 읽기 가능한 상태로 파일을 열었다면 버퍼는 제거된다. 함수 fflush()의 동작이 성공적으로 수행되면 0을 돌려주고, 에러가 발생하면 EOF를 돌려준다. 함수 flushall()은 열린 스트림의 개수를 돌려준다.

7. 파일의 순차적인 사용과 무작위 사용
: 열려 있는 모든 파일은 관련된 파일 위치 표시(file position indicator)를 가지고 있다. 위치 표시는 파일에서 읽기와 쓰기 동작이 수행되는 위치를 가리킨다. 위치는 항상 파일의 시작을 기준으로 해서 바이트 단위로 표현된다. 새로운 파일을 열 때 위치 표시는 항상 파일의 시작 부분인 위치 0을 가리킨다. 새로운 파일의 길이는 0이므로 다른 곳을 지적할 수 없다. 이미 존재하는 파일을 열 때 파일이 추가 가능한 상태로 열리면 위치 표시는 파일의 마지막을 지적하고, 파일이 다른 어떤 모드로 열리면 파일의 시작 부분을 지적한다.

여기서의 범위를 벗어나는 내용이기는 하지만, 이 장의 앞 부분에서 설명한 파일 입출력 함수는 위치 표시를 사용한다. 쓰기와 읽기 동작을 현재의 위치 표시에서 수행하고 나서 위치 표시를 변경하는 것이다. 예를 들어, 읽기 가능한 상태로 파일을 열고 10바이트를 읽어 들이면 위치 0부터 9가지에 해당하는 10바이트를 파일의 처음부터 읽어들이는 것이다. 읽기 동작을 수행하고 나면 위치 표시는 위치 10을 지적하게 되고 다음 읽기 동작은 위치 10부터 시작된다. 그래서 파일의 모든 데이터를 순차적으로(sequentially) 읽어들이거나 또는 파일에 데이터를 순차적으로 기록하는 경우에는 스트림 입출력 함수가 자동으로 다루어 주므로 위치 표시에 대해서 신경 쓸 필요가 없다. 그러나 파일을 다른 방법으로 사용할 필요가 있을 때에는 위치 표시의 값을 결정하거나 변경하게 해주는 C 라이브러리 함수를 사용해야 한다. 위치 표시의 값을 제어하면 파일을 무작위(random) 상태로 사용할 수 있다. 여기서 '무작위'라는 것은 앞 부분의 모든 데이터를 읽어들이거나 또는 앞 부분에 기록하지 않고 파일에서 임의의 위치에 있는 데이터를 읽어들이거나 데이터를 기록할 수 있다는 것을 뜻한다.

7.1 ftell()과 rewind() 함수
: 위치 표시가 파일의 시작 부분을 지적하도록 설정하기 위해서 라이브러리 함수 rewind()를 사용하자. STDIO.H에 정의되어 있는 함수의 원형은 다음과 같다.

void rewind(FILE *fp);

인수 fp는 스트림과 관련된 FILE 포인터이다. rewind()를 호출한 후에 파일의 위치 표시는 파일의 시작 부분인 바이트 0을 지적하게 된다. 파일에서 약간의 데이터를 일어들인 후에 파일을 닫고나서 다시 열지 않고도 파일의 시작부터 읽어들이기 원한다면 rewind()를 사용함. 파일의 위치 표시 값을 설정하기 위해서는 ftell()을 사용하자. STDIO.H에 정의되어 있는 이 함수의 원형은 다음과 같다.

long ftell(FILE *fp);

인수 fp는 파일을 열 때 fopen()이 돌려주는 FILE 포인터이다. 함수 ftell()은 파일의 시작부터 현재 파일의 위치까지를 바이트 단위로 나타내는 long형 값을 돌려준다. 첫 번째 바이트는 위치 0이다. 만약 에러가 발생하면 ftell()은 -1의 long형인 -1L을 돌려준다. rewind()와 ftell()의 동작을 이해하기 위해서 <리스트 16.5>에 있는 프로그램을 살펴보자.

<리스트 16.5> ftell()과 rewind()의 사용

/* ftell()과 rewind()의 사용 예 */

#include

#include

 

#define VUFLEN 6

 

char msg[] = "abcdefghijklmnopqrstuvwxyz";

 

main()

{

FILE *fp;

char buf[BUFLEN];

 

if((fp = fopen("TEXT.TXT", "w")) == NULL)

{

fprintf(stderr, "Error opening file.");

exit(1);

}

 

if(fputs(msg, fp) == EOF)

{

fprintf(stderr, "Error writing to file.");

exit(1);

}

 

fclose(fp);

 

/* 읽기 상태로 파일을 연다 */

 

if((fp = fopen("TEXT.TXT", "r")) == NULL)

{

fprintf(stderr, "Error opening file.");

exit(1);

}

printf("\nImmediately after opening, position = %ld", ftell(fp));

 

/* 5 문자를 읽어들인다. */

 

fgets(buf, BUFLEN, fp);

printf("\nAfter reading in %s, position = %ld", buf, ftell(fp));

 

/* 다음 5문자를 읽어들인다. */

 

fgets(buf, BUFLEN, fp);

printf("\n\nThe next 5 characters are %s, and position now = %ld",

buf, ftell(fp));

 

/* 스트림을 시작 부분으로 설정한다. */

 

rewind(fp);

 

printf("\n\nAfter rewinding, the position is back at %ld",

ftell(fp));

 

/* 5 문자를 읽어들인다. */

 

fgets(buf, BUFLEN, fp);

printf("\nand reading starts at the beginning again: %s", buf);

fclose(fp);

return(0);

}

 

7.2 fseek() 함수
: 스트림의 위치 표시(position indicator)를 더욱 정확하게 제어하기 위해서는 라이브러리 함수 fseek()를 사용할 수 있다. fseek()를 사용하면 위치 표시가 파일 내의 임의의 위치를 지적하도록 설정할 수 있다. STDIO.H에 정의되어 있는 함수 원형은 다음과 같다.

int fseek(FILE *fp, long offset, int origin);

인수 fp는 파일과 관련된 FILE 포인터이다. 위치 표시가 이동되는 거리는 offset에 바이트 단위로 지정된다. 인수 origin은 이동이 시작되는 위치를 지정한다. origin에 사용할 수 있는 기호 상수는 IO.H에 정의되어 있는데, <표 16.2>에 나타나 있듯이 세 가지 값이 있다.

<표 16.2> fseek()에서 사용되는 origin의 값

상수이름 의미
SEEK_SET 0 위치 표시를 파일의 시작부터 offset 바이트 뒤로 이동
SEEK_CUR 1 위치 표시를 현재 위치에서 offset 바이트 뒤로 이동
SEEK_END 2 위치 표시를 파일의 마지막부터 offset 바이트 앞으로 이동

함수 fseek()는 위치 표시를 성공적으로 이동시키면 0을 돌려주고, 에러가 발생하면 0이 아닌 값을 돌려준다. <리스트 16.6>에 있는 프로글매은 파일을 무작위 상태로 사용하기 위해서 fseek()를 사용하고 있다.

<리스트 16.6> fseek()를 사용하여 무작위 상태로 파일을 사용하는 예

/* fseek()를 사용한 무작위 사용 */

#include

#include

#include

 

#define MAX 50

 

main()

{

FILE *fp;

int data, count, array[MAX];

long offset;

 

/* 배열의 초기화 */

 

for(count = 0; count < MAX; count++)

array[count] = count * 10;

 

/* 쓰기 상태로 이진 파일 열기 */

 

if((fp = fopen("RANDOM.DAT", "wb")) == NULL)

{

fprintf(stderr, "\nError opening file.");

exit(1);

}

 

/* 배열을 파일에 기록하고 나서 닫는다. */

 

if((fwrite(array, sizeof(int), MAX, fp)) != MAX)

{

fprintf(stderr, "\nError writing data to file.");

exit(1);

}

 

fclose(fp);

 

/* 읽기 상태로 파일 열기 */

 

if((fp = fopen("RANDOM.DAT", "rb")) == NULL)

{

fprintf(stderr, "\nError opening file.");

exit(1);

}

 

/* 읽어들일 요소를 요구한다. */

/* 요소를 입력하면 출력해주며, -1을 입력하면 마친다. */

 

while(1)

{

printf("\nError element to read, 0-%d, -1 to quit: ", MAX - 1);

scanf("%ld", &offset);

 

if(offset < 0)

break;

else if (offset > MAX-1)

continue;

 

/* 위치 표시를 지정된 요소로 이동시킨다. */

 

if((fseek(fp, (offset*sizeof(int)), SEEK_SET)) != 0)

{

fprintf(stderr, "\nError using fseek().");

exit(1);

}

 

/* 하나의 정수를 읽어들인다. */

 

fread(&data, sizeof(int), 1, fp);

 

printf("\nElement %ld has value %d.", offset, data);

}

 

fclose(fp);

return(0);

}

 

=> 14번째 줄부터 35번째 줄까지는 <리스트 16.5>와 비슷하다. 16번째 줄과 17번째 줄은 50개의 int형 값을 가지는 data라는 배열을 초기화한다. 각 요소에 저장되는 값은 색인을 10배한 것이다. 그리고 나서 배열은 RANDOM.DAT라는 이진 파일에 저장된다. 21번째 줄에서 'wb'를 사용하여 파일을 열었으므로 이진 모드라는 것을 알 수 있다. 39번째 줄에서는 무한 루프인 while문을 실행하기 전에 파일을 읽기 가능한 이진 모드로 다시 열고 있다. while문에서는 값을 읽어들이기 원하는 배열 요소의 번호를 입력하도록 요구한다. 53번째 줄부터 56번째 줄까지는 입력된 요소가 파일 내에 포함되는 것인지 확인해본다는 것에 주의하자. 그렇다면 파일의 마지막을 벗어난 요소를 읽어들인다는 뜻일까? 실제로 그렇다. 배열의 마지막을 지난 곳에 값이 저장될 수 있는 것과 마찬가지로, C는 파일의 마지막을 지난 곳에서 값을 읽어들이게 해준다. 만약 파일의 마지막을 지난 곳이나 시작 이전에서 값을 읽어들인다면 결과는 예상할 수 없을 것이다. 이 프로그램의 53번째 줄부터 56번째 줄까지에 나타나 있는 것처럼 수행하는 동작의 결과를 항상 확인해 보는 것이 좋다. 읽어들이기 원하는 요소의 번호를 확인하고 나면 60번째 줄에서는 fseek()를 사용하여 적절한 위치로 이동한다. SEEK_SET이 사용되므로 이동은 파일의 시작을 기준으로 수행된다. 파일 내에서 이동되는 거리는 offset의 값이 아니라 offset의 값에 요소의 크기를 곱한 만큼이라는 것을 기억하자. 그리고 나서 68번째 줄에서는 값을 읽고, 70번째 줄에서는 값을 출력한다.

8. 파일의 마지막을 찾는 방법
: 파일의 길이를 정확하게 알고 있는 경우에는 파일의 마지막을 찾을 필요가 없을 것이다. 예를 들어, 100개의 요소를 가지는 정수형 배열을 저장하기 위해서 fwrite()를 사용한다면 전체적인 파일은 200바이트의 길이(2바이트 정수라고 가정할 때)가 된다는 것을 알 수 있다. 그러나 파일의 정확한 길이를 모르는 상태에서 파일의 처음부터 마지막까지를 읽어들이기 원하는 경우에는 어떻게 할 것인가? 파일의 마지막을 찾는 두 가지 방법이 있다. 텍스트 모드의 파일에서 문자 단위로 값을 읽어들일 때에는 EOF 문자를 찾을 수 있다. 기호 상수 EOF는 STDIO.H에서 -1로 정의되어 있고 '실제' 문자에서 사용되지 않는 값이다. 그래서 문자 입력 함수가 텍스트 모드의 스트림에서 EOF를 읽어들일 때 파일의 마지막에 도달했다는 것을 알 수 있다. 예를 들어, 다음과 같은 문장을 작성할 수 있을 것이다.

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

이진 모드의 스트림에서는 -1의 값을 가지는 데이터가 사용될 수도 있으므로 EOF가 파일의 마지막을 뜻하지는 않는다. -1의 값을 파일의 마지막을 뜻하지 않을 것이다. 대신에, 이진 모드에서는 라이브러리 함수 feof()를 사용할 수 있다. 이 함수는 이진 모드와 텍스트 모드의 파일에서 사용할 수 있다.

int feof(FILE *fp);

인수 fp는 파일을 열 때 fopen()이 돌려주는 FILE 포인터이다. 함수 feof()는 파일 fp의 마지막에 도달하지 않았다면 0을 돌려주고, 파일의 마짐가에 도달하면 0이 아닌 값을 돌려준다. feof() 함수를 사용했을 때 파일의 마지막에 도달했다는 것을 확인하면 rewind()를 수행하거나, fseek()를 사용하거나, 또는 파일을 닫고 다시 열 때가지 더 이상의 일기 동작이 허용되지 않는다. <리스트 16.7>에 있는 프로그램은 feof()의 사용 예를 보여준다. 프로그램이 파일의 이름을 입력하도록 요구할 때 텍스트 파일의 이름을 입력하자. 예를 들어, C 소스 파일이나 STDIO.H와 같은 헤더 파일의 이름을 입력할 수 있다. 파일이 현재 디렉토리에 있다는 것을 확인하거나 또는 파일명의 일부로 경로를 입력하기 바란다. 프로그램은 feof()가 파일의 마지막을 발견할 때까지 파일을 한 번에 한 줄씩 읽어들이고 stdout으로 출력한다.

<리스트 16.7> 파일의 마지막을 찾기 위한 feof()의 사용 예

/* 파일의 마지막 찾기 */

#include

#include

 

#define BUFSIZE 100

 

main()

{

char buf[BUFSIZE];

char filename[60];

FILE *fp;

 

puts("Error name of text file to display: ");

gets(filename);

 

/* 읽기 상태로 파일 열기 */

if((fp = fopen(filename, "r")) == NULL)

{

fprintf(stderr, "Error opening file.");

exit(1);

}

 

/* 파일의 마지막에 도달하지 않았다면 한 줄을 읽고 출력한다. */

 

while(!feof(fp))

{

fgets(buf, BUFSIZE, fp);

printf("%s", buf);

}

 

fclose(fp);

return(0);

}

 

-> 입력 / 출력

Enter name of text file to display:
hello.c
#include
main()
{
printf("Hello, world.");
return(0);
}

9. 파일 처리 함수
: 파일 처리(file management)는 디스크에 존재하는 파일을 다루는 동작을 뜻한다. 즉, 파일에서 데이터를 읽어들이거나 파이에 저장하는 것이 아니라 파일 자체를 삭제하거나 파일의 이름을 변경하고 복사하는 것을 말한다. C 표준 라이브러리에서는 파일을 삭제하고, 이름을 변경하기 위한 함수가 제공되며, 자신만의 파일 복사 함수를 작성할 수 있다.

9.1 파일 삭제하기
: 파일을 삭제하기 위해서는 라이브러리 함수 remove()를 사용한다. 함수 원형은 다음과 같이 STDIO.H에 정의되어 있다.

int remove(const char *filename);

변수 *filename은 삭제되는 파일의 이름에 대한 포인터이다. 파일의 이름에 대해서는 이 장의 앞 부분을 참고하기 바란다. 지정된 파일이 존재한다면 DOS 프롬프트에서 DEL 명령이나 UNIX에서 rm 명령을 사용한 것과 마찬가지로 삭제되고, remove() 함수는 0을 돌려준다. 파일이 존재하지 않거나 또는 읽기 전용 상태로 되어 있거나, 사용자가 적절한 이용 권한을 가지지 않거나, 다른 어떤 에러가 발생하면 remove()는 -1을 돌려준다. <리스트 16.8>에 있는 간단한 프로그램은 remove()의 사용 예를 보여준다. 주의하자. 파일을 '삭제'하면 완전히 사라지는 것이다.

<리스트 16.8> 디스크 파일을 삭제하기 위한 remove()함수의 사용

/* remove() 함수의 사용 예 */

#include

 

main()

{

char filename[80];

 

printf("Enter the filename to delete: ");

gets(filename);

 

if(remove(filename) == 0)

printf("The file %s has been deleted.", filename);

else

fprintf(stderr, "Error deleting the file %s.", filename);

return(0);

}

 

9.2 파일의 이름 변경하기
: rename() 함수는 이미 존재하는 디스크 파일의 이름을 변경한다. 함수 원형은 다음과 같이 STDIO.H에 정의되어 있다.

int rename(const char *oldname, const char *newname);

oldname과 newname이 지적하는 파일의 이름은 이 장의 앞부분에서 설명한 규칙을 따른다. 한 가지 제한이 있다면 두 이름은 동일한 디스크 드라이브를 지정해야 한다는 것이다. 서로 다른 디스크 드라이브에 존재하는 파일의 이름을 '변경'할 수는 없다. 함수 rename()의 동작이 성공적이라면 0을 돌려주고, 에러가 발생하면 -1을 돌려준다. 에러는 다음과 같은 경우에 발생할 수 있다.

·oldname이라는 파일이 존재하지 않는다.
·newname이라는 이름의 파일이 이미 존재한다.
·서로 다른 디스크에서 이름을 변경하려고 한다.

<리스트 16.9>에 있는 프로그램은 rename()의 사용 예를 보여준다.

<리스트 16.9> 디스크 파일의 이름을 변경하기 위한 rename()의 사용

/* 파일명을 변경하기 위한 rename()의 사용 예 */

#include

 

main()

{

char oldname[80], newname[80];

 

printf("Enter current filename: ");

gets(oldname);

printf("Enter new name for file: ");

gets(newname);

 

if(rename(oldname, newname) == 0)

printf("%s has been renamed %s.", oldname, newname);

else

fprintf(stderr, "An error has occurred renaming %s.", oldname);

return(0);

}

 

9.3 파일 복사하기
: 프로그램에서는 가끔 파일을 복사할 필요가 있다. 파일을 복사한다는 것은 다른 이름이나 또는 동일한 이름이지만 다른 드라이브나 디렉토리에 동일한 내용의 복사본을 만드는 것을 뜻한다. DOS에서는 COPY 명령을 수행하여 파일을 복사할 수 있고, 다른 운영체제에서는 같은 기능의 명령이 있다. C 프로그램에서는 어떻게 파일을 복사할 수 있을까? C 에서는 파일을 복사는 라이브러리 함수를 제공하지 않으므로 직접 자신만의 함수를 작성할 필요가 있다. 이것은 어렵게 생각될 수 있지만, C의 입출력용 스트림을 사용한다면 아주 간단하게 수행할 수 있다. 다음은 함수에서 수행할 필요가 있는 단계들이다.

① 원본 파일을 읽기 가능한 상태의 이진 모드로 연다. 이진 모드를 사용하는 이유는 텍스트 파일뿐 아니라 모든 종류의 파일을 복사할 수 있도록 하기 위해서이다.

② 목적 파일을 쓰기 가능한 상태의 이진 모드로 연다.

③ 원본 파일에서 문자를 읽어들인다. 파일이 처음 열릴 때 위치 표시는 파일의 사직 부분을 지적하고 있으므로 파일 포인터를 다시 위치시킬 필요는 없다는 사실을 기억하자.

④ 함수 feof()가 원본 파일의 마지막에 도달했다는 것을 알려주면 복사가 완료된 것이므로 두 개의 파일을 닫고 함수를 호출한 프로그램으로 돌아갈 수 있다.

⑤ 파일의 마지막에 도달하지 않았다면 읽어들인 문자를 목적 파일에 기록하고 나서 단계 3으로 돌아간다.

<리스트 16.10>에 있는 프로그램은 앞에서 설명한 순서대로 원본과 목적 파일의 이름을 전달받아서 복사 동작을 수행하는 함수 copy_file()을 사용하고 있다. 어떤 파일을 열 때 에러가 발생하면 이 함수는 복사를 수행하지 않고 함수를 호출했던 프로그램에 -1을 돌려줌. 복사 과정이 완료되면 프로그램은 두 개의 파일을 닫고 0을 돌려준다.

<리스트 16.10> 파일을 복사하는 함수

/* 파일 복사 */

#include

 

int file_copy(char *oldname, char *newname);

 

main()

{

char source[80], destination[80];

 

/* 원본과 목적 파일의 이름을 구한다. */

 

printf("\nEnter source file: ");

gets(source);

printf("\nEnter destination file: ");

gets(destination);

 

if(file_copy(source, destination) == 0)

puts("Copy operation successful");

else

fprintf(stderr, "Error during copy operation");

return(0);

}

int file_copy(char *oldname, char *newname)

{

FILE *fold, *fnew;

int c;

 

/* 원본 파일을 읽기 상태의 이진 모드로 연다. */

 

if((fold = fopen(oldname, "rb")) == NULL)

return -1;

 

/* 목적 파일을 쓰기 상태의 이진 모드로 연다. */

 

if((fnew = fopen(newname, "wb")) == NULL)

{

fclose(fold);

return -1;

}

 

/* 원본에서 한 번에 한 바이트를 읽는다. */

/* 만약 파일의 마지막에 도달하지 않았다면 */

/* 목적 파일에 바이트를 기록한다. */

 

while(1)

{

c = fgetc(foid);

 

if(!feof(foid))

fputc(c, fnew);

else

break;

}

 

fclose(fnew);

fclose(fold);

 

return(0);

}

 

10. 임시 파일 사용하기
: 어떤 프로그램은 실행되는 동안 하나이상의 임시 파일을 사용한다. 임시 파일(temporary file)은 프로그램에 의해서 생성되고 프로그램이 실행되는 동안 다른 목적으로 사용되다가 프로그램이 종료되기 전에 삭제되는 파일이다. 임시 파일을 생성할 때에는 나중에 삭제할 것이므로 파일의 이름에 대해서 신경 쓰지 않는다. 그러나 이미 사용중이 아닌 파일의 이름을 사용해야 한다. C 표준 라이브러리에서는 어떤 존재하는 파일과 충돌하지 않는 파일의 이름을 생성하는 함수 tmpnam()이 제공된다. STDIO.H에 정의되어 있는 함수의 원형은 다음과 같다.

char *tmpnam(char *s);

인수 s는 파일 이름을 저장하기에 충분한 버퍼에 대한 포인터가 되어야 한다. 또한, 파일의 임시 이름을 tmpnam의 내장 버퍼에 저장하는 경우에는 널(NULL) 포인터를 전달할 수 있고, 함수는 버퍼에 대한 포인터를 돌려준다. <리스트 16.11>에 있는 프로그램은 파일의 임시 이름을 생성하기 위해서 tmpnam()을 사용하는 두 가지 방법을 보여준다.

<리스트 16.11> 파일의 임시 이름을 생성하기 위한 tmpnam()의 사용

/* 임시 파일명의 사용 예 */

#include

 

main()

{

char buffer[10], *c;

 

/* 정의된 버퍼에 임시 이름을 저장한다. */

 

tmpnam(buffer);

 

/* 다른 이름을 구한다. */

/* 이번에는 함수의 내장 버퍼에 저장한다. */

 

c = tmpnam(NULL);

 

/* 이름을 출력한다. */

 

printf("Temporary name 1: %s", buffer);

printf("\nTemporary name 2: %s", c);

}

 

 

'pc관련 > C언어' 카테고리의 다른 글

CreateWindow() - 10개의 인수  (0) 2019.04.14
API 윈도우 창띄우기  (0) 2019.04.14
C언어_링크리스트  (0) 2019.04.07
C언어_포인터,고급기능  (0) 2019.04.07
고급 프로그램제어문  (0) 2019.04.03
Posted by 둥이파파^^
pc관련/C언어2019. 4. 7. 22:57

5. 링크 리스트
: 링크드 리스트(linked list)는 C에서 쉽게 구현할 수 있는 유용한 데이터 저장방법이다. 왜 포인터에 대한 주제를 다루면서 링크드 리스트를 언급하는 것일까? 잠시 후에 배울 것처럼 포인터는 링크드 리스트의 핵심이다.

단순 링크드 리스트, 이중 링크드 리스트, 이진 트리를 포함하여 많은 종류의 링크드 리스트가 있다. 각각의 형태는 데이터를 저장할 필요가 있는 특별한 경우에 적절히 사용된다. 이런 링크드 리스트에서 공통적인 사항은 데이터 항목 간의 결합이 데이터 항목 자체 내에 포함되어 있는 정보에 의해 포인터의 형식으로 정의된다는 것이다. 이런 사실은 데이터 항목 간의 결합이 배열의 배치와 저장을 기반으로 하는 배열과 링크드 리스트를 분명히 구분하는 기준이다. 이 단원에서는 가장 간단한 형태의 링크드 리스트이고 간단히 링크드 리스트라고도 하는 단순 링크드 리스트에 대해서 설명한다.

5.1 링크드 리스트의 기초
: 링크드 리스트의 각 데이터 항목은 구조체에 포함되어 있다. 구조체는 데이터를 저장하는 데 필요한 요소들을 가지고 있다. 어떤 데이터 요소를 가지는지는 프로그램에 따라 달라진다. 링크드 리스트에는 한 가지 요소가 추가되는데 바로 포인터이다. 이 포인터는 링크드 리스트에서 연결 방법을 제공한다. 다음은 간단한 예제이다.

struct person{
char name[20];
struct person *next;
};

이 코드는 person이라는 구조체를 정의한다. 데이터만을 이야기한다면 person은 단지 20개 의 요소로 구성되는 문자형 배열을 가진다. 여러분은 일반적으로 이런 간단한 데이터에 대해 링크드 리스트를 사용하지 않겠지만, 여기서는 예로 들어 설명하는 것이므로 실용성에 대해서는 생각할 필요가 없다. person 구조체는 person형의 포인터를 가진다. 다시 말해서 같은 형의 다른 구조체에 대한 포인터이다. 이것은 person형의 각 구조체가 데이터의 모음을 가질 뿐 아니라 다른 person 구조체를 지적할 수 있음을 뜻한다. <그림 15.7>은 이런 구조체가 링크드 리스트에서 서로 연결되는 방법을 보여준다.

<그림 15.7> 링크드 리스트에서의 연결 상태

<그림 15.7>에서 각 person구조체가 다음 person 구조체를 지적한다는 것에 주목하기 바란다 마지막 person구조체는 어떤 것도 지적하지 않는다. 링크드 리스트에서 마지막 요소는 NULL의 값으로 할당되는 포인터 요소에 의해 정의된다.

* 참고
: 링크드 리스트에서 하나의 요소가 되는 각 구조체를 링크드 리스트의 링크(link), 노드(node) , 요소(element)라고 한다.

여러분은 링크드 리스트에서 마지막 링크가 어떻게 구분되는지 배웠다. 첫번째 링크는 어떻게 구분될까? 첫 번째 링크는 헤드 포인터(head pointer)라는 특별한 포인터(구조체가 아닌 포인터)에 의해 구분된다. 헤드 포인터는 항상 링크드 리스트에서 첫번째 링크를 지적한다. 첫 번째 링크는 두 변째 링크에 대한 포인터를 가지고, 두 번째 링크는 세 번째에 대한 포인터를 가지고, 이런 연결 상태는 포인터가 NULL인 마지막 링크를 만날 때까지 계속된다. 만약 전체 링크드 리스트가 비어 있다면, 즉 어떤 링크도 없다면 헤드 포인터가 NULL로 설정된다. <그림 15.8>은 링크드 리스트가 시작되기 전과 링크드 리스트에 첫 번째 링크가 추가된 후의 헤드 포인터를 보여준다.

<그림 15.8> 링크드 리스트의 헤드 포인터

5.2 링크드 리스트 다루기
: 링크드 리스트를 사용할 때에는 각 링크를 추가, 삭제, 변경할 수 있다. 링크를 변경하는 것은 크게 어렵지 않다. 그러나 링크를 추가하고 삭제하는 것은 약간 복잡하다. 앞에서도 설명했듯이 링크드 리스트에서 링크들은 포인터로 연결된다. 링크를 추가하고 삭제하는 것은 대부분 이런 포인터를 다루는 일이다. 각 링크는 링크드 리스트의 시작, 중간, 마지막에 추가될 수 있다. 어디에 추가하느냐에 따라 포인터를 변경하는 방법이 달라진다. 이 장에서는 간단한 링크드 리스트 예제와 함께 더욱 복잡한 프로그램을 살펴볼 것이다. 그러나 복잡한 프로그램을 설명하기 전에 링크드 리스트를 사용할 때 필요한 동작을 하나씩 살펴보도록 하자. 이 단원에서는 앞에서 사용했던 person 구조체를 계속해서 사용할 것이다.

▶ 준비 작업
: 링크드 리스트를 사용하려면 링크드 리스트에서 사용할 데이터 구조체를 정의할 필요가 있고 헤드 포인터를 선언할 필요가 있다. 링크드 리스트는 비어있는 상태로 시작하므로 헤드 포인터는 NULL로 초기화되어야 한다. 또한, 링크를 추가하는 데 사용할 링크드 리스트의 구조체형에 대한 포인터를 추가로 선언해야 한다. 잠시 후에 설명할 것처럼 하나 이상의 포인터가 필요하다. 다음은 예제이다.

struct person{
char name[20];
struct person *next;
};
struct person *new;
struct person *head;
head = NULL;

▶ 링크드 리스트의 시작 부분에 링크 추가하기
: 만약 헤드 포인터가 NULL이면 링크드 리스트는 비어 있는 것이고 새로운 링크는 유일한 멤버가 될 것이다. 헤드 포인터가 NULL이 아니라면 링크드 리스트는 이미 하나 이상의 링크를 가지고 있는 것이다. 어떤 경우든지 링크드 리스트의 시작 부분에 새로운 링크를 추가하는 방법은 똑같다.

① malloc()을 사용하여 메모리 공간을 할당하며 구조체형 변수를 생성한다.

② 새로운 링크의 next 포인터를 헤드 포인터의 현재 값으로 설정한다. 이 값은 링크드 리스트가 비어 있다면 NULL이 될 것이고, 그렇지 않다면 현재 첫 번째 링크의 주소가 될 것이다.

③ 헤드 포인터가 새로운 요소를 지적하게 한다.

다음은 이런 작업을 수행하는 코드이다.

new = (person*)malloc(sizeof(struct person));
new -> next = head;
head = new;

malloc()은 새로운 링크를 위한 메모리를 할당하는 데 사용된다는 것에 주목하기 바란다. 각 새로운 링크를 추가할 때 단지 해당 링크에 필요한 메모리만이 할당된다. calloc() 함수를 사용할 수도 있을 것이다. 두 함수의 차이점에 주의할 필요가 있다. 주요 차이점은 calloc()이 새로운 링크를 초기화한다는 것이다. malloc()은 새로운 링크를 초기화하지 않는다.

▶ 링크드 리스트의 마지막에 링크 추가하기
: 링크드 리스트의 마지막에 추가하기 위해서는 헤드 포인터에서 시작하고 마지막 링크를 찾을 때까지 링크드 리스트를 통해 차례대로 진행할 필요가 있다. 마지막 링크를 발견하면 다음과 같은 단계를 따른다.

① malloc()을 사용하여 메모리 공간을 할당하며 구조체형 변수를 생성한다.

② 마지막 링크의 next 포인터가 새로운 링크를 지적하게 설정한다. 새로운 링크의 주소는 malloc()에 의해 복귀된다.

③ 새로운 링크가 링크드 리스트에서 마지막 항목이라는 것을 표시하기 위해 새로운 링크의 next 포인터를 NULL로 설정한다.

다음은 코드이다.

person *current;

current = head;
while(current -> next != NULL)
current = current -> next;
new = (person*)malloc(sizeof(struct person));
current -> next = new;
new -> next = NULL;

▶ 링크드 리스트의 중간에 링크 추가하기
: 힝크드 리스트를 사용하다 보면 대개는 링크드 리스트의 중간에 어디든지 링크를 추가하게 될 것이다. 정확히 새로운 링크가 위치되는 곳은 링크드 리스트를 어떻게 사용하느냐에 따라 다르다. 예를 들어, 하나 이상의 데이터 요소에 따라 링크드 리스트를 정렬한다면 링크를 추가하고 나서 정렬해야 하므로 새로운 링크의 위치가 달라진다. 다음과 같이 진행하기 바란다.

① 리스트에서 새로운 링크가 그 다음에 위치될 기존의 링크를 찾는다. 이것을 표식 요소(marker element)라고 하자.

② malloc()을 사용하여 메모리 공간을 할당하며 구조체형 변수를 생성한다.

③ 표식 요소의 next 포인터가 새로운 링크를 지적하게 한다. 새로운 링크의 주소는 malloc()에 의해 반환된다.

④ 새로운 링크의 next 포인터를 표식 요소가 지적하던 기존의 링크를 지적하게 설정한다. 다음은 예제 코드이다.

person *marker;
/* 링크드 리스트에서 원하는 위치를 지적하도록 표식을 설정하는 코드 */

new = (LINK)malloc(sizeof(PERSON));
new -> next = marker -> next;
marker -> next = new;

▶ 링크드 리스트에서 링크 삭제하기
: 링크드 리스트에서 링크를 삭제하는 것은 포인터를 다루면 될 정도로 간단한 문제이다. 정확한 과정은 링크드 리스트에서 링크의 위치에 따라 다르다.

·첫 링크를 삭제하기 위해서 헤드 포인터가 링크드 리스트에서 두 번째 링크를 지적하도록 설정한다.

·마지막 링크를 삭제하기 위해서 마지막 바로 앞의 링크의 next 포인터를 NULL로 설정한다

·다른 어떤 링크를 삭제하기 위해서 삭제되는 바로 앞 링크의 next 포인터를 삭제되는 바로 다음 링크를 지적하도록 설정한다.

다음은 링크드 리스트에서 첫 링크를 삭제하는 코드이다.

head = head -> next;

다음은 링크드 리스트에서 마지막 링크를 삭제하는 코드이다.

person *current1, *current2;
current1 = head;
current2 = current1 -> next;
while (current2 -> next != NULL)
{
current1 = current2;
current2 = current1 -> next;
}
current1 -> next = NULL;
if(head == current1)
head = null;

마지막으로, 다음 코드는 링크드 리스트에서 특정 링크를 삭제한다.

person *current1, *current2;
/* current1이 삭제되는 바로 앞 링크를 지적하도록 설정하는 코드 */
current2 = current1 -> next;
current1 -> next = current2 -> next;

어느 부분에서 링크를 삭제하든지 삭제된 링크는 여전히 메모리에 남아 있지만 링크드 리스트에서 지적하는 포인터가 없으므로 제거된다. 실제 프로그래밍에서는 "삭제된" 링크가 차지하던 메모리를 해제시켜 확보하기 원할 것이다. free() 함수를 이용하면 되는데, 자세한 내용은 나중에 "메모리 다루기"에서 설명할 것이다.

5.3 간단한 링크드 리스트 예제
: <리스트 15.12>는 링크드 리스트를 사용하는 기본적인 방법을 보여준다. 이 프로그램은 사용자 입력을 받아들이지 앟고 대부분의 기본적인 링크드 리스트 작업에 요구되는 코드를 보여주는 것 외에 특별한 동작을 수행하지 않으며 예제로만 사용되는 것이다.

이 프로그램은 다음과 같은 작업을 수행한다.

① 링크드 리스트를 위한 구조체와 포인터를 정의한다.

② 링크드 리스트에 첫 링크를 추가한다.

③ 링크드 리스트의 마지막에 링크를 추가한다.

④ 링크드 리스트의 중간에 링크를 추가한다.

⑤ 링크드 리스트의 내용을 화면에 출력한다.

<리스트 15.12> 링크드 리스트의 기초

/* 링크드 리스트의 기본적인 사용 방법을

보여주는 예제 */

 

#include

#include

#include

 

/* 링크드 리스트인 data 구조체 */

struct data {

char name[20];

struct data *next;

};

 

/* 구조체에 대한 typedef형을 정의하고 포인터를 사용해서 지적한다. */

 

typedef struct data PERSON;

typedef PERSON *LINK;

 

main()

{

/* head, new, current 요소 포인터 */

LINK head = NULL;

LINK new = NULL;

LINK current = NULL;

 

/* 첫 번째 요소를 추가한다. */

/* 이 예제에서는 링크드 리스트가 항상 비어 있지만

비어 있는 것으로 가정하지 않는다. */

 

new = (LINK)malloc(sizeof(PERSON));

new -> next = head;

head = new;

strcpy(new -> name, "Abigail");

 

/* 링크드 리스트의 마지막에 요소를 추가한다. */

/* 링크드 리스트에 최소한 하나의 요소가 있다고 가정한다. */

 

current = head;

while(current -> next != NULL)

{

current = current -> next;

}

 

new = (LINK)malloc(sizeof(PERSON));

current -> next = new;

new -> next = NULL;

strcpy(new -> name, "Catherine");

 

/* 링크드 리스트의 두 번째 위치에 새로운 요소 추가 */

new = (LINK)malloc(sizeof(PERSON));

new -> next = head -> next;

head -> next = new;

strcpy(new -> name, "Beatrice");

 

/* 모든 데이터 항목을 차례대로 출력 */

current = head;

while(current != NULL)

{

printf("\n%s", current -> name);

current = current -> next;

}

 

printf("\n");

return(0);

}

 

=> 아마도 최소한 코드의 일부분을 이해할 수 있을 것이다. 9번째 줄부터 12번째 줄까지는 링크드 리스트를 위한 데이터 구조를 선언한다. 16번째 줄과 17번째 줄은 데이터 구조체와 데이터 구조체에 대한 포인터를 위한 typedef문을 정의한다. 이런 typedef가 반드시 필요하지는 않지만 struct data 대신에 PERSON을 사용하고, struct data* 대신에 LINK를 사용하게 해주므로 코드를 단순화할 수 있을 것이다.

22번째 줄부터 24번째 줄까지는 헤드 포인터를 선언하고 링크드 리스트를 다룰 때 사용할 다른 한 쌍의 포인터를 선언한다. 모든 포인터는 NULL로 초기화된다.

30번째 줄부터 33번째 줄까지는 링크드 리스트의 시작 부분에 새로운 링크를 추가한다. 30번째 줄은 새로운 데이터 구조체를 할당한다. malloc()의 결과가 성공적이라고 가정한다는 것에 주목하기 바란다. 실제 프로그래밍에서는 이렇게 가정해서는 안된다.

31번째 줄은 이 새로운 구조체의 next 포인터가 헤드 포인터를 지적하도록 설정한다. 왜 단순히 이 포인터에 NULL을 설정하지 않을까? 링크드 리스트가 현재 비어 있기 때문이다. 이 코드는 링크드 리스트에 이미 다른 링크가 있더라도 그대로 사용할 수 있을 것이다. 새로운 첫 링크는 여러분이 원하는 대로 이전의 첫 번째 링크를 지적하게 될 것이다.

32번째 줄은 헤드 포인터가 새로운 링크를 지적하게 하고, 33번째 줄은 링크에 임의의 데이터를 저장한다.

링크드 리스트의 마지막에 새로운 링크를 추가하는 것은 약간 더 복잡하다. 예제에서는 링크드 리스트에 하나의 링크만 존재하지만 실제로 프로그램을 작성할 때에는 이런 사실을 알 수 없으므로 전체 링크드 리스트에서 마지막 링크를 찾을 때까지 반복해야 한다. next 포인터가 NULL을 지적하는 링크가 마지막 링크이다. 이런 과정은 38번째 줄부터 42번째 줄까지 수행된다. 일단 마지막 링크를 찾았다면 새로운 데이터 구조를 할당하고, 이전의 마지막 링크가 새로운 구조체를 지적하게 하고, 새로운 링크의 next 포인터를 NULL로 설정하면 된다. 이것은 44번째 줄부터 47번째 줄까지의 동작이다.

다음 작업은 링크드 리스트의 중간에 링크를 추가하는 일이다. 이 예제에서는 두 번째 위치에 링크를 추가하면 된다. 50번째 줄에서 새로운 데이터 구조체를 할당하고 나서 새로운 링크의 next 포인터가 두 번째로 사용되는 링크를 지적하도록 설정한다. 그리고 51번째 줄에서 두 번째 링크를 세 번째 링크로 만들며, 52번째 줄에서 첫 링크의 next 포인터가 새로운 링크를 지적하게 한다.

마지막으로, 프로그램은 링크드 리스트의 모든 내용을 출력한다. 헤드 포인터가 지적하는 링크에서부터 NULL 포인터로 표시되는 마지막 링크를 찾을 때까지 전체 링크드 리스트를 통해서 차례대로 진행하면 된다. 56번째 줄부터 61번째 줄까지 이런 작업을 수행하고 있다.

5.4 링크드 리스트 구현하기
: 이제 링크드 리스트에 링크를 추가하는 방법을 보았으므로 실제로 사용하는 방법을 살펴볼 필요가 있다. <리스트 15.13>은 5개의 항목을 가지는 링크드 리스트를 사용하는 다소 긴 프로그램이다. 입력된 문자들은 링크드 리스트를 사용해서 메모리에 저장된다. 이런 문자들은 단지 이름, 주소, 다른 어떤 데이터를 표현할 정도로 간단하다. 예제를 가능한 한 쉽게 만들기 위해 한 문자씩 저장하도록 하겠다.

이 링크드 리스트 프로그램을 복잡하게 만드는 원인은 문자를 입력하고 나서 링크를 정렬하기 때문이다. 물론 이 기능이 프로그램을 상당히 가치있게 만드는 특징이기도 하다. 각 링크들은 값에 다라 시작, 중간, 마지막에 추가되고 링크드 리스트 전체는 항상 정렬된다. 만약 간단히 링크를 마지막에 추가하는 프로그램을 작성했다면 전체적인 구조는 훨씬 더 간단할 것이다. 그러나 프로그램은 그리 유용하지 않을 것이다.

<리스트 15.13> 문자들의 링크드 리스트 구현하기

/*====================================================*

* 프로그램 : list1513.c *

* 도서명 : C 언어 21일 완성 *

* 목적 : 링크드 리스트 구현하기 *

*====================================================*/

#include

#include

 

#define NULL

#define NULL 0

#endif

 

/* 링크드 리스트로 사용되는 구조체 */

struct list

{

int ch; /* char형을 저장할 int형 선언 */

struct list *next_rec;

};

 

/* 구조체와 포인터에 대한 typedef형 */

typedef struct list LIST;

typedef LIST *LISTPTR;

 

/* 함수 원형 */

LISTPTR add_to_list(int, LISTPTR);

void show_list(LISTPTR);

void free_memory_list(LISTPTR);

 

int main(void)

{

LISTPTR first = NULL; /* 헤드 포인터 */

int i = 0;

int ch;

char traash[256]; /* stdin 버퍼를 정리한 */

 

while(i++ < 5) /* 주어진 5항목을 근거로 링크드 리스트를 구성 */

{

ch = 0;

printf("\nEnter character %d, ", i);

 

do

{

printf("\nMust be a to z: ");

ch = getc(stdin); /* 버퍼에서 다음 문자를 구함 */

gets(trash); /* 버퍼에서 trash를 제거 */

} while((ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z'));

 

first = add_to_list(ch, first);

}

 

show_list(first); /* 전체 링크드 리스트를 출력 */

free_memory_list(first); /* 모든 메모리를 해제 */

return(0);

}

 

/*====================================================*

* 함수 : add_to_list()

* 목적 : 링크드 리스트에 새로운 링크를 추가

* 입력값 : int ch = 저장할 문자

LISTPTR first = 원래 헤드 포인터의 주소

* 복귀값 : 헤드 포인터의 주소(first)

*====================================================*/

 

LISTPTR add_to_list(int ch, LISTPTR first)

{

LISTPTR new_rec = NULL; /* 새로운 링크의 주소를 저장 */

LISTPTR tmp_rec = NULL; /* 임시 포인터를 저장 */

LISTPTR prev_rec = NULL;

 

/* 메모리 할당 */

new_rec = (LISTPTR)malloc(sizeof(LIST));

if(!new_rec) /* 메모리 할당 불가 */

{

printf("\nunable to allocate memory!\n");

exit(1);

}

 

/* 새로운 링크의 데이터 설정 */

new_rec -> ch = ch;

new_rec -> next_rec = NULL;

 

if(first == NULL) /* 링크드 리스트에 첫 링크 추가 */

{

first = new_rec;

new_rec -> next_rec = NULL; /* 불필요하지만 안전을 보장함 */

}

else /* 첫 링크가 아니면 */

{

/* 첫 링크 앞인지 확인 */

if(new_rec -> ch < first -> ch)

{

new_rec -> next_rec = first;

first = new_rec;

}

else /* 중간이나 긑에 추가됨 */

{

tmp_rec = first -> next_rec;

prev_rec = first;

 

/* 링크가 추가되는 곳을 확인 */

 

if(tmp_rec == NULL)

{

/* 끝에 두 번째 링크를 추가 */

prev_rec -> next_rec = new_rec;

}

else

{

/* 중간에 추가하는지 확인 */

while(tmp_rec -> next_rec != NULL)

{

if(new_rec -> ch < tmp_rec -> ch)

{

new_rec -> next_rec = tmp_rec;

if(new_rec -> next_rec != prev_rec -> next_rec)

{

printf("ERROR");

gets(stdin);

exit(0);

}

prev_rec -> next_rec = new_rec;

break; /* 링크가 추가되고 while을 마침 */

}

else

{

tmp_rec = tmp_rec -> next_rec;

prev_rec = prev_rec -> next_rec;

}

}

 

/* 끝에 추가하는지 확인 */

if(tmp_rec -> next_rec == NULL)

{

if(new_rec -> ch < tmp_rec -> ch) /* 끝에서 두 번째 위치 */

{

new_rec -> next_rec = tmp_rec;

prev_rec -> next_rec = new_rec;

}

else /* 끝 위치 */

{

tmp_rec -> next_rec = new_rec;

new_rec -> next_rec = NULL; /* 중복 작업 */

}

}

}

}

}

return(first);

}

 

/*====================================================*

* 함수 : show_list

* 목적 : 링크드 리스트의 정보 출력

*====================================================*

 

void show_list(LISTPTR first)

{

LISTPTR cur_ptr;

int counter = 1;

 

printf("\n\nRec addr Position Data Next Rec addr\n");

printf("======== ======== ==== ==============\n");

 

cur_ptr = first;

while(cur_ptr != NULL)

{

printf(" %x ", cur_ptr);

printf(" %2i %c", counter++, cur_ptr -> ch);

printf(" %x \n", cur_ptr -> next_rec);

cur_ptr = cur_ptr -> next_rec;

}

}

 

/*====================================================*

* 함수 : free_memory_list

* 목적 : 링크드 리스트를 위해 정리된 모든 메모리 해제

*====================================================*

 

void free_memory_list(LISTPTR first)

{

LISTPTR cur_ptr, next_rec;

cur_ptr = first; /* 처음에서 시작 */

 

while(cur_ptr != NULL) /* 링크드 리스트의 끝까지 진행 */

{

next_rec = cur_ptr -> next_rec; /* 다음 링크의 주소 구하기 */

free(cur_ptr); /* 현재 링크 해제 */

cur_ptr = next_rec; /* 현재 링크 정리 */

}

}

 

-> 입력 / 출력

Enter character 1,
Must be a to z: q

Enter character 2,
Must be a to z: b

Enter character 3,
Must be a to z: z

Enter character 4,
Must be a to z: c

Enter character 5,
Must be a to z: a

Rec addr Position Data Next Rec addr
======== ======== ==== ==============
C3A 1 a C22
C22 2 b C32
C32 3 c C1A
C1A 4 q C2A
C2A 5 z 0

=> 분석
: 이 프로그램은 링크드 리스트에 링크를 추가하는 것을 보여준다. 예제를 이해하기는 어려울 것이다. 그러나 자세히 분석해 보면 앞에서 설명하는 링크를 추가하는 세 가지 방법을 모두 다루고 있음을 알 수 있다. 이 프로그램은 링크드 리스트의 시작, 중간, 마지막에 새로운 링크를 추가하는데 사용할 수 있다. 또한, 시작 부분에 추가되는 첫 링크와 중간에 추가되는 두 번째 링크를 새로 추가하는 경우에 대해서 중점적으로 다루고 있다. <리스트 15.13>의 시작 부분에 있는 내용들은 익숙하므로 쉽게 이해할 수 있을 것이다. 9번째 줄부터 14번째 줄까지는 NULL값이 이미 정의되어 있는지 확인해본다. 만약 그렇지 않다면 10번째 줄은 NULL값을 0으로 정의한다. 14번째 줄부터 22번째 줄까지는 링크드 리스트를 위한 구조체를 정의하고, 구조체와 포인터를 쉽게 사용할 수 있도록 하기 위해서 typedef를 사용하고 있다. main()함수는 이해하기 쉽다. first라는 헤드 포인터가 31번째 줄에서 선언된다. 이 포인터가 NULL로 초기화된다는 것에 주목하지 바란다. 여러분은 결코 포인터를 초기화하지 않은 상태로 내 버려두어서는 안된다는 것을 기억하기 바란다. 36번째 줄부터 49번째 줄까지는 사용자로부터 5문자를 읽어들이는 while 순환문이 있다. 다섯 번을 반복하는 이 외부 while 순환문 내에서 do...while은 입력된 각 문자가 영문자인지 확인하는 역할을 한다. 내부 순환문 대신에 isalpha()함수를 사용할 수도 있을 것이다. 데이터를 읽어들이고 나면 add_to_list()가 호출된다. 링크드 리스트의 시작에 대한 포인터와 링크드 리스트에 추가되는 데이터가 함수로 전달된다. main() 함수는 링크드 리스트의 데이터를 출력하기 위해 show_list()를 호출하고, 링크드 리스트에서 링크를 저장하기 위해 할당된 모든 메모리를 해제하는 free_memory_list()를 호출하면 끝난다. 이런 함수들은 비슷한 방법으로 동작한다. 각각은 헤드 포인터인 first를 사용하여 링크드 리스트의 링크드 리스트의 처음에서 시작한다. while 순환문은 특정 링크에 서 next_ptr 값을 사용하여 다음 링크로 진행한다. next_ptr이 NULL과 같을 때 링크드 리스트의 마지막에 도달한 것이므로 함수는 종료된다. 이 리스트에서 가장 중요하고 가장 복잡한 함수는 56번째 줄부터 149번째 줄까지의 add_to_list()이다. 66번째 줄부터 68번째 줄까지는 세 개의 서로 다른 링크를 지적하는 데 사용할 세 포인터를 선언한다. new_rec 포인터는 추가되는 새로운 링크를 지적할 것이다. tmp_rec 포인터는 링크드 리스트에서 기준이 되는 현재 링크를 지적할 것이다. 링크드 리스트에 하나 이상의 링크가 있다면 prev_rec 포인터는 기준이 되는 현재 링크 앞의 링크를 지적하는 데 사용된다.

71번째 줄은 추가되는 새로운 링크를 위한 메모리를 할당한다. new_rec 포인터는 malloc()에 의해 반환되는 값으로 설정된다. 메모리가 할당될 수 없다면 74번째 줄과 75번째 줄은 에러 메시지를 출력하고 프로그램을 마친다. 만약 메모리가 성공적으로 할당되었다면 프로그램은 계속된다.

79번째 줄은 구조체에 이 함수로 전달된 데이터를 저장한다. 이렇게 하기 위해서 단순히 함수로 전달된 문자 ch를 새로운 링크의 문자 필드인 new_rec -> ch로 설정하면 된다. 더 복잡한 프로그램에서는 여러 필드를 설정해야 할 것이다. 80번째 줄은 새로운 링크가 임의의 위치를 지적하지 않도록 next_rec을 NULL로 설정한다.

82번째 줄은 링크드 리스트에 어던 링크가 있는지 확인하면서 '링크 추가' 과정을 시작한다. 만약 헤드 포인터 first가 NULL로 지적되면, 즉 추가되는 링크가 링크드 리스트의 첫 번째 링크이면 헤드 포인터는 단순히 새로운 포인터로 설정되고 작업은 끝난다. 만약 새로운 링크가 처음이 아니면 함수는 87번째 줄의 else 내에서 계속 진행한다.

90번째 줄은 새로운 링크가 링크드 리스트의 시작 부분으로 이동되어야 하는지 확인해본다. 앞에서 배웠듯이 이것은 링크를 추가하는 세 가지 경우의 하나이다. 만약 링크를 처음에 위치시킨다면 92번째 줄은 새로운 링크의 next_rec 포인터가 이전의 '처음' 링크를 지적하게 한다. 그리고 나서 93번째 줄은 포인터 first가 새로운 링크를 지적하게 한다. 이렇게 해서 링크드 리스트의 시작 부분에 새로운 링크가 추가된다. 만약 새로운 링크가 비어 있는 링크드 리스트에 추가되는 첫 번째 링크가 아니고 기존 링크드 리스트의 첫 번째 위치에 추가되는 것도 아니라면 일크드 리스트의 중간이나 마지막에 위치하는 것임을 알 수 있다. 97번째 줄과 98번째 줄은 앞에서 선언한 tmp_rec와 prev_rec 포인터를 설정한다. 포인터 tmp_rec는 링크드 리스트에서 두 번째 링크의 주소로 설정되고, prev_rec는 링크드 리스트에서 첫 번째 링크로 설정된다. 링크드 리스트에 단지 하나의 링크만 있다면 tmp_rec가 NULL과 같아진다는 것에 주의하기 바란다. tmp_rec가 NULL로 설정되는 첫 링크의 next_ptr로 설정되기 때문이다.

102번째 줄은 이런 경우를 확인하고 있다. 만약 tmp_rec가 NULL이면 새로운 링크가 링크드 리스트에 추가되는 두 번째 링크라는 것을 알 수 있다. 새로운 링크가 첫 링크 앞에 위치되지 않는다는 것을 이미 알고 있으므로 마지막 링크가 되는 것이다. 이렇게 하기 위해서 단순히 prev_rec -> next_ptr을 새로운 링크로 설정하면 되고 작업은 끝난다. 만약 tmp_rec 포인터가 NULL이 아니라면 이미 링크드 리스트에 두 개 이상의 링크가 있다는 것을 알 수 있다. 110번째 줄부터 129번째 줄까지에 있는 while 순환문은 새로운 링크가 위치되어야 하는 곳을 결정하기 위해 링크의 나머지 부분을 차례대로 확인한다. 112번째 줄은 새로운 링크의 데이터 값이 현재 지적되는 링크보다 작은지 확인해본다. 만약 데이터 값이 작다면 여기에 링크를 추가하면 된다. 그러나 새로운 링크의 데이터가 현재 링크의 데이터보다 크다면 링크드 리스트의 다음 링크를 살펴볼 필요가 있다. 126번째 줄과 127번째 줄은 포인터 tmp_rec와 next_rec를 다음 링크로 설정한다. 만약 문자가 현재 링크의 문자보다 '작다면' 링크드 리스트의 중간에 링크를 추가하기 위해 앞에서 설명했던 방법을 따르면 된다. 이것은 114번째 줄부터 122번째 줄까지 나타나 있다. 114번째 줄에서 새로운 링크의 next 포인터를 현재 링크의 주소 tmp_rec와 같게 설정한다. 121번째 줄은 이전 링크의 next 포인터가 새로운 링크를 지적하도록 설정한다. 그리고 나서 작업이 끝난다. 코드는 while 순환문을 마치기 위해 break문을 사용한다.

앞에서 설명한 내용은 링크드 리스트의 중간에 추가되는 새로운 링크를 다루는 것이다. 만약 링크드 리스트의 마지막에 도달했다면 110번째 줄부터 129번째 줄까지의 while 순환문 은 링크를 추가하지 않고 끝날 것이다. 132번째 줄부터 144번째 줄까지는 링크를 마지막에 추가하는 작업을 수행한다.

만약 링크드 리스트의 마지막 링크에 도달하면 tmp_rec -> next_rec는 NULL과 같아질 것이다. 132번째 줄은 이것을 확인한다. 134번째 줄은 링크가 마지막 링크의 앞이나 뒤에 위치되어야 하는지 확인한다. 만약 새로운 링크가 마지막 링크 다음으로 가야 한다면 링크의 next_rec를 132번째 줄에서 새로운 링크로 설정하고 새로운 링크의 next 포인터를 142번째 줄에서 NULL로 설정한다.

▶ <리스트 15.13> 수정하기
: 링크드 리스트는 상당히 어려운 주제이다. 그러나 <리스트 15.13>에서 볼 수 있듯이 일정한 순서대로 데이터를 저장하는 가장 좋은 방법이기도 하다. 링크드 리스트에서 어디든지 새로운 데이터 항목을 추가하는 것이 쉬우므로 링크드 리스트로 데이터 항목의 목록을 정렬해서 사용하는 것이 배열을 사용하는 것보다 훨씬 더 간단하다. 앞의 예제는 이름, 전화번호, 다른 어떤 데이터를 정렬하도록 쉽게 수정할 수 있다. 또한, 앞의 프로그램에서는 오름차순(A에서 Z)으로 정렬했지만 내림차순(Z에서 A)으로 정렬하도록 수정하는 것도 어렵지 않다.

▶ 링크드 리스트에서 삭제하기
: 링크드 리스트에 정보를 추가하는 기능은 기본적이지만 가끔 정보를 제거하기 원할 때가 있을 것이다. 링크나 요소를 삭제하는 것은 추가하는 것과 비슷하다. 여러분은 링크드 리스트의 시작, 중간, 마지막에서 링크를 삭제할 수 있다. 각각의 경우에 적절한 포인터를 조절하면 된다. 또한, 삭제된 링크가 차지하고 있던 메모리를 해제시킬 필요가 있다.

'pc관련 > C언어' 카테고리의 다른 글

API 윈도우 창띄우기  (0) 2019.04.14
C언어_디스크파일의 사용  (0) 2019.04.11
C언어_포인터,고급기능  (0) 2019.04.07
고급 프로그램제어문  (0) 2019.04.03
C언어_변수에 범위  (0) 2019.03.28
Posted by 둥이파파^^
pc관련/C언어2019. 4. 7. 22:49

"포인터에 대해서"에서는 C 프로그래밍 언어의 가장 중요한 주제인 포인터에 대한 기본적인 내용을 다루었다. 이 장에서는 더욱 유용한 프로그램을 작성할 수 있도록 포인터에 대한 좀더 많은 내용을 설명할 것이다. 오늘은 다음과 같은 내용을 배울 것이다.

·포인터에 대한 포인터를 선언하는 방법
·다차원 배열과 포인터를 함께 사용하는 방법
·포인터의 배열을 선언하는 방법
·함수에 대한 포인터를 선언하는 방법
·데이터 저장을 위한 링크드 리스트를 생성하기 위해 포인터를 사용하는 방법

1. 포인터에 대한 포인터
: 포인터는 다른 어떤 변수의 주소값을 가지는 숫자 변수이다. 포인터는 간접 연산자(*)를 사용하여 선언할 수 있다. 예를 들어, 다음 문장은

int *ptr;

int형 변수를 지적하는 ptr이라는 이름의 포인터를 선언한다. 이런 포인터를 선언하고 나면 대응하는 형태의 어떤 변수를 지적하도록 하기 위해서 주소 연산자(&)를 사용한다. x가 int형 변수로 선언되어 있다고 가정한다면, 다음 문장은

ptr = &x;

x의 주소를 ptr에 할당하여 ptr이 x를 지적하도록 한다. 또한, 간접 연산자를 사용하면 포인터가 지적하는 변수의 값을 참조할 수 있다. 다음 문장은 모두 x에 12의 값을 저장한다.

x = 12;
*ptr = 12;

포인터 자체는 숫자 변수이므로 컴퓨터 내의 메모리에서 특정 주소에 저장된다. 그래서 포인터에 대한 포인터, 즉 포인터 변수의 값이 다른 포인터의 주소인 변수를 생성할 수도 있다. 다음은 예이다.

int x = 12; /* x는 int형 변수이다. */
int *ptr = &x; /* ptr은 x에 대한 포인터이다. */
int **ptr_to_ptr = &ptr; /* ptr_to_ptr은 int형 변수에 대한 포인터의 포인터이다. */

포인터의 포인터를 선언할 때에는 이중 간접 연산자(**)를 사용한다는 것을 기억하자. 또한, 포인터의 포인터가 지적하는 변수를 참조할 때에도 이중 간접 연산자를 사용한다. 그래서 다음의 문장은

**ptr_to_ptr = 12;

변수 x에 12의 값을 할당하고, 다음 문장은

printf("%d", **ptr_to_prt);

x의 값을 화면 상에 출력한다. 여기에서 실수로 하나의 간접 연산자를 사용하면 에러가 발생한다. 다음 문장은

*ptr_to_ptr = 12;

ptr에 12의 값을 할당하고 ptr은 이미 다른 어떤 값이 저장되어 있는 주소 12를 지적하게 된다. 이것은 분명히 잘못된 것이다.

포인터의 포인터를 선언하고 사용하는 것을 이중 간접 사용(multiple indirection)이라고 한다. <그림 15.1>에는 변수, 포인터, 포인터에 대한 포인터의 관계가 나타나 있다. 간접 사용의 단계에는 아무런 제한이 없다. 필요하다면 무한한(ad_infinitum) 단계의 포인터의 포인터를 사용할 수도 있지만, 일반적으로 2단게를 초과하는 포인터의 사용에는 특별한 장점이 없다. 오히려 복잡해짐에 따라 실수가 발생할 가능성만 높아진다.

<그림 15.1> 포인터의 포인터를 설명하는 그림

그렇다면 포인터의 포인터는 어떤 경우에 사용될까? 포인터의 포인터는 이 장의 후반부에서 설명할 포인터의 배열에서 가장 많이 사용된다.

2. 포인터와 다차원 배열
: 8번째 강의 "숫자 배열 사용하기"에서는 포인터와 배열간의 특별한 관계에 대해서 설명했었다. 특히, 대괄호를 포함하지 않는 배열의 이름이 배열의 첫 번째 요소에 대한 포인터라는 사실은 중요하다. 결과적으로, 특정 형태의 배열을 참조할 때에는 포인터식 표기 방법을 사용하는 것이 낫다. 그러나 지금까지의 예제는 일차원 배열에만 국한된 것이었다. 다차원 배려의 경우는 어떨까? 다차원 배열은 각각의 차원에 대해 대괄호를 사용하여 선언된다는 것을 기억하자. 예를 들어, 다음 문장은 8개의 int형 변수를 가지는 2차원 배열을 선언한다.

int multi[2][4];

배열은 행과 열을 가지는 것으로 생각할 수 잇다. 앞의 배열은 2행 4열로 구성된다. 그러나 다차원 배열의 구조를 표현하는 다른 한 가지 방법이 있다. C가 실제로 배열을 다루는 방법에 더 가까운 것으로 multi를 두 개의 요소를 가지는 배열로 생각할 수 있다. 각각의 요소는 4개의 정수를 가지는 배열이다. 이런 사실을 이해하기 어렵다면 배열의 선언문을 4개의 구성 요소로 나누어서 설명하고 있는 <그림 15.2>를 참고하기 바란다.

<그림 15.2> 다차원 배열 선언문의 구성 요소

각각의 구성 요소는 다음과 같은 뜻을 가진다.

1. multi라는 이름의 배열을 선언한다.
2. 배열 multi는 두 개의 요소를 가진다.
3. 각각의 요소는 다시 네 개의 요소를 가지고 있다.
4. 네 개의 요소는 int형이다.

다차원 배열의 선언문은 배열의 이름에서부터 시작하여 오른쪽으로 이동하며 대괄호 내에 포함된 내용을 처리한다. 마지막 대괄호의 내용이 처리되고 나면 배열의 기본 데이터형을 결정하기 위해 선언문의 시작 부분으로 이동하게 된다.

이 장에서는 포인터에 대한 내용을 다루므로, 이제 원래의 주제인 포인터로 사용되는 배열의 이름에 대해서 다시 살펴보자. 1차원 배열에서와 마찬가지로 다차원 배열의 이름은 배열의 첫 번째 요서에 대한 포인터이다. 앞에서 사용된 예제의 경우, multi는 int multi[2][4]를 통해서 선언된 2차원 배열의 첫번째 요소에 대한 포인터이다. multi의 첫번째 요소는 정확히 무엇일까 multi의 첫번째 요소는 int형 변수인 multi[0][0]이 아니라 multi가 배열을 가지는 배열이므로 네 개의 int형 변수를 가지는 배열 multi[0]이라는 것을 기억하자. multi[0]은 multi에 포함된 두 배열의 하나이다. 또한, multi[0]이 하나의 배열이라면 어떤 값을 지적하는가? 실제로, multi[0]은 첫 번째 요소인 multi[0][0]을 지적한다. 이런 사실에 대해서는 의문을 가질 수 있다. 대괄호를 포함하지 않는 배열의 이름이 배열의 첫번째 요소에 대한 포인터라는 것을 기억하자. multi[0]은 대괄호를 포함하고 있는 multi[0][0]의 이름이므로 하나의 포인터라고 볼 수 있다. 지금까지의 내용이 혼란스럽더라도 걱정할 필요는 없다. 사실, 포인터와 배열의 관계는 이해하기 어려운 내용이다. n차원의 배열을 사용할 때 다음과 같은 규칙을 기억한다면 유용할 것이다.

·n개의 대괄호와 적절한 색인을 포함하는 배열의 이름은 배열의 데이터를 뜻한다. 즉, 지정된 배열의 요소에 저장된 데이터를 나타내는 것이다.

·n개 이하의 대괄호를 포함하는 배열의 이름은 배열의 요소에 대한 포인터를 뜻한다. 그래서 앞의 예제에서는 multi가 포인터이고, multi[0]도 포인터이며, multi[0][0]은 배열의 데이터를 나타내는 것이다.

이제, 이런 모든 포인터가 실제로 지적하는 것이 무엇인지 살펴보도록 하자. <리스트 15.1>에 있는 프로그램은 앞에서 사용된 것과 비슷한 2차원 배열을 선언하고 관련된 포인터의 값을 출력한다. 또한 첫 번째 배열 요소의 주소를 출력한다.

<리스트 15.1> 다차원 배열과 포인터의 관계

/* 포인터와 다차원 배열의 사용 예 */

#include

 

int multi[2][4];

 

main()

{

printf("\nmulti = %u", multi);

printf("\nmulti[0] = %u", multi[0]);

printf("\n&multi[0][0] = %u", &multi[0][0]);

return(0);

}

 

=> 실제 값을 시스템에 따라 1.328이 아닐 수도 있겠지만 세 값이 동일하다는 사실은 변함 없다. 배열 multi의 주소는 배열 multi[0]의 주소와 동일하고, 이런 값은 배열 multi[0][0]에 저장되어 있는 첫 번째 정수값의 주소와 동일하다. 세 포인터가 동일한 값을 가진다면 프로그램의 측면에서 실제 차이점은 무엇일까? 9번째 강의에서는 포인터가 지적하는 것을 C 컴파일러가 '알고 있다'고 설명했다. 더욱 정확히 표현하자면, 컴파일러는 포인터가 지적하는 항목의 크기를 알고 있다.
앞에서 사용된 각 항목은 어떤 크기를 가지고 있을까? <리스트 15.2>는 이런 각 항목의 크기를 바이트 단위로 출력하기 위해서 sizeof() 연산자를 사용하고 있다.

<리스트 15.2> 각 항목의 크기 확인하기

/* 다차원 배열 요소의 크기 */

#include

 

int multi[2][4];

 

main()

{

printf("\nThe size of multi = %u", sizeof(multi));

printf("\nThe size of multi[0] = %u", sizeof(multi[0]));

printf("\nThe size of multi[0][0] = %u", sizeof(multi[0][0]));

return(0);

}

 

=> IBM의 OS/2와 같은 32비트 운영체제를 사용중이라면 결과는 32, 16, 4가 될 것이다. 이것은 OS/2와 같은 운영체제에서 int형이 4바이트이기 때문이다. 결과에 대해서 생각해보자. 배열 multi는 네 개의 정수값을 가지는 두 개의 배열을 포함하고 있다. 각각의 정수는 2바이트를 차지한다. 전체적으로는 8개의 정수가 존재하므로 16바이트 라는 크기는 정확한 값이다. 다음으로, multi[0]은 네 개의 정수를 가지는 배열이다. 각각의 정수는 2바이트를 차지하므로 multi[0]의 크기가 8바이트로 표현된 것도 정확하다. 마지막으로, multi[0][0]은 정수이므로 정수형의 크기는 당연히 2바이트이다.

이제, 이런 결과에 유의해서 9번째 강의에서 설명한 포인터 연산에 대해 생각해보도록 하자. C 컴파일러는 포인터가 지적하는 값의 크기를 '알고' 잇고 포인터 연산에서는 이런 크기를 사용한다. 포인터를 증가시키면 현재 지적하고 있는 어떤 내용의 '다음 위치에 있는 것'을 지적하기 위해서 필요한 만큼 증가된다. 즉, 포인터가 지적하고 있는 값의 크기만큼 증가된다.

이런 포인터 연산의 개념을 앞의 예제에 적용해보자. multi는 8개의 요소를 가지는 정수형 배열에 대한 포인터로 크기는 8이다. 만약 multi를 증가시키면 포인터의 값을 네 개의 요소를 가지는 정수형 배열의 크기인 8만큼 증가된다. multi가 multi[0]을 지적하고 있다면 (multi + 1)은 multi[1]을 지적할 것이다. <리스트 15.3>에 있는 프로그램은 이런 사실을 증명한다.

<리스트 15.3> 다차원 배열에서의 포인터 연산

/* 다차원 배열에 대한 포인터로 포인터 연산하기 */

#include

 

int multi[2][4];

 

main()

{

printf("\nThe value of (multi) = %u", multi);

printf("\nThe value of (mulit + 1) = %u", (multi + 1));

printf("\nThe address of multi[i] = %u", &umlti[1]);

return(0);

}

 

=> 정확한 값은 시스템에 따라 달라질 수 있지만 개념은 같을 것이다. multi를 1증가시키면 실제 값은 8(32비트 환경에서는 16) 증가되고 배열의 다음 요소인 multi[1]을 지적하게 된다. 이 예제에서는 multi가 multi[0]에 대한 포인터라는 사실을 알 수 있다. 또한, multi[0] 자체는 multi[0][0]에 대한 포인터라는 것을 알 수 있다. 그래서 multi는 포인터에 대한 포인터이다. 배열의 데이터를 참조하는 경우에 multi를 사용하기 위해서는 이중 간접 연산자를 사용해야 한다. multi[0][0]에 저장된 값을 출력하기 위해서는 다음과 같은 세 문장의 하나를 사용할 수 있을 것이다.

printf("%d", multi[0][0]);
printf("%d", *multi[0]);
printf("%d", **multi);

이런 개념은 3차원 이상의 배열에도 적용된다. 그래서 3차원 배열은 2차원 배열을 요소로 가지는 배열이라고 할 수 있다. 다시, 각각의 요소는 1차원 배열을 요소로 가진다. 여기서 설명한 다차원 배열과 포인터에 대한 내용은 다소 혼란스러울 것이다. 다차원 배열을 사용할 때에는 한 가지 사실만 기억하자. n차원의 배열은 n - 1차원의 배열을 요소로 가진다. n의 값이 1일 때 배열의 요소는 배열 선언문의 앞에서 지정한 데이터형의 변수이다. 지금까지는 포인터 상수이고 변경이 불가능한 배열의 이름을 사용하였다. 그렇다면 다차원 배열의 요소를 지적하는 포인터 변수를 어떻게 선언할 수 있을까? 2차원 배열을 선언하는 앞의 예를 다시 한 번 살펴보자.

int multi[2][4];

multi의 각 요소, 즉 네 개의 요소를 가지는 정수형 배열을 지적할 수 있는 포인터 변수를 선언하기 위해서는 다음과 같은 문장을 작성할 수 있다.

int (*ptr)[4];

그리고 나서 다음의 문장을 사용하여 ptr이 multi의 첫 번째 요소를 지적하도록 할 수 있다.

ptr = multi;

포인터 선언에서 괄호를 사용한 이유를 이해할 수 있는가? 대괄호([])는 포인터 연산자(*)보다 우선 순위를 가진다. 만약 다음과 같은 문장을 작성했다면

int *ptr[4];

int형에 대한 네 개의 포인터의 배열을 선언할 것이다. 사실, 포인터의 배열을 선언하고 사용할 수는 있다. 그러나 여기서 필요한 것은 포인터의 배열이 아니다. 다차원 배열의 요소에 대한 포인터를 어떻게 사용할 수 있을까? 일차원 배열에서와 마찬가지로 포인터는 함수에 배열을 전달하는 경우에 사용된다. 이런 사실은 함수에 다차원 배열을 전달하는 두 가지 방법을 사용하고 잇는 <리스트 15.4>에서 설명된다.

<리스트 15.4> 포인터를 사용하여 다차원 배열을 함수에 전달하기

/* 다차원 배열에 대한 포인터를 함수에 전달하는 예 */

#include

 

void printarray_1(int (*ptr)[4]);

void printarray_2(int (*ptr)[4], int n);

 

main()
{

int multi[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};

 

/* ptr은 4 정수 배열에 대한 포인터이다. */

 

int (*ptr)[4], count;

 

/* ptr이 multi의 첫 요소를 지적하게 한다. */

 

ptr = multi;

 

/* 순환문에서 ptr은 multi의 다음요소, 즉 다음 4요소 정수배열을 지적하도록 증가된다. */

 

for(count = 0; count < 3; count++)

printarray_1(ptr++);

 

puts("\n\nPress Enter...");

getchar();

printarray_2(multk, 3);

printf("\n");

return(0);

}

 

void printarray_1(int (*ptr)[4])

{

/* 4 요소 정수 배열의 요소들을 출력한다. */

/* p는 INT형에 대한 포인터이다. */

/* P를 PTR의 주소와 같게 하기 위해 형 변환을 사용해야 한다. */

 

int *p, count;

p = (int *)ptr;

 

for(count = 0; count < 4; count++)

printf("\n%d", *p++);

}

 

void printarray_2(int (*ptr)[4], int n)

{

/* n번째 4 요소 정수 배열의 요소들을 출력한다. */

 

int *p, count;

p = (int *)ptr;

 

for(count = 0; count < (4 * n); count++)

printf("\n%d", *p++);

}

 

3. 포인터의 배열
: 8번째 강의 "숫자 배열 사용하기"에서는 배열이 동일한 데이터형을 사용하고 같은 이름으로 사용되는 집단적인 데이터 저장 영역이라고 설명했다. 포인터는 C에서 제공되는 한 가지 데이터형이므로 포인터의 배열을 선언하고 사용할 수 있다. 포인터의 배열은 특별한 상황에서 매우 유용할 수 있다. 아마도 포인터의 배열을 가장 많이 사용하는 경우는 문자열을 처리할 때일 것이다. 10번째 강의 "문자와 문자열"에서 배웠듯이 문자열은 메모리에 저장되는 일련의 문자들을 말한다. 문자열의 시작 부분은 첫 번째 문자에 대한 포인터, 즉 char형에 대한 포인터로 지적된다. 문자열의 마지막은 널 문자로 구분된다.. char형에 대한 포인터의 배열을 선언하고 초기화하면 포인터의 배열을 통해서 방대한 양의 문자열을 사용하거나 처리할 수 있다. 배열의 각 요소는 서로 다른 문자열을 가리키므로 배열을 통해서 순환하면서 차례대로 배열 요소를 이용할 수 있는 것이다.

3.1 문자열과 포인터에 대한 복습
: 문자열 할당과 초기화를 되새기면서 열 번째 강의에서 설명했던 내용을 다시 한번 살펴보도록 하자. 문자열을 할당하고 초기화하는 한 가지 방법은 다음과 같이 char형 배열을 선언하는 것이다.

char message[] = "This is the message.";

char형에 대한 포인터를 선언하여 문자열을 할당하고 초기화할 수도 있을 것이다.

char *message = "This is the message.");

두 문장은 동이한 것이다. 어떤 문장을 사용하든지 컴파일러는 널 문자를 포함하는 문자열을 저장하기 위한 영역을 할당하며, 수식 message는 문자열의 시작 부분에 대한 포인터가 된다. 다음 두 문장은 어떤 뜻을 가지고 있는가?

char message1[20];
char *message2;

첫 번째 문장은 20자 길이의 char형 배열을 선언하고, message1은 배열의 첫 번째 요소에 대한 포인터이다. 배열의 저장 영역은 할당되었지만 배열이 초기화되지는 않았고 배열의 내용은 정해지지 않았다. 두 번째 문장은 char형에 대한 포인터 message2를 선언한다. 이 경우, 문자열을 저장하기 위한 영역이 할당되지 않고 단지 포인터를 저장하기 위한 공간만이 할당되어 있다. 만약 문자열을 생성하여 message2가 문자열을 지적하기 원한다면, 우선 문자열을 저장하기 위한 영역을 할당해야 한다. 열 번째 강의에서 이런 용도로 메모리 할당 함수 malloc()을 사용하는 방법을 배웠다. 모든 문자열을 저장하기 위해서는 컴파일 과정에서 미리 준비하거나 도는 프로그램 실행 과정에서 malloc()을 사용하여 필요한 공간을 할당해야 한다는 것을 기억하자.

3.2 char에 대한 포인터의 배열
: 잠시동안 앞에서 배웠던 것을 복습했으므로 포인터의 배열을 선언해보자. 다음 문장은 char형에 대한 10개의 포인터를 가지는 포인터의 배열을 선언한다.

char *message[10];

배열 message[]의 요소는 각각 char형에 대한 포인터이다. 짐작할 수 있듯이 배열을 선언할 대에는 문자열을 저장하기 위한 저장 영역을 할당하는 동시에 문자열을 초기화할 수 있다.

char *message[10] = {"one, "two", "three"};

이 문장은 다음과 같은 동작을 수행한다.

·10개의 요소를 가지는 message라는 이름의 배열을 선언한다. 배열의 요소는 각각 char형에 대한 포인터이다.

·메모리 내에서 저장 영역을 할당하여 널 문자를 포함하는 초기화 문자열을 저장한다. 메모리의 정확한 위치는 신경 쓸 필요가 없다.

·message[0]은 첫 번째 문자열의 첫 번째 문자를 지적하고, message[1]은 두 번째 문자열의 첫 번째 문자를 지적하며, message[3]은 세 번째 문자열의 첫 번째 문자를 지적하도록 초기화된다.

이제 포인터의 배열을 사용하는 예제를 살펴보자.

<리스트 15.5> char형에 대한 포인터의 배열을 초기화하고 사용하는 프로그램

/* char형에 대한 포인터의 배열 초기화 */

#include

 

main()

{

char *message[8] = { "Four", "score", "and", "seven", "years", "ago,", "our", "forefathers"};

 

int count;

 

for(count = 0; count < 8; count++)

printf("%s ", message[count]);

printf"\n");

return(0);

}

 

=> <리스트 15.5>에 있는 프로그램은 char형에 대한 8개의 포인터를 가지는 배열을 선언하고, 7번째 줄과 8번째 줄에서는 배열 요소인 포인터가 8개의 문자열을 지적하도록 초기화하고 있다. 그리고 나서 11번째 줄과 12번째 줄은 배열의 각 요소를 화면 상에 출력하기 위해서 for문을 사용한다.

여기에서는 포인터의 배열을 다루는 것이 문자열 자체를 다루는 것보다 쉽다는 사실을 알 수 있을 것이다. 이런 장점은 이 장에서 나중에 설명할 복잡한 프로그램에서 더욱 분명하게 드러난다. 또한, 나중에 설명할 것처럼 이런 장점은 함수를 사용할 때 가장 강력하다. 함수에서 문자열을 출력하기 위해서 여러 개의 문자열을 전달하는 것보다는 포인터의 배열을 전달하는 것이 더욱 쉽다. 문자열을 출력하기 위해 함수를 사용하도록 <리스트 15.5>에 있는 프로그램을 변경하여 포인터의 배열을 전달하는 것이 더 쉽다는 사실을 확인할 수 있다. <리스트 15.6>에는 변경된 프로그램이 나타나 있다.

<리스트 15.6> 포인터의 배열을 함수에 전달하기

/* 함수에 포인터의 배열 전달하기 */

#include

 

void print_strings(char *p[], int n);

 

main()

{

char *message[8] = { "Four", "score", "and", "seven", "years", "ago,", "our", "forefathers"};

 

printf_strings(message, 8);

return(0);

}

 

void print_strings(char *p[], int n)

{

int count;

 

for(count = 0; count < n; count++)

printf("%s", p[count]);

printf("\n");

}

 

앞에서 포인터의 포인터를 설명할 때 나중에 예제를 설명할 것이라고 언급했던 것을 기억하는가? 앞의 예제가 바로 포인터의 포인터를 사용하는 예제이다. <리스트 15.6> 에서는 포인터의 배열을 선언했다. 배열의 이름은 첫 번째 요소에 대한 포인터이다. 함수에 배열을 전달할 때에는 포인터(배열의 첫번째 요소)에 대한 포인터 (배열 이름)를 전달하는 것이다.

3.3 예제
: 이제 좀더 복잡한 예제를 살펴보자. <리스트 15.7>에 있는 프로그램은 포인터의 배열을 포함하여 지금까지 설명한 여러 가지 내용을 활용하고 있다. 프로그램은 문자열이 입력될 때 문자열을 저장하기 위한 영역을 할당하고, char형에 대한 포인터의 배열을 사용하여 저장 영역을 관리하며, 여러 줄의 입력을 키보드에서 받아들인다. 빈 줄을 입력하여 마지막이라는 것을 알려주면 프로그램은 입력된 내용을 알파벳 순으로 정렬하고 화면 상에 출력한다. 만약 이 프로그램을 처음부터 작성한다면 구조화 프로그래밍 방식으로 작성할 것이다. 우선, 프로그램이 수행해야 하는 동작을 목록으로 만들어보자.

① 빈 줄이 입력될 때까지 한 번에 한 줄씩 키보드에서 입력을 받아들인다.

② 입력된 내용을 알파벳 순으로 정렬한다.

③ 정렬된 내용을 화면 상에 출력한다.

이 목록에서 알 수 있듯이 프로그램은 적어도 세 개의 함수를 사용해야 한다. 하나는 입력을 받아들이고, 다른 하나는 입력된 내용을 정렬하며, 세 번째 함수는 입력된 내용을 출력한다. 이제, 독립적으로 각각의 함수를 작성할 수 있을 것이다. get_lines()라는 입력 함수는 무엇을 수행할 필요가 있을까? 다시, 목록을 만들어 보자.

① 입력된 문장의 수를 일단 모든 내용이 입력되면 함수를 호출한 프로그램으로 문장의 수를 돌려준다.

② 미리 설정되어 있는 문장의 수보다 많은 내용은 받아들이지 않는다.

③ 각각의 문장을 저장하기 위한 영역을 할당한다.

④ 문자열에 대한 포인터를 배열에 저장하여 모든 내용을 관리한다.

⑤ 빈 줄이 입력되면 원래의 프로그램으로 돌아간다.

이제, 알파벳 순으로 문장을 정렬하는 두 번째 함수에 대해서 생각해보자. 이 함수를 sort() 라고 하자. 여기서 사용되는 정렬 방식은 인접한 두 개의 문자열을 비교해서 두 번째 문자열이 첫 번째 문자열보다 작으면 서로 교환하는 아주 간단한 방법이다. 더욱 정확히 말하자면, 함수는 포인터의 배열 내에서 인접한 두 개의 문자열을 비교하여 필요하다면 포인터를 교환한다.

문자열을 완전히 정렬하기 위해서는 배열의 처음부터 마지막까지 필요할 때마다 각 쌍의 문자열을 비교해야 한다. n개의 요소를 가지는 배열에서는 배열을 n - 1번 통과하며 비교해야 한다. 왜 n - 1번이나 배열을 통과해야 하는 것일까? 배열을 한 번 통과할 때 각각의 요소는 기껏해야 한 칸씩 이동될 수 있다. 예를 들면, 맨 앞에 위치되어야 하는 문자열이 실제로 끝에 위치되어 있다면 처음에는 배열을 통과하며 문자열을 맨 끝에서 앞으로 한 번 이동시키고, 다음에는 다시 한 번 앞으로 이동시키며, 계속해서 이런 과정을 반복해야 한다. 맨 끝에 위치되어 있는 문자열을 배열의 맨 앞으로 옮기려면 n - 1번 이동시켜야 한다. 이것은 아주 비효율적이고 좋지 못한 정렬 방식이라는 것을 기억하자. 그러나 예제 프로그램에서는 간단한 데이터를 정렬할 것이므로 이 방법이 사용하기 쉽고 이해하기 쉬우며 가장 적절하다. 마지막 함수는 화면 상에 정렬된 문자열을 출력한다. 사실, 이 함수는 이미 <리스트 15.6>에서 작성한 것을 약간 변경한 것이므로 <리스트 15.7>에서 사용하려면 약간 수정하면 된다.

<리스트 15.7> 키보드에서 여러 줄의 텍스트를 읽어들이고 알파벳순으로 정렬하여 출력하는 프로그램

/* 키보드에서 문자열을 읽어들이고 정렬하여 화면에 출력한다. */

#include

#include

#include

 

#define MAXLINES 25

 

int get_lines(char *lines[]);

void sort(char *p[], int n);

void print_strings(char *p[], int n);

 

char *lines[MAXLINES];

 

main()

{

int number_of_lines;

 

/* 키보드에서 여러 줄을 읽어들인다. */

 

number_of_lines = get_lines(lines);

 

if(number_of_lines < 0)

{

puts("Memory allocation error");

exit(-1);

}

 

sort(lines, number_of_lines);

print_strings(lines, number_of_lines);

return(0);

}

 

int get_lines9char *lines[])

{

int n = 0;

char buffer[80]; /* 각 줄에 대한 임시 저장 공간 */

 

puts("Enter one line at time; enter a blank when done.");

 

while((n < MAXLINES) && (gets(buffer) != 0) &&

(buffer[0] != '\0'))

{

if((lines[n] = (char *)malloc(strlen(buffer) + 1)) == NULL)

return -1;

strcpy(lines[n++], buffer);

}

return n;

 

} /* get_lines()의 끝 */

 

void sort9char *p[], int n)

{

int a, b;

char *x;

 

for(a = 1; a < n; a++)

{

for(b = 0; b < n-1; b++)

{

if(strcmp(p[b], p[b+1]) > 0)

{

x = p[b];

p[b] = p[b+1];

p[b+1] = x;

}

}

}

}

 

void print_strings(char *p[], int n)

{

int count;

 

for(count = 0; count < n; count++)

printf("\n%s ", p[count]);

}

 

=> 이 프로그램을 상세히 살펴보면 도움이 될 것이다. 프로그램에서는 문자열을 다루기 위해서 여러 가지 새로운 라이브러리 함수를 사용하고 있다. 여기에서는 이런 함수들을 간단히 설명하고, 상세한 내용은 낭중에 "문자열 다루기"에서 설명하도록 하겠다. 이런 함수들을 사용하는 프로그램에는 헤더 파일 STRING.H가 포함되어야 한다.

get_lines() 함수에서 41번째 줄과 42번째 줄의 다음과 같은 while문은 입력을 제어한다. 한 줄로 살펴보자.

while((n < MAXLINES) && (gets(buffer) != 0) && (vuffer[0] != '\0'))

while문에서는 세 가지 조건이 확인되고 있다. 첫 번째 조건인 n < MAXLINES는 지정된 수의 문장이 입력되지 않았는지 확인한다. 두 번째 조건인 get(buffer) != 0은 키보드에서 버퍼로 문장을 읽어들이기 위해서 라이브러리 함수 gets()를 호출하여 파일의 마지막에 도달하거나 또는 다른 어떤 에러가 발생하지 않았는지 확인한다. 세 번째 조건인 buffer[0] != '\0'는 방금 입력된 문장의 첫 문자가 널 문자가 아닌지 확인한다. 널 문자는 빈 줄이 입력되었다는 것을 알려주는 역할을 한다.

세가지 조건의 어떤 것도 만족되지 않으면, while문은 종료되고 함수는 지금까지 입력된 문장의 수를 프로그램으로 돌려주며 제어를 전달한다. 만약 세가지 조건이 모두 참이라면 44번째 줄에 있는 다음과 같은 if문이 실행된다.

if((lines[n] = (char *)malloc9strlin(buffer) + 1)) == NULL)

이 문장은 방금 입력된 문자열의 저장 영역을 할당하기 위해서 malloc()를 호출한다. strlen() 함수는 인수로 전달된 문자열의 길이를 돌려준다. 복귀값에는 malloc() 함수가 문자열과 함께 널 문자를 저장할 수 있는 영역을 할당하도록 하기 위해서 1이 더해진다. 앞에서 설명했던 malloc() 함수는 포인터를 돌려준다. 이 문장은 malloc()이 돌려주는 포인터의 값을 포인터의 배열에서 대응하는 요소에 할당한다. 만약 malloc()이 NULL을 돌려주면, if문에서는 함수를 호출한 프로그램으로 -1의 복귀값을 돌려준다. main()에서는 get_lines()의 복귀값을 확인하여 함수가 0보다 작은 값을 돌려주는지 확인한다. 23번째 줄부터 27번째 줄까지는 메모리 할당에러를 알려주고 프로그램을 종료한다. 메모리의 할당이 성공적이었다면 프로그램의 46번째 줄에서는 임시 저장 영역의 버퍼에서 방금 malloc()에 의해 할당된 저장 영역으로 문자열을 복사하기 위해 strcpy()함수를 사용한다. 그리고 나서 while문은 다른 문장을 읽어들이기 위해 전체 과정을 반복한다. 메모리 할당에러가 발생하지 않았다고 가정하고, 일단 get_lines()에서 main()으로 제어가 전달되면 다음과 같은 동작이 수행된 것이다.

·키보드에서 여러 줄의 텍스트가 입력되고 널 문자를 포함하는 문자열 형태로 메모리에 저장되었다.

·배열 lines[]는 각 문자열에 대한 포인터를 가진다. 배열 내의 포인터의 순서는 문자열이 입력된 순서이다.

·변수 number_of_lines는 입력된 문장의 수를 가진다.

이제, 문자열의 정렬 부분을 살펴보자. 프로그램에서는 실제로 문자열을 이동시키는 것이 아니라 단지 배열 lines[]에 포함되어 있는 포인터의 순서만을 이동시킨다는 것을 기억하자. 함수 sort()의 내용을 살펴보자. 57번째 줄부터 68번째 줄까지는 for문 내에 종속되어 있는 다른 하나의 for문이 사용되고 잇다. 바깥쪽의 순환문은 number_of_lines - 1번 실행된다. 바깥쪽의 순환문이 실행될 때마다 안쪽의 순환문은 n = 0에서부터 n = number_of_lines-1이 될 때까지 포인터의 배열에서 (string n)을 (string n + 1)과 비교한다. 실제 비교 동작은 두 개의 문자열에 대한 포인터를 바다아들이는 61번째 줄의 라이브러리 함수 strcmp()에 의해서 수행된다. 함수 strcmp()는 다음 중에서 하나의 값을 돌려준다.

·첫 번째 문자열이 두 번째 문자열보다 크다면 -> 0보다 큰 값

·두 문자열이 동일하다면 -> 0

·두 번째 문자열이 첫 번째 문자열보다 크다면 -> 0보다 작은 값 프로그램에서 strcmp()가 0보다 큰 값을 돌려준다면 첫 번째 문자열이 두 번째 문자열보다 '크다'는 것을 뜻하므로 문자열을 서로 교환해야 한다. 즉, lines[]내의 포인터를 교환해야 한다

이런 교환 동작은 임시 변수인 x를 사용하여 수행된다. 63번째 줄부터 65번째 줄까지는 실제 교환을 수행한다.

sort()에서 프로그램으로 제어가 전달될 때 lines[]내의 포인터는 순서대로 정렬된 상태가 된다. '가장 작은' 문자열에 대한 포인터는 lines[0]에 저장되고, 다음으로 '작은' 문자열에 대한 포인터는 lines[1]에 저장된다. 마지막으로, 프로그램은 화면 상에 정렬된 문자열을 출력하기 위해서 함수 print_strings()를 호출한다. 이 함수는 앞의 예제와 비슷하다. <리스트 15.7>에 있는 프로그램은 지금까지 여기서 사용한 것들 중에서 가장 복잡한 예제이다 이 프로그램은 지금까지 설명한 C 프로그래밍에 대한 많은 내용을 다루고 있다. 코드 분석을 참고로 해서 이 프로그램의 동작과 각 단계별 내용을 이해하기 바란다. 만약 이해할 수 없는 부분이 있다면, 다음 주제로 진행하기 전에 프로그램을 완전히 이해할 할 수 있을 때까지 여기서 관련된 내용을 다시 한 번 읽어보도록 하자.

4. 함수에 대한 포인터
: 함수에 대한 포인터는 함수를 호출하는 또다른 방법을 제공해준다. 여기서 아마도 '함수에 대한 포인터를 어떻게 구할 수 있지? 포인터는 변수가 저장된 주소값을 가지는 것이 아닌가?'라는 의문을 가질 것이다. 이 질문에 대한 해답은 양면성을 가지고 있다. 포인터가 주소값을 가진다는 것을 사실이지만 반드시 변수가 저장된 주소값일 필요는 없다. 프로그램이 실행될 때 각 함수의 코드는 특정 주소에서부터 시작하는 메모리 영역에 위치된다. 함수에 대한 포인터는 이렇게 메모리 영역에 저장된 함수의 시작 주소값을 가진다. 그렇다면 함수에 대한 포인터를 사용하는 이유는 무엇일까? 앞에서도 언급했듯이, 함수를 호출하는 방법에 많은 융통성을 제공해준다. 함수에 대한 포인터는 프로그램이 여러 가지 함수들 중에서 현재 상황에 적합한 것을 '선택하여' 실행하게 해준다.

4.1 함수에 대한 포인터 선언
: 다른 일반적인 포인터와 마찬가지로 함수에 대한 포인터를 사용하려면 먼저 선언해야 한다. 포인터는 다음의 형식을 사용한다.

type (*ptr_to_func)(parameter_list);

이 문장은 type형을 돌려주고 parameter_list에 포함되어 있는 매개 변수를 받아들이는 함수에 대한 포인터 ptr_to_func를 선언한다. 다음은 몇 가지 구체적인 예이다.

int (*func1)(int x);
void (*func2)(double y, double z);
char (*func3)(char *p[]);
void (*func4)();

첫 번째 문장은 하나의 int형 인수를 받아들이고 int형 값을 돌려주는 함수에 대한 포인터 func1을 선언한다. 두 번째 문장은 두 개의 double형 인수를 받아들이고 void의 복귀형을 가지는, 즉 복귀값이 없는 함수에 대한 포인터 func2를 선언한다. 세 번째 문장은 chart형에 대한 포인터의 배열을 인수로 받아들이고 char형의 값을 돌려주는 함수에 대한 포인터 func3을 선언한다. 마지막 문장은 아무런 인수도 받아들이지 않고 void의 복귀형을 가지는 함수에 대한 포인터 func4를 선언한다. 포인터의 이름 주위에 괄호를 사용할 필요가 있을까? 첫 번째 예제의 경우 다음과 같은 문장을 사용할 수 없는가?

int *func1(int x);

이 것은 간접 연산자인 *의 우선 순위에 관련된 문제이다. 간접 연산자는 매개 변수의 목록을 둘러싸고 있는 괄호보다 상대적으로 낮은 우선 순위를 가진다. 첫 번째 예제에서 괄호를 생략한 선언문은 func1을 int형에 대한 포인터를 돌려주는 함수로 선언한다. 포인터를 돌려주는 함수는 8번째 강의 "함수를 효율적으로 사용하는 방법"에서 다루어진다. 함수에 대한 포인터를 선언할 때에는 항상 포인터의 이름과 간접 연산자 주위에 괄호를 사용해야 한다는 사실을 기억하자. 그렇지 않으면 분명히 문제가 발생할 것이다.

4.2 함수에 대한 포인터의 초기화와 사용
: 함수에 대한 포인터를 사용하려면 포인터를 선언해야 할 뿐 아니라 어떤 것을 지적하도록 초기화해야 한다. 물론 여기서 '어떤 것'은 함수이다. 포인터가 지적해야 하는 함수에는 아무런 제한이 없다. 한가지 주의해야 할 사항이 있다면 함수의 복귀형과 매개 변수의 목록이 포인터를 선언할 때 지정된 복귀형이나 매개 변수의 목록과 일치해야 한다는 것이다. 예를 들어, 다음 문장은 함수와 함수에 대한 포인터를 선언하고 정의한다.

float square(float x); /* 함수 원형 */
float (*p)(float x); /* 포인터 선언 */
float square(float x) /* 함수 정의 부분 */
{
return x * x;
}

함수 square()와 포인터 p는 동일한 매개 변수와 복귀 형태를 사용하므로 다음과 같이 p가 square를 지적하도록 초기화할 수 있다.

p = square;

이제, 다음과 같이 포인터를 사용하여 함수를 호출할 수 있다.

answer = p(x);

함수에 대한 포인터는 이처럼 간단한 것이다. 실제 사용 예는 <리스트 15.8>에 있는 프로그램 을 컴파일하고 실행하여 살펴보도록 하자. 이 프로그램은 함수에 대한 포인터를 선언하고 초기화한 후에, 처음에는 함수의 이름을 사용하고 그 다음에는 포인터를 사용하여 함수를 두 번 호출한다. 함수를 어떤 방법으로 호출하든지 결과는 같다.

<리스트 15.8> 함수를 호출하기 위해서 함수에 대한 포인터를 사용하는 프로그램

/* 함수에 대한 포인터 선언과 사용 예 */

#include

 

/* 함수 원형 */

 

double square(double x);

 

/* 포인터 선언 */

 

double (*p)(duble x);

 

main()

{

/* p가 square()를 지적하도록 초기화 */

 

p = square;

 

/* square()를 두 가지 방법으로 호출 */

printf("%f %f", square(6.6), p(6.6));

return(0);

}

 

double square(double x)

{

return x * x;

}

 

=> 7번째 줄에서는 square()를 선언한고 ,11번째 줄에서는 double형 인수를 받아들이고 double값 을 돌려주며 square()의 선언문과 일치하는 함수에 대한 포인터 p를 선언한다. 17번째 줄에 서는 포인터 p를 square로 설정한다. square나 p에서 괄호가 사용되지 않았다는 것에 주의하자. 20번째 줄은 square()와 p()의 호출에서 복귀되는 값을 출력한다.
괄호를 포함하지 않는 함수의 이름은 함수에 대한 포인터이다. 이것은 배열의 경우와 비슷하다. 함수에 대한 포인터를 선언하고 사용하는 것과 어떤 차이점이 있을까? 함수의 이름 자체는 포인터 상수로 변경이 불가능하다. 이 사실은 배열에서와 같다. 그러나 포인터 변수는 변경할 수 있다. 특히, 필요할 때마다 다른 함수를 지적하도록 설정할 수 있다.

<리스트 15.9>에 있는 프로그램은 함수에 정수형 값을 인수로 전달하여 함수를 호출한다. 전달되는 인수의 값에 따라 함수는 포인터가 세 가지 다른 함수의 하나를 지적하도록 초기화하고 나서 대응하는 마수를 호출하기 위해서 포인터를 사용한다. 이런 세 함수의 각각은 화면 상에 독특한 메시지를 출력한다.

<리스트 15.9> 상황에 따라 다른 함수를 호출하기 위해서 함수에 대한 포인터를 사용하는 프로그램

/* 서로 다른 함수를 호출하기 위한 포인터의 사용 예 */

#include

 

/* 함수 원형 */

 

void func1(int x);

void one(void);

void two(void);

void other(void);

 

main()

{

int a;

 

for(;;)

{

puts("\nEnter an integer between 1 and 10, 0 to exit: ");

scanf("%d", &a);

 

if(a == 0)

break;

func1(a);

}

return(0);

}

 

void func1(int x)

{

/* 함수에 대한 포인터 */

 

void(*ptr)(void);

 

if(x == 1)

ptr = one;

else if(x == 2)

ptr = two;

else

ptr = other;

 

ptr();

};

 

void one(void)

{

puts("You entered 1.");

}

 

void two(void)

{

puts("You entered 2.");

}

 

void other(void)

{

puts("You entered something other than 1 or 2.");

}

 

=> 이 프로그램의 16번째 줄에서는 0의 값이 입력될 때가지 프로그램을 계속 실행하기 위해서 무한 루프를 사용하고 있다. 0이 아닌 값이 입력될 때 입력된 값은 func1()에 전달된다. func1()의 32번째 줄에 나타나 있는 함수에 대한 포인터 ptr의 선언문을 주의해서 살펴보자. 이 선언문은 ptr이 func1()에 대해서 지역 변수의 상태가 되도록 해주는데, 프로그램의 다른 부분에서는 이 포인터를 사용할 필요가 없으므로 이렇게 지역 변수로 선언하는 것이 좋다. func1()의 34번째 줄부터 39번째 줄까지는 입력된 값에 따라 ptr을 특정 함수로 설정한다. 41번째 줄은 특정 함수에 설정된 ptr()을 호출한다. 물론, <리스트 15.9>에 있는 프로그램은 예제로 사용하기 위한 것이다. 이 프로그램에서는 함수에 대한 포인터를 사용하지 않고도 쉽게 동일한 결과를 얻을 수 있을 것이다. 이제, 여러 가지 함수를 호출하기 위해서 함수의 인수로 포인터를 전달하여 사용하는 방법을 알아보도록 하자. <리스트 15.10>에 있는 프로그램은 <리스트 15.9>를 수정한 것이다.

<리스트 15.10> 함수에 대한 포인터를 인수로 전달하기

/* 함수에 대한 포인터를 인수로 전달하기 */

#include

 

/* 함수 원형. 함수 func1()은 어떤 인수도 받아들이지 않고 복귀값을 가지지 않는 함수에

대한 포인터를 하나의 인수로 받아들인다. */



void func1(void (*p)(void));

void one(void);

void two(void);

void other(void);

 

main()

{

/* 함수에 대한 포인터 */

 

void (*ptr)(void);

int a;

 

for(;;)

{

puts("\nEnter an integer between 1 and 10, 0 to exit: ");

scanf("%d", &a);

 

if(a == 0)

break;

else if(a == 1)

ptr = one;

else if(a == 2)

ptr = two;

else

ptr = other;

func1(ptr);

}

return(0);

}

 

void func1(void (*p)(void)

{

p();

}

 

void one(void)

{

puts("You entered 1.");

}

 

void two(void)

{

puts("You entered 2.");

}

 

void other(void)

{

puts("You entered something other than 1 or 2.");

}

 

=> <리스트 15.9>와 <리스트 15.10>의 차이점을 주의해서 살펴보자. 함수에 대한 포인터의 선언문은 main()의 18번째 줄로 이동되었다. 이제 main()의 26번째 줄부터 33번째 줄까지는 사용자가 입력한 값에 따라 포인터가 정확한 함수를 지적하도록 초기화하고 나서 초기화된 포인터를 func1()에 전달한다. <리스트 15.10>에서는 함수 func1()이 어떤 특별한 동작을 수행하지 않고 있다. 단지 ptr이 지적하는 함수를 호출한다. 이 프로그램도 또한 예제로 사용된 것이다. 여기서 설명하는 내용은 잠시 후에 설명할 것과 같은 실용적인 프로그램에 응용될 수 있다.

함수에 대한 포인터를 사용하는 프로그래밍 작업의 한 가지 예는 정렬 작업이 필요한 경우이다. 프로그램에서는 가금 여러 가지 정렬 방식을 사용하기 원할 것이다. 예를 들어, 한 번은 알파벳 순서로 데이터를 정렬하고, 한 번은 알파벳의 역순으로 정렬하기 원할 수 있다. 함수에 대한 포인터를 사용하면 프로그램은 정확한 정렬 함수를 호출할 수 있다. 더욱 정확히 말하자면, 일반적으로 여러 가지 비교 함수를 호출하여 사용할 수 있는 것이다. <리스트 15.7>에 있는 프로그램을 다시 한 번 살펴보자. sort() 함수의 실제 정렬 순서는 라이브러리 함수 strcmp()가 돌려주는 값에 의해서 결정된다. 이 함수는 프로그램에서 사용되는 문자열이 다른 하나의 문자열보다 '작은지' 또는 '큰지'의 여부를 알려준다. 만약 A가 Z보다 작은 것으로 평가되는 알파벳 순서에 의해서 정렬 동작을 수행하는 하나의 함수와 Z가 Z보다 큰 것으로 평가되는 알파벳 역순에 의해서 정렬 동작을 수행하는 하나의 함수를 작성한다면 어떨까? 프로그램은 사용자에게 어떤 순서로 정렬하기 원하는지 결정하도록 요구하고, 정렬 함수는 포인터를 사용하여 적절한 비교 함수를 호출할 수 있다. <리스트 15.11>은 <리스트 15.7>에 있는 프로그램을 수정하고 이런 기능을 결합시킨 것이다.

<리스트 15.11> 여러 가지 정렬 방식을 사용하기 위해서 함수에 대한 포인터를 사용하는 프로그램

/* 키보드에서 문자열을 읽어들이고 오름차순이나 내림차순으로 정렬하여 화면에

출력한다. */

 

#include

#include

#include

 

#define MAXLINES 25

 

int get_lines(char *lines[]);

void sort(char *p[], int n, int sort_type);

void print_strings(char *p[], int n);

int alpha(char *p1, char *p2);

int reverse(char *p1, char *p2);

 

char *lines[MAXLINES];

 

main()

{

int number_of_lines, sort_type;

 

/* 키보드에서 여러 줄을 읽어들인다. */

 

number_of_lines = get_lines(lines);

 

if(number_of_lines < 0)

{

puts("Memory allocation error");

esit(-1);

}

 

puts("Enter 0 for reverse order sort, 1 for alphabetical: ");

scanf("%d", &sort_type);

 

sort(lines, number_of_lines, sort_type);

print_strings(lines, number_of_lines);

return(0);

}

 

int get_lines(char *lines[])

{

int n = 0;

char buffer[80]; /* 각 줄에 대한 임시 저장 공간 */

 

puts("Enter one line at a time; enter a blank when done.");

 

while(n < MAXLINES && gets(buffer) != 0 && buffer[0] != '\0')

{

if((lines[n] = (char *)malloc(strlen(buffer)+1)) == NULL)

return -1;

strcpy(lines[n++], buffer);

}

return n;

 

} /* get_lines()의 끝 */

 

void sort(char *p[], int n, int sort_type)

{

int a, b;

char *x;

 

/* 함수에 대한 포인터 */

 

int(*compare)(char *s1, char *s2);

 

/* 포인터가 인수 sort_type에 따라 적절한 비교 함수를 지적하도록 초기화한다. */



compare = (sort_type) ? reverse : alpha;

 

for(a = 1; a < n; a++)

{

for(b = 0; b < n - 1; b++)

{

if(compare(p[b], p[b+1]) > 0)

{

x = p[b];

p[b] = p[b+1];

p{b+1] = x;

}

}

}

} /* sort()의 끝 */

 

void print_strings(char *p[], int n)

{

int count;

 

for(count = 0; count < n; count++)

printf("\n%s", p[count]);

}

 

int alpha(char *p1, char *p2)

/* 알파벳 여순 비교 */

{

return(strcmp(p1, p2));

}

 

=> main()의 32번째 줄과 33번째 줄은 어떤 정렬 방식을 사용하기 원하는지 묻는다. 선택된 정렬 순서는 sort_type에 저장된다. 이 값은 <리스트 15.7>에서 설명했던 다른 값과 함께 sort() 함수에 전달된다. sort() 함수는 약간 변경되었다. 64번째 줄은 두 개의 문자형 포인터(문자열)를 인수로 받아들이는 함수에 대한 포인터 compare()를 선언한다. 69번째 줄에서는 sort_type의 값을 기본으로 하여 compare()를 리스트에 추가된 두 가지 새로운 함수의 하나로 서정한다. 두 개의 새로운 함수는 alpha()와 reverse()이다. alpha()는 <리스트 15.7>에서 사용했던 것과 같은 라이브러리 함수 strcmp()를 사용한다. reverse()는 역순으로 정렬을 수행하기 위해서 전달된 매개 변수의 위치를 변경하여 사용한다.

 

 

 

 

'pc관련 > C언어' 카테고리의 다른 글

C언어_디스크파일의 사용  (0) 2019.04.11
C언어_링크리스트  (0) 2019.04.07
고급 프로그램제어문  (0) 2019.04.03
C언어_변수에 범위  (0) 2019.03.28
C언어_구조체  (0) 2019.03.25
Posted by 둥이파파^^
pc관련/C언어2019. 4. 3. 21:26

여섯번째 강의 "기본적인 프로그램 제어문"에서는 프로그램에서 다른 문장의 실행을 제어하는 C의 프로그램 제어문을 간단히 살펴보았다. 이 장에서는 지금까지 다루지 않았던 goto문과 프로그램의 순환문에서 사용할 수 있는 유용한 프로그램 제어문을 살펴보도록 하겠다. 오늘은 다음과 같은 내용을 배운다.

·break와 continue문을 사용하는 방법
·무한 루프는 무엇이고, 왜 필요한가?
·goto문을 사용하는 방법과 프로그램에서 goto문을 사용하지 않아야 하는 이유
·switch문을 사용하는 방법
·프로그램의 종료를 제어하는 방법
·프로그램이 종료될 때 자동으로 함수를 실행하는 방법
·프로그램 내에서 시스템의 명령을 실행하는 방법

1. 순환문을 미리 종료하는 방법
: 여섯번째 강의에서는 for문, while문, do...while문이 프로그램의 실행을 제어한다는 것을 배웠다. 이런 순환문은 프로그램의 조건에 따라 C의 문장이나 블록을 전혀 실행하지 않거나 한 번 실행하거나 또는 여러 번 실행한다. 이런 세 가지 순환문에서는 주어진 조건에 일치하는 상황이 발생하는 경우에만 순환문을 종료할 수 있다. 그러나 가끔 순환문의 반복 상태를 직접 제어할 수 있기를 원할 것이다. break와 continue문은 이런 순환문의 실행을 제어할 수 있게 해준다.

1.1 break문
: break문은 for문, while문 또는 do...while문 내에서만 사용할 수 있다. switch문에서도 사용되지만 자세한 내용은 이 장의 후반부에서 다시 설명할 것이다. 간단히 말해서 break문이 나타나면 순환문은 즉시 종료된다. 다음은 예제이다.

for(count = 0; count < 10; count++)
{
if(count == 5)
break;
}

일반적인 경우에 for문은 10번 실행될 것이다. 그러나 여기에서는 for문이 6번째 반복되기 전에 count가 5의 값을 가지므로 break문이 실행되고 for문이 종료된다. 제어는 for문 바로 다음에 있는 문장으로 전달될 것이다. 종속된 순환문에서 break문이 사용될 때에는 가장 내부에서 사용되는 순환문이 종료된다.

<리스트 13.1> break문 사용하기

/* break문의 사용 에 */

#include

 

char s[] = "This is a test string. It contains two sentences.";

 

main(0

{

int count;

 

printf("\noriginal string: %s", s);

 

for(count = 0; s[count] != '\n0'; count++)

{

if(s[count] == '.')

{

s[count = 1] = '\n';

break;

}

}

printf("\nModified string: %s\n", s);

 

return 0;

}

 

-> 출 력

Original string: This is a test string. It contains two sentences.
modified string: This is a test string.

1.2 continue문
: break문과 마찬가지로 continue문은 for문, while문, do...while문 내에서만 사용될 수 있다. continue문이 실행되면 제어는 순환훈의 마지막 부분으로 전달되고 다음 순환 동작이 시작된다. continue문과 순환문의 마지막 부분으로 전달되고 다음 순환 동작이 시작된다. continue문과 순환문의 마지막 부분 사이에 있는 문장은 실행되지 않는다. <리스트 13.2>에는 continue를 사용하는 프로그램이 나타나 있다. 이 프로그램은 키보드에서 문자열을 받아들이고 모든 소문자 형태의 모음을 제거하여 다시 출력한다.

<리스트 13.2> continue문 사용하기

/* continue문의 사용 예 */

#include

 

main()

{

/* 입력용 버퍼와 카운터 변수를 선언 */

 

char buffer[81];

int ctr;

 

/* 한 줄의 텍스트 입력 */

 

puts("Enter a line of text: ");

gets(buffer);

 

/* 소문자 모음이 아닌 문자만을 출력하며 문자열을 차례대로 사용 */

 

for(ctr = 0; buffer[ctr] != '\n0'; ctr++)

{

 

/* 문자가 소문자 모음이면 출력하지 않고 다음으로 진행 */

 

if(buffer[ctr] == 'a' || buffer[ctr] == 'e' || buffer[ctr] == 'i'

|| buffer[ctr] == 'o' || buffer[ctr] == 'u')

continue;

 

/* 모음이 아니면 출력 */

 

putchar(buffer[ctr]0;

}

return 0;

}

 

2. goto문
: goto문은 C에서 조건 없이 이동(unconditional jump)하거나 분기(branching)하는 명령의 하나이다. 프로그램에서 goto문이 나타날 때 제어는 즉시 goto문에 의해 지정된 위치로 이동하거나 분기한다. goto문이 실행될 때에는 조건 없이(unconditional) 항상 분기 동작이 발생한다. 분기 동작은 if문처럼 프로그램의 어떤 조건에 의해서 발생하는 것이 아니다. goto문과 목적지의 레이블은 서로 다른 블록에 존재할 수 있지만 동일한 함수 내에 존재하는 것이어야 한다. goto문의 사용 예를 보여주는 <리스트 13.3>의 간단한 프로그램을 살펴보자.

<리스트 13.3> goto문 사용하기

/* goto문의 사용 예 */

#include

 

main()

{

int n;

 

start: ;

 

puts("Enter a number between 0 and 10: ");

scanf("%d", &n);

 

if(n < 0 || n > 10)

goto start;

else if(n ==0)

goto location0;

else if(n == 1)

goto location1;

else

goto location2;

 

location0: ;

puts("You entered 0.\n");

goto end;

 

location1: ;

puts("You entered 1.\n");

goto end;

 

location2: ;

puts("You entered something between 2 and 10.\n");

 

end: ;

return 0;

}

 

goto문의 목적지는 프로그램 내에서 goto문의 앞이나 뒤에 위치될 수 있다. 앞에서도 언급했듯이 goto문에 대한 유일한 제한 사항은 goto문과 목적지가 같은 함수 내에 존재해야 한다는 것이다. 그러나 함수 내의 서로 다른 블록에 위치될 수 있다. for문과 같은 순환문에서 제어를 순환문의 바깥으로 이동시키기 위해서 goto를 사용할 수도 있지만 goto문을 이렇게 사용해서는 안된다. 그리고 프로그램에서는 가능하다면 goto문을 사용하지 않는 것이 좋은데, 두 가지 이유가 있다.

·goto문은 필요하지 않다. goto문을 사용해야 하는 프로그래밍 작업은 없다. goto문이 필요한 경우가 있더라도 항상 C에서 제공되는 다른 분기문을 사용하여 필요한 작업을 할 수 있다.

·goto문은 위험하다. goto문이 특별한 프로그래밍 문제에 대한 이상적인 해결 방법처럼 생각되겠지만 실제로는 전혀 그렇지 않다. goto문에 의해서 프로그램이 분기될 때에는 어디에서 분기되었는지 알 수 없으므로 프로그램이 혼란스러워질 수 있다. 이런 형태의 혼잡한 프로그래밍(spaghetti-code)은 좋지 않다. 이런 두 가지 사실을 알고 주의해서 프로그램을 작성한다면 goto문을 사용하더라도 문제가 없는 프로그램을 작성할 수 있을 것이다. 어떤 경우에는 goto문을 주의해서 사용한다면 프로그래밍 문제를 가장 간단한 방법으로 해결할 수도 있을 것이다 .그러나 goto문의 사용이 유일한 해결 방법은 아니다. 앞에서 설명한 두 가지 이유를 무시하더라도 goto문을 사용할 때에는 적어도 주의할 필요는 있을 것이다.

3. 무한 루프
: 무한 루프는 무엇이고, 프로그램 내에서 무한 루프가 필요한 이유는 무엇일까? 무한 루프는 실행을 마치는 상황이 발생하지 않고 계속해서 반복되는 순환문이다. 무한 루프는 for, while, do...while문이 될 수 있다. 예를 들어, 다음 문장은

while(1)
{
/* 다른 프로그램 문장들 */
}

무한 루프가 될 것이다. while네 주어진 조건은 항상 참으로 평가되고 프로그램이 실행되더라도 변경되지 않는 상수이다. 1은 결코 바뀌지 않는 값이므로 순환문은 절대로 끝나지 않을 것이다. 앞에서는 순환문을 벗어나기 위해서 break문을 사용할 수 있다는 것을 설명했다. break문을 사용하지 않는다면 무한 루프의 가치는 없다. 무한 루프는 break문과 함께 사용될 때 유용하다. 또한, 다음과 같이 for나 do...while을 사용하여 무한 루프를 생성할 수 있다.

for( ; ; )
{
/* 다른 프로그램 문장들 */
}
do
{
/* 다른 프로그램 문장들 */
} while(1);

이론적으로 이런 세 가지 형태의 순환문은 같은 것이다. 여기에서는 while문을 사용할 것이다. 무한 루프는 여러 개의 조건을 확인해서 순환문을 끝내야 하는지 결정하는데 사용할 수 있다 while문의 실행 조건이 입력되는 부분에 여러 개의 조건을 포함시키는 것은 어려울 것이다. 대신에, 순환문 내에서 개별적으로 여러 개의 조건을 확인해서 필요에 따라 break를 사용하여 종료하는 것이 더 낫다.

또한, 무한 루프는 프로그램의 동작을 지시하는 메뉴 체계를 구성하기 위해서 사용될 수 있다. 다섯번째 강의 "함수의 기본"에서는 프로그램의 main() 함수가 여러 가지 동작을 수행하는 다양한 함수의 실행을 지시하는 '교통 경찰'과 같은 역할을 수행하는 예를 살펴보았다. 프로그램의 사용자에게는 여러 가지 항목이 제공되고, 원하는 항목의 하나를 선택하여 필요한 동작을 수행할 수 있다. 또한, 이런 항목 중에는 프로그램을 종료하는 선택 사항이 포함되어야 한다. 일단 항목이 선택되면 선택된 항목에 따라 프로그램이 실행된다. <리스트 13.4>에 있는 프로그램은 메뉴 체계의 사용 예를 보여준다.

<리스트 13.4> 메뉴 체계를 사용하기 위한 무한 루프의 사용 예

/* 메뉴 체계를 구현하기 위한 무한루프의 사용 예 */

#include

#define DELAY 1500000 /* 지연에 사용되는 값 */

 

int menu(void0;

void delay(void);

 

main()

{

int choice;

 

while(1)

{

 

/* 사용자의 선택을 요구 */

 

choice = menu();

 

/* 입력에 따라 분기 */

 

if(choice == 1)

{

puts("\nExecuting choice 1.");

delay();

}

else if(choice == 2)

{

puts("\nExecuting choice 2.");

delay();

}

else if(choice == 3)

{

puts("\nExecuting choice 3.l");

delay();

}

else if(choice == 4)

{

puts("\nExecuting choice 4.");

delay();

}

else if(choice == 5)

{

puts("\nExiting program now...\n");

delay();

break;

}

else

{

puts("\nInvalid choice, try again.");

delay();

}

}

return 0;

}

 

/* 메뉴를 출력하고 사용자의 선택을 읽어들인다. */

int menu(void)

{

int reply;

 

puts("\nEnter 1 for task A.");

puts("Enter 2 for task B.");

puts("Enter 3 for task C.");

puts("Enter 4 for task D.");

puts("Enter 5 to exit program.");

 

scanf("%d", &reply);

 

return reply;

}

 

void delay(void)

{

long x;

for(x = 0; x < DELAY; x++)

;

}

 

4. switch문
: C에서 제공되는 가장 융통성 있는 프로그램 제어문은 프로그램 내에 포함되는 두 가지 이상의 값을 기본적으로 하여 여러 가지 문장을 실행하게 해주는 switch문이다. 지금까지 설명했던 if문과 같은 제어문에서는 단지 두 개의 값인 참이나 거짓으로 평가되는 수식만을 사용할 수 있었다. 두 개 이상의 값을 기준으로 해서 프로그램의 흐름을 제어하기 위해서는 <리스트 13.4>에 나타나 있듯이 여러 개의 종속된 if문을 사용해야 한다. switch문은 이런 종속문을 대신할 수 있다. switch문의 일반적인 형식은 다음과 같다.

switch(expression)
{
case template_1: statement(s);
case template_2: statement(s);

case template_n: statement(s);
default: statement(s);
}

여기서 expression은 long, int, char형과 같은 정수값으로 평가되는 수식이다. switch문은 expression을 평가하여 결과를 각각의 case 다음에 포함되어 있는 template과 비교하고 나서 다음과 같은 동작을 수행한다.

·expression의 결과와 template의 어떤 것이 일치한다면 해당 case에 포함되어 있는 문장이 실행된다.
·아무 것도 일치하지 않으면 선택적으로 사용되는 default에 포함되어 있는 문장이 실행된다
·아무 것도 일치하지 않고 default도 포함되어 있지 않다면 switch문의 바로 다음에 있는 문장이 실행된다.

<리스트 13.5>에는 사용자가 입력한 값에 따라 메시지를 출력하기 위해 switch문을 사용하는 간단한 프로그램이 나타나 있다.

<리스트 13.5> switch문의 사용 예

/* switch문의 사용 예 */

#include

 

main()

{

int reply;

 

puts("Enter a number between 1 and 5: ");

scanf("%d", &reply);

 

switch(reply)

{

case 1:

puts("You entered 1.");

case 2:

puts("You entered 2.");

case 3:

puts("You entered 3.");

case 4:

puts("You entered 4.");

case 5:

puts("You entered 5.");

default:

puts("Out of range, try again.");

}

return 0;

}

 

=> 분명히 무언가 잘못되었다는 것을 알 수 잇을 것이다. 여기서 switch문은 처음에 일치하는 템플릿(template)을 발견하고 해당 case에 있는 문장뿐 아니라 이후의 모든 문장들을 실행하고 있다. 잘못된 결과이기는 하지만 switch문은 실제로 이렇게 조건에 따라 정해진 동작을 수행한다. switch문은 값이 일치하는 템플릿으로 분기한다. 그러나 값이 일치하는 템플릿에 포함된 문장만 실행하기 위해서는 필요한 부분에 break문을 포함시켜야 한다. <리스트 13.6>에는 break문을 사용하여 다시 작성한 프로그램이 나타나 있다. 이제 프로그램은 정상적으로 동작할 것이다.

<리스트 13.6> 필요한 부분에 break문을 포함시켜서 정상적으로 실행되는 switch문의 사용예

/* switch문의 빠른 사용 예 */

#include

 

main()

{

int reply;

 

puts("Enter a number between 1 and 5: ");

scanf("%d", &reply);

 

switch(reply)

{

case 0:

break;

case 1:

{

puts("You entered 1.");

break;

}

case 2:

{

puts("You entered 2.");

break;

}

case 3:

{

puts("You entered 3.");

break;

}

case 4:

{

puts("You entered 4.");

break;

}

case 5:

{

puts("You entered 5.");

break;

}

default:

{

puts("Out of range, try again.");

}

} /* switch의 끝 */

return 0;

}

 

=> 프로그램을 컴파일하고 실행해보자. 정상적으로 동작할 것이다. switch문은 <리스트 13.4>에 나타나 있는 것과 같이 메뉴를 처리하는 경우에 가장 많이 사용된다. <리스트 13.7>에 있는 프로그램은 메뉴를 구현하기 위해 if문 대신에 switch문을 사용하고 있다. switch를 사용하는 것은 <리스트 1.34>에 나타나 있는 메뉴 프로그램의 이전 버전에서 사용되었던 종속된 if문을 사용하는 것보다 훨씬 낫다.

<리스트 13.7> 메뉴 체계를 구성하기 위한 switch문의 사용 예

/* 메뉴 체계를 구현하기 이한 무한 루프와 switch문의 사용 예 */

#include

#include

 

#define DELAY 150000

 

int menu(void);

void delay(void);

 

main()

{

while(1)

{

/* 사용자의 선택을 받아들이고 입력에 따라 분기 */

 

switch(menu())

{

case 1:

{

puts("\nExecuting choice 1.");

delay();

break;

}

case 2:

{

puts("\nExecuting choice 2.");

delay();

break;

}

case 3:

{

puts("\nExecuting choice 3.");

delay();

break;

}

case 4:

{

puts("\nExecuting choice 4.");

delay();

break;

}

case 5: /* 프로그램의 끝 */

{

puts("\nExiting program now...\n");

delay();

exit(0);

}

default:

{

puts("\nInvalid choice, try again.");

delay();

}

} /* switch의 끝 */

} /* while의 끝 */

return 0;

}

 

/* 메뉴를 출력하고 사용자의 선택을 받아들인다. */

int menu(void)

{

int reply;

 

puts("\nEnter 1 for task A.");

puts("Enter 2 for task B.");

puts("Enter 3 for task C.");

puts("Enter 4 for task D.");

puts("Enter 5 to exit program.");

 

scanf("%d", &reply);

 

return reply;

}

 

void delay(void)

{

long x;

for(x = 0; x < DELAY; x++)

;

}

 

=> 이 프로그램에서는 새로운 함수가 사용되고 있다. case 5:에 포함된 48번째 줄의 라이브러리 함수 exit()를 주목하기 바란다. 여기에서는 <리스트 13.4>와 같이 break문을 사용할 수 없다. 여기서 break를 사용하면 무한 루프인 while문을 벗어나는 것이 아니라 switch문을 벗어나게 된다. 잠수 후에 설명하겠지만 exit() 함수는 프로그램 자체를 종료하는 기능을 가진다. 가끔 switch를 구성하는 여러 개의 항목을 동일하게 '처리'하는 것이 유용할 때가 있다. 예를 들어, 여러 가지 항목에서 어떤 것을 선택하든지 특정 문장을 실행할 필요가 있다고 하자. 이 때에는 break문을 사용하지 말고 필요한 문장 앞에 모든 case 템플릿을 입력하면 된다. 만약 조건 수식이 어떤 case에 일치한다면 여러분이 실행하기 원하는 코드 블록에 도달할 때까지 case 다음의 모든 문장이 실행될 것이다. <리스트 13.8>에 있는 프로그램은 이런 경우를 보여준다.

<리스트 13.8> switch문을 사용하는 다른 한 가지 방법

/* switch문을 사용하는 다른 한 가지 방법 */

#include

#include

 

main()

{

int reply;

 

while(1)

{

puts("\nEnter a value between 1 and 10, 0 to exit: ");

scanf("%d", &reply);

 

switch(reply)

{

case 0:

exit(0);

case 1:

case 2:

case 3:

case 4:

case 5:

{

puts("You entered 5 or below.\n");

break;

}

case 6:

case 7:

case 8:

case 9:

case 10:

{

puts("You entered 6 or higher.\n");

break;

}

default:

puts("Between 1 and 10, please!\n");

} /* switch의 끝 */

} /* while의 끝 */

return 0;

}

 

=> 이 프로그램은 키보드에서 값을 읽어들이고 5이하인지 6이상인지 또는 1과 10사이의 값인지 알려준다. 입력된 값이 0이라면 18번째 줄에서는 exit() 함수를 호출하므로 프로그램은 종료.

5. 프로그램의 종료
: C 프로그램은 일반적으로 main() 함수의 실행이 끝날 때 종료된다. 그러나 라이브러리 함수 exit()를 사용하면 언제든지 원하는 시기에 프로그램을 마칠 수 있다. 또한, 프로그램이 종료될 때 하나 이상의 함수를 자동으로 실행하게끔 지정할 수도 있다.

5.1 exit() 함수
: exit() 함수는 프로그램 실행을 종료하고, 제어를 운영체제에 돌려준다. 이 함수는 프로그램이 성공적으로 실행되었는지 또는 실행에 문제가 있었는지를 지적하기 위해서 운영체제에 전달하는 하나의 int형 인수를 가진다. exit() 함수의 형식은 다음과 같다.

exit(status);

status의 값이 0이라면 프로그램이 정상적으로 종료되었다는 것을 뜻한다. status가 1의 값을 가지면 어떤 에러가 발생해서 프로그램이 비정상적으로 종료되었다는 것을 뜻한다. 이런 복귀값은 대개 무시된다. DOS에서는 배치 파일과 if errorlevel문을 사용하여 이런 복귀값을 확인하고 사용할 수 있다. 그러나 여기서는 DOS 설명서가 아니므로 복귀값을 사용하는 방법에 대해서 알라보기 원한다면 DOS 관련 서적을 참고하기 바란다.

exit() 함수를 사용하기 위해서는 프로그램에서 헤더 파일 STDLIB.H를 포함시켜야 한다. 또한, 이 헤더 파일에서는 exit() 함수에 대한 인수로 사용되는 두 개의 기호 상수를 다음과 정의하고 있다.

#define EXIT_SUCCESS 0
#define EXIT_FAILURE 1

그래서 복귀값을 0으로 설정하여 프로그램을 마치기 원한다면 exit(EXIT_SUCCESS)를 사용할 수 있다. 1의 값을 돌려주기 위해서는 exit(EXIT_FAILURE)를 사용하면 된다.

6. 프로그램 내에서 운영테제의 명령을 실행하는 방법
: C의 표준 라이브러리는 실행 중인 C 프로그램 내에서 운영테제의 명령을 실행하게 해주는 함수 system()을 제공한다. 이 함수는 프로그램을 종료하지 않은 상태에서 디렉토리 목록을 사렾보고나 디스크를 초기화에 해주므로 때에 따라 유용하게 사용할 수 있다. system() 함수를 사용하기 위해서는 프로그램에 헤더 파일 STDLIB.H를 포함시켜야 한다. system()의 형식은 다음과 같다.

system(command);

인수 command는 문자열 상수나 문자열에 대한 포인터가 될 수 있다. 예를 들어 DOS의 디렉토리 목록을 살펴보기 위해서 다음과 같은 내용을 사용할 수 있을 것이다.

system("dir");

또는 다음 문장을 사용할 수도 있다.

char *command = "dir";
system(command);

운영체제의 명령을 실행하고 나면 프로그램의 제어는 system() 함수 바로 다음의 문장으로 전달된다. system()에서 사용한 명령이 운영체제에서 유효하지 않은 명령이라면 프로그램이 다시 실행되기 전에 'Bad command or file name'이라는 에러 메시지가 출력된다. system()의 사용 예가 <리스트 13.9>에 나타나 있다.

<리스트 13.9> 운영체제의 명령을 실행하기 위해서 system() 함수를 사용하는 프로그램

/* system() 함수의 사용 예 */

#include

#include

 

main(0

{

/* 입력을 저장할 버퍼 선언 */

 

char input[40];

 

while(1)

{

/* 사용자의 명령을 받아들인다. */

 

puts("\nInput the desired system command, blank to exit");

gets(input);

 

/* 빈 줄이 입력되면 마친다. */

 

if(input[0] == '\0'

exit(0);

 

/* 명령을 실행한다. */

 

system(input);

}

return 0;

}

 

system()에서 사용할 수 있는 명령은 디렉토리 목록을 살펴보거나 디스크를 초기화하는 것과 같은 간단한 명령에만 제한되지 않는다. 또한, 실행 가능한 파일이나 배치 파일의 이름을 전달하여 프로그램을 정상적으로 실행할 수도 있다. 예를 들어, system의 인수로 LIST1308을 전달하면 LIST1308이라는 프로그램이 실행될 것이다. 프로그램의 실행이 끝나면 제어는 다시 system() 함수가 사용되었던 곳으로 전달된다.

system()을 사용할 때 유일한 제한 사항이 있다면 메모리와 관련된 문제이다. system()이 실행될 때 원래의 프로그램은 컴퓨터의 RAM에 남게 되고 운영체제의 명령을 처리하는 코드와 함수를 통해서 실행되는 프로그램이 메모리에 읽어들여지게 된다. 이렇게 다른 프로그램을 실행하는 것은 컴퓨터에 충분한 메모리가 남아 있는 경우에만 가능하다. 그렇지 않다면 에러 메시지가 출력될 것이다.

'pc관련 > C언어' 카테고리의 다른 글

C언어_링크리스트  (0) 2019.04.07
C언어_포인터,고급기능  (0) 2019.04.07
C언어_변수에 범위  (0) 2019.03.28
C언어_구조체  (0) 2019.03.25
C언어_문자,문자열  (0) 2019.03.24
Posted by 둥이파파^^
pc관련/C언어2019. 3. 28. 22:17

함수 내에서 선언된 변수가 함수의 밖에서 선언된 변수와 분명히 구분된다는 것을 설명했다. 이런 사실은 C 프로그래밍에서 중요한 변수의 범위(variable scope)를 부분적으로 설명하는 예이다.

·변수의 범위
·외부 변수의 의미와 외부 변수를 사용하지 않아야 하는 이유
·지역 변수에 대해서
·정적 변수와 자동 변수의 차이
·지역 변수와 블록에 대해서
·적절한 변수의 형태를 선택하는 방법

1. 변수의 범위란?
: 변수의 범위(scope)는 프로그램에서 변수를 사용할 수 있는 범위나 또는 변수가 프로그램 내에서 효과를 나타낼 수 있는(visible) 범위를 말한다. C에서 변수에 대해서 설명할 때에는 변수의 값을 사용할 수 있는 가능성(accessibility)과 주어진 범위 내에서 효과를 나타내는 것을 뜻하는 유효성(visibility)을 함께 사용한다. 변수의 범위를 언급할 대 변수(variable)는 C의 모든 데이터형을 뜻한다. 즉, 간단한 변수에서부터 배열, 구조체, 포인터 등을 모두 포함한다. 또한, const 키워드로 정의된 기호 상수도 포함한다. 변수의 범위는 변수의 생명, 즉 메모리에서 변수의 값이 보존되는 기간이나 변수에 저장 영역이 할당되고 해제되는 시기에 영향을 준다. 우선, 변수의 유효성(visibility)에 대해서 알아보도록 하자.

1.1 변수의 범위를 설명하는 예제
: <리스트 12.1>에 있는 프로그램을 살펴보자. 프로그램은 5번째 줄에서 변수 x를 정의하고, 11번째 줄에서 x의 값을 출력하기 위해서 printf() 함수를 호출하며 다시 x의 값을 출력하기 위해 printf_value()를 호출한다. 함수 print_value()에는 x의 값이 인수로 전달되지 않는다는 것에 주의하기 바란다. 이 함수는 19번째 줄에서 printf() 함수에 대한 인수로 x를 사용하고 있다.

 

<리스트 12.1> 변수 x는 함수 print_value내에서도 사용될 수 있다.

/* 변수의 범위를 설명하는 에제 */

#include

 

int x = 999;

 

void print_value(void);

 

main()

{

printf("%d\n", x);

print_value();

 

return 0;

}

 

void print_value(void)

{

printf("%d\n", x);

}

 

-> 출 력
999
999

이 프로그램은 아무런 문제 없이 컴파일되고 실행된다. 이제 이 프로그램에서 x를 정의하는 문장을 main() 함수 내의 위치로 이동시켜 보자. 변경된 소스 코드는 <리스트 12.2>에 나타나 있다.

 

<리스트 12.2> 변수 x는 함수 print_value 내에서 사용될 수 없다.

/* 변수의 범위를 설명하는 예제 */

#include

 

void print_value(void);

 

main()

{

int x = 999;

 

printf("%d\n", x);

print_value();

 

return 0;

}

 

void print_value(void)

{

printf("%d\n", x);

}

 

리스트 12.2>를 컴파일하면 컴파일러는 다음과 비슷한 에러 메시지를 출력할 것이다.

list1202.c[19] : Error: undefined identifier 'x'.

에러 메시지에서 괄호 내에 나타나는 숫자는 에러가 발생한 프로그램의 문장 번호를 뜻한다. 19번째 줄은 print_value() 함수 내에서 printf() 함수를 호출하는 문장이다. 에러 메시지는 print_value() 함수 내에서 변수 x가 '정의되지 않았다.'는 것을 알려준다. 즉, 함수 내에서는 변수 x가 유효하지 않다. 그러나 11번째 줄에 있는 printf()함수에서는 에러 메시지가 발생하지 않는다는 것에 주목할 필요가 있다. 프로그램의 11번째 줄에서는 변수 x가 유효한 것이다.

<리스트 12.1>과 <리스트 12.2>의 유일한 차이는 변수 x가 정의된 위치이다. x를 정의하는 문장을 이동시키면 변수의 범위가 바뀐다. <리스트 12.1>에서 x는 외부(external) 변수로 선언되었고, 이때 변수의 유효 범위는 전체 프로그램이다. 변수 x는 main() 함수와 print_value() 함수에서 모두 사용할 수 있다. 그러나 <리스트 12.2>에서 x는 지역(local) 변수로 선언되었고, 이때 변수의 유효 범위는 main() 함수로 제한된다. 그래서 함수 print_value()에서는 x가 존재하지 않는 것으로 간주된다 지역 변수와 전역 변수에 대해서는 나중에 상세히 설명할 것이므로 여기서는 변수 범위의 중요성을 알아둘 필요가 있다.

1.2 변수의 범위가 중요한 이유
: 변수의 범위가 중요하다는 것을 이해하기 위해서는 구조화 프로그래밍에 대해서 다시 한 번 언급할 필요가 있다. 구조화 프로그래밍에서는 특정 작업을 수행하는 독립된 함수로 프로그램을 분할한다. 여기서 중요한 것은 독립적(independent)이라는 단어이다. 완전히 독립된 함수를 작성하기 위해서는 함수 내의 변수가 다른 함수에 의해서 간섭되지 않아야 한다. 함수 내에서 독립된 변수를 사용하면 프로그램의 다른 부분에 영향을 받지 않고 주어진 작업을 정상적인 상태로 수행하는 함수를 작성할 수 있다. 여기서, 함수들 간에 완전히 독립된 데이터를 사용하는 것이 항상 바람직하다는 것을 짐작할 수 있을 것이다. 사용되는 변수의 범위를 지정하면 함수들간에 독립된 데이터를 사용할 수 있다. 변수의 범위를 지정하는 것은 이처럼 함수의 독립성에 영향을 준다.

2. 외부 변수
: 외부(external) 변수는 어떤 함수의 바깥에서 정의되는 것이다. main() 함수도 하나의 함수이므로 외부 변수는 main()의 박에서 선언되는 것을 포함한다. 외부 변수는 가금 전역(global) 변수라고도 한다.

2.1 외부 변수의 범위
: 외부 변수의 범위는 전체 프로그램이다. 그래서 외부 변수는 main()이나 프로그램 내의 다른 모든 함수에서 사용될 수 있다. 예를 들어, <리스트 12.1>에 있는 변수 x는 외부 변수다. 프로그램을 컴파일하고 실행하여 살펴보았듯이 x는 두 개의 함수 main()과 print_value()에서 유효하다. 그러나 정확히 말해서 외부 변수의 유효 범위가 전체 프로그램이라는 사실은 잘못된 것이다. 외부 변수의 범위가 변수를 정의하는 전체 소스 코드 파일이라는 표현이 더 정학하다. 전체 프로그램이 하나의 소스 코드 파일로 구성된다면 두 가지 범위는 같다. 대부분의 간단한 C 프로그램은 하나의 파일로 구성되고 프로그램도 이렇게 하나의 파일로 구성된다.

2.2 외부 변수를 사용하는 시기
: 몇 가지 예제 프로그램에서 외부 변수를 사용하고 있지만 실제로는 외부 변수를 사용하지 않도록 해야 한다. 그 이유는 무엇일까? 외부 변수를 사용하면 구조화 프로그래밍에서 중심이 되는 모듈의 독립성(modular independence)이 사라지게 된다. 모듈의 독립성은 프로그램을 구성하는 각각의 함수나 모듈이 주어진 작업을 수행하기 위해서 필요로 하는 모든 코드와 데이터를 내부에 포함하고 있는 것을 뜻한다. 지금까지 사용된 간단한 프로그램에서는 이런 모듈의 독립성이 중요하지 않게 생각될 수도 있지만, 더 크고 복잡한 프로그램을 작성하게 되면 외부 변수에 대한 함수의 의존성은 중요한 문제가 될 수 있다. 그렇다면 외부 변수는 어떤 경우에 사용해야 할까? 프로그램을 구성하는 대부분의 함수나 또는 모든 함수가 사용해야 하는 변수를 선언하는 경우에만 외부 변수를 사용하는 것이 좋다 const 키워드를 사용하여 정의되는 기호 상수는 외부 변수로 선언하기에 적합한 것이다. 변수가 몇 개의 함수에서만 사용된다면 외부 변수로 선언하지 말고 함수에 인수로 전달하자.

2.3 extern 키워드
: 함수에서 외부 변수를 사용할 필요가 있을 때에는 extern 키워드를 사용하여 함수 내에서 변수를 선언하는 것이 좋다. 변수는 다음과 같은 형식으로 선언된다.

extern type name;

type은 변수의 형태이고, name은 변수의 이름이다. 예를 들어, <리스트 12.1>에 포함디어 있는 함수 main()과 print_value()에는 변수 x를 선언하는 문장을 포함시킬 수 있다. <리스트 12.3>에는 수정된 프로그램이 나타나 있다.

 

<리스트 12.3> 변수 x는 함수 main()과 print_value에서 외부 변수로 선언된다.

/* 외부 변수 선언 */

#include

 

int x = 999;

 

void print_value(void);

 

main()

{

extern int x;

 

printf("%d\n", x);

print_value();

 

return 0;

}

 

void print_value(void)

{

extern int x;

printf("%d\n", x);

}

 

3. 지역 변수
: 지역 변수(local variable)는 함수 내에서 정의되는 변수이다. 지역 변수의 범위는 변수가 정의된 함수로 제한된다. 다섯번째 강의 "함수의 기본"에서 함수 내에서 사용되는 지역 변수, 지역 변수를 정의하는 방법, 지역 변수의 장점에 대해서 설명했다. 지역 변수는 컴파일러에 의해서 자동으로 0의 값으로 초기화되지 않는다. 지역 변수를 정의할 때 변수를 초기화하지 않으면 지역 변수에는 임의의 값(garbage)이 저장된다. 그래서 지역 변수를 사용하기 전에는 반드시 필요한 값을 저장해야 한다. 또한, main() 함수 내에서도 지역 변수를 사용할 수 있다. <리스트 12.2>에 있는 변수 x는 main() 함수에서 사용되는 지역 변수의 예이다. 변수 x는 main() 내에서 정의되고 프로그램의 결과로도 알 수 있듯이 main() 내에서만 유효하다.

3.1 정적 변수와 자동 변수
: 지역 변수는 기본적으로 자동 변수이다. 자동 변수는 함수가 호출될 때마다 새롭게 생성되고 함수의 실행이 끝나면 사라지는 변수를 뜻한다. 좀더 정확히 표현하자면 자동 변수는 변수가 정의되어 있는 함수가 호출될 때마다 변수의 값을 보존하지 않는다는 것을 뜻한다. 지역 변수 x를 사용하는 함수가 프로그램에서 호출된다고 하자. 또한, 함수가 처음 호출될 때 변수 x에는 100의 값이 할당된다고 가정하자. 함수의 동작이 안료되면 제어는 다시 함수를 호출한 프로그램으로 돌아가고 함수는 나중에 다시 호출된다. 변수 x는 여전히 100의 값을 가지고 있을까? 그렇지 않다 처음에 생성되었던 변수 x는 제어가 프로그램으로 다시 전달될 때 사라졌다. 함수가 다시 호출될 때에는 변수 x가 새롭게 생성된다. 과거의 x는 완전히 사라진다. 그렇다면 함수가 호출될 때마다 지역 변수의 값을 보존해둘 필요가 있을 때에는 어떻게 할 것인가? 예를 들어, 데이터를 인쇄하는 함수는 새로운 페이지로 진행해야 하는 시기를 결정하기 위해서 이미 프린터로 전송된 줄(line)의 수를 기억할 필요가 있을 것이다. 함수가 호출될 때 계속해서 지역 변수의 값을 보존하기 위해서는 static 키워드를 사용하여 변수를 정적(static) 변수로 정의해야 한다. 예를 들어, 다음과 같이 할 수 있다.

void func1( int x )
{
static int a;
/* 추가적인 코드 */
}

<리스트 12.4>에 있는 프로그램은 자도 변수와 정적 지역 변수 간의 차이를 보여준다.

<리스트 12.4> 자동 변수와 정적 지역 변수 간의 차이를 보여준다.

/* 자동 변수와 정적 지역 변수의 사용 예 */

#incude

 

void func1(void);

 

main()

{

int count;

 

for(count = 0; count < 20; count++)

{

printf("At iteration l%d: ", count);

func1();

}

 

return 0;

}

 

void func1(void)

{

static int x = 0;

int y = 0;

 

printf("x = %d, y = %d\n", x++, y++);

}

 

프로그램은 다음과 같은 결과를 보여준다. 정적 변수로 선언된 x는 함수가 호출될 때 값을 보존하므로 변수의 값은 계속해서 증가한다. 반면에, 자도 변수로 선언된 y는 함수가 호출될 때마다 0으로 다시 초기화된다. 또한, 이 프로그램은 두 가지 형태의 변수에서 초기화가 수행되는 방법의 차이점을 보여준다. 즉, 변수가 정의되는 것과 초기화되는 시기에 대해서 알려준다. 정적 변수는 단지 함수가 처음 호출될 때에만 초기화된다. 나중에 다시 함수가 호출될 때 프로그램은 정적 변수가 이미 초기화되었다는 사실을 기억하고 있으므로 다시 초기화하지 않는다. 변수는 함수가 최근에 실행되었을 때 저장되어 있던 값을 그대로 가지게 된다. 이와는 대조적으로, 자동 변수는 함수가 호출될 때마다 지정된 값으로 다시 초기화된다. 자동 변수를 사용해보면 지금까지 설명한 내용을 충분히 이해할 수 있을 것이다. 예를 들어, <리스트 12.4>에 있는 프로그램에서 두 개의 지역 변수가 초기화되지 않도록 변경하면 함수 func1()은 다음과 같은 내용이 될 것이다.

void func1(void)
{
static int x;
int y;

printf("x = %d, y = %d\n", x++, y++);
}

프로그램을 이렇게 변경하여 실행하면 y의 값은 함수가 호출될 때마다 증가한다는 사실을 알 수 있다. 함수가 호출될 때 y의 값이 계속 보존된다는 것을 알 수 있는 것이다. 그렇다면 지금까지 자동 변수에 대해서 설명했던 내용은 잘못된 것일까? 절대로 그렇지 않다. 지금까지 설명한 내용은 모두 사실이다. 앞에서 설명한 내용과 프로그램의 실행 결과를 보고 알 수 있듯이, 함수가 반복적으로 호출될 동안 자동 변수가 값을 보존하는 것은 단지 우연한 일에 불과하다. 좀더 구체적으로 살펴보자. 함수가 호출될 때마다 새로운 y가 생성된다. 컴파일러는 이전에 함수가 호출되었을 때 사용하던 곳과 동일한 메모리 영역을 새로운 y에 할당하여 사용할 것이다. y가 함수 내에서 분명하게 초기화되지 않는다면 메모리의 저장 영역에는 y가 이전에 가지고 있던 값이 저장되어 있을수 있다. 그래서 변수는 계속해서 값을 보존하는 것처럼 보일 수 있지만, 사실은 우현한 일치에 불과한 것이다. 이렇게 같은 값을 가지게 되는 상황이 항상 발생한다고 볼 수는 없는 것이다.
지역 변수는 기본적으로 자동 변수의 형태를 가지므로 변수를 정의할 때 지정할 필요가 없다 그러나 필요하다면 다음과 같이 변수의 데이터형을 표현하는 키워드 앞에 auto 키워드를 추가할 수 있다.

void func1(int y)
{
auto int count;
/* 그 밖의 프로그램 문장 */
}

3.2 매개 변수의 범위
: 함수의 헤더에서 매개 변수로 사용되는 변수는 지역(local) 변수와 같은 유효 범위를 가진다. 예를 들어, 다음 함수를 살펴보자.

void func1(int x)
{
int y;
/* 그 밖의 프로그램 문장 */
}

x와 y는 모두 함수 func1() 내에서만 유효하게 사용되는 지역 변수이다. 물론, x는 처음에 함수를 호출한 프로그램에서 전달되는 어떤 값을 가진다. 이렇게 전달된 값을 사용할 때에는 x를 다른 어떤 지역 변수와 같은 방법으로 사용할 수 있다. 매개 변수는 항상 대응하는 인수에 의해서 전달되는 값을 가지게 되므로 static이나 auto로 지정하는 것은 아무런 의미가 앖다.

3.3 외부 정적 변수
: 외부 변수를 정의할 때 static 키워드를 포함시키면 정적 변수로 말들 수 있다.

static float rate;
main()
{
/* 그 밖의 프로그램 문장 */
}

일반적인 외부 변수와 정적 외부 변수와의 차이는 유효 범위에 있다 .일반적인 외부 변수는 파일 내에 포함되어 있는 모든 함수에서 유효하고 다른 파일에 포함되어 있는 함수에 의해서도 사용될 수 있다. 정적 외부 변수는 단지 변수가 정의된 파일 내에서 변수가 정의된 부분 이후에 있는 함수에만 유효하다.

3.4 레지스터 변수
: register 키워드는 자동 지역 변수가 메모리 대신에 프로세서의 레지스터에 저장되도록 컴파일러에게 지시하는 데 사용된다. 프로세서의 레지스터(processor register)는 무엇이고 레지스터를 사용할 때의 장점은 무엇일까? 컴퓨터의 CPU(central processing unit)에는 레지스터라는 몇 개의 데이터 저장 영역이 존재한다. 덧셈이나 뺄셈과 같은 실제 데이터 연산은 CPU의 레지스터에서 수행된다. CPU는 데이터를 처리하기 위해서 메모리 내에서 레지스터로 값을 읽어들이고 처리된 결과를 다시 메모리에 저장한다. 데이터를 메모리에서 읽어들이고 다시 메모리로 저장하는 일련의 과정에서는 약간의 시간이 소모된다. 만약 어떤 변수의 값을 레지스터에 저장하여 사용한다면 변수의 값을 더 빨리 사용할 수 있을 것이다.

자동 변수를 정의할 때 register 키워드를 포함시키면 변수를 레지스터에 저장하도록 '요쳥' 할 수 있다. 다음 예제를 살펴보자

void runc1(void)
{
register int x;
/* 그 밖의 프로그램 문장 */
}

앞에서 '요청'이라는 단어를 사용했다는 것에 주의하기 바란다. 프로그램의 상태에 따라 변수의 값을 저장하기 위한 레지스터가 남아 있지 않을 수도 있다. 이때 컴파일러는 변수를 일반적인 레지스터 변수가 아니라 일반적인 자동 변수로 취급할 것이다. register 키워드는 명령하는 것이 아니라 일반적인 자동 변수로 취급할 것이다.

register 키워드는 명령하는 것이 아니라 '요청'하는 것이다 .레지스터 변수의 가장 큰 장점은 순환문의 카운터와 같이 함수 내에서 자주 사용되는 변수에서 증명된다. register 키워는 배열이나 구조체가 아니라 간단한 숫자 변수에 대해서만 사용될 수 있다. 또한 , 정적 변수나 외부 변수로 사용할 수 없다. 레지스터 변수에 대한 포인터를 정의하는 것도 불가능하다.

4. 지역 변수와 main() 함수
: 지금가지 설명한 내용은 모두 main()과 다른 함수에 적용되는 사항이다. 정확히 말하면, main()은 다른 모든 함수와 마찬가지로 하나의 함수이다. main() 함수는 프로그램이 운영체제에서 실행될 때 호출되고 프로그램이 종료될 때 제어는 main()에서 운영체제로 돌아간다. 이것은 main()에서 정의된 지역 변수가 프로그램이 시작될 때 생성되고 프로그램이 종료될 때 사라진다는 것을 뜻한다. 그래서 main() 함수가 호출될 때 값을 보존하는 static 상태의 지역 변수를 생성하는 것은 아무런 의미가 없다.

변수는 프로그램이 실행될 때마다 값을 보존할 수 없다. 결과적으로, main() 함수에서는 자동 변수와 정적 지역 변수의 차이가 없다. main() 내에서는 물론 static 키워드를 사용하여 지역 변수를 정의할 수 있지만 아무런 효과를 나타내지 않는다.

# 잠깐 노트
·main()은 다른 모든 함수와 비슷한 구조를 가지는 하나의 함수라는 사실을 기억하자
·main() 내에서 정적 변수는 아무런 효과도 나타내지 않으므로 정적 변수를 생성한지 않도록 하자.

5. 어떤 형태의 변수를 생성해야 하는가?
: 프로그램에서 특정 변수의 형태를 결정해야 할 때에는 다음 도표를 참고하면 도움이 될 것이다. <표 12.1>에는 C에서 사용할 수 있는 5가지 종류의 변수 형태가 나타나 있다.

<표 12.1> C 5가지 변수 형태 

변수의 형태를 결정할 때에는 가능하다면 자동 변수를 사용해야 하고, 필요한 경우에만 다른 형태를 선택해야 한다. 다음은 몇 가지 참고 사항이다.

·일단 모든 변수를 자동 지역 변수의 형태로 사용하자.
·변수가 자주 사용된다면 register 키워드를 사용하여 레지스터 변수로 정의하자.
·main()이 아닌 다른 함수 내에서 함수가 호출될 때 변수의 값이 보존되어야 한다면 정적 변수로 정의하자.
·변수가 프로그램의 대부분이나 또는 모든 함수에 의해서 사용된다면 외부 변수로 정의하자

6. 지역 변수와 블록
: 지금까지 설명한 내용은 단지 함수 내에서 지역 변수를 사용하는 것을 중심으로 했다. 지역 변수는 기본적으로 함수 내에서만 사용되지만 중괄호 내에 포함된 프로그램의 어떤 블록 내에서도 지역적으로 사용되는 변수를 생성할 수 있다. 블록 내에서 사용되는 변수를 선언할 때에는 가장 먼저 변수가 선언되어야 한다는 것을 기억하자. 예를 들어, <리스트 12.5>를 살펴보자.

<리스트 12.5> 프로그램의 블록 내에서 지역 변수 정의하기

/* 블록 내에서 지역 변수 사용하기 */

#include

 

main()

{

/* main()에 지역적인 변수 정의 */

 

int count = 0;

 

printf("\nOutside the block, count = %d", count);

 

/* 블록의 시작 */

{

/* 블록에 지역적인 변수 정의 */

 

int count = 999;

printf("\nWithin the block, count = %d", count);

}

 

printf("\nOutside the block again, count = %d\n", count);

return 0;

}

 

이렇게 지역 변수를 사용하는 것은 C 프로그래밍에서 흔하지 않고 필요하다고 생각되지도 않을 것이다. 실제로, 이렇게 같은 이름의 변수를 지역적으로 사용하는 방법은 프로그램에서 문제점을 찾는 경우에 가장 많이 사용된다. 즉, 프로그램의 일부분을 괄호 내에 포함시켜 일시적으로 독립시키고 잘못된 내용을 찾기 위해서 지역 변수를 사용할 수 있다. 다른 한 가지 장점은 변수가 사용되는 부분과 가까운 곳에서 변수를 선언하고 초기화할 수 있다는 것이다. 이렇게 하면 프로그램을 이해하기 쉽게 만들 수 있다.

'pc관련 > C언어' 카테고리의 다른 글

C언어_포인터,고급기능  (0) 2019.04.07
고급 프로그램제어문  (0) 2019.04.03
C언어_구조체  (0) 2019.03.25
C언어_문자,문자열  (0) 2019.03.24
C언어_포인터에 대하여...  (0) 2019.03.24
Posted by 둥이파파^^