Каков правильный способ обработки строк char *?

114
7

У меня есть сторонняя библиотека, которая использует char * (не const) в качестве заполнителя для строковых значений. Каков правильный и безопасный способ присвоения значений этим типам данных? У меня есть следующий тестовый тест, который использует мой собственный класс таймера для измерения времени выполнения:


#include "string.h"
#include <iostream>
#include <sj/timer_chrono.hpp>

using namespace std;

int main()
{
sj::timer_chrono sw;

int iterations = 1e7;

// first method gives compiler warning:
// conversion from string literal to 'char *' is deprecated [-Wdeprecated-writable-strings]
cout << "creating c-strings unsafe(?) way..." << endl;
sw.start();
for (int i = 0; i < iterations; ++i)
{
char* str = "teststring";
}
sw.stop();
cout << sw.elapsed_ns() / (double)iterations << " ns" << endl;

cout << "creating c-strings safe(?) way..." << endl;
sw.start();
for (int i = 0; i < iterations; ++i)
{
char* str = new char[strlen("teststr")];
strcpy(str, "teststring");
}
sw.stop();
cout << sw.elapsed_ns() / (double)iterations << " ns" << endl;

return 0;

}


Вывод:


creating c-strings unsafe(?) way...
1.9164 ns
creating c-strings safe(?) way...
31.7406 ns

В то время как "безопасный" способ избавиться от предупреждения компилятора, он делает код примерно в 15-20 раз медленнее в соответствии с этим эталоном (1,9 наносекунды на итерацию против 31,7 наносекунд на итерацию). Каков правильный способ и что опасно для этого "устаревшего" способа?

спросил(а) 2021-01-19T16:47:07+03:00 2 месяца, 3 недели назад
1
Решение
144

Стандарт С++ понятен:


Обычный строковый литерал имеет тип "array of n const char" (раздел 2.14.5.8 на С++ 11).


и


Эффект попытки изменить строковый литерал undefined (раздел 2.14.5.12 на С++ 11).


Для строки, известной во время компиляции, безопасным способом получения non-const char* является этот


char literal[] = "teststring";

вы можете безопасно


char* ptr = literal;

Если во время компиляции вы не знаете строку, но знаете ее длину, вы можете использовать массив:


char str[STR_LENGTH + 1];

Если вы не знаете длину, вам нужно будет использовать динамическое распределение. Убедитесь, что вы освободили память, когда строки больше не нужны.


Это будет работать только в том случае, если API не получит права собственности на char*, который вы передаете.


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


char literal[] = "test";

создаст локальный массив из 5 символов с памятью automatinc (это означает, что переменная будет уничтожена, когда выполнение покинет область, в которой объявлена ​​переменная) и инициализирует каждый символ в массиве символами 't', 'e ',' s ',' t 'и'\0 '.


Вы можете впоследствии отредактировать эти символы: literal[2] = 'x';


Если вы пишете это:


char* str1 = "test";
char* str2 = "test";

тогда, в зависимости от компилятора, str1 и str2 могут быть одного и того же значения (т.е. указывать на одну и ту же строку).


( "Независимо от того, являются ли все строковые литералы различными (то есть, хранятся в объектах, не поддерживающих перекрывание), определяется реализацией". в разделе 2.14.5.12 стандарта С++)


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


Они также, в действительности, типа const char*, поэтому эта строка:


char * str = "test";


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

ответил(а) 2021-01-19T16:47:07+03:00 2 месяца, 3 недели назад
106

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


Ваш "безопасный" способ утечки памяти и довольно ужасающий.


Обычно у вас есть здравый API C, который принимает const char *, поэтому вы можете использовать правильный безопасный способ в С++, т.е. std::string и его метод c_str().

Если ваш C API предполагает принадлежность к строке, ваш "безопасный путь" имеет еще один недостаток: вы не можете смешивать new[] и free(), передавая память, выделенную с помощью оператора С++ new[], в C API, который Ожидается, что вызов free() на нем не разрешен. Если API C не хочет вызывать free() позже в строке, должно быть хорошо использовать new[] на стороне С++.


Кроме того, это странная смесь С++ и C.

ответил(а) 2021-01-19T16:47:07+03:00 2 месяца, 3 недели назад
97

У вас, похоже, есть фундаментальное недоразумение о C-строках здесь.


cout << "creating c-strings unsafe(?) way..." << endl;
sw.start();
for (int i = 0; i < iterations; ++i)
{
char* str = "teststring";
}

Здесь вы просто назначаете указатель на константу строкового литерала. В C и С++ строковые литералы имеют тип char[N], и вы можете назначить указатель на массив строковых литералов из-за массива "decay". (Тем не менее, он устарел, чтобы назначить неконстантный указатель на строковый литерал.)


Но назначение указателя на строковый литерал не может быть тем, что вы хотите сделать. Ваш API ожидает неконстантную строку. Строковые литералы const.


Каков правильный и безопасный способ назначения значений этим [ char * строкам]?



Нет никакого общего ответа на этот вопрос. Всякий раз, когда вы работаете со строками C (или указателями в целом), вам нужно иметь дело с концепцией собственности. С++ позаботится об этом для вас автоматически с помощью std::string. Внутри std::string имеет указатель на массив char*, но он управляет памятью для вас, поэтому вам не нужно заботиться об этом. Но когда вы используете необработанные C-строки, вам нужно подумать над управлением памятью.


Как вы управляете памятью, зависит от того, что вы делаете с вашей программой. Если вы выделяете C-строку с помощью new[], вам необходимо освободить ее с помощью delete[]. Если вы выделите его с помощью malloc, вы должны освободить его с помощью free(). Хорошим решением для работы с C-строками в С++ является использование умного указателя, который берет на себя ответственность за выделенную строку C. (Но вам нужно использовать deleter, который освобождает память с помощью delete[]). Или вы можете просто использовать std::vector<char>. Как всегда, не забудьте выделить место для завершающего нуля char.


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

ответил(а) 2021-01-19T16:47:07+03:00 2 месяца, 3 недели назад
Ваш ответ
Введите минимум 50 символов
Чтобы , пожалуйста,
Выберите тему жалобы:

Другая проблема