1 C 中的类型
C 中类型分为两种:内置类型和自定义类型。内置类型中分为算术类型和空类型。其中算术类型包含字符,整型,布尔值和浮点数。空类型不对应具体的值,仅用于一些特殊的场合,例如最常见的是,当函数不返回任何值时使用空类型作为返回类型。
类型 | 含义 | 尺寸 |
---|---|---|
bool | 布尔类型 | 只有一位 1表示True 0表示False 有数据表示为True,0表示为False |
char | 字符 | 8 位 |
wchar_t | 宽字符 | 16位 |
char16_t | Unicode字符 | 16位 |
char32_t | Unicode字符 | 32位 |
short | 短整型 | C 要求short类型不少于16位 ,一般为16位 |
int | 整型 | C 要求int类型至少与short类型一样长,一般为32位 |
long | 长整型 | C 要求long至少为32位,且至少与int一样长 , 一般为32位 |
long long | 长整型 | C 要求long long至少为64位,且至少与long一样长,一般为64位 |
float | 单精度浮点型 | C 要求至少为32位。后缀F or f |
double | 双精度浮点型 | C 要求至少为48位,且不少于float |
long double | 扩展精度浮点型 | C 要求为80,96,128位,至少和double类型位数一样多 |
对于这些内置类型,在使用时将一个类型赋值给另一个类型或者是在进行运算时,如果两个类型有关联就会发生隐式类型转换,这种转换不需要程序员介入,是自动执行的,这种转换是有可能造成数据丢失的!
2 类型转换
在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与接收返回值类型不一致时,就需要发生类型转化,C语言中总共有两种形式的类型转换:隐式类型转换和显式类型转换。
- 隐式类型转化:编译器在编译阶段自动进行,能转就转(有关联才能转),不能转就编译失败。整型之间,浮点数和整型之间
- 显式类型转化(强制类型转换):需要用户自己处理,各类指针是可以显式类型转换的!
在C 中同样支持C语言风格的类型转换,并且新增了内置类型向自定义类型的转换和自定义类型向内置类型的转换! 我们来写一个类来看看:
代码语言:javascript复制class A
{
public:
//explicit A(int a1) 这样不支持隐式类型转换!
A(int a1)
:_a1(a1)
{}
private:
int _a1;
int _a2;
};
int main()
{
A aa1 = 1;
return 0;
}
像这样的单参数构造函数的类支持隐式类型转换!多参数的构造函数就需要使用{ }
来进行列表初始化,才可以做到类型转换!
如果不希望该类进行隐式类型转换,可以使用explicit
关键字进行修饰,这样就不支持内置类型向自定义类型的隐式类型转换了!
在来看自定义类型如何向内置类型进行转换呢?C 通过了一个十分直接的方法,想转什么类型就operator
重载什么类型:
class A
{
public:
//explicit A(int a1) 这样不支持隐式类型转换!
A(int a1)
:_a1(a1)
{}
operator int()
{
return _a1 _a2;
}
private:
int _a1;
int _a2;
};
很简单的操作逻辑,这就是C 支持的自定义类型向内置类型的转换,这个不太常用,只要是在IO
流中的对象中会有operator bool()
来支持进行布尔的判断!
自定义类型之间的类型转换可以通过拷贝构造来进行!
3 四种类型转换
C风格的转换格式很简单,但是有不少缺点的:
- 隐式类型转化有些情况下可能会出问题:比如数据精度丢失
- 显式类型转换将所有情况混合在一起,代码不够清晰
因此C 提出了自己的类型转化风格,注意因为C 要兼容C语言,所以C 中还可以使用C语言的转化风格:
- 隐式类型转换(静态转换):static_cast
- 强制类型转换(重新解释):reinterpret_cast
- 去常转换:const_cast
- 动态转换:dynamic_cast
3.1 static_cast 静态转换
static_cast用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用static_cast,但它不能用于两个不相关的类型进行转换。也就是说,只要是C风格的类型转换,可以都套上static_cast
!
任何具有明确定义的类型转换,只有是不包括底层const,都可以使用static_cast!
注意:隐式类型转换不支持的转换,套上static_cast也不支持!
如果我们将一些需要进行强制类型转换的场景也套上 static_cast,这时就会发生报错了!这保证了不能乱用
对于需要强制类型转换的场景需要使用reinterpret_cast
总结:
- static_cast 可以用于基本类型的转换
- static_cast 不能用于基本类型指针间的转换(需要强制类型转换)
- static_cast可以用于有继承关系类对象之间的转换和类指针之间的转换 (派生类转换成基类时安全(上行转换),基类转换成派生类时不安全(下行转换))
3.2 reinterpret_cast 重新解释
在隐式类型转换不能进行转换时,我们就需要强制类型转换。强制类型转换很有可能会造成运行时的错误!
reinterpret_cast操作符通常为操作数的位模式提供较低层次的重新解释,用于将一种类型转换为另一种不同的类型
3.3 const_cast 去常转换
const_cast 只能改变运算对象的底层const。只有 const_cast 可以做到去常,并且还要配合使用volatie
关键字。const_cast最常用的用途就是删除变量的const属性,方便赋值。会将一个稳定的变量变成不稳定的!
去常操作常常在函数重载中进行使用:比如在类内我们要实习一个功能:比较两个字符串的大小。为了适配常量字符串和非常量字符串,我们需要进行一个函数重载:
代码语言:javascript复制const string& func(const string& s1 , const string& s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
string& func(string& s1 , string& s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
上面的场景是很常见的,如果遇到函数实现比较复杂的情况,在使用上面的做法就有些冗余了,我们可以在上层对const
版本的函数进行一次包装,来适配正常版本:
string& func(string& s1 , string& s2)
{
auto &r = func(static_cast<const string&>(s1) , static_cast<const string&> (s2));
return const_cast<string&>(r);
}
这样就简单通过一个const
版本的函数,通过去常操作实现了非const
版本的函数!这是十分安全的操作!
3.4 dynamic_cast 动态转换
dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换) 向上转换:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则) 向下转换:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的)
学习过继承之后,我们知道派生类内部是包含一个基类,可以通过切片的方式来转换成基类!甚至不需要产生临时对象!这是天然支持的!但是对于基类转换为子类就有点复杂了!
代码语言:javascript复制void func(A* pa)
{
B* pb = (B*)pa;
}
对于这样一个函数,基类指针会强制类型转换为子类指针,当pa指针本来就是指向的是一个B对象,在转换回去,没有问题。但是当pa指针指向的是A对象,那么强行转换会造成越界的问题!在读取时会造成崩溃!
为了解决这个问题,可以使用 dynamic_cast
:
void func(A* pa)
{
B* pb = dynamic_cast<B*>(pa);
if (pb)
{
cout << pb << endl;
pb->_b1 ;
}
else
{
cout << "转换失败!" << endl;
}
}
如果pa指针指向的是B对象,转换成功! 如果pa指针指向的是A对象,转换失败!返回空!
总结:
- dynamic_cast只能用于父类含有虚函数的类
- dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回0
3.5 RTTI(了解)
RTTI:Run-time Type identification
的简称,即:运行时类型识别。
C 通过以下方式来支持RTTI:
- typeid 运算符
- dynamic_cast 运算符
- decltype
4 使用建议
强制类型转换干扰了正常的类型检查,因此我们强烈建议程序员避免使用强制类型转换。这个建议对于reinterpret_cast
尤其适用,因为此类类型转换总是充满了风险。
在有重载函数的上下文中使用const cast
无可厚非,但是在其他情况下使用const cast
也就意味着程序存在某种设计缺陷。其他强制类型转换,比如static_cast
和dynamic_cast
,都不应该频繁使用。
每次书写了一条强制类型转换语句,都应该反复斟酌能否以其他方式实现相同的目标。就算实在无法避免,也应该尽量限制类型转换值的作用域,并且记录对相关类型的所有假定,这样可以减少错误发生的机会。