Преобразование типов в стиле С

Когда, мы что-либо делаем, нам, несомненно, важно знать каков будет результат. Вполне очевидно, что из тех ингредиентов из которых, скажем вариться суп харчо, вряд ли можно приготовить торт со взбитыми сливками. Следовательно, результат напрямую зависит, от составных частей. То же самое происходит с переменными. Если, скажем, складывается два числа типа int, вполне понятно, что результат так же будет иметь тип int. А вот как быть, если данные имеют разные типы? Именно об этом мы и поговорим в текущем разделе данного урока.

Итак, прежде всего, давайте разберемся с тем, как типы данных взаимодействуют друг с другом. Существует так называемая иерархия типов, где все типы размещены по старшинству. Для того, что бы разбираться в преобразовании типов, необходимо всегда помнить порядок типов этой иерархии.

bool,char,short-int-unsigned int-long-unsigned long-float-double-long double

Несмотря на то, что некоторые типы имеют одинаковый размер, в них помещается разный диапазон значений, например, unsigned int в отличие от int может поместить в себя в два раза больше положительных значений, и потому является старше по иерархии, при этом оба типа имеют размер 4 байта. Кроме того, следует отметить, очень важную особенность, отраженную здесь, если в преобразовании типов участвуют такие типы, как bool,char,short, они автоматически преобразовываются к типу int.

Теперь, давайте рассмотрим, различные классификации преобразований.

Классификация по диапазону содержащихся значений. Все преобразования можно разделить на две группы относительно местоположения в иерархии типов участвующих в преобразовании.

  • Сужающее преобразование – при таком преобразовании - больший тип данных в иерархии преобразуется к меньшему типу, безусловно, в этом случае может произойти потеря данных, поэтому с сужающим преобразованием, следует быть осторожными. Например:
int A=23.5;
cout<<A; // на экране 23

Как видите, такое преобразование от double к int, ведет к грубому обрезанию вещественного числа, без математического округления, результатом которого являлось бы число 24.

  • Расширяющее преобразование. Данный вид преобразования, ведет к так называемому расширению типа данных от меньшего диапазона значений к большему диапазону. В качестве примера предлагается такая ситуация.
unsigned int a=3000000000;
cout<<a; // на экране 3000000000

В данном случае 3000000000 - это литерал типа int, который благопоучно расширяется до unsigned int, что и позволяет нам увидеть на экране именно 3000000000, а не что-то другое. Тогда, как в обычный int такое число не поместиться.

Классификация по способу осуществления преобразования

Вне зависимости от направления преобразования, оно может быть осуществлено одним из двух способов.

  1. Неявное преобразование. Все вышеописанные примеры относились к этому типу преобразования. Такой вид преобразования также называют автоматическим, т. к. оно происходит автоматически без вмешательства программиста, другими словами, мы ничего не делаем для того, что бы оно произошло.
    float A=23,5; - double стал  float без каких-либо дополнительных действий
  2. Явное преобразование. (второе название – приведение типов). В данном случае, преобразование производится программистом, тогда, когда это необходимо. Давайте рассмотрим простой пример такого действия:
    double z=37.4;
    float y=(int) z;
    cout<<z<<”*** ”<<y; // на экране 37.4 ***37

(int)z – есть явное сужающее преобразование от типа double к типу int. В этой же строке происходит расширяющее неявное преобразование от полученного типа int к типу float. Следует запомнить, что любое преобразование носит временный характер и действует только в пределах текущей строки. То есть переменная z как была double, так и останется на протяжении всей программы, а ее преобразование в int носило временный характер.

Преобразование типов в выражении

И, вот, мы наконец-то подошли к тому, о чем говорили в самом начале данного раздела урока, как выяснить какого типа будет результат какого-то выражения. Давайте попробуем это вычислить, пользуясь полученными знаниями. Предположим у нас есть следующие переменные:

int I=27;
short S=2;
float F=22.3;
bool B=false;

Пользуясь этими переменными, мы собираемся составить такое выражение:

I-F+S*B

В переменную какого типа данных нам следует записать результат? Решить это просто, если представить выражение в виде типов данных:

int-float+short*bool

Напоминаем, что short и bool сразу же примут тип int, так что выражение будет выглядеть так:

int-float+int*int,  при этом false станет 0

Умножение int на int даст, несомненно, результат типа int. А вот сложение float с int, даст на выходе float, так как, здесь вступает в игру новое правило: Если в каком-либо выражении используются разные типы данных, то результат, приводится к большему из этих типов. Ну и, наконец – вычитание из int типа float, согласно только что упомянутому правилу снова даст float. Таким образом, результат выражения будет иметь тип float.

float res= I-F+S*B; // 27-22.3+2*0
cout<<res; // на экране число 4.7

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

Примечание: Будьте очень внимательны также и при сочетании переменных с одинаковыми типами данных. Например, помните, если целое делится на целое, то и получиться целое. То есть, int A=3; int B=2; cout«A/B; на экране 1, так как результат - int и дробная часть утеряна. cout«(float)A/B; на экране 1.5, так как результат - float.

Теперь давайте закрепим знания на практике. Создадим проект и напишем нижеследующий код.

# include <iostream>
using namespace std;
void main(){

	// объявление переменных и запрос на ввод данных
	float digit;
	cout<<"Enter digit:";
	cin>>digit;

	/* Даже, если пользователь ввел число с вещественной частью,
	результат выражения запишется в int и вещественная часть будет утеряна,
	разделив число на 100 мы получим количество сотен в нем. */   
	int res=digit/100;
	cout<<res<<" hundred in you digit!!!\n\n";
}

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

Преобразование типов в стиле С++

Для начала напомним, что оператор приведения типов позволяет компилятору преобразовывать один тип данных в другой. И язык программирования С, и язык программирования С++ поддерживают такую форму записи приведения типа:

(тип) выражение

Например,

double d;
d=(double) 10/3;

Кроме стандартной формы в стиле С, язык С++ поддерживает дополнительные операторы приведения типа. Вполне очевидны положительные стороны этих новых способов. Во-первых данные преобразования позволяют сделать то, чего стандарт языка С даже не предполагал: снять модификатор const, или радикально поменять тип данных. Во-вторых, стиль С++ увеличивает контроль над преобразованиями и позволяет выявить ошибки с ними связанные на ранних этапах.

  • const_cast<тип> (объект)
  • dynamic_cast<тип> (объект) проверяет законность выполнения заданной операции приведения типа
  • reinterpret_cast<тип>(объект) следует использовать для перевода типов указателей, которые несовместимы по своей природе
  • static_cast<тип> (объект) Фактически, static_cast - это аналог стандартной операции преобразования в стиле С.

const_cast используется для явного переопределения модификаторов const и/или volatile. Новый тип должен совпадать с исходным типом, за исключением изменения его атрибутов const или volatile. Чаще всего оператор const_cast используется для снятия атрибута const. Например:

Следует помнить, что только оператор const_cast может освободить от "обета постоянства", т.е. ни один из остальных операторов этой группы не может "снять" с объекта атрибут const.
#include <iostream>
using namespace std;
// указатель на объект является константным,
// следовательно, через него изменить значение
// объекта нельзя
void test_pow(const int* v){
	int*temp;
	// снимаем модификатор const
	// и теперь можем изменять объект
	temp=const_cast<int*>(v);
	// изменение объекта
	*temp= *v * *v;
}
void main(){
	int x=10;
	// на экране - 10
	cout<<"Before - "<<x<<"\n\n";
	test_pow(&x);
	// на экране - 100
	cout<<"After - "<<x<<"\n\n";
}

Примечание: Кстати, стоит упомянуть о том, что такое volatile!!! Данный модификатор даёт компилятору понять, что значение переменной, может быть изменено неявным образом. Например, есть некая глобальная переменная, её адрес передаётся встроенному таймеру операционной системы. В дальнейшем эта конструкция может использоваться для отсчёта реального времени. Естественно, что значение этой переменной изменяется автоматически без участия операторов присваивания. Так вот такая переменная должна быть объявлена с использованием ключевого слова volatile. Это связано с тем, что многие компиляторы не обнаружив ни одного изменения переменной с помощью какого-либо присваивания, считают, что она не изменяется и оптимизируют выражения с ней, подставляя на её место конкретное значение. При этом компилятор переменную не перепроверяет. Действительно, зачем, если переменная для него имеет характер "неизменяемой"?! volatile - будет блокировать подобные действия компиляторов.

dynamic_cast проверяет законность выполнения заданной операции приведения типа. Если такую операцию выполнить нельзя, то выражение устанавливается равным нулю. Этот оператор в основном используется для полиморфных типов. Например, если даны два полиморфных класса, B и D, причем класс D выведен из класса B, то оператор dynamic_cast всегда может преобразовать указатель D* в указатель B*. Оператор dynamic_cast может преобразовать указатель B* в указатель D* только в том случае, если адресуемым объектом является объект D. И вообще, оператор dynamic_cast будет успешно выполнен только при условии, что разрешено полиморфное приведение типов (т.е. если новый тип можно законно применять к типу объекта, который подвергается этой операции). Рассмотрим пример, демонстрирующий всё вышесказанное:

#include <iostream>
using namespace std;
// базовый класс
class B{
	public:
	// виртуальная функция для 
	// последующего переопределения в потомке
	virtual void Test(){
		cout<<"Test B\n\n";
	}
};
// класс-потомок
class D:public B{
	public: 
	// переопределение виртуальной функции
	void Test(){
		cout<<"Test D\n\n";
	}
};
void main(){
	// указатель на класс-родитель
	// и объект класса-родителя
	B *ptr_b, obj_b;
	// указатель на класс-потомок
	// и объект класса-потомка
	D *ptr_d, obj_d;
	
	// приводим адрес объекта (D*) к указателю типа D*
	ptr_d= dynamic_cast<D*> (&obj_d);
	// если все прошло успешно - вернулся !0
	// произошло приведение 
	if(ptr_d){
		cout<<"Good work - ";
		// здесь вызов функции класса-потомка
		// на экране - Test D
		ptr_d->Test(); 
	}
	// если произошла ошибка - вернулся 0
	else cout<<"Error work!!!\n\n";
	
	// приводим адрес объекта (D*) к указателю типа B*
	ptr_b= dynamic_cast<B*> (&obj_d);
	// если все прошло успешно - вернулся !0
	// произошло приведение 
	if(ptr_b){
		cout<<"Good work - ";
		// здесь вызов функции класса-потомка
		// на экране - Test D
		ptr_b->Test(); 
	}
	// если произошла ошибка - вернулся 0
	else cout<<"Error work!!!\n\n";
	
	// приводим адрес объекта (B*) к указателю типа B*
	ptr_b= dynamic_cast<B*>(&obj_b);
	// если все прошло успешно - вернулся !0
	// произошло приведение 
	if(ptr_b){
		cout<<"Good work - ";
		// здесь вызов функции класса-потомка
		// на экране - Test B
		ptr_b->Test(); 
	}
	// если произошла ошибка - вернулся 0
	else cout<<"Error work!!!\n\n";
	
	// ВНИМАНИЕ!!! ЭТО НЕВОЗМОЖНО
	// попытка привести адрес объекта (B*) к указателю типа D*
	ptr_d= dynamic_cast<D*> (&obj_b);
	// если все прошло успешно - вернулся !0
	// произошло приведение 
	if(ptr_d)
		cout<<"Good work - ";		
	// если произошла ошибка - вернулся 0
	else cout<<"Error work!!!\n\n";	
}
Результат работы программы:
Good work - Test D
Good work - Test D
Good work - Test B
Error work!!!

static_cast выполняет неполиморфное приведение типов. Его можно использовать для любого стандартного преобразования. При этом никакие проверки во время работы программы не выполняются. Фактически, static_cast - это аналог стандартной операции преобразования в стиле С. Например, так:

#include <iostream>
using namespace std;
void main(){
	int i;
	for(i=0;i<10;i++)
		// приведение переменной i к типу double
		// результаты деления на экране, естественно
		// вещественные
		cout<<static_cast<double>(i)/3<<"\t";	
}

reinterpret_cast переводит один тип в совершенно другой. Например, его можно использовать для перевода указателя в целый тип и наоборот. Оператор reinterpret_cast следует использовать для перевода типов указателей, которые несовместимы по своей природе. Рассмотрим пример:

#include <iostream>
using namespace std;
void main(){
	// целочисленная переменная
	int x;
	// строка (указатель типа char)
	char*str="This is string!!!";
	// демонстрируем строку на экран
	cout<<str<<"\n\n"; // на экране - This is string!!!
	// преобразуем указатель типа char в число
	x=reinterpret_cast<int>(str);
	// демонстрируем результат
	cout<<x<<"\n\n"; // на экране - 4286208
}

В C++ для динамической идентификации типов применяются операторы dynamic_cast и typeid (определён в файле typeinfo.h), для использования которых информацию о типах во время выполнения обычно необходимо добавить через опции компилятора при компиляции модуля.

  • Оператор dynamic_cast пытается выполнить приведение к указанному типу с проверкой. Целевой тип операции должен быть типом указателя, ссылки или void*.
  • Оператор typeid возвращает ссылку на структуру type_info, которая содержит поля, позволяющие получить информацию о типе.
PQ VPS сервера в 28+ странах.