20111105 :: Learning C – 변수의 종류

아 이거 다 늙어서(?) C언어 공부하려니 훽훽 안돌아가는 내 머리가 원망스러울 따름이고 ㅠㅠ. 어쨌든 이 글은 변수를 설명하는 강좌라기 보다는 C 소스를 볼 때 마법처럼(?) 느껴지는 여러 용어에 대한 이해를 돕기 위한 메모차원의 포스팅

변수

많은 프로그래밍 서적들이 설명하듯이 변수는 어떤 값을 보관하는 상자나 그릇 같은 것이다. 물론 이런 비유는 상당히 시각화하기 쉽기 때문에 이 추상적인 ‘변수’라는 개념을 좀 더 와닿게 느끼게 하는데는 도움을 준다. 다만 조금 더 정확하게 이야기하자면 변수는 특정한 값을 저장하기 위해 마련해 놓은 메모리상의 영역이다. 왜 쉬운말을 다시 어렵게 풀어보느냐면 C를 비롯하여 다양한 프로그래밍 언어들이 변수를 다루는 방법은 앞서 이야기한 ‘그릇 모델’로 이해하기에는 한계가 있기 때문이다. 특히 변수의 유형마다 사용하는 메모리의 단위가 다른 C와 같은 언어에서는 저러한 비유는 초심자에게 팍 와닿을지는 몰라도 조금 위험한 비유인 것이 사실이다. 사실상 저 그릇모델은 C의 복병인 포인터를 이해하는 데 발목을 잡는 역할을 톡톡히 한다.

C언어는 애초부터 편의성보다는 성능에 중점을 두고 설계된 언어이기 때문에 변수를 이해하는데는 메모리에 대한 약간의 지식이 (사실상) 절대적으로 필요하다. 또한 역시 이 ‘성능’과 ‘효율성’에 중점을 두고 있는 연유로 메모리 낭비를 줄이기 위해서 다양한 변수 유형이 존재하는 데, 이러한 변수 유형은 하나의 변수가 차지하는 메모리의 크기를 따로 따로 가진다. 큰 데이터는 큰 그릇에, 작은 데이터는 종지에 담아야 메모리를 절약할 수 있는게 당연한 것 아니겠는가.

C의 변수들은 크게 기본형과 유도형으로 나누어진다는 사실에서 출발하자. 기본형이란 하나의 단일 값을 기억하는 변수이며, 유도형은 이러한 기본형으로부터 확장되는 변수 타입이다.

먼저 기본형에는 정수형, 문자형, 실수형, 열거형, void형 등이 있다. 가장 먼저 살펴볼 것은 정수형이다. 정수를 뜻하는 영어단어 integer의 머리글자를 따서 int 형이라고 하고 보통 4바이트의 크기를 갖는다. 물론 아주 작은 정수를 담는 short int 와 같은 형도 있는데, 일단 요즘 컴퓨터들은 메모리를 좀 넉넉히 가지고 있다고 가정하고 뭐 그건 굳이 신경쓰지 말자.

정수형 변수

정수는 우리가 중학교에서 배웠듯이 자연수와 0, 그리고 자연수의 반대편에 있는 음수들을 이야기한다. 이들은 소수점 이하의 값을 가지지 않는 그냥 숫자와 부호만으로 이루어진 값들이다. 물론 수학시간에 배우는 정수는 무한하게 많은 범위를 가지고 있지만 실질적으로 int 형의 변수가 담을 수 있는 정수의 크기에는 한계가 있다.이건 하나의 변수에 들어가는 숫자의 개수가 유한할 수 없다기 보다는 역시 ‘메모리’를 사용하는 방식 때문이다.

우리가 컴퓨터에서 용량의 단위를 이야기하는 바이트(Byte)를 기준으로 살펴보자. 어렴풋이 기억이 날지는 몰라도 Byte는 8개의 비트가 이루어져서 모이는 단위다. (1 Byte = 8비트) 여기서 비트는 컴퓨터가 좋아하는 0과 1. 이 둘 중 하나의 값을 가리키는 단위이다. 한 개 비트는 2진수의 단위이다.

하나의 바이트는 예를 들면 “00000001“과 같이 8개의 비트(0 혹은 1)이 모여서 구성되는데, 그렇다면 00000000 에서 11111111 까지에 이르는 모든 0과 1의 조합을 구분할 수 있는 단위가 된다. 이는 쉽게 계산하면 28이 되어 256까지의 값을 표현할 수 있다. 즉 만약 어떤 변수가 1바이트의 크기를 가진다고 하면 이 안에는 0~255까지의 256가지의 정보에 한정하여 값을 넣을 수 있다는 의미이다. 솔직히 인간적으로 이 범위는 너무 작으므로 C에서는 정수형 변수에 4바이트의 메모리를 할당한다. 즉 232까지의 범위, 그러니까 0에서 40억 정도까지의 값을 포함할 수 있다. 이 정도면 쓸만한 것 같다. 아 그런데 뭐 빼 먹은 것 없는가? 맞다 정수에는 음수도 있다고 했다. 그래서 하나의 예를 들면 하나의 정수형 변수에는 대충 다음과 같은 값이 들어갈 수 있는데,

00000000 00000000 00000000 10101010

여기서 맨 처음의 0을 +/-를 구분하는 값으로 정하는 거다. 그러면 맨 처음 비트가 1이면 음수, 0이면 양수 이런 식으로 구분할 수 있지 않겠는가? 하지만 4바이트라는 한정된 공간에서 구분할 수 있는 값의 가지수는 일정하기 때문에 -20억~20억 사이의 범위를 표현하는 방법도 있다. 당연히 20억보다 큰 수를 쓰는 경우라면 부호를 포기해야 한다. 따라서 변수형은 제각각 unsigned 인지 signed 인지를 구분하여 사용할 수도 있게끔 해 두었다. (기본적으로는 음수를 쓰게 될 가능성이 크므로 부호가 있는 4바이트 정수형이 기본이 된다.)

그리고 변수는 미리 선언해 두어야 쓸 수 있다. 무슨 소리냐면 C 컴파일러는 미리 언질을 해주지 않은 이름에 대해서는 너무 당혹스러워하면서 에러를 뱉어낸다. 따라서 변수든, 함수든 미리 이런게 있다라고 이야기해주어야 하는 것이다. 변수의 선언은 다음과 같이 한다.

변수형 변수이름;

예를 들어 int 타입의 i라는 변수를 선언한다면,

int i;

라고 선언을 해준다. 물론 같은 형의 변수라면

int i, j, k, l;

과 같이 컴마로 구분하여 여러 변수를 한 번에 선언할 수도 있다. 또한

int i=0;

이렇게 변수를 선언하면서 동시에 어떤 값을 집어 넣는 것도 가능하다. 이건 변수를 선언하면서 초기화했다고 한다. 초기화가 뭔지는 묻지 말자 이 아래로 써야 할 글이 수백만 줄인 것 같은 기분이다.

실수형 변수

소수점이 있는 수로 범위를 확장하면 조금 얘기가 달라진다. 소수점이 있는 수는 그 자리수가 유한한지 무한한지에 따라 유리수와 무리수로 나뉘는데 이를 통틀어 실수라 한다. 잠깐 생각해보자. 0과 1사이에는 몇 개의 실수가 있을까? 당연히 무수히 많이 있다. 그런데 변수의 크기는 이런 많은 수들을 구분짓는 종류와 직결된다. 결국 정수처럼 이런 실수를 다루려고 한다면 우리는 아무리 좋은 컴퓨터가 있어도 안된다. 1/3의 계산을 할 수가 없게된다. 그래서 실수는 정수와는 다른 방식을 사용하여 저장하고 표현한다. 학교 때 잠깐 배운적이 있는 2.4534635645 * 109 과 같은 식으로 특정한 한계까지의 자리수와 몇 자리로 이루어진 수인지의 정보를 조합하여 이런 실수를 표현한다. 물론 덕분이 소수점 아주 많이 아래로 내려가거나 하는 경우는 구분할 수 없는 지경에 이르므로 어느 정도의 정확도 손실을 감안해야 한다. (이러한 정밀한 계산을 위해서는 일반적인 사칙연산을 하는 방법이 아닌 다른 방법을 통해 계산한다.)

아참, 그리고 이러한 실수 표현 방식을 부동소수점이라 하는데 (소수점이 움직이지 않는게 아니라 둥둥 떠다닌다는 의미다.) 이게 컴퓨터가 다루기에 참 힘든 종류의 계산이라 힘이 좋은 CPU를 만들기 위해서 언제부터인가 이런 계산에 특화된 회로를 달고 나오게 된다. 아주 오래전에 인텔의 펜티엄 프로세서가 이 계산을 제대로 못한다고 세상의 비웃음을 산 적이 있다.

실수형 변수의 종류에는 floatdouble 이 있다. 물론 각각은 signed/unsigned로 다시 구분이 되며, 보다 큰 범위를 위한 long 형과 조합을 하는 경우도 있는데 역시나 나같은 사람에겐 어울리지 않으니까 패스.

문자형 변수

문자형 변수는 긴말하지 않겠다. 여기서 말이 길어지면 왜 우리 선조들이 컴퓨터를 만들지 못했나 하는 원망부터 시작해서 온갖 인코딩과 유니코드까지 갈것만 같다. 이에 대해서는 조엘 아저씨가 멋진 글을 쓴 적이 있다. 아무튼 이 컴퓨터를 만든 미쿡사람들은 사용하는 문자가 52개 밖에 없다. 그외에 우리 생활에 쓰이거나 정말 한 번도 보지 못한 문자들을 다 갖다 모아도 128자 안에는 다 때려박을 수 있었다. (그게 아스키코드다) 그래서 문자형 변수는 고작 1바이트만 쓴다. 한글은 이보다 훨씬 많은 자모조합을 가지고 있다. 그래서 2바이트로 써야 한다. 이건 나중에 문자열에서 다시 기회가 있으면 이야기하자.문자형 변수는 char 라고 한다. 당연히 character의 줄임이겠지.

void 형

void는 “비어있는”이라는 의미이다. 타입이 비어있다는 말은 뭐가 들어올지 모른다는 이야긴데. 어렵다. 이건 넘어가자. 유형이 없으므로 어떤 값이 담겨도 그게 1이라는 수인지 어떤 글자인지, 뭔지 알 수가 없다.

열거형

이 글을 쓰고 앉아있게 된 근본원인은 열거형이다. 열거(enumerous)라는 단어도 생소한데 상당히 요상하게 쓰여서 이게 꼭 마법의 주문 같고 무슨 말인지 모르겠다는게 문제. 예를 들면 이런 건데

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

이건 direction이라는 변수를 열거형으로 선언한 것이다. 열거형은 사실 정수형과 똑같은 것인데 한가지 재밌는 것은 direction=12 이런 식으로 넣는 것은 안되고 저기 중괄호 안에 들어있는 단어만 넣을 수 있다는 점이다. 즉 direction=0 이라고 해 놓으면 나중에 보는 사람이 이게 무슨 의미인지 도통 알 수가 없지만 direction=EAST 라고 하면 그 의미가 딱 눈에 들어오지 않는가? 중괄호 속에 나열된 단어들은 각각 0, 1, 2, 3과 같은 식으로 증가하는 정수 값이 된다. 물론 이 값들을 계산에 사용할 수도 있는데 그 경우를 위해서 각각의 값이 실제로는 얼마인지 정의해줄 수도 있다.

enum {EAST=11, WEST=13, SOUTH=13, NORTH} direction;

NORTH는 13다음이니 14를 의미하게 된다. WEST와 SOUTH는 같은 값으로 썼는데 이건 에러가 나는 게 아니라 두 단어를 동의어로 간주하게 된다. C가 이렇게 친절할 수 있는 것일까. 심지어는 다음과 같은 방식으로도 쓰는데

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

이때 origin은 태그라고 하는데 이 태그는 마치 변수의 타입처럼 동작한다. 따라서 origin direction; 이라고 변수를 선언하면 저 열거형을 그대로 사용할 수 있게 되는 것이다.

사용자 정의형

여기서 한 번 더 마법의 단어가 등장한다. C는 심지어 특정한 타입의 변수를 사용자가 정의할 수 있게 하는데 그것이 사용자 정의형이다. 예를 들면 나는 int 가 무슨 말인지 모르겠으니 jungsoo 라는 말로 풀어 쓰고 싶다고 하면 다음과 같이 jungsoo 라는 유형을 정하면 된다.

typedef int jungsoo;

사실, 사용자 정의형은 일종의 줄임말 같은 것이다. 여기서는 int 형이 아닌 jungsoo형을 만들었다. 물론 두 개는 같은 거다. 이는 이런 단순한 유형보다는 뒤에 나올 구조체나 그런 복잡한 유도형을 간단히 줄여서 쓸 데 사용한다. 그러니까 이제는 무슨 말인지 알았으니 놀라지 말자.

유도형

앞에서 유도형은 기본형 변수를 확장한 형태라고 했다. 그 유명한 배열 / 구조체 / 공용체 / 포인터  같은 것들이 유도형 변수에 속한다. 기본형에 대해 쓰는데도 저렇게 많이 썼는데.. ㅠㅠ

배열

배열은 C가 가장 사랑하는 변수 유형이다. 그리고 또 매우 흔하며, 처음에는 쉽다가 나중에는 머리 아픈 형이다. 배열은 동일한 타입을 가지는 변수가 연속해서 붙어있는 덩어리이다. 이건 마치…. 속옷 정리함 같은 그런 개념이다!!!

여기서 포인트는 동일한 타입을 붙여 놓았다는 것이다. 즉 정수면 정수, 실수면 실수, 문자면 문자… 이렇게 같은 종류만 모아서 취급한다. 이런 제약이 있지만 구조가 매우 단순하고 또 “메모리에서 연속해 있기 때문에” 아주 빠르게 돌아간다. 배열 속에 뭔가 주루룩 넣어 두고 반복해서 작업을 처리해야 할 때 배열은 매우 잘 어울린다. 배열을 구성하는 각각의 단위는 Element라고 하는데 우리말로는 ‘원소’는 좀 이상하고 ‘요소’도 좀 그렇다.

배열의 선언은 생각보다 단순한데,

타입 배열이름 [크기][크기]….;

각각 요소가 어떤 타입인지를 말하고 이름을 쓴 다음 뒤에 대괄호를 쓰고 그 속에 몇 개짜리 집합인지를 써 주면 된다. 또한 이런 배열을 중첩해서 다시 배열로 만드는 것이 가능해서 뒤에 대괄호를 계속 쓸 수도 있다.

이렇게 크기를 미리 지정해주는 것은 C에서 매우 중요한데, 왜냐면 변수는 메모리의 영역이고 크기를 지정해 주어야 컴파일러가 적당한 메모리 구간에 우리가 자료를 넣을 수 있도록 준비해 줄 수 있는 것이다. 다만, [크기]를 한 번은 생략하고 []만 적는 것도 가능하다. 그럼 컴파일러는 적당한 시점에 그 크기를 정해서 메모리를 할당해 준다. (이런 기능이 있는 것만으로도 감사해야 할 지경이다.)

int arr[5];

컴파일러는 이 배열 선언문을 만나면 정수형 변수 5개의 영역만큼 메모리 안의 연속된 공간을 미리 찜해준다. 각각의 칸은 4바이트니까 이 배열은 메모리에서 20바이트를 차지하는 셈이다.

배열의 각 요소를 참조하는 방법으로는 배열이름에 [순서]를 적어주는 것이다. arr[1] 이라고 하면 위의 배열에서 두 번째 칸을 지칭한다. 컴퓨터는 거의 대부분의 언어에서 0부터 시작해서 순서를 센다. (애플 스크립트는 1부터 세던데, 이는 그 언어가 무척이나 사람의 자연어와 가깝게 만들려고 했기 때문일 거다.)

참고로 C에는 “문자열”이라는 변수형은 없다. 문자열을 다루려면 문자형 배열을 써야 한다. (아 이게 또 골치아프지) 만약에 computer라는 7글자짜리 단어를 저장하고 싶다면 다음과 같이 선언해야 한다.

char com[8];

7글자인데 왜 8칸짜리 배열이 필요한가? 컴퓨터에게 이 배열은 그냥 문자가 각각 다른 변수에 들어있고 그게 붙어있는 속옷 정리함 같은 것이라 했다. 그래서 이것을 문자열로 인식하기 위해서는 문자열이 어디서 끝나는지 알아야 한다. (컴파일러는 배열이 메모리상에서 어디서 시작하는 것에는 관심이 있지만 어디서 끝나는지는 관심이 없다.) 따라서 이 배열의 맨 마지막에는 null 문자가 들어간다. null은 ‘없음’을 의미하는 개념인데 그렇다고 아예 없는 것은 아니고 null 이라는 개념으로 존재한다. 아무튼 이게 맨 끝에 있어야 저 배열이 문자열이구나하는 것을 알게 된다는 것만 알아두고 넘어가도록 하자.

구조체

구조체는 배열하고 약간 다르다. 배열은 여러번 강조했지만 같은 타입의 변수를 모아놓은 집합체이다. 구조체는 서로 다른 타입의 집합체이다. 구조체를 만드는 키워드는 struct 인데, 대략 다음과 같이 선언한다.

struct {

    char Name[10];
int Age;
float Height;

} friend;

friend 라는 변수는 구조체 변수인데, 그 속에는 이름, 나이, 키를 넣을 수 있는 변수가 멤버로 속해있다. (구조체 안에 들어간 변수를 멤버라 한다.) 구조체는 기본형외에도 배열이나 다른 구조체를 그 멤버로 가질 수 있다. 멤버는 선언된 순서대로 메모리에 할당된다. 만약 friend의 나이를 처리하고 싶다면 friend.Age와 같은 식으로 구두점(.)을 사용하여 두 이름을 연결하면 된다. (이건 다른 언어들에서도 많이 봤으니 패스)

구조체를 매번 저런 형태로 선언하는 게 좀 불편하니까 보통은 구조체 자체를 배열로 선언하여 사용하거나, 혹은 사용자 정의형을 사용하여 구조체 자체를 특정한 형식으로 만들어서 사용하는 것을 많이 쓴다.

typedef struct {
char Name[10];
int Age;
float Height;
} friend;  //이 friend가 변수명이 아닌 타입명이 된다.

friend kimchulsoo, hongkildong;

이런 식으로 특정한 구조체를 타입으로 만들어서 사용하는 방법이 그나마 많이 보이는 패턴인 듯 하다.

공용체는 구조체와 유사한데, 멤버끼리 기억공간을 공유한다. 그런데 이게 너무 어렵다. 사실 아직까지는 공용체를 사용하는 소스를 본 적이 없기도 하고 여러 자료를 찾아봐도 감이 안오니 패스.

포인터

드디어 포인터다. C에서 가장 어려운 개념중에 하나이자, 가장 중요한 개념이라고 한다. C언어를 누구는 고급언어라고 하는데[1. 고급언어라는 말은 ‘고수준언어’로 고쳐써야 할 것이다. 꼭 C가 완전 대단한 사람들만 쓰는 언어라고 생각이 들게 하는가.]

아까 정수형 변수 설명으로 돌아가본다. 변수를 선언하면 컴파일러는 해당 변수 유형의 크기만큼의 공간을 메모리에 할당한다고 했다. 그러면 나중에 그 변수의 값을 다시 찾아와야 하거나, 혹은 그 자리에 다른 값을 바꿔 집어 넣어야 한다면 어떻게 그 영역을 찾을 수 있을까? 그게 가능한 것은 모든 메모리의 바이트에는 주소가 있기 때문이다. 메모리상에서의 주소, 즉 위치를 표시하는 값을 번지(address)라고 한다. 포인터는 이 메모리의 번지를 기억하는 변수이다.

물론 이렇게 말로는 정의할 수 있지만, 이건 “그래서 뭐 어쩌라고” 같은 그런 느낌이다. 그래서 좀 적절하지는 않지만 비유를 들어서 생각해보자면 도서관에 책들을 생각해보면 된다.

도서관에는 각 분류별로 책을 나누고 다시 이렇게 나누어진 책들을 제목순으로 정리한다. 그렇게하면서 책을 쉽게 찾게하기 위해서 서지번호라는 걸 매긴다. 우리는 도서관에서 책을 찾을 때 이 서지번호를 이용해서 책을 찾게 된다. 하지만 단순히 제목만으로는 도서관에서 책을 찾을 수가 없다. 왜냐면 도서관 전체를 봤을 땐 책들이 어떤 기준으로 분류되어 있는지 명확하지 않기 때문이다. 따라서 서지번호를 통해 간접적으로 책을 찾는 것이다. 이는 ‘정렬’과 같은 경우에 상당히 유용해질 수 있는 개념인데, 도서관의 모든 책을 제목순으로 정렬하는 것은 만만한 일이 아니다. 설령 그렇게 큰 작업을 해 놓더라도 다시 저자순으로 정렬하거나 출판년도 순으로 정렬을 다시하려면 엄두가 안날 것이다. 하지만 서지 번호가 적힌 쪽지를 그렇게 정렬하는 것은 비교적 쉬운 일이다. 즉 포인터는 실제 책을 가리키는 서지번호에 해당된다고 생각하면 된다.

한가지 재미있는 사실은 배열에서 배열명 그 자체는 배열이 시작하는 메모리 번지를 가리키는 포인터가 된다. 그래서 이런 우스꽝스러운 코드는 C 문법상 하자가 없으며, 심지어 제대로 동작하기도 한다.

int a[10] = {0,1,2,3,4,5,6,7,8,9};
int i;
for(i=0;i<10;i++){
    printf("%d", i[a]);
}
  • Good

    맨 마지막 이미지에
    printf 함수에서 ‘a’ 와 ‘i’의 위치가 바꼈습니다.

    • 원래는 a[i] 로 써야 맞지만 i[a] 로 써도 됩니다. 한 번 컴파일해보시면 아실 겁니다.