Строки C ++ - Как избежать получения недопустимого указателя?

17

В нашем коде на C ++ у нас есть собственный класс строк (по старым причинам). Он поддерживает метод c_str() , как std::string . Я заметил, что многие разработчики используют его неправильно. Я уменьшил проблему до следующей строки:

 const char* x = std::string("abc").c_str();

Этот, казалось бы, невинный код довольно опасен в том смысле, что деструктор на std::string вызывается сразу после вызова c_str() . В результате вы удерживаете указатель на выделенную ячейку памяти.

Вот еще один пример:

  std::string x("abc");
  const char* y = x.substr(0,1).c_str();

Здесь также мы используем указатель на выделенное местоположение.

Эти проблемы нелегко найти во время тестирования, поскольку в памяти по-прежнему содержатся достоверные данные (хотя сама ячейка памяти недействительна).

Мне интересно, есть ли у вас какие-либо предложения относительно того, как я могу изменить определение класса / метода, чтобы разработчики никогда не могли совершить такую ​​ошибку.

    
задан Peter 27.05.2014 в 02:26
источник
  • Здесь есть похожие обсуждения: stackoverflow.com/questions/10540157/... –  Fantastic Mr Fox 27.05.2014 в 02:33
  • Решение для разработчиков тщательно подумать перед вызовом c_str () и прекратить использование char * в их коде. Возможно, вы могли бы рассмотреть, почему они используют указатели в своем коде, а не просто используют строки, и советуют другие решения. –  M.M 27.05.2014 в 02:50

6 ответов

9

Современная часть кода не должна обрабатывать необработанные указатели. Вызовите c_str только при предоставлении аргумента устаревшей функции, которая принимает const char* . Как:

legacy_print(x.substr(0,1).c_str())

Почему вы хотите создать локальную переменную типа const char* ? Даже если вы пишете версию для копирования c_str_copy() , вы просто получите больше головной боли, потому что теперь клиентский код отвечает за удаление результирующего указателя.

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

    
ответ дан isarandi 27.05.2014 в 02:45
источник
7

В базовом случае вы можете добавить квалификатор ref на «этот» объект, чтобы убедиться, что .c_str () никогда не немедленно вызывает временный. Конечно, это не может помешать им хранить в переменной, которая оставляет область до того, как будет сделан указатель.

const char *c_str() & { return ...; }

Но решение большего размера заключается в замене всех функций на принятие «const char *» в вашей кодовой базе функциями, которые берут один из ваших строковых классов (по крайней мере, вам нужно два: владеющая строка и заимствованная slice) - и убедитесь, что ни один из ваших строковых классов не умеет не может быть неявным образом создан из «const char *».

    
ответ дан o11c 27.05.2014 в 02:46
источник
  • Примечание: требуется C ++ 11 –  M.M 27.05.2014 в 02:51
  • И требует предоставления удаленного && варианта, если предоставляется const & variant. И VS2013 не справляется с этим. Но мне это нравится для предотвращения этого во время компиляции. –  Michael Urman 27.05.2014 в 03:23
  • Извините мое невежество, но как бы вы это добавили, не изменяя сам файл заголовка строки? –  LordAro 27.05.2014 в 11:02
  • @LordAro И почему бы не изменить его? OP утверждает, что использует собственный строковый класс, а не std :: string. –  Angew 27.05.2014 в 13:37
6

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

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

(Как вы уже упоминали, в настоящий момент ошибки остаются незамеченными, потому что по большей части код будет успешно работать с недопустимой памятью.)

    
ответ дан CaptainCodeman 27.05.2014 в 09:08
источник
  • Я думаю, вместо этого я бы заменил его на «#» или что-то еще необычное, что делает его несколько легче обнаруживать в журналах. Хорошая идея. –  Mooing Duck 18.06.2014 в 01:16
3

Рассмотрите возможность использования Valgrind или Electric Fence для проверки вашего кода. Любой из этих инструментов должен тривиально и сразу найти эти ошибки.

    
ответ дан Steger 27.05.2014 в 03:53
источник
1

Я не уверен, что вы можете очень многого сделать с людьми, использующими вашу библиотеку, если вы их предупреждаете. Рассмотрим фактическую библиотеку stl string . Если я сделаю это:

const char * lala = std::string("lala").c_str();
std::cout << lala << std::endl;
const char * lala2 = std::string("lalb").c_str();
std::cout << lala << std::endl;
std::cout << lala2 << std::endl;

Я в основном создаю неопределенное поведение. В случае, когда я запускаю его на ideone.com, я получаю следующий вывод:

lala
lalb
lalb

Таким образом, память первоначального lala была перезаписана. Я бы просто очень четко дал понять пользователю в документации, что такой вид кодирования является плохой практикой.     

ответ дан Fantastic Mr Fox 27.05.2014 в 02:45
источник
1

Вы можете удалить функцию c_str () и вместо этого предоставить функцию, которая принимает ссылку на уже созданный пустой интеллектуальный указатель, который сбрасывает значение интеллектуального указателя на новую копию строки. Это заставит пользователя создать не временный объект, который они могли бы использовать для получения исходной строки c, и он будет разрушен и освободится память при выходе из области метода.

Это предполагает, что ваша библиотека и ее пользователи будут использовать одну и ту же кучу.

ИЗМЕНИТЬ

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

    
ответ дан mclaassen 27.05.2014 в 03:09
источник
  • Приведите пример того, как вы удаляете функцию c_str? –  texasbruce 27.05.2014 в 03:19
  • Я думал, что он хочет изменить определение класса, я думаю, что функция c_str - это функция его собственного класса, а не фактическая std :: string c_str. –  mclaassen 27.05.2014 в 03:20