Было бы неплохо сделать серию статей, в которых описывались различные не очевидные «особенности» языков программирования. Во-первых, «предупреждён — значит, вооружён», во-вторых, знание их позволяет глубже понимать язык и объяснить, в случае чего, чем они опасны. Даже если в своём собственном коде такие конструкции не используются, с этими ловушками можно встретиться при разборе чужого кода или работая в команде.
Итак, пусть будет С++ и тип char
.
Основные источники проблем это:
- отсутствие в языке специализированного целочисленного типа для 8-битных величин. Из-за этого char’у приходится брать на себя роль byte;
- наличие в языке двух совершенно разных видов строк —
std::string
(«C++ строки») иconst char*
(С-строки, которые обязаны поддерживаться для совместимости).
Подробнее по первому пункту. Так как «родного типа» byte
нет, его конструируют через тип char
. В Стандарте сделано специальное уточнение, что типы char
, signed char
и unsigned char
— это три разных типа. Этого свойства нет у других целочисленных типов, к примеру, int
и signed int
— тождественные определения. Дополнительными граблями здесь выступает и то, что сам по себе тип char должен быть или знаковым или беззнаковым — это зависит от платформы (грубо говоря, от компилятора и от его набора ключей). Но при этом всё равно компилятор обязан различать их все друг от друга.
Соответственно, такое вот определение:
void foo(char c);
void foo(signed char c);
void foo(unsigned char c);
объявляет три разных функции. Проблему, где происходит «смешивание» различных свойств этих типов, можно показать, например, таким куском кода:
#include <iostream>
#include <stdint.h>
int main()
{
uint8_t b = 42;
std::cout << b << std::endl; // выводит символ *, а не число 42.
}
Резюмируя: «байтовый» целочисленный тип в определённых ситуациях может проявить свою «символьную» сущность с неочевидными последствиями.
Перейдём ко второму пункту. В языке С нет специального типа для работы со строками. По соглашению считается, что если у нас есть указатель char*
(или const char*
), то это, скорее всего и есть строка, и его можно передавать соответствующим функциям. Plain C допускает даже такие удивительные вещи как, например, такое:
int main(void)
{
char* ptr = "hello"; // допускается в C, неконстантный указатель на строку
ptr[1] = 'q'; // получаем неопределенное поведение
"abcd"[1] = '2'; // то есть компилятор всё-таки знает, что строки у нас - read-only, менять их нельзя и закономерно ругается на вот эту конструкцию
return 0;
}
Хорошей новостью является то, что в С++ эту «фичу» не перенесли.
Но остальные никуда не делись. К примеру, string literal допускает наличие нулевых символов внутри себя (например, "abc123"
), а функции, которые предназначены для работы с ними (strlen
и т.п) такие строки не поддерживают. То есть, из-за решения, что «все строки — это последовательность ненулевых символов, заканчивающаяся нулём» сразу получили ситуацию, что не со всеми строками стало возможно работать и забавные эффекты типа сложность O(n) для такой операции как «получить длину данной строки».
Далее, так как компилятор автоматически добавляет ''
для всех string literal, то это приводит к таким следствиям:
char[] str1 = "1234"; // размер этого массива 5 символов, а не 4
char[4] str2 = "1234"; // ошибка, компилятор не даст создать такой массив
char[4] str3 = {'1', '2', '3', '4'}; // ...хотя это легко обойти
Казалось бы, всё хорошо. Но последняя строка содержит скрытые грабли — она выглядит как обычный char*
, то есть её можно передать в puts
, strlen
и т.п. и получить undefined behaviour.
Резюмируя: по возможности, избегайте использования строк в своих C++ программах в «старом» Си-стиле.
Автор: qehgt