Строки как одномерные массивы данных типа char
В языке Си отдельного типа данных «строка символов» нет. Работа со строками реализована путем использования одномерных массивов типа 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 = "БГУИР" создает не строковую переменную, а указатель на строковую константу, изменить которую невозможно, причем это касается не только адреса ОП, но и его размера. Знак равенства перед строковым литералом означает инициализацию, а не присваивание.
Операция присваивания одной строки другой в языке Си не определена (поскольку строка является массивом) и может обрабатываться при помощи оператора цикла (с использованием стандартной библиотечной функций).
Процесс копирования строки s1 в строку s2 имеет вид
char s1[25], s2[25];
for (int i = 0; i <= strlen(s1); i++)
s2[i] = s1[i];
Длина строки определяется с помощью стандартной функции strlen, которая вычисляет длину, выполняя поиск нуль-символа (прототип функции приведен ниже). Таким образом, строка фактически просматривается дважды.
А вот следующие действия будут ошибочными:
сhar s1[51];
s1 = ”Minsk”;
Это связано с тем, что s1 – константный указатель и не может использоваться в левой части операции присваивания.
Большинство действий со строковыми объектами в Си выполняются при помощи стандартных библиотечных функций, так, для правильного выполнения операции присваивания в последнем примере необходимо использовать стандартную функцию
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(S1, S2) – копирует содержимое строки S2 в строку S1.
Функция strcat(S1, S2) – присоединяет строку S2 к строке S1 и помещает ее в массив, где находилась строка S1, при этом строка S2 не изменяется. Нулевой байт, который завершал строку S1, заменяется первым символом строки S2.
Функция int strcmp(S1, S2) сравнивает строки S1 и S2 и возвращает значение <0, если S1<S2; >0, если S1>S2; =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 размерностью n1´n2 размещаются в ОП в следующей последовательности:
Х(0,0), Х(0,1), Х(0,2),... Х(0, n2–1), ..., Х(1,0), Х(1,1), Х(1,2),... Х(1, n2–1), ..., Х(n1–1,0), Х(n1–1,1), Х(n1–1,2), ..., Х(n1–1, n2–1).
Адресация элементов массива определяется некоторой адресной функцией, связывающей адрес и индексы элемента.
Пример адресной функции для массива Х:
K(i, j) = n2*i + j;
где i = 0,1,2,... ,(n1–1); j = 0,1,2,... ,(n2–1); j – изменяется в первую очередь.
Адресная функция двухмерного массива A(n,m) будет выглядеть так:
N1 = K(i, j) = m*i + j,
i=0,1,..., n–1; j=0,1,... , m–1 .
Тогда справедливо следующее:
A(i, j) « B(K(i, j)) = B(N1),
B – одномерный массив с размером N1 = n*m.
Например, для двухмерного массива A(2,3) имеем:
(0,0) | (0,1) | (0,2) | (1,0) | (1,1) | (1,2) | – индексы массива А; |
– индексы массива В. |
Проведем расчеты:
i = 0, j = 0 N1 = 3*0+0 = 0 B(0)
i = 0, j = 1 N1 = 3*0+1 = 1 B(1)
i = 0, j = 2 N1 = 3*0+2 = 2 B(2)
i = 1, j = 0 N1 = 3*1+0 = 3 B(3)
i = 1, j = 1 N1 = 3*1+1 = 4 B(4)
i = 1, j = 2 N1 = 3*1+2 = 5 B(5)
Аналогично получаем адресную функцию для трехмерного массива Х(n1, n2, n3):
K(i, j, k) = n3*n2*i + n2*j + k ,
где i = 0,1,2,... ,(n1–1); j = 0,1,2,... ,(n2–1); ); k = 0,1,2,... ,(n3–1); значение k – изменяется в первую очередь.
Для размещения такого массива потребуется участок ОП размером (n1*n2*n3)*sizeof(type). Рассматривая такую область как одномерный массив Y(0,1,..., n1*n2*n3), можно установить соответствие между элементом трехмерного массива X и элементом одномерного массива Y:
X(i, j, k) « Y(K(i, j, k)) .
Необходимость введения адресных функций возникает лишь в случаях, когда требуется изменить способ отображения с учетом особенностей конкретной задачи.