HOWTO · Arduino
Arduino 함수에서 배열 반환
C++ 함수는 내장 배열을 값으로 반환할 수 없습니다. Arduino에서 배열을 함수에 전달해 수정하거나, 유효 기간이 충분한 저장 공간을 포인터로 가리키는 방법을 알아봅니다.
이 페이지의 내용
C++ 함수는 내장 배열을 값으로 반환할 수 없습니다. Arduino 스케치에서는 기존 배열을 함수에 전달해 채우거나, 함수가 반환된 뒤에도 유지되는 저장 공간의 포인터를 반환하거나, 전역 배열 같은 공유 저장 공간을 사용할 수 있습니다. 적절한 방법은 저장 공간을 누가 소유하는지, 값이 얼마나 오래 유지되어야 하는지에 따라 달라집니다.
아래 예제는 데이터 흐름을 쉽게 살펴볼 수 있도록 작성한 완전한 표준 C++ 프로그램입니다. 스케치에서는 관련 작업을 setup() 또는 다른 함수에 넣고 std::cout 대신 Serial.print()를 사용하면 됩니다. 이 예제들은 실제 Arduino 보드에서 검증되지 않았습니다.
함수에서 기존 배열 수정하기
크기가 고정된 결과를 다루는 가장 간단한 방법은 호출자가 배열을 만들고 함수에 전달하는 것입니다. 함수 매개변수에서 배열은 포인터로 조정되므로 함수가 호출자의 원래 요소를 변경합니다. 요소 개수도 함께 전달하세요. 함수는 포인터만으로 배열의 용량을 알아낼 수 없습니다.
다음 예제를 실행해 보세요.
이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.
#include <iostream>
void fillValues(int values[], int count) {
for (int i = 0; i < count; ++i) {
values[i] = i + 1;
}
}
int main() {
int readings[5];
fillValues(readings, 5);
for (int value : readings) {
std::cout << value << '\n';
}
}
이 방법은 기존 저장 공간을 변경할 뿐 배열을 반환하지는 않습니다. 여기서 readings는 지역 자동 배열이며, fillValues가 값을 쓰기 전까지 요소에는 초기값이 없습니다. 반면 전역 배열을 포함해 정적 저장 기간을 갖는 배열은 프로그램이 시작될 때 0으로 초기화됩니다. 정수 0은 값이지 널 포인터가 아닙니다.
지역 배열을 선언할 때 int readings[5]와 같은 고정 경계는 Arduino C++ 도구 체인에서 폭넓게 지원됩니다. 일부 C++ 구현은 확장 기능으로 가변 길이 배열을 지원하지만, 이는 표준 C++가 아닙니다. 대상 도구 체인과 메모리 예산이 허용하는 경우 고정 경계나 동적 할당 저장 공간을 사용하세요.
정적 지역 배열의 포인터 반환하기
정적 지역 배열은 함수가 반환된 뒤에도 계속 존재하므로 함수에서 그 포인터를 반환할 수 있습니다. 배열 크기는 고정되어 있고, 호출할 때마다 같은 저장 공간을 가리키는 포인터가 반환됩니다. 나중에 함수를 다시 호출하면 이전 호출자가 계속 보관하려던 값이 바뀔 수 있습니다.
이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.
#include <iostream>
int* makeValues() {
static int values[3];
for (int i = 0; i < 3; ++i) {
values[i] = (i + 1) * 10;
}
return values;
}
int main() {
int* result = makeValues();
for (int i = 0; i < 3; ++i) {
std::cout << result[i] << '\n';
}
}
포인터는 프로그램이 실행되는 동안 유효하지만, 소유권을 넘기지는 않으며 호출할 때마다 새 배열을 제공하지도 않습니다. 호출자마다 독립된 결과가 필요하거나 이전 결과를 사용하기 전에 함수가 다시 호출될 수 있다면 이 방법을 피하세요.
동적으로 배열을 할당해 반환하기
결과 크기를 실행 중에 정해야 한다면 동적 할당이 유용할 수 있지만, 소유권과 메모리 관리에 신경 써야 합니다. 호출자는 포인터를 사용하기 전에 할당 성공 여부를 확인하고, 할당에 성공한 메모리는 free()로 해제해야 합니다. 메모리가 제한된 Arduino 보드에서는 할당과 해제를 반복하면 힙이 단편화될 수 있으므로 필요한 용량을 알고 있다면 고정 저장 공간을 우선 사용하세요.
이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.
#include <cstdlib>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <limits>
int* makeValues(int count) {
if (count <= 0) return nullptr;
const std::size_t maxCount = std::numeric_limits<std::size_t>::max() / sizeof(int);
if (static_cast<std::uintmax_t>(count) > static_cast<std::uintmax_t>(maxCount)) return nullptr;
const std::size_t bytes = static_cast<std::size_t>(count) * sizeof(int);
int* values = static_cast<int*>(std::malloc(bytes));
if (values == nullptr) return nullptr;
for (int i = 0; i < count; ++i) {
values[i] = i + 1;
}
return values;
}
int main() {
const int count = 3;
int* result = makeValues(count);
if (result == nullptr) {
std::cout << "allocation failed\n";
return 0;
}
for (int i = 0; i < count; ++i) {
std::cout << result[i] << '\n';
}
std::free(result);
}
반환된 메모리 블록은 호출자의 소유이며, 사용을 마친 뒤 free()를 정확히 한 번 호출해야 합니다. 해제한 뒤에는 포인터를 사용하지 말고, malloc()과 delete를 함께 쓰거나 new와 free()를 섞어 사용하지 마세요. 곱셈을 수행하기 전에 makeValues는 양수인 개수를 size_t에 들어갈 수 있는 int 요소의 최대 개수와 비교합니다. 바이트 수가 오버플로될 수 있으면 nullptr를 반환하며, 여기에는 size_t가 int보다 좁은 경우도 포함됩니다.
함수에서 전역 배열 수정하기
전역 배열은 스케치 전반의 함수에서 접근할 수 있는 고정 저장 공간을 제공합니다. 하나의 결과를 공유하는 작은 프로그램에서는 편리할 수 있지만, 함수가 전역 상태에 결합되어 독립적으로 사용하거나 반복 호출할 때 동작을 파악하기 어려워집니다. 전역 배열은 setup()이 실행되기 전에 0으로 초기화됩니다.
이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.
#include <iostream>
int readings[3];
void fillReadings() {
for (int i = 0; i < 3; ++i) {
readings[i] = 10 + i;
}
}
int main() {
fillReadings();
for (int value : readings) {
std::cout << value << '\n';
}
}
배열은 프로그램이 실행되는 동안 계속 사용할 수 있습니다. 용량을 명시하고, 배열에 값을 쓰는 모든 함수가 그 용량을 지키도록 하세요.
참조 매개변수로 배열 수정하기
C++ 참조 매개변수를 사용하면 매개변수 형식에 배열 크기를 유지할 수 있습니다. 따라서 필요한 용량이 함수 시그니처에 포함되고, 크기가 다른 배열을 전달하면 컴파일러가 호출을 거부할 수 있습니다. 템플릿을 사용하면 여러 고정 크기의 배열에 사용할 수 있는 도우미 함수를 만들 수 있습니다.
이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.
#include <cstddef>
#include <iostream>
template <std::size_t N>
void fillValues(int (&values)[N]) {
for (std::size_t i = 0; i < N; ++i) {
values[i] = static_cast<int>(i + 1);
}
}
int main() {
int readings[4];
fillValues(readings);
for (int value : readings) {
std::cout << value << '\n';
}
}
이 방법은 용량을 별도로 전달하지 않아도 호출자의 배열을 수정합니다. 도우미 함수는 컴파일 시 크기가 정해진 내장 배열을 받습니다. 포인터나 동적 할당 블록에 길이 정보가 들어 있는 것처럼 취급해 전달할 수는 없습니다.
Arduino 스케치에 적합한 방법 선택하기
일반적인 Arduino 스케치에서는 배열을 사용할 위치에 크기가 고정된 배열을 만들고, 길이와 함께 함수에 전달하세요. 도우미 함수가 특정 크기의 배열만 받게 하거나 템플릿이 배열 크기를 자동으로 유지하게 하려면 참조 매개변수가 유용합니다. 프로그램 전반에서 상태를 공유하는 것이 실제로 도움이 될 때는 전역 배열을 사용하세요.
정적 지역 배열의 포인터는 하나의 고정 크기 결과를 공유해도 되고 이후 호출이 그 값을 덮어써도 괜찮을 때만 적합합니다. 동적 할당은 실행 중 크기를 정해야 하고 스케치에서 할당 실패, 소유권, 해제, 힙의 제약을 관리할 수 있을 때만 사용하세요. 어떤 방법을 사용하든 함수는 포인터를 반환하거나 호출자가 볼 수 있는 저장 공간을 수정할 수 있지만, 내장 배열을 값으로 반환할 수는 없습니다.