数据类型
变量的属性
众所周知,程序=数据结构+算法,而数据类型正是数据结构的基础,其重要性无可比拟,因而,本章将重点探讨数据类型相关的内容。首先,我们把数据类型按照下面的方式分类:
这是最常见的一种分类方式,除此外,还可以按照存储类型进行分类,即:
字符型变量的实际存储方式是对应字符的ASCII码,布尔型变量的取值只有0(false)或1(true),而指针类型的存储的值是地址,而地址是一个整型值,因此,整型、字符型、布尔型和指针类型均属于整存储类型。
一般来说,数组是由单一类型存储多值的一个变量,可以通过下标访问某一值,而结构体和共用体含有不同的数据类型。
我们如此分类是为了方便讨论变量的属性中的共性和特性。在C++中,一个变量具有的属性很多,且部分属性复杂,本书中只涉及常用的属性,下面是常用变量属性表:
属性名称 | 定义方式 | 属性值 |
---|---|---|
地址 | 隐式 | 地址值,在64位机器上占8个字节 |
值 | 显式、(限定)隐式 | 由数据类型决定 |
名称 | 显式 | 标识符 |
数据基础类型 | 显式、(限定)隐式 | 除指针、数组之外的数据类型 |
数据附加类型 | (限定)显式、隐式 | 指针、数组 |
存储类型 | 显式、隐式 | auto,register,extern,static |
作用域 | 显式、隐式 | 由定义位置和存储类型决定 |
生存期 | 显式、隐式 | 由定义位置和存储类型决定 |
修饰类型 | 显式、隐式 | const |
表达范围 | 隐式 | 由数据类型决定 |
占用空间 | 隐式 | 由数据类型决定 |
我们将逐一介绍变量的这些属性。我们把需要人为编写代码规定的操作称为显式操作,把系统自动匹配识别进行的操作称为隐式操作。
地址是变量在内存中存储的位置,当定义变量时,系统会自动从内存中找到位置存储,绝大多数情况下无需显式定义。
值是变量的首要属性,也是唯一一个可变属性,一般来说,值必须显式定义,即必须赋初值,否则不能引用,尤其const变量必须在声明时初始化,但是以下几种变量例外:
- 全局变量
- 静态变量
- 类的成员变量
数值型变量会自动初始化为0,字符型变量会自动初始化空字符。
名称也是变量的首要属性,定义要求符合标识符的规则即可,因为名称也是标识符,是唯一一个需要必须显式定义的属性。
数据基础类型包括整型、单精度浮点型、双精度浮点型、布尔型、字符型、结构体、共用体、枚举、类和空类型,简单记忆就是由英文标识符表示的数据类型。其中,整型、双精度浮点型和字符型可以使用修饰符衍生出新的数据类型,修饰符有unsigned、signed、short、long四个,其中双精度浮点型只能被long修饰、字符型只能被unsigned和signed修饰、整型在不矛盾的前提下,这四个修饰符可以任意结合修饰。使用修饰符修饰后,其表达范围和占用空间会受到影响,其具体情况在表达范围中讲到。
数据附加类型包括指针和数组,其没有直接的英文标识符,使用符号标识,且能和数据基础类型的整型、单精度浮点型、双精度浮点型、布尔型、字符型结合。注意数据附加类型可以两者同时结合,例如指针单精度浮点数组。
在程序中,语句之间存在着并列关系和嵌套(包含)关系。并列关系的语句中,在前面的语句称为前级语句,在后面的语句称为后级语句,代码在编译时,从前级往后级运行,这也就是变量需要在使用前定义的原因。嵌套关系的语句中,在外部的语句称为外层语句(父语句),在内部的语句称为内层语句(子语句),任何语句都可以和其它语句有并列关系,但只有一些语句能和其它语句有嵌套关系,这类语句有:
- 块语句(复合语句)。即用花括号括起来的语句。
- 循环语句。while、do-while语句和for语句。
- 条件语句。if-else语句和switch语句。
变量从时间和空间上有两个属性,分别是生存期和作用域。一般来说,一个变量被定义后,与其并列最后一个语句结束后,该变量被销毁,也就是生存期结束,一个变量的作用域也是从定义语句开始,直到并列的最后一个语句结束。也就是说,一个变量的后级语句和内层语句能调用它,而该变量的前级语句和外层语句不能调用它,这种定义在程序块或者函数体内的变量就被称为局部变量,其作用域和生存期有限。
而另一类变量定义在程序开头,主函数外部,其生存期是从程序运行开始到结束,其作用域是整个程序,这种变量被称为全局变量。作用域和生存期除定义位置外,还可以使用一些关键字修改其作用域和生存期。
变量的存储方式分为静态存储和动态存储。动态存储方式分为自动变量和寄存器变量,自动变量使用auto关键字,使用时不能指定数据类型,但必须要初始化,因为自动变量需要根据初值判断变量类型,因而使用auto关键字是一种隐式定义变量类型的方法。寄存器变量使用register关键字,这样定义的变量存储在CPU的通用寄存器中,由于CPU读写寄存器的数据速度要比读写内存里的数据速度快,可以使用register关键字提升运行效率,但由于CPU的通用寄存器数量有限,不建议使用过多的寄存器变量,如果寄存器被占用时,定义的变量会转换为自动变量。
静态存储方式分为外部变量和静态变量,外部变量是在程序运行期间分配固定的空间给变量,存储在静态存储区,是最常用的方式,使用extern关键字指定,但是这种方式是默认的,因此extern可以省略,静态变量使用static关键字,也存储在静态存储区。外部变量和静态变量对于全局变量和局部变量的表现效果不同,如下表:
extern | static |
---|---|
全局变量 | 将作用域从本文件扩展其它文件 |
局部变量 | 将作用域从本代码块扩展的本文件 |
修饰类型是本书中自定义的一个名称,使用const修饰符(关键字),使得变量成为不可变的量,在定义时必须初始化,但是其值不能再修改,这种不可变的量称为常量。后文中均称const类型变量。 这种常量时存储在内存中实实在在的常量,其除了值不可变外,其它属性与一般变量类似,但是,还有另一种常量,多是编写程序时用到的临时值,用完就销毁,这种常量称为临时常量,也称字面量。通过实践不难发现,使用指针可以取到const常量的值,但不能取到字面量的值,会产生错误。
由于变量类型决定占用空间和表达范围,因而在已知变量类型和语言环境设定的占用空间的前提下,可以推算表达范围。如果需要记忆,可以只记忆占用空间即可(但一般情况下不会用这种要求)。例如字符型变量的占用空间为1字节,已知1字节是8位,则其表达范围为0至2^8-1,即0至255。
基本变量类型的使用语法规则:
- 声明
C++ | |
---|---|
方括号表示这个参数或者关键字是可选的。
- 赋值
C++ | |
---|---|
引用是指对变量的使用,例如为其它变量赋值、作函数参数、参与运算等,说明变量名即可。赋值是一种特殊的引用,赋值号的左值必须是可变的变量,而不能是常量或者字面量。
- 初始化
C++ | |
---|---|
初始化是指在声明变量时对其赋值。
自定义数据类型
自定义数据类型也称为构造类型,顾名思义,是指在使用时不能像基本数据类型那样直接使用,而是需要进行自定义构造之后,才能创建实例进行使用。自定义数据类型有结构体(struct)、共用体(union)和枚举(enum)。
自定义变量在语法上有相似之处,但其实质不同。
结构体用于将不同类型的数据组织在一起形成一个单一的数据单元。结构体的成员可以大多数数据类型,包括结构体。共用体在同一内存空间存储不同类型的数据。共用体的成员共享相同的内存位置,因此它们只能同时保存其中一个成员的值。枚举是一种用于定义命名整数常量的数据类型,其实质是常量列表的整合。 为了能更好的理解这三种变量类型,下面将以实例介绍:
枚举变量实质时常量表的集合,从第一个枚举符开始,默认的值为0、1、2……,也可以自行定义枚举符代表的值,未定义的部分会自动顺延前面已经定义的。
已知有如下代码:
当我们输入3.5时,输出的值为1080033280,这与数据在计算机中的表示方法有关,在上一节末尾,我们说到了float变量的存储格式,3.5的二进制为:0 10000000 11000000000 000000000000,其转换为整数即为1080033280。具体转换原理参考IEEE 754。
可以用typedef定义一个新类型的名字,其既没有创造一个新的变量类型,也没有影响原来的变量类型,原变量标识符仍然是可用的。
最后,我们探讨各自定义变量类型所占用的空间大小。首先探讨结构体类型占用空间大小,下面是几个例子:
C++ | |
---|---|
如果说结构体所占用空间大小是其各成员变量之和,则z1,z2,z3,z4的占用空间为8,13,21,15。试着使用sizeof取占用空间大小操作符,判断猜想是否正确。经过测试发现,其结果为8,24,32,24。这和猜测大相径庭,再进一步输出地址,查看其地址分配情况,可以发现以下规律:
- 默认情况下,结构体的大小是结构体中最长类型的整数倍。
- 结构体中的空间分布是按照结构体中最长类型对齐的。
- 结构体中不同类型的成员,一定是按照自己的类型对齐。
这个规律被称为结构体成员对齐规则。这是默认情况下的对齐方式,也可以指定对齐方式,使用预处理命令#pragma pack(n),n为常正整数,表示n字节对齐。
共用体的占用内存在前文中说明过,由其最长类型成员决定。
枚举的占用内存一般情况下为定值4,是因为其实际上是一个整型常数表。
到此为止,我们说明了数据基础类型,即有英文单词作为关键字的数据类型,包括基本数据类型和构造数据类型。从下一节开始,开始说明数据附加类型,即数组和指针。数据基础类型彼此之间不能组合,例如一个变量不能既是int,又是double,但是数据附加类型可以和数据基础类型结合,例如char数组,float指针等。
数组类型
对于少量的数据,使用几个变量就可以表示和操作,但对于大量的数据,重复定义大量个变量是非常不现实而繁杂的,因而,引用了数组作为大量数据的存储方法,对数组可以进行访问和操作。结构体是不同类型变量的集合,而数组是相同类型变量的集合。
数组属于数据附加类型,必须有一个数据基础类型,定义是需要指定,数组和指针属于可嵌套类型,例如,数组有一维数组、二维数组、三维数组……,而指针有一级指针、二级指针、三级指针……。
数组采取连续存储的方式,一维数组的元素地址计算方式为:数组元素地址=数组起始地址+元素下标,二维数组的元素地址计算方式为:元素地址=数组起始地址+(元素下标1*元素列数+元素下标2)*数组类型字节数。
更高维的数组也可以推广。在这里给出n维数组的元素地址运算公式和占用内存公式。首先是元素地址运算公式:
对于数据类型字节长为\(t\)的\(n\)维数组\(a[i_1][i_2][i_3]...[i_k]...[i_n]\),符合对于任意\(j_k<i_k\),其某一元素\(a[j_1][j_2][j_3]...[j_k]...[j_n]\)的运算公式为:
对于数据类型字节长为\(t\)的\(n\)维数组\(a[i_1][i_2][i_3]...[i_k]...[i_n]\),其占用内存公式为:
最后讨论关于字符串的问题,字符串是指一串字符,但char只能表示单个字符,不难想到,可以使用字符数组来表示一串字符。C++中可以使用这种方法,但为了表示一串字符的结束,使用结束表示字符(也称终止符)“\0”来标识,否则会出现一些问题。
其实,除了使用字符数组,有着更为方便的定义方式,即string标识符,但string定义的字符串变量不能像字符数组那样单字符操作,但在不同的场合,使用字符串和使用字符数组各有优劣。使用string标识符前,需先引用头文件。
如果在字符串结尾不使用\0会出现一些问题,如果没有使用\0会输出“烫”,这是因为在调试时,内存会用0xcc来初始化,而0xcccc恰好是"烫"的编码。
指针类型
数组作函数参数传递时,对于形参的操作会影响到实参,而对于一般变量作函数参数传递时,对于形参的操作却不会影响实参,这种方式称为值传递。而数组名我们说过是第一个元素的地址,即数组作函数参数时的传递方式称为地址传递。值传递具有形实分离的效果,而地址传递具有形实一体的效果。也可以获得一般变量的地址,实现和数组一样的传递方式。
如果想要获取一个变量的地址,我们可以使用取地址运算符&,对于变量a,&a就是其地址,但是如果想要把这个地址存储起来,我们可以使用一种专门存储地址的变量,即指针变量。例如对于int a,可以取其地址int *p=&a。指针变量定义的方法和基本变量类似,只不过是带上了符号*,其数据类型指的是这个指针只能指向哪种变量,而不是指针变量本身的值。有关指针的一些问题值得注意:
- 指针不能被直接输入或使用字面量指定,但可以输出,即对于int *p=&a,cout << p是合法的而cin >> p是非法的。
- 指针必须现指向一个变量,即初始化后才能进行使用,例如int *p;cin >> *p是非法的。
- 指针运算符*没有严格的格式要求,即int * p、int* p和int *p是等价的。
*称为间址运算符,又成为解引用运算符,在定义时起到指针变量的标识符,在调用时起到指向地址的所对应的量,而指针变量被定义后,如果不带*表示地址,带*表示地址对应的量。
指针可以通过加、减、自增和自减运算进行移动,例如对于int型指针p的p++是指向下移动一个整型变量的位置,若p初值为5000,则移动后的值为5000+1*4=5008。指针可以进行比较,低地址端的存储单元小于高地址端的存储单元。
两个指针变量相加减对于非数组和结构体变量没有实际意义。对于数组或者结构体具有一定意义。
一般而言,数组中元素存储的地址是连续的,我们可以根据这一规律使用指针访问数组内的不同元素,首先我们研究一维数组如何使用指针表示,已有数组a[n],且*p=a或(*p=&a[0]),则对于数组a的元素,可有以下表示方法:
下标法 | 指针法 |
---|---|
a[0]、p[0] | *a、*p |
a[i]、p[i] | *(a+i)、*(p+i) |
接下来是二维数组使用指针指定元素的方法:
下标法 | 下标-指针法 | 指针法 |
---|---|---|
首地址 | - | &a[0] |
首值 | a[0][0] | *a[0] |
i行首地址 | - | a[i]、&a[i][0]、&a[i] |
i行首值 | a[i][0] | *a[i] |
i行j列首地址 | - | a[i]+j、&a[i][j] |
i行j列首值 | a[i][j] | *(a[i]+j)、(*(a+i))[j] |
同样也能得到三维及 n 维数组的指针指定元素的方法。使用数组地址运算公式即可得出,具体方法是展开这个公式,然后提公因式,最后在每一层括号外添加指针符号即可。例如取 n=5 时,可以展开为:
将\(a\)的一维数组指针代入内部,由于指针运算时会自动乘以其字长,因此 t=1:
最后变换得到:
\(a[j_{1}][j_{2}][j_{3}][j_{4}][j_{5}]=*(*(*(*(a+j_{1})+j_{2})+j_{3})+j_{4})+j_{5})\)
即为五维数组的指针。
由此容易得到用指针表示数组的一般规律:
可以求出 n 维数组的一种表示后,再求出 n-m(m<n)维数组的另一种表示,再求出 n-\(k(k<n,k\neq m)\)维数组的还一种表示,然后彼此带入,形成混合表示,例如:
\(\(*\left(*\left(*\left(*\left(*\left(a+j_1\right)+j_2\right)+j_3\right)+j_4\right)+j_5\right)\)\) \(a[j_1][j_2][j_3][j_4]\)
下式带入上式得到:
这是简单的一种混合,可以自由进行混合使用。
如同前面这样,进行连续解引用,或着定义时使用多个指针标识符*,这种指针被称为多级指针,和数组类似,也分为一级指针、二级指针、三级指针……,注意,一级指针是指向一般变量的指针,而二级指针是指向一级指针的指针,三级指针是指向二级指针的指针……,不能直接越级指定。
由于指针属于数据附加类型,因此可以有常量指针,常量指针既可以指向常量,也可以指向非常量,当指向非常量时,被指向的非常量可变,但指针本身不可变。
还有一种指针是void指针,也称为通用指针,可以用来存放任何数据类型的引用。通用指针有四个性质:
- void指针具有与char指针相同形式的内存对齐方式。
- void指针和别的指针永远不会相等,但赋值为NULL的两个void指针相等。
- void指针可以被任何指针赋值,且都可以转换为原来的指针类型。
- void指针只能做数据指针,不能做函数指针。
先思考下面程序的可行性,再试着上机运行以验证你的猜想。
C++ | |
---|---|
下面是动态申请内存空间的常见操作:
但这种内存连续的二维数组本质还是一维数组,只是将一维数组模拟成了二维数组,使用这类方法还能建立不规则的二维数组(即不同列的行数不一定相同)。
使用动态申请内存空间后,当内存使用结束后,需要对内存进行释放,即把已使用完的内存归还给系统,还需要注意保存内存地址的指针变量不能再赋其它值,如果这样做,就丢失了原来的内存地址。上面两种情况被称为内存泄漏,前者称为隐式内存泄漏,后者称为显示内存泄漏。
如果内存已经释放,但是指针还在引用原始内存,这样的指针被称为迷途指针,这种指针没有指向有效对象,迷途指针存在潜在的安全隐患,若有不慎可能会造成严重后果。
如果内存已经释放,又再一次释放了内存,这种情况称为重复释放,会产生程序异常。解决这种问题的方法是在释放指针后,将指针的值置为NULL,大部分堆管理器会忽略后续对空指针的释放。
有这样一种数组,不预先指定其有多少个元素,而是直接进行输入,直到检测到输入的值有某一标识符,这时停止输入。实现这种数组使用到动态申请内存空间的方式,具体方法如下:
指针可以为简单或者复杂的数据结构提供更多的灵活性,例如链表、队列、栈和树。
该文章由作者完全原创,请在转载时标明出处。