Главная страница
Навигация по странице:

  • Решение задач

  • Урок 7. Переменные, адреса и указатели

  • Урок 8. Функции. Передача аргументов по значению и по ссылке Общее представление

  • Статические переменные

  • Программирование в Linux. Учебное пособие С. В. Шапошникова, Лаборатория юного линуксоида, май 2012 1 Пояснительная записка


    Скачать 0.88 Mb.
    НазваниеУчебное пособие С. В. Шапошникова, Лаборатория юного линуксоида, май 2012 1 Пояснительная записка
    АнкорПрограммирование в Linux
    Дата02.12.2022
    Размер0.88 Mb.
    Формат файлаpdf
    Имя файлаProgramming Linux C.pdf
    ТипУчебное пособие
    #824650
    страница4 из 10
    1   2   3   4   5   6   7   8   9   10

    C
    , а с особенностью работы операционных систем, в которых реализован буферизованный ввод- вывод. При операциях ввода-вывода выделяется область временной памяти (буфер), куда и помещаются поступающие символы. Как только поступает специальный сигнал (например, переход на новую строку при нажатии Enter), данные из буфера передаются по месту своего назначения (на экран, в переменную и др.).
    Теперь, зная это, давайте посмотрим, что происходило в нашей программе, и сначала разберем второй случай с "некорректным пользователем", т.к. для понимания этот случай проще. Когда пользователь ввел первый символ, он попал в переменную a, далее сработала функция putchar(a)
    и символ попал в буфер. Т.к. Enter'а не было, то содержимое буфера на экране не было отображено. Пользователь ввел второй символ, переменная b получила свое значение, а putchar(b)
    отправила это значение в буфер. Аналогично с третьим символом.
    Как только пользователь нажал Enter, содержимое буфера было выведено на экран. Но символы, которые были выведены на экран, были выведены не программой, а операционной системой. Программа же выводила символы еще до того, как мы нажали Enter.
    Почему же в первом случае при выполнении программы мы смогли ввести и увидеть на экране только два символа? Когда был введен первый символ, то он был присвоен переменной a и далее выведен в буфер. Затем был нажат Enter. Это сигнал для выброса данных их буфера, но это еще и символ перехода на новую строку. Этот символ '\n' и был благополучно записан в переменную b. Тогда в буфере должен оказаться переход на новую строку, после этого введенный символ (уже помещенный в переменную c). После нажатия
    Enter мы должны были бы увидеть переход на новую строку от символа '\n' и букву. Однако печатается только буква. Почему?
    Во многих учебниках по языку
    C
    приводится пример считывания символов, вводимых пользователем, и их вывод на экран:
    int a;
    a = getchar();
    while (a != '\n') { putchar(a);
    a = getchar();
    }
    putchar('\n');
    29

    В переменной a всегда хранится последний введенный символ, но перед тем как присвоить a
    новое значение с помощью функции putchar()
    старое значение сбрасывается в буфер. Как только поступает символ новой строки, работа программы прекращается, а также, поскольку была нажата клавиша Enter, происходит вывод содержимого буфер на экран. Если в условии цикла while будет не символ '\n', а какой-нибудь другой, то программа будет продолжать обрабатывать символы, даже после нажатия Enter. В результате чего мы можем вводить и выводить множество строк текста.
    Задание
    Напишите программу посимвольного ввода-вывода, используя в качестве признака окончания ввода любой символ, кроме '\n'. Протестируйте ее.
    При совместном использовании функций putchar()
    и getchar()
    обычно пользуются более коротким способом записи. Например:
    while ((a = getchar()) != '

    ')
    putchar(a);
    Задание
    1. Объясните, почему сокращенный вариант записи посимвольного ввода-вывода работает правильно. Для этого опишите последовательность операций в условии цикла while.
    2. Перепишите вашу программу на более короткий вариант.
    EOF
    Как быть, если требуется "прочитать" с клавиатуры или файла неизвестный текст, в котором может быть абсолютно любой символ? Как сообщить программе, что ввод окончен, и при этом не использовать ни один из возможных символов?
    В операционных системах и языках программирования вводят специальное значение, которое служит признаком окончания потока ввода или признаком конца файла. Называется это значение EOF (end of file), а его конкретное значение может быть разным, но чаще всего это число -1. EOF представляет собой константу, в программном коде обычно используется именно имя (идентификатор) константы, а не число -1. EOF определена в файле stdio.h.
    В операционных системах GNU/Linux можно передать функции getchar()
    значение EOF, если нажать комбинацию клавиш Ctrl + D, в Windows – Ctrl + Z.
    Задание
    Исправьте вашу программу таким образом, чтобы считывание потока символов прерывалось признаком EOF.
    Решение задач
    Не смотря на свою кажущуюся примитивность, функции getchar()
    и putchar()
    часто используются, т.к. посимвольный анализ данных при вводе-выводе не такая уж редкая задача. Используя только функцию getchar()
    , можно получить массив символов (строку) и при этом отсеять ненужные символы. Вот пример помещения в строку только цифр из потока ввода, в котором может быть набран абсолютно любой символ:
    #include
    #define N 100 30
    main () { char ch;
    char nums[N];
    int i;
    i = 0;
    while ((ch = getchar()) != EOF && i < N-1)
    if (ch >= 48 && ch <= 57) {
    nums[i] = ch;
    i++;
    }
    nums[i] = '\0';
    printf("%s\n", nums);
    }
    Здесь ввод символов может прекратиться не только при поступлении значения EOF, но и в случае, если массив заполнен (
    i < N-1
    ). В цикле while проверяется условие, что числовой код очередного символа принадлежит диапазону [48, 57]. Именно в этом диапазоне кодируются цифры (0-9). Если поступивший символ является символом-цифрой, то он помещается в массив по индексу i, далее i увеличивается на 1, указывая на следующий элемент массива. После завершения цикла к массиву символов добавляется нулевой символ, т.к. по условию задачи должна быть получена строка (именно для этого символа ранее резервируется одна ячейка массива –
    N-1
    ).
    Задание
    1. Напишите программу, которая считает количество введенных пользователем символов и строк.
    2. Напишите программу, которая подсчитывает количество слов в строке.
    Урок 7. Переменные, адреса и указатели
    Переменная — это область памяти, к которой мы обращаемся за находящимися там данными, используя идентификатор (в данном случае, имя переменной). При этом у этой помеченной именем области есть еще и адрес, выраженный числом и понятный компьютерной системе. Этот адрес можно получить и записать в особую переменную.
    Переменную, содержащую адрес области памяти, называют указателем.
    Когда мы меняем значение обычной переменной, то, можно сказать, просто удаляем из конкретной области памяти данные и записываем туда новые. Когда мы меняем значение переменной-указателя, то начинаем работать с совершенно иным участком памяти, т.к. меняем содержащийся в ней адрес.
    Тема указателей тесно связана с темой динамических типов данных. Когда программа компилируется, то под объявленные переменные так или иначе (в зависимости от того, где они были объявлены) выделяются участки памяти. Потом размер этих участков не меняется, может меняться только их содержимое (значения или данные). Однако именно с помощью указателей можно захватывать и освобождать новые участки памяти уже в процессе
    31
    выполнения программы. Динамические типы данных будут рассмотрены позже.
    Прежде чем перейти к рассмотрению объявления и определения переменных-указателей, посмотрим, что из себя представляет адрес любой переменной и как его получить.
    int i = 0;
    printf ("i=%d, &i=%p \n", i, &i);
    В результате выполнения данного программного кода на экране появляется примерно следующее (шестнадцатеричное число у вас будет другим):
    i=0, &i=0x7fffa40c5fac
    Знак амперсанда (&) перед переменной позволяет получить ее адрес в памяти. Для вывода адреса переменной на экран используется специальный формат
    %p
    . Адреса обычных переменных (не указателей) в процессе выполнения программы никогда не меняются. В этом можно убедиться:
    int a = 6;
    float b = 10.11;
    char c = 'k';
    printf("%d - %p, %.2f - %p, %c - %p\n", a,&a, b,&b, c,&c);
    a = 2; b = 50.99; c = 'z';
    printf("%d - %p, %.2f - %p, %c - %p\n", a,&a, b,&b, c,&c);
    Результат:
    6 - 0x7fff8e1d38e4, 10.11 - 0x7fff8e1d38e8, k - 0x7fff8e1d38ef
    2 - 0x7fff8e1d38e4, 50.99 - 0x7fff8e1d38e8, z - 0x7fff8e1d38ef
    Как мы видим, несмотря на то, что значения переменных поменялись, ячейки памяти остались прежними.
    32

    Зная адрес, можно получить значение, которое находится по этому адресу, поставив знак * перед адресом:
    int a = 8;
    printf("%d \n", *&a);
    На экране будет выведено число 8.
    Однако запись типа
    &a не всегда возможна или удобна. Поэтому существует специальный тип данных — указатели, которым и присваивается адрес на область памяти.
    Указатели в языке
    C
    , как и другие переменные, являются типизированными, т.е. при объявлении указателя надо указывать его тип. Как мы узнаем позже, с указателями можно выполнять некоторые арифметические операции, и именно точное определение их типа позволяет протекать им корректно. Чтобы объявить указатель, надо перед его именем поставить знак *. Например:
    int *pi;
    float *pf;
    Обратите внимание на то, что в данном случае * говорит о том, что объявляется переменная- указатель. Когда * используется перед указателем не при его объявлении, а в выражениях, то обозначает совсем иное — "получить значение (данные) по адресу, который присвоен указателю". Посмотрите на код ниже:
    int x = 1, y, z = 3;
    int *p, *q;
    p = &x;
    printf("%d\n", *p); // 1
    y = *p;
    printf("%d\n", y); // 1
    *p = 0;
    printf("%d %d\n", x, y); // 0 1
    q = &z;
    printf("%d\n", *q); // 3
    p = q;
    *p = 4;
    printf("%d\n", z); // 4
    printf("%p %p\n", p, q); // p == q
    С помощью комментариев указаны текущие значения ячеек памяти. Подробно опишем, что происходит:
    1. Выделяется память под пять переменных: три типа int и два указателя на int
    . В ячейки x и z записываются числа 1 и 3 соответственно.
    2. Указателю p присваивается адрес ячейки x. Извлекая оттуда значение (
    *p
    ), получаем
    1.
    3. В область памяти, которая названа именем у, помещают значение равное содержимому ячейки, на которую ссылается указатель p. В результате имеем две области памяти (x и y), в которые записаны единицы.
    4. В качестве значения по адресу p записываем 0. Поскольку p указывает на x, то значение x меняется. Переменная p не указывает на y, поэтому там остается прежнее
    33
    значение.
    5. Указателю q присваивается адрес переменной z. Извлекая оттуда значение (
    *q
    ), получаем 3.
    6. Переменной p присваивается значение, хранимое в q. Это значит, что p начинает ссылаться на тот же участок памяти, что и q. Поскольку q ссылается на z, то и p
    начинает ссылаться туда же.
    7. В качестве значения по адресу p записываем 4. Т.к. p является указателем на z, следовательно, меняется значение z.
    8. Проверяем, p и q являются указателями на одну и туже ячейку памяти.
    Если для вас вышеописанное не очевидно, то повторите урок сначала, почитайте другие источники и добейтесь полного понимания, т.к. без этого дальше двигаться бесполезно.
    Под сам указатель (там, где хранится адрес) также должна быть выделена память. Объем этой памяти можно узнать с помощью функции sizeof()
    :
    int *pi;
    float *pf;
    printf("%lu\n", sizeof(pi)); printf("%lu\n", sizeof(pf));
    Под указатели всех типов выделяется одинаковый объем памяти, т.к. размер адреса не зависит от типа, а зависит от вычислительной системы. В таком случае, зачем при объявлении указателя следует указывать тип данных, на который он может ссылаться? Дело в том, что по типу данных определяется, сколько ячеек памяти занимает значение, на которое ссылается указатель, и через сколько ячеек начнется следующее значение.
    Если указатель объявлен, но не определен, то он ссылается на произвольный участок памяти с неизвестно каким значением:
    int *pa, *pb;
    float *pc;
    printf(" %p %p %p\n", pa, pc, pb);
    printf(" %d %f\n", *pa, *pc); // может возникнуть ошибка
    Результат (в Ubuntu):
    0x400410 0x7fff5b729580 (nil)
    -1991643855 0.000000
    Использование неопределенных указателей в программе при вычислениях чревато возникновением серьезных ошибок. Чтобы избежать этого, указателю можно присвоить значение, говорящее, что указатель никуда не ссылается (NULL). Использовать такой указатель в выражениях не получится, пока ему не будет присвоено конкретное значение:
    int a = 5, b = 7;
    float c = 6.98;
    int *pa, *pb;
    float *pc;
    pa = pb = NULL;
    pc = NULL;
    printf(" %15p %15p %15p\n", pa, pc, pb);
    // printf(" %15d %15f %15d\n", *pa, *pc, *pb); // Error
    34
    pa = &a;
    pb = &b;
    pc = &c;
    printf(" %15p %15p %15p\n", pa, pc, pb);
    printf(" %15d %15f %15d\n", *pa, *pc, *pb);
    Результат (в Ubuntu):
    (nil) (nil) (nil)
    0x7fffeabf4e44 0x7fffeabf4e4c 0x7fffeabf4e48 5 6.980000 7
    В данном случае, если попытаться извлечь значение из памяти с помощью указателя, который никуда не ссылается, то возникает "ошибка сегментирования".
    На этом уроке вы должны понять, что такое адрес переменной и как его получить (
    &var
    ), что такое переменная-указатель (
    type *p_var; p_var = &var
    ) и как получить значение, хранимое в памяти, зная адрес ячейки (
    *p_var
    ). Однако у вас может остаться неприятный осадок из-за непонимания, зачем все это надо? Это нормально. Понимание практической значимости указателей придет позже по мере знакомства с новым материалом.
    Задание
    Практически проверьте результат работы всех примеров данного урока, придумайте свои примеры работы с указателями.
    Урок 8. Функции. Передача аргументов по значению и по
    ссылке
    Общее представление
    Язык
    C
    как и большинство других языков программирования позволяет создавать программы, состоящие из множества функций, а также из одного или нескольких файлов исходного кода. До сих пор мы видели только функцию main()
    , которая является главной в программе на
    C
    , поскольку выполнение кода всегда начинается с нее. Однако ничего не мешает создавать другие функции, которые могут быть вызваны из main()
    или любой другой функции. На этом уроке мы рассмотрим создание только однофайловых программ, содержащих более чем одну функцию.
    При изучении работы функций важно понимать, что такое локальная и что такое глобальная переменные. В языке программирования
    C
    глобальные (внешние) переменные объявляются вне какой-либо функции. С их помощью удобно организовывать обмен данными между функциями, однако это считается дурным тоном, т.к. легко запутывает программу.
    Локальные переменные в языке программирования
    C
    называют автоматическими. Область действия автоматических переменных распространяется только на ту функцию, в которой они были объявлены. Параметры функции также являются локальными переменными.
    Структурная организация файла программы на языке C, содержащего несколько функций, может выглядеть немного по-разному. Т.к. выполнение начинается с main()
    , то ей должны быть известны спецификации (имена, количество и тип параметров, тип возвращаемого значения) всех функций, которые из нее вызываются. Отсюда следует, что объявляться функции должны до того, как будут вызваны. А вот определение функции уже может следовать и до и после main()
    . Рассмотрим такую программу:
    #include
    float median (int a, int b); // объявление функции
    35
    main () {
    int num1 = 18, num2 = 35;
    float result;
    printf("%10.1f\n", median(num1, num2));
    result = median(121, 346);
    printf("%10.1f\n", result);
    printf("%10.1f\n", median(1032, 1896));
    }
    float median (int n1, int n2) { // определение функции float m;
    m = (float) (n1 + n2) / 2;
    return m;
    }
    В данном случае в начале программы объявляется функция median()
    . Объявляются тип возвращаемого ею значения (
    float
    ), количество и типы параметров (
    int a, int b
    ).
    Обратите внимание, когда объявляются переменные, то их можно группировать: int a, b;
    Однако с параметрами функций так делать нельзя, для каждого параметра тип указывается отдельно:
    (int a, int b)
    Далее идет функция main()
    , а после нее — определение median()
    . Имена переменных- параметров в объявлении функции никакой роли не играют (их вообще можно опустить, например, float median (int, int);
    ). Поэтому когда функция определяется, то имена параметров могут быть другими, однако тип и количество должны строго совпадать с объявлением.
    Функция median()
    возвращает число типа float
    . Оператор return возвращает результат выполнения переданного ему выражения; после return функция завершает свое выполнение, даже если далее тело функции имеет продолжение. Функция median() вычисляет среднее значение от двух целых чисел. В выражении
    (float) (n1 + n2) / 2 сначала вычисляется сумма двух целых чисел, результат преобразуется в вещественное число и только после этого делится на 2. Иначе мы бы делили целое на целое и получили целое (в таком случае дробная часть просто усекается).
    В теле main()
    функция median()
    вызывается три раза. Результат выполнения функции не обязательно должен быть присвоен переменной.
    Вышеописанную программу можно было бы записать так:
    #include
    float median (int n1, int n2) {
    float m;
    m = (float) (n1 + n2) / 2;
    return m;
    }
    main () {
    int num1 = 18, num2 = 35;
    float result;
    printf("%10.1f\n", median(num1, num2));
    result = median(121, 346);
    printf("%10.1f\n", result);
    printf("%10.1f\n", median(1032, 1896));
    }
    36

    Хотя такой способ и экономит одну строчку кода, однако главная функция, в которой отражена основная логика программы, опускается вниз, что может быть неудобно. Поэтому первый вариант предпочтительней.
    Задание
    Напишите функцию, возводящую куб числа, переданного ей в качестве аргумента. Вызовите эту функцию с разными аргументами.
    Статические переменные
    В языке программирования
    C
    существуют так называемые статические переменные. Они могут быть как глобальными, так и локальными. Перед именем статической переменной пишется ключевое слово static
    Внешние статические переменные, в отличие от обычных глобальных переменных, нельзя использовать из других файлов в случае программы, состоящей не из одного файла. Они глобальны только для функций того файла, в котором объявлены. Это своего рода сокрытие данных, по принципу "не выставлять наружу ничего лишнего, чтобы 'что-нибудь' нечаянно не могло 'испортить' данные".
    Статические переменные, объявленные внутри функций имеют такую же область действия, как автоматические. Однако в отличие от автоматических, значения локальных статических переменных не теряются, а сохраняются между вызовами функции:
    #include
    int hello();
    main() {
    printf(" - %d-й вызов\n", hello());
    printf(" - %d-й вызов\n", hello());
    printf(" - %d-й вызов\n", hello());
    }
    int hello () {
    static count = 1;
    printf("Hello world!");
    return count++;
    }
    Результат:
    Hello world! - 1-й вызов
    Hello world! - 2-й вызов
    Hello world! - 3-й вызов
    В этом примере в функции hello()
    производится подсчет ее вызовов.
    Задание
    Придумайте свой пример с использованием статической переменной.
    1   2   3   4   5   6   7   8   9   10


    написать администратору сайта