Обобщенная обертка
Часть раздела Object Oriented Programming путешествия по C на Coddy — урок 50 из 61.
Сам по себе void* может хранить любые данные, но у него есть критический недостаток: вы теряете информацию о том, какой тип он содержит. Чтобы создавать по-настоящему полезные универсальные контейнеры, нам нужно связать указатель на данные с информацией о типе. Именно здесь на помощь приходит паттерн Generic Wrapper.
Идея проста: создать структуру (struct), которая объединяет void* с перечислением (enum), идентифицирующим сохраненный тип. Таким образом, обертка всегда знает, как интерпретировать свое содержимое.
typedef enum {
TYPE_INT,
TYPE_STRING
} DataType;
typedef struct {
void* data;
DataType type;
} Wrapper;При хранении данных вы выделяете память для фактического значения и записываете его тип. При извлечении вы проверяете тип перед приведением:
Wrapper wrap_int(int value) {
int* p = malloc(sizeof(int));
*p = value;
return (Wrapper){ .data = p, .type = TYPE_INT };
}
void print_wrapper(Wrapper* w) {
if (w->type == TYPE_INT) {
printf("%d\n", *(int*)w->data);
} else if (w->type == TYPE_STRING) {
printf("%s\n", (char*)w->data);
}
}Этот паттерн является основой для создания типобезопасных универсальных контейнеров на языке C. Перечисление enum служит меткой типа времени выполнения, позволяя вашему коду принимать безопасные решения о том, как обрабатывать сохраненные данные.
Задание
ЛегкоДавайте создадим типобезопасную систему универсальных оберток (wrapper), которая может хранить либо целое число, либо строку, отслеживая тип хранимых данных, чтобы мы могли безопасно извлекать и выводить их позже.
Вы организуете свой код в трех файлах:
wrapper.h: Определите перечислениеDataTypeсо значениямиTYPE_INTиTYPE_STRING. Затем определите структуруWrapper, которая объединяет указатель на данныеvoid*с полемDataType. Объявите прототипы функций для создания оберток и вывода их содержимого.wrapper.c: Реализуйте функциональность обертки:wrap_int— принимает целое число, выделяет для него память, сохраняет значение и возвращает Wrapper с соответствующим тегом типаwrap_string— принимает строку (char*), выделяет память и копирует строку, а затем возвращает Wrapper с тегом строкового типаprint_wrapper— проверяет тег типа и выводит данные соответствующим образом (целое число как есть, строку как есть)free_wrapper— освобождает динамически выделенную память внутри обертки
main.c: Соберите все вместе. Считайте индикатор типа ('i'для целого числа,'s'для строки), а затем само значение. Создайте соответствующую обертку, выведите ее содержимое и освободите память.
Ваша программа получит два входных значения:
- Индикатор типа:
'i'для целого числа или's'для строки - Значение для обертки
Пример вывода, когда входные данные 'i' и '42':
42Пример вывода, когда входные данные 's' и "Hello":
HelloПример вывода, когда входные данные 'i' и '-100':
-100Пример вывода, когда входные данные 's' и "Generic Programming":
Generic ProgrammingНе забудьте использовать include guards в заголовочном файле. Для копирования строк выделите достаточно места для строки плюс нулевой терминатор, используя strlen и strcpy.
Шпаргалка
Паттерн Generic Wrapper (универсальная обертка) связывает указатель void* с информацией о типе для создания типизированных универсальных контейнеров. Это позволяет хранить данные любого типа, сохраняя при этом информацию о том, какой именно тип данных был сохранен.
Определите перечисление (enum) для идентификации хранимых типов:
typedef enum {
TYPE_INT,
TYPE_STRING
} DataType;Создайте структуру, которая объединяет указатель на данные с тегом типа:
typedef struct {
void* data;
DataType type;
} Wrapper;При сохранении данных выделите память и запишите тип:
Wrapper wrap_int(int value) {
int* p = malloc(sizeof(int));
*p = value;
return (Wrapper){ .data = p, .type = TYPE_INT };
}При извлечении данных проверьте тег типа перед приведением:
void print_wrapper(Wrapper* w) {
if (w->type == TYPE_INT) {
printf("%d\n", *(int*)w->data);
} else if (w->type == TYPE_STRING) {
printf("%s\n", (char*)w->data);
}
}Перечисление выступает в роли тега типа времени выполнения, обеспечивая безопасную проверку типов и соответствующую обработку сохраненных данных.
Попробуйте сами
#include <stdio.h>
#include <string.h>
#include "wrapper.h"
int main() {
char type;
scanf("%c", &type);
getchar(); // пропустить символ новой строки
Wrapper w;
if (type == 'i') {
int value;
scanf("%d", &value);
// TODO: Создать целочисленную обертку с помощью wrap_int
} else if (type == 's') {
char str[256];
fgets(str, sizeof(str), stdin);
// Удалить завершающий символ новой строки, если он есть
str[strcspn(str, "\n")] = '\0';
// TODO: Создать строковую обертку с помощью wrap_string
}
// TODO: Вывести содержимое обертки с помощью print_wrapper
// TODO: Освободить память обертки с помощью free_wrapper
return 0;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Object Oriented Programming
1Основы модульного программирования
Заголовочные файлыСтражи включенияИсходные файлыСтатические функцииПовторение: Модульный калькулятор4Инкапсуляция
Концепция непрозрачных указателейОпределение непрозрачных структурГеттеры и сеттерыВалидация в сеттерахИтоги: Секретный ящик7Указатели на функции
Объявление указателей на функцииВызов указателей на функцииTypedef для указателей на функцииПередача функций в качестве аргументовИтоги: Диспетчеризация калькулятора10Обобщенные контейнеры
Повторение: указатели voidОбобщенная оберткаОбобщенный обмен (Swap)Обобщенное сравнениеПовторение: обобщенный массив2Объекты и методы
Структуры как объектыУказатель 'Self'Константная корректностьУказатель против значенияВспомогательные методыИтоги: Point Manager5Проект: Простой банковский счет
Настройка проектаРеализация счета3Жизненный цикл объекта
Паттерн «Конструктор»Паттерн «Деструктор»Инициализация в стекеГлубокое копированиеПовторение: String Wrapper6Наследование через композицию
Встраивание структурПравило первого элементаДоступ к элементам родителяUpcastingПовторение: Иерархия фигур9Проект: Рисование фигур
Обзор проектаРеализация круга