Bootstrap

C++:基本内置类型和复合类型

前言

数据类型是程序的基础,其说明了数据的意义,以及在数据上所能执行的操作。

C++语言支持广泛的数据类型,其定义了基本内置类型,和提供了自定义数据类型的机制。基于基本内置类型自定义数据类型的机制,C++标准库定义了一些更加复杂的数据类型,如可变长字符串向量等,同样,程序员本人也可以基于此定义一些自己需要的数据类型。

基本内置类型

C++定义的一组内置数据类型包括:算术类型(Arithmetic Type)和空类型(Void Type)。其中,算术类型包含了字符、整型数、布尔值和浮点数。空类型不对应具体的值,仅用于一些特殊的场合,如当函数不返回任何值时使用空类型(void)作为返回类型

算术类型

算术类型分为两类:整型(Integral Type)浮点型(Float Type),其中,整型包括字符型(Character Type)布尔型(Bool Type)在内。布尔类型(bool)的取值是真(true)或者假(false)。

算术类型的尺寸,即该类型数据所占的比特数在不同机器上是有所差别的。如下为C++标准规定的尺寸最小值,同时允许编译器赋予这些类型更大的尺寸。某一类型所占的比特数不同,它所能表示的数据范围也不一样。

C++:算术类型

类型含义最小尺寸
bool布尔类型未定义
char字符8位
wchar_t宽字符16位
char16_tUnicode字符16位
char32_tUnicode字符32位
short短整型16位
int整型16位
long长整型32位
long long长整型64位
float单精度浮点数6位有效数字
double双精度浮点数10位有效数字
long double扩展精度浮点数10位有效数字

复合类型

复合类型(Compound Type)是指基于其它类型定义的类型。C++语言有几种复合类型,下面将介绍其中的三种:引用指针数组。相比于普通变量的声明,定义复合类型的变量要复杂很多。一条声明语句由一个基本数据类型(Base Type)和紧随其后的一个声明符(Declarator)列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。其实,声明符就是变量名,变量的类型就是声明的基本数据类型。

引用

引用(Reference)为对象起了另外一个名字,引用类型引用(Refers to)另外一种类型。引用即别名,其并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。因为引用本身不是一个对象,所以不能定义引用的引用。通过将声明符写成&d的形式,即可定义一个引用,其中d是声明的变量名。在一条语句中,允许定义多个引用,其中,每个引用标识符都必须以符号&开头。

一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序会把引用和它的初始值绑定(Bind)在一起,而不是将初始值拷贝给引用。初始化引用的初始值是变量名,而不能是具体的字面值。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令应用重新绑定到另外一个对象,因此引用必须初始化。

定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的。为引用赋值,实际上是把值赋给了与引用绑定的对象。获取引用的值,实际上是获取了与引用绑定的对象的值。同理,以引用作为初始值,实际上是以与引用绑定的对象作为初始值。

代码清单

//i1和i2都是int型变量
int i1=1024,i2=2048;
//r1是一个引用,并以变量i1作为其初始值,r2是int型变量
int &r1=i1,r2=i2;
//i3是int型变量,ri是一个引用,与i3绑定在一起
int i3=1024,&ri=i3;
//r3和r4都是引用
int &r3=i3,&r4=i2;

指针

指针(Pointer)是“指向(Point To)”另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的简介访问。然而指针与引用相比又有很多不同点。其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的声明周期内它可以先后指向几个不同的对象。其二,指针无须在定义时赋初值。和其它内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。

数组

数组是一种类似于标准库vector的数据结构,但是在性能和灵活性的权衡上又与vector有所不同。与vector相似的地方是,数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问。与vector不同的地方是,数组的大小确定不变,不能随意向数组中增加元素。因为数组的大小固定,因此对某些特殊的应用来说程序的运行时性能较好,但是相应地也损失了一些灵活性。(如果不清楚元素的确切个数,建议使用vector)

定义和初始化内置数组

数组是一种复合类型。数组的声明形如a[d],其中a是数组的名字,d是数组的维度。维度说明了数组中元素中的个数,因此必须大于0。数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。也就是说,维度必须是一个常量表达式:

unsigned cnt=42;           //不是常量表达式
constexpr unsigned sz=42;  //常量表达式
int arr[10];               //含有10个整数的数组
int *parr[sz];             //含有42个整型指针的数组
string bad[cnt];           //错误:cnt不是常量表达式
string str[get_size()];    //当get_size是constexpr时正确;否则错误

默认情况下,数组的元素被默认初始化。和基本内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值。

定义数组的时候必须指定数组的类型,不允许用auto关键字有初始值的列表推断类型。另外和vector一样,数组的元素应为对象,因此不存在引用的数组。

显示初始化数组元素

可以对数组的元素进行列表初始化,此时允许忽略数组的维度。如果在声明时没有指明维度,编译器会根据初始值的数量计算并推测出来;相反,如果指明了维度,那么初始值的总数量不应该超出由维度指定的大小。如果维度比提供的初始值数量大,则用提供的初始值初始化靠前的元素,剩下的元素被初始化成默认值。

const unsigned sz=3;
int a1[sz]={0,1,2};       //含有3个元素的数组,元素分别是0,1,2
int a2[]={0,1,2};         //维度是3的数组
int a3[5]={0,1,2};        //等价于a3[]={0,1,2,0,0}
string a4[3]={"hi","bye"};//等价于a4[]={"hi","bye",""}
int a5[2]={0,1,2};        //错误:初始值过多

字符数组的特殊性

字符数组有一种额外的初始化形式,在程序中,可以用字符串字面值对此类数组初始化。当使用这种方式时,一定要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去:

char a1[]={'C','+','+'};     //列表初始化,没有空字符
char a2[]={'C','+','+','\0'};//列表初始化,含有显示的空字符
char a3[]="C++";             //自动添加表示字符串结束的空字符
const char a4[6]="Daniel";   //错误:没有空间可存放空字符

a1的维度是3,a2和a3的维度都是4,a4的定义是错误的。尽管字符串字面值"Daniel"看起来只有6个字符,但是数组的大小必须至少是7,其中6个位置存放字面值的内容,另外1个存放结尾处的空字符。

不允许拷贝和赋值

不能将数组的内容拷贝给其它数组作为其初始值,也不能用数组为其它数组赋值。

int a1[]={0,1,2}; //含有3个整数的数组
int a2[]=a1;      //错误:不允许使用一个数组初始化另一个数组
a2=a1;            //错误:不能把一个数组直接赋值给另一个数组

一些编译器支持数组的赋值,这就是所谓的编译器扩展(Compiler Extension)。但一般来说,最好避免使用非标准特性,因为含有非标准特性的程序可能在其它编译器上无法正常工作。

理解复杂的数组声明

和vector一样,数组能存放大多数类型的对象。例如,可以定义一个存放指针的数组。又因为数组本身就是对象,所以允许定义数组的指针数组的引用。在这几种情况中,定义存放指针的数组比较简单和直接,但是定义数组的指针或数组的引用就稍微复杂一点了。

int *ptrs[10];         //ptrs是含有10个整型指针的数组
int &refs[10]=/* ? */  //错误:不存在引用的数组
int (*Parray)[10]=&arr;//Parray指向一个含有10个整数的数组
int (&arrRef)[10]=arr; //arrRef引用一个含有10个整数的数组

默认情况下,类型修饰符从右向左依次绑定。对于ptrs来说,从右向左理解其含义比较简单:首先知道我们定义的是一个大小为10的数组,它的名字是ptrs,然后知道数组中存放的是指向int的指针。ptrs是一个数组,数组中含有10个整型指针。

但是对于Parray来说,从右向左理解就不太合理了。因为数组的维度是紧跟被声明的名字的,所以就数组而言,由内向外阅读要比从右向左好多了。由内向外的顺序可帮助我们更好地理解Parray地含义:首先是圆括号括起来地部分,*Parray意味着Parray是个指针,接下来观察右边,可知道Parray是个指向大小为10地数组地指针,最后观察左边,直到数组中的元素是int。这样最终的含义就明白无误了,Parray是一个指针,它指向一个int数组,数组中包含10个元素

同理,(&arrRef)表示arrRef是一个引用,它引用的对象是一个大小为10的数组,数组中元素的类型是int

当然,对修饰符的数量并没有特殊限制:

int *(&arr)[10]=ptrs; //arr是数组的引用,该数组含有10个指针

按照由内向外的顺序阅读上述语句,首先由(&arr)知道arr是一个引用,然后观察右边的[10]知道,arr引用的对象是一个维度为10的数组,最后观察左边的int *知道,数组的元素类型是指向int的指针。这样,arr就是一个含有10个int型指针的数组的引用

要想理解数组声明的含义,最好的办法是从数组的名字开始按照由内向外的顺序阅读。

访问数组元素

与标准库类型vector和string一样,数组的元素也能使用范围for语句或下标运算符来访问。数组的索引从0开始,以一个包含10个元素的数组为例,它的索引从0到9,而非从1到10.

在使用数组下标的时候,通常将其定义为size_t类型。size_t是一种机器相关的无符号类型,它被设计的足够大以便能表示内存中任意对象的大小。在cstddef头文件中定义size_t类型,这个文件是C标准库stddet.h头文件的C++语言版本。

数组除了大小固定这一特点外,其它用法与vector基本类似。例如,可以用数组来记录各分数段的成绩个数。

//以10分为一个分数段统计成绩的数量:0~9,10~19,...,90~99,100
unsigned scores[11]={}; //11个分数段,全部初始化为0
unsigned grade;
while(cin>>grade){
  if(grade<=100){
    ++score[grade/10]; //将当前分数段的计数值加1
  }
}

这里,scores是一个含有11个无符号元素的数组。此处使用的下标运算符是由C++语言直接定义的,这个运算符能用在数组类型的运算对象上。而库模板vector中的下标运算符是vector自己定义的,只能用于vector类型的运算对象。

与vector和string一样,当需要遍历数组的所有元素时,最好的办法也是使用范围for语句。例如,下面的程序输出所有的scores。

for(auto i:scores) //对于scores中的每个计数值
  cout<<i<<" ";    //输出当前的计数值
cout<<endl;

因为维度是数组类型的一部分,所以系统知道数组scores中有多少个元素,使用范围for语句可以减轻人为控制遍历过程的负担。

检查下标的值

与vector和string一样,数组的下标是否在合理范围之内由程序员负责检查,所谓合理就是说下标应该大于等于0而且下雨数组的大小。要想防止数组下标越界,除了小心谨慎注意细节以及对代码进行彻底的测试之外,没有其它好办法。对于一个程序来说,即使顺利通过编译并执行,也不能肯定它不包含此类致命的错误。

大多数常见的安全问题都源于缓冲区溢出错误。当数组或其它类似数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误。

;