Урок 8. Указатель и массивы. Динамическая память

Теория

    Шаблонные функции

  • Шаблонная переменная объявляется началом класса или функции:
  • template <typename T>
    int max(T a, T b) {
      if (a > b) { return a; }
         return b;
    }
  • Шаблонные переменные проверяются во время компиляции, что позволяет отлавливать ошибки перед запуском программы.
  • #include <iostream>
    using namespace std;
     
    template <typename T>
    T max(T a, T b) {
    	if (a > b) { return a; }
    	return b;
    }
    int main()
    {
    	cout << "max(3, 5): " << max(3, 5) << endl; // 5
    	cout << "max('a', 'd'): " << max('a', 'd') << endl; // d
    	cout << "max(\"Hello\", \"World\"): " << max("Hello", "World") << endl; // Hello
    	cout << "max(3, 5, 6): " << max(3, 5, 6) << endl; // error
     
    	return 0;
    }
  • Шаблонные функции определяют семейство функций.
  • Объявлять их лучше в заголовочном файле, там, где и объявлены функции.
  • lib.cpp

    #include "lib.h"

    lib.h

    template<typename T>
    class My
    {
        My() { }
    T f(T a, T b);
    };
     
    template<typename T>
    T My::f(T a, T b)
    {
      return a + b;
    }
  • Шаблонная функция сама по себе не является типом, или функцией, или какой-либо другой сущностью. Никакой код не генерируется из исходного файла, содержащего только определения шаблонов. Для того чтобы появился какой-либо код, должен быть создан экземпляр шаблона: аргументы шаблона должны быть определены таким образом, чтобы компилятор мог сгенерировать функцию.
  • Явное определение экземпляра принудительно создает экземпляр функции или функции-члена, на которые они ссылаются. Он может появиться в программе в любом месте после определения шаблона, и для данного списка аргументов разрешается появляться в программе только один раз, диагностика не требуется.
  • template<typename T>
    void f(T s)
    {
        std::cout << s << '\n';
    }
     
    template void f<double>(double); // создает экземпляр f<double>(double)
    template void f<>(char); //создает экземпляр f<char>(char)
    template void f(int); // создает экземпляр f<int>(int)
  • Неявное определение экземпляра: Когда код ссылается на функцию в контексте, который требует существования определения функции, и эта конкретная функция не была создана явно, происходит неявное создание экземпляра. Список аргументов шаблона указывать не обязательно, если он может быть выведен из контекста.
  • #include <iostream>
     
    template<typename T>
    void f(T s)
    {
        std::cout << s << '\n';
    }
     
    int main()
    {
        f<double>(1); // создает экземпляр и вызывает f<double>(double)
        f<>('a'); // создает экземпляры и вызывает f<char>(char)
        f(7); // создает экземпляры и вызывает f<int>(int)
        void (*ptr)(std::string) = f; //
    }

    1. Указатель на одномерный массив в динамической памяти

    int* p = new int[10];
    for (int* q = p; q<p+10; q++)
      *q = 1;
     
    for (int i=0; i<10; i++)
      p[i] = 1;
     
    delete [] p;

    Для простых типов можно написать delete p (подсистема знает, что мы выделили массив), но для классов это ошибка.

    Errors related to using of dynamic memory

    Error 1. Accessing a section of dynamic memory that we have already returned using delete

    int* p = new int;
    *p = 777;
    delete p;
    *p = 666;
  • What to do?:
  • int* p = new int;
    *p = 777;
    delete p;
    p = nullptr; // !
    *p = 666;
  • Why it’s not enouph?:
  • int* p = new int;
    int * q = p;delete p;
    p = nullptr; // !
    *q = 666;
  • Error 2. We returned dynamic memory twice using delete
  • int* p = new int;
    *p = 777;
    delete p;delete p;
  • Error 3. Dynamic memory leak
  • int* p = new int[1000];
    *p = 777;// and forgot to delete!
  • Error 3a. Dynamic memory leak in the loop
  • for (int i = 0; i<100000; i++)
    {
      int* p = new int[1000];
      *p = 777;// and forgot to delete!
     
    }
  • Error 3b. Dynamic memory leak in the function.
  • void p()
    {
      int* p = new int[1000];
      *p = 777;// and forgot to delete!
    }
  • The rule. If dynamic memory is allocated in a function, then it must be returned in the same function. There are exceptions.
  • Two-dimensional arrays

  • A two-dimensional array is considered as a one-dimensional array of one-dimensional arrays
  • int a[3][4];
  • Two-dimensional rectangular arrays («matrices») are conveniently modeled using pointer arrays:
  • int ** mat = new int *[rows]; // future matrix of rows of rows

    Each element mat[i] of this array is a pointer, it should be initialized in a loop, assigning it the result new int [cols], where cols is the desired number of columns of the matrix.

  • The reference to the (i, j)-th element of the matrix looks like this: mat[i][j]. To iterate over the matrix, just as in the case of a regular array, you can use pointers.
  • It is necessary to empty (devastate) memory, it should be done in the reverse order: first, the memory for each row is released in the loop: delete [] mat[i]. And then the memory of the array of pointers mat: delete [] mat is released. This procedure should be executed as a function with one argument rows(number of rows).
  • Another example:

  • This is an array of three elements of type int [4]
  • a[i][j] ~ *(a[i] + j) ~ *(*(a + i) + j)

    Printing an array:

    void print(int a[][4], int m, int n)
    {
      for (int i=0; i<m; i++)
      {
        for (int j=0; j<n; j++)
        cout << a[i][j] << " ";
        cout << endl;
      }
    }

    Two-dimentional arrays and dynamic memory

  • In order to pass a two-dimensional array of arbitrary dimension to a function, it should be allocated in dynamic memory:
  • void print(int** a, int m, int n)
    {
      for (int i=0; i<m; i++)
      {
        for (int j=0; j<n; j++)
        cout << a[i][j]; // a[i][j] ~ *(*(a + i) + j)
        cout << endl;
      }
    }
    int main()
    {
    // allocation in memory
      int** p = new int*[3]; // pointer pointing to pointer (3 rows)
      for (int i=0; i<3; i++)
         p[i] = new int[4]; // 4 columns
      print(p,3,4);}
  • After using it in the function, the allocated memory must be returned:
  • int main()
    {
      int** p = new int*[3];
      for (int i=0; i<3; i++)
        p[i] = new int[4];
     
    print(p,3,4); // Нарисовать на доске
    // deallocation of memory:
    for (int i=0; i<3; i++)
      delete [] p[i];
    delete [] p;
    p = nullptr;
    }

    Stepwise two-dimensional dynamic arrays

    int main()
    {
      int** p = new int*[3];
     
      for (int i=0; i<3; i++)
        p[i] = new int[4];
     
      print(p,3,4);
      for (int i=0; i<3; i++)
        delete [] p[i];
      delete [] p;
      p = nullptr;
    }

Лабораторные работы

1. Одномерные массивы в динамической памяти

Лабораторная работа 1:
Выполнить: Создайте приложение с двумя шаблонными функциями. Одна из них запрашивает элементы одномерного массива заданного размера, другая — выводит элементы одномерного массива заданного размера.

Указание: Заголовки функций должны быть следующими:

template <typename T> 
void inputArr(T* arr, int &size);
 
template <typename T>
void printArr(const T* arr, int &size);

Примерный вывод:

Lab 1:
Array[0] 1.1
Array[1] 2.1
Array[2] 3.2
Array[3] 4.1
Array[4] 5.4

1.1 2.1 3.2 4.1 5.4 

✍ Алгоритм:

  • Над функцией main() добавьте код шаблонной функции для ввода элементов массива. Функция будет иметь два параметра — указатель на массив и ссылочную переменную, хранящую размер массива.
  • template <typename T> 
    void inputArr(T* arr, int &size) { // arr - указатель на массив любого типа
    	for (int i = 0; i < size; i++) {
    		cout << "Array[" << i << "] ";
    		cin >> arr[i]; // разыменование указателя
    	}
    }
    
  • В основной функции инициализируем переменную, хранящую размер массива (массив будет состоять из 5 элементов), и объявим указатель на новый массив типа double:
  •   int size = 5;
      double* myArray = new double[size];
    
  • Вызовите функцию:
  • inputArr(myArray, size);
    
  • Над функцией main() добавьте код шаблонной функции для вывода элементов массива:
  • template <typename T>
    void printArr(const T* arr, int &size) {
    	for (int i = 0; i < size; i++) {
    		//cout << arr[i] << " "; // разыменование способ 1:
    		cout << *(arr + i) << " "; //разыменование способ 2:
    	}
    }
    
    Используйте один из двух способов разыменования указателя.
  • Вызовите функцию:
  • printArr(myArray, size);
    
  • Раз для динамического массива была выделена память, то ее необходимо освободить:
  • delete[]myArray;
    myArray = NULL;
    
    Чтобы массив указателей не указывал на какой-либо адрес после оператора удаления, необходимо присвоить значение NULL.
  • Запустите программу.
Задание 1:

Выполнить: Дан массив целых чисел (A). Создайте массив, скопировав в него все элементы, но вместо элементов, последняя цифра которых равна 0, необходимо вставить 0. В этой задаче необходимо использовать динамическую память (new / delete). Создайте шаблонную функцию для вывода массива.

Указание 1: Убедитесь, что в основной программе после вывода результирующего массива память была освобождена (delete []).

Указание 2: Заголовок функции:

void MakeArrayWithZero(int* a, int& size);

Где a - массив, а size - размер массива.

Указание 3: Шаблонная функция для вывода массива должна быть объявлена в заголовочном файле:

template<typename T>
void printArray(T const * arr, int size, char delim = ' ') {
  ...
}

Примерный вывод:

Task 1:
Исходный массив:
1 20 30 4 5 
Результирующий массив:
1 0 0 4 5

[Solution and Project name: Lesson_8task1, file name L8Task1main.cpp, L8Task1imp.cpp, L8Task1.h]

Задание 2:

Выполнить: Дан целочисленный массив. Создать новый массив, в котором будут удалены из исходного массива все вхождения заданного числа. Исходный массив не изменяется. Не следует использовать какие-либо стандартные функции.

Указание 1: Сначала необходимо создать новый массив с указанным количеством элементов, как в исходном массиве. Для этого используйте функцию со следующим заголовком:

int* delete_all_entries(int* a, int size, int n, int& new_size)

Указание 2: Создайте шаблонную функцию для печати элементов массива. Используйте следующий заголовок функции:

template<typename T>
void print_array(T const* a, int size, char delim = ' ');

Примерный вывод:

Задание 2:
исходный массив:
1 2 3 2
число, которое следует удалить:
2
новый массив:
1 3

[Solution and Project name: Lesson_8task2, file name L8Task2main.cpp, L8Task2imp.cpp, L8Task2.h]

Двумерные массивы

Лабораторная работа №2:

Выполнить: Определить функцию для создания матрицы заданных размеров, все элементы которой равны заданному числу::
заголовочный файл:

int ** createMatrix(int rows, int cols, int value = 0);

The default value of 0 for the value parameter should be specified only in the header file. In the cpp file, the header contains three parameters in the usual form:
implementation file:

int ** createMatrix(int rows, int cols, int value) {
}

Значение по умолчанию равное 0 для параметра value должно быть указано только в заголовочном файле. В cpp файле заголовок функции содержит три параметра в обычной форме:

Указание : Для вывода элементов матрицы следует создать еще одну функцию с заголовком:

void printMatrix(int ** mat, int rows, int cols);

Примерный вывод:

Lab 2:
matrix 5 by 5:
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0

[Solution and Project name: Lesson_8Lab2, file name L8Lab2main.cpp, L8Lab2imp.cpp, L8Lab2.h]

✍ Алгоритм:

  1. Создайте приложение и добавьте необходимые директивы.
  2. Поскольку необходимо создать матрицу в функции, откройте заголовочный файл и добавьте сигнатуру функции:
  3. #pragma once
    
    #ifndef HEADER_H
    #define HEADER_H
    
    int ** createMatrix(int rows, int cols, int value = 0);
    
    #endif
    
  4. Затем откройте файл реализации и добавьте функцию:
  5. // ...
    int ** createMatrix(int rows, int cols, int value) {
      //...
    }
    
  6. Прежде всего, необходимо выделить память для двумерного массива (матрицы). Для этого добавьте код в функцию:
  7. int** res = new int*[rows];
    for (int i = 0; i < rows; i++)
    	{
    		res[i] = new int[cols];
    		for (int j = 0; j < cols; j++) {
    			res[i][j] = value;
    		}
    	}
    
    В первой строке объявляем указатель, указывающий на адреса строк матрицы, которые сами являются указателями, указывающими на адреса столбцов. Таким образом, выделяется память для матрицы.
    В строке #4 для каждой строки выделяется память для столбцов. Количество строк и столбцов берем из значений аргументов.
  8. Функция возвращает матрицу:
  9.   return res;
    } // конец функции
    
  10. Теперь перейдем в функцию main для вызова созданной нами функции.
  11. Поскольку функция вернет созданную матрицу, необходимо создать переменную для хранения этой матрицы и присвоить ей результат вызова функции.
  12. int main() {
      // матрица 4 х 5 , каждый элемент = 0:
      int** matrix = createMatrix(4, 5, 0);
      //...
    }
    
  13. Попробуйте запустить программу и убедитесь, что ошибок нет.
  14. Далее необходимо создать функцию для вывода элементов матрицы. Там будет три аргумента - сама матрица, количество столбцов и строк. Добавьте код в файл реализации:
  15. void printMatrix(int ** mat, int rows, int cols) {
    	for (int i = 0; i < rows; i++) {
    		for (int j = 0; j < cols; j++) {
    			cout << mat[i][j] << " ";
    		}
    	cout << endl;
    	}
    }
    
  16. Добавьте заголовок функции в заголовочный файл. Сделайте это самостоятельно.
  17. Запустите программу и убедитесь в отсутствии ошибок.
  18. После создания динамического массива мы должны освободить память. Создадим функцию для освобождения памяти. Добавьте код функции:
  19. void freeMemory(int ** mat, int rows) {
    	for (int i = 0; i < rows; i++) {
    		delete[] mat[i];
    	}
    	delete[] mat;
    	mat = NULL;
    }
    
    Поскольку нужно использовать указатели, необходимо знать только количество строк, которое передается из основной программы в качестве аргумента.
  20. Добавьте заголовок функции в заголовочный файл и вызовите функцию в основном исходном файле.
Задание 3:

Алгоритм: Создайте функцию для нахождения минимального и максимального значений матрицы. Для выполнения задания можно использовать аналогичные функции из лабораторной работы №2 (создать матрицу, вывести ее и освободить память). Элементы матрицы должны быть сгенерированы случайным образом. Заголовок функции для нахождения min и max должен быть следующим:

void matrixMinMax(int ** mat, int rows, int cols, int &min, int &max);

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

#include <ctime>
// ...
srand(time(0));
//.. в цикле:
array_name[i][j] = 1 + rand() % 10;

Примерный вывод:

Task 3:
matrix 4 by 5:
5 4 0 7 0
0 8 0 1 2
0 5 0 7 0
4 0 0 9 0

max = 9  min = 0

[Solution and Project name: Lesson_8task3, file name L8Task3main.cpp, L8Task3imp.cpp, L8Task3.h]

Задание 4:

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

Примерный вывод:

Task 4:
matrix:
8 3 3 6
6 4 9 8
6 1 9 6
4 5 1 5

sum = 26

[Solution and Project name: Lesson_8task4, file name L8Task4main.cpp, L8Task4imp.cpp, L8Task4.h]