МАССИВЫ. Массив - это несколько пронумерованных переменных,
Массив - это несколько пронумерованных переменных, объединенных общим именем. Все переменные имеют ОДИН И ТОТ ЖЕ ТИП.
Рассмотрим ПОЛКУ с N ящиками, пусть имя полки - var. Тогда кажждый ящик-ячейка имеет имя var[0] var[1] ... var[N-1]
Нумерация идет с НУЛЯ.
-------- / var / / / ------------------------------------------- ------------------ | | | | | | | | | |....... | | | | | | | | ------------------------------------------- ------------------ / var[0] / / var[1] / / var[2] / / var[N-1] / --------- --------- --------- -----------
Массив объявляется так:
int var[N];
здесь N - его размер, число ячеек.
Это описание как бы объявляет N переменных типа int с именами var[0]... var[N-1];
В операторах для обращения к n-ому ящичку (где 0 <= n < N) используется имя ящика
var[n]
где n - целое значение (или значение целой переменной, или целочисленного выражения), "индекс в массиве". Эта операция [] называется "индексация массива". Индексация - есть ВЫБОР одного из N ящиков при помощи указания целого номера. var - массив (N ячеек) n - выражение (формула), выдающая целое значение в интервале 0..N-1 var[n] - взят один из элементов массива. Один из всех. n - номер ящика - называется еще и "индексом" этой переменной в массиве.
Пример:
int var[5]; /* 1 */
var[0] = 2; /* 2 */ var[1] = 3 + var[0]; /* 3 */ var[2] = var[0] * var[1]; /* 4 */ var[3] = (var[0] + 4) * var[1]; /* 5 */
printf("var третье есть %d\n", var[3]);
В ходе этой программы элементы массива меняются таким образом:
var[0] var[1] var[2] var[3] var[4] ------------------------------------------------ /* 1 */ мусор мусор мусор мусор мусор /* 2 */ 2 мусор мусор мусор мусор /* 3 */ 2 5 мусор мусор мусор /* 4 */ 2 5 10 мусор мусор /* 5 */ 2 5 10 30 мусор
Как видим, каждый оператор изменяет лишь ОДНУ ячейку массива за раз.
Массив - набор переменных, которые не ИМЕНОВАНЫ разными именами, вроде var0, var1, var2,... а ПРОНУМЕРОВАНЫ под одним именем: var[0], var[1], var[2],...
Индекс - часть ИМЕНИ ПЕРЕМЕННОЙ.
На самом деле индексация - это 1) выбор элемента в массиве 2) справа от присваиваний и в выражениях - еще и разыменование, то есть взятие вместо имени переменной - значения, в ней хранящегося.
Если в переменную не было занесено значение, а мы используем эту переменную, то в ней лежит МУСОР (любое, непредсказуемое значение).
printf("var4 есть %d\n", var[4]);
напечатает все что угодно.
Поэтому переменные надо всегда инициализировать (давать им начальное значение).
Глобальные переменные автоматически инициализируются нулем, если мы не задали иначе.
Локальные переменные не инициализируются автоматически, и содержат МУСОР.
Массивы НЕЛЬЗЯ присваивать целиком, язык Си этого не умеет.
int a[5]; int b[5];
a = b; /* ошибка */
Также нельзя присвоить значение сразу всем элементам (ячейкам) массива:
a = 0; /* ошибка */
не делает того, что нами ожидалось, а является ошибкой. Для обнуления всех ячеек следует использовать цикл:
int i;
for(i=0; i < 5; i++) /* для каждого i присвоить a[i] = 0; */ a[i] = 0;
СВЯЗЬ МАССИВОВ И ЦИКЛОВ ======================= Вследствие этого массивы приходится копировать (и инициализировать) поэлементно, в цикле перебирая все (или часть) ячейки массива.
int i;
for(i=0; i < 5; i++) a[i] = b[i];
В данном случае индекс цикла служит также и индексом в массиве.
Индексы в массиве идут с НУЛЯ.
Пример инициализации:
int index, array[5];
for(index=0; index < 5; index++) array[index] = index * 2 + 1;
или
int index, array[5];
index = 0; while(index < 5){ array[index] = index * 2 + 1; index++; }
/* В массиве будет: { 1, 3, 5, 7, 9 } */
ИНДЕКС для массивов - номер "ящика/ячейки" в массиве.
для циклов - номер повторения цикла, счетчик. Мы должны изменять его САМИ.
Обычно массивы и циклы совмещаются так: индекс цикла есть индекс в массиве; то есть индекс цикла используется для перебора всех элементов массива:
int a[N], i;
for(i=0; i < N; i++) ...a[i]...
Примеры:
int a[5];
a[0] = 17; a[0] += 4; a[0]++;
Пример: числа Фибоначчи. Задаются математическими формулами:
f[1] = 1 f[2] = 1 f[n+2] = f[n+1] + f[n]
Вот программа:
#include /* магическая строка */ #define N 20 /* сколько первых чисел посчитать */
void main(){ int fibs[N], index;
fibs[0] = 1; /* индексы отсчитываются с нуля!!! */ fibs[1] = 1;
/* Тут показано, что индекс элемента массива может вычисляться */
for(index=2; index < N; index++) fibs[index] = fibs[index-1] + fibs[index-2];
/* Распечатка в обратном порядке */ for(index = N-1; index >= 0; index--) printf("%d-ое число Фибоначчи есть %d\n", index+1, fibs[index]); }
Здесь мы видим новый для нас оператор #define Он задает текстуальную ЗАМЕНУ слова N на слово 20, в данном случае просто являясь эквивалентом
const int N = 20;
К несчастью размер массива не может быть задан при помощи переменной, а вот при помощи имени, определенного в #define - может.
СТРОКИ
Строки есть массивы БУКВ - типа char, оканчивающиеся спецсимволом \0
char string[20];
string[0] = 'П'; string[1] = 'р'; string[2] = 'и'; string[3] = 'в'; string[4] = 'е'; string[5] = 'т'; string[6] = '\0';
printf("%s\n", string);
%s - формат для печати СТРОК. Никакие другие массивы не могут быть напечатаны целиком одним оператором.
char string[20];
string[0] = 'П'; string[1] = 'р'; string[2] = 'и'; string[3] = 'в'; string[4] = 'е'; string[5] = 'т'; string[6] = '\n'; /* Перевод строки - тоже буква */ string[7] = '\0';
printf("%s", string);
или даже просто
printf(string);
Такие массивы можно записать в виде строки букв в ""
char string[20] = "Привет\n";
Оставшиеся неиспользованными символы массива от string[8] до string[19] содержат МУСОР.
ПОЧЕМУ ДЛЯ СТРОК ИЗОБРЕЛИ СИМВОЛ "ПРИЗНАК КОНЦА"? ================================================= Строка - это ЧАСТЬ массива букв. В разное время число букв в строке может быть различным, лишь бы не превышало размер массива (тогда случится сбой программы). Значит, следует где-то хранить текущую длину строки (число использованных символов). Есть три решения: (1) В отдельной переменной. Ее следует передавать во все функции обработки данной строки (причем она может изменяться).
char str[32]; /* массив для строки */ int slen; /* брать первые slen букв в этом массиве */ ... func(str, &slen); /* ДВА аргумента для передачи ОДНОЙ строки */ ...
Этот подход работоспособен, но строка разбивается на два объекта: сам массив и переменную для его длины. Неудобно.
(2) Хранить текущую длину в элементе str[0], а буквы - в str[1]... итд. Плохо тем, что в str[0] можно хранить лишь числа от 0 до 255, и если строка длиннее - то такой подход неприменим.
(3) Не хранить длину НИГДЕ, а ввести символ-признак конца строки. Теперь в
func(str); /* ОДИН аргумент - сам массив */
передается только сам массив, а его текущая длина может быть при нужде вычислена при помощи некоей функции, вроде такой:
int strlen(char s[]){ /* функция от массива букв */ int counter = 0; /* счетчик и одновременно индекс */
while(s[counter]!= '\0') /* пока не встретился признак конца текста */ counter++; /* посчитать символ */ return counter; /* сколько символов, отличных от '\0' */ }
Тут никаких ограничений нет. Именно этот подход и был избран в языке Си, хотя в принципе можно самому пользоваться и другими. На самом деле в языке есть такая СТАНДАРТНАЯ функция strlen(s) (вам не надо писать ее самому, ее уже написали за вас).
ИНИЦИАЛИЗАЦИЯ ГЛОБАЛЬНОГО МАССИВА ================================= Массив, заданный вне каких-либо функций, можно проинициализировать константными начальными значениями:
int array[5] = { 12, 23, 34, 45, 56 };
char string[7] = { 'П', 'р', 'и', 'в', 'е', 'т', '\0' };
Если размер массива указан БОЛЬШЕ, чем мы перечислим элементов, то остальные элементы заполнятся нулями (для int) или '\0' для char.
int array[5] = { 12, 23, 34 };
Если мы перечислим больше элементов, чем позволяет размер массива - это будет ошибкой.
int a[5] = { 177, 255, 133 };
Операция индексации массива a[] дает:
при n значение выражения a[n] есть
-1 не определено (ошибка: "индекс за границей массива") 0 177 1 255 2 133 3 0 4 0 5 не определено (ошибка) * 13_FUNCS.txt *
КАК ПРОИСХОДИТ ВЫЗОВ ФУНКЦИИ ============================
Пусть у нас описана функция, возвращающая целое значение.
/* ОПРЕДЕЛЕНИЕ ФУНКЦИИ func(). */ /* Где func - ее имя. Назвать мы ее можем как нам угодно. */
int func(int a, int b, int c){ int x, y;
... x = a + 7; ... b = b + 4; ...
return(некое_значение); }
Здесь a, b, c - аргументы функции (параметры) x, y - локальные переменные
Точка вызова - находится внутри какой-то другой функции, например функции main()
main(){
int zz, var; ... var = 17; zz = func(33, 77, var + 3) + 44; ... }
Когда выполнение программы доходит до строки
zz = func(33, 77, var + 3) + 44;
1) Происходит ВЫЗОВ ФУНКЦИИ func()
(a) Этот пункт мы увидим ниже.
(b) Создаются переменные с именами a, b, c, x, y;
(c) Переменным-аргументам присваиваются начальные значения, которые берутся из точки вызова. В точке вызова перечислен список (через запятую) выражений (формул):
func(выражение1, выражение2, выражение3)
Вычисленные значения этих выражений соответственно будут присвоены 1-ому, 2-ому и 3-ему аргументам (параметрам) из определения функции:
int func(a, b, c){ /* a = номер 1, b = 2, c = 3 */
Первый параметр:
a = 33;
Второй параметр:
b = 77;
Третий параметр:
c = var + 3;
то есть, вычисляя,
c = 20;
Локальные переменные x и y содержат неопределенные значения, то есть мусор (мы не можем предсказать их значения, пока не присвоим им явным образом какое-либо значение сами).
2) Выполняется ТЕЛО функции, то есть вычисления, записанные внутри {... } в определении функции. Например:
x = a + 7;
И параметры, и локальные переменные - это ПЕРЕМЕННЫЕ, то есть их можно изменять.
b = b + 4;
При этом никакие переменные ВНЕ этой функции не изменяются. (Об этом еще раз позже).
3) Производится ВОЗВРАТ из функции.
... return(некое_значение); }
Например, это может быть
... return(a + 2 * x); }
Рассмотрим, что при этом происходит в точке вызова:
zz = func(33, 77, var + 3) + 44;
(1) Вычеркиваем func(.....)
zz = XXXXXXX + 44;
(2) Вычисляем значение "некое_значение" в операторе return, и берем КОПИЮ этого значения. Пусть при вычислении там получилось 128.
(3) Подставляем это значение на место вычеркнутого func(.....) У нас получается
zz = 128 + 44;
(4) АВТОМАТИЧЕСКИ УНИЧТОЖАЮТСЯ локальные переменные и аргументы функции:
a - убито b - убито c - убито x - убито y - убито
Таких переменных (и их значений) больше нет в природе.
(5) Пункт, который мы обсудим позже.
(6) Продолжаем вычисление:
zz = 128 + 44;
Вычисляется в
zz = 172; /* оператор присваивания */
int func1(int x){ printf("func1: x=%d\n", x); /* 1 */ x = 77; printf("func1: x=%d\n", x); /* 2 */ return x; }
void main(){ int var, y;
var = 111; y = func1(var); /* @ */
printf("main: var=%d\n", var); /* 3 */ }
В данном случае в точке @ мы передаем в функцию func1() ЗНАЧЕНИЕ переменной var, равное 111. Это значит, что при вызове функции будет создана переменная x и ей будет присвоено начальное значение 111
x = 111;
Поэтому первый оператор printf() напечатает 111.
Затем мы изменяем значение переменной x на 77. Мы меняем переменную x, но не переменную var!!! Использовав ЗНАЧЕНИЕ (его копию) из переменной var для x, мы о переменной var забыли - она нас не касается (а мы - ее).
Поэтому второй оператор printf() напечатает 77. В переменной же var осталось значение 111, что и подтвердит нам третий оператор printf, который напечатает 111.
ВРЕМЕННОЕ СОКРЫТИЕ ПЕРЕМЕННЫХ =============================
int func1(int x){ /* f.1 */ printf("func1: x=%d\n", x); /* f.2 */ x = 77; /* f.3 */ printf("func1: x=%d\n", x); /* f.4 */ return x; /* f.5 */ }
void main(){ int x, y; /* 1 */
x = 111; /* 2 */ y = func1(x); /* 3 */
printf("main: x=%d y=%d\n", x, y); /* 4 */ }
А теперь мы и переменную внутри main(), и аргумент функции func1() назвали одним и тем же именем. Что будет?
Будет то же самое, что в предыдущем примере.
В момент вызова функции func1() будет создана НОВАЯ переменная с именем x, а старая (прежняя) переменная и ее значение будут ВРЕМЕННО СПРЯТАНЫ (скрыты).
Можно было бы уточнить эти переменные именами функций, в которых они определены:
main::x
и
func1::x
(но это уже конструкции из языка Си++, а не Си).
Выполним программу по операторам:
|/* 1 */ Отводятся переменные main::x и main::y для целых чисел; |/* 2 */ main::x = 111; |/* 3 */ Вызывается func1(111); | +-------+ . |/* f.1 */ Отводится переменная func1::x со значением 111; . |/* f.2 */ Печатается 111 из переменной func1::x; . | . |/* f.3 */ func1::x = 77; (это не main::x, а другая переменная, . | ЛОКАЛЬНАЯ для функции func1. . | Переменную main::x мы сейчас не видим - . | она "заслонена" именем нашей локальной . | переменной. . | Поэтому мы не можем ее изменить). . | . |/* f.4 */ Печатает 77 из func1::x; . |/* f.5 */ Возвращает значение func1::x, то есть 77. . | Переменная func1::x уничтожается. . | . | Теперь мы снова возвращаемся в функцию main(), . | где имя x обозначает переменную main::x . | а не func1::x +-------+ | |/* 3 */ y = 77; |/* 4 */ Печатает значения main::x и main::y, то есть | 111 и 77.
Этот механизм сокрытия имен позволяет писать функции main() и func1() разным программистам, позволяя им НЕ ЗАБОТИТЬСЯ о том, чтобы имена локальных переменных в функциях НЕ СОВПАДАЛИ. Пусть совпадают - хуже не будет, механизм упрятывания имен разрешит конфликт. Зато программист может использовать любое понравившееся ему имя в любой функции - хотя бы и x, или i.
То же самое происходит с локальными переменными, а не с аргументами функции.
int func1(int arg){ /* локальная переменная-параметр func1::arg */ int x; /* локальная переменная func1::x */
x = arg; printf("func1: x=%d\n", x); x = 77; printf("func1: x=%d\n", x); return x; }
void main(){ int x, y; /* переменные main::x и main::y */
x = 111; y = func1(x);
printf("main: x=%d y=%d\n", x, y); }
Действует тот же самый механизм временного сокрытия имени x. Вообще же, аргументы функции и ее локальные переменные отличаются только одним: аргументам автоматически присваиваются начальные значения, равные значениям соответствующих выражений в списке
имя_функции(...,...,....) арг1 арг2 арг3
в месте вызова функции.
То есть
ОПИСАНИЕ ФУНКЦИИ:
int f(int арг1, int арг2, int арг3){ int перем1, перем2; ... /* продолжение */ }
ВЫЗОВ:
.... f(выражение1, выражение2, выражение3)...
ТО В ТЕЛЕ ФУНКЦИИ ВЫПОЛНИТСЯ (в момент ее вызова):
арг1 = выражение1; арг2 = выражение2; арг3 = выражение3;
перем1 = МУСОР; перем2 = МУСОР;
... /* продолжение */
ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ =====================
Наконец, существуют переменные, которые объявляются ВНЕ ВСЕХ ФУНКЦИЙ, и существующие все время выполнения программы (а не только то время, когда активна функция, в которой они созданы).
Локальные переменные и аргументы УНИЧТОЖАЮТСЯ при выходе из функции. Глобальные переменные - нет.
int x = 12; /*::x - ей можно заранее присвоить константу */ int globvar; /*::globvar */
int f1(){ int x; /* f1::x */
x = 77; printf("x=%d\n", x); /* 4 */ return x; }
int f2(){ printf("x=%d\n", x); /* 5 */ return 0; }
void main(){ int x, y; /* main::x */
x = 111; /* 1 */ printf("x=%d\n", x); /* 2 */ printf("glob=%d\n", globvar); /* 3 */
y = f1(); y = f2(); }
В данном примере мы видим: - во-первых мы видим ФУНКЦИИ БЕЗ ПАРАМЕТРОВ. Это нормальная ситуация. - во-вторых тут используются ТРИ переменные с именем "x".
Как выполняется программа?
/* 1 */ main::x = 111; Это локальный x, а не глобальный. Глобальный x попрежнему содержит 12.
/* 2 */ Напечатает значение переменной main::x, то есть 111. Внутри функции main глобальная переменная::x заслонена своей собственной переменной x. В данном случае НЕТ СПОСОБА добраться из main к глобальной переменной x, это возможно только в языке Си++ по имени::x
К переменной же globvar у нас доступ есть.
/* 3 */ Печатает::globvar. Мы обнаруживаем, что ее значение 0. В отличие от глобальных переменных, которые изначально содержат МУСОР, глобальные переменные изначально содержат значение 0.
В рамочку, подчеркнуть.
/* 4 */ При вызове f1() переменная f1::x заслоняет собой как main::x так и ::x
В данном случае напечатается 77, но ни::x ни main::x не будут изменены оператором x = 77. Это изменялась f1::x
/* 5 */ При вызове f2() история интереснее. Тут нет своей собственной переменной x. Но какая переменная печатается тут - ::x или main::x?
Ответ:::x то есть 12.
Переменные названы локальными еще и потому, что они НЕВИДИМЫ В ВЫЗЫВАЕМЫХ ФУНКЦИЯХ.
Это ОПРЕДЕЛЕНИЕ локальных переменных. (Поэтому не спрашивайте "почему?" По определению)
То есть, если мы имеем
funca(){ int vara; ... ...funcb();... /* вызов */ ... }
то из функции funcb() мы НЕ ИМЕЕМ ДОСТУПА К ПЕРЕМЕННОЙ vara.
funcb(){ int z;
z = vara + 1; /* ошибка, vara неизвестна внутри funcb() */ }
Если, в свою очередь, funcb() вызывает funcc(), то и из funcc() переменная vara невидима.
Остановитесь и осознайте. Это правило служит все той же цели - разные функции могут быть написаны разными программистами, которые могут использовать одни и те же имена для РАЗНЫХ переменных, не боясь их взаимопересечения. Множества имен, использованных в разных функциях, независимы друг от друга. Имена из одной функции НИКАК не относятся к переменным с теми же именами ИЗ ДРУГОЙ функции.
Вернемся к параграфу КАК ПРОИСХОДИТ ВЫЗОВ ФУНКЦИИ и рассмотрим пункт (a). Теперь он может быть описан как
(a) Локальные переменные и аргументы вызывающей функции делаются невидимыми. ~~~~~~~~~~ А при возврате из функции:
(5) Локальные переменные и аргументы вызывающей функции снова делаются видимыми.
ОДНАКО глобальные переменные видимы из ЛЮБОЙ функции, исключая случай, когда глобальная переменная заслонена одноименной локальной переменной данной функции.
ПРОЦЕДУРЫ ========= Бывают функции, которые не возвращают никакого значения. Такие функции обозначаются void ("пустышка"). Такие функции называют еще ПРОЦЕДУРАМИ.
void func(){ printf("Приветик!\n"); return; /* вернуться в вызывающую функцию */ }
Такие функции вызываются ради "побочных эффектов", например печати строчки на экран или изменения глобальных (и только) переменных.
int glob;
void func(int a){ glob += a; }
Оператор return тут необязателен, он автоматически выполняется перед последней скобкой }
Вызов таких функций не может быть использован в операторе присваивания:
main(){ int z;
z = func(7); /* ошибка, а что мы присваиваем??? */ }
Корректный вызов таков:
main(){ func(7); }
Просто вызов и все.
ЗАЧЕМ ФУНКЦИИ?
Чтобы вызывать их с разными аргументами!
int res1, res2; ...
res1 = func(12 * x * x + 177, 865, 'x'); res2 = func(432 * y + x, 123 * y - 12, 'z');
Кстати, вы заметили, что список фактических параметров следует через запятую; и выражений ровно столько, сколько параметров у функции?
Функция описывает ПОСЛЕДОВАТЕЬНОСТЬ ДЕЙСТВИЙ, которую можно выполнить много раз, но с разными исходными данными (аргументами). В зависимости от данных она будет выдавать разные результаты, но выполняя одни и те же действия.
В том то и состоит ее прелесть: мы не дублируем один кусок программы много раз, а просто "вызываем" его.
Функция - абстракция АЛГОРИТМА, то есть последовательности действий. Ее конкретизация - вызов функции с уже определенными параметрами.
Оператор return может находиться не только в конце функции, но и в ее середине. Он вызывает немедленное прекращение тела функции и возврат значения в точку вызова.
int f(int x){ int y;
y = x + 4; if(y > 10) return (x - 1); y *= 2; return (x + y); }
|