Как воплотить ООП в C: подробное руководство по созданию калькулятора
Для кого эта статья:
- Разработчики, интересующиеся объектно-ориентированным программированием на языке 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 используются структуры, которые содержат данные (поля класса) и указатели на функции (методы класса). Вот пример определения "класса" калькулятора:
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++ или других ООП языках):
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;
}
Затем создадим "конструктор" — функцию, которая инициализирует новый экземпляр структуры:
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;
}
И "деструктор", чтобы корректно освобождать ресурсы:
void calculator_free(Calculator* calc) {
if (calc != NULL) {
free(calc);
}
}
Применение этих принципов позволяет писать код в C, который следует объектно-ориентированной парадигме. Это особенно полезно для сложных проектов, где модульность и организация кода имеют решающее значение.

Проектирование объектной модели калькулятора на C
Александр Петров, ведущий разработчик систем встроенного ПО
Недавно я столкнулся с задачей создания калькулятора для микроконтроллера, где требовалась поддержка различных режимов работы и возможность добавления новых функций без переписывания существующего кода. Традиционное процедурное программирование превратило бы это в кошмар условных операторов и глобальных переменных.
Вместо этого я применил объектную модель даже на С. Структурировав код вокруг "объектов" операций и "класса" калькулятора, я создал гибкое решение, где каждая математическая операция стала отдельным модулем с унифицированным интерфейсом. Когда позже потребовалось добавить тригонометрические функции, я просто расширил иерархию, не трогая базовый код калькулятора. Такой подход сэкономил мне недели отладки и позволил безболезненно масштабировать проект.
При проектировании объектной модели калькулятора на языке C необходимо определить ключевые абстракции, их взаимодействие и иерархию. Основная цель — создать модель, которая будет гибкой, расширяемой и легко поддерживаемой. 📐
Рассмотрим главные компоненты нашей объектной модели:
- Calculator — основной класс, представляющий сам калькулятор
- Operation — абстрактный класс для представления математических операций
- Memory — класс для управления памятью калькулятора
- Display — класс для вывода результатов и взаимодействия с пользователем
Определим структуру для базового класса операций:
typedef struct Operation {
char symbol; // Символ операции (+, -, *, /)
double (*execute)(double, double); // Указатель на функцию выполнения
char* (*get_name)(void); // Получение названия операции
} Operation;
Для каждой конкретной операции мы создадим отдельную структуру, которая будет "наследоваться" от базовой:
// "Реализации" операций
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 };
Теперь определим основную структуру калькулятора, которая будет использовать эти операции:
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;
Реализация метода выполнения операции может выглядеть так:
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;
}
Важным аспектом объектной модели является её расширяемость. Например, мы можем создать "наследника" нашего базового калькулятора — научный калькулятор:
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 |
Проектирование объектной модели — это фундамент успешной реализации калькулятора с принципами ООП. Хорошо продуманная архитектура позволяет легко расширять функциональность и поддерживать код в долгосрочной перспективе.
Реализация математических операций через методы объектов
После проектирования объектной модели переходим к реализации математических операций в виде методов. Этот подход превращает операции из простых функций в полноценные методы объектов, что даёт нам гибкость и расширяемость, характерные для ООП. 🧮
Начнем с определения интерфейса для математических операций:
// 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 */
Теперь реализуем эти функции:
// 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;
}
Теперь определим конкретные математические операции:
// 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);
}
Теперь интегрируем эти операции в наш калькулятор:
// 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 */
Реализация калькулятора:
// 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++ или другого ООП-языка.
Создание пользовательского интерфейса для калькулятора — это важная часть проекта, которая должна обеспечить удобное взаимодействие пользователя с логикой приложения. В объектно-ориентированном подходе интерфейс также должен быть спроектирован как отдельный "класс" или набор классов. 🖥️
Начнем с определения интерфейса для нашего дисплея калькулятора:
// 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 */
Реализация дисплея для консольного интерфейса:
// 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();
}
}
Теперь создадим класс для пользовательского интерфейса калькулятора, который будет связывать дисплей с логикой калькулятора:
// 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 */
Реализация пользовательского интерфейса:
// 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");
}
Главная функция для запуска программы:
// 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:
// 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();
}
Теперь напишем интеграционный тест для взаимодействия калькулятора с операциями:
// 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 для этой цели:
# Компиляция с отладочной информацией
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:
# Запуск 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-указатели)
- Проверка на утечки памяти при создании и уничтожении объектов
- Тестирование полиморфного поведения через указатели на функции
- Изоляция компонентов для модульного тестирования с помощью техники моков
Для сложных систем рекомендуется создать тестовую среду, которая имитирует сценарии использования калькулятора:
// 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 для калькулятора
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/)