cpp 언어로 메모리 직접 관리하기
임베디드 특정 칩에서 한정된 메모리로 관리해야하는 상황이 생길 수 있다.
stdlib.h 의 malloc, memcpy 사용하지 않는 상황에서 배열 관련으로 코드 가독성도 높이고 메모리 관리가 필요하여 별도의 라이브러리를 구축하게 되었다.
동작 코드는 사실상 c 언어인데, IDE에서 불필요한 함수와 변수 호출을 최소화하려고 cpp 클래스로 캡슐화하였다.
ArrayMap.h
#ifndef ARRAY_MAP_H
#define ARRAY_MAP_H
class ArrayMap {
private:
struct Row {
int* data;
int length;
};
// maximum space
// POOL_SIZE / Row =
// 1024 / 4 =
// 256 =
// full capacity
static constexpr int MAX_POOL_SIZE = 256 * 4;
static constexpr int MAX_DEPTH_SIZE = 64;
unsigned char mem[MAX_POOL_SIZE];
int* di[MAX_DEPTH_SIZE];
Row rows[MAX_DEPTH_SIZE];
int offset;
int capacity;
int size;
void reset();
public:
int alloc(int size);
int* getArray(int rowAddr);
int getArrayLength(int rowAddr);
int* setReverse(int rowAddr);
int** getMap();
int length();
};
#endif
구형 호환성으로 #progma once 대신 ifndef, define 매크로로 중복으로 호출하는 것을 방지하였다.
호출자에게 불필요한 변수 변경 방지 및 내부용 구조체(struct Row{}) 호출되지 않도록 private 으로 선언한다.
메모리 크기는 mem으로 선언한 것으로 볼 수 있다. 힙 공간의 1024byte 사용한다.
마찬가지로 rows 변수로 힙 메모리 공간을 64byte * Row(12~16byte) 768~1024byte 사용한다.
MAX_POOL_SIZE, MAX_DEPTH_SIZE 상수 값 수정으로 크기를 늘릴 수 있다.
Row 구조체의 메모리 공간은 다음과 같다.
struct Row {
int* data; // 포인터 주소 4byte. 64bit 칩셋이면 8byte
int length; // 4byte
// 구조체 padding 4byte
}
그리고 사용하는 멤버함수 기능은 심플하다.
public:
int alloc(int size)
: 힙메모리인 mem[MAX_POOL_SIZE] 매개변수 공간만큼 Row 구조체 data 배열과 length 값을 할당한다.int* getArray(int rowAddr)
: 매개변수로 인덱스를 전달되면 자신의 Row 구조체 data 배열을 반환한다.int* setReverse(int rowAddr)
: 매개변수로 인덱스를 전달되면 자신의 Row 구조체 data 배열을 역순으로 변경하여 반환한다.int getArrayLength(int rowAddr)
: 매개변수로 인덱스를 전달되면 자신의 Row 구조체의 length 값을 반환한다.int** getMap()
: 자신의 Row 구조체 data 전체를 반환한다.int getMapLength()
: Row 구조체 크기를 반환한다.
코드를 살펴보면 알겠지만, 메모리 직접 관리로 아슬아슬한 줄타기를 타고 있다. 이러한 라이브러리를 사용하면 all, any, fill 과 같은 부가기능 멤버함수를 추가하기도 수월하고 유지보수도 쉽다.
ArrayMap.cpp
#include "ArrayMap.h"
#include <stdio.h>
void ArrayMap::reset() {
capacity = 0;
offset = 0;
size = 0;
}
int ArrayMap::alloc(int currentSize) {
if (offset < 0) reset();
if (offset + currentSize > (MAX_POOL_SIZE + sizeof(int))) return -1;
void* ptr = mem + offset;
offset = offset + (currentSize * sizeof(int));
di[size] = (int*)ptr;
rows[size].data = (int*)ptr;
rows[size].length = currentSize;
return size++;
}
int* ArrayMap::getArray(int rowAddr) {
if (rows[rowAddr].length < 0) return nullptr;
return (int*)rows[rowAddr].data;
}
int ArrayMap::getArrayLength(int rowAddr) {
if (rows[rowAddr].length < 0) return -1;
return rows[rowAddr].length;
}
int* ArrayMap::setReverse(int rowAddr) {
// TODO: 인메모리 변경으로 멀티스레드 환경에서 보장 불가능. 뮤텍스 락 필요 및 싱글스레드로만 할당
if (rows[rowAddr].length < 0) return nullptr;
int len = rows[rowAddr].length;
int* arr = rows[rowAddr].data;
for (int i = 0; i < len / 2; i++) {
int tmp = arr[i];
arr[i] = arr[len - 1 -i];
arr[len - 1 - i] = tmp;
}
return arr;
}
int** ArrayMap::getMap() {
if (di == nullptr) return nullptr;
return (int**)di;
}
int ArrayMap::length() {
return size;
}
장점과 단점을 살펴보자
장점
- 클래스 접근제어자 사용으로 불필요한 변수, 함수를 호출하지 않는다.
- 유지보수성이 올라간다.
- 재사용이 높다.
단점
- 호출부에서 배열의 크기 이상으로 할당할 수 있어서 배열 침범이 일어난다. (데이터유실 발생, 크래쉬 발생 가능. Todo: 스택 멤버 함수 추가해서 개선하기)
- 힙공간을 전역변수로 사용하고 있다. (반환이 없다. stdlib 라이브러리를 불려와 malloc 인스턴스로 사용하는 것도 고려해볼만 하다.)
- 힙공간을 임의의 크기로 다시 재정의할 수 없다. (Todo: realloc 멤버 함수 추가하도록 하자.)
- 배열이 초기화되지 않으면 쓰레기값 출력함. (지역 변수에서 조심 해야한다.)
단점이 매우 많은데, 숨겨진 위험 요소와 정의되지 않은 동작(undefined behavior)이 많으므로 오동작/크래시/데이터 손상이 발생될 수 있다는 것이 문제다. 앞으로의 c / cpp 메모리 안전성이 떨어진다는 것을 다시 한번 바라볼 계기가 되었다.
가능하다면 std의 vector 라이브러리를 사용하자.
테스트 케이스
사용한 라이브러리는 테스트케이스를 만들어 기능 추가나 코드 변경한 경우 영향이 없는지 검증이 늘 필요하다.
assert 라이브러리 기능을 사용하면 컴파일 단계에서만 검증하고 런타임에는 동작하지 않으므로 유용하다.
ArrayMap map;
// 조건
int arrTen = map.alloc(10); // 첫번 째 배열 10개 메모리 할당
int arrFive = map.alloc(5); // 두번 째 배열 5개 메모리 할당
int arrReverse = map.alloc(4); // 세번 째 배열 4개 메모리 할당
int* arr = map.getArray(arrTen); // 첫번 째 인덱스 배열 불려오기
arr[0] = 500; // 첫번 째 배열 메모리에 500 대입
arr[9] = 60000; // 첫번 째 배열 메모리에 60000 대입
arr = map.getArray(arrFive); // 두번 째 인덱스 배열 불려오기
arr[0] = 10000; // 두번 째 배열 메모리에 10000 대입
arr[4] = 50; // 두번 째 배열 메모리에 50 대입
arr = map.getArray(arrReverse); // 세번 째 인덱스 배열 불려오기
arr[0] = 100; // 세번 째 배열 메모리에 100 대입
arr[3] = 900; // 세번 째 배열 메모리에 900 대입
// 세번 째 배열 모든 공간을 역순으로 치환함
assert(map.setReverse(arrReverse) != nullptr);
assert(map.setReverse(-1) == nullptr); // 비정상 케이스 확인
// 첫번째 getLength 결과
assert(map.getArrayLength(arrTen) == 10);
// 두번째 getLength 결과
assert(map.getArrayLength(arrFive) == 5);
// 첫번째 배열 대입 확인
assert(map.getArray(arrTen)[0] == 500);
assert(map.getArray(arrTen)[9] == 60000);
// 두번째 배열 대입 확인
assert(map.getArray(arrFive)[0] == 10000);
assert(map.getArray(arrFive)[4] == 50);
// 세번째 배열 역순 치환 확인
assert(map.getArray(arrReverse)[0] == 900);
assert(map.getArray(arrReverse)[3] == 100);
// 전체 ArrayMap 2차원으로 펼쳐서 불려오기
assert(map.getMap()[0][0] == 500);
assert(map.getMap()[1][0] == 10000);
assert(map.getMap()[2][0] == 900);
// length 추가한 배열의 Dimention 전체 크기 확인하기
assert(map.length() == 3);