[C언어 50강] 04강. 변수와 상수: 선언/초기화, 네이밍 규칙, 스코프 맛보기
변수와 상수는 C언어에서 ‘값을 다루는 질서’를 만드는 가장 기본 단위입니다. 초보 단계에서는 변수 선언 문법만 외우고 넘어가기 쉽지만, 실무에서는 변수 이름 하나, 초기화 시점 하나, 스코프 설계 하나가 버그를 만들기도 막기도 합니다. 오늘은 "어떻게 쓰는가"보다 "왜 그렇게 써야 하는가"에 초점을 맞춰, 선언/초기화/네이밍/스코프를 하나의 개념 체계로 정리해보겠습니다.
핵심 개념
- 변수는 단순한 ‘상자’가 아니라, 타입·수명·가시범위를 함께 가진 메모리 사용 계약이다.
- 상수는 값을 바꾸지 않겠다는 약속이며, 코드 안정성과 의도를 명확하게 만들어준다.
- 스코프를 좁힐수록 코드의 예측 가능성이 높아지고, 디버깅 비용이 크게 줄어든다.
개념 먼저 이해하기
변수를 처음 배울 때 흔히 듣는 설명은 “변수는 값을 저장하는 공간”입니다. 틀린 말은 아니지만, 실무에서 이 설명만 믿고 코드를 쓰면 금방 한계에 부딪힙니다. C에서 변수는 단순 저장소가 아니라 **타입(type), 초기값(initialization), 스코프(scope), 수명(lifetime)**이라는 네 가지 축으로 관리되는 객체입니다. 즉, 변수 한 줄은 “이 값은 어떤 형태로, 언제부터 언제까지, 어디서 접근할 수 있는가”를 한 번에 정의하는 선언입니다.
먼저 타입 관점부터 보겠습니다. int count;는 숫자를 저장한다는 뜻을 넘어서, 메모리에서 몇 바이트를 쓰고 어떤 연산 규칙을 따를지까지 결정합니다. 타입이 잘못되면 계산 정확도, 오버플로 위험, 출력 형식 모두가 연쇄적으로 흔들립니다. 그래서 “일단 int로 선언하고 보자”는 습관은 작은 예제에서는 넘어가도, 데이터 크기와 범위가 커지는 순간 위험해집니다. 변수 선언은 곧 데이터 모델링의 시작입니다.
다음은 초기화입니다. C는 초기화되지 않은 지역 변수를 자동으로 0으로 채워주지 않습니다. 즉 int x; printf("%d", x);는 운이 좋으면 0처럼 보여도, 사실은 쓰레기 값을 읽는 코드입니다. 이 지점이 다른 고수준 언어와 비교해 C가 까다롭게 느껴지는 핵심 이유 중 하나입니다. 하지만 반대로 말하면, 개발자가 메모리 상태를 명시적으로 제어할 수 있다는 뜻이기도 합니다. 실무에서는 이 자유가 강력한 장점이 되지만, 전제는 “항상 초기화한다”는 규율입니다.
상수는 “고정된 숫자” 정도로 배우고 넘어가는 경우가 많습니다. 그러나 상수의 본질은 값 고정이 아니라 의도 고정에 가깝습니다. 예를 들어 0.1을 코드 여기저기에 직접 쓰는 것보다 const double TAX_RATE = 0.1;로 선언하면, 이 값이 무엇을 의미하는지 드러나고 변경 영향 범위도 즉시 추적됩니다. 또 컴파일러가 실수로 값을 바꾸는 코드를 잡아줄 수 있어서 방어력이 올라갑니다. 즉 상수는 가독성과 안정성을 동시에 올리는 설계 장치입니다.
네이밍 규칙은 스타일 취향 문제가 아니라 협업 생산성 문제입니다. a, tmp, data2 같은 이름은 작성 순간에는 빨라 보이지만, 며칠 뒤 읽을 때는 맥락 손실을 부릅니다. 반대로 total_price, retry_count, max_buffer_size처럼 역할이 드러나는 이름은 주석을 줄이고 리뷰 시간을 줄입니다. C 프로젝트는 함수 분리와 파일 분리가 많아지면서 변수의 의미를 이름에 실어야 하는 빈도가 더욱 높습니다. 이름을 잘 짓는 것은 문학이 아니라 유지보수 비용 절감입니다.
마지막으로 스코프입니다. 스코프는 “어디서 보이느냐”의 문제인데, 실제로는 “누가 바꿀 수 있느냐”의 문제이기도 합니다. 전역 변수를 쉽게 열어두면 어디서든 접근 가능해 편해 보이지만, 디버깅할 때 값이 언제 바뀌었는지 추적이 어려워집니다. 반대로 변수를 최대한 좁은 블록 내부에서 선언하면 변경 가능 지점이 줄어들고, 오류 원인도 빠르게 좁혀집니다. 그래서 현대 C 코딩 습관은 "필요한 가장 좁은 스코프에 선언"입니다.
정리하면, 변수와 상수를 다루는 핵심은 문법이 아니라 설계 감각입니다. 어떤 타입이 맞는지, 언제 초기화할지, 어떤 이름이 의도를 드러내는지, 어디까지 보이게 둘지. 이 네 가지를 한 세트로 생각하면 이후 조건문/반복문/함수/포인터 단계에서도 코드 품질이 안정적으로 유지됩니다.
기본 사용
예제 1) 선언과 초기화의 기본 원칙
#include <stdio.h>
int main(void) {
int user_age = 27; // 선언과 동시에 초기화
double account_balance = 0; // 0으로 초기화(암묵적 형변환 허용)
char grade = 'A';
printf("age=%d, balance=%.2f, grade=%c\n", user_age, account_balance, grade);
return 0;
}
설명:
- 지역 변수는 사용 전에 반드시 초기화하는 습관이 안전합니다.
- 선언과 초기화를 붙이면 누락 가능성이 줄어듭니다.
- 이름에 역할(
user_age,account_balance)을 담으면 읽는 속도가 빨라집니다.
예제 2) 상수로 의도를 고정하는 패턴
#include <stdio.h>
int main(void) {
const double TAX_RATE = 0.10;
const int FREE_SHIPPING_MIN = 50000;
int item_price = 42000;
double final_price = item_price + (item_price * TAX_RATE);
printf("final_price=%.0f\n", final_price);
printf("free_shipping=%s\n", item_price >= FREE_SHIPPING_MIN ? "YES" : "NO");
return 0;
}
설명:
- 매직 넘버를 직접 쓰지 않고 상수명으로 의미를 전달합니다.
- 비즈니스 규칙(세율, 무료배송 기준)을 한 곳에서 관리할 수 있습니다.
const는 실수로 값이 변경되는 사고를 컴파일 단계에서 막아줍니다.
예제 3) 스코프를 좁혀 버그 가능성을 줄이기
#include <stdio.h>
int main(void) {
int total = 0;
for (int i = 1; i <= 3; i++) {
int price = i * 1000; // price는 반복 블록 안에서만 의미가 있음
total += price;
printf("loop=%d, price=%d, total=%d\n", i, price, total);
}
// 여기서는 price에 접근할 수 없음: 의도된 제한
printf("final total=%d\n", total);
return 0;
}
설명:
price를 루프 내부에 두면, 외부에서 잘못 쓰는 실수를 원천 차단합니다.- 스코프 제한은 컴파일러 오류를 통해 조기 피드백을 줍니다.
- “가능한 좁은 범위 선언”은 C에서 매우 강력한 방어 습관입니다.
자주 하는 실수
실수 1) 초기화하지 않은 지역 변수 사용
- 원인: "어차피 기본값 0이겠지"라는 다른 언어 습관.
- 해결: 선언 즉시 초기화. 의미 있는 초기값이 없다면 0/NULL 등 정책값을 명확히 둔다.
실수 2) 의미 없는 변수명 사용
- 원인: 작성 속도를 우선하고 읽기 비용을 과소평가함.
- 해결: 역할 중심 네이밍(
retry_count,max_users,is_valid)으로 의도를 드러낸다.
실수 3) 매직 넘버 남발
- 원인: 빠르게 구현하려고 숫자를 직접 박아 넣음.
- 해결:
const상수로 추출하고, 단위/맥락이 드러나는 이름을 부여한다.
실수 4) 전역 변수에 과도하게 의존
- 원인: 함수 간 값 전달이 귀찮아서 전역으로 우회.
- 해결: 매개변수/반환값 설계를 우선하고, 정말 필요한 공유 상태만 제한적으로 전역화한다.
실무 패턴
- 선언은 사용 지점 가까이: 함수 맨 위 몰아쓰기보다 실제 사용 직전에 선언해 스코프를 줄인다.
- const 우선 사고: "이 값이 바뀌어야 하나?"를 먼저 묻고, 아니면
const를 기본으로 둔다. - 네이밍 컨벤션 고정: 예)
snake_case+ 불리언은is_/has_접두어 사용. - 매직 넘버 금지 규칙: 의미 있는 숫자는 전부 상수화하여 정책 변경 비용을 줄인다.
- 경고 0개 유지:
-Wall -Wextra -Wpedantic에서 깨끗한 상태를 일상화한다.
오늘의 결론
한 줄 요약: 변수와 상수는 값 저장 도구가 아니라, 타입·의도·가시범위를 설계해 코드의 안정성과 유지보수성을 만드는 기본 장치다.
연습문제
- 할인율, 배송비 기준, 최대 재시도 횟수를 상수로 선언하고, 주문 금액에 따라 최종 금액을 계산하는 프로그램을 작성하세요.
- 같은 기능을 가진 코드 두 버전을 만들어 비교하세요: (a) 전역 변수 중심, (b) 지역 변수 + 매개변수 중심. 어떤 버전이 디버깅하기 쉬운지 이유를 쓰세요.
- 일부러 의미 없는 변수명으로 작성한 뒤, 역할 중심 이름으로 리팩터링해 가독성 차이를 정리하세요.
이전 강의 정답
- 3강 연습문제 1) 종료 코드 비교
return 0;은 정상 종료,return 1;은 비정상/오류 종료를 의미합니다.- 터미널에서 실행 후
echo $?로 직전 프로그램의 종료 코드를 확인할 수 있습니다.
- 3강 연습문제 2)
printf형식 지정자 실험- 변수 타입과 형식 지정자가 불일치하면 경고가 발생하거나 출력이 깨질 수 있습니다.
- 핵심은 “보이는 결과”보다 “타입 계약을 정확히 맞추는 것”입니다.
- 3강 연습문제 3) 좋은 주석 vs 나쁜 주석
- 나쁜 주석: 코드의 표면 행동만 번역 (
i++ // i 증가) - 좋은 주석: 의도/제약/선택 이유를 설명 (
재시도 정책상 최대 3회 제한)
- 나쁜 주석: 코드의 표면 행동만 번역 (
실습 환경/재현 정보
- 컴파일러: clang 17+ 또는 gcc 13+
- 컴파일 옵션:
-std=c11 -Wall -Wextra -Wpedantic -O0 -g - 실행 환경: macOS 15+ / Linux 터미널
- 재현 체크:
clang lesson04.c -o lesson04 -std=c11 -Wall -Wextra -Wpedantic -O0 -g./lesson04- 경고가 발생하면 변수 초기화/형식 지정자/스코프를 우선 점검