9.2. Преобразование указателей и работа с памятью на низком уровне

    Особенности работы с памятью в Си

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

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

    Наличие указателей или ссылочных типов (Паскаль, Си, Java) а также динамических переменных позволяет «вручную» реализовать динамические структуры данных средствами языка программирования, однако принципиально не меняет систему взаимоотношений и меру ответственности программиста и транслятора.

    В языке Си ситуация принципиально иная по двум причинам:

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

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

    Преобразование типа указателя

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

    C
    char A[20]={0x11,0x15,0x32,0x16,0x44,0x1,0x6,0x8A};
    
    char      *p; int *q; long *l;
    
    p = A; q = (int*)p;  l = (long*)p;
    
    p[2] = 5;                        // записать 5 во второй байт области A
    
    q[1] = 7;                        // записать 7 в первое слово области A

    Здесь p - указатель на область байтов, q - на область целых, l - на область длинных целых. Соответственно операции адресной арифметики (p+i), (q+i), *(l+i) или p[i], q[i], l[i] адресуют i-ый байт, i-ое целое и i-ое длинное целое от начала области. бласть памяти имеет различную структуру (байтовую, словную и т.д.) в зависимости от того, через какой указатель мы с ней работаем. При этом неважно, что сама область определена как массив типа char - это имеет отношение только к операциям с использованием идентификатора массива.

    Рис.102-1. Указатели различных типов в общей памяти

    Присваивание значения указателя одного типа указателю другого типа сопровождается действием, которое называется в Си преобразованием типа указателя, и которое в Си++ обозначается всегда явно. Операция (int)p меняет в текущем контексте тип указателя char на int*. На самом деле это действие является чистой фикцией (команды транслятором не генерируются). Транслятор просто запоминает, что тип указуемой переменной изменился, поэтому операции адресной арифметики и косвенного обращения нужно выполнять с учетом нового типа указателя.

    Преобразование типа указателя можно выполнить не только при присваивании, но и внутри выражения, «на лету». Операция явного преобразования типа указателя меняет тип указуемого элемента только в цепочке выполняемых операций.

    C
    char      A[20];   ((int *)A)[2] = 5;           

    Имя массива A - указатель на его начало - имеет тип char, который явно преобразуется в int. Тем самым в текущем контексте мы ссылаемся на массив как на область целых переменных. Применительно к указателю на массив целых выполняется операция индексации и последующее присваивание целого значения 5 во второй элемент целого массива, размещенного в А.

    Операция *p++ применительно к любому указателю интерпретируется как «взять указуемую переменную и перейти к следующей», значением указателя после выполнения операции будет адрес переменной, следующей за выбранной. Использование такой операции в сочетании с явным преобразованием типа указателя позволяет извлекать или записывать переменные различных типов, последовательно расположенных в памяти.

    C
    char A[20], *p=A;
    
    *p++ = 5;                      // Записать в массив байт с кодом 5
    
    *((int*)p)++ = 5;              // Записать в массив целое 5 вслед за ним
    
    *((double*)p)++ = 5.5;     // Записать в массив вещественное 5.5 вслед за ним

    Замечание: операция ++ в сочетании с преобразованием типа указателя «на лету» принимается не всеми трансляторами. В случае неудачи необходимо использовать промежуточное присваивание со сменой типа указателя.

    Работа с памятью на низком уровне.

    Операции преобразования типа указателя и адресной арифметики дают Си невиданную для языков высокого уровня свободу действий по управлению памятью. Традиционно языки программирования, даже если они работают с указателями или с их неявными эквивалентами - ссылками, не могут выйти за пределы единожды определенных типов данных для используемых в программе переменных. Напротив, в Си имеется возможность работать с памятью на «низком» уровне (можно сказать, ассемблерном или архитектурном). На этом уровне программист имеет дело не с переменными, а с помеченными областями памяти, внутри которых он может размещать данные любых типов и в любой последовательности, в какой только пожелает. Естественно, что при этом ответственность за корректность размещения данных ложится целиком на программиста.

    Простейшим примером перехода от низкоуровневого представления к традиционному является использование функций динамического распределения памяти. Функция malloc возвращает указатель типа void как адрес выделенной области памяти, который сразу же приводится к требуемому типа double. Если исходить из заданной физической размерности области памяти (в байтах), то размерность полученного массива «промеряется» с использованием операции sizeof.

    C
    double   *d; int N;
    
    printf (“Сколько байтов:); scanf(%d”,&N);
    
    d=(double*)malloc(N);
    
    int sz = N / sizeof(double);          // Количество вещественных в массиве
    
    for (i=0; i < sz; i++) d[i] = i;

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

    Для работы с последовательностью данных разных типов можно также применить объединение (union, см. 5.3), которое, как известно, позволяет размещать все свои элементы в общей памяти (не «друг за другом», а «друг на друге»). Если элементами union являются указатели, то все они «сливаются в один», к которому можно обращаться за любым указуемым типом.

    C
    union    ptr {    
    
    int         *p;
    
                double   *d;
    
                long      *l;  } PTR;
    
    int A[100];
    
    PTR.p=A;  *(PTR.p)++=5;  *(PTR.l)++=5L;  *(PTR.d)++=5.56;

    Следующая программа упаковывает массив вещественных чисел, «сворачивая» последовательности подряд идущих нулевых элементов. Формат упакованной последовательности:

    • последовательность ненулевых элементов кодируется целым счетчиком (типа int), за которым следуют сами элементы;

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

    • нулевое значение целого счетчика обозначает конец последовательности;

    Исходная и упакованная последовательности выглядят так: 2.2, 3.3, 4.4, 5.5, 0.0, 0.0, 0.0, 1.1, 2.2, 0.0, 0.0, 4.4 и 4, 2.2, 3.3, 4.4, 5.5, -3, 2, 1.1, 2.2, -2, 1, 4.4, 0.

    В процессе упаковки требуется подсчитывать количество подряд идущих нулей. В выходной последовательности запоминается место расположения последнего счетчика - также в виде указателя. Смена счетчика происходит, если текущий и предыдущий элементы относятся к разным последовательностям (комбинации «нулевой - ненулевой» и наоборот). Для записи в последовательность ненулевых значений из вещественного массива используется явное преобразование типа указателя int в double.

    C
    //------------------------------------------------------92-01.cpp
    
    //----- Упаковка массива с нулевыми элементами
    
     void pack(int *p, double v[], int n)
    
     { int *pcnt=p++;                                    // Указатель на последний счетчик
    
     *pcnt=0;                                               // Обнулить последний счетчик
    
     for (int i=0; i<n; i++){                                         // Смена счетчика
    
                if (i!=0 && (v[i]==0 && v[i-1]!=0) || v[i]!=0 && v[i-1]==0)
    
                { pcnt=p++; *pcnt=0; }                // Обнулить последний счетчик
    
                if (v[i]==0) (*pcnt)--;                    // -1 к счетчику нулевых
    
    else {
    
                (*pcnt)++ ;                     // +1 к счетчику ненулевых
    
    //                      *((double*)p)++ = v[i];     // сохранить само значение
    
                            double *q=(double*)p; *q++=v[i];
    
                            p=(int*)q;  }}
    
     *p++ = 0;}
    
     //------ Распаковка массива с нулевыми элементами
    
     int unpack(int *p, double v[])
    
     { int i=0,cnt;
    
     while ((cnt= *p++)!=0)                            // Пока нет нулевого счетчика
    
                  {
    
                  if (cnt<0)                                 // Последовательность нулей
    
                  while(cnt++!=0) v[i++]=0;
    
                  else                                       // Ненулевые элементы
    
                  while(cnt--!=0)                          // извлечь с преобразованием
    
    //            v[i++]=*((double*)p)++;             // типа указателя
    
                  double *q=(double*)p; v[i++] = *q++;
    
                  p=(int*)q; }
    
                  } return i;}                                        

    Преобразование «целое-указатель» и работа с машинными адресами

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

    • системы преобразования адресов компьютера, размерностей используемых указателей (int или long);

    • распределения памяти транслятором и операционной системой;

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

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

    C
    *(int*)0x1000=5;                // Запись целого 5 по шестнадцатеричному адресу 1000
    
    #define REG *(char*)0xFFFFF0D0
    
    REG=0xFF;                      // Запись константы «все 1» по адресу 0xFFFFF0D0
    
                                            // регистра данных в пространстве адресов основной памяти

    Динамическое распределение памяти

    Как говорится, «не боги горшки обжигают». Система динамического распределения памяти (ДРП) может быть написана на самом же Си, но при этом обязательным будет сочетание работы с данными на низком и на высоком уровне. Внутреннее представление областей свободной и выделенной памяти может быть выполнено в виде любой динамической структуры данных (массив, массив указателей, список), работа с которой происходит на традиционном (высоком) уровне. Но размерности элементов этой структуры данных будут меняться, поэтому здесь не обойтись и без их физического (низкоуровневого) представления. К тому же управляющие компоненты структуры данных также являются динамическими, поэтому для них требуется динамическая память, распределяемая самой системой. Формальный парадокс: при использовании динамического массива указателей на свободные блоки во время выполнения функции free может произойти увеличение его размерности, что потребует вызова функций realloc или malloc/free той же самой проектируемой библиотеки.

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

    рис. 102-2. Система ДРП как последовательность блоков переменной длины

    Для того, чтобы двигаться по цепочке блоков, можно использовать указатель типа char, единица адресации которого соответствует одному байту. Тогда для извлечения счетчиков длин блоков необходимо преобразовать его «на лету» к int. В исходном состоянии распределяемая область памяти представляет собой один свободный блок: в его начало записывается исходная длина.

    C
    //-----------------------------------------------92-02.cpp
    
    // Система динамического распределения памяти
    
    char *pa;                                   // Область распределяемой памяти – «кучи»
    
    int sz0;                                     // Исходная размерность кучи
    
    void create(int sz){                      // Начальное состояние – один свободный блок
    
                pa=new char[sz];
    
                *(int*)pa=sz-sizeof(int);   // Размерность свободного куска – записать в начало
    
                sz0=sz; }
    
    //--------------------------------------------------------------------
    
    void _show(){                             // Просмотр состояния кучи                              
    
    char *p=pa;
    
    int lnt;
    
    while(p<pa+sz0){                       // Пока не достигли адреса конца области
    
                lnt=*(int*)p;                    // Извлечь из-под указателя длину блока
    
                if (lnt<0){                       // Занятый блок - пропустить
    
                            lnt=-lnt;             // Инвертировать длину
    
                            printf("busy:");
    
                            }
    
                else printf("free:");          // Вывести адрес (шестнадцатеричный) и длину
    
                printf(" addr=%8x sz=%d\n",p,lnt);
    
                p+=lnt+sizeof(int);          // Сдвинуть указатель на длину блока + длина счетчика
    
                            }}

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

    C
    //-----------------------------------------------92-02.cpp
    
    // Поиск строго подходящего или отрезание от первого
    
    void *_malloc(int sz){                                                                 
    
    char *p=pa;
    
    int lnt;
    
    while(p<pa+sz0){                                   // Пока не достигли адреса конца области
    
                lnt=*(int*)p;                                // Извлечь из-под указателя длину блока
    
                if (lnt<0)                                    // Занятый блок - пропустить
    
                            p+=-lnt+sizeof(int);
    
                else      {
    
                            if (sz==lnt) {                   // Свободный – строго подходящий
    
                            *(int*)p=-lnt;                   // Обозначить как занятый
    
                            return p+sizeof(int);        // Возвратить указатель на область данных
    
                            }
    
                            p+=lnt+sizeof(int);          // К следующему блоку
    
                            }
    
                }
    
    lnt=*(int*)pa;                                          // Отрезать от первого – взять длину
    
    if (sz+sizeof(int)>lnt) return NULL;           // Остаток мал – нет памяти
    
    lnt -=sz+sizeof(int);                                // Уменьшить размер первого блока
    
    *(int*)pa=lnt;                                          // и записать полученный остаток
    
    p=pa+lnt+sizeof(int);                              // Указатель на новый блок
    
    *(int*)p=-sz;                                           // Записать в него размерность (занят – <0)
    
    return p+sizeof(int);                                // Возвратить указатель на память
    
    }                                                           // «вслед за» счетчиком

    Функция выделения памяти возвращает указатель на память «вслед за» счетчиком длины выделенной области. Таким образом, ДРП запоминает, сколько памяти было выделено, и при выполнении функции освобождения первым делом смещает указатель назад к счетчику длины. Заметим, что данная версия ДРП в состоянии проверить корректность возвращаемого main-ом адреса занятого блока (хотя и не обязана это делать).

    Кроме формального объявления блока свободным (путем инвертирования счетчика) необходимо еще «склеить» с текущим блоком предыдущий и последующий, если они также свободны (дефрагментация). Для поиска предыдущего блока приходится сделать лишний цикл просмотра содержимого распределяемой памяти с запоминанием указателя на предыдущий блок. Сама процедура «склеивания» заключается в увеличении длины предыдущего блока на размер текущего (+ счетчик длины текущего). Аналогичное действие предпринимается и для следующего блока

    C
    //-----------------------------------------------92-02.cpp
    
    // Функция освобождения памяти
    
    void _free(void *q0){
    
    char *q=(char*)q0-sizeof(int);                   // Сместить указатель на счетчик длины
    
    int lnt=*(int*)q;                                       // Извлечь счетчик длины
    
    lnt=-lnt;
    
    char *pr=NULL,*p=pa;                            // pr – указатель на предыдущий блок
    
    while(p!=q){                                            // Пока не достигли освобождаемого блока
    
                int ln=*(int*)p;
    
                if (ln<0) ln=-ln;
    
                pr=p;                                         // Текущий становится предыдущим
    
                p+=ln+sizeof(int);                       // Переход к следующему
    
                }                                               // Просмотр до предыдущего
    
    *(int*)q=lnt;                                            // Освободить блок
    
    if (*(int*)pr>0) {
    
                *(int*)pr+=lnt+sizeof(int); // Склеить с предыдущим
    
                p=pr;                                         // Сделать предыдущий текущим
    
                }
    
    lnt=*(int*)p;
    
    q=p+lnt+sizeof(int);
    
    if (q<pa+sz0 && *(int*)q>0)                     // Есть следующий и он свободен
    
    *(int*)p+=*(int*)q+sizeof(int);                    // Склеить со следующим
    
    }

    Функции с переменным числом параметров.

    Хотя для работы с переменным списком параметров существуют специальные макрокоманды, мы попробуем реализовать эту возможность «вручную», используя знания механизма вызова функции (см. 6.1) и технику извлечения данных различных типов с использованием преобразования типа указателя.

    рис. 102-3. Передача переменного списка параметров через стек

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

    C
    void var_list_fun(int a1, int a2, int a3,){
    
    int *p=&a3;                    // Указатель на последний  явный параметр функции
    
    int *q=&a3+1;                // Указатель на первый параметр из переменного списка

    Текущее количество фактических параметров, передаваемых при вызове, может быть передано:

    • отдельным параметром - счетчиком;

    • параметром ограничителем, значение которого отмечает конец списка параметров;

    • форматной строкой, в которой перечислены спецификации параметров.

    Функция с параметром - счетчиком. Первый параметр является счетчиком, определяющим количество параметров в переменном списке.

    C
    //------------------------------------------------------92-03.cpp
    
    //----- Сумма произвольного количества параметров по счетчику
    
     int sum(int n,...)                          // n - счетчик параметров
    
     { int s,*p = &n+1;                      // Указатель на область параметров
    
     for (s=0; n > 0; n--)                    // назначается на область памяти
    
                s += *p++;                    // вслед за счетчиком
    
     return(s); }
    
     void main(){ printf("sum(..=%d sum(...=%d\n",sum(5,0,4,2,56,7),sum(2,6,46)); }

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

    C
    //------------------------------------------------------92-04.cpp
    
    //----- Сумма произвольного количества ненулевых параметров
    
     int sum(int a,...)
    
     { int s,*p = &a;    // Указатель на область параметров назначается на
    
     for (s=0; *p > 0; p++ )                // первый параметр из переменного списка
    
     s += *p;                                   // Ограничитель - отрицательное
    
     return(s); }                                // значение
    
     void main() {
    
    printf("sum(..=%d sum(...=%d\n",sum(4,2,56,7,0),sum(6,46,-1 ,7,0));}

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

    C
    //------------------------------------------------------92-05.cpp
    
    //--- Функция с параметром форматной строкой ( printf)
    
    int my_printf(char *s,...)
    
    { int *p = (int*)(&s+1);                             // Указатель на начало списка параметров
    
    while (*s != '\0') {                                    // Просмотр форматной строки
    
    if (*s != '%') putchar(*s++);          // Копирование форматной строки
    
                else
    
                            { s++;                           // Спецификация параметра вида "%d"
    
                            switch(*s++){                 // Извлечение параметра
    
    case 'c':            putchar(*p++); break;     // Извлечение символа
    
    case 'd':            printf( "%d", *((int*)p)++ );
    
    break;                           // Извлечение целого
    
    case 'f': printf( "%lf", *((double*)p)++ );
    
    break;                           // Извлечение вещественного
    
    case 's': puts( *((char**)p)++ );                // Извлечение указателя
    
                            break;                           // на строку
    
    }}}}
    
    void main(){
    
    my_printf("char=%c int=%d double=%f char[]=%s",'f',44,5.5,"qwerty"); }

    Лабораторный практикум

    Упаковка переменных различного типа в заданном формате

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

    1. Последовательность прямоугольных матриц вещественных чисел, предваренная двумя целыми переменными - размерностью матрицы.

    2. Последовательность строк символов. Каждая строка предваряется целым - счетчиком символов. Ограничение последовательности - счетчик со значением 0.

    3. Упакованный массив целых переменных. Байт-счетчик, имеющий положительное значение n, предваряет последовательность из n различных целых переменных, байт-счетчик, имеющий отрицательное значение -n, обозначает n подряд идущих одинаковых значений целой переменной. Пример:

      • исходная последовательность:       2 3 3 3 5 2 4 4 4 4 4 8 -6 8

      • упакованная последовательность: (1) 2 (-3) 3 (2) 5 2 (-5) 4 (3) 8 -6 8

    4. Упакованная строка, содержащая символьное представление длинных целых чисел. Все символы строки, кроме цифр, помещаются в последовательность в исходном виде. Последовательность цифр преобразуется в целую переменную, которая записывается в упакованную строку, предваренная символом \1. Конец строки - символ \0. Пример:

      • исходная строка:       "aa2456bbbb6665"

      • упакованная строка: 'a' 'a' '\1' 2456L 'b' 'b' 'b' 'b' '\0' 6665 '\0'

    5. Произвольная последовательность переменных типа char, int и long. Перед каждой переменной размещается байт, определяющий ее тип (0-char, 1-int, 2-long). Последовательность вводится в виде целых переменных типа long, которые затем «укорачиваются» до минимальной размерности без потери значащих цифр.

    6. Последовательность структурированных переменных типа struct man { char name[20]; int dd,mm,yy; char addr[]; }; Последняя компонента представляет собой строку переменной размерности, расположенную непосредственно за структурированной переменной. Конец последовательности - структурированная переменная с пустой строкой в поле name.

    7. То же самое, что п.4, но для шестнадцатеричных чисел.

      • исходная строка:     "aa0x24FFbbb0xAA65"

      • упакованная строка: 'a' 'a''\0' 0x24FF 'b' 'b' 'b' '\0' 0xAA65 '\0' '\0'

    8. В упакованной строке последовательность одинаковых символов длиной N заменяется на байт со значением 0, байт со значением N и байт - повторяющийся символ. Конец строки обозначается через два нулевых байта.

    9. Произвольная последовательность строк и целых переменных. Байт со значением 0 - обозначает начало строки (последовательность символов, ограниченная нулем). Байт со значением N является началом последовательности N целых чисел. Конец последовательности - два нулевых байта.

    10. В начале области памяти размещается форматная строка, аналогичная используемой в printf - (%d, %f и %s целое, вещественное и строку соответственно). Сразу же вслед за строкой размещается последовательность целых, вещественных и строк в соответствии с заданным форматом.

    11. В начале области памяти размещается форматная строка. Выражение "%nnnd", где nnn - целое - определяет массив из nnn целых чисел, "%d" - одно целое число, "%nnnf" - массив из nnn вещественных чисел, "%f" - одно вещественное число. Сразу же вслед за строкой размещается последовательность целых, вещественных и их массивов в соответствии с заданным форматом.

    12. Область памяти представляет собой строку. Если в ней встречается выражение "%nnnd", где nnn - целое, то сразу же за ним следует массив из nnn целых чисел (во внутреннем представлении, то есть типа int). За выражением "%d" - одно целое число, за "%nnnf" - массив из nnn вещественных чисел, за "%f" - одно вещественное число.

    13. Область памяти представляет собой строку. Если в ней встречается символ "%", то сразу же за ним находится указатель на другую (обычную )строку. Все сроки располагаются в той же области памяти вслед за основной строкой.

    14. Разреженная матрица (содержащая значительное число нулевых элементов) упаковывается с сохранением значений ненулевых элементов в следующем формате: размерности (int), количество ненулевых элементов (int), для каждого элемента - координаты x,y (int) и значение (double).

    Функция с переменным числом параметров

    Разработать функцию с переменным числом параметров. Для извлечения параметров из списка использовать операцию преобразования типа указателя.

    1. Первый параметр - строка, в которой каждый символ «*» обозначает место включения строки, являющейся очередным параметром. Функция выводит на экран полученный текст.

    2. Каждый параметр - строка, последний параметр - NULL. Функция возвращает строку в динамической памяти, содержащую объединение строк-параметров.

    3. Последовательность указателей на вещественные переменные, ограниченная NULL.. Функция возвращает упорядоченный динамический массив указателей на эти переменные.

    4. Последовательность вещественных массивов. Сначала идет целый параметр - размерность массива (int), затем непосредственно последовательность значений типа double. Значение целого параметра - 0 обозначает конец последовательности. Функция возвращает сумму всех элементов.

    5. Последовательность вещественных массивов. Сначала идет целый параметр - размерность массива (int), затем указатель на массив значений типа double (имя массива). . Значение целого параметра - 0 обозначает конец последовательности. Функция возвращает сумму всех элементов.

    6. Первый параметр - строка, в которой каждый символ «*n», где n-цифра - обозначает место включения строки, являющейся n+1 параметром. Функция выводит на экран полученный текст.

    7. Первым параметром является форматная строка. Выражение "%nnnd", где nnn - целое - определяет массив из nnn целых чисел, "%d" - одно целое число, "%nnnf" - массив из nnn вещественных чисел, "%f" - одно вещественное число. Сразу же вслед за строкой размещается последовательность целых, вещественных и их массивов в соответствии с заданным форматом. Массив передается непосредственно в виде последовательности параметров (например "%4d%2f",44,66,55,33,66.5,66.7 )

    8. Первым параметром является форматная строка. Выражение "%nnnd", где nnn - целое - определяет массив из nnn целых чисел, "%d" - одно целое число, "%nnnf" - массив из nnn вещественных чисел, "%f" - одно вещественное число. Сразу же вслед за строкой размещается последовательность целых, вещественных и их массивов в соответствии с заданным форматом. Массив передается в виде указателя (имя массива) (например "%4d%2f",A,B )

    9. Первый параметр - строка, в которой каждый символ «*n», где n-цифра - обозначает место включения целого (int), являющегося n+1 параметром. Функция выводит на экран полученный текст, содержащий целые значения.

    10. Параметр функции - целое - определяет количество строк в следующей за ним группе. Групп может быть несколько. Целое со значением 0 - конец последовательности.

    11. Функция получает разреженный массив (содержащая значительное число нулевых элементов) в виде списка значений ненулевых элементов в следующем формате: размерность массива (int), количество ненулевых элементов (int), для каждого элемента - индекс (int) и значение (double). Функция создает и возвращает динамический массив соответствующим содержимым.

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

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

    Пример оформления тестового задания

    C
    //--------------------------------------------------------102-06.cpp
    
    double F(int *p)                                      // по умолчанию - извлекается int
    
    { double s=0;                                         // начальная сумма равна 0
    
    while (*p!=0){                                         // пока не извлечен нулевой int
    
                int n=*p++;                                // очередной int - счетчик цикла
    
                double *q=*((double**)p)++;         // следующий за ним - double*
    
                while (n--!=0) s+=*q++;               // суммирование массива
    
                }                                               // указателем q
    
    return s; }
    
    double d1[]={1,2,3,4};
    
    double d2[]={5,6};
    
    int a1=4;                                               // размерность первого массива
    
    double *q1=d1;                                      // указатель на первый массив double*
    
    int a2=2;                                               // размерность второго массива
    
    double *q2=d2;                                      // указатель на второй массив double*
    
    int a3=0;                                               // ограничитель последовательности
    
    void main(){
    
    printf("%lf\n",F(&a1));                              // Должна вывести 21 - сумму d1 и d2
    
    }

    Функция работает с указателем p, извлекая из-под него целые переменные, пока не обнаружит 0. Очередная переменная запоминается в n и используется в дальнейшем в качестве счетчика повторения цикла, то есть определяет количество элементов в некотором массиве. В том же цикле суммируемые значения извлекаются из-под указателя q типа double*, то есть речь идет о массиве вещественных. Остается определить, как формируется q. Он извлекается из той же последовательности, что и целые переменные - с использованием p. Для этого последний преобразуется "на лету" в указатель на извлекаемый тип, то есть приводится к типу double**. Таким образом, последовательность представляет собой пары переменных - целая размерность массива и указатель на сам вещественный массив. Размерность, равная 0 - ограничитель последовательности.

    C
    //------------------------------------------------------92-07.cpp
    
    //------------------------------------------------------ 1
    
     struct man {char name[20]; int dd,mm,yy; char *addr; };
    
     char *F1(char *p, char *nm, char *ad)
    
     { man *q =(man*)p;
    
     strcpy(q->name,nm);
    
     strcpy((char*) (q+1 ),ad);
    
     q->addr = (char*) (q+1 );
    
     for (p=(char*) (q+1 ); *p!=0; p++);
    
     p++; return p;}
    
     //------------------------------------------------------ 2
    
     struct man1 {char name[20]; int dd,mm,yy; char addr[]; };
    
     char *F2(char *p, char *nm, char *ad)
    
     { man1 *q =(man1*)p;
    
     strcpy(q->name,nm);
    
     strcpy(q->addr,ad);
    
     for (p=q->addr; *p!=0; p++);
    
     p++; return p;}
    
     //------------------------------------------------------ 3
    
     int *F3(int *q, char *p[])
    
     { char *s;
    
     for ( int i=0; p[i]!=NULL; i++);
    
     *q = i;
    
     for (s = (char*)(q+1), i=0; p[i]!=NULL; i++) {
    
          for ( int j=0; p[i][j]!='\0'; j++) *s++ = p[i][j];
    
          *s++ = '\0';
    
          }
    
     return (int*)s;}
    
     //------------------------------------------------------- 4
    
     double F4(int *p)
    
     { double *q,s; int m;
    
     for (q=(double*)(p+1), m=*p, s=0.; m>=0; m--) s+= *q++;
    
     return s;}
    
     //------------------------------------------------------- 5
    
     char *F5(char *s, char *p[])
    
     { int i,j;
    
     for (i=0; p[i]!=NULL; i++) {
    
                for (j=0; p[i][j]!='\0'; j++) *s++ = p[i][j];
    
                *s++ = '\0';
    
                }
    
     *s = '\0'; return s;}
    
     //------------------------------------------------------- 6
    
     union x {int *pi; long *pl; double *pd;};
    
     double F6(int *p)
    
     { union x ptr;
    
     double dd=0;
    
     for (ptr.pi=p; *ptr.pi !=0; )
    
               switch (*ptr.pi++) {
    
     case 1: dd += *ptr.pi++; break;
    
     case 2: dd += *ptr.pl++; break;
    
     case 3: dd += *ptr.pd++; break;
    
                }
    
     return dd;}
    
     //------------------------------------------------------- 7
    
     unsigned char *F7(unsigned char *s, char *p)
    
     { int n;
    
     for (n=0; p[n] != '\0'; n++);
    
     *((int*)s)++ = n;
    
     for (; *p != '\0'; *s++ = *p++);
    
     s++; return s;}
    
     //------------------------------------------------------- 8
    
     int *F8(int *p, int n, double v[])
    
     { *p++ = n;
    
     for (int i=0; i<n; i++) *((double*)p)++ = v[i];
    
     return p;}
    
     //------------------------------------------------------- 9
    
     double F9(int *p)
    
     { double s=0;
    
          while(*p!=0) {
    
          if (*p>0) s+=*p++;
    
          else
    
               { p++; s += *((double*)p)++; }
    
          }
    
     return s; }
    
     //------------------------------------------------------ 10
    
     double F10(char *p)
    
     { double s; char *q;
    
     for (q=p; *q!=0; q++);
    
     for (q++; *p!=0; p++)
    
               switch(*p) {
    
     case 'd': s+=*((int*)q)++; break;
    
     case 'f': s+=*((double*)q)++; break;
    
     case 'l': s+=*((long*)q)++; break;
    
          }
    
     return s; }
    
     //-------------------------------------------------------11
    
     int F11(char *p)
    
     { int s=0, *v; char *q;
    
     for (q=p; *q!=0; q++);
    
     q++; v=(int*)q;
    
     for(;*p!=0;p++)
    
          if (*p>='0' && *p<='9') s+=v[*p-'0'];
    
     return s; }

    Определите формат последовательности параметров функции и напишите ее вызов с фактическими параметрами – константами.

    Пример оформления тестового задания.

    C
    //----------------------------------------------------92-08.cpp
    
    double F(int a1,...)                                  // Первый параметр - счетчик цикла
    
    { int i,n;
    
    double s,*q=(double*)(&a1+1);                 // Указатель на второй и последующие
    
    for (s=0, n=a1; n!=0; n--)             // параметры - типа double*
    
    s += *q++;                                // Сумма параметров, начиная
    
    return s;}                                               // со второго
    
    void main() { printf("%lf\n",F(3,1.5,2.5,3.5)); }
    
    Указатель q типа double* ссылается на второй параметр функции (первый из переменного списка) - &a1+1 – указатель на область памяти, «следующую за…». Первый параметр используется в качестве счетчика повторений цикла, цикл суммирует значения, последовательно извлекаемые из-под указателя q. Результат – функция суммирует вещественные переменные из списка, предваренного целым счетчиком.
    
     
    
    //------------------------------------------------------92-09.cpp
    
    //--------------------------------------------------------1
    
     void F1(int *p,...)
    
     { int **q, i, d;
    
     for (i=1, q = &p, d=*p; q[i]!=NULL; i++) *q[i-1] = *q[i];
    
     *q[i-1] = d;}
    
     //--------------------------------------------------------2
    
     int *F2(int *p,...)
    
     { int **q, i, *s;
    
     for (i=1, q = &p, s=p; q[i]!=NULL; i++)
    
          if (*q[i] > *s) s = q[i];
    
     return s; }
    
     //--------------------------------------------------------3
    
     int F3(int p[], int a1,...)
    
     { int *q, i;
    
     for (i=0, q = &a1; q[i] > 0; i++) p[i] = q[i];
    
     return i;}
    
     //--------------------------------------------------------4
    
     union x { int *pi; long *pl; double *pd; };
    
     void F4(int p,...)
    
     { union x ptr;
    
     for (ptr.pi = &p; *ptr.pi != 0; ) {
    
          switch(*ptr.pi++) {
    
                  case 1: printf("%d",*ptr.pi++); break;
    
                  case 2: printf("%ld",*ptr.pl++); break;
    
                  case 3: printf("%lf",*ptr.pd++); break;
    
               }}}
    
     //--------------------------------------------------------5
    
     char **F5(char *p,...)
    
     { char **q,**s; int i,n;
    
     for (n=0, q = &p; q[n] !=NULL; n++);
    
     s = new char*[n+1];
    
     for (i=0, q = &p; q[i] !=NULL; i++) s[i]=q[i];
    
     s[n]=NULL; return s;}
    
     //--------------------------------------------------------6
    
     char *F6(char *p,...)
    
     { char **q; int i,n;
    
     for (i=0, n=0, q = &p; q[i] !=NULL; i++)
    
          if (strlen(q[i]) > strlen(q[n])) n=i;
    
     return q[n]; }
    
     //--------------------------------------------------------7
    
     int F7(int a1,...)
    
     { int *q, s;
    
     for (s=0, q = &a1; *q > 0; q++) s+= *q;
    
     return s;}
    
     //--------------------------------------------------------8
    
     union xx { int *pi; long *pl; double *pd; };
    
     double F8(int p,...)
    
     { union xx ptr;
    
     double dd=0;
    
     for (ptr.pi = &p; *ptr.pi != 0; )
    
                {
    
             switch(*ptr.pi++) {
    
      case 1: dd+= *ptr.pi++; break;
    
      case 2: dd+= *ptr.pl++; break;
    
      case 3: dd+= *ptr.pd++; break;
    
             }}
    
     return dd;}
    
     //-------------------------------------------------------9
    
     double F9(int a1,...)
    
     { double s=0; int *p=&a1;
    
     while(*p!=0) {
    
                if (*p>0) s+=*p++;
    
                else
    
               { p++; s += *((double*)p)++; }
    
                } return s; }
    
     //--------------------------------------------------------10
    
     double F10(char *p,...)
    
     { double s;
    
     int *q=(int *)(&p+1);
    
     for (;*p!=0; p++)
    
               switch(*p) {
    
     case 'd': s+=*q++; break;
    
     case 'f': s+=*((double*)q)++; break;
    
     case 'l': s+=*((long*)q)++; break;
    
                } return s; }
    
     //--------------------------------------------------------11
    
     int F11(char *p,...)
    
     { int s=0, *q=(int *)(&p+1);
    
     for(;*p!=0;p++)
    
          if (*p>='0' && *p<='9') s+=q[*p-'0'];
    
     return s; }
    
     //--------------------------------------------------------12
    
     double F12(int p,...)
    
     { double dd=0; int *q=&p;
    
          for (; *q != 0; ) {
    
          switch(*q++)
    
               {
    
      case 1: dd+= *q++; break;
    
      case 2: dd+= *((long*)q)++; break;
    
      case 3: dd+= *((double*)q)++; break;
    
               }}
    
     return dd;}