Главная страница

Васин Д.Ю. - Язык программирования Си. Курс лекций - 2003. Руководство для начинающих. М. Мир, 1988г. 512 с. Трой Д. Программирование на языке Си для персонального компьютера ibm pc Пер с англ. М. Радио и связь, 1991г. 432 с


Скачать 1.1 Mb.
НазваниеРуководство для начинающих. М. Мир, 1988г. 512 с. Трой Д. Программирование на языке Си для персонального компьютера ibm pc Пер с англ. М. Радио и связь, 1991г. 432 с
Дата30.06.2022
Размер1.1 Mb.
Формат файлаdoc
Имя файлаВасин Д.Ю. - Язык программирования Си. Курс лекций - 2003.doc
ТипРуководство
#621128
страница24 из 29
1   ...   21   22   23   24   25   26   27   28   29

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


Указатель - это переменная, содержащая адрес переменной. Указатели широко применяются в Си - отчасти потому, что в некоторых случаях без них просто не обойтись, а отчасти потому, что программы с ними обычно короче и эффективнее. Указатели и массивы тесно связаны друг с другом: в данной главе мы рассмотрим эту зависимость и покажем, как ею пользоваться. Наряду с goto указатели когда-то были объявлены лучшим средством для написания малопонятных программ. Так оно и есть, если ими пользоваться бездумно. Ведь очень легко получить указатель, указывающий на что-нибудь совсем нежелательное. При соблюдении же определенной дисциплины с помощью указателей можно достичь ясности и простоты. Мы попытаемся убедить вас в этом.

Изменения, внесенные стандартом ANSI, связаны в основном с формулированием точных правил, как работать с указателями. Стандарт узаконил накопленный положительный опыт программистов и удачные нововведения разработчиков компиляторов. Кроме того, взамен char* в качестве типа обобщенного указателя предлагается тип void* (указатель на void).

Для чего нужны указатели? Вот наиболее частые примеры их использования:

  • Доступ к элементам массива

  • Передача аргументов в функцию, от которой требуется изменить эти аргументы

  • Передача в функции массивов и строковых переменных

  • Выделение памяти

  • Создание сложных структур, таких, как связный список или бинарное дерево.

Идея указателей несложна. Каждый байт в памяти машины имеет свой уникальный адрес. Адреса начинаются с 0, а затем монотонно возрастают. Если у нас есть 1 Мб ОП, то максимальным адресом будет число 1 048 575.

Загружаясь в память, наша программа занимает некоторое количество этих адресов. Это означает, что каждая переменная и каждая функция нашей программы начинается с некоторого конкретного адреса.

4.16.1.Операция получения адреса &


Мы можем получить адрес переменной, используя операцию получения
адреса &.

Int xl=ll, x2=22, x3=33;

&xl - возвращает адрес переменной xl;

&х2 - возвращает адрес переменной х2;

&хЗ - возвращает адрес переменной хЗ;

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

Ox8f4ffff4 - адрес переменной xl;

Ox8f4ffff2 - адрес переменной х2;

Ox8f4ffffO - адрес переменной хЗ;

Важно понимать, что адрес переменной - это не то же самое, что значение переменной. Содержимое трех переменных - это числа 11, 22 и 33.

Заметим, что адреса отличаются друг от друга двумя байтами. Это произошло потому, что переменная типа int занимает в памяти два байта. Если бы мы использовали переменные типа char, то значения адресов отличались бы на единицу.

4.16.2. Переменные указатели


Адресное пространство ограничено. Нам необходимы переменные, хранящие значение адреса. Нам знакомы переменные, хранящие знаки, целые или вещественные числа и т.д. Адреса хранятся точно так же. Переменная,содержащая в себе значение адреса, называется переменной-указателем или просто указателем.

Какого же типа может быть переменная-указатель? Она не того же типа, что и переменная, адрес которой мы храним: указатель на int не имеет типа int.

Int *ptr; - определение переменной-указателя на целое. * - означает указатель на. Т.е. переменная ptr может содержать в себе адрес переменной типа int.

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

pointer ptr;

4.16.3. Указатели должны иметь значение


Пусть у нас есть адрес Ox8f4ffff4, мы можем назвать его значением указателя. Указатель ptr называется переменной-указателем. Как переменная int xl может принимать значение, равное 11, так переменная-указатель может принимать значение, равное Ox8f4ffff4.

Пусть мы определили некоторую переменную для хранения некоторого значения, но не проинициализировали ее. Она будет содержать некоторое случайное число. В случае с переменной-указателем это случайное число является некоторым адресом в памяти. Этот адрес может оказаться чем угодно: от кода нашей программы до кода ОС. Неинициализированный указатель может привести к краху ОС, его очень тяжело выявить при отладке программы, т.к. компилятор не выдает предупреждения о подобных ошибках. Поэтому всегда важно убедиться, что каждому указателю перед его использованием было присвоено значение, т.е. его необходимо проинициализировать определенным адресом, например ptr =&xl.

4.16.4. Доступ к переменной по указателю


Существует запись, позволяющая получить доступ к значению переменной по ее адресу

int xl=ll,x2=22;

int *ptr;

ptr = &xl;

printf("%d",*ptr); //печать содержимого через указатель

ptr = &х2;

printf("%d",*ptr);

Выражение *ptr дважды встречается в нашей программе, позволяет нам получить значения переменных xl и х2.

*, стоящая перед именем переменной, как в выражении *ptr, называется операцией разыменования. Эта запись означает: взять значение переменной, на которую указывает указатель. Таким образом, выражение *ptr представляет собой значение переменной, на которую указывает указатель ptr.

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

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

4.16.5 Указатель на void


Отметим одну особенность указателей. Адрес, который мы помещаем в указатель, должен быть одного с ним типа. Мы не можем присвоить указателю на int адрес переменной типа float.

float fx=98.6;

int ptr = &fx; //так нельзя, типы int * и float * несовместны

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

void *ptr.

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

int ix;

float fx;

int *iptr;

float *fptr;

void *vptr;

iptr =&ix;

fptr = &fx;

vptr = &ix;

vptr = &fx;

Следующие строки недопустимы

iptr - &fx;

fptr = &ix;

4.16.6. Указатели-константы и указатели переменные


Можно ли записать *(аггау++), т.е использовать операцию увеличения вместо прибавления шага j к имени array? Нет, так писать нельзя, поскольку нельзя изменять константы. Выражение array является адресом в памяти, где размещен наш массив, поэтому array - это указатель

константы. Мы не можем сказать аггау++, так же как не можем сказать 7++. Мы не можем увеличивать адрес, но имеем возможность увеличить указатель, который содержит этот адрес.
int main()

{

int array[5]={3 1,54,77,52,93};

intj;

int *prt;

prt = array;

for(j=0; j<5; j++) printf("%6d",*(prt++);

return 0;

}

Здесь мы определили указатель на int и затем присвоили ему значение адреса массива. После этого мы можем получить доступ к элементам массива, используя операцию разыменования *(prt++). Переменная pit имеет тот же адрес, что и array, поэтому доступ к первому элементу осуществляется как и раньше. Но prt - это уже переменная-указатель, то мы ее можем увеличивать. После этого она уже будет указывать на первый элемент массива array.

4.16.7 Передача простой переменной в функцию


Рассмотрим функцию void summ(int a, int b, int *us). 3ий параметр объявлен в этой функции как указатель на int. Когда головная программа вызывает функцию summ, то она передает в качестве 3 аргумента адрес переменной summ(a, b,&s). Это не сама переменная, а ее адрес. Т.к. функция summ получает адрес переменной s, то она может применить к ней операцию разыменования *us = а+b. Т.к. us содержит адрес переменной s, то все действия, которые мы совершим с *us, мы в действительности совершим с s.

4.16.8. Передача массивов


Пусть мы хотим передать в функцию массив int A[10]. Прототип такой функции может выглядеть следующим образом void func(int[]). В этом случае мы обеспечиваем передачу массива по значению. Если мы хотим использовать аппарат указателей, то передача аргумента будет выглядеть как void func(int *).

Т.к. имя массива является его адресом, то нет нужды использовать при вызове функции операцию взятия адреса, т можно записать func(A).

4.16.9.Указатели и адреса


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



Унарный оператор & выдает адрес объекта, так что инструкция

p = &c;

присваивает переменной p адрес ячейки c (говорят, что p указывает на c). Оператор & применяется только к объектам, расположенным в памяти: к переменным и элементам массивов. Его операндом не может быть ни выражение, ни константа, ни регистровая переменная.

Унарный оператор * есть оператор косвенного доступа. Примененный к указателю он выдает объект, на который данный указатель указывает. Предположим, что x и y имеют тип int, а ip – укаэатель на int. Следующие несколько строк придуманы специально для того, чтобы показать, каким образом объявляются указатели и как используются операторы & и *.

int х = 1, у = 2, z[10];

int *ip; ip - указатель на int
ip = &x; теперь ip указывает на x

y = *ip; y теперь равен 1

*ip = 0; x теперь равен 0

ip = &z[0]; ip теперь указывает на z[0]
Объявления x, y и z нам уже знакомы. Объявление указателя ip

int *ip;

мы стремились сделать мнемоничным - оно гласит: "выражение *ip имеет тип int". Синтаксис объявления переменной "подстраивается" под синтаксис выражений, в которых эта переменная может встретиться. Указанный принцип применим и в объявлениях функций. Например, запись

double *dp, atof (char *);

означает, что выражения *dp и atof(s) имеют тип double, а аргумент функции atof есть указатель на char.

Вы, наверное, заметили, что указателю разрешено указывать только на объекты определенного типа. (Существует одно исключение: "указатель на void" может указывать на объекты любого типа, но к такому указателю нельзя применять оператор косвенного доступа)

Если ip указывает на x целочисленного типа, то *ip можно использовать в любом месте, где допустимо применение x; например,

*ip = *ip + 10;

увеличивает *ip на 10.

Унарные операторы * и & имеют более высокий приоритет, чем арифметические операторы, так что присваивание

y = *ip + 1;

берет то, на что указывает ip, и добавляет к нему 1, а результат присваивает переменной y. Аналогично

*ip += 1;

увеличивает на единицу то, на что указывает ip; те же действия выполняют

++*ip;

и

(*iр)++;

В последней записи скобки необходимы, поскольку если их не будет, увеличится значение самого указателя, а не то, на что он указывает. Это обусловлено тем, что унарные операторы * и ++ имеют одинаковый приоритет и порядок выполнения - справа налево.

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

iq = ip;

копирует содержимое ip в iq, чтобы ip и iq указывали на один и тот же объект.

4.16.10. Указатели и аргументы функций


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

swap(a, b);

где функция swap определена следующим образом:

void swap(int х, int у) /* НЕВЕРНО */

{

int temp;

temp = х;

x = y;

у = temp;

}

Поскольку swap получает лишь копии переменных a и b, она не может повлиять на переменные a и b той программы, которая к ней обратилась. Чтобы получить желаемый эффект, вызывающей программе надо передать указатели на те значения, которые должны быть изменены:

swap(&a, &b);

Так как оператор & получает адрес переменной, &a есть указатель на a. В самой же функции swap параметры должны быть объявлены как указатели, при этом доступ к значениям параметров будет осуществляться косвенно.

void swap(int *px, int *py) /* перестановка *px и *py */

{

int temp;

temp = *рх;

*рх = *py;

*ру = temp;

}

Графически это выглядит следующим образом: в вызывающей программе:



Аргументы-указатели позволяют функции осуществлять доступ к объектам вызвавшей ее программы и дают возможность изменить эти объекты.

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


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

Объявление

int a[10];

Определяет массив a размера 10, т. е. блок из 10 последовательных объектов с именами a[0], a[1], ..., a[9].



Запись a[i] отсылает нас к i-му элементу массива. Если pa есть указатель на int, т. е. объявлен как

int *pa;

то в результате присваивания

pa = &a[0];

pa будет указывать на нулевой элемент a, иначе говоря, pa будет содержать адрес элемента a[0].

Теперь присваивание

x = *pa;

будет копировать содержимое a[0] в x.

Если pa указывает на некоторый элемент массива, то pa+1 по определению указывает на следующий элемент, pa+i - на i-й элемент после pa, a pa-i - на i-й элемент перед pa. Таким образом, если pa указывает на a[0], то

*(pa+1)

есть содержимое a[1], a+i - адрес a[i], a *(pa+i) - содержимое a[i].



Сделанные замечания верны безотносительно к типу и размеру элементов массива a. Смысл слов "добавить 1 к указателю", как и смысл любой арифметики с указателями, состоит в том, чтобы pa+1 указывал на следующий объект, a pa+i - на i-й после pa.

Между индексированием и арифметикой с указателями существует очень тесная связь. По определению значение переменной или выражения типа массив есть адрес нулевого элемента массива. После присваивания

pa = &a[0];

ра и a имеют одно и то же значение. Поскольку имя массива является синонимом расположения его начального элемента, присваивание pa=&a[0] можно также записать в следующем виде:

pa = a;

Еще более удивительно (по крайней мере на первый взгляд) то, что a[i] можно записать как *(a+i). Вычисляя a[i], Си сразу преобразует его в *(a+i); указанные две формы записи эквивалентны. Из этого следует, что полученные в результате применения оператора & записи &a[i] и a+i также будут эквивалентными, т. е. и в том и в другом случае это адрес i-го элемента после a. С другой стороны, если pa - указатель, то его можно использовать с индексом, т. е. запись pa[i] эквивалентна записи *(pa+i). Короче говоря, элемент массива можно изображать как в виде указателя со смещением, так и в виде имени массива с индексом.

Между именем массива и указателем, выступающим в роли имени массива, существует одно различие. Указатель - это переменная, поэтому можно написать pa=a или pa++. Но имя массива не является переменной, и записи вроде a=pa или a++ не допускаются.

Указатели и массивы очень похожи. Оказывается, что доступ к элементам массива можно получить как используя операции с массивами, так и используя указатели. Рассмотрим два способа доступа к элементам массива

int main()

{

int array[5]={31,54,77,52,93};

int j;

for(j=0;j<5;j++){printf("%6d",arraj[j]); printf("%6d",*(array+j);}

return 0;

}

Заметим, что результат действия выражения *(array+j) тот же, что и выражения arraj[j]. Вспомним, что имя массива является его адресом. Таким образом, выражение array+j – это адрес чего-то в массиве. Пусть j-З, тогда можно ожидать, что array+j будет означать 3 байта массива array. Поскольку array - это массив элементов типа int, и три байта в этом массиве - середина второго элемента, что не очень полезно для нас. Мы хотим получить четвертый элемент массива, а не его четвертый байт.



a rray array[0]



не (array+3) array[1]


array[2]



(array+3)

array[3]



array[4]

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

Но нам необходимо значение четвертого элемента, а не его адрес. Для его получения мы используем операцию разыменования(*). Поэтому результатом выполнения выражения *(аггау+3) будет значение четвертого элемента массива, то есть 52.

Теперь становится понятно, почему при объявлении указателя мы должны указать тип переменной, на которую он будет указывать. Компилятору необходимо знать, на переменные какого типа указатель указывает, чтобы осуществлять правильный доступ к элементам массива.Он умножает значение индекса на 2, в случае типа int, или на 4 - в случае типа float и т.д.

Если имя массива передается функции, то последняя получает в качестве аргумента адрес его начального элемента. Внутри вызываемой функции этот аргумент является локальной переменной, содержащей адрес.

Формальные параметры

char s[];

и

char *s;

в определении функции эквивалентны. Мы отдаем предпочтение последнему варианту, поскольку он более явно сообщает, что s есть указатель. Если функции в качестве аргумента передается имя массива, то она может рассматривать его так, как ей удобно - либо как имя массива, либо как указатель, и поступать с ним соответственно. Она может даже использовать оба вида записи, если это покажется уместным и понятным.

Функции можно передать часть массива, для этого аргумент должен указывать на начало подмассива. Например, если a - массив, то в записях

f(&a[2])

или

f(a+2)

функции f передается адрес подмассива, начинающегося с элемента a[2]. Внутри функции f описание параметров может выглядеть как

f(int arr[]) {...}

или

f(int *arr) {...}

Следовательно, для f тот факт, что параметр указывает на часть массива, а не на весь массив, не имеет значения.

Если есть уверенность, что элементы массива существуют, то возможно индексирование и в "обратную" сторону по отношению к нулевому элементу; выражения p[-1], p[-2] и т.д. не противоречат синтаксису языка и обращаются к элементам, стоящим непосредственно перед p[0]. Разумеется, нельзя "выходить" за границы массива и тем самым обращаться к несуществующим объектам.

4.16.12. Адресная арифметика


Если p есть указатель на некоторый элемент массива, то p++ увеличивает p так, чтобы он указывал на следующий элемент, а p+=i увеличивает его, чтобы он указывал на i-й элемент после того, на который указывал ранее. Эти и подобные конструкции - самые простые примеры арифметики над указателями, называемой также адресной арифметикой.

Си последователен и единообразен в своем подходе к адресной арифметике. Это соединение в одном языке указателей, массивов и адресной арифметики - одна из сильных его сторон.

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

#define ALLOCSIZE 10000 /* размер доступного пространства */
static char allocbuf[ALLOCSIZE]; /* память для alloc */

static char *allocp = allocbuf; /* указатель на своб. место */

Объявление

static char *allocp = allocbuf;

определяет allocp как указатель на char и инициализирует его адресом массива allocbuf, поскольку перед началом работы программы массив allocbuf пуст. Указанное объявление могло бы иметь и такой вид:

static char *allocp = &allocbuf[0];

поскольку имя массива и есть адрес его нулевого элемента.

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

Указатели и целые не являются взаимозаменяемыми объектами. Константа нуль - единственное исключение из этого правила: ее можно присвоить указателю, и указатель можно сравнить с нулевой константой. Чтобы показать, что нуль - это специальное значение для указателя, вместо цифры нуль, как правило, записывают NULL - константу, определенную в файле .

Несколько важных свойств арифметики с указателями.

Во- первых, при соблюдении некоторых правил указатели можно сравнивать.

Если p и q указывают на элементы одного массива, то к ним можно применять операторы отношения ==, !=, <, >= и т. д. Например, отношение вида

p < q

истинно, если p указывает на более ранний элемент массива, чем q. Любой указатель всегда можно сравнить на равенство и неравенство с нулем. А вот для указателей, не указывающих на элементы одного массива, результат арифметических операций или сравнений не определен. (Существует одно исключение: в арифметике с указателями можно использовать адрес несуществующего "следующего за массивом" элемента, т. е. адрес того "элемента", который станет последним, если в массив добавить еще один элемент.)

Во-вторых, указатели и целые можно складывать и вычитать. Конструкция

p + n

означает адрес объекта, занимающего n-е место после объекта, на который указывает p. Это справедливо безотносительно к типу объекта, на который указывает p; n автоматически домножается на коэффициент, соответствующий размеру объекта. Информация о размере неявно присутствует в объявлении p. Если, к примеру, int занимает четыре байта, то коэффициент умножения будет равен четырем.

Допускается также вычитание указателей. Например, если p и q указывают на элементы одного массива и p, то q-p+1 есть число элементов от p до q включительно

Арифметика с указателями учитывает тип: если она имеет дело со значениями float, занимающими больше памяти, чем char, и p - указатель на float, то p++ продвинет p на следующее значение float. Это значит, что другую версию alloc, которая имеет дело с элементами типа float, а не char, можно получить простой заменой в alloc и afree всех char на float. Все операции с указателями будут автоматически откорректированы в соответствии с размером объектов, на которые указывают указатели.

Можно производить следующие операции с указателями:

  • присваивание значения указателя другому указателю того же типа,

  • сложение и вычитание указателя и целого,

  • вычитание и сравнение двух указателей, указывающих на элементы одного и того же массива,

  • присваивание указателю нуля и сравнение указателя с нулем.

Других операций с указателями производить не допускается.

Нельзя складывать два указателя, перемножать их, делить, сдвигать, выделять разряды; указатель нельзя складывать со значением типа float или double; указателю одного типа нельзя даже присвоить указатель другого типа, не выполнив предварительно операции приведения (исключение составляют лишь указатели типа void*).

4.16.13. Символьные указатели функции


Строковая константа, написанная в виде

"Я строка"

есть массив символов. Во внутреннем представлении этот массив заканчивается нулевым символом '\0', по которому программа может найти конец строки. Число занятых ячеек памяти на одну больше, чем количество символов, помещенных между двойными кавычками.

Чаще всего строковые константы используются в качестве аргументов функций, как, например, в

printf("здравствуй, мир\n");

Когда такая символьная строка появляется в программе, доступ к ней осуществляется через символьный указатель; printf получает указатель на начало массива символов. Точнее, доступ к строковой константе осуществляется через указатель на ее первый элемент.

Строковые константы нужны не только в качестве аргументов функций. Если, например, переменную pmessage объявить как

char *pmessage;

то присваивание

pmessage = "now is the time";

поместит в нее указатель на символьный массив, при этом сама строка не копируется, копируется лишь указатель на нее. Операции для работы со строкой как с единым целым в Си не предусмотрены.

Существует важное различие между следующими определениями:

char amessage[] = "now is the time"; /* массив */

char *pmessage = "now is the time"; /* указатель */

amessage - это массив, имеющий такой объем, что в нем как раз помещается указанная последовательность символов и '\0'. Отдельные символы внутри массива могут изменяться, но amessage всегда указывает на одно и то же место памяти. В противоположность ему pmessage есть указатель, инициализированный так, чтобы указывать на строковую константу. А значение указателя можно изменить, и тогда последний будет указывать на что-либо другое. Кроме того, результат будет неопределен, если вы попытаетесь изменить содержимое константы.



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

4.16.14. Многомерные массивы


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

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

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

Например,

Int A[2][3]={

{0,0,0};

{1,1,1};

};

Если двумерный массив передается функции в качестве аргумента, то объявление соответствующего ему параметра должно содержать количество столбцов; количество строк в данном случае несущественно, поскольку, как и прежде, функции будет передан указатель на массив строк, каждая из которых есть массив из 13 значений типа int. B нашем частном случае мы имеем указатель на объекты, являющиеся массивами из 13 значений типа int. Таким образом, если массив daytab передается некоторой функции f, то эту функцию можно было бы определить следующим образом:

f(int daytab[2][13]) {...}

Вместо этого можно записать

f(int daytab[][13]) {...}

поскольку число строк здесь не имеет значения, или

f(int (*daytab)[13]) {...}

Последняя запись объявляет, что параметр есть указатель на массив из 13 значений типа int. Скобки здесь необходимы, так как квадратные скобки [] имеют более высокий приоритет, чем *. Без скобок объявление

int *daytab[13]

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

4.16.15. Указатели против многомерных массивов


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

int a[10][20];

int *b[10];

записи a[3][4] и b[3][4] будут синтаксически правильным обращением к некоторому значению типа int. Однако только a является истинно двумерным массивом: для двухсот элементов типа int будет выделена память, а вычисление смещения элемента a[строка][столбец] от начала массива будет вестись по формуле 20 * строка + столбец, учитывающей его прямоугольную природу. Для b же определено только 10 указателей, причем без инициализации. Инициализация должна задаваться явно -либо статически, либо в программе. Предположим, что каждый элемент b указывает на двадцатиэлементный массив, в результате где-то будут выделены пространство, в котором разместятся 200 значений типа int, и еще 10 ячеек для указателей. Важное преимущество массива указателей в том, что строки такого массива могут иметь разные длины. Таким образом, каждый элемент массива b не обязательно указывает на двадцатиэлементный вектор; один может указывать на два элемента, другой - на пятьдесят, а некоторые и вовсе могут ни на что не указывать.

Наши рассуждения здесь касались целых значений, однако чаще массивы указателей используются для работы со строками символов, различающимися по длине, как это было в функции month_name. Сравните определение массива указателей и соответствующий ему рисунок:

char *name[] = {"Неправильный месяц", "Янв", "Февр", "Март"};



с объявлением и рисунком для двумерного массива:

char aname[][15] = {"Неправ. месяц", "Янв", "Февр", "Март"};


4.16.16. Аргументы командной строки


В операционной среде, обеспечивающей поддержку Си, имеется возможность передать аргументы или параметры запускаемой программе с помощью командной строки. В момент вызова main получает два аргумента. В первом, обычно называемом argc (сокращение от argument count), стоит количество аргументов, задаваемых в командной строке. Второй, argv (от argument vector), является указателем на массив символьных строк, содержащих сами аргументы. Для работы с этими строками обычно используются указатели нескольких уровней.
int main(int argc, char *argv[])
Аргумент argv - указатель на начало массива строк аргументов.По соглашению argv[0] есть имя вызываемой программы, так что значение argc никогда не бывает меньше 1. Если argc равен 1, то в командной строке после имени программы никаких аргументов нет. Кроме того, стандарт требует, чтобы argv[argc] всегда был пустым указателем.

Так как argv - это указатель на массив указателей, мы можем работать с ним как с указателем, а не как с индексируемым массивом.

4.16.17. Указатели на функции


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

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

Лексикографическое сравнение двух строк выполняется функцией strcmp (мы уже использовали эту функцию в ранее рассмотренной программе сортировки); нам также потребуется программа numcmp, сравнивающая две строки как числовые значения и возвращающая результат сравнения в том же виде, в каком его выдает strcmp. Эти функции объявляются перед main, а указатель на одну из них передается функции qsort. Чтобы сосредоточиться на главном, мы упростили себе задачу, отказавшись от анализа возможных ошибок при задании аргументов.

#include

#include
#define MAXLINES 5000 /* максимальное число строк */

char *lineptr[MAXLINES]; /* указатели на строки текста */
int readlines(char *lineptr[], int nlines);

void writelines(char *lineptr[], int nlines);

void qsort(void *lineptr[], int left, int right,

int (*comp)(void *, void *));

int numcmp(char *, char *);
/* сортировка строк */

main(int argc, char *argv[])

{

int nlines; /* количество прочитанных строк */

int numeric = 0; /* 1, если сорт. по числ. знач. */

if (argc > 1 && strcmp(argv[1], "-n") == 0)

numeric = 1;

if ((nlines = readlines(lineptr, MAXLINES)) >= 0) {

qsort((void **) lineptr, 0, nlines-1,

(int (*)(void*,void*))(numeric ? numcmp : strcmp));

writelines(lineptr, nlines);

return 0;

} else {

printf("Bведено слишком много строк\n");

return 1;

}

}

В обращениях к функциям qsort, strcmp и numcmp их имена трактуются как адреса этих функций, поэтому оператор & перед ними не нужен, как он не был нужен и перед именем массива.

Мы написали qsort так, чтобы она могла обрабатывать данные любого типа, а не только строки символов. Как видно из прототипа, функция qsort в качестве своих аргументов ожидает массив указателей, два целых значения и функцию с двумя аргументами-указателями. В качестве аргументов-указателей заданы указатели обобщенного типа void *. Любой указатель можно привести к типу void * и обратно без потери информации, поэтому мы можем обратиться к qsort, предварительно преобразовав аргументы в void *. Внутри функции сравнения ее аргументы будут приведены к нужному ей типу. На самом деле эти преобразования никакого влияния на представления аргументов не оказывают, они лишь обеспечивают согласованность типов для компилятора.

/* qsort: сортирует v[left]...v[right] по возрастанию */

void qsort(void *v[], int left, int right,

int (*comp)(void *, void *))

{

int i, last;

void swap(void *v[], int, int);
if (left >= right) /* ничего не делается, если */

return; /* в массиве менее двух элементов */

swap(v, left, (left + right)/2);

last = left;

for (i = left+1; i <= right; i++)

if ((*comp)(v[i], v[left]) < 0)

swap(v, ++last, i);

swap(v, left, last);

qsort(v, left, last-1, comp);

qsort(v, last+1, right, comp);

}

Повнимательней приглядимся к объявлениям. Четвертый параметр функции qsort:

int (*comp)(void *, void *)

сообщает, что comp - это указатель на функцию, которая имеет два аргумента- указателя и выдает результат типа int. Использование comp в строке

if ((*comp)(v[i], v[left]) < 0)

согласуется с объявлением "comp - это указатель на функцию", и, следовательно, *comp - это функция, а

(*comp)(v[i], v[left])

- обращение к ней. Скобки здесь нужны, чтобы обеспечить правильную трактовку объявления; без них объявление

int *comp(void *, void *) /* НЕВЕРНО */

говорило бы, что comp - это функция, возвращающая указатель на int, а это совсем не то, что требуется.

Мы уже рассматривали функцию strcmp, сравнивающую две строки. Ниже приведена функция numcmp, которая сравнивает две строки, рассматривая их как числа; предварительно они переводятся в числовые значения функцией atof.

#include
/* numcmp: сравнивает s1 и s2 как числа */
int numcmp(char *s1, char *s2)

{

double v1, v2;
v1 = atof(s1);

v2 = atof(s2);

if (v1 < v2)

return -1;

else if (v1 > v2)

return 1;

else

return 0;

}

Функция swap, меняющая местами два указателя, идентична той, что мы привели ранее в этой главе за исключением того, что объявления указателей заменены на void*.

void swap(void *v[], int i, int j)

{

void *temp;

temp = v[i];

v[i] = v[j];

v[j] = temp;

}

Программу сортировки можно дополнить и множеством других возможностей; реализовать некоторые из них предлагается в качестве упражнений.
1   ...   21   22   23   24   25   26   27   28   29


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