Menu
Coddy logo textTech

Обобщенная обертка

Часть раздела 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 служит меткой типа времени выполнения, позволяя вашему коду принимать безопасные решения о том, как обрабатывать сохраненные данные.

challenge icon

Задание

Легко

Давайте создадим типобезопасную систему универсальных оберток (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' для строки), а затем само значение. Создайте соответствующую обертку, выведите ее содержимое и освободите память.

Ваша программа получит два входных значения:

  1. Индикатор типа: 'i' для целого числа или 's' для строки
  2. Значение для обертки

Пример вывода, когда входные данные '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;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Object Oriented Programming