
변수란, 변경 가능한 수를 의미한다.
프로그래밍 시 데이터를 메모리에 저장 후, 해당 데이터를 다시 꺼내 사용하려면
메모리 상 저장된 주소값을 알아야한다.
메모리 주소는 0x1000 0x1001 과 같은 형태로 저장되는데,
수 많은 데이터를 저장하고 꺼내써야하는 상황에서 주소값들을 모두 기억하기는 어렵다.
때문에 우리가 기억하기 쉬운 별명에 메모리 주소값을 저장하여 사용하는데, 이를 ‘변수’라고 칭한다.
변수는 선언 위치와 메모리 영역에 따라 다음과 같이 구분된다.
지역변수는 함수 내부에서 선언되는 변수이다.
지역변수는 함수가 호출될 때 생성되고, 함수 실행이 끝나면 스택 메모리에서 제거된다.
즉, 해당 함수의 실행 범위 안에서만 사용할 수 있는 변수이다.
예시:
void func()
{
int local = 10; // 지역변수
}
지역변수는 스택(Stack) 영역에 저장되며 함수 호출이 끝나면 메모리에서 자동으로 소멸한다.
매개변수 역시 함수 내부에서 사용되는 지역변수의 한 종류이다.
함수가 호출될 때 전달받은 값을 저장하기 위해 생성된다.
int func(int a, int b)
{
int result = 0;
result = a + b;
return result;
}
위 코드에서 a, b는 함수 호출 시 전달되는 값을 저장하는 매개변수이며
이 또한 스택 메모리 영역에 저장되는 지역변수이다.
전역변수는 함수 밖에서 선언된 변수이다.
int global = 10;
void func() { printf("%d", global); }
전역변수는 프로그램 전체에서 접근할 수 있으며
스택 영역이 아닌 데이터(Data) 영역에 저장된다.
또한 전역변수는 프로그램이 시작될 때 생성되고
프로그램이 종료될 때 메모리에서 소멸한다.
static 변수는 지역변수처럼 함수 내부에서 선언되지만, 전역변수처럼 메모리에 유지되는 변수이다.
void func()
{
static int count = 0;
count++;
}
일반적인 지역변수는 함수가 종료되면 스택에서 제거되지만
static 변수는 데이터 영역에 저장되며 프로그램이 종료될 때까지 유지된다.
메모리는 크게 4가지 영역으로 나뉘며, 각 역할은 아래와 같다.
위 영역 중 변수를 저장할 땐 스택 메모리가 사용된다.
명칭에서 알 수 있듯, Stack 형태로 싸이며 Last In First Out의 특성을 띈다.
변수에 담긴 데이터의 타입에 따라서 저장구조가 달라질 수 있는데,
a = 10 ) 스택 메모리에 주소/값 형태로 저장된다.a = [1, 2, 3])문자열은 메모리에 저장될 때 문자들이 순서대로 저장되며, 문자열의 끝에는 반드시 \0(널 문자, NULL 문자) 가 함께 저장된다.
예를 들어 다음과 같은 문자열이 있다고 가정해보자.
char str[] = "hello";
이 문자열은 메모리에 다음과 같이 저장된다.
h e l l o \0 sadfsdkfljas;fj
각 문자는 1바이트의 메모리를 차지하며, 마지막에 저장되는 \0은 문자열의 끝을 나타내는 역할을 한다.
C 언어에서는 문자열의 길이를 따로 저장하지 않기 때문에, 문자열을 처리하는 함수들은 이 \0을 기준으로 문자열의 끝을 판단한다.
예를 들어 printf()나 strlen() 같은 함수들은 문자열을 읽다가 \0을 만나면 문자열이 끝났다고 판단하고 처리를 멈춘다.
따라서 C 언어에서 문자열을 다룰 때는 문자열의 끝을 표시하는 NULL 문자(\0)의 존재가 매우 중요하다.
상수란, 변하지 않는 수를 의미한다.
메모리 공간에 수가 존재하지만, 그 값을 변경할 수 없다. (재할당 불가)
변수 선언시, const 기호를 붙여주면 상수화된다.
주의할 점은, 아래와 같이 선언과 동시에 반드시 초기화 해야한다.
// 올바른 예시
const int TEN = 10;
// 올바르지 않은 예시
const int TEN;
TEN = 10;
C언어에서 사용자로부터 데이터를 입력 받을 시 사용하는 함수는 scanf() 이다.
데이터 입력 함수 예시를 살펴보면 서식 문자열과 &변수를 포함하고 있다.
scanf("%d", &input);
이때 변수 앞에 붙은 기호 & 은 주소 연산자이다.
컴퓨터에 input 값을 입력할 때 대부분 키보드 또는 마우스를 사용할 것이다.
키보드나 마우스는 하드웨이고, 하드웨어와 가장 밀접한 소프트웨어는 운영체제(OS)이다.
입력한 값은 운영체제의 시스템 큐에 전달되어 저장된다.
반복문은 특정 조건이 만족되는 동안 동일한 작업을 반복적으로 수행하기 위해 사용하는 구조이다.
프로그램에서는 동일한 로직을 여러 번 실행해야 하는 경우가 매우 많다.
예를 들어 다음과 같은 상황을 생각해볼 수 있다.
이런 작업을 반복문 없이 구현한다면 같은 코드를 여러 번 작성해야 한다.
반복문은 이러한 중복되는 작업을 효율적으로 처리하기 위한 제어 구조이다.
C 언어에서 대표적으로 사용되는 반복문은 다음과 같다.
while : 조건이 참인 동안 반복for : 반복 횟수가 명확할 때 사용while 문은 조건식이 참(true)인 동안 계속해서 코드 블록을 실행한다.
int i = 1;
while(i <= 5)
{
printf("%d\n", i);
i++;
}
실행 흐름은 다음과 같다.
반복문 안에 또 다른 반복문이 들어가는 구조를 이중 반복문(nested loop) 이라고 한다.
이 구조는 큰 반복 안에서 작은 반복이 함께 수행되는 구조이다.
이를 이해하기 쉽게 비유하면 지구의 공전과 자전과 비슷하다.
공전(큰 반복)
└ 자전(작은 반복)
이러한 구조가 바로 이중 반복문의 개념과 유사하다.
int i = 0;
int j = 0;
while(i < 3)
{
j = 0;
while(j < 3)
{
printf("%d %d\n", i, j);
j++;
}
i++;
}
위 코드는 다음과 같은 순서로 실행된다.
(0,0)
(0,1)
(0,2)
(1,0)
(1,1)
(1,2)
(2,0)
(2,1)
(2,2)
즉 외부 반복이 한 번 실행될 때마다 내부 반복이 전체 실행되는 구조이다.
이러한 구조는 다음과 같은 문제를 해결할 때 자주 사용된다.
배열(Array)은 같은 타입의 데이터를 연속된 메모리 공간에 저장하는 자료구조이다.
하나의 변수에 여러 개의 값을 저장할 수 있으며, 각 값은 인덱스(index) 를 통해 접근한다.
예를 들어 다음과 같은 배열이 있다고 가정해보자.
int arr[5];
이 배열은 메모리 상에 다음과 같이 연속된 구조로 저장된다.
arr[0] arr[1] arr[2] arr[3] arr[4]
각 요소는 자신의 데이터 타입만큼의 메모리 공간을 차지한다.
예를 들어 int 타입이 4byte라면 아래와 같이 연속된 메모리 공간에 저장되는 구조를 가진다.
arr[0] → 4byte
arr[1] → 4byte
arr[2] → 4byte
arr[3] → 4byte
arr[4] → 4byte
배열은 선언과 동시에 초기화할 수 있다.
int arr[5] = {1, 2, 3, 4, 5};
위 코드는 다음과 같은 의미를 가진다.
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5
또한 배열 크기를 생략하고 초기화할 수도 있다.
int arr[] = {1, 2, 3, 4, 5};
이 경우 컴파일러가 초기화된 데이터 개수를 기준으로 배열 크기를 자동으로 결정한다.
C 언어에서는 배열 전체를 대입 연산자로 복사할 수 없다.
다음과 같은 코드는 컴파일 에러가 발생한다.
int arr1[5] = {1,2,3,4,5};
int arr2[5];
arr2 = arr1; // 오류 발생
배열은 단일 값이 아니라 여러 개의 메모리 공간으로 이루어진 데이터 집합이기 때문이다.
따라서 배열을 복사하려면 각 요소를 하나씩 복사해야 한다.
int arr1[5] = {1,2,3,4,5};
int arr2[5];
for(int i = 0; i < 5; i++) { arr2[i] = arr1[i]; }
위 코드처럼 반복문을 사용하여 각 요소를 개별적으로 복사해야 한다.
이는 배열이 연속된 메모리 공간에 여러 개의 데이터가 저장된 구조이기 때문이다.
포인터(pointer)는 메모리의 주소값을 저장하는 변수이다.
일반적인 변수는 메모리에 실제 값을 저장하지만,
포인터 변수는 특정 메모리 공간의 주소를 저장하고 해당 메모리를 가리킨다.
즉 포인터는 어떤 데이터가 저장된 메모리 위치를 기억하는 변수라고 볼 수 있다.
예를 들어 다음과 같은 변수가 있다고 가정해보자.
char a = 65;
int b = 100;
double c = 3.14;
각 변수는 메모리에 다음과 같이 저장된다.
주소 값
0x01 → 65
0x02 → 100
0x03 → 3.14
포인터 변수는 이러한 메모리 주소값을 저장하여 해당 데이터를 간접적으로 참조할 수 있게 한다.
포인터 변수는 변수 선언 시 * 기호를 사용하여 선언한다.
char *pA;
int *pB;
double *pC;
위 선언의 의미는 다음과 같다.
pA → char 타입 데이터를 가리키는 포인터
pB → int 타입 데이터를 가리키는 포인터
pC → double 타입 데이터를 가리키는 포인터
포인터 변수는 주소값을 저장하기 때문에 반드시 가리킬 데이터 타입을 함께 명시해야 한다.
변수의 주소값은 주소 연산자 & 를 통해 얻을 수 있다.
int b = 100;
int *pB = &b;
이 코드는 다음과 같은 의미를 가진다.
b : 값 100 저장
pB : 변수 b의 메모리 주소 저장
즉 포인터 변수 pB는 변수 b의 주소를 가리키는 포인터가 된다.
C 언어에서 배열과 포인터는 매우 밀접한 관계를 가진다.
배열은 메모리 상에 연속된 공간으로 저장되며,
배열 이름은 배열의 첫 번째 요소의 주소값을 의미한다.
예를 들어 다음과 같은 배열이 있다고 가정해보자.
int arr[5] = {1, 2, 3, 4, 5};
메모리는 다음과 같이 구성된다.
주소 값
0x12ff4c → 1
0x12ff50 → 2
0x12ff54 → 3
0x12ff58 → 4
0x12ff5c → 5
이때 배열 이름 arr은 첫 번째 요소의 주소를 의미한다.
즉 다음 두 표현은 동일한 값을 가진다.
arr == &arr[0]
따라서 배열 이름은 배열의 시작 주소를 가리키는 포인터처럼 동작한다.
배열 이름은 주소값을 가지지만 일반 포인터와 중요한 차이가 있다.
배열 이름은 상수 포인터(constant pointer) 와 같은 특성을 가진다.
즉 주소값을 변경할 수 없다.
#include <stdio.h>
int main(void)
{
int arr[] = {1, 2, 3, 4, 5};
int temp = 10;
arr = temp; // 오류 발생
return 0;
}
위 예시에서 배열 이름 arr은 주소값을 가지고 있지만
상수처럼 동작하기 때문에 다른 주소를 대입할 수 없다.
C 언어에서 함수의 기본적인 인자 전달 방식은 값에 의한 호출(Call by Value) 이다.
즉 함수에 인자를 전달할 때 값이 복사되어 전달된다.
#include <stdio.h>
void Temp(int b) { b = 20; }
int main(void)
{
int a = 10;
Temp(a);
printf("%d", a); // 결과: 10
return 0;
}
위 코드에서는 a의 값이 함수로 전달될 때 값이 복사되어 전달된다.
a → 10
b → 10 (복사된 값)
따라서 함수 내부에서 b 값을 변경해도 원래 변수 a에는 영향을 주지 않는다.
하지만 포인터를 사용하면 변수의 주소를 전달할 수 있다.
이 경우 함수 내부에서 원래 변수의 값을 직접 변경할 수 있다.
void Temp(int *b) { *b = 20; }
int main(void)
{
int a = 10;
Temp(&a);
printf("%d", a); // 결과: 20
}
여기서는 다음과 같은 과정이 발생한다.
&a → a의 주소 전달
b → a의 주소 저장
*b → a의 실제 값 접근
즉 포인터를 사용하면 함수 내부에서 원본 변수의 값을 수정할 수 있다.
이러한 방식은 일반적으로 Call by Reference 형태의 동작이라고 표현한다.