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을 무심코 사용하면 시스템 자원이 빠르게 고갈되거나 불안정해질 수 있다는 점을 염두에 두어야 한다.