- •Программная реализация формальных средств анализа и моделирования интегрированных систем ла
- •Примеры реализации пмо на Delphi
- •Реализация на основе записей (record)
- •Использование классов и объектных интерфейсов
- •Реализация с самостоятельным управлениям памятью
- •Реализация с использованием библиотеки шаблонов
- •Литература
Реализация с самостоятельным управлениям памятью
Программный модуль в С++ представлен двумя файлами – заголовочным (*.h) и файлом реализации (*.cpp). В заголовочном файле объявляются типы, классы, функции (интерфейс модуля), а во втором файле помещается их реализация. В рассматриваемом примере заголовочный файл в рамках пространства имён MyLinearAlgebra содержит объявления трёх классов – TVector, TMatrix и TQuaternion, причем последний агрегирует объект типа TVector (для хранения векторной части кватерниона).
В классах TVector и TMatrix есть защищённая секция, в которой объявлены переменные для хранения данных – массива значений и количества элементов в нем (для матрицы отдельно хранится количество строк и столбцов). В классе TQuaternion в защищённой секции объявлены переменные для хранения скалярной и векторной части кватеорниона. Использование для инкапсуляции полей с данными спецификатора protected вместо private вызвано возможной необходимостью наследования этих классов с сохранением прямого доступа к данным.
В классе TVector поле для доступа к данным представляет собой указатель на массив чисел типа double, который создаётся в динамической памяти при вызове конструктора или метода изменения размера массива void resize(int). В классе TMatrix аналогичное поле содержит указатель на массив указателей, каждый из которых, в свою очередь, адресует массив чисел (строк матрицы). Изменение размеров и количества этих массивов происходит при вызове конструктора или метода void resize(int, int), в который передаётся два параметра – желаемое количество строк и столбцов матрицы.
В качестве типа числовых данных предлагается использовать double, так как переменные этого типа имеют одинаковое представление в памяти на различных платформах и компиляторах (занимая 64 бита), и точности расчётов с его использованием достаточно для большинства математических моделей динамики интегирированных систем ЛА. В среде разработки Embarcadero RAD Studio на платформе x86 для повышения точности расчётов можно использовать тип long double, занимающий 80 бит и являющийся аналогом типа Extended в Delphi. Для компилятора С++ от Microsoft или Embarcadero RAD Studio на x64 тип long double является синонимом обычного double, поэтому преимуществ в точности от его использования не будет. Некоторые платформы и компиляторы (например, GNU C++ Complilier на Linux x64) отводят для размещения чисел типа long double 128 бит, обеспечивая наиболее высокую точность математических расчётов стандартными средствами.
В С++ возможна перегрузка операторов для пользовательских типов, причём в отличие от Delphi, эти типы могут быть представлены и классами. В рассматриваемом примере эта возможность используется для последующего упрощения записи вычислительных выражений с векторами, матрицами и кватернионами.
Ниже представлен пример содержимого заголовочного файла модуля mylinalg.h, в котором объявляются классы TVector, TMatrix и TQuaternion.
Листинг 8
namespace MyLinearAlgebra {
// Опережающие объявления
class TMatrix;
class TQuaternion;
// Объявление класса векторов
class TVector {
protected:
// Размерность вектора
int n;
// Элементы вектора
double *data;
public:
// Конструктор по умолчанию
TVector();
// Конструктор с заданным кол-вом элементов
TVector(int n);
// Конструктор копий
TVector(const TVector& rvalue);
// Оператор присваивания
TVector& operator = (const TVector& rvalue);
// Деструктор
virtual ~TVector();
// Функция получение кол-ва элементов вектора
inline int size() const { return n; }
// Функция получения индекса последнего элемента
inline int high() const { return n - 1; }
// Функция задания кол-ва элементов вектора
void resize(int n);
// Оператор доступа к элементам вектора
inline double& operator[](int i) { return data[i]; }
// Оператор константного доступа к элементам вектора
inline const double& operator[](int i) const { return data[i]; }
// Оператор - унарный минус
TVector operator - () const;
// Оператор вычитания векторов
TVector operator - (const TVector& arg) const;
// Оператор сложения векторов
TVector operator + (const TVector& arg) const;
// Оператор умножения вектора на число
TVector operator * (double arg) const;
// Оператор скалярного умножения векторов
double operator * (const TVector& arg) const;
// Оператор умножения вектора на матрицу
TVector operator * (const TMatrix& arg) const;
// Оператор умножения вектора на кватернион
TQuaternion operator * (const TQuaternion& arg) const;
// Оператор векторного умножения векторов
TVector operator ^ (const TVector& arg) const;
// Дружественная функция - оператор умножения числа на вектор
friend TVector operator * (double lvalue, const TVector& rvalue);
// Функция получения модуля вектора
double length() const;
// Функция нормирования вектора
TVector& norm();
// Поворот вектора вокруг заданной оси на заданный угол при помощи формулы Родрига
TVector rotateByRodrigFormula(double phi, const TVector& axis) const;
// Поворот вектора вокруг заданной оси на заданный угол при помощи кватерниона
TVector rotate(double phi, const TVector& axis) const;
// Поворот векора при помощи заданного кватерниона
TVector rotateByQuaternion(const TQuaternion& L) const;
};
// Объявление класса матриц
class TMatrix {
protected:
// Размерность матрицы (число строк и столбцов)
int n, m;
// Элементы матрицы
double **data;
public:
// Конструктор по умолчанию
TMatrix();
// Конструктор с заданной размерностью
TMatrix(int n, int m);
// Конструктор копий
TMatrix(const TMatrix& rvalue);
// Оператор присваивания
TMatrix& operator = (const TMatrix& rvalue);
// Деструктор
virtual ~TMatrix();
// Функция получения количества строк
inline int rowCount() const { return n; }
// Функция получения кол-ва столбцов
inline int colCount() const { return m; }
// Функция получения индекса последней строки
inline int rowHigh() const { return n-1; }
// Функция получения индекса последнего столбца
inline int colHigh() const { return m-1; }
// Функция задания размерности
void resize(int n, int m);
// Оператор доступа к элементам матрицы
inline double& operator()(int i, int j) { return data[i][j]; }
// Оператор константного доступа к элементам вектора
inline const double& operator()(int i, int j) const { return data[i][j]; }
// Оператор - унарный минус
TMatrix operator - () const;
// Оператор вычитания матриц
TMatrix operator - (const TMatrix& arg) const;
// Оператор сложения матриц
TMatrix operator + (const TMatrix& arg) const;
// Оператор умножения матрицы на число
TMatrix operator * (double arg) const;
// Оператор умножения матриц
TMatrix operator * (const TMatrix& arg) const;
// Оператор умножения матрицы на вектор
TVector operator * (const TVector& arg) const;
// Дружественная функция - оператор умножения числа на матрицу
friend TMatrix operator * (double lvalue, const TMatrix& rvalue);
// Оператор обращения матриц (метод Гаусса)
TMatrix operator ! () const throw(int);
// Функция вычисления детерминанта
double det() const;
// Функция транспонирования
TMatrix t() const;
// Функция формирования единичной матрицы
static TMatrix E(int n);
// Функция перестановки строк
TMatrix& swapRows(int i, int j);
};
class TQuaternion {
protected:
// Скалярная часть
double q0;
// Векторная часть
TVector Q;
public:
// Конструктор по умолчанию
TQuaternion();
// Конструктор по компонентам кватерниона
TQuaternion(double l0, double l1, double l2, double l3);
// Конструктор по углу поворота (рад.) и оси вращения
TQuaternion(double phi, const TVector& e);
// Конструктор копирования
TQuaternion(const TQuaternion& rvalue);
// Оператор присваивания
TQuaternion& operator = (const TQuaternion& rvalue);
// Оператор вычитания кватернионов
TQuaternion operator - (const TQuaternion& arg) const;
// Оператор сложения кватернионов
TQuaternion operator + (const TQuaternion& arg) const;
// Оператор умножения кватернионов
TQuaternion operator * (const TQuaternion& arg) const;
// Оператор умножения кватерниона на вектор
TQuaternion operator * (const TVector& arg) const;
// Оператор умножения кватерниона на число
TQuaternion operator * (double arg) const;
// Дружественная функция - оператор умножения числа на кватернион
friend TQuaternion operator * (double lvalue, const TQuaternion & rvalue);
// Оператор обращения кватерниона
TQuaternion operator ! () const;
// Доступ к скалярной части
inline double scal() const { return q0; }
// Доступ к векторной части
inline TVector vect() const { return Q; }
// Функция нормирования кватерниона
TQuaternion& norm();
// Функция получения сопряженного кватерниона
TQuaternion conj() const;
// Функция получения матрицы вращения из компонентов кватерниона
TMatrix toRotateMatrix() const;
// Производящая функция для создания кватерниона по углам Крылова
static TQuaternion fromKrylovAngles(double yaw, double pitch, double roll);
}; }
Следует обратить внимание на специальные методы: конструктор копирования – метод, вызываемый при передаче в функцию динамического объекта (по значению) и возврате его из функции, а также оператор присваивания – метод, вызываемый при присваивании переменной типа TVector, TMatrix или TQuaternion значения другой аналогичной переменной соответствующего типа. Реализация обоих этих методов должна предполагать создание копии объекта, являющегося их аргументом, но в операторе присваивания нужно ещё и удалить старое содержимое памяти, если до совершения присваивания она уже была выделена. Конструктор копирования, как и обычный конструктор, всегда имеет имя, совпадающее с именем класса, а аргумент этого конструтора (имеющий тип данного класса) должен быть объявлен константынм и перадаваться по ссылке.
Также важно отметить, что для доступа к элементам вектора используется перегруженный оператор operator [] (int) в двух модификациях – константной (с модификатором const для применения к констанатным объектам только на чтение значений) и неконстантной (для чтения и записи значений). Для доступа к элементам матрицы используется перегруженный оператор operator () (int, int) также в двух модификациях. Использование для матрицы именно оператора «круглые скобки» вызвано тем, что этот оператор поддерживает передачу ему нескольких аргументов (в частности, индексов строки и столбца матрицы), в отличие от оператора «квадратные скобки». При этом с точки зрения соблюдения принципа инкапсуляции нежелательно использовать последний оператор в «составном» виде ([i][j]), предоставляя прямой незащищённый доступ к полю с данными.
Широкое использование модификатора const является хорошим тоном в программировании, повышает надежность кода, позволяя избежать ошибок, связанных с непреднамерной модификацией объектов внутри функций.
Применение модификатора inline перед объявлением заголовка функции указывает компилятору на необходимость включения её кода непосредственно в вызывающий код. Этот модификатор позволяет оптимизировать быстродействие при наличии большого количества вызвов коротких функций (правда, за счёт некоторого проигрыша в объёме машинного кода). Большинство компиляторов могут игнорировать этот модификатор, если с точки зрения заданного кртиерия оптимизации кода такая подстановка функции окажется нецелесообразна.
Ещё один приём, направленный на повышение быстродействия (а это весьма важная характеристика моделирующего комплекса), состоит в передаче параметров со сложной структурой по ссылке (с применением модификатора const для запрета изменений таких параметров), а не по значению. В таком случае упомянутый выше конструктор копирования не вызывается, а внутрь функции попадает лишь ссылка на существующий вовне объект.
Также можно отметить применение модификатора static к методам классов TMatrix, TQuaternion. Этот модификатор делает объявленный с ним метод «классовым» или статическим – вызов такого метода возможен без создания экземпляра класса. Часто такие методы используются в качестве «производящих» или «фабричных» (как и в отмеченных случаях), осуществляя создание экземпляра класса на основе переданных параметров (формирование единичной матрицы заданного размера и кватерниона по значениям углов Крылова, см. раздел Error: Reference source not found).
Операторы «^» и «!», перегруженные в классах TVector и TMatrix соответственно, предназначены для вычисления векторного произведения векторов и обращения матриц. Следует заметить, что приоритет оператора «^» ниже приоритета логических и арифметических операторов, поэтому его использование в выражениях должно сопровождаться явным указанием приоритета вычислений при помощи круглых скобок.
Модификатор friend используется для объявления функции – не члена класса, но «дружественной» ему (внутри такой функции можно получать доступ к закрытым полям класса). В нашем примере функции с таким модификатором используются для умножения числа на вектор, матрицу и кватернион (т.к. первым операндом в этих функциях является число, то они не могут быть членами соответствующих классов, и мы вынуждены объявить эти функции «дружественными» им).
Представленные объявления классов могут быть расширены различными методами, необходимыми для решения задач моделирования (например, перестановки столбцов, получения алгебраических дополнений, решения проблемы собственных значений и т.д.). Учитывая примененный объектно-ориентированный подход, модификация и расширение методов легко могут быть выполнены при помощи наследования.
Далее приводится фрагмент файла mylinalg.cpp, в котором с комментариями представлен исходный код методов работы с памятью, таких как конструкторы, деструкторы, операторы присваивания, функции изменения размерности векторов и матриц. Также в качестве примера приводится реализация некоторых алгебраических методов соответствующих классов: сложение векторов, поворот вектора при помощи заданного кватерниона, сложение матриц, производящая функция для получения единичной матрицы заданной размерности, умножение кватерниона на другой кватернион и на вектор.
Листинг 9
#include "mylinalg.h"
#include <cstring>
#include <math.h>
namespace MyLinearAlgebra {
// Векторы
// Конструктор по умолчанию
TVector::TVector() : n(0), data(NULL) {}
// Конструктор по количеству элементов
TVector::TVector(int n) : n(0), data(NULL) {
resize(n);
}
// Конструктор копирования
TVector::TVector(const TVector& rvalue) : n(0), data(NULL) {
(*this) = rvalue;
}
// Оператор присваивания
TVector& TVector::operator = (const TVector& rvalue) {
// Если левый операнд не совпадает с правым
if (this != &rvalue) {
// Если размер левого операнда не совпадает с правым, то память выделяется заново
if (n != rvalue.n) {
// Если память уже была выделена, удалить её
if (data) { delete[] data; }
// Выделение новой памяти
data = new double[rvalue.n];
// Сохранение нового размера
n = rvalue.n;
}
// Перенос данных из правого операнда в левый
memcpy(data, rvalue.data, sizeof(double) * n);
}
// Возврат ссылки на левый операнд для возможности цепочки присваиваний
return *this;
}
// Деструктор
TVector::~TVector() {
// Если блок данных ранее был инициализирован, удаляем его
if (data) {
delete[] data;
n = 0;
data = NULL;
}
}
// Функция задания кол-ва элементов вектора
void TVector::resize(int n) {
#ifdef _DEBUG
if (n < 0)
throw 1;
#endif
// Если новый размер совпадает со старым - выходим
if (n == this->n) return;
// Новый блок памяти
double *newData = new double[n];
// Если блок данных ранее был инициализирован...
if (data) {
// Минимальный из старого и нового размера блока
int min_n = (this->n < n) ? this->n : n;
// Перенос данных из старого блока в новый
memcpy(newData, data, sizeof(double)*min_n);
// Удаление старого блока
delete[] data;
}
// Прикрепление нового блока к объекту вектора
data = newData;
// Сохранение нового размера
this->n = n;
}
// Оператор сложения векторов
TVector TVector::operator + (const TVector& arg) const {
#ifdef _DEBUG
if (n != arg.n)
throw 1;
#endif
TVector V( n );
for (int i = 0; i < n; i++)
V[i] = data[i] + arg[i];
return V;
}
// Поворот вектора при помощи заданного кватерниона
TVector TVector::rotateByQuaternion(const TQuaternion& L) const {
return (L * (*this) * L.conj()).vect();
}
// Матрицы
// Конструктор по умолчанию
TMatrix::TMatrix() : n(0), m(0), data(NULL) {}
// Конструктор с заданной размерностью
TMatrix::TMatrix(int n, int m) : n(0), m(0), data(NULL) { resize(n, m); }
// Конструктор копий
TMatrix::TMatrix(const TMatrix& rvalue) : n(0), m(0), data(NULL) {
(*this) = rvalue;
}
// Оператор присваивания
TMatrix& TMatrix::operator = (const TMatrix& rvalue) {
// Если левый операнд не совпадает с правым
if (this != &rvalue) {
// Удаление ранее выделенной памяти
this->~TMatrix();
// Выделение новой памяти по размерам правого операнда
resize(rvalue.n, rvalue.m);
// Перенос данных из правого операнда в левый построчно
for (int i = 0; i < n; i++)
memcpy(data[i], rvalue.data[i], sizeof(double)*m);
}
// Возврат ссылки на левый операнд для возможности цепочки присваиваний
return (*this);
}
// Деструктор объекта матрицы
TMatrix::~TMatrix() {
if (data) {
for (int i = 0; i < n; i++)
delete[] data[i];
delete[] data;
data = NULL;
n = m = 0;
}
}
// Функция задания размерности матрицы
void TMatrix::resize(int n, int m) {
// Кол-во строк, которые нужно перенести в новые блоки данных
int min_n = this->n < n ? this->n : n;
// Если кол-во столбцов не совпадает
if (this->m != m) {
// Кол-во столбцов, которые нужно перенести в новые блоки данных
int min_m = this->m < m ? this->m : m;
// Цикл построчного переноса данных в новые блоки
for (int i = 0; i < min_n; i++) {
// Создание нового блока-строки
double *newDataRow = new double[m];
// Перенос данных в новый блок-строку
memcpy(newDataRow, data[i], sizeof(double)*min_m);
// Удаление старого блока строки на этом месте
delete[] data[i];
// Прикрепление нового блока-строки на старое место
data[i] = newDataRow;
}
// Сохранение нового размера
this->m = m;
}
// Если кол-во строк не совпадает
if (this->n != n) {
// Создание нового блока-контейнера
double **newData = new double*[n];
// Перенос содержимого старого контейнера в новый
memcpy(newData, data, sizeof(double*)*min_n);
// Удаление лишних строк из старого контейнера
for (int i = n; i < this->n; i++) { delete[] data[i]; }
// Удаление старого контейнера
if (data) { delete[] data; }
// Создание недостающих строк в новом контейнере
for (int i = this->n; i < n; i++) { newData[i] = new double[m]; }
// Привязка старого контейнера к новому
data = newData;
this->n = n;
}
}
// Оператор сложения матриц
TMatrix TMatrix::operator + (const TMatrix& arg) const {
#ifdef _DEBUG
if ((n != arg.n) || (m != arg.m))
throw 1;
#endif
TMatrix M(n, m);
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
M(i,j) = data[i][j] + arg(i,j);
return M;
}
// Производящая функция для формирования единичной матрицы
TMatrix TMatrix::E(int n) {
TMatrix E(n,n);
for (int i = 0; i < n; i++) {
E(i,i) = 1;
for (int j = i+1; j < n; j++) { E(i,j) = E(j,i) = 0; }
}
return E;
}
// Кватернионы
// Конструктор по умолчанию
TQuaternion::TQuaternion() : q0(0), Q(3) {}
// Конструктор по компонентам кватерниона
TQuaternion::TQuaternion(double q0, double q1, double q2, double q3)
: q0(q0), Q(3) {
Q[0] = q1; Q[1] = q2; Q[2] = q3;
}
// Конструктор по углу поворота (радианы) и оси вращения
TQuaternion::TQuaternion(double phi, const TVector& e) {
q0 = cos(phi/2);
Q = e;
Q.norm();
Q = Q * sin(phi/2);
}
// Конструктор копирования
TQuaternion::TQuaternion(const TQuaternion& rvalue) { *this = rvalue; }
// Оператор присваивания
TQuaternion& TQuaternion::operator = (const TQuaternion& rvalue) {
if (this != &rvalue) {
q0 = rvalue.q0;
Q = rvalue.Q;
}
return *this;
}
// Оператор умножения кватернионов
TQuaternion TQuaternion::operator * (const TQuaternion& arg) const {
TQuaternion L;
L.q0 = q0 * arg.q0 – Q * arg.Q;
L.Q = Q * arg.q0 + arg.Q * q0 + (Q^arg.Q);
return L;
}
// Оператор умножения кватерниона на вектор
TQuaternion TQuaternion::operator * (const TVector& arg) const {
TQuaternion L(0, arg[0], arg[1], arg[2]);
return (*this) * L;
} … }
В представленном примере методы выделения, изменения размера и освобождения памяти реализованы непосредственно с использованием операторов new и delete[], причём код метода установки размерности матриц void TMatrix::resize(int, int) оптимизирован для ситуации, при которой количество строк в матрице увеличивается в процессе выполнения вычислений. Обработка организована так, что при изменении количества строк заново выделяется только память под указатели на строки, сами же строки остаются в памяти неизменными. Это существенно повышает быстродействие и снижает ресурсоёмкость вычислений.
Следует обратить внимание на использование в реализации директивы условной компиляции #ifdef … #endif. Её назначение здесь аналогично описанному в примере для Delphi – при компиляции в режиме отладки обеспечить проверку корректности параметров и предоставить разработчику информацию о типе возникшей ошибки (см. раздел 3.1.1). Для контроля режима компиляции используется макроопределение _DEBUG, которое в режиме отладки принимает значение «истина» (макроопределение с таким именем используется во многих средах разработки). Для информирования разработчика (или внешних автоматизированных средств тестирования) о типе ошибки внутри функции может генерироваться объект исключения – целое число, значение которого соответствует коду ошибки (вообще же объект исключения может быть любого типа).
Ниже представлен фрагмент программного кода, в котором демонстрируется работа с объектами описанных классов.
Листинг 10
// Указание на использование пространства имён
using MyLinearAlgebra;
// Объявление переменных типа матрица с заданием размеров
TMatrix A(3, 3), B(3, 3), C;
// Инициализация элементов матриц
A(0,0) := 1; A(0,1) := 2; …
B(0,0) := 10; A(0,1) := 20; …
// Сложение матриц А и В
C = A + B;
// Обращение матрицы А
B = !A;
// Умножение матриц А и В
A = A * B;
// Объявление переменных типа вектор с заданием размеров
TVector X(3), e(3), Z;
// Инициализация элементов векторов
X[0] := 1; X[1] := 2; X[2] := 3;
e[0] := 5; e[1] := 0; e[2] := 0;
// Умножение матрицы на вектор
Z = C * X;
// Создание объекта кватерниона по углу и оси вращения
TQuaternion Q(M_PI / 2, e);
// Поворот вектора при помощи кватерниона
Z = X.rotateByQuaternion(Q);
