第一章 C:穿越时空的迷雾 1.1 C语言的史前阶段 1.2 C语言的早期体验 1.数组下标从0而不是1开始 2.C语言的基本数据类型直接与底层硬件相应 3.auto关键字显然是摆设 auto是默认的变量内存分配方式,只对创建符号表入口的编译器设计者有意义。 4.表达式中的数组名可以看作是指针 注:数组和指针并不是在任何情况下都是等效的 5.float被自动扩展为double 注:在ANSIC中不在如此 6.不允许嵌套函数(函数内部包含另一个函数的定义) 简化了编译器,并稍微提高了C程序的运行时组织结构 7.register关键字 这个关键字定义的变量将存放到寄存器中,简化了编译器,但把包袱丢给了程序员 1.3 标准I/O库和C预处理器 C预处理的3个主要功能 1.字符串替换 通常用于为常量提供一个符号名 2.头文件包含 一般性的声明可以被分离到头文件中,并且可以被许多源文件使用。 3.通用代码模板的扩展 宏(marco)在连续几个调用中所接收的类型可以不同(宏的实际参数只是按照原样输出) 1.4 K&R C 《The C programming Langauage》(中文版为《C程序设计语言》) 1.5 今日之ANSI C 1.6 它很棒,但它符合标准吗 1.不可移植的代码(unportable code) (1).由编译器定义的(implementation-defined),不同的编译器可能不同 例:当整型数向右移位时,要不要扩展符号位。 (2).未确定的(unspecified),在某些正确情况下的做法,标准并未明确规定怎么做(即不在ANSI C标准内的做法) 例:参数求值的顺序 2.坏代码(bad code) (1).未定义的(undefined),在某些不正确情况下的做法,但标准为规定怎么做 例:当一个有符号整数溢出该采取什么行动 (2).约束条件(a constraint),必须遵守的限制或要求 例:求余操作符(%)的操作数必须属于整型,在非整型数据上使用%操作符会引发一条错误信息 3.可移植的代码(portable code) (1).严格遵循标准的(strictly-conforming) 只使用已确定的特性 不突破任何由编译器实现的限制 不产生任何依赖由编译器定义的或未确定的或未定义的特性的输出 (2).遵循标准的(conforming),可以依赖一些某种编译器特有的不可移植的特性,但移植时需对其进行修改。 1.7 编译限制 ANSI C 编译器必须能够支持 1.在函数定义中形参数量的上限至少可以达到31个 2.在函数调用时实参数量的上限至少可以达到31个 3.在一条源代码行里至少可以有509个字符 4.在表达式中至少可以支持32层嵌套的括号 5.long int的最大值不得小于2147483647(即long型整数不得低于32位) 1.8 ANSI C标准的结构 1.9 阅读ANSI C标准,寻找乐趣和裨益 1.10 “安静的改变”究竟有多少安静 K&R C采用无符号保留(unsigned preserving)原则,就是当一个无符号类型与int或更小的整型(如char)混合使用时,结果类型是无符号类型,这会使 一个负数丢失符号位。 ANSI C采用值保留(value preserving)原则,即 当执行算术运算时,操作数的类型如果不同,就会发生转换。数据类型一般朝着浮点精度更高、长度更长的方向装换,整型数如果转换为signed不会 丢失信息,就转换为signed,否则转换为unsigned。 对无符号类型的建议 1.尽量不要使用无符号类型,以免增加不必要的复杂性。 2.尽量使用有符号类型,这样在设计升级混合类型的复杂细节时 3.只有在使用位段和二进制掩码时,才可以用无符号数。应该在表达式中使用强制类型转换,使操作数均为有符号数或无符号数,避免编译器来选择结果的类型。 容易混淆的const 关键字const并不能把变量变成常量。只是表示这个变量不能被赋值,即**只读**的,但不能防止通过程序的内部(甚至是外部)的方法来修改这个值。 const最有用之处是限定函数的形参,使得函数不会修改实参指针所指的数据,但其他函数却可能修改它。 建议: const和*的组合通常只用于在数组形式的参数中模拟传值调用。 第二章 这不是Bug,而是语言特性 2.1 这关语言特性何事,在Fortran里这就是Bug呀 NUL:用于结束一个ASCII字符串 NULL:用于表示空指针。 2.2 多做之过 2.2.1 由于存在fall through,switch语句会带来麻烦 switch语句的一般形式如下: switch(表达式) { case 常量表达式:零条或多条语句 default:零条或多条语句 case 常量表达式:零条或多条语句 } 如果没有default,而且所有的case均不匹配,那整条switch语句便什么都不做。 switch存在的问题: switch语句不会再每个case标签后面的语句执行完毕后自动中止。 一旦执行某个case语句,程序将会依次执行后面所有的case,除非遇到break语句。 这称之为"fall through"。 2.2.2 粉笔也成了可用的硬件 ANSI C引入的一个新特性: 相邻的字符串常量将被自动合并成一个字符串的约定 char c = "aa" "bb" c = "aabb" 这省掉了过去在书写多行信息时必须在行末加“\”的做法,但这种自动合并意味着 字符串数组在初始化时,如果不小心漏掉了一个逗号,编译器将不会发出错误信息,而是 悄无声息地把两个字符串合并在一起。 2.2.3 太多的缺省可见性 定义C函数时,在缺省情况下函数的名字是全局可见的,即加不加extern效果一样。 如果想限制对这个函数的访问,必须加个static关键字 function apple() {/* 在任何地方均可见 */} extern function pear() {/* 在任何地方均可见 */} static function turnip() {/* 在这个文件之外不可见 */} 作用域过宽的问题常见于库中:一个库需要让一个对象在另一个库中可见。唯一的办法 是让它变得全局可见,但这样一来,它对于链接到该库的所有对象都是可见的了。 2.3 误做之过 2.3.1 骆驼背上的重载 许多符号是被“重载”的————在不同的上下文环境里有不同的意义。 C语言的符号重载 1.static:在函数内部,表示该变量的值在各个调用间一直保持延续性,即只做一次初始化 在函数这一级,表示该函数只对本文件可见 2.extern:用于函数定义,表示全局可见(属于冗余的) 用于变量,表示它在其他地方定义 3.void :作为函数的返回类型,表示不返回任何值 在指针声明中,表示通用指针的类型 位于参数列表这种,表示没有参数 4.* :乘法运算符 用于指针,间接引用 在声明中,表示指针 5.& :位的AND操作符 取地址操作符 6.<<= :左移复合赋值运算符 7.< :小于运算符 #include指令的左定界符 8.() :在函数定义中,包围形式参数表 调用一个函数 改变表达式的运算次序 将值转换为其他类型(强制类型转换) 定义带参数的宏 包围sizeof操作符的操作数(如果它是类型名) 2.3.2 有些运算符的优先级是错误的 C语言运算符优先级存在的问题 2.3.3 早期gets()中的Bug导致了Internet蠕虫 gets()函数从流中读入一个字符串,但其无法检查缓冲区的空间,故如果函数调用者提供一个指向堆栈的指针, 并且gets()函数读入的字符数量超过缓冲区的空间,多出来的字符会继续写入到堆栈,覆盖原先的内容。 所以推荐用fgets()彻底取代gets(),fgets()函数对读入的字符数设置了一个限制,这样就不会超出缓冲区范围。 2.4 少做之过 2.4.4 编译器日期被破坏 当函数返回的是一个指向局部变量的指针时,当控制流离开局部变量的范围,变量将失效,无法得知变量的内容。 解决方案 1.返回一个指向字符串常量的指针(最简单的解决方案,适用于无需计算字符串的内容) 例:return "Only"; 2.使用全局声明的数组 3.使用静态数组(static关键字) 4.显示的分配一些内存,保存返回的值 但要注意释放内存,在函数内分配后,很容易忘记在使用函数后释放该内存,造成严重的问题。 5.要求调用者分配内存来保存函数的返回值。 例:buffer = malloc(size); fuc(buffer,size); free(buffer); 2.4.5 lint程序绝不应该被分离出来 lint程序是程序的道德准则。当你做错事时,它会告诉你那里不对。应该始终使用lint程序,按照它的道德准则办事。 第三章 分析C语言的声明 3.1 只有编译器才会承认的语法 涉及指针和const的声明可能出现集中不同的顺序: const int * grape; int const * grape; int * const grape_jelly; 在最后一种情况下,指针是只读的,而在另外两种情况下,指针所指向的对象是只读的。 3.对象和指针都是只读的声明方法有两种: const int * const grape_jam; int const * const grape_jam; 3.2 声明是如何形成的 声明的核心————声明器(declarator) 是标识符以及与它组合在一起的任何指针、函数括号、数组下标等。 合法的声明存在限制条件: 1.函数的返回值不能是一个函数,例:foo()()是非法的 2.函数的返回值不能是一个数组,例:foo()[]是非法的 3.数组里面不能有函数, 例:foo[]()是非法的 下面的声明是合法的 1.函数的返回值允许是一个函数指针, 例:int(*foo())() 解析到函数时 int()要看成一个整体 2.函数的返回值允许是一个指向数组的指针,例:int(*foo())[] 3.数组里面允许有函数指针, 例:int(*foo[])() 4.数组里面允许有其他数组, 例:int foo[][] 3.2.1 关于结构 结构就是一种把一些数据项组合在一起的数据结构。 结构的通常形式是: struct 结构标签(可选){ 类型1 标识符1; 类型2 标识符2; ... 类型N 标识符N; }变量定义(可选); 结构的内容可以是任何其他数据声明:单个数据项、数组、其他结构、指针等。 结构的参数传递: 1.参数在传递时首先尽可能地存放到寄存器中(追求速度) 注:对于小型数据类型,编译器可能会选择将参数存放在寄存器中以提高速度,而对于较大的数据类型(数组、结构),参数很可能会被传递到堆栈中。 3.2.2 关于联合 联合的一般形式: union 可选的标签 { 类型1 标识符1; 类型2 标识符2; ... 类型N 标识符N; }可选的变量定义。 联合(一般作为大型结构的一部分存在)的作用: 1.节省空间,有些数据项不可能同时出现,可用联合节省空间 2.把同一个数据解释成两种不同的东西。 3.2.3 关于枚举 枚举(enum)通过一种简单的途径,把一串名字与一串整型值联系在一起。 枚举的一般形式: enum 可选的标签 {内容...}可选的变量定义; 3.3 优先级规则 C语言声明的优先级规则 A 声明从它的名字开始读取,然后按照优先级顺序依次读取 B 优先级从高到低依次是: B.1 声明中被括号括起来的那部分 B.2 后缀操作符: 括号()表示这是一个函数,而 方括号[]表示这是一个数组。 B.3 前缀操作符:星号*表示 “指向...的指针” C 如果const和(或)volatile关键字的后面紧跟类型说明符(如int,long等),其作用于类型说明符 在其他情况下,const和(或)volatile关键字作用于它左边紧邻的指针星号。 3.4 通过图表分析C语言的声明 char * const *(*next)(); 结果是: next是一个指向函数的指针,该函数返回另一个指针,该指针指向一个只读的指向char的指针。 3.5 typedef可以成为你的朋友 typedef为一种类型引入新的名字,而不是为变量分配空间。“宣称这个名字是指定类型的同义词” 一般情况下,typedef用于简洁地表示指向其他东西的指针。 注:1.不要在一个typedef中放入几个声明器。 2.千万不要把typedef嵌入到声明的中间部分。 3.7 typedef struct foo{... foo;}的含义 例:typedef struct fruit {int weight,price_per_1b;}fruit; /*语句1*/ 语句1声明了结构标签“fruit”和由typedef声明的结构类型“fruit” 操作typedef的提示 typdef应该用在: 1.数组、结构、指针以及函数的组合类型 2.可移植类型。 当把代码移植到不同平台时,要选择正确的类型如short、int、long时,只要在typedef中进行修改。 3.typedef也可以为后面的强制类型转换提供一个简单的名字。 第四章 令人震惊的事实:数组和指针并不相同 4.1 数组并非指针 extern int *x; extern int y[10]; 第一条语句声明x是个int型的指针;第二条语句声明y是个int型数组。 4.2 我的代码为什么无法运行 例:文件1: int mango[100]; 文件2: extern int *mango; 这相当于把整数和浮点数混为一谈,即类型不匹配(int数组和int指针并不是同一数据类型) 之所以会产生这种混淆,是因为对数组的引用总可以写成对指针的引用,而且确实存在一种指针和数组的定义完全相同的上下文环境。 4.3 什么是声明,什么是定义。 定义:只能出现在一个地方 确定对象的类型并分配内存,用于创建新的对象。例:int my_array[100]; 声明:可以多次出现 描述对象的类型,用于指代其他地方定义的对象(例如在其他文件里)。例:extern int my_array[]; 注:声明不分配内存,只是描述其他地方创建的对象。 数组下标引用与指针的区别: 1.关键:每个符号的地址在编译时可知。故数组下标可以直接操作;但指针使用时需要在运行时取得它的当前值, 然后对它进行解除引用。 2.对于指针,编译器想要取得指针指向的内容,需要先得到指针的内容,把它作为指针指向的内容的地址,再到 这个地址去取内容。 4.3.2 当你“定义为指针,但以数组方式引用”时会发生什么 例:char *p = "abcdefgh";c = p[3]; 编译器将会: 1.取得符号表示p的地址,提取存储于此处的指针 2.把下标所表示的偏移量与指针的值相加,产生一个地址。 3.访问上面这个地址,取得字符。 4.5 数组和指针的其他区别 表4-1 数组和指针的区别 ------------------------------------------------------------------------------------ | 指针 | 数组 | |----------------------------------------|-----------------------------------------| |保存数据的地址 |保存数据 | |----------------------------------------|-----------------------------------------| |间接访问数据,首先取得指针的内容,把它作为 |直接访问数据,a[I]只是简单地以a+I为地址取得 | |地址,然后从这个地址提取数据。 |数据 | |如果指针有一个下标[I],就把指针的内容加上I | | |作为地址,从中提取数据 | | |----------------------------------------|-----------------------------------------| |通常用于动态数据结构 |通常用于存储固定数目且数据类型相同的元素 | |----------------------------------------|-----------------------------------------| |相关的函数为malloc(),free()。 |隐式分配和删除 | |----------------------------------------|-----------------------------------------| |通常指向匿名数据 |自身即为数据名 | ------------------------------------------------------------------------------------ 第五章 对链接的思考 5.1 函数库、链接和载入 链接器位于编译过程的哪一阶段 编译器驱动器(compiler driver)包括 预编译器(preprocessor)、语法和语义检查器(syntactic and semantic checker)、 代码生成器(code generator)、汇编程序(assembler)、优化器(optimizer)、 链接器(linker)、驱动器程序(driver program) 优化器几乎可以加在上述所有阶段的后面。 静态链接:函数库的一份拷贝是可执行文件的物理组成部分; 动态链接:可执行文件只是包含文件名,让载入器在运行时能够寻找程序所需要的函数库。 5.2 动态链接的优点 动态链接是一种更为现代的方法,可执行文件的体积可以非常小。 虽然运行速度稍慢,但能够更有效地利用磁盘空间,编译-编辑阶段时间缩短。 动态链接的主要目的:把程序与它们使用的特定的函数库版本中分离出来。 约定由系统向程序提供一个接口,介于应用程序和函数库二进制可执行文件 所提供的服务之间的接口,称为应用程序二进制接口(Application Binary Interface,ABI). 动态链接必须保证4个特定的函数库: libc(C运行时函数库)、libsys(其他系统函数)、libX(X windowing)和libnsl(网络服务). 动态链接可以从两个方面提高性能: 1.动态链接可执行文件比功能相同的静态链接可执行文件的体积小。 2.所有动态链接到某个特定函数库的可执行文件在运行时共享该函数库的一个单独拷贝。 动态链接是一种"just-in-time(JIT)"链接,这意味着程序在运行时必须能够找到它们所需要的函数库。 链接器通过把库文件名或路径名植入可执行文件来完成,故函数库的路径不能随意移动,除非在链接器中 进行特别说明。 静态库(archive):通过ar(用于archive的实用工具)来创建和更新。拓展名为“.a” 动态链接库有链接编辑器ld创建。拓展名为".so" 5.3 函数库链接的5个特殊秘密 1.静态库(archive):通过ar(用于archive的实用工具)来创建和更新。拓展名为“.a” 动态链接库有链接编辑器ld创建。拓展名为".so" 2.例如,你通过-lthread选项,告诉编译链接到libthread.so 编译器被告知根据选项-lthread链接到相应的函数库,函数库的名字是libthread.so——"lib" 部分和文件的扩展名被省掉了,但在前面加一个"l"。 3.编译器期望在确定的目录找到库 编译器查看一些特殊的位置,如在/usr/lib中查找函数库。 同时,编译器选项-Lpathname告诉链接器一些其他的目录,如果命令中加入了-l选项,链接器就 往这些目录查找函数库。同理-Rpathname选项也是如此。 同时系统中存在几个环境变量,LD_LIBRARY_PATH和LD_RUN_PATH,也用于提供这类信息,但出于 安全性、性能和创建/运行独立性方面的考虑,不提倡使用环境变量。 4.与提取动态库中的符号相比,静态库中的符号提取的方法限制更严 5.5 产生链接器报告文件 可以在ld程序中使用"-m"选项,让链接器产生一个报告。 第六章 运动的诗章:运行时数据结构 6.1 a.out及其传说 a.out是“assembler output(汇编程序输出)”的缩写形式 注:事实上它不是汇编程序输出,而是链接器输出!!! 6.2 段(segments) 目标文件和可执行文件可以有几种不同的格式。 这些不同的格式具有一个共同的概念:段(segments). 还有另一个概念section 在UNIX中,段表示一个二进制文件相关的内容块。(在本书中不作说明,段都指UNIX上的段) 6.3 操作系统在a.out文件里干了些什么 文本段包含程序的指令。 数据段包含经过初始化的全局和静态变量以及它们的值。 BSS段大小从可执行文件中得到,紧跟在数据段之后,当内存区进入程序的地址空间后全部清零。 数据区:数据段和BSS段。 堆栈段(stack segment):用于保存局部变量、临时数据、传递到函数中的参数等。 堆(heap)空间:用于动态分配的内存。 注:虚拟地址空间的最低部分未被映射,用于捕捉空指针和小整型值的指针引用内存的情况。 6.4 C语言运行时系统在a.out文件里干了些什么 堆栈段 1.堆栈为函数内部声明的局部变量提供存储空间 2.进行函数调用时,对照存储于此有关的一些维护性信息。 称为:堆栈结构(stack frame),或者叫过程活动记录(precedure activation recored). 包括函数调用地址(即当所调用的函数结束后跳回的地方)、任何不适合装入寄存器的参数以及一些寄存器值的保存。 3.用作暂时存储区。alloca()函数分配。 6.7 控制线程 在进程中支持不同的控制线程只用简单地为每个控制线程**分配不同的堆栈即可**。 6.9 UNIX中的堆栈段 在UNIX中,当进程需要更多空间时,堆栈会自动生长。 当试图访问当前系统分配给堆栈的空间之外时,它将产生一个硬件中断,称为页错误(page fault)。 在堆栈顶部的下端有一个称为red zone的小型区域,如果对这个区域进行引用,并不会产生失败。 内存映射硬件确保你无法访问操作系统分配给你的进程之外的内存。 第七章 对内存的思考 7.1 Intel 80x86系列 7.2 Intel 80x86内存模型以及它的工作原理 8086中的段是一块64KB的内存区域,由一个段寄存器所指向。 7.3 虚拟内存 虚拟内存使用磁盘而不是主存来保存运行进程的映像。 虚拟内存通过"页"的形式组织。 页:操作系统在磁盘和内存之间移来移去或进行保护的单位,一般为几K字节。 进程只能操作位于物理内存的页面。 操作系统使用相同的底层数据结构(vnode"虚拟结点")来操纵文件系统和内存。 7.4 Cache存储器 Cache存储器:容量小、价格高、速度快。 Cache位于CPU和内存之间,是一种极快的存储缓冲区。 Cache包含一个地址的列表以及它们的内容。 1.全写法(write-through)Cache:每次写入Cache时总是同时写入到内存中,使内存和 Cache始终保持一致。 2.写回法(write-back)Cache:当第一次写入时,只对Cache进行写入。 7.5 数据段和堆 堆区域用于动态分配的存储,即通过malloc(内存分配)函数获得的内存,并通过指针访问。 calloc函数在返回指针之前先把分配好的内存的内容都清空为零。 realloc函数改变一个指针所指向的内存块的大小,既可以扩大,也可以缩小, 它经常把内存拷贝到别的地方然后将指向新地址的指针返回。 堆内存管理策略: 建立一个可用块("自由存储区")的链表,每块由malloc分配的内存块都在自己的前面标明自己的大小。 堆的末端由一个称为break的指针来标识。 7.6 内存泄漏 堆经常出现两种类型的问题: 1.释放或改写仍在使用的内存(称为"内存损坏")。 2.未释放不再使用的内存(称为"内存泄漏")。 避免内存泄漏的方法: 在可能的时候使用alloca()来分配动态内存。当离开调用alloca的函数时,它所分配的内存会被自动释放。 但alloca函数并不是一种可移植的方法。 7.7 总线错误 7.7.1 总线错误 总线错误几乎都是由于未对齐的读或写引起的。 出现未对齐的内存访问请求时,被堵塞的组件就是地址总线。 对齐(alignment):数据项只能存储在地址是数据项大小的整数倍的内存位置上。 编译器通过自动分配和填充数据(在内存中)来进行对齐。 7.7.2 段错误 段错误或段违规(segmentation violation): 由于内存管理单元(负责支持虚拟内存的硬件)的异常所致,而该异常则通常是由于接触引用一个未初始化 或非法值的指针引起的。 通常导致段错误的几个直接原因: 1.解除引用一个包含非法值的指针 2.解除引用一个空指针(常常由于从系统程序中返回空指针,并未检查就使用)。 3.在未得到正确的权限时进行访问。例:试图往一个只读的文本段存储值 4.用完了堆栈或堆空间(虚拟内存虽然巨大但并非无限)。 最终可能导致段错误的常见编程错误是: 1.坏指针错误: 在指针赋值前就用它来引用内存, 向库函数传送一个坏指针。 对指针进行释放之后再访问它的内容 2.改写(overwrite)错误: 越过数组边界写入数据,在动态分配的内存两端之外写入数据 改写一些堆管理数据结构(在动态分配的内存之前的区域写入数据容易发生这种情况) 3.指针释放引起的错误: 释放同一个内存块两次 释放一块未曾使用malloc分配的内存 释放仍在使用中的内存 释放一个无效的指针 第八章 为什么程序员无法分清万圣节和圣诞节 第九章 再论数组 数组和指针相同的规则 规则1.表达式中的数组名(与声明不同)被编译器当做一个指向该数组第一个元素的指针 在表达式中,指针和数组时可以互换的,因为它们在编译器里的最终形式都是指针,并且都可以进行取下标操作。 注:以下情况对数组的引用不能用第一个元素的指针来代替: 1.数组作为sizeof()的操作数,此时需要的是整个数组的大小,例:sizeof(a[]) 2.使用&操作符取数组地址 3.数组是一个字符串(或宽字符串)常量初始值 规则2.下标总是与指针的偏移量相同 在编写数组算法时,使用指针在通常情况下并不比使用数组“更有效率”。 规则3.在函数参数的声明中,数组名被编译器当做指向该数组第一个元素的指针 出于效率原因的考虑,编译器只向函数传递数组的地址,而不是整个数组的拷贝 9.5 数组和指针可交换性的总结 1.用a[i]这样的形式对数组进行访问总是被编译器“改写”或解释为像*(a+1)的指针访问 2.指针始终是指针。它绝不可改写为数组。用下标形式访问指针,一般都是指针作为函数参数时,而实际传递给函数的是一个数组。 3.在特定的上下文中,也就是它作为函数的参数(也只有这种情况),一个数组的声明可以看作一个指针。 4.当把一个数组定义为函数的参数时,可以选择把它定义为数组,也可以定义为指针。 5.在其他所有情况下,定义和声明必须匹配。即:定义一个数组,在其他文件对它进行声明也必须声明为数组。 第十章 再论指针 10.1 多维数组的内存布局 对于二维数组pea[i][j],被编译器解析为 *(*(pea + i) + j) 10.4 向函数传递一个一维数组 形参被改写为指向数组第一个元素的指针,需要一个约定来提示数组的长度。 1.增加一个额外的参数,来表示元素的数目(argc就是起这个作用) 2.赋予数组最后一个元素一个特殊的值,提示它是数组的尾部(字符串结尾的'\0'字符)。 这个特殊值必须不会作为正常的元素值在数组中出现。 二维数组,需要两个预定,一个提示每行的结束,另一个提示所有行的结束。 1.接收一个指向数组第一个元素的指针,每次对指针执行自增操作时,指针就指向数组中下一行的起始位置。 增加一个额外的行,行内所有元素的值都不可能在数组正常元素中出现,用来提示数组超出范围。当对指针 进行自增操作时,要对它进行检查,看指针是否到达额外的行。 2.定义一个额外的参数,提示数组的行数。 10.5 使用指针向函数传递一个多维数组 在函数内部声明一个二维数组参数 在C语言中,无法向函数传递一个普通的多维数组(三维及三维以上) invert_in_palce(int a[][3][5]) 可以调用 int b[10][3][5]; invert_in_palce(b); int b[999][3][5]; invert_in_palce(b); 但无法调用任意的三维数组(第二、第三位不同) int fails1[10][5][5]; invert_in_palce(fails1);//无法通过编译 int fails2[999][3][6]; invert_in_palce(fails2);//无法通过编译 10.5.1 方法一 my_function(int my_array[10][20]); 最简单,但它迫使函数只处理10行20列的int数组。 10.5.2 方法二 省略第一维的长度 my_function(int my_array[][20]); 不够充分,限制每一行必须正好是20个整数的长度。也可声明为 my_function(int(*my_array)[20]); 括号是必须的,确保它是一个指向20个元素的int数组的指针,而不是一个20个int指针元素的数组 10.7 使用指针创建和使用动态数组 有一个realloc()函数,能对一个现在的内存块大小进行重新分配,同时不丢失原先内存块的内容。 当需要在动态表中增长一个项目时,可以进行如下操作 1.对表进行检查,看它是否已满 2.如果已满,使用realloc()函数扩展表的长度。并进行检查,确保realloc()操作成功 3.在表中增加所需要的项目