구조체와 객체의 수명
110분 안팎
학습 목표
샘플 구조체의 스택·정적·힙 수명을 비교하고 지역 주소 반환 오류를 수정합니다.
개념
같은 값과 같은 객체를 구분합니다
기록 한 건에 timestamp_ms와 raw를 묶으면 어떤 값이 어느 시각의 값인지 잃지 않고 전달할 수 있습니다. Sample s = {100, 12};는 한 객체 안의 두 필드를 초기화합니다. s.raw로 필드를 읽고 Sample *p = &s;를 만들었다면 p->raw로 같은 객체를 읽습니다. 구조체 대입은 이 레슨의 두 정수 필드 값을 복사합니다. 같은 필드 값을 가진 복사본은 원본과 다른 객체일 수 있습니다.
포인터 변수에 들어 있는 숫자가 그럴듯해도 가리키는 객체가 아직 살아 있는지 확인해야 합니다. 수명은 그 객체를 합법적으로 사용할 수 있는 기간입니다. 자동 저장 기간의 지역 객체는 해당 블록을 벗어나면 수명이 끝납니다. 구현에서 이런 지역 객체를 보통 스택에 배치하지만 C의 수명 규칙은 주소가 실제 어디에 있었는지보다 중요합니다. 함수가 돌아온 뒤 같은 메모리 자리에 값이 남아 보이는 것은 사용 허가가 아닙니다.
bad_lifetime.c의 bad는 지역 Sample local을 만들고 &local을 반환합니다. 반환된 순간 호출자는 수명이 끝난 객체의 주소를 받습니다. 이 파일은 경고를 관찰하기 위한 파일이며 정상 테스트 실행 파일에는 연결하지 않습니다. 컴파일러가 address of stack memory 또는 address of local variable처럼 가리키면 반환문의 주소 연산과 지역 선언을 함께 읽습니다. 경고를 숨기기 위해 캐스팅하거나 static을 붙이기 전에 필요한 소유권을 먼저 정합니다.
호출자 소유 객체에 채우는 방법
sample_make(uint32_t time, int raw, Sample *out)는 호출자가 소유한 객체에 값을 채웁니다. 함수는 NULL과 raw 범위를 확인한 다음 *out = (Sample){time, (uint16_t)raw};를 대입합니다. 함수가 반환되어도 out이 가리키는 객체의 수명이 호출자 블록에서 계속되면 값을 쓸 수 있습니다. 함수 자신의 임시 지역 주소를 반환하지 않으므로 수명 연결이 명확합니다. 반환값 1이 성공이며 값 0과는 관계없습니다.
호출자는 Sample s를 선언하고 sample_make(100, 0, &s)를 호출합니다. 인자 &s는 주소이고 out 매개변수에는 주소의 복사본이 들어갑니다. out 변수 자체는 함수 반환 시 없어져도 s는 호출자의 객체입니다. 이 차이를 설명할 수 있어야 함수의 포인터 매개변수를 안전하게 사용할 수 있습니다. 포인터가 지역 매개변수라는 사실과 대상 객체가 지역이라는 사실은 별개의 질문입니다.
필드가 정수만 있는 Sample에서는 Sample copy = s;로 독립된 값을 보관합니다. copy.raw를 바꾸더라도 s.raw는 달라지지 않습니다. 뒤에 포인터 필드를 추가하면 대입은 주소도 복사하므로 대상 메모리는 공유될 수 있습니다. 여기의 값 복사 정책을 모든 구조체의 깊은 복사로 일반화하지 않습니다. 수명과 소유권 설명은 필드 구성의 변경과 함께 다시 검토합니다.
정적 저장은 오래 살지만 공유됩니다
sample_static은 함수 안의 static Sample stored 하나를 재사용하고 그 주소를 반환합니다. static 저장 기간의 객체는 프로그램 실행 동안 존재합니다. 그래서 지역 주소 반환과 같은 수명 문제는 피하지만, 호출할 때마다 같은 객체를 덮어씁니다. 첫 호출 주소 a와 두 번째 호출 주소 b가 같다면 a를 다시 읽을 때도 두 번째 raw가 보입니다. 호출별 기록 보관에는 주소를 모으는 대신 값을 배열에 복사해야 합니다.
demo는 첫 호출의 raw 12를 copy로 보관하고 두 번째 호출로 shared 객체에 24를 씁니다. 이후 a->raw는 24이고 copy.raw는 12입니다. 주소가 살아 있다는 것과 과거 값이 보존된다는 것은 다릅니다. 정적 객체는 반복 호출과 동시 호출에서 공유 상태가 될 수 있습니다. 현재 실습은 단일 스레드이고, 인터럽트나 태스크가 함께 쓰는 경우의 동기화는 뒤의 동시성 모듈에서 다룹니다.
const Sample *를 반환하면 호출자가 그 포인터 경로로 필드를 쓰는 일을 제한할 수 있습니다. 함수 내부의 다음 호출이 같은 객체를 갱신하는 일까지 막아 주지는 않습니다. 읽기 전용 주소라는 설명만으로 스냅샷이 보장된다고 말하지 않습니다. 검증은 주소 동일성과 값 변화, 미리 복사한 값의 유지 세 항목을 비교합니다. 서로 다른 시간에 읽은 동일한 주소를 독립 기록 두 건으로 계산하지 않습니다.
동적 저장은 해제 책임을 만듭니다
sample_new는 malloc(sizeof *p)로 Sample 공간을 확보한 뒤 초기화합니다. malloc은 실패할 수 있으므로 NULL인지 확인하고 성공한 객체만 사용합니다. 포인터 p가 함수 안의 지역 변수라도 동적으로 할당한 대상의 수명은 free까지 이어집니다. 함수가 반환하는 주소는 local의 주소와 다르게 사용할 수 있습니다. 이 실습에서는 소유권을 호출자에게 넘기며 호출자가 마지막 사용 뒤 한 번 free합니다.
free(p)는 동적 객체의 수명을 끝냅니다. 이후 p->raw를 읽거나 같은 주소를 다시 free하는 동작은 안전하지 않습니다. 소유 포인터를 NULL로 바꾸면 그 변수의 후속 사용을 점검하기 쉽지만 다른 곳에 복사한 별칭까지 지워지지는 않습니다. 해제 책임자가 누구인지, 다른 코드가 주소를 언제까지 빌릴 수 있는지를 문서로 적는 것이 필요합니다. 메모리를 돌려주지 않는 누수와 너무 일찍 돌려주는 오류를 따로 봅니다.
미션 기록기는 최대 16건으로 제한되어 있으므로 매 샘플마다 malloc을 사용하지 않습니다. 고정 배열을 소유하는 Buffer를 쓰면 용량과 할당 실패 처리 부담이 명확해집니다. 동적 할당을 배웠다고 항상 적용할 필요는 없습니다. 반대로 고정 배열이라고 범위와 수명이 자동으로 안전해지는 것도 아닙니다. Buffer가 살아 있을 때만 내부 배열이 유효하고, 읽을 수 있는 칸은 length가 결정합니다.
수명 문제를 실행과 경고로 나눠 확인합니다
실습은 먼저 sample_make의 실패 시 원본 유지와 정상 필드 초기화를 확인합니다. sample_new의 정상 할당과 잘못된 raw 거부를 검사하고 할당된 객체는 테스트 안에서도 해제합니다. static 경로는 동일한 주소를 반환하면서 값이 갱신되는지 확인합니다. 할당 실패는 NULL 계약으로 처리하지만 이 테스트가 실제 시스템 메모리 고갈을 재현했다고 주장하지 않습니다. 테스트가 검증한 경로와 처리하도록 작성한 경로를 구분합니다.
bad_lifetime.c를 -Werror로 객체 컴파일하면 실패해야 할 수명 경고를 읽을 수 있습니다. 이때 링크까지 진행하지 않으므로 main 정의 누락은 관찰 대상이 아닙니다. 정상 구현에서는 지역 주소를 반환하는 함수 자체를 sample_make로 대체합니다. 컴파일러가 한 수명 오류를 잡아 주었다고 모든 댕글링 주소를 찾아냈다고 말하지 않습니다. 실행 경로의 메모리 접근은 다음 레슨에서 추가 검사합니다.
신입 코드 리뷰에서 Sample이 어디에 저장되는지, 누가 수정하는지, 언제 사라지는지 세 문장을 먼저 적게 합니다. 스택·정적·힙은 외울 표가 아니라 호출 뒤 주소 사용과 기록 보존 정책을 설명하는 도구입니다. 더 읽기의 구조체 배치와 복사 예제를 참고해 필드가 추가될 때의 영향을 보충합니다. 다음 미션에서는 내부 주소를 노출하지 않고 timestamp_ms와 raw를 값으로 복사해 누적합니다.
따라하기
검증 실패에서 구현 시작
starter 폴더에서 실행합니다. 실패 개수는 수정 정도에 따라 달라지므로 자신의 결과를 확인합니다.
make test관찰 실행 파일 만들기
구현을 수정한 뒤 실행합니다. 컴파일 성공은 출력 없이 끝납니다.
make -s demo잘못된 지역 주소 반환 경고 읽기
bad_lifetime.c는 정상 빌드에 포함하지 않습니다. 아래 명령은 객체 컴파일 단계에서 실패하는 경고 예제입니다. 실제 진단에서 local과 반환 위치를 찾습니다.
gcc -std=c11 -Wall -Wextra -Werror -c bad_lifetime.c -o bad.o실행 결과
bad_lifetime.c:2:52: error: address of stack memory associated with local variable 'local' returned [-Werror,-Wreturn-stack-address]
2 | Sample *bad(void) { Sample local={100,12}; return &local; }
| ^~~~~
1 error generated.
값과 계약 관찰
solution과 같은 구현에서 실행한 결과입니다. 숫자 크기는 이 맥의 측정값입니다.
./demo실행 결과
shared=1 current=24 copy=12 owned=4095 released=1
같은 테스트 재실행
수정 후 기대 반환값과 상태 유지가 함께 확인됩니다.
make -s test실행 결과
lifetime: 7 cases, 0 failures
확인 문제
실습
lab.c의 sample_make와 sample_new를 구현합니다. 호출자 소유 객체의 실패 시 유지, 할당 실패 계약, 호출자 해제 책임을 설명합니다. sample_static의 주소 재사용을 값 복사와 비교하고 make test의 7개 검사를 통과시킵니다.
실행 명령
make test
기대 결과
lifetime: 7 cases, 0 failures
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 배열과 포인터를 사용할 때 메모리 오류를 확인하는 방법을 설명해 주시면 됩니다.