7 ответов
Строка с нулевым символом в конце - это непрерывная последовательность символов, последний из которых имеет двоичную битовую комбинацию всех нулей. Я не уверен, что вы подразумеваете под "обычной строкой", но если вы имеете в виду std::string
, затем std::string
не требуется ( до C++ 11) быть смежным и не обязательно иметь терминатор. Также std::string
Строковые данные всегда выделяются и управляются std::string
объект, который содержит его; для строки с нулевым символом в конце такого контейнера нет, и вы обычно обращаетесь к таким строкам и управляете ими, используя голые указатели.
Все это действительно должно быть освещено в любом приличном учебнике по C++ - я рекомендую приобрести Accelerated C++, один из лучших.
"Строка" на самом деле просто массив char
s; строка с нулевым символом в конце - это строка с нулевым символом '\0'
отмечает конец строки (не обязательно конец массива). Все строки в коде (разделены двойными кавычками ""
) автоматически заканчиваются на ноль компилятором.
Так, например, "hi"
такой же как {'h', 'i', '\0'}
,
Существует два основных способа представления строки:
1) Последовательность символов с нулевым (нулевым) символом ASCII, 0, в конце. Вы можете сказать, как долго это происходит, ища терминатор. Это называется строкой с нулевым символом в конце или иногда с нулевым символом в конце.
2) Последовательность символов плюс отдельное поле (либо целочисленная длина, либо указатель на конец строки), чтобы сообщить вам, как долго это будет.
Я не уверен насчет "обычной строки", но часто случается так, что, говоря о конкретном языке, слово "строка" используется для обозначения стандартного представления для этого языка. Таким образом, в Java java.lang.String является строкой типа 2, так что именно это и означает "строка". В C "строка", вероятно, означает строку типа 1. Стандарт достаточно многословен, чтобы быть точным, но люди всегда хотят не учитывать то, что "очевидно".
В C++, к сожалению, оба типа являются стандартными. std::string является строкой типа 2 [*], но стандартные библиотечные функции, унаследованные от C, работают со строками типа 1.
[*] На самом деле std:: string часто реализуется как массив символов с отдельным полем длины и нулевым терминатором. Это так, что c_str()
Функция может быть реализована без необходимости копировать или перераспределять строковые данные. Я не могу вспомнить, правомерно ли реализовывать std:: string без сохранения поля длины: вопрос в том, какие гарантии сложности требуются стандартом. Для контейнеров в целом size()
рекомендуется быть O(1), но на самом деле не обязательно. Поэтому, даже если это допустимо, реализация std:: string, которая использует только nul-терминаторы, будет удивительной.
'\0'
является символом ASCII с кодом 0, нулевым терминатором, нулевым символом, NUL. В языке Си это зарезервированный символ, используемый для обозначения конца строки. Многие стандартные функции, такие как strcpy, strlen, strcmp и другие, полагаются на это. В противном случае, если не было NUL, должен был использоваться другой способ сообщить об окончании строки:
Это позволяет строке иметь любую длину с издержками только на один байт; альтернатива хранения счетчика требует либо ограничения длины строки в 255, либо служебных данных более одного байта.
из википедии
C++ std::string
следует этому другому соглашению, и его данные представлены структурой, называемой _Rep
:
// _Rep: string representation
// Invariants:
// 1. String really contains _M_length + 1 characters: due to 21.3.4
// must be kept null-terminated.
// 2. _M_capacity >= _M_length
// Allocated memory is always (_M_capacity + 1) * sizeof(_CharT).
// 3. _M_refcount has three states:
// -1: leaked, one reference, no ref-copies allowed, non-const.
// 0: one reference, non-const.
// n>0: n + 1 references, operations require a lock, const.
// 4. All fields==0 is an empty string, given the extra storage
// beyond-the-end for a null terminator; thus, the shared
// empty string representation needs no constructor.
struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};
struct _Rep : _Rep_base
{
// Types:
typedef typename _Alloc::template rebind<char>::other _Raw_bytes_alloc;
// (Public) Data members:
// The maximum number of individual char_type elements of an
// individual string is determined by _S_max_size. This is the
// value that will be returned by max_size(). (Whereas npos
// is the maximum number of bytes the allocator can allocate.)
// If one was to divvy up the theoretical largest size string,
// with a terminating character and m _CharT elements, it'd
// look like this:
// npos = sizeof(_Rep) + (m * sizeof(_CharT)) + sizeof(_CharT)
// Solving for m:
// m = ((npos - sizeof(_Rep))/sizeof(CharT)) - 1
// In addition, this implementation quarters this amount.
static const size_type _S_max_size;
static const _CharT _S_terminal;
// The following storage is init'd to 0 by the linker, resulting
// (carefully) in an empty string with one reference.
static size_type _S_empty_rep_storage[];
static _Rep&
_S_empty_rep()
{
// NB: Mild hack to avoid strict-aliasing warnings. Note that
// _S_empty_rep_storage is never modified and the punning should
// be reasonably safe in this case.
void* __p = reinterpret_cast<void*>(&_S_empty_rep_storage);
return *reinterpret_cast<_Rep*>(__p);
}
bool
_M_is_leaked() const
{ return this->_M_refcount < 0; }
bool
_M_is_shared() const
{ return this->_M_refcount > 0; }
void
_M_set_leaked()
{ this->_M_refcount = -1; }
void
_M_set_sharable()
{ this->_M_refcount = 0; }
void
_M_set_length_and_sharable(size_type __n)
{
#ifndef _GLIBCXX_FULLY_DYNAMIC_STRING
if (__builtin_expect(this != &_S_empty_rep(), false))
#endif
{
this->_M_set_sharable(); // One reference.
this->_M_length = __n;
traits_type::assign(this->_M_refdata()[__n], _S_terminal);
// grrr. (per 21.3.4)
// You cannot leave those LWG people alone for a second.
}
}
_CharT*
_M_refdata() throw()
{ return reinterpret_cast<_CharT*>(this + 1); }
_CharT*
_M_grab(const _Alloc& __alloc1, const _Alloc& __alloc2)
{
return (!_M_is_leaked() && __alloc1 == __alloc2)
? _M_refcopy() : _M_clone(__alloc1);
}
// Create & Destroy
static _Rep*
_S_create(size_type, size_type, const _Alloc&);
void
_M_dispose(const _Alloc& __a)
{
#ifndef _GLIBCXX_FULLY_DYNAMIC_STRING
if (__builtin_expect(this != &_S_empty_rep(), false))
#endif
if (__gnu_cxx::__exchange_and_add_dispatch(&this->_M_refcount,
-1) <= 0)
_M_destroy(__a);
} // XXX MT
void
_M_destroy(const _Alloc&) throw();
_CharT*
_M_refcopy() throw()
{
#ifndef _GLIBCXX_FULLY_DYNAMIC_STRING
if (__builtin_expect(this != &_S_empty_rep(), false))
#endif
__gnu_cxx::__atomic_add_dispatch(&this->_M_refcount, 1);
return _M_refdata();
} // XXX MT
_CharT*
_M_clone(const _Alloc&, size_type __res = 0);
};
Фактические данные могут быть получены с помощью:
_Rep* _M_rep() const
{ return &((reinterpret_cast<_Rep*> (_M_data()))[-1]); }
этот фрагмент кода приходит из файла basic_string.h
который на моей машине находится в usr/include/c++/4.4/bits/basic_string.h
Итак, как вы можете видеть, разница значительна.
Строка с нулевым символом в конце означает, что конец вашей строки определяется появлением нулевого символа (все биты равны нулю).
"Другие строки", например, должны хранить свою длину.
Строка с нулевым символом в конце - это собственный формат строки в C. Строковые литералы, например, реализованы как символ с нулевым символом в конце. В результате весь код (для начала C-библиотека) предполагает, что строки заканчиваются нулем.
Строка с нулевым символом в конце (c-string) является массивом символов, а последний элемент массива имеет значение 0x0. Std::string по сути является вектором в том смысле, что он является контейнером с автоматически изменяемым размером для значений. Ему не нужен нулевой терминатор, поскольку он должен отслеживать размер, чтобы знать, когда необходимо изменить размер.
Честно говоря, я предпочитаю c-strings, а не std, у них просто больше приложений в базовых библиотеках, с минимальным кодом и распределением, и из-за этого сложнее использовать.