Урок 1

В котором создаётся простейшая игра, а читатель убеждается, что понимает, что такое ветвление - и что хочет большего.

Кроме вывода информации на стандартный поток вывода, операционная система позволяет и производить чтение из стандартного потока ввода. Игра в любом случае предполагает некую интерактивность, какое-либо взаимодействие с игроком, и мы это взаимодействие осуществим через клавиатуру. Игрок будет набирать цифры на клавиатуре, клавиатура - отправлять эти символы в стандартный поток ввода, а ОС (по просьбе нашей программы) будет отдавать эти цифры нам - вот так и построим взаимодействие.

Чтобы считать символ из стандартного потока ввода - здесь мы можем использовать функцию scanf(). В общем случае использование scanf() приносит больше вреда, чем пользы - но в таких маленьких примерах, как наш, оправдывает себя.

Итак, библиотечная функция scanf() была создана для облегчения разбора введённых в программу данных на составляющие. По сути, она является парной функции printf() - одна для ввода данных, другая для вывода. Но, будучи разработанной в стародавние времена, когда компьютеры были большими, а под термином "пользователь компьютера" подразумевался исключительно программист, функция scanf() очень требовательна к правильности введённых данных. Если данные введены не в том формате (буковка f в названиях printf и scanf как раз и означает - форматированный) данные в программе в лучшем случае испортятся, а в худшем - злоумышленник сможет очень легко использовать нашу программу для взлома, в том числе и не только нашей программы. Мораль всего вышенаписанного - scanf() очень удобно применять для маленьких учебных программ, которые мы пишем для себя, но для любых других целей использовать её в современном мире нужно абсолютно точно знать все последсвия такого использования, а многие программисты и вовсе исповедуют категорический отказ от её использования.
Значат, scanf():

#include <stdio.h>

int main()
{
    char name[15];
    printf("Hello, what is your name?\n");
    scanf("%s", (char *)&name);
    printf("Hello, %s!\n", name);
    return 0;
}

Компилируем программу, запускаем, вводим своё имя, наслаждаемся результатом. Запускаем ещё раз, вводим имя больше 15 символов, ещё раз наслаждаемся результатом.

Итак, мы научились читать введённые данные. Используем знания для создания первой игры - угадайки: Компьютер будет загадывать число, а мы будем пытаться его отгадать.

#include <stdio.h>


int main()
{
	int x = 7;
	int a;
	printf("I guessed a number. What is it?\n");
	scanf("%d", &a);
	if (a == x)
	{
		printf("Right, it's 7!");
	}
	else
	{
		printf("Wrong, it was 7.");
	}
	return 0;
}
Здорово, игра работает! Но не интересная, сложная и реиграбельность низкая. Какие аспекты можно доработать?

Во-первых, сложно, так как у нас всего одна попытка. Если мы с первого раза не угадали, игра завершается, не давая нам второго шанса - а так же и подсказки, насколько близки мы к верному ответу. Для решения данной проблемы мы используем циклы - будем подсказывать, меньше верный ответ или больше, и предоставлять следующую попытку. Во-вторых - задуманное число постоянно одно и то же, захардкоженное при написании программы. Для решения этой проблемы будем использовать псевдослучайную функцию.

Почему не случайную? Процессор является машиной по обработке цифр, он производит вычисления над введёнными в него данными. И вычисления эти над одинаковыми данными всегда приводят к одному и тому же ответу. Иначе, если бы операция "сложить 2 и 2" давала бы разные результаты, не обязательно 4 - это значило бы просто, что процессор сломался. А для получения в компьютере настоящего случайного (в математическом смысле) числа - нужно использовать специальное устройство, Генератор Случайных Чисел, устройство это сложное и дорогое настолько, что встраивать его в процессор или вставлять в обычный бытовой компьютер - просто экономически не имеет смысла. Ведь для нас, для нашей игры пока что абсолютно не важно, число которое задумает наша программа математически "случайное", или всего лишь "псевдослучайное".

Как написать псевдослучайную функцию? Функция должна возвращать число, это понятно. Но какое число, откуда его брать, какими свойствами число должно обладать? Начнём с последнего вопроса: свойство псевдослучайного числа - должно быть трудно догадаться до вызова функции, какое число она вернёт в результате работы. То есть, написать в функции return 7; - плохо, легко предсказать результат работы такой функции. Написать return 7 / 3 * 5 + 2; ничем не лучше, после первого же вызова функции мы будем знать, что она отныне будет возвращать. Совсем другое дело вот такая вещь: return (количество миллисекунд, прошедших с первого января 1970 года)/(уровень шума с микрофона компьютера в настоящий момент) * (количество нажатых пользователем клавиш за последние 10 минут) + (количество запущенных в настоящий момент программ). Вот так уже намного лучше, хотя с такой реализацией тоже есть множество проблем:
-все вызовы функции, осуществлённые в одну милисекунду - скорее всего вернут один и тот же результат (хоть и зависит от звука на микрофоне, но микрофон возвращает значение звука не мгновенно, а скорее всего накапливает и обновляет "время от времени")
-Каждая из переменных не может быть получена без ведома операционной системы - а это значит несколько системных вызовов на каждый вызов функции! То есть, отряд из сотни врагов в игре заставит программу в основном простаивать, пока ОСь выполняет системные вызовы по их просьбе - если каждый враг в отряде будет обращаться к функции хотя бы один раз. С другой стороны, давать "индивидуальное сознание" отряду врагов обычно не стоит, и стоит подумать, как бы управлять членами отряда не по отдельности, а всего одной программой - программой управления отрядом.
-Понаблюдав некоторое время за работой программы, можно выявить некоторые закономерности, как то: с каждым запуском программы среднее значение случайной фукнкции растёт, а программа запущенная сразу после старта компьютера имеет случайные значения намного меньшие, чем программа, запущенная через несколько часов плодотворной работы за компом. Конечно, это не большая проблема, до поры до времени правда - когда-нибудь это может позволить некоторым нехорошим (но очень любопытным) личностям жульничать в этой игре. Если же сделать игру "на деньги", рулетку, например - жульничать и использовать уязвимость нашей псевдослучайной функции станут абсолютно точно, за наш счёт между прочим.

Решение такое: создаётся функция, которая принимает на вход значение, преобразует это значение так, что результат выглядит как случайный, но - в отличие от действительно случайной функции этот результат зависит от входного значения. То есть если случайной функции не нужно подавать значение на вход, то псевдослучайной нужно, и нужно чтобы эти значения каждый раз было разным - иначе результат будет каждый раз одинаковый.
И это значит, что при первом вызове функции мы дадим ей на вход какое-то число, которое будет гарантированно отличаться при запуске программы, а потом просто - при каждом последующем вызове - будем скармливать ей результаты предыдущего вызова. Функцию (даже две, первую для инициализации, вторую для использования) возьмём у Кернигана & Ричи:

unsigned long int next = 1;

int rand(void)
{
    next = next * 1103515243 + 12345;
    return (unsigned int)(next / 65536) % 32768;
}

void srand(unsigned int seed)
{
    next = seed;
}

Теперь мы можем использовать случайную функцию, чтобы задумывать число, но где взять инициализируещее значение? Спросим его у пользователя! Пусть не жульничает, и не вводит одно и то же число. Вообще-то на роль инициализирующего значения почти идеально подходит время системных часов (хотя, как обычно, и тут есть нюансы), но разбираться ещё и со временем сейчас неактуально, так что пользовательский ввод в программе:

int main()
{
    int rnd_value;
    int rnd_initializer;
    int user_answer;
    printf("Enter random number to start. Don't repeat yourself!");
    printf("\n");
    scanf("%d", &rnd_initializer);
    srand(rnd_initializer);
    rnd_value = rand();
    printf("I guessed too. What is it?\n");
    scanf("%d", &user_answer);
    if (user_answer != rnd_value)
    {
        printf("Wrong, it was %d.\n", rnd_value);
    }
    else
    {
        printf("Right, it's %d!\n", rnd_value);
    }
    return 0;
}
Уже лучше! Даже мы, как программист, написавший программу, не знаем, что она задумала - если не жульничать с инициализирующим значением функции srand(), конечно.

Но игра всё ещё довольно бессмысленна, так как даётся всего одна попытка, при том что диапазон в котором программа загадывает число - от нуля до 32767. Снизить диапазон, в котором игра загадывает число, довольно легко - для этого стоит поменять константу 32768 в тексте функции rand(void). Но нужно помнить: когда лезешь в чужую функцию, работу которой не вполне понимаешь - функция может сломаться. В нашем случае - значение, которое функция будет возвращать может стать более предсказуемо. Так как для нас это предположение (истинности которого мы не знаем!) допустимо, мы модифицируем текст функции. Вообще, использование любых цифр непосредственно в тексте функций - плохой стиль. Практически невозможно, встретив где-то в глубине проекта число 12345, понять - к чему оно относится, почему именно 12345, а не 54321, и главное - если нам по какой-то причине понадобится его поменять, практически невозможно быть уверенным, что оно не встречается где-то ещё в тексте программы, и его не нужно изменить где-то ещё. Даже если заменить все числа 12345 в программе, есть вероятность, что мы поменяем те числа, которые относились к чему-то другому, и которые менять не входило в наши планы.

Вышеобозначеные проблемы в С решаются использованием препроцессора. Препроцессор занимается тем, что перед работой компилятора проходит по тексту программы и выполняет найденные в тексте препроцессорные директивы. Мы уже использовали одну директиву препроцессора: #include <stdio.h>. Встречая в тексте (ещё раз, препроцессор работает просто с текстом, он не знает ничего про язык Си!) директиву include препроцессор вместо строки с директивой находит файл "stdio.h" и вставляет текст файла целиком вместо строки с директивой.

Сейчас мы используем другу директиву: #define RAND_RANGE 100. Для препроцессора эта директива значит: "определить переменную RAND_RANGE как текст '100'", или, в более общем случае - определить первое слово после define как строку после первого слова. Далее, после этой директивы, каждый раз когда препроцессор будет встречать слово RAND_RANGE, он будет заменять это слово строкой 100.
Следовательно, изменим программу следующим образом:
В начале файла будет уже две директивы препроцессора, к
#include <stdio.h>
добавляется строка
#define RAND_RANGE 10
Функция rand() же принимает следующий вид:

int rand(void)
{
    next = next * 1103515243 + 12345;
    return (unsigned int)(next / 65536) % RAND_RANGE;
}

Теперь, когда программа стала загадывать число от 0 до 10, угадать число стало намного проще.

Переделайте программу с использованием циклов так, чтобы она предлагала повторить ввод числа, пока ответ не окажется правильным.

Вот как может выглядеть подобная реализация:

#include <stdio.h>
#define RAND_RANGE 10

unsigned long int next = 1;

int rand(void)
{
    next = next * 1103515243 + 12345;
    return (unsigned int)(next / 65536) % RAND_RANGE;
}

void srand(unsigned int seed)
{
    next = seed;
}

int main()
{
    int rnd_value;
    int rnd_initializer;
    int user_answer;
    printf("Enter random number to start. Don't repeat yourself!");
    printf("\n");
    scanf("%d", &rnd_initializer);
    srand(rnd_initializer);
    rnd_value = rand();
    printf("I guessed. It's less than %d. What is it?\n", RAND_RANGE);
    scanf("%d", &user_answer);   
    while (user_answer != rnd_value)
    {
        printf("Wrong, try again: ");
	scanf("%d", &user_answer);   
    }   
    printf("Right, it's %d!\n", rnd_value);
    return 0;
}

Уже почти идеально, осталось добавить пару мелочей: хочется, чтобы при каждой попытке программа подсказывала, как мы ошиблись? Введённое число больше или меньше задуманного?
И, возможно, стоит сделать, чтобы программа задумывала число не от 0 до 9, а от 1 до 10 или от 1 до 100.
Реализуйте эти фичи.
Проверьте, что программа работает так, как задумано!

Самостоятельная работа:
  • Присваивание. Оператор присваивания в Си.
  • Обьявление переменной. Определение переменной. В чём разница? Можно ли объявить переменную, не определяя? А определить, не объявляя?
  • Переменная как адрес в памяти. Операция взятия адреса.
  • Статические переменные. Зачем понадобилось переменную "next" писать вне функции main(), ведь все остальные мы написали внутри? А почему не стоит все переменные делать статическими?
  • Форматирование в функциях форматированного ввода/вывода. Почему программа в текущем состоянии не сможет поздороваться с нами по имени-отчеству?
  • Функции в Си. Что они собой представляют? Что представляют собой аргументы функции, возвращаемое значение? Фактические параметры. Формальные параметры. Когда мы говорим про аргумент, а когда про параметр, и как не запутаться?
  • Объявление функции. Определение функции.
  • Массивы. Указатели в Си. C-строка. Почему массив не является указателем?
  • Приведение типов. Зачем мы написали (char *)? Что изменится, если убрать эти скобки, зачем они стоят - и что в этих скобках плохого, какая проблема есть в нашей программе, с которой мы уже успели столкнуться.
Углублённые задания для самостоятельной работы:
  • Прочитайте и проработайте "Язык программирования C" Брайана Кернигана и Денниса Ритчи.
  • Не путайте Кена Томпсона с Брайаном Керниганом.