Как работают массивы?

Я уже несколько дней пытаюсь понять, как работают массивы в Arduino IDE. Для лучшего понимания я собрал некоторые фрагменты кода, которые я нашел тут и там, чтобы написать этот скетч:

#define arrayLength(x) (sizeof(x) / sizeof(int)) //размер массива

typedef struct {
  int note;
  int duration;
} BUZZER_NOTE ;

class Buzzer {

  public:

    BUZZER_NOTE notes[];  //-- Забавно, что то же самое объявление, опробованное ниже, РАБОТАЕТ, если оно находится внутри класса

};


void setup() {

  Serial.begin(9600);

// BUZZER_NOTE отмечает[]; //-- Ошибка компиляции:

  int *bob;               //-- Объявление таким образом не показывает ошибок компиляции, но выводит странные значения
// целое число[]; //-- Ошибка компиляции: "размер хранилища 'bob' неизвестен"

  int bob1[] = {10, 20};
  int bob2[5] = {10, 20, 30, 40, 50};
  int bob3[] = {60, 70, 80};

  bob[0] = 10;
  bob[1] = 20;
  bob[2] = 30;
  bob[3] = 40;
  bob[4] = 50;
  bob[5] = 60;
  bob[6] = 70;   //-- сколько бы я ни создал индексов, он всегда будет показывать длину 2

  test(1);       //-- вызовет первую функцию ниже, как и ожидалось
  test({1});     //-- Также вызывает первую функцию ниже, когда ожидается вторая (с параметром массива)

  test(bob);     //-- Длина: 1 (ожидается 5); Размер: 2 (?????) ; Первый элемент: 163 (ожидается 10);
  test(bob1);    //-- Длина: 1 (ожидается 2); Размер: 2 (?????) ; Первый элемент: 10 (хорошо);
  test(bob2);    //-- Длина: 1 (ожидается 5); Размер: 2 (?????) ; Первый элемент: 10 (хорошо);
  test(bob3);    //-- Длина: 1 (ожидается 3); Размер: 2 (?????) ; Первый элемент: 60 (хорошо);

}

void loop() {
}

void test(int a) {
  Serial.println("Called int function");
  Serial.println();
}

void test(int a[]) {
  Serial.println("Called array function");
  Serial.print("Length: ");
  Serial.println(arrayLength(a));
  Serial.print("Size: ");
  Serial.println(sizeof(a));
  Serial.print("First Element: ");
  Serial.println(a[0]);
  Serial.println();
}

Приведенный выше код выводит следующие строки:

Called int function    // как и ожидалось

Called int function    // ожидаемая функция массива

Called array function  // как и ожидалось
Length: 1              // ожидается 5
Size: 2                
First Element: 163     // ожидается 10

Called array function  // как и ожидалось
Length: 1              // ожидается 5
Size: 2
First Element: 10      // как и ожидалось

Called array function  // как и ожидалось
Length: 1              // ожидается 3
Size: 2
First Element: 10      // как и ожидалось

Called array function  // как и ожидалось
Length: 1              // ожидается 5
Size: 2
First Element: 60      // как и ожидалось

Моя цель на данный момент — добиться чего-то вроде этого:

int bob[]; // это дает ошибку компилятора: "размер хранилища 'bob' неизвестен"
bob = {10, 20, 30, 40};

test(bob);

void test(int arr[]) {
  for (int i = 0; i < FIND_ARR_LENGTH_SOMEHOW; i++) {
    Serial.print("Index i: ");
    Serial.println(arr[i]);
}

Вопрос: как я могу объявить массив без упоминания его размера, заполнить его нужным количеством индексов, а затем передать его функции, которая сможет обработать все содержимое?

Я знаю, что могу объявить массив и оставить его длину для передачи в качестве второго параметра, но должен быть способ получить длину массива.

Заранее спасибо.

, 👍1

Обсуждение

https://majenko.co.uk/blog/arrays-pointers-what-c, @Majenko

#define arrayLength(x) (sizeof(x) / sizeof(int)) //размер массива неверный! #define arrayLength(x) (sizeof(x) / sizeof(x[0])) может работать, если x является массивом, @DataFiddler

@Majenko Если бы я нашел такую статью раньше, это сэкономило бы мне несколько часов. Большое спасибо., @MrCabana


4 ответа


3
BUZZER_NOTE notes[];  //-- Забавно, как то же самое объявление, опробованное ниже, РАБОТАЕТ, если оно находится внутри класса

Это не совсем так. Язык C++ запрещает это. Это просто нестандартная особенность компилятора GCC, используемого Arduino IDE, который поддерживает стиль C элементы гибкого массива в коде C++.

// BUZZER_NOTE notes[]; //-- Ошибка компиляции:

Конечно. Вы не можете использовать неполные типы в определениях объектов. А массив неопределенного размера — это неполный тип. Широко используемым исключением из этого правила является массив [], объявленный с помощью инициализатора. В этом случае предполагается, что размер массива неявно указан инициализатором.

int *bob;               //-- Объявление таким образом не показывает ошибок компиляции, но выводит странные значения

Конечно. Это вообще не массив. Это указатель. И никуда не указывает. Это недействительно. Вам даже не разрешен доступ к такому указателю.

// int bob[]; //-- Ошибка компиляции: "размер хранилища 'bob' неизвестен"

См. выше.

 int bob1[] = {10, 20};
 int bob2[5] = {10, 20, 30, 40, 50};
 int bob3[] = {60, 70, 80};

Это правильно объявленные массивы. Как я сказал выше, объявление [] разрешено, поскольку вы объявляете свой массив с помощью инициализатора.

bob[6] = 70;   //-- сколько бы я ни создал индексов, он всегда будет показывать длину 2

В этом нет никакого смысла. Вы не можете «создавать индексы» в массиве. Размер массива фиксируется в момент объявления. Его нельзя уменьшить или расширить, нельзя «создать» или «удалить» индексы.

Но в этом примере bob даже не является массивом. Это указатель. А так как это мусорный указатель, который никуда не указывает, вам не разрешен доступ к нему с помощью каких-либо индексов.

После того как вы сделаете так, чтобы ваш bob указывал на какой-то допустимый блок памяти, вы сможете получить к нему доступ как bob[3] и т. д., если вы оставаться в пределах этого блока памяти.

test({1});     //-- Также вызывает первую функцию ниже, когда ожидается вторая (с параметром массива)

Здесь несколько ошибок.

Во-первых, у второй функции нет "параметра массива". В объявлениях параметров int a[] эквивалентен int *a. Это параметр указателя, а не параметр массива.

Во-вторых, с незапамятных времен в C и C++ можно было использовать скалярное значение в {} для инициализации скалярного объекта

int a = { 42 }; /* <- this has always been legal in C and C++ */

Итак, первая функция, очевидно, является кандидатом в данном случае. Правила языка C++ предписывают, что инициализатор {1} может использоваться в качестве инициализатора для значения int или std::initializer_list<int>. объект, но для указателя. Вот почему вызывается первая функция.

Вы можете заставить его создать временный массив с помощью псевдонима типа массива и синтаксиса функционального приведения

using ARR = int[];
test(ARR{1});

но в GCC вы столкнетесь с ошибкой GCC, не позволяющей вам формировать указатели и ссылки на временные массивы (Clang это примет).

void test(int a[]) {
  ...
  Serial.print("Length: ");
  Serial.println(arrayLength(a));

Это не имеет смысла. Этот трюк с "длиной массива"

#define arrayLength(x) (sizeof(x) / sizeof(int))

работает только с массивами. Ваш a внутри test не является массивом. Это указатель. Совершенно бессмысленно применять этот прием sizeof к указателю.

Моя цель на данный момент — добиться чего-то вроде этого:

int bob[]; // это дает ошибку компилятора: "размер хранилища 'bob' неизвестен"
bob = {10, 20, 30, 40};

Невозможно. При определении массива всегда нужно указывать фиксированный размер времени компиляции. А массивы в C и C++ нельзя копировать, нельзя присваивать. Если вы хотите копировать данные между массивами, вы должны либо делать это вручную, элемент за элементом, либо использовать процедуру из библиотеки, которая делает то же самое.

{}– инициализаторы можно использовать с массивами, но только в момент объявления. Не позже. Вы можете сделать что-то вроде

using A = int[5];
A a;
std::memcpy(a, A{1, 2, 3, 4, 5}, sizeof(A));

но это приведет к той же ошибке GCC, о которой я упоминал выше.

Кстати, стандартная библиотека предоставляет шаблон std::array — тонкую оболочку для необработанных массивов. Объекты std::array можно копировать и назначать с помощью почти такого же синтаксиса, который вы использовали

std::array<int, 5> a;
a = { 1, 2, 3, 4, 5 };

Но они по-прежнему являются массивами фиксированного размера во время компиляции.

void test(int arr[]) {
  for (int i = 0; i < FIND_ARR_LENGTH_SOMEHOW; i++) { 

Невозможно. Ваш arr — это указатель. Невозможно извлечь информацию о «размере массива» для указателя, так как эта информация просто не существует. Если вы хотите «FIND_ARR_LENGTH_SOMEHOW», вам нужно либо разработать собственное соглашение для нахождения этой длины, либо просто передать эту длину функции через отдельный параметр.

,

Прошу прощения за глупый вопрос. Я не привык к программированию на c/c++ и просто пытался написать библиотеку, пока ничего не узнаю. Никогда не приходилось иметь дело с указателями на языках, к которым я привык. В любом случае, ваши объяснения довольно поучительны. Мне придется еще немного покопаться в указателях и памяти. Спасибо., @MrCabana


1

Это скорее общий вопрос по программированию, но, поскольку для микроконтроллеров/Arduino существуют некоторые подводные камни, я отвечу на него.

Как я могу объявить массив без упоминания его размера, заполнить его нужным количеством индексов, а затем передать его функции, которая сможет обработать все содержимое?

Короткий ответ: вы не можете. Массив всегда имеет фиксированный размер, который нельзя изменить в течение всего времени существования массива.

Подробный ответ: хотя каждый массив имеет фиксированный размер, вы можете создавать новые массивы с размером, неизвестным во время компиляции. Для этого вам потребуется динамическое выделение памяти, как это делается с ключевым словом new:

BUZZER_NOTE *notes = new BUZZER_NOTE[x];

с размером нового массива x. notes здесь является указателем на массив (на самом деле это указатель на первый элемент массива). Если вы не знаете, как правильно обращаться с указателями, вам следует поискать в Google руководство по этому поводу, их много в Интернете. Итак, если ваш определенный массив заполнен и вы хотите добавить больше элементов, вам нужно определить новый массив большего размера, скопировать старое содержимое и удалить старое (на самом деле это то, что происходит внутри String класс).

Чтобы получить простой способ добавления дополнительных элементов без копирования всего содержимого, программист на C++ может использовать связанный список. Это определяет класс элемента, который содержит переменную для содержимого элемента и указатель на следующий элемент в списке. Таким образом, чтобы перебрать связанный список, вы должны начать с первого элемента и следовать указателям по списку до конца. (Опять же, вы можете больше узнать об этой концепции в Интернете)

НО: вы не должны использовать ничего из этого на Arduino, особенно на устройствах с очень ограниченным объемом оперативной памяти (таких как Uno, Nano, ...). Когда вы выполняете динамическое выделение памяти, происходит фрагментация памяти. Возьмем пример сверху. У вас есть динамически выделенный массив с вашими данными. Теперь вы хотите хранить больше данных. Вы определяете новый и больший массив. Для простоты объяснения он будет размещен сразу после первого массива в памяти. Вы копируете содержимое и удаляете первый массив, чтобы освободить память. Теперь в вашей памяти есть свободное место размером с первый массив. Теперь вы хотите снова добавить больше данных, поэтому вы определяете новый и больший массив. Этот массив не может поместиться в свободное пространство в начале, так как места недостаточно. Это пространство памяти теперь мертво, потому что его нельзя использовать для ваших данных.

В реальных случаях вы бы выделяли и удаляли переменные разного размера в своем скетче. Это приводит к тому, что вы получите много дыр в памяти, которые слишком малы, чтобы вместить ваши данные. На ПК это не большая проблема, так как у вас много оперативной памяти и ОС может реорганизовать данные. чтобы лучше подходил. Но на Arduino у вас очень ограниченная оперативная память и нет ОС, поэтому у вас быстро заканчивается оперативная память. В этот момент ваш скетч перестанет работать, так как на устройстве не останется доступной памяти.

Вместо этого вам следует пойти более сложным путем и просто использовать фиксированные массивы, которые достаточно велики, чтобы вместить наибольший объем данных, который вы ожидаете.


И так как AnT не осветил это полностью:

bob[6] = 70;

Эта строка не может работать правильно по двум причинам: во-первых, вы только что объявили bob как указатель, но этот указатель не указывает на действительный адрес памяти. Вам нужно будет создать соответствующий массив с ключевым словом new, как показано выше, или позволить ему указывать на существующий массив. В настоящее время вы пытаетесь выполнить запись в неверный или случайный адрес памяти.

Во-вторых, компилятор не может знать, сколько элементов в массиве, на которые должен указывать bob. Это просто не та информация, которой располагает компилятор. Так что это не помешает вам написать более высокие индексы, чем есть в массиве. Но фактически запись в память либо завершается неудачно, потому что адрес несуществующего элемента недействителен, либо вы перезаписываете данные из каких-то других переменных --> Очень опасно! Вы несете ответственность за то, чтобы вы не записывали данные там, где им не место.

И имейте в виду, что у микроконтроллеров нет исключений. Таким образом, некоторые вещи, которые через исключение на ПК остановят работу микроконтроллера, или он просто будет делать сумасшедшие вещи, которые вы ему сказали (где доступ к памяти вне массива приведет к ошибке времени выполнения на ПК). ).

,

Да, это имеет большой смысл. Я пытаюсь написать библиотеку общего назначения для NodeMCU, которая имеет много памяти (я думаю), и это не будет проблемой. Но, учитывая, что я мог бы использовать его в других проектах для других моделей Arduino, ваш ответ многое проясняет. Спасибо., @MrCabana


0

Массивы C сложны, и они описаны в каждом руководстве по C. Другие ответы касаются ошибок/вопросов в комментариях, но ни один из них не показывает, как это можно сделать:

struct BuzzNote {
  int note;
  int duration;
};

const BuzzNote melody[] = {
  {10, 200}, { 45, 300}, {90, 200}, // ...
};

void test(BuzzNote const * arr, size_t len) {
  for (size_t i = 0; i < len; ++i) {
    Serial.print(i);
    Serial.print(". ");
    Serial.print(arr[i].note);
    Serial.print(' ');
    Serial.println(arr[i].duration);
  } 
}

// использование шаблона для определения размера массива
// (и в качестве бонуса он не будет работать с простыми указателями)
template<typename T, size_t size> 
size_t GetArrLength(T(&)[size]){return size;}

void setup() {
  Serial.begin(9600);
}

void loop() {
  test(melody, GetArrLength(melody));
  delay(10000);
}

Я использую шаблонную версию получения длины массива (он не будет компилироваться с указателями, так как указатель не является массивом).

И эта версия хранит данные в оперативной памяти (и тратит их впустую). Если у вас нет свободного места в оперативной памяти, вам придется копаться в переменных/массивах PROGMEM. Но это квест на другой день :D

,

1

Хорошо, прочитав и прочитав ответы и ссылки на мой вопрос, я в конечном итоге использовал функцию, как было предложено. Что-то вроде:

void foo(char* myArray, int size) {}

Но в итоге я нашел другое решение проблемы, которое немного сложнее:

#define arrayLen(x) (sizeof(x) / sizeof(x[0]))

char myArray[64] = "my array test with a long value in it";

void setup() {

  Serial.begin(9600);
  while (!Serial);

  // ИМХО, было бы неплохо, если бы компилятор принимал эти два синтаксиса
// myArray = "мой тестовый массив с длинным значением";
// "мой тест массива с длинным значением в нем".toCharArray(myArray, 64);

  Serial.print("Value before: ");
  Serial.println(myArray);
  Serial.print("Size before: ");
  Serial.println(arrayLen(myArray));

  String temp;
  temp = myArray;

  foo(temp);

  Serial.print("Value after: ");
  Serial.println(myArray);
  Serial.print("Size after: ");
  Serial.println(arrayLen(myArray));

}

void loop() {}

void foo(String str) {
  for (int i = 0; i < str.length(); i++) {
    Serial.print("Index ");
    Serial.print(i);
    Serial.print(": ");
    Serial.println(str.charAt(i));
  }
}

Я знаю, что у меня есть плохая привычка использовать строки вместо массива символов (из других языков), но в этом примере проблемы с ограничением памяти не будут значительными, поскольку строка будет освобождена из памяти после выполнения установки. () и foo().

Есть еще одна вещь, которой я хотел бы поделиться (не имеющая прямого отношения к моему предыдущему вопросу), которая, как мне кажется, может быть полезна другим людям. Этот небольшой набросок показывает, как использовать ту же технику String для управления содержимым массива char, переданного по ссылке во внешнюю функцию. Вот оно:

#define arrayLen(x) (sizeof(x) / sizeof(x[0]))

char myArray[64] = "my array test with a long value in it";

void setup() {

  Serial.begin(9600);
  while (!Serial);

  Serial.print("Value before: ");
  Serial.println(myArray);
  Serial.print("Size before: ");
  Serial.println(arrayLen(myArray));

  String temp;
  temp = myArray;

  foo(temp);
  temp.toCharArray(myArray, 64);

  Serial.print("Value after: ");
  Serial.println(myArray);
  Serial.print("Size after: ");
  Serial.println(arrayLen(myArray));

}

void loop() {}

void foo(String &str) {
  str = "abcde";
}

Результаты (как и ожидалось):

Value before: my array test with a long value in it
Size before: 64
Value after: abcde
Size after: 64

В глазах программиста на C++ это может выглядеть ужасно, но это работает!

Еще раз спасибо за все ответы/комментарии.

,