42 Starter 스터디 모임에서 진행하는 C 언어 A to Z 스터디에 내용을 정리해서 포스팅을 진행합니다.

해당 스터디에서는 42 Piscine 과정에서 채우지못한 부족한 C언어 역량을 키우는 것을 목적으로 합니다.


1. Const

const 는 수정하면 안되는 변수를 수정하지 못하게 막는 방법

int calculate_rist(const int id)
{
    int age = db_get_age(id);
    int amout;

    /* 코드 천줄 */

    id *= 2; /* 컴파일 오류 */

    amout = db_get_depoist_amount(id);

    return (risk);
}

중간에 변경되면 안되는 변수를 지정할 때 사용한다.

const의 가장 좋은 사용은 기본적으로 모든 변수에 const 를 붙이고 정말 값 변경이 필요한 변수에만 const를 생략하는 방식으로 사용하는 것이다. 원칙적으로 말하면 언어의 기본동작이 바뀌어야 한다.


2. goto 문

`goto <label_name>;
...
<label_name>;

goto문은 특정 label로 점프하는 구문을 얘기한다.

C는 위에서 아래로 코드가 실행되는 순차적 구조인데 goto문은 이를 어기기 때문에 코드를 스파게티 코드로 만들기 때문에 사용되지 않고 반복문으로 이용한다.

그래도 goto문을 사용하고 싶다면 언제나 아래쪽으로만 점프하게 하고 내포된 루프에서 빠져나오는 경우에 사용해야한다.


3. Stack Memory

C언어에서 함수의 호출과 반환을 메모리에 저장할 때 stack 메모리 구조를 사용한다. 함수가 호출될 때마다 그 함수에서 필요한 공간을 스택에서 떼어줬다가 그 함수가 반환하면 그냥 자리를 다시 뺏는 방식으로 메모리에 저장된다. 그리고 stack 메모리는 빈 공간이 없이 하나씩 차곡차곡 쌓여 있는 구조를 이룬다.

또한, 기본 자료형의 변수들은 스택 메모리에 저장이 된다. newmalloc을 이용한 데이터들은 heap 메모리에 할당이 된다. heap 메모리 공간은 필요한 영역만큼 heap 메모리에서 떼어주기 때문에 중간 중간 구멍이 뚫려있는 구조로 되어있을 수 있다. 또한 stack과 다르게 빈 공간을 찾아야 하기 때문에 메모리 할당에 있어서 시간이 더 오래 걸릴 수 있다.

너무 큰 데이터나 재귀함수의 많은 호출이 있게 된다면 stack 메모리 영역의 공간이 부족하여 스택 오버플로우가 발생할 수 있으니 주의해야한다.

*추가정보: 배열의 요소 개수 구하는 방법은 sizeof(values) / sizeof(values[0]) 를 통해서 구할 수 있다.

스택 메모리를 사용할 때 생각해야 할 점은 함수 호출과 같이 스택 메모리를 사용했다가 그 공간을 비워주게 되었을 때 메모리 상에 데이터가 남아 있다는 것이다. 따라서 새롭게 변수나 함수의 호출을 통해서 무언가를 할 때 변수 값은 항상 초기화 해주어서 예상치 못한 값이 저장되어 있는 상태를 고려해야한다.


4. Build Step

Build란, 사람이 읽기 쉬운 소스코드를 기계어 명령어로 변환하는 과정과 그 명령어들을 모아 기계에서 실행 가능한 실행파일로 만드는 과정을 말한다.

C의 Build는 4단계로 나뉘어져 있다.

  1. 전처리(preprocessing)
  2. 컴파일(compilation)
  3. 어셈블(assembling)
  4. 링크(linking)

전처리, 컴파일 어셈블을 합쳐서 컴파일과 링크와 같이 크게 2가지로 분류하기도 한다.

clanggcc에서는 알아서 모든 4단계를 진행해준다. 하지만, clang에서 한 단계씩 실행도 가능하다.

한 단계씩 분리해서 컴파일을 해보는 과정에 앞서 헤더와 소스파일에 대해서 먼저 알아보자.

c 파일은 실제 프로그램을 돌게하는 로직 코드를 저장해 두는 파일을 말한다. 주로 내용물로 함수 정의, 전역 변수, 매크로 등이 있다.

헤더 파일은 여러 소스코드 파일에 공통적으로 필요한 것들을 저장해 두는 파일을 말한다. 주로 내용물로 함수 선언문, 매크로, extern 변수 선언이 있고 c 파일에서 #include를 통하여 불러온다.

헤더 파일을 사용하는 이유는 더 효율적인 구조를 잡고 난장판인 코드를 보는 것을 방지하기 위해 사용한다. 그리고 중복된 코드의 사용을 방지하기 위해 c 파일들을 나누게 되는 데, 헤더 파일을 사용하면 이 함수 들의 선언을 여러 C 파일들과 공유할 수 있다. 그리고 나눈 c 파일들의 각각의 헤더를 만들어 분리하여 다른 프로그램을 만들때 이용할 수 도 있다.

*추가정보: #include <>#include ""의 차이는 <>의 경우 시스템 경로에서만 헤더 파일을 검색할때나 보통 컴파일러가 제공하는 시스템 헤더 파일을 포함할 때 사용한다. ""의 경우 현재 작업중인 디렉토리에서 헤더 파일을 먼저 검색한 뒤 시스템 경로를 검색할 때 사용하며 일반적으로 개발자가 구현한 헤더 파일들을 포함할 때 사용한다.


5. 전처리 단계

다음의 과정이 순서대로 이어진다.

  1. 주석 제거
  2. 매크로를 확장한다. (즉, #으로 시작하는 전처리 지시어들을 모두 치환한다.)
  3. 인클루드 파일들을 확장한다. (즉, #include 헤더파일을 지우고 그 자리에 헤더 파일 속에 있는 내용을 복사해서 붙인다.)
  4. 위에 과정을 거쳐 컴파일의 기본 단위인 translation unit을 만들고 이것을 출력한다.

clang 컴파일러의 -E 플래그를 통하여 translation unit을 화면에 출력해볼 수 있다.

clang -std=c89 -W -Wall -pedantic-errors -E filename.c

6. 컴파일 단계

컴파일러라는 프로그램이 컴파일 단계를 담당한다. 입력으로는 translation unit이 출력으로는 assembly code가 나오게 된다.

어셈블리어는 기계 코드와 거의 1:1로 대응이 되는 언어로 사람이 그나마 여전히 읽기 쉬운 언어의 상태라고 볼 수 있다.

어셈블리어 상태에서는 아직 정의를 모르는 심볼(함수나 변수의 이름)을 사용할 수 있으며 컴파일러가 어떤 함수나 변수의 정의를 못 찾을 경우 선언만 보고 일단 구멍으로 남겨놓는다. 이후 링크 단계에서 이것을 마무리 해준다.

clang 컴파일러의 -S 플래그를 통하여 assembly code를 볼 수 있다.

# assemly code가 .s 파일로 저장된다.
clang -std=c89 -W -Wall -pedantic-errors -S filename.c

assembly code가 나왔다는 의미는 이제부터는 특정 플랫폼에서만 동작한다는 의미를 갖고있다. C 자체는 크로스 플랫폼이지만 컴파일 되기 전까지를 얘기한다. 타겟 플랫폼(예를 들어 OS)이 몇 비트냐에 따라 C의 자료형 크기가 달라질 수 있다는 얘기와 같은 맥락이다.


6. 어셈블 단계

assembler라는 프로그램이 어셈블 단계를 담당한다.

assembly code를 조립하여 오브젝트 코드를 만드는 과정을 말한다.

오브젝트 코드란, 기계가 곧바로 이해 가능한 기계코드 즉, 기계어이다. 그렇기 때문에 이진 코드이다. assembly code와 마찬가지로 여전히 메꾸어야하는 구멍들이 존재한다. (링커가 일을 하기 이전이기 때문이다.)

clang 컴파일러의 -c 플래그를 통하여 오브젝트 코드를 볼 수 있다.

# assemly code가 .o 파일로 저장된다.
clang -std=c89 -W -Wall -pedantic-errors -S filename.c
# hexdump를 통해 직접 데이터를 찍어볼 수 있다.
hexdump -C filename.o

7. 링크 단계

링커(linker)가 링크 단계를 담당한다.

입력으로 모든 오브젝트 코드를 받아오고 구멍을 메꾼 뒤 실행파일로 저장을 하는 일을 한다.

링커가 메꾸지 못한 구멍이 생긴다면 그 함수나 변수가 없어 실행할 방법이 없기 때문에 링커가 오류를 내보낸다.

정상적으로 잘 메꾸어 졌다면 출력으로 실행파일을 생성한다.

굳이 링크단계가 분리되어 있는 이유는 다음과 같다.

수많은 구멍을 컴파일할 때 마다 메꾸기에는 너무 비효율적이다. 그래서 마지막에 한번에 구멍을 메꾸어주는 것이 좋다. 또한, 여러 개의 .c 파일에서 동일한 외부 함수를 사용할 경우 최종 실행파일에 그 함수 정의가 중복으로 들어가는 것도 막아야 하기 때문에 마지막에 한다.


8. Library와 링크

Library는 어떤 것을 컴파일 했을 때 실행파일이 아니라 코드를 모아놓은 바이너리 파일로 저장한 것을 얘기한다.

정적 라이브러리는 필요한 오브젝트 파일을 컴파일해서 모아놓은 파일을 얘기하며 이것을 사용하는 매커니즘은 라이브러리 안에 있는 기계어를 최종 실행파일에 가져다가 복사하는 방식이다.

복사하기 때문에 동적 링킹에 비해 실행파일의 크기가 커지고 메모리를 더 잡아 먹을 수 있다. 하지만 이미 실행파일안에 넣어두었기 때문에 실행 속도가 빠르다.

동적 라이브러리는 실행파일 안에 여전히 채우지 않은 구멍을 남겨 두고 실행파일을 실행할 때 실제로 링킹을 하는 방식이다. 이 링킹은 실행 중 OS가 해주게 된다.

.dll 파일이 바로 동적 링크 라이브러리를 의미한다. 이 동적 링킹은 정적 링킹보다 실행파일의 크기가 적고 여러 실행파일이 동일한 라이브러리를 공유할 수 있는 메모리 절약에 있어서 좋은 점을 가지고 있다. 하지만, 여러 실행파일이 이름은 같지만 버전이 다른 동적 라이브러리를 사용한다면 DLL 지옥을 맞보게 될 수 있다. (dll 버전이 다르기 때문에 실행파일에서 오류가 나게 되는 것을 말한다.)


8. 전역변수

다른 파일에 있는 전역 변수 사용 시 문제점에 대해서 다루려고한다.

컴파일러는 .c 파일을 따로따로 컴파일 하기 때문에 전역변수에 경우 문제가 생긴다. 예를 들어 1이라는 c파일과 2라는 c파일에서 모두 전역변수로 같은 변수명을 사용했을 때 1과 2파일의 전역변수 내용을 링킹하는 과정에서 오류가 발생한다. 그렇다면 같은 전역변수 어떻게 사용해야할까?

.c 파일안에 새로운 전역변수를 쓰는 것이 아니라 다른 파일에서 전역변수를 갖고 오는 것을 표현하면된다. extern 키워드를 변수 선언 앞에 붙여주면 된다. 그리고 함수 프로토타입에는 extern이 생략 되어 있는 것이다.

extern 키워드를 쓰게되면 어느 .c파일에서도 사용을 할 수 있다. 한 파일에서만 사용하도록 하는 방법이 있는데 바로 static 키워드를 사용하는 것이다. 이 키워드를 사용하면 다른 파일에서 전역 변수에 접근 못하도록 막을 수 있다.

static 키워드를 지역 변수에도 사용할 수 있다, 이 때는 의미가 다르다. 함수 내에서 static을 사용하게 되면 처음 선언시 개념상 전역 변수와 같이 사용을 할 수 있다. (하지만, 그 함수 안에서만 접근이 가능하다.) 그 이후 다시 그 함수가 호출 된다면 변수가 재선언이 되는 것이 아니라 데이터 영역에 있는 static 변수를 가져와서 사용하게 된다.


9. 헤더 중복 방지

여러 .c 파일들에서 헤더를 계속해서 불러오게 되면 헤더가 중복으로 선언이 되거나 순환 헤더 인클루드가 생겨서 헤더가 계속 다른 헤더를 불러오는 경우가 생길 수 있다. 따라서 중복 방지를 꼭 해주어야 한다.

기본적으로 #include를 .c 파일에서만 진행을 하고 .c 파일에서도 중복선언을 하지 않고 중복을 1차적으로 피하고 중복 선언이 필요하게 될때에는 다음과 같이 인클루드 가드를 해서 헤더의 중복을 방지하자.

#ifndef HEADER_H // HEADER_H가 정의 되어 있지 않다면,
# define HEADER_H // HEADER_H를 정의한다.

/* 헤더의 내용 */

#endif  // HEADER_H의 끝
⤧  Next post Oh-My-C-P-P 02 ⤧  Previous post Oh-My-C-P-P 01