Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

변수

C에서 사용되는 문자들

인간이 쓰는 언어의 숫자만 해도 아마 100여개가 넘을거라고 생각된다. C(:12)언어는 기계어(:12)를 대신해서 사용할 수 있도록 인간의 언어로 쓰여진 프로그래밍 언어인데, 그렇다면 인간의 언어중 어떤 언어를 사용하고 있을까. 답은 영어다. 컴퓨터라는 기계가 영어문명권에서 발명이 되다보니, 컴퓨터에서 프로그램을 만들기 위해 사용한 C언어도 자연스럽게 영어 알파벳을 기본 문자로 사용하도록 만들어 졌다. - 한글로된 컴퓨터 언어를 사용해보자라고 해서 씨앗(:12)이라는 한글로 사용가능한 언어가 있었기는 했다. 당시에는 꽤 주목을 받기도 했었는데, 소리소문 없이 잊혀지고 말았다. 그때가 내가 대학 1학년 때인가? 되었던 듯 싶다. -

C에서 사용하는 문자들은 다음과 같다.
a b c d e f g h i j k l m n o p q r s t u v w x y z
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
0 1 2 3 4 5 6 7 8 9
! " # % & ' ( ) * + , - . /
: ; < = > ? [ \ ] ^ _ { | } ~
space, tab, newline, form feed
위의 문자셋은 C언어 뿐만 아니라. 다른 거의 대부분의 프로그래밍 언어에서도 기본으로 사용하고 있다.

2바이트 이상의 문자는 주석외에는 사용할 수 없는게 일반적이다.

변수와 상수

컴퓨터는 연산을 빠르게 하기 위한 기계로, 연속된 수 많은 연산들을 처리해 나간다. 연산에는 연산자피연산자가 필요하다는 것은 누구나 알고 있을 것이다.
    +-------------- 피연산자
    |
 +-------+
 |       |
 1   +   2
     |
     +-------------- 연산자
이제 연속된 연산, 예를들어서 1부터 100까지의 숫자를 더하는 연산을 해야한다고 가정해보자. 단순하게 1부터 100까지 더할경우 100번의 덧셈이 필요하게 되는데, 이를 위해서는 각각의 연산결과를 어딘가에 저장을 해야 한다. 이런경우 암산능력이 좋은 사람은 머리에 연산결과를 기억할 것이고, 그렇지 못한 사람은 종이등을 이용하게 될 것이다.

컴퓨터는 메모리공간을 이용한다. 위의 1부터 100까지 더하는 연산을 할경우 매번 메모리의 저장장소에 연산결과를 넣었다 뺐다하는 일을 한다. 이런 컴퓨터의 메모리 공간에는 일련의 숫자로된 주소가 메겨져 있다. 여러분의 컴퓨터의 메모리가 2 Gigabyte(:12)라면, 0부터 2147483647까지의 번호가 매겨져 있고, 이중 사용하지 않는 곳의 주소의 번호를 알아내어서 그곳에 연산결과를 저장하는 식이다.

여러분이 연산결과를 저장하기 위해서 2184912 번째 주소를 사용해야 한다라고 가정해보자. 기억해서 사용하기가 그렇게 만만치 않을 것이다. 그래서 사용하기 쉽게, 숫자대신 알파벳문자를 사용해서 해당 주소를 대신 가리키게 된다. 이때 연산의 결과로 저장되는 값을 변수라고 하고, 변수가 저장될 공간을 쉽게 사용하기 위해서 영어로 명명한 것을 변수명 이라고 한다.

변하는 수가 있으면 변치 않는 수가 있을 것이다. 이를 상수라고 하며, 마찬가지로 상수명을 이용해서 상수가 저장될 메모리 공간을 가리키게 된다.

              +----------<--------- sum = 1+2
              |
 +-----------+---+---------+
 | 메모리    |   |         |
 +-----------+---+---------+
위의 그림에서 sum이 변수명이 되고, sum에는 연산의 결과인 3이 저장된다.

간단하게 1+10을 더하는 프로그램을 만들어 보자.
#include <stdio.h>

int main()
{
	int a, b;
	int sum;

	a = 1;
	b = 10;
	sum = a+b; 
	printf("%d\n", sum);
}
아직 C문법을 공부하진 못했지만, 대략 이해하는데 어려움이 없을 것이다.
  1. a = 1 : 변수 a가 가리키는 곳에 1을 저장하고
  2. b = 10 : 변수 b가 가리키는 곳에 10을 저장한다.
  3. sum = a+b : 변수 a가 가리키는 곳의 값 1과 변수 b가 가리키는 곳에 저장된 값 10을 가져와서 더하고, 그 결과값인 11을 sum에 저장한다.
  4. 변수 sum이 가리키는 곳에 저장된 값 11을 가져오고 printf라는 함수를 이용해서 화면에 출력한다.

명명 규칙

변수명과 상수명은 알파벳 영문자와 소문자, 숫자,_가 사용될 수 있다. 또한 첫글자는 반드시 '영문소문자','대문자', '_'여야 한다.
변수명 허용여부 설명
sum1 O
Sum_2 O
_sum O
1sum X 첫머리에 숫자가 왔다
sum 1 X 공백이 존재하면 안된다
sum!2 X 특수문자는 사용될 수 없다

데이터 형과 크기

기본 데이터 형

인간과 달리 사물을 추상화 시켜서 생각할 수 있는 능력이 없는 컴퓨터이다 보니, 데이터가 어떤 종류의 것이며 (데이터형), 어느정도의 크기 (데이터 크기)를 가지고 있는지를 명확히 알려줘야 할 필요가 있다.

인간은 "천원은 큰돈이다"와 "1000+2000=3000" 에서 문자열 "천"과 숫자 "1000"을 구분해 낼 수 있지만 컴퓨터는 도대체 구분해 낼 수가 없기 때문이다.

때문에 모든 언어는 고유의 크기를 가지는 데이터 형(Type)을 제공하고 있다. C언어는 다음과 같은 크기를 가지는 데이터 형이 준비되어 있다.
데이터 형 크기 설명
int 4byte 정수형 숫자
char 1byte 단일 문자
float 4byte 실수
double 8byte 실수
예를 들어 데이터타입이 inta라는 변수가 있다고 가정했을 때, 여기에 저장할 수 있는 데이터의 크기는 2^32 이니 0 - 4294967295이 될거라고 생각할 수 있다. 그러나 실제로는 음수를 표현해야 하기 때문에 -2147483648 - 2147483647사이의 값을 저장할 수 있다.

char 형은 하나의 문자를 저장하기 위해서 사용하는데, 2^8에서 음수를 표현해야 하므로 -127 - 128의 값을 사용할 수 있다.

float,과 double 형은 소숫점을 가지는 실수를 표현하기 위해서 사용한다.

signed 와 unsigned형

정수에는 음의 정수와 양의 정수가 있다. 그렇다면 컴퓨터에서 어떻게 음수와 양수가 처리되는지에 대해서 알아보도록 하자. 정수를 저장하기 위한 데이터형은 4byte의 크기를 가지는 int 형이다. 이것을 컴퓨터 메모리 상에서 보자면 다음과 같은 모습을 하고 있을 것이다.
  |<----------- 32 --------->|
   31              ...   43210
  +---------------    -------+
  |                ...       |
  +----------------   -------+
int형 변수 a에 1을 저장했다면, 0번째 비트에 1이, 2를 저장했다면 1번째 비트에 1, 2번째 비트에 0이 저장될 것이다. -1을 저장하고 싶다면 ? 이러한 음의 값의 표시를 위해서 마지막 비트를 따로 남겨 두었다. 즉 31번째 비트에 1이 있으면 음수, 0이 있으면 양수라고 약속을 한 것이다. 다음은 int형 정수가 메모리 상에 어떻게 저장되는지를 나타내고 있다.
  • 2 -> 00000000 0000000 00000000 00000010
  • 1 -> 00000000 0000000 00000000 00000001
  • 0 -> 00000000 0000000 00000000 00000000
여기에서 -가 되면 아래와 같이 표현된다.
  • -1 : 11111111 11111111 11111111 11111111
  • -2 : 11111111 11111111 11111111 11111110
  • -3 : 11111111 11111111 11111111 11111101
이렇게 해보면 양의 정수로 가장큰 수와 음의 정수중 가장 작은 수는 아래와 같음을 계산할 수 있다.
  • 가장 큰 양의 정수 : 011111111 11111111 11111111 11111111 (2147483647)
  • 가장 작은 양의 정수 : 100000000 00000000 00000000 00000000 (-2147483648)
약간 혼란스럽기는 하겠지만, 이해하는데 크게 어려움은 없을 것이다.

이렇게 해서 음의 수를 표현하긴 했는데, 대신에 저장가능한 숫자의 크기가 2^32 에서 2^31으로 2배만큼 줄어듬을 알 수 있다. 그렇다면 음수를 사용할 필요가 없을 경우 굳이 마지막 비트를 음수인지 양수인지를 판단하기 위해서 낭비할 필요가 없을 것이다.

이를 위해서 unsigned가 제공된다. 부호없는 뜻으로 해석하면 되며, 기본 자료형앞에 써주기만 하면 된다. unsigned int부호없는 정수를 저장하기 위한 데이터형이다. 동일하게 unsigned char, unsigned double 등의 자료형을 사용할 수 있다. unsigned가 생략될 경우 signed가 적용된다.

100000000 00000000 00000000 00000000가 주어질 경우 signed int 에서는 -2147483648이던 것이 unsigned int 에서는 2147483648이 되는 식이다.

다음은 signed와 unsigned를 이해하기 위한 간단한 C 코드다.
#include <stdio.h>
#include <string.h>

int main()
{
  int a = -2147483648;
  printf("%d\n", a);
  printf("%u\n", a);
  printf("================\n");
  a = a + 1;
  printf("%d\n", a);
  printf("%u\n", a);
}
아래와 같이 컴파일 시키고 결과까지 확인해 보도록 하자.
# gcc -o signed signed.c
# ./signed
-2147483648
2147483648
================
-2147483647
2147483649
printf 함수는 값을 포맷에 맞추어 화면에 출력시키기 위해서 사용하는 함수다. %d는 주어진 인자를 signed int 형으로 출력하라는 포맷옵션이고, %uunsigned 형으로 출력하라는 포맷옵션이다.

상수

변수는 말그대로 변하는 수이고, 상수는 말그대로 변하지 않는 수이다.

사용자의 나이를 입력받아서 어떤 일을 하는 프로그램을 만들어야 하는 경우를 생각해보자. 대부분의 사용자가 정상적으로 입력하겠지만, 900, 28129315 와 같은 터무니 없는 수를 입력하는 경우도 발생할 것이다. 이런 문제는 입력가능한 최대값을 정해놓고 비교하는 것으로 해결가능 할것이다.

아래는 컴파일 후 실행가능한 예제 프로그램이다. 이 프로그램은 사용자의 입력을 숫자로 변환한다음 250을 초과하는지를 검사한다.
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
	int age;                         // ** 변수 age의 선언
	char buf[12];                    // 사용자의 입력을 저장할 버퍼
	const int maxage=250;            // ** 상수 maxage의 선언

  age = 0;                         // age의 기본값으로 0을 입력한다.
	printf("Your Age is ? ");
	fgets(buf, 11, stdin);           // 표준입력(:12)으로 입력을 받아들여서 buf에 저장한다.
	age = atoi(buf);                 // 읽어들인 값을 int 정수형으로 변환한다.

	if (age > maxage)                // age와 maxage를 비교한다. 
	{
		printf("Are you crazy ? \n");
		printf("Max age is %d\n", maxage);
		return 1;
	}
	printf("OK your age is %d\n", age);
	return 0;
}
상수는 const 키워드를 이름 앞에 붙이면된다. 예를 들어 문자열 상수를 선언하기를 원한다면, const char *name = "yundream" 하면 된다. const가 일단 붙으면, 선언할때에만 값을 넣어줄 수 있으며, 중간에 값을 변경할 수 없다. age= atoi(buf) 다음줄에 maxage=180를 넣어보기 바란다. 실행은 커녕 컴파일조차 안될 것이다.
# gcc -o input input.c
input.c: In function ‘main’:
input.c:13: error: assignment of read-only variable ‘maxage’

상수를 사용하는 이유는 뭘까 ? 상수는 권한을 정함으로써, 프로그래머의 실수를 미연에 방지하고자 할 때 유용하게 사용할 수 있다. 위의 예에서와 같이 maxage는 고정된 값으로 프로그램 여기저기에서 사용될 수 있을 것이다. 만약 이게 변수라면, 프로그래머가 실수로, 한쪽에서는 150, 다른 함수에서는 100 으로 입력하는 등의 문제가 발생할 수 있을 것이고, 이는 프로그램의 심각한 논리적 버그가 될 수 있다. const를 이용해서 변수를 상수로 선언함으로써 이러한 문제를 예방할 수 있다.

변수의 선언과 정의

엄격히 하자면 선언이름만 만드는 것이고, 정의는 선언된 이름에 을 주는 행위다.
  1. int i; // int형 변수 i를 선언하다.
  2. int i=a; // int형 변수 i를 정의하다.
그러나 C에서 변수는 선언과 동시에 정의가 이루어 지므로 차이가 없으므로, 변수 x를 선언했다는 의미는 변수에 대한 선언과 정의까지를 의미한다. C 컴파일러가 int i를 선언함과 동시에 4byte의 메모리 공간을 할당하고, 그 메모리에 있던 값을 사용하게 되기 때문이다. 메모리에 있던 값이 어떤 값인데? 물론 그것은 알 도리가 없다. 프로그램의 실행시 할당된 4byte의 메모리에 어떤값이 있느냐에 따라 달라지기 때문이다. 아래의 프로그램을 연속해서 실행시켜 보기 바란다.
#include <stdio.h>

int main()
{
  int i;
  printf("%d\n", i);
}
실행할 때 마다 값이 달라지는 것을 알 수 있을 것이다. 이렇게 변수를 선언할 경우 어떤 값이 들어 있을지 알 수 없으므로, 선언과 동시에 값을 입력해주는 초기화작업이 필요하다. 초기화 작업을 하지 않을 경우 심각한 문제를 가진 프로그램이 만들어 질 수 있기 때문이다. <!> 아래의 예제는 좀 더 쉬운 예제로 변경할 필요가 있음..
#include <stdio.h>

int main()
{
  int i = 0;
  int k;
  while(k <= 100)
  {
    i = i+k;
    k++;
  }
  printf("%d\n", i);
}
1부터 100까지의 모든 수를 더하는 프로그램을 만드는게 목적이였지만, k의 값을 초기화 해주지 않은 이유로 전혀 엉뚱한 결과가 출력이 될 것이다. 아래와 같은 방법으로 선언과 동시에 초기화 하거나 혹은 사용하기 전에 초기화 해주는 센스가 필요하다.
  int k = 0;
  혹은
  int k;
  k = 0;
  while(i< 100)

이미 몇개의 셈플 프로그램의 코드를 (비록 완전히 이해 하지 못했겠지만)보아 왔으므로, 어떻게 변수를 선언해야 하는지에 대한 감은 잡고 있으리라 생각된다. 몇개의 예를 더 드는 것으로 설명을 마치도록 하겠다.
  1. char a : 하나의 문자를 저장하기 위한 문자형 변수 a의 선언
  2. long a : long형 변수 a의 선언
  3. char *a : 메모리의 주소정보를 저장하기 위한 포인터 변수 a의 선언
    • 포인터는 나중에 따로 다룰 것이다.
  4. 연산자

    연산자는 단일 문자혹은 단어로 내부함수를 사용할 수 있도록 한다. 우리는 이미 몇개의 예제 프로그램을 통해서 사칙연산을 위한 연산자, 비교연산자를 사용했었다. 이러한 연산자는 결과를 출력하기 위해서 하나이상의 피연산자를 필요로 한다. 사칙연산자라면 2개의 피연산자를 필요로 할 것이다. 예를 들어 덧셈을 위한 연산자인 +를 이용해서 4 + 5를 했다면 2개의 피연산자를 더한 결과로 9를 얻게 될 것이다.

    C는 많은 수의 연산자를 제공하는데, 대략 3개의 큰 카테고리로 분류할 수 있다.
    • 대입 연산자 : 변수에 값을 대입하기 위한 연산자
    • 산술 연산자 : +, -, *, / 와 같은 수치계산을 위한 연산자
    • 비교 연산자 : >, =, < 와 같은 비교를 위한 연산자
    • 기타 연산자 : 논리 연산자, 비트 연산자, 캐스트 연산자
    이들 연산자는 카테고리 별로 자세히 살펴보도록 하겠다.

    대입 연산자

    +,- 혹은 함수의 계산에 의해서 만들어진 결과는 변수에 저장이 되어야 한다. 이렇게 어떤 결과값을 다른 변수에 저장하기 위해 사용하는 연산자가 대입 연산자이다. 대입연산자는 =를 사용하며, 하나의 피연산자만을 가진다.

    아래에 대입연산자를 사용하는 방법이 나왔다.
    int eng, kor, math, total; 
    
    eng = 85;
    kor = 76;
    math = 80;
    
    total = eng+kor+math;
    대입연산자는 =를 기준으로 오른쪽의 값을 왼쪽의 변수에 밀어넣는다. 오른쪽의 값을 변수에 저장한다고 이해하면 된다. 위 코드에서 eng변수에 85가 저장되었음을 알 수 있다.

    왼쪽에는 반드시 변수가 와야 한다. 아래는 잘못된 코드다
    5 = 2 + 3;

    일반 수치연산자

    가장 기본적인 연산을 위해서 사용된다. 사칙연산을 위한 연산자가 대표적이다. 다음은 C언어에서 지원하는 일반 수치연산자들이다. 이들 연산자는 C뿐만 아닌 다른 모든 언어에서 공통적으로 찾아볼 수 있다.
    • + : 덧셈
    • - : 뺄셈
    • / : 나눗셈
    • * : 곱셈
    • % : 나머지값
    +-는 역수를 만들기 위한 목적으로도 사용된다.
    a = 5;
    b = -a;
    // b에는 -5가 대입된다. 

    논리 연산자

    논리 연산자는 또는 거짓을 판별하기 위해서 사용된다. C는 다음과 같은 연산자를 지원한다.
    • && : AND (모두 참일 때 참)
    • || : OR (하나라도 참이면 참)
    • ! : NOT (참이면 거짓, 거짓이면 참)
    아래의 경우 ab보다 크고, bc보다 큰 조건이 모두 만족할 때 참이 된다.
    ( a > b) && ( b > c)

    아래는 약간 더 복잡한 경우다.
    (a > b) || !(a > c)
    a가 b보다 더크거나 혹은 a 가 c보다 크지 않다면 참이 된다.

    증감 연산자

    C에서는 ++--라는 특수한 형태의 연산자를 제공한다. 이들은 각각 증가연산자감소 연산자라고 불리운다.
    • ++ : 변수에 1을 더한다
    • -- : 변수에서 1을 뺀다.
    a = a + 1a++와 동일한 결과를 보여준다. 또한 ++a와도 동일한다. 코드의 양을 줄여서 가독성을 높이기 위한 목적으로 주로 사용된다- 증감연산자를 사용한다고 해서 반드시 가독성이 좋아지는 건 아니긴 하다 -. 이들 증감연산자는 포인터의 위치를 증가하거나 감소하기 위한 목적으로도 사용할 수 있다. 포인터는 나중에 다루게 될 것이다.

    <!> 엄격히 말하자면 a++++a는 사용되는 코드에 따라서 다른 결과를 보여줄 수도 있는데, 이는 나중에 언급하도록 하겠다.

    비트 연산자

    컴퓨터는 0-9를 사용하는 인간과 달리 0과 1로된 비트를 이용해서 계산을 한다는 것은 다들 알고 있을 것이다. 그러하다 보니 컴퓨터를 이용해서 정보를 제대로 다루기 위해서는 비트를 제대로 이해하고 다루는게 매우 중요하게 된다. 비트 연산자는 비트를 다루기 위한 목적으로 사용한다.

    우리가 일반적으로 사용하는 x86컴퓨터의 경우 byte를 기본단위로 사용하게 된다. 때문에 비트연산자를 통해서 비트연산을 할 때에도 byte단위로 연산하게 된다. C는 다음과 같은 비트연산자를 준비하고 있다.
    a & b 비트단위 AND AND 연산자
    a | b 비트단위 OR OR 연산자
    a ^ b 비트단위 exclusive (XOR)
    a << b 왼쪽으로 비트를 이동 쉬프트 연산자
    a >> b 오른쪽으로 비트를 이동
    ~ a NOT
    & 연산자는 간단하다. 십진수 10와 14가 있을경우 이 두 수를 & 연산하면 다음과 같은 결과를 보여줄 것이다. |연산은 굳이 설명하지 않도록 하겠다.
     10      00000000 00000000 00000000 00001010
     14      00000000 00000000 00000000 00001110
    --------------------------------------------
            00000000 00000000 00000000 00001010   10 

    ^는 둘중 하나만 참이여야 참이된다.
    • 0 ^ 0 = 0
    • 0 ^ 1 = 1
    • 1 ^ 0 = 1
    • 1 ^ 1 = 0
    쉬프트 연산자 중 << 는 비트를 왼쪽으로 이동시킨다. 왼쪽으로 이동시키면 오른쪽이 남게 될건데, 남은 자리는 0으로 채워진다. 6<< 1하면 12가 될것이다.
        00000000 00000000 00000000 00000110
        00000000 00000000 00000000 00001100 

    >>연산자는 오른쪽으로 이동시키는데, 주의할 필요가 있다. 오른쪽으로 이동시키면 왼쪽 비트가 남게 될건데, 이때 남는 비트는 오른쪽에 있던 비트로 채워지게 된다는 점이다. 아래의 예를 보자.
        10000000 00000000 00000000 00000000 이것을 >> 1하면
        11000000 00000000 00000000 00000000 이렇게 된다. 

    ~연산자는 0을 1로, 1을 0으로 변경한다. 만약 int 데이터 타입인 1에 대해서 ~ 해줄경우 -2가 될것이다. 왜냐하면
        00000000 00000000 00000000 00000001
        11111111 11111111 11111111 11111110  ~
    이 되는데, 2의 보수 연산에 의해서 -1로 재현된다. 2의 보수에 대해서는 리눅스환경에서의 C 프로그래밍 5장 데이터 다루기를 참고한다.

    연산자의 사용

    다음은 지금까지 배운 연산자를 활용한 간단한 프로그램이다.

    #include <stdio.h>
    
    int main()
    {
    	int my_int;
    	printf("일반 수치연산 : \n\n");
    	my_int = 6;				// 대입
    	printf("my_int = %d, -my_int = %d\n", my_int, -my_int);
    	
    	printf("int 1+2 = %d\n", 1 + 2);
    	printf("int 5-1 = %d\n", 5 - 1);
    	printf("int 5*1 = %d\n", 5 * 1);
    	printf("int 5/2 = %d\n", 5 / 2);
    	printf("int 5/2 = %d\n", 5 % 2);
    
    	printf("double 9/4 = %f\n", 9.0 / 4.0); 
    
    }
    프로그램의 실행결과가 어떠할런지 예상하는건 어렵지 않을 것이다. 컴파일 후 실행시켜서 예상한 결과가 나왔는지 확인해 보도록 하자.

    연산자 우선순위

    연산자 우선순위는 여러개의 연산자로 이루어진 라인 코드가 있을 경우, 어떤 순서로 처리할 것인지를 결정하기 위해서 사용된다. 다음은 2+5*4 의 연산결과를 출력하는 코드다.
    #include <stdio.h>
    
    int main()
    {
      printf("%d\n", 2 + 5 * 4);
    }
    언뜻 새각하기에 28이라는 결과가 나올 거라고 예상할 수 있지만, C는 나름대로의 연산자 우선순위에 따라서 *연산을 +연산보다 먼저하게 된다. 그래서 22라는 결과가 나오게 된다. 즉 위의 코드의 경우 C는 ((5*4) + 2)로 해석해서 계산을 한다. 눈치챘겠지만 연산순위를 무시하거나 잘못 이해할 경우 전혀 엉뚱한 잘못된 프로그램을 만들어 낼 수 있다.

    다음은 C에서 적용되는 연산자 우선순위다. 참고로 결합성은 우선순위가 같은 연산자들이 여럿있을 경우 어느방향으로 처리할 건지를 결정하기 위해 사용된다. 좌->우는 왼쪽에서 먼저, 우->좌는 오른쪽에서 먼저 계산한다는 걸 의미한다.
    순위 연산자 결합성
    1 (), [], -> . 좌-> 우
    2 !, -, ++, --, +(단항), -(단항), *(포인터), &, sizeof 우->좌
    3 *, /, % 좌->우
    4 +, - 좌->우
    5 <<, >> 좌->우
    6 <, <=, >, >= 좌->우
    7 ==, != 좌->우
    8 &(비트연산자) 좌->우
    9 ~ 좌->우
    10 | 좌->우
    11 && 좌->우
    12 | | 좌->우
    13 ?: 좌->우
    14 , 우->좌
    위의 우선순위를 이해했다면, 아래의 좀 복잡해 보이는 코드가 어떻게 계산될지 예상할 수 있을 것이다.
    a=10*3-40/20*12+20%-2
    위의 코드는 다음고 같이 계산된다.
    a=[(10*3)-{(40/20)*15}]+{20%(-2)}

    그러나 연산순위를 이해하고 있다고 하더라도 많은 연산이 들어가는 코드를 작성하다 보면 필연적으로 실수를 하게 된다. 게다가 코드의 가독성도 극적으로 떨어진다. a=10*3-40/20*12+20%-2가 무슨일을 하는지 한눈에 이해하기란 쉬운일이 아니다. 그러니 연산순위같은 것에 신경쓰지말고 괄호 ()를 이용해서 직접 우선순위를 정하는 방법을 사용하도록 한다. a=10*3-40/20*12+20%-2를 괄호를 이용하면 다음과 같이 재 작성할 수 있다. 실수도 막아줄 뿐더러, 이해하기도 훨씬 쉽다는걸 느낄 것이다.
    a = ( (10*3) - ((40/20)*15) ) + ( 20%(-2) )

    define 문의 사용

    execl과 같은 표계산 프로그램을 사용해봤다면, 매크로라는 기능에 대해서 알고 있을 것이다. 매크로라는 것은 어떤 값이나 계산의 공식을 미리 정의(define)를 해두는 것을 말한다. 이렇게 정의된 매크로는 표계산 본문에 그대로 사용할 수 있으며, 매크로에 정의된 그 기능을 그대로 수행하게 된다. 달리 말하자면, 표계산 본문을 읽어가다가 매크로에 선언해둔 문자열을 만나면, 정의된 값이나 공식으로 풀어서 쓰는 것으로 해석할 수 있다.

    이것은 많은 장점을 가져다 준다. 자주 사용하는 기능을 매크로로 만들어두면, 매번 작성할 필요 없이 간단하게 매크로만 가져다 사용할 수 있다. 함수를 사용할 때와 마찬가지의 편리함이라고 볼 수 있다. 이경우 함수와는 다른 점이 있는데, 이에 대해서는 나중에 설명하도록 하겠다. 또다른 사용처는 상수처럼 사용할 수 있다는 점이다.

    define 문법

    define는 다음과 같은 방식으로 사용할 수 있다.
    #define NAME VALUE

    앞에서 define는 상수처럼 사용할 수 있다고 배웠다. 지금 나이를 입력하는 프로그램을 작성한다고 가정해 보자. 이때 나이의 한계치를 정해서, 이 값을 초과하는 경우 에러처리를 하도록 하고 싶다면, 다음과 같이 상수를 정의해서 사용할 수 있을 것이다.
    const int MAX_AGE 250;
    define 문을 이용해서 동일한 일을 하도록 작성할 수 있다.
    #define MAX_AGE 250

    뿐만 아니라 함수처럼 사용하도록 할수 있다. 다음은 덧셈을 하는 과정을 define 문으로 작성한 예이다.
    #define add(a,b) (a+b)
    
    int main()
    {
        printf("%d\n", add(1,2));
    }

    define 문에 대해서 생각해보자

    위에서 define 문을 사용한 것을 보면, 함수의 용도로써 혹은 상수를 정의하기 위한 용도로써 사용할 수 있음을 알 수 있다. 언뜻 보기엔 동일해 보이지만 몇가지 차이점이 있다.

    이 차이점을 알아보기 위해서는 C에서 define문을 어떻게 다루는지에 대해서 이해를 해야 한다.

    C로 만들어진 코드가 실행가능한 파일이 되기 위해서는 몇가지의 과정을 거쳐야 된다는 것을 알고 있을 것이다. 복습차원에서 과정을 요약해 보았다. 대략적인 설명은 3장을 읽어보기 바란다.
    1. precomile
    2. Assembly 코드 생성
    3. Object 파일 생성
    4. linker
    5. 실행파일
    여기에서 precompile를 주목할 필요가 있다. precompile는 컴파일 전단계로 인간이 해석하기 쉽게 되어 있는 코드를 컴파일러가 더 해석하기 좋은 형태로 풀어쓰게 된다. 여기에서 define문으로 정의된 값들이 모두 코드 형태로 풀어써지게 된다. 예를 들어 다음과 같은 코드가 있다고 가정해보자. 아래는 컴파일 가능한 완전한 코드는 아니다. precomile 과정에서 define 문이 어떻게 처리되는지를 설명하기 위한 프로시져(:12) 코드다.
    #define add(a,b) (a+b);
    #define MAX_NUM 250
    #define MIN_NUM 0
    int main()
    {
        int a, b;
        a = add(1,2);
        b = add(51,88);
    
        if (a < MIN_NUM)
        {
            ....
        }
        if ( b > MAX_NUM)
        {
        }
        
    }

    위의 코드는 precompile 과정을 거치게 되면, 코드에 등장하는 모든 define값이 정의된 코드로 풀어써지게 된다.
    int main()
    {
       a = 1 + 2;
       b = 51 + 88;
       if (a < 0)
       {
          ...
       }
       if (b > 250)
       {
           ...
       }
    }

    define 문이 어떻게 처리되는지 이해가 되었을 것이다.

    define 문과 함수의 차이

    위의 코드에서 우리는 #define 문을 이용해서 덧셈을 하는 매크로 함수를 정의해서 사용했다. 그렇다면, 아래와 같이 만들어진 함수와 무슨 차이가 있는지가 궁금해질 것이다.
    int add(int a, int b)
    {
        return a+b;
    }

    define 문으로 정의된 것들은 precompile 과정을 거치면서 원래 코드로 치환된다는 점을 배웠다. 프로그램에 코드가 완전히 박혀 버리는 거라고 생각하면 된다. 100개의 add 매크로를 사용했다면, 코드에 100개 만큼이 복사되어서 들어간다. 반면 함수는 하나의 원본코드가 존재하고, 필요할때 마다 호출해서 실행시키는 방법을 사용한다.

    여기에서 다음과 같은 차이점이 생김을 알 수 있다.
    1. define문은 define값이 쓰여진 모든 곳에 코드가 박힌다. 당연히 실행파일의 크기가 커질 것이다.
    2. 반면 함수보다 더 빠르게 실행된다. 코드가 직접 박혀있기 때문인데 반해, 함수의 경우 함수를 호출하기 위한 시간이 소비되기 때문이다.

    define 문과 const 상수의 차이

    define 문과 함수의 차이점과 비슷하다. define 문을 이용하면, precompile 과정을 거치면서 변수가 아닌 값이 직접 코드에 박혀버린다. 반면 상수는 값을 가진게 아닌, 값이 저장된 주소의 값을 가진 것이므로, 해당 주소의 값을 읽어오는 과정을 거치게 된다. 일반변수와 근본적으로 동일한 과정을 거친다. 단 const 라고 명시함으로써, 쓰기가 불가능하도록 제한하는 것일 뿐이다.

    역시 define 문을 사용하면 상수를 사용하는 것보다 속도에 있어서 약간의 이득을 얻을 수 있을 것이다. 물론 이러한 이득을 노리고 define 문을 써야하는 경우는 생기지 않겠지만 말이다.

    단 define문은 상수를 선언하는 것과는 달리 타입을 따로 지정할 수 없다는 단점을 가지고 있다. 간단하게 사용할 수 있기는 하지만 #define MAX_NUM 100 이라고 되어 있을때, 이게 unsigned 형인지, long long 형, short 형 인지를 명확히 알 수 없게 된다. 이러한 점은 타입을 엄격히 해야 하는 프로그램에 잠재적인 문제요소로 작용할 수 있다.

    메크로로써의 define

    define는 메크로의 특징을 가진다. 이는 그 define문이 프로그램처럼 작동하게끔 만들 수 있음을 의미한다. 이러한 기능은 특히 서로 다른 운영체제에서 작동해야 하는 프로그램을 작성하고자 할때 유용하게 사용할 수 있다. 다음의 코드를 보자.
        FILE *fp = NULL;
        fp = fopen("/home/test/test.txt", "rt");
        ....
        fread( buffer, size, 1, fp );
        fclose(fp);
    이 프로그램은 파일을 읽어들여서, 그 내용을 출력하는 일을 한다. 위 코드는 리눅스상에서 사용하는데 전혀 문제가 없을 것이다. 그러나 윈도우에서 사용할 경우 문제가 될것이다. 윈도우와 리눅스는 파일의 디렉토리 경로를 정하는 규칙이 서로 다르기 때문이다. 윈도우는 /home/test/test.txt라는 경로를 인식할 수가 없으므로, 위 프로그램은 윈도우에서는 제대로 기능을 할 수가 없을 것다.

    제일 간단한 방법은 다음과 같이 윈도우만을 위한 전용의 프로그램을 작성하는 것이다.
        FILE *fp = NULL;
        fp = fopen("c:\\tmp\\test.txt", "rt");
        ....
        fread( buffer, size, 1, fp );
        fclose(fp);
    간단하긴 하지만 코드가 2개로 분리가 됨으로써 유지보수가 까다로와진다는 문제가 발생한다. define 문을 이용하면 이러한 문제를 해결할 수 있다. 아래는 윈도우와 리눅스 모두에서 사용가능하도록 재작성된 코드다.
    #define LINUX 
        FILE *fp = NULL;
    #ifdef LINUX
        fp = fopen("/home/test/test.txt", "rt");
    #else
        fp = fopen("c:\\tmp\\test.txt", "rt" ); 
    #endif
    
        fread( buffer, size, 1, fp );
        fclose(fp);
    ifdef 는 뒤에 오는 값이 define을 통해서 정의되어 있는지를 확인한다. 앞서 LINUX가 정의되어 있기 때문에, 참이되고 밑에 있는 fopen("/home/test/test.txt","rt")가 코드에 남게 된다. 만약 이 프로그램을 윈도우에서 사용할 수 있도록 하고 싶다면 #define LINUX를 제거하면 된다. 그러면 #ifdef LINUX를 만족하지 못하게 되고 fp = fopen("c:\\tmp\\test.txt", "rt" ); 가 대신 코드에 남게 됨으로써, 윈도우에서 문제없이 실행가능하게 될 것이다.

    리눅스는 유닉스와 비슷한 개발환경을 제공하는 이유로, 리눅스에서 만들어진 프로그램을 다른 유닉스로 옮겨야 하는 요구가 흔히 발생한다. 실제 대부분의 공개소프트웨어들이 리눅스 뿐만 아니라 다른 유닉스에서도 동일하게 작동하게 작성되어 있다. define 문은 이렇게 여러개의 운영체제에서 사용되어야 하는 프로그램을 작성하고자 할때, 거의 필수적으로 사용된다.