본문 바로가기
모던 C

레벨0 만남 - 02 프로그램의 핵심 구조

by 왕초보 독학 코딩 2024. 2. 3.

이 포스트는 '모던 C'를 요약한 내용입니다.

 

이 장에서 다루는 내용

  • C 언어 문법
  • 식별자 선언하기
  • 객체 정의하기
  • 컴파일러에게 지시하기

 

실전에서 다룰 프로그램은 앞 장의 예제 1-1보다 훨씬 다양하고 복잡하게 구성되겠지만, 기본 골격은 거의 비슷하다.

 

C 프로그램에서 고려할 두 가지 관점이 있는데, 하나는 컴파일러가 이해할 수 있도록 프로그램을 작성하는 구문 관점이고, 다른 하나는 우리가 의도한 대로 작동하도록 프로그램을 작성하는 의미 관점이다. 이 장에서는 구문 관점과 의미 관점의 세 가지 핵심 요소인 선언, 정의, 문장에 대해 살펴본다.

 

2.1 문법

C 프로그램의 구조를 보면 특정한 문법에 따라 여러 텍스트 요소가 엮인 것을 알 수 있다.

 

텍스트 요소는 다음과 같다.

  • 특수어 : #include, int, void, double, for, return과 같은 특수어가 있다. 특수어는 C 언어에서 특별히 정의되어 변하지 않는 개념이나 기능을 가리킨다.
  • 구두점 : C 언어는 프로그램 구조와 관련된 구두점을 다양하게 제공한다. 구두점은 괄호와 구분자가 있으며, 코드의 경계를 구분하기 위해서 사용된다.
    • 괄호의 종류는 {...}, (...), [...], /*...*/, <...>로 다섯 가지가 있다. 괄호는 특정 부분을 한데 묶으며, 항상 여는 괄호와 닫는 괄호가 짝을 이뤄야 한다. 그중 <...>는 논리적으로 동일한 문장의 한 줄 안에서 사용한다. 나머지 네 가지 괄호는 한 줄뿐만 아니라 여라 줄에 걸쳐 사용할 수 있다.
    • 구분자는 콤마(,)와 세미콜론(;)이 있다. 
  • 주석 : /*...*/ 구문은 그 안에 담긴 내용이 주석임을 컴파일러에게 알려 준다. 컴파일러는 주석을 무시한다. 주석은 코드에 대한 설명을 담고 문서화하는 데 가장 적합하다. //로 시작하는 주석도 사용할 수 있다. //부터 그 줄 끝까지 주석으로 처리된다.
  • 리터럴 : 예제 1-1에서 0, 1, 3, 2.0, 3.E+24, .0007이나 "element %zu is %g, \tits square is %g\n" 같은 공정된 값을 리터럴이라 부른다.
  • 식별자 : 식별자는 이름과 같은 성격을 가진다. 예제 1-1에서 A, i, main, printf, size_t, EXIT_SUCCESS가 여기에 해당한다. 식별자는 다양한 역할을 한다.
    • 데이터 오브젝트 : A와 i가 여기에 해당하며, 변수라고도 부른다.
    • 타입 또는 타입 앨리어스 : 새로운 오브젝트의 종류를 지정한다. size_t가 여기에 해당한다. 여기서 _t는 타입을 가리키는 식별자 뒤에 붙이는 C 표준에서 흔히 사용하는 명명규칙이다.
    • 함수 : main, prinft가 여기에 해당한다.
    • 상수 : EXIT_SUCCESS가 여기에 해당한다.
  • 함수 : 예제에 나온 식별자 중 main과 printf는 함수다. printf는 프로그램에서 화면에 뭔가를 출력하기 위해 사용한 것이다. 반면 main 함수는 정의한 것이다. 다시 말해 int main(void)라는 선언문 뒤에 {}로 묶은 블록 안에 수행할 일을 나열했다. C 프로그램에서 main은 반드시 있어야 한다. 프로그램 실행을 시작하는 부분이기 때문이다.
  • 연산자 : C 언어에는 다양한 연산자가 있는데, 예제 1-1에서 사용한 연산자는 다음과 같다.
    • = : 초기화 또는 대입
    • < : 비교
    • ++ : 변수 값을 1만큼 증가시킨다.
    • * : 두 값을 곱한다.

 

2.2 선언

프로그램에서 어떤 식별자를 사용하기 전에 그 식별자가 무엇을 가리키는지 컴파일러에게 알려 주도록 선언해야 한다. 키워드는 언어에서 미리 정의했기 때문에 선언하거나 정의할 수 없지만, 식별자는 모두 선언해야 한다는 차이점이 있다.

 

예제 1-1에서 직접 선언한 식별자는 A, i, main이다. 이 세 식별자의 선언문만 모아 보면 다음과 같다.

int main(void)
double A[5];
size_t i;

 

세 선언문은 일정한 패턴을 따른다. 각 문장마다 식별자(A, i, maikn)가 나오고, 그 식별자가 갖는 특정한 속성을 명시한다.

  • i의 타입은 size_t다.
  • main 뒤에 소괄호()가 나온다. 그래서 선언 대상은 함수라는 것을 알 수 있다.
  • A 뒤에 꺾쇠괄호[]가 나온다. 따라서 선언 대상은 배열이다. 배열은 타입이 같은 원소를 하나로 묶는 데 사용한다. 여기서는 double 타입 원소 다섯 개로 구성했다. 이 배열을 구성하는 다섯 원소는 순서가 정해져 있고, 인덱스라 부르는 숫자 0부터 4까지로 각 원소를 접근할 수 있다. 

i와 A는 값을 저장하는 이름 있는 변수로 선언한다. 변수는 특정한 타입의 '뭔가'를 담은 박스로 표현하면 이해하기 쉽다.

 

다른 식별자(printf, size_t, EXIT_SUCCESS)에 대한 선언문은 코드에 나오지 않는다. 사실 이 식별자들은 이미 선언된 상태인데, 컴파일러에게 이 식별자에 대한 정보를 명시적으로 알려줘야 한다. printf는 stdio.h에 있고, size_t와 EXIT_SUCCESS는 stdlib.h에 선언되어 있다. 그래서 #include로 헤더 파일(.h)를 포함시켜야 한다. 이러한 식별자의 의미를 알기 위해 헤더 파일을 읽는 것은 대부분 읽기 어렵게 적혀 있기 때문에 좋은 방법이 아니다. 최신 C 표준 문서를 참조하는 것이 좋다. 쉬운 방법을 원한다면 다음과 같이 명령을 실행해 보자.

apropos printf
man printf
man 3 printf

 

선언은 대상의 특징을 표현하기만 할 뿐, 실제로 생성하지 않는다. 따라서 같은 선언을 여러 번 적으면 텍스트만 중복될 뿐 문제가 발생하지 않는다.

 

프로그램의 일정한 영억 안에서 어떤 식별자에 대해 서로 다른 선언문이 여러 개가 될 수 없다. C 표준에서는 '프로그램의 일정한 영역'의 의미를 구체적으로 정의한다. 이 일정한 영역, 즉 프로그램에서 식별자가 보이는 영역을 스코프(scope)라고 한다. 식별자는 선언문이 속한 스코프에 바인딩(binding)된다. 

 

식별자의 스코프는 문법에 의해 명확히 표현된다. 예제 1-1에 정의된 식별자의 스코프는 다음과 같다.

  • A : main의 정의 안에서만 보인다. 
  • i : i를 선언한 for 구문에 바인딩되어 for 선언문과 for 구문의 { ... } 블록에서만 보인다.
  • main : main 선언문부터 파일 끝까지가 스코프가 된다.

첫 번째와 두 번째 같은 스코프를 블록 스코프라 부른다. 스코프가 { ... } 블록으로 제한되기 때문이다. 세 번째에 나온 main은 파일 스코프라 하고, 흔히 글로벌(전역) 식별자라 부른다.

 

2.3 정의

선언은 식별자가 가리키는 오브젝트의 종류만 지정하고, 구체적인 값이나 오브젝트의 위치는 지정하지 않는다. 이러한 역할은 정의가 담당한다. 

지금까지는 변수를 항상 초기화했다. 다음 코드는 변수 i를 선언하면서 초깃값을 0으로 지정한 것이다. C 언어에서는 이렇게 선언에 초기자를 함께 사용하는 방식으로 식별자에 오브젝트를 정의할 수 있다. 

site_t i = 0;

 

 

A는 여러 요소로 구성되어 있어서 이보다 더 복잡하다.

double A[5] = {
    [0] = 9.0,
    [1] = 2.9,
    [4] = 3.E+25,
    [3] = .00007,
};

A를 구성하는 다서 요소의 값은 각각 9.0, 2.9, 0.0, 0.00007, 3.0E+25가 된다. 이런 방식을 지정 초기자라 부른다. 즉, 초기화할 배열 원소를 대괄호 안의 정수로 지정해서 해당 원소의 값을 초기화한다. 또한 C 언어 규칙에 따라 초기자로 지정하지 않은 원소 값은 0으로 설정된다.

 

여기서 볼 수 있듯이 배열의 위치(인덱스라고 한다)를 표현할 때, 첫 번째 원소는 1이 아닌 0부터 시작한다. 따라서 원소가 n개인 배열에서 첫 번째 원소의 인덱스는 0이고, 마지막 원소의 인덱스는 n-1이다. 

 

함수를 정의하려면 선언문의 함수 코드를 중괄호로 묶어서 적으면 된다.

int main(void) {
    ...
}

 

지금까지 살펴본 예제에서는 두 가지 요소에 대해 이름 짓는 방법(식별자)을 살펴봤다. i와 A는 오브젝트를 가리키고, main과 printf는 함수를 가리킨다. 오브젝트와 함수를 선언하는 문장은 여러 번 나올 수 있지만 이들은 고유해야 한다. 그리고 C 프로그램이 제대로 작동하려면 프로그램에서 사용하는 함수와 오브젝트는 반드시 정의되어 있어야 한다. 

 

2.4 문장

문장은 지금까지 선언된 식별자를 다루는 방법을 컴파일러에게 알려준다. 예제에서 문장은 다음과 같이 구성돼 있다.

for (size_t i = 0; i < 5; ++i)
{
    printf("element %zu is %g, \tits square is %g\n",
            i,
            A[i],
            A[i] * A[i]);
}

return EXIT_SUCCESS;

 

2.4.1 반복

앞에 나온 예제에서 for문은 printf문을 여러 번 실행하라고 컴파일러에게 알려 준다.

 

도메인 반복의 가장 간단한 형태이며, 크게 네 부분으로 구성된다. 

  1. 루프 변수 선언, 정의, 초기화 : size_t i = 0에 해당하며, 여기 나온 초기화 부분은 for문 전체를 실행하기 전에 단 한 번만 실행된다.
  2. 루프 조건 : i < 5에 해당하며, 루프 바디를 실행시키는 조건이 된다. 루프 조건이 참이면 루프 바디가 실행된다.
  3. 루프 바디 : for() 뒤에 나오는 { ... } 블록으로 표현한다.
  4. 루프 변수 증감부 : ++i에 해당하며, 이 코드는 루프 바디가 실행되면 루프 변수 증감부를 실행한다. 그리고 다시 루프 조건으로 이동한다.

이 모든 부분을 합치면, 루프 본문에 있는 코드를 5번 반복하면서 i의 값은 0부터 5까지 차례대로 설정한다. 이처럼 i가 0 ~ 4라는 도메인에 대해 반복하기 때문에 반복할 때마다 i에 특정한 값을 지정할 수 있다. C 언어는 다양한 반복문을 제공한다.

 

2.4.2 함수 호출

함수 호출은 현재 함수의 실행을 잠시 멈추고 호출문으로 지정한 함수로 제어권을 넘긴다. 예제에서는 main 함수의 실행을 멈추고 printf란 함수를 호출했다. 

printf("element %zu is %g, \tits square is %g\n",
        i,
        A[i],
        A[i] * A[i]);

 

함수 호출문에서 함수 이름뿐만 아니라 인수(argument)도 함께 지정할 때가 많다. 예제에서는 i, A[i], A[i]*A[i]와 같이 인수 목록이 길게 나열되어 있다. 이렇게 지정된 인수의 값은 호출하는 함수로 전달된다. 이렇게 값을 전달하는 것을 값 호출(call bye value)라고 한다.

 

2.4.3 함수 리턴

main의 마지막 문장은 return이다. 이 문장은 main 함수의 실행을 중단하고 main 함수를 호출한 곳으로 돌려보내게 된다. 앞의 예제에서 main 함수는 int main()으로 선언하였기 때문에 int 값을 리턴한다는 것을 의미한다. 예제에서는 EXIT_SUCCESS라는 식별자를 지정했다.

 

printf 함수를 정의한 코드를 직접 볼 수 없지만, return문이 정의되어 있다는 것을 알 수 있다. main 함수에서 printf 함수를 호출하고 printf 함수 내에서 return을 만나면 다시 main 함수로 돌아가는 흐름이 된다. 이런 과정은 제어 흐름이라고 한다. 

 

예제 1-1을 실행하면 가장 먼저 현재 플랫폼에서 제공하는 프로세스 구동(process-startup) 루틴이 사용자가 제시한 함수인 main을 호출한다. 그러면 main은 제어 흐름에 따라 C 라이브러리에서 제공하는 printf를 호출한다. printf를 실행하다가 return을 만나면, main 문장으로 돌아가서 실행을 재개한다. 그러다 main이 return을 만나면 프로세스 구동 루틴으로 다시 돌아간다. 여기서 제어권이 넘어가는 과정은 프로그래머 입장에서 프로그램이 종료되는 것으로 보인다.

 

2.5 요약

  • C 언어는 어휘(예: 구두점, 식별자, 숫자 등)와 구문(문법)과 의미를 명확히 구분한다.
  • 식별자는 표현하려는 대상의 속성이 잘 드러나도록 선언한다.
  • 프로그램에서 다루는 대상인 오브젝트와 이런 대상을 다루는 수단인 함수를 반드시 정의해야 한다. 
  • 문장은 주어진 대상을 처리하는 방식을 표현한다. for와 같은 반복문은 특정 작업을 반복하고, printf와 같은 함수 호출은 특정한 작업을 함수에게 위임하고, return과 같은 함수 리턴은 호출 이전의 지점으로 돌아간다.

 

 

'모던 C' 카테고리의 다른 글

레벨 1 친숙 - 04 계산 표현하기  (0) 2024.02.23
레벨 1 친숙 - 03 결국은 제어  (0) 2024.02.03
레벨1 친숙 - 들어가며  (0) 2024.02.03
레벨0 만남 - 01 들어가며  (0) 2024.02.02
들어가며  (0) 2024.02.02