Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Метод_материалы / Учебники / Программирование_С

.pdf
Скачиваний:
66
Добавлен:
16.03.2016
Размер:
2.31 Mб
Скачать

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

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

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

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

Такими функциями являются многие библиотечные функции, которые требуют выделения памяти под вновь созданный объект, и, как правило, возвращают вновь созданное значение. Например, многие функции работы со строками текста библиотеки <string.h>, многие функции работы с динамической памятью библиотеки <alloc.h>, многие функции ввода данных библиотек

<stdio.h> и <stdlib.h> и другие.

Пример функции, возвращающей указатель: // пример для простой переменной

int * func (int n)

 

{

 

int * a;

// объявлен указатель,

*a = n;

// его значение равно значению параметра n,

+eturn a;

// возвращается адрес

}

 

void main (void)

 

{

 

int b;

 

b = *func (12);

// простая переменная b получает значение

printf("%d", b);

// возвращенного функцией адреса

 

int *d;

 

d = func (12);

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

printf ("%d", *d);

// который вернула функция

 

}

1.3.4.6.4. Указатели на функции Оператор-выражение для вызова функции имеет вид: Имя_функции (список_фактических_параметров);

91

Здесь в качестве имени до сих пор мы использовали идентификатор, а на самом деле Имя_функции — это указатель на функцию, возвращающую значение конкретного типа. В соответствии с синтаксисом С указатель на функцию — это выражение или переменная, используемые для представления адреса функции. Указатель на функцию содержит адрес первого байта или первого слова ее исполнимого кода (арифметические операции над указателями на функции запрещены).

Самый употребительный указатель на функцию — это ее имя (идентификатор), именно так указатель на функцию вводится в ее описании и в прототипе:

тип Имя_функции (список_формальных_параметров)

{

тело_функции

}

Имя_функции в ее описании и в прототипе — константный указатель. Он навсегда связан с описываемой функцией и не может быть изменен.

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

тип (*Имя_указателя) (список_формальных_параметров); Здесь все как обычно, кроме (*Имя_указателя) — это произвольный

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

Например, запись

int (*Function) (void);

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

Круглые скобки обязательны в определении указателя на функции. Запись int * Function (void) ;

будет не определением указателя, а прототипом функции без параметров с именем Function, возвращающей значения типа int*.

Таким образом, при обращении к функции можно использовать:

имя_функции,

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

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

void fl (void)

{

printf ("\n Функция fl( ) ") ;

}

92

void f2 (void)

{

printf ("\n Функция f2( )");

)

void main (void)

{

void (*Function) (void);

// Function – переменная указатель на функцию

f2 ();

// явный вызов функции f2()

Function = f2;

// настройка указателя Function на f2()

*Function) ();

// вызов f2() по адресу с разыменованием указателя

Function = f1();

// настройка указателя Function на f1()

*Function) () ;

// вызов fl() по адресу с разыменованием указателя

Function ();

// вызов fl() без разыменования указателя

}

 

Результат выполнения программы: Функция f2 ()

Функция f2 () Функция f1 () Функция f1 ()

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

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

int F_char (char);

int (*pchar) (char) = F_char;

Массивы указателей на функции.

По смыслу не отличаются от массивов других объектов, их описание: тип (*Имя_массива [размер]) (список_формальных_параметров); Например:

int (*pchar [4]) (char);

Здесь pchar – массив указателей на функции, каждому из которых можно присвоить адрес определенной выше функции int F_char (char), и адрес любой функции с прототипом вида

int Имя_функции (char);

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

93

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

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

Например:

void Graph (float (*Function)(float),float x0, float xn)

{

// построение графика функции Function от x0 до xn

}

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

float f1(float x)

{

return sin(x)+cos(x);

}

float f2(float x)

{

return pow(exp(x),–sin(x);

}

void main(void)

{

//построение графика первой функции

Graph (f1, –M_PI, M_PI);

//построение графика второй функции

Graph (f2, 0., 2*M_PI);

}

1.3.5. Обработка текстовой информации

Текстовая информация — это символы (константы), переменные символьного типа (char) и строки, для которых специального типа данных нет.

1.3.5.1. Символьные константы и символьные переменные

Символьная константа, это лексема, которая состоит из изображения символа и ограничивающих апострофов, например: '*' '?' 'f' 'Я' '~' '#' '1'. Внутри апострофов записывается любой символ, изображаемый на экране в текстовом режиме.

94

Для объявления объектов символьного типа используются ключевые слова char или unsigned char.

Данные символьного типа занимают один байт. Внутреннее представление символьного данного — это его целочисленный код. Для кодирования символов используется код ASCII (число в пределах от 0 до 256). Первые 32 символа управляющие. Они не имеют графического отображения на экране, а для их представления в тексте программы требуется два символа, первый из которых слэш: '\n', '\\' и т.д.

Такое представление позволяет обращаться к символам как к числовым величинам, что дает некоторые преимущества, а именно:

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

char c1, c2;

// функция getchar получает символ с клавиатуры

c1 = getchar();

c2 = getchar();

 

// любую

операцию отношения можно использовать для сравнения

int k = c1

= = c2;

// k = 1, если символы равны, k = 0, если нет

k = c1 > c2;

// k = 1 , если код с1 больше кода с2, иначе 0

//для проверки условия «символ с1 является цифрой», можно

//использовать то, что коды цифр упорядочены по возрастанию: k = (c1 >= '0' && c1 <= '9')

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

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

например, перебор в алфавитном порядке: char c;

for (c = 'a'; c <= 'z'; c++)

{

...// например, сравнение символа со значениями с

}

Здесь символьная переменная c является операндом арифметической операции ++, выполняемой над значением ее внутреннего кода.

Коды символов иногда знать необходимо. Например, внешнее управление программой реализуется с использованием стандартных клавиш, например, стрелок, Enter, Esc, F1–F12 и пр. Для того, чтобы узнать код клавиши, можно использовать функцию bioskey библиотеки <bios.h>. Эта функция ожидает ввод с клавиатуры значения символа, и имеет три способа обращения. Если фактический параметр равен 1, функция просто ожидает нажатия любой клавиши, что используется для организации бесконечного цикла ожидания события. Если фактический параметр равен 0, функция читает символ, и если равен 2, то функция читает символ с определением регистра (нажата ли клавиша Shift).

95

Пример:

 

#include <bios.h>

// для bioskey

#define ESC 0x11b

// 16-ричный код клавиши Esc

void main(void)

{

int key;

printf ("Press any key…"\n);

while (bioskey(1) – – 0);

// ждет нажатия

do

 

{

 

key = bioskey(0);

// читает значение и анализирует его

printf ("Шестнадцатеричный код %#0x\n", key);

}

 

while (key != ESC);

// обрабатывается событие «Нажатие Esc»

}

1.3.5.2. Строковые переменные и строковые константы

Формально строки не относятся к константам языка С+, а представляют отдельный тип его лексем. Строковая константа, это последовательность символов, заключенная в двойные кавычки " ".

Управляющие символы (Esc-последовательности) входят в состав текстовой строки, вызывая управляющее воздействие.

Примеры символьных констант: "Cтрока символов",

"cлэш запишем как \\ , новую строку как \n",

" ""апостроф"" повторяем дважды."

Внутри длинных строк комбинация \n означает переход на новую строку. 1.3.5.2.1. Механизм представления строки

Строка символов рассматривается как массив символов (типа char[]). Каждый символ хранится в отдельном байте, включая специальные символы. В конце строки должен быть нулевой байт '\0', как признак конца строки. Добавляется автоматически при инициализации строки, при вводе строки. Общее число символов в строке определяется с учетом нулевого символа, и равно «длина строки +1». При формировании строки вручную необходимо заботиться о том, чтобы этот символ был добавлен. Так, длина константы 'W' равна одному байту, а длина строки "W" равна двум байтам.

Операции с массивом в С не допускаются, следовательно, и для строк в целом операций нет. Символьная строка в выражении — это адрес массива символов. Указатель на строку может быть использован везде, где можно использовать указатель. Конечно, не в левой части присваивания.

96

1.3.5.2.2. Объявление и инициализация строковых переменных Объявить строковую переменную можно двумя способами.

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

достаточно расплывчатым.

 

char Str [80];

// длина не более 79 символов

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

char * Str;

 

Str = new char [80];

// как видим, длина строки

 

// и здесь определена числом

При инициализации, независимо от способа объявления строки, всегда происходит выделение памяти под запись значений строки.

char

*Str1

= "строка 1";

// длина строки 9

байт

char

Str2[9]= "строка 2";

// длина строки 9

байт, как объявлено

char

Str1[]

= "строка 3";

// специальный инициализатор

char

Err[4]

= "ошибка";

// длина строки больше, чем объявлено

char

Err[10]

= "не ошибка";

// длина строки меньше, чем объявлено,

//остальные символы заполнены нулями

1.3.5.3.Ввод-вывод символов и текстовых строк.

Для ввода и вывода текстовой информации инструментов довольно много, это функции библиотеки <stdio.h>.

1. Форматированный ввод-вывод символов и строк.

Используются функции форматированного ввода printf, scanf. Управляющие форматы для символов %c, для строк %s. При вводе строка вводится только до первого пробела.

2. Ввод-вывод символов.

Используются функции getchar, putchar. Функция getchar() читает из входного потока по одному символу за обращение. Чтение данных начинается после нажатия клавиши Enter. Функция putchar(символ) выводит символьное значение в стандартный выходной поток, обычно это экран.

3. Ввод-вывод строк.

Используются функции gets, puts. Функция gets(строка) читает из входного потока последовательность символов до Enter. Чтение начинается после нажатия клавиши. Функция puts(строка) выводит строку в стандартный выходной поток.

Прототипы функций:

 

char *gets (char *Str);

// вернет NULL при ошибке

int puts (const char * Str)

// вернет EOF при ошибке

Пример.

 

#include <stdio.h>

 

void main(void)

 

97

{

char Str[80];

printf ("Введите строку\n"); gets (Str);

printf("Введена строка:\n"); puts (Str);

}

Следующий пример покажет, как можно вводить строки при работе с текстом как с массивом строк.

#include <stdio.h> void main(void)

{

char text [3][20];

for (int i = 0; i < 3; i++) gets (text[i]);

}

1.3.5.4. Строки и указатели.

Особенности строк связаны с представлением их в памяти как массивов однобайтовых переменных. Соотношение строк и указателей такое же, как соотношение строк и массивов, но в конце строки есть нулевой байт '\0'. Для статических строк память выделяется на этапе компиляции программы, для динамических строк при выполнении программы, но в любом случае массив должен где-то разместиться. Пусть есть два описания:

char

Str1[20] ;

// массив, в который можно записать строку

char

*Str2;

// указатель, с которым можно связать строку

Для статического массива Str1 память будет выделена автоматически при обработке описания компилятором. Строке, с которой можно связать указатель * Str2, память не выделяется, поэтому, если далее следуют операторы:

gets (Str1);

// ошибки нет

gets (Str2);

// ошибка

то первый из них допустим, а второй приведет к ошибке времени выполнения программы, так как gets вводит строку в участок памяти с неизвестным адресом. Перед использованием строки следует связать адрес области памяти, в котором может разместиться строка, с указателем Str2. Во-первых, переменной Str2 можно присвоить адрес уже определенного символьного массива. Во-вторых, можно выделить динамическую память в требуемом размере с помощью операции new, а затем полученный адрес связать с указателем Str2. Например:

Str2

= Str1;

// указатель Str2 адресует строку Str1

Str2

= new char [80];

98

Во втором случае выделен блок памяти 80 байт, и связан с указателем Str2. Строка не имеет собственного имени, а имя указателя Str2 является его синонимом.

Символьная строка, встреченная в выражении (например, Str1), это адрес массива символов. Может встречаться везде, где можно использовать указатель. Для доступа к отдельным символам строк используются обычные механизмы прямой или косвенной адресации.

Для прямой адресации используются индексы элементов массива. Выделим динамическую память для строки Str2, и выполним копирование в нее исходной строки Str1 в цикле do, управляемом индексом. Реальное число символов в исходной строке, чаще всего, неизвестно, поэтому используется цикл с выходом по событию «достигнут конец строки». Нулевой символ должен быть перенесен в новую строку:

char

Str1[80] = "Первая строка.";

char

* Str2;

Str2 = new char [80];

//оператор do выполняет посимвольное копирование,

//перенесет нулевой байт в новую строку

i = 0;

 

do

 

Str2[i] = Str1[i];

 

while ( Str1[i ++] !='\0');

// проверка достижения конца строки

puts (Str2);

 

Для косвенной адресации используются указатели типа char * в качестве переменных, управляющих обработкой строки. Текущий указатель будет адресовать ровно один символ строки, но в отладчике будет видна оставшаяся часть строки до нулевого байта '\0'. Покажем на том же примере.

char *pts1, *pts2;

Это рабочие переменные для косвенной адресации. Управляет циклом переменная pts1. Ее начальное значение равно адресу исходной строки Str1. *pts1, это значение очередного символа строки, pts++ смещает указатель. Выход из цикла происходит, когда найден признак конца строки:

char

Str1[80] = "Первая строка.";

char

* Str2 = new char [80];

char

*pts1, *pts2;

pts1 = Str1;

// подготовка цикла, это настройка указателей

pts2 = Str2;

 

do

// сначала присваивание, затем смещение

*pts2 ++ = *pts1;

while (*pts1 ++ != '\0');

 

puts (Str2);

 

В стиле C++ этот цикл выглядит так: pts1 = Str1;

pts2 = Str2;

99

while (( *pts2++ = *pts1++)); puts(Str2);

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

В примере описан и проинициализирован одномерный массив Rainbow[ ] указателей типа char*, каждый элемент которого — строка. Для вывода строк используется функция puts(), но и функция printf() со спецификатором преобразования %s допускает использование указателя на строку в качестве параметра. В выходной поток выводится не значение указателя Rainbow[i], а содержимое адресуемой им строки:

#include <stdio.h> void main (void)

{

//объявлен массив строк, каждое слово – название цвета радуги

//память выделена при инициализации

char * Rainbow[ ] = {"Красный", "Оранжевый", "Желтый", "Зеленый", "Голубой", "Синий", "Фиолетовый"};

int i, len ;

len = sizeof (Rainbow) / sizeof (Rainbow [0]) ; // определена длина

// массива строк.

for (i == 0; i < len; i++) puts (Rainbow [i]) ;

}

В результате выполнения программы мы увидим на экране названия цветов радуги:

Красный

Оранжевый и так далее.

1.3.5.5. Строки и функции

Когда строка является параметром функции, то передача строки в функцию выполняется так же, как и для массивов. Строка в качестве формального параметра в заголовке функции описывается как char Имя_строки[] или char * Имя_строки. Строка в качестве фактического параметра в вызывающей программе может быть описана как одномерный массив типа char — char Имя[Константа], или как указатель типа char * — char * Имя = new char [80].

100

Соседние файлы в папке Учебники