Вы находитесь на странице: 1из 33

ПЯВУ. Лекция 4.

Основы С
А.М. Задорожный
Контрольные вопросы
1. Перечислите побитовые операции над целыми? Какой тип
значений возникает при таких операциях?
2. Как в компьютере представлены дробные числа?
3. Что такое ссылка? Чем отличается от переменной? Как объявить
ссылку в тексте?
4. Какими способами передаются параметры в функции?
5. Из чего (каких файлов) состоит модуль в C? Что помещается в C-
файл? Что помещается в заголовочном файле?
6. Как сработает инструкция препроцессора #define <имя>
<подстановка>?
7. Что позволяет делать инструкция typedef?
8. Что такое структура в C? Как она определяется?
9. Как выполняются операторы break и continue?
Содержание
1. Глобальные и локальные переменные
2. Указатели
a) Указатели и массивы
b) Указатели и структуры
3. С-строки
4. Динамическая помять
Глобальные и локальные переменные.

Переменные, объявленные в блоке ({…}) называются


локальными.

Локальные переменные исчезают по завершении


блока.

Переменные, объявленные вне всех блоков, доступны


из любых функций и называются глобальными.

Глобальные переменные существуют в течение всего


времени работы программы.
Параметры и локальные переменные.

int main(int argc, char ** argv)


{

}

Параметры любой функции (argc, argv) – локальные


переменные.

Их значения поступают извне при вызове функции.

Время их жизни ограничено функцией.


Глобальные и локальные переменные.
Пример.
int gx; // Глобальная переменная

int main(int argc, char ** argv)


{
int n = 3; // n - локальная переменная
for(int i=0; i<n; i++) //i - локальная
{…}
return 0;
}
Глобальные переменные и заголовочные
файлы.
Чтобы глобальную переменную можно было использовать
в другом файле ее объявление нужно включить в
заголовочный файл с ключевым словом extern;

Например, в “tst.h” можно написать:

extern int gx;

Теперь, если включить заголовочный файл “tst.h”,


компилятор будет знать, что где-то существует переменная
gx целого типа и сможет скомпилировать программу.
Глобальные переменные и заголовочные
файлы.
Уже известные факты.
• Каждый C-файл компилируется независимо;
• Сначала выполняются инструкции препроцессора, результат передается
компилятору;
• Из полученных модулей собирается выполняемая программа.

Что произойдет, если глобальную переменную


объявить в заголовочном файле?

В модуле, где мы хотим воспользоваться


глобальной переменной, нужно включить
соответствующий заголовочный файл?
Глобальные переменные.
“История одного проекта”
Пусть глобальная переменная называется
int gx;
и размещена в “tst.h”.
Хотим обращаться к ней в “A.c” и в “B.c”.
В “A.c” включаем #include “tst.h”
После препроцессора имеем
int gx;
Компилятор создает модуль “A.obj”, который содержит переменную gx.
В “B.c” включаем #include “tst.h”
После препроцессора имеем
int gx;
Компилятор создает модуль “B.obj”, который содержит переменную gx.
Компановщик начинает собирать программу и видит, что имеется 2-е gx.
Сборка не пройдет!
Глобальные переменные.
“История одного проекта”
Понимаем, что сборка не прошла, т.к. возникло 2 переменных с
одним именем.

Пробуем исключить ее из “tst.h” и объявить в “A.c”.


Результат: Модуль A отлично компилируется, но при
компиляции “B.c” ошибка – не определено имя gx.

Удаляем из “A.c” и снова в “tst.h”. Вспоминаем, что в


заголовочном файле нужно писать
extern int gx;
Снова пытаемся построить проект.
Результат: Модули компилируются, но при сборке ошибка – не
найдено gx.
Глобальные переменные.
“История одного проекта”
Близки к панике. Текущее состояние:
• Включаем в заголовок int gx – возникает 2
одинаковых глобальных переменных;
• Объявляем в одном модуле, так точно будет одна gx –
второй модуль не видит gx;
• Снова возвращаемся к заголовку, но объявляем как
extern int gx – теперь видны везде, но компановщик
не находит gх.

Что ни так-то? Как быть?


Глобальные переменные.
Объявление и создание.
Для объявления глобальной переменной нужно и то и другое!
• И объявить ее в ОДНОМ из модулей (“A.c”)
• И включить ее в заголовочный файл с квалификатором extern.
=> после препроцессора в каждом модуле будет
extern int gx;
А в одном
int gx;
Каждый модуль будет компилироваться, но переменная gx будет создана
ТОЛЬКО В ОДНОМ модуле!
Т.е. extern int gx; говорит компилятору, что где то будет определена int gx.
Этого достаточно для компиляции! Причем, место под нее выделять в
модуле не нужно!
В одном модуле gx будет создана.
Это ровно то, чего мы хотели!
Глобальные переменные.
Объявление и создание.
Для объявления глобальной переменной нужно:
• Объявить переменную в ОДНОМ из модулей
(“<имя файла>.c”) и
• включить ее объявление в заголовочный файл
(“<имя файла>.h”) с квалификатором extern.

В модулях, где необходим доступ к глобальной


переменной, нужно включить соответствующий
заголовок:
#include “<имя файла>.h”
Контрольные вопросы
1. Когда создается и когда уничтожается глобальная
переменная?

2. Как объявить глобальную переменную?

3. Как можно обеспечить использование глобальных


переменных в различных C-файлах?

4. Какие виды локальных переменных существуют в C?

5. Какова область жизни каждого вида локальных


переменных?
Указатели
В C часто используются указатели.
Указатели – это производный (от определенных типов) тип данных.
Значениями указателей являются адреса других величин.
Примеры объявления указателей
int *p; // Указатель на целое
double *p1; // Указатель на double
struct St *p2; // Указатель на структуру St
или
ST *p3; // Указатель на структуру St,
// если было typedef struct St ST;
Т.е. для объявления указателя используется *.
Если перед именем в объявлении стоит *, значит это объявлен
указатель!
Указатели
Указатели – это производный (от определенных типов) тип
данных. Значениями указателей являются адреса других
величин.
Как же получать адреса величин?
Для этого имеется операция &. Если объявлена переменная x, то
&x – адрес этой переменной.
Примеры:
int x=3;
double y=1.5;
int *p = &x; // Указатель на целое, а его значение – адрес x.
double *p1 = &y; // Указатель на double, а его значение – адрес y.
Указатели
Указатели – это производный (от определенных типов) тип данных. Значениями
указателей являются адреса других величин.
Как же использовать адреса величин?
Имея адрес переменной или другой величины, с ней можно делать все!
В частности, можно получить или изменить ее значение. Для этого снова
используется *.
Операция * играет разную роль при объявлении и использовании указателя. При
объявлении * означает, что объявляется указатель. Но после объявления - * перед
указателем означает ‘получение ссылки на то, на что показывает указатель’.
Примеры:
int x=3;
int *p = &x; // Указатель на целое, а его значение – адрес x.
ptintf(“%d”, *p); // Выведет значение x, т.е. 3.
Почему *p – ссылка (а не значение)?
*p = 5; // Изменили x, теперь в x хранится 5.
ptintf(“%d %d”, *p, x); // Выведет: 5 5.
Указатели
Важная операция над указателями * позволяет получать ссылку на величину,
а значит получать и изменять ее значение!

Эта операция (*) называется “разименование”.


Что еще можно делать над указателями?
Для компьютера адреса самая естественная вещь. Поэтому с указателями
можно много.
p++ - передвигает указатель на следующий ‘объект’ того типа, на который
указывает указатель.
Т.е., если p указывал на целое, то после p++ адрес изменится на 4 байта
(размер целого), а если p показывает на double, то после p++ адрес изменится
на 8 байт.
Аналогично ведет себя и p--.
Указатели можно сравнивать, ведь это просто номера байтов, т.е. целые
числа. <, <=, ==, >, >= и !=.
Указатели
Что еще можно делать над указателями?
Указатели можно изменять на целое число
p += N или p -= N.
При этом снова получим указатель того же типа, но смещенный на N
‘объектов’ вправо или влево.
Указатели одного типа можно вычитать. Если p и d указатели одного типа, то
N = p – d;
При этом получим целое, которое указывает сколько ‘объектов’ лежит между
этими указателями (с учетом знака).
Можно ли указатели складывать? – А можете придумать полезный смысл?
Указатели одного типа, как и большинство типов, можно присваивать: p = d;
Для вывода указателей применяется код %p - printf(“%p”, p);
Указатели и массивы
Попробуем создать указатель, который показывает на массив целых.
int m[8];
int *p = m; //Нет амперсанда (int *p = &x; )
Имя массива – это указатель на его начало!
Вот нам и пригодились операции над указателями.
p + i – новый указатель, который показывает на i-тый элемент
массива!
*(p + i) – ссылка на i-тый элемент.
Т.е. p[i] тождественно *(p + i).
Теперь понимаем, как выполняется доступ к i-тому элементу
массива:
Вычисляется его адрес (p+i) и получаем ссылку * (p+i).
Указатели и массивы
Выведем на консоль массив целых в одну строку,
разными способами.
for(int i = 0; i < sizeof(m)/sizeof(m[0]); i++)
printf(“%d ”, m[i]);
printf(“\n”);
for(int *p = m; p-m < sizeof(m)/sizeof(m[0]); p++)
printf(“%d ”, *p);
printf(“\n”);
Во втором случае мы не использовали счетчик цикла,
а обошлись указателями! Это чуть быстрее.
Указатели и структуры
Что еще можно делать над указателями?
К указателям на структуры имеется еще одна операция – стрелка!
Рассмотрим на примере.
// Объявляем структуру _student и тип Student
typedef struct _student
{
int ID;
char Name[64];
} Student;

//Работаем со структурой через указатель


Student s1, *p = &s1;
p -> ID=1;
strcpy(p -> Name, “Михаэль Шумахер”);

printf(“%d: %s\n”, (*p).ID, p -> Name);


Указатели
промежуточный итог
Как используются указатели в C на практике познакомимся позже. Хотя ответ
почти очевиден – как компьютер использует адреса?
- Знаем как объявить указатель и что это такое.
Используем * при объявлении. Переменная производного типа.
int *p = &x;
Знаем область значений, которые могут принимать указатели.
Значения указателей адреса?
Знаем набор операций над указателями
*, ++, --, += N, -=N, =, - и все операции сравнения.
Для вывода “%p”.
Понимаем связь между указателями и массивами
Имя массива – указатель на первый элемент.
Знаем, что для указатель на структуру есть операция стрелка: ->
-> позволяет получать поля структуры через указатель
Контрольные вопросы
1. Что представляет собой указатель?
2. Как объявить указатель? Имеет ли указатель тип?
3. Если p – указатель на переменную x, то что такое *p? Как
называется операция *?
4. Что получается при вычитании указателей одного типа?
5. Можно ли складывать указатели одного типа?
6. Как получить адрес переменной x?
7. Какие 3 смысла имеет операция * в разных контекстах?
8. Какие 3 смысла имеет операция & в разных контекстах?
9. Какой более привычyой операции эквивалентна команда
*(p+3) = 5?
10.Что означает “o->Val”? Что такое o? Что такое Val?
C-строка (z-строка)
В C нет встроенного типа строк. Вместо этого используют C-строки или z-строки.
C(z)-строка – массив символов, который заканчивается 0 (символа с кодом 0 нет!).
Рассмотрим код:
char s[] = “123”;
printf(“size of ‘%s’ = %d”, s, sizeof(s));
Результат >size of ‘123’ = 4
Что произошло?
Компилятор увидел объявления массива без размера, но с инициализатором.
- Вычисли необходимый объем памяти – 3 символа + завершающий 0, выделил
его и
- Заполнил массив данными из строкового литерала и добавил в конце 0.
При исполнении программы:
- Printf увидела %s и поняла, что надо выводить строку. Вывела все символы, пока
не встретила 0.
C-строка (z-строка)
C(z)-строка – массив символов, который заканчивается 0 (символа с
кодом 0 нет!).
Таким образом, объем памяти, необходимый под C-строку всегда на 1
больше чем количество символов в строке.
Кроме printf в C есть и другие функции, для работы со строками. Самые
частые из них:
strlen(s) – возвращает длину строки
strcpy(d, s) – копирует строку s в массив d.
Как в присваивании, сначала ‘куда’, а потом, ‘что’.
Важно! Ответственность за то, что в массиве d достаточно
памяти од строку s лежит на программисте!
Функция не может узнать его и просто копирует все, пока не встретит 0.
C-строка (z-строка)
C-строки – хороший полигон для изучения
указателей.
Вот как вычисляется длина строки s:
char *p = s;
while(*p) p++;
p – s равно длине строки.
Вот как копируются строки s в d:
for (char *p = d, *t = s; *p = * t; p++, t++);
Динамическая память – “куча”
Часто размер необходимой памяти становится ясен ТОЛЬКО по
ходу выполнения программы.
Тривиальный пример. Программа читает размер массива с
консоли и выполняет действия над ним.
Для этого имеется “куча” – не структурированная память.
Для выделения памяти в куче служит функция malloc
стандартной библиотеки C (<stdlib.h>).
Позволяет выделять память под данные.
int *p = malloc(n * sizeof(int));
Выделит память под массив из n целых.
Параметр malloc указывает количество байтов.
Динамическая память – “куча”
Память, выделяемая в “куче” иногда называют “динамической”, т.к.
на момент компиляции программы, ее объем не известен. Все
определяется в процессе выполнения.

Отвечает за освобождение динамической памяти программист. Для


этого существует функция стандартной библиотеки C - memfree.

memfree (p);

Здесь p – указатель, который был возвращен функцией malloc.

Управление динамической памятью – одна из центральных точек


внимания программистов на C!
Динамическая память
Примеры
Пусть нам нужно сложить 2 строки s1 и s2.
Задача решается так:
1. Подготовить достаточно места под сумму строк;
2. Скопировать первую строку;
3. Скопировать вторую, туда, где заканчивается
первая;

char *p = malloc(strlen(s1) + strlen(s2) + 1);


strcpy(p, s1);
strcpy(p + strlen(s1), s2);
Промежуточный итог
- Узнали что такое С или Z-строка.
Массив символов, который заканчивается 0.
- Знаем несколько операция над ними.
strlen strcpy;
- Познакомились с понятием динамической
памяти.
Память выделяемая в куче.
- Знаем правила работы с ней.
Функции malloc и memfree.
Контрольные вопросы
1. Что такое “куча” в контексте управления памятью в
C?

2. Почему память, выделяемая в “куче” называют


“динамической”?

3. Какие функции существуют в C для работы с


динамической памятью?

4. Какие функции стандартной библиотеки для работы


с z-строками были рассмотрены?
Контрольные вопросы
char *p = malloc(strlen(s1) + strlen(s2) + 1);
strcpy(p, s1);
strcpy(p + strlen(s1), s2);

1. Опишите как работает приведенный


фрагмент программы?
2. Как следует поступить с памятью
суммарной строки, когда она окажется не
нужна?