카테고리 없음

[2주차] C 언어 기본 문법

2026. 4. 12. 00:43

VEDA 주간 학습 정리

[2주차] C 언어 기본 문법

이번 주 학습을 한 문장으로 정리하면: C 언어 기본 문법, 주소와 포인터, 배열, 변수의 사용 영역, 입출력 처리 이해

작성일: 2026.04.12

이번 주 학습 목표

  • C 언어 기본 문법 이해하기
  • 주소, 포인터, 배열의 관계 이해하기
  • 변수 사용 영역과 메모리 배치 이해하기

1. C 언어 기본 문법

변수 (Variable)

  • 정의: 데이터를 저장하기 위한 이름 붙은 메모리 공간
  • 역할: 프로그램 실행 중 값을 보관하고, 필요할 때 읽거나 수정할 수 있음
  • 형식: 자료형 변수명;

함수 (Function)

  • 정의: 특정 작업을 수행하는 코드 블록
  • 역할: 반복되는 작업을 묶어 재사용 가능하게 하고, 프로그램 구조를 체계적으로 만듦
  • 형식: 반환형 함수명(매개변수) { ... }
  • 컴퓨터 구조적으로 보면 함수 호출 시 EIP가 해당 함수의 스크립트 주소로 이동해 실행됨
#include <stdio.h>          // 헤더 파일: 표준 입출력 라이브러리를 불러와 printf 같은 함수를 사용 가능하게 함

int print_num(int num);     // 함수 선언: print_num이라는 함수가 있고, 정수형 매개변수를 받아 동작할 것임을 알림

int main(void)              // 메인 함수: 프로그램이 실행될 때 가장 먼저 호출되는 진입점
{
    int num;                // 변수 선언: 정수형 변수 num을 메모리에 생성
    print_num(num);         // 함수 호출: print_num 함수를 실행, num 값을 전달
    return 0;               // 프로그램 정상 종료를 알림
}

int print_num(int num)      // 함수 정의: 실제로 print_num 함수가 어떻게 동작할지 작성
{
    printf("%d\n", num);    // 표준 출력 함수 printf: 전달받은 정수 num을 화면에 출력
    return 0;               // 함수 종료, 호출한 쪽으로 제어 반환
}

2. 주소, 포인터, 배열

  • 주소: 메모리 공간의 위치를 나타내는 값
  • 포인터: 주소를 저장하는 변수
  • 배열: 같은 자료형의 데이터를 연속된 메모리 공간에 저장하는 구조
  • 주소는 상수처럼 취급되는 위치 정보이고, 포인터는 그 주소를 저장하는 변수이다
  • 두 개 이상의 포인터가 하나의 주소를 동시에 가리킬 수 있다
  • 배열 이름은 첫 번째 원소의 주소로 해석될 수 있다
  • 주소 연산에서는 + n 할 때 자료형 크기만큼 자동으로 곱해서 이동한다
int ary[3];

주소 : 100, 104, 108
       a, a + 1, a + 2
ary  : a[0], a[1], a[2]

주소 연산에서 + n 하면 base size(sizeof(int))를 곱해서 계산!
int *pt = 주소;
*pt = pt 주소가 가리키는 값

따라서 int *pt = ary; 가능하다!
#include <stdio.h>    // 표준 입출력 라이브러리: printf, puts 등을 사용하기 위해 포함
#include <stdlib.h>   // 표준 라이브러리: 메모리 관리, 난수 등 다양한 함수 제공

int main(void) {
    int ary[5] = {0, 10, 20, 30, 40}; // 정수형 배열 선언 및 초기화
    int *pt = ary;                    // 배열 이름은 첫 번째 원소의 주소를 가리킴 → 포인터에 저장
    pt = &(ary[0]);                   // 배열의 첫 번째 원소 주소를 직접 대입 (위와 동일한 의미)

    // 반복문: 배열의 원소를 차례대로 출력
    for (int i = 0; i < 5; i++) {
        printf("%d ", *(pt + i));     // 포인터 연산으로 i번째 원소 접근: *(pt + i) == ary[i]
    }

    // 3[ary] == *(3 + ary) == *(ary + 3) == ary[3]
    // 즉, 인덱스와 배열 이름은 덧셈 교환법칙에 의해 같은 의미를 가짐
    3[ary] == ary[3] ?
        puts("3[ary] == ary[3]") :
        puts("3[ary] != ary[3]");

    return 0; // 프로그램 정상 종료
}

3. 이차원 배열과 포인터

이차원 배열의 구조

  • 이차원 배열은 사실상 1차원 배열이 여러 개 모여 있는 형태
  • int arr[4][3];는 4행 3열 배열
  • arr[i]는 i번째 행 자체를 의미
  • arr[i][j]는 i행 j열의 원소를 의미
arr
 ├─ arr[0] → [ arr[0][0] | arr[0][1] | arr[0][2] ]
 ├─ arr[1] → [ arr[1][0] | arr[1][1] | arr[1][2] ]
 ├─ arr[2] → [ arr[2][0] | arr[2][1] | arr[2][2] ]
 └─ arr[3] → [ arr[3][0] | arr[3][1] | arr[3][2] ]

포인터와 이차원 배열

  • 배열 이름 arr는 행을 가리키는 포인터로 해석할 수 있다
  • int (*p)[4];는 열 크기가 4인 배열을 가리키는 포인터
  • p = arr;라고 하면 p는 arr[0]을 가리킨다
  • (p + i)는 i번째 행, *(*(p + i) + j)는 i행 j열 원소를 뜻한다

함수로 전달할 때의 규칙

  • 함수 매개변수로 이차원 배열을 전달할 때는 열 크기를 반드시 지정해야 한다
  • 예: void func(int arr[][4], int rows);
  • 이유: 컴파일러가 한 행의 크기를 알아야 올바른 메모리 주소 계산이 가능하기 때문이다
  • 포인터를 사용하면 배열과 동일한 방식으로 접근할 수 있다
#include <stdio.h>

int main(void) {
    char dim2_ary[][3] = {
        {'f','o','o'},
        {'b','a','r'}
    };

    // 2차원 배열은 열단위로 연속하여 메모리에 저장
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 3; j++) {
            printf("dim2_ary[%d][%d] = %c\n",
                   i, j, *(dim2_ary[i] + j));
        }
    }
    return 0;
}

4. 변수 사용 영역

구분 선언 위치 사용 범위 (Scope) 생존 기간 (Lifetime) 초기값
지역 변수 블록 { } 내부 선언된 블록 안 블록 종료 시 소멸 쓰레기 값
전역 변수 함수 외부 프로그램 전체 프로그램 종료 시 소멸 0
정적 변수 static 키워드 사용 선언된 영역 안 프로그램 종료 시 소멸 0

주의사항

  • 동일한 이름의 지역 변수와 전역 변수가 있다면 가장 가까운 지역 변수가 우선권을 가진다
  • 지역 변수는 스택(Stack), 전역 변수와 정적 변수는 데이터(Data/Static) 영역에 저장된다
  • 버그 예방을 위해 변수의 범위는 가능한 한 좁게 유지하는 것이 좋다
구분 스택 (Stack) 힙 (Heap) 데이터 (Data/Static)
관리 방식 OS/컴파일러가 자동 관리 프로그래머가 수동 관리 (malloc, free) 프로그램 시작 시 준비되고 종료 시 정리
할당 속도 매우 빠름 상대적으로 느림 프로그램 시작 시 결정
기본 크기 작음 (보통 1MB ~ 8MB) 상대적으로 큼 (사용 가능한 RAM 범위 내) 프로그램 크기에 따라 정적 결정
주요 버그 Stack Overflow Memory Leak, 단편화 초기화 순서, 공유 상태 관리 문제

주의

C 언어에서 malloc으로 받은 힙 주소는 프로그램 입장에서는 가상 주소일 수 있다. 운영체제는 스와핑, 압축, 페이지 교체를 수행할 수 있지만, MMU가 주소 변환을 처리하므로 프로그램이 들고 있는 포인터 값 자체는 바뀌지 않는다.

기술 동작 방식 프로그램에 미치는 영향
스와핑 (Swapping) 당장 안 쓰는 메모리 블록(Page)을 하드디스크나 SSD로 옮김 주소는 그대로지만 다시 읽을 때 느려짐
압축 (Memory Compression) RAM 내부 데이터를 압축해서 공간 확보 CPU를 더 쓰지만 디스크 접근보다 빠름
페이지 교체 (Paging) 실제 물리 메모리 위치를 이동시킴 MMU 번역 테이블만 바뀌므로 프로그램은 이동 사실을 모름

예제

주차 예제 실행

아래 박스에서 2주차 예제 실행 결과를 바로 확인할 수 있습니다.

문제 및 해결 과정

문제 1

scanf 사용 후 다음 입력에서 엔터(\n)가 버퍼에 남아 의도하지 않게 입력이 건너뛰어지는 문제가 발생함.

원인 분석

scanf("%d", &num);처럼 숫자를 입력받으면 사용자가 친 엔터 문자는 입력 버퍼에 그대로 남는다. 이후 scanf("%c", &ch);를 호출하면 사용자의 새 입력을 기다리지 않고 남아 있던 개행 문자를 바로 읽어버린다.

해결 방법

입력 함수 사이에서 getchar()를 호출해 버퍼에 남아 있는 개행 문자를 먼저 소비한다.

또는 처음부터 gets(buf) 또는 fgets(buf, sizeof(buf), stdin);를 이용하여 입력받는다.

#include <stdio.h>

int main(void) {
    int num;
    char ch;

    scanf("%d", &num);
    getchar(); // 버퍼에 남은 '\n' 제거
    scanf("%c", &ch);

    return 0;
}

문제 2 (가상의 문제)

세상은 넓고 이상한 디바이스는 많다. 임베디드 개발자라면 MMU가 없는 장치도 고려해야 한다. 이 경우 힙 영역에서 가상 주소를 쓸 수 없으므로 malloc을 무심코 사용하면 시스템 자원이 빠르게 고갈되거나 불안정해질 수 있다는 점을 염두에 두어야 한다.