Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Билеты по проге.docx
Скачиваний:
5
Добавлен:
01.07.2025
Размер:
630.61 Кб
Скачать

Локальные и глобальные переменные

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

Локальные переменные

переменные, объявленные внутри функции. Их можно использовать только внутри блока, где они объявлены. За его границами лок.пер. не видны. Они существуют только при входе в блок и умирают при выходе из него. void f1 ( ){ int x; x=1; } void f2 ( ){ int x; x=99; } - объявленные переменные в этих процедурах никак не связаны друг с другом.

Так же локальную переменную можно объявить так: void f ( ) { int t; if (t==1) { int z; ……….. } /* Здесь переменная Z уже не видна! */ } Если же так объявить переменные с одинаковым названием, то переменная внутреннего блока спрячет переменную внешнего: void f ( ) { int t=1; if (t==1) { int t; t=99; /* Здесь t равна 99 */ } /* Здесь t равна 1*/ }

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

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

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

Локальной переменной является любой параметр функции(например, в языке Си), что не использует ссылок или указателей.

Глобальные переменные

Область видимости - обозначает область программы, в пределах которой идентификатор (имя) некоторой переменной продолжает быть связанным с этой переменной и возвращать её значение. За пределами области видимости тот же самый идентификатор может быть связан с другой переменной, либо быть свободным (не связанным ни с какой из них).

В отличие от локальных, глобальные переменные видимы и могут использоваться в любом месте программы. Они сохраняют свое значение на протяжении всей работы программы. Чтобы создать глобальную переменную, ее необходимо объявить за пределами функций в любой части программы, но до ее первого использования. Глобальные переменные хранятся в отдельной фиксированной области памяти, созданной компилятором специально для этого. Глобальные переменные используются в тех случаях, когда разные функции программы используют одни и те же данные. Глобальные переменные могут использоваться для взаимодействия между процедурами и функциями как альтернатива передачи аргументов и возвращения значений Недостатки глобальных переменных: 1) Занимают память в течение всего времени выполнения программы, а не только тогда, когда они необходимы. 2) Использование глобальной переменной делает функцию менее универсальной, потому что в этом случае функция использует нечто, определенное вне ее. 3) Большое количество глобальных переменных легко приводит к ошибкам в программе из-за нежелательных побочных эффектов. При увеличении размера программы серьезной проблемой становится случайное изменение значения переменной где-то в другой части программы, а когда глобальных переменных много, предотвратить это очень трудно.

4) Неконтролируемые изменения – т.к. переменная видна везде, невозможно отследить, что ее изменяет. Она может быть изменена в любой точке программы ,что может повлиять на работу других частей.

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

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

Билет 8. Массивы и указатели в СИ, связь между ними. Передача массивов и указателей как параметров процедур.

В языке С нельзя передать весь массив как аргумент функции. Однако можно передать указатель на массив, т.е. имя массива без индекса. Например, в представленной программе в func1() передается указатель на массив i:

int main(void)

{

int i[10];

func1(i);

/* ... */

}

Если в функцию передается указатель на одномерный массив, то в самой функции его можно объявить одним из трех вариантов: как указатель, как массив определенного размера и как массив без определенного размера. Например, чтобы функция func1() получила доступ к значениям, хранящимся в массиве i, она может быть объявлена как

void func1(int *x) /* указатель */

{

/* ... */

}

или как

void func1(int x[10]) /* массив определенного размера */

{

/* ... */

}

и наконец как

void func1(int x[]) /* массив без определенного размера */

{

/* ... */

}

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

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

void func1(int x[][10])

{

/* ... */

}

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

void display_array(int *q[])

{

int t;

for(t=0; t<10; t++)

printf("%d ", *q[t]);

}

Необходимо помнить, что q — это не указатель на целые, а указатель на массив указателей на целые. Поэтому параметр q нужно объявить как массив указателей на целые. Нельзя объявить q просто как указатель на целые, потому что он представляет собой указатель на указатель.

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

void syntax_error(int num)

{

static char *err[] = {

"Нельзя открыть файл\n",

"Ошибка при чтении\n",

"Ошибка при записи\n",

"Некачественный носитель\n"

};

printf("%s", err[num]);

}

Массив err содержит указатели на строки с сообщениями об ошибках. Здесь строковые константы в выражении инициализации создают указатели на строки. Аргументом функции printf() служит один из указателей массива err, который в соответствии с индексом num указывает на нужную строку с сообщением об ошибке. Например, если в функцию syntax_error() передается num со значением 2, то выводится сообщение Ошибка при записи.

Если в качестве параметра функции используется обозначение массива, то на самом деле внутрь функции передается только адрес начала массива

Пр.: float function(int n,int *a) {…}

К элементам массива можно обращаться как: a[i], *(a+i).

С одной стороны, имя массива является константным указателем со значением, равным адресу 0 элемента. С другой стороны, имя массива, использованное в качестве параметра, играет в теле функции роль обычного (неконстантного) указателя, т.е. может использоваться в качестве леводопустимого выражения(*а*=*а++ - присваивание после умножения).

Строки в качетсве фактических параметров могут быть специфицированы либо как одномерные массивы типа char, либо как указатели типа char *. В обоих случаях с помощью параметра в функцию передается адрес начала символьного массива, содержащего строку. В отличие от обычных массивов для параметров-строк нет необходимости явно указывать их длину. нулевой байт ‘\0’, размещаемый в конце каждой строки, позволяет всегда определить ее длину, точнее, позволяет перебирать символы строки и не выйти за ее пределы.

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

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

void swap(int & a, int & b)

 void swap(int * a, int * b)

Билет 9. Представление строк символов в С. Операции над строками

Строковая константа – последовательность символов в двойных кавычках, по сути, массив символов. Самое популярное применение – передача в функцию в качестве аргументов.

В языке С признаком окончания строки (нулевым символом) служит символ '\0'. Таким образом, строка содержит символы, составляющие строку, а также нулевой символ. Это единственный вид строки, определенный в С.

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

char str[11]; - Последний, 11-й байт предназначен для нулевого символа.

Записанная в тексте программы строка символов, заключенных в двойные кавычки, является строковой константой, например, "некоторая строка", в конец которой компилятор автоматически добавляет нулевой символ.

Например, функция printf(“ggg”); получает указатель на начало массива символов.

Например, char *p; p = “fdhd”; Не выполняется копирование строки

Операции над строками Задание строки 4 вида задания строки «HELLO»: 1) char s[5] = { ‘ h ‘ , ‘ e ‘ , ‘ l ‘ , ‘ l ‘ , ‘ o ‘ }; 2) char s[5+1] = “ hello “ ; 3) chat s[ ] = “ hello “ ; 4) chat *s = “ hello “ ; - не создается массив, указатель ссылается на 1 элемент строки «HELLO», т.е. на букву Н.

Функции работы со строками 1) int strlen (char *s) – длина строки 2) char *strcpy(char *s1, char *s2) – копирование строки s2 в строку s1 3) char *strcat(char *s1, char *s2) – строка s2 добавляется в конец строки s1 4) char *strchr(char *s, char c) – поиск символа С в строке S 5) int strcmp(char *s1, char *s2) – сравнение строки s1 и s2 6) char *strncpy(char *s1, char *s2, int n) – копирует в строку s1 не больше n символов из строки s2 7) char *strncat(char *s1, char *s2, int n) – в конец строки s1 добавляется не более n символов из s2 8) char *strrchr(const char *s, char x) – поиск символа С в строке S с конца.

Строка в Си - это последовательность байт (букв, символов, литер, character), завершающаяся в конце специальным признаком - байтом '\0'. Этот признак добавляется компилятором автоматически, когда мы задаем строку в виде "строка". Длина строки (т.е. число литер, предшествующих '\0') нигде явно не хранится. Длина строки ограничена лишь размером массива, в котором сохранена строка, и может изменяться в процессе работы программы в пределах от 0 до длины массива-1. При передаче строки в качестве аргумента в функцию, функции не требуется знать длину строки, т.к. передается указатель на начало массива, а наличие ограничителя '\0' позволяет обнаружить конец строки при ее просмотре.

Пр.: char s[10]; char *c=”Иванов”

Операции над строками - <string.h>:

Заголовок функции

Что делает функция

int strlen(const char *);

возвращает длину строки

char *strcpy(char *s1, char *s2);

копирует s2 в s1

char *strncpy(char *s1,char *s2, int n);

копирует n символов из s2 в s1

char *strcat(char *s1, char *s2);

дописывает строку s2 в конец s1

char *strncat(char *s1,char *s2,int n);

дописывает n символов строки s2 в конец s1

char *strchr(const char *s, char x);

Находит символ х в строке s

char *strrchr(const char *s, char x);

Находит символ х в строке s, начиная с конца

int strcmp(char *s1,char *s2);

лексикографическое сравнение строк

int strncmp(char *s1,char *s2,int n);

лексикографическое сравнение первых n симв.

Строки и массивы в Си есть указатели.

Для вывода строки используется спецификатор преобразования %s

Строки в качетсве фактических параметров функций могут быть специфицированы либо как одномерные массивы типа char, либо как указатели типа char *. В обоих случаях с помощью параметра в функцию передается адрес начала символьного массива, содержащего строку. В отличие от обычных массивов для параметров-строк нет необходимости явно указывать их длину. нулевой байт ‘\0’, размещаемый в конце каждой строки, позволяет всегда определить ее длину, точнее, позволяет перебирать символы строки и не выйти за ее пределы.

Билет 10. Эффективность алгоритма.

Существует несколько способов измерения сложности алгоритма. Программисты обычно сосредотачивают внимание на скорости алгоритма, но не менее важны и другие показатели – требования к объёму памяти, свободному месте на диске. Использование быстрого алгоритма не приведёт к ожидаемым результатам, если для его работы понадобится больше памяти, чем есть у компьютера.

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

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

Чем больше входных данных, тем дольше будет выполняться алгоритм.

Основной способ оценки эффективности алгоритма - подсчет его операций. Быстродействие

алгоритма связано с количеством выполняемых операций, поэтому

оценить его эффективность можно путем их простого подсчета.

Алгоритм с рабочим шагом 3N^3 рассматривается, как O(N^3). Это делает зависимость отношения O(N) от изменения размера задачи более очевидной.

Если во внутренних циклах процедуры Fast происходит вызов процедуры Slow, то сложности процедур перемножаются. В данном случае сложность алгоритма составляет O(N^2 )*O(N^3 )=O(N^5). Если же основная программа вызывает процедуры по очереди, то их сложности складываются: O(N^2 )+O(N^3 )=O(N^3)

Сложность рекурсивных алгоритмов:

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

Простая рекурсия -. Рекурсия является простой, если вызов функции встречается в некоторой ветви лишь один раз. Процедура выполняется, например, N раз, тогда сложность О(N). (пример – факториал

long double fact(int N)

{

if (N < 0)

return 0;

if (N == 0)

return 1;

else

return N * fact(N - 1);

Сложная рекурсия - Рекурсивный алгоритм, который вызывает себя несколько раз, называется многократной рекурсией. Такие процедуры гораздо сложнее анализировать, кроме того, они могут сделать алгоритм гораздо сложнее.

procedure DoubleRecursive(N: integer); begin if N>0 then begin DoubleRecursive(N-1); DoubleRecursive(N-1); end; end;

Поскольку процедура вызывается дважды, можно было бы предположить, что её рабочий цикл будет равен O(2N)=O(N). Но на самом деле ситуация гораздо сложнее. Если внимательно исследовать этот алгоритм, то станет очевидно, что его сложность равна O(2^(N+1)-1)=O(2^N).

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