C++11:系统类型增强
C++11:系统类型增强
- 强枚举类型
- 作用域限定
- 隐式类型转换
- 指定类型
- 前置声明
- 类型别名 using
- 模板别名
- 复杂指针别名
- auto
- 限制性 auto
- 注意事项
- nullptr
- decltype
强枚举类型
在C++98的枚举设计中,存在很多缺陷,为此C++11推出了强枚举
来代替旧版的枚举,提供更加安全可靠的枚举类型。
强枚举的语法如下:
enum class e : type
{val1 = 1,val2 = 2
}
此处定义了一个名为e
的强枚举,只需要在enum
后面加一个calss
关键字即可,此处的: type
用于指定底层类型,后面讲解,可以省略。
作用域限定
旧版本的 enum
会直接把枚举内部的值放到外层作用域,如果多个枚举有一样的值,或者在相同作用域定义了与枚举同名的变量,那么就会导致命名冲突:
enum e1
{a = 1
};enum e2
{a = 2
};
以上代码中,e1::a
和e2::a
发生了冲突,全局作用域存在两个叫做a
的变量。
在强枚举中,每个枚举自成一个作用域,不会污染外部变量,相互之间也不会冲突。
enum class e1
{a = 1
};enum class e2
{a = 2
};
以上代码就不会报错了,因为e1
和e2
都是强类型枚举,各自作用域独立,枚举也不会放到全局作用域。
由于枚举值不在全局作用域了,那么也就不能直接访问a
了,必须通过域限定符来访问:e1::a
和 e2::a
。
隐式类型转换
旧版本的 enum
可以随意的隐式转换成其他类型,这是从C语言继承下来的极其不安全的特性,它可以转化为任意整形家族,甚至char
,bool
,float
。
enum type_old
{a = 128
};int main()
{int x = a;signed char c = a;long long ll = a;float f = a;return 0;
}
以上代码是合法的(在 vs2022
与 g++13.3
编译均通过),这非常不安全,例如此处的 a = 128
,它其实是超出了 signed char
的范围的,但是他不仅没有报错,而且还隐式的发生了截断,此时如果再std::cout << (int)c
,你会得到-128
这个值。
强枚举有非常严格的类型限定,他不能隐式转化为强枚举之外的类型。
enum class type_new
{a = 1
};int main()
{// err: 不允许隐式转化int x = type_new::a;type_new e1 = 1;// success: 显示类型转换int y = static_cast<int>(type_new::a);type_new e2 = (type_new)1;// success: C风格显示类型转换int z = (int)type_new::a;type_new e3 = static_cast<type_new>(1);return 0;
}
使用强枚举后,既不允许从其他类型隐式转为强枚举(哪怕这个值在枚举中存在),也不允许强枚举隐式转为其他类型。如果需要转换,那么必须用static_cast
或者C语言风格的显式类型转换。
指定类型
在旧版enum
中,其类型往往是不确定的,这可能随着编译器不同而变化,一般为int
。就算你传入一个long long
类型,最后也会被转回int
。
这是因为 在C++标准中规定:编译器指定的枚举底层类型,只要可以存储所有的枚举值即可。
例如你的枚举值是1 2 3
,那么就有可能用short
这样的来存储,如果再大一点就可能是int
。不过就算C++标准这么规定,其实大部分主流编译器都固定使用int
,不论你数据范围是多少,例如MSVC
和gcc
。
enum type_old
{a = INT_MAX + 1ll // 此处 1ll 表示 long long 字面量,后缀ll不可省略
};int main()
{std::cout << a << std::endl;return 0;
}
以上代码中,type_old::a
这个枚举接受了一个大于int
最大值的long long
字面量,程序正常运行,最后输出结果为:-2147483648
也就是int
的最小值,这是因为发生了从long long -> int
的隐式截断,你无法在 C++98 的枚举中存储大于int
范围的值。相应的,当你存储的数据范围比较小,比如枚举值都是0 ~ 127
,你也不能用一个字节来存,必须用int
,(或者你可以祈祷某种编译器检测到了你的数据范围比较小,给你改用小范围的类型来存储)。
在强枚举中,可以指定枚举值的底层类型:
enum class type_new : long long
{a = INT_MAX + 1ll
};int main()
{std::cout << static_cast<long long>(type_new::a) << std::endl;return 0;
}
代码中: long long
指定了枚举的底层使用long long
存储,最后程序输出:2147483648
,也就是INT_MAX + 1
。
要注意的是,如果你在C++11环境运行以下代码,也是合法的:
enum type_new : long long // 此处把 class 删掉了,是普通枚举
{a = INT_MAX + 1ll
};
因为C++11在更新强枚举的同时,对旧版枚举也做了优化,允许普通枚举也指定底层类型!但是普通枚举作用域,隐式转化等特性依然保留。
前置声明
旧版枚举是不允许前置声明的,例如以下代码会报错:
enum type_old;void func(type_old e)
{
}enum type_old
{a = 128
};int main()
{func(a);return 0;
}
以上代码在C++98环境运行会报错,刚才说过,枚举底层使用什么类型是不确定的,在不同编译器可能不同,这就导致func
函数的第一个参数type_old
声明后,无法确定其内存大小,从而编译失败。
在C++11中,其实强枚举直接这么写也会报错,例如把上面的声明改成:enum class type_new
这样的强枚举,还是会报错。
根本原因是无法确定枚举的底层变量,从而无法得知大小。因为C++标准明确说了编译器可以自己来指定底层变量,在枚举值还没有定义之前,编译器根本就无法推断用什么类型来存,也就不知道这个枚举的底层类型。
此时上一个特性就派上用场了,用户可以自己显式指定枚举底层类型,那编译器不就明确了枚举的大小了么?
而刚才又说过,C++11对普通枚举和强枚举都支持指定底层类型,那么代码就可以这样改写:
enum type_old : int;
enum type_new : int;void func(type_old e1, type_new e2)
{
}enum type_old : int
{a = 128
};enum type_new : int
{a = 128
};
现在不论强枚举还是普通枚举,都可以提前声明了,因为通过: int
明确指定了底层使用int
存储,那么func
的两个参数就知道自己要给参数预留多少空间,此时前置声明就有用了。
类型别名 using
在 C++11 之前,typedef
是定义类型别名的唯一方式,但它存在语法局限,尤其是在模板编程中不够灵活。
在C++11之前,using
主要用于展开命名空间,或者声明其它命名空间内部的变量。
C++11 给 using
添加了类型别名的功能,不仅替代了 typedef
,还提供了更强大的功能,特别是在模板别名和复杂类型表达式简化方面。
语法如下:
using new_name = old_name;
模板别名
如果想给一系列模板类取别名,例如希望简化list
的迭代器类型std::list<T>::iterator
变成list_it<T>
,使用typedef
是无法做到的,而using
就可以配合模板使用:
template<typename T>
using list_it = std::list<T>::iterator;int main()
{list_it<int> p; // successreturn 0;
}
这是typedef
无法做到的,也是using
最大的优势。
复杂指针别名
在使用typedef
给函数指针或者数组指针取别名的时候,语法会很复杂,而且可读性很差,例如:
// 把 void(*)(int, int) 类型的函数指针取别名为 func_ptr
typedef void(*func_ptr)(int, int);// 把 int(*)[] 类型的数组指针取别名为 arr_ptr
typedef int(*arr_ptr)[];
这是因为在typedef
中,要求新名称必须写在*
后面,这样编译器才知道这个新的名称是一个指针,这就导致可读性很差,例如一个带有回调函数的函数指针取别名:
typedef void(*func_ptr)(void(*)(void), int);
此处func_ptr
的类型是void(*)(void(*)(void), int)
,如果C语言基础差一些,这段代码要琢磨一点时间。
在using
中,无需把新名称写到*
后面,就是固定写在=
左边,右边就是原始类型,例如:
// 把 void(*)(int, int) 类型的函数指针取别名为 func_ptr
using func_ptr = void(*)(int, int);// 把 int(*)[] 类型的数组指针取别名为 arr_ptr
typedef arr_ptr = int(*)[];
这样语义就明确很多了,程序员一下就看出来新名称是什么,原始类型是什么。
auto
在C++中,auto
关键字可以用来自动推断变量的类型,它在编译时会根据初始化表达式的类型来确定变量的类型。
使用auto
的主要好处是可以简化代码并提高可读性。它可以减少手动指定变量类型的工作,并且可以防止类型错误。相比于显式指定变量类型,使用auto
可以让代码更加灵活和易于维护。
- 自动推断基本类型变量的类型
auto age = 25; // 推断age为int类型
auto salary = 5000.50; // 推断salary为double类型
auto
也可以自动推断指针的类型,比如这样:
int x = 10;
auto y = &x;
此时y
的类型自动判别为int*
。
实际上不建议这么做,C++中最好还是明确每个变量的类型,对于这种简单的类型还是不要用auto
的好。
- 自动推断非常长的类型
std::vector<int> numbers = {1, 2, 3, 4, 5};for (auto it = numbers.begin(); it != numbers.end(); ++it)
{std::cout << *it << " ";
}
有的时候获得变量的类型会需要很长的代码,使用auto
可以缩短变量类型的长度,这是C++推荐的做法,当然用户心里还是要清楚auto
最后接收到了什么类型,只是懒得写出来而已。
- 接受不确定的类型
auto add = [](int a, int b){ return a + b; };
在lambda
表达式中,返回值类型是不确定的,必须用auto
接收。这是因为lambda
设计出来,就是只用一次就不再用的匿名函数,那么用户就无需知道这个表达式的类型,因为拿到类型就可以再去定义相同的函数了,那就不要用lambda
,直接写一个函数/仿函数就行了。因此C++中lambda
的类型是随机生成的,必须用auto
才能接收。
限制性 auto
除去基本的类型推断,auto
可以限制接收到的类型必须是指针或引用。
看到一段代码:
int x = 10;auto* a1 = x;
auto* a2 = &x;
auto a3 = &x;
在auto* a1 = x;
中,x的类型是int,那么auto本应将其值判别为int,但是由于auto*
被*
限制了,此时auto
必须得到一个指针,所以编译器会报错;而auto* a2 = &x;
得到的就是指针,此时代码不会报错,可以正常识别为int*
。
在本质上auto* a2 = &x;
和auto a3 = &x;
的结果是没有区别的,只是auto*
要求得到的必须是一个指针类型,而auto
不限制其类型。
同理auto&
也可以限定类型必须是一个引用,否则会报错。
注意事项
auto
不能作为函数的参数auto
不能用于声明数组
比如以下代码:
int arr1[] = {1, 3, 5, 7, 9};
auto arr2[] = {1, 3, 5, 7, 9};
此时第二条代码就会报错,因为其用auto
类型定义了一个数组。
- 在同一行定义多个变量时,如果将
auto
作为其类型,必须一整行都是同一个类型的变量。
比如以下代码:
int x = 1, y = 2;
auto a = 3, b = 4;
auto c = 5, d = 6.0;
以上代码中,auto a = 3, b = 4;
是合法的,因为一行内都是int类型。
但是auto c = 5, d = 6.0;
是非法的,因为同一行内有不同类型,会报错。
nullptr
在C++11后,推出了新的空指针nullptr
,明明已经有NULL
了,为啥还需要nullptr
?
NULL
在C语言中,表示的是((void*)0)
,也就是被强制转为void*
类型的0。但是在C++中,NULL
就是整数0
比如可以用刚才学的typeid
验证一下:
cout << typeid(NULL).name() << endl;
输出结果为:int
,这下就石锤了NULL
在C++中就是int
。
这会导致不少问题,比如这样:
void func(int x)
{cout << "参数为整型" << endl;
}void func(void* x)
{cout << "参数为指针" << endl;
}int main()
{func(NULL);return 0;
}
以上代码中,func
函数有两个重载,一个是参数为指针,一个是参数为整型。我现在就是想传一个空指针去调用指针版本的func
。但是最后还是会调用int
类型的。
而nullptr
不一样,nullptr
不仅不是整型,而且其也不是void*
。C++给了nullptr
一个专属类型nullptr_t
。这个类型有一个非常非常大的优势,该类型只能转化为其它指针类型,不能转化为指针以外的类型。
比如以下代码:
int x1 = NULL;//正确
int x2 = nullptr;//错误
因为NULL
本质是0,其可以转化为很多非指针类型,比如int
,double
,char
。但是nullptr
是nullptr_t
,它只能转化为其他指针。上述代码中,我们把nullptr
转化为一个int
,此时编译器会直接报错,绝对禁止这个行为。
但是这样是可以的:
void* p1 = nullptr;
int* p2 = nullptr;
char* p3 = nullptr;
double* p4 = nullptr;
可以看到,nullptr
保证了指针类型的稳定,空指针不会被传递到指针以外的类型。因此nullptr
在各方面都有足够的优势,以更加安全的形式给用户提供空指针。
decltype
在C++11以前,有一个关键字typeid
,其可以识别一个类型,并且可以通过name
成员函数来输出类型名。
比如这样:
int i = 0;
int* pi = &i;cout << typeid(i).name() << endl;
cout << typeid(pi).name() << endl;
输出结果为:
int
int * __ptr64
也就是说,我们可以通过typeid
来检测甚至输出变量类型。
而decltype
也是用于识别类型的,但是decltype
与typeid
应用方向不同。
decltype
可以检测一个变量的类型,并且拿这个类型去声明新的类型
比如这样:
int i = 0;
decltype(i) x = 5;
decltype(i)
检测出i
的类型为int
,于是decltype(i)
整体就变成int
,从而定义出一个新的变量x
。
auto
和 decltype
的区别在于,decltype
声明变量可以无需初始化。
int a;
decltype(a) b; // success
auto c = a; // success
auto d; // error
decltype
还可以作为模板参数
例如把lambda
传给std::priority_queue
作为比较条件:
auto comp = [](const std::string& a, const std::string& b) {return a.size() >= b.size();};std::priority_queue<std::string, std::deque<std::string>, decltype(comp)> q(comp);