# C언어 (5)

## 1\. Null 포인터

포인터를 선언만 하고 초기화하지 않으면 **쓰레기값** 이 들어있다. 이 상태에서 역참조(`*p`)를 하면 엉뚱한 메모리에 접근하게 되어 프로그램이 터지거나, 더 위험하게는 조용히 잘못된 데이터를 건드릴 수 있다.

이런 상황을 방지하기 위해 "아직 아무것도 가리키지 않는다"는 의미로 **NULL** 을 대입한다.

```c
int *p = NULL;
```

`NULL` 은 보통 `0` 또는 `(void *)0`으로 정의되어 있다. 유효한 메모리 주소가 아니므로, `NULL` 포인터를 역참조하면 프로그램이 확실하게 크래시한다. 쓰레기값으로 인한 알 수 없는 동작보다는 차라리 확실하게 터지는 것이 디버깅하기 훨씬 쉽다.

포인터를 사용하기 전에 `NULL` 인지 확인하는 것은 C 프로그래밍에서 기본적인 안전 습관이다.

```c
int *p = NULL;

if (p != NULL) {
    printf("%d\n", *p);  // 안전하게 접근
} else {
    printf("포인터가 초기화되지 않았습니다.\n");
}
```

메모리를 해제한 뒤에도 포인터에 `NULL` 을 대입하는 것이 좋다. `free` 한 뒤에도 포인터 변수에는 이전 주소값이 남아있는데, 이런 포인터를 **댕글링 포인터(dangling pointer)** 라고 한다. 이미 해제된 메모리를 다시 접근하면 큰 문제가 생길 수 있으므로, `free` 직후 `NULL` 을 넣어주면 실수를 방지할 수 있다.

```c
int *p = malloc(sizeof(int));
*p = 42;
free(p);
p = NULL;  // 댕글링 포인터 방지
```

* * *

## 2\. 구조체와 공용체

### 2-1. 구조체란?

구조체는 **서로 다른 타입의 데이터를 하나로 묶는 사용자 정의 자료형** 이다.

배열은 같은 타입만 묶을 수 있다. 하지만 현실의 데이터는 그렇지 않다. 학생 한 명의 정보를 표현하려면 이름(문자열), 나이(정수), 평균 점수(실수)가 필요한데, 이걸 각각 별도의 변수로 관리하면 코드가 지저분해진다. 구조체는 이 문제를 해결한다.

```c
struct Student {
    char name[20];
    int age;
    double gpa;
};
```

JavaScript의 객체(`{ name: "홍길동", age: 20, gpa: 3.8 }`)와 비슷한 개념이다. 다만 C의 구조체는 타입을 미리 정의해야 하고, 메서드를 가질 수 없다는 차이가 있다. C++에서 구조체에 함수를 넣을 수 있게 되고, 그것이 곧 클래스로 발전한다.

### 2-2. 구조체 사용

#### 구조체 변수

구조체를 정의한 뒤 변수를 선언하고 사용한다.

```c
struct Student {
    char name[20];
    int age;
    double gpa;
};

int main(void) {
    struct Student s1;  // 구조체 변수 선언

    strcpy(s1.name, "홍길동");  // 문자열은 strcpy로 대입
    s1.age = 20;
    s1.gpa = 3.8;

    printf("이름: %s\n", s1.name);
    printf("나이: %d\n", s1.age);
    printf("학점: %.1f\n", s1.gpa);

    return 0;
}
```

구조체 멤버에 접근할 때는 **점(**`.`**) 연산자** 를 사용한다. `s1.age`는 "`s1`이라는 구조체 안의 `age` 멤버"라는 뜻이다.

선언과 동시에 초기화할 수도 있다.

```c
struct Student s1 = {"홍길동", 20, 3.8};
```

매번 `struct Student`라고 쓰는 것이 번거로우면 `typedef`로 별칭을 만들 수 있다.

```c
typedef struct {
    char name[20];
    int age;
    double gpa;
} Student;

Student s1 = {"홍길동", 20, 3.8};  // struct 키워드 생략 가능
```

### 2-3. 구조체의 배열과 포인터

구조체도 배열로 만들 수 있다. 학생 여러 명의 데이터를 관리할 때 유용하다.

```c
Student students[3] = {
    {"홍길동", 20, 3.8},
    {"김철수", 21, 3.2},
    {"이영희", 19, 4.0}
};

for (int i = 0; i < 3; i++) {
    printf("%s: %.1f\n", students[i].name, students[i].gpa);
}
```

구조체 포인터도 사용할 수 있다. 구조체 포인터로 멤버에 접근할 때는 점(`.`) 대신 **화살표(**`->`**) 연산자** 를 사용한다.

```c
Student s1 = {"홍길동", 20, 3.8};
Student *p = &s1;

printf("이름: %s\n", p->name);   // 화살표 연산자
printf("나이: %d\n", p->age);
```

`p->age`는 `(*p).age`와 같은 의미다. 포인터를 역참조한 뒤 점으로 멤버에 접근하는 것을 화살표 연산자로 줄여 쓴 것이다.

구조체가 크면 함수에 넘길 때 통째로 복사하는 것은 비효율적이다. 포인터로 넘기면 주소 하나만 전달하므로 훨씬 효율적이다.

```c
void printStudent(const Student *s) {
    printf("이름: %s, 나이: %d\n", s->name, s->age);
}

int main(void) {
    Student s1 = {"홍길동", 20, 3.8};
    printStudent(&s1);
    return 0;
}
```

매개변수에 `const`를 붙이면 함수 안에서 구조체를 수정할 수 없게 된다. 읽기만 할 때는 `const`를 붙이는 것이 안전하다.

### 2-4. 공용체

공용체(`union`)는 구조체와 비슷하게 생겼지만, 핵심적인 차이가 있다. 구조체는 모든 멤버가 **각자의 메모리 공간** 을 가진다. 공용체는 모든 멤버가 **같은 메모리 공간을 공유** 한다.

```c
union Data {
    int i;
    float f;
    char c;
};
```

이 공용체의 크기는 가장 큰 멤버(`int` 또는 `float`, 4바이트)의 크기와 같다. `int`, `float`, `char` 세 멤버가 전부 같은 4바이트를 나눠 쓴다.

```c
union Data d;
d.i = 42;
printf("%d\n", d.i);  // 42

d.f = 3.14f;
printf("%f\n", d.f);  // 3.140000
printf("%d\n", d.i);  // 엉뚱한 값 (f를 쓰면서 i가 덮어써짐)
```

한 멤버에 값을 넣으면 다른 멤버의 값은 의미 없어진다. **한 번에 하나의 멤버만 유효하다.** 메모리를 절약해야 하거나, 같은 데이터를 다른 타입으로 해석하고 싶을 때 사용한다. 실무에서는 프로토콜 파싱이나 하드웨어 레지스터 접근에서 가끔 쓰인다.

### 2-5. 열거형 (enum)

열거형은 **관련된 상수들에 이름을 붙이는 자료형** 이다. 매직 넘버(의미를 알 수 없는 숫자) 대신 의미 있는 이름을 사용해서 코드의 가독성을 높인다.

```c
enum Day {
    MON,   // 0
    TUE,   // 1
    WED,   // 2
    THU,   // 3
    FRI,   // 4
    SAT,   // 5
    SUN    // 6
};
```

별도로 값을 지정하지 않으면 0부터 순서대로 정수가 부여된다. 값을 직접 지정할 수도 있다.

```c
enum StatusCode {
    OK = 200,
    NOT_FOUND = 404,
    SERVER_ERROR = 500
};
```

```c
enum Day today = WED;

if (today == SAT || today == SUN) {
    printf("주말\n");
} else {
    printf("평일\n");
}
```

`#define`으로 상수를 정의하는 것과 비슷하지만, `enum` 은 관련된 값들을 하나의 타입으로 묶어준다는 점에서 더 체계적이다. 디버거에서도 숫자 대신 이름으로 보여주므로 디버깅이 편해진다.

* * *

## 3\. 동적 메모리 할당과 메모리 표준함수

### 3-1. 메모리의 구조

C 프로그램이 실행되면 OS로부터 메모리를 할당받는데, 이 메모리는 크게 4개 영역으로 나뉜다.

**코드 영역(Code/Text Segment)** 에는 컴파일된 기계어 코드가 올라간다. 작성한 함수들의 실행 코드가 여기에 저장되며, 읽기 전용이다.

**데이터 영역(Data Segment)** 에는 전역변수와 정적변수(`static`)가 저장된다. 프로그램 시작 시 할당되어 프로그램이 종료될 때까지 유지된다. 초기화된 변수와 초기화되지 않은 변수(BSS 영역)로 다시 나뉘기도 한다.

**스택 영역(Stack)** 에는 지역변수와 함수 호출 정보(매개변수, 반환 주소 등)가 저장된다. 함수가 호출되면 공간이 확보되고, 함수가 끝나면 자동으로 해제된다. 높은 주소에서 낮은 주소 방향으로 자란다.

**힙 영역(Heap)** 은 프로그래머가 직접 할당하고 해제하는 공간이다. `malloc`, `calloc` 등으로 할당하고 `free`로 해제한다. 낮은 주소에서 높은 주소 방향으로 자란다. 스택과 힙이 서로를 향해 자라는 구조다.

```plaintext
높은 주소
┌────────────────┐
│   스택 (Stack)    │  ← 지역변수, 함수 호출 정보 (위에서 아래로 성장)
│       ↓        │
│                │
│       ↑        │
│    힙 (Heap)    │  ← 동적 할당 (아래에서 위로 성장)
├────────────────┤
│  데이터 (Data)   │  ← 전역변수, static 변수
├────────────────┤
│  코드 (Code)    │  ← 실행 코드 (읽기 전용)
└────────────────┘
낮은 주소
```

### 3-2. 동적 메모리 할당

배열을 선언할 때 크기를 상수로 지정해야 한다고 했다. 그런데 실제 프로그램에서는 필요한 메모리 크기를 런타임에야 알 수 있는 경우가 많다. 사용자가 몇 개의 데이터를 입력할지, 파일에 데이터가 몇 줄인지는 프로그램이 실행되어야 알 수 있기 때문이다.

이때 **힙 메모리** 에 원하는 크기만큼 공간을 확보하는 것이 동적 메모리 할당이다.

> **타입캐스팅(Type Casting)**
> 
> 동적 메모리 할당 함수들을 이해하려면 타입캐스팅을 알아야 한다. 타입캐스팅은 하나의 자료형을 다른 자료형으로 변환하는 것이다.
> 
> **암시적 캐스팅** 은 컴파일러가 자동으로 수행한다. `int`를 `double`에 대입하면 자동으로 변환된다. **명시적 캐스팅** 은 프로그래머가 `(타입)`을 직접 붙여서 강제로 변환하는 것이다.
> 
> ```c
> int a = 10;
> double b = a;          // 암시적 캐스팅 (int → double)
> int c = (int)3.14;     // 명시적 캐스팅 (double → int, 소수점 버림)
> ```
> 
> `malloc` 은 `void *` 를 반환하는데, 이것을 원하는 포인터 타입으로 캐스팅해서 사용한다. `void *` 는 "어떤 타입인지 정해지지 않은 포인터"라는 뜻으로, 어떤 포인터 타입으로든 변환할 수 있다.

#### malloc

`malloc`(memory allocation)은 지정한 바이트 수만큼 힙에 메모리를 할당하고, 그 시작 주소를 반환한다.

```c
#include <stdlib.h>

int *p = (int *)malloc(sizeof(int) * 5);  // int 5개 크기 할당
```

`malloc(sizeof(int) * 5)` 는 20바이트를 힙에 확보한다. 반환 타입이 `void *` 이므로 `(int *)`로 캐스팅해서 `int` 포인터에 저장한다. C에서는 `void *` 가 다른 포인터 타입으로 자동 변환되므로 캐스팅을 생략해도 되지만, C++에서는 반드시 캐스팅해야 한다. 명시적으로 쓰는 습관을 들이는 것이 좋다.

할당한 메모리는 배열처럼 사용할 수 있다.

```c
int *p = (int *)malloc(sizeof(int) * 5);

if (p == NULL) {
    printf("메모리 할당 실패\n");
    return 1;
}

for (int i = 0; i < 5; i++) {
    p[i] = (i + 1) * 10;
}

for (int i = 0; i < 5; i++) {
    printf("%d ", p[i]);  // 10 20 30 40 50
}

free(p);
p = NULL;
```

`malloc` 은 할당에 실패하면 `NULL` 을 반환한다. 메모리가 부족하면 실패할 수 있으므로, 반드시 `NULL` 체크를 해야 한다.

`malloc` 으로 할당한 메모리는 **초기화되지 않는다.** 쓰레기값이 들어있다.

#### free

`free`는 `malloc`으로 할당한 메모리를 OS에 반환한다.

```c
int *p = (int *)malloc(sizeof(int) * 10);
// ... 사용 ...
free(p);     // 메모리 해제
p = NULL;    // 댕글링 포인터 방지
```

`free` 는 반드시 `malloc`, `calloc`, `realloc`으로 할당한 메모리에만 사용해야 한다. 스택에 있는 지역변수의 주소를 `free` 하면 정의되지 않은 동작이 발생한다. 같은 메모리를 두 번 `free` 하는 것(double free)도 위험하다.

JavaScript에서는 가비지 컬렉터가 알아서 메모리를 해제해주지만, C에서는 프로그래머가 직접 해야 한다. 이것이 C에서 메모리 관리가 어렵다고 하는 핵심 이유다.

#### calloc

`calloc`(contiguous allocation)은 `malloc`과 비슷하지만 두 가지 차이가 있다. 매개변수를 "요소 개수"와 "요소 크기"로 나눠서 받고, 할당한 메모리를 **0으로 초기화** 해준다.

```c
int *p = (int *)calloc(5, sizeof(int));  // int 5개, 전부 0으로 초기화
```

`malloc(sizeof(int) * 5)` 와 같은 크기를 할당하지만, `calloc` 은 모든 바이트를 0으로 채워준다. 배열을 0으로 시작해야 하는 경우에 편리하다.

#### realloc

`realloc`(reallocation)은 이미 할당한 메모리의 **크기를 변경** 한다. 배열 크기가 부족해졌을 때 늘리거나, 너무 클 때 줄일 수 있다.

```c
int *p = (int *)malloc(sizeof(int) * 3);
p[0] = 10;
p[1] = 20;
p[2] = 30;

// 크기를 5로 늘림
p = (int *)realloc(p, sizeof(int) * 5);
p[3] = 40;
p[4] = 50;
```

`realloc` 은 가능하면 기존 위치에서 크기를 늘린다. 공간이 부족하면 새로운 위치에 메모리를 할당하고, 기존 데이터를 복사한 뒤, 이전 메모리를 해제한다. 기존 데이터는 보존된다.

다만 `realloc` 도 실패하면 `NULL` 을 반환한다. 이때 원래 포인터에 바로 대입하면 기존 메모리 주소를 잃어버려 메모리 누수가 발생한다. 안전하게 사용하려면 임시 포인터를 쓰는 것이 좋다.

```c
int *temp = (int *)realloc(p, sizeof(int) * 10);
if (temp == NULL) {
    printf("재할당 실패\n");
    free(p);  // 기존 메모리는 수동으로 해제
    return 1;
}
p = temp;
```

#### 메모리 누수

메모리 누수(memory leak)는 `malloc`으로 할당한 메모리를 `free` 하지 않아서 반환되지 않은 메모리가 쌓이는 현상이다.

```c
void leak(void) {
    int *p = (int *)malloc(sizeof(int) * 100);
    // free(p)를 하지 않고 함수 종료
}  // p는 지역변수이므로 사라지지만, 힙 메모리는 그대로 남음
```

`p`는 스택에 있는 지역변수이므로 함수가 끝나면 사라진다. 하지만 `p`가 가리키던 힙 메모리는 `free` 하지 않았으므로 그대로 남아있다. 이 메모리에 접근할 방법도 없고, 해제할 방법도 없다. 이것이 메모리 누수다.

짧은 프로그램에서는 큰 문제가 되지 않을 수 있다(프로그램 종료 시 OS가 전부 회수하므로). 하지만 서버처럼 오래 실행되는 프로그램에서는 메모리가 점점 쌓여서 결국 시스템을 다운시킬 수 있다.

원칙은 간단하다 — `malloc` **한 만큼** `free` **한다.**

### 3-3. 메모리 표준함수

`<string.h>`에 포함된 메모리 조작 함수들이다. 문자열뿐 아니라 모든 메모리 영역에 사용할 수 있다.

#### memset

메모리 블록을 **특정 값으로 채운다.** 배열을 0으로 초기화하거나 특정 값으로 채울 때 사용한다.

```c
#include <string.h>

int arr[5];
memset(arr, 0, sizeof(arr));  // 전부 0으로 채움
```

`memset` 은 **바이트 단위** 로 값을 채운다. 두 번째 인자가 각 바이트에 들어갈 값이다. `0`으로 채우는 것은 문제없지만, `int` 배열을 `1`로 채우겠다고 `memset(arr, 1, sizeof(arr))`를 하면 각 바이트가 `0x01`이 되어 의도한 정수 `1`이 아니라 `0x01010101`(약 1677만)이 들어간다. `0`이 아닌 값으로 `int` 배열을 초기화하려면 반복문을 사용해야 한다.

#### memcpy

한 메모리 블록의 내용을 다른 곳으로 **복사** 한다.

```c
int src[5] = {1, 2, 3, 4, 5};
int dest[5];

memcpy(dest, src, sizeof(src));
// dest는 이제 {1, 2, 3, 4, 5}
```

첫 번째 인자가 목적지, 두 번째가 출발지, 세 번째가 복사할 바이트 수다. `strcpy`는 문자열만 복사할 수 있지만, `memcpy`는 어떤 타입의 메모리든 복사할 수 있다.

단, 출발지와 목적지 메모리가 **겹치면 안 된다.** 겹치는 경우에는 `memmove`를 사용해야 한다.

#### memcmp

두 메모리 블록을 **바이트 단위로 비교** 한다.

```c
int a[3] = {1, 2, 3};
int b[3] = {1, 2, 3};
int c[3] = {1, 2, 4};

printf("%d\n", memcmp(a, b, sizeof(a)));  // 0 (같음)
printf("%d\n", memcmp(a, c, sizeof(a)));  // 음수 (a < c)
```

반환값이 `0`이면 같고, 양수면 첫 번째가 크고, 음수면 두 번째가 크다. `strcmp`가 문자열을 비교하는 것처럼, `memcmp`는 임의의 메모리를 비교한다. 구조체 두 개를 통째로 비교할 때도 사용할 수 있다.

#### memmove

`memcpy`와 같은 기능이지만, 출발지와 목적지 메모리가 **겹쳐도 안전하게 동작** 한다.

```c
int arr[5] = {1, 2, 3, 4, 5};

// arr[0~2]의 내용을 arr[1~3]으로 이동 (겹침 발생)
memmove(&arr[1], &arr[0], sizeof(int) * 3);
// arr는 이제 {1, 1, 2, 3, 5}
```

`memcpy` 는 앞에서부터 순서대로 복사하기 때문에, 겹치는 영역에서는 아직 복사하지 않은 데이터를 덮어쓸 수 있다. `memmove` 는 내부적으로 겹침을 감지하고, 필요하면 뒤에서부터 복사하는 등의 방법으로 안전하게 처리한다.

성능은 `memcpy`가 약간 더 빠를 수 있다. 메모리가 겹치지 않는 것이 확실하면 `memcpy`를, 겹칠 가능성이 있으면 `memmove`를 사용한다.

* * *

## 마무리

이번 글에서는 C언어의 중급 개념들을 다뤘다. `NULL` 포인터로 안전한 포인터 사용의 기초를 다지고, 구조체로 복잡한 데이터를 하나로 묶는 방법을 배웠다. 동적 메모리 할당은 C 프로그래밍에서 가장 강력하면서도 가장 위험한 기능이다. `malloc`으로 할당하고 `free`로 해제하는 것, 그리고 그 사이에서 `NULL` 체크와 메모리 누수에 주의하는 것이 핵심이다. 메모리 표준함수들은 이런 메모리를 효율적으로 조작하는 도구다.
