目录
- 一、复合类型
- 1.引用
- 1.1 引用的定义和使用
- 1.2 引用的注意事项
- 2. 指针
- 2.1 指针的定义和使用
- 2.2 指针的四种状态
- 2.3 指针的注意事项
- 2.4 其他指针操作
- 2.5 void* 指针
- 3. 理解复合类型变量的声明
- 4. const 限定符
- 4.1 const 对象的注意事项
- 4.2 const 和引用
- 4.2.1 术语:常量引用是对 const 的引用
- 4.2.2 const引用的注意事项
- 4.3 const 和指针
- 4.3.1指向 const 对象的指针注意事项
- 4.3.2 const 指针
- 4.3.3 顶层 const 和 底层 const
- 5. constexpr 和常量表达式
- 5.1 常量表达式
- 5.2 constexpr 类型
- 5.3 constexpr 类型的注意事项
- 6. 处理类型
- 6.1 类型别名
- 6.1.1 关键字 typedef
- 6.1.2 C++11新增 using 别名声明
- 6.1.3 类型别名注意事项
- 6.2 auto 类型说明符
- 6.2.1 为什么需要 auto 类型说明符
- 6.2.2 auto 类型说明符的注意事项
- 6.3 decltype 类型指示符
- 6.3.1 decltype 的不同
- 6.3.2 decltype 和引用
一、复合类型
复合类型是基于其他类型定义的类型。C++语言有几种复合类型,这里介绍其中两种常用的复合类型:引用和指针。
前面定义变量的格式都是:基本数据类型+变量名。如:int a,double b = 3.14 等。而更通用变量定义的格式为:基本数据类型 + 声明符列表。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。
1.引用
引用其实是为已存在的对象起了个别名,实际上并未创建新的对象。创建对象的实质其实是编译器根据对象的类型开辟对应大小的空间,然后程序员通过变量名来访问这块空间。而引用就是给这块空间起了第二个名字,通过这个新名字访问这块空间的效果和通过原变量名访问的效果一样。
1.1 引用的定义和使用
引用的定义格式为:类型& 变量名 。
如下代码:
这里先创建了 int 类型的变量 a,并初始化为 10,然后定义了指向 a 的引用ra,然后 ra 就是 a 的别名,使用 ra 和使用 a 的效果是一样的,并且它们的地址相同,因为它们都是同一块空间的标识符。所以修改 ra 的值就是修改 a 的值。如下图所示:
引用类型的对象没有属于自己的存储空间,它们只是其他对象的别名,和其他对象共用一块空间。而且引用一旦绑定了对象,就不允许绑定其他对象,只能一直和最初的对象绑定在一起。所以,引用必须初始化。
对引用的操作就是对其绑定对象的操作,两者互为一体。为引用赋值其实就是为其绑定对象赋值。获取引用的值,实际上就是获取其绑定对象的值。
1.2 引用的注意事项
1. 引用本身不是对象,它只是其他对象的别名,所以不能定义引用的引用。
2. 允许在一条语句中定义多个引用,但是必须在每个引用标识符前面加上符号 & 。
int i = 10, j = 20;
int &ri = i, rj = j; // 这里 ri 是引用,而 rj 是 int 类型的变量
int &ri = i; &rj = j; // 这里的 ri 和 rj 都是引用
3. 引用必须和引用对象的类型严格匹配,否则编译器会报错。但是以下两种情况例外:
(1)const 引用既可以绑定 const 对象,也可以绑定非 const 对象。而非 const 对象只能绑定非 const 对象。而 const 对象还可以绑定字面值常量。
(2)在类继承中,基类对象的指针和引用可以在不显式转换的情况下绑定基类的对象。
类型不匹配的情况:
这里试图用 double 类型的引用绑定 int 类型的变量 a ,编译器报错。
2. 指针
指针是一种指向其他类型的复合类型,它和引用类似,也实现了对其他对象的间接访问。但是指针与引用又有许多不同点:
(1)指针本身是一个对象,拥有属于自己的存储空间,存储指向对象的地址。
(2)允许对指针赋值和拷贝,且在指针的生命周期中可以前后指向多个不同对象。
(3)指针可以不初始化。和其他内置类型一样,如果是局部指针变量,那么其内存储的地址就是随机值。
2.1 指针的定义和使用
指针的定义格式:类型* 变量名 。
如下代码:
通过取地址符(&)可以得到变量的地址。可以看到指针 pa 指向了变量 a,然后指针 pa 中存储了变量 a 的地址。然后便可以通过解引用操作符(*)对指针 pa 进行解引用来访问变量 a 。如下代码:
可以看到,通过对指针 pa 进行解引操作得到了其指向对象 a,并修改了变量 a 的值。可以这么理解,*pa 就是变量 a,对*pa的操作就是对变量 a 的操作,这一点和引用类似。
2.2 指针的四种状态
指针应属于下列四种状态之一:
(1)指向某个已存在的对象。
(2)指向紧邻对象所占空间的下一个位置。
(3)空指针,意味着指针没有指向任何对象。
(4)无效指针,上述三种情况之外的其他情况。
2.3 指针的注意事项
1. 不能对无效指针进行解引用操作。
例如:如果指针未初始化,则其中存储的地址是随机值,若该地址上面刚好存储了计算机系统的重要信息,我们通过解引用操作去访问这些信息会造成难以预测的结果。
2. 创建指针变量最好初始化,若实在不知道指向哪个对象,则将其初始化为空指针(nullptr)。
空指针(nullptr)不指向任何对象,可以在使用指针之前对其进行判断是否为空,这样安全性就大大提高。在过去的程序中还使用一个名为 NULL 的预处理变量对指针赋值,使其成为空指针。但是,在新标准下,最好使用现在的 nullptr 。
如下代码给指针 pi 初始化为空指针:
int *pi = nullptr; // 把指针 pi 初始化为空指针
3. 除了以下两种情况,其他所有指针的类型必须和被指向的类型严格配对。
(1)const 指针既可以指向 const 对象,也可以指向非 const 对象,还可以指向字面值常量。而非 const 指针只能指向非 const 常量。
(2)在类继承中,基类的引用和指针可以在不进行显式转换的情况下直接指向派生类对象。
2.4 其他指针操作
可以对指针进行加减整数,结果是在当前位置的基础上移动该整数乘以其类型大小的字节数。如下代码:
可以看到对 int 类型的指针 pi 加 5 实际上增加了 5*4 = 20(字节),其他类型也是如此。
只要指针是合法的,就可以把它用在条件表达式中,任何非空指针(nullptr)的条件值都是 true。如下代码:
对于两个类型相同的合法指针,可以使用相等操作符(==)或不等操作符(!=)来比较它们,比较的结果为 bool 类型。如果两个指针存放的地址相同,则它们相等;否则它们不相等。这里指针存放的地址相等有三种可能:它们都为空、都指向同一个对象,或者都指向了同一个对象的下一地址。注意, 一个指针指向某个对象,另一个指针指向该对象的下一地址,此时也有可能出现这两个指针值相同的情况,即指针相等。
2.5 void* 指针
void* 是一种特殊类型的指针,可以存放任意已经存在的对象的地址。如下代码:
int a = 10;
double b = 20;
void* p = &a; // 可以指向 int 变量
p = &b; // 也可以指向 double 变量
但是 void* 类型的指针仅仅只能存放对象的地址,并不能通过解引用操作访问该对象。因为编译器并不知道该对象的类型,不能对该地址往后的空间进行操作。但是可以拿 void* 类型的指针与别的指针进行比较、作为函数的参数或者返回值,也可以赋值给另一个 void* 指针。
3. 理解复合类型变量的声明
复合类型变量的声明包含一个基本数据类型和一组声明符。虽然一条声明语句中的基本数据类型只有一个,但是声明符的形式可以不同。如下代码,分别定义了一个 int 的变量、int* 类型的指针和 int& 类型的引用:
int a = 10, *pa = &a, &ra = a; // 定义了三种不同类型的变量
再次证明,如果要在一条声明语句中定义多个指针或者引用,需要在每个标识符前面加上符号(*)或者(&)。
由于引用并不是一个对象,所以不能创建引用的引用,也不能创建引用的指针。但是指针是一个对象,可以创建指针的引用。如下所示:
可以看到指向指针 pi 的引用 rpi 与指针 pi 不但存储的地址相同,而且同样可以进行解引用操作,再次证明对引用的操作,实际上是对其所指向对象的操作,引用只是其所指向对象的别名。
还可以声明指向指针的指针,因为指针是一个对象,它也有自己的地址。如下代码:
int a = 10;
int *pa = &a; // 指向变量 a 的指针
int **ppa = &pa; // 指向指针 pa 的指针
其他类型也是如此,上述 ppa 也称为二级指针。如果想定义三级指针,则再加上一个符号(*),以此类推。而对二级指针 ppa 解引用一次得到指针 pa,再解引用一次得到变量 a 。如下代码:
4. const 限定符
const 限定符的作用是使变量的值不能被修改,只能在声明变量的时候使用,且必须初始化,否则编译器会报错。如下代码:
使用 const 修饰的变量依旧是变量,不是常量。只是它的属性变成了只读。const 变量除了不能修改其值以外和普通变量没有什么区别,普通变量能进行的操作它也能进行。
4.1 const 对象的注意事项
(1)对于简单的并且能在编译时就确定值的 const 对象,编译器将在编译过程中把用到该变量的地方替换为其对应的值。
(2)const 对象默认状态下只在当前文件有效,如果在头文件中声明了 const 对象,则包含该头文件的每个文件中都定义了一个该 const 对象。如果想在多个文件中使用同一个 const 对象,则不能将其放在头文件中。并且需要在一个文件中定义该 const 对象时添加 extren 关键字,在其需要使用该 const 对象的文件中也使用 extren 关键字进行声明。
如下代码:
// text1.cpp 文件
extern const int SIZE = 20; // 定义
// text2.cpp 文件
extern const int SIZE; // 声明
上述代码在 text1.cpp 文件中对 const 对象 SIZE 进行定义,然后在 text2.cpp 中声明,告诉编译器该对象的定义在其他文件。这样,两个文件中使用的都是同一个 const 对象。
4.2 const 和引用
在引用声明前添加 const 就可以绑定 const 对象,我们称之为对常量的引用。但是不能通过对常量的引用修改它绑定的对象,否则编译器会报错。如下代码:
但是除了修改自身值这个操作以外,rA 和 A 都可以进行其他变量可以进行的操作。
4.2.1 术语:常量引用是对 const 的引用
C++程序员们经常把词组 ”对 const 的引用“ 简称为 ”常量引用“,这一简称还是挺靠谱的,不过前提是你的时刻记得这就是个简称而已。
严格来说,并不存在常量引用。因为引用不是一个对象,所以我们没法让引用本身恒定不变。事实上,由于C++语言并不允许随意改变引用所绑定的对象,所以从这层意义上理解所有的引用又都算常量。引用的对象是常量还是非常量可以决定其所能参与的操作,却无论如何都不会影响到引用和对象的绑定关系本身。
4.2.2 const引用的注意事项
(1)const 引用还可以绑定非 const 对象和字面值常量。
如下代码:
(2)const 引用也可以绑定一个表达式
如下代码:
(3)不能通过 const 引用改变 绑定的对象的值,不管绑定的对象是否为 const 对象。
如下代码:
计算表达式时,不会改变参与表达式运算的变量,而是会在其中生成临时变量存储表达式的计算结果,执行完该语句之后,该临时变量会被销毁。临时变量具有常性,即不能被修改,只读属性。
4.3 const 和指针
与引用一样,指针也可以指向常量。指向常量的指针不能修改所指向对象的值。想要存放常量的地址必须使用指向常量的指针,否则编译器会报错。如下代码:
第二行代码试图让 int* 指针 pi 指向 const int 变量 a,编译器报错。
4.3.1指向 const 对象的指针注意事项
(1) 指向 const 对象的指针和 const 引用一样,既可以指向 const 对象,也可以指向非 const 对象。
如下代码:
(2)不能通过指向 const 对象的指针去修改被指向对象的值,不管被指向对象是否为 const 对象。
如下代码:
4.3.2 const 指针
指针和引用不一样,引用不是对象并且引用一旦绑定一个对象就必须一直绑定该对象,相当于该引用本身就被 const 修饰。而指针是对象且不管是否为指向 const 对象的指针,它都可以再次指向其他对象。我们可以通过改变 const 的位置,使其修饰指针本身,这样指针就不能指向其他对象了,该指针称为常量指针。如下代码:
如果 const 是修饰指针的,则需要把 const 放在 * 号的后面。且试图使常量指针指向其他对象是非法的,编译器会报错。常量指针必须初始化。
4.3.3 顶层 const 和 底层 const
顶层 const 表示 指针本身被 const 修饰,指针本身是常量。而底层 const 表示指针所指向的对象被 const 修饰,该被指向的对象是常量。实际上就是上述指针的两种 const 形式,只是换了个叫法。
引用只能是底层 const,因为引用必须初始化,且一旦绑定对象就不能修改,相当于自带引用本身自带 const 修饰。而我们使用 const 只能修饰其绑定对象。
5. constexpr 和常量表达式
5.1 常量表达式
常量表达式是指值不会改变并且在编译过程中就能得到计算结果的表达式。显然字面值属于常量表达式,用常量表达式初始化的 const 对象也是常量表达式。
一个对象是不是常量表达式由它的数据类型和初始值共同决定,如下代码:
const int A = 65; // A 为常量表达式
const int B = A + 1; // B 为常量表达式
int C = 67; // C 为常量表达式
const int sz = get_size(); // D 为常量表达式
A 是 const 对象,B 是const 对象且用常量表达式初始化,所以 A 和 B 均为常量表达式。而 C 不是 const 对象,D 虽然是 const 对象,但是 D 的值需要在运行时才能确定,所以 C 和 D 均不是常量表达式。
5.2 constexpr 类型
C++11 规定,可以将变量声明为 constexpr 类型,表明该变量的值是一个常量表达式,且编译器会对其进行检查,若其值不是常量表达式,则报错。如下代码:
一般来说,如果你认定变量是一个常量表达式,那就把它声明为 constexpr 类型。
5.3 constexpr 类型的注意事项
指针和引用虽然都可以定义成 constexpr,但是用来初始化它们的值有严格的限制。
(1)一个 constexpr 指针的初始值必须是空指针(nullptr 或者 0),或者存储于某个固定地址中的对象。即,不能使用局部变量初始化该类指针,否则编译器会报错,因为局部变量出了其作用域就会被销毁。如下代码:
(2)constexpr 只能修饰指针本身,不能修饰指针所指向的对象。效果相当于常量指针,即指针的指向不能改变。如下代码:
6. 处理类型
6.1 类型别名
类型别名顾名思义就是其他类型的别名,使用它就和使用原类型是一个效果。当类型名称较长难以理解时,用自己的方式定义该类型别名,便于写代码和理解。下面有两种方法可以定义类型别名。
6.1.1 关键字 typedef
如下代码:
上述代码把 INT 声明为了 int 的别名,然后使用 INT 相当于使用 int,所以 a 和 b 均为 int 类型的变量。
6.1.2 C++11新增 using 别名声明
如下代码:
此代码和上述使用 typedef 关键字的代码一致,INT 均为 int 类型的别名,且 a 和 b 均为 int 类型的变量。
6.1.3 类型别名注意事项
如果对复合类型定义类型别名,则在使用时有些许不同。
1. 类型别名在定义中为基本数据类型
如下代码:
typedef char* pstr;
const pstr cstr = 0;
const pstr *ps;
第一条语句让 pstr 成为 char* 的别名,则在下面两行代码中,pstr 是基本数据类型,且 const 修饰 pstr,则 cstr 为指向字符常量的指针。而 * 修饰 ps 所以,ps 本身是一个指针,它指向的对象为 const pstr,即指向字符常量的指针。
2. 不需要在每个变量前都加修饰符
如下代码分别使用正常方式和类型别名声明两个 int 类型的指针。
正常声明复合类型时,每个变量名前都需要添加相应的修饰符,而类型别名在定义时作为基本数据类型,所以 pint 表示其基本类型就是 int 类型的指针。所以,就不需要在变量名前面添加 * 修饰。
6.2 auto 类型说明符
auto 类型说明符在变量初始化时,通过对初始值进行分析,来推断所创建变量的类型。如下代码:
int a = 0;
auto b = a; // b 的类型也是 int
auto c = 0; // c 的类型也是 int
第二条语句,编译器根据 a 的类型为 int,推断出 b 的类型也为 int,并且用 b 的值来初始化 a。第三条语句类似,只不过 0 时 int 类型字面值。
6.2.1 为什么需要 auto 类型说明符
当编写大型程序时,用到一个比较前面的变量来初始化现有刚创建的变量时,使用 auto 能提供很大的方便。且当要创建的变量类型复杂时,通过使用 auto 使代码简洁,节省时间。
6.2.2 auto 类型说明符的注意事项
(1)使用 auto 创建的变量必须初始化,不然编译器推断不出其类型。
如下代码:
(2)编译器推断 auto 类型的变量时有时和初始值的类型并不完全一样,编译器会适当改变结果的类型使其更符合初始化规则。
规则如下:
a. 我们在使用引用时,其实使用的是引用所绑定的对象,特别当引用被用作初始值时。所以当使用引用对象来推断 auto 的类型时,实际上是根据引用所绑定的对象来进行推断的。
b. auto 一般会忽略顶层 const,保留底层 const 。
如下代码:
这里 a 的类型为 const int,但是该 const 为顶层 const,被 auto 忽略,所以 b 推断出来的类型为 int 。
如下代码:
上述代码中,指针 pa 既是顶层 const 又是底层 const,但是 auto 在推断类型时忽略了顶层 const,保留底层 const。则指针 pi 只是底层 const,可以修改指针的指向,但是不能修改指针所指向的值。
(3)可以使用 auto 来设置引用,但是初始值中的顶层 const 仍保留。
如下代码:
使用 auto 设置引用时,若初始值是顶层 const 对象,则该 const 会保留。若想绑定 字面值,则需要前缀 const,表明该类型是 const 引用。
(4)在一条语句中使用 auto 定义多个变量时,符号 & 和 * 只属于某个变量名,而不是基本数据类型的一部分,因此初始值必须是同一种基本类型。
如下代码:
第一条 auto 声明语句,ra 是 int 类型的引用,则编译器推断基本数据类型为 int,所以 pb 为指向 int 的指针。第二条 auto 声明语句,c 是 double 类型的引用,但是后面的 pi 为 int 类型,导致编译器判断不了该条声明语句的基本数据类型,故报错。
6.3 decltype 类型指示符
decltype 类型指示符和 auto 类型说明符相似。但是 decltype 类型指示符只通过表达式的类型来判断所创建的变量的类型,但不使用该表达式来对变量进行初始化。
如下代码:
decltype(f()) a = b;
上述代码通过函数 f() 的返回类型来确定变量 a 的类型,但是通过变量 b 类初始化变量 a。
6.3.1 decltype 的不同
decltype 处理顶层 const 的方式和 auto 有所不同。如果 decltype 使用的表达式是一个变量,则返回该变变量的类型(包括顶层 const 的引用在内)。
如下代码:
上述代码中,第一条 decltype 语句中由于变量 ci 的类型为 const int,则变量 x 的类型也为 const int,顶层 const 保留。第二条 decltype 语句中 cj 的类型为 const int 类型的引用,则 y 也为 const int 类型的引用,绑定变量 x 。最后 z 为 const int 类型的引用,但是没有初始化,编译器报错。
6.3.2 decltype 和引用
如果 decltype 使用的是一个表达式而不是变量,则 decltype 返回表达式结果对应的类型。如下代码:
decltype d = 1 + 3.0; // d 为 double 类型
上述表达式最后的计算结果为 double 类型,则变量 d 的类型为 double 。
如果表达式的内容是解引用操作,则 decltype 得到引用类型。
如下代码:
上述代码中 *pa 等价于 &a,则 c 的类型推断出来为 int 类型的引用,然后没有对其初始化,编译器报错。
如果 decltype 使用的是一个不加括号的变量,则得到的类型就是该变量的类型;如果给该变量加上一层或多层括号,则编译器把它当作一个表达式。一般会得到引用类型。
如下代码:
这里第一条语句 a 为 int 类型,而第二条语句多加了一个括号,则 ri 为 int 类型的引用,必须初始化。
注意: decltype( (variable) ) 的结果永远是引用;而 decltype( variable) 的结果只有 variable本身是引用时,其结果才是引用。