воскресенье, 14 октября 2012 г.

Двумерный динамический массив в C++ (из 1D): перегрузка оператора ()

В предыдущей статье было показано, как можно использовать одномерный массив в качестве двумерного. Одновременно был отмечен ряд недостатков такой реализации. Повторим их кратко:
  1. при "ручном пересчёте" есть вероятность "глупой ошибки" вроде опечатки и т.п.; 
  2. использование функции "удлиняет" форму записи и делает менее прозрачным для понимания код; 
  3. и "ручной пересчёт", и вызов функции выглядят непривычно по сравнению с традиционной адресацией двумерного массива "[][]".

В C++ существует возможность перегрузки операторов, которая позволяет задать собственное поведение операторов пользовательских типов. Т.к. переопределить операторы стандартных типов нельзя, то повлиять на поведение '[]' одномерного массива мы никак не можем. Поэтому, чтобу "подружить" одномерный массив с его интерпритацией в качестве двумерного, нам потребуется сделать специальный класс-обёртку, которая и будет реализовывать необходимое поведение.

Зададим для "обёртки" следующие ограничения:
  1. не занимается управлением памятью, в т.ч. не выделяет память под массив, не освобождает её;
  2.  при инициализации (на этапе декларации) получает в качестве входных параметров-констант А) адрес начала памяти (одномерного массива), Б) размерность массива (предполагаем матрицу квадратной);
  3. должна отражать поведение, как если бы использовался исходный указатель;
  4. должна позволять производить чтение и модификацию элемента массива.
В С++ оператор "[]" принимает только один параметр, в то время как нам требуется два - первый (по строкам) и второй (по столбцам). Поэтому для решения задачи в такой формулировке мы "[]" использовать не можем, зато оператор "()" принимает любое количество параметров.

Напомним, что проектируемый класс просто обёртка над уже существующим 1D массивом:
typedef long CT; // Cell Type of the array

     /* ... */

    CT *arr = new(nothrow) CT[n*n]; //pointer to 1D-array of n*n elements
    if(!arr){ // no memory allocated
        cout << "The array was not allocated" << endl;
        return 1;
    }

Создадим к показанному выше 1D-массиву класс-обёртку:
class arrWrapper{
  private:
    CT *const &arr_ptr; //pointer to 1D array of n*n size
    const long n; // dimension of the square matrix [n,n]


  public:
    arrWrapper(CT *const &ptr, const long N):arr_ptr(ptr), n(N) {};
    inline CT& operator()(const int i, const int j)const 
                                        { return arr_ptr[i*n+j]; };
};

Здесь
-arr_ptr - ссылка на указатель (ссылка на массив). Именно этим приёмом (ссылкой) гарантируется, что класс-обёртка отреагирует на изменение исходного указателя (одномерного массива). Пример: память исходного массива была освобождена, а указатель установлен в NULL. Тогда, если по ошибке будет использована обёртка, произойдёт исключение по разыменованию NULL-указателя. Второй эффект такой реализации: если исходный указатель будет указывать на новый участок памяти (допустим, другой массив и т.п.), то и "обёртка" будет указывать туда же. При этом она будет считать, что размерность нового массива полностью соответствует старому!
-n - размерность массива [n,n]. Объявлена константой, т.к. изменение размерности у исходного массива произойти не может - для этого нужно выделить новую область памяти, скопировать туда данные, а затем освободить "старую" область.
arr_ptr и n размещены с секции private, чтобы избежать случайной порчи этих членов класса - вся работа с ними скрыта от пользователя. Более того, само это сокрытие, т.е. предоставление прозрачного интерфейса для обращения по индексам (и сокрытие механизма пересчёта индексом) и является целью создания класса-обёртки.
-arrWrapper(CT *const &ptr, const long N) - конструктор, принимающий в качестве параметров указатель (ссылку на указатель) на выделенную область памяти и соответствующий ей размер двумерного массива. Т.к. изменять значения этих параметров в процессе инициализации не требуется, они имеют квалификатор const.
- CT& operator()(const int i, const int j)const - определённая нами версия оператора (), которая и осуществляет пересчёт "двумерного" индекса в "одномерный". По реализации полностью совпадает с функцией пересчета из предыдущего поста. Обращение к элементу будет осуществляться как wrp(i,j), где wrp - "связанная" с некоторым одномерным массивом "обёртка".

Полный пример создания и использования двумерного динамического массива (на основе 1D-массива и перегрузки оператора ())

#include<stdio.h>   //to use memset
#include<iostream>  //to use cin, cout

typedef long CT; // Cell Type of the array

class arrWrapper{//Wrapper for 1D array
  private:
    CT *const &arr_ptr; //pointer to 1D array of n*n size
    const long n;       // dimension of the square matrix [n,n]

    arrWrapper& operator=(arrWrapper &)const{}; //to prevent 
              //default '=" operator, not needed to show the ()

  public:
    arrWrapper(CT *const &ptr, const long N):
                            arr_ptr(ptr), n(N) {};
    inline CT& operator()(const int i, const int j)const 
                            { return arr_ptr[i*n+j]; };
};


int main (int argc, char** argv)
{
    long n = 0; //variable to obtaint 2D-array size

    using namespace std; // to be able to use cin & cout
    cout << "Please, inter the matrix size n = ";

    //GET ARRAY SIZE
    cin >> n;      // read size n from keyboard

    //GET MEMORY FOR 1D ARRAY OF n*n SIZE
    CT *arr = new(nothrow) CT[n*n]; //pointer to 1D-array 
                                    //of n*n elements
    if(!arr){ // no memory allocated
        cout << "The array was not allocated" << endl;
        return 1;
    }

    //Fill array with char(0) values for sizeof(int)*n*n bytes
    memset(arr, static_cast<char>(0), 
           (sizeof(long)/sizeof(char))*n*n);

    //************************************************
    //INIT ('link') WRAPPER WITH THE 1D ARRAY and SIZE
    //************************************************
    //DECLARE WRAPPER
    arrWrapper wrp(arr, n);//derlare & init with array 

    //USE WRAPPER
    for (int i = 0; i < n; ++i){
       for(int j = 0; j < n; ++j){
           //ACCESS ELEMENT [i,j] USING the WRAPPER 
           wrp(i,j) = 10000*i+j;
       }
    }

    //FREE MEM
    delete [] arr;
    arr = NULL;

среда, 26 сентября 2012 г.

Двумерный динамический массив в C++: эмулирование одномерным массивом

Этот пост - первый в коллекции "велосипедов" по работе с двумерными массивами в С++. Оказалось, что удобно иметь место, куда можно послать и где держать краткое описание с пояснениями. Гугление же как правило выдаёт не очень хороший результат: нужное в нём найти можно, но много мусора, а велосипед чаще всего оказывается "не той системы".

Известно, что в С++ поддерживаются команды динамического выделения непрерывной области памяти - под одномерные массивы, для чего сущестует расширенный "квадратными скобками" синтаксис операторов new и delete

  int *iptr = new int[100];//get memory for 100 elements
  delete [] iptr; //free memory

Ожидания многих новичков не оправдываются - оператор для двумерного массива в языке отсутсвует, т.е.
  new int[100][100];//!error
просто ошибка. С другой стороны, он особенно и не требуется, поскольку динамические массивы любой размерности прекрасно создаются при помощи одномерного массива. Для этого достаточно помнить, что в С/С++ массивы хранятся по строкам (в отличие от, например, фортрана). Это значит, что двумерная матрица
   1 2 3
   4 5 6
хранится как одномерный массив
   1 2 3 4 5 6

Иногда после этого начинающий в С/С++ пытается создать одномерный динамический массив и присвоить указатель на него переменной типа двумерного массива (или указателя на указатели)
  int **ptr = NULL;
  int *iptr = new int[100*100];
  ptr = iptr; //!error
Такое присваивание не позволит сделать компилятор, кроме случая явного приведения int** к int*. Но в последнем случае хотя и не будет происходить ошибки на стадии компиляции, при выполнении (как минимум на операциях записи в массив) программа будет остановлена по нарушению доступа к памяти. Это произойдёт потому, что в ptr[i][j] значение из ptr[i] будет рассматриваться как адрес одномерного массива длиной [j]. Поэтому мы не можем использовать нотацию [][] и должны самостоятельно пересчитивать индекс элемента внутри одномерного массива.
Если m и n - размерность требуемого двумерного массива (т.е. число строк и столбцов), то пересчет индексов (обращение к [i,j]-тому элементу) можно осуществлять как
  iptr[i*n+j]

Полный пример создания и использования двумерного динамического массива (на основе 1D-массива)

#include<stdio.h>   //to use memset

typedef long CT; // Cell Type of the array

///////////////////////////////////////////
//   Main function demonstrate 2D array  //
///////////////////////////////////////////
int main (int argc, char** argv)
{
    long n = 10; //variable to obtain 
                 //2D [n,n]-array size.
                 //Input is better, 
                 //but for example purposes
                 //we don't use io routines. 

    //HERE WE GET ARRAY
    //Pointer to and memory allocation for
    //1D-array of n*n elements
    CT *arr = new(nothrow) CT[n*n]; 

    if(!arr){ // no memory allocated
        return 1; //STOP. Put here your code
                  //for no-memory case.
    }

   //Fill array with char(0) values
   //for sizeof(int)*n*n bytes,
   //i.e. init memory with zeroes. 
    memset(arr, static_cast<char>(0),        
           (sizeof(long)/sizeof(char))*n*n);

    //and HERE WE USE ARRAY
    for( int i = 0; i < n; ++i){
       for( int j = 0; j < n; ++j){
           //set arr[x][y] => arr[x*n+y]
           arr[i*n+j]=i*100+j; //write some
       }
    }

    //Free memory when you don't need it 
    delete [] arr; 

    //Set pointer to the de-allocated chunk
    //of memory to NULL to prevent 
    //misuse of already freed memory.
    arr = NULL;

    return 0;
}

С другой стороны, приходится постоянно вручную писать пересчёт индексов [i*n+j], что черевато опечатками и как следствие плохо диагностируемымы ошибками. Поэтому иногда предпочитают определить функцию пересчёта индексов для иключения таких ошибок, например
inline CT& element(const int i, const int j, 
                   CT*const arr, 
                   const int size)
{
    return arr[i*size+j];
}

Тогда цикл обращения будет выглядеть как
    //and HERE WE USE ARRAY
    for( int i = 0; i < n; ++i)
       for( int j = 0; j < n; ++j)
           element(i,j,arr,n) = i*100+j;

Обратим внимание, что функция возвращает ссылку (CT&) на элемент массива, и именно это обеспечивает возможность присваивания element(i,j,arr,n) = 1. Если же возвращать не ссылку на значение, а значение типа CT, то изменять значения элемента массива не получится (хотя чтение так же как и в верхнем случае будет выполняться нормально). Передаваемые же в функцию параметры объявлены константными (const) "для порядку", т.к. внутри функции мы их не меняем.