Главная страница
Навигация по странице:

  • Указатели на объекты Адреса и указатели.

  • Операции над указателями.

  • Арифметические операции и указатели.

  • Указатели и отношения.

  • Рис. 4.4.

  • Курс на Си. Подбельский. Курс программирования на Си. В., Фомин С. С. Курс программирования на языке Си Учебник


    Скачать 1.57 Mb.
    НазваниеВ., Фомин С. С. Курс программирования на языке Си Учебник
    АнкорКурс на Си
    Дата18.02.2023
    Размер1.57 Mb.
    Формат файлаdocx
    Имя файлаПодбельский. Курс программирования на Си.docx
    ТипУчебник
    #943863
    страница13 из 42
    1   ...   9   10   11   12   13   14   15   16   ...   42
    Глава 4

    УКАЗАТЕЛИ, МАССИВЫ, СТРОКИ

    В предыдущих главах были введены все базовые (основные) типы языка Си. Для их определения и описания используются служеб­ные слова: char, short, int, long, signed, unsigned, float, double, enum, void.

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

    • массив элементов заданного типа;

    • указатель на объект заданного типа;

    • функция, возвращающая значение заданного типа.

    С массивами и функциями мы уже немного знакомы по мате­риалам главы 2, а вот указатели требуют особого рассмотрения. В языке Си указатели введены как объекты, значениями которых служат адреса других объектов либо функций. Рассмотрим вначале указатели на объекты.

      1. Указатели на объекты

    Адреса и указатели. Начнем изучение указателей, обратившись к понятию переменной. Каждая переменная в программе - это объ­ект, имеющий имя и значение. По имени можно обратиться к пере­менной и получить (а затем, например, напечатать) ее значение. В операторе присваивания выполняется обратное действие - имени переменной из левой части оператора присваивания ставится в со­ответствие значение выражения его правой части. С точки зрения машинной реализации, имя переменной соответствует адресу того участка памяти, который для нее выделен, а значение переменной - содержимому этого участка памяти. Соотношение между именем и адресом условно представлено на рис. 4.1.

    E - имя

    Программный уровень ;

    Значение

    Переменная

    Участок Содержимое

    памяти

    Машинный

    уровень

    Рис. 4.1. Соотношение между именем и адресом

    На рис. 4.1 имя переменной явно не связано с адресом, однако, например, в операторе присваивания Е=С+В; имя переменной Е адресует некоторый участок памяти, а выражение С+В определяет значение, которое должно быть помещено в этот участок памяти. В операторе присваивания адрес переменной из левой части опе­ратора обычно не интересует программиста и недоступен. Чтобы получить адрес в явном виде, в языке Си применяют унарную опе­рацию &. Выражение &Е позволяет получить адрес участка памяти, выделенного на машинном уровне для переменной Е.

    Операция & применима только к объектам, имеющим имя и раз­мещенным в памяти. Ее нельзя применять к выражениям, констан­там-литералам, битовым полям структур (см. главу 6).

    Рисунок 4.2, взятый с некоторыми изменениями из [6], хорошо ил­люстрирует связь между именами, адресами и значениями перемен­ных. На рис. 4.2 предполагается, что в программе использована, на­пример, такая последовательность определений (с инициализацией):

    char ch='G';

    int date=1937;

    float summa=2.015E-6;

    Примечание

    В примере переменная ch занимает 1 байт, date - 2 байта и summa - 4 байта. В современных ПК переменная типа int может занимать 4 байта, а переменная типа float - 8 байтов.

    В соответствии с приведенным рисунком переменные размеще­ны в памяти, начиная с байта, имеющего шестнадцатеричный адрес

    Машинный адрес:

    1А2

    В

    1А2

    С

    1А2

    D

    1А2

    Е

    1А2

    F

    1АЗ 0

    1АЗ

    1

    1АЗ

    2

    байт

    байт

    байт

    байт

    байт

    байт

    байт

    байт

    Значение в памяти:

    'G'

    1937




    2.015

    * 10 6







    Имя:

    ch

    date




    summa







    Рис. 4.2. Разные типы данных в памяти ЭВМ

    1A2B. При указанных выше размерах участков памяти в данном примере &ch = = 1A2B (адрес переменной ch); &date = = 1A2C; &summa = = 1A2E. Адреса имеют целочисленные беззнаковые зна­чения, и их можно обрабатывать как целочисленные величины.

    Имея возможность с помощью операции & определять адрес пере­менной или другого объекта программы, нужно уметь его сохранять, преобразовывать и передавать. Для этих целей в языке Си введены переменные типа «указатель», которые для краткости будем назы­вать просто указателями, если это не приводит к неоднозначности или путанице. Указатель в языке Си можно определить как пере­менную, значением которой служит адрес объекта конкретного типа. Кроме того, значением указателя может быть заведомо не равное никакому адресу значение, принимаемое за нулевой адрес. Для его обозначения в ряде заголовочных файлов, например в файле stdio.h, определена специальная константа NULL.

    Как и всякие переменные, указатели нужно определять и описы­вать, для чего используется, во-первых, разделитель '*'. В описании и определении переменных типа «указатель» необходимо сообщать, на объект какого типа ссылается описываемый указатель. Поэтому, кроме разделителя '*', в определения и описания указателей входят спецификации типов, задающие типы объектов, на которые ссыла­ются указатели. Примеры определения указателей:

    После определения указателя к нему применима унарная опе­рация '*', называемая операцией разыменования, или операцией обращения по адресу. Операндом операции разыменования всегда является указатель. Результат этой операции - тот объект, который адресует указатель-операнд. Таким образом, *z обозначает объект типа char (символьная переменная), на который указывает z; *k - объект типа int (целая переменная), на который указывает k, и т. д. Обозначения *z, *i, *f имеют права переменных соответствующих типов. Оператор *z=' '; засылает символ «пробел» в тот участок па­мяти, адрес которого определяет указатель z. Оператор *k=*i=0; за­носит целые нулевые значения в те участки памяти, адреса которых заданы указателями k, i.

    Обратите внимание на то, что указатель может ссылаться на объекты того типа, который присутствует в определении указате­ля. Исключением являются указатели, в определении которых ис­пользован тип void - отсутствие значения. Такие указатели могут ссылаться на объекты любого типа, однако к ним нельзя применять операцию разыменования, то есть операцию '*'.

    Скрытую в операторе присваивания E=B+C; работу с адресом переменной левой части можно сделать явной, если заменить один оператор присваивания следующей последовательностью:

    /* Определения переменных и указателя m: */ int E,C,B,*m;

    /* Значению m присвоить адрес переменной Е: */ m=&E;

    /* Переслать значение выражения C+B в участок памяти с адресом, равным значению m: */

    *m=B+C;

    Данный пример не объясняет необходимости применения ука­зателей, а только иллюстрирует их особенности. Возможности и преимущества указателей проявляются при работе с функциями, массивами, строками, структурами и т. д. Перед тем как привести более содержательные примеры использования указателей, остано­вимся подробнее на допустимых действиях с указателями.

    Операции над указателями. В языке Си допустимы следующие (основные) операции над указателями: присваивание; получение значения того объекта, на который ссылается указатель (синонимы: косвенная адресация, разыменование, раскрытие ссылки); получе­ние адреса самого указателя; унарные операции изменения значения

    указателя; аддитивные операции и операции сравнений. Рассмотрим перечисленные операции подробнее.

    Операция присваивания предполагает, что слева от знака опера­ции присваивания помещено имя указателя, справа - указатель, уже имеющий значение, либо константа NULL, определяющая условное нулевое значение указателя, либо адрес любого объекта того же ти­па, что и указатель слева.

    Если для имен действуют описания предыдущих примеров, то до­пустимы операторы:

    i=&date; k=i;

    z=NULL;

    Комментируя эти операторы, напомним, что выражение *имя_ указателя позволяет получить значение, находящееся по адресу, ко­торый определяет указатель. В предыдущих примерах было опреде­лено значение переменной date (1937), затем ее адрес присвоен ука­зателю i и указателю k, поэтому значением *k является целое 1937. Обратите внимание, что имя переменной date и разыменования *i, *k указателей i, k обеспечивают в этом примере доступ к одному и тому же участку памяти, выделенному только для переменной date. Любая из операций *k=выражение, *i=выражение, date=выражение приведет к изменению содержимого одного и того же участка в па­мяти ЭВМ.

    Иногда требуется присвоить указателю одного типа значение указателя (адрес объекта) другого типа. В этом случае использует­ся «приведение типов», механизм которого понятен из следующего примера:

    char *z; /* z- указатель на символ */

    int *k; /* k - указатель на целое */

    z=(char *)k; /* Преобразование указателей */

    Подобно любым переменным, переменная типа указатель имеет имя, собственный адрес в памяти и значение. Значение можно ис­пользовать, например печатать или присваивать другому указате­лю, как это сделано в рассмотренных примерах. Адрес указателя может быть получен с помощью унарной операции &. Выражение &имя_указателя определяет, где в памяти размещен указатель. Со­держимое этого участка памяти является значением указателя. Со­отношение между именем, адресом и значением указателя иллюст­рирует рис. 4.3.

    &А - адрес указателя

    Значение

    Адрес объекта

    Значение




    указателя

    (Значение указателя А)

    объекта


    Указатель А указателем А


    *A - объект, адресуемый


    Рис. 4.3. Имя, адрес и значение указателя




    С помощью унарных операций '++' и '—' числовые (арифмети­ческие) значения переменных типа указатель меняются по-разному в зависимости от типа данных, с которыми связаны эти переменные. Если указатель связан с типом char, то при выполнении операций '++' и '—' его числовое значение изменяется на 1 (указатель z в рас­смотренных примерах). Если указатель связан с типом int (указа­тели i, k), то операции ++i, i++, —k, k— изменяют числовые зна­чения указателей на 2. Указатель, связанный с типом float или long унарными операциями '++', '—', изменяется на 4. Размеры участков памяти указаны в соответствии с примечанием на стр. 142. Таким образом, при изменении указателя на единицу указатель «переходит к началу» следующего (или предыдущего) поля той длины, которая определяется типом.

    Аддитивные операции по-разному применимы к указателям, точнее имеются некоторые ограничения при их использовании. Две переменные типа указатель нельзя суммировать, однако к ука­зателю можно прибавить целую величину. При этом вычисляемое значение зависит не только от значения прибавляемой целой вели­чины, но и от типа объекта, с которым связан указатель. Например, если указатель, как в примере, относится к целочисленному объек­ту типа int, то прибавление к нему единицы увеличивает реальное значение на 2, то есть выполняется «переход» к адресу следующего участка.

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

    Например, после выполнения операторов

    int x[5], *i, *k, j;

    i=&x[0]; k=&x[4]; j=k-i;

    j принимает значение 4, а не 8, как можно было бы предположить, исходя из того, что каждый элемент массива x[ ] занимает два байта.

    В данном примере разность указателей присвоена переменной типа int. Однако тип разности указателей определяется по-разному в зависимости от особенностей компилятора и аппаратной платфор­мы. Чтобы сделать язык Си независимым от реализаций, в заголо­вочном файле stddef.h определено имя (название) ptrdiff_t, с по­мощью которого обозначается тип разности указателей в конкретной реализации.

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

    #include

    #include void main()

    {

    int x[5];

    int *i,*k;

    ptrdiff_t j;

    i=&x[0];

    k=&x[4];

    j=k-i;

    printf(“\nj=%d”,(int)j);

    }

    Результат будет таким:

    j=4

    Арифметические операции и указатели. Унарные адресные опе­рации '&' и '*' имеют более высокий приоритет, чем арифметиче­ские операции. Рассмотрим следующий пример, иллюстрирующий это правило:

    float a=4.0, *u, z;

    u=&z;

    *u=5;

    a=a + *u + 1;

    /* a равно 10, u - не изменилось, z равно 5 */

    При использовании адресной операции '*' в арифметических вы­ражениях следует остерегаться случайного сочетания знаков опе­раций деления '/' и разыменования '*', так как комбинацию '/*' компилятор воспринимает как начало комментария. Например, вы­ражение

    a/*u

    следует заменить таким:

    a/(*u)

    Унарные операции '*' и '++' или '--' имеют одинаковый приори­тет и при размещении рядом выполняются справа налево.

    Добавление целочисленного значения n к указателю, адресующе­му некоторый элемент массива, приводит к тому, что указатель по­лучает значение адреса того элемента, который отстоит от текущего на n позиций (элементов). Если длина элемента массива равна d байтов, то численное значение указателя изменяется на (d*n). Рас­смотрим следующий фрагмент программы, иллюстрирующий пере­численные правила:

    int x[4]={

    0,

    2, 4, 6

    }, *

    i, y;

    i=&x[0]; /*

    i

    равно адресу

    элемента x[0] */

    y=*i; /*

    y

    равно 0;

    i

    равно &x[0] */

    y=*i++; /*

    y

    равно 0;

    i

    равно &x[1] */

    y=++*i; /*

    y

    равно 3;

    i

    равно &x[1] */

    y=*++i; /*

    y

    равно 4;

    i

    равно &x[2] */

    y=(*i)++; /*

    y

    равно 4;

    i

    равно &x[2] */

    y=++(*i); /*

    y

    равно 6;

    i

    равно &x[2] */

    Указатели и отношения. К указателям применяются операции сравнения '>', '>=', '!=', '==', '<=', '<'. Таким образом, указатели мож­но использовать в отношениях. Но сравнивать указатели допустимо только с другими указателями того же типа или с константой NULL, обозначающей значение условного нулевого адреса.

    Приведем пример, в котором используются операции над указа­телями и выводятся (печатаются) получаемые значения. Обратите внимание, что для вывода значений указателей (адресов) в формат­ной строке функции printf( ) используется спецификация преоб­разования %p.

    #include

    float x[ ] = { 10.0, 20.0, 30.0, 40.0, 50.0 };

    void main( )

    {

    float *u1, *u2;

    int i;

    printf("\n Адреса указателей: &u1=%p &u2=%p", &u1, &u2 );

    printf("\n Адреса элементов массива: \n");

    for(i=0; i<5; i++)

    {

    if (i==3) printf("\n");

    printf(" &x[%d] = %p", i, &x[i]);

    }

    printf("\n Значения элементов массива: \n");

    for(i=0; i<5; i++)

    {

    if (i==3) printf("\n");

    printf(" x[%d] = %5.1f ", i, x[i]);

    }

    for(u1=&x[0], u2=&x[4]; u2>=&x[0]; u1++, u2--)

    {

    printf("\n u1=%p *u1=%5.1f u2=%p *u2=%5.1f",u1,*u1,u2,*u2);

    printf("\n u2-u1=%d", u2-u1);

    }

    }

    При печати значений разностей указателей и адресов в функции printf( ) использована спецификация преобразования %d - вывод знакового десятичного целого.

    Возможный результат выполнения программы (конкретные зна­чения адресов могут быть другими):

    Адреса указателей: &u1=FFF4 &u2=FFF2

    Адреса элементов массива:

    &x[0]=00A8 &x[1]=00AC &x[2]=00B0

    &x[3]=00B4 &x[4]=00B8

    Значения элементов массива:

    150

    x[0]=10.0

    x[1]=20.0

    x[2]=30.0

    x[3]=40.0

    x[4]=50.0







    u1=00A8

    *u1=10.0

    u2=00B8

    *u2=50.0

    u2-u1=4










    u1=00AC

    *u1=20.0

    u2=00B4

    *u2=40.0

    u2-u1=2










    u1=00B0

    *u1=30.0

    u2=00B0

    *u2=30.0

    u2-u1=0










    u1=00B4

    *u1=40.0

    u2=00AC

    *u2=20.0

    u2-u1=-2










    u1=00B8

    *u1=50.0

    u2=00A8

    *u2=10.0

    u2-u1=-4










    На рис. 4.4 приводится схема размещения в памяти массива float x[5] и указателей до начала выполнения цикла изменения указателей.



    Рис. 4.4. Схема размещения в памяти массива и указателей

      1. Указатели и массивы

    Указатели и доступ к элементам массивов. По определению, ука­затель - это либо объект со значением «адрес объекта» или «адрес функции», либо выражение, позволяющее получить адрес объекта или функции. Рассмотрим фрагмент:

    int x,y;

    int *p =&x;

    p=&y;

    Здесь p - указатель-объект, a &x, &y - указатели-выражения, то есть адреса-константы. Мы уже знаем, что p - переменная того же типа, что и значения &x, &y. Различие между адресом (то есть указателем-выражением) и указателем-объектом заключается в воз­можности изменять значения указателей-объектов. Именно поэто­му указатели-выражения называют указателями-константами или адресами, а для указателя-объекта используют название указатель- переменная или просто указатель.

    В соответствии с синтаксисом языка Си имя массива без индексов является указателем-константой, то есть адресом его первого эле­мента (с нулевым индексом). Это нужно учитывать и помнить при работе с массивами и указателями.

    Рассмотрим задачу «инвертирования» массива символов и раз­личные способы ее решения с применением указателей (заметим, что задача может быть легко решена и без указателей - с исполь­зованием индексации). Предположим, что длина массива типа char равна 80.

    Первое решение задачи инвертирования массива:

    char z[80],s;

    char *d,*h;

    /* d и h — указатели на символьные объекты */ for (d=z, h=&z[79]; d
    1   ...   9   10   11   12   13   14   15   16   ...   42


    написать администратору сайта