C언어 학습 노트 : 11. 메모리와 포인터#

1. 메모리 종류(프로세스 관점)#

  • Stack(스택)

    • 함수 호출 시 생성되는 자동 변수 저장. LIFO.
    • 각 스레드마다 독립 스택이 생성된다.
  • Heap(힙)

    • 동적 할당 영역. malloc/calloc/realloc/free로 관리한다.
  • 실행 코드/데이터(이미지 섹션)

    • Text(Code) section: 기계어 코드. 실행 전용.
    • RO Data: 변경 불가 상수(문자열 리터럴 등).
    • RW Data / BSS: 전역/정적 변수의 초기화된/미초기화 구역(정적 저장 영역).

주의: Windows는 PE, 리눅스/유닉스는 ELF 형식이 일반적이나, 개념(코드/데이터/정적 영역) 자체는 유사하다.


2. 가상 메모리#

  • 가상 메모리는 “RAM + 디스크를 이어붙인다”라기보다, 각 프로세스에 연속적인 가상 주소 공간을 제공하고 이를 물리 메모리/백킹 스토어에 매핑하는 추상화이다.
  • 스택/힙/코드/데이터 모두 프로세스의 가상 주소 공간 안에 존재한다.

3. 정적 영역#

  • 전역 변수, 정적 지역 변수(static), 문자열 리터럴(보통 RO) 이 여기에 위치한다.
  • 수명은 프로그램 시작부터 종료까지이다(정적 저장 기간).

4. 실행 코드 구조#

  • 실행 파일은 여러 섹션(code, rodata, data, bss 등)으로 구분되며, 섹션별 보호 속성(실행/읽기/쓰기)이 다르고 메모리 배치도 구분되어 로드된다.
  • (그림을 넣을 계획이라면: “Text → RO Data → Data/BSS → Heap → … ← Stack” 순의 개략도 권장)

5. 포인터 변수#

  • 포인터는 주소를 저장하는 변수이다. 포인터 자체의 값은 “어디를 가리키는가”를 의미한다.
  • 포인터가 가리키는 대상의 저장 위치는 스택/힙/정적 어느 곳이든 될 수 있다.

6. 포인터와 1차원 배열#

  • 배열은 포인터가 아니다. 다만 대부분의 식에서 배열 이름이 “첫 원소의 포인터”로 변환(decay) 된다.

    • int arr[5]; 에서

      • arr → (대부분의 식에서) int* 로 변환
      • &arrint (*)[5] (배열 전체의 포인터, 타입이 다름)
  • 인덱싱 등가식: arr[i] == *(arr + i)

  • 크기 차이: sizeof(arr) == 5*sizeof(int), sizeof(int*)는 보통 8바이트(64비트 환경) 등 서로 다르다.

  • 배열 이름은 재대입 불가(비상수 lvalue가 아님), 포인터 변수는 재대입 가능.


7. 메모리 동적 할당 및 관리#

  • 힙 사용: malloc(size)는 초기화하지 않은 메모리를, calloc(n,sz)0으로 초기화된 메모리를 반환한다.
  • 새로 할당받은 메모리는 읽기 전에 반드시 값을 써서 초기화해야 한다(미정 의 값 사용 금지).
  • 용량이 매우 크면 0 초기화에 비용이 든다. 필요한 경우에만 calloc을 선택한다.
  • 해제는 반드시 free(p)로 대응한다(누수 방지).

8. 메모리 관리 단위#

  • 페이지(page): 운영체제가 메모리를 관리하는 기본 단위(일반적으로 4KB).
  • (Windows 특이점) 할당 그라뉴러리(allocation granularity) 64KB 개념이 있으나 이는 특정 API(VirtualAlloc) 레벨의 영역 관리 단위이다. C의 malloc 사용자 입장에서는 페이지/슬랩/풀 등 할당기 내부 전략으로 추상화된다.

9. calloc#

  • 동적 할당과 제로 초기화를 동시에 수행한다.
  • 문자열 버퍼처럼 “초기값이 0이어야 의미가 명확한” 경우에 적합하다.

10. 메모리 값 복사/비교#

  • memcpy(dst, src, n): 바이트 단위 복사. 구조체 내부에 포인터가 있으면 “깊은 복사”가 아니다. (얕은 복사)
  • memcmp(a, b, n): 바이트 단위 비교. 길이 n을 반드시 명시한다.

문자열 관련#

  • strcpy(dst, src): NULL 종료 문자열 복사. 버퍼 크기 검증이 없으므로 위험.

    • MS 계열: strcpy_s(dst, dstsz, src) (Annex K, 구현 의존)
    • POSIX/BSD 계열: strlcpy(dst, src, dstsz) (비표준 확장)
    • 범용 대안: snprintf(dst, dstsz, "%s", src) 또는 memcpy + 길이 검사.
  • strcmp(a, b): NULL 종료 문자열 비교(크기 인자 없음).

  • strstr(haystack, needle): 부분 문자열 검색. 찾으면 포인터 반환, 없으면 NULL.

    • 인덱스가 필요하면:

      char *p = strstr(buf, "am");
      if (p) {
          size_t idx = (size_t)(p - buf);
      }
      

11. realloc()#

  • 기존 블록 크기를 조정하여 새 포인터를 반환한다(이동·복사 발생 가능).

  • 실패 시 NULL을 반환하고 원래 블록은 그대로이므로, 안전 패턴:

    void *tmp = realloc(p, new_size);
    if (tmp) p = tmp; else { /* 실패 처리 */ }
    
  • 빈번한 크기 변경은 복사 비용으로 인해 느려질 수 있다. 여유를 둔 성장 전략(예: 1.5–2배)을 고려한다.


12. 정적 메모리와 저장 기간/링케이지#

  • 자동 변수(auto, 지역): 스택에 위치, 블록 종료 시 소멸.

  • 정적 저장 기간(static storage duration): 프로그램 시작~종료(전역/정적).

  • static 키워드

    • 블록 내부: 지역 변수를 정적 저장 기간으로 만든다(호출 간 값 유지).
    • 파일 스코프: 변수/함수의 내부 링케이지(모듈 내부 전용) 부여. “모듈 전역”처럼 동작.
  • 경고: 전역/정적 상태는 동시성 이슈와 테스트 어려움을 유발한다. 함수 내 static 사용 시 스레드 안전성을 사전에 설계해야 한다.


13. extern#

  • 다른 번역 단위(파일)에 정의가 존재함을 알리는 선언 지정자이다(외부 링케이지).

  • “external file의 줄임말”이 아니라 external linkage의 명시라는 점을 기억한다.

    // a.c
    int g_counter = 0;
    
    // b.c
    extern int g_counter;  // a.c의 정의를 사용
    

14. 문자열과 저장 영역 주의#

  • 문자열 리터럴은 보통 읽기 전용 영역에 놓인다. char *p = "abc"; *p = 'z';UB(정의되지 않은 동작) 가 될 수 있다. 수정이 필요하면 char p[] = "abc"; 처럼 배열로 복사하여 사용한다.

학습 포인트 정리#

  • 배열 ≠ 포인터. 배열 이름은 다수의 문맥에서 첫 원소 포인터로 decay 되지만 타입/크기/재대입 가능성에서 차이가 난다.
  • 동적 메모리는 할당→초기화→사용→해제 생명주기를 지킨다.
  • OS는 메모리를 페이지 단위로 관리하며, realloc은 이동·복사가 발생 가능하므로 안전 패턴을 사용한다.
  • 문자열 API는 버퍼 크기 검증이 핵심이다(strcpy 지양, 안전 대안 사용).
  • 전역/정적 상태는 편리하지만 동시성·테스트·설계 복잡도를 높인다. 최소화가 원칙이다.
  • extern은 외부 정의 연결, static은 저장 기간 또는 내부 링케이지를 의미한다.