Язык СИ. Часть 3. ПАМЯТЬ. УКАЗАТЕЛИ. МАССИВЫ
Классы памяти переменных КЛЮЧЕВЫЕ СЛОВА AUTO. STATIC. EXTERN. REGISTER
Локальные (автоматические) переменные – объявляются внутри какого-либо блока { }. Ключевое слово auto обычно опускают (подразумевается по умолчанию) main() { int i = 0; …. }. Область видимости – блок, т. е. такие переменные доступны только внутри того блока {}, где они объявлены! int main(){ { int j = 0; } j = 9; ОШИБКА! Undefined symbol ‘j’ }
Глобальные (внешние) переменные – объявляются вне какого-либо блока. Область видимости – любая функция, следующая за их объявлением. Не рекомендуется использовать! (такие программы очень сложно отлаживать) int j = 10; //Глобальная переменная int return. J(); //Прототип функции int main() { printf("j = %d, from function j = %d", j, return. J()); } int return. J() { return j; }
Ключевое слово extern Во всех файлах, образующих исходный текст программы, должно быть не больше одного определения одной и той же внешней переменной! В случае, когда переменная определена в одном файле, а использовать ее нужно в другом, необходимо в одном файле объявить ее как обычно, а во всех остальных – с ключевым словом extern. Файл 1. с: int Global. Var; Файл 2. с: extern int Global. Var;
Ключевое слово static Ограничивает область видимости внешней переменной или функции тем файлом, где она объявлена. Если локальную переменную объявить как static, то ее значение будет сохраняться между вызовами функции, в которой она объявлена. int return. J(); int main(){ printf("%d ", return. J()); printf("%d", return. J()); } int return. J(){ Присваивание 0 static j = 0; произойдет 1 раз! j++; return j; } Выведется: 1 2
Ключевое слово register сообщает компилятору, что соответствующая переменная будет интенсивно использоваться программой. Идея заключается в том, чтобы поместить такие регистровые переменные в регистры процессора и добиться повышения быстродействия и уменьшения объема кода. Впрочем, компилятор имеет право игнорировать эту информацию. register int x; register char с;
Построение проекта С/С++ Преобразование исходного кода в исполняемый exe-файл происходит в несколько этапов. Если вы пользуетесь IDE, то все эти этапы выполняет за вас она по нажатию на зеленую кнопку F 9 (для Builder) или F 5 (для VS). 0. Препроцессинг Препроцессор обрабатывает директивы, которые начинаются с символа #. Директива include заставляет препроцессор вставить в требуемое место все содержимое указанного файла. <имя_файла> - поиск файла в системных директориях. “имя_файла” – поиск файла рядом с файлом кода. С помощью директивы define можно объявлять макросы: #define идентификатор имя_для_замены В этом случае перед компиляцией препроцессор заменит все идентификаторы на имя_для_замены.
1. Компиляция: Файлы с исходным кодом (*. с, *. срр) Команды на языке высокого уровня (x += 8) объектные файлы (*. o, *. obj) машинные команды add eax, 8 2. Компоновка: объектные файлы + файлы библиотек (*. o, *. obj + *. lib) lib – статические библиотеки dll – динамические библиотеки исполняемые файлы (*. exe, *. dll, *. lib)
Организация памяти
Программы и процессы Программа – определенный набор инструкций. При каждом запуске программы для нее создается свой процесс. Процесс – экземпляр выполняемой программы. Это набор ресурсов и данных, использующихся при выполнении программы. Каждый процесс включает: 1) структуру данных, описывающую сам процесс (идентификатор процесса, список используемых ресурсов, статистическая информация); 2) адресное пространство – список доступных процессу адресов памяти; 3) исполняемая программа и данные.
Потоки Поток (thread) – некая сущность внутри процесса, получающая процессорное время для выполнения кода программы. В каждом процессе есть минимум один поток. Этот первичный поток создается системой автоматически при создании процесса. Далее этот поток может породить другие потоки, те в свою очередь новые и т. д.
Чтобы все потоки работали, операционная система отводит каждому из них определенное процессорное время. Тем самым создается иллюзия одновременного выполнения потоков (разумеется, для многопроцессорных компьютеров возможен истинный параллелизм). Планирование в Windows осуществляется на уровне потоков, а не процессов. Это кажется понятным, так как сами процессы не выполняются, а лишь предоставляют ресурсы и контекст для выполнения потоков.
Память Каждому процессу в 32 -разрядной системе доступно 4 Гб линейной виртуальной памяти. Каждый адрес указывает на конкретный байт памяти, поэтому с помощью 232 возможных комбинаций (не забываем, что адрес записывается в двоичном виде) можно адресовать 4 Гб памяти). В 64 -разрядных системах возможна адресация до 2 Тб памяти. За отображение виртуальной памяти на физическую и обратно отвечает компонент ОС под названием диспетчер памяти.
Описание областей Код – все операции (main(), printf()), написанные вами, + функции стандартных библиотек преобразуются компилятором в машинные команды, которые загружаются в память для последующего выполнения потоком (потоками). Пример: вычисление sin(x) call _sin add esp, 8 fstp [ebp+var_C] fld [ebp+var_4] fadd ds: dbl_40124 C add esp, 0 FFFFFFF 8 h ; x fstp [esp+38 h+var_38] Глобальные и статические переменные – статические данные, память для которых выделяется при запуске программы автоматически. Размер этой области можно вычислить при запуске и во время выполнения он не меняется!
Куча (heap) – область динамической памяти. Здесь хранятся данные, размер которых на момент запуска программы неизвестен. Эта память выделяется специальными функциями языка (пример – malloc, realloc). Ее размер во время выполнения программы может меняться. Пример: выделение памяти под массив из 1000 элементов типа int * i = (int)malloc(1000); Использование динамической памяти позволяет эффективно использовать память и управлять ею вручную (особенности языков С/С++).
Стек (stack) – используется для хранения локальных переменных, передачи параметров в функцию и для хранения адресов возврата из функции. Стек представляет из себя динамическую структуру данных, работающую по принципу LIFO – Last In First Out – последний вошел – первый вышел. Добавление (push) и извлечение (pop) данных всегда происходит по адресу вершины стека. Сохранение данных в стек Удаление данных из стека Ячейки памяти
Указатели
Указатель (pointer) * - это специальный тип данных, предназначенный для хранения адресов памяти, по которым находятся какие-либо данные. Пример: int number = 5; // переменная для хранения адреса переменной типа int * number. Addr = &number; printf("number = %d, number Address = %#x", number. Addr);
Операции * и & & - операция получения адреса объекта. * - операция ссылки по указателю (разыменование указателя). Применяя ее к указателю получаем объект, на который он указывает. Пример: int х = 1, у = 2 ; int *ip; /* ip - указатель на int */ ip = &х; /* ip теперь указывает на х */ y = *ip; /* у теперь равно 1 */ *ip = 0; /* х теперь равно 0 */
Указатели могут быть: ▪Указатель на объект (переменную). ▪Указатель на функцию. ▪void-указатель (неизвестно на что).
Операции с указателями - 6 Оп сравнения: == != > >= < <= - ++ (инкремент), -- (декремент), - вычитание, сложение, - присваивания: = для Ук одного типа !!! Пример: int * pk; // Значение pk увел-ся на размер типа (для int на 4 байта) pk++ ; //Сложение с константой pk = pk + 4; // то же самое, что pk = pk + 4 * ( размер типа )
Массивы
Массивы (arrays) Массив (array) – это набор данных одного типа, имеющий определенное имя. Нумерация элементов – с 0 до N-1, где N – длина массива. Объявление статического массива (Стат. М) : int. Array [10] ; Тип элементов массива Имя массива sizeof(int. Array) = ? Длина массива (количество элементов)
Способы указания размера Стат. М ▪Как сonst при объявлении: int arr[ 100 ] ; ▪именованная сonst в директиве Препроц: # define Narr 100. . . int arr[ Narr ]; ▪именованная сonst: const int M = 50; ▪ int arr[M]; Работает не во всех компиляторах!
Массивы в памяти Массив из 10 элементов типа short int (2 байта) Память Адрес 1000 1001 1002 1003 1004 располагаются непрерывно 1 Эл-т М a[0] a[1]. . . a[9] N – размер М a[N-1]
Обращение к элементам массива [ ] – операция индексации. int i, array [10] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; //вывод всех элементов массива for( i = 0; i<10; i++) printf(“a[%d] = %d”, i, array[i]); При попытке доступа к array[10] получим ошибку!
Массивы и указатели int * pa; int a[10]; pa = a; // то же самое что pa = &a[0]; int x = *pa; // что будет в х? int y = *(pa + 1); // что будет в y? Имя массива – это указатель на 0 -ой элемент!
Инициализация элементов массива int number[10] = {0, 1, 2, 3, 1}; //инициализация первых 5 эл-ов int number[10] = {0}; //инициализация всех эл-ов нулями int num [] = {0, 1, 2, 3, 1}; //определение длины массива printf("length(arr)=%d", sizeof(num)/sizeof(num[0])); //5 int i, number[10]; //инициализация в цикле for(i = 0; i<10; i++) { number[i] = i; }
const int N = 100; int m[ N ]; int *pm = m; /1/ Ввод массива – в цикле по индексу эл-ов: for( int i = 0; i < N; i++) scanf(“%d”, &m[ i ]); /2/ Ввод массива – в цикле по Ук for( int i = 0; i < N; i++) scanf(“%d”, pm++) ; // Почему не указываем Оп & ?
/3/ Ввод массива – в цикле по Ук for( int i = 0; i < N; i++) scanf(“%d”, pm+i) ; // В этом случае значение Ук pm не изм-ся // Можно ли вместо pm исп-ть Ук m ? ! for( int i = 0; i < N; i++) scanf(“%d”, m+i) ;
/1/ Вывод Массива – в цикле: for( int i = 0; i < N; i++) printf(“%d ”, m[ i ]); /2/ Вывод Массива по Ук: for( int i = 0; i < N; i++) printf(“%d ”, *pm++) ; // Вывод значения, расположенного // по Ук рm и увеличение Ук на sizeof(int) байт
/3/ Вывод Массива по Ук: // Увеличение Ук рm на sizeof(int)*i байта // и вывод значения, расположенного по Ук (pm + i) for( int i = 0; i < N; i++) printf(“%d ”, *(pm + i)) ; // Можно ли вместо pm использовать Ук m ? ! // Да, можно: printf(“%d ”, *(m + i)) ;
Двумерные массивы – матрицы (Матр) int А[ 3 ] [ 5 ] ; // Матрица 3 х 5, А={ ai, j } // 3 строки (m) и 5 столбцов (n) 0 1 2 3 4 0 1 2 3 4 5 А = 1 4 5 6 7 8 2 7 8 9 8 8 А[ i ] [ j ] – обращение к элементу матрицы по индексам Размерность Матр – 2 Размер памяти Матр: (размер типа) x m x n в нашем примере = 30
Инициализация Матр: int А[3] = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }; int N = 3; Ввод Матр – в Ц: for( int i = 0; i < N; i++) // Ввод строками for( int j = 0; j < N; j++) scanf(“%d”, &A[ i ] [ j ] ); Вывод Матр – аналогично Инициализация Матр – все эл-ты 0: int А[3] = { 0 } ; Удобно при отладке П
Строка Матрицы – одномерный М, int А[3] [3], *ptr ; ptr = &A[ 1 ] [ 0 ] ; // Адрес начала 1 -ой строки в Ук ptr, // с ptr можно работать как с М: // ptr[ i ] - i-ый эл-т 1 -ой строки Обращение к элементам матрицы: A – это Ук на Ук, Массив Ук на строки Матрицы A[ i ] – Массив элементов i-ой строки, A[ i ] [ j ] – j-й элемент Массива A[ i ] [ j ] = *( *(A + i ) + j ) = *(A[ i ] + j )