1、基本内置类型
- 算术类型
(1) 整数类型:short、int、long、char、bool
(2) 浮点数类型:float、double- 空类型:void
1.1 内置类型的机器实现
对算术类型,C++标准只规定了各类型所占的最小尺寸,因此,在不同的机器上,同样的算术类型可能具有不同的尺寸。这一点和Java不同(Java规定了每种内置类型的具体尺寸,是平台无关的,这也是Java可移植性好的原因之一)。
1.2 选择类型的经验准则
和C语言一样,C++的设计准则之一也是尽可能地接近硬件,C++的算术类型必须满足各种硬件特质。一些选择类型的经验准则:
- 当明确知道数值不为负时,选择无符号类型
- 使用 int 执行整数运算:实际应用中,short 往往太小;long 往往和 int 尺寸一样大。int 不够用时,使用 long long
- 算术表达式中不要使用 char、bool:不同机器是编译器对 char 的处理可能不一样,有些是有符号的,有些是无符号的。如果必须使用 char,请明确指明其类型是 signed char 或者 unsigned char
- 浮点数计算使用 double:float通常精度不够,而且单双精度浮点数的计算代价相差无几。long double 一般用不到。
1.3 基本算术类型的转换
- 非布尔类型的算术值赋给布尔类型时,非0为true,0为false;布尔值赋给非布尔类型时,true转为1,false转为0
- 浮点数赋给整型时,只保留非小数部分;整型值转为浮点数时,小数部分记为0,如果该整数所占的空间超过浮点类型的容量,精度可能有损失。
- 赋给无符号类型一个超出其范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8bit 大小的 unsigned char 可以表示0~255范围的值,如果赋给一个超出范围的值,结果是该值对256取模后的余数。因此,-1赋给8bit 大小的 unsigned char 的结果是255。注意: 切勿混用有符号类型和无符号类型。因为,如果表达式中既有带符号类型又有无符号类型,带符号类型会自动转换成无符号数,当带符号类型为负值时会出现异常结果。
- 赋给有符号类型一个超出其范围的值时,结果是 未定义的(undefined) 。程序可能崩溃,也有可能产生垃圾数据。
1.4 字面值常量
using namespace std;
cout << "Hello World!" << endl;
cout << 0.5 + 99 << endl;
上面代码片段中,"Hello World!" 和 0.5、99 就是所谓的字面值常量,指的是它们的值就是字面上呈现的样子,无法改变。
尽管我们没有为字面值常量明确指明其数据类型,但每个字面值常量都有对应的数据类型,否则计算机将不知道如何来存储这些字面值常量。
字面值常量的形式和值决定了它的数据类型:
- 十进制整型字面值的类型是在满足能容纳当前值的前提下,int、long、long long 中尺寸最小的那个
- 浮点型字面值默认是 double 类型。可以在字面值后添加后缀来表示其它浮点类型
- 单引号括起来的一个字符是 char 型字面值
- 双引号括起来的0个或多个字符是 字符串型字面值 。字符串字面值的类型本质上是由常量字符构成的字符数组。编译器在每个字符串的结尾处添加一个空字符('\0') [注意它和字符0('0')的区别]。因此,字符串字面值的实际长度要比它的内容多1。
- true 和 false 是布尔类型字面值
- nullptr是指针字面值
2、变量
2.1 基本概念
变量的本质是一个具名的、可供程序操作的 存储空间。为了标识和操作这块存储空间,给它起个名字,这个名字叫做 变量名。
int a; // 定义
extern int b; // 声明
因此,上述代码中的 a
是变量名,它标识了一块存储空间,这块存储空间才是变量。但一般不说这么绕,直接说变量a。
- 变量定义 是一个 申请存储空间,并 将变量名和申请的存储空间关联起来 的过程。C++中,使用的变量必须先定义,否则没有空间存储数据。
-
变量声明 不申请存储空间,仅仅是告诉后面的程序,有这么一个某种类型的变量名。
- 如果所有程序都在同一个源文件中,我们只需要变量定义就够了,此时变量定义同时也起到了声明的作用;变量声明主要的作用在于支持C++的分离式编译,实现代码共享(一处定义,多处使用)。比如在一个头文件中定义了变量b,我们在其它文件中使用这个变量b时,就需要先声明,告诉程序我们要用的变量b是已经定义好的。此时,在变量名之前添加关键字 extern 即可。
- 变量只能被定义一次,但可以被多次声明。
-
初始值是变量在定义时获得的一个特定值。此时我们说变量被 初始化 了。
-
初始化和赋值是两个不同的概念。尽管很多时候我们使用
=
来初始化一个变量。强调:初始化不是赋值。初始化是创建变量时赋予其一个初始值;赋值是把变量的当前值擦除,然后用一个新值来替代。 - C++中初始化有多种不同形式,不仅仅是
=
这一种。比如下面4条语句都能完成对变量的初始化:int t1 = 0; // =初始化 int t2 = {0}; // 列表初始化 int t3{0}; // 列表初始化 int t4(0); // 列表初始化
- 默认初始化:如果定义变量时没有指定初始值,则变量会被
默认初始化
,也就是被赋予一个默认值。内置类型在函数外(全局变量)被初始化为0;在函数内(局部变量)不被初始化,其值是未定义的。
-
初始化和赋值是两个不同的概念。尽管很多时候我们使用
3、复合类型
复合类型是在其它类型基础上定义的类型。
声明变量的语法: 基本数据类型 声明符列表
声明符为变量起了个名字(变量名),并指定该变量是和基本数据类型相关的某种类型。
因此,在基本内置类型的声明语句中,声明符就是变量名,此时变量的类型就是声明中的基本数据类型。而在复合类型的声明语句中,声明符更加复杂。下面介绍两种常用的复合类型,引用 和 指针。
3.1 引用(reference)类型
引用类型:引用另外一种类型。就像写论文时引用文献中的论据,一个引用类型的变量r
(简称一个引用)引用另一个类型变量s
中的内容,代表变量r
的内容来源于另一个变量s
。
本质上,引用就是为变量s
起了一个别名。打个比方,就像一个人既有大名又有小名,但不论叫大名还是小名,最终指向的都是这个人。
强调:引用只是已有对象的别名。引用本身不创建对象。
引用必须初始化:
变量初始化时,初始值会被拷贝到新创建的对象中。而在声明引用时,由于引用并不创建对象,所以程序是把引用和它的初始值(即它引用的对象)绑定到一起,而不是拷贝初始值给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起,无法重新绑定到另一个对象,因此引用必须初始化。
另:因为引用本身不是一个对象,所以不能定义引用的引用。
int ival = 1024;
int &refVal = ival; //声明引用,并初始化。refVal引用ival
int &refVal2; //报错 ‘refVal2’ declared as reference but not initialized:引用必须初始化
引用的定义:
- 为了和一般变量的定义区别开,引用的声明符在变量名之前加
&
前缀- 引用只能绑定在对象上,不能绑定在字面值和表达式的计算结果上
int &refVal1 = 10; //错误:引用类型必须绑定在对象上
double val = 3.14;
int &refVal2 = val; //错误:引用类型和绑定的对象的类型不匹配
3.2 指针(pointer)类型
与引用类似,指针也能实现对其他对象的间接访问。指针和引用的不同点在于:1.指针本身就是一个对象,因此可以对指针赋值和拷贝,从而指针可以先后指向不同的对象(非常量指针)。2.指针无须再定义时赋初值,在块作用域内如果指针未被初始化,也将拥有一个不确定的值。
指针的定义:
double *dp, dp2;
其中,声明符dp是一个整体*:dp是变量名,*
说明这是一个指针,*
只对dp生效;dp2是一个普通的double型变量。
在定义变量时,
*
表示定义一个指针变量;而在访问指针指向的对象时,*
表示解引用符。
空指针:
空指针(null pointer)不指向任何对象。生成空指针的方法:
// 下面3种方法本质上是等价的,都是给指针赋予一个初始值0
int *p1 = nullptr; //使用字面值nullptr进行初始化
int *p2 = 0; //将p2初始化为字面常量0
int *p3 = NULL; //需要#include cstdlib
// 不能直接使用int变量给指针赋值,即使这个int变量的值为0
int zero = 0;
int *p4 = zero; //报错:invalid conversion from ‘int’ to ‘int*’
void* 指针:
void*是一种特殊的指针类型,可存放任意对象的地址。
void*指针主要用于指针比较、函数的输入输出等。不能直接操作void*指针所指的对象,因为不知道这个对象的具体类型,从而也就无法确定能在这个对象上进行哪些操作。
3.3 理解复合类型的声明
变量定义:
基本数据类型 声明符列表
在同一条定义语句中,基本数据类型只有一个,但声明符的形式却可以不同,从而一条定义语句可以定义出不同类型的变量。
// 一条定义语句中,定义了int型变量a,int型指针b,int型引用c
int a = 1024, *b = &a, &c = a;
指向指针的指针:
int ival = 1024;
int *pi = &ival; //pi指向一个int型数据
int **ppi = π //ppi指向一个int型的指针
指向指针的引用:
指针本身是一个对象,因此可以定义对指针的引用。
#include <iostream>
using namespace std;
int main()
{
int i = 42;
int *p;
//int &r = p; //r是一个指向int型变量的引用,无法和指针绑定
int *&r = p; //r是一个对指针的引用
cout << "r=" << r << ", p=" << p << endl;
cout << "*r=" << *r << ", *p=" << *p << ", i=" << i << endl;
cout << "---------------------------------\n"<< endl;
r = &i;
cout << "r=" << r << ", p=" << p << endl;
cout << "*r=" << *r << ", *p=" << *p << ", i=" << i << endl;
cout << "---------------------------------\n"<< endl;
*r = 0;
cout << "r=" << r << ", p=" << p << endl;
cout << "*r=" << *r << ", *p=" << *p << ", i=" << i << endl;
return 0;
}
结果是:
r=0x400c61, p=0x400c61
*r=1961723208, *p=1961723208, i=42
---------------------------------
r=0x7fff8bbdec74, p=0x7fff8bbdec74
*r=42, *p=42, i=42
---------------------------------
r=0x7fff8bbdec74, p=0x7fff8bbdec74
*r=0, *p=0, i=0
示例代码中的 r 是对指针 p 的引用,我们应该从右往左阅读 r 的定义:离变量名最近的符号对变量的类型有最直接的影响,因此&
告诉我们 r 是一个引用。声明符的其余部分用以确定 r 引用的类型是什么,例子中的*
说明 r 引用的是一个指针。最后,基本数据类型部分指出 r 引用的是一个int型指针。
指向引用的指针:
引用本身不是一个对象,因此不能定义指向引用的指针。
4、const限定符
const准确的含义是只读,即使用相同的值给一个const变量重新赋值也不行。const对象一旦创建后其值就无法改变,所以 const对象必须初始化。
const int i = 42;
const int j = i;
j = i; //即使用相同的值重新赋值也会报错:assignment of read-only variable ‘j’
const int k; //const变量没有初始化,报错:uninitialized const ‘k’
编译器在编译过程中,会把使用到const变量的地方都替换成const变量相应的值。const变量默认只在文件内生效,如果需要在多个文件之间共享const变量,必须在const变量定义之前添加extern
关键字,其它文件内使用该变量时,在声明之前也添加extern
关键字,用于表明该变量的定义在别的文件中。
4.1 const和引用
把引用绑定在 const 对象上,称之为对常量的引用(reference to const)。和普通引用不同,对常量的引用不能用于修改其绑定的对象。
const int ci = 1024;
const int &r1 = ci;
r1 = 42; //error: assignment of read-only reference ‘r1’
int &r2 = ci; //error: binding ‘const int’ to reference of type ‘int&’ discards qualifiers
const 对象不能被赋值,所以也就不能通过引用去改变ci
。用ci
初始化r2
报错的原因是,假如该初始化合法,那么可以通过r2
来改变它引用对象的值,这显然是错误的。
对常量的引用(reference to const)在工作中也常简称为"常量引用"。严格地说,由于引用本身不是一个对象,我们无法让引用本身恒定不变,所以并不存在常量引用;C++中无法改变引用所绑定的对象,从这一层意义上看的话所有的引用又都算常量。无论引用的对象是不是常量,都不会影响引用和对象的绑定关系本身。
通常,引用的类型必须和其所引用对象的类型一致。但有两个例外:
其一:常量引用初始化时,允许任何可转化成引用的类型的表达式作为初始值。
int i = 42;
// 允许为常量引用绑定非常量对象、字面值、表达式
const int &r1 = i; //绑定非常量对象
const int &r2 = 42; //绑定字面值
const int &r3 = r1 * 2; //绑定表达式
int &r4 = r1 * 2; //错误:r4是普通的非常量引用
const int &r5 = 3.14; //正确,double类型可转化为 const int 类型
4.2 const和指针
指向常量的指针(pointer to const):不能用于改变所指对象的值。要想保存常量对象的地址,只能使用指向常量的指针。但指向常量的对象可以指向一个非常量的对象。
const double pi = 3.14; //pi是常量,值不能改变
double *ptr = π //ptr是个普通指针,报错:invalid conversion from ‘const double*’ to ‘double*’
const double *cptr = π //正确
*cptr = 3.1415926; //cptr指向常量,报错:assignment of read-only location ‘* cptr’
double dval = 10.24;
cptr = &dval; //正确,指向常量的指针可以指向一个非常量对象
const 指针:和引用不同,指针本身是对象,所以指针本身可以是常量,即常量指针(const pointer)。和一般 const 对象一样,常量指针也必须初始化,而且一旦初始化,值就不能再改变。
把
*
放在const
关键字之前,用以说明指针是一个常量,指明不变的是指针本身的值而非指向的那个值。
int errNumb = 0;
int *const curErr = &errNumb; //curErr将一直指向errNumb
const double pi = 3.14;
const double *const pip = π //pip是一个指向常量对象的常量指针
如之前变量声明
所述,声明的含义应该从右往左读:*const curErr
是一个整体,可以理解为一个声明符。离变量名curErr
最近的是符号是const
,代表curErr
本身是一个常量对象;其次是符号*
,说明curErr
是一个指针,合起来指明curErr
是一个常量指针。最后,基本数据类型部分确定了curErr
指向一个 int 对象。
按这样的套路来分析pip
:声明符整体说明pip
是一个常量指针;基本数据类型部分确定了pip
指向一个 double 类型的常量。最终,我们确定,pip
是一个指向常量的常量指针。
4.3 顶层/底层 const
- 顶层const:对象本身是常量,如const pointer
- 底层const:指针、引用所指的对象是一个常量
指针本身是一个对象,指针既可以是顶层 const 也可以是底层 const;引用本身不是对象,因此引用只能是底层 const。
为什么要区分顶层/底层 const ?
在执行对象的拷贝操作时,顶层 const 不受什么影响(特指拷贝的对象是顶层 const,拷入的对象当然不能是顶层 const)。
另一方面,当拷贝对象时,拷入和拷出的对象必须具有相同的底层 const
资格,或两个对象的数据类型可以转换。通常,非 const 可以转换成 const,反之则不可。
int i = 0;
const int *const p = &i; // 正确,非const可以转换成const
int *p1 = p; // 报错:p包含底层const的定义,而p1没有
const int *p2 = p; //正确,p和p2都是底层const,p的顶层const 部分不影响
const int ci = 42; //顶层const
int &r = ci; //非const不能绑定到const上
const int &r2 = i; //正确,非const可以转换成const
4.4 constexpr 和常量表达式
常量表达式(const expression):值不会改变并且在
编译过程
中就能得到计算结果的表达式。
字面值是常量表达式,用常量表达式初始化的 const 对象也是常量表达式。
5、处理类型
5.1 类型别名
两种方式:
(1). C风格方式
typedef 旧类型名 新类型声明符([修饰符]类型名);
如:
typedef double wages; // wages 是 double 的同义词
typedef wages base, *p; // base 是 double 的同义词,p是 double * 的同义词
(2). 新标准方式
using 新类型 = 旧类型;
注意:
复合类型的别名的理解方式,如下代码:
typedef char *pstring;
const pstring cstr = 0;
const pstring *ps;
typedef
将 pstring
指定为指向 char 的指针类型的别名。因此,在后面使用过程中,pstring
就应该始终被理解为指针类型。把类型别名机械地进行文本替换,这种理解方式是错误的!
类比const int a;
,声明了int型的常量a
,意味着a
是int类型,它本身是不变的;同理,const pstring cstr;
意味着cstr
是pstring类型(指向char的指针),且本身是不变的,也就是说,这条语句指明cstr
是一个指向char的常量指针。
类似地,ps
是一个指向常量的指针,ps
指向一个char型常量指针。
typedef int *pstring; //pstring是指针类型的别名
const pstring *ps; //ps是指向常量指针的指针
int i = 42;
int *p = &i;
ps = &p; //非const可以转换成const
cout << "p=" << p << endl; //p=0x7fff6fd55944
cout << "*ps=" << *ps << endl; //*ps=0x7fff6fd55944
int j = 55;
int *const q = &j;
ps = &q; //ps可以指向另一个对象,说明ps本身不是const的
cout << "q=" << q << endl; //q=0x7fff6fd55934
cout << "*ps=" << *ps << endl; //*ps=0x7fff6fd55934
*ps = &i; //报错:assignment of read-only location ‘* ps’
5.2 auto 类型说明符
C++新标准引入了auto
类型说明符,可以让编译器通过初始值来推算变量的类型。因此,auto
定义的变量必须有初始值。
auto
会忽略顶层const (引用例外),保留底层const
auto i = 0, *p = &i; //推算出i是整数,p是整型指针
auto sz = 0, pi = 3.14; //错误,sz和pi类型不一致
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是指向整型常量的指针,对e而言,ci是底层const