Загрузка данных


Ход работы и тестирование (сплошным текстом)

Ход работы

Для выполнения лабораторной работы был разработан класс Vector, описывающий вектор в трёхмерном пространстве с компонентами типа double. При проектировании класса мы руководствовались принципом инкапсуляции, поэтому компоненты вектора x, y, z были помещены в закрытую секцию private, а доступ к ним осуществляется только через публичные методы-геттеры и сеттеры. Для того чтобы каждый создаваемый объект имел уникальный идентификатор, который требовался для вывода информационных сообщений вида «Constructor N» и «Destructor N», мы ввели в класс дополнительное приватное поле int id и статическую переменную static int counter. Выбор статической переменной был обусловлен тем, что счётчик должен быть общим для всех объектов класса, а не индивидуальным для каждого экземпляра, иначе все векторы получали бы одинаковый идентификатор. Инициализацию статического счётчика мы выполнили вне класса строкой int Vector::counter = 0;, поскольку в C++ статические поля класса требуют отдельного определения в области видимости файла.

Для создания объектов класса мы реализовали два конструктора, как и требовалось в задании. Первый конструктор — конструктор по умолчанию без параметров — инициализирует все компоненты вектора нулями и применяется тогда, когда нужно создать нулевой вектор. Второй конструктор принимает три аргумента типа double и позволяет задать компоненты вектора сразу при создании. В обоих конструкторах мы использовали список инициализации (например, : x(x_), y(y_), z(z_)), так как это более правильный с точки зрения C++ способ инициализации полей: значения присваиваются непосредственно при создании объекта, а не после этого в теле конструктора. В каждом конструкторе мы увеличиваем статический счётчик и присваиваем его значение в поле id, а также выводим на экран сообщение с номером созданного объекта, как того требует задание. Деструктор класса ~Vector() не выполняет никаких действий по освобождению памяти, поскольку класс не работает с динамической памятью, но выводит информационное сообщение с идентификатором уничтожаемого объекта, что позволяет наглядно проследить порядок вызова деструкторов при завершении программы.

Для доступа к компонентам вектора извне мы реализовали три геттера getX(), getY(), getZ() и три сеттера setX(), setY(), setZ(). Все геттеры помечены модификатором const в конце объявления, что означает обещание компилятору не изменять объект внутри метода. Это важно, потому что геттеры вызываются в том числе у константных ссылок, которые используются в качестве параметров внешних функций, и без модификатора const такой вызов не скомпилировался бы. Сеттеры, наоборот, модификатор const не имеют, так как их назначение — изменять состояние объекта. Далее в классе были реализованы функции-элементы для выполнения основных операций над вектором. Метод length() вычисляет модуль вектора по формуле √(x² + y² + z²), при этом для скорости и надёжности используется умножение x*x вместо вызова pow(). Метод copy() возвращает новый вектор с теми же компонентами, что и исходный, причём возврат осуществляется по значению, так как возвращать локальный объект по ссылке нельзя — он будет уничтожен сразу после выхода из функции. Метод multiply(double k) возвращает новый вектор, компоненты которого умножены на скаляр, что позволяет использовать его в цепочках выражений, не изменяя исходный вектор. Метод normalize() возвращает нормированный вектор единичной длины путём деления каждой компоненты на длину исходного вектора. Метод print() выводит компоненты вектора в требуемом формате x = <значение>; y = <значение>; z = <значение> и также помечен const, потому что вывод не изменяет объект.

Для реализации двуместных операций над векторами мы, следуя заданию, использовали не методы класса, а внешние функции. Чтобы эти функции имели доступ к закрытым полям x, y, z, мы объявили их дружественными внутри класса с помощью ключевого слова friend. Все параметры внешних функций передаются по константной ссылке const Vector&, что, с одной стороны, избавляет от лишнего копирования объектов, а с другой — гарантирует, что функция не изменит переданные векторы. Функция pluss() вычисляет сумму двух векторов и возвращает новый объект Vector; функция minuss() аналогично вычисляет разность. Функция vectornoeproizved() реализует векторное произведение по стандартной формуле, причём порядок компонент в формуле важен, поскольку перестановка слагаемых изменила бы знак результата. Функция scalar() возвращает скалярное произведение в виде числа типа double. Функции cosAngle() и sinAngle() вычисляют соответственно косинус и синус угла между векторами через скалярное и векторное произведения, поделённые на произведение длин векторов. Наконец, функция angleDegrees() возвращает угол между векторами в градусах в диапазоне от −180° до 180°; для этого мы использовали функцию atan2(sin, cos) из библиотеки <cmath>, которая, в отличие от acos, учитывает знак угла и корректно определяет направление поворота, а для перевода радиан в градусы результат умножался на 180.0 / M_PI, причём именно 180.0, а не 180, чтобы деление было вещественным, а не целочисленным.

В функции main() была организована демонстрация работы всех возможностей класса: создавались два вектора A и B с конкретными значениями, выводились их координаты и модули, выполнялись все двуместные операции, вычислялись скалярное произведение, косинус, синус и угол между векторами. Программа компилировалась командой g++ main.cpp -o lab1 -O0; флаг -O0 отключает оптимизацию компилятора, что позволяет наглядно наблюдать вызовы конструкторов и деструкторов без оптимизации возврата значения (RVO). В ходе исследования мы установили, что конструкторы вызываются не только при явном создании объектов, но и при каждом возврате объекта Vector из функции по значению, а также при создании временных объектов внутри выражений. Деструкторы временных объектов вызываются сразу после завершения выражения, в котором эти объекты были созданы, а деструкторы локальных переменных main — в самом конце программы, причём в порядке, обратном порядку создания: сначала уничтожается вектор B, созданный последним, затем вектор A. Это объясняется тем, что локальные переменные хранятся в стеке и разрушаются по принципу «последним пришёл — первым вышел».

Тестирование

Для проверки правильности работы программы было проведено несколько тестов, охватывающих все реализованные операции. В первом тесте проверялось создание двух векторов A(1, 2, 3) и B(4, 5, 6) и вывод их координат методом print(). Программа вывела строки x = 1; y = 2; z = 3 и x = 4; y = 5; z = 6, что полностью соответствует ожидаемому результату. Во втором тесте проверялся модуль вектора: для вектора A(1, 2, 3) программа вывела значение 3.74166, что совпадает с ручным расчётом √14 ≈ 3.74166; для вектора B(4, 5, 6) было получено значение 8.77496, равное √77. В третьем тесте проверялась операция сложения: результат pluss(A, B) дал вектор (5, 7, 9), что совпадает с покомпонентной суммой исходных векторов. В четвёртом тесте проверялось вычитание: результат minuss(A, B) дал вектор (−3, −3, −3), что соответствует ручному расчёту. В пятом тесте проверялось векторное произведение: для векторов A(1, 2, 3) и B(4, 5, 6) программа вывела (−3, 6, −3), что совпадает с вычислением по формуле A × B = (Ay·Bz − Az·By; Az·Bx − Ax·Bz; Ax·By − Ay·Bx). В шестом тесте проверялось скалярное произведение: программа вывела значение 32, что совпадает с ручным расчётом 1·4 + 2·5 + 3·6 = 32. В седьмом тесте проверялись косинус и синус угла между векторами: программа вывела cos = 0.974632 и sin = 0.22382, что совпадает с ручными расчётами по формулам cos φ = (A·B) / (|A|·|B|) и sin φ = |A × B| / (|A|·|B|); при этом проверка основного тригонометрического тождества дала cos² + sin² ≈ 1.0, что подтверждает правильность вычислений. В восьмом тесте проверялся угол между векторами: программа вывела 12.9332 градуса, что совпадает с результатом atan2(0.22382, 0.974632) ≈ 0.22576 радиан, переведённых в градусы. В девятом тесте проверялся граничный случай с нулевым вектором: модуль нулевого вектора оказался равен 0, что корректно. Однако при попытке нормировать нулевой вектор возникает деление на ноль и результат становится неопределённым (nan); этот недостаток был выявлен в ходе тестирования и может быть устранён добавлением проверки на нулевую длину в методах normalize(), cosAngle() и sinAngle(). В десятом тесте проверялся порядок вызовов конструкторов и деструкторов при запуске программы с флагом -O0. Анализ вывода показал, что после создания векторов A и B (сообщения Constructor N1 и Constructor N2) при каждом вызове внешней функции, возвращающей Vector по значению, создаётся временный объект (например, Constructor N3 при вызове pluss), который уничтожается сразу после завершения выражения (Destructor N3), а в самом конце программы уничтожаются локальные объекты main в обратном порядке: сначала Destructor N2 для вектора B, затем Destructor N1 для вектора A.

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