Собеседование на знание языка C – 12 вопросов и ответов

В этой статье мы рассмотрим интересные задачи на языке C, которые могут помочь студентам (и не только) освежить в памяти свои знания этого языка и подготовиться к заковыристым вопросам на собеседовании.

1. Функция gets()

Вопрос: В приведенной программе есть проблема. Можете её найти?

#include <stdio.h>

int main(void)
{
    char buff[10];
    memset(buff,0,sizeof(buff));

    gets(buff);

    printf("\n The buffer entered is [%s]\n",buff);

    return 0;
}

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

2. Функция strcpy()

Вопрос: Приведенный код реализует простейшую защиту по паролю. Можно ли вы взломать эту защиту, не зная пароля?

#include <stdio.h>

int main(int argc, char *argv[])
{
    int flag = 0;
    char passwd[10];

    memset(passwd,0,sizeof(passwd));

    strcpy(passwd, argv[1]);

    if(0 == strcmp("LinuxGeek", passwd))
    {
        flag = 1;
    }

    if(flag)
    {
        printf("\n Password cracked \n");
    }
    else
    {
        printf("\n Incorrect passwd \n");

    }
    return 0;
}

Ответ: Да. Логику кода аутентификации, приведенного выше, можно обойти при помощи уязвимости в функции strcpy(). Эта функция копирует пароль, предоставленный пользователем, в буфер ‘passwd’, не проверяя, достаточно ли в этом буфере места. Предположим, что пользователь введет случайный пароль, имеющий длину достаточную для того, чтобы заполнить как буфер ‘passwd’, так и перезаписать область памяти, содержащую изначальное значение ‘0’ переменной ‘flag’. В этом случае, даже если сравнение введенной строки и пароля не пройдет, то все равно проверка флага, который изначально имел нулевое значение, станет ненулевым, и таким образом, защита будет “взломана”.

К примеру:

$ ./psswd aaaaaaaaaaaaa

 Password cracked

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

Для защиты от подобных случаев следует пользоваться функцией strncpy().

Замечание от автора: В наши дни компиляторы стали довольно умными – они автоматически отлавливают подобные случаи и располагают переменные на стеке таким образом, чтобы значительно усложнить взлом стека. В моем случае, компилятор gcc включил эту опцию по умолчанию, и поэтому мне пришлось воспользоваться обратной опцией ‘-fno-stack-protector’, чтобы воспроизвести традиционное поведение компилятора.

3. Тип возвращаемого значения функции main()

Вопрос: Скомпилируется ли следующий код? Если да, то какие проблемы могут возникнуть?

#include <stdio.h>

void main(void)
{
    char *ptr = (char*)malloc(10);

    if(NULL == ptr)
    {
        printf("\n Malloc failed \n");
        return;
    }
    else
    {
        // Do some processing

        free(ptr);
    }

    return;
}

Ответ: Этот код скомпилируется без ошибок, но с варнингом (на большинстве компиляторов) о том, что значение, возвращаемое функцией main(), должно иметь тип ‘int’, а не ‘void’. Тип ‘int’ позволяет программам возвращать код статуса, что очень важно, когда программа выполняется как часть скрипта, и внутри скрипта есть условия, которые зависят от результата выполнения программы.

4. Утечка памяти

Вопрос: Приведет ли следующий код к утечкам памяти?

#include <stdio.h>

void main(void)
{
    char *ptr = (char*)malloc(10);

    if(NULL == ptr)
    {
        printf("\n Malloc failed \n");
        return;
    }
    else
    {
        // Do some processing
    }

    return;
}

Ответ: Конечно, этот код не освобождает память после ее выделения, но это не приведет к утечке памяти, ведь программа завершается почти сразу же. Операционная система автоматически очищает всю задействованную программой память, при ее завершении. Однако, если вышеприведенный код будет находиться в цикле вроде while, это приведет к серьезным утечкам памяти.

Замечание: Если хотите узнать больше об утечках памяти и о том, как их отлавливать, прочтите нашу статью о Valgrind.

5. Функция free()

Вопрос: Следующая программа вылетает с ошибкой сегментации, если ввести ‘freeze’. Однако если ввести ‘zebra’, то все будет хорошо. Почему?

#include <stdio.h>

int main(int argc, char *argv[])
{
    char *ptr = (char*)malloc(10);

    if(NULL == ptr)
    {
        printf("\n Malloc failed \n");
        return -1;
    }
    else if(argc == 1)
    {
        printf("\n Usage  \n");
    }
    else
    {
        memset(ptr, 0, 10);

        strncpy(ptr, argv[1], 9);

        while(*ptr != 'z')
        {
            if(*ptr == '\0')
                break;
            else
                ptr++;
        }

        if(*ptr == 'z')
        {
            printf("\n String contains 'z'\n");
            // Do some more processing
        }

       free(ptr);
    }

    return 0;
}

Ответ: Здесь проблема заключается в том, что код изменяет адрес указателя ‘ptr’ (путем инкремента переменной ‘ptr’) внутри цикла while. Когда пользователь вводит ‘zebra’, цикл while завершается без единой итерации, поэтому адрес, переданный функции free(), будет точно такой же, какой был присвоен функцией malloc(). Однако в случае с ‘freeze’, значение переменной ptr изменяется внутри цикла while, что приводит к передаче неправильного адреса в функцию free() и ошибке сегментации.

6. Функции atexit и _exit

Вопрос: В следующем коде функция func() не вызывается. Почему?

#include <stdio.h>

void func(void)
{
    printf("\n Cleanup function called \n");
    return;
}

int main(void)
{
    int i = 0;

    atexit(func);

    for(;i<0xffffff;i++);

    _exit(0);
}

Ответ: Из-за функции _exit(). Эта функция не вызывает функции очистки вроде atexit(). Если нужно, чтобы функции atexit() все-таки вызывались, нужно воспользоваться exit() или ключевым словом return.

7. void* и структуры C

Вопрос: Напишите функцию, которая принимает аргумент любого типа, а возвращает int. И еще, можно ли будет передать ей более чем один аргумент?

Ответ: Функция, которая принимает аргумент любого типа, выглядит так:

 int func(void *ptr)

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

8. Операторы * и ++

Вопрос: Что напечатает данная программа и почему?

#include <stdio.h>

int main(void)
{
    char *ptr = "Linux";
    printf("\n [%c] \n",*ptr++);
    printf("\n [%c] \n",*ptr);

    return 0;
}

Ответ: Программа выведет следующее:

[L] 

[i]

Так как приоритет операторов '++' и '*' одинаков, то обработка '*ptr++' будет осуществляться справа налево. Следуя этой логике, сначала вычисляется ptr++, а затем *ptr. Таким образом, получаем 'L'. Так как к ptr был применен постфиксный оператор '++', второй printf() напечатает 'i'.

9. Редактирование в сегменте кода

Вопрос: Следующий код завершается с ошибкой сегментации. Объясните, почему?

#include <stdio.h>

int main(void)
{
    char *ptr = "Linux";
    *ptr = 'T';

    printf("\n [%s] \n", ptr);

    return 0;
}

Ответ: Потому что операция *ptr = 'T' пытается изменить первый байт строки 'Linux', которая хранится в памяти в сегменте кода (а этот сегмент только для чтения). Эта операция некорректна, и приводит к падению программы с ошибкой сегментации.

10. Процесс, изменяющий свое имя

Вопрос: Напишите программу, которая изменяет свое имя во время выполнения.

Ответ: Например, такая:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int i = 0;
    char buff[100];

    memset(buff,0,sizeof(buff));

    strncpy(buff, argv[0], sizeof(buff));
    memset(argv[0],0,strlen(buff));

    strncpy(argv[0], "NewName", 7);

    // Simulate a wait. Check the process
    // name at this point.
    for(;i<0xffffffff;i++);

    return 0;
}

11. Вернуть адрес локальной переменной

Вопрос: Есть ли проблемы с данным кодом? Если да, то как от них можно избавиться?

#include <stdio.h>

int* inc(int val)
{
  int a = val;
  a++;
  return &a;
}

int main(void)
{
    int a = 10;

    int *val = inc(a);

    printf("\n Incremented value is equal to [%d] \n", *val);

    return 0;
}

Ответ: Хотя эта программа может отрабатывать нормально, однако у нее есть серьезная ошибка в функции inc(). Эта функция возвращает адрес локальной переменной. Время жизни этой локальной переменной - это время, пока выполняется функция inc(). После того, как функция inc() отработала, использование адреса локальной переменной может приводить к непредсказуемым результатам. Эту программу можно исправить, если передавать в функцию inc() не значение переменной 'a', а ее адрес. Таким образом, мы будем вносить изменения только по этому адресу, и не будем задействовать локальные адреса переменных.

12. Аргументы printf()

Вопрос: Что выведет следующий код?

#include <stdio.h>

int main(void)
{
    int a = 10, b = 20, c = 30;

    printf("\n %d..%d..%d \n", a+b+c, (b = b*2), (c = c*2));

    return 0;
}

Ответ: Программа выведет следующее:

110..40..60

Несмотря на то, что аргументы функции printf отображаются слева направо, но обрабатываются справа налево.

Это перевод статьи 12 Interesting C Interview Questions and Answers by Himanshu Arora.

7 thoughts on “Собеседование на знание языка C – 12 вопросов и ответов”

  1. В последнем вопросе в строке:
    printf(“\n %d..%d..%d \n”, a+b+c, (b = b*2), (c = c*2));
    имеем undefined behavior
    так как реализация функции может быть с обработкой параметров в другом порядке

  2. Восьмой ответ не корректен.
    Приоритет у постфикс оператора ‘++’ выше, чем у ‘*’ (http://en.cppreference.com/w/c/language/operator_precedence). Но это ещё не все новости 🙂
    Порядок выполнения регулируется не приоритетами операций, а другими правилами (http://en.cppreference.com/w/c/language/eval_order).

  3. В одиннадцатом нужно использовать статическую переменную и все проблемы решены.
    А передавать адрес это уже не исправление ошибки, а целый рефакторинг.
    int* inc(int val)
    {
    static int a = val;
    a++;
    return &a;
    }

  4. 2 Михаил
    статическая переменная для таких вещей – полнейший ахтунг:
    int* a = inc(10);
    int* b = inc(100);
    cout << *a << ", " << *b << endl;

    // Упс…

    А да, еще стоит посмотреть чем конструкция
    static int a = val;

    отличается от

    static int a;
    a = val;

  5. по второй задаче. Бред. Функция strcpy().

    как распределиться память в ОЗУ:
    допустим flag по адресу 0х8000’0000, массив passwd по адресу 0x8000’0004, passwd занимает 10 байт, т.е. 0x8000’0004 по 0x8000’000D. если вылезти за пасворд, то поменяешь данные по адресам 0x8000’000E, 0x8000’000F, 0x8000’0010, 0x8000’0011 и т.д. А данные по адресам 0x8000’0000-0x8000’0004 не изменяться

    Возможно, если этот пример запустить с MMU (например под Linux), то так и будет. ОС грубо говоря, выделит под задачу 14 байт ( с адресами 0x0000’0000 по 0x0000’000D). при переполнении стека в strcpy указатель укажет на начало памяти. strcpy по кругу перезатерёт адреса 0x0000’0000 – 0x0000’0003, т.е. переменную flag. И это сработает.

    Но если эту программу запустить на процессоре без MMU (ARM Cortex-M, AVR, MSP430), даже на линуксе (например на uLinux), то ни какого взлома пароля не будет. В худшем случае перетрётся какая-нибудь память и программа или устройство зависнет/перезапустится.

    в задаче нет оговорок на MMU, это задача на знание Си. В Си нет ни какого зацикливания указателей памяти и перепишется память которая ЗА passwd, но не перед.

  6. 11 задача. полный ахтунг. ошибка на лицо. но предлагают её решение не понятно какое? передавать указатель в inc()? И? у вас будет меняться переменная а, объявленная в main(). А это требуется?

    Вообще, какая задача? не понятно. “Вернуть адрес локальной переменной”? А зачем? И как можно вернуть адрес локальной переменной, если она “умирает” при выходе? Постановка задачи не правильная. Вернуть адрес статической локальной переменной – это другое дело. делаем статик. Создать динамически перемеренную и вернуть адрес – это третье. Ошибку видно в коде. Но как исправить? Ни как! Что нужно сделать? Что должна делать функция inc()?

  7. ну если на то пошло, то в 3-м задании будет warning и по поводу того, что вы не подключили библиотеку для функции malloc

Leave a Reply

Your email address will not be published. Required fields are marked *