Строки как одномерные массивы данных типа 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 размерностью nn2 размещаются в ОП в следующей последовательности:

Х(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)) .

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