Ловушки языка С++

в 12:52, , рубрики: bugs, c++, pitfalls, метки: , ,

Было бы неплохо сделать серию статей, в которых описывались различные не очевидные «особенности» языков программирования. Во-первых, «предупреждён — значит, вооружён», во-вторых, знание их позволяет глубже понимать язык и объяснить, в случае чего, чем они опасны. Даже если в своём собственном коде такие конструкции не используются, с этими ловушками можно встретиться при разборе чужого кода или работая в команде.

Итак, пусть будет С++ и тип 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

Поделиться

* - обязательные к заполнению поля