Devin.KR

구조체·배열·출력 포인터

180분 안팎

학습 목표

고정 크기 샘플 배열과 상태 코드를 갖는 HAL 인터페이스를 구현합니다.

개념

읽기 결과와 성공 여부를 분리합니다

거리 함수가 실패할 때 0을 반환하도록 만들면 진짜 숫자와 실패 신호가 섞입니다. 앞 모듈에서 거리 범위를 검사했듯 C HAL도 성공 여부를 별도로 전달해야 합니다. read_range는 HalStatus를 반환하고 성공한 측정값은 double 출력 포인터로 씁니다. 이 레슨에서는 배열을 소유하는 장치 구조체를 만들고, 검사 실패 시 호출자가 제공한 출력 칸을 바꾸지 않는 규칙을 확인합니다.

구조체는 한 장치의 상태를 묶습니다

Hal에는 samples, count, index, stamp_s, left_pwm, right_pwm, injected, status가 있습니다. samples는 고정 크기 거리 배열이며 count는 현재 유효한 원소 수입니다. index는 다음 호출이 읽을 현재 위치, stamp_s는 논리 시각입니다. 모터와 결함 필드는 이후 레슨에서 사용합니다. 관련 숫자를 구조체 하나로 묶으면 서로 다른 장치 인스턴스의 상태가 섞이는 일을 줄일 수 있습니다.

Hal h를 선언하는 것만으로 내부 값이 정해지지는 않습니다. hal_init은 먼저 전체를 0으로 초기화하고 입력 포인터와 길이를 검사한 뒤 유효 샘플을 복사합니다. 빈 배열과 용량 4를 넘는 배열은 HAL_INVALID입니다. 초기화 실패로 사용할 수 없는 장치도 두 출력은 0으로 남습니다. 이전 실험의 메모리 내용에 기대지 않아야 같은 입력으로 재실행한 결과를 비교할 수 있습니다.

배열 길이는 저장 공간과 별개입니다

samples의 용량은 HAL_CAPACITY인 4개지만 count가 2이면 유효 인덱스는 0과 1입니다. 인덱스 2가 실제 저장 공간 안에 있어도 이번 입력의 측정값은 아닙니다. 따라서 읽기 전에 index가 count보다 작은지 확인합니다. 길이가 4인 배열의 인덱스 4는 한 칸 뒤를 뜻하며 읽지 않습니다. 경계 검사를 값 접근보다 뒤에 두면 상태 코드를 반환하기 전에 잘못된 메모리를 읽을 수 있습니다.

함수로 전달된 const double *samples만 보고 원래 배열 원소 수를 알 수는 없습니다. sizeof 포인터를 원소 크기로 나누는 방식은 포인터 자체의 크기를 계산하므로 잘못된 길이가 됩니다. 호출자가 count를 같이 넘기고, 실제로 그 개수만큼 읽을 수 있는 저장 공간을 보장합니다. HAL은 count가 용량 이내인지 검사하지만 외부 포인터가 가리킨 공간의 실제 크기를 자동으로 알아내지는 못합니다.

입력 배열의 수명을 분리합니다

hal_init은 전달받은 배열의 주소를 장치 안에 보관하지 않고 원소를 samples에 복사합니다. 호출자가 초기화 후 원본 배열을 수정해도 HAL의 측정 기록은 유지됩니다. 테스트는 원본 첫 값을 0.05에서 7로 바꾸고 장치가 여전히 0.05를 읽는지 확인합니다. 이는 값뿐 아니라 데이터 소유권을 시험하는 사례입니다. 동적 할당 없이 작은 fixture를 다루는 목적에 맞는 선택입니다.

const double *는 이 함수가 그 포인터를 통해 입력 배열을 수정하지 않겠다는 의미입니다. 같은 배열을 가리키는 다른 코드의 수정까지 막는 동기화 장치는 아닙니다. 이 실습은 단일 실행 흐름에서 초기화할 때 복사하므로 입력과 내부 상태를 명확히 분리합니다. 장치 구조체를 직접 수정하는 일은 테스트의 경계 강제 주입에만 사용하고 일반 호출 경로에서는 공개 API로 접근합니다.

주소를 넘겨 결과를 씁니다

double range=99.0;으로 출력 칸을 준비하고 read_range(&h,&range)를 호출합니다. &range는 range의 주소이며 함수의 out_m에는 그 주소 값이 복사됩니다. 함수에서 *out_m=value를 실행하면 호출자의 range가 바뀝니다. out_m=value처럼 작성하면 주소 변수에 거리 숫자를 대입하려는 형식 오류가 생깁니다. 값 전달과 그 값이 가리키는 저장 공간을 구분합니다.

h.status는 구조체 변수의 멤버를 읽는 표기이고 hal->status는 구조체 포인터가 가리키는 멤버를 읽는 표기입니다. 두 표현은 접근 출발점이 다를 뿐 같은 종류의 필드를 다룹니다. NULL인 hal에서 화살표로 접근하면 유효한 장치가 없으므로 먼저 검사해야 합니다. NULL 검사는 초기화하지 않은 임의 주소나 수명이 끝난 지역 변수 주소까지 유효하게 만들어 주지 않습니다.

실패에서는 출력 칸을 보존합니다

정상 상태에서 out_m가 NULL이면 HAL_INVALID를 반환하고 장치를 정지 오류 상태로 둡니다. 출력 주소가 없으니 값을 쓰지 않습니다. 거리 범위와 배열 경계를 확인한 뒤 성공 경로의 마지막에서만 *out_m에 씁니다. 읽기 실패 뒤 출력 칸에 남은 숫자는 새 측정값이 아닙니다. 호출자는 상태가 HAL_OK일 때만 측정값을 다음 계산으로 넘깁니다.

상태 코드는 0이 HAL_OK, 1이 HAL_INVALID, 2가 HAL_SENSOR, 3이 HAL_END입니다. 이미 오류나 종료 상태라면 read_range는 그 상태를 우선 반환합니다. 그래서 결함 상태에서 NULL 출력 포인터를 추가로 전달해도 기존 이유가 보존됩니다. 처음 원인이 무엇이었는지 추적하기 위한 우선순위입니다. 함수 호출이 성공했는지 판단할 때 숫자 암기보다 열거형 이름을 코드에 사용합니다.

읽기와 시간 진행을 나눕니다

read_range는 현재 샘플을 반환하지만 index나 stamp_s를 진행하지 않습니다. 반복해서 읽으면 같은 시각의 같은 값을 얻습니다. step은 유한하고 0보다 크며 1 이하인 dt_s를 받아 다음 샘플로 이동합니다. 다음 원소가 없으면 HAL_END로 정지하고 마지막 인덱스와 시각을 유지합니다. 읽은 횟수와 시간 흐름을 분리해야 테스트 실행 순서에 따라 기록이 달라지는 일을 피할 수 있습니다.

샘플이 두 개라면 초기 위치 0에서 첫 읽기, step(0.1), 위치 1에서 마지막 읽기까지 성공합니다. 그 다음 step은 END이며 마지막 값을 새 시각의 측정으로 반복 생성하지 않습니다. END는 고장 센서와 다른 상태지만 이번 실험에서는 더 이상 운전하지 않는다는 출력 정책을 공유합니다. 고정 fixture의 끝을 실제 장치의 데이터 대기와 혼동하지 않습니다.

테스트 메시지로 수정 위치를 좁힙니다

starter는 성공 상태를 반환하지만 출력 포인터 쓰기가 빠져 있습니다. owned copy 검사에서 값이 99로 남는다면 배열 복사만 확인하지 말고 *out_m 대입 경로를 봅니다. last sample과 latched end도 연달아 실패할 수 있습니다. 첫 실패가 뒤의 기대값에 영향을 주므로 실패 개수만 보고 여러 독립 결함으로 판단하지 않습니다. 입력 상태와 출력 칸을 함께 확인합니다.

실습 완료는 PASS struct checks=14로 확인합니다. 정상 두 샘플, 최대 용량, 빈 입력, 용량 초과, NULL 인자, 경계 강제 주입을 포함합니다. demo는 현재 인덱스와 거리, NULL 요청 후 보존된 출력을 보여 줍니다. 이 검사는 PC 모델의 메모리 접근 계약을 다룹니다. 포인터 연산의 다른 형태와 주소 그림은 더 읽기로 확장하고 다음 레슨에서는 같은 구조체에 모터 요청을 저장합니다.

호출 경계를 코드로 확인합니다

다음 호출 조각은 초기화 성공과 읽기 성공을 각각 확인합니다. 함수가 반환한 상태를 검사한 뒤에만 출력 칸의 값을 후속 계산으로 보냅니다.

double samples[] = {0.05, 8.0};
Hal h;
double range_m = 99.0;
if (hal_init(&h, samples, 2) == HAL_OK) {
    HalStatus status = read_range(&h, &range_m);
    if (status == HAL_OK) {
        /* range_m만 다음 계산에 전달합니다. */
    }
}

따라하기

시작 코드의 실패를 확인합니다

starter ZIP을 푼 폴더에서 실행합니다. stdout 요약과 별도로 stderr의 FAIL 항목이 어느 계약을 가리키는지 읽습니다. 종료 상태 1은 이번 TODO에서 기대한 실패입니다.

bash check.sh

실행 결과

FAIL struct checks=14 failures=5

계약에 맞는 대입을 작성합니다

hal_sim.c의 해당 TODO를 다음 코드로 고칩니다. 아래는 편집할 C 조각이며 셸에서 직접 실행하는 명령이 아닙니다. 헤더와 테스트는 유지합니다.

/* read_range의 모든 검사 뒤 성공 경로에 추가합니다. */
*out_m = value;
return HAL_OK;

같은 검사를 다시 실행합니다

수정한 starter 폴더에서 다시 검사합니다. 성공 요약과 종료 상태 0을 확인합니다. 이 출력은 solution 구현에서 실행해 확인한 값입니다.

bash check.sh

실행 결과

PASS struct checks=14

저장된 결과를 관찰합니다

수정 후 관찰 실행 파일로 값과 상태를 확인합니다. 아래 명령은 임시 디렉터리에서 빌드하고 정리합니다.

bash demo.sh

실행 결과

status=0 index=0 range=1.000
status=0 index=1 range=0.050
null status=1 range=0.050

확인 문제

실습

read_range의 성공 경로에서 출력 포인터에 측정값을 씁니다. 배열 소유 복사, 길이 0·최대·초과, NULL 포인터, 마지막 샘플 및 경계 검사 14개를 통과합니다. 실패 시 출력 칸을 변경하지 않습니다.

시작 코드·테스트 내려받기

실행 명령

bash check.sh

기대 결과

PASS struct checks=14

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • HAL의 출력 포인터에서 성공 코드와 측정값을 분리하는 이유는 무엇입니까?