Преобразования типов и безопасность типов
Этот документ определяет распространенные проблемы преобразования типов и описывает, как их можно избежать в коде C++.
При написании программы C++ важно убедиться, что она является типобезопасной. Это означает, что каждое значение переменной, аргумента функции и возвращаемого функции сохраняет приемлемый тип данных, и что операции, включающие значения различных типов", "имеют смысл" и не вызывают потери данных, неправильной интерпретации битовых шаблонов или повреждения памяти. Программа, которая никогда явно или неявно преобразовывает значения из одного типа в другой является типобезопасной по определению. Однако иногда требуются преобразования типов, даже небезопасные преобразования. Например, может потребоваться сохранить результат операции с плавающей запятой в переменной типа int
или передать значение в unsigned int
функцию, которая принимает значение signed int
. Оба примера иллюстрируют небезопасные преобразования, так как они могут привести к потере данных или повторной интерпретации значения.
Когда компилятор обнаруживает небезопасное преобразование, он выдает ошибку или предупреждение. Ошибка останавливает компиляцию; Предупреждение позволяет продолжить компиляцию, но указывает на возможную ошибку в коде. Однако даже если программа компилируется без предупреждений, она по-прежнему может содержать код, который приводит к неявным преобразованиям типов, которые создают неправильные результаты. Ошибки типа также могут быть представлены явными преобразованиями или приведениями в коде.
Неявные преобразования типов
Если выражение содержит операнды различных встроенных типов и нет явных приведения, компилятор использует встроенные стандартные преобразования для преобразования одного из операндов таким образом, чтобы типы соответствовали. Компилятор пытается выполнить преобразования в четко определенной последовательности, пока не завершится успешно. Если выбранное преобразование является повышением, компилятор не выдает предупреждения. Если преобразование является сужающим, компилятор выдает предупреждение о возможной потере данных. Указывает, происходит ли фактическая потеря данных, зависит от фактических значений, но рекомендуется рассматривать это предупреждение как ошибку. Если используется определяемый пользователем тип, компилятор пытается использовать преобразования, указанные в определении класса. Если не удается найти допустимое преобразование, компилятор выдает ошибку и не компилирует программу. Дополнительные сведения о правилах, которые управляют стандартными преобразованиями, см. в разделе "Стандартные преобразования". Дополнительные сведения о пользовательских преобразованиях см. в разделе "Определяемые пользователем преобразования" (C++/CLI).
Расширение преобразований (повышение уровня)
При расширении преобразования значение в меньшей переменной назначается большей переменной без потери данных. Так как расширяющие преобразования всегда безопасны, компилятор выполняет их автоматически и не выдает предупреждения. Следующие преобразования расширяются.
С дт. | Кому |
---|---|
Любой signed или unsigned целочисленный тип, кроме long long или __int64 |
double |
bool или char |
Любой другой встроенный тип |
short или wchar_t |
int , long , long long |
int , long |
long long |
float |
double |
Сужение преобразований (приведение)
Компилятор выполняет неявно сужающие преобразования, но предупреждает о потенциальной потере данных. Примите эти предупреждения очень серьезно. Если вы уверены, что потеря данных не будет происходить, так как значения в большей переменной всегда будут соответствовать меньшей переменной, добавьте явный приведение, чтобы компилятор больше не выдает предупреждение. Если вы не уверены, что преобразование безопасно, добавьте в код какой-то вид среды выполнения проверка для обработки возможной потери данных, чтобы она не приводила к неправильному результату программы.
Любое преобразование из типа с плавающей запятой в целочисленный тип является сужающим преобразованием, так как дробная часть значения с плавающей запятой не карта и потеряна.
В следующем примере кода показаны некоторые неявные сужающие преобразования и предупреждения о проблемах компилятора.
int i = INT_MAX + 1; //warning C4307:'+':integral constant overflow
wchar_t wch = 'A'; //OK
char c = wch; // warning C4244:'initializing':conversion from 'wchar_t'
// to 'char', possible loss of data
unsigned char c2 = 0xfffe; //warning C4305:'initializing':truncation from
// 'int' to 'unsigned char'
int j = 1.9f; // warning C4244:'initializing':conversion from 'float' to
// 'int', possible loss of data
int k = 7.7; // warning C4244:'initializing':conversion from 'double' to
// 'int', possible loss of data
Подписанные — незаписанные преобразования
Подписанный целочисленный тип и его неподписанный аналог всегда одинаковый, но они отличаются в том, как битовый шаблон интерпретируется для преобразования значений. В следующем примере кода показано, что происходит, когда тот же битовый шаблон интерпретируется как подписанное значение и как неподписаемое значение. Битовый шаблон, хранящийся в обоих num
и num2
никогда не изменяется от того, что показано на предыдущем рисунке.
using namespace std;
unsigned short num = numeric_limits<unsigned short>::max(); // #include <limits>
short num2 = num;
cout << "unsigned val = " << num << " signed val = " << num2 << endl;
// Prints: "unsigned val = 65535 signed val = -1"
// Go the other way.
num2 = -1;
num = num2;
cout << "unsigned val = " << num << " signed val = " << num2 << endl;
// Prints: "unsigned val = 65535 signed val = -1"
Обратите внимание, что значения переосмыслены в обоих направлениях. Если программа создает нечетные результаты, в которых знак значения по-видимому, повернут из ожидаемого значения, найдите неявные преобразования между подписанными и неподписанными целочисленными типами. В следующем примере результат выражения (0 – 1) неявно преобразуется из int
unsigned int
того, когда он хранится.num
Это приводит к повторному интерпретации битового шаблона.
unsigned int u3 = 0 - 1;
cout << u3 << endl; // prints 4294967295
Компилятор не предупреждает о неявных преобразованиях между подписанными и неподписанными целочисленными типами. Поэтому рекомендуется избегать полностью подписанных преобразований в unsigned. Если вы не можете избежать их, добавьте среду выполнения проверка, чтобы определить, больше ли преобразованное значение больше или равно нулю и меньше или равно максимальному значению подписанного типа. Значения в этом диапазоне будут передаваться из входа в unsigned или из без знака на подписанный без повторного понимания.
Преобразования указателей
Во многих выражениях массив стилей C неявно преобразуется в указатель на первый элемент в массиве, а преобразования констант могут выполняться автоматически. Хотя это удобно, это также потенциально подвержено ошибкам. Например, следующий плохо разработанный пример кода кажется нечувствичным, но он компилирует и создает результат p. Во-первых, константный литерал строки help преобразуется char*
в тот, который указывает на первый элемент массива. Затем указатель увеличивается тремя элементами, чтобы он указывал на последний элемент p.
char* s = "Help" + 3;
Явные преобразования (приведение)
С помощью операции приведения можно указать компилятору преобразовать значение одного типа в другой тип. Компилятор вызовет ошибку в некоторых случаях, если два типа полностью не связаны, но в других случаях это не приведет к возникновению ошибки, даже если операция не является типобезопасной. Используйте приведение с разреженным способом, так как любое преобразование из одного типа в другой является потенциальным источником ошибки программы. Однако литья иногда требуются, и не все литые одинаково опасны. Одно эффективное использование приведения заключается в том, что код выполняет сужение преобразования, и вы знаете, что преобразование не приводит к неправильному результату программы. В действительности это сообщает компилятору, что вы знаете, что вы делаете, и прекратить беспокоить вас с предупреждениями об этом. Другим способом является приведение от класса указателя на производный от указателя к базовому классу. Другое использование заключается в том, чтобы отбросить константу переменной, чтобы передать ее в функцию, требующую аргумента, отличного от константа. Большинство этих операций приведения включают некоторый риск.
В программировании в стиле C один и тот же оператор приведения в стиле C используется для всех видов приведения.
(int) x; // old-style cast, old-style syntax
int(x); // old-style cast, functional syntax
Оператор приведения в стиле C идентичен оператору вызова () и поэтому неудобен в коде и легко упускать из виду. Оба плохо, потому что они трудно распознать на первый взгляд или найти, и они достаточно разрозненные, чтобы вызвать любое сочетание static
, const
и reinterpret_cast
. Определение того, что на самом деле делает старый стиль, может быть трудным и подверженным ошибкам. По всем этим причинам, если требуется приведение, рекомендуется использовать один из следующих операторов приведения C++, которые в некоторых случаях значительно более типобезопасны, и которые выражают гораздо более явно намерение программирования:
static_cast
, для приведения, которые проверка только во время компиляции.static_cast
возвращает ошибку, если компилятор обнаруживает, что вы пытаетесь приведения между типами, которые полностью несовместимы. Вы также можете использовать его для приведения между указателем на базу и указатель на производный, но компилятор не всегда может определить, будут ли такие преобразования безопасными во время выполнения.double d = 1.58947; int i = d; // warning C4244 possible loss of data int j = static_cast<int>(d); // No warning. string s = static_cast<string>(d); // Error C2440:cannot convert from // double to std:string // No error but not necessarily safe. Base* b = new Base(); Derived* d2 = static_cast<Derived*>(b);
Дополнительные сведения см. в разделе
static_cast
.dynamic_cast
, для безопасных, проверка проверка приведения указателя на базу к указателю на производный. Этоdynamic_cast
безопаснее, чемstatic_cast
для даункастов, но среда выполнения проверка несет некоторые издержки.Base* b = new Base(); // Run-time check to determine whether b is actually a Derived* Derived* d3 = dynamic_cast<Derived*>(b); // If b was originally a Derived*, then d3 is a valid pointer. if(d3) { // Safe to call Derived method. cout << d3->DoSomethingMore() << endl; } else { // Run-time check failed. cout << "d3 is null" << endl; } //Output: d3 is null;
Дополнительные сведения см. в разделе
dynamic_cast
.const_cast
, для отведенияconst
от -ness переменной или преобразованияconst
не-переменной в значениеconst
. Отбрасываниеconst
-ness с помощью этого оператора так же подвержено ошибкам, как и использование приведения в стиле C, за исключением того, что сconst_cast
вами меньше шансов случайно выполнить приведение. Иногда необходимо отброситьconst
-ness переменной, например, чтобы передатьconst
переменную в функцию, которая принимает параметр, отличныйconst
от параметра. В приведенном ниже примере показано, как это сделать.void Func(double& d) { ... } void ConstCast() { const double pi = 3.14; Func(const_cast<double&>(pi)); //No error. }
Дополнительные сведения см. в разделе
const_cast
.reinterpret_cast
для приведения между несвязанными типами, такими как тип указателя и объектint
.Примечание.
Этот оператор приведения не используется так часто, как и другие, и он не гарантируется переносимым к другим компиляторам.
В следующем примере показано, как
reinterpret_cast
отличается отstatic_cast
.const char* str = "hello"; int i = static_cast<int>(str);//error C2440: 'static_cast' : cannot // convert from 'const char *' to 'int' int j = (int)str; // C-style cast. Did the programmer really intend // to do this? int k = reinterpret_cast<int>(str);// Programming intent is clear. // However, it is not 64-bit safe.
Дополнительные сведения см. в разделе
reinterpret_cast
"Оператор".
См. также
Система типов C++
Возвращение к C++
Справочник по языку C++
Стандартная библиотека C++
Обратная связь
https://aka.ms/ContentUserFeedback.
Ожидается в ближайшее время: в течение 2024 года мы постепенно откажемся от GitHub Issues как механизма обратной связи для контента и заменим его новой системой обратной связи. Дополнительные сведения см. в разделеОтправить и просмотреть отзыв по