Курс на Си. Подбельский. Курс программирования на Си. В., Фомин С. С. Курс программирования на языке Си Учебник
Скачать 1.57 Mb.
|
Массивы в параметрах. Если в качестве параметра функции используется обозначение массива, то на самом деле внутрь функции передается только адрес начала массива. Применяя массивы в качестве параметров для функций из главы 2, мы не отмечали этой особенности. Например, заголовок функции для вычисления скалярного произведения векторов выглядел так: float Scalar_Product(int n, float a[ ], float b[ ]). . . Но заголовок той же функции можно записать и следующим образом: float Scalar_Product(int n, float *a, float *b). . . Конструкции float b[ ]; и float *b; совершенно равноправны в спецификациях параметров. Однако в первом случае роль имени b как указателя не так явственна. Во втором варианте все более очевидно - b явно специфицируется как указатель типа float *. В теле функции Scalar_Product( ) из главы 2 обращение к элементам массивов-параметров выполнялось с помощью индексированных элементов a[i] и b[i]. Однако можно обращаться к элементам массивов, разыменовывая соответствующие значения адресов, то есть используя выражения *(a+i) и *(i+b). Так как массив всегда передается в функцию как указатель, то внутри функции можно изменять значения элементов массива-ар гумента, определенного в вызывающей программе. Это возможно и при использовании индексирования, и при разыменовании указателей на элементы массива. Для иллюстрации указанных возможностей рассмотрим функцию, возводящую в квадрат значения элементов одномерного массива и вызывающую ее программу: #include /* Определение функции: */ void quart(int n, float * x) { int i; for(i=0;i /* Присваивание после умножения: */ *(x+i)*=*(x+i); } void main() { /* Определение массива: */ float z[ ]={1.0, 2.0, 3.0, 4.0}; int j; quart(4,z); /* Обращение к функции */ /* Печать измененного массива */ for(j=0;j<4;j++) printf("\nz[%d]=%f",j,z[j]); } Результат выполнения программы: z[0]=1.000000 z[1[=4.000000 z[2]=9.000000 z[3]=16.000000 Чтобы еще раз обратить внимание на равноправие параметров в виде массива и указателя того же типа, отметим, что заголовок функции в нашей программе может быть и таким: void quart (int n, float x [ ]) В теле функции разыменовано выражение, содержащее имя массива-параметра, то есть вместо индексированной переменной x[i] используется *(x+i). Эту возможность мы уже неоднократно отмечали. Более интересная возможность состоит в том, что можно изменять внутри тела функции значение указателя на массив, то есть в теле цикла записать, например, такой оператор: *x*=*x++; Обратите внимание, что неверным для нашей задачи будет следующий вариант этого оператора: *x++*=*x++; /*ошибка!*/ В этом ошибочном случае «смещение» указателя x вдоль массива будет при каждой итерации цикла не на один элемент массива, а на 2, так как х изменяется и в левом, и в правом операндах операции присваивания. Следует отметить, что имя массива внутри тела функции не воспринимается как константный (не допускающий изменений) указатель, однако такая возможность отсутствует в основной программе, где определен соответствующий массив-параметр. Если в цикле основной программы вместо значения z[j] попытаться использовать в функции printf( ) выражение *z++, то получим сообщение об ошибке, то есть следующие операторы не верны: for (j=0; j<4; j++) printf(“\nz[%d]=%f”, j, *z++); Сообщение об ошибке при компиляции выглядит так: Error. . . Lvalue required Подводя итоги, отметим, что, с одной стороны, имя массива является константным указателем со значением, равным адресу нулевого элемента массива. С другой стороны, имя массива, использованное в качестве параметра, играет в теле функции роль обычного (неконстантного) указателя, то есть может использоваться в качестве леводопустимого выражения. Именно поэтому в спецификациях параметров эквивалентны, как указано выше, например, такие формы: double x[ ] и double *x Строки как параметры функций. Строки в качестве параметров могут быть специфицированы либо как одномерные массивы типа char [ ], либо как указатели типа char *. В обоих случаях с помощью параметра в функцию передается адрес начала символьного массива, содержащего строку. В отличие от обычных массивов, для параметров-строк нет необходимости явно указывать их длину. Терминальный символ '\0', размещаемый в конце каждой строки, позволяет всегда определить ее длину, точнее, позволяет перебирать символы строки и не выйти за ее пределы. Как примеры использования строк в качестве параметров функций рассмотрим несколько функций, решающих типовые задачи обработки строк. Аналоги большинства из приводимых ниже функций имеются в библиотеке стандартных функций (см. приложение 3). Их прототипы и другие средства связи с этими функциями находятся в заголовочных файлах string.h и stdlib.h. Однако для целей темы, посвященной использованию строк в качестве параметров, удобно заглянуть «внутрь» таких функций. Итак, в иллюстративных целях приведем определения функций для решения некоторых типовых задач обработки строк. Будем для полноты картины использовать различные способы задания строк- параметров. Функция вычисления длины строки (в стандартной библиотеке ей соответствует функция strlen( ), см. приложение 3): int len (char e[ ]) { int m; for (m=0; e[m]!='\0'; m++); return m; } В этом примере и в заголовке, и в теле функции нет даже упоминаний о родстве массивов и указателей. Однако компилятор всегда воспринимает массив как указатель на его начало, а индекс - как смещение относительно начала массива. Следующий вариант той же функции явно реализует механизм работы с указателями: int len (char *s) { int m; for (m=0; *s++!='\0'; m++) return m; } Для параметра-указателя s внутри функции выделяется участок памяти, куда записывается значение аргумента. Так как s не константа, то значение этого указателя может изменяться. Именно поэтому допустимо выражение s++. Функция инвертирования строки-аргумента с параметром- массивом: void invert(char e[ ]) { char s; int i, j, m; /*m - номер позиции символа '\0' в строке е */ for (m=0; e[m]!='\0'; m++); for (i=0, j=m-1; i { s=e[i]; e[i]=e[j]; e[j]=s; } } В определении функции invert( ) с помощью ключевого слова void указано, что функция не возвращает значения. В качестве упражнения можно переписать функцию invert( ), заменив параметр-массив параметром-указателем типа char*. При выполнении функции invert( ) строка - параметр, например «сироп» превратится в строку «порис». При этом терминальный символ '\0' остается на своем месте в конце строки. Пример использования функции invert( ): #include { char ct[ ]="0123456789"; /* Прототип функции: */ void invert(char [ ]); /* Вызов функции: */ invert(ct); printf("\n%s",ct); } Результат выполнения программы: 9876543210 Функция поиска в строке ближайшего слева вхождения другой строки (в стандартной библиотеке имеется подобная функция strstr( ), см. приложение 3): /*Поиск строки СТ2 в строке СТ1 */ int index (char * СТ1, char * СТ2) { int i, j, m1, m2; /* Вычисляются m1 и m2 - длины строк */ for (m1=0; CT1[m1] !='\0'; m1++); for (m2=0; CT2[m2] !='\0'; m2++); if (m2>m1) return -1; for (i=0; i<=m1-m2; i++) { for (j=0; j if (CT2[j] !=CT1[i+j]) break; if (j==m2) return i; } /* Конец цикла по i */ return -1; } Функция index( ) возвращает номер позиции, начиная с которой CT2 полностью совпадает с частью строки CT1. Если строка CT2 не входит в CT1, то возвращается значение -1. Пример обращения к функции index( ): #include void main( ) { char C1[ ]="сумма масс"; /* Прототип функции: */ int index(char [ ], char [ ]); char C2[ ]="ма"; char C3[ ]="ам"; ргШТСДпДля %s индекс=%б", C2, index(C1, C2)); ргШТСДпДля %s индекс=%б", C3, index(C1, C3)); } Результат выполнения программы: Для ма индекс=3 Для ам индекс=-1 В функции index( ) параметры специфицированы как указатели на тип char, а в теле функции обращение к символам строк выполняется с помощью индексированных переменных. Вместо параметров- указателей подставляют в качестве аргументов имена символьных массивов С1[ ], С2[ ], С3[ ]. Никакой неточности здесь нет: к массивам допустимы обе формы обращения - и с помощью индексированных переменных, и с использованием разыменования указателей. Функция сравнения строк. Для сравнения двух строк можно написать следующую функцию (в стандартной библиотеке имеется близкая к этой функция strcmp( ), см. приложение 3): int row(char С1[ ], char С2[ ]) { int i,m1,m2; /* m1,m2 - длины строк C1,C2 */ for (m1=0;*(C1+m1)= '\0'; m1++); for (m2=0;*(C2+m2)= '\0'; m2++); if (m1!=m2) return -1; for (i=0; i if (*С1++ != *С2++) return (i+1); return 0; } В теле функции обращение к элементам массивов-строк реализовано через разыменование указателей. Функция row( ) возвращает: значение -1, если длины строк-аргументов C1, C2 различны; 0 - если все символы строк совпадают. Если длины строк одинаковы, но символы не совпадают, то возвращается порядковый номер (слева) первых несовпадающих символов. Особенность функции row( ) - спецификация параметров как массивов и обращение к элементам массивов внутри тела функции с помощью разыменования. При этом за счет операций С1++ и С2++ изменяются начальные «настройки» указателей на массивы. Одновременно в той же функции к тем же массивам-параметрам выполняется обращение и с помощью выражений *(С1+т1) и *(С2+т2), при вычислении которых значения C1 и C2 не меняются. Функция соединения строк. Следующая функция позволяет «присоединить» к первой строке-аргументу вторую строку-аргумент (в стандартной библиотеке есть подобная функция strncat( ), см. приложение 3): /* Соединение (конкатенация) двух строк: */ void conc(char *С1, char *С2)
*(C1+m+i)=*(C2+i); *(C1+m+i)='\0'; Результат возвращается как значение той строки, которая адресована аргументом, замещающим первый параметр C1. Обратите внимание на то, что при использовании функции conc( ) длина строки, заменяющей параметр C1, должна быть достаточной для приема результирующей строки. Функция выделения подстроки. Для выделения из строки, представленной параметром C1, фрагмента заданной длины (подстроки) можно предложить такую функцию: void substr(char *C1, char *C2, int n, int k) /* C1 - исходная строка */ /* C2 - выделяемая подстрока */ /* n - начало выделяемой подстроки */ /* k - длина выделяемой подстроки */ { int i,m; /* m - длина исходной строки */ for (m=0; C1[m]!='\0'; m++); if (n<0 || n>m || k<0 || k>m-n) { C2[0]='\0'; return; } for (i=n; i C2[i-n]=C1[i-1]; C2[i-n]='\0'; return; } Результат выполнения функции - адресованная параметром C2 строка из k символов, выделенных из строки, адресованной параметром C1, начиная с символа, имеющего номер n. При неверном сочетании значений аргументов возвращается пустая строка. Функция копирования содержимого строки. Так как в языке Си отсутствует оператор присваивания для строк, то полезно иметь специальную функцию, позволяющую «переносить» содержимое строки в другую строку (такая функция strcpy( ) есть в стандартной библиотеке, но она имеет другое возвращаемое значение): /* Копирование содержимого строки С2 в С1 */ void copy(char *C1, char *C2) /* С2 - оригинал, С1 - копия */ { int i; for (i=0; C2[i]!='\0'; i++) C1[i]=C2[i]; C1[i]='\0'; } Пример использования функции copy( ): #include void main() { char X[ ]="SIC TRANSIT GLORIA MUNDI!"; /* Прототип функции: */ void copy(char[ ], char[ ]); char B[100]; /* Обращение к функции: */ copy (B,X); printf("%s\n", B); } Результат выполнения тестовой программы: SIC TRANSIT GLORIA MUNDI! Другой вариант функции копирования строки: void copy(char C1[ ], char C2[ ]) { int i=0; do { C1[i] = C2[i]; } while (C1[i++]!='\0'); } В третьем варианте той же функции операция присваивания перенесена в выражение-условие продолжения цикла. Ранги операций требуют заключения выражения-присваивания в скобки: void copy(char *C1, char *C2) { int i=0; while ((C1[i] = C2[i]) != '\0') i++; } Так как ненулевое значение в выражении после while считается истинным, то явное сравнение с '\0' необязательно и возможно следующее упрощение функции: void copy(char * C1, char * C2) { int i=0; while (C1[i] = C2[i]) i++; } И наконец, наиболее короткий вариант: void copy (char * C1, char * C2) { while (*C1++ = *C2++); } В заключение параграфа продемонстрируем возможности библиотеки стандартных функций для задач обработки строк, точнее, перепишем функцию конкатенации строк, используя библиотечную функцию strlen( ), позволяющую вычислить длину строки-параметра. Функция для сцепления (конкатенации) строк: #include void concat(char * C1, char * C2) { int m, i; m=strlen (C1); i=0; while ((*(C1+m+i) = *(C2+i))!='\0') i++; } Вместо функции strlen( ) можно было бы использовать определенную выше функцию len( ). При обращении к функции concat( ), а также при использовании похожей на нее библиотечной функции strcat( ) нужно, чтобы длина строки, использованной в качестве первого параметра, была достаточной для размещения результирующей (объединенной) строки. В противном случае результат выполнения непредсказуем. Резюме по строкам-параметрам. Часть из приведенных функций для работы со строками, а именно функции: конкатенации строк conc( ), инвертирования строки invert( ), копирования одной строки в другую copy( ), выделения подстроки substr( ); - предусматривают изменение объектов вызывающей программы. Это возможно и допустимо только потому, что параметрами являются указатели. Это либо имена массивов, либо указатели на строки. Как уже говорилось, параметры-указатели позволяют функции получить непосредственный доступ к объектам той программы, из которой функция вызвана. В приведенных определениях функций этот доступ осуществляется как с помощью индексированных переменных, так и с помощью разыменования указателей. На самом деле компилятор языка Си, встречая, например, такое обращение к элементу массива s[i], всегда заменяет его выражением *(s+i), то есть указателем s со смещением i. Такую замену, то есть использование указателей со смещениями вместо индексированных переменных, можно явно использовать в определениях функций. Иногда это делает нагляднее связь между функцией и вызывающей ее программой. Указатели на функции |