Как воплотить ООП в C: подробное руководство по созданию калькулятора

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Разработчики, интересующиеся объектно-ориентированным программированием на языке C
  • Студенты и начинающие программисты, желающие углубить свои знания в программировании без использования стандартных ООП-языков
  • Профессионалы, стремящиеся улучшить свои навыки проектирования и архитектуры программного обеспечения

    Однажды мой студент заявил: "ООП невозможно в чистом С, это же нонсенс!" — и я решил доказать обратное. Реализация калькулятора с принципами объектно-ориентированного программирования на С — не просто академическое упражнение, а мощный инструмент понимания сути ООП. Когда вы создаёте "классы" без языковой поддержки классов, вы проникаете в саму философию объектной парадигмы, получая контроль над каждым аспектом инкапсуляции, наследования и полиморфизма. Готовы погрузиться в мир, где указатели на функции становятся методами, а структуры превращаются в полноценные объекты? 🧮

Хотите углубить понимание объектно-ориентированного программирования и применить эти знания в реальных проектах? Курс Java-разработки от Skypro даст вам не только фундаментальное понимание ООП, но и практические навыки его применения в современной Java-разработке. Если вы способны реализовать ООП в С, представьте, каких высот достигнете с языком, где объектная парадигма встроена на уровне синтаксиса! Трансформируйте свое мышление из процедурного в объектное под руководством опытных разработчиков.

Основы ООП в языке C: имитация классов и методов

Язык C не предоставляет встроенных механизмов для объектно-ориентированного программирования, но это не означает, что мы не можем применять принципы ООП в C-проектах. Фактически, понимание того, как имитировать ООП в C, часто дает более глубокое понимание самих принципов объектно-ориентированного программирования. 🔍

Начнем с основных концепций ООП и их реализации в C:

Концепция ООП Реализация в C Пример кода
Класс Структура (struct) struct Calculator { ... };
Метод Указатель на функцию double (*add)(double, double);
Инкапсуляция Модули и файлы .h/.c static переменные, функции в .c
Полиморфизм Указатели на функции в структурах struct { void (*operation)(void*); };
Наследование Вложенные структуры struct ScientificCalc { struct Calculator base; ... };

Для имитации классов в C используются структуры, которые содержат данные (поля класса) и указатели на функции (методы класса). Вот пример определения "класса" калькулятора:

c
Скопировать код
typedef struct Calculator {
// Данные (свойства)
double memory;

// Методы (указатели на функции)
double (*add)(struct Calculator*, double, double);
double (*subtract)(struct Calculator*, double, double);
double (*multiply)(struct Calculator*, double, double);
double (*divide)(struct Calculator*, double, double);

// Метод для управления памятью
void (*store)(struct Calculator*, double);
double (*recall)(struct Calculator*);
void (*clear)(struct Calculator*);
} Calculator;

Для создания "методов" класса определим функции, которые принимают указатель на экземпляр структуры в качестве первого аргумента (аналог this в C++ или других ООП языках):

c
Скопировать код
double calculator_add(Calculator* self, double a, double b) {
return a + b;
}

void calculator_store(Calculator* self, double value) {
self->memory = value;
}

double calculator_recall(Calculator* self) {
return self->memory;
}

Затем создадим "конструктор" — функцию, которая инициализирует новый экземпляр структуры:

c
Скопировать код
Calculator* calculator_new() {
Calculator* calc = (Calculator*)malloc(sizeof(Calculator));
if (calc == NULL) {
return NULL; // Проверка на ошибку выделения памяти
}

// Инициализация данных
calc->memory = 0.0;

// Привязка методов
calc->add = calculator_add;
calc->subtract = calculator_subtract;
calc->multiply = calculator_multiply;
calc->divide = calculator_divide;
calc->store = calculator_store;
calc->recall = calculator_recall;
calc->clear = calculator_clear;

return calc;
}

И "деструктор", чтобы корректно освобождать ресурсы:

c
Скопировать код
void calculator_free(Calculator* calc) {
if (calc != NULL) {
free(calc);
}
}

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

Пошаговый план для смены профессии

Проектирование объектной модели калькулятора на C

Александр Петров, ведущий разработчик систем встроенного ПО

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

Вместо этого я применил объектную модель даже на С. Структурировав код вокруг "объектов" операций и "класса" калькулятора, я создал гибкое решение, где каждая математическая операция стала отдельным модулем с унифицированным интерфейсом. Когда позже потребовалось добавить тригонометрические функции, я просто расширил иерархию, не трогая базовый код калькулятора. Такой подход сэкономил мне недели отладки и позволил безболезненно масштабировать проект.

При проектировании объектной модели калькулятора на языке C необходимо определить ключевые абстракции, их взаимодействие и иерархию. Основная цель — создать модель, которая будет гибкой, расширяемой и легко поддерживаемой. 📐

Рассмотрим главные компоненты нашей объектной модели:

  • Calculator — основной класс, представляющий сам калькулятор
  • Operation — абстрактный класс для представления математических операций
  • Memory — класс для управления памятью калькулятора
  • Display — класс для вывода результатов и взаимодействия с пользователем

Определим структуру для базового класса операций:

c
Скопировать код
typedef struct Operation {
char symbol; // Символ операции (+, -, *, /)
double (*execute)(double, double); // Указатель на функцию выполнения
char* (*get_name)(void); // Получение названия операции
} Operation;

Для каждой конкретной операции мы создадим отдельную структуру, которая будет "наследоваться" от базовой:

c
Скопировать код
// "Реализации" операций
double add_execute(double a, double b) { return a + b; }
char* add_get_name(void) { return "Addition"; }

double subtract_execute(double a, double b) { return a – b; }
char* subtract_get_name(void) { return "Subtraction"; }

// Создание конкретных экземпляров операций
Operation add_operation = { '+', add_execute, add_get_name };
Operation subtract_operation = { '-', subtract_execute, subtract_get_name };

Теперь определим основную структуру калькулятора, которая будет использовать эти операции:

c
Скопировать код
typedef struct Calculator {
// Состояние
double current_value;
double memory;

// Операции
Operation* operations[10]; // Массив доступных операций
int num_operations;

// Методы
double (*perform_operation)(struct Calculator*, char, double);
void (*store_memory)(struct Calculator*, double);
double (*recall_memory)(struct Calculator*);
void (*clear_memory)(struct Calculator*);
void (*display_result)(struct Calculator*);
} Calculator;

Реализация метода выполнения операции может выглядеть так:

c
Скопировать код
double calculator_perform_operation(Calculator* self, char op_symbol, double operand) {
for (int i = 0; i < self->num_operations; i++) {
if (self->operations[i]->symbol == op_symbol) {
self->current_value = self->operations[i]->execute(self->current_value, operand);
return self->current_value;
}
}
// Операция не найдена
return self->current_value;
}

Важным аспектом объектной модели является её расширяемость. Например, мы можем создать "наследника" нашего базового калькулятора — научный калькулятор:

c
Скопировать код
typedef struct ScientificCalculator {
Calculator base; // "Наследование" базового калькулятора

// Дополнительные методы
double (*sin)(struct ScientificCalculator*, double);
double (*cos)(struct ScientificCalculator*, double);
double (*tan)(struct ScientificCalculator*, double);
double (*log)(struct ScientificCalculator*, double);
} ScientificCalculator;

Такая модель позволяет нам добавлять новые функциональные возможности без изменения существующего кода, что соответствует принципу открытости/закрытости из SOLID.

Диаграмма ниже иллюстрирует взаимосвязь между компонентами нашей объектной модели:

Класс Ответственности Взаимодействует с
Calculator Основная логика калькулятора, хранение состояния Operation, Memory, Display
Operation Выполнение математических операций Calculator
Memory Хранение и управление памятью калькулятора Calculator
Display Отображение результатов и взаимодействие с пользователем Calculator
ScientificCalculator Расширенные математические операции Calculator, Operation

Проектирование объектной модели — это фундамент успешной реализации калькулятора с принципами ООП. Хорошо продуманная архитектура позволяет легко расширять функциональность и поддерживать код в долгосрочной перспективе.

Реализация математических операций через методы объектов

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

Начнем с определения интерфейса для математических операций:

c
Скопировать код
// operation.h
#ifndef OPERATION_H
#define OPERATION_H

typedef struct {
char symbol; // Символ операции
const char* name; // Название операции
double (*execute)(double, double); // Функция выполнения
} Operation;

// Конструктор для создания операции
Operation* operation_create(char symbol, const char* name, double (*execute)(double, double));

// Деструктор
void operation_destroy(Operation* op);

// Выполнить операцию
double operation_execute(Operation* op, double a, double b);

// Получить символ операции
char operation_get_symbol(Operation* op);

// Получить название операции
const char* operation_get_name(Operation* op);

#endif /* OPERATION_H */

Теперь реализуем эти функции:

c
Скопировать код
// operation.c
#include "operation.h"
#include <stdlib.h>

Operation* operation_create(char symbol, const char* name, double (*execute)(double, double)) {
Operation* op = (Operation*)malloc(sizeof(Operation));
if (op) {
op->symbol = symbol;
op->name = name;
op->execute = execute;
}
return op;
}

void operation_destroy(Operation* op) {
free(op);
}

double operation_execute(Operation* op, double a, double b) {
return op->execute(a, b);
}

char operation_get_symbol(Operation* op) {
return op->symbol;
}

const char* operation_get_name(Operation* op) {
return op->name;
}

Теперь определим конкретные математические операции:

c
Скопировать код
// math_operations.c
#include "operation.h"
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

// Базовые арифметические операции
static double add_function(double a, double b) { return a + b; }
static double subtract_function(double a, double b) { return a – b; }
static double multiply_function(double a, double b) { return a * b; }
static double divide_function(double a, double b) {
if (b == 0) {
fprintf(stderr, "Error: Division by zero\n");
return 0;
}
return a / b;
}

// Продвинутые математические операции
static double power_function(double a, double b) { return pow(a, b); }
static double modulo_function(double a, double b) {
if (b == 0) {
fprintf(stderr, "Error: Modulo by zero\n");
return 0;
}
return fmod(a, b);
}

// Создание операций
Operation* create_add_operation() {
return operation_create('+', "Addition", add_function);
}

Operation* create_subtract_operation() {
return operation_create('-', "Subtraction", subtract_function);
}

Operation* create_multiply_operation() {
return operation_create('*', "Multiplication", multiply_function);
}

Operation* create_divide_operation() {
return operation_create('/', "Division", divide_function);
}

Operation* create_power_operation() {
return operation_create('^', "Power", power_function);
}

Operation* create_modulo_operation() {
return operation_create('%', "Modulo", modulo_function);
}

Теперь интегрируем эти операции в наш калькулятор:

c
Скопировать код
// calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H

#include "operation.h"

typedef struct {
double current_value; // Текущее значение на дисплее
double memory_value; // Значение в памяти
Operation** operations; // Массив доступных операций
int num_operations; // Количество операций
int max_operations; // Максимально возможное количество операций
} Calculator;

// Конструктор
Calculator* calculator_create(int max_operations);

// Деструктор
void calculator_destroy(Calculator* calc);

// Добавить операцию
int calculator_add_operation(Calculator* calc, Operation* op);

// Выполнить операцию по символу
double calculator_perform_operation(Calculator* calc, char op_symbol, double operand);

// Методы работы с памятью
void calculator_store_memory(Calculator* calc);
double calculator_recall_memory(Calculator* calc);
void calculator_clear_memory(Calculator* calc);

// Сброс текущего значения
void calculator_clear(Calculator* calc);

// Получить текущее значение
double calculator_get_value(Calculator* calc);

#endif /* CALCULATOR_H */

Реализация калькулятора:

c
Скопировать код
// calculator.c
#include "calculator.h"
#include <stdlib.h>
#include <stdio.h>

Calculator* calculator_create(int max_operations) {
Calculator* calc = (Calculator*)malloc(sizeof(Calculator));
if (!calc) return NULL;

calc->operations = (Operation**)malloc(max_operations * sizeof(Operation*));
if (!calc->operations) {
free(calc);
return NULL;
}

calc->current_value = 0.0;
calc->memory_value = 0.0;
calc->num_operations = 0;
calc->max_operations = max_operations;

return calc;
}

void calculator_destroy(Calculator* calc) {
if (!calc) return;

// Освобождаем память для операций
if (calc->operations) {
for (int i = 0; i < calc->num_operations; i++) {
operation_destroy(calc->operations[i]);
}
free(calc->operations);
}

free(calc);
}

int calculator_add_operation(Calculator* calc, Operation* op) {
if (!calc || !op) return 0;

if (calc->num_operations >= calc->max_operations) {
// Расширение массива операций при необходимости
int new_max = calc->max_operations * 2;
Operation** new_ops = (Operation**)realloc(calc->operations, new_max * sizeof(Operation*));
if (!new_ops) return 0;

calc->operations = new_ops;
calc->max_operations = new_max;
}

calc->operations[calc->num_operations++] = op;
return 1;
}

double calculator_perform_operation(Calculator* calc, char op_symbol, double operand) {
if (!calc) return 0.0;

for (int i = 0; i < calc->num_operations; i++) {
if (operation_get_symbol(calc->operations[i]) == op_symbol) {
calc->current_value = operation_execute(calc->operations[i], calc->current_value, operand);
return calc->current_value;
}
}

fprintf(stderr, "Error: Operation '%c' not found\n", op_symbol);
return calc->current_value;
}

void calculator_store_memory(Calculator* calc) {
if (calc) calc->memory_value = calc->current_value;
}

double calculator_recall_memory(Calculator* calc) {
if (!calc) return 0.0;
calc->current_value = calc->memory_value;
return calc->current_value;
}

void calculator_clear_memory(Calculator* calc) {
if (calc) calc->memory_value = 0.0;
}

void calculator_clear(Calculator* calc) {
if (calc) calc->current_value = 0.0;
}

double calculator_get_value(Calculator* calc) {
return calc ? calc->current_value : 0.0;
}

Такая реализация математических операций через методы объектов обеспечивает несколько преимуществ:

  • Модульность: каждая операция инкапсулирована в своем объекте
  • Расширяемость: легко добавлять новые операции без изменения существующего кода
  • Поддерживаемость: отдельные операции могут быть изменены или исправлены независимо
  • Тестируемость: операции можно тестировать изолированно

Этот подход также демонстрирует полиморфизм в действии: калькулятор взаимодействует с операциями через единый интерфейс, не заботясь о конкретных деталях реализации каждой операции.

Создание пользовательского интерфейса калькулятора

Иван Соколов, архитектор программного обеспечения

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

Когда дело дошло до добавления новых форматов вывода данных, система начала рушиться. Я предложил переработать интерфейс с использованием объектно-ориентированного подхода. Мы создали абстракцию "дисплея" с единым интерфейсом и разными реализациями: для консоли, для файлового вывода и для API.

Преобразование заняло всего неделю, но после этого добавление нового формата вывода требовало написания лишь одного нового "класса", а не модификации всей системы. Команда была поражена, насколько чище и понятнее стал код даже без использования C++ или другого ООП-языка.

Создание пользовательского интерфейса для калькулятора — это важная часть проекта, которая должна обеспечить удобное взаимодействие пользователя с логикой приложения. В объектно-ориентированном подходе интерфейс также должен быть спроектирован как отдельный "класс" или набор классов. 🖥️

Начнем с определения интерфейса для нашего дисплея калькулятора:

c
Скопировать код
// display.h
#ifndef DISPLAY_H
#define DISPLAY_H

typedef struct {
void (*show_value)(double value); // Метод отображения значения
void (*show_operation)(char op, double value); // Метод отображения операции
void (*show_error)(const char* message); // Метод отображения ошибки
void (*clear)(void); // Метод очистки дисплея
} Display;

// Конструктор
Display* display_create();

// Деструктор
void display_destroy(Display* display);

// Функции-обертки для методов
void display_show_value(Display* display, double value);
void display_show_operation(Display* display, char op, double value);
void display_show_error(Display* display, const char* message);
void display_clear(Display* display);

#endif /* DISPLAY_H */

Реализация дисплея для консольного интерфейса:

c
Скопировать код
// console_display.c
#include "display.h"
#include <stdio.h>
#include <stdlib.h>

// Реализации методов для консольного дисплея
static void console_show_value(double value) {
printf("Result: %g\n", value);
}

static void console_show_operation(char op, double value) {
printf("Operation: %c %g\n", op, value);
}

static void console_show_error(const char* message) {
fprintf(stderr, "Error: %s\n", message);
}

static void console_clear(void) {
// Для консоли просто выводим разделитель
printf("\n---------------------------------\n");
}

Display* display_create() {
Display* display = (Display*)malloc(sizeof(Display));
if (display) {
display->show_value = console_show_value;
display->show_operation = console_show_operation;
display->show_error = console_show_error;
display->clear = console_clear;
}
return display;
}

void display_destroy(Display* display) {
free(display);
}

void display_show_value(Display* display, double value) {
if (display && display->show_value) {
display->show_value(value);
}
}

void display_show_operation(Display* display, char op, double value) {
if (display && display->show_operation) {
display->show_operation(op, value);
}
}

void display_show_error(Display* display, const char* message) {
if (display && display->show_error) {
display->show_error(message);
}
}

void display_clear(Display* display) {
if (display && display->clear) {
display->clear();
}
}

Теперь создадим класс для пользовательского интерфейса калькулятора, который будет связывать дисплей с логикой калькулятора:

c
Скопировать код
// calculator_ui.h
#ifndef CALCULATOR_UI_H
#define CALCULATOR_UI_H

#include "calculator.h"
#include "display.h"

typedef struct {
Calculator* calculator; // Ссылка на калькулятор
Display* display; // Ссылка на дисплей
} CalculatorUI;

// Конструктор
CalculatorUI* calculator_ui_create(Calculator* calculator, Display* display);

// Деструктор
void calculator_ui_destroy(CalculatorUI* ui);

// Запуск интерактивного режима
void calculator_ui_run(CalculatorUI* ui);

// Обработка одной команды
int calculator_ui_process_command(CalculatorUI* ui, const char* command);

#endif /* CALCULATOR_UI_H */

Реализация пользовательского интерфейса:

c
Скопировать код
// calculator_ui.c
#include "calculator_ui.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

CalculatorUI* calculator_ui_create(Calculator* calculator, Display* display) {
if (!calculator || !display) return NULL;

CalculatorUI* ui = (CalculatorUI*)malloc(sizeof(CalculatorUI));
if (ui) {
ui->calculator = calculator;
ui->display = display;
}
return ui;
}

void calculator_ui_destroy(CalculatorUI* ui) {
// Не освобождаем calculator и display, так как они могут использоваться где-то еще
free(ui);
}

int calculator_ui_process_command(CalculatorUI* ui, const char* command) {
if (!ui || !command) return 0;

// Пропускаем начальные пробелы
while (*command && isspace(*command)) command++;

if (!*command) return 1; // Пустая команда

char cmd = *command++;

// Пропускаем пробелы после команды
while (*command && isspace(*command)) command++;

double value = 0.0;

switch (cmd) {
case '+': case '-': case '*': case '/': case '^': case '%':
// Математическая операция
if (*command) {
value = atof(command);
} else {
display_show_error(ui->display, "No operand provided");
return 1;
}

calculator_perform_operation(ui->calculator, cmd, value);
display_show_value(ui->display, calculator_get_value(ui->calculator));
break;

case 'c': case 'C':
// Очистка текущего значения
calculator_clear(ui->calculator);
display_show_value(ui->display, calculator_get_value(ui->calculator));
break;

case 'm': case 'M':
// Сохранение в память
calculator_store_memory(ui->calculator);
display_show_value(ui->display, calculator_get_value(ui->calculator));
break;

case 'r': case 'R':
// Вызов из памяти
calculator_recall_memory(ui->calculator);
display_show_value(ui->display, calculator_get_value(ui->calculator));
break;

case 'x': case 'X': case 'q': case 'Q':
// Выход
return 0;

case 'h': case 'H': case '?':
// Справка
printf("Calculator Commands:\n");
printf(" +/-/*/^/%% <value> – Perform operation\n");
printf(" c – Clear current value\n");
printf(" m – Store in memory\n");
printf(" r – Recall from memory\n");
printf(" h or ? – Show this help\n");
printf(" q or x – Exit\n");
break;

default:
display_show_error(ui->display, "Unknown command");
break;
}

return 1; // Продолжаем работу
}

void calculator_ui_run(CalculatorUI* ui) {
if (!ui) return;

char buffer[256];
int running = 1;

display_clear(ui->display);
display_show_value(ui->display, calculator_get_value(ui->calculator));

while (running) {
printf("> ");
if (!fgets(buffer, sizeof(buffer), stdin)) break;

running = calculator_ui_process_command(ui, buffer);
}

display_clear(ui->display);
printf("Calculator terminated.\n");
}

Главная функция для запуска программы:

c
Скопировать код
// main.c
#include "calculator.h"
#include "display.h"
#include "calculator_ui.h"
#include "operation.h"
#include <stdio.h>

// Функции создания операций (из math_operations.c)
extern Operation* create_add_operation();
extern Operation* create_subtract_operation();
extern Operation* create_multiply_operation();
extern Operation* create_divide_operation();
extern Operation* create_power_operation();
extern Operation* create_modulo_operation();

int main() {
// Создаем калькулятор
Calculator* calc = calculator_create(10);
if (!calc) {
fprintf(stderr, "Failed to create calculator\n");
return 1;
}

// Добавляем операции
calculator_add_operation(calc, create_add_operation());
calculator_add_operation(calc, create_subtract_operation());
calculator_add_operation(calc, create_multiply_operation());
calculator_add_operation(calc, create_divide_operation());
calculator_add_operation(calc, create_power_operation());
calculator_add_operation(calc, create_modulo_operation());

// Создаем дисплей
Display* display = display_create();
if (!display) {
fprintf(stderr, "Failed to create display\n");
calculator_destroy(calc);
return 1;
}

// Создаем UI
CalculatorUI* ui = calculator_ui_create(calc, display);
if (!ui) {
fprintf(stderr, "Failed to create UI\n");
display_destroy(display);
calculator_destroy(calc);
return 1;
}

// Запускаем интерактивный режим
calculator_ui_run(ui);

// Освобождаем ресурсы
calculator_ui_destroy(ui);
display_destroy(display);
calculator_destroy(calc);

return 0;
}

Этот пользовательский интерфейс демонстрирует несколько ООП-принципов:

  • Инкапсуляция: логика интерфейса отделена от логики калькулятора и дисплея
  • Абстракция: дисплей представлен как абстрактный интерфейс, который может иметь разные реализации
  • Композиция: UI содержит ссылки на калькулятор и дисплей, но не владеет ими

Такой подход позволяет легко расширять или изменять интерфейс без влияния на остальные компоненты системы. Например, мы могли бы создать графический интерфейс, реализовав новый класс дисплея, без изменения логики калькулятора.

Тестирование и отладка ООП-калькулятора на C

Тестирование и отладка объектно-ориентированного кода имеет свои особенности, особенно когда речь идет о языке C, где ООП-принципы реализуются вручную. Правильный подход к тестированию гарантирует надежность и качество приложения. 🔍

Рассмотрим несколько уровней тестирования нашего калькулятора:

Уровень тестирования Что тестируем Инструменты
Модульное (юнит-тесты) Отдельные классы (структуры) и их методы Unity, Check, CUnit
Интеграционное Взаимодействие между компонентами Собственные тестовые программы
Системное Приложение целиком Сценарии использования, ввод/вывод
Отладка Поиск и исправление ошибок GDB, Valgrind, AddressSanitizer

Начнем с написания модульных тестов для нашего класса Operation, используя фреймворк Unity:

c
Скопировать код
// test_operation.c
#include "unity.h"
#include "operation.h"
#include <stdlib.h>

static Operation* op = NULL;
static double add_func(double a, double b) { return a + b; }

void setUp(void) {
// Создание тестового объекта перед каждым тестом
op = operation_create('+', "Addition", add_func);
}

void tearDown(void) {
// Освобождение ресурсов после каждого теста
operation_destroy(op);
op = NULL;
}

void test_operation_create(void) {
TEST_ASSERT_NOT_NULL(op);
TEST_ASSERT_EQUAL_CHAR('+', operation_get_symbol(op));
TEST_ASSERT_EQUAL_STRING("Addition", operation_get_name(op));
}

void test_operation_execute(void) {
double result = operation_execute(op, 5.0, 3.0);
TEST_ASSERT_EQUAL_FLOAT(8.0, result);

result = operation_execute(op, -1.0, 1.0);
TEST_ASSERT_EQUAL_FLOAT(0.0, result);

result = operation_execute(op, 0.1, 0.2);
TEST_ASSERT_FLOAT_WITHIN(0.0001, 0.3, result);
}

// Тест на передачу NULL-указателей
void test_operation_null_safety(void) {
Operation* null_op = NULL;

// Эти вызовы не должны вызывать сегментацию
TEST_ASSERT_EQUAL_FLOAT(0.0, operation_execute(null_op, 1.0, 2.0));
TEST_ASSERT_EQUAL_CHAR('\0', operation_get_symbol(null_op));
TEST_ASSERT_NULL(operation_get_name(null_op));
}

int main(void) {
UNITY_BEGIN();
RUN_TEST(test_operation_create);
RUN_TEST(test_operation_execute);
RUN_TEST(test_operation_null_safety);
return UNITY_END();
}

Теперь напишем интеграционный тест для взаимодействия калькулятора с операциями:

c
Скопировать код
// test_calculator_integration.c
#include "unity.h"
#include "calculator.h"
#include "operation.h"

// Функции для операций
static double test_add(double a, double b) { return a + b; }
static double test_subtract(double a, double b) { return a – b; }

// Глобальные переменные для тестов
static Calculator* calc = NULL;
static Operation* add_op = NULL;
static Operation* subtract_op = NULL;

void setUp(void) {
calc = calculator_create(5);
add_op = operation_create('+', "Add", test_add);
subtract_op = operation_create('-', "Subtract", test_subtract);

calculator_add_operation(calc, add_op);
calculator_add_operation(calc, subtract_op);
}

void tearDown(void) {
calculator_destroy(calc);
// Операции освобождаются в calculator_destroy
calc = NULL;
add_op = NULL;
subtract_op = NULL;
}

void test_calculator_operations(void) {
// Начальное значение должно быть 0
TEST_ASSERT_EQUAL_FLOAT(0.0, calculator_get_value(calc));

// Тест сложения
calculator_perform_operation(calc, '+', 5.0);
TEST_ASSERT_EQUAL_FLOAT(5.0, calculator_get_value(calc));

// Тест вычитания после сложения
calculator_perform_operation(calc, '-', 3.0);
TEST_ASSERT_EQUAL_FLOAT(2.0, calculator_get_value(calc));
}

void test_calculator_memory(void) {
// Установим значение
calculator_perform_operation(calc, '+', 10.0);

// Сохраним в память
calculator_store_memory(calc);

// Изменим текущее значение
calculator_perform_operation(calc, '+', 5.0);
TEST_ASSERT_EQUAL_FLOAT(15.0, calculator_get_value(calc));

// Очистим калькулятор
calculator_clear(calc);
TEST_ASSERT_EQUAL_FLOAT(0.0, calculator_get_value(calc));

// Восстановим из памяти
calculator_recall_memory(calc);
TEST_ASSERT_EQUAL_FLOAT(10.0, calculator_get_value(calc));

// Очистим память
calculator_clear_memory(calc);
calculator_recall_memory(calc);
TEST_ASSERT_EQUAL_FLOAT(0.0, calculator_get_value(calc));
}

int main(void) {
UNITY_BEGIN();
RUN_TEST(test_calculator_operations);
RUN_TEST(test_calculator_memory);
return UNITY_END();
}

Для отладки нашего калькулятора с ООП-подходом, особенно важно отслеживать утечки памяти и правильное использование указателей. Вот пример использования Valgrind для этой цели:

Bash
Скопировать код
# Компиляция с отладочной информацией
gcc -g -o calculator main.c calculator.c operation.c display.c calculator_ui.c

# Запуск Valgrind для поиска утечек памяти
valgrind --leak-check=full --show-leak-kinds=all ./calculator

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

Bash
Скопировать код
# Запуск GDB с нашим приложением
gdb ./calculator

# Установка точки останова в функции обработки операции
(gdb) break calculator_perform_operation
(gdb) run

# Просмотр указателей на функции
(gdb) print calc->operations[0]->execute
$1 = (double (*)(double, double)) 0x4012a0 <add_function>

# Проверка значения указателя
(gdb) p *calc->operations[0]
$2 = {symbol = 43 '+', name = 0x404040 "Addition", execute = 0x4012a0 <add_function>}

Важные аспекты при тестировании и отладке ООП-кода на C:

  • Проверка правильной инициализации объектов и их деструкции
  • Тестирование граничных случаев, особенно связанных с указателями (NULL-указатели)
  • Проверка на утечки памяти при создании и уничтожении объектов
  • Тестирование полиморфного поведения через указатели на функции
  • Изоляция компонентов для модульного тестирования с помощью техники моков

Для сложных систем рекомендуется создать тестовую среду, которая имитирует сценарии использования калькулятора:

c
Скопировать код
// test_calculator_scenarios.c
void test_complex_calculation_scenario(void) {
// Создаем калькулятор со всеми операциями
Calculator* calc = setup_full_calculator();

// Сценарий: (5 + 3) * 2 – 4
calculator_clear(calc);
calculator_perform_operation(calc, '+', 5.0); // 0 + 5 = 5
calculator_perform_operation(calc, '+', 3.0); // 5 + 3 = 8
calculator_perform_operation(calc, '*', 2.0); // 8 * 2 = 16
calculator_perform_operation(calc, '-', 4.0); // 16 – 4 = 12

TEST_ASSERT_EQUAL_FLOAT(12.0, calculator_get_value(calc));

// Сценарий с памятью: запомнить результат, очистить, восстановить
calculator_store_memory(calc);
calculator_clear(calc);
TEST_ASSERT_EQUAL_FLOAT(0.0, calculator_get_value(calc));
calculator_recall_memory(calc);
TEST_ASSERT_EQUAL_FLOAT(12.0, calculator_get_value(calc));

calculator_destroy(calc);
}

Автоматизация тестирования через скрипт сборки (например, с использованием Makefile) поможет регулярно запускать тесты при внесении изменений в код:

makefile
Скопировать код
# Makefile для калькулятора
CC = gcc
CFLAGS = -Wall -Wextra -g
LDFLAGS = -lm
TEST_FLAGS = -lunity

SRC = calculator.c operation.c display.c calculator_ui.c
OBJ = $(SRC:.c=.o)
TEST_SRC = test_operation.c test_calculator_integration.c test_calculator_scenarios.c
TEST_OBJ = $(TEST_SRC:.c=.o)

all: calculator tests

calculator: main.c $(OBJ)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

tests: $(TEST_OBJ) $(OBJ)
$(CC) $(CFLAGS) -o test_operation test_operation.c operation.o $(TEST_FLAGS)
$(CC) $(CFLAGS) -o test_calculator test_calculator_integration.c calculator.o operation.o $(TEST_FLAGS)
$

**Читайте также**
- [Программирование микроконтроллеров: от первых шагов до умных устройств](/profession/programmirovanie-mikrokontrollerov-dlya-nachinayushih/)
- [Языки программирования для Telegram ботов](/javascript/yazyki-programmirovaniya-dlya-telegram-botov/)
- [Основы ООП на Python для начинающих](/python/osnovy-oop-na-python-dlya-nachinayushih/)
- [История ООП: когда и зачем появилось?](/java/istoriya-oop-kogda-i-zachem-poyavilos/)
- [Примеры ООП в реальных проектах на Python](/python/primery-oop-v-realnyh-proektah-na-python/)
- [Практические задания по ООП на Java](/java/prakticheskie-zadaniya-po-oop-na-java/)
- [ООП: разбираем абстракцию](/java/oop-razbiraem-abstrakciyu/)
- [Основные понятия ООП: объекты, классы, атрибуты и методы](/python/osnovnye-ponyatiya-oop-obuekty-klassy-atributy-i-metody/)
- [Интерпретируемые и компилируемые языки программирования](/python/interpretiruemye-i-kompiliruemye-yazyki-programmirovaniya/)
- [Парадигмы программирования: как выбрать оптимальный подход к коду](/profession/osnovnye-paradigmy-programmirovaniya/)

Проверь как ты усвоил материалы статьи
Пройди тест и узнай насколько ты лучше других читателей
Какой парадигмой программирования является ООП?
1 / 5

Загрузка...