Массив динамического размера в качестве члена класса

Я хочу создать массив символов динамического размера в качестве члена класса. Это делается внутри библиотеки, которую я создал. У меня созданы файлы .h и .cpp. Я не уверен, как объявить массив в файле .h. До сих пор я пробовал кое-что: char* content; содержание символов[]; Но это, похоже, не работает.

Вот что у меня есть в моем файле .h:

class bufr{
    public:
    //constructor and deconstructor
    bufr(char* _chars, uint8_t _len);
    ~bufr();

    //variables
    uint8_t len;
    char content[];

};

Вот что у меня есть в моем файле .cpp (это конструктор):

bufr::bufr(char* _chars, uint8_t _len){
    char* content = new char[_len];
    content = _chars;
    len = _len;
}

Я также попытался заменить содержимое строки = _chars следующим:

for (int i = 0; i < _len; i++){
    content[i] = _chars[i];
}

Но это также не работает. Когда я печатаю элемент содержимого в последовательный порт из конструктора или в основном цикле, он печатает бессмысленные символы, а не то, что я передал.

Вот мой основной цикл:

void loop(){
    char chars[] = "1234";
    bufr buf(chars, 4);
}

Достаточно ли хорошо я объяснился? Кто-нибудь может мне помочь? Очень признателен!

О, и да, я знаю - в моем деконструкторе, который не показан, у меня есть контент для удаления []

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

Спасибо!

, 👍1

Обсуждение

На самом деле, я солгал выше... Я просто проводил еще несколько тестов. Если я печатаю на последовательный порт из конструктора, элемент "содержимое"печатается правильно: "1234". Но, если я печатаю из основного цикла следующим образом: `serial.println(buf.content); " отображается неправильно. Что я делаю не так?, @Killerb81


2 ответа


Лучший ответ:

1

Это неправильно по ряду причин. Сначала в объявлении класса:

char content[];

Это дает вам указатель на массив нулевой длины.

Это дает вам массив символов нулевой длины. Это бесполезно.

char* content = new char[_len];
content = _chars;

Это создает новое содержимое в новой переменной, а не в переменной класса. Затем вы назначаете (перезаписываете) переданный указатель на содержимое (отбрасывая только что выделенную память). Этот указатель затем отбрасывается, когда конструктор заканчивается. Возможно, было бы лучше использовать strcpy.

Я составил небольшой пример на основе вашего кода, который выводит правильный результат:

class bufr 
  {
  public:
    //конструктор и деконструктор
    bufr(char* chars, uint8_t len);
    ~bufr();

    //переменные
    uint8_t len_;
    char * content_;
  };    // end of class bufr

bufr::bufr(char* chars, uint8_t len) 
  {
  content_ = new char[len];
  memcpy (content_, chars, len);  // скопируйте в выделенную память
  len_ = len;   // запомните длину
  } // конец конструктора

bufr::~bufr()
{
  delete [] content_;
} // конец деструктора

void setup() 
{
  Serial.begin (115200);

  char chars[] = "1234 ";
  bufr buf(chars, sizeof chars);

  Serial.println (buf.content_);
} // завершение настройки

void loop() 
{
} // конец цикла

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

Небольшой пример:

char * content;

void loop ()
  {
  char * content = "foo";  // создает НОВУЮ переменную
  }

В этом примере есть две переменные с именем content. Тот, который находится в функции "тени", является глобальным. Однако приведенный ниже код содержит только одну переменную:

char * content;

void loop ()
  {
  content = "foo";  // использует существующую переменную
  }

В деструкторе, когда вы удаляете указатель на массив, освобождает ли это память, выделенную массиву? Теперь он свободен, чтобы его снова назначило что-то другое? Или он все еще находится там, инициализированный как этот массив, и его нельзя использовать снова?

Вы не удаляете указатель. Вы удаляете память, на которую она указывает. Указатель теперь указывает на освобожденную память, поэтому использовать его в качестве указателя на данном этапе было бы неправильно. Вы можете сделать еще одно новое, чтобы оно указывало на что-то другое.


они удаляют указатель, а затем удаляют указатель [].

Вы неправильно истолковали это. Вы либо удаляете указатель, либо удаляете указатель [] в зависимости от того, является ли указатель массивом или нет. Я переработал контент, чтобы он не был массивом, поэтому удаление его без скобок правильно, однако, поскольку он выделен с помощью new [], он должен быть удален с помощью delete [].


по ссылке, опубликованной выше, они говорят, чтобы использовать удаление следующим образом: удалите указатель, если это был один элемент, выделенный новым, или удалите указатель [], чтобы освободить выделение памяти для массивов элементов, выделенных новым и размером в []. Итак, в моем случае я хочу использовать delete [] content_?

Нет, вы не хотите создавать массив в первую очередь. Взгляните на этот небольшой пример:

struct   {
  char bar [];
  } foo;

void setup() 
{
Serial.begin (115200);
Serial.println ("Starting");
Serial.println (sizeof (foo));
Serial.println (sizeof (foo.bar));
}

void loop() 
{
}

Это печатает:

Starting
0
0

Панель переменных представляет собой массив нулевой длины. Таким образом, вся структура foo также имеет нулевую длину. Это даже не указатель (на этой платформе это было бы два байта).

Неправильно использовать такой массив символов, когда вы хотите динамически выделять память. Посмотрите на это:

char foo [4];

Это выделяет 4 байта памяти для foo. Вы не можете позже сделать это дольше, сделав новое.

Чего вы хотите, так это:

char * foo;

Теперь вы выделили указатель и можете создать новый, например, такой:

foo = new char [10];  // выделит 10 байт

А затем удалите его:

delete [] foo;

Вы используете скобки при удалении, как вы использовали их при создании.

Если бы вы выделили один символ, вы бы не использовали скобки. например.

char * foo;
foo = new char;  // выделит указатель на один байт
...
delete foo;
,

На самом деле, содержимое символа[]; не дает вам указатель - он дает вам _буффер_ нулевой длины., @John Burger

Спасибо за вашу помощь. Можете ли вы объяснить мне, почему он создавал **новую** переменную "содержимое" и не использовал переменную класса?, @Killerb81

Еще один вопрос относительно вашего ответа: В деструкторе, когда вы удаляете указатель на массив, освобождает ли это память, выделенную массиву? Теперь он свободен, чтобы его снова назначило что-то другое? Или он по-прежнему инициализирован как этот массив и не может быть использован снова?, @Killerb81

На этой странице: [ссылка](http://www.cplusplus.com/doc/tutorial/dynamic/) они показывают создание динамического массива так же, как и вы, но когда они удаляют его, они "удаляют указатель", а затем " удаляют указатель []". Ты пропустил это мимо ушей? Или это необходимо?, @Killerb81

Извините, еще одна правка.. Я допустил ошибку, по ссылке, опубликованной выше, они говорят, что нужно использовать`удалить "следующим образом: "удалить указатель", если это был один элемент, выделенный с помощью "нового" или "удалить [] указатель", чтобы освободить выделение памяти для массивов элементов, выделенных с помощью "нового" и размером в" []". Итак, в моем случае я хочу использовать "удалить [] содержимое"? Еще раз спасибо!, @Killerb81

@JohnBurger - я написал это изначально, а затем изменил, потому что задание, казалось, сработало. Но вы правы. :), @Nick Gammon

Смотрите исправленный ответ., @Nick Gammon

@Killerb81 Вы написали char* content = новый символ[_len]; в конструкторе. Здесь говорится: "Мне нужна совершенно новая переменная char*, которую я буду называть"содержимым" (неважно, что есть такой член!), И я хочу, чтобы вы инициализировали ее указателем "новый" из кучи символов "_len"". Если вы хотите изменить "содержимое" участника, удалите `символ * " в начале., @John Burger

Спасибо, что нашли время, ребята, я действительно ценю это. @NickGammon, в приведенном выше примере " удалите foo;", хотя " foo " является указателем, это позволяет использовать все 10 байтов, выделенных для ОЗУ (и два байта для указателя?) снова доступно в оперативной памяти? Это та часть, которая сбивает меня с толку, и поэтому я спросил, правильно ли вставлять " [] ` в инструкцию delete., @Killerb81

@JohnBurger ну, разве " foo " на самом деле не указатель на символ? Не 10 байт, выделенных для самого массива символов. Я просто не понимаю, как использование "удалить" на указателе освобождает 10 байтов (его пример), выделенных для массива., @Killerb81

Да, " фу "- это указатель на "чар". Но это _ _ таким образом любезно предоставлено foo = новый символ[10];, что делает его указателем не только на первый символ, но и на следующие 9, все они находятся в куче. Если бы вы выполнили следующий код: 'char c = 'A'; char *p = &c; удалить [] p;`, то "удалить []" было бы очень приятно - при этом полностью разрушив вашу кучу. **Содрогнись** !, @John Burger

хорошо, я понимаю, что ты имеешь в виду. Значит, определенно правильно использовать " удалить содержимое []`? Я просто не понимаю, почему он написал: "foo-это не массив, поэтому вы не используете скобки". Я не хочу случайно повредить свою оперативную память во время выполнения., @Killerb81

Джон Бергер и Киллербах правы. Я запутался в скобках при создании/удалении. Я исправил свой ответ. Мне пришлось поработать над этим в [этом недавнем посте](http://arduinoprosto.ru/q/25315/building-a-vector-class-in-arduino). "Я просто не понимаю, почему он написал" foo - это не массив, поэтому вы не используете скобки". - Я был неправ. Ну что ж, это будет не в последний раз. :), @Nick Gammon


2

У многих программистов есть несоответствие между указателем и массивом. Вот вам объяснение.

Вот переменная типа char, инициализированная значением "A":

char c = 'A';

"A" имеет значение ASCII 65, так что это означает, что в памяти есть байт, содержащий значение 65. Предположим, что байт в памяти находится по адресу 0x2000. Если бы вы добавили 1 к этому байту, он стал бы 66, а если бы вы напечатали его как символ, он бы напечатал B (ASCII 66).

Вот указатель на символ, инициализированный адресом в памяти c:

char *p_c = &c; // Get the address of c into p_c

[Обратите внимание, что char* p_c = c; будет недопустимым: вы не можете инициализировать указатель символом! Это дало бы p_c значение 65 - не то, что вы хотите!]

В Arduino p_c представляет собой два последовательных байта памяти, которые вместе хранят адрес символа: в данном случае значение 0x2000. Где находятся эти два байта, не имеет значения для этого объяснения, но поверьте, что у них тоже есть адрес...

Если бы вы изменили c на "Z", это изменило бы содержимое c, но не p_c(это все равно было бы 0x2000). Но если бы вы использовали p_c, чтобы посмотреть на байт, на который он указывал (c), вы бы увидели "Z".

  • Если бы вы сделали printf("%c", c); вы получили бы "Z".

    %c означает "У меня есть символ в качестве следующей переменной в списке printf ()".

  • Если бы вы сделали printf("%c", *p_c); вы бы также получили "Z".
  • Если бы вы сделали printf("%c", p_c); вы получили бы ужасные результаты.

    Вы солгали printf() - вы не дали ему однобайтовый символ, вы дали ему двухбайтовый указатель!

Теперь давайте перейдем к массивам.

Вот переменная типа char [], инициализированная значением "Джон".

char s[] = "John";

"J" имеет значение ASCII 74, "o" 112, "h" 104 и "n" 111. Это означает, что в памяти имеется четыре последовательных байта со значениями 74, 112, 104, 111. Предположим, что эти байты находятся по адресам от 0x3000 до 0x3003.

А вот еще один указатель на символ, на этот раз инициализированный адресом в памяти s.

char *p_s = s; // Point to s with p_s

Я ненавижу, что язык Си допускает все вышесказанное. Это один из корней всей путаницы между массивом и указателем. Это как бы подразумевает, что p_s каким-то образом получает все "Джон", назначенное ему. Нет!

Лучшим утверждением было бы:

char *p_s = &s; // Initialise p_s with the address of s // ILLEGAL!

Это дает ГОРАЗДО лучшее представление о том, что происходит - увы, это не так, как это делается...

Но также обратите внимание, что ни один из вышеперечисленных вариантов не может хранить 0x3000-0x3003 (адреса для всех s), поэтому они должны довольствоваться сохранением только начала строки: p_s получит значение 0x3000.

Обратите внимание, что в s нет значения 0x3000. Это его адрес, как 0x2000 был адресом c (в нем тоже нет 0x2000). Но в p_s есть 0x3000 - и не забывайте, что у p_s также есть свой собственный адрес (я не буду беспокоиться об этом здесь).

И вот тут "эквивалентность" (их нет) между указателем и массивом начинает сбивать людей с толку:

  • s[2] = 'h'; и p_s[2] = 'h'; сделайте то же самое - но по-разному!
    • Первый обращается непосредственно к 0x3002.
    • Второму нужно выяснить, что p_s равен 0x3000, и добавить к нему 2, прежде чем он сможет получить доступ к 0x3002.
  • *(p_s + 2) = 'h'; это еще один третий способ сделать то же самое.

Итак, теперь давайте распечатаем() эти новые переменные:

  • printf("%c", *s); не будет компилироваться.

    s не является указателем, поэтому вы не можете разыменовать его с помощью *.

  • printf("%c", *p_s); выведет символ в том месте, куда указывал p_s.

    Это позволило бы распечатать Дж.

  • printf("%c", s[0]); будет выводить символ в s[0].

    Это позволило бы распечатать Дж.

  • printf("%c", s); даст ужасные результаты.

    Опять ты солгал printf()!

  • printf("%s", s); будет работать (но см. Ниже).

    %s означает "У меня есть последовательность символов, которую я хотел бы, чтобы вы напечатали. Я указал адрес первого символа."

  • printf("%s", p_s); будет идентичен предыдущему.

    В конце концов, было передано одно и то же значение адреса!

  • printf("%s", *p_s); дал бы ужасные результаты.

    Опять ты солгал printf()!

Но обратите внимание на следующее: вы дали printf() начало строки, чтобы она могла начать печать. Как функция printf() узнает, где находится конец строки? Вы не указали номер 4; он нигде не хранится; как он может знать, когда остановиться?

Вот где вступает в действие соглашение C для строк. По соглашению, когда компилятор видит строку "между кавычками", он сохраняет значения ASCII символов, а затем следует за ними с 0. Не ASCII '0' (со значением 48), а ASCII NUL (со значением 0). Таким образом, если вы посмотрите на адрес 0x3004 (байт непосредственно после Джона), вы увидите 0 в этом байте памяти. Вот как printf() знает, что нужно прекратить печать - и да, многие ошибки произошли из-за того, что он забыл сохранить значение NUL в конце последовательности символов!

Это затем приводит меня к функции sizeof()

sizeof() всегда возвращает константу во время компиляции, используя информацию, о которой компилятор знает:

  • sizeof(c) вернет 1.
  • sizeof(p_c) вернет 2.
  • sizeof(p_s) также вернет 2.

    В конце концов, это тоже всего лишь указатель.

  • sizeof(*p_c) вернет 1.
  • sizeof(*p_s) также вернет 1.

    Это указывает только на один символ...

  • sizeof(s) вернет 5.

    Компилятор знает, что такое s: массив из 5 байт (включая окончательное значение NUL).

  • При наличии указателя нет (собственного) способа узнать, насколько большим может быть буфер, на который он указывает.

Хорошо: итак, если у вас есть символ [], вы можете узнать его размер. Обратите внимание, что это его максимальный размер - строка внутри него может быть намного короче! Если вы сделали следующее:

s[3] = 0; // Overwrite the fourth character (arrays start at 0)
printf("%s", s);

[Обратите внимание, что лучшей версией первой строки будет s[3] = '\0'; что означает "хранить символ ASCII 0 (НЕ" 0")"вместо" хранить число 0".]

ты бы распечатал Джо. Примечание:

  • размер(ы) остается 5;
  • Вы не можете получить длину с помощью sizeof(*p_s);
  • Строка внутри него состоит всего из 3 символов.

Итак... если у вас есть строковый буфер или указатель на него, как вы узнаете, какой он длины? Посчитайте количество символов, пока не получите НОЛЬ - и для этого уже есть функция: strlen().

Что теперь приводит к заключительному пункту об указателях: динамическое выделение памяти.

Массив s выше был определен компилятором во время компиляции: он мог подсчитать символы, добавить один для NULи зарезервировать нужное количество байтов. Вы также могли бы написать:

char s[10] = "John";

который зарезервировал бы 10 байт и инициализировал только первые 5 из них. Следующее было бы ошибкой:

char s[3] = "John"; // Buffer too small!

Страшно, что char s[4] = "Джон"; работает... (Компилятор говорит себе: "Нет места для NUL?! Ну что ж...”) Тогда лучше никогда не передавайте эту строку strlen() или printf ()!

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

char *p_b = (char *)malloc(size); // Where size is a variable
char *buffer = new char[size];    // Grab 'size' new chars

Первый вызывает malloc(), передавая количество требуемых байтов. Если malloc() может предоставить эти байты, он зарезервирует их и вернет указатель на них. Если он не сможет, он вернет 0. Вы всегда должны проверять возвращаемое значение! Если нет, то вы ужасно все испортите...

Во втором используется более сложный новый синтаксис [], который является более объектно-ориентированным. Обратите внимание, как возвращаемое значение malloc ()должно быть преобразовано в (char*)? Это потому, что он не знает, какой тип указателя он возвращает, поэтому он просто выбирает void *. new действительно знает, поэтому он возвращает правильный тип - но он все равно может вернуть 0, если нет места!

Как только у вас есть любой указатель, вы используете их точно так же, как раньше p_s. Ничто не мешает вам получить доступ за пределы буфера - точно так же, как это невозможно для p_s! Будь осторожен!

О, и как только вы закончите с куском памяти, вам нужно вернуть его в кучу:

free(p_b);
delete [] buffer;

Если вы использовали функцию malloc(), верните ее с помощью функции free(). Если вы использовали new[], верните его с помощью delete[].

И все вышесказанное объясняет, почему они изобрели объект String.

Он управляет памятью; отслеживает длину строки; позволяет получать доступ к отдельным символам и изменять их; позволяет изменять размер строки по мере необходимости; и избавляется от используемой памяти, как только вы закончите с ней. Неудивительно, что люди начинают использовать строковый синтаксис, когда видят строковые указатели...

Но строкиs полезны только для символов. Массивы и указатели могут относиться и ко всему, включая строкиs и (АХ!) указатели! О - о-о, вот мы и снова...

,

Это должна быть глава в учебнике!!! Спасибо @John., @Killerb81

@Killerb81, вероятно, несколько глав в учебнике ;), @Paul

Спасибо, люди. Я предложил это в качестве записи в [Вики-теге](http://arduino.stackexchange.com/tags/pointer/info) для указателя. Посмотрим..., @John Burger

Я забыл указать, что произойдет, если вы напишете printf("%s", p_c); . Излишне говорить, что вы получили бы хороший результат, а затем ужасные, так как он продолжал печатать символы, пока, наконец, не достиг "НУЛЯ"..., @John Burger