欢乐C++ —— 2. 基本类型与类型转换
参考:
《C++ Primer》 p59 constexpr
https://en.cppreference.com/w/cpp/language/constexpr constexpr
声明与定义
变量名,函数名等在编译中统称为符号
符号的声明与定义:为了支持分离式编译(即可以将程序划分为多个文件,每个文件可独立编译),C/C++将声明与定义分离开来。
- 声明:使符号在编译期对程序可见。
- 定义:创建与符号关联的实体。
不完全类型,就是指已经声明但没有定义的类型。
特殊类型
constexpr
简介
什么是constexpr 关键字? 官方文档这样定义:
- constexpr: specifies that the value of a variable or function can appear in constant expressions.
- constant expressions: Defines an expression that can be evaluated at compile time.
也就是说变量或函数的值在编译期就可见。
示例
假如你想用一个变量或函数返回值定义数组的大小。数组大小就必须为常量表达式
const int fun( ) {
return 2;
}
constexpr int fun_ce( ) {
return 2;
}
constexpr int fun_ce2(int n ) {
return n+4;
}
int main( ) {
const int n = 10;
int nn = 10;
int arr[n];
int brr[fun( )]; //编译器报错
int crr[fun_ce( )];
int drr[fun_ce2(n)];
int err[fun_ce2(fun_ce2(n))];
int frr[fun_ce2(fun_ce2(nn))]; //编译器报错
return 0;
}
brr 是因为 fun 在运行期执行才能得到值,而其它两个函数都要求在编译器求值。frr 是因为 变量 nn 是运行期才能确定值的变量,自然不能在编译期得到其值。
而arr,drr,err 之所以正常运行是因为编译器在编译期间就自动替换了这些值。
还有其它,constexpr 可以用在类的非静态函数,甚至从C++20开始类的析构函数也可以声明为 constexpr 函数跟模板那块也有一定联系。constexpr 不同的C++ 标准增加或删除了一些限制。由于本篇是初学教程,暂时涉及不到那些,如果感兴趣见参考中的C++ 官方文档。
总结
const 更准确的说是声明不能被修改的变量,而实际上可以通过地址间接修改。constexpr 则是在编译期就求值,所以在运行期就是一个字面量,没办法改。
总之如果想要一些在常量表达式中使用某些值,那么就将其设置为constexpr。
auto
通过变量的初始值推断出变量的类型。
decltype ()
auto 很方便,但有时候我们只是想知道初始值的类型,而不使用初始值。这个时候就需要用decltype。
一般来说,引用从来都是作为所指对象的同义词出现,只有在decltype 这块却是个意外,decltype 会老老实实判断出类型是否为引用。
指针解引用得到的是左值引用,所以decltype (*p) 得到的是引用类型。
类型转换
主要分为隐式转换和显示转换:
- 隐式转化:什么算数转换,赋值转换,后面类初始化时的构造函数实参形参的转换等等。后面用到再说。
- 显示转换:命名的强制类型转换都是这种形式,
name_cast<type> (expression);
而原先的强转是type(expression);
或(type) expression;
几个显示类型转换
-
static_cast
() 任何有明确定义的类型转换,只要不包含底层const
-
const_cast
() 只能改变运算对象的底层const,通常用于去掉左值对象的const 属性
-
reinterpret_cast
() 更改运算对象的底层解释方式。从某种程度上来说,比static_cast 更加底层,更加灵活。
-
dynamic_cast
() 将派生体系中的指针或引用进行转换。
int main(){
double d = 20.5;
int n = static_cast< float >( d );
void *p = &d;
const double* pd = static_cast< double* >( p );
const double* cpd = static_cast< double* >( p );
const char * pc = static_cast<char* >( pd ); //error double* 类型指针转化为char* 是未定义的行为
const double * pd2 = static_cast< const int* >( pd ); //error 和上面一样是未定义的行为
pd2 = static_cast< double * >( pd ); //error 无法去掉底层const 属性
pd2 = const_cast< double * >( pd );
pc = reinterpret_cast< char* >( pd ); //error 无法去掉底层const 属性
pc = reinterpret_cast<const char* >( pd );
reinterpret_cast< char* >( 0x11111111 );//其它类型转换无法达到这样效果
}
运行时类型识别
运行时类型识别(run-time type identification ,RTTI)主要应用在 当我们想通过绑定在派生类对象的基类指针或引用来调用派生类非虚函数。而在某些场景下,不能将所有的函数都设置为虚函数。
其功能主要由两个运算符实现:
- typeid 运算符,用于返回表达式类型,可以通过基类的指针或引用来区分是指向的是派生类还是基类
- dynamic_cast 运算符,用于将基类的指针和引用安全的转化为派生类指针和引用。
typeid 与 decltype 的区别在于,前者在运行期获得类型,而后者是在编译期就得出类型,如果用于上述应用场景,得到的永远是基类类型。
dynamic_cast
其有三种使用形式:
- dynamic_cast<Type*>();
- dynamic_cast<Type&>();
- dynamic_cast<Type&&>();
其中,Type 必须是派生体系中的类类型。被转化的表达式必须和目标类型有关,且必须是公有的继承关系。
若不满足,则转化失败,如果是指针,则返回nullptr ;如果是引用,则抛出bad_cast 异常。
if(Dervied *p = dynamic_cast<Dervied*>(bp)){
//说明bp 指向派生类对象,通过p 可以访问派生类对象
}
else{
//说明bp 指向基类对象。
}
try{
Dervied &d = dynamic_cast<Dervied&>(b);
}
catch(bad_cast){
}
//如果引用出错,则抛出bad_cast
你会发现,将dynamic_cast 换成 static_cast 也不会编译报错。
这样的确是允许的,但实际上dynamic_cast 会在运行时期检查,如果转化不符合条件会返回nullptr 或抛异常。而static_cast 则不会,它会给你强制转化,不管你符不符合条件,所以后面使用可能就会出问题。
typeid
当运算对象不包含多态的条件则typeid 的结果在编译期就可知,和decltype 大体一样。而当含有多态的可能,typeid 才会对表达式求值的其结果只有在运行期才可知。
int main(){
Dervied *dp = new Dervied;
Base *bp = dp;
if ( typeid( bp ) == typeid( dp ) ) {
//比较的是指针静态类型,永远不成立。
}
if ( typeid( *bp ) == typeid( *dp ) ) {
//这样是比较两个指针的动态类型是否一致
}
if ( typeid( *bp ) == typeid(Dervied ) ) {
//这样是比较基类指针bp 是否指向派生类对象
}
}
type_info 类定义在==
!=
.name()
等操作
枚举
C++11 引入了限定作用域的枚举,与原先枚举的区别在于作用域。
enum class Color { RED, BLUE, BLACK };
int main(){
Color c = RED; //error 没有指出作用域
c = Color::RED;
}
可以指定大小的枚举:一般而言,枚举量是int,我们可以指定枚举量类型
enum class LongValue:long long {};