이 섹션의 다중 페이지 출력 화면임. 여기를 클릭하여 프린트.

이 페이지의 일반 화면으로 돌아가기.

그밖에 프로그래밍 언어

1 - C 언어

C 언어

1970년대부터 오래된 프로그래밍 언어이면서, 지금도 많은 소프트웨어 개발 및 교육 기관에서 채용되고 있는 “C 프로그래밍 언어"에 대해 해설하는 입문자를 위한 문서이다.

이 책의 설명과 예제 코드는 ANSI X3.159-1989, American National Standard for Information Systems -Programming Language-C를 기반으로 설명하고 있다.

1.1 - C 언어 | 컴퓨터 시스템 개발

1.1.1 - C 언어 | 컴퓨터 시스템 개발 | 프로그램의 구조

프로그램이 작동하는 방식과 응용 프로그램이나 운영 체제 등 소프트웨어의 종류에 대해 설명한다.

소프트웨어와 프로그래밍

만약 이미 C언어 이외의 프로그래밍 경험이 있고, 프로그램이 어떤 것인지를 이해한다면 이 항목은 건너 뛰어도 된다.

모든 컴퓨터는 그 시스템은 달라도 “소프트웨어"라는 논리적인 정보에 의해 움직이고 있다. 소프트웨어는 컴퓨터가 수행해야 할 처리의 절차와 정보를 기록하는 프로그램이다. 컴퓨터가 부팅을 하고 그 때부터 정해진 절차에 따라 프로그램이 CPU에 로드된 컴퓨터의 전원이 꺼질 때까지 실행을 계속하고 있다. 컴퓨터는 소프트웨어 없이 움직일 수 없다.

CPU가 읽고 처리하는 프로그램은 기계어라고 하는 수치만으로 표현된 데이터로 구성되어 있다. 이 수치는 각각 CPU가 정하고 있는 의미로 할당되어 있으며, CPU는 명령을 읽고, 가져온 명령을 해석하고 실행한다. CPU가 다음에 읽어 들일 명령의 위치를 판단하고 프로그램을 넣는 동작을 패치(fetch)라고 한다. 이 패치 사이클과 명령어 실행 사이클을 반복하여 제대로 된 프로그램이 작동하게 된다.

그러나 프로그래머가 만드는 대부분의 소프트웨어 응용 프로그램은 컴퓨터의 전원을 켠 후 바로 동작하는 프로그램을 만드는 것은 아니다. 애플리케이션은 응용 소프트웨어 또는 응용 프로그램이라고도 불리며, 컴퓨터로 업무 처리 등 특정 이용 목적을 가지고 만들어진 소프트웨어를 가리킨다. 그럼, 응용 소프트웨어를 실행하기까지의 과정은 어떻게 되는 것인가?

컴퓨터의 전원을 켜면 처음에 정해진 프로그램이 실행된다. 그리고 하드웨어의 초기화 처리가 종료된 시점에서 정해진 기록 장치에서 프로그램을 읽는다. 이러한 일련의 동작을 부팅이라고 한다. 부트 프로세스에 대한 자세한 내용을 알고 있는 프로그래머는 극히 일부이며, 일반 프로그래머는 알 필요가 없다.

부팅 프로세스가 완료되면 운영 체제가 시작된다. 운영 체제(Operating System)는 물리적 컴퓨터의 제어 및 시스템 관리, 기본적인 작업 환경을 제공하는 소프트웨어로, 기본 소프트웨어라고도 한다. 운영 체제가 없으면 컴퓨터를 사용할 수 없다. 대표적인 운영 체제는 Microsoft Windows와 Solaris, HP-UX OS/2, Linux 등이 있다.

우리가 만드는 소프트웨어는 운영 체제를 토대로 한다. 물론 운영 시스템도 프로그램이기 때문에 개인적으로도 만들 수 있지만, 그러기 위해서는 상응하는 개발 경험과 정보 과학 및 시스템에 대한 고급 지식이 필요하다. 그러나 응용 소프트웨어는 운영 체제에서 실행되므로 하드웨어 지식은 그다지 필요하지 않는다. 운영 체제가 제공하는 기능을 사용하여 효율적으로 원하는 프로그램을 구축할 수 있다는 것이다.

비즈니스 응용 프로그램이나 게임 소프트, 심지어 바이러스조차도 어떤 기본 소프트웨어의 위에서 실행되는 응용 소프트웨어이다.

한편, 이 책에서는 “시스템"이라는 말을 많이 사용하고 있는데, 이는 운영 체제의 단순히 줄인 것이 아니라 하드웨어와 소프트웨어로 구성된 정보 처리 환경 전체를 의미한다. 예를 들어, 운영 체제와 같이 동작하는 거대한 응용 소프트웨어를 움이지는 중간 언어라고 할 수 있는 데이터를 생성하기 위해 C언어가 사용될 가능성도 있다. 따라서 C언어 동작 대상이 반드시 운영 체제라고 할 수 없기 때문에, 시스템이라는 추상적 표현을 즐겨 사용하고 있다.

1.1.2 - C 언어 | 컴퓨터 시스템 개발 | C 프로그래밍 언어

C 언어의 기본적인 특성과 국제 표준의 관계에 대해 설명하고, CPU가 인식 할 수 있는 기계어와 C 언어 등 텍스트로 표현 가능한 고급 언어의 관계에 대해 설명한다.

C 프로그래밍 언어

C 언어는 1972년에 탄생한 프로그래밍 언어로 C++ 언어를 포함하여 현재 사용되고 있는 많은 프로그래밍 언어의 기초가 되고 있다. 어셈블리 언어와 같이 매우 세부적으로 제어할 수 있는 고급 언어로 제한이 적고 유연한 프로그래밍 언어로 80년대 이후 널리 보급이 됐다. 하드웨어 성향의 프로그램에 적합한 언어이기 때문에 OS의 핵심 부분이나 가전 제품 등에 내장되는 기기의 제어용 프로그램, 게임 등 다양한 분야에서 실제 사용되고 있다.

현재는 C 언어를 기반으로 하는 C++ 언어와 함께 사용되는 경우가 많다. C++는 1980년대에 등장한 C 언어를 확장한 프로그래밍 언어이다. 문법은 C 언어에서 계승하고 있기 때문에 C 언어로 작성된 프로그램의 대부분은 그대로 C++로 컴파일 할 수 있다. 따라서 C++ 컴파일러는 C 컴파일러로 사용할 수 있는 것이 일반적이다. C++ 개발 환경이 있으면 C 언어를 학습 할 수 있다.

C 언어는 탄생부터 현대에 이르기까지 오랜 역사를 가지고, 그 사이에 많은 전문 프로그래머에게 애용되어 온 일반적인 언어이기도 하다. 현재에도 C 언어는 전문 프로그래머의 등용문 같은 존재인 것은 잘 알려져 있다.

국제 표준

C 언어로 작성된 프로그램은 컴파일러에 의해 기계어로 번역되지 않으면 실행할 수 없다. 그래서 다양한 시스템을 위한 컴파일러가 만들어지는 것이지만, 컴파일러를 만드는 사람이 마음대로 새로운 기능(문법)을 추가하거나 일부 기능을 변경하면 어떻게 될까. C 언어 방언이 대량으로 생성된 소스 코드가 특정 컴파일러에 의존하게 되어, 다른 시스템의 컴파일러에서는 컴파일할 수 없게 되어 버린다.

이러한 문제가 발생하지 않도록 전 세계적으로 사용되는 실용도 높은 프로그래밍 언어의 대부분은 국제 표준화 기구(International Organization for Standardization, ISO)에서 명확하게 문법이 정해져 있다. 표준화 기관에 의해 정해져 있는 문법을 사양이라고 하고, 이 사양에 따라 개발된 프로그램을 구현(또는 처리계)이라고 한다. ISO 표준을 따른다면 구현에 의존하지 않고 동일한 코드를 확실하게 컴파일할 수 있다.

C 언어 같은 역사가 있는 언어의 경우 ISO 표준도 여러 버전이 존재한다. 첫번째 기준은 1989년 미국에서 제정된 ANSI X3.159-1989이다. 이후 ISO/IEC 9899:1990 국제 표준화된 현재에도 이 표준은 C 언어의 주류를 이루고 있다.

C 언어 표준화의 역사

년도 내용
1972년 탄생 표준화 이전
1989년 ANSI X3.159-1989, ANSI로 표준화 통칭 C89
1990년 ISO/IEC 9899:1990, ISO로 표준화 통칭 C90
1995년 국제화 대응
2018년 ISO/IEC 9899:1990, C90의 개정 통칭 C99 I

1989년에 ANSI에서 표준화되기도 이전에 C 언어를 개발한 Brian Kernighan 씨와 Dennis Ritchie 씨의 공저 “프로그래밍 언어 C”(The C Programming Language) 표준이었다. 이 두사람 이름의 머리 글자에서 K&R이라고도 한다. 지금도 C 언어 개발자를 위한 성경적인 책으로 교과서처럼 추천하는 기술자도 적지 않지만, 코드가 오래되었고 덧붙여 초보자에게 배우기 쉬운 책이라고 말할 수 없다. 컬렉션으로 권장하지만, 학습을 위한 다른 책을 추천한다.

First edition Second edition

그후의 국제 표준 규격은 ANSI X3.159-1989를 기반으로 하고 있기 때문에, 지금도 C 언어 표준을 ANSI라고 부르는 경우도 적지 않다. 1990년에 제정된 ISO/IEC 9899:1990은 통칭 C90이라고 하고, 최신 표준은 2018년에 제정된 ISO/IEC 9899:2018을 통칭 C99이라고 한다.

주류 컴파일러의 대부분은 C90을 준수하고 있는데, C99에 대한 대응에는 편차가 있다. 현재는 C 언어의 기능밖에 사용하지 않는 코드여도 C++ 언어 코드로 컴파일하는 경우도 많고, 기능으로도 C++가 뛰어 나기 때문에 C 언어 사양을 확장할 필요성이 적기 때문에 것이다.

기계어와 고급 언어

CPU가 직접 인식할 수 언어는 기계어로 작성된 프로그램이다. 기계어는 순수한 바이너리 데이터이므로, 그 마음만 먹으면 바이너리 편집기에서만으로 프로그래밍을 할 수 있다. 그런데 숫자만으로는 인간은 읽고 쓰기가 어렵기 때문에 기계어의 숫자를 알파벳으로 대체한 어셈블리 언어라는 것도 있다.

그러나 오늘날 기계어나 어셈블리 언어를 사용하는 프로그래머는 거의 없다. 이 언어들을 사용한 개발은 너무 비효율적이다. 기계어나 어셈블리 언어를 작성하는 경우 다른 프로그래밍 언어에서 필요하지 않은 계산과 설계가 필요하며, 관리가 매우 어렵다.

그래서 숫자나 기호만으로 된 이해하기 어려운 프로그래밍보다 더 인간이 이해하기 쉬운 언어로 소프트웨어를 개발하기 위해 태어난 것이 프로그래밍 언어이다. 보통 기계어나 어셈블리 언어와 같은 컴퓨터 측의 언어를 저급 프로그래밍 언어라고 부르며, 인간의 언어에 가까운 언어를 고급 프로그래밍 언어라고 한다. C언어와 이 밖에 유명하고 많이 사용되고 있는 언어인 BASIC 또는 Java도 고급 언어에 속한다.

그러나 어떤 형태로든 최종적으로 기계어로 번역되지 않으면 CPU가 프로그램을 실행할 수 없다. 고급 프로그래밍 언어와 어셈블리 언어로 작성된 텍스트를 원시 프로그램 (또는 소스 프로그램)이라고 하고, 이를 기계어로 번역된 코드를 목적 프로그램 (또는 오브젝트 프로그램)이라고 한다.

기계어로 번역에는 몇 가지 방법이 있다. C언어는 원시 프로그램을 한꺼번에 목적 프로그램으로 변환한 후에 이를 기계어로 실행하는 방법을 사용한다.

한편, BASIC과 스크립트 언어 등은 원시 프로그램을 한 문장마다 읽고, 이것을 해석하고 실행하는 방식을 채택하고 있다. 이 경우 텍스트 데이터를 직접 실행할 수 있기 때문에 대화적인 개발을 할 수 있는 장점이 있지만, 실행에 문장을 순서대로 해석해야 하기 때문에 속도가 크게 저하된다는 것과, 원시 프로그램을 해석하고 실행하는 인터프리터라는 프로그램이 필요하다는 단점도 있다.

C언어는 최종적으로 기계어로 변환되기 때문에, 최적의 형태로 수행할 수 있다. 따라서 실행 속도가 빠르다는 장점이 있지만, 한 번 컴파일해야 한다는 것과, 컴파일된 실행 파일은 특정 시스템에 의존되기 때문에 호환성이 약하다는 약점을 가지고 있다. 그러나 현재 사용되고 있는 대부분의 프로그래밍 언어는 C언어를 기반으로 하고 있는 것이 많고, 컴퓨터의 본질적인 이해를 위해서는 C언어 학습이 필수가 되고 있다. 또한 C언어는 기계어에 가까운 고급 언어이므로 빠른 프로그램을 제공할 수 있으며, 시스템의 높은 성능을 발휘시킬 수 있다.

또한 C 언어를 발전시킨 프로그래밍 언어로 C++ (시 플러스 플러스)라는 언어가 있다. 이는 C 언어에 객체 지향이라는 개념을 도입한 것으로, 거대하고 유연한 시스템을 구축할 때 적합하다. C++는 많은 개발 현장에서 사용되고 있는데, 이것도 C언어가 기반이 되고 있다.

이 책에서는 C 언어를 배움으로써 C 언어에서 파생된 많은 프로그래밍 언어 학습의 길을 열 수 있을 것이다. 현재는 다양한 기술과 프로그래밍 언어들 이 속속 등장하고 있는데, 그 대부분은 C 언어의 영향을 받고 있다. C 언어를 아는 것은 상급 프로그래머의 첫 걸음이 되기도 한다.

1.1.3 - C 언어 | 컴퓨터 시스템 개발 | 데이터 표현

컴퓨터의 세계에서는 전기적으로 켜거나 끄거나 하는 ON, OFF 두가지 상태를 되풀이 하는 2진법으로 모든 정보가 표현되어 있다. 여기에서는 2진법이나 음수 표현에 사용되는 보수에 대해 설명한다.

기본수

정보 과학의 기초는 2진수와 16진수 등의 데이터 표현과 논리 연산에 있다. 특히 2진수와 16진수를 이해하지 않으면 C 언어를 충분히 이해할 수 없기 때문에 여기에서는 10진수를 2진수나 16진수로 변환하는 방법 등 정보의 기초 이론을 학습한다 .

우리 인간이 사용하는 숫자는 10진수라고 한다. 10진수는 0 ~ 9까지의 숫자를 사용한 10개의 기호로 구성되어 있다. 이 10진수를 사용하여 숫자를 세는 것을 10진법이라고 하고, 자리수가 올라가는 숫자 10을 기본수라고 한다. 이는 많은 현대인이 초등학생 시절부터 당연하게 가르쳐 있기 때문에 10진법을 사용하는 것이 상식으로 되어 있지만 세계 인류가 반드시 10진수를 사용하는 것은 아니며 인류가 처음부터 10개의 기호로 숫자를 세고 있었던 것도 아니다.

뉴기니의 일부 원주민은 신체의 일부를 숫자에 대응시켜 계산하는 방법을 사용하고 있으며, 홋카이도의 아이누 민족이나 로마 숫자는 5진법과 10진법을 혼합시킨 숫자를 사용하고 있다. 이렇게 대부분 숫자 5와 10을 기준으로 구분되어 있다는 특징을 볼 수 있다. 바빌로니아와 이집트의 숫자도 10개가 모이면 기호가 변화하는 구조를 가지고 있었다. 이것은 아이가 손가락으로 숫자를 세는 것처럼, 옛날 원시인이 손가락으로 손가락 계산을 하고 있었기 때문이라고 생각한다. 우리가 사용하는 0, 1, 2, 3~8, 9라는 기호는 인도가 발상지이다. 여기서 처음으로 0이라는 개념이 생기고, 이것이 아라비아에 전해져, 그 후 유럽 각지에 전해진 것으로 간주한다.

그런데, 이것이 우리가 일상에서 사용하는 숫자의 태어난 과정이지만, 컴퓨터의 숫자 분석 방법은 인간의 그것과는 전혀 다르다. 적어도 컴퓨터는 10개의 손가락을 가지고 있지 않는다. 즉, 컴퓨터가 수를 계산할 경우, 10진법으로는 그다지 좋지않다.

그래서 컴퓨터는 수를 전압의 차이로 판단하는 방법을 이용하고 있다. 전압이 높으면 1, 그렇지 않으면 0이라고 하는 방식이다. 즉, 컴퓨터가 취급하는 기호는 0과 1이고, 이 두 기호를 이용한 숫자를 2진수라고 부르는 것이다. 2진수는 1자리당 1까지만 계산되지 않기 때문에 2n마다 자릿수가 오른다. 이것은 n자리 번째 2진수의 값이 2(n - 1)인 것을 의미한다.

표1 - 10진수와 2진수

10진수 2진수
0 0
1 1
2 10
3 11
4 100
5 101
6 110
7 111
8 1000
9 1001

이와 같이 10진수는 0과 1만을 사용하는 2진수로 변환하여 표현할 수 있고, 디지털 데이터는 2진수로 표현되어 있다. 그러나 표1을 보면 알 수 있듯이, 2진수는 숫자가 많아지기 때문에 인간은 읽기가 어렵다. 그래서 인간이 디지털 데이터를 직접 다루는 경우는 16진수를 사용하는 것이 일반적이다. 2진수를 다른 진수로 변환하여 표현하는 경우, 4진수, 8진수, 16진수 중 하나를 사용한다. 4진수는 3이상이 되면 자릿수가 올라가고, 8진수는 7이상이 되면 자릿수가 오른다.

그러나 16진수의 경우는 숫자만으로는 표현할 수 없기 때문에, 9이상은 A에서 F까지의 알파벳으로 표현한다.

표2 - 다양한 기본수

10진수 2진수 4진수 8진수 16진수
0 0 0 0 0
1 1 1 1 1
2 10 2 2 2
3 11 3 3 3
4 100 10 4 4
5 101 11 5 5
6 110 12 6 6
7 111 13 7 7
8 1000 100 10 8
9 1001 101 11 9
10 1010 102 12 A
11 1011 103 13 B
12 1100 110 14 C
13 1101 111 15 D
14 1110 112 16 E
15 1111 113 17 F

표2는 2, 4, 8, 16을 기본수으로 수치의 관계를 나타낸다. 컴퓨터 내부의 디지털 데이터의 본질은 2진수이며, 프로그래밍 언어와 바이너리 편집기(디지털 데이터를 직접 편집 할 수 있는 소프트웨어) 등은 16진수로 표현한다.

음수 표현과 보수

2진수만으로 모든 것이 표현되는 세계에는 부호가 존재하지 않는다. 그래서 디지털에 음수를 표현하기 위해 사용되는 것이 보수이다. 보수(補數)는 보충을 해주는 수를 의미한다. 보수는 2종류로 나뉘는데, 더하면 하나 많은 숫자의 최소가 되는 값을 진정한 보수, 숫자의 범위에서 최대치가 되는 수를 모의 수라고 한다. 10진수로 생각하면 4에 대한 10의 보수는 6(4 + 6 = 10)이며, 9의 보수는 5(4 + 5 = 9)이다.

2진수에서 가능한 보수는 1의 보수(모의 수)와 2의 보수(진정한 보수)이다. 1의 보수의 경우, 더해서 1이 되는 수는 0이면 1, 1이면 0이므로 단순히 각 숫자를 반전 시키면 된다는 것이다. 2의 보수의 경우는 더해서 1자리에서 최소가 되는 수를 생각한다. 예를 들어 2진수 1010의 2의 보수는 0110이다. 2의 보수를 요구하는 간단한 방법은 1의 보수에 1을 가산하면 된다.

1100 1010의 1의 보수 = 0011 0101 (1100 1010 + 0011 0101 = 11111111)

0110 1101 2의 보수 = 1001 0011 (0110 1101 + 1001 0011 = 100000000)

2 진수는 2의 보수를 사용하여 음수를 표현할 수 있다. 첫째, 음수를 나타내기 위해 비트 열의 최상위 비트를 부호용으로 사용한다. 최상위 비트가 1이면 음수, 0이면 양수임을 보여준다. 양수의 경우 나머지 비트를 그대로 계산할 수 있지만 음수인 경우 나머지 비트 2의 보수가 절대 값이다.

1111 1100라는 8비트 열을 상정한 경우, 최상위 비트가 1이므로 음수라고 판단할 수 있다. 그리고 나머지 7비트 2의 보수를 구하면되기 때문에 000 0100 즉 -4임을 알 수 있다. 2의 보수를 이용한 음수 표현은 감산 처리를 가산 처리할 수 있음을 나타낸다. 2의 보수에 의한 음수 표현은 정보 이론의 기초이며 C 언어를 배우는 데에도 중요하다.

1.1.4 - C 언어 | 컴퓨터 시스템 개발 | 하드웨어 구성

표준 컴퓨터의 하드웨어 구성과 그 관계를 소개한다.

컴퓨터의 구조

다른 프로그래밍 언어의 경험으로 충분히 컴퓨터에 대한 이해를 하고 있는 경우나, 컴퓨터의 조립해 본 경험이 있으며, 하드웨어에 대한 지식이 있는 경우는 이 항목을 넘어 “개발 환경 및 컴파일러"로 이동 바란다.

C 언어는 고급 언어이면서 저급 언어에 가장 가까운 언어이기도 하다. 따라서 C 언어를 구사하기 위해서는 어느 정도의 컴퓨터에 대한 지식이 필요하다. C에서 좌절하는 사람의 대부분은 C 언어 지식보다 컴퓨터에 대한 지식이 부족하고 있기 때문에 이해하기 힘든 경향이 있다. 그래서 이 자리에서는 컴퓨터 시스템에 대한 기본을 설명한다.

원래 컴퓨터는 어떤 데이터를 입력하고 데이터를 적절한 방법으로 계산하여 그 결과를 출력하는 도구로 생각할 수 있다. 이를 실현하기 위해 필요한 최소한의 장치가 입력 장치, 출력 장치, 연산 장치, 제어 장치, 기억 장치 5개에 이를 컴퓨터의 5대 요소라고 한다.

표1 - 컴퓨터 5 대 요소

장치명 설명
입력 장치 데이터를 컴퓨터로 전송하는 장치
출력 장치 계산 결과 데이터를 추출 장치
연산 장치 데이터를 프로그램에 따라 계산하는 장치
제어 장치 다른 장치의 작동을 제어하는 장치
저장 장치 데이터를 저장하기위한 장치

그림1. 컴퓨터 5 대 요소와 관계

컴퓨터의 5 대 요소와 관계

입력 장치는 문자나 숫자를 입력하는 키보드나 좌표를 입력하는 포인팅 장치(마우스와 펜, 터치 스크린 등), 영상을 입력하는 스캐너, 마크 시트를 읽는 OMR, 시간을 입력하는 타이머 등 다양한 존재한다.

출력 장치는 디스플레이가 대표적이지만, 그 외에도 프린터나 플로터 등이 존재한다.

연산 장치와 제어 장치는 한 세트에서 중앙 처리 장치라고 한다. CPU라고 하면 알기 쉬울지도 모르겠다. 중앙 처리 장치는 입출력을 관리하고 컴퓨터 전체를 제어한다. 이것이 없으면 컴퓨터는 계산할 수 없기 때문에 컴퓨터의 중추 신경이라고 생각된다.

저장 장치는 계산하는 데이터 및 계산 결과를 저장하기위한 장소로, 특히 CPU와 직접 회선으로 연결되어있는 중요한 기억 장치를 주기억 장치라고 한다. 프로그램은 데이터의 저장과 계산 결과를 저장하는 주기억 장치를 사용하기 때문에 주기억 장치에 대한 이해는 중요하다. 저장 장치는 메모리라고도 하지만, 일반적으로 메모리라는 용어를 사용하는 경우, 주기억 장치를 말한다. 컴퓨터는 이 밖에 하드 드라이브 또는 ROM과 같은 다른 저장 장치가 존재하고 이를 보조 기억 장치라고 한다. 주기억 장치는 컴퓨터의 전원을 끄면 저장했던 데이터가 삭제되지만, 보조 기억 장치는 전원을 꺼도 데이터가 저장되어 있다. 대신 주기억 장치는 데이터 액세스 속도가 빠른 CPU와 직접 데이터 주고 받는 것에 적합하지만, 보조 기억 장치는 데이터 액세스 속도가 주기억 장치보다 느린 CPU와의 데이터 교환에 적합하지 않다. 이와 같이, 기억 장치는 그 역할에 따라 성질이 다른다.

이러한 저장 장치에 저장되는 데이터는 “2진수와 16진수"에서 학습한 2진수 바이너리 데이터이다. 이 때, 1자리 2진수 값을 ‘비트’라는 단위로 나타냈지만, CPU와 1비트 단위로 계산을 할 수는 없다. 최소 CPU의 계산 단위를 ‘바이트’라고 부르며, 일반적으로 8비트가 1바이트이다. 즉 많은 컴퓨터가 8비트를 하나의 덩어리로 취급한다.

단, 8비트 = 1바이트가 아니기 때문에 주의해야 한다. 어디까지나 바이트는 컴퓨터의 최소 처리 단위를 나타내는 1바이트의 비트 수는 컴퓨터 아키텍처에 의존하는 문제이다. 그러나 일반적으로 8비트 단위로 처리되는 경우가 많고, 확실하게 8비트를 나타내는 단위가 필요하다. 그래서 네트워크 관련 서적에서는 8비트를 나타내는 단위 “octet"를 사용한다. 1바이트는 8비트 컴퓨터에 특화한 대화는 1바이트를 8비트로 한정하여 이야기를 진행시킬 수 있지만, 네트워크 관련 서적 등에서는 컴퓨터를 제한할 수 없기 때문에 octet이 사용된다 . 그러나 까다로운 때문에 이 책에서는 원칙적으로 1바이트를 8비트로 처리한다.

저장 장치에는 어떤한 방법으로 1비트씩 데이터가 저장되어 있다. 데이터는 각각 바이트 단위로 취급되기 때문에, 저장 장치는 바이트마다 주소로 대체되는 숫자를 할당한다. 저장 장치의 데이터의 위치를 나타내는 이 숫자를 메모리 주소라고 하고, 프로그램은 메모리 주소를 사용하여 원하는 정보에 액세스한다. 이 아이디어는 C언어에서 중요한 “포인터"에서 자세히 설명한다. 이 장에서는 컴퓨터에서 처리되는 모든 데이터는 프로그램이 데이터에 액세스하기 위한 주소가 반드시 존재한다는 것을 기억하자. 어쨌든 프로그래밍 세계에서는 데이터를 참조하려면 데이터가 존재하는 위치를 지정해야 한다. 따라서 모든 데이터에 고유하게 식별할 수 있는 주소가 필요하다.

1.2 - C 언어 | C 언어 입문

1.2.1 - C 언어 | C 언어 입문 | 개발 환경 및 컴파일러

C 언어로 프로그래밍을 하는데 필요한 툴의 구성 및 개발 환경에 대해 소개한다.

프로그래밍에 필요한 것

어떤 작업에도 꼭 필요한 도구라는 것이 있다. 만화를 그리려면 원고 용지와 잉크, 펜, 자, 연필과 지우개, 스크린 톤이 필요하고, 음악 연주는 악기 외에도 악보와 악보대, 연습용 튜너와 박자 측정기 등도 필요할 것이다. 프로그래밍도 마찬가지로, 단지 컴퓨터가 있으면 만들 수 있는 것은 아니다. 효율적으로 양질의 프로그램을 생산하려면, 고급 개발 환경이 필요하다. 개발 환경은 프로그램을 만들기 위한 프로그램이다. 프로그램의 오류를 찾는 디버거(debugger) 및 프로그램을 작성하기 위한 편집기, 개발 프로세스를 자동화 해주는 CASE Tool 등이 있다.이러한 개발 환경도 프로그램에 의해 만들어지고 있기 때문에, 마음만 먹으면 스스로 만들어 버리는 것도 불가능하지는 않지만, 그런 수 있는 기술이 있다면 이 책을 손에 들고 읽고 있을 일이 없을 것이다. 그래서이 자리에서 간단히 C 언어 개발에 필요한 정보를 소개한다.

C 언어는 원시 프로그램을 목적 프로그램으로 번역하여, 그 후에 실행 가능한 형식으로 정리부터 하고 실행하는 방법으로 진행한다. 이와 같이 원시 프로그램을 목적 프로그램으로 변환하는 작업을 컴파일이라고 한다. 원시 프로그램을 목적 프로그램으로 변환하는 소프트웨어를 컴파일러라고 하며, C 언어로 작성된 소스를 실행하려면 컴파일러가 필요하다. C 언어의 개발 단계는 텍스트 편집기에서 원시 프로그램을 작성하고, 컴파일러를 사용하여 이를 목적 프로그램으로 변환한다. 그리고 마지막으로 링커(linker)라는 소프트웨어를 사용하여 필요한 목적 프로그램을 결합시켜 실행 파일을 생성한다. 대부분의 경우, 링커 등의 실행 가능 파일 형식을 만드는데 필요한 도구는 컴파일러에 포함되어 있다.

그림1 프로그램 작성 흐름

프로그램 작성 흐름

컴파일러를 구하는 방법은 다양하지만, 만약 당신이 Microsoft Windows 사용자이며, 컴퓨터 초보자라면 Microsoft Visual C ++는 통합 개발 환경을 권장한다. 이러한 개발 환경은 그래픽 소프트웨어를 개발할 수 있으며, 필요한 에디터와 컴파일러, 문서 등이 갖추어져 있다. C 언어 프로그램을 작성하여, 버튼을 누르는 것만으로 컴파일, 링킹, 실행을 할 수 있기 때문에 초보자도 다루기 쉽고, 본격적인 개발에 적합하다.

인터넷에서 무료로 제공되는 무료 컴파일러도 존재한다. 예를 들어, Embarcadero 사가 제공하는 Borland C++ Compiler 등을 사용할 수 있다. Borland C++ Compiler는 개인의 개발과 학습을 목적으로 공개된 컴파일러이며, 어떠한 보증이 없더라도 설치가 제한적이지 않다. 그러나 Borland C++ Compiler는 통합 개발 환경이 아니기 때문에, 커맨드 라인을 사용해야 한다.

텍스트 편집기로 작성된 C 언어 프로그램을 어떻게 컴파일하고 실행하거나 컴파일러에 따라 달라진다. 일반적으로 C 언어의 소스 파일 “.C” 확장자를 가진다. 또한, C 언어를 발전시킨 객체 지향 언어인 C ++ 언어 확장 “.CPP"이다.

Microsoft Visual C++와 Borland C++ Compiler 등은 C++ 컴파일러이지만 C++ 언어는 C 언어와 호환성이 있기 때문에 C++ 컴파일러로 C 언어를 컴파일 할 수 있다. C++ 컴파일러에서 C 언어를 컴파일하려면 반드시 확장자를 “.C"해야 합니다. “.CPP"로 컴파일하면, 컴파일러는 C++ 언어로 컴파일하기 때문에, 경우에 따라서는 오류가 발생한다. 컴파일에 따라서는 확장자에 관계없이, 컴파일러 옵션으로 설정할 수 있는 것도 있을지도 모르겠지만, 자세한 내용은 컴파일러 함께 제공된 설명서를 참조하도록 한다.

1.2.2 - C 언어 | C 언어 입문 | 처음하는 C 언어

프로그램의 시작점(entry point)이 되는 main() 함수의 작성과 printf() 함수를 사용하여 화면에 텍스트를 표시하는 방법을 설명한다.

첫 번째 프로그램

C 언어 프로그램은 복수의 명령을 모은 함수(function)라는 블럭이 실행 단위인 부품이 합쳐지고 구성되어 이루어 진다. 반드시 C 언어 프로그램은 하나 이상의 함수가 존재하고 이 함수에서 명령문이 작성되어 있다. 함수에는 여러가지 프로그램의 흐름이 하나의 묶음으로 등록된다.

함수는 함수를 호출로 부터 어떤 정보를 받아 정보를 처리하고 그 결과를 반환할 수 있다. 예를 들어, 삼각형의 면적을 계산하는 함수를 만드는 경우, 함수는 삼각형의 밑변과 높이를 받는다. 이 때, 함수가 받는 정보를 인수라고 한다.

함수는 받은 인수부터, “밑변 × 높이 ÷ 2"를 구하고, 그 결과를 호출한 곳으로 반환한다. 이 함수가 반환하는 값을 반환 값이라고 한다. 여기서 프로그래밍은 수학이 아니기 때문에, 함수가 받은 값과 반환 값은 수치뿐만은 아니다. 문자열이거나, 값을 받지 않거나, 반환하지 않는 함수도 있을 수 있다. 중요한 것은 어떤 목적의 하나의 처리를 함수로 정리하고, 그것을 재사용(여러 번 호출)할 수 있다는 것이다. 함수에 대해 구체적으로 이해하기 위해서는, C 언어의 기본을 더 알아야 할 필요가 있다. 자세한 내용은 나중에 “함수"에 대해 설명할 것이기 때문에, 이 자리에서는 상기한 바와 같은 함수의 기본 개념을 이해한다.

함수를 작성하려면, 앞에서 설명한 인수와 반환 값의 지정 및 함수 이름을 지정한다. 그리고 함수는 중괄호 {}로 둘러싸인 명령문으로 구성된다.

함수의 정의

반환값의형식 함수명 (가인수...)
{
    명령문
}

반환값의 형식은 값이 몇 바이트로 구성되는 것인가라고 하는 같은 정보를 컴파일러에 알리기 위한 것이다. 형식에 대한 자세한 내용은 곧 나중에 자세히 설명한다. 가인수…은 함수가 받아오는 값을 쉼표로 구분된 목록이다.

이와 같은 함수를 작성하는 것을 정의라고 한다. 그러나 이것만으로는 아직 실행되지 않는다. 함수는 호출되어야 처음으로 동작한다. 그럼, C 언어 프로그램은 어디부터 시작되는 것인가? 기본적으로 프로그램은 위에서 아래로 흘러간다. 그러나, C 언어가 실제로 실행되는 것은 main() 함수부터 시작하도록 되어 있다.

main() 함수 정의

int main(void)
{
  명령문
}

따라서, 반드시 C 언어에는 main() 함수를 정의해야 한다. 반환값의 형식인 int는 숫자 데이터를 반환하는 것을 나타낸다. C 언어는 대소문자를 구별하기 위해 Main() 혹은 MAIN()라고 쓴다면 컴파일러는 인식하지 않기 때문에 주의해야 한다. 가인수…에는 값을 받지 않는다는 것을 나타내는 void를 지정하고 있다.

응용 프로그램을 기동하면 main() 함수 안의 명령이 순서대로 실행되고, 함수의 끝에 도달하면 응용 프로그램이 종료된다. main() 함수를 호출하는 것은 운영 체제 시스템이며, 반환 값은 운영 체제에 반환된다. 덧붙여서 운영 체제 시스템이 호출해야 하는 프로그램의 첫 번째 실행 지점을 응용 프로그램 엔트리(entry) 포인트라고 한다. 그럼 세계 제일 간단한 C 언어 프로그램을 작성해 보자.

코드1

int main()
{
 return 0;
}

이 프로그램은 아무것도 하지 않고 즉시 종료하는 간단한 프로그램이다. 코드1를 실행하면 명령 프롬프트에 아무것도 표시되지 않는다. 우선 이 프로그램을 사용하여 컴파일러를 사용하여 제대로 컴파일할 수 있는지 여부를 확인해 보자. 코드에 오류가 없으면 오류 없이 빌드가 될 것이다.

컴퓨터, 그리고 프로그래밍 언어는 융통성이 없는 완고한 점이 있다. 우리 인간은 문장 속에 약간의 애매함과 마침표와 쉼표의 오타, 오탈자 등의 기술 실수가 있었다고 해도 앞뒤 문장의 흐름 등에서 상대의 의도를 추론하고 이해할 수 있다. 그러나 컴퓨터의 세계에서는 약간의 모호함과 기술 실수도 허용되지 않는다.

만약 코드에서 세미콜론(;)과 콜론(:)의 오타가 하나라도 있으면 오류로 빌드할 수 없다. 실제로 이러한 단순한 오타 초보자가 자주 머리를 아프게 하는 오류의 원인이 되고 있다. 만약 이 책의 샘플 프로그램을 열중하다가 오류가 발생한다면, 기호의 위치와 종류를 포함하여 정확히 입력되어 있는지 확인한다. 특히 괄호”(“와 중괄호 “{“의 실수와 특수 문자가 섞여있는 경우 발견이 어려울 수 있으므로 주의하도록 한다.

return은 함수를 종료하고 호출한 곳에 반환하는 것을 나타내는 문장이다. 즉, 이 프로그램은 실행 직후 return에 의해 제어를 호출한 곳에 반환하므로 즉시 종료하는 것이다. return 문은 다음과 같은 구문을 갖는다.

return [반환 ];

반환 값은 함수가 반환하는 값이 되는 식을 작성한다. 이 식의 형태는 함수를 정의하는 반환 형식과 호환이 되어야 한다.

텍스트 표시하기

다음은 화면에 문자를 표시하는 프로그램을 만들려고 하는데, 쇼킹한 것이 C 언어는 화면에 문자를 표시시키는 명령이 존재하지 않는다. 왜냐하면 하드웨어 다루는 처리는 프로그래밍 언어가 아니라 운영 체제의 역할이라고 생각하기 때문이다. 따라서 화면에 문자 출력은 운영 체제 서비스를 호출해야 한다. 그러나 화면에 문자를 표시하기 위해서만 그런 낮은 수준에 복잡한 것을 하는 것은 프로그래밍되지 않는다. 그래서 ANSI C 표준을 준수하는 컴파일러는 ANSI C 표준이 정하는 표준 라이브러리를 제공하고 있다. 표준 라이브러리는 입출력이나 복잡한 처리 등을 함수로 제공하고 있다. 우리 같은 프로그래머는 이 표준 함수를 이용하여 번거로운 작업을 생략할 수 있는 것이다.

프로그램이 의도한대로 작동하는지 확인하기 위해 결과를 화면에 표시해야 한다. 그래서 프로그래밍 언어를 학습할 때는 먼저 반드시 텍스트를 표시하는 방법을 배운다.

컴퓨터의 세계에서는 장비간에 데이터를 전달하는 것을 입출력(input/output)이라고 한다. 이 중에 다른 장비쪽으로 데이터를 송신하는 것을 출력이라고 하고, 다른 기기에서 데이터를 수신하는 것을 입력이라고 한다. 프로그램에서 다루고 있는 데이터를 화면에 표시하려면 데이터를 디스플레이에 출력해야 한다.

화면에 텍스트를 표시만 하는 프로그램이라면 매우 간단하다. C에서 한 줄의 코드만으로 임의 데이터를 출력할 수 있다.

printf("Hello, world!");

이것으로 화면에는 큰 따옴표 ““로 표시한 텍스트 Hello, world!가 표시된다. 이 코드에 포함되어 있는 의미를 제대로 이해하는 것은 쉬운 일이 아니다.

최초의 printf는 텍스트로 화면에 표시할 데이터를 출력하는 함수의 이름이다. printf() 함수에 데이터를 전달하면, 보통은 화면(Windows이면 명령 프롬프트)에 텍스트로 데이터가 표시될 것이다. 큰 따옴표로 묶인 부분 문자열이라고 한다. 문자열의 내용은 임의로 변경할 수 있다.

#include을 이용하여 헤더 추가

표준 라이브러리 함수를 사용하려면 소스 라이브러리 함수의 정의를 포함하지 않으면 안된다. 다른 소스 파일을 포함하려면 #include라는 전 처리기 지시문이라는 명령어을 사용한다. 전처리에 대해서는 뒷부분에서 자세히 설명하겠지만, 여기서는 #include를 사용하여 다른 소스 파일을 포함할 수 있다는 것만 알기로 하자. 참고로 다른 파일을 포함하는 것을 인클루드라고 한다.

#include "헤더 파일 이름"
#include <헤더 파일 이름>

#include 구문은 위의 두 가지가 있는데, 큰 따옴표는 일반적으로 자신의 라이브러리 파일을 포함하는 경우에 이용하며, 컴파일러는 인클루드를 지정한 파일과 같은 디렉토리를 검색한다. 괄호를 사용하면 컴파일러는 표준 라이브러리 파일이 위치하고 있는 디렉토리를 검색한다. 일반적으로 표준 라이브러리를 포함하는 경우는 후자의 <~>를 사용한다.

이처럼 인클루드하여 이용되는 함수들을 정의한 라이브러리를 헤더 파일이라고 하고, “.H"를 확장자로 하여 저장되어 있다. 그 중에서도 앞으로 우리가 모든 프로그램에서 반드시 사용하게 될 라이브러리가 stdio.h 헤더 파일이다. 이 헤더 파일에는 문자를 화면에 표시하거나 파일 입출력 등의 기본적인 입출력을 지원하는 함수가 정의되어 있다. 다음 프로그램은 함수를 사용하여 화면에 문자를 표시한다.

코드2

#include <stdio.h>

int main() {
 printf("Kitty on your lap\n");
 return 0;
}

코드2는 명령 프롬프트 문자열을 표시하는 프로그램이다. 코드 맨 위에 #include <stdio.h>가 추가되어 있는데, 이것은 printf() 함수의 기능을 사용하기 위해서 필요하다. 이 #include 명령은 컴파일시 <~>안에 지정된 파일을 코드에 통합하는 작용이 있다. #include 명령으로 다른 소스 파일을 가져오는 것을 인클루드한다고 한다. 또한 stdio.h와 같은 표준 기능을 이용하기 위해서는 포함하는 파일을 헤더 파일(header file)이라고 한다.

코드2는 stdio.h라는 헤더 파일을 소스 파일의 시작 부분에 불러올 수 있다. 이 stdio.h 파일 안에 printf() 함수를 사용하기 위해서 필요한 코드가 포함되어 있다.

이 프로그램은 표준 함수의 하나인 printf() 함수를 호출한다. printf() 함수는 인수에 문자열을 전달할 수 있고, 함수는 이 문자를 화면에 표시한다. C 언어에서 문자열을 큰 따옴표로 묶는다. 큰 따옴표로 둘러싸인 문자열을 문자열 리터럴이라고 한다. 위의 프로그램에서는 “Kitty on your lap"가 리터럴 문자열이다. 그 결과, 이 프로그램은 콘솔에 Kitty on your lap 문자열을 표시한다. printf() 함수에 전달하는 리터럴 문자열의 내용을 자신이 좋아하는 문자열로 대체하여 콘솔에 문자열이 표시되는지 시도해 보자.

문자열의 끝에 있는 \n는 줄바꿈을 나타낸다. Visual C++의 “디버깅하지 않고 시작"부터 기동하면, 프로그램 종료시에 “계속하려면 아무 키나 누르십시오.“라는 텍스트가 추가되므로 프로그램에서 출력된 결과의 레이아웃을 위해 사용하고 있을 뿐이다. 필요 없으면 생략해도 상관없다. 줄바꿈과 같은 눈에 보이지 않는 특수 문자를 처리하려면 이와 같이 기호를 사용하여 표현한다. 이러한 \로 시작하는 문자열의 문자를 이스케이프 문자라고 한다. 문자로 \를 표현하고 싶으면 \\로 기술한다. 이스케이프 문자에 대한 자세한 내용은 앞으로 배우게 될 “상수"에서 자세히 설명한다.

또한 리터럴 문자열은 줄바꿈할 수 없다. 위의 프로그램을 다음과 같이하면 컴파일러는 오류를 발생한다. 줄바꿈을 표현하는 기호을 가진 이유가 이것으로 이해할 수 있다.

코드3

#include <stdio.h>

int main() {
  printf("Kitty on your lap\n
 ");
  return 0;
}

printf() 함수의 인수로 지정하는 문자열 리터럴이 도중에 줄바꿈을 하고 있다. 이 경우 컴파일러는 큰 따옴표가 닫혀 있지 않다고 인식 오류를 출력한다. 그렇다면 긴 문자열의 경우, 한 줄로 길게 쓸 수 밖에 없다. 물론 그래도, 프로그램이 올바르게 작동을 하긴 하지만 소스의 가독성 떨어진다는 단점이 있다. 이 경우 해결 방법은 두 가지가 있다.

하나는 개행 문자 앞에 \ 기호를 붙인다. 이 경우 \는 행 연속 문자로 기능하고, \ 기호에 계속되는 개행 문자를 무시한다. 행 연속 문자는 문자열에서뿐만 아니라, 소스의 모든 곳에서 사용할 수 있다.

"Kitty on \
your lap";

또 다른 방법은 단순히 문자열을 분할하여 작성하는 방법이다. C 언어에서는 큰 따옴표가 분할되는 경우는 컴파일할 시에 이를 결합한다.

"Kitty on " "your lap"

예를 들어, 위의 공백으로 구분된 두 개의 문자열은 “Kitty on your lap"인 것으로 간주한다. 따라서 줄바꿈할 위치에 한번 문자열을 닫고, 개행 후에 문자열을 쓴다.

코드4

#include <stdio.h>

int main() {
 printf("나 보기가 역겨워 가실때에는\n\
말없이 고이 보내드리오리다.\n");

 printf("영변의 약산 진달래꽃 아름따다\n"
    "가실길에 뿌리오리다\n");
   
    printf("가시는 걸음걸음 놓은 그 꽃을\n");
    printf("사뿐히 즈려밟고 가시옵소서\n");
    
    return 0;
}

이 프로그램은 앞에서 제시한 방법을 사용하여, 문자열의 중간에 줄바꿈을 시도하고 있다. 첫번째 printf() 함수에서 행 연속 문자를 사용하여 개행 문자를 제거하는 방법을 사용하고 있다. 두 번째 printf()은 중간에 문자열 리터럴을 닫고, 개행 후에 다시 문자열 자세히 쓴는 방법을 사용하여 있다. 혹은 데이터마다 printf()로 출력해도 된다. 두번째의 방법이라면 문자열을 닫은 후에 자유롭게 탭 문자 등을 삽입할 수 있기 때문에 일반적으로 두번째나 세번째의 방법이 사용된다.

1.2.3 - C 언어 | C 언어 입문 | 토큰과 문장

C언어의 토큰과 문장에 대해 설명한다. 코드에 적혀있는 모든 문자 순서와 기호는 토큰이라는 최소 단위로 분해할 수 있다. 여러 토큰 순서는 문장라는 작은 실행 단위이다.

코드의 최소 단위

아무리 복잡하게 작성된 프로그램도 토큰(token)이라는 최소 단위의 텍스트로 분해할 수 있다. 토큰은 더 이상 분해할 수 없는 프로그램의 최소 단위이며, 영어의 단어에 해당하는 것이다. 프로그래밍 언어의 명령이 되는 문장은 여러 토큰으로 구성되어 있다.

프로그래밍 언어를 기계어로 번역하기까지 여러 공정을 거쳐야 한다. 컴파일러에 입력된 텍스트는 소스 코드에 쓰여진 텍스트가 C언어에서 정하고 있는 문법을 준수하는지 여부를 확인하기 위해 토큰의 열에 분해된다. 이 공정을 어휘 분석이라고 한다.

토큰에는 여러 종류가 있으며, 프로그래밍 언어의 사양에 정해진 키워드, 조작 대상의 이름을 나타내는 식별자, 계산 기호 등의 연산자 등이 있다. 토큰은 다음 6 가지로 분류된다.

  • 식별자 (identifer)
  • 키워드 (keyword)
  • 상수 (constant)
  • 문자열 리터럴 (string-literal)
  • 연산자 (operator)
  • 구분자 (punctuator)

키워드는 C언어가 사양 레벨에서 예약되어 있는 이름(영단어)의 것으로, 이전의 프로그램에서 소개한 return 등은 키워드에 속한다. Visual C++ 같은 표준적인 개발 환경의 텍스트 편집기에서 파란색으로 표시된다.

식별자는 작업하는 데이터 및 명령을 식별하기 위해 소스 상에 명명된 이름이다. 예를 들어, main 등의 함수 이름은 식별자이다.

상수는 코드 상에 지정된 고정적인 값이다. 예를 들어 10과 3.14라는 같은 수치는 상수이다.

문자열 리터럴도 상수와 마찬가지로 코드 상의 고정 값에서 큰 따옴표로 묶인 여러 문자를 나타낸다. 문자열 리터럴은 코드에 텍스트를 삽입할 수 있다.

연산자는 계산을 위해 사용하는 기호으로써, + 기호와 - 기호 연산자에 속한다.

구분자는 [ ] ( ) { } * , : = ; … # 같은 기호로, 어떤 요소를 구분하고 모와 정리하는 것을 나타내는 기호로 사용되고 있다. 일부 기호는 연산자와 동일하지만, 기호가 출현하는 위치에 따라 연산자인지 구분 기호인지를 확인할 수 있다. main 함수 이름 뒤에 괄호와 명령의 끝을 나타내는 세미콜론(;) 등은 구분자로 분류된다.

예를 들어, 아래 예제를 보도록 하자.

#include <stdio.h>

int main() {
 printf("Kitty on your lap\n");
 return 0;
}

이 프로그램은 다음과 같은 토큰 열로 구성되어 있다. 그러나 여기에 맨 위에 있는 #include부터 시작되는 행을 생각하지 않는다.

토큰 토큰의 종류
int 키워드
main 식별자
( 구분자
) 구분자
{ 구분자
printf 식별자
( 연산자
“Kitty on your lap \ n” 문자열 리터럴
) 연산자
; 구분자
return 키워드
0 상수
; 구분자
} 구분자

컴파일러는 이런 문장을 토큰 레벨로 분해하여, 하나 하나의 명령을 인식하고 있다. 한국어와 같은 인간이 사용하는 자연 언어에 비해 프로그래밍 언어에 불필요한 기호가 하나도 없다는 것을 알 수 있을 것이다. 프로그램 중의 모든 문자와 기호는 명확한 의미가 정의되어 있다. 거기에는 애매한 것은 없다.

각 토큰의 의미는 C언어 학습을 진행하면서 조금씩 이해해 나갈 것이다. 중요한 것은 소스 코드에 쓰는 모든 기호와 알파벳은 위의 토큰 중 하나로 분류할 수 있다는 것이다.

컴파일할시에 문법 등에서 에러가 나오는 경우는 작성한 코드가 올바른 토큰의 줄(line)인지 여부를 확인하여 오류를 쉽게 찾을 수 있을 것이다. 익숙한 기술자라면 의식하지 않고 토큰의 종류와 순서를 파악하고 컴파일러에 의존하지 않고 코드가 맞는지를 확인할 수 있다.

토큰 사이에 연산자와 구분자를 사용한 토큰이 있으면 구문에 따라 토큰은 분해되지만 키워드와 식별자, 상수 등이 연속적으로 계속되는 경우, 각각의 토큰은 공백으로 분리해야 한다. 정확히 공백(white space)이라는 부르는 다음 문자는 토큰을 분리할 수 있다.

  • 스페이스(space)
  • 수평 탭(\t)
  • 수직 탭(\v)
  • 개행, 줄 바꿈(\n\r)
  • 페이지 나누기(\f)

이 중에 수직 탭 및 페이지 나누기는 주로 프린터를 대상으로 한 전용 문자이며, 현재 일반적인 PC나 텍스트 편집기는 사용되지 않는다. Visual Studio에 의한 개발에서 사용하는 공백은 스페이스, 수평 탭, 개행 중 하나가 될 것이다.

코드1

  int
main (
void
){
   return
0;
  }

위의 코드1은 코드가 엉망이 되어있는 것처럼 보이지만 성공적으로 컴파일하고 실행할 수 있다. 공백이나 줄 바꿈이 제각각이므로 읽기 어렵지만, 각각의 단어나 기호의 줄을 토큰 열로 보면 올바르다는 것을 확인할 수 있다.

마찬가지로 토큰의 줄이 맞다면 수평 탭과 줄 바꿈없이 한 줄에 프로그램을 작성할 수 있습니다.

코드2

int main(){return 0;}

연산자와 구분자라는 기호의 토큰은 전후의 토큰을 구분하는 특성을 가지고 있다. 따라서 괄호()와 중괄호{}, 세미콜론; 등 전후의 토큰에는 공백이 없어도 문제 없다.

한편, 토큰 구분이 제대로 이루어지지 않은 경우 컴파일러 오류가 될 것이다. 예를 들어 다음과 같은 코드는 구문 오류다.

intmain(){return0;}

이 경우 시작 부분의 키워드 int과 함수 이름 main을 구분하지 않기 때문에 intmain라는 하나의 토큰으로 간주되어 버린다. 또한 함수 안에 return 키워드와 숫자 0 사이도 구분하지 않기 때문에 return0라는 토큰으로 해석된다. 컴파일러는 이 이름을 처리하지 못하고 오류를 보고할 수 있다.

반대로 구분해서는 안되는 부분에 공백이 삽입된 경우에도, 토큰이 추가로 분리되어 버려서 오류가 되어 버리는 것이다.

int mai n(){ retu rn 0; }

위의 코드는 함수 이름과 식별자 main 중간이 공백으로 구분되어 버려서 mai과 n이라는 두 개의 토큰으로 나누어져 있다. 컴파일러는 식별자 mai 직후에 나타난 n이 구문 부정이라고 판단하고 오류를 보고 한다. 마찬가지로 return 키워드를 공백으로 retu과 rn이라고 두 개의 토큰으로 분해되어 버리고 있다. 컴파일러는 retu과 rn라는 이름을 확인할 수 없기 때문에 역시 오류이다.

명령의 실행 단위

여러 토큰으로 이루어진 하나의 실행 단위를 문장 (statement)이라고 한다. 즉, 컴퓨터에 대한 명령은 문장 단위이며, 함수는 문장의 집합이라고 생각할 수 있다. 많은 문장은 세미콜론;으로 종료하기 위해 컴파일러는 세미콜론을 찾아내서 문장이 완료된 것으로 인식할 수 있다. 예를 들어 다음 프로그램은 2개의 문으로 이루어져 있다.

printf("Stand by Ready!!\n");
return 0;

화면에 텍스트를 표시하기 위해 printf() 함수의 행과 return 키워드로 시작하는 줄의 끝에 세미콜론; 기호가 있다. 이들은 여기에 문장이 종료하는 것을 나타낸다. 코드를 보기 쉽게하기 위해 문장이 끝나는 개행을 넣지만, 개행 자체는 의미를 가지지 않기 때문에, 원한다면 한 줄에 여러 문장을 작성할 수 있다.

코드3

#include <stdio.h>
int main() { printf("Stand by Ready!!\n"); return 0; }

코드3의 main() 함수에서 여러 문장을 줄 바꿈없이 기술하고 있지는데, 컴파일러는 세미콜론으로 문장의 끝을 인식할 수 있기 때문에 문제없이 컴파일 수 있다. 다만, 첫번째 행의 #include 전처리기 지시문(preprocessor directive)은 문장이 아니기 때문에 개행으로 종료시킬 필요가 있다.

문장이라고 해도 여러 종류가 존재하며, 크게 다음과 같이 분류되어 있다.

  • 명찰 부착 문장 (labeled-statement)
  • (표현)식 문 (expression-statement)
  • 복합 문 (compound-statement)
  • 선택문 (selection-statement)
  • 반복 문장 (iteration-statement)
  • 점프 문 (jump-statement)

이 중에 일반적인 계산이나 기능의 호출은 표현식 문로 분류된다. 함수 안에 기술된 문장을 많은 표현식 문으로 될 것이다. 표현식 문이나 점프 문 등은 끝에 세미콜론;를 붙이지 않으면 안되도록 정해져 있지만, 반드시 모든 문장 끝에 세미콜론이 추가되는 것은 아니다.

예를 들어, 복합 문장은 세미콜론을 붙이지 않는다. 복합 문은 함수 본체 (function-body)에 이용하고 있는 {}로, 블록이라고도 한다. 함수 본체는 하나의 복합문으로 생각할 수 있지만, 함수의 끝에;를 붙일 필요가 없는 이유는 복합 문장의 끝은 시작을 나타내는 중괄호 {에 대응 하는}으로 판단되기 때문이다.

각각의 문장이 구체적으로 무엇인지는 여기에서 순차적으로 설명하고 있다. 지금은 문장이 위와 같이 분류되어 있음을 기억해 두도록 한다.

1.2.4 - C 언어 | C 언어 입문 | 주석(comment)

주석(comment)를 이용하여 코드에 어떤 메모를 남길 수 있다. 주석은 컴파일시 무시되므로 코드에 영향을주지 않는다.

언젠가 다시 보기 보기 위하여

이 책의 C 언어 샘플 프로그램은 아무리 길어도 수십 줄 정도일 것이다. 이는 독자가 각 장이 주제에 따라 중요한 기능의 원리를 충분히 학습할 수 있도록, 소스 코드는 가능한 심플한 형태로 정리하고, 불필요한 것을 최대한 포함하지 않았기 때문이다.

그러나 실제 개발 현장의 코드는 적어도 수천, 대규모 개발의 경우 수십만 ~ 수백만 줄에 달한다. 이 정도의 대규모 프로그램 코드의 경우 개발자도 관리가 힘듭니다. 다른 사람이 쓴 소스을 읽어야 하는 경우도 있다. 비록 자신의 코드도 나중에 다시 읽어 때, 그것이 무엇을 의미하는지 알 수 없게 될 수있을 것이다.

그래서 프로그램의 실행과는 아무 상관없는 코멘트(주석)을 소스에 적어 둘 수 있다. C 언어 주석은 /*로 시작해서 */로 끝난다. /**/ 사이의 문자는 예외없이 컴파일시에 제거된다. 개행 문자를 포함할 수 있기 때문에, 여러 줄의 코멘트도 가능하다. 코멘트를 남겨두면 다른 사람이 소스를 볼 때, 무엇을 하고 있는지를 설명할 수 있으며, 향후 스스로 다시 읽게 되면 코드가 무엇을 의미하는지를 기억나게 해줄 것이다.

코드1

#include <stdio.h>

int main()
{
    /* 코멘트이므로 프로그램에는 어떤 영향도 없다 */
    printf("이것은 실행됩니다.\n");
    /* printf("코멘트에 포함되어 있기 때문에 실행되지 않는다.\n"); */
    /*
    소스를 해독 할 수 있도록 보충 설명 등을 기술한다.
    댓글은 몇줄 있어도 괜찮다.
    */
    return 0;
}

이 프로그램은 코드 내에 한글로 코멘트를 남기고 있다. 이처럼 대규모 개발 프로젝트는 프로그램의 설명을 코멘트로 남기는 것이 바람직하다. 다만, 코멘트는 문자열 리터럴 안에는 남길 수 없다.

또한 코멘트을 겹쳐서 작성할 수 없다. 예를 들어 /* /* */ */ 라고 쓴다면 /* /* */까지가 주석으로 해석된다.

C 언어 사양으로 정의되어 있지 않지만, 많은 컴파일러는 한 줄 주석도 지원하고 있다. 한줄 주석은 연속된 슬래시 문자 // 이루어지고, 여기에서 다음 줄까지를 코멘트로 한다. 예를 들어 다음과 같이 될 것이다.

// 여기 코멘트이다.
// 범위는 1행 뿐이므로, 각 행에 지정해야 한다.
printf("Kitty on your lap"); // 줄의 도중에도 가능하다.
/////////// 이것도 코멘트이다. ///////////

이 한줄 주석은 C++ 언어 사양이며, C 언어 사양은 아니지만, 현대의 많은 컴파일러는 C++를 지원하기 때문에, C 언어에서도 한줄 댓글을 허용하고 있다. 예를 들어, 어떤 행을 삭제하고 컴파일하고 싶은 경우는이 한줄 주석을 사용하여 스마트한 실험하는 것도 할 수 있는 것이다. 물론 순수한 C 코드를 작성하려 한다면, 이 코멘트는 사용해서는 안된다.

참고로 C 언어의 코멘트 /* */ C ++에서 추가된 코멘트 //는 꽤 많은 프로그래밍 언어에서 공통 코멘트의 표준적인 존재이다. C/C++ 언어는 물론 Java 언어, C # 언어, JavaScript 언어 등에서도 이 댓글이 사용되고 있다.

1.2.5 - C 언어 | C 언어 입문 | 변수와 데이터 유형

변수 선언과 데이터 유형에 대해 설명한다. 계산 결과 등을 프로그램 내에서 재사용하기 위해 임시로 저장하려면 변수를 사용한다.

변수의 정의 및 초기화

“정보 처리"는 정보를 컴퓨터에 입력하고, 프로그램이 이를 분석하여 원하는 형태로 변환하여 출력하는 기본적인 과정을 말한다. 이를 실행하려면 프로그램은 정보를 받아 계산해야 한다. 그러기 위해서는 먼저 정보를 기록하는 방법이 필요하다. 그렇지 않으면 받은 정보와 계산 결과를 저장할 수 없다.

그래서 프로그램은 일시적으로 정보를 주기억 장치에 저장하여 연산 결과 등을 기록한다. 주기억 장치는 CPU와의 데이터 전송 속도를 엄격하게 생각해서 구성되어 있는 휘발성(전원을 끄면 저장하고 있던 정보를 잃게 저장 장치) 장치로써 비트당 비용이 상대적으로 비싸다.

사용자가 데이터 파일을 저장하고 있는 저장 장치는 하드 디스크나 플로피 디스크이지만, 이러한 저장 장치는 CPU로의 데이터 전송 속도가 매우 느려서 프로그램의 일시적인 데이터 저장에는 적합하지 않다. 그 대신 주기억 장치에 비해 비트당 비용이 저렴하고, 비휘발성(전원 공급이 끊어져도 데이터를 저장할 수 있는 저장 장치)이므로 장기 데이터 저장에 적합하다.

주기억 장치에 정보를 저장하기 위해, 기계어에서는 저장할 위치를 나타내는 주소를 지정한다. 그러나 그런 귀찮은 것은 전문 프로그래머도 하고 싶은 작업이 아니다. 계산이 복잡하고, 조금이라도 잘못된 주소를 지정하면 프로그램은 충돌하게 된다. 이 작업은 누가 봐도 효율적이라고 할 수 없다.

그래서 고급 프로그래밍 언어에서는 변수를 사용한다. 변수는 메모리의 특정 저장소의 대명사와 같은 것이다. 변수는 식별자로 구분하고, 컴파일러는 식별자 및 물리적 저장 공간을 연결하여 정보의 쓰기와 읽기를 정확하게 수행한다. 따라서 우리는 복잡한 메모리 주소 계산에서 해방되어, 기억 공간의 할당 및 계산을 컴파일러에 맡길 수 있다. 따라서 초보자도 간단히 메모리를 사용할 수 있다.

그러나 변수는 메모리의 저장 공간을 나타내는 식별자이며, 실제 저장 공간은 크기가 존재한다. 어떤 저장 영역은 4바이트를 확보하고 있을 수도 있고, 다른 영역은 1바이트일 수도 있다. 이렇게 할당 저장 공간의 크기를 설정하기 위해 변수를 선언해야 한다. 여기에는 다음과 같은 구문을 사용한다.

변수 선언

유형 변수1, 변수2, ...;

유형은 형식 지정자라는 것을 지정한다. 형식 지정자는 변수의 종류를 나타내는 것으로, 컴파일러는 형식에 따라 메모리를 할당하고 메모리의 주소를 계산한다.

유형의 다음의 토큰은 변수의 이름을 지정한다. 변수 이름에는 문자와 숫자를 사용할 수 있지만, 첫번째 문자는 숫자를 사용하지 못하고 반드시 영문자 또는 밑줄(_)로 시작해야 한다. 또한, int와 return 등 C 언어를 사용하는 키워드를 식별자로 할 수 없다. 식별자의 대문자와 소문자는 구별된다. 이 식별자 명명 규칙은 C 언어의 모든 식별자의 이름에 해당된다.

여러 변수를 동시에 선언 또는 정의하려면 콤마(,)로 구분한다. 프로그램은 여기에 지정된 변수의 이름으로 기억 영역에 액세스할 수 있다. 마지막으로, 선언은 세미콜론(;)으로 종료한다. 구문상, 선언은 문장에 포함되지 않는다.

형식 지정자는 표1과 같다.

표1 - 형식 지정자

형식 지정자 크기
char 1바이트
int CPU의 표준 정수 크기
float 짧은 정밀도 부동 소수점 숫자
double float형 이상, long double형 이하의 배정 밀도 부동 소수점 형

char형은 크기가 정해져 있기 때문에 매우 알기 쉽다. 이것은 영숫자 1문자분의 기억 영역을 나타낸다. int형이라는 것은 보통의 정수를 저장하기 위한 저장 공간으로, 그 크기는 컴퓨터에 따라 다르다. 이 크기는, 예를 들어 32비트 CPU라면 32비트(4바이트)를 할당할 수 있다. 64비트 CPU의 컴퓨터라면 64비트, 아주 오래된 CPU를 사용하는 16비트 시스템이면 16 비트가 할당된다는 것이다.

float와 double은 소수점을 취급하는 경우에 사용하는 저장 영역이다. 오차를 최대한 줄이고 싶은 계산 등에 사용할 수 있다. 이도 int 마찬가지로 기종에 따라 달라진다. 형식 지정자가 할당되는 구체적인 크기는 컴파일러 문서를 참조한다. 다음 문장은 정수형 변수 iVariable1과 iVariable2을 정의하고 있다.

int iVariable1 , iVariable2;

그러나, 이 시점에서 변수에 어떤 값이 저장되어 있는지 정해져 있지 않다 (아무 의미도 없는 부정 값이 저장되어 있다). 변수에 값을 저장하려면 대입 연산자 등호(=)를 사용하여 변수에 데이터를 할당한다. 다만, 일반적인 수학 표기법과 달리, 좌변에 대입하는 변수를 지정하고 오른쪽에 식을 기술한다. 예를 들어 다음 문장은 변수 iVariable1에 10이라는 값을 할당한다.

iVariable1 = 10;

이것은 iVariable1가 나타내는 기억 영역에 10이라는 값을 저장하는 것을 의미한다. 또한 이와는 별도로 변수를 선언할 때 초기 값을 주는 방법도 있다. 변수의 초기화에는 이니셜라이저를 사용한다.

유형 변수1 = 초기식1, 변수2 = 초기식2 ...;

형식 지정자 및 변수 이름 지정까지는 방금 전의 선언과 동일한다. 초기화는 등호(=)를 사용하여 변수의 초기 값을 나타내는 식을 지정한다. 초기화되지 않은 변수는 어떤 값이 대입될 때까지 어떤 값을 포함하고 있는지 보장되지 않는다. 확실히 변수에 초기 값을 할당하려면 이렇게 초기 값을 주는 것이 좋다.

코드1

#include <stdio.h>

int main()
{
  int iVariable1 = 10 , iVariable2;
 iVariable2 = 100;
 return 0;
}

코드1는 변수를 선언하면 메모리에 지정된 영역을 확보하고 그 영역에 지정된 값을 대입하고 종료한다. 변수의 값을 화면에 표시하는 방법은 아직 설명하지 않기 때문에, 여기서는 실시하지 않는다. 그래서 이 프로그램은 오류가 발생하지 않으면 성공이다.

이 프로그램에서는 먼저 변수의 선언을 하고 있다. int iVariable1 = 10이라는 것은 iVariable1라는 변수를 10으로 초기화하고 있다. 그 후에 콤마를 사용하여 iVariable2을 정의하고 있는데, 이 변수는 초기화되지 않는다. 이어 iVariable2 = 100는 대입식을 이용하여 변수 iVariable2에 100을 대입하고 있다.

덧붙여서 식에서의 항목을 피연산자라고 한다. iVariable2 = 100의 경우 iVaeiable2과 100는 피연산자이고, 등호(=)를 대입 연산자라고 한다. 다만, 이니셜라이저와 식을 혼동하지 말자. 변수 이니셜라이저에 사용되는 등호(=)는 일반적으로 대입식과는 기본적으로 이질적인 것이다.

서식 지정

그럼 이제 변수를 선언하고 값을 할당하는 방법은 알았다. 하지만 이것으로 정말 제대로 값이 할당되어 있는지 여부 모른다. 그래서 printf() 함수를 사용하여 변수의 내용을 출력하고 싶다.

printf() 함수에서 문자열이 아닌 값을 출력하려면, 서식 제어 문자열이라는 것을 서식 지정을 해야 한다. 형식 지정은 반드시 백분율 기호(%)로 시작하여, 왼쪽에서 오른쪽으로 해석되어 간다. printf() 함수는 단순히 리터럴 문자열을 출력하는 함수가 아니라, 실제 고급 형식 변환 함수에서 변수를 문자열의 어디에 어떤 형태로 할당할지 여부를 지정 할 수 있다. 그것을 수행하는 것이 서식 제어이다.

printf() 함수는 두번째 인수 이후에 임의의 수 만큼 인수를 지정할 수 있으며, 첫번째 인수에는 서식 제어 문자열을 지정한다. 서식 제어 문자열에 % 기호를 이용한 서식을 사용할 수 있으며, printf() 함수는 첫번째 형식 지정을 발견하면 두번째 인수의 변수를 지정한 형태로 출력하고, 두번째 서식 지정를 발견하면, 세번째 인수의 변수를 지정한 형태로 출력하는 같은 구조로 되어 있다. 따라서 서식 제어 문자열에 있는 서식의 수 만큼, 두번째 인수 이후에 변수를 지정한다. 서식 지정보다 인수의 수가 많으면 여분의 인수는 무시되지만, 서식 지정보다 인수의 수가 적은 경우, 동작은 보증되지 않는다.

printf() 함수의 서식 제어는 사실은 매우 복잡한 구조로 되어 있기 때문에 이 자리에서 자세한 설명은하지 않는다. 지금은 변수의 내용을 표시하는 데 필요한 형식만 설명한다.

서식 인수 형 변환 결과
%d 또는 %i int 10 진수 정수
%x int 부호 없는 16 진수. 9 이상은 “abcdef"를 사용
%X int 부호 없는 16 진수. 9 이상은 “ABCDEF"를 사용
%c int 문자
%s char * 문자열
%f double 부동 소수점 [-]dddd.dddd 형식
%e double 부동 소수점 [-]d.dddd e [+]ddd 형식
%E double 지수의 앞에 붙는 것이 e 대신 E 인 점을 제외하고 %e 서식과 동일
%g double % f 또는 % e 중 지정된 값과 정확하게 표현 가능한 짧은 형식.
%G double 지수 앞에있는 것이 e 대신 E 인 점을 제외하고는 %g의 서식과 동일
%% - 변환하지 않고 두 %보기

우선, 이 자리에서는 이것만 이해하고 있으면 충분하다. int 변수를 표시하려면 서식 지정에 %d를 지정하고 그 후에 인수 목적 변수를 지정한다. 예를 들어 printf("%d", iValiable);라고 하면, iValiable 변수의 값이 표시된다. 인수를 복수 지정하는 경우, 인수는 콤마(,)로 구분한다.

코드2

#include <stdio.h>

int main() {
 int iValiable = 10;
 printf("iValiable = %d\n" , iValiable);
  return 0;
}

이 프로그램을 실행하면 iValiable = 10라고 화면에 표시된다. 이 결과를 보면 서식 제어 문자열에 지정된 %d가 iValiable의 내용으로 변환되어 있는 것을 알 수 있다.

형식 지정자의 혼합

변수 선언자에는 “변수의 정의 및 초기화"에서 소개한 기본형 이외에도 세부적 변수의 크기와 특성을 지정하는 형식 지정자가 존재한다. 방금 전에 소개한 기본적인 형식 지정자 외에, long과 short라는 지정자도 존재하고 이들을 이용하여 일반 숫자와는 다른 길이의 변수를 만들 수 있다. 이러한 형식 지정자를 사용하여 자격을 선언에는 다음과 같은 것이 있다.

형지정 크기
short int 16 비트, int 이하. int는 생략 가능.
long int 32 비트 이상, int와 동일하거나 그 이상. int는 생략 가능.
long double double과 같거나 그 이상.

short intlong int는 short나 long과 같이 생략된 형태를 사용하는 것이 가능하며, 일반적으로 생략한다. 일반적으로 short는 16비트 long은 32비트인 경우가 많을 것이다. 부동 소수점 형의 크기도 정수처럼 처리계에 의존하기 때문에 long double은 double과 같은 크기 일 수도 있고, 다를 수도 있다. 예를 들어 Microsoft Visual C ++ 6.0에서 long double은 80비트 (부호에 1비트, 지수에 15비트, 가수 64비트)로 되어 있다.

또한 숫자형 변수는 음수를 나타낼 수 있다. 그러나 음수를 표현하는 경우는 최상위 비트를 부호용으로 사용하기 때문에 표현 가능한 최대 값이 반이 되어 버리는 단점도 있다. 음수를 표현하지 않는 경우, 최상위 비트도 숫자 표현을 위해 사용할 수 얻으면, 표현할 수있는 범위가 확대된다.

그래서 부호가 있는 변수를 선언하는 경우 signed를 부호가 없는 경우는 unsigned 형 지정자를 사용한다. 일반적으로 signed를 분명히 적을 필요는 없지만, 컴파일러는 옵션으로 변수의 기본값을 부호 없이도 가능하고, 그러한 경우에 대비하여 음수를 취급하는 것을 명시적으로 표현하기 위해 사용할 수 있다. 음수를 취급할 필요가 없는 변수는 unsigned를 지정하여 높은 수치를 취급할 수 있게 된다.

예를 들어, 부호가 있는 char 형 변수는 -127 ~ +127 까지의 범위를 처리할 수 있다. 1바이트는 순수하게 생각하면 255까지, 이진수 1111 1111까지 이용할 수 있지만, 부호가 있는 경우는 이 중 최상위 비트가 플래그로 사용되기 때문에 부호가 있는 char 형 변수에 255을 대입하면 최상위 비트가 1이므로 음수 판단되며 2의 보수 표현으로는 2진수 1111 1111의 2의 보수를 구한 값 0000 0001 즉 -1이다. 색상 등의 정보를 1바이트로 취급 경우, 음수를 사용할 필요가 없기 때문에 unsigned를 사용하여 부호를 작성하는 방식이 사용된다.

코드3

#include <stdio.h>

int main() {
 signed char chVariable = 255;
 unsigned char uchVariable = 255;

  printf("chVariable = %d\nuchVaruabke = %d\n" , chVariable , uchVariable);
 return 0;
}

코드3은 부호가 있는 char형, 부호가 없는 char 형 변수에 255을 대입하고 있다. 계산대로 부호 있는 변수는 -1을 부호 없는 변수는 255을 출력한다. 그럼 1바이트의 변수에 대해 1바이트로 표현할 수 없는 높은 수치를 즉, 255 이상의 값을 대입하면 어떻게 될까? C 언어에서는 변수의 크기 이상의 값을 대입하면, 부호 없는 경우는 상위 비트가 비트가 잘려서 할당된다. 부호 있는 경우는 구현에 의존한다.

1.2.6 - C 언어 | C 언어 입문 | 상수

숫자나 문자 등의 고정 데이터를 소스 코드에 작성하는 방법을 설명한다.

접미사

100와 같은 숫자나 큰 따옴표로 둘러싸인 문자열 등을 총칭하여 상수라고 한다. 사실, 상수도 변수와 같은 형태가 존재한다. 상수를 변수에 할당하는 경우, 물론 상수는 변수의 형에 대해 호환이 되어야 한다.

숫자를 나타내는 상수는 부동 소수점 상수 및 정수 상수로 나누어 진다. 숫자만의 경우, 컴파일러는 int형의 정수 상수로 판단하고, 소수점이 발견되면 double형 상수로 처리한다. 그러나 이것으로는, float와 long double, long형 등의 상수를 표현할 수 없다. 그래서 C 언어에서는 정수 형을 명시하기 위해 숫자의 끝에 형태를 나타내는 알파벳을 지정한다. 이를 접미사라고 한다. 정수 상수로 지정할 수 있는 접미사는 표1 중에 하나이다.

표1 정수 접미사

접미사 기입 예
l 또는 L 상수의 크기에 따라 long int 또는 unsigned long int 123456789L
u 또는 U 상수의 크기에 따라 unsigned int 또는 unsigned long int 123456789U
l 또는 L과 u 또는 U unsigned long int 123456789UL

이러한 접미사를 정수 상수로 지정하여, 정수 형을 명시할 수 있다. 마찬가지로 부동 소수점 상수도 float, double, long double 여부를 지정하는 접미사가 존재한다. 부동 소수점 상수는 표2 중 하나의 접미사를 지정할 수 있다.

표2 부동 소수점 상수 접미사

접미사 기입 예
f 또는 F float 3.14F
l 또는 L long double 3.14L

일반적으로 접미사를 지정하지는 않지만, 정수 형을 컴파일러에게 전하고 싶은 경우에 접미사를 사용한다. 예를 들어, 부동 소수점 상수는 기본적으로 double형으로 인식되기 때문에 float형 변수에 부동 소수점 상수를 대입하면 컴파일러가 경고를 발생시킨다.

코드1

#include <stdio.h>

int main() {
  float fVar = 3.14F;
 printf("%g\n" , fVar);
 return 0;
}

코드1은 float형 변수 fVar에 3.14이라는 float형의 부동 소수점 상수를 대입하고 있다. 이 때 상수 끝에 접미사 F를 이용하고 있는 것에 주목하자. 이를 제외하면 컴파일러는 값을 일부를 잘라내어 작게 할 수 있음을 경고한다. 그래서 프로그래머는 이 상수가 float형임을 증명하고 안전하게 float형 변수에 대입할 수 있는 것을 접미사를 사용하여 어필할 수 있다.

상수의 다양한 작성

부동 소수점도 정수도 보통은 특별히 의식하지 않고 직관적인 작성해도 상관없다. 0.5이라고 쓰면 부동 소수점 상수이고, 소수점이 없는 숫자라면 정수로 인식되기 때문이다. 그러나 부동 소수점도 정수도, 이외의 표현 방법이 몇개가 있기에 소개하도록 하겠다.

부동 소수점 상수는 정수부, 소수부 지수로 나누어져 있다. 정수 부분과 소수 부분은 그 중 한 방향를 생략할 수 있지만, 양 방향을 동시에 생략할 수는 없다. 다만, 지수 표기를 사용하면 생략할 수 있다. 지수는 e 또는 E 뒤를 이어 값을 지정한다.

부동 소수점은 가수 × 기수 지수로 계산되기 때문에 지수를 지정하면 정수 부분을 지정할 필요가 없는 것이다. 예를 들면 3.14 값을 부동 소수점 상수로 표현하는 경우 몇 가지 표현 방법이 있다.

코드2

#include <stdio.h>

int main() {
 float fVar1 = .314e1F;
  float fVar2 = 314e-2F;
  float fVar3 = 31.4e-1F;
 printf("fVar1 = %g\nfVar2 = %g\nfVar3 = %g\n" , fVar1 , fVar2 , fVar3);

  return 0;
}

코드2는 3개의 float형 변수를 부동 소수점 상수로 초기화한다. 이러한 부동 소수점 상수는 모두가 3.14라는 값이다. 그러나 소스를 보고 확인할 수 있도록 표현은 모두 다르다. 따라서, 부동 소수점 상수는 같은 값이라도 다양한 표현을 할 수 있으므로 알아두면 좋다.

정수는 10진수 외에 8진수와 16진수를 사용할 수 있다. 2진수를 의식해야 하는 숫자 데이터를 처리할 경우는 10진수를 사용하는 것보다 편리하다. 특히 16진수는 실제 프로그래밍에서도 많이 하는 경우가 많기 때문에 중요하다. 예를 들어 ARGB 형식의 32비트 색상 데이터를 나타내는 경우, 각각의 1바이트의 요소를 16진수로 지정하면 알기 쉽고 편리하다.

정수를 8진수로 지정하려면 숫자 앞에 0을 16진수로 지정하는 경우 0x 또는 0X를 지정한다. 16진수의 경우, A ~ F까지의 숫자는 대문자여도 소문자이어도 상관없다.

코드3

#include <stdio.h>

int main() {
  printf("0xFF = %d\n0377 = %d\n" , 0xFF , 0377);
 return 0;
}

16진수 0xFF 및 8진수 0377은 모두 10진수로 255값이다. 예상대로 코드3을 실행하면 모두 10진수로 255이라는 값을 출력한다. 특히 2자리의 16진수와 8자리의 2진수(8비트)과 동일하므로 비트 연산 등에 사용할 수 있다.

문자 상수

ASCII 문자 세트는 1개의 영숫자 문자를 1바이트로 표현한다. ASCII 코드는 많은 컴퓨터가 지원하는 표준 문자 코드이다. C언어의 문자 상수는 즉, 1바이트의 ASCII 코드라는 것이다. ASCII는 American Standard Code for Information Interchange의 약자로, ANSI는 1962년에 제정한 표준 코드이다. 실제로는 7비트로 구성되어 있고, 영문자와 기호나 숫자 등을 표현할 수 있다.

문자 상수는 따옴표(’)에 문자를 지정한다. char형의 변수에 문자를 할당하는 경우 이러한 문자 상수를 지정한다. char형에 숫자를 대입 할 수 있지만, 1바이트라는 성질에서 문자를 표현하기에 적합하다. 문자 상수로 나타낼 수 있는 것은 항상 1문자이자만, 이스케이프 문자를 지정할 수 있다. 이스케이프 문자를 지정한 경우 \n처럼 두 글자로 보이지만 실제로 나타내고있는 것은 한 문자 이스케이프 문자이다.

그런데, 중요한 것은 컴퓨터는 문자를 숫자로 취급하고 있다. 예를 들면 ASCII 코드 ‘A’는 16진수 0x41에 해당하며, ‘B’는 0x42, ‘C’는 0x43와 같이 일련 번호로 되어 있다. 즉, 소스 코드로 문자 상수를 지정하여도 실제로는 수치로 취급한다. 이것을 이해하면 컴퓨터에서 문자라는 것이 어떻게 다루어지고 있는지, 그 원리를 배울 수있을 것이다.

#include <stdio.h>

int main() {
 char chVarA = 'A';
  char chVarB = 0x42;

 printf("chVarA(%%X) = %X\nchVarA(%%c) = %c\n" , chVarA , chVarA);
 printf("chVarB(%%X) = %X\nchVarB(%%c) = %c\n" , chVarB , chVarB);

 return 0;
}

코드4를 실행하면, 매우 흥미로운 결과가 표시된다.

이 프로그램은 char형 변수 chVarA과 chVarB이 포함된 숫자와 문자 표현을 표시한다. chVarA에는 이니셜라이저에서 ‘A’라는 문자 상수를 대입하고 있지만, 앞에서 설명한대로 이 값을 16진수로 출력하면 41이라는 결과를 얻을 수 있다. chVarB는 16진수 42이라는 정수를 할당하고 있다. 마찬가지로 이 변수를 출력하면 숫자는 당연히 42을 출력하지만, 문자로 표시하면 B라는 결과가 된다. 방금 전의 ASCII 코드 A에 이어지고 있는 것을 알 수 있다.

이와 같이, 1문자는 문자 상수로 지정할 수 있지만, 실제로는 숫자로 취급할 수 있는 것이다. 여기서 하나 주의를 해야 하는 것은, 리터럴 문자열 ““와 문자 상수 ‘‘는 근본적으로 다른 종류로 인식한다. “A"와 ‘A’는 다르다.

이스케이프 문자

문자열이나 문자 상수는 \ 기호로 시작하는 이스케이프 문자를 지정할 수 있다. 이스케이프 문자는 줄 바꿈이나 탭 문자 등 일반 문자로 표현할 수 없는 표현이 어려운 문자 코드를 나타낼 수 있다. 이스케이프 문자는 표3과 같은 것이 있다.

표3 이스케이프 문자

기호 의미
\a 벨 문자 (경고)
\b 한 칸 뒤로
\f 페이징 (일반)
\n 개행 복귀
\r 같은 줄의 맨 위로
\t 수평 탭
\v 수직 탭
\ \보기
? ?보기
' 따옴표 (’)를 표시
" 큰 따옴표 (")를 표시
\0
\N 8진수 정수 (N은 8 진수 상수)
\xN 16진수 정수 (N은 16 진수 정수)

예를 들어, 문자열에 큰 따옴표를 사용하려는 경우와 문자 상수에서 따옴표를 표현하거나 \ 기호 또는 탭 문자 등을 표현하고 싶은 경우에 이러한 이스케이프 문자를 사용다. 그러나 수직 탭 등 일부 이스케이프 문자는 특정 장치에서만 의미를 인식하지 않을 수도 있다.

1.2.7 - C 언어 | C 언어 입문 | 예약어(keyword)

다음은 C 프로그래밍 언어의 키워드 목록이다.

프로그램에서 다음 식별자를 식별자로 사용할 수 없다. 그리고 최근의 컴파일러들은 C++ 언어 프로그램의 식별자도 사용할 수 없다.

C 예약어

예약어 설명
asm 인라인 어셈를리 코드를 나타내는 키워드.
auto 기본적인 변수의 저장 방식을 지정하는 키워드.
break for, while, switch, do…while문을 조건 없이 마치는 명령.
case switch문 내에서 사용되는 명령.
char 가장 간단한 데이터형.
const 변수가 변경되지 않도록 방지하는 데이터 지정자. volatile참고.
continue for, while, do…while문을 다음 반복 동작으로 진행시키는 명령.
default case문에 일치하지 않는 경우를 처리하기 위해 switch문에서 사용되는 명령.
do while문과 함께 사용되는 순환 명령. 순환문은 최소한 한번 실행됨.
double 배정도 부동 소수형 값을 저장할 수 있는 데이터형.
else if문이 FALSE로 평가될 때 실행되는 선택적인 문장을 나타내는 명령.
extern 변수가 프로그램의 다른 부분에서 선언된다는 것을 알려주는 데이터 지정자.
float 부동소수형 숫자 값을 저장하기 위해 사용되는 데이터형.
for 초기화, 증가, 조건 부분을 가지는 순환명령.
goto 정의되어 있는 레이블로 이동시키는 명령.
if TRUE/FALSE의 결과에 따라 프로그램의 제어를 변경하는데 사용되는 명령.
int 정수형 값을 저장하는데 사용되는 데이터형.
long int형보다 큰 정수형 값을 저장하는데 사용되는 데이터형.
register 가능하다면 변수를 레지스터에 저장하도록 지정하는 저장형태 지정자.
return 현재의 함수를 마치고 호출한 함수로 프로그램의 제어를 돌려주는 함수. 함수 값을 돌려주기위해 사용됨.
short 정수형 값을 저장하는데 사용되는 데이터형. 자주사용되지는 않지만 대부분의 컴퓨터에서 int형과 동일한 크기를 가짐.
signed 변수가 양수와 음수값을 모두저장할수 있다는것을 지정하기 위해서 사용되는 지정자.
sizeof 항목의 크기를 바이트 단위로 알려주는 연산자.
static 컴파일러가 변수의 값을 보존해야 한다는 것을 지정하는데 사용되는 지정자.
struct C에서 어떤 데이터형의 변수를 함께 결합시키는 데 사용되는 키워드.
switch 여러가지 조건을 통해서 프로그램의 흐름을 변경하는데 사용되는 명령. case문과 함께 사용됨.
typedef 이미 존재하는 변수와 함수의 형태를 새로운 이름으로 변경하는데 사용되는 지정자.
union 여러개의 변수가 동일한 메모리영역을 공유하도록 해주는데 사용되는 키워드.
unsigned 변수가 양수 값만을 저장할수 있다는 것을 지정하는데 사용되는 지정자. signed를 참고.
void 함수가 어떤 값을 돌려주지 않거나 또는 사용되는 포인터가 범용포인터이거나, 모든 데이터형을 지적할수 있다는 것을 지정하는데 사용되는 키워드.
volatile 변수가 변경될 수 있다는 것을 나타내는 지정자. const참고.
while 지정된 조건이 TRUE로 평가되는한 게속해서 포함된 문장을 실행하는 순환문.

C++ 추가 예약어

catch inline template
class new this
delete operator throw
except private try
finally protected virtual
friend public

1.2.8 - C 언어 | C 언어 입문 | 데이터 입력 scanf()

scanf() 함수를 사용하여 키보드에서 입력한 임의의 값을 받는 방법을 설명한다.

사용자 입력 받기

지금까지의 프로그램에서는 변수에 상수를 대입해 왔지만, 실제 프로그램에서는 이렇게 무의미한 행위는 하지 않는다. 실행시에 동적으로 변화하는 부정확한 값일 경우에 변수를 사용하는 의미가 있다. 항상 같은 값을 화면에 표시하길 원하지 않는다면, 직접 상수를 지정하면 된다는 것이다.

그럼 “실행시에 동적으로 변화하는 값"은 어떻게 받아야 하는 걸까? 하나는 디스크 파일에서 데이터를 받는 방법을 생각할 수 있다. 예를 들어, 텍스트 파일을 입력하여 그것을 화면에 표시한다면, 그것은 실행시에 동적으로 변화하는 값이라고 할 수 있다. 그러나 디스크 파일을 읽는 프로그램은 중급 수준의 작업이 되기 때문에 여기서는 설명할 수 없다. 이는 C 언어 기초부터 배우고, 후반에서 설명하도록 하겠다.

그럼, 더 쉽게 데이터를 입력하는 방법은 없는 것인가? 프로그램 실행 중에 키보드로부터 문자나 숫자를 입력할 수 있다면, 프로그램의 활용성도 크게 확대되고 향후 프로그램의 테스트도 보다 유연하게 할 수 있게 될 것이다. 키보드에서 값을 입력하려면 scanf() 함수를 사용한다. scanf() 함수는 printf() 함수의 입력 판에 지정한 변수에 대해 키보드에서 값을 입력한다.

scanf() 함수

int scanf("포멧" , &변수명);

scanf() 함수의 첫번째 인수에 서식 제어를 두번째 인수 이후에 입력된 값을 저장하는 변수를 지정한다. 서식 제어는 기본적으로 printf() 함수와 같은 것으로 생각될 수 있다. 숫자를 입력한다면 %d, 문자를 입력하는 경우 %c를 지정하는 식이다. 두번째 인수 이후는 서식 제어의 포멧 문자에 대한 변수를 지정하는 것인데, 단순하게 변수를 지정하는 것이 아니라 변수 이름 앞에 &를 지정한다.

왜 변수 앞에 &를 붙일 필요가 있는가 하면, 이는 함수에 변수의 주소를 알릴 필요가 있기 때문이다. 이것에 대해서는 포인터에 대한 자세한 내용을 알아야 할 필요가 있는데, 여기서는 키보드에서 입력하는 경우는 scanf() 함수 사용한다는 것을 설명하는 것이기에, 서식 제어의 포멧 문자에 대한 변수를 지정할 때는 변수명 앞에 &를 붙인다라는 정도만 기억하도록 하자. 이 &의 의미는 포인터를 학습할 때에 다시 설명하도록 하겠다.

scanf() 함수는 제대로 변환되어서 변수에 할당되는 포멧의 수를 반환한다.

코드1

#include <stdio.h>

int main() {
 int iVar = 0;

 printf("정수를 입력해 주세요. > ");
  scanf("%d" , &iVar);

  printf("입력한 수는 %d입니다.\n" , iVar);
  return 0;
}

코드1을 실행하면 값을 입력하도록 요구된다. 정수 값을 입력하면 iVar 변수에 입력한 값이 할당되고 화면에 숫자가 나타날 것이다. 올바른 값이 입력되지 않은 경우 변환할 수 없기 때문에 변수에 값이 할당되지 않는다.

실전 수준으로 문제를 생각하면, 사실 scanf() 함수가 사용되는 경우는 거의 없다. 이유는 오류 검사가 충분하지 않기 때문이다. 충분한 오류 검사를 할 필요가 있는 입력은(상용 프로그램은 충분한 오류 검사가 필요하기에, 결과적으로 본격적인 프로그램은 scanf()를 많이 사용하지 않는다) 변환 작업을 하지 않고 다른 표준 입력 함수를 사용하여 문자열로 입력된 정보를 받아 입력된 문자열을 조사해 적절한 값으로 변환하는 작업을 수행한다. 물론 이러한 작업은 복잡하다. 따라서 이 책에서는 입력 작업은 scanf() 함수를 사용하여 작업을 단순화한다.

1.2.9 - C 언어 | C 언어 입문 | 수식 및 계산

간단한 산술 및 대입, 증가 연산에 대해 설명한다.

산술식

컴퓨터는 인간처럼 자유롭게 말을 말하거나, 논리적으로 물건을 추론하는 것에는 좋지 않다 (무엇보다, 이것은 문서 작성 시점인 2002년경의 이야기이며, 변화하는 이 분야에서 항상이 상식이 통용된다고는 않지만…). 그 대신 인간에 비해 컴퓨터는 계산에 좋다. 인간에게 계산을 시키면 몇 년이 걸릴거 같은 난해한 계산도 현대의 컴퓨터는 순식간에 대답을 낸다. 암산 챔피언이라도 컴퓨터의 계산 속도에 따라갈 사람은 없다. 컴퓨터가 하는 그 계산은 프로그램의 산물이다. 컴퓨터는 계산이 특기라서 이를 프로그래밍에서 사용하지 않을 수는 없다. 결국은 계산 없이 프로그램은 성립되지 않는다.

계산을 위해서는 먼저 식을 만든다. 여기서 말하는 식은 기본적으로 수학에서 사용되는 “식"와 같은 의미이다. 식은 피연산자와 연산자로 구성되어 있다.

피연산자 연산자 피연산자 ...

그럼 “연산자(operator)“이란 무엇일까? 연산자는 한마디로! “더하기"와 “빼기"로 사용되는 수학 기호 +, - 등이다. 이것들은 앞에서 대입 연산을 하였기에, 이미 이해하고 있을 것이다. A = B라는 표현식은 A와 B는 피연산자와 =라는 연산자로 구성되어 있다. 주의해야 하는 것은 컴퓨터에서는 ÷ 기호와 × 기호를 계산에 사용하지 않는다. 나누기(÷)는 / 기호를 곱하기(×)에는 * 기호를 사용한다. C 언어에서 사용되는 산술 연산자는 표1과 같다.

표1 산술 연산자

연산자 의미
+ 추가
- 빼기
* 곱셈
/ 나누기
% 나머지
= 대입

연산자가 필요로 하는 피연산자의 수를 항이라고 하고, 이러한 연산자는 좌우에 맞게 두 피연산자가 필요하므로 이항 연산자라고 한다. 이 밖에도 피연산자를 하나만 요구하는 단항 연산자와 피연산자 3개를 필요로하는 3항 연산자라는 것도 존재한다.

C 언어의 결과는 항상 결과의 얻는 대상이 필요하다. 그것은 대입 연산자를 사용하여 변수를 받아도 상관 없고, 함수의 인수 등으로 건네 주어도 상관없다. 어쨌든, 결과를 뭔가를 받지 않으면, 계산하는 의미가 없다.

코드1

#include <stdio.h>

int main() {
  int op1 = 0 , op2 = 0;

  printf("두개의 숫자를 입력해 주세요. >");
 scanf("%d %d" , &op1 , &op2);

 printf("%d + %d = %d\n" , op1 , op2 , op1 + op2);
  return 0;
}

코드1은 두 숫자형 변수 op1과 op2에 scanf() 함수를 사용하여 값을 입력한다. 그 후에 printf() 함수를 사용하여 두 값을 더한 결과를 출력한다. 예를 들어 8과 16라는 값을 입력하면 그 결과는 8 + 16 = 24로 출력되는 것이다.

연산 결과는 대입식을 이용하여 다른 변수에 저장할 수 있다. 연산 결과를 화면에 표시하기만 한다면 코드1과 같이 인수로 함수에 전달하기 직전에 계산하는 방법으로도 괜찮지만, 연산 결과를 저장해야 할 경우 대입식을 사용한다.

코드2

#include <stdio.h>

int main() {
 int op1 = 0 , op2 = 0 , op3;

  printf("두개의 숫자를 입력해 주세요. >");
 scanf("%d %d" , &op1 , &op2);

 op3 = op1 * op2;
  printf("%d * %d = %d\n" , op1 , op2 , op3);

  return 0;
}

코드2는 변수 op3에 op1과 op2의 곱한 결과를 대입하고 있다. 연산 결과를 여러 번 프로그램에서 사용하는 경우, 그 때마다 계산을 반복하는 것은 CPU의 낭비되기 때문에, 계산 결과를 저장하고 사용하는 것이 좋다. 참고로 계산하는 변수와 대입하는 변수로 같은 것을 사용해도 문제는 없다. 예를 들어 op1 = op1 * op2로 먼저 op1 * op2가 계산되기 때문에 결과에 문제는 없다.

연산자의 우선 순위는 수학 계산 순서와 동일하다. 단순히 왼쪽에서 오른쪽으로 계산되는 것은 아니기 때문에 주의해야 한다. 예를 들면 2 + 2 * 3이라는 수식이 있을 경우, 먼저 2 * 3이 계신되고, 그 후에 2 + 6이 계산된다. 계산 순서를 변경하려면 수학 마찬가지로 괄호를 넣는다. 이 경우, 뎃셈을 먼저하고 싶은 경우 (2 + 2) * 3와 같이 함으로써 먼저 2 + 2가 계산된 후에 4 * 3가 계산된다.

코드3

#include <stdio.h>

int main() {
 printf("2 + 2 * 3 = %d\n" , 2 + 2 * 3);
  printf("(2 + 2) * 3 = %d\n" , (2 + 2) * 3);

  return 0;
}

코드3의 실행 결과를 보면, 괄호를 사용하지 않는 경우는 뎃셈보다 곱셈이 먼저 계산되고 있다. 괄호로 뎃셈 부분을 묶어서 먼저 2 + 2가 계산되고 있는 것을 확인할 수 있다.

또한 지금까지의 프로그램은 변수끼리를 계산하고 있었지만, 변수와 상수 또는 상수끼리라도 형태에 호환된다면 문제는 없다. 상수끼리를 계산한 경우에는 컴파일러가 컴파일시에 연산 결과를 예상할 수 있기 때문에 기계어로 변환된 때에는 최적화된다.

복합 대입 연산

통상적으로 대입 연산자 =를 단순 대입이라고 한다. 이 외에 계산 및 대입을 동시에 하는 연산자가 존재한다. 예를 들어 op1 = op1 + op2라는 계산을 할 경우, 이를 op1 += op2로 기술될 수 있는 것이다. 이와 같은 다른 연산자와 단순 대입을 결합한 대입 연산자를 복합 대입이라고 한다. 산술 및 대입을 동시에 대입 연산자는 표2와 같이 정리할 수 있다.

표2 - 복합 대입 연산자

연산자 의미
+= 더하고 대입
-= 빼고 대입
*= 곱하고 대입
/= 나누고 대입
%= 나머지 대입

코드4

#include <stdio.h>

int main() {
  int op1 = 0 , op2 = 0;

  printf ( "삼각형의 밑변과 높이를 입력하십시오. >");
 scanf("%d %d" , &op1 , &op2);

 op1 *= op2 / 2;
 printf("삼각형의 면적 = %d" , op1);

 return 0;
}

코드4의 식 op1 * = op2 / 2op1 = op1 * (op2 / 2)와 같은 계산과 동일하다고 생각할 수 있다.

증가 및 감소

지금 단계에서는 생각하기 어려울지도 모르지만, 많은 프로그램은 현재 변수에 1을 가산 또는 감산하는 것을 자주한다. 이 때, 통상적으로 var = var + 1 또는 var += 1로 기술하는 것을 생각해내는 것이다. 물론 이것도 틀린 것은 아니지만, C 언어에서 이러한 작성은 일반적으로하지 않는다.

C 언어는 현재 변수가 보유한 값에 “1 더하기”, “1 빼기” 전문 연산자가 존재한다. 그것이 증가 연산자와 감소 연산자이다.

증가 연산자

변수명++

감소 연산자

변수명--

++가 증가 연산자이다. 그 변수가 현재 들어있는 값을 1 증가한다. –는 감소 연산자이다. 증가와는 반대로 그 변수가 현재 들어있는 값을 1 감소한다. 즉, var = var + 1var ++var = var - 1var--로 기술할 수 있다.

코드5

#include <stdio.h>

int main()
{
  int iVar1 = 0 , iVar2 = 0;

  iVar1++;
  printf("증감 연산자 후에 var1 = %d\n" , iVar1);

 iVar2--;
  printf("감소 연산자 후에 var2 = %d\n" , iVar2);

 return 0;
}

이 프로그램은 0으로 초기화된 변수 iVar1과 iVar2를 각각 증가, 감소하고 화면에 표시하도록 하는데 있다. 예상했던대로 증가를 하면 1증가하고, 감소를 하면 1감산된 결과가 표시된다.

증가 연산자와 감소 연산자는 두 가지 존재한다. 코드5는 후위 연산자라는 것을 사용하고 있는데, 이 밖에 전위 연산자라는 것도 존재한다. 각각 후위 증가 연산자, 후위 감소 연산자, 전위 증가 연산자, 전위 감소 연산자라고 한다.

예를 들어, 전위 증가 연산자 ++var와 같이 변수 앞에 연산자를 놓는다. 후위와 무엇이 다른가하면, 식의 결과가 다르다.

전위 증가 연산자

++변수명

전위 감소 연산자

--변수명

후위 연산자는 변수를 사용하고 1 가산한다. 이에 대해, 전위의 경우 최초에 1을 더한 다음 변수를 사용한다. 코드5와 같이 증가 또는 감소뿐만 문장이면 전위에서도 후위도 결과는 동일하지만 복잡한 다항식에서 증가 연산자와 감소 연산자를 사용한 경우 영향이 알 수 있다.

코드6

#include <stdio.h>

int main() {
  int iVar1 = 0 , iVar2 = 0;
  printf("후위 증가 연산자 = %d\n" , iVar1++);
  printf("전위 증가 연산자 = %d\n" , ++iVar2);
  printf("iVar1 = %d iVar2 = %d\n\n" , iVar1 , iVar2);

  printf("후위 감소 연산자 = %d\n" , iVar1--);
  printf("전위 감소 연산자 = %d\n" , --iVar2);
  printf("iVar1 = %d iVar2 = %d\n\n" , iVar1 , iVar2);
  return 0;
}

이 프로그램은 인수에 값을 전달하기 전에 증가 및 감소를 실시하고 있다. 이 때, 전위와 후위에서 어떻게 다르게 동작하는지 확인하자.

실행 결과를 보면, 전위의 경우는 증가나 감소를 실시한 후에 함수에 값을 전달하고 있지만, 후위 경우 화면에 값이 표시된 후에 사용되는 것을 알 수 있다. 최종 결과는 동일하지만 이처럼 함수의 인수와 다항식으로 증가 또는 감소를 지정하는 경우는 매우 중요한 문제이다.

즉, 후위 연산자의 경우는 printf() 현재의 변수 값을 전달하고 변수를 증가하고 있는 것이다. 따라서 최초의 증가는 변수의 값 0을 전달하고 증가하고 있다. 최초의 printf()가 표시한 값은 0이지만, 그 후에 iVar1의 값이 증가되어 있기 때문에 1로 된다.

전위의 경우, 증가/감소하고 값을 함수에 전달되기 때문에 printf() 함수가 받는 값은 이미 계산된 후이다.

그래서 만약 x = var++라고 하는 계산을 하는 경우에 주의해야 한다. x에 전달된 값은 var의 값이며, 그 후에 var가 증가되는 것이다. var 증가하고 x에 var의 값을 전달하려면 x = ++var로 기술되어야 한다.

1.2.10 - C 언어 | C 언어 입문 | 비트 처리

논리적, 논리화, 비트 시프트 등을 사용하여 정수를 비트 단위로 조작하는 방법을 설명한다.

비트 연산

산술 연산는 수학적인 계산을 수행하는데 적합하지만, 2진수를 제어하려면 적합하다고는 말할 수 없다. 즉, 비트 단위로 계산을 할 경우, 산술 연산는 적합하지 않다. 예를 들어, 32비트의 정수형 변수에서 상위 8비트를 얻고하고자 하는 경우, 산술 연산이 아니라 비트 단위의 계산이 필요하다. 그래서 이러한 비트 제어를 할 경우는 비트 논리 연산을 한다.

표1 - 비트 연산자

연산자 의미
& 비트 논리곱
| 비트 논리합
^ 비트 배타적 논리합
~ 1의 보수
&= 비트 AND 대입
|= 비트 OR 대입
^= 비트 배타적 OR 대입

표1의 연산자를 비트 연산자라고 한다. 어떤지, 논리곱라든지 논리합든가, 어려울 것 같은 말이 나왔지만 복잡하게 생각하지 말자. 논리 연산은 2진수의 0과 1을 스위치의 ON과 OFF처럼 생각하고, 0의 상태를 거짓(FALSE), 1의 상태를 참(TRUE)이라고 생각할 수 있다.

우선는 논리곱부터 사용해 보자. 논리곱(AND)과 비교하는 두 비트가 양쪽 모두 1이면 참, 그렇지 않으면 거짓을 산출한다. 이 관계는 표2와 같이 된다.

표2 - 논리곱 관계

A B A & B
0 0 0
0 1 0
1 0 0
1 1 1

이러한 논리곱의 성질을 이용하면, 특정 비트만을 추출하는 것이 가능하다. 예를 들어 0101 1100는 2진수가 있는 경우, 하위 4비트의 값을 얻고 싶다면 0000 1111과의 논리곱을 구한다.

표1 - 논리곱에 의한 비트 추출

  0101 1100
& 0000 1111
------------
  0000 1100

이러한 계산을 시키면, 마스크 비트의 상위 4비트는 0이므로 결과는 항상 0이 되고, 하위 4비트는 모두가 1이므로, 비교하는 비트 중에 1부분만 1로 추출된다. 이 성질은 RGB 형식의 색상 정보를 나타내는 32비트 값에서 붉은 요소만을 추출하고 싶은 경우에 적용된다. 32비트 중 16 ~ 23 비트가 붉은 요소를 나타내는 경우, 이에 0xFF0000과의 논리곱을 구하여 붉은 요소의 값만을 얻을 수 있다.

코드1

#include <stdio.h>

int main() {
 char ch5 = '5';
 printf("ch5:%%c = %c , %%X = %X , & = %d\n" , ch5 , ch5 , ch5 & 0x0F);
 return 0;
}

이 프로그램은 ASCII 문자의 숫자와 실제의 수치의 관계를 알 수 있다. 문자 상수로써의 숫자(예를 들어 ‘5’)와 정수(예 : 5)는 전혀 다른 것이다. 실제로 문자 상수 ‘5’를 숫자로 출력하면 전혀 관계없는 값임을 알 수 있다. 그러나, ASCII 문자는 숫자도 0 ~ 9까지 존재하고, 그 규칙을 아는 것으로 문자 상수를 실제 숫자로 변환할 수 있다.

사실는 ASCII 문자의 숫자는 16진수 0x30 ~ 0x39까지 매핑되어 있다. 즉, 문자 상수 ‘0’은 ASCII 코드로 0x30이며, 반대로 생각을 하면 ASCII 코드로 0x37은 문자 상수 ‘7’이라고 생각할 수 있다. 이 규칙성을 살려, 문자 상수의 상위 4비트 값을 제거하면 순수한 숫자로 변환할 수 있다는 것이다. 코드1은 바로 이것을 ch5 & 0x0F으로 실행하고 있다. ASCII 코드의 숫자에 대해서 0x0F와의 논리곱을 구하여 숫자로 변환할 수 있다.

다음으로, 논리합(OR)인데, 이것은 비트 중에 하나가 1이면 참이라는 결과를 내고 있다. 모두 0으로 비교하면 원래 값이 모두 1로 비교하면 모든 비트가 1이 된다.

표3 - 논리합 관계

A B A | B
0 0 0
0 1 1
1 0 1
1 1 1

이 성질을 잘 이용하면 비트 플래그의 조합을 만들 수 있다. 또, 이전과는 반대로 숫자부터 ASCII 코드의 숫자로 변환하는 처리를 할 수 있다.

코드2

#include <stdio.h>

int main() {
  int iVar = 5;
 printf("iVar:%%d = %d\niVar | 0x30:%%c = %c\n" , iVar , iVar | 0x30);
 return 0;
}

이 프로그램은 숫자형 변수 iVar에 저장되어 있는 값을 ASCII 문자의 숫자로 변환한다. 다만, iVar 값은 1 자리이어야 한다.

배타적 논리합(XOR)는 두 값이 같으면 거짓이 되는 논리 연산으로 한쪽이 1이고 한쪽이 0일 때 참이다. 같은 비트 열의 배타적 논리합 A ^ A는 항상 0이 되는 성질이 있다. 또 A ^ B를 실행한 결과 C에 C ^ B를 구하면 결과는 A로 된다라고 하는 성질이 있다.

표4 - 배타적 논리합 관계

A B A ^ B
0 0 0
0 1 1
1 0 1
1 1 0

코드3

#include <stdio.h>

int main() {
  int iVar1 = 0xF0F0 , iVar2;
  printf("iVar1 ^ iVar1 = %X\n" , iVar1 ^ iVar1);

  iVar2 = iVar1 ^ 0xABCD;
  printf("iVar2 = %X\n" , iVar2);
  printf("iVar2 ^ 0xABCD = %X\n" , iVar2 ^ 0xABCD);
  return 0;
}

코드3은 변수 iVar1에 0xF0F0을 대입하고 있다. 최초에 printf()는 iVar1끼리의 배타적 논리합을 구하고 있다. 동일한 비트 열의 배타적 논리합은 항상 0이 되는 성질이 있기 때문에, 이 결과가 0이 되는 것을 확인할 수 있을 것이다. 다음 iVar2에 iVar1과 0xABCD의 배타적 논리합을 저장한다. 이 값은 원래 값과는 관계가없는 수치로되어 있습니다 만, iVar2에 다시 동일한 값 0xABCD의 배타적 논리합을 구하면 원래으; 값 iVar1와 동일하다.

마지막으로 1의 보수인데, 이것은 단순히 비트 열을 반전시킨다. ~ 연산자는 다른 비트 연산와 다르게 단항 연산식으로, 주어진 값의 비트열을 반전시킨다. 예를 들어, 비트 맵 이미지에서 각 픽셀의 색상 정보를 얻고, ~ 연산자를 사용하여 비트를 반전시킴으로써 색상을 반전시키는 것도 가능하다.

코드4

#include <stdio.h>

int main() {
  unsigned int iVar = 0xCC33CC33;
  printf("~%X = %X\n" , iVar , ~iVar);
  return 0;
}

코드4는 부호없는 정수 iVar에 0xCC33CC33을 대입하고 있다. 이것은 2진수로 1100 1100 0011 0011 1100 1100 0011 0011는 32 비트 값이지만, 이를 반전 시키면 0011 0011 1100 1100 0011 0011 1100 1100 즉, 16진수 0x33CC33CC라는 값이 된다. 이 프로그램을 실행하면 ~ 연산자는 의해 비트가 반전하고 있는 것을 확인할 수 있을 것이다.

비트 시프트

비트 시프트는 비트 열을 그대로 왼쪽 또는 오른쪽으로 이동시키는 연산이다. 비트 시프트는 시프트 연산자를 사용하여 실행한다. 시프트 연산자와 시프트 연산을 이용한 복합 대입 연산자는 표5와 같다.

표5 - 시프트 연산자

연산자 의미
« 왼쪽 시프트
» 오른쪽 시프트
«= 왼쪽 시프트 대입
»= 오른쪽 시프트 대입

시프트 연산자는 다음과 같이 사용한다.

값 << 시프트수
값 >> 시프트수

<<는 비트를 왼쪽으로 이동하고, >> 비트를 오른쪽으로 이동한다. 값을 몇번을 이동할지를 이동 수를 지정한다. 이동한 만큼 발생한 공백은 0으로 채워진다.

1111 1111 » 2 – 오른쪽으로 2이동 –> 0011 1111

이러한 기능은 도대체 어디에 사용하는 거지라고 생각할지 모른다. 실은 오른쪽으로 이동할 때마다 2로 나눈 결과와 같다. 왼쪽으로 이동 할 때마다 2로 곱한 것과 동일하다. 일반적으로 CPU는 산술 연산보다 시프트 연산을 사용하는 것이 처리가 빠르다는 장점이 있다 (컴파일러에 의해 컴파일시에 최적화되어 버리기 때문에, 변환가 없는 경우도 있다). 그 외에도 32비트 열을 24비트 오른쪽 시프트하여 상위 8비트를 얻는 이용 방법도 있다.

코드5

#include <stdio.h>

int main() {
  int iVar = 100;
  printf("iVar / 4 = %d\niVar * 4 = %d\n" , iVar >> 2 , iVar << 2);
  return 0;
}

이 프로그램은 값이 100인 변수에 대해 오른쪽으로 2번 이동한 값과 왼쪽으로 2번 이동한 값을 표시한다. 2회 이동하는 것은 4를 곱하거나, 나눈 것이 동일하다는 것을 확인할 수 있다.

데이터 형의 크기를 초과 이동한 경우는 잘린다. 또한 부호있는 정수를 오른쪽으로 이동하면, 최상위 비트가 변화하기 때문 부호가 바뀌어 버릴 것이다. 실은 오른쪽 시프트해서 부호는 저장될 수도 있다. 부호있는 정수를 오른쪽으로 이동하면 최상위 비트는 저장된 상태로 이동되고, 부호없는 정수를 오른쪽으로 이동하면 최상위 비트가 0으로 클리어된다. 이를 산술 시프트라고 한다. 반대로, 어떤 상태이든 최상위 비트가 항상 0으로 클리어되는 시프트를 논리 시프트라고 한다. 산술 시프트가 행해지는지, 논리 변화가 일어나는지는 구현에 따라 달라진다. 따라서 이식성있는 프로그램을 작성하는 것이 목적인 경우는 산술 시프트와 논리 시프트 등의 결과에 의존하지 않도록 주의해야 한다.

코드6

#include <stdio.h>

int main() {
  int iVar = -100;
  printf("iVar >> 2 = %d\n" , iVar >> 2);
  return 0;
}

코드6은 -100의 값을 가진 부호있는 정수형 변수 iVar에 대해 오른쪽 시프트를 하고 있는데, 산술 시프트를 하고 있는지, 논리 시프트를 하고 있는지는 프로그램을 실행하는 계산에 따라 다르다.

1.2.11 - C 언어 | C 언어 입문 | 형변환

정수와 부동 소수점 등 다른 자료형끼리 값을 계산시키면 어떻게 될까 자료형과 규칙에 대해 설명한다.

산술 변환

지금까지 계산식을 여러번 사용해 왔지만, 실제 프로그래밍에서는 연산식의 피연산자에 변수를 이용하는 경우가 많이 있는데, 그 변수가 반드시 동일한 자료형은 아니다. 예를 들어 계산식의 연산으로 int 형과 float 형의 연산이 행해진 경우는 어떻게 될까? C 언어에서는 이 같은 다른 형태의 연산을 해도 문법적으로는 틀린 것은 아니다.

피연산자는 계산식 중에서 큰 자료형에 맞게 변환된다는 규칙이 있다. 만약 사이즈가 큰 자료형을 작은 자료형으로 변환하면 상위 비트를 제거하게 되므로 정보를 잃어 버릴 가능성이 있다. 그러나 사이즈를 확장하는 경우는 정보를 잃지 않는다. 또한, int 형 이하의 사이즈의 자료형은 계산식에 의해 int 형(즉, 컴퓨터 연산에 사용하는 정수 사이즈)으로 변환된다. 이것은 평소 프로그래머가 의식할 필요는 없지만, 알아두면 손해는 없을 것이다.

이러한 연산식의 변환을 산술 변환이라고 한다. 변환 규칙을 정리하면 표1과 같이 된다.

표1 산술 변환의 법칙

산술 변환의 법칙

코드1

#include <stdio.h>

int main() {
 char chVar = 50;
  int iVar = 100;
 float fVar = .555;

  printf("%g\n" , chVar + iVar + fVar);
  return 0;
}

이 프로그램에슨 printf() 함수의 인수부에 chVar + iVar + fVar라는 연산을 하고 있는데, 이러한 변수는 모두가 다른 자료형이다. 그래서 산술 변환이 되어 최종적으로는 가장 큰 사이즈인 float 형으로 변환된다. char 형과 int 형의 값은 float형으로 변환되어 정보 손실은 없다. 최종적으로는 150.555이라는 결과를 얻을 수 있을 것이다.

대입 변환

계산식에서 다른 자료형의 피연산자가 있으면 산술 변환을 수행했는데, 계산의 결과를 대입하는 변수가 연산 결과의 자료형과 동일하지는 않는다.

대입의 경우에도 자료형이 다른 경우는 암시적으로 변환하여 대입된다. 이것을 대입 변환이라고 한다. 이 때 변수의 사이즈가 계산식의 자료형보다 큰 경우는 아무런 문제가 없다. 계산 결과 자료형보다 큰 사이즈로 변환하면, 손실되는 정보가 없기 때문이다.

코드2

#include <stdio.h>

int main() {
 char chVar = 100;
 int iVar = chVar;

 printf("iVar = %d\n" , iVar);
  return 0;
}

코드2는 char 형 변수를 int 형 변수에 대입하고 있다. 이 경우 char 형 변수 chVar값이 int로 확장될 것이다. 원래 사이즈보다 확장된 변환의 경우, 정보 손실이 아니기 때문에 문제가 없다.

그러나 반대로 왼쪽의 변수 자료형이 우변보다 사이즈가 작은 경우에는 문제가 있다. 이 경우에도 오류가 발생하지 않지만 값은 정보의 일부를 손실되어 대입된다. 예를 들어 int에서 char로 변환된 경우 상위 비트가 잘려서 대입된다.

코드3

#include <stdio.h>

int main() {
  int iVar = 0xABCD;
  unsigned char chVar = iVar;

 printf("chVar = %X\n" , chVar);
  return 0;
}

코드3에서는 int 형 변수 iVar를 char 형 변수 chVar에 대입하고 있다. 이 경우 자료형이 다르기 때문에 대입 변환이 적용되는데, int 형에서 char 형으로의 변환에 문제가 발생한다. char 형는 int 형보다 사이즈가 작기 때문에, 이 경우는 축소 변환되어 버린다. 따라서 8비트 이상의 상위 바이트는 절단되기 때문에 그 결과 일부 정보를 잃게 된다.

int 형이 32비트라고 가정하면 iVar 값은 2진수로 0000 0000 0000 0000 1010 1011 1100 1101이다. 이 8비트인 char 형으로 변환한 경우, 상위 비트가 버려지고 1100 1101이 된다. 그 결과, 이 프로그램은 16진수 CD를 표시하는 것이다. 만약 부동 소수점에서 정수로 변환이 이루어진 경우, 소수의 정보가 손실된다.

“정보가 손실된다"라고 표현하면 뭔가 나쁜 것처럼 느껴지지만, 하위 8비트를 얻어려는 경우와 부동 소수점의 정수 부분만을 추출하고자 하는 경우에는 이러한 축소 변환이 유용하다. 경우에 따라 잘 이용할 수 있도록 해보자.

형변환

지금까지의 산술 변환 및 대입 변환은 암시적 변환이었다. 그러나 지정된 형으로 값을 변환하도록 프로그래머가 전하는 명시적 변환 방법도 존재한다. 이것을 형변환(type casting)이라고 한다. C 언어에서 어떤 형을 다른 형으로 강제로 변환할 수 있다. 형변환을 사용하려면 다음과 같은 구문을 사용한다.

형변환식

(변환하는 형 이름)값

형을 변환하는 형 이름으로 지정하고 그 형식으로 변환하는 값을 지정한다. 값은 지정된 형으로 변환되고, 대입하는 곳와 함수의 인수로써 전달될 수 있다.

코드4

#include <stdio.h>

int main()
{
  float fVar = 12.34f;

  printf("전체 = %g\n" , fVar);
  printf("실수 = %d\n" , (int)fVar);
 printf("소수 = %g\n" , fVar - (int)fVar);

  return 0;
}

코드4는 타입 캐스팅을 이용하여 부동 소수점 변수 fVar에서 실수와 소수을 얻어내고 있다. 이 처럼 캐스트를 이용하면 작은 형으로 변환할 수 있기 때문에 상위 비트를 잘라내고, 소수점 형을 정수형으로 변환하는 것이 가능하다.

1.2.12 - C 언어 | C 언어 입문 | 3문자 표기

C에서 ??에서 시작하는 3문자를 사용하여 정해진 다른 기호로 대체할 수 있다.

문자 대체

별로 알려져 있지 않지만, C 언어에는 문자 상수에 3문자 표기(트라이그래프:trigraph)를 사용할 수 있다. 이것은 ISO 646라는 규격과 관련이 있는데, 왜 이런(까다로운) 표기가 존재 하는가 하면, 거기에는 문자 코드의 역사적인 배경이 있다. ISO 646는 ASCII 코드의 일부 문자를 변경할 수 있도록 고려되어 있다. 이렇게 하면, 영어 이외의 언어에 필요한 특수 기호를 국가별 규격에서 사용할 수 있도록 되어 있지만, 같은 코드에서도 다른 문자가 표시되는 문제가 발생했다.

이 ISO 646의 영향으로 일부 언어(키보드)에서 처리할 수 없는 ASCII 코드를 표현하기 위해 사용된 것이 3문자 표기이다. 3문자 표기는?? 두 문자와 그 뒤의 기호로 표현된다. 현대에는 거의 사용하지 않지만, 모른다면, 리터럴 문자열에 ??라는 단어가 포함된 경우에 예상치 못한 문자로 변환되어 버릴 수 있다.

3문자 표기는 표1과 같은 세 문자로 이루어진 문자 상수이다. 이 3문자는 컴파일시에 대응하는 단일 문자로 대체된다.

표1 - 3문자 표기

트라이그래프 대체 문자
??= #
??( [
??/ \
??) ]
??' ^
??< {
??! |
??> }
??- ~

이러한 변환은 컴파일시 먼저 이루어진다. 더 자세히 설명하면, 컴파일러는 컴파일시에 번역 단계(phase)라는 단위로 소스 프로그램을 정해진 순서에 따라 분석하지만, 3문자 표기는 행이나 토큰이 식별되는 무엇보다 먼저 분석된다.

코드1

??=include <stdio.h>

int main() ??<
  printf("??= , ??( , ??)??/n");
  return 0;
??>

이러한 C 언어 소스 프로그램은 잘못되지 않았다. 3문자 표기는 무엇보다도 앞서 변환되므로 컴파일러 토큰을 분석할 무렵에는 올바른 기호로 변환된다.

문자열 리터럴에 ??을 표현하려면 이스케이프 시퀀스를 사용하여 ? ?를 기술한다.

3문자 표기는 이용 빈도가 낮기 때문에 컴파일 속도 향상을 위해 3문자 표기를 구현하지 않거나 기본적으로는 처리를 하지 컴파일러가 주류이다. 예를 들면 Borland C ++ Compiler 5.5에서는 구현되지 않고, 그 대신 변환 도구 TRIGRAPH.EXE이 제공하고 있다.

>trigraph 소스파일명

이 도구를 사용하면 소스의 3문자 표기를 해당하는 문자로 변환해 준다. Borland C ++ Compiler 5.5에서 3문자 표기를 포함한 소스를 컴파일하려면 먼저 이 도구를 사용하여 올바른 기호로 변환해야 한다.

Microsoft Visual C ++에서는 기본 설정은 비활성화되어 있기 때문에 컴파일러 옵션 /Zc:trigraphs을 지정하여 사용할 수 있다. 단, 편집기에서 3문자 표기를 사용한 코드 입력은 지원하지 않는다.

1.3 - C 언어 | 흐름 제어

1.3.1 - C 언어 | 흐름 제어 | 진위 판정

두 값의 대소 관계를 확인해서 관계 연산이나, 두 값이 동일한지 여부를 확인하는 등가 연산을 소개한다.

관계 연산과 등가 연산

지금까지의 프로그램은 항상 위에서 아래로 순서대로 실행되는 순서대로 실행했지만 프로그램은 상황에 따라 처리를 변경시킬 필요가 있다. 즉, 분기나 반복하는 처리이다. 이러한 조건 분기나 반복 처리 프로그래밍의 기본 중의 기본이며, 가장 중요한 부분이기도 하므로, 이 장에서는 철저히 그 구조를 설명하고, 프로그램의 흐름을 자유롭게 제어할 수 있게 될 때까지 해보도록 하자.

프로그램은 프로그램의 흐름을 제어하기 위해 어떤 정보를 소재로 해야 한다. 예를 들어, “변수 A가 100 이하 이면” 또는 “변수 A와 변수 B의 값이 같으면"과 같은 비교의 결과로 프로그램을 분기시킬 수 있다면, 상황에 따른 적절한 처리로 진행할 수 있다고 생각할 수 있다.

그래서 C 언어에는 비교 결과가 참(TRUE) 또는 거짓(FALSE)인지에 따라 상황을 판단한다. 진위는 값에 의해 식별되도록 0이면 거짓, 그렇지 않으면 참이라고 해석된다. 예를 들어, 변수 A가 변수 B와 동일한지 여부를 판단한 결과, 동일한 상태를 참이라고 하고, 동일하지 않은 상태를 거짓이라고 한다.

일반적으로 값의 비교는 관계 연산자, 등가 연산자 및 논리적 연산자를 사용한다. 이러한 연산자는 표현식을 확인하여, 그 결과에 따라 참 또는 거짓을 반환한다. 사실 실체는 수치이므로 이러한 연산자가 반환하는 결과는 int형이다. 각 연산자는 표1에 정리한다.

표1

관계 연산자

연산자 내용
A < B A가 B보다 작으면 참
A <= B A가 B와 같거나 작으면 참
A > B A가 B보다 크면 참
A >= B A가 B와 같거나 크면 참

등가 · 부등가 연산자

연산자 내용
A == B A와 B가 같으면 참
A != B A와 B가 같지 않으면 참

논리 연산자

연산자 내용
A && B A와 B가 참이면 참
A || B A 또는 B가 참이면 참
!A A가 거짓이면 참

“같으면"라고 판단을 할 경우는 A = B가 아니라 A == B인 점에 유의하자. A = B로 기술하면 B를 A에 대입한다는 의미가 되어 버리기 때문에 잘못된 결과를 얻을 수 있게 될 것이다. 해당 연산에 의한 실수를 방지하기 위해서는 왼쪽 피연산자에 상수를 가져오는 습관을 들이는 방법이 있다. 왼쪽 피연산자가 정수의 경우 실수로 대입 연산자 =를 지정 버려도 상수 값을 할당할 수 없기 때문에 컴파일 오류가 발생한다. 따라서 등가 연산을 지정하는 곳에 대입 연산자를 지정하는 실수에 알아차릴 수 있을 것이다.

그러나 (A = B) == 0 구문은 잘못 곳이 않았다. 대입 연산자 =는 결과(이 경우는 최종적인 A의 값)을 반환하기 위해, 어떤 계산 결과를 판단하고자 하는 경우에 사용하는 것은 가능하다. 예를 들어, 어떤 계산을하고 그 결과를 변수에 저장한 후, 오류 검사 등을 실시하는 경우에 이를 이용할 수 있다. B + C의 결과를 A에 저장하지만, 그 결과가 0인지 여부를 판단하는 경우 (A = B + C) == 0라고 기술하면, 그 진위를 묻는 수 있다 .

논리 연산자는 등가 · 부등가 연산자보다 등가 · 부등가 연산자는 관계 연산자보다 우선 순위가 낮기 때문에, 이러한 연산자를 동시에 사용하는 경우는 의식할 필요가 있다.

코드1

#include <stdio.h>

int main() {
 int iVar1 , iVar2;
  printf("2개 값을 입력해 주세요. >");
 scanf("%d %d" , &iVar1 , &iVar2);

 printf("iVar1 == iVar2 = %d\n" , iVar1 == iVar2);
  printf("iVar1 < 1000 = %d\n" , iVar1 < 1000);
  printf("iVar1 < iVar2 && iVar1 > 100 = %d\n" , (iVar1 < iVar2) && (iVar1 > 100));

  return 0;
}

이 프로그램을 실행하면 두 값을 입력하도록 요구한다. 값은 iVar1과 iVar2 변수에 각각 저장되어 다음 조건식에 의해 그 값의 관계를 확인하고 있다.

최초의 printf()는 iVar1과 iVar2가 동일한지 여부를 확인한다. 만약, 입력한 두 값이 동일한 값이면, 이 결과는 참이 되므로 0이 아닌 값이 출력된다. 일반적으로 참을 나타내는 0 이외의 값은 1이 사용된다. 다음의 printf() 함수는 iVar1가 1000보다 낮은지 여부를 확인한 결과를 출력하고, 마지막 printf() 함수는 iVar1이 iVar2보다 낮고, iVar1가 100보다 큰지 여부를 확인한 결과를 출력한다.

마지막의 논리 연산자 &&를 이용한 조건식은 조금 복잡하게 보이지만, 이렇게 하면 매우 유연한 비교를 할 수 있다. && 연산자에 의해 “좌우의 비교 결과가 참일 때만 참이다"라는 체크를 할 수 있다.

&&와 || 특징으로 조건에 따라서는 왼쪽 피연산자를 분석한 시점에서 결과를 알 수 있다. 논리 연산자는 항상 왼쪽 피연산자에서 오른쪽 피연산자의 순서로 분석하는 것이지만, &&는 피연산자가 거짓이면 다른 피연산자가 어떻든 간에 결과는 거짓이다. 따라서 만약 왼쪽 피연산자가 거짓이면 오른쪽 피연산자이 어째던 결과는 항상 거짓임을 보장되기 때문에 프로그램은 오른쪽 피연산자를 확인하지 않고 false를 반환한다. 마찬가지로 || 연산자의 경우는 한쪽이 피연산자가 참이면 그 결과는 항상 참임을 보장되므로 왼쪽 피연산자가 참이면, 오른쪽 피연산자는 확인하지 않고 참을 반환한다.

항상 이것을 의식할 필요는 없지만, 진위 여부를 확인시에 어떤 연산을 수행하는 식이 포함된 경우는 위험하다. 예를 들어, 논리 연산자의 오른쪽 피연산자로 증가 또는 감소를 사용한 경우 왼쪽 피연산자만으로 결과가 판정되어 버리면, 오른쪽 피연산자는 계산되지 않는다. 이것은 코드2에서 입증될 수 있다.

코드2

#include <stdio.h>

int main() {
 int iVar = 0 , iTmp;

  iTmp = 0 && iVar++;
 printf("iVar = %d\n" , iVar);
  return 0;
}

코드2의 iTmp = 0 && iVar ++;행을 보도록 한다. 왼쪽 피연산자에 상수 0을 지정하고 있는데, 이는 곧 거짓이다. 프로그램은 왼쪽 피연산자를 확인한 시점에 이 식의 결과가 거짓임을 보증할 수 있기 때문에, 오른쪽 피연산자를 확인할 필요가 없다. 따라서 iVar++는 실행되지 않기 않아서 printf는 iVar 값을 0으로 표시하는 것이다. 6번째 줄의 코드를 1 && iVar ++와 같이 써서 변경하면 iVar++가 수행이 되고확인한다는 것을 확인할 수 있다.

그런데 “한쪽이 참, 다른 한쪽이 거짓일 때 참"이라는 결과를 얻고 싶다면 어떻게하면 좋을까? 이런 논리 연산을 배타적 논리합이라고 한다. 배타적 논리합은 C 언어의 3가지 논리 연산자를 사용하여 계산할 수 있다. 비트 논리 연산과 혼동하지 말자. 여기의 배타적 논리합은 조건식의 논리 값을 얻기 위한 것이다.

이것을 실현하려면 평가해야 두 피연산자 중 하나가 참인지 확인하고, 쌍방이 참이라면 부정해서 거짓으로하면 된다. 한쪽이 참이면 참인 것은 논리합 bool1 || bool2, 양쪽이 참이라면 참인 것은 논리곱 bool1 && bool2이다. 첫번째는 논리합과 논리곱을 부정한 결과를 논리곱을 구한다.

(bool1 || bool2) && !(bool1 && bool2)

이를 구하면 bool1과 bool2 중 “한쪽은 참, 다른 한쪽은 거짓이면 참"이라는 배타적인 결과를 얻을 수 있다.

코드3

#include <stdio.h>

int main()
{
  int iBool1 , iBool2;
  printf("2개의 연산값을 입력해 주세요. >");
  scanf("%d %d" , &iBool1 , &iBool2);

 printf("iBool1 XOR iBool2 = %d\n" , (iBool1 || iBool2) && !(iBool1 && iBool2));
  return 0;
}

이것은 입력된 두 값 중에 한쪽이 참이고 다른 한쪽이 거짓인 경우에만 참인 결과를 얻기위한 테스트 프로그램이다. 양쪽의 피연산자가 거짓 또는 진실인 경우는 0(거짓)이 표시되고, 그렇지 않으면 1(참)인 결과를 얻을 수 있을 것이다.

1.3.2 - C 언어 | 흐름 제어 | if문

조건에 따라 실행하는 문장을 선택하는 if 문을 소개한다. if 문을 이용함으로써 상황에 따라 프로그램의 흐름을 변경할 수 있다.

프로그램을 분기시키기

지금까지는 프로그램은 main() 함수의 내부를 위에서 아래로 성실하게 수행하여 왔다. 입력을 배워서 보다 동적인 결과를 얻을 수 있게 되었지만, 프로그램은 항상 1개의 길로 나아갈 뿐이었다.

여기에서는 추가적으로 값을 판정하고 프로그래머의 의도에 따라 프로그램을 동작시킨다. 즉, 프로그램을 분기시킨다. 값을 판단하는 방법은 앞에서 “진위 판정"에서 배웠다.

프로그램을 분기하는 가장 기본적인 제어문은 if 문다. if 문은 “~가 ~하면 ~한다"라는 프로그램 제어를 가능하게 한다. ~가 ~라면이라는 부분은 관계 연산자와 논리 연산자를 이용한 조건식으로 표현할 수 있다.

if 문

if (조건식) 문장

조건식은 참 또는 거짓을 나타내는 값(즉, 결국은 숫자)을 지정한다. if 문은 조건식의 결과가 참인 경우에만 실행되고, 그렇지 않으면 실행되지 않는다. 보다 간단하게 말하면, 조건식에 0이 아닌 값이 지정된 경우에만 실행되는 것이다.

코드1

#include <stdio.h>

int main() {
  int iBool;
  printf("0 또는 다른 값을 입력하십시오. >");
 scanf("%d" , &iBool);

 if (iBool) printf("참이 입력되었습니다.\n");
  if (!iBool) printf("거짓이 입력되었습니다.\n");

  return 0;
}

코드1을 실행하면 값의 입력이 요구되므로 0 또는 다른 값(즉, true 또는 false)을 입력한다. 그러면 프로그램은 if 문에 입력된 값을 판단해 그 결과에 따라 표시하는 문장이 변한다. 첫번째 if 문은 iBool이 참이라면 그 다음의 if 문은 부정 연산자! 부정하고 있기 때문에 iBool이 거짓이면 계속해서 문을 실행한다.

iBool이 거짓이면 실행하는 if 문을 기술할 경우, !iBool 대신 iBool == 0라고 써도 같은 결과를 얻을 수 있다. if (!iBool)라는 문장은 영어권 사람들이 보면 직관적으로 if not iBool 느낄 수 있지만, 한국인은 if (iBool == 0) ...라고 쓰는 것이 이해가 쉬울지도 모른다. 이것은 취향의 문제이다.

코드1은 “~이 참이어야"라는 표현을 !iBool는 부정 연산자를 이용한 if 문에서 실현하고 있다. 그러나 이처럼 iBool을 판단하여 결과를 참일 경우와 거짓일 경우의 두 가지로 분기시키고 싶은 경우는 else부분을 사용하는 방법이 있다. else 부분은 if 문과 한 세트로 사용되는 것으로, if에서 판단한 조건식이 거짓인 경우에 실행된다. else 부분을 이용한 if 문은 다음과 같이 기술한다.

else 부분이 있는 if 문

if (조건식) 문장 1 else  2

if 문 조건식이 참이면 문장1이 실행되고 그렇지 않으면 else 키워드 다음 문장2가 실행된다. else 전에 반드시 if가 필요하며, if 문이 없는 곳에서 else 부분을 지정할 수 없다. else 부분이 있는 if 문을 사용하면 코드1을 다음과 같이 개선할 수 있다.

코드 2

#include <stdio.h>

int main() {
  int iBool;
  printf("0 또는 다른 값을 입력하십시오. >");
  scanf("%d" , &iBool);

  if (iBool) printf("참이 입력되었습니다. \n");
  else printf("거짓이 입력되었습니다. \n");

  return 0;
}

코드2는 코드1을 더 최적화된 방식으로 개량한 것이다. 이와 같이 else 부분을 이용하면 iBool가 가짜인지 여부를 확인할 필요가 없기 때문에 쓸데없는 처리를 생략할 수 있다. else 부분은 if 문 평가가 거짓인 경우에 실행된다. 물론 참이면 else 부분은 실행되지 않는다.

또한 if와 else는 다음과 같이 중첩(같은 형의 상자 등을 크기 순으로 몇개 겹쳐 중으로 들어갈 수 있게 한 것. 프로그래밍에는 동일한 명령을 거듭하는 것을 중첩이라고 함)할 수 있다. 이것은 “~하면 ~한다. 그렇지 않으면 ~한다. 그렇지 않으면 ~ 한다 …“라는 프로그램의 흐름을 만들 수 있도록 되고, 복잡한 제어 및 분석을 할 경우에 일반적으로 사용되는 있다. 다만, 중첩 관계는 가능한 적게하고, 읽기 쉬운 프로그램을 쓸 수 있도록 해야 한다.

코드3

#include <stdio.h>

int main() {
  char chVar;
  printf("Do you like C language? Y/N>");
  scanf("%c" , &chVar);

  if (chVar == 'Y' || chVar == 'y')
    printf("Good! Just go for it!!\n");
  else if (chVar == 'N' || chVar == 'n')
    printf("Why are you studying C language?\n");
  else
    printf("Error\n");

 return 0;
}

코드3은 프로그램에 “당신은 C 언어가 좋아?“라고 질문하고 Y(Yes) 또는 N(No)를 입력하고 질문에 대답하는 일반적인 명령 줄의 대화를 상정한 것이다. 프로그램은 입력된 값을 확인하고, “Y가 입력 된 경우”, “N이 입력 된 경우”, “그 이외의 값이 입력 된 경우"의 3가지 패턴으로 분기시킬 수 있다.

프로그램에서는 첫번째 if 문에서 입력된 문자를 저장하는 chVar 변수를 확인하고, ‘Y’또는 ‘y’중 하나가 입력되어 있는지 여부를 조사하고 있다. 이 판단의 결과가 거짓이면 else가 실행되지만, 이 if 문 else에는 또한 if 문을 이용하고 있기 때문에 중첩 구조로 되어 있다.

복합 문(블록)

지금까지의 프로그램에서는 if와 else 부분에 의해 프로그램을 분기시킬 수 있었지만, 실행할 수 있는 것은 1개의 문장만 이었다. 이것으로는 if 문으로 여러 문장을 지정할 수 없다. 예를 들어, 다음과 같은 프로그램은 잘못된 것이다.

if (exp) 
  iVar += 50;
  printf("iVar = %d" , iVar);
else ...

if 문에서 지정한 조건 exp가 참이면 iVar += 50는 if 문으로 실행되지만 printf()의 시점에서 원래의 제어에 돌아간다. 따라서 else는 직전의 if 문이 존재하지 않는 것으로 판단되는 컴파일 오류가 발생하게된다. if에서 여러 문장을 한꺼번에 실행시키고 싶은 경우, 복합 문(블록라고도 함)를 사용해야 한다.

복합문

{
  문장1
  문장2
  문장N...
}

복합 문은 {로 시작해서 }으로 끝난다. 문장을 작성할 수 있는 장소라면 어디든지 쓸 수 있다. {} 내부는 여러 문장으로 구성되어, 어떤 문장의 본체로서 기능할 수 있다. 예를 들어, 함수의 본체는 복합 문이라고 생각할 수 있다. C 언어는 문장의 끝에 세미콜론;을 지정하는 것이 보통이지만, 복합 문장의 끝에;를 붙이지 않는다. 또한, 복합 문은 중첩될 수 있다.

무언의 규칙으로는 복합 문의 내부는 들여 쓰기를 1단계 깊게하는 규칙이 있다. 이것은 C의 구문이 아니기 때문에, 따르지 않아도 컴파일할 수 있지만, 매우 읽기 어려운 소스가되어 버린다. 만약 다음과 같은 프로그램 소스를 작성했다면, 당신 이외에 읽어주는 사람은 없다고 생각해도 좋을 것이다.

if (a == b) {
printf("...");
if (a == c) {
printf("...")
}
}
else {
printf("...");
}

공동 개발과 오픈 소스 개발을 생각해서. 초반부터 올바른 소스 작성을 습관화하자. 통합 개발 환경의 프로그래밍 전용 편집기에서 자동으로 변경해 주기도 하지만, 간단한 텍스트 편집기를 사용하는 경우는 의식 할 필요가 있다. 다음과 같이 기술하면 쉽게 읽을 수 있다.

if (a == b) {
 printf("...");
  if (a == c) {
   printf("...")
 }
}
else {
  printf("...");
}

이렇게 기술하게 것으로, 문장이 어떤 복합 문에 속해 있는지를 판단하기 쉬워진다.

코드4

#include <stdio.h>

int main() {
  int iBool;
  printf("0 또는 다른 값을 입력하십시오. >");
 scanf("%d" , &iBool);

 if (iBool) {
    printf("참이 입력되었습니다.\n");
   return 1;
 }
 else {
    printf("거짓이 입력되었습니다.\n");
    return 0;
 }
}

이 프로그램은 참 또는 거짓을 나타내는 숫자를 입력하고, 그 결과에 따라 시스템에 반환 값을 변화시키고 있다. 중첩된 복합 문장의 내부에서 함수를 종료시킬 수 있기 때문에, if와 else에서 지정하고 있는 복합 문 내부에서 프로그램을 종료한다.

코드4의 if-else 문 처리는 printf() 함수와 return 키워드를 이용한 2개의 문장으로 구성되어 있기 때문에 하나의 문장으로 표현할 수 없다. 그래서 복합 문을 사용하여 여러 문장을 지정하고 있다. return이 반환 값이 어떻게 이용되는지는 시스템에 따라 다르다. 자세한 내용은 당신이 사용하고 있는 시스템에 대한 자세한 내용 공부하도록 한다. 시스템에 반환 값의 그 후는 C 언어의 관할이 아닌 것이다.

1.3.3 - C 언어 | 흐름 제어 | switch문

하나의 값에서 대응하는 복수의 코드 중에 하나를 선택하여 실행 switch 문을 소개한다.

다중 분기 판단

어떤 값의 상황에 따라 프로그램의 흐름을 변경하려면 if 문을 이용하여 해결할 수 있다. 분기의 수가 많은 경우는 if 문을 중첩된 if else if else ….라는 구조를 만들어서 구현할 수 있지만, 이것이 너무 많으면 if 문은 그다지 적합하지 않다.

예를 들어, 프로그램에 의미있는 수치를 메시지로 전달하고, 프로그램은 받은 값을 분석하고 적절한 처리를 실시하는 시스템을 실현하려면 메시지의 수는 엄청날 수 있다. 그래서 이런 프로그램을 실현하려면 switch 문을 사용하는 방법이 최적이라고 볼 수 있다.

switch문

switch (표현식) {
case 상수:
  블록문
default:
 디폴트 블록문
}

switch 문은 지정된 표현식을 판단해서 case에 지정된 상수와 일치하는 블록 문에서 실행을 시작한다. case는 복수 지정할 수 있으며 상수가 중복되어서는 안된다. default는 모든 case와 일치하지 않은 경우에 실행하는 특수 라벨이다. case에 상수만 지정할 수 있고, 변수는 지정할 수 없기 때문에 주의하도록 하자.

그런데 switch 문은 case와 default 뒤에 콜론:을 붙이는 이상한 구문을 가지고 있는데, 이것은 라벨 정의라는 구문에서, 그 직후 문장에 이름을 붙이기 위한 것이다. 라벨에 대한 자세한 내용은 goto 문으로 설명하겠지만, case와 default도 라벨의 일종이다.

라벨의 개념에 따라, 보다 정확한 switch 문의 성질을 해설하는 것이라면, 이것은 분기보다는 점프에 가까운 제어문이라고 생각된다. switch는 표현식을 판단하여 그 결과와 동일한 상수의 case 라벨을 가진 문장까지 이동한다. switch를 if 문 같은 분기문이라고 생각하고 사용하면, 아래 문장까지 실행(fall through)되는 현상으로 인해 버그가 될 수 있다. 다음과 같은 switch 문을 생각해 보자.

switch (0) {
case 0:
  printf("case 0");
case 1:
  printf("case 1");
default:
 printf("default");
}

이 switch 문을 실행하면 어떻게 될까? 식의 판단 부분에는 0이라는 상수를 지정한다. 이것은 case 0: 라벨과 일치하기 때문에 printf("case 0");가 실행된다는 것까지는 이해할 수 있는데, 생각치도 못하게 프로그램은 printf("case 1");printf("default");까지도 수행하고 마는 것이다. 이와 같이, 하단의 case와 default도 실행해 버리는 switch의 성질을 일반적으로 폴스루(Fall-through)라고 하는데, 이 결과에서 switch 문의 case는 if-else와는 달리, 단순한 문장의 라벨이 있는 것을 확인할 수 있다. switch 문은 표현식과 일치하는 case 라벨의 문장으로 점프할 뿐이다라는 개념을 이해하면, 어째서 그 후(다른 case와 default 다음)의 문장까지 실행되어 버리는지 납득할 수 있을 것이다.

그런데 대부분의 경우 아래 문장까지 실행되는 것은 바람직하지 않다. 가능하다면 목적의 case 라벨의 문장을 실행한 후의 switch를 벗어나 싶을 것이다. 여기에는 몇 가지 방법이 있지만, 가장 일반적인 것은 break 문을 사용하는 것이다. break는 이를 포함하는 가장 안쪽의 제어를 빠져 나오는 역할로써, switch와 루프를 벗어나기 위해 사용되는 키워드이다. 다음과 같이 기술하면, 특정 라벨을 실행한 후 빠져 나오게 된다.

switch (0) {
case 0:
 printf("case 0");
 break;
case 1:
 printf("case 1");
 break;
default:
  printf("default");
  break;
}

덧붙여서, case도 default도 선택적이며, 지정 위치는 임의이다. 일반적으로 default는 그 특성상, 하단 (마지막)로 지정되어 있지만, case 라벨이 default를 걸쳐도 문제는 없다. default가 생략된 상태에서 일치하는 case가 없으면 아무것도하지 않고 switch 문을 빠져 나온다.

코드1

#include <stdio.h>

int main() {
  int iSelected;
  printf("당신은, 귀여운 고양이를 만났다.\n");
  printf("0=머리를 쓰다듬는다, 1=꼬리를 만진다, 2=손가락을 내민다 >");
 scanf("%d" , &iSelected);

 switch(iSelected) {
 case 0:
   printf("반기는 듯하다.\n");
    break;
  case 1:
   printf("고양이가 싫어한다.\n");
    break;
  case 2:
   printf("손가가락의 냄새를 맡고 있다. 본능 같다.\n");
   break;
  default:
    printf("올바른 선택 번호를 입력하세요.\n");
 }
 return 0;
}

코드1은 간단한 분기 프로그램이다. 프로그램을 실행하면 문장이 표시되고 어떻게 행동 할 것인지를 선택하는 입력이 요구된다. 그 후에 프로그램은 입력된 값을 switch 문으로 판단하여 각 case문에에 분기한다. 적절한 문장을 표시시킨 후, 프로그램을 종료한다.

case에서 지정할 수 것은 상수이었지만, ASCII 코드는 1바이트의 수치로 표현할 수 있으므로, 1개의 문자 상수를 case로 지정할 수도 있다. 문자 상수를 지정하는 경우는 case 'A': 와 같이 기술한다.

코드2

#include <stdio.h>

int main() {
  char chSelected;
  printf("당신은, 귀여운 고양이를 만났다.\n");
  printf("A=머리를 쓰다듬는다, B=꼬리를 만진다, C=손가락을 내민다 >");
 scanf("%c" , &chSelected);

  switch(chSelected) {
  case 'A':
   printf("고양이가 싫어한다.\n");
    break;
  case 'B':
   printf("고양이가 싫어한다.\n");
    break;
  case 'C':
   printf("손가가락의 냄새를 맡고 있다. 본능 같다.\n");
   break;
  default:
    printf("올바른 선택 번호를 입력하세요.\n");
 }
 return 0;
}

이 프로그램은 코드1을 고쳐서 선택을 문자로 할 수 있도록 한 것이다.

1.3.4 - C 언어 | 흐름 제어 | while문

while 문은 조건이 참일 때, 지정한 코드를 반복한다. 여기에서는 while 문을 이용한 반복의 제어 방법을 설명한다.

루프 만들기

프로그램 제어에서 분기와 마찬가지로 매우 중요한 존재가 반복 처리이다. 복잡한 계산이나 검색 등, 실전 프로그램에서 여러번 사용되므로 확실히 기본을 익힐 필요가 있다. 반복 처리는 특정 문장을 지정한 조건이 충족될 때까지 반복하여 수행한다. 이를 수행하는 가장 간단한 방법은 while 문을 이용한 방법이다.

while은 if 문에 유사한 구문으로, 조건식이 참이면 지정된 문장을 반복하는 것이다. 반대로 말하면 while은 조건식의 판단 결과가 거짓이 즉, 0이 될 때까지 문장을 반복한다.

while 문

while(조건식) 문장

조건식이 거짓이 될 때까지 문장이 실행되므로, 일반적으로 while 문장에서 조건식에 영향을 주는 변수 등을 조작하여 카운터의 역할을 갖게 한다. 카운터가 되는 변수를 사용하여, 문장 반복 횟수와 현재 몇 번째 반복인지를 알 수 있다.

코드 1

#include <stdio.h>

int main() {
  int iCount;
 printf("반복 횟수를 입력하십시오. >");
 scanf("%d" , &iCount);

  while(iCount > 0) {
   printf("카운터 = %d\n" , iCount);
   iCount--;
 }
 return 0;
}

코드1을 실행하면 반복 횟수를 요구하고 숫자를 입력하면, while은 입력된 값을 저장한 iCount을 확인하여 이것이 0이상이면 문장을 반복한다. while의 본체 문장에서는 카운터 값을 표시한 다음에 카운터 값을 감소시키고 있다. 이것을 반복하다 보면 카운터는 언젠가 0이 되고, 그러고 루프를 빠져 나온다.

무한 루프가 되어 버린 경우는 프로그램을 종료시키는 수단이 없다. 도중에 강제로 루프에서 벗어나려면, while 확인하고 있는 조건식이 거짓이 되도록 카운터를 조작하거나, break 문을 사용하여 while에서 강제로 나갈 수 있다. break 문은 제어를 벗어나 기능이 있기 때문에, 현재의 루프에서 벗어날 수 있다.

코드2

#include <stdio.h>

int main() {
  int iCount = 0;
 while(1) {
    if (iCount == 1000) break;
    printf("iCount = %d\n" , iCount++);
  }
 return 0;
}

코드2는 의도적으로 while의 조건식을 1(true)하여 무한 루프로 돌도록 하고, while을 빠져 수단으로 break 문을 사용하고 있다. iCount 변수의 값을 반복할 때마다 증가하고 이것이 1000에 도달하면 빠져 있다.

증가 처리는 printf("iCount = %d\n" , iCount++);에서 수행하고 있다. 초보자에게는 별로 익숙하지 않은 작성법이기 때문에 조금 설명하겠다. 이것은 두 줄로 나누어 다음과 같이 쓸 수도 있지만, 이 프로그램에서는 굳이 printf() 함수의 인수로 증가하고 있다.

printf("iCount = %d\n" , iCount);
iCount++;

이렇게 써서 문제는 없지만, 필자는 해설서 등에서 긴 소스 프로그램을 보면 피곤하므로, 불필요한 처리 및 중복 부분은 없애려고 노력하고 있다. 따라서 위의 두줄을 한줄로 정리하는 방법으로, printf() 함수의 인수로 지정하고 있는 iCount를 잘 살펴보면, printf()에서 사용된 후에 증가되도록 후위 증가 연산자를 사용했다. 후위 증가 연산자는 변수를 사용한 후에 증가하는 특징이 있었다는 것을 기억하도록 하자.

덧붙여서, while와 같은 반복 제어는 중첩해도 문제는 없다. 실용적인 프로그램은 복잡한 알고리즘과 그래픽 처리 등에서 비번히 중첩된 반복 제어를 수행한다.

while (expr1) {
  while (expr2) {
   ...
 }
}

이러한 중첩된 반복 처리는 응용 프로그램 개발은 결코 드문 일이 아니다. 예를 들어, 2차원의 정보에 대한 계산을 할 때, 반복 처리를 중첩하여 순서대로 계산하는 방법을 사용하고 있다. 이것은 2차원 컴퓨터 그래픽에 대한 전반적인 처리를 할 때 등에 응용할 수 있을 것이다. 다음 프로그램은 구구단 표를 만들어 화면에 표시한다.

코드3

#include <stdio.h>

int main() {
  int iOp1 = 1 , iOp2 = 1;

  while (iOp1 < 10) {
   while(iOp2 < 10) {
      printf("%2d " , iOp1 * iOp2);
     iOp2++;
   }
   printf("\n");
    iOp2 = 1;
   iOp1++;
 }
 return 0;
}

코드3을 실행하면 구구단 표가 출력된다.

덧붙이자면, printf() 함수의 형식 제어으로 %2d라는 표현이 있는데, 이 2라는 숫자는 문자 폭을 지정하고 있다. 이 경우는 %d를 지정했을 때와 동일하게 정수 값을 표시하지만, 문자 폭을 지정하고 있기 때문에, 최소 2문자의 폭을 사용하여 숫자를 표시한다. 1자리의 경우는 2문자에 못 미치기 때문에 공백으로 채워진다. 이것은 단순히 구구단 표를 아름답게 표시하기 위해 사용했을뿐, 그다지 중요하지 않다.

루프의 맨 위로 돌아가기

어떠한 사정으로 반복 처리의 복합 문장의 내부에서 처리를 중단하고, 다음 반복으로 넘어가고 싶은 경우가 있다. 즉, 현재의 반복은 그 시점에서 종료하고, while 문의 처음으로 돌아 표현식을 판단하고자 하는 상황이다.

이를 실현하려면 continue 문을 사용한다. 이것을 사용하면 문장의 나머지 처리를 수행하지 않고 루프의 선두로 돌아올 수 있다.

코드4

#include <stdio.h>

int main() {
 int iCount = 0;

 while(iCount++ < 100) {
   if (iCount % 2) continue;
   printf("%d " , iCount);
 }
 return 0;
}

이 프로그램은 화면에 짝수만 표시한다. 카운터 변수인 iCount를 2로 나눈 나머지가 0이 아닌 경우, 이는 즉 홀수인 경우라고 생각할 수 있으며 iCount % 2을 구하고, 이것이 참이라면 continue 문을 사용하여 나머지 처리는 무시한다. 그 결과 printf()가 실행되는 것은 iCount가 짝수인 경우만 이다.

1.3.5 - C 언어 | 흐름 제어 | do~while문

문장을 반복 여부의 판단을 반복할 문장을 실행한 후에 실시하는 do 문장에 대해 설명한다. do 문은 while 문과는 다르게 조건에 관계없이 반드시 1번은 문장을 실행한다.

지연 판단형의 반복 처리

while는 반복할 문장을 실행하기 전에, 조건식을 판단하여 반복 처리를 계속할지 여부를 결정한다. 그러나 이와는 반대로 문장을 실행한 후에 조건식을 판단하는 do 문장이라는 것도 있다.

do 문

do 문장 while (조건식);

반복 처리의 대상이 되는 문장을 먼저 실행하는 것을 제외하고는 while 문과 기본적으로 동일하다. while 문은 식을 판단하고 루프를 실행하기 때문에, 최초에 판단이 거짓이면 문장을 한번도 실행하지 않고 그냥 지나쳐 버린다. 비록 판단을 해서 어떤 결과이든 최소한 한번은 실행될 경우에 do 문은 위력을 발휘된다.

코드1

#include <stdio.h>

int main()
{
  int iCount = 1 , iMax;
  printf("반복 횟수를 입력하십시오. >");
 scanf("%d" , &iMax);

  do {
    printf("%d번째 루프입니다.\n" , iCount++);
  } while (iCount <= iMax);
 return 0;
}

코드1은 첫번째 반복할 횟수를 입력하고 지정된 값을 바탕으로 do 문으로 문장으로 반복한다. 하지만 while 문과 크게 다른 점은 iCount <= iMax가 성립하지 않고(거짓이라고 해도) 반드시 한번은 문을 실행하는 것이다. 입력된 값이 음수나 0이었다해도, do는 반드시 문장을 한 번 실행한다. 이 식의 판단이 문을 실행한 후에 이루어지고 있기 때문이다.

그러나 상당히 특별한 경우가 아닌 한 do 문장을 사용하지 않는다. 필자의 경험으로는 수만 행에 이르는 애플리케이션 시스템을 설계했을 때도 적극적인 사용 의사가 없으면 do 문장을 사용하는 필요에 직면한 적은 없었다. 하지만 다른 사람이 작성한 소스를 읽을 때에 do 문장이 나오는 것은 충분히 생각할 수 있으며, 알고리즘에 따라 do 문을 사용하는 것이 스마트하게 쓸 수 있다는 경우도 있기 때문에 기억할 둘 필요가 있다.

1.3.6 - C 언어 | 흐름 제어 | for문

for 문에 대해 설명한다. for 문은 반복 횟수를 계산하기 위한 변수의 초기화나 증가를 구문으로 작성할 수 있다.

카운터 제어 루프

기본적으로 반복 처리는 while 문만 알고 있다면 충분히 사용할 수 있지만, 대부분의 프로그래머는 앞으로 소개하는 for 문을 많이 사용하고 있다. for 문은 조건식 외에 카운터 변수의 초기화 및 계산 처리를 관리해주는 편리한 제어문이다.

for 문

for (초기화 식; 조건식; 루프 식) 문장

조건식이 참이면 문장을 반복한다는 점에서는 while과 같다. 초기화 식은 for 루프가 시작되기 전에 한번만 계산되는 식으로, 보통은 루프 제어를 이용하기 위한 카운터 변수 등을 초기화하는데 사용한다. 루프 식은 문장을 반복할 때마다 계산되는 식으로 카운터 변수를 증가하거나 감소하는데 사용된다. 이것은 다음의 while 문을 이용한 프로그램과 동일하다.

초기화 식;
while (조건식) {
  문장;
  루프 식;
}

대부분의 경우 while 문에서 반복 처리하는 때에 루프 제어를 위해 카운터를 이용한 위와 같은 구조이다. for 문은 이러한 구조를 완전한 구문으로 규정하고 있기에 대부분의 프로그래머는 적극적으로 for 문을 이용하고 있다.

또한 for 문은 초기화 식, 조건식, 루프 식 모두 생략할 수 있다. 조건식을 생략하면 항상 참으로 해석되기 때문에 무한 루프이다. 이 경우 break 문을 사용하여 빠져 나올 수 있는 대책이 필요하다. 다만 생략하는 경우는 세미콜론;을 생략할 수 없다. 예를 들어, 모든 식을 생략하는 for 문은 for (;;) {...와 같이 기술해야 한다.

코드1

#include <stdio.h>

int main() {
 int iMax , iCount;
  printf("반복 횟수를 입력하십시오. >");

 for(scanf("%d" , &iMax) , iCount = 0 ; iCount < iMax ; iCount++)
    printf("%d번째 반복입니다.\n" , iCount);
  return 0;
}

코드1은 입력된 횟수만큼 for 문을 이용하여 표시 처리를 반복하는 간단한 프로그램이다. 그러나 이 프로그램은 조금 짓궂은 소스 부분이 있다. 그것은 for 문의 초기화 식의 부분에 scanf ( "%d", &iMax), iCount = 0이다. 이 문장에는 쉼표가 2개 있지만, 사실은 이것들은 같은 쉼표로 C 언어 구문에 다른 성질을 가지고 있다. 즉, 이질적인 기능을 가지고 있다.

scanf ( "%d", &iMax), iCount = 0이라는 식의 두 쉼표의 차이에 대한 질문에 제대로 대답할 수있는 프로그래머는 그렇게 많지 않다. 사실 쉼표는 단순한 구분으로서 이용하는 경우와 수식 연산자로 사용할 수 있다. scanf("%d", &iMax)라는 문장안에 있는 쉼표 연산자가 아니다. 이것은 구분 기호이다.

이에 대해서 식에서 사용된 경우는 순차적으로 수행하는 콤마 연산자로써 기능을 한다. 이 쉼표의 사용법은 매우 희소하다. 콤마로 분리된 수식은 반드시 왼쪽에서 오른쪽으로 수행된다. 순차적으로 콤마 연산자의 연산 결과는 오른쪽 피연산자와 같은 값과 형식을 갖는다. 피연산자는 모든 타입을 지정할 수 피연산자 간의 형식 변환되지 않는다.

for 문의 초기화 식이나 루프 식으로 여러 수식을 수행하려면 코드1과 같이 순차적으로 콤마 연산자를 이용하여 여러 식을 지정할 수 있다.

그러나 일반론으로서 순차적인 콤마 연산자를 적극적으로 사용하는 것은 피해야 한다고 생각한다. 서로 밀접하게 관련 구문이거나, 여러 줄의 계산을 어떻게 해서든 하나의 문장으로 서술해야 하는 경우라면 사용해도 좋다. 예를 들어, 두 변수의 내용을 교환하는 프로그램을 만드는 경우 등에 이용하는 것도 적당하다.

코드2

#include <stdio.h>

int main() {
 int iVar1 = 100 , iVar2 = 200;
  iVar2 ^= iVar1 , iVar1 ^= iVar2 , iVar2 ^= iVar1;
 printf("iVar1 = %d : iVar2 = %d\n" , iVar1 , iVar2);

 return 0;
}

이 프로그램은 변수 iVar1과 iVar2의 값을 교환하여, 그 결과를 표시하는 것이다. 값의 교환 알고리즘은 순차적으로 콤마 연산자를 사용하여 하나의 식으로 표현하고 있다. 그런데 이 프로그램에서도 조금 심술 궂은 소스가 있다. 일반적으로 변수 값의 교환은 임시 저장용 변수를 정의하고 다음과 같이 이루어지는 것이 일반적이다.

iTmp = iVar1 , iVar1 = iVar2 , iVar2 = iTmp;

그러나 코드 2에서는 배타적 논리합의 성질을 잘 이용하고, 임시 저장용 변수를 준비하지 않고 교환을 실시하고 있다. 주제에서 벗어나는데 이렇게 연구함으로써 프로그램의 요구에 적합한 처리를 실현할 수 있을 거에요.

그러나 코드2는 배타적 논리합의 성질을 잘 이용하여, 임시 저장을 위한 변수를 제공하지 않고 교환을 수행하고 있다. 본론에서 벗어났지만, 이렇게 연구하여 프로그램의 요구에 적합한 처리를 실현할 수 있게 되었다.

이야기를 본론으로 되돌아와서, 코드2와 같이 순차적인 콤마 연산자를 사용하여 하나의 문장으로 여러 식을 계산시킬 수 있었다. 하지만 역시 먼저 말한대로 순차적으로 콤마 연산자를 적극적으로 사용하는 것은 피해야하며, 이 경우에도 하나의 문장으로 표현하는 것에 의미는 없다. 오히려 다음과 같이 기술하는 것이 일반적이다

iVar2 ^= iVar1 ; iVar1 ^= iVar2 ; iVar2 ^= iVar1;

이것은 하나의 표현이 아니라 세미콜론으로 3개의 문장으로 나누어져 있다 만, 이것으로 문제가 발생하지 않는다. 오히려 이 작성법이 일반적이기 때문에 많은 프로그래머에게는 이쪽이 더 읽기 쉽다고 느낄것이다.

for 문도 while과 마찬가지로 중첩될 수 있다. 물론 break 문이나 continue 문을 사용할 수도 있다. 복잡한 계산이나 처리에는 반드시라고 할 정도로 반복 처리가 필요하기 때문에 for 문이나 while 문 사용법은 확실히 마스터한다. 다음 프로그램은 while 문으로 만든 구구단 표를 for 문으로 만든 것이다.

코드3

#include <stdio.h>

int main() {
 int iOp1 , iOp2;

  for(iOp1 = 1 ; iOp1 < 10 ; iOp1++) {
    for(iOp2 = 1 ; iOp2 < 10 ; iOp2++) {
      printf("%2d " , iOp1 * iOp2);
   }
   printf("\n");
  }
 return 0;
}

while을 사용하는 경우에 비해, 적은 소스 줄이 스마트해진 것을 느껴졌을 거라고 생각한다. while의 경우는 복합한 문장의 내부에서 증가 및 초기화 등의 작업을 수행해야 했지만, for를 사용하면 이들을 초기화 식이나 루프 식으로 구성할 수 있다. 많은 프로그래머들이 적극적으로 for를 사용하는 이유를 이제 알았다고 생각된다.

1.3.7 - C 언어 | 흐름 제어 | goto문

지정한 문장에 무조건 점프하는 goto 문장을 소개한다.

무조건 점프

지금까지의 분기와 반복은 어떤 조건을 지정하여 제어를 실시했지만, 무조건 지정한 위치에 프로그램을 이동시킬 수 있다. 무조건 점프를 할 때에는 goto 문을 사용한다.

goto 문

goto 레벨;

라벨(label)에는 점프하는 문장이 있는 라벨을 지정한다. 라벨은 “switch 문"에서 조금 소개 했었는데, 쉽게 말하면 문장에 붙이는 식별자와 같은 것이다. 문장에 라벨을 붙이면 goto 문으로 그 자리로 이동할 수 있다. 레벨은 다음과 같이 지정한다.

라벨 선언

라벨 : 문장

이렇게 하면 문장에 라벨을 지정할 수 있다. 라벨에 사용되는 이름은 변수 등의 명명 규칙과 동일하다. 문장에 라벨을 붙이면, goto 문을 사용하여 자유롭게 프로그램의 흐름을 변경할 수 있다. 다만 goto 문은 동일한 함수 내에 있지 않으면 이동할 수 없다. 다른 함수의 문장에 바로 이동할 수 없기 때문에 주의하자.

코드1

#include <stdio.h>

int main() {
  int iCount = 0;

LOOP:
  printf("카운터 = %d\n" , iCount);
 iCount++;
 if (iCount < 10) goto LOOP;

 return 0;
}

이 프로그램은 반복 처리를 goto 문으로 재현한 것이다. iCount 변수가 10 이하이면 goto 문을 사용하여 LOOP 레이블로 돌아가고, 그렇지 않으면 프로그램을 종료한다.

그러나 for 문이나 while 문을 사용할 수 있는 곳에서는 어김없이 for 문이나 while 문을 사용하여 반복 처리를 실시해야 하며, goto 문을 사용해서는 안된다. 대부분의 경우 goto 문을 사용하지 않아도, if 나 for 문 같은 제어문에서 충분히 원하는 처리을 수행할 수 있다. goto 문을 남용하는 경우 프로그램의 흐름을 파악할 수 없으며 유지 보수가 어렵 기 때문에 goto 문은 원칙적으로 사용해서는 안된다.

goto 문을 사용하는 곳이라고 생각되는 곳은, goto 문을 사용하여 프로그램을 스마트하게 기술할 수 있는 특별한 알고리즘을 작성하는 경우이거나, 여러 단계로 중첩된 제어문에서 벗어날 같은 경우이다. 예를 들어 for 문을 중첩된 프로그램이 중간에 처리를 끝내고 싶은 경우, break 문을 사용하여 한단계의 제어만 벗어날뿐 모든 제어를 벗어날 수 없다. 그래서 goto 문을 사용하여 벗어날 수 있다.

for(;;) {
  for(;;) {
   ...
   if(error) goto ERROR;
 }
}
ERROR:  ...

이와 같이 중첩된 반복 처리의 내부에서 오류가 발생하는 등의 이유로 최상위 제어로 전환하고자 하는 경우, goto 문을 사용하여 강제로 나갈라고 하는 수단으로 효과적이다.

1.3.8 - C 언어 | 흐름 제어 | 조건 연산자

조건의 결과에 따라 판단하는 식을 선택하는 조건 연산자에 대해 설명한다. if 문에 의한 제어와 달리, 조건 연산자는 조건에 따라 어떤 식을 선택할지에 대해 판단한다.

3항 연산자

C 언어 연산자의 대부분은 하나의 피연산자를 받는 단항 연산자 또는 두 개의 피연산자를 받는 2항 연산자이며, 3개의 피연산자를 받는 3항 연산자가 존재한다. 그것은 조건 연산자 “? :“이다.

조건 연산자는 if 문과 매우 유사한 동작을 하나의 식으로 실현시킬 수 있다. 따라서 소규모 분기 처리에 적합하다. 예를 들어, 어떤 값 n이 0이 아니라면, 변수 z에 x를 대입하고, 그렇지 않으면 z에 y를 대입한다라는 식으로 분기에 적합하다. 조건 연산자는 다음과 같이 설명한다.

조건 연산자

조건식? 수식1 : 수식2

조건식에는 판단하는 식을 지정한다. 여기에서 지정한 조건식이 참이면 수식1이 선택되고, 이 연산자의 결과는 그 값이 되고, 그렇지 않으면 수식2가 선택되어 그 값이 결과로 반환된다. 변수 z에 대입하는 값을 조건에 의해 분기시키고 싶은 경우는 다음과 같이 될 것이다.

z = n ? x : y;

이 수식에서 n이 참이면 z에 x를 대입하고, 그렇지 않으면 z에 y를 대입한다는 것을 나타낸다. 이를 if 문을 사용하여 작성할 수 있지만, if-else를 이용한 경우에는 이보다 스마트하게 기술할 수 없다.

코드1

#include <stdio.h>

int main() {
  int iVariable;
  printf("Please input a number 0 or some else>");
  scanf("%d" , &iVariable);
 printf("An input value is %s.\n" , iVariable ? "True" : "False");
  return 0;
}

이 프로그램은 사용자로부터 0 또는 다른 값을 입력받아 그 값을 확인한다. 입력된 값이 참, 또는 거짓에 의해 printf() 함수가 표시하는 문자가 변화하는 프로그램이다. printf() 함수에 자세히 보도록 하자.

iVariable ? "True" : "False"라는 식은 iVariable을 확인하여 이것이 참이라면 “True"를, 그렇지 않으면 “Flase"을 돌려준다. printf() 함수의 첫번째 파라미터에는 문자열을 받을 서식 제어 문자 %s를 지정하여 이를 표시하고 있다.

1.4 - C 언어 | 함수

함수(function)란 “기능"을 뜻한다. 즉, 기능을 구현하는 부분을 따로 떼어 구현하는 것으로, 구조화 프로그램의 중요한 개념이라 하겠다. 여기서는 함수 작성법 및 호출 방법에 대해서 설명하겠다.

1.4.1 - C 언어 | 함수 | 함수 만들기

반복되는 처리는 함수로서 부품화할 수 있다.

특정 처리를 함수에 정리한다

지금까지 프로그램은 main() 함수에 작성해 왔다. 처음에 설명했듯이 main() 함수는 프로그램이 실행될 때에 최초에 호출되는 애플리케이션 진입점을 나타내는 특수한 함수이다. 우리는 필요한 경우 main() 이외의 함수를 만들 수 있다. 지금까지는 표준 함수로 정해져 있는 printf()와 scanf() 등의 함수를 사용하여 왔는데, 이러한 일부 기능을 정리한 함수를 직접 만들 수 있는 것이다.

프로그램에서 여러번 사용되는 처리 함수로 정리하여, 동일한 코드를 여러번 작성하는 번거로움에서 해방되어 프로그램 전체에 정합성을 갖게 할 수 있다. 이는 응용 프로그램의 설계에 있어서 매우 중요한 것이다. 처음에 약간 설명했지만, 함수를 정의하려면 다음과 같이 설명한다.

함수의 정의

반환자료형 함수명 (매개 변수 목록) {
  문장
  ...
}

기본적인 작성법은 main() 함수와 동일하다. 함수명은 변수와 동일하게 C 언어의 명명 규칙을 따르고 있으면 자유롭게 지정할 수 있다. 함수의 기능을 나타내어 알기 쉽게 의미있는 이름으로하는 것이 바람직하다. 반환 값과 매개 변수 목록은 “파라미터와 반환 값"에서 자세히 설명하겠지만, 이것들을 이용하면 함수간에 데이터를 교환할 수 있다. 이 장에서는 우선 함수는 다음과 같이 정의한다.

void 함수명() { ... }

이 함수는 값을 받지 않고 값을 돌려주지 않는 것을 나타낸다. 반환 값의 자료형으로 지정하는 void는 함수가 값을 반환하지 않는 것을 의미한다. 값을 반환하지 않는 함수는 return 키워드 값을 반환할 수 없다. 함수를 호출할 때는 printf()와 같은 표준 함수처럼 호출할 수 있다.

함수명();

한번 만든 함수는 여러번 호출할 수 있기 때문에, 프로그램의 재사용이 가능하다. 다음의 프로그램은 새로운 함수 Function()를 정의하고 main() 함수에서 이것을 호출하여 사용하고 있다.

코드1

#include <stdio.h>

void Function() {
 printf("Kitty on your lap\n");
}

int main() {
  Function();
 Function();
 return 0;
}

Function() 함수는 printf() 함수를 호출하여 화면에 문자를 표시하는 간단한 처리를 수행한다. main() 함수에서 Function() 함수를 두번 호출한다. 그 결과, 화면에는 “Kitty on your lap"이라는 문자가 두 줄에 걸쳐 표시된다. 그러나 함수는 사용되는 함수보다 먼저 기술해야 합니다. 코드1을 보고 알 수 있듯이, Function() 함수는 이를 호출하는 main() 함수보다 이전에 정의되어 있다.

왜 함수가 사용되기 이전의 위치로 지정해야 하냐면, 함수를 발견하기 전에 함수를 호출하면 컴파일러는 기본적으로 int 형의 반환 값을 가진 인수를 받지 않는 함수로 인식한다. 따라서 기본 형식이 아닌 함수를 정의보다 이전에 호출하고, 형의 불일치로 컴파일 에러가 되어 발생한다.

예를 들어, 다음 프로그램은 Function1()는 문제없이 호출할 수 있지만 Function2()는 오류이다.

int main() {
 Function1();
  Function2(); /*형식 불일치 오류*/
}
int Function1() {
  return 0;
}
void Function2() {}

Function1()는 반환 값이 int 형으로 매개 변수가 없는 함수이므로 정의가 호출보다 뒤에 있어도 호출과 같은 형태이므로 문제없이 호출할 수 있지만, Function2()는 반환 값이 void 형이므로 그 이전 호출과 형태가 다르다. 컴파일러는 함수를 다시 선언으로 간주하고, 형태가 다르기 때문에 오류를 발생시키는 것이다.

코드1의 흐름을 따르면, 먼저 main() 함수가 실행되고 Function() 함수가 호출된다. 함수가 호출되면 프로그램은 그 함수에 제어를 이동시킨다. 코드1의 경우 Function() 함수의 본체에 제어가 이동하게 된다. Function() 함수의 처리가 완료되면, 프로그램은 함수를 호출한 원래 위치에 제어를 리턴한다. 이 경우는 main() 함수로 돌아 오게 된다.

그림1 - 함수의 호출과 복귀

함수의 호출과 복귀

함수를 호출하면 함수의 처리가 완료되면 제어가 함수를 호출한 원래의 위치로 돌아오기 때문에 프로그램의 흐름은 결국 main() 함수로 돌아간다.

void 형의 반환 값을 갖는 함수는 값을 돌려 줄 필요가 없기 때문에 return 문을 생략할 수 있다. main() 함수는 int 형의 종료 코드를 시스템에 반환해야 하므로 return 문을 사용하여 값을 반환하지만 Function() 함수는 return 문을 사용하지 않는다. 그러나 다음과 같이 명시적으로 return 문을 사용하여 함수를 종료 할 수 있다. 그러나 값은 반환시키지 않기 때문에 식을 지정할 수는 없다.

void Function() {
  printf("Kitty on your lap\n");
 return;
}

특정 위치에서 함수를 끝내고 싶다면, return 문을 사용하여 제어를 반환할 수 있다.

함수는 몇겹씩 호출할 수 있습니다.main()함수가 Function1()함수를 호출하는, Function1()함수가 Function2()함수를 호출하는……라는 듯 함수에서 다른 함수를 몇겹으로 불러냈다고 해도 함수는 자신을 불러낸 장소에 제어를 되돌리는 성질이 있으므로 반드시 최고 수준의 제어, 즉 main()함수로 돌아갑니다.

함수는 몇번이나 겹쳐서 호출할 수 있다. main() 함수가 Function1() 함수를 호출하고, Function1() 함수가 Function2() 함수를 호출 …… 등등 함수에서 다른 함수를 여러번 호출해도 함수는 자신을 호출한 곳으로 제어를 되돌리는 성질이 있으므로 반드시 최상위 제어, 즉 main() 함수로 복귀한다.

코드2

#include <stdio.h>

void Function2() {
 printf("Function2() : return\n");
}

void Function1() {
 printf("Function1() : Call Function2()\n");
  Function2();
  printf("Function1() : return\n");
}

int main() {
 printf("main() : Call Function1()\n");
 Function1();
  printf("main() : return\n");

 return 0;
}

코드2는 Function1() 함수에서 Function2() 함수를 호출한다. 이 프로그램은 제어의 흐름을 시각적으로 확인할 수 있도록 하기 위해, 각 함수는 자신의 함수명과 처리를 화면에 표시한다.

프로그램은 먼저 main() 함수에서 Function1() 함수를 호출하여 Function1() 함수는 Function2() 함수를 호출한다. Function2() 함수는 즉시 제어를 반환하기 때문에 Function1() 함수에 제어가 돌아간다. 그리고 Function1() 함수를 종료하고, 마지막으로 main() 함수에 복귀하고 있는 것을 확인할 수 있다. 표시된 결과를 보면 프로그램이 어떤 순서로 실행되고 있는지 이해할 수 있다.

1.4.2 - C 언어 | 함수 | 파라미터와 반환 값

함수는 호출한 곳에서 처리에 이용하기 위한 값을 받거나, 처리 결과가 되는 값을 호출자에게 되돌려 받을 수 있다.

함수에 값을 전달하기

그동안 일부의 표준 함수를 사용하였다. 그 모든 것은 반드시 괄호 () 안에 값을 지정할 수 있었다. 예를 들어 printf() 함수에는 문자열을 화면에 표시하는데 문자열 상수를 () 안에 지정했다.

이처럼 함수에 어떤 정보를 전달할 수 있다. 이렇게 하면 동적으로 데이터를 처리할 수 있는 실용적인 함수를 구현이 가능하게 될 것이다.

함수에 전달되는 정보를 인수라고 한다. 인수의 형태는 임의로 지정할 수 있다. 인수를 호출하는 곳에서 받으려면 전달된 값을 저장하는 변수가 필요하다. 인수 목록으로 인수를 받기 위한 변수를 매개 변수라고 한다. 간단하게 말하면 함수를 호출하는 측이 함수에 전달 값을 인수라고 하고, 함수 본문이 어떤 데이터를 받기 위하여 미리 확보하는 변수를 매개 변수라고 부르는 것이다.

인수를 받으려면 함수 정의의 괄호 () 안에 받을 값의 형태와 식별자를 지정한다. 여러 매개 변수를 지정하려면 쉼표로 구분한다. 예를 들어 다음 함수는 int 값과 float 형의 값을 받는 함수이다.

void Function( int iValue , float fValue) { ...

당연히, 이 함수를 호출할 때는 매개 변수에 지정된 정보를 전달해야 한다. 이 함수를 호출하려면, 예를 들어 Function(10, 0.5F)와 같이 파라미터 형과 호환되는 식을 인수로 지정해야 한다.

코드1

#include <stdio.h>

void Function(int iValue , float fValue) {
 printf("iValue = %d : fValue = %g\n" , iValue , fValue);
}

int main() {
  Function(10 , 3.14F);
 Function(100 , 1000);
 return 0;
}

코드 1에서 주목하고 원하는 Function(100 , 1000)라는행입니다. 여기서는 float형의 파라미터에 대해서 1000이라는 int형식 인수를 주고 있습니다.통상, 틀에 호환성 없는 인수를 줄 경우 함수는 값을 받을 수 없기 때문에 에러가 되지만, 대입 변환과 마찬가지로 인수에 값 대입 호출이라도 묵적 형 변환이 기능합니다.

코드1에서 주목해야 하는 것은 Function (100, 1000)라는 행이다. 여기에서는 float 형의 파라미터에 1000이라는 int 형의 인수를 전달하고 있다. 일반적으로 형식에 호환되지 않는 인수를 전달하면 함수는 값을 받을 수 없기 때문에 오류가 발생하지만, 대입 변환과 같은 인수에 값이 전달되도 암묵적인 형변환이 된다.

iValue과 fValue 같은 매개 변수는 매개 변수를 선언하고 함수 내에서만 사용할 수 있다. main() 함수에서 iValue과 fValue을 사용할 수 없다.

함수의 결과 반환하기

함수는 호출하는 곳에 함수의 결과를 알리기 위해 값을 반환할 수 있다. 이것을 반환 값이라고 한다. 함수가 주어진 수치를 바탕으로 어떤 계산을 실시하는 경우, 반환 값으로 결과를 반환할 수 있으며, 함수가 어떤 기능을 제공하는 경우, 성공 또는 실패 등의 정보를 반환할 수 있다.

함수는 호출하는 곳에 반환 값을 얻어도 어떤 변수 등에 저장할 수 있으며, 반환 값을 무시할 수도 있다. 반환 값을 무시하면 함수가 반환한 값이 저장되지 않고 파기된다.

코드2

#include <stdio.h>

int Triangle(int iBase , int iHeight) {
 return iBase * iHeight / 2;
}

int main() {
  int iBase , iHeight , iArea;
  printf("삼각형의 밑변과 높이를 입력하십시오. >");
 scanf("%d %d" , &iBase , &iHeight);

 iArea = Triangle(iBase , iHeight);
  printf("삼각형의 면적 = %d\n" , iArea);
  return 0;
}

이 프로그램 Triangle() 함수는 삼각형의 밑변 iBase와 높이 iHeight을 받아서 넓이를 반환하는 기능을 제공한다. 따라서, 어떤 기능이나 계산을 함수로 정리하면 함수를 호출하면 몇 번이라도 그 기능을 다시 사용할 수 있게 될 것이다.

코드2는 iBase과 iHeight라는 식별자의 int 형 변수가 Triangle() 함수와 main() 함수의 두 곳에서 정의되어 있다. 동일한 함수 내에서 식별자가 충돌한 경우 컴파일 오류가 발생하지만, 이 경우는 함수가 다르기 때문에 문제가 없다. Triangle()의 파라미터인 iBase와 main()의 iBase는 이름이 같지만 전혀 다른 변수이다.

C에서 함수의 반환 값은 생략할 수 있다. C++ 언어로 컴파일하면 오류가 발생한다. C 언어에서 형태가 생략된 경우는 기본적으로 int 형으로 해석한다. 따라서 반환 방법을 생략하는 경우는 int 형의 반환 값을 가진 것으로 인식된다. 그러나 반환 값을 생략한 오해를 낳을거 같이 이해하기 어려운 작성은 권장되지 않는다. 함수의 목적을 확실히 하기 위해서라도 반환 값은 명확하게 기술해야 한다.

코드3

#include <stdio.h>

int main() {
  return Function();
}

Function() {
 printf("Function()\n");
  return 0;
}

이 프로그램은 main() 함수에서 Function() 함수를 호출한다. 이 시점에서 함수는 아직 정의되어 있지 않기 때문에, 컴파일러는 기본 함수 int Function()라는 형태로 이 함수를 인식한다. 이후에 Function() 함수가 반환 값을 생략하여 정의되어 있다. 이 함수는 반환 값을 생략하고 있기 때문에, 반환 값은 기본 int로서 인식된다. 기본 함수와 같은 형태이므로 프로그램은 성공적으로 컴파일된다.

다시 말하지만, 현재에는 형의 생략은 권장되지 않는다. 이는 오래된 문법과의 호환성을 위해 남아있는 것이다. 새로 작성한 코드라면 형식은 명시적으로 기술해야 한다.

1.4.3 - C 언어 | 함수 | 함수의 선언

함수 선언은 함수의 본체(정의)가 포함되지 않은 함수 이름과 매개 변수 목록 및 반환 값만을 선언하고 지정된 함수의 존재를 컴파일러에게 알려준다. 사전에 함수를 선언하여 함수의 정의 위치에 관계없이 함수를 호출할 수 있다.

함수 선언자

기본 형식 이외의 함수를 호출하려면 함수를 그 이전에 정의해야 했다. 하나 둘 정도의 함수는 그다시 상관 없을지도 모른다. 그러나 실제 프로그램은 수십, 수백 개의 함수를 처리한다. 예를 들어 Microsoft Windows가 제공하는 함수는 1000개를 넘는다. 내부에서는 더 많은 함수가 정의되어 있는 것이다. 그것들을 모든 main() 함수보다 먼저 정의하고, 또한 함수에서 함수를 호출할 때의 관계를 파악하는 것은 불가능할 것이다.

그래서 함수 선언자라는 것이 있다. 함수 선언자는 함수의 프로토타입 선언이라고도 하고, 반환 함수 이름, 매개 변수 목록만을 제공한다. 함수 선언자는 C 언어가 발안된 당시에는 존재하지 않았다가 1989년에 표준화가 되었을 때에 추가된 사양이다.

함수 선언자는 함수의 정의보다 앞에 선언하는 것으로, 컴파일러에 함수의 이름과 매개 변수 형식 및 반환 형식을 알려준다. 이 때, 본체는 정의하지 않는다.

함수 선언자

반환형식 함수이름(매개 변수 목록);

이러한 함수 선언을 프로그램의 시작 부분으로 구성하므로써, 함수의 정의 위치를 신경 쓸 필요가 없다. 함수를 정의할 때는 물론 함수의 선언에 근거한 형태로 정의되어야 한다. 선언과 다른 형태로 정의하면, 형태의 불일치로 인해 오류가 발생한다.

코드1

#include <stdio.h>

void CharLoop(char chMark , int iNum);

int main() {
  CharLoop('*' , 30);
 printf("\n---\n");
  CharLoop('*' , 40);
 printf("\n");
  return 0;
}

void CharLoop(char chMark , int iNum) {
 int iCount;
 for(iCount = 0 ; iCount < iNum ; iCount++) {
    printf("%c" , chMark);
  }
}

코드1은 문자 chMark을 iNum번만 반복 처리하여 표시하는 함수 CharLoop() 함수를 정의하고 있다. CharLoop() 함수는 main() 함수보다 뒤에 정의되어 있지만, main() 함수보다 앞에 CharLoop() 함수를 선언한다. 따라서 main() 함수에서 CharLoop()를 호출하고 있지만, 컴파일러는 이 시점에서 CharLoop() 함수의 매개 변수와 반환 형식을 인식하고 있기 때문에 문제는 없다.

이 함수 선언자는 비교적 새로운 사양이다. 사실 사양이 제정되기 이전에는 반환 값의 데이터 형을 컴파일러에게 알리기 위해 함수 선언자가 사용되었다. 이전 함수 선언자는 매개 변수 목록에 형식 정보를 포함하지 않고, 오직 식별자(변수명)를 기술하였다. 그러나, 이런 식으로는 컴파일러가 함수에 전달되는 값의 형식을 확인할 수 없기 때문에 요즘에는 권장되지 않는다.

void Function (value1, value2)

예를 들어, 위의 코드는 이전 함수 선언자이다. 보면 알 수 있듯이, 이 함수 선언의 매개 변수 형식 정보가 포함되어 있지 않는다.

이전 함수 선언자는 과거의 언어와의 호환성을 고려하여 일반적으로 현재의 컴파일러에도 구현되어 있다. 그러나 새로 프로그램 코드를 작성하는 경우는 항상 최신 사양을 유의하여 작성하여야 하며, 오늘날 오래된 함수 선언자를 이용하는 것은 넌센스이다. 모든 함수는 새로운 함수 선언자를 이용하여 함수를 선언해야 한다.

그러나 오래된 함수 선언자의 존재를 모르는 경우, 뜻밖의 곳에서 문제가 발생한다. 예를 들어 다음과 같은 프로그램을 작성했다고 하자.

void Function ();

이 경우는 식별자를 생략한 형의 형태 정보를 포함하지 않는 이전 함수 선언자되어 버린다. 형태 정보를 명시 한다면, 그것은 새로운 함수 선언자로 인식되지만, 형태를 생략하는 기술은 이전 함수 선언자의 사양이다.

따라서 새로운 사양에 따른 함수 선언자를 신경써서 작성을 한다고 해도, 이전의 함수 선언자를 작성하는 경우가 자주 발생한다. 이는 컴파일이 되기 때문에 의외로 알아채기 힘든 실수이다.

매개 변수가 없는 함수를 선언에는 형 정보로 void를 지정한다.

void Function (void);

이것은 새로운 함수 선언자로 인식된다.

코드2

#include <stdio.h>

void Function(void);

int main() {
  Function();
 return 0;
}

void Function(void) {
 printf("Kitty on your lap\n");
}

이 프로그램은 새로운 함수 선언자를 사용하여 매개 변수가 없는 함수 선언하고 있다.

식별자(변수명)의 생략

코드1의 함수 선언자 void CharLoop(char chMark, int iNum);을 보면, 매개 변수는 형식뿐만 아니라 변수 이름도 지정하고 있다. 그러나 선언은 본체를 포함하지 않기 때문에 식별자(변수명)은 의미가 없다.

그래서 함수 선언자에는 데이터형만을 지정하고, 식별자는 생략할 수 있다. 원래 새로운 함수 선언자의 목적은 컴파일러에 자료형을 전달하여 함수 호출시 오류 검출 및 인수의 대응 강화에 있다. 식별자 작성은 이전 함수 선언자에서 이어 받은 것이며, 정의가 없는 상태에서는 큰 의미가 없다.

void Function (int, char, double);

예를 들어, 이 경우는 순서대로 int형, char형, double형의 인수를 받는 것을 나타낸다. 인수로 받는 매개 변수의 식별자는 함수의 정의로 지정하면 되는 것이다. 덧붙여서 식별자를 생략한 매개 변수와 식별자를 명확히 작성한 매개 변수를 동일한 함수 선언자에 포함할 수 있다.

void Function (int iValue char, double);

이 함수 선언자는 첫번째 매개 변수 iValue만 식별자를 포함하여 선언하고 있지만, 그 이외의 파라미터는 식별자를 생략되어 있다. 이렇게 해도 문제는 없다.

코드3

#include <stdio.h>

void CharLoop(char chMark , int);

int main() {
  CharLoop('*' , 30);
 printf("\n---\n");
  CharLoop('*' , 40);
 printf("\n");
  return 0;
}

void CharLoop(char chMark , int iNum) {
 int iCount;
 for(iCount = 0 ; iCount < iNum ; iCount++) {
    printf("%c" , chMark);
  }
}

코드3은 코드1을 개량하고 식별자를 기입한 경우와 생략한 경우의 조합으로 함수를 선언하고 있다. 함수 선언자의 식별자가 사용되는 것은 아니므로, 식별자를 지정하는 것은 다소 중복이 된다. 그래서 식별자는 생략하고 형식만 지정한다.

1.4.4 - C 언어 | 함수 | 변수의 유효 범위

변수의 유효범위에 대해 설명한다. 변수의 범위는 선언된 함수 내에서만 유효한 자동 변수와 응용 프로그램이 실행되는 동안 항상 유효한 외부 변수에 따라 달라진다.

자동 변수

변수는 사용 가능한 범위 즉, 수명과 같은 것이 존재한다. 변수의 수명을 결정하는 것은 기억 클래스(Storage Class)라는 변수의 특성이다. 복합문의 내부에서 선언된 변수는 프로그램이 복합 문장에서 벗어날 때에 자동으로 해제되는 구조로 되어 있다. 이러한 변수를 자동 변수라고 한다 (또는, 그 국소성에서 로컬 변수라고도 한다).

변수의 기억 클래스는 변수의 가시성이 생긴다. 변수의 가시성은 그 위치에서 변수에 액세스할 수 있는지하는 것이다. 예를 들어, 다음과 같은 프로그램은 오류가 발생한다. 이유는 main() 함수에서 iValue가 보이지 않기 때문이다.

void Function() {
 int iValue;
}
int main() {
  iValue = 10;
  ...

Function() 함수에서 분명히 iValue 변수를 선언하고 있지만, 이것은 자동 변수이므로 main() 함수에서 액세스할 수 없다. 자동 변수은 그것이 선언된 블록이 실행될 때 만들어고, 블록을 벗어날 때에 삭제된다. 즉, 변수가 선언된 블록 밖에서는 그 변수는 사용할 수 없다는 것이다.

반대로 생각하면 다른 블록의 변수는 보이지 않기 때문에 다음과 같이 여러 블록에서 변수 이름이 중복되어도 프로그램은 문제 없다.

void Function() {
  int iValue;
}
int main() {
  int iValue;
 ...

식별자는 항상 고유해야 하며 같은 이름이 있으면 오류가 발생한다. 위의 코드는 iValue 변수의 이름이 충돌될 거처럼 느껴지지만, Function() 함수 iValue는 main() 함수에서 보이지 않고, 마찬가지로 main() 함수의 iValue는 Function() 함수에서 보이지 않기 때문에 문제가 없다. 이러한 자동 변수는 함수 블록 내에서만 유효하기 때문이다.

여기에서 중요한 것은 변수의 수명은 함수 단위가 아닌 복합 문장의 블록이라는 것이다. C 언어 초보자중에는 “함수 내의 변수는 선언된 함수 내에서만 유효하다"고 기억하는 사람도 있지만, 그렇지 않고 “자동 변수가 선언된 블록 중간에서만 유효하다"고 표현하는 것이 옳다고 말할 수 있다. 이는 다음의 프로그램에서 이해할 수있을 것이다.

코드1

#include <stdio.h>

int main() {
 {
   int iCount;
   for(iCount = 0 ; iCount < 10 ; iCount++)
      printf("1st for : iCount = %d\n" , iCount);
  }
 {
   int iCount;
   for(iCount = 0 ; iCount < 10 ; iCount++)
      printf("2st for : iCount = %d\n" , iCount);
  }
 /*printf("iCount = %d" , iCount); /*에러*/
  return 0;
}

이 프로그램은 main() 함수에서 두 개의 복합 문에 의한 블록을 형성하고 있다. 중요한 것은 두 블록 내부에서 같은 이름의 변수 iCount을 선언하고 있다. 그러나 이는 오류가 발생하지 않는다. 블록 안에서 선언 된 변수는 블록 밖에서는 보이지 않기 때문이다.

마지막에 printf() 함수의 주석을 풀어서 컴파일하면 오류가 발생한다. 이 점에서도 블록 밖에서는 블록의 내부 변수에 액세스할 수 없다는 것을 확인할 수 있다.

외부 변수

변수는 반드시 복합문 내부에 선언해야 한다는 것은 아니다. 함수 외부에서 변수를 선언할 수 있다. 이와 같은 위치에서 선언을 ‘외부적’이라고 표현한다. 예를 들어, 함수 자신은 항상 외부적이다. 함수는 어떤 블록 내부에 배치되는 것이 아니라 어디서든 호출할 수 있는 광역적인 존재이기 때문이다.

함수 밖에서 선언된 변수를 외부 변수라고 한다(또는, 그 광역성에서 전역 및 글로벌 변수라고 한다). 내부 변수와의 가장 큰 차이점은 모든 함수에서 액세스할 수 있으며, 수명이 영구적이라는 점이다. 외부 변수는 프로그램이 종료될 때까지 유효하다. 외부 변수는 프로그램 전체에서 공유하고 싶은 정보를 저장하는데 적합하다.

이와 같이, 변수는 선언 위치에서 기억 클래스와 가시성에 영향을 준다. 함수의 외부에 있는 선언을 외부 레벨이라고 하고, 함수내의 선언을 내부 레벨이라고 표현한다. 이러한 차이에서 변수의 가시성 및 수명이 다르기 때문에 변수의 역할에 따라 구분하고 구분한다.

코드2

#include <stdio.h>

int iValue = 10;
void Function(void);

int main() {
 Function();
 iValue = 100;
 Function();
 return 0;
}

void Function() {
 printf("iValue = %d\n" , iValue);
}

코드2는 외부 변수 iValue를 선언하고 있다. 이 변수는 main() 함수에서도 Function() 함수에서 액세스 할 수 있다. 프로그램을 실행하면 main() 함수에서 iValue의 값을 변경하고, Function() 함수에서 iValue를 표시하고 있다. 이것으로 같은 변수를 조작하고 있는 것을 확인할 수 있다.

다만, 외부 변수는 어떤 함수에서도 자유롭게 값을 변경할 수 있기 때문에 대규모 프로그램이 되면 어디에서 어떤 함수가 어떤 값을 대입하는지 알수가 없게 되거나 버그와 보기 어려운 코드를 만들어 버리는 원인이 될 수 있다. 일반적으로 인수 또는 반환 값을 사용하여 함수간에 정보를 전달하여야 하며, 특별한 사유가 없다면 외부 변수는 최대한 피해야 한다.

외부 변수와 자동 변수의 식별자가 충돌하면, 로컬 변수가 우선시 된다. 이 경우는 컴파일 오류가 발생하지 않는다.

코드3

#include <stdio.h>

int iValue = 1;

int main() {
 int iValue = 10;
  {
   int iValue = 100;
   printf("iValue = %d\n" , iValue);
  }
 printf("iValue = %d\n" , iValue);
  return 0;
}

이 프로그램은 외부 변수의 iValue 변수와 main() 함수 내에 iValue 변수, 그리고 블록의 iValue 변수 이렇게 3가지 변수가 선언되어 있다. 이들은 동일한 이름을 가지고 있지만, 선언 위치가 다르기 때문에, 기억 클래스가 다르다. 실행 결과에서 printf() 함수의 인수로 지정하고 있는 iValue 변수는 보다 가까운 로컬(내부) 변수가 우선되는 것을 확인할 수 있다.

1.4.5 - C 언어 | 함수 | 재귀 처리

자기 자신을 호출하는 함수에 의한 재귀 처리에 대해 설명한다.

자신을 호출하는 함수

어떤 함수에서 다른 함수를 호출하는 방법은 지금까지 설명하였다. 그런데, 함수 내에서 자기 자신을 호출하면 어떻게 될까. 이런 함수 내에서 자신을 호출하는 것을 재귀라고 한다. 예를 들어, 다음과 같은 함수를 생각해 보자.

void Function() {
 Function();
}

Function() 함수는 자기자신을 함수 내에서 호출하고 있다. 이 경우는 영원히 자신을 호출하기 때문에 무한 루프에 빠지게 된다.

재귀를 사용하여 특수한 루프를 만들 수 있다. 그러나 대부분의 것은 재귀를 사용하지 않아도 반복 문장에서 가능하다. 따라서 재귀를 사용하는 것은 극히 드문 경우이다. 왜냐하면 함수 호출은 반복 문장으로 만든 반복보다 느리기 때문이다. 재귀는 일부 알고리즘을 단순화하는데 사용될 수 있다.

코드1

#include <stdio.h>

void Function(int , int);

int main() {
 Function(0 , 10);
 return 0;
}

void Function(int iCount , int iMax) {
  if (iCount < iMax) {
    printf("Count = %d\n" , iCount);
   Function(iCount + 1 , iMax);
  }
}

코드1은 재귀 처리의 이용법을 이해하는 간단한 프로그램이다. 재귀 처리를 이용한 함수 Function()는 첫번째 인수에 카운터의 초기 값을 지정하고, 두번째 인수에 최대 값을 지정한다. Function() 함수는 iCount가 iMax이하이면, Function(iCount + 1, iMax)과 같이 카운터를 증가시키고 자신을 호출한다.

이것을 반복하는 것으로 iCount는 언젠가 iMax에 도달하고, Function() 함수는 차례 차례로 제어를 반환하여 최종적으로 호출한 곳까지 복귀하는 구조로 되어 있다. 이 밖에도 재귀를 이용한 기술로는 함수 A() 함수가 B()를 호출하고, 함수 B()가 함수 A()를 호출하는 상호 재귀라는 관계도 생각할 수 있다.

void FunctionA() {
 FunctionB();
}

void FunctionB() {
 FunctionA();
}

재귀를 실제 이용하는 사례는 많지 않지만, 트리 구조와 같은 데이터의 분석 및 검색 등에서 응용된다.

1.5 - C 언어 | 배열

배열(array)은 번호(인덱스)와 번호에 대응하는 데이터들로 이루어진 자료 구조를 나타낸다. 여기서는 배열의 선언 및 사용법에 대해서 설명한다.

1.5.1 - C 언어 | 배열 | 배열

배열을 이용함으로써 동일한 유형의 여러 값을 하나의 식별자 번호로 관리할 수 있다. 배열의 선언과 기본적인 사용법을 설명한다.

값의 열

어떤 정보를 일시적으로 저장하는 수단으로 프로그램은 변수를 사용할 수 있었다. 그러나 예를 들어 10가지의 정보를 저장하기 위해, 변수를 일부러 10개나 선언하는 것은 매우 귀찮은 일이며, 그들 모두를 관리하는 것은 큰 일인 것이다. 만약 수천, 수만의 값을 변수에 저장하고 싶은 경우에 모든 변수를 선언한다는 것은 어리석은 일이다. 따라서 특정 데이터의 집합이나 데이터의 열은 배열 형태로 정리하여 관리, 제어 할 수 있다.

배열은 동일한 자료형의 같은 변수 이름으로 여러 저장 공간을 확보하는 것이다. 이러한 변수 목록의 수는 프로그래머가 자유롭게 지정할 수 있으며, 각각의 배열의 값을 요소라고 한다. 배열을 선언하기 위해서는 배열 선언자를 사용한다. 배열 선언자에서는 변수 이름과 요소의 수를 다음과 같이 지정한다.

배열 선언자

변수명[요소 수]

요소 수는 배열의 크기를 0이상의 정수 정수로 지정한다. 배열 선언자를 사용하여 선언된 변수는 지정된 요소 수대로 값을 저장하는 영역을 지속적으로 확보한다. 기차처럼 여러 차량이 연결되어 있는 상태를 생각하면 좋을 것이다. 다음은 가장 간단한 배열인 숫자 형식의 1차원 배열의 선언이다.

int iArray[2];

요소 수를 지정하는 것을 제외하고 일반 변수 선언과 변함이 없다. 이 선언에서는 int 형의 iArray 배열 변수가 만들어지고, 그 배열은 iArray[0]iArray[1]이라는 영역을 가진다. iArray[1]의 [1]는 요소 번호 지정을 인덱스라고 한다.

C 언어에서는 첨자는 0번부터 시작한다. 선언할 때에 지정한 수는 개수이며, int iArray[2]으로 작성되는 배열은 0번과 1번 이렇게 2개로 되는 것을 잊지 말자. 첨자는 반드시 괄호 []로 묶는다. 정의된 배열은 다음 그림1과 같은 연속적인 저장 공간을 가지고 있다.

그림1 - 데이터의 열

iArray[0] iArray[1] iArray[2] iArray[n]…

요소를 2개를 갖는 int 형 배열 변수는 int 형의 일반 변수 2개와 같은 크기이다. 배열은 첨자를 지정하여 개별 요소에 액세스할 수 있으므로 반복 등에서 일괄적으로 변수를 처리할 수 있다는 큰 장점이 있다. 첨자는 정수 상수뿐만 아니라 숫자 변수를 지정하는 것도 가능하다. 다만, 배열 선언자에 요소 수를 지정하는 경우에는 반드시 정수이어야 한다.

코드1

#include <stdio.h>

int main() {
 int iArray[3];

  iArray[0] = 10;
 iArray[1] = 100;
  iArray[2] = 1000;

 printf("%d : %d : %d\n" , iArray[0] , iArray[1] , iArray[2]);
  return 0;
}

코드1은 3개의 숫자를 저장하는 수단으로 별도의 숫자 변수를 사용하는 것이 아니라, 배열을 사용하고 있다. 이 프로그램의 iArray 변수는 메모리에서 다음과 같은 구조로 되어 있다.

그림2 - 코드1의 배열과 요소의 관계

iArray[0] iArray[1] iArray[2]
iArray 10 100 1000

이와 같이 iArray 변수는 연속된 저장 공간을 가지고 있으며, 각 영역에 저장되어 있는 요소에 액세스 하려면 인덱스에 의한 번호로 지정하는 것이다. 코드1에서 iArray[0]에 값 10을 iArray[1]에 100을 iArray[2]에 1000을 넣고 있다.

첨자는 변수에 지정할 수 있기 때문에, 배열을 이용하여 대량의 데이터를 일괄적으로 처리할 수 있다. 대부분의 경우, 배열은 반복에 의해 효율적으로 처리된다.

예를 들어 비트맵(bitmap)은 픽셀 단위로 색상 정보를 가지고 있으며, 이러한 정보를 별도의 변수로 나타낼 수 없다. 각 픽셀 정보를 배열로 관리하면 비트맵 전체에 반복 문장을 사용한 일괄 처리를 실현할 수 있다. 데이터베이스 및 음성 데이터에서도 같은 것을 말할 수 있다. 이러한 대량의 정보는 배열로 모와서 처리한다.

코드2

#include <stdio.h>

int main() {
 int iArray[5] , iCount , iAnswer;
 for(iCount = 0 ; iCount < 5 ; iCount++) {
   printf("iArray[%d] : 값을 입력하세요. >" , iCount);
    scanf("%d" , &iArray[iCount]);
  }
 for(iCount = 0 , iAnswer = 0 ; iCount < 5 ; iCount++)
   iAnswer += iArray[iCount];

  printf("iArray의 합계 = %d\n" , iAnswer);
 return 0;
}

이 프로그램은 5개의 요소를 가진 iArray 배열 변수를 정의하고 이를 사용자의 입력으로 초기화하고, iArray 배열의 각 요소를 iAnswer에 가산 대입하여 배열의 총 합계를 구한다. 그리고, iArray 변수의 합계을 표시한다. for 문을 사용하여 배열을 처리할 때에 iArray[iCount]와 같이 변수에 첨자를 지정하고 있는 것에 주목한다.

1.5.2 - C 언어 | 배열 | 문자열

변수로 문자열을 저장하는 방법을 설명한다. 문자열은 문자의 배열이며 char 형의 배열로 관리할 수 있다.

문자열을 변수에 저장하기

지금까지 변수에 저장된 정보는 문자, 정수, 부동 소수점 중 하나였다. 그러나 문자열을 저장하는 방법은 아직 설명하지 않았다. 사실 C 언어에는 문자열을 나타내는 전용 형태라는 것이 없다.

그럼 문자열을 변수로 다루고 싶은 경우는 어떻게 해야 할까? 문자열은 연속된 문자 상수라고 생각할 수 있다. 즉, 복수 개의 연속된 ASCII 코드 값이다. “연속적인 값"이라는 것을 제어하기 위한 가장 좋은 방법은 이미 설명하였다. “배열"에서 설명한 배열 이야말로 문자열을 표현하는 수단이다.

문자열 char 형의 배열로 저장할 수 있다. 그리고 표시할 때 한 글자씩 순서대로 표시하는 방법을 생각할 수 있을 것이다.

코드1

#include <stdio.h>

int main() {
  char chStr[6];
  int iCount;

 chStr[0] = 'K';
 chStr[1] = 'i';
 chStr[2] = 't';
 chStr[3] = 't';
 chStr[4] = 'y';
 chStr[5] = '\n';

 for(iCount = 0 ; iCount < 6 ; iCount++)
   printf("%c" , chStr[iCount]);
 return 0;
}

코드1에서 char 형의 배열 chStr의 각 요소에 1문자씩 문자 상수를 대입하고 있다. 마지막으로 printf() 함수를 사용하여 대입한 문자를 역시 한 글자 씩 표시하고 있다. 물론 문자열은 표시되지만, 효율적인 방법이라고는 할 수 없다.

먼저 문자를 표시하려면 문자의 수만큼 반복 처리할 필요가 있지만, 반드시 문자 수를 관리할 수 있는 것은 아니다. 그래서 문자열의 마지막은 항상 0으로 표시하도록 한다. 이러한 규칙을 있으면 문자수를 관리할 필요가 없어진다.

코드2

#include <stdio.h>

int main() {
 char chStr[7];
  int iCount;

 chStr[0] = 'K';
 chStr[1] = 'i';
 chStr[2] = 't';
 chStr[3] = 't';
 chStr[4] = 'y';
 chStr[5] = '\n';
 chStr[6] = 0;

 for(iCount = 0 ; chStr[iCount] ; iCount++) 
   printf("%c" , chStr[iCount]);
 return 0;
}

코드2는 코드1을 개량하여 배열 변수의 끝은 0이라고 정하고, 그 규칙에 적응한 for 루프를 작성하고 있다.

printf() 함수 같은 C 언어의 표준 함수는 바로 이와 같이 문자열의 끝은 0으로 끝나는 규칙을 정하고 있다. 따라서 “Kitty"라는 문자열 리터럴은 문자 배열로 생각하면 그 끝은 y 대신 0으로 끝나는 6 개의 요소로 구성된 배열로 간주한다. 문자열의 끝을 나타내는 이 0을 NULL 문자라고 한다.

printf() 함수의 서식 제어 문자에 %s를 지정하면 문자 배열을 표시하는 것을 나타낸다. printf() 함수는 전달된 문자 배열을 조사해 NULL 문자가 나타날 때까지 배열을 순서대로 표시한다. 즉, 코드2와 같은 것을 해준다. 그리고 printf() 함수의 첫번째 인수로 지정하여 서식 제어 문자열도 또한 NULL로 끝나는 문자열로 취급되고 있는 것이다.

코드3

#include <stdio.h>

int main() {
  char chStr[7];
  int iCount;

 chStr[0] = 'K';
 chStr[1] = 'i';
 chStr[2] = 't';
 chStr[3] = 't';
 chStr[4] = 'y';
 chStr[5] = '\n';
 chStr[6] = 0;

 printf("%s : %s" , "Kitty" , chStr);
  return 0;
}

이 프로그램 printf() 함수에 주목한다. printf() 함수는 “Kitty"라는 문자열 리터럴과 chStr 배열 변수를 인수로 전달한다. 문자열 리터럴은 암묵적으로 그 끝이 NULL 문자이다. 그리고 chStr 명시적으로 chStr[6]에 0을 대입하고 있다. 이들은 NULL 문자로 끝나는 문자 배열로 printf() 함수에 의해 화면에 표시된다.

그런데 printf() 함수에 chStr 배열 변수를 전달할 때에 인덱스를 사용하지 않고 변수 이름만을 지정하고 있다. 배열은 첨자를 지정하지 않고 변수명만을 지정했을 경우, 그 배열의 시작을 나타낸다. 다만, 보다 정확하게는 배열의 첫번째 메모리 주소를 나타내는 것이라도 할 수 있다. 이것에 대해서는 포인터를 설명할 때 자세히 설명한다. 배열과 포인터는 밀접한 관계를 가지고 있기 때문에 배열을 충분히 이해하기 위해서는 포인터의 이해가 필요하다.

1.5.3 - C 언어 | 배열 | 다차원 배열

테이블와 같은 행과 열로 이루어진 2차원 배열을 예로 여러 차원으로 구성된 다차원 배열을 선언하는 방법을 소개한다.

다차원 정보

정보는 물리적인 기억 장치에는 일렬로 배치되어 있지만, 논리적(개념적)으로 항상 일렬이라고 할 수 없다. 예를 들어, 테이블와 같은 데이터를 관리하는 것을 생각해 보자. 정보는 일렬이 아니다. 테이블는 행과 열을 가지고 있다. 즉, 가로와 세로라는 2차원 정보가 배치되어 있는 것이다.

이 경우에도 일반 배열로 관리할 수 없다. 이는 2차원 배열을 생성하여, 보다 직관적으로 테이블에 액세스할 수 있을 것이다. 배열의 요소가 배열인 배열을 다차원 배열이라고 한다. 다차원 배열은 다음과 같이 선언한다.

다차원 배열의 선언

형태 변수명[1차원 요소 수][2차원 요소 수] ...

이와 같이, 다차원 배열의 선언에 []를 차수만 기술하고, 각 차원의 요소 수(크기)를 지정한다. 다차원 배열 변수에 액세스하는 방법은 1차원 배열과 마찬가지로, 각 차원의 인덱스를 지정하면 된다. 다차원 배열 인덱스 수식은 가장 왼쪽에서 오른쪽으로 접근된다.

실제 세계에서 다차원 배열형의 정보가 많이 있다. 버스나 기차의 좌석 관리와 오셀로 게임과 장기 등의 테이블 게임의 비교적 큰 정보는 이처럼 어떤 식으로든 분류되기 때문에 논리적으로 분할된 정보를 처리하려면 다차원 배열를 사용하면 편리하다.

코드1

#include <stdio.h>

int main() {
 int iArray[2][2];

 iArray[0][0] = 10;
  iArray[0][1] = 100;
 iArray[1][0] = 1000;
  iArray[1][1] = 10000;

 printf("0,0 = %d : 0,1 = %d\n" , iArray[0][0] , iArray[0][1]);
 printf("1,0 = %d : 1,1 = %d\n" , iArray[1][0] , iArray[1][1]);

 return 0;
}

이 프로그램은 각 차원이 두 가지 요소를 가진 2차원 배열을 정의하고 있다. 이 배열은 2 × 2 개의 요소, 즉 4개의 int 형 변수를 저장하는 영역을 메모리에 할당한다. 코드1을 보면 알 수 있듯이 확실히 iArray 배열 변수는 4개의 숫자를 저장하고있는 것을 확인할 수 있다. 이 프로그램의 iArray 배열 변수는 논리적으로 다음과 같은 구조로 되어 있다.

표1 - iArray 배열의 내용

[][0] [][1]
[0][] 10 100
[1][] 1000 10000

다차원 배열은 그렇게 자주 사용되는 것은 아니다. 대부분의 정보 처리는 1차원 배열로 제공할 수 있다. 그러나 3차원 그래픽 등의 분야에서는 4차원 배열이 사용되는 경우도 있다. 차원 수가 많아지면 당연히 사용하는 메모리의 용량도 늘어나기 때문에 분별적인 범위에서 메모리를 사용하도록 한다.

1.5.4 - C 언어 | 배열 | 배열 초기화

정수형 등의 변수와 마찬가지로 배열도 선언과 동시에 개별 요소를 초기화할 수 있다.

배열 변수의 초기화

일반 변수의 선언은 동시에 변수의 값을 초기화할 수 있었다. 배열도 이와 같이 선언시에 초기화할 수 있다. 배열처럼 간단한 형태(int 나 char 등)가 아닌 단순한 형태를 집합시킨 것 같은 기억 영역을 합성체라고 한다. 합성체를 초기화하려면 대괄호{}에서 초기 값의 목록을 지정한다. 이것은 다음과 같은 구문이다.

배열 초기화

형태 변명명[요소 수] = {제1요소 값, 제2요소 값, 제3요소 값, ...};

목록에서 지정하는 각 요소의 초기 값의 수는 배열의 요소 수를 초과해서는 안된다. 반대로, 초기값의 수가 배열의 크기보다도 적은 경우에는 나머지 요소가 0으로 초기화된다. 예를 들어, 다음은 int형 배열을 초기화 정의를 같이하고 있다.

int iArray[4] = { 10 , 100 , 1000 , 10000 };

이 배열은 처음부터 10, 100, 1000, 10000이라는 값으로 초기화된다. 이와 같이, 배열의 각 요소의 값이 미리 결정되어 있다면 이니셜라이저를 사용하면 소스 코드를 간략해 진다.

코드1

#include <stdio.h>

int main() {
  int iArray[4] = { 10 , 100 , 1000 , 10000 } , iCount;
 for(iCount = 0 ; iCount < 4 ; iCount++)
   printf("iArray[%d] = %d\n" , iCount , iArray[iCount]);
 return 0;
}

코드1은 iArray 배열 변수를 선언과 동시에 초기화한다. for 문을 사용하여 이 배열의 값을 표시하고 있기 때문에, 프로그램을 실행하면 배열이 올바르게 초기화되었는지를 확인할 수있는 것이다.

목록의 값이 배열에 초기 값으로 주어지고 있는 것을 알 수 있다. 물론 부동 소수점에서도 초기화 방법은 동일하다.

char 형 배열을 문자열로 초기화하려면, 하나는 연속된 문자 상수로 초기화하는 방법이 있다. 이 때 C 언어의 문자열의 약속인 배열 끝에 NULL 문자의 제로하는 것을 잊어서는 안된다.

코드2

#include <stdio.h>

int main() {
  char chStr[6] = { 'K' , 'i' , 't' , 't' , 'y' , 0 };
  printf("%s\n" , chStr);
  return 0;
}

코드2의 배열 변수 chStr은 초기화 문자열을 저장하고 있다. 초기화 목록에는 문자열의 각 문자를 문자 상수로 지정하고, 그 마지막에는 정수 0을 지정한다. 예상대로, 이 프로그램을 실행하면 Kitty라는 문자가 화면에 표시된다.

char 형의 배열 변수를 문자 배열로 초기화 할 경우 큰 따옴표로 묶어 초기화 할 수 있다. 리터럴 문자열 끝에 암시적으로 NULL 문자가 포함되어 있기 때문에, 그 만큼의 크기를 할당하는 것도 잊지 말자. 한국어와 같은 ASCII가 아닌 문자를 사용하면 문자 코드에 따라 다르지만 일반적으로 1문자가 2 바이트로 표현되므로 주의가 필요하다.

코드3

#include <stdio.h>

int main() {
 char chStr[6] = "Kitty";
  printf("%s\n" , chStr);
  return 0;
}

코드3은 프로그램으로 코드2와 동일하다. chStr 초기화 연속된 문자 상수가 아니라 리터럴 문자열을 사용한다는 점에서 코드2와 다르다. 문자 배열의 char 형 배열 변수를 초기화하려면 이것이 가장 편리한 작성법이다.

다차원 배열의 초기화

다차원 배열의 초기화를 시도할 경우 1차원 배열의 초기화보다 다소 복잡하다. 다차원의 경우는 그 차원 수만 중괄호를 {} 지정하여 각 차원의 목록을 중첩하여 초기화할 수 있다. 예를 들어, 2차원 배열의 경우는 다음과 같이 초기화한다.

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

이 경우, 첫번째 {는 iArray 초기화를 나타내며, 다음에 {는 iArray[0] 초기화 리스트라는 구조로 되어 있다. 즉, { 1, 2 }iArray[0] [0]iArray[0][1]을 초기화한다. 마찬가지로, 다음의 { 3, 4 }iArray[1]을 초기화한다. 배열의 수보다 목록의 수가 적은 경우, 나머지는 0으로 초기화된다.

코드4

#include <stdio.h>

int main() {
 int iCount1 , iCount2;
  int iArray[3][3] = {
    { 2 , 4 } ,
   { 8 , 16 , 32 }
 };

  for(iCount1 = 0 ; iCount1 < 3 ; iCount1++) {
    for(iCount2 = 0 ; iCount2 < 3 ; iCount2++) {
      printf("iArray[%d][%d] = %d\n" ,
       iCount1 , iCount2 , iArray[iCount1][iCount2]);
    }
 }
 return 0;
}

배열의 초기화에 주목한다. { 2, 4 }iArray[0]을 초기화하고 있지만 iArray[0][2]의 초기값이 존재하지 않기 때문에 iArray[0][2]는 0으로 초기화되어 있다. 다음 줄의 { 8, 16, 32 }iArray[1]을 초기화한다. 초기화는 여기서 종료하고 있기 때문에 iArray[2] 초기화 리스트는 존재하지 않는다. 따라서 iArray[2]는 모두 0으로 초기화되어 있다. 이것은 실행 결과를 보면 분명하다.

코드4에서 나타낸 초기화 방법은 {}를 중첩하고 있지만, 다음과 같이 하나의 리스트로 초기화할 수 있다.

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

이 경우, 배열의 처음부터 순서대로 목록의 값으로 초기화된다. 처음에는 iArray[0]에서 초기화된 목록의 처음 두 요소가 사용된다. 즉, iArray[0][0]이 1로, iArray[0][1]가 2로 초기화되는 것이다. 그리고 다음의 요소에서 iArray[1]가 초기화되는 순서이다. 이 경우 역시 배열의 수보다 목록의 값이 적으면 나머지는 0으로 초기화된다.

코드5

#include <stdio.h>

int main() {
  int iCount1 , iCount2;
  int iArray[3][3] = { 
   2 , 4 , 8 , 16 , 32 , 64 , 128 , 256
  };

  for(iCount1 = 0 ; iCount1 < 3 ; iCount1++) {
    for(iCount2 = 0 ; iCount2 < 3 ; iCount2++) {
      printf("iArray[%d][%d] = %d\n" ,
       iCount1 , iCount2 , iArray[iCount1][iCount2]);
    }
 }
 return 0;
}

배열이 목록의 상위부터 순서대로 올바르게 초기화되는 것을 확인할 수 있다. 이니셜라이저으로 지정하는 값은 8개이고, 배열의 요소 수 9보다 적기 때문에 마지막 iArray[2][2]는 0으로 초기화되어 있다. 이러한 다차원 배열의 초기화는 예를 들어 문자열 테이블을 배열로 제공할 때 등에 편리하다.

코드6

#include <stdio.h>

int main() {
 char chStr[3][8] = {
    "Kitty" , "Kitten" , "Feline"
 };
  printf("%s : %s : %s\n" , chStr[0] , chStr[1] , chStr[2]);
 return 0;
}

이 프로그램은 다차원 배열에서 문자열 배열을 생성하고 있다. 문자열은 자체가 문자 배열이므로 문자열의 배열을 만들 수 있기에 자동으로 2차원 배열을 만들어야 하는 것이 된다.

요소 수의 생략

초기화를 지정하는 배열의 경우, 컴파일러는 초기화 목록에서 배열처럼 그 수를 예상 할 수 있다. 따라서 0으로 초기화하는 여분의 요소를 보유하려는 경우를 제외하고 초기화를 지정하는 배열의 크기를 직접 지정하는 것은 중복으로 간주된다. 초기화 식에서 배열의 요소 수를 제한할 수 있는 경우, 배열의 선언에서 크기를 생략할 수 있다. 다만, 그 경우에도 []를 생략할 수 없다.

int iArray[ ] = { ... }

이것은 문자열을 배열 변수에 초기화할 때 등에 유용할 것이다. 요소 수를 생략하면 잘못된 크기를 할당해 버리는 실수를 피할 수 있다.

코드7

#include <stdio.h>

int main() {
  char chStr[] = "Kitty on your lap";
 printf("%s\n" , chStr);
  return 0;
}

코드7는 chStr 배열 변수의 요소 수를 생략한다. 컴파일러는 초기화 “Kitty on your lap"를 배열에 저장하는데 필요한 크기를 계산하여 최적의 요소 수를 지정해 준다.

1.6 - C 언어 | 포인트

포인터(pointer)는 C 언어가 갖는 가장 강력한 힘이자 C 언어를 배우고자 하는 사람들이 넘어야 하는 가장 높은 벽 이기도 하다. 여기서는 기본 개념부터 차근히 설명해 보도록 하겠다.

1.6.1 - C 언어 | 포인터 | 포인터

포인터의 기본 개념과 응용에 대해 코드를 사용하여 설명한다. 포인터는 주소 연산자에서 얻은 주소를 저장하고 간접 연산자를 사용하여 원래의 기억 영역에 액세스한다.

메모리 참조

C 언어 초보자가 좌절하기 쉬운 난관 중 하나가 포인터라고 알려져 있다. 하지만 포인터를 이해하기 위해 수학적인 지식이 필요하지 않으며, 특별히 어려운 이론을 학습해야 하는 일도 없다. 원리만 이해하고 나면 포인터는 어려운 것이 아니다. 중요한 순서대로 조금씩 확실하게 이해 가도록 노력하는 것이다.

그럼, 본문에 들어가겠다. 포인터는 메모리 영역의 위치를 가리키는 변수의 일종이다. 지금까지 우리는 변수를 사용해 왔다. 변수에 문자열을 사용할 수 있게 되었다. 포인터도 변수이다. 어려운 것은 없다.

문제는 포인터에 무엇을 저장하는가 하는 것이다. 보통의 변수는 특정 메모리 영역에 어떤 값을 저장하기 위해 사용하고 왔지만, 포인터는 값의 저장을 위해 사용하는 것은 아니다. 포인터는 메모리 주소를 저장하는 변수이다.

변수는 값을 메모리의 어딘가에 일시적으로 저장하는 것이다. 메모리는 1바이트 단위로 어드레스(주소)가 할당되어 있고, 컴퓨터는 이 주소를 사용하여 메모리에 저장되어 있는 값에 액세스한다. 실제로 순수 기계어에는 변수 따위는 존재하지 않고 값을 저장하고 검색하려면 주소를 사용하고 있다. 주소는 처음부터 순서대로 수치로 표현된다.

그림1 - 메모리 구성

메모리 구성

그림1은 메모리를 그림으로 쉽게 설명하고 있다. 메모리 값을 저장할 수 있다. 문자나 실수도 결국은 값(그림에서는 알기 쉽게 10진수로 표현하고 있지만, 사실은 2진수로 저장되어 있다)으로 기록되어 있는 것이다. 값이 어떤 형태로 저장되거나 컴퓨터 아키텍처에 따라 다르지만 메모리 주소는 바이트 단위로 할당 순서대로 단순 증가하고 있다. 메모리 주소가 몇 바이트로 표시되는 것인가 하는 문제도 컴퓨터에 따라 다르다.

포인터는 메모리에 메모리 주소를 나타내는 값을 저장하여 특정 변수(메모리에 저장되어 있는 값)의 위치 정보를 교환하는 수단으로 사용되는 것이다. “메모리에 메모리 주소를 저장"이라는 개념이 까다롭지만, 이는 곧 나중에 C 언어으로 구현하고 확인해 보자.

그럼 변수가 메모리의 어디에 저장되어 있는지를 실제로 살펴보자. 변수의 주소를 얻으려면 변수 이름 앞에 앰퍼샌드 “&“을 붙인다. 이는 주소 연산자라고 한다.

C언어에서 변수 이름을 수식에 사용하면 그 변수의 내용(저장되어 있는 값)을 반환하지만 주소 연산자를 변수 앞에 지정된 경우 해당 변수의 메모리 주소를 반환한다. 주소 연산자는 scanf("%d”, &iVariable)과 같이 scanf() 함수에서 이미 사용했던 적이 있다. 이 때 & 기호는 변수의 주소를 함수에 전달한다.

주소 연산자 &는 논리 연산자 &와 다르기 때문에 주의하자. 단항 연산자 &가 사용된 경우는 주소 연산자이며, 2항 연산자 &가 이용되면 논리 연산자로 해석된다.

주소의 표현은 처리계에 의존하는 문제이다. 기계어 수준에서 생각하면 주소는 정수형으로 표현되고 있지만, 구현에 의존하는 처리는 피하는 것이 좋다. 주소를 printf() 함수를 사용하여 표시하는 경우 %X와 같은 형식을 사용하여 표시할 수 있지만, 보다 확실하게 표시하려면 %p를 사용하여 처리계에 맞는 표현으로 출력한다.

코드1

#include <stdio.h>

int main() {
  char chVar = 'G';
  int iVar = 10;

  printf("chVar : 내용 = %c, 주소 = %p\n" , chVar , &chVar);
  printf("iVar : 내용 = %d, 주소 = %p\n" , iVar , &iVar);
  return 0;
}

각 변수의 내용은 변수를 초기화할 때 저장한 값이다. 이것은 변수가 가리키는 메모리에 저장되어 있던 정보이다. 이에 대해 주소는 이 변수를 나타내고 있는(즉, 이 값이 저장되어 있는) 메모리 주소이다. printf()의 인수에 주소 연산자 &를 사용하는 것에 주목한다.

표시되는 실제 숫자는 실행하는 컴퓨터와 그 때의 메모리 상황에 따라 달라진다. 메모리 주소는 응용 프로그램을 실행했을 때 시스템이 할당한 때 결정되기 때문에 정적으로 고정할 수 없다. 요즘 대부분의 컴퓨터에서 메모리를 자유롭게 처리할 수 있는 권한이 있는 것은 기본 소프트웨어(운영 체제)뿐이다.

주소 저장

메모리 주소를 얻는 방법은 알았지만, 그것만으로는 아무것도 할 수 없다. 메모리 주소의 수치를 이용하는 것은 컴퓨터이며, 개발자와 이용자에게 메모리 주소는 그다지 의미가 없다.

그래서 포인터가 필요하다. 포인터는 메모리 주소를 저장하는 것을 전문으로하는 특수한 변수이다. 포인터에 주소를 대입하여 포인터 변수는 직접 메모리 주소에 액세스할 수 있게 되는 것이다. 원격에서 변수의 내용을 조작하는 것 같은 이미지이다.

포인터의 선언은 기본적으로 일반 변수와 동일하지만, 변수 이름 앞에 별표 *를 붙입니다.

포인터의 선언

형식 *변수명

이렇게 하면 포인터 변수에 주소를 저장할 수 있다. 예를 들어 int *iPo 선언한 경우 iPo 변수는 int 형 변수에 대한 포인터임을 나타낸다. 메모리 주소는 정수임을 설명했다. 포인터 변수는 주소 저장하기 위한 변수이며 실체로는 정수형인 것을 의미하고 있다.

포인터 변수가 들어있는 주소를 따라 원래의 변수(주소가 가리키는 메모리의 값)을 얻기 위해서는 간접 연산자 *를 사용한다. 이 포인터 변수 앞에 지정하여 포인터 변수에 포함된 주소 값을 검색할 수 있다. 간접 연산자 *는 곱셈 연산자 *는 다르므로 구별해야 한다. 간접 연산자는 단항 연산자이다.

간접 연산자를 사용하여 포인터가 가리키는 메모리의 값을 취득하는 것을 간접 참조라고 한다.

그림2 - 주소에서 간접 참조

주소에서 간접 참조

그림2는 포인터를 저장하는 주소를 사용하여 간접 참조를하는 상황을 나타낸다. 포인터도 변수의 일종이며 메모리의 어딘가에 정수 값으로 주소를 저장한다. 포인터는 이것을 사용하여 원래의 변수에 액세스하는 것이다.

코드2

#include <stdio.h>

int main() {
 int iVar = 100;
 int *iPo = &iVar;

 printf("iPo = %p : &iVar = %p : *iPo = %d\n" , iPo , &iVar , *iPo);
  return 0;
}

iPo 변수는 int 형 변수에 대한 포인터이다. 포인터가 저장하고 있는 메모리 주소이며, iPo 변수의 내용을 printf() 함수로 표시하면 저장된 메모리 주소임을 알 수 있다. 이것은 & iVar와 같은 값인 것을 확인할 수 있다.

포인터가 저장된 주소가 가리키는 메모리의 값을 얻으려면 간접 연산자 *를 사용한다. printf() 함수의 마지막에 *iPo을 지정하여 iPo 변수에 할당된 주소를 사용하여 간접 참조를 하고 있다. 이렇게 함으로써 iPo에 저장되어 있는 주소, 즉 iVar 값을 간접적으로 검색할 수 있다.

간접 연산자를 이용하면 주소를 간접 참조 할뿐만 아니라, 포인터가 나타내는 주소 값을 간접 할당할 수 있다. 즉, 포인터를 통해 변수에 값을 할당한다.

코드3

#include <stdio.h>

int main() {
  int iVar = 0;
 int *iPo = &iVar;

 *iPo = 100;
 printf("iVar = %d\n" , iVar);
  return 0;
}

이 프로그램은 iPo 포인터 변수에 iVar int 형 변수의 주소를 할당한다. 그럼 *iPo = 100;와 같이 포인터를 사용하여 iVar 간접 할당을 하고 있는 것이다. 그 결과 printf() 함수는 iVar 값은 100이 되어있는 것을 확인할 수 있다.

이러한 간접 대입에 무슨 의미가 있는지 이해하기 어렵지도 모른다. 확실히 코드3을 보면 값은 직접 iVar 변수에 할당하면 되는 것만으로 포인터를 사용하는 의의를 느낄수 없다.

포인터는 다른 함수끼리 자동 변수를 공유할 때 위력을 발휘한다. 함수 안에서 선언된 변수는 자동 변수가되고, 변수는 선언된 블록 내부에서만 사용할 수 있다. 함수의 인수로 전달할 수 있는 값의 복사이며, 다른 함수에서 자동 변수를 조작하는 방법은 없다.

그래서 함수의 인수에 포인터를 전달한다. 그러면 다른 함수의 자동 변수를 원격 조작할 수 있다. 포인터를 전달하면 함수는 이를 간접 참조하여 다른 함수의 자동 변수에 값을 대입하는 것이 가능하게 되는 것이다. 따라서 함수 포인터를 전달 간접적으로 정보에 액세스하는 방법을 참조로 전달이라고 단순히 값을 복사하여 전달 방법을 값 전달이라고 한다.

예를 들어, “두 변수의 값을 바꿀 함수를 만드십시오"라는 과제가 주어진 경우 기존의 방법으로는 달성할 수 없다. 함수의 반환 값은 항상 하나의 값 밖에 없으며, 매개 변수의 값을 교체해도 함수의 호출에 아무런 영향도 주지 않는다. 이런 경우에 포인터를 매개 변수로 받는 방법을 생각할 수 있다.

코드4

#include <stdio.h>

void swapInt(int * , int *);

int main() {
  int iVar1 = 100 , iVar2 = 1000;
 swapInt(&iVar1 , &iVar2);

 printf("iVar1 = %d : iVar2 = %d\n" , iVar1 , iVar2);
 return 0;
}

void swapInt(int *iPo1 , int *iPo2) {
 *iPo2 ^= *iPo1 , *iPo1 ^= *iPo2 , *iPo2 ^= *iPo1;
}

코드4의 swapInt() 함수는 두 개의 int 형의 포인터를 받는다. 이 함수는 전달된 포인터에서 간접 참조를 하고 호출자가 지정한 변수의 값을 변경시킬 수 있는 것이다. 프로그램을 실행하면 iVar1 = 1000 : iVar2 = 100이라는 결과를 얻을 수 있을 것이다.

그런데 포인터 변수에 데이터 형이 존재한다. char 형 변수의 주소는 char 형의 포인터를 int 변수의 주소는 int 형 포인터에 할당해야 한다. 하지만 포인터는 메모리 주소를 나타내는 정수를 저장하기 위한 것으로, 포인터 변수가 점유하는 메모리 영역은 항상 같은 크기이다. 포인터는 C 컴파일러가 포인터가 가리키는 변수가 몇 바이트인지를 인식하는데 사용된다. 이제 간접 참조로 할당하고 검색을 갔을 때 몇 바이트 단위로 값을 복사하거나 대입하는 방법이 결정된다. 따라서 특별한 경우를 제외하고 서로 다른 타입의 포인터 변수에 주소를 할당할 수는 없다.

코드5

#include <stdio.h>

int main(){
  int iVar1 = 0xFFF , iVar2;
  unsigned char *chPo = &iVar1;

 iVar2 = *chPo;

  printf("iVar2 = %d\n" , iVar2);
  return 0;
}

이 프로그램은 부호없는 char 형의 포인터 chPo에 int 형 변수 iVar1 주소를 대입한다. 확실히 chPo는 iVar1의 주소를 틀림없이 저장할 수 있지만 iVar2 = *chPo에서 간접 참조했을 때, 컴파일러는 이 포인터 참조는 1바이트(char 타입)으로 판단한다. iVar1에 저장되는 값은 16 진수 0xFFF이지만, char 형태로 간접 참조하면, 이 중 하위 1바이트 밖에 보이지 않기 때문에 iVar2는 16 진수 0xFF 즉 10 진수 255이 대입된다.

1.6.2 - C 언어 | 포인트 | 포인터 연산

포인터에 저장되는 주소는 실질적으로 정수이므로 포인터를 연산하여 참조를 변경할 수 있다. 이를 응용하여 배열과 같은 구조적인 데이터에 대한 포인터에서 모든 요소를 가르키 있습니다.

주소를 계산하기

포인터는 메모리 주소를 저장하는 특수한 변수이다. 포인터도 다른 변수와 동일하게 데이터 형이 존재하고, 변수처럼 취급할 수 있다. 중요한 것은 포인터의 실체는 간단한 정수 값이며,이를 산술 계산을 할 수 있다는 것이다.

하지만 엉터리 숫자를 메모리 주소로 참조해서는 그 정보에 의미는 없다. 변수 등에서 취득한 올바른 주소 이외를 간접 참조하는 것은 프로그램의 충돌로 이어진다.

포인터 연산은 연속된 메모리 영역을 확보하고 있는 주소에 유효한 것이다. 연속된 메모리 영역은 예를 들어 배열이다. 배열과 같은 연속적인 공간을 가진 합성체는 메모리 주소도 연속하고 있기 때문에 그 범위를 예상할 수 있다. 따라서 인덱스를 사용하는 대신 포인터를 연산하여 특정 요소를 볼 수 있게 되는 것이다.

코드1

#include <stdio.h>

int main() {
  char chStr[] = "Kitty on your lap";
 int iCount;

 for(iCount = 0 ; chStr[iCount] ; ++iCount)
    printf("&chStr[%d] = %p\n" , iCount , &chStr[iCount]);
 return 0;
}

코드1은 chStr 배열의 각 요소의 메모리 주소를 순서대로 표시한다. 이 결과에서 흥미로운 것은 배열과 주소의 관계이다. 재미있는 것은 주소가 배열의 처음부터 순서대로 단순 증가하고 있다.

이것은 배열이 연속된 메모리 영역을 확보하고 있음을 나타낸다. 이 특징을 잘 이용하면 포인터를 산술 연산자로 계산하여 간접 참조를 할 수 있다. 위의 예제에서 생각해 보면, chStr[0]의 주소에 다섯을 가산하면 chStr[5]의 주소와 동일하다.

코드2

#include <stdio.h>

int main() {
 int iArray[] = { 10 , 100 , 1000 };
 int *iPo = &iArray[0] + 1;

  printf("*iPo = %d\n" , *iPo);
  return 0;
}

코드2는 배열이 연속된 주소를 가지는 것을 이용하여, iPo 포인터에 iArray 배열 변수의 시작 주소를 할당 할 때, 덧셈 연산자 +를 사용하여 메모리 주소에 1을 가산하고 있다. 그 결과, 본래는 iArray[0]을 나타내는 것이었던 주소가 그 다음에 iArray[1]로 변경된다. printf()에서 iPo 포인터에서 간접 참조 값을 출력하고 있는데, 결과에서 포인터가 iArray [1]을 나타내고 있음을 확인할 수 있다.

포인터 연산에서는 모든 주소가 1바이트 단위로 할당한다는 점에 유의해야 한다. 32비트 컴퓨터에서 int는 4바이트의 메모리 공간을 확보한다. 이 경우 int 형 변수 다음의 주소는 이 변수의 주소에 4를 가산한다. 코드1의 실행 결과가 단순히 1씩 증가되어 있던 것은 chStr 배열 변수가 1바이트의 char 형 이었기 때문이다.

코드3

#include <stdio.h>

int main() {
  int iArray[] = { 2 , 4 };

 printf("&iArray[0] = %p\n" , &iArray[0]);
  printf("&iArray[1] = %p\n" , &iArray[1]);

  return 0;
}

코드3와 코드1의 결과와는 달리, 주소가 4단위로 증가하고 있는 것을 알 수 있다. 이 결과는 int 형이 4바이트의 컴퓨터에서 실행한 것이다. iArray 배열 변수는 int 형이기 때문에 각 요소가 4바이트 단위로 다루어지고 있는 것이다.

다행히도, 개발자는 이 사실을 거의 신경 쓸 필요는 없다. 예를 들어 iArray[0] 포인터 iPointer이 존재하는 것처럼, 이 포인터를 사용하여 iArray[1]의 주소를 계산하려면, iPointer += 4 로 할 필요는 없다. 직관적으로 iPointer += 1으로 한다.

“포인터"절에서도 설명했지만, 포인터 형은 컴파일러가 참조하는 주소를 몇 바이트 단위로 취급해야 하는지 판단하기 위해 이용된다. 컴파일러는 주소의 계산을 수행하면, 포인터의 형태에 따라 올바른 것라고 생각할 수 있는 연산 결과를 산출한다. 32비트 컴퓨터에서 int 형 변수에 대한 포인터에 1을 가산 한 경우 실제 메모리 주소는 4가 가산되는 구조로 되어 있다.

즉, 포인터에 대한 연산이 행해진 경우 암시적으로 정수로 포인터의 크기를 곱한 값이 계산된다. 컴파일러가 이 계산을 해주기 때문에, 개발자는 포인터 형에 따른 번거로운 메모리 주소 계산을 할 필요가 없다.

코드4

#include <stdio.h>

int main() {
 int iArray[] = { 2 , 4 };
 int *iPo = &iArray[0];

  printf("*iPo = %d : iPo = %p\n" , *iPo , iPo);
 iPo += 1;
 printf("*iPo = %d : iPo = %p\n" , *iPo , iPo);

 return 0;
}

주소는 실행했을 때 상황에 따라 다르지만, 그것은 중요하지 않다. 결과를 보면, 프로그램을 실행했을 때 iArray 배열 변수의 선두 요소에 할당된 메모리 주소는 0033FDA4라는 주소 이었다. int 형는 32비트 컴퓨터에서 실행되고 있다고 가정하고 0033FDA4, 0033FDA5,0033FDA6,0033FDA7 4 바이트을 iArray[0]가 점유하고 있다고 생각된다.

프로그램은 iArray 배열 변수의 첫번째 요소에 대한 포인터 iPo가 정의되어 있다. 먼저 iPo의 포인터가 가리키는 값으로 저장하는 메모리 주소를 표시하지만, 이것은 당연히 iArray[0]의 값 2와 주소 0033FDA4가 표시된다. 다음 iPo += 1을 의해 iPo에 정수1을 가산하고 있지만만, 여기에서는 iPo 값이 0033FDA5 대신 0033FDA8이 되는 것이 포인트이다.

이것은 두번째 printf() 함수의 결과에서 확인할 수 있다. iPo의 주소에 1을 더하면 int 형의 크기만큼 메모리 주소가 나아가서, 정확하게 두번째 요소 iArray[1]을 가리킨다.

1.6.3 - C 언어 | 포인트 | 배열과 포인터

배열을 가리키는 포인터를 계산하고, 어떤 요소에 간접 참조하는 방법을 설명한다.

배열에 간접 참조

지금까지 배열의 첫번째 요소의 주소를 얻기 위해 &iArray[0]과 같이 쓰는 방법으로 사용해 왔다. 그러나 사실은 배열 변수라는 것은 특별한 변수 이름만 지정하면 배열의 위로의 주소를 나타내는 것으로 되어 있다. 예를 들어, 지금까지는 배열 변수 iArray의 선두 요소의 메모리 주소를 얻으려면 다음과 같이 기술하였다.

int *iPo = &iArray[0];

이는 틀린 것은 아니지만, 이보다 단순히 배열 이름에서 선두의 주소를 가져올 수 있다. 이는 다음과 같이 쓸 수도 있다.

int *iPo = iArray;

자세히 다루지 않았지만, 이전 printf() 함수에 %s으로 문자열을 표시하는 경우, 배열 이름만 전달했던 이유가 여기에 있다. printf() 함수의 서식 지정 문자 %s에 대응하는 인수는 문자 배열에 대한 포인터해야만 한다. 일반적으로 주소 연산자를 사용하여 주소를 가져오지만, 문자 배열의 경우는 붙어있지 않는다. 왜냐하면 배열 변수의 경우, 인덱스 생략의 변수 이름은 배열의 시작 주소를 나타내기 때문이다.

그러나 배열 변수의 변수 이름은 포인터가 아니므로 착각하지 않도록 주의하자. 인덱스를 생략한 배열 변수의 이름은 선두 요소의 주소를 나타내는 상수와 같은 것이며, 이에 어떤 값을 대입할 수는 없다.

코드1

#include <stdio.h>

int main() {
 char chStr[] = "Kitty on your lap";
 printf("&chStr[0] = %p : chStr = %p\n" , &chStr[0] , chStr);

 return 0;
}

코드1은 &chStr[0]chStr가 동일한 주소를 나타내고 있음을 증명하고 있다. printf() 함수는 &chStr[0]chStr을 인수 받아, 이를 16진수로 화면에 표시하면, 프로그램은 동일한 수치를 화면에 표시한다.

이 특성을 이용하면 인덱스 대신에 간접 연산자 *를 사용하여 배열을 제어할 수 있다. 배열이 메모리 상에 어떻게 배치되어 있는가하는 본질에 관련되어 있기 때문에, 이 방법에 의한 요소에 대한 액세스를 이해하는 것은 매우 중요하다. 다음 문은 배열 변수 iArray 중 iArray[2]에 간접 연산자를 사용하여 참조한다.

*(iArray + 2)

이 때 주의해야 하는 것이, 간접 연산자 *는 덧셈 연산자 +보다 우선 순위가 높기 때문에 괄호를 빼버리면 먼저 *iArray이 연산된다. 따라서 괄호로 iArray + 2를 먼저 계산하도록 작성해야 한다. 이와 같이, 연산과 간접 참조가 동일한 식에 있으면, * 연산자 우선 순위에 주의해야 한다. 특히 증가 연산자와 간접 참조 연산자를 동시에 사용하는 경우 *iPo++로 작성하면 iPo 포인터의 주소를 증가하는 것을 나타내며, (*iPo)++로 작성하면 iPo가 가리키는 내용을 증가하는 것을 나타내는 것이다. 이러한 미묘한 차이에 당황하는 경우가 많기 때문에, 조심해야 한다.

코드2

#include <stdio.h>

int main() {
  int iArray[] = { 2 , 4 , 8 };

 printf("간접참조=%d,%d,%d\n", *iArray , *(iArray + 1) , *(iArray + 2));
  printf("첨자지정=%d,%d,%d\n" , iArray[0] , iArray[1] , iArray[2]);
 return 0;
}

코드2를 실행하면 *(iArray + 1)iArray[1]이나 *(iArray + 2)iArray[2]가 같은 값을 참조하는 것을 확인할 수 있다.

실질적으로 iArray[index]라는 인덱스 연산은 *(iArray + index)와 동일한 조작이다. 따라서 iArray[index]*(iArray + index)는 동일하다고 생각할 수 있다. 마찬가지로, &iArray[index] 문장은 iArray + index와 동일하다. 이것은 포인터와 배열의 관계에서 가장 중요한 관계이기 때문에 충분히 이해해야 한다.

이 특성을 반대로 생각하면, 인덱스를 지정하고 배열 변수의 요소에 액세스하는 것은 간접 참조이라는 것이다. 결국은 배열의 주소를 저장하는 포인터 iPo에서도 iPo[index]와 같이 인덱스 지정에 간접 참조를 할 수 있다.

다차원 배열과 포인터

저장 공간 및 주소의 관계는 1차원 배열과 같은 것이다. 다차원 배열은 프로그래밍 언어가 개념적 계층을 위한 것이며, 물리적으로 (기계어 수준) 다원화되는 것은 아니다. 2개의 요소를 가진 2차원 배열 Array [2] [2]는 4개의 요소를 가진 배열 Array[4]와 요소의 수를 동일하기 때문에 할당된 메모리 크기는 같다.

코드3

#include <stdio.h>

int main() {
 char chArray[2][2] = { 2 , 4 , 8 , 16 };

  printf("chArray = %p\n" , chArray);
  printf("&chArray[0][0] = %p\n" , &chArray[0][0]);
  printf("&chArray[0][1] = %p\n" , &chArray[0][1]);
  printf("&chArray[1][0] = %p\n" , &chArray[1][0]);
  printf("&chArray[1][1] = %p\n" , &chArray[1][1]);

  return 0;
}

코드3은 다차원 배열의 주소 구조를 이해하기 위한 것이다. 먼저 첨자를 생략한 배열 이름이 항상 배열의 시작 주소를 나타내는 것은 동일하다. 2차원 배열에서 배열의 선두는 [0] [0] 요소이다. chArray 돌려주는 값이 &chArray[0] [0]가 반환된 값과 동일하다는 것을 확인할 수 있다.

주목해야 하는 것은 각 요소가 반환 메모리 주소이다. 2차원 배열이라고 해도, 할당된 메모리는 일렬로 되어 있다는 것을 이 결과에서 확인할 수 있다. 다차원 배열도 보통의 1차원 배열처럼 연속된 메모리 영역에 할당된다.

이 특성을 알면, 다차원 배열에서도 목적의 차원과 요소에 포인터에서 액세스할 수 있다. 예를 들어 2차원 배열에 있어서 다음과 같은 계산식을 이용하는 방법이 있다.

*(iPointer + (일차원 첨자 * 이차원 요소 수) + 이차원 첨자)

반복 문장의 카운터를 이용하여 다차원 배열을 처리하는 일이 요구된 경우는 이러한 방법으로 간접 참조할 수 있을 것이다. 그러나 다차원 배열의 첨자를 생략한 변수 이름에서 간접 참조를 시도하면 뜻밖의 결과를 얻을 수 있다. 다음 문장은 아마 예상한 것과 다른 결과가 될 것이다.

int iArray[2][2] = { 1 , 2 , 3 , 4 };
printf("%d" , *iArray);

이 경우 iArray는 배열의 선두 iArray[0] [0]의 주소를 보여주기 위해, *iArray는 1이라는 결과를 간접 참조해 줄 것을 기대하고 있지만, 전혀 다른 값을 표시한다. 첨자를 생략한 변수 이름 iArray을 printf()함수에서 예시로 표시시켜 보면, 확실히 &iArray[0][0]에 동일한 비록 간접으로 얻을 수 있는 값은 iArray[0][0]에 저장되는 값이 아니다. 이것은 매우 이상한 현상이다.

이전, iArray[index] 문은 *(iArray + index)와 동일하다고 설명했지만 ,이를 역으로 생각하면 다차원 배열의 *iArrayiArray[0]에 동일한 것이다. 이것은 차원을 지정하고 있지만 요소를 지정하지 않는다. 2차원 배열 변수에서 iArray[0]을 지정한 경우는 iArray[0][0]의 주소를 돌려준다. 따라서, 위에 예문은 기대한 결과 1 대신, &iArray[0][0]을 표시하게 되는 것이다.

이것을 해결하는 하나의 방법으로는 iArray 2차원 배열 변수를 (int *)로 캐스팅해 버리는 것이다. 그러면 차원을 신경 쓸 필요없이 직접 int 변수에 대한 간접 참조로 각 요소에 액세스할 수 있다.

코드4

#include <stdio.h>

int main() {
  int iCount1 , iCount2;
  int iArray[][9] = {
   { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 } ,
   { 2 , 4 , 6 , 8 , 10 , 12 , 14 , 16 , 18 } ,
    { 0 } ,
 };
  int *iPo = (int *)iArray;

 for(iCount1 = 0 ; *(iPo + (iCount1 * 9)) ; iCount1++) {
   for(iCount2 = 0 ; iCount2 < 9 ; iCount2++) {
      printf("%d " , *(iPo + (iCount1 * 9) + iCount2));
   }
   printf("\n");
  }

 return 0;
}

코드4는 for 문을 사용하여 2차원 배열을 처리하지만, 배열 액세스는 인덱스가 아닌, 포인터의 연산에 의한 간접 참조로 실현하고 있다. iArray을 직접 연산한 경우는 간접 참조를 하여도 요소 주소가 반환되기 때문에, 한번 int 형 포인터 iPo에 캐스팅하고 이를 조작하고 있다. 형식 변환은 명시적으로 할 필요는 없지만 컴파일러에서 잘못된 포인터 조작임을 경고될 가능성이 있기 때문에 (int *)iArray와 같이 명시적으로 변환한다.

이 프로그램은 iArray의 각 요소를 화면에 표시하고 첫 번째 요소가 0 인 경우에 처리를 종료한다.

함수와 포인터

표준적인 설계는 배열이나 구조체 등의 대용량 데이터를 함수에 전달하는 방법으로 포인터를 이용한다. 포인터를 이용하지 않는 이유는 값을 반환하지 입력 목적의 단순 형식을 사용하는 경우에만, 예를 들면 char, int, double 등 형태의 입력할 경우이다.

문자열과 데이터 배열을 받거나 호출자에게 돌려주는 경우, 함수는 포인터를 받아야한다 생각되고 있다. 포인터가 아닌 일반 배열로 전달할 수도 있는데, 이 경우 크기가 명확하게 결정되어 있어야 해서 불편하다. 포인터 이외의 방법으로 합성체를 전달하려면 함수를 호출 할 때마다 배열 등을 통째로 복사해야 되고, 이것은 실행 속도와 메모리에 심각한 부담을 주게 된다. 이 방법에 장점이 없기 때문에, 상급자는 망설임없이 포인터 정보를 교환해야 한다고 판단하는 것이다.

코드5

#include <stdio.h>

void stringToLower(char *chpStr) { 
 int iCount;
 for(iCount = 0 ; *(chpStr + iCount) ; iCount++) {
   if (*(chpStr + iCount) > 0x40 && *(chpStr + iCount) < 0x5B)
   *(chpStr + iCount) += 0x20;
 }
}

int main() {
  char chStr[] = "~KITTY on YOUR lap~";
 stringToLower(chStr);

 printf("%s\n" , chStr);
  return 0;
}

이 프로그램의 함수 tolower()에는 인수 문자열에 대한 포인터를 지정한다. tolower() 함수는 주어진 포인터로부터 문자에 간접 참조하고, 그 문자가 대문자인지 여부를 ASCII 코드의 성질을 이용하여 조사하고 있다. ASCII 코드의 대문자에 16진수 0x20를 추가하면 그대로 소문자가 되기 때문에, 이를 이용하여 문자열 내에 대문자가 존재하는 경우에 이를 소문자로 변환한다. 즉,이 함수의 역할은 주어진 문자 배열의 문자로 변환할 수 있는 모든 문자를 소문자로 하는 것이다.

덧붙여서, tolower 함수 정의의 매개 변수에

char *chpStr

라는 선언은 다음과 같이 설명할 수 있다.

char chpStr[]

이 두개는 함수의 매개 변수에만 동일하다고 해석되지만, 전자 char *chpStr이라는 기법이 인수가 포인터임을 명확하게 보여주고 있기 때문에 권장된다.

1.6.4 - C 언어 | 포인트 | 문자열 포인터

문자 배열과 문자열에 대한 포인터에 대해 설명한다. 특성상 리터럴 문자열에 대한 포인터에서 리터럴 문자열을 변경해서는 안된다. 리터럴 문자열에서 읽어 들인 문자열을 편집할 경우는 배열에 복사해야 한다.

리터럴 문자열에 대한 포인터

리터럴 문자열을 배열 초기화를 하면 배열에 문자열을 저장할 수 있었다. 그런데 어떤 의문이 생긴다. 초기화 이외의 식에서 리터럴 문자열을 지정한 경우, 이 문자열 리터럴은 어떤 형태로 사용되는가. 사실, 문자열 리터럴은 그 문자열에 대한 포인터를 반환한다.

예를 들어 printf("Kitty on your lap")라는 문장은 printf() 함수에 char *형의 값을 인수로 전달하고 있다고 생각할 수 있다. 그럼 이 문자열은 어디에 저장되어 있는가.

리터럴 문자열이나 숫자 등의 상수는 모두 컴파일시 고정 값으로 바이너리화되어 결국 실행 파일에 데이터가 저장된다. 프로그램 실행시 데이터가 상수 값으로 메모리에 로드할 수 있도록 되어 있다. 구체적으로 상수를 어떻게 저장하고 실행시에 어떻게 가져올지 컴파일러와 시스템에 의존한다.

이 점을 감안하면 문자열 리터럴은 프로그램을 실행하고 있을 때에 이미 메모리에 저장되어 있기 때문에 배열 이니셜 라이저를 사용하여 다른 배열에 복사를 한다는 것은, 문자 단위의 편집 등을 의도 하지 않으면 의미가 없는 행위이다. 목적에 따라 리터럴에 대한 포인터를 사용하는지 배열에 복사할지 여부를 선택해야 한다.

char chStr[] = "Kitty on your lap";

위의 문장은 리터럴 문자열을 새로운 배열 chStr에 복사할 수 있다. 리터럴 문자열에서 생성된 배열을 편집할 경우에 유효하지만 단순히 문자열을 가리키는 변수를 준비하고 싶은 것 뿐이라면 리터럴 문자열을 배열에 복사할 필요가 없다.

char *chpStr = "Kitty on your lap";

이것은 배열 이니셜 라이저에 리터럴 문자열을 지정하는 문장과는 큰 차이가 있다. chStr[] = "..."는 문자열을 저장만 하는 충분한 배열이 할당되고, 이것을 지정한 리터럴 문자열을 복사하지만, *chpStr = "Kitty on your lap"의 경우는 실행 파일에서 메모리에 로드된 문자열 리터럴에 주소를 대입하고 있는 것에 지나지 않는다.

코드1

#include <stdio.h>

int main() {
 char *chpStr = "Kitty on your lap";
 printf("%s\n" , chpStr);
 return 0;
}

코드1에서 포인터 chpStr에 리터럴 문자열 “Kitty on your lap"에 주소를 할당한다. 결과는 예상대로 “Kitty on your lap"라는 문자열을 표시할 뿐이다. 이 때, 포인터에서 리터럴 문자열의 내용을 변경할 수 없다. 내용의 변경을 시도했을 경우의 결과는 부정된다.

1.6.5 - C 언어 | 포인트 | 포인트 배열

각 요소가 포인터 형인 포인터의 배열을 만든다. 포인터의 배열을 이용하여 대용량 데이터를 구조적으로 관리할 수 있다.

포인터의 배열을 만들기

포인터는 메모리 주소를 저장하는 변수의 일종이므로, 정수형 등의 일반적인 변수와 마찬가지로 포인터의 배열을 만들 수 있다. 배열의 각 요소가 포인터라는 점을 제외하면, 일반 배열처럼 처리할 수 있다. 1차원에서도 다차원에서도 기본적으로 지금까지의 배열과 동일하다.

포인터의 배열은 다음과 같이 선언할 수 있다.

포인터 배열의 선언

형식 *포인터변수명[];

이렇게 하면, 여러 포인터를 배열로 통일된 관리를 할 수 있다. 첨자를 지정하여 저장되어 있는 주소를 꺼내거나 요소에 저장되어 있는 주소의 내용에 액세스할 수 있다. 단순히 포인터 변수를 배열화한 것이고, 그 취급은 배열과 포인터를 이해하고 있으면 어려운 것이 아니다.

예를 들어, 여러 문자열을 배열처럼 관리할 필요가 있는 경우, 다차원 배열을 이용하는 것보다도 문자열에 대한 포인터를 배열로 관리하는 형태가 자연스럽다.

코드1

#include <stdio.h>

int main() {
 char *chStr[] = {
   "Blue Blue Glass Moon" ,
    "Under The Crimson Air"
 };

  printf("%s\n%s\n" , *chStr , *(chStr + 1));
 return 0;
}

코드1의 배열 chStr는 문자열에 대한 포인터를 저장하는 2가지 요소로 구성되어 있다. 이 배열의 요소는 문자열을 포함하고 있는 것이 아니라, 문자열에 대한 포인터를 저장하고 있을 뿐이다. 2차원 배열에 의한 문자열 테이블에 비해 유연하게 처리할 수 포인터가 참조하는 주소를 바꾸는 것만으로 테이블을 갱신할 수 있다.

1.6.6 - C 언어 | 포인트 | 포인터의 포인터

주소를 저장하는 포인터도 편집의 일종이며, 포인터 자신도 주소가 있다. 주소 연산자를 사용하여 포인터의 주소 즉, 포인터에 대한 포인터를 얻을 수 있다.

포인터의 주소

포인터의 실체는 참조된 주소를 나타내는 숫자를 저장하는 일종의 변수라고 설명했다. 포인터도 주소를 저장하는 변수의 일종이며, 포인터 자신의 주소가 존재한다. 말이 반복되지만, 포인터의 포인터를 만들 수 있다는 것이다. 즉, 포인터의 주소를 다른 포인터에 저장하는 것이 가능하다.

포인터의 포인터가 참조하는 포인터이며, 이 포인터에서 더 참조할 수 있는 원래의 변수에 액세스할 수 있다. 이러한 다중 간접 참조의 이해는 포인터의 개념에서 중요한 부분이므로 확실히 이해하도록 한다. 포인터의 포인터를 선언하려면 별표 *를 더 추가한다.

포인터의 포인터 선언

형식 **변수명;

다중 간접 참조하려면 역시 간접 연산자 *를 참조하는 수준만큼 지정한다. 포인터의 참조 횟수가 많아지면 처리 속도에도 영향을 주기 때문에, 무의미한 다중 간접 참조는 좋을 것이 없다. 그러나 개발자가 보유하고 있는 포인터를 다른 함수에서 제어하고 싶은 경우에, 포인터의 포인터가 이용될 수 있다.

#include <stdio.h>

int main() {
  int iVariable = 100;
  int *ip = &iVariable;
 int **ipp = &ip;

  printf("--ip 포인터가 나타내는 값--\n");
  printf("주소 = %p\n" , &ip);
 printf("저장 주소 = %p\n" , ip);
 printf("저장 주소 내용 = %d\n\n" , *ip);

  printf("--ipp 포인터의 포인터가 나타내는 값--\n");
  printf("저장 값  = %p\n" , ipp);
  printf("저장 주소의 내용 = %p\n" , *ipp);
 printf("저장 주소 주소 내용 = %d\n", **ipp);

 return 0;
}

코드1은 정수 100을 저장하는 변수 iVariable과 iVariable의 주소를 저장하는 포인터 ip, 그리고 ip로 주소를 저장하는 포인터의 포인터 ipp가 정의되어 있다. 이 프로그램에서는 포인터와 “포인터의 포인터 ‘의 관계를 이해하기 위해 printf() 함수에서 각종 정보를 표시한다. 표시되는 주소는 실행시기에 따라 다르지만 위의 결과처럼 될 것이다.

포인터 ip 주소를 주소 연산자 &로 얻을 수 있다. 포인터의 포인터 ipp는 이를 저장하고 있기 때문에 ip 주소와 ipp의 저장 값은 동일하다는 것을 확인할 수 있다. 또한 포인터의 포인터 ipp을 간접 참조하는 것은 ip가 저장한 값을 참조한다는 것이다. ipp 저장하는 주소의 내용과 ip가 저장 주소가 동일한 지에서이를 확인할 수 있다.

포인터의 포인터에서 원래 변수의 값을 참조하려면 다중 간접 참조를 해야 한다. **ipp로 작성하여 다중 간접 참조를 하고, 마지막 printf() 함수에서 ipp가 저장한 주소(즉 ip)가 저장 주소(즉, iVariable)를 참조하고 있다.

1.6.7 - C 언어 | 포인트 | 포인터의 형변환

보통의 변수가 변환 연산자로 형변환할 수 있도록 포인터 변수도 캐스트 연산자로 다른 포인터 형식으로 변환할 수 있다.

포인터의 형변환을 이용하기

하드웨어에 가까운 저급 프로그램을 이해하는데 가장 중요한 것은 메모리 구조를 이해하는 것이다. C 언어는 고급 언어에 속하지만, 그 본질은 어셈블리 언어에 가까운 하드웨어 쪽의 저수준 처리도 특기로 여기고 있다. 그 대표적인 개념이 포인터이다.

포인터를 충분히 학습하면 C 언어에 있어서 형은 컴파일러가 변수와 포인터에 액세스할 때에 조작해야 하는 사이즈를 알 수 있기 위한 정보에 불과하다는 것이 알수 있다. 예를 들어, 32비트 컴퓨터에서 int의 배열 iArray[2]의 크기는 총 8바이트이며, 이를 char 형의 포인터로 다루기 위해 8개의 요소를 가지는 char 배열처럼 처리할 수 있다.

코드1

#include <stdio.h>

int main() {
  int iCount , iArray[2] = { 0x02040810 , 0x20408000 };
 unsigned char *cp = (unsigned char *)iArray;

  for(iCount = 0 ; iCount < 8 ; iCount++)
   printf("cp + %d = %d\n" , iCount , *(cp + iCount));

  return 0;
}

코드1은 int 형의 배열 iArray의 첫번째 요소에 주소를 char 형의 포인터 cp에 대입하고 있다. char 형을 중심으로 생각하면 2개의 요소를 가지는 int 형 배열은 32비트 컴퓨터에서는 8개의 요소를 가진 배열로 조작할 수 있기 때문에 for 문은 cp + 8까지의 값을 바이트 단위로 표시하고 있다.

이렇게 강제로 포인터를 변환하여 바이트 단위 또는 워드 단위로 값을 추출하는 것이 가능하다. 형은 한번에 조작할 바이트 크기를 나타내기 위한 정보에 불과하다는 것은 이와 같은 프로그램을 만들어 보면 쉽게 이해할 수 있을 것이다. 다만, int 형에서 원하는 바이트를 추출하는 경우는 일반적으로 이동 및 형 캐스팅 만 제공하므로 코드1과 같은 방식은 드물다.

어떤 기록 방식을 채용하고 있는 컴퓨터에서 이 프로그램을 실행하면 이상한 현상이 발생한다. 예를 들어 Microsoft Windows를 실행하는 Intel x86 호환 CPU의 경우 위와 같은 결과가 나타날 것이다. 프로그램을 보면 메모리는 표1과 같이 처음부터 2,4,8 … 그리고 순서에 값이 저장되는 것을 기대하고 있지만 결과를 보면 표 2와 같이 16, 8, 4, 2 …와 같이 역순으로 배치되어 있는 것을 확인할 수 있다. 왜 이런 결과를 얻게 되는 것일까?

표1 - 기대되는 배열 내용

iArray[0] iArray[1]
2 | 4 | 8 | 16 32 | 64 | 128 | 0

표2 - Intel x86 호환 CPU에서 실행하는 경우 물리적 메모리의 내용

iArray[0] iArray[1]
16 | 8 | 4 | 2 0 | 128 | 64 | 32

Intel의 CPU는 리틀 엔디안(little endian)이라는 방식으로 메모리에 정보를 기록하고 있다. 리틀 엔디안은 멀티 바이트(2바이트 이상의 정보)를 저장할 때, 하위 바이트부터 저장하는 특성을 나타낸다. 이와는 반대로, 모든 정보를 표1과 같이 상위 바이트부터 저장 형식을 빅 엔디안(big endian)이라고 한다. 옛 PDP 시리즈와 Motorola 등은 이 방법을 채용하고 있었다. 지금은 접할 기회가 적다고 생각되지만 빅 엔디안 형식을 채용한 컴퓨터에서 코드1을 실행하면 위의 결과와는 다른 결과를 얻을 수 있다.

문자 등의 바이트 단위의 정보를 취급하는 경우는 신경 쓸 필요는 없지만, 멀티 바이트 포인터 등으로 직접 취급하거나 메모리 덤프 등을 사용하여 디버깅할 경우 이러한 지식이 요구될 수 있다 .

포인터 형에서 다른 포인터 형식으로 변환은 프로그래머의 책임으로 할 수 있었지만, 포인터를 정수로 변환을 하는 것은 가능한가? 원칙적으로는 포인터와 정수는 상호 교환할 수 없다고 규정하고 있다. 그러나 물리적 메모리 주소는 숫자로 표현되고 있기 때문에 많은 처리계에서는 정수와의 상호 교환을 실현할 수 있을 것이다. 그러나 이식성 등을 중시하는 경우 처리계 의존 코드는 피해야 한다.

다만, 포인터와 정수의 상호 교환에서 0은 유일한 예외라고 되어 있다. 일반적으로 포인터는 어떤 변수를 가리키지만, 반드시 유효한 주소를 보유할 수 있다고는 할 수 없다. 어떤 유효한 포인터를 반환하는 함수를 만드는 경우, 받은 인수가 잘못된 값이었기 때문에 함수의 처리를 이행할 수 없는 경우, 함수는 무엇을 반복하면 좋은 것일까 요? 적어도 엉터리 값을 반환 피해야 한다. 그래서 사용되는 것이 0의 포인터이다.

0을 저장하는 포인터는 유효한 포인터와 비교하면 동일하지 않다는 것을 보증하고 있다. 즉, 포인터가 유효한 경우 0과 동일하게 되지 않는다.

포인터에 사용되는 0은 NULL이라는 별명이 주어 있다. 이 NULL이라는 식별자(NULL은 식별자이며, 키워드가 아니다)는 일반적으로 매크로로 정의되어 있다. 매크로에 대해서는 “매크로 상수"에서 자세히 설명하겠지만, 컴파일시에 NULL을 0으로 대체되기 때문에 NULL이라는 식별자와 0은 동일하다고 생각할 수 있다. (NULL == 0)은 성립한다.

포인터가 유효한지 여부를 조사하고 싶으면 NULL로 비교한다. 마찬가지로 포인터를 해제하고 싶다면 NULL 포인터에 대입한다.

char *str = NULL;

유효한 객체를 참조할 수 없는 포인터에는 NULL을 대입하는 것으로 잘못된 포인터임을 어필할 수 있다. NULL 포인터를 참조할 수 없다. NULL 포인터를 참조한 결과는 미정으로되어 있다. 포인터를 반환해야 함수가 처리에 실패하고, 적절한 포인터를 돌려 줄 수 없는 경우 NULL을 반환하여 오류를 알리는 방법은 사용 오래되었기 때문에, 포인터와 NULL의 비교는 프로그램에서 자주 사용되고 있을 것이다.

1.6.8 - C 언어 | 포인트 | 범용 포인터

임의 포인터를 저장할 수 있는 void 형 포인터를 소개한다. void 형 포인터는 함수를 통해 형식에 관계없이 포인터를 전달하고자 할 때 응용할 수 있다.

범용형

포인터를 받는 함수를 만들 때, 포인터의 형태를 특정하고 싶지 않은 경우가 있다. 예를 들어, 함수가 상태에 따라 다른 형태의 포인터를 받거나, 또는 반환하는 구조가 바람직한 경우이다.

예를 들어, 형식에 관계없이 특정 메모리 범위를 지정한 값으로 초기화하는 함수를 요청한 경우 어떻게 함수를 설계해야 할까? 하나의 방법으로는 char *형 포인터를 받고, 이에 지정된 값을 대입하는 방법을 생각할 수 있다. 그러나 함수의 이용자는 char * 이외의 경우는 함수를 호출할 때마다 포인터를 캐스팅해야한다.

특정 메모리 영역을 초기화하는 행위에 형태는 관계 없기 때문에 이 함수는 포인터 형에 관계없이 포인터를 받아야 한다. 이러한 경우 범용 포인터를 이용한다. 일반 포인터는 임의의 포인터를 대입할 수 있으며, 마찬가지로 형식 캐스팅에 의해 형태를 복원할 수 있다. 일반 포인터와 포인터의 형식에 void 키워드가 지정된 포인터이다.

void 형 포인터 선언

void * 변수명

void 형 포인터는 모든 형태의 포인터를 캐스팅하여 할당할 수 있다. 이를 이용하면 보다 확실하고 간단하게 형식에 관계없이 포인터를 받을 수 있다.

코드1

#include <stdio.h>

void FillMemory(void *mem , int size , char n) {
 int iCount;
 for(iCount = 0 ; iCount < size ; iCount++)
    *((char *)mem + iCount) = n;
}

int main() {
 unsigned int iCount , iArray[8];
  FillMemory(iArray , 4 * 8 ,0xFF);

 for(iCount = 0 ; iCount < 8 ; iCount++)
   printf("iArray[%d] = %X\n" , iCount , iArray[iCount]);

 return 0;
}

코드1의 FillMemory() 함수는 mem 초기화하는 영역에 대한 포인터를 size는 mem의 크기를 n은 초기화 값을 지정한다. main() 함수는 int 형의 배열 iArray[8]을 작성하고, 이를 FillMemory() 함수를 사용하여 0xFF로 초기화한다. 이 프로그램은 32비트 컴퓨터를 상정하고 있기 때문에, int 형은 4바이트, 배열은 8요소까지이기 때문에 iArray 배열 변수는 32바이트로 구성되어 있다. 따라서 FillMemory() 함수의 size 인수는 4 * 8을 지정한다.

이러한 형식에 관계없이 순수하게 메모리 주소를 전달하는 것이 목적 함수를 만드는 것은 드문 일이 아니다. 확장성이 뛰어난 시스템을 만들 때에는 실체를 추상화할 필요가 있기에 void 형 포인터를 적용할 수 있다.

1.6.9 - C 언어 | 포인트 | main() 함수의 매개 변수

프로그램 기동시에 부모 프로세스와 명령으로부터 받은 문자열을 가져온다.

명령 줄(command line) 인수

main() 함수도 인수를 받을 수 있다. 그럼 main() 함수는 어디에서 인수를 받는가? main() 함수는 자신을 기동시키는 부모 프로세스부터 시작시에 지정된 옵션을 문자열로 받는다. 일반적으로 명령 줄에서 전달된다. 이에 따라 프로그램의 실행에 필요한 정보를 요청할 수 있게 된다.

main() 함수는 두 개의 인수가 전달된다. 하나는 명령 줄에서 전달된 인수의 수이다. 다른 하나는 명령 줄 인수로 전달된 문자열이다. 관행적으로 첫번째 가인수을 argc, 둘째 가인수를 argv라고 명명한다. argc에는 인수의 수가 argv에는 인수의 문자열이 들어간다. 따라서 일반적인 main() 함수는 지금까지 사용해 온 인수를 받지 않는 형식 외에도 인수를 받는 형태 중 하나를 사용할 수 있다.

main() 함수

int main(int argc , char *argv[])

명령 인수를 처리하는 경우는 위의 형식을 사용해야 한다. 가인수의 이름은 선택 사항이지만 argument count를 축약한 argc와 argument vector를 축약한 argv를 사용하는 것이 관행으로 되어 있다. 배열 변수 argv에 액세스하여 지정된 번호의 명령 문자열을 얻을 수 있다.

코드 1

#include<stdio.h>

int main(int argc , char *argv[]) {
  int iCount;
 for(iCount = 0 ; iCount < argc ; iCount++)
    printf("%d번째 인수 = %s\n" , iCount + 1 , argv[iCount]);
  return 0;
}

코드1은 main() 함수에 주어진 인수를 표시한다. argc가 상수인 경우 argv[0]은 반드시 실행된 프로그램의 이름으로 정해져 있다. argc가 1이상의 값이면 어떤 명령 인수가 전달되었는지를 나타낸다.

실행 결과처럼 명령 인수는 문자열 테이블로 저장되어 있기 때문에 argc를 점검하고 argv의 적절한 요소에 액세스하여 원하는 인수를 얻을 수 있을 것이다. 실질적인 첫번째 인수는 argv[1]이며, 반드시 마지막 인수는 argv[argc - 1]이다. 이외에 표준은 argv[argc]이 NULL임을 보증한다.

1.7 - C 언어 | 구조체 선언

구조체에 대해서 설명한다.

1.7.1 - C 언어 | 구조체 선언 | 구조체 struct

int 형과 float 형 등 다양한 데이터 유형을 결합하여 하나의 변수로하는 구조에 대해 설명한다. 구조체를 응용하여, 여러 값의 조합으로 이루어진 복잡한 정보를 효율적으로 관리할 수 있다.

여러 형태로 이루어진 구조

배열은 어느 형태의 변수를 여러개를 모은 집합이었다. 따라서 C 언어 단순형을 모은 집합적인 요소를 합성체라고 부르고 있다. 여기서는 설명하는 구조체도 배열과 같은 간단한 형식을 모은 합성체이다.

배열은 지정된 형을 여러개를 모은 합성체이지만, 구조체는 다른 타입의 변수를 하나로 묶은 형태와 같은 것이다. 이는 논리적으로 연결되는 여러 정보를 정리하기 위해서, 유효하고 매우 중요한 기능이다. 대규모 시스템 개발에서 구조체는 없어서는 안될 존재이다.

논리적으로 연결하는 정보에는 예를 들어, 그래픽에서 좌표와 크기는 중요한 요소이다. 좌표는 x 좌표와 y 좌표의 두 숫자 정보로 구성하고, 크기도 폭과 높이의 두 가지 정보로 구성된다. 이러한 따로따로 다룰 수도수도 있지만, 현대 프로그래밍에서의 설계 사상에는 어긋난다. 이처럼 사람이 봤을 때 연결이 있는 정보는 하나로 정리하는 것이 바람직하다. 이렇게하면 프로그래머가 보다 확실하게 스마트 정보를 함수와 시스템 사이에서 전달시킬 수 있다.

이 때, 구조체가 가지고 있는 각 변수를 멤버라고 한다. 배열과 같은 요소라고 불러도 개념적으로 틀린 것은 아니지만, 그다지 그런 표현이 사용하지는 않는다.

구조체 선언은 struct 키워드를 이용하여 다음과 같은 형식을 갖는다.

구조체 선언

struct 태그명 {
 형식 멤버명1;
  형식 멤버명2;
  ...
} 구조체변수목록;

태그명에는 이 구조체의 이름을 지정한다. 이것은 구조체 변수를 선언할 때에 구조체를 참조하는데 사용된다. 구조체의 내부에서는 형식 및 멤버명을 지정하여, 구조체의 멤버를 선언한다. 멤버 선언은 일반 변수 선언과 기본적으로 비슷하지만, 초기화되지 않는다. 구조체 변수 목록에는 그 구조체의 변수 이름을 지정하여 구조체 변수를 정의한다. 이 구조체 변수 목록에 변수명을 선언함으로써 실제 메모리가 할당된다. 좌표 정보를 저장하기위한 구조체 변수, 예를 들어 다음과 같이 될 것이다.

struct Point {
 int x;
  int y;
} pt ;

이 때, Point 구조체는 int 형의 멤버 x와 y를 보유하고 있다고 생각할 수 있다. 이 구조체의 선언은 동시에 구조체 변수 pt를 정의하고 있다. 구조체 변수 목록은 생략할 수 있으며, 이 경우는 구조의 형식만이 선언 된 메모리가 할당되지 않는다. 구조체 변수를 임의의 장소에서 선언하려면 태그 이름을 이용하여 다음과 같이 설명한다.

태그에 의한 구조체 선언

struct 태그명 변수명;

struct 키워드는 이 변수가 지정한 태그의 구조체 변수임을 명시하기 위해 필요하며, 생략할 수 없다. 참고로 구조체 변수는 선언된 구조체의 실체라는 생각에서 인스턴스라고 부르기도 한다. 구조체는 인스턴스를 생성하기 위해, 컴파일러에 통지하기 위한 정보에 불과하다는 것을 이해한다.

생성한 구조체 변수의 멤버에 액세스하려면 구조체 변수명과 멤버명 사이에 마침표(.)를 지정한다. 이를 멤버 액세스 연산자라고 한다. 멤버 액세스 연산자는 구조체 변수를 왼쪽 피연산자에 오른쪽 피연산자 지정한 변수의 멤버를 오른쪽 피연산자에 지정하는 2항 연산자이다.

멤버 액서스 연산자

구조체변수명.멤버명

예를 들어, 이전의 Point 구조체의 변수 pt의 x 멤버값 10을 대입하려 한다면, pt.x = 10과 같이 작성한다.

코드1

#include <stdio.h>

int main() {
  struct Point {
    int x , y;
  } pt ;

  pt.x = 100;
 pt.y = 50;

  printf("pt.x = %d : pt.y = %d\n" , pt.x , pt.y);
 return 0;
}

이 프로그램은 좌표를 관리하는 Point 구조체를 선언하고, Point 구조체 형의 pt 변수를 만들고 있다. 멤버 액세스 연산자(.)를 사용하여 각 멤버에 값을 대입하여 printf() 함수에서 각 멤버의 값을 표시하고 있다.

구조체는 배열과 마찬가지로 합성체임을 설명하였다. 합성체는 이니셜 라이저를 사용하여 초기화할 수 있기 때문에 코드1과 같이 정적인 값으로 초기값을 할당한다면, 이니셜 라이저를 사용하는 것이 효과적이다. 즉 변수 선언시에 다음과 같이 초기화할 수 있다.

struct Point {
  int x , y;
} pt = { 100 , 50 } ;

초기화 방법은 배열과 동일하게 처음부터 순서대로 할당된 멤버의 값을 나타낸다. 초기화 목록의 값이 멤버 수보다 적은 경우, 나머지는 0으로 초기화된다.

또한, Point 구조체는 그 특징으로 재사용성이 높고, 어떤 함수에서도 자유롭게 접근할 수 있어야 한다. 일시적으로 구조체의 경우는 코드1과 같은 작성법이 권장되지만, 프로그램의 많은 곳에서 사용할 수 있는 재사용성이 높은 구조체는 글로벌 영역에 작성하고 필요할 때에 인스턴스를 만들 수 있도록 설계해야 한다.

코드2

#include <stdio.h>

struct Point {
 int x;
  int y;
};

int main() {
  struct Point pt = { 100 , 50 };
 printf("pt.x = %d : pt.y = %d\n" , pt.x , pt.y);
 return 0;
}

코드2는 Point 구조체를 선언할 때에 변수를 생성하지 않는다. 구조체의 인스턴스는 main() 함수에서 struct 키워드와 태그명을 사용하여 작성하고 있는 것에 주목한다. 대부분의 경우, 구조체는 이렇게 이용된다. 또한 초기값이 정해져 있다면, 이니셜 라이저를 사용하여 초기화할 수 있다.

코드2에서 만든 Point 구조체처럼, 구조체 자체의 선언은 새로운 형태 정보이며, 그 인스턴스는 필요할 때에 개발자가 정의하여 사용한다. 구조체에는 여러 단순 형식을 합성하여 새로운 형태를 만들어 버리는 것이라고 생각해도 좋을 것이다. 인스턴스가 생성될 때까지 메모리는 확보되지 않는다는 것과 인스턴스는 여러개를 만드는 것이 가능하다는 것을 이해한다.

다른 구조체 변수는 인스턴스가 다르기 때문에 멤버의 값을 공유할 수 없다. 구조체는 생성된 인스턴스마다 메모리를 할당한다. 구조체가 사용자 정의의 새로운 형정보이라는 것은 이 때문이다. 한번 만든 구조체(태그명이 정해져 있다면)는 여러 번 인스턴스를 만들고, 그 구조를 이용할 수 있는 것이다.

코드3

#include <stdio.h>

struct Point {
  int x;
  int y;
};

int main() {
  struct Point pt1 = { 100 , 50 } , pt2 = { 200 , 100 };

  printf("pt1 : x = %d , y = %d\n" , pt1.x , pt1.y);
 printf("pt2 : x = %d , y = %d\n" , pt2.x , pt2.y);

 return 0;
}

코드3에는 2개의 구조체 변수 pt1과 pt2를 정의하고 있다. 이들은 각각 독립적인 메모리 공간을 가지고 있기 때문에 멤버가 공유되지 않는다. pt1의 x 멤버의 값을 변경해도 pt2에는 물리적으로 관계가 없기 때문에 pt2 멤버의 값이 변화하는 것은 아니다. 그림1은 코드3의 두개의 구조체 변수의 메모리 구조를 간단히 나타낸다.

그림1 - 구조체의 인스턴스

구조체의 인스턴스

이렇게 구조체의 인스턴스는 멤버에 값을 저장하기 위한 메모리 영역을 개별적으로 보유하고 있다. pt1.x과 pt2.x의 메모리 주소는 다르기 때문에 실제 관련이 없다.

익명의 구조체

태그명을 생략하고 이름없는 구조체를 만들 수 있다. 다만, 태그명이 없는 경우는 구조체의 선언부 이외의 인스턴스를 만들 수 없기 때문에, 이 기법이 채용되는 것은 어느 일부에서 사용되는 구조의 경우뿐이다. 개발자와 설계자가 프로그램의 간편성을 위해 일시적으로 필요하다고 판단할 때에 사용하는 구조체으로 사용한다.

#include <stdio.h>

int main() {
  struct {
    int x;
    int y;
  } pt = { 100 , 35 };

  printf("pt.x = % d: pt.y = %d\n" , pt.x , pt.y);
 return 0;
}

코드4의 main() 함수 내에서 선언된 구조체는 태그명을 생략하고 있기 때문에 무명이다. 이 구조체의 인스턴스는 이 프로그램에서 pt 변수 뿐이며, 새로운 인스턴스를 만들 수 없다.

구조체 대입

모두가 같은 형태의 구조체 변수라면 구조체 변수를 다른 구조체 변수에 할당할 수 있다. 이것은 순전히 인스턴스를 복제하는 것이며, 구조체 변수가 보유하고 있는 멤버의 값도 똑같이 복사된다.

구조체 변수1 = 구조체 변수2;

이렇게 하면 위의 “구조체 변수1"은 “구조체 변수2"와 동일한 정보를 보유하게 된다. 이와 같은 대입을 실시하는 것으로, 간단하게 구조체 변수의 복사를 할 수 있다.

코드6

#include <stdio.h>

struct Point {
 int x;
  int y;
};

int main() {
  struct Point pt1 = { 200 , 100 } , pt2 = { 0 };

 printf("pt2.x = %d : pt2.y = %d\n" , pt2.x , pt2.y);
 pt2 = pt1;
  printf("pt2.x = %d : pt2.y = %d\n" , pt2.x , pt2.y);
 return 0; 
}

코드6은 Point 형의 pt1 변수와 pt2 변수를 선언하고 각각을 초기화한다. 이 때 pt2 변수는 0으로 초기화하고 있기 때문에 최초의 printf() 문에서 x 멤버도 y 멤버도 0이라는 값을 표시한다. 그러나, 다음에 pt2 = pt1라고하는 대입식으로 pt1 변수를 pt2 변수에 할당하고 있다. 그 결과 pt1 변수의 멤버가 그대로 pt2 변수에 복사되었기 때문에, 두 번째 printf() 문장에서는 pt1 변수와 같은 값을 표시하는 것이다.

이 특성은 함수의 인수 또는 반환 값으로 구조체를 전달할 때에도 적용할 수 있다. 인수로 구조체 변수를 전달하면 대입과 마찬가지로 인스턴스의 내용을 그대로 복사된다.

코드7

#include <stdio.h>

struct Point {
 int x;
  int y;
};

struct Point SizeToPoint(struct Point offsetPoint , int width , int height) {
 struct Point pt;
  pt.x = offsetPoint.x + width;
 pt.y = offsetPoint.y + height;
  return pt;
}

int main() {
 struct Point location = { 100 , 100 };
  struct Point target = SizeToPoint(location , 200 , 40);

 printf(
   "Rectangle\n\t"
   "Left = %d : Top = %d\n\t"
    "Right = %d : Bottom = %d\n" ,
   location.x , location.y , target.x , target.y
 );
  return 0; 
}

코드7은 어떤 좌표를 원점으로 지정된 폭과 높이의 좌표를 구하는 SizeToPoint() 함수를 만들고, 이 함수와 구조체 변수를 전달하고 있다. SizeToPoint() 함수는 offsetPoint에 지정된 좌표를 원점으로 폭 width와 높이 height에서 새로운 좌표를 생성하고 이를 반환한다. 이 함수를 이용하면 폭과 높이를 바탕으로 사각형의 오른쪽 아래 모서리의 좌표를 얻을 수 있다. 실행 결과의 Left와 Top는 사각형 왼쪽 위 모서리의 좌표 Right와 Bottom은 우측 하단의 좌표를 나타낸다. 프로그램에서는 오른쪽 아래 모서리의 좌표를 얻기 위해 SizeToPoint() 함수를 이용하고 있다.

구조체의 대입은 모든 멤버를 통째로 복사하는 방법으로 메모리 사용량과 CPU에 부하가 발생하는 점에주의해야 한다. 값의 복제가 아니라, 단순히 함수에 구조체의 값을 전달하는 것이 목적이라면 함수를 호출 할 때마다 구조체를 통째로 복사하는 방법보다 변수에 대한 포인터를 전달하는 것이 분명히 효율적이다. 이 방법은 “구조체에 대한 포인터"에서 명확하게 설명한다.

프로그램을 작성하고 종종 함수가 반환하는 합성체에 있어서, 합성체의 전체 요소에는 관심이 없고 특정 요소만이 식에서 필요한 것을 확인하는 경우가 있다. 예를 들어, 코드7의 SizeToPoint() 함수가 반환하는 Point 구조체의 정보 중에 x 또는 y 멤버의 값이 특정 표현식에서 한번만 필요한 경우이다. 이러한 경우 코드8와 같은 표기법을 사용하면 스마트하게 된다.

코드8

#include <stdio.h>

struct Point {
 int x;
  int y;
};

struct Point SizeToPoint(struct Point offsetPoint , int width , int height) {
 struct Point pt;
  pt.x = offsetPoint.x + width;
 pt.y = offsetPoint.y + height;
  return pt;
}

int main() {
 struct Point location = { 100 , 100 };
  printf(
   "Left = %d : Right = %d\n" ,
   location.x , SizeToPoint(location , 200 , 40).x
 );
  return 0;
}

주목해야 하는 것은 printf() 함수의 인수로 지정하고 있는 SizeToPoint(location, 200, 40).x라는 식이다. 마치 SizeToPoint() 함수 자체가 구조체 변수인 것처럼 작성하고 있다. 그러나, 이 작성법은 유효하다.

함수을 식에서 지정한 경우, 그 식에 있어서 함수는 반환값 형식의 값으로 해석할 수 있다. 식을 계산할 때에 함수가 실행되고, 제어가 돌아오면 반환 값이 식에서 사용되는 것이다. 코드8에 printf() 함수는 세번째 인수가 판단될 때, SizeToPoint() 함수가 호출된다. SizeToPoint() 함수의 반환 값은 Point 형이며, 이 함수는 식에서 Point 형이라고 생각할 수 있다. 따라서 SizeToPoint(...).x는 SizeToPoint() 함수가 반환하는 Point 형 인스턴스의 x멤버를 참조한다는 것을 나타낸다.

1.7.2 - C 언어 | 구조체 선언 | 구조체의 포인터

구조체의 값도 주소를 가지고 있기 때문에 구조체를 포인터로 처리할 수 있다. 구조체 포인터에서 구조를 가진 멤버에 액세스하는 방법을 설명한다.

구조체의 멤버에 간접 참조

구조체의 멤버가 포인터의 경우는 일반 포인터와 그 다루는 것은 변하지 않는다. 그러나 구조체의 포인터를 다룰 때 참조 방법에 주의해야 한다. 구조체 형의 포인터는 구조체 변수(인스턴스)의 메모리 주소를 저장한다. 이것은 “포인터"에서 설명된 일반 변수에 대한 포인터와 동일하다. 다음과 같은 구조체 선언을 생각해 보자.

struct Point *pointer = &pt;

pointer는 Point 형 구조체 변수의 주소를 저장하는 포인터 변수이다. 포인터를 이해하고 있으면, 이 선언과 초기화에 대해서는 특히 의문은 없을 것이다. 문제는 이 포인터가 가리키는 구조체 멤버에 간접 참조하려면 어떻게 하는가하는 것이다. 단순히 구조체의 인스턴스를 취득하는 경우는 간접 연산자를 사용뿐이다.

struct Point pt = *pointer;

그러나 구조체의 역할을 생각하면, 일반적으로 구조체의 인스턴스를 반환하는 목적이 아니라, 구조체의 인스턴스 멤버에 액세스하기 위해 간접 참조를 하는 것이다. 처음에는 다음과 같은 방법으로 접근을 시도 할지도 모른다.

int x = *pointer.x ;

언뜻 보면 pointer 포인터 변수가 가리키는 구조체의 x 멤버에 액세스하는 것처럼 보이지만, 불행히도 다르다. 이 경우 구조체의 x 멤버를 간접 참조하고 있음을 나타낸다. 구조체의 멤버가 포인터 형의 경우에 이러한 구문이 사용된다. 예를 들어 다음과 같은 구조이다.

struct Point {
 int *x;
 int *y;
};

하지만 이번 목표는 포인터 형의 멤버에서 간접 참조를 실시하는 것이 아니라 구조체의 포인터에서 구조체의 인스턴스에 간접 참조하는 것이다. 이 경우는 연산자 우선 순위의 관계에서 다음과 같이 작성해야 한다.

int x = (*pointer).x;

이렇게 하여 구조체 형의 포인터에서 간접 참조할 수 있다. 위의 문장은 (*pointer)가 구조에 간접 참조를 실시하는 구조체의 인스턴스를 반납하고, 그 x 멤버에 액세스하는 것을 나타낸다.

코드1

#include <stdio.h>

struct Point {
  int x;
  int y;
};

int main() {
  struct Point pt = { 100 , 200 };
  struct Point *ppt = &pt;

  printf("pt.x = %d : pt.y = %d\n" , (*ppt).x , (*ppt).y);
 return 0;
}

실행결과

pt.x = 100 : pt.y = 200

코드1은 Point 형의 포인터 ppt 구조체 변수 pt 주소를 대입한다. printf() 함수에서 포인터에서 구조체의 멤버 값을 표시하기 위해 (*ppt) .x라고 하는 형태로 간접 참조를 하고 있는 것에 주목하자.

구조체에 대한 포인터는 실제 프로그래밍에서 자주 사용된다. 거대한 시스템에서는 이것인가?라고 말할 만큼 구조체가 사용되고 있으며, 이 구조체의 정보 전달과 생산, 가공을 함수로 하는 수단으로 포인터에 의한 참조 전달이 사용되는 것이다. 구조체의 포인터를 간접 참조할 때 일부러 위와 같이 (*pointer).~라고 작성하는 것은 조금 귀찮다.

그래서 멤버 액세스 연산자 “->“를 사용할 수 있다. 이 연산자는 기능적으로 “.“연산자와 동일하지만 구조체의 포인터에서 멤버를 참조한다는 점에서 성격이 다르다. 멤버 액세스 연산자 “->“는 종종 화살표 연산자라고 한다.

화살표 연산자

구조체에 대한 포인터명 -> 멤버명

이 -> 연산자는 빼기 -와 > 기호의 조합으로 구성되어 있다. (*pointer) .memberpointer->member와 똑같은 것이라고 생각할 수 있다. 많은 프로그래머는 (*pointer).member보다 pointer->member라는 기법을 선호하기 때문에 특별한 의도가 없다면 -> 연산자를 사용하는 것을 권장한다.

코드2

#include <stdio.h>

struct Point {
 int x;
  int y;
};

int main() {
  struct Point pt = { 100 , 200 };
  struct Point *ppt = &pt;

  printf("pt.x = %d : pt.y = %d\n" , ppt->x , ppt->y);
 return 0;
}

이 프로그램의 동작은 코드1과 동일하지만, 구조에 대한 포인터 ppt에서 멤버를 참조하면 -> 연산자를 사용한다는 점에서 차이가 있다.

“구조체의 코드7"에서 함수에 구조체의 값 전달은 하지 않는다고 설명하였다. 이유는 값을 통째로 복사하므로, 합성체의 값 전달은 메모리와 CPU에 높은 부하를 가할 가능성이 있다. 일반적으로 배열이나 구조체 등은 포인터에 의한 참조 전달을 채용한다. 이번 해설한 구조체의 포인터를 다루는 방법을 이해하면 이를 구현할 수 있을 것이다.

#include <stdio.h>

struct Point {
 int x;
  int y;
};
void SizeToPoint(struct Point *offsetPoint , struct Point *target , int width , int height) {
 target->x = offsetPoint->x + width;
 target->y = offsetPoint->y + height;
}

int main() {
 struct Point location = { 100 , 100 } , target;
 SizeToPoint(&location , &target , 200 , 40);

  printf(
   "Rectangle\n\t"
   "Left = %d : Top = %d\n\t"
    "Right = %d : Bottom = %d\n" ,
   location.x , location.y , target.x , target.y
 );
  return 0;
}

이것은 “구조체의 코드7"의 SizeToPoint() 함수를 개량한 것이다. 정보원이 되는 기준점을 포함하는 구조체에 주소를 offsetPoint 지정하고 새로운 좌표를 저장하는 구조체의 주소를 target에 지정한다. width와 hieght은 지금까지와 같이 offsetPoint 대한 폭과 높이를 지정란다. 이 함수는 offsetPoint 매개 변수에서 상대적인 폭과 높이의 위치를 나타내는 좌표를 target 매개 변수에 간접 참조로 저장한다.

SizeToPoint()를 호출 할 때, 구조체의 인스턴스가 아닌 포인터를 전달하고 있기 때문에, 합성체를 통째로 복제하는 부하를 피할 수 있다. 또한 반환 값으로 구조체를 반환하는 것이 아니라, target을 간접 참조 값을 대입하고 있기 때문에, 반환 값은 사용되지 않는다. 예를 들어, 함수가 성공했는지 실패했는지를 호출자에게 통지하는 수단으로 사용하는 등 다른 용도로 반환 값을 사용할 수 있게 된다.

1.7.3 - C 언어 | 구조체 선언 | 구조 유형 변환

구조체의 값을 다른 값으로 직접 변환할 수 없지만, 구조체에 대한 포인터를 다른 형식에 대한 포인터로 변환할 수 있다. 이를 응용하여 물리적 구조에 호환되는 구조체에 대한 포인터를 변환하여 사용할 수 있다.

포인터로 형변환(casting)

이번에는 구조체에 대한 포인터를 사용해 형변환하는 기술에 대해 설명한다. 이 기법은 구조체의 특성을 생각하면, 의미가 다른 구조로 값을 변환하는 반칙 행위와 같은 것이다. 구조 설계자는 경우에 따라 이러한 사용 방법을 의도하고 있지 않을 가능성도 있고, 잘못 취급하면 프로그램이 충돌 디버그를 복잡하게 만들 수 있다는 것을 인식하지 않을 수도 없다.

구조체는 다른 형태의 구조체에 대입할 수 없으며, 형변환도 허용되지 않는다. 예를 들어 다음과 같은 코드는 컴파일시에 에러 판정을 받는다.

struct Point pt = { 10 , 100 };
struct Size sz = pt;

캐스트 연산자를 사용하여 sz = (struct Size)pt;라고 기술한 경우도 동일하다. 다른 구조체 형은 형태에 호환성이 없기 때문에 대입은 인정되지 않는다.

그래서 포인터의 형태에 대해 생각해 보자. 포인터 형은 컴파일러가 포인터를 간접 참조할 때에 사이즈를 식별하는 것에 불과했다. 구조체의 경우도 마찬가지로 구조체에 대한 포인터는 구조체의 형(인스턴스 메모리 사이즈)를 결정하기 위한 정보에 불과하다. 다음의 구조를 보자.

struct Point { int x , y; };
struct Size { int width , height; };

이러한 Point 구조체와 Size 구조체는 다른 형태이므로 의미적인 호환성은 없다. 그러나 멤버를 보면 이러한 구조체는 int 형의 멤버를 2개 보유한다는 공통점이 있다. 이것은 Point 구조체와 Size 구조체의 인스턴스에 할당된 메모리 크기가 동일함을 나타낸다. 32비트 컴퓨터의 경우 이러한 구조체의 인스턴스는 8바이트가 될 것이다.

이 사실만 안다면, 구조체에 대한 포인터를 다른 포인터 형식으로 변환하여 인스턴스를 제어할 수 있다. 8바이트라는 실체는 int 형의 두 가지 요소를 가진 배열로 인식할 수 있다면, char 형의 8 개의 요소를 가진 배열로 인식할 수 있다. 당연히 사이즈가 같으므로 Point 형의 인스턴스를 Size 형의 포인터로 취급할 수도 있다.

코드1

#include <stdio.h>

struct Point { int x , y; };
struct Size { int width , height; };

int main() {
 struct Point pt = { 400 , 300 };
  struct Size *psz = (struct Size *)&pt; 

 printf("&pt.x = %p : pt.y = %p\n" , &pt.x , &pt.y);
  printf("&psz->width = %p , &psz->height = %p\n" , &psz->width , &psz->height);
 printf("psz->width = %d : psz->height = %d\n" , psz->width , psz->height);
 return 0;
}

코드1은 Point 형의 pt 변수의 주소를 Size 형의 포인터 psz 변수에 대입하고 있다. 이들은 논리적인 형태의 호환성은 없지만, 메모리에 저장되어 있는 물리적 정보를 통제한다는 점에서 생각하면 문제는 없다. Size 형도 Point 형이 멤버 이름이 다를 뿐이고, 실질적인 구성은 동일하다. 결과를 보면, 이들이 동일하다는 것을 확인할 수 있다.

런타임 상태에 따라 주소는 다르지만 &pt.x&psz->width의 메모리 주소가 동일하다는 것이 중요하다. &pt.y&psz->height가 동일한 지도 말할 것도 없다. psz 변수가 가리키는 인스턴스는 Point 형이든 Size 인 메모리 크기라는 사실에는 변함이 없다.

이 사실은 시스템을 개발하는 설계자들에게 중요한 요소이다. 시스템은 규모가 커질수록 향후의 확장과 버그를 줄이고, 보다 최적의 프로그램(필요한 메모리의 감소와 속도 이식 등)이 요구된다. 이러한 프로그램의 확장에 있어서, 구조체와 함수 관계는 시스템의 설계(디자인)에 직접 영향을 주는 존재이다. 그렇다면 구조체와 함수 관계는 유연한 것이어야 하며, 시스템을 확장할 때에 기존의 프로그램 코드를 모두 다시 작성하여 다시 컴파일해야 한다 같은 디자인은 피해야 한다.

시스템 개발자는 향후 확장에 대비한 함수의 구현 방법으로, 예를 들어 이 포인터에 의한 구조체의 제어를 사용할 수 있다. 포인터를 사용하면 인스턴스에 관계없이, 멤버를 조작하는 것만으로 원하는 정보를 얻거나 설정할 수 있는 시스템이 제공하는 기능의 역할을 이행할 수 있다.

코드2

#include <stdio.h>

struct Color {
 char *name;
 int r , g , b;
};
struct ColorEx {
  char *name;
 int r , g , b , a;
};

void SetColor(struct Color *color) {
  printf(
   "%s r = %d : g = %d : b = %d\n" ,
    color->name , color->r , color->g , color->b
  );
}

int main() {
 struct Color color = { "Color" , 0xFF , 0 , 0 };
  struct ColorEx colorEx = { "ColorEx" , 0 , 0xFF , 0 , 0xA0 };

 SetColor(&color);
 SetColor((struct Color *)&colorEx);

 return 0;
}

코드2는 Color 구조체에 대한 포인터를 받는 함수 SetColor() 함수를 정의하고 있다. 이 함수는 Color 구조체의 멤버 값을 표시할 뿐이지만 그 동작은 중요하지 않다.

시스템의 형편상 지금까지 색상 정보에 대해 Color 구조체를 사용해오고 있었지만, 새로운 알파 값을 추가한 ColorEx을 다루어야 않게 되었을 경우를 가정해보자. 코드2는 이러한 요구를 해결하는 방법의 힌트가 되는 것이다. 이 때, 새롭게 정의하는 ColorEx 구조체는 기존에 사용하던 Color 구조체와 메모리 구조 수준에서 호환성을 얻기 위해서 name, r, g, b까지의 멤버를 동일한 위치에 선언한다. 그리고 추가하는 정보를 그때부터 선언하는 것이다. 코드2에서 추가 정보인 a 멤버를 끝으로 선언하고 있는 것을 확인할 수 있다.

앞에 멤버부터의 구조가 Color 구조체와 동일하기 때문에, ColorEx 구조체의 메모리 구조는 Color 구조체와 호환성이 있다고 생각된다. 이 경우은 기존에 사용하던 Color 구조체에 대한 포인터를 받는 함수에 ColorEx 구조체의 포인터를 전달해도, 아무 문제도 발생하지 않는 것이다. 왜냐하면 기존 사용하던 Color 구조체에 대한 포인터를 받는 함수는 ColorEx 구조체의 추가 정보에는 원래 관심이 없기 때문이다. ColorEx 구조체도 Color 구조체를 기반으로 하고 있기 때문에 name, r, g, b 멤버에 대해 Color 구조체로 조작하는데 문제는 없다.

이렇게 SetColor() 함수에 ColorEx 구조체의 포인터를 넘겨도 문제가 없다. 이러한 설계를 기반으로 한 시스템은 기존의 코드를 변경하지 않고 ColorEx 구조체를 새롭게 정의하여 기존과는 다른 알파 값을 처리보다 편리한 함수 등을 시스템에 추가할 수 있을 것이다. 코드2는 colorEx 구조체 변수를 전달할 때 명시적 형변환을 실시하고 있지만, 이것은 컴파일러에서 경고를 받을 수 있기 때문이다. 더 유연하게 설계하고자 한다면 SetColor() 함수에서 받을 포인터를 void*로 하는 방법도 있다.

더불어 유연성을 추구하는 SetColor() 함수가 향후 확장한 ColorEx 구조체의 예기치 않은 형식을 처리할 수 있도록 하려면, 구조체에 그 구조체 자체의 크기를 저장하는 멤버를 추가한다. 이 기법은 “형의 사이즈"로 증명한다.

1.7.4 - C 언어 | 구조체 선언 | 구조체의 멤버

구조체의 멤버에 다른 구조체 형식을 지정할 수 있다. 여러 구조를 조합한 복잡한 구조와 구조체에 대한 포인터를 멤버로 가지는 구조체 등을 만들 수 있다.

구조체 멤버를 가지는 구조체

구조체는 멤버에 다른 구조체를 포함할 수 있다. 이것은 구조체의 특성에 따라 편리한 경우가 있다. 예를 들어, 좌표를 나타내는 Point 구조체와 크기를 나타내는 Size 구조체가 있다고 하여, 이러한 구조체의 멤버를 포함한 Rectangle 구조체를 만들 수 있다.

struct Point { int x , y; };
struct Size { int width , height; };
struct Rectangle {
  struct Point location;
  struct Size size;
};

Rectangle 구조체는 Point 및 Size 구조체의 멤버를 포함한다. 사각형을 나타내는 Rectangle의 정보는 좌표와 크기로 구성되어 있기 때문에 이러한 형태가 유용하다고 생각된다. 물론 실전용을 생각하면 멤버를 추적하는 것이 귀찮아서 단순히 4개의 int 형 멤버를 선언해야 한다는 주장도 있을 것이다. Rectangle의 폭 width를 참조하려면 다음과 같이 작성한다.

rect.size.width
rect->size.width

위는 일반 변수이고, 아래는 포인터에서 액세스한 경우이다. 따라서, 가장 바깥 쪽의 구조체부터 멤버를 차례로 접근해 간다.

코드1

#include <stdio.h>

struct Point { int x , y; };
struct Size { int width , height; };
struct Rectangle {
  struct Point location;
  struct Size size;
};

int main() {
 struct Rectangle rect = { 100 , 50 , 400 , 300 };

 printf("Location (%d , %d) : Size (%d , %d)\n" ,
   rect.location.x , rect.location.y ,
   rect.size.width , rect.size.height
  );
  return 0;
}

이 프로그램은 Point 및 Size 구조체를 포함하는 Rectangle 구조체를 선언하고 이를 이용하고 있다. 초기화를 보면 인식할 수 있다고 생각하지만, 비록 구조체의 멤버를 가져도 기본적인 개념는 동일하며, Rectangle 구조체의 실체는 int 형 4개라는 사실은 변하지 않을 것이다 .

이 방법을 사용하여 “구조체 형변환 코드2"를 개선하고, 보다 확실하게 기본이 되는 구조를 상속할 수 있다.

코드2

#include <stdio.h>

struct Color {
  char *name;
 int r , g , b;
};
struct ColorEx {
  struct Color color;
 int a;
};

void SetColor(struct Color *color) {
  printf(
   "%s r = %d : g = %d : b = %d\n" ,
    color->name , color->r , color->g , color->b
  );
}

int main() {
 struct Color color = { "Color" , 0xFF , 0 , 0 };
  struct ColorEx colorEx = { "ColorEx" , 0 , 0xFF , 0 , 0xA0 };

 SetColor(&color);
 SetColor((struct Color *)&colorEx);

 return 0;
}

ColorEx 구조체의 첫번째 멤버가 Color 구조체인 것에 주목해 보자. 이전은 Color 구조체와 동일하게 되도록 name, r, g, b를 각각 다시 정의하고 있지만, 코드2의 ColorEx는 보다 확실하게 기본이 되는 Color 구조체를 계승하고 있다. 디자인으로써는 이러한 작성하는 것이 일관성이 높아지기 때문에 안전성이 향상된다고 생각할 수 있다.

자기 참조적 구조

구조체가 구조체의 멤버를 보유할 수 있는 것을 알 수 있었다. 그럼 그 구조체 자신을 멤버로 보유한 경우는 어떻게 될까. 이러한 구조체를 자기 참조적 구조라고 한다. 예를 들어 다음과 같은 구조의 선언이다.

struct Node { struct Node node; };

유감스럽게도, 이것은 오류이다. 그러나 포기는 이르다. 자기 참조의 목적은 구조체의 멤버로서 자신과 같은 정보를 가진 존재에 액세스할 수 있다. 구조체은 포인터 형의 멤버를 보유할 수 있기 때문에 다음의 선언이면 유효하다.

struct Node { struct Node *node; };

이 Node 구조체는 Node 형에 대한 포인터를 보유한다. 이 포인터에서 간접 참조함으로써 자기 참조적인 구조를 만들 수 있다. 자기 참조적인 구조체는 친자 관계의 존재하는 개체을 만들때 적합하다. 예를 들어, 윈도우 시스템의 컴포넌트 관계 및 메뉴와 트리로 사용되는 항목의 계층 관계이다. 트리 구조의 성질을 가진 정보를 취급하는 경우 동질의 부모와 자녀를 보유할 가능성이 높고, 이러한 정보의 관리는 자기 참조적인 구조체가 유용할 것이다.

#include <stdio.h>

struct Chain {
 char *text;
 struct Chain *next;
};

void ShowChain(struct Chain *chain) {
  if (chain == NULL) return;

  printf("%s\n" , chain->text);
  ShowChain(chain->next);
}

int main() {
  struct Chain first = { "First Chain" };
 struct Chain second = { "Second Chain" };
 struct Chain third = { "Third Chain" };

 first.next = &second;
 second.next = &third;

 ShowChain(&first);

  return 0;
}

코드3은 텍스트 데이터의 관련을 연결하는 자기 참조적인 구조체 Chain을 선언하고 있다. 이 구조체를 이용하면, 자기 참조하여 바로 체인과 같은 데이터 관계를 만들 수 있다. ShowChain() 함수는 주어진 Chain 포인터에서 체인을 접근하면서 요소가 없어질 때(next 멤버가 NULL)까지 재귀 프로세스를 반복한다.

1.7.5 - C 언어 | 구조체 선언 | 비트 필드

구조체의 여러 멤버를 미세하게 비트 단위로 분할하여 사용하는 방법을 설명한다. 비트 단위에 분리된 여러 정보를 하나의 구조로 패키지화할 수 있다.

비트 단위의 분할

비트 필드에는 구조체의 멤버에 관한 기능에 여러 값을 미세하게 비트 단위로 분리해 사용하는 경우에 유용하다.

예를 들어, 전자 음악 규약의 MIDI(Musical Instrumunt Digital Interface)이다. 이 프로토콜은 1980 년대 초반의 8bit CPU 시대의 산물로, 지금도 변함없이 전자 악기 분야에서 이용되고 있다. 당시의 프로세서는 지금보다 훨씬 느리고, 메모리도 지금과는 비교할 수 없을 정도로 용량이 적고 고가였다. 당시는 어떻게 정보를 효율적으로 전달하고, 적은 메모리 공간에서 더 빠르게 처리할 것인가하는 문제가 프로그램의 간단함과 구조의 아름다움을 무시하게 되었다. 그 수단으로 바이트에 비트 단위의 데이터를 분할하여 저장하는 방법을 생각할 수 있다.

비트 필드는 2개의 정보를 4비트씩 분할하여 1바이트로 압축하여 전송하는 처리에 사용할 수 있다. 이것은 처리계에 의존하는 수치형의 실체를 필드는 비트 단위로 분할하여 사용하는 것이다. 따라서 4비트와 6비트의 멤버를 만들 수 있게 된다. 다만, 비트 필드는 주소가 존재하지 않기 때문에 포인터로 사용할 수 없다.

비트 필드를 이용하려면 구조체의 선언으로 멤버 이름 뒤에 콜론":“과 필드의 비트 길이를 지정한다.

struct Msg {
  unsigned int type : 1;
  unsigned int attr : 3;
  unsigned int id : 4;
};

이 Msg 구조체는 1비트로 구성되는 type, 3비트로 구성 attr, 4비트로 구성 id 멤버를 가지고 있다. 부호없는 정수임을 강조하기 위해 일반적으로 비트 필드의 선언은 unsigned int를 사용한다.

다만, 비트 필드를 가진 구조체의 인스턴스가 어떤 메모리 구조가 될지는 구현에 크게 의존한다. 바이트 단위를 걸쳐 비트 필드의 구조가 메모리 공간에서 어떻게 표현되는지에 대해 C 언어는 정해져 있지 않다. 처리 단위의 상위 비트에서 필드를 할당할지도 모르고, 하위 비트에서 할당될 수 있다.

코드1

#include <stdio.h>

struct MidiMsg {
 unsigned int status : 4;
  unsigned int channel : 4;
 unsigned int second : 8;
  unsigned int third : 8;
};

int main() {
 struct MidiMsg msg = { 9 , 0 , 0x3C , 40 };
 printf(
   "status = %d\nchannel = %d\n"
   "second = %d\third = %d\n" ,
    msg.status , msg.channel , msg.second , msg.third
 );

  return 0;
}

코드1은 비트 필드를 가진 MidiMsg 구조체를 선언하고 있다. 이 구조체는 MIDI 하드웨어에 전달하는 메시지를 나타낸다. MIDI에 대한 지식이 필요하지 않는다. MIDI 메세지의 status 그리고 channel 정보가 4비트 단위로 분할되어 있다는 것에 주목하자. 예를 들어, 이 구조는 32비트 컴퓨터에서는 다음과 같은 구성이 될 것으로 예상된다.

표1 - 32비트 컴퓨터에서 MidiMsg 구조체의 구조

unsigned int 32 bit
status 4 bit | channel 4 bit | second 8 bit | third 8 bit | 사용하지 않는 영역8 bit

반복이 되지만, 비트 필드가 어떻게 인스턴스화 되는지는 구현에 따라 달라진다. 그러나 실행 결과는 예상대로라고 생각한다. 비트 필드가 그 비트 단위로 제대로 분해되어 있는지 확인하고 싶다면, status 멤버에 0xF 이상의 값을 대입해 보면 알 수 있다. status 멤버는 4비트이므로, 그 이상의 값을 대입하면 상위 비트가 잘린다.

1.7.6 - C 언어 | 구조체 선언 | 공용체 - union

여러 멤버를 공유하는 하나의 값을 공용체이라고 한다. 공용체는 구조체와 비슷하지만, 모든 구성원은 동일한 영역을 의미하며, 공용체의 인스턴스는 멤버 중 가장 큰 크기에 맞게 만들어 진다. 단일 값을 여러 형태로 표현하고 싶은 경우에 적용 할 수 있다.

다른 형태의 메모리를 공유

포인터 형변환을 잘 활용하여 어느 형을 다른 형처럼 사용할 수 있었다. 4개의 int 형의 멤버를 가지는 구조체의 인스턴스는 int 형 포인터로 캐스팅하여 4개의 요소를 가지는 int 형 배열로 처리할 수 있다. 이는 데이터가 메모리에 어떻게 기록되어 있는가하는 원리를 아는 중요한 실마리가 될 것이다.

이 생각을 발전시켜, 더 실제적으로 행동하는 것이 공용체이다. 공용체의 구문은 매우 구조체와 비슷하지만, 모든 멤버가 동일한 기록 영역을 공유한다는 점에서 그 성격이 크게 다르다. 예를 들어 수학의 행렬을 생각해 보자. 3차원 그래픽 프로그래밍에서는 자주 행렬 연산을 한다. float 형에서 4 × 4 행렬을 구현하자면 float matrix[4*4]으로도 float matrix[4][4]으로도 동일하며, 행렬의 각 요소를 모두 개별 멤버로 보유해도 상관없다. 어떤 형식이 좋은가하는 것은 대부분은 프로그래머의 취향의 문제가 될 것이다. 공용체는 이러한 문제를 한번에 해결해주는 효과적인 수단이다.

공용체는 구조체의 struct 대신에 union 키워드를 사용한다.

공용체 선언

union 태그명 {
형식 멤버명;
...
} 공용체 변수명;

태그명을 생략하고 익명의 공용체를 만들 수 있다는 점에서도 구조체와 동일한다. 공용체는 Pascal 언어 경험자에게는 가변 레코드와 유사한 기능이라고 설명하는 것이 알기 쉬울지도 모른다. 공용체의 모든 구성원이 동일한 주소를 돌려준다. 이것은 공용체가 동일한 저장 공간을 공유하고 있음을 증명하고 있다. 또한 공용체의 멤버에 액세스하는 방법은 구조체와 동일하다.

코드1

#include <stdio.h>

union Value {
 unsigned char chValue;
  int iValue;
};

int main() {
 union Value u;
  u.iValue = 0xFFFF;

  printf(
   "chValue = %08X : &chValue = %p\n"
   "iValue = %08X : &iValue = %p\n" ,
   u.chValue , &u.chValue , u.iValue , &u.iValue
 );
  return 0;
}

코드1은 공용체 Value를 선언하고 있다. 이 공용체는 unsigned char 형의 멤버 chValue과 int 형의 멤버 iValue을 보유하고 있지만, 구조체와는 다르게 이 멤버들은 저장 공간을 공유하고 있다. 따라서 실행 결과에서 확인할 수 있듯이 chValue 멤버의 값을 변경은 iValue 멤버에도 영향을 받고, iValue 멤버의 변경은 chValue 멤버에 영향을 받는다.

먼저 &chValue과 &iValue가 같은 주소를 반환하는 것을 유의한다. 이 결과에서 공용체가 동일한 저장 공간을 공유하고 있다는 것이 증명되고 있다. 공용체는 모든 멤버를 공유하기 위해서 가장 큰 멤버 형식에 따라 저장 공간을 확보한다. 코드1의 Value 구조체는 int 형에 맞춘다. chValue은 확보되어 있는 int 형의 기억 영역 중에 하위 1바이트를 공유하고 있는 것이다. 따라서 공용체 변수 u가 보유하고 있는 값은 0xFFFF이지만, chValue에 액세스한 경우 하위 1바이트 밖에 볼 수 없기 때문에 0xFF가 반환된다.

공용체는 구조체 마찬가지로 구조체나 공용체, 배열과 같은 복잡한 형태를 멤버로 보유할 수 있다. 예를 들어, 다음과 같은 복잡한 형태를 만들 수 있다.

union {
 struct {
    float _11, _12, _13, _14;
   float _21, _22, _23, _24;
   float _31, _32, _33, _34;
   float _41, _42, _43, _44;
 } matrix ;
  float m[4][4];
} u ;

이 공용체는 멤버에 16개의 float 형 멤버를 가지는 구조체와 4 × 4 개의 요소를 가지는 float 형 2차원 배열을 선언하고 있다. 구조체와 배열은 모두 16개의 float 형 요소를 보유한다는 점에서 이러한 멤버에 필요한 저장 공간의 크기는 동일하다. 개발자는 구조체의 각 멤버에 액세스할 수 있으며, 배열에서 인덱스를 지정해 액세스할 수 있다. 이러한 공용체는 3차원 그래픽의 좌표 변환을 위한 행렬에 사용된다. 구조체의 멤버로는 u.matrix._11와 같이 외부에서 순차적으로 멤버 지정한다.

공용체의 초기화

공용체의 초기화는 구조체 혹은 배열과 같이 각각의 요소에 수행할 수 없다. 왜냐하면 공용체의 멤버는 저장 공간을 공유하고 있기 때문에, 공용체가 보유하는 요소의 실체는 1개로 간주하기 때문이다. 그래서 공용체의 초기화는 첫번째 멤버 형에서만 초기화를 할 수 있다.

코드2

#include <stdio.h>

union Point {
 struct {
    short int x , y;
  } point;
  int location;
};

int main() {
 union Point u = { 100 , 50 };

 printf(
   "x = %d : y = %d\nlocation X = %d : location Y = %d\n" ,
    u.point.x , u.point.y , (short int)u.location , u.location >> 16
  );
  return 0;
}

이 프로그램의 Point 공용체는 32비트 컴퓨터 시스템에 있어서, 상위 16비트와 하위 16비트에 논리적으로 분할된 32비트에 팩(pack)된 좌표를 표시하는 사양에 적합하다. 이 공용체의 첫번째 멤버는 short int 형의 x와 y를 보유하는 무명 구조체이다. 따라서 초기화에는 short int 형의 두 정수를 지정할 수 있다. 이것은 개발자가 좌표를 지정하는 경우는 직관적인 사양이므로 환영받을 것이다.

그러나 시스템은 사정상 32비트 값으로 일괄하는 것이 더 빠르게 처리할 수 있을지도 모른다. 이 경우에는 location 멤버에 액세스하면 좋을 것이다. 이러한 공용체는 사용법에 따라 팔방 미인이 될 수 있다. 다만, 코드2는 int 형이 32비트, short int 형이 16비트인 것을 상정하고 있다. 그 이외의 환경에서 올바른 결과를 표시하지 않으므로 주의하자.

1.7.7 - C 언어 | 구조체 선언 | 열거형 - enum

열거형을 이용하여 쉽게 임의의 정수에 대응하는 식별자를 만들 수 있다. 여러 항목이 있는 상수의 목록을 만들 때 유용하다.

명명된 상수

실전 프로그램에서는 함수나 시스템에 정보의 특성을 전하는 수단으로 상수를 줄 수 있다. 상수 자체는 의미가 없으며, 설계자가 상수에 대응하는 의미를 붙인다. 예를 들어 2가 전달되면 OK, 4가 전달되면 취소, 8이면 재시도라고하는 상태이다.

이러한 상수를 요구하는 경우, 상수를 직접 코드로 작성하기 보다는 상수에 이름으로 붙여 추상적으로 표현하는 것이 융통성이 있다고 설계자는 생각할 것이다. 그래서 하나의 수단으로 열거형을 사용한다.

열거형은 열거라는 명명된 상수의 집합으로 구성된다. 열거는 상수를 지정할 수 있는 모든 장소에서 지정할 수 있다. 기본적으로 논리적 의미가 있는 상수의 집합을 고유한 이름으로 추상 표현하고 싶은 경우에 효과적인 방법이다. 열거를 사용하면 개발자는 열거자가 가지는 상수가 아닌, 열거가 갖는 의미에 주목해야한다. 열거자의 의미론은 시스템 설계자에게 맡길 수 있다.

열거형은 enum 키워드를 사용하여 선언다. 구문 및 취급 방법은 구조체와 유사하기 때문에 어려운 것이 없다.

열거형 선언

enum 태그명 {
열거1 = 상수, 열거2 = 상수 ...
} 열거 변수;

태그명과 끝에 열거 변수의 선언은 구조체나 공용체와 마찬가지로 생략할 수 있다. 열거자는 상수에 미치는 식별자이다. 여기에는 C 언어 식별자 명명 규칙이 적용되지만 관행적으로 모든 대문자로 한다. C 언어의 대문자와 소문자를 구별하는 언어에 있어서, 상수와 변경 불가능한 정적 변수의 식별에는 대문자를 사용하는 관행이 있다. 이것은 다른 변수와 같은 식별자와 구별하기 위해서이다.

상수는 열거자가 보유하는 상수를 지정한다. 여기에는 숫자 상수, 또는 문자 상수를 지정할 수 있다. 열거자의 상수는 생략할 수 있으며, 상수가 생략되는 경우에는 그 이전 열거자 값을 증가 값이 자동으로 설정된다. 첫번째 열거자 상수가 생략되는 경우는 0이 주어진다.

enum Message { MSG_YES , MSG_NO };

예를 들어, 이 열거는 MSG_YES는 0을 MSG_NO는 1을 나타내는 상수로 취급할 수 있다.

enum Message { MSG_YES , MSG_NO , MSG_OK = 8 , MSG_CANCEL };

그러나, 이 경우는 MSG_OK에 8이라는 상수를 명시적으로 부여하고 있기 때문에 MSG_CANCEL는 9를 나타낸다. 열거형의 변수를 만들 수 있지만, 이 변수는 실질적으로는 int 형에 지나지 않는다. 열거는 구조체의 멤버와는 성격이 다르다. 열거자는 멤버가 아니라 단순히 상수의 별명이라고 인식한다.

코드1

#include <stdio.h>

enum Message { MSG_OK , MSG_YES = 2 , MSG_NO };

int main() {
 enum Message msg = MSG_NO;
  printf(
   "MSG_OK = %d : MSG_YES = %d : MSG_NO = %d\n" ,
   MSG_OK , MSG_YES , msg
  );
  return 0;
}

코드1에는 열거형 Message를 작성하고 있다. 이 열거형은 3가지의 열거자 MSG_OK, MSG_YES, MSG_NO을 가지고 있다. 실행 결과를 보면, 열거형의 선언으로 MSG_YES에는 2라는 상수를 명시적으로 지정하고 있기 때문에, MSG_NO은 이에 영향받아서 3이라는 값을 나타내고 있는 것을 확인할 수 있다. 이와 같이 열거 상수를 생략할 수 있기 때문에, 상수 자체가 아니라 상수에 미치는 논리적인 의미에 관심이 있는 경우에 매우 유용하다.

코드1을 보고 궁금한 것이 몇가지 있을 것이다. 먼저 열거형의 msg 변수의 존재이다. 이 변수는 초기화시 MSG_NO을 주고 있지만, 만약 다른 값을 주면 어떻게 될 것일까? 사실은 열거형 변수의 실체는 단순한 int 형이다. 열거자 이외에 것을 주어도 오류가 발생하지 않고, 컴파일러는 그것을 감시하지 않는다.

enum Message msg = 100;

이와 같이 작성해도 컴파일러는 아무 일도 없는 것처럼 컴파일이 된다. 또한 구조적인 설계를 중시하는 프로그래머는 Message.MSG_OK와 같은 상수에 대한 액세스를 선호할지 모르지만, 유감이지만 이것은 불가능하다. 열거자는 멤버가 아니므로, 구조체와 같은 액세스는 할 수 없다. 이 사양은 이상하다고 느낄 수 있지만, 열거 변수에 대입된 값을 실행시에 체크해 버리면, 속도를 중시하는 C 언어의 기본 방침에 위반되 버린다. 주어진 상수가 올바른 것인지를 조사하는 것은 개발자의 역할로 여겨진다.

코드2

#include <stdio.h>

enum { MSG_OK , MSG_YESNO };
enum { ID_OK = 1 , ID_YES , ID_NO };

int Message(char *msg , int type) {
  char ch;
  switch(type) {
  case MSG_OK:
    printf("%s\tPush Enter>" , msg);
   scanf("%c" , &ch);
    return ID_OK;
 case MSG_YESNO: 
    printf("%s y/n>" , msg);
    scanf("%c" , &ch);
    return (ch == 'y' ? ID_YES : ID_NO);
  }
 return 0;
}

int main() {
  Message("Stand by Ready!" , MSG_OK);
  if (Message("Are you sure that's enough aromr?" , MSG_YESNO) == ID_YES)
    printf("This is not your appointed time to die.\n");
 return 0;
}

이 프로그램의 Message() 함수는 사용자가 지정한 어떤 문자열을 메시지로 표시하고 입력을 기다린다. type 인수가 MSG_OK의 경우는 입력을 기다리지만 입력된 값은 평가하지 않고 ID_OK를 항상 반환한다. type 인자에 MSG_YESNO을 지정하면 y 또는 n의 입력을 촉구하는 사용자에게 선택을 하게 한다. 사용자가 입력한 결과에 따라 함수는 ID_YES 또는 ID_NO를 돌려준다.

메시지를 표시하는 목적은 통일되어 있기 때문에, 이러한 기능은 Message()라는 하나의 함수로 정리하여 상수로 동작을 분기시키는 방법이 가장 현명하다고 생각된다. 기본적인 함수의 동작이 동일하다하더라도 분할되어 버리는 것은 현명하지 않다.

코드1은 열거를 사용한 간단한 실습 예제이다. 많은 C 언어로 작성된 시스템은 이러한 상수에 논리적인 의미를 규정하여 정보의 속성과 동작의 요구 등을 할 수 있다.

1.7.8 - C 언어 | 구조체 선언 | 형식의 별명 - typedef

typedef 지정자를 이용한 형식의 별명을 정의하는 방법을 소개한다. 이에 따라 기존의 자료형에 대해서 간단한 이름을 주는 곳이 있으며, 복잡한 구조체와 포인터 형의 존재를 은폐할 수 있다.

고유한 유형

C 언어에서는 개발자가 새로운 고유한 형식을 선언하는 수단이 존재한다. 정확하게는 기존의 자료형에 대해 다른 별명을 준다는 것이다. 이미 존재하는 자료형에 다른 이름을 붙이는 것은 무의미하다고 느낄지도 모른다. 그러나 사용법에 따라서는 코드를 단순하고 변경에 강한 유연하고 보수성이 높은 프로그램을 제공한다.

C 언어는 모든 시스템에서 구현되는 국제적인 프로그래밍 언어이다. 특히 표준화된 함수만을 사용하는 것이라면 이론적으로는 이식할 필요없이 다른 시스템에서도 컴파일할 수 있어야 한다. 그러나 다른 시스템은 32비트 컴퓨터도 모르고, 64비트 컴퓨터일지도 모른다. 이러한 다른 시스템에 이식 작업을 최소화하는 방법으로 자료형의 별명을 사용할 수 있다.

예를 들어, 프로그램 코드는 int 형이 32비트인 것으로 가정되므로써, 이를 16비트 컴퓨터에서 실행해도 올바른 동작은 기대할 수 없다. 그래서 32비트 정수를 DWORD, 16비트 정수를 WORD 형으로 소스를 통일하면 다른 시스템에 이식도 간단한다. 32비트 컴퓨터에서는 DWORD 형을 int 형의 별명으로 하고, WORD 형을 short int 형의 별명으로 정의하면 실체가 추상화된다.

이것을 다른 시스템에 이식하는 경우, 해당 시스템에서 32비트와 정의되는 자료형에 DWORD으로하고, 16비트와 정의되는 자료형에 WORD라는 별칭을 지정한다. 그러면 나머지 소스는 전혀 변경할 필요가 없어지는 것이다. 이것은 이식성 향상에 중요한 기술이 될 것이다.

기존의 형태에 별명을 붙일 때에는 typedef 지정자를 사용한다. typedef에 의해 형식에 대한 새로운 식별자를 정의할 수 있다. 새로운 유형 식별자를 정의한 후 기존 형식을 사용할 수 없게 되는 것은 아니다.

typedef 지정자

typedef 기존의형명 새로운 형명;

기존의 형명은 int와 char * 같은 이미 존재하는 형태의 이름을 지정한다. 새로운 형명에 아직 존재하지 않는 형태의 식별자를 지정한다. 예를 들어 unsigned char의 별명은 다음과 같이 정의할 수 있다.

typedef unsigned char BYTE;

이 선언은 unsigned char 형의 별명으로 BYTE 형을 정하고 있다. typedef 키워드는 형식을 선언할 수있는 곳이면 지정할 수 있지만, 별명은 typedef 키워드보다도 이후에야 사용할 수 없기 때문에, 일반적으로 함수보다 이전 소스의 맨 위에 한다. 기존의 형명은 이미 존재하는 형식이면 무엇이든 상관 없다. 이전 typedef에 의해 정의된 형명도 지정할 수 있다.

코드1

#include <stdio.h>

typedef unsigned char BYTE;
typedef int DWORD;

struct Color {
  BYTE r , g , b;
};

DWORD main() {
 struct Color color = { 0xFF , 0xAA , 0xAA };
  printf("R = %d : G = %d : B = %d\n" , color.r , color.g , color.b);
  return 0;
}

이 프로그램에는 unsigned char 형의 별명 BYTE으로 하고, int 형의 별명을 DWORD를 정의하고 있다. Color 구조체와 main() 함수에는 이것들을 typedef 명을 사용하여 프로그램되어 있다. 그러나 BYTE형도 DWORD형도 그 실체는 C 언어 간단한 형인 unsigned char와 int이다. 이건 그냥 별명에 불과하다는 것을 잊지 말자.

포인터 형의 별명은 기존의 형명 후에 * 를 지정하면 한다. char 형 포인터의 별칭 STR을 작성하는 경우에

typedef char * STR;

위와 같이 작성한다. 또한 typedef의 새로운 형명 콤마 “,“로 구분하여 한번에 여러 별명을 줄 수 있다. 예를 들어 다음과 같이 작성하는 것으로, char 형의 별명과 char 형 포인터의 별명을 한번에 정의할 수 있다.

typedef char STR , *PSTR;

이것은 char형의 별명 STR과 char형 포인터의 별칭 PSTR가 새롭게 정의되고 있다. 일반적으로 문자열 포인터를 작성하는 경우는 char *와 같이 쓴다고 생각하지만, 이렇게 포인터 형의 별명을 작성하면 PSTR을 지정하는 것만으로 포인터 변수를 선언할 수 있다.

코드2

#include <stdio.h>
typedef char STR , *PSTR;

int main() {
 STR str[] = "Kitty on your lap";
  PSTR pstr = str;
  printf("%s\n" , pstr);
 return 0;
}

코드 2는 char 형의 별명 STR과 char *형의 별명으로 PSTR를 정의하고, 이를 main() 함수에서 실제 사용하고 있다. str[] 변수는 STR 형이므로 실체는 char 형의 배열이다. pstr 변수는 PSTR 형이므로 char *의 변수라고 생각할 수 있다. 이와 같이 포인터에 별명을 구사하여 포인터의 존재를 어느 정도 은폐할 수 있다.

특히 typedef가 위력을 발휘하는 것은 구조체나 공용체의 별명을 만들 때이다. 단순형의 별명에 큰 메리트가 느껴지지 않을지도 모르지만, 구조체의 별명을 작성하면 소스의 가독성 향상과 생산성에 연결할 수 있다.

기존 구조체의 태그명에서 구조체의 인스턴스를 생성하는 작업은 귀찮은 일로 struct 키워드를 명시적으로 지정할 필요가 있었다. 이는 길고 번거로운 구문으로 대부분의 프로그래머는 이렇게 낭비인거 같은 struct나 union 키워드의 지정을 싫어한다. 그래서 typedef에 의해 구조체나 공용체에 새로운 형명을 할당하게 된다. 이렇게 하면 struct나 union 키워드를 지정하지 않고, 형식 이름만으로 인스턴스를 만들 수있게 될 것이다.

struct tag_Point { int x , y; };
typedef struct tag_Point Point;

이처럼 구조체의 별명을 정의할 수 있다. 태그명으로 인스턴스를 만들려면 struct tag_Point를 작성하지 않으면, 컴파일러는 인식을 하지 못했다. 그러나 struct tag_Point 형에 별명으로 Point를 지정하면 Point를 지정하는 것만으로 tag_Point 구조체의 인스턴스를 만들 수 있게 된다.

그러나 tag_Point은 여기서 밖에 사용하지 않는 태그명이므로 그다지 필요가 없다. 태그명이 그 후에 요구되지 않는다면, 무명 구조체에 별명을 주는 것이 효율적이다. 구조체 선언 시에 다음과 같은 구문을 이용하여 동시에 별명을 정의할 수 있기 때문에 무명 구조체에 별명을 줄 수도 있다.

typedef 지정자에 구조체 별명

typedef struct 태그명 { 멤버선언 } 새로운형명;

구조체 선언에서 태그명은 생략할 수 있기 때문에, 필요하지 않은 경우 무명 구조체를 선언하고 동시에 새로운 형명을 줄 수 있다. 많은 C 언어 프로그래머는 구조체에 별명을 부여하는 것을 선호하고, 구조체를 선언할 시에 typedef를 사용하여 별명을 부여한다.

코드3

#include <stdio.h>
typedef struct { int x , y; } Point;

int main() {
 Point po = { 200 , 50 };
  printf("X = %d : Y = %d\n" , po.x , po.y);
 return 0;
}

코드3은 int 형의 멤버 x와 y를 가진 무명 구조체에 Point라는 별명을 부여하고 있다. 많은 프로그래머는 이렇게 태그명 대신에 typedef 명을 구조체로 지정한다. 이것이 struct 키워드를 지정할 필요가 없기 때문에 구조체 변수의 선언이 쉬워진다. 공용체에 별명을 붙이는 경우도 동일하다.

함수형과 별명

사실은 C 언어에서는 함수도 하나의 형으로 해석되고 있다. 함수의 형은 반환 값과 인수 목록으로 구성되어 있다. 하지만 함수 자체가 식별자(즉, 함수명)으로 판단되기 때문에, 평소에는 이를 의식하는 것은 아니다. 하지만 함수형이라는 것을 의식하면, 함수와 함수형의 인스턴스으로써 생각할 수 있는 것이다.

예를 들어, 다음과 같은 함수가 선언되는 경우를 생각해 보자.

int Function1(char * , float);
int Function2(char * , float);

이 경우 Function1() 함수와 Function2() 함수는 같은 함수형이라고 표현할 수 있다. 물론 이러한 함수의 정의에 상호 관계는 존재하지 않는다. 이 함수는 독립적이며 물리적 연결 같은건 아무것도 없다. 유일한 공통점은 형태가 같다는 것이다.

이처럼 함수를 형태라는 관점에서 바라 볼 수 있는 것이다. 그러면 typedef에 의해 함수형의 이름을 붙일 수 있다고 한다면 재미있는 발상이 떠오르는 것이다. 사실 이것은 구문적으로 가능해지고 있다. 다음과 같이 작성하여 함수형에 이름을 붙일 수 있는 것이다.

typedef 지정자에 의해 함수형의 별명

typedef 반환값 새로운형명(인수 목록);

이는 매우 흥미로운 시도이다. 함수형에 이름을 붙이면, 형명 및 함수명만으로 함수를 선언할 수 있다.

코드4

#include <stdio.h>

typedef void TEXTOUT(char *str);
TEXTOUT println;

int main() {
  println("Kitty on your lap");
 return 0;
}

void println(char *str) {
 printf("%s\n" , str);
}

코드4의 println() 함수에 주목해 보자. 이 println() 함수는 TEXTOUT 형의 함수로 해석할 수 있는 것이다. TEXTOUT 형은 typedef 지정자로 만들었다. 반환값은 void으로 하고, 매개 변수가 char *형의 함수형의 별명이다. TEXTOUT println이라는 함수 선언자는 void println (char *str)라는 선언에 동일하다고 생각할 수 있다.

1.7.9 - C 언어 | 구조체 선언 | 형의 사이즈 - sizeof

값 또는 정의된 형식에서 해당 인스턴스를 저장하는데 필요한 저장 공간의 사이즈를 얻으려면 sizeof 연산자를 사용한다.

값의 크기를 얻기

어떤 변수도 구조체나 공용체의 인스턴스에 값을 저장하기 위한 메모리 공간이 할당되어 있다. 예를 들어 int 형은 32비트 컴퓨터에서 4바이트의 메모리 영역을 할당한다. 즉, int 변수를 사용하면 메모리를 4바이트 소비할 것이다. 요즘에는 메모리의 대용량화에 따라, 개발자가 용량을 크게 걱정할 필요가 없게 되었지만, 메모리는 프로그래밍에서 중요한 요소이다.

C 언어에서 변수와 형식에서 사이즈를 얻을 sizeof 연산자를 사용해 형태 크기를 확인할 수 있다. sizeof 연산자는 지정한 값의 바이트 수를 돌려준다. sizeof가 반환하는 정수 값을 사용하여, 포인터 연산과 구조체의 작업에 응용할 수 있다.

sizeof 연산자

sizeof 식

이것으로 식으로 지정된 변수와 형식의 메모리 크기를 얻을 수 있다. sizeof 연산자로 지정하는 식은 일반적으로 어떤 식별자(변수명)이다.

코드1

#include <stdio.h>

int main() {
 char c;
 short si;
 int i;

  printf("char = %d : short = %d : int = %d\n" ,
   sizeof c , sizeof si , sizeof i
 );
  return 0;
}

코드1에는 char 형 변수 c, short 형의 변수 si, 그리고 int형의 변수 i의 크기를 sizeof 연산자 조사하고 이를 표시한다. 결과는 시스템에 따라 다르지만, char의 크기는 반드시 1이 될 것이다. 만약 32비트 컴퓨터이면 int형의 크기는 4바이트, short는 2바이트인 것으로 예상된다. sizeof 연산자를 사용하여 사용중인 시스템에서 C 언어 단순형이 몇 바이트의 메모리를 할당하고 있는지를 정확하게 확인할 수 있다.

그러나 형의 사이즈를 확인하고 싶은 것 뿐인데, 일부러 사용하지 않는 변수를 작성하는 것은 낭비이다. sizeof 연산자는 식을 지정 이외에 괄호로 둘러싼 형를 지정할 수 있다. 따라서 값이 아닌 형명에서 크기를 확인할 수 있다.

sizeof 연산자

sizeof (형식 지정자)

형식 지정자는 단순형뿐만 아니라, 구조체 형이나 typedef 명을 지정할 수 있다.

코드2

#include <stdio.h>

typedef struct { int x , y; } Point;
typedef union {
  char c;
 short si;
 int i;
} Value;

int main() {
  printf("Point = %d : Value = %d\n" , sizeof(Point) , sizeof(Value));
 return 0;
}

코드2는 sizeof 연산자에 형을 지정한다. 그러면 Point 구조체와 Value 공용체의 바이트 크기를 얻을 수 있다. Point 구조체는 sizeof (int) * 2와 같고, Value 공용체는 사이즈가 가장 큰 멤버 int에 동일한 크기임을 확인할 수 있다.

1.7.10 - C 언어 | 구조체 선언 | 자동 변수 - auto

변수 선언시에 auto 키워드를 사용하여, 해당 변수가 선언된 복합문의 범위에서만 유효한 자동 변수임을 명시적으로 지정할 수 있다. 그러나 선언된 변수는 기본적으로 자동 변수이며, 현대에서는 auto 지정자를 사용하는 것은 의미가 없다.

로컬 수명을 가지는 변수

C 언어 변수 선언에는 변수의 수명을 명시적으로 지정하는 기억 클래스라 불리는 종류의 지정자를 사용할 수 있다. typedef는 사실은 기억 클래스의 일종이다. 그러나 typedef는 비교적 특수한 기억 클래스로, 본래의 기억 클래스는 변수가 어느 타이밍에 메모리에서 해제될 것인지를 결정하는 것이다.

기억 클래스 지정자

기억클래스지정자 형 변수형 ...

기억 클래스 지정자는 동일한 선언내에 다른 지정자를 조합해 지정할 수 없다.

함수 등의 복합문의 내부에서 선언된 변수를 자동 변수라고 한다. 자동 변수는 그 블록 내에서만 유효한 변수로 제어가 블록에 들어간 시점에서 초기화되고, 블록에서 벗어날 때 해제된다. 변수의 유효 범위에 대해서는 “변수의 유효 범위“에서 자세히 설명하고 있다. 기억 클래스는 변수를 선언하는 위치에 따라 자동으로 결정된다. 복합문 내부에서 선언된 변수는 컴파일러가 자동 변수라고 판단 해준다.

그러나 auto 지정자에 의해서 명시적으로 자동 변수를 선언 할 수 있다. 다만, 이 키워드를 적극적으로 이용하는 것에 장점이 없기 때문에 일반적으로 생략된다. 현재는 auto 키워드가 원래 의미로 사용되는 경우는 거의 없다.

auto 기억 클래스 지정자

auto 형 변수명 ...

기억 클래스로 선언시에 auto를 지정하면 변수가 자동 변수임을 명시적으로 지정할 수 있다.

C ++ 표준 규격인 ISO/IEC 14882:2011, 통칭 C++11에서 auto 키워드가 형 추론을 나타내는 다른 의미로 사용되도록 되었기 때문에, auto 지정자를 사용하는 C 언어의 코드를 C++ 언어로 컴파일하면 오류가 발생한다. auto 키워드의 의미는 C 언어와 C ++ 언어로 다르다는 점에 유의하자.

코드1

#include <stdio.h>

int main() {
 auto char *str = "Kitty on your lap";
 printf("%s\n" , str);
  return 0;
}

코드1의 str 변수는 auto 기억 클래스 지정자를 사용하여 명시적으로 자동 변수임을 나타내고 선언되어 있다. 그러나 지금까지 학습해 온 것을 되돌아 보면 알 수 있듯이, auto는 생략해도 아무런 문제가 없다. 따라서 이것은 생략되어야 한다. C 언어 사양으로 함수에서 선언된 변수는 auto로 된다는 것이 원칙이다.

1.7.11 - C 언어 | 구조체 선언 | 레지스터 변수 - register

일반적으로 변수는 주기억 장치의 기억 영역에 해당되지만, register 지정자에 의해 선언된 레지스터 변수는 CPU의 빠른 레지스터라는 기억 영역에 배치된다. 다만, 실제로 레지스터를 사용할 것인지의 판단은 컴파일러에 맡길 수 있다.

레지스터에 저장하기

함수 내에 있는 변수가 자주 참조되는 경우가 종종 있다. 예를 들어, 그래픽 등 큰 정보를 반복 문장으로 제어하는 경우에 처리해야 픽셀의 변환에 사용되는 매개 변수를 저장하는 변수는 자주 액세스될 것이다. 이와 같이, 일정한 시간 내에 수천 수만 번 참조가 예상되는 변수는 조금이라도 빠른 메모리에 저장해야 한다 생각할 수 있다.

그래서 C 언어에는 이러한 변수를 레지스터 변수로 선언할 수 있다. 레지스터 변수는 컴퓨터의 내부에서 가장 빠른 레지스터라고 불리는 CPU의 저장 공간에 정보를 저장하는 것을 나타낸다.

보통의 변수는 주기억 장치에 저장된다. 주기억 장치는 우리가 평소 메모리라고 부르고 있는 장소로 하드 드라이브 등의 파일 저장용 보조 기억 장치에 비해서 고속으로 동작한다. 이보다 더 고속으로 동작하는 캐시 메모리도 존재하고 있지만, 이것들을 이용하는 것은 시스템으로 물리적 구조를 시스템이 은폐되어 있기 때문에 응용 프로그램은 사용할 수 없다.

그리고, 주기억 장치와 캐시 메모리 이상 빠르게 작동하는 메모리 레지스터이다. 왜냐하면 레지스터는 CPU에 포함된 메모리이기 때문이다. 주기억 장치 등의 정보를 처리할 경우, CPU가 계산을 위해 값을 가져와야 한다. 이 처리는 C 언어로 볼 수 없지만, 기계어에는 하나의 명령이 된다. 계산을 위해 메모리에서 CPU에 값을 읽거나 계산 결과를 메모리에 옮기는 처리도 CPU와 메모리의 통신 회로에 의존하는 시간이 소요될 것이다.

레지스터에 값이 저장 있다면, 이런 시간을 단축할 수있을 것으로 기대한다. 따라서 자주 사용되는 변수는 주기억이 아닌 레지스터에 저장하는 것을 권장한다. 레지스터 변수를 선언하려면 register 지정자를 지정한다.

register 지정자

register 형 변수명 ...

레지스터의 수와 레지스터의 사이즈 등은 물리적 컴퓨터에 의존하는 문제이다. 컴파일러는 register 선언을 무시하는 것이 허락되어 있으며, 레지스터에 저장이 불가능하다면 일반 변수로 처리된다. 일반적인 해석으로는 하나의 함수 내에 2개 정도의 레지스터 변수가 적당하다고 생각할 수 있다. 다만, 레지스터 변수를 과도하게 선언해도 문제는 없다.

레지스터 변수의 주의점으로는 레지스터 변수는 주기억에 배치되지 않는다는 것이 원칙이므로 주소를 요구할 수 없다.

코드3

#include <stdio.h>

int main() {
 register int i;
 register int k;

 i = 5;
  k = i * 3;

  printf("i = %d, k = %d\n" , i, k);
 return 0;
}

코드1에서는 int 형의 레지스터 변수 i와 k를 선언하고 있다. 이 변수의 값이 정말로 레지스터에 저장되는지 여부는 컴파일러에 의존하는 문제이다. 컴파일러는 register 지정자 자체를 무시할 수 있다.

최근의 컴파일러가 생성하는 코드는 최적화가 진행되고 있기 때문에, 능숙하지 않은 개발자는 register 지정자를 따로 지정할 필요는 없고, 일반 코드에도 충분히 최적화된다. 따라서 현재에는 auto 지정자와 마찬가지로 register 지정자도 거의 이용되지 않는다.

1.7.12 - C 언어 | 구조체 선언 | 정적 변수 - static

일반 로컬 변수는 함수가 종료되면 값을 없어지지만 static 지정자를 이용한 정적 변수는 영구적인 수명을 가지고 있다. 함수 내에서만 사용하는 로컬 변수이면서, 글로벌 변수와 마찬가지로 응용 프로그램이 종료될 때까지 변수의 값을 계속 유지한다.

영구 로컬 변수

일반적으로 함수 안에서 선언된 함수는 로컬 수명을 가진다. 그것은 함수가 실행될 때에 초기화되어 함수가 제어를 돌려줄 때에 개방된다는 것이다. 이것은 함수가 계산을 수행하는데 필요한 일시적인 저장 영역에 적합하다.

그러나, 때로는 프로그램은 더 복잡한 장기 처리를 요청할 수 있다. 예를 들어, 함수가 여러번 호출되었는지를 저장해야 한다는 요구가 주어진다면 어떻게 해야 할까? 함수의 참조 횟수를 세는 것에는 그 함수 내부에서 전용으로 카운터 변수를 증가하는 방법이 가장 확실하지만 문제는 이 변수의 기억 클래스이다.

자동 변수라면, 함수가 종료할 때 정보가 손실되어 버린다. 이것으로는 자신이 몇 번 호출되고 있는지를 관리할 수 없기 때문에 전역 변수로 카운터를 사용하는 것을 생각할 것이다. 그러나 전역 변수는 다른 함수에서 잘못된 값을 변경되어 버릴 가능성이 있다. 함수가 이 전역 변수를 사용하여 참조 횟수를 세어서 정말 그 값이 정확한지는 보장할 수 없다.

그래서 정적 변수를 사용한다. 정적 변수는 자동 변수와 달리 영구적인 수명을 가지고 있다. 즉, 전역 변수처럼 프로그램의 시작시에 초기화되며 프로그램이 종료할 때까지 값을 유지하는 것이다. 함수의 내부 변수를 정적 변수로 선언하여 함수가 종료되도 해제되지 않은 변수를 만들 수 있다. 게다가, 이 변수의 가시성은 자동 변수와 같고, 다른 함수에서 액세스할 수 없다. 정적 변수를 선언하려면 static 지정자를 사용한다.

static 지정자

static 형 변수명 ...

이와 같이 변수를 선언하면, 이 변수는 글로벌 수명을 가지게 된다. 함수 내부에서 선언된 경우는 함수가 종료되어도 변수가 파기되는 것은 아니다. 함수의 외부, 즉 외부 레벨에서 선언된 경우에 대해서는 “외부 레벨 선언"에서 자세히 소개한다.

코드1

#include <stdio.h>

void ShowCount(void) {
 static int iCount = 0;
  printf("iCount = %d\n" , iCount++);
}

int main() {
 ShowCount();
  ShowCount();
  ShowCount();
  return 0;
}

코드1의 ShowCount() 함수는 이 함수의 참조 횟수를 표시한다. 함수의 참조 횟수를 기억하기 위해서 함수 내부에서 static 기억 클래스를 가진 숫자 변수 iCount을 선언하고 있다. 보통의 변수는 함수가 호출될 때마다 초기화되어 버리지만, 이 변수는 static을 가진 정적 변수이므로 초기화는 한번 밖에 수행되지 않는다. 실행 결과에서 확인할 수 있듯이 함수를 호출마다 정적 변수 iCount 증가하여 참조 횟수를 정확하게 저장하고 있다.

static을 지정한 정적 변수는 이 밖에도 초기화의 부담을 완화하는 수단으로 이용된다. 항상 같은 값을 나타내는 변수의 경우는 함수를 실행할 때마다 초기화하는 오버 헤드를 피할 수 있을 것이다. 항상 같은 문자열을 나타내는 char 형배열 등에 유효하다.

1.7.13 - C 언어 | 구조체 선언 | 외부 레벨 선언 - extern

extern 기억 클래스 지정자를 사용하여 함수의 외부에서 선언된 전역 변수를 여러 개의 소스 파일로 공유하는 방법, 또는 static 기억 클래스 지정자를 사용하여 선언된 파일 내에서 사용되고, 다른 소스 파일에서는 볼 수 없도록하는 방법을 소개한다.

전역 변수에 대한 참조

변수의 유효 범위“에서 설명한 바와 같이, 함수의 외부 공간을 외부 레벨이라고 하고, 외부 레벨에서 수행된 선언을 외부 레벨 선언이라고 한다. 반대로, 함수의 내부 레벨이라고 자동 변수 등의 선언을 내부 레벨 선언이라고 한다. 내부 레벨과는 달리 외부 레벨의 가시 공간은 전체 파일이다. 그 C 언어 소스 파일의 모든 함수에서 외부 레벨에서 선언된 변수 등에 액세스할 수 있다.

외부 레벨 선언의 가시성은 파일 범위(scope)로 간주한다. 즉, 외부 레벨에서 선언한 변수와 함수는 그 선언을 한 소스 파일의 어느 위치에서도 액세스할 수 있다는 것이다. 다만, 선언을 하기 전에 함수 등에서 액세스할 수 없다. 따라서 일반적으로 파일의 시작에 필요한 선언을 하게 된다. 함수는 항상 외부 레벨 선언이고, 함수 내부에서 함수를 선언할 수 없다.

선언되기 전 위치에서 변수나 함수를 사용할 수 없다는 것은, 예를 들면 다음과 같은 경우이다.

void Function() {
  printf("%s\n" , str);
}
char *str;

이 경우 Function() 함수에서 str 변수를 참조하고 있지만, str 변수는 Function() 함수보다 뒤에 선언되어 있다. 따라서 Function() 함수에서는 str 변수를 인식할 수 없다. str은 정의되지 않은 심볼로 해석되어 버리는 것이다. 함수이면 이러한 문제는 프로토타입의 선언으로 극복할 수 있었다.

선언의 위치가 불명확한 외부 레벨의 변수에 액세스하려면, 기억 클래스 지정자의 일종인 extern 지정자를 사용한다. 이 기억 클래스를 갖는 변수는 프로그램을 구성하는 C 언어 소스 파일 중 하나에서 외부 레벨에서 정의된 동명의 변수에 대한 참조이다. 즉, 함수 선언자의 변수 버전과 같은 것이라고 생각하면 된다. extern으로 선언된 변수는 그 시점에는 메모리를 확보(초기화)하지 않지만, 소스 어딘가에 동명의 외부 레벨 변수가 존재하는 것을 컴파일러에 통지한다.

extern 지정자

extern 형 변수명 ...

extern 지정자를 갖는 변수 선언은 초기화되지 않는다. extern으로 선언된 변수는 다른 곳에서 실체가 정의되어 있는지를 나타낸다.

또한 외부 레벨 선언으로 auto와 register를 사용할 수 없다. 외부 레벨 선언은 그 성질상 자동 변수가 될 수도 없고, 장기적으로 CPU의 레지스터를 차지할 수도 어렵다고 생각되기 때문이다.

코드1

#include <stdio.h>

extern char *str;
int main() {
 printf("%s\n" , str);
  return 0;
}
char *str = "Kitty on your lap";

코드1에는 파일의 시작 부분에 extern 기억 클래스를 가진 변수 str을 선언하고 있다. str 변수의 실체는 main() 함수보다 뒤에 초기화되어 있지만, extern 의해 참조가 생성되어 있기 때문에, main() 함수에서 str 변수를 참조하는 것이 허용되고 있다. 이 프로그램은 문제없이 컴파일할 수 있을 것이다.

extern 기억 클래스 지정자는 내부 레벨의 변수로도 지정할 수 있다. 이 경우 변수가 선언된 블록에서 지정된 외부 레벨에 대한 참조를 사용할 수 있다. 당연히, 블록 외부에서 extern을 지정한 변수가 보이지 않기 때문에 참조는 사용할 수 없다.

코드2

#include <stdio.h>

int main() {
  {
   extern char *str;
   printf("%s\n" , str); /* OK */
 }
 /*printf("%s\n" , str);  /* error */
 return 0;
}
char *str = "Kitty on your lap";

코드2는 main() 함수 내에서 중첩된 익명의 블록으로 extern 기억 클래스를 가진 변수 str을 선언하고 있다. 블록 내에서 이 extern을 지정한 str에 대한 참조를 사용할 수 있다. 블록 밖에서는 str에 대한 참조를 유지하지 않기 때문에 main() 함수의 주석 처리된 printf() 함수를 컴파일하면 오류가 될 것이다. 어느 한 곳에 전역 변수를 시각화하고 싶은 경우에 유효하다.

정적 전역 변수

static 기억 클래스 지정자와 가진 글로벌 변수는 그 변수가 선언된 파일 내에서만 참조할 수 있다는 뜻이다. 사실 여러 C 언어 파일을 컴파일하면 다른 파일에서 extern 기억 클래스를 사용하여 전역 변수로 참조 할 수 있다. 예를 들어 다음과 같은 경우이다.

코드3

int iValue = 0xFF;
void Function(void);

int main() {
  Function();
 return 0;
}

코드4

#include <stdio.h>

extern int iValue;
void Function() {
 printf("iValue = %d\n" , iValue);
}

코드3과 코드4를 동시에 컴파일하는 경우, 코드4는 iValue 변수를 extern 기억 클래스 지정자를 사용하여 선언하고, 코드3의 iValue 변수에 참조하고 있다. 여러 파일을 컴파일하는 방법은 사용하는 컴파일러의 도움말을 참조한다. Boland C ++ Compiler에서 여러 개의 소스 파일을 컴파일하려면 하나 이상의 공백으로 구분된 파일 이름을 지정한오.

BCC32 test1.c test2.c

다른 파일의 특정 전역 변수에 액세스하려면 extern을 사용하면 해결할 수 있지만, 경우에 따라서는 이것이 바람직하지 않은 경우도 있다. 어느 정도의 신뢰성이 필요한 글로벌 변수와 해당 파일의 함수 사이의 정보 전달에만 사용 목적 전역 변수라면, 다른 개발자가 만든 가시성이 있는 다른 소스 파일에서 액세스할 수 있는 것은 바람직하지 않다.

따라서 특히 공개가 필요없는 글로벌 변수는 static 기억 클래스 지정자를 설정하고 외부 파일에서 액세스 할 수 없도록 구조이다. 이렇게 함으로써 식별자의 충돌 등, 불필요한 오류와 오해에 의한 버그를 피할 수 있다.

코드5

#include <stdio.h>

static int iValue = 0xFF;

int main() {
  printf("iValue = %d\n" , iValue);
  return 0;
}

코드5의 iValue 변수는 static 기억 클래스와 글로벌 변수이다. 이 변수는 이 파일에서만 참조할 수 있으며, 외부 파일에서 extern 등을 사용하여 참조할 수 없다. 이렇게 함으로써 식별자의 예기치 않은 충돌을 피할 수 있다. 코드5와 코드4를 동시에 컴파일해도, 코드4에서는 iValue를 참조할 수 없기 때문에 오류가 발생할 것이다.

1.7.14 - C 언어 | 구조체 선언 | 함수의 기억 클래스

함수 선언에서는 extern 지정자와 static 지정자를 사용하여 기억 클래스를 지정할 수 있다. 대상의 함수를 다른 소스 파일에서 호출할 수 없게 은폐하려면, static 지정자 함수를 선언하면 좋을 것이다.

함수의 가시성

함수는 변수와 달리 내부 레벨이라는 것이 존재하지 않는다. 즉, 함수는 항상 글로벌 지속되어야하며 동일한 파일이면 어디서든 참조할 수 있다. 물론 함수를 호출하기 위해서는 그 이전에 프로토타입 선언되지 않으면 안되는 것은 “함수의 선언"에서 해설했다.

그러나 변수뿐만 아니라 여러 파일을 동시에 컴파일할 때, 함수 이름 충돌 문제가 발생한다. 실은 이를 해결하기 위해 함수도 기억 클래스가 존재하고 있는 것이다. 함수에 지정할 수 있는 기억 클래스는 static과 extern 중 하나이다.

static 기억 클래스를 가진 함수는 그 함수가 정의되어 있는 파일의 함수에서만 호출할 수 있다. 다른 파일의 함수에서 static 함수를 호출할 수 없다. 반대로 extern 기억 클래스와 함수는 다른 파일의 함수에서 호출할 수 있다. 지금까지 처럼 함수의 기억 클래스를 생략한 경우 extern이 암시적으로 채용된다.

코드1

#include <stdio.h>

void Function1(void);
void Function2(void);

int main() {
 /*Function1();  /*error*/
 Function2();
  return 0;
}

코드2

static void Function1() {}
extern void Function2() {}

코드1과 코드2를 동시에 컴파일하면 main() 함수에서 Function2()를 호출 할 수 있지만 Function1() 함수는 코드1에서 보이지 않기 때문에 호출할 수 없다. 주석 처리되어 있는 Function1()의 주석을 해제하고 컴파일하면 오류가 있음을 확인할 수 있다. Function1() 함수는 static 기억 클래스를 가지고 있기 때문에 외부 파일에서 보이지 않는다.

이것은 어느 한 곳에서 사용하는 특정의 처리에 특화된 함수를 만들 때 유용한다. 특정 파일의 특정 처리에 특화된 독립성이 낮은 함수의 경우는 이름 충돌을 피하기 위해서 static 기억 클래스를 지정하는 것이 좋다.

1.7.15 - C 언어 | 구조체 선언 | 형식 한정자 - const

값을 변경할 수 없는 변수를 선언는 const 형식 한정자와 컴파일러에 최적화를 거부하는 volatile 형식 한정자를 소개한다.

상수화

지금까지 선언은 기억 클래스 지정자(static 등), 형식 지정자(int, char 등), 선언자(변수명, 함수명 등), 초기화(변수의 초기 값)을 조합하여 실시하는 라는 것을 설명했다. 형식 한정자는 이 외에도 선언 대상의 성질을 결정하는 키워드를 제공한다. 이것을 선언 이외에 감안할 때, C 언어 선언 구문은 항상 다음과 같은 형태로 간주할 수 있다.

선언

기억클래스지정자 형지정자 형한정자 선언자 초기화 ...;

기억 클래스 지정자, 형 지정자, 형식 한정자(type qualifier)는 어떤 순서도 상관없다. 또한 기억 클래스 지정자, 형 한정자, 초기화(Initializer)는 생략할 수 있다. 조합은 형식 지정자를 생략할 수 있다. 선언자는 변수의 이름 등을 지정하며, 지금까지 “식별자"라고 하였다.

형식 한정자에서 가장 많이 사용되는 것은 const 키워드이다. 이것은 초기화 이후에 이 변수가 변경되지 않는다는 것을 나타낸다.

const int ciValue = n;

이 경우는 정수형의 변수 ciValue는 앞으로 계속해서 저장한 n값이 변경되지 않는 것을 나타낸다. 만약 프로그램에서 이 변수의 내용을 바꾸려고 하면 컴파일 오류가 발생한다. 덧붙여서 const 형식 한정자를 이용한 선언으로 형식 지정자를 생략한 경우는 int 형으로 해석된다.

예를 들어 “Kitty"라는 문자열을 프로그램의 많은 곳에서 사용하는 경우, 그 때마다 리터럴 문자열을 지정하는 것은 매우 비효율적인 코드이다. 어느 날, 프로그램을 개선하기 위해 문자열을 “Kitten"로 변경해야 되면, 모든 리터럴 문자열을 찾아 변경해야 한다. 만약 1개라도 변경하는 것을 놓친다면, 버그가 될 가능성이 있다. 즉, 무결성이 낮은 프로그램이 되고 마는 것이다. 이것은 계산용의 정수형 상수를 작성하는 경우에도 마찬가지이다.

그래서 당신은 자주 사용되는 정보는 하나의 전역 변수 등을 초기화하고, 이를 여러 곳에서 사용할 수 있다. 이렇게 하면 프로그램의 사양 변경시에 수정하는 소스는 적어지고 정합성이 유지된다. 그러나 이렇게 되면 새로운 불안한 것이 생긴다. 변수는 누군가가(어떤 함수가) 잘못 변경되어 버릴 가능성이 있다. 그래서 const를 사용하여 변수를 보호한다.

많은 곳에서 참조되고, 변경되는 것을 원하지 않는 정적인 정보라고 하면 응용 프로그램의 이름과 버전 정보, 회사 이름 등일 것이다. 이러한 정보를 응용 프로그램이 보유하는 것은 매우 일반적이다.

코드1

#include <stdio.h>

typedef unsigned char BYTE;
const char APPNAME[] = "Kitty on your lap";
const VERSION = (15 << 8) | 7;

int main() {
  /* APPNAME[1] = 'X'; /*error*/
  /* VERSION = 0; /*error*/

 printf("APPNAME = %s\n" , APPNAME);
  printf(
   "VERSION = %d.%d\n" ,
    (BYTE)VERSION , (BYTE)(VERSION >> 8)
  );
  return 0;
}

코드1은 문자 배열의 변수 APPNAME와 정수 변수 VERSION를 외부 레벨에서 선언한다. 이러한 변수는 const 형 한정자가 지정되어 있기 때문에 상수처럼 그 값은 항상 동일하다. 이러한 변수는 표현식에서 참조는 일반 변수처럼 할 수 있지만, 대입할 수 없다. 주석 처리되어 있는 대입식을 컴파일하면 오류가 발생하는 것을 확인할 수 있을 것이다.

APPNAME 변수처럼 배열에 const를 지정한 경우, 배열의 각 요소에 대해 const가 걸린다. 또한 VERSION 변수처럼 const 형 한정자만 선언하면 const int 형으로 해석된다. VERSION은 하위 8비트에 메이저 버전을 다음의 8비트에 마이너 버전을 포함하는 16비트 정수 정보로 취급하고 있다.

이것은 const를 이용한 예이다. 이 밖에도 많은 곳에서 const는 이용된다. 예를 들어, 함수가 주어진 포인터를 변경하지 않는다면, 그것을 개발자에게 명시적으로 전달 수단으로 const 형의 인수를 선언하는 방법이 있다. 포인터 형에 const 형 한정자가 지정되어 있는 경우는 그 포인터가 아닌 포인터가 가리키는 값을 변경하지 않는 것을 나타낸다.

리터럴 문자열에 대한 포인터에서 간접 참조를 하며, 리터럴 문자열 값을 변경하려고 하는 경우의 결과는 부정이다. 이것은 어떤 개발자도 선호하지 않지만 구문에 틀린 것이 아니기 때문에 버그의 원인이 될 수 있다. 그래서 리터럴 문자열에 대한 포인터를 만들 때 경험있는 프로그래머는 const를 습관적으로 지정한다.

코드2

#include <stdio.h>

void Function(const char *str) {
  /* str[0] = 'X';      /* error */
 /* str = "당신의 무릎 위에 고양이"; /* OK */
  printf("%s\n" , str);
}

int main() {
 Function("Kitty on your lap");
  return 0;
}

코드2의 Function() 함수는 const 형 한정자를 가지는 문자열에 대한 포인터를 받는다. 포인터는 간접 참조를 사용하여 포인터가 가리키는 주소의 값을 변경할 수 없다. 그러나 const 포인터가 보유한 주소 자체는 변경할 수 있기 때문에 str 매개 변수에 다른 주소를 대입할 수 있다.

이와는 반대로 포인터를 상수화하고 싶은 경우가 있을 것이다. 즉, 포인터가 참조하는 변수의 값은 변경될 수 있으나, 포인터 자체는 변경해서는 안된다는 포인터이다. 코드2와 같이 const char *str와 같이 선언하면, 상수 데이터 포인터로 해석되기 때문에 포인터 자체는 고정되지 않는다. 즉, 다음과 같은 특성을 가지고 있다.

const char *str = buf1; /* 상수의 포인터 */
*str = 'a'; /* error */
str = buf2; /* OK */

상수 포인터는 간접 참조에 의해 참조된 값을 변경할 수 없는 포인터이며, 포인터 자체가 상수라는 것은 아니다. 포인터 자체를 상수로 선언하려면 const 키워드의 위치를 * 후에 가지고 있다. 즉, char *const str와 같이 선언한다.

 char *const str = buf1; /*상수 포인터*/
*str = 'a'; /* OK */
str = buf2; /* error*/

상수에 대한 포인터와 상수 포인터는 선언이 비슷하지만 이질적인 것이므로, 그 차이를 인식할 수 있게 하자.

이와 같이 const는 다양한 상황에서 사용된다. 특히 값을 변경할 수 없는 변수에 대해서는 적극적으로 const를 이용해야 한다고 생각된다. 이렇게 함으로써 시스템은 변수를 읽기 전용 메모리 영역에 배치시켜보다 빠른 동작을 실현할 수있는 가능성이 있기 때문이다. 물론 이러한 최적화 처리는 컴파일러와 시스템에 의존하는 문제이므로 개발자가 의도하는 것은 아니다.

최적화 거부

const에 이어서 또 하나의 형식 한정자 volatile 키워드가 존재한다. 이 형식 한정자는 상당히 특별한 소프트웨어 및 시스템 개발을 제외하고 사용하지 않을 것이다. volatile은 값이 항상 가변이며, 시스템이 최적화 처리를 하지 않도록 명시적으로 나타낸다. 다음 문장은 volatile을 지정된 숫자형 변수의 선언이다.

volatile int viValue = n;

volatile 형 한정자를 지정하는 경우는 const 마찬가지로 형식 지정자를 생략할 수 있다. 형식 지정자가 생략된 경우 volatile int로 해석된다.

volatile은 이를 실행하는 프로그램이 아닌 미지의 프로세스(OS 나 특수 하드웨어 등)이 변수를 사용할 경우, 컴파일러가 마음대로 의도하지 않은 모양으로 최적화하는 것을 방지한다. 물론 많은 경우는 외부 프로세스가 값을 변경하는 것을 원하지 않기 때문에, 독립된 소프트웨어는 volatile을 지정할 수 없다.

코드3

#include <stdio.h>

volatile viVariable = 0xFF;
const volatile char *str = "Kitty on your lap";

int main() {
  printf("viVariable = %d\nstr = %s\n" , viVariable , str);
 return 0;
}

이 프로그램에서는 volatile 형 한정자를 지정하는 정수형 변수 viVariable와 문자열에 대한 포인터 str을 선언하고 있다. 이러한 변수는 다른 권한있는 프로그램이 변경될 가능성을 보여주고 있지만, 당연히 시스템과 하드웨어와 통신을 하지 않으면 변경될 수도 없고, 이 프로그램의 volatile 형 한정자는 실질적인 의미가 없다.

덧붙여서 str 변수의 선언을 보고 알 수 있듯이, const와 volatile 형 한정자는 동시에 지정할 수 있다. 이 경우 const가 지정되어 있기 때문에 이 프로그램에서 변수의 내용을 변경할 수 없음을 나타낸다. 그러나 시스템이나 하드웨어의 변경은 받아 들일 것을 의미한다.

1.8 - C 언어 | 전처리(preprocess)

전처리 지시자를 컴파일 전에 소스코드를 가공하는 방법을 알려주는 역할을 한다.

1.8.1 - C 언어 | 전처리(preprocess) | 포함 - #include

컴파일 전에 처리되는 #include 전처리기 지시문(Preprocessor Directives)을 사용하여 선언 등을 정리한 헤더 파일을 모든 소스 파일에 포함시킬 수 있다. 이를 포함(include)라고 한다.

헤더(header) 파일 만들기

여러 파일로 구성된 중간 규모 이상의 개발 프로젝트는 프로그램 방법뿐만 아니라 파일의 구성도 중요해지고 있다. 여기서 다루고 있는 작은 테스트용 프로그램이라면 하나의 파일로 충분히 만들 수 있지만 실전은 그렇지 않다. 개발 멤버는 혼자가 아니며, 어떤 그룹은 그래픽, 다른 그룹에서는 음성 등 다른 분야의 개발을 동시 진행으로 할 가능성이 충분히 있다. 그리고 이는 하나의 파일로 할 수 없다.

논리적으로 구분된 기능을 파일마다 분할하여 개발을 진행하는 경우는 마지막으로 소집하여 컴파일할 필요가 있다. 일반적으로 main() 함수를 포함하는 실행 파일을 작성하기 위한 소스 파일의 확장자를 *.c로하지만, 그 이외의 라이브러리의 역할과 기능 세트를 제공하는 파일은 헤더 파일로 *. h를 확장자로 하는 관행이 있다. 헤더 파일은 main() 함수 앞에 삽입된다.

여러 파일을 하나로 정리하여 컴파일하려면, #include 전처리기 지시문을 사용하여 파일의 지정된 위치에 헤더 파일을 삽입하도록 컴파일러에 전달된다. 컴파일러는 #include를 발견하면 이 위치에 지정한 파일을 삽입한다. 지금까지는 #include <stdio.h>와 같이 작성하여 C 언어를 기본으로 지원하는 라이브러리 파일을 결합해 왔다. 이를 포함하도록 표현한다.

표준 파일 이외의 포함에 #include "파일 이름"과 같이 큰 따옴표로 묶어 작성한다. 이것은 “처음하는 C 언어“에서 설명하였다. 컴파일러가 어떻게 파일을 검색하거나 컴파일러 문서를 참조한다. 큰 따옴표로 묶여있는 경우는 보통 *.c 파일과 같은 디렉토리에서 검색된다.

포함은 단순히 파일의 삽입하는 것으로 생각할 수 있다. 이 #으로 시작하는 명령은 컴파일러가 아닌 전처리에 대한 명령이다. 전처리는 소스가 컴파일되기 전에 실행된다. #include 파일을 삽입하는 명령이므로, 컴파일러가 소스를 컴파일하기 전에 지정된 위치에 파일의 내용을 그대로 삽입하는 것이다. 따라서 개발자는 헤더 파일에 일반 함수 등을 작성하고, 이를 프로젝트의 C 소스 파일에 포함하는 것이다. 그러면 같은 함수를 여러번 작성하는 작업에서 해방된다.

코드1

/* sample.h */
int strlen(const char *str);

코드2

int strlen(const char *str) {
  int count;
  for(count = 0 ; *(str + count) ; count++);
  return count;
}

코드3

#include <stdio.h>
#include "sample.h"

int main() {
 char *str = "Kitty on your lap";
  printf("%s length=%d\n" , str , strlen(str));
  return 0;
}

헤더 파일의 코드1을 작성하고 이를 코드3에 포함하여 컴파일한다. 그러면 #include "sample.h"라고 작성한 위치에 헤더 파일의 내용이 그대로 복사되고 컴파일된다. 코드1에는 NULL 문자를 제외한 문자열의 문자 수를 반환 strlen() 함수를 선언한다. strlen() 함수의 구현은 코드2에서 작성하고 있다. 이러한 헤더 파일을 한번 만들면 모든 개발 프로젝트에서 재사용할 수 있다.

1.8.2 - C 언어 | 전처리(preprocess) | 매크로 상수 - #define, #undef

##define 지시문을 사용하여 텍스트에 이름을 지정하고 코드에 상수로 배포할 수 있다. 자주 등장하는 의미있는 값이나 연산 등을 자동화하는데 응용할 수 있다.

토큰 열의 전개

이전에 const 형 한정자를 사용하여 변경 불가능한 변수를 만드는 방법을 설명했다. 이것은 개발자에 프로그램 안에서 항상 특정 상수를 나타내는 식별자를 주었다. 그러나 변수는 만드는 것만으로도 메모리 손실이 발생하며, 추가적으로 포인터에 의한 간접 참조되어 CPU의 주소 계산이 발생하게 된다. 많은 C 언어 프로그래머는 이러한 낭비를 없애고, 더 빠르고 똑똑한 프로그램을 만들고 싶다고 생각하기에 이는 큰 문제일 것이다.

그래서 가능한 것이 전처리이다. 전처리는 컴파일 전에 소스를 성형하는 능력을 가지고 있기 때문에, 전처리로 할 수 있는 것은 전처리 수행함으로써, 처리의 효율화를 도모할 수 있다.

전처리 명령은 토큰 열을 전개하는 #define 지시문이 존재한다. #define 지시문에 정의된 식별자를 매크로라고 부르고, 많은 C 언어 프로그래머가 이 기능을 많이 사용하고 있다.

#define 지시문

#define 식별자 토큰열

토큰 열에 지정하는 내용은 어떤 것이라도 상관없다. 프로그램 중에 #define 지시문으로 만든 식별자를 지정하면, 그 위치에 토큰 열이 그대로 전개된다. 토큰 열이 전개될 때는 앞뒤의 공백이 잘린다. #define 지시문으로 만든 토큰 열을 나타내는 식별자는 #define 지시어 다음의 모든 위치에 지정할 수 있다. 극단적으로 다음 프로그램도 문제없다.

#define MAIN int main() { return 0; }
MAIN

이것은 #define 지시문을 사용하여 int main() { return 0; } 라는 토큰 열을 나타내는 MAIN이라는 식별자를 생성한다. 이처럼 #define 지시어는 토큰 열에 이름을 지정할 수 있다. 후에는 프로그램 중에 필요한 위치에 토큰 열의 이름을 지정하면 컴파일시에 전처리 토큰 라인에 옮겨준다. 위에 두줄은 컴파일시에는 int main () {return 0;} 문자열로 대체한다.

이 기능을 잘 이용하면 변수를 사용하지 않고 일관된 정수 처리를 할 수 있다. 실제로 C 언어 프로그래머는 상수를 사용하는 경우 const 형 한정자와 enum을 사용하는 것보다 #define 지시어에 의해서 전처리 할시에 대체를 하도록 하는 것을 많이 이용한다. 함수에 전달하는 논리적 의미가 있는 속성값이나 함수가 반환하는 오류 코드 등은 #define 지시문의 식별자로 구분하는 수법이 일반적이다.

코드1

#include <stdio.h>
#define KITTY "Kitty on your lap"
#define BUFFER 0xFF

int main() {
  char str[BUFFER] = KITTY;
 printf("%s\n" , str);
  return 0;
}

코드1은 리터럴 문자열을 나타내는 KITTY와 0xFF라는 정수 상수를 나타내는 BUF라는 이름을 정의하고 있다. #define 지시문에 정의된 토큰 열 이름은 프로그램 중 어느 곳에서도 지정할 수 있다. 이 이름들은 컴파일 전에 전처리에서 대체되어, C 언어의 구문에는 관여하지 않는다. 물론 확장된 토큰 열이 구문적으로 올바른지는 개발자의 책임이다.

헤더 파일 등은 이러한 상수의 이름을 붙일 수 있는 유효한 수단으로 동시에, 다른 한편으로는 어느 한곳에 상수의 이름을 붙이는 처리해야 할 문제도 발생한다. 전처리기 지시문에는 가시성이라는 개념이 존재하지 않기 때문에, 항상 #define 지시문에서 정한 이름이 공개되어 버린다. const 형 한정자 등이라면 로컬 변수를 사용하여 지역성을 제공됐지만, #define 지시어를 사용하는 경우는 어떻게 해결해야 할까?

이러한 문제는 #undef 지시문을 사용하여 해결할 수 있다. #undef 지시문은 #define 지시문에서 정의 된 이름을 삭제한다. 이에 따라 정의 해제된 이름은 그 이하 줄에서는 사용할 수 없다.

#undef 전처리 명령

#undef 식별자

식별자는 정의 해제를 보장하는 이름을 지정한다. #undef 지시문은 반드시 정의된 이름을 지정할 필요가 없다. 정의되지 않은 이름을 지정해도, 해당 식별자의 정의되지 않은 상태를 보장하는 효력이 있기 때문이다.

코드2

#include <stdio.h>

int main() {
#define PI 3.14159265358979323846
 printf("%f\n" , PI); /*OK*/
#undef PI
 /*printf("%f\n" , PI); /*error*/
 return 0;
}

코드2는 원주율을 나타내는 상수 PI를 정의하고 있다. main() 함수의 시작 부분에서 PI를 printf()에서 사용하고 있지만, 그 후에 #undef 지시문에 의해 정의 해제를 하고 있다. #undef 지시문의 행 이후에 PI라는 이름은 정의되지 않은 상태가 보장되기 때문에, 주석 처리된 printf() 함수를 컴파일하면 오류가 발생하는 것을 확인할 수 있다.

헤더 파일 등으로 어느 한곳에서 사용하는 상수는 헤더 파일의 끝에서 정의 해제하면 이름 충돌을 피할 수 있다.

1.8.3 - C 언어 | 전처리(preprocess) | 매크로 함수 - #define

매크로 함수는 #define 지시어와 매크로 전개로 매개 변수를 받을 수 있으며, 임의의 텍스트를 인수 매크로 전개할 수 있다. 이것으로 함수와 같이 행동 매크로를 만들 수 있지만, 어디까지나 전처리에 의해 컴파일 전에 전개되는 텍스트이므로 함수 호출 오버 헤드가 발생하지 않는다. 간단한 계산 처리의 전개에 적합하다.

매개 변수가 있는 매크로 전개

여러번 사용되는 계산식 등은 함수로 정리하여 여러번 재작성 작업에서 해방되지만, 간단한 계산식 함수화는 다른 문제를 생긴다. 함수를 호출하려면 함수의 매개 변수에 인수를 전달하는 값을 복사하여 스택에 저장하는 작업이 발생한다. 매개 변수가 없는 함수에서도 제어가 호출자에게 반환하기 위해, 기계어 레벨에서는 주소의 저장이 이루어지고 있다.

빠른 프로그램을 실현하기 위해서는 이러한 불필요한 처리는 적게해야 한다. 그러나 함수를 사용하지 않는 프로그램은 매우 비효율적이고 재사용이 되지 않는다. 간단한 계산을 효율적으로 기능할 수 있는 방법은 없는 것일까?

그래서 간단한 계산식 등은 변수 마찬가지로 컴파일시에 전개해 버리는 수법이 적용된다. 즉, 전처리를 사용한다. #define은 토큰 열에 이름을 붙여서 컴파일시에 전처리에 의해 이것이 대체하는 것이었다. 상수가 아닌 계산식에 이름을 붙여 소스 내에 필요한 곳에 사용하고, 컴파일시에 대체하여 효율적으로 빠른 프로그램을 만들 수 있다.

#define는 매크로 함수라고 하는 인수를 받을 수 있는 대체 처리를 지원하고 있다. 이것은 이용자로부터 보면 함수와 같은 동작을 한다. #define가 인수 대체를 지원하려면 다음과 같은 구문을 사용한다.

#define 지시문 (매크로 함수)

#define 식별자(인수1, 인수2 ...) 토큰열

이 매크로 함수를 보면, 식별자가 함수 이름이며, 토큰 열의 형식이 반환값처럼 느껴질 것이다. 그러나 매크로는 인수를 받는 것은 아니다. 단순히 토큰 열에 대해 인수를 전개할 뿐이다. 예를 들어, 다음 매크로 함수를 보도록 하자.

#define ADD(a , b) a + b

이것은 ADD() 매크로 함수를 정의하고 있다. ADD(10, 20)과 소스 내에 작성하면, 그 위치에 전처리가 10 + 20이라는 텍스트로 바꾼다. 매크로 함수의 개발자는 함수가 호출되는 것이 아니라, 단순히 그 위치에 토큰 열이 전개될 뿐임을 의식하지 않으면 안된다.

코드1

#include <stdio.h>
#define MUL(multiplicand , multiplier) multiplicand * multiplier

int main() {
  printf("5 * 5 = %d\n" , MUL(5 , 5));
 return 0;
}

코드1은 곱셈을 하는 MUL()라는 매크로 함수를 정의하고 있다. MUL() 매크로 함수의 이용자에게는 이것이 매크로 함수임을 특별히 의식할 필요가 없다. 매크로 함수의 개발자는 이 매크로 함수의 사양을 다른 함수처럼 동일하게 정할 수 있다.

그러나 여러번 말하듯이 매크로 함수의 개발자는 매크로 함수는 텍스트 레벨에서 대체하는 것이며, 실제 함수처럼 실행시에 호출되는 것은 아니라는 것을 깨달아야 한다 . 사실은 코드1의 MUL() 매크로 함수는 일반 함수에서는 발생하지 않는 큰 문제가 포함되어 있다. 이것은 다음의 프로그램을 실행하면 이해할 수있는 것이다.

코드2

#include <stdio.h>

#define MUL(multiplicand , multiplier) multiplicand * multiplier
int mul(int multiplicand , int multiplier) {
 return multiplicand * multiplier;
}

int main() {
  printf("메크로 함수 : MUL(3 + 2 , 5) = %d\n" , MUL(3 + 2 , 5));
 printf("일반 함수 : mul(3 + 2 , 5) = %d\n" , mul(3 + 2 , 5));
  return 0;
}

코드2는 곱셈을 하는 매크로 함수 MUL()와 같은 처리를 하는 일반 함수 mul()가 정의되어 있다. main() 함수에서 이 두 함수에 같은 인수 값을 주고 결과를 표시하고 있지만, 이들은 다른 값을 반환한다. 일반 함수가 반환한 값이 25이면 개발자가 의도한 값 것이다. 그러나 매크로 함수는 13이라는 이상한 계산 결과를 반환한다. 왜일까요?

함수는 함수가 호출되기 전에 인수에 지정된 표현식이 계산된다. 따라서 코드2는 3 + 2 표현식이 계산되어 최종적으로는 mul(5, 5)의 형태로 함수가 호출된다.

그러나 매크로 함수는 텍스트 레벨의 간단한 교체 작업 때문에 이런 식의 계산이 수행되지 않는다. MUL(3 + 2, 5)라는 매크로 지정은 3 + 2라는 텍스트가 multiplicand로 전개되기 때문에 컴파일시에는 3 + 2 * 5라는 식으로 전개되고 있는 것이다. 따라서 연산자의 우선 순위에 따라 2 * 5가 먼저 계산된 다음에 10 + 3이 계산된다. 그 결과 MUL(3 + 2, 5)는 13을 반환된다.

이러한 문제를 해결하기 위해서는 매크로 함수의 인수에는 반드시 ()를 지정하여 우선 순위를 보호하는 방법이 있다. MUL() 함수는 다음과 같이 정의한다.

#define MUL(multiplicand , multiplier) ((multiplicand) * (multiplier))

이와 같이 괄호로 둘러싸여, 전개 후에 계산 순서를 보호할 수 있다.

마지막으로, 대표적인 매크로 함수의 실전 방법을 보여준다. 매크로 함수가 위력을 발휘하는 것은 복잡한 수식을 단순화하는 경우이다. 행정 절차와 세제가 복잡하다면 주민이 기피하듯이, 당신이 만든 시스템의 사양이 아무리 효율적이라도, 계산 처리 등에 시간이 걸린다면 개발자는 시스템 서비스를 사용하고 싶은 생각하지 들지 않을 것이다.

예를 들어, 32비트 시스템에서 RGB 값이 각 요소 8비트씩으로 표현하는 시스템의 경우, 색의 요소에 액세스하기 위해서는 시프트 연산 등을 실시하여 특정 바이트를 추출해야 한다. 이러한 연산 처리는 매크로 함수가 자랑하는 분야이다.

코드3

#include <stdio.h>

typedef unsigned char BYTE;
#define RGB(r , g , b) ((BYTE)(r) << 16) | ((BYTE)(g) << 8) | (BYTE)(b)
#define RED(color) (BYTE)((color) >> 16)
#define GREEN(color) (BYTE)((color) >> 8)
#define BLUE(color) (BYTE)(color)

int main() {
 int color = RGB(0xFF , 0xEE , 0xAA);
  printf("R = %X : G = %X : B = %X\n" ,
    RED(color) , GREEN(color) , BLUE(color));
 return 0;
}

코드3는 색깔을 나타내는 숫자를 만들기 위한 매크로 함수 RGB()와 빨강, 녹색, 파랑의 각 요소의 값을 추출하기 위한 매크로 함수 RED(), GREEN() BLUE()를 정의하고 있다. 이 프로그램은 32비트 컴퓨터에서 작동하며, 하위에서 차례로 파랑, 녹색, 빨강의 요소 값이 8비트에 대등한 컬러 정보를 취급하는 것을 상정하고 있다.

단일 수치로 여러 정보를 보유하는 것은 드문 일이 아니다. 이러한 사양이 주어진 경우는 데이터의 생성과 단일 정보를 얻기 위해 비트 연산이 필요하다. 그러나 여러번 그것을 작성하는 것이 번거롭고, 복잡한 프로그램은 단순한 실수를 유발할 수 있다. 그래서 코드3과 같이 매크로 함수를 사용하여 문제를 단순화한다.

RGB() 매크로는 r에 빨간색 요소를 g에 녹색 요소를 b에 파란색 요소를 각각 1바이트로 지정해야 한다. 매크로 함수는 이러한 인수를 바탕으로 각 요소를 적절한 위치로 이동하는 식으로 전개한다. RED(), GREEN(), BLUE() 매크로 함수의 color는 RGB() 매크로 함수에서 만든 32비트 숫자 형식을 전달한다. 이 매크로 함수들은 색상 정보에서 원하는 1바이트를 추출하는 식으로 전개한다.

이 외에도 함수 호출을 은폐하는 매크로 함수(매크로 함수에서 함수를 호출하여 절차를 간소화하기) 등의 이용도 된다. 이처럼 매크로 함수는 계산만으로 구성되는 비교적 간단한 변환 처리 등에 많이 사용된다.

1.8.4 - C 언어 | 전처리(preprocess) | 조건부 컴파일 - defined

조건부 컴파일은 전처리으로 코드를 컴파일의 대상으로 할 것인지를 선택하는 방법이다. 예를 들면 디버깅 할 때 컴파일하려는 코드와 릴리스 시에 컴파일하려는 코드를 전처리 지시문으로 분기할 수 있다.

컴파일을 제어하기

C 언어는 많은 시스템에서 사용되는 국제적인 프로그래밍 언어이며, 전문 프로그래머들 사이에서는 표준어와 같은 존재이다. 그 만큼 많은 시스템에서 사용되는 언어라면, 당연히 프로그래머는 이식성이 높은 코드를 만들려고 노력하는 것이다.

하지만 컴파일시의 상황에 따라 꼭 다시 써야 할 부분이 생긴다. OS 버전과 개발 환경이 다르면 약간의 사양의 차이가 프로그램의 동작에 영향을 미칠 수 있고, 코드를 부분적으로 OS에 특화시키고 싶은 경우도 있을 것이다.

이러한 문제는 소스를 다시 작성하는 것 보다도, 컴파일해야 소스를 분기시키는 컴파일 과정을 제어하는 조건부 컴파일라는 기술을 사용하여 효율적으로 해결할 수 있다. 이는 컴파일 전에 소스를 변경하는 전처리로 할 수 있다.

컴파일 부분적인 통제는 #if 지시문 및 #endif 지시문의 조합으로 구성된다. #if 지시문은 #endif이 대응되어야 한다.

#if 지시문과 #endif 지시문

#if 상수 표현식
    컴파일 코드
#endif

상수 표현식은 정수형 상수 표현식이다. 이 상수 표현식이 거짓이면 대상 코드는 컴파일되지 않는다. 이 기능을 이용하면 디버깅시 임시 코드 등을 작성할 수 있습니다. 릴리스 버전을 컴파일하는 경우는 상수 식을 0으로 하면 된다.

상수 식에는 #define 지시문에 정의된 이름을 사용할 수 있지만, 변수와 sizeof 연산자, 형 캐스팅은 사용할 수 없다. 다른 기본 연산자는 사용할 수 있다.

코드1

#include <stdio.h>
#define DEBUG_MODE 1

int main() {
#if DEBUG_MODE
  printf("Debug mode\n");
#endif
  return 0;
}

코드1에는 main() 함수에서 #if 지시문을 사용하고 있다. 이 프로그램은 상수 DEBUG_MODE가 참이면 printf()를 컴파일하고, 거짓이면 컴파일을 실시하지 않는 것을 나타낸다. DEBUG_MODE은 1이므로 printf() 함수가 컴파일되지만, 이를 0으로 변경하고 다시 컴파일하면 #if에서 #endif까지의 텍스트가 컴파일 대상에서 배제되는 것을 확인할 수 있다.

C 언어 구문인 if-else 마찬가지로 #if 지시문이 거짓인 경우에 컴파일하는 코드를 지정하려면 #else 지시문를 사용한다. #else 지시문은 기본적으로 if-else의 관계와 같아서 이해하기 쉬울 것이다.

#else 지시문

#if 상수 표현식
    참일 , 컴파일 코드
#else
    거짓을 , 컴파일 코드
#endif

이 경우, 상수식이 0이 아니라면 #if 후에 텍스트가, 0이면 #else부터 #endif까지의 텍스트를 컴파일된다. #else 지시문은 #if와 함께 사용되며, 반드시 #endif 지시문 앞에 작성된다. #else 지시문은 #if ~ #endif 사이에 최대 하나만 지정할 수 있다. 덧붙이면, #if 지시문는 중첩될 수 있다.

#if N
  #if M
   ...
 #else
   ...
 #endif
#else
 ...
#endif

이 구문을 사용하여 컴파일할 국가나 개발 환경, 시스템 등에 따라 대상 코드를 선택할 수 있다. 다만, 필요 이상으로 복잡하게 해서는 안된다. 너무 복잡하다면 프로그램에 맡기는 것이 무난하다.

코드2

#include <stdio.h>
#define EN 1
#define  KO 2

#define LANG KO

int main() {
#if LANG == EN
 printf("Kitty on your lap\n");
#else 
 #if LANG == KO
    printf("당신의 무릎 위에 고양이\n");
 #endif
#endif
  return 0;
}

이 프로그램은 국가 코드를 나타내는 EN과 KO 상수를 정의하고 #if 등으로 비교에 이용하기 위한 상수 LANG을 정의하고 있다. 프로그램은 LANG이 EN이면 영어로 KO이면 한국어로 문자열을 표시한다. #else 지시문 후에 추가적인 #if 지시문을 사용하여 중첩 구조로 되어있는 곳에도 주목하자.

코드2와 같이 #if 지시문을 중첩하는 경우는 전처리 지시문의 구문상의 문제로 #else 지시문 후에 지정하는 경우에서도 줄 바꿈을 해야 하고, 추가적으로 #endif 지시문에 대응시켜야 하기에 코드가 복잡해 진다. 그래서 #else 지시문과 #if 지시문을 조합한 #elif 지시문이 준비되어 있다. #elif 지시어는 다음과 같이 사용할 수 있다.

#elif 지시문

#if 상수 식
    컴파일 코드
#elif 상수 식
    컴파일 코드
#elif 상수 식
...
#endif

#elif는 #else와 #if이 일체가 된 효력이 있고, #if를 #else 후에 중첩하는 경우와 동일한 결과를 얻을 수 있기 때문에, 단계적으로 #if 지시문에 의해 상수를 평가하고 싶은 경우에 유효한 수단이다. #elif는 상수 표현식이 참이면 그 이후의 텍스트를 컴파일 대상으로 한다. #else와 마찬가지로 #if ~ #endif 사이에 지정할 수 있고 #else와는 달리 여러개를 지정할 수 있다.

코드3

#include <stdio.h>
#define EN 1
#define KO 2

#define LANG EN

int main() {
#if LANG == EN
 printf("Kitty on your lap\n");
#elif LANG == KO
 printf("당신의 무릎 위에 고양이\n");
#endif
 return 0;
}

코드3는 코드2를 #elif 지시어로 표현한 것이다. 동작은 동일하지만 #if 지시문을 중첩하는 것보다 소스가 짧아지기 때문에 가독성이 향상된다.

이름 정의에 의한 제어

사실 #define으로 정의하는 이름은 반드시 어떠한 토큰 열에 전개되는 것은 아니다. 이름만 정의하여 토큰 열을 생략할 수 있는 것이다. 예를 들어 다음과 같은 정의가 허용된다.

#define DEBUG

상수 DEBUG는 토큰 열을 가지고 있지 않지만 이름은 정의되어 있다. 이와 같은 이름의 정의는 코드에 영향을 미치는 것은 아니지만 전처리 레벨의 처리에는 의미가 있다.

예를 들어, 코드1과 같이 디버그 및 릴리스에서는 다른 소스를 컴파일하고 싶다면, 상수 DEBUG 값을 평가하는 것보다 DEBUG라는 이름이 정의되어 있는지 여부를 평가하는 것이 스마트하다고 생각할 수 있다. 그러면 #define을 주석 처리하거나 #undef를 사용하여 정의 해제하는 것으로 릴리스 버전의 컴파일을 할 수 있다.

이름의 정의를 평가하려면 defined 전처리기 연산자를 사용한다. 이 전처리 연산자는 #if 지시문 또는 #elif 지시문의 상수 표현식으로만 사용할 수 있다.

defined 전처리 연산자

defined (식별자)
defined 식별자

defined 연산자는 정의를 평가하는 식별자를 ()로 묶거나, defined 후에 공백을 두고 지정한다. 지정된 식별자가 #define에 의해 정의되어 있는 경우는 true를, 정의되지 않았다면 false를 반환한다.

코드4

#include <stdio.h>
#define DEBUG

int main() {
#if defined(DEBUG)
  printf("Debug mode\n");
#else
 printf("Release mode\n");
#endif
  return 0;
}

코드4는 DEBUG라는 이름이 정의되어 있으면 디버그 모드용의 코드를, 그렇지 않으면 릴리스 코드를 컴파일하는 프로그램의 예이다. #if 지시문으로 defined 연산자를 사용하여 식별자를 평가하고 있다.

그러나, 일반적으로 defined 연산자가 사용되는 것은 아니다. 대부분 C 언어 프로그래머는 단순하게 작성할 수 있는 #ifdef 지시문 및 #ifndef 지시문을 사용하는 것을 선호하고 있기 때문이다. 이들은 #if defined …와 같은 형태를 생략한 지시문이다.

#ifdef 지시문

#ifdef 식별자

#ifndef 지시문

#ifndef 식별자

이 두개의 지시문은 #if defined (식별자)#if !defined (식별자)와 같은 코드와 같다고 생각할 수 있다. #ifdef 지시문은 식별자가 정의되어 있으면, 그 후에 코드를 컴파일하다. 반대로 #ifndef는 식별자가 정의되어 있지 않으면 다음의 코드를 컴파일한다. 이러한 지시문은 #if 지시문과 마찬가지로 #endif에 대응해야 한다.

코드5

#include <stdio.h>
#define DEBUG

int main() {
#ifdef DEBUG
 printf("Debug mode\n");
#else
 printf("Release mode\n");
#endif
  return 0;
}

코드5는 코드4를 #ifdef 지시문을 사용하여 수정한 것이다. 결과는 동일하지만 #if defined (...) 대신에 #ifdef 지시문을 사용하고 있다. 많은 C 언어 프로그래머는 이와 같은 작성을 선호한다.

컴파일 오류

#if 지시문을 사용하여 상수와 이름을 조사한 결과, 컴파일을 하려면 불충분한 상태라고 하면, 개발자는 어떻게 소스를 작성해야 하나? 충분한 정보가 설정되어 있지 않은 경우의 처리 방법 중 하나는 기본 동작하는 것을 미리 정하고 그것을 설정하는 방법도 있지만, 그것을 선호하지 않은 경우도 있다.

이러한 경우 #error 지시문을 사용하여 컴파일을 강제로 중단시키는 방법이 있다.

#error 지시문

#error 출력 에러 문자열

전처리는 #error가 평가되면 컴파일을 중지하고 오류 문자를 출력한다. 컴파일에 필요한 개발자의 의사로 이름이 정의되어 있지 않거나, 필요한 헤더 파일이 포함되어 있지 않은 경우에 이러한 오류를 출력하는 방법도 있다.

코드6

#include <stdio.h>
/*#define BUFFERSIZE 0xFF*/

int main() {
#ifdef BUFFERSIZE
  int iArray[BUFFERSIZE];
#else
  #error BUFFERSIZE 상수가 정의 되어 있지 않는다.
#endif
 return 0;
}

코드6은 프로그램이 할당되는 배열의 크기 BUFFERSIZE 상수가 정의되어 있는지 여부를 #ifdef 지시문을 이용하여 조사하고 있다. BUFFERSIZE가 정의되어 있으면, 이 값을 사용하여 배열을 초기화되지만 그렇지 않으면 배열을 초기화할 수 없기 때문에 오류를 발생시킨다. 에러 문자열이 어떻게 표시되는지는 컴파일 환경에 의존하는 문제이다.

1.8.5 - C 언어 | 전처리(preprocess) | 문자열화

매크로 함수에 전달된 코드를 문자열 화하는 방법을 소개한다. 이것으로 문자열이 아닌 텍스트를 매크로에 주어 이를 전개할 때에 문자열로 변환할 수 있다.

매크로 함수와 문자열화 연산자

#define으로 작성된 매크로 함수에만 지정할 수 있는 처리기 연산자에 매개 변수로 받은 토큰 열을 문자열로 변환하는 연산자가 있다. 이것은 주어진 토큰 열을 분석하고 따옴표를 부가한 형태로 전개한다. 이렇게 하면 숫자와 토큰 열을 자동으로 문자열 화하는 매크로 함수를 만들 수 있을 것이다.

토큰 열을 문자열 화하려면 매크로 함수의 파라미터 앞에 # 연산자를 지정한다. 예를 들어, 다음과 같이 작성하면 함수는 주어진 인수를 문자열 화한다.

#define TOSTRING(param) #param

이것은 매크로 함수이므로 param에 전달되는 토큰 열의 형태를 규제할 수 없다. 그러나 param의 토큰 열이 어떤 것이 든 # 연산자는 토큰 열을 문자열 화한다. 그 관계는 다음과 같은 것이 될 것이다.

TOSTRING(1234) → "1234"

TOSTRING(int iValue = 10\n) → "int iValue = 10\\n"

TOSTRING("Kitty") → "\"Kitty\""

이러한 변환은 컴파일 전에 소스 텍스트 레벨에서 전개되는 것에 유의한다. 프로그램 실행시에 동적으로 변환되는 것은 아니다. 문자열 화 연산자는 토큰 열의 따옴표(")은 "으로 변환하고 슬래시()는 \로 변환된다. 이렇게 함으로써 토큰 열을 확실하게 그대로의 형태로 문자열화 할 수 있다.

코드1

#include <stdio.h>
#define PRINTLN(string) printf(#string "\n")

int main() {
  PRINTLN(0xFF);
  PRINTLN(Kitty on your lap);
 PRINTLN(Kernighan and Ritchie wrote "hello, world\n" on their book.);
  return 0;
}

코드1의 PRINTLN() 매크로 함수는 인수로 지정한 토큰 열을 문자열로 printf()로 표시한다. 처음 두개는 그대로 문자열화되어 있는 것을 확인할 수 있다. 재미있는 것은 마지막 장문으로 "hello, world\n"이라는 토큰이 문자열 화되어 있는 곳이다. 이러한 문자열은 전처리에 의해 \"hello, world\\n\"로 변환된다. 따라서 따옴표나 이스케이프 문자가 그대로 표시된다.

1.8.6 - C 언어 | 전처리(preprocess) | 토큰 연결 ##

임의의 토큰을 결합하는 ## 연산자를 설명한다. 주로 매크로 함수에 전달된 매개 변수를 다른 매크로나 값과 결합하기 위해 사용된다.

토큰 연결 연산자

#define을 이용한 매크로 전용 처리기 연산자로 문자열화 연산자 외에도 또 하나 중요한 연산자가 존재한다. 그것은 두 개의 샵 기호로 구성된 ## 연산자로 토큰 연결 연산자라고도 한다. 이 연산자는 일반 매크로와 함수형 매크로로 사용할 수 있다.

## 연산자

토큰1 ## 토큰2

토큰 연결 연산자는 좌변의 토큰과 우변의 토큰을 접합한다. 토큰의 접합은 단순히 C 언어의 소스 레벨의 텍스트를 전처리에 의해 연결시키는 것이고, 프로그램이 동적으로 접합시키는 것은 아니라는 점에 유의하한다. 예를 들어, 매크로으로 WIN ## 32을 지정하면 WIN32이라는 토큰에 연결되고, WIN ## 16이라면 WIN16이 된다. 매크로 함수의 인수와 토큰 연결 연산자를 사용하면, 매크로를 사용하여 토큰의 지정을 감출 수 있다.

#define INT16 short
#define INT32 int
#define INT(n) INT ## n

위의 INT() 매크로 함수는 INT는 토큰과 n 매개 변수를 연결한다. 예를 들어 INT(16)라고 지정한 경우 INT16에 전개되고, 덧붙여서 INT16 매크로가 처리되어 최종적으로는 short로 확장된다.

코드

#include <stdio.h>
#define TOKEN0 "Kitty"
#define TOKEN1 "Kitten"
#define TOKEN(n) TOKEN ## n

int main() {
  printf("%s\n" , TOKEN(0));
 printf("%s\n" , TOKEN(1));
 return 0;
}

코드1의 TOKEN() 매크로 함수는 인수에 연결하는 토큰을 받는다. 프로그램의 사양에서 인수로 지정할 수 있는 값은 0 또는 1 중 하나이다. 예를 들어 TOKEN(0)을 지정했을 경우, 이것은 TOKEN ## 0으로 해석되며, 이러한 토큰을 결합하여 궁극적으로 TOKEN0으로 전개된다. TOKEN0라는 #define으로 정의된 이름이므로 문제 없다.

이와 같이, 토큰 연결 연산자를 사용하여 결합된 새로운 토큰은 반드시 유효한 토큰이 아니면 안된다. 잘못된 토큰이라면 당연히 컴파일 시에 에러가 발생한다. 토큰의 연결은 전처리 컴파일 전에 처리하는 소스 레벨의 전개에 불과한 것을 잊지 말도록 하자.

1.9 - C 언어 | 고급 기능

1.9.1 - C 언어 | 고급 기능 | 동적 메모리 할당 - malloc(), free()

실행할 때까지 필요한 기억 용량을 확인할 수 없는 데이터 처리하는 경우, 컴파일 할때가 아닌 실행할 때에 시스템에서 새로운 메모리 영역을 확보해야 한다. C 언어에서 표준 함수로 malloc() 함수를 사용하여 메모리 공간을 확보하고, free() 함수으로 해제할 수 있다.

실행 시에 메모리를 할당

일반적으로 변수에 할당된 메모리 크기는 정적이었다. 예를 들어, 문자열을 포함하는 배열을 준비하는 경우에 배열 선언시에 그 크기를 지정하였다. 그러나 배열의 선언은 크기를 정수로만 지정할 수 있으며, 변수는 지정할 수 없다. 이것은 컴파일시에 프로그램이 필요로 하는 메모리 크기가 판명해야 한다는 것을 나타낸다.

그러나 이것으로는 하드웨어의 성능을 최대한 살릴 수 없다. 그 컴퓨터가 기가 바이트 이상의 메모리를 탑재하고 있었다고 해도, 컴파일시에 지정된 배열의 바이트 수가 64 킬로바이트이면 남아있는 여유 메모리를 사용할 수 없다. 물론, 응용 프로그램이 더 많은 메모리를 필요로 하지 않으면 문제가 되지 않지만 편집 소프트웨어 등은 상한이 정해지지 않는 문제가 있다.

예를 들어, 텍스트 편집기의 경우, 컴파일시에 사용자가 입력하는 데이터의 양을 결정할 수 없다. 그러나 지금까지의 방법으로는 응용 프로그램이 상한을 정하지 않으면 안되기 때문에, 편집 가능한 문자 수를 응용 프로그램이 강제하는 것이다. 이것은 좋은 소프트웨어라고 부를 수 없다. 사용자는 사용하는 컴퓨터의 성능을 충분히 발휘할 수 있는 소프트웨어를 사용하고 싶을 것이다.

그래서 런타임 시에 동적으로 메모리를 할당하는 것이 요구된다. 동적으로 메모리를 할당하는 것은 프로그램 실행 중에 동적으로 할당하는 영역을 변화시키는 것이다. 이것이 있으면 필요한 메모리를 할당할 수 있다. 원래 메모리를 소프트웨어에 제공하는 역할은 시스템이 하는 것이며, 메모리 확보 및 제어를 수행하려면 시스템이 제공하는 API를 호출할 필요가 있다. 그러나 이러한 방법은 복잡하면서도 시스템에 의존하는 코드를 작성해야 된다.

그래서 C 언어는 표준 라이브러리 메모리 할당 함수를 제공한다. 동적 메모리 할당은 stdlib.h 헤더 파일에 선언되어 있는 malloc() 함수를 사용한다. 이 함수는 지정된 크기의 공간을 확보하고 void 형 포인터를 돌려준다.

malloc() 함수

void* malloc( size_t size );

size 매개 변수는 할당하는 크기를 바이트 단위로 지정한다. malloc() 함수는 지정된 크기의 영역을 확보하고, 그 기억 영역에 대한 포인터를 반환한다. 이 void 형 포인터를 캐스팅하여 할당된 메모리 영역을 사용할 수 있다. 이렇게 동적으로 할당된 메모리 영역을 힙 공간이라고 한다.

만약 요청된 크기를 할당하면 힙 영역이 존재하지 않는 경우, malloc() 함수는 NULL을 반환한다. 메모리는 한계가 있고, 메모리가 과도하게 소비되는 경우는 동적으로 할당할 수 없기 때문에 malloc()에서 받은 포인터는 NULL 여부를 조사한 후 사용하는 것이 좋다.

코드1

#include <stdio.h>
#include <stdlib.h>
#define ALPHABET_COUNT 26

int main() {
 int iCount;
 char *str = (char *)malloc(ALPHABET_COUNT + 1);
 for(iCount = 0 ; iCount < ALPHABET_COUNT ; iCount++)
    str[iCount] = 0x41 + iCount;
  str[iCount] = 0;
  printf("%s\n" , str);
  return 0;
}

코드1은 malloc() 함수를 사용하여 27 바이트의 저장 공간을 확보하고, 그 주소를 char 형의 포인터 str에 할당한다. 이는 str 포인터는 27의 요소를 가지는 char 형의 배열을 가리키는 것으로 해석할 수 있다. 프로그램에는 저장 공간이 할당되어 있는지를 확인하기 위해 배열에 값을 할당하고 문자열로 그것을 표시하고 있다. 0x41에서 값을 단순 증가시키고 대입하고 있기 때문에, ASCII 코드 A부터 순서대로 문자열이 표시된다.

물론, malloc() 함수으로 할당한 이상의 영역에 액세스할 수 없다. 코드1의 경우는 str 포인터에서 27 이상의 주소에 액세스하는 것을 허용하지 않는다. char 형 이외의 형식을 지정하는 경우 sizeof 연산자를 사용하여 바이트 수를 얻는 것이 좋다. int 형의 배열에 대한 포인터를 malloc() 함수를 사용하여 얻을 경우는 sizeof(int)에 요소 수를 곱하면 필요한 바이트 수를 얻을 수 있다. 예를 들어 4개의 요소를 가진 배열을 만들려면 sizeof(int) * 4를 계산하여 필요한 바이트 수를 얻을 수 있다.

하지만 malloc()으로 할당한 메모리는 보통의 변수에는 발생하지 않는 특별한 문제가 있다. 일반 로컬 변수이면, 함수에서 제어를 벗어날 때 메모리가 해제되지만 malloc() 함수에서 할당한 메모리는 프로그램이 끝날 때까지 자동으로 해제되는 것은 아니다.

코드2

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

int main() {
 while(1) {
    int *iPo = (int *)malloc(sizeof(int) * 0x100000);
   if (iPo == NULL) {
      printf("메모리가 할당되지 않았습니다.\n");
      break;
    }
   printf("iPo = %p\n" , iPo);
  }
 return 0;
}

코드2는 while 루프에서 malloc() 함수를 이용하여 1048576 개의 요소를 가지는 int 형 배열을 저장하는데 필요한 메모리를 연속적으로 할당한다. 일반 로컬 변수이면 루프가 종료될 때마다 메모리가 초기화되지만 malloc()의 경우 메모리가 해제되지 않는다. malloc() 함수로 할당된 메모리는 컴파일시에 결정되는 변수와는 달리 관리 대상이 아니다. 동적 메모리 관리는 개발자에 달려있는 것이다.

코드2의 경우, malloc()를 반복 호출하여 여러번 메모리를 할당하지만 해제는 될 수는 없다. 그러나 포인터가 손실되어 있기 때문에 모두 사용할 수 없는 메모리 영역이 만들어 버린다. 이러한 메모리 유출을 메모리 누수(memory leck)라고 하고, 고급 프로그래머도 귀찮은 버그 중 하나이다.

메모리 누수는 무엇이 문제가 무엇인지는 코드2를 실행하면 알 수 있다. 메모리를 해제하지 않고 메모리를 계속 할당하여 사용할 수 물리 메모리의 용량이 점점 계속 줄고 있다. 실행하는 컴퓨터의 메모리가 적으면 메모리 잔량이 없어 malloc ()는 NULL을 반환해 버리는 것이다. 이것은 은행이 상환이 불가능한 융자처에 대출을 계속해 주어 부실 채권이 급증 원리와 비슷하다.

시스템에서 빌린 메모리 영역은 해제하는 형태로 언젠가 반환해야 한다. 그렇지 않으면 다른 프로그램과 시스템을 정상적으로 운영하는데 필요한 메모리가 손실될 수 있다. 단기간만 실행하는 응용 프로그램의 소량의 메모리 누수라면 문제가 없지만, 장기간 실행하는 서버 등의 프로그램 메모리 누수가 있는 경우 치명적이다.

그러나 불행히도 시스템은 빚쟁이처럼 “메모리를 돌려줘!“라고 경고 해주지 않는다. 따라서 할당된 메모리는 free() 함수를 사용하여 명시적으로 해제해야 한다. 메모리가 불필요하게 되면 free() 함수에 할당된 메모리에 대한 포인터를 전달한다.

free() 함수

void free( void * memblock );

memblock에는 해제하기 이전 할당된 메모리에 대한 포인터를 지정한다. 이 함수에는 malloc() 등의 메모리 할당에 대한 함수가 반환한 포인터 이외의 값이나, 이미 해제된 포인터를 전달해서는 안된다.

코드3

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

int main() {
  int *iPo;
 while(1) {
    int *iPo = (int *)malloc(sizeof(int) * 0x100000);
   if (iPo == NULL) {
      printf("メモリが割り当てられませんでした\n");
      break;
    }
   printf("iPo = %p\n" , iPo);
    free(iPo);
  }
 return 0;
}

코드3은 루프의 마지막에서 free() 함수를 호출하여 할당된 메모리를 해제하고 있다. malloc() 함수와 free() 함수가 서로 대응하고 있기 때문에, 메모리 누수가 발생하지 않는다.

그러나 실전의 개발에는 이러한 의미있는 관계가 되어주는 것은 거의 없다. 메모리의 할당과 해제가 여러 함수에서 이뤄지는 등 복잡한 구조가 될 수 있다. 개발자는 어떤 타이밍에서 free() 함수를 호출 여부를 충분히 검토하여, 포인터의 소유권을 명확히 해둘 필요가 있다.

1.9.2 - C 언어 | 고급 기능 | 가변 인수

printf() 함수와 같은 다른 수의 매개 변수를 받을 수 있는 함수를 만드는 방법을 설명한다.

줄임표와 가변 인수 정렬

지금까지의 설명에서 C 언어의 기본 문법과 사양들 봐왔지만, 여전히 printf() 함수와 같은 것을 만드는 것을 생각하면 어떤 의문이 제기된다. printf()와 scanf() 함수처럼 C 언어의 표준 함수에는 인수를 동적으로 얻을 수 있는 것이 존재한다. 예를 들어 printf() 함수를 보면, printf ("...")으로도 printf("...", variable)으로도 컴파일할 수 있다. 이러한 일반적인 함수는 어떻게 만들까?

인수를 가변 개수로 받을 수 있는 동적 인수를 실현하려면 3개의 점으로 구성된 줄임표(…)을 사용한다. 가변 인수를 다루는 함수의 대표적인 예로 printf() 함수는 다음과 같이 선언되어 있다.

printf() 함수

int printf( const char *format ,...);

이것은 첫번째 인수인 format은 반드시 서식 제어 문자열을 지정하지 않으면 안되지만, 두번째 인수 이후는 자유롭게 지정할 수 있다는 것을 나타낸다. 따라서, 줄임표는 인수의 마지막에 지정할 수 있다. 줄임표 후 명시적 인수가 나타나서는 안된다.

이것으로 가변 인자를 받을 수는 있지만, 문제는 함수 본문에 나열된 인수를 얻는 방법이다. 인수의 수와 형은 함수가 호출될 때까지 알 수 없으므로, 매개 변수로 받을 수 없다. 그래서 가변 인수 열를 얻으려면 C 언어가 제공하는 표준 라이브러리에 정의되어 있는 매크로 함수를 사용한다.

인수의 취득에는 va_start() 매크로 함수, va_arg() 매크로 함수, va_end() 매크로 함수의 3개의 매크로 함수를 사용한다. 이 매크로를 사용하려면 stdio.h 헤더와 stdarg.h 헤더 파일을 포함해야 한다.

va_start() 메크로 함수

void va_start( va_list arg_ptr, prev_param );

va_arg() 메크로 함수

type va_arg( va_list arg_ptr, type );

va_end() 메크로 함수

void va_end( va_list arg_ptr );

모든 매크로 함수로 지정하는 arg_ptr라는 인수는 va_list 구조체의 변수이다. va_list 구조체는 stdarg.h에서 선언되는 형태로 매크로 정보원이 된다. 개발자가 이 구조체의 정의에 대해 알 필요가 없다. 이 변수는 매크로를 사용하는 것이며, 개발자가 멤버를 직접 조작하는 것이 허용되지 않기 때문이다.

우선, 먼저 va_start () 매크로 함수를 호출하여 va_list 구조체 변수를 초기화해야 한다. 두번째 인수의 prev_param에는 가변 인수 열의 직전의 매개 변수의 이름을 지정한다. 예를 들어, printf() 함수의 선언하면 줄임표의 직전의 매개 변수인 format을 여기에 지정한다. prev_param이 register 기억 클래스에서 선언되는 경우는 이 매크로의 동작은 정의되지 않는다.

prev_param에 지정하는 매개 변수는 함수의 정의에 따라 달라진다. 매크로 함수는 지정된 매개 변수 다음에서 얻는 가변 인수 열이다고 판단하는 것이다. 다음과 같은 함수 정의시 va_start()의 prev_param에 지정하는 매개 변수는 다음과 같이 될 것이다.

void Function(int iValue , ...) → va_start(arg_ptr , iValue)

void Function(int iValue , char * pStr , ...) → va_start(arg_ptr , pStr)

초기화가 끝나면 va_arg() 매크로 함수로 인수를 얻을 수 있다. 두번째 인수의 type에는 인수의 형을 지정한다. 이 매크로 함수는 va_list 형 구조체 변수의 정보를 바탕으로 지정한 형의 값을 가져온다. 동시에 매크로 type에 지정된 형 정보에서 va_list를 나타내는 인수 위치를 이동시켜, 다음 인수가 시작되는 위치를 가리키도록 처리한다. 즉, va_arg() 매크로 함수를 호출할 때마다 다음 인수를 얻을 수 있다.

va_arg() 매크로 함수를 이용하기 위해서는 가변 인수 열에 주어진 인수의 개수와 인수의 형태를 알 필요가 있다. printf() 함수는 서식 제어 문자열에서 인수의 수와 형태를 인식하고 있는 것이다. 예를 들어 가변 인수 열에 부동 소수점과 문자열에 대한 포인터가 순서대로 부여하는 경우, 다음과 같이 얻을 수 있다.

fValue = va_arg(arg_ptr , float);
pStr = va_arg(arg_ptr , char *);

마지막으로, 처리가 끝나면 va_end() 매크로를 사용하여 arg_ptr을 해제한다. 이것으로 가변 개수의 인수를 취득하기 위한 일련의 처리가 종료된다. 이러한 3개의 매크로를 사용하여 동적 인수에 액세스할 수 있다.

코드1

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

void DynamicParameter(int arg_num , ...) {
 va_list args;
 int iValue , iCount;

  if (arg_num < 1) return;

  va_start(args , arg_num);

 for (iCount = 0 ; iCount < arg_num ; iCount++) {
    iValue = va_arg(args , int);
    printf("제 %d 인수 = %d\n" , iCount + 2 , iValue);
  }

 va_end(args);
}

int main() {
  DynamicParameter(4 , 10 , 20 , 30 , 40);
  return 0;
}

코드1는 가변 개수의 인수를 받는 함수 DynamicParameter() 함수를 작성하고 있다. 이 함수는 제 1 인수 이후에 옵션 인수의 개수를 지정한다. 동적 인수는 va_arg() 매크로를 여러번 호출할 것인가를 결정해야하기 때문에 인수의 개수를 알 필요가 있다. printf() 함수는 서식 제어 문자열 인수의 수를 분석하고 있다.

DynamicParameter()는 가변 인수를 얻기 위해, arg_num에서 그 인수의 수를 얻는다. arg_num가 1 이하이면 그 이하의 인수가 없는 것을 나타내므로, 아무것도 하지 않고 제어를 반환한다. 최초의 va_start()에는 va_list를 초기화하고, 그 후에 for 루프으로 va_arg()에서 숫자 형식으로 인수 값을 받고 있다.

이러한 동적으로 인수를 얻는 함수는 매우 범용적인 기능을 제공할 수 있지만, 인수의 해석이나 복잡한 기능의 제공은 필요 이상의 계산을 필요로 하기 때문에 처리 부담이 있다는 문제도 있다. 따라서 이러한 함수를 만드는 것은 극히 희소하다고 할 수 있다.

1.9.3 - C 언어 | 고급 기능 | 함수의 포인터

함수에 주소가 존재한다. 함수 주소를 포인터에 저장하여 호출할 함수를 변수로 교환 할 수 있다. 이것으로 호출하는 함수를 실행할 때 변화시킬 수 있다.

함수 주소의 저장

변수에 주소가 존재하듯이, 함수에도 주소라는 것이 존재한다. 함수도 결국 기계어이다, 즉 2진수 문자로 표현된 데이터가 저장되는 것이다. 함수를 실행하기 위해서는, 함수 자체의 처리 정보를 포함한 데이터를 메모리에 로드해야 한다. CPU는 기계어를 처리할 때에 메모리에 로드된 기계어를 CPU에 통합해야 한다.

물리적 컴퓨터의 사양에 따라 의존되지만, 일반적으로 CPU는 실행해야 할 기계어의 메모리 주소를 나타내는 레지스터를 보유하고, 하나의 기계어를 실행할 때마다 레지스터의 값을 증가하는 구조되어 있다. 즉, 기계어 레벨에서 명령마다 포인터가 존재하는 것과 같다.

C 언어에서 문장에 주소는 존재하지 않는다. 문장의 위치를 나타내는 것에 라벨을 사용했었다. 문장 단위의 이동을 하려면 이것으로 충분하기 때문이다. 그러나 함수의 경우는 함수명에 의한 호출은 충분하지 않을 수 있다. 함수명을 사용하여 함수를 호출하는 방법은 호출할 함수가 컴파일시에 결정되어야 한다는 것을 나타낸다. 동시에 호출할 함수를 실행할 때에 동적으로 변경할 수 없는 것을 의미한다. 이것에는 프로그램의 가능성을 크게 제한해 버리는 것이다.

예를 들어, 실행시에 호출할 함수를 동적으로 변경시킬 수 있다면, 기능을 바꾸거나 부분적인 갱신이 가능한 유연성 있는 시스템을 구축할 수 있다. 이를 실현하기 위한 기능이 함수의 주소이다.

호출할 함수의 메모리상의 위치를 시작점(entry point)이라고 한다. 시스템이 호출할 프로그램의 최초의 실행 지점을 어플리케이션 엔트리 포인트라고 한다고 하는 것은 “처음하는 C 언어“에서 설명했던 대로이다. 모든 함수에는 주소가 존재한다.

그리고 함수에 주소가 존재한다는 사실은 동시에 함수를 포인터로 처리할 수있는 것을 나타낸다. 포인터는 주소를 저장하는 수단이며, 이 사실은 함수를 변수처럼 취급하는 큰 가능성을 나타낸다. 함수의 주소를 저장하는 함수 포인터는 다음과 같이 선언한다.

함수 포인터 선언

반환형식 (* 식별자) (파라미터형 목록)

이는 대부분이 함수의 선언과 비슷하지만, 식별자 주변을 괄호로 묶는 점이 다르다. 예를 들어 문자 배열에는 인수를 값을 반환하는 함수의 포인터는 int (*pf)(const char*)와 같이 될 것이다. int *pf(const char*)로 작성하는 경우는 int에 대한 포인터를 반환하는 함수로 해석되기 때문에, 이러한 의미는 전혀 다른 것임을 이해하자.

변수의 주소를 얻을 경우는 & 연산자를 사용했지만, 함수의 주소는 함수 이름을 지정한다. 함수의 이름만을 지정했을 경우, 그것은 함수의 주소를 나타내는 것이다. 다음과 같은 함수가 선언되어 있다고 하자.

int Function(int , void *);

이 경우 다음과 같이 포인터에 주소를 대입할 수 있다.

int (*pf)(int , void*) = Function;

이것으로 함수의 포인터 pf에 Function() 함수의 주소를 대입한 것이다. 포인터에서 함수를 호출하는 경우는 일반 함수처럼 포인터 이름과 인수 목록을 지정하거나 * 연산자를 사용하여 간접 참조를 명시적으로 나타내는 두 가지 방법이 있다.

i = pf(iValue , pointer);
i = (* pf)(iValue , pointer);

어느 것을 사용하는가는 개발자의 취향이지만, 일반적으로 일반 함수와 함수 포인터를 구별할 필요가 없기 때문에 함수 포인터도 일반 함수처럼 호출한다.

코드1

#include <stdio.h>

void f(void) {
 printf("Kitty on your lap\n");
}

int main() {
  void (*fp)(void) = f;
 fp();
 (*fp)();
  return 0;
}

코드1에는 인수를 받지 않고 값을 반환하지 않는 간단한 함수를 저장 fp 포인터를 선언하고 이 포인터를 f() 함수의 주소로 초기화한다. 이 포인터는 f() 함수의 엔트리 포인트를 저장하고 있기 때문에, fp 포인터를 역참조하여 함수를 호출할 수 있다.

이러한 포인터에 의한 함수 호출은 실전에 어떻게 도움이 될 수 있을까? 하나는 앞서 설명한 바와 같이 실행할 때까지 호출할 함수가 결정되지 않은 경우에 유효하다. 예를 들어, 시스템에 함수의 주소를 전달하여 필요에 따라 시스템에서 특정 함수를 호출하여 받을 수 있다. 이러한 콜백 기능은 많은 시스템에서 채용되고 있다. Microsoft Windows와 같은 그래픽 이벤트 구동에서는 사용자가 버튼을 누르는 액션을 통해 작업을 수행하기 위해, 이벤트가 발생할 때까지 프로그램을 대기시킬 필요가 있다. 그래서 이벤트가 발생했을 때 호출되기 바라는 함수의 포인터를 미리 시스템에 등록해 두는 방법이 이용되고 있다.

또는 개발자가 자유롭게 기능을 확장할 수 있는 시스템을 구축할 때에도 사용된다. 함수의 포인터 배열을 제공하고, 필요에 따라서 이를 개발자가 함수의 포인터를 등록한다. 시스템은 처리을 할 때에 배열로부터 순서대로 함수를 호출한다. 이 함수의 연계에 의해 함수의 호출을 자유롭게 연쇄시킬 수 있기 때문에, 시스템은 확장성이 높고 유연하게 된다. 이것은 객체 지향의 상속과 오버라이드의 개념에 연결된다.

코드2

#include <stdio.h>
#define ADD_FUNC 2
#define REMOVE_FUNC 4
#define RESULT_OK 0
#define RESULT_ERROR 1

typedef void(* REGISTERPROC)(const char* , int);

int RegisterFunc(REGISTERPROC func , int mode) {
  static int iLen = 0;
  static REGISTERPROC procs[256];
 int iCount;
 char removed;

 switch(mode) {
  case ADD_FUNC:  /*함수의 추가*/
    if (iLen == 256) return RESULT_ERROR;

   /*같은 함수는 추가할 수 없다*/
   for(iCount = 0 ; iCount < iLen ; iCount++)
      if (procs[iCount] == func) return RESULT_ERROR;

   procs[iLen] = func;
   iLen++;

   for(iCount = 0 ; iCount < iLen ; iCount++)
      (*procs[iCount])("Add" , iCount);
   break;
  case REMOVE_FUNC: /*함수의 삭제*/
    removed = 0;
    for(iCount = 0 ; iCount < iLen ; iCount++) {
      if (procs[iCount] == func) {
        for(;iCount < iLen - 1 ; iCount++)
          procs[iCount] = procs[iCount + 1];
        iLen--;

       for(iCount = 0 ; iCount < iLen ; iCount++)
          (*procs[iCount])("Remove" , iCount);

        removed = 1;
        break;
      }
   }
   if (!removed) return RESULT_ERROR;
    break;
  default:
    return RESULT_ERROR;
  }

 return RESULT_OK;
}

void Function1(const char * msg , int iValue) {
 printf("Function1 : msg = %s , iValue = %d\n" , msg , iValue);
}
void Function2(const char * msg , int iValue) {
 printf("Function2 : msg = %s , iValue = %d\n" , msg , iValue);
}
void Function3(const char * msg , int iValue) {
 printf("Function3 : msg = %s , iValue = %d\n" , msg , iValue);
}

int main() {
  printf("Add Function1\n");
 RegisterFunc(Function1 , ADD_FUNC);

 printf("\nAdd Function2\n");
  RegisterFunc(Function2 , ADD_FUNC);

 printf("\nAdd Function3\n");
  RegisterFunc(Function3 , ADD_FUNC);

 printf("\nRemove Function2\n");
 RegisterFunc(Function2 , REMOVE_FUNC);

  printf("\nRemove Function3\n");
 RegisterFunc(Function3 , REMOVE_FUNC);

  return 0;
}

코드2는 함수의 포인터가 실전에서 어떤 효과를 만들어 주는지를 알기위한 시험적인 콜백 설정 함수 RegisterFunc() 함수를 작성하고 있다. RegisterFunc() 함수는 첫번째 인수에 REGISTERPROC 형의 함수의 포인터를 두번째 파라미터에 추가하거나 삭제할지 여부를 나타내는 값을 지정한다. 함수가 추가 또는 삭제에 성공하면 0 이외를, 그렇지 않으면 0을 반환한다.

먼저, 이 프로그램에서 흥미로운 것은 함수의 형태에 별명을 붙이고 있다. 함수의 포인터 타입에는 typedef 기억 클래스 지정자를 사용하여 이름을 지정할 수 있다. 이 경우는 함수명에 해당하는 부분이 포인터 형의 별명이 된다. REGISTERPFOC 형은 첫번째 인는 문자열에 대한 포인터를 두번째 인수에 숫자를 받는다. 이 함수는 발생한 이벤트를 나타내는 문자열과 등록된 함수 체인의 인덱스를 받는다.

RegisterFunc() 함수는 최대로 256의 함수를 등록할 수 있다. 함수를 등록할 경우 두번째 인수에 ADD_FUNC를 지정하고, 해제하는 경우는 REMOVE_FUNC을 지정한다. 함수가 등록되면 RegisterFunc() 함수는 등록하는 함수 및 이미 등록된 함수를 호출하고 이벤트로 통지한다. 해제하려면 해제된 함수를 제외하고 등록된 함수에 이벤트를 통지한다.

실행 결과를 보면 추가 또는 해제 할 때마다, RegisterFunc() 함수는 등록되어 있는 함수를 호출하여 추가 및 제거가 있었다는 것을 전하고 있는 것을 확인할 수 있다. 이와 같이 시스템에 등록하고 기능을 확장하는 같은 방법을 사용하는 경우, 등록하는 함수를 사용자 정의 콜백 함수라고 부른다. 설계 분야가 되므로 자세한 내용은 피하고 있지만, 고도의 시스템은 개발자가 시스템의 확장을 간단히 실현할 수 있도록, 이러한 동적 확장 수단을 제공해야 한다. 그러면 개발자는 시스템의 실체를 알 필요없이, 원하는 기능만을 확장할 수 있기 때문이다.

1.9.4 - C 언어 | 고급 기능 | 파일 및 스트림 - fopen(), fclose(), fflush() 등

표준 C 언어의 함수를 이용하여 디스크에 임의의 파일에서 문자 데이터를 읽고 쓰는 방법을 소개한다.

데이터 로드

지금까지는 메모리(주기억 장치)에 데이터를 읽거나 써왔다. 숫자나 문자 배열 등을 변수로 저장하거나, 참조할 수 있었다. 변수를 처리하는 작업은 프로그램의 가장 기본적인 항목이며, 복잡하지 않다.

그러나 메모리에 저장된 정보는 프로그램을 종료되면 사라져 버린다. 사용자가 편집 소프트웨어에서 만든 데이터나 프로그램의 로그, 설정 정보 등을 프로그램 종료 후에도 저장하고 싶은 경우 보조 기억 장치에 기록해야 한다. 즉, 하드 디스크나 플로피 디스크 등이 있다. 이러한 보조 기억 장치에 데이터를 저장하기 위해서는 파일을 조작해야 한다.

파일과 데이터를 취급할 때의 한묶음 단위로, 디스크에 있는 프로그램이나 데이터를 말한다. 디스크에 데이터를 저장하려면 변수가 아닌, 이 파일에 데이터를 저장해야 한다.

고수준 프로그래밍 언어는 어떤 입출력 장치이든지 같은 방법으로 조작할 수 있다. 만약, 하드웨어에 따라 다른 작업을 수행해야 한다면, 프로그래머에게는 큰 부담이 되어 버린다. 따라서 시스템이나 프로그래밍 언어가 이를 감추고 있는 것이다. 액세스하는 대상이 디스크 파일에서도, 네트워크에서도, 스크린이라해도 데이터의 입출력 목적은 동일에서 일관된 조작으로 실행할 수 있어야 한다는 생각이다.

이것은 스트림라는 추상적인 인터페이스를 조작하는 것으로 실현되고 있다. C 언어 프로그래머는 논리적 인 인터페이스인 “스트림"을 통해서 파일에 액세스한다. 개발자는 네트워크나 다른 장치와 입출력을 하는 경우도, 스트림을 사용하는 것만으로 프로그램 할 수 있기 때문에, 코드 재사용 및 유지 보수가 용이하게 된다.

따라서 파일 작업을 수행하려면 먼저 스트림과 파일을 연결해야 한다. 그러기 위해서는 FILE 구조체를 사용한다. FILE 구조체는 스트림의 상태에 대한 정보를 저장하고, 파일 함수에서 사용한다. FILE 구조체는 stdio.h 헤더 파일에 정의된 typedef 이름이다.

FILE *fp;

이러한 파일 구조체의 포인터 변수를 선언한다. 이 구조가 어떻게 선언되어 있는지 개발자가 신경 쓸 필요는 없다. 이 구조체를 이용하는 것은 표준 함수이고, 개발자가 멤버를 호출하는 것은 아니기 때문이다. 파일을 열려면 fopen() 함수를 사용한다. 표준 라이브러리 파일 관련 함수는 접두사 f로 시작하는 명명 규칙이 마련되어 있다.

fopen() 함수

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

이 함수는 FILE 구조체에 대한 포인터를 반환한다. 이렇게 FILE 구조체의 메모리를 할당하여 초기화하는 것은 fopen() 함수의 일이며, 개발자가 FILE 구조체를 초기화하는 일은 없다. 우리는 FILE 구조체에 대한 포인터를 받아서, 이를 파일로 처리만 하면 된다.

filename에는 열려는 파일 이름을 지정한다. 파일 이름만의 경우는 기본적으로 대상이 되는 작업 디렉토리(working directory)의 파일을 검색한다. 다른 디렉토리를 검색 할 경우, 시스템 고유의 방법으로 디렉토리를 지정한다. 예를 들어 Microsoft MS-DOS라면 C:\DirName\test.txt과 같이 지정한다.

mode에는 파일을 열 때에 읽기 전용, 쓰기 전용, 또는 모두의 액세스 모드를 문자열로 지정한다. ASCII 문자로 문자 변환(개행 코드 등)하는 텍스트 파일과 변환이 되지 않는 이진 파일의 선택 등을 지정할 수 있다. 바이너리 파일에 대해서는 “바이너리 파일"에서 설명한다.

표1 액세스 모드

모드 의미
“r” 읽기 모드로 파일을 열린다. 파일이 존재하지 않으면 NULL을 반환한다.
“w” 쓰기 모드로 파일을 열린다. 파일이 존재한다면 그 내용을 파괴하고 없으면 새로 만든다.
“a” 추가 모드로 파일을 열린다. 파일이 존재하지 않으면 만든다.
“r+” 기존 파일을 대상으로 읽기/쓰기 모드 모두에서 열린다. 파일이 존재하지 않으면 NULL을 반환한다.
“w+” 파일을 만들고 읽기/쓰기 모드 모두에서 열린다. 파일이 존재한다면 그 내용을 파괴한다.
“a+” 읽기/쓰기 모드의 두 가지 모드로 열린다. 파일이 있으면 추가하고, 존재하지 않는 경우 만든다.
“t” 텍스트 모드에서 열린다.
“b” 이진 모드로 열린다.

fopen() 함수는 파일을 여는데 실패하면 NULL을 반환한다.

FILE 구조체를 초기화하는 표준 함수의 역할이므로, 이를 해제하는 것도 표준 함수의 역할이다. 필요한 작업을 마치고 스트림이 필요하지 않은 경우 마지막으로 해제해야 한다. 파일을 닫으려면 fclose() 함수를 사용한다.

fclose() 함수

int fclose( FILE *stream );

stream에는 닫기 스트림을 지정한다. 이는 fopen() 함수에서 취득한 유효한 FILE 구조체에 대한 포인터이어야 한다. 또한 fclose() 함수에는 버퍼를 플래시하는 역할도 한다.

많은 파일 시스템은 데이터의 쓰기는 느리다. 디스크 파일 및 메모리의 큰 차이 중 하나에 액세스 속도의 격차가 있고, 디스크 액세스는 메모리 액세스보다 훨씬 느리다. 따라서 데이터를 쓸 때는 쓰기가 끝날 때까지 프로그램은 처리를 중지하고 기다려야 할 필요가 있다. 이를 효율적으로 하기 위해 파일 시스템은 일정한 데이터 양이 모이기까지는 디스크에 기록하고, 버퍼라고 하는 일시적인 저장 영역에 데이터를 대기시킨다. 버퍼에 남아있는 데이터를 쓰는 작업을 “버퍼를 플래시한다"고 표현한다.

덧붙여서, fclose() 함수 이외에, 명시적으로 플래시를 수행하려면 fflush() 함수를 사용한다.

fflush() 함수

int fflush( FILE *stream );

stream에는 플래시하는 대상의 스트림을 지정한다. 이 함수를 호출하면, 지금까지의 파일 조작이 이행되는 것을 보장된다. 스트림이 입력용으로 열려있는 경우는 버퍼의 내용을 지운다. 또한 NULL을 전달하면 출력용으로 열려있는 모든 스트림을 플래시한다.

코드1

#include <stdio.h>
#define FILENAME "test.txt"

int main(int argc , char *argv[]) {
 FILE *file;

 file = fopen(FILENAME , "r");

 if (file == NULL) {
   printf("%s: 파일이 열리지 않습니다.\n" , FILENAME);
    return 0;
 }

 printf("%s: 파일이 열립니다.\n" , FILENAME);
  fclose(file);
 return 0;
}

코드1는 fopen() 함수를 사용하여 파일을 열고 있다. 특별히 파일 작업을 하고 있지 않기 때문에 파일에 영향을 주지 않는다. 데이터의 입출력은 아무것도 하지 않고, 파일을 열 수 있는지 확인만 한다. 파일을 열 수 있다면 fclose() 함수를 사용하여 스트림을 닫은 후 프로그램을 종료한다. 이 프로그램을 실행하려면 test.txt라는 파일을 실행 파일과 같은 디렉토리에 배치해야 한다.

파일에 기록된 정보를 이용하려면 파일의 데이터를 일단 메모리에(즉, 변수에) 로드해야 한다. 읽은 파일의 내용을 검색하는 방법은 여러 가지가 있지만 그 중 하나가 fgetc() 함수이다. fgetc() 함수는 파일의 데이터를 문자로 받을 수 있기 때문에 텍스트 데이터의 로드할 때 사용한다.

fgetc() 함수

int fgetc( FILE *stream );

이 함수는 지정된 스트림의 다음 문자를 int 형으로 반환한다. 정확하게는 unsigned char 형으로 읽은 문자를 int로 캐스팅하여 반환한다. 스트림은 이 같은 파일 조작 함수를 사용하면 데이터의 읽고 쓰기의 대상이 되는 파일 위치를 자동으로 이동한다.

오류가 발생하거나 파일의 마지막에 이르렀을 경우 EOF를 반환한다. EOF는 매크로로 정의되어 있으며 보통 -1의 값이다. EOF는 End Of File의 약자이다. fgetc() 함수가 돌려주는 것은 int 형이지만, 하위 8바이트는 파일에서 로드된 내용이므로 char 형에 캐스트하여 이용할 수 있다.

코드2

#include <stdio.h>
#define FILENAME "test.txt"

int main(int argc , char *argv[]) {
  FILE *file;

 file = fopen(FILENAME , "r");

 if (file == NULL) {
   printf("%s: 파일이 열리지 않습니다.\n" , FILENAME);
    return 0;
 }

 while(1) {
    int iValue = fgetc(file);
   if (iValue == EOF) break;
   else printf("%c" , iValue);
 }
 printf("\n");

  fclose(file);
 return 0;
}

코드2는 test.txt라는 이름의 텍스트 파일을 읽고, 화면에 표시한다. 텍스트 파일은 텍스트 편집기 등을 사용하여 적당한 것을 준비한다. while 문의 루프로 fgetc() 함수를 사용하여 문자를 읽고, EOF이 아니면 printf() 함수를 사용하여 한 글자 씩 표시하고 있다.

다만, 이러한 파일 입출력 함수가 반환하는 EOF를 검출하는 방법은 확실하지 않다. 이 방법에는 EOF가 에러를 발생한건지 파일의 맨 끝까지 도달했는지를 확인할 수 없다.

그래서 지정된 스트림의 위치가 파일의 마지막을 조사하기 위해 feof() 함수를 사용한다. 이 함수를 사용하면 확실하게 파일 맨 끝을 구할 수 있다.

feof() 함수

int feof( FILE *stream );

stream에는 유효한 FILE 구조체에 대한 포인터를 지정한다. 지정된 스트림의 위치가 EOF라면 0이 아닌 값을 반환하고 그렇지 않으면 0을 반환한다.

또한 에러를 확인하려면 ferror() 함수를 사용한다.

ferror() 함수

int ferror( FILE *stream );

stream에는 유효한 FILE 구조체에 대한 포인터를 지정한다. 이 함수는 스트림에서 에러가 발생하면 0이 아닌 값을 반환하고 그렇지 않으면 0을 반환한다.

코드3

#include <stdio.h>
#define FILENAME "test.txt"

int main(int argc , char *argv[]) {
 FILE *file;

 file = fopen(FILENAME , "r");

 if (file == NULL) {
   printf("%s: 파일이 열리지 않습니다.\n" , FILENAME);
    return 0;
 }

 while(1) {
    int iValue = fgetc(file);
   if (feof(file)) break;

    if (ferror(file)) {
     printf("스트림에서 에러가 발생하였다.\n");
      break;
    }
   else printf("%c" , iValue);
 }
 printf("\n");

  fclose(file);
 return 0;
}

코드3와 코드2를 개량한 것으로, 스트림 작업에서 발생한 에러를 감지할 수 있다. 별 문제없이 파일 가져 오기를 실시할 수 있으면, 결과는 코드2와 동일하다.

데이터 쓰기

데이터를 디스크에 저장하려면 파일에 기록해야 한다. 문자 데이터의 저장은 fgetc() 대신에 문자를 쓸 함수를 사용하기만하면 된다. 문자 쓰기는 fputc() 함수를 사용한다.

fputc() 함수

int fputc( int c, FILE *stream );

c에는 기입하는 문자를, stream에 쓰기를 대상으로 하는 스트림을 지정한다. 함수가 성공하면 쓴 문자를, 실패하면 EOF를 반환한다. fputc()는 스트림의 현재 위치에 문자를 기록하여 다음 위치로 이동한다.

다만, 데이터를 쓰는 경우는 fopen() 함수의 액세스 모드가 쓰기 모드에 있어야 한다. 일반적으로 “w"를 지정하면 된다.

코드4

#include <stdio.h>
#define FILENAME "test.txt"

int main(int argc , char *argv[]) {
 FILE *file;
 int iCount;
 const char *str = "Stand by Ready!";

  file = fopen(FILENAME , "w");

 if (file == NULL) {
   printf("%s: 파일이 열리지 않습니다.\n" , FILENAME);
    return 0;
 }

 for(iCount = 0 ; str[iCount] ; iCount++) {
    fputc(str[iCount] , file);
  }

 printf("%s: 정상적으로 쓰기를 하였습니다.\n", FILENAME);

  fclose(file);
 return 0;
}

코드4는 문자열을 기록하는 파일 이름 “test.txt"과 파일에 쓸 문자열 “Stand by Ready!“을 지정한다. for 루프에서 문자 배열의 끝이 감지될 때까지 순차적으로 fputc() 함수에서 파일에 문자를 쓰는 것을 알 수 있다.

문자열의 입출력

지금까지의 파일에 대한 입출력은 문자 단위로 되어 있었다. 물론, 바이트 단위의 입출력로 할 수 있다면 문자열의 입출력이 가능하지만, 루프를 만들고 EOF를 검출하는 등 귀찮은 프로그램을 써야한다. 이것은 비생산적이다.

텍스트 파일을 다룰 때는 문자열로 입출력을 할 수 있어야 한다. 문자열의 입출력을 할 시에는 fgets() 함수 와 fputs() 함수를 사용한다. 이것들은 fgetc()와 fputc() 함수와 기본적으로 동일하지만 문자열 단위로 입출력을 할 수 있다는 점이 다르다.

fgets() 함수

char *fgets( char *string, int n, FILE *stream );

fputs() 함수

int fputs( const char *string, FILE *stream );

fgets() 함수는 string에 문자열을 포함하는 버퍼에 대한 포인터를, n에는 읽을 수 있는 최대 문자 수를 지정한다. fputs()이라면 string에 쓸 문자열에 대한 포인터를 지정한다. stream에는 대상 스트림을 지정한다.

fgets() 함수는 지정된 입력 스트림 stream에서 문자열을 읽어 들여, string에 저장한다. 문자의 해독은 현재의 스트림 위치에서 첫번째 개행 문자가 나타날지, 스트림의 끝에 도달하거나 읽은 문자가 n -1이 되는지 조건 중에 하나가 충족 될 때까지 계속된다. 함수가 성공하면 string을 반환하고, 오류가 발생하거나 스트림의 끝에 도착한다면 NULL을 반환한다.

fputs() 함수는 성공하면 음수가 아닌 값을, 에러가 발생하면 EOF를 반환한다.

코드5

#include <stdio.h>
#define READ_FILENAME "in.txt"
#define WRITE_FILENAME "out.txt"
#define BUFFER_SIZE 1024

int main(int argc , char *argv[]) {
 FILE *readFile, *writeFile;

 readFile = fopen(READ_FILENAME , "r");
  if (readFile == NULL) {
   printf("%s: 파일이 읽기 모드로 열리지 않습니다.\n" , READ_FILENAME);
    return 0;
 }

 writeFile = fopen(WRITE_FILENAME , "w");
  if (writeFile == NULL) {
    printf("%s: 파일이 쓰기 모드로 열리지 않습니다.\n" , WRITE_FILENAME);
   return 0;
 }

 while(1) {
    char chLine[BUFFER_SIZE];
   if (fgets(chLine , BUFFER_SIZE , readFile) != NULL) 
      fputs(chLine , writeFile);
    else break;
 }

 printf("%s -> %s: 정상적으로 파일 복제되었습니다.\n", READ_FILENAME, WRITE_FILENAME);
  fclose(readFile);
 fclose(writeFile);

  return 0;
}

코드5는 단순히 텍스트 파일을 복사하는 프로그램이다. 명령 라인 인수에서 복사한 텍스트 파일과 출력 파일 이름을 지정한다. 그러면 프로그램은 원본 텍스트에서 문자열을 읽어 들여, 지정된 이름의 파일을 생성하여 문자열을 복사한다. 프로그램에서는 fgets() 함수를 호출하여 문자열을 검색하고 fputs() 함수로 기록하는 작업을 반복한다.

1.9.5 - C 언어 | 고급 기능 | 표준 입출력 - fprintf(), fscanf()

커멘드 출력과 키보드 입력은 표준 출력이나 표준 입력라는 특별한 FILE 구조체의 포인터로 정의되어 있다.

기본 입출력

지금까지 printf() 함수는 화면에 문자열을 표시하고, scanf() 함수는 키보드에서 문자열을 입력할 수 있다고 설명하였다. 이는 기본적으로 틀린 것은 아니지만 정확한 표현은 아니다. 입출력에 대해 제대로 이해하고 있는 기술자라면 “printf()는 표준 출력에 문자열을 출력하고, scanf() 함수는 표준 입력에서 문자열을 입력한다"고 표현할 것이다.

실은 C 언어에서는 화면이나 키보드에 대한 데이터 입출력을 파일과 같이 조작할 수 있다. 앞전에 설명한 바와 같이 입출력은 모두가 스트림 개념으로 통일되어 있기 때문이다. 개발자는 문자열의 입출력을 파일에서도 화면에서도 네트워크에서도 동일한 방식으로 개발할 수 있는 것이다. 그러나 자주 입출력을 할 수있는 디바이스(device), 즉 화면 출력과 키보드 입력은 특별 취급된다. 많은 시스템에는 디스플레이를 표준 출력, 키보드를 표준 입력으로 정하고 있다.

표준 출력은 프로그램이 응용 프로그램 사용자에게 메시지를 표시할 경우에, 문자열을 출력할 위치를 나타내고, 표준 입력은 어떤 정보를 입력할 경우에 대상이 되는 장치를 나타낸다. 이 외에도 에러 메시지를 표시하는 전용 표준 에러라는 디바이스도 준비되어 있다. 표준 에러도 보통 디스플레이이다.

표준 출력과 표준 입력, 표준 오류는 프로그램이 결정해야하는 것은 아니다. 이러한 시스템의 이용자가 설정으로 결정해야 문제이다. 어떤 이용자는 프로그램이 화면에 출력하는 문자를 프린터와 로그 파일에 저장하고 싶을지도 모른다. 이 경우는 프로그램을 다시 작성하지 않고, 표준 출력을 변경하면 된다. 일반적으로 표준 출력 및 표준 입력은 시스템 설정으로 변경할 수 있다. 이것을 리디렉션이라고 한다. 리디렉션에 대해서는 사용하는 시스템의 도움말을 참조하자.

printf() 함수는 화면에 문자를 표시하는 함수가 아닌 표준 출력에 문자열을 표시하는 함수이며, scanf() 함수는 표준 입력에 입력하라는 함수이었다. 예를 들어, Microsoft MS-DOS를 사용하는 경우는 커멘드 라인에서 다음과 같이 프로그램을 실행 시켜서 표준 출력을 변경할 수 있다.

C:\...>dir > dir.txt

이것은 dir 명령을 사용하여 현재 디렉토리의 정보를 표시하고 있는데, 화면에 표시되어야 할 문자열이 표시되지 않고 dir.txt라는 파일이 생성된다. 이 파일이 표준 출력으로 취급되고, 문자열이 저장된다.

이렇게 문자열을 결과로 출력하는 커멘드 레벨의 응용 프로그램의 경우는 파일 작업을 할 필요는 그다지 없다. 강제로 파일 작업을 수행하는 것보다도, 오히려 이용자에게 출력하는 곳을 맡긴 것이 유연하고 사용하기 쉬운 소프트웨어가 될 것이다.

표준 출력과 표준 입력, 표준 에러도 스트림이므로 파일 관련 함수로 처리 할 수 있다. 파일 관련 함수로 입출력을 하려면 FILE 구조체에 대한 포인터가 필요하는데, stdio.h 헤더 파일에서 아래의 스트림 포인터가 제공되고 있어서 이들을 연결한 표준 스트림에 액세스할 수 있다.

stdin 상수

FILE *stdin;

stdout 상수

FILE *stdout;

stderr 상수

FILE *stderr;

이 포인터 상수에 새로운 값을 대입할 수 없다. stdin 상수는 표준 입력, stdout 상수는 표준 출력, stderr 상수는 표준 에러로 되어 있다. 함수 등이 에러를 반환하고 프로그램을 성공적으로 수행할 수 없다는 것을 사용자에게 통지하는 경우, 지금까지는 printf() 함수를 사용했지만, 정확하게는 stderr에 출력해야 한다.

코드1

#include <stdio.h>

int main(int argc , char *argv[]) {
  char chError[0xFF];

 fputs("출력하는 에러 문자열을 입력해 주세요.>" , stdout);
 fgets(chError , 0xFF , stdin);
  fputs(chError , stderr);

  return 0;
}

코드1는 stdout, stdin, stderr를 이용하여 상호 처리를 하고 있다. 예를 들어 첫번째 fputs() 함수는 표준 출력에 문자열을 표시하고 있다. 이것은 평소라면 printf() 함수를 사용하지만, 이러한 파일 입출력 함수로 정의된 표준 입출력 스트림을 사용하여 프로그램할 수 있다.

그러나 이러한 함수를 사용하면 문자열 단위 입출력 밖에 할 수 없다. printf()와 scanf() 함수와 같은 서식 제어을 파일 입출력 함수으로 한다면 편리하다. 실은 스트림을 지정할 수 있는 fprintf() 함수와 fscanf() 함수라는 것이 준비되어 있다.

fprintf() 함수

int fprintf( FILE *stream, const char *format [, argument ]...);

fscanf() 함수

int fscanf( FILE *stream, const char *format [, argument ]... );

기본적으로, 첫번째 인수에 입출력 대상의 스트림을 지정하는 것을 제외하고는 printf()와 scanf() 함수와 동일하다. fprintf() 함수는 성공하면 출력한 문자 수를 실패하면 음수를 반환한다. fscanf() 함수는 성공하면 입력된 데이터의 개수를, 실패하면 EOF를 반환한다. 이 함수를 사용하면 파일 입출력에서도 printf()와 scanf() 함수처럼 프로그램할 수 있다. 반대로

printf("Kitty on your lap");

fprintf(stdout , "Kitty on your lap");

는 같다고 생각할 수 있다.

코드2

#include <stdio.h>

int main() {
  char chSelect;
  fprintf(stdout , "출력을 선택하세요. o/e>");
  fscanf(stdin , "%c" , &chSelect);

 switch(chSelect) {
  case 'e':
 case 'E':
   fprintf(stderr , "Kitty on your lap\n");
   break;
  case 'o':
 case 'O':
   fprintf(stdout , "Kitty on your lap\n");
   break;
  }

 return 0;
}

코드2는 fscanf() 함수으로 stdin 상수를 매개 변수로 지정하고 있기 때문에 scanf() 함수처럼 표준 입력에서 데이터를 읽어들인다. 프로그램은 사용자에게 표준 출력 또는 표준 에러를 어느쪽으로 출력을 선택하고 있다. 그 후에 fprintf() 함수를 사용하여 표준 출력이나 표준 에러에 문자열을 출력한다.

이 프로그램에는 단순히 표준 입력과 표준 출력에 대해 출력하고 있는데, 매개 변수에 파일을 가리키는 FILE 구조체의 포인터를 설정하면, 서식 지정을 이용하여 데이터의 쓰기와 읽기를 할 수 있다.

1.9.6 - C 언어 | 고급 기능 | 바이너리 파일 - fwrite(), fread()

표준 C의 함수를 사용하여 텍스트가 아닌 순수 이진 수열의 데이터를 파일에 읽고 쓰는 방법을 설명한다.

순수한 데이터

fputs() 함수와 fgets() 함수는 텍스트 데이터의 입출력을 할 때에 편리했다. 또한 데이터의 변환을 하는 경우는 fprintf() 함수와 fscanf()가 편리하다. 그러나 이들은 ASCII 코드를 다루는 경우에는 편리하지만, 순수한 숫자 데이터를 입출력시키고 싶은 경우는 적절하지 않다. 텍스트가 아닌 원시 이진 데이터를 처리하려면이 함수는 사용할 수 없다.

물론 방법의 하나로는 fprintf()와 fscanf()를 사용하여 텍스트 및 숫자의 변환을 채용할 수 있다. 저장할 때는 텍스트 데이터로 변환하여 로드할 때에 필요한 데이터는 숫자로 변환하는 것이다. 그러나 변환을 할 때 걸리는 시간 문제도 있고, 텍스트 데이터는 다른 소프트웨어를 사용하여 저장된 데이터를 볼 수 있거나 재작성 될 가능성이 있다. 응용 프로그램의 종류에 따라서는 이것이 선호되지 않는 경우도 있을 것이다.

이진 데이터이면 프로그램이 직접 수치로 읽어 올 수 있다. 예를 들어 구조체의 값을 출력하는 것을 생각해보자. 이것을 텍스트 데이터를 취급되면, 번거로운 변환 처리을 작성해야 한다. 그러나 바이너리 데이터이면 직접 읽고 쓸 수 있는 것이다.

바이너리 데이터를 쓰는데는 fwrite() 함수를 사용한다.

fwrite() 함수

size_t fwrite( const void*buffer, size_t size, size_t count, FILE *stream );*stream );

buffer에 기록하는 데이터를 저장하는 메모리에 대한 포인터를 size에는 바이트 단위로 데이터의 크기를 count에는 데이터의 개수를 지정한다. 예를 들어 buffer가 int 형의 배열에 대한 포인터를 가리키는 한다면 size에는 sizeof(int)를 지정하고, count에 쓰는 요소의 수를 지정한다. 마지막 인수 stream에는 쓰는 대상의 스트림을 지정한다.

buffer는 void*의 포인터이므로 형식은 묻지 않는다. 함수는 일체의 변환 처리를 하지 않고 입출력한다. 함수가 성공하면 실제로 쓴 항목 수를 반환한다. count 인수로 지정한 값보다 낮은 값이 반환된 경우, 어떠한 에러가 발생하고 있을 가능성이 있다.

fwrite() 함수는 인수가 복잡한 것처럼 보이지만, 단순하게 말한다면 buffer에서 size 바이트 단위로 count 회수로 stream에 기록하는 것이다. 배열 등의 연속적인 데이터를 저장할 때 유용하다.

순수한 바이너리 데이터로 입출력을 할 경우는 fopen() 함수의 액세스 모드를 지정으로 이진 데이터임을 나타내는 “b"를 지정한다. 쓰기 모드 “w"를 같이하여 “wb"으로 표현할 수 있다. 텍스트 데이터로 fwrite() 함수로 쓴 경우, 일부 코드가 자동 변환되어 버리는 등의 문제가 발생할 수 있다.

코드1

#include <stdio.h>

typedef struct {
  int left , top , right , bottom;
} RECT;

int main() {
 FILE *file;
 RECT rect;

  printf("사각형의 네 모서리의 좌표를 나타내는 숫자를 입력하십시오. >");
 scanf("%d %d %d %d" , &rect.left , &rect.top , &rect.right , &rect.bottom);

 printf("left=%d, top=%d, right=%d, bottom=%d\n",
   rect.left , rect.top , rect.right , rect.bottom);

 file = fopen("rect.dat" , "wb");
  if (file == NULL) {
   printf("파일을 쓰기 모드로 열 수 없습니다.");
   return 0;
 }
 fwrite(&rect , sizeof(RECT) , 1 , file);
  fclose(file);
 return 0;
}

코드1은 값의 입력을 요구되므로 4개의 숫자를 입력한다. 이 값은 사각형의 좌표로 RECT 구조체의 변수에 저장된다. 그 후, rect.dat라는 파일이 생성되고, fwrite() 함수를 사용하여 값을 저장한다. fwrite() 함수에는 저장하는 대상 버퍼에 rect 변수에 대한 포인터를 지정하고, 크기에 sizeof(RECT)을 지정한다. rect 변수는 배열이 아니기 때문에 count 인수에는 1을 지정한다.

생성된 rect.dat 파일은 텍스트 데이터가 아닌 바이너리 데이터이므로, 바이너리 편집기 등으로 편집할 수 있다. 32비트 환경이라면 16바이트의 파일이 생성되어 있을 것이다.

바이너리 데이터를 읽어들이는 fread() 함수를 사용한다.

fread() 함수

size_t fread( void *buffer, size_t size, size_t count, FILE *stream );

기본적으로 인수는 fwrite() 함수와 동일하다. buffer에는 읽어 들인 데이터를 저장할 버퍼의 포인터를 지정하고, size는 바이트 단위로 데이터의 크기를, count에는 데이터의 개수를, stream에는 로드되는 스트림을 각각 지정한다. buffer는 읽기에 충분한 메모리 공간이 확보되어 있지 않으면 안된다. 반환 값은 실제로 읽은 전체 항목 수를 반환한다. 이 수가 count보다 작은 경우는 에러가 발생했는지 파일의 맨 끝에 알린다.

코드2

#include <stdio.h>

typedef struct {
 int left , top , right , bottom;
} RECT;

int main() {
 RECT rect;
  FILE *file = fopen("rect.dat" , "rb");

  if (file == NULL) {
   printf("파일을 읽기 모드로 열 수 없습니다.\n");
    return 0;
 }
 fread(&rect , sizeof(RECT) , 1 , file);
 fclose(file);

 printf("left=%d, top=%d, right=%d, bottom = %d\n" ,
    rect.left , rect.top , rect.right , rect.bottom);
 return 0;
}

코드2는 코드1에서 생성된 rect.dat 파일을 읽어 들이기 위한 프로그램이다. fread() 함수를 사용하여 RECT 형 변수 rect에 rect.dat 파일에서 데이터를 로드하는 것을 확인할 수있을 것이다. 프로그램은 마지막으로 printf() 함수 rect 변수의 값을 표시한다.

1.9.7 - C 언어 | 고급 기능 | 랜덤 액세스 - fseek(), ftell()

파일의 시작 부분부터 데이터를 읽고 쓰는 방식을 순차 액세스라 하고, 임의의 위치로 이동하여 필요한 부분 만 읽고 쓰는 방식을 랜덤 액세스라고 한다. 여기에서는 표준 C 함수를 사용하여 파일의 임의의 위치에서 데이터를 읽고 쓰는 방법을 설명한다.

임의의 위치를 읽어오기

지금까지의 파일 읽기는 파일의 처음부터 끝까지 순서대로 진행되었다. 파일 함수를 사용하여 가져 오면 파일 위치는 다음 항목에 자동으로 진행되기 때문에, 파일의 시작부터 순서대로 입출력할 수 있었다. 스트림의 다음에 읽고 쓰는 대상의 바이트의 정보를 파일 포인터라고 한다.

파일의 처음부터 순서대로 읽을 파일 형식을 시컨스 액세스라고 한다. 단순한 텍스트 데이터의 경우는 대부분이 순서대로 읽거나 기록하는데 매우 효율적이다. 그러나 바이너리 데이터의 경우는 이 방법만에는 문제가 발생한다.

이진 파일의 경우는 다른 목적의 데이터가 연속하고 있는 것이 많다고 생각된다. 예를 들어, 최초의 16바이트는 위치와 크기, 그 다음에는 4바이트으로 응용 프로그램의 종료 상태를 나타내는 숫자, 그 다음에는 이전의 기동 일시와 같은 상태이다. 이러한 정보는 별도의 파일로 나누는 것보다도, 한번에 모와 버리는 것이 효율적이기에 많은 프로그래머들이 이러한 설계하는 것이다.

그러나 처음부터 20번째 바이트의 어떤 수치만 가져 오려는 것뿐이라면, 지금까지처럼 처음부터 차례로 읽어 들이는 방법은 최적이 아니다. 그래서 바이너리 파일의 경우는 임의의 위치에 접근할 수 있다면 매우 유용하다고 생각된다. 이러한 특정 위치에 파일 액세스를 랜덤 액세스라고 한다.

랜덤 액세스를 실현하려면, 파일 포인터의 위치를 이동시킬 필요가 있다. 지정한 위치에 파일 포인터를 이동 시키려면 fseek() 함수를 사용한다.

fseek() 함수

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

stream에는 유효한 FILE 형 변수의 포인터를 지정한다. offset에는 origin으로 지정된 위치에서 이동하는 바이트 수를 지정한다. origin에는 stdio.h 헤더에 정의되어있는 다음 상수 중 하나를 지정한다. stream에 지정된 스트림의 파일 포인터는 origin로부터 offset만큼 이동한 위치로 변경된다.

표1 - 탐색(seek) 시작 위치

상수 의미
SEEK_CUR 현재의 파일 포인터의 위치
SEEK_SET 파일의 시작
SEEK_END 파일 끝

반환 값은 성공하면 0을 반환하고, 그렇지 않으면 0이 아닌 값을 반환한다.

만약 현재의 파일 포인터의 위치를 알고 싶어한다면 ftell() 함수를 사용하여 얻을 수 있다.

ftell() 함수

long ftell( FILE *stream);

이 함수는 지정된 스트림의 파일 포인터를 돌려준다.

코드1

#include <stdio.h>

int main() {
 char fileName[256];
 int fileIndex , text;
 FILE *file;

 printf("읽을 파일 이름을 지정하십시오>");
  scanf("%s" , fileName);
 printf("파일을 읽을 시작 위치를 바이트 단위로 지정하십시오.");
 scanf("%d" , &fileIndex);

 file = fopen(fileName , "rb");
  if (file == NULL) {
   fprintf(stderr , "파일 조작에 에러가 발생했습니다.\n");
    return 0;
 }

 fseek(file , fileIndex , SEEK_SET);
 printf("파일 위치 %d에서 읽습니다--\n" , ftell(file));

 while(1) {
    text = fgetc(file);
   if (feof(file)) break;
    printf("%c" , text);
  }

 printf("\n");
  fclose(file);
 return 0;
}

코드1을 실행하면 먼저 오픈할 파일 이름을 입력해야 한다. 다음에 지정된 파일이 읽기 시작할 바이트를 지정한다. 예를 들어 100을 지정하면 지정한 파일의 100번째 바이트부터 표시한다. 이 프로그램은 읽어 들인 데이터를 문자로 표준 출력에 출력한다.

코드1은 fseek 함수로 SEEK_SET 즉, 파일의 처음부터 입력된 값 fileIndex 바이트가 나아간 위치에 파일 포인터를 이동시키고 있다. 이렇게 하면 파일 작업을 할 때에 목적하는 특정 값만 얻을 수 있다. 이러한 랜덤 액세스는 바이너리 데이터를 관리할 때 매우 유효하게 될 것이다.

1.9.8 - C 언어 | 고급 기능 | 문자열 편집 - strcat(), strlen(), strcpy(), strcmp() 등

여러 문자열을 결합하거나 문자열의 일부를 다른 문자열을 삽입하는 것과 같은 작업은 문자열 작업을 수행 표준 함수를 사용한다.

문자열을 추가 및 변환 처리

일반적으로 많은 고급 언어는 직관적인 문자열 조작 기능을 제공한다. 인간의 감성을 생각하면 다음과 같은 문장은 문자열의 추가 처리인 것을 기대할 것이다.

"오늘은 " + 2017 + "년 "+ 11 + "월 " + 26 + "일 입니다"

이 수식은 여러 문자열과 숫자로 구성되어 있다. 고급 언어의 대부분은 식을 최종적으로 문자열로 변환한다.

"오늘은 2017년 11월 26일 입니다"

많은 프로그래머는 문자열의 덧셈 연산이 가능한 결과를 원할 것이다. 그러나, C 언어의 경우는 문자열 리터럴의 실체는 문자 배열이므로 배열의 앞에 포인터로 처리된다. 따라서 문자열 편집하려면 직접 배열을 조작할 수 밖에 없다. 문자열을 추가하려면 충분한 메모리 공간을 할당하고, 문자열의 맨 끝에서 추가할 문자열의 각 문자를 순서대로 대입해 나갈 필요가 있다. 문자열의 일부를 잘라내거나 숫자와 문자열의 상호 변환을 할 때도 마찬가지이다.

그러나, 이를 모두 자기 부담으로 준비하는 것은 효율적이지 않다. 그래서 표준 라이브러리는 문자열을 조작하기 위한 기본적인 함수들을 제공하고 있다. 문자열 관련 함수는 string.h 헤더 파일에 선언되어 있다. 기본적인 문자열의 추가는 strcat() 함수를 사용한다.

strcat() 함수

char *strcat( char *string1, const char *string2 );

strcat() 함수는 string1에 string2를 추가한다. 이 함수의 반환 값은 string1과 동일한 것이고, 에러 값을 반환하는 것은 아니다. string1와 string2은 NULL로 끝나는 문자 배열이어야 한다.

함수는 string1의 NULL 문자를 string2의 앞에 문자열로 덮어쓰고, 그 후의 문자열을 추가하는 것이다. string1에 할당된 메모리 공간이 부족하기도 하고, string1과 string2가 겹쳐져 있는 경우의 동작은 정의되지 않는다. 즉, string1과 string2는 원칙적으로 다른 문자 배열에 대한 포인터이어야 한다.

코드1

#include <stdio.h>
#include <string.h>

int main() {
 char str1[256] = "Kitty on your lap ";
  strcat(str1 , "~당신의 무릎 위에 고양이~");
 printf("%s\n" , str1);

 return 0;
}

코드1에는 단순히 str1에 다른 문자열을 strcat() 함수를 사용하여 추가한다. str1는 문자열을 추가할 수 있도록 여분에 배열 크기를 할당하고 있다. 리터럴 문자열에 문자열을 추가하지 않도록 주의한다.

이러한 처리를 위해서는 문자열의 문자수가 중요하다. 함수가 항상 문자열의 수를 파악할 수 있는 것은 아니다. 동적으로 문자열 포인터에서 문자 수를 확인하고, malloc() 함수에서 힙을 할당하여, 이에 문자열을 편집하는 처리를 할 수 있을 것이다. 문자수를 확인하려면 배열의 처음부터 조사하여 NULL 즉 0의 값을 발견 할 때까지 계산하여 얻을 수 있지만, 이 처리도 표준 함수가 지원해주고 있다. 문자수를 얻으려면 strlen() 함수를 사용한다.

strlen() 함수

size_t strlen( const char *string );

string의 문자수를 확인하고 싶은 NULL로 끝나는 문자 배열의 포인터를 지정한다. strlen () 함수는 string의 문자 수를 반환한다. 이 문자 수에는 맨 끝의 NULL 문자는 포함되지 않으므로 주의하자.

코드2

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

int main() {
 const char *str1 = "Kitty on your lap ";
  const char *str2 = "~당신의 무릎 위에 고양이~";
 char *str3 = (char*)malloc(strlen(str1) + strlen(str2) + 1);
  *str3 = 0;

  strcat(str3 , str1);
  strcat(str3 , str2);
  printf("%s\n" , str3);

 free(str3);
 return 0;
}

코드2에는 strlen() 함수를 사용하여 문자열에 대한 포인터 str1과 str2에서 문자 수를 조사하고 있다. 문자 수는 malloc() 함수에 의해 메모리 할당에 사용되고, 동적으로 문자의 가산 처리를 실시하고 있다. str3는 str1과 str2의 문자열을 strcat()에 추가하기 위해 충분한 메모리 공간이 할당한다. 이러한 처리를 실시하면, 매개 변수로 받은 문자열에 대한 포인터 등, 함수의 작성 단계에서 문자를 정적으로 파악할 수 없는 경우에도 올바른 문자열 처리를 할 수 있다.

코드2의 중간에 str3에 문자열 str1을 추가하기 위해 str3의 앞 요소에 일부러 0 (NULL 문자)를 대입하고, strcat() 함수를 사용하고 있는데, 이것은 제대로 된 방법이라고 할 수 없다. 첫번째 strcat(str3, str1)는 단순히 str3에 str1을 복사하고 있을 뿐이다. 문자열을 복사할 경우 strcpy() 함수를 사용하는 것이 좋다.

strcpy() 함수

char *strcpy( char *string1, const char *string2 );

이 함수는 단순히 string1에 string2를 NULL 문자를 포함하여 복사한다. 그 이외의 동작은 기본적으로 strcat() 함수와 같고, 함수는 대상에 문자열 string1을 반환한다. string1에 할당된 메모리 공간이 부족하거나, string1과 string2가 겹쳐져 있는 경우의 동작은 정의되지 않는다.

그런데 이러한 문자열 편집 처리를 프로그램을 수행할 경우, if문으로 문자열 비교가 요구될지도 모른다. 문자열에 대한 포인터가 동일한지 여부를 조사하는 것은 간단하지만, 두개의 NULL로 끝나는 문자 배열이 완전히 똑같은 내용의 문자 배열을 확인하려면 strcmp() 함수를 사용한다.

strcmp() 함수

int strcmp( const char *string1, const char *string2 );

string1과 string2는 비교할 NULL으로 끝나는 문자열에 대한 포인터를 지정한다. 함수의 반환 값이 0이면 두 문자열이 같은 문자열임을 나타낸다. 음수를 돌려 주었을 경우는 사전 순으로 string1이 string2보다 작고, 양수를 반환하면 string1이 string2보다 크다는 것을 나타낸다.

코드3

#include <stdio.h>
#include <string.h>

int main() {
 char str1[255], str2[255];

  printf("문자열 2개를 입력하십시오.>");
 scanf("%s %s", str1, str2);

 if (strcmp(str1 , str2) == 0)
   printf("%s와 %s는 같다.\n" , str1 , str2);
 else if(strcmp(str1 , str2) < 0)
    printf("%s와 %s보다 작다.\n" , str1 , str2);
  else
    printf("%s는 %s보다 크다.\n" , str1 , str2);

  return 0;
}

코드3은 커멘드 라인 인수로 2개의 문자열을 입력한다. 그러면 프로그램은 입력된 문자열을 strcmp() 함수로 비교하고, 그 결과를 표준 출력에 표시한다. 문자열을 사전 순으로 정렬하거나, 동일한 문자열 여부를 확인 할때에 strcmp()는 유용하다.

여기까지 소개한 문자열 관련 함수는 간단한 문자열의 제어에 자주 사용되는 함수이지만, 복잡한 처리가되면 이것만으로는 실현될 수 없다. 예를 들어 숫자 형식의 변수를 문자열로 변환하여 다른 문자열에 추가하려면 매우 시간이 걸릴 것이다. 이를 쉽게 달성할 방법이 있다면 편리하지만, 문자열 관련 함수에 그런 것은 없다.

그러나, 우리는 이미 고급 문자열 변환을 수행하는 함수를 사용하고 있다. printf() 함수와 fprintf() 함수이다. 이것들은 서식 제어 문자열과 가변 개수의 옵션 인수를 사용하여 숫자나 부동소수, 문자 등을 하나의 문자열로 출력할 수 있었다. printf()와 fprintf() 함수는 문자열을 스트림에 출력했지만, 메모리 버퍼에 출력할 수 있으면, 문자열로 변환을 printf() 함수처럼 할 수 있다는 것이다. 이를 실현하는 함수가 sprintf () 함수이다.

sprintf() 함수

int sprintf( char *buffer, const char *format [, argument] ... );

buffer에는 문자열의 출력이 되는 버퍼의 포인터를, format에는 서식 제어 문자열을, argument는 옵션 인수를 지정한다. format과 argument 내용은 printf() 함수와 완전히 동일하다. 첫번째 인수에 출력의 포인터를 지정하는 점에서 printf() 함수와 다르다. 이 함수를 사용하면, 본래 printf() 함수가 표준 출력에 표시하게 될 문자열을 버퍼에, 즉 충분한 저장 공간이 할당된 문자 배열로 출력할 수 있다.

sprintf() 함수가 있기 때문에 예상할 수 있지만 sscanf() 함수도 존재한다. 이 함수는 버퍼에서 입력을 수행한다.

sscanf() 함수

int sscanf( const char *buffer, const char *format [, argument ] ... );

이 함수도 buffer에 버퍼에 대한 포인터를 지정한다. format에는 서식 제어 문자열, argument에는 옵션 인수를 지정한다. 역시, sprintf()와 동일하게 첫번째 인수에 버퍼에 대한 포인터를 지정하는 점을 제외하고 scanf() 함수와 동일하다.

코드4

#include <stdio.h>

int main() {
 char str[256];
  sprintf(str , "오늘은 %d년 %d월 %d일이다." , 2017 , 11 , 26);
 printf("%s\n" , str);
  return 0;
}

코드4는 sprintf() 함수를 사용하여, 지금까지 표준 출력에 문자열을 표시해 온 방법과 동일한 방법으로 문자열 편집을 실시하고 있다. 이 방법이라면 수치나 다른 문자열 등을 일괄적으로 문자열로 변환할 수 있다. 마찬가지로, 문자열을 숫자 등으로 변환하는 경우는 sscanf() 함수를 사용하기만 하면 된다.

1.9.9 - C 언어 | 고급 기능 | 시간 함수 - time(), localtime()

C 언어의 표준 함수를 사용하여 컴퓨터의 시간을 가져온다. 시간은 단순한 수치로 처리되어 있기 때문에, 날짜로 취급하려면 시스템의 로컬 시간으로 변환해야 한다.

시간을 처리하기

데이터 관리를 할 때, 시간은 중요한 존재이다. 예를 들어 정보가 업데이트 되었을 때는 프로그램 업데이트 로그를 남기는 등의 처리가 요구된다. 많은 시스템에는 시스템 시계가 있기 때문에 이용자에게 “지금의 시간을 입력하라”‘고 묻는다 프로그램은 불친절하다. 프로그램은 지금 날짜를 알고 싶다면, 시스템 시계에서 시간을 산출한다.

현재 시간을 얻거나, 새로 설정하는 프로그램을 만드는 경우 기본적으로 시스템에 문의해야 한다. 시스템이 지원하고 있는 시간 관련 기능을 최대한으로 발휘시키는 경우는 C 언어의 표준 함수에서만 지원할 수 있는 것이 아니기 때문이다. 그러나, 복잡한 API를 사용하지 않아도 시간의 취득이나 변환 등의 기본적인 처리는 표준 함수에서 지원되고 있다.

시간 관련 함수는 time.h 헤더 파일에 선언되어 있다. 시스템 시계에서 시간을 얻으려면 time() 함수를 사용한다.

time() 함수

time_t time( time_t *timer );

timer에는 시스템 시간을 얻는 time_t 형 변수의 포인터를 지정한다. time_t 형은 time.h 헤더 파일에 정의되어 있는 시간을 나타내는 산술형의 typedef 명이다. time_t 형의 실제 형식은 구현에 따라 달라진다. time() 함수는 인수에서 얻은 값과 같은 값을 반환한다. 즉, time () 함수는 시간을 얻는 수단으로 포인터에 간접 참조에서 값을 저장하는 방법 및 반환 값에서 얻는 방법을 선택할 수 있다. 반환 값을 얻는 경우는 인수를 NULL로 해도 상관없다.

이 함수에서 얻을 수 있는 값은 시스템의 부호화된 time_t 형 달력 시간이다. 시간을 얻을 수 없는 경우는 -1을 돌려준다. time() 함수가 반환하는 값은 구현에 의존하기 때문에, 사용하는 시스템의 시간 방식을 확인한다. Microsoft Windows에는 시스템 클럭에 따라 만국 표준시(UCT)의 1970 년 1월 1일 0시 0분 0초부터 경과된 시간을 초단위로 나타내는 숫자를 반환한다.

이대로는 매우 시간이라고는 생각되지 않는듯한 수치가 나타날 뿐이기 때문에(무엇보다, 시스템에 의존하는 시간 처리는 시스템에 의존한 소스를 쓰는 것이 되기 때문), 이를 현재 시간으로 변환해야 한다. 연산에 의해 경과 시간에서 현재 시간을 얻을 수 있지만, localtime() 함수가 이 처리를 수행해 준다.

localtime() 함수

struct tm *localtime( const time_t *timer );

timer에는 변환하는 시간 값에 대한 포인터를 지정한다. 시간은 년/월/일 등의 값을 나타내는 멤버가 있는 tm 구조체에 대한 포인터를 반환한다. tm 구조체는 다음과 같이 선언되어 있다.

tm 구조체

struct tm {
        int tm_sec;   /* 초 - [0~61] (閏秒を考慮) */
        int tm_min;   /* 분 - [0~59] */
        int tm_hour;  /* 시 - [0~23] */
        int tm_mday;  /* 일 - [1~31] */
        int tm_mon;   /* 월 - [0~11] */
        int tm_year;  /* 1900부터의 년 */
        int tm_wday;  /* 일요일부터의 요일 - [0~6] */
        int tm_yday;  /* 년초부터의 통산 일수 - [0~365] */
        int tm_isdst; /* 서머 타임이 유효하면 양수, 유효하지 않으면 0, 불명이면 음수*/
};

localtime() 함수는 인수 timer에서 지정된 시간 값을 바탕으로 각 멤버를 적절한 값으로 초기화되어 있는 tm 구조체에 대한 포인터를 반환한다. 이 함수가 반환된 포인터를 이용하면 시간 값에서 사람이 이해할 수 있는 시간을 표시하는 것이 가능하다.

tm 구조체의 월은 0부터 시작되므로, 1월은 0임을 주의한다. 또한 년은 1900년부터 계산된 값이다. 실제 서기로 변환하려면 1900을 추가해야 한다.

코드1

#include <stdio.h>
#include <time.h>

int main() {
 struct tm *date;
  const time_t t = time(NULL);
  date = localtime(&t);

 printf(
   "%d/%d/%d %d:%d:%d\n" , date->tm_year + 1900 , date->tm_mon + 1 ,
    date->tm_mday , date->tm_hour , date->tm_min , date->tm_sec
 );

  return 0;
}

코드1은 현재 시간을 표시하는 프로그램이다. 시간은 년/월/일 시:분:초 형태로 표준 출력에 표시된다. 프로그램은 먼저 time() 함수를 사용하여 시스템 시계에서 시간 값을 가져온다. 다음에 localtime() 함수를 사용하여 시간 값을 tm 구조체로 변환한다. 다음은 tm 구조체의 시간을 나타내는 각 멤버에 액세스하여 시간을 표시한다.

1.9.10 - C 언어 | 고급 기능 | 난수 - rand(), srand()

C 언어의 표준 함수를 사용하여 적당한 값을 얻는 방법을 설명한다.

랜덤 값 얻기

많은 게임에서는 사용자가 예상할 수 없는 결과를 얻어야 한다. 또는 자연 과학 및 사회 과학 등의 시뮬레이션을 수행하는 프로그램에서도 일정하게 예기치 않은 변화를 준비해야 한다. 일반적으로 비즈니스 응용 프로그램은 필요하지 않겠지만, 프로그램의 종류에 따라서는 억지로 부정확한 결과를 산출해야 하는 경우도 있는 것이다.

이것을 실현하려면 난수를 구해야 한다. 난수를 사용하기 위한 함수는 stdlib.h 헤더 파일에 선언되어 있다. 난수를 이용하면, 게임 프로그래밍은 물론, 반복 처리와 난수를 사용한 문제를 처리하는 몬테 카를로 방법(Monte Carlo method)이라는 수학 기법을 사용한 프로그램 등, 다양한 용도가 있을 수 있다.

난수를 구하려면 rand() 함수를 사용한다.

rand() 함수

int rand( void );

rand() 함수는 0부터 RAND_MAX 범위 내에서 int 형 난수를 반환한다. RAND_MAX는 stdlib.h 헤더 파일에 정의되어 있는 rand() 함수가 반환하는 최대 값을 나타내는 상수이다.

이 함수가 반환하는 값은 완전한 난수가 아닌, 특정 계산식으로 산출한 적당한 값이다. 일반적으로 컴퓨터는 난수를 발생시키는 하드웨어를 가지고 있지 않다. 그래서 난수를 얻기 위해 기본이 되는 값을 계산하고 난수를 생성한다. rand() 함수는 생성된 난수를 얻기 위한 함수이다.

코드1

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

int main() {
  int iCount;

 printf("/// 난수의 최대값 = %d ///\n" , RAND_MAX);

 for(iCount = 0 ; iCount < 10 ; iCount++)
    printf("난수 열 %d = %d\n" , iCount , rand());

  return 0;
}

코드1을 실행하면 RAND_MAX 상수 값과 rand() 함수에서 얻은 의사 난수를 표시한다.

(컴퓨터에서 만들 수 있는 난수는 임의적일 수 밖에 없어서 난수처럼 보이지만 완전한 무작위는 아니다. 이를 의사난수라고 한다.)

표준은 stdlib.h에 정의되어 있는 매크로 RAND_MAX는 32767 이상임을 보증한다. 표시되는 의사 난수를 보면 확실히 rand() 함수는 0 ~ 32767 이내의 적당한 값을 반환하는 것을 확인할 수 있다.

그러나 코드1을 다시 실행하면 똑같은 결과를 얻는다. 난수 열는 확실히 적당한 값이지만, 프로그램을 실행할 때마다 같은 난수 열을 반환하도록 것은 이 프로그램의 실행 결과가 항상 같으므로 임의 처리로는 사용할 수 없다. 왜, rand() 함수는 같은 난수 열을 반환할까?

실은 의사 난수를 발생시키기 위해서는 “씨"을 뿌려야 한다. 난수 열은 기본이 되는 값을 사용하여 생성된다. 사용되는 계산식은 항상 동일하기 때문에, 기본 값이 동일하면 같은 난수 열이 생성되어 버린다. 코드1이 항상 같은 결과가 되는 것은 난수를 생성하기 위한 기준 값이 같았기 때문이다. 이 기준 값을 시드(Seed)라고 한다. 난수를 생성하기 위한 시드를 설정하려면 srand() 함수를 사용한다.

srand() 함수

void srand( unsigned int seed );

seed에는 난수를 생성하기 위한 시드 값을 지정한다. 난수는 seed로 지정된 값을 초기 값으로 난수를 생성한다. seed에 1을 지정하면 난수가 초기화된다. srand ()를 호출하기 전에 rand()가 호출된 경우 seed를 1로 난수 열이 생성된다.

프로그램을 실행할 때마다 예기치 못한 적당한 값을 얻기 위해서는 항상 다른 시드를 부여해야 한다. 난수의 생성에 가장 많이 사용되는 초기 값은 시간이다. time() 함수를 사용하여 얻은 값은 항상 다른 값이 되기 때문에 개발자도 예상할 수 없는 적당한 값을 동적으로 얻을 수 있다.

코드2

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

int main() {
  int iCount;

 srand(time(NULL));
  for(iCount = 0 ; iCount < 10 ; iCount++)
    printf("난수 열 %d = %d\n" , iCount , rand());

  return 0;
}

코드2는 rand() 함수를 호출하기 전에 srand() 함수를 이용하여 난수의 초기 값을 지정한다. time() 함수를 사용하여 현재 시간 값을 시드로 지정하고 있기 때문에 이 프로그램을 실행할 때마다 다른 초기 값을 난수의 생성에 사용한다. 이번에는 실행할 때마다 적당한 값을 얻을 수 있을 것이다.

그러나 이 상태에서는 특정 범위의 난수를 얻을 수 없다. 일반적으로 32767까지의 넓은 범위의 난수를 사용하지 않는다. 반대로, 경우에 따라서는 32767 이상의 넓은 범위의 난수를 얻고 싶을 때도 있을 것이다. 이것을 실현하려면 rand() 함수에서 얻은 결과를 계산하고 해결한다. 지정 범위의 난수를 얻는 가장 쉬운 방법은 난수와 최대 값을 나눈 나머지 값을 얻는 방법이다. 0~9의 난수가 필요한 경우는 rand() % 10를 계산하여 얻을 수 있다.

하지만, 이 방법은 그다지 권장되지 않는다. 더 좋은 방법으로, 0.0에서 1.0까지의 부동 소수점형 의사 난수를 사용하는 수법이 선호된다. 0~1 사이의 부동 소수점 의사 난수를 얻을 수 있다면, 이것은 최대 값을 곱하여 특정 범위의 난수를 얻을 수 있다. 이 방법이면 난수의 최대 값에 얽매이지는 않는다. 0.0에서 1.0 사이의 난수를 얻으려면 다음과 같은 매크로를 작성하는 것이 좋다.

#define random() ((double)rand() / (RAND_MAX + 1))

random () 매크로 함수는 0.0에서 1.0 사이의 난수를 반환한다.

코드3

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

#undef random
#define random() ((double)rand() / (RAND_MAX + 1))

int main() {
 int iCount;
 srand(time(NULL));
  for(iCount = 0 ; iCount < 10 ; iCount++)
    printf("난수 열 %d = %g\n" , iCount , random());
  return 0;
}

코드3은 0.0에서 1.0까지의 double 형의 난수를 반환 random() 매크로 함수를 정의하고 있다. 덧붙여서 호환성을 위해 일부 처리계에서는 매크로 함수 randam()가 이미 정의되어 있을 수 있다. 그래서 #undef 지시문을 사용하여 random()가 중복되지 않는 것을 보장하고 있다. 불필요한 수고가 싫다면 이름이 충돌하지 않도록 다른 이름으로 매크로 함수를 정의해도 상관없다.

1.9.11 - C 언어 | 고급 기능 | 와이드 문자 - wprintf(), wscanf(), setlocale()

C 언어로 1문자에 2바이트 이상을 요구하는 문자 집합을 처리하는 와이드 문자에 대해 설명한다. 와이드 문자에 사용되는 대표적인 문자는 Unicode이고, C 언어에서 Unicode와 같은 국제화 대응의 문자 집합을 처리하기 위해 와이드 문자가 사용된다.

국제화 대응

지금까지의 시대는 모든 컴퓨터에서 안정적으로 지원되는 문자 코드는 ASCII 코드 뿐이며, 영어 이외의 언어는 시스템에 의존하는 문제였다. 그러나 글로벌화가 진행되는 가운데 국제적인 소프트웨어 개발이 빈번하게 이루어지게 되면서, 지역마다 서로 다른 정보를 효율적으로 관리하는 방법이 요구되고 있다.

그래서 국제적인 응용 프로그램을 개발하는 경우는 C 언어로 와이드 문자를 처리하는 방법을 학습해야 한다. char 형의 문자는 1바이트로 구성되기 때문에, 고유하게 표현할 수 있는 255개의 문자이다. 알파벳과 기호 정도라면 분명히 1바이트로 표현할 수 있으므로 효과적이지만, 한글과 같은 많은 문자가 있는 언어를 다루는 경우는 255개로는 분명히 부족하다.

한글과 같은 표준에서 규정되지 않은 시스템 고유의 확장 문자 세트를 이용하려면 와이드 문자를 사용한다. 와이드 문자는 1문자의 표현이 2바이트 이상의 문자이다. 와이드 문자 stddef.h 헤더 파일에 정의된 wchar_t 형으로 표시된다.

와이드 문자는 char 형의 문자와는 다르기 때문에, 와이드 문자형의 상수를 작성하려면 문자 상수의 앞에 L을 붙인다. wchar_t 형의 리터럴 문자열을 작성하는 경우도 마찬가지로, 인용 부호 앞에 L을 지정한다. 이것은 예를 들어 다음과 같이 변수에 저장할 수 있다.

wchar_t wc = L'개';
wchar_t wstr[] = L"당신의 무릎에 개";

다만, 와이드 문자형은 우리가 지금까지 사용한 문자나 문자열과는 이질적인 것이라는 것을 인식해야 한다. 문자 수를 계산하면 경우, 와이드 문자는 1문자 1바이트가 아니다. 따라서 strlen() 함수를 사용할 수 없다. 와이드 문자를 사용하는 경우는 지금까지 사용해 온 문자열 관련 함수를 사용할 수 없다.

코드1

#include <stdio.h>
#include <stddef.h>

int main() {
  wchar_t wc = L'개';
  printf("wchar_t size = %d\n" , sizeof wc); 
  return 0;
}

코드1은 wchar_t 형의 변수를 선언하고, 이를 와이드 문자 상수로 초기화한다. sizeof 연산자를 사용하여 변수의 크기를 확인하고 있는데, 각 프로그램의 결과를 보면 와이드 문자열이 1바이트가 아닌 것을 확인할 수 있을 것이다.

하지만, 와이드 문자 및 와이드 문자 배열은 지금까지처럼 문자열 관련 함수 편집하거나 입출력을 할 수 없다. 와이드 문자를 다루는 프로그램은 와이드 문자 전용 함수를 사용해야 한다. 와이드 문자 전용의 함수는 기존의 문자열 관련 함수에 w를 붙이는 명명 규칙을 가지고 있다. 예를 들어 printf() 함수의 와이드 문자 버전은 wprintf() 함수에 대응하고, 동일하게 scanf() 함수는 wscanf() 함수에 대응한다.

wprintf() 함수

int wprintf( const wchar_t *format [, argument]... );

wscanf() 함수

int wscanf( const wchar_t *format [,argument]... );

wprintf() 함수도 wscanf() 함수도, 문자나 문자열 wchar_t 형을 사용할 수 있다는 점을 제외하고 printf() 함수와 scanf() 함수와는 동일하다. 그러나 기대하듯이 wprintf() 함수를 사용하여 와이드 문자열을 표준 출력에 표시하려고 해도, 놀랍게도 wprintf() 함수는 아무것도 하지 않는 것이다. 와이드 문자를 사용하는 경우는 로케일 설정을 하여야 한다.

로케일(locale)은 특정 지리적 지역에서 지방 특유의 규칙과 언어를 반영하는 규칙을 나타낸다.한글의 와이드 문자를 사용하려면, 일본어가 지원되는 시스템에서 로케일을 “korean"으로 설정해야 한다. 로케일 설정은 locale.h 헤더 파일에서 선언된 setlocale() 함수를 사용한다.

setlocale() 함수

char *setlocale( int category, const char *locale );

category에는 프로그램의 로케일 정보의 어떤 부분을 변경할지 여부를 정수로 지정한다. locale에는 로케일을 지정한다. 함수는 유효한 로케일 및 카테고리를 지정하면, 로케일에 대한 문자열에 대한 포인터를 반환한다. 잘못된 경우 NULL 포인터를 반환하고 설정을 변경하지 않는다.

category는 다음 중 하나를 지정한다.

표1 - 로케일 카테고리를 나타내는 상수

상수 의미
LC_ALL 모든 카테고리
LC_COLLATE strcoll(), _ stricoll(), wcscoll() _ wcsicoll(), strxfrm()의 각 함수
LC_CTYPE 문자 처리 함수
LC_MONETARY localeconv() 함수가 반환하는 통화 형식 정보
LC_NUMERIC 형식이 지정된 출력 루틴 및 데이터 변환 루틴의 소수점 문자, localeconv() 함수가 반환하는 비화폐 형식 정보의 소수점 문자
LC_TIME strftime() 함수와 wcsftime () 함수

로케일 설정이 실제로 값 얻기 영향은 실행 환경에 따라 달라진다. 로케일 문자는 예를 들어 “korean"등 구현에 정의되어 있는 값을 지정하여 언어를 한글로 설정할 수 있다. locale에 NULL 문자열을 지정하면 그 로케일은 처리계 정의의 네이티브 환경이다. 보다 확실한 프로그램을 만들려면, setlocale() 함수의 반환 값을 확인하고, 로케일 설정이 실패했을 경우의 처리도 작성해야 한다.

코드2

#include <stdio.h>
#include <stddef.h>
#include <locale.h>

int main() {
  wchar_t wcat = L'개';
  wchar_t *wstr = L"당신의 무릎에";

 setlocale(LC_ALL , "");
 wprintf(L"%s%c\n" , wstr , wcat);

  return 0;
}

코드2는 setlocale() 함수를 사용하여 모든 카테고리의 로케일을 한글로 설정한다. 그 후 wprintf() 함수를 사용하여 와이드 문자 상수와 와이드 문자 배열을 표준 출력에 표시하고 있다. 이렇게 함으로써, 한글 문자를 처리할 수 있다. 물론, 실행하는 시스템이 한글를 지원하지 않으면 setlocale() 함수는 실패한다.

1.9.12 - C 언어 | 고급 기능 | 어셈블리 언어 - __asm

C 언어 코드에 어셈블리 언어를 통합하는 방법을 제공한다. 어셈블리 언어는 컴파일러와 시스템에 의존하기 때문에 이 자리에서는 Microoft Visual C ++ 및 Intel x86 호환 프로세서를 전제로 코드 내에 어셈블리를 통합하는 방법을 설명한다.

C 언어에 어셈블리 언어를 포함하기

C 언어는 인간이 이해하기 쉬운 고급 언어로 분류되지만, 고급 언어 중에서는 매우 기계어에 가까운 존재이기도 한다. 따라서 저급 언어라는 기계어에 가까운 프로그래밍 언어인 어셈블리 언어와의 궁합도 좋다고 생각된다. 어셈블리 언어는 고급 언어와 달리, 기계어로 직접 번역할 수 있는 언어로 하나의 문장이 하나의 기계어로 대응하고 있다.

현대에서는 대부분의 프로그래머가 어셈블리 언어을 사용하지 않게 되었고, 어셈블리 언어를 몰라도 프로그래머가 될 수 있다. 그럼 정말로 어셈블리 언어는 필요없는 언어인 것인까? 그러나 실전의 개발을 하면 의외로 어셈블리 언어의 지식이 필요한 경우가 있다. 예를 들어, 고급 게임 프로그래머가 되기 위해 어셈블리 언어는 빼놓을 수 없다. CPU의 특정 명령 세트(※ 1)를 어셈블리 언어에서 명시적으로 사용함으로써, 연산 속도를 가속화시킬 수 있는 기술이 존재하기 때문이다.

또는 운영 체제의 연구를 할 경우에도 어셈블리 언어의 지식이 필요하다. 현대의 대부분의 운영 체제는 C언어로 작성되어 있지만, 그 기본 부분에는 역시 어셈블리 언어를 사용하지 않으면 안된다.

따라서 C 언어와 어셈블리 언어의 관계는 매우 밀접한 것이 있다. 많은 프로그램은 C 언어를 사용하여 실현할 있지만, 프로그램의 일부로 하드웨어의 기능을 직접 조작하는 것 같은 필요가 있는 경우, 어셈블리 언어를 사용하지 않으면 안된다. 이런 사태에 대비해 일부 컴파일러는 C 언어 소스에서 어셈블리 언어를 작성하는 것이 가능하게 되어 있다. 이를 인라인 어셈블러라고 한다.

인라인 어셈블러는 순수한 어셈블리 언어가 아니라, 일반적으로 C 언어와 어느 정도 협조할 수 있다. 예를 들어, 어셈블리에서 C 언어의 변수와 함수를 호출하는 일이 생길지도 모른다. C 언어의 인라인 어셈블러는 표준이 아닌, 개발 환경에 의존하는 것이므로, 컴파일러는 전혀 어셈블러를 지원하지 않을 수 있다. 물론, 어셈블리 언어는 CPU의 명령 세트에 의존하기 때문에 사용되는 어셈블리 언어 컴파일 환경에 따라 달라진다. C 언어와는 달리, 어셈블리 언어에는 표준이라는 개념이 없다. 따라서 개발 환경이 인라인 어셈블러를 지원하고 있는지, 어떤 명령을 사용할 것인가하는 문제는 이 문서의 범위를 초과한다. 사용하는 개발 환경의 도움말 등을 참조하자.

일반적으로 인라인 어셈블러는 asm 키워드가 사용된다. 이 책을 쓰는 시점에서 가장 유명한 컴파일러 중 하나인 Microsoft Visual C ++에서 __asm 키워드를 사용하여 인라인 어셈블러를 작성할 수 있다.

asm문(Visual C++)

__asm 어셈블러 명령
__asm { 어셈블러 명령 목록 }

__asm 키워드 뒤에는 어셈블리 언어의 명령을 작성할 수 있다. 여러 줄의 명령을 작성하려면 {}를 사용하여 __asm 블록을 작성한다. 한 줄 어셈블러이면 __asm 키워드 직후에 문장을 작성한다.

코드1

#include <stdio.h>

char strInputMessage[] = "2개의 정수를 입력하십시오.>";
char strScanf[] = "%d %d";
char strResult[] = "%d + %d = %d\n";

int main() {
  int x, y;

 __asm {
   push offset strInputMessage
   call dword ptr printf
   add esp, 4
    
    lea eax, [y]
    push eax
    lea ecx, [x]
    push ecx
    push offset strScanf
    call dword ptr scanf
    add esp, 0Ch

    mov eax, x
    mov ebx, y
    add eax, ebx

    push eax
    push y
    push x
    push offset strResult
   call dword ptr printf
   add esp,10h
 }
 return 0;
}

코드1는 __asm 키워드를 사용하여 인라인 어셈블러로 작성된 C 언어 프로그램의 예이다. 이 프로그램은 Microsoft Visual C ++ 6.0에서 컴파일되는 것을 전제로 하고 있다. Microsoft Visual C ++ 인라인 어셈블러는 Intel 486 프로세서의 명령어 세트를 지원하고 있다.

어셈블리 언어의 해설은 특정 컴퓨터에 의존하는데, 이 문서의 범위가 아니므로 생략하지만, 코드1의 인라인 어셈블러는 C 언어의 printf()와 scanf() 함수를 호출한다. 어셈블러에서 C 언어의 변수명 및 함수명이 사용되고 있는 것에 주목한다. 프로그램은 2개의 정수를 x 변수와 y 변수에 입력하고 이들을 더한 결과를 표시하고 종료한다.

이와 같이, C 컴파일러에서 인라인 어셈블러를 지원하는 것이 있기 때문에 연산 속도가 요구되는 프로그램 등에서는 어셈블리 언어를 사용할 수 있다.

2 - Python 입문

Python 입문

Python는 PHP 및 Ruby 등과 같이 누구나 즉시 배울 수 있닌 스크립트 언어이다. A.I. 개발 등으로 최근 지명도가 높아지고있는 Python. 그 기초 부분을 여기에서 설명한다.

2.1 - Python 입문 | Python 개발 환경

우선, GAE를 이용하기 위해 배우고 싶은 Python의 기본적인 지식을 대충 정리해 했다. 이것으로 “Python이란 무엇인가"라는 개념을 잡아 보도록 하자.

2.1.1 - Python 입문 | Python 개발 환경 | Python을 사용하기

Python 언어

“Python (파이썬라고 읽는다)“은 스크립트 언어이다. C언어이나 Java와 같은 컴파일 언어가 아니라 PHP이나 Ruby와 같은 스크립트언어이다. PHP도 Ruby도 실제로 사용한 적이 없는가? 그렇다면…

스크립트 언어라는 것은 “인터프리터"이기도 한다. 인터프리터라는 것은 소스 코드를 작성한 텍스트를 한 줄씩 로드하여 컴퓨터가 수행할 수 있는 명령으로 변환하면서 동작하는 프로그램 언어이다

그렇다는 것은 Python이 “텍스트 파일에서 소스 코드를 작성하면 그것을 바로 실행 시킬 수 있다"라는 것을 의미한다. 컴파일이나 프로그램 빌드라던가, 그런 까다로운 것은 일절 없다. 단지 편집기에서 쓰는 것만으로 움직일 수 있다는 것이다.

그러나 프로그램을 움직이려면 인터프리터가 필요하다. C와 같은 언어는 프로그램이 실행 가능한 명령의 묶음으로 소스 코드를 변환시켜 EXE 파일을 만든다. 이 EXE 파일은 그대로 두 번 클릭하여 이동할 수 있다. Python은 그런 것은 없다. Python 프로그램을 움직이려면, Python 소스 코드를 번역하고 실행하기위한 ‘인터프리터’가 필요하다.

그 외에 객체 지향이라던가, 동적 타이핑(dynamic typing) 언어라는 특징이 여러 가지 있지만, 지금은 그런건 전혀 기억할 필요가 없다. “인터프리터 언어이기 때문에 인터프리터를 설치하여 텍스트를 작성하고 실행하면 움직인다"라는 것만 알면 충분하다.

우선은 이 “Python 인터프리터"준비를 하도록 하자. 인터프리터는 Python 사이트에서 다운로드 할 수 있다. 다음 주소를 방문해 보자.

http://www.python.org/download/releases/

주의 할 점은 Python 버전이다. 현재 최신 버전은 3.6이다.

Python은 ver. 2부터 ver. 3으로 업그레이드될 때, 상당한 변화가 이루어 졌다. 따라서 ver. 2로 작성된 프로그램의 상당수는 ver. 3에서 움직이지 않게 되어 버렸다. 그래서 ver. 2를 이용하고 있던 사람들을 위해 지금도 ver. 2 업데이트가 이루어지고 있는 것이다.

앞으로 새롭게 배우는 사람은 새로운 ver. 3을 선택하면 좋을 것이다. 여기에서는 ver. 3.6 기반으로 설명을 하고 있다.

Python 설치

그럼 다운로드한 설치 프로그램을 시작하여 설치를 하도록 하자. Windows 버전은 시작하면 “Install Now"라는 표시가 나타난다. 이것을 클릭하면 된다. 그러고 기다리면 설치가 완료된다. 정말 쉽다!

또한 이 때, 윈도우 아래에 보이는 2개의 체크 박스는 양쪽 모두 ON으로 해두자. 이것을 잊어 버리면 나중에 명령 프롬프트에서 Python 명령을 실행할 수 없게 되기도 한다.

Mac OS X의 경우

Mac OS X의 경우 사정이 좀 다르다. Mac OS X에서는 기본적으로 Python이 설치되어 있다. 이를 이용한다면, 설치 등은 필요없다.   그러나! 기본적으로 설치되어 있는 것은 현재 ver. 2.5 버전이다. ver. 3 버전이 아니다. 향후에 OS 버전 업으로 변화 할지도 모르지만, 지금으로써는 ver. 3을 사용하고 싶다면 별도로 설치할 수 밖에 없다.

Mac OS X 버전의 설치 프로그램은 Mac의 표준 설치 프로그램 자체이므로 “소개”, “읽어보기”, “사용권 계약”, “대상 디스크 선택”, “설치 유형”, “설치”, “요약” 순서대로 설정해 가면 된다. 즉, 기본적으로 모두 기본값 그대로 진행해 간다면 문제가 없을 것이다. “대상 디스크 선택"는 설치 위치 변경 없다면 그대로 넘어갈 것이고, 그대로 설치를 해 주시기 바란다.

파이썬 맥 설치

2.1.2 - Python 입문 | Python 개발 환경 | IDLE 기동하기

IDLE 기동

그러면 실제로 Python을 사용해 보자. Python 프로그램 본체는 그냥 인터프리터로 GUI도 아무것도없는 명령 프로그램이다. 이 밖에 “IDLE"라는 Python을 이용하기 위한 간단한 도구가 붙어 있다. 이것을 사용하여 Python을 시험해 보자.

Windows의 경우, 시작 버튼에서 “Python 3.6"는 바로 가기를 찾고, 그 안에 있는 “IDLE” 메뉴를 선택해서 기동하면 된다. Ma OS X의 경우는 응용 프로그램 폴더에 설치되는 “Python 3.6"폴더에 IDLE가 들어 있기 때문에 이것을 시작하면 된다.

IDLE은 매우 간단한 텍스트 편집기처럼 보이는 도구디다. 실제로 이것은 텍스트 편집기로 사용할 수 있다. 하지만, 이 IDLE의 특징은 “대화형 쉘(interactive shell)“이라는 기능을 가지고 있다.

대화형 쉘이라는 것은 “대화형으로 Python을 수행 할 수 있는 기능"이다. 이 IDLE에서는 Python 문장을 입력하고 Enter 또는 Return 키를 누르면 해당 문장만 그 자리에서 실행 결과를 표시 할 수 있다. 그렇게 하나씩 문을 실행하면서 Python의 동작을 확인하는 것이 가능하다.

그럼 해보자. 지금 열려있는 IDLE 창에서 다음과 같이 입력하고, Enter/Return 키를 누르면 된다.

print("Hello Python!")

이렇게 하면 다음 줄에 “Hello Python!“라는 텍스트가 표시된다.

IDLE

이런 상태로, Python 문장을 실행하고 결과를 표시하는 것을 반복하면서 작업 해 나갈 수 있다. 이 대화형 쉘은 Python을 배우기 시작할 시에 특히 유용하다.

2.1.3 - Python 입문 | Python 개발 환경 | 스크립트 실행하기

Python은 일반적으로 스크립트 파일(스크립트 = Python 소스 코드를 쓴 텍스트 파일)를 작성하여 이를 Python 명령으로 실행한다. 이 방법에 대해 설명하겠다.

우선, 스크립트 파일을 작성한다. 이것은 단순한 텍스트 파일이므로 텍스트 편집기라면 어떤 것이라도 작성할 수 있다. 만약 적당한 편집기를 가지고 있지 않다면, IDLE을 사용하자. 이것은 사실 Python 전용 편집기로도 사용할 수 있다.

IDLE 윈도우의 메뉴에서 [File] - [New File] 를 선택하면 새 창이 열린다. 이것은 IDLE 대화형 쉘 윈도우와는 다른 단순한 텍스트 편집기 창이다. 여기에 그대로 Python 스크립트를 작성하고 편집할 수 있다. 그럼 다음의 스크립트를 작성하자.

for n in range(10): 
    print("Hello Python!")

작성한 후 [File] 메뉴의 [save]를 선택하여 파일을 저장한다. 파일 이름은 “myscript.py"로 하였다. 저장 위치는 본인이 알기 쉽게 적당한 곳에 해두면 된다.

저장한 후에 스크립트를 편집하는 윈도우의 [Run] 메뉴에서 [Run Module] 을 선택한다. 편집기 창에서 열려있는 myscript.py을 그 자리에서 실행하고 대화형 쉘 창에 결과를 출력한다.

실행하면 “Hello Python!“라는 텍스트가 10 줄 출력된다. 이 스크립트의 실행 결과이다.     python run

python run

명령 실행

Python 프로그램은 일반적으로 명령 프롬프트 또는 터미널에서 명령을 사용하여 스크립트를 실행한다. 그럼, 이것도 해보도록 하자. 명령 프롬프트(Windows) 또는 터미널(Mac OS X)를 시작하자.

cd {py파일이 저장된 위치}

이렇게 실행할 스크립트 파일이 저장한 디렉토리로 이동한다. 그리고 다음과 같이 명령을 실행한다.

Windows의 경우 (다음의 어느 쪽도 가능)

py myscript.py
python myscript.py

Windows의 경우 “python"명령어로 실행한다. 이것은 생략해서 “py"만으로도 실행할 수 있다.

Mac OS X의 경우

python3 myscript.py

Mac OS X의 경우 “python"명령어를 실행하면 OS에 처음부터 설치되어있는 Python 2.5을 시작한다. 새로 설치했다면 “python3"라고 실행해야 한다.

Max OS X에서 실행이 안될 경우

처음에 Mac OS X에 설치해서 그대로 실행하려고 하면 python3 명령을 사용할 수 없는 경우가 있다. 이는 쉘에 Python의 경로가 추가되지 않기 때문이다. 이것은 미리 준비되어있는 명령 프로그램으로 실행할 수 있다.

“응용 프로그램"폴더에 설치되어 있는 Python의 폴더(“Python 3.6"과 같은 이름으로되어 있다)에 “Update Shell Profile.command ‘라는 파일이 있을 것이다. 이를 더블 클릭하여 실행 하자. 이것으로 Python3 명령을 사용할 수있게 될 것이다.

2.1.4 - Python 입문 | Python 개발 환경 | 스크립트를 작성할 때의 주의점

우선 이것에서 스크립트를 작성하여 움직이는 프로그래밍의 기본 중의 기본은 알았다. 그러면 실제로 스크립트를 공부하고 가기 전에 “스크립트를 작성 할 때, 주의점에 대해서 정리하겠다.

1. 기본은 “영숫자"로 작성한다.

이것은 Python에 한정된 이야기는 아니지만, 프로그래밍 소스 코드는 모든 영문, 숫자가 기본이다. 한글이 포함되어 있으면 동작하지 않는다. 기본적으로 ‘한글은 텍스트를 값으로 사용하거나 코멘트를 작성할 때 뿐"이라고 생각하자. 나머지는 모두 영문과 숫자가 기본이다.

2. 대소문자는 다른 문자!

이것은 특히 Windows 사용자에게는 중요하다. Windows를 사용하고 있으면 왠지 “대문자와 소문자는 동일 문자"라는 생각이 배어 버린다. 즉, “A도 a도 어느 쪽도 같은 문자"라고 생각해 버린다(그렇지 않다면 다행이다).

하지만 Python은 다르다. “A"와 “a"는 다른 문자이다. 예를 들어, 이전에 사용한 “print~“라는 것을 “Print~“라고 쓰면 실행이 되지 않는다. 또한 값을 저장 변수의 이름도 대소 문자를 정확하게 쓰지 않으면 안된다. “A"라는 변수를 사용하는데, “a"라고 쓰면 인식하지 못한다.

3. 선행 공백은 의미가 있다!

이것도 매우 중요하다. 프로그래밍 언어에는 소스 코드를 보기 쉽게하기 위하여 “들여 쓰기(indent)“라는 것을 잘 사용해야 한다. 즉, 문장의 시작 부분에 탭이나 공백을 넣어, 시작 위치를 오른쪽으로 이동하고 보기 쉽게하기 위함이다.

Python에서는 들여 쓰기를 마음대로 해는 안된다. 다음에서 설명하겠지만, Python에는 문장의 들여 쓰기가 중요한 의미를 가지고 있다. Python에서는 들여 쓰기에 따라 문법 구문 등을 인식하게 되어있다.

그래서 꼭 규칙에 따라 들여 쓰기를 해야 한다. “이런 것이 보기 편안한 때문"이라고 적당히 공간 사이를 떼면, 바로 문법 오류가 되어 버린다.

 

우선, 이 3가지 점만 제대로 이해해 두자. 그럼 다음에서 구체적인 프로그래밍 이야기에 들어가기로 하자.

2.2 - Python 입문 | 우선 값과 계산의 기본

프로그래밍의 기본이라고 하면, 우선 “값"과 “계산"이다. Python으로 사용되는 기본 값과 계산 방법 등을 설명한다.

2.2.1 - Python 입문 | 우선 값과 계산의 기본 | 값에는 유형이 있다

프로그래밍에 익숙하지 않은 사람이 처음 도전했을 때, 처음에 걸리는 것은 “값은 종류가 있다"는 것이다.

많은 초보자는 Python과 같은 스크립트 언어부터 시작을 공부한다. 이러한 언어에서는 변수(값을 보관 해 두는 곳) 등을 사용하는 경우도 그다지 “값 유형"등을 의식하지 않도록 되어 있다. 따라서 “어떤 값도 변수에 넣어 사용하면 그것으로 움직인다"고 착각해 버린다.

나중에 설명 하겠지만, Python에도 값에는 “유형"이 있다. 숫자, 텍스트, 문자 ……라는 식으로 다양한 종류가 있으며, 그 종류마다 값의 사용법은 달라진다. 하지만, 실제로 프로그램을 쓸 때는 대부분 값의 “유형"을 의식하지 않고 쓸 수 있게 되어 있다.

간단한 예를 살펴 보자. IDLE을 시작하고 아래에 올린 3문장의 스크립트를 한 줄씩 실행하길 바란다.

print(123 + 456)
print('123' + '456')
print(123 + '456')

보면 대체로 비슷한 두 값을 덧셈하고 있다. 어떤 것도 같은 생각이 든다.

그런데 실제로 실행해 보면, 이 3개는 전혀 다르게 동작한다. 첫 번째는 “579"가되고, 두 번째는 “123456"가 되고, 세 번째는 에러가 발생한다.

python variable

이것은 첫 번째 숫자로 계산하고, 두 번째는 텍스트로 계산된다. 세 번째는 두 가지의 종류의 다른 값을 억지로 계산하려는 시도했다가 실패를 했다.

즉, Python이라는 언어는 “값 유형"라는 것을 잘 이해해 두지 않으면 사용할 수 없다. 우선, 이 점을 잘 이해해 두자.

2.2.2 - Python 입문 | 우선 값과 계산의 기본 | 주요 값의 유형

그럼 Python에는 어떤 값의 유형이 있을까? 기본적인 사항을 설명한다.

숫자

프로그래밍에서 사용 값이라고 하면, 우선 “숫자"이다. 숫자에 대해서는 Python에서는 많은 유형이 있는데 “정수”, “부동 소수점” “복소수"등이 있다.

  • 정수 : 보통의 정수이다. 단지 숫자를 쓰는 것만으로 된다.
  • 부동 소수점 : 소수점 이하의 값이다. 또는 매우 자리수가 많은 숫자에 사용하기도 한다. 이것은 보통 소수의 “.“을 붙여 쓴다.
  • 복소수 : 허수이다. 이것은 끝에 “J"를 붙인다.

이 중에 우선 ‘정수’와 ‘부동 소수점"만 기억해 두도록 하자. 복소수는 필요하여 사용하게 될때까지 잊고 있어도 된다.

텍스트

텍스트는 값의 전후를 따옴표로 묶어 설명한다. 이것은 “작은 따옴표”, “큰 따옴표”, “트리플 쿼트"라고 한 것이 사용할 수 있다.

'Hello'    "Welcome"   '''Bye'''

이런 느낌이다. 이 가운데 작은 따옴표(’)와 큰 따옴표(")은 동일하다. 일반적으로 텍스트를 쓸 때, 이 중 하나에 쓴다.

마지막 트리플 쿼트(’’’)은 여러 줄의 텍스트를 쓸 때 사용한다. 작은 따옴표와 큰 따옴표는 텍스트 값의 줄로 할 수 없다. 트리플 쿼트는 도중에 행을 변경해도 된다.

부울

이것은 프로그래밍 특유의 값이다. 이것은 “양자 택일의 값"이다. 참 또는 거짓, yes 또는 no, 올바른 또는 그른지 이런 것을 나타내는데 사용한다. 이것은 “True”, “False"라는 Python에 포함되어 있는 키워드를 사용하여 작성한다. 다른 값은 사용할 수 없다.

실제로 이러한 값을 사용한 예제는 아래와 같다. 아래의 코드를 IDLE에서 한 줄씩 실행해 보자.

print(12345) 
print('Hello') 
print('''welcome, 
and bye.''') 
print(True)

조금 이해하기 어려운 것은 트리플 쿼트 텍스트일 것이다. 그 외에 그렇게 어려운 것은 없을 거다.

python datatype

2.2.3 - Python 입문 | 우선 값과 계산의 기본 | 변수와 계산

값은 그대로 자체를 그대로 사용하는 일은 그다지 많지 않다. 보통은 “변수"에 넣어 사용한다.

변수란 값을 보관할 준비가 된 메모리 영역을 나타내는 것이다. 어째서 이런 것이 있는가? 하고 의문을 가질 수 있는데, 여기서 이를 설명하기에는 너무 내용이 방대하다. 그래서 우선은 알기 쉽게 “여러가지 값을 넣어 두는 용기"라고 생각두면 충분하다. 값은 변수에 넣어 놓고, 계산하고, 그 결과를 다시 변수에 넣고 처리해 나간다.

이 변수는 등호(=)를 사용하여 값을 넣는다. 예를 들면 이런 식이다.

변수명 = 값

이것은, 우변 값이 좌변의 변수에 포함되어 있다(대입라고 한다). 변수 이름을 쓰고, 이런 식으로 값을 대입하면 바로 변수를 만들어 사용할 수 있게 된다. “미리 이런 변수를 만들어 두는 거"라고 해서 귀찮은 일은 아니다.

예를 들어, “a = 10"라고 하면, 변수 a가 바로 만들어 진다. 이 변수는 값과 동일하게 취급 할 수 있다.

a = 10
print(a) 
b = 'Hello'
print(b)

연산에 대해

변수는 단지 값을 보관할 뿐만 아니라, 다양한 계산을 한 결과를 저장하기 위해 많이 사용된다.

숫자 연산

숫자 연산 기호는 이른바 사칙 연산 기호를 그대로 사용할 수 있다. 즉, 덧셈(+), 뺄셈(-), 곱셈(*), 나눗셈(/), 나머지(%)를 말한다. 키보드에 기호가 보이기 때문에 알 것이다. 그런데, 여기서 %는 무엇인가 라고 생각한 사람이 있을 것이다. 이는 “나눗셈을 하고 남은 나머지"를 계산하는 것이다.

a = 10
b = 20
c = a + b 
print(c)

이 밖에 ‘지수’의 기호 있다. [**]이다. 예를 들어, “10의 제곱"이라면, “10**2” 이렇게 쓴다.

텍스트 연산

또한 “텍스트의 연산 ‘라는 것도 있다. 연산은 ‘덧셈’과 ‘곱셈’을 사용한다.

  • [ + ] 기호 : 왼쪽과 오른쪽 텍스트를 하나로 연결한다.
  • [ * ] 기호 : 왼쪽의 텍스트를 오른쪽 회수 만큼 반복한다.

덧셈은 간단한다. 예를 들어, [ ‘A’ + ‘B’ ]라고 하면 “AB"라는 텍스트가 될 것이다. 곱셈은 [ ‘A’ * 3 ]라고하면 “AAA"이다.

a = 'A'
b = 'B'
c = a + b 
print(c)

print('A' * 3)

2.2.4 - Python 입문 | 우선 값과 계산의 기본 | 값 유형의 변환과 텍스트

값에는 유형이 있다. 다른 유형의 값끼리는 계산할 수 없다. 그렇게 되면, 예를 들어 “텍스트와 숫자를 사용하여 계산한다"라고하는 것은 불가능하다는 걸 의미한다.

그러나 물론 그런 일은 없다. 제대로 된 방법으로 Python에는 값을 다른 유형으로 변환하는 기능이 포함되어있다. 우선 다음의 것만 제대로 배워보도록 하자.

  • int(값) - ()의 값을 정수로 변환한다.
  • float(값) - ()의 값을 실수로 변환한다.
  • str(값) - ()의 값을 텍스트로 변환한다.
  • bool(값) - ()의 값을 부울 값으로 변환한다.

이 처럼 어떤 유형의 값을 다른 유형으로 변환하는 것을 “형 변환"이라고 하고, 영어로는 “캐스팅(casting)“이라고 한다.

그럼 아래 형 변환 간단한 샘플들을 보도록 하겠다. 이런 방법로 텍스트와 정수를 변환하여 사용한다면 될 것이다.

텍스트에 값을 정리

값 형 변환는 “계산 값을 맞출려고"하는 경우도 있지만, 그 보다 자주 사용하는 것이 “print에 값을 출력하기 위해서"이다. print는 어떤 값도 출력할 수 있지만, 텍스트를 사용하여 값을 처리하려는 순간 오류가 발생한다.

a = 123
b = 456
c = a + b
print(c)

이것은 매우 간단한 샘플이다. 그런데 이것을 조금 처리하려는 순간 갑자기 문제가 발생한다.

a = 123
b = 456
c = a + b
print('answer :'+ c)

갑자기 왜 오류가 발생하는가? 라고하면, print()에 쓴 ‘answer :’ + c가 원인이다. 텍스트와 숫자를 연결해 보려고 했기 때문에 “변수 c는 텍스트 아니야"라고 오류가 발생하는 것이다. 텍스트를 + 기호로 연결하기 위해서는 연결 값이 텍스트가 아니면 안된다.

a = 123
b = 456
c = a + b
print('answer :' + str(c))

이제 문제없이 동작하게 되었다. print()를 보면 ‘answer :’+ str(c)와 같이 되어 있다. 변수 c를 str로 텍스트로 변환한 것으로 정상적으로 동작하게 되는 것이다.

이와 같이, “텍스트으로 값을 연결하려고 하여, 오류가 발생하게 되었다"라는 것은 초보자로써 언제나 하는 실수이기에, print에 오류가 발생하면 먼저 “값 형 변환, 값 형 변환"라고 머릿속에서 반복하자.

2.3 - Python 입문 | 구문(statement)

Python의 가장 큰 특징은 그 독특한 “구문(statement)“스타일에 있다. “들여쓰기(intent)“를 이용한 구문 작성 및 기본적인 제어 구문에 대해 설명한다.

2.3.1 - Python 입문 | 구문(statement) | 구문과 들여 쓰기의 관계

프로그래밍 언어로는 “값"과 “계산"이 기본라고 했었다. 그럼, 다음에 중요한 것은 무엇일까요? 여러가지 생각나는 것이 있지만, 아마 프로그램의 “제어"일 거다.

단지, 명령을 순차적으로 실행하는 것만으로는 극히 제한된 사용법 밖에 할 수 없다. 프로그램의 상황에 따라 “여기는 이것을 실행”, “이것은 OO번 반복"과 같이 프로그램의 흐름을 제어함으로써 보다 복잡한 프로그램이 만들 수 있게 되기 때문이다.

이러한 프로그램의 움직임에 대해 이것 저것 지시하기 위해 준비되어 있는 것이 ‘구문’이라는 것이다. 그 중에서도 그 흐름을 제어하기 위해 마련된 것을 “제어 구문(control statement)“라고 한다.

Python의 구문 표기법은 매우 독특하다. 그것은 “들여 쓰기"를 사용한 방법이다. “들여 쓰기"란 텍스트의 시작 위치를 탭이나 공백으로 오른쪽으로 보내는 것이다.

Python에서는 다양한 구문이 들여 쓰기를 사용하여 작성한다. 예를 들어, 구문에서 “이런 경우 다음 작업을 수행한다"라는 것을 설명한다고 하자. 그러면, 그 구문 중에 준비하는 처리는 그거보다 오른쪽으로 들여 쓰여져 있다. 그리고 그 들여 쓰기 위치에서 쓰여져 있는 것이 “그 구문 안에 처리"라고 판단되는 것이다.

그 구문을 끝내고 원래대로 돌아가려면 이전 위치로 들여 쓰기를 되돌린다. 즉, Python은 “그 문장이 어떤 위치에서 쓰기 시작하고 있는가"에 따라 어떤 구문의 처리인가를 인식하는 것이다.

간단히 이해를 돕기 위해 아래의 구문 작성 내용을 보도록 하겠다.

Python 구문 작성

보통 문장 ......
구문 그 1
     구문 1의 처리 ......
     구문 1의 처리 ......
     구문의 1에 또한 구문
         그 또한 중 처리 ......
         그 또한 중 처리 ......
     구문 1의 처리 ......
구문 2
     구문 2의 처리 ......
보통 문장
보통 문장
...... 중략 ......

이런 식으로 문장의 시작 위치를 조금씩 다르게 하여 구문이 작성된다. 이는 들여 쓰기 공백 수를 잘못하게 되면 문법적인 에러가 발생하기도 한다는 것을 의미한다.

Python 들여 쓰기는 일반적으로 탭 대신 공백이 사용된다. 표준으로 공백 8개 문자씩 넣는 방식이 많지만, 이것은 특별히 정해져 있는 것은 아니고, 4개 문자도 2개 문자로도 인식하고 동작한다.

단, 너무 공백 수가 적으면 문법의 구성이 알아 보기 힘들어 지거나, 들여 쓰기가 실수가 증가하기도 하고, 공백 수가 너무 많으면 점점 문장이 오른쪽으로 이동하여 문장의 끝이 보이지 않게 될 수도 있다. 그러기에 적당한 폭을 생각하면서 쓰도록 하자.

2.3.2 - Python 입문 | 구문(statement) | 조건 분기의 기본은 "if"구문

제어 구문을 크게 나누면 “조건 분기"와 “반복"으로 구성되어 있다. 우선 조건 분기부터 살펴 보겠다.

조건 분기는 문자 그대로 “조건에 따라 처리를 분기한다"는 것이다. 그 기본은 양자 택일의 분기하는 “if"구문이다. 이것은 다음과 같은 형태를 하고 있다.

** if의 기본형 (1)**

if 조건:
    옳았을 때의 처리

if의 기본형 (2)

if 조건:
    옳았을 때의 처리
else:
    잘못된 때의 처리

if 문은 여러가지 작성 방법이 있다. 기본은 조건을 확인하고 그것이 옳았을 때에 작업을 수행한다는 것이다. 이것은 if문 후에 검사 조건이 되는 것을 쓰고 콜론(:)을 쓴다. 그러고 그 이후의 들여 쓰기된 부분을 수행한다.

옳았을 때의 처리와는 별도로 잘못된 경우에도 어떤 처리를 하고 싶다면, 옳았을 때 수행할 처리가 끝나는 곳에, 들여 쓰기를 if 위치로 돌아가서 “else :“라고 쓴다. 그리고 또 오른쪽으로 들여 쓰기하여 수행 할 서치를 작성하면 된다.

또한 조건을 하나뿐만 아니라 차례로 확인하는 “elif:“와 같은 것도 있지만, 일단은 “if ~: “, “else : " 2개만 기억해두면 충분하다.

아래에 간단한 예제는 아래와 같다.

x = 1234
check = x % 2
if check == 0: 
    print(str(x) + "는、짝수입니다.") 
else: 
    print(str(x)  + "는, 홀수입니다.") 
print("....end.")

변수 x가 짝수인지 홀수인지를 검사 프로그램이다. x를 2로 나눈 나머지를 확인하고, 그것이 제로인지 여부에 표시할 텍스트를 변경하고 있다. 변수 x의 값을 다양하게 변경하여 동작을 확인해 보자.

2.3.3 - Python 입문 | 구문(statement) | 조건은 어떻게 쓰는거야?

이 if문을 사용하기 위해서는 ‘조건’이라는 것을 어떻게 작성해야 해야 할지가 중요하다. Python이라는 프로그래밍 언어를 이해하는데 있어서 조건은 꼭 알아야 한다. 조건에 대해 정리하면 대략 다음과 같다.

숫자를 비교하는 식

가장 많이 쓰는 것은 숫자를 비교하는 식이다. 이전 페이지 샘플도 숫자 비교 식을 사용했다. 두 값을 비교하여 “어느 쪽이 큰지"라든가 “같은 값인지에 대한 여부"등을 체크하는 식이다. 이것은 다음과 같은 기호가 준비되어 있다.

기호 설명
값1 == 값2 값1과 값2는 동일하다.
값1 != 값2 값1과 값2는 같지 않다.
값1 < 값2 값1보다 값2 더 크다.
값1 <= 값2 값1보다 값2 쪽이 크거나 같다.
값1 > 값2 값1보다 값2 쪽이 작다.
값1 >= 값2 값1보다 값2 쪽이 작거나 같다.

부울 값과 변수

“부울"이라는 것은 “옳고 그른지? “라는 양자 택일에 대한 값이었다. 이것은 True 또는 False의 값 중 하나였다. if문에는 그 후의 변수와 값이 True이면 다음의 작업을 수행한다. False라면 작업을 수행하지 않거나 또는 else: 이후의 처리를 실행한다.

결론

사실은 “숫자를 비교하는 식"과 “부울 값과 변수"는 어느 쪽도 같은 것이다. 첫번째에서 “두 값을 비교하는 식"에서는 두개의 식을 비교한 결과를 논리 값으로 반환하는 역할을 한다. 즉, 상세히 따져보면 “True인가? False인가?“에서 모든 if 조건문의 결정이 가능하다는 것이다.

2.3.4 - Python 입문 | 구문(statement) | 조건에서 반복 while 문

이어서, 또 하나의 제어 구문 “반복"에 대해 알아보겠다. 반복은 2개의 구문으로 되어 있다. 첫번째 조건을 사용하여 반복을 체크하는 “while"구문이라는 것이다.

while 구문의 기본형 (1)

while 조건:
    반복 처리 ......

while 구문의 기본형 (2)

while 조건:
    반복 처리 ......
else :
    반복 종료시 처리 ......

여기에서도 “조건"라는 것이 등장했다. 이것은 if에서 나온 조건과 동일하다. 즉, “True인가? False인가?“를 확인하는 수식 및 변수 혹은 값이다.

이 while 구문은 조건을 확인하고 그것이 True인 동안에는 그 구문의 처리를 반복 실행을 계속한다. 그리고 조건이 False가 되면 반복에서 빠져 나와 다음 처리를 한다. 만약, else:문이 있었다면, 반복을 빠져 나오면서 이를 실행한다.

아래에 간단한 샘플을 보도록 하자.

x = 100
count = 1
total = 0
while count <= x: 
    total = total + count 
    count = count + 1
else: 
    print(str(x) + "까지 합계는 " + str(total) +"이다.") 
print(".....end.")

1에서 변수 x까지의 합을 계산하고 표시하는 샘플이다. x의 값을 다양하게 변경하여 결과를 확인해 보도록 하자.

이 샘플에서는 “while count <= x:“와 같은 while의 조건을 설정한다. 즉, count 값이 x보다 작거나 같은 동안 반복을 계속하고, x보다 커지면 빠져 나오게 된다.

결국은 당연 하지만, 반복 실행하는 과정에서 변수 count 값이 조금씩 커지게 하지 않으면 안된다. 그렇지 않으면, while에서 영원히 벗어날 수 없게 되어 버린다. 이런 경우를 “무한 루프"라고 한다. while을 사용할 때에는 이런 무한 루프에 빠지지 않도록 조건의 내용과 그 결과가 반복에서 어떻게 변화해 가는가를 잘 생각해서 작성을 해야 한다.

2.3.5 - Python 입문 | 구문(statement) | 많은 값을 순서대로 반복하는 for구문

사실, 반복에는 또 다른 구문이 있다. 그것은 “for"구문이다. 이 for는 “많은 값을 순서대로 처리하는 경우"에 사용한다

for 구문의 기본형 (1)

for 변수 in 많은 값:
    반복 처리 ......

for 구문의 기본형 (2)

for 변수 in 많은 값:
    반복 처리 ......
else :
    반복 종료시 처리

프로그래밍 언어에는 “많은 값을 한곳에 모아 처리하는 기능"이 준비되어 있다. for는 그러한 것들을 위한 전용 반복 구문이다. 즉, 많은 값을 차례로 꺼내 처리를 실행하는 것은 결국에 준비되어 있는 모든 값에 대해 반복을 하는 것이다.

이 “많은 값"이라는 것이 무엇인가 대해 실제 사용 예제로 알아보도록 하자. 아래의 예제는 이전 페이지에서 while에 대한 샘플을 for 구문에 쓰고 다시 작성한 거다.

x = 100
total = 0
for n in range(1, x + 1): 
    total = total + n 
else: 
    print(str(x) + "까지의 합계는 " + str(total)) 
print("....end.")

여기에서는 range(…)는 본 적이 없는 것이 사용되고 있는데, 이것은 “1에서 변수 x까지의 모든 숫자를 하나의 묶음으로 만드는 함수"이다. 이것으로 “1,2, 3 … 100"는 모든 숫자를 하나의 묶음을 만들고 그 하나 하나를 꺼내 total에 더해 가고 있는 거다.

그런데, 이 for문을 사용하기 위한 포인트는 “많은 값을 모은 것"이라는 게 무엇인가? 라는 점인데, 이것은 일반적으로 “배열"라는 것이다. 다음에는 배열과 그와 관련된 것에 대해 설명하기로 하겠다.

2.4 - Python 입문 | 리스트, 튜플, 레인지, 세트, 사전

Python에는 여러 값을 처리하는 것(컨테이너)가 일부 포함되어 있다. 그 기본적인 사용법에 대해 설명한다.

2.4.1 - Python 입문 | 리스트, 튜플, 레인지, 세트, 사전 | 배열 = 리스트?

프로그래밍 언어는 여러 값을 한곳에 모아 처리하는 특별한 변수 같은 것이 대부분 준비되어 있다. 일반적으로 ‘배열’로 불리는 것으로, 이것은 번호를 붙여 값을 관리 할 수 있다. 예를 들어, “1번의 값을 XX로 변경” 또는 “3번 값을 꺼내기"라고 하여, 많은 값을 번호로 관리한다.

Python에서 제공되는 배열 기능은 “목록"라는 것이다. 이것은 다음과 같은 형태로 작성된다.

변수 = [값1, 값2, ...]

[] 안에는 각각의 값을 쉼표로 구분하여 작성한다. 이것으로 그 값을 순서에 번호를 매긴 목록이 만들어 진다. 이 번호는 일반적으로 “인덱스"라고 한다.

중요한 것은 “인덱스는 0부터 시작한다"라는 점이다. 즉, 첫 번째 값은 “0번"이 되고 두 번째 값이 “1번”, 세 번째 값이 “2번” …..와 같은 식으로 넘버링이 된다. 10개의 값이 있었다면, 인덱스 번호는 0~9이다 (1~10이 아니다!).

목록에 있는 개별 요소를 꺼낼 경우, “변수[번호]“라는 식으로 작성한다. 예를 들어,

arr[0] = "OK"
val = arr[1]

이런 식으로 사용할 수 있다. 이것으로 목록에 지정된 번호의 요소를 변경하거나 제거할 수 있다.

아래에는 간단한 사용 예제를 살펴보자.

arr = ['hello','welcome','good-bye']
for n in arr:
    print(n)
 
print("....end.")

여기에서는 이전에 소개한 “for ~ in …“구문을 사용하여 목록에있는 모든 요소를 반복해 나가고 있다. 이 구문은

for 변수 in 목록 :

이런 식으로 작성하여 목록에서 순서대로 값을 꺼내서 변수로 얻어서 반복을 실행한다. 목록과 for는 매우 잘 사용되므로, 두 세트 꼭 기억하도록 하자.

2.4.2 - Python 입문 | 리스트, 튜플, 레인지, 세트, 사전 | 리스트와 텍스트의 관계

목록은 다양한 곳에서 사용되지만, 실제로는 뜻밖의 곳에서 사용될 수 있다. 그것은 “텍스트"이다.

Python에서 텍스트의 값은 “문자 목록"으로 처리 할 수 있다. 예를 들어 “Hello"라는 텍스트는

str = ['H', 'e', 'l', 'l', 'o']

이런 식으로 5개의 문자 목록으로 생각할 수도 있는 거다. 예를 들면, str[0] 이것으로 ‘H’의 문자를 꺼낼 수 있다.

그러나 이렇게 하는 경우는 “문자를 꺼낼 때"뿐이다. 같은 방법으로 문자를 변경할 수 없다. 즉, 텍스트와 목록이 같은 것은 아니다. 어디까지나 “텍스트 내의 문자를 꺼내기 위해 목록을 이용할 수 있도록 하고 있다"고 생각하면 된다. 이렇게 하면 매우 알기 쉽게 텍스트 내의 문자를 검색 할 수 있다.

아래에 간단한 사용 예제를 보도록 하자. “Hello"텍스트부터 문자를 제거하고, 새 텍스트를 생성하는 샘플이다.

str = "Hello"
str2 = ""
for n in str:
    str2 = str2 + (n * 2) + '~'
print(str2)

실행해 보면 “HH~ee~ll~ll~oo~“와 텍스트가 표시된다. 텍스트 내의 문자를 다루기 위하여 목록을 사용할 수 있다. 이를 기억해두면 꽤 편리할 거다.

2.4.3 - Python 입문 | 리스트, 튜플, 레인지, 세트, 사전 | 튜플은 변경 불가능한 리스트?

이 목록에 비슷 “튜플"라는 것도 Python에 있다. 이것은 다음과 같이 작성한다.

변수 = (값1, 값2, ...)

값을 꺼낼 때는 변수와 마찬가지로 []에 인덱스를 지정한다. 예를 들어, str[0]와 같은 식으로 쓰면 된다.

그럼 기존 목록과 튜플은 무엇이 다른가? 그것은 “튜플 값을 변경할 수 없다"는 점이다. 즉, ‘변수’가 아니라 ‘상수’이다.

프로그래밍 세계에서는 변수처럼 값을 자유롭게 변경할 수 있는 것도 중요하지만, 반대로 “값을 변경할 수 없다"는 것도 중요하다. 어디선가 마음대로 값이 갱신 된다면 문제가 발생하게 된다 …… 그런 중요한 값을 배열처럼 많이 이용하려면 목록으로는 곤란한다.

튜플은 값이 변하지 않는 것이 보증된 목록이다. 그렇게 생각하면, 이 튜플을 사용하는 경우가 없지 않나?라고 생각 할 수 있을 것이다.

이런 “변경 불가능한 컨테이너"는 튜플 외에도 있다. 앞장에서 잠깐 나온 레인지(range) 등이 있다. 이러한 변경 불가 것을 “불변 객체(immutable)“라고 한다.

이에 대해 변경이 가능한 것은 “가변 객체(mutable)“라고 한다. 목록는 mutable 컨테이너의 대표라고 할 수 있다.

그럼 “튜플로 제공한 값을 나중에 목록으로 사용하고 싶다"라고 하는 경우는 어떻게 해야 하나? 이러한 경우에는 변환을 해주는 함수를 사용하면 된다.

튜플을 목록으로 변환

변수 = list(튜플)

목록을 튜플로 변환

변수 = tuple(목록)

그럼 아래 튜플과 리스트의 사용 예제를 보도록 하자.

tp = (0,1,2,3,4)
ls = list(tp)
for n in range(0,5):
    ls[n] = ls[n] * 2
for n in tp:
    print(ls[n])

튜플 tp를 준비해서 거기에서 목록 ls를 만들고, 목록의 값을 변경한다. 출력 결과를 보면서 ls값과 tp값이 어떻게 사용되고 있는지 생각해 보자.

2.4.4 - Python 입문 | 리스트, 튜플, 레인지, 세트, 사전 | 수열을 다루는 레인지

시퀀스 동료의 마지막은 “레인지(range)“이다. 이것은 이전에 등장 했었다. for 등에서 숫자의 범위를 지정하는데,

for n in range(10)

이런 식으로 쓰기도 했다. 이 range(10)라는 것이 레인지이다. 레인지는 다음과 같이 만든다. 알기 쉽게 예로서, 생성된 레인지에 포함되는 수열을 리스트로 표시해 두었다.

0부터 지정한 값의 직전까지의 범위

변수 = range(종료 값)

예)

range (10)
↓
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

   

지정된 값부터 지정한 값의 직전까지의 범위

변수 = range (시작 값, 종료 값)

예)

range(10, 20)
↓
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

지정된 값부터 지정된 값의 앞까지 일정한 간격으로 값을 얻는 범위

변수 = range(시작 값, 종료 값 간격)

예)

range(10, 50, 5)
↓
[10, 15, 20, 25, 30, 35, 40, 45]

레인지는 차례를 잘 늘어놓은 수열을 만들기 위한 것이다. 이것은 물론 일반적인 용도로도 사용할 수 있지만, 가장 많이 사용하는 것은 for일 것이다. for문으로 반복 처리할 때, 반복 범위 지정을 위해 레인지를 사용하는 경우가 가장 많은 것이다.

그럼 아래의 목록 란에 간단한 사용 예제를 보도록 하자.

for n in range(10): 
    print(n)

range 값을 for으로 순서대로 출력하고 있다.

2.4.5 - Python 입문 | 리스트, 튜플, 레인지, 세트, 사전 | 시퀀스 기능

목록 등의 “여러 값을 한꺼번에 처리한다"라는 것은 매우 유용하지만, 그러나 처음부터 고정된 수의 요소 밖에 사용할 수 없다면 조금 융통성이 없어 보인다.

하지만 염려하지 말자. 목록에는 요소를 추가하거나 제거하는 기능이 제대로 마련되어 있다.

그 대부분은 “순서"에 포함되어 있는 기능이다. 즉, 리스트, 튜플, 범위 중에서도 사용할 수 있는 기능이라는 것이다. 그럼 시퀀스의 기본적인 기능에 대해 정리해 보자.

새로운 요소를 추가

목록.append(값)

목록의 가장 마지막에 값을 추가한다.

지정된 인덱스 위치에 값을 삽입

목록.insert(인덱스, 값)

목록의 지정된 인덱스 번호의 위치에 값을 삽입한다.

지정된 값을 제거

목록.remove(값)

지정된 값을 목록에서 제거한다.

지정된 인덱스 번호의 요소 제거

del 목록 [번호]

목록 정해진 번호의 값을 제거하는 것이다. 이것은 del 후에 목록 삭제하는 인덱스 번호를 지정하여 작성한다. del 다음에, () 필요 없다.

여기까지 “추가”, “삽입”, “삭제"라는 것은 시컨스라고 해도 목록 밖에 대응되지 않는다. 왜냐하면, 이것은 목록 수정하는 작업이기 때문이다. 튜플과 레인지는 불변 객체 (변경 불가)이므로 이러한 작업은 할 수 없다.

이후에는 3개의 컨테이너에 공통되는 것이다. “시퀀스 전반에서 사용할 수 있는 기능"이다.

컨테이너의 덧셈

변수 = 컨테이너 + 컨테이너

목록과 튜플은 “+“로 덧셈을 할 수 있다. 2개의 컨테이너를 1개에 연결한 것을 만들 수 있다. 레인지는 수열이라는 이유로 해당하지 않는다.    

컨테이너의 곱셈

변수 = 컨테이너 * 정수

컨테이너의 값을 지정한 수 만큼 연결 한 것을 만든다. 예를 들어, 아래와 같다.

[1, 2, 3] * 3
↓
[1, 2, 3, 1, 2, 3, 1, 2, 3]

인덱스의 범위를 반환

변수 = 컨테이너[시작 값:종료 값]

인덱스 번호로 지정된 범위의 값을 꺼내기 위한 것이다. 예를 들어, [2:5]라고 하면 2~5의 인덱스 번호의 요소를 컨테이너에서 반환한다.

   

값이 포함되어 있는지?

값 in 컨테이너
값 not in 컨테이너

이것은 값이 컨테이너에 포함되어 있는지 여부를 확인한다. 결과는 부울이다. in은 값이 포함되어 있으면 True, 없으면 False이다. not in 반대로 포함되어 있으면 False, 없으면 True이다.

요소의 개수 얻기

변수 = len(컨테이너)

그 컨테이너에 몇개의 값이 저장되어 있는지를 정수로 반환한다.

최대 값, 최소값 얻기

변수 = max(컨테이너)
변수 = min(컨테이너)

컨테이너에 저장되어 있는 값 중에서 가장 큰 것, 가장 작은 것을 찾아서 반환한다.

대충 이것들을 사용해 리스트 내의 요소를 사용할 수 있게 되면 매우 편리하다.

아래에 간단한 사용 예를 들어 둡니다.

arr = ['hello','bye'] 
arr.append('finish!') 
arr.insert(1, 'welcome') 
arr.remove('bye') 
for n in arr: 
    print(n)

목록 arr에 새로운 값을 추가하거나 삭제한 결과를 출력한다. 최초의 입력한 목록과는 대부분 저장된 값이 변화하고 있는 것을 알 수 있을 거다.

2.4.6 - Python 입문 | 리스트, 튜플, 레인지, 세트, 사전 | 집합을 다루는 세트

여기까지의 리스트, 튜플, 범위는 모든 시퀀스라는 것이었다. 바꿔 말하면, 인덱스 번호를 사용하여 값에 일련 번호를 할당하고 순차적으로 정리하는 컨테이너였다.     하지만, Python에는 “값이 순차적이지 않는 컨테이너"도 있다. 그 중 하나가 “세트(set)“이다.     세트는 집합의 컨테이너이다. 세트는 값을 순서대로 정리하지 않는다. 세트 안에는 동일한 값을 여러개로 가질 수 없다. 저장되어 있는 값과 같은 값은 세트에 존재하지 않는 것이다.

이 세트는 {} 기호를 붙여 작성한다.

변수 = {값1, 값2, ...}

또는 set 함수를 사용하여 만들 수 있다. 인수에는 목록 등의 컨테이너를 제공한다.

변수 = set(값1, 값2, ...)

이제 세트를 만들 수 있다. 그러나! 인덱스가 없기 때문에, 여기에서 필요한 값을 뺄 수 없다. 그럼, 무엇을 위해 있는지?라고 생각할 수도 있다. 세트는 집합이다. 따라서 어떤 값이 이 집합에 포함되어 있는지에 대한 여부를 확인할때 사용을 한다.

세트 조작

세트에서도 세트를 조작하는 기능이 여러가지 준비되어 있다. 그러나 주의하지 않으면 안되는 것은, “세트는 값의 순서가 없다"는 점이다. 이것을 잊지 말고 가보도록 하자.        

값 추가

세트.add(값)

값 추가는 “add"라는 것을 사용한다. 이것으로 ()안에 값이 세트에 추가된다. 그러나 이미 세트에 동일한 값이 있는 경우는 아무런 변화가 없다.        

값을 삭제

세트.remove(값)

값 삭제는 remove를 사용한다. 이것은 시컨스와 같다. 이것으로 ()안의 값이 세트에서 삭제된다.    

요소의 개수 얻기

변수 = len(세트)

그 세트에 몇개의 값이 저장되어 있는지를 정수를 반환다. 이것은 이미 나왔던 거다.        

최대 값, 최소값 얻기

변수 = max(세트)
변수 = min(세트)

세트에 저장되어 있는 값 중에서 가장 큰 것, 작은 것을 찾아서 반환해 준다. 이것도 시컨스에서 사용했었다.

세트의 뺄셈

세트1 - 세트2

세트는 덧셈과 곱셈은 없지만, 뺄셈은 있다. 이것으로 “세트1"에서 “세트2” 요소를 제거한 나머지를 새로운 세트로 얻을 수 있다.

   

세트의 비교 연산

세트1 == 세트2 등

세트는 비교 연산이 가능하다. = <> 등의 기호 류를 사용한 비교 식을 사용하여 두 세트를 비교할 수 있다. 다만, <>는 “어느 쪽의 세트가 크거나 작다"는 의미는 아니다. 이것은 “어딘가가 어딘가에 포함되어 있는지"를 나타낸다. 예를 들어 A>B라고 하면, “A세트에 B세트가 포함되어 있는지"를 나타낸다.        

세트의 논리 연산

 세트1 & 세트2
 세트1 | 세트2
 세트1 ^ 세트2

이 논리 연산은 집합인 세트 특유의 것이다. 이것들은 두개의 세트(집합)을 연산하여 새로운 세트를 만든다. 이것은 다음의 연산자를 사용해서 세트를 만들어 낸다.  

기호 설명
& 2개의 세트에 공통되는 요소만을 가진 세트를 생성한다. (논리적)
| 2개의 세트에 있는 모든 요소를 가진 세트를 생성한다. (논리합)
^ 두 세트의 어느 한쪽에만 있는 요소로 구성된 세트를 생성한다. (배타적 논리합)

 

마지막 논리 연산은 조금 이해하기 어려울지도 모른다. 실제 사용 예제를 참고하여 여러가지를 시도해 보자.

a = {'a', 'b'} 
b = {'b', 'c'} 
c1 = a & b 
c2 = a | b 
c3 = a ^ b 
print(c1) 
print(c2) 
print(c3)

2.4.7 - Python 입문 | 리스트, 튜플, 레인지, 세트, 사전 | 키 값을 관리하는 사전

목록도 튜플도 인덱스라는 번호를 사용하여 값을 관리한다는 점에서는 같았다. 이 숫자가 아닌 “이름"을 사용하여 값을 관리하는 것도 Python에는 제공이 되어 있다. 그것은 “사전(dictionary)“이 라는 것이다.   사전은 ‘키워드’라는 이름을 붙여 값을 관리한다. 그리고 값을 제거하거나 변경하는 경우에는 그 값의 키를 지정한다. 사전은 다음과 같은 형태로 만든다.

변수 = {키1:값1, 키2:값2, ...}

또는 dict라는 것을 사용하여 만들 수 있다. 다만, 이 경우는 작성 방식이 조금 다르기 때문에 주의해야 한다.

변수 = dict (키1=값1, 키2=값2, ...)

사전에서 값을 꺼내는 경우는 시퀀스와 마찬가지로 []를 사용한다. 다만, 인덱스가 아닌 키워드를 []로 지정한다.

변수 = 사전[키]
사전[키] = 값

사전을 이용했을 때, 초보자가 착각하기 쉬운 것은 “사전은 키를 사용해도 다른 것과 동일하게 값을 꺼낼 수 있다"라는 생각이다. 즉, 키로도 꺼낼수 있고, 번호로도 꺼낼 수 있다라고 생각할 수 있다. 하지만 사전은 “키"밖에 사용할 수 없다. 다시 말하면, 사전에 있는 값을 번호 순서대로 추출 할 수 없다.  

for in 주의!

모든 요소를 처리하기 위한 “for ~ in"구문은 사전에서도 사용할 수 있지만, 그 동작이 미묘하게 다르므로 주의가 필요하다.

리스트나 튜플에서 “for 변수 in 목록"이라고 하면, 리스트의 값이 변수로 꺼낸진다. 하지만 사전의 경우 꺼내지는 것은 각각의 “값"이 아니고 “키"이다. 즉, 변수에 추출된 키를 사용하여 값을 꺼내 사용하는 형태가 되는 거다.     그럼, 아래에 간단한 사용 예를 보도록 하자.

dic = {'taro':'taro@yamada.com', 
       'hanako':'hanako@flower', 
       'ichiro':'ichiro@baseball'} 
for n in dic: 
    print(n + ' (' + dic[n] + ')') 

여기에서는 각각의 이름을 키로하여 이메일 주소를 설정하고 있다. for를 사용하여 사전의 모든 데이터를 표시하고 있다. 사전을 사용하면, 이 처럼 작은 데이터베이스와 같은 사용이 가능해 진다.

사전 조작

사전도 다른 컨테이너와 같은 방법으로 조작 할 수 있는 기능이 여러가지가 제공되어 있다. 주요한 것에 대해 정리하겠다.

· 값 추가

사전[키] = 값

사전에 새로운 값을 추가하는 것은 간단한다. []으로 추가할 키워드를 지정하고 값을 대입하면 된다. 사전에서 해당 키워드가 아직 사용되지 않고 있다면, 새로운 키워드 항목이 추가된다. append와 add와 같은 기능의 함수가 필요없다.    

값 삭제

del 사전[키]

값을 삭제하려면, remove는 사용할 수 없다. del을 사용하여 삭제한다.  

모든 키 얻기

변수 = 사전.keys()

모든 값 얻기

변수 = 사전.values()

모든 항목(키, 값) 얻기

변수 = 사전.items ()

사전에 키워드와 값을 함께 얻는 기능이 있다. keys/values는 사전에 저장되어 있는 모든 키워드/값을 컨테이너로 모와서 꺼낸다. 또한, items는 키워드와 값을 튜플에 정리한 것을 얻는 거다.

정리

사전는 목록 등에 비해 ‘키워드 값을 꺼내기’라는 특성상 다소 특수한 용도로 사용된다. 데이터의 순서가 중요하지 않는 경우라면, 데이터 이름을 붙여 관리하는 것이 번호로 관리하는 것보다 압도적으로 편리하다. 프로그램 작성에 있어서 경우에 따라 “리스트로 하는 것이 좋을지, 사전을 사용하는 편이 편리할지"를 생각해서 이용하도록 하자.

2.5 - Python 입문 | 함수(function)

스크립트의 일부분을 잘라내어 언제든지 사용할 수 있도록 하는 “함수”, 이것을 잘 다루면 긴 프로그램을 구조적으로 조립이 가능하다. 그 기본을 설명한다.

2.5.1 - Python 입문 | 함수(function) | 함수란?

스크립트라고 하는 것은, 같은 처리를 여러번 반복하는 경우가 많다. 그 때마다 일일이 같은 스크립트를 여러번 쓰는 것은 매우 귀찮다. 이러한 “정해진 처리"를 언제 어디서나 호출할 수 있도록 하는 것이 “함수"이다.

예를 들어, 아래에 올린 예제(1)과 같은 예를 생각해 보자. 변수에 이름을 넣어 “Hello, OO. How are you?“라고 출력하는 스크립트이다. 이것은 유사한 텍스트를 출력하기 위해, 유사한 print 문을 여러번 작성을 하였다. 어쩐지 너무 바보 같지 않은가?     예제 (1)

a = "Taro" 
b = "Hanako" 
c = "Ichiro" 
   
print("Hello, " + a + ". How are you?") 
print("Hello, " + b + ". How are you?") 
print("Hello, " + c + ". How are you?")

이러한 때 “함수"가 도움이 된다. 예제(2) 함수를 사용하여 다시 작성한 것이다. 첫째로, 정해진 형태로 출력하는 함수를 먼저 준비해두면, 그 후로는 “showMsg(“Taro “)“라고 하면 언제든지 호출 할 수 있다. 호출을 하는 것만으로 지정된 형태의 메시지가 표시될 것이다.

예제(2)

def showMsg(str): 
    print("Hello, " + str + ". How are you?") 
   
showMsg("Taro") 
showMsg("Hanako") 
showMsg("Ichiro") 

여기에서는 짧은 메시지를 표시 할 뿐이지만만, 좀 더 복잡한 처리가 되면 “한번 스크립트를 작성해 놓으면, 그것은 언제든지 호출하여 실행할 수 있다"라는 것은 매우 편리하다라는 것을 알 수있을 것이다.

print도 함수?

이 함수라는 것은 이미 실은 여러분은 사용하고 있었다. 값을 출력하는 “print"이다. Python은 기본적으로 많은 기능을 사용할 수 있게 되어 있다. 대부분은 “함수"로 준비되어 있는 것이다.

2.5.2 - Python 입문 | 함수(function) | 함수의 정의

이제 이 함수는 어떻게 만드는지, 설명하겠다. 함수는 다음과 같은 형태로 정의한다.     함수의 정의 (1)

def 함수 이름(인수1, 인수 2, ...):
    ...... 수행  작업 ......

함수 정의의 기본은 “def 함수 이름"이다. 이전에 샘플(2)는 “def showMsg ~“라고 되어 있기 때문에, showMsg라는 함수가 만들어진 것이다.

그리고 함수 이름 뒤에 ():을 붙이고 그 이후로는 줄 바꿈하고 들여 쓰기를 하여, 수행할 처리를 작성한다.

(): 안에는 ‘인수’라는 것을 추가할 수 있다. 인수는 함수를 호출할 때에 어떤 값을 받아서 전달하는데 사용한다. 예를 들어 샘플(2)에서

def showMsg(str) :

이렇게 되어 있었다. 이는 ()안에 있는 “str"라는 인수가 포함되어 있다는 것이다.

이것은 “이 함수를 호출 할 때, 어떤 값을 함께 쓰기 때문에, 그것을 str이라는 변수에 넣어 전달한다"라는 의미이다. 샘플에서 호출하는 부분을 살펴 보자.

showMsg( "Taro")

자, 이런 식으로 함수 이름 뒤에 ()를 붙이고, “Taro"라는 값이 작성되어 있는 걸까? 이 “Taro"가 showMsg 함수의 “str” 변수에 전달된다.

실행중인 처리를 보면, 이렇게 되어 있다.

print("Hello, " + str + ". How are you?") 

전달된 변수로 str을 사용하여 메시지를 print하고 있는 것을 알 수 있다.     인수는 하나뿐 아니라 얼마든지 추가할 수 있다. 이 경우 각각의 변수를 쉼표로 구분하여 작성한다.

def showMsg(a, b, c): 

이런 식이다. 인수가 없는 경우에도 ()은 붙이지 않으면 안된다.   함수를 이용하는데 있어서 최소한 기억하지 않으면 안 것은 우선 이것뿐이다. 의외로 간단하지 않는가?

2.5.3 - Python 입문 | 함수(function) | 반환 값

함수는 함수 이름과 인수가 제대로 알면 정의 할 수 있다. 사실은 함수 정의 부분에 나타나지 않는 또 하나의 중요한 요소가 있다. 그것은 “반환 값"이다.

반환 값은 함수를 실행한 후, 어떤 값을 호출한 곳에 돌려주는 역할을 한다. 이 반환 값은 “return"이라는 것을 사용하여 설정한다.     함수의 정의 (2)

 def 함수 이름(인수 1, 인수 2, ...):
     ...... 수행  작업 ......
     return 

이와 같이 처리 한 후, 마지막에 “return 값"으로 인해 값을 반환하고, 호출 곳에 값이 전달된다.

실제로 반환 값을 사용해 보자. 이전에 샘플을 반환 값을 반환하는 형태로 고치면 아래와 같다.  

def showMsg(str): 
    return "Hello," + str + ".How are you?"
   
res = showMsg("Taro") 
print(res) 
res = showMsg("Hanako") 
print(res) 

여기에서는 showMsg 함수에서 return을 사용하여 텍스트를 반환한다. 이 함수를 호출하는 부분을 보면,

res = showMsg("Taro")

이렇게 되어있는 것을 확인할 수 있다. showMsg의 결과를 변수 res에 대입하고, 이것으로 반환 값이 res에 할당되게 된다. 그러고 나서는 이 res를 사용하여 결과를 표시하고 있다.

2.5.4 - Python 입문 | 함수(function) | 키워드 인수

함수를 구성하는 요소 중에 의외로 다기능 것이 “인수"이다. 이것은 일반적으로 값을 전달 외에 여러가지 옵션을 가지고 있다.

먼저 “키워드 인수"라는 것이 있다. 이것은 인수에 키워드(이름)을 붙여 사용할 수 있도록 하는 기능이다. 무슨 말인가 하면, 아래와 같은 것이다.     함수의 정의 (3)

 def 함수(키1=초기값1, 키2=초기값2, ...):

키와 초기 값을 지정하는 것이다. 그러면 키를 사용하여 인수를 지정할 수 있다. 보통 인수는 순서가 정해져 있지만, 키를 사용하여 순서에 관계없이 값을 작성할 수 있다.

또한, 초기 값을 설정할 수 있기 때문에 값을 생략할 수 있다 (생략하면 기본값이 사용된다). 보통 인수는 반드시 값을 전달하지 않으면 안되지만, 키워드 인수로 하게 되면 옵션 다루는 (없어도 OK) 인수를 만들 수 있다.

그럼 실제 사용 예를 살펴 보자.

def showMsg(name, header='Hello', footer='How are you?'): 
    print(header + "," + name + ". " + footer) 
   
showMsg("Taro") 
showMsg("철수", '안녕', '건강하니?') 
showMsg("영희", footer='잘지내니?', header='야') 

여기에는 세가지 인수의 지정을 해서 showMsg를 호출한다. showMsg(“Taro”)와 같이, 첫번째 인수에 이름을 지정하는 것만으로도 제대로 동작하고, 두번째와 세번째는 초기값으로 동작한다.

또한 키워드는 붙여도 붙이지 않아도 동작한다. 다만, 키워드를 붙이지 않는 경우는 인수가 정의된 순서대로 지정해야 한다. 키워드를 붙여 인수를 작성할 경우는 어떤 순서라도 상관없다.

여기에서는 키워드가 없는 인수와 있는 인수가 혼재하고 있지만, 이러한 경우에는 반드시 “키워드가 없는 인수"를 먼저 정의하고 키워드 인수는 다음에 정의해야 한다. 키워드 인수 후에 키워드가 없는 인수를 지정하면 문법 오류이다.

2.5.5 - Python 입문 | 함수(function) | 가변 인자

인수에 대해 또 다른 설명을 하고 싶은 것이 “가변 인자"라는 것이다. 가변 인자라는 것은 “길이 (인수의 수)가 가변 인수"이다. 즉, “몇 개의 인수를 붙여도 된다"라는 특별한 인수이다.     “인수가 몇개 있어도 된다? 그것을 어떻게 정의하는 걸까?“라고 이상하게 생각 하겠지만, 가능하다. “정의 할 수 있지만 어떻게 값을 받을 거야?“라고 생각 하겠지만, 받을 수 있다.

가변 인자라는 것은 알기 쉽게 말하자면, “많은 인수를 컨테이너에 모와서 받을 인수"이다. 즉, “목록을 인수로 설정한 것"이라고 말할 수 있다. 다만, 목록을 인수에 쓰는 번거로움 때문에, (목록에 보관해 두는 값을) 하나씩 인수에 넣으면 자동으로 그것들을 한꺼번에 넘겨주게 되어 있다.

이 가변 인자는 다음과 같이 정의한다.     함수의 정의 (4)

def 함수(*인수):

인수 정의하는 변수 이름 앞에 별표 (*)를 붙이면, 그 인수가 가변 인자로 설정된다. 이 인수에는 여러 인수로 정의한 값이 n개로 모와서 전달된다. 그 후로는 거기로 부터 필요한 값을 꺼내는 처리만 하면 된다.

그럼 이것도 간단한 예제를 살펴 보자.

def calc(*num): 
    total = 0
    for n in num: 
        total += int(n) 
    print('합계:' + str(total)) 
    print('평균:' + str(total // len(num))) 
   
calc(123, 456, 789, 246, 357, 910) 

여기에서는 calc(* num)와 같이 함수를 정의하고 있다. 이것으로 num이라는 변수에 입력된 모든 인수를 컨테이너에 모와서 전달된다. 그 후에는 이 num을 for문으로 반복해 나가면 된다.

2.6 - Python 입문 | 클래스 사용

함수뿐만 아니라 다양한 변수를 포함한 큰 프로그램을 하나의 묶음으로 정의하는 것이 “클래스(class)“이다. 클래스의 기본적인 사용법부터 설명한다.

2.6.1 - Python 입문 | 클래스 사용 | 함수와 클래스

함수는 하나의 처리를 하나로 통합한 것이지만, 이런 함수가 많이 늘어나면, 점차적으로 어느 것이 무슨 역할를 하는지 의미를 알기 힘들어 진다. 예를 들어, 수백개의 함수가 나열되어 있으면, 그것을 전부 이해해 나가는 것은 힘들 것이다.

그래서 “비슷한 역할을 하는 것을 한곳에 모으자"라고 누구든지 생각된다.   예를 들어, 어떤 데이터 처리를 만드는 것을 생각해 보자. 데이터를 관리하는 함수, 데이터를 추가하는 함수, 데이터를 삭제하는 함수, 데이터를 출력하는 함수 …… 따위가 쭉 늘어서 있는 것은 그다지 사용하기가 쉽지 않다.    그래서 “데이터를 처리하기 위해 필요한 것"을 모두 한 묶음으로 두자"라고 생각하게 된다. 큰 “데이터 관계 묶음’이라는 것을 만들고, 그 안에 “데이터를 보관할 변수”, “데이터를 파일에 읽고 쓰는 함수”, “데이터를 추가하거나 삭제하는 함수”, “데이터를 출력하는 함수” ……와 같이, 그 데이터 처리에 필요한 변수와 함수를 모두 하나로 모으자는 것이다.

그렇게 되면 데이터의 처리에 관해서는 우선 “이 묶음에 안에 반드시 있다"라는 것이 되기 때문에, 곳곳의 함수를 찾지 않아도 된다.   이것이 “클래스(class)“의 개념이다. 클래스라는 것은 어떤 목적을 위해 필요한 ‘값’과 ‘처리’를 모두 한 묶음으로 한 것이다.   이 클래스는 아래와 같은 같은 형태로 만든다. “class 클래스 이름:“라는 것으로 시작하여 그 아래에 클래스가 제공하는 변수와 함수를 들여 쓰기하여 작성한다.    

클래스의 정의

class 클래스 이름:
  변수1
  변수2
  ......필요한 만큼 변수를 제공......

  def 메소드1(인수):
     ......메소드의 처리......
   
  def 메소드2(인수):
     ......메소드의 처리......
   
  ......필요한만큼 메소드를 제공......

클래스에 필요한 값을 저장하는 변수를 “멤버 변수”, 클래스에 제공하는 함수를 “메소드"라고 한다.

이러한 작성법은 기본적으로 일반 변수와 함수의 작성법 동일하다. 단지 “class OO:“라는 정의에 쓰면 멤버 변수와 메소드로 처리 할 수 있게 된다는 것 뿐이다. 특별한 작성법 등은 없다.

2.6.2 - Python 입문 | 클래스 사용 | 클래스 생성

이제 실제로 클래스를 만들어 사용해 보기로 하자. 이전에 “이름을 사용하여 메시지를 표시한다"는 것을 클래스에 해보기로 한다.

아래에 샘플을 예제를 보도록 하자. 여기에서는 “Member"라는 클래스를 만들었다.

class Member: 
    name = "" 
   
    def showMsg(self): 
        print("Hello, " + self.name + ". How are you?")

멤버 변수는 이름을 저장하는 “name"이라는 변수를 선언하였다. 메소드는 메시지를 표시하는 “showMsg"를 선언하고 있다.

이 클래스의 소스 코드를 보면, 여태 보지 못한 것이 등장하고 있다. “self"라는 것인다. 이것은 showMsg 인수에 사용되고 있다. “뭐야, 단지 인수에 사용한 변수가?“라고 생각하면 그렇지 않다는 것을 알 수 있다.

메소드 안에는 ‘self.name’라는 식으로 왠지 잘 모르는 사용 법을 사용하고 있다.

사실은 이 “self"라는 것은 “자신"을 나타내는 특별한 값이다. 자신은 클래스가 아니다. 클래스에서 만들어진 “인스턴스"라는 거다.

인스턴스와 self

클래스라는 것은 함수 등과 같이 그대로 클래스에서 메소드를 호출하거나 해서 사용하지는 않는다. 클래스를 이용하기 위해서는 “인스턴스"라는 것을 만들어야 한다.     클래스라는 것은 말하자면 프로그램의 “청사진"이다. 이것 자체를 조작하는 것이 아니라, 이 클래스는 설계도를 바탕으로 실제로 사용할 수 있는 부품을 만들어 그것을 조작하는 거다. 이 부품이 인스턴스이다.

만약 클래스를 그대로 사용하면, 그 클래스의 기능을 몇번이고 사용하고 싶다면 많은 클래스를 만들어야 한다. 예를 들어, 샘플 Member 클래스를 사용하여 “Taro"며 “Hanako"의 데이터를 관리하려 했다고 생각해 보자. 클래스를 그대로 사용하게 되면 그 name에 “Taro"로 설정하면, 또 “Hanako"는 보관할 수 없게되어 버린다.

그래서 클래스를 바탕으로 “인스턴스"라는 부품을 만들고, 그것에 Taro로 설정해 주어야 한다. Hanako가 필요하게 되면, 또한 클래스에서 새로운 인스턴스를 만들고, 거기에 Hanako라고 설정 해준다. 이런 상태로 “Member 클래스를 사용할 필요가 있으면 새로 Member의 인스턴스를 만들고, 이름을 설정해 주어야"입니다. 이렇게 하면 이 클래스를 바탕으로 얼마든지 데이터를 처리할 수 있게 된다.

그리고, 이 인스턴스 자신을 가리키는 데 준비하는 것이 “self"라는 것이다.

예를 들어, 어떤 메소드에서 “이 인스턴스에 저장되어 있는 멤버 변수의 값을 사용해야 한다"고 해보자. 이 예에서 말한다면, Member의 name 값을 showMsg에서 사용하는 경우이다.

이 때, 그냥 ’name’변수 이름으로 사용할 수 없다. “이 인스턴스 안에 있는 name” 형태로 지정을 해주지 않으면 안된다.   그래서 Python 클래스에 제공되는 메소드는 반드시 첫 번째 인수에 인스턴스 자신을 나타내는 값을 전달하도록 해야 한다. 이것이 “self"의 정체는 것이다. 이 self 안에있는 멤버 변수와 메소드는

self."변수"

따라서, self후에 점(.)으로 지정한다. 예를 들어, 여기에 “self.name"라고 하고 name 멤버 변수를 지정하고 이용하면 된다.

2.6.3 - Python 입문 | 클래스 사용 | 클래스 사용

이제 만든 Member 클래스를 사용해 보자. 아래에 사용할 소스 코드 예제가 있다.

class Member: 
    name = "" 
   
    def showMsg(self): 
        print("Hello," + self.name + ".How are you?") 
   
taro = Member() 
taro.name = "Taro"
taro.showMsg() 
   
hanako = Member() 
hanako.name = "Hanako"
hanako.showMsg()

여기에서는 Taro와 Hanako라는 2명의 데이터를 처리하기 위해 2개의 인스턴스를 만들어 사용하고 있다.     인스턴스의 생성은 “클래스 이름()“과 같이 이걸을 호출을 한다. 여기에서는

taro = Member()

이렇게 호출하고 있다. 이제 변수 taro에 Member 클래스의 인스턴스가 만들어 보관된다. 또한 인스턴스의 멤버 변수와 메소드는 점(.)을 사용하여 변수 이름 뒤에 해당되는 변수와 메소드를 작성해서 호출한다. 예를 들어,

taro.name = "Taro"
taro.showMsg()

이것은 taro 인스턴스의 name 멤버 변수에 “Taro"라고 값을 설정 해주고, 그러고 나서 showMsg 메소드를 호출해 실행을 한다. 이런 식으로 인스턴스를 만들어 변수에 할당 해두면 클래스 안에 있는 요소는 자유롭게 사용할 수 있다.

또한, 이 name와 같이 각 인스턴스에 값을 저장하고 사용하는 멤버 변수를 “인스턴스 변수"라고도 한다.    

self는 어디 갔지?

그런데 이 사용 예를 보고 무언가 의문을 생기지 않나요? 그것은 showMsg를 호출하는 부분이다. “taro.showMsg()“라고 되어 있다.

어? showMsg는, 첫번째 인수에 “self"가 준비되어 있지 않은가? 그 self는 도대체 어떻게 된 것일까?

사실을 말하면, 메소드의 첫번째 인수로 전달되는 “인스턴스 자신"의 값은 Python의 시스템에 의해 자동으로 넘겨지게 된다. 즉, 첫번째 인수(self) 메서드를 호출할 때 불필요한 거다. 호출할 때, 두 번째 인수 이후만 작성한다. (여기에서는 첫번째 인수밖에 없기 때문에, 호출할 때 인수를 생략했다)

이런 식으로 인스턴스를 만들고 그 안의 멤버 변수를 설정하고 메서드를 호출한다. 이것이 클래스를 사용하는 기본이다. 이러한 기본 작업을 알면 클래스는 쉽게 사용할 수 있다.

2.6.4 - Python 입문 | 클래스 사용 | 생성자 사용

그렇게 하더라도,이 Member클래스 별로 유용하지 않는다. 인스턴스를 만들고, name을 설정하여 showMsg를 호출 … 결국 하나 하나 다하고 있는 것은 클래스를 사용하지 않는 경우와 별로 차이가 없다. 게다가 인스턴스를 만든 후에 멤버 변수의 설정을 잊으면, 생각대로 움직이지 않게 되어 버린다. 적어도 “필요한 값은 처음부터 제대로 설정 사용"하도록 하고 싶다.

이럴 때에 유용한 것이 ‘생성자(constructor)‘라는 것이 있다. 생성자는 “인스턴스를 만들 때 자동으로 호출되는 인스턴스 초기화를 위한 특별한 방법"이다. 이것은 다음과 같이 만든다.

def __init __ (self 인수 ...):
    ...... 초기화 처리 ......

생성자는 “__init__“와 같은 이름으로 메소드를 작성한다. 만약 어떤 값을 인수로 전달 싶다면, 2번째 인수 이후에 지정한다(첫번째 인수는 반드시 self이다).

이와 같이 생성자를 준비하면 인스턴스를 만들 때, 이 생성자를 사용하게 된다. 아래 예제를 보도록 하자.  

class Member: 
   name = "" 
  
   def __init__(self, str): 
       self.name = str
  
   def showMsg(self): 
       print("Hello," + self.name + ".How are you?") 
  
taro = Member("Taro") 
taro.showMsg() 
  
hanako = Member("Hanako") 
hanako.showMsg()

여기에서는 다음과 같이 하여 str라는 인수를 전달하는 형태로 생성자를 제공하고 있다.

def __init__(self, str):

이제 인스턴스를 만들 때 name을 설정해야 한다. 실제로 인스턴스를 만들고 있는 곳을 보면,

taro = Member("Taro")

이런 식으로 ()에 이름을 인수로 넣고 있는 것을 볼 수 있다. 이렇게 해서 인수를 지정하여 인스턴스를 만들 수 있게 하면 필요한 멤버 변수의 설정도 한꺼번에 할 수 있어 매우 편리하다.

2.6.5 - Python 입문 | 클래스 사용 | 상속 클래스

클래스를 정의하는 가장 큰 장점은 “재사용이 가능하다"라는 것이다. 한번 만들면, 이후에는 그것을 그대로 복사하여 여기 저기에서 사용할 수 있다.

그러나 실제로 사용해 보면 “여기는 이렇게 하는 것이 더 좋다"라고 하는 것이 나올 거다. 하지만, 어느 정도의 규모의 프로그램가 되면, 곳곳에서 클래스를 사용하고 있으면 이런 수정은 제공되지 않는다.

이럴 때 정말 편리한 기능이 준비되어 있다. 그것은 “상속"이라는 것이다.

상속이라는 것은 이미 클래스를 그대로 이어 받아 새로운 클래스를 만드는 것이다. “이어 받는다"라는 것은 “클래스의 모든 기능을 그대로 이어 받는다"라는 것이다. 즉, 그 클래스에 있는 것을 통째로 그대로 받아 새로운 클래스를 만드는 거다.

이 상속을 사용하여 클래스를 정의하려면 다음과 같이 클래스를 만듭니다.

def 클래스 이름(상속하는 클래스):
    ...... 클래스의 내용 ......

상속에는, 상속하는 원래 클래스를 “기본 클래스”, 새로 만든 클래스를 “파생 클래스"라고 한다.

실제 사용 예를 아래와 같다.

class Member: 
    name = "" 
   
    def __init__(self,str): 
        self.name = str
   
    def showMsg(self): 
        print("Hello," + self.name + ".How are you?") 
   
class PowerMember (Member): 
    mail = "" 
   
    def __init__(self,str1,str2): 
        self.name = str1 
        self.mail = str2 
   
    def showMsg(self): 
        print("Hello," + self.name + ".") 
        print("Your mail address is '" + self.mail + "'.") 
   
   
taro = Member("Taro") 
taro.showMsg() 
   
hanako = PowerMember("Hanako","hanako@flower.com") 
hanako.showMsg() 

여기에서는 Member 클래스를 상속하여 PowerMember라는 파생 클래스를 만들고 있다.

이 PowerMember 클래스는 def __init__(self, str1, str2):이렇게 하여, 2개의 인수를 준비했다. 그리고 self.name과 self.mail에 각각 설정하고 있다. 그런데, 이상한게 있다. 조금 자세히 더 살펴 보도록 하자.

잘 보면 이 PowerMember 클래스는 “mail"밖에 인스턴스 변수가 포함되어 있지 않았다. 그런데 self.name 값은 잘 저장하고 있다. 이것은 기본 클래스인 Member에 name가 준비되어 있기 때문이다. 상속은 기본 클래스의 모든 기능을 이어 받아 사용할 수 있다. 따라서 PowerMember에 name을 준비 할 필요는 없다.

멤버 변수뿐만 아니라 메소드도 모든 슈퍼 클래스에 있는 것은 그대로 사용할 수 있다. 이 상속을 사용하면 이미 있는 클래스를 점점 확장하여 기능 강화해 나갈 수 있다는 것이다.

Python에는 다양한 클래스가 라이브러리에 포함되어 있다. 그리고 그들을 이용할 때, 이 “상속"이 사용되고 있다.

예를 들어, 먼저 “리스트”, “튜플”, “레인지"라는 컨테이너에 대해 설명을 했었다. 이런 것도 실은 모든 “클래스"로 준비되어 있다. 이 3개의 클래스에 공통된 기능이 많은 것은 ‘시컨스’라는 인덱스에서 관리하는 컨테이너 클래스가 있고 그것을 계승하고 리스트나 튜플이 있다고 생각하면 이미지화하기 쉬울 것이다.

3 - Swift 입문

Swift 입문

Swift(스위프트)는 Mac OS X 및 iOS (iPhone, iPad) 앱을 만들기 위해 새롭게 개발 된 프로그래밍 언어이다. Swift의 기본을 배워서 앱 개발에 도전해 보자!

3.1 - Swift 입문 | Swift 사용 준비

Swift는 애플의 순정 개발 환경인 “Xcode"로 이용한다. 우선 사용을 위한 준비를 하여 간단한 프로그램을 실행에 설명한다.

3.1.1 - Swift 입문 | Swift 사용 준비 | Swift(스위프트)란?

Swift는 2014년 6월에 개최된 애플 개발자 컨퍼런스에서 갑자기 발표된 완전히 새로운 프로그래밍 언어이다.

기존에는 Mac OS X 및 iOS (iPhone과 iPad) 앱 개발은 “Objective-C (오브젝트-씨)“라는 프로그래밍 언어를 사용했었다. 이는 Mac OS X의 성립 유래에 따른다.

Mac OS X은, 실은 그 이전에 있었던 ‘NeXT STEP’라는 OS를 기반으로 하고 있다. NeXT STEP은 원래 Objective-C라는 강력한 객체 지향 언어를 개발했을 때, 이 Objective-C에 의한 프로그램 작성 및 실행을 위한 플랫폼으로 만들어진 것이다. 즉, Objective-C와 NeXT STEP은 일심 동체에서 이 둘은 거의 같은 것으로 해도 좋을 정도로 밀접한 관계에 있었다. 이 NeXT STEP을 개발하고 있던 곳이 당시 NeXT사였고, CEO가 스티브 잡스였다.

스티브 잡스가 애플에 복귀했을 때,이 NeXT STEP도 함께(?) 애플로 이적하고 이를 바탕으로 “Mac OS X"가 만들어 졌다. 물론 외관은 Mac 다워 졌지만, 내용은 NeXT STEP이었다. 그래서 Mac OS X도 “Objective-C 프로그램을 만들어 움직인다"것이 전제가 되어 있었다.

이 대전제는 사실 지금도 바뀌지 않는다. Mac OS X (그리고, 이를 바탕으로 만들어진 iOS)는 Objective-C을 위해 준비된 프레임 워크 내부에 포함되어 있으며,이를 이용하여 모든 프로그램이 움직이고 있다. “Mac 또는 iPhone의 개발은 왜 Objective-C가 아니면 안되는 걸까” 이유는 이런 것이었다. 원래 “Objective-C로 만드는 것을 전제로 설계된 OS “이기 때문이다.

이 Objective-C는 솔직히 매우 이해하기 어려운 언어이다. 이것은 C언어에 Smalltalk는 객체 지향 언어의 원조라고도 말할 수있는 것을 결합시킨 같은 언어로 하나의 언어에서 두 언어의 문법이 동거하고 있는 것 같은 이상한 문법으로 되어 있다. C의 또 다른 개체 언어 발전형인 C++, 이것에 문법이 가까운 Java, C # 등의 언어가 주류가 됨에 따라 이들로 부터 벗어난 Objective-C는 “보통의 언어와 다르고, 배우기 어려운 언어"라고 인식되어 갔다.

Mac 또는 iPhone이 붐이 되어 많은 개발자가 이러한 응용 프로그램을 만들려고 모여들었다. 하지만 이 “Objective-C의 첫인상 어려움"에 질리게 되다. 왜 좀 더 이해하기 쉬운 언어를 사용할 수없는 거야? 라고 많은 개발자들이 생각하게 된다.

이러한 상황은 애플에게도 바람직한 것은 아니다. 그래서 Objective-C와는 다른 새로운 “배우기 쉽고 알기 쉬운 사용하기 쉬운 언어"등장하게 된 것이다. 그것이 “Swift"이다.

3.1.2 - Swift 입문 | Swift 사용 준비 | Swift 특징

그럼, 이 Swift라는 것은 어떤 걸까? 그 특징을 간단하게 정리해 보겠다.

현대 언어이다

Objective-C는 현대의 언어가 채택하고 있는 다양한 “멋진 기능"을 많이 지원하지 않았다. Swift는 Objective-C에서는 사용할 수 없었던 다양한 언어의 기능을 사용할 수 있게 되어 있다. “클로져"라는 값 처럼 처리를 취급할 수 있는 기능과 제네릭이라는 다양한 종류의 값을 처리하는 기능 등이 준비되어 있다.

안전하다

Swift는 프로그램에 버그가 섞여 쉬운 다양한 기능을 코드에서 제거 문법이 있다. 예를 들어, 변수는 반드시 초기 값을 설정하지 않으면 안되게 하거나, 객체를 처리하는 변수에 “개체가 없는 텅 빈 상태"를 금지하고, 변수는 처음부터 형태를 설정하도록 되어 있고, 제어 구문에서는 반드시 {}으로 처리를 둘려싸지 않으면 안되거나… 어쨌든 “프로그래머에 의한 무심코하게 되는 실수"를 없애도록 할 수 있다.

인터랙티브이다

Swift는 컴파일러 언어이다. 미리 프로그램을 컴파일 해두고 컴퓨터가 직접 실행할 수 있는 바이너리 코드로 프로그램을 작성한다. 그러나 동시에 인터프리터(순서대로 실행하는 방식)으로도 움직일 수 있다. Xcode는 “플레이 그라운드(play grount)“라는 것이 준비되어 그 자리에서 구문을 쓰고 실행하면서 움직일 수도 있다. 당분간은 이런 식으로 Swift의 기본을 배워나가게 될 것이다.

수행이 빠르다.

이러한 다양한 기능이 포함되어 있기 때문에 “과연, 편리한 것 같지만, 그래도 작성한 응용 프로그램은 Objective-C 쪽이 척척 움직일 것 같다"고 생각할지 모른다. 하지만 실은 그렇지도 않는다. 애플에 따르면, Swift로 만들어진 프로그램은 Objective-C보다 빠르게 수행 할 수 있는 거다.

그러나 다른 프로그래머들에 의하면 Swift에는 많은 병목 현상(bottleneck)이 존재하는 것으로 알려져 있으며, 현재 “Objective-C보다 빠르다"고 단언은 할 수 없지만, 어느쪽으로 정식 출시될 때에 아마 그만한 성능를 실현하고 있는 것이다.

사실 백본(backbone)은 Objective-C와 같다

원래 Mac과 iOS에서 Objective-C가 사용 된 것은 OS 자체가 이 언어를 위해 최적화되어 있었기 때문이었다. Swift와 같은 새로운 언어가 등장해도 이 점은 변함이 없다. Mac OS X/iOS에 내장된 프레임 워크 등은 Objective-C 용이며, Swift는 이것을 그대로 사용한다. 즉, 언어로는 새롭겠지만, 실제로 Mac OS X 및 iOS의 기능을 이용할 때는 Objective-C에서 사용하던 기능을 그대로 이용된다는 것이다. 따라서 “새로운 언어를 위한 모든 기능을 처음부터 모두 배우기"라는 것은 없다. 지금까지 Objective-C에서 배운 지식의 대부분은 그대로 활용할 수 있다.

3.1.3 - Swift 입문 | Swift 사용 준비 | Swift 사용하기 위한 준비

그럼 Swift를 사용할 수 있게 해보자. Swift는 애플이 제공하는 개발 환경 “Xocde6"이상에서 지원된다. 이 Xcode6은 2014 년 가을에 출시되었으며, 현재 Xcode7까지 나왔다(2017년 9월 기준).

설치를 하기 위해서는앱스토어를 이용해도 됙, 애플 개발자 사이트에서 다운로드 받아서 설치를 하여도 된다. 단, 애플 개발자 사이트에서 다운 받기 위해서는 Apple Developer Program의 구성원이어야 한다.

https://developer.apple.com/xcode/downloads/

애플 개발자 사이트에서 다운로드 받았다면, 디스크 이미지(.dmg)를 마운트하여 Xcode을 그대로 복사하는 것만으로 설치가 완료된다.

(단, 처음 시작할 때 구성 요소의 기본 작업을 수행하기 때문에, 처음 시작은 시간이 좀 걸린다. 구성 요소 기본 제공에 대한 확인 대화 상자가 나타나면 반드시 설치하도록 한다. 제대로 기동이 되었다면 설치가 완료된다.)

플레이 그라운드를 사용하자

Xcode을 시작하면, 우선 ‘플레이 그라운드’를 만들어 보자. 이것은 Swift 프로그램을 바로 실행할 수 있는 특수 파일이다.

Xcode를 시작할 때 나타나는 Welcome 창에서 “Get started with a playground"항목을 클릭을 한다. 그리고 나타난 대화 상자에서 name에 프로그램(파일)명과 Platform에 “OS X"을 선택한다. 그리고 저장할 폴더를 선택하고 저장하면 플레이 그라운드 윈도우가 나타난다. 이것으로 사용할 수 있게 되었다.

xcode welcome

3.1.4 - Swift 입문 | Swift 사용 준비 | 플레이그라운드 실행

작성된 플레이 그라운드에는 기본적으로 간단한 문장이 적혀 있다다 (아래 소스 코드 참조). 우선 이것의 의미를 이해해 보자.

// Playground - noun: a place where people can play
 
import Cocoa
 
var str = "Hello, playground"

라이브러리 가져오기

import Cocoa

이것은 Cocoa 프레임워크의 라이브러리를 가져 오는 것이다. Mac 또는 iOS는 OS 고유의 기능을 처리하는 ‘Cocoa’라는 프레임 워크가 포함되어 있으며, 이를 통해서 OS에 액세스하고 조작할 수 있다. 이 Cocoa를 프로그램 중에서 사용할 수 있도록 하는 것이, 이 import 문이다. import는 지정된 라이브러리를 로드하여 사용할 수 있도록 하는 것이다.

변수 사용