Тема 7. Указатели и массивы

 

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

Рассмотрим следующие операторы:

int mas[N];

int *p;

Первый из них описывает целочисленный массив размерностью N, а второй – указатель на целочисленную переменную, который пока ни на что не указывает. Если выполнить присваивание

p = &mas[0];

то теперь указатель p будет содержать адрес начального элемента массива mas, или, другими словами, будет указывать на элемент mas[0] (точнее, на первый байта элемента mas[0], т.к. значение типа int хранится в 4-х байтах).

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

 

p ® mas[0]
p + 1 ® mas[1]
p + 2 ® mas[2]
. . . . . .
p+N-1 ® mas[N-1]

 

Так как операция разадресации (*) позволяет получить значение элемента массива, на который указывает указатель, то можно записать следующее:

*p = mas[0];

*(p+1) = mas[1];

*(p+2) = mas[2];

. . .

*(p+N-1) = mas[N-1];

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

p = &mas[0];

можно заменить на

p = mas;

С учетом этого можно записать, что:

*mas = mas[0];

*(mas +1) = mas[1];

*(mas +2) = mas[2];

. . .

*(mas +N-1) = mas[N-1];

Между указателем p и именем массива mas существует одно существенное различие. Указатель p – это переменная, предназначенная для хранения любых адресов, поэтому можно, например, написать:

p++;

p += i;

Но имя массива mas не является переменной, это указатель-константа на начальный элемент массива, и записи типа:

mas++;

mas += i;

недопустимы.

Пример 1: вывести на экран значения всех элементов массива, введенных с клавиатуры.

#include <stdio.h>

void main()

{

const int N = 10;

int i, mas[N];

int *p;

p = mas;

//Ввод (один из вариантов)

for (i = 0; i < N; i++)

scanf(“%d”, p+i);

//Вывод: вариант 1

for (i = 0; i < N; i++)

printf(“%d ”, *(p+i));

//Вывод: вариант 2

for (i = 0; i < N; i++)

{

printf(“%d ”, *p);

p++;

}

//Вывод: вариант 3

p = mas;

for (i = 0; i < N; i++)

printf(“%d ”, *p++);

//Вывод: вариант 4

p = mas;

for (i = 0; i < N; p++, i++)

printf(“%d ”, *p);

//Вывод: вариант 5

for (i = 0; i < N; i++)

printf(“%d ”, *(mas+i));

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

p = &mas[N-1];

for (i = 0; i < N; i++)

printf(“%d ”, *(p-i));

}

 

Пример 2: вывести на экран все значения элементов двумерного массива.

#include <stdio.h>

void main()

{

const int N = 2, M = 4;

int mas[N ][M], *p, i, j;

p = &mas[0][0]; //можно p = mas[0] или p = *mas;

//Ввод (один из вариантов)

for (i = 0; i < N*M; i++)

scanf(“%d”, p+i);

//Вывод: вариант 1

for (i = 0; i < N*M; i++)

printf("%d%c", *p++, (i+1)%M ? ' ' : '\n');

//Вывод: вариант 2

p = mas[0];

for (i = 0; i < N; i++)

{

for (j = 0; j < M; j++)

printf("%d ", *(p+i*M+j));

printf("\n");

}

//Вывод: вариант 3

for (i = 0; i < N; i++)

{

for (j = 0; j < M; j++)

printf("%d ", *(mas[i]+j));

printf("\n");

}

//Вывод: вариант 4

for (i = 0; i < N; i++)

{

for (j = 0; j < M; j++)

printf("%d ", *(*(mas+i)+j));

printf("\n");

}

}

Эти варианты программы основаны на следующей схеме взаимосвязей между указателями и элементами двумерного массива:

p = mas[0] = *mas ® mas[0][0]
p+1 = mas[0]+1 = *mas+1 ® mas[0][1]
p+2 = mas[0]+2 = *mas+2 ® mas[0][2]
p+3 = mas[0]+3 = *mas+3 ® mas[0][3]
p+4 = mas[1] = *(mas+1) ® mas[1][0]
p+5 = mas[1]+1 = *(mas+1)+1 ® mas[1][1]
p+6 = mas[1]+2 = *(mas+1)+2 ® mas[1][2]
p+7 = mas[1]+3 = *(mas+1)+3 ® mas[1][3]

 

 

Тема 8. Строки символов

 

Строка представляет собой массив символов, заканчивающийся нуль-символом. Нуль-символ – это символ с кодом, равным нулю, что записывается в виде управляющей последовательности ‘\0’. По положению нуль-символа определяется фактическая длина строки. Строку можно инициализировать строковой константой:

char str[50] = “Vasia”; //первые элементы массива str: ‘V’, ‘a’, ‘s’, ‘i’, ‘a’, ‘\0’

Можно не указывать размерность массива:

char str[ ] = “Vasia”; //выделяется память под 6 элементов: 5 букв и нуль-символ

Возможна инициализация с помощью набора символьных констант:

char str[50] = {‘V’, ‘a’, ‘s’, ‘i’, ‘a’}; // первые элементы: 5 букв и нуль-символ

Если размерность не указывать, то к инициализаторам нужно обязательно добавить нуль-символ:

char str[ ] = {‘V’, ‘a’, ‘s’, ‘i’, ‘a’, ‘\0’}; //выделяется память под 6 элементов

 

Для ввода данных с клавиатуры в строку символов используются функции gets, scanf, getchar. Для вывода содержимого строки на экран используются функции puts, printf, putchar. Все эти функции описаны в заголовочном файле stdio.h:

#include <stdio.h>

void main()

{

const int N = 50;

char str1[N], str2[N], str3[N], str4[N];

int i;

//ввод: вариант 1

gets(str1); //в str1 вводится строка символов. Окончание ввода – по нажатию //клавиши ВВОД (Enter)

//ввод: вариант 2

scanf("%s", str2); //в str2 вводится строка в которой недопустимы пробельные

//символы. Окончание ввода – по нажатию клавиши ВВОД или клавиши ПРОБЕЛ.

fflush(stdin); //очистка потока ввода stdin

 

//ввод: вариант 3

scanf("%10c", str3); //в str3 вводится 10 символов (завершение ввода клавишей ВВОД)

str3[10] = ‘\0’; //с помощью операции присваивания добавляется нуль-символ

fflush(stdin); //очистка stdin, если с помощью scanf было введено > 10 символов

 

//ввод: вариант 4

i = 0;

while(1)

{

str4[i] = getchar();

if (str4[i] == ‘\n’) break;

i++;

}

str4[i] = ‘\0’; //последний введенный символ (‘\n’) заменяется на ‘\0’

// вывод: вариант 1

puts(str1); //вывод str1 и переход на начало новой строки

// вывод: вариант 2

printf("%s\n", str2); //вывод str2 и переход на начало новой строки

// вывод: вариант 3

i = 0;

while(str3[i] != ‘\0’) //можно записать короче: while(str3[i])

{

printf("%c", str3[i]); //можно совместить 2 строки: printf("%c", str3[i++]);

i++;

}

printf("\n");

 

// вывод: вариант 4

i = 0;

while(str4[i] != ‘\0’) //можно записать короче: while(str4[i])

{

putchar(str4[i]); //можно совместить 2 строки: putchar(str4[i++]);

i++;

}

}

 

Пример программы копирования одной строки в другую:

#include <stdio.h>

void main()

{

const int N = 50;

char str1[N], str2[N] = “\”Zubilo\” – chempion”;

char *p1 = str1, *p2 = str2;

 

//вариант 1

int i = 0;

do

str1[i] = str2[i];

while(str1[i++] != ‘\0’); //можно записать короче: while(str1[i++]);

 

//вариант 2

int i = 0;

while(1);

{

str1[i] = str2[i];

if (str1[i++] == ‘\0’) break; //можно записать короче: if (!str1[i++]) break;

}

 

//вариант 3

int i = 0;

while((str1[i] = str2[i]) != ‘\0’) //можно записать короче: while(str1[i] = str2[i])

i++;

 

//вариант 4

while((*p1 = *p2) != ‘\0’)

{

p1++;

p2++;

}

 

//вариант 5

while((*p1++ = *p2++) != ‘\0’);

 

//вариант 6

while(*p1++ = *p2++);

//вывод результата копирования на экран

puts(str1); //можно и так: p1 = str1; puts(p1);

}

Тема 9. Структуры

 

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

Сначала структуру нужно объявить (декларировать). Объявление структуры начинается с ключевого слова struct и содержит список описаний, заключенный в фигурные скобки, например:

struct point

{

int x, y;

char color[20];

};

где point – это имя структуры.

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

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

struct point A, B, C;

с точки зрения синтаксиса аналогична записи:

int a, b, c;

и описывает 3 переменные структурного типа с именами A, B, C. Одновременно можно выполнить инициализацию полей структур:

struct point A = {50, 50, “Red”}, B, C = {100, 100, “Green”};

Можно совместить объявление структуры с описанием переменных структурного типа:

struct point

{

int x, y;

chat color[20];

} A = {50, 50, “Red”}, B, C;

Более того, если кроме A, B и C нам больше не нужны переменные этого типа, то имя point можно опустить.

Доступ к отдельному полю структуры выполняется посредством конструкции вида:

ИмяСтруктуры.ИмяПоля

Например, введем с клавиатуры данные в поля структуры B, а затем выведем эти данные на экран:

scanf("%d%d%s", &B.x, &B.y, &B.color);

printf("(%d, %d) %s", B.x, B.y, B.color);

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

УказательНаСтруктуру->ИмяПоля

Например:

struct point *p;

p = &B;

scanf("%d%d%s", &p->x, &p->y, &p->color);

printf("(%d, %d) %s\n", p->x, p->y, p->color);

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

struct rect //задает прямоугольник, стороны которого ||-ы осям координат

{ // pt1 и pt2 – угловые точки, лежащие на диагонали прямоугольника

struct point pt1;

struct point pt2;

} R;

R.pt1.x = R.pt1.y = 50;

R.pt1.color = “Red”;

R.pt2.x = R.pt2.y = 100;

R.pt2.color = “Green”;

Массивы структур

В практике программирования большое распространение получили массивы структур, т.е. массивы, элементами которых являются переменные структурного типа. Например, оператор

struct point mas[3];

описывает массив, состоящий из трех элементов типа struct point. Описание можно дополнить инициализацией:

struct point mas[ ] = {{10, 10, “Red”}, {20, 20, “Green”}, {30, 30, “Blue”}};

Если инициализаторы – простые константы или строки символов, и все они имеются в наличии, во внутренних скобках нет необходимости:

struct point mas[ ] = {10, 10, “Red”, 20, 20, “Green”, 30, 30, “Blue”};

Ввод данных в массив структур с клавиатуры и вывод их на экран можно выполнить следующим образом:

int i;

for(i=0; i<3; i++)

{

scanf(“%d%d%s”, &mas[i].x, &mas[i].y, &mas[i].color);

printf("(%d, %d) %s", mas[i].x, mas[i].y, mas[i].color);

}

То же самое с использованием механизма указателей:

struct point *p;

p = mas;

for(i=0; i<3; i++)

{

scanf("%d%d%s", &(p+i)->x, &(p+i)->y, &(p+i)->color);

printf("(%d, %d) %s", (p+i)->x, (p+i)->y, (p+i)->color);

}

Битовые поля

Битовые поля – это особый вид полей структуры. Они используются для плотной упаковки данных, например, флажков типа «да/нет». Это объясняется тем, что минимальная адресуемая ячейка оперативной памяти имеет длину один байт (8 бит), в то время как для хранения флажка достаточно одного бита. При описании битового поля после имени через двоеточие указывается длина поля в битах. Битовые поля могут быть любого целого типа и обычно используется тип unsigned int (сокращенно unsigned) . Например:

struct options

{

unsigned bold : 1;

unsigned italic : 1;

unsigned underline : 1;

unsigned background : 4;

} opt;

Под переменную opt структурного типа options будет отведена память размером 1 байт (если не использовать битовые поля, то структурная переменная будет занимать минимум 4 байта).

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

Объединения

Объединение представляет собой частный случай структуры, все поля которой располагаются по одному и тому же адресу. Формат описания такой же, как и у структуры, только вместо ключевого слова struct используется слово union. Длина объединения равна наибольшей из длин его полей. В каждый момент времени объединение хранит только одно значение, и ответственность за его правильное использование лежит на программисте.

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

#include <stdio.h>

void main()

{

int payType; //тип платежа

union payment

{

char card[25]; //оплата по карте

int check; //оплата чеком

} info;

// . . . присваивание значений переменным payType и info

switch (payType)

{

case 0: printf("Оплата по карте: %s", info.card); break;

case 1: printf("Оплата чеком: %d", info.check); break;

}

}

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

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

#include <stdio.h>

void main()

{

struct

{

int payType;

union

{

char card[25];

int check;

};

} info;

// . . . присваивание значения переменной info

switch (info.payType)

{

case 0: printf("Оплата по карте: %s", info.card); break;

case 1: printf("Оплата чеком: %d", info.check); break;

}

}

Перечисления

Перечисление – это список именованных целых констант. Для описания перечисления используется следующий формат:

enum [имяПеречисления] {списокИменованныхКонстант};

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

enum boolean {NO, YES}; // NO = 0, YES = 1

boolean bool; //описали переменную bool типа boolean

bool = YES; //bool = 1

 

enum months {JAN=1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC};

months mon; // FEB=2, MAR=3, APR=4, . . . , DEC=12

mon = DEC; //mon = 12

enum controls {BELL=’\a’, BACKSPACE=’\b’, NEWLINE=’\n’, TAB=’\t’, VTAB=’\v’};

char ch;

while(1)

{

if ((ch = getchar()) == NEWLINE) break;

. . .

}

 

Пример формирования на экране меню

#include <stdio.h>

void main()

{

enum menu {READ=1, WRITE, APPEND, EXIT};

int num;

do

{

printf("1. Читать\n");

printf("2. Писать\n");

printf("3. Дописать в конец\n");

printf("4. Выйти\n");

printf("\nВаш выбор?\n");

scanf("%d", &num);

if (num == EXIT) break;

switch (num)

{

case READ: . . . ; break;

case WRITE: . . . ; break;

case APPEND: . . . ; break;

default: printf("Неверный ввод\n"); break;

}

} while(1);

}

 

Переименование типов

 

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

typedef Тип НовоеИмяТипа[Размерность];

Размерность может отсутствовать. Примеры:

typedef unsigned int uint;

typedef char message[100];

typedef char * string;

Введенные таким образом имена можно использовать таким же образом, как и имена простых типов данных:

uint i, j ; //две переменные типа unsigned int

message str[10]; //массив из 10 строк по 100 символов в каждой строке

string p = str; //инициализированный указатель на char

Кроме задания типам с длинными описаниями более коротких имен, typedef используется для облегчения переносимости программ: если машинно-зависимые типы объявить с помощью операторов typedef, при переносе программы потребуется внести изменения только в эти операторы.

 

Тема 10. Функции

 

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

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

Любая функция должна быть определена. Определение функции состоит из заголовка и тела:

тип имя ([список параметров]) //заголовок функции

{

тело функции

}

Заголовок функции задает ее имя, тип возвращаемого значения (результата) и список передаваемых параметров. Тип возвращаемого функцией значения может быть любым, кроме массива и функции (но может быть указателем на массив или функцию). Если функция не должна возвращать значение, указывается тип void. Список параметров определяет величины, которые требуется передать в функцию при ее вызове. Элементы списка параметров разделяются запятыми. Для каждого параметра, передаваемого в функцию, указывается его тип и имя.

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

int sumFunc(int n, int m, int p)

{

int result; //эту и 2 последующие строки можно

result = n + m + p; //заменить одной строкой:

return result; // return = n + m + p;

}

Эта простейшая функция, имя которой sumFunc, предназначена для нахождения суммы трех целых чисел n, m и p, которые передаются ей в качестве параметров. Возврат из функции подсчитанной суммы выполняется с помощью оператора, имеющего следующий формат:

return [выражение];

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

void main()

{

int a = 10, b = 20, c = 30, res;

puts("a = 10, b = 20, c = 30");

res = sumFunc(a, b, c); //эту и следующую строки можно совместить:

printf("a + b + c = %d", res); // printf("a + b + c = %d", sumFunc(a, b, c));

}

Если в программе сначала определить главную функцию main, а затем – функцию sumFunc, то перед функцией main нужно будет поместить так называемый прототип функции sumFunc, иначе при выполнении оператора

res = sumFunc(a, b, c);

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

#include <stdio.h>

int sumFunc(int n, int m, int p); //прототип функции sumFunc

void main() //определение функции main

{

int a = 10, b = 20, c = 30;

puts("a = 10, b = 20, c = 30");

printf("a + b + c = %d", sumFunc(a, b, c)); //вызов функции sumFunc

}

int sumFunc(int n, int m, int p) //определение функции sumFunc

{

return n + m + p;

}

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

int sumFunc(int, int, int);

Глобальные, локальные и статические переменные

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

#include <stdio.h>

void func(int n)

{

static int a = 0;

a += n;

printf("%d ", a);

}

void main()

{

func(5);

func(5);

func(5);

}

Результат работы программы:

5 10 15

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

Параметры функции

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

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

Существует два способа передачи параметров в функцию: по значению и по адресу.

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

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

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

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

swap(a, b);

функции, имеющей прототип

void swap(int n, int m);

то переменные a и b сохранят свои первоначальные значения, поскольку swap получает лишь копии значений этих переменных.

Чтобы получить желаемый эффект, надо вызывающей программе передать указатели на те значения, которые должны быть изменены:

swap(&a, &b);

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

void swap(int *pn, int *pm)

{

int temp;

temp = *pn;

*pn = *pm;

*pm = temp;

}