임베디드 스터디 - 포인터 및 어레이 연산
임베디드 스터디 - 포인터 및 어레이 연산
이번 글 참고자료:
EmbeddedInterviewlab
포인터
&: 특정 변수의 데이터를 저장하는 주소*: 특정 변수의 주소에 해당하는 데이터*(&x) = x
1
2
3
4
/* volatile tells the compiler: never optimise away reads/writes */
volatile uint32_t *const GPIOA_ODR = (volatile uint32_t *)0x40020014;
*GPIOA_ODR |= (1U << 5); /* set bit 5 — turn on an LED */
포인터 연산
- 포인터의 연산은 포인터의 원본 변수 데이터 타입을 따른다.
1
2
3
4
5
6
7
uint8_t *bp = (uint8_t *)0x2000; /* byte pointer */
uint16_t *hp = (uint16_t *)0x2000; /* halfword pointer */
uint32_t *wp = (uint32_t *)0x2000; /* word pointer */
bp++; /* 0x2001 — advances 1 byte */
hp++; /* 0x2002 — advances 2 bytes */
wp++; /* 0x2004 — advances 4 bytes */
- 포인터 연산과 어레이 기반 데이터 선언은 다음과 같이 동질성을 띈다.
1
2
3
4
5
6
7
int32_t samples[4] = {10, 20, 30, 40};
int32_t *p = samples; /* points to samples[0] */
/* All three lines read samples[2] == 30 */
int32_t a = samples[2];
int32_t b = *(p + 2); /* pointer + offset */
int32_t c = p[2]; /* equivalent syntax */
- 포인터 연산의 임베디드 SW 개발에서 다음 상황에 유심히 고려해서 사용해야한다.
- 버퍼 조작(Buffer traversal) : UART 수신 버퍼를 바이트 단위로 조작하는 상황
- 레지스터 뱅크(Register Banks) : 인접한 레지스터 사이에 일정한 오프셋이 있는데, 포인터가 빈 오프셋 구간을 접근하는 상황
- 데이터 캐스팅(Casting between width) :
uint32_t레지스터를 4개의uint8_t데이터로 읽기 위해서 Byte 포인터로 변환하여 읽어야한다. 그렇지 않으면 포인터 연산 과정에서 3byte가 스킵된다.
어레이 vs 포인터
| 비고 | Array (int arr[8]) | Pointer (int *p) |
|---|---|---|
| sizeof | 전체 크기 (32) | 포인터 크기 (4 or 8) |
| Assignment | 할당 불가 : arr = other; 잘못된 문법 | 할당 가능 p = other; |
| Decay | 함수에 전달될 때 int* 형태로 전달됨 | 형태 변환 없음 |
| Storage | 각 인자를 메모리에 할당함 | 주소만 갖고 있음 (각 인자는 다른 메모리 주소에 저장) |
| &arr | int (*)[8] (pointer to array) | int ** (pointer to pointer) |
| Initialization | 데이터가 메모리에 복사됨; "hello" 가 어레이에 저장됨 | string 데이터가 있는 주소로 포인터가 지정됨 (read-only) |
- 어레이는 자신의 전체 크기를 정보로 갖고 있지만, 포인터는 주소값만 갖고 있다.
1
2
3
4
5
6
7
8
9
10
11
void print_size(int data[]) {
/* data has decayed — sizeof gives pointer size, NOT array size */
printf("%zu\n", sizeof(data)); /* prints 4 or 8 */
}
int main(void) {
int buf[16];
printf("%zu\n", sizeof(buf)); /* prints 64 (16 * 4) */
print_size(buf); /* prints 4 or 8! */
return 0;
}
이중포인터 (Pointer-to-Pointer)
- 포인터 주소를 저장하는 포인터
- 임베디드에서 주로 2가지 용도로 사용된다.
- 호출자의 포인터를 이동시킬 때
- 포인터 어레이 (Arrays of Pointers)에서 각 요소의 포인터를 참조할 때
1
2
3
4
5
6
7
/* parse_field advances *cursor past the parsed bytes */
bool parse_field(const uint8_t **cursor, const uint8_t *end, uint16_t *out) {
if (*cursor + 2 > end) return false;
*out = (uint16_t)((*cursor)[0] << 8 | (*cursor)[1]);
*cursor += 2; /* caller's pointer moves forward */
return true;
}
Void 포인터
- 타입의 지정이 없는 포인터 변수
void*에 어떠한 포인터 변수도 할당할 수 있지만, 해당 주소에 있는 변수를 불러오기 위해 데이터 타입 캐스팅이 필수적이다.
- 임베디드 시스템에서 다음 용도로 주로 사용된다
- Callback context pointers : ISR 콜백이나 RTOS 태스크에 데이터를 전달하기 위해 사용
memcpy/memset: 메모리에 변수 할당이나 전달용으로 사용된다- Generic data structure : 링 버퍼(Ring Buffer) 라이브러리에 데이터를 스태킹하기 위해 사용한다.
void*변수는 포인터 연산이 금지되어있다. 포인터 연산을 위해, 우선 캐스팅을 해야한다.
1
2
3
4
5
6
7
8
9
10
/* Generic swap — works for any type by operating on raw bytes */
void swap(void *a, void *b, size_t size) {
uint8_t temp[size]; /* VLA — acceptable for small sizes */
memcpy(temp, a, size);
memcpy(a, b, size);
memcpy(b, temp, size);
}
int x = 1, y = 2;
swap(&x, &y, sizeof(int)); /* x == 2, y == 1 */
Null 포인터
NULL포인터 참조는 정의되지 않은 동작으로 정의한다.- Cortex-M은 Hard Fault 트리거로 사용되기도 한다.
- 프로세서에 따라
NULL포인터를 처리하는 방식이 다르기도 한다.
- 기능 동작의 신뢰성 유지를 위해 검증용으로 사용되기도 한다.
- MISRA C에서는 모든 포인터는 변수 참조 전에
NULL포인터 검사를 수행해야한다
또한,NULL포인터를 사용하는 함수는 무조건 설계 문서에 반영해야한다.
- MISRA C에서는 모든 포인터는 변수 참조 전에
1
2
3
4
5
6
7
/* Guard every pointer before use */
void uart_send(const uint8_t *buf, size_t len) {
if (buf == NULL || len == 0) return;
for (size_t i = 0; i < len; i++) {
UART_TX_REG = buf[i];
}
}
허상 포인터 (Dangling Pointer)
- 이미 메모리의 변수 할당이 해제된 주소를 포인팅하는 포인터
- 오류가 발생해도 디버깅하기 까다롭다.
- Dangling Pointer 방지법
- 지역 변수의 주소를 포인터로 return하지 않는다.
- 동적 할당 변수 해제(
free()) 후 해당 포인터는NULL로 처리한다. static이나 호출자 입력 변수를 사용한다.
함수 포인터
- 함수가 선언된 메모리 주소를 포인팅하는 포인터 변수.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* typedef for readability */
typedef void (*irq_handler_t)(void);
/* Dispatch table — one handler per IRQ line */
static irq_handler_t handlers[16] = {NULL};
void register_handler(uint8_t irq, irq_handler_t fn) {
if (irq < 16) handlers[irq] = fn;
}
void dispatch(uint8_t irq) {
if (irq < 16 && handlers[irq] != NULL) {
handlers[irq](); /* call through pointer */
}
}
Const 포인터
- const 포인터는 사용 방법에 따라 메모리 할당 방식이 다르다.
- const 위치에 따른 해석 방법은 다른 변수들과 마찬가지로 Right to left 방법을 따른다.
| 선언 | 해석 | 포인터 위치 | 데이터 위치 |
|---|---|---|---|
const int *p | Pointer to const int | RAM (스택/전역) | .rodata (플래시) |
int * const p | const pointer to int | .rodata (플래시) | RAM |
const int * const p | const pointer to const int | .rodata (플래시) | .rodata (플래시) |
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.