您的位置:首页 > 编程语言 > C语言/C++

C++ Primer 第二遍阅读笔记(第四章)

2013-02-22 20:23 232 查看
与 vector 类型相比,数组的显著缺陷在于:数组的长度是固定的,而且程序员无法知道一个给定数组的长度。数组没有获取其容量大小的 size 操作,也不提供 push_back 操作在其中自动添加元素。如果需要更改数组的长度,程序员只能创建一个更大的新数组,然后把原数组的所有元素复制到新数组空间中去。与使用标准 vector 类型的程序相比,依赖于内置数组的程序更容易出错而且难于调试。

数组定义中的类型名可以是内置数据类型或类类型;除引用之外,数组元素的类型还可以是任意的复合类型。没有所有元素都是引用的数组。

非 const 变量以及要到运行阶段才知道其值的 const 变量都不能用于定义数组的维数。

数组的维数必须在一对方括号 [] 内指定:



虽然 staff_size 是用字面值常量进行初始化,但 staff_size 本身是一个非 const 对象,只有在运行时才能获得它的值,因此,使用该变量来定义数组维数是非法的。而对于 sz,尽管它是一个 const 对象,但它的值要到运行时调用 get_size 函数后才知道,因此,它也不能用于定义数组维数。

在定义数组时,可为其元素提供一组用逗号分隔的初值,这些初值用花括号{}括起来,称为初始化列表:



如果没有显式提供元素初值,则数组元素会像普通变量一样初始化(第 2.3.4 节):

在函数体外定义的内置数组,其元素均初始化为 0。

在函数体内定义的内置数组,其元素无初始化。

不管数组在哪里定义,如果其元素为类类型,则自动调用该类的默认构造函数进行初始化;如果该类没有默认构造函数,则必须为该数组的元素提供显式初始化。

除非显式地提供元素初值,否则内置类型的局部数组的元素没有初始化。此时,除了给元素赋值外,其他使用这些元素的操作没有定义。

显式初始化的数组不需要指定数组的维数值,编译器会根据列出的元素个数来确定数组的长度:



如果指定了数组维数,那么初始化列表提供的元素个数不能超过维数值。如果维数大于列出的元素初值个数,则只初始化前面的数组元素;剩下的其他元素,若是内置类型则初始化为0,若是类类型则调用该类的默认构造函数进行初始化:



字符数组既可以用一组由花括号括起来、逗号隔开的字符字面值进行初始化,也可以用一个字符串字面值进行初始化。然而,要注意这两种初始化形式并不完全相同,字符串字面值(第 2.2 节)包含一个额外的空字符(null)用于结束字符串。当使用字符串字面值来初始化创建的新数组时,将在新数组中加入空字符:



使用一组字符字面值初始化字符数组时,一定要记得添加结束字符串的空字符。



与vector不同,一个数组不能用另外一个数组初始化,也不能将一个数组赋值给另一个数组,这些操作都是非法的:



在用下标访问元素时,vector 使用 vector::size_type 作为下标的类型,而数组下标的正确类型则是 size_t(第 3.5.2 节)。

在下面的例子中,for 循环遍历数组的 10 个元素,并以其下标值作为各个元素的初始值:



使用类似的循环,可以实现把一个数组复制给另一个数组:



导致安全问题的最常见原因是所谓“缓冲区溢出(buffer overflow)”错误。当我们在编程时没有检查下标,并且引用了越出数组或其他类似数据结构边界的元素时,就会导致这类错误。

指针的概念很简单:指针用于指向对象。与迭代器一样,指针提供对其所指对象的间接访问,只是指针结构更通用一些。与迭代器不同的是,指针用于指向单个对象,而迭代器只能用于访问容器内的元素。

具体来说,指针保存的是另一个对象的地址:



第二条语句定义了一个指向 string 类型的指针 sp,并初始化 sp 使其指向 string 类型的对象s。*sp 中的 * 操作符表明 sp 是一个指针变量,&s 中的 & 符号是取地址操作符,当此操作符用于一个对象上时,返回的是该对象的存储地址。取地址操作符只能用于左值(第 2.3.1 节),因为只有当变量用作左值时,才能取其地址。同样地,由于用于 vector 类型、string 类型或内置数组的下标操作和解引用操作生成左值,因此可对这两种操作的结果做取地址操作,这样即可获取某一特定对象的存储地址。

建议:尽量避免使用指针和数组

C++ 语言使用 * 符号把一个标识符声明为指针:



理解指针声明语句时,请从右向左阅读。

在声明语句中,符号 * 可用在指定类型的对象列表的任何位置:



该语句定义了一个 double 类型的 dp 对象以及一个指向 double 类型对象的指针dp2。

一个有效的指针必然是以下三种状态之一:保存一个特定对象的地址;指向某个对象后面的另一对象;或者是0值。若指针保存0值,表明它不指向任何对象。未初始化的指针是无效的,直到给该指针赋值后,才可使用它。下列定义和赋值都是合法的:



很多运行时错误都源于使用了未初始化的指针。

对大多数的编译器来说,如果使用未初始化的指针,会将指针中存放的不确定值视为地址,然后操纵该内存地址中存放的位内容。使用未初始化的指针相当于操纵这个不确定地址中存储的基础数据。因此,在对未初始化的指针进行解引用时,通常会导致程序崩溃。

C++ 语言无法检测指针是否未被初始化,也无法区分有效地址和由指针分配到的存储空间中存放的二进制位形成的地址。建议程序员在使用之前初始化所有的变量,尤其是指针。

如果可能的话,除非所指向的对象已经存在,否则不要先定义指针,这样可避免定义一个未初始化的指针。

如果必须分开定义指针和其所指向的对象,则将指针初始化为 0。因为编译器可检测出 0 值的指针,程序可判断该指针并未指向一个对象。

对指针进行初始化或赋值只能使用以下四种类型的值:

0 值常量表达式(第 2.7 节),例如,在编译时可获得 0 值的整型 const 对象或字面值常量 0。

类型匹配的对象的地址。

另一对象末的下一地址。

同类型的另一个有效指针


把 int 型变量赋给指针是非法的,尽管此 int 型变量的值可能为 0。但允许把数值 0 或在编译时可获得 0 值的 const 量赋给指针:



除了使用数值0或在编译时值为 0 的 const 量外,还可以使用 C++ 语言从 C 语言中继承下来的预处理器变量 NULL(第 2.9.2 节),该变量在 cstdlib 头文件中定义,其值为 0。如果在代码中使用了这个预处理器变量,则编译时会自动被数值 0 替换。因此,把指针初始化为 NULL 等效于初始化为 0 值:



除了将在第 4.2.5 节和第 15.3 节介绍的两种例外情况之外,指针只能初始化或赋值为同类型的变量地址或另一指针:



由于指针的类型用于确定指针所指对象的类型,因此初始化或赋值时必须保证类型匹配。指针用于间接访问对象,并基于指针的类型提供可执行的操作,例如,int 型指针只能把其指向的对象当作 int 型数据来处理,如果该指针确实指向了其他类型(如 double 类型)的对象,则在指针上执行的任何操作都有可能出错。

C++ 提供了一种特殊的指针类型 void*,它可以保存任何类型对象的地址:



void* 指针只支持几种有限的操作:与另一个指针进行比较;向函数传递 void* 指针或从函数返回 void* 指针;给另一个 void* 指针赋值。不允许使用 void* 指针操纵它所指向的对象。

指针提供间接操纵其所指对象的功能。与对迭代器进行解引用操作(第 3.4 节)一样,对指针进行解引用可访问它所指的对象,* 操作符(解引用操作符)将获取指针所指的对象:



解引用操作符返回指定对象的左值,利用这个功能可修改指针所指对象的值:



因为 sp 指向 s,所以给 *sp 赋值也就修改了 s 的值。

也可以修改指针 sp 本身的值,使 sp 指向另外一个新对象:



给指针直接赋值即可修改指针的值——不需要对指针进行解引用。

虽然使用引用(reference)和指针都可间接访问另一个值,但它们之间有两个重要区别。第一个区别在于引用总是指向某个对象:定义引用时没有初始化是错误的。第二个重要区别则是赋值行为的差异:给引用赋值修改的是该引用所关联的对象的值,而并不是使引用与另一个对象关联。引用一经初始化,就始终指向同一个特定对象(这就是为什么引用必须在定义时初始化的原因)。

考虑以下两个程序段。第一个程序段将一个指针赋给另一指针:



赋值结束后,pi 所指向的 ival 对象值保持不变,赋值操作修改了 pi 指针的值,使其指向另一个不同的对象。现在考虑另一段相似的程序,使用两个引用赋值:



这个赋值操作修改了 ri 引用的值 ival 对象,而并非引用本身。赋值后,这两个引用还是分别指向原来关联的对象,此时这两个对象的值相等。

指针本身也是可用指针指向的内存对象。指针占用内存空间存放其值,因此指针的存储地址可存放在指针中。下面程序段:



为了真正地访问到 ival 对象,必须对 ppi 进行两次解引用:



C++ 语言中,指针和数组密切相关。特别是在表达式中使用数组名时,该名字会自动转换为指向数组第一个元素的指针:



如果希望使指针指向数组中的另一个元素,则可使用下标操作符给某个元素定位,然后用取地址操作符 & 获取该元素的存储地址:



与其使用下标操作,倒不如通过指针的算术操作来获取指定内容的存储地址。指针的算术操作和迭代器的算术操作(第 3.4.1 节)以相同的方式实现(也具有相同的约束)。使用指针的算术操作在指向数组某个元素的指针上加上(或减去)一个整型数值,就可以计算出指向数组另一元素的指针值:



通常,在指针上加上(或减去)一个整型数值 n 等效于获得一个新指针,该新指针指向指针原来指向的元素之后(或之前)的第 n 个元素。

指针的算术操作只有在原指针和计算出来的新指针都指向同一个数组的元素,或指向该数组存储空间的下一单元时才是合法的。如果指针指向一对象,我们还可以在指针上加1从而获取指向相邻的下一个对象的指针。

假设数组 ia 只有 4 个元素,则在 ia 上加 10 是错误的:



只要两个指针指向同一数组或有一个指向该数组末端的下一单元,C++ 还支持对这两个指针做减法操作:



结果是 4,这两个指针所指向的元素间隔为 4 个对象。两个指针减法操作的结果是标准库类型(library type)ptrdiff_t 的数据。与 size_t 类型一样,ptrdiff_t 也是一种与机器相关的类型,在 cstddef 头文件中定义。size_t 是 unsigned 类型,而 ptrdiff_t 则是 signed 整型。

这两种类型的差别体现了它们各自的用途:size_t 类型用于指明数组长度,它必须是一个正数;ptrdiff_t 类型则应保证足以存放同一数组中两个指针之间的差距,它有可能是负数。例如,ip 减去 ip2,结果为 -4。

允许在指针上加减 0,使指针保持不变。更有趣的是,如果一指针具有 0 值(空指针),则在该指针上加 0 仍然是合法的,结果得到另一个值为 0 的指针。也可以对两个空指针做减法操作,得到的结果仍是 0。

在指针上加一个整型数值,其结果仍然是指针。允许在这个结果上直接进行解引用操作,而不必先把它赋给一个新指针:



这个表达式计算出 ia 所指向元素后面的第 4 个元素的地址,然后对该地址进行解引用操作,等价于 ia[4]。

加法操作两边用圆括号括起来是必要的。如果写为:



意味着对 ia 进行解引用,获得 ia 所指元素的值 ia[0],然后加 4。

在表达式中使用数组名时,实际上使用的是指向数组第一个元素的指针。

其中一个重要的应用是使用下标访问数组时,实际上是使用下标访问指针:



ia[0] 是一个使用数组名的表达式。在使用下标访问数组时,实际上是对指向数组元素的指针做下标操作。只要指针指向数组元素,就可以对它进行下标操作:



vector 类型提供的 end 操作将返回指向超出 vector 末端位置的一个迭代器。这个迭代器常用作哨兵,来控制处理 vector 中元素的循环。类似地,可以计算数组的超出末端指针的值:



本例中,p 指向数组 arr 的第一个元素,在指针 p 上加数组长度即可计算出数组 arr 的超出末端指针。p 加 5 即得 p 所指向的元素后面的第五个 int 元素的地址——换句话说,p + 5指向数组的超出末端的位置。

C++ 允许计算数组或对象的超出末端的地址,但不允许对此地址进行解引用操作。而计算数组超出末端位置之后或数组首地址之前的地址都是不合法的。

用指针编写以下程序:



这段程序使用了一个我们以前没有用过的 for 循环性质:只要定义的多个变量具有相同的类型,就可以在 for 循环的初始化语句(第 1.4.2 节)中同时定义它们。本例在初始化语句中定义了两个 int 型指针 pbegin 和 pend。

如果指针指向 const 对象,则不允许用指针来改变其所指的 const 值。为了保证这个特性,C++ 语言强制要求指向 const 对象的指针也必须具有 const 特性:



这里的 cptr 是一个指向 double 类型 const 对象的指针,const 限定了 cptr 指针所指向的对象类型,而并非 cptr 本身。也就是说,cptr 本身并不是 const。在定义时不需要对它进行初始化,如果需要的话,允许给 cptr 重新赋值,使其指向另一个 const 对象。但不能通过 cptr 修改其所指对象的值:



把一个 const 对象的地址赋给一个普通的、非 const 对象的指针也会导致编译时的错误:



-=============



允许把非 const 对象的地址赋给指向 const 对象的指针,例如:



尽管 dval 不是 const 对象,但任何企图通过指针 cptr 修改其值的行为都会导致编译时的错误。cptr 一经定义,就不允许修改其所指对象的值。如果该指针恰好指向非 const 对象时,同样必须遵循这个规则。

如果指向 const 的指针所指的对象并非 const,则可直接给该对象赋值或间接地利用普通的非 const 指针修改其值:毕竟这个值不是 const。重要的是要记住:不能保证指向 const 的指针所指对象的值一定不可修改。

在实际的程序中,指向 const 的指针常用作函数的形参。将形参定义为指向 const 的指针,以此确保传递给函数的实际对象在函数中不因为形参而被修改。

除指向 const 对象的指针外,C++ 语言还提供了 const 指针——本身的值不能修改:



我们可以从右向左把上述定义语句读作“curErr 是指向 int 型对象的 const 指针”。与其他 const 量一样,const 指针的值不能修改,这就意味着不能使 curErr 指向其他对象。任何企图给 const 指针赋值的行为(即使给 curErr 赋回同样的值)都会导致编译时的错误:



与任何 const 量一样,const 指针也必须在定义时初始化。

指针本身是 const 的事实并没有说明是否能使用该指针修改它所指向对象的值。指针所指对象的值能否修改完全取决于该对象的类型。例如,curErr 指向一个普通的非常量 int 型对象 errNumb,则可使用 curErr 修改该对象的值:



还可以如下定义指向 const 对象的 const 指针:



本例中,既不能修改 pi_ptr 所指向对象的值,也不允许修改该指针的指向(即 pi_ptr 中存放的地址值)。可从右向左阅读上述声明语句:“pi_ptr 首先是一个 const 指针,指向 double 类型的 const 对象”。

在 typedef(第 2.6 节)中使用指针往往会带来意外的结果。下面是一个几乎所有人刚开始时都会答错的问题。假设给出以下语句:



很多人都认为真正的类型是:



错误的原因在于将 typedef 当做文本扩展了。声明 const pstring 时,const 修饰的是 pstring 的类型,这是一个指针。因此,该声明语句应该是把 cstr 定义为指向 string 类型对象的 const 指针,这个定义等价于:



用 typedef 写 const 类型定义时,const 限定符加在类型名前面容易引起对所定义的真正类型的误解:



把 const 放在类型 pstring 之后,然后从右向左阅读该声明语句就会非常清楚地知道 cstr2 是 const pstring 类型,即指向 string 对象的 const 指针。

不幸的是,大多数人在阅读 C++ 程序时都习惯看到 const 放在类型前面。于是为了遵照惯例,只好建议编程时把 const 放在类型前面。但是,把声明语句重写为置 const 于类型之后更便于理解。

尽管 C++ 支持 C 风格字符串,但不应该在 C++ 程序中使用这个类型。C 风格字符串常常带来许多错误,是导致大量安全问题的根源。

字符串字面值的类型就是 const char 类型的数组。C++ 从 C 语言继承下来的一种通用结构是C 风格字符串,而字符串字面值就是该类型的实例。实际上,C 风格字符串既不能确切地归结为 C 语言的类型,也不能归结为 C++ 语言的类型,而是以空字符 null 结束的字符数组:



ca1 和 cp1 都不是 C 风格字符串:ca1 是一个不带结束符 null 的字符数组,而指针 cp1 指向 ca1,因此,它指向的并不是以 null 结束的数组。其他的声明则都是 C 风格字符串,数组的名字即是指向该数组第一个元素的指针。于是,ca2 和 ca3 分别是指向各自数组第一个元素的指针。

C++ 语言通过(const)char*类型的指针来操纵 C 风格字符串。一般来说,我们使用指针的算术操作来遍历 C 风格字符串,每次对指针进行测试并递增 1,直到到达结束符 null 为止:



while 语句的循环条件是对 const char* 类型的指针 cp 进行解引用,并判断 cp 当前指向的字符是 true 值还是 false 值。真值表明这是除 null 外的任意字符,则继续循环直到 cp 指向结束字符数组的 null 时,循环结束。while 循环体做完必要的处理后,cp 加1,向下移动指针指向数组中的下一个字符。

如果 cp 所指向的字符数组没有 null 结束符,则此循环将会失败。这时,循环会从 cp 指向的位置开始读数,直到遇到内存中某处 null 结束符为止。

表4-1列出了 C 语言标准库提供的一系列处理 C 风格字符串的库函数。要使用这些标准库函数,必须包含相应的 C 头文件:#include <cstring>

cstring 是 string.h 头文件的 C++ 版本,而 string.h 则是 C 语言提供的标准库。

表 4.1. 操纵 C 风格字符串的标准库函数



传递给这些标准库函数例程的指针必须具有非零值,并且指向以 null 结束的字符数组中的第一个元素。其中一些标准库函数会修改传递给它的字符串,这些函数将假定它们所修改的字符串具有足够大的空间接收本函数新生成的字符,程序员必须确保目标字符串必须足够大。

C++ 语言提供普通的关系操作符实现标准库类型 string 的对象的比较。这些操作符也可用于比较指向C风格字符串的指针,但效果却很不相同:实际上,此时比较的是指针上存放的地址值,而并非它们所指向的字符串:



如果 cp1 和 cp2 指向同一数组中的元素(或该数组的溢出位置),上述表达式等效于比较在 cp1 和 cp2 中存放的地址;如果这两个指针指向不同的数组,则该表达式实现的比较没有定义。

字符串的比较和比较结果的解释都须使用标准库函数 strcmp 进行:



标准库函数 strcmp 有 3 种可能的返回值:若两个字符串相等,则返回 0 值;若第一个字符串大于第二个字符串,则返回正数,否则返回负数。

在使用处理 C 风格字符串的标准库函数时,牢记字符串必须以结束符 null 结束:



在这个例题中,ca 是一个没有 null 结束符的字符数组,则计算的结果不可预料。标准库函数 strlen 总是假定其参数字符串以 null 字符结束,当调用该标准库函数时,系统将会从实参 ca 指向的内存空间开始一直搜索结束符,直到恰好遇到 null 为止。strlen 返回这一段内存空间中总共有多少个字符,无论如何这个数值不可能是正确的。

传递给标准库函数 strcat 和 strcpy 的第一个实参数组必须具有足够大的空间存放新生成的字符串。以下代码虽然演示了一种通常的用法,但是却有潜在的严重错误:



问题在于我们经常会算错 largeStr 需要的大小。同样地,如果 cp1 或 cp2 所指向的字符串大小发生了变化,largeStr 所需要的大小则会计算错误。不幸的是,类似于上述代码的程序应用非常广泛,这类程序往往容易出错,并导致严重的安全漏洞。

如果必须使用 C 风格字符串,则使用标准库函数 strncat 和 strncpy 比 strcat 和 strcpy 函数更安全:



如果使用 C++ 标准库类型 string,则不存在上述问题:



每一个程序在执行时都占用一块可用的内存空间,用于存放动态分配的对象,此内存空间称为程序的自由存储区或堆。C 语言程序使用一对标准库函数 malloc 和 free 在自由存储区中分配存储空间,而 C++ 语言则使用 new 和 delete 表达式实现相同的功能。

数组变量通过指定类型、数组名和维数来定义。而动态分配数组时,只需指定类型和数组长度,不必为数组对象命名,new 表达式返回指向新分配数组的第一个元素的指针:



new 表达式需要指定指针类型以及在方括号中给出的数组维数,该维数可以是任意的复杂表达式。创建数组后,new 将返回指向数组第一个元素的指针。在自由存储区中创建的数组对象是没有名字的,程序员只能通过其地址间接地访问堆中的对象。

动态分配数组时,如果数组元素具有类类型,将使用该类的默认构造函数(第 2.3.4 节)实现初始化;如果数组元素是内置类型,则无初始化:



这两个 new 表达式都分配了含有 10 个对象的数组。其中第一个数组是 string 类型,分配了保存对象的内存空间后,将调用 string 类型的默认构造函数依次初始化数组中的每个元素。第二个数组则具有内置类型的元素,分配了存储 10 个 int 对象的内存空间,但这些元素没有初始化。

也可使用跟在数组长度后面的一对空圆括号,对数组元素做值初始化(第 3.3.1 节):



圆括号要求编译器对数组做值初始化,在本例中即把数组元素都设置为0。

如果我们在自由存储区中创建的数组存储了内置类型的 const 对象,则必须为这个数组提供初始化:因为数组元素都是 const 对象,无法赋值。实现这个要求的唯一方法是对数组做值初始化:



C++ 允许定义类类型的 const 数组,但该类类型必须提供默认构造函数:



已创建的常量元素不允许修改——因此这样的数组实际上用处不大。

之所以要动态分配数组,往往是由于编译时并不知道数组的长度。我们可以编写如下代码



有趣的是,如果 get_size 返回 0 则会怎么样?答案是:代码仍然正确执行。C++ 虽然不允许定义长度为 0 的数组变量,但明确指出,调用 new 动态创建长度为 0 的数组是合法的:



用 new 动态创建长度为 0 的数组时,new 返回有效的非零指针。该指针与 new 返回的其他指针不同,不能进行解引用操作,因为它毕竟没有指向任何元素。而允许的操作包括:比较运算,因此该指针能在循环中使用;在该指针上加(减)0;或者减去本身,得 0 值。

动态分配的内存最后必须进行释放,否则,内存最终将会逐渐耗尽。如果不再需要使用动态创建的数组,程序员必须显式地将其占用的存储空间返还给程序的自由存储区。C++ 语言为指针提供 delete [] 表达式释放指针所指向的数组空间:



该语句回收了 pia 所指向的数组,把相应的内存返还给自由存储区。在关键字 delete 和指针之间的空方括号对是必不可少的:它告诉编译器该指针指向的是自由存储区中的数组,而并非单个对象。

理论上,回收数组时缺少空方括号对,至少会导致运行时少释放了内存空间,从而产生内存泄漏(memory leak)。对于某些系统和/或元素类型,有可能会带来更严重的运行时错误。因此,在释放动态数组时千万别忘了方括号对。

以下两段程序反映了使用 C 风格字符串与 C++ 的标准库类型 string 的不同之处。使用 string 类型的版本更短、更容易理解,而且出错的可能性更小:



通常,由于 C 风格字符串与字符串字面值具有相同的数据类型,而且都是以空字符 null 结束,因此可以把 C 风格字符串用在任何可以使用字符串字面值的地方:

可以使用 C 风格字符串对 string 对象进行初始化或赋值。

string 类型的加法操作需要两个操作数,可以使用 C 风格字符串作为其中的一个操作数,也允许将 C 风格字符串用作复合赋值操作的右操作数。

反之则不成立:在要求C风格字符串的地方不可直接使用标准库 string 类型对象。例如,无法使用 string 对象初始化字符指针:



但是,string 类提供了一个名为 c_str 的成员函数,以实现我们的要求:



c_str 函数返回 C 风格字符串,其字面意思是:“返回 C 风格字符串的表示方法”,即返回指向字符数组首地址的指针,该数组存放了与 string 对象相同的内容,并且以结束符 null 结束。

c_str 返回的数组并不保证一定是有效的,接下来对 st2 的操作有可能会改变 st2 的值,使刚才返回的数组失效。如果程序需要持续访问该数据,则应该复制 c_str 函数返回的数组。

使用数组初始化 vector 对象,必须指出用于初始化式的第一个元素以及数组最后一个元素的下一位置的地址:



传递给 ivec 的两个指针标出了 vector 初值的范围。第二个指针指向被复制的最后一个元素之后的地址空间。被标出的元素范围可以是数组的子集:



严格地说,C++ 中没有多维数组,通常所指的多维数组其实就是数组的数组:



第一维通常称为行(row),第二维则称为列(column)。C++ 中并未限制可用的下标个数,也就是说,我们可以定义元素是数组(其元素又是数组,如此类推)的数组。

和处理一维数组一样,程序员可以使用由花括号括起来的初始化式列表来初始化多维数组的元素。对于多维数组的每一行,可以再用花括号指定其元素的初始化式:



其中用来标志每一行的内嵌的花括号是可选的。下面的初始化尽管有点不清楚,但与前面的声明完全等价:



与一维数组一样,有些元素将不使用初始化列表提供的初始化式进行初始化。下面的声明只初始化了每行的第一个元素:



如果省略内嵌的花括号,结果会完全不同:



该声明初始化了第一行的元素,其余元素都被初始化为 0。

为了对多维数组进行索引,每一维都需要一个下标。例如,下面的嵌套 for 循环初始化了一个二维数组:



因为多维数组其实就是数组的数组,所以由多维数组转换而成的指针类型应是指向第一个内层数组的指针。尽管这个概念非常明了,但声明这种指针的语法还是不容易理解:



定义指向数组的指针与如何定义数组本身类似:首先声明元素类型,后接(数组)变量名字和维数。窍门在于(数组)变量的名字其实是指针,因此需在标识符前加上 *。如果从内向外阅读 ip 的声明,则可理解为:*ip 是 int[4] 类型——即 ip 是一个指向含有 4 个元素的数组的指针。

在下面的声明中,圆括号是必不可少的:



typedef 类型定义(第 2.6 节)可使指向多维数组元素的指针更容易读、写和理解。以下程序用 typedef 为 ia 的元素类型定义新的类型名:



可使用 typedef 类型输出 ia 的元素:



外层的 for 循环首先初始化 p 指向 ia 的第一个内部数组,然后一直循环到 ia 的三行数据都处理完为止。++p 使 p 加 1,等效于移动指针使其指向 ia 的下一行(例如:下一个元素)。

内层的 for 循环实际上处理的是存储在内部数组中的 int 型元素值。首先让 q 指向 p 所指向的数组的第一个元素。对 p 进行解引用获得一个有 4 个 int 型元素的数组,通常,使用这个数组时,系统会自动将它转换为指向该数组第一个元素的指针。在本例中,第一个元素是int型数据,q指向这个整数。系统执行内层的 for 循环直到处理完当前 p 指向的内部数组中所有的元素为止。当 q 指针刚达到该内部数组的超出末端位置时,再次对 p 进行解引用以获得指向下一个内部数组第一个元素的指针。在 p 指向的地址上加
4 使得系统可循环处理每一个内部数组的 4 个元素。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: