С точки зрения интервьюируемого вы можете много узнать о том, кто написал этот тест. Составлен ли тест ради того, чтобы показать насколько хорошо автор знает мельчайшие подробности ANSI-стандарта, вместо того чтобы проверить реальные знания? Проверяет ли он абсурдные знания, такие как коды определенных ASCII символов? Имеют ли вопросы тенденцию к выявлению ваших знаний системных вызовов и стратегий распределения памяти, указывая на то, что автор мог заниматься программированием компьютеров, вместо программирования встраиваемых систем? Если какие-либо из перечисленных вещей имеют место, то я бы серьёзно задумался – хочу ли я такую работу.
С точки зрения работодателя, тест может много рассказать о кандидате. В первую очередь, тест может определить уровень знания кандидатом языка Си. Впрочем, также интересно бывает посмотреть, как люди отвечают на вопросы, на которые они не знают ответа. Делают ли они осмысленный выбор, подкреплённый хорошим чувством интуиции, или же просто пытаются угадать? Занимают ли они оборонительную позицию, когда оказываются в затруднительном положении, или проявляют к проблеме живой интерес и видят в ней возможность узнать что-то новое? Я нахожу эту информацию такой же полезной, как и непосредственно саму работу во время теста.
Держа эти идеи в уме, я попытался создать тест, который имеет прямое отношение к требованиям встраиваемых систем. Почти все вопросы навеяны случаями, с которыми я столкнулся за годы работы. Некоторые из них очень трудные, однако, все они весьма поучительные.
Этот тест может быть предложен широкому кругу кандидатов. Большинство претендентов начального уровня едва ли справятся с ним, в то время как закалённые ветераны должны пройти его на отлично. Очки не присуждаются, я стремлюсь произвольно оценивать вопросы. Однако, если вы собираетесь приспособить этот тест для ваших собственных потребностей, то можете спокойно присуждать баллы.
Препроцессор
1. Используя директиву #define, как бы вы описали именованную константу, которая возвращает число секунд в году? Високосными годами следует пренебречь.
#define SECONDS_PER_YEAR (60UL * 60UL * 24UL * 365UL)
Здесь я смотрю на несколько моментов:
(а) Базовое знание синтаксиса #define (т.е. отсутствие точки с запятой в конце, необходимость заключать в круглые скобки и т.д.).
(b) Правильный выбор имени - с применением заглавных букв и подчёркиванием.
(c) Понимание того, что препроцессор будет вычислять для вас константное выражение.
(d) Понимание того, что выражение переполнит integer аргумент на 16-и битной машине - а следовательно потребность в L, указывающей компилятору обращаться с выражением как с Long.
(е) А если вы в добавок написали выражение с UL (обозначающее unsigned long), то вы отлично начали тест, потому что показываете, что знаете об опасности типов со знаками и без, и запомните - первое впечатление считается!
2. Напишите «стандартный» макрос MIN. То есть, макрос, который берет два аргумента и возвращает меньший из них.
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
Цель этого вопроса - проверить следующее:
(а) Умение использовать директиву #define для написания макросов. Это важно, потому что до того, как inline оператор стал частью Си стандарта, макросы были единственным способом генерирования встраиваемого кода. А такой код часто бывает необходим для достижения требуемого уровня производительности.
(b) Знание троичного условного оператора. Он используется в Си, потому что позволяет компилятору производить потенциально более оптимальный код, чем последовательность if-else. Производительность обычно является важной составляющей во встраиваемых системах, поэтому необходимо знать и уметь использовать эту конструкцию.
(с) Понимание необходимости заключать аргументы макросов в скобки.
(d) Я также использую этот вопрос, чтобы начать разговор о побочных эффектах макросов. Например, о том, что происходит, когда вы пишите такой код:
least = MIN(*p++, b);
Цель этого вопроса - проверить следующее:
(а) Умение использовать директиву #define для написания макросов. Это важно, потому что до того, как inline оператор стал частью Си стандарта, макросы были единственным способом генерирования встраиваемого кода. А такой код часто бывает необходим для достижения требуемого уровня производительности.
(b) Знание троичного условного оператора. Он используется в Си, потому что позволяет компилятору производить потенциально более оптимальный код, чем последовательность if-else. Производительность обычно является важной составляющей во встраиваемых системах, поэтому необходимо знать и уметь использовать эту конструкцию.
(с) Понимание необходимости заключать аргументы макросов в скобки.
(d) Я также использую этот вопрос, чтобы начать разговор о побочных эффектах макросов. Например, о том, что происходит, когда вы пишите такой код:
least = MIN(*p++, b);
3. Каково назначение директивы препроцессора #error?
Этот вопрос очень полезен, если требуется отличить нормальных парней от ботаников. Обычно только ботаники читают приложения к руководствам по Си, чтобы узнать о таких вещах.
Бесконечные циклы
4. При программировании встраиваемых систем часто используются бесконечные циклы. Как реализовать бесконечный цикл в Си?
Существует несколько решений этой проблемы. Я предпочитаю такое:
while(1)
{
…
}
Другая общепринятая конструкция выглядит так:
for( ; ; )
{
…
}
Лично я не люблю эту конструкцию, потому что такой синтаксис не объясняет, что происходит. Если кандидат предлагает именно это решение, я пытаюсь выяснить, чем он обосновывает свои действия. Если ответ сводится к тому, что - «Меня научили так делать, и я никогда об этом с тех пор не думал» - это говорит не в пользу кандидата. С другой стороны, если он заявляет, что Керниган и Ритчи предпочитали этот метод, и это единственный способ для бесконечного цикла пройти контроль на соответствие стандартам, то он получает дополнительные очки.
Третье решение заключается в использовании goto:
Loop:
…
goto Loop;
Кандидаты, которые предлагают этот вариант, являются либо программистами на языке ассемблера, либо они оторванные от жизни программисты Бейсика/Фортрана, ищущие новое поле для деятельности.
Объявление данных
5. Используя переменную «a», запишите объявления для:(а) Целого
(b) Указателя на целое
(с) Указателя на указатель на целое
(d) Массива из десяти целых
(е) Массива из десяти указателей на целые
(f) Указателя на массив из десяти целых
(g) Указателя на функцию, которая принимает целочисленный аргумент и возвращает целое
(h) Массива из десяти указателей на функции, которая принимает целочисленный аргумент и возвращает целое
Ответы:
(a) int a; // Целое
(b) int *a; // Указатель на целое
(c) int **a; // Указатель на указатель на целое
(d) int a[10]; // Массив из десяти целых
(e) int *a[10]; // Массив из десяти указателей на целые
(f) int (*a)[10]; // Указатель на массив из десяти целых
(g) int (*a)(int); // Указатель на функцию, которая берет целый аргумент и возвращает целое
(h) int (*a[10])(int); // Массив из десяти указателей на функции, которые берут целый аргумент и возвращают целое
Люди часто утверждают, что на некоторые из этих вопросов они обычно ищут ответы в руководствах – согласен. Во время написания этой статьи я сверялся с руководствами, чтобы убедиться, что синтаксис является верным. Однако во время своего интервью я обычно ожидаю, что мне зададут подобный вопрос. Поэтому я должен быть уверен, что у меня есть ответы, по крайней мере, на несколько часов интервью. Кандидаты, которые не знают ответов (или, по крайней мере, большую их часть), просто не готовы к интервью. Если они не могут быть готовы к интервью, к чему они могут быть готовы вообще?
Static
6. В каких случаях используется ключевое слово static? Полностью отвечают на этот вопрос довольно редко. Спецификатор static в языке Си используется в трёх случаях:
(а) Переменная, описанная внутри тела функции как статическая, сохраняет свое значение между вызовами функции.
(b) Переменная, описанная как статическая внутри модуля, но снаружи тела функции, доступна для всех функций в пределах этого модуля и не доступна функциям любых других модулей. То есть, это локализованная глобальная переменная.
(с) Функции, описанные внутри модуля как статические, могут быть вызваны только другими функциями из этого модуля. То есть, область видимости функции локализована модулем, внутри которого она описана.
Большинство кандидатов отвечают правильно на первую часть. Умеренное число кандидатов справляется со второй частью, ну и небольшое количество понимают ответ (с). Это серьёзный недостаток кандидата, если он не понимает важность и преимущества ограничения области видимости данных и кода.
(а) Переменная, описанная внутри тела функции как статическая, сохраняет свое значение между вызовами функции.
(b) Переменная, описанная как статическая внутри модуля, но снаружи тела функции, доступна для всех функций в пределах этого модуля и не доступна функциям любых других модулей. То есть, это локализованная глобальная переменная.
(с) Функции, описанные внутри модуля как статические, могут быть вызваны только другими функциями из этого модуля. То есть, область видимости функции локализована модулем, внутри которого она описана.
Большинство кандидатов отвечают правильно на первую часть. Умеренное число кандидатов справляется со второй частью, ну и небольшое количество понимают ответ (с). Это серьёзный недостаток кандидата, если он не понимает важность и преимущества ограничения области видимости данных и кода.
Const
7. Что означает ключевое слово const? Как только интервьюируемый говорит: «Const - значит константа», я понимаю, что имею дело с непрофессионалом. Дэн Сакс в прошлом году дал исчерпывающее объяснение спецификатору const, так что каждый читатель ESP должен быть досконально ознакомлен с тем, что const может сделать для вас и чего он не может. Если вы не читали эту рубрику, достаточно будет сказать, что const означает «только для чтения». Хотя этот ответ не совсем справедливо отражает предмет разговора, я бы принял его в качестве правильного.
Если кандидат даст правильный ответ, то я задам ему следующие дополнительные вопросы:
Что означают следующие объявления?
const int a;
int const a;
const int *a;
int * const a;
const int * const a;
Если кандидат даст правильный ответ, то я задам ему следующие дополнительные вопросы:
Что означают следующие объявления?
const int a;
int const a;
const int *a;
int * const a;
const int * const a;
Первые два объявления означают одну и ту же вещь, а именно: «а» - это целочисленная константа (только для чтения). Третье означает, что «а» является указателем на целочисленную константу. Четвёртое описывает «а» как константный указатель на целое. И последнее объявление — константный указатель на целочисленную константу.
Если кандидат правильно ответит на эти вопросы, я буду впечатлён.
В данном случае он может поинтересоваться, почему я делаю такой упор на спецификатор const, так как очень легко написать правильно функционирующую программу, не используя его ни разу. Существует несколько причин:
(а) Использование спецификатора const сообщает полезную информацию тому, кто читает ваш код. Фактически, объявление параметра как const, говорит пользователю о его предполагаемом использовании. Если вы когда-нибудь тратили много времени, устраняя неразбериху, оставленную другими людьми, то вы быстро научитесь ценить эту дополнительную информацию. (Конечно, программисты, использующие const, редко оставляют после себя путаницу, которую приходится устранять другим…)
(b) Const сообщает оптимизатору некоторую дополнительную информацию, что потенциально позволяет генерировать более оптимальный код.
(с) Код, в котором используется спецификатор const, проявляет тенденцию к меньшему количеству ошибок.
Если кандидат правильно ответит на эти вопросы, я буду впечатлён.
В данном случае он может поинтересоваться, почему я делаю такой упор на спецификатор const, так как очень легко написать правильно функционирующую программу, не используя его ни разу. Существует несколько причин:
(а) Использование спецификатора const сообщает полезную информацию тому, кто читает ваш код. Фактически, объявление параметра как const, говорит пользователю о его предполагаемом использовании. Если вы когда-нибудь тратили много времени, устраняя неразбериху, оставленную другими людьми, то вы быстро научитесь ценить эту дополнительную информацию. (Конечно, программисты, использующие const, редко оставляют после себя путаницу, которую приходится устранять другим…)
(b) Const сообщает оптимизатору некоторую дополнительную информацию, что потенциально позволяет генерировать более оптимальный код.
(с) Код, в котором используется спецификатор const, проявляет тенденцию к меньшему количеству ошибок.
Volatile
8. Что означает ключевое слово volatile? Приведите три различных примера его использования. Ключевое слово volatile информирует компилятор о том, что переменная может быть изменена не только из текущего выполняемого кода, но и из других мест. Тогда компилятор будет избегать определенных оптимизаций этой переменной.
Примеры volatile переменных:
(а) Регистры в периферийных устройствах (например, регистры состояния)
(b) Глобальные переменные, используемые в обработчиках прерываний.
(с) Глобальные переменные, используемые совместно несколькими задачами в многопотоковом приложении.
Если кандидат не знает ответ на этот вопрос, он не получит работу. Я считаю, что это наиболее существенный вопрос, который позволяет отличить «Си-программиста» от «программиста встраиваемых систем». Программисты встраиваемых систем сталкиваются с аппаратными средствами, прерываниями, ОСРВ, и тому подобным. Все эти вещи требуют использования volatile переменных. Непонимание идеи спецификатора volatile приведет к катастрофе.
Исходя из (сомнительного) предположения, что интервьюируемый ответит на этот вопрос правильно, я люблю копнуть немного глубже, чтобы посмотреть, на самом ли деле он полностью понимает значение этого спецификатора. В частности, я задам ему следующие вопросы:
Примеры volatile переменных:
(а) Регистры в периферийных устройствах (например, регистры состояния)
(b) Глобальные переменные, используемые в обработчиках прерываний.
(с) Глобальные переменные, используемые совместно несколькими задачами в многопотоковом приложении.
Если кандидат не знает ответ на этот вопрос, он не получит работу. Я считаю, что это наиболее существенный вопрос, который позволяет отличить «Си-программиста» от «программиста встраиваемых систем». Программисты встраиваемых систем сталкиваются с аппаратными средствами, прерываниями, ОСРВ, и тому подобным. Все эти вещи требуют использования volatile переменных. Непонимание идеи спецификатора volatile приведет к катастрофе.
Исходя из (сомнительного) предположения, что интервьюируемый ответит на этот вопрос правильно, я люблю копнуть немного глубже, чтобы посмотреть, на самом ли деле он полностью понимает значение этого спецификатора. В частности, я задам ему следующие вопросы:
(а) Может ли аргумент быть одновременно и const и volatile? Аргументируйте ваш ответ.
(b) Может ли указатель быть volatile? Аргументируйте ваш ответ.
(с) Что не так со следующей функцией?:
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
Ответы следующие:
(а) Да. Например, регистр состояния доступный только для чтения. Он volatile, потому что может меняться неожиданно. Он const, потому что программа не должна пытаться изменить его.
(b) Да. Хотя это не общепринятый случай. Например, когда обработчик прерываний изменяет указатель на буфер.
(с) Эта функция потенциально опасна. Назначение кода состоит в возвращении квадрата значения, указанного при помощи *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;
}
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}
Поскольку значение переменной на которую указывает ptr может неожиданно измениться, то существует возможность, что «а» и «b» будут разными. Следовательно, этот код может возвратить число, которое не будет квадратом! Правильный вариант кода в данном случае такой:
int square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}
Операции с битами
9. При программировании встраиваемых систем приходится часто манипулировать битами в регистрах или переменных. Дана целая переменная «а», напишите два фрагмента кода. Первый должен установить 3-ий бит этой переменной. Второй должен очищать его. В обоих случаях, другие биты должны остаться без изменений.
Вот три наиболее распространённых ответа на этот вопрос:
(а) Никаких идей. Кандидат никоим образом не может работать со встраиваемыми системами.
(b) Используйте битовые поля. Битовые поля покинули этот мир, наряду с триграфами, как самая бестолковая часть языка Си. Битовые поля по своей природе не переносимы между компиляторами, и по существу гарантируют, что ваш код не допускает многократного использования. Недавно я имел несчастье взглянуть на драйвер, который написали в Infineon для одного из их наиболее сложных коммуникационных чипов. Там использовались битовые поля, и драйвер был полностью бесполезен, потому что мой компилятор выполнял битовые поля другим путём. Мораль – никогда не позволяйте программисту, не знакомому со встраиваемыми системами, подходить к реальному оборудованию.
Я недавно смягчил свою позицию по битовым полям. По меньшей мере один производитель компиляторов (IAR) теперь предлагает ключ компилятора для определения расположения битовых полей. К тому же их компилятор генерирует оптимальный код с регистрами описанными как битовые поля, и по существу - теперь я использую битовые поля в IAR-приложениях.
Вот три наиболее распространённых ответа на этот вопрос:
(а) Никаких идей. Кандидат никоим образом не может работать со встраиваемыми системами.
(b) Используйте битовые поля. Битовые поля покинули этот мир, наряду с триграфами, как самая бестолковая часть языка Си. Битовые поля по своей природе не переносимы между компиляторами, и по существу гарантируют, что ваш код не допускает многократного использования. Недавно я имел несчастье взглянуть на драйвер, который написали в Infineon для одного из их наиболее сложных коммуникационных чипов. Там использовались битовые поля, и драйвер был полностью бесполезен, потому что мой компилятор выполнял битовые поля другим путём. Мораль – никогда не позволяйте программисту, не знакомому со встраиваемыми системами, подходить к реальному оборудованию.
Я недавно смягчил свою позицию по битовым полям. По меньшей мере один производитель компиляторов (IAR) теперь предлагает ключ компилятора для определения расположения битовых полей. К тому же их компилятор генерирует оптимальный код с регистрами описанными как битовые поля, и по существу - теперь я использую битовые поля в IAR-приложениях.
(с) Используйте #define и битовые маски. Это хорошо переносимый метод и его стоит использовать. Оптимальное решение этой проблемы, на мой взгляд, было бы таким:
#define BIT3 (0?1 << 3)
static int a;
void set_bit3(void)
{
a |= BIT3;
}
void clear_bit3(void)
{
a &= ~BIT3;
}
#define BIT3 (0?1 << 3)
static int a;
void set_bit3(void)
{
a |= BIT3;
}
void clear_bit3(void)
{
a &= ~BIT3;
}
Некоторые люди, наряду с именованными константами, предпочитают задавать маску для значений set и clear. Это тоже приемлемо. Важные элементы, которые мне нужны – это использование именованных констант, наряду с конструкциями |= и &= ~
Доступ к ячейкам памяти
10. Довольно часто программистам встраиваемых систем требуется доступ к определенной ячейке памяти. В некотором проекте требуется установить целую переменную по абсолютному адресу 0?67a9 к значению 0xaa55. Напишите код, выполняющий эту задачу.
Цель данной задачи выяснить, знаете ли Вы, что разрешено приведение целочисленных типов к указателю, чтобы получить доступ по абсолютному адресу. Правильный синтаксис может варьироваться в зависимости от стиля. Однако обычно я хочу увидеть что-то вроде этого:
int *ptr;
ptr = (int *)0?67a9;
*ptr = 0xaa55;
Более запутывающий вариант выглядит так:
*(int * const)(0?67a9) = 0xaa55;
Доступ к ячейкам памяти
10. Довольно часто программистам встраиваемых систем требуется доступ к определенной ячейке памяти. В некотором проекте требуется установить целую переменную по абсолютному адресу 0?67a9 к значению 0xaa55. Напишите код, выполняющий эту задачу.Цель данной задачи выяснить, знаете ли Вы, что разрешено приведение целочисленных типов к указателю, чтобы получить доступ по абсолютному адресу. Правильный синтаксис может варьироваться в зависимости от стиля. Однако обычно я хочу увидеть что-то вроде этого:
int *ptr;
ptr = (int *)0?67a9;
*ptr = 0xaa55;
Более запутывающий вариант выглядит так:
*(int * const)(0?67a9) = 0xaa55;
Прерывания
11. Прерывания являются важной частью встраиваемых систем. Поэтому многие производители компиляторов предлагают к стандартному языку Си расширение для поддержки прерываний. Обычно - это новое ключевое слово __interrupt. Следующий код использует __interrupt, чтобы описать программу обработки прерываний. Прокомментируйте его.
__interrupt double compute_area(double radius)
{
double area = PI * radius * radius;
printf(“\nArea = %f”, area);
return area;
}
В этой функции так много неправильного, что почти невозможно понять с чего начинать.
(а) Обработчик прерываний не может возвращать значение. Если вы не понимаете этого, то не получите работу.
(b) Обработчик прерываний не может принимать параметры. Если вы упустили это, смотрите пункт (а) для понимания ваших перспектив получить работу.
(с) Во многих процессорах/компиляторах, операции с плавающей точкой не обязательно реентерабельны. В некоторых случаях они нуждаются в добавлении в стек дополнительных регистров, в других случаях, они просто не могут выполнить эти операции в обработчике прерываний. Более того, при условии главного практического требования, что обработчики прерываний должны быть короткими и ясными, возникает вопрос об уместности выполнения здесь подобной математики.
(d) Функция printf() тоже часто испытывает трудности с реентерабельностью и работоспособностью. Если бы вы пропустили пункты (с) и (d), то я не был бы с вами слишком строг. А если вы справились с этим двумя пунктами, то ваши перспективы получить работу выглядят всё лучше и лучше.
{
double area = PI * radius * radius;
printf(“\nArea = %f”, area);
return area;
}
В этой функции так много неправильного, что почти невозможно понять с чего начинать.
(а) Обработчик прерываний не может возвращать значение. Если вы не понимаете этого, то не получите работу.
(b) Обработчик прерываний не может принимать параметры. Если вы упустили это, смотрите пункт (а) для понимания ваших перспектив получить работу.
(с) Во многих процессорах/компиляторах, операции с плавающей точкой не обязательно реентерабельны. В некоторых случаях они нуждаются в добавлении в стек дополнительных регистров, в других случаях, они просто не могут выполнить эти операции в обработчике прерываний. Более того, при условии главного практического требования, что обработчики прерываний должны быть короткими и ясными, возникает вопрос об уместности выполнения здесь подобной математики.
(d) Функция printf() тоже часто испытывает трудности с реентерабельностью и работоспособностью. Если бы вы пропустили пункты (с) и (d), то я не был бы с вами слишком строг. А если вы справились с этим двумя пунктами, то ваши перспективы получить работу выглядят всё лучше и лучше.
Примеры кода
12. Что делает следующий фрагмент кода и почему?void foo(void)
{
unsigned int a = 6;
int b = -20;
(a+b > 6) ? puts(“> 6?) : puts(“<= 6?);
}
Этот вопрос проверяет, разбираетесь ли вы в правилах представления целых чисел в Си, – области, которую я нахожу очень плохо понимаемой многими разработчиками. Ответ состоит в том, что выведется “> 6”. Причина - в выражениях, включающих типы со знаком и без все операнды приводятся к типам без знака. Таким образом, -20 становится очень большим положительным целым, и выражение оценивается больше, чем 6. Это очень важный момент во встраиваемых системах, где часто используются типы данных без знака. Если вы ответили на этот вопрос неправильно, то вы сильно рискуете быть не принятым на работу.
13. Прокомментируйте следующий фрагмент кода:
unsigned int zero = 0;
unsigned int compzero = 0xFFFF; /*1’s complement of zero */
В машинах, где int не равен 16-и битам, это будет неправильно. Следует записать так:
unsigned int compzero = ~0;
Этот вопрос даёт возможность по-настоящему узнать, понимает ли кандидат важность длины слова в компьютере.
13. Прокомментируйте следующий фрагмент кода:
unsigned int zero = 0;
unsigned int compzero = 0xFFFF; /*1’s complement of zero */
В машинах, где int не равен 16-и битам, это будет неправильно. Следует записать так:
unsigned int compzero = ~0;
Этот вопрос даёт возможность по-настоящему узнать, понимает ли кандидат важность длины слова в компьютере.
На этой стадии кандидаты либо полностью деморализованы, либо они на коне и предаются приятному времяпровождению. Если очевидно, что кандидат не очень хорош, то на этом тест заканчивается. Однако если кандидат отлично себя проявил, то я задаю дополнительные вопросы. Эти вопросы сложные и я ожидаю, что только лучшие кандидаты справятся с ними.
Здесь я ожидаю, что пользователь упомянет фрагментацию памяти, проблемы сбора мусора и т.д. Эта тема была хорошо описана в ESP, главным образом, Плогером. Его объяснения гораздо более проницательны, чем что-либо из того, что я могу предложить здесь, поэтому прочтите старые номера! Убаюкав кандидата ощущением ложной безопасности, я задаю такой интересный вопрос:
Динамическое распределение памяти
14. В чём заключаются проблемы с динамическим распределением памяти во встраиваемых системах?Здесь я ожидаю, что пользователь упомянет фрагментацию памяти, проблемы сбора мусора и т.д. Эта тема была хорошо описана в ESP, главным образом, Плогером. Его объяснения гораздо более проницательны, чем что-либо из того, что я могу предложить здесь, поэтому прочтите старые номера! Убаюкав кандидата ощущением ложной безопасности, я задаю такой интересный вопрос:
Что делает следующий фрагмент кода и почему?
char *ptr;
if ((ptr = (char *)malloc(0)) == NULL) {
puts(“Got a null pointer”);
}
else {
puts(“Got a valid pointer”);
}
Это довольно забавная задачка. Я столкнулся с ней недавно, когда мой коллега ненароком передал значение 0 в malloc и получил назад действительный указатель! Покопавшись немного, я обнаружил, что результат функции malloc (0) определён реализацией, так что правильный ответ гласит «по обстоятельствам». Я использую этот вопрос, чтобы начать беседу о том, что, по мнению интервьюируемого, является верным для действий функции malloc. Получение правильного ответа здесь не важно, главное как вы подойдёте к проблеме и логически обоснуете свое решение.
if ((ptr = (char *)malloc(0)) == NULL) {
puts(“Got a null pointer”);
}
else {
puts(“Got a valid pointer”);
}
Это довольно забавная задачка. Я столкнулся с ней недавно, когда мой коллега ненароком передал значение 0 в malloc и получил назад действительный указатель! Покопавшись немного, я обнаружил, что результат функции malloc (0) определён реализацией, так что правильный ответ гласит «по обстоятельствам». Я использую этот вопрос, чтобы начать беседу о том, что, по мнению интервьюируемого, является верным для действий функции malloc. Получение правильного ответа здесь не важно, главное как вы подойдёте к проблеме и логически обоснуете свое решение.
Typedef
15. Typedef часто используется в Си для объявления синонимов существующих типов данных. Также для подобных действий возможно использование препроцессора. Например, рассмотрите следующий фрагмент кода:
#define dPS struct s *
typedef struct s * tPS;
Цель в обоих случаях состоит в объявлении dPS и tPS как указателей на структуру «s». Какой метод предпочтительней и почему?
Это очень хитрый вопрос, и всякий, кто ответит на него правильно, получит мои совершенно справедливые поздравления. Ответ заключается в том, что предпочтителен typedef. Рассмотрите объявления:
dPS p1,p2;
tPS p3,p4;
Из первого примера следует:
struct s * p1, p2;
что объявляет р1 как указатель на структуру и р2 как обычную структуру, что, вероятно, вовсе не то, что вы хотели. Второй пример правильно определяет р3 и р4 как указатели.
Это очень хитрый вопрос, и всякий, кто ответит на него правильно, получит мои совершенно справедливые поздравления. Ответ заключается в том, что предпочтителен typedef. Рассмотрите объявления:
dPS p1,p2;
tPS p3,p4;
Из первого примера следует:
struct s * p1, p2;
что объявляет р1 как указатель на структуру и р2 как обычную структуру, что, вероятно, вовсе не то, что вы хотели. Второй пример правильно определяет р3 и р4 как указатели.
Запутывающий синтаксис
16. Си позволяет некоторые ужасные конструкции. Допустима ли эта конструкция, и если да, то что делает этот код?
int a = 5, b = 7, c;
c = a+++b;
Предполагается, что этот вопрос будет весёлым завершением опроса, в то время как, верите, или нет, это вполне допустимый синтаксис. Вопрос в том, как компилятор его воспримет? Бедные авторы компиляторов обсуждали эту проблему и пришли к правилу «максимального перемалывания», которое гласит, что компилятор должен «откусить» такой большой (допустимый) кусок, какой только может. Следовательно, этот код будет восприниматься так:
c = a++ + b;
Поэтому, после выполнения этого кода, a = 6, b = 7 & c = 12;
Если вы знали ответ, или правильно угадали – тогда дело сделано. Если вы не знали ответа – не страшно. Я нахожу наибольшую пользу от этой задачи в том, что она очень хорошо стимулирует вопросы относительно стилей кодирования и преимуществ использования контроля стиля программирования на соответствие стандартам.
c = a++ + b;
Поэтому, после выполнения этого кода, a = 6, b = 7 & c = 12;
Если вы знали ответ, или правильно угадали – тогда дело сделано. Если вы не знали ответа – не страшно. Я нахожу наибольшую пользу от этой задачи в том, что она очень хорошо стимулирует вопросы относительно стилей кодирования и преимуществ использования контроля стиля программирования на соответствие стандартам.
Ну, вот и всё, друзья. Это была моя версия Си-теста. Я надеюсь, вы получили столько же удовольствия при его выполнении, сколько я - при написании. Если Вам нравится этот тест, то, пожалуйста, используйте его при найме сотрудников.
Comments
Где впервые была опубликована не знаю. Висит на нескольких сайтах.
Бывает оптимально в смысле какого-то критерия оптимальности.
И подоптимально.
Зарубежной литературы по этой теме гораздо больше.
Ответил на все вопросы верно, за исключением 13-го. Разве stdtypes отменили, и юзать типы с известной длиной нельзя ?
Если вы такой поклонник макросов (макрух), отчего не дбавили вопросы про дупль диезы (##). На мой взгляд это достаточно редкий случай, когда макрухи имеют право на жисть.
Как Вы сами относитесь к макросам.
Или, если тестируемый отметит, что стиль программировани я, который используется в доброй половине примеров - кроме очень редких случаев, когда сокращение действительно необходимо, вроде int *a[10]; - это пример плохого стиля, недоопределений , итп, и писать так программу - недопустимо само по себе?
/* Чтобы пояснить сказанное выше, представим себе вопрос теста - что делает следующий код?
int p=1; printf("%d %d %d\n", p++, p++, p++);
Правильный ответ - да ничего хорошего он не делает, написать так - ошибка, причем школьная. Будет ли принят данный ответ автором текста? :)
А как насчет такого (a > b | c) ? Если мне такое действительно будет нужно, то я скорее напишу ((a>b)|c), чтобы не думать. Или (a>(b|c)), в зависимости от того, что реально имелось в виду. Вариант, "чтобы не дразнить гусей", вроде (((a>b)?1:0)|c) , тоже абсолютно законный, т.к. компилятор при генерации кода все равно делает именно это. */
int p=1;
printf("%d %d %d\n", p++, p++, p++);
Может кто-нибудь объяснить природу ошибки, поскольку все написанное в рамках дозволенного.
Вот последние два вопроса (или три, в зависимости от того, как считать) - в отличие от ряда других вопросов - потенциальному аппликанту я бы задал. Они требуют не столько книжного знания, которое можно прочитать, сколько опыта работы и понимания, что и для чего делается в программировани и вообще.
некорректными вопросами на собеседовании можно запутать кого угодно! Придумать запутанный вопрос - плёвое дело, но насколько этот вопрос действительно соответствует реалиям?
Для ембеддеров важнО не столько знание синтаксиса, сколько понимание того, что происходит в реальной железке с её ограниченными возможностями, а для реализации концепции, сформировавшейс я на основе знаний железа, всегда можно заглянуть в шпаргалку!
кстати, встречал утверждение, что способность работать с литературой, способность найти нужную информацию как раз и является произнаком профессионализм а! так я с этис утверждением абсолютно согласен!
и ещё: автор считает себя крутым "сишником", и не стесняется оценивать способности кандидатов, но что-то он ассемблер не упоминает. Или он отрицает его использование во встраиваемых устройствах?
ЗЫ: программирую не то чтобы чистые ембеды, но тоже не десктопы, больше 10 лет.
к тому же чем больше знаешь - тем лучше в любом случае.
11.Вы уверены, что 6+7=12?
12.Что сделает компилятор увидев такую конструкцию Quote: ? :-)
Я в профессии уже 10 лет из них разработчиком embedded 8. Скажу прямо, статья крайне субъективна. Во-первых, относительно стандарта «С», сразу скажу - это для теоретиков. Многие вещи, описанные в стандарте, просто не возможно реализовать на 8-ми, 16-ти или иногда даже 32-ух битных контроллерах, или, если реализуешь, то пользы будет как от выеденного яйца. Это связано с многими аспектами о которых долго можно говорить, кто не верит, может для начала просто сравнить карту памяти для разных ядер микроконтроллер ов. Ну а кому не понятно какое имеет значение карты памяти на язы, то боюсь придеться еще поработать в профессии, чтение книг здесь может не помочь. Многие полезные фичи ядра вообще не возможно расскрыть не перелопатив этот стандарт и не создав свой стандарт в стандарте. Кто хочет пример, привожу, компилятор «Keil», «Iar». В компиляторе «Keil» по умолчанию нельзя сделать рекурсию. Что об этом скажет стандарт, многоуважаемые читатели такового? Таких примеров вагон и телега. Даже для одного ядра язык двух приведенных выше компиляторов настолько разный, что об переносимости кода и речи быть не может. Это часть статьи вообще меня порадовала, типа биты это зло! Однако, похоже, где-то все-таки сознание сработало, «Iar» натолкнул на мысли.
Что там еще из самого, а ну да установить третий бит. Ну вообще на этот вопрос мало кто не ответил, за мою практику, тем не менее, это не показатель. Был бы мозг!!! А булева алгебра учиться за день. У меня студент был в подчинении, он вообще «С» не знал. Я ему всего три лекции по часу прочитал и на телефоне постоянно с ним был, наверно пол недели, и в течении следующего месяца он мне сдал проект без нареканий. Кто-нибудь из вас видел, что бы язык учили за полнедели? Вообще, ребята скажу так, человека способного выполнить работу в срок и хорошо видно сразу, но не каждый теоретик может его заметить. Я могу скастить большие пробелы в знаниях соискателю, все-таки, как я уже заметил, нефиг быть такими узколобыми и тупа прикрывать свой зад словами «это стандарт». Я практик, не теоретик, мне этот стандарт хорош, только если он напечатан на мягкой бумаге. Надо понять чего стоит сам человек. Уровень интеллекта и желание капать - вот характеристики плодотворной почвы в которую стоит заложить зерно и не сожалеть потом о том, что потратил время. Да вот еще, о «volatile» походу автор долго врубался нах. это вообще надо и как оно работает, потому, наверно и не знает, как покруче зарисоватьсятьс я этим своим приобретенным знанием. Хотя вроде врубился без нареканий, описал синдромы правильно, правда насчет работоспособнос ти примера с функцией можно поспорить. Дело в том, что компиляторы обычно не настолько тупы и преобразуют не к виду: a = *ptr; b = *ptr;
а к виду a = b = *ptr; что в корень меняет дело. Ну да ладно, в целом согласен, порой лучше перебздеть, чем не добздеть и автор это нам демонстрирует.
Тот кто не писал под embedded наверняка не будет знать volatile, и это не говорит ровным счетом не о чем, кроме того, что он либо, как я уже говорил, не писал на embedded, либо писал на таком уровне, до которого автору еще похоже приодеться подрасти. Это к примеру «TreadX» или «Android». Мало кто догадывается, особенно те кто на нем пишет, но «Android» - это тоже embedded. Начнем с того что попроще, «ThreadX». В этой операционке до регистра придется капать так далеко, что свет покажется не мил, да и собственно зачем, если есть масса функций, которые и значение заберут и преобразуют и mutex поставят и semaphore прикрутить не забудут и event тебе дернут, если ты уже совсем ленивый.
Я было закончил свои коменты, но душа поэта не сдержалась, упал глаз еще на пару узколобостей автора:
Code:
void foo(void)
{
unsigned int a = 6;
int b = -20;
(a+b > 6) ? puts(“> 6?) : puts(“<=?);
}
Не правильный ответ на данный вопрос может быть не по причине на знания, цетирую: "правил представления целых чисел", но так же из-за вопроса, на который мало кто ответит однозначна: к какому виду (знаковому или безнаковому) предпочтет привезти ответ компилятор. Сказать по правде, человек, который однозначна на это ответит меня насторожит. На практике достаточно просто привести тип, и это правильно. Тем самым разработчик экономит массу времени своего на отладке (если он всетаки ошибся) и моего на рефакторинке и чеккоде. Более того, если он этого не сделает, я попрошу его это сделать, или исправлю за него. Тем самым я создаю более надежный код, который с большей вероятностью заработает на другом компиляторе, в котором, к примеру, по каким-то причинам разработчики не дочитали стандарт и лоханулись, или наоборот, лоханулись на предыдущей версии компилятора, а в новой все исправили и после обновления код стал не рабочим. Вот это все далеко не редкость.
Nigel Jones плевать хотел на Вас. =)
В целом, мне статья очень понравилась. Ошибки? Оно, конечно, есть. Но, кто не ошибается, тот ничего не делает. На СИ смотрю, как на Закон, а на компилятор, как на Исполнительную власть. И то, и другое воспринимаю, как есть. Ну и, не знание закона не освобожгвет от ответственности при его несоблюдении.
Для меня - это первый сайт, который я выловил в Инете по СИ для IAR на русском. И примеры достаточно навороченные, а не просто типа "дернуть ножку". Хотя реально все, конечно, значительно сложней.
Все это читал на английском в фирменных материалах и, конечно, многое не допонял.
Подавляющее большинство пользователей делают простые проекты. Для них информации вполне достаточно. У меня же еще остались вопросы и, если тема не закрыта, буду пробовать их задавать.
А тестовые вопросы - оно, конечно, приятно, какие бы они там не были!
ptr = (int *)0?67a9;
*ptr = 0xaa55;
Более запутывающий вариант выглядит так:
*(int * const)(0?67a9) = 0xaa55;
ОНО НЕ КОМПИЛИРУЕТСЯ В ИАРЕ даже, не говоря уже о том, работает ли.
ptr = (int *)0?67a9;
*ptr = 0xaa55;
Более запутывающий вариант выглядит так:
*(int * const)(0?67a9) = 0xaa55;
ОНО НЕ КОМПИЛИРУЕТСЯ В ИАРЕ.
RSS feed for comments to this post