基本内置类型
- 算术类型
- 字符
- 整型
- 布尔值
- 浮点数
- 空类型(void)
算术类型
类型 | 说明 | 最小尺寸 | 测试尺寸 |
---|---|---|---|
bool | 布尔类型(true、false) | 未定义 | 1 Byte |
char | 字符 | 1 Byte | 1 Byte |
wchar_t | 宽字符 | 2 Byte | 4 Byte |
char16_t | Unicode 字符 | 2 Byte | 未测试 |
char32_t | Unicode 字符 | 4 Byte | 未测试 |
short | 短整型 | 2 Byte | 2 Byte |
int | 整型 | 2 Byte | 4 Byte |
long | 长整型 | 4 Byte | 8 Byte |
long long | 长整型(C++ 11) | 8 Byte | 8 Byte |
float | 单精度浮点数 | 6位有效数字 | 4 Byte,一般7位有效数字 |
double | 双精度浮点数 | 10位有效数字 | 8 Byte,一般16位有效数字 |
long double | 扩展精度浮点数 | 10 位有效数字 | 16 Byte,具体实现不同精度不同 |
-
带符号类型和无符号类型
- int、short、long、 long long都是带符号的,通过类型前面添加unsigned 得到无符号类型,如 unsigned int,可以缩写为unsigned。
- char 类型 带符号的 signed char 和不带符号的unsigned char ,char 类型具体为哪一种和编译器有关。
- 无符号类型所有bit 都被用来表示数字,有符号的最高位为符号位
-
如何选择类型
- 当数值不可能为负时 ,选用无符号类型
- 使用int执行整数运算。
- 算术表达中不要 使用char 或bool,只有在存放字符或布尔值才使用。如果需要一个不大的整数,明确指出类型是signed char 还是unsigned char
- 执行浮点运算使用double,因为float精度不够而且单双精度浮点运算代价相差无几。
类型转换浅谈
当在程序中我们使用一种类型而其实对象应该取另一种类型时,程序会自动进行类型转换,当我们像下面这样把一种算术类型的值赋给另外一种类型时:
bool b=42; //b 位true
int i= b; //i 为1
i =3.14 ; //i 为3
double pi=i ;//pi 为3.0
unsigned char c =-1; //char 占一个字节时,c 值为255
signed char c2=256 ;//char 占一个字节时, c 未定义
类型所能表示的值得范围决定了转换的过程:
- 非布尔类型赋值给布尔类型时,初始值为0 ,结果为false ,否则结果为true。
- 布尔类型赋值给非布尔类型时,初始值为false ,结果为0,初始值为true 则结果为1。
- 浮点数赋值给整型时,将仅保留浮点数中小数点之前的整数部分。
- 整数赋值给浮点数时,小数部分记为0,若整数所占的空间超过浮点类型的容量,精度可能损失。
- 当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。
- 当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的。
含有无符号类型的表达式:
- 尽管我们不会故意给无符号对象赋一个负值,却可能写出这样的代码
- 当一个表达式中既有无符号数又有int值时,int值会被转为为无符号数。
unsigned u=10;
int i= -42;
std::cout <
注意: 不要混用带符号类型和无符号类型
字面值常量
什么是字面值常量? 形如42这样的值就是,一看就知道是多少,每种字面值常量对应一种数据类型,字面值常量的形式和值决定了它的数据类型。
-
整型和浮点型字面值
整型字面值可以写作十进制数,八进制数或者16进制的形式,以0开头的代表八进制数,以0x或0X开头的代表十六进制数。- 十进制字面值的类型是int ,long,long long,中尺寸最小的那个(例如,三者当中最小是int),当然前提是这种类型要能容纳下当前的值。
- 八进制和十六进制字面值类型是能够容纳其数值的int,unsigned int,long ,unsigned long, long long和unsigned long long 中的最小者。如果一个字面值连与之相连的最大数据类型都放不下,将产生错误。
- 尽管整型字面值可以存储在带符号数据类型中,但严格来说,十进制字面值不会是负数,它的作用仅仅是对字面值取负值而已。
- 浮点型字面值是一个double
字符和字符串字面值
由单引号括起来的一个字符称为char型字面值,双引号括起来的0个或多个字符则构成字符串型字面值。
'a' //字符字面值
“hello world!” //字符串字面值
字符串字面值的类型实际上是由常量字符构成的数组(array),编译器在每个字符串的结尾处添加一个空字符('\0')转义序列
有两类字符程序员不能直接使用:一类是不可打印字符,如退格或其他控制字符,因为他们没有可视的图符,另一类是语言中有特殊含义的字符,(单引号,双引号,问好,。。)。在这些情况下需要用到转义序列:
名称 | 形式 | 名称 | 形式 | 名称 | 形式 |
---|---|---|---|---|---|
换行符 | \n | 横向制表符 | \t | 报警(响铃)符 | \a |
纵向制表符 | \v | 退格符 | \b | 双引号 | " |
反斜线 | |问号 | ? | 单引号 | \’ | |
回车符 | \r | 进纸符 | \f |
在程序中,转义序列被当做一个字符使用。
- 指定字面值的类型
字符和字符串字面值
前缀 | 含义 | 类型 |
---|---|---|
u | Unicode16字符 | char16_t |
U | Unicode32字符 | char32_t |
L | 宽字符 | wchar_t |
u8 | UTF-8 | char |
整型字面值
后缀 | 最小匹配类型 |
---|---|
u or U | unsigned |
l or L | long |
ll or LL | long long |
浮点型字面值
后缀 | 最小匹配类型 |
---|---|
f or F | float |
l or L | long double |
ex:
L'a' // 宽字符型字面值,类型时wchar_t
u8"hi!" //utf-8 用8位编码一个unicode 字符
42ULL //无符号整型字面值,类型是unsigned long long
1E-3F // 单精度浮点型字面值,类型是float
变量
变量对应一个可供操作的存储空间,C++中的每个变量都有其数据类型,数据类型决定着变量所占内存空间的大小和布局方式,该空间能存储的值得范围,以及变量能参与的运算。对C++程序员来说,“变量”和“对象”一般可以互换使用。
变量的定义
基本形式: 类型说明符 随后紧跟一个或多个变量名组成的列表,以逗号分隔,分号结束定义。列表中每个变量名的类型都由类型说明符指定,定义时还可以为一个或多个变量赋初值:
int a=0,b,c=0; // abc 都为int 类型
Sales_item item; //item 的类型是sales_item 自定义类型
std::string book("0-12-33-x"); //book 通过一个string 字面值初始化,string 库类型
初始值
当对象(变量)在创建时获得了一个特定的值,我们说这个对象被初始化了。用于初始化变量的值可以是任意复杂的表达式。当一次定义了两个或多个变量时,对象的名字随着定义也就马上可以使用了。因此在同一条定义语句中,可以用先定义的变量值去初始化后定的其他变量。
在C++中,初始化是一个异常复杂的问题,很多程序员对于使用等号来初始化变量的方式倍感困惑,这种方式容易让人认为初始化是赋值的一种。事实上,初始化和复制是两个完全不同的操作,然而在很多编程语言中二者的区别几乎可以忽略不计,即使在C++中有时这种区别也无关紧要,所以人们特别容易吧二者混为一谈。但是,这是两种不同的操作,这个概念很重要。
- 初始化:创建变量时赋予其一个初始值。
- 赋值: 把对象(变量)的当前值擦除,以一个新值来替代。
列表初始化
C++语言定义了初始化的好几种不同形式,这也是初始化问题复杂性的一个体现。例如,要想定义一个名为units_sold 的int变量并初始化为0,以下的4条语句都可以做到这一点:
int units_sold = 0;
int units_sold = {0} ;
int units_sold{0};
int units_sold(0);
作为C++ 11 新标准用花括号来初始化变量得到了全面应用,而在此之前,这种初始化的形式仅在某受限的场合下才能使用。这种初始化的形式被称为列表初始化。现在无论是初始化对象还是某些时候为对象赋新值,都可以使用这样一组由花括号括起来的初始值了。
当用于内置类型的变量时, 这种初始化形式有一个重要特点:如果我们使用列表初始值存在丢失信息的风险,则编译器将报错:
long double ld =3.1415925;
int a{ld}, b={ld}; //错误: 转换未执行,因为存在丢失信息的危险
int c(ld), d=ld; //正确: 转换执行,且确实丢失了部分值
使用long double 值初始化int变量时可能丢失数据,所以编译器拒绝了a 和 b 的初始化请求。其中,至少ld的小数部分会丢失掉,而且int也可能存不下ld的整数部分。
默认初始化
如果定义变量时没有指定初值,则变量被默认初始化(default initialized),此时变量被赋予了“默认值”。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。
如果是内置类型的变量未被显示初始化,它的值由定义的位置决定。定义任何函数体之外的变量被初始化为0。然而,一种例外情况是,定义在函数体内部的内置类型变量将不被初始化。一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其他形式访问此类值将引发错误。
每个类各自决定其初始化对象的方式。而且,是否允许不经初始化就定义对象也由类自己决定。如果类允许这种行为,他将决定对象的初始值到底是什么。
绝大多数类都支持无需显示初始化而定义对象,这样的类提供了一个合适的默认值:
std::string empty; //empty 非显式地初始化为一个空串
Sales_item item; // 被默认初始化的Sales_item 对象
一些类要求每个对象都显式初始化,此时如果创建了一个该类的对象而未对其做明确的初始化操作,将引发错误。
变量声明和定义的关系
为了允许把程序拆成多个逻辑部分来编写,c++语言支持分离式编译(separate compilation) 机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。
如果将程序分为多个文件,则需要在文件间共享代码的方法。 例如,一个文件的代码可能需要使用另一个文件中定义的变量。
为了支持分离式编译,c++语言将声明和定义区分开来,声明使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义负责与名字关联的实体。
变量声明规定了变量的类型和名字,在这一点上定义与之相同,但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。如果想声明一个变量而非定义它,就在变量名前添加关键字 extern,而且不要显式地初始化变量:
extern int i; //声明 i 而非定义 i
int j; //声明并定义 j
任何包含了显式初始化的声明即成为定义。我们能给由extern 关键字标记的变量赋一个初始值,但是这么做也就抵消了 extern 的作用。extern 语句如果包含初始值就不再是声明,而变成定义了:
extern double pi =3.1416; // 定义
在函数体内部,如果试图初始化一个由extern 关键字标记的变量,将引发错误。
变量能且只能被定义一次,但是可以被多次声明。
注意:c++是一种静态类型语言,含义是在编译阶段检查类型,其中,检查类型的过程称为类型检查。
标识符
C++ 的标识符由字母,数字和下划线组成,其中必须以字母或下划线开头。标识符的长度没有限制,但是对大小写字母敏感:
// 定义4个不同的int 变量
int somename , someName ,SomeName,SOMENAME;
用户自定义的标识符中不能连续出现两个下划线,也不能以下划线紧连大写字母开头。此外,定义在函数体外的标识符不能以下划线开头。
变量命名规范
- 标识符要能体现实际含义
- 变量名一般用小写字母,如 index,不要使用Index 或INDEX
- 用户自定义的类名一般以大写字母开头,如Sale_item
- 如果标识符由多个单词组成,则单词之间应有明显区分,如 student_loan 或 studenLoan ,不要使用studentloan
名字的作用域
不论是在程序的什么位置,使用到的每个名字都会指向一个特定的实体: 变量、函数、类型等。然而,同一个名字如果出现在程序的不同位置,也可能指向的是不同的实体
作用域是程序的一部分,在其中名字有其特定的含义。C++语言中大多数作用域都以花括号分隔。
同一个名字在不同的作用域中可能指向不同的实体,名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束
#include
int main()
{
int sum=0;
//sum 用于存放从1 到10 所有数的和
for(int val =1 ;val <=10;++val)
sum+=val; //等价于sum =sum+val
std::cout <<"Sum of 1 to 10 inclusive is "
<
这段程序定义了3个名字:main、sum 和 val ,同时使用了命名空间名字std,该空间提供了2个名字cout 和 cin 供程序使用。
名字main 定义所有花括号之外,他和其他大多数定义在函数体之外的名字一样拥有全局作用域,一旦声明之后,在整个程序的范围内都可以使用。名字sum定义在mian函数所限定的作用域之内,从声明sum开始直到main函数结束为止都可以访问它,但是除了main函数所在的块就无法访问了,因此说变量sum拥有块作用域,名字val定义在for语句内,在for语句内可以访问val,但是在main函数的其它部分就不能访问它了。
建议:第一次使用变量时再去定义它
嵌套的作用域
作用域能彼此包含,被包含(或者说被嵌套)的作用域称为内层作用域,包含着别的作用域的作用域称为外层作用域。
作用中一旦声明名了某个名字,它所嵌套着的所有作用域都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字:
#include
//该程序仅用于说明:函数内部不宜定义与全局变量同名的新变量
int reused = 42; //reused 拥有全局作用域
int main()
{
int unique =0 ;//unique 拥有块作用域
std::cout <
第一次输出全局变量reused值,第二次输出局部变量reused值,第三次输出全局变量reused值。 因为全局作用域本身并没有名字,所以,当作用域操作符的左侧为空是,向全局作用域发出请求获取作用于操作符右侧名字对应的变量。
Note:如果函数有可能用到某个全局变量,则不宜再定义一个同名的局部变量
复合类型
复合类型 是指基于其他类型定义的类型。C++中有好几种复合类型,本章将介绍其中的两种: 引用和指针
与我们已经掌握的变量声明相比,定义复合类型的变量要复杂很多。
引用
引用为对象起了另外一个名字,引用类型引用(refers to)另外一种类型。通过将声明符写成&d的形式来定义引用类型,其中d就是声明的变量名;
int ival =1024;
int &refval=ival; //refvel 指向ival(是ival 的另一种类型)
int &refval2; //错误: 应用必须被初始化
一般在初始化变量时,初始值就会被拷贝到新建的对象中,然而定义引用时,程序把引用和其初始值绑定在一起,而不是将初始值拷贝给引用,而且无法令引用重新绑定到另一个对象,因此引用必须初始化。
引用即别名:就是为变量新起了一个名字
引用的定义
允许在同一条语句内定义多个引用,其中每个引用标识符都必须以符号&开头
int i=1024,i2=2048; //i 和i2 都是int
int &r = i,r2= i2; //r 是一个引用,与 i绑定在一起,r2 int
int i3=1024,&ri=i3;
int &r3=i3,&r4 = i2;
引用的类型要和与之绑定对象严格匹配,引用只能绑定在对象上,不能与字面值或某个表达式的计算结果绑定在一起,
int &refval4=10; //error
double dval=22.22;
int &ff=dval; //error
指针
指针(pointer)是 "指向"另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问
指针与引用相比有很多不同点:
- 指针是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内可以先后指向多个不同的对象
- 指针无需再定义时赋初值
- 和其他内置类型一样,在块作用域内定义的指针如果没有初始化,也将拥有一个不确定的值。
定义指着类型的方法将声明符写成d的形式,其中d是变量名。如果在一条语句定义了多个指针变量,则每个变量前面都必须有符号:
int *p1,*p2; //声明了两个int型对象的指针
double dp,*dp2;//dp 是double 对象,dp2 是指向double类型的指针
获取对象的地址
指针存放某个对象的地址,要想获取该地址,需要使用取地址符(&)
int vial =42;
int *p=&vial; // p 存放变量ival 的地址
由于引用不是对象,没有自己的地址,所以不能定义指向引用的指针。
指针的类型都要和其所指向的对象严格匹配,除了一些特殊情况,譬如 void * 。
指针的值应是下面几种状态之一:
- 指向一个对象
- 指向紧邻对象所占空间的下一个位置
- 空指针,没有指向任何对象
- 无效指针,上述情况之外的其他值
试图拷贝或以其他方式访问无效指针的值都将引发错误,编译器并不负责检查此类错误,这一点和试图使用未经初始化的变量是一样的。访问无效指针的后果无法对象。
利用指针访问对象
如果指针指向了一个对象,则允许使用解引用符()*来操作对象:
int ival =42;
int *p =&ival; //p 存放着变量ival 的地址,或者说p 是指向变量ival 的指针
count << *p; // 42
对指针解引用将会得出所指的对象,因此如果将解引用的结果赋值,实际上也就是给指针所指的对象赋值:
*p = 0; //由符号*得到指针p 所指的对象,即可经由 P为变量ival 赋值
count <<*p ; //0
由此可以得出: 某些符号具有多重含义,如 & 。
空指针
空指针 不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。下面列出几个生成空指针的方法:
int *p1 =nullptr; //c++ 新标准刚引入的一种方法,nullptr 是一种特殊类型的字面值。可以被转化成任意其他的指针类型
int *p2=0;
int *p3 = NULL; //需要 #include cstdlib
初始化所有指针:使用未经初始化的指针是引发运行时错误的一大原因
赋值和指针
指针和引用都能提供对其他对象的简介访问,然而在具体实现细节上二者有很大不同,其中最重要的一点就是引用本身并非一个对象。
有时候想要搞清楚一条赋值语句到底是改变了指针的值还是改变了指针所指对象的值不太容易,最好的办法就是记住赋值永远改变的是等号左侧的对象。
pi=&val; // pi的值改变,pi 指向了ival
* pi =0; //ival 的值改变,指针pi并没有改变
其他指针操作
只要指针拥有一个合法值,就能将他用在条件表达式中,和采用算术值作为条件遵循的规则类似,如果指针的值为0,条件取false。
void * 指针
void* 指针是一种特殊的指针类型,可用于存放任意对象的地址,一个 void* 指针存放着一个地址,这一点和其他指针类似,但不同的是,我们对该地址中到底是个什么类型的对象并不了解:
double obj =3.14, * pd=&obj;
void *pv=&obj; //void 指针类型可以存放任意类型的对象地址
pv=pd;
由于不知道void * 指针所存的对象的具体类型,所以无法直接操作void 指针所指的对象,从void * 的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。
理解复合类型的声明
如前所述,变量的定义包括一个基本数据类型 (base type) 和一组声明符。在同一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式却可以不同。也就是说,一条定义语句可能定义出不同类型的变量:
// i int , p int 型指针,r int 型引用
int i=1024,* p= &i, &r =i;
很多程序员容易迷惑于基本数据类型和类型修饰符的关系,其实后者不过是声明符的一部分罢了。
定义多个变量
经常有一种观点会误以为,在定义语句中,类型修饰符(* 或 &)作用与本次定义的全部变量。原因之一是由于可以把空格写在类型修饰符和变量名之间:
int * p; //合法但是容易产生误导
这种写法可能产生误导是因为int * 放在一起好像是这条语句中所有变量共同的类型一样。其实恰恰相反,基本数据类型是int 而非 int * 。 * 仅仅是修饰了 P 而已,对该声明语句中的其他变量,它并不产生任何副作用:
int * p1, p2; //p1 int 型指针,p2 int
涉及指针或引用的声明,一般有两种写法,第一种把修饰符和变量标识符写在一起:
int *p1, *p2; //强调变量具有的复合类型
第二种把修饰符和类型名写在一起,并且每条语句只定义一个变量:
int* p1;
int* p2; //着重强调本次声明定义了一种复合类型;
tips:上面两种写法没有什么谁对谁错,关键是选择并坚持其中一种写法,不要老是变来变去
指向指针的指针
一般来说, 声明符中修饰符的个数并没有限制,当有多个修饰符连写在一起时,按照其逻辑关系加以解释即可。以指针为例,指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。
通过*的个数可以区分指针的级别,也就是说,两个表示指向指针的指针,三个表示指向指针的指针的指针,以此类推。
int ival=1024;
int *pi=&ival;
int **ppi=π
指向关系如下:
ppi>>>pi>>>ival(1024)
cout <<"The Value of ival \n"
<<"direct value :" <
上面使用三种不同方式输出了变量ival的值。
指向指针的引用
引用本身不是一个对象,因此不能定义指向引用的指针,但是指针是对象,所以存在对指针的引用:
int i=42;
int *p; //p 是一个int 型指针
int * &r=p; //r 是一个对指针p的引用
r =&i; //r 引用了一个指针,因此给r赋值&i就是p指向i
*r =0; //*p=0,i=0
const 限定符
有时候我们希望定义这样一种变量,它的值不能被改变。为了满足这一要求,可以用关键字const 来对变量的类型加以限定。
const int buffsize =512 ; // 输入缓冲区大小
这样就把bufsize定义为了一个常量,任何试图为bufsize 赋值的行为都将引发错误:
bufsize =512; //错误,试图向const 对象写值
因为const对象一旦创建后其值就不能在改变,所以const 对象必须初始化。一如既往,初始化可以是任意复杂的表达式:
const int i=get_size(); // 正确 运行时初始化
const int j=42; //编译时初始化,正确
const int k; //error ,k 是一个未经初始化的常量
初始化和const
正如之前所反复提到的,对象的类型决定了其上的操作。与非const 类型所能参与的操作相比,const类型的对象能完成其中大部分,但也不是所有的操作都适合,主要的限制就是只能在const 类型的对象上执行不改变内容的操作。 例如, const int 和普通的int一样都能参与算术运算,也都能转换成一个布尔值。。。。。。
注意: 默认情况下const 对象仅仅在本文件内有效
当以编译时初始化的方式定义一个const 对象时,就如对bufsize的定义一样:
const int bufsize =512; // 输入缓冲区大小
编译器将在编译过程中把用到的该变量的地方都替换成相应的值,也就是说,编译器会找到代码中所有用到bufsize的地方,然后用512 替换。
为了执行上述转换,编译器必须知道变量的初始值,如果包含多个文件,就必须在每一个用到变量的文件中都有对它的定义。为了支持这一用法,同时避免对同一个变量的重复定义,默认情况下,const 对象被设定为仅在本文件内有效。当多个文件中同时出现了同名的const 变量时,其实等同与在不同文件中分别定义了独立的变量。
有时候有这样一种const 变量,它的初始值不是一个常量表达式,但又确实有必要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量。相反,我们想让这种const 对象像其他(非常量)对象一样工作,也就是说,只在一个文件内定义const,而在其他多个文件中声明并使用它。
解决的方法是:对于const 变量不管是声明还是定义都要添加** extern ** 关键字,这样只需要定义一次就可以了;
//file_1.cc 定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufsize =fcn();
// file_1.h 头文件
extern const int bufsize ; //与上面定义的bufsize是同一个
如上述程序所示,file_1.cc 定义并初始化了bufsize,因为这条语句包含了初始值,所以它是一次定义,然而,因为bufsize 是一个常量,必须用extern 加以限定才能被其他文件使用。
file_1.h 头文件中的声明也由extern 做了限定,其作用是指明bufsize 并非本文件独有,它的定义将在别处出现。
如果想要在多个文件之间共享const 对象,必须在变量的定义之前添加extern关键字
const 的引用
可以把引用绑到const 对象上,就像绑定到其他对象一样,称之为对常量的引用,与普通引用不同之处在于,对常量的引用不能用作修改它所绑定的对象: 就是说不能更改
const int ci =104;
const int &refci =ci ;
refci =42; //error 不能改
int &refci2 =ci; //错误: 试图让一个非常量引用指向一个常量对象
常量引用是对const 的引用
初始化和对cosnt 的引用
前面所述,引用的类型必须与其所引用对象的类型一致植,但是有两个例外
- 第一种例外是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可,尤其允许为一个常量引用绑定非常量的对象,字面值或是个一般表达式
int i=42;
const int &r1 =i ;// 允许将const int& 绑定到一个普通int 对象上
const int &r2 =42; // ok
const int &r3 =r1*2; //ok
int &r4= r1* 32; //error
要想理解这种例外情况的原因,最简单的方式是弄清楚当一个常量引用绑定到另外一个类型上时到底发生了什么:
double dval =3.14; //由双精度浮点数生成一个临时的整型常量
const int &ri =dval;
此时ri 引用了一个int 型的数,对ri的操纵应该是整数运算,但是dval 是一个双精度浮点数而不是整数,因此为了确保让ri绑定一个整数,编译器把上述代码变成了如下形式:
const int temp =dval ; //由 双精度浮点数生成一个临时的整型变量
const int & ri =temp ;// ri 绑定这个临时变量
在这种情况下,ri 绑定了一个临时量对象,所谓临时量 对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。
接下来谈论当ri不是常量时,如果执行了类似于上面的初始化过程将带来什么样的后果,如果ri不是常量,就允许对ri赋值,这样就会改变ri所引用对象的值。注意,此时绑定的对象是一个临时量而非dval。程序员既然让ri引用dval,就肯定想通过ri改变dval的值,否则干什么要给ri赋值呢?如此看来,既然大家基本上不会想着把引用绑定到临时变量上,C++ 语言也就把这种行为归为非法。
对const 的引用可能引用一个并非const的对象
必须认识到,常量引用仅对引用可参与的操纵做出了限定,对于引用的对象本身是不是一个常量未做限定,因为对象也可能是个非常量,所以允许通过其他途径改变它的值:
int i=42;
int &ri=i;
const int &r2 =i;
r1=0;
r2=0; //错误,r2是一个而常量引用
指针和const
与引用一样,类比常量引用,指向常量的指针不能用于改变所指对象的值,要想存放常量对象的地址,只能使用指向常量的指针:
const double pi =3.14; //pi 是个常量,它的值不能改变
double * ptr =π //错误
const double *cptr =π //ok
* cptr =42; //error
如前文所说,指针的类型必须与其所指对象的类型一致,但是有两个例外,第一种例外情况是允许另一个指向常量的指针指向一个非常量对象:
double dval=3.14;
cptr=&dval; //right ,but can't modified dval by cptr;
和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量,所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。
const 指针
指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量,常量指针必须初始化,一旦初始化,它的值就不能在改变了,把*放在const关键字之前用来说明指针是一个常量,这样的书写形式意味着指针本身的值是不变的而并非指向的那个值:
int errnumb=0;
int *const curerr =&errnumb ; //curerr 将一直指向errnumb
const double pi=3.14;
const double * coust pip=π //pip 是一个指向常量对象的常量指针
顶层const
如前所述,指针本是是一个对象,它又可以指向另外一个对象,因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题,用名词顶层const和底层const来分别表示这两个问题:
- 顶层const: 表示指针本身是个常量
- 底层const: 表示指针所指的对象是一个常量
更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型,类、指针等。底层const则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层const 也可以是底层const,这一点和其他类型相比区别明显:
int i=0;
int *const p1=&i; //顶层const
const int ci =42; //顶层const
const int *p2=&ci; //底层const
const int * const p3=p2;
const int &r =ci; //用于声明引用的const 都是底层const
当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的类型必须能够转换,一般来说,非常量可以转化成常量,反值则不行:
int * p=p3; //错误:p3 包含底层const 定义,p没有
p2=p3; //ok
p2=&i; //ok
int &r=ci ;// error 普通的int& 不能绑定到int常量上
const int &r2 =i ; //正确: const int & 可以绑定到一个普通int 上
constexpr 和常量表达式
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式,显然字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。后面将会提到,C++语言中有几种情况下是要用到常量表达式的。
一个对象(表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如:
const int max_files =20 ;//是常量表达式
const int limit =max_files +1 ; //limit 是常量表达式
int staff_size =27 ; //staff_size 不是常量表达式
虽然不能使用普通函数作为constexpr 变量的初始值,但是新标准定义了一种特殊的constexpr 函数,这种函数应该足够简单以使得编译时可以计算器结果,这样就能用constexpr函数去初始化cosntexpr变量了。
字面值类型
常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见,容易得到,就把它们称为“字面值类型”(literal type)。
目前为止所接触到的数据类型中,算术类型,引用和指针类型,都属于字面值类型,而自定义类sales_item、IO 库等类型不属于字面值类型。
尽管指针和引用都能被定义为constexpr,但它们的初始值却受到严格限制,一个consteptr指针的初始值必须是nullptr或0,或者是存储于某个固定地址的变量(对象)(全局变量或者局部变量中的静态变量(staitc 修饰的))。
函数体内定义的变量一般并非固定的地址,因此constexpr指针不能指向这样的变量,相反的,定义与所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针,函数体内定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址,因此,constepr引用能绑定到这样的变量上,cosntexpr指针也能指向这样的变量。
指针和constexpr
在constexpr声明中如果定义了一个指针,限定符constexpr进对指针有效,与指针所指的对象无关:
const int * p=nullptr; // p 是一个指向整型常量的指针
constexpr int * q =nullptr;// q 是一个指向整数的常量指针
处理类型###
类型别名
类型别名是一个名字,有两种方法可用于定义类型别名,传统的方法是试用关键字typedef:
typedef double wages ; //wages 是double的同义词
typedef wages base , *p ; //base 是double的同义词,p是double * 的同义词
新标准规定一种新的方法,试用别名声明来定义类型的别名:
using SI =Sales_item; //SI 是Sales_item 的同义词
这种方法用关键字using作为别名声明的开始,其后紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名。
类型别名和类型名字等价,只要是类型的名字能出现的地方,就能使用类型别名。
指针、常量和类型别名
如果某个类型别名指的是复合类型或常量,那么把它用到声明语句中就会产生想不到的后果,如下:
typedef char * pstring ;
const pstring cstr =0 ;//cstr 是指向char 的常量指针
const pstring * ps ; //ps 是一个指针,它的对象是指向char 的常量指针
pstring 实际上是指向char的指针,因此,const pstring 就是指向char 的常量指针,而非指向常量字符的指针。
遇到一条使用了类型别名的声明语句时,人们往往会错误的尝试将类型别名替换成它本来的样子,来理解该语句的含义:
const char * cstr =0;// 是对const pstring cstr 的错误理解
再强调一遍,这种理解是错误的,声明语句中用到pstring 时,其基本数据类型是指针,可是用char *
重写了声明语句后,数据类型就变成了char, *
成为了声明符的一部分,这样改写的结果是,const char 成了基本数据类型,前后两种声明含义截然不同,前者声明了一个指向char的常量指针,改写后的形式则声明了一个指向const char的指针。
auto 类型说明符
C++11 新标准引用了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型,显然,auto定义的变量必须有初始值:
//由 val1 和val2 想加的结果就可以推断出item的类型
auto item = val1+val2 ;//item 初始化为val1 和val2 想加的结果
此处编译器将自己推断item的类型,使用auto也能在一条语句中声明多个变量,因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:
auto i=0, * p=&i; //right: i is int ,p is int point
auto sz=0 ,pi=3.14 ;// error: sz 和 pi的类型不一致
复合类型、常量和auto
编译器推断出的auto 类型有时候会和初始值的类型并不完全一样,编译器会适当的改变结果类型使其更符合初始化规则。
int i=0,&r=i;
auto a =r ; //a is int
其次,auto 一般会忽略顶层const,同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:
const int ci =i, &cr =ci ;
auto b =ci ; //b 是一个整数 (ci 的顶层const 特性被忽略掉了)
auto c =cr ; //c 是一个整数 (cr是ci 的别名,ci 本身是一个顶层const )
auto d =&i; //d 是一个整型指针(整数的地址就是指向整数的指针)
auto e =&ci ; //e 是一个指向整数常量的指针(对常量对象取地址是一种底层const )
如果希望推断出的auto类型是一个顶层const ,需明确指出:
const auto f=ci ;
decltype类型指示符
有时候遇到这种情况,希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量,为了满足这一要求,C++11 新标准引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:
decltype (f()) sum =x ; // sum 的类型就是函数f 的返回类型
编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。decltype 处理顶层const 和引用的方式与auto有些许不同,如果decltype 使用的表达式是一个变量,则decltype 返回该变量的类型(包括顶层const 和引用在内):
const int ci =0 ,&cj =ci ;
decltype(ci) x =0 ; //x 的类型是const int
decltype(cj) y=x; // y 的类型是const int& , y 绑定到变量x
decltype (cj) z; // error z 是一个引用,必须初始化
decltype 和引用
//decltype 的结果可以是引用类型
int i=42 ,* p=&i ,&r =i ;
decltype (r + 0) b; //正确: 加法的结果是int
decltype (*p ) c; //error : c是int& ,必须初始化
decltype 和 auto 的另一处重要区别是,decltype 的结果类型与表达式形式密切相关,有一种情况需要特别注意,加上括号与不加括号的区别:
//decltype 的表达式如果是加上了括号的变量,结果将是引用
decltype((i))d; //错误: d 是int & ,必须初始化
decltype(i) e ; //正确,e 是一个int