C 프로그래밍 입문/C 문법
프로그램을 이해하기 쉽게 만드는 법
프로그래밍을 하는 데 있어서 논리적인 코드를 만드는 것 보다 더 중요한 일은 누구나 쉽게 이해할 수 있는 코드를 만드는 것 이다. 쉽게 이해할 수 없도록 꼬여있고, 일반적으로 사용하지 않는 방법을 써서 효율을 높이는 트리키(tricky)한 코드가 멋져보일지 모르지만, 프로그램 코드로서는, 특히 여러사람이 함께 작업하는 프로젝트의 프로그램 코드로서는 낙제점이다.
작성된 코드는 내가 아닌 다른 누군가가 보고 수정해야 하는 상황이 온다. 내 코드의 일부를 재활용해서 다른 프로그램의 일부분으로 사용될 수 도 있을 것이다. 스스로 만든 프로그램을 오랜 시간이 지난 후에 새로운 기능을 추가하기 위해 수정해야 하는 경우도 있을 수 있다. 그 모든 경우에, 만들어진 프로그램의 코드가 이해하기 어렵다면 종종 원래의 코드를 버리고 완전히 새로 작성하는 편이 더 빠른 경우가 비일비재하다. 그 시점에서 부터 원래의 코드를 작성한 사람은 아무일도 안한 셈이 되어 버리는 것이다.
프로그램을 이해하기 쉽도록 돕기위한 도구는 기본적으로 두 가지가 있다. 하나는 '주석'이고 다른 하나는 '들여쓰기' 이다. 주석은 컴파일러가 프로그램 코드를 컴파일 할 때 무시하고 컴파일 하게 되어 있어, 프로그램 코드를 볼 사람에게 전할 메시지나, 프로그램 코드에 대한 설명들을 추가할 때 사용된다. 최근(?!!)에는 이 주석 영역을 이용하여 자동으로 문서작업을 해주는 프로그램들도 많이 사용된다. 대표적인 C/C++ 문서화 도구로는 Doxygen이 있다.
주석달기
주석(Comment)은 스스로, 그리고 다른 사람이 프로그램 코드를 쉽게 이해하도록 돕기위해 사용되는 도구이다. 물론 주석을 달지 않고, 프로그램 코드를 보는 것 만으로 프로그램을 이해할 수 있도록 작성되어 있다면 최상의 프로그램 코드가 되겠지만, 그런 프로그램 코드를 작성하는 것은 현실적으로 불가능(computational impossible)하다. 그런 프로그램을 작성하기 위해 100%의 노력을 들여 1년만에 완성하는 것 보다. 50%의 노력만 하고 나머지 50%를 주석으로 채워 넣고 6개월 안에 완성하는 편이 현실적으로 더 나은 선택이라 믿는다.
원래 C에서 사용할 수 있는 주석의 형태는
/*
원래 C 표준에서 사용하는 주석의 형태는 이것 하나 뿐 입니다.
*/
위와 같이 '/*'와 '*/' 사이에 원하는 메시지를 집어 넣는 형태 뿐이다. 이 형태의 주석은 여러줄의 추가 설명을 달 수 있다는 장점을 갖고 있다. 주석으로 지정된 내용은 프로그램을 컴파일 할 때 무시하고 컴파일을 한다. 그렇기 때문에 어떤 내용을 기록해도 사회적으로 법적으로 문제를 일으킬 수 는 있을지 몰라도 컴파일되어 만들어지는 프로그램에는 아무런 영향을 미치지 못한다. :)
이런형태의 주석을 사용할 때 주의할 점은 중첩되어(nested) 사용할 수 없다는 것이다.
/*
유효한 주석의 영역
/*
유효한 주석의 영역 2
*/
유효하지 않은 주석의 영역
이 부분에 있는 문장들은 프로그램의 일부로 여기고 컴파일을 시도하게 됩니다.
당연히 에러가 발생하겠지요
*/
위의 예제 코드에서 볼 수 있는 것 처럼 두번째 '/*'는 주석의 일부로 인식 되기 때문에, 컴파일러는 이 내용을 볼 수 없다. 따라서 첫번째 '*/'가 두번째 '/*'의 짝이 아니라 첫번째 '/*'의 짝으로 인식 된다. 그렇기 때문에 첫번째 '*/'이후의 내용은 프로그램 코드의 일부로 인식되고, 두번째 '*/'는 짝이 없는 놈이 된다. 당연히 컴파일 할때 문법 에러가 잔뜩 나오게 된다.
C++ 에서 새로운 형태의 주석이 도입 되었는데, 특정위치 부터 줄의 끝까지를 주석으로 처리하도록 하는 것이다. 이 형태의 주석은 주로 간단한 주석을 기록하기 위한 용도로 많이 사용 된다.
return 0; // 이부분은 주석으로 처리됩니다.
이 형태의 주석은 한 줄만 주석으로 처리할 수 있기 때문에 조금 더 편리하기도 하거니와, 영역을 표시하는 주석과 섞어서 사용할 수 있다는 장점을 갖고 있기도 하다. 예를 들어 코드의 일부를 잠시 주석 처리 하여 동작하지 않도록 하려는 경우 원래 C 형태의 주석이 중첩되지 않는 다는 문제 때문에 사용하기가 아주 어렵지만, '//'를 이용하여 주석처리가 되어 있다면 큰 어려움 없이 적용 할 수 있다.
* 블럭 주석을 쓰는 경우 *
int main()
{
/*
printf("Hello World!\n"); /* 난 헬로 월드가 싫어요 */
여기에서 에러가 납니다. */
return 0;
}
* 한줄 주석을 쓰는 경우 *
int main()
{
/*
printf("Hello World!\n"); // 난 헬로 월드가 싫어요
에러없이 깔끔하게 컴파일 됩니다. */
return 0;
}
대부분의 C++ 컴파일러와 함께 파서를 공유하는 C 컴파일러들이 한줄 주석을 쓸 수 있도록 되어 있기는 하지만, 드물게 사용할 수 없는 경우도 있으니 '당연히 될 것 이다'라는 생각을 갖고 써서는 안된다.
들여쓰기
작성된 프로그램 코드를 이해하기 쉽도록 해주는 두번째 툴로는 들여쓰기(indentation)이다. 들여쓰기는 포함관계를 명확하게 하기 위해 일정 문자만큼 안으로 들여 써 주는 것이다. 다음의 두 예를 보도록 하자
#include <stdio.h>
int main ()
{
for (i=0;i<12;i++)
{
if ((ch=getch())
{
}
else
{
}
}
return 0;
};
#include <stdio.h>
int main ()
{
for (i=0;i<12;i++)
{
if ((ch=getch())
{
}
else
{
}
}
return 0;
};
내용도 없고 몇 줄 되지도 않는 프로그램임에도 불구하고 두 프로그램 코드 간에 차이가 심한 것을 알 수 있을 것이다. 이와 같이 프로그램 작성하는데 있어서 들여쓰기를 하는 것은 프로그램을 이해하기 쉽게 하는데 많은 도움이 된다. 들여쓰기를 하는 정도에 있어서는 개인적인 취향이 많이 적용되는 것으로 스스로 코드 작성하기에 편하고, 쉽게 구조를 이해할 수 있는 정도로 해주면 된다. 들여쓰기에 많이 사용되는 정도는 스페이스 3개, 4개, 8개(혹은 탭 키)가 많이 사용된다. 탭 키를 사용하는 경우에는 여백이 많기 때문에 시원해 보이고 보기에는 가장 편하지만 문장 하나의 길이가 긴 경우나, 여러 단계에 걸쳐서 블록이 생성된 경우에는 오히려 보기 힘들다는 단점이 있다.
식별자
식별자(identifier)라는 용어는 실상 자주 사용되는 용어는 아니다. 주로 프로그래밍 언어의 스펙(specification)에서나 사용하는 용어로 변수, 상수, 함수, 사용자 정의 타입 등에서 다른 것들과 구분하기 위해서 사용되는 변수의 이름, 상수의 이름, 함수의 이름, 사용자 정의 타입의 이름등 '이름'을 일반화 해서 지칭하는 용어이다.
C에서 사용되는 식별자-이름을 짓는데는 몇가지 규칙이 있다:
1. 영문 대소문자, 숫자, 언더스코어('_')만 사용할 수 있다.
2. 키워드를 식별자로 사용할 수 없다.
- 키워드는 C언어의 명령어를 지칭한다고 생각하면된다. 당연히 명령어와 같은 이름이 있다면 컴퓨터 뿐아니라 사람도 헷갈릴 것이다. 키워드는 아래에서 어떤 것들이 있는지 나열해 보겠다.
3. 숫자로 시작될 수 없으며, 반드시 영문자나 언더스코어(_)로 시작 되어야 한다.
- 주의할 것은 언더스코어(_)로 시작되는 이름들은 주로 C언어 자체나, 운영체제에서 제공되는 하위 기능의 이름으로 사용되는 경우가 아주 많다는 점이다. '언더스코어로 시작되는 식별자는 시스템에서 사용하는 식별자'라는 것이 표준은 아니지만 관습적으로 그렇게 이용되기 때문에 언더스코어로 시작된 식별자를 보면 무언가 함부로 사용하면 안되는, 함수 자체에서 효율을 위해 보호를 위한 코드가 거의 작성되지 않았기 때문에 함부로 사용하면 위험할 수 도 있는 대상이라는 생각을 하는 사람들도 있을 것이다. 그렇기 때문에 가능하면 밑줄로 시작되는 식별자를 사용하지 않는 것이 혼돈을 줄이는데 도움이 될 것이다. 물론 프로그램 코드를 작성할때 앞서 말한 용도로 사용되는 함수나 변수를 만들었다면, 경고하는 의미에서 언더스코어로 시작하는 식별자를 사용해 주는 것도 좋을 것이다.
C 에서의 식별자는 대소문자를 구별하여 사용된다. 예를 들어 'number'와 'Number'는 다른 식별자로 인식된다.
키워드
키워드란 C 언어에서 명령어나 어떤 의미있는 의미로 사용되는 단어들을 말하며, 프로그램 코드를 작성하는 사람은 키워드를 다른 용도로 사용해서는 안된다. C에서 사용되는 키워드는 다음과 같은 것들이 있다. 각각의 키워드가 어떤 의미로 사용되는지에 대해서는 C 문법 전체에 걸쳐서 설명이 될 것 이다.
auto break case char const continue default do double else enum extern float for goto if inline int long register restrict return short signed sizeof static struct switch typedef union
다시 한번 말하지만 위에 나열된 키워드는 C 언어의 일부이기 때문에 다른 용도로 사용해서는 안된다.
C에서 사용되는 문자들
C 프로그램 코드에서 사용될 수 있는 공식적인 문자는 다음과 같다.
1. 26개의 라틴 대문자
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
2. 26개의 라틴 소문자
a b c d e f g h i j k l m n o p q r s t u v w x y z
3. 10개의 숫자
0 1 2 3 4 5 6 7 8 9
4. 29개의 특수문자
! " # % & ' ( ) * + , - . / : ; < = > ? [ \ ] ^ _ { | } ~
5. white space 문자
' '(스페이스), 수평탭, 수직탭, 줄바꿈(엔터)
물론 위에 언급된 문자들은 나중에 다시 설명할 '상수'에서 사용할 수 있는 문자를 의미하는 것은 아니다. 화면에 출력하기 위한 문자열이 아닌 프로그램 코드에서 사용할 수 있는 문자들을 의미한다. 화면에 출력하기 위한 문자열에 사용될 수 있는 문자들은 C 프로그램 코드에서 사용할수 있는 문자와 관련된 내용은'지역화(localization)'나 '국제화(internationalization)'와 관련된 내용에서 찾을 수 있을 것이다. 국제화와 지역화에 관련된 내용은 국제화와 지역화에서 다룰 것이다.
확장문자 (Escape Character)
확장문자는 문자열을 표시하는 경우에만 사용되며, 일반적으로 키보드를 통해 입력하기 곤란한 제어문자(Control Character)들을 출력하기 위한 용도로 사용된다:
- \a (alert) 보통은 연결되어 있는 스피커를 통해 '삑'소리를 내며, 경우에 따라 화면에 표시를 할 수 도 있다.
- \b (backspace) 키보드의 백스페이스와 동일한 역할을 한다. 화면 상에서 백스페이스는 커서 이전의 문자를 지우는 효과가 있지만, 프린터에서는 이미 인쇄한 문자를 지울 수 있는 방법은 없기 때문에 주로 두개의 문자를 겹쳐 찍으려고 할때 사용되었었다. 예를 들어 printf("A\b_"); 라는 명령은 밑줄친 A를 인쇄하는 효과가 있다.
- \f (form feed) 인쇄시 페이지를 넘기는 용도로 사용되는 문자로, 화면에 인쇄하는 경우에는 화면을 지우는 효과가 있고, 프린터로 인쇄하는 경우에는 종이를 바꾸도록 하는 효과가 있다.
- \n (new line) 줄바꿈을 하려는 경우에 사용되며, 원래 의미는 '현재 위치의 바로 아래줄로 이동한다'였으나 현재는 의미가 변하여 다음에 설명할 '\r'의 의미까지 포함한 '현재 위치의 아랫줄의 첫 위치로 이동한다'라는 의미로 사용된다.
- \r (carriage return) 커서가 있는 줄의 가장 첫 위치로 커서를 이동하도록 하는 컨트롤 문자.
- \t (horizontal tab) 다음 번 탭 위치로 커서를 이동하여 인쇄를 시작하도록 하게 하는 컨트롤 문자, 키보드에 있는 탭키와 동일한 의미로 동작한다.
- \v (vertical tab) 다음 번 수직 탭 위치로 커서를 이동하여 인쇄를 시작하도록 하기 위해 사용되던 컨트롤 문자.
실제로 다음 줄 첫 위치로 인쇄할 위치를 옮기기 위해서는 "\n\r" 혹은 "\r\n"을 써야 했었으나, 단순히 다음 줄의 현재 위치로 커서를 옮기는(\n)동작이나, 현재 줄의 첫 위치로 커서를 옮기는(\r)동작을 구분해서 사용할 이유가 사라졌기 때문에 다음 줄의 첫 위치로 인쇄할 위치를 옮기는 동작은 \n으로 사용한다. 그러나 MS Windows의 경우에는 여전히 \r\n을 사용하여야 하며, MS Windows와 다른 운영체제 간에 이런 면에서 차이점이 존재하기도 한다.
동일한 방법으로 문자를 인쇄할 때 '아스키 코드'나 '유니코드', 'ISO/IEC 10646'같은 문자코드를 직접 입력해서 지정할 수 도 있다. 이러한 방법을 'Universal character names'라 하며 다음과 같이 사용된다:
- \u (2 바이트 16진 코드) '\unnnn'의 형태로 사용된다.
- \U (4 바이트 16진 코드) '\Unnnnnnnn'의 형태로 사용된다.
위에서 n은 16진수 한자리 값으로 0~9, a, b, c, d, e, f 중 한 글자가 된다. 또한 이 표기방법에서는 정상적으로 표시할 수 있는 문자는 사용되지 않는데, 그 값은 \u00A0 보다 작은 값은 사용하지 않는다. 이때 예외적으로 \u0024 ($), \u0040 (@), \u0060 (`)의 세 문자는 허용된다.
Trigraph sequence
국내의 상황과는 상관 없는 이야기 이지만, C 표준에는 'Trigraph sequence'라는 것도 존재한다. 특정 언어의 경우 위에 나열한 코드에 사용가능한 문자중 일부를 키보드를 이용해 입력하지 못하는 경우가 있는데, 그런 경우를 위해 마련된 것이다. trigraph sequence는 ??로 시작되는 세 문자이며, 다음과 같은 것들이 있다:
??= # ??( [ ??/ \ ??) ] ??' ^ ??< { ??! | ??> } ??- ~
trigraph의 예를 들자면, 아래의 두 코드는 완전히 같은 결과를 얻게 해준다.
printf("Hello???/n");
printf("Hello?\n");
미리 지정된 특수한 식별자
- __func__
- 이 식별자는 현재 함수의 이름이다. 주로 디버깅을 돕는 용도로 많이 사용된다. 아래의 프로그램을 입력하여 컴파일, 실행하면 어떤 역할을 하는지 알 수 있을 것이다.(해당 함수 이름으로 변환하는 매크로)
#include <stdio.h>
int main (int argc, char * argv[])
{
show_your_name();
return 0;
}
void show_your_name(void)
{
printf("%s\n", __func__);
return;
}
데이터 다루기
C 관점에서의 변수 혹은 상수와 데이터 타입
먼저 결론부터 이야기 하자면, C의 '변수' 혹은 '상수' 라는 것은 '메모리의 일부'를 이야기 하며, 메모리에 있는 '데이터를 어떻게 다룰지'를 C 컴파일러에게 알려주기 위해 데이터 타입 이라는 것을 사용 한다.
C 라는 언어는 사람 보다는 기계에 가까운 언어이다. 그렇기 때문에 C 라는 언어를 대할 때엔 기계의 관점에서 대하는 편이 이해하기 쉬운 경우가 다반사 이다. 그중에 가장 민감한 부분이 이 바로 데이터를 다루는 것에 관한 문제인데, C를 공부하는 많은 이들이 포인터의 개념을 이해하고 활용하는데 있어 가장 어려움을 느끼는 이유 이기도 하다. 변수와 포인터의 관계를 이해하는데 있어 고전을 면치 못하는 이유는, C 에서 다루는 데이터는 개념적인 것이 아니라 실물인데 사람들은 그걸 개념으로 이해 하려 시도 하기 때문에 점점 복잡해지고 어려워 지는 것이다.
그럼 함께 생각해 보도록 하자, 물리적인 컴퓨터를 볼때 데이터(예를 들어 그림이나 책의 본문)가 쉽게 저장되고, 수정될 수 있는 곳은 다음 세군데 이다.
- 하드디스크
- 메모리(RAM, ROM)
- 레지스터
프로그래머가 아니더라도 하드디스크에 그림이나 책의 본문이 저장된다는 사실은 알고 있을 것이다. 그리고 어느정도 컴퓨터의 하드웨어에 대한 지식이 있는 사람이라면 메모리에 데이터가 저장된다는 사실을 아는 사람도 적지는 않을 것이다. 마지막으로 가장 적은 수의 사람만이 '레지스터'라는 존재와, 레지스터에 데이터가 저장 된다는 사실을 알고 있을 것 이라 생각한다. 레지스터는 CPU 내부에 존재하며, 연산을 하거는 등의 작업을 하려면 반드시 레지스터에 데이터를 넣어야만 한다. 일반적인 CPU 내에 존재하는 레지스터의 수는 적게는 10여개로 부터 100여개의 레지스터까지 다양하다.[1]
레지스터는 예외적으로 이름을 가지고 있는 메모리 이다. 물론 더 하위 영역으로 내려가면 일련 번호를 가지고 있는 경우도 있겠지만, 기본적으로는 이름을 가진 메모리 이다. 이 경우는 예외로 치고, 다른 두 저장장치, 하드디스크와 메모리는 그 안에 데이터를 써넣거나 읽어들일때 일련번호를 이용한다.
먼저 메모리의 경우 메모리의 특정위치를 지정하기 위해 일련번호를 사용한다. 일련번호는 0부터 시작되며 메모리의 크기만큼 숫자가 할당된다. 예를 들어 64K 메모리를 갖는 시스템이라면 메모리는 0번부터 65535까지의 일련번호를 갖는다.[2] 프로그래머가 지정한 변수나 상수이름은 변환과정을 통해 메모리상의 특정 위치에 해당되는 일련번호와 1:1로 변환된다. 그래서 프로그래머가 변수에 어떤 값을 넣겠다는 코딩을 하면, 해당 값은 변수/상수 이름과 1:1로 연결되어 있는 메모리의 위치에 저장 된다. 마찬가지로 변수나 상수의 값을 읽어들이겠다고 요청하면 변수/상수 이름과 연결되어 있는 메모리 위치에서 데이터를 읽어들이게 되는 것이다.
다음의 그림은 실행되고 있는 프로그램이 메모리안에 어떻게 배열되는지를 그림으로 표시한 것이다:
위의 그림에서 메모리 주소는 위쪽이 작은 쪽이고, 아래쪽이 큰 쪽이다. 혹시 다른 그림에서 순서가 뒤집힌 걸 본 사람도 있을지 모르겠다. 그 경우에는 아래쪽이 메모리 주소가 작은 쪽이고 위쪽이 큰 쪽이다. 결국 같은 그림이니 오해 없길 바란다.
프로그램이 저장되는 메모리 위치가 일련번호 상으로 앞쪽이고, 그 다음에 배치되는 것이 앞에서 처음으로 코드를 입력해서 컴파일을 수행했던 Hello World 프로그램의 경우 printf 함수 내부에 있는 "Hello World!\n"에 해당되는 내용이 배치되는 스태틱 데이터 영역이다. 이 영역에는 이런 상수가 먼저 저장되고, 나중에 다시 설명할 스태틱 데이터가 저장 된다. 힙(heap)영역과 스택(stack)영역에 대해서는 나중에 다시 언급 하도록 하겠다.
프로그래머가 작성한 프로그램 코드 내의 모든 함수와 변수와 상수들은 위에 표시한 것과 같이 메모리 안 어디엔가 곱게 저장이 되어있고, 모든 것들에는 그에 상응하는 메모리의 일련번호가 존재한다. 그리고, 메모리의 일련번호를 주소(address)라 한다.
하드디스크의 경우 조금 더 복잡한 구조를 갖기는 하지만, 근원적으로 일련번호를 기반으로 한다는 데엔 변함이 없다. 하드 디스크를 액세스하는 운영체제의 일부분을 만드는 경우가 아니라면 하드디스크의 구조나 액세스 방법에 대해 관심을 가질 필요는 없을 것 이라고 본다. 대신에, 하드디스크에 저장되어 있는 파일을 읽거나 쓸 때에는 파일의 시작 위치로 부터 몇번째 바이트를 읽고 쓴다 라는 개념을 사용한다는 정도만 기억해 두면 되겠다. 이와 관련된 내용은 프로그래밍/C/기초 프로그래밍영역의 파일의 랜덤 액세스 부분에서 다루게 될 것이다.
이렇게 변수 혹은 상수가 메모리에 저장되어 있는데, 컴퓨터 입장에선 모두 다 동일한 데이터 이기 때문에 이 데이터를 어떻게 다룰지에 대해 프로그래머가 코드를 작성할 때 알려줘야 한다. 예를 들자면 int number라는 형태로 정의된 변수는 '정수이고 메모리 공간을 4바이트 차지하는 데이터를 처리 할 땐, 지정된 메모리에서 4바이트를 가져다가 계산을 할 땐 정수를 계산하는 방법으로 처리 한다.'는 것이다. 나중에 다시 언급 되겠지만, 그래서 C에는 동일한 정수를 다루는 데이터 타입들이 short int, int, long int, long long int, unsigned short int, unsigned int, unsigned long int, unsigned long long int 등의 여러 종류가 있는 것이다. 각각마다 메모리 공간을 차지하는 크기가 다르고, 부호가 있느냐 없느냐에 따라 연산하는 방법이 달라지기 때문에 종류별로 타입이 따로 있어야 하는 것이다.
이와같이 '변수' 혹은 '상수'라는 것은 메모리에 저장된 데이터 이고, 타입은 '변수'나 '상수'를 메모리에 저장하거나 꺼낼때, 그리고 연산할 때 어떻게 할 것인가를 컴파일러에게 알려주는 역할을 하고, '변수'나 '상수'가 저장될 위치는 메모리의 일련번호 -주소(address)를 가지고 지정하게 된다. 예를 들자면 '1264832 번 메모리 공간에서 시작하는 정수형 변수' 라던가, '7326892 번 메모리 공간에서 시작하는 부동 소수형 변수'라는 식으로 데이터의 위치가 지정된다. 문제는 프로그램 코드를 작성하는 사람이 이 메모리의 일련번호를 일일히 외워서 프로그램 코드를 작성할 수 없다는 것 이다. 그래서 고안된 것이 '심볼 테이블(symbol table)'이다. 다음은 그 심볼 테이블의 한 예이다.
Symbol | Type | Address |
age | int | 63245744 |
height | float | 63245748 |
프로그램 코드를 작성하고 변수를 선언하면, 컴파일시에 컴파일러가 위와 같은 심볼 테이블을 만든다. 그리고 C 언어를 컴파일해서 기계어로 변환 할 때 심볼 테이블을 이용해서 변수에 해당하는 메모리 위치를 찾아 변수이름을 모두 메모리 위치로 변경해서 실행 파일을 만들게 된다.[3] 프로그램 코드를 작성할 때 변수를 선언 한다면, '이 변수의 이름은 마지막엔 메모리 위치에 해당되는 숫자로 바뀐다'라는 사실 정도는 기억해 두면 전체적인 C 언어를 이해하는데 도움이 될 것이다.
변수의 유효범위와 저장 위치
변수는 선언되는 위치에 따라 사용 가능한 영역이 정해지며, 필요한 경우 사용자가 변수를 저장할 위치와 특성을 지정할 수 있도록 되어있다.
- 변수는 기본적으로 선언된 블럭과 그 하위 블럭에서 사용할 수 있다.
- 하위 블럭에 동일한 이름으로 변수가 선언되면 하위 블럭 내에서는 하위 블럭에서 선언된 변수가 사용된다.
C 에서 말하는 '블럭'이란 프로그램 코드 단위를 말한다. 앞서 단순한 함수를 만들때 함수의 시작과 끝을 표시하기 위해 '{'와 '}'를 사용했던 것을 기억할 것이다. 이 '{'와 '}'로 묶여진 영역이 바로 블럭이다.
아래의 프로그램 코드를 잠시 보자:
#include <stdio.h>
int main (int argc, char * argv[])
{
int var = 10;
printf("The value before: %d\n", var);
{
int var = 20;
printf("The value in block: %d\n", var);
}
printf("The value after: %d\n", var);
return 0;
}
int some_subfunction()
{
/*
여기에선
1. var 라는 변수가 선언되지 않았고
2. main 과는 나란히 놓여진 별개의 영역이기 때문에
main에서 선언된 변수 var를 사용할 수 없습니다.
var = 20;
이라고 무작정 값을 넣으려 시도하면 컴파일시에 에러가 납니다.
*/
}
프로그램을 실행해 보면 선언된 변수의 유효 범위에 대해 알 수 있을 것 이다. some_subfunction()안에 var에 값을 넣으려는 시도를 하는 코드를 넣을 수 도 있겠지만, 컴파일을 시도하면 그런 변수 선언한 적 없다는 에러 메시지가 표시되고 컴파일을 중단 하게 될 것이기 때문에 넣지는 않았다.
위의 프로그램에서 선언되고 사용된 변수 var은 '지역변수(local variable)'라고 부르기도 한다. 말 그대로 선언된 영역 내에서만 유효하기 때문에 지역 변수라 부른다. 실제로 이 지역변수를 선언한 선언문에는 생략된 내용이 있다. 문법을 제대로 지켜서 코딩을 한다면 다음과 같이 선언 해야만 한다.
auto int var = 10;
auto
auto 키워드는 거의 사용되지 않는 키워드중 하나 인데, '프로그램 최적화시에 가장 빠르게 동작할 수 있는 메모리를 선택하라'는 의미 이다. 지역변수들은 기본적으로 메모리내의 '스택영역'에 위치하며, 블럭영역 안에 들어갈 때 생성 되었다가, 블럭 영역을 빠져나가게 되면 '자동적으로' 제거 된다. auto로 선언된 변수들은 CPU 레지스터중 활용이 가능한 레지스터가 존재하는 경우에는 스택에 변수를 할당하는 대신에 레지스터에 변수를 할당한다. 레지스터는 메모리에 비해 액세스 속도가 월등하게 빠르기 때문에 자주 사용되는 변수가 레지스터에 할당되면 프로그램이 상대적으로 빠르게 동작하게 된다. 문제는 auto 키워드를 사용하는 경우에는 어떤 변수가 레지스터에 할당될지 모른다는 데 있다. 만약에 특정 변수를 레지스터에 할당하고 싶다면 auto 키워드 대신에 register 키워드를 사용하면 된다.
register int var = 10;
register
register 키워드를 사용하여 선언된 변수는 가용 레지스터가 있는 경우 우선적으로 레지스터에 할당된다. 당연히 가용 레지스터가 없는 경우에는 스택에 할당된다. 그렇기 때문에 register 키워드를 남용하면 전혀 사용하지 않는 것과 별다른 차이가 없게 되기 때문에 한 블럭 내에서는 register 키워드를 사용한 변수의 수를 반드시 필요한 최소한으로 제한 하는 것이 좋다.
static
앞서 설명한 바와 같이 auto 변수들은 블럭에 들어가는 순간에 생성 되었다가 블럭을 빠져나가는 순간 제거된다. 다음 프로그램을 실행해 보자:
#include <stdio.h>
int main (int argc, char * argv[])
{
some_subfunction();
some_subfunction();
return 0;
}
int some_subfunction()
{
int var = 0;
var += 10;
printf ("A variable in subfunction: %d\n", var);
return 0;
}
some_subfunction() 이라는 함수 블럭 내에 선언된 auto 변수 var는 함수 블럭에 들어가는 순간 - 이 경우 함수가 호출되는 순간에 생성되었다가 함수블럭에서 빠져나오는 순간 - 이 경우 return 되는 순간 제거 된다. 그렇기 때문에 아무리 var의 값을 증가 시키더라도 증가 될 수 없다.
이제 소스를 다음과 같이 수정해서 실행해 보자
#include <stdio.h>
int main (int argc, char * argv[])
{
some_subfunction();
some_subfunction();
return 0;
}
int some_subfunction()
{
static int var = 0;
var += 10;
printf ("A variable in subfunction: %d\n", var);
return 0;
}
이와 같이 static으로 선언된 변수는 블럭을 벗어나도 제거되지 않는다. 실제로 auto 변수는 블럭에 들어가는 순간 메모리 공간이 할당되고 지정된 값으로 초기화 된다. 그러나 static 변수는 위에서 언급한 메모리 영역중 '스태틱 데이터' 영역에 위치하며 프로그램이 실행될 때 메모리 공간이 할당되고 지정된 값으로 초기화 된다. 물론 static 변수가 제거되는 시점은 프로그램이 종료되는 순간에 제거된다.
전역변수(global variable)
지역변수와 대비되는 변수는 '전역변수(global variable)'라는 것이 있다. 전역 변수는 말 그대로 프로그램 내의 모든 영역에서 액세스가 가능한 변수이다. 전역변수로 선언하기 위한 특별한 키워드가 있는 것은 아니고, 함수들의 외부에 변수를 선언하면 전역변수가 된다. 전역변수는 기본적으로 스태틱 데이터 영역에 배치된다. 전역변수는 auto 키워드나 register 키워드를 사용할 수 없다. 일단 다음 코드를 실행시켜 보자:
#include <stdio.h>
int var = 0;
int main (int argc, char * argv[])
{
printf("The variable: %d\n", var);
some_subfunction();
some_subfunction();
printf("The variable: %d\n", var);
return 0;
}
int some_subfunction()
{
var += 10;
printf ("The variable in subfunction: %d\n", var);
return 0;
}
프로그램의 실행 결과를 확인해 보면 쉽게 알 수 있겠지만, 전역변수는 프로그램 내에서 제한없이 액세스가 가능하다. 그렇기 때문에 전역변수는 잘못 사용하면 양날검과 같아서 아주 이해하기 힘들고, 문제가 생겼을때 문제의 해결을 불가능에 가깝게 몰아가는 특성을 가지고 있다. 그렇기 때문에 전역변수는 1. 반드시 필요한 경우에만, 2. 전역변수의 값을 할당하는 함수는 최소한으로만 해서 써야 한다.
여러 파일에서 통용되는 전역변수
프로그램을 작성하다보면 모든 코드를 파일 하나에만 몰아 넣을 수 없는 경우가 대부분이다. 연습용으로 만드는 프로그램 코드 정도야 길어야 100줄도 안되겠지만, 수십, 수백만줄에 달하는 프로그램 코드를 파일 하나에 몰아넣어 사용할 수 는 없는 노릇이다. 그렇기 때문에 프로그램을 여러개의 파일로 분할해서 사용하게 되는데, 이때 파일이 달라지면 다른 파일에서 선언한 전역변수를 액세스 할 수 없다. 그런데, 액세스 할 수 없는 이유는 전역변수 자체가 액세스 할 수 없도록 막혀있는 것이 아니라, 그러한 전역 변수가 존재한다는 사실 자체를 모르기 때문에 사용할 수 없는 것이다. 다음의 두 코드를 각각 입력해서 함께 컴파일해 보자.
mainfile.c
#include <stdio.h>
int some_subfunction();
int global_variable = 0;
int main (int argc, char * argv[])
{
printf("The variable: %d\n", global_variable);
some_subfunction();
some_subfunction();
global_variable += 10;
printf("The variable: %d\n", global_variable);
}
subfile.c
#include <stdio.h>
extern int global_variable;
int some_subfunction()
{
global_variable += 10;
printf("The variable in subfunction: %d\n", global_variable);
return 0;
}
mainfile.c 쪽의 2번 라인에 있는 int some_subfunction(); 는 그런 함수가 존재한다는사실을 컴파일러에게 미리 알려주는 역할을 한다. 이경우 별도의 extern 키워드를 사용하지 않아도 된다. 많은 컴파일러가 함수의 프로토타입을 미리 선언해 주지 않아도 정상적으로 처리해 주기는 하지만, 원래 프로토타입을 미리 선언해 주지 않으면 컴파일이 되지 않는 것이 정상이므로 가능하면 함수를 호출하는 함수가 호출되는 함수보다 먼저 나오거나, 다른 파일에 호출되는 함수가 정의되어 있는 경우에는 함수의 프로토타입을 미리 선언해 주는 것이 좋다. subfile.c 쪽의 2 번라인에는 mainfile.c 에서 선언된 변수 global_variable 이 존재한다는 사실을 컴파일러에게 알려줌으로서 subfile.c 파일내 코드에서 해당 전역변수를 사용할 수 있도록 준비하도록 한다.
전역변수나 함수에 사용되는 static 키워드는 로컬 변수에서 사용되는 static 키워드는 의미가 다르다. 전역 변수나 함수에 static 키워드가 사용되면 해당 변수나 함수는 현재 파일 외부에서 액세스가 불가능해진다. 다시 말해서 만일 위의 샘플코드에서 전역변수 global_variable 의 속성을 static으로 지정해주면 아무리 subfile.c 에서 extern으로 전역변수를 사용하겠다고 선언해 줘도 실제 외부에서 액세스 하도록 허용하지 않은 셈이 되기 때문에 그런 전역변수가 없다는 내용의 에러메시지를 받게 될 것이다. 마찬가지로 함수의 속성을 static으로 주면 파일 외부에서 해당 함수를 호출할 수 없게 된다.
volatile
마지막으로 다룰 변수의 속성은 volatile 이다. 이 속성은 변수에만 할당되며, 가장 기본적인 개념은 '현재 코드 밖에서 변수의 값을 수정할 수 있기 때문에 컴파일러가 컴파일시에 최적화를 하지 않도록 한다'이다. 컴파일러가 하는 일은 단순히 소스 코드를 기계가 실행할 수 있는 형태로 바꾸는 작업만 하는 것은 아니다. 필요 없는 코드를 제거 한다던가, 생략이 가능한 부분은 생략해서 코드를 단순화 한다던가, 더 빠른 처리를 위해 메모리 대신 레지스터를 사용하게 한다던가 하는 등의 작업을 수행한다. 이렇게 컴파일러에 의해 코드가 최적화 되면 경우에 따라서 이런 작업이 프로그램의 정상적인 동작을 방해하기도 하는데, 하드웨어를 제어하는 프로그램을 만든다던가 멀티 쓰레드 프로그램을 만드는 경우 가장 많은 영향을 받게된다. 다음 예를 보자:
#include <stdio.h>
int main (int argc, char * argv[])
{
static int target;
int cnt;
target = 10;
target = 20;
for(cnt = 0; cnt < 1024; cnt++)
{
target += 10;
}
return 0;
}
사실 의미있는 샘플 코드가 되려면 target이 변수가 아니라 포인터 이어야만 하지만 아직 익숙하지 않은 사람을 위해 그냥 변수로 대체를 했다. 어찌 되었든 target 변수가 앞서 말한 것 처럼 하드웨어를 액세스 하기 위해 매핑된 메모리라 가정을 하고 설명 하겠다. target에 값을 써 넣으면 하드웨어가 그 값을 이용해서 어떤 처리를 한다고 가정 해보자. 실제 위 코드의 7번과 8번 라인의 의미는 하드웨어에게 먼저 10이라는 정보를 보내고, 다시 20이라는 정보를 보낸다는 의미가 된다. 그러나, 컴파일러는 의미 없이 10을 넣고 연이어 20을 넣는 작업을 하는 것으로 인식하기 때문에 컴파일 시에 7번라인을 제거하고 8번 라인만 남긴채 컴파일을 하게 된다. 그러면 하드웨어는 프로그래머의 의도와는 다르게 10과 20을 연이어 받는 것이 아니라 20만 받게 된다. 일반적으로 디버그 코드는 최적화를 하지 않기 때문에 디버그 코드를 넣어 개발작업을 할 때엔 제대로 잘 동작하던 프로그램이 릴리즈를 위해 최적화 작업만 거치면 제대로 동작하지 않는 이상한 상황을 맞게 되는 것이다.
유사한 상황은 10번 라인에서 13번 라인에 걸쳐서도 일어나게 된다. 앞서 이야기한 맥락대로 라면 먼저 10과 20을 보낸다음에 계속해서 10씩 증가된 값을 1024번 하드웨어에게 전달하는 의미로 코드가 작성된 것 이지만, 컴파일러는 단순이 10씩 증가시키는 코드라 간주하고 네줄의 코드를 실행하는 형태로 컴파일 하는 대신에 실제 연산을 미리 수행하고 결과만 대입하게 하거나, 속도를 향상하기 위해 여분의 레지스터를 하나 할당해서 target의 값을 옮겨 넣은 후 for 루프를 수행하고, 그 결과를 다시 target 변수에 할당하는 형태로 프로그램을 만들 수 도 있다. 두 경우 모두 target과 연결된 하드웨어는 for 루프를 수행한 결과값만 한 번 넘겨받게 될 것이다. 물론 일반적인 상황이라면 이러한 형태의 최적화는 프로그램 코드를 단순화 하고 처리 속도를 향상하는 효과가 있지만, 전술한 바와 같이 하나하나 데이터를 전달해야 하는 경우에는 전혀 의도하지 않은 결과를 얻게 된다.[4]
하드웨어를 조작하는 프로그램을 만들거나 나중에 설명할 멀티 쓰레드 프로그램을 만드는 경우에는 메모리와 관련된 문제가 발생하는 경우가 다반사 이니, 관련된 작업을 수행할 시에는 volatile 속성에 대해 한 번쯤 생각해 볼 수 있었으면 좋겠다. 많은 경우에 최적화 - 메모리와 관련된 문제가 발생하면 최적화를 아예 수행하지 못하도록 컴파일 옵션에서 지정해버리는 경우도 많은데, 어찌 생각해보면 빈대 잡자고 초가삼간 태우는 꼴이 될런지도 모르겠다.
위의 코드는 입력해서 실행을 시켜도 큰 의미는 없을 것이며, volatile 형의 변수가 필요한 이유에 대해서는 다른 곳에서 쓰레드 프로그래밍을 설명할 때 언급하고 의미있는 코드를 작성하게 될 것 이다.
변수의 값을 다루는 방법에 따른 분류
앞서 이야기 했듯이, C의 관점에서 보면 변수는 메모리 공간이다. 그런 관점에서 볼때 변수에 접근하는 방식은 두가지가 있는데, 하나는 메모리를 다루는 방법이고 다른 하나는 그 값을 다루는 방법이다. 메모리를 다루는 방법에 따른 접근 방식과 관련된 내용은 앞에서 설명 하였고, 이번에는 값을 다루는 방법에 대해 설명을 하도록 하겠다.
C에서 다루어지는 데이터는 크게 정수와 실수 두 가지로 나눌 수 있다. 하지만 조금 더 아래쪽의 개념에서 접근 한다면 컴퓨터는 0과 1 두 가지 정보만을 다룰 수 있을 뿐 그 이상의 데이터는 0과 1의 조합으로 관리 하도록 되어 있다.
위의 관점에서 8비트 컴퓨터 라는 개념에 대해 이야길 먼저 해보도록 하겠다. 0이나 1 둘중 하나의 값을 보통은 비트(bit)라 한다. 그리고 8 비트 컴퓨터라 함은 0과 1의 값을 다루는 비트 8개를 함께 묶어서 의미있는 데이터로 처리할 수 있다는 의미이다. 8개의 비트를 묶어주면 한 비트가 0과 1 두가지 값을 처리할 수 있는데 비해서 2^8가지, 즉 256가지의 정보를 표현할 수 있게 된다.
위의 개념이 잘 이해되지 않는 사람을 위해 조금 더 쉽게 풀어서 설명해 보도록 하겠다. 2비트 데이터를 처리하는 것 부터 생각해 보도록 하자 1비트 인 경우에는 0과 1 두 가지 상태 뿐이지만, 2비트가 되면 00, 01, 10, 11의 네가지 상태가 되고, 3비트가 되면 000, 001, 010, 011, 100, 101, 110, 111의 여덟가지 상태를 표현할 수 있게 된다. 3비트 데이터인 경우에는 0, 1, 2, 3, 4, 5, 6, 7의 여덟개의 정수값을 표현하게 된다. 결국 n 비트를 합하면 0 ~ (2^n)-1사이의 2^n가지 정수값을 표현하게 된다.
위의 개념을 확장하면 16비트, 32비트, 64비트 컴퓨터의 개념중 일부를 이해할 수 있게 될 것 이다. n비트 컴퓨터에 대한 정의는 단순히 n개의 정보를 동시에 처리할 수 있다는 것 보다는 더 많은 정의가 필요하지만 기본적으로는 n비트의 정보를 처리하는데 가장 효율적인 컴퓨터를 의미한다고 생각하면 큰 범위에서는 맞는다.
여기에서 설명되는 여러가지 형태의 데이터들은 위에서 설명된 바와 같이 비트들을 몇개나 모았는지, 각각의 비트를 어떤 의미로 사용하는 지에 따라 분류되어 있는데, 이는 각각의 데이터를 다루는 방법 자체가 달라질 수 밖에 없기 때문이다.
부정형 데이터
C 에서 제공되는 타입 중에는 void 라는 타입이 있다. 이 타입은 '데이터를 다루는 방법이 정해지지 않았다'는 의미의 타입이다. void 타입의 변수는 1바이트 이며, 변수로 사용되는 경우는 거의 없고 보통은 나중에 설명할 포인터 형태로 사용된다. 함수의 리턴 타입이 void로 잡혀있는 경우는 해당 함수는 리턴을 하지 않는다는 의미로 사용된다. 또한 함수의 매개변수로 변수의 이름 없이 void만 달랑 넣어주는 경우가 있는데 이는 '매개 변수가 없다'는 의미로 사용된다.
void some_function (void) {
}
위 함수는 인수도 없고 리턴값도 없는 함수이며, 실제로 매개변수 항목에 void는 일반적으로 생략된다.
부동소수형 데이터
부동소수형(floating point number type)을 일반적으로 수학에서 사용되는 용어로 바꾼다면 '실수'-소수점이 있는 숫자를 말한다. 가장 익숙한 실수의 예는 '3.141592' 일 것이다. 컴퓨터에서 소수점을 갖는 실수를 다루는 방법은 두 가지가 있다. '고정소수형'과 '부동소수형'인데, 고정 소수형은 소수점의 위치가 정해져 있어서 소수점 이상의 자릿수와 소수점 이하 자릿수가 제한 되어있는 방법이고, 부동소수형은 소수점 위치가 정해져있지 않고, 소수점이 없는 값과 소수점의 위치를 표기하는 방법이다.[5]사실 '부동소수형'이라는 단어는 사람을 헷갈리게 만드는 단어중 하나 인데, '부동(浮動)'이라는 거의 사용되지 않는 단어를 사용하기 때문 일 것이다.[6] '부동소수형'이라는 단어를 들으면 마치 소수점이 꼼짝 않고 제자리에 있다는 의미로 이해 될 텐데, 실제는 '소수점이 떠다니는'이라는 의미이다.
C에서 사용할 수 있는 부동 소수형은 다음과 같다:
타입 | 바이트수 | 유효자리수(정수부)[7] | 최소허용값/최대허용값[8] | 설명 |
---|---|---|---|---|
float | 4 | 24 (6) | 1.175494351 E - 38 3.402823466 E + 38 |
단일 정밀도 부동소수 (single precision) |
double | 8 | 53 (15) | 2.2250738585072014 E - 308 1.7976931348623158 E + 308 |
두배 정밀도 부동소수 (double precision) |
long double | 16[9] | 113 (33) | 3.36210314311209350626267781732175260 E - 4932 1.18973149535723176508575932662800702 E + 4932 |
사배 정밀도 부동소수 (quadruple precision) |
float _Complex[10] | 8 | 단일 정밀도 복소수 | ||
double _Complex | 16 | 두배 정밀도 복소수 | ||
long double _Complex | 32 | 사배 정밀도 복소수 | ||
float _Imaginary | 단일 정밀도 허수 | |||
double _Imaginary | 두배 정밀도 허수 | |||
long double _Imaginary | 사배 정밀도 허수 |
부동소수형 데이터와 관련된 정의는 <float.h>파일에 있으며, 관련된 구체적인 값을 알고 싶다면 이 파일을 확인해 보면 된다. 실제로 부동소수형은 지수부와 가수부로 나뉘어 저장되고 처리 되기 때문에 자리수나 최소/최대값은 의미가 없다. 중요한 것은 소수점 이하 몇자리 까지 정확하게 연산이 되는가 하는 점 인데, 실상은 C 컴파일러/라이브러리에서 제공하는 연산능력은 그다지 정밀하지 않기 때문에 보통은 별도의 연산 라이브러리를 사용하는 경우가 다반사이다.
수학연산을 위한 함수들의 정의는 <math.h>에 정의되어 있으며, 자릿수가 많은 연산을 해야 하는 경우에는 별도의 라이브러리를 사용한다. 대표적인 큰 수 연산을 위한 라이브러리로는 GMP(GNU MP Bignum Library)가 있다.
복소수 표기는 수학에서 표기하는 것과 동일하게 '1.0 + 2.0i'라는 형식으로 표기한다. 추가적으로 일관성 있는 표기를 위해 _Complex_I와 I 라는 매크로가 제공된다. 이 매크로는 <complex.h>에 정의되어 있기 때문에 매크로를 사용 하려면 이 헤더 파일을 소스에 포함 시켜야 한다. <complex.h>가 제대로 포함되어 있다면 '1.0 + 2.0i'를 '1.0 + 2.0 * _Complex_I' 혹은 '1.0 + 2.0 * I' 라는 형태로 사용할 수 있다. 복소수나 허수를 위한 수학연산 함수는 별도로 존재한다. 복소수를 위한 연산 함수들은 <complex.h>에 정의되어 있다.
허수(imagenary)는 실제로 복소수에서 실수부가 없는 수를 말하기 때문에 _Imaginary 타입이 정의되어 있지 않은 컴파일러도 있다[11]. 다음은 복소수를 사용한 연산의 예이다:
#include <stdio.h>
#include <float.h>
int main(int argc, char *argv[])
{
double _Complex a = 3.4 + 2.7i;
double _Complex b = 0.0 + 2.0i;
printf("The complex number: (%6.4f, %6.4f)\n", a);
printf("The imaginary number: (%6.4f, %6.4f)\n", b * b);
return 0;
}
C++ 에서는 복소수를 표현하기 위해 complex 라는 타입을 별도로 가지고 있습니다.
부동소수형 상수를 표기할 때 표기 방법은 두 가지 방법이 있다. 하나는 일반적으로 쓰는 부동소수점 표기 방법과 과학에서 표기할 때 쓰는 표기 방법이다. 다음의 두 수는 같은 값을 다른 방법으로 표기한 것이다.
0.0012 1.2e-3
기본적으로 부동소수형 상수는 double 타입으로 간주되며, float 타입의 상수가 필요한 경우에 f나 F접미사를 사용하여 해당 상수가 float 타임임을 분명히 할 수 있다. 마찬가지로 long double 타입의 상수가 필요한 경우에는 l혹은 L접미사를 지정함으로서 해당 상수가 long double 타입으로 다루어 지도록 할 수 있다. 다음은 각각의 타입에 해당되는 접미사를 사용하여 상수의 타입을 변환하는 것을 확인할 수 있는 프로그램이다.
#include <stdio.h>
int main(int argc, char * argv[])
{
printf("The size of float constant: %d\n", sizeof(1.2f));
printf("The size of double constant: %d\n", sizeof(1.2));
printf("The size of long double constant: %d\n", sizeof(1.2l));
printf("The size of float complex constant: %d\n", sizeof(1.2f + 1.2if));
printf("The size of double complex constant: %d\n", sizeof(1.2 + 1.2i));
printf("The size of long complex constant: %d\n", sizeof(1.2l + 1.2il));
return 0;
}
정수형 데이터
C 에서 가장 많이 다루는 데이터 형태는 정수형이고, 그만큼 데이터 타입도 다양하다. 먼저 C 정수형 데이터의 기본 데이터를 확인해 보도록 하자:
타입 | 바이트수 | 최소값/최대값 | limits.h 상수 | 접미사 | 비고 |
---|---|---|---|---|---|
(signed) short (int) unsigned short (int) |
2 | -32768 - 32767 0 - 65535 |
SHRT_MIN, SHRT_MAX USHRT_MAX |
||
(signed) int unsigned (int) |
4* | -2147483648 - 2147483647 0 - 4294967295 |
INT_MIN, INT_MAX UINT_MAX |
U |
|
(signed) long (int) unsigned long (int) |
4** | -2147483648 - 2147483647 0 - 4294967295 |
LONG_MIN, LONG_MAX ULONG_MAX |
L UL |
|
(signed) long long (int) unsigned long long (int) |
8 | -9223372036854775808 - 9223372036854775807 0 - 18446744073709551615 |
LONG_LONG_MIN, LONG_LONG_MAX ULONG_LONG_MAX |
LL ULL |
*, **: 이 크기는 컴파일러에 따라 달라집니다. 이 아래에 자세한 내용을 설명하였습니다.
표의 타입에 괄호가 쳐진 것은 원래 있어야 하는 내용이지만 생략이 가능하고 보통은 생략하여 사용한다는 뜻 입니다. 예를 들어 unsinged int 타입은 그냥 unsinged 로 사용할 수 있습니다.
정수 타입의 데이터는 많은 변화를 거쳐왔기 때문에 약간의 혼동의 여지가 있다. 먼저 int 타입이 있는데, 이 int 타입은 가장 효율적으로 처리될 수 있는 정수타입이라는 의미이다. 문제는 효율적으로 처리라는 문구에서 시작되는데, 이 이야기를 하기 전에 먼저 해야할 이야기가 하나 있다.
위에서 설명했듯이 컴퓨터를 분류하는 방법 중에는 n 비트 컴퓨터 - 8비트 컴퓨터, 16비트 컴퓨터 등등 - 라는 표현이 있고, 이 n 비트 컴퓨터라는 말의 기본적인 정의는 n 비트 단위로 데이터를 처리하는 컴퓨터라는 뜻이다. 다른 표현을 사용 한다면 8비트 컴퓨터는 데이터를 처리할 때 8비트씩 읽어들여서 처리 하고, 16비트 컴퓨터는 16비트씩 읽어서 처리 한다는 뜻이다. 의미 상으로는 16비트 데이터를 처리할 때 8비트 컴퓨터는 두 번 일을 해야 하고, 16비트 컴퓨터는 한 번만 해도 된다는 의미로, 수치적으로는 16비트 컴퓨터의 속도가 8비트 컴퓨터의 두 배 빠른 컴퓨터 라는 의미 이다[12].
다시 int 타입에 대해서 이야기 하자면, int 타입은 '가장 효율적으로 처리될 수 있는 정수 타입'이기 때문에 16비트 컴퓨터에서는 16비트, 그러니까 2바이트일 때 가장 효율적이므로 int 타입의 크기는 2바이트 이다. 그리고 32 비트 컴퓨터 일 땐 32비트 즉, 4 바이트일 때 가장 효율적으로 32비트 컴퓨터에서는 int 타입의 크기는 4 바이트가 된다. 그래서 8 비트 컴퓨터나 16비트 컴퓨터의 int 타입은 short int 와 동일한 데이터 타입이 되고, 32비트 컴퓨터의 int 타입은 long int와 동일한 데이터 타입이 된다.
문제는 64비트 컴퓨터가 대두 되었을 때 발생 하였는데, int 타입을 64비트로 잡아주면 long int가 int 보다 다룰 수 있는 데이터의 크기가 적어지는 모순이 생기게 되었다. 그래서 64비트 컴퓨터에서는 int를 32비트로 쓰고 long int를 64비트를 쓰는 경우와, int와 long int 모두를 32비트 컴퓨터와 동일하게 32비트를 쓰고, long long int 라는 새로운 64비트 타입을 도입한 경우가 있다.
일단 위의 표는 C99에서 제시한 표준에 따라 작성되었으며, 상황에 따라서는 위의 표와 다른 경우가 있을 수 도 있다는 것을 알려두기 위해서 위의 내용을 기술하였다.
정수형 상수는 기본적으로 int 타입으로 간주되며 short 타입을 강제하는 접미사는 없다. unsigned 타입의 상수임을 명기 하기 위해서는 u나 U를 사용하면 된다. 예를 들어 그냥 '1'이라고 상수를 사용하면 signed int 타입이 되지만, '1U'혹은 '1u'라 표기하면 unsigned int 타입의 상수가 되고, '1ul' 혹은 '1UL'이라 표기하면 unsigned long int 타입의 상수로 다루어 진다.
정수형 상수를 표기할때 사용할 수 있는 진법은 8진법, 10진법, 16진법이다. 8진법 상수를 표기 할때는 0을 시작으로 8진 숫자를 사용한다. 예를들어 8진수 72를 표기하기위해서는 '072'라 표기해 주면 된다. 16진법을 표기하기 위해 사용되는 접두어는 0x 혹은 0X이고, 16진 숫자를 쓰면 된다. 예를 들어 16진수 ae4f를 표기하기 위해서는 '0xae4f'라 쓰면 된다. 8진수나 16진수 상수를 사용하는 경우에도 10진수에서 사용하는 타입지정 접미사는 그대로 사용할 수 있다.
고정폭 정수 타입
이렇듯 데이터의 크기가 환경에 따라 조금씩 달라지기 때문에, 데이터를 쓸 수 있는 값의 차이가 아닌 데이터가 차지하는 메모리 공간이라는 입장에서 봐야 할 땐 이런 일괄적이지 못한 데이터 타입은 프로그래머의 혼란을 야기할 뿐 아니라, 프로그램 코드가 컴파일되는 환경에 따라 다른 코드를 사용해야 하는 문제가 발생한다. 그래서 새로 제시된 타입이 다음과 같은 것 들이 있다.
타입 | 바이트수 | 최소값/최대값 | stdint.h 상수 | 비고 |
---|---|---|---|---|
int8_t uint8_t |
1 | -128 - 127 0 - 255 |
INT8_MIN, INT8_MAX UINT8_MAX |
|
int16_t uint16_t |
2 | -32768 - 32767 0 - 65535 |
INT16_MIN, INT16_MAX UINT16_MAX |
|
int32_t uint32_t |
4 | -2147483648 - 2147483647 0 - 4294967295 |
INT32_MIN, INT32_MAX UINT32_MAX |
|
int64_t uint64_t |
8 | -9223372036854775808 - 9223372036854775807 0 - 18446744073709551615 |
INT64_MIN, INT64_MAX UINT64_MAX |
데이터 폭이 정해져 있는 타입들과 관련된 상수는 <limits.h> 파일이 아닌 <stdint.h> 파일에 정의되어 있다. 위에 언급된 타입들 외에도 최소 n 비트 이상의 정수 데이터를 저장할 수 있는 타입을 의미하는 int_leastn_t, uint_leastn_t (이때 n은 8, 16, 32, 64)타입과 n비트 이상의 데이터 타입중 가장 빠른 타입이라는 의미의 int_fastn_t, uint_fastn_t 타입, 현 시스템에서 사용할 수 있는 가장 큰 정수형 데이터 타입을 의미하는 intmax_t와 uintmax_t 타입도 표준에는 정의 되어있다.
위 표에 나열 되어있는 데이터 타입들은 사용되는 메모리의 양이 분명해야 하는 경우에 사용되며 산술연산을 위해서는 거의 사용되지 않는다. 주로 네트워크 패킷을 처리하는 프로그램 코드에서 자주 볼 수 있다.
조금 어려운 이야기 -- 바이트 오더 혹은 엔디언
위에서 8비트 컴퓨터는 데이터를 8비트 단위로 처리한다는 이야길 했었다. 그러나 8비트로 표현할 수 있는 정수 값은 0 ~ 255 사이의 256개의 숫자 뿐이다. 그러나 실제 프로그램을 작성할 때에는 255보다 큰 값을 처리 해야 할 상황이 발생하기 마련이다. 그런 경우에 2개의 8비트 데이터를 합쳐서 16비트 데이터로 처리 하거나 4개의 8비트 데이터를 합쳐서 32비트 데이터를 처리 해야만 한다.
2개의 8비트 데이터를 합쳐서 덧셈을 하는 경우에는 먼저 2개의 8비트 데이터를 더한 다음에 자리 올림을 해서 다음번 8비트 데이터를 덧셈 하는 방식을 사용하게 된다. 좀더 간단하게 설명하자면 19 + 27을 계산할 때 먼저 9 + 7을 한 다음에 자리 올림 1과 6이라고 계산하고 다시 1 + 2 + 1 (자리올림)을 계산해서 46이라는 값을 얻게 된다.
10진 덧셈 | 2바이트 덧셈 | |||||
올림 | 1 | 1 | ||||
1 | 9 | 1E | 2F | |||
+ | 2 | 7 | + | 3A | E0 | |
4 | 6 | 59 | 0F |
위의 표에서와 같이 10진 덧셈이나 바이트 단위의 덧셈 모두 첫번째 자리에서 먼저 덧셈을 한 다음에 올림 값을 포함해서 윗자리의 덧셈을 하게 된다. 문제는 이 덧셈방식이 사람의 감각을 기준으로 해서 진행되는 것 이다. 실제 8비트 컴퓨터에서 16비트 - 2바이트 덧셈을 한다면 방법이 복잡해 진다. 변수는 메모리 공간 이라고 했던 것을 기억 할 것이다. 그렇기 때문에 실제로 8비트 컴퓨터가 하는 덧셈 이라는 것은 먼저 메모리에서 덧셈을 할 데이터를 두개 꺼내서 덧셈을 하고 두 값을 더한 다음에 다시 메모리에 넣는 세 단계 작업을 의미한다. 그림으로 그린다면 다음과 같다.
8비트 컴퓨터에서 16비트 덧셈을 수행하는 것은 조금 더 복잡하다. 먼저 아랫자리 1바이트 두개를 읽어 더한 다음 자리 올림을 확인하고, 더한 결과를 아랫자리 1바이트 영역에 저장해 둔다. 그 다음 윗자리 1바이트 두개를 읽고 자리 올림과 함께 더해 윗자리 1바이트 영역에 넣어 줘야 한다. 문제는 변수의 메모리 영역을 표시할 때 아랫자리의 위치를 표시하는 것이 아니라 윗자리의 위치를 기준으로 한다는 것이다. 그렇기 때문에 윗자리의 메모리 위치를 얻어낸 다음에 아랫자리 위치를 구하기 위해 메모리 위치 계산을 한번 해야 한다는 것이다. 그리고 위치 계산을 한 값을 가지고 더할 값을 꺼내서 더한 다음 비슷한 과정을 거쳐 저장할 위치를 계산해 내서 값을 저장한다. 아랫자리를 계산하기 위해 덧셈을 세번이나 더 해야 하고, 기준값을 유지하는 작업을 해야 하기 때문에 메모리를 더 필요로 하거나 복잡한 절차를 거처야 한다.
하지만 위의 그림에서 윗자리에 해당되는 바이트와 아랫자리에 해당되는 바이트를 바꾸면 위와 같은 복잡한 절차를 거치지 않더라도 아랫자리를 더한 다음에 결과를 넣고, 메모리 위치를 1증가 시킨 다음에 윗자리를 더하면 되므로 연산의 수가 줄어들고 연산의 복잡도 역시 줄어들게 된다. 실상 대부분의 CPU에는 인덱스드 모드(indexed mode)라는 연산 방식이 있어 여러자리의 연산을 쉽게 할 수 있도록 하는 어셈블리 명령이 별도로 존재하지만 아무래도 상대적으로 오래된 CPU들은 위에 설명한 방식으로 연산을 수행해야 했기 때문에 연산의 복잡도를 낮추기 위해 아랫자리와 윗자리를 바꿔서 저장을 했다.
아랫 자리와 윗 자리를 바꿔넣는 형태의 데이터 저장방식을 리틀 엔디언(little-endian)이라 하며, 이 방식을 사용하는 가장 대표적인 CPU로는 Intel의 x86 계열의 CPU들이 있다. 반대로 아랫 자리와 윗자리를 바꾸지 않고 그대로 저장하는 방식을 빅 엔디언(big-endian)이라 하며 네트워크를 통해 전송할 때에는 이 형태로 전송 하도록 약속되어 있다. 또한 빅 엔디언을 사용하는 CPU중에 가장 알기 쉬운 것은 매킨토시에서 사용되는 모토롤라의 68계열의 CPU들이다.
빅 엔디언과 리틀 엔디언을 통틀어 엔디언혹은 바이트오더 라고 하며 한 시스템이 아닌 여러 시스템에서 공통적으로 사용되는 데이터를 다룰 때, 특히 네트워크를 통해 데이터를 전송하는 경우에는 엔디언 - 바이트 오더 문제를 반드시 고려해야만 한다.[13]
문자형 데이터
C 에는 문자를 저장하기 위한 타입은 없다. char과 unsigned char 라는 타입이 있기는 하지만, 실제 이 타입은 8비트 정수형 데이터 타입이다. 기본적으로 컴퓨터 에게는 '문자'라는 개념 자체가 없다. 단지 인간이 문자가 익숙하기 때문에 A 는 1 B 는 2 라는 식으로 문자와 숫자를 1:1로 변환해서 저장해 놓고, 나중에 출력할 때 다시 1을 A로 2를 B로 바꾸어 화면이나 프린터에 찍어주는 것 뿐이다. 이렇게 사람이 인식할 수 있는 문자를 숫자로 변환해서 저장하는 것을 문자 인코딩이라 한다. 가장 기본적인 인코딩 방식은 ASCII으로 영문자 대소문자와 숫자, 그리고 특수문자들로 구성되어있다. 기본적으로 C에서 제공되는 char 타입은 이 ASCII 문자를 저장하는 것으로 간주되는 것이 일반적이다. 알파벳을 사용하지 않는 문자들을 위해 사용되는 문자형 데이터 타입은 wchar_t로 char 타입과는 달리 한 바이트가 아닌 여러바이트(multibyte) 정수형 데이터 이다. wchar_t 타입의 문자코드는 로케일의 영향을 받아 현재 로케일에 해당되는 데이터를 저장할 수 있도록 되어있다. 다음은 C에서 제공하는 문자와 관련된 기본타입과 추가 타입이다.
타입 | 바이트수 | 최소값/최대값 | limits.h 상수 | 접미사 | 비고 |
---|---|---|---|---|---|
char unsigned char |
1 | -128 - 127 0 - 255 |
SCHAR_MIN, SCHAR_MAX UCHAR_MAX |
||
wchar_t |
wchar_t는 <stddef.h>에 정의되어 있는 정수형 타입이다. 이 타입의 변수에는 멀티 바이트 문자 한 글자가 저장될 수 있다.
문자형 상수는 홑 따옴표(' - single quotation)를 사용해서 지정 하도록 되어 있다. 다음은 문자변수에 문자 A 를 저장하는 프로그램이다. 프로그램을 실행시켜 보고 각각의 라인이 어떤 의미가 있는지 생각해 보기 바란다.
#include <stdio.h>
int main (int argc, char * argv[])
{
char achar = 'A';
printf("The character is: %c\n", achar);
printf("ASCII code of the character: %d\n", achar);
printf("A character next the character: %c\n", achar + 1);
for(achar = 'A'; achar <= 'Z'; achar++)
{
printf("%c", achar);
}
printf("\n");
}
문자열 상수는 겹 따옴표(" - double quotation)을 사용해서 지정하도록 되어있다. 주의할점은 C에는 '문자열 타입'은 없다. 실제로 문자열을 메모리에 저장하는 방법에 대해서는 나중에 다시 설명을 하게 될 것이다. '문자열 타입'의 변수가 존재하지 않기 때문에 문자열은 변수에 바로 대입할 수 는 없다. 다음 프로그램은 가장 처음 만들었던 바로 그 프로그램이다. 프로그램내에 있는 "Hello World!\n"가 바로 문자열 상수 이다.
#include<stdio.h>
int main(int argc, char * argv[])
{
printf("Hello World!\n");
return 0;
}
'A'와 "A"는 언뜻 보기엔 같아 보인다. 그리고 C가 아닌 다른 프로그래밍 언어에서는 같은 의미로 쓰이기도 한다. 그러나 C에서는 두 상수의 의미가 완전히 다른데 'A'는 A 한 바이트 데이터를 의미 하지만 "A"는 실제로 한 바이트가 더 있어 두 바이트 이다. C에서는 문자열의 길이를 따로 관리하지 않고 문자열의 끝을 표시하는 방법을 사용하는데 "A"라는 문자열의 제일 마지막에는 문자열을 표시하는 문자가 숨어있다. 문자열의 끝을 표시하는 문자는 \0으로 실제 "A"는 문자 A와 문자열 끝을 나타내는 문자 \0 두개의 문자가 합쳐진 상수 이다.
C에서 문자 상수와 문자열 상수를 표기할 때 아스키 코드를 직접 지정할 수 도 있는데 다음 네 코드는 결과적으로는 같은 동작을 하는 것이다.
#include<stdio.h>
int main(int argc, char * argv[])
{
unsigned char a = 'A';
unsigned char b = 65; /* 아스키 코드를 직접 할당 */
unsigned char c = 0x41; /* 16진수 아스키 코드로 할당 */
unsigned char d = '\x41'; /* 16진수 아스키 문자로 할당 */
unsigned char e = 0101; /* 8진수 아스키 코드로 할당 */
unsigned char f = '\101'; /* 8진수 아스키 문자로 할당 */
return 0;
}
16진수 아스키 문자는 보통 문자열 안에 여러개의 문자를 할당하려는 목적으로 사용한다. 16진수 아스키 문자를 사용할 때 주의할 점은, x다음에 오는 문자는 반드시 두 자 이어야 한다는 것이다. 사용자가 2바이트 문자코드를 사용하고자 하는 경우 "\x1234"라고 하면 될 것으로 생각하기 쉬운데 실제 C컴파일러는 '\x12'라는 문자와 '3' 그리고 문자 '4'라고 인식하게 되므로 2 바이트 문자 코드를 사용하려고 할땐 반드시 "\x12\x34"와 같이 한 바이트씩 잘라서 표기해 주어야 한다. 오해하기 쉬운 것은 '\123'으로 할당하는 경우에 10진수 123에 해당되는 아스키 문자가 할당될 것으로 생각하는 경우가 많은데, 실제로는 10진수 아스키 코드를 아스키문자로 할당할 수 는 없다. 8진수 문자를 사용하는 경우역시 마찬가지 이다. 8진수 문자는 최대 3자리 일 수 밖에 없기 때문에 "\0123"라는 식으로 써주면 8진수 표기법에 의해 아스키 코드 0123 번이 할당될 것으로 이해되기 쉽지만, 실제로는 '\012'라는 문자와 문자 '3'이 조합된 것으로 인식된다.
wchar_t 타입의 문자 변수에 값을 할당 할때는 wchar_t z = L'\x0041'와 같이 접두사 L을 달아주고, 2바이트 데이터를 그대로 사용하여 할당한다.
데이터 배열
서너 개의 데이터를 다룰 때엔 변수를 이용하면 충분히 데이터를 저장하거나 관리하는 것이 가능하지만 수십~수백의 데이터를 다룬다던가, 수천~수만의 데이터를 다룰때는 변수를 이용하는 것은 배보다 배꼽이 더 큰 작업이 될 뿐 아니라 프로그램 코드를 불필요하게 크게 만든다는 문제가 있다. 예를 들자면 스터디 그룹내의 다섯친구들의 국어 성적을 저장하고 관리하는 작업이야 어떻게 변수를 이용할 수 있겠지만 반 전체나 학교 전체의 국어 성적을 저장하고 관리하는 작업을 변수로 처리하려는 행동은 프로그램화를 하려는 시도 자체를 포기하게 만들고 말 것이다. 이때 사용할 수 있는 구조가 바로 배열이다.
배열은 '변수의 아파트'라고 생각하면 된다. 변수 하나는 하나의 이름을 가지고 있지만, 배열은 여러개의 변수가 하나의 이름과 일련번호를 사용한다. 다음은 float 형 변수 열 개를 갖는 배열의 선언 방법이다.
float data[10];
위와 같이 선언하면 float 형 데이터가 저장될 수 있는 공간이 10개가 연이은 메모리 공간내에 만들어 지고, 10개의 메모리 공간의 첫번째 위치에 data라는 이름이 달리게 된다. 배열을 선언한 경우에는 배열내 몇번째 변수인지를 나타내기 위해 인덱스(index)라는 개념을 사용하며, 이는 배열내 몇번째 변수인지를 일련 번호로 표시 하는 것이다. C 에서 사용되는 배열의 인덱스는 0에서 시작되며, 배열내 멤버의 수가 10개 라면, C 배열의 인덱스는 0에서 9 까지가 된다. 그래서 배열 data의 첫 번째 값과 두번째 값을 더해서 세번째에 넣는다는 프로그램 코드는 다음과 같다.
float data[10];
... 중간 생략 ...
data[2] = data[0] + data[1];
다시한번 말하지만, C 에서 인덱스는 1이 아니라 0에서 부터 시작된다.
그럼 이제 내부에서는 어떻게 동작하는지를 설명 하겠다. 위의 코드에서 선언된 data 라는 이름이 달린 메모리 공간은 메모리의 n 번째 메모리 공간이라 가정해 보자. 다른 변수와는 달리 배열형 변수는 데이터 열(a sequence of data)이라는 개념이 사용 된다. ' n 번째 메모리 공간에 있는 float 형 데이터와 n + 4 번째 메모리 공간에 있는 float 형 데이터를 대상으로 덧셈을 한다. '라는 형태로 기술을 한다. 이때 float 형 데이터가 메모리 공간에 저장될 때 차지하는 메모리 공간은 4바이트 라는 사실은 알려져 있기 때문에 프로그램 코드를 작성하는 사람이 일일히 계산하지 않아도 컴파일러가 대신 처리해 줄 수 있을 것이다.
그래서 메모리 공간 n은 data라는 이름으로 사용되고, 메모리 공간 n 에서 시작되는 float 형 공간에 값을 넣을땐 ' data[0] = 3.1416 '이 되는 것이다. 그리고, 앞에서 말했듯이 float 형 데이터는 메모리를 4바이트 차지한다는 것을 컴파일러도 알고 있으므로, 궂이 +4 라고 하지 않고, 'float 형 데이터의 넓이 * 1'이라는 개념을 사용할 수 있다. 그래서 '메모리 위치 data에서 4 바이트 떨어진 메모리 공간'을 ' data[1] '이라고 쓰는 것 이다.
다차배열
앞에서 배열을 설명하기 위해 '전교생의 국어성적'이라는 대 단위 데이터에 대하여 언급 했었다. 그러나 앞서 언급된 배열만 가지고는 '전교생의 국어성적'을 저장하기엔 무언가 부족한 점이 있다. 실제로, 앞에서 설명한 배열만 가지고는 '한 반의 국어성적'을 처리할 수 있을 뿐 그 이상은 다루기가 어렵다. 물론 1반을 위한 배열, 2반을 위한 배열, 3반을 위한 배열... 이라는 식으로 반 갯수 만큼의 배열을 따로 만들어서 프로그램 코드를 만드는 방법도 있겠지만, 아무래도 각각의 경우를 따져서 코드를 작성해야 하므로 프로그램의 크기도 커지고 실수를 할 확률도 높아지게 될 것이다.
그래서 사용할 수 있는 방법이 2차 배열 이라는 개념이다. 먼저 한반에 최대 50명 까지 있는 15개 반을 위한 배열 선언은 다음과 같이 하면 된다:
float data[15][50];
위와 같이 선언을 해주면 '15개의 반에 한 반당 50명씩'의 국어 성적을 저장 할 수 있는 배열이 만들어 진다. 여기에서 한 가지 개념만 이해하면 C 언어를 다루기 편해진다. 위의 선언을 메모리 관점에서 다른 표현을 사용해 보겠다.
'15개의 영역을 연이어 생성한다. 각각의 영역 안에는 50개의 영역이 연이어 존재하며 각각의 영역에는 float 형 데이터가 저장될 수 있다'
아래의 그림은 float[5][5] 배열에 해당되는 메모리 구조이다. 위의 샘플에서 제시한 배열을 그림으로 그리기엔 너무 큰 그림이 되겠기에 부득이 배열을 변경 하였다.
위와같이 메모리에 배열이 배치된다. 그림을 보면 금방 깨달을 수 있겠지만 2차 배열이란 결국 배열의 배열을 말한다. 일반적으로 프로그래밍을 할 때엔 두 그림중 위쪽 그림과 같이 평면으로 2차원 배열을 간주하고 프로그래밍을 하게 되며, 'iarr[행][열]' 라는 형태의 좌표계로 생각하고 작업을 하게 되며, 실제 프로그래밍을 할 때 메모리와 관련된 작업을 수행할 때엔 아래쪽 그림과 같은 메모리 구조를 머릿속에 넣고 작업을 하면 된다. 아래쪽 그림을 보면 실제 메모리에 배치될 때엔 float[25] 배열과 float[5][5] 배열간에는 차이가 없다는 것을 알 수 있을 것 이다. 두 배열의 차이점은 단지 다루는 방법이 다를 뿐 이다. 나중에 포인터에 대해 설명할 때 이 특성에 대해 다시한번 언급해 보도록 하겠다.
배열을 2차배열로 확장할 수 있게 되었다면, 동일한 방법으로 2차 배열을 확장할 수 있다. 2차 배열이 1차 배열의 배열 이었듯이, 3차 배열은 2차 배열의 배열이다. 실생활의 이미지 상으로는 2차 배열을 쌓아올린 3차원 입체가 될 것이다. 동일한 방법으로 다차원 배열을 생성할 수 있다. 이론상으로는 무한 차원의 배열을 만들 수 있지만 실제로는 배열의 차원이 제한될 가능성이 전혀 없는 것은 아니고, 배열 자체의 최대 크기는 여러가지 이유에 의해 제한 된다.[14] 그렇기 때문에 너무 큰 배열을 할당하는 것은 그다지 좋은 프로그래밍 습관이 아니다. 일정크기 이상의 배열은 로컬 오토 변수로 선언하지 말고 스태틱 변수나 전역 변수로 선언해서 사용하는 것이 stack overflow 에러를 발생시킬 확률을 제거하는 길이다. 사실 전역 변수나 스태틱 변수를 사용하여 배열을 만드는 것 보다 더 좋은 방법은 alloc() 계열의 함수를 이용해서 힙 영역을 이용하는 것이다.
문자 배열
문자배열이란, 당연한 이야기 이겠지만, '문자형 데이터(char)의 배열 '이다. 기본적으로 char 타입은 부호있는 8비트 정수형 데이터로, 숫자와 영어 대소문자, 그리고 구두점 등의 기호들만 표현이 가능하다. 한글이 안되는 프로그램을 자주 볼 수 있는 이유가 바로 이 때문이다. 한글은 부호 없는 16비트의 데이터가 필요한데, 기본으로 제공되는 데이터가 부호 있는 8비트, 결국 7비트 데이터 이기 때문에 처리가 안되는 것 이다. 인터넷을 통해 메일을 보내거나 할 때도 한글로 보낸 이메일 제목이나 본문이 제대로 전달되지 않는 이유역시 이것과 관련이 깊다.[15]
문자배열은 8비트 정수 배열의 특성도 가지고 있지만 문자열을 저장하기 위한 용도로 사용된다. 이해를 돕기 위해 문자열 상수 이야기를 먼저 하고 시작하겠다. 아래 코드는 워낙 많이 봤기 때문에 익숙 할 것 이다:
printf("Hello World!\n");
위 코드에서 "Hello World!\n" 부분이 바로 문자열 상수 이다. 문자열상수는 큰 따옴표(")로 둘러싸인 문자들을 말한다. 문자형 데이터를 설명하면서도 언급 했지만, 최근에 만들어진 컴퓨터 언어들은 큰 따옴표와 작은 따옴표(')를 구분없이 사용하는 언어들이 많지만 C 언어에서 큰 따옴표와 작은 따옴표는 그 의미가 다르다. 다음은 문자열 상수를 문자 배열에 할당하는 코드 이다.
#include <stdio.h>
int main (int argc, char *argv[])
{
char str1[20] = "Hello World!";
char str2[] = "Hello Another World!";
char *str3 = "Hello The Other World!";
printf("Str1: %s\n", str1);
printf("Str2: %s\n", str2);
printf("Str3: %s\n", str3);
return 0;
}
위 프로그램에서 세개의 문자배열에 각각의 값들이 초기화 되어 저장되고 그 문자열을 화면에 출력하게 되는데 결과적으로는 4, 5, 6 세 라인의 의미가 같은 것 처럼 보이지만 실제 내부적인 동작방식은 세 라인이 전혀 다르며, 세 라인의 가장 중요한 차이점은 프로그램이 실행될 때 할당되는 메모리의 양이 다르다는 것이다.
먼저 기억해 두어야 할 것은 str1, str2, str3 세 변수는 자동(auto) 변수이다. 즉 세 변수는 main함수가 실행될 때 스택영역이나 레지스터에 지정된 문자열이 저장될 메모리가 할당되고 main함수가 종료될 때 제거 된다. 실제로 str1 과 str2는 배열의 이름이므로 레지스터를 사용할 수 없겠지만 str3는 포인터 변수 이므로 레지스터에 여유가 있다면 레지스터에 할당되어 사용될 수 도 있을 것이다.[16] 이 것은 세 변수 공히 동일하게 동작하지만 스택에 문자배열을 저장하기 위해 할당하는 메모리 양의 차이가 발생한다. str1 변수는 문자열의 실제 길이와 상관 없이 20바이트가 할당되고 그중 앞부분 13바이트에 문자열이 할당되고 나머지 7바이트는 비워둘 것이다.[17] 그 다음, str2는 컴파일러가 문자열의 길이를 세서 메모리를 할당하기 때문에 모두 21바이트가 스택 영역에 할당될 것이다. 마지막으로 str3는 문자 배열의 이름이 아니라 포인터 타입의 변수 이므로 실제 스택영역에 할당되는 메모리는 문자열의 길이와 상관없이 포인터 타입의 변수 크기만큼의 공간을 할당하고, 스태틱 영역에 저장되어 있는 문자열의 시작 주소를 str3변수의 값으로 할당 하게 된다.
주의해야 할 것은 C에는 ‘문자열’이라는 변수 타입은 없다는 것 이다. 문자열 이라는 것은 결국 n개의 문자 데이터를 저장할 수 있는 메모리 변수를 의미하는 것 이기 때문에 한계가 분명해야 하는 C 데이터의 특성상 ‘문자열’은 그대로 다룰 수 없는 형태의 변수이다. 그 한 례로 다음의 프로그램은 에러가 발생하게 된다.
#include <stdio.h>
int main (int argc, char *argv[])
{
char str1[20] = "Hello World!";
printf("Before: %s\n", str1);
str1 = "Another World!"; /* 컴파일 시에 여기에서 에러가 발생합니다. */
printf("After: %s\n", str1);
return 0;
}
위 프로그램을 컴파일 하려고 시도하면 7번 라인에서 컴파일 에러가 발생하게 된다. 컴파일러의 종류에 따라 에러 메시지가 다를 수 있겠지만 그 의미는 변수에 값을 넣을 수 없다는 의미이다.[18] 위 프로그램에서 str1은 스택영역내의 20바이트에 해당되는 메모리의 시작 위치를 표시하는 역할만 하는 것으로 실제 데이터를 저장할 수 있는 공간을 가지고 있지 않다.
상상을 한 번 해보자. 집 스무 채를 짓고 작은 마을을 하나 만들었다고 가정하자. 그리고 그 마을을 '앗싸리'라 명명한다면, 앗싸리에는 집이 20채 있으니 우편 배달을 하시는 분의 편의를 위해 1번지, 2번지... 20번지 라는 식으로 각각의 집에 번호를 붙여주게 될 것이다. 그럼 누군가 앗싸리에 이사를 한다는 말을 했다면 그 의미는 '앗싸리에 있는 집을 한채 얻어서 그 집으로 들어간다'라는 의미가 된다. 이제 이삿짐 센터에 전화를 해서 앗싸리로 이사해 들어가 보자, 이삿짐 센터에 전화를 해서 '앗싸리로 우리집좀 이사해 주세요'라고 한다면 이삿짐 센터에서는 무어라 하겠는가? 당연히 '앗싸리 몇번지 인가요?'라고 물어볼 것이다.
위의 프로그램에서 str1이라는 문자열 배열의 이름은 '앗싸리'라는 마을의 이름과 동급이다. 그렇기 때문에 7번줄과 같이 프로그램 코드를 작성하는 것은 구체적인 번지를 이야기 해주지 않고 무작정 '앗싸리'로 이사해 달라고 말하는 것과 같은 의미가 되기 때문에 정확한 정의를 필요로 하는 컴파일러로서는 할 수 없는 일을 요청받았다는 에러 메시지를 내보내고 컴파일을 중단 할 수 밖에 없게 되는 것이다.
배열의 개념에 대해 좀더 깊은 이해를 가지고 있는 사람이라면 'str1 배열의 첫번 위치부터 문자를 하나씩 옮겨 넣어 달라는 의미'아니냐는 의미론적인 - 실제로 최근의 대다수 프로그래밍 언어는 그렇게 받아들이는 의미로 이해되지 않는가'라는 질문을 할 수 있을 것이다. 그러나 그런 의미론적인 기술은 C언어에 비해 한참 나중에 만들어진 상대적으로 최신의 기술에 해당된다. C라는 언어는 인간의 의미론적인 접근 방법 보다는 기계적인 동작의 관점에서 접근하기 때문에 그렇게 접근할 수 없다. 이 관점은 C라는 언어를 사용하는데 있어서 전반적으로 공통된 것 이므로 아주 중요하다.
아래 프로그램은 7번 라인의 동작을 수행해주는 strcpy(3)이나 memcpy(3) 함수를 사용하지 않고 7번 라인의 동작을 수행하는 코드 이다.
#include <stdio.h>
int main (int argc, char *argv[])
{
char str1[20] = "Hello World!";
printf("Before: %s\n", str1);
str1[0] = 'A';
str1[1] = 'n';
str1[2] = 'o';
str1[3] = 't';
str1[4] = 'h';
str1[5] = 'e';
str1[6] = 'r';
str1[7] = ' ';
str1[8] = 'W';
str1[9] = 'o';
str1[10] = 'r';
str1[11] = 'l';
str1[12] = 'd';
str1[13] = '!';
str1[14] = '\0';
printf("After: %s\n", str1);
return 0;
}
프로그램이 조금 어이없어 보일지 모르겠지만, C프로그래밍을 하면서 프로그램의 수행속도 향상등의 이유로 위와 같은 코드를 작성하는 경우가 가끔 있다. 어찌 되었든 한 단위 이상의 메모리 공간에 값을 넣기 위해서는 위와 같이 반복작업을 수행해야 한다.
구조체와 유니온 데이터
이 글은 편집중입니다. 누가 언제 마지막으로 글을 썼으며 그리고 쓰고 있는지는 문서역사에서 살펴 보시기 바랍니다. |
구조체를 설명하기 위해, 성적표를 출력하는 프로그램을 작성하는 과정을 생각해 보자. 한 사람의 성적 데이터라는 것은 실제로 여러 정보를 포함한다. 성적표에 들어가는 정보는 학생의 이름, 학년, 반, 번호, 각 과목의 점수, 총점 그리고 평균 등이 될 것이다. 실제로 프로그램을 작성할때 이러한 성적표를 출력하는 프로그램을 만들때 한 반의 정보 전체를 넘겨 받아서 각 사람의 정보를 성적표로 출력해주는 프로그램을 만드는 것 보다. 한 사람의 정보를 받아서 프린터로 인쇄하는 프로그램을 만든 후 다시 그 프로그램으로 반 전체의 성적 리스트 중에 인쇄하려는 사람의 성적표 정보를 뽑아서 넘겨주는 프로그램 두 개를 만드는 편이 개념적으로도 이해하기 쉽고 나중에 프로그램을 변형해야 하더라도 좀 더 쉽게 작업을 할 수 있게 될 것이다. 이 경우, 정수를 저장할 수 있는 데이터 타입이 있는 것 처럼 한 사람의 정보를 저장할 수 있는 데이터 타입이 있다면 프로그램을 작성 할 때에도 편리 할 것인데, 이렇게 관계있는 데이터들을 하나로 묶어서 관리할 수 있도록 제공해주는 자료 구조가 바로 '구조체와 유니온'이다. 구조체와 유니온은 사용되는 개념에서 약간의 차이가 있지만 기본적인 개념의 시작은 동일하고, 유니온에는 구조체가 갖는 특성 위에 다른 특성이 부가 된다.[19]
구조체 (Structure)
C 스펙에 있는 구조체의 정의는 '동일하지 않은 데이터 타입을 갖는 일련의 데이터의 집합'이다. 그에 비해 배열의 정의는 '동일한 데이터 타입을 갖는 일련의 데이터의 집합' 이다. 두 정의에 들어있는 '일련의(sequential)'이란 단어의 의미는 '연달아 놓여있는'의 의미 이다. 위에서 배열의 정의에 관해 정확하게 이해 했다면 '연달아 놓여있는 데이터의 집합'이라는 말의 의미를 정확하게 이해할 수 있을 것이다.구조체는 배열과 마찬 가지로 데이터들이 연달아 놓여 있는데 배열과의 차이점 이라면 구성 요소가 되는 데이터의 타입이 다르다는 말이다. 여기에서 '변수'의 정의를 다시 한 번 기억해 보도록 하자. 변수는 '메모리 공간'이었고, 배열은 같은 종류의 변수가 여러개가 메모리상에 연달아 존재 한다. 배열의 특성상 같은 종류의 메모리가 여러개 연달아 놓여있기 때문에 배열 내 n 번째 메모리 공간을 사용하고 싶으면 '(n - 1) * 배열내 요소 하나의 크기'라는 간단한 계산식으로 사용하고자 하는 메모리 공간의 주소를 얻을 수 있기 때문에 배열에서는 인덱스(index)라는 개념을 이용해서 배열 멤버를 다룰 수 있었다. 그러나 구조체의 경우에는 '동일하지 않은 데이터 타입'이라는 전제 조건이 있기 때문에 구조체 내의 멤버를 다루기 위해 멤버가 위치하는 메모리내 위치를 계산하기 위해서는 위에서 배열내 멤버의 위치를 계산하는 방법을 사용할 수 없다. 그렇기 때문에 구조체는 인덱스가 아닌 멤버의 이름을 일일히 지정하고 그 이름을 사용하는 방법을 사용한다.
다음은 구조체를 사용하는 프로그램의 간단한 예 이다.
#include <stdio.h>
struct articles_type {
int korean, english, math;
};
structure score_card_type {
int year, grade, seq;
struct articles_type scores;
int total;
double average;
};
int print_a_scorecard (struct score_card_type sc) {
printf ("%2d 학년 %2d 반 %3d 번\n", sc.year, sc.grade, sc.seq);
printf ("=========");
printf ("국어: %4d 영어: %4d 수학: %4d\n", sc.scores.korean, sc.scores.english, sc.scores.math);
printf ("총점: %5d 평균: %5.1f\n", sc.total, sc.average);
return 0;
}
int main (int argc, char *argv[]) {
structure score_card_type a_student;
/* 여기에 전체 데이터 베이스에서 한 학생의 성적을 추출해 내는 프로그램 코드가 있다고 가정 합니다.
나중에 포인터를 배운 후에 이해될 수 있는 부분이기 때문에 이 내용은 잠시 비워둡니다. */
print_a_scorecard (a_student);
return 0;
}
구조체를 사용하기 위해서는 구조체의 형태를 먼저 정의하고, 정의된 형태의 구조체 타입의 변수를 만들어서 사용한다. 좀 다른 표현으로 설명하자면, 여러개의 데이터가 들어가는 새로운 변수의 타입을 하나 만든 다음에 그 타입의 변수를 만들어서 사용한다.[20] 배열의 경우 동일한 타입의 변수가 반복되는 것 이기 때문에 다로 그 내용에 대한 타입을 별도로 정의해 줄 필요가 없지만, 구조체의 경우에는 포함되는 데이터의 타입이 다르다는 전제가 있으므로 먼저 어떤 내용이 들어갈지를 정의해 주는 작업을 거쳐야 한다. 위의 코드를 보면 2번 라인에서 11번 라인까지의 프로그램 코드에 구조체의 구조를 정의 했다(혹은 struct articles_type 타입과 struct score_card_type 타입을 만들었다). 그리고 23번 라인에서 struct score_card_type 타입의 변수를 하나 만든 것 이다.
위 코드에서 주의할 점이 두 가지가 있는데, 첫째, 구조체의 끝에는 반드시 세미콜론(;)을 찍어 주어야 한다. 아직 설명되지 않았지만 구조체의 닫힘 중괄호(}) 다음에는 생략 가능한 내용이 있으며(실제로 위의 프로그램에서는 두 구조체 모두 생략했다), C 컴파일러는 그 생략가능한 내용이 생략된 것 인지 아닌지 확인할 방법이 없기 때문에 중괄호 다음에 더이상 내용이 없다면 반드시 세미콜론을 찍어서 문장이 더 이상 없다는 것을 표시해 주어야 하는 것 이다. 둘째, articles_type과 score_card_type은 변수 이름이 아니고 타입 이름의 일부 일 뿐이다. 많은 초보 C 프로그래머들이 쉽게 실수하는 내용이므로, 아무리 당연하다고 생각 되어도 헷갈리지 않도록 주의할 필요가 있다.
만들어진 구조체 변수내의 멤버를 액세스 하는 방법은 구별자인 마침표(.;dot, full stop)를 이용한다. 기본 형태는 '구조체변수.멤버변수'의 형태와 같이 구조체변수와 그 멤버이름 사이에 점을 찍음 으로서 두 변수가 포함 관계에 있음을 표시해 준다. 위 프로그램의 8번 라인을 보면 struct subjects_type scores 라는 구조체 멤버 변수를 볼 수 있을 것이다. 이 샘플 코드에서 볼 수 있듯이 일단 한번 만들어진 구조체는 그 자체로도 하나의 데이터 타입으로서 다루어 지므로 다른 구조체의 멤버 변수로 선언 될 수 있다. 구조체 변수 내의 구조체 멤버의 멤버를 액세스 할 때도 동일한 방법을 반복하여 사용해 주면 된다. 16번 라인을 보면 sc.scores.korean 라는 변수를 볼 수 있는데, 이는 'sc구조체 변수 내 멤버 구조체 변수인 scores의 멤버 변수 korean'이라는 의미로 액세스 하게 된다. 다차 배열에서 단순한 규칙을 이용해 다차 변수를 액세스 했듯이 다중 구조의 구조체(nested structure)를 액세스 할 때역시 기본 구조체 액세스 방법을 반복해 줌으로서 포함되어 있는 구조체 변수의 멤버 변수를 액세스 할 수 있다.
구조체 타입의 변수 역시 변수 임에는 변함이 없으므로 기본형의 데이터타입을 배열로 만드는 것과 같은 방법으로 구조체의 배열 역시 만들 수 있다. 다음 코드는 한반에 최대 60명까지 있을 수 있는 반 전체 성적표 데이터를 저장할 수 있는 배열의 선언이 된다.
struct score_card_type a_class[60];
위와 같이 선언된 구조체 배열은 일반 배열과 동일하게 액세스 할 수 있으며, 배열내 n번째 구조체 변수의 멤버 변수를 액세스 하는 방법에 대해서는 해당 구조체 변수 배열이 메모리에 저장되어 있는 방식을 잠시 생각해 보면 쉽게 유추해 볼 수 있을 것이다. 전체 구조를 컨테이너라 가정한다면 가장 외부의 컨테이너는 배열이 된다. 그러므로 배열 컨테이너에 있는 구조체 변수 컨테이너 하나를 꺼낸 다음, 다시 구조체 안에 있는 내용물인 변수를 끄집어 내 주면 되는 것 이다.
아래 내용을 설명할 샘플 코드가 여기에 삽입 됩니다.
여기에 구조체를 선언함과 동시에 구조체 타입이름의 정의 없이 변수를 정의하는 방법에 대해 설명할 계획입니다.
아래 내용을 설명할 샘플 코드가 여기에 삽입 됩니다. 여기에 구조체 선언문 안에 다른 구조체 선언이 포함될 수 있다는 점을 설명합니다.
!포인터를 이용한 구조체 멤버 액세스 방법에 대해서는 포인터에서 설명할 것 입니다!
유니온 (Union)
나열형 데이터
포인터
포인터란 메모리 주소를 담는 변수다.
기본 포인터
배열 포인터
구조체 포인터
함수 포인터
확장된 포인터 개념
특별한 데이터 타입
데이터 타입 정의 방법
데이터 타입 정리 표
데이터 타입 연습
연산자
C 관점에서의 연산자
산술 연산자
비교 연산자
논리 연산자
비트 연산자
대입 연산자
단항 연산자
삼항 연산자
타입 캐스팅
나머지 연산자
저장방법의 지정
연산식과 연산자 우선순위
흐름 제어문
조건분기 제어
반복 제어
기타 제어문
각주 및 참고
- ↑ 바이트당 저장 비용은 하드 디스크가 가장 싸고, 그 다음이 메모리, 가장 비싼 저장 장치는 레지스터 입니다. (저장매체 / 용량)을 계산 하면 한 바이트 데이터를 저장하는데 드는 비용이 어느정도 되는지 계산이 될 것 입니다. 데이터를 처리하는 속도는 반대로 레지스터가 가장 빠르고, 그 다음이 메모리 마지막으로 하드디스크 순이 됩니다. 당연히 느린 장치에 저장되어 있는 데이터를 가능하면 빨리 사용할 수 있는 방법을 궁리하기 마련이고 많이 사용되는 방법중 하나가 캐쉬 입니다. 하드 디스크를 살때 18M 라는 등의 데이터가 있는 경우를 볼 수 있는데, 그 숫자가 하드디스크의 읽기/쓰기 속도를 향상하기 위해 마련된 캐쉬의 용량입니다. 데이터를 많이 읽고 쓰는 환경일 수 록 이 숫자가 클수록 더 빠르게 하드디스크가 동작 하겠지요. 마찬가지로 CPU에도 L1 캐쉬니 L2 캐쉬니 하는 설명이 붙어있는 것을 볼 수 있는데 이 캐쉬 역시 상대적으로 느린 램에서 빠른 CPU의 레지스터로 데이터를 보낼때 더 빨리 보내기 위해 읽어들이게 될 확률이 높다고 예상되는 데이터를 미리 옮겨두는 영역입니다.
- ↑ 메모리를 사용하는데 있어 몇가지 주의할 점이 있는데, 64K 메모리를 갖는 시스템이라고 해서 64K모두를 데이터를 저장하는 용도로 사용할 수 있는 것은 아닙니다. 일부 영역은 시스템을 관리하는데 필요한 기본적인 프로그램이나 읽기 전용이 데이터가 저장되어 있고, 이 영역은 보통 ROM 영역이라고 부릅니다. 또 다른 영역은 CPU와 램 이외의 외부장치(가장 쉽게 접할수 있는 외부장치의 예는 그래픽 카드입니다.)와의 통신을 위해 사용되는 경우도 있습니다. 이런 영역을 매핑(mapping)메모리라고 하는데, 해당 영역에 데이터를 써 넣거나, 데이터를 읽으면, 그 메모리 영역에 연결되어 있는 외부장치에 해당 데이터가 보내지거나, 그 외부장치가 관리하는 데이터를 읽어들이게 됩니다.
- ↑ 주: 기술적으로 볼 때, C 컴파일러가 바로 기계어로 바꾸는 것은 아니다. 실제로 C 컴파일러는 어셈블러로 변환을 하고, 변환이 끝난후 어셈블러가 기계어로 변환하는 작업을 하게 되는데, 실제 심볼 테이블을 생성하고 관리하는 쪽은 C 컴파일러 보다는 어셈블러 쪽이다. 또한 심볼 테이블에 저장되는 것은 변수나 상수 뿐 아니라 모든 식별자(identifier)가 저장된다.
- ↑ 유사하게, 의미 없이 루프를 돌려 시간을 벌고자 하는 경우 최적화된 코드에 의해 아무 의미 없게 되는 경우도 종종 있습니다. 대다수의 경우에 빈 루프는 백번을 돌리던 백만번을 돌리던 수행 시간의 차이는 없게 됩니다.
- ↑ 엄밀하게 말하면 정확한 설명은 아닙니다만 이해를 돕기 위해 대략적인 이야기만 썼습니다. 또한 부동소수형 표기법에 관한 자세한 내용을 알기 원한다면 표준 문서 IEC 60559:1989, Binary floating-point arithmetic for microprocessor systems (previously designated IEC 559:1989)를 찾아 보거나, IEEE 754와 관련된 내용을 인터넷에서 찾아 보시기 바랍니다.
- ↑ 아마도 이는 일본에서 건너온 잔재가 남아있는 것 이거나 오래전에 수학 교재들이 번역되어야 했던 시절에 선택된 단어가 그대로 사용되기 때문이 아닐까 합니다.
- ↑ 정수부와 소수부 모두를 합한 자리수를 의미하며, 유효자리수는 시스템마다, 컴파일러마다 다릅니다.
- ↑ 최소허용값이나 최대 허용값은 실제로 큰 의미는 없습니다.
- ↑ long double 타입은 시스템마다 정의된 자리수가 약간씩 다릅니다. 경우에 따라 double과 동일하게 8 바이트로 처리하는 경우도 있고, 10 바이트로 처리 하는 경우도 있고, 3배 정밀도 부동소수라 하여 12 바이트로 처리하는 경우도 있습니다.
- ↑ _Complex와 _Imaginary 타입은 C99에서 도입 되었습니다.
- ↑ joshuajh: 제 경우에는 아직 _Imagenary를 구현한 컴파일러를 확인한 적은 없습니다. 수학연산에 관한 관심은 대부분 정수쪽에 집중되어 있는 관계로 지금 당장 테스트 해 볼 수 없는, 이전에 사용했던 컴파일러들이 _Imaginary 타입을 구현 했는지 아닌지에 대해 아는바가 없습니다.
- ↑ 물론 실제적으로는 여러가지 이유에 의해서 두 배 정도가 아니라 몇배에서 수십배 이상의 속도 차이가 납니다. 그 위에 이 n 비트 컴퓨터라는 용어에 대한 수많은 의견이 있기 때문에 이렇게 한마디로 정의해 버리기엔 여러가지 불편한 점이 많기는 하지만, C 언어를 배우는데 있어 가장 필요한 정보만 제공한다는 의미에서 필요 이상으로 용어의 정의에 있어야 할 이야기들을 가지쳐 버렸습니다.
- ↑ 일반적으로 네트워크를 통해 데이터를 전송 할 때엔 빅 엔디언으로 전송을 합니다. 네트워크를 통해 전송할 때 사용해야 하는 바이트 오더라는 의미로 네트워크 프로그래밍을 할 때엔 빅 엔디언 이라는 용어 보다는 '네트워크 오더'라는 용어를 더 많이 사용합니다. 네트워크를 통해 데이터를 전송할 때엔 상대방 프로그램 외에도 중간에 거쳐야 하는 여러 네트워크 장비를 거쳐야 하기 때문에 오로지 자신이 만든 두 프로그램 사이에서 통신을 하는 경우에라도 데이터, 특히 네트워크 전송시 사용되는 패킷의 해더 정보는 반드시 네트워크 오더로 변환하는 작업을 해야만 합니다.
- ↑ 배열 자체의 크기나 차원에 대한 제한은 스펙상에 정의되어 있지 않습니다. 다시 말해서 C 언어 자체는 그러한 제한을 두고 있지 않습니다. 그러나 컴파일러의 구현이나 시스템의 한계등에 의해 어쩔 수 없이 제한 될 수 밖에 없습니다. 배열의 차원을 제한하는 컴파일러를 경험해 본 적은 없으나, 특수한 상황에서 제한할 가능성이 있습니다. 또한 배열의 전체 크기가 제한되는 경우는
- CPU의 특성 - 가장 대표적인 예로는 80286 CPU입니다. 어드레스 버스의 특성 때문에 배열의 최대 크기가 64k로 제한 되는 경우가 있었습니다.
- 메모리의 전체 크기
- 로컬 변수인 경우 스택의 크기
- ↑ 주: 실상은 한글이 안되는 이유는 많은 가능성이 복합적으로 작용하기 때문입니다. 이렇게 단순하게 말하면 어떤 사실에 대해 고지식 하게 접근하는 사람들은 할 말이 아주 많겠지만, 이해를 돕는다는 허울을 뒤집어 쓰고 여러 이유중 한 가지만 기술 하였습니다.
- ↑ 좀더 엄밀하게 따진다면 배열의 시작위치가 main함수 시작시에 레지스터에 할당되어 사용될 수 도 있을 것 입니다.
- ↑ 문자열 상수에 포함되어 있는 문자의 개수는 공백문자와 느낌표를 포함하여 12개의 문자인데도 13바이트라 한 것은 문자열 상수의 제일 마지막에는 항상 문자열의 끝임을 표시하는 널문자 ‘\0’가 포함되어 있기 때문에 이 문자를 포함 하였기 때문 입니다.
- ↑ str1 배열에 문자열을 넣는 작업을 하려면 strcpy(3)이나 memcpy(3)등의 함수를 이용하여 문자열 상수에 있는 각각의 문자 값을 배열의 대응위치로 복사하는 작업을 수행하여야 합니다. 여기에서 언급된 두 함수는 프로그래밍/C/기초_프로그래밍에서 다룰 것 입니다.
- ↑ 실제로 구조체와 유니온 이라는 개념은 객체지향 언어에서의 객체 개념이나 데이터의 다형성 개념의 뿌리가 되는 개념 이기도 합니다.
- ↑ 실제로 정수형, 실수형등의 데이터 타입을 기본형(base type)이라고 부르고 배열, 구조체, 유니온 등을 딜리버드 타입(delivered type;유도형 이라는 표현을 사용하는 경우도 있습니다.)혹은 사용자 정의 타입(user define type)이라고 부릅니다.