관리 메뉴

Rootable의 개발일기

왜 C언어를 사용하는가 본문

C

왜 C언어를 사용하는가

dev-rootable 2024. 7. 1. 15:22

🔎 C언어의 주요 기능

 

🚀 저수준 메모리 접근

 

포인터를 통해 메모리 할당, 해제 및 데이터 조작 등의 작업을 수행할 수 있다. 이 기능은 메모리 관리가 중요한 시스템 프로그래밍과 임베디드 시스템 개발에서 큰 장점으로 작용한다.

 

🚀 다양한 표준 라이브러리

 

C 언어는 표준 입출력, 문자열 처리, 수학 연산, 파일 처리 등 다양한 기본적인 프로그래밍 작업을 지원하는 방대한 표준 라이브러리를 제공한다. 이것은 C 언어가 저수준뿐만 아니라 고수준 언어의 특징을 갖고 있어 다양한 애플리케이션 개발을 지원한다는 것을 의미한다.

 

🚀 이식성

 

C언어의 표준화 덕분에, 소스 코드는 매우 적은 변경이나 추가 작업 없이도 다른 시스템으로 옮겨져 컴파일될 수 있다. 따라서, C언어로 작성된 프로그램은 다양한 플랫폼과 하드웨어에서 실행될 수 있다.

 

🚀 구조체를 통한 복잡한 데이터 구조의 구현

 

C언어는 구조체(struct)를 사용하여 사용자 정의 데이터 타입을 생성할 수 있다. 이를 통해 복잡한 데이터 구조를 효율적으로 표현할 수 있어 코드 가독성을 높일 수 있다.

 

🚀 코드 재사용

 

C언어는 함수를 통해 코드를 모듈 단위로 나눌 수 있다. 이를 통해 코드의 재사용성이 증가하고 유지 보수가 용이해진다. 또한, 헤더 파일을 통해 함수 선언과 정의를 분리할 수 있어, 대규모 프로젝트의 관리가 더욱 효율적으로 이루어질 수 있다.

 

그래서 C언어의 특징을 이렇게 요약하고자 한다.

 

저수준 언어와 고수준 언어의 특징을 모두 가진 언어

 

🔩 C언어 처리 과정

 

출처: https://gracefulprograming.tistory.com/16

 

🔻 전처리 과정

 

전처리 과정은 크게 두 부분으로 나눌 수 있다.

 

  • 헤더 파일 삽입
  • 매크로 치환 및 적용

 

#define 된 부분은 심볼 테이블에 저장되고, 심볼 테이블에 들어 있는 문자열과 같은 문자열을 만나면 #define 된 내용으로 치환한다. 이때 #ifdef와 같은 다른 전처리기 매크로들도 같이 처리된다.

 

🔻 컴파일 과정

 

컴파일 과정은 크게 전단부, 중단부, 후단부로 나눌 수 있다. 컴파일 결과로 .s 어셈블리 코드로 이루어진 파일이 만들어진다.

 

✔ 전단부

 

전단부에서는 소스코드가 올바르게 작성되었는지 분석하고, 중단부에 넘겨주기 위한 GIMPLE 트리를 생성하는 일을 수행한다.

 

GIMPLE 트리 : 소스 코드를 트리 형태로 표현한 자료 구조

 

과정

1. 어휘 분석: C 소스코드를 의미 있는 최소단위(토큰)로 나눈다.
2. 구문 분석: 토큰으로 Parse Tree를 만들면서 문법적 오류 검출
3. 의미 분석: Parse Tree를 이용해 문법적 오류는 없지만 의미상 오류가 있는지 검사
(함수 매개변수 잘못 사용, 변수 자료형 불일치 등)
4. 중간 표현 생성: 언어 독립적인 특성을 제공하기 위해 트리 형태의 중간 표현(GIMPLE Tree)을 생성

 

✔ 중단부

 

전단부에서 넘겨받은 GIMPLE 트리를 SSA(Static Single Assignment) 형태로 변환한 후 아키텍쳐 비종속적인 최적화를 수행한다.

 

SSA(Static Single Assignment)

각 변수가 정확히 한 번만 할당되는 중간 표현(IR) 유형을 말한다. 많은 상용 컴파일러를 포함하여 명령형 언어에 대한 대부분의 고품질 최적화 컴파일러에서 사용된다.

SSA 변환

변수마다 버전 부여

Φ 함수를 통해 재할당

Reference:
https://en.wikipedia.org/wiki/Static_single-assignment_form

 

아키텍처 비종속적인 최적화란 서로 다른 CPU 아키텍처에 구애받지 않고 공통적으로 수행할 수 있는 최적화를 말한다. 중단부에서는 SSA 기반으로 최적화를 수행한다.

 

✔ 후단부

 

후반부에서는 RTL Optimizer에 의해 아키텍처 비종속적인 최적화와 함께 아키텍처 종속적인 최적화를 수행한다.

 

아키텍처 종속적인 최적화는 각 프로그램 내의 명령어 중 아키텍처별로 좀 더 효율적인 명령어로 대체해 성능을 높이는 작업과 같이 아키텍쳐 특성에 따라 최적화를 수행하는 것을 말한다.

 

🔻 어셈블 과정

 

컴파일이 끝난 어셈블리 코드는 어셈블러에 의해 기계어로 어셈블 된다.

 

어셈블러에 의해 생성되는 목적코드(.o) 파일은 어셈블 된 프로그램의 명령어와 데이터가 들어있는 ELF 바이너리 포맷(Binary Format) 구조를 갖는다.

 

다음 단계인 링킹에서 링커가 바이너리 파일을 하나의 실행 파일로 묶기 위해서 각 바이너리의 정보를 효과적으로 파악하기 위해서(명령어와 데이터의 범위 등) 일정한 규칙을 갖게 형식화해놓은 것이다.

 

🔻 링킹 과정

 

링커는 오브젝트 파일들과 프로그램에서 사용된 표준 C 라이브러리, 사용자 라이브러리를 링크(Link)한다.

 

이렇게 링킹 과정이 끝나면 실행 가능한 실행 파일이 만들어지게 된다.

 

🧪 C언어를 사용하는 목적

 

✍ Instuction code를 생성할 수 있는 언어

 

CPU는 아래와 같은 자신이 이해할 수 있는 포맷이 있는데 이를 Instruction Format이라 한다.

 

 

메모리를 직접 제어할 수 있으며, 컴파일 결과 CPU가 직접 이해할 수 있는 Instruction code를 생성할 수 있다.

 

C언어는 포인터를 통해 메모리를 직접 제어할 수 있다는 점, 컴파일 결과 중간 코드 없이 바로 바이너리 코드를 생성할 수 있다는 점에서 시스템 제어와 효율성이 중요한 분야에서 유리하다.

 

void swap(int *xp, int *yp)
{
    int t0 = *xp;
    int t1 = *yp;
    *xp = t1;
    *yp = t0;
}

 

어셈블러 결과

 

✍ 포인터

 

데이터가 저장된 메모리의 주소값을 저장하는 변수
즉, 프로그래머가 컴퓨터 메모리에 직접 접근해서 제어할 수 있게 하는 도구

 

🔸 포인터는 접근한 값을 수정할 수 있다.

 

#include <stdio.h>

int main(void)
{
    int val1 = 10;
    int val2 = 20;
    int val3 = 30;

    printf("메모리상 val1 위치 : %d\n", &val1);
    printf("메모리상 val2 위치 : %d\n", &val2);
    printf("메모리상 val3 위치 : %d\n", &val3);

    int *cursor = &val1;

    //포인터 변수 cursor를 통해 val1 접근
    printf("val1의 주소: %d, 값: %d\n", &val1, val1);
    printf("cursor의 주소: %d, 값: %d\n", cursor, *cursor);

    *cursor = *cursor + 5;

    //포인터 변수는 가리키는 값을 공유한다.
    printf("val1의 주소: %d, 값: %d\n", &val1, val1);
    printf("cursor의 주소: %d, 값: %d\n", cursor, *cursor);

    return 0;
}

 

 

🔸 포인터는 접근한 값을 저장할 수 있다.

 

#include <stdio.h>
#define SIZE 1000

void swap(int *a, int *b);
void quickSort(int start, int end);

int a[SIZE];

int main(void)
{
    int size;
    printf("Array size : ");
    scanf("%d", &size);
    
    //배열 값 입력
    printf("Input array values >> ");
    for (int i = 0; i < size; i++) {
        scanf("%d", &a[i]);
    }
    
    quickSort(0, size - 1);

    printf("Result: ");
    for (int i = 0; i < size; i++)
    {
        printf("%d ", a[i]);
    }
    
    return 0;
}

void quickSort(int start, int end) {
    if (start >= end) return;

    int key = start, i = start + 1, j = end; //가장 좌측 값을 키로
    
    while (i <= j) {
        while (i <= end && a[i] <= a[key]) i++;
        while (j > start && a[j] >= a[key]) j--;
        
        if (i > j) swap(&a[key], &a[j]);
        else swap(&a[i], &a[j]);
    }
    
    quickSort(start, j - 1);
    quickSort(j + 1, end);
 }

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

 

 

🔸 포인터는 자료구조의 가장 앞 부분을 가리킨다.

 

#include <stdio.h>

struct Student
{
    int id;     // 학번
    int grade;  // 학년
    int total;  // 총학점
    char *name; // 이름
};

int main(void)
{
    struct Student student[3] = {{1, 3, 88, "Mike"}, {2, 4, 116, "Tony"}, {3, 1, 38, "Justin"}};
    struct Student *ptr;
    ptr = &student;

    printf(" **** Student Info ****\n");
    for (int i = 0; i < 3; i++)
    {
        printf("\nID: %d\n", ptr->id);
        printf("Grade: %d\n", ptr->grade);
        printf("Total score: %d\n", ptr->total);
        printf("Name: %s\n", ptr->name);
        printf("------------------------------");
        ptr++; // 구조체 시작 부분을 가리킴
    }

    return 0;
}

 

결과

 

도식화

 

References:

 

https://it-stargazer.com/c%EC%96%B8%EC%96%B4-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%EC%9D%98-%EC%9D%B4%EC%9C%A0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8/

 

https://lacommune.tistory.com/76

 

https://www.learncomputerscienceonline.com/instruction-cycle/

 

https://suhwanc.tistory.com/m/134

 

https://gracefulprograming.tistory.com/16

 

 

 

'C' 카테고리의 다른 글

C 계산기 구현  (2) 2024.07.05