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

  • Урок 11. Особенности массивов при работе с указателями К указателям можно прибавлять целые числа

  • Имя массива содержит адрес его первого элемента

  • Взаимозаменяемость имени массива и указателя

  • Имя массива — это указатель-константа

  • Урок 12. Массивы и функции

  • Урок 13. Особенности работы со строками Неформатированные ввод из стандартного потока и вывод в

  • Массив символов и указатель на строку

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


    Скачать 0.88 Mb.
    НазваниеУчебное пособие С. В. Шапошникова, Лаборатория юного линуксоида, май 2012 1 Пояснительная записка
    АнкорПрограммирование в Linux
    Дата02.12.2022
    Размер0.88 Mb.
    Формат файлаpdf
    Имя файлаProgramming Linux C.pdf
    ТипУчебное пособие
    #824650
    страница6 из 10
    1   2   3   4   5   6   7   8   9   10
    Равновероятные случайные числа
    Функция rand()
    генерирует любое случайное число от 0 до
    RAND_MAX
    с равной долей вероятности. Другими словами, у числа 100 есть такой же шанс выпасть, как и у числа 25876.
    Чтобы доказать это, достаточно написать программу, подсчитывающую количество выпадений каждого из значений. Если выборка (количество "испытуемых") будет достаточно большой, а диапазон (разброс значений) маленьким, то мы должны увидеть, что процент выпадений того или иного значения приблизительно такой же как у других.
    #include
    #include
    #define N 500
    main () {
    int i;
    int arr[5] = {0};
    srand(time(NULL));
    for (i=0; i < N; i++) switch (rand() % 5) {
    case 0: arr[0]++; break;
    case 1: arr[1]++; break;
    case 2: arr[2]++; break;
    case 3: arr[3]++; break;
    case 4: arr[4]++; break;
    }
    for (i=0; i < 5; i++) printf("%d - %.2f%%\n", i, ((float) arr[i] / N) * 100);
    }
    В приведенной программе массив из пяти элементов сначала заполняется нулями.
    Случайные числа генерируются от 0 до 4 включительно. Если выпадает число 0, то увеличивается значение первого элемента массива, если число 1, то второго, и т.д. В конце на экран выводится процент выпадения каждого из чисел.
    Задание
    Спишите данную программу. Посмотрите на результат ее выполнения при различных значениях N: 10, 50, 500, 5000, 50000. Объясните увиденное.
    45

    Урок 11. Особенности массивов при работе с указателями
    К указателям можно прибавлять целые числа
    Рассмотрим программу:
    #include
    #define N 5
    main () {
    int arrI[N], i;
    for (i=0; iprintf("%p\n", &arrI[i]);
    }
    Создается массив arrI, далее в цикле for выводятся значения адресов ячеек памяти каждого элемента массива. Результат выполнения программы будет выглядеть примерно так:
    0x7ffffbff4050 0x7ffffbff4054 0x7ffffbff4058 0x7ffffbff405c
    0x7ffffbff4060
    Обратите внимание на то, что значение адреса каждого последующего элемента массива больше значения адреса предыдущего элемента на 4 единицы. В вашей системе эта разница может составлять 2 единицы. Такой результат вполне очевиден, если вспомнить, сколько байтов отводится на одно данное типа int
    , и что элементы массива сохраняются в памяти друг за другом.
    Теперь объявим указатель на целый тип и присвоим ему адрес первого элемента массива:
    int *pI;
    pI = &arrI[0];
    Цикл for изменим таким образом:
    for (i=0; iprintf("%p\n", pI + i);
    Здесь к значению pI, которое является адресом ячейки памяти, прибавляется сначала 0, затем
    1, 2, 3 и 4. Можно было бы предположить, что прибавление к pI единицы в результате дает адрес следующего байта за тем, на который указывает pI. А прибавление двойки вернет адрес байта, через один от исходного. Однако подобное предположение не верно.
    Вспомним, что тип указателя сообщает, на сколько байт простирается значение по адресу, на который он указывает. Таким образом, хотя pI указывает только на один байт (первый), но "знает", что его "собственность" простирается на все четыре (или два). Когда мы прибавляем к указателю единицу, то получаем указатель на следующее значение, но никак не на следующий байт. А следующее значение начинается только через 4 байта (в данном случае).
    Поэтому результат выполнения приведенного цикла с указателем правильно отобразит адреса элементов массива.
    Задание
    Убедитесь в этом сами.
    Прибавляя к указателям (или вычитая из них) целые значения, мы имеем дело с так называемой адресной арифметикой.
    46

    Задание
    Напишите программу, в которой объявлен массив вещественных чисел из десяти элементов.
    Присвойте указателю адрес четвертого элемента, затем, используя цикл, выведите на экран адреса 4, 5 и 6-ого элементов массива.
    Имя массива содержит адрес его первого элемента
    Да, это именно так, данный факт следует принять как аксиому. Вы можете убедиться в этом выполнив такое выражение:
    printf("%p = %p\n", arrI, &arrI[0]);
    Отсюда следует, что имя массива – это ничто иное, как указатель. (Хотя это немного особенный указатель, о чем будет упомянуто ниже.) Поэтому выражения pI = &arrI[N]
    и pI = arrI
    дают одинаковый результат: присваивают указателю pI
    адрес первого элемента массива.
    Раз имя массива — это указатель, ничего не мешает получать адреса элементов вот так:
    for (i=0; iprintf("%p\n", arrI + i);
    Соответственно значения элементов массива можно получить так:
    for (i=0; iprintf("%d\n", *(arrI + i));
    Примечание. Если массив был объявлен как автоматическая переменная (т.е. не глобальная и не статическая) и при этом не был инициализирован (не присваивались значения), то в нем будет содержаться "мусор" (случайные числа).
    Получается, что запись вида arrI[3] является сокращенным (более удобным) вариантом выражения *(arr+3).
    Взаимозаменяемость имени массива и указателя
    Если имя массива является указателем, то почему бы не использовать обычный указатель в нотации обращения к элементам массива также, как при обращении через имя массива:
    int arrI[N], i;
    int *pI;
    pI = arrI;
    for (i=0; iprintf("%d\n", pI[i]);
    Отсюда следуют выводы. Если arrI — массив, а pI — указатель на его первый элемент, то пары следующих выражений дают один и тот же результат:

    arrI[i]
    и pI[i]
    ;

    &arrI[i]
    и
    &pI[i]
    ;

    arrI+i и pI+i
    ;

    *(arrI+i)
    и
    *(pI+i)
    Задание
    Что получается в результате выполнения данных пар выражений: адреса или значения элементов массива? Если вы испытываете трудности при ответе на этот вопрос, перечитайте урок 7, этот урок, изучите другие источники.
    47

    Указателю pI можно присвоить адрес любого из элементов массива. Например, так pI =
    &arrI[2]
    или так pI = arr+2
    . В таком случае результат приведенных выше пар выражений совпадать не будет. Например, когда будет выполняться выражение arrI[i]
    , то будет возвращаться i-ый элемент массива. А вот выражение pI[i]
    уже вернет не i-ый элемент от начала массива, а i-ый элемент от того, адрес которого был присвоен pI
    . Например, если pI был присвоен адрес третьего элемента массива (
    pI = arr+2
    ), то выражение arrI[1]
    вернет значение второго элемента массива, а pI[1]
    — четвертого.
    Задание
    Присвойте указателю (
    pI) ссылку не на первый элемент массива (arrI). В одном и том же цикле выводите результат выражений arrI[i] и pI[i], где на каждой итерации цикла i для обоих выражений имеет одинаковое значение. Объясните результат выполнения такой программы.
    Имя массива — это указатель-константа
    Несмотря на вышеописанную взаимозаменяемость имени массива определенного типа на указатель того же типа, между ними есть разница. Указатель может указывать на любой элемент массива, его значение можно изменять. Имя массива всегда указывает только на первый элемент массива, изменять его значение нельзя.
    Это значит, что выражение pI = arrI
    допустимо, а arrI = pI
    нет. Имя массива является константой. При этом не надо путать имя массива (адрес) и значения элементов массива.
    Последние константами не являются. Действительно, ведь для всех переменных мы не можем менять их адрес в процессе выполнения программы, можем менять лишь их значения.
    В этом смысле имя массива — это обычная переменная, хотя и содержащая адрес.
    Как следствие в программном коде выражения присваивания, инкрементирования и декрементирования допустимы для указателей, а для имени массива — запрещены.
    Задание
    Посмотрите на программу ниже. Что она делает? Почему? Проверьте ваши рассуждения опытным путем.
    #include
    main () {
    char str[20], *ps = str, n=0;
    printf("Enter word: ");
    scanf("%s", str);
    while(*ps++ != '\0') n++;
    printf("%d\n", n);
    }
    Урок 12. Массивы и функции
    Массивы, также как остальные переменные, можно передавать в функции в качестве аргументов. Рассмотрим такую программу:
    #include
    #include
    #define N 10 48
    void arr_make(int arr[], int min, int max);
    main () {
    int arrI[N], i;
    arr_make(arrI, 30, 90);
    for (i=0; i printf("%d ", arrI[i]);
    printf("\n");
    }
    void arr_make(int arr[], int min, int max) {
    int i;
    srand(time(NULL));
    for (i=0; i arr[i] = rand() % (max - min + 1) + min;
    }
    В теле функции main()
    объявляется массив, состоящий из 10 элементов. Далее вызывается функция arr_make()
    , которой передаются в качестве аргументов имя массива и два целых числа.
    Если посмотреть на функцию arr_make()
    , то можно заметить, что ее первый параметр выглядит немного странно. Функция принимает массив неизвестно какого размера. Если предположить, что массивы передаются по значению, т.е. передаются их копии, то как при компиляции будет вычислен необходимый объем памяти для функции arr_make()
    , если неизвестно какого размера будет один из ее параметров?
    На прошлом уроке мы выяснили, что имя массива — это константный указатель на первый элемент массива; т.е. имя массива содержит адрес. Выходит, что мы передаем в функцию копию адреса, а не копию значения. Как мы уже знаем, передача адреса приводит к возможности изменения локальных переменных в вызывающей функции из вызываемой.
    Ведь на одну и ту же ячейку памяти могут ссылаться множество переменных, и изменение значения в этой ячейке с помощью одной переменной неминуемо отражается на значениях других переменных.
    Описание вида arr[]
    в параметрах функций говорит о том, что в качестве значения мы получаем указатель на массив, а не обычную (скалярную) переменную типа int, char, float и т.п.
    Задание
    Проверьте как работает программа. Что происходит внутри тела функции arr_make()?
    Продолжим рассуждения. Если в функцию передается только адрес массива, то в теле функции никакого массива не существует, и когда там выполняется выражение типа arr[i]
    , то на самом деле arr — это не имя массива, а переменная-указатель, к которой прибавляется смещение. Поэтому цикл в функции arr_make()
    можно переписать на такой:
    for(i=0; i*arr++ = rand() % (max - min + 1) + min;
    В теле цикла результат выражения справа от знака присваивания записывается по адресу, на который указывает arr. За это отвечает выражение
    *arr
    . Затем указатель arr начинает указывать на следующую ячейку памяти, т.к. к нему прибавляется единица (
    arr++
    ). Еще раз: сначала выполняется выражение записи значения по адресу, который содержится в arr; после чего изменяется адрес, содержащийся в указателе (сдвигается на одну ячейку памяти
    49
    определенного размера).
    Поскольку мы можем изменять arr, это доказывает, что arr — обычный указатель, а не имя массива. Тогда зачем в заголовке функции такой гламур, как arr[]
    ? Действительно, чаще используют просто переменную-указатель:
    void arr_make(int *arr, int min, int max);
    Хотя в таком случае становится не очевидно, что принимает функция - указатель на обычную переменную или все-таки на массив. В любом случае она будет работать.
    Задание
    Перепишите программу с использованием нотации указателей.
    Часто при передаче в функцию массивов туда же передают и количество его элементов в виде отдельного параметра. В примере выше N является глобальной константой, поэтому ее значение доступно как из функции main()
    , так и arr_make()
    . Иначе, более грамотно было бы написать функцию arr_make()
    так:
    void arr_make(int *arr, int n, int min, int max) {
    int i;
    srand(time(NULL));
    for (i=0; i arr[i] = rand() % (max - min + 1) + min;
    }
    В данном случае параметр n — это количество обрабатываемых элементов массива.
    Следует еще раз обратить внимание на то, что при передачи имени массива в функцию, последняя может его изменять. Однако такой эффект не всегда является желательным.
    Конечно, можно просто не менять значения элементов массива внутри функции, как в данном примере, где вычисляется сумма элементов массива; при этом сами элементы никак не изменяются:
    int arr_sum(int *arr) {
    int i, s=0;
    for(i=0; is = s + arr[i];
    }
    return s;
    }
    Но если вы хотите написать более надежную программу, в которой большинство функций не должны менять значения элементов массивов, то лучше в заголовках этих функций объявлять параметр-указатель как константу, например:
    int arr_sum(const int *arr);
    В этом случае, любая попытка изменить значение по адресу, содержащемуся в таком константном указателе, будет приводить к ошибке и программист будет знать, что в функция пытается изменить массив.
    Усовершенствуем программу, которая была приведена в начале этого урока:
    #include
    #include
    #define N 10
    void arr_make(int *arr, int min, int max);
    50
    void arr_inc_dec(int arr[], char sign);
    void arr_print(int *arr);
    main () {
    int arrI[N], i, minimum, maximum;
    char ch;
    printf("Enter minimum & maximum: ");
    scanf("%d %d", &minimum, &maximum);
    arr_make(arrI, minimum, maximum);
    arr_print(arrI);
    scanf("%*c"); // избавляемся от \n printf("Enter sign (+,-): ");
    scanf("%c", &ch);
    arr_inc_dec(arrI, ch);
    arr_print(arrI);
    }
    void arr_make(int *arr, int min, int max) { int i;
    srand(time(NULL));
    for(i=0; i *arr++ = rand() % (max - min + 1) + min;
    }
    void arr_inc_dec(int *arr, char sign) {
    int i;
    for (i=0; iif (sign == '+') arr[i]++;
    if (sign == '-') arr[i]--;
    }
    }
    void arr_print(int *arr) {
    int i;
    printf("The array is: ");
    for (i=0; iprintf("%d ", *arr++);
    printf("\n");
    }
    Теперь у пользователя запрашивается минимум и максимум, затем создается массив из элементов, значения которых лежат в указанном диапазоне. Массив выводится на экран с помощью функции arr_print()
    . Далее у пользователя запрашивается знак + или -.
    Вызывается функция arr_inc_dec()
    , которая в зависимости от введенного знака увеличивает или уменьшает на единицу значения элементов массива.
    В функциях arr_make()
    и arr_print()
    используется нотация указателей. Причем в теле функций значения указателей меняются: они указывают сначала на первый элемент массива, затем на второй и т.д. В функции arr_inc_dec()
    используется вид обращения к элементам массива. При этом значение указателя не меняется: к arr прибавляется смещение, которое увеличивается на каждой итерации цикла. Ведь на самом деле запись arr[i]
    означает
    *(arr+i)
    При использовании нотации обращения к элементам массива программы получаются более ясные, а при использовании записи с помощью указателей они компилируются чуть быстрее.
    51

    Это связано с тем, что когда компилятор встречает выражение типа arr[i]
    , то он тратит время на преобразование его к виду
    *(arr+i)
    . Однако лучше потратить лишнюю секунду при компиляции, но получить более читаемый код.
    Задания
    1. Переделайте программу, которая приведена выше таким образом, чтобы она работала с вещественными числами. Вместо функции arr_inc_dec() напишите другую, которая изменяет значения элементов массива на любое значение, которое указывает пользователь.
    2. Напишите программу, в которой из одной функции в другую передается указатель не на начало массива, а на его середину.
    3. Напишите программу, в которой из функции main() в другую функцию передаются два массива: "заполненный" и "пустой". В теле этой функции элементам "пустого" массива должны присваиваться значения, так или иначе преобразованные из значений элементов "заполненного" массива, который не должен изменяться.
    Урок 13. Особенности работы со строками
    Неформатированные ввод из стандартного потока и вывод в
    стандартный поток
    С помощью функции printf()
    можно легко вывести на экран строку, содержащую пробелы:
    printf("%s", "Hello world");
    С другой стороны, ввести строку произвольной длины, содержащую пробелы в неизвестных местах, исключительно с помощью функции scanf()
    невозможно. Для scanf()
    любой символ пустого пространства является сигналом завершения ввода очередных данных, если только не производится считывание символа.
    На помощь может прийти функция getchar()
    , осуществляющая посимвольный ввод данных:
    int i;
    char str[20];
    for (i=0; (str[i] = getchar()) != '\n'; i++);
    str[i] = '\0';
    printf("\n%s\n", str);
    В заголовке цикла getchar() возвращает символ, далее записываемый в очередную ячейку массива. После этого элемент массива сравнивается с символом '\n'. Если они равны, то цикл завершается. После цикла символ '\n' в массиве "затирается" символом '\0'. В условии цикла должна быть также предусмотрена проверка на выход за пределы массива; чтобы не усложнять пример, опущена.
    Однако в языке программирования
    C
    работать со строками можно проще. С помощью функций стандартной библиотеки gets()
    и puts()
    получают строку из стандартного потока и выводят в стандартный поток. Буква s в конце слов gets и puts является сокращением от слова string (строка).
    В качестве параметров обе функции принимают указатель на массив символов (либо имя массива, либо указатель).
    Функция gets()
    помещает полученные с ввода символы в указанный в качестве аргумента массив. При этом символ перехода на новую строку, который завершает ее работу, игнорируется.
    52

    Функция puts()
    выводит строку на экран и при этом сама добавляет символ перехода на новую строку. Простейший пример использования этих функций выглядит так:
    char str[20];
    gets(str);
    puts(str);
    Итак, если вы работаете со строками, а не другими типами данных, при этом нет необходимости выполнять их посимвольную обработку, то удобнее пользоваться функциями puts()
    и gets()
    . В таком случае даже не надо подключать заголовочный файл stdio.h.
    Массив символов и указатель на строку
    Как мы знаем, строка представляет собой массив символов, последний элемент которого является нулевым символом по таблице ASCII, обозначаемым '\0'. При работе со строками также как с численными массивами можно использовать указатели. Мы можем объявить в программе массив символов, записать туда строку, потом присвоить указателю адрес на первый или любой другой элемент этого массива и работать со строкой через указатель: char name[30];
    char *nP;
    printf("Введите имя и фамилию: ");
    gets(name);
    printf("Имя: ");
    for(nP = name; *nP != ' '; nP++)
    putchar(*nP);
    printf("\nФамилия: ");
    puts(nP+1);
    В заголовке цикла указателю сначала присваивается адрес первого элемента массива, его значение увеличивается до тех пор, пока не встретится пробел. В итоге указатель указывает на пробел и мы можем получить с его помощью вторую часть строки.
    Иногда в программах можно видеть такое объявление и определение переменной-указателя:
    char *strP = "Hello World!";
    Строку, которая была присвоена не массиву, а указателю, также можно получить, обратившись по указателю:
    puts(strP);
    Но давайте посмотрим, что же все-таки происходит, и чем такая строка, присвоенная указателю, отличается от строки, присвоенной массиву.
    Когда в программе определяются данные и объявляются переменные, то под них отводится память. При этом данные, которые не были присвоены переменным, поменять в процессе выполнения программы уже нельзя.
    Что происходит в примере? В программе вводится строковый объект, который по сути является строковой константой (литералом). Ссылка на первый элемент этой строки присваивается указателю. Мы можем менять значение указателя сколько угодно, переходить к любому из элементов константного массива символов или даже начать ссылаться на совершенно другую строку. Но вот поменять значение элементов строки не можем. Это можно доказать таким кодом:
    char *strP;
    strP = "This is a literal"; // работает, но строку нельзя изменить puts(strP);
    53
    printf("%c\n",strP[3]);
    strP[3] = 'z'; // не получится
    В последней строке кода возникнет ошибка, т.к. совершается попытка изменить строку- константу.
    Тем более нельзя делать так:
    char *strP;
    scanf("%s",strP); // ошибка сегментирования
    В данном случае память не была выделена под массив символов, который мы пытаемся считать функцией scanf()
    ; память была выделена только под указатель. Поэтому записать строку просто некуда. Другое дело, если память была выделена с помощью объявления массива, после чего указателю был присвоен адрес на этот массив:
    char str[12];
    char *strP; strP = str; gets(strP); // память резервируется под массив ранее puts(strP);
    Поэтому если вам требуется в программе неизменяемый массив символов, то можете определить его через указатель.
    1   2   3   4   5   6   7   8   9   10


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