Гарвардский университет предлагает курс компьютерных наук, который также бесплатно доступен онлайн, под названием CS50: Introduction to Computer Science. Лекции может посмотреть любой желающий, и я решил не только смотреть лекции, но и писать и делиться конспектами лекций.

Это мои конспекты к лекции 2, посвященной аргументам командной строки, компиляции, отладке, массивам, строкам, состояниям выхода и криптографии.

Первая часть этой серии — конспект лекций 0:



Аргументы командной строки и шаги компиляции

С помощью команды make вы можете скомпилировать файл. Это преобразует исходный код в машинный код, как мы узнали в лекции 1. Но команда make не является компилятором, она автоматизирует использование компилятора clang, что означает язык C.

Если вы хотите скомпилировать его вручную, вы можете использовать команду: clang hello.c, это создает файл a.out, который обозначает вывод ассемблера.

Аргумент командной строки — это дополнительная информация, передаваемая команде для изменения или добавления параметра программы. В качестве примера мы можем добавить аргумент командной строки -o hello к команде clang hello.c. Таким образом, выходной файл будет называться hello вместо a.out.

Мы также можем добавить аргумент командной строки -lcs50 к команде clang. Это необходимо для связывания машинного кода библиотеки cs50 с машинным кодом файла, который вы компилируете. Этого не нужно делать для библиотеки stdio, потому что это стандартная библиотека, которая поставляется с C по умолчанию, в отличие от библиотеки cs50.

Шаги компиляции, автоматизируемые командой make, следующие:

  • предварительная обработка — это когда объявления функций внутри заголовочного файла, который вы включаете строкой вроде #include <stdio.h>, копируются в вашу программу на C, чтобы функции можно было использовать в вашем коде.
  • компиляция — это преобразование предварительно обработанного исходного кода в код на ассемблере. Ассемблерный код — это язык программирования, который на шаг ближе к машинному коду, он содержит низкоуровневые операции, понятные компьютерам.
  • ассемблирование — это преобразование ассемблерного кода в машинный код, состоящий из нулей и единиц.
  • связывание — это когда машинный код вашей программы объединяется с машинным кодом используемых библиотек.

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

Отладка

Для устранения ошибок, которые были допущены при написании кода, есть некоторые инструменты и приемы:

  • Временные операторы печати. Выводя текст в окно терминала, вы можете получить представление о том, что происходит и что происходит не так, когда код выполняется. Таким образом, вы можете видеть (промежуточные) значения переменных во время выполнения и смотреть на порядок, в котором печатается текст, и, следовательно, на порядок, в котором выполняются строки.
  • Отладчик. Отладчик — это программное обеспечение, помогающее отлаживать код. В VS Code есть встроенный отладчик, но он зависит от шагов настройки.
    CS50 автоматизировал процесс запуска отладчика с помощью команды debug50, доступной в онлайн-среде VS Code, так что вы можете пропустить настройку.
    Если вы наведете курсор слева от номеров строк, вы увидите красную точку, щелкнув ее, вы добавите точку останова в строку. Когда код выполняется внутри отладчика, он приостанавливается в точке останова, чтобы вы могли пошагово исследовать выполнение кода.
    После добавления точек останова вы можете написать команду с синтаксисом: debug50 ./filename, имя файла должно принадлежать скомпилированному файлу. Это запускает файл в отладчике. Он останавливается в точке останова и показывает значения всех переменных в это время. Затем вы можете нажать Продолжить, чтобы продолжить до конца программы, Перейти, чтобы перейти к следующей строке, или Перейти, чтобы перейти к строки вызываемой функции.
  • Метод резиновой уточки. Это метод, при котором вы пытаетесь объяснить ошибку, с которой имеете дело (резиновой утке). В надежде, что, объясняя, вы скажете что-то, что заставит вас задуматься: «Погодите, я понимаю, что идет не так».

Массивы

Типы данных, которые мы уже видели, такие как int, bool и char, имеют определенное конечное количество байтов, используемых для значения. Количество байтов, необходимых для строк, зависит от длины текста, который вы в них помещаете.

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

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

int score1 = 72;
int score2 = 73;
int score3 = 33;

Вы можете инициализировать массив следующим образом:

int scores[3];

так что scores будет именем переменной-массива, она будет иметь размер 3, что означает, что она может хранить 3 элемента, и каждый элемент будет целым числом.

Затем вы можете указать значения для хранения в массиве, используя индексы значений. Индекс – это число, начинающееся с 0 и указывающее место в массиве. Например:

int scores[3];
scores[0] = 72;
scores[1] = 73;
scores[2] = 33;

Таким образом, вы используете только одну переменную для хранения нескольких значений. И это также позволяет вам сохранять значения в переменных в цикле. Как в следующем примере, где мы циклически переходим от i=0 к i=2, чтобы получить числа 0, 1 и 2 и использовать их в качестве индексов для размещения 3 чисел в массиве.

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

#include <cs50.h>
#include <stdio.h>

int main(void)
{
    int scores[3];
    for (int i = 0; i < 3; i++)
    {
        scores[i] = get_int("Score: ");
    }
    printf("Average: %f\n", (scores[0] + scores[1] + scores[2]) / (float) 3);
}

Обратите внимание, что здесь мы используем приведение типов, чтобы сделать 3 числом с плавающей запятой. Когда хотя бы одно из чисел в (scores[0] + scores[1] + scores[2]) / (float) 3) является числом с плавающей запятой, результат также может стать числом с плавающей запятой. Мы также могли бы использовать 3.0 вместо (float) 3.

Элементы массива хранятся в памяти в байтах, которые являются непрерывными, то есть они находятся рядом друг с другом.

Чтобы передать массив целых чисел в качестве аргумента функции в C, вы можете использовать синтаксис: int array[].

В следующем коде мы создаем функцию с именем Average, которая возвращает массив с именем array и принимает список целых чисел в качестве аргумента. [] указывает, что это массив, а array указывает имя, которое будет использоваться внутри функции.

float average(int array[])
{
    int sum = 0;
    for (int i = 0; i < N; i++)
    {
        sum += array[i];
    }
    return sum / (float) N;
}

В этом коде N — это глобальная переменная, которая инициализируется в начале программы. Затем эту переменную можно использовать во всем коде, а также внутри функций. Глобальная переменная используется для объявления размера массива оценок в одном месте и только в одном месте.

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

Весь код будет таким:

#include <cs50.h>
#include <stdio.h>

const int N = 3;

float average(int length, int array[]);

int main(void)
{
    int scores[N];
    for (int i = 0; i < N; i++)
    {
        scores[i] = get_int("Score: ");
    }
    printf("Average: %f\n", average(N, scores));
}

float average(int length, int array[])
{
    int sum = 0;
    for (int i = 0; i < length; i++)
    {
        sum += array[i];
    }
    return sum / (float) length;
}

Струны

Вместо того, чтобы хранить текст в отдельных переменных типа char, например:

char c1 = 'H';
char c2 = 'I';
char c3 = '!';

мы можем использовать строку, которая объединяет эти значения в одну переменную (для использования string включите cs50.h):

#include <cs50.h>

int main(void)
{
    string s = "HI!";
}

Строка — это просто набор символов. Большинство типов данных имеют определенный размер в байтах, а массивы инициализируются с определенным размером. Но размер строки определяется тем, где находится ее конец. А конец определяется запоминанием числа 0.

Итак, когда вы создаете строку "HI!", в массиве сохраняются следующие значения: 72 73 33 0. Это значения, которые соответствуют символам ASCII: H I ! \0. Обратная косая черта в \0 указывает, что вы сохраняете не символ 0, а символ с именем NUL, который имеет особое значение, означающее, что строка заканчивается на этом.

Поскольку строки на самом деле являются массивами символов, мы можем использовать индексы внутри квадратных скобок, чтобы получить отдельные символы:

#include <cs50.h>
#include <stdio.h>

int main(void)
{
    string s = "HI!";
    printf("%c %c %c %c\n", s[0], s[1], s[2], s[3]);
    printf("%i %i %i %i\n", s[0], s[1], s[2], s[3]);

}

Это печатает:

H I ! 
72 73 33 0

С помощью %c мы напечатали значения, хранящиеся в строке/массиве s, как символы. И с %i мы напечатали их как целые числа, числовые значения, которые формируют байты.

Мы также можем создать массив строк с именем words размера 2:

string words[2];

и заполните его строками:

words[0] = "HI!";
words[1] = "BYE!";

Теперь доступ к символам можно получить, указав сначала индекс слова в массиве words, а затем индекс символа в этом слове:

printf("%c%c%c\n", words[0][0], words[0][1], words[0][2]);
printf("%c%c%c%c\n", words[1][0], words[1][1], words[1][2], words[1][3]);

Считая символы до тех пор, пока вы не достигнете символа NUL \0, вы можете определить длину строки:

#include <cs50.h>
#include <stdio.h>

int main(void)
{
    string name = get_string("What's your name? ");

    int n = 0;
    while (name[n] != '\0')
    {
        n++;
    }
    printf("%i\n", n);
}

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

Используя это, вам не нужно писать функцию самостоятельно:

#include <cs50.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
    string name = get_string("What's your name? ");
    int n = strlen(name);
    printf("%i\n", n);
}

Другим способом работы со строками может быть преобразование символов в строке из нижнего регистра в верхний.

Это можно сделать, используя тот факт, что в ASCII заглавные буквы отображаются в числа от 65 до 90, а строчные буквы — от 97 до 122. Это означает, что если вы вычтете 32 из значения строчной буквы, вы получите значение прописной буквы. Например, 97 (а) - 32 = 65 (А), 98 (б) - 32 = 66 (В) и 122 (z) - 32 = 90 (Z).

Эти знания используются в следующей программе:

#include <cs50.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
    string s = get_string("Before: ");
    printf("After:  ");
    for (int i = 0; i < strlen(s); i++)
    if (s[i] >= 'a' && s[i] <= 'z')
    {
        printf("%c", s[i] - 32);
    }
    else
    {
        printf("%c", s[i]);
    }
    printf("\n");
}

Однако есть библиотека, в которой есть функция, позволяющая делать то же самое. Библиотека называется ctype, а соответствующий заголовочный файл — ctype.h. Это позволяет вам использовать функцию toupper, которая проверяет, является ли символ строчным, а затем преобразует его в верхний регистр, в противном случае оставляет его без изменений.

Вот пример использования функции toupper:

#include <cs50.h>
#include <ctype.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
    string s = get_string("Before: ");
    printf("After:  ");
    for (int i = 0; i < strlen(s); i++)
    {
        printf("%c", toupper(s[i]));
    }
    printf("\n");
}

В качестве шага, чтобы сделать этот код более эффективным, мы можем изменить место strlen(s). Как и сейчас, код выполняется на каждой итерации, но поскольку s остается прежним, длина также останется прежней. Следующий код вызывает strlen(s) только один раз, когда n инициализируется:

#include <cs50.h>
#include <ctype.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
    string s = get_string("Before: ");
    printf("After:  ");
    for (int i = 0, n = strlen(s); i < n; i++)
    {
        printf("%c", toupper(s[i]));
    }
    printf("\n");
}

Аргументы командной строки в C

Команда для запуска скомпилированной программы C — ./filename. Мы также можем добавить к этой команде аргументы командной строки.
Например: ./greet David.

Доступ к аргументу David и любому другому аргументу можно получить в C, позволив функции main принимать аргументы.

Это делается путем замены void на int argc, string argv[]. argc — это количество аргументов, а argv — это список строк, где каждая строка является аргументом.

В этом коде:

#include <cs50.h>
#include <stdio.h>

int main(int argc, string argv[])
{
    printf("hello, %s\n", argv[1]);
}

мы берем аргумент с индексом 1 и печатаем его. Если мы запустим команду: ./greet David, она напечатает: hello, David.

Аргумент с индексом 0 — это само имя файла.

Если мы запустим ./greet David Malan, текст Malan будет доступен через argv[2].

Выходные статусы в C

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

Статус выхода программы C можно узнать, выполнив команду: echo $? после запуска программы.

По умолчанию основная функция возвращает 0, но добавив операторы return в основную функцию, вы можете заставить ее возвращать другие значения. «Отлавливая» ошибки до их возникновения, вы можете быть уверены, что программа не выйдет из строя и вернет статус выхода, указывающий на наличие проблемы. Используя разные статусы в разных ситуациях, вы можете легко намекнуть, в чем заключается проблема.

Вот пример кода, в котором возвращается статус выхода 1:

#include <cs50.h>
#include <stdio.h>

int main(int argc, string argv[])
{
    if (argc != 2)
    {
        printf("Missing command-line argument\n");
        return 1;
    }
    else
    {
        printf("hello, %s\n", argv[1]);
    }
}

Если мы запустим команду ./greet David, она напечатает hello, David, а когда мы затем запустим echo $?, мы увидим статус выхода по умолчанию 0.

Когда мы запускаем ./greet, он печатает Missing command-line argument, а когда мы затем запускаем echo $?, мы видим указанный статус выхода 1.

Криптография

Криптография – это наука о шифровании информации. Шифрование — это процесс преобразования открытого текста в зашифрованный текст с помощью алгоритма. Алгоритмы шифрования информации называются шифрами. Они берут открытый текст и ключ и возвращают зашифрованный текст.

Примером шифра является алгоритм, в котором вы сдвигаете буквы в алфавитном порядке так, что A становится B, B становится C и так далее. В этом случае ключ будет равен 1, так как вы сдвинули каждую букву на 1. Секретную информацию можно обнаружить только тогда, когда вы знаете и алгоритм, и ключ.

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

Если вы получили зашифрованный текст: UIJT XBT DT50, вы можете расшифровать его, сдвинув его на 1 букву в другом направлении. U становится T, I становится H и так далее. Затем зашифрованный текст преобразуется в открытый текст: THIS WAS CS50.

Спасибо за чтение!

Надеюсь мой пост был полезен.
Если вам понравилось, подписывайтесь на мою страницу, чтобы узнать другие конспекты лекций по CS50 и узнать больше о программировании, это мне очень поможет:



Следующая лекция посвящена алгоритмам: