+ All Categories
Home > Documents > 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의...

응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의...

Date post: 08-Sep-2020
Category:
Upload: others
View: 1 times
Download: 0 times
Share this document with a friend
303
응용컴퓨터프로그래밍
Transcript
Page 1: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

응 용 컴 퓨 터 프 로 그 래 밍

Page 2: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

목 차

1장 프로그램 개발과정 ........................................................1

2장 프로그래밍 기초 .........................................................19

3장 조건문, 제어구조, 함수의 사용법 이해.......................55

4장 배열과 포인터, 동적할당.............................................69

5장 수치계산.......................................................................94

6장 난수, 배열응용...........................................................111

7장 정렬............................................................................128

8장 파일입출력 및 탐색...................................................146

9장 문자열처리.................................................................164

10장 게임..........................................................................203

11장 연결리스트...............................................................252

Page 3: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

1

제1장 프로그램 개발 과정

학습 목표

프로그램의 의미를 이해한다.

프로그램 개발자의 역할을 이해한다.

구조적 프로그래밍 언어의 특징과 제어 구조를 이해한다.

프로그램 개발 절차를 이해한다.

내용

1. 프로그램

2. 프로그래머

2. 구조적 프로그래밍 언어

3. 프로그램 개발 과정

4. 교재의 구성

5. 요약

1.1 프로그램

컴퓨터는 계산을 수행하는 기계이다. 컴퓨터는 수치 계산뿐만 아니라 문자, 음성, 영상 등과 같

은 여러 가지 형태의 데이터도 수치로 변환하여 계산하기 때문에 계산이라는 좁은 의미의 용어

대신에 넓은 의미의 처리(processing)라는 용어를 사용한다. 컴퓨터는 그림 1-1과 같이 데이터를

입력하고, 그 데이터를 처리하고, 처리한 결과를 출력한다. 프로그램은 특정한 문제를 해결하기

위하여 컴퓨터가 실행할 수 있는 최소 단위의 명령어들을 조합하여 컴퓨터에게 지시하는 일련의

작업 명세서이다. 프로그램의 예는 다음과 같다.

<그림 1-1> 프로그램의 기능

문서 편집기

입력: 사용자의 키보드 또는 마우스를 통하여 문자 또는 편집 명령을 입력 받고

처리: 문서를 사용자가 원하는 형태로 편집하고

출력: 화면, 프린터로 출력하거나 파일에 저장한다.

웹브라우저

입력: 사용자의 요청에 의하여 웹 문서를 인터넷으로 입력 받고

처리: 화면에 표시하는 형태로 가공하여

출력: 화면에 웹 문서를 출력한다.

그림 1-2를 참고로 프로그램이 컴퓨터 하드웨어 상에서 실행될 때까지의 과정을 알아보자. 프

Page 4: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

2

로그래머는 컴퓨터의 도움을 받아 해결할 작업을 프로그래밍 언어를 사용하여 프로그램으로 작성

한다. 이것을 소스 프로그램(source program) 혹은 소스 코드(source code)라고 한다. 소스 프로그

램은 프로그래밍 언어가 이해하는 문장들을 처리 순서에 따라 나열한 것으로서 텍스트 문서의 형

태로 만들어져 있다.

컴퓨터의 두뇌 역할을 하는 처리장치(processor)는 자신만 이해할 수 있는 기계어 명령어

(machine instructions)로 구성된 기계어 프로그램만 실행할 수 있다. 그러므로 소스 프로그램은

기계어 프로그램으로 변환(컴파일, compile)되어야 컴퓨터 하드웨어 상에서 실행될 수 있다. 이 동

작을 컴파일러가 담당한다. 프로그래밍 언어와 컴파일러 덕분에 우리는 기계어 명령어를 모르더

라도 프로그램을 개발할 수 있다.

컴퓨터 하드웨어 상에서 실행될 수 있는 상태인 기계어 프로그램은 일반적으로 하드 디스크와

같은 보조기억장치에 저장되어 있다. 사용자는 컴퓨터의 콘솔 화면에서 실행 명령을 입력하거나

마우스로 바탕화면의 아이콘을 더블클릭 하여 컴퓨터에게 기계어 프로그램을 실행하도록 지시한

다. 이 요청에 의하여 컴퓨터는 보조기억장치에 있는 기계어 프로그램을 컴퓨터의 기억장치로 적

재하고 실행한다. 기억장치에 적재되어 실행되고 있는 프로그램의 상태를 프로세스(process)라고

부른다.

<그림 1-2> 프로그램 실행 과정

비주얼 스튜디오와 같은 프로그램 통합개발환경(IDE, Integrated Development Environment)은

소스 프로그램을 편집하고, 컴파일하고, 실행하는 환경을 모두 제공한다. 그렇다면, 프로그램 개발

자 또는 프로그래머의 역할은 해결해야 할 문제를 소스 프로그램으로 변환하는 것이다.

1.2 프로그래머

컴퓨터로 해결하려고 하는 응용 문제가 주어지면, 프로그래머는 이것을 작업 명세서인 프로그

램으로 변환한다. 프로그램은 프로그래밍 언어가 실행할 수 있는 기본 작업 단위인 문장

(statement)을 의미 있는 순서로 나열한 것이다. 프로그래머는 그림 1-3과 같은 순서로 프로그램

을 개발한다.

문제: 프로그램으로 개발하려고 하는 문제를 선택한다. 대부분의 경우에 문제 자체는 프로그

램의 상세한 기능을 포함하지 않는다. 예를 들면, “사용자의 편리성을 높인 웹 브라우저를 개

발하자” 등과 같이 개발 목표만 명시되어 있는 경우가 많다.

요구사항 분석: 먼저 개발하려는 프로그램의 세부적인 기능을 정의한다. 프로그램의 내부적인

Page 5: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

3

처리 과정을 고려하지 않고, 사용자가 시스템을 사용하는 사례(use case)를 모두 나열한다. 모

든 사용사례에 대하여 프로그램이 처리할 입력과 산출할 출력을 문서로 정리한다.

프로그램 설계: 요구사항 분석 결과를 바탕으로, 프로그램이 처리해야 할 입력 데이터와 출력

데이터를 결정한 후, 입력 데이터로부터 출력 데이터를 찾아내고, 출력 데이터를 유도하는 처

리 절차를 설계한다.

프로그램 구현: 설계한 처리 절차를 프로그래밍 언어가 제공하는 문장으로 변환한다.

<그림 1-3> 프로그램 개발 과정

프로그램을 개발하는 작업은 결국 해결하고자 하는 문제를 프로그래밍 언어가 제공하는 문장으

로 변환하는 것이다. 이를 위하여 프로그래머에게 요구되는 자질에 대하여 알아보자.

프로그래머가 아니더라도 요구사항을 분석하는데 문제가 없다. 응용 문제는 프로그램의 개념을

포함하고 있지 않은 것이 일반적이다. 오히려 해당 응용 분야에 대한 지식을 더 많이 갖고 있는

사람이 요구사항을 더 잘 분석할 수 있다.

프로그램 설계 단계의 결과물은 입출력 데이터를 표현하기 위한 자료구조와 처리 절차를 표현

하는 알고리즘의 형태로 나타난다. 이러한 결과물은 컴퓨터가 수행할 수 있는 기본적인 능력의

조합으로 표현된다. 따라서, 프로그래머는 컴퓨터의 기본적인 기능을 알고 있어야 한다. 컴퓨터의

기능은 특정 프로그램 언어에 한정적인 것이 아니고, 모든 프로그래밍 언어에 대하여 공통적이다.

이 부분을 “1.3 구조적 프로그래밍 언어”에서 다룬다. 프로그램 언어의 기능에 대하여 알고 있다

면, 프로그램 설계 단계는 응용 분야의 개념을 프로그램 언어의 개념으로 변환하는 것으로 생각

할 수 있다. 이 과정은 상당한 경험과 노하우를 필요로 하며, 이에 대하여 “1.4 프로그램 개발 과

정”에서 설명한다.

프로그래머는 여러 가지 프로그래밍 언어 중 하나를 선택하여 설계 단계의 결과물인 자료구조

와 알고리즘을 소스 프로그램으로 구현한다. 따라서, 프로그래머는 자신이 선택한 프로그래밍 언

어에 대한 기능과 문법을 알고 있어야 한다. 우리는 C 언어를 사용하여 프로그램을 구현할 것이

다. 이 교재로 학습하는 학생들은 대부분 C 언어에 익숙하지 않을 것이므로 설계 결과를 프로그

램으로 구현하는데 필요한 C 언어 기능과 문법을 먼저 설명할 것이다. 그렇지만, 관련 주제를 아

주 상세하게 설명하지 않고, 소스 프로그램을 작성하기 충분할 정도만 설명한다. 해당 주제에 대

한 C 언어의 기능을 더 자세히 알고 싶다면, 문법 교재를 곁에 두고 필요할 때마다 찾아 보기를

권한다.

1.3 구조적 프로그래밍 언어

컴퓨터가 발전하면서, 프로그래밍 언어도 함께 발전하고 있다. 프로그램 언어는 응용 프로그램

Page 6: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

4

을 프로그램 언어로 설계하고 조직화 하기 쉬운 방향으로 발전하고 있다. 요즈음 주로 사용되는

프로그램의 개념은 구조적 프로그래밍 기법(structured programming technique)과 객체지향 프로

그래밍 기법(object-oriented programming technique)이다. C 언어가 구조적 프로그래밍 언어의 대

표적인 것이고, C++와 Java가 객체지향 프로그래밍 언어이다. 객체지향 언어는 구조적 언어 이후

에 개발된 것으로, 기본적인 문법과 제어구조는 구조적 언어를 그대로 따르고 있다. 다만 구조적

프로그램 개념 위에 객체의 개념을 도입하여 프로그램의 구조를 체계적으로 표현하도록 기능을

강화한 것이다.

이 절에서는 프로그램을 설계하기 위하여 기본적으로 알고 있어야 할 구조적 프로그래밍 언어

의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고, 프로그램을 설계할 때 반드

시 알고 있어야 할 주요 특징들만 간단히 다룬다. 여기에서 설명하는 내용은 C 언어에만 해당되

는 것이 아니고, 모든 프로그래밍 언어에 대하여 공통적이다. 특별히 C 언어에 관련된 내용을 설

명할 때는 명시적으로 C 언어에 해당하는 것이라고 밝힐 것이다. 주제는 다음과 같다.

변수

수식과 문장

제어 구조

함수

C 프로그램의 구조

참고 자료

1.3.1 변수

프로그램은 데이터를 처리한다. 프로그램이 처리하는 데이터의 종류는 상수(constant)와 변수

(variable)로 나눌 수 있다. 수학의 수식과 마찬가지로 상수는 값을 변경할 수 없는 데이터이고, 변

수는 값을 변경할 수 있는 데이터이다. 예를 들어 다음과 같은 수식에서, 3.14는 상수이고, area와

radius는 변수이다.

area = 3.14 radius radius

수학의 표기법과 마찬가지로 프로그래밍 언어는 숫자를 적어 상수를 표현하고, 이름을 부여하

여 변수를 표현한다. 프로그램이 실행될 때, 상수와 변수는 모두 컴퓨터의 기억장치에 할당된다.

프로그램은 상수를 할당한 기억장치의 값을 변경할 수 없지만, 변수를 할당한 기억장치의 값을

변경할 수 있다. 프로그래밍 언어에서 변수의 특징은 다음과 같다.

변수는 이름이 있다. 변수의 이름은 그 변수가 저장하고 있는 데이터의 값을 의미한다.

변수에 대하여 자료형(data type)을 지정하여야 한다.

프로그래머는 영문자와 숫자를 조합하여 변수의 이름을 만들 수 있으나, 반드시 영문자로 시작

해야 한다. 변수의 이름은 그 의미를 쉽게 알 수 있도록 정하는 것이 좋다.

Page 7: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

5

컴퓨터는 데이터를 이진수로 변환하여 기억장치에 저장하고 한 개의 데이터를 저장하는데 한정

된 기억공간을 할당한다. 한정된 기억공간에 수를 표현하기 때문에 컴퓨터는 이론적으로 무한한

수를 모두 표현할 수 없다. 따라서, 컴퓨터는 소수점이 없는 정수와 소수점이 있는 실수로 구분하

고, 이 두 가지에 대하여 다른 표현 방법을 사용한다. 컴퓨터가 수를 표현하는 기억공간의 크기와

표현 방법의 조합을 자료형(data type)이라고 한다. C 언어는 다음과 같은 자료형을 제공한다.

문자형(char): 1 바이트를 할당하여 정수를 저장한다. 주로 영문자 한 글자를 저장하기 위하여

사용되지만, -127~128 범위의 정수를 저장하기도 한다.

짧은 정수형(short): 2 바이트를 할당하여 정수를 저장한다.

정수형(int): 4 바이트를 할당하여 정수를 저장한다.

실수형(float): 4 바이트를 할당하여 실수를 저장한다.

배정도 실수형(double): 8 바이트를 할당하여 실수를 저장한다.

프로그래머는 변수를 정할 때 반드시 변수의 자료형도 함께 지정해야 한다. 컴퓨터는 변수를 할

당하는데 기억공간을 얼마나 할당할지 그리고 어떻게 수를 표현할지 결정할 수 있기 때문이다.

1.3.2 수식과 문장

수식(expression)은 여러 가지 연산자를 사용하여 상수 또는 변수의 값으로부터 새로운 값을 계

산하는 것을 표현한다. C 언어에서 사용할 수 있는 연산자의 종류는 다음과 같다.

산술 연산자: 덧셈(+), 뺄셈(-), 곱셈(*), 나눗셈(/), 나머지 연산(%)

증감 연산자: 정수에 대한 1 증가(++), 1 감소(--)

관계 연산자: 값을 서로 비교. 같다(==), 크다(>), 작다(<), 같지 않다(!=), 크거나 같다(>=), 작

거나 같다(<=)

논리 연산자: 논리합(OR, ||), 논리곱(AND, &&), 논리역(NOT, !)

직관적으로 알 수 있는 연산자 이외의 연산자의 사용 방법은 필요할 때 설명하기로 한다. C 언

어에서 =의 의미는 수학의 =과 의미가 다르다. C 언어에서 = 기호는 기호 오른쪽에 있는 수식을

계산하여 그 결과를 왼쪽에 있는 변수에 저장하라는 의미이다. 그러므로, = 기호의 왼쪽에는 항상

값을 저장할 수 있는 변수가 있어야 한다. = 기호를 할당 연산자(assignment)라고 하고, “변수 =

수식” 형태의 문장을 할당문이라고 한다.

프로그래밍 언어의 문장(statement)는 프로그램을 구성하는 명령문의 기본 단위이다. C 언어는

할당문 뒤에 세미콜론(;)을 붙여 문장을 표현한다. C 언어 문장의 예는 다음과 같다.

x = 1; // 변수 x에 1을 저장한다.

z = x + y; // 변수 z에 변수 x와 y의 값을 더하여 저장한다.

dist = speed * time; // 속도(speed)와 시간(time)을 곱하여 거리(dist)에 저장한다.

프로그램의 문장은 할당문 이외에 프로그램의 실행 순서를 변경하는 제어 구조와 함수 호출을

Page 8: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

6

포함한다. 다음 절부터 이 주제에 대하여 설명한다.

1.3.3 제어 구조

프로그램은 일반적으로 나열되어 있는 문장들을 순차적으로 실행하지만 프로그램의 실행 순서

를 변경할 수도 있다. 프로그램의 실행 순서를 결정하는 것을 제어 구조 혹은 흐름 제어라고 한

다. 프로그램의 제어 구조의 종류는 그림 1-4와 같이 순차형, 선택형, 반복형이 있다.

<그림 1-4> 프로그램 제어 구조

특별히 제어 구조라고 할 것도 없는 순차형은 그림 1-4(a)와 같이 프로그램에 나열된 문장을

차례대로 실행하는 형태를 말한다. 한 개 이상의 문장이 모여 있는 것을 복합문(composite

statement) 또는 프로그램 블록(block)이라고 한다. 순차형 프로그램의 예는 다음과 같다.

1. x = 1; // 변수 x에 1을 저장한다.

2. y = 3; // 변수 y에 3을 저장한다.

3. z = x + 2*y; // 변수 z에 x + 2y를 계산하여 저장한다.

선택형은 그림 1-4(b)와 같이 먼저 조건을 검사하고, 조건이 참(true)일 때 수행할 작업과 거짓

(false)일 때 수행할 작업을 선택하여 수행할 수 있는 구조이다. 만일 조건이 참인 경우에 블록 A

만 수행하고, 거짓인 경우에 블록 B만 수행한다. 경우에 따라 거짓에 해당하는 처리는 없을 수도

있다. 선택형 프로그램의 예는 다음과 같다.

1. 만일 신호등이 파란색이면 // 참에 해당하는 처리만 있는 경우

2. 길을 건너라.

1. 만일 가위바위보에서 이겼다면 // 조건이 참이면

2. 뽕 망치를 들고

3. 상대방의 머리를 가볍게 쳐라.

4. 아니면 // 조건이 거짓이면

5. 냄비를 들어

6. 머리를 막아라.

Page 9: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

7

반복형은 그림 1-4(c)와 같이 조건이 참이면 프로그램 블록을 계속 실행하는 형태이다. 조건이

거짓으로 판정될 때 반복형에서 벗어난다. 반복형 구조를 프로그램 루프(loop)라고도 부른다. 반복

형은 다시 그림 1-5와 같이 세 가지 형태로 세분화 된다..

<그림 1-5> 반복문의 형태

while 형은 먼저 조건을 검사한다. 조건이 거짓으로 판정될 경우, 블록을 한 번도 실행하지 않

을 수도 있다. do-while 형은 프로그램 블록을 먼저 실행하고 나서 조건을 검사한다. 그러므로 프

로그램 블록을 최소 한 번 실행한다. for 형은 루프를 제어하기 위하여 초기화, 조건, 증감 세 가

지를 표현한다. 먼저 초기화를 실행 한 후, 조건을 검사하고 조건이 참이면 동안에 블록을 실행한

다. 블록을 실행한 다음에 조건에 영향을 주는 증감식을 실행한다. 이 방법은 프로그램 블록을 반

복하는 횟수가 일정한 경우에 편리하다. 반복문의 예는 다음과 같다.

1. 사용자로부터 정수를 입력 받는다. // 라인 1과 라인 2는 순차형

2. 만일 2보다 큰 정수를 입력하였다면 다음을 반복한다. // while 형

3. 정수가 소수인지 판정하고

4. 그 결과를 출력한다.

5. 다음 데이터를 입력 받는다.

1. 다음을 반복한다. // do-while 형

2. 사용자로부터 정수를 입력 받는다.

3. 사용자가 입력한 수의 합을 구한다.

4. 합이 100보다 작으면 라인 1부터 반복한다.

1. n=1부터 n<=100일 때까지 n을 증가시키면서 // for 형

2. n2을 구하고

3. sum에 n2을 더한다.

지금까지 설명한 순차형, 선택형, 반복형의 처리 구조에 전체를 감싸는 사각형을 그려 놓고 보

면 제어 구조로 들어가는 입구가 하나이고 출구도 하나이다. 그러므로, 프로그램을 기능 단위로

구분하여 생각하기 편리하다. 그림 1-4와 그림 1-5에 표시된 프로그램 블록이 다른 제어 구조를

포함하고 있을 수 있다. 그림 1-6은 제어 구조가 중첩되어 있는 예이다. 프로그램 블록 안에는 얼

Page 10: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

8

마든지 형태가 같거나 다른 제어 블록이 반복적으로 들어갈 수 있다. 그렇지만, 중첩 단계가 많아

질수록 프로그램의 처리 과정을 이해하기가 불편해진다. 일반적으로 프로그램을 설계할 때 중첩

단계를 2 또는 3 단계 이하로 유지하는 것이 바람직하다.

<그림 1-6> 제어 구조의 중첩 예

1.3.4 함수

함수(function)란 특정 작업을 수행하도록 설계된 독립적인 프로그램이다. 규모가 큰 프로그램을

작성하는 경우에, 프로그램을 기능 단위로 분해하고 각 기능을 함수로 나누어 작성하면 편리하다.

프로그램은 한 개 이상의 함수로 구성되며, 반드시 한 개의 main() 함수가 존재해야 한다. 프로그

램은 실행될 때 main() 함수의 첫 번째 문장부터 차례대로 실행한다. 함수의 특징은 다음과 같다.

함수는 서로 구별되는 이름을 갖는다. 이름을 만드는 규칙은 변수 이름 규칙과 같다.

함수는 특정한 작업을 수행한다.

함수는 파라미터를 통하여 데이터를 입력 받을 수 있고 실행 결과를 리턴할 수 있다.

함수는 함수 이름으로 시작하고 리턴 문장으로 끝난다. 함수도 프로그램의 흐름을 변경하는 제

어 구조의 일종이다. 프로그램 문장 중에 함수의 이름을 적으면, 그 함수가 호출(function call)되고

실행된다. 수식을 연산하는 문장 이외에 함수 호출도 프로그램 문장의 기본 단위이다. 그림 1-7은

함수를 호출하는 과정을 보여준다.

1. 프로그램이 시작되면, main() 함수의 문장1부터 차례대로 문장을 실행한다.

2. main() 함수의 라인 2에서 func1()을 호출한다.

3. 프로그램은 이제 func1()의 문장1부터 차례대로 문장을 실행한다.

4. func1()의 라인 2에서 func2()를 호출한다.

5. 프로그램은 func2()의 문장을 차례대로 실행한다.

6. func2()의 실행이 끝나면 자신을 호출하였던 func1()의 라인2로 리턴한다.

7. func1()은 라인 3부터 다시 차례대로 문장을 실행하고 라인5에서 리턴한다.

Page 11: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

9

8. main() 함수는 문장2, 3을 계속 실행하고 라인 5에서 func1()을 다시 또 호출한다.

9. 3 단계부터 7 단계를 다시 또 실행하고 리턴한다. 이번에는 main() 함수의 라인 5에서

func1()을 호출하였으므로, 라인 5로 리턴한다.

10. main() 함수는 라인 6의 문장 4를 실행하고, 프로그램을 종료한다.

<그림 1-7> 함수 호출과 리턴

함수는 프로그램을 기능 단위로 모듈화 함으로써 프로그램을 설계하기 편리하고 이해하기 쉽게

만들어주는 장점을 제공한다. 특히, 큰 문제를 작은 문제로 분해하여 문제를 해결하는 하향식

(top-down) 프로그램 개발 방식에 적합한 방법이다. 프로그램을 개발할 때 다음과 같은 부분을

함수로 분리하는 것이 좋다.

프로그램에서 여러 번 사용되는 부분을 독립된 함수로 분리한다.

프로그램에서 한 번 사용되더라도 기능이 명확하게 구분된다면 독립된 함수로 분리한다.

한 개의 함수를 구현한 소스 코드가 너무 길다면, 함수로 분리하는 것을 고려한다.

함수의 종류는 다음과 같이 두 가지가 있다.

라이브러리 함수(library function): 컴파일러가 제공하는 함수

사용자 정의 함수(user defined function): 사용자가 필요에 의하여 직접 만드는 함수.

컴파일러는 사용자들이 많이 사용할 것이라고 생각되는 기능을 미리 함수로 만들어 사용자에게

제공한다. 우리가 프로그램을 작성하면서 “이런 기능은 컴파일러가 제공하면 좋겠는데?”라고 생각

되는 기능은 대부분 라이브러리 함수에 포함되어 있다. 라이브러리 함수의 예는 입출력 함수, 파

일 관리 함수, 기억장치 관리 함수, 문자열 처리 함수, 수치계산 함수, 시간 측정 및 관리 함수 등

이 있다. 컴파일러가 어떤 기능의 라이브러리를 제공하는지 알고 있는 것이 프로그램 개발 능력

을 향상시키는 방법 중 하나이다. 시간이 있을 때, 라이브러리 함수 목록을 읽어보면 많은 도움을

받을 수 있다. “3.6 참고 자료”에서 라이브러리에 대한 정보를 찾아보는 방법을 설명한다.

1.3.5 C 프로그램의 구조

C 프로그램은 그림 1-8과 같이 선행처리기, 함수 및 전역변수 선언, 한 개의 main() 함수, 그리

고 0 개 이상의 함수들로 구성된다. 컴파일러는 소스 코드를 컴파일하기 전에 선행처리기

(preprocessor) 문장을 먼저 처리한다. 선행처리기 문장은 # 기호로 시작하고 마지막에 세미콜론(;)

Page 12: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

10

을 붙이지 않는다. 선행처리기의 종류는 #include와 #define 문이 있다.

<그림 1-8> 전형적인 C 프로그램 구조

#include 문의 일반 형식은 다음과 같고, 프로그램을 컴파일하기 전에 #include 문으로 지정한

파일을 소스코드에 포함하여 컴파일하라는 의미를 갖고 있다.

#include <파일이름>

#include 문으로 헤더 파일과 소스 파일을 포함시킬 수 있다. 헤더 파일의 이름은 확장자가 .h이

고, 헤더 파일은 컴파일러가 제공하는 라이브러리 함수 또는 기호 상수를 정의하고 있다. 따라서,

라이브러리 함수를 사용하려면 해당 함수를 정의하고 있는 헤더 파일을 찾아 소스 파일에 포함시

켜야 한다.

#define 문은 기호화된 상수를 정의하거나 간단한 수식을 매크로(macro) 함수로 정의한다.

#define 문은 다음의 예와 같이 두 개의 항으로 구성되어 있으며, 컴파일러는 소스 코드를 컴파

일하기 전에 프로그램에 나타난 첫째 항을 둘째 항으로 교체한 후 컴파일 한다. 예를 들면, 소스

코드에 나타나는 모든 PI를 3.141592로 교체한 후 컴파일 한다.

#define PI 3.141592 // 기호 상수 정의

#define AVG(x, y) (((x)+(y))/2) // 매크로 함수 정의

함수의 이름과 변수의 이름을, 서로 구별되는 이름을 부여하여야 한다는 의미로, 식별자

(identifier)라고 부른다. C 프로그램에서는 식별자를 사용하기 전에 반드시 선언해야 한다. 함수를

선언할 때는 리턴하는 데이터의 형, 함수의 이름, 그리고 함수가 입력으로 받는 파라미터 목록을

Page 13: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

11

함께 표현해야 한다. 변수의 종류는 전역 변수(global variable)과 지역 변수(local variable)이 있다.

전역 변수는 프로그램 전체에서 사용할 수 있는 변수이고, 프로그램이 실행되는 동안 항상 기억

장치에 존재한다. 지역 변수는 그것을 선언한 함수 안에서만 사용할 수 있는 변수이고, 지역 변수

를 선언한 함수가 실행될 때 기억장치에 할당되었다가 함수의 실행이 종료되면 기억장치에서 제

거된다. 리스트 1-1은 C 프로그램 구조를 이해하기 위하여 제시한 프로그램의 예이다. 프로그램이

수행하는 일을 정확하게 해석하기 보다는 프로그램의 실행 순서를 이해하도록 하자.

<리스트 1-1> C 프로그램의 예

1. #include <stdio.h> // printf(), scanf()를 정의하고 있는 헤더 파일 포함

2. #define ERR “A rectangle can’t be formed\n” // 매크로 정의

3. int get_data(schar *str); // 함수 선언

4. int calc_area(int width, int height); // 함수 선언

5. int width, height, area; // 전역 변수 선언

6. void main() // main() 함수 정의

7. {

8. width = get_data(“폭을 입력하세요: “); // 함수 호출

9. height = get_data(“높이를 입력하세요: “); // 함수 호출

10. if ((width <= 0) || (height <= 0)) // 조건문

11. printf(ERR); // 함수 호출

12. else // 조건문

13. {

14. area = calc_area(width, height); // 사용자 정의 함수 호출

15. printf(“%d\n”, area); // 결과 출력

16. }

17. }

18. int get_data(char *str) // 사용자 정의 함수 구현

19. {

20. int data; // 지역 변수 선언

21. printf(“%s”, str); // 화면 출력

22. scanf(“%d”, &data); // 데이터 입력

23. return data; // 리턴문

24. }

25. int calc_area(int width, int height) // 사용자 정의함수 구현

26. {

27. int rect_area; // 지역 변수 선언

28. rect_area = width * height; // 연산문

Page 14: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

12

29. return rect_area; // 리턴문

30. }

프로그램은 main() 함수의 첫 번째 실행 문인 라인 8부터 시작한다.

라인 8: get_data() 함수를 호출하였으므로, 라인 21~23을 실행하고 리턴한다. 함수가 리턴한

데이터가 변수 width에 저장된다.

라인 9: 마찬가지로 get_data() 함수를 호출하고, 함수가 리턴한 값을 변수 height에 저장한다.

라인 10~16: 선택형 제어 구조이다.

라인 13~16: 제어 구조의 블록에 포함되는 문장이 한 개 이상이면 시작과 끝을 { }로 표시해

야 한다.

라인 14: calc_area() 함수를 호출하면서 파라미터로 변수 width와 height의 값을 전달한다. 함

수는 라인 28~29를 실행하고 리턴한다. 리턴한 결과는 변수 area에 저장된다.

1.3.6 참고 자료

프로그램을 설계한 후에 소스 코드로 구현할 때 특정 기능을 제공하는 라이브러리 함수를 찾아

보아야 하는 경우가 자주 발생한다. 요즈음은 인터넷이 발달되어 있기 때문에 프로그래밍 언어에

대한 상세한 설명을 제공하는 블로그도 많이 있으며 관련 주제에 대한 강의자료도 쉽게 찾아볼

수 있다. 공식적인 C 언어 라이브러리 사용설명서(library reference)는 www.cplusplus.com에서 제

공한다. 이 웹 사이트는 C 언어뿐만 아니라 C++ 언어에 대한 참고자료도 함께 제공한다. 그림 1-

9와 같이 이 사이트에 접속하여 검색창에 라이브러리 함수의 이름을 입력하면 그 라이브러리 함

수에 대한 정보를 얻을 수 있다.

<그림 1-9> C 라이브러리 함수 사용 설명서 홈 사이트

이 웹 사이트는 라이브러리 함수에 대하여 다음과 같은 정보들을 제공한다.

Page 15: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

13

함수의 이름

함수의 원형

함수의 기능

파라미터 의미

리턴 값

사용 예

함수와 유사한 함수

이와 같은 사용자 설명서(user manual)의 단점은 용어 설명이 없다는 것이다. 사용자는 기본적

인 용어를 충분히 알고 있어야 웹 사이트에서 제공하는 설명을 쉽게 알아볼 수 있다.

1.4 프로그램 개발 과정

프로그램으로 해결해야 할 문제가 주어졌을 때, 아무런 준비 과정을 거치지 않고 바로 프로그

램의 소스 코드를 작성하는 것은 불가능하다. 프로그램을 개발한다는 것은 응용 문제에 포함되어

있는 개념을 컴퓨터가 문제를 해결할 수 있도록 프로그래밍 언어로 변환하는 과정이다. 그림 1-9

는 프로그램 개발 과정을 도시한 것이다. 프로그램 개발 과정은 매우 중요한 주제이고, 기초적인

프로그램 기법을 학습한 후에 별도의 교육 과정을 통하여 체계적으로 학습할 필요가 있다. 이 절

에서는 구조적 프로그래밍 언어에 적합한 프로그램 개발 과정을 설명한다. 2장부터 제시되는 프로

그램 문제들에 대하여 이 절에서 설명하는 개발 과정에 따라 프로그램을 구현할 것이다.

<그림 1-10> 프로그램 개발 과정

컴퓨터로 해결해야 할 문제가 주어질 때, 프로그램 개발 과정이 시작된다. 누구나 일상 생활에

서 컴퓨터의 도움을 받으면 생활이 편리해질 수 있는 과제를 프로그램 문제로 도출할 수 있다.

문제 자체는 컴퓨터 프로그램의 개념을 전혀 포함하고 있지 않을 수 있고, 프로그램으로 구현하

Page 16: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

14

기 위한 충분한 정보도 포함하고 있지 않을 수 있다. 다만 해결해야 할 문제의 목표만 제시할 뿐

이다. 프로그램 개발 과정의 예제로서, 매우 간단하지만, “사각형의 면적을 계산하라”는 문제를 가

지고 프로그램 개발 단계를 설명한다.

1.4.1 요구사항 분석

프로그램으로 구현해야 할 문제의 요구사항을 정리하는 단계이다. 이 단계에 아직 프로그램의

개념은 도입되지 않으며, 제시된 문제를 분석하여 프로그램으로 구현해야 할 부분을 글 또는 그

림으로 표현하는 단계이다. 프로그램 자체를 블랙 박스로 생각하고, 즉, 프로그램의 내부적인 처

리를 고려하지 않고, 프로그램의 외부적인 모습을 표현한다. 문제가 간단한 경우, 제시된 문제만

으로 쉽게 문제가 파악되지만, 문제가 복잡해질수록 요구사항을 분석하는 과정이 복잡해진다. 사

용자가 프로그램에 요구하는 기능이 한 가지인 경우도 있지만 여러 가지를 요구할 수도 있기 때

문이다.

처리하려는 입력 데이터에 대하여 어떤 출력이 도출되어야 하는지 분석하는 사용 사례(use

case)를 표현하는 것이 중요하다. 간단한 경우 사용 사례는 입력 데이터와 출력 데이터를 표시하

는 입출력 설계(또는 화면 설계)로 충분한 경우도 있다. 이 단계의 결과물은 사용자가 프로그램을

사용하여 제공하는 입력 데이터와 프로그램이 산출해야 하는 출력 데이터를 구체적으로 기술한

문제 정의서이다. 사용자는 프로그램에 여러 가지 데이터를 입력할 수도 있으며, 이 경우에는 각

각에 대한 출력을 정의해야 한다. 2장부터 제시하는 문제들은 비교적 간단하고 또한 텍스트 기반

의 콘솔 화면을 사용하여 입출력 하기 때문에 사용 사례라는 용어 대신에 문제 정의와 입출력 설

계를 제시하도록 한다.

사각형의 면적을 계산하려면 사각형의 폭과 높이가 필요하므로, 문제 정의서는 “사용자로부터

사각형의 폭과 면적을 입력 받고 면적을 계산하여 출력한다.” 정도만으로 충분하다. 그리고 다음

과 같이 입력 데이터와 출력데이터의 예시를 사용하여 화면 설계를 제시한다.

입력 화면 출력화면

사각형의 폭을 입력하세요: 10

사각형의 높이를 입력하세요: 5

사각형의 면적 = 50

1.4.2 프로그램 설계

이 단계는 문제를 프로그램으로 구현할 수 있도록 컴퓨터 프로그램의 개념을 도입하여 문제해

결 방법을 설계하는 과정이다. 이 단계에서 수행하는 일은 아직까지 특정 프로그램 언어와 관련

이 없으며, 일반적인 글 또는 그림으로 표현되어 있는 요구사항 분석 단계의 결과물을 기반으로

최종적으로 다음과 같은 결과를 도출하는 것이 이 단계의 목표이다.

자료구조 설계: 입력 데이터와 결과를 저장할 변수를 정의한다.

문제해결 방법 설계: 입력 데이터로부터 출력을 유도하는 방법을 설계한다.

Page 17: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

15

알고리즘 설계: 프로그래밍 언어로 문제해결 방법을 산출할 수 있도록 처리 절차를 기술한다.

컴퓨터는 데이터를 변수에 저장한다. 그러므로 프로그램이 처리할 데이터를 저장할 변수의 자

료형(data type)과 이름을 결정할 필요가 있다. 변수는 자료형에 따라 표현할 수 있는 수의 상세

도와 범위가 다르기 때문에 자료구조를 정할 때 변수의 자료형도 함께 결정해야 한다. 프로그램

은 다음과 같은 변수를 사용하게 된다.

문제 정의서에 표현되어 있는 입출력 데이터를 저장할 변수

내부적으로 프로그램을 실행하면서 프로그램의 흐름을 제어하거나 중간 결과를 저장할 변수

첫 번째에 해당하는 문제 정의서에서 쉽게 변수를 결정할 수 있으나, 두 번째에 해당하는 변수는

문제해결 방법과 알고리즘을 설계할 때 필요한 변수가 찾아진다. 프로그램 설계 초기에는 입출력

데이터를 저장할 변수만 정의하고, 문제해결 방법을 설계하면서 그 때 필요한 변수를 도출하는

것이 좋다. 그리고 프로그램 전체에 영향을 주는 변수는 전역 변수로 선언하고, 세부적인 처리 과

정에 사용되는 변수는 지역 변수로 선언하여 사용하는 것이 좋다.

예를 들어, 사각형 면적 계산 문제에서 필요한 변수를 다음과 같이 결정할 수 있다.

정수형 변수 width: 사각형의 폭 저장

정수형 변수 height: 사각형의 높이 저장

정수형 변수 area: 사각형의 면적 저장

만일 사용자가 사각형의 폭과 높이를 소수점이 있는 실수로 처리할 필요가 있다면, 위 변수들을

실수형으로 정의하여야 한다. 그리고 변수의 이름을 정할 때, 변수의 이름만으로 그 변수가 저장

하고 있는 값의 의미를 알 수 있도록 적절한 단어를 선택하는 것이 바람직하다.

입출력 변수를 결정한 다음에 입력 데이터로부터 출력 데이터를 유도하는 방법을 설계한다. 입

력 값으로부터 결과 값을 산출하는 방법은 프로그램 언어와 관련이 없으며, 대부분의 경우 입력

데이터로부터 직접 출력 데이터를 유도할 수 없는 경우가 많다. 이런 경우에는 중간 결과를 산출

하기 위한 절차를 기술해야 하며, 중간 결과를 저장하기 위한 변수를 결정해야 한다. 문제 해결

방법을 수식으로 표현할 수 있다면 가장 간단한 문제에 해당한다. 사각형 면적 계산의 경우는 다

음과 같은 수식으로 결과를 도출할 수 있다.

area = width height

결과값을 산출하는 방법을 알았다면, 그 다음 단계는 프로그램이 시작될 때부터 데이터를 입력

받고 결과를 산출하기까지의 처리 절차, 즉 알고리즘을 기술하는 과정이다. 알고리즘을 표현하는

대표적인 방법은 순서도(flowchart)와 가상 언어 또는 의사 코드(pseudo-code)가 있다. 순서도는

그림 1-10과 같이 프로그램의 흐름을 그림으로 표현하는 방법이다. 가상 언어 표현은 사람이 사

용하는 언어와 컴퓨터 프로그래밍 언어의 중간쯤 되는 언어이며, 프로그램의 처리 과정을 사람이

알아보기 쉽게 일상 언어로 기술한 것이다. 초창기에는 순서도를 많이 사용하였으나, 요즈음은 가

Page 18: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

16

상 언어 표현을 더 많이 사용하고 있다. 가상 언어로 표현하는 데 표현 방법에 특별한 제한이 없

고, 일상에서 사용하는 언어를 사용하여 알아보기 쉽게 표현하면 된다.

모든 프로그램은 데이터를 입력하고, 입력 데이터를 처리하고, 결과를 출력하는 과정을 거친다.

그러므로 알고리즘을 설계할 때도 이 순서로 생각하는 것이 좋다. 사각형 면적 계산 문제는 간단

하기 때문에 다음과 같이 간단하게 알고리즘을 표현할 수 있다.

<리스트 1-2> 사각형 넓이 계산 가상 언어 표현

1. 폭을 입력 받는다. // 데이터 입력

2. 높이를 입력 받는다.

3. 만일 폭과 높이가 모두 양수가 아니면

4. 입력이 잘못되었다고 출력한다.

5. 그렇지 않다면

6. 면적 = 폭 높이를 계산한다. // 데이터 처리

7. 면적을 출력한다. // 결과 출력

알고리즘을 설계하는 과정은 문제를 분해하여 프로그램 언어로 기술할 수 있도록 문제를 작은

문제들의 조합으로 표현하는 과정이다. 프로그램은 함수 그리고 순차형, 선택형, 반복형 제어 구

조의 조합으로 구성되므로, 이러한 제어 구조를 사용하여 해결하고자 하는 문제에 대한 처리 절

차를 가성 언어로 표현한다. 최종적으로 프로그램 언어가 제공하는 문장으로 표현될 수 있을 때

까지 알고리즘을 상세하게 표현되어야 한다. C 언어와 같은 구조적 프로그래밍 언어는 하향식

(top-down) 방식으로 설계하기 적합한 언어이다.

하향식 설계 방법은 문제가 포함하고 있는 기능을 단계적으로 구체화 해 나가는 설계 방법이다.

설계 초기에 아직까지 구체화 되지 않은 기능을 함수로 구현하고, 함수를 다시 또 구체화한다. 최

종적으로 설계된 함수는 프로그래밍 언어로 표현될 수 있어야 한다.

1.4.3 프로그램 구현

프로그램 설계가 완료되면, 설계한 알고리즘을 프로그래밍 언어를 사용하여 소스 코드로 변환

하고 디버깅 한다. 사각형 면적 계산 프로그램을 C 언어로 변환하면 다음과 같다.

<리스트 1-3> 사각형 면적 계산 프로그램

1. #include <stdio.h> // 표준 입출력함수를 정의하고 있는 헤더 파일

2. int main()

3. {

4. int width, height, area; // 변수 선언

5. printf(“사각형의 폭을 입력하세요: “); // 안내문 출력

6. scanf(“%d”, &width); // 폭 입력

7. printf(“사각형의 높이를 입력하세요: “); // 안내문 출력

Page 19: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

17

8. scanf(“%d”, &height); // 넓이 입력

9. if ((width < 0) || (height < 0)) // 조건 검사

10. printf(“음수를 입력했으므로, 면적을 구할 수 없습니다.\n”);

11. else

12. {

13. area = width * height; // 데이터 처리

14. printf(“사각형의 면적 = %d\n”, area); // 데이터 출력

15. }

16. return 0;

17. }

프로그램 크기가 작은 경우는 한 번에 프로그램 전체를 소스 코드로 변환할 수 있지만, 프로그

램의 크기가 커서 여러 개의 기능들로 구성되어 있는 경우에 각 기능 단위로 소스 코드를 작성하

고 디버깅 해 나가는 것이 더 바람직하다.

본 교재는 C 프로그램 개발 환경으로 PC 윈도우 기반의 환경에서 마이크로소프트사의 비주얼

스튜디오(Visual Studio)를 사용한다. 비주얼 스튜디오를 사용하는 방법은 일반 C 언어 교재를 참

고하기 바란다.

1.4.5 디버깅

프로그램 설계 단계에서 만든 자료구조와 알고리즘은 완전하지 않은 경우가 대부분이다. 아직

까지 프로그램으로 구현하지 않았기 때문에, 실제로 프로그램을 작성하고 실행해 보면 설계할 때

미처 예상하지 못한 부분이 발견되는 것이 당연하다. 프로그램을 구현할 때, 기능별로 올바로 동

작하는지 테스트하는 과정에서 설계 오류를 수정해야 한다. 오류가 발견되면 언제든지 프로그램

설계를 수정하고 이에 따라 소스 코드도 수정해야 한다.

2장부터 소개하는 프로그래밍 문제도 처음에 설계한 대로 완전한 프로그램이 한 번에 구현된

것이 아니다. 프로그램을 개발하는 과정에서 설계에 오류가 존재하였고 구현과 디버깅 과정에서

오류를 확인한 후 그것을 반영하여 반복적으로 설계와 구현을 수정하였다. 프로그램 개발을 완성

한 후, 그 동안의 과정을 정리하여 마치 한 번의 분석, 설계, 구현 과정을 거친 것처럼 교재를 작

성한 것이다.

디버깅은 프로그램 개발 단계의 매우 중요한 기술이다. 대부분의 프로그램 개발 툴은 디버깅

기능을 제공한다. 프로그램 개발 능력은 결국 디버깅 능력이라고 해도 과언이 아니다.

1.5 교재의 구성

프로그램을 처음 접하는 학생은 C 언어를 배운다. C 언어는 그 자체로 기능이 강력할 뿐만 아

니라 객체 지향 언어를 공부할 때도 기초를 제공하기 때문이다. 대부분의 경우 C 언어 문법 교재

를 선택하여 C 언어를 공부를 시작한다. 어려운 문법 위주로 공부하다 보면, 학생들은 프로그램에

Page 20: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

18

대한 흥미를 쉽게 잃어버린다. 본 교재는 문제 풀이 위주로 구성되어 있고, 문제를 풀어 가면서

프로그래밍 개발 능력을 키우고 자연스럽게 문법도 함께 익힐 수 있도록 작성되어 있다.

문제의 크기가 아무리 크더라도 원칙적으로 프로그램을 모두 설계한 후 구현해야 한다. 프로그

램을 모두 설계해야 전체적인 관점에서 프로그램의 구조를 관찰하고 설계할 수 있기 때문이다.

그렇지만, 이 교재에서는 학생들이 하향식 구현 방법을 이해하기 쉽도록 문제를 단계적으로 설계

하고 구현하는 방법을 채택하였다. 다음과 같은 순서로 문제를 해결하는 과정을 설명한다.

문제 제시: 프로그램으로 해결할 문제를 제시한다. 프로그램으로 해결할 목표만 제시하고 구

체적인 기능이나 문제해결 방법은 표현되지 않는 경우가 많다.

요구사항 분석: 해결해야 할 문제를 정확하게 기술하고, 입출력 화면을 설계한다.

자료구조 설계: 요구사항 분석 결과로 나타난 입출력 데이터를 저장하기 위한 변수를 선정하

고 프로그램에서 변수를 활용할 방법을 제시한다.

프로그램 구조 설계: 프로그램을 여러 개의 함수로 구현해야 하는 경우, 프로그램을 제어하기

위한 전체적인 흐름을 설계하고, 각 기능을 구현하기 위한 함수를 결정한다. 그리고 단계적으

로 프로그램을 구현하기 위하여 함수를 설계하고 구현하는 순서를 제시한다.

순서에 따라 각 기능마다 다음 과정을 반복하여 설명한다.

문제해결 방법: 문제를 해결하는 방법을 제시한다.

알고리즘 설계: 문제 해결 방법을 해결하기 위한 절차를 가상 언어로 표현한다.

프로그래밍 기법: 함수를 구현하기 위하여 필요한 라이브러리 함수와 새로 소개되는 C

언어 문법을 설명한다.

프로그램 구현: 알고리즘을 C 언어로 구현한다.

한 가지 기능을 완성할 때마다 중간 결과를 확인해 가면서 프로그램을 완성해 갈 것이다. 그러

므로 프로그램의 소스 코드를 한 번에 모두 제시하지 않는다. 단계를 진행할수록 소스 코드를 보

강해 가면서 점진적으로 프로그램을 완성해 갈 것이다.

1.6 요약

프로그램은 데이터를 입력하고 그것을 처리하고 결과를 출력한다. 프로그램은 특정한 문제를

해결하기 위하여 컴퓨터가 실행할 수 있는 최소 단위의 명령어들을 조합하여 컴퓨터에게 지시하

는 일련의 작업 명세서이다. 프로그래머는 프로그래밍 언어를 사용하여 컴퓨터로 해결하고자 하

는 문제를 컴퓨터가 처리할 수 있도록 소스 코드를 작성한다. 컴파일러는 이것을 하드웨어가 실

행할 수 있도록 기계어 프로그램으로 변환한다.

프로그램을 개발하려면 주어진 문제로부터 프로그램을 설계하고 구현하는 과정을 거쳐야 한다.

프로그래머는 프로그램 언어가 이해할 수 있는 형태로 문제를 표현할 수 있어야 한다. 이를 위하

여 프로그래머는 프로그램 언어에 대한 기초적인 제어 구조와 문장 표현 방법을 알고 있어야 한

다. 그리고 프로그래밍 언어가 제공하는 라이브러리 함수를 많이 알고 있을수록 프로그램을 개발

하기 쉬워진다.

Page 21: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

19

프로그램을 개발하는 과정은 요구사항 분석, 프로그램 설계, 프로그램 구현 및 디버깅 과정으로

구성된다. 요구사항 분석 단계는 프로그램의 처리를 블랙 박스로 보고 어떤 입력에 대하여 어떤

결과물이 도출되었는지를 글 또는 그림으로 표현한다. 프로그램 설계 단계는 입출력 변수를 도출

하고 처리 과정을 알고리즘으로 표현한다. 여기까지는 프로그램 언어와 무관하게 진행될 수 있다.

마지막으로 알고리즘을 프로그래밍 언어로 변환한다. 이 과정에서 프로그램이 올바로 설계되었는

지 확인하는 과정을 거쳐야 하며, 만일 설계가 잘못되었다면 프로그램 설계를 수정하고 다시 구

현해야 한다.

제2장 프로그래밍 기초

프로그래밍 개발 과정에 따라 간단한 문제를 해결하면서 프로그래밍의 기초 지식을 습득하는

것이 이 장의 목표이다. 이 장에서는 다음과 같은 문제를 다룬다.

1. 자기소개: 사용자로부터 데이터를 입력 받고, 형식을 변경하여 출력한다.

2. 수의 차이 구하기: 세 개의 수 중에서 가장 큰 수와 가장 작은 수의 차이를 구하여 출력한다.

3. 평균과 표준편차: 사용자로부터 10 개 이하의 데이터를 입력 받고 평균과 표준편차를 구하여

출력한다.

이 장을 통하여 프로그래밍 개발 과정에 대한 이해 이외에 다음과 같은 프로그래밍 개념을 학

습할 수 있다.

변수의 종류 및 이해

라이브러리 함수 printf()와 scanf() 함수의 사용법 이해

조건문, 제어 구조, 함수의 사용법 이해

배열과 포인터의 기본 개념 이해

처음부터 너무 어려운 내용을 다룬다고 생각될 수도 있겠으나, 각 주제에 대하여 자세하게 모

든 것을 다 설명하지 않고, 주어진 프로젝트를 수행하면서 필요한 기능만을 설명한다. 프로그램

하나씩 그 프로그램에서 사용된 프로그래밍 기법을 이해하도록 유도하는 것이 이 교재의 특징이

다.

2.1 자기소개

본인이 다니는 학과, 학년, 입학년도, 이름을 입력 받고, 본인을 소개하는 문장을 출력하는 프로

그램을 작성하라.

2.1.1 요구사항 분석

문제에 입력 받을 데이터가 모두 정의되어 있다. 사용자로부터 학과, 학년, 입학년도, 이름을 입

력 받고, 다음과 같이 출력하기로 한다. 이탤릭 체로 표시된 부분이 사용자가 입력한 데이터이다.

Page 22: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

20

입력 출력

정보를 입력하세요.

학과: 소프트웨어학과

학년: 1

입학년도: 13

이름: 고길동

나는 소프트웨어학과 1학년 13학번 고길동입니다.

2.1.2 자료형과 변수

자료구조 설계는 요구사항 분석 결과로부터 입력 데이터와 출력 데이터를 저장할 변수를 선정

하고 프로그램에서 선정한 변수를 활용할 방법을 제시하는 단계이다. 자료구조를 설계하기 위하

여 프로그래밍 언어가 제공하는 자료형(data type)과 변수에 대하여 알고 있어야 한다. 주어진 문

제에 대한 자료구조를 결정하기 전에 변수를 선언하는 방법을 먼저 설명한다.

“1.3.1 변수”에서 간단히 설명한 바와 같이, 변수에 대하여 자료형을 지정해야 한다. 컴퓨터는

사용자가 사용하려는 변수에 대하여 할당할 기억장치의 크기와 내부적인 표현 방법을 결정한다.

변수를 선언하는 방법은 다음과 같다.

자료형 변수이름1[, 변수이름2, …];

표기법에서 []는 추가로 더 있을 수도 있음을 의미한다. 즉, 자료형에 대하여 변수를 한 개 이상

선언할 수 있고, 여러 개의 변수를 선언할 때는 쉽표(,)로 구분해야 한다. 프로그램에서 사용자가

정하는 이름을 식별자(identifier)라고 하는데, 변수의 이름과 함수의 이름이 식별자이다. C 언어에

서 식별자를 정하는 규칙은 다음과 같다.

영문자, 숫자, 그리고 밑줄 기호(_)를 조합하여 식별자를 만든다. 다만, 첫 번째 글자는 숫자일

수 없다.

C 언어는 소문자와 대문자를 구별한다.

프로그래밍 언어가 미리 예약해놓은 단어(reserved word)는 식별자로 사용할 수 없다.

프로그래밍 언어는 프로그램의 흐름을 제어하기 위하여 몇 가지 단어를 미리 특정한 의미를 갖

도록 예약해 두었다. 선택형을 제어하기 위한 if, else, 그리고 반복형을 제어하기 위한 for, while,

do 등이 그 예이다. 이와 같은 예약어는 고유의 의미를 갖고 있기 때문에 식별자로 사용할 수 없

다. 다음은 변수를 선언한 예이다.

char c1, c2; // 문자형 변수 두 개를 선언한다.

int grade; // 정수형 변수 한 개를 선언한다.

float _under; // 실수형 변수 한 개를 선언한다.

int for; // 예약어를 변수로 사용해서 잘못된 선언이다.

int 3M; // 첫 번째 문자로 숫자가 사용되었으므로 잘못된 선언이다.

Page 23: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

21

int threeM, ThreeM; // C 언어는 대문자와 소문자를 구별하므로, 서로 다른 변수이다.

프로그램을 작성하다 보면, 동일한 자료형의 변수를 여러 개 선언해야 하는 경우가 있다. 예를

들어 40 명으로 구성된 한 반 학생의 성적을 변수로 선언하고자 하는 경우, 변수를 40 개 선언하

는 것은 매우 불편하다. 이러한 경우에 배열(array)를 사용할 수 있다. 배열은 다음과 같이 선언한

다.

자료형 배열이름[배열크기];

배열의 크기는 항상 정수이어야 한다. 배열을 선언하면, 배열의 원소는 마치 단일 변수와 동일하

게 한 개의 변수로 취급된다. 배열을 선언하면 배열의 원소는 배열이름[0] ~ 배열이름[배열크기-1]

까지 모두 배열크기만큼 변수가 할당된다. 예를 들어, C 언어에서 학생 성적을 처리하기 위한 배

열을 선언한 예는 다음과 같다.

int score[40]; // score란 이름으로 변수 40 개를 선언한다.

이 경우 배열의 원소는 score[0], score[1], …, score[39]까지 40개가 할당된다.

2.1.3 문자와 문자열(string)

컴퓨터는 모든 데이터를 이진수로 변환하여 저장한다. 컴퓨터는 문자를 표현할 때도 문자를 숫

자로 변환하여 표현한다. 기본적으로 영문자를 표현하는 자료형이 문자형(char)이다. 즉, 문자형은

한 개의 영문자를 저장하는 자료형이다. C 언어는 문자형 데이터를 저장하기 위하여 한 개의 바

이트를 할당한다.

문자형 상수를 표현하기 위하여 작은 따옴표를 사용한다. 문자형 상수는 컴퓨터 내부적으로 한

바이트의 숫자로 변환되어 저장된다. 문자형 상수는 영문자의 대문자, 소문자 이외에도 숫자, 특

수 기호도 포함한다. 문자형 상수의 예는 다음과 같다.

‘A’, ‘a’ 영문자 A와 a

‘1’, ‘3’ 문자로 표현된 숫자 1과 3

‘ ‘ 공백(space)

‘!’, ‘?’ 느낌표와 물음표

한글을 한 글자는 두 개의 영문자에 해당하는 공간을 차지한다. 따라서 한글을 저장하기 위하

여 문자형 배열을 사용해야 한다. 문자형 배열을 문자열(string)이라고 한다. C 언어는 문자열 상수

를 큰 따옴표로 표시한다. 다음은 문자열의 예이다.

“hello” 영문자 ‘h’, ‘e’, ‘l’, ‘l’, ‘o’가 연속적으로 배치된 문자열

“123” 숫자 ‘1’, ‘2’, ‘3’이 연속적으로 배치된 문자열

“한글” 두 개의 한글을 포함하고 있는 문자열

“” 문자를 포함하고 있지 않은 문자열

Page 24: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

22

C 언어는 문자열의 끝을 구별하기 위하여 문자열의 마지막에 널(NULL) 문자(값이 0인 문자)를 추

가한다. 예를 들어, “hello”는 마지막에 널 문자가 하나 더 추가되어 있는 것이다. 문자를 포함하고

있지 않은 널 문자열(“”)도 문자열이다. 따라서, 문자열을 저장하기 위하여 문자열 배열의 크기를

정할 때, 눈에 보이는 글자 이외에 한 개의 공간을 더 고려해야 한다.

변수와 문자열에 대한 기초 지식을 알았으니, 이제 문제로 돌아가서 자료구조를 설계해 보자.

2.1.4 자료구조 설계

사용자로부터 입력 받을 데이터는 학과, 학년, 입학년도, 이름이다. 정수형 변수를 사용하면 학

년과 학번을 표현할 수 있다. 학과와 이름은 문자열에 저장해야 한다. 따라서, 자료구조를 다음과

같이 결정한다.

char dept[20]; // 학과를 저장할 문자열 변수

int grade; // 학년을 저장할 정수형 변수

int year; // 입학년도를 저장할 정수형 변수

char name[20]; // 이름을 저장할 문자열 변수

C 언어에서 문자열을 저장할 문자형 배열을 선언할 때, 배열의 크기를 문자열이 저장할 (최대

문자수+1)로 결정해야 한다. C 언어는 문자열의 끝을 표현하는 NULL 문자도 문자열에 함께 저장

해야 하기 때문이다. 따라서, 위와 같이 선언하면, 학과는 한글 9 글자, 이름은 한글 9글자까지 저

장할 수 있다.

이 문제의 경우, 입력 받은 데이터를 그대로 출력하기 때문에 출력 데이터를 저장할 변수를 별

도로 선언할 필요가 없다.

2.1.5 문제해결 방법

자기소개 문제는 입력 데이터를 가공하지 않고 그대로 출력한다. 따라서, 출력 데이터를 산출하

기 위하여 별도의 처리가 필요하지 않다.

2.1.6 알고리즘 설계

자기소개 문제는 “2.1.4 자료구조 설계”에서 결정한 자료구조에 차례대로 데이터를 입력 받고,

출력 형식에 맞추어 그대로 출력하면 된다. 처리 알고리즘은 다음과 같다.

<리스트 2-1> 자기소개 알고리즘

1. “정보를 입력하세요.” 안내문을 출력한다.

2. 학과를 입력 받는다.

3. 학년을 입력 받는다.

4. 학번을 입력 받는다.

5. 이름을 입력 받는다.

6. 자기 소개 글을 출력한다.

Page 25: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

23

프로그램을 구현하려면, 화면에 문자열을 출력하는 방법과 사용자로부터 데이터를 입력 받는

방법을 알아야 한다. 첫 번째로 작성하는 프로그램이므로, 프로그램 구현을 설명하기 전에 몇 가

지 기본 사항을 먼저 설명한다.

2.1.7 printf() 함수

일반적으로 함수는 다음과 같은 형식으로 선언되어 있다.

return_type function_name(parameter_list)

return_type: 함수가 실행을 마치고 리턴하는 데이터의 자료형

function_name: 함수들을 서로 구별하기 위한 함수의 고유 이름

parameter_list: 0 개 이상의 파라미터 목록

파라미터는 자료형과 변수이름으로 구성되어 있다. 함수에 대한 파라미터 목록의 용도와 리턴 값

을 알면, 함수가 내부적으로 처리하는 방법을 모르더라도, 함수를 사용할 수 있다. 앞으로 이 형

식에 따라 라이브러리 함수를 소개하고, 우리가 프로그램을 구현하면서 사용자 정의 함수를 구현

할 때도 먼저 이 형식으로 함수를 선언한다.

화면에 형식화된 문자열을 화면에 출력하는 printf() 함수는 다음과 같이 정의되어 있다.

#include 문은 함수를 선언하고 있는 헤더 파일을 나타낸다.

#include <stdio.h>

int printf(const char *format, …);

기능: 표준 출력장치(즉, 화면)으로 형식화된 문자열을 출력한다.

파라미터

const char *format: 표준출력장치로 출력할 문자열

추가 파라미터(…): format 안에 포함되어 있는 변환 기호 대신에 출력할 값을 저장하고

있는 변수

리턴

성공적으로 출력한 문자의 수 (일반적으로 리턴 값을 사용하지 않는다.)

파라미터 목록에서 const(상수라는 뜻)는 함수가 자신의 기능을 수행하면서 const 형식으로 전

달된 파라미터의 값을 변경하지 않는다는 의미이다. 즉, printf() 함수의 format으로 전달한 문자열

이 printf() 함수를 실행하고 리턴 하더라도 그 값이 변경되지 않는다. 그리고 일반적으로 scanf()

함수의 리턴 값을 사용하지 않는다.

printf() 함수는 파라미터 format으로 지정한 문자열을 화면에 출력한다. 출력 형식을 지정하는

format은 큰 따옴표로 표시되는 문자열 안에 % 기호로 시작하는 변환 기호와 ‘\’로 시작하는 특

수 문자를 포함하고 있으며, 변환 기호에 일대일로 대응하는 변수를 추가 파라미터로 제공하여야

한다. 형식 문자열 안에 포함되어 있는 변환 기호의 수가 추가 파라미터로 제공되는 변수의 수와

같아야 한다. 변환 기호와 출력 편집용 특수 문자의 의미는 다음과 같다.

Page 26: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

24

%c: 문자형 변수의 값을 문자로 변환하여 출력한다.

%d: 정수형 변수의 값을 십진수로 변환하여 출력한다.

%f: 실수형 변수의 값을 십진수로 변환하여 출력한다.

%s: 문자형 배열에 저장되어 있는 문자열을 출력한다.

\n: 문자열을 출력하고, 다음 줄로 이동한다.

변환 기호와 출력 제어용 특수 기호는 이 외에도 여러 가지가 있으나, 일단 이것들만 알아두자.

정수형 변수 varint=3이고 문자열 변수 str[]=”Easy C Program”일 때, printf() 함수를 사용한 문장

과 출력 예는 다음과 같다.

[예제] printf() 함수 사용 예

printf(“안녕하세요?\nprintf() 함수의 사용법을 소개합니다.\n”);

printf(“정수형 변수의 값은 %d 입니다.\n”, varint);

printf(“문자열의 내용은 %s 입니다.\n”, str);

printf(“여러 개의 변환 기호를 포함할 수도 있습니다\n”);

printf(“정수의 값은 %d, 문자열의 값은 %s 입니다.\n”, varint, str);

안녕하세요?

printf() 함수의 사용법을 소개합니다.

정수형 변수의 값은 3 입니다.

문자열의 내용은 Easy C Program 입니다.

여러 개의 변환 기호를 포함할 수도 있습니다.

정수의 값은 3, 문자열의 값은 Easy C Program 입니다.

2.1.8 scanf() 함수

사용자가 입력한 데이터를 변수에 저장하는 기능을 수행하는 scanf() 함수는 다음과 같이 정의

되어 있다.

#include <stdio.h>

int scanf(const char *format, …)

기능: 표준입력장치(즉, 키보드)에서 데이터를 읽는다.

파라미터

const char *format: 읽을 데이터의 형식을 나타내는 문자열.

추가 파라미터(…): 데이터를 받을 변수의 포인터

리턴

성공적으로 읽은 데이터의 수 (일반적으로 리턴 값을 사용하지 않는다.)

scanf() 함수의 format은 큰 따옴표로 표현되는 문자열이고, 그 안에 % 기호로 표시되는 변환 기

호를 포함하고 있다. 일반적으로 scanf() 함수의 리턴 값을 사용하지 않는다. 추가 파라미터는

Page 27: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

25

scanf() 함수를 통하여 데이터를 입력하고 그 데이터를 저장할 변수의 주소(포인터)이다.

printf() 함수는 추가 파라미터로 제공하는 변수의 값을 전달하고, scanf() 함수는 변수에 값을 받

아 온다는 것이 두 함수의 차이점이다. 이 차이점에 의하여 추가 파라미터를 적는 방법에 차이가

생긴다.

C 언어에서 변수의 이름은 “변수의 값”이다.

printf() 함수는 추가 파라미터로 변수의 이름을 적어 변수의 값을 전달한다.

변수이름 앞에 ‘&’ 기호를 붙이면 그 변수의 주소(즉, 포인터)를 의미한다.

함수의 파라미터로 전달하여 변수에 값을 받아 오려면, 변수의 주소(포인터)를 전달해야 한다.

scanf() 함수는 추가 파라미터로 제공하는 변수에 값을 받아 와야 하기 때문에 변수의 주소

(포인터)를 전달해야 한다.

C 언어에서 배열의 이름은 배열이 할당된 기억장치의 주소이다. 문자열은 “문자형 데이터의

배열”이므로, scanf() 함수로 문자열을 읽어 저장할 때, 문자열 배열의 이름 앞에 ‘&’ 기호를

붙이지 않는다.

위와 같은 복잡한 설명을 지금 당장 이해하지 못하더라도 문제가 없다. 다만 다음과 같은 사용

예를 확실하게 알아 두자.

scanf(“%d”, &grade); // int grade에 데이터를 입력하는 방법

scanf(“%s”, dept); // char dept[20]에 데이터를 입력하는 방법

즉, 단일 변수에 값을 받아오려면, 변수이름 앞에 ‘&’ 기호를 붙여야 하고, 배열에 값을 받아오려

면 배열의 이름을 적는다.

2.1.9 main() 함수

C 프로그램은 main() 함수를 반드시 포함하고 있어야 한다. main() 함수는 다음과 같이 두 가지

방법 중 하나로 작성할 수 있다.

<방법 1> int 형으로 선언하는 경우

int main()

{ // 함수 시작

… // 지역변수 선언 및 처리문

return 0; // 정수를 리턴해야 한다.

} // 함수 끝

<방법 2> void 형으로 선언하는 경우

void main()

{ // 함수 시작

… // 지역변수 선언 및 처리문. return 문이 없다.

Page 28: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

26

} // 함수 끝

첫 번째 방법은 main() 함수의 리턴 형을 정수형으로 선언하는 것이다. 이 경우에 main() 함수

의 마지막에 return 0;를 넣는다. 두 번째 방법은 main() 함수의 리턴 형을 void 형으로 선언하는

것이다. void 형이란 “아무 것도 없다” 또는 “아무 것도 아니다”라는 의미이다. 이 경우에는 마지

막에 return 문장을 넣지 않는다. 이 두 가지 방법 중 어떤 것을 사용해도 무방하다.

2.1.10 주석(comment)

주석은 소스 코드 안에 프로그램에 대한 설명을 추가하는 기능이다. 컴파일러는 주석에 해당하

는 부분을 해석하지 않는다. 주석은 프로그램 작성자가 소스 코드의 이해를 돕기 위하여 추가한

다. C 언어에서 주석을 표현하는 방법은 다음과 같이 두 가지가 있다.

<방법 1> //로 시작하는 주석

//로 시작하는 주석은 // 이후 끝까지 주석이다.

<방법 2> /*로 시작하고 */로 끝나는 주석

/*로 시작하는 주석은

여러 줄을 주석으로 만들 수도 있으며

주석의 끝은 이렇게 표시한다. */

2.1.11 자기소개 구현

자기소개 프로그램을 구현하기 위한 기초 지식을 이해하였다면, “2.1.6 알고리즘 설계”에서 설계

한 프로그램을 C 언어로 구현한다. 구현 결과와 실행 화면은 다음과 같다.

<리스트 2-2> 자기소개 프로그램(intro.c)

1. #include <stdio.h> // 헤더 파일 포함

2. int main()

3. {

4. char dept[20], name[20]; // 지역변수 선언

5. int grade, year; // 지역변수 선언

6. printf("정보를 입력하세요.\n\n"); // 안내문 출력

7. printf("학과: "); // 안내문 출력

8. scanf("%s", dept); // 학과 입력

9. printf("학년: "); // 안내문 출력

10. scanf("%d", &grade); // 학년 입력

11. printf("입학년도: "); // 안내문 출력

12. scanf("%d", &year); // 입학년도 입력

13. printf("이름: "); // 안내문 출력

Page 29: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

27

14. scanf("%s", name); // 이름 입력

15. // 자기소개 출력

16. printf("\n나는 %s %d학년 %d학번 %s입니다.\n", dept, grade, year, name);

17. return 0; // main() 함수의 리턴문

18. }

라인 4, 5: 지역변수를 선언한다. 자료형이 동일한 변수는 한 개의 라인에 여러 개의 변수를

선언할 수 있다. 변수를 선언하는 순서는 의미가 없다.

라인 6~14: 사용자의 입력을 받는다. 정수형 변수에 데이터를 입력할 때는 변수 이름에 & 기

호를 붙이고, 문자열 변수에 데이터를 입력할 때는 & 기호를 붙이지 않는다.

라인 16: 입력 받은 데이터를 형식에 맞추어 출력한다. 이 때는 변수의 이름에 & 기호를 붙

이지 않는다. 형식 문자열 안에 있는 변환 기호의 수는 추가 파라미터로 제공되는 변수의 수

와 같고, 변환 형식도 변수의 형과 같아야 한다.

<그림 2-1> 자기소개 프로그램 실행 화면

2.2 수의 차이 구하기

세 개의 수를 입력 받고, 그 중에서 가장 큰 값과 가장 작은 값의 차이를 구하여 출력하라.

2.2.1 요구사항 분석

세 개의 수 중에서 최대값과 최소값을 구하고 두 수의 차이를 구하여 출력하는 간단한 문제이

므로, 문제를 더 구체화할 필요가 없다. 문제 자체에 수의 형식이 정해져 있지 않으나, 실수를 처

리할 수 있도록 프로그램을 개발해 보자. 다음은 입력 화면과 프로그램의 출력 화면의 예시이다.

입력 출력

세 개의 수를 입력하세요.

첫 번째 수: 47.6

두 번째 수: -20.4

세 번째 수: 100.1

가장 큰 수와 작은 수의 차이 = 120.5

2.2.2 자료구조 설계

자료구조를 설계할 때, 프로그램의 입출력 데이터를 저장하기 위한 변수를 제일 먼저 고려하고,

Page 30: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

28

그 다음에 프로그램 내부적으로 결과를 산출하는 과정에서 필요로 하는 변수를 고려하는 것이 좋

다. 사용자로부터 세 개의 실수를 입력 받아야 하고, 가장 큰 수와 가장 작은 수의 차이를 출력하

여야 한다. 그러므로, 이것들을 저장할 변수를 다음과 같이 정한다.

float num1, num2, num3; // 사용자에게 입력 받을 세 개의 수

float difference; // 최대값과 최소값의 차이

출력 값은 최대값과 최소값의 차이이므로, 이것들을 저장할 변수도 필요하다. 따라서 다음과 같

은 두 개의 변수를 추가한다.

float max, min; // 최대값과 최소값을 저장할 변수

참고: float와 double

C 언어에서 소수점이 있는 실수를 표현하는 자료형은 float와 double이 있다.

float 형 변수는 4 바이트로 실수를 표현하고,

double 형 변수는 8 바이트로 실수를 표현한다.

double 형 변수는 float 형 변수보다 더 정밀하고 더 큰 수를 표현할 수 있다.

float 형 수의 표현 범위: 1.1754910-38 ~ 3.4028210+38

double 형 수의 표현 범위: 2.2250710-308 ~ 1.7976910+308

2.2.3 문제해결 방법

num1 = 47.6, num2 = -20.4, num3 = 100.1이라고 할 때, 다음과 같은 순서에 의하여 최대값과

최소값의 차이를 구할 수 있다.

최대값을 구한다. max = 100.1

최소값을 구한다. min = -20.4

차이를 구한다. difference = max - min = 120.5

세 개의 수 중에서 가장 큰 값을 구하는 방법을 알아보자. 사람은 여러 개의 수를 동시에 보고

크기를 비교하여 그 중에서 가장 큰 값을 알 수 있지만, 컴퓨터는 여러 개의 수를 동시에 비교할

수 없다. 컴퓨터는 한 번에 두 개의 수만 비교할 수 있다. 따라서, 이 기능을 반복적으로 사용하

여 가장 큰 수를 구하여야 한다. 세 개의 수 중에서 최대값을 찾는 방법은 다음과 같다.

<리스트 2-3> 세 개의 수 a, b, c 중에서 최대값 찾기 알고리즘

1. 만일 (a > b)이면

2. 만일 (a > c)이면

3. max = a

4. 아니면

5. max = c

6. 아니면

Page 31: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

29

7. 만일 (b > c)이면

8. max = b

9. 아니면

10. max = c

라인 2~5: a > b인 경우에 해당한다. 따라서, a과 c를 비교하여 최대값을 찾을 수 있다.

라인 7~10: a > b가 아닌 경우에만 해당한다. 따라서, b와 c를 비교하여 최대값을 찾을 수 있

다.

이와 같이 두 개의 수를 연속적으로 비교하여 최대값을 찾을 수 있다. 최소값을 찾는 방법도 마

찬가지 방법을 사용할 수 있으며, 다만 두 개의 수를 비교하면서 작은 수를 찾으면 된다. 그렇지

만 다음과 같은 알고리즘을 사용하면, 문제해결 방법이 더 간단해진다.

<리스트 2-4> 세 개의 수 a, b, c 중에서 최소값 찾기 알고리즘

1. min = a으로 설정하여 a을 최소값이라고 가정한다.

2. 만일 (min > b)이면

3. min = b로 설정하여 b를 최소값으로 결정한다.

4. 만일 (min > c)이면

5. min = c로 설정하여 c을 최소값으로 결정한다.

동일한 문제를 여러 가지 알고리즘으로 구현할 수 있다. 동일한 문제를 해결하는 알고리즘 중

에서는 사용하는 기억장치가 적고 처리 시간이 적게 소요되는 것이 좋은 알고리즘이다.

2.2.4 알고리즘 설계

이 프로그램도 입력, 처리, 출력의 순서에 따라 처리 절차를 기술할 수 있다. 다만, 최대값과 최

소값을 구하는 기능을 별개의 함수로 구현하기로 한다.

<리스트 2-5> 세 수 중에서 최대값과 최소값의 차이 구하기 알고리즘

1. 안내문을 출력한다. // 데이터 입력 과정

2. num1, num2, num3를 입력 받는다.

3. 최대값을 구하여 max에 저장한다. // 처리 과정

4. 최소값을 구하여 min에 저장한다.

5. difference = max - min을 구한다.

6. difference를 출력한다. // 결과 출력 과정

라인 3과 라인 4는 별도의 함수로 구현한다. 리스트 2-4가 더 효율적인 알고리즘이지만, 최대값과

최소값을 구하는 알고리즘을 각각 리스트 2-3과 리스트 2-4로 구현해 보기로 한다. 프로그램을

구현하기 전에 조건문과 함수에 대하여 설명한다.

2.2.5 조건문

C 언어에서 두 개의 수를 비교하는 문장을 조건문(conditional statement)라고 한다. 조건문은

Page 32: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

30

프로그램 제어 구조 중에서 선택형을 구현하는 방법이다. 조건문은 다음과 같은 관계 연산자

(relational operator)를 사용하여 조건을 표현하며, 관계 연산자의 결과 값은 연산 결과가 옳으면

참(TRUE), 틀리면 거짓(FALSE)이다.

표 2-1 관계연산자

연산자 사용 예 결과

== x == y x와 y가 같으면 참, 다르면 거짓

!= x != y x와 y가 다르면 참, 같으면 거짓

> x > y x가 y보다 크면 참, 작거나 같으면 거짓

< x < y x가 y보다 작으면 참, 크거나 같으면 거짓

>= x >= y x가 y보다 크거나 같으면 참, 작으면 거짓

<= x <= y x가 y보다 작거나 같으면 참, 크면 거짓

C 언어의 관계 연산자에 대하여 다음 사항을 반드시 알아 두자.

관계 연산자의 실행 결과는 참일 때 1, 거짓일 때 0이다.

C 언어는 0이 아닌 숫자를 모두 참으로 판정하고, 0을 거짓으로 판정한다.

C 언어는 “1,1,3 재어 구조”에서 설명한 선택형을 다음과 같이 if 문으로 구현한다. C 언어에서

‘{‘는 블록의 시작을 나타내고 ‘}’는 블록의 끝을 나타낸다. 만일 블록에 해당하는 문장이 한 개일

때는 블록 시작과 끝을 나타내는 ‘{‘와 ‘}’를 생략할 수 있다.

<리스트 2-6> if 문의 형식

1. if (관계식) // 관계연산자를 사용하여 관계식을 표현한다.

2. { // 조건식이 참인 경우에 수행할 블록 시작

3. 블록 A // 조건식이 참인 경우에 수행할 문장

4. } // 조건식이 참인 경우에 수행할 블록 끝

5. else

6. { // 조건식이 거짓인 경우에 수행할 블록 시작

7. 블록 B // 조건식이 거짓인 경우에 수행할 문장

8. } // 조건식이 거짓인 경우에 수행할 블록 끝

2.2.6 함수 파라미터 전달과 리턴

“2.1.7 printf() 함수”에서 설명한 바와 같이, 사용자 정의 함수도 다음과 같이 선언한다.

return_type function_name(parameter_list)

return_type: 함수가 실행을 마치고 리턴하는 데이터의 자료형

function_name: 함수들을 서로 구별하기 위한 함수의 고유 이름

parameter_list: 0 개 이상의 파라미터 목록

Page 33: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

31

여기에서는 이 문제에서 구현할 사용자 정의 함수인 세 개의 수를 함수의 파라미터로 입력 받

고, 그 중에서 가장 큰 수를 리턴하는 find_max() 함수를 예로 들어 파라미터 전달 방법과 리턴

값을 사용하는 방법에 대하여 설명한다. 이 함수를 다음과 같이 선언하고 구현할 수 있다.

float find_max(float a, float b, float c)

파라미터

float a, float b, float c: 이 함수를 호출하는 함수가 전달하는 세 개의 실수

리턴

세 개의 수 중에서 가장 큰 실수

<리스트 2-7> find_max()의 구조

1. float find_max(float a, float b, float c) // 함수 구현

2. { // 함수 시작

3. float max; // 지역 변수 선언

4. // 리스트 2-3의 알고리즘에 따라

5. // a, b, c 중에서 최대값을 max에 저장한다.

6. retrun max; // 실수 값 리턴

7. } // 함수 끝

<그림 2-2> 함수의 호출과 리턴

함수를 호출하는 문장이 전달하는 데이터를 아규먼트(argument, 인수)라고 부르고, 함수가 전달

받는 데이터를 파라미터(parameter)라고 부른다. 함수의 파라미터 전달 및 리턴과 관련하여 다음

과 같은 사항을 반드시 알아두자. 그림 2-2를 예로 설명한다.

아규먼트와 파라미터는 서로 데이터의 자료형과 수가 같아야 한다.

아규먼트는 복사되어 함수 호출 문장에 나타난 순서대로 파라미터 목록으로 전달된다.

함수는 파라미터를 자신의 지역변수와 마찬가지로 취급한다.

함수는 종료하기 전에 마지막에 자신의 리턴형에 해당하는 데이터를 리턴해야 한다.

따라서, 함수를 호출하는 문장에 사용된 변수(예: num1, num2, num3)와 함수가 파라미터로 받은

Page 34: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

32

변수(예: a, b, c)는 별개의 변수이다. 함수 안에서 선언한 지역변수(예: max)도 함수를 호출한 문장

에서 사용하는 변수(예: max)와 별개의 변수이다. 함수를 실행하는 과정은 다음과 같다.

문장에 함수의 이름을 적으면 함수가 호출된다.

함수는 구현되어 있는 기능을 수행하고 값을 리턴한다.

함수에서 리턴하면, 함수를 호출한 문장은 함수의 리턴 값을 하나의 값으로 취급하여, 변수에

저장한다.

2.2.7 세 수의 차이 구현

함수를 사용하는 문제이므로, 먼저 main() 함수만 구현하고 나주에 두 개의 함수를 추가하기로

한다. main() 함수는 다음과 같다.

<리스트 2-8> main() 함수 구현

1. #include <stdio.h> // 헤더 파일 추가

2. void main()

3. {

4. float num1, num2, num3; // 지역변수 선언

5. float max, min;

6. float difference;

7. printf("세 개의 수를 입력하세요.\n\n"); // 데이터 입력

8. printf("첫 번째 수: "); scanf("%f", &num1);

9. printf("두 번째 수: "); scanf("%f", &num2);

10. printf("세 번째 수: "); scanf("%f", &num3);

11. max = 100.1; // 데이터 처리

12. min = -20.4;

13. difference = max - min;

14. printf("\n가장 큰 수와 작은 수의 차이 = %f\n\n", difference); // 결과 출력

15. }

main() 함수를 void 형으로 선언하였기 때문에, 마지막에 return 문이 없다.

printf() 함수와 scanf() 함수에서 실수를 변환하는 기호는 %f이다.

라인 4~6: 지역변수를 선언한다. 변수의 자료형이 같으면 한 줄에 선언해도 되지만, 입력 변

수, 중간 결과 저장 변수, 결과 저장 변수를 나누어 선언하였다.

라인 8~10: 데이터를 입력하기 위한 안내문을 출력하는 문장과 데이터를 입력하는 문장을 한

줄에 구현하였다. C 언어는 문장을 줄로 구별하는 것이 아니라, 문장의 끝을 나타내는 ‘;’으로

Page 35: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

33

구별한다.

라인 11, 12: 함수를 호출할 부분인데, 일단 화면 설계에서 예시로 사용하는 값을 입력한다고

가정하고 최대값과 최소값을 임시로 할당하였다. 나중에 함수 호출로 대치할 것이다.

라인 14: 출력 문장에 의하여 결과값이 올바른지 확인한다.

프로그램을 실행하고, 예시로 사용하였던 47.6, -20.4, 100.1을 차례로 입력하고, 결과값이 올바른

지 확인해 보자. 프로그램 전체를 구현하는 것보다 이와 같이 단계적으로 구현해 가면서 중간 결

과를 확인하는 것이 좋은 습관이다. 실행 결과는 다음과 같다.

<그림 2-3> 리스트 2-8 실행 결과

다음 단계로 리스트 2-4로 설계한 find_max() 함수를 구현한다.

<리스트 2-9> find_max() 함수 구현

1. float find_max(float a, float b, float c); // main() 함수 전에 함수 선언

2. void main()

3. {

4. …

5. max = find_max(num1, num2, num3); // 리스트 2-8의 라인 11 수정

6. …

7. }

8. float find_max(float a, float b, float c) // 함수 구현

9. {

10. float max; // 지역 변수 선언

11. if (a > b) // 리스트 2-3 구현

12. if (a > c)

13. max = a;

14. else

15. max = c;

16. else

17. max = (b > c) ? b : c;

Page 36: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

34

18. return max; // 최대값 리턴

19. }

라인 1: C 언어는 모든 식별자(변수 및 함수의 이름)를 사용하기 전에 반드시 선언하도록 규

정되어 있으므로, find_max() 함수를 선언한다. 만일 함수를 선언하지 않는다면, 이 함수를 호

출하는 문장에서 문법 오류(syntax error)가 발생한다.

라인 5: main() 함수 안에서 find_max() 함수를 호출하도록 리스트 2-8의 라인 11을 수정한다.

라인 11~17: 리스트 2-3의 최대값 찾기 알고리즘을 구현한다.

C 언어에서 블록을 표현할 때 문장이 한 개 이상이면 ‘{‘로 블록을 열고 ‘}’로 블록을 닫아야 한

다. 라인 12~15까지는 (a>b)가 만족할 경우에 실행할 블록이다. 그렇지만 라인 12~15까지 전체

가 하나의 문장으로 취급되기 때문에 블록을 열고 닫지 않아도 문제가 없다.

라인 16의 else는 라인 11의 if 문과 짝이다. 라인 17을 조건 연산자라고도 부르며, 사용 방법은

다음과 같다. 조건이 참일 때 ‘?’ 이후의 연산식을 실행하여 변수에 저장하고, 조건이 거짓일 때, ‘:’

이후의 연산식을 실행하여 변수에 저장한다.

변수 = (조건) ? 참일 때 실행할 연산식 : 거짓일 때 실행할 연산식;

연산식이 간단할 때, 조건 연산자를 사용하는 것이 편리하다. 결국 라인 17은 다음과 같은 문장을

한 개의 문장으로 표현한 것이다.

if (b > c)

max = b;

else

max = c;

다시 프로그램을 실행해 보자. 이전과 같은 데이터를 입력한다면 실행 결과는 그림 2-3과 같다.

마지막으로 리스트 2-5로 설계한 find_min() 함수를 구현해 보자. 다음과 같이 구현하고 나서, 프

로그램을 실행하면 그림 2-3과 같은 결과를 얻을 수 있다.

<리스트 2-10> find_min() 함수 구현

1. float find_min(float a, float b, float c); // 함수 선언

2. void main()

3. {

4. …

5. min = find_min(num1, num2, num3); // 리스트 2-8의 라인 12 수정

6. …

7. }

Page 37: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

35

8. float find_min(float a, float b, float c) // 함수 구현

9. {

10. float min = a; // 지역 변수 선언과 초기화

11. if (min > b) // min보다 b가 더 작으면

12. min = b; // b를 min으로 설정

13. if (min > c) min = c; // min 보다 c가 더 작으면, c를 min으로 설정

14. return min; // 최소값 리턴

15. }

라인 5: main() 함수에서 최소값을 정하는 부분을 함수 호출로 수정한다.

라인 11, 13: 두 개의 if () 문은 else에 해당하는 처리 부분이 없다.

라인 13: 라인 11, 12와 같이 두 줄에 작성할 수도 있으나, 문장이 간단한 경우 한 줄에 작성

해도 이해하는데 어려움이 없다.

2.3 평균과 표준편차

최대 10 개의 0보다 큰 실수를 사용자로부터 입력 받고, 평균과 표준편차를 구하여 출력하는

프로그램을 작성하라. 단, 평균과 표준편차를 소수점 이하 두 자리까지 출력하라.

2.3.1 요구사항 분석

문제의 요구사항에 의하여 최대 10 개의 실수를 입력 받아야 한다. 따라서, 실수를 입력 받는

도중에 입력을 중단할 수 있는 방법이 있어야 한다. 여기에서는 다음과 같은 방법을 사용하기로

한다.

사용자가 최대 10 개의 데이터를 입력하도록 제어한다.

사용자가 0보다 작은 수를 입력하면 입력을 중단한다.

사용자가 입력한 실수의 숫자를 세어서 데이터의 수를 결정한다.

입력한 데이터에 대한 평균과 표준편차를 구하여 출력한다.

그리고 출력을 소수점 이하 두 자리까지 출력하라는 요구사항이 있다. 이러한 점을 고려하여 다

음과 같이 입출력 화면을 설계한다.

입력 출력

최대 10 개의 실수를 입력하세요.

입력을 종료하려면 0 이하의 수를 입력하세요.

숫자 1: 68.9

숫자 2: 96.7

평균 = 80.38

표준편차 = 10.28

Page 38: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

36

숫자 3: 80.5

숫자 4: 75.4

숫자 5: -1.

2.3.2 배열

이 문제를 해결하기 위하여 자료형이 같은 실수형 데이터를 최대 10 개 저장해야 한다. 자료구

조를 설계하기 전에 먼저 배열에 대하여 좀 더 자세히 알아보자.

배열은 자료형이 같은 여러 개의 변수를 한 개의 이름으로 선언하는 자료구조이다. 배열을 선

언하는 방법은 다음과 같다.

자료형 배열이름[배열 크기];

프로그래밍 언어가 제공하는 자료형에 대하여 배열을 선언할 수 있고, 배열의 이름도 식별자이기

때문에, 배열 이름을 정하는 규칙도 단일 변수의 이름을 정하는 규칙과 같다. 배열에 대하여 다음

과 같은 사항을 알아두자.

배열을 선언할 때 배열의 크기는 반드시 정수형 상수이어야 한다.

배열에 속한 각 변수를 배열의 원소(element)라고 한다.

배열의 원소는 단일 변수와 같이 사용될 수 있다.

배열의 원소를 지정하기 위하여 첨자(index)를 사용한다.

배열의 이름을 array, 배열의 크기를 n이라고 할 때, 배열의 원소는 array[0]부터 array[n-1]까

지 n 개가 할당된다.

첨자는 반드시 정수형 상수 또는 정수형 변수이어야 한다.

배열의 첨자는 배열의 범위를 초과하여 배열의 원소를 지정할 수 없다.

배열을 선언하면 배열의 원소는 기억장치에 연속적으로 할당된다.

2.3.3 자료구조 설계

사용자는 실수형 데이터를 최대 10 개 입력한다. 처리할 데이터의 자료형이 동일하므로, 배열에

데이터를 저장하기로 결정한다. 또한 사용자가 입력한 데이터의 수를 별도로 관리할 필요가 있다.

따라서, 데이터를 입력하기 위한 자료구조를 다음과 같이 결정한다.

float num[10]; // 크기가 10인 실수형 배열

int n; // 사용자가 입력한 데이터의 수

처리 결과로 평균과 표준편차를 출력하여야 하므로, 다음과 같은 변수를 두 개 추가한다.

float avg; // 평균(average)

float stdev; // 표준편차(standard deviation)

평균과 표준편차를 구하는 방법은 다음과 같이 두 가지 방법이 있다.

Page 39: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

37

평균을 먼저 구하고 표준편차를 구하는 방법

평균과 표준편차를 동시에 구하는 방법

원칙적으로 프로그래밍 언어로 문제를 해결할 수 있도록 처리 절차를 기술한 후 프로그램을 구

현해야 한다. 그렇지만 이 문제의 프로그램이 제법 길기 때문에, 하향식 프로그램 구현 방법을 쉽

게 이해할 수 있도록 알고리즘 설계와 구현을 동시에 설명하기로 한다. 이후의 전개 과정은 다음

과 같다.

2.3.4 프로그램 구조 1: 평균을 먼저 구하고 표준편차를 구하는 방법의 전체적인 흐름을 정한다.

2.3.5 데이터 입력 함수: 데이터 입력 함수를 설계하고 구현한다.

2.3.6 프로그램 디버깅: 프로그램 디버깅 방법을 설명한다.

2.3.7 평균: 평균을 구하는 공식을 제시하고 평균을 구하는 함수를 구현한다.

2.3.8 표준편차: 표준편차를 구하는 공식을 제시하고 표준편차를 구하는 함수를 구현한다.

2.3.9 프로그램 구조 2: 평균과 표준편차를 동시에 구하는 방법의 흐름을 정한다.

2.3.10 평균과 표준편차: 평균과 표준편차를 동시에 구하는 공식을 설명한다.

2.3.11 포인터 전달: 함수로 포인터를 전달하여 값을 받아오는 방법을 설명한다.

2.3.12 평균과 표준편차 구현: 평균과 표준편차를 동시에 구하는 함수를 구현한다.

2.3.4 프로그램 구조 1

프로그램의 전체적인 흐름을 결정하고 나서 각 기능을 구현하기 위하여 필요한 함수를 결정해

보자. 데이터 입력, 데이터 처리, 결과 출력의 순서에 따라 프로그램의 처리 절차를 다음과 같이

정한다.

<리스트 2-11> 평균과 표준편차를 구하는 프로그램의 흐름

1. 사용자로부터 10 개 이하의 실수를 입력 받는다.

2. 평균을 구한다.

3. 표준편차를 구한다.

4. 평균과 표준편차를 출력한다.

리스트 2-11의 라인 1, 2, 3를 모두 별개의 함수로 구현하기로 하자. 그리고 데이터를 저장하는 배

열과 데이터의 수를 나타내는 변수를 전역변수로 선언하여 사용하기로 한다. 이 변수들을 전역변

수로 선언하면, 함수를 호출할 때 아규먼트로 전달할 필요가 없다. 구현해야 할 함수의 이름과 기

능을 다음과 같이 결정한다.

void get_data()

파라미터도 없고 리턴 값도 없다.

기능

데이터를 입력 받아 전역변수로 선언되어 있는 num[] 배열에 저장하고,

사용자가 입력한 데이터의 수를 전역 변수 n에 저장한다.

float calc_average()

Page 40: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

38

파라미터는 없다.

리턴

평균을 구하여 리턴한다.

float calc_stdev(float mean)

파라미터

float mean: 표준 편차를 구하기 위하여 평균 값을 파라미터로 받는다.

리턴

표준편차를 구하여 리턴한다.

이와 같이 함수 이름을 정하고 일단 main() 함수를 다음과 같이 구현한다.

<리스트 2-12> 평균 표준편차 main() 함수

1. #include <stdio.h>

2. #define NUMMAX 10 // 데이터의 최대 수를 정의한다.

3. void get_data(); // 함수 선언

4. float calc_average();

5. float calc_stdev(float mean);

6. float num[NUMMAX]; // 전역변수 선언

7. int n;

8. void main()

9. {

10. float avg, stdev; // 지역변수 선언

11. get_data(); // 데이터를 입력 받고

12. avg = calc_average(); // 평균을 구하고

13. stdev = calc_stdev(avg); // 표준편차를 구한다.

14. printf("\n 평균 = %6.2f\n", avg); // 결과를 출력한다.

15. printf("표준편차 = %6.2f\n\n", stdev);

16. }

17. void get_data() {} // 비어 있는 함수 구현

18. float calc_average() { return 1.0; }

19. float calc_stdev(float mean) { return 1.0; }

라인 2: #define 문으로 상수를 선언하였다. 소스 코드에 나타나는 NUMMAX는 모두 10으로

Page 41: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

39

대치된 후 컴파일된다.

라인 6, 7: 전역변수를 선언한다. 배열 num[NUMMAX]은 num[10]으로 선언한 것과 동일하다.

전역변수는 프로그램을 구성하는 임의의 함수에서 사용될 수 있다.

라인 17~19: 일단 비어 있는 함수를 구현하였다. 실수를 리턴하는 함수는 임시로 1.0을 리턴

하도록 구현해 둔다.

라인 14, 15에서 변환 기호 %f 대신에 %6.2f를 사용하였다. printf() 함수의 변환 기호 %f 대신

에 %m.nf로 사용할 수 있다. 여기서 m과 n은 모두 정수이고 m>n이다. 이것은 실수를 출력할 때,

전체 m 자리를 확보하고 그 중에서 소수점 이하를 n 자리로 출력하라는 의미이다. %6.2f로 지시

하면, 소수점을 포함하여 6 자리를 확보하고 소수점 이하에 2 자리를 배정한다. 따라서 표시되는

숫자는 XXX.XX 형태이다. 만일 숫자가 지시한 공간보다 작으면 빈 자리를 공백으로 채운다. 문제

의 요구사항에 소수점 이하 두 자리까지 출력하라는 요건을 이것으로 만족시킬 수 있다. 프로그

램을 컴파일 하여 문법 오류가 없음을 확인한 후, 함수들을 하나씩 구현해 보자.

2.3.5 데이터 입력 함수

사용자로부터 데이터를 입력 받는 get_data() 함수를 다음과 같이 설계할 수 있다.

<리스트 2-13> 데이터 입력 알고리즘

1. 안내문을 출력한다.

2. 데이터의 수 n을 0으로 초기화 한다.

3. 만일 (n < 10)이면 다음을 반복한다.

4. 사용자에게 n 번째 데이터를 입력하라고 안내문을 출력한다.

5. 사용자가 입력한 데이터를 배열 num[n]에 저장한다.

6. 만일 사용자가 입력한 값이 0보다 크면

7. n을 증가시킨다.

8. 그렇지 않다면 // 입력 값이 0보다 작다면

9. 반복문을 중단한다.

리스트 2-13의 라인 3~9는 반복형 제어구조이다. 데이터 입력 함수를 구현하기 전에 반복문을

구현하는 while 루프와 반복문을 벗어나기 위한 break 문에 대하여 설명한다. while 루프는 먼저

조건을 검사한다. 만일 조건이 참이면 계속해서 블록을 실행하고, 조건이 거짓이면, 반복문을 벗

어난다. C 언어에서 while 형 반복문을 구현하는 문장은 다음과 같다.

while (조건)

{

블록

}

반복문을 벗어나기 위하여 break 문을 사용할 수 있다. break 문은 가장 가까이 있는 제어 구조

Page 42: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

40

를 벗어나는 효과가 있다. 단, if 조건문은 break의 영향을 받지 않는다. break 문은 while, do-

while, for 루프, 그리고 switch 문을 빠져 나간다. while 루프를 제외한 나머지 제어 구조는 나중에

설명한다. 리스트 2-13을 C 언어로 구현하면 다음과 같다.

<리스트 2-14> 데이터 입력 함수 구현

1. void get_data()

2. {

3. printf("최대 10개의 실수를 입력하세요.\n"); // 안내문 출력

4. printf("입력을 종료하려면, 0 이하의 수를 입력하세요.\n\n");

5. n = 0; // 데이터의 수를 0으로 초기화 한다.

6. while (n < 10) // n<10이면 블록을 계속 반복한다.

7. { // 블록 시작

8. printf("숫자 %d: ", n+1); // 안내문 출력

9. scanf("%f", &num[n]); // 데이터 저장

10. if (num[n] > 0.0) // 입력 받은 데이터가 0보다 크다면

11. n += 1; // 데이터의 수를 증가시킨다.

12. else // 아니면

13. break; // 반복문을 벗어난다.

14. } // while 블록 끝

15. }

전역변수를 사용하므로, 지역변수를 선언할 필요가 없다.

라인 6: while 형 반복문은 조건을 만족한다면 계속해서 블록을 실행한다.

라인 8: 배열 원소의 인덱스는 n=0부터 시작하고, 사용자에게 표시하는 숫자 1부터 시작한다.

따라서, n+1을 출력함으로써, 화면에 입력 1, 입력 2, …의 순서로 출력한다.

라인 9: 배열의 원소는 배열이름[인덱스]로 참조한다. 인덱스는 항상 정수형이어야 하고, 배열

의 원소는 단일 변수와 동일하게 취급된다. 배열의 원소에 값을 받아와야 하므로, 배열의 원

소 앞에 ‘&’ 기호를 붙인다.

라인 11: n의 값을 1 증가시키는 문장이다. “변수 n에 1을 더한다”로 읽는다. 다음 문장과 효

과가 같다.

n = n + 1; // n에 1을 더하여 n에 저장한다.

라인 13: while 반복문 안에 break 문이 있으면, 반복 조건과 상관 없이 반복문의 실행을 중

단하고 반복문을 벗어난다.

2.3.6 프로그램 디버깅

프로그램을 실행하면 get_data() 함수가 실행되어 데이터를 입력 받지만, 올바로 수행되었는지

확인하려면 별도로 입력한 데이터를 출력하는 함수를 추가해 보아야 한다. 컴파일러는 사용자가

쉽게 프로그램을 디버깅 할 수 있도록 프로그램을 추적(trace)하는 기능을 제공한다. 이 기능을 사

Page 43: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

41

용하면, 출력 함수를 추가하지 않더라도 변수의 값을 확인할 수 있다. 변수의 값을 조사하는 과정

은 다음과 같다.

소스 프로그램에서 프로그램 실행을 중단시킬 라인에 중단점(break point)을 설정한다.

프로그래머가 조사하고자 하는 변수의 값을 확인한다.

다음에 설명하는 순서에 따라 배열 num[]과 변수 n의 값을 확인해 보자.

1 단계: 중단점 설정

프로그램 소스 코드를 보고 다음 중 한 가지 방법으로 프로그램의 실행을 정지할 라인에

중단점을 설정한다.

프로그램의 실행을 중단할 라인으로 편집용 커서를 옮기고 키보드의 F9를 누른다.

메뉴의 [디버그/중단점 설정/해제]를 선택한다.

마우스로 그림 2-4(a)와 같이 소스 코드의 왼쪽에 조그만 빨간색 원이 표시되는 부

분을 클릭한다.

위 동작을 반복할 때마다, 해당 라인에 중단점의 설정과 해제를 반복한다.

중단점을 여러 개 설정할 수도 있다.

우리는 get_data() 함수를 실행한 결과를 확인할 예정이므로, 그림 2-4(a)와 같이 이 문장

다음에 중단점을 설정한다.

2 단계: 디버그 모드에서 프로그램 실행

디버그 모드를 확인하고, 다음 중 한 가지 방법으로 디버그 모드에서 프로그램을 실행한

다.

키보드로 F5를 누른다.

메뉴에서 [디버그/디버깅 시작]을 선택한다.

마우스로 그림 2-4(a)의 사각형으로 표시한 부분의 작은 삼각형을 누른다.

Page 44: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

42

(a) 중단점 설정

(b) 변수 값 확인

<그림 2-4> 프로그램 추적

위 방법으로 프로그램을 실행하면, calc_average() 함수를 호출한 다음에 프로그램이 정지

된다. 데이터 확인을 위하여 1.1, 2.2, 3.3, -1을 입력해 보자.

3 단계: 변수의 값 확인

프로그램의 실행이 정지되면 그림 2-4(b)와 같이 중단점 위에 노란색 화살표가 표시되면

Page 45: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

43

서 프로그램 실행이 그 라인에서 중단되었음을 알려준다.

마우스로 그림 2-4(b)의 아래쪽에 있는 [조사식1] 탭을 클릭하고, 조사하려는 변수의 이름

또는 배열의 이름을 추가하면, 변수의 값을 볼 수 있다.

배열의 이름을 입력한 경우, 배열의 이름 왼쪽에 있는 ‘+’ 기호를 클릭하면, 배열의 값 전

체를 다 볼 수 있다.

4 단계: 디버깅 중단

메뉴에서 [디버그/디버깅 중지]를 선택하여 디버깅을 중단한다.

지금까지 설명한 추적 기능은 매우 강력한 디버깅 도구이다. 프로그램을 처음에 설계한 대로

프로그램 개발을 완료하는 것은 거의 불가능하다. 프로그램을 설계할 때, 모든 상황을 다 고려할

수 없고, 아무리 능숙한 개발자라고 하더라도 생각에 한계가 있기 때문이다. 하향식 개발 방법에

따라 프로그램을 개발하는 과정에서 단계마다 중간 결과를 확인해야만 한다.

지면 관계상 디버깅 기술을 모두 다 소개하고 실행 예제를 제공할 수 없다. 프로그램 개발 툴

의 디버그 메뉴를 살펴보면 많은 디버깅 정보를 알 수 있다. 유용한 디버깅 명령은 다음과 같은

것들이 있다.

한 단계씩 코드 실행(F11): 프로그램을 소스 코드의 라인 단위로 실행한다. 만일 실행하는 라

인에 함수가 있다면, 함수 호출을 따라서 그 함수 안으로 이동한다.

프로시저 단위 실행(F10): 프로그램을 소스 코드의 라인 단위로 실행한다. 만일 실행하는 라

인에 함수가 있다면, 그 함수를 모두 실행하고 다음 라인에 정지한다.

모든 프로그램 개발 툴은 디버깅 기능을 제공한다. 만일 컴파일러가 디버깅 기능을 제공하지

않았다면, 세상은 지금과 같지 않을 것이다. 다시 한 번 강조하지만, 디버깅 기술은 매우 중요하

다. 프로그램 개발 능력은 디버깅 능력에 비례한다고 해도 과언이 아니다. 수 많은 경험을 통해서

만 디버깅 기술은 습득할 수 있다.

2.3.7 평균

먼저 평균을 구하는 함수를 설계하고 구현해 보자. 평균을 구하는 문제해결 방법은 평균 공식

으로 표현할 수 있다. xk는 데이터, m은 평균, n은 데이터의 수라고 할 때, 평균 공식은 다음과 같

다.

nxnxxxm kn /)(/)( 21

자료구조 설계 단계에서 데이터를 배열 num[]에 저장하였고, 데이터의 수를 변수 n에 저장하였

다. 데이터의 수는 n이지만 데이터가 저장되어 있는 배열의 원소는 num[0], num[1], …, num[n-1]

까지 n 개이다. 평균을 구하려면, 먼저 합을 구해야 한다. 다음과 같은 연산문으로 평균을 구할

수 있다.

sum = num[0] + num[1] + … + num[n-1];

Page 46: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

44

그렇지만 배열에 저장되어 있는 데이터의 수가 고정되어 있지 않기 때문에, 위와 같이 평균을 구

하는 공식을 하나의 연산문으로 작성할 수 없다. 대신에 배열의 원소를 하나씩 n번 반복적으로

더하는 방법을 사용하여야 한다. 배열에 저장되어 있는 데이터의 평균을 다음과 같은 방법으로

구할 수 있다.

<리스트 2-15> 평균 계산 알고리즘

1. sum = 0 // 임시 변수 sum을 0으로 초기화 한다.

2. k=0부터 k<n까지 k를 증가시키면서

3. sum += num[k] // 합에 배열의 원소를 더한다.

4. avg = sum / n; // 평균을 구한다.

리스트 2-15의 라인 2, 3은 배열의 원소를 하나씩 참조하는 형태이고 반복하는 횟수가 n번으로

정해져 있다. 이런 경우에 for 반복문이 적당하다. C 언어에서 for 반복문의 형식은 다음과 같다.

for (초기식; 조건식; 증감식)

{

블록

}

초기식: 반복문을 수행하기 전에 한 번만 실행한다.

조건식: 블록을 실행하기 전에 조건을 검사한다. 만일 결과가 참이면 블록을 실행하고, 결과

가 거짓이면 반복문을 중단한다.

증감식: 블록을 실행하고 나서 실행하는 문장이다. 증감식은 조건식에 영향을 주어야 하고,

언젠가는 조건식이 거짓으로 판정될 수 있도록 조건식에 포함되는 변수의 값을 수정하여야

한다.

리스트 2-15의 평균 계산 알고리즘을 다음과 같이 구현할 수 있다.

<리스트 2-16> 평균 함수 구현

1. float calc_average()

2. {

3. float sum, avg; // 합, 평균 지역변수

4. int k; // 반복문 제어 변수

5. sum = 0.0; // 합 초기화

6. for (k=0; k<n; k++) // k=0부터 k<n까지 k를 증가시키면서

7. sum += num[k]; // 임시 변수 sum에 배열의 원소를 더한다.

8. avg = sum / (float)n; // 평균을 구한다.

9. return avg; // 평균을 리턴한다.

Page 47: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

45

10. }

라인 5: 반복문에 의하여 합을 구하기 위하여 합(sum)을 0으로 초기화 한다.

라인 6, 7: 반복문에 의하여, 배열의 원소를 하나씩 합(sum)에 더한다. 반복문은 n 번 실행된

다. 결과적으로 다음과 같은 덧셈을 실행한다.

sum = num1[0] + num[1] + … + num[n-1]

라인 7: for 반복문의 블록이 한 개의 문장이기 때문에, 블록 시작과 블록 끝을 나타내는 { }를

생략하였다.

라인 8: 변수 avg와 sum은 실수형(float0이고, 변수 n은 정수형(int)이다. 자료형이 다른 변수

간에 연산을 수행할 때, 변수의 형을 동일하게 맞추기 위하여 변수 n의 자료형을 변경하여

연산을 수행한다. 이러한 지시문을 형 변환(type casting)이라고 부른다.

참고: ++ 연산자와 --연산자

++ 연산자는 변수의 값을 1 증가시키고, -- 연산자는 변수의 값을 1 감소시킨다.

정수의 값을 갖는 변수에만 사용할 수 있다. 즉, 실수형 변수에는 사용할 수 없다.

연산자가 변수명 앞에 붙는 경우와 변수명 뒤에 붙는 경우가 있다.

예: x가 정수형 변수일 때

++x; // 변수 x의 값을 먼저 증가시키고 나서 사용한다.

x++; // 변수 x의 값을 먼저 사용하고 나중에 증가시킨다.

참고: 자료형 변환 (type casting)

프로그래밍 언어는 자료형에 따라 데이터를 이진수로 저장하는 방법이 다르다.

그러므로 연산문에 사용되는 변수들의 자료형을 동일하게 맞추어 주어야 한다.

연산문을 수행하는 동안 잠시 변수의 자료형을 변경하는 것을 형 변환이라고 한다.

형 변환 방법은 변수의 이름 앞에 “(자료형)”을 적는다.

2.3.8 표준편차

프로그램을 실행하면, 평균값까지 올바로 출력하는 것을 볼 수 있다. 마지막으로 표준편차를 구

하는 함수를 구현해 보자. 표준편차를 구하기 위하여 다음 공식에 따라 먼저 분산(variance)를 구

한 다음에 표준편차를 구한다. 공식에서 xk는 데이터, m은 평균, n은 데이터의 수, variance는 분산,

그리고 stdev는 표준편차이다.

variancestdev

nmx

nmxmxmxvariance

k

n

/))((

/)(...)()(

2

22

2

2

1

표준편차를 구하려면 제곱근(square root)를 계산할 수 있어야 한다. 제곱근을 구하는 라이브러

리 함수는 다음과 같이 정의되어 있다.

Page 48: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

46

#include <math.h>

double sqrt(double x);

파라미터

double x: 제곱근을 구할 값.

리턴

x의 제곱근. 값이 음수인 경우에는 오류가 발생한다.

이 함수를 사용하고 공식에 의하여 다음과 같은 절차에 따라 표준편차를 구할 수 있다.

<리스트 2-17> 표준편차 알고리즘

1. ssum = 0 // 임시 변수 ssum을 0으로 초기화 한다.

2. k=0부터 k<n까지 k를 증가시키면서

3. diff = num[k] – mean; // 배열의 원소와 평균의 차이를 구한다.

4. ssum = ssum + diff*diff; // ssum에 차이의 제곱을 더한다.

5. variance = ssum / n; // 분산을 구한다.

6. stdev = sqrt(variance) // 표준편차를 구한다.

이 알고리즘을 구현하면 다음과 같고, 프로그램의 실행 결과는 그림 2-5이다.

<리스트 2-18> 표준편차 함수 구현

1. #include <math.h> // sqrt() 함수를 정의하고 있는 헤더 파일

2. float calc_stdev(float mean)

3. {

4. float ssum, diff, variance, stdev; // 제곱 합, 차이, 분산, 표준편차

5. int k; // 반복문 제어 변수

6. ssum = 0.0; // 합을 0으로 초기화 한다.

7. for (k=0; k<n; k++) // 배열의 각 원소에 대하여

8. {

9. diff = num[k] - mean; // 차이를 구하고

10. ssum = ssum + diff * diff; // 합에 차이의 제곱을 더한다.

11. }

12. variance = ssum / (float)n; // 분산을 구한다.

13. stdev = (float)sqrt((double)variance); // 제곱근 함수로 표준편차를 구한다.

14. return stdev; // 표준편차를 리턴한다.

15. }

라인 1: 기능: sqrt() 라이브러리 함수를 선언하고 있는 헤더 파일을 소스 코드의 앞에 추가한

다. 일반적으로 소스 코드의 첫 부분에 #include 문을 모두 모아 적는다. #include <stdio.h>

Page 49: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

47

의 다음 라인에 추가한다.

라인 8, 11: for 반복문의 영향을 받는 문장이 두 개이므로, ‘{‘로 블록을 열고, ‘}’로 닫는다.

라인 10: 다음 문장과 동일한 결과를 얻는다.

ssum += (diff * diff);

라인 12: 실수형(float)과 정수형 변수의 계산이므로 정수형 변수의 형을 변환한다.

라인 13: sqrt(0 함수가 double 형 변수를 파라미터로 받으므로, float 형을 double 형으로 변

환하여 값을 전달하고, sqrt() 함수가 리턴하는 double 형 값을 float 형으로 변환하여 저장한

다.

<그림 2-5> 평균과 표준편차 프로그램 실행 결과

2.3.9 프로그램 구조 2

이번에는 하나의 함수에서 평균과 표준편차를 모두 구하는 방법을 구현해 보자. 프로그램의 흐

름은 “2.3.4 프로그램 구조 1”과 동일하므로, main() 함수와 get_data() 함수를 다음과 같이 구현한

다. 이번에는 데이터를 저장하기 위하여 float 형 대신에 double 형을 사용해 보자. 소스 코드에서

다음과 같은 부분이 변경된다.

소스 코드의 float를 모두 double로 변경한다.

printf()와 scanf() 함수의 실수형(float)에 대한 변환 기호 %f를 배정도 실수형 변환 기호 %lf

로 변경한다. 변환기호 lf는 long float의 약자이다.

<리스트 2-19> 프로그램의 구조

1. #include <stdio.h> // 표준 입출력 함수

2. #include <math.h> // sqrt(0 함수 선언

3. #define NUMMAX 10 // 배열의 크기 정의

4. void get_data(); // 함수 선언

5. // 여기에 평균과 표준편차를 구하는 함수를 선언한다.

Page 50: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

48

6. double num[NUMMAX]; // 전역변수 선언, double 형으로 수정

7. int n;

8. void main()

9. {

10. double avg, stdev;

11. get_data();

12. // 여기에서 평균과 표준편차를 구하는 함수를 호출한다.

13. printf("\n 평균 = %6.2lf\n", avg); // 변환기호를 %6.2lf로 변경한다.

14. printf("표준편차 = %6.2lf\n\n", stdev); // 변환기호를 %6.2lf로 변경한다.

15. }

16. void get_data() { … } // 리스트 2-14와 동일

// 리스트 2-14의 라인 9 scanf() 함수의 변환기호를 %lf로 변경한다.

2.3.10 평균과 표준편차

지금까지 분산을 구하는 공식이 평균을 포함하고 있기 때문에, 먼저 평균을 구하고 평균을 사

용해서 표준편차를 구하였다. 그러나, 분산을 구하는 공식을 다음과 같이 수정하면, 평균과 분산

을 동시에 구할 수 있다. 공식에서 xk는 데이터, m은 평균, n은 데이터의 수, variance는 분산, 그리

고 stdev는 표준편차이다.

2

2

22

222

22

2

2

2

)(

mn

x

n

mnx

n

mnmnx

n

mxmx

n

mxvariance

k

k

k

kk

k

평균과 표준편차를 동시에 구하는 과정은 다음과 같고, 이 과정을 알고리즘으로 표현한 것이

리스트 2-20이다.

1. kxsum

xk의 합을 구한다.

2. 2

kxssum

xk2의 합을 구한다.

3. nsummean /

xk의 합을 n으로 나누어 평균을 구한다.

Page 51: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

49

4. 2mean

n

ssumvariance

xk2의 합을 n으로 나누고 평균의 제곱을 빼서 분산을 구한다.

5. variancestdev

분산의 제곱근으로 표준편차를 구한다.

<리스트 2-20> 평균과 표준편차 알고리즘

1. sum = 0 // 합을 누적할 변수를 0으로 초기화 한다.

2. ssum = 0 // 제곱 합을 누적할 변수를 0으로 초기화 한다.

3. k=0부터 k<n까지 k를 1씩 증가시키면서

4. sum += num[k] // 데이터를 누적한다.

5. ssum == (num[k] * num[k]) // 데이터의 제곱을 누적한다.

6. mean = sum / n // 평균을 구한다.

7. variance = ssum /n – mean*mean; // 분산을 구한다.

8. std = sqrt(variance) // 표준편차를 구한다.

이와 같이 한 번의 for 루프를 수행함으로써 평균과 표준편차를 모두 구할 수 있다. 따라서, 평

균과 표준편차를 각각 구하는 것보다 실행 시간이 줄어들 수 있다.

2.3.11 포인터 전달

리스트 2-20을 함수로 구현하려면, 하나의 함수에서 평균과 표준편차를 모두 리턴해야 한다. 그

렇지만, 함수는 return 문을 통하여 명시적으로 한 개의 값만 리턴할 수 있기 때문에, 파라미터를

통하여 간접적으로 값을 리턴 받는 과정에 대한 이해가 필요하다. 그림 2-6를 통하여 이 과정을

설명한다.

그림 2-6(a)는 함수를 호출할 때, 변수를 파라미터로 전달하는 예이다. 호출 전후의 상황은 다음

과 같다.

호출하는 함수는 지역 변수 x를 갖고 있다. 변수 x의 주소는 예를 들어 1004 번지이고 값은

8이라고 가정한다.

func1()을 호출하면서 변수 x를 전달한다. 프로그램 문장에서 “변수는 그 변수의 값을 의미”하

므로, 실제로 func1() 함수로 전달되는 값은 변수 x의 값인 8이다.

함수 func1()은 전달되는 값을 파라미터 z로 받는다. 변수 z는 x와 별개의 변수이고, z가 할당

된 기억장치 주소를 1100 번지라고 가정한다. func1()이 시작될 때, 변수 z의 값은 8이다.

printf() 문으로 z의 값을 출력하면 당연히 8이 화면에 나타난다.

다음에 z = 1을 실행하더라도, z의 값은 1로 변경되지만 호출하는 함수의 변수 x의 값은 변하

지 않는다.

함수 func1()의 실행이 끝나고, 프로그램의 제어는 호출하는 함수로 돌아온다.

호출하는 함수에서 x의 값을 출력하면, 원래 x의 값이었던 8이 그대로 화면에 인쇄된다.

이 과정에서 우리가 알 수 있는 것은 변수를 함수의 파라미터로 전달할 때, 변수의 값이 전달

된다는 것이다. 이것을 전문 용어로 값에 의한 호출(call by value)라고 한다. 따라서, 함수를 호출

하면서 변수를 전달하면, 전달된 변수를 통하여 값을 리턴 받을 수 없다.

Page 52: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

50

<그림 2-6> 파라미터 전달 방법

우리는 그 동안 scanf() 함수를 사용하였다. 이 함수를 통하여 변수에 값을 받아와야 하기 때문

에 변수의 이름 앞에 ‘&’를 붙여서 변수의 주소를 전달하였고, scanf() 함수는 사용자가 입력한 값

을 변수의 주소를 사용하여 그 변수에 저장한다. “변수의 이름 앞에 붙어 있는 ‘&’는 변수의 주소

를 의미”한다. 변수의 주소를 포인터(pointer)라고 부른다. 그리고 주소를 저장하는 변수를 포인터

변수라고 부른다.

호출하는 함수는 파라미터로 변수의 포인터를 전달한다는 것을 다음과 같이 표현한다.

호출하는 함수: 함수이름(&변수이름); // 예: func2(&x)

호출 받은 함수는 자신이 포인터를 전달 받았다는 것을 다음과 같이 표현한다.

호출되는 함수의 선언: 리턴형 함수이름(자료형 *변수이름); // 예: void func2(int *p)

함수의 선언 func2(int *p)에서 int *p는 다음과 같은 의미를 갖는다.

func2()가 받은 파라미터의 이름은 p이다.

*는 p가 주소를 저장하는 포인터 변수임을 나타낸다.

자료형 int는 p로 전달 받는 주소에 정수형 데이터가 들어있다는 것을 나타낸다.

함수를 호출하면서 변수의 포인터를 전달할 때, 호출된 함수가 포인터를 사용하여 값을 호출한

함수로 전달하는 과정을 그림 2-6(b)를 통하여 알아보자.

호출하는 함수는 지역 변수 x를 갖고 있다. 변수 x의 주소는 1004 번지이고 값은 8이라고 가

정한다.

func2()를 호출하면서 변수 x의 주소를 전달한다. 변수의 주소를 전달한다는 의미로 변수 이

름 앞에 &를 붙인다. 실제로 func2() 함수로 전달되는 값은 변수 x의 주소인 1004이다.

Page 53: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

51

함수 func2()는 파라미터로 지정된 포인터 변수 p를 통하여 x의 주소를 받으며, p의 값은

1004이다. 포인터 변수도 변수이기 때문에 기억장치 공간을 할당 받는다. p의 주소는 1200

번지라고 가정한다.

printf() 문으로 p의 값을 출력하면 1004가 화면에 나타난다. printf() 함수의 형식 변환 기

호 %p는 주소를 출력하라는 의미이다.

다음 문장인 *p = 1을 실행하는 과정은 그림 2-7(a)와 같다.

포인터 변수 p의 값은 1004이다.

* 기호에 의하여, p의 값에 해당하는 1004 번지에 1을 저장한다.

1004 번지에는 호출하는 함수에 속한 변수 x가 있으므로, x의 값이 1로 변경된다.

함수 func2()의 실행이 끝나고, 프로그램의 제어는 호출하는 함수로 돌아온다.

호출하는 함수에서 x의 값을 출력하면, x의 값이 1로 출력된다. 결과적으로 호출하는 함수에

속한 변수 x의 값이 변경되었다.

<그림 2-7> 포인터의 개념

그림 2-6(b)의 예에서 변수 부분만 별도로 생각해 보자. 변수가 기억장치에 할당된 상태는 그림

2-7(a)이다. 변수 x는 1004 번지에 할당되어 있다. func2의 파라미터로 받은 포인터 변수 p는

1200 번지에 할당되어 있고, p의 값은 x의 주소인 1004이다. 이 상태를 “포인터 p가 x를 가리킨

다.”라고 부르고, 포인터 변수 p에서 변수 x 방향으로 화살표를 그려서 표시한다.

그런데 우리는 변수 x와 포인터 변수 p가 할당된 주소를 알고 있을 필요가 없다. 컴퓨터가 프

로그램을 실행하면서 소스 코드에 있는 변수들을 기억장치 어디엔가 할당한다. 컴퓨터가 변수의

주소를 어디로 할당하는지 알 필요가 없는 것이다. 프로그램을 이해할 때, 그림 2-7(b)와 같이 “포

인터 변수 p가 변수 x를 가리킨다”는 개념만 알고 있으면 충분하다.

처음 C 언어를 배우는 입장에서 지금까지 설명한 포인터의 개념이 조금 어려울 수 있다. 함수

의 파라미터로 포인터를 전달하여 간접적으로 값을 돌려 받는 방법과 관련하여 다음 사항을 반드

시 기억하자.

호출하는 함수는 변수의 이름 앞에 &를 붙여 변수의 주소(포인터)를 전달한다. (예: func2(&x))

함수를 선언할 때, 포인터를 받는다는 것을 표현하기 위하여 파라미터로 전달받는 변수의 이

름 앞에 *를 붙인다. (예: void func2(int *p))

호출하는 함수로 값을 전달하기 위하여 포인터 변수 이름 앞에 *를 붙이고 값을 할당한다.

(예: *p = 1)

Page 54: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

52

2.3.12 평균과 표준편차 구현

평균과 표준편차를 구하고 파라미터를 통하여 간접적으로 리턴하는 함수를 다음과 같이 정의한

다. 포인터 변수는 그 변수가 포인터임을 표현하기 위하여 변수의 이름 앞에 p를 붙이는 것이 좋

다.

void calc_avgstd(double *pavg, double *pstdev)

파라미터

double *pavg: 파라미터를 통하여 간접적으로 평균을 리턴하기 위한 변수의 포인터

double *pstdev: : 파라미터를 통하여 간접적으로 표준편차를 리턴하기 위한 포인터

리턴

명시적으로 값을 리턴하지 않는다.

먼저 리스트 2-19의 프로그램 구조에서 아직 해결하지 않은 부분을 다음과 같이 수정한다.

라인 5: 평균과 표준편차를 구하는 함수 선언 추가

void calc_avgstd(double *pavg, double *pstdev); // double 형 포인터 두 개를 받는다.

라인 12: 함수 호출 추가

calc_avgstd(&avg, &stdev); // avg와 stdev의 주소를 전달한다.

그리고 나서 평균과 표준편차를 구하는 함수를 다음과 같이 구현한다.

<리스트 2-21> 평균과 표준편차를 구하는 함수 구현

1. void calc_avgstd(double *pavg, double *pstdev)

2. {

3. double sum, ssum, mean, variance, std; // 합, 제곱합, 평균, 분산, 표준편차

4. int k; // 반복문 제어 변수

5. sum = ssum = 0.0; // 합과 제곱합 초기화

6. for (k=0; k<n; k++)

7. {

8. sum += num[k]; // 합 누적

9. ssum += (num[k] * num[k]); // 제곱합 누적

10. }

11. mean = sum / (double)n; // 평균

12. variance = ssum / (double)n - mean*mean; // 분산

13. std = sqrt(variance); // 표준편차

14. *pavg = mean; // 포인터에 의한 평균 전달

15. *pstdev = std; // 포인터에 의한 표준편차 전달

16. }

Page 55: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

53

라인 1: 함수의 파라미터를 포인터로 선언한다.

라인 5: 변수에 값을 할당하는 문장을 연속해서 적었다. 이 경우에 =의 뒤쪽부터 실행한다.

즉, ssum에 0.0을 저장하고, 그 다음에 sum에 ssum을 저장한다. 결과적으로 다음과 같이 두

개의 문장으로 적은 것과 같은 효과가 있다.

sum = 0.0;

ssum = 0.0;

라인 14, 15: 포인터에 의하여 함수에서 계산한 값을 전달한다.

리스트 2-21은 소스 코드를 쉽게 이해할 수 있도록, 평균(mean)과 표준편차(std)를 저장할 지역

변수를 별도로 사용하였다. 포인터 변수는 변수의 이름 앞에 *를 붙여 일반 변수와 동일하게 사

용할 수 있다. 그러므로, calc_avgstd() 함수를 다음과 같이 개선할 수 있다.

<리스트 2-22> 평균과 표준편차를 구하는 함수 개선

1. void calc_avgstd(double *pavg, double *pstdev)

2. {

3. double sum, ssum, variance; // 합, 제곱합, 분산

4. int k; // 반복문 제어 변수

5. sum = ssum = 0.0; // 합, 제곱합 초기화

6. for (k=0; k<n; k++)

7. {

8. sum += num[k];

9. ssum += (num[k] * num[k]);

10. }

11. *pavg = sum / (double)n; // 포인터 변수가 가리키는 곳에 평균을 저장한다.

12. variance = ssum / (double)n - (*pavg)*(*pavg); // 분산을 구한다.

13. *pstdev = sqrt(variance); // 포인터 변수가 가리키는 곳에 표준편자를 저장한다.

14. }

리스트 2-21 또는 리스트 2-22를 구현한 후, 프로그램을 실행하면 그림 2.5와 동일한 결과를

얻을 수 있다.

2.4 요약

처음으로 간단한 문제들을 프로그램으로 구현해 가면서 많은 내용을 다루었다. 문제해결 방법

이 수학 공식으로 주어지기 때문에 알고리즘 설계보다는 프로그램의 구조와 C 언어 사용법을 중

심으로 설명하였다. 이 장에서 다룬 C 언어 사용법을 주제별로 요약하면 다음과 같다.

변수와 배열

Page 56: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

54

변수와 배열을 선언하는 방법은 다음과 같다.

자료형 변수이름;

자료형 배열이름[배열크기];

변수의 이름은 변수의 값이다.

식별자는 영문자와 숫자, 그리고 밑줄 기호(_)의 조합으로 작성한다. 다만, 첫 번째 글자

가 숫자일 수 없다.

문자열은 문자형의 배열이고, 문자열의 끝은 널 문자로 표시된다.

한글의 한 글자는 영문자의 두 배 공간을 차지한다.

배열을 선언할 때 배열의 크기는 반드시 정수형 상수이어야 한다.

배열의 크기를 n이라고 할 때 배열의 원소는 0부터 n-1까지 n 개가 생성된다.

자료형이 다른 변수들 간에 연산을 수행할 때는 자료형을 변환해 주어야 한다.

표준 입출력 함수

printf() 함수의 형식 문자열은 %로 시작하는 변환기호를 포함하며, 추가 파라미터로 변수

를 제공하여야 한다.

scanf() 함수로 데이터를 입력하려면 변수의 이름 앞에 &를 붙여야 한다.

제어 구조

C 언어에서 조건문은 if ~ else ~ 문으로 구현된다.

while 반복문은 조건을 만족할 때까지 블록을 실행한다.

for 반복문은 초기화, 조건식, 증감식으로 구성되며, 반복 횟수가 일정한 경우에 유리하다.

함수

함수를 정의하는 방법은 다음과 같다.

return_type function_name(parameter_list)

return_type: 함수가 실행을 마치고 리턴하는 데이터의 자료형

function_name: 함수들을 서로 구별하기 위한 함수의 고유 이름

parameter_list: 0 개 이상의 파라미터 목록

함수를 호출할 때 아규먼트는 함수의 파라미터와 일대일로 대응되며 자료형과 수가 같아

야 한다.

함수는 파라미터를 자신의 지역변수로 취급한다.

함수의 파라미터를 통하여 간접적으로 값을 리턴 받으려면, 변수의 포인터를 전달해야

한다.

포인터 변수 앞에 *를 붙여 일반 변수와 같이 사용할 수 있다.

디버깅

프로그램 디버깅은 매우 중요하다.

중단점을 설정하고 디버그 모드로 실행하여 변수의 값을 확인할 수 있다.

Page 57: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

55

제3장 조건문, 제어구조, 함수의 사용법이해

“2장 프로그래밍 기초”에서 입력 데이터로부터 출력 데이터를 간단한 수식으로 구할 수 있는

프로젝트들을 다루어 보았다. 이 장에서는 약수와 소수를 구하는 문제를 중심으로 조금 더 복잡

한, 그렇지만 아직은 간단한, 프로그램을 개발해 본다. 이 장에서 다루는 문제는 다음과 같다.

1. 학점출력: 학생의 성적을 입력 받아 학점을 출력하라.

2. 약수 : 정수를 입력 받고 이 수의 모든 약수를 구한다.

3. 완전수 : 1000보다 작은 자연수중 완전수를 모두 찾아서 출력하라.

이 문제들은 입력과 출력의 관계가 간단한 수식으로 표현되지 않고, 문제해결 방법이 규칙을

설명하는 문장으로 표현된다. 이 장을 통하여 다음과 같은 프로그래밍 기법을 학습할 수 있다.

조건문, 반복문 및 함수 사용법 숙달

기억장소 할당 및 해제에 대한 이해

일차원 배열을 함수로 전달하는 방법

3.1 학점출력

학생의 성적을 입력 받아 학점을 출력하라. 단, 성적이 100~90 : A학점 89 ~ 80 : B학점 79~70 :

C학점 69~60 : D학점 59점 이하 : F학점

3.1.2 요구사항 분석

학점 출력

1. 학생 점수를 입력 받는다.

2. 입력한 점수에 해당되는 학점을 출력한다.

(100~90 : A, 89~80 : B,79~70 : C , 69~60 : D 59점 이하 이면 F학점)

3. 입력한 점수가 0~100 이외 범위인 경우

부적절한 점수임을 안내

입출력 설계

입력 출력

점수를 입력하세요 : 70 학점은 B입니다.

3.1.2 자료구조 설계

입력변수

int score; //학생 점수 입력

출력변수

char grade; //입력점수에 해당되는 학점

Page 58: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

56

3.1.2 프로그램 구조 설계

학점 출력 알고리즘

학생의 점수(score)입력 받는다.

만일 점수가 0 ~ 100사이에 있으면

학점 계산

(score/10)의 몫이

10또는 9이면 A학점

8이면, B학점

7이면 C학점

6이면 D학점

5이하 이면 F학점

결과값(gread)을 출력

그렇지 않으면

부적절한 점수임을 출력

3.1.3 학점출력 함수 구현

1. #include<stdio.h>

2. void main()

3. {

4. int score;

5. char grade;

6. printf("\n점수를 입력하세요 :");

7. scanf("%d",&score);

8. if((score>=0) && (score <=100))

9. {

10. switch(score/10)

11. {

12. case 10:

13. case 9:

14. grade = 'A';

15. break;

16. case 8:

17. grade = 'B';

18. break;

19. case 7:

Page 59: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

57

20. grade = 'C';

21. break;

22. case 6:

23. grade = 'D';

24. break;

25. default:

26. grade = 'F';

27. break;

28. }

29. printf("학점은 %c 입니다.\n",grade);

30. }

31. else

32. printf("입력을 잘못하셨습니다\n");

33. }

<실행결과>

3.2 약수 (divisor)

양의 정수를 입력 받고, 이 수의 모든 약수를 구하는 프로그램을 작성하라. 단, 입력 값이 1보

다 작은 정수이면, 프로그램을 종료한다.

3.2.1 요구사항 분석

어떤 정수의 약수는 1 이외의 수로 나누어 나머지가 0인 양의 정수이다. 예를 들어, 정수 24의

약수는 2, 3, 4, 6, 12, 24 이다. 이 프로젝트는 단순히 입력, 처리, 출력의 과정을 통하여 정수 하나

를 입력 받고, 그 정수에 대한 약수 목록을 출력하면 된다. 입출력 화면을 다음과 같이 설계한다.

입력 출력

약수를 구할 정수를 입력하세요(1 이하의 수이면 종료): 24 24의 약수: 2, 3, 4, 6, 8, 12, 14

약수를 구할 정수를 입력하세요(1 이하의 수이면 종료): 1 약수를 구할 수 없습니다.

3.2.2 자료구조 설계

사용자로부터 입력 받을 데이터는 약수를 구할 정수 한 개이다. 이 변수를 다음과 같이 정한다.

Page 60: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

58

int number; // 약수를 구할 정수

약수를 구하는 작업은 명확하게 구별되는 기능이므로 별도의 함수로 구현하는 것이 좋다. 약수

를 구할 때, 사용자가 입력한 number를 2부터 number까지 나누어 보아야 한다. 그러므로 이 과

정에서 다음과 같은 변수가 필요하다.

int n; // 2부터 number까지 따라갈 변수

int remains; // number를 n으로 나눈 나머지를 저장할 변수

3.2.2 프로그램 구조 설계

프로그램의 처리 순서를 데이터 입력, 처리, 결과 출력으로 볼 때, 단순히 한 개의 정수만 입력

하면 되므로 데이터 입력 부분을 별도의 함수로 만들 필요가 없다. 약수 구하기 과정은 알고리즘

을 필요로 하고 독립된 개념이므로 별도의 함수로 만드는 것이 좋다고 판단된다. 그리고 약수를

구함과 동시에 약수를 출력하면 되므로 약수를 출력하는 부분도 약수 구하기 함수에 포함해도 문

제가 되지 않는다. 결과적으로 약수를 구하여 출력하는 함수를 만들기로 한다.

사용자가 약수를 구할 수 없는 정수를 입력할 때까지 계속 사용자의 입력을 받도록 하자. 프로

그램의 흐름을 제어하는 main() 함수는 다음과 같다.

<리스트 3-1> 약수 구하기 main() 함수

1. 무한 반복

2. 약수를 구할 정수를 입력한다. (변수 number)

3. number의 값이 2보다 크면

4. number에 대한 약수를 구하여 출력하는 함수를 호출한다.

5. 아니면 (1보다 작으면)

6. 무한 반복 루프를 벗어나서 프로그램을 종료한다.

프로젝트의 이름을 divisor(약수)라고 정하고, 먼저 비어 있는 C 프로젝트를 생성하고, 소스 파

일(divisor.c)을 추가한다. 프로그램으로 구현할 때는 전체를 한 번에 구현하고 테스트 하기 보다,

단위 기능 별로 구현하고 테스트하는 것이 좋다. 먼저 main() 함수만 구현하여 프로그램의 흐름이

올바로 동작하는지 확인해 보자.

<리스트 3-2> 약수 구하기 main() 함수의 구조

1. #include <stdio.h>

2. int main(void)

3. {

4. int number; // 지역 변수로 선언

5. while (1) // 무한 반복

6. {

7. printf("약수를 구할 정수를 입력하세요(1 이하의 수면 종료): ");

8. scanf("%d", &number); // 데이터 입력

Page 61: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

59

9. if (number >= 2) // 약수를 구할 수 있다면

10. {

11. printf("%d에 대한 약수를 구합니다.\n", number);

12. }

13. else // 약수를 구할 수 없다면

14. {

15. printf("%d에 대한 약수를 구할 수 없습니다.\n", number);

16. printf("프로그램을 종료합니다.\n");

17. break;

18. }

19. printf("\n");

20. }

21. return 0;

22. }

라인 5: C 언어는 0이 아닌 모든 수를 참으로, 0을 거짓으로 판정한다. 따라서 while (1) 문장

의 조건은 함상 참이다. 이것은 무한 반복을 구현하는 방법이다.

라인 7, 8: 약수를 구할 데이터를 입력 받는다.

라인 11: 약수를 구할 함수를 호출하는 문장으로 변경할 부분이다.

라인 15, 16, 17: 입력 값이 1 이하의 정수일 때 수행되며, 프로그램을 종료한다는 안내문을

출력하고 프로그램을 종료한다.

라인 17: break 문은 if 문을 제외한 제어 구조를 벗어나는 역할을 한다. 따라서, 사용자가 1

보다 작은 수를 입력하는 경우에 라인 5의 while 무한 루프를 벗어나고, 프로그램을 종료한다.

<그림 3-1> 약수 구하기 처리 흐름

그림 3-1은 이 프로그램의 실행 결과이다. 이제 프로그램의 전체적인 흐름에 이상이 없다는 것

이 확인되었다. 만일 위와 같이 동작하지 않는다면, 디버거를 사용하여 이 부분이 올바로 동작하

도록 수정하여야 한다.

Page 62: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

60

3.2.3 약수 판정 방법

문제해결 방법은 입력 데이터로부터 출력 데이터를 유도하는 방법을 알아내는 과정이다. 약수

를 구하는 방법은 하나의 수식으로 표현되지 않는다. 약수의 정의에 의하여 약수를 구하는 방법

은 다음과 같다.

정수 number에 대하여 number/n을 수행할 때 나머지가 0이면, n이 number의 약수이다. 단 n은

2보다 크고 number보다 같거나 작은 정수이다.

약수를 구하는 과정을 프로그램으로 처리할 수 있도록 처리 절차를 기술해 보자. 그 결과는 다

음과 같다.

<리스트 3-3> 약수 구하기 알고리즘

1. “number의 약수: “를 출력한다.

2. n = 2부터 n<= number까지 1씩 증가시키면서

3. number를 n으로 나누어 나머지가 0이면

4. n을 약수로 출력한다

3.2.4 약수 함수 구현

이제 약수를 구하여 출력하는 함수를 구현하자. 이 함수의 원형(prototype)을 다음과 같이 정하

고 프로그램을 수정한다.

void print_divisor(int value)

파라미터:

int value: 약수를 구할 정수

기능:

value에 대하여 약수를 구하여 출력한다. 리턴 값은 없다.

<리스트 3-4> 약수 출력 함수 구현

1. void print_divisor(int value); // 프로그램 헤더 부분에 함수를 선언한다.

2. int main(void)

3. {

4. …

5. print_divisor(number); // 리스트 3-2의 라인 11을 함수 호출로 대치한다.

6. …

7. }

8. void print_divisor(int value) // main() 함수 이후에 구현

9. {

10. int n, remains; // 지역변수 선언

Page 63: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

61

11. printf("%d의 약수: ", value);

12. for (n=2; n<=value; n++) // n=2부터 n<=value까지 n을 증가시키면서

13. {

14. remains = value % n; // 나머지를 구한다.

15. if (remains == 0) // 나머지가 0 이면

16. printf("%d ", n); // n을 약수로 출력한다.

17. }

18. printf("\n");

19. }

라인 14, 15: 다음과 같이 하나의 문장으로 합칠 수 있다. 이와 같이 합친다면, 라인 10에서

변수 remains를 선언할 필요가 없다.

if ((value % n) == 0)

라인 14: 연산문의 %는 정수 나누기에서 나머지를 구하는 연산자이다. 예를 들면 5 % 3의 값

은 2이다.

그림 3-2는 약수를 구하여 출력하는 프로그램의 실행 결과이다.

<그림 3-2> 약수 구하기 실행 결과

Page 64: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

62

3.3 완전수(Perfect Number)

1000보다 작은 자연수 중 약수의 합이 자기자신과 같아 지는수(완전수)를 모두 찾아서 출력하는

프로그램을 작성하라.

3.3.1 요구사항 분석

완전수는 자연수에 대하여 자기자신의 수를 제외한 모든 약수의 합이 자신과 같아 지는 수이다.

완전수 예시

6의 약수 : 1, 2, 3, 6 -> 1+2+3 = 6(완전수)

28의 약수 : 1, 2, 4, 7, 14, 28 -> 1+2+4+7+14 = 28(완전수)

3.3.2 자료구조 설계

변수 : int number; //1~1000범위 자연수

입출력 설계

1000이하 완전수 출력 :

6

28

496

3.3.3 프로그램 구조 설계

완전수 구하기 알고리즘

1. 반복(1~1000 이하)

2. number가 완전수 인지 검사

3. number가 완전수 이면 number출력

1. main() 함수

2. #include <stdio.h>

3. main(void)

4. {

5. int number,n;

6. printf("1000 이하의 완전수: \n");

7. for(number =1 ; number<=1000;number++)

8. {

9. printf("%d 가 완전수 인지 체크해봅시다.\n",number); // 임시

Page 65: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

63

10. }

11. return 0;

12. }

<실행결과>

완전수 판정

i 에 대하여 number까지 수행할 때 나머지가 0이면, i 가 number의 약수 이므로 i 를 sum에 더

하고, 약수들의 총합 sum과 number가 같은 경우 완전수 조건 충족

완전수 판정 알고리즘

1. i=1부터 i<number 까지 1씩 증가시키면서

2. number를 i으로 나누어 나머지가 0이면

3. sum = sum +1;

4. 만약 sum == number( 완전수 조건 충족)

5. TRUE 리턴

6. 아니면

7. FALSE 리턴

Page 66: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

64

<main( ) 함수 수정>

1. #include <stdio.h>

2. int perfect_number(int number); // 완전수 여부 판별 함수 추가

3. int main(void)

4. {

5. int number;

6. printf("1000 이하의 완전수: \n");

7. for(number =1 ; number<=1000;number++)

8. {

9. if(perfect_number(number)==1)

10. //완전수 판별 호출, 함수 반환 값이 참(완전수 조건충족) 한다면

11. printf(“ %d \n",number); //number 출력

12. }

13. }

<perfect_number( ) 함수>

1. void perfect_number(int number)

2. {

3. int i, sum; // 완전수 구하기 위한 인덱스 i, 약수의 합을 저장할 변수 sum 선언

4. sum =0; // sum 값 0으로 초기화

5. for (i=1; i<number; i++) // i=1부터 i<number까지 i을 증가시키면서

6. {

7. if (number%i == 0) // 나머지가 0 이면

8. sum=sum+i; // i을 sum 에 더한다.

9. }

10. if(sum==number) //완전수 조건 만족하면

11. return 1; //1(참) 리턴

12. else

13. return 0; //0(거짓) 리턴

14. }

<실행결과>

Page 67: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

65

디버깅 추적기능활용

중단점 설정하기

단축키 [F9]

중단점을 설정하면, 그 위치까지 컴파일러가 컴파일/실행]하다가 중단점을 만나는 순간 컴파일을

중단 하게 됩니다.

단계 실행

단계 실행을 하게 되면 노란색 화살표가 나타난다. 이는 실행할 코드위치를 안내한다.

[F10]를 계속 누르면 한 단계씩 위치가 변경된다.

[F10] – 프로시저 단위 실행

[F10]키를 누르면 단계별로 실행합니다. 그러나 프로시저를 만나면 바로 중단점까지 온다는 것을

확인 할 수 있습니다.

디버그 모드에서 코드를 한 줄씩 실행하되 프로시저를 만나면 프로시저를 모두 실행하고 원래의

Page 68: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

66

코드 한 줄을 실행할 경우 [F10] – 프로시저 단위 실행합니다.

함수 안으로 들어가기

함수 호출문의 경우 함수 안으로 들어 가야 하는 경우에 단축키 [F11](한 단계씩 코드실행)을 통

하여 가능하다.

Page 69: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

67

Page 70: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

68

변수/상수 값 확인

디버깅 중지

단축키 [F5] 또는 디버그 -> 디버깅중지

Page 71: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

69

제4장 배열과 포인터, 동적할당

이 장에서는 약수와 소수를 구하는 문제를 중심으로 조금 더 복잡한, 그렇지만 아직은 간단한,

프로그램을 개발해 본다. 이 장에서 다루는 문제는 다음과 같다.

4. 개표문제

5. 소수: 양의 정수를 입력 받고 이 수가 소수인지 아닌지 판별한다.

6. 에라토스테네스의 체: 양의 정수 n에 대하여 n보다 같거나 작은 모든 소수를 출력한다.

이 문제들은 입력과 출력의 관계가 간단한 수식으로 표현되지 않고, 문제해결 방법이 규칙을

설명하는 문장으로 표현된다. 이 장을 통하여 다음과 같은 프로그래밍 기법을 학습할 수 있다.

조건문, 반복문 및 함수 사용법 숙달

기억장소 할당 및 해제에 대한 이해

일차원 배열을 함수로 전달하는 방법

4.1 개표문제

어떤 학급의 반장을 선출하는데 4명의 후보가 나왔다. 각각의 후보에게 1번부터 4번까지 번

호가 붙여져 있을 때, 투표자의 수와 각 후보자의 득표결과, 그리고 무효표의 수를 출력하는 프로

그램을 작성하라.

4.1.1 요구사항 분석

투표자들이 숫자를 입력하는 동안 계속해서 입력 받는다. 각 후보의 득표수를 저장하기 위하

여 득표수 배열을 선언하되 각 후보의 번호를 인덱스로 사용한다. 크기가 5인 배열은 선언하면 [0]

~ [4]까지 5개의 배열 생성된다. 배열[0]은 무효표의 수로 사용 ,배열 [1] ~ [4]는 각 후보의 득표수

로 사용한다. 입력 받은 정수 값이 1~4사이의 값이 아닌 경우 무효 표를 1 증가시킨다. 입력 받

은 정수 값이 1~4사이의 값인 경우에는 입력 받은 정수를 인덱스로 하는 배열 원소 값을 1 증가

시킨다 . 더 이상 입력이 없으면(즉, 숫자 입력이 끝나면)결과를 출력한다.

4.1.2 자료구조 설계

입력변수

int xdata //투표자가 투표한 번호

출력변수

int n; // 총 투표수

int ip[5] //각 후보의 투표수

Page 72: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

70

입출력 화면

입력화면 출력화면

1~4번 중 한 후보의 번호를 입력하시오 :

득표수

1번 : 3표

2번 : 0표

3번 : 2표

4번 : 1표

무효표 : 1표, 총 6표

<알고리즘 설계>

1. 초기화

2. 총 투표수 n=0

3. 득표수 배열 ip[i] (i=0~4) = 0;

4. 데이터 입력

5. 입력된 데이터가 번호인 동안(문자 입력 시 종료)

6. 총 투표수(n)증가

7. 만일 입력 데이터가 1~4사이의 값이면

8. ip[입력 값]증가

9. 그렇지 않으면

10. ip[0] 증가 //무효 표 수 증가

11. 데이터를 입력한다.

12. 각 후보의 득표수, 무효 표 수 총 투표수 출력한다.

4.1.3 함수 구현

1. #include<stdio.h>

2. #define NMAX 5

3. void main()

4. {

5. int ip[NMAX],n, xdata=0,i;

6. . n=0;

7. for(i=0;i<NMAX;++i)

8. {

9. ip[i]=0; // 배열 0으로 초기화

10. }

11. printf("1~4번 중 한 후보의 번호를 입력하시오 :");

12. while(scanf("%d",&xdata)==1)

Page 73: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

71

// 입력된 데이터가 번호인 동안 (문자 입력 시 반복문 탈출)

13. {

14. n++; // 총 투표 횟수 증가

15. if(xdata<1 || xdata>NMAX) // 1~4 이외의 숫자 입력 시

16. ++ip[0]; //무효 표 수 증가

17. else // 1~4 사이의 값이면

18. ++ip[xdata]; //해당 후보투표수 증가

19. printf("1~4번중 한 후보의 번호를 입력하시오 :");

20. }

21. printf("\n득표수\n");

22. for(i=1;i<NMAX;i++)

23. printf("%d번 : %d 표\n",i,ip[i]); // 1~4번 후보 투표수 출력

24. printf("무효표 : %d표 \n",ip[0]); // 무효표 출력

25. printf("총투표수 : %d표 \n",n); //총 투표횟수 출력

26. }

<실행 결과>

Page 74: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

72

4.2 소수 (Prime Number)

양의 정수를 입력 받고, 이 수가 소수인지 아닌지 판별하는 프로그램을 작성하라. 입력 값이 1

보다 작은 정수이면, 프로그램을 종료한다.

4.2.1 요구사항 분석

소수는 1, 2, 3, 5, 7, 11 과 같이 자기 자신을 제외한 어떠한 수로도 나누어 떨어지지 않는 양의

정수이다. 단, 1은 독립수로 소수에서 제외된다. 단순히 정수 하나를 입력 받고, 이 수가 소수인지

아닌지를 판정하여 출력하면 된다. 입출력 방법을 다음과 같이 설계한다. 이와 같이 입출력 화면

을 설계할 때, 가능한 입력을 모두 기술하는 것이 좋다.

입력 출력

정수를 입력하세요(1 이하의 수이면 종료): 19 19는 소수입니다.

정수를 입력하세요(1 이하의 수이면 종료): 24 24는 소수가 아닙니다.

정수를 입력하세요(1 이하의 수이면 종료): 1 1에 대한 소수를 판정할 수 없습니다.

프로그램을 종료합니다.

4.2.2 자료구조 설계

사용자로부터 입력 받을 데이터는 소수를 판정할 정수 한 개이다. “약수”의 경우와 마찬가지로

이 변수를 다음과 같이 정한다.

int number; // 입력 변수: 소수를 판정할 정수

이 프로그램의 출력은 number가 소수인지 아닌지 판정하여 그 결과에 따라 문자열을 출력하는

것이다. 소수 여부를 판정하는 작업이 독립된 기능이므로 함수로 구현하기로 하고, 이 함수가 리

턴하는 값을 저장할 변수를 다음과 같이 정한다.

int prime; // 출력 변수: 소수이면 값이 1, 소수가 아니면 값이 0을 저장한다.

4.2.3 프로그램 구조 설계

소수를 판정하는 프로그램도 전체적인 구조는 약수 구하는 문제와 마찬가지로 다음과 같이 구

성할 수 있다.

<리스트 4-1> 소수 판정 프로그램의 흐름 제어

1. 무한 반복

2. 소수를 판정할 정수를 입력하여 number에 저장한다.

3. 만일 number >= 2이면

4. 소수인지 판별한다.

5. 판별 결과에 따라 소수 여부를 출력한다.

6. 아니면

7. 소수를 구할 수 없다고 출력하고,

Page 75: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

73

8. 무한 반복 루프를 벗어나서 프로그램을 종료한다.

프로젝트 이름을 prime으로 정하고, 소스 파일(prime.c)을 생성한다. 이번에도 main() 함수만 먼

저 구현해 보자.

<리스트 4-2> 소수 판정 main() 함수 (prime.c)

1. #include <stdio.h>

2. int main()

3. {

4. int number;

5. while (1) // 무한 루프

6. {

7. printf("소수를 판정할 정수를 입력하세요(1 이하의 수이면 종료): ");

8. scanf("%d", &number);

9. if (number >= 2)

10. {

11. printf("%d에 대한 소수를 판정합니다.\n", number);

12. }

13. else

14. {

15. printf("%d에 대한 소수를 판정할 수 없습니다.\n", number);

16. printf("프로그램을 종료합니다.\n");

17. break;

18. }

19. printf("\n");

20. } // end of while

21. return 0;

22. } // end of main

라인 7, 8: 소수 여부를 판정할 데이터를 입력 받는다.

라인 11: 소수를 판정할 부분으로 변경되어야 한다.

라인 13~18: 1 이하의 수를 입력한 경우를 처리하며, 라인 17의 break 문에 의하여 무한 루

프를 벗어난다.

Page 76: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

74

<그림 4-1> 소수 판정 main() 함수의 동작

이 프로그램의 실행 결과는 그림 4-1과 같다. 이와 같이 프로그램의 흐름을 확인하고, 소수를

판정하는 함수를 추가한다.

4.2.3 소수 판정 방법

이제 정수 n에 대하여 소수를 판별하는 방법을 생각해 보자. 정의에 따라 소수를 판정하는 방

법을 다음과 같이 기술할 수 있다.

소수는 1과 자시 자신 이외의 수로 나누어 떨어지지 않는 양의 정수이다.

그러므로, 정수 n에 대하여 n을 2부터 n-1까지 나누었을 때 나머지가 0인 것이 하나라도 존재하

면 소수가 아니고, 나머지가 0인 것이 하나도 없으면 소수이다.

이 개념을 그대로 알고리즘으로 표현하면 다음과 같다.

<리스트 4-3> 소수 판정 알고리즘

1. d=2부터 d<=n-1까지 d를 증가시키면서

2. 만일 n/d의 나머지가 0인 d가 하나라도 존재한다면

3. n은 소수가 아니다.

4. 라인 1의 반복문을 마치면, // 나머지가 0인 d가 하나도 존재하지 않는 것이므로

5. n은 소수이다.

그렇지만, 조금만 생각해보면 반복 횟수를 줄일 수 있다. 정수 n을 정수 d로 나눌 때 몫을 q,

나머지를 r이라고 하면, 다음 공식이 성립한다.

n / d = q … r

만일 d가 n 보다 크다면, 몫인 q가 n 보다 작은 값이기 때문에, d의 최대값은 n 이다. 예를

들어 n = 19인 경우, 19가 소소인지 아닌지 판정하기 위하여 19 =4.xxx이므로, 4까지만 나누어

보아도 충분하다.

Page 77: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

75

19 / 2 = 8 … 1

19 / 3 = 6 … 1

19 / 4 = 4 … 3 ----- 여기까지 나누어 보는 것으로 충분. 여기까지는 몫이 4보다 큰 값.

19 / 5 = 3 … 4 ---- 이후는 나누어 볼 필요가 없음. 이후는 몫이 4보다 작은 값.

19 / 6 = 3 … 1

19 / 18 = 1 … 1

이 개념을 적용하여, 소수를 판정하는 알고리즘을 다음과 같이 개선할 수 있다.

<리스트 4-4> 개선한 소수 판정 알고리즘

1. to = n 을 넘지 않는 정수

2. d = 2부터 d<=to일 때까지 1씩 증가시키면서

3. 만일 n/ d의 나머지가 0인 것이 하나라도 존재한다면

4. n은 소수가 아니다.

5. 라인 2의 반복문을 마치면, // 나머지가 0인 것이 하나도 존재하지 않는 것이므로

6. n은 소수이다.

4.2.4 소수 판정 함수 구현

리스트 4-4의 소수 판정 알고리즘을 수행하는 함수를 다음과 같이 정의하자.

int is_prime(int n)

파라미터

int n: 소수를 판정할 정수

리턴

n이 소수이면 1, 소수가 아니면 0을 리턴한다.

C 언어의 조건문은 0을 거짓, 0이 아닌 모든 수를 참으로 판정한다. 따라서, is_prime()에서

number가 소수이면 참을 리턴하고, number가 소수가 아니면 거짓을 리턴하도록 설계한 것이다.

이제 프로그램의 흐름을 is_prime() 함수를 호출하도록 변경해 보자. is_prime() 함수가 정수를 반

환하므로 함수가 처리한 결과를 받을 정수형 변수를 선언하여야 한다. 이 변수를 prime이라고 하

자. 프로그램을 다음과 같이 수정한다.

<리스트 4-5> 프로그램 수정

1. int is_prime(int n); // 함수 선언 추가

2. int main()

3. {

Page 78: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

76

4. int prime; // 변수 추가

5. …

6. // 리스트 3-6의 라인 11을 다음과 같이 변경한다.

7. prime = is_prime(number);

8. if (prime == 1)

9. printf("%d은(는) 소수입니다.\n", number);

10. else

11. printf("%d은(는) 소수가 아닙니다.\n", number);

12. …

13. }

만일 prime 변수를 사용하지 않으려면, main() 함수를 다음과 같이 구현할 수도 있다.

<리스트 4-6> is_prime()가 리턴하는 값을 직접 비교하는 경우

1. // 변수 prime을 선언하지 않고

2. // 리스트 3-6의 라인 11을 다음과 같이 변경한다.

3. if (is_prime(number)) // 함수가 리턴한 값이 참이면

4. printf("%d은(는) 소수입니다.\n", number);

5. else

6. printf("%d은(는) 소수가 아닙니다.\n", number);

리스트 4-5 라인 7에서 is_prime() 함수가 리턴한 값을 변수 prime에 저장하고, 라인 8에서 그

값을 1과 비교하였다. 프로그램이 처리하는 절차를 단계별로 기록하기 때문에, 이와 같이 작성하

는 것이 이해도가 높다. 리스트 4-7의 라인 3은 이 두 개의 처리 과정을 합쳐서 is_prime()이 리턴

한 값을 직접 사용한 것이다. is_prime() 함수가 1을 리턴하면, 참으로 판정한다. 실제로 C 프로그

램에서는 이런 형태도 자주 사용한다. 리스트 4-6 라인 3을 다음과 같이 작성해도 결과는 마찬가

지이다.

if (is_prime(number) == 1)

이제 is_prime() 함수를 구현한다. 리스트 4-3의 기본적인 소수 판정 알고리즘과 리스트 4-4의

개선된 소수 판정 알고리즘을 모두 구현해 보자. 먼저 리스트 3-7을 다음과 같이 구현한다.

<리스트 4-7> is_prime() 구현

1. int is_prime(int n); // main() 함수 앞에 함수를 선언한다.

2. int is_prime(int n)

3. {

4. int divisor;

Page 79: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

77

5. for (divisor=2; divisor<n; divisor++)

6. {

7. if ((n % divisor) == 0) // 나머지가 0이면

8. return 0; // 소수가 아님을 리턴한다.

9. }

10. return 1; // 소수임을 리턴한다.

11. }

라인 7: n을 divisor로 나눈 나머지와 0을 직접 비교하였다.

라인 8: n을 divisor로 나눈 나머지가 0인 것이 하나라도 존재한다면, 소수가 아니기 때문에 0

을 리턴한다.

라인 10: 반복문을 모두 벗어났다면, 나머지가 0인 것이 하나도 존재하지 않으므로, 1을 리턴

한다.

<그림 4-2> 소수 판정 프로그램 실행 결과

리스트 4-7과 같이 함수의 return 문은 여러 곳에 있을 수 있다. 그렇지만, 만일 함수의 소스

코드가 길다면 소스 코드의 이해도가 떨어지는 단점이 있기 때문에, 좋은 프로그래밍 스타일이

아니다. 이 코드를 한 곳에서 리턴하도록 수정하면 다음과 같다.

<리스트 4-8> is_prime() 수정

1. int is_prime(int n)

2. {

3. int divisor, prime; // 결과를 저장하기 위한 변수 prime 추가

4. prime = 1; // 일단 소수라고 가정한다.

5. for (divisor=2; divisor<n; divisor++)

6. {

7. if ((n % divisor) == 0) // 나머지가 0이면

8. {

9. prime = 0; // 소수가 아님을 저장하고

Page 80: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

78

10. break; // for 문을 벗어 난다.

11. }

12. }

13. return prime; // 여기서 한 번에 리턴한다.

14. }

라인 3: 소수 판정 결과를 저장하기 위하여 변수 prime을 추가한다.

라인 4: 일단 소수라고 가정한다.

라인 9: 나머지가 0이면, 소수가 아니기 때문에 prime에 0을 저장하고

라인 10: 더 이상 소수인지 조사할 필요가 없으므로, 반복문을 벗어난다.

라인 13: 소수가 아닌 경우에는 라인 9를 실행하지 않는다. 그러므로 소수가 아닌 경우에

prime의 값은 1을 유지하고 있다.

이제 is_prime() 함수를 개선된 소수 판정 알고리즘으로 교체하여 구현해 보자. 이 알고리즘은

sqrt() 함수를 사용하므로, 이 라이브러리 함수를 선언하고 있는 헤더 파일을 추가하여야 한다. 이

번에는 for 루프 대신에 while 루프를 사용해 본다.

<리스트 4-9> 개선한 is_prime() 구현

1. #include <math.h> // 헤더 파일 추가

2. int is_prime(int n)

3. {

4. int divisor, to; // to: 마지막으로 나눌 값을 저장할 지역변수

5. int prime = 1; // 변수 선언과 초기화

6. to = (int)sqrt((double)n); // 마지막으로 나눌 값

7. divisor = 2; // 처음에 나눌 값 (초기식)

8. while (divisor <= to) // 2부터 마지막 값까지

9. {

10. if ((n % divisor) == 0) // 나머지가 0이면

11. {

12. prime = 0; // 소수가 아님을 저장하고

13. break; // while 문을 벗어 난다.

14. }

15. divisor += 1; // 나눌 수를 증가시킨다. (증감식)

16. }

17. return prime; // 여기서 한 번에 리턴한다.

18. }

Page 81: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

79

라인 5: 변수 선언과 초기화를 동시에 수행한다.

라인 6: 마지막으로 나누어 볼 정수를 구한다. 처리 구문은 별도로 설명한다.

라인 7, 8, 15: while 문을 사용하였으므로, 라인 5에서 반복문 제어 변수를 초기화하고, 라인

8에서 반복문을 계속 실행할지 조건을 검사하고, 라인 15에서 반복문 제어 변수를 갱신한다.

제곱근을 구하여 리턴하는 sqrt() 함수는 다음과 같이 정의되어 있다.

double sqrt(double x);

이 함수는 double 형 데이터를 파라미터로 받고, 제곱근을 구하여 double 형 데이터를 리턴한다.

따라서 다음과 같이 데이터 형을 변경(type cast)해 주어야 한다.

정수형 변수 = (double 형 결과를 정수로 변환)sqrt((정수형 변수를 double 형으로 변환)n);

프로그램의 실행 결과는 그림 4-2와 같다. 그렇지만 반복문의 실행 횟수는 많은 차이가 있다.

개선된 알고리즘을 사용하고 데이터를 1399를 입력하였을 때, 리스트 4-9의 라인 7에 중단점을

설정하고 to의 값을 살펴보면, 그림 4-3 와 같이 변수 to의 값이 37인 것을 확인할 수 있다.

<그림 4-3> 디버거에 의한 반복문 실행 횟수 확인

Page 82: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

80

4.3 에라토스테네스의 체

그리스 수학자 에라토스테네스가 고안한 방법을 사용하여 양의 정수를 n에 대하여 n보다 같거

나 작은 소수를 모두 출력하는 프로그램을 작성하라.

4.3.1 요구사항 분석

이 프로젝트를 해결하려면 에라토스테네스의 체(sieve of Eratosthenes)에 대하여 알고 있어야

한다. 이 방법은 다음과 같은 과정을 거쳐 임의의 정수에 대한 소수를 모두 구한다.

1은 소수에 해당하지 않으므로, 종이에 2부터 n까지 정수를 모두 적는다.

처음에 인수를 2로 정하고 2의 배수에 해당하는 수를 모두 지운다.

다음에 인수를 3으로 정하고 3의 배수에 해당하는 배열의 원소를 모두 지운다.

이와 같은 방법으로 인수를 하나씩 증가시켜가면서 계속 인수의 배수를 모두 지운다.

마지막까지 남아 있는 숫자들이 소수이다.

사용자 입력은 간단하다. 한 개의 정수를 입력 받고, 이 정수에 대한 모든 소수를 출력한다. 입

력된 숫자가 큰 경우 출력되는 소수가 많을 수 있다. 콘솔 화면이 한 줄에 80 개의 영문자를 표

시한다는 것을 고려하여 한 줄에 8개의 소수를 출력하도록 하자. 화면 설계는 다음과 같다.

입력 출력

정수를 입력하세요: 500 500의 소수

2 3 5 7 11 …

4.3.2 자료구조 설계

사용자로부터 한 개의 정수를 입력 받으면 되므로, 변수를 다음과 같이 정한다.

int number; // 소수를 구할 정수

에라토스테네스의 체 알고리즘을 처리하기 위한 자료구조를 생각해 보자. 처음에 2부터

number까지 정수를 모두 종이에 적어야 한다. 프로그램으로 처리할 때, 정수를 적는 대신에 배열

에 소수 여부를 기록하는 것이 바람직하다. 배열을 사용하여 에라토스테네스의 체 알고리즘을 다

음과 같이 표현할 수 있다.

크기가 number인 일차원 배열을 생성하고, 배열의 초기값은 모두 0이다.

배열 원소의 값이 0이면 배열의 인덱스가 소수라는 의미이고, 배열 원소의 값이 1이면 배열

의 인덱스가 소수가 아니라는 의미이다.

처음에 0으로 초기화 한 것은 모두 소수라고 가정한 것이다.

처음에 인수를 2로 정하고 2의 배수에 해당하는 배열의 원소를 모두 1로 만든다. 소수가 아

니라는 의미이다.

다음에 인수를 3으로 정하고 3의 배수에 해당하는 배열의 원소를 모두 1로 만든다.

Page 83: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

81

이와 같은 방법으로 인수를 하나씩 증가시켜가면서 계속 인수의 배수를 1로 만든다.

마지막까지 값이 0으로 남아 있는 배열의 인덱스들이 소수이다.

에라토스테네스의 체 알고리즘을 구현하기 위하여 배열을 사용하는 경우, 다음과 같은 두 가지

를 더 생각해 보아야 한다.

배열의 크기: 크기가 number인 배열을 사용할 것인지 크기가 number+1인 배열을 사용할 것

인지 결정한다.

배열을 선언하는 방법: 배열의 크기가 가변적이고 클 수 있기 때문에, 배열을 정적으로 선언

할지 가변적으로 선언할지 결정한다.

배열의 크기에 대하여 생각해 보자. 예를 들어 C 언어에서 10 개의 배열을 선언할 때, 배열의

원소의 수는 10 개이지만, 배열의 인덱스는 0부터 9까지 할당된다. 에라토스테네스의 체를 구현

하려고 할 때, 배열의 크기를 다음과 같이 두 가지 방법 중 하나로 정하여 사용할 수 있다.

크기가 number 개인 일차원 배열 array[]를 사용한다.

배열의 원소는 array[0]부터 array[number-1]까지이다.

2보다 크고 number보다 같거나 작은 정수 n이 소수가 아니라는 것을 표시하기 위하여

배열의 array[n-1]을 0으로 만든다.

예를 들면, 4가 소수가 아니라는 것을 표현하기 위하여 array[3]을 1로 표시해야 한다.

출력할 때, 배열 원소의 값이 0이 아닌 배열의 인덱스+1을 소수로 출력한다.

크기가 number+1 개인 일차원 배열 array[]를 사용한다.

배열의 원소는 array[0]부터 array[n]까지 이다.

2보다 크고 number보다 같거나 작은 정수 n이 소수가 아니라는 것을 표시하기 위하여

배열의 array[n]을 만든다.

예를 들면, 4가 소수가 아니라는 것을 표현하기 위하여 array[4]을 1로 표시한다.

출력할 때, 0이 아닌 배열의 인덱스를 소수로 출력한다.

위 두 가지 방법 중 어떤 것을 사용해도 좋다. 여기에서는 두 번째 방법을 사용하기로 한다. 단,

array[0]과 array[1]을 사용하지 않는다.

배열을 선언하는 방법은 정적(static)으로 선언하는 방법과 동적(dynamic)으로 기억장소를 할당

하는 방법이 있다. 사실, 그 동안 우리가 사용해 왔던 단일 변수와 배열은 모두 정적으로 선언한

것이다. 배열을 선언하는 예는 그림 3-6과 같다.

자료형 배열이름[배열 크기];

int array[6]; // 크기가 6인 정수형 배열 array[] 선언

Page 84: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

82

<그림 4-4> 배열의 정적 선언과 동적 할당

그림 4-4(a)는 배열을 정적으로 선언한 모습이다. 배열의 선언과 관련하여 다음 사항을 알아두자.

배열을 선언하면 프로그램을 시작할 때 크기가 고정되어 있는 배열을 기억장치에 할당하고,

프로그램이 끝날 때까지 그 기억장치를 유지한다.

배열의 원소는 기억장치에 연속적으로 배치된다.

배열을 정적으로 선언할 때 크기가 고정되어야 한다.

배열의 이름은 배열이 할당된 기억장치의 시작 주소이고 상수이다.

에라토스테네스의 체 문제를 해결하기 위하여 필요한 배열은 크기가 가변적이다. 그러므로, 배

열을 정적으로 할당하려면, 다음과 같은 정책을 사용해야 한다.

사용자가 입력하는 정수의 최대값을 예상하여 배열의 크기를 가능한 크게 선언한다.

그 중에서 필요한 만큼만 배열을 사용한다.

예를 들어, 배열의 크기가 1,000,000인 배열을 선언하고, 사용자가 1,000,000 이하의 수를 입력하

도록 제어하는 방법이다. 그런데 이 방법은 사용자가 1,000,000 이상의 수를 입력할 수도 있으므

로 처리 범위를 제한하고, 사용자가 입력하는 숫자가 작을 경우에 기억장치를 불필요하게 낭비한

다는 단점이 있다.

이러한 단점을 해소할 수 있는 방법이 그림 4.4(b)와 같이 배열을 동적으로 할당하는 것이다.

이 방법의 개념은 다음과 같다.

1. 배열의 원소로 저장하려는 데이터의 자료형에 대한 포인터를 선언한다.

2. 프로그램 실행 중 배열이 필요할 때, 기억장소를 동적으로 할당하여 포인터에 연결한다.

3. 할당된 기억장소를 배열처럼 사용한다.

4. 배열을 모두 사용하였다면, 할당 받은 기억장소를 해제한다.

프로그램이 시작될 때 기억장소에는 포인터 변수 하나만 만들어진다. 그림 3-6(b)는 포인터 변

수에 배열을 할당한 모습이고, 이 후에 정적 배열을 사용하는 것과 마찬가지 방법으로, array[0],

array[1] 등과 같이, 할당 받은 기억장소를 사용할 수 있다. 배열을 정적으로 할당 받았을 때, 배열

Page 85: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

83

의 이름은 배열의 시작 주소이고, 포인터에 동적으로 배열을 할당하였을 때도 포인터의 값이 할

당 받은 배열의 시작 주소이기 때문이다. 다만, 정적으로 배열을 선언할 때, 배열의 이름은 상수

이고, 동적으로 할당할 때 포인터가 변수이라는 것만 차이가 있다.

컴퓨터의 운영체제는 실행되고 있는 프로그램이 사용하지 않는 별도로 기억장소를 관리하고,

프로그램에서 기억장소를 요청하면 할당해 주는 기능이 있다. 또한 컴파일러는 기억장소를 할당

하고 해제하는 라이브러리 함수를 제공한다. 다음 절에서 이 함수들에 대하여 설명한다.

에라토스테네스 체 문제는 배열의 크기가 가변적이기 때문에 동적으로 배열을 할당하는 방법이

더 적합하다. 마지막으로 배열의 데이터 형을 생각해 보자. C 언어에서 정수형 데이터를 저장하는

데이터 형은 표 3.1과 같은 종류가 있다.

<표 4.1> 정수 데이터 자료형

자료형 바이트 수 수의 표현 범위

char 1 -128 ~ 127

short 2 -32,768 ~ 32767

int 4 -2,147,483,648 ~ -2,147,483,647

long 4 -2,147,483,648 ~ -2,147,483,647

unsigned char 1 0 ~ 255

unsigned short 2 0 ~ 65535

unsigned int 4 0 ~ 4,294,967,295

unsigned long 4 0 ~ 4,294,967,295

에라토스테네스 체 알고리즘에서 배열에 저장되는 값은 0 또는 1이다. 따라서, 크기가 가장 작

은 char 형 배열을 사용해도 문제가 없다. 기억장소를 절약한다는 의미에서 알고리즘을 처리하기

위한 배열의 포인터를 다음과 같이 결정한다.

char *array;

요즈음 많이 사용되는 컴퓨터는 32 비트(4 바이트)의 데이터를 한 번에 처리한다. 처리 속도 면

에서 컴퓨터는 int 형을 더 빠르게 처리한다. 왜냐하면, 1 바이트를 처리하려면, 4 바이트에서 1

바이트를 분리해야 하기 때문이다. 그러므로, 기억장소 용량에 문제가 없다면 가능한 int 형을 사

용하는 것이 좋다.

지금까지 설명을 요약하면, 에라토스테네스의 체를 구현하기 위한 자료구조는 다음과 같다.

int number; // 사용자에게 입력 받을 소수를 구할 정수

char *array; // 에라토스테네스의 체 알고리즘을 수행하기 위하여

// 크기가 number+1 개인 배열을 할당할 포인터

Page 86: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

84

4.3.3 기억장소 할당 및 해제

프로그램을 구현하기 위하여 기억장소를 할당하고 해제할 필요가 있기 때문에 이 동작과 관련

된 라이브러리 함수를 먼저 설명한다. 기억장소를 할당하는 라이브러리 함수는 다음과 같다.

#include <stdlib.h> // malloc() 함수를 선언하는 헤더 파일

void *malloc(size_t size)

파라미터

size_t size: 할당할 메모리 블록에 바이트 단위의 크기. size_t는 unsigned int를 다시 정의

한 것이다. 즉, unsigned int와 같다.

리턴

성공하면, 할당한 메모리 블록의 주소, 즉 포인터

실패하면, 널 포인터(NULL pointer)

기억장치를 동적으로 할당하는 malloc() 함수는 할당하고자 하는 기억장치의 바이트 수를 파라

미터로 받고, 할당한 기억장치의 시작 주소를 리턴한다. 이 함수의 리턴 값은 void *(void 형 포인

터)이다. 포인터의 자료형으로써 void *의 의미는 기억장치의 주소이지만, 그 형을 결정할 수 없다

는 의미이다. 따라서, malloc() 함수로 메모리 블록을 할당 받고 포인터에 연결할 때, 메모리 블록

의 자료형을 변환해 주어야 한다.

동적으로 할당 받은 기억장치는 사용이 끝난 후 다시 운영체제로 반납해야 하는데, 이 동작을

기억장치 해제(release)라고 한다. 이 때 사용하는 함수가 free() 이다. 이 함수는 다음과 같이 선언

되어 있다.

#include <stdlib.h> // free() 함수를 선언하는 헤더 파일

void free(void *ptr)

파라미터

void *ptr: 해제할 메모리 블록에 대한 포인터

리턴

없음.

이 함수는 임의의 주소를 파라미터로 받고, 해당 메모리 블록을 운영제제로 반납한다. 파라미터로

malloc() 함수가 리턴한 포인터를 전달하면 된다. 프로그램을 작성할 때, 기억장치를 동적으로 할

당하였으면, 항상 기억장치를 해제할 것을 고려하여야 한다. 마치 괄호를 열었으면, 먼저 닫고 중

간에 프로그램을 작성하는 것과 같이, 기억장치를 할당하였으면, 먼저 free()부터 작성하는 습관을

갖는 것이 좋다. 정수형 포인터에 기억장소를 할당 받고 해제하는 예는 다음과 같다.

<리스트 4-10> 기억장소 할당 및 해제

1. #define SIZE 100 // 배열의 크기를 정의한다.

2. int *array; // 정수형 포인터

3. array = (int *)malloc(sizeof(int)*SIZE); // 기억장소 할당

Page 87: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

85

4. if (array != NULL) // 기억장소가 할당되었는지 확인한다.

5. {

6. // 배열을 사용한다. // 배열처럼 사용한다.

7. free(array); // 기억장소 해제

8. }

sizeof() 함수는 자료형 또는 변수를 입력으로 받아 자료형의 바이트 단위의 크기를 리턴한다. 정

수형의 크기가 4 바이트임을 알고 있으므로, 다음과 같이 작성해도 효과는 동일하다. 그렇지만,

컴퓨터에 따라 int 형의 크기가 변할 수 있기 때문에 위와 같이 작성하는 것이 더 좋다.

array = (int *)malloc(400);

리스트 4-10의 라인 3은 malloc() 함수가 void *를 리턴하기 때문에 array의 형에 맞추어 데이

터 형을 변환한 것이다. 운영체제가 보유하고 있는 동적 기억장치가 사용자가 요구한 크기보다

작으면, malloc()은 기억장소 할당에 실패하고 널 포인터(포인터의 값이 0)를 리턴한다. 이 경우에

대비하기 위하여 라인 4에서 if 문을 사용한 것이다. 그렇지만, 실제 상황에서 기억장소 할당에 실

패하는 경우가 거의 없기 때문에, 일반적으로 malloc()의 리턴 값을 검사하지 않아도 큰 문제는

없다.

할당 받은 기억장치는 초기화되어 있지 않다. 따라서, 기억장치를 할당 받은 후, 초기화하고 사

용하여야 한다. 예를 들어 할당 받은 배열의 값을 모두 초기화 하는 0으로 방법은 다음과 같다.

<리스트 4-11> 동적 할당 및 초기화

1. #define SiZE 100

2. int *array, n;

3. array = (int *)malloc(sizeof(int)*SIZE); // 정수형 배열을 할당한다.

4. for (n=0; n<SIZE; n++) // 배열을 초기화 한다.

5. array[n] = 0;

6. // 배열을 사용한다.

7. free(array); // 할당 받은 기억장소를 해제한다.

배열의 크기가 큰 경우 for 루프로 초기화하는데 시간이 많이 소요될 수 있으므로, 컴파일러는

기억장소를 한 번의 동작으로 임의의 값으로 초기화하는 memset() 함수를 제공한다. 이 함수는

다음과 같이 정의되어 있다.

#include <string.h> // memset() 함수를 선언하는 헤더 파일

void *memset(void *ptr, int c, size_t count)

파라미터

void *ptr: 특정 값으로 채울 메모리 블록에 대한 포인터

int c: 메모리 블록을 채울 값

size_t count: 메모리 블록의 크기

Page 88: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

86

리턴

ptr을 그대로 리턴한다.

이 함수를 사용하면 배열을 초기화 하는 과정을 다음과 같이 작성할 수 있다.

<리스트 4-12> memset() 함수를 사용한 기억장소 초기화

1. #define SIZE 100

2. int *array;

3. array = (int *)malloc(sizeof(int)* SIZE); // 정수형 배열 SIZE 개를 할당한다.

4. memset(array, 0, sizeof(int)*SIZE); // 배열을 0으로 초기화 한다.

5. // 배열을 사용한다.

6. free(array); 할당 받은 기억장소를 해제한다.

그리고 0으로 초기화한 기억장치를 할당하는 calloc() 함수를 사용할 수도 있다. 이 함수는 다음

과 같이 정의되어 있다.

#include <stdlib.h>

void *calloc(size_t num, size_t size)

파라미터

size_t num: 할당할 원소의 수

size_t size: 각 원소의 크기

리턴

성공하면, 메모리 블록을 할당하고 포인터를 리턴한다.

실패하면, 널 포인터를 리턴한다.

이 함수도 void *를 리턴하므로, 원하는 데이터 형에 대한 포인터로 변환하여 사용하여야 한다.

이 함수를 사용하면, 크기가 number 개인 정수형 배열을 할당하고 해제하는 과정을 다음과 같이

작성할 수 있다.

<리스트 4-13> calloc() 사용 예

1. #define SIZE 100

2. int *array;

3. array = (int *)calloc(SIZE, sizeof(int)); // 정수형 배열 SIZE 개를 할당한다.

4. // 배열을 사용한다.

8. free(array); // 할당 받은 기억장소를 해제한다.

4.3.4 프로그램 구조 설계

프로그램의 구조를 결정하기 위하여, 에라토스테네스의 체 프로그램의 처리 절차를 데이터 입

력, 데이터 처리, 데이터 출력으로 나누어 생각해 보자.

Page 89: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

87

데이터 입력: 한 개의 정수만 입력하면 되므로 데이터 입력 과정은 별 문제가 없다.

데이터 처리: 기능이 명확하게 분리되므로 에라토스테네스의 체 알고리즘을 별개의 함수로

구현한다.

데이터 출력: 배열의 값에 의하여 알고리즘에 의하여 산출된 소수를 알 수 있고, 소수의 수가

일정하지 않다. 출력이 단순하지 않으므로 별개의 함수로 구현한다.

이러한 분석 결과에 의하여 프로그램의 구조를 다음과 같이 작성해 보자.

<리스트 4-14> 소수 구하기 main() 함수

1. 사용자로부터 정수를 입력 받아 number에 저장한다.

2. number>2 이면

3. 배열을 할당한다.

4. 에라토스테네스의 체 알고리즘에 의하여 소수를 구한다.

5. 구한 소수를 출력한다.

6. 할당 받은 배열을 해제한다.

7. 프로그램을 종료한다.

콘솔 응용프로그램을 작성하기 위하여 프로젝트 이름을 eratos로 정하고, 비어 있는 소스 파일

(eratos.c)을 생성하고, 소스 코드를 다음과 같이 작성한다. 프로그램의 실행 결과는 그림 3-7이다.

<리스트 4-15> main() 함수 구현

1. #include <stdio.h>

2. #include <stdlib.h> // calloc(), free() 함수 선언

3. int main()

4. {

5. int number;

6. char *array; // 문자형 포인터

7. printf("정수를 입력하세요: "); // 정수 입력

8. scanf("%d", &number);

9. if (number > 2)

10. {

11. array = (char *)calloc(number+1, sizeof(char)); // 배열 동적 할당

12. if (array != NULL)

13. {

14. printf("%d개의 배열을 할당하였습니다.\n", number+1);

15. free(array);

16. }

17. }

Page 90: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

88

18. return 0;

19. }

라인 11: calloc() 함수를 사용하여 0으로 초기화된 문자형 배열 number+1개를 할당한다.

라인 12: 기억장소가 올바로 할당되었는지 확인한다.

라인 14: 소수를 구하고 출력하는 함수 호출로 대치될 부분이다.

라인 15: 할당 받은 기억장소를 해제한다.

<그림 4-5> 에라토스테네스의 체 흐름 제어 출력 화면

4.3.5 에라토스테네스의 체 알고리즘

자료구조를 결정하는 과정에서 에라토스테네스의 체 방식으로 소수를 찾는 문제해결 방법이 거

의 결정되었다. 문제해결 방법을 다시 정리하면 다음과 같다.

처음에 인수를 2로 정하고 2의 배수에 해당하는 배열의 원소를 모두 1로 만든다.

다음에 인수를 3으로 정하고 3의 배수에 해당하는 배열의 원소를 모두 1로 만든다.

이와 같은 방법으로 인수를 하나씩 증가시켜가면서 계속 인수의 배수를 1로 만든다.

마지막까지 값이 0으로 남아 있는 배열의 인덱스는 소수이다.

문제해결 방법을 알고리즘을 표현해 보자. 알고리즘으로 표현하기 위하여 절차를 일반화하는

것이 필요하다. 문제해결 방법을 아래로 보면 인수의 값이 2부터 number까지 변한다. 그리고 각

인수마다 배수의 값이 2부터 number까지 변한다. 따라서, 알고리즘 형태로 문제해결 방법을 다시

정리하면 다음과 같다.

인수 n=2부터 number까지

1은 배수에서 제외되기 때문에, 배수 m = 2부터 number까지

index = nm으로 배수를 구하고

index가 배열의 크기를 초과할 수 있기 때문에, index <= number인 경우

array[index] = 1로 표시한다.

마지막까지 값이 0으로 남아 있는 배열의 인덱스는 소수이다.

이러한 분석에 의하여 에라토스테네스의 체 알고리즘을 다음과 같이 이중 루프로 설계할 수 있다.

<리스트 4-16> 에라토스테네스의 체 알고리즘

1. 인수 n=2 부터 number 까지 // 바깥쪽 루프

2. 배수 m=2 부터 number 까지 // 안쪽 루프

Page 91: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

89

3. index = n * m; // 곱을 구한다.

4. if (index <= number) // 만일 곱이 number보다 같거나 작으면

5. array[index] = 1; // 소수가 아님을 표시한다.

6. else // 더 조사할 필요가 없으므로

7. break; // 안쪽 루프를 벗어나고, 다음 바깥 루프를 실행한다.

8. 이중 반복문이 끝나고 array[]의 값이 0인 인덱스가 소수이다.

이 알고리즘은 두 가지 개선할 수 있는 부분이 있다.

소수 구할 때와 마찬가지 이유로 라인 1의 반복문을 2부터 number까지 수행할 필요가 없다.

즉, 2부터 number 까지만 조사해도 충분하다.

인수의 값이 결정되면, 안쪽 루프의 마지막 값은 number/n이다. 라인 2의 반복문을 배수

m=2부터 number/n까지로 제한하면, 곱한 값(mul)이 number보다 큰 경우가 발생하지 않는

다. 따라서, 라인 4의 if 조건문과 라인 6의 else가 불필요해진다.

이와 같은 두 가지 사항을 적용하여 개선한 에라토스테네스의 체 알고리즘은 다음과 같다.

<리스트 4-17> 개선한 에라토스테네스의 체 알고리즘

1. to = number

2. 인수 n=2 부터 to 까지 // 바깥쪽 루프

3. last = number/n

4. 배수 m=2 부터 last 까지 // 안쪽 루프

5. index = n * m; // 곱을 구한다.

6. array[index] = 1; // 소수가 아님을 표시한다.

7. 이중 반복문이 끝나고 array[]의 값이 0인 인덱스가 소수이다.

4.3.6 일차원 배열 전달

에라토스테네스의 체 알고리즘을 구하려면, main() 함수가 갖고 있는 일차원 배열을 함수로 전

달하여야 한다. 이 알고리즘을 소스 코드로 구현하기 전에 일차원 배열을 함수로 전달하는 방법

을 알아보자. 정수형 배열 array[10]을 전달하려고 하고, 배열을 파라미터로 전달 받는 함수를

void func()라고 하자.

배열을 전달하려는 함수는 다음과 같이 func() 함수를 호출하면서 배열의 이름을 전달한다. 배

열의 이름은 배열의 시작 주소이기 때문에 실제로 전달되는 것은 배열의 시작 주소이다.

func(array); // 배열의 이름 전달

배열을 파라미터로 전달 받는 함수는 함수를 정의할 때 파라미터가 배열임을 선언하여야 한다.

이 방법은 다음과 같이 두 가지 방법이 있으나, 효과 면에서는 동일하다.

void func(int array[]); // array가 일차원 배열임을 표시한다.

Page 92: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

90

void func(int *array); // array로 전달 받은 것이 정수형 포인터임을 표시한다.

그러나, func() 함수는 배열의 시작 주소만을 전달 받았기 때문에, 배열의 크기를 알 수 없다. 따라

서, 배열의 크기를 별도의 파라미터로 전달 받거나 또는 전역적으로 사용할 수 있는 변수를 사용

해야 한다.

4.3.7 에라토스테네스의 체 구현

소수를 구하는 함수와 소수를 출력하는 함수를 구현해 보자. 소수를 구하는 함수를 다음과 같

이 정의한다.

void eratos(int number, char array[])

파라미터

int number: 소수를 판정하려고 하는 최대 수

char array[]: 함수의 입력과 출력으로 모두 사용

입력: 크기가 number+1이고 0으로 초기화된 배열

출력: 소수에 해당하는 원소의 값이 0이고, 소수가 아닌 원소의 값이 1인 배열

리턴 값: 없음.

배열의 이름은 포인터이기 때문에 eratos() 함수에서 배열의 값을 변경하면 호출한 main() 함수에

서 변경된 값을 사용할 수 있다. 이제 eratos() 함수를 구현해 보자. 이 함수 안에서 sqrt() 함수를

사용하므로, 헤더파일을 추가하고, 함수 정의문을 소스 코드의 앞 부분에 추가하고 나서, 다음과

같이 eratos() 함수를 구현한다.

<리스트 4-18> eratos() 함수의 구현

1. #include <math.h> // 소스 파일의 앞 부분에

2. void eratos(int number, char array[]); // 추가한다.

3. void main()

4. {

5. …

6. // 리스트 4-15의 라인 14를 소수를 구하는 함수로 교체한다.

7. eratos(number, array); // 배열의 수, 배열의 이름

8. …

9. }

// 리스트 4-17 구현

10. void eratos(int number, char array[]) // main() 함수 뒤에 구현한다.

11. {

12. int n, m, to, last, index;

13. to = (int)sqrt((double)number); // 마지막 나눌 값

Page 93: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

91

14. for (n=2; n<=to; n++) // 바깥쪽 루프

15. {

16. last = number/n; // 마지막 조사 값

17. for (m=2; m<=last; m++) // 안쪽 루프

18. {

19. index = n * m; // 배열의 인덱스

20. array[index] = 1; // 소수가 아님을 표시한다.

21. }

22. } // 이중 반복문이 끝났을 때까지 값이 0인 원소의 인덱스가 소수이다.

23. }

마지막으로 소수를 출력하는 함수를 구현한다. 이 함수를 다음과 같이 선언한다. 배열을 전달

받을 때, char array[]로 표시하는 것과 char *array로 표시하는 것의 효과가 동일하다는 것을 보이

기 위하여 파라미터를 char *array로 받는다.

void print_primes(int count, char *array)

파라미터

int count: 배열의 크기. 실제 크기는 count+1

char *array: 소수를 결정한 값이 저장된 배열에 대한 포인터

리턴 값: 없음

소수를 판정한 결과는 일차원 배열에 저장되어 있다. 출력할 숫자가 많을 수 있으므로, 콘솔 응

용 프로그램의 화면의 크기가 가로 80 문자 세로 25 문자임을 고려하여, 한 줄에 8 개씩 출력하

도록 한다. print_prime() 함수를 다음과 같이 설계하고 구현할 수 있다. 프로그램의 실행 결과는

그림 3-8이다.

<리스트 4-19> 출력 함수

1. 출력한 숫자의 수를 0으로 초기화 한다. (count = 0)

2. 인수 n=2 부터 n<=number 까지

3. 만일 array[n] = 0 이면,

4. 배열의 인덱스인 n을 출력하고,

5. count를 증가시킨다.

6. 만일 한 줄에 8 개의 소수를 출력하였다면

7. 다음 줄로 이동한다. (\n을 출력한다.)

<리스트 4-20> print_prime() 함수의 구현

1. void print_primes(int count, char *array); // 소스 앞 부분에 함수 선언 추가

2. // main()의 eratos() 함수 호출 뒤에 추가

3. print_primes(number, array);

Page 94: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

92

4. void print_primes(int value, char *array) // 리스트 3-23 구현

5. {

6. int n;

7. int count = 0;

8. printf(“%d의 소수:\n”, value);

9. for (n=2; n<=value; n++)

10. {

11. if (array[n] == 0) // 소수인 것만

12. {

13. printf("%9d", n); // 소수를 9 칸의 공간에 출력한다.

14. count += 1; // 한 줄에 출력한 소수의 수

15. if ((count % 8) == 0) // 한 줄에 8 개를 출력하였다면,

16. printf("\n"); // 다음 줄로 이동한다.

17. }

18. }

19. printf("\n");

20. }

<그림 4-6> 에라토스테네스의 체 실행 결과

4.4 요약

이 장에서는 소수 문제를 사용하여 프로그램을 개발하였다. 이 과정에서 서술 문장으로 표현되

는 문제해결 방법을 알고리즘으로 표현하는 방법을 다루었다. 새로 소개된 C 언어 사용법은 다음

Page 95: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

93

과 같다.

무한 루프를 사용하기 위하여 while (1) { … } 구조를 사용한다.

C 언어는 0이 아닌 모든 수를 참으로, 0을 거짓으로 판정한다.

함수의 리턴 값을 직접 if 문의 조건으로 사용할 수 있다.

for 반복문으로 표현되는 구조를 while 반복문으로 만들 수 있다.

배열의 크기가 일정하지 않을 때 동적으로 배열을 할당하여 사용하는 것이 좋다.

배열을 동적으로 할당하는 방법은 다음과 같다.

배열의 원소로 저장하려는 데이터의 자료형에 대한 포인터를 선언한다.

프로그램 실행 중 배열이 필요할 때, 기억장소를 동적으로 할당하여 포인터에 연결한다.

할당된 기억장소를 배열처럼 사용한다.

배열을 모두 사용하였다면, 할당 받은 기억장소를 해제한다.

동적으로 배열을 할당하였다면, 프로그램이 종료하기 전에 반드시 해제하여야 한다.

일차원 배열을 함수로 전달할 때 배열의 이름을 전달한다. 배열의 이름은 배열의 시작주소이

고 상수이다.

배열의 이름을 전달 받은 함수는 배열의 시작 주소를 알 수 있을 뿐이고, 배열의 크기를 알

수 없다. 따라서 배열의 크기를 전역 변수로 선언하거나 별도의 파라미터로 전달해 주어야

한다.

이 장에서 새로 소개된 라이브러리 함수는 다음과 같다.

void *malloc(size_t size) // size 바이트의 기억장소를 할당한다.

void free(void *ptr) // 기억장소를 해제한다.

void *memset(void *ptr, int c, size_t count) // 기억장소를 c로 초기화 한다.

void *calloc(size_t num, size_t size) // 0으로 초기화된 기억장소를 할당한다.

Page 96: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

94

제5장 수치계산

컴퓨터는 그 이름이 의미하듯이 계산을 수행하는 기계이다. 이 장에서는 컴퓨터 다음과 같은

계산 문제들을 프로그램으로 개발해 본다.

1. 팩토리얼: n!을 계산하여 출력한다.

2. 자연대수(e): 무한 수열 공식을 사용하여 자연대수 e의 값을 구한다.

3. 원주율(π): 무한 수열 공식을 사용하여 원주율 π의 값을 구한다.

4. 적분: 사다리꼴 공식에 의하여 수학 함수를 적분한다.

이 장에서 다루는 수학 문제는 입력 변수로부터 출력을 산출하는 문제해결 방법이 공식으로 주

어진다. 따라서, 문제해결 방법을 별도로 기술하지 않는다. 알고리즘 설계는 주어진 수학 공식에

대한 일반항을 구하고 이것을 반복적으로 계산하여 결과를 도출하는 과정으로 이루어진다. 이 장

에서는 반복문 이외에 다음과 같은 프로그래밍 기법을 학습할 수 있다.

순환 호출(recursive call)

자료형의 표현 범위로 인한 수의 오버플로우

#define 문에 의한 매크로 함수

수치 계산 라이브러리 함수

5.1 팩토리얼

정수 n에 대한 n!의 계산식은 다음과 같다. 1!부터 20!까지 계산하여 출력하라.

nn 21!

5.1.1 요구사항 분석

문제에 요구사항이 명확하게 기술되어 있으므로, 더 이상 요구사항을 정리할 필요가 없다. 사용

할 자료형과 입출력 화면에 대하여 생각해 보자.

팩토리얼은 n이 증가함에 따라 계산 값이 기하급수적으로 증가한다. 따라서, 정수형 변수를 사

용하여 팩토리얼을 계산하면, 계산 결과가 정수의 표현 범위를 넘어설 가능성이 있다. 따라서, 정

수로 계산한 팩토리얼 값과 수의 표현 범위가 가장 큰 배정도 실수(double)로 계산한 팩토리얼을

구하여 서로 비교해보기로 하자. 프로그램의 출력을 다음과 같이 정한다.

n 정수 팩토리얼 실수 팩토리얼

==========================================

1: 1 1

2: 2 2

3: 6 6

Page 97: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

95

20: xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxx

5.1.2 자료구조 설계

이 문제는 사용자로부터 입력을 받지 않는다. 프로그램 내부적으로 1부터 20까지 변하는 정수

를 사용하고, 정수 팩토리얼 결과와 실수 팩토리얼 결과를 저장할 변수를 다음과 같이 정한다.

int n; // 1부터 20까지 변하는 변수

int ifact; // 정수로 계산한 팩토리얼

double dfact; // 실수로 계산한 팩토리얼

5.1.3 알고리즘 설계

팩토리얼 계산식은 nn 21! 이다. 팩토리얼을 계산하는 알고리즘을 반복문으로 기술할

수 있다. 곱셈을 누적하는 경우 초기값을 1로 설정해야 한다.

<리스트 5-1> 팩토리얼 계산 알고리즘

1. fact = 1; // 초기화

2. n=1부터 n=20까지

3. fact = fact * n;

5.1.4 프로그램 구현

프로그램이 간단하므로, main 함수 안에 팩토리얼 계산 알고리즘을 구현해 보자. 다만, 리스트

1을 구현할 때, 정수 팩토리얼과 실수 팩토리얼을 각각 계산하고 출력하는 부분이 추가된다. 구현

결과는 다음과 같다.

<리스트 5-2> 팩토리얼 계산 구현

1. #include <stdio.h>

2. void main()

3. {

4. int n; // 1부터 20까지 변하는 변수

5. int ifact = 1; // 정수 팩토리얼 변수 선언 및 초기화

6. double dfact = 1.0; // 실수 팩토리얼 변수 선언 및 초기화

7.

8. printf(" n 정수 팩토리얼 실수 팩토리얼\n"); // 제목 줄 인쇄

Page 98: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

96

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

10. for (n=1; n<=20; n++)

11. {

12. ifact = ifact * n; // 정수 팩토리얼

13. dfact = dfact * (double)n; // 실수 팩토리얼

14. printf("%2d: %20d %20.0lf\n", n, ifact, dfact); // 결과 출력

15. }

16. }

라인 5, 6: 변수를 선언하면서 초기화한다.

라인 13: 실수 연산을 수행하기 위하여 정수형 변수 n의 자료형을 변환한다.

<그림 5-1> 팩토리얼 계산 실행 결과

<표 5-1> 수의 표현 범위

자료형 바이트 수 표현 범위

Int 4 -2,147,483,648 ~ -2,147,483,647

Double 8 2.210-308 ~ 1.810+308

그림 5-1은 프로그램의 출력 화면이다. C 언어의 int 형과 double 형의 수 표현 범위는 표 5-1

과 같다. 따라서, 정수형 데이터가 2,147,483,647 이상이면, 오버플로우(overflow)가 발생한다. 오버

플로우란 자료형의 표현 범위를 초과한다는 의미이다. 그림 5-1을 보면 13! 이후에 오버플로우가

발생하였음을 알 수 있다.

Page 99: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

97

5.1.5 순환 호출

앞에서 팩토리얼을 계산한 알고리즘은 팩토리얼 공식을 반복문으로 구현한 것이다. 수학 공식

을 계산할 때, 이전 항과의 차이를 이용하여 계산할 수도 있다. n!을 계산하는 방법은 (n-1)!을 계

산하는 방법과 같다. n! 계산 방법을 다음과 같이 표현할 수 있다.

n=1일 때, n! = 1;

n>=2일 때, n! = (n-1)! * n;

위 공식을 보면, n!을 계산하는데 (n-1)!이 포함되어 있다. 팩토리얼 계산을 factorial() 함수로 구현

하는 경우, 위 표현을 그대로 함수로 구현할 수 있다.

<리스트 5-3> factorial() 함수 알고리즘

1. int factorial(int n) // n!을 계산하여 리턴한다.

2. {

3. 만일 n=1이면, // n=1 일 때,

4. 1을 리턴한다. // n!의 계산 값은 1이다.

5. 아니면, // n>=2 일 때,

6. factorial(n-1) * n을 리턴한다. // n!의 계산 값은 (n-1)! * n 이다.

7. }

factorial() 함수의 라인 6은 자기 자신을 호출한다. 이것을 순환 호출(recursive call)이라고 한다.

순환 호출 함수를 잘못 만들면, 무한히 자기 자신을 호출하여 프로그램이 종료하지 않을 수 있다.

그러므로 순환 호출을 만들 때 함수를 종료하는 조건을 분명하게 확인하여야 한다. 리스트 5-3에

서 라인 3, 4가 그 역할을 한다. 다음은 순환 호출 알고리즘으로 오버플로우가 발생하지 않는 12!

까지 계산하여 출력하는 예제이다.

<리스트 5-4> 순환 함수에 의한 팩토리얼 계산

1. #include <stdio.h>

2. int factorial(int n);

3. void main()

4. {

5. int n, ifact = 1;

6. printf(" n 정수 팩토리얼\n");

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

8. for (n=1; n<=12; n++)

9. {

10. ifact = factorial(n); // 함수 호출

11. printf("%2d: %20d\n", n, ifact);

Page 100: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

98

12. }

13. }

14. int factorial(int n)

15. {

16. if (n==1) // 순환함수 탈출 조건

17. return 1;

18. else

19. return n*ifactorial(n-1); // 순환 호출

20. }

프로그래밍 언어는 순환 호출 기능을 제공하므로, 순환 호출은 프로그램의 특성을 학습하는데

도움이 된다. 그리고, 이전 항으로 수열을 계산하는 공식을 그대로 알고리즘으로 표현하면 되므로,

알고리즘을 개발하기 쉽다는 장점이 있다. 그렇지만, 컴퓨터가 프로그램을 실행할 때 함수 호출이

가장 시간을 많이 소모한다는 특성 때문에, 알고리즘을 순환 호출로 개발하는 것보다 반복문으로

해결하는 것이 더 효율적이다.

5.2 자연대수(e)

다음 공식을 사용하여 자연대수 e의 값을 구하라.

!

1

!3

1

!2

1

!1

11

ne

5.2.1 요구사항 분석

수열 문제의 일종이다. 팩토리얼 n!의 값은 n이 증가할 때마다 지수적으로 증가한다. 1/n!의 값

은 n의 값이 증가함에 따라 0으로 수렴한다. 반복문을 제어하는 n의 값을 입력 받지 않고, n-1 단

계까지 계산한 자연대수의 값이 n 단계까지 계산한 자연대수의 값과 같아질 때까지, 즉 1/n! 0

일 때까지 자연대수의 값을 계산해 보자. 다음과 같이 실제 자연대수 값과 수열로 계산한 값의

오차도 출력해 보자. 자연대수를 소수점 18자리까지 표시한다.

n 자연대수 수열 계산 오차

================================================

1 2.718… 1.000… 1.718….

2 2.718… 2.000… 0.718….

5.2.2 자료구조 설계

이 문제도 사용자의 입력을 받지 않는다. 다만, 자연대수 수열 계산을 이전 값과 새로 계산한

값이 같아질 때까지 반복하기로 하였으므로, n-1 단계까지 계산한 자연대수 값과 n 단계까지 계

산한 자연대수 값을 저장할 변수를 별도로 사용한다. 필요한 변수와 변수의 초기 값은 다음과 같

Page 101: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

99

다.

int n = 1: 자연대수를 계산하는 항의 수

double nexp: 라이브러리 함수로 계산한 실제 자연대수

double fact = 1: 팩토리얼을 계산한 값. 팩토리얼을 계속 곱해야 하므로 1로 초기화한다.

double newexp = 0.0: n 단계까지 계산한 자연대수 값. 항을 계속 더해야 하므로 0으로 설정

한다.

double oldexp = -1.0: n-1 단계까지 계산한 자연대수 값. newexp와 다른 값을 할당하여 초기

화한다.

5.2.3 알고리즘 설계

반복문을 통하여 주어진 공식을 바로 계산할 수 있다. 자연대수를 구하는 프로그램의 구조는

다음과 같다.

<리스트 5-5> 자연대수 계산 알고리즘

1. 자연대수를 구하여 nexp에 저장한다.

2. 헤드라인을 출력한다.

3. 변수를 초기화 한다. (n = 1, newexp = 0, oldexp = -1.0, fact = 1)

4. while (oldexp != newexp) // 같지 않으면, 다음을 반복한다.

5. oldexp = newexp; // 이전 값을 갱신한다.

6. newexp = newexp + 1/fact // 팩토리얼의 역수를 더한다.

7. 자연대수를 계산한 결과와 오차를 출력한다.

8. fact = fact * n; // 다음 반복에서 사용할 팩토리얼을 갱신한다.

9. n = n + 1; // n을 증가시킨다.

5.2.4 절대값 계산

프로그램을 구현하기 전에 두 수의 차이를 구하는 방법을 알아보자. 두 수 a와 b의 차이는 a-b

의 절대값(absolute value)이다. 프로그램으로 두 수의 차이를 구하는 방법은 다음과 같다.

if (a > b)

diff = a – b;

else

diff = b – a;

C 언어는 위와 같이 간단한 if 문을 한 개의 문장으로 표현하는 방법을 제공한다.

diff = (a > b) ? a – b : b – a;

이 기능과 #define 매크로 기능을 사용하여 절대값을 구할 수도 있다.

#define ABS(x) ((x) > 0) ? (x) : -(x) // 함수 헤더 부분에 선언

Page 102: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

100

diff = ABS(a-b); // 매크로 사용

파라미터를 받는 매크로는 전처리 과정에서 변수 x에 파라미터로 전달된 인수를 확장한다. 그러

므로 위 문장은 다음과 같이 확장된다.

diff = ((a-b) > 0) ? (a –b) : -(a-b);

마지막 방법은 라이브러리 함수를 사용하는 것이다. 라이브러리 함수는 사용하는 변수의 형에 따

라 두 가지 절대값 함수가 있다.

#include <math.h>

int abs(int x); // 정수에 대한 절대값을 구함.

double fabs(int x); // 실수에 대한 절대값을 구함

5.2.5 프로그램 구현

자연대수를 계산하는 라이브러리 함수는 다음과 같이 정의되어 있다.

#include <math.h>

double exp(double x); // ex를 계산하여 리턴한다.

따라서 자연대수 e의 값은 exp(1.0)으로 구할 수 있다. 자연대수 값과 계산한 값의 오차를 구하

기 위하여 fabs() 함수를 사용하기로 한다. 다음은 자연대수 알고리즘을 프로그램 언어로 구현한

것이다. 프로그램의 실행 결과는 그림 4-2와 같으며, 19항 이후 계산 값이 변하지 않는다.

<리스트 5-6> 자연대수 프로그램

1. #include <stdio.h>

2. #include <math.h> // exp(), fabs() 함수 선언

3. void main()

4. {

5. int n;

6. double nexp, newexp, oldexp, fact;

7. nexp = exp(1.0); // 자연대수 e

8. printf(" n 자연대수 계산 오차\n");

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

10. n = 1; // 변수 초기화

11. newexp = 0.0; // 새로 계산할 자연대수

12. oldexp = -1.0; // 이전 자연대수. 초기에 oldexp와 newexp를 다르게 설정한다.

Page 103: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

101

13. fact = 1.0; // 팩토리얼

14. while (oldexp != newexp) // 이전 값과 새로운 값이 다르면 계속 반복

15. {

16. oldexp = newexp; // 이전 값으로 저장한다.

17. newexp = newexp + 1.0/fact; // 새로 계산한다.

18. printf("%2d %20.18lf %20.18lf %20.18lf\n",

n, nexp, newexp, fabs(nexp-newexp));

19. fact = fact * (double)n; // 다음을 위하여 팩토리얼을 미리 계산한다.

20. n = n + 1; // n을 증가시킨다.

21. }

22. }

라인 12: 처음에 라인 14의 반복문의 조건이 참이 되도록 oldexp의 값을 newexp의 초기값인

0과 다르게 초기화한다. oldexp의 초기값은 0 이외의 값으로 설정할 수 있다.

<그림 5-2> 자연대수 실행 결과

5.3 원주율(π)

두 개의 무한 수열을 사용하여 원주율(π)의 값을 구하고, 실제 원주율 값과 오차를 출력하라.

11

4

9

4

7

4

5

4

3

4

1

4

Page 104: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

102

)4

1

3

1

2

1

1

1(6

2222

5.3.1 요구사항 분석

무한 수열은 항의 수가 많아질수록 원주율(π)의 값에 가까운 근사치를 구할 수 있다. 그러나 수

열 공식 모두 항의 수에 따라 값이 지수적으로 감소하지 않기 때문에 원래의 원주율 값에 빠르게

수렴하지 않는다. 원주율을 10,000 항까지 계산하고 1,000 항마다. 결과를 출력해 보자. 출력 화면

을 다음과 같이 설계한다. 원주율을 소수점 8째 자리까지 출력하기로 한다.

원주율 = 3.141592…

====================================================

항의 수 공식1 오차 공식2 오차

====================================================

1000 xxxxxx xxxxx xxxxx xxxxx

2000 xxxxxx xxxxx xxxxx xxxxx

5.3.2 자료구조 설계

원래의 원주율과 주어진 공식을 계산하기 위한 변수를 다음과 같이 정한다.

int n; // 1부터 10,000까지 변하는 항의 수

double PI; // 원래의 원주율

double pi1; // 첫 번째 공식으로 구한 원주율

double pi2; // 두 번째 공식으로 구한 원주율

5.3.3 알고리즘 설계

수열 공식으로 주어진 원주율을 프로그램을 구하려면, n 번째 항을 일반화하여 표현할 수 있어

야 한다. 일반항을 사용하여 첫 번째 원주율 수열을 표현하면 다음과 같다.

)12

4()1(

11

4

9

4

7

4

5

4

3

4

1

4

n

n

첫 번째 공식부터 알고리즘으로 변환해 보자. 이 함수를 C 언어로 구현할 때 (-1)n을 계산하는

부분이 문제이다. 물론 xy을 계산하는 pow() 함수를 사용할 수도 있다.

#include <math.h>

double pow(double x, double y)

pow() 함수는 두 개의 실수에 대하여 xy을 계산하므로, (-1)n을 계산하기 위하여 pow(-1.0,

(double)n)과 같이 사용해도 되지만 부담스럽다. 대신에 다음과 같은 두 가지 방법 중 하나를 사

용하는 것이 좋다.

Page 105: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

103

n이 짝수이면 sign = 1, n이 홀수이면 sign = -1

sign = ((n % 2)==0) ? 1 : -1;

정수형 변수 sign을 -1로 초기화하고, 반복문을 수행할 때마다 -1을 곱한다.

sign = -1;

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

{

sign *= -1;

}

두 번째 방법을 사용해 보자. 첫 번째 공식을 다음과 같은 알고리즘으로 구할 수 있다.

<리스트 5-7> 첫 번째 공식 알고리즘

1. pi1 = 0; // 초기값 설정

2. sign = -1; // 부호 초기화

3. for (n=1; n<=10000; n++)

4. {

5. sign *= -1; // 부호 결정

6. pi1 += (double)sign * 4.0 / (2.0 * (double)n – 1.0); // 일반항 누적

7. }

두 번째 원주율 공식을 일반항으로 표현하면 다음과 같다.

22222

6)

4

1

3

1

2

1

1

1(6

n

baba 이므로 제곱근 안의 공식을 누적한 후 제곱근을 구해야 한다. 합 항을 누적할

변수를 pi2sqr이라고 할 때, 두 번째 공식으로 원주율을 구하는 알고리즘을 다음과 같이 설계할

수 있다.

<리스트 5-8> 두 번째 공식 알고리즘

1. pi2 = 0; // 초기값 설정

2. for (n=1; n<=10000; n++)

3. {

4. pi2sqr += 6.0 / ((double)n*(double)n); // 일반 항 누적

5. pi2 = sqrt(pi2sqr); // 제곱근

6. }

Page 106: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

104

5.3.4 원주율

먼저 원주율을 구하는 방법을 알아보자. 수학 함수를 사용하는 프로그램의 경우 원주율의 값은

자주 사용된다. 원주율의 값이 무한 소수이므로, 컴퓨터 프로그램으로 구현하는 경우 어느 정도의

오차를 허용할 수 밖에 없다. 간단하게 다음과 같이 #define 문으로 원주율을 정의하여 사용할

수 있다.

#define PI 3.141592

그렇지만, 삼각 함수를 사용하여 컴퓨터가 허용하는 최소의 오차 범위로 원주율 값을 정할 수 있

다. tan 45는 1.0이고, 45는 π/4이므로, tan-1을 구하는 라이브러리 함수를 사용하면, 정확한 원

주율을 구할 수 있다.

pi = 4.0 * atan(1.0); // arctan(1.0) = π/4

이 공식을 사용하여 구한 원주율의 값은 그림 5-3과 같다. 원주율은 무한 소수이지만, 컴퓨터는

이 이상 상세한 원주율을 처리하지 못한다.

<리스트 5-9> 원주율 계산

1. #include <stdio.h>

2. #include <math.h>

3. void main(void)

4. {

5. double PI; // 지역변수

6. PI = 4.0 * atan(1.0);

7. printf(“PI = %20.18lf\n”, PI);

8. }

<그림 5-3> 삼각함수로 구한 원주율

5.3.5 프로그램 구현

두 개의 원주율 알고리즘을 하나의 반복문으로 구현해 보자. 알고리즘을 설계한 과정에서 두

개의 변수가 추가되었다. 구현 결과는 다음과 같다.

<리스트 5-10> main() 함수

1. #include <stdio.h>

2. #include <math.h>

3. void main()

Page 107: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

105

4. {

5. int n, sign = -1;

6. double PI, pi1, pi2, pi2sqr;

7. PI = 4.0*atan(1.0); // 원주율 계산

8. printf("원주율 = %10.8lf\n", PI); // 원주율 출력

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

10. printf("항의 수\t공식1\t\t오차\t\t공식2\t\t오차\n"); // 제목 줄 출력

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

12. pi1 = 0.0; // 첫 번째 공식

13. pi2sqr = 0.0; // 두 번째 공식

14. sign = -1; // 부호 초기화

15. for (n=1; n<=10000; n++)

16. {

17. sign *= -1; // 부호

18. pi1 += (double)sign * 4.0 / (2.0 * (double)n - 1.0); // 첫 번째 공식

19. pi2sqr += 6.0 / ((double)n*(double)n); // 두 번째 공식

20. pi2 = sqrt(pi2sqr);

21. if ((n % 1000) == 0) // 1,000 항마다

22. printf("%6d\t%10.8lf\t%10.8lf\t%10.8lf\t%10.8lf\n", // 결과 출력

n, pi1, fabs(PI-pi1), pi2, fabs(PI-pi2));

23. }

24. }

C 언어는 대문자와 소문자를 구별하여 식별자(identifier, 변수 및 함수의 이름)를 인식한다.

라인 21: 1,000 항마다 계산 결과를 출력하기 위하여 n을 1,000으로 나눈 나머지가 0일 때

출력한다.

라인 10, 22: printf() 함수의 형식 문자열 안에 있는 ‘\t’는 탭(tab)을 출력하라는 의미이다.

그림 5-4는 프로그램 출력 화면이다. 원주율을 구하는 두 수열의 정확도가 생각보다 크게 차이

나지 않는다.

Page 108: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

106

<그림 5-4> 원주율 출력 화면

5.4 수치적분 (사다리꼴 공식)

b

adxxf )( 를 구할 때 적분 구간 [a, b]를 n 등분하면, 소구간의 폭은 h = (b-a)/n이다. 각 분점

의 x 좌표를 x0(=a), x1, x2, …, xn(=b)라고 할 때, 이에 대응하는 y의 값은 y0=f(a), y1=f(x1), y2=f(x2)…,

yn=f(b)이다. 사다리꼴 공식(trapezoid equation)을 이용한 정적분의 근사값은 다음과 같다.

hyyhyyhyyhyydxxf nn

b

a)(

2

1)(

2

1)(

2

1)(

2

1)( 1322110

사다리꼴 공식에 의하여 2log1

11

0edx

x

에 대하여 적분을 구하고, 라이브러리 함수로 loge2를

계산하여 정적분 값과 비교하라.

5.4.1 요구사항 분석

적분 문제는 소구간을 잘게 나눌수록 실제 적분의 값에 수렴한다. 구간을 10부터 10,000,000까

지 10 배씩 증가시켜가면서 loge2의 값과 사다리꼴 공식에 의한 적분 값을 비교해 보자. 출력 화

면을 다음과 같이 정한다.

구간 log2 사다리꼴 적분 오차

=====================================================

10 0.693147xxx 0.693……. 0.0006…….

100 0.693147xxx 0.693……. 0.0000…….

5.4.2 자료구조 설계

소구간의 수가 변할 때마다 사다리꼴 적분을 다시 계산해야 한다. 일단 main() 함수에서 필요한

변수를 다음과 같이 정한다.

Page 109: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

107

int steps; // 구간의 수

double log2; // loge2

double integ; // 사다리꼴 공식으로 구한 적분 값

5.4.3 프로그램 구조 설계

사다리꼴 적분을 함수로 구현하기로 하고, main() 함수의 구조를 먼저 설계하고 구현해 보자. 프

로그램의 흐름을 다음과 같이 설계할 수 있다.

<리스트 5-11> 사다리꼴 공식 main() 함수

1. loge2를 구한다.

2. 구간을 10부터 10,000,000까지 10 배씩 증가시켜가면서

3. 사다리꼴 공식에 의한 적분을 구하고

4. 계산 결과와 오차를 출력한다.

로그를 계산하는 라이브러리 함수는 두 가지가 있으며, 다음과 같이 정의되어 있다.

#include <math.h>

double log(double x); // logex를 구한다.

double log10(double x); // log10x를 구한다.

loge2를 구하기 위하여 log() 함수를 사용한다. 먼저 main() 함수를 다음과 같이 구현한다.

<리스트 5-12> 프로그램의 흐름 구현

1. #include <stdio.h>

2. #include <math.h>

3. void main()

4. {

5. double log2, integ;

6. int steps;

7. log2 = log(2.0); // loge2

8. printf(" 구간 log2 사다리꼴 적분 오차\n");

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

10. for (steps=10; steps<=10000000; steps *= 10)

11. {

12. integ = log2; // 임시. 함수 호출로 대치할 부분

13. printf("%8d %20.18lf %20.18lf %20.18lf\n",

steps, log2, integ, fabs(log2 - integ));

Page 110: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

108

14. }

15. }

라인 10: 반복문을 실행할 때마다, 구간을 10 배로 늘린다.

라인 12: 임시로 loge2를 할당하였고, 나중에 함수 호출로 대치한다.

프로그램을 실행하여 흐름 제어에 오류가 없음을 확인하고 다음 단계로 넘어가자.

5.4.4 사다리꼴 적분

사다리꼴 적분 공식에 의하여

1

0 1

1dx

x을 수행하려면 각 구간에 해당하는 xk에 대하여 yk =

1/(1+xk)를 계산해야 한다. 적분할 함수가 간단하므로 yk을 다음과 같이 매크로로 정의하여 사용

하기로 하자. 만일 함수가 복잡하다면 별도의 함수로 만드는 것이 좋다.

#define YN(x) (1.0/(1.0 + (x)))

사다리꼴 공식을 정리하면 다음과 같다. 따라서, y0와 yn은 한 번만 더하고, 나머지 y1, y2, …, yn-1

은 두 번 더해야 한다. 적분 알고리즘은 다음과 같다.

})(2{2

)(2

1)(

2

1)(

2

1)(

2

1)(

1210

1322110

nn

nn

b

a

yyyyyh

hyyhyyhyyhyydxxf

<리스트 5-13> 사다리꼴 적분 알고리즘

1. 적분 값을 초기화 한다.

2. h를 초기화한다. (h = 1/steps)

3. n=0부터 steps까지

4. xk를 구한다. (xk = h * n)

5. 만일 n = 0 또는 n = steps이면

6. 적분 값에 yk을 더한다. ( yk= YN(xk))

7. 그 외의 경우는

8. 적분 값에 2 * yk을 더한다.

9. 적분 값에 h/2를 곱한다.

10. 적분 값을 리턴한다.

5.4.5 논리 연산자

리스트 5-13을 구현하려면 라인 5의 조건문에서 두 개의 조건식에 대한 참과 거짓을 결정해야

한다. 관계 연산자로 표현되는 조건식의 결과는 참 또는 거짓 값을 갖는 논리항이다. 프로그래밍

언어는 논리항들을 연산하는 논리 연산자를 제공한다. C 언어는 표 5-2 같이 논리곱, 논리합, 논

리부정에 대한 논리 연산자를 제공한다.

Page 111: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

109

표 5-2 논리 연산자

논리 연산자 의미 사용 예

&& 논리곱(AND) if ((a>0) && (a<10))

|| 논리합(OR) if ((n==0) || (n==steps))

! 논리부정(NOT) if ( !(x==0) )

논리곱은 두 개의 논리항의 값이 모두 참일 때만 참이고, 논리합은 두 개의 논리항의 값 중에서

하나라도 참이면 전체가 참이다. 논리부정은 하나의 논리항에 대하여 반대 값을 산출한다.

5.4.6 사다리꼴 적분 구현

적분 알고리즘을 구현하면, 다음과 같다. 그림 4-5는 프로그램 실행 결과이다.

<리스트 5-14> 적분 함수 구현

1. #define YN(x) (1.0/(1.0 + (x)) // 매크로 함수 정의

2. double trapez(int steps); // 함수 선언

3. …. // main() 함수

4. integ = trapez(steps); // 리스트 4-13의 라인 12를 함수 호출로 수정한다.

5. ….

6. double trapez(int steps) // 함수 구현

7. {

8. double integral, h, x; // 적분 값, 구간, xk

9. int k; // 반복문 제어 변수

10. integral = 0.0; // 적분 값 초기화

11. h = 1.0/(double) steps; // 구간의 폭

12. for (k=0; k<=steps; k++)

13. {

14. x = h * (double)k; // xk를 구한다. (xk = h * n)

15. if ((k==0) || (k== steps)) // 논리합. 만일 n = 0 또는 n = steps이면

16. integral += YN(x); // 적분 값에 yk을 더한다.

17. else // 그 외의 경우는

18. integral += 2.0 * YN(x); // 적분 값에 2 * yk을 더한다.

19. }

20. integral = h * integral / 2.0; // 적분 값에 h/2를 곱한다.

21. return integral; // 적분 값을 리턴한다.

22. }

Page 112: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

110

<그림 5-5> 사다리꼴 실행 결과

5.5 요약

이 장에서는 수학 문제들을 프로그램을 구현해 보았다. 수학 문제들은 문제해결 방법이 공식으

로 표현되는 경우가 많다. 이와 같은 문제를 해결하기 위하여 공식을 일반항으로 표현하고 일반

항을 반복적으로 계산하도록 알고리즘을 개발하였다. 새로 소개된 프로그래밍 기법은 다음과 같

다.

자료형에 따라 수의 표현 범위가 제한이 있다. 따라서, 자료형을 잘못 선택하면 오버플로우가

발생하여 올바른 결과를 산출하지 못할 수도 있다.

n 항까지 계산하는 공식이 (n-1) 항까지 계산하는 공식으로 표현될 수 있을 때, 알고리즘을

순환 호출 방법으로 설계하면 개념이 쉽다.

순환 호출 알고리즘을 설계할 때, 함수를 탈출하는 조건을 분명하게 확인하여야 한다.

#define 매크로를 사용하여 간단한 함수를 구현할 수 있다.

조건식은 논리 연산자를 포함할 수 있으며, 논리 연산자는 논리합(&&), 논리곱(||), 논리부정(!)

이 있다.

이 장에서는 주로 수치 계산을 위한 라이브러리 함수들이 소개되었다.

int abs(int x); // 정수에 대한 절대값을 구한다.

double fabs(int x); // 실수에 대한 절대값을 구한다.

double exp(double x); // ex를 계산하여 리턴한다.

double pow(double x, double y) // xy을 계산한다.

double atan(double x); // arctan(x)를 계산한다.

double log(double x); // logex를 구한다.

double log10(double x); // log10x를 구한다.

Page 113: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

111

제6장 난수, 배열응용

난수

- 특정한 배열 순서나 규칙을 가지지 않는 연속적인 임의의 수

rand() 함수

- C언어에서 난수를 발생시키는 시스템 라이브러리 함수

- stdlib.h. 파일을 include 해야 함.

- Rand() 함수에 의해 생성되는 정수의 범위는 0부터 RAND_MAX까지.

srand() 함수

- 매번 난수를 다르게 발생시키기 위하여 시드(seed) 값을 주는 방법

- 매번 다른 시드(seed) 값을 주기 위해 현재시간을 리턴하는 함수 time()으로부터 반환되

는 값을 활용

srand(time(NULL)); time() 함수 사용을 위해 time.h를 include 해야 함.

1. 최대값 찾기: 배열에 저장되어 있는 수 중에서 가장 큰 수를 찾는다.

2. 로또 게임: 컴퓨터가 생성한 6개의 숫자와 사용자가 입력한 6개의 숫자를 비교하여 맞은 개수

를 출력한다.

3. 빙고 게임: 5 X 5 임의의 수를 생성하여 컴퓨터와 사용자가 번갈아 숫자를 선택하여 게임을

진행하고 가로, 세로 한 줄을 모두 선택하면 게임 종료한다.

6.1 최대값 찾기

그림 6-1과 같이 정수형 배열을 초기화하고, 그 중에서 최대값을 찾아 출력하라.

<그림 6.1> 배열의 초기값

6.1.1 요구사항 분석

정렬과 탐색 알고리즘을 학습하기에 앞서, 정렬과 탐색의 기초 기술에 해당하는 최대값 구하는

과정을 먼저 이해해 보자. 요구사항 분석은 해결해야 할 문제를 명확하게 이해하고, 일단 처리 과

정은 제외하고 입력에 대하여 어떤 출력이 산출되어야 하는지 파악하는 단계이다.

문제에 입력 데이터가 명시되어 있으므로, 배열의 값 중에서 값이 가장 큰 9를 찾아 출력하는

것이 문제의 핵심이다. 초기화 과정에서 입력 데이터를 결정하므로 별도의 입력 과정은 없다. 출

력 데이터의 형식을 다음과 같이 정한다. 원본 데이터의 값을 한 줄에 5 개씩 출력하고, 최대값을

갖고 있는 배열의 인덱스와 최대값을 출력한다.

배열의 값

data[0]=3 data[1]=2 data[2]=9 data[3]=7 data[4]=1

data[5]=4 data[6]=8 data[7]=0 data[8]=6 data[9]=5

Page 114: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

112

최대값: data[2] = 9

6.1.2 자료구조 설계

프로그램의 입력 데이터를 정수형 배열에 저장해야 한다. 최대값을 갖고 있는 배열의 인덱스와

배열의 값을 출력해야 하므로, 최대값을 갖고 있는 배열의 인덱스를 저장할 변수가 필요하다. 그

러므로, 다음과 같이 자료구조를 정한다.

int data[10]; // 원본 데이터를 저장하고 있는 배열

int maxindex; // 배열 원소가 최대값인 배열의 인덱스

6.1.3 최대값 찾기 알고리즘

그림 6.1과 같은 상태의 배열에서 최대값을 찾는 알고리즘을 생각해 보자. 우리는 데이터를 한

번에 모두 보고 9가 가장 크다는 것을 알 수 있다. 그렇지만, 컴퓨터는 한 번에 두 개의 숫자만

비교할 수 있다. “2.2 수의 차이 구하기”에서 세 개의 변수 a, b, c에 저장되어 있는 수 중에서 최

대값과 최소값을 찾는 방법을 학습한 적이 있다. 이 알고리즘은 다음과 같다.

<리스트 6-1> 세 개의 수 a, b, c 중에서 최대값 찾기

6. max = a으로 설정하여 a을 최대값이라고 가정한다.

7. 만일 (max < b)이면

8. max = b로 설정하여 b를 최대값으로 결정한다.

9. 만일 (max < c)이면

10. max = c로 설정하여 c을 최대값으로 결정한다.

이 방법을 배열에 저장되어 있는 데이터에 적용할 수 있다. 배열의 원소가 여러 개이므로, 리스

트 6-1의 라인 2~5를 반복문으로 처리하여야 한다.

<리스트 6-2> 배열에서 최대값 찾기

1. max = data[0]; // data[0]을 최대값이라고 가정한다.

2. n=1부터 n<10일 때까지

3. 만일 max < data[n] 이면

4. max = data[n]; // 최대값을 교체한다.

5. 반복문을 마쳤을 때 max가 최대값이다.

리스트 6-2의 최대값 찾기 알고리즘은 반복문을 수행하면서 배열 안에서 최대값을 찾을 수 있으

나, 알고리즘이 끝난 후 배열의 몇 번째 원소가 최대값인지 알 수 없다. 최대값 찾기 알고리즘을

다음과 같이 수정하면, 이 문제를 해결할 수 있다.

<리스트 6-3> 배열 인덱스에 의한 최대값 찾기

1. maxindex = 0; // data[maxindex]를 최대값으로 가정한다.

2. n=1부터 n<10일 때까지

Page 115: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

113

3. 만일 data[maxindex] < data[n] 이면

4. maxindex = n; // 최대값을 갖고 있는 배열의 인덱스를 저장한다.

5. data[maxindex]가 최대값이다.

6.1.4 프로그램 구현

프로그램을 구현하기 위하여 배열을 초기화 하는 방법을 알아야 한다. C 언어는 배열을 선언하

면서 배열을 초기화하는 방법을 제공한다. 다음은 변수와 배열을 초기화 하는 방법이다.

자료형 변수이름 = 초기값; // 변수 초기화

자료형 배열이름[배열크기] = { 값1, 값2, …, 값n }; // 배열 초기화

배열의 초기값은 { } 사이에 배열의 원소 수만큼 쉼표(,)로 구분해서 적는다. 다만 마지막 값 다음

에 쉼표를 붙이지 않는다. 다음은 변수와 배열을 초기화 한 예이다.

int varint = 5; // 변수 초기화

int data[10] = { 3, 2, 9, 7, 1, 4, 8, 0, 6, 5 }; // 배열 초기화

프로그램을 작성할 때, 기능 단위로 나누어 별도의 함수로 작성하는 것이 좋다. 프로그램의 흐

름을 다음과 같이 정하고, 배열의 값을 출력하는 기능과 최대값을 구하는 기능을 함수로 구현하

기로 한다.

<리스트 6-4> 최대값 찾기 프로그램 구조

17. 배열을 초기화 한다.

18. 배열의 값을 출력한다. // void print_array(int array[]);

19. 최대값을 구한다. // int find_max(int array[]);

20. 최대값을 출력한다.

먼저 프로젝트(max)를 생성하고 소스 코드(max.c)를 다음과 같이 작성한다.

<리스트 6-5> 최대값 찾기 프로그램 구현

1. #include <stdio.h>

2. #define SIZE 10 // 배열의 크기를 선언한다.

3. void print_array(int array[]); // 배열 데이터를 출력하는 함수

4. int find_max(int array[]); // 배열에서 값이 가장 큰 원소의 인덱스를 리턴

5. void main(void)

6. {

7. int data[SIZE] = { 3, 2, 9, 7, 1, 4, 8, 0, 6, 5 };

8. int maxindex;

9. print_array(data); // 배열의 값을 출력한다.

Page 116: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

114

10. maxindex = find_max(data); // 최대 값이 저장되어 있는 배열의 인덱스를 구한다.

11. printf("최대값: data[%d] = %d\n", maxindex, data[maxindex]); // 출력

12. }

13. void print_array(int array[]) { } // 비어 있는 함수

14. int find_max(int array[]) { return 1 } // 임시로 1을 리턴

배열을 전달받은 함수는 배열의 크기를 알 수 없다. 각 함수에서 배열의 크기를 알 수 있도록

#define 문으로 배열의 크기를 정의하였다. 각 함수의 구현은 다음과 같다. 프로그램의 실행 결과

는 그림 6-2이다.

<리스트 6-6> 최대값 찾기 함수 구현

1. void print_array(int array[])

2. {

3. int n;

4. printf("배열의 값\n");

5. for (n=0; n<SIZE; n++)

6. {

7. printf("data[%d]=%d ", n, array[n]); // 배열의 원소 출력

8. if (((n+1)%5) == 0) // 5개 마다

9. printf("\n"); // 줄 바꿈 출력

10. }

11. }

라인 8: 한 줄에 배열의 원소 5 개를 출력하고, 라인을 변경한다. 배열의 원소를 지정하는 변

수 n의 값이 0부터 변하므로, (n+1)의 값이 5의 배수일 때마다 줄 바꿈 문자(‘\n’)를 출력한다.

<리스트 6-7> 최대값 찾기 함수

1. int find_max(int array[])

2. {

3. int n;

4. int maxindex = 0; // 0 번째 원소가 최대값이라고 가정한다.

5. for (n=1; n<SIZE; n++)

6. if (array[maxindex] < array[n]) // array[n]이 최대값보다 크면

7. maxindex = n; // 최대값의 인덱스를 교체한다.

8. return maxindex; // 최대값의 인덱스를 리턴한다.

9. }

Page 117: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

115

<그림 6-2> 최대값 찾기 프로그램 실행 결과

6.2 로또 게임

컴퓨터가 생성한 6개의 숫자와 사용자가 입력한 6개의 숫자를 비교하여 맞은 개수를 출력한다.

6.2.1 요구사항 분석

사용자에게 6개의 로또 번호를 입력하도록 하고, 컴퓨터가 생성한 6개의 로또 번호와 비교하

여 일치한 번호의 개수를 알려주는 프로그램을 작성하라. 단, 사용자는 번호를 중복으로 입력할

수 있지만, 컴퓨터는 1~45의 범위에서 서로 다른 숫자를 6개 생성해야 한다.

입출력 설계

1~45 사이의 번호를 6개 선택 하세요 : 6 13 26 37 40 43

입력 번호: 6 13 26 37 40 43

당첨 번호: 19 2 37 13 8 12

당첨 번호는 2개입니다.

6.2.2 자료구조 설계

int arr_lotto[6] // 컴퓨터가 생성한 로또 숫자 6개

int arr_user[6] // 사용자가 입력한 숫자 6개

int win; // 두 배열에서 일치한 숫자의 개수

6.2.3 문제 해결방법

중복되는 번호 없이 6개의 난수 발생 시키기

1. 반복 시작

2. 난수를 생성하여 배열에 저장한다.

3. 배열 인덱스가 1 이상인 경우에 대하여,

4. 배열 인덱스 0~현재까지 중복되는 숫자가 있으면 2번으로 돌아간다.

5. 중복되는 숫자가 하나도 없으면 배열 인덱스를 증가시킨다.

6. 배열 인덱스가 6이 되면 종료

Page 118: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

116

6.2.4 프로그램 구현

1. #include <stdio.h>

2. #include <stdlib.h> // random() 함수

3. #include <time.h> //srand() 함수 실행에 사용할 time() 함수

4. void user_check(int arr_user[]); //사용자가 선택한 숫자를 배열에 저장

5. void generate_lotto(int arr_lotto[]); // 컴퓨터가 로또 번호 생성

6. int mappingNumber(int arr_user[], int arr_lotto[]); // 일치하는 번호 매핑

7. int main(void)

8. {

9. int arr_lotto[6]; //컴퓨터가 뽑은 6개의 번호 저장

10. int arr_user[6]; // 사용자가 입력한 6개의 번호 저장

11. int i, win, cnt = 0; //1줄U에??번聚?호£를? 뽑?은º 갯튜?수?

12. srand((unsigned)time(NULL));

13. user_check(arr_user);

14. generate_lotto(arr_lotto);

15. printf("\n입력 번호: ");

16. for (i = 0; i < 6; i++) {

17. printf("%3d ", arr_user[i]);

18. }

19. printf(“\n당첨 번호: ");

20. for (i = 0; i < 6; i++) {

21. printf("%3d ", arr_lotto[i]);

22. }

23.

24. win = mappingNumber(arr_user, arr_lotto);

25. printf(“\n\n당첨 번호는 %d개입니다. \n\n", win);

26. return 0;

27. }

28. void generate_lotto(int arr_lotto[])

29. {

30. int i, dup, cnt=0;

31. int i;

32. do {

33. arr_lotto[cnt] = rand() % 45 + 1; //1~45 범위의 난수 생성

34. if (cnt > 0) { //두 번째 실행 부터

35. for (i = 0; i < cnt; i++) { // 앞서 생성한 번호와의 중복 여부 검사

36. if (arr_lotto[i] == arr_lotto[cnt]) {

37. dup++;

Page 119: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

117

38. }

39. }

40. }

41.

42. if (dup == 0) {

43. cnt++; //중복이 없을 경우에는 다음 번호를 뽑는다.

44. }

45. else {

46. dup = 0; //중복이 있으면 반복 (cnt는 바뀌지 않음)

47. }

48.

49. } while (cnt < 6); //6개 뽑을 때까지 반복

50. }

51. void user_check(int arr_user[])

52. {

53. int i;

54. int num;

55. printf("1~45 사이의 번호를 6개 선택하세요 : ");

56. for(i=0; i<6; i++)

57. scanf("%d", &arr_user[i]);

58. }

59. int mappingNumber(int arr_user[], int arr_lotto[])

60. {

61. int count;

62. int i, j;

63. for(i=0; i<6; i++)

64. for( j=0; j<6; j++)

65. if(arr_user[i] == arr_lotto[ j]) {

66. count++;

67. break;

68. }

69. return count;

70. }

71.

Page 120: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

118

실행 결과

6.3 빙고 게임

컴퓨터와 사용자가 5X5 숫자판으로 빙고 게임을 하는 프로그램을 작성하시오.

6.3.1 요구사항 분석

빙고판 초기 상태

1~25까지의 숫자가 5×5 배열의 임의의 위치에 등록되어 있다.

게임 운영

화면에 사용자 빙고판만 보인다. 컴퓨터 빙고판은 보이지 않는다.

사용자가 1~25번 중에서 그 동안 선택하지 않은 번호를 선택한다.

사용자와 컴퓨터는 해당 번호를 빙고판에서 지운다.

컴퓨터가 1~25번 중에서 그 동안 선택하지 않은 번호를 선택한다.

사용자와 컴퓨터는 해당 번호를 빙고판에서 지운다.

빙고를 완성하면, 승자를 표시하고 게임 종료.

빙고를 완성하지 못하면, 처음부터 반복

6.3.2 자료구조 설계

전역 변수

Page 121: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

119

int ubingo[5][5]; // 사용자 빙고판

int cbingo[5][5]; // 컴퓨터 빙고

빙고판 운영

처음에 1~25의 수를 임의의 위치에 배치한다.

게임이 진행되면서, 사용자 또는 컴퓨터가 선택한 번호에 해당하는 배열의 값을 -1

로 변경한다.

6.3.3 문제 해결 방법

리스트 1. 전체 알고리즘

빙고판 초기화 initialize();

반복

사용자의 빙고판을 출력한다. print_bingo(ubingo);

사용자가 번호를 선택한다. get_number(0);

사용자와 컴퓨터가 해당 번호를 빙고판에서 지운다. erase_bingo();

컴퓨터가 번호를 선택한다. get_number(1);

사용자와 컴퓨터가 해당 번호를 빙고판에서 지운다. erase_bingo();

사용자와 컴퓨터가 빙고를 완성했는지 검사한다. check_bingo();

완성하지 않았다면, 반복.

사용자의 빙고판을 출력한다. print_bingo(ubingo);

컴퓨터의 빙고판을 출력한다. print_bingo(cbingo)

승자를 표시한다. print_winner();

구조 구현

1. #include <stdio.h>

2.

3. void initialize();

4. void erase_bingo(int arr[][5], int number);

5. void print_bingo(int arr[][5]);

6. void print_winner(int winner);

7. int get_number(int from); // 0: user, 1: computer

8. int check_bingo();

9.

10. int ubingo[5][5];

11. int cbingo[5][5];

12.

13. void main()

14. {

15. int number, uwin, cwin;

Page 122: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

120

16.

17 initialize(); // 빙고판 초기화

18. do { //반복

19. printf("사용자\n"); print_bingo(ubingo); //사용자의 빙고판을 출력한다.

20. number = get_number(0); //사용자가 번호를 선택한다.

21. erase_bingo(ubingo, number); //사용자와 컴퓨터가 해당 번호를 빙고판에서 지운다.

22. erase_bingo(cbingo, number);

23. number = get_number(1); //컴퓨터가 번호를 선택한다.

24. erase_bingo(ubingo, number); //사용자와 컴퓨터가 해당 번호를 빙고판에서 지운다.

25. erase_bingo(cbingo, number);

26. uwin = check_bingo(ubingo); //사용자가 빙고를 완성했는지 검사한다.

27. cwin = check_bingo(cbingo); //사용자가 빙고를 완성했는지 검사한다.

28. } while ((uwin == 0) && (cwin ==0)); //완성하지 않았다면, 반복.

29. printf("사용자\n"); print_bingo(ubingo); //사용자와 컴퓨터의 빙고판을 출력한다.

30. printf("컴퓨터\n"); print_bingo(cbingo);

31. print_winner(cwin*2 + uwin); //승자를 표시한다.

32. }

33. void initialize() {}

34. void erase_bingo(int arr[][5], int number) {}

35. void print_bingo(int arr[][5]) {}

36. void print_winner(int winner) {}

37. int get_number(int from) { return 1; }

38. int check_bingo() { return 1; }

기능구현

4.2 빙고판 초기화: initialize()

4.3 빙고판 출력: print_bingo()

4.4 번호 선택: number = get_number()

4.5 빙고판에서 번호 지우기: erase_bingo(number)

4.6 빙고 완성 여부 검사: winner = check_bingo()

4.7 승자 표시: print_winner(winner)

4.2 방고판 초기화 : initialize()

void initialize(void)

입력 파라미터: 없음.

Page 123: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

121

기능

난수 생성기 초기화

사용자 빙고판과 컴퓨터 빙고판 초기화

리턴 : 없음.

빙고판 초기화

목표 : 빙고판에 1~25까지의 수를 임의의 위치에 저장한다.

알고리즘

배열에 1~25까지의 수를 차례대로 저장한후,

배열[i]와 배열[x]를 교환한다. (x는 배열의 임의의 위치)

구현

// 헤더 추가

1. #include <stdlib.h>

2. #include <time.h>

// 함수 선언 추가

3. void set_rand(int *array);

4. void swap(int *x, int *y);

// 함수 구현

5. void initialize()

6. {

7. srand((unsigned int)time(NULL));

8. set_rand((int *)ubingo);

9. set_rand((int *)cbingo);

10. }

11. void set_rand(int *array)

12. {

13. int i;

14.

15. for (i=0; i<SIZE2; i++)

16. array[i] = i+1;

17. for (i=0; i<SIZE2; i++)

18. swap(&array[i], &array[rand() % 25]);

Page 124: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

122

19. }

20. void swap(int *x, int *y)

21. {

22. int temp;

23. temp = *x;

24. *x = *y;

25. *y = temp;

26. }

동작 확인

main() 함수 안의 초기화 함수 호출 이후에 디버그 중단점을 걸고,

디버깅을 시작하여 배열에 난수가 올바로 저장되었는지 확인할 수 있다.

4.3 빙고판 출력: print_bingo()

void print_bingo(int arr[][5]);

설계입력 파라미터:

int arr[][5]: 출력할 2차원 배열. 사용자 빙고판 또는 컴퓨터 빙고판

기능

5×5 배열을 화면에 출력한다.

배열의 값이 -1이면, “XX”를 출력한다.

리턴: 없음.

구현

1. void print_bingo(int arr[][5])

2. {

3. int x, y;

4.

5. for (y=0; y<SIZE; y++) {

6. for (x=0; x<SIZE; x++) {

7. if (arr[y][x] != -1)

8. printf("%5d", arr[y][x]);

9. else

10. printf(" XX");

11. }

12. printf("\n");

13. }

14. }

Page 125: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

123

동작 확인

프로그램을 실행하면, 현재의 상태를 화면에서 확인할 수 있다.

4.2 번호 선택: number = get_number()

int get_number(int from)

입력 파라미터

from = 0/1: 사용자/컴퓨터

동작

from = 0/1: 사용자로부터 1~25의 숫자를 입력 받는다.

from = 1: 컴퓨터에서 1~25 사이의 난수를 생성한다.

리턴

1~25 사이의 숫자.

숫자 생성

한 번 빙고판을 검사한 숫자를 다시 생성할 수 없다.

따라서, 그 동안 생성된 숫자를 별도로 저장해 두어야 한다.

전역 변수 추가

int checked[25]; // 그 동안 선택된 숫자. 최대 25 개

int count = 0; // 선택된 숫자의 수. 초기값은 0.

알고리즘

do {

from == 0인 경우 // 사용자로부터 입력

number = 사용자에게 1~25 사이의 값을 입력 받는다.

1~25 이외의 숫자이면 다시 입력 받도록 조치한다.

from == 1인 경우

number = 컴퓨터에서 1~25 사이의 난수를 생성한다.

number가 checked[] 안에 있는지 검사

} 있다면 반복

checked[count++] = number 를 저장하고,

number를 리턴.

구현

1. // 전역변수 추가

2. int checked[25];

3. int count = 0;

4. int get_number(int from)

5. {

Page 126: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

124

6. int number;

7. int x, retry;

8.

9. do {

10. retry = 0;

11. if (from == 0) {

12. printf("1~25 사이의 숫자를 입력하세요: ");

13. scanf("%d", &number);

14. if (number < 1 || number > 25)

15. retry = 1;

16. }

17. else

18. number = rand()%25 + 1;

19.

20. if (retry == 0) {

21. for (x=0; x<count; x++) {

22. if (checked[x] == number) {

23. retry = 1;

24. break;

25. }

26. }

27. }

28. } while (retry == 1);

29. checked[count++] = number;

30. if (from == 0) printf(“사용자가 %d를 선택했습니다.\n”, number);

31. else printf(“컴퓨터가 %d를 선택했습니다.\n”, number);

32. return number;

33. }

동작 확인

get_number를 호출하는 곳에 중단점을 설정하고,

디버거로 함수가 올바로 동작하는지 확인한다.

4.3 빙고판에서 번호 지우기: erase_bingo()

void erase_bingo(int arr[][5], int number)

파라미터

Page 127: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

125

int arr[][5]: 검사할 빙고판

int number: 빙고판과 비교할 숫자

기능

arr[5][5] 안에 number가 있는지 확인하고,

있다면 해당 값을 0으로 설정한다.

리턴: 없음.

구현

1. void erase_bingo(int arr[][5], int number)

2. {

3. int x, y;

4.

5. for (y=0; y<SIZE; y++)

6. for (x=0; x<SIZE; x++)

7. if (arr[y][x] == number)

8. arr[y][x] = 0;

9. }

동작 확인

프로그램을 실행하면, 사용자가 입력한 빙고가 XX로 표시되는 것을 확인할 수 있다.

4.5 빙고 완성 여부 검사: win = check_bingo()

int check_bingo(int arr[][5])

파라미터:

int arr[][5]; // 검사하려는 빙고판

기능

빙고판에 대하여 완성된 빙고열이 있는지 검사한다.

완성된 빙고열: 가로, 세로, 또는 대각선이 모두 0인 경우.

리턴

0: 완성되지 않은 경우

1: 완성된 빙고열이 있는 경우

완성된 빙고열 검사 알고리즘

배열의 행, 열, 대각선에 대하여, 배열의 값이 0인 것의 수가 5개.

배열의 행, 열, 대각선에 대하여, 배열 원소의 합이 -0.

구현

1. int check_bingo(int arr[][5])

2.

Page 128: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

126

3. int x, y, sum;

4.

5. for (y=0; y<SIZE; y++) { // check x-axis

6. sum = 0;

7. for (x=0; x<SIZE; x++) {

8. sum += arr[y][x];

9. }

10. if (sum == 0) return 1;

11. }

12.

13. for (x=0; x<SIZE; x++) { // check y-axis

14. sum = 0;

15. for (y=0; y<SIZE; y++) {

16. sum += arr[y][x];

17. }

18. if (sum == 0) return 1;

19. }

20.

21. sum = 0; // check cross 1

22. for (x=0; x<SIZE; x++) {

23. sum += arr[x][x];

24. }

25. if (sum == 0) return 1;

26.

27. sum = 0; // check cross 2

28. for (x=0; x<SIZE; x++) {

29. sum += arr[x][SIZE-x-1];

30. }

31. if (sum == 0) return 1;

32.

33. return 0;

34. }

4.6 승자 표시: print_winner()

void print_winner(int winner)

파라미터:

winner = 1: 사용자 승

Page 129: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

127

winner = 2: 컴퓨터 승

winner = 3: 동시 승. 비김.

기능

누가 이겼는지 출력한다.

리턴: 없음.

구현

1. void print_winner(int winner)

2. {

3. switch(winner) {

4. case 1: printf("사용자가 이겼습니다.\n"); break;

5. case 2: printf("컴퓨터가 이겼습니다.\n"); break;

6. case 3: printf("비겼습니다.\n"); break;

7. default: printf("뭔가 이상합니다.\n"); break;

8. }

9. }

Page 130: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

128

제7장 정렬

정렬과 탐색은 컴퓨터 소프트웨어의 중요한 역할을 담당하는 기초 기술 중 하나이고, 자료구조

와 알고리즘 교과목의 주요 주제이다. 정렬과 탐색 문제를 해결하기 위하여 프로그램 개발 단계

중에서 문제 해결 방법과 알고리즘에 대한 이해가 중요하다. 이 장에서는 다음과 같은 문제를 프

로그램으로 구현한다.

4. 선택정렬: 배열의 원소 중에서 가장 큰 수를 찾는 방법으로 배열에 저장되어 있는 수를 오름

차순으로 정렬한다.

5. 버블정렬: 인접한 원소를 비교하는 과정을 통하여 배열에 저장되어 있는 수를 오름차순으로

정렬한다.

7.1 선택정렬(Selection Sort)

1부터 1000까지 값을 갖는 10 개의 난수(random number)를 생성하고, 오름차순으로 정렬하여

출력하라.

7.1.1 요구사항 분석132

난수를 생성하여 처리할 데이터로 사용하라는 조건이 있다. 난수를 생성하는 방법은 프로그램

구현 단계에서 고민하기로 하자. 정렬 알고리즘은 데이터의 값에 상관 없이 항상 일정하게 동작

하여야 하므로, 일단 입력 조건이 “5.1 최대값 찾기”와 같다고 생각하고 문제를 분석해 보자. 배열

의 값을 오름차순으로 정렬하여 출력하는 문제이다. 정렬 전과 정렬 후 배열의 값을 비교하기 쉽

도록 다음과 같이 출력해 보자.

원본: 3 2 9 7 1 4 8 0 6 5

정렬: 0 1 2 3 4 5 6 7 8 9

7.1.2 자료구조 설계

배열 데이터를 정렬할 때, 원본 배열의 값이 변경된다. 그러므로 처음 프로그램의 자료구조를

설계하는 단계에서 필요한 데이터는 정렬할 데이터를 저장할 배열 한 개이다.

int data[10]; // 원본 데이터 및 정렬 데이터를 저장하는 배열

7.1.3 프로그램 구조 설계

문제해결 방법과 알고리즘 설계를 뒤로 미루고 먼저 전체적인 프로그램의 구조를 설계해 보자.

난수를 사용하므로, 프로그램이 실행될 때마다 서로 다른 난수가 생성되는 것을 확인하기 위하여

선택정렬을 모두 세 번 실행해 보자. 전체적인 흐름은 다음과 같다.

<리스트 7-1> 선택정렬 프로그램 구조

1. 다음 과정을 세 번 실행한다.

Page 131: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

129

2. 배열의 값을 난수로 초기화 한다. // generate_random();

3. 생성된 난수를 출력한다. // print_array();

4. 배열을 오름차순으로 정렬한다. // selection_sort();

5. 정렬된 배열을 출력한다. // print_array();

선택정렬 프로그램을 구현하기 위하여 다음과 같은 함수들을 정의하여 사용하기로 한다. 배열

의 크기는 #define 문으로 선언하여 사용한다.

void generate_random(int array[])

파라미터

int array[]: 난수를 저장할 배열

기능

난수를 생성하여 배열에 저장한다.

void print_array(char *str, int array[])

파라미터

char *str: 배열을 출력하기 전에 출력할 문자열

int array[]: 출력할 배열

기능

문자열과 배열의 값을 화면에 출력한다.

void selection_sort(int array[])

파라미터

int array[]: 오름차순으로 정렬할 배열

기능

배열의 값을 오름차순으로 정렬한다.

일단 프로그램을 다음과 같이 구현하고 다음 단계로 진행한다.

<리스트 7-2> 선택정렬 main() 함수 구현

1. #include <stdio.h>

2. #define SIZE 10 // 배열의 크기

3. void generate_random(int array[]); // 함수 선언

4. void print_array(char *str, int array[]);

5. void selection_sort(int array[]);

6. void main(void)

7. {

Page 132: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

130

8. int data[SIZE], n; // 데이터를 저장할 배열

9. for (n=0; n<3; n++) // 모두 3 회 수행

10. {

11. generate_random(data); // 난수를 생성한다.

12. print_array("원본: ", data); // 원본 데이터를 출력한다.

13. selection_sort(data); // 오름차순으로 정렬한다.

14. print_array("정렬: ", data); // 정렬된 데이터를 출력한다.

15. printf("\n"); // 구분을 위하여 줄 바꿈을 출력한다.

16. }

17. }

18. void generate_random(int array[]) { } // 비어 있는 함수

19. void selection_sort(int array[]) { }

20. void print_array(char *str, int array[]) { }

프로그램을 컴파일하고 실행해도 아직까지 출력되는 것이 없다. 다음과 같은 순서로 선택정렬

을 구현한다.

7.1.4 난수 생성: 난수를 생성하는 방법을 알아보고, 난수 생성 함수를 구현한다.

7.1.5 데이터 출력 함수: 데이터를 출력하는 함수를 구현한다.

7.1.6 선택정렬 알고리즘: 선택정렬 알고리즘을 설명한다.

7.1.7 데이터 교환: 선택정렬 알고리즘을 구현하기 위하여 배열의 두 개의 원소를 교환하는 방법

을 설명하고 구현한다.

7.1.8 선택정렬 구현: 최소값 찾기 기능을 사용하여 선택정렬 알고리즘을 구현한다.

7.1.9 선택정렬 함수: 선택정렬 알고리즘을 하나의 함수로 구현한다.

7.1.4 난수 생성

C 컴파일러는 난수 생성과 관련하여 다음과 같은 라이브러리 라이브러리 함수를 제공한다.

#include <stdlib.h>

int rand(void)

파라미터: 없음.

리턴

0 ~ RAND_MAX 범위의 난수를 생성하여 리턴한다. RAND_MAX는 컴파일러 자체적으로

정의하고 있는 상수이고, 그 값은 32,767이다.

void srand(unsigned int seed)

파라미터

unsigned int seed: 난수를 생성기를 초기화하는 정수(seed)

Page 133: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

131

리턴: 없음.

rand() 함수를 호출할 때 초기화 정수(seed)의 값이 같으면, 난수를 항상 같은 순서로 생성하기

때문에 난수를 생성하기 전에 난수 생성기를 초기화해야 한다. 컴퓨터가 실행될 때마다 서로 다

른 초기화 정수 값을 전달하기 위하여 일반적으로 time() 함수를 사용한다.

#include <time.h>

time_t time(time_t *timer)

파라미터

time_t timer: time_t 형 데이터에 대한 포인터.

일단 time_t 자료형은 long 형을 재정의 한 것으로 이해하자.

리턴

time_t 형으로 환산한 현재 달력 시간

파라미터로 NULL로 전달하면, 1970년 1월 1일부터 현재까지 경과한 초를 리턴한다.

세 개의 함수가 새로 소개되어 난수를 생성하는 것이 복잡해 보이지만 실제로는 복잡하지 않다.

다음과 같은 두 단계로 충분하다. 프로그램이 시작될 때, 난수를 초기화 하는 rand() 함수를 한 번

만 호출해야 한다는 것에 주의하라.

srand(time(NULL)); // 난수 생성기를 초기화 한다.

r = rand(); // 0 ~ RAND_MAX 범위의 난수를 생성한다.

다음 프로그램은 10 개의 난수를 생성하여 출력한 예이다. 만일 라인 7의 난수생성기 초기화

함수를 호출하지 않는다면, 이 프로그램은 항상 동일한 순서로 난수를 생성한다. 직접 확인해 보

기 바란다.

<리스트 7-3> 난수 생성 프로그램

1. #include <stdio.h>

2. #include <stdlib.h> // rand(), srand() 정의

3. #include <time.h> // time() 함수 정의

4. void main(void)

5. {

6. int n, r;

7. srand(time(NULL)); // 난수 생성기 초기화

8. for (n=0; n<5; n++)

9. {

10. r = rand(); // 난수 생성

11. printf("%5d\n", r);

12. }

13. }

Page 134: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

132

문제의 요구사항에 의하여 1부터 1,000 사이의 난수를 생성해야 한다. 난수를 생성하는 rand()

함수는 0 ~ RAND_MAX(32,767) 범위의 난수를 생성하므로, 다음과 같이 1부터 1,000 사이의 난수

를 생성할 수 있다.

#define MAX 1000

r = (rand() % MAX) + 1; // 난수를 1000으로 나눈 나머지에 1을 더한다.

우리는 난수 생성기를 함수로 만들어야 한다. 따라서, 리스트 5-10의 라인 8~10을 함수로 만들

어 배열에 저장하면 된다. 프로그램을 다음과 같이 수정하고, 난수 생성 함수를 구현한다.

<리스트 7-4> 프로그램 구조 수정

1. #include <stdlib.h> // 난수 생성 함수 선언 헤더 파일 추가

2. #include <time.h> // time() 헤더 파일 추가

3. #define MAX 1000 // 생성하는 난수의 최대 값 정의 추가

4. void main(void)

5. {

6. int data[SIZE], n; // 데이터를 저장할 배열

7. srand(time(NULL)); // 난수 생성기 초기화 추가

8. for (n=0; n<3; n++) // 모두 3 회 수행

9. ….

라인 7: 프로그램이 시작할 때 난수 생성기를 초기화하여야 하므로, 첫 번째 실행문으로 난수

생성기를 초기화하는 문장을 추가한다.

<리스트 7-5> generate_random() 함수 구현

1. void generate_random(int array[])

2. {

3. int n;

4. for (n=0; n<SIZE; n++) // 배열의 크기만큼

5. array[n] = (rand() % MAX) + 1; // 난수를 생성하여 저장한다.

6. }

라인 5: rand() 함수는 0 ~ RAND_MAX 범위의 난수를 생성한다. 문제의 조건에 의하여 1 ~

1000 사이의 난수를 생성해야 한다. 생성된 난수를 MAX로 나눈 나머지는 0 ~ 999이므로, 1

을 더하여 배열에 저장한다.

프로그램을 컴파일하여 오류가 없음을 확인한다. 난수가 올바로 생성되었는지 확인하기 위하여

출력문을 추가할 수도 있다. 대신에 main() 함수의 generate_randon() 함수를 호출한 다음 라인에

중단점을 설정하고, 디버그 모드에서 실행한 후 배열의 값을 확인하면 그림 7-1과 같이 배열에

Page 135: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

133

난수가 생성된 것을 확인할 수 있다. 프로그램은 모두 3번 실행하므로, 키보드에서 F5(중단점까지

실행)를 눌러서 반복할 때마다 다른 값이 생성되는 것도 확인할 수 있다.

<그림 7-1> 디버거에 의한 난수 생성 확인

7.1.5 데이터 출력 함수

난수 생성기가 올바로 동작하는 것을 확인한 후, print_array() 함수를 구현한다. 이 함수는 첫번

째 파라미터로 전달 받은 문자열과 두 번째 파라미터로 전달 받은 배열의 값을 출력한다. 이 함

수의 구현은 다음과 같다.

<리스트 7-6> print_array() 함수

1. void print_array(char *str, int array[])

2. {

3. int n;

4. printf("%s", str); // 배열의 제목을 출력한다.

5. for (n=0; n<SIZE; n++)

6. printf("%5d", array[n]); // 배열의 원소를 5 칸에 출력한다.

Page 136: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

134

7. printf("\n"); // 다음 반복과 구별하기 위하여 한 줄을 추가한다.

8. }

라인 1: 이 함수를 호출할 때, 문자열 상수(“원본”과 “정렬”)를 전달한다. 이렇게 문자열 상수

를 전달하면, 실제로 문자열의 시작 주소가 전달된다. 따라서, 파라미터를 char * 형으로 받는

다.

라인 4: 파라미터로 받은 문자열을 출력한다.

라인 5, 6: 반복문으로 10 개의 배열의 원소를 모두 출력한다.

출력하는 함수까지 작성하였으므로, 프로그램을 실행해 보자. 아직 정렬하는 함수를 구현하지

않았으므로, 원본 데이터와 정렬 데이터가 동일하게 출력된다. 그리고, 난수 생성기가 올바로 동

작하고 있는 것을 화면으로 확인할 수 있다.

<그림 7-2> 프로그램 실행 결과

7.1.6 선택정렬 알고리즘

이 문제의 핵심은 오름차순으로 정렬하는 알고리즘이다. 여기에서는 정렬 방법 중에서 가장 개

념이 간단한 선택정렬 알고리즘을 사용하기로 한다. 이 알고리즘은 그림 5-5와 같이 동작한다.

그림 7-3(a) 초기 상태: 배열의 초기값이다.

그림 7-3(b) 1 단계: 배열 전체, 즉 data[0]부터 data[9]까지의 범위에 대하여 최소값의 위치를

찾는다. data[7] = 0이 최소값이다. data[0]과 data[7]을 교환한다. 그 결과는 그림 5-5(c)이다.

배열 전체에 대하여 최소값이 data[0] 에 확정되었다.

그림 7-3(c) 2 단계: 배열 data[1]부터 data[9]까지의 범위에서 최소값의 위치를 찾는다. data[4]

= 1이 최소값이다. data[1]과 data[4]를 교환한다. 그 결과는 그림 5-5(d)이다. 배열 전체에 대

하여 두 번째 최소값이 data[1]에 확정되었다.

이와 같은 방법으로 배열의 탐색 위치 k를 증가시켜 탐색 범위를 줄여가면서 최소값을 찾고,

최소값을 배열의 k번째 원소와 교환한다.

이 알고리즘의 마지막 단계는 data[8]과 data[9] 중에서 최소값을 data[8]로 옮기는 것이다.

Page 137: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

135

<그림 7-3> 선택정렬 알고리즘

이와 같이 배열의 원소 중에서 최소값을 찾아 배열의 앞으로 옮기는 방법이 선택정렬 알고리즘

이다. 선택정렬 알고리즘을 일반화하여 알고리즘으로 표현해 보자. 크기가 SIZE인 배열 data[]에

대한 선택 정렬 알고리즘을 가상 언어로 표현하면 다음과 같다.

<리스트 7-7> 선택정렬 알고리즘

1. n=0부터 n<SIZE-1까지 // 배열의 마지막 한 개는 정렬할 필요가 없다.

2. data[n]부터 data[SIZE-1]까지의 배열 원소에서 최소값의 인덱스(minindex)를 구한다.

3. data[n]과 data[minindex]를 교환한다.

라인 2는 6.1 최대값 찾기”에서 학습하였던 find_max() 함수를 응용하여 만들 수 있다. find_max()

함수는 배열 전체에 대하여 최대값의 인덱스를 찾으므로, 최소값을 찾도록 이 함수를 다음과 같

이 수정한다.

int find_min(int array[], int start)

파라미터

int array[]: 최소값을 탐색할 배열

int start: 최소값 탐색을 시작할 배열의 인덱스

리턴

array[start]부터 array[SIZE-1] 중에서 최소값의 인덱스를 리턴한다.

<리스트 7-8> find_min() 알고리즘

1. minindex = start; // array[start]를 최소값이라고 가정한다.

2. m=start+1부터 m<SIZE까지

3. 만일 array[minindex] > array[m]이면

4. minindex = m; // array[m]이 최소값이다.

5. return minindex; // 최소값의 인덱스를 리턴한다.

Page 138: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

136

리스트 7-7의 선택 정렬 알고리즘을 find_min() 함수를 사용하는 것으로 표현하면 다음과 같다.

<리스트 7-9> find_min() 함수를 사용한 선택정렬 알고리즘

1. n=0 부터 n<SIZE-1까지

2. minindex = find_min(data, n);

3. data[n]과 data[minindex]를 교환한다.

7.1.7 데이터 교환

정렬 알고리즘은 데이터를 교환하는 기능을 포함하고 있다. 변수 a = 3, b = 5라고 가정하고 두

변수의 값을 교환하는 방법을 알아보자. 다음과 같은 두 개의 문장으로 변수의 데이터를 교환할

수 없다.

a = b; // a = 5로 갱신되므로,

b = a; // b = 5이다. 결과적으로, a와 b가 모두 5이다.

<그림 7-4> 데이터 교환 과정

데이터를 교환하려면 임시 변수가 하나 필요하다. 그림 7-4은 데이터를 교환하는 과정이고, 다

음과 같은 순서에 따라 세 개의 문장을 실행해야 한다.

temp = a; // a의 데이터를 temp로 피신시킨다.

a = b; // a에 b의 값을 저장한다.

b = temp; // b에 temp의 값을 저장한다.

데이터를 교환하는 기능을 함수로 만들려면, 교환 함수에서 데이터를 교환한 결과 값을 간접적

으로 받아와야 하기 때문에 함수로 변수의 포인터를 전달해야 한다. 다음은 함수를 호출하여 변

수의 값을 교환하는 예이다.

<리스트 7-10> 데이터 교환 함수

1. main()

2. {

3. int a = 3, b = 5;

4. printf(“a = %d, b=%d”, a, b); // 출력: a = 3, b=5

5. swap(&a, &b); // a와 b의 주소를 전달한다.

6. printf(“a = %d, b=%d”, a, b); // 출력: a = 5, b = 3

Page 139: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

137

7. …

8. }

9. void swap(int *xp, int *yp) // 파라미터를 통하여 값을 리턴해야 하므로

10. { // 포인터를 받는다.

11. int temp;

12. temp = *xp; // 데이터를 교환한다.

13. *xp = *yp;

14. *yp = temp;

15. }

라인 4: 출력문에 의하여 a, b의 원래 값을 출력한다.

라인 5: 데이터 교환 함수를 호출하면서 변수의 주소를 전달한다.

라인 6: 변수의 값이 교환된 것을 확인한다.

7.1.8 선택정렬 구현

선택정렬 알고리즘과 필요한 함수를 모두 알았으므로, 이제 선택정렬 알고리즘을 구현한다. 새

로 추가되는 함수는 다음과 같다.

int find_min(int array[], int start)

파라미터

int array[]: 최소값을 구할 배열

int start: 최소값을 구할 배열의 시작 인덱스

리턴

array[start]부터 array[SIZE-1]까지 배열 원소 중에서 최소값을 갖는 배열의 인덱스를 리턴

한다.

void swap(int *xp, int *yp)

파라미터

int *xp, *yp: 데이터를 교환할 변수의 포인터

기능

*xp와 *yp의 값을 교환한다.

지금까지 작성하였던 프로그램을 다음과 같이 수정한다.

<리스트 7-11> 선택정렬 main() 함수 수정

1. int find_min(int array[], int start); // 함수 선언 추가

2. void swap(int *xp, int *yp);

3. void selection_sort(int array[]) // 리스트 7-9 구현

Page 140: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

138

4. {

5. int n, minindex;

6. for (n=0; n<SIZE-1; n++)

7. {

8. minindex = find_min(array, n); // array[n] ~ array[SIZE-1]에서 최소값을 찾아

9. swap(&array[minindex], &array[n]); // 교환한다.

10. }

11. }

12. int find_min(int array[], int start) // 리스트 7-8 구현

13. {

14. int m;

15. int minindex = start; // start를 최소값 인덱스로 가정한다.

16. for (m=start+1; m<SIZE; m++) // arrau[start+1]부터 마지막 원소까지

17. if (array[minindex] > array[m]) // 최소값의 위치를 찾아

18. minindex = m; // minindex에 저장한다.

19. return minindex; // 최소값의 인덱스를 리턴한다.

20. }

21. void swap(int *xp, int *yp) { … } // 리스트 7-10 라인 9~15와 동일

그림 7-5은 선택정렬 프로그램의 실행 결과이다. 실제로 출력되는 데이터는 난수 생성 결과에

따라 다를 수 있다. 다시 한번 강조하지만 프로그램을 구현하는 과정에서 함수 하나를 추가할 때

마다 디버거의 추적 기능을 활용하거나 출력 문장을 추가하여 각 함수가 올바로 동작하는지 확인

하는 습관을 길러야 한다.

<그림 7-5> 선택정렬 프로그램의 실행 결과

7.1.9 선택정렬 함수

find_min() 함수를 사용하지 않고 selection_sort() 함수를 구현해 보자. 다음은 selection_sort()

함수 find_min() 함수를 서로 비교하기 좋게 적은 것이다. selection_sort() 함수의 밑줄 부분에서

find_min() 함수를 호출하므로, find_min() 함수가 selection_sort()의 밑줄 부분으로 들어오도록 작

성하면 두 함수를 합칠 수 있다. 그 결과는 리스트 7-12이고, 라인 6~9가 find_min() 함수의 기능

에 해당한다. 결과적으로 선택정렬 알고리즘은 이중 루프로 구현된다.

Page 141: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

139

void selection_sort(int array[])

{

int n, minindex;

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

{

minindex = find_min(array, n);

swap(&array[minindex], &array[n]);

}

}

int find_min(int array[], int start)

{

int m;

int minindex = start;

for (m=start+1; m<SIZE; m++)

if (array[minindex] > array[m])

minindex = m;

return minindex;

}

<리스트 7-12> 하나의 함수로 구현한 선택정렬 알고리즘

1. void selection_sort(int array[])

2. {

3. int n, m, minindex; // 변수 m 추가

4. for (n=0; n<SIZE-1; n++)

5. {

6. minindex = n; // 배열의 시작 위치를 최소값으로 가정

7. for (m=n+1; m<SIZE; m++) // data[n]~data[SIZE-1]에서

8. if (array[minindex] > array[m]) // 최소값의 인덱스를 구한다.

9. minindex = m;

10. swap(&array[minindex], &array[n]);

11. }

12. }

7.2 버블정렬(Bubble Sort)

1부터 1000까지 값을 갖는 n 개의 난수(random number)를 생성하고, 버블정렬 알고리즘으로

데이터를 오름차순으로 정렬하고, 그 결과를 파일에 저장하라.

7.2.1 요구사항 분석

문제를 해결하는 방법은 뒤로 미루고, 요구사항을 명확하게 정의해 보자. 선택정렬 문제와 비교

할 때, 요구사항의 다른 점이 세 가지이다.

사용자로부터 배열의 크기를 입력 받는다.

선택정렬 알고리즘 대신에 버블정렬(bubble sort) 알고리즘을 사용한다.

배열을 정렬한 결과를 파일로 저장하여야 한다. 정렬 결과를 화면으로도 출력하기로 한다.

프로그램의 입출력 설계는 다음과 같다. 사용자로부터 배열의 크기를 입력 받고, 난수로 생성한

Page 142: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

140

원본 데이터를 한 줄에 10 개씩 출력한다. 데이터를 정렬한 후, 정렬된 데이터도 한 줄에 10 개

씩 출력한다. 파일에는 정렬된 데이터만 출력한다. 첫 줄에 배열의 크기를 출력하고, 다음 줄부터

한 줄에 10 개씩 데이터를 기록하기로 한다.

입력 배열의 크기를 입력하세요: 12

화면 출력 원본: 12

57 588 161 586 375 178 943 482 54 910

696 236

정렬: 12

54 57 161 178 236 375 482 586 588 696

910 943

파일 출력 정렬: 12

54 57 161 178 236 375 482 586 588 696

910 943

7.2.2 자료구조 설계

이 문제는 정렬에 필요한 자료구조 이외에 파일에 데이터를 저장하기 위한 자료구조도 필요하

다. 그렇지만 파일 저장에 필요한 자료구조는 뒤로 미루고 먼저 데이터를 정렬하기 위한 자료구

조만 생각해 보자. 배열의 크기가 정해져 있지 않으므로, 배열을 동적으로 할당하여 사용하여야

한다. 따라서, 다음과 같은 두 개의 자료구조가 필요하다.

int size; // 사용자가 입력한 배열의 크기

int *data; // 배열을 동적으로 할당하여 연결하기 위한 포인터

7.2.3 프로그램 구조 설계

요구사항에 따라 프로그램의 전체적인 흐름을 다음과 같이 정할 수 있다.

<리스트 7-13> 버블정렬 프로그램의 main() 함수

1. 난수 생성기를 초기화 한다.

2. 배열의 크기를 입력한다.

3. 배열을 동적으로 할당한다.

4. 난수를 생성하여 배열에 저장한다.

5. 원본 배열의 데이터를 출력한다.

6. 버블정렬 알고리즘으로 배열을 오름차순으로 정렬한다.

7. 정렬된 데이터를 출력한다.

8. 정렬된 배열을 파일에 기록한다.

9. 할당 받은 배열을 해제한다.

리스트 7-13에서 버블정렬을 수행하는 부분(라인 6)과 파일에 저장하는 부분(라인 8)을 제외한

나머지는 이미 구현해 본 경험이 있다. 난수 생성과 배열 데이터를 출력하는 함수를 다음과 같이

Page 143: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

141

정의한다.

void generate_random(int array[], int size);

void print_array(char *str, int array[], int size);

선택정렬 문제에서는 배열의 크기가 10으로 고정되어 있기 때문에, 이 함수들을 구현할 때 배열

의 크기를 전달할 필요가 없었다. 그러나 버블정렬 문제에서 배열의 크기가 변하므로 배열의 크

기를 별도의 파라미터로 전달하는 것만 다르다.

먼저 구현 가능한 부분을 다음과 같이 구현한다.

<리스트 7-14> 버블정렬 프로그램의 흐름 구현

1. #include <stdio.h>

2. #include <stdlib.h> // 난수 생성기 함수

3. #include <time.h> // time() 함수

4. void generate_random(int array[], int size); // 난수 생성 함수 선언

5. void print_array(char *str, int array[], int size); // 배열 출력 힘수 선언

6. void main(void)

7. {

8. int *data, size;

9. srand(time(NULL)); // 난수 초기화

10. printf("배열의 크기를 입력하세요: "); // 배열의 크기 입력

11. scanf("%d", &size);

12. data = (int *)malloc(sizeof(int)*size); // 배열 동적 할당

13. generate_random(data, size); // 난수 생성

14. print_array("원본: ", data, size); // 원본 데이터 출력

15. // 버블정렬

16. print_array("정렬: ", data, size); // 정렬 데이터 출력

17. // 파일에 저장

18. free(data); // 기억장치 해제

19. }

20. void generate_random(int array[], int size)

21. {

22. int n;

23. for (n=0; n<size; n++)

Page 144: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

142

24. array[n] = (rand() % 1000) + 1;

25. }

26. void print_array(char *str, int array[], int size)

27. {

28. int n;

29. printf("%s %d\n", str, size);

30. for (n=0; n<size; n++)

31. {

32. printf("%5d", array[n]);

33. if ((n+1)%10 == 0)

34. printf("\n");

35. }

36. printf(“\n”);

37. }

라인 15와 라인 17은 각각 버블정렬 함수와 파일 저장 함수를 호출할 부분이다. 데이터의 크기

를 55로 입력한 경우, 프로그램의 실행 결과는 그림 7-6과 같이 아직까지 원본 데이터와 정렬 데

이터를 동일하게 출력한다. 이후의 개발 과정을 다음과 같은 순서로 설명한다.

7.2.4 버블정렬 알고리즘: 버블정렬 알고리즘을 설명한다.

7.2.5 버블정렬 구현: 버블정렬 알고리즘을 구현한다.

<그림 7-6> 프로그램 흐름 제어 실행 결과

7.2.4 버블정렬 알고리즘

선택정렬 알고리즘은 배열의 가장 작은 원소를 찾아 앞으로 옮기는 반면에, 버블정렬 알고리즘

은 인접한 두 수를 비교하여 큰 수를 뒤로 보내는 과정을 반복함으로써 배열을 정렬한다. 배열의

크기를 size라고 할 때, 버블정렬은 다음과 같은 과정을 수행한다.

Page 145: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

143

n=0일 때, 배열 전체(array[0] ~ array[size-1])에서 가장 큰 값을 array[size-1]로 보낸다.

n=1일 때, array[0] ~ array[size -2] 중에서 가장 큰 값을 array[size -2]로 보낸다.

n=2일 때, array[0] ~ array[size -3] 중에서 가장 큰 값을 array[size -3]로 보낸다.

이와 같은 과정을 반복하여,

n=(size-2)일 때, array[0]과 array[1] 중에서 큰 값을 array[1]로 보낸다.

버블정렬 알고리즘은 배열의 크기를 하나씩 줄여가면서 배열의 뒤쪽부터 값을 확정한다. 위 과

정을 일반화하여 알고리즘으로 표현하면 다음과 같다.

<리스트 7-15> 버블 정렬 알고리즘

1. void bubble_sort(int array[], int size)

2. {

3. n=0부터 n<size-1일 때까지

4. // array[0] ~ array[size–n–1] 중에서 가장 큰 값을 array[size-n-1]으로 보낸다.

5. bubble(array, size-n-1);

6. }

bubble() 함수는 array[0]~array[size–n–1] 중에서 가장 큰 값을 array[size-n-1]으로 보내는 기능

을 수행한다. 이 함수를 다음과 같이 정의할 수 있다.

void bubble(int array[], int last)

파라미터

int array[]: 오름차순으로 정렬할 배열

int last: 정렬할 배열의 범위

기능

array[0]~array[last] 범위에서 가장 큰 값을 array[last]로 보낸다.

이제 버블정렬은 배열 array[]의 array[0] ~ array[last] 중에서 가장 큰 값을 array[last]로 보내는

기능을 수행하는 bubble() 함수를 구현하는 것으로 줄어든다. 그림 5-9는 last=9일 때, bubble()

함수 처리 과정을 보여준다. last>=10인 배열의 원소는 이미 정렬되어 있다고 가정한다.

m=0일 때, array[0]과 array[1]을 비교하여, array[0]이 array[1]보다 크면 두 수를 교환한다.

array[0]=3이고 array[1]=2이므로 두 수를 교환한다. 결과적으로 더 큰 수가 뒤로 이동하였다.

m=1일 때, array[1]과 array[2]을 비교하여, array[1]이 array[2]보다 크면 두 수를 교환한다.

array[1]=3이고 array[2]=9이므로 교환하지 않는다.

m=2일 때, array[2]과 array[3]을 비교하여, array[2]가 array[3]보다 크면 두 수를 교환한다.

array[2]=9이고 array[3]=7이므로 두 수를 교환한다.

이와 같은 과정을 m=8(즉, m<last)이 될 때까지 반복한다.

m=8일 때가 마지막 단계이며, 반복 과정을 통하여 가장 큰 수인 9가 배열의 마지막으로 이

동하였다. 9보다 작은 수는 나머지는 한 칸씩 앞으로 옮겨진다.

Page 146: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

144

<그림 7-7> 버블정렬 알고리즘

이 과정을 일반화하여 알고리즘으로 표현하면 다음과 같다.

<리스트 7-16> bubble() 함수

1. void bubble(int array[], int last)

2. {

3. m=0부터 m<last까지

4. 만일 array[m] > array[m+1]

5. array[m]과 array[m+1]을 교환한다.

6. }

7.2.5 버블정렬 구현

버블정렬 알고리즘을 이해하였다면, 프로그램의 구조에 다음과 같이 버블정렬 알고리즘을 구현

하여 추가해보자. 프로그램을 실행하면, 그림 5-10과 같이 원본 배열을 정렬한 데이터를 출력한다.

<리스트 7-17> 버블정럴 기능 추가

1. void bubble_sort(int array[], int size); // 함수 선언 추가

2. void bubble(int array[], int last);

3. void swap(int *xp, int *yp);

4. void main()

5. {

6. …

7. bubble_sort(data, size); // 리스트 7-14 라인 15에 함수 호출 추가

8. …

9. }

10. void bubble_sort(int array[], int size) // 리스트 7-15 구현

11. {

Page 147: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

145

12. int n;

13. for (n=0; n<size-1; n++) // array[0] ~ array[size–n–1] 중에서

14. bubble(array, size-n-1); // 가장 큰 값을 array[size-n-1]으로 보낸다.

15. }

16. void bubble(int array[], int last) // 리스트 7-16 구현

17. {

18. int m;

19. for (m=0; m<last; m++)

20. if (array[m] > array[m+1]) // 배열의 뒤 원소가 더 크면

21. swap(&array[m], &array[m+1]); // 교환한다.

22. }

23. void swap(int *xp, int *yp) { … } // 선택정렬과 동일

<그림 5-10> 버블정렬 실행 결과

선택정렬 알고리즘의 경우와 마찬가지 방법으로, 리스트 7-17의 라인 14에서 bubble() 함수를

호출하는 것 대신에 라인 19~21을 그 자리에 끼워 넣음으로써 버블정렬 알고리즘을 하나의 함수

로 구현할 수 있다. 그 결과는 다음과 같다.

<리스트 7-18> 하나의 함수로 구현한 버블정렬 알고리즘

1. void bubble_sort(int array[], int size)

2. {

3. int n, m; // 변수 m 추가

4. for (n=0; n<size-1; n++)

5. for (m=0; m<size-n-1; m++) // bubble() 함수 삽입

6. if (array[m] > array[m+1])

7. swap(&array[m], &array[m+1]);

8. }

Page 148: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

146

제8장 파일입출력 및 탐색

6. 파일입출력

7. 선형탐색: 정렬되어 있는 배열의 원소를 첫 번째 원소부터 하나씩 차례대로 검사하면서 사용

자가 입력한 데이터가 배열에 존재하는지 검사한다.

8. 이진탐색: 정렬되어 있는 배열의 원소를 반씩 나누어 검사하는 방법으로 사용자가 입력한 데

이터가 배열에 존재하는지 검사한다.

8.1 파일 입출력

이제 파일에 데이터를 기록하는 방법을 알아보자. 우리는 그 동안 키보드(표준입력장치)에서 입

력하고 화면(표준출력장치)으로 출력하는 것만 다루었다. 파일에 대한 입출력도 표준입출력장치에

대한 입출력과 비슷하다. 다만, 표준입출력장치는 항상 열려(open) 있어 표준입출력장치를 열지

않고 아무 때나 사용할 수 있지만, 파일은 사용하기 전에 열고(open) 사용이 끝난 다음에 닫아야

한다(close).

파일 처리 함수들은 모두 <stdio.h>에 정의되어 있다. 파일의 종류는 텍스트 파일과 이진 파일

이 있다.

텍스트 파일: 데이터를 문자열로 변환하여 파일에 저장한다. 텍스트 파일은 메모장으로 열어

볼 수 있다.

이진 파일: 컴퓨터가 데이터를 저장하는 형식 그대로 파일에 저장한다. 이진 파일의 예는 음

악 파일, 영상 파일 등이 있고, 메모장으로 열어 보아도 사람이 알아 볼 수 없다.

여기에서는 텍스트 파일을 사용하는 방법만 설명한다. 파일을 열고 닫는 함수는 다음과 같이

선언되어 있다.

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

파라미터

const char *filename: 파일의 이름(또는 경로)를 나타내는 문자열

const char *mode: 파일을 사용하는 방법을 나타내는 문자열.

“w”: 파일을 쓰기 모드로 연다.

“r”: 파일을 읽기 모드로 연다.

리턴

파일을 성공적으로 열면 FILE 객체에 대한 포인터를 리턴하고,

파일을 여는데 실패하면, NULL을 리턴한다.

int fclose(FILE *stream)

파라미터

FILE *stream: 닫고자 하는 FILE 객체에 대한 포인터

Page 149: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

147

리턴

성공적으로 닫으면, 0을 리턴하고,

닫기에 실패하면, EOF를 리턴한다.

파일 모드는 사용자가 파일을 여는 목적을 나타내며, 여기에서는 일반적으로 가장 많이 사용하

는 쓰기 모드와 읽기 모드만 소개하였다. 기타 자세한 사항은 라이브러리 설명서를 참조하기 바

란다.

파일 포인터는 운영체제 파일을 관리하기 위하여 내부적으로 운영하는 FILE 객체에 대한 포인

터이다. 컴퓨터는 다음과 같은 세 개의 파일 포인터를 항상 열어두고 있다. 따라서, 이것들은 사

용자가 열지 않고 사용할 수 있으며, 사용한 후에도 닫을 필요가 없다.

stdin: 표준입력장치. 키보드로 설정되어 있다.

stdout: 표준출력장치. 컴퓨터의 모니터 화면으로 설정되어 있다.

stderr: 표준오류장치. 오류 메시지를 출력하는 장치이며, 모니터 화면으로 설정되어 있다.

C 언어는 파일에 데이터를 읽고 쓰기 위하여 다음과 같은 라이브러리 함수들을 제공한다. 이

함수들은 표준입출력장치에 대한 함수들과 사용 방법이 거의 같다.

파일 읽기 함수

int fgetc(FILE *fp): 문자단위로 데이터를 읽는다.

char *fgets(char *buf, int n, FILE *fp): 문자열 단위로 데이터를 읽는다.

int fscanf(FILE *fp, …): 형식에 맞추어 데이터를 읽는다.

size_t fread(char *buffer, int size, int count, FILE *fp): 이진 데이터를 읽는다.

파일 쓰기 함수

int fputc(FILE *fp): 문자단위로 데이터를 기록한다.

char *fputs(char *buf, int n, FILE *fp): 문자열 단위로 데이터를 기록한다.

int fprintf(FILE *fp, …): 형식에 맞추어 데이터를 기록한다.

size_t fwrite(char *buffer, int size, int count, FILE *fp): 이진 데이터를 기록한다.

선택정렬 문제는 파일에 데이터를 기록하는 것이므로, fprintf() 함수만 소개하기로 한다. 이 함

수는 다음과 같이 선언되어 있다.

int fprintf(FILE *fp const char *format, …)

파라미터

FILE *fp: 데이터를 출력할 파일 포인터. 파일을 열 때, 리턴한 값을 적는다.

char *format(): 출력할 형식 문자열

추가 파라미터(…): format 안에 포함되어 있는 변환 기호 대신에 출력할 값을 저장하고

있는 변수

리턴

Page 150: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

148

성공적으로 출력한 문자의 수

따라서, fprintf() 함수는 첫 번째 파라미터로 파일 포인터를 받는다는 것 이외에 printf() 함수와 사

용법이 같다. 다음은 파일에 데이터를 기록하는 예이다. 프로그램을 실행하고, 프로젝트가 생성된

디렉토리에서 myfile.txt를 찾아 메모장으로 열어보면, 그림 8-1과 같이 파일의 내용을 확인할 수

있다.

<리스트 8-1> 텍스트 파일 저장 프로그램의 예

1. #include <stdio.h>

2. void main()

3. {

4. FILE *fp; // 파일 포인터를 선언한다.

5. fp = fopen(“myfile.txt”, “w”); // 텍스트 파일을 쓰기 모드로 연다.

6. if (fp != NULL) // 파일 열기에 성공하였다면,

7. {

8. fprintf(fp, “My first file.\n”); // 파일을 사용하고

9. fclose(fp); // 파일을 닫는다.

10. }

11. }

<그림 8-1> myfile.txt

8.1.1 파일 저장

이제 마지막으로 정렬된 배열을 파일에 저장하는 부분을 작성해 보자.. 화면에 데이터를 출력하

는 것과 파일에 데이터를 출력하는 것이 같다는 것을 확인하기 위하여, 하나의 함수로 두 가지

기능을 모두 수행하도록 만들어 보자. 우선 print_array() 함수가 화면 출력과 파일 출력을 모두 수

행하도록 소스 코드를 다음과 같이 수정한다.

<리스트 8-2> 소스 코드 수정 – 7장 버블정렬 소스코드

1. void print_array(FILE *fp, char *str, int array[], int size); // 파라미터 FILE *를 추가한다.

2. void save_data(int array[], int size); // 파일 저장함수 선언을 추가한다.

3. void main(void)

4. {

5. …

Page 151: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

149

6. print_array(stdout, "원본: ", data, size); // 표준출력장치를 전달한다.

7. …

8. print_array(stdout, "정렬: ", data, size); // 표준출력장치를 전달한다.

9. save_data(data, size); // 파일 저장 함수를 호출한다.

10. …

11. }

12. void print_array(FILE *fp, char *str, int array[], int size) // 파라미터 FILE * 추가

13. {

14. int n;

15. fprintf(fp, "%s %d\n", str, size); // printf(…)를 fprintf(fp, …)로 수정한다.

16. for (n=0; n<size; n++)

17. {

18. fprintf(fp, "%5d", array[n]); // printf(…)를 fprintf(fp, …)로 수정한다.

19. if ((n+1)%10 == 0)

20. fprintf(fp, "\n"); // printf(…)를 fprintf(fp, …)로 수정한다.

21. }

22. fprintf(fp, "\n"); // printf(…)를 fprintf(fp, …)로 수정한다.

23. }

24. void save_data(int array[], int size) { } // 비어 있는 함수. 추후 구현

라인 1: print_array() 함수의 첫 번째 파라미터로 FILE *fp를 추가한다.

라인 6, 8: 첫 번째 파라미터로 표준출력장치를 전달한다. 따라서, print_array() 함수는 화면으

로 출력한다.

이와 같이 수정하고 프로그램을 실행하면, 여전히 화면으로 배열의 값이 출력되는 것을 볼 수

있다. 마지막으로 save_data() 함수를 구현한다. 구조는 리스트8-1과 같고, 다만 한 줄을 기록하는

것 대신에 print_array() 함수를 호출하여 배열의 값을 파일에 기록한다.

<리스트 8-3> 파일 저장 함수 구현

1. void save_data(int array[], int size)

2. {

3. FILE *fp; // 파일 포인터를 선언한다.

4. fp = fopen("sorted.txt", "w"); // 텍스트 파일을 쓰기 모드로 연다.

5. if (fp != NULL) // 파일 열기에 성공하였다면,

6. {

7. print_array(fp, "정렬: ", array, size); // 파일을 사용하고

8. fclose(fp); // 파일을 닫는다.

9. }

10. }

Page 152: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

150

<그림 8-2> 버블정렬 화면 출력

<그림 8-3> sorted.txt 파일 내용

프로그램을 실행해 보면 현재 작업 디렉토리(프로젝트 디렉토리)의 소스 파일이 들어 있는 곳에

sorted.txt 파일이 생성된 것을 볼 수 있다. 그림 8-2는 완성된 버블정렬 프로그램의 화면 출력이

고, 그림 8-3은 저장된 파일을 메모장으로 열어 본 것이다. 두 가지의 출력 형식이 동일한 것을

확인할 수 있다.

8.2 탐색(Search)

버블정렬 프로그램으로 파일로 저장한 데이터를 읽어 배열에 저장하고, 사용자로부터 정수를

입력 받아 그 수가 배열에 존재하는지 탐색하는 프로그램을 작성하라.

8.2.1 요구사항 분석

버블정렬 프로그램을 실행하면, sorted.txt 파일에 그림 8-3과 같이 데이터의 수와 정렬되어 있

는 데이터가 차례대로 저장한다. 이 파일을 읽어 배열에 저장하고, 사용자가 입력한 숫자가 배열

에 들어있는지 탐색하는 문제이다. 사용자의 입력 이외에도 파일에서도 데이터를 입력하여야 한

다는 것이 이 문제의 특징이다. 프로그램의 입출력을 다음과 같이 정한다.

Page 153: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

151

파일 데이터 출력 원본: 12

54 57 161 178 236 375 482 586 588 696

910 943

입력 출력

검색할 수를 입력하세요: 499 데이터가 파일에 없습니다.

검색할 수를 입력하세요: 586 데이터가 8 번째에 있습니다.

검색할 수를 입력하세요: -1 탐색을 종료합니다.

프로그램을 시작하면서 데이터 파일을 읽고, 파일 읽기가 올바로 실행되었는지 확인하기 위

하여 파일에서 읽은 데이터를 한 줄에 10 개씩 출력한다.

사용자가 0보다 큰 수를 입력하면, 데이터 검색을 반복한다.

데이터가 존재하면, 몇 번째 존재하는지 출력한다.

데이터가 존재하지 않으면, 데이터가 없다고 출력한다.

사용자가 0 이하의 수를 입력하면, 프로그램을 종료한다.

8.2.2 자료구조 설계

이 문제의 경우도 데이터를 저장할 배열의 크기가 정해져 있지 않다. 즉, 파일에서 데이터의 수

를 읽어 배열의 크기를 정해야 한다. 그러므로 “7.2 버블정렬”에서와 같이 배열을 동적으로 할당

하여 사용해야 한다. 이외에도 사용자가 입력한 데이터를 저장하고, 탐색 결과를 저장할 변수가

필요하다. 필요한 자료구조는 다음과 같다.

int size; // 배열의 크기. 파일에서 배열의 크기를 읽는다.

int *data; // 배열을 동적으로 할당하여 연결하기 위한 포인터

int key; // 사용자가 입력한 데이터

int index; // 배열에서 key를 탐색한 결과

8.2.3 프로그램 구조 설계

요구 사항에 의하면 프로그램은 다음과 같은 순서로 진행되어야 한다. 사용자가 입력한 데이터

를 탐색하는 부분을 반복문으로 처리한다.

<리스트 8-4> 탐색 프로그램의 흐름

1. 파일에서 데이터를 읽어 배열에 저장한다.

2. 읽은 데이터를 화면으로 출력한다.

3. 다음을 반복한다.

4. 사용자의 입력을 받는다.

5. 파일에 사용자가 입력한 수가 존재하는지 검사한다.

6. 탐색 결과를 출력한다.

파일에서 데이터를 읽는 부분(라인 1)과 탐색(라인 5)를 제외한 부분을 제외하고, 나머지는 이미

구현해 보았다. 일단 프로그램의 흐름을 다음과 같이 구현한다.

Page 154: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

152

<리스트 8-5> 프로그램의 흐름 구현

1. #include <stdio.h>

2. void print_array(char *str, int array[], int size); // 배열 출력 함수 선언

3. void main(void)

4. {

5. int data[] = { 1, 2, 3, 4, 5, 6, 7 }; // 임시로 배열을 사용한다.

6. int size = 7; // 임시로 배열의 크기를 7로 정한다.

7. int index, key = 1;

8. // 파일에서 데이터를 읽는다. // 추후 구현

9. print_array("원본: ", data, size);

10. while (key > 0)

11. {

12. printf("검색할 수를 입력하세요: "); // 사용자의 입력을 받는다.

13. scanf("%d", &key);

14. if (key > 0)

15. {

16. // key를 탐색하고 결과를 출력한다. // 추후 구현

17. printf("%d를 탐색합니다.", key);

18. }

19. }

20. printf("프로그램을 종료합니다.\n\n");

21. }

22. void print_array(char *str, int array[], int size) { … } // “7.2 버블정렬”의 함수와 동일

라인 5: 아직 파일에서 데이터를 읽지 않았기 때문에, print_array() 함수가 올바로 동작하는지

확인하기 위하여 임시로 data를 배열로 선언하고 초기화 한다. 나중에 정수형 포인터로 변경

되어야 한다. 배열을 선언하고 초기화 할 때, 배열의 크기를 정하지 않으면, 컴파일러가 초기

화 목록에서 원소의 수를 계산하여 배열의 크기를 정해준다.

라인 6: 배열의 크기도 일단 7로 초기화 한다. 나중에 파일에서 읽은 데이터 수로 변경되어야

한다.

라인 7: 처음에 라인 10의 while 반복문을 실행하도록 key를 1로 초기화 한다.

라인 8: 파일 읽기 함수를 호출할 예정이다.

라인 16: 탐색 함수를 호출하는 부분으로 변경되어야 한다.

라인 17: 탐색 결과를 출력하는 부분으로 대치되어야 한다. 일단 사용자가 입력한 수를 출력

한다.

Page 155: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

153

프로그램을 실행하면 다음과 같은 결과를 볼 수 있다. 전체적인 흐름 제어가 올바로 동작하는

것을 확인한 후 다음 과정으로 진행한다.

<그림 8-4> 프로그램의 흐름 구현 결과

이 문제의 핵심은 데이터가 오름차순으로 정렬되어 있는 배열에서 특정 데이터를 탐색하는 것

이다. 탐색 알고리즘은 선형탐색(linear search)과 이진탐색(binary search) 알고리즘이 있으며, 이

두 가지 알고리즘을 모두 구현해 보기로 한다. 다음과 같은 순서로 프로그램을 개발해 보자.

8.2.4 파일 입력: 파일에서 데이터를 입력하는 방법을 설명한다.

8.2.5 파일 읽기 구현: 파일에서 데이터를 읽어 배열에 저장한다.

8.2.6 파일 읽기 함수 분리: 파일 읽기를 함수로 분리하여 구현한다.

8.2.7 선형탐색(linear search): 선형탐색 알고리즘을 설계하고 구현한다.

8.2.8 이진탐색(binary search): 이진탐색 알고리즘을 설계하고 구현한다.

8.2.4 파일 입력

파일에서 데이터를 입력하는 라이브러리 함수들 중에서 fscanf() 함수가 있다. 이 함수는 다음과

같이 선언되어 있다.

#include <stdio.h>

int fscanf(FILE *fp, const char *format, …)

파라미터

FILE *fp: 데이터를 입력할 파일 포인터. 파일을 열 때, 리턴한 값을 적는다.

char *format(): 입력할 형식 문자열

추가 파라미터(…): format 안에 포함되어 있는 변환 기호에 대응하여 데이터를 받을 변수

의 포인터

리턴

성공적으로 읽은 데이터의 수

fscanf() 함수 설명에서 알 수 있듯이, 이 함수는 scanf()와 기능이 동일하고 다만 표준입력장치

(stdin) 대신에 파일에서 데이터를 읽는 것만 다르다. 형식 문자열에 포함되는 변환 기호도 우리가

그 동안 사용하였던 scanf() 함수의 변환 기호와 동일하다. 파일에서 데이터를 읽는 예는 다음과

같다. 이 프로그램이 동작하려면, myfile.txt가 존재해야 하고, 이 안에 실수 데이터와 문자열 데이

터가 들어 있어야 한다. 파일의 내용 예제와 출력 결과도 함께 제시하였다.

Page 156: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

154

<리스트 8-6> fscanf() 사용 예

1. #include <stdio.h>

2. int main ()

3. {

4. char str [80];

5. float f;

6. FILE * pFile;

7. pFile = fopen ("myfile.txt","r"); // myfile.txt를 읽기 모드로 연다.

8. fscanf (pFile, "%f", &f); // 파일에서 실수를 읽는다.

9. fscanf (pFile, "%s", str); // 파일에서 문자열을 읽는다.

10. fclose (pFile); // 파일을 닫는다.

11. printf ("I have read: %f and %s \n", f, str); // 읽은 데이터를 출력한다.

12. return 0;

13. }

myfile.txt 파일의 내용 예

1.234

예제 파일입니다.

프로그램의 출력

I have read: 1.245 and 예제 파일입니다.

파일에서 데이터를 읽으려면, 데이터를 포함하고 있는 파일이 있어야 한다. 우리는 “7.2 버블정

렬”에서 생성한 sorted.txt를 사용할 것이다. 프로그램으로 sorted.txt 파일을 읽을 때, 파일의 위치

를 지정해야 한다. 파일을 지정하는 방법은 다음과 같이 두 가지 방법이 있을 수 있다.

절대 경로: “디스크 이름:\경로\...\파일이름”과 같이 파일이 존재하는 경로를 모두 표시한다.

상대 경로: 현재 작업 중인 디렉토리를 기준으로 파일 이름만 지정한다.

“8.1.1 파일 저장”에서 sorted.txt를 저장할 때, 파일 이름만 지정하였으므로 상대 경로를 지정한

것이다. 파일의 절대 경로는 “C:\작업디렉토리\프로젝트디렉토리\소스코드디렉토리\sorted.txt”

이다. 디스크, 작업디렉토리, 프로젝트디렉토리, 소스코드디렉토리는 사용자의 프로그램 개발 환경

에 따라 그 이름이 다를 것이다. 여기에서는 sorted.txt 파일을 그대로 사용하기 위하여 절대 경로

를 사용하기로 한다.

Page 157: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

155

C 언어는 문자열을 표현할 때 ‘\’(backslash) 기호를 특수 문자를 표현하는 용도로 사용한다. 그

동안 우리가 사용하였던 특수 문자는 ‘\n’(new line), ‘\t’(tab)이 있다. 그러므로, ‘\’ 자체를 표현하

기 위하여 특별한 방법이 필요하며, C 언어에서는 ‘\\’로 ‘\’ 문자를 표현한다. 위에서 예로 든

절대 경로를 C 언어의 문자열로 초기화하는 방법은 다음과 같다.

char *fpath = “C:\\작업디렉토리\\프로젝트디렉토리\\소스코드디렉토리\\sorted.txt”;

8.2.5 파일 읽기 구현

파일 읽기 기능을 main() 함수 안에 먼저 구현하고 나서, 나중에 함수로 분리하기로 하자. 파일

을 읽는 방법은 다음과 같다.

<리스트 8-7> 파일 읽기

1. 파일을 연다.

2. 파일에서 배열의 크기를 읽는다.

3. 배열을 저장할 기억장치를 할당한다.

4. 배열에서 배열의 크기만큼 데이터를 읽는다.

5. 파일을 닫는다.

파일 읽기를 구현하기 위하여 리스트 5-30의 main() 함수를 다음과 같이 수정한다.

<리스트 8-8> 파일 읽기 기능 추가

1. #include <stdlib.h> // malloc()을 선언하는 헤더 파일 추가

2. void main(void)

3. {

4. int *data = NULL, size = 0; // 변수 수정 및 초기화

5. int index, key = 1;

6. char temp[20]; // 파일 읽기에 필요한 변수 추가

7. int n;

8. char *fpath = "G:\\교양\\Projects\\Chap05\\bubble\\bubble\\sorted.txt";

9. FILE *fp = fopen(fpath, "r"); // 파일 포인터 선언 및 파일 열기

10. if (fp != NULL) // 파일을 성공적으로 열었다면

11. {

12. fscanf(fp, "%s %d", temp, &size); // 문자열 “원본:”과 데이터 크기를 읽는다.

13. if (size != 0)

14. {

15. data = (int *)malloc(sizeof(int)*size); // 배열을 동적으로 할당한다.

16. for (n=0; n<size; n++) // 데이터를 읽는다.

17. fscanf(fp, "%d", &data[n]);

Page 158: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

156

18. }

19. fclose(fp); // 파일을 닫는다.

20. }

21. else

22. printf(“파일이 존재하지 않습니다.”);

23. ... // 리스트 8-5의 라인 9 ~ 20과 동일

24. if (data != NULL) // 기억장소를 동적으로 할당하므로 프로그램을 종료하기 전에

25. free(data); // 해제한다.

26. }

라인 4: 배열을 할당할 포인터와 데이터의 수를 선언하고 초기화 한다.

라인 7: 파일의 첫 줄에 “정렬: 50”이 기록되어 있다. 문자열 “정렬: “을 읽어 버리기 위하여

문자열 변수가 필요하다.

라인 8: 경로를 표현하는 문자열을 초기화 한다.

라인 9: 파일 포인터를 선언하고 파일을 연다.

라인 12: scanf()와 fscanf() 함수는 한 줄의 데이터를 읽고 그 안에서 형식 문자열의 변환 기

호에 해당하는 데이터를 읽는다. 파일의 첫 번째 줄에 문자열과 숫자가 들어 있으므로, 각각

temp와 size 변수에 읽는다.

라인 19: 파일을 다 읽었으므로 파일을 닫는다.

라인 21, 22: 만일 이 부분이 실행된다면, 파일 경로를 잘못 지정한 것이다.

라인 24, 25: 기억장소를 동적으로 할당하므로, 기억장소를 해제하는 부분을 추가하여야 한다.

프로그램을 실행하여 파일을 올바로 읽었는지 확인한다. 그림 8-5는 실행 결과이다.

<그림 8-5> 파일 읽기 실행 결과

8.2.6 파일 읽기 함수 분리

main() 함수 안에 프로그램 흐름 제어 이외에 특정한 기능을 수행하는 처리 부분이 들어 있는

것은 좋은 프로그램 스타일이 아니다. 리스트 8-8의 라인 6부터 라인 22까지 파일을 읽는 부분을

read_data() 함수로 분리해 보자.

Page 159: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

157

그런데 이 함수는 할당 받은 배열의 포인터와 데이터 크기를 리턴해야 한다. C 언어의 함수는

명시적으로 return 문을 통하여 한 개의 데이터만 리턴할 수 있다. 두 개 이상의 데이터를 리턴

받으려면, 포인터를 파라미터로 전달하여 간접적으로 리턴 받아야 한다. 여기에서는 둘 중 하나를

명시적으로 리턴 받고 나머지 하나를 파라미터를 통하여 간접적으로 리턴 받도록 작성하려고 한

다. 두 가지 방법으로 read_data() 함수를 구현해 보자. 방법에 따라 함수를 선언하는 방법이 달리

지며, 두 방법의 차이점은 다음과 같다.

int *read_data1(int *arrsize)

파라미터

int *arrsize: 배열의 크기를 간접적으로 리턴하기 위하여, 정수형 변수의 포인터를 파라미

터로 받는다.

리턴

파일에서 데이터를 수와 데이터를 읽어 배열에 저장하고, 배열의 포인터를 명시적으로

리턴한다.

int read_data2(int **parray)

파라미터

int **parray: 배열의 포인터를 간접적으로 리턴하기 위하여, 배열을 가리킬 포인터의 포

인터를 파라미터로 받는다. 따라서 파라미터를 이중 포인터로 선언한다.

리턴

파일에서 데이터를 수와 데이터를 읽어 배열에 저장하고, 데이터의 수를 명시적으로 리

턴한다.

다음은 함수를 호출하는 방법과 함수의 처리 과정을 서로 비교하여 설명한 것이다.

main()

{

int *data=NULL, size=0;

// size의 주소를 전달하고

// read_data1() 함수가 리턴하는 포인터를

// data에 저장한다.

data = read_data(&size); // 함수 호출

}

// 파라미터 int arrsize:

// 간접적으로 크기를 전달할 변수의 주소

// 리턴: 배열의 주소(포인터)

int *read_data1(int *arrsize)

{

int *array= NULL, size=0;

main()

{

int *data=NULL, size=0;

// 포인터인 data의 포인터를 전달하고

// read_data2() 함수가 리턴하는 크기를

// size에 저장한다.

size = read_data2(&data); // 함수 호출

}

// 파라미터 int **parray:

// 간접적으로 배열을 주소를 이중 포인터

// 리턴: 배열의 크기

int read_data2(int **parray)

{

int *array=NULL, size=0;

Page 160: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

158

size = 배열의 수; // 배열의 크기를 결정

array = malloc(size); // 배열을 할당

// 배열에 데이터 저장

*arrsize = size; // 간접적으로 크기 전달

return array; // 포인터 리턴

}

size = 배열의 수; // 배열의 크기를 결정

array = malloc(size); // 배열을 할당

// 배열에 데이터 저장

*parray = array; // 간접적으로 포인터 전달

return size; // 정수 리턴

}

여기에서는 두 가지 방법 중 두 번째 방법만 구현하기로 한다. 파일 읽기 함수는 다음과 같이

구현된다. 프로그램의 실행 결과는 그림 8-5와 같아야 한다.

<리스트 8-9> 파일 읽기 함수 구현 2

1. int read_data2(int **dparray); // 프로그램 앞 부분에 함수 선언 추가

2. void main()

3.

4. {

5. …

6. size= read_data2(&pdata); // 리스트 8-8의 라인 6~22를 함수 호출로 교체

7. }

8. int read_data2(int **parray) // read_data2() 구현

9. {

10. char temp[20];

11. int *array = NULL, size = 0, n;

12. char *fpath = "G:\\교양\\Projects\\Chap05\\bubble\\bubble\\sorted.txt";

13. FILE *fp = fopen(fpath, "r");

14. if (fp != NULL)

15. {

16. fscanf(fp, "%s %d", temp, &size); // 크기를 결정한다.

17. if (size != 0)

18. {

19. array = (int *)malloc(sizeof(int)*size); // 배열을 할당한다.

20. for (n=0; n<size; n++)

21. fscanf(fp, "%d", &array[n]); // 배열에 데이터를 읽는다.

22. }

23. fclose(fp);

24. }

25. else

Page 161: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

159

26. printf("파일이 존재하지 않습니다.\n");

27. *parray = array; // 간접적으로 배열의 포인터를 전달한다.

28. return size; // 크기를 리턴한다.

29. }

8.2.7 선형탐색(linear search)

데이터가 정렬되어 있는 배열에서 특정한 수를 찾는 알고리즘 중에서 개념이 간단한 선형탐색

을 먼저 설계하고 구현해 보자. 그림 8-6은 선형탐색 알고리즘의 처리 과정을 보여준다.

배열에 17이 존재하는지 검사하기 위하여 배열의 처음부터 17과 같은 숫자가 있는지 검사한다.

만일 현재 탐색 중인 배열 원소의 값이 더 적으면, 다음 원소를 검사한다. 그림 8-6(a)는 다섯 번

째 만에 찾기에 성공한 모습이다. 그림 8-6(b)는 배열에서 25를 탐색하는 예이다. 그렇지만 이 경

우에 25가 배열에 존재하지 않는다. 배열에 수가 오름차순으로 정렬되어 있기 때문에, 만일 배열

의 값이 찾는 수보다 더 크면 더 이상 검사할 필요가 없다.

<그림 8-6> 선형탐색 알고리즘

배열이 data[SIZE]라고 하고, 탐색하는 수를 키(key)라고 때, 선형탐색 알고리즘을 다음과 같이

정리할 수 있다.

<리스트 8-10> 선형탐색 알고리즘

1. n=0부터 n<SIZE까지

2. 만일 key = data[n]이면

3. 배열 인덱스 n을 리턴한다.

4. 만일 key < data[n] 이면

5. 더 이상 탐색할 필요가 없으므로 반복문을 벗어난다.

6. 찾지 못했다는 의미로 -1을 리턴한다.

이제 제시한 선형탐색 알고리즘을 구현해 보자. 선형탐색 함수의 선언과 구현은 다음과 같다.

int linear_search(int array[], int size, int key);

파라미터

int array[]: 탐색할 배열

int size: 배열의 크기

Page 162: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

160

int key: 탐색할 값

리턴

key가 배열에 존재하면, 해당 원소의 인덱스를 리턴하고,

존재하지 않으면 -1을 리턴한다.

<리스트 8-11> 선형탐색 구현

1. int linear_search(int array[], int size, int key); // 함수 선언 추가

2. void main(void)

3. {

4. ...

5. if (key > 0) // 리스트 5-30의 라인 14~18을 교체한다.

6. {

7. index = linear_search(data, size, key); // 탐색을 수행한다.

8. if (index != -1) // 발견하였다면

9. printf("데이터가 %d 번째에 있습니다.\n", index+1);

10. else // 발견하지 못했다면

11. printf("데이터가 파일에 없습니다.\n");

12. }

13. ...

14. }

15. int linear_search(int array[], int size, int key) // 리스트 5-35 구현

16. {

17. int n;

18. for (n=0; n<size; n++) // 배열의 처음부터 끝까지

19. {

20. if (key == array[n]) // 찾았으면

21. return n; // 배열의 인덱스를 리턴한다.

22. if (key < array[n]) // 배열의 원소가 키 보다 크면

23. break; // 탐색을 중단한다.

24. }

25. return -1; // 찾지 못했으므로, -1을 리턴한다.

26. }

라인 9에서 배열의 원소는 data[0]부터 시작하므로, index+1을 출력하였다. 프로그램 실행 결과는

다음과 같다.

Page 163: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

161

<그림 8-7> 탐색 결과

8.2.8 이진탐색(binary search)

선형탐색 알고리즘은 처음부터 끝까지 탐색하기 때문에 탐색 시간이 많이 소요된다. 이 문제점

을 해결할 수 있는 방법이 이진탐색 알고리즘이다. 그림 8-8은 10 개의 원소를 갖는 배열

data[SIZE]에서 key=25를 이진탐색 알고리즘으로 탐색하는 과정을 보여준다. 이 알고리즘은 탐색

범위를 나타내는 low, high 변수와 탐색 위치를 나타내는 center 변수를 사용한다.

<그림 8-8> 이진탐색 알고리즘

초기화 과정으로 low=0(배열의 첫 번째 인덱스), high=SIZE-1=9(배열의 마지막 인덱스)로 설

정한다.

탐색 위치는 배열 탐색 구간의 중간이다. center = (low+high)/2 = 4이다.

키와 data[4]를 비교한다.

키가 더 크므로, 찾고자 하는 데이터는 data[4] 이후에 존재한다는 것을 알 수 있다.

low=center+1=5로 갱신하여, 탐색 구간을 data[5]부터 data[9]로 좁힌다.

다시 탐색 위치를 center=(low+high)/2=7로 설정한다.

키와 data[7]을 비교한다.

키가 더 작으므로, 찾고자 하는 데이터는 data[7] 이전에 존재한다는 것을 알 수 있다.

이번에는 high=center-1=6으로 갱신하여, 탐색 구간을 data[5]부터 data[6]으로 좁혀서 다시

탐색한다.

탐색 과정에서 키와 같은 배열 원소를 발견하면 그 인덱스를 리턴하고, 만일 low>high로 될

Page 164: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

162

때까지 키를 찾지 못한다면 탐색에 실패한 것이다.

이진탐색 과정을 알고리즘으로 표현하면 다음과 같다.

<리스트 8-12> 이진탐색 알고리즘

1. low=0(배열의 첫 번째 인덱스), high=SIZE-1(배열의 마지막 인덱스)로 설정한다.

2. low<=high이면 아직 탐색할 영역이 남아 있으므로 다음을 반복한다.

3. center = (low+high)/2로 설정한다.

4. 만일 key = data[center]이면, center를 리턴한다.

5. 만일 key > data[center]이면, low=center+1로 갱신한다.

6. 만일 key < data[center]이면, high=center-1로 갱신한다.

7. 반복문을 벗어났다면, key를 찾지 못한 것이므로 -1을 리턴한다.

마지막으로 리스트 8-12의 이진탐색 프로그램을 구현해보자. 이진탐색 함수 binary_search()의 파

라미터와 리턴 값은 linear_search()와 같다. 리스트 8-11의 라인 7을 binary_search()를 호출하도록

변경하고, 다음과 같이 이진탐색 알고리즘을 구현하고 프로그램을 실행해 보자. 실행 결과는 그림

8-7과 같다.

<리스트 8-13> 이진탐색 구현

1. int binary_search(int array[], int size, int key)

2. {

3. int low, high, center;

4. low = 0;

5. high = size - 1;

6. while (low <= high)

7. {

8. center = (low + high)/2;

9. if (key == array[center])

10. return center;

11. else if (key > array[center])

12. low = center + 1;

13. else // key < array[center]

14. high = center - 1;

15. }

16. return -1;

17. }

8.3 요약

파일의 종류는 텍스트 파일과 이진 파일이 있다.

파일을 입출력하려면, 먼저 파일을 열고, 파일을 사용하고, 마지막으로 파일을 닫는다.

Page 165: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

163

텍스트 파일에 데이터를 출력하는 fprintf() 함수는 표준출력장치로 출력하는 printf() 함수와

기능이 같다. 파일 포인터를 사용하는 것만 다르다.

텍스트 파일에서 데이터를 입력하는 fscanf() 함수는 표준입력장치에서 입력하는 scanf() 함수

와 기능이 같다. 파일 포인터를 사용하는 것만 다르다.

파라미터를 통하여 데이터를 간접적으로 리턴 받으려면 변수의 포인터를 파라미터로 전달하

여야 한다. 마찬가지로 파라미터를 통하여 배열의 포인터를 간접적으로 리턴 받으려면 포인

터 변수의 포인터를 파라미터로 전달하여야 한다.

FILE *fopen(cost char *filename, const char *mode) // 파일을 오픈한다.

int fclose(FILE *stream) // 파일을 닫는다

int fprintf(FILE *fp const char *format, …) // 파일에 데이터를 출력한다.

int fscanf(FILE *fp, const char *format, …) // 파일에서 데이터를 읽는다.

Page 166: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

164

제9장 문자열 처리

컴퓨터는 숫자뿐만 아니라 문자열도 잘 처리한다. 다만, 컴퓨터는 문자 자체를 인식할 수 없기

때문에 문자를 숫자의 형태인 문자 코드(character code)로 변환하여 인식한다. 이 장에서는 문자

열 처리와 관련하여 다음과 같은 프로젝트를 다룬다.

1. 문자 입출력: 사용자가 키보드로 입력한 문자를 파일로 저장한다.

2. 영문자 세기: 영문이 기록되어 있는 텍스트 파일을 읽고, A부터 Z까지 영문자가 몇 개 포함되

어 있는지 세어 출력한다.

3. 단어 정렬: 사용자로부터 단어를 입력 받아 저장하고, 입력이 끝나면 사전 순으로 정렬하여

출력한다

4. 단어 세기: 영문이 기록되어 있는 텍스트 파일을 읽고, 파일에 포함되어 있는 단어를 추출한

다. 서로 다른 단어를 구별하고 단어를 오름차순으로 정렬하여 파일에 단어가 나타난 횟수를

출력한다.

이 장에서 소개되는 프로그래밍 기술은 다음과 같다.

문자 및 문자열의 파일 입출력

포인터 활용

문자열 정렬 및 탐색

구조체 및 구조체 배열 사용 방법

9.1 영문자 세기

영어 문장이 기록되어 있는 텍스트 파일을 읽고, A부터 Z까지 영문자가 몇 개 포함되어 있는지

세어 출력하는 프로그램을 작성하라. 단, 소문자와 대문자를 구별하지 않고 숫자 및 특수 문자는

제외한다.

9.1.1 요구사항 분석

프로그램이 처리해야 할 입력 데이터가 텍스트 파일로 주어진다. 텍스트 파일의 내용을 읽고,

그 안에 포함되어 있는 영문자의 수를 소문자와 대문자를 구별하지 않고 세어야 한다. C 라이브

러리 함수인 malloc()의 동작을 설명하는 텍스트 파일로 저장하여 입력 데이터로 활용하기로 한다.

프로그램의 입출력 설계는 다음과 같다.

입력 (파일) 출력

Function malloc()

void* malloc(size_t size);

Allocate memory block

Allocates a block of size bytes of memory, returning a pointer

to the beginning of the block.

The content of the newly allocated block of memory is not

A: 23 B: 10 C: 13 D: 9

… Z: 6

Page 167: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

165

initialized, remaining with indeterminate values.

If size is zero, the return value depends on the particular library

implementation (it may or may not be a null pointer),

but the returned pointer shall not be dereferenced.

9.1.2 문자 코드

컴퓨터는 내부적으로 문자를 숫자로 변환하여 표현한다. 코드란 문자를 서로 구분할 수 있도록

부여한 숫자이다. 영문자는 표 9-1과 같이 아스키(ASCII, American Standard Code for Information

Interchange) 코드를 사용한다. 아스키 코드는 원래 7 비트로 정의되어 있으나 일반적으로 제일

앞에 0을 추가하여 8 비트로 표현한다. 괄호 안의 수는 이진수를 16진수로 읽은 값이다. 임의의

문자에 대한 아스키 코드는 0b6b5b4b3b2b1b0의 순서로 읽는다.

<표 9-1> 아스키 코드표

b3b2b1b0

b6b5b4

000(0) 001(1) 010(2) 011(3) 100(4) 101(5) 110(6) 111(7)

0000(0)

0001(1)

0010(2)

0011(3)

0100(4)

0101(5)

0110(6)

0111(7)

1000(8)

1001(9)

1010(A)

1011(B)

1100(C)

1101(D)

1110(E)

1111(F)

NULL

SOH

STX

ETX

EOT

ENQ

ACK

BEL

BS

HT

LF

VT

FF

CR

SO

SI

DLE

DC1

DC2

DC3

DC4

NAK

SYN

ETB

CAN

EM

SUB

ESC

FS

GS

RS

US

SP

!

#

$

%

&

(

)

*

+

,

-

.

/

0

1

2

3

4

5

6

7

8

9

:

;

<

=

>

?

@

A

B

C

D

E

F

G

H

I

J

K

L

M

N

O

P

Q

R

S

T

U

V

W

X

Y

Z

[

\

]

^

_

`

a

b

c

d

e

f

g

h

i

j

k

l

m

n

o

p

q

r

s

t

u

v

w

x

y

z

{

|

}

~

DEL

아스키 코드는 영문자, 숫자, 특수 기호, 이외에 파일 및 통신 제어 문자도 포함하고 있다. 알아

두면 편리한 문자에 대한 아스키 코드의 값은 표 9-2와 같다.

표 9-2 화면 제어 코드와 영문자와 숫자의 아스키 코드

2진수 16진수 10진수 문자 의미

0000 0111 0x07 7 BEL Bell. 화면으로 출력하면 스피커로 삑 소리가 난다.

0000 1000 0x08 8 BS Backspace. 커서를 왼쪽으로 이동한다.

Page 168: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

166

0000 1001 0x09 9 HT Horizontal Tab. 키보드의 탭 키

0000 1010 0x0A 10 LF Line Feed. 커서를 화면의 다음 라인으로 옮긴다.

0000 1101 0x0D 13 CR Carriage Return. 커서를 화면의 맨 앞으로 옮긴다.

0001 1011 0x1b 27 ESC Escape. 키보의 ESC 키

0010 0000 0x20 32 SP Space. 키보드의 공백 키

0011 0000 0x30 48 ‘0’ 숫자 0. 이후 ‘9’까지 연속적으로 배치되어 있다.

0100 0001 0x41 65 ‘A’ 대문자 A. 이후 ‘Z’까지 연속적으로 배치되어 있다.

0110 0001 0x61 97 ‘a’ 소문자 a. 이후 ‘z’까지 연속적으로 배치되어 있다.

C 언어에서 16진수는 앞에 0x를 붙여 표시한다. 아스키 코드 값을 외울 필요는 없다. 표를 참조

하는 방법을 아는 것으로 족하다. C 언어에서 작은 따옴표로 문자형 상수를 표시하면, 내부적으로

아스키 코드 값을 저장한다. 예를 들어 다음 프로그램을 보자. 출력문의 변환 기호에 따라 문자형

변수 ch의 값을 각각 문자, 십진수, 16진수로 화면에 출력한다. 이와 같이, 프로그램에서는 문자형

상수 표기법을 사용하여 코드 값을 지정할 수 있다.

char ch = ‘A’; // 문자형 변수 ch에 문자형 상수 ‘A’를 저장한다.

printf(“%c %d %x, ch); // 문자형 변수의 값을 문자, 10진수, 16진수로 출력한다.

출력 결과

A 65 41

9.1.3 자료구조 설계

파일에서 데이터를 입력하기 위하여 파일 포인터와 입력한 데이터를 저장하는 변수가 필요하다.

파일에서 데이터를 읽어 들이는 방법에 따라 입력 데이터를 저장하기 위한 자료구조가 달라지며,

다음과 같이 두 가지 방법을 생각할 수 있다.

파일에서 한 개의 문자씩 읽고 문자형 변수에 데이터를 저장하는 방법

파일에서 한 라인씩 읽고 문자열 변수에 데이터를 저장하는 방법

여기에서는 두 가지 방법에 대하여 각각 프로그램을 구현해 보기로 한다. 파일에서 데이터를 입

력하여 저장할 자료구조 설계는 뒤로 미룬다.

출력 데이터를 저장할 자료구조를 설계해 보자. 요구사항에 의하면 텍스트 파일을 읽어 A부터

Z까지 영문자만 세어야 하고, 소문자와 대문자를 구별하지 않아야 한다. 영문자를 세어서 영문자

가 나타난 횟수를 저장하기 위한 변수가 필요하다. 대문자가 모두 26자이므로 다음과 같은 배열

을 사용하기로 한다.

int alphabet[26]; // 영문자가 나타난 횟수를 저장하는 배열

다음과 같은 간단한 공식으로 영문자가 나타난 횟수를 증가시킬 배열의 인덱스를 구할 수 있다.

Page 169: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

167

영문자가 대문자인 경우: 배열의 인덱스 = 문자 – ‘A’

영문자가 소문자인 경우: 배열의 인덱스 = 문자 – ‘a’

9.1.4 프로그램 구조 설계

텍스트 파일의 처음부터 끝까지 데이터를 읽고 알파벳의 수를 세어 저장한 후 출력하여야 하므

로 전체적인 프로그램의 구조를 다음과 같이 설계할 수 있다.

<리스트 9-1> 문자 세기 프로그램의 흐름

1. alphabet[] 배열을 0으로 초기화 한다.

2. 파일을 연다.

3. 파일의 끝까지 다음을 반복한다.

4. 파일에서 영문자를 읽고

5. alphabet[] 배열의 해당 위치를 증가시킨다.

6. 파일을 닫는다.

7. alphabet[] 배열에 저장되어 있는 알파벳의 수를 출력한다.

이 프로그램은 라인 3~5까지 파일에서 문자 또는 문자열을 읽는 부분이 중요하고 나머지 부분

은 그렇게 복잡하지 않다. 따라서, 별도의 함수를 만들지 않고, main() 함수에 모두 구현하기로 한

다. 라인 4, 5를 한 개의 영문자씩 처리하는가 아니면 한 라인씩 처리하는가에 따라 사용할 라이

브러리 함수가 달라진다. 여기에서는 두 가지 방법 모두 구현해 보기로 하였으므로, 다음과 같은

순서에 따라 프로그램을 구현해 보자.

9.1.5 문자 단위 문자 세기 설계 및 구현: 파일에서 문자를 읽는 라이브러리 함수를 소개하고, 문

자 세기 프로그램을 구현한다.

9.1.6 라인 단위 문자 세기 설계 및 구현: 파일에서 한 라인씩 데이터를 읽는 라이브러리 함수를

소개하고, 문자 세기 프로그램을 구현한다.

9.1.5 문자 단위 문자 세기 설계 및 구현

이 문제를 해결하기 위하여 파일의 처음부터 파일의 끝까지 한 개의 문자씩 읽는 방법과 파일

의 끝을 구별하는 방법을 알고 있어야 한다. 이 두 가지 기능을 수행하는 라이브러리 함수는 다

음과 같다.

int fgetc(FILE *stream)

파라미터

FILE *stream: 파일을 구별하는 파일 객체에 대한 포인터

리턴 값

파일 포인터가 가리티는 문자를 리턴하고, 파일 포인터를 다음 문자로 옮긴다.

int feof(FILE *stream)

파라미터

FILE *stream: 파일을 구별하는 파일 객체에 대한 포인터

Page 170: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

168

리턴 값

파일의 끝에 도달하였으면 파일의 끝을 의미하는 0이 아닌 값(EOF)

파일의 끝이 아니면 0(즉, FALSE)

화면에 문자를 출력하기 위하여 scanf() 함수가 아닌 화면으로 한 개의 문자를 출력하는 라이브

러리 함수를 사용해 보자. 이 함수는 다음과 같이 정의되어 있다.

int putchar(int character)

파라미터

int character: 표준출력장치로 출력할 문자

리턴 값

성공하면, 출력한 문자를 리턴한다.

실패하면 EOF를 리턴한다. 일반적으로 리턴 값을 사용하지 않는다.

다음은 이 함수들을 사용하여 텍스트 파일에서 문자를 읽고 화면에 출력하는 예이다. 프로그램

이 실행하려면 텍스트 파일이 존재해야 하므로, 프로젝트를 생성하고 프로젝트의 소스 코드가 있

는 디렉토리에 텍스트 파일을 malloc.txt란 이름으로 저장해 두어야 한다.

<리스트 9-2> 문자 단위 파일 읽기

1. #include <stdio.h>

2. void main()

3. {

4. char ch;

5. FILE *fp = fopen(“malloc.txt”, “r”); // 읽기 모드로 파일을 연다

6. while ( !feof(fp) ) // 파일의 끝이 아니면

7. {

8. ch = fgetc(fp); // 문자 한 개를 읽는다.

9. putchar(ch); // 읽은 문자를 출력한다.

10. }

11. fclose(fp); // 파일을 닫는다.

12. }

프로그램을 실행한 결과는 다음과 같다.

<그림 9-1> 텍스트 파일 화면 출력

Page 171: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

169

이제 문자를 세는 기능을 추가해 보자. 리스트 9-1의 알고리즘을 한 개의 문자씩 처리하도록

다음과 같이 수정한다.

<리스트 9-3> 문자 단위 영문자 세기 알고리즘

1. alphabet[] 배열을 0으로 초기화 한다.

2. 파일을 연다.

3. 파일의 끝(EOF)가 아니면 다음을 반복한다.

4. ch = fgetch(); // 파일에서 문자를 읽는다.

5. 만일 ch가 대문자이면

6. alphabet[ch – ‘A’]를 증가시킨다. // 배열의 인덱스 = ch – ‘A’

7. 만일 ch가 소문자이면

8. alphabet[ch – ‘a’]를 증가시킨다. // 배열의 인덱스 = ch – ‘a’

9. 파일을 닫는다.

10. alphabet[] 배열에 저장되어 있는 알파벳의 수를 출력한다.

다음과 같은 C 언어의 문자 검사 라이브러리를 사용하여 문자가 영문자인지, 대문자인지, 아니

면 소문자인지 구별할 수 있다.

#include <ctype.h>

int isalpha(int c): 파라미터 c가 알파벳 문자이면 0이 아닌 값을 리턴한다.

int isupper(int c): 파라미터 c가 알파벳 대문자이면 0이 아닌 값을 리턴한다.

int islower(int c): 파라미터 c가 알파벳 소문자이면 0이 아닌 값을 리턴한다.

필요한 라이브러리 함수가 모두 해결되었으므로, 다음과 같이 프로그램을 구현한다.

<리스트 9-4> 문자 단위 영문자 세기 프로그램

1. #include <stdio.h>

2. #include <ctype.h> // isupper(), islower(0 선언

3. int alphabet[26]; // 전역 변수

4. void main(void)

5. {

6. char ch;

7. int n;

8. FILE *fp = fopen("malloc.txt", "r");

9. for (n=0; n<26; n++) // alphabet[] 초기화

10. alphabet[n] = 0;

Page 172: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

170

11. while ( !feof(fp) ) // 파일의 끝이 아니면

12. {

13. ch = fgetc(fp); // 문자 한 개를 읽는다.

14. if (isupper(ch)) // 대문자이면

15. alphabet[ch - 'A'] += 1; // 해당 배열 원소를 증가시킨다.

16. else if (islower(ch)) // 소문자이면

17. alphabet[ch - 'a'] += 1; // 해당 배열 원소를 증가시킨다.

18. }

19. fclose(fp); // 파일을 닫는다.

20. printf("파일의 영문자 수\n\n"); // 제목 줄을 출력한다.

21. for (n=0; n<26; n++) // 영문자의 문자 수를 출력한다.

22. printf("%c: %d\t", n+'A', alphabet[n]);

23. }

라인 3: alphabet[]을 전역변수로 사용한다. 컴파일러가 전역변수를 0으로 초기화 하므로, 라

인 9, 10의 초기화 과정은 수행하지 않아도 되지만, 확실하게 0으로 만들기 위하여 추가하였

다.

라인 14~17: 영문자를 배열 영역으로 변환하여 숫자를 세는 부분이다.

프로그램 실행 결과는 그림 6-2와 같다.

<그림 9-2> 영문자 세기 실행 결과

9.1.6 라인 단위 문자 세기 설계 및 구현

이제 파일에서 라인 단위로 문자열을 읽도록 프로그램을 수정해 보자. 텍스트 파일에서 한 라

인씩 문자열을 읽는 라이브러리 함수는 다음과 같다.

char *fgets(char *str, int num, FILE *stream)

파라미터

char *str: 문자열을 읽어올 문자형 배열에 대한 포인터

int num: 문자형 배열에 복사할 문자의 최대 수

FILE *stream: 파일 객체에 대한 포인터.

리턴

파일에서 문자열 읽기에 성공하면 str을 리턴한다.

Page 173: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

171

만일 읽을 때 읽기에 실패하면, 널 포인터를 리턴한다.

이 함수는 파일의 끝에 도달할 때 널 포인터를 리턴하므로, feof() 함수를 사용할 필요가 없다. 그

리고 화면에 문자열을 출력하는 함수는 다음과 같다.

int puts(const char *str): 표준출력장치로 문자열을 출력하고 다음 줄(‘\n’)을 추가한다.

파일에서 한 라인씩 문자열을 읽으려면, 문자열을 저장할 문자열 변수, 즉 문자형 배열이 필요

하다. 문자형 배열의 크기는 한 라인에 포함되어 있는 문자열의 최대 문자 수 보다 커야 한다. 한

라인의 최대 길이가 256 문자라고 가정한다. 이 배열을 다음과 같이 정한다.

char buffer[256]; // 한 라인의 문자열을 읽어 저장할 문자열 변수

앞에서 소개한 두 가지 함수를 사용하여 텍스트 파일을 읽어 화면으로 출력하는 프로그램을 다

음과 같이 작성할 수 있다.

<리스트 9-5> 문자열 단위로 파일 읽기

1. #include <stdio.h>

2. void main(void)

3. {

4. char buffer[256]; // 문자열을 읽을 버퍼

5. FILE *fp = fopen("malloc.txt", "r"); // 텍스트 파일을 읽기 모드로 연다.

6. while (fgets(buffer, 255, fp) != NULL ) // 문자열 읽기에 성공하면

7. {

8. puts(buffer); // 읽은 줄을 출력한다.

9. }

10. fclose(fp); // 파일을 닫는다.

11. }

라인 6: fgets() 함수가 문자열 읽기에 실패하는 경우를 검출하므로, feof() 함수를 사용하지 않

았다. fgets()의 두 번째 파라미터가 읽을 문자열의 최대 문자 수인데, C 언어는 문자열의 마지

막에 NULL 문자(\0)을 추가하므로, 버퍼의 크기–1=255를 전달하여야 한다.

라인 8: 문자열을 출력하기 위하여 puts() 함수를 사용하였다.

프로그램 실행 결과를 보면, 그림 9-1과 다르게 문장마다 줄바꿈(\n)이 한 번 더 추가되는 것을

볼 수 있다. 그 이유는 fgets() 함수가 파일에서 줄바꿈을 포함하여 한 줄을 읽고, puts() 함수가

문자열을 출력하면서 줄바꿈을 한 번 더 추가하기 때문이다.

리스트 9-5에 영문자를 세는 기능을 추가하면 리스트 6-6과 같다. 문자열 buffer[] 내부를 탐색

하기 위하여 문자형 포인터(char *pch)를 사용하였다. 프로그램 실행 결과는 그림 9-2와 같다.

Page 174: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

172

<리스트 9-6> 라인 단위 영문자 세기 프로그램

1. 헤더 부분 리스트 9-4와 동일

2. void main(void)

3. {

4. char buffer[256], *pch; // 문자형 포인터 추가

5. while 문 전까지 리스트 9-4와 동일

6. while ( fgets(buffer, 255, fp) != NULL )

7. {

8. pch = buffer; // 문자 포인터를 버퍼의 첫 번째로 지정한다.

9. while (*pch != NULL) // 문자열의 끝이 아니면

10. {

11. if (isupper(*pch))

12. alphabet[*pch - 'A'] += 1;

13. else if (islower(*pch))

14. alphabet[*pch - 'a'] += 1;

15. pch++; // 문자 포인터를 증가시킨다.

16. }

17. }

18. 이후 리스트 9-4와 동일

19. }

라인 8: 문자 포인터를 buffer의 시작 주소로 설정한다.

라인 15: 포인터를 증가시키면, 배열의 다음 원소를 가리킨다.

9.2 단어 정렬

사용자로부터 영어 단어를 입력 받고, 입력이 끝나면 사전 순으로 정렬하여 출력하는 프로그램

을 작성하라.

9.2.1 요구사항 분석

사용자로부터 영어 단어를 입력 받아 컴퓨터 내부에 저장하였다가, 사용자의 입력이 끝나면 사

전 순으로 정렬하여 출력하는 프로그램이다. 사용자가 입력할 단어의 수를 제한하여야 하기 때문

에 사용자로부터 최대 100 개의 단어를 입력 받기로 한다. 다음과 같이 입력 안내문을 출력하고,

사용자가 빈 문자열을 입력할 때까지 단어를 입력 받도록 한다. 그 동안 입력 받은 단어 수를 먼

저 출력하고, 사용자가 입력한 단어를 사전 순으로 정렬하여 한 라인에 한 단어씩 출력한다.

입력 출력

단어를 입력하세요. 최대 단어 수는 100 개 입니다. 정렬 후 출력: 단어 수 = 3

Page 175: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

173

입력을 중지하려면 엔터를 입력하세요.

1. 단어 입력: hello

2. 단어 입력: world

3. 단어 입력: everyone

4. 단어 입력:

everyone

hello

world

9.2.2 자료구조 설계

사용자가 단어 입력을 중단할 때까지 단어를 입력 받아 저장해 두어야 한다. 단어를 문자형 포

인터 배열에 저장하기로 한다. 사용자가 입력한 단어를 저장하기 위한 자료 구조는 다음과 같다.

#define MAXWORD 100 // 최대 단어 수

char *pstrarray [MAXWORD]; // 단어를 저장할 문자 포인터 배열

int nword; // 단어 수

포인터 배열에 단어를 저장하는 방법에 대하여 조금 더 자세히 알아보자. 포인터 배열은 여러

개의 포인터들을 저장하는 자료구조이다. 포인터는 기억장치의 주소를 의미하므로, pstrarray[]는

100 개의 기억장치 주소를 저장한다. 일반적으로 포인터에 값이 할당되어 있지 않다는 의미로 그

림 9-3(a)와 같이 포인터 배열의 원소에 0을 저장한다. 이 상태를 그림 9-3(b)와 같이 화살표를

사용하여 표현한다. 그림 9-3(c)는 pstrarray[0]에 문자열 “hello”가 연결된 모습이다.

문자열 “hello”는 기억장치 1004 번지부터 차례로 저장되어 있다고 가정하였고 문자열의 마지

막은 0(널 문자)이다.

pstrarray[0]에 문자열 “hello”의 시작 주소인 1004를 저장함으로써, pstrarray[0]에 문자열

“hello”가 연결된 것을 나타낸다.

이 상태를 그림 9-3(d)와 같이 화살표로 표현한다.

Page 176: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

174

<그림 9-3> 포인터 배열에 문자열 저장

<그림 9-4> 단어 입력 후와 정렬 후의 자료 구조

그림 9-4는 세 개의 단어를 입력하였을 때, 자료 구조의 모습이다. 사용자가 입력한 순서대로

단어를 그림 9-4(a)와 같이 포인터 배열에 저장해 두었다가, 입력이 완료되면 데이터를 그림 9-

4(b)와 같이 정렬하여 출력한다.

9.2.3 프로그램 구조 설계

프로그램의 전체적인 흐름을 다음과 같이 설계할 수 있다.

<리스트 9-7> 단어 정렬 프로그램

1. 사용자에게 단어를 입력 받아 저장한다.

2. 단어를 오름차순으로 정렬한다.

3. 정렬된 단어를 출력한다.

Page 177: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

175

4. 할당 받은 공간을 해제한다.

네 가지 기능을 모두 별도의 함수로 구현하기로 한다. 단어 정렬 프로그램을 구현하기 위하여

필요한 자료구조를 전역 변수로 선언하도록 하자. 프로그램의 임의의 영역에서 전역 변수를 사용

할 수 있으므로, 함수를 호출할 때 파라미터로 전달하지 않아도 되므로 편리한 점이 있다. 그리고,

main() 함수를 다음과 같이 네 개의 함수로 구현한다.

<리스트 9-8> 단어 정렬 프로그램 구조

1. #include <stdio.h>

2. #define MAXWORD 100 // 단어 최대 수

3. void get_words(); // 함수 선언

4. void sort_string(); // 함수 선언

5. void print_words(); // 함수 선언

6. void deallocate(); // 함수 선언

7. char *pstrarray[MAXWORD]; // 단어를 저장할 문자 포인터 배열

8. int nword; // 단어 수

9. void main()

10. {

11. get_words(); // 사용자에게 단어를 입력 받아 저장한다.

12. sort_string(); // 단어를 오름차순으로 정렬한다.

13. print_words(); // 정렬된 단어를 출력한다.

14. deallocate(); // 할당 받은 공간을 해제한다.

15. }

16. void get_words() { } // 향후 구현

17. void sort_string() { } // 향후 구현

18. void print_words() { } // 향후 구현

19. void deallocate() { } // 향후 구현

이와 같이 프로그램의 골격을 만들고 컴파일하여 오류가 없음을 확인하고 나서, 함수를 하나씩

구현해 보자. 프로그램 구현 과정을 다음과 같은 순서로 설명한다.

9.2.4 단어 입력: 단어를 입력하여 포인터 배열에 저장하는 get_words() 함수를 구현한다.

9.2.5 기억 공간 해제: 단어를 입력할 때 기억장치를 할당 받으므로, 할당 받은 기억 공간을 해제

하는 deallocate() 함수를 먼저 구현한다.

Page 178: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

176

9.2.6 문자열 출력: 입력 받은 문자열을 출력하는 print_words() 함수를 구현한다.

9.2.7 단어 정렬: 마지막으로 단어를 정렬하는 sort_string() 함수를 구현한다.

9.2.4 단어 입력

사용자가 입력한 단어를 문자형 포인터 배열에 저장하는 방법을 생각해 보자. 만일 사용자가

입력한 데이터가 정수이고, 이것을 정수형 배열에 저장하는 것은 간단하다. 그렇지만, 문자열로

표현되는 단어를 문자형 포인터 배열에 저장하는 일은 몇 가지 고려할 사항이 있다.

사용자로부터 입력 받은 문자열을 임시로 저장하기 위한 문자형 배열이 필요하다. 사용자가 입

력하는 단어의 최대 길이를 MAX_LENGTH라고 한다면, 다음과 같이 지역 변수를 선언하여 사용자

의 데이터를 입력 받는다.

#define MAXLENGTH 100

char buffer[MAX_LENGTH]; // 지역 변수

scanf(“%s”, buffer); // 단어 입력

사용자로부터 단어를 입력 받은 후, 단어를 포인터 배열 pstrarray[]에 저장해야 한다. 그렇지만,

pstrarray[]는 포인터를 저장할 공간만 갖고 있을 뿐이고, 문자열을 저장할 공간을 갖고 있지 않다.

따라서, 포인터 배열의 nword 번째 원소에 단어를 연결하려면, 그림 9-5와 같은 과정을 거쳐야

한다.

<그림 9-5> 단어 저장 과정

1. 사용자에게 단어를 입력 받아 문자형 배열 buffer[]에 저장한다.

2. pstrarray[nword]에 단어를 저장할 기억 공간을 할당한다.

3. 할당 받은 기억 공간에 문자열을 복사한다.

이와 같이 단어를 복사해서 저장해야 다음 단어를 입력 받을 때, buffer[]를 다시 사용할 수 있다.

지금까지의 설명을 종합하여 단어를 입력하여 포인터 배열에 저장하는 알고리즘을 다음과 같이

표현할 수 있다.

<리스트 9-9> 단어 입력 알고리즘

1. nwords를 0으로 초기화 한다.

2. 단어 입력 안내문을 출력한다.

Page 179: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

177

3. (nword < MAXWORD)를 만족하면, 다음 과정을 반복한다.

4. buffer[]에 단어를 입력한다.

5. 사용자가 단어를 입력하지 않았다면

6. 데이터 입력을 중단한다.

7. 아니면

8. buffer[]에 입력 받은 단어의 길이를 구한다.

9. pstrarray[nword]에 기억 장소를 할당한다.

10. buffer[]에 저장되어 있는 문자열을 할당 받은 기억공간에 복사한다.

11. 입력 받은 단어 수 nword를 증가시킨다.

리스트 9-9를 구현하려면, 다음과 같은 문자열 처리 라이브러리 함수를 사용하여야 한다.

get_word() 함수를 구현할 때, 각 함수의 용도도 함께 설명한다.

#include <string.h>

int strcmp(const char*str1, const char str2)

파라미터

char *str1, char *str2: 비교할 문자열의 포인터

리턴

두 문자열이 같으면 0을 리턴하고,

사전 순으로 str1이 str2보다 앞에 있으면, 음수를 리턴하고,

사전 순으로 str1이 str2보다 뒤에 있으면, 양수를 리턴한다.

용도

사용자가 비어 있는 문자열을 입력하였는지 검사하기 위하여 사용한다.

strcmp(buffer, “”)의 값이 0이면, 사용자가 단어를 입력하지 않은 것이다.

size_t strlen(const char *str)

파라미터

char *str: 길이를 구할 문자열의 포인터

리턴

문자열의 길이 (단, 문자열의 마지막을 의미하는 NULL 문자는 포함하지 않는다.)

용도

pstrarray[nword]에 기억 공간을 할당 받으려면, 단어의 길이를 알아야 한다.

문자열의 마지막을 표시하는 널 문자도 포함하도록 기억공간을 할당해야 하기 때문에 기

억 공간의 크기를 strlen(buffer)+1 개 할당한다.

char *strcpy(char *destination, const char *source)

파라미터

char *destination: 문자열의 복사될 목적지에 대한 포인터

Page 180: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

178

char *source: 복사할 문자열이 저장되어 있는 포인터

리턴

문자열 source를 destination으로 복사하고, destination을 리턴한다.

용도

pstrarray[nword]에 기억 공간을 할당 받은 후, 여기에 단어를 복사한다.

strcpy(pstrarray[nword], buffer)를 실행하여 단어를 복사한다.

get_word() 함수의 구현은 다음과 같다.

<리스트 9-10> 단어 입력 get_word() 함수 구현

1. #include <string.h> // 문자열 처리 함수 선언 추가

2. #include <stdlib.h> // 메모리 할당/해제 함수 선언 추가

3. #define MAXLENGTH 100 // 단어의 최대 길이 정의 추가

4. void get_words()

5. {

6. char buffer[MAX_LENGTH]; // 사용자가 입력한 문자열 저장

7. int size; // 받은 문자열의 길이

8. nword = 0; // 전역 변수 초기화

9. printf("단어를 입력하세요. 최대 단어 수는 100 개 입니다.\n"); // 안내문 출력

10. printf("입력을 중지하려면 엔터를 입력하세요.\n\n");

11. while( nword < MAXWORD )

12. {

13. printf("%d. 단어 입력: ", nword+1); // 안내문 출력

14. gets(buffer); // 문자열 입력

15. if (strcmp(buffer, "") == 0) // 엔터키를 입력하였다면

16. break; // 반복문을 벗어나서, 함수 종료

17. else // 단어를 입력하였다면

18. {

19. size = strlen(buffer) + 1; // 단어의 크기를 구하고

20. pstrarray[nword] = (char *)malloc(size); // 기억 공간을 할당하고

21. strcpy(pstrarray[nword], buffer); // 단어를 복사한다.

22. nword += 1; // 단어 수를 증가시킨다.

23. }

24. }

25. }

라인 22: 문자열의 마지막을 나타내는 NULL 문자도 저장해야 하기 때문에, 사용자가 입력한

단어의 문자수 보다 하나 많게 기억 공간을 할당한다.

Page 181: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

179

여기까지 프로그램을 작성하고 실행해 보자. 사용자가 입력한 단어가 올바로 입력되는지 확인

하기 위하여 그림 9-6과 같이 get_words()를 호출하는 문장 다음에 중단점을 설정하고, 디버그 모

드로 실행한다. 단어 수를 나타내는 nword와 단어를 저장하는 포인터 배열 pstrarray[]가 전역 변

수로 선언되어 있으므로, 데이터 확인 창을 조사식1로 선택하고 전역 변수의 이름을 추가한다. 세

개의 단어를 입력하였을 때, 그림 9-3과 같이 올바로 단어가 저장된 것을 확인할 수 있다.

<그림 9-6> 단어 입력 확인

9.2.5 기억 공간 해제

사용자에게 단어를 입력 받아 저장할 때 기억 공간을 할당 받으므로, main() 함수의 마지막 단

계에서 기억 공간을 해제하는 과정이 필요한 것이다. 기억 공간을 할당하였으므로, 다른 부분을

구현하기 전에 기억 공간을 해제하는 deallocate() 함수를 먼저 구현한자. 이 함수는 pstrarray[0] ~

pstrarray[nword-1]까지 할당된 nword 개의 기억 공간을 해제한다. 프로그램이 간단하므로, 구현

결과만 제시한다.

<리스트 9-11> 기억 공간 해제 deallocate() 함수 구현

1. void deallocate()

2. {

3. int n;

4. for (n=0; n<nword; n++) // nword 까지

5. free(pstrarray[n]); // 기억 공간을 해제한다.

6. }

Page 182: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

180

9.2.6 문자열 출력

다음에 리스트 9-12와 같이 print_word() 함수를 구현하고, 프로그램을 실행해 보자. 이 프로그

램은 단순히 포인터 배열에 연결되어 있는 단어를 출력한다. 아직 문자열을 정렬하지 않았지만,

그림 9-7과 같은 실행 결과를 볼 수 있다.

<리스트 9-12> 문자열 출력 print_word() 함수 구현

1. void print_words()

2. {

3. int n;

4. printf("\n정렬 후 출력: 단어 수=%d\n", nword);

5. for (n=0; n<nword; n++)

6. puts(pstrarray[n]);

7. }

라인 6: 출력 함수로 printf() 함수 대신에 puts() 함수를 사용하였다. 이 함수는 자동으로 줄

바꿈을 추가한다.

<그림 9-7> 문자열 입력 및 출력 화면

9.2.7 단어 정렬

단어를 정렬하는 알고리즘은 정수를 정렬하는 알고리즘과 같다. 여기서는 선택정렬 방법을 사

용해 보자. 리스트 9-13은 “7.2.9 선택정렬 함수”에서 학습하였던 정수에 대한 선택 정렬 알고리즘

이다. 이 알고리즘의 라인 4와 라인 6을 문자열을 처리할 수 있도록 수정하면 문자열 정렬에 적

용할 수 있다.

<리스트 9-3> 정수 선택정렬 알고리즘

1. n=0부터 n<SIZE-1까지

2. minindex = n으로 가정한다.

3. m=n부터 m<SIZE까지

4. 만일 array[minindex] > array[m] 이면

5. minindex = m을 저장한다.

6. 최소값의 인덱스를 구한 후, array[n]과 array[minindex]를 교환한다.

Page 183: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

181

라인 4: 정수를 비교하는 대신에 strcmp() 함수를 사용하여 문자열을 비교한다.

라인 6: 정수를 교환하는 대신에 문자열에 대한 포인터를 교환한다.

단어를 선택정렬 알고리즘으로 정렬하는 sort_string() 함수를 구현해 보자. 쉽게 구현하기 위하

여, 정수형 데이터에 대한 선택정렬 알고리즘을 복사하고 나서 리스트 9-17과 같이 수정해 보자.

이제 단어 정렬 프로그램이 완성되었다. 프로그램 실행 결과는 그림 9-8과 같다.

<리스트 9-17> 단어 정렬 sort_string() 함수 구현

1. void sort_string() // selection_sort(int array[]) 복사하여 수정한다.

2. {

3. int n, m, minindex;

4. char *temp; // 문자열을 교체하기 위하여 임시 변수를 추가한다.

5. // for (n=0; n<SIZE-1; n++) // 배열의 크기를 nword로 변경한다.

6. for (n=0; n<nword-1; n++)

7. {

8. minindex = n; // pstrarray[n]을 사전 순으로 가장 적다고 가정한다.

9. //for (m=n; m<SIZE; m++) // 배열의 크기를 nword로 변경한다.

10. for (m=n; m<nword; m++)

11. //if (array[minindex] > array[m]) // 문자열 비교 함수로 교체한다.

12. if (strcmp(pstrarray[minindex], pstrarray[m]) > 0)

13. minindex = m; // 최소 값의 인덱스를 저장한다.

14. // swap(&array[minindex], &array[n]); // 문자열을 직접 교체한다.

15. temp = pstrarray[minindex];

16. pstrarray[minindex] = pstrarray[n];

17. pstrarray[n] = temp;

18. }

19. }

Page 184: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

182

<그림 9-8> 단어 정렬 프로그램 실행 결과

9.3 단어 세기

영어 문장이 기록되어 있는 단어를 텍스트 파일을 읽고, 파일에 포함되어 있는 단어를 추출하

라. 서로 다른 단어를 구별하고 단어가 나타난 횟수를 구하고, 단어를 오름차순으로 정렬하여 파

일에 단어와 단어가 나타난 횟수를 화면으로 출력하라. 대문자와 소문자를 구별하지 않고, 단어를

모두 소문자로 출력한다.

9.3.1 요구사항 분석

문제에 요구사항이 잘 정리되어 있다. 처리할 작업을 명확하게 정리하면 다음과 같다.

텍스트 파일을 입력으로 사용한다.

텍스트 파일에 포함되어 있는 영어 문장에서 단어를 분리한다.

단어를 분리한 후 단어를 모두 소문자로 변환하여 저장한다.

텍스트 파일의 영어 문장을 끝까지 읽으면서 단어가 나타난 횟수를 구한다.

최종적으로 단어와 단어가 나타난 횟수를 오름차순으로 정렬하여 출력한다.

입력 파일로 팝송 가사를 사용해 보자. 단어를 정렬한 후 단어가 나타난 횟수를 다음과 같이

출력한다.

Page 185: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

183

입력 파일 출력

When I find myself in times of trouble

Mother Mary comes to me

Speaking words of wisdom

Let it be.

And in my hours of darkness

She is standing bright in front of me

Speaking words of wisdom

Let it be.

Let it be.

Let it be.

Let it be.

Let it be.

Whisper words of wisdom

Let it be.

1. and : 1

2. be : 6

3. bright : 1

4. comes : 1

5. darkness : 1

6. find : 1

9.3.2 구조체

단어 세기 문제에서 단어와 단어 수가 서로 연관되어 있다. 이와 같은 경우에 단어와 단어 수

를 별도의 변수로 관리하는 것보다 통합하여 관리하는 것이 더 편리하다. 서로 연관되어 있는 여

러 개의 항목(또는 변수)을 조직화하여 새로운 자료형을 정의한 것을 구조체(structure)라고 한다.

구조체를 사용하려면 다음과 같은 순서를 따라야 한다.

1. 구조체 정의: 사용자가 정의하는 구조체 자료형이 어떻게 구성되는지 정의한다.

2. 구조체 변수 선언: 정의한 구조체에 대한 변수를 선언한다.

3. 구조체 변수 사용: 일반 변수와 마찬가지 방법으로 프로그램 문장에서 구조체 변수를 사

용한다.

성적을 처리하기 위한 학생 정보를 예로 들어 구조체를 정의하고, 구조체 변수를 선언하고, 구

조체 변수를 사용하는 방법을 알아보자. 영어, 수학 과목에 대한 성적을 관리한다면, 다음과 같은

변수들이 필요하다.

char name[20]; // 학생 이름

char ID[20]; // 학번

int english; // 영어 성적

int math; // 수학 성적

구조체를 정의하는 방법은 다음과 같다.

Page 186: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

184

struct 구조체이름 // 사용자가 구조체 이름을 정한다.

{ // 구조체 블록 시작

구조체 멤버 목록; // 한 개 이상의 구조체 멤버를 나열한다.

}; // 구조체 끝. 세미콜론으로 구조체 정의를 종료한다.

struct는 구조체를 정의한다는 의미를 가진 C 언어의 예약어(reserved word)이다. 구조체 이름은

사용자가 임의로 정한다. 구초제 멤버는 {자료형, 변수}로 구성된다. 자료형은 C 언어가 제공하는

int, char, float, double 등과 같은 기본 자료형이다. 학생 정보를 구조체로 표현하면 다음과 같다.

struct STUDENT_INFO // 구조체 이름

{

char name[20]; // 학생 이름

char ID[20]; // 학번

int english; // 영어 성적

int math; // 수학 성적

};

네 개의 구조체 멤버로 구성된 학생 정보를 하나의 자료형으로 묶을 수 있다. 구조체는 사용자가

정의하는 자료형이다. 따라서, 구조체를 정의하였다고 해서 변수가 만들어지는 것이 아니다. 구조

체를 정의한다는 것은 새로운 자료형을 만든 것이다. 구조체를 정의한 후, 구조체 자료형을

“struct 구조체이름”으로 참조한다.

구조체를 정의한 후, 구조체에 대한 변수를 선언한다. 변수를 선언할 때 {자료형, 변수이름}을

명시하는 것과 같은 방법으로 구조체 변수를 선언한다. 구조체 변수를 선언하는 방법은 다음과

같다.

struct 구조체이름 변수이름; // “struct 구조체이름”이 자료형에 해당한다.

구조체 변수를 선언할 때, 구조체 변수를 저장하기 위한 기억장소가 할당되고 이후에 구조체 변

수를 사용할 수 있다. 구조체에 대하여 단일 변수, 배열, 그리고 포인터도 선언할 수 있다. 다음은

학생 정보에 대한 구조체 변수를 선언한 예이다.

struct STUDENT_INFO chulsoo; // 단일 변수 선언

struct STUDENT_INFO class[30]; // 구조체 배열 선언

struct STUDENT_INFO *sptr; // 구조체 포인터 선언

구조체 변수의 기억 장소의 크기는 구조체 멤버들의 기억장소 크기의 합과 같다. 단일 변수를 선

언한 경우, 구조체 멤버들로 구성된 구조체 변수 하나가 생성되고, 구조체 배열을 선언한 경우,

배열의 크기만큼 구조체 변수들이 생성된다. 구조체 포인터를 선언하면, 주소를 저장할 변수가 만

들어지고, 향후 포인터를 사용할 때 포인터 변수에 할당되는 주소에 구조체 변수가 존재한다.

Page 187: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

185

구조체에 대하여 변수를 선언한 후, 프로그램 문장에서 구조체 변수를 사용할 수 있다. 구조체

변수를 사용한다는 것은 궁극적으로 구조체 멤버를 참조(reference)하는 것이다. 구조체 변수를 통

하여 구조체 멤버를 참조하는 방법은 구조체 변수일 때와 구조체 포인터일 때가 다르다. 구조체

멤버를 참조하는 방법은 다음과 같다.

구조체변수.멤버변수 // 변수일 때는 점(.) 연산자를 사용한다.

구조체포인터->멤버변수 // 포인터일 때는 화살표(->) 연산자를 사용한다.

구조체 멤버는 일반 변수처럼 사용될 수 있다. 구조체 포인터를 사용하는 방법에 대한 설명은 뒤

로 미룬다. 다음은 프로그램에서 학생 정보 구조체 변수와 구조체 배열을 사용하는 예이다.

scanf(“%s”, chulsoo.name); // chulsoo.nam[]에 문자열을 입력 받는다.

chulsoo.english = 90; // chulsoo.english에 90을 저장한다.

printf(“%s”, class[0].ID); // class[0].ID를 출력한다.

class[1].math = 85; // class[1].math에 85를 저장한다.

프로그램에서 구초제 변수를 직접 사용할 수 있다. 구조체 변수는 구조체의 멤버 전체를 의미

한다. 예를 들면, 다음과 같은 문장이 가능하다.

class[0] = chulsoo; // chulsoo의 모든 멤버를 class[0]에 복사한다.

이 문장은 다음과 같은 프로그램과 효과가 같다.

for (n=0; n<20; n++) class[0].name[n] = chulsoo.name[n];

for (n=0; n<20; n++) class[0].ID[n] = chulsoo.ID[n];

class[0].english = chulsoo.english;

class[0].math = chulsoo.math;

9.3.3 자료구조 설계

데이터를 저장할 자료구조를 설계해 보자. 텍스트 파일에 데이터가 몇 개나 존재할지 모르기

때문에 이러한 유형의 프로그램에서 데이터를 연결 리스트(linked list)에 저장하는 것이 가장 적합

하다. 그렇지만, 아직 연결 리스트에 대하여 학습하지 않았으므로, 일단 배열에 데이터를 저장하

기로 한다.

배열에 데이터를 저장하려면, 텍스트 파일이 포함하고 있는 단어의 최대수를 가정해야 한다. 일

단 1000개로 가정한다. 이 문제에서 단어와 단어 수가 서로 관련이 있는 데이터이므로 구조체로

만들어 사용하기로 한다. 단어 정보를 저장하기 위한 자료 구조를 다음과 같이 정한다.

#define MAXWORD 1000

struct WORDCOUNT // 한 개의 단어를 저장할 구조체 선언

{

Page 188: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

186

char *str; // 단어를 저장할 문자형 포인터

int count; // 단어가 나타난 횟수

};

struct WORDCOUNT words[MAXWORD]; // 단어를 저장할 배열 선언

int nword; // 단어 수

<그림 9-9> 단어 세기 자료 구조 (nword=0)

그림 9-9는 자료구조의 초기 모습이다. 단어를 저장할 포인터 str에 NULL이 할당되어 있고, 각

단어의 수 count의 값은 0이다. 컴퓨터는 파일을 순차적으로 액세스(sequential access) 하므로, 단

어 세기 프로그램은 텍스트를 처음부터 끝까지 읽어 가면서 단어를 분리한 후 한 단어씩 처리해

야 한다. 문제가 단어를 오름차순으로 정렬하는 것을 요구하고 있으므로, 단어를 읽으면서 바로

정렬하여 저장하도록 하자. 그림 9-10은 텍스트 파일에서 “When I find myself in times of trouble

…”를 읽어 데이터를 처리하는 과정을 보여준다.

그림 9-10(a): 첫 번째 단어 “When”를 분리한다. words[] 배열에 데이터가 하나도 없으므로,

words[0]에 “When”를 소문자로 변환하여 “when”를 저장한다.

그림 9-10(b): 다음 단어는 “I”이다. 이 단어는 알파벳 순서로 “when”보다 배열의 앞에 저장되

어야 하므로, words[0]을 words[1]로 옮기고, 소문자로 바꾸고 “i"를 words[0]에 저장한다.

그림 9-10(c): 다음 단어는 “find”이다. 이 단어는 알파벳 순서로 “i” 앞에 저장되어야 한다. 배

열에 저장되어 있는 “i”와 “when”을 한 칸씩 아래로 옮기고, “find”를 words[0]에 저장한다.

그림 9-10(d): 다음 단어는 “myself”이다. 이 단어는 알페벳 순서로 “i" 다음에 저장되어야 한

다. 배열에 저장되어 있는 “when”을 한 칸 아래로 옮기고, “myself”를 words[2]에 저장한다.

그림 3(c): 네 번째 단어는 “a”이고, 이 단어는 배열의 맨 처음에 들어가야 한다.

그림 3(d): 다섯 번째 단어는 “popular”이고, 이 단어는 “is” 다음에 그리고 “programming” 전

에 저장되어야 한다.

단어를 처리하는 과정에서 배열에 저장되어 있는 단어와 동일한 단어가 나타나면, 단어 수를 증

가시킨다.

Page 189: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

187

<그림 9-10> 단어 처리 과정

프로그램 설계로 넘어가기 전에 크기가 큰 배열을 전역변수 또는 지역변수로 선언하여 사용하

는 방법에 대하여 생각해 보자.

프로그램에서 변수를 전역 변수로 선언하면, 실행 파일이 디스크에 저장될 때 디스크 공간을

차지한다. 따라서, 크기가 큰 배열을 사용하는 경우에 배열을 전역 변수로 선언하면 디스크가 낭

비된다. 크기가 큰 배열을 전역 변수로 선언하지 않고, 전역 변수로 배열의 포인터만 선언하고 실

행할 때 기억장치를 할당하여 사용함으로써 이런 문제점을 해결할 수 있다.

크기가 큰 배열을 함수의 지역 변수로 사용할 때도 문제점이 있다. 지역 변수는 지역 변수를

선언한 함수가 실행될 때, 컴퓨터가 운영하는 기억장치 중에서 스택(stack)이란 곳에 할당된다. 컴

퓨터는 스택에 많은 기억장치를 배정하지 않기 때문에 스택은 한정된 자원이다. 그러므로, 크기가

큰 변수를 지역 변수로 선언하면 스택이 부족해질 가능성이 있다., 따라서, 크기가 큰 변수를 지

역 변수로 사용할 때도, 배열의 포인터만 선언하고 실행할 때 기억장치를 할당하여 사용하는 것

이 좋다.

단어 정렬 문제에서 단어를 저장하기 위하여 구조체 배열이 필요하고, 배열의 최대 수를 1,000

개로 가정하였다. 따라서 전역 변수로 배열을 선언하는 것 대신에 다음과 같이 구조체 포인터를

선언하고 배열을 할당하여 사용하기로 한다.

struct WORDCOUNT *words; // 단어를 저장할 배열 선언

Page 190: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

188

구조체 포인터에 배열을 할당한 후 일반 배열과 같은 방법으로 사용할 수 있다.

9.3.4 프로그램 구조 설계

단어 세기 프로그램의 전체적인 구조를 다음과 같이 설계할 수 있다. 이 중에서 단어를 처리하

는 핵심 부분은 라인 2이다.

<리스트 9-18> 문자 세기 프로그램

1. 구조체 배열을 할당하고 초기화 한다.

2. 파일에서 단어를 추출하여 words[]에 저장한다.

3. words[]에 저장되어 있는 단어와 횟수를 출력한다.

4. 단어를 저장할 때 동적으로 할당 받은 기억 장소를 해제한다.

이 프로그램도 각 라인을 별도의 함수로 구현해 보자. 우선 프로그램을 다음과 같이 구현한다.

<리스트 9-19> 단어 세기 프로그램 구조

1. #include <stdio.h>

2. #define MAXWORD 1000 // 파일에 들어 있는 단어의 최대 수

3. void intialize(); // 함수 선언

4. void read_file(); // 함수 선언

5. void print_words(); // 함수 선언

6. void deallocate(); // 함수 선언

7. struct WORDCOUNT // 단어를 저장할 구조체 선언

8. {

9. char *str; // 단어를 저장할 문자형 포인터

10. int count; // 단어가 나타난 횟수

11. };

12. struct WORDCOUNT *words; // 단어를 저장할 배열 (포인터로 선언)

13. int nwords; // 배열에 저장되어 있는 단어 수

14. void main()

15. {

16. intialize(); // 구조제 배열을 할당하고 초기화 한다.

17. read_file(); // 파일에서 단어를 추출하여 words[]에 저장한다.

18. print_words(); // words[]에 저장되어 있는 단어와 횟수를 출력한다.

19. deallocate(); // 동적으로 할당 받은 기억장소를 해제한다.

20. }

21. void intialize() {} // 향후 구현

Page 191: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

189

22. void read_file() {} // 향후 구현

23. void print_words() {} // 향후 구현

24. void deallocate() {} // 향후 구현

프로그램을 컴파일하여 구조에 오류가 없음을 확인하고, 프로그램을 다음과 같은 순서로 구현

한다.

9.3.5 기억장치 할당 및 해제: 구조체 배열을 할당하고 해제하는 함수를 먼저 구현한다.

9.3.6 단어 추출 및 저장: 파일에서 단어를 추출하는 알고리즘을 기술하고, 기능을 파일 읽기, 단

어 추출, 소문자 변환, 배열 탐색, 배열 이동, 단어 저장으로 세분화한다. 각 기능을 별도의 절에서

다룬다.

9.3.13 단어 출력: 마지막으로 배열에 저장된 단어와 나타난 횟수를 출력한다.

9.3.5 기억장소 할당 및 해제

초기화 함수와 할당된 기억 장소를 해제하는 함수를 구현해 보자. 초기화 함수는 다음과 같은

동작을 수행하여야 한다.

<리스트 9-20> 초기화 함수의 동작

1. struct WORDCOUNT *words에 1,000 개의 구조체 배열을 동적으로 할당하고,

2. 할당 받은 기억 장소 전체를 0으로 초기화 한다.

3. 단어 수 nwords를 0으로 초기화한다.

동적으로 기억장치를 할당 받기 위하여 malloc() 함수를 사용한다. 동적으로 할당 받은 기억 장소

는 임의의 값이 들어 있으므로, 0으로 초기화 한 후에 사용해야 한다. 할당 받은 기억 장소를 해

제하는 함수는 할당 받은 기억장소만 해제한다. 프로그램의 구현은 다음과 같다.

<리스트 9-21> 기억장치 할당 및 해제

1. #include <stdlib.h> // malloc(), free() 선언 선언

2. #include <string.h> // memset() 함수 선언

3. void intialize()

4. {

5. words = (struct WORDCOUNT *)malloc(sizeof(struct WORDCOUNT)*MAXWORD);

6. memset(words, 0, sizeof(struct WORDCOUNT)*MAXWORD);

7. nwords = 0; // 저장된 단어 수 = 0

8. }

9. void deallocate()

10. {

11. free(words);

Page 192: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

190

12. }

라인 5: 구조체 WORDCOUNT를 MAXWORD 개 할당한다. malloc() 함수가 void 형 포인터를

리턴하므로, 데이터 형을 변환하였다.

라인 6: 할당 받은 기억장소를 모두 0으로 초기화한다.

라인 11: 라인 5에서 할당 받은 기억 공간을 해제한다.

일반적으로 배열을 할당하고 초기화하는 문장은 라인 5, 6의 패턴을 갖고 있으므로, 이 형태를

익히도록 하자.

9.3.6 단어 추출 및 저장

이 문제의 핵심인 파일에서 단어를 추출하고 구조체 배열에 저장하는 read_file() 함수를 설계해

보자. “9.3.3 자료구조 설계”에서 구조체 배열을 운영하는 방법을 설명한 바 있다. 이에 의하면,

read_file()의 흐름을 다음과 같이 구체화할 수 있다.

<리스트 9-22> 단어 추출 및 저장 과정

1. 파일을 연다.

2. 파일의 끝까지 다음 과정을 반복한다.

3. 파일에서 단어를 추출한다.

4. 구조체 배열(words[])에서 단어를 찾는다.

5. 만일 단어가 존재한다면,

6. 단어가 나타난 횟수(count)를 증가시킨다.

7. 만일 단어가 존재하지 않는다면,

8. 만일 단어들을 한 칸씩 밀어야 한다면

9. 추가할 단어 이후의 단어들을 한 칸씩 뒤로 옮긴다.

10. 단어를 저장하고, 단어의 수(nword)를 증가시킨다.

11. 파일을 닫는다.

파일을 열고 읽는 라인 1, 2, 11은 앞에서 이미 학습한 바 있다. 나머지 텍스트 파일에서 단어를

추출하는 방법(라인 4), 배열에서 단어를 찾는 방법(라인 5), 배열을 이동하는 방법(라인 9), 그리

고 단어를 저장하는 방법(라인 10)에 대하여 좀 더 생각해 보아야 한다. 다음과 같은 순서로 설계

하고 구현해 보자.

9.3.7 파일 읽기: 파일을 열고 텍스트 파일을 읽고 그대로 출력한다.

9.3.8 단어 분리: 텍스트 파일을 라인 단위로 읽고 단어를 분리하여 출력한다.

9.3.9 소문자 변환: 분리한 단어를 소문자로 변환한다.

9.3.10 배열 탐색: 단어를 저장할 배열의 위치를 찾는다.

9.3.11 배열 이동: 단어가 배열의 중간에 삽입될 경우, 삽입 위치 이후의 배열 원소를 뒤로 옮긴다.

9.3.12 단어 저장: 삽입 위치에 단어를 저장한다.

Page 193: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

191

9.3.7 파일 읽기

파일을 읽어 출력하는 부분은 이미 앞에서 작성해 보았다. 파일에서 한 개의 라인씩 읽고, 화면

에 출력하는 기능을 리스트 9-23과 같이 구현할 수 있다. 프로그램을 실행하기 전에 입력 파일로

사용할 텍스트 파일을 메모장(notepad)으로 만들어, 소스 파일이 있는 디렉토리에 저장한다. 파일

의 이름을 popsong.txt로 정한다. 프로그램을 빌드하여 실행해 보면 그림 9-11의 결과를 얻을 수

있다.

<리스트 9-22> 파일 읽기 구현

1. void read_file()

2. {

3. char buffer[256]; // 한 개의 라인을 읽을 버퍼

4. FILE *fp = fopen("popsong.txt", "r"); // 읽기 모드로 파일 열기

5. while (fgets(buffer, 255, fp) != NULL) // 한 줄을 읽었으면

6. {

7. printf("%s", buffer); // 그 줄을 출력한다.

8. }

9. fclose(fp); // 파일을 닫는다.

10. }

<그림 9-11> 파일 읽기 실행 결과

9.3.9 단어 분리

파일 읽는 부분이 정상적으로 동작한다는 것을 확인하였으므로, 여기에 단어를 추출하는 기능

을 추가해 보자. 여러 개의 단어로 구성되어 있는 문자열에서 단어를 분리하는 방법은 strtok()(스

트링 토큰)이라는 라이브러리 함수로 간단하게 구현할 수 있다.

char *strtok(char *str, const char *delemeters)

파라미터

Page 194: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

192

char *str: 분리할 문자열. 이 문자열의 내용이 변경되므로 const 한정자가 없다.

char *delemeters: 문자열을 단어 분리할 문자들의 포함하고 있는 문자열

리턴

문자열에서 구한 마지막 토큰에 대한 포인터

만일 남아 있는 토큰이 없다면 null 포인터를 리턴한다.

토큰이란 더 이상 나눌 수 없는 문장의 기본 요소를 의미한다. 이 문제에서 토큰은 문장에서

분리한 단어이다. 이 함수를 사용한 예는 리스트 9-23이다. 라인 7이 분리자(deletmeter)로 “공백,

콤마, 마침표, 하이픈”로 지정하여 strtok() 함수를 호출하여 첫 번째 토큰을 분리한 예이고, 라인

11이 연속적으로 다음 토큰을 분리할 때 strtok()을 호출하는 방법의 예이다. 리스트 9-23의 라인

7~12는 토큰 분리의 전형적인 패턴이므로 잘 알아 두자. 실행 결과도 함께 제시한다.

<리스트 9-23> 토큰 분리 프로그램의 예

1. #include <stdio.h>

2. #include <string.h>

3. int main ()

4. {

5. char str[] ="- This, a sample string."; // 토큰으로 분리할 문자열

6. char *token; // 분리된 토큰을 저장할 포인터

7. token = strtok (str, " ,.-"); // 스트링 토큰 함수 호출

8. while (pch != NULL) // 남아 있는 토큰이 있다면

9. {

10. printf ("%s\n", token); // 추출된 토큰을 출력한다.

11. token = strtok (NULL, " ,.-"); // 다음 토큰을 추출한다.

12. }

13. return 0;

14. }

This

a

sample

string

리스트 9-22의 라인 7을 다음과 같이 토큰 분리 기능으로 대치하면, 단어를 추출할 수 있다. 프

로그램의 실행 결과는 그림 9-12이다.

<리스트 9-24> 단어 분리

1. char *token; // read_file() 함수 안에 지역 변수를 추가한다.

Page 195: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

193

2. while (fgets(buffer, 255, fp) != NULL)

3. {

4. //printf("%s", buffer); // 리스트 9-22의 라인 7을 다음과 같이 교체한다.

5. token = strtok(buffer, " ,.!?\t\n"); // 첫 번째 토큰 분리

6. while (token != NULL) // 남아 있는 토큰이 있다면

7. {

8. printf("%s\n", token); // 추출된 단어를 출력한다.

9. token = strtok(NULL, " ,.!?\t\n"); // 다음 토큰을 추출한다.

10. }

11. fclose();

12. }

라인 5: 토큰을 분리하기 위한 분리자(delimeter)로 {공백, 느낌표(!), 물음표(?), 콤마(,), 마침표

(.), 탭(\t), 다음 줄(\n) }을 지정하였다.

<그림 9-12> 단어 분리 결과

9.3.9 소문자 변환

텍스트 파일에서 단어를 분리하였으나, 단어를 소문자로 변환하여 구조체 배열에 저장해야 한

다. 영문자를 변환하는 라이브러리 함수는 다음과 같다.

#include <ctype.h>

Page 196: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

194

int tolower(int c;) // 영문자 c가 대문자이면 소문자로 변환한다.

int toupper(int c); // 영문자 c가 소문자이면 대문자로 변환한다.

리스트 9-24에서 단어는 변수 token에 저장되어 있다. 그러므로 다음과 같은 함수를 작성하여

단어에 속한 문자를 모두 소문자로 변환할 수 있다.

void convert_lower(char *str)

파라미터

char *str: 단어를 소문자로 변환한다.

리턴

없음

conver_lower() 함수는 분리된 단어에 포함된 알파벳을 소문자로 변환하는 기능을 수행한다. 이

함수는 리스트 9-25와 같이 구현할 수 있다.

<리스트 9-25> convert_lower(0 함수 구현)

1. #include <ctype.h> // tolower() 함수 정의 추가

2. void convert_lower(char *str); // 함수 선언 추가

3. void read_file()

4. {

5. ….

6. convert_lower(token): // 함수 호출 추가

7. printf("%s\n", token); // 리스트 6-24의 라인 8

8. token = strtok(NULL, " ,.!?\t\n"); // 리스트 6-24의 라인 9

9. }

10. void convert_lower(char *str)

11. {

12. while (*str != NULL) // 문자열의 끝이 아니면

13. {

14. *str = tolower(*str); // 소문자로 변환한다.

15. str++; // 다음 문자를 처리한다.

16. }

17. }

라인 7: 리스트 9-24의 토큰 분리 후, 출력 전에 소문자로 변환하는 함수를 호출한다.

라인 12, 15: 문자열을 처음부터 끝까지 탐색하는 전형적인 방법이다.

라인 15: 포인터를 증가시키면, 배열의 다음 원소를 가리킨다.

Page 197: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

195

프로그램을 실행하면, 그림 9-12와 비슷한 결과를 볼 수 있고, 자세히 보면 단어가 모두 소문자

로 출력되는 것을 확인할 수 있다.

9.3.10 배열 탐색

배열의 단어가 알파벳 순서로 정렬되어 있으므로 배열에서 단어를 찾는 방법은 탐색 문제에 해

당한다. 개념이 쉬운 선형탐색 알고리즘을 사용해 보자. “8.4.7 선형탐색”의 정수 선형탐색 알고리

즘은 정수형 배열에서 키(key)를 찾으면 배열의 인덱스를 리턴하고, 찾지 못하면 -1을 리턴한다.

그러나, 이 경우는 다음과 같이 두 가지를 리턴하도록 선형탐색 알고리즘을 수정하여야 한다.

키로 주어진 문자열(단어)를 찾았는지 찾지 못했는지 나타내는 값

단어가 저장되어 있는 위치 또는 단어를 저장할 위치(배열의 인덱스)

따라서, 선형 탐색 함수를 다음과 같이 선언한다.

int linear_search(char *key, int *found)

파라미터

char *key: 검색할 단어

int *found: 단어가 존재하는지 간접적으로 리턴하기 위한 변수

단어가 존재하면 TURE를 리턴하고, 존재하지 않으면 FALSE를 리턴한다.

리턴

단어가 존재할 때, 단어가 저장되어 있는 배열의 인덱스

단어가 존재하지 않을 때, 단어를 삽입할 배열의 인덱스

이와 같은 수정 사항을 반영한 선형 탐색 알고리즘은 다음과 같다.

<리스트 9-26> 단어 선형탐색 알고리즘

7. *found = FALSE로 가정한다.

8. n=0부터 n<nword까지

9. 만일 key = words[n].word 이면

10. *found = TRUE;

11. 단어를 찾았으므로, 반복문을 벗어난다.

12. 만일 key < words[n].word 이면

13. 더 이상 탐색할 필요가 없으므로, 반복문을 벗어난다.

14. n을 리턴한다.

라인 1, 4: 변수 found가 포인터로 전달되므로, 간접적으로 값을 전달한다.

라인 8: 단어를 찾았다면 n의 값이 단어가 저장되어 있는 배열의 주소이고, 단어를 찾지 못했

다면 n의 값이 단어가 저장된 배열의 다음 인덱스이다.

linear_search() 함수의 구현은 리스트 9-27이다. 정수 비교 대신에 strcmp() 함수를 사용하였고,

단어를 찾았는지 여부를 변수 found를 통하여 간접적으로 리턴한다.

Page 198: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

196

<리스트 9-27> linear_search() 함수 구현

1. int linear_search(char *key, int *found)

2. {

3. int n, compare;

4. *found = 0; // 찾지 못했다고 가정한다.

5. for (n=0; n<nword; n++) // 배열의 처음부터 배열의 단어 수까지

6. {

7. compare = strcmp(key, words[n].str); // 문자열을 비교한다.

8. if (compare == 0) // 찾았다면

9. {

10. *found = 1; // 단어를 찾았다고 표시하고

11. break; // 루프를 벗어난다.

12. }

13. if (compare < 0) // 배열의 단어가 키 보다 크면

14. {

15. break; // 루프를 벗어난다.

16. }

17. }

18. return n; // 배열의 인덱스를 리턴한다.

19. }

단어를 탐색하는 함수를 구현한 후, 리스트 9-22의 단어 추출 및 저장 과정을 참고하여 이 탐

색 함수를 호출하도록 read_file() 함수를 다음과 같이 수정한다.

<리스트 9-28> read_file() 함수 수정

1. int linear_search(char *key, int *found); // 함수 선언 추가

2. void read_file()

3. {

4. …

5. int found, index; // 변수를 추가한다.

6. …

7. convert_lower(token); // 단어를 소문자로 변환한 후에

8. // printf(“%s\n”, token); // 단어 출력을 지우고

9. index = linear_search(token, &found); // 탐색 함수를 호출한다.

10. token = strtok(NULL, “ ,.!?\t\n”); // 다음 토큰을 추출한다.

11. …

12. }

Page 199: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

197

라인 8: 단어를 출력할 필요가 없으므로 주석으로 처리한다.

라인 9: 단어를 소문자로 변환한 후에 단어를 탐색한다.

프로그램을 실행하더라도, 아직 구조체 배열에 단어가 저장되어 있지 않기 때문에 탐색 함수는

항상 단어를 발견하지 못할 것이다.

9.3.11 배열 이동

리스트 9-22의 단어 추출 및 저장 과정을 참고하면, 단어를 탐색한 후 수행할 일은 다음과 같

다.

1. 만일 단어가 존재한다면,

2. 단어가 나타난 횟수(count)를 증가시킨다.

3. 만일 단어가 존재하지 않는다면,

4. 만일 단어들을 한 칸씩 밀어야 한다면

5. 추가할 단어 이후의 단어들을 한 칸씩 뒤로 옮긴다.

6. 단어를 저장하고, 단어의 수(nword)를 증가시킨다.

라인 4의 조건은 추가할 단어의 위치와 단어 수를 비교(index < nword)하여 결정할 수 있다. 단

어가 존재하지 않을 때 배열에 저장되어 있는 단어들을 한 칸씩 뒤로 옮기는 방법을 생각해 보자.

탐색 함수가 리턴한 index는 단어가 삽입될 배열의 위치를 가리키고 있고, index는 단어 수인

nword보다 작은 값이다. 단어가 저장되어 있는 words[] 배열에 현재까지 nword 개의 단어가 저

장되어 있고, index 이후의 데이터를 뒤로 한 칸씩 옮겨야 한다. 이 알고리즘을 구현하는 함수를

다음과 같이 정의한다.

void move_downward(int index)

파라미터

int index: 단어를 저장한 배열의 index부터 배열에 저장된 단어 수(nword)까지 한 칸씩

아래로 이동한다.

리턴

없음

단어를 옮길 때 배열의 마지막부터 한 칸씩 옮겨야 한다. 만일 앞 부분부터 다음 칸으로 옮긴

다면, 데이터를 잃어버리기 때문이다. 단어를 한 칸씩 아래로 옮기는 알고리즘은 다음과 같다. 그

리고 단어를 옮긴 후 배열에 저장되는 단어의 수가 배열의 크기보다 적다는 것을 확인할 필요가

있다.

<리스트 9-29> words[]배열 데이터 이동 알고리즘

1. 만일 배열 words[]의 크기를 초과하지 않는다면

2. n=nword부터 n=index까지 n을 감소시키면서

3. words[n+1]에 words[n]을 저장한다.

Page 200: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

198

move_downward() 함수의 구현은 다음과 같다.

<리스트 9-30> move_downward() 함수 구현

1. void move_downward(int index)

2. {

3. int n;

4. if (nword < MAXWORD-1) // 단어 수가 배열의 최대 수를 초과하지 않으면

5. {

6. for (n=nword; n>=index; n--) // 마지막 단어부터

7. {

8. words[n+1].str = words[n].str; // 단어를 옮긴다.

9. words[n+1].count = words[n].count; // 단어 수를 옮긴다.

10. // words[n+1] = words[n]; // 구조체 멤버를 한 번에 옮긴다.

11. }

12. }

13. }

라인 4: 현재까지 배열에 저장되어 있는 단어 수(nword)가 배열의 크기보다 작다는 것을 확

인한다.

라인 6: 배열에 저장되어 있는 마지막 단어부터 옮긴다.

라인 8, 9는 구조체 멤버를 하나씩 옮기는 문장이다. 구조체 변수의 이름을 직접 사용하면, 구조

체 멤버 전체를 지칭한다. 그러므로 라인 10은 구조체 멤버 전체를 옮기는 문장이다. 즉, 라인 10

의 문장은 라인 8, 9의 두 개의 문장과 효과가 동일하다.

이제 read_file() 함수에 단어를 탐색한 후 처리 절차를 추가해 보자.

<리스트 9-31> read_file() 함수 수정

1. void move_downward(int index); // 함수 선언 추가

2. void read_file()

3. {

4. …

5. convert_lower(token); // 단어를 소문자로 변환한 후에

6. // printf(“%s\n”, token); // 단어 출력을 지우고

7. index = linear_search(token, &found); // 탐색 함수를 호출한다.

8. if (found == 1) // 단어를 찾았다면

9. words[index].count += 1; // 횟수를 증가시킨다.

10. else // 단어를 찾지 못했다면

11. {

Page 201: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

199

12. if (index < nword) // 단어들을 옮겨야 한다면

13. movd_downword(index); // index 이후의 단어를 아래로 옮기고

14. // index에 단어를 추가한다.

15. }

16. token = strtok(NULL, “ ,.!?\t\n”); // 다음 토큰을 추출한다.

17. …

18. }

단어를 탐색하는 함수를 호출한 후, 라인 8~14를 추가한다. 라인 14는 단어를 배열에 저장하는

함수를 호출할 부분이다.

9.3.12 단어 저장

마지막으로 새로 찾은 단어를 구조체 배열의 words[index]에 저장하는 방법을 생각해 보자. 이

함수를 다음과 같이 정의한다.

void insert_data(int index, char *str)

파라미터

int index: 새로운 단어를 저장할 배열의 인덱스

char *str: 배열에 저장할 단어

리턴

없음

저장할 문자열을 파라미터 char *str로 받는다. 이 방법은 앞에서 다룬 “9.2.4 단어 입력”에서 문

자형 포인터에 단어를 저장하는 방법과 동일하다.

<리스트 9-32> 단어(char *str)를 구조체 배열(words[index]에 저장하는 과정

1. 단어(char *str)의 크기를 구한다.

2. Words[index].str에 단어를 저장할 기억 공간을 할당한다.

3. 할당 받은 기억 공간에 문자열(char *str)을 복사한다.

4. 구조체 배열의 단어 수(words[index].count)를 1로 설정한다.

5. 단어 수(nword)를 증가시킨다.

이 함수의 구현은 다음과 같다.

<리스트 9-33> insert_data() 함수 구현

1. void insert_data(int index, char *str) // 함수 선언 추가

2. void read_file()

3. {

4. …

5. insert_data(index, token); // 리스트 9-21 라인 13을 함수 호출로 변경

Page 202: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

200

6. …

7. }

8. void insert_data(int index, char *str)

9. {

10. int size; // 단어의 문자 수

11. size = strlen(str) + 1; // 단어의 문자수를 구하고

12. words[index].str = (char *)malloc(size); // 기억장소를 할당하고

13. strcpy(words[index].str, str); // 단어를 복사한다.

14. words[index].count = 1; // 단어 횟수를 1로 설정한다.

15. nword += 1; // 나타난 단어 수를 증가시킨다.

16. }

리스트 9-33의 라인 12에서 단어를 추가하기 위하여 기억 공간을 할당하였으므로, 프로그램이

종료될 때 이 공간도 해제되어야 한다. 따라서, deallocate() 함수를 다음과 같이 수정한다.

<리스트 9-34> deallocate() 함수

1. void deallocate()

2. {

3. int n;

4. for (n=0; n<nword; n++) // 저장되어 있는 단어에 대하여

5. if (words[n].str != NULL) // 기억장소가 할당되었다면

6. free(words[n].str); // 단어를 저장한 기억장소를 해제한다.

7. free(words); // 구조체 배열을 해제한다.

8. }

9.3.13 단어 출력

이제 read_file() 함수가 완성되었다. 마지막으로 리스트 9-35와 같이 구조체 배열의 원소를 출

력하는 print_words() 함수를 구현한다. 프로그램 수행 결과는 화면 9-13과 같다.

<리스트 9-35> print_words() 함수 구현

1. void print_words()

2. {

3. int n;

4. for (n=0; n<nword; n++)

5. printf("%3d. %-18s: %d\n", n+1, words[n].str, words[n].count);

6. }

라인 5: printf() 함수의 형식 변환기호 %-18s는 문자열을 출력할 때 전체 18 개의 공간을 확

Page 203: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

201

보하고 왼쪽 맞춤하여 출력하라는 의미이다.

[과제] 리스트 9-27의 선형탐색 알고리즘을 이진탐색 알고리즘으로 교체하라.

<그림 9-13> 단어 세기 프로그램 실행 결과

9.4 요약

이 장에서는 문자 와 문자열 처리를 주로 다루었다. 이 장에서 다룬 문제들은 문자로 구성된

문서를 다루는 웹 브라우저와 같은 프로그램의 기초 기술에 해당한다. 이 장에서 새로 소개된 프

로그래밍 기법은 다음과 같다.

컴퓨터는 문자를 코드로 표현하며, 영문자는 아스키 코드로 표현한다.

컴퓨터는 영문자를 char 형으로 저장하며, 실제로 저장되는 값은 아스키 코드 값이다.

텍스트 파일을 읽을 때 한 문자씩 읽거나 한 라인씩 읽을 수 있다.

포인터 변수에 문자열을 저장하려면 다음과 같은 과정을 거쳐야 한다.

문자열의 길이를 구한다.

포인터 변수에 문자열을 저장할 기억 장소를 할당한다.

할당 받은 기억 장소로 문자열을 복사한다.

동적으로 기억 장소를 할당하였다면 프로그램이 종료되기 전에 해제하여야 한다.

문자열을 정렬하거나 정렬되어 있는 문자열 배열에서 문자를 탐색하는 방법은 정수에 대한

정렬 및 탐색 알고리즘과 같다. 다만, 크기를 비교할 때, 문자열을 사전 순으로 비교하는 함

수를 사용해야 한다.

Page 204: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

202

구조체는 서로 연관되어 있는 데이터를 조직화하여 하나의 자료형으로 정의하는 것이다.

다음과 같은 순서에 의하여 구조체를 사용한다.

사용자 정의 구조체를 정의한다.

구조체 변수를 선언한다.

구조체 변수를 사용한다.

구조체에 대하여 단일 변수, 배열, 포인터 변수를 선언할 수 있다.

구조체 변수의 멤버를 지정하기 위하여 점(.) 연산자를 사용한다.

구조체 변수의 이름은 구조체 멤버 전체를 의미한다.

배열의 크기가 큰 경우에 배열로 선언하지 않고, 포인터를 선언한 후 기억장소를 할당하여

사용하는 것이 좋은 프로그래밍 방법이다.

동적으로 할당한 기억 장소는 초기화 되어 있지 않다. 따라서, 필요시 초기화 한 후 사용하여

야 한다.

이 장에서는 주로 텍스트 파일을 조작하는 함수와 문자 또는 문자열을 처리하는 라이브러리 함

수들이 새로 소개되었다.

int fgetc(FILE *stream) // 파일에서 한 개의 문자를 읽는다.

int feof(FILE *stream) // 파일의 끝에 도달하였는지 검사한다.

int putchar(int character) // 표준출력장치로 문자를 출력한다.

int isalpha(int c) // 파라미터 c가 알파벳 문자이면 0이 아닌 값을 리턴한다.

int isupper(int c) // 파라미터 c가 알파벳 대문자이면 0이 아닌 값을 리턴한다.

int islower(int c) // 파라미터 c가 알파벳 소문자이면 0이 아닌 값을 리턴한다.

char *fgets(char *str, int num, FILE *stream) // 파일에서 한 라인을 읽는다.

int puts(const char *str) // 표준출력장치로 문자열을 출력하고 다음 줄(‘\n’)을 추가한다.

int strcmp(const char*str1, const char str2) // 문자열을 비교한다.

size_t strlen(const char *str) // 문자열의 길이를 구한다.

char *strcpy(char *destination, const char *source) // 문자열을 복사한다.

char *strtok(char *str, const char *delemeters) // 문자열에서 토큰을 분리한다.

int tolower(int c;) // 영문자 c가 대문자이면 소문자로 변환한다.

int toupper(int c); // 영문자 c가 소문자이면 대문자로 변환한다.

Page 205: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

203

제10장 게임

게임도 프로그램의 훌륭한 응용 분야이고, 처음 프로그램을 접하는 학생들은 게임 프로그램에

관심을 많이 갖고 있다. 콘솔 응용 프로그램은 텍스트 만으로 화면을 처리해야 하기 때문에, 그래

픽이 화려한 게임 프로그램을 개발할 수 없지만, 몇 가지 간단한 게임 프로그램을 통하여 프로그

래밍 기술을 학습해 보자. 이 장에서는 다음과 같은 게임 프로그램을 다룬다.

1. 기억력 게임: 화면에 임의로 나타나는 문자를 맞춘다. 게임을 진행할수록 화면에 나타나는 문

자가 많아진다.

2. 문자열 회전: 광고판과 같이 화면에 출력된 문자열이 일정 시간 간격으로 회전한다. 사용자가

키보르를 누르면, 회전 방향을 변경한다.

3. 타자연습 게임: 일정 시간 간격으로 화면의 한 줄에 임의의 영문자가 하나씩 나타나고, 영문

자는 아래로 스크롤 한다. 사용자가 화면에 나타난 영문자를 맞추면, 해당 영문자는 화면에서

제거된다. 영문자가 화면의 맨 아래에 닿으면 게임을 종료한다.

이 장에서 새로 소개하는 C 언어 문법은 많지 않지만, 다음과 같은 새로운 프로그래밍 기술을

많이 소개한다.

시간 측정 및 프로그램 지연(delay) 기술

사용자가 키 보드를 눌렀는지 검사하는 방법

화면의 임의의 위치에 출력하는 방법

키보드의 기능 키(function key) 검출 방법

프로시저 구동 프로그램과 이벤트 구동 프로그램의 차이점

상태도에 의한 이벤트 처리 기술

프로그램의 소스 파일을 여러 개의 파일로 분리하는 방법

10.1 기억력 게임(Memory Game)

화면에 새로 표시되는 영문자를 맞추는 게임 프로그램을 작성하라. 처음에 화면에는 아무 것도

표시되지 않는다. 게임을 진행하면서, A부터 Z까지 중에서 새로운 영문자 한 개가 화면의 임의의

위치에 추가되어 나타난다. 사용자는 새로 표시된 영문자를 맞춘다. 사용자가 새로 나타난 영문자

를 입력하면, 화면이 모두 지워졌다가 1초 후에 화면의 임의의 위치에 새로 영문자를 한 개 추가

한 화면이 다시 나타난다. 사용자가 새로 추가된 영문자를 맞추면 점수가 1점씩 증가하고, 사용자

가 새로 추가된 영문자를 맞추지 못한다면 게임을 종료한다.

10.1.1 요구사항 분석

그림 7-1은 게임 진행 화면의 예이다. 화면에 임의의 위치에 J가 표시된다. 사용자가 J를 입력

하면, 화면은 1초간 모두 지워졌다가 새로운 영문자 N을 추가한 화면을 제시한다. 사용자가 N을

입력하면, 화면은 1초간 지워졌다가 다시 새로운 영문자 X를 추가한 화면을 제시한다. 이와 같은

과정을 반복하면서 사용자는 새로 추가된 영문자를 맞춘다. 사용자가 새로 나타난 영문자를 맞추

Page 206: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

204

지 못한다면 게임을 종료한다.

<그림 10-1> 게임 진행 화면

프로그램의 화면을 설계해 보자. 콘솔 응용 프로그램의 화면의 크기는 영문자 가로 80, 세로

25이다. 마지막 라인의 마지막 칸에 글자를 표시하면, 화면 전체가 스크롤되어 글자가 표시된 위

치가 변한다. 그러므로 그림 10-2와 같이 마지막 라인을 제외한 영역에 영문자를 표시하기로 하

고, 맨 아래 라인은 게임 안내문과 점수를 출력하는 용도로 사용하기로 한다. 점수는 사용자가 새

로 나타난 영문자를 맞출 때마다 1점씩 증가한다.

<그림 10-2> 화면 설계

Page 207: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

205

10.1.2 자료구조 설계

화면에 출력할 데이터를 저장하는 자료구조를 생각해 보자. 영문자를 표시할 화면 영역의 크기

가 가로 80 문자, 세로 24 문자이므로, 다음과 같은 이차원 배열에 영문자를 저장하도록 한다.

char screen[24][80]; // y 좌표와 x 좌표에 영문자를 저장한다.

지금까지 우리는 배열의 원소가 선형적으로 배치되는 일차원 배열만 사용하였다. 이차원 배열

은 배열의 원소가 평면적으로 배치되어 있는 자료구조이다. 위와 같은 배열은 그림 10-3과 같이

원소의 수가 80 개인 일차원 배열이 24 개 연속적으로 배치되어 있는 것으로 생각할 수 있다. 배

열의 인덱스는 가로가 0부터 79까지 80 개이고, 세로가 0부터 23까지 24 개이다. 이 문제에서 배

열의 인덱스는 화면의 y 좌표와 x 좌표에 해당한다. 프로그래밍 언어에서는 임의의 차원의 배열

이 가능하다.

<그림 10-3> screen[24][90] 배열의 구조

화면에 표시할 데이터를 저장할 자료구조 외에 게임을 진행하기 위하여 필요한 자료구조를 다

음과 같이 정한다.

char keyin; // 사용자가 입력한 문자

char alphabet; // 새로 생성한 문자

int score; // 게임 점수

10.1.3 프로그램 구조 설계

요구사항에 주어진 기억력 게임의 흐름을 가상 코드로 리스트 10-1과 같이 표현할 수 있고, 라

인 5에 해당하는 게임의 진행 과정은 리스트 10-2와 같이 표현할 수 있다.

<리스트 10-1> 기억력 게임의 구조

1. 프로그램을 초기화 한다.

2. 다음 과정을 반복한다.

3. 게임을 시작한다는 안내문을 출력한다.

4. 사용자가 ENTER를 입력하였다면,

5. 게임을 진행한다. (리스트 10-2)

Page 208: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

206

6. 사용자가 ESC를 입력하였다면,

7. 프로그램을 중단한다.

<리스트 10-2> 게임 진행

1. 점수를 0으로 만들고 화면을 초기화 한다.

2. 다음 과정을 반복한다.

3. 영문자 표시 영역을 지운다.

4. 1초간 대기한다.

5. 임의의 영어 대문자 한 개를 생성한다.

6. 화면을 갱신한다.

7. 사용자로부터 키보드에서 문자 한 개를 입력 받는다.

8. 입력된 키 값이 화면에 새로 표시된 영문자와 같으면,

9. 점수를 갱신하고 게임을 계속 진행한다.

10. 입력된 키 값이 화면에 새로 표시된 영문자와 다르면,

11. 게임을 중단한다.

이 프로그램을 구현하기 위하여, 다음과 같은 기능이 필요하다.

리스트 10-1 라인 3: 안내문을 출력하거나 영문자를 화면에 표시하기 위하여 화면의 임의의

위치에 문자 또는 문자열을 표시할 수 있어야 한다. 또한, 이와 함께 커서(cursor)를 화면에서

보이지 않도록 제거하여야 한다.

리스트 7-2 라인 4: 시간 지연 기능을 사용하여 1초 동안 대기하는 기능이 구현되어야 한다.

리스트 10-3 라인 5: 영문자를 하나씩 생성하여야 한다. 단, 새로 생성되는 영문자는 이전에

존재하던 영문자와 다른 위치에 표시되어야 한다.

프로그램을 다음과 같은 순서로 구현해 보자

10.1.4 화면 출력 및 초기화: 커서를 지우고 화면의 임의의 위치에 문자와 문자열을 출력한다.

10.1.5 영문자 생성 및 표시: 임의의 위치에 알파벳을 생성하여 화면에 표시한다.

10.1.6 시간 지연: 화면을 1초 단위로 깜빡인다.

10.1.7 게임 진행: 게임을 흐름을 구현한다.

10.1.4 화면 출력 및 초기화

화면의 임의의 위치에 출력하는 방법과 커서를 제거하는 방법을 알아보자. 이 기능은 PC의 콘

솔 프로그램에 한정되어 있는 기능이기 때문에, 표준 C 라이브러리는 이 두 가지 기능을 제공하

지 않는다. 그렇지만 윈도우 운영체제가 제공하는 기능을 사용할 수 있다. 이 두 가지 함수의 구

현은 리스트 10-3과 같다. 일단 함수의 세부 동작을 이해하려 하지 말고, 기능만 알고 그대로 사

용하자.

<리스트 10-3> 콘솔 화면 제어 함수

Page 209: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

207

1. #include <windows.h>

2. // 커서를 화면의 x 좌표, y 좌표로 옮긴다.

3. void gotoxy(int x, int y)

4. {

5. COORD Pos = { x, y };

6. SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), Pos);

7. }

8. // 화면에서 커서를 제거한다.

9. void cursor_off()

10. {

11. CONSOLE_CURSOR_INFO Coff = { 100, 0 };

12. SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &Coff);

13. }

화면 출력 방법 이외에 문자를 입력 받는 방법도 달라져야 한다. 그 동안 우리는 문자를 입력

할 때 scanf(“%c”, &ch) 혹은 getchar() 함수를 사용하였다. 이 함수들은 사용자가 키를 입력하면

컴퓨터가 운영하는 시스템 버퍼에 저장하였다가 사용자가 엔터 키를 입력할 때 버퍼에서 데이터

를 읽는다. 또한 이 함수들은 사용자가 입력한 키를 화면으로 다시 출력한다. 이 기능을 에코

(echo)라고 한다. 그런데, 기억력 게임 프로그램에서는 사용자 입력을 에코 하지 않아야 한다. 따

라서 이 입력 함수들을 사용할 수 없다. 윈도우는 환경에서 시스템 버퍼를 사용하지 않으면서 문

자를 입출력하는 함수는 다음과 같이 두 가지가 있다. 기억력 게임 프로그램에서 문자를 입출력

하기 위하여 이 함수들을 사용할 것이다.

#include <conio.h>

int _getch(void) // 버퍼를 사용하지 않고, 에코도 하지 않고 문자를 입력한다.

int _putch(int c) // 버퍼를 사용하지 않고 화면에 문자를 출력한다.

이 프로그램은 난수 생성기를 사용한다. 따라서 초기화 과정에서는 다음과 같은 두 가지 일을

수행하면 된다.

<리스트 10-4> 게임 초기화

1. 난수 생성기를 초기화 한다.

2. 커서를 제거한다.

화면의 임의의 위치에 출력하는 방법과 사용자의 입력을 받는 방법을 알았으므로, 리스트 10-1

의 main() 함수와 초기화를 수행하는 함수를 구현해 보자.

<리스트 10-5> 기억력 게임 main() 함수 구현

1. #include <stdio.h>

Page 210: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

208

2. #include <conio.h> // _getch()와 _putch() 함수 선언

3. #include <time.h> // time() 함수 선언

4. #include <windows.h> // SetConsoleCursorPosition() 함수 선언

5. #define ESC 0x1b // ESC의 ASCII 코드

6. void initailize(); // 함수 선언

7. void gotoxy(int, int); // 함수 선언

8. void cursor_off(); // 함수 선언

9. void main()

10. {

11. char ch = 0;

12. initailize(); // 게임 초기화

13. while (ch != ESC)

14. {

15. gotoxy(0, 24); // 커서를 마지막 라인으로 옮기고, 안내문을 출력한다.

16. printf("게임을 시작하려면 엔터, 종료하려면 ESC를 누르세요.");

17. ch = _getch(); // 키를 입력한다.

18. }

19. }

20. void initailize()

21. {

22. srand(time(NULL)); // 난수 발생기를 초기화 한다.

23. cursor_off(); // 커서를 지운다.

24. }

25. void gotoxy(int x, int y) { … } // 리스트 7-3과 동일

26. void cursor_off() { … } // 리스트 7-3과 동일

라인 5: 이 프로그램에서 사용할 제어 문자에 대한 ASCII 코드 값을 정의한다.

라인 15, 16: 커서를 화면의 맨 아래 줄로 옮기고 게임 안내문을 출력한다.

라인 17: 화면으로 에코 하지 않는 _getch() 함수로 사용자의 입력을 받는다.

<그림 10-4> 게임 초기 화면

Page 211: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

209

아직 게임의 흐름 제어만 구현하였기 때문에, 자료구조 설계에서 결정한 변수도 선언하지 않았

다. 프로그램을 실행하면 그림 10-4와 같이 화면의 맨 아래 라인에 게임 안내문이 출력되는 것을

볼 수 있다. 키 값을 입력해도 화면에 입력한 키가 에코되지 않음을 확인하자. ESC 키를 입력하면

프로그램이 종료된다.

10.1.5 영문자 생성 및 표시

영문자를 생성하는 방법을 알아보자. 영문자와 함께, 영문자를 표시할 화면의 x, y 좌표도 생성

하여야 한다. 단, 생성된 좌표에 영문자가 저장되어 있지 않아야 한다. 이 함수를 다음과 같이 정

의한다.

char get_alphabet()

파라미터: 없음

리턴

난수 발생기로 영문자와 좌표와 영문자를 생성한다.

영문자를 표시할 좌표에 생성한 문자를 저장하고, 생성한 영문자를 리턴한다.

get_alpahbet() 함수는 생성된 문자를 리턴하는데, 게임 프로그램은 이 문자와 사용자가 입력한

문자를 비교하는 용도로 사용할 것이다. 이 함수의 처리 과정과 구현은 다음과 같다.

<리스트 10-6> get_alphabet() 함수

1. 다음을 반복한다.

2. 좌표 x에 0부터 79까지 난수를 생성한다.

3. 좌표 y에 0부터 24까지 난수를 생성한다.

4. 만일 screen[y][x]에 영문자가 이미 저장되어 있다면, 라인 1부터 다시 반복한다.

5. ‘A’부터 ‘Z’까지 영문자를 하나 생성하여 screen[y][x]에 저장한다.

6. 생성된 문자를 리턴한다.

<리스트 10-7> get_alphabet() 함수 구현

1. char get_alphabet(0; // 함수 선언 추가

2. char screen[24][80]; // 함수 선언 이후에 전역 변수로 선언한다.

3. char get_alphabet() // 함수 구현

4. {

5. int y, x;

6. char alpha;

7. do {

8. x = rand() % 80; // x 좌표를 생성한다.

9. y = rand() % 24; // y 좌표를 생성한다.

10. } while (screen[y][x] != 0); // 만일 화면 (x, y)에 문자가 있다면 다시 생성한다.

Page 212: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

210

11. alpha = (rand()%26) + 'A'; // ‘A’부터 ‘Z’까지 문자를 하나 생성한다.

12. screen[y][x] = alpha; // 생성한 문자를 screen[][]에 저장한다.

13. return alpha; // 생성된 문자를 리턴한다.

14. }

라인 6~9: do { 블록 } while (조건); 반복문이다. 이 제어구조는 먼저 블록을 실행하고 나서 조

건을 검사한다. 만일 조건이 참이면 반복문을 계속 실행한다.

라인 10: 0~25 범위의 난수를 생성하고 문자형 상수 ‘A’를 더하여 영문자 ‘A’~’Z’를 생성한다.

생성한 영문자를 화면에 표시해야 하므로, 영문자를 출력하는 display_screen() 함수도 함께 설

계해 보자. 이 함수는 파라미터와 리턴 값이 없으며, 처리 과정과 구현은 다음과 같다.

<리스트 10-8> 화면 출력 함수

1. y=0부터 y<24까지

2. x=0부터 x<80까지

3. 만일 screen[y][x]가 0이 아니면

4. 커서를 (x, y)로 이동하고

5. screen[y][x]를 출력한다.

<리스트 10-9> display_screen() 함수 구현

1. void display_screen(); // 함수 선언 추가

2. void display_screen() // 함수 구현

3. {

4. int y, x;

5. for (y=0; y<24; y++) // y 축 좌표에 대하여

6. for (x=0; x<80; x++) // x 축 좌표에 대하여

7. if (screen[y][x] != 0) // 만일 화면의 (x, y)에 문자가 있으면

8. {

9. gotoxy(x,y); // 커서를 옮기고

10. _putch(screen[y][x]); // 문자를 화면에 출력한다.

11. }

12. }

지금까지 구현한 것을 테스트할 수 있도록 main() 함수의 while() 문 내부를 다음과 같이 수정

한다.

<리스트 10-10> 문자 생성 및 표시 테스트

1. void main()

2. {

Page 213: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

211

3. …

4. while (ch != ESC)

5. {

6. gotoxy(0, 24); // 커서를 마지막 라인으로 옮기고, 안내문을 출력한다.

7. printf("게임을 시작하려면 엔터, 종료하려면 ESC를 누르세요.");

8. get_alphabet(); // 문자를 생성한다.

9. display_screen(); // 화면에 출력한다.

10. ch = _getch(); // 키를 입력한다.

11. }

12. }

테스트 용으로 라인 8과 9를 추가한다. 나중에 이 두 라인을 지울 것이다. 프로그램을 실행해

보면 사용자가 ESC 이외의 키를 누를 때마다 그림 10-5와 같이 문자가 하나씩 추가되는 것을 확

인할 수 있다.

<그림 10-5> 문자 생성 및 표시

10.1.6 시간 지연

시간을 지연하는 기능에 대하여 알아보자. 컴퓨터는 주기적으로 시간을 측정하는 기능을 갖고

있는데, 이 기능을 클럭 틱(clock tick)이라고 한다. C 언어는 클럭 틱을 읽는 clock() 라이브러리

함수를 제공한다. 이 함수는 다음과 같이 정의되어 있다.

Page 214: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

212

#include <time.h>

clock_t clock(void) // typedef long clock_t

파라미터: 없음.

리턴

함수를 호출하였을 때, 시간을 클럭 틱 단위로 리턴한다.

이 함수가 리턴하는 clock_t 자료형은 long을 재정의 한 것이며, long 형과 같다고 생각해도 무방

하다. 1 초에 생성되는 클럭 틱은 time.h 안에 전역 변수인 CLOCKS_PER_SEC로 정의되어 있다. PC

의 경우, 이 값이 1,000이다. 즉, PC에서는 1 밀리초마다 클럭 틱이 발생한다. 이 함수를 사용하여,

시간을 밀리초 단위로 지연하는 sleep() 함수를 다음과 같이 설계하고 구현할 수 있다.

<리스트 10-11> 밀리초 단위의 시간 지연 함수

1. 현재 시간에 대기 시간을 더하여 목표 시간을 설정한다.

2. 현재 시간이 목표시간보다 작으면 계속 대기한다.

<리스트 10-11> sleep() 함수 구현

1. void sleep(long wait); // 함수 선언 추가

2. void sleep(long wait) // 파라미터로 밀리초를 받는다.

3. {

4. clock_t goal;

5. goal = wait + clock(); // 목표시간을 현재시간 + 지연시간으로 설정한다.

6. while (goal > clock()) ; // 현재시간이 목표시간에 도달할 때까지 대기한다.

7. }

라인 4: sleep(1000)으로 호출하였다면, 목표시간은 현재 시간 + 1초이다.

라인 5: 현재시간이 목표시간 보다 적으면, 계속 시간을 읽고 대기한다. 이 반복문은 블록에

해당하는 부분이 비어 있다. 그러므로 문장의 마지막에 세미콜론(;)을 붙여야 한다.

시간 지연 기능을 테스트하기 위하여 main() 함수를 다음과 같이 수정하고, 화면을 지우는

clear_screen() 함수도 구현한다. 화면을 지우는 방법은 display_screen() 함수와 같고, 다만

_putch(screen[y][x])로 문자를 출력하는 대신에 _putch(‘ ‘)로 공백을 출력하면 된다.

<리스트 10-12> 시간 지연 테스트

1. void clear_screen(); // 함수 선언 추가

2. void main()

3. {

4. …

5. while (ch != ESC)

Page 215: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

213

6. {

7. …

8. ch = _getch(); // 키를 입력한다. 이 라인 다음에 두 줄을 추가한다.

9. clear_screen(); // 화면을 지우고

10. sleep(1000); // 1 초간 대기한다.

11. }

12. }

13. void clear_screen() // 함수 구현

14. // display_screen()과 같고, 리스트 7-9의 라인 10을 _putch(‘ ‘)로 수정한다.

라인 9와 10도 테스트 용으로 추가한 것이므로 나중에 지울 예정이다. 프로그램을 실행하면, 사용

자가 ESC 이외의 키를 입력할 때마다 화면이 1초간 지워졌다가 새로운 문자가 추가되어 화면에

출력되는 것을 볼 수 있다.

10.1.7 게임 진행

마지막으로 게임을 진행하는 play_game() 함수를 구현한다. 먼저 main() 함수를 원래대로 복구

하고, 게임을 진행하기 위한 코드를 다음과 같이 추가한다.

<리스트 10-13> main() 함수 수정

1. #define ENTER 0x0d // 엔터 키에 대한 ASCII 코드 정의 추가

2. void play_game(); // 함수 선언 추가

3. void main()

4. {

5. char ch = 0;

6. initialize();

7. while (ch != ESC)

8. {

9. gotoxy(0, 24);

10. printf("게임을 시작하려면 엔터, 종료하려면 ESC를 누르세요.");

11. ch = _getch(); // 키를 입력한다.

12. if (ch == ENTER) // 엔터키를 입력하였다면

13. {

14. gotoxy(0, 24); // 안내문을 지운다.

15. printf("점수: ");

16. play_game(); // 게임을 시작한다.

16. }

17. }

Page 216: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

214

18. }

19. void playgme() {} // 구현할 함수

라인 15: 빈 공간을 길게 출력하여 게임 안내문을 지운다.

게임을 진행하는 알고리즘은 리스트 10-2이다. 이것을 다시 보면서, 구현할 방법을 생각해 보자.

<리스트 10-14> 게임 진행 알고리즘

1. 점수를 0으로 만들고 화면을 초기화 한다. // screen[][] 초기화

2. 다음 과정을 반복한다. // do 반복문 시작

3. 영문자 표시 영역을 지운다. // clear_screen() 호출

4. 1초간 대기한다. // sleep(1000) 호출

5. 임의의 영어 대문자 한 개를 생성한다. // get_alphabet() 호출

6. 화면을 갱신한다. // display_screen() 호출

7. 사용자로부터 키보드에서 문자 한 개를 입력 받는다. // _getch() 호출

8. 입력된 키 값이 화면에 새로 표시된 영문자와 같으면,

9. 점수를 갱신하고 게임을 계속 진행한다. // 반복문 계속 실행

10. 입력된 키 값이 화면에 새로 표시된 영문자와 다르면, // 반복문 중단

11. 게임을 중단한다.

라인 1에서 이차원 배열을 초기화 하는 것 이외의 기능을 거의 구현하였다. 이 기능을 함수로

만들면 다음과 같다.

<리스트 10-15> 배열 초기화

1. void clear_array()

2. {

3. int x, y;

4. for (y=0; y<24; y++)

5. for (x=0; x<80; x++)

6. screen[y][x] = 0; // 배열을 0으로 초기화 한다.

7. }

이차원 배열이므로 이중 루프로 초기화 한다. 또는 배열을 지우는 함수를 만들지 않고, memset()

함수를 사용하여 다음과 같이 초기화 할 수도 있다.

memset(screen, 0, sizeof(char)*24*80);

play_game() 함수의 구현은 다음과 같다. 당연히 프로그램의 헤드 부분에 clear_array() 함수를

선언해 주어야 한다. 기억력 게임 프로그램을 완성하였다.

Page 217: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

215

<리스트 10-16> 게임 진행 구현

1. void play_game()

2. {

3. int score; // 점수 계산을 위한 변수 추가

4. char keyin, alphabet; // 지역 변수 추가

5. score = 0; // 점수 초기화

6. clear_array(); // screen[][] 배열 초기화

7. do {

8. clear_screen(); // 화면을 지우고

9. sleep(1000); // 1 초(1,000 밀리초)간 대기한다.

10. alphabet = get_alphabet(); // 임의의 영문자를 생성한다.

11. display_screen(); // 화면에 출력한다.

12. keyin = _getch(); // 영문자를 입력 받는다.

13. keyin = toupper(keyin); // 영문자를 대문자로 변환한다.

14. if (kein == alphabet);

15. {

16. score += 1; // 점수를 증가시키고

17. gotoxy(7, 24); // 점수 표시 위치로 커서를 옮기고

18. printf("%2d", score); // 점수를 표시한다.

19. }

20. } while (keyin == alphabet); // 입력 값이 같으면 반복한다.

21. }

라인 13: 사용자가 소문자를 입력할 수도 있기 때문에, 대문자로 변환한다.

10.2 문자열 회전

“C Programming is easy.”란 문자열을 광고판처럼 0.5 초에 한 문자씩 회전하여 출력하는 프로

그램을 작성하라. 처음에는 왼쪽으로 회전하다가, 사용자가 키보드의 공백(SPACE) 키를 입력할 때

마다 문자열의 회전 방향이 변경되어야 한다. 사용자가 ESC 키를 입력하면 프로그램을 종료한다.

화면의 중앙에 문자열을 표시하라.

10.2.1 요구사항 분석

그림 10-6은 화면에 문자열이 출력되는 모습이다. 처음에 문자열을 화면의 중앙에 표시하고,

0.5 초 후 문자열을 왼쪽으로 회전하여 다시 표시한다. 출력 도중에 사용자가 공백을 입력하면 오

른쪽으로 문자열을 회전하여 출력한다. 사용자가 다시 또 공백을 입력하면 회전 방향을 왼쪽으로

변경한다.

Page 218: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

216

<그림 10-6> 화면 출력

<그림 10-7> 출력 화면 설계

먼저 출력 화면을 설계해 보자. 화면의 중앙에 문자열을 출력하기 위한 좌표를 계산한다. 콘솔

응용 프로그램의 화면의 크기가 영문자 가로 80, 세로 25이므로, 문자열을 화면의 중앙에 출력할

좌표를 다음과 같이 구할 수 있다. 그림 10-7과 같이 화면의 {x, y} 좌표에 문자열을 표시한다.

x 좌표 = (80 – 문자열 길이)/2, y 좌표 = 12

10.2.2 자료구조 설계

이 프로그램은 처리할 데이터가 고정되어 있다. 그러므로, 문자열 변수를 “C Programming is

easy.”로 초기화하여 사용하기로 한다. 문자열을 초기화 하는 방법은 다음과 같이 세 가지 방법이

있다. 어느 방법을 사용해도 좋다.

char *str = “C Programming is easy.”; // 포인터를 사용하는 방법

그림 7-8(a)와 같이 문자형 포인터 변수를 선언한다.

컴파일러는 기억장치 어딘가에 문자열 상수를 배치한 후, 포인터를 문자열의 시작 주소

를 가리키도록 초기화 한다.

char str[25] = “C Programming is easy.”; // 크기를 고정한 배열을 사용하는 방법

str 이름으로 크기가 25인 문자형 배열을 만든다.

Page 219: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

217

배열의 값을 그림 7-8(b)와 같이 문자열을 초기화 한다.

문자열의 문자 수보다 크도록 배열의 크기를 정해야 한다.

char str[] = “C Programming is easy.”; // 크기를 정하지 않은 배열을 사용하는 방법

배열을 선언하고 초기화 할 때 배열의 크기를 정하지 않는다.

컴파일러는 그림 7-8(c)와 같이 문자열 상수의 길이를 계산해서 문자열 상수의 크기에 알

맞게 배열의 크기를 정한다.

<그림 10-8> 문자열 초기화 방법

자료구조 설계 단계에서 변수를 선정하는 작업 이외에 활용 방법도 고려해야 한다. 문자열을

회전하는 방법을 생각해 보자. 다음과 같이 두 가지 방법이 있을 수 있다.

문자열 자체를 회전시켜 저장하고, 문자열을 전체를 한 번에 출력하는 방법

문자열은 그대로 두고, 문자열을 출력할 때 시작 위치를 변경하고 한 문자씩 출력하는 방법

첫 번째 방법은 문자열 str = “C programming is easy.”을 str = “ programming is easy.C”과 같이

변경하고, 문자열을 그대로 출력하는 방법이다. 문자열의 길이를 length라고 하면, 문자열을 왼쪽

으로 회전하는 과정을 다음과 같은 알고리즘으로 설계할 수 있다. 이 방법을 사용한다면, 오른쪽

으로 회전하는 기능을 별도로 만들어야 한다.

<리스트 10-17> 문자열을 왼쪽으로 회전하는 방법

1. temp = str[0] // 첫 번째 문자를 보관한다.

2. n=1부터 n=length까지

3. str[n-1] = str[n] // 나머지 문자들을 한 칸씩 앞으로 옮긴다.

4. str[length-1] = temp // 첫 번째 문자를 문자열의 마지막에 저장한다.

5. 문자열 전체를 출력한다.

두 번째 방법은 문자열(문자형 배열)에서 첫 번째로 표시할 배열의 인덱스만 변경하면서 출력하는

방법이다. 첫 번째로 표시할 문자의 위치를 pos라고 한다면, 이 방법을 다음과 같이 설계할 수 있

다. 이 방법을 사용한다면, 단지 pos의 위치를 증가 또는 감소시킴으로써 문자열을 회전하여 출력

할 수 있다.

Page 220: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

218

<리스트 10-18> 문자열의 표시 위치를 변경하는 방법

1. n=0부터 n=length까지

2. index = (pos + n) % length // 표시할 문자의 위치를 정한다.

3. str[index]를 표시한다. // 문자 한 개를 출력한다.

위에서 설명한 두 가지 방법 중에서, 두 번째 방법이 더 간단하다. 그러므로, 우리는 두 번째 방

법을 사용하기로 한다. 이 방법을 사용하면 문자열을 회전하는 방향을 변경하는 방법도 간단해진

다. 회전 방향을 나타내는 변수를 dir이라고 하고, 사용자가 공백을 입력할 때마다 dir의 값을

1(왼쪽 회전)과 -1(오른쪽 회전)의 값을 번갈아 갖도록 만든다. 문자열을 출력할 첫 번째 문자의

위치를 표시하는 pos의 값을 pos+dir로 변경함으로써 회전 방향을 표시할 수 있다.

문자열을 저장하고 회전하는 변수들 이외에 시간을 측정하고 사용자가 입력을 읽어 저장할 변

수도 필요하다. 그러므로, 필요한 변수들을 다음과 같이 정리할 수 있다.

char *str = “C programming is easy.”: 문자열

int dx, dy: 문자열을 표시할 화면의 좌표

int length: 문자열의 길이

int dir = 1: 문자열 회전 방향. 1은 왼쪽 회전, -1은 오른쪽 회전

int pos = 0: 첫 번째로 화면에 출력할 문자열의 위치

clock_t refresh_time = 0: 화면 갱신 시간을 저장할 변수

char ch: 사용자가 키보드를 눌렀을 때, 키 값을 읽어 저장할 변수

10.2.3 프로그램 구조 설계

이 프로그램의 핵심은 화면에 문자열을 표시함과 동시에 키보드에서 문자열을 읽는 것이다. 그

동안 우리가 학습해 왔던 scanf() 또는 _getch()와 같은 입력 함수들은 사용자가 데이터를 입력할

때까지 대기한다. 즉, 프로그램이 입력 함수를 호출한 위치에 정지해 있다가 사용자가 데이터를

입력한 후에 데이터를 받고 다시 프로그램이 동작한다. 이와 같은 입력 함수만 사용한다면 문자

열 회전 프로그램을 작성할 수 없다.

PC 계열에 사용되는 컴파일러는 사용자가 키보드를 눌렀는지 검사하여 참 또는 거짓을 리턴하

는 라이브러리 함수를 제공한다. 이 함수를 사용한다면, 사용자가 키를 입력할 때까지 기다리지

않으면서 사용자의 입력을 처리할 수 있다.

요구사항을 고려하여 프로그램의 전체적인 흐름을 다음과 같이 설계한다.

<리스트 10-19> 문자열 회전 프로그램의 구조

1. 문자열을 표시할 위치 {x, y} 좌표를 구한다.

2. 다음 과정을 반복한다.

3. 화면에 문자열을 출력한 후 0.5 초가 지났다면

4. {x, y} 좌표에 문자열을 출력한다.

Page 221: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

219

5. 회전 방향에 따라 문자열을 왼쪽 혹은 오른쪽으로 회전한다.

6. 사용자가 키보드를 눌렀다면,

7. 문자를 읽는다.

8. 만일 사용자가 공백을 눌렀다면

9. 회전 방향을 변경한다.

10. 사용자가 ESC를 입력하였다면, 프로그램을 중단한다.

이 프로그램에서 라인 3과 라인 6이 중요하다. 이 프로그램은 시간과 사용자의 입력 두 가지 이

벤트의 발생을 검출하고 그것에 적합한 동작을 실행한다. 다음과 같은 순서로 프로그램을 설계하

고 구현해 보자.

10.2.4 문자열 표시: 화면의 중앙에 문자열을 표시한다.

10.2.5 문자열 회전: 문자열의 임의의 위치부터 문자들을 출력한다.

10.2.6 시간 측정: 0.5초 간격으로 문자열을 왼쪽으로 회전하여 출력한다.

10.2.7 회전 방향 변경: 사용자로부터 키 입력을 받고 회전 방향을 변경한다.

10.2.4 문자열 표시

처음으로 구현한 프로그램은 다음과 같다. 커서를 임의의 위치로 옮기기 위하여 gotoxy() 함수

를 사용하였다. 이 함수는 기억력 게임에서 사용한 것과 동일하므로, 소스 코드를 생략하였다. 프

로그램을 실행하여, 문자열이 화면의 중앙에 표시되는 것을 확인해 보자.

<리스트 10-20> 화면의 중앙에 문자열 출력

1. #include <stdio.h>

2. #include <string.h> // strlen() 함수 선언

3. #include <windows.h> // SetConsoleCursorPosition() 함수 선언

4. void gotoxy(int x, int y);

5. void main()

6. {

7. char *str = "C programming is easy."; // 문자열 초기화

8. int length = strlen(str); // 문자열의 길이

9. int dx = (80 - length)/2; // 화면의 x 좌표

10. int dy = 12; // 화면의 y 좌표

11. gotoxy(dx, dy); // 커서를 (x, y)로 이동하고

12. printf("%s\n", str); // 문자열을 출력한다.

13. }

14. void gotoxy(int x, int y) { …. } // 생략

Page 222: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

220

10.2.5 문자열 회전

임의의 위치에서 문자열을 출력하는 기능을 별도의 함수로 만들어 보자. 이 함수를 다음과 같

이 정의한다.

void print_string(int x, int y, char *str, int length, int pos)

파라미터

int x, y: 문자열을 출력할 화면의 좌표

char *str: 출력할 문자열

int length: 문자열의 길이

int pos: 문자열 중에서 처음으로 출력할 문자의 위치

리턴: 없음

print_string() 함수는 화면의 (x, y) 좌표에 pos 번째 문자부터 화면에 문자열을 출력한다. 이 함

수의 구현은 다음과 같다.

<리스트 10-21> 문자열 출력 함수

1. #include <conio.h> // _putch() 함수 정의 추가

2. void print_string(int x, int y, char *str, int length, int pos); // 함수 선언 추가

3. void print_string(int x, int y, char *str, int length, int pos) // 함수 구현

4. {

5. int n, index;

6. gotoxy(x, y); // 커서를 옮긴다.

7. for (n=0; n<length; n++)

8. {

9. index = (pos + n) % length; // 출력할 문자의 인덱스

10. _putch( str[index] ); // 문자 한 개 출력

11. }

12. }

라인 8: 배열의 인덱스는 항상 length보다 작아야 한다. 이 문장은 다음과 같은 문장을 하나

의 문장으로 적은 것이다.

index = pos + n; // index를 다음 위치로 증가시킨다.

if (index > length) // index가 length보다 큰 경우

index = index – length; // index를 0부터 다시 시작하도록 조정한다.

print_string() 함수를 테스트해 보기 위하여, 리스트 10-20의 라인 11과 12를 지우고 다음과 같

이 세 번의 함수 호출을 추가한다. 프로그램을 실행하면, 화면의 중앙에 세 줄이 표시되면서 한

글자씩 회전한 것을 확인할 수 있다.

Page 223: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

221

print_string(dx, dy-1, str, length, 0);

print_string(dx, dy, str, length, 1);

print_string(dx, dy+1, str, length, 2);

10.2.6 시간 측정

우리는 “10.1 기억력 게임”에서 clock() 함수가 현재 시간에 해당하는 클럭 틱을 리턴한다는 것

을 학습한 바 있다. PC에서 클럭 틱은 밀리초 단위이다. 그렇지만, “10.1.6 시간 지연”과 같이

sleep() 함수를 사용할 수 없다. 이 함수는 아무 일도 하기 않고 시간이 경과되기를 기다리기 때

문이다. 따라서, 동적으로 화면을 갱신할 시간이 경과되었는지 계속 확인하여야 한다. 이 알고리

즘은 다음과 같다.

<리스트 10-22> 시간 측정 방법

1. 화면 갱신 시간(refresh time)을 0으로 초기화 한다.

2. 다음 과정을 무한히 반복한다.

3. 만일 현재 시간(clock() 함수가 리턴하는 값)이 화면 갱신 시간보다 크다면

4. 문자열을 출력한다.

5. 화면 갱신 시간을 (현재 시간+0.5 초)로 변경한다.

6. 문자열을 회전하여 출력하도록 문자의 위치를 변경한다.

이 방법에서는 무한 루프(라인 2~5)를 반복할 때마다, 현재 시간을 검사한다(라인 3). 따라서, 프

로그램이 정지되지 않는다. 문자열을 출력하고(라인 4) 나서 화면갱신 시간(refresh time)을 나중에

화면을 다시 갱신할 시간으로 설정함으로써(라인 5) 화면 출력 시간을 제어할 수 있다. 라인 1에

서 화면갱신 시간을 0으로 초기화하기 때문에, 처음에는 바로 문자열을 출력한다.

이제 0.5초 간격으로 문자열을 왼쪽으로 회전하는 기능을 추가해 보자. 리스트 10-20의 main()

함수를 다음과 같이 수정한다.

<리스트 10-23> 문자열 회전

1. #include <time.h> // 헤더 파일 추가

2. #define REFRESH_TIME 500 // 500 msec 정의

3. void main()

4. {

5. … // 변수 선언 이후에 다음과 같이 변수 추가

6. int dir = 1; // 회전 방향 제어 1: 왼쪽 회전, -1: 오른쪽 회전

7. int pos = 0; // 첫 번째로 출력할 문자의 위치

8. clock_t refresh_time = 0; // 화면 갱신 시간

9. for (;;) // 무한 루프, while (1)과 동일

Page 224: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

222

10. {

11. if (clock() > refresh_time) // 만일 (현재 시간>화면 갱신 시간)이면

12. {

13. print_string(dx, dy, str, length, pos); // 문자열을 출력한다.

14. refresh_time = clock() + REFRESH_TIME; // 다음 갱신시간을 설정한다.

15. pos = pos + dir; // 표시 위치를 변경한다.

16. if (pos == length) // 표시 위치가 문자열의 범위를 초과하면

17. pos = 0; // 첫 번째 문자로 되돌린다.

18. if (pos < 0) // 표시 위치가 음수로 바뀌면

19. pos = length–1; // 마지막 문자를 표시하도록 만든다.

20. }

21. // 여기에 키 입력을 처리하는 기능을 추가한다.

22. } // end of for

23. }

라인 15: 문자열의 첫 번째 표시 위치를 정한다. 회전 방향을 나타내는 dir의 값은 1 또는 -1

이다.

라인 16~19: 문자 표시 위치가 문자열의 범위를 벗어나는 경우에 대한 조치이다.

라인 21: 다음 절에서 설명하는 사용자 키 입력을 처리할 루틴을 추가할 예정이다.

여기까지 작성하고 프로그램을 실행해 보면, 문자열이 왼쪽으로 회전하는 것을 확인할 수 있다.

만일 라인 6에서 dir = -1로 초기화 하고 실행한다면, 문자열이 오른쪽으로 회전한다.

10.2.7 회전 방향 변경

마지막으로 사용자의 입력을 받고, 회전 방향을 변경하는 기능을 추가한다. 이 기능을 구현하려

면 사용자가 키를 누른 것을 검출해야 한다. 이 함수는 다음과 같이 정의되어 있다.

#include <conio.h>

int _kbhit(void)

파라미터: 없음

리턴

사용자가 키를 눌렀으면 0이 아닌 값(즉, 참)을 리턴하고,

그렇지 않다면, 0(거짓)을 리턴한다.

이 함수를 사용하여 사용자의 키 입력을 검사하고, 사용자가 공백을 누를 때마다, dir의 값을 1과

-1로 변경함으로써 간단하게 회전 방향을 변경할 수 있다. 회전 방향을 변경하는 알고리즘은 리

스트 7-19의 라인 6~10이다. 이것을 다시 적으면 다음과 같고, 프로그램 구현도 함께 제시한다.

<리스트 10-24> 키 입력 검출 처리

1. 사용자가 키보드를 눌렀다면, // if (_kbhit())

Page 225: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

223

2. 문자를 읽는다. // ch = _getch()

3. 만일 사용자가 공백을 눌렀다면 // if (ch == SPACE)

4. 회전 방향을 변경한다. // dir = 1 or -1

5. 사용자가 ESC를 입력하였다면, 프로그램을 중단한다. // if (ch == ESC) break

<리스트 10-25> 회전 방향 변경

1. #define SPACE 0x20 // 정의 추가, 공백 문자의 ASCII 코드

2. #define ESC 0x1b // 정의 추가, ESC 문자의 ASCII 코드

3. // main() 함수 내부

4. char ch; // main 함수에 변수 추가

5. … // 리스트 10-23의 라인 21에 다음 라인들을 추가한다.

6. if (_kbhit()) // 만일 키보드가 눌려졌다면

7. {

8. ch = _getch(); // 문자를 읽는다.

9. if (ch == SPACE) // 공백이면

10. dir = (dir == 1) ? -1 : 1; // 화전 방향을 변경한다.

11. if (ch == ESC) // ESC 키 이면

12. break; // 무한 루프를 벗어나서 프로그램을 종료한다.

13. }

라인 8: 사용자가 입력한 키를 화면으로 에코 하지 않기 위하여 _getch() 함수로 문자를 읽는

다.

프로그램을 실행해 보면, 공백을 누를 때마다 문자열의 회전 방향이 반대로 변경되는 것을 확

인할 수 있다

.

10.3 타자연습 게임

화면에 나타난 영문자를 맞추는 타자연습 게임을 작성하라. 일정한 시간 간격으로 화면의 첫

번째 줄에 한 개의 새로운 영문자가 표시되고, 화면에 표시된 영문자들이 한 줄씩 아래로 스크롤

된다. 사용자가 키보드에서 화면에 표시된 영문자들 중 하나를 입력하면, 해당 영문자가 화면에서

제거된다. 사용자가 영문자를 맞출 때마다 게임 점수는 1점씩 증가한다. 만일 영문자가 화면의 마

지막 라인에 도달하면 게임을 종료하고 점수를 출력한다. 화면은 처음에 1초 간격으로 갱신되고,

점수가 10점 올라갈 때마다 0.1초씩 감소한다. 화면 갱신 시간은 0.3초까지 감소한다.

Page 226: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

224

10.3.1 요구사항 분석

그림 10-9는 게임 진행 화면의 예시이다. 프로그램이 시작되면, 그림 10-9(a)의 초기 화면을 출

력한다. 사용자가 엔터 키를 입력하면, 게임을 시작한다.

그림 10-9(b)는 게임이 진행되고 있는 상태이다. 일정 시간 간격으로 화면이 아래로 한 줄 스크

롤 하면서 새로운 영문자가 화면의 첫 번째 라인에 표시된다. 한 라인에는 영문자가 하나씩 표시

된다. 사용자가 화면에 표시된 영문자를 입력하면, 해당 영문자는 화면에서 제거된다. 동일한 영

문자가 서로 다른 라인에 표시될 수 있으므로, 영문자는 아래에 있는 라인부터 제거되어야 한다.

그림 10-9(c)와 같이 영문자가 화면의 마지막 라인에 도착하면, 게임이 종료된다. 게임이 종료되면,

그림 10-9(d)와 같이 안내 화면을 출력한다.

<그림 10-9> 타자연습 게임 화면 설계

또 한 가지 요구 조건은 화면 갱신 시간이다. 화면은 처음에 1초 간격으로 갱신되고, 점수가 10

점 올라갈 때마다 화면 갱신 시간이 0.1초씩 감소되어야 한다. 최소 화면 갱신 시간은 0.3 초이다.

10.3.2 이벤트 구동 프로그램

프로그램의 종류는 프로시저 구동(procedure-driven) 프로그램과 이벤트 구동(event-driven) 프

로그램으로 나눌 수 있다.

Page 227: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

225

프로시저 구동 프로그램은 프로그램이 주도하는 순서 또는 절차(procedure)에 따라 프로그램이

진행되는 형태이다. 그 동안 우리가 작성해 왔던 예제 프로그램들은 대부분이 프로시저 구동형

프로그램이다. 예를 들어, 다음과 같은 프로그램을 생각해 보자.

<리스트 10-26> 프로시저 구동 프로그램

1. print(“X를 입력하세요: “);

2. scanf(“%d”, &x);

3. print(“Y를 입력하세요: “);

4. scanf(“%d”, &y);

5. z =x + y;

6. printf(“%d + %d = %d\n”, x, y, z);

사용자는 프로그램이 요구하는 절차에 따라 데이터를 입력할 수 밖에 없다. 이와 같은 형태가 프

로시저 구동 프로그램이다.

“10.2 문자열 회전” 프로그램은 간단하지만 이벤트 구동 프로그램이다. 이 프로그램은 시간 경

과와 사용자의 키 입력을 검출하여, 이벤트가 발생하였다면 해당 이벤트를 처리한다. 이벤트 구동

프로그램의 다른 예는 그림 7-10과 같은 윈도우 프로그램이다. 이 프로그램은 사용자가 x를 입력

할 수도 있고, y를 먼저 입력할 수도 있다. 사용자가 마우스로 = 버튼을 누르면, 그 때 계산하여

결과를 출력한다. 이와 같이 시간 경과, 마우스 클릭, 키보드 입력 등과 같은 이벤트를 검출하고,

발생한 이벤트를 처리하는 프로그램을 이벤트 구동 프로그램이라고 한다.

<그림 7-10> 이벤트 구동 프로그램

이벤트 구동 프로그램의 동작은 상태도(state diagram)로 표현하면 이해하기 쉽다. 상태도는 이

벤트와 상태로 구성된다. 이벤트는 어느 한 순간에 발생하는 사건을 의미하여, 상태는 일정한 시

간 동안 특정한 작업을 수행하는 것을 말한다. 그림 10-11과 같이 이벤트는 시간 축 상에서 한

점으로 표시되고, 상태는 시간 구간으로 표시된다. 이벤트는 결과적으로 상태의 변화를 유발한다.

그림 10-12는 세 개의 상태를 갖는 상태도의 예이다. 상태의 변화를 유발하는 이벤트는 화살표로

표현하고, 특정 상태는 원으로 표현한다. 조그만 까만 원은 상태 변화의 시작과 끝을 나타낸다.

<그림 10-11> 이벤트와 상태

Page 228: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

226

<그림 10-12> 상태도의 예

한 개의 상태는 여러 개의 이벤트를 처리할 수 있다. 한 개의 상태는 이벤트를 대기하고, 이벤

트가 요구하는 작업을 처리한다. 각 상태는 각 이벤트에 대하여 다음과 같은 동작을 수행할 수

있다. 한 개의 상태에 대하여 네 가지 동작이 항상 존재하는 것은 아니다.

진입 동작(entry operation): 현재의 상태로 들어오기 전에 수행해야 할 동작이다.

상태 동작(state operation): 현재 상태에서 발생한 이벤트에 대하여 고유의 동작을 수행한다.

출구 동작(exit operation): 현재 상태를 종료하면서 처리해야 할 동작이다.

상태 변경(state transition): 필요한 경우 다음 상태로 상태를 변경한다.

구현해야 할 동작을 이와 같이 네 가지 형태로 분석하고, 그 결과를 표 10-1과 같은 상태표

(state table)로 정리하는 것이 편리하다. 진입 동작을 제외한 나머지 세 열은 해당 상태에서 처리

할 이벤트와 처리할 동작을 함께 기술한다.

표 10-1 그림 10-12에 대한 상태표

상태 이름 진입 동작 상태 동작 출구 동작 상태 변경

상태 1 이벤트 1: 상태 2

상태 2 이벤트2: 처리 이벤트 3: 상태 3

상태 3 종료

이벤트 구동 프로그램을 알고리즘으로 표현하는 일반적인 패턴이 있다. 이 패턴은 리스트

10-27과 같이 이벤트가 발생하였는지 검사하고 그것을 처리하는 과정으로 구성된다.

<리스트 10-27> 이벤트 구동 프로그램의 구조

1. 다음을 무한히 반복한다.

2. 처리해야 할 이벤트들을 검출한다.

3. 각 상태마다

4. 현재 상태에서 처리해야 할 이벤트가 발생하였다면,

5. 상태 동작: 이벤트에 대하여 수행할 동작을 수행한다.

6. 상태를 변경할 이벤트가 발생하였다면,

7. 출구 동작: 현재 상태를 종료하면서 처리할 동작을 수행하고,

8. 진입 동작: 다음 상태로 진입하기 위하여 필요한 진입 동작을 수행하고

9. 상태 변경: 상태를 변경한다.

Page 229: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

227

라인 3 이후는 각 상태마다 하나씩 만들어야 한다. 이 알고리즘은 상태도를 구현하는 전형적인

프로그래밍 패턴이므로 잘 알아둘 필요가 있다.

10.3.3 프로그램 구조 설계

타자연습 게임 프로그램은 이벤트 구동 프로그램으로 작성하여야 한다. 타자연습 게임은 그림

10-13과 같이 세 개의 상태를 갖는 상태도로 표현할 수 있다.

초기 상태: 프로그램이 시작되면 그림 10-9(a)와 같은 안내문을 출력하고, 초기 상태로 들어간

다. 사용자가 엔터 키를 입력하면, 게임 상태로 이동한다.

게임 상태: 사용자가 영문자를 입력하면 해당 영문자를 처리하면서 계속 게임 상태를 유지한

다. 그림 10-9(c)와 같이 영문자가 마지막 화면의 마지막 라인에 도착하면, 그림 10-9(d)의 안

내 화면을 출력하고 종료 상태로 들어간다.

종료 상태: 사용자가 엔터 키를 입력하면 다시 게임 상태로 이동한다. 사용자가 ESC를 입력

하였다면 프로그램을 종료한다.

<그림 10-13> 프로그램의 상태도

표 10-2. 타자연습 게임의 상태표

상태 진입 동작 상태 동작 상태 변경

초기 상태 초기화면 출력 없음 엔터: 게임 상태로 이동

게임 상태 화면, 점수 초기화

화면갱신 시간 설정

시간: 화면 갱신

문자 입력: 문자 처리

마지막 라인: 종료 상태로 이동

종료 상태 안내문 출력 없음 엔터: 게임 상태로 이동

ESC: 게임 종료

표 10-2는 타자연습 게임의 각 상태에서 수행할 동작 분석하여 자세히 정리한 것이다. 출구 동

작은 없으므로 표에서 제외하였다.

초기 상태: 프로그램이 시작되면, 그림 1(a)의 초기화면을 출력하고 초기 상태로 진입한다. 이

상태에서 처리할 상태 동작은 없다. 만일 사용자가 엔터를 입력한다면, 게임 상태로 이동한다.

게임 상태: 이 상태로 들어오기 전에 화면을 지우고, 점수를 초기화 하고, 화면갱신 시간을

설정한다. 시간이 경과하면 화면을 갱신하고, 사용자가 입력한 문자를 처리하면서 게임을 진

행한다. 마지막 라인에 영문자가 도착한다면, 종료 상태로 이동한다.

Page 230: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

228

종료 상태: 종료 상태로 들어오기 전에 그림 1(d)의 게임 안내문을 출력한다. 이 상태에서 처

리할 상태 동작은 없다. 사용자가 엔터 키를 입력하면 게임 상태로 이동하고, ESC를 입력하면

게임을 종료한다.

다음은 이벤트 구동 프로그램의 패턴에 의하여 타자연습 게임의 처리 과정을 표현한 것이다.

<리스트 10-28> 타자연습 게임 프로그램의 구조

1. 프로그램을 초기화한다. (초기화면 출력, 초기 상태로 변경)

2. 다음을 무한히 반복한다.

3. 사용자가 키보드를 눌렀다면, 문자를 입력한다.

4. 만일 초기 상태이면

5. 입력한 문자가 엔터이면

6. 게임 상태의 진입 동작(화면, 점수 초기화, 화면 갱신 시간 설정)을 수행하고,

7. 게임 상태로 이동한다.

8. 만일 게임 상태이면

9. 화면을 갱신할 시간이 되었다면

10. 화면을 한 라인 아래로 옮기고 새로운 영문자를 추가한다.

11. 입력한 문자가 영문자이면,

12. 해당 영문자를 제거한다.

13. 마지막 라인에 영문자가 도착하였다면,

14. 종료 상태의 진입 동작(종료 안내문 출력)을 수행하고,

15. 종료 상태로 이동한다.

16. 만일 종료 상태이면

17. 입력한 문자가 엔터이면,

18. 게임 상태의 진입 동작(화면, 점수 초기화, 화면 갱신 시간 설정)을 수행하고,

19. 게임 상태로 이동한다.

20. 입력한 문자가 ESC 이면

21. 무한 루프를 벗어나고 프로그램을 종료한다.

라인 1: 프로그램 초기화와 초기 상태의 진입 동작을 수행한다.

라인 2 이후: 프로그램은 흐름 제어는 무한 루프로 구성된다.

라인 3: 각 상태를 처리하기 전에, 프로그램에서 공통적으로 사용하는 이벤트가 발생하였는지

검사하는 기능을 수행한다.

라인 4, 8, 16: 프로그램을 구성하는 상태마다 리스트 10-27의 동작을 수행하도록 설계한다.

각 상태의 동작은 표 10-2로 정리한 상태의 동작을 가상 언어로 표현한 부분이다.

프로그램이 필요로 하는 자료 구조 및 변수 설계와 초기화 과정은 프로그램을 구성하는 기능을

하나씩 구현해 가면서 그 때마다 필요한 부분을 추가해 나가는 것이 좋다. 프로그램을 다음과 같

은 순서로 설계하고 구현해 보자.

Page 231: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

229

10.3.4 소스 파일 분리: 프로그램을 여러 개의 소스 파일로 나누어 작성하는 방법을 설명한다.

10.3.5 프로그램 구조 구현: 프로그램의 전반적인 흐름을 제어하는 상태도를 구현한다.

10.3.6 초기 화면 표시: 게임의 초기 화면을 출력한다.

10.3.7 상태 변경: 이벤트를 검출하고 상태 간 이동 기능을 구현한다.

10.3.8 초기 상태: 초기상태의 동작을 구현한다.

10.3.9 게임 상태: 게임을 운영하는 알고리즘을 구현한다.

10.3.10 종료 상태: 마지막으로 종료 상태의 기능을 구현한다

10.3.4 소스 파일 분리

프로그램의 크기가 큰 경우 소스 파일을 한 개의 파일로 만들면, 프로그램을 작성하기 불편할

뿐만 아니라, 함수나 변수를 찾기도 힘들다. 따라서, 이런 경우에는 소스 파일을 분리하여 작성하

는 것이 더 바람직하다.

비주얼 스튜디오와 같은 프로그램 개발 도구는 여러 개의 소스 파일로 구성된 프로젝트를 자동

으로 관리해 주는 기능을 제공한다. 그림 10-14는 여러 개의 소스 파일들이 하나의 실행 파일로

만들어지는 과정을 보여준다. 소스 파일은 각각 컴파일되어 목적 파일(object file)이 만들어진다.

목적 파일들과 라이브러리가 연결(link)되어 하나의 실행 파일이 만들어진다. 프로그램 개발 도구

는 사용자가 느끼지 못하게 이 두 가지 과정을 한 번에 처리하며, 소스 파일이 실행 파일로 만들

어지는 전체 과정을 빌드(build)라고 한다.

<그림 10-14> 프로젝트 빌드 과정

C 프로그램의 소스 파일은 두 가지 종류가 있다. 하나는 확장자가 .h인 헤더 파일이고 다른 하

나는 확장자가 .c인 소스 파일(최근 C 컴파일러와 C++ 컴파일러가 통합됨에 따라, 소스 파일의

확장자가 .cpp일 수도 있다.)로 구분된다. 한 개의 프로젝트는 여러 개의 헤더 파일과 여러 개의

소스 파일을 포함할 수 있다. 소스 파일 중에 main() 함수는 반드시 한 개 존재해야 한다.

타자연습 게임 프로젝트를 다음과 같이 세 개의 파일로 나누어 작성해 보자

keypractice.h: #include 문, #define 문, 그리고 함수 및 구조체 정의를 포함한다.

keypractice.c: 전역변수 선언, main() 함수, 그리고 게임 운영에 관련된 함수를 포함한다.

myinout.c: 표준입력장치와 표준출력장치에 대한 입출력 함수를 구현한다.

그림 10-14는 타자연습 게임의 프로젝트를 생성하고 나서, 세 개의 비어 있는 파일을 추가한 모

Page 232: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

230

습을 보여준다. 비주얼 스튜디오의 솔루션 탐색기에서 소스 파일 폴더에 두 개의 소스 파일과 헤

더 파일 폴더에 한 개의 헤더 파일을 추가한다. 소스 파일을 분리하여 프로그램을 작성하므로, 어

느 소스 파일에 어느 기능을 구현하는지 주의하여 보기 바란다.

<그림 10-14> 프로젝트 생성 화면

10.3.5 프로그램 구조 구현

상태도를 구현하려면 현재의 상태를 저장하는 변수가 필요하다. 가능한 상태를 #define 문으로

정의하고, 상태를 저장할 변수를 다음과 같이 정한다.

#define INITSTATE 0

#define GAMESTATE 1

#define GAMEOVER 2

#define PROGRAMEXIT 3

int state; // 초기값은 INITSTATE

“10.3.3 프로그램 구조 설계”에서 정의한 상태들 이외에 PROGRAMEXIT 상태를 하나 더 정의하였

Page 233: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

231

다. GAMEOVER 상태에서 사용자가 ESC를 누르면, 상태를 PROGRAMEXIT을 설정함으로써 프로그

램을 종료시킬 예정이다. 먼저 헤더 파일을 다음과 같이 작성한다.

<리스트 10-29> 최초 헤더 파일

// keypractice.h

1. #include <stdio.h> // 헤더 파일

2. #define INITSTATE 0 // 상태 정의

3. #define GAMESTATE 1

4. #define GAMEOVER 2

5. #define PROGTAMEXIT 3

6. // 여기에 myinout.c에 구현할 함수들의 정의를 추가한다.

7. // 다음에 keypactice.c에 구현할 함수들의 정의를 추가한다.

8. // 구조체 정의를 추가한다.

라인 6, 7, 8: 향후 주석에 따라 함수 정의와 구조체 정의를 추가할 예정이다. 프로그램의 전체적

인 구조를 다음과 같이 작성한다. 단순히 프로그램의 전체적인 형태만 만들어 놓은 것이다. 여기

까지 작성하고 프로그램을 컴파일하여 오류가 없음을 확인하고 다음 단계로 진행한다.

<리스트 10-30> 프로그램의 흐름 제어 구현

// keypractice.c

1. #include "keypractice.h” // 사용자가 만든 헤더 파일을 포함한다.

2. void main()

3. {

4. int state = INITSTATE; // 상태 제어 변수 선언 및 초기화

5. while (state != PROGRAMEXIT) // state를 PROGRAMEXIT으로 설정하면,

6. { // 프로그램이 종료된다.

7. switch(state)

8. {

9. case INITSTATE:

10. break;

11. case GAMESTATE:

12. break;

13. case GAMEOVER: // 사용자가 ESC를 누르면

14. break; // state를 PROGRAMEXIT으로 설정한다.

15. default:

Page 234: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

232

16. break;

17. }

18. } // end of while

19. }

라인 1: 사용자가 작성한 헤더 파일을 소스 코드에 포함시키려면 #include 문으로 헤더 파일

을 포함시킬 때 <> 대신에 “”를 사용하여야 한다.

이와 같이 헤더 파일을 포함시키면, keypactice.c의 앞 부분에 keypractice.h를 직접 작성한 것과

동일한 효과가 있다. 헤더 파일에 변수를 선언하는 것은 좋지 않다. 만일 여러 개의 소스 파일이

한 개의 헤더 파일을 포함한다면, 동일한 이름으로 여러 개의 변수가 선언되기 때문이다.

10.3.6 초기 화면 표시

가장 먼저 그림 10-9(a)와 같은 게임의 초기 화면을 표시해 보자. 초기 화면을 출력하는 알고리

즘은 다음과 같다.

<리스트 10-31> 초기 화면 출력 알고리즘

1. 커서를 지우고

2. 화면의 10 번째 라인에 "=== 타자 연습 ===="을 출력한다.

3. 화면의 12 번째 라인에 "게임을 시작하려면 엔터 키를 치세요."를 출력한다.

이 기능을 구현하기 위하여 myinout.c에 다음과 같은 화면 출력 함수를 작성한다.

void gotoxy(int x, int y): 커서를 화면의 (x, y) 좌표로 옮긴다.

void cursor_off(): 화면에서 커서를 제거한다.

void print_string(int line, char *str): 라인의 화면 가운데 문자열을 출력한다.

<리스트 10-32> 화면 출력 함수

// myinout.c에 함수 구현

1. #include <stdio.h>

2. #include <conio.h>

3. #include <windows.h>

4. void gotoxy(int x, int y)

5. {

6. COORD Pos = { x, y };

7. SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), Pos);

8. }

9. void cursor_off()

Page 235: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

233

10. {

11. CONSOLE_CURSOR_INFO Coff = { 100, 0 };

12. SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &Coff);

13. }

14. void print_string(int line, char *str) // 라인의 화면 가운데 문자열을 출력한다.

15. {

16. int dx;

17. dx = (80 - strlen(str)) / 2; // 문자열을 출력할 x 좌표를 구한다.

18. gotoxy(dx, line); // 커서를 이동하고

19. puts(str); // 문자열을 출력한다.

20. }

라인 17: 화면의 중앙에 문자열을 출력하기 위하여 x 좌표를 구한다.

keypractice.c에서 myinout.c에 구현한 함수를 사용하기 위하여 keypractice.h에 함수를 선언하여

야 한다. 따라서, 리스트 10-29의 라인 6 이후에 다음과 같은 사항을 추가한다.

<리스트 10-32> 헤더 파일에 함수 선언 추가

// keypractice.h

1. …

2. // 여기에 myinout.c에 구현할 함수들의 정의를 추가한다.

3. void gotoxy(int x, int y);

4. void cursor_off();

5. void print_string(int line, char *str);

초기 화면을 표시하기 위한 보조 함수들이 마련되었으므로, keypractice.c에 리스트 10-31의 초

기화 기능을 initialize() 함수로 구현한다. keypractice.h에 initialize() 함수를 선언하고, keypractice.c

에 함수 호출과 함수 구현을 추가한다. 컴파일하고 실행해 보면 그림 1(a)와 같은 초기 화면이 표

시되는 것을 볼 수 있다.

<리스트 10-33> 초기화 함수 구현

// keypractice.h 수정

1. …

2. // 다음에 keypactice.c에 구현할 함수들의 정의를 추가한다.

3. void initialize(); // 함수 선언 추가

// keypractice.c 수정

4. void main()

5. {

Page 236: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

234

6. … // main() 함수 안에서 while 문을 시작하기 전에

7. initialize(); // initialize()를 호출하는 문장을 추가한다.

8. while (state != PROGRAMEXIT)

9. …

10. }

11. void initialize() // 함수 구현

12. {

13. cursor_off(); // 커서를 지우고

14. print_string(10, "=== 타자 연습 ===="); // 초기화면을 출력한다.

15. print_string(12, "게임을 시작하려면 엔터 키를 치세요.");

16. }

10.3.7 상태 변경

사용자가 엔터 키를 입력할 때마다 상태를 변경하는 부분을 작성해 보자. 사용자의 키 입력을

검사하여 문자를 읽는 방법은 “10.2 문자열 회전” 문제의 “10.2.7 회전 방향 변경”에서 다루어 보

았다. 이 부분을 get_key() 함수로 구현하면, 다음과 같이 설계하고 구현할 수 있다.

<리스트 10-34> 사용자 키 입력을 검출하는 get_key() 함수

1. key = 0 // 사용자가 키를 누르지 않았다고 가정한다.

2. 만일 사용자가 키를 눌렀다면

3. 문자를 읽어 key에 저장한다.

4. key를 리턴한다.

<리스트 10-35> get_key() 함수 구현

// myinout.c에 함수를 추가한다.

1. int get_key() // 프로그램 설계의 리스트 4 구현

2. {

3. int key = 0; // 키를 누르지 않았다고 가정한다.

4. if ( _kbhit() ) // 만일 사용자가 키가 눌렀다면

5. key = _getch(); // 키를 읽고

6. return key; // 리턴한다.

7. }

상태를 변경할 때마다 화면을 모두 지워야 하므로, 화면을 지우는 clear_screen() 함수를 다음과

같이 구현한다.

<리스트 10-36> clear_screen() 함수 구현

// myinout.c에 함수를 추가한다.

Page 237: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

235

1. void clear_screen()

2. {

3. int x, y;

4. for (y=0; y<24; y++) // 화면의 각 라인마다

5. {

6. gotoxy(0, y); // 라인의 첫 번째로 커서를 이동하고,

7. for (x=0; x<80; x++)

8. _putch(‘ ‘); // 공백을 출력하여 화면을 지운다.

9. }

10. }

라인 8: _putch() 함수를 수행할 때마다 눈에 보이지 않지만 커서는 한 칸 오른쪽으로 이동한

다. 따라서 커서의 위치를 다시 설정할 필요가 없다.

이제 상태 간 이동 기능을 구현해 보자. keypractice.h에 프로그램에서 사용할 키에 대한 아스키

코드 정의와 함수 선언을 추가한다.

<리스트 10-37> 헤더 파일 수정

// keypractice.h 수정

// #define 문의 마지막 부분에 추가

1. #define ENTER 0x0d // ENTER 키 ASCII 코드

2. #define ESC 0x1b // ESC ASCII 코드

// myinout.c에 구현한 함수 선언의 마지막에 추가

3. int get_key(); // myinout.c에 구현하는 함수를 선언하는 부분에 추가한다.

4. void clear_screen();

상태 변경을 위하여 필요한 함수들이 모두 준비되었으므로, main() 함수에 상태간 이동 기능을

추가한다. 다음과 같이 사용자가 입력한 키를 저장할 변수를 추가하고, while() 문 내부를 수정한

다.

<리스트 10-38> 상태 변경 구현

// keypractice.c 파일 main() 함수의 while() 문 내부 수정

1. int key; // 지역 변수 선언 추가

2. …

3. while (state != PROGRAMEXIT)

4. {

5. key = get_key(); // 키가 눌려졌으면, 키를 읽는다.

6. switch(state)

7. {

Page 238: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

236

8. case INITSTATE: // 초기 상태

9. if (key == ENTER) // 엔터를 입력하였다면

10. {

11. clear_screen(); // 화면을 지우고

12. print_string(12, “게임 상태입니다.”); // 임시로 다음 상태를 출력한다.

13. state = GAMESTATE; // 게임 상태로 변경한다.

14. }

15. break;

16. case GAMESTATE: // 게임 상태

17. if (key == ENTER) // 엔터를 입력하였다면

18. {

19. clear_screen(); // 화면을 지우고,

20. print_string(12, "게임 오버 상태입니다."); // 다음 상태를 출력한다.

21. state = GAMEOVER; // 종료 상태로 변경한다.

22. }

23. break;

24. case GAMEOVER: // 종료 상태

25. if (key == ENTER) // 엔터를 입력하였다면

26. {

27. clear_screen(); // 화면을 지우고,

28. print_string(12, “게임 상태입니다.”); // 임시로 다음 상태를 출력한다.

29. state = GAMESTATE; // 게임 상태로 변경한다.

30. }

31. if (key == ESC) // ESC를 입력하였다면

32. state = PROGRAMEXIT; // 프로그램을 종료한다.

33. break;

34. default:

35. break;

36. } // end of switch

37. } // end of while

프로그램을 실행해 보면, 초기 화면이 표시된 이후에 엔터 키를 칠 때마다 게임 상태와 종료

상태를 반복하고, 종료 상태에서 ESC를 입력하면 프로그램이 끝나는 것을 확인할 수 있다.

10.3.8 초기 상태

세 개의 상태 중에서 첫 번째 상태인 초기 상태의 동작을 구현해 보자. “10.3.3 프로그램 구조

설계”의 리스트 10-28로 설계한 초기 상태의 동작을 다시 기술하면 다음과 같다.

<리스트 10-39> 타자연습 게임의 초기 상태 동작

Page 239: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

237

1. 만일 초기 상태이면

2. 입력한 문자가 엔터이면

3. 게임 상태의 진입 동작(화면, 점수 초기화, 화면 갱신 시간 설정)을 수행하고,

4. 게임 상태로 이동한다.

라인 3이 새로 구현해야 할 부분이다. 이 기능을 game_entry() 함수로 구현하자. 이 함수는 화면

을 지우고, 점수를 0으로 만들고, 화면 갱신 시간을 설정해야 한다.

점수와 화면 갱신 시간을 관리하기 위하여 다음과 같은 전역 변수를 사용하기로 한다.

int score: // 게임 점수

clock_t update_time: // 화면 갱신 시간

점수 score의 초기값은 0이고, 영문자를 하나씩 맞출 때마다 1 증가한다. 화면 갱신 시간

update_time의 초기값은 (현재 시간+1초)이고, 점수가 10점씩 증가할 때마다 (현재 시간+0.3초)까

지 감소한다.

game_entry() 함수의 알고리즘과 구현은 다음과 같다.

<리스트 10-40> 게임 상태로 진입하는 game_entry() 함수

1. clear_screen() // 화면을 지운다.

2. score = 0 // 점수를 0으로 만든다.

3. update_time = 현재 시간 + 1초 // 화면 갱신 시간을 설정한다.

<리스트 10-41> game_entry() 함수 구현

// keypractice.h

1. #inlcude <time.h> // 헤더 파일 추가. clock_t 형 및, clock() 함수 선언

2. void game_entry(); // 함수 선언 추가

// keypactice.c

1. int score; // 전역 변수 추가

2. clock_t update_time; // 전역 변수 추가

3. void game_entry()

4. {

5. clear_screen(); // 화면을 지운다.

6. score = 0; // 점수를 초기화 한다.

7. update_time = clock() + 1000; // 현재 시간 + 1000 msec로 설정한다.

8. }

game_entry() 함수를 구현하고 나서 초기 상태의 동작을 수정한다. 단순히 print_string() 함수

Page 240: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

238

호출을 제거하고 game_entry() 함수를 호출하면 된다.

<리스트 10-42> 게임 초기상태 수정

// keypractice.c 수정

1. … // main() 함수 초기 상태 수정

2. case INITSTATE:

3. if (key == ENTER) // 엔터를 입력하였다면

4. {

5. clear_screen(); // 화면을 지우고

6. //print_string(12, “게임 상태입니다.”); // 이 라인을 지우고,

7. game_entry(); // 게임 진입 함수를 호출한다.

8. state = GAMESTATE; // 게임 상태로 변경한다.

9. }

10. break;

10.3.9 게임 상태

“10.3.3 프로그램 구조 설계”의 게임 상태 동작을 정리하면 다음과 같다.

<리스트 10-43> 게임 상태의 동작

1. 만일 게임 상태이면

2. 화면을 갱신할 시간이 되었다면

3. 화면을 한 라인 아래로 옮기고 새로운 영문자를 추가한다.

4. 입력한 문자가 영문자이면,

5. 해당 영문자를 제거한다.

6. 마지막 라인에 영문자가 도착하였다면,

7. 종료 상태의 진입 동작(종료 안내문 출력)을 수행하고,

8. 종료 상태로 이동한다.

게임 상태의 동작을 세부적으로 설계하기 전에 먼저 영문자를 표시하기 위한 자료구조를 생각

해 보자. 콘솔 화면의 크기가 가로 80, 세로 25이고, 요구사항에 의하여 화면의 한 라인의 임의의

위치에 영문자가 하나 표시된다. 그런데 콘솔의 마지막 라인의 마지막에 문자를 표시하면, 화면이

스크롤되므로, 마지막 줄은 사용하지 않기로 한다.

화면 전체를 2차원 배열 screen[24][80]으로 표시하고, 난수 발생기로 x 좌표와 영문자를 생성하

여, 배열의 screen[0][x]에 생성된 영문자를 저장하는 방법을 생각할 수 있다. 그렇지만, 이 방법은

screen[][] 배열의 한 줄에 해당하는 80 개의 공간 중에서 한 곳에만 영문자를 저장하므로 기억

공간이 낭비된다.

화면의 한 라인에 한 개의 영문자만 저장하면 충분하므로, 다음과 같은 구조체와 구조체 배열

Page 241: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

239

을 사용해 보자

struct LINE_ALPHABET

{

int dx; // 문자를 표시할 x 좌표

char alpha; // 한 라인에 표시되는 문자

};

struct LINE_ALPHABET alphalist[24]; // 화면에 표시할 문자를 저장할 구조체 배열

화면 전체는 구조체 배열로 표시되고, 배열의 인덱스는 화면의 y 좌표에 해당한다. 이 배열은 게

임이 새로 시작할 때마다, 즉 게임 상태로 진입하는 game_entry() 함수에서 초기화 되어야 한다.

게임 상태에서 처리할 동작을 세부적으로 살펴보자. 이 부분에서는 화면 갱신 시간 처리, 사용

자 입력 처리, 그리고 게임 종료 검사를 수행해야 한다. 각 기능을 별개의 함수로 만든다면, 게임

상태의 동작을 다음과 같이 표현할 수 있다.

<리스트 10-44> 게임 상태의 동작

1. case GAMESTATE: // 만일 게임 상태이면

2. if (현재 시간>update_time) // 화면을 갱신할 시간이 되었다면

3. hide_alpha() // 화면에 표시된 영문자를 지우고

4. generate_alpha() // 새로운 영문자를 추가한다.

5. show_alpha() // 화면을 새로 표시한다.

6. set_update_time() // 화면 갱신 시간 변경

7. if (key != 0) // 사용자가 문자를 입력하였다면

8. match_key(key) // 해당 영문자를 제거한다.

9. if (alphalist[24].alpha != 0) // 마지막 라인에 영문자가 도착하였다면,

10. game_exit() // 종료 안내문 출력을 수행하고,

11. state = GAMEOVER // 종료 상태로 이동한다.

화면에 표시된 영문자들을 한 라인씩 아래로 이동하는 과정은 현재 화면에 표시된 영문자를 지

우고(라인 3), alphalist[] 배열의 원소를 다음 라인으로 옮기고 첫 번째 원소에 새로운 영문자를 추

가하고(라인 4), 다시 화면에 영문자들을 출력하는(라인 5)로 분해할 수 있다. 그리고, 화면 갱신

시간을 다음 갱신 시간으로 변경(라인 6)하여야 한다. 새로 추가할 함수와 기능은 다음과 같다.

void show_alpha(); 화면에 alphalist[]를 표시한다.

void hide_alpha(): 화면에서 alphalist[]을 보이지 않게 지운다.

void generate_alpha(): 첫 번째 라인에 영문자를 생성하여 alphalist[0]에 저장한다.

void match_key(int key): alphalist[]에서 사용자가 입력한 영문자를 제거한다.

void set_update_time(): 화면 갱신 시간을 변경한다.

void game_exit(): 종료 안내문을 출력한다.

Page 242: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

240

프로그램의 구조를 만들어 놓고 각 함수를 구현하기로 하자. 구조체를 정의하는 struct 문은 직

접 변수를 만드는 것이 아니므로 헤더 파일에 작성하고, 구조체 변수는 소스 파일에 작성한다. 프

로그램을 다음과 같이 수정한다.

<리스트 10-45> 게임 상태의 동작 구조 구현

1. // keypractice.h

2. void generate_alpha(); // 함수 선언 추가

3. void show_alpha();

4. void hide_alpha();

5. void set_update_time();

6. void match_key(int key);

7. void game_exit();

8. struct LINE_ALPHABET // 구조체 선언 추가

9. {

10. int dx;

11. char alpha;

12. };

13. // keypractice.c

14. struct LINE_ALPHABET alphalist[24]; // 구조체 배열 선언;

15. void main()

16. {

17. …

18. case GAMESTATE: // 수정

19. if (clock()>update_time) // 화면을 갱신할 시간이 되었다면

20. {

21. hide_alpha(); // 화면에 표시된 영문자를 지우고

22. generate_alpha(); // 새로운 영문자를 추가한다.

23. show_alpha(); // 새로운 화면을 표시한다.

24. set_update_time(); // 화면 갱신 시간 변경

25. }

26. if (key != 0) // 사용자가 문자를 입력하였다면

27. match_key(key); // 해당 영문자를 제거한다.

28. if (alphalist[23].alpha != 0) // 마지막 라인에 영문자가 도착하였다면,

29. {

30. game_exit(); // 종료 안내문 출력을 수행하고,

31. state = GAMEOVER; // 종료 상태로 이동한다.

Page 243: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

241

32. }

33. break;

34. …

35. }

36. void game_entry()

37. {

38. int n; // 변수를 추가한다.

39. …

40. gotoxy(0, 24); // 레벨과 점수를 출력한다.

41. printf("레벨: %d 점수: %3d", score/10+1, score);

42. for (n=0; n<24; n++) // 구조체 배열을 초기화 한다.

43. alphalist[n].alpha = 0; // 저장하고 있는 영문자를 지운다.

44. }

45. void generate_alpha() { } // 비어 있는 함수 구현

46. void show_alpha() { }

47. void hide_alpha() { }

48. void set_update_time() { }

49. void match_key(int key) { }

50. void game_exit() { }

구조체 배열을 선언하였으므로, 게임을 시작할 때, 이 배열을 초기화 하는 부분과 화면의 마지

막 라인에 레벨과 점수를 출력하는 부분을 추가하였다. 이와 같이 프로그램 구조를 만들어 놓고,

이벤트 처리 기능을 하나씩 구현해 보자. 구조체 배열에 저장되어 있는 영문자를 화면에 표시하

고 화면을 지우는 알고리즘은 다음과 같다.

<리스트 10-46> show_alpha() 함수의 동작

1. 화면의 y 축에 대하여

2. 커서를 (alphalist[y].dx, y)로 옮기고

3. 해당 위치에 alphalist[y].alpah를 출력한다.

<리스트 10-47> hide_alpha() 함수의 동작

1. 화면의 y 축에 대하여

2. 커서를 (alphalist[y].dx, y)로 옮기고

3. 해당 위치에 공백(‘ ‘)을 출력한다.

두 함수의 차이점은 화면에 영문자를 출력하는 것과 공백을 출력하는 것이다. 기능이 유사하므로

다음과 같은 함수를 만든다.

Page 244: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

242

<리스트 10-48> 문자 출력 함수 구현

// keypractice.h

1. void myputch(int x, int y, char ch); // 함수 선언 추가

2. // myinout.c

3. void myputch(int x, int y, char ch) // 함수 구현

4. {

5. gotoxy(x, y); // 커서를 옮기고

6. _putch(ch); // 문자를 출력한다.

7. }

myinout() 함수를 사용하여 show_alpha()와 hide_alpha() 함수를 구현한 결과는 다음과 같다.

<리스트 10-48> show_alpha()와 hide_alpha() 함수를 구현

// keypractice.c

1. void show_alpha() // 리스트 10-46 구현

2. {

3. int y;

4. for (y=0; y<24; y++) // 영문자를 모두 출력한다.

5. if (alphalist[y].alpha != 0) // 문자가 존재하면

6. myputch(alphalist[y].dx, y, alphalist[y].alpha); // 문자를 출력한다.

7. }

8. void hide_alpha() // 리스트 10-47 구현

9. {

10. int y;

11. for (y=0; y<24; y++)

12. if (alphalist[y].alpha != 0) // 문자가 존재하면

13. myputch(alphalist[y].dx, y, ' '); // 공백을 출력하여 문자를 지운다.

14. }

영문자를 생성하는 generate_alph() 함수를 설계한다. 이 함수는 구조체 배열의 값을 하나씩 아

래로 옮기고, 난수 발생기로 영문자를 표시할 x 좌표와 영문자를 생성하여 구조체 배열의 첫 번

째 원소에 저장한다. 이 알고리즘은 다음과 같다.

<리스트 10-49> genearate_alpha() 함수의 동작

1. y=23부터 y=1까지 y를 1씩 줄여가면서

2. alphalist[y] = alphalist[y-1] // 구조체 원소를 아래로 옮긴다.

Page 245: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

243

3. alphalist[0].dx에 난수 발생기로 x 축 좌표를 생성하여 저장한다.

4. alphalist[0].alpah에 난수 발생기로 영문자를 생성하여 저장한다.

영문자 생성 함수를 구현하려면 난수 발생기를 사용하여야 한다. 그러므로 프로그램을 다음과 같

이 구현한다.

<리스트 10-50> genearate_alpha() 함수 구현

// keypractice.h

1. #include <stdlib.h> // 난수 발생기를 위한 헤더 파일 추가

// keypractice.c

1. void initialize()

2. {

3. srand(time(NULL)); // 첫 번째 라인에 난수 발생기 초기화 추가

4. …

5. }

6. void generate_alpha() // 리스트 10-49 구현

7. {

8. int y;

9. for (y=23; y>0; y--) // y 축 방향으로

10. alphalist[y] = alphalist[y-1]; // 영문자를 다음 라인으로 옮기고

11. alphalist[y].alpha = (rand() % 26) + 'A'; // 영문자를 생성하고

12. alphalist[y].dx = rand() % 80; // x 좌표를 생성한다.

13. }

라인 9: 배열의 원소를 옮길 때, 뒤부터 옮겨야 데이터를 잃어버리지 않는다.

라인 10: 구조체 원소를 옮기면, 구조체 멤버가 모두 옮겨진다. 즉, 다음 두 개의 문장과 효과

가 같다.

alphalist[y].alpha = alphalist[y-1].alpha;

alphalist[y].dx = alphalist[y-1].dx;

라인 11, 12: 난수 발생기로 영문자와 x 좌표를 생성한다.

화면 갱신 시간을 설정하는 set_update_time() 함수를 설계해 보자. 점수을 부여하는 정책에 따

라 화면 갱신 시간이 달라진다. 화면 갱신 시간 update_time의 초기값은 (현재 시간+1초)이고, 점

수가 10점씩 증가할 때마다 (현재 시간+0.3초)까지 감소한다. 이 함수의 알고리즘과 구현은 다음

과 같다.

<리스트 10-49> set_update_time() 함수의 동작

1. update_timed에 (1초 – 점수/10*0.1초)를 저장한다.

Page 246: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

244

2. 만일 update_time < 0.3초이면, update_time을 0.3초로 설정한다.

<리스트 10-50> set_update_time() 함수 구현

// keypractice.c

1. void set_update_time() // 리스트 10-49 구현

2. {

3. long next_time;

4. next_time = 1000 - (score/10)*100; // 화면 다음 갱신 시간을 계산한다.

5. if (next_time < 300) // 0.3 초보다 작으면

6. next_time = 300; // 0.3 초로 설정한다.

7. update_time = clock() + next_time; // 현재시간 + 다음 갱신 시간

8. }

PC에서 clock() 함수는 밀리초 단위의 시간을 리턴한다.

여기까지 프로그램을 작성한 후 컴파일 하고 실행하면, 그림 10-15와 같이 1초 간격으로 화면

이 갱신되는 것을 확인할 수 있다. 그렇지만, 사용자가 입력한 키를 처리하지 않았기 때문에, 점

수를 처리하지 않고 문자가 마지막 라인에 도달하면 게임 상태를 벗어난다. 화면으로 확인할 수

는 없지만, 화면이 정지되었을 때, 엔터 키를 입력하면 다시 게임을 진행하고, ESC 키를 입력하면

프로그램이 종료된다.

<그림 10-15> 영문자 생성 화면

Page 247: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

245

사용자가 입력한 키에 해당하는 영문자를 삭제하는 match_key() 함수를 설계해 보자. 사용자가 입

력한 영문자를 대문자로 변환한 후, 구조체 배열에 저장된 영문자와 비교하여 같으면 배열에서

제거한다. 사용자가 입력한 영문자를 제거할 때, 배열의 뒤쪽부터 검사해서 일치하는 것을 제거해

야 한다. 화면에 동일한 문자가 여러 개 존재할 수 있기 때문이다. 그리고, 사용자가 영문자를 맞

추었을 때, 효과음으로 삑 소리를 출력해 보자. 프로그램의 알고리즘과 구현은 다음과 같다.

<리스트 10-51> match_key(key) 함수의 동작

1. key를 대문자로 변환한다.

2. y=23부터 y=0까지 y를 1씩 줄여가면서

3. 만일 key와 alphalist[y].alpha가 같다면

4. 삑 소리를 출력한다.

5. alphalist[y]에 저장된 영문자를 지운다.

6. 점수를 증가시킨다.

7. 반복문을 벗어난다.

8. 게임 레벨과 점수를 출력한다.

<리스트 10-52> match_key()와 game_exit() 구현

// keypractice.h

1. #define BEL 0x07 // 벨 코드 정의 추가

// keypractice.c

1. void match_key(int key) // 리스트 10-51 구현

2. {

3. int y;

4. if (key >= 'a') // 키가 소문자 이면

5. key = key - ('a' - 'A'); // 대문자로 변경한다.

6. for (y=23; y>=0; y--) // 화면의 아래쪽부터

7. if (key == alphalist[y].alpha) // key에 해당하는 영문자가 있다면

8. {

9. printf(“%c”, BEL); // 삑 소리를 출력한다.

10. myputch(alphalist[y].dx, y, ' '); // 그 자리를 지운다.

11. alphalist[y].alpha = 0; // 저장된 문자를 지운다.

12. score += 1; // 점수를 증가시킨다.

13. break; // for 루프를 벗어난다.

14. }

15. gotoxy(0, 24); // 게임 레벨과 점수를 출력한다.

16. printf("레벨: %d 점수: %3d", score/10+1, score);

17. }

Page 248: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

246

라인 4, 5: 소문자인 경우, 대문자로 변환한다. toupper() 함수를 사용할 수도 있지만, 라인 5

와 같이 구현해 보았다.

라인 9: 아스키 코드의 BEL을 화면으로 출력하면, 스피커로 삑 소리가 난다.

라인 10: 사용자가 입력한 영문자를 화면에서 지운다.

라인 11: 사용자가 입력한 영문자를 구조체 배열에서 지운다.

라인 13: 영문자를 맞추었으므로, 반복문을 계속 수행할 필요가 없다.

라인 16: 10 점마다 레벨을 증가시키므로, 레벨을 (점수/10+1)로 계산하여 출력한다.

프로그램을 실행하면, 사용자가 입력한 키가 화면에서 제거되면서 게임이 진행되는 것을 확인할

수 있다. 이제 게임이 종료될 때, 게임 종료 상태로 변경되는 game_exit() 함수를 구현해 보자.

game_exit() 함수는 “10.3.1 요구사항 분석”에서 설계한 그림 10-9(d)와 같은 게임 안내문을 출력

하면 된다. 이 함수를 구현하기 위하여 다음과 같은 라이브러리 함수를 사용한다.

#include <stdio.h>

int sprint(char *str, const char *format, …)

파라미터

char *str: 문자열을 출력할 버퍼에 대한 포인터

const char *format: 출력할 형식 문자열

추가 파라미터: 형식 문자열에 포함되어 있는 변환 기호에 해당하는 변수 목록

리턴

성공하면, 버퍼에 기록한 문자의 수(널 문자는 제외)

실패하면, 음수

sprint() 함수는 printf() 함수와 사용 방법이 같으나, 다만 첫 번째 파라이터로 전달되는 문자열 버

퍼에 문자열을 저장한다. 형식에 맞춘 문자열을 만들고자 할 때 아주 유용하게 사용되는 함수이

다.

<리스트 10-53> game_exit() 함수 구현

// keypractice.c

1. void game_exit()

2. {

3. char buf[100];

4. print_string(8, "=== 게 임 오 버 ====");

5. sprintf(buf, "당신의 점수는 %2d점입니다.", score);

6. print_string(10, buf);

7. print_string(12, "게임을 다시 시작하려면, 엔터 키를 치세요.");

8. print_string(14, "게임을 종료하려면, ESC 키를 치세요.");

9. }

프로그램을 실행하면, 게임이 종료될 때까지 프로그램이 정상적으로 실행되는 것을 볼 수 있다.

Page 249: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

247

게임 레벨이 올라갈수록 화면 갱신 시간도 줄어드는 것도 확인할 수 있다. 그림 10-16은 게임이

종료되었을 때의 화면이다.

<그림 10-16> 게임 종료 화면

10.3.10 종료 상태

프로그램의 마지막 상태인 종료 상태의 동작을 구현해 보자. “10.3.3 프로그램 구조 설계”에서

설계한 종료 상태의 동작을 다시 기술하면 다음과 같다.

<리스트 10-54> 게임 종료 상태

1. 만일 종료 상태이면

2. 입력한 문자가 엔터이면,

3. 게임 상태의 진입 동작(화면, 점수 초기화, 화면 갱신 시간 설정)을 수행하고,

4. 게임 상태로 이동한다.

5. 입력한 문자가 ESC 이면

6. 무한 루프를 벗어나고 프로그램을 종료한다.

이 동작을 구현하기 위하여 새로 추가할 함수는 없다. “10.3.7 상태 변경”에서 임시로 만들어 두었

던 게임 종료 상태의 동작을 다음과 같이 수정한다.

Page 250: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

248

<리스트 10-55> 게임 종료 상태 구현

// keypractice.c main() 함수 수정

1. case GAMEOVER: // 종료 상태

2. if (key == ENTER) // 엔터를 입력하였다면

3. {

4. //clear_screen(); // 라인 삭제

5. //print_string(12, “게임 상태입니다.”); // 라인 삭제

6. game_entry(); // 함수 호출 추가

7. state = GAMESTATE; // 게임 상태로 변경한다.

8. }

9. if (key == ESC) // ESC를 입력하였다면

10. state = PROGRAMEXIT; // 프로그램을 종료한다.

11. break;

라인 4: game_entry() 함수 안에서 clear_screen()을 호출하므로, 이 라인을 삭제한다.

라인 5: 임시로 작성한 출력문을 삭제한다.

라인 6: 게임 진입 함수를 호출한다.

이제 프로그램이 완성되었다. 게임이 끝나도 연속해서 게임을 실행할 수 있다.

10.3.11 타자연습 게임 정리

지금까지 타자연습 게임을 세 개의 소스 파일로 나누어 구현하였다. 각 파일의 헤더 부분과 각

파일에서 구현한 함수를 정리하면 다음과 같다. 헤더 파일 keypractice.h는 전체 소스를 제시한다.

myinout.c

입출력에 관련된 함수를 구현한다.

#include <stdio.h> // 헤더 부분

#include <conio.h>

#include <Windows.h>

// 구현한 함수 목록

void gotoxy(int x, int y) { … } // 커서를 (x, y)로 이동한다.

void cursor_off() { … } // 화면에서 커서를 지운다.

void print_string(int line, char *str) { … } // line 번째 라인의 가운데에 문자열을 출력한다.

int get_key() { … } // 키보드를 검사하고, 키를 읽어 리턴한다.

void clear_screen() { … } // 화면의 영문자를 모두 지운다.

void myputch(int x, int y, char ch) // 화면의 (x, y) 좌표에 문자를 출력한다.

Page 251: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

249

keypractice.h

keypractice.c에서 사용할 헤더 부분을 구현한다.

#include <stdio.h> // 헤더 파일 추가

#include <time.h>

#include <stdlib.h>

#define INITSTATE 0 // 상태 상수 정의

#define GAMESTATE 1

#define GAMEOVER 2

#define PROGRAMEXIT 3

#define ENTER 0x0d // 아스키 코드 정의

#define ESC 0x1b

#define BEL 0x07

// myincout.에 구현한 함수 선언

void gotoxy(int x, int y);

void cursor_off();

void print_string(int line, char *str);

int get_key();

void clear_screen();

void myputch(int x, int y, char ch);

// keypractice.c에 구현한 함수 선언

void initialize();

void game_entry();

void generate_alpha();

void show_alpha();

void hide_alpha();

void set_update_time();

void match_key(int key);

void game_exit();

struct LINE_ALPHABET // 구조체 선언

{

int dx;

char alpha;

};

Page 252: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

250

keypractice.c

전역 변수를 선언하고 타자연습 게임에 관련된 함수를 구현한다.

#include "keypractice.h" // 헤더 파일 추가

int score; // 전역 변수 선언

clock_t update_time;

struct LINE_ALPHABET alphalist[24];

void main() { … } // main() 함수 구현

void initialize() { … } // 게임을 초기화 한다.

void game_entry() { … } // 게임 진입 동작을 구현한다.

void generate_alpha() { … } // 영문자를 한 라인씩 아래로 옮기고, 영문자를 생성한다.

void show_alpha() { … } // 구조체 배열에 저장된 영문자를 화면에 표시한다.

void hide_alpha() { … } // 구조체 배열에 저장된 영문자를 화면에서 지운다.

void set_update_time() { … } // 화면 갱신 시간을 설정한다.

void match_key(int key) { … } // 사용자가 입력한 영문자를 구조체 배열에서 제거한다.

void game_exit() { … } // 게임 오버 화면을 출력한다.

10.4 요약

이 장에서 몇 가지 게임 프로그램을 개발해 보았다. 새로운 프로그램 문법이 많이 소개되기 보

다는 프로그램의 알고리즘을 설계하는 방법과 규모가 큰 프로그램을 하향식 방법으로 설계하고

구현하는 방법에 대하여 많이 학습하였을 것으로 생각한다. 이 장에서 다루었던 중요한 프로그래

밍 기법은 다음과 같다.

이차원 배열은 일차원 배열이 여러 개 평면적으로 배치되는 구조이다.

프로그램을 단계적으로 구현하면서 중간 결과를 확인해야 한다.

프로그램 전체에 영향을 주는 변수를 전역 변수로 사용함으로써 프로그램을 간단하게 만들

수 있다.

시간을 측정하기 위하여 clock() 함수를 사용한다.

아스키 코드 중 화면 제어 코드를 사용할 때는 #define 문으로 정의하여 사용하는 것이 편리

하다.

사용자가 키보드를 눌렀는지 검사하는 함수를 사용하면, 사용자의 입력을 대기하지 않고 데

이터를 입력할 수 있다.

프로그램의 종류를 프로시저 구동 방식과 이벤트 구동 방식으로 나눌 수 있다.

이벤트 구동 프로그램은 이벤트를 검출하고 그 이벤트에 대한 동작을 처리하는 구조로 만들

어진다.

이벤트 구동 프로그램은 상태도와 상태표로 표현한다.

한 개의 상태를 진입 동작, 상태 동작, 출구 동작, 그리고 상태 변경으로 표현할 수 있다.

이벤트 구동 프로그램의 각 상태를 처리하는 프로그램의 구조는 다음과 같다.

Page 253: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

251

현재 상태에서 처리해야 할 이벤트가 발생하였다면,

상태 동작: 이벤트에 대하여 수행할 동작을 수행한다.

상태를 변경할 이벤트가 발생하였다면,

출구 동작: 현재 상태를 종료하면서 처리할 동작을 수행하고,

진입 동작: 다음 상태로 진입하기 위하여 필요한 진입 동작을 수행하고

상태 변경: 상태를 변경한다.

한 개의 프로그램을 여러 개의 소스 파일로 분리하여 작성할 수 있다.

컴파일러는 각 소스 파일을 목적 파일로 컴파일하고, 링커는 목적 파일들과 라이브러리를 연

결하여 실행 파일을 생성한다.

사용자가 만든 헤더 파일을 소스 파일에 포함시키려면 “”를 사용한다.

이 장에서 새로 소개된 라이브러리 함수는 다음과 같다.

int _getch(void) // 버퍼를 사용하지 않고, 에코도 하지 않고 문자를 입력한다.

int _putch(int c) // 버퍼를 사용하지 않고 화면에 문자를 출력한다.

clock_t clock(void) // 컴퓨터가 관리하는 시간을 밀리초 단위로 린턴한다.

int _kbhit(void); // 사용자가 키보드를 눌렀는지 검사한다.

int sprint(char *str, const char *format, …) // 문자열 버퍼에 형식화된 문자열을 출력한다.

Page 254: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

252

제11장 연결리스트

자료형이 같은 여러 개의 데이터를 관리하기 위하여 그 동안 우리는 배열을 사용하였다. 배열

이외에 여러 개의 데이터를 관리하는 자료구조가 연결리스트(linked list)이다. 여러 개의 데이터를

포인터로 연결하는 연결리스트는 매우 중요한 데이터 표현 방법이다. 이 장에서는 다음과 같은

문제를 해결하는 과정을 통하여 연결리스트를 학습한다.

성적 정렬: 여러 명의 학생 정보(학번, 이름, 성적)를 입력 받고 성적을 기준으로 내림차순으

로 정렬하여 출력한다.

기억력 게임 2: 화면에 새로 표시되는 영문자를 맞추는 게임 프로그램을 연결리스트로 구현

한다.

이 장을 통하여 다음과 같은 프로그래밍 기술을 학습할 수 있다.

자기참조 구조체에 의한 연결리스트 기본 개념

연결리스트로 구축되어 있는 데이터의 검색, 삽입, 삭제, 정렬 기술

메뉴 구동형 프로그램 작성 기술

이진 파일 입출력 기술

11.1 성적 정렬

여러 명의 학생 정보(학번, 이름, 성적)를 입력 받고 성적을 기준으로 내림차순으로 정렬하여 출

력하는 프로그램을 작성하라. 학생 정보를 입력하다가 학번이 -1이면 입력을 중단하고, 그 동안

입력 받은 학생 정보를 출력한다. 단, 프로그램의 자료 구조로 연결리스트를 사용하라.

11.1.1 요구사항 분석

이 문제의 전제적인 구조는 영어 단어를 입력 받고 사전 순으로 정렬하여 출력하는 “9.2 단어

정렬” 문제와 동일하다. 데이터가 여러 개의 항목들로 구성되어 있고, 데이터를 저장하기 위하여

배열 대신에 연결리스트를 사용하는 것이 차이점이다.

요구사항 분석 단계에서는 어떻게 구현할지 고려할 필요가 없으므로, 성적 정렬 프로그램의 입

출력을 명확하게 정리해 보자. 다음과 같이 안내문을 출력하고, 사용자가 -1을 입력할 때까지 사

용자로부터 학번, 이름, 성적을 계속 입력 받는다. 입력이 끝나면 오른쪽과 같이 출력한다.

입력 출력

학생 정보를 입력하세요.

종료하려면 학번에 -1을 넣으세요.

학번: 2013041001

이름: 심청

성적: 78

학번 이름 성적

=========================

2013041002 홍길동 85

2013041001 심청 78

2013041005 성춘향 67

Page 255: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

253

학번: 2013041002

이름: 홍길동

성적: 85

학번: 2013041005

이름: 성춘향

성적: 67

학번: -1

11.1.2 연결리스트

자료구조를 설계하기 전에 포인터와 연결리스트에 대하여 알아보자. 연결리스트는 포인터로 연

결되는 자료구조이다. 그 동안 우리는 함수의 파라미터를 통하여 간접적으로 데이터를 리턴 받기

위하여 포인터를 사용하였다. 연결리스트를 사용하려면, 포인터 변수(pointer variable)를 잘 활용

할 수 있어야 한다. 포인터 변수를 줄여서 포인터(pointer)라고 부른다. 포인터에 대하여 다음과

같은 사항을 알아 두어야 한다.

포인터는 기억장치의 주소를 저장하는 변수이다.

포인터는 변수(또는 객체)의 주소를 저장한다.

포인터도 자료형이 있다.

포인터의 자료형은 포인터 변수가 가리키는 변수의 자료형이다.

포인터는 기억장치의 주소를 저장하므로 자신이 가리키는 자료형과 무관하게 크기가 같다.

포인터 변수에 값이 할당되지 않았다는 의미로 포인터에 0(널, NULL)을 할당한다. 즉, 포인터

의 값이 널이란 것은 포인터가 아무것도 가리키지 않는다는 것을 의미한다.

void *(void 형 포인터)도 포인터 변수이다. 다만, 포인터가 가리키는 자료형이 결정되어 있지

않다는 의미이다.

그림 11-1은 문자형, 정수형, 배정도 실수형, 그리고 구조체 변수에 대한 포인터의 예를 나타낸

것이다. 포인터 변수에 값이 할당된 것을 화살표로 표시한다. 그림 11-1의 왼쪽은 포인터 변수를

선언하는 방법의 예이고, 오른쪽은 포인터 변수에 주소 값이 할당되고 해당 주소에 데이터가 들

어 있는 모습이다. 네 개의 포인터는 모두 크기가 같지만, 포인터가 가리키는 데이터의 크기는 포

인터의 자료형의 크기와 같다.

Page 256: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

254

<그림 11-1> 포인터 사용 예

연결리스트를 사용하여 데이터를 저장하는 방법을 알아보자. 그림 11-2와 같이 데이터를 서로

연결하여 저장하는 자료구조를 연결리스트라고 한다.

<그림 11-2> 연결리스트의 예

리스트 헤드(list head): 연결리스트의 시작을 나타내는 포인터 변수를 일반적으로 리스트 헤

드라고 부른다.

노드(node): 연결리스트에서 한 개의 데이터를 저장하는 자료구조를 노드라고 한다. 노드는

{데이터, 포인터} 쌍으로 구성되어 있다.

연결리스트의 끝: 연결리스트의 마지막 노드에 포함되어 있는 포인터의 값을 널로 설정함으

로써, 연결리스트의 마지막 노드임을 나타낸다.

연결리스트를 구현하기 위하여 노드를 표현하는 자료구조를 다음과 같이 구조체로 선언한다.

struct 구조체이름 // 구조체 선언

{

데이터 필드; // 데이터를 저장하기 위한 변수 목록

struct 구조체이름 *next; // 노드를 연결하기 위한 포인터

};

데이터 필드는 실질적으로 연결리스트에 저장할 변수들의 모임이다. 포인터 필드는 노드를 서

로 연결하기 위하여 사용되며, 일반적으로 포인터 변수의 이름으로 next를 사용한다. 구조체를 선

언하고 나서, 다음과 같이 리스트 헤드에 해당하는 구조체 변수를 선언한다. 리스트 헤드의 초기

값을 NULL로 할당함으로써, 처음에 연결되어 있는 노드가 없다는 것을 나타낸다.

struct 구조체이름 *listhead = NULL; // 리스트 헤드 변수

연결리스트를 사용하기 위하여 리스트 헤드에 노드를 삽입하고 제거하는 방법과 연결리스트의

각 노드를 추적하는 방법을 알고 있어야 한다. 이에 대하여 문제를 풀어가면서 설명하기로 한다.

Page 257: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

255

11.1.3 자료구조 설계

학생에 대한 정보가 세 개의 필드로 구성되어 있으므로, 학생을 표현하기 위하여 구조체를 사

용하는 것이 바람직하다. 학번은 숫자로만 구성되지만 일반적으로 10 자리 이상의 자리수를 갖는

숫자이므로 문자열로 처리하도록 한다. 학번과 이름에 크기가 16인 문자형 배열을 할당하고, 성적

은 정수형 데이터에 저장하자. 학생 정보를 연결리스트로 저장하려면, 구조체에 다음 노드를 연결

할 포인터도 필요하다. 따라서, 학생 정보 구조체를 다음과 같이 정한다.

struct STUDENT_INFO

{

char id[16]; // 학번

char name[16]; // 이름

int score; // 성적

struct STUDENT_INFO *next; // 다음 노드를 연결하기 위한 포인터

};

그리고 리스트 헤드에 해당하는 변수를 선언하여 사용해야 한다. 리스트 헤드를 다음과 같이 선

언하고 초기화 한다.

struct STUDENT_INFO *head = NULL; // 연결리스트의 헤드

<그림 11-3> 연결리스트에 데이터를 저장하는 과정

이와 같은 자료구조를 활용하는 방법을 알아보자. 그림 11-3은 심청, 홍길동, 성춘향에 대한 학

생 정보를 입력하였을 때, 성적에 대하여 내림차순으로 연결리스트를 구축하는 과정을 보여준다.

Page 258: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

256

그림 11-3(a): 처음에 리스트 헤드의 값은 NULL이고 데이터가 없음을 의미한다.

그림 11-3(b): 심청의 데이터를 리스트 헤드에 연결한다. 심청 노드의 다음 포인터를 NULL로

만들어 그 이후에 데이터가 없음을 표시한다.

그림 11-3(c): 홍길동의 데이터를 입력한 후, 홍길동의 성적이 심청의 성적보다 높으므로 리스

트 헤드에 홍길동 노드를 연결한다. 홍길동 노드의 다음 포인터는 심청 노드를 가리키도록

만든다.

그림 11-3(d): 성춘향의 데이터를 입력한 후, 점수가 가장 낮으므로 리스트의 마지막에 연결

한다. 심청 노드의 다음 포인터를 성춘향 노드를 가리키도록 만들고, 성춘향 노드의 다음 포

인터의 값을 NULL로 설정하여 그 이후에 데이터가 없음을 표시한다.

11.1.4 프로그램 구조 설계

성적 정렬 프로그램은 가장 간단한 형태인 데이터 입력, 처리, 결과 출력의 형태이다. 입력 받

은 데이터를 그대로 출력하므로, 데이터를 입력 받으면서 정렬한다면 특별한 처리 과정도 없다.

다만, 데이터를 저장하는 과정이 복잡할 뿐이다. 프로그램의 구조는 리스트 11-1과 같이 설계하면

충분하다.

<리스트 11-1> 성적 정렬 프로그램

7. 학생 정보를 입력 받고 정렬하여 저장한다.

8. 정렬 결과를 출력한다.

리스트 헤드를 전역변수로 사용하고, 각 라인을 다음과 같은 함수로 구현해 보자.

void get_strudentinfo(): 학생 정보를 입력 받고 연결리스트에 저장한다.

void print_list(): 연결리스트에 저장되어 있는 학생 정보를 출력한다.

처음으로 구현한 프로그램은 다음과 같다.

<리스트 11-2> 성적 정렬 프로그램의 구조

1. #include <stdio.h>

2. typedef struct STUDENT_INFO SINFO; // 구조체 형식 재정의

3. void get_strudentinfo(); // 함수 선언

4. void print_list();

5. void delete_list();

6. struct STUDENT_INFO // 구조체 선언

7. {

8. char id[16];

9. char name[16];

10. int score;

Page 259: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

257

11. struct STUDENT_INFO *next;

12. };

13. SINFO *listhead = NULL; // 리스트 헤드 선언 및 초기화

14. void main()

15. {

16. get_studentinfo(); // 학생 정보를 저장하고

17. print_list(); // 결과를 출력하고

18. }

19. void get_strudentinfo() { } // 비어 있는 함수 구현

20. void print_list() { }

21. void delete_list() { }

라인 2의 typedef 문은 자료형을 재정의하는 방법이며, 기존의 자료형을 다른 이름으로 정의한

다. typedef 문의 예는 다음과 같다.

typedef long time_t; // long 형을 time_t 형으로 다시 정의한다.

typedef int AGE; // int 형을 AGE라는 이름의 자료형으로 정의한다.

typedef struct STUDENTINFO SINFO; // struct STUDENTINFO를 SINFO 형으로 정의한다.

이와 같이 자료형을 재정의하면, time_t, AGE, SINFO 형 변수를 선언할 수 있다. typedef 문을 사

용함으로써 자료형에 대한 변수의 의미가 명확해지는 장점이 있다. 구조체를 재정의하면 문장의

길이가 짧아지는 장점이 있다. 라인 2에서 자료형을 재정의하였기 때문에, 프로그램에서 struct

STUDENT_INFO 대신에 SINFO를 사용할 수 있다. 만일 typedef 문을 정의하지 않았다면, 라인 12

를 다음과 같이 적어야 한다.

struct STDUENTINFO *listhead = NULL;

성적 정렬 프로그램의 핵심은 데이터를 입력 받고 연결리스트에 정렬하여 저장하는 부분이다.

프로그램을 다음과 같은 순서로 개발해 보자.

11.1.5 학생 데이터 입력: 안내문을 출력하고 학생 데이터를 입력한다.

11.1.6 연결리스트 구축: 일단 노드를 내림차순으로 정렬하지 않고 사용자가 입력한 순서대로

연결리스트를 구축한다.

11.1.7 연결리스트 해제: 연결리스트를 만들 할당 받은 기억 장소를 해제하는 부분을 추가한

다.

11.1.8 연결리스트 출력: 연결리스트에 저장된 학생 정보를 출력한다.

11.1.9 연결리스트 정렬: 데이터를 입력 받으면서 연결리스트에 성적 내림차순으로 정렬하면

Page 260: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

258

서 저장하도록 수정한다.

11.1.5 학생 데이터 입력

학생의 정보를 입력 받는 부분을 생각해 보자. 학생 정보를 여러 번 입력 받아야 하므로 알고

리즘을 다음과 같이 설계할 수 있다.

<리스트 11-3> 학생 정보 입력

1. 안내문을 출력한다.

2. 다음을 반복한다.

3. 학번을 입력 받는다.

4. 학번이 –1이 아니면

5. 이름을 입력 받는다.

6. 성적을 입력 받는다.

7. 학생 정보를 정렬하여 저장한다.

8. 학번이 -1이면

9. 입력을 중단한다.

라인 7의 학생 정보를 정렬하여 저장하는 부분을 뒤로 미루고, 프로그램을 구현하면 다음과 같다.

<리스트 11-4> get_studentinfo() 함수 구현

1. void get_studentinfo()

2. {

3. SINFO student; // 학생 정보를 입력 받기 위한 임시 변수

4. printf("학생 정보를 입력하세요.\n"); // 안내문을 출력한다.

5. printf("입력을 마치려면 학번에 -1을 넣으세요.\n\n");

6. while (1) // 다음을 반복한다.

7. {

8. printf("학번: "); // 학번을 입력 받는다.

9. scanf("%s", student.id); // id가 문자열이므로 &를 붙이지 않는다.

10. if (strcmp(student.id, "-1") != 0) // 학번이 –1이 아니면

11. {

12. printf("이름: "); // 이름을 입력 받는다.

13. scanf("%s", student.name);

14. printf("성적: "); // 성적을 입력 받는다.

15. scanf("%d", &student.score);

16. // 학생 정보를 정렬하여 저장한다.

17. printf("\n");

18. }

19. else // 학번이 -1이면

Page 261: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

259

20. break; // 입력을 중단한다.

21. } // end of while

22. }

프로그램을 컴파일하고 실행해 보면, 입력 화면 설계와 같이 학번에 -1을 입력할 때까지 계속

해서 학생정보를 입력 받는 것을 확인할 수 있다. 그렇지만, 아직까지 입력 받은 데이터를 저장하

지 않는다.

11.1.6 연결리스트 구축

학생 정보를 저장할 때, 연결리스트를 사용하라는 것이 문제의 요구사항이다. 일단 노드를 내림

차순으로 정렬하지 않고 사용자가 입력한 순서대로 연결리스트의 앞에 저장해 보자. 그림 11-4로

노드를 리스트 헤드에 추가하는 과정을 설명한다.

<그림 11-4> 연결리스트의 헤드에 노드 삽입 과정

그림 11-4(a): 현재 연결리스트가 구축되어 있다. 여기에, temp가 가리키는 노드를 제일 앞에

추가할 것이다.

그림 11-4(b): 다음과 같은 두 단계로 temp가 가리키는 노드를 리스트 헤드에 추가한다.

1. temp->next = head; // temp->next를 head가 가리키는 노드로 연결한다.

2. head = temp; //리스트 헤드를 temp가 가리키는 노드로 변경한다.

위 과정에서 노드를 추가하는 순서가 매우 중요하다. 만일 두 번째 단계를 먼저 실행한다면, head

가 가리키던 노드에 대한 포인터를 잃어버리기 때문이다.

학생 정보를 저장하는 기능이 제법 복잡하므로 다음과 같은 별도의 함수로 만들자.

void insert_node(SINFO *student): 포인터 student가 가리키는 노드를 연결리스트에 추가한다.

이 함수의 알고리즘을 다음과 같이 설계하고 구현할 수 있다.

<리스트 11-5> 학생 정보 저장

1. 노드를 동적으로 하나 생성한다. // 프로그램이 종료되기 전에 해제되어야 한다.

Page 262: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

260

2. 새로 생성한 노드에 학생 정보를 복사한다.

3. 연결리스트의 헤드에 생성한 노드를 삽입한다.

<리스트 11-6> insert_node() 함수 구현

1. #include <string.h> // 문자열 함수 선언

2. #include <stdlib.h> // 기억 장소 할당 및 해제 함수 선언

3. // typedef 문 이후에

4. void insert_node(SINFO *student); // 함수 선언 추가

5. void get_studentinfo()

6. {

7. …

8. insert_node(&student); // 리스트 11-4 라인 16을 함수 호출로 변경

9. …

10. }

11. void insert_node(SINFO *student) // 함수 구현

12. {

13. SINFO *temp = (SINFO *)malloc(sizeof(SINFO)); // 동적으로 노드를 생성한다.

14. strcpy(temp->id, student->id); // 학생 데이터를 복사한다.

15. strcpy(temp->name, student->name);

16. temp->score = student->score;

17. temp->next = listhead; // 리스트의 앞에 삽입한다.

18. listhead = temp;

19. }

라인 4: 함수를 선언할 때 SINFO를 사용하므로, typedef 문은 그 전에 위치하여야 한다.

라인 8: get_studentinfo() 함수에서 데이터를 입력 받은 후, insert_node() 함수를 호출한다.

라인 13: 만일 SINFO를 정의하지 않았다면, 다음과 같이 작성해야 한다.

struct STUDENTINFO *temp = (struct STUDENT_INFO *)malloc(sizeof(struct STUDENT_INFO));

라인 14, 15: 문자열을 복사한다. 문자열은 할당문(=)으로 복사하지 못하고 strcpy() 함수로 문

자열을 복사해야 한다.

라인 14~17: 구조체 포인터를 통하여 구조체의 멤버 변수를 참조하려면 ‘.’ 연산자 대신 ‘->’

연산자를 사용해야 한다.

11.1.7 연결리스트 해제

학생 정보를 입력한 후 연결리스트를 만들기 위하여 노드를 동적으로 생성한다. 따라서, 입력이

완료되면, 노드가 학생의 수만큼 할당된다. 그러므로, 프로그램이 종료되기 전에 할당 받은 노드

Page 263: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

261

들을 모두 해제하여야 한다. 프로그램을 작성할 때, 기억 장소를 할당 받았다면, 즉시 해제할 것

을 고려해야 한다. 그러므로 연결리스트의 노드를 해제하는 함수를 먼저 작성하는 것이 좋다.

그림 11-5는 연결리스트의 첫 번째 노드를 해제하는 과정을 보여준다.

<그림 11-5> 연결리스트의 노드 해제 과정

그림 11-5(a): 리스트 헤드 head에 네 개의 노드가 연결되어 있다. 임시 포인터 temp를 첫

번째 노드로 연결한다.

그림 11-5(b): 다음과 같은 과정을 통하여 temp가 가리키는 노드를 해제한다.

1. head = head->next; // head를 다음 노드를 가리키도록 옮긴다.

2. free(temp); // temp가 가리키는 노드를 해제한다.

head가 가리키는 노드가 NULL일 때까지, 이와 같은 과정을 반복함으로써 전체 노드를 해제할

수 있다. 연결리스트를 해제하는 알고리즘은 다음과 같다.

<리스트 11-7> 연결리스트 노드 해제 알고리즘

1. 포인터 변수 temp를 첫 번째 노드를 가리키도록 설정한다. (temp = head)

2. head가 NULL이 아니면 다음을 반복한다.

3. head를 다음 노드를 가리키도록 이동한다. (head = head->next)

4. temp가 가리키는 노드를 해제한다.

5. temp를 head가 가리키는 노드를 가리키도록 설정한다. (temp = head)

이 기능을 delete_list() 함수로 만들자. 함수의 구현은 다음과 같다.

<리스트 11-8> delete_list() 함수 구현

1. void delete_list(); // 함수 선언 추가

2. void main()

3. {

4. …

5. delete_list(); // main() 함수의 마지막에 추가한다.

Page 264: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

262

6. }

7. void delete_list() // 함수 구현

8. {

9. SINFO *temp = listhead; // temp를 초기화 한다.

10. while (listhead != NULL) // 연결리스트의 끝에 도달할 때까지

11. {

12. listhead = listhead->next; // 리스트 헤드를 다음 노드로 옮기고

13. free(temp); // temp가 가리키는 노드를 해제한다.

14. temp = listhead; // temp를 수정한다.

15. }

16. }

11.1.8 연결리스트 출력

지금까지 학생 정보를 입력 받고 노드를 생성하여 연결리스트의 처음에 저장하는 과정까지 구

현하였다. 연결리스트를 정렬하여 저장하기 전에, 데이터가 올바로 저장되었는지 확인하기 위하여,

먼저 학생 정보를 출력해 보자.

연결리스트에 저장되어 있는 데이터를 출력하려면, 연결리스트의 첫 번째 노드부터 마지막 노

드까지 리스트를 따라가면서 각 노드를 방문하는 구조체 포인터 변수가 필요하다. 이 변수를

search라고 하자. search를 리스트 헤드로 초기화 하고, search가 널이 아닐 때까지 계속 다음 노

드로 이동하여 노드를 방문한다.

<그림 11-6> 노드 방문 과정

연결리스트 출력 알고리즘과 구현은 다음과 같다.

<리스트 11-9> 연결리스트 출력 알고리즘

1. search를 연결리스트의 첫 번째 노드를 가리키도록 설정한다. (search = head)

2. search가 NULL이 될 때까지 다음을 반복한다.

3. search가 가리키는 데이터를 출력하고

4. search를 다음 노드를 가리키도록 수정한다. (search = search->next)

Page 265: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

263

<리스트 11-10> print_list() 함수 구현

1. void print_list()

2. {

3. SINFO *search; // 연결리스트를 추적하기 위한 포인터

4. search = listhead; // 포인터 초기화

5. printf("\n%16s%16s%6s\n", "학번", "이름", "성적");

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

7. while(search != NULL) // 리스트의 끝까지

8. {

9. printf("%16s", search->id); // 데이터를 출력하고

10. printf("%16s", search->name);

11. printf("%6d\n", search->score);

12. search = search->next; // 다음 노드로 이동한다.

13. }

14. }

<그림 11-7> 학생 정보 출력 화면

프로그램을 컴파일하고 실행하면, 그림 11-7과 같은 출력을 볼 수 있다. 학생 데이터를 연결리

스트의 앞에 추가하였으므로, 입력한 순서의 역순으로 출력되는 것을 볼 수 있다.

Page 266: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

264

11.1.9 연결리스트 정렬

마지막으로 insert_node() 함수를 성적의 내림차순으로 학생 데이터를 저장할 수 있도록 수정해

보자. 학생 정보를 입력 받으면서, 정렬하여 저장하려면 다음과 같은 과정을 거쳐야 한다.

연결리스트를 추적하여 노드를 삽입할 위치를 찾는다.

해당 위치에 노드를 삽입한다.

노드를 삽입할 위치를 탐색하는 방법은 그림 8-6과 같은 방법으로 각 노드를 방문하면서 노드

의 값을 삽입할 노드의 값과 비교해 보면 알 수 있다. 그러므로 연결리스트에 데이터를 정렬하여

저장하는 알고리즘을 다음과 같이 설계할 수 있다.

<리스트 11-11> 노드 삽입 알고리즘

1. search를 리스트의 첫 번째 노드를 가리키도록 만든다. (search = head)

2. search가 리스트의 끝(NULL)이 아니면 다음을 반복한다.

3. 학생의 성적이 search가 가리키는 노드의 성적보다 작으면

4. search를 다음 노드를 가리키도록 변경한다. (search = search->next)

5. 그렇지 않으면

6. 반복문을 벗어난다.

7. search가 가리키는 노드 앞에 새로운 노드를 추가한다.

이 알고리즘에서 노드를 따라가는 포인터 search는 분명히 추가할 노드보다 성적이 낮은 노드

를 가리킨다. 그렇지만 search가 가리키는 노드 앞에 새로운 노드를 추가할 때 문제가 발생한다.

리스트 11-11의 라인 4에서 포인터 search의 값을 다음 노드로 옮기는 순간에 이전 노드에 대한

포인터를 잃어버리기 때문이다.

이 문제를 해결하기 위하여, search가 가리키는 노드의 앞에 있는 노드를 가리키는 포인터가 하

나 더 필요해진다. 이 포인터 변수를 previous라고 하자. 이해를 돕기 위하여 그림 11-8을 보자.

그림 11-8(a): 탐색 초기에 previous=NULL, search=head로 설정한다. 만일 값이 90인 노드를

삽입하려고 한다면, 이 노드는 연결리스트의 첫 번째 위치에 추가되어야 한다.

그림 11-8(b): 만일 값이 75인 노드를 삽입하려고 한다면, 이 노드는 두 번째와 세 번째 노드

사이에 추가되어야 한다.

Page 267: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

265

<그림 11-8> 노드 삽입을 위한 연결리스트 탐색

<그림 11-9> 노드 삽입 과정

이와 같은 분석 결과를 반영하여 개선한 노드 삽입 알고리즘은 다음과 같다.

<리스트 11-12> 개선한 노드 삽입 알고리즘

1. search를 리스트의 첫 번째 노드를 가리키도록 만든다. (search = head)

2. previous를 NULL로 만든다. (previous = NULL)

3. search가 리스트의 끝(NULL)이 아니면 다음을 반복한다.

4. 학생의 성적이 search가 가리키는 노드의 성적보다 작으면

5. previous를 search가 가리키는 노드를 가리키도록 변경한다. (previous = search)

6. search를 다음 노드를 가리키도록 변경한다. (search = search->next)

7. 그렇지 않으면

8. 반복문을 벗어난다.

9. previous가 NULL이면

Page 268: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

266

10. 연결리스트의 맨 앞에 노드를 추가한다.

11. 그렇지 않으면

12. previous와 search가 가리키는 노드 사이에 새로운 노드를 삽입한다.

리스트 5의 삽입 알고리즘에서 노드를 추가하는 방법은 노드 삽입 위치에 따라 다르다. 그림

11-9는 포인터 temp가 가리키는 노드를 삽입 과정을 보여준다.

연결리스트의 맨 앞에 노드를 추가하는 방법 (이 때, previous=NULL)

1. temp->next = search;

2. head = temp;

연결리스트의 중간 혹은 끝에 노드를 추가하는 방법

1. temp->next = search;

2. previous->next = temp;

알고리즘 개발이 완료되었으므로, insert_node() 함수를 다음과 같이 수정한다.

<리스트 11-13> 노드 삽입 알고리즘

1. void insert_node(SINFO *student)

2. {

3. SINFO *search, *previous; // 탐색을 위한 노드 포인터

4. SINFO *temp = (SINFO *)malloc(sizeof(SINFO)); // 노드 할당

5. strcpy(temp->id, student->id); // 노드 복사

6. strcpy(temp->name, student->name);

7. temp->score = student->score;

8. //temp->next = listhead; // 임시로 작성하였던 두 라인을

9. //listhead = temp; // 삭제한다.

10. search = listhead; // search 초기화

11. previous = NULL; // previous 초기화

12. while (search != NULL)

13. {

14. if (temp->score < search->score) // 학생의 성적이 노드보다 작으면

15. {

16. previous = search; // 다음 노드로 이동

17. search = search->next;

18. }

19. else // 삽입 위치를 찾았으면

Page 269: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

267

20. break; // 반복문을 벗어난다.

21. }

22. if (previous == NULL) // 삽입 위치가 맨 앞인 경우

23. {

24. temp->next = listhead;

25. listhead = temp;

26. }

27. else // 삽입 위치가 중간 혹은 끝인 경우

28. {

29. temp->next = search;

30. previous->next = temp;

31. }

32. }

<그림 11-10> 성적 정렬 실행 결과

프로그램이 완성되었다. 컴파일하고 실행해 보면 그림 11-10과 같이 여러 명의 데이터를 입력

하더라도 성적으로 정렬된 결과를 볼 수 있다.

지금까지 연결리스트를 사용하기 위한 노드 삽입, 연결리스트의 노드 방문, 할당 받은 노드 해

제 알고리즘을 설명하였다. 연결리스트를 사용할 때는 포인터 변수들을 많이 사용하는데, 포인터

변수들이 노드를 잃어버리지 않도록 주의해야 한다.

Page 270: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

268

11.2 기억력 게임 2

화면에 새로 표시되는 영문자를 맞추는 기억력 게임 프로그램을 연결리스트를 사용하여 구현하

라.

11.2.1 요구사항 분석

프로그램의 전체적인 구조와 흐름은 “10.1 기억력 게임”과 같고, 다만 영문자를 screen[][] 배열

에 저장하고 표시하는 부분을 연결리스트로 변경하는 문제이다. 기억력 게임 프로젝트를 열고, 그

안에 포함되어 있는 함수들의 기능을 정리하면 다음과 같다.

void main(): 전체적인 프로그램의 흐름을 제어한다.

void initailize(): 난수 발생기를 초기화하고, 커서를 지운다.

void gotoxy(int, int): 커서를 임의의 위치로 이동한다.

void cursor_off(): 화면에서 커서를 보이지 않게 만든다.

void play_game(): 게임의 흐름을 제어한다.

void sleep(long): 프로그램을 지정한 시간만큼 지연한다.

char get_alphabet(): 영문자와 영문자를 화면에 표시할 좌표를 생성한다.

void display_screen(): 영문자를 화면에 표시한다.

void clear_screen(): 화면에 표시된 영문자를 지운다.

void clear_array(): screen[][] 배열을 초기화 한다.

이 함수들 중에서 get_alphabet(), display_screen(), 그리고 clear_array() 함수가 screen[][] 배열을

사용한다. 자료구조를 수정하면, clear_screen() 함수도 수정할 여지가 있다. 그러므로, 영문자를 저

장하는 이차원 배열을 연결리스트로 교체하고, 이 세 개의 함수들을 수정함으로써 이 문제를 해

결해 보자.

이미 구현해 보았던 프로그램이므로 프로그램의 전체적인 흐름은 동일하다. 그러므로 프로그램

을 복사하고, 필요한 부분만 수정해 보자. 다음과 같이 수정할 소스 코드를 준비한다.

1. 새로운 프로젝트(memorylist)를 생성하고, 비어 있는 소스 파일(memorylist.c)을 생성한다.

2. 기억력 게임에서 작성하였던 소스 파일을 그대로 복사한다.

프로그램을 준비한 후, 다음과 같은 순서로 함수들을 수정해 보자.

11.2.2 자료구조 설계: 연결리스트를 구현하기 위한 구조체를 결정한다.

11.2.3 화면 표시 함수: display_screen() 함수와 clear_screen() 함수를 수정한다.

11.2.4 영문자 제거 함수: clear_array() 함수를 수정한다.

11.2.5 영문자 생성 함수: get_alphabet() 함수를 수정한다.

Page 271: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

269

11.2.2 자료구조 설계

자료구조를 설계해 보자. 기억력 게임은 영문자를 임의의 위치에 표시한다. 그러므로 그림 11-

11과 같이 데이터 영역이 {x 좌표, y 좌표, 영문자}이고 다음 포인터를 갖는 노드들의 연결리스트

로 자료구조를 표현할 수 있다.

<그림 11-11> 기억력 게임을 위한 연결리스트

소스 코드의 전역 변수 screen[][] 선언을 삭제하고, 다음의 자료구조를 추가한다. 리스트 헤드

역할을 하는 alpahlist를 전역 변수로 선언하고 NULL로 초기화 한다.

<리스트 11-14> 자료구조 정의 및 리스트 헤드 선언

1. struct ALPHA_NODE

2. {

3. int x; // x 좌표

4. int y; // y 좌표

5. char alpha; // {x, y}에 표시할 영문자

6. struct ALPHS_NODE *next; // 다음 노드 포인터

7. };

8. struct ALPHA_NODE alphalist = NULL; // 리스트 헤드

“11.1 성적 정렬” 예제에서는 typedef 문을 사용하여 구조체를 재정의하여 사용하였으나, 이 예

제에서는 typedef 문을 사용하지 않고 사용할 때와 사용하지 않을 때의 차이점을 살펴 보기로 한

다.

11.2.3 화면 표시 함수

화면에 영문자를 표시하는 display_screen() 함수는 screen[][] 배열을 검색하여, 영문자가 존재하

면 해당 위치에 영문자를 출력하는 기능을 수행한다. 연결리스트를 사용한다면, 배열을 검색하는

대신에 연결리스트에 포함되어 있는 노드를 추적하면서, 노드에 표현되어 있는 영문자를 표시하

면 된다. 화면 표시 알고리즘과 구현은 다음과 같다.

<리스트 11-15> 화면 표시 알고리즘

1. 임시 변수 search를 리스트의 첫 번째 노드로 설정한다.

2. search가 가리키는 노드가 리스트의 끝이 아니면

Page 272: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

270

3. 커서를 (x, y) 좌표로 옮기고

4. 영문자를 출력한다.

5. search를 다음 노드로 이동한다.

<리스트 11-16> display_screen() 함수 구현

1. void display_screen()

2. {

3. struct ALPHA_NODE *search = alphalist;

4. while (search != NULL)

5. {

6. gotoxy(search->x, search->y);

7. putch(search->alpha);

8. search = search->next;

9. }

10. }

화면을 지우는 clear_screen() 함수는 비록 screen[][] 배열을 사용하지는 않지만, 화면의 이중 루

프를 수행하면서 모든 위치를 이동하면서 공백(‘ ‘)을 출력한다. 이 동작이 비효율적이므로, 영문자

를 표시한 위치만 공백을 출력하도록 변경해 보자. 알고리즘은 display_screen() 함수와 같고, 다만

문자를 표시하는 대신에 공백을 출력하면 된다. 구현은 다음과 같다.

<리스트 11-17> clear_screen() 함수 구현

1. void clear_screen()

2. {

3. struct ALPHA_NODE *search = alphalist;

4. while (search != NULL)

5. {

6. gotoxy(search->x, search->y);

7. putch(‘ ‘);

8. search = search->next;

9. }

10. }

기존의 clear_screen() 함수는 화면 전체를 지우는 반면에, 수정한 함수는 화면에 영문자가 표시

되는 위치만 지운다. 함수를 수정하면, 한 번의 게임이 끝나고 게임을 다시 시작할 때 이전 게임

에서 생성되었던 영문자들이 화면에 그대로 남아 있게 된다. 그러므로, paly_game() 함수에서 리

턴하기 전에 clear_screen() 함수를 호출해 주어야 한다.

Page 273: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

271

<리스트 11-18> play_game() 함수 수정

1. void play_game()

2. {

3. …

4. clear_screen(); // 리턴하기 전에 함수 호출

5. }

11.2.4 영문자 제거 함수

배열에서 영문자를 지우는 clear_array() 함수는 screen[][] 배열을 0으로 초기화 하는 기능을 수

행한다. 연결리스트를 사용하는 경우에, alphalist에 연결되어 있는 노드를 모두 해제하도록 이 함

수의 기능을 대체 할 수 있다. 즉, “11.1 성적 정렬” 예제에서 학습하였던 노드를 해제하는

delete_list()와 그 역할이 같다.

사용하는 자료구조가 배열이 아니고 연결리스트이므로 함수의 이름을 clear_list()로 변경하자.

이에 따라 함수 선언 부분과 프로그램의 다른 부분에서 clear_array()를 호출하는 부분도 clear_list()

로 변경한다. “11.1.7 연결리스트 해제”의 노드 해제 함수를 참고하여 다음과 같이 clear_list() 함수

를 구현한다.

<리스트 11-19> clear_list() 함수 구현

1. void clear_list()

2. {

3. struct ALPHS_NODE *temp = alphalist; // 삭제할 노드 포인터

4. while (alphalist != NULL) // 마지막 노드가 아니면

5. {

6. alphalist = alphalist ->next; // 다음 노드로 포인터를 옮기고

7. free(temp); // 노드를 해제한다.

8. temp = alphalist; // 다음에 노드를 해제할 준비를 한다.

9. }

10. }

프로그램이 종료할 때에도 동적으로 할당 받은 노드들을 해제해야 하므로, main() 프로그램의

마지막 부분에 clear_list() 함수를 호출하는 문장을 추가한다.

<리스트 11-20> main()의 마지막에 노드 해제 추가

1. void main()

2. {

3. …

4. clear_list(); // 프로그램이 끝나기 전에 노드 해제

Page 274: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

272

5. }

11.2.5 영문자 생성 함수

마지막으로 수정할 함수는 화면의 임의의 위치에 표시할 영문자를 생성하는 get_alphabet() 함

수이다. 이 함수는 {x, y} 좌표와 영문자를 난수로 생성하고 이차원 배열 screen[y][x]에 생성한 영

문자를 저장한다. 단 새로 영문자를 표시할 {x, y} 좌표는 기존에 존재하지 않아야 한다. 따라서,

다음과 같이 두 부분을 수정해야 한다.

생성된 좌표에 영문자가 이미 존재하는지 검사하는 부분

생성된 좌표에 영문자를 저장하는 부분

첫 번째 부분은 연결리스트를 추적하면서, 좌표가 {x, y}인 노드가 존재하는지 검사해야 한다. 두

번째 부분은 새로운 노드를 할당하고, 영문자를 노드에 저장하고, 연결리스트의 맨 앞에 노드를

추가하는 과정으로 구현될 수 있다. 전체적인 알고리즘은 다음과 같다.

<리스트 11-21> 영문자 생성 알고리즘

1. {x, y} 좌표를 생성한다.

2. 만일 연결리스트에 {x, y} 좌표가 들어 있다면 라인 1을 반복한다.

3. 영문자를 생성한다.

4. 노드를 동적으로 할당한다.

5. 노드에 {x, y} 좌표와 영문자를 저장한다.

6. 노드를 연결리스트의 맨 앞에 연결한다.

리스트 11-21의 라인 2에서 좌표를 검사하는 부분을 더 구체화해야 한다. 이 부분을 다음과 같

이 별도의 함수로 구현하기로 한다.

int check_node(int x, int y)

파라미터

int x, int y: 노드가 존재하는지 검사할 좌표

리턴

만일 해당 위치에 노드가 존재하면 1

존재하지 않으면 0

이 함수는 연결리스트를 추적하면서 노드의 좌표를 검사하도록 구현할 수 있다. 그 알고리즘은

다음과 같다.

<리스트 11-22> 노드 검사 알고리즘

1. 임시 포인터 변수 search를 연결리스트의 첫 번째 노드로 설정한다.

2. 연결리스트의 끝까지 다음을 반복한다.

3. 만일 search가 가리키는 노드의 좌표가 새로 생성한 좌표 {x, y}와 같다면

Page 275: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

273

4. 루프를 벗어난다.

5. 만일 search가 NULL이면 0을 리턴하고, 그렇지 않으면 1을 리턴한다.

라인 5에서 search가 NULL이면 연결리스트의 마지막까지 도달한 것이므로, 노드를 찾지 못한 경

우에 해당한다. 함수 get_alphabet()과 check_node()를 구현한 소스 코드는 다음과 같다.

<리스트 11-23> 영문자 생성

1. int check_node(int x, int y); // 헤더 부분에 함수 선언 추가

2. char get_alphabet() // 리스트 11-21 구현

3. {

4. int y, x; // 영문자의 좌표

5. char alpha; // 영문자

6. struct ALPHA_NODE *temp; // 영문자를 저장할 노드

7. do {

8. x = rand() % 80; // x 좌표 생성

9. y = rand() % 24; // y 좌표 생성

10. } while (check_node(x, y) == 1); // 기존 노드에 {x, y}가 존재하면 반복

11. alpha = (rand()%26) + 'A'; // 영문자 생성

12. temp = (struct ALPHA_NODE *)malloc(sizeof(struct ALPHA_NODE)); // 노드 할당

13. temp->x = x; // 노드에 좌표 저장

14. temp->y = y;

15. temp->alpha = alpha; // 노드에 영문자 저장

16. temp->next = alphalist; // 연결리스트의 맨 앞에 노드 연결

17. alphalist = temp;

18. return alpha; // 생성된 영문자 리턴

19. }

20. int check_node(int x, int y) // 리스트 11-22 구현

21. {

22. struct ALPHA_NODE *search= alphalist; // 추적할 포인터

23. while (search!= NULL) // 연결리스트의 끝까지

24. {

25. if ((search->x == x) && (search->y == y)) // {x, y} 좌표가 존재하면

26. break; // 루프를 벗어난다.

27. else // 같지 않으면

Page 276: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

274

28. search = search->next; // 다음 노드를 검사한다.

29. }

30. return (search== NULL) ? 0 : 1; // 노드가 존재하면 1, 존재하지 않으면 0을 리턴

31. }

라인 10: check_node()가 리턴한 값을 직접 1과 비교한다.

라인 12: 구조체 형을 재정의하지 않았기 때문에 구조체 이름을 모두 적어야 한다.

프로그램 수정이 완료되었다. “11.1 기억력 게임”에서 작성하였던 프로그램과 실행 결과는 같다.

11.3 학생 관리

학생에 대한 정보를 관리하는 메뉴 기반 프로그램을 작성하라. 학생에 대하여 학과, 학번, 이름,

성적을 관리한다. 관리 기능은 다음과 같다.

1. 자료추가: 한 명의 학생에 대한 정보를 입력 받아 저장한다.

2. 목록검색: 학번 또는 이름을 입력 받고 해당 학생에 대한 정보를 출력한다.

3. 목록삭제: 학번 이름을 입력 받고 해당 학생에 대한 정보를 제거한다.

4. 목록정렬: 학과 또는 학번으로 데이터를 정렬한다.

5. 목록보기: 전체 학생에 대한 데이터를 출력한다.

6. 메뉴보기: 메뉴를 다시 출력한다.

7. 종료: 프로그램을 종료한다.

프로그램이 종료할 때, 학생 데이터를 student.dat 파일로 저장하고, 프로그램이 시작할 때 파일에

서 학생 데이터를 읽도록 프로그램을 작성하라.

11.3.1 요구사항 분석

이 문제는 프로그램이 한 가지 기능만 수행하는 것이 아니고, 사용자에게 메뉴를 제시하고 사

용자의 선택에 따라 여러 가지 일을 수행한다. 기능 별로 프로그램의 화면 처리를 생각해 보자.

프로그램 시작: 학생 데이터를 읽어 내부에 저장하고 그림 11-12(a)와 같은 메뉴를 출력한다.

자료추가: 사용자가 1번 기능을 선택하면, 그림 11-12(b)와 같이 학생에 대한 학과, 학번, 이

름, 성적을 입력 받고 학생 목록에 데이터를 저장한다.

목록검색: 사용자가 2번 기능을 선택하면, 그림 11-12(c)와 같이 학번 또는 이름으로 검색할

지 물어본다. 사용자가 검색 조건으로 이름을 선택하였다면, 이름을 입력 받고, 학생 목록에

서 사용자가 입력한 학생을 검색하여 데이터를 출력한다. 만일 목록에 학생 데이터가 존재하

지 않는다면 존재하지 않음을 출력해야 한다.

목록삭제: 사용자가 3번 기능을 선택하면, 그림 11-12(d)와 같이 해당 학생의 데이터를 삭제

한다.

목록정렬: 사용자가 4번 기능을 선택하면, 그림 11-12(e)와 같이 정렬 기준을 물어본 후, 사용

Page 277: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

275

자가 지정한 정력 기준에 따라 학생 데이터를 정렬한다.

목록보기: 사용자가 5번 기능을 선택하였다면, 그림 11-12(f)와 같이 저장되어 있는 학생 데이

터를 모두 출력한다.

메뉴보기: 사용자가 6번 기능을 선택하면, 그림 11-12(a)의 메뉴를 다시 표시한다.

종료: 사용자가 7번을 선택하여 프로그램을 종료하면서 학생 데이터를 파일로 저장한다.

그리고 1, 2, 3, 4 번 기능을 수행한 후에는 다시 메뉴를 표시하여, 사용자에게 다음 명령을 입력

할 것을 요구한다.

<그림 11-12> 학생 관리 화면 처리

11.3.2 프로그램 설계 및 구현

이 프로그램은 전형적인 메뉴 기반 프로그램이다. 프로그램의 전체적인 흐름은 다음과 같이 메

뉴를 출력하고, 사용자가 선택한 기능을 실행하는 형태이다.

<리스트 11-24> 학생 관리 프로그램

1. 파일에서 데이터를 읽는다.

Page 278: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

276

2. 다음을 반복한다.

3. 메뉴를 출력하고, 사용자로부터 수행할 기능을 입력 받는다.

4. 사용자의 선택에 따라 해당 기능을 수행한다.

5. 데이터를 파일에 저장한다

프로그램이 처리할 일이 많으므로, 다음과 같은 순서로 각 기능을 설계하고 구현해 보자.

11.3.3 메뉴출력: 먼저 메뉴를 출력하고, 사용자로부터 명령을 받는 부분을 처리한다.

11.3.4 자료추가: 자료구조를 결정하고, 학생 데이터를 저장하는 부분을 처리한다.

11.3.5 목록보기: 학생 데이터를 출력하는 기능을 구현하여 학생 데이터가 연결리스트에 올바로

저장되는지 확인한다.

11.3.6 파일저장 및 읽기: 프로그램이 종료할 때 연결리스트에 저장되어 있는 데이터를 모두 저장

하고, 프로그램이 시작할 때 학생 데이터를 읽어 연결리스트를 구성한다.

11.3.7 목록검색: 학생 데이터를 검색하여 출력하는 기능을 구현한다.

11.3.8 목록정렬: 사용자의 요구에 따라 목록을 검색하는 기능을 구현한다.

11.3.9 목록삭제: 학생 이름을 입력 받아 연결리스트에서 학생 데이터를 삭제한다.

<그림 11-13> 프로젝트 생성

Page 279: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

277

헤더 파일과 소스 파일을 분리하여 프로그램을 작성해 보자. 그림 11-13과 같이 프로젝트

(studentmgnt)를 생성하고, 비어 있는 헤더 파일(studentmgnt.h)과 소스 파일(studentmgnt.c)을 만

든다. 헤더 파일에는 #include 문과 구조체 정의 그리고 함수 선언을 작성하고, 소스 파일에는 전

역변수 선언과 함수 구현을 작성한다.

11.3.3 메뉴출력

제일 먼저 메뉴를 출력하고 사용자로부터 처리할 명령을 입력 받는 함수를 작성하자. 이 함수

는 간단하므로 바로 다음과 같이 작성한다.

<리스트 11-25> 프로그램의 흐름 및 메뉴 출력 함수

// studentmgnt.h 헤더 파일

1. #include <stdio.h>

2. // 함수 선언

3. char print_menu();

// studentmgnt.c 소스 파일

1. #include "studentmgnt.h" // 헤더 파일 포함

2. void main()

3. {

4. char menu = 0; // 사용자 명령

5. while (menu != '7') // 사용자 명령이 종료가 아니면 반복

6. {

7. menu = print_menu(); // 메뉴 출력 및 명령 받기

8. printf("%c 번 기능을 선택하였습니다.\n", menu); // 임시로 명령 출력

9. }

10. }

11. char print_menu()

12. {

13. char menu; // 지역 변수

14. printf("처리할 기능의 번호를 선택하세요.\n\n"); // 메뉴 출력

15. printf("1. 자료추가\n");

16. printf("2. 목록검색\n");

17. printf("3. 목록삭제\n");

18. printf("4. 목록정렬\n");

19. printf("5. 목록보기\n");

20. printf("6. 메뉴보기\n");

Page 280: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

278

21. printf("7. 종료\n\n");

22. printf("선택 > "); // 프롬프트 출력

23. menu = getchar(); // 사용자 명령 받기

24. getchar(); // 엔터키 제거

25. return menu;

26. }

라인 1: 사용자가 만든 헤더 파일을 포함할 때, <> 대신 “”를 사용해야 한다.

라인 8: 임시로 사용자가 입력한 문자를 출력한다.

만일 라인 24의 함수 호출을 제거하고 프로그램을 컴파일하고 실행하면, 그림 11-14와 같은 결

과를 볼 수 있다. 사용자가 명령을 입력하면, print_menu() 함수를 두 번 실행한다. 그 이유는 라

인 23에서 사용자 입력을 받기 위하여 getchar() 함수를 사용했기 때문이다. 이 함수는 사용자가

키보드로 데이터를 입력하면 컴퓨터 내부의 키보드 버퍼에 데이터를 저장하였다가 사용자가 엔터

를 입력하고 나서 처리한다. 그러므로, 한 번은 사용자가 입력한 문자를 입력하고, 다시 한번 루

프를 반복하여 엔터키를 리턴한다. 그러므로, 두 번째 선택에서 줄바꿈이 출력된 것이다. 사용자

입력에 따라 한 번씩만 동작하려면, 라인 24에서 getchar()를 한번 더 호출하여 엔터키를 버퍼에

서 제거해야 올바로 동작한다.

<그림 11-14> 메뉴 입력 화면

Page 281: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

279

11.3.4 자료추가

학생 정보를 입력 받고 연결리스트에 저장하는 기능을 설계하고 구현해 보자. 먼저 학생 데이

터를 연결리스트에 저장하기 위한 자료구조를 정의한다.

학생 정보가 학과, 학번, 이름, 성적으로 구성되어 있으므로, 여기에 다음 포인터를 추가하여 구

조체를 정의한다. 헤더 파일의 #include 문 다음에 구조체를 정의하고, 소스 파일의 main() 함수

앞에 전역 변수로 연결리스트의 헤드를 정의하고 초기화 한다.

<리스트 11-26> 자료구조 및 리스트 헤드 선언

// studentmgnt.h 헤더 파일

1. typedef struct STUDENT_INFO SINFO; // 구조체 재정의

2. struct STUDENT_INFO // 구조체 선언

3. {

4. char major[32]; // 학과

5. char id[16]; // 학번

6. char name[16]; // 이름

7. int score; // 성적

8. struct STUDENT_INFO *next; // 다음 학생 데이터에 대한 포인터

9. };

// studentmgnt.c 소스 파일

1. SINFO *listhead = NULL; // 연결리스트의 헤드

자료추가를 결정하였으므로, 다음에 사용자가 입력하는 학생 정보를 순서대로 연결리스트의 앞

에 저장하는 기능을 구현해 보자. 이 기능을 수행하는 함수를 add_student()라고 하자. 이 함수는

다음과 같이 학생 정보를 입력 받고, 연결리스트에 학생 데이터를 추가한다.

<리스트 11-27> 자료 추가

1. 학생 데이터를 저장할 SINFO에 대한 포인터 변수 temp를 선언한다.

2. temp에 동적으로 SINFO 구조체를 할당한다.

3. temp에 할당된 구조체에 학생 학과, 학번, 이름, 성적을 입력 받는다.

4. 연결리스트의 헤드에 temp를 연결한다.

연결리스트에 노드를 추가하는 기능(라인 4)은 나중에도 사용할 예정이므로, 별도의 함수로 작성

하기로 한다. 이 함수를 insert_node()라고 하자. “11.1.9 연결리스트 정렬”에서 살펴 보았듯이 연결

리스트에 노드를 추가할 때, 다음과 같이 두 개의 노드 포인터가 필요하다.

Page 282: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

280

SINFO *pos: 새로운 노드를 추가할 노드에 대한 포인터

SINFO *previous: pos가 가리키는 노드의 앞 노드에 대한 포인터

즉, 새로운 노드는 previous와 pos가 가리키는 노드 사이에 삽입된다. 따라서, insert_node() 함수

의 원형을 다음과 같이 정한다.

void insert_node(SINFO *temp, SINFO *previous, SINFO *pos)

파라미터

SINFO *temp: 연결리스트에 저장할 새로운 노드에 대한 포인터

SINFO *previous: 저장할 노드의 앞 노드에 대한 포인터

SINFO *pos: 저장할 노드에 대한 포인터

연결리스트에 노드를 삽입할 때, 삽입할 위치가 연결리스트의 맨 앞인 경우와 중간 혹은 마지

막인 경우로 나누어 처리하여야 한다. 그림 11-15는 이 두 가지 경우에 대하여 노드를 추가하기

전의 상태를 보여준다. 연결리스트의 처음에 노드를 추가할 때는 previous 포인터의 값이 NULL이

다.

<그림 11-15> 연결리스트에 노드 삽입

따라서 insert_node() 함수가 수행할 일을 다음과 같이 표현할 수 있다.

<리스트 11-28> 노드 삽입 함수

1. 만일 previous가 NULL 이면, // 리스트의 처음에 연결한다.

2. temp->next = pos;

3. listhead = temp;

4. 아니면 // 리스트의 중간 혹은 끝에 연결한다.

5. temp->next = pos;

6. previous->next = temp;

Page 283: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

281

학생 데이터를 리스트에 추가하기 위하여 리스트 11-27의 라인 1에서 기억장치를 동적으로 할당

하였으므로, 할당 받은 기억장치를 해제하는 기능도 지금 구현하는 것이 좋다. 이 함수는 “11.1

성적 정렬” 문제의 delete_list() 함수와 동일하다.

지금부터 자료추가 함수를 구현해 보자. 먼저 헤더 파일을 다음과 같이 수정한다.

<리스트 11-29> studentmgnt.h 헤더 파일 수정

1. #include <malloc.h> // 기억장치 할당, 해제 함수를 위한 헤더 파일 포함

2. …

3. void add_student(); // 새로 구현할 함수 선언 추가

4. void insert_node(SINFO *temp, SINFO *previous, SINFO *pos);

5. void delete_list();

그리고 나서, main() 함수를 다음과 같이 수정한다. 자료추가 기능에 함수 호출을 구현하고, 나머

지 메뉴의 기능을 아직 구현하지 않는다.

<리스트 11-29> main() 함수 수정

1. void main()

2. {

3. char menu = 0;

4. while (menu != '7')

5. {

6. menu = print_menu();

7. //printf("%2x번 기능을 선택하였습니다.\n", menu); // 이 문장을 주석처리

8. switch (menu) // 명령 처리를 위한 switch 문 추가

9. {

10. case '1': add_student(); break; // 자료추가

11. case '2': break; // 목록검색 함수 나중에 추가

12. case '3': break; // 목록삭제 함수 나중에 추가

13. case '4': break; // 목록정렬 함수 나중에 추가

14. case '5': break; // 목록보기 함수 나중에 추가

15. case '6': break; // 메뉴보기 함수 나중에 추가

16. default: break;

17. }

18. }

19. delete_list(); // 프로그램이 끝나기 전에 노드 해제

20. }

Page 284: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

282

그리고 나서, add_student(), insert_node(), 그리고 delete_list()를 구현한다. delete_list() 함수는

“11.1 성적 정렬” 문제의 리스트 11-8과 동일하므로, 소스 코드를 생략한다.

<리스트 11-30> 노드 삽입 함수

1. void add_student()

2. {

3. SINFO *temp; // 임시 변수

4. temp = (SINFO *)malloc(sizeof(SINFO)); // 기억장소 할당

5. printf("데이터를 입력하세요.\n"); // 안내문 출력

6. printf("학과: "); scanf("%s", temp->major); // 학과 입력

7. printf("학번: "); scanf("%s", temp->id); // 학번 입력

8. printf("이름: "); scanf("%s", temp->name); // 이름 입력

9. printf("성적: "); scanf("%d", &temp->score); // 성적 입력

10. insert_node(temp, NULL, listhead); // 리스트에 추가

11. printf("%s의 데이터를 저장하였습니다.\n\n", temp->name); // 안내문 출력

12. getchar(); // 엔터 제거

13. }

14. void insert_node(SINFO *temp, SINFO *previous, SINFO *pos)

15. {

16. if (previous == NULL) // 리스트의 처음에 연결

17. {

18. temp->next = pos;

19. listhead = temp;

20. }

21. else // 리스트의 중간 이후에 연결

22. {

23. temp->next = pos;

24. previous->next = temp;

25. }

26. }

27. void delete_list() { … } // 리스트 8-8과 동일

라인 10: insert_node를 호출할 때, 학생 데이터를 항상 노드의 맨 앞에 저장하기 위하여 파

라미터 previous 값을 NULL로 전달하고, 파라미터 pos의 값을 listhead로 전달한다.

라인 11: 학생의 이름을 출력한다.

라인 12: add_student() 함수의 마지막에 getchar() 함수를 호출하지 않으면, 컴퓨터의 버퍼에

Page 285: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

283

엔터 키가 남아 있어 main()에서 다음에 사용자 명령을 받는 부분이 정상적으로 동작하지 않

는다.

자료추가까지 구현하고 실행해 보자. 입력한 데이터를 출력하는 기능을 아직 구현하지 않았으

므로, 프로그램 실행 만으로 정상적으로 실행되는지 확인할 수 없다. 비주얼 스튜디오의 디버거

기능으로 insert_node()를 호출하는 라인 다음(리스트 11-30의 라인 11)에 브레이크 포인터를 걸

어 놓고, 프로그램을 추적해 보면, 그림 11-16과 같이 디버거 상에서 학생 데이터가 연결리스트에

저장되는 것을 확인해 볼 수 있다.

<그림 11-16> 자료 추가 확인

11.3.5 목록보기

데이터가 올바로 저장되었는지 확인하기 위하여 목록보기 기능을 먼저 구현하자. 이 기능은 출

력할 항목이 많다는 것 이외에 “11.1 성적 정렬” 문제의 print_list()와 같다. 구현 결과는 다음과

같다.

<리스트 11-31> 목록보기 구현

// studentmgnt.h 파일에 함수 선언

1. void print_list();

Page 286: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

284

// studentmgnt.c 파일에 함수 구현

1. void print_list()

2. {

3. SINFO *search = listhead; // 리스트 헤드로 초기화

4. printf("\n%-16s%-16s%-16s%-6s\n", "학과", "학번", "이름", "성적");

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

6. while (search != NULL) // 리스트의 모든 노드에 대하여

7. {

8. printf("%-16s", search->major); // 데이터 출력

9. printf("%-16s", search->id);

10. printf("%-16s", search->name);

11. printf("%6d\n", search->score);

12. search = search->next; // 다음 노드로 이동

13. }

14. printf(“\n”);

15. }

라인 8: printf() 함수의 형식지시자 “%-16s”는 문자열을 전체 16자리로 출력하고 오른쪽 맞춤

하라는 의미이다.

프로그램을 실행하고, 학생 두 명의 데이터를 입력한 후, 목록보기를 실행하면 그림 11-17과 같은

결과를 볼 수 있다. 학생 정보를 리스트의 앞에 추가하므로, 입력한 역순으로 연결리스트에 저장

되어 출력되는 것을 확인할 수 있다.

Page 287: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

285

<그림 11-17> 학생 데이터 입력 및 출력 결과

11.3.6 파일저장 및 읽기

프로그램을 테스트하기 위하여 매번 학생 데이터를 입력하는 것이 불편하므로, 학생 데이터를

파일로 저장하고 파일에서 데이터를 읽는 기능을 구현해 보자. 일단 프로그램을 다음과 같이 수

정해 놓고, write_list()와 read_list() 함수의 내부를 구현해 보자.

<리스트 11-32> 파일 저장 및 읽기 기능 추가

// studentmgnt.h 파일에 함수 선언

1. void wirte_list();

2. void read_list();

// studentmgnt.c 파일에 함수 구현

1. void main()

2. {

3. ….

4. read_list(); // while 문 전에 파일을 읽는다.

5. while()

Page 288: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

286

6. { …. }

7. write_list(); // while 문이 끝나고 리스트를 해제하기 전에 파일로 기록한다.

8. delete_list();

9. }

10. void write_list() { } // 비어 있는 함수로 구현

11. void read_list() { } // 비어 있는 함수로 구현

앞에서 우리는 텍스트 파일을 사용해 보았다. 이번에는 이진 파일을 사용해 보자. 텍스트 파일

과 이진 파일은 다음과 같은 점이 다르다.

텍스트 파일(text file): 사람이 볼 수 있는 코드로 변환해서 파일로 저장한다. 메모장(notepad)

프로그램으로 텍스트 파일을 열어 그 내용을 확인할 수 있다.

이진 파일(binary file): 컴퓨터가 데이터를 저장하는 형식 그대로 저장한다. 메모장으로 이진

파일의 내용을 확인할 수 없다.

이진 파일도 텍스트 파일과 마찬가지로 먼저 파일을 열고, 파일을 사용하고, 끝나기 전에 파일

을 닫아야 한다. 이진 파일을 열 때, 파일 모드(mode) 파라미터를 통하여 이진 파일임을 컴퓨터

에게 알려주어야 한다. 모드의 값은 다음과 같다.

“wb”: 파일을 쓰기+ 이진 모드로 연다.

“rb”: 파일을 읽기 + 이진 모드로 연다.

<stdio.h>에 이진 파일에 데이터를 쓰고 읽는 함수는 다음과 같이 정의되어 있다.

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream)

파라미터

const void *ptr: 파일에 기록할 데이터를 포함하고 있는 버퍼의 주소

size_t size: 파일에 기록할 원소의 바이트 크기

size_t count: 크기가 size인 원소의 수

FILE *stream: 파일 객체에 대한 포인터

리턴

파일에 성공적으로 기록한 원소의 수

size_t fread(void *ptr, size_t size, size_t count, FILE *stream)

파라미터

void *ptr: 파일에서 데이터를 읽어 저장할 버퍼의 주소. 버퍼의 크기는 size * count.

size_t size: 파일에서 읽을 원소의 바이트 크기

size_t count: 크기가 size인 원소의 수

FILE *stream: 파일 객체에 대한 포인터

리턴

Page 289: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

287

파일에서 성공적으로 읽은 원소의 수

이 함수들은 원소의 크기가 size인 데이터 count 개를 파일에 기록하고 읽는다. 리턴 값은 성공적

으로 기록하거나 읽은 원소의 수이다.

이진 파일을 사용하는 방법을 알았으므로, 먼저 write_list() 함수를 다음과 같이 구현해 보자.

<리스트 11-33> 파일 쓰기 구현

1. void write_list()

2. {

3. SINFO *temp; // 임시 변수

4. FILE *fp; // 파일 객체 포인터

5. if ((fp = fopen("student.dat", "wb")) != NULL) // 파일을 성공적으로 오픈하면

6. {

7. temp = listhead; // temp를 리스트의 처음으로 설정한다.

8. while (temp != NULL) // temp가 리스트의 끝이 아니면

9. {

10. fwrite(temp, sizeof(SINFO), 1, fp); // SINFO 한 개를 파일에 기록한다.

11. temp = temp->next; // temp를 다음 노드로 이동한다.

12. }

13. fclose(fp); // 파일을 닫는다.

14. }

15. }

라인 5: 이진 파일을 쓰기 모드로 연다.

라인 7~12: 연결리스트의 노드를 추적하면서, 이진 파일에 데이터를 기록한다.

라인 10: temp가 가리키는 주소에서 구조체 SINFO 한 개에 해당하는 크기만큼 이진파일에

기록한다.

라인 13: 이진 파일을 닫는다.

프로그램을 실행하고, 학생 데이터를 입력하고 프로그램을 종료해 보자. 프로젝트 디렉토리의

소스 파일이 있는 디렉토리 안에 student.dat 파일이 만들어진 것을 확인할 수 있다. 이 파일을

메모장으로 열어 보면, 텍스트 부분과 이상한 문자들이 혼합되어 있는 것을 볼 수 있다.

이와 같이 파일 저장이 올바로 수행된 것을 확인하고, 파일 읽기 함수를 구현한다. 사용자에게

입력을 받아 연결리스트를 만드는 대신에, 이 함수는 파일에서 학생 데이터를 읽어 연결리스트를

만든다. 이 함수의 구현은 다음과 같다.

<리스트 11-34> 파일 읽기 구현

Page 290: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

288

1. void read_list()

2. {

3. SINFO *temp; // 임시 포인터

4. FILE *fp; // 파일 객체 포인터

5. int count; // 읽은 데이터 수

6. if ((fp = fopen("student.dat", "rb")) != NULL) // 파일을 읽기 모드로 오픈한다.

7. {

8. do

9. {

10. temp = (SINFO *)malloc(sizeof(SINFO)); // 기억장소를 할당한다.

11. count = fread(temp, sizeof(SINFO), 1, fp); // 데이터를 읽는다.

12. if (count != 0) // 성공적으로 읽었다면

13. insert_node(temp, NULL, listhead); // 리스트에 추가한다.

14. } while (count != 0); // 다음을 반복한다.

15. fclose(fp); // 파일을 닫는다.

16. }

17. }

라인 6: 이진 파일을 읽기 모드로 연다.

라인 8~14: 이진 파일에 데이터를 기록한다.

라인 10: 기억장소를 할당한다.

라인 11: 이진 파일에서 구조체 SINFO 한 개에 해당하는 크기만큼 데이터를 읽어 temp에 저

장한다.

라인 13: 파일 읽기에 성공하였다면, insert_node() 함수를 호출하여 리스트에 추가한다.

라인 15: 이진 파일을 닫는다.

프로그램을 실행하고, 바로 [목록보기] 명령을 수행해 보면, 학생 정보가 출력되는 것을 볼 수

있다. [자료추가] 기능을 수행하여 몇 명의 학생 데이터를 더 입력하고, 프로그램을 종료하였다가

다시 시작하면, 그 동안 입력한 데이터를 모두 저장하고 읽는 것을 확인할 수 있다. 다만, 데이터

를 저장할 때 연결리스트의 앞 항목부터 저장하고, 읽을 때 뒤에 저장된 데이터를 읽어 리스트의

앞에 추가힌다. 따라서, [목록보기]를 실행하면, 출력되는 학생 데이터의 순서가 달라진다.

11.3.7 목록검색

이제 학생 학번 또는 이름을 입력 받아, 해당 학생의 데이터를 출력하는 기능을 구현해 보자.

이 기능을 구현하기 위하여 다음과 같은 두 개의 함수를 추가한다.

void search_menu()

기능

그림 11-12(c)와 같이 학번으로 검색할지 이름으로 검색할지 메뉴를 제시하고, 검색 방법

Page 291: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

289

을 입력 받는다.

그리고 나서, 검색 방법에 따라 학번 또는 이름을 입력 받고, print_node() 함수를 호출한

다.

void print_node(char *key, char cond):

파라미터

char *key: 검색할 문자열

char cond: 검색 방법

기능

조건(cond)에 따라 학번 또는 이름(*key)으로 연결리스트를 검색하여, 학생 정보를 출력한

다.

단계적으로 구현하기 위하여, 다음과 같이 두 개의 함수 선언을 추가하고, main() 함수의 switch

문에 search_menu()를 호출하도록 수정한다.

<리스트 11-35> 자료검색

// 헤더 파일(studentmgnt.h)에 함수 선언 추가

1. void search_menu();

2. void print_node(char *key, char cond);

// studentmgnt.c 수정

1. void main()

2. {

3. …

4. case '2': search_menu(); break; // 함수 호출 추가

5. …

6. }

7. void search_menu() { } // 비어 있는 함수 구현

8. void print_node(char *key, char cond) { }

검색 방법을 결정하고 검색할 문자열을 입력 받는 함수를 다음과 같이 구현한다. 검색 결과를

출력하는 함수는 임시로 구현한다.

<리스트 11-36> 검색 메뉴 구현

// 소스 파일(studentmgnt.c)에 함수 구현

1. void search_menu()

2. {

3. char key[32], cond; // 검색조건 버퍼, 검색조건

4. printf("\n무엇으로 검색하시겠습니까?\n"); // 안내문 출력

5. printf("1. 학번\n");

Page 292: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

290

6. printf("2. 이름\n\n");

7. printf("검색 조건 > ");

8. cond = getchar(); // 검색 조건

9. if (cond == '1') // 학번으로 검색하는 경우

10. {

11. printf("\n학번은? "); scanf("%s", key); // 학번 입력

12. print_node(key, cond); // 학번으로 검색하여 출력

13. }

14. else if (cond == '2') // 이름으로 검색하는 경우

15. {

16. printf("\n이름은? "); scanf("%s", key); // 이름 입력

17. print_node(key, cond); // 이름으로 입력하여 출력

18. }

19. else // 검색조건이 틀린 경우

20. printf("\n검색 조건이 틀립니다.\n\n");

21. getchar(); // 남아 있는 엔터 제거

22. }

23. void print_node(char *key, char cond) // 임시로 구현

24. {

25. if (cond == '1')

26. printf("\n학번으로 %s를 검색합니다.\n\n", key);

27. else

28. printf("\n이름으로 %s를 검색합니다.\n\n", key);

29. }

프로그램을 실행하여 print_node() 함수로 파라미터가 올바로 전달되는 것을 확인한 후에, 다음

과 같이 print_node() 함수를 구현한다. 이 과정에서 문자열을 비교하는 strcmp() 함수를 선언하기

위하여 헤더 파일에 #include <string.h>를 추가한다.

<리스트 11-37> 노드 출력 함수

// studentmgnt.h

1. #include <string.h> // 문자열 처리 함수 선언 헤더 파일 추가

// studentmgnt.c

1. void print_node(char *key, char cond)

2. {

3. SINFO *search= listhead; // 리스트를 추적하기 위한 포인터 선언, 초기화

4. int result; // 비교 결과

Page 293: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

291

5. while (search != NULL) // 리스트의 끝이 아니면

6. {

7. if (cond == '1') // 검색 조건이 학번이면

8. result = strcmp(search ->id, key); // 학번을 비교한다.

9. else // 검색 조건이 이름이면

10. result = strcmp(search ->name, key); // 이름을 비교한다.

11. if (result == 0) // 비교 결과가 같으면

12. break; // 루프를 벗어난다.

13. search = search ->next; // 다음 노드로 이동한다.

14. }

15. if (search!= NULL) // 만일 일치하는 노드를 찾았으면

16. {

17. printf("\n검색 결과: "); // 학생 데이터를 출력한다.

18. printf("%s ", search ->major);

19. printf("%s ", search ->id);

20. printf("%s ", search ->name);

21. printf("%3d\n\n", search->score);

22. }

23. else // 학생을 찾지 못하였으면

24. printf("\n\n검색 조건에 해당하는 학생이 없습니다.\n\n");

25. }

라인 5~14: 연결리스트의 노드를 추적하면서 key와 같은 값을 갖는 노드를 탐색한다.

라인 11: 비교 결과가 0이면, key에 해당하는 문자열을 찾은 것이다.

라인 15~22: 일치하는 노드의 데이터를 출력한다.

그림 11-18은 이름으로 검색을 실행한 화면이다. 학번으로 검색해도 비슷한 결과를 확인할 수

있다.

Page 294: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

292

<그림 11-18> 이름으로 검색한 결과

11.3.8 목록정렬

연결리스트를 정렬하는 기본 개념은 headlist에 연결되어 있는 리스트에서 노드를 하나씩 분리

하고, 분리된 노드를 정렬되어 있는 리스트로 다시 구성하는 것이다. 예들 들어 학번을 기준으로

오름차순으로 정렬하는 과정을 그림 11-19를 통하여 알아보자.

그림 11-19(a): 정렬 전에 listhead에 세 개의 노드가 정렬되어 있지 않은 상태로 연결되어 있

다.

그림 11-19(b): 임시 변수 oldlist를 사용하여, oldlist가 기존의 리스트를 가리키도록 만들고,

listhead를 NULL로 초기화 한다.

그림 11-19(c): oldlist에서 노드를 하나씩 분리하고, 분리된 노드를 listhead에 연결한다.

그림 11-19(d), (e): oldlist가 가리키는 다음 노드를 분리하고, 분리된 노드를 listhead에 정렬하

여 연결한다. 이 과정을 oldlist가 NULL이 될 때까지 반복한다.

이와 같은 정렬 과정을 구현하려면, oldlist에서 분리한 노드를 가리키는 임시 포인터가 하나 더

필요하다. 정렬 알고리즘은 다음과 같다.

<알고리즘 11-38> 연결리스트 정렬 알고리즘

1. 리스트를 초기화 한다. (oldlist = listhead, listhead = NULL)

2. oldlist가 NULL이 될 때까지 다음 과정을 반복한다.

3. 임시 포인터 temp를 oldlist가 가리키는 노드로 설정한다.

Page 295: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

293

4. oldlist를 다음 노드를 가리키도록 옮긴다. (oldlist = oldlist->next)

5. temp가 가리키는 노드를 listhead에 오름차순으로 정렬하여 저장한다.

<그림 11-19> 학번 기준 오름차순 정렬 과정

리스트 11-38의 라인 5에서 search가 가리키는 노드를 연결리스트에 오름차순으로 정렬하여 저

장하는 방법을 해결해야 한다. 이 동작을 수행하기 위하여 먼저 listhead에 새로운 노드가 추가될

위치를 찾은 다음, 그 위치에 노드를 추가해야 한다. “11.1.9 연결리스트 정렬”의 리스트 11-13에

서 노드 삽입 알고리즘을 학습한 경험이 있다. 이 알고리즘을 그대로 이 문제에 적용할 수 있다.

<리스트 11-39> 정렬 저장 알고리즘

1. search를 리스트의 첫 번째 노드를 가리키도록 만든다. (search = listhead)

2. previous를 NULL로 초기화 한다.

3. search가 리스트의 끝이 아니면 다음을 반복한다.

4. 리스트에 추가할 노드의 학번이 search가 가리키는 노드의 학번보다 크면

5. previous를 search가 가리키는 노드를 가리키도록 변경한다. (previous = search)

6. search를 다음 노드를 가리키도록 변경한다. (search = search->next)

7. 그렇지 않으면

8. 루프를 벗어난다.

9. search가 가리키는 노드 앞에 temp가 가리키는 노드를 추가한다.

“11.3.4 자료추가”에서 임의의 위치에 노드를 추가하는 기능(리스트 11-39의 라인 9)을 이미 구

현해 놓았다. 함수의 원형은 다음과 같고, pos는 노드를 삽입할 위치를 가리키는 포인터이고,

previous는 pos의 이전 노드를 가리키는 포인터이다.

Page 296: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

294

void insert_node(SINFO *temp, SINFO *previous, SINFO *pos) { … }

연결리스트를 정렬하는 알고리즘 설계가 끝났으니, 이제 프로그램을 구현해 보자. 구현에 필요

한 함수를 다음과 같이 정한다.

void sort_menu()

기능

학과로 정렬할지 아니면 학번으로 정렬할지 결정(cond)하고, sort_list() 함수를 호출한다.

void sort_list(char cond)

파라미터

char cond: 사용자가 선택한 정렬 조건(cond)

기능

이 함수는 리스트 8-38을 구현한다.

연결리스트에 저장할 위치를 찾고, sort_add() 함수를 호출한다.

void sort_add(SINFO *temp, char cond)

파라미터

SINFO *temp: 원래의 연결리스트에서 분리한 노드에 대한 포인터

char cond: 사용자가 선택한 정렬 조건

기능

이 함수는 리스트 8-39를 구현한다.

insert_node() 함수를 호출하여 정렬 조건에 따라 listhead에 temp를 추가한다.

일단 소스 코드를 다음과 같이 수정하고 함수를 하나씩 구현해 보자.

<리스트 11-40> 소스 코드 수정

// studentmgnt.h

1. void sort_menu() // 함수 선언 추가

2. void sort_list(char cond);

3. void sort_add(SINFO *temp, char cond)

// studentmgnt.c 수정

1. void main()

2. {

3. …

4. case '4': sort_menu(); break; // 함수 호출 추가

5. …

6. }

Page 297: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

295

7. void sort_menu() { } // 비어 있는 함수 구현

8. void sort_list(char cond) { }

9. void sort_add(SINFO *temp, char cond) { }

정렬 방법을 선택하는 sort_menu() 함수는 검색 메뉴를 선택하는 search_menu() 함수와 구조가

같다. 그러므로 다음과 같이 구현할 수 있다.

<리스트 11-41> 정렬 방법 선택 함수 구현

// studentmgnt.c

1. void sort_menu()

2. {

3. char cond; // 정렬 조건

4. printf("\n무엇으로 정렬하시겠습니까?\n"); // 안내문 출력

5. printf("1. 학과\n");

6. printf("2. 학번\n\n");

7. printf("정렬 조건 > ");

8. cond = getchar(); // 정렬 조건 입력

9. if ((cond == '1') || (cond == '2')) // 사용자가 1 또는 2를 입력한 경우

10. sort_list(cond); // 정렬 조건에 따라 정렬

11. else // 정렬 조건이 틀린 경우

12. printf("\n검색 조건이 틀립니다.\n\n");

13. getchar(); // 남아 있는 엔터 키 제거

14. }

라인 10에서 사용자의 선택에 따라 정렬하는 sort_list()함수를 호출한다. 이 함수는 리스트 11-

38의 기능을 수행한다.

<리스트 11-42> 연결리스트 정렬

// studentmgnt.c

1. void sort_list(char cond) // 리스트 8-38 구현

2. {

3. SINFO *oldlist, *temp; // 임시 포인터 변수

4. oldlist = listhead; // 기존 리스트를 이전 리스트로 설정

5. listhead = NULL; // listhead를 비어 있는 리스트로 초기화

6. while (oldlist != NULL) // 기존 리스트의 끝까지

7. {

8. temp = oldlist; // 노드를 하나 분리한다.

9. oldlist = oldlist->next; // 기존 리스트를 다음 노드로 설정한다.

10. sort_add(temp, cond); // 분리한 노드를 조건에 따라 정렬하여 저장한다.

Page 298: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

296

11. }

12. if (cond == '1') // 정렬 후 안내문 출력

13. printf("\n학과순으로 정렬하였습니다.\n\n");

14. else

15. printf("\n학번순으로 정렬하였습니다.\n\n");

16. }

이 함수는 노드를 분리하고(라인 8), 기존 리스트를 다음 노드를 가리키도록 설정한 후(라인 9),

노드를 정렬하여 저장하는 sort_add() 함수를 호출한다. 이 함수는 리스트 11-39의 기능을 수행한

다.

<리스트 11-43> 연결리스트 정렬 후 저장 함수 구현

// studentmgnt.c

1. void sort_add(SINFO *temp, char cond) // 리스트 11-39 구현

2. {

3. int result; // 비교 결과를 저장하기 위한 지역 변수

4. SINFO *search, *previous; // 리스트 추적을 위한 포인터

5. search = listhead; // search를 첫 번쨰 노드를 가리키도록 설정

6. previous = NULL; // previous를 NULL로 설정

7. while (search != NULL) // 리스트의 끝까지 반복한다.

8. {

9. if (cond == '1') // 검색 조건에 따라

10. result = strcmp(temp->major, search->major); // 학과를 비교한다.

11. else

12. result = strcmp(temp->id, search->id); // 학번을 비교한다.

13. if (result > 0) // 추가할 노드의 값이 더 크면

14. {

15. previous = search; // 다음 노드로 진행한다.

16. search = search->next;

17. }

18. else // 삽입 위치를 찾았다면

19. break; // 루프를 벗어난다.

20. }

21. insert_node(temp, previous, search); // temp를 search 전에 추가한다.

22. }

이 함수는 temp가 가리키는 노드가 삽입될 위치를 찾고(라인 7~20), 그 위치에 노드를 추가하기

Page 299: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

297

위하여 insert_node() 함수를 호출한다(라인 21). 프로그램을 실행해 보자. 그림 11-20은 학번 순

으로 정렬한 후 목록보기를 수행한 결과이다.

<그림 11-20> 목록정렬 실행 결과

11.3.9 목록삭제

마지막으로 학생 이름을 입력 받고 해당 학생의 데이터를 리스트에서 삭제하는 기능이 남아 있

다. 노드를 특정 위치에 삽입할 때와 마찬가지로 노드를 삭제할 때도 삭제할 노드를 찾아야 한다.

정렬할 때와 동일한 방법으로(previous와 search 포인터를 사용하여) 삭제할 노드를 찾는다. 삭제

할 노드를 찾았을 때, search는 삭제할 노드를 가리키고, previous는 그 앞 노드를 가리킨다. 이 상

태에서 그림 11-21과 같이 search가 가리키는 노드를 삭제한다.

<리스트 11-44> 노드 삭제 과정

// 연결리스트의 맨 앞에 있는 노드를 삭제하는 방법 (이 때, previous=NULL)

1. head->next = search->next; // 리스트 헤드를 삭제할 노드의 다음으로 연결한다.

2. free(search); // 삭제할 노드의 기억장소를 해제한다.

// 연결리스트의 중간 혹은 끝에 노드를 추가하는 방법

3. previous->next = search->next; // 이전 노드를 삭제할 노드의 다음으로 연결한다.

4. free(search); // 삭제할 노드의 기억장소를 해제한다.

Page 300: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

298

<그림 11-21> 연결리스트의 노드 삭제

삭제 알고리즘을 알았으므로, 다음과 같이 프로그램으로 구현한다.

<리스트 11-45> 연결리스트 삭제

// 헤더 파일(studentmgnt.h)

1. void delete_node(); // 함수 선언

// 소스 파일(studentmgnt.c)

1. case '3': delete_node(); break; // main() 함수의 switch 문 안에 함수 호출 추가

2. void delete_node()

3. {

4. char name[16]; // 사용자에게 이름을 입력 받기 위한 배열

5. SINFO *search, *previous; // 연결리스트 탐색

6. printf("\n삭제할 학생의 이름을 입력하세요: \n"); // 안내문 출력

7. printf("이름: "); scanf("%s", name); // 이름 입력

8. getchar(); // 남아 있는 엔터 키 제거

9. search = listhead; // 연결리스트에서 삭제할 노드를 찾기 위한 초기화

10. previous = NULL;

11. while (search != NULL) // 연결리스트의 끝까지

12. {

13. if (strcmp(name, search->name) != 0) // 학생 이름과 같지 않으면

Page 301: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

299

14. {

15. previous = search; // 다음 노드로 이동

16. search = search->next;

17. }

18. else // 학생을 찾았으면

19. break; // 루프를 벗어난다.

20. }

21. if (search == NULL) // 학생 데이터가 연결리스트에 없으면

22. {

23. printf("학생 데이터가 존재하지 않습니다.\n");

24. }

25. else // 학생 데이터가 연결리스트에 존재하면

26. { // 리스트 8-44 구현

27. if (previous == NULL) // 첫 번째 노드이면

28. {

29. listhead = search->next; // 다음 노드로 연결하고

30. free(search); // 노드를 해제한다.

31. }

32. else // 중간 노드이면

33. {

34. previous->next = search->next; // 다음 노드로 연결하고

35. free(search); // 노드를 해제한다.

36. }

37. printf("%s 학생의 데이터를 삭제하였습니다.\n\n", name); // 안내문 출력

38. }

39. }

프로그램을 실행하여, 목록삭제 후 목록보기를 연속해서 실행해 보면, 해당 노드가 올바로 삭제

되는 것을 확인할 수 있다.

지금까지 연결리스트를 활용하는 학생 관리 프로그램을 모두 작성하였다. 프로그램 완성 후, 이

프로그램의 헤더 파일은 다음과 같이 작성되어 있다. 정리하는 의미에서 제시한다.

<리스트 11-46> 헤더 파일(studentmgnt.h>

1. #include <stdio.h> // 헤더 파일 포함

2. #include <malloc.h>

3. #include <string.h>

Page 302: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

300

4. typedef struct STUDENT_INFO SINFO; // 구조체 재정의

5. char print_menu(); // 함수 선언

6. void add_student();

7. void insert_node(SINFO *temp, SINFO *previous, SINFO *pos);

8. void delete_list();

9. void print_list();

10. void write_list();

11. void read_list();

12. void search_menu();

13. void print_node(char *key, char cond);

14. void sort_menu();

15. void sort_list(char cond);

16. void sort_add(SINFO *temp, char cond);

17. void delete_node();

18. struct STUDENT_INFO // 구조체 정의

19. {

20. char major[32]; // 학과

21. char id[16]; // 학번

22. char name[16]; // 이름

23. int score; // 성적

24. struct STUDENT_INFO *next; // 다음 학생 데이터에 대한 포인터

25. };

비록 프로그램이 길어 복잡해 보일지 모르겠지만, 프로그램이 수행해야 할 기능이 많을 뿐이고

각 기능에 해당하는 소스 코드는 그렇게 많지 않다. 아무리 큰 프로그램도 마찬가지이다.

11.4 요약

이 장에서는 연결리스트에 대하여 학습하였다. 연결리스트는 배열을 대신하여 여러 개의 동일한

데이터를 저장하는 중요한 자료구조이다. 이러한 용도 이외에도 연결리스트는 트리, 그래프 등을

표현하는데 사용된다. 이 장에서 새로 소개한 프로그래밍 기법은 다음과 같다.

포인터 변수는 기억장치의 주소를 저장한다.

포인터 변수도 자료형이 있으며, 포인터 변수의 자료형은 포인터가 가리키는 변수의 자료형

이다.

포인터에 값이 할당되어 있지 않다는 의미로 널 값을 할당한다.

포인터 변수로 구조체 멤버를 참조할 때는 ‘->’ 연산자를 사용한다.

연결리스트를 구현하기 위한 구조체는 다음과 같은 구조를 갖는다.

Page 303: 응 용 컴 퓨 터 프 로 그 래 밍image.cbnu.ac.kr/jhahn/lecture/c-lang/c-lang2.pdf의 특징에 대하여 설명한다. 각 주제에 대하여 자세히 설명하지 않고,

301

struct 구조체이름 // 구조체 선언

{

데이터 필드; // 데이터를 저장하기 위한 변수 목록

struct 구조체이름 *next; // 노드를 연결하기 위한 포인터

};

구조체 포인터로 선언되는 리스트 헤드에 노드들이 연결된다. 리스트 헤드는 다음과 같이 선

언하고 초기화 한다.

struct 구조체이름 *listhead = NULL;

자료형을 재정의 하는 typedef 문을 사용하면, 자료형의 의미가 간단해진다.

연결리스트에 노드를 삽입하거나 제거할 때, 노드에 대한 포인터를 잃어버리지 않도록 주의

해야 한다.

연결리스트의 각 노드를 추적하는 방법은 다음과 같다.

1. temp = listhead; // 임시 포인터 temp를 listhead로 설정한다.

2. while (temp != NULL) // temp가 널이 아니면 다음 과정을 반복한다.

3. {

4. temp가 가리키는 노드를 사용한다.

5. temp = temp->next; // temp를 다음 노드를 가리키도록 변경한다.

6. }

연결리스트에 노드를 삽입하려면, 삽입할 노드에 대한 포인터와 그 이전 노드에 대한 포인터

가 필요하다.

연결리스트를 사용하는 프로그램은 프로그램이 종료되기 전에 노드를 모두 해제해야 한다.

이진 파일은 컴퓨터가 데이터를 저장하는 형태 그대로 파일에 데이터를 저장한다.

이진 파일을 쓰기 모드로 열 때 파일 모드를 “wb”로 지정하고, 읽기 모드로 열 때 파일 모드

를 “rb”로 지정한다.

이 장에서는 이진 파일을 액세스하는 두 개의 라이브러리 함수가 소개되었다.

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream) // 이진 파일에 데이터를 기

록한다.

size_t fread(void *ptr, size_t size, size_t count, FILE *stream) // 이진 파일에서 데이터를 읽는다.


Recommended