Skip to main content

Задачка на собеседование

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

#include <cstdlib>
#include <iostream>
using namespace std;
int main (int argc, char** argv){
cout<<argv[argc-1]<<endl
<<argc[argv-1]<<endl;
return EXIT_SUCCESS;
}

Ответ ниже белым по белому (да, такой способ я у Эльдара Мусаева подсмотрел) чтоб посмотреть выделяем текст
===отсюда===
На самом деле все просто и вытекает из комутативности операции сложения:

#include <cstdlib>
#include <iostream>
using namespace std;
int main (int argc, char** argv){
cout<<(*argv+(argc-1))<<endl<<((argc-1)+*argv)<<endl;
cout<<*(argv+(argc-1))<<endl<<*((argc-1)+argv)<<endl;
return EXIT_SUCCESS;
}

===досюда===
Отдельное Спасибо White Knight за нахождение ошибки в коде ответа(см. комментарии).

Comments

  1. В принципе тривиально, если не забывать про это свойство сишных массивов.

    Гораздо больше времени требуется чтобы понять как это всетаки работает. То есть что хотел этим кодом изобразить автор. :)

    Хотя вот откомментировал и задумался... сложение int с чем либо изменяет int известно на сколько. А вот сложение чего-то с указателем зависит от типа указателя. Хотя не, все логично. в первом случае массив указателей argv и индекс int - 1, во втором случае int преобразовывается в безтиповый указатель вероятно, размер элемента которого вроде бы приравнивается к 1 в некоторых версиях компиляторов, в то время как argv выступает в роли индекса но -1 уменьшает его на размер указателя.

    И еще один момент как воспримет ostream argc[argv], если он ошибется с типом может получиться очень нехорошо.

    И главное все прекрасно понимают что за такой код руки поотрывать мало, но тем не менее постоянно спрашивают это на собеседованиях... :) дескать надо знать. Тоже верно.

    ReplyDelete
  2. И главное никаких варнингов от компилятора... загадочно...

    Я понимаю что есть комутативность операции сложения и всякое такое.. мне не понятно как компилятор пропускает такое без ругани??? Тем более что это строгий в отношении типов компилятор С++.... :)

    Не иначе как опять таки - зло совместимости со старыми сишными проектами, которые эту фишку активно юзали когда-то.

    ReplyDelete
  3. я думаю что тут дело не в совместимости, а именно в интерпретации массивов как указателей и свойствах операции над ними. Теоретически код сгенерированный для a[i] и a+i должен выглядеть одинаково, потому и для i+a и i[a] должен выглядеть тоже одинаково, иначе нарушается принцип коммутативности, а следовательно и компилятор матюкаться не обязан, так как такое поведение вполне обоснованно.

    ReplyDelete
  4. Это в си все в принципе обоснованно. А в С++ все должно быть гораздо сложнее, хотя бы потому, что там есть operator []...

    кстати нашел вот в gcc: gcc/gcc/c-typeck.c, функция build_array_ref.

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

    В то время как отсутствие хотя бы одного указателя - argc[argc - 1] вызывает ошибку.

    ИМХО, это продиктовано именно требованиями совместимости. То есть раньше это видимо активно практиковали. :)

    Непонятно почему C++ это допускает. Я бы запретил. :)

    ReplyDelete
  5. Век живи - век учись))

    Даже такой код проходит:
    char str[] = "1";
    int i = 0;
    i[str] = '2';
    cout << str;

    Взглянув на такой код, ни о какой коммутативности и думать не придется. В голове будет стоять только один вопрос: "Как??.." ))

    ReplyDelete
  6. И вот попробуй скажи на собеседовании, что это нифига никакая не комутативность, а просто компилятор молча переставляет то, что глупые программисты сдуру написали наоборот...

    Не поймут. :)

    ReplyDelete
  7. Сергей, а твой код вообще странно, что работает... Потому что "1" это по сути своей const char *. По идее он должен располагаться в .rodata, И ни о какой модификации не должно быть и речи.

    Крайне странно, что работает.

    Но даже не смотря на это за i[str] всеравно стоит отрывать руки по самое... :)

    ReplyDelete
  8. char str[] = "1"; - инициализация массива - элементы копируются, поэтому его менять можно. РО было бы, если бы код выглядел вот так:
    char * str = "1";

    ReplyDelete
  9. Во-первых, по стандарту не совсем валидный - выход указателя за начало массива. array-1+2 это не array+2-1, в первом выражении ошибка, во втором нет.

    Во-вторых, вроде же вменяемые интервьюверы высказывают своё "Фу" по задачам на игру в синтаксис и малоизвестные особенности.
    Так как человек, который действительно хорошо знает С++ и вообще как решать проблемы и создавать программы, может либо озвереть и доказать интервьюверу что это ОН не знает С++, либо просто составить плохое мнение о компании, либо просто зависнуть от некорректности вопроса (какой конкретно из подвохов имелся ввиду, и почему невалидный код называют валидным).

    ReplyDelete
  10. This comment has been removed by the author.

    ReplyDelete
  11. Ещё один подвох - http://www.open-std.org/JTC1/SC22/WG21/docs/lwg-active.html#343 . Тут используется endl, а где он объявлен?

    ReplyDelete
  12. Yuriy Volkov, да я подозревал что копируется. был неправ.

    Кстати по поводу кода. Многие процессоры умеют оперировать с памятью посредством базы/индекса/множителя. И собственно приведение операции индексирования к сложению не позволит использовать весьма эффективные оптимизации подобного рода.

    ИМХО.

    ReplyDelete
  13. это задача на собеседование? интересно что нею хотели выяснить :)))
    в реальность такой код даже обкуреный индус не напишет, а как способ проверить понимание взаимосвязи указателей и масивов оно просто тупо выглядит. имхо language laweyrs которые такое пробуют спрашивать нужно сразу нафиг посылать.

    ReplyDelete
  14. 2_winnie:
    Во-первых, по стандарту не совсем валидный - выход указателя за начало массива. array-1+2 это не array+2-1, в первом выражении ошибка, во втором нет.
    а по моему ошибки нету ни там ни там. в С++ кажись нет проверки выхода указателя за границу массива

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

    2cencio:
    нет, это реальный код, чисто машинально перепутали местами argc и argv, при ревью это обнаружили и зависли, так как код работал корректно.

    ReplyDelete
  15. Не люблю C++ за то, что приходится думать не только на уровне языка, но и на уровень ниже. Иначе побочные эффекты неизбежны.

    ReplyDelete
  16. ну ситуации подобные описанной в посте возникают на самом деле редко ))

    ReplyDelete
  17. Где-то у Саттера на эту тему было. Тянется это правило из C - x[y] раскрывается в *(x+y), при этом там должны быть указаталь и целое число в любом порядке.

    Можно получить такой забавный код:
    int x[3] = { 11, 12, 13 };
    cout << 0[x] << endl;

    И он скомпилируется и будет работать.

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

    ReplyDelete
  18. Я заранее извиняюсь, если что недосмотрел, но я постарался внимательно прочитать комменты.

    Кажется, никто не увидел ошибку в ответе...

    Как сказал jia3ep, такой код работает, потому что это вытекает из принципа работы оператора []: запись a[i] эквивалентна *(a + i) или *(i + a).

    А у Вас в ответе совсем не то же самое:
    cout<<(*argv+(argc-1))<<endl<<((argc-1)+*argv)

    Должно быть так:
    cout<< *(argv+(argc-1))<<endl<< *((argc-1)+argv)

    ReplyDelete
  19. 2White Knight: совершенно верно, большое спасибо, исправлю

    ReplyDelete
  20. Долго думал и экспериментировал.

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

    проходят даже выражения типа
    BOOST_ASSERT (argv + argc - 1 == argc + argv - 1);

    то есть судя по всему, компилятор автоматически переворачивает выражения типа (int) + (ptr).

    Хоть бы варнинг чтоль какой нибудь сделали на эту тему. Имхо так писать не стоит.

    ReplyDelete
  21. Если не ошибаюсь этот пример с указателями и массивами есть ещё в книжке Кернигана и Ричи 1978 г.

    Кроме того, всегда думал что при сложении вообще говоря тип суммы совпадает с самым широким типом операндов: int+double=double, int+char=int и т.д. Аналогичнно int+ptr=ptr.

    Вот, кстати, кусок из стандарта:
    3.3.6 Additive operators
    ...
    When an expression that has integral type is added to or subtracted
    from a pointer, the integral value is first multiplied by the size of
    the object pointed to. The result has the type of the pointer
    operand.

    ...
    Просто по-другому бессмысленно делать.

    ReplyDelete

Post a Comment

СООБЩЕНИЕ СПАМЕРАМ: прежде чем пытаться оставить ссылку на свой ресурс в комментарии, прошу обратить внимание на тег nofollow, которым они помечены и зря не терять ни свое ни мое время. А будете упорствовать еще и noindex поставлю