1 变量和基本类型
1.1 long long 类型
扩展精度浮点数,10位有效数字
1.2 列表初始化
初始化的几种不同形式,其中用花括号来初始化变量称为列表初始化;
比如:
int i = 0;
int i = {0}; // 新特性
int i(0);
int i{0}; // 新特性
需要注意的是,当用于内置类型的变量时,这种初始化形式有一个重要的特点:如果我们使用初始化且初始值存在丢失信息的风险,则编译器报错;
如:
long double Pi = 3.1415926;
int a{Pi}, b = {Pi}; // 错误,编译会做类型检查
int c(Pi); // 正确,编译不会做检查,但存在溢出风险
int d = (Pi); // 正确,但存在溢出风险
运行的时候,a,b则会提示报错信息:error: type 'long double' cannot be narrowed to 'int' in initializer list [-Wc++11-narrowing],这是因为使用long double的值初始化int变量时可能会丢失数据,所以拒绝a和b的初始化请求;虽然c,d虽然没有报错,但是确实丢失了数据;
1.3 nullptr 常量
有几种生成空指针的方法:
int *p1 = nullptr; // 等价于int *p1 = 0;
int *p2 = 0;
int *p3 = NULL; // 等价于int *p3 = 0;
新标准中建议使用nullptr代替NULL来声明空指针。nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型,到这里,大家心里有没有疑问:为什么C++11要引入nullptr?它与NULL相比又有什么不同呢?这就是我们今天要解决的问题。
C/C++中的NULL到底是什么
我们查看一下C和C++的源码,不难发现:
- NULL在C++中的定义
NULL在C++中被明确定义为整数0:
/* Define NULL pointer value */
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else /* __cplusplus */
#define NULL ((void *)0)
#endif /* __cplusplus */
#endif /* NULL */
- NULL在C中的定义
在C中,NULL通常被定义为如下:
#define NULL ((void *)0)
也就是说NULL实质上是一个void *指针。
那么问题又来了,我们从一开始学习C++的时候就被告诫C++是兼容C的,为什么对于NULLC++却不完全兼容C呢?通过查找维基百科,才发现这其中的原因。
简单地说,C++之所以做出这样的选择,根本原因和C++的函数重载机制有关。考虑下面这段代码:
void Func(char *);
void Func(int);
int main()
{
Func(NULL);
}
如果C++让NULL也支持void *的隐式类型转换,这样编译器就不知道应该调用哪一个函数。
为什么要引入nullptr
C++把NULL定义为0,解决了函数重载后的函数匹配问题,但是又引入了另一个“问题”,同样是上面这段代码:
void Func(char *);
void Func(int);
int main()
{
Func(NULL); // 调用Func(int)
}
由于我们经常使用NULL表示空指针,所以从程序员的角度来看,Func(NULL)应该调用的是Func(char *)但实际上NULL的值是0,所以调用了Func(int)。nullptr关键字真是为了解决这个问题而引入的。
另外我们还有注意到NULL只是一个宏定义,而nullptr是一个C++关键字。
nullptr如何使用
nullptr关键字用于标识空指针,是std::nullptr_t类型的(constexpr)变量。它可以转换成任何指针类型和bool布尔类型(主要是为了兼容普通指针可以作为条件判断语句的写法),但是不能被转换为整数。
char *p1 = nullptr; // 正确
int *p2 = nullptr; // 正确
bool b = nullptr; // 正确. if(b)判断为false
int a = nullptr; // error
1.4 constexpr 变量
将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式;
声明为constexpr的变量一定是一个常量,而且必须用常量表达式来初始化,比如说下面的情况则是不正确的:
int t = 10;
constexpr int q = t + 20;
cout << "q" << q << endl;
需要将t声明为 const
才是正确的;
一般来说,如果你认定变量是一个常量表达式,那就把它声明为constexpr类型;
1.5 类型别名声明
使用类型别名可以使复杂的类型名字变得更简单明了,易于理解和使用;
现在有两种方法可以用来定义类型别名,一种是 typedef
,另一种则是新标准中的 using
;
1.6 auto 类型指示符
随着C++11标准的发布,C++语言也引入了类型自动推断的功能,这就是我们今天要介绍的auto关键字
- C++是一种强类型语言,声明变量时必须明确指出其类型。但是,在实践中,优势我们很难推断出某个表达式的值的类型,尤其是随着模板类型的出现,要想弄明白某些复杂表达式的返回类型就变得更加困难。为了解决这个问题,C++11重新定义了auto关键字的语义,用于进行自动类型推断。为什么说“重新定义”呢?auto关键字在C++98中已经出现,用于自动变量的声明,但是极少被使用(关于此定义,可以查看这里)。
auto让编译器通过初始值来推算变量的类型,所以,其定义的变量必须要有初始值;
使用auto也能在一条语句中声明多个变量;因为一条声明语句只能有一个基本数据类型,所以该语句中所有的变量的初始基本数据类型都必须是一样的;
C++引入auto关键字主要有两种用途:一是在变量声明时根据初始化表达式自动推断该变量的类型,二是在声明函数时作为函数返回值的占位符。
自动类型推断
使用auto从初始化表达式中推断出变量的数据类型,可以大大简化我们的编程工作,特别是对于一些类型冗长复杂的变量。比如,在以前的做法中,对于vector类型的变量,如果我们需要获取它的迭代器,我们需要这样声明vector::iterator iter,而使用auto关键字后我们可以让编译器帮我们推断出迭代器的具体类型。另外,在模板函数定义时,如果变量的类型依赖于模板参数,我们也很难确定变量的类型,使用auto关键字则可以把这些“脏活累活”交给编译器完成。
下面看一些简单的例子:
#include <iostream>
#include <vector>
using namespace std;
template<class T, class U>
void add(T t, U u)
{
auto s = t + u;
cout << "type of t + u is " << typeid(s).name() << endl;
}
int main()
{
// 简单自动类型推断
auto a = 123;
cout << "type of a is " << typeid(a).name() << endl;
auto s("fred");
cout << "type of s is " << typeid(s).name() << endl;
// 冗长的类型说明(如迭代器)
vector<int> vec;
auto iter = vec.begin();
cout << "type of iter is " << typeid(iter).name() << endl;
// 使用模板技术时,如果某个变量的类型依赖于模板参数,使用auto确定变量类型
add(101, 1.1);
}
结果:
type of a is i
type of s is PKc
type of iter is N9__gnu_cxx17__normal_iteratorIPiSt6vectorIiSaIiEEEE
type of t + u is d
函数返回值占位符
在这种情况下,auto主要与decltype关键字配合使用,作为返回值类型后置时的占位符。此时,关键字不表示自动类型检测,仅仅是表示后置返回值的语法的一部分。
template<class T, class U>
auto add(T t, U u) -> decltype(t + u)
{
return t + u;
}
注意事项
1、使用auto关键字的变量必须有初始值。
2、可以使用valatile,*(指针类型说明符),&(引用类型说明符),&&(右值引用)来修饰auto关键字。
auto a = 10;
auto *pa = new auto(a);
auto **rpa = new auto(&a);
cout << typeid(a).name() << endl; // 输出: int
cout << typeid(pa).name() << endl; // 输出: int *
cout << typeid(rpa).name() << endl; // 输出: int **
3、函数参数和模板参数不能被声明为auto。
4 、使用auto关键字声明变量的类型,不能自动推导出顶层的CV-qualifiers和引用类型,除非显示声明。例如:
- 使用auto关键字进行类型推导时,如果初始化表达式是引用类型,编译器会去除引用,除非显示声明。例如:
int i = 10;
int &r = i;
auto a = r;
a = 13; // 重新赋值
cout << "i = " << i << " a = " << a << endl; // 输出i=10,a=13
// 显式声明
auto &b = r;
b = 15; // 重新赋值
cout << "i = " << i << " b = " << b << endl; // 输出i=15,a=15
- 使用auto关键字进行类型推导时,编译器会自动忽略顶层const,除非显示声明。例如:
const int c1 = 10;
auto c2 = c1;
c1 = 11; // 报错,c1为const int类型,无法修改const变量
c2 = 14; // 正确,c2为int类型
// 显示声明
const auto c3 = c1;
c3 = 15; // 报错,c3为const int类型,无法修改const变量
5、对于数组类型,auto关键字会推导为指针类型,除非被声明为引用。例如:
int a[10];
auto b = a;
cout << typeid(b).name() << endl; // 输出:int *
auto &c = a;
cout << typeid(c).name() << endl; // 输出:int [10]
- 顶层const:指针本身是一个常量;
- 底层const:指针指向的对象是一个常量;
int main() {
int i = 0;
const int ci = i;
auto b = &i; // b是一个整形指针(整数的地址就是指向整数的指针)
auto c = &ci; // c是一个指向整数常量的指针(对常量对象取地址是一种底层const)
return 0;
}
1.7 decltype 类型指示符
有时候会有这样的需求,我们需要知道一个表达式的类型,并使用该类型去定义一个变量,例如:
int a = 10;
int b = 20;
auto c = a + b; // OK a+b的类型是int,此时c的类型是int,并且c的值是 a+b
auto可以解决部分问题,例如我们定义的变量的类型就是表达式 a+b 的类型,但是如果我们仅仅需要定义一个与表达式 a+b 的类型相同的变量,但是我们又不希望将表达式a+b的值赋值给刚刚定义的变量,我们希望赋另外一个值或者是仅仅定义变量而不赋值呢。 这就需要用到C++11 提供的另一个类型说明符 decltype了。decltype作用于一个表达式,并且返回该表达式的类型,在此过程中编译器分析表达式的类型,并不会计算表达式的值。例如
int a = 10;
int b = 20;
decltype(a+b) c = 50; // OK c的类型就是 a+b 的类型int
对于引用类型decltype有一些特别的地方:
nt a = 20 ;
int &b = a;
decltype(b) c ; // Error c是引用类型必须赋值
decltype(b) d = a; // OK d是引用类型,指向a
可以看到decltype如果作用于一个引用类型,其得到的还是一个引用类型。我们知道一个引用类型在使用的时候一般会当作其关联的那个变量的同义词处理,例如如果使用 cout<<b<<endl; 其中b实际上相当于a,但是decltype作用于引用类型的时候会保留引用性质。
如果一个表达式是一个解指针引用的操作,decltype得到的也是一个引用类型:
int a = 20 ;
int *p = &a;
decltype(*p) c = a; // c的类型是int&
c = 50;
cout<<a<<endl; // 输出50
当decltype作用于一个变量的时候,变量加不加括号是有区别的,例如:
int i = 0;
decltype((i)) a; //报错,因为a类型为 int&,必须进行初始化
decltype((i)) a = 30; //正确,必须进行初始化
decltype(i) b; //正确
需要注意的是,decltype((variable))的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用,其实就是根据它的类型决定; 加上括号之后编译器会把(a)当作是一个表达式处理,而变量是一种可以作为赋值语句左值的表达式,所以会解释成引用类型。