5.5. Типы данных и переменные

    Настало время свести воедино все интуитивно используемые понятия, касающиеся организации данных - типов данных, переменных. Язык Си имеет здесь свою специфику. Во-первых, он является жестко типизированным с привязкой при трансляции к каждому объекту (переменной) раз и навсегда заданных типов данных. Во-вторых, способ определения этого типа довольно специфичен: он задается неявно, в контексте (окружении) тех операций, которые можно выполнить над объектом. Это создает дополнительную путаницу у начинающих: они зачастую путают синтаксис использования переменной в выражении и синтаксис ее определения, путают определение с объявлением, поскольку в том и другом случае применяются одни и те же операции, один и тот же синтаксис.

    Контекстное определение типа данных в Си

    Кошка:  Здесь у меня столовая. 
    Вся мебель в ней дубовая.
    Вот это стул - на нём сидят.
    Вот это стол - за ним едят.
    Свинья: Вот это стол - на нём сидят!..
    Коза: Вот это стул - его едят!..

    С.Я.Маршак. «Кошкин дом»

    Идея контекстного определения типа данных состоит в том, что определяемая переменная помещается в контекст (окружение) тех операций, с которыми она может быть использована. В более широком плане это восходит к способу определения предметов через действия, которые над ними можно произвести, т.е. «автомобиль – это то, на чем ездят». Поскольку речь идет об определении переменных производного типа данных, то операции, которые применимы к нему – это не арифметические и им подобные, а операции выделения составляющего типа данных. Если при классификации операций исходить из их функционального назначения, то для переменной каждого производного типа данных можно выделить операцию, при помощи которой можно добраться до составляющих ее компонент.

    Тип данныхОперацияИспользование в контекстном определенииДругие операции
    Массивp[i] – доступ по индексуДа
    Указатель*p - переход к указуемому объекту, разыменованиеДа&a - получение указателя (взятие адреса)
    Структураp.name (точка) – доступ по имениНетp->name доступ по имени через указатель (сочетание * и точка)
    Функцияp(…) – вызов функцииДа

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

    • определение переменной содержит имя переменной в окружении (контексте) операций выделения составляющего типа данных, последовательно выполняемых над ней;

    • допустимыми операциями являются [] – для массива, * - для указателя и () – для функции. Кроме того, используются приоритетные скобки;

    • операции записываются в той последовательности, в которой они могут быть применены к переменной, с учетом их естественных приоритетов. Для изменения естественной последовательности могут использоваться приоритетные скобки;

    • в результате последовательного применения указанных операций получается тип данных, стоящий слева в определении.

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

    int *p; Переменная, при косвенном обращении к которой получается целое -указатель на целое (прямая цепочка типов указатель – int, обратная - int - указатель).

    char *p[]; Переменная, которая является массивом, при косвенном обращении к элементу которого получаем указатель на символ (строку) - массив указателей на символы (прямая цепочка типов массив – указатель – char, обратная – char – указатель - массив).

    char (*p)[][80]; Переменная, при косвенном обращении к которой получается двумерный массив, состоящий из массивов по 80 символов - указатель на двумерный массив строк по 80 символов в строке (прямая цепочка типов указатель – двумерный массив - char)..

    int (*p)(); Переменная, при косвенном обращении к которой получается вызов функции, возвращающей в качестве результата целое - указатель на функцию, возвращающую целое (прямая цепочка типов указатель – функция - int)..

    int (*p[10])(); Переменная, которая является массивом, при косвенном обращении к элементу которого получается вызов функции, возвращающей целое -массив указателей на функции, возвращающих целое (прямая цепочка типов массив - указатель – функция - int)..

    char *(*(*p)())()); Переменная, при косвенном обращении к которой получается вызов функции, при косвенном обращении к ее результату получается вызов функции, которая в качестве результата возвращает переменную, при косвенном обращении к которой получается символ. Тип переменной p - указатель на функцию, возвращающую в качестве результата указатель на функцию, возвращающую указатель на строку (прямая цепочка типов указатель – функция – указатель – функция – указатель - char).

    Естественно, что не все сочетания типов данных допустимы, а только те, которые напрямую представимы в компьютерной архитектуре (что следует из свойств самого Си). Например, массив указателей (адресов) функций допустим, а массив функций – нет, поскольку адреса (указатели) имеют фиксированную размерность и допускают копирование, а тип данных – функция допускает только вызов.

    C
    void (*pp[10])(); // Массив указателей на функции - допустимо
    void (pp[10])();  // Массив функций – что это такое???

    Синтаксис контекстного определения не ограничивается обычными переменными. Он появляется всегда, когда речь заходит о типизированных компонентах программы. По правилам контекстного определения записываются:

    • определения и описания переменных;

    • формальные параметры функций;

    • результат функции;

    • определения элементов структуры (struct);

    • определения абстрактных типов данных;

    • определения промежуточных типов данных (спецификатор typedef).

    Иерархия типов данных и переменных

    Любая программа задает иерархическую систему типов данных, на основе которой определяются переменные. Наконец-то мы может соединить воедино все средства, предназначенные в Си для этих целей. Прежде всего, программа оперирует набором имен, которые обозначают типы данных, «источники» их появления в программе могут быть самыми разными:

    • имена базовых типов данных заданы в трансляторе «от рождения»;

    • определение структурированного типа (struct) и класса (class), кроме всего прочего, вводит имя структурированного типа или класса;

    • спецификатор typedef – явно присваивает имя производному типу данных;

    Кроме этих явно заданных имен в иерархии типов возможны неявно заданные промежуточные типы, которые вводятся вышеупомянутыми контекстными определениями. Если же тип данных упоминается сам по себе, безотносительно к какой-либо переменной, то это можно сделать с использованием абстрактного типа данных.

    Абстрактный тип данных. Используется в тех случаях, когда требуется обозначить некоторый тип данных как таковой без привязки к конкретной переменной. Синтаксис абстрактного типа данных таков: берется контекстное определение переменной такого же самого типа, в котором само имя переменной отсутствует. Абстрактный тип данных используется:

    • в операции sizeof;

    • в операторе создания динамических переменных new;

    • в операции явного преобразования типа данных;

    • при объявлении формальных параметров внешней функции с использованием прототипа.

    Например, при резервировании памяти функцией нижнего уровня malloc для создания массива из 20 указателей необходимо знать размерность указателя char, это можно сделать выражением, содержащим операцию sizeof для абстрактного типа данных char - malloc(20sizeof(char)).

    Определение промежуточного типа данных (спецификатор typedef). Спецификатор typedef позволяет в явном виде определить производный тип данных и использовать его имя в программе как обозначение этого типа, аналогично базовым (int, char...). В этом смысле он похож на определение структуры, в котором имя структуры (со служебным словом struct) становится идентификатором структурированного типа данных. Спецификатор typedef позволяет сделать то же самое для любого типа данных. Спецификатор typedef имеет синтаксис контекстного определения типа данных, в котором вместо имени переменной присутствует имя вводимого типа данных.

    C
    typedef char *PSTR; // PSTR - имя производного типа данных – указатель на char или char*
    PSTR p, q[20],*pp;  // Эквивалентно char *p, *q[20], **pp;

    Тип данных PSTR определяется в контексте как указатель на символ (строку). Переменная p типа PSTR, массив из 20 переменных типа PSTR и указатель типа PSTR представляют собой указатель на строку, массив указателей на строку и указатель на указатель на строку соответственно.

    Определение структурированного типа. Первая часть определения структурированной переменной представляет собой определение структурированного типа. Оно задает способ построения этого типа данных из уже известных (типы данных элементов структуры). Имя структурированного типа данных (person) обладает всеми синтаксическими свойствами базового типа данных, то есть использоваться наряду с ними во всех определениях и объявлениях.

    C
    struct person { // person - имя структуры, имя типа данных
      char name[20];// Элементы структуры
      int dd,mm,yy; // 
      PSTR address; //
    }               // Определение структурированных
    A, *B, X[10];   // переменных

    Это определение создает систему явно поименованных и промежуточных типов, порождающих переменные.

    Рис.55.1. Иерархия типов данных и переменных
    Рис.55.1. Иерархия типов данных и переменных

    Базовый тип char используется для создания производного типа - массива из 20 символов. Спецификатор typedef вводит промежуточный тип char* и дает ему имя PSTR. Тип данных – структура person использует массив символов в качестве одного из составляющих ее элементов. Неявно заданный тип данных - массив из 10 структур порождает переменную X соответствующего типа, а указатель на структуру – переменную B. Затем все происходит в обратном порядке. Операции [],"." и [] последовательно выделяют в переменной X i-ю структуру, элемент структуры name и j-й символ в этом элементе.

    Если внимательно посмотреть на схему, то можно заметить, что в программе в явном виде упоминаются только часть типов данных - базовые char,int и структура person и синоним указателя PSTR. Остальные типы - массив символов, массив структур, указатель на структуру - отсутствуют. Эти типы данных создаются «по ходу дела», в процессе определения переменных B,X и элемента структуры name.

    Функция как тип данных

    В системе определения типов данных функция стоит особняком, все-таки она является алгоритмической компонентой. То, что функция рассматривается как тип данных, возможно потому, что она имеет результат – т.е. данные. Формально функция – это переменная производного типа данных по отношению к результату, который она дает. Иначе говоря, функция – это то, что можно вызвать, применив к ее имени операцию () справа. Еще одна возможность использовать функцию как переменную – получить указатель на нее (см. 9.3).

    Указатель на массив

    Поскольку любой указатель может ссылаться на массив, термин «указатель на массив» для Си – то же самое, что «масло масляное». Тем не менее, он имеет смысл, если речь идет об указателе на область памяти, содержащей двумерный массив (матрицу), а адресуемой единицей является одномерный массив (строка).

    Для работы с многомерными массивами вводятся особые указатели -указатели на массивы. Они представляют собой обычные указатели, адресуемым элементом которых является не базовый тип, а массив элементов этого типа:

    C
    char A[25][80], (*p)[80];
    p=A[0];
    for (int i=0; i<25;i++,p++)
      for (int j=0; (*p)[j]; j++) putchar((*p)[j]);

    Круглые скобки имеют здесь принципиальное значение. В контексте определения p является переменной, при косвенном обращении к которой получается массив символов, то есть p является указателем на память, заполненную массивами символов по 80 в каждом. При отсутствии скобок имел бы место массив указателей на строки. Следовательно, указатель на массив может быть настроен и может перемещаться по двумерному массиву.

    Вопросы без ответов

    Определите вид объекта (переменная, функция), задаваемого в контекстном определении или объявлении, а также все неявно заданные типы данных. Найдите абстрактный тип данных и определите назначение.

    C
    //------------------------------------------------------55-01.cpp
    //------------------------------------------------------1
    char f(void);
    
    //------------------------------------------------------2
    char *f(void);
    
    //------------------------------------------------------3
    int (*p[5])(void);
    
    //------------------------------------------------------4
    void ( *(*p)(void) )(void);
    
    //------------------------------------------------------5
    int (*f(void))();
    
    //------------------------------------------------------6
    char **f(void);
    
    //------------------------------------------------------7
    typedef char *PTR;
    PTR a[20];
    
    //------------------------------------------------------8
    typedef void (*PTR)(void);
    PTR F(void);
    
    //------------------------------------------------------9
    typedef void (*PTR)(void);
    PTR F[20];
    
    //------------------------------------------------------10
    struct list {...};
    list *F(list *);
    
    //------------------------------------------------------11
    void **p[20];
    
    //------------------------------------------------------12
    char *(*pf)(char *);
    
    //------------------------------------------------------13
    int F(char *,...);
    
    //------------------------------------------------------14
    char **F(int);
    
    //------------------------------------------------------15
    typedef char *PTR;
    PTR F(int);
    
    //------------------------------------------------------16
    char **p = malloc(sizeof(char *) * 20);
    
    //------------------------------------------------------17
    char **p = malloc(sizeof(char * [20]));
    
    //------------------------------------------------------18
    char **p = new char*[20];
    
    //------------------------------------------------------19
    double d=2.56; double z=d-(int)d;
    
    //------------------------------------------------------20
    long l;  ((char *)&l) [2] = 5;
    
    //------------------------------------------------------21
    extern int strcmp(char *, char *);