Адрес ячейки памяти Значение переменной в памяти 1000 1003

advertisement
Тема : знакомство с С (Си).
Рекурсия и стеки (продолжение) Содержание: указатели, аргументы функции , доступ к элементам массива через указатели , рекурсия,
разделение программы на файлы, упражнение 1, упражнение 2  Указатели
Указатель – это переменная, значением которой является адрес некоторого объекта (обычно другой
переменной) в памяти компьютера. Например, если одна переменная содержит адрес другой
переменной, то говорят, что первая переменная указывает (ссылается) на вторую (см. рисунок ниже).
Адрес ячейки памяти
Значение
переменной в памяти
1000
1003
1001
1002
1003
1004
1005
1006
Память
 Аргументы функции: вызовы по значению (by value) и по ссылке (by
reference)


вызовы по значению (by value)
o При его применении в формальный параметр функции копируется значение (value)
аргумента. В таком случае изменения параметра на аргумент не влияют.
вызовы по ссылке (by reference)
o При его применении копируется адрес аргумента. Это значит, что изменения значения
параметра приводят к точно таким же изменениям значения аргумента.
1 Данный пример (взят из материалов Владимира Вийеса) показывает особенности передачи значений
аргументов функции. В программе сначала происходит инициализация переменных m = 123,
n=999. Затем вызывается функция change1(),которая принимает в качестве аргументов
значения переменных m и n, то есть копируются значения 123 и 999. В самой функции происходит
обмен (swap) этих значений между переменными a и b, но при этом эти манипуляции не сказываются
на значениях переменных m и n. При вызове же функции change2(), в качестве аргументов
передаются (копируются) адреса переменных m и n. Таким образом, в функции change2()
происходит обращение через указатели к переменным m и n, и изменение их значений.
Пример: #include <stdio.h>
#include <stdlib.h>
void change1(int a, int b);
void change2(int *p, int *q);
void change1(int
int abi;
printf("a =
abi=a; a=b;
printf("a =
}
a, int b){
%d and b = %d\n" , a, b);
b=abi;
%d and b = %d\n" , a, b);
void change2(int *p, int *q){
int abi;
printf("addresses: p = %p and q = %p\n" , p, q);
printf("initial values: *p = %d and *q = %d\n" , *p,*q);
abi=*p; *p=*q; *q=abi;
printf("End values: *p = %d and *q = %d\n" , *p,*q);
}
int main(void){
int m = 123, n=999;
change1(m,n);
printf("change1: m=%d and n=%d\n" , m,n);
change2(&m,&n);
printf("change2: m=%d and n=%d\n", m,n);
return 0;
}
2 Вывод программы:
 Выделение памяти и доступ к элементам массива и структур через
указатели
Выделение памяти для массива (array) происходит либо при объявлении массива, когда известен
требуемый размер массива:
int arr[5];
либо динамически в процессе работы программы, с помощью функций malloc(), calloc(), используя
указатели.
int *arr;
arr = (int *) malloc(5 * sizeof(int));// malloc(количество выделяемых
// байтов);
либо
arr = (int *) calloc(5, sizeof(int)); // malloc(количество объектов,
// размер типа одного объекта);
При объявлении int arr[5], имя массива arr можно использовать в качестве указателя на 1-й
элемент массива. В следующем фрагменте кода адрес 1-ого элемента массива arr присваивается
указателю p:
int *p;
int arr[5];
p = arr;
В обоих переменных (p и arr) хранится адрес 1-ого элемента, отличаются эти переменные только тем,
что значение arr в программе изменить нельзя. Адрес первого элемента можно также получить,
использую оператор получения адреса &. Например, выражение arr и &arr[0] имеют одно и то же
значение.
При объявлении массива с указанием размера (или при динамическом выделении) выделяется
непрерывный кусок памяти для хранения всех элементов массива. Тип данных элементов массива
определяет, сколько памяти занимает каждый элемент (char – 1 байт, int – 4 байта, double – 8 байтов
(в зависимости от платформы)). Если взять, что каждая ячейка памяти может хранить 1 байт, то для
int arr[5] – выделится 20 ячеек(байтов).
3 Таким образом, arr и &arr[0] содержат адрес первого байта 1-ого элемента массива. Так как мы
указали, что тип данных массива – int, то компилятор знает, что значение хранится в первых четырех
байтах. При увеличении указателя на 1, (arr + 1) или &arr[1], переменная уже указывает на
начальный адрес 2-ого элемента массива. Обратиться к значению переменной i-ого элемента можно
через индекс массива arr[i] или указатель *(arr + i). Аналогично (через индекс массива или
указатели) можно обращаться и к структурам.
int – 4 байта
значение
*(arr+1) и arr[1] i = 0
i = 1
Адрес:
arr
&arr[0]
i = 2
i = 3
i = 4
Адрес: (arr +1) &arr[1]
 Рекурсия
Данный пример использования рекусии взят из предыдущего занятия. Функция main вызывает
функцию recursion. Это будет «первый уровень рекурсии». Затем функция recursion вызывает саму
себя. Это будет «вторым уровнем рекурсии» и так далее. Для того чтобы проследить за рекурсией
функция recursion выводит номера уровней в окно консоли.
Чтобы упростить понимание рекурсии, ниже показана функция recursion_plain(1), которая
раскрывает рекурсивный вызов функции recursion(1), то есть показывает последовательно действия
выполнение программы (проецирует рекурсивный вызов функции на одну плоскость). Черными
вертикальными линиями показаны область действия каждого вызова функции recursion(), начиная от
вызова recursion(1) и заканчивая вызовом recursion(5).
Пример: #include <stdio.h>
#include <stdlib.h>
void recursion (int n);
void recursion_plain(int n);
int main(void){
recursion(1);
recursion_plain(1);
return 0;
}
4 void recursion (int n){
printf ("Заходим на уровень %d\n", n); // "0"
if (n<5)
recursion (n+1);
printf ("Уходим с уровня %d\n",n); // "1"
}
void recursion_plain(int n){
printf ("Заходим на уровень %d\n", n); // n=1
if (n<5){
// recursion (n+1);  n=2
printf ("Заходим на уровень %d\n", ++n); // n=2
if (n<5){
//recursion (n+1);  n=3
printf ("Заходим на уровень %d\n", ++n); // n=3
if (n<5){
// recursion (n+1);  n=4
printf ("Заходим на уровень %d\n", ++n); // n=4
if (n<5){
// recursion (n+1);  n=5
printf ("Заходим на уровень %d\n", ++n); // n=5
1
2
3
4
5 if (n<5){
//recursion (n+1); n=5, сюда уже не заходим
}
printf ("Уходим с уровня %d\n",n--); // n=5
//выходим из recursion (5) в recursion (4)  n=4
}//n=4
printf ("Уходим с уровня %d\n",n--); // n=4
//выходим из recursion (4) в recursion (3)  n=3
}//n=3
printf ("Уходим с уровня %d\n",n--); // n=3
//выходим из recursion (3) в recursion (2)  n=2
}//n=2
printf ("Уходим с уровня %d\n",n--); // n=2
//выходим из recursion (2) в recursion (1)  n=1
}// n=1
printf ("Уходим с уровня %d\n",n); // n=1
}
5  Разделение программы на несколько файлов и их компиляция
Данная программа разбита на три файла: stack_func.h, stack_func.c, recursion_func.c.
Файл stack_func.h – заголовочный файл (header file), в котором объявлены константа N = 15 и два
прототипа функций стека: push() и pop(), которые определены в файле stack_func.c.
Так как в файле stack_func.c мы используем константу N = 15 и определяем функции push() и pop(),
то необходимо добавить в файл строчку: #include "stack_func.h". Функции push() и pop()
взяты из примеров предыдущих занятий и немного изменены.
В recursion_func.c программа просит ввести число n (n<=15) и затем рекурсивно вызывает функцию
recursion_func(), чтобы найти все множители для вычисления факториала n! , то есть n, n-1, n-2, …1.
При нахождении множителя, он помещается в стек, с помощью функции push(). После того как все
множители найдены, при помощи функции pop() множители поочередно извлекаются из стека и
вычисляется их произведение, результат которого сохраняется в переменной fact. Затем все
множители и значение факториала n выводятся на экран. Так как в recursion_func.c вызываются
функции push() и pop(), которые объявлены в stack_func.h, то необходимо также добавить в файл
строчку: #include "stack_func.h".
Для того, чтобы избежать включения файла stack_func.h несколько раз окружаем содержимое
«.h»-файла директивами препроцессора следующим образом (как на предыдущем занятии):
#ifndef STACK_FUNC_H #define STACK_FUNC_H ... // содержимое «.h» файла #endif
Пример: ###### Файл stack_func.h:
#ifndef STACK_FUNC_H
#define STACK_FUNC_H
#define N 15
void push(int i, int stack[], int *tos);
int pop(int *stack, int *tos);
#endif
6 ###### Файл stack_func.c:
#include <stdio.h>
#include <stdlib.h>
#include "stack_func.h"
void push(int i, int *stack, int *tos){
if(*tos + 1 == N){
printf("Push failed. Stack is full\n");
return;
}
(*tos)++;
stack[*tos] = i;
return;
}
int pop(int *stack, int *tos){
if(*tos == -1){
printf("Stack is empty\n");
return 0;
}
return stack[(*tos)--];
} ###### Файл recursion_func.c
#include <stdio.h>
#include <stdlib.h>
#include "stack_func.h"
void recursion_func(int n, int *stack, int *tos);
int main(void){
int n, i;
int *stack;
int fact;
int mult;
int tos =-1;
printf("Enter n: ");
scanf("%d", &n);
stack = (int *) calloc(n, sizeof(int));
recursion_func(n, stack, &tos);
fact = 1;
for(i=0; i<n; i++){
mult = pop(stack, &tos);
7 fact *= mult;
printf("%d: %d\n", i, mult);
}
printf("Factorial of %d is %d", n, fact);
free(stack);
return 0;
}
void recursion_func(int n, int *stack, int *tos){
push(n, stack, tos);
if(n<=1){
return;
}
recursion_func(n-1, stack, tos);
return;
}
Для того, чтобы скомпилировать программу, состоящую из несколько файлов, компилятору нужно
указать все «.с»-файлы. В данном случае файлы recursion_func.c и stack_func.c. Заголовочный файл
stack_func.h должен находиться в той же директории, что и «.с»-файлы.
NB! При компиляции в среде Dev-Cpp данные файлы должны находиться в одном проекте, то есть
необходимо создать проект, если не создан, и поместить в него эти файлы.
Вывод программы:
Упражнение 1:



Создать файлы stack_func.h, stack_func.c, recursion_func.c и скопировать в них содержимое
данного примера.
Откомпилировать и запустить программу. Проверить корректность выполнения программы
(правильно ли вычисляет факториал?).
Изменить программу таким образом, чтобы множители записывались в стек в обратном
порядке, то есть 1, 2, 3, …, n-1, n.
8 Упражнение 2:

Изменить программу из упражнения 3 предыдущего урока таким образом, чтобы слагаемые
записывались сначала в стек. Затем почередно извлекались из стека и вычислялась сумма
слагаемых.
Использованы материалы:

Владимир Вийес. Материалы лекций предмета «Programmeerimine II».

Марина Брик. Материалы практикумов предмета «Programmeerimine II».

Герберт Шилдт. Полный справочник по С. Четвертое издание.
Сергей Костин Подготовлено: 15.03.2013 Обновлено: 25.03.2014 9 
Download