Тема 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;
}