Вопросы и ответы с собеседований по C
Вопросы и ответы с собеседований C-разработчиков. Указатели, функция main(), особенности работы с gets(), strcpy(), free(), разница между sprintf() и printf(), утечки памяти, статические сущности и многое другое.
Какого типа значение возвращает main()?
Скомпилируется ли следующий код? Если да, то какие проблемы могут возникнуть?
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
позволяет программам возвращать код статуса, что очень важно, когда программа выполняется как часть скрипта, и внутри скрипта есть условия, которые зависят от результата выполнения программы.
Что такое указатель?
Указатель — это переменная, которая хранит адрес памяти объекта. Указатели широко используются в C и C++, например, в подобных случаях: 1. Для выделения новых объектов в куче, 2. Для передачи функций другим функциям 3. Для итерации элементов в массивах или других структурах данных.
Различия sprintf() и printf()?
Метод sprint()
работает аналогично методу printf()
за исключением одной небольшой детали. Метод printf()
записывает вывод на экран консоли, тогда как метод sprintf()
записывает вывод в массив символов.
Расскажите про порядок аргументов printf()
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
отображаются слева направо, вычисляются они справа налево. Однако, порядок вычисления аргументов стандартом не определен. Аргументы кладутся в стек справа налево, но порядок их вычисления может быть иным.
В Java, например, такой проблемы нет.
Расскажите про функцию gets()
В приведенной программе есть проблема. Можете её найти?
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()
.
Дополнение: gets()
является deprecated
.
Расскажите про функцию strcpy()
Приведенный код реализует простейшую защиту по паролю. Можно ли вы взломать эту защиту, не зная пароля?
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()
.
Расскажите про функцию free()
Следующая программа вылетает с ошибкой сегментации, если ввести freeze
. Однако, если ввести zebra
, то все будет хорошо. Почему?
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()
и ошибке сегментации.
Расскажите про функции atexit() и _exit()
void func(void)
{
printf("\n Cleanup function called \n");
return;
}
int main(void)
{
int i = 0;
atexit(func);
for(; i<0xffffff; i++);
_exit(0);
}
Вопрос: В приведённом коде функция
func()
не вызывается. Почему?
Ответ: Из-за функции _exit()
. Эта функция не вызывает функции очистки вроде atexit()
. Если нужно, чтобы функции atexit()
все-таки вызывались, нужно воспользоваться exit()
или ключевым словом return
.
Что не так со следующей функцией?
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
Эта функция потенциально опасна. Назначение кода состоит в возвращении квадрата значения, указанного при помощи ptr
. Однако, поскольку *ptr
указывает на volatile
переменную, компилятор сгенерирует код, который выглядит примерно так:
int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}
Поскольку значение переменной, на которую указывает *ptr
, может неожиданно измениться, то существует возможность, что а
и b
будут разными. Следовательно, этот код может возвратить число, которое не будет квадратом! Правильный вариант кода в данном случае такой:
int square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}
Течёт ли тут память?
void main(void)
{
char *ptr = (char*)malloc(10);
if(NULL == ptr)
{
printf("\n Malloc failed \n");
return;
}
else
{
// Do some processing
}
return;
}
Да, этот код не освобождает память после ее выделения, но это не приведет к утечке памяти, ведь программа завершается почти сразу же. Операционная система автоматически очищает всю задействованную программой память при ее завершении.
Однако, если вышеприведенный код будет находиться в цикле вроде
while
, это приведет к серьезным утечкам памяти.
void* и структуры C
Вопрос: Напишите функцию, которая принимает аргумент любого типа, а возвращает int
. И еще, можно ли будет передать ей более чем один аргумент?
Ответ: Функция, которая принимает аргумент любого типа, выглядит так:
int func(void *ptr)
Если нужно передать более одного аргумента, тогда этой функции нужно передавать в качестве аргумента указатель на структуру, а в полях структуры - аргументы, которые нужно передать.
Операторы * и ++
Вопрос: Что напечатает данная программа и почему?
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'
.
Реализуйте процесс, изменяющий свое имя
Напишите программу, которая изменяет свое имя во время выполнения.
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;
}
или
int main(int argc, char *argv[]) {
argv[0][0] = 65;
sleep(10);
}
Адрес локальной переменной
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
, а ее адрес. Таким образом, мы будем вносить изменения только по этому адресу, и не будем задействовать локальные адреса переменных.
В каких случаях используется ключевое слово static?
Спецификатор static
в языке Си используется в трёх случаях:
1. Переменная, описанная внутри тела функции как статическая, сохраняет свое значение между вызовами функции.
2. Переменная, описанная как статическая внутри файла, но снаружи тела функции, доступна для всех функций в пределах этого файла и не доступна функциям любых других файлов. То есть это локализованная глобальная переменная.
3. Функции, описанные внутри файла как статические, могут быть вызваны только другими функциями из этого файла. То есть область видимости функции локализована файлом, внутри которого она описана.
Зачем используется volatile?
Ключевое слово volatile
информирует компилятор о том, что переменная может быть изменена не только из текущего выполняемого кода, но и из других мест. Тогда компилятор будет избегать определенных оптимизаций этой переменной.
Примеры volatile
переменных:
1. Регистры в периферийных устройствах (например, регистры состояния)
2. Глобальные переменные, используемые в обработчиках прерываний.
3. Глобальные переменные, используемые совместно несколькими задачами в многопотоковом приложении.
Может ли указатель быть volatile?
Да. Хотя это не общепринятый случай. Например, когда обработчик прерываний изменяет указатель на буфер.