В языке Си отдельного типа данных «строка символов» нет. Работа со строками реализована путем использования одномерных массивов типа char, т.е. строка символов – это одномерный массив символов, заканчивающийся нулевым байтом.
Нулевой байт – это байт, каждый бит которого равен нулю, при этом для нулевого байта определена символьная константа ´\0´ (признак окончания строки, или «нуль-символ»). Поэтому если строка должна содержать k символов, то в описании массива размер должен быть k +1. По положению нуль-символа определяется фактическая длина строки.
Например, char s [7]; – означает, что строка может содержать не более шести символов, а последний байт отводится под нуль-символ.
Отсутствие нуль-символа и выход указателя при просмотре строки за ее пределы – распространенная ошибка при работе со строками.
Строку можно инициализировать строковой константой (строковым литералом), которая представляет собой набор символов, заключенных в двойные кавычки. Например:
сhar S[ ] = “Работа со строками”;
для данной строки выделено и заполнено 19 байт – 18 на символы и 19-й на нуль-символ.
В конце строковой константы явно указывать символ ´\0´ не нужно. Компилятор добавит его автоматически.
Символ ´\0´ нужно использовать явно тогда, когда символьный массив при декларации инициализируется списком начальных значений, например, следующим образом:
char str[10] ={‘V’, ‘a’, ‘s’, ‘j’, ‘а’, ‘\0’};
или когда строка формируется посимвольно в коде программы. Пример такого формирования приведен в конце этого раздела.
При работе со строками можно пользоваться указателями, например:
char *x;
x = "БГУИР";
x = (i>0)? "положительное": (i<0)? "отрицательное": "нулевое";
Такая декларация строки – единственный случай, когда в коде программы можно использовать операцию присваивания явно.
Операция char *str = "БГУИР" создает не строковую переменную, а указатель на строковую константу, изменить которую невозможно, причем это касается не только адреса ОП, но и его размера. Знак равенства перед строковым литералом означает инициализацию, а не присваивание.
Операция присваивания одной строки другой в языке Си не определена (поскольку строка является массивом) и может обрабатываться при помощи оператора цикла (с использованием стандартной библиотечной функций).
Процесс копирования строки s 1 в строку s 2 имеет вид
char s1[25], s2[25];
for (int i = 0; i <= strlen(s1); i++)
s2[i] = s1[i];
Длина строки определяется с помощью стандартной функции strlen, которая вычисляет длину, выполняя поиск нуль-символа (прототип функции приведен ниже). Таким образом, строка фактически просматривается дважды.
А вот следующие действия будут ошибочными:
сhar s1[51];
s1 = ”Minsk”;
Это связано с тем, что s 1 – константный указатель и не может использоваться в левой части операции присваивания.
Большинство действий со строковыми объектами в Си выполняются при помощи стандартных библиотечных функций, так, для правильного выполнения операции присваивания в последнем примере необходимо использовать стандартную функцию
strcpy (s1, ”Minsk”);
Напомним, что для ввода строк, как и для других объектов программы, обычно используются две стандартные функции:
Функция scanf вводит значения для строковых переменных при помощи формата (спецификатора ввода) % s до появления первого символа “пробел” (символ «&» перед ID строковых данных указывать не надо);
Функция gets осуществляет ввод строки, которая может содержать пробелы. Завершается ввод нажатием клавиши Enter.
Обе функции автоматически ставят в конец строки нулевой байт.
Вывод строк производится функциями printf или puts до нулевого байта.
Функция printf не переводит курсор после вывода на начало новой строки, а puts автоматически переводит курсор после вывода строковой информации в начало новой строки. Например:
char Str[30];
printf(“ Введите строку без пробелов: \n”);
scanf(“%s”, Str);
или
puts(“ Введите строку ”);
gets(Str);
Остальные операции над строками, как уже отмечалось ранее, выполняются с использованием стандартных библиотечных функций, декларация прототипов которых находятся в файле string.h.
Приведем наиболее часто используемые стандартные строковые функции.
Функция strlen (S) возвращает длину строки (количество символов в строке), при этом завершающий нулевой байт не учитывается, например:
char *S1 = ”Минск!\0”, S2[] = ”БГУИР–Ура!”;
printf(“ %d, %d.”, strlen(S1), strlen(S2));
Результат выполнения данного участка программы:
6, 10.
Функция strcpy (S 1, S 2) – копирует содержимое строки S 2 в строку S 1.
Функция strcat (S 1, S 2) – присоединяет строку S 2 к строке S 1 и помещает ее в массив, где находилась строка S 1, при этом строка S 2 не изменяется. Нулевой байт, который завершал строку S 1, заменяется первым символом строки S2.
Функция int strcmp (S 1, S 2) сравнивает строки S 1 и S 2 и возвращает значение <0, если S 1< S 2; >0, если S 1> S 2; =0, если строки равны, т.е. содержат одно и то же число одинаковых символов.
Функции преобразования строковых объектов в числовые описаны в библиотеке stdlib. h. Рассмотрим некоторые из них.
Преобразование строки S в число:
– целое: int atoi (S);
– длинное целое: long atol (S);
– действительное: double atof (S);
при возникновении ошибки данные функции возвращают значение 0.
Функции преобразования числа V в строку S:
– целое: itoa (V, S, kod);
– длинное целое: ltoa (V, S, kod);
2 £ kod £ 36, для десятичных чисел со знаком kod = 10.
Пример участка кода программы, в котором из строки s удаляется символ, значение которого содержится в переменной с каждый раз, когда он встречается
char s[81], c;
...
for(i = j = 0; s[i]!= '\0'; i++)
if(s[i]!= c) s[j++] = s[i];
s[j]='\0';
...
__________________________________________________________________
В режиме консольных приложений в среде Visual C++ 6.0 вывод символов русского языка сопряжен с определенными неудобствами. Разрешение данной проблемы рассматривается в разд. 16.3.
__________________________________________________________________
Указатели на указатели
Указатели, как и переменные любого другого типа, могут объединяться в массивы.
Объявление массива указателей на целые числа имеет вид
int *a[10], y;
Теперь каждому из элементов массива указателей a можно присвоить адрес целочисленной переменной y, например: a [1]=& y;
Чтобы теперь найти значение переменной y через данный элемент массива а, необходимо записать * a [1].
В языке Си можно описать переменную типа «указатель на указатель». Это ячейка оперативной памяти (переменная), в которой будет храниться адрес указателя на некоторую переменную. Признак такого типа данных – повторение символа «*» перед идентификатором переменной. Количество символов «*» определяет уровень вложенности указателей друг в друга. При объявлении указателей на указатели возможна их одновременная инициализация. Например:
int a=5;
int *p1=&a;
int **pp1=&p1;
int ***ppp1=&pp1;
Если присвоить переменной а новое значение, например 10, то одинаковые результаты будут получены в следующих операциях:
a=10; *p1=10; **pp1=10; ***ppp1=10;
Для доступа к области ОП, отведенной под переменную а, можно использовать и индексы. Эквивалентны следующие выражения:
*p1 ~ p1[0];
**pp1 ~ pp1[0][0];
***ppp1 ~ ppp1[0][0][0].
Фактически, используя указатели на указатели, мы имеем дело с многомерными массивами.
Многомерные массивы
Декларация многомерного массива имеет следующий формат:
тип ID [ размер 1][ размер 2]…[ размерN ] =
{ { список начальных значений },
{ список начальных значений },
…
};
Списки начальных значений – атрибут необязательный.
Наиболее быстро изменяется последний индекс элементов массива, поскольку многомерные массивы в языке Си размещаются в памяти компьютера построчно друг за другом (см. следующую тему «Адресная функция»).
Рассмотрим особенности работы с многомерными массивами на конкретном примере двухмерного массива.
Например, пусть приведена следующая декларация двухмерного массива:
int m [3][4];
Идентификатор двухмерного массива – это указатель на массив указателей (переменная типа указатель на указатель: int ** m;).
Поэтому двухмерный массив m [3][4]; компилятор рассматривает как массив трех указателей, каждый из которых указывает на начало массива со значениями размером по четыре элемента каждый. В ОП данный массив будет расположен следующим образом:
Указа-тели | m [0] | ® | m [0][0] | m [0][1] | m [0][2] | m [0][3] | |
m [1] | m [1][0] | m [1][1] | m [1][2] | m [1][3] | |||
m [2] | m [2][0] | m [2][1] | m [2][2] | m [2][3] |
(А) (В)
Рис. 10.1. Схема размещения элементов массива m размером 3×4
Причем в данном случае указатель m [1] будет иметь адрес m [0]+4* sizeof (int), т.е. каждый первый элемент следующей строки располагается за последним элементом предыдущей строки.
Приведем пример программы конструирования массива массивов:
#include <stdio.h>
void main()
{
int x0[4] = { 1, 2, 3,4}; // Декларация и инициализация
int x1[4] = {11,12,13,14}; // одномерных массивов
int x2[4] = {21,22,23,24};
int *m[3] = {x0, x1, x2,}; // Создание массива указателей
int i,j;
for (i=0; i<3; i++) {
printf("\n Cтрока %d) ", i+1);
for (j=0; j<4; j++)
printf("%3d", m[ i ] [ j ]);
}
}
Результаты работы программы:
Cтрока 1) 1 2 3 4
Cтрока 2) 11 12 13 14
Cтрока 3) 21 22 23 24
Такие же результаты будут получены и в следующей программе:
#include <stdio.h>
void main()
{
int i, j;
int m[3][4] = { { 1, 2, 3, 4}, {11,12,13,14}, {21,22,23,24} };
for (i=0; i<3; i++) {
printf("\n %2d)", i+1);
for (j=0; j<4; j++)
printf(" %3d",m[ i ] [ j ]);
}
}
В последней программе массив указателей на соответствующие массивы элементов создается компилятором автоматически, т.е. данные массива располагаются в памяти последовательно по строкам, что является основанием для декларации массива m в виде
int m[3][4] = {1, 2, 3, 4, 11, 12, 13, 14, 21, 22, 23, 24};
Замена скобочного выражения m [3][4] на m [12] здесь не допускается, так как массив указателей не будет создан.
Таким образом, использование многомерных массивов в языке Си связано с расходами памяти на создание массивов указателей.
Очевидна и схема размещения такого массива в памяти – последовательное (друг за другом) размещение «строк» – одномерных массивов со значениями (векторная организация памяти).
Обращению к элементам массива при помощи операции индексации m [ i ][ j ] соответствует эквивалентное выражение, использующее адресную арифметику – *(*(m + i)+ j).
Аналогичным образом можно установить соответствие между указателями и массивами с произвольным числом измерений.
Адресная функция
Векторная память поддерживается почти всеми языками высокого уровня и предназначена для хранения массивов различной размерности и различных размеров. Каждому массиву выделяется непрерывный участок памяти указанного размера. При этом элементы, например, двухмерного массива X размерностью n 1´ n 2 размещаются в ОП в следующей последовательности:
Х (0,0), Х (0,1), Х (0,2),... Х (0, n 2–1),..., Х (1,0), Х (1,1), Х (1,2),... Х (1, n 2–1),..., Х (n 1–1,0), Х (n 1–1,1), Х (n 1–1,2),..., Х (n 1–1, n 2–1).
Адресация элементов массива определяется некоторой адресной функцией, связывающей адрес и индексы элемента.
Пример адресной функции для массива Х:
K (i, j) = n 2* i + j;
где i = 0,1,2,...,(n 1–1); j = 0,1,2,...,(n 2–1); j – изменяется в первую очередь.
Адресная функция двухмерного массива A (n, m) будет выглядеть так:
N 1 = K (i, j) = m * i + j,
i =0,1,..., n –1; j =0,1,..., m –1.
Тогда справедливо следующее:
A (i, j) «B (K (i, j)) = B (N 1),
B – одномерный массив с размером N 1 = n * m.
Например, для двухмерного массива A (2,3) имеем:
(0,0) | (0,1) | (0,2) | (1,0) | (1,1) | (1,2) | – индексы массива А; |
– индексы массива В. |
Проведем расчеты:
i = 0, j = 0 N 1 = 3*0+0 = 0 B (0)
i = 0, j = 1 N 1 = 3*0+1 = 1 B (1)
i = 0, j = 2 N 1 = 3*0+2 = 2 B (2)
i = 1, j = 0 N 1 = 3*1+0 = 3 B (3)
i = 1, j = 1 N 1 = 3*1+1 = 4 B (4)
i = 1, j = 2 N 1 = 3*1+2 = 5 B (5)
Аналогично получаем адресную функцию для трехмерного массива Х (n 1, n 2, n 3):
K (i, j, k) = n 3* n 2* i + n 2* j + k,
где i = 0,1,2,...,(n 1–1); j = 0,1,2,...,(n 2–1);); k = 0,1,2,...,(n 3–1); значение k – изменяется в первую очередь.
Для размещения такого массива потребуется участок ОП размером (n 1* n 2* n 3)* sizeof (type). Рассматривая такую область как одномерный массив Y (0,1,..., n 1* n 2* n 3), можно установить соответствие между элементом трехмерного массива X и элементом одномерного массива Y:
X (i, j, k) «Y (K (i, j, k)).
Необходимость введения адресных функций возникает лишь в случаях, когда требуется изменить способ отображения с учетом особенностей конкретной задачи.