본문 바로가기
스터디/POCU COMP2200 C 언매니지드 프로그래밍

POCU 아카데미 1주차 학습

by argentdarae 2025. 2. 17.

개요

POCU 아카데미 C언매니지드 학습 1주차 내용을 회고하고 복습하는 글이다

강의 스크린샷은 포함되지 않으며 내용또한 학습 및 저작권 상의 이유로 그대로 쓰지 않고 정제하여 쓸 예정이다

 

회고 및 복습은 다음의 순서로 이루어질 예정이다

  1. 무엇을 배웠는지
  2. 인상 깊었던 내용
  3. 마무리

 

 

무엇을 배웠는지

1주차에는 포큐 아카데미의 목표와 C 언어 개요에 대해 학습하였다

 

실무에서 문제를 해결할 수 있는 개발자로 성장하기 위해 필요한 학습 태도와 피드백 방식을 익혔다. 그리고 과제와 실습, 시험 진행을 통해 단순한 코드 작성 능력만이 아니라 개념을 확실하게 이해하는 것이 중요함을 리마인드하였다

 

또한 C언어에 대한 개요와 특징. 그리고 C 를 알아두어야 하는 것이 왜 중요하고 필수적으로 알아야할 개념은 무엇인지 학습하는 시간이 있었다

 

문법의 대한 내용은 아주 기초적이고, 이미 C#에서 습득한 내용이 있으므로 C만의 특별한 문법에 대해서만 정리할 예정이다

 

C언어 개요

C 언어를 배워야 하는 이유

운영체제, 네트워크, 임베디드, 그래픽 등 성능이 중요한 분야에서 GC없이도 성능을 최적화 할 수 있어 필수적으로 사용된다

C 언어는 가비지 컬렉션(GC)이 없어 메모리 관리와 같은 시스템 리소스를 개발자가 직접 제어할 수 있다. 이는 메모리 할당과 해제를 프로그래머가 명시적으로 수행하여 불필요한 메모리 사용을 방지하고, 필요한 시점에 메모리를 효율적으로 활용할 수 있음을 의미한다

 

운영체제의 커널은 하드웨어와 소프트웨어 간의 인터페이스로서, CPU, 메모리, 디스크, 네트워크 등 하드웨어 리소스를 직접 제어한다

대표적인 예시로 Linux 커널은 대부분 C로 작성되어 있다.

 

네트워크 통신을 다룰 때 소켓 프로그래밍은 필수적이다. 네트워크 서버에서는 동시에 수천, 수만 개의 연결을 처리해야 한다. 따라서 메모리 사용을 직접 제어하는 것이 중요하다

C 언어로 작성된 웹 서버의 대표적인 예시로 NGINX가 있다. 하지만 요새 디바이스의 발달로 매니지드 언어로도 서버 개발을 진행하기도한다 예를 들어 매니지드 언어인 javascript로 구현된 Node.js가 있다

 

임베디드 시스템의 큰 범주로 보면 아이폰, 안드로이드도 임베디드 시스템에 들어가지만 통상적으로 말하는 임베디드 시스템은 작은 규모의 하드웨어 장치에서 동작하는 소프트웨어를 의미한다. 예를 들어 의료기기, 소형 가전제품, 산업용 장비 등이 있다

이러한 시스템은 CPU와 메모리 리소스가 제한적이므로 불필요한 메모리 사용을 최소화 해야 한다

 

그래픽 프로그래밍 또한 성능 최적화가 중요한 분야로, CPU가 아닌 GPU에서 직접 연산을 수행하도록 명령을 전달해야 한다. 이를 위해 프로그래머는 OpenGLVulkanDirectX 같은 그래픽 API를 사용하여 GPU 메모리에 데이터를 업로드하고, 셰이더 프로그램을 실행하며, 렌더링 작업을 처리한다.

 

C 언어의 특징

가장 하드웨어에 가까운 언어

언매니지드 언어(C)의 실행 과정

  1. 소스 코드 작성 → .c 파일 생성
  2. 전처리(Preprocessing) → #include, #define 등 전처리 지시문을 처리하여 확장된 소스 코드 생성
  3. 컴파일(Compile) → 컴파일러(GCC, Clang 등)가 어셈블리 코드(Assembly)로 변환
  4. 어셈블(Assemble) → 어셈블러가 어셈블리 코드를 기계어(Machine Code)로 변환하여 개별 오브젝트 파일(.o, .obj) 생성
  5. 링킹(Linking) → 실행에 필요한 라이브러리, 다른 오브젝트 파일과 결합하여 실행 파일(.exe, .out) 생성
  6. 운영체제(OS) 로드(Load) → 실행 파일을 메모리에 로드
  7. CPU 실행(Execution) → CPU가 기계어 명령을 직접 수행

=> C는 모든 과정이 사전에 이루어지고, 실행 시점에서 추가적인 변환이 없으므로 성능이 빠름

 

매니지드 언어(C#)의 실행 과정

  1. 소스 코드 작성 → .cs 파일 생성
  2. 컴파일(Compile) → C# 컴파일러(csc)가 IL로 변환 (.dll, .exe 파일 생성)
  3. 실행 시점(Runtime)에서 CLR(Common Language Runtime)이 실행
  4. JIT(Just-In-Time) 컴파일 또는 AOT(Ahead-Of-Time) 컴파일 수행
    • JIT: 실행 시 IL을 네이티브 코드(기계어)로 변환하여 실행
    • AOT: 실행 전에 미리 기계어로 변환하여 JIT 오버헤드를 줄임
  5. CPU 실행 → 변환된 네이티브 코드 실행

=> C#은 실행 시점에서 IL을 네이티브 코드로 변환하는 과정이 추가되므로, C보다 상대적으로 오버헤드가 발생할 수 있음

 

절차적 언어

C 언어는 절차적 프로그램이을 기반으로 하는 언어로 프로그램이 순차적인 흐름을 따라 실행된다

위에서 아래로 읽어내려가면 되어 구조적으로 명확하지만 프로그램 규모가 커질 수록 코드 재사용성과 유지보수성이 낮을 수 있다

 

모든 함수가 전역 함수이며, 전역/지역 변수만 존재

C 에서는 모든 함수가 기본적으로 전역 함수이며, 특정 객체에 속하지 않는다.
따라서 함수가 선언되면, 동일한 프로그램 내의 모든 코드에서 접근할 수 있다.

========= math_functions.h =========

#ifndef MATH_FUNCTIONS_H
#define MATH_FUNCTIONS_H

/* 전역 함수 선언 */
int add(int a, int b);
int subtract(int a, int b);

#endif /* MATH_FUNCTIONS_H */

========= math_functions.c =========

#include "math_functions.h"

/* 두 정수를 더하는 전역 함수 */
int add(int a, int b) 
{
    return a + b;
}

/* 두 정수를 빼는 전역 함수 */
int subtract(int a, int b) 
{
    return a - b;
}

============== main.c ==============

#include <stdio.h>
#include "math_functions.h"

int main(void) 
{
    /* 전역 함수 호출 */
    int sum = add(5, 3);
    int difference = subtract(10, 4);

    printf("Sum: %d\n", sum);
    printf("Difference: %d\n", difference);

    return 0;
}

 

C 에서는 객체의 멤버 변수(인스턴스 변수) 개념이 없으며, 변수는 전역 또는 지역 변수로만 선언 가능하다.

 

전역 변수는 프로그램이 실행되는 동안 유지되며 모든 함수에서 접근 가능하다. 프로그램 어디서든 값을 변경할 수 있어 명확하게 사용 범위를 제한해서 사용하는 것이 중요하다

지역 변수의 경우 함수 내부에서 선언되는 변수이다. 함수가 실행될 때 스택 프레임에 데이터가 추가되고, 함수가 종료되면 메모리에서 해제된다

========= global_local.h =========

#ifndef GLOBAL_LOCAL_H
#define GLOBAL_LOCAL_H

/* 전역 변수 선언 */
extern int globalVar;

/* 전역 함수 선언 */
void modifyGlobalVar(int value);
void useLocalVar(void);

#endif /* GLOBAL_LOCAL_H */

========= global_local.c =========

#include <stdio.h>
#include "global_local.h"

/* 전역 변수 정의 (초기값 10) */
int globalVar = 10;

/* 전역 변수를 변경하는 함수 */
void modifyGlobalVar(int value) 
{
    globalVar = value;
    printf("Global Variable Updated: %d\n", globalVar);
}

/* 지역 변수 사용 예제 */
void useLocalVar(void) 
{
    /* 지역 변수 선언 */
    int localVar = 5;
    printf("Local Variable: %d\n", localVar);
}

============== main.c ==============

#include <stdio.h>
#include "global_local.h"

int main(void) 
{
    /* 전역 변수 사용 */
    printf("Initial Global Variable: %d\n", globalVar);
    
    /* 전역 변수 값 변경 */
    modifyGlobalVar(20);
    
    /* 지역 변수 테스트 */
    useLocalVar();

    /* 전역 변수는 어디서든 접근 가능 */
    printf("Final Global Variable: %d\n", globalVar);

    return 0;
}

 

 

C 언어의 문법

#include 지시문과 헤더 파일

C 언어에서 #include 지시문은 다른 파일의 내용을 현재 소스 파일에 포함할 때 사용된다

전처리 과정에서 다른 파일의 내용을 현재 소스 파일에 포함하는 것이다

 

#include 지시문을 사용할 때, 두 가지 방식이 있다

  • <파일명> 형식은 표준 라이브러리를 포함할 때 사용되며, 컴파일러가 지정한 시스템 디렉토리에서 해당 파일을 검색한다
  • "파일명" 형식은 사용자 정의 헤더 파일을 포함할 때 사용되며, 현재 디렉토리에서 먼저 파일을 찾은 후, 시스템 디렉토리에서 검색한다
/* #include를 사용하여 헤더 파일을 포함한 코드 */

========= math_functions.h =========

#ifndef MATH_FUNCTIONS_H
#define MATH_FUNCTIONS_H

/* 전역 함수 선언 */
int add(int a, int b);
int subtract(int a, int b);

#endif /* MATH_FUNCTIONS_H */


========= math_functions.c =========

#include "math_functions.h"

/* 두 정수를 더하는 전역 함수 */
int add(int a, int b) 
{
    return a + b;
}

/* 두 정수를 빼는 전역 함수 */
int subtract(int a, int b) 
{
    return a - b;
}


========= main.c =========

#include <stdio.h>
#include "math_functions.h"

int main(void) 
{
    /* 전역 함수 호출 */
    int sum = add(5, 3);
    int difference = subtract(10, 4);

    printf("Sum: %d\n", sum);
    printf("Difference: %d\n", difference);

    return 0;
}


========= 실행 결과 =========
Sum: 8
Difference: 6

 

위와 같이 #include 지시문을 사용하면 여러 파일에서 공통된 선언을 공유할 수 있으며, 컴파일 과정에서 전처리기가 해당 파일의 내용을 포함한 후, 컴파일과 링킹을 수행한다

이를 통해 코드의 모듈화가 가능해지고, 유지보수성과 재사용성이 향상된다

 

역참조 연산자(*)와 주소 연산자(&)

C 언어에서는 포인터를 활용하여 메모리 주소를 다룰 수 있으며, 이를 위해 역참조 연산자와 주소 연산자가 사용된다

  • & : 변수의 메모리 주소를 반환
  • * : 포인터가 가리키는 주소의 값을 반환
/* 주소 연산자와 역참조 연산자의 사용 */

#include <stdio.h>

int main(void) 
{
    int num = 10;
    int *ptr = &num;  /* num의 주소를 ptr에 저장 */

    printf("num의 값: %d\n", num);
    printf("num의 주소: %p\n", &num);
    printf("ptr이 가리키는 값: %d\n", *ptr);

    /* 포인터를 이용한 값 변경 */
    *ptr = 20;
    printf("num의 새로운 값: %d\n", num);

    return 0;
}

================ 실행 결과 ================
num의 값: 10
num의 주소: 0x7ffdb4c9d8bc
ptr이 가리키는 값: 10
num의 새로운 값: 20

 

위와 같이 포인터를 사용하면 변수의 값을 직접 변경할 수 있으며, 메모리의 효율적인 관리가 가능하다

그래서 배열, 함수 포인터, 동적 메모리 할당 등 다양한 기능에서 필수적으로 사용된다고 한다

 

C 언어의 bool 형

C89 표준에서는 bool 자료형이 존재하지 않았으며, 대신 정수 int 를 사용하여 0이면 false, 0이 아닌 값을 true로 취급하였다

C99 이후 stdbool.h 가 추가되어 bool 키워드를 사용할 수 있게 되었다

 

그래서 C에서는 전처리기를 사용하여 0을 FALSE, 1을 TRUE로 매핑하여 사용하는 방식을 주로 사용한다고 한다

#include <stdio.h>

/* 전처리기를 사용하여 FALSE와 TRUE 정의 */
#define FALSE 0
#define TRUE 1

int main(void) 
{
    int isTrue = TRUE;
    int isFalse = FALSE;

    printf("isTrue: %d\n", isTrue);   /* 출력: 1 */
    printf("isFalse: %d\n", isFalse); /* 출력: 0 */

    /* 조건문에서 사용 */
    if (isTrue) 
    {
        printf("TRUE입니다.\n");
    }

    if (!isFalse) 
    {
        printf("FALSE가 아닙니다.\n");
    }

    return 0;
}

 

 

인상 깊었던 내용

전역/지역 변수를 어떻게 활용할 것인가?

C 언어에는 접근 한정자가 없기 때문에 변수의 가시성을 잘 관리해야 한다. 특히, 전역 변수를 남발해서 사용하면 스파게티 코드가 될 여지가 있어 적절한 설계가 필요하다

 

C에서 전역 변수를 남용하면 발생하는 문제는 다음과 같다

  • 모든 함수에서 접근 가능 → 코드 의존성이 높아지고 디버깅이 어려워짐
  • 동시 수정 가능성 증가 → 여러 함수가 동일한 전역 변수를 수정하면 예측하기 어려운 버그 발생 가능
  • 캡슐화가 어렵다 → 객체지향 언어처럼 데이터 보호가 불가능

따라서 이러한 문제들을 해결하기 위하여, 정적 변수 등을 활용하는 방법을 사용한다

========= config.h =========
#ifndef CONFIG_H
#define CONFIG_H

/* 전역 변수처럼 보이지만, 파일 내부에서만 사용 가능 */
static int s_app_mode = 1;

/* getter, setter 함수 제공 */
int get_app_mode(void);
void set_app_mode(int mode);

#endif /* CONFIG_H */


========= config.c =========
#include "config.h"

/* s_app_mode를 접근하기 위한 함수 */
int get_app_mode(void) 
{
    return s_app_mode;
}

void set_app_mode(int mode) 
{
    s_app_mode = mode;
}


========= main.c =========
#include <stdio.h>
#include "config.h"

int main(void) 
{
    printf("Current Mode: %d\n", get_app_mode());

    set_app_mode(2);
    printf("Updated Mode: %d\n", get_app_mode());

    return 0;
}

 

위와 같이 전역 변수를 외부에서 직접 수정하지 못하도록 막고, getter/setter를 통해 접근하도록 제한하였다

핵심은 접근 한정자를 응용하듯이 최대한 변수를 사용 가능한 범위를 줄여 코드에 의도를 담는 것인 것 같다

 

인클루드 가드

C에서는 같은 헤더 파일이 여러 번 포함될 경우 중복 정의 오류가 발생할 수 있다. 이를 방지하기 위해 인클루드 가드를 사용한다

========= example.h =========

/* #ifndef EXAMPLE_H → 처음 포함될 때만 컴파일되며, 이후 중복 포함 방지 */
#ifndef EXAMPLE_H
#define EXAMPLE_H

/* 헤더 파일의 내용 */
void hello(void);

#endif /* EXAMPLE_H */

 

만약 인클루드 가드를 사용하지 않으면 다음과 같은 오류가 발생할 수 있다

/* 예제 코드 */

========= example.h =========

/* 인클루드 가드를 사용하지 않음 */
#include <stdio.h>

/* hello 함수 정의 포함 */
void hello(void) 
{
    printf("Hello, World!\n");
}


========= example1.h =========

#include "example.h"


========= example2.h =========

#include "example.h"


========= main.c =========

#include <stdio.h>
#include "example1.h"
#include "example2.h"

void hello(void) 
{
    printf("Hello, World!\n");
}

int main(void) 
{
    hello();
    return 0;
}

에러 발생

 

C 에서는 동일한 함수가 여러 번 정의될 수 없으므로 컴파일러가 중복 정의 오류를 출력하는 것이다

 

 

마무리

이번 1주차 학습을 통해 C 언어의 본질적인 특성과 메모리의 직접적인 제어가 가능하다는 중요한 특징에 대해서 알 수 있었다

게다가 역참조 연산자나 주소 연산자, 인클루드 가드 등. C#에서는 쓰지 않았던 여러 문법들이 보여 이에 익숙해져야겠다는 생각도 들었다

 

앞으로 학습할 때는 두 가지 부분을 좀 더 신경쓰려고 한다

  • C 언어만의 장점인 포인터를 이용한 빠른 연산 구현
  • 언어적 한계 속에서도 얼마나 가독성이 좋은 코드를 짤 수 있는가

최선을 다해보려고 한다