변수

old/C Grammer 2010. 11. 12. 16:22

1-1. 변수의 정의

변수는 고정되어 있지 않은 수라는 뜻이며 1이나 45 또는 3.14같은 상수의 반대 개념이다. 이름은 변수이지만 반드시 수치값만 저장되는 것은 아니며 문자열이나 포인터같은 좀 더 복잡한 값도 저장될 수 있다. 변수에서 말하는 수(數)를 좀 더 일반적으로 표현하면 데이터(data)이며 한국말로 알아듣기 쉽게 설명하면 값이다.

80386이상의 CPU는 최대 4G까지의 메모리를 장착할 수 있다. 40억개나 되는 이런 기억 소자들에 대해 이놈, 저놈, 개똥이, 말똥이 같은 이름을 일일이 붙여줄 수는 없다. 그래서 컴퓨터는 연산 대상 메모리의 위치를 구분하기 위해 숫자로 된 번지(Address)라는 개념을 사용한다. 메모리를 구성하는 각 바이트들은 0부터 시작해서 40억까지의 고유한 번지를 가지고 있으며 이 번지를 대상으로 값을 읽거나 쓴다. 예를 들어 "1234번지에서 1237번지까지의 4바이트에 56을 기록한다" 는 식으로 동작한다.

그런데 번지라는 것은 사람의 입장에서 보면 굉장히 다루기 힘든 형태로 되어 있다. 번지는 값이 크기 때문에 통상 8자리의 16진수로 표현하는데 사람들이 흔히 쓰는 10진수를 쓰면 자리수가 가변적이어서 오히려 더 혼란스럽다. 16진수는 각 자리수의 형태가 비슷 비슷하기 때문에 사람이 그 위치를 정확하게 기억하기 힘들다. 예를 들어 어떤 값이 0x183c7eda번지에 저장되어 있다면 사람이 이 위치를 암기하여 사용하는 것은 거의 불가능하다. 이런 값을 10개만 사용해도 사람의 머리로는 각 값을 구분할 수 없다. 즉 번지라는 것은 전혀 인간적이지 않은 기계의 값이다.

그래서 번지를 직접 사용하는 대신 좀 더 기억하기 쉬운 변수를 사용한다. 숫자로 된 번지에 별도의 이름을 붙여 놓은 것이 바로 변수이다.



이렇게 이름을 붙여 놓으면 Num=56같이 간단하게 값을 기록할 수 있고 Result=Num같이 값을 읽기도 쉬워진다. 만약 변수라는 것이 없다면 두 값을 더해 다른 변수에 대입하고자 할 때 다음과 같은 식을 사용해야 할 것이다. 다음 표현식에서 [ ] 기호는 이 번지에 들어있는 값을 의미한다.

 

[0x3e98234a] = [0x3eff81a0] + [0x3d00aef6]

 

이런 숫자, 그것도 16진수의 나열보다는 salary=pay+bonus가 사람에게 훨씬 더 쉬우며 변수의 이름이 대상을 잘 표현할수록 더욱 쉬워진다. 컴파일러는 변수가 실제 어떤 번지를 가리키는지를 기억하고 있다가 이 변수를 사용하면 실제 번지에 값을 읽거나 쓰는 코드를 대신 작성해 준다. 흔히 변수는 값을 저장하는 상자에 비유되곤 한다. 상자에 물건을 넣어 두듯이 변수를 하나 만들어 놓으면 여기에 값을 저장하거나 다시 꺼내올 수 있다.

변수는 C언어의 구성 요소 중에 명칭으로 분류된다. 그래서 명칭을 작성하는 일반적인 규칙대로 자유롭게 작성할 수 있다. 키워드는 쓸 수 없고 영문, 숫자, 밑줄 문자로만 구성되어야 하며 대소문자를 구분한다. 이런 문법적인 제약 외에도 변수명을 작성하는 일반적인 몇 가지 법칙이 있다.

 

 의미를 잘 설명할 수 있는 이름을 주는 것이 좋다. 사람의 이름을 기억한다면 Name이라는 이름을 쓰는 것이 좋고 평균값을 기억하는 변수에는 Average, 위치를 기억한다면 Position 같은 이름을 준다. a, b, ttt 같은 의미없는 이름을 쓰는 것도 물론 가능은 하지만 변수의 수가 많아지면 변수끼리 구분하기 어려워진다.

 변수명의 길이에는 제약이 없지만 3~10자 내외로 작성하는 것이 좋다. Num, TotalScore, NowStage 등이 좋은 예이다. Highest_Score_Of_Today와 같이 변수명을 길게 쓰면 의미를 분명하게 나타낼 수는 있지만 변수명을 기억하기 어려워지고 오타를 입력할 가능성이 많아져 바람직하지 않다.

 대소문자 구성을 일관되게 하는 것이 좋다. C언어는 대소문자를 구분하므로 Score와 score는 다른 변수이다. 모두 소문자로 쓰든지 아니면 첫 문자만 대문자로 쓰든지 자신만의 규칙을 정하고 그대로 따르는 것이 좋다. 선언할 때는 Score라고 해 놓고 쓸 때는 score라고 쓰면 이 변수는 선언되지 않은 것으로 에러 처리된다.

 변수명은 보통 짧은 영어 단어를 활용한다. 한글 변수명은 사용할 수 없지만 원한다면 한글같은 변수명을 사용할 수는 있다. JumSoo, Saram, Juso 같은 식으로 작성해도 된다. 좀 어색하기는 하지만 영어 실력이 부족해서 이런 변수명이 더 기억하기 쉽다면 그렇게 하는 편이 좋다. 다른 변수와 구분 가능하고 의미를 쉽게 알 수 있으므로 명칭의 요건은 제대로 만족하는 것이다.

 

처음 실습을 할 때는 편의상 a, b, i같은 짧은 변수명을 즐겨 사용하지만 실무에서는 적절한 변수명을 붙이는 것이 아주 중요하다. 좀 쓸만한 프로그램을 만들어 볼려면 수백개나 되는 변수가 필요한데 이 변수들에 아무렇게나 이름을 붙여 놓으면 정말 힘들어진다. 더구나 여럿이 같이 하는 팀 프로젝트의 경우 내맘대로 성의없이 이름을 붙이면 팀원들에게 왕따를 당하는 수가 있으며 시간이 지난 후 자기 스스로도 코드를 파악하기 어려워진다. 간단 명료한 이름을 붙이는 것은 생각보다 훨씬 더 중요하며 또한 어려운 기술이기도 하다. 



1-2. 변수의 종

변수는 메모리의 위치를 기억한다고 했다. 그러나 실제로 컴파일러가 변수를 참조할 때는 메모리 번지를 참조하는 것이 아니라 번지에 기억된 값을 참조한다. 예를 들어 Num=56이라는 대입문이 있다고 하자. 컴파일러는 이 명령을 다음 둘 중 어떤 것으로 해석할까?

 

 Num이 가리키는 번지를 56으로 바꾸어라.

 Num이 가리키는 번지에 들어있는 값을 56으로 바꾸어라.

 

상식적으로 생각해 봐도 후자가 맞다. 변수는 내부적으로 번지에 대한 위치를 가리키지만 컴파일러는 변수를 번지에 기억된 값으로 참조한다. 즉, 변수의 실체는 번지가 아니라 그 번지에 기록된 값이다. 컴파일러가 변수를 통해 값을 참조하기 위해서는 단순히 메모리 위치만 알아서는 안된다. Num이 1234번지를 가리키고 있다고 할 때 "Num을 읽어라"는 명령을 내리면 1234번지부터 어디까지를 읽어야 하는 것인지를 알아야 하므로Num의 길이가 필요하다.

또한 읽은 값을 어떻게 해석할 것인가도 알아야 한다. 메모리에는 0과 1만 나열되어 있으므로 위치와 길이 정보만으로는 값의 형태를 파악할 수 없다. 1234번지에 들어있는 값이 정수인지 실수인지 또는 구조체인지 등의 정보가 있어야 컴파일러가 이 값을 정확하게 해석할 수 있다. 정수와 실수는 값의 형태가 다르며 메모리에 기록되는 방식도 당연히 다르다. 그래서 변수로부터 값을 정확하게 읽으려면 변수의 형태에 대해서도 알아야 한다.


변수는 저장된 메모리 위치와 함께 길이와 형태에 대한 정보를 가지는데 이런 변수의 특성을 타입(Type)이라고 한다. 타입이란 컴파일러가 변수를 읽고 쓰는 방법에 대한 정보이며 타입에 따라 Num은 정수형 변수이고 Average는 실수형 변수임을 알 수 있게 된다. C언어는 아주 많은 타입을 지원한다. 일단 어떤 타입들이 있는지 정리해 보자. 크게 기본형과 유도형으로 나누어진다.

 

구분

타입

설명

기본형

정수형

정수

문자형

문자

실수형

실수

열거형

가능한 값들에 대한 나열형

void

타입이 정해지지 않은 자료형

유도형

배열

같은 타입의 자료 집합

구조체

다른 타입의 자료 집합

공용체

메모리를 공유하는 자료 집합

포인터

대상체의 번지를 가리키는 타입

함수형

함수의 번지를 가리키는 타입

 

기본형이란 하나의 단일 값을 기억하는 단순한 타입이다. 정수값 하나를 기억하는 정수형이 가장 직관적으로 이해하기 쉽고 사용 빈도도 높다. 정수형 변수는 1234나 38같은 정수값 하나를 저장할 수 있고 실수형 변수는 3.14나 1.5 같은 실수값 하나를 저장할 수 있다. 기본형은 전부 수치를 저장한다는 점에서 공통적이다.

유도형은 기본형으로부터 만들어지는 타입이다. 같은 종류의 변수 여러 개를 모으면 배열이 되고 다른 종류의 변수 여러 개를 모으면 구조체가 된다. 유도형은 다소 복잡하기 때문에 이 장에서는 소개만 하고 관련 장에서 다시 상세하게 다룰 것이다. 각 타입별로 한 장 이상씩을 차지할 정도로 유도형은 복잡하다.


1-2. 변수의 선언
 

변수를 사용하려면 먼저 선언해야 한다. 선언(Declaration)이란 컴파일러에게 앞으로 이런 이런 이름을 가진 어떤 타입의 변수를 사용하겠으니 준비해 달라고 부탁하는 것이다. 예를 들어 Num이라는 이름으로 정수형 변수를 선언하면 컴파일러는 이 변수를 위해 정수형 하나를 저장할 수 있는 메모리 4바이트를 할당한다. 앞으로 이 메모리에 들어있는 값은Num이라는 변수 이름으로 참조하면 된다. 변수를 선언하는 기본 형식은 다음과 같다.

 

타입 변수명[=초기값][,변수명변수명, ...];

 

기본 형식에서 [ ] 괄호안에 있는 것은 모두 생략 가능하다는 뜻이며 ... 표기는 임의 개수만큼 반복될 수 있다는 뜻이다. 반드시 필요한 것은 타입과 변수명, 그리고 제일 끝에 있는 세미콜론밖에 없다. 변수 선언문도 문장이므로 끝에 세미콜론은 반드시 있어야 한다. 정수형 변수 Num을 선언하고 싶다면 다음과 같이 선언문을 작성한다.

 

int Num;

 

잠시 후에 다시 배우겠지만 정수형 타입은 int키워드로 표현한다. 이렇게 선언만 하면 Num을 위해 메모리가 할당된다. 그러나 할당만 되며 값은 초기화되지 않는데 C는 속도를 위해 특별한 지정이 없으면 변수값을 초기화하지 않는다. 성능을 최우선으로 하는 C의 특징을 잘 보여주는 대목이다. 이렇게 되면 변수는 할당된 번지에 원래부터 들어 있던 의미없는 값을 가지게 되며 이 값을 쓰레기값(Garbage)이라고 한다. 만약 선언과 동시에 어떤 값으로 초기화하고 싶다면 변수명 다음에 = 구두점을 쓰고 원하는 초기값을 뒤에 쓰면 된다.

 

int Num=12;

 

Num 변수가 선언됨과 동시에 이 변수가 12라는 값을 가지도록 초기화된다. C는 성능을 최우선으로 하기 때문에 명시적인 지정이 없으면 별다른 동작을 하지 않는다. 초기값이 필요없으면 선언만 하고 필요할 때만 초기값을 지정한다. 여러 개의 변수를 같은 타입으로 한꺼번에 선언할 수도 있는데 콤마로 구분된 변수명을 계속 나열하면 된다. 이때도 = 구분자로 그 중 일부를 초기화할 수 있다.

 

int a,b=3,c,d;

 

4개의 정수형 변수가 선언되며 이 중 b는 3으로 초기화된다. 물론 다음처럼 각각의 변수 선언을 별도의 명령으로 따로 선언해도 상관없다. 효과는 동일하지만 줄 수가 길어진다.

 

int a;

int b=3;

int c;

int d;

 

다른 타입의 변수는 한꺼번에 같이 선언할 수 없으며 각각 따로 선언해야 한다. 다음은 정수형 변수 두 개와 실수형 변수 하나를 선언한 것이다.

 

int angle, radius;

double ratio;

 

변수는 필요한만큼 얼마든지 선언할 수 있다. 앞장에서 실습했던 삼각형 그리기 예제를 보면 두 개의 정수형 변수 i와 j를 선언하고 있다.






3. 정수형

2-1. 정의

정수(Integer)란 부호는 있지만 소수점 이하를 표현하지 못하는 수이다. 0, -23, 156 이런 값들은 정수이며 1.28, 25.4 이런 값은 소수점 이하가 있으므로 정수가 아니다. 정수의 정의는 중학교 수학 수준에서 설명되는 것이므로 더 상세한 설명이 필요하지는 않을 것이다. 단, 컴퓨터의 메모리는 유한하기 때문에 수학적 정의와 같은 무한대의 범위를 지원하지 않는다는 정도만 다르다.

정수형이란 이런 정수값을 저장할 수 있는 타입이다. 컴퓨터라는 존재가 원래 정수적인 존재이고 실생활에서 가장 많이 사용되는 수이기 때문에 정수형 타입이 가장 흔하게 사용된다. 정수형 변수의 타입 이름은 Integer의 앞 세 자를 딴 int이다. 따라서 정수형 변수 i를 선언하려면 다음과 같은 선언문을 사용한다.

 

int i;

 

키워드 int 다음에 원하는 변수 이름 그리고 세미콜론으로 구성되어 있다. 컴파일러는 이 선언문을 만났을 때 정수값을 저장할만한 4바이트의 공간을 할당하고 이 공간에 대해 i라는 이름을 붙여줄 것이다. 이후 i라는 변수명을 통해 이 메모리에 정수값을 저장할 수 있고 또 값을 읽을 수도 있다. 정수형은 최대 표현 가능한 값의 크기와 부호의 존재 유무에 따라 여러 가지 종류로 나누어진다.

먼저 변수의 크기와 표현 가지수의 관계에 대해 알아보자. 값 하나를 표현하기 위해 몇 비트를 사용할 것인가에 따라 표현 가능한 수의 개수가 달라진다. 예를 들어 1비트로만 구성된 정수형이 있다면 이 정수로는 0과 1의 두 가지 상태밖에 기억하지 못한다. 이런 비트가 두 개 모이면 00, 01, 10, 11 네 가지 각각 다른 상태를 표현할 수 있다. 비트 세 개가 모인다면 각 비트값의 조합에 따라 다음 8가지 상태를 표현할 수 있다.

 

이진수

십진수

000

0

001

1

010

2

011

3

100

4

101

5

110

6

111

7

 

같은 원리로 비트가 4개 모이면 16가지 상태를 표현할 수 있을 것이다. 일반적으로 n개의 비트가 모이면 2n가지의 수를 표현할 수 있으며 0부터 시작하므로 최대 표현 가능한 수는 2n-1이 된다. 8비트로 구성되는 1바이트는 총 256가지 종류의 수를 표현할 수 있고 표현 가능한 최대 수는 255가 되어 0~255까지의 정수를 기억할 수 있다. 2바이트(16비트)라면 216종류의 값을 기억할 수 있고 4바이트(32비트)라면 232 종류의 값을 기억할 수 있을 것이다.

다음은 부호 여부에 따른 표현 범위의 차이를 보자. 부호가 있는 정수(signed)는 제일 왼쪽의 비트(MSB라고 한다)를 부호 비트로 사용하며 이 비트가 0이면 양수이고 1이면 음수가 된다. MSB를 부호 비트로 사용하면 값을 기억하는 비트 하나가 줄어들게 되므로 표현할 수 있는 최대값은 절반으로 줄어드는 대신 음의 값을 표현할 수 있다. 값의 범위가 음수 영역으로 평행이동하는 것이다.

8비트 정수의 경우 총 256가지의 수를 표현할 수 있는데 부호가 없으면 0~255까지의 표현 범위를 가진다. 부호가 있다면 음수 범위로 절반이 이동하여 -128 ~ 127까지의 수를 표현할 수 있을 것이다. 16비트 정수는 부호가 없을 때 0~65535까지, 부호가 있으면 -32768~32767까지를 표현한다. 결국 표현할 수 있는 가지수는 같지만 부호 여부에 따라 범위만 달라지는 것이다.

int라는 정수형의 타입앞에 크기를 나타내는 short, long같은 수식어와 부호를 나타내는 signed, unsigned같은 수식어가 붙어 다음과 같은 다양한 정수형 타입을 선언할 수 있다.

 

타입

크기(바이트)

부호

범위

int

4

있음

-2147483648~2147483647

short int

2

있음

-32768~32767

long int

4

있음

-2147483648~2147483647

unsigned int

4

없음

0~4294967295

unsigned short int

2

없음

0~65535

 

도표를 보면 각 타입별로 할당된 바이트 수와 부호 여부가 다른데 이 크기와 부호 여부에 따라 표현 가능한 수의 범위가 달라진다. 4바이트 크기의 unsigned int는 최대 42억이라는 큰 값을 기억할 수 있는데 비해 2바이트 크기의 unsigned short int는 65535까지만 기억할 수 있다. int형은 부호가 있고 4바이트의 크기를 가지므로 -231~231-1까지의 범위를 가지는데 비해 unsigned형은 같은 4바이트이지만 부호가 없기 때문에 음수를 표현할 수 없는 대신 0~232-1까지 표현할 수 있다. 타입의 이름이 좀 긴데 다음 두 가지 규칙에 의해 좀 더 간략하게 표현할 수도 있다.

 

① 부호에 대한 수식어가 생략되면 signed가 적용되어 부호가 있는 것으로 선언된다. signed int는 int와 같고 signed short int는 short int와 같다. 그래서 signed는 보통 붙이지 않는다.

② int앞에 수식어가 있을 경우 int는 생략할 수 있다. 그래서 unsigned int는 unsigned로 간단하게 쓸 수 있으며 long int는 long과 같다. 부호있는 4바이트 정수형은 signed int라고 쓰는 것이 원칙이나 signed를 생략하고 int로 쓸 수도 있고 int를 생략하고 signed로 쓸 수도 있다. 그러나 통상 signed라고 쓰지 않고 int라고 간략하게 쓰는 것이 보통이다.

 

똑같은 정수형에 대해서도 다양한 타입이 준비되어 있는데 이는 상황에 따라 가장 적절한 타입을 선택해서 쓸 수 있도록 하기 위해서이다. 년도나 온도를 기억한다면 2바이트의 short형으로도 충분하므로 굳이 기억 장소를 낭비해 가면서 4바이트나 차지하는 int형을 쓸 필요가 없다. 또한 나이나 성적같이 음수값이 존재하지 않는다면 부호가 없어도 상관없으므로 unsigned형을 쓰는 것이 더 좋다.

 

2-2. 정수형의 길이

C의 정수형, 더 정확하게 말해서 컴퓨터가 표현하는 정수라는 개념은 수학에서 말하는 정수와는 의미가 약간 다르다. 수학의 정수는 음양으로 무한대의 값을 표현할 수 있지만 유한한 메모리를 가진 컴퓨터는 이런 무한한 값을 표현하지 못하며 자신에게 할당된 메모리 양만큼의 값만 기억할 수 있다. 그래서 가끔 연산 결과가 용량을 넘어서는 경우가 발생하기도 하는데 다음 예제를 실행해 보자.

[overflow]

정수형 변수로 간단한 덧셈, 뺄셈을 해 보았는데 실행 결과는 다음과 같다.

 

20000+30000=-15536

20000-30000=55536

 

세 개의 2바이트 정수(short) a, b, c를 선언하고 a에 20000, b에 30000을 대입한 후 이 두값을 + 연산자로 더해 c에 대입했다. 그러면 c는 당연히 50000이라는 값을 가져야겠지만 실제 결과는 엉뚱하게도 -15536으로 출력된다. 왜냐하면 a, b, c 변수는 부호있는 2바이트의 정수형인 short형으로 선언되었고 최대 32767이상의 수를 저장할 수 없기 때문이다. 50000이라는 값이 대입되기는 하지만 short형은 최상위 비트를 부호 비트로 해석하기 때문에 음수가 되어 버리는 것이다. 이런 식으로 변수의 저장 용량을 넘어서는 현상을 오버플로우(Overflow)라고 한다.

이런 문제가 발생한 근본적인 원인은 만단위의 수치를 저장하는데 short형을 사용했다는데 있다. a,b,c를 unsigned short형으로만 바꾸어도 위 예제는 제대로 실행된다. 그러나 그렇게 하더라도 65535이상의 수를 저장할 수는 없다. 더 큰 수를 다루려면 int나 unsigned같은 4바이트의 더 큰 타입을 사용해야 한다. int는 20억 정도의 큰 수치를 저장할 수 있으므로 일반적으로 오버플로우 걱정을 하지 않아도 된다.

변수의 표현 범위를 초과하는 현상과 반대로 최소 표현수에 미치지 못하는 경우도 발생할 수 있다. s, t, u는 모두 부호를 표현하지 못하는 unsigned short로 선언되었으며 20000이라는 값을 가지는 s에서 30000이라는 값을 가지는 t를 빼서 u에 대입했다. u에 대입되는 값은 -10000이 아니라 55536이라는 양수값이 되어 버린다. unsigned short형이 표현할 수 있는 최소수는 0인데 이 값보다 더 작은 값을 대입했으므로 계산 결과가 틀려지는 것이다.

수학적인 연산을 할 때는 항상 이 점을 주의해야 한다. 아주 간단할 것 같은 연산도 정확한 타입과 함께 사용해야만 결과가 제대로 나온다. 메모리가 지극히 부족한 상황이 아닌 한은 정수가 필요할 때 부호 있는 4바이트 정수인 int를 사용하면 별 문제가 없다. int는 음양으로 20억이라는 실생활에서 거의 부족하지 않는 정도의 표현 범위를 가지고 있기 때문이다.

정수형 타입의 도표를 보면 int와 long은 크기나 부호 여부가 동일하며 따라서 표현할 수 있는 수의 범위도 완전히 동일하다. 왜 똑같은 타입을 둘 씩이나 정의해 놓았는지 의아하겠지만 이 둘은 엄밀하게 말하면 다른 타입이다. 아니, 다른 타입이라기 보다는 달라질 수 있는 타입이라고 하는 편이 옳을 것 같다.

C 언어의 타입 정의에 int 형은 "CPU의 레지스터와 동일한 크기를 가지는 타입"으로 정의되어 있다. 레지스터란 CPU내의 임시 기억 장소이며 레지스터의 크기에 따라 CPU의 비트 수를 정의한다. 즉, 레지스터가 16비트이면 16비트 컴퓨터, 32비트이면 32비트 컴퓨터라고 부른다. 비트 수가 높으면 높을수록 CPU가 한 번에 처리할 수 있는 자료양이 많아지므로 더 성능이 높다고 할 수 있다.

, int형은 CPU가 가장 효율적으로 다룰 수 있는 정수형으로 정의되어 있으며 그래서 int형의 실제 크기는 플랫폼에 따라 달라진다. 다음에 알아볼 포인터형도 마찬가지이다. 과거 8086이나 80286같은 16비트 CPU 시절, 윈도우즈 3.1같은 16비트 운영체제에서 int는 16비트였었다. 그러나 386이후의 CPU와 윈도우즈 95이후의 32비트 운영체제에서 int는 32비트이다. 64비트 CPU가 나오면(이미 나와 있다) 그때는 int형이 64비트(8바이트)가 될 것이다.

반면 long형은 그 크기가 4바이트로 고정되어 있어 어떤 플랫폼에서나 4바이트이다. int와 long이 동일한 크기를 가지는 것은 32비트 플랫폼에서 뿐이며 16비트에서는 서로 다른 타입이고 64비트에서도 달라질 것이다. 꼭 4바이트를 쓰고 싶으면 long형으로 선언하고 플랫폼의 환경에 따라 적절한 크기를 자동으로 선택하고 싶다면 int형으로 선언하면 된다.

최근 64비트 CPU가 발표되고 점점 더 큰 수를 다룰 일들이 많아지면서부터 C언어도 64비트의 정수를 지원하기 시작했다. 비주얼 C++과 Dev-C++은 __int64라는 타입을 지원하며 이 타입을 사용하면 무려 1800경(264)이라는 엄청난 수를 표현할 수 있다. 다음은 64비트 정수를 사용하여 억단위의 정수끼리 곱해본 것이다.

[int64]

흔히 전자 계산기를 테스트하기 위해 일련의 1을 곱해보는데 12345678987654321이라는 결과가 나오면 제대로 동작하는 것이다. printf로 64비트 정수를 출력하려면 %I64d라는 서식을 사용한다.


2-3. 정수형 상수

정수형 상수를 표기하는 방법은 아주 쉽고 상식적이다. 아라비아 숫자와 부호로 직접 그 값을 표기하면 된다. 다음이 정수형 상수의 예이다.

 

123

8906299

-86400

 

이렇게 값을 바로 표기하면 그 크기와 형태를 보고 적당한 타입으로 메모리에 저장된다. 위 예의 경우 차례대로 short, int, int의 타입을 가진다. 123은 2바이트로 표현할 수 있으므로 short형이면 충분하고 8906299는 2바이트의 범위를 넘으므로 int형이 되어야 한다. 만약 크기를 강제로 지정하고 싶으면 상수 뒤에 L(Long, 소문자로 써도 됨)을 붙이고, 부호없는 타입으로 지정하고 싶으면 U(Unsigned, 소문자로 써도 됨)을 붙인다.

예를 들어 28은 short형이지만 28L로 표기하면 4바이트의 long형 상수가 되고 123U는 부호없는 2바이트의 정수인 unsigned short가 된다. 만약 부호없는 4바이트의 상수로 표기하고 싶으면 순서나 대소문자에 상관없이 UL, LU, ul, lu 중 하나를 붙이면 된다. C/C++언어는 타입을 중요하게 생각하므로 상수에도 정확한 타입을 지정할 수 있도록 되어 있다. 정수형 상수를 별도의 표기없이 그냥 쓰면 10진수로 해석된다. 그러나 진법에 따라 다음 두가지 형식으로 상수를 표현할 수도 있다.

 

 8진수 : 0으로 시작하면 8진수로 인식되며 027, 032등과 같이 표현한다. 09같은 상수는 에러로 처리되는데 9라는 숫자는 8진수에서 쓸 수 없는 숫자다.

 16진수 : 0x 또는 0X로 시작하면 16진수이다. 0x12ab, 0x3f와 같이 표현한다. 16진수에서 10 이상의 값을 표현하는 A~F는 대소문자에 상관없이 아무 문자나 사용할 수 있다. 접두로 붙는 0x는 알파벳의 "오엑스"가 아니라 숫자 0과 알파벳 x이므로 "영엑스" 또는 "공엑스"로 읽는다.

 

보편적으로 10진수를 사용하지만 어셈블리와 관계있는 값이나 비트별로 의미가 다른 상수는 16진수로 표기하면 더 편리한 경우가 많다. 아쉽게도 C는 2진 상수 표기법은 제공하지 않으므로 2진수는 16진수로 바꾸어 표기해야 한다. 예를 들어 2진수 10100110은 16진수 0xa6으로 표기한다. 사실 8진 표기보다는 2진 표기법이 더 많이 사용되는데 베이직도 지원하는 2진 표기법이 C문법에 빠진 것은 데니스 리치의 실수가 아닌가 생각된다. 실제로 표준 위원회에 2진 표기법을 도입하자는 건의가 여러 차례 있었으나 익숙해지면 16진수로 암산 가능하다는 이유로 표준에서 제외됐다.

C에서 0으로 시작하는 상수는 8진수라는 것은 잘 알아 두어야 한다. 그렇지 않으면 다음과 같은 엉뚱한 실수를 할 수도 있다. HPNum에 10진수로 17을 대입하고자 했는데 전혀 문제가 없는 문장이기 때문에 컴파일러는 어떠한 에러 메시지도 출력하지 않는다.

 

int HPNum;

HPNum=017;

 

이렇게 하면 상수 017이 8진수로 해석되어 Num에 17이 대입되는 것이 아니라 15가 대입될 것이다. C 컴파일러는 017과 17을 엄연히 다른 상수로 취급하는데 0으로 시작하는 상수는 8진수라는 것을 잘 기억해 두도록 하자.

참고로 진법이란 수치값을 표기하는 다른 방법일 뿐이지 표현하는 값 자체가 다른 것은 아니다. 012나 10이나 0xa나 모두 십진수로 10(이진수로는 1010)을 나타내며 메모리에 기록될 때는 똑같은 값이다. 따라서 "10진수 값을 어떻게 16진수로 바꾸나요?" 이런 질문은 잘못된 것이다. 10진수를 16진수 형태로 출력한다거나 문자열 형태로 저장된 16진수를 10진값으로 구하는 방법을 질문하는 것은 옳지만 말이다.



4. 실수형

4-1. 종류
 

실수(Real Number)란 소수점 이하를 가지는 수이며 정수보다는 한 단계 더 확장된 범위를 포괄한다. 3.14나 57.4같이 정수부 다음에 소수점과 소수 이하의 소수부가 있다. 실수형 타입은 이런 실수를 저장하는 타입이며 C에서는 크기별로 다음 세 가지 종류가 제공된다. 실수 타입은 모두 부호를 가지고 있다.

 

타입

바이트 

범위

유효자리수(십진)

float

4

3.4*10-38~3.4*1038

7

double

8

1.7*10-308~1.7*10308

15

long double

10~16

1.2*10-4932~3.4*104932

19

 

float는 4바이트의 작은 실수형이며 double은 8바이트의 큰 실수형이다. 실수형의 값을 기억할 변수가 필요하다면 double d; 형식으로 선언하면 된다. long double형은 C언어 표준에는 있지만 비주얼 C++은 6.0과 7.0 모두 이 타입을 지원하지 않으며 double형과 동일하게 취급한다. gcc는 12바이트 크기의 long double형을 지원한다. 정수와 마찬가지로 실수도 수학에서의 실수와는 달리 무한대의 크기와 정밀도를 제공하지는 않으며 할당된 메모리 크기만큼의 크기와 정밀도만 표현한다.

컴퓨터는 원래 정수만 다룰 수 있기 때문에 실수를 기억하는 방법이 아주 독특하다. 만약 4바이트를 2바이트씩 나누어 정수부와 소수부를 따로 저장한다고 해 보자. 이렇게 단순한 방법을 사용하면 32767.00000 보다 더 큰 수를 표현하지도 못할 뿐더러 소수부도 기껏해야 소수점 이하 다섯 자리도 채 표현하지 못할 것이다. 그래서 실수는 부동 소수점이라는 좀 특이한 방법으로 저장한다. 부동(浮動) 소수점이란 실수를 정수부와 소수부로 나누는 것이 아니라 지수부와 가수부로 나누어 기억하는 방식이다.

가수부는 값의 모양을 표현하며 지수부는 10의 거듭승으로 값의 크기를 표현한다. 실수 123.456을 부동 소수점 형식으로 표현하면 1.23456*102으로 표현할 수 있으며 이를 공학적 표기법으로 바꾸어 1.23456E2로 표현하기도 한다. 이 예에서 가수는 123456이고 지수는 2이다. 정수부와 소수부를 기억하는 방식보다 부동 소수점 방식으로 실수를 기억하면 훨씬 더 큰 수를 표현할 수 있고 정밀도도 높아진다.

제일 왼쪽 비트(MSB)는 항상 부호 비트이며 이 비트가 0이면 양수, 1이면 음수이다. 지수부와 가수부의 크기는 float의 경우 8비트, 23비트이며 double형의 경우 11비트 52비트이다. 그래서 float보다는 double이 두 배의 크기를 가지는 대신 훨씬 더 큰 수를 정확하게 표현할 수 있다.

이런 실수 표현법은 C언어의 고유한 방식이 아니라 IEEE에서 제정한 국제 표준이며 이 표준은 모든 언어가 공통적으로 따르고 있다. 부동 소수점 수의 정확한 구조는 위에서 대충 설명한 것보다 조금 더 복잡한데 필요하다면 별도의 자료를 찾아 보기 바란다. 이 책의 18장에서 실수의 구조에 대해 대략적인 설명을 하고 있다.


4-2. 실수형 상수

소수부를 가지면 실수형 상수로 인식된다. 다음이 실수형 상수의 예이며 실생활에서 직접 사용하는 표기법이므로 전혀 어렵지 않을 것이다.

 

-3.14

123.456

5.0

 

5.0의 경우 같은 값이라도 5라고 적으면 정수형 상수가 된다. 실수형 상수임을 명확히 나타내려면 5.0이라고 적든가 아니면 0은 생략하고 5.이라고 적어야 한다. 이런 식으로 소수점을 기준으로 왼쪽에 정수부 오른쪽에 소수부를 적는 방법을 고정 소수점 표기법이라 한다. 실수형 상수 표기에 부동 소수점 표기법을 사용할 수도 있다.

 

3.14e-1

1.23456e2

 

e를 기준으로 왼쪽에 가수 오른쪽에 지수를 적는다. 실수는 내부적으로 모두 부동 소수점 방식으로 기억되지만 상수를 표현할 때는 고정 소수점, 부동 소수점 표기법을 모두 사용할 수 있다. 정수 상수 뒤에 크기와 부호를 나타내는 U, L 등의 접미사를 붙일 수 있듯이 실수형 상수 뒤에도 정확한 크기를 명시하는 F 접미사(소문자도 가능)를 붙일 수 있다. 접미사가 없으면 double형 상수로 인식되며 F를 붙이면 float형으로 인식되고 L을 붙이면 long double형이 된다. 다음 예제는 실수형 값을 출력하는 여러 가지 방법을 보여준다.


[float]

실수형 변수 d를 선언하고 이 변수에 123.456이라는 실수 상수를 대입했다. d=1.23456e2;와 같이 부동 소수점 표기법으로 대입해도 결과는 같다. 실수를 표현하는 방법이 여러 가지가 있기 때문에 printf의 실수에 대응되는 서식도 여러 가지가 있다. %f 서식은 고정 소수점으로 실수를 출력하며 %e 서식은 부동 소수점으로 출력한다. %g 서식은 %f와 %e 중 더 짧고 간단한 방법을 자동으로 선택한다. 실행 결과는 다음과 같다.

 

고정 소수점 : 123.456000

부동 소수점 : 1.234560e+002

일반형 : 123.456

 

앞 절에서 배운대로 %와 서식 사이에 총 자리수나 소수점 이하 자리수를 지정할 수도 있다. 소수점 이후 두 자리까지만 출력하고 싶다면 %f 대신 %.2f 서식을 사용하면 된다. 이 경우 소수점 이하 3번째 자리에서 반올림되어 123.46이 출력될 것이다.

실수는 정수에 비해 소수점 이하를 표현할 수 있고 천문학적인 큰 수를 다룰 수 있는 장점이 있지만 구조가 복잡하기 때문에 굉장히 느리다. 요즘 CPU는 부동 소수점을 보조 프로세서가 직접 처리하므로 훨씬 더 빨라졌지만 그렇지 못한 시스템에서는 정수에 비해 대략 10배 정도 느리다. 그래서 꼭 필요한 경우가 아니면 잘 사용되지 않으며 불가피한 경우라도 정수형으로 바꾸어서 다루는 경우가 많다. 예를 들어 소수점 이하 두 자리까지의 정확도를 가지는 백분율이 필요하다면 아예 100을 곱해 만분율을 사용하는 것이 더 유리하다.



5.  문자형
 

5-1. 문자

문자형이란 문자 하나를 표현하는 자료형이다. 컴퓨터는 원래 숫자밖에 모르기 때문에 문자도 숫자로 기억한다. 이때 어떤 숫자와 어떤 문자를 대응시키는가에 따라 여러 가지 인코딩 방식이 있는데 통상 아스키(ASCII) 코드 방식을 많이 사용한다. 아스키 코드는 0~255사이의 숫자에 문자를 대응시켜 놓았는데 이 표를 보면 대문자 A는 문자 코드 65(0x41)로 표현하며 숫자 1은 49(0x31)로 표현한다.

 

10

16

문자

10

16

문자

10

16

문자

10

16

문자

10

16

문자

0

0

Null

47

2F

/

68

44

D

89

59

Y

110

6E

n

7

7

Bell

48

30

0

69

45

E

90

5A

Z

111

6F

o

8

8

BS

49

31

1

70

46

F

91

5B

[

112

70

p

9

9

Tab

50

32

2

71

47

G

92

5C

\

113

71

q

10

A

LF

51

33

3

72

48

H

93

5D

]

114

72

r

13

D

CR

52

34

4

73

49

I

94

5E

^

115

73

s

32

20

공백

53

35

5

74

4A

J

95

5F

_

116

74

t

33

21

!

54

36

6

75

4B

K

96

60

`

117

75

u

34

22

"

55

37

7

76

4C

L

97

61

a

118

76

v

35

23

#

56

38

8

77

4D

M

98

62

b

119

77

w

36

24

$

57

39

9

78

4E

N

99

63

c

120

78

x

37

25

%

58

3A

:

79

4F

O

100

64

d

121

79

y

38

26

&

59

3B

;

80

50

P

101

65

e

122

7A

z

39

27

'

60

3C

<

81

51

Q

102

66

f

123

7B

{

40

28

(

61

3D

=

82

52

R

103

67

g

124

7C

|

41

29

)

62

3E

>

83

53

S

104

68

h

125

7D

}

42

2A

*

63

3F

?

84

54

T

105

69

i

126

7E

~

43

2B

+

64

40

@

85

55

U

106

6A

j

127

7F

Del

44

2C

,

65

41

A

86

56

V

107

6B

k

 

 

 

45

2D

-

66

42

B

87

57

W

108

6C

l

 

 

 

46

2E

.

67

43

C

88

58

X

109

6D

m

 

 

 

 

255개의 서로 다른 문자를 기억하기 위해서는 단지 1 바이트만 있으면 된다. 문자형 타입은 char인데 정수형과 마찬가지로 char형도 앞에 수식어를 붙일 수 있다. 길이는 1바이트로 고정되어 있으므로 long이나 short같은 크기에 대한 수식어는 붙일 수 없고 부호의 여부에 따라 unsigned, signed 수식어만 붙일 수 있다. unsigned char형은 8비트 길이를 가지므로 0~255까지를 표현할 수 있고 signed char 형은 1바이트의 좁은 공간에도 음수 표현을 위해 MSB를 부호 비트로 사용하므로 최대 표현 수가 절반으로 줄어든다.

 

타입

크기(바이트)

부호

범위

signed char

1

있음

-128 ~ 127

unsigned char

1

없음

0 ~ 255

 

부호 수식어없이 char라고만 쓰면 컴파일러와 설정 옵션에 따라 부호가 있을 수도 있고 없을 수도 있다. 대부분의 컴파일러들은 char형을 부호있는 타입으로 인식하므로 signed수식어는 생략할 수 있으며 signed char는 char 타입과 일단 동일하다. 컴파일러의 설정에 상관없이 부호 여부를 정확하게 지정하려면 signed, unsigned 수식어를 명시적으로 붙여야 한다. 문자 상수는 다음과 같이 표기한다.

 

'A', 'Z', '1', '&'

 

홑따옴표로 문자 하나를 감싸 주면 된다. 문자 상수는 아스키 코드값으로 해석되는데 'A'는 A문자의 아스키 코드값인 65와 같다. 다음 예제는 문자형 변수 ch를 선언하고 이 변수에 대문자 A를 대입한 후 출력해 본 것이다.


[char]

ch 변수는 char형으로 선언되었으므로 문자 하나를 저장할 수 있는 1바이트가 할당될 것이다. 이 변수에 'A', 'B' 같은 문자형 상수를 대입할 수 있는데 예제에서는 'A'를 대입하여 ch 변수에 'A' 문자를 기억시켰다. 문자형 상수를 쓰는 대신 ch=65(또는 ch=0x41)와 같이 아스키 코드값을 직접 대입해도 결과는 동일하다. 하지만 누가 보더라도 ch=65; 보다는 ch='A'가 훨씬 더 읽기 쉽다.

문자형 변수를 출력할 때는 putch 함수를 사용한다. 또는 printf 함수의 %c 서식을 사용하여 printf("%c",ch); 로 출력할 수 있다. 예제를 실행해 보면 화면에 A 문자만 하나 출력될 것이다. 한글은 한 음절이 2바이트로 구성되기 때문에 문자형 변수에는 저장할 수 없으며 잠시 후에 배울 문자열을 사용해야 한다.

문자형은 문자 하나를 저장할 수 있는 적당한 길이를 가진다는 뜻으로 붙여진 이름이지 오로지 문자만 저장할 수 있다는 뜻은 아니다. char 타입은 실제로 8비트의 정수형이므로 크기가 작은 정수를 저장하는 용도로도 사용할 수 있다. 예를 들어 0~100까지의 범위를 가지는 점수라면 Score라는 이름으로 char형 변수를 선언한 후 여기에 저장하면 적당하다. 문자형이 일종의 정수형이라면 이 변수에 들어 있는 값은 과연 어떻게 해석될까? 다음 예제를 보자.


[charcontext]

문자열 변수가 문자로 해석될 것인가 정수로 해석될 것인가는 이 변수가 사용되는 위치에 따라 달라지는데 문자가 올 수 있는 곳이면 문자로, 정수가 올 수 있는 곳이면 정수로 해석된다. printf 함수의 %c 서식과 대응되면 문자가 출력될 것이고 %d와 대응되면 정수값이 출력될 것이다. putch(ch)는 ch값을 문자로 해석하며 gotoxh(ch, 8)은 ch를 x 좌표를 나타내는 정수로 해석한다.


5-2. 확장열

문자 상수는 홑따옴표안에 문자를 써서 표기한다. 문자 Y에 대한 문자 상수는 'Y'다. 이 상수를 키보드로 입력하려면 키보드에서 '를 치고 Y를 치고 '를 치면 된다. 그런데 따옴표안에 직접 입력할 수 없는 문자가 있다. 대표적으로 개행 코드를 들 수 있는데 Enter키를 누르는 즉시 정말로 다음 줄로 내려가 버리기 때문에 따옴표안에 개행 코드를 담아서 표현하는 것은 불가능하다.

또한 문자 상수를 표현할 때 사용하는 홑 따옴표 구두점도 문자 상수로 바로 표현할 수 없다. ''' 이렇게 쓰면 두 번째 '가 닫는 따옴표인지 문자 '를 나타내는지 컴파일러가 구분할 수 없다. 그래서 키보드로 직접 입력할 수 없는 문자들은 좀 특수한 방법으로 표현하는데 이를 확장열(Escape Sequence)이라고 한다. 확장열은 백슬레쉬(\) 문자 다음에 기호 하나를 써서 표현한다.

 

확장열

코드

설명

\a

0x07

 소리

\b

0x08

 스페이스

\t

0x09

\n

0x0a

개행

\x##

0x##

16 코드

\###

0###

8 코드

\\

0x5c

백슬레쉬

\'

0x27

홑따옴표

\"

0x22

겹따옴표

\?

0x3f

물음표

 

개행 코드는 \n으로 표현하는데 이 코드는 First예제에서 이미 실습해 보았다. 탭이나 백 스페이스 같은 문자도 직접 키보드로 입력해서는 따옴표안에 표기할 수 없기 때문에 확장열로 표기해야 한다. 홑따옴표 문자 하나는 '''와 같이 표기할 수 없고 확장열을 사용해서 '\''와 같이 표기해야 한다.

확장열 표기에 \문자를 사용하기 때문에 \문자 자체도 확장열이 될 수밖에 없다. 16진 코드는 키보드에 없는 문자에 대해 코드를 직접 쓸 때 사용한다. 'A'는 '\x41'과 동일하다. 16진 코드를 확장열로 표기할 때 \다음의 x는 반드시 소문자로 써야 하며 대문자는 인정하지 않는다. 문자열 상수내에 16진 코드를 직접 쓸 경우 16진수로 인식되는 모든 문자를 확장열로 취급한다는 점을 주의하자. '\x53trike"는 \x53이 s이므로 "strike"이지만 "\53econd"는 \다음의 53ec까지를 16진수로 해석해 버리므로 "second"가 되지 않고 에러로 처리된다. 왜냐하면 53ec는 문자 코드 범위 바깥이므로 하나의 문자가 아니기 때문이다. 이런 경우는 16진 코드를 쓰지 않거나 다른 방법으로 문자열을 표기해야 한다.

확장열을 쓰는 이유는 꼭 키보드로 표현하지 못해서뿐만 아니라 환경에 따라 달라질 수 있는 코드를 논리적으로 표현하기 위해서이다. 개행을 하는 방식은 시스템마다 다른데 윈도우즈에서는 CR, LF의 조합으로, 유닉스는 LF만으로, 매킨토시는 CR만으로 개행 문자를 표현한다. C는 이런 방식에 상관없이 개행을 표현할 수 있는 확장열을 제공하고 프로그래머는 개행이 필요할 때 \n이라고만 적으면 된다.

탭의 경우 일정한 자리를 띄우는 것이 아니라 현재 위치에서 다음 탭 위치로 이동하는 기능을 하므로 소스상에 입력된 탭에 의해 띄워진 빈칸과 실제 출력될 위치의 탭 크기가 다를 수 있다. 그래서 탭키를 문자열 상수에 직접 쓰지 않고 \t 확장열로 표기하여 출력되는 상황에 맞게 탭을 적용하도록 한다.



5-3. 문자열

문자열(String)은 일련의 문자가 연속되어 있는 것이며 문자의 집합이 곧 문자열이다. 사람의 이름이나 에러 메시지 등과 같은 일반적인 문장을 문자열이라고 한다. 문자열 상수는 문자 상수와 달리 겹따옴표로 감싸서 표현한다.

 

"Korea", "문자열"

 

문자 상수는 홑따옴표로 표현하는데 비해 문자열 상수는 겹따옴표를 사용함을 분명히 구분해야 한다. 'S'와 "S"는 비슷해 보여도 내부적으로 엄청난 차이가 있다. 'S'는 S라는 문자 하나만을 표현하지만 "S"는 한 글자로만 된 문자열이다.

문자열 상수는 있지만 이런 문자열을 저장할 수 있는 문자열 타입이라는 것은 없다. 베이직이나 파스칼, 자바같은 언어는 별도의 문자열 타입을 제공하지만 C언어는 별도의 문자열 타입을 제공하지 않는다. 왜냐하면 문자열이란 문자형 변수의 배열로 표현할 수 있고 포인터와 함께 사용하면 훨씬 더 유연하게 활용할 수 있기 때문이다.

그러나 처음 배우는 사람에게 문자열 변수가 없다는 것은 상당히 불편한 점이다. 그래서 C보다 상위 언어인 C++은 string이라는 클래스를 제공하며 MFC 라이브러리에도 CString이라는 문자열 클래스가 정의되어 있다. 다음 코드는 str이라는 이름으로 문자 배열을 선언하고 이 배열에 "Korea"라는 문자열 상수를 저장한다.

 

char str[6]="Korea";

 

이 선언문에 의해 메모리에 다음과 같은 기억 공간이 할당되고 초기화된다.

배열이란 같은 타입의 변수를 모아 놓은 것이며 문자 배열이란 문자형 변수 여러 개가 연속적으로 배치되어 있는 것이다. 그림에서 각 격자는 1바이트의 문자들이며 각 격자에 문자 하나씩이 들어있다. 제일 끝에 있는 \0는 여기가 문자열의 끝임을 알리는 역할을 하며 널 종료 문자라고 한다. 메모리는 연속적인 공간이기 때문에 그 끝을 명확히 표시해야 한다.

문자열 상수를 쓰면 컴파일러가 상수의 끝에 널 종료 문자를 자동으로 붙여 준다. 그래서 "Korea"라는 다섯 글자를 저장하기 위해서는 널 종료 문자의 길이까지 고려하여 배열 크기를 6으로 선언해야 한다. 문자열을 화면으로 출력할 때는 printf 함수의 %s 서식을 사용하거나 아니면 puts같은 좀 더 간단한 함수를 사용한다.

문자열 상수내에서도 확장열을 모두 사용할 수 있다. 단, 문자 상수와는 다른 차이점이 한가지 있는데 홑따옴표는 그대로 써도 되나 겹따옴표는 반드시 확장열로 표기해야 한다는 점이다. "Let's go" 이런 문자열 상수를 쓸 수 있지만 "say "go" now" 이런 문자열 상수는 쓸 수 없다. 문자열 상수 표기에 겹따옴표를 사용하기 때문에 "say \"go\" now"와 같이 문자열 내의 겹 따옴표는 확장열로 표기해야 한다. 마찬가지 이유로 홑따옴표를 사용하는 문자 상수의 경우는 ' ' '가 안되며 ' " '는 가능하다.

문자열은 굉장히 기본적인 타입이지만 C언어는 별도의 문자열 타입을 제공하지 않고 배열로 문자열을 표현하도록 되어 있다. 그래서 문자열을 자유 자재로 다루려면 배열에 대해 익숙해져야 한다. 배열과 문자열의 관계는 다음에 배우기로 하고 여기서는 문자열은 문자 배열로 기억한다는 것과 문자열 상수의 표기법 정도만 알아 두고 넘어가도록 하자.


5-4. 3중 문자

3중 문자(Trigraph)란 세 개의 연속된 문자를 하나의 문자로 대체하는 표현이다. 잘 알다시피 C 언어는 미국에 의해 만들어졌고 그러다보니 자기네 나라의 문자 코드인 ASCII표에 정의되어 있는 { } [ ] 문자들을 별 생각없이 구두점으로 정의하고 있다. 그런데 유럽의 몇 개 국가들은 영어에 없는 자신의 고유한 글자(움라우트 등)를 표현하기 위해 이 문자 코드에 다른 문자를 할당해서 사용하고 있으며 그러다 보니 이런 나라의 키보드에는 ASCII표에 있는 9개의 문자들이 없을 수도 있다. 우리 나라의 경우도 \ 문자를 원화 표시인 \로 다르게 표시하고 있지 않은가?

그래서 C표준은 C에서 사용할 수 있는 문자들을 모든 나라에 공통적인 영문자, 숫자와 몇 가지 기호만으로 제한하고 있으며 나머지 문자들은 표준에 있는 문자들의 조합으로 입력할 수 있는 방법을 제공해야 할 필요가 생긴 것이다. 이런 이유로 C 표준은 3중 문자를 정의하고 있다. 물론 키보드에 이런 문자들이 있다면 굳이 3중 문자를 쓰지 않아도 된다. 3중 문자의 종류는 다음과 같다.

 

3 문자

대체 문자

3 문자

대체 문자

??=

#

??/

\

??'

^

??!

|

??(

[

??)

]

??<

{

??<

}

??-

~

 

 

 

??로 시작하는 세 개의 문자를 연속으로 사용하면 이 문자는 컴파일러가 토큰을 분석하는 과정에서 대체 문자로 바뀐다. 다음 예제는 3중 문자로 작성한 것이다. 굉장히 이상해 보이지만 잘 컴파일되며 Trigraph Test #1. num is 2를 출력한다. 

[Trigraph]

{ } 괄호 대신 ??< ??>괄호를 사용할 수 있고 배열을 표기할 때도 [ ] 대신 ??( ??)를 사용할 수 있다. 다소 혼란스러워 보이기도 하고 가독성이 심하게 떨어지기는 하지만 C표준에 의해 정상적인 소스로 인정된다. 재미있는 것은 3중 문자 치환은 구문 해석(Parsing) 전에 일어나므로 문자열 상수에 있는 3중 문자까지도 치환된다는 점이다. 그래서 문자열 내에서 부작용이 발생할 수 있으며 이 부작용을 해결하기 위해 확장열에 3중 문자의 선두로 사용되는 ?를 표기할 수 있는 \?가 따로 정의되어 있는 것이다.

이 외에도 C++ 표준은 연산자나 구두점을 위한 대체 토큰인 이중 문자(Alternative token)라는 것도 정의하고 있다. 예를 들어 <%는 {로 대체되며 %>는 }로 대체되는 식인데 비주얼 C++과 Dev-C++은 2중 문자를 지원하지 않는다. 키보드에 { } 문자가 없는 나라들은 3중 문자가 필요하겠지만 우리나라 키보드에는 이 문자들이 모두 있으므로 우리가 3중 문자에 관심을 가져야 할 이유는 전혀 없는 셈이다.



  
6. 열거형


6-1. 정의

열거형(Enumeration)이란 변수가 가질 수 있는 가능한 값들을 나열해 놓은 타입이다. 어떤 변수가 가질 수 있는 값의 종류가 일정한 범위로 정해져 있다면 정수형 대신 열거형을 쓰는 것이 더 편리하다. 열거형 변수를 선언할 때는 enum 키워드를 사용한다.

 

enum { 멤버멤버, ... } 변수;

 

열거형으로 가능한 값들을 열거 멤버라고 하는데 { } 괄호안에 값의 이름을 나열하면 된다. 구체적인 예를 들어 보자.

 

enum { EAST, WEST, SOUTH, NORTH } mark;

 

mark라는 변수는 방향값을 기억하는데 가능한 값은 동서남북 넷 중 하나이다. mark에 값을 대입하거나 비교할 때는 열거 멤버를 사용한다.

 

mark=EAST;           // mark에 EAST를 대입

if (mark==WEST)          // mark가 WEST이면

 

열거형은 내부적으로 정수로 처리되며 각 열거 멤버는 0부터 1씩 증가하는 정수값을 가진다. 위 예에서 EAST는 0이고 WEST는 1이고 SOUTH, NORTH는 각각 2와 3이다. 컴파일러는 열거형의 멤버들이 어떤 정수값을 가지는지 기억해 두었다가 열거 멤버를 만나면 실제값을 적용한다. 열거형은 정수를 직접 사용하는 방식에 비해 다음과 같은 장점이 있다.

 

① 기억이 용이하다. 열거형 대신 정수형 변수를 대신 사용할 수도 있으나 이렇게 되면 각 정수의 의미가 무엇인지 사람이 일일이 기억해야 하는 불편함이 있다. 값이 10개 정도 된다면 무척 혼란스러울 것이다. 부서를 기억하는 변수 part가 있다고 하자. 이 변수를 정수형으로 선언한다면 각 부서에 대해 0은 총무부, 1은 영업부, 2는 인사부, 3은 경리부, .... 10은 관리부 등으로 의미를 정하고 외워야 하지만 열거형을 사용하면 그럴 필요가 없다. 사람은 숫자보다 문자를 더 잘 기억한다. 10개 정도가 아니라도 LEFT, RIGHT 두 개만 있어도 사람은 무척 혼란스러워한다.

② 소스의 가독성(Readability)이 높아진다. 즉, 읽기만 하면 어떤 의도로 작성된 소스인지 쉽게 파악할 수 있다. mark=3이라고 되어 있으면 3이 무슨 뜻인지 바로 알기 어렵지만 mark=NORTH라고 되어 있으면 북쪽을 대입했다는 것을 분명히 알 수 있다. 팀 작업을 할 때는 이런 가독성이 아주 중요하다.

③ 열거형은 정수형보다 안전하다. mark에 대입될 수 있는 값은 4가지중 하나로 제한되며 동서남북중 하나만 대입될 수 있다. mark를 정수형으로 선언한다면 이 변수에 5를 대입할 수도 있지만 열거형은 가능한 값 중 하나만 대입할 수 있기 때문에 이런 어처구니없는 실수를 컴파일러가 허락하지 않는다.

 

열거 멤버는 별도의 지정이 없으면 0부터 시작하는 정수값으로 정의되며 이어지는 멤버는 앞 멤버의 값+1이 된다. 만약 열거 멤버의 값을 특정한 값으로 분명히 지정하고 싶다면 =다음에 원하는 값을 직접 적어준다.

 

enum { EAST=5, WEST=10, SOUTH, NORTH} mark;

 

이렇게 하면 EAST는 5의 값을 가지면 WEST는 10의 값을 가진다. SOUTH, NORTH는 별도의 값 지정이 없으므로 WEST다음의 정수인 11, 12의 값을 가진다. 다음 열거형은 요일 타입을 정의한다. 요일의 가능한 값은 월요일~일요일까지 일곱가지밖에 없으므로 열거형으로 선언하기에 적당하다.

 

enum { mon, tue, wed, thr, fri, sat, sun } day;

 

이렇게 선언하면 mon이 0이 되고 tue부터 차례대로 1~6까지의 값을 가질 것이다. C는 항상 0부터 수를 세지만 사람은 1부터 세는 것에 더 익숙하기 때문에 첫 번째 멤버의 값을 1로 바꾸는 것이 더 합리적인 경우가 많다. 이럴 때 enum {mon=1, ... 으로 선언하면 월요일부터 일요일까지 1~7의 값을 가지게 된다.

열거 멤버는 일종의 명칭이므로 다른 변수명과 중복되어서는 안되며 유일한 이름을 가져야 한다. 한 열거형내에서 열거 멤버끼리 중복되는 것도 물론 허용되지 않는다. 따라서 다음과 같이 열거형을 선언하는 것은 잘못된 것이다.

 

enum { man, woman, man } human;

 

man이라는 멤버가 두 번 중복되어 있는데 이렇게 되면 human=man이라는 대입문이 0을 대입하라는 것인지 2를 대입하라는 것인지 애매해지게 된다. 언어는 애매해서는 안되므로 이런 식의 열거형 선언은 허용되지 않는다. 하지만 열거 멤버의 값은 중복되어도 상관없다.

 

enum { man=1, woman=2, girl=2 } human;

 

woman과 girl이 똑같은 값을 가지고 있지만 애매하지는 않으므로 이것이 문제가 되지는 않는다. 이 경우 woman과 girl은 일종의 동의어로 처리되며 human=woman 대입문과 human=girl 대입문은 둘 다 똑같이 human에 2를 대입하게 된다. 열거 멤버는 내부적으로 정수형으로 처리되므로 음수를 사용하는 것도 물론 가능하다.






7. 유도형의 소개
 

유도형은 기본형의 조합에 의해 만들어지는 타입들이며 기본형 변수 여러 개를 모아서 또는 기본형을 약간 변형하여 만들어지는 타입들이다. 이절의 제목을 보면 알겠지만 상세한 이론은 다루지 않고 간단한 소개만 하기로 한다. 각각의 유도형들에 대한 문법은 분량도 방대하거니와 복잡하기 때문에 여기서 한꺼번에 다 알기는 어렵다. 각 주제들은 개별 장에서 다시 상세하게 다룰 것이므로 여기서는 개념 위주로만 알아보도록 하자.


7-1. 배열 

배열(Array)은 가장 흔한 자료 구조이면서 또한 가장 실용적이다. 돌(silicon)로 만든 돌머리인 컴퓨터는 판단이나 인식같은 것은 할 수 없으며 오로지 주어진 명령을 아무 생각없이 반복하는 것을 제일 잘한다. 배열은 이런 반복적인 작업과 아주 잘 어울린다. 구조가 단순하기 때문에 속도가 빠르며 이해하기 쉽고 사용하기도 쉽다.

우선 배열의 정의부터 문장화해 보면 동일한 타입을 가지는 자료들의 집합으로 정의된다. 동일한 타입이라는 뜻은 정수형이면 정수형끼리만, 문자형이면 문자형끼리만 모여야 배열이 된다는 뜻이다. 다른 타입들이 모이면 배열이 될 수 없으며 다음에 배울 구조체가 되어야 한다. 동일한 타입의 자료 여러 개가 집합을 이루어야만 배열이 될 수 있다.

정수형 변수 하나는 정수값 하나만 기억할 수 있고 실수형 변수 하나는 실수값 하나만 기억할 수 있다. 배열은 이런 개별 변수들을 여러 개 묶어서 하나의 이름으로 선언한 것이며 동종 자료의 집합을 표현할 수 있다. 배열을 구성하는 각 개별 변수들을 요소(Element)라고 한다. 배열은 다음과 같이 선언한다.

 

타입 배열명[크기][크기]...;

 

 타입 : 배열의 요소가 어떠한 값을 담는지를 지정한다. 즉, 배열이 어떤 값들의 집합인가를 지정한다. int, char, double 등의 기본 타입은 물론이고 유도형이나 사용자가 만든 타입도 가능하다.

 배열명 : 배열도 변수이므로 이름이 있어야 한다. 명칭 규칙에 합당하게 마음대로 이름을 작성할 수 있다.

 크기 : 요소의 개수가 몇개인가를 [ ] 괄호안에 정수 상수로 지정한다. 크기 지정이 하나만 있으면 1차원 배열이라고 하며 두 개 이상이면 다차원 배열이라고 한다.

 

다음은 배열을 선언한 예이다.

 

int array[5];           // 정수형 변수 5개의 집합인 배열 array를 선언

double rate[10];      // 실수형 변수 10개의 집합인 배열 rate를 선언

 

컴파일러는 배열 선언문을 만나면 요소의 크기만한 메모리를 개수만큼 연속적으로 할당한다. array는 정수형이므로 4바이트의 메모리 5개, 즉 20바이트가 할당될 것이며 rate는 80바이트가 할당될 것이다. 메모리에는 다음과 같이 array가 생성되며 이 그림에서 각 격자는 정수 하나의 크기인 4바이트이다.

이렇게 할당된 배열에서 요소를 참조할 때는 [ ] 괄호와 첨자(Index)를 사용한다. 첨자란 요소가 그 배열의 몇 번째에 있는지를 나타내는 순서값이다. C는 항상 0부터 수를 세기 때문에(Zero Base) 첫 번째 요소의 첨자는 0이 되며 마지막 요소의 첨자는 배열 크기보다 항상 하나 더 작다. int array[5]의 마지막 요소는 array[5]가 아니라 array[4]가 된다. 배열 요소를 이렇게 첨자로 참조할 수 있는 이유는 같은 배열에 속한 요소들은 모두 연속적인 메모리에 이웃하게 배치되기 때문이다. 그래서 첨자에 타입의 크기를 곱한 위치를 읽으면 쉽게 요소의 메모리를 액세스할 수 있다.

배열의 각 요소는 배열이라는 큰 집합의 일부분이라는 것 외에는 같은 타입의 변수와 완전히 동일한 자격을 가진다. array[3]이라는 요소는 정수형 변수이며 정수형 변수와 똑같이 사용한다. array[3]=123;과 같이 정수를 대입할 수도 있고 printf("%d",array[3]); 과 같이 값을 읽을 수도 있다. array가 정수형 변수를 모아 놓은 것이고 array[3]은 그 중 하나를 떼어 놓은 것이므로 완전한 정수형 변수인 것이다.

배열의 사용예를 보도록 하자. 학생 30명의 성적을 처리하는 프로그램을 작성한다고 해 보자. 30개나 되는 변수를 각각 따로 만들 필요없이 크기 30의 배열을 하나 선언하기만 하면 된다.

 

int Score[30];

 

이렇게 하면 Score라는 이름으로 30개의 성적을 저장할 수 있는 정수형 변수 집합이 생성된다. Score[0]부터 Score[29]까지 정수형 변수 30개가 연속적인 메모리 공간에 생성되는 것이다. 이 배열에 학생들의 성적을 입력받을 때는 다음과 같은 반복문을 사용한다.

 

for (i=0;i<30;i++)

     scanf("%d",&Score[i]);

 

for문은 반복적인 처리를 하는 문장인데 다음 장에서 배우게 될 것이다. 이렇게 입력된 성적을 가공하는 것도 아주 쉽다. 점수의 총합을 구하고 싶으면 Score[0]~Score[29]까지의 값을 더하기만 하면 된다.

 

Sum=0;

for (i=0;i<30;i++)

     Sum=Sum+Score[i];

 

Score라는 하나의 이름으로 성적 자료를 한곳에 모아 두었으므로 평균이나 분산, 최빈값 등을 구하는 것도 아주 쉽다. 뿐만 아니라 특정 학생의 성적을 조회한다거나 바꾸는 조작도 학생 번호를 첨자로 사용하면 간단하게 해결된다. 만약 배열이 없다면 반복을 할 수가 없고 다음과 같이 해야 할 것이다.

 

int Score0, Score1, Score2, Score3, ..... Score29;

scanf("%d",&Score1);

scanf("%d",&Score2);

scanf("%d",&Score3);

....

scanf("%d",&Score29);

 

필요한만큼 변수를 일일이 선언한 후 사용해야 하니 얼마나 끔찍한가? 30명 정도라면 이렇게 할 수 있겠지만 학생이 천 명정도 된다면 도저히 이런 방식으로는 자료를 처리할 수가 없을 것이다. 가끔 다음 코드가 동작할 것이라고 생각하는 순진한 사람도 있다.

 

for (i=0;i<30;i++)

     scanf(%d",&Scorei);

 

i를 0 ~ 29까지 반복하면서 Scorei를 참조하면 차례대로 Score0, Score1, Score2 변수를 사용할 것 같지만 컴파일러는 Scorei를 하나의 명칭으로 인식하므로 이렇게 되지는 않는다. 그래서 배열이 필요한 것이다. 배열은 하나의 이름으로 동일한 자료들의 집합을 다룰 수 있고 반복적인 처리가 가능하다는 점에서 아주 실용적인 타입이다.

배열의 크기값을 두 개 주면 2차 배열이 된다. 2차 배열은 두 개의 첨자를 가지며 요소를 참조할 때는 두 첨자를 밝혀야 한다. 마치 2차원 좌표 공간에서 한 점을 지정할 때 x, y 두 개의 좌표값을 주는 것과 같다. 다음은 학생과 과목번호를 첨자로 사용하는 2차 배열을 선언한 예이다.

 

int Score[3][10];

 

첫 번째 첨자가 과목의 번호이고 두 번째 첨자가 학생의 번호이다. 컴파일러는 이 선언문에 의해 Score라는 배열에 3*10개의 정수형 변수를 담을 수 있는 메모리(총 120바이트)를 할당한다. 이때의 메모리 모양을 그림으로 그려 보면 다음과 같을 것이다. 실제 메모리에는 선형적으로 배치되지만 2차 배열은 개념적인 도표(table)로 생각할 수 있으므로 일반적으로 표 형식으로 그리며 그렇게 생각하는 것이 더 편리하다.

이렇게 배열에 도표 형태로 성적값들이 저장되어 있다면 이 배열로 성적을 처리하는 것은 아주 쉽다.

 

1번 과목의 총합 : Score[1][0] ~ Score[1][9]까지의 합

5번 학생의 총점 : Score[0][5] ~ Score[2][5]의 합

전체 학생 전체 과목의 총점 : Score[0][0] ~ Score[2][9]까지의 합

 

이런 식으로 반복 처리하면 된다. 총점이 구해지면 평균이나 석차는 약간의 처리만 하면 쉽게 구할 수 있다. 3차 배열이나 4차 배열도 만들 수 있는데 메모리만 충분하다면 얼마든지 큰 배열도 가능하다. 배열에 대한 좀 더 자세한 내용에 대해서는 다음에 따로 배우게 될 것이다. 여기서는 배열이 동일 타입 변수의 집합이라는 정의와 반복적인 처리에 유리하다는 점에 대해서만 직관적으로 이해하도록 하자.


7-2. 구조체

동일한 타입의 집합인 배열과는 달리 구조체(Structure)는 서로 다른 타입의 집합이다. 이때 구조체에 속하는 개별 변수들을 멤버(Member)라고 한다. 정수형 변수와 실수형, 문자형 등의 기본형 변수뿐만 아니라 배열이나 구조체같은 큰 변수도 멤버가 될 수 있다. 구조체도 변수이므로 당연히 이름을 가져야 한다. 또한 구조체에 속하는 각 멤버들도 고유의 타입과 이름을 가진다. 구조체를 선언할 때는 struct라는 키워드를 사용하며 기본 형식은 다음과 같다.

 

struct {

          멤버 목록

변수명;

 

멤버 목록에는 일반 변수 선언문이 오는데 구조체 하나에 여러 개의 변수들을 포함할 수 있으므로 필요한만큼 멤버 변수를 선언하면 된다. 다음이 구조체의 예이다.

 

struct {

     char Name[10];

     int Age;

     double Height;

} Friend;

 

Friend 구조체는 친구 한명의 신상 정보를 가지며 이름, 나이, 키 등을 멤버로 가지고 있다. Name, Age, Height 등은 타입이 다른 변수들이지만 Friend라는 하나의 구조체 이름으로 모여 있다. 더 많은 정보를 저장하고 싶다면 주소, 성별, 전화번호, 취미, 몸무게 등의 멤버를 얼마든지 추가할 수 있다. 멤버는 선언된 순서대로 메모리에 할당된다.

Name 멤버가 10바이트, Age 멤버는 정수형이므로 4바이트, Height는 실수형이므로 8바이트를 차지할 것이다. 구조체 전체의 크기는 멤버들 크기의 총합과 같은데 Friend 구조체는 22바이트의 크기를 가진다. 구조체의 멤버를 참조할 때는 .(점)연산자를 사용하여 구조체.멤버 식으로 적으면 되는데 이때 . 연산자는 영어로 of, 한국어로 ~의로 해석하면 쉽게 이해할 수 있다.

 

Friend.Age=28;

printf("내 친구 이름은 %s이다",Friend.Name);

 

구조체 하나는 여러 개의 다른 타입 변수들을 포함할 수 있으므로 얼마든지 복잡한 정보를 표현할 수 있다. 도서 정보라면 책 제목, 저자, 출판사, 가격, ISBN 등의 정보를 가질 것이고 컴퓨터에 대한 정보라면 CPU, 하드 디스크, 메모리, 광학 드라이브, 모니터 크기 등의 정보들을 가지는데 이런 정보들을 하나의 구조체로 관리할 수 있다.

또한 이런 구조체 여러 개를 모아 구조체 배열을 만들면 복잡한 정보의 집합을 다룰 수도 있다. Friend 구조체는 한 사람에 대한 신상을 기억하는데 이런 타입의 배열을 만들면 주소록이 된다. Friend[100]의 형식으로 구조체 배열을 작성하면 최대 100명의 친구 신상을 기억시킬 수 있을 것이다.

공용체(Union)는 구조체와 유사하지만 멤버끼리 기억 공간을 같이 공유한다는 점이 조금 다르다. struct 키워드 대신 union키워드를 사용하며 초보자에게는 다소 어려운 개념이므로 여기서는 공용체라는 것도 있다는 것만 알아두고 자세한 사용 방법은 다음에 보도록 하자. 

7-3. 포인터

다음은 C의 데이터 타입중에 가장 이해하기 어려운 포인터에 대해 간략하게 소개한다. C 공부를 시작할 때 벌써 소문을 들어서 알고 있겠지만 C의 문법중에서 가장 어렵고 난해한 주제이면서 또한 C를 가장 강력한 언어로 만들어주는 일등 공신이기도 하다. 포인터를 직접 다룰 수 있기 때문에 C언어를 고급언어가 아닌 중급언어로 분류하며 어셈블리와 같은 수준의 시스템 프로그래밍까지도 가능하다.

정수형이나 실수형의 일반적인 변수들은 수치값을 저장한다. 이에 비해 포인터형은 번지를 기억한다는 면에서 일반적인 변수와는 조금 다르다. 데이터가 보관되어 있는 메모리 위치를 기억하고 있기 때문에 직접 값을 조작할 수도 있고 주변의 다른 값까지도 손델 수 있다. 또한 위치는 단순히 4바이트의 번지이기 때문에 함수의 인수로 전달하거나 받기도 효율적이며 함수로 포인터를 전달하면 포인터가 가리키는 메모리를 함수가 직접 조작하는 것이 가능하다.

포인터를 알려면 우선 번지(Address)의 개념에 대해 알아야 한다. 컴퓨터의 주 기억 장치로 사용되는 RAM은 용량이 아주 커서 보통 백만(M) 또는 10억(G) 단위를 사용한다. 컴퓨터에 실제 RAM이 얼마만큼 장착되어 있는가에 상관없이 32비트 운영체제 환경에서 각 프로그램은 32비트의 가상 메모리 공간을 사용할 수 있다. 즉 값을 기억할 수 있는 메모리의 용량은 최대 4G가 된다.

컴퓨터는 메모리의 위치를 구분하기 위해 순서대로 번호를 붙여 관리하는데 이 번호를 번지라고 한다. 최초의 시작 번지는 0번이고 순서대로 1번, 2번, 3번,... 순으로 번호가 매겨져서 40억까지 번지가 붙어 있다. 그래서 컴퓨터는 메모리 중의 특정 바이트를 지칭할 때 이 번지를 사용하여 정확한 위치의 값을 읽고 쓴다.

 

int Num;

 

이 선언문은 앞으로 Num이라는 이름의 정수형 변수를 사용하고 싶다는 뜻이다. 컴파일러는 이 선언문을 만났을 때 당장 사용되지 않는 메모리 공간 4바이트를 할당하고 이 번지에 Num이라는 이름을 붙여 준다. Num이 실제 어떤 번지에 할당될 것인가는 여유 메모리가 어디쯤에 있는지에 따라 달라지기 때문에 실행할 때마다 다르다. 설명의 편의상 1234번지에 할당되었다고 한다면 이때의 메모리 상황은 다음과 같을 것이다.

사용자는 Num 변수가 메모리의 어느 위치에 할당되어 있는가에 상관없이 Num=629; 이런 식으로 변수의 값을 읽거나 쓰기만 하면 된다. 컴파일러는 Num이 할당된 번지를 기억하고 있다가 이 변수를 참조하면 해당 메모리의 값을 읽거나 변경한다.

이때 Num으로 읽고 쓰는 것은 Num이 할당된 번지에 기억되어 있는 값이지 Num의 실제 위치인 번지가 아니다. 즉, C 컴파일러는 변수에 대한 참조문을 변수의 값에 대한 참조로 해석하며 변수의 번지를 참조하는 것이 아니다. 따라서 Num을 읽으면 629가 읽혀지며 1234가 읽혀지지 않는다. 만약 Num이 할당되어 있는 번지값을 알고 싶거나 직접 다루고 싶다면 이때 포인터라는 것이 필요하다. 포인터란 변수의 값이 아닌 변수가 저장되어 있는 메모리의 번지를 기억하는 타입이다. 포인터는 다음과 같이 선언한다.

 

타입 *변수명;

 

타입은 포인터가 가리키는 변수가 어떤 종류인가를 지정하며 int, char, double 등의 기본형과 배열, 구조체, 사용자 정의형 등의 모든 타입이 가능하다. 이 선언문에 사용된 *는 뒤쪽의 명칭이 포인터 변수임을 지정하는 구두점이다. 정수형 변수의 번지를 기억하는 변수 pi는 다음과 같이 선언한다.

 

int *pi;

 

이후 pi는 임의의 정수형 변수가 기억된 번지를 가질 수 있다. 다음 두 연산자는 포인터 변수와 함께 사용되어 번지와 관련된 연산을 한다.

 

* : 포인터가 가리키는 번지의 값을 읽는다.

& : 변수가 기억되어 있는 메모리 번지를 읽는다.

 

일단 두 연산자의 의미를 암기한 후 다음 예제를 작성해 보자. 

[pointer]

포인터의 개념을 설명하기 위한 아주 간단한 예제이다. 실행하면 "Num의 값은 629입니다."라는 문자열이 출력된다. int Num=629; 선언에 의해 4바이트의 메모리 공간이 할당되고 629라는 값으로 초기화된다. Num이 어떤 번지에 할당될 것인가는 알 수 없지만 설명의 편의상 1234번지에 할당되었다고 가정하자.

int *pi; 선언에 의해 정수형 변수의 번지를 가리킬 수 있는 포인터 변수 pi가 생성된다. 그리고 pi=&Num; 대입문에 의해 pi는 Num이 기억되어 있는 메모리 번지 1234라는 값을 가지게 될 것이다. &연산자를 변수명앞에 붙이면 변수의 값이 아닌 번지가 조사된다. 여기까지의 메모리 상황은 다음과 같다.

Num은 1234번지에 할당되어 있고 629라는 값을 가지고 있다. pi=&Num; 대입문에 의해 pi에 1234라는 번지값이 기억되는데 pi는 Num이 저장된 번지를 가리키고 있는 것이다. 이 상태에서 *pi로 값을 읽으면 Num 변수의 값이 읽혀진다.

*연산자는 포인터 변수가 가리키는 번지의 값을 읽는데 pi가 1234번지를 가리키고 있으므로 이 번지로 찾아가 그 값을 읽어온다. pi가 &Num의 값, 즉 Num의 번지값을 가지고 있는 상황에서는 *pi가 Num과 동일하다. Num에 어떤 값을 대입하는 문장 대신 *pi=값; 식으로 대입하는 것도 가능하다. 이 경우 pi가 가리키는 1234번지를 찾아가 값을 변경한다.

그렇다면 Num 변수를 바로 읽으면 되는데 왜 이렇게 포인터라는 간접적인 방법을 사용하는가? 한단계 더 중간 과정을 거치게 되면 그 중간 과정에서 많은 유용한 조작이 가능해지기 때문이다. 소프트웨어 공학에서는 융통성을 위해 중간 과정(전문 용어로 레이어라고 한다)을 삽입하는 경우가 아주 빈번하며 한 번 더 과정을 거침으로써 많은 것들이 가능해진다. 예를 들어 비디오 드라이버, 자바 가상 머신 등등이 레이어의 좋은 응용예이며 네트워크는 무려 7개의 레이어로 구성되어 있다.

위 예제는 실용적인 가치는 없지만 포인터의 개념을 이해하는데 많은 도움을 준다. 이 예제가 어떻게 실행되는지 잘 감이 잡히지 않는 사람을 위해 비슷한 예제 하나를 더 작성해 보도록 하자. 다음 예제는 포인터를 사용하여 간접적으로 값을 대입한다.

[pointer2]


이번에는 실수형 변수를 사용해 보았다. Num1, Num2 두 개의 실수형 변수를 선언하고 Num1에 3.14라는 상수를 기억시켜 놓았다. Num2는 초기화되지 않았으므로 쓰레기값을 가지고 있을 것이다. 실수형이므로 둘 다 8바이트의 메모리를 점유하고 있다. 이 변수들이 할당된 실제 번지는 알 수 없지만 편의상 Num1은 1234, Num2는 5678에 할당되었다고 하자.

실수형 포인터 변수 pd는 &Num1, 즉 Num1이 할당된 번지값을 대입받았으므로 그 값은 1234일 것이다. 이때의 메모리 상황은 다음과 같다.

pd가 Num1의 번지를 가리키고 있다. 이 상태에서 Num2=*pd; 대입문을 실행하면 다음과 같은 연산이 수행된다.

pd가 가리키고 있는 번지를 먼저 찾아 가고 *연산자로 그 값을 읽는다. 그리고 Num2에 값을 대입하였다. 결국 이 예제는 포인터를 통해 간접적인 연산을 하지만 결과적으로는 Num2=Num1; 이라는 대입 연산을 하고 있다. 이 예제를 이해하면 포인터의 개념과 *, & 연산자에 대해 이해했다고 할 수 있다. 이 예제는 *, & 연산자의 동작을 잘 설명하는 전형적인 예제인데 만약 이 두 연산자가 헷갈린다면 항상 이 예제를 생각해 보기 바란다.

이상으로 포인터 타입에 대한 간략한 소개만 했다. 포인터는 한 장을 다 할애해도 설명하기 힘들고 포인터만 다루는 전문 서적이 있을 정도로 어려운 개념이므로 현재 단계에서 크게 욕심 내지 말고 개념만 익히기 바란다. 누가 "포인터가 뭐야?" 라고 물으면 짧게나마 "포인터란 이런 것이야" 라고 대답할 수 있을 정도면 충분하다.

 

앞의 두 예제는 포인터의 기본 개념과 *, & 연산자의 동작을 이해하는데 아주 중요한 의미를 가지고 있다. 논리를 이해한 후 소스를 다시 복원해 보고 이왕이면 메모리속에서 일어나는 일들을 그림으로 그려 설명해 보자. 


7-4. 사용자 정의형

언어가 지원하는 데이터 타입이 아무리 풍부해도 프로그램의 특수한 요구를 다 수용할 수는 없다. 그래서 C는 기존 타입들로부터 사용자가 새로운 타입을 정의하는 방법을 제공한다. 사용자 정의형 타입을 만드는 기본 형식은 다음과 같다.

 

typedef 타입정의 타입이름;

 

형을 정의하는 것도 문장이므로 끝에 세미콜론을 반드시 붙여야 한다. 다음은 사용자 정의형 타입에 대한 몇 가지 예이다.

 

typedef int jungsoo;

typedef enum { True, False } Bool;

typedef int *pint;

typedef int arint[10];

typedef struct { int a; double b; } myst;

 

첫 번째 예는 별로 실용성은 없지만 jungsoo라는 이름으로 int 형에 대한 단순한 별명을 만든 것이다. jungsoo가 int와 똑같은 타입이 되었으므로 int i;라고 선언하나 jungsoo i;라고 선언하나 똑같은 변수가 만들어질 것이다. 두 번째 예는 True, False를 멤버로 가지는 열거형을 Bool이라는 이름의 사용자 정의형 타입으로 새로 만들었다. 이렇게 타입을 정의해 놓으면 다음부터는 Bool형의 변수를 언제든지 선언할 수 있다. 사용자가 만든 타입도 int나 double같은 기본형과 완전히 동일하며 기본형이 올 수 있는 모든 곳에 사용자 정의형도 올 수 있다.

 

Bool Male;

Male=True;

 

pint는 정수형 포인터 타입이며 arint는 크기 10의 정수형 배열 타입이다. 보다시피 타입이름이 반드시 끝에만 나오는 것이 아니라 조금 혼란스럽기도 하다. X형의 데이터 타입을 정의하는 방식은 요령만 알면 생각보다 간단하다. X형의 변수를 선언하는 문장에서 변수를 타입명으로 바꾸고 앞에 typedef키워드만 붙이면 된다.

사용자 정의 타입도 모든 면에서 기존 타입과 동등한 자격을 가진다. 사용자 정의형의 배열을 만들 수 있고 사용자 정의형 포인터도 가능하다. 사용자 정의형은 가독성을 높이는 효과가 있는데 주로 구조체같이 큰 타입에 대해 별도의 타입을 정의하여 사용하는 경우가 많다.


 

'old > C Grammer' 카테고리의 다른 글

기억 부류  (0) 2010.11.18
함수  (0) 2010.11.17
연산자  (0) 2010.11.16
제어문  (0) 2010.11.15
현정누나 요약 C/C++  (0) 2010.08.17
Posted by jazzlife
,