четверг, октября 02, 2008

Приключение со строками

Злосчастный код, который похитил у меня несколько часов времени.
char c='0';
string str="Text"+c;

Что будет лежать в str? Отнюдь не "Text0" как можно было бы ожидать.

Здесь к адресу строки "Text" добавляется 48 байт. И конструктор str получает указатель на эту область памяти. По несчастному стечению обстоятельств в этой области памяти у меня была строка "RESTART". И вот с этого момента начались удивительные приключения этого RESTART'а, которые в итоге привели к обращению по нулевому указателю, причины которого мне и пришлось раскапывать.

Компиляторы, которые я смотрела - CodeWarrior и Visual Studio 2005 даже ворнинга в этом случае не дают.

57 комментариев:

  1. Это еще раз подтверждает тот факт, что когда работаешь на C++ не стоит связываться с примитивными типами. :)

    string str = string("Text") + c;

    ИМХО сработает как надо, хотя не проверял. Надо вырабатывать у себя привычки работать с объектами.

    PS: Ни в коем случае не хочу никого обидеть.

    ОтветитьУдалить
  2. это еще раз подтверждает тот факт, что перегруженные операторы C++ вырабатывают дурацкие привычки.
    а потом за такими код вычищать неделями приходится.

    ОтветитьУдалить
  3. Анонимный2/10/08 15:48

    нудык ктож к const char * прибавляет char ?
    ))))

    ОтветитьУдалить
  4. Соглашусь с waker. Я вообще на plain C пишу ;-)

    ОтветитьУдалить
  5. GCC может быть предупредил бы.

    Еще Студия, не выдает предупреждения если написать что-то вроде
    printf("%s", myClass);
    GCC же в этом случае говорит почти по-русски, что тут использовать не POD-типы нельзя.
    Я согласен, кстати с Андреем. Еще лучше не использовать типы int или short или long, а использовать что-то вроде Int16, UInt32, size_t, и т.п.

    ОтветитьУдалить
  6. 2arkanoid: Ну дык тем, кто пишет на си, никогда не придет в голову складывать строки. :)

    А С++ предоставляет для этой цели ИМХО достаточно удачную абстракцию. И это балует. :)

    string text = "Text";
    string srt = text + c;

    тоже не создал бы проблем, но тяжелое наследие си, в виде арифметики над указателями, мешает это интуитивно совместить. :)

    2waker: как объединяют строки типа string у вас?

    ОтветитьУдалить
  7. Я еще раз порадовался, что перешел на PHP/Javascript.
    Велик и могуч C++, но чем более он велик, тем больше возможность совершить подобную ошибку

    ОтветитьУдалить
  8. gcc тоже никак на это не ругается.

    Для любого компилятора что char что int - одно и тож.

    2naishin: Я не очень люблю замещать long, int, short - последний нужен крайне редко, а первые два и так достаточно понятны. Хотя это имеет смысл при настоящем кроссплатформенном программировании.

    ОтветитьУдалить
  9. Анонимный2/10/08 23:47

    Я, конечно, дико извиняюсь, но возможностей набагать в PHP примерно столько же, сколько и в C++, то есть немеряно.

    ОтветитьУдалить
  10. Анонимный3/10/08 01:54

    недавно был очень похожий случай.
    есть функция:
    string addsign(char sign, const string& s)
    {
    1) return sign+s; // так нельзя
    2) return string(s1)+s; //тоже нельзя
    3) return string(s1, 1)+s;// только так можно... но блин не сразу допетриваешь
    }

    ОтветитьУдалить
  11. Анонимный3/10/08 03:40

    Если очень хочется писать "Text"+c, лучше определить враппер над char'ом и обозвать его, скажем, ANSI_CHAR.
    Затем добавить глобальный оператор который принимает строку (на стеке) как lhs и конст-реф ANSI_CHAR как rhs.

    Тогда код string str="Text"+c; превратится в создание строки с текстом "Text" и вызов нашего оператора +.

    А использование голых си-строк запретить в кодинг стандарте и жестоко бить нарушителей :)

    ОтветитьУдалить
  12. Анонимный3/10/08 03:45

    правда, вышеописаный способ всё равно не поможет от str = "Text" + '0';

    ОтветитьУдалить
  13. Да... Увы, но потихоньку начинаем забывать, что С++ хоть и с двумя плюсами, но все равно С, и все, что свойственно С в нем остается.
    Попробуйте тот же код на C компиляторе (без всяких ++) - и увидите, что будет и Warning (а то и error в зависимости от конкретного экземпляра компилятора. Разбираем? OK!

    char c='0';
    /*
    Ну, тут собственно и писать не о чем... Инструкция полностью эквивалентна
    BYTE c=0x30; // Илиже 84 десятичное
    */
    string str="Text"+c;
    /*
    А тут уже интереснее "Text" вполне себе константная строка (которая кстати говоря заканчивается двоичным нулем).
    Тип string - хм... Это 100% char*. Да, я понимаю, очень хотелось, чтоб это был класс, но увы, при таком объявлении - это именно тип и здается мне, что компилятор его понял именно как char* (кстати, компилятор прав).
    Итак получается, что компилятор видит полны эквивалент кода
    char *str = (char *)("Text") + 0x30;
    Результат - ровно тот, который Вы описали... Т.е. указатель на байт по адресу (Начало строки "Text" + 48)
    */

    Более чем уверен, что Вы до этого додумались и сами, но комменты насторожили... Решил таки расписать... Прошу прощения если кого-нить ненароком обидел. Видит бог это не со зла...

    ОтветитьУдалить
  14. Все даже еще проще, в данном случае слева от знака равно стоит один тип, а справа два других типа.

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

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

    Так тоже работает:
    string str = "Text" + string(1, c);

    Хотя конструирование строки из char - менее доходчиво.

    ОтветитьУдалить
  15. Анонимный3/10/08 11:25

    Да нужно пользоваться всегда += с единственной переменной справа и никаких проблем не будет!

    ОтветитьУдалить
  16. Используйте стримы или boost::lexical_cast, что в принципе одно и то же :) Суммирование строк - дурная затея. А суммирование строк и char - это вообще некрасивей некуда.

    ОтветитьУдалить
  17. 2Сергей Кищенко:

    Используйте стримы или boost::lexical_cast, что в принципе одно и то же

    Это, возможно, неплохой совет если речь идет о работе на PC и не об играх.
    В играх обычно не используется boost, исключения еще не используются, потому что считается, что это тормозно очень. Нет, ну возможно где-то они и используются, но общие настроения такие. Мы же сейчас не на PC работаем, у нас тут скорости не те, да и работа с памятью довольно специфическая.

    ОтветитьУдалить
  18. Пакаль (делфи) рулит =)

    ОтветитьУдалить
  19. 2Алёна: А string в gamedev не считается чем-то тяжелым? и вобще STL. :)

    ОтветитьУдалить
  20. Анонимный3/10/08 15:59

    как на счет strncat("Text",'0',1) ?
    в итоге к "Text" будет присоединен '0', добавлен '\0' в конце и возвращен указатель на начало строки.
    Mario.

    ОтветитьУдалить
  21. 2Street:
    Пакаль (делфи) рулит =)

    Интересное замечание, особенно в сочетании с предыдущим комментарием. :-)
    Мне вообще не известны консоли, к которым есть компилятор Паскаля. С/C++, ну ассемблер свой еще. Вот и всё, пожалуй.

    2Андрей Валяев
    2Алёна: А string в gamedev не считается чем-то тяжелым? и вобще STL. :)

    Хех, я разглядела иронию :-). И тем не менее - считается. Но без STL живется совсем хреново. Поэтому он либо таки используется, но очень аккуратно, с пониманием того, как он устроен внутри. Иначе начнет вектор память переаллоцировать в самые неудачные моменты времени или еще чего...
    Если у компании есть время и деньги разрабатывается свой STL. Например я когда-то писала про EASTL. Вот тот string, который участвует в примере этого поста, это не STL'евский string на самом деле. Правда тут это не важно, потому что в такой ситуации они ведут себя одинаково.

    ОтветитьУдалить
  22. 2Анонимный:
    как на счет strncat("Text",'0',1) ?
    в итоге к "Text" будет присоединен '0', добавлен '\0' в конце и возвращен указатель на начало строки.
    Mario.


    Да тут много возможных вариантов разруливания ситуации, с явным вызовом конструктора string уже был вариант.

    ОтветитьУдалить
  23. Анонимный3/10/08 17:16

    Извините за нескромный вопрос. А зачем такой "загадочный" код может понадобиться?

    ОтветитьУдалить
  24. 2Анонимный:
    Извините за нескромный вопрос. А зачем такой "загадочный" код может понадобиться?

    В упрощенном виде ситуация такая: считывание массива значений из текстового конфига.

    ОтветитьУдалить
  25. Анонимный3/10/08 17:54

    На мой взгляд с таким кодом лучше бороться "архитектурным" способом а не программным :) Т.е. никаких текстовых конфигов и парсинга в игровом коде. А во время сборки игры использовать потоки или что-нибудь еще высокоуровневое и потенциально устойчивое к ошибкам.

    Предложу свой кривой вариант
    string s( "Text_" );
    *s.rbegin() = '1';

    ОтветитьУдалить
  26. 2Алёна:
    да уж случай со строкой нечастый.
    Boost конечно лучше применять с умом. Некоторые его вещи вполне подойдут для игр boost::shared_ptr, boost::any. А boost::lexical_cast полный тормоз. лучше snprintf пока-еще ничего нет, и быстро и переносимо. Со своим Wraper'ом уходит и последний минус snprintf - отсутствие проверки типов().
    STL просто незаменим но тут спорный вопрос вот в Crysis используется STL Port а в Unreal свои контейнеры. Я видел коммерческий проект где написаны свои контейнеры, все настолько убого, тормознуто и с кучей ошибок, что без слез смотреть нельзя, хотя автор кода человек с законченными проектами и с большим опытом. К примеру в своей строке утечка памяти. В Сортировка пузырьком и в Critical Time коде и везде. "STL там не применяется по историческим причинам"

    ОтветитьУдалить
  27. = считывание массива значений из текстового конфигаига =

    Не сильно в теме, но - а как же считывают значения из текстового конфига в линуксе? Неужели нет готовых решений?

    ОтветитьУдалить
  28. Анонимный6/10/08 10:06

    за-то можно так извратиться

    char *s = "qweqweqwe";
    int a=1;
    cout << a[s] ;

    ОтветитьУдалить
  29. Анонимный6/10/08 13:27

    Яркий пример точек следования:

    решение всегда проще
    char c='0';
    std::string str="Text";
    str =str +c;
    std::cout<< str;

    ОтветитьУдалить
  30. > Вот тот string, который участвует в примере этого поста, это не STL'евский string на самом деле. Правда тут это не важно, потому что в такой ситуации они ведут себя одинаково.

    Так в этой ситуации же неважно, одинаково ведут или нет, и вообще string не причём - происходит-то всё в правой части выражения, и никак не зависит от левой. Алёна, Вы же наверняка это прекрасно понимаете, зачем же других путать? Теперь некоторые могут подумать - фу, какая гадость этот ваш string, а он вроде и не виноват ни разу :)

    ОтветитьУдалить
  31. Анонимный7/10/08 00:13

    Мне вообще не известны консоли, к которым есть компилятор Паскаля.вообще gcc есть под некоторые консоли. под wii и ds — 100%. поэтому при большом желании можно и на паскале, и на фортране, и наверное даже на аде :)

    ОтветитьУдалить
  32. 2n2s:
    Так в этой ситуации же неважно, одинаково ведут или нет, и вообще string не причём - происходит-то всё в правой части выражения, и никак не зависит от левой. Алёна, Вы же наверняка это прекрасно понимаете, зачем же других путать?

    Тут важно, что конструктор string'а все так же принимает char*. Ну и то, что любые варианты решения, которые работают с STL'евским string'ом будут работать и тут.

    ОтветитьУдалить
  33. Алёна,
    Не понимаю, "всё так же принимает char*" - это в каком смысле?

    Выражение "Text"+c имеет тип const char*, так же как и сам "Text". Поэтому если str="Text" работает, то и поведение str="Text"+c будет однозначным. Разве нет?

    ОтветитьУдалить
  34. Что-то переливаем из пустого в порожнее.

    Поведение char *, который прочно ассоциируется со строкой, при сложении для программиста C++ кажется неестественным.

    Програмисты си легко видят здесь ошибку, потому что в си нет operator +. А программист C++ начинает сразу задумываться, что куда к чему приводится для сложения, и какое сложение здесь может быть использовано. :)

    Поэтому лучше использовать string сразу, чтобы меньше думать. :)

    ОтветитьУдалить
  35. 2Сергей Кищенко:
    boost-вские библиотеки очень хороши для прототипирования, но потом надо используемые классы/функции переписывать - проверено на опыте. lexical_cast очень тормозной. boost::any - удобный класс, но я после замены его на union получил почти 25% повышение производительности...

    ОтветитьУдалить
  36. 2 n2s:
    Поэтому если str="Text" работает, то и поведение str="Text"+c будет однозначным. Разве нет?

    Ну мало ли какой у нас string и чем он отличается от стандартного. Может, он имеет ограничение на длину в 5 символов или какие-нибудь другие экзотические ограничения.

    ОтветитьУдалить
  37. 2zg:
    вообще gcc есть под некоторые консоли. под wii и ds — 100%.

    Искала - не нашла. Можешь дать ссылку?

    ОтветитьУдалить
  38. Многие пишут что Сишник бы такое не написал :) или те кто раньше писал и пишет на ассемблере + си.... ибо для них как и для меня собственно оно выглядит иначе.... что такое строка-константа "Text" - это число обычно число ... что такое '0' - тоже число ... и не более обычные числа... их сложить можно ... при этом будет новое число... но дальше мы уже работаем с этим числом как с адресом на строку ... и конечно же получается что адрес на строку указывает вовсе не туда куда якобы хотелось :) Си++ развращает программистов ИМХО

    ОтветитьУдалить
  39. Анонимный9/10/08 11:44

    Между прочим, даже g++ -Wall не выдает предупреждений, ибо формально это совершенно правильный код: справа работает арифметика указателей...

    ОтветитьУдалить
  40. 2Алена: gcc работает везде! :)

    Может эта ссылка окажется полезной?

    ОтветитьУдалить
  41. 2saabeilin: С точки зрения компилятора код правильный. но с точки зрения логики - нет. И компилятор мог бы предупретить по хорошему.

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

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

    Здесь почти та же ситуация.

    ОтветитьУдалить
  42. Анонимный9/10/08 12:31

    2 Андрей Валяев: согласен, почему и был весьма удивлен отсутствием предупреждений (gcc 4.1.2, но подозревая что и 4.3 поведет себя аналогично, нет под рукой)

    ОтветитьУдалить
  43. Анонимный11/10/08 01:59

    Искала - не нашла. Можешь дать ссылку?да как бы и не секрет: devkitpro.org
    это уже собранное с либами и т.д.
    а так в gcc таргеты powerpc и arm присутствуют официально: http://gcc.gnu.org/install/specific.html

    ОтветитьУдалить
  44. >> А string в gamedev не считается чем-то тяжелым? и вобще STL. :)
    >Хех, я разглядела иронию :-). И тем не менее - считается. Но без STL живется совсем хреново.

    Я вот работаю в проекте, в котором нельзя ни STL, ни исключений. Есть, правда, замена такая, в виде набора контейнеров собственного авторства. Строки есть юникодовые, векторы, мапы. Не STL, конечно, но жить можно.

    А классы строк, вон вообще все кому не лень пишут. Бери любой да пользуйся.

    ОтветитьУдалить
  45. Анонимный11/10/08 15:50

    Мде, прочитал комментарии на простой в общем случай. Кстати, кто еще хорошо помнит Builder C++ версии 3.5, тот который под DOS :) такие ошибки не допускает, с другой стороны подобные вещи сразу в глаза могут и не бросаться. Да и вообще, не ошибается только тот, кто ничего не делает.
    А чтобы такого не было нужно использовать managed код :). При работе со строго типитизированными классами таких проблем не будет :)
    Кстати, а если выставить повыше уровень предупреждений Warning - неужели компилятор не ругнется? (правда в этом случае он будет ругаться на все библиотеки Microsoft, но это легко обходится)

    ОтветитьУдалить
  46. 2Clevelus:
    Кстати, а если выставить повыше уровень предупреждений Warning - неужели компилятор не ругнется?

    У меня сейчас под рукой только VC++7.1. Проверила в нем - не ругается.

    ОтветитьУдалить
  47. 2zg:

    да как бы и не секрет: devkitpro.org
    это уже собранное с либами и т.д.
    а так в gcc таргеты powerpc и arm присутствуют официально: http://gcc.gnu.org/install/specific.html


    угу, спасибо

    ОтветитьУдалить
  48. Хе-хе. Прикольно.

    std::string str = "Text" + c;

    В чём тут ошибка? Просто в понимании того, что определяет тип операции.
    1) компилятор анализирует типы и обнаруживает
    string = const char* + char
    2) что получаем теперь при вычислении?
    В первую очередь у нас будет вызван operator+ для встроенного типа const char*...
    И только потом он вызовет копирующий конструктор и создаст объект типа std::string.

    Это не ошибка программиста, развращённого C++, нет. Это особенность ООП-подхода конкретно в языке: вызываемый оператор определяется не возвращаемым типом, а левым операндом. И всего-то :)

    Варианты:

    #include <iostream>

    int main ()
    {
     { //исходный вариант
      char c = '0';
      std::string str = "Text" + c;
      
      std::cout << str << std::endl;
     }

     { //предпочитаю так
      char c = '0';
      std::string str = "Text";
      str += c;
      
      std::cout << str << std::endl;
     }

     { //но можно и так
      char c = '0';
      std::string str = std::string("Text") + c;
      
      std::cout << str << std::endl;
     }

     return 0;
    }

    ОтветитьУдалить
  49. Забавно!
    недавно был очень похожий случай.
    есть функция:
    string addsign(char sign, const string & s)
    {
    1) return sign+s; // так нельзя
    2) return string(s1)+s; //тоже нельзя
    3) return string(s1, 1)+s;// только так можно... но блин не сразу допетриваешь
    }

    Решил это сам проверить, на компиляторе gcc 3.4.2.
    вот такая программулинка:

    #include <string>
    #include <stdio.h>
    using namespace std;
    string addsign(char s1, const string & s)
    {
    //1)return s1+s; // так нельзя
    //2)return string(s1)+s; //тоже нельзя
    //3)return string(s1, 1)+s;// только так можно... но блин не сразу допетриваешь
    //4)return string(&s1, 1)+s;
    }
    int main(int n, char **args)
    {
    string text = "1.9876";
    string res = addsign('-', text);
    puts(res.c_str());
    return 0;
    }

    выдала мне срвсем другое:
    первый вариант (как "нельзя") выдал правильный результат: -1.9876, второй вариант - ошибку компиляции, а третий(который "только так и можно") дал нечто невразумительное - кучу смеющихся рожиц и в конце-число 1.9876. Мне даже непонятно, как у автора сообщения получился нормальный результат.
    Последний (4-ый, "мой" вариант) дал тоже верный результат: -1.9876

    ОтветитьУдалить
  50. Анонимный14/10/08 09:53

    Малость ошибка закралась в Ваши рассуждения :)

    string addsign(char s1, const string & s)
    {
    //1)return s1+s; // так нельзя

    тут то как раз все можно, в STL есть оператор +, который в качестве одного из операндов принимает char, второго - string

    //2)return string(s1)+s; //тоже нельзя

    ага, компилятор ругается, нет такого конструктора у string

    //3)return string(s1, 1)+s;// только так можно... но блин не сразу допетриваешь

    все замечательно, только параметры у конструктора перепутали, в итоге создается строка из s1 символов с кодом 1 (ага, тех самых, смеющися рожиц :) )

    //4)return string(&s1, 1)+s;
    }

    тут тоже все хорошо - string создается по одному символу из строки &s1

    проверялось на VC8(SP1)

    ОтветитьУдалить
  51. Анонимный16/10/08 21:39

    вас stl'щиков в утиль давно пора.
    если лень писать свои классы, чтобы были шустрые и расчитанные на мультипроцессор, пишите на C#.
    микрософт так старались appdev от c++ отделить, а вы все ещё кипятите.

    оставьте c++ для системных прогеров :)

    ОтветитьУдалить
  52. Анонимный16/10/08 22:02

    да btw, советую почитать ISO стандарт c++.
    по стандарту код

    char c='0';
    string str="Text"+c;

    это

    class char c = __int32(0x30)
    class string str = __int32(char*("Text")) + __int32(c);

    т.е все приводится к классу int перед выполением стандартных операторов, незнать это тяжкий грех!
    например

    longlong n;
    long t = 0;
    n = t -1;

    что будет в n? -1? ага три раза оно там будет. там будет 0xffffffff
    что для longlong не -1.
    на х86 это нормально, а вот х64... таких программистов надо растреливать.

    char c='0';
    string str = string("Text")+c;

    приведение типов спасет этот мир.

    ОтветитьУдалить
  53. Помоему Алёне давно пора закрывать эту дискуссию, а комментирующим не мешает читать впередиидущие комментарии.

    ОтветитьУдалить
  54. 2Андрей Валяев:

    Помоему Алёне давно пора закрывать эту дискуссию, а комментирующим не мешает читать впередиидущие комментарии.

    Да уж, а то разговор пошел по кругу.
    И 53 комментария на две строки кода - это как-то многовато.
    Давайте завершим на этом.

    ОтветитьУдалить
  55. Анонимный19/11/08 12:52

    Тут упоминали php, а в нём кокатенация строк происходит через оператор "точка".

    $a= "10Text".1; // $a= "10Text1";
    $b= "10Text"+1; // $b= 10+1;

    Через + происходит сложение чисел.

    ОтветитьУдалить
  56. Анонимный29/11/08 02:02

    Проблема ясна. Символ должен быть символом, а байт - байтом )

    Андрей Валяев пишет...
    string str = string("Text") + c;
    Создаем временную переменную (вызываем конструктор), вызываем перегруженный operator+, вызываем перегруженный operator=, удаляем временный объект (вызываем деструктор)...

    Mario пишет...
    как на счет strncat("Text",'0',1) ?
    в итоге к "Text" будет присоединен '0', добавлен '\0' в конце и возвращен указатель на начало строки.

    Во-первых, не strncat("Text",'0',1), а strncat("Text","0",1)...
    Во-вторых, запись в const-секцию (которая read-only)? Тогда вызови для начала VirtualProtect... И убедись, что после "Text" не лежит "Text2".

    coff

    ОтветитьУдалить
  57. Анонимный8/12/08 01:03

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

    ОтветитьУдалить