Construindo uma classe Vector no Arduino

Estou lutando com esse problema que consigo contornar. Ao definir uma classe vetorial, estou parecendo para obter alguns problemas ao excluir o ponteiro alocado.

Curiosamente, isso só acontece depois de eu “ler” a memória. O que quero dizer com isso é que, se eu empurrar de volta uma série de valores para o vetor, eles parecem permanecer lá. Mas, depois de ler os valores, o ponteiro de dados parece se tornar inválido de algum tipo, então eu travo ao tentar desalocá-lo.

Código:

#ifndef VECTOR_H #define VECTOR_H #include <string.h> /* memcpy */ #define min(a,b) (a < b) ? a : b template <typename T> class vector { public: vector() : __capacity(0), __count(0), __data(0) {} //empty constructor vector(T obj, int count = 1) { if (this->__data) { // free data ptr if used delete this->__data; } this->__data = new T[count]; this->__count = count; this->__capacity = count; for (int i = 0; i < count; i++) //populate array with given object this->__data[i] = obj; } ~vector() { if (this->__data) { // free data ptr if used delete [] this->__data; } this->__count = this->__capacity = 0; } T const & operator[] (unsigned int idx) const { if (idx < this->__count) { return this->__data[idx]; } else { // throw exception or handle error to be implemented } } T& operator[] (unsigned int idx) { if (idx < this->__count) { return this->__data[idx]; } else { // throw exception or handle error to be implemented } } void resize_to_fit() { resize(this->__count); } T& pop_back(){ return this->__data[--this->__count]; } void push_back(T obj) { if (this->__capacity == this->__count) { resize(this->__capacity + 1); } this->__data[this->__count++] = obj; } bool isempty() { return !this->__data || !this->capacity || !this->size; } void clear() { this->~vector(); } T* data() { return this->__data; } int size() { return this->__count; } int capacity() { return this->__capacity; } private: void resize(int capacity) { if (this->__data == nullptr) { this->__data = new T[capacity]; this->__count = 0; this->__capacity = capacity; } else if (capacity != this->__capacity) { //else do nothing T* data = new T[capacity]; this->__count = min(this->__count, capacity); this->__capacity = capacity; memcpy(data, this->__data, sizeof(T) * this->__count); delete this->__data; //program crashes here, but the pointer is already broken... this->__data = data; } } protected: int __capacity; int __count; T* __data; }; #endif//VECTOR_H 

Eu estava usando este código no meu Arduino, não tenho certeza se ajuda

void print_vec(vector<int> v){ Serial.println("Printing new vec!"); for( int i = 0; i < v.size(); i++){ Serial.println(v[i]); } } void setup() { // put your setup code here, to run once: Serial.begin(9600); // Open serial connection to report values to host while(!Serial); //Waiting for serial to open vector<int> i = vector<int>(); i.push_back(10); i.push_back(2); print_vec(i); //prints 10 and 2, perfect so far i.push_back(3); while(true){ print_vec(i); i.pop_back(); delay(2000); } } void loop() { // put your main code here, to run repeatedly: } 

Este código resulta: Printing new vec! 10 2 Imprimindo novo vec! 0 2 3 Imprimindo novo vec! 0 2 Imprimindo novo vec! 0 (…)

O que pode estar causando isso? Estou bloqueado por um tempo agora, qualquer ideia sobre como resolver isso é bem-vinda. Obrigado!

Comentários

  • No final do que você digamos que as saídas do código sejam “(…)” parte da saída que você vê ou não? Se não for ‘ t, como a saída difere do que você esperava ver? Se houver quatro itens em um vetor, quatro i.pop_back() chamadas devem esvaziar o vetor.
  • delete this->__data; //program crashes here ... deve usar delete[] certo? Além disso, não há código de plataforma em sua classe, então você pode depurar no pc, onde há ferramentas de depuração muito melhores disponíveis.

Resposta

Você teve muitos problemas com seu código, por isso é difícil saber por onde começar. 🙂

Eu concordo com o que John Burger disse.

Levei seu código para o meu PC para economizar tempo de enviá-lo toda vez e também para poder usar o valgrind nele. Certamente valgrind relatou um erro após você imprimir o vetor. A razão para isso é simples. Você passou uma cópia da classe para print_vec, o que significa que o destruidor obteve chamado quando print_vec saiu, desalocando assim a memória. Você deve ter um construtor de cópia . Sem ele, o compilador faz uma cópia bit a bit da classe, o que significa que agora você tem dois objetos compartilhando a mesma memória alocada.

Uma solução rápida e suja é chamar print_vec por referência:

void print_vec(vector<int> & v){ 

No entanto, isso deixa o bug à espreita para o futuro.

Eu implementei o construtor de cópia no meu exemplo abaixo, no entanto, chamar print_vec por referência evita que a classe precise ser copiada, reduzindo o número de new/delete s que você está fazendo, possivelmente reduzindo fragmentação da memória.


Como disse John Burger: não chame o destruidor você mesmo! Você não posso fazer isso. Se você quiser fazer a mesma coisa no destruidor e na função clear, faça com que o destruidor chame clear().


O uso de sublinhados duplos à esquerda em variáveis é contrário ao padrão C ++. Não faça isso. Use um sublinhado à direita se quiser indicar uma variável de membro. Perca todas as this-> referências. Eles apenas bagunçam as coisas.


Visto que você aloca uma matriz, você deve usar delete [] – você fez isso em um lugar, mas não no outro.


Por que fazer isso? É uma tarefa desnecessária:

 vector<int> i = vector<int>(); 

Basta fazer:

 vector<int> i; 

Se você vai atribuir a classe por algum motivo, você também deve implementar um operator= ou terá os mesmos problemas que teve com o construtor de cópia. (Veja meu exemplo).


Aqui, você está testando funções (capacidade e tamanho) sem chamá-las:

bool isempty() { return !this->__data || !this->capacity || !this->size; } 

Você realmente não pode fazer isso :

 T const & operator[] (unsigned int idx) const { if (idx < this->__count) { return this->__data[idx]; } else { // throw exception or handle error to be implemented } } 

Se você usar o caminho “else”, a função não retornará nenhum valor (você receberá um aviso do compilador se ativar os avisos).


Meu exemplo retrabalhado, com minhas sugestões nele. Compila sem avisos ou erros e é executado sem travar (em um PC):

// g++ -std=c++11 -g3 test.cpp -o test // OR: clang -std=c++11 -g3 -Wall test.cpp -lstdc++ -o test // // valgrind --leak-check=yes ./test #include <stdio.h> // printf #include <string.h> /* memcpy */ #define min(a,b) (a < b) ? a : b template <typename T> class vector { public: //empty constructor vector() : capacity_(0), count_(0), data_(nullptr) {} // copy constructor vector (const vector & rhs) : capacity_(0), count_(0), data_(nullptr) { data_ = new T [rhs.capacity_]; capacity_ = rhs.capacity_; count_ = rhs.count_; memcpy (data_, rhs.data_, sizeof (T) * count_); } // end of copy constructor // operator= vector & operator= (const vector & rhs) { // gracefully handle self-assignment (eg. a = a;) if (this == &rhs ) return *this; data_ = new T [rhs.capacity_]; capacity_ = rhs.capacity_; count_ = rhs.count_; memcpy (data_, rhs.data_, sizeof (T) * count_); return *this; } // end of operator= // destructor ~vector() { clear (); } // end of destructor T const & operator[] (unsigned int idx) const { return data_[idx]; } T& operator[] (unsigned int idx) { return data_[idx]; } void resize_to_fit() { resize(count_); } T& pop_back(){ return data_[--count_]; } void push_back(T obj) { if (capacity_ == count_) { resize(capacity_ + 1); } data_[count_++] = obj; } bool isempty() { return count_ == 0; } void clear() { delete [] data_; data_ = nullptr; count_ = capacity_ = 0; } T* data() { return data_; } int size() { return count_; } int capacity() { return capacity_; } private: void resize(int capacity) { if (data_ == nullptr) { data_ = new T[capacity]; count_ = 0; capacity_ = capacity; } else if (capacity > capacity_) { //else do nothing // allocate new memory T* data = new T[capacity]; // count can"t be higher than capacity count_ = min(count_, capacity); capacity_ = capacity; // copy data across to new pointer memcpy(data, data_, sizeof(T) * count_); // delete old pointer delete [] data_; // now remember the new pointer data_ = data; } } protected: int capacity_; int count_; T* data_; }; void print_vec(vector<int> v){ printf("%s\n", "Printing new vec!"); for( int i = 0; i < v.size(); i++){ printf("%i\n", v[i]); } } int main() { vector<int> i; i.push_back(10); i.push_back(2); print_vec(i); i.push_back(3); vector<int> j; // test assignment j = i; print_vec(j); print_vec(i); while(i.size () > 0){ print_vec(i); i.pop_back(); } } 

Comentários

  • Tudo bem, muito obrigado! Muitos dos problemas eram simplesmente uma distração da minha parte, eu estava tentando me concentrar em consertar os problemas de memória que estava enfrentando no início.

Resposta

Em seu segundo construtor, você começa com o seguinte código:

if (this->__data) { // free data ptr if used delete this->__data; } // if 

Isso é mortal! Você “não inicializou __data, então poderia conter qualquer valor sob o Sol. E não há nenhuma maneira de possivelmente ser um ponteiro válido para dados existentes – é um objeto não inicializado totalmente novo. Retornar esse ponteiro de lixo para o heap é simplesmente pedir problemas.

Remova essas linhas – ou melhor ainda, use uma lista de inicializadores como você fez com o primeiro construtor:

vector(T obj, int count = 1) : __data(new T[count]), __count(count), __capacity(count) { for (int i = 0; i < count; i++) //populate array with given object __data[i] = obj; } // vector(T, count) 

Outro problema: seu clear() membro que você escreveu:

void clear() { this->~vector(); } 

Você nunca deve, nunca , nunca chame diretamente um destruidor como este. O compilador pode colocar todos os tipos de outros códigos no código do destruidor, uma vez que “sabe” que é o único que vai chamá-lo. Por exemplo, alguns compiladores pré-enviam um sinalizador para dizer se delete o objeto da pilha após fazer a destruição. Você não fez isso, então o que foi descrito acima pode corromper todos os tipos de coisas.

Em vez disso, mova o código que está em o destruidor em clear() e, em seguida, basta chamar clear() do destruidor.

Outro, este pegou por @Nick Gammon:

bool isempty() { return !this->__data || !this->capacity || !this->size; } 

Isso testa se o membro __data é falso e se o capacity e size as funções foram definidas. Não se preocupe: os dois últimos foram … você perdeu o prefixo __

[Além disso, pare com this-> em todos os lugares. Você “usou um __ antes de todas as suas variáveis de membro (o que é contra a convenção ); você não precisa martelar o fato de que são variáveis de membro: o compilador já sabe.]

Comentários

  • Eu excluí todo o construtor vector(T obj, int count = 1) e ele compilou sem erros, de modo que não foi ‘ o problema neste caso, mas você está certo que __data não foi inicializado.
  • should never have compiled – testou as funções em vez de chamar eles. capacity é uma função de membro, não uma variável. Percebi isso quando tentei compilá-lo em clang.
  • Hee hee! Senti falta disso – e você ‘ está certo!
  • um conselho: Seu código é praticamente ilegível. Use um estilo de codificação melhor : google.github.io/styleguide/cppguide.html
  • Também não consigo entender por que você está fazendo isso. STL não é suportado, existem vários well-debugg ed implementações que você pode encontrar com uma simples pesquisa no google. Se você está fazendo isso para aprender C ++, o ambiente arduino é provavelmente a pior maneira de fazer isso!

Resposta

Há algo que gostaria de acrescentar.
Outros já apontaram vários erros no código. E eu poderia apontar muitos mais, mas não é esse o ponto.
IMO, o maior erro é – implementar uma classe Vector para o Arduino!
Mesmo que funcione, é apenas uma má ideia. Lembre-se de que o Arduino possui uma quantidade de memória MUITO limitada. A alocação de memória dinâmica em um espaço tão restrito parece uma má ideia – você está desperdiçando memória, está fragmentando o heap e não ganha quase nada, exceto alguma conveniência de codificação.
Ainda estou para ver um caso em que realmente precisa de alocação de memória dinâmica em um projeto Arduino. E mesmo se você fizer isso, existem muitas outras ferramentas que você pode usar (como buffers temporários de pilha, alocadores de arena, alocadores de pool e outros). Mas, novamente, provavelmente o código e o custo de memória de essas soluções “genéricas” serão inviáveis no Arduino.
Na maioria das vezes, não há problema em usar vetores, mapas e o que você quiser em plataformas como o PC (trabalho como programador de jogos para PC, então frequentemente encontro situações onde extras como esses são grandes “não-não”, mas para a maioria das aplicações é bom).
Mas, em uma plataforma “compacta” como o Arduino, acho que você deve ficar “perto do metal” e ser no controle total do que está acontecendo (memória e ciclos de CPU sábios). Idealmente, você só deve pagar “luxuri é “assim” quando você sabe o que está acontecendo “por baixo” e sabe que pode escapar impune. O que raramente é o caso em plataformas “restritas” como o Arduino.

Comentários

  • Você tem razão aqui. Em particular, a ideia push_back que aloca mais memória de forma incremental (o que normalmente funciona em um PC com 4 Gb de RAM) provavelmente introduz fragmentação da memória. Acho que até mesmo as implementações normais de STL alocam memória vetorial (capacidade, neste caso) em lotes (por exemplo, 10 de cada vez) para reduzir esse problema.

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *