[Программирование, C++, C] Динамическая типизация C
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
ПреамбулаЭта статья была написана и опубликована мной на своем сайте более десяти лет назад, сам сайт с тех пор канул в лету, а я так и не начал писать что-то более вразумительное в плане статей. Все ниже описанное является результатом исследования C как языка двадцатилетним парнем, а, следовательно, не претендует на звание учебного пособия, несмотря на стиль изложения. Тем не менее, я искренне надеюсь, что она побудит молодых разработчиков погрузиться в эксперименты с C также, как когда-то делал это я.ПредупреждениеЭта короткая статья, окажется абсолютно бесполезной для опытных программистов C/C++, но кому-то из начинающих, возможно, позволит сэкономить время. Хочу подчеркнуть, что в большинстве хороших книг по C/C++ данная тема рассмотрена в достаточной степени.Динамическая и статическая типизацияВо многих интерпретируемых языках используется динамическая типизация. Такой подход позволяет хранить в переменной с одним именем значения разных типов. В языке C используется строгая типизация, что, на мой взгляд более, чем правильно. Однако бывают случаи (хоть и не так часто), когда гораздо удобней было бы использовать динамическую типизацию. Зачастую, такая потребность напрямую связана с некачественным проектированием, но не всегда. Не зря же в Qt присутствует тип QVariant.Здесь мы поговорим про язык C, хотя все, что описано ниже, применимо и к C++.Магия указателя пустотыНа самом деле, никакой динамической типизации в C нет и быть не может, однако существует универсальный указатель, тип которому void *. Объявление переменной такого типа, скажем, в качестве аргумента функции, позволяет передавать в нее указатель на переменную любого типа, что может быть крайне полезно. И вот он — первый пример:
#include <stdio.h>
int main()
{
void *var;
int i = 22;
var = &i;
int *i_ptr = (int *)(var);
if(i_ptr)
printf("i_ptr: %d\n", *i_ptr);
double d = 22.5;
var = &d;
double *d_ptr = (double *)(var);
if(d_ptr)
printf("d_ptr: %f\n", *d_ptr);
return 0;
}
Вывод:
i_ptr: 22
d_ptr: 22.500000
Здесь мы одному и тому же указателю присвоили указатели (простите за тавтологию) как на тип int, так и на double.Примечание: в некоторых источниках говорится о том, что присвоение указателю типа void * следует производить также с приведением типа. Возможно, это — особенности конкретных компиляторов, GCC же без ругательств обработал предыдущий пример. Но, если возникли ошибки, попробуйте:
void *var;
int i = 22;
var = (void *)(&i);
Так точно должно работать.Первый пример не нес никакой полезной нагрузки. Попробуем ее поискать во втором примере:
#include <stdio.h>
int lilround(const void *arg, const char type)
{
if(type == 0) // если передан int
return *((int *)arg); // просто возвращаем значение целого аргумента
// если передан double
double a = *((double *)arg);
int b = (int)a;
return b == (int)(a - 0.5) // если дробная часть >= 0.5
? b + 1 // округляем в плюс
: b; // отбрасываем дробную часть
}
int main()
{
int i = 12;
double j = 12.5;
printf("round int: %d\n", lilround(&i, 0)); // пытаемся округлить целое число
printf("round double: %d\n", lilround(&j, 1)); // пытаемся округлить число двойной точности
return 0;
}
Вывод:
round int: 12
round double: 13
Здесь мы создали, можно сказать, универсальную функцию для округления как целых чисел (которым оно не требуется, конечно), так и для чисел двойной точности. Следует понимать, что функция может выполнять и что-то более полезное, в зависимости от типа аргумента.Для тех, кому хочется слегка поломать мозг — альтернативная реализация функции lilround():
int lilround(const void *arg, const char type)
{
return type == 0
? *((int *)arg)
: ((int)*((double *)arg) == (int)(*((double *)arg) - 0.5)
? (int)(*((double *)arg)) + 1
: (int)(*((double *)arg)));
}
Но для того, чтобы функция знала — с чем имеет дело — мы передаем в нее второй аргумент. Если он равен 0, то первый интерпретируется как указатель на int, если нет — как указатель на double. Такой подход может во многих случаях сгодиться, но, в основном, смысл использования универсального указателя как раз-таки в том, чтобы не указывать тип передаваемого параметра.Предположим, что у нас две или более структур (struct), которые содержат различный набор полей. Но так уж получилось, что нужно передать их одной и той же функции. Почему так вышло рассуждать не будем.Что же делать? Ответ почти очевиден: передавать их в виде указателя неопределенного типа. И, все ничего, но как же тогда наша функция узнает об их типе? Все просто: в самое начало структуры добавим поле type, в которое будем записывать идентификатор структуры, по которому наша функция и будет определять ее тип, предварительно приведя неопределенный указатель к любой из структур. Идентификатором может быть поле любого типа, хоть еще одна структура, но оно должно стоять первым в каждой из структур и иметь один и тот же тип. Такое условие следует из способа расположения структур в памяти компьютера. Если написать так:
typedef struct {
char type;
int value;
} iStruct;
typedef struct {
char type;
double value;
} dStruct;
То все сработает корректно. Но если написать так:
typedef struct {
char type;
int value;
} iStruct;
typedef struct {
double value;
char type;
} dStruct;
То программа соберется, но во время работы выдаст неверный вариант, так как, в зависимости от того — к какой структуре приведем указатель, в случае обращения программа попытается считать первый байт из double value или, вообще, неизвестно откуда.А вот и пример использования такого подхода:
#include <stdio.h>
#pragma pack(push, 1)
typedef struct {
char type; // идентификатор типа структуры
int value; // целочисленное значение
} iStruct;
#pragma pack(pop)
#pragma pack(push, 1)
typedef struct {
char type; // идентификатор типа структуры
double value; // значение двойной точности
} dStruct;
#pragma pack(pop)
int lilround(const void *arg)
{
iStruct *s = (iStruct *)arg;
if(s->type == 0) // если передан int
return s->value; // просто возвращаем значение целого аргумента
// если передан double
double a = ((dStruct *)arg)->value;
int b = (int)a;
return b == (int)(a - 0.5) // если дробная часть >= 0.5
? b + 1 // округляем в плюс
: b; // отбрасываем дробную часть
}
int main()
{
iStruct i;
i.type = 0;
i.value = 12;
dStruct j;
j.type = 1;
j.value = 12.5;
printf("round int: %d\n", lilround(&i)); // пытаемся округлить целое число
printf("round double: %d\n", lilround(&j)); // пытаемся округлить число двойной точности
return 0;
}
Примечание: директивы компилятора #pragma pack(push, 1) и #pragma pack(pop) необходимо помещать до и после каждой специфической структуры, соответственно. Данная директива используется для выравнивания структуры в памяти, что обеспечит корректность метода. Однако не стоит также забывать о порядке полей.В теле функции аргумент приводится к структуре iStruct и проверяется значение поля type. Дальше уже аргумент приводится к другому типу структуры, если нужно.Перед тем, как перейти к последней части, стоить пояснить работу с простыми void-указателями. Сложение, вычитание, инкремент, декремент и т.д. не запрещены для типа void, однако могут вызывать предупреждения в C++ и не вполне понятное поведение. Поэтому необходимо сперва привести аргумент к нужному типу, а уж затем совершать операцию:
#include <stdio.h>
int main()
{
int i = 22;
void *var = &i; // объявляем void-указатель и инициализируем его адресом переменной i
(*(int *)var)++; // приводим void-указатель к int-указателю, разыменовываем его и производим операцию инкремента
printf("result: %d\n", i); // выводим измененное значение i
return 0;
}
Исходя из кода: для совершения операции необходимо записать (*(int *)var) и уже к данной записи применить требуемый оператор.Подобие интерфейсов в CВернемся к структурам. Если структура "засылается" далеко и глубоко в код, возможно даже чужой, то имеет смысл передать вместе с ней и методы, которые будут обрабатывать ее значения. Для этого создадим дополнительную структуру, которая заменит поле type:
typedef struct {
void (*printType)(); // указатель на функцию, выводящую тип
int (*round)(const void *); // указатель на функцию, округляющую значение
} uMethods;
Опишем реализации указанных функций для разных сткрутур, а также — функции инициализации разных типов структур. Результат ниже:
#include <stdio.h>
typedef struct {
void (*printType)(); // указатель на функцию, выводящую тип
int (*round)(const void *); // указатель на функцию, округляющую значение
} uMethods;
#pragma pack(push, 1)
typedef struct {
uMethods m; // структура с указателями на функции
int value; // целочисленное значение
} iStruct;
#pragma pack(pop)
#pragma pack(push, 1)
typedef struct {
uMethods m; // структура с указателями на функции
double value; // значение двойной точности
} dStruct;
#pragma pack(pop)
void intPrintType() // вывод типа для iStruct
{
printf("integer\n");
}
int intRound(const void *arg) // округление для iStruct
{
return ((iStruct *)arg)->value; // приводим аргумент к указателю на iStruct и возвращаем значение
}
void intInit(iStruct *s) // инициализация iStruct
{
s->m.printType = intPrintType; // задаем полю printType указатель на функцию вывода для iStruct
s->m.round = intRound; // задаем полю round указатель на функцию округления для iStruct
s->value = 0;
}
void doublePrintType() // вывод типа для dStruct
{
printf("double\n");
}
int doubleRound(const void *arg) // округление для dStruct
{
double a = ((dStruct *)arg)->value;
int b = (int)a;
return b == (int)(a - 0.5) // если дробная часть >= 0.5
? b + 1 // округляем в плюс
: b; // отбрасываем дробную часть
}
void doubleInit(dStruct *s)
{
s->m.printType = doublePrintType; // задаем полю printType указатель на функцию вывода для dStruct
s->m.round = doubleRound; // задаем полю round указатель на функцию округления для dStruct
s->value = 0;
}
int lilround(const void *arg)
{
((iStruct *)arg)->m.printType(); // приводим к любой структуре, в данном случае iStruct, и выводим тип
return ((iStruct *)arg)->m.round(arg); // возвращаем округленное значение
}
int main()
{
iStruct i;
intInit(&i); // инициализируем целочисленную структуру
i.value = 12;
dStruct j;
doubleInit(&j); // инициализируем структуру с данными двойной точности
j.value = 12.5;
printf("round int: %d\n", lilround(&i)); // пытаемся округлить целое число
printf("round double: %d\n", lilround(&j)); // пытаемся округлить число двойной точности
return 0;
}
Вывод:
integer
round int: 12
double
round double: 13
Примечание: директивами компилятора следует обрамлять только те структуры, которые необходимо использовать в качестве аргумента для void-указателя.ЗаключениеВ последнем примере можно заметить сходство с ОПП, что, в общем-то, правда. Здесь мы создаем структуру, инициализируем ее, задаем ее ключевым полям значения и вызываем функцию округления, которая, кстати говоря, крайне упростилась, хотя мы сюда же добавили вывод типа аргумента. На этом все. И помните, что применять подобные конструкции нужно размумно, ведь, в подавляющем большинстве задач их наличие не требуется.
===========
Источник:
habr.com
===========
Похожие новости:
- [Программирование, Управление проектами] Если у вас нашли SCRUM
- [Программирование, C++, Программирование микроконтроллеров] Работа с параметрами в EEPROM
- [Python, Data Mining, Big Data, Data Engineering] Оффер за 2 дня в X5: для Data Analyst/Data Scientist
- [Настройка Linux, Сетевые технологии, Big Data, DevOps] Сеть в bitly: Linux tc для минимизации издержек и забавы ради (перевод)
- [Open source, GitHub, Разработка под Windows, Софт, IT-компании] В репозитории пакетного менеджера winget много дубликатов, плохо сформированных пакетов и искаженных манифестов
- [Ненормальное программирование, Assembler] Benchmark CPU's Instructions (just before loading the OS) — XCHG vs XOR, XOR, XOR
- [JavaScript, Программирование] Доступные текстовые метки для всех (перевод)
- [Системное администрирование, Облачные вычисления, DevOps, Kubernetes] Self-Hosted, или Kubernetes для богатых: почему самостоятельное развертывание кластера — не всегда способ сэкономить
- [Высокая производительность, Программирование, Алгоритмы, Научно-популярное] Либо быстро, либо неправильно (перевод)
- [Разработка веб-сайтов, JavaScript, Программирование, TypeScript] Как мы потерпели неудачу, а затем преуспели в переходе на TypeScript (перевод)
Теги для поиска: #_programmirovanie (Программирование), #_c++, #_c, #_ukazateli (указатели), #_dinamicheskaja_tipizatsija (динамическая типизация), #_void, #_programmirovanie (
Программирование
), #_c++, #_c
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:51
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
ПреамбулаЭта статья была написана и опубликована мной на своем сайте более десяти лет назад, сам сайт с тех пор канул в лету, а я так и не начал писать что-то более вразумительное в плане статей. Все ниже описанное является результатом исследования C как языка двадцатилетним парнем, а, следовательно, не претендует на звание учебного пособия, несмотря на стиль изложения. Тем не менее, я искренне надеюсь, что она побудит молодых разработчиков погрузиться в эксперименты с C также, как когда-то делал это я.ПредупреждениеЭта короткая статья, окажется абсолютно бесполезной для опытных программистов C/C++, но кому-то из начинающих, возможно, позволит сэкономить время. Хочу подчеркнуть, что в большинстве хороших книг по C/C++ данная тема рассмотрена в достаточной степени.Динамическая и статическая типизацияВо многих интерпретируемых языках используется динамическая типизация. Такой подход позволяет хранить в переменной с одним именем значения разных типов. В языке C используется строгая типизация, что, на мой взгляд более, чем правильно. Однако бывают случаи (хоть и не так часто), когда гораздо удобней было бы использовать динамическую типизацию. Зачастую, такая потребность напрямую связана с некачественным проектированием, но не всегда. Не зря же в Qt присутствует тип QVariant.Здесь мы поговорим про язык C, хотя все, что описано ниже, применимо и к C++.Магия указателя пустотыНа самом деле, никакой динамической типизации в C нет и быть не может, однако существует универсальный указатель, тип которому void *. Объявление переменной такого типа, скажем, в качестве аргумента функции, позволяет передавать в нее указатель на переменную любого типа, что может быть крайне полезно. И вот он — первый пример: #include <stdio.h>
int main() { void *var; int i = 22; var = &i; int *i_ptr = (int *)(var); if(i_ptr) printf("i_ptr: %d\n", *i_ptr); double d = 22.5; var = &d; double *d_ptr = (double *)(var); if(d_ptr) printf("d_ptr: %f\n", *d_ptr); return 0; } i_ptr: 22
d_ptr: 22.500000 void *var;
int i = 22; var = (void *)(&i); #include <stdio.h>
int lilround(const void *arg, const char type) { if(type == 0) // если передан int return *((int *)arg); // просто возвращаем значение целого аргумента // если передан double double a = *((double *)arg); int b = (int)a; return b == (int)(a - 0.5) // если дробная часть >= 0.5 ? b + 1 // округляем в плюс : b; // отбрасываем дробную часть } int main() { int i = 12; double j = 12.5; printf("round int: %d\n", lilround(&i, 0)); // пытаемся округлить целое число printf("round double: %d\n", lilround(&j, 1)); // пытаемся округлить число двойной точности return 0; } round int: 12
round double: 13 int lilround(const void *arg, const char type)
{ return type == 0 ? *((int *)arg) : ((int)*((double *)arg) == (int)(*((double *)arg) - 0.5) ? (int)(*((double *)arg)) + 1 : (int)(*((double *)arg))); } typedef struct {
char type; int value; } iStruct; typedef struct { char type; double value; } dStruct; typedef struct {
char type; int value; } iStruct; typedef struct { double value; char type; } dStruct; #include <stdio.h>
#pragma pack(push, 1) typedef struct { char type; // идентификатор типа структуры int value; // целочисленное значение } iStruct; #pragma pack(pop) #pragma pack(push, 1) typedef struct { char type; // идентификатор типа структуры double value; // значение двойной точности } dStruct; #pragma pack(pop) int lilround(const void *arg) { iStruct *s = (iStruct *)arg; if(s->type == 0) // если передан int return s->value; // просто возвращаем значение целого аргумента // если передан double double a = ((dStruct *)arg)->value; int b = (int)a; return b == (int)(a - 0.5) // если дробная часть >= 0.5 ? b + 1 // округляем в плюс : b; // отбрасываем дробную часть } int main() { iStruct i; i.type = 0; i.value = 12; dStruct j; j.type = 1; j.value = 12.5; printf("round int: %d\n", lilround(&i)); // пытаемся округлить целое число printf("round double: %d\n", lilround(&j)); // пытаемся округлить число двойной точности return 0; } #include <stdio.h>
int main() { int i = 22; void *var = &i; // объявляем void-указатель и инициализируем его адресом переменной i (*(int *)var)++; // приводим void-указатель к int-указателю, разыменовываем его и производим операцию инкремента printf("result: %d\n", i); // выводим измененное значение i return 0; } typedef struct {
void (*printType)(); // указатель на функцию, выводящую тип int (*round)(const void *); // указатель на функцию, округляющую значение } uMethods; #include <stdio.h>
typedef struct { void (*printType)(); // указатель на функцию, выводящую тип int (*round)(const void *); // указатель на функцию, округляющую значение } uMethods; #pragma pack(push, 1) typedef struct { uMethods m; // структура с указателями на функции int value; // целочисленное значение } iStruct; #pragma pack(pop) #pragma pack(push, 1) typedef struct { uMethods m; // структура с указателями на функции double value; // значение двойной точности } dStruct; #pragma pack(pop) void intPrintType() // вывод типа для iStruct { printf("integer\n"); } int intRound(const void *arg) // округление для iStruct { return ((iStruct *)arg)->value; // приводим аргумент к указателю на iStruct и возвращаем значение } void intInit(iStruct *s) // инициализация iStruct { s->m.printType = intPrintType; // задаем полю printType указатель на функцию вывода для iStruct s->m.round = intRound; // задаем полю round указатель на функцию округления для iStruct s->value = 0; } void doublePrintType() // вывод типа для dStruct { printf("double\n"); } int doubleRound(const void *arg) // округление для dStruct { double a = ((dStruct *)arg)->value; int b = (int)a; return b == (int)(a - 0.5) // если дробная часть >= 0.5 ? b + 1 // округляем в плюс : b; // отбрасываем дробную часть } void doubleInit(dStruct *s) { s->m.printType = doublePrintType; // задаем полю printType указатель на функцию вывода для dStruct s->m.round = doubleRound; // задаем полю round указатель на функцию округления для dStruct s->value = 0; } int lilround(const void *arg) { ((iStruct *)arg)->m.printType(); // приводим к любой структуре, в данном случае iStruct, и выводим тип return ((iStruct *)arg)->m.round(arg); // возвращаем округленное значение } int main() { iStruct i; intInit(&i); // инициализируем целочисленную структуру i.value = 12; dStruct j; doubleInit(&j); // инициализируем структуру с данными двойной точности j.value = 12.5; printf("round int: %d\n", lilround(&i)); // пытаемся округлить целое число printf("round double: %d\n", lilround(&j)); // пытаемся округлить число двойной точности return 0; } integer
round int: 12 double round double: 13 =========== Источник: habr.com =========== Похожие новости:
Программирование ), #_c++, #_c |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:51
Часовой пояс: UTC + 5