Лабораторная работа №1. Динамическое выделение памяти, работа с указателями

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

Требования к заданию
ЛК1 Указатели ЛК1 Стек и куча ЛК1 Константный указатель

Теоретическая основа

Указатели и динамическая память

Указатель — это переменная, которая в качестве своего значения содержит адрес байта памяти.

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

Основные операции с указателями:

  • & — операция получения адреса
  • * — операция разыменования указателя
  • new — выделение памяти
  • delete — освобождение памяти
  • new[] — выделение памяти под массив
  • delete[] — освобождение памяти массива

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

Требования

Создать одномерный динамический массив типа int, заполнить его случайными числами, вывести на экран. Размер массива необходимо хранить в первом элементе массива.

Реализация

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

// Функция генерации случайного массива
int* genRandArray(int size, int maxValue) {
    // Выделяем память для массива (размер + 1 для хранения размера)
    int* arr = new int[size + 1];
    
    // Проверяем успешность выделения памяти
    if (!arr) {
        cout << "Ошибка выделения памяти" << endl;
        return nullptr;
    }
    
    // Сохраняем размер в первом элементе
    arr[0] = size;
    
    // Заполняем массив случайными числами
    for (int i = 1; i <= size; i++) {
        arr[i] = rand() % maxValue + 1;
    }
    
    return arr;
}

// Функция печати массива
void print(int* arr) {
    if (!arr) {
        cout << "Массив не инициализирован" << endl;
        return;
    }
    
    int size = arr[0]; // Получаем размер из первого элемента
    cout << size << ": ";
    
    for (int i = 1; i <= size; i++) {
        cout << arr[i];
        if (i < size) cout << " ";
    }
    cout << endl;
}

int main() {
    srand(time(0)); // Инициализация генератора случайных чисел
    
    int size = rand() % 10 + 1; // Размер от 1 до 10
    int maxValue = 100;
    
    int* arr = genRandArray(size, maxValue);
    print(arr);
    
    // Очистка выделенной памяти
    delete[] arr;
    
    return 0;
}

Пример вывода

7: 44 11 24 41 10 57 100

Задание 2. Двумерный динамический массив

Требования

Создать двумерный динамический массив типа int, заполнить его случайными числами, вывести на экран. Размер каждого одномерного массива — произвольный (матрица не обязана быть прямоугольной).

Реализация

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

// Функция генерации случайного массива (из задания 1)
int* genRandArray(int size, int maxValue) {
    int* arr = new int[size + 1];
    if (!arr) {
        cout << "Ошибка выделения памяти" << endl;
        return nullptr;
    }
    
    arr[0] = size;
    for (int i = 1; i <= size; i++) {
        arr[i] = rand() % maxValue + 1;
    }
    
    return arr;
}

// Функция печати одномерного массива (из задания 1)
void print(int* arr) {
    if (!arr) return;
    
    int size = arr[0];
    cout << size << ": ";
    
    for (int i = 1; i <= size; i++) {
        cout << arr[i];
        if (i < size) cout << " ";
    }
    cout << endl;
}

// Функция генерации двумерного массива
int** genRandMatrix(int size, int maxValue) {
    // Выделяем память для массива указателей
    int** matrix = new int*[size];
    if (!matrix) {
        cout << "Ошибка выделения памяти для матрицы" << endl;
        return nullptr;
    }
    
    // Создаем каждую строку с произвольным размером
    for (int i = 0; i < size; i++) {
        int rowSize = rand() % 8 + 1; // Размер строки от 1 до 8
        matrix[i] = genRandArray(rowSize, maxValue);
        
        if (!matrix[i]) {
            // Если не удалось выделить память для строки, очищаем уже выделенные
            for (int j = 0; j < i; j++) {
                delete[] matrix[j];
            }
            delete[] matrix;
            return nullptr;
        }
    }
    
    return matrix;
}

// Функция печати двумерного массива
void printMatrix(int** matrix, int size) {
    if (!matrix) {
        cout << "Матрица не инициализирована" << endl;
        return;
    }
    
    cout << size << endl; // Выводим количество строк
    
    for (int i = 0; i < size; i++) {
        print(matrix[i]); // Используем функцию печати одномерного массива
    }
}

int main() {
    srand(time(0));
    
    int size = rand() % 10 + 1; // Количество строк от 1 до 10
    int maxValue = 100;
    
    int** matrix = genRandMatrix(size, maxValue);
    printMatrix(matrix, size);
    
    // Очистка памяти
    if (matrix) {
        for (int i = 0; i < size; i++) {
            delete[] matrix[i];
        }
        delete[] matrix;
    }
    
    return 0;
}

Пример вывода

4
1: 15
5: 54 23 15 5 12
7: 1 32 51 42 51 100 12
3: 50 37 17

Полный код программы

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

int* genRandArray(int size, int maxValue) {
    int* arr = new int[size + 1];
    if (!arr) {
        cout << "Ошибка выделения памяти" << endl;
        return nullptr;
    }
    
    arr[0] = size;
    for (int i = 1; i <= size; i++) {
        arr[i] = rand() % maxValue + 1;
    }
    
    return arr;
}

void print(int* arr) {
    if (!arr) return;
    
    int size = arr[0];
    cout << size << ": ";
    
    for (int i = 1; i <= size; i++) {
        cout << arr[i];
        if (i < size) cout << " ";
    }
    cout << endl;
}

int** genRandMatrix(int size, int maxValue) {
    int** matrix = new int*[size];
    if (!matrix) {
        cout << "Ошибка выделения памяти для матрицы" << endl;
        return nullptr;
    }
    
    for (int i = 0; i < size; i++) {
        int rowSize = rand() % 8 + 1;
        matrix[i] = genRandArray(rowSize, maxValue);
        
        if (!matrix[i]) {
            for (int j = 0; j < i; j++) {
                delete[] matrix[j];
            }
            delete[] matrix;
            return nullptr;
        }
    }
    
    return matrix;
}

void printMatrix(int** matrix, int size) {
    if (!matrix) {
        cout << "Матрица не инициализирована" << endl;
        return;
    }
    
    cout << size << endl;
    
    for (int i = 0; i < size; i++) {
        print(matrix[i]);
    }
}

int main() {
    srand(time(0));
    
    cout << "=== ЗАДАНИЕ 1: Одномерный массив ===" << endl;
    int size = rand() % 10 + 1;
    int maxValue = 100;
    int* arr = genRandArray(size, maxValue);
    print(arr);
    delete[] arr;
    
    cout << "\n=== ЗАДАНИЕ 2: Двумерный массив ===" << endl;
    size = rand() % 10 + 1;
    int** matrix = genRandMatrix(size, maxValue);
    printMatrix(matrix, size);
    
    if (matrix) {
        for (int i = 0; i < size; i++) {
            delete[] matrix[i];
        }
        delete[] matrix;
    }
    
    return 0;
}

Ответы на контрольные вопросы

1. В чем разница между статической и динамической памятью? Преимущества, недостатки.

Статическая память:

  • Размещение: В области данных программы (глобальные переменные) или в стеке (локальные переменные)
  • Время выделения: На этапе компиляции
  • Размер: Фиксированный, известен заранее
  • Управление: Автоматическое (освобождается при выходе из области видимости)
  • Преимущества: Быстрое выделение, автоматическое управление
  • Недостатки: Фиксированный размер, ограниченный объем стека

Динамическая память:

  • Размещение: В куче (heap)
  • Время выделения: Во время выполнения программы
  • Размер: Может изменяться во время выполнения
  • Управление: Ручное (программист должен освобождать память)
  • Преимущества: Гибкость размера, большой объем доступной памяти
  • Недостатки: Медленнее выделение, возможны утечки памяти

2. Что такое указатель?

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

int x = 10;
int* p = &x; // p содержит адрес переменной x

3. Что означает тип указателя?

Тип указателя определяет:

  • Тип данных, на которые указывает указатель
  • Количество байт, которые будут читаться/записываться при разыменовании
  • Правила арифметики указателей
int* p;    // указатель на int (4 байта)
char* c;   // указатель на char (1 байт)
double* d; // указатель на double (8 байт)

4. Какие операции допустимы по отношению к указателю?

  • Присваивание адреса: p = &x;
  • Разыменование: *p = 5;
  • Арифметические операции: p++, p--, p + n, p - n
  • Сравнение: p1 == p2, p1 != p2, p1 < p2
  • Присваивание NULL: p = nullptr;
  • Проверка на NULL: if (p != nullptr)

5. Константность указателей

int x = 10, y = 20;

// Константный указатель (нельзя изменить адрес)
int* const p1 = &x;
*p1 = 15;    // OK
// p1 = &y;  // ОШИБКА

// Указатель на константу (нельзя изменить значение)
const int* p2 = &x;
// *p2 = 15; // ОШИБКА
p2 = &y;     // OK

// Константный указатель на константу
const int* const p3 = &x;
// *p3 = 15; // ОШИБКА
// p3 = &y;  // ОШИБКА

// Константный массив
const int arr[] = {1, 2, 3, 4, 5};

// Константная матрица
const int* const matrix[] = {arr, arr+2};

6. Размер (sizeof) указателя, от чего зависит

Размер указателя зависит от архитектуры процессора и операционной системы:

  • 32-битные системы: 4 байта
  • 64-битные системы: 8 байт
cout << "Размер int*: " << sizeof(int*) << " байт" << endl;
cout << "Размер char*: " << sizeof(char*) << " байт" << endl;
cout << "Размер double*: " << sizeof(double*) << " байт" << endl;
// Все указатели имеют одинаковый размер независимо от типа данных

7. Зачем необходимо чистить память?

Причины освобождения динамической памяти:

  1. Предотвращение утечек памяти: Неосвобожденная память остается занятой до завершения программы
  2. Экономия ресурсов: Освобожденная память может быть использована повторно
  3. Предотвращение фрагментации: Своевременное освобождение предотвращает фрагментацию кучи
  4. Стабильность программы: Утечки памяти могут привести к исчерпанию доступной памяти
// Правильное использование
int* p = new int[100];
// ... работа с массивом ...
delete[] p;           // Обязательно освобождаем память
p = nullptr;          // Обнуляем указатель для безопасности

// Для двумерных массивов
int** matrix = new int*[rows];
for (int i = 0; i < rows; i++) {
    matrix[i] = new int[cols];
}
// ... работа с матрицей ...
for (int i = 0; i < rows; i++) {
    delete[] matrix[i]; // Освобождаем каждую строку
}
delete[] matrix;        // Освобождаем массив указателей

Правило: Каждому new должен соответствовать delete, каждому new[] должен соответствовать delete[].