C语言解惑:指针、数组、函数和多文件编程.pdf
http://www.100md.com
2020年4月2日
![]() |
| 第1页 |
![]() |
| 第7页 |
![]() |
| 第11页 |
![]() |
| 第25页 |
![]() |
| 第40页 |
![]() |
| 第581页 |
参见附件(8108KB,975页)。
C语言解惑是作者刘燕君写的关于c语言知识的书籍,主要讲述了指针基础知识,一维数组,指针与数组,函数基础知识,函数设计,函数设计实例,多文件综合设计实例等内容。

C语言解惑内容简介
本书的前提是读者已经学过C语言,书中将完整、系统地论述各个部分的知识并结合实用程序和趣味游戏程序,综合讲解函数设计、多文件编程和结构化程序设计的方法。本书既可以作为教师、学生及工程技术人员的参考书,也可以作为常备手册。
精彩内容
现在流行的为32位操作系统配备的C编译器已经能识别长文件名,不再受8位的限制。另外,在选取时不仅要保证正确性,还要考虑容易区分,不易混淆。例如,数字1和字母i在一起,就不易辨认。在取名时,还应该使名字有很清楚的含义,例如使用area作为求面积函数的名字,area的英文含义就是“面积”,这就很容易从名字猜出函数的功能。对一个可读性好的程序,必须选择恰当的标识符,取名应统一规范,以便使读者能一目了然。在现在的编译系统中,内部名字中至少前31个字符是有效的,所以应该采用直观的名字。一般可以遵循如下简单规律。
1)使用能代表数据类型的前缀。
2)名称尽量接近变量的作用。
3)如果名称由多个英文单词组成,每个单词的第一个字母大写。
4)由于库函数通常使用下划线开头的名字,因此不要将这类名字用作变量名。
5)局部变量使用比较短的名字,尤其是循环控制变量(又称循环位标)的名字。
6)外部变量使用比较长且贴近所代表变量的含义。
7)函数名字使用动词,如Get_char(void)。变量使用名词,如iMen_Number。
变量命名可以参考windows API编程推荐的匈牙利命名法。它是通过在数据和函数名中加入额外的信息,既增进程序员对程序的理解,又方便查错。
所有的变量在使用之前必须声明,所谓声明即指出该变量的数据类型及长度等信息。声明由类型和具有该类型的变量列表组成。
章节目录
第1章 引入指针变量
1.1 变量的三要素
1.2 变量的操作
1.3 指针变量
1.4 指针类型
第2章 指针基础知识
2.1 指针运算符
2.2 指针移动
2.3 指针地址的有效性
2.4 指针的初始化
2.5 指针相等
2.6 对指针使用const限定符
2.7 使用动态内存
2.7.1 动态内存分配函数
2.7.2 内存分配实例
2.7.3 NULL指针
第3章 一维数组
3.1 一维数值数组
3.2 一维字符串数组
3.3 使用一维数组容易出现的错误
3.3.1 一维数组越界错误
3.3.2 一维数组初始化错误
3.3.3 数组赋值错误
3.3.4 求值顺序产生歧义错误
3.4 综合实例
第4章 指针与数组
4.1 数组与指针的关系
4.2 一维字符串数组与指针
4.3 字符串常量
4.4 指针数组
4.5 配合使用一维数组与指针
4.5.1 使用一维数组名简化操作
4.5.2 使用指针操作一维数值数组
4.5.3 使用一维字符数组
4.5.4 指针初始化实例
4.6 动态内存分配与非数组的指针
4.7 二维数组与指针
4.7.1 二维数组
4.7.2 二维数组操作实例
4.7.3 二维数组与指针的关系
4.7.4 二维数组与指向一维数组的指针
4.7.5 二维字符串数组
4.8 综合设计实例
4.8.1 使用数组求解
4.8.2 使用动态内存求解
4.8.3 使用二级字符串指针求解
第5章 函数基础知识
5.1 函数
5.1.1 函数和函数原型
5.1.2 函数值和return语句
5.1.3 函数调用形式
5.1.4 函数参数的基础知识
5.1.5 被调用函数的返回位置
5.2 C程序的典型结构
5.2.1 单文件结构
5.2.2 一个源文件和一个头文件
5.2.3 多文件结构
5.3 变量的作用域
5.3.1 单文件里的块结构及函数
5.3.2 单文件多函数的变量
5.3.3 多文件变量作用域
5.4 变量的存储地址分配
5.4.1 单文件变量的存储地址分配
5.4.2 多文件变量的存储地址分配
5.5 main函数原型及命令行参数
第6章 函数设计
6.1 函数设计的一般原则
6.1.1 函数设计基础
6.1.2 函数设计的注意事项
6.1.3 函数的一般结构
6.2 函数的返回值
6.2.1 无返回值的void类型函数
6.2.2 非void类型的函数必须返回一个值
6.2.3 使用临时变量作为返回值的函数
6.2.4 不能使用临时数组名作为返回值
6.2.5 返回临时指针必须是首地址
6.2.6 返回结构的函数
6.2.7 返回结构指针的函数
6.2.8 返回枚举的函数
6.3 函数参数的传递方式
6.3.1 传数值
6.3.2 传地址值
6.4 函数指针
6.5 理解函数声明
6.5.1 词法分析中的“贪心法”
6.5.2 克服语法“陷阱”读懂函数
6.6 函数设计举例
6.6.1 完璧归赵
6.6.2 多余的参数
6.6.3 传递的参数与函数参数匹配问题
6.6.4 等效替换参数
6.6.5 设计状态机函数
第7章 函数设计实例
7.1 函数的类型和返回值
7.1.1 函数的类型应力求简单
7.1.2 实参要与函数形参的类型匹配
7.1.3 正确设计函数的返回方式
7.1.4 正确区别指针函数和函数指针
7.2 正确选择函数参数
7.2.1 使用结构作为参数
7.2.2 使用键盘为参数赋值
7.2.3 结构的内存分配
7.3 算法基本概念
7.4 使用库函数
7.5 设计实例
7.5.1 递推与递归
7.5.2 递推求解切饼问题
7.5.3 八皇后问题
7.5.4 疑案求解
7.5.5 二分查找
7.5.6 汉诺塔问题
7.5.7 青蛙过河
7.5.8 猜数游戏
7.5.9 生死游戏
7.5.10 最短路径
第8章 多文件中的函数设计
8.1 C语言预处理器
8.1.1 宏定义与const修饰符
8.1.2 文件包含
8.1.3 条件编译
8.2 模块化程序设计基础
8.2.1 模块化程序设计
8.2.2 分块开发
8.2.3 工程文件
8.2.4 函数设计的注意事项
8.3 使用两个文件的设计实例
8.3.1 设计题目和实现方法
8.3.2 算法和函数设计
8.3.3 完整源程序
8.3.4 组成工程并运行程序
8.4 使用3个文件的设计实例
8.4.1 设计思想
8.4.2 算法分析
8.4.3 完整源程序
8.4.4 程序运行
8.5 使用条件编译的多文件设计实例
8.5.1 实现功能
8.5.2 设计思想
8.5.3 参考程序
8.5.4 程序运行
第9章 多文件综合设计实例
9.1 使用链表设计一个小型通讯录程序
9.1.1 功能设计要求
9.1.2 设计思想
9.1.3 程序设计
9.1.4 运行示范
9.2 使用数组设计一个实用的小型学生成绩管理程序
9.2.1 功能设计要求
9.2.2 总体设计
9.2.3 函数设计
9.2.4 参考程序
9.2.5 运行示范
第10章 设计游戏程序实例
10.1 剪刀、石头、布
10.1.1 设计思想
10.1.2 参考程序
10.1.3 运行示范
10.2 迷宫
10.2.1 设计思想
10.2.2 参考程序
10.2.3 运行示范
10.3 空战
10.3.1 设计思想
10.3.2 参考程序
10.4 贪吃蛇
10.4.1 供改造的源程序
10.4.2 运行示范
10.5 停车场
10.5.1 参考程序
10.5.2 运行示范
10.6 画矩形
10.6.1 用C语言编写Windows程序
10.6.2 Windows的程序结构
10.6.3 用C语言编写画矩形程序
10.7 俄罗斯方块
10.7.1 基本游戏规则
10.7.2 基本操作方法
10.7.3 编写游戏交互界面问题
10.7.4 用C语言编写控制台俄罗斯方块游戏
10.7.5 编写Windows俄罗斯方块游戏
10.8 用C语言编写Windows下的贪吃蛇游戏
10.8.1 程序清单
10.8.2 运行示范
C语言解惑:指针、数组、函数和多文件编程截图


C语言解惑:指针、数组、函数和多文件编程
刘振安 刘燕君 编著
ISBN:978-7-111-55406-6
本书纸版由机械工业出版社于2016年出版,电子版由华
章分社(北京华章图文信息有限公司,北京奥维博世图书发
行有限公司)全球范围内制作与发行。
版权所有,侵权必究
客服热线:+ 86-10-68995265
客服信箱:service@bbbvip.com
官方网址:www.hzmedia.com.cn
新浪微博 @华章数媒
微信公众号 华章电子书(微信号:hzebook)目录
前言
第1章 引入指针变量
1.1 变量的三要素
1.2 变量的操作
1.3 指针变量
1.4 指针类型
第2章 指针基础知识
2.1 指针运算符
2.2 指针移动
2.3 指针地址的有效性2.4 指针的初始化
2.5 指针相等
2.6 对指针使用const限定符
2.7 使用动态内存
2.7.1 动态内存分配函数
2.7.2 内存分配实例
2.7.3 NULL指针
第3章 一维数组
3.1 一维数值数组
3.2 一维字符串数组
3.3 使用一维数组容易出现的错误3.3.1 一维数组越界错误
3.3.2 一维数组初始化错误
3.3.3 数组赋值错误
3.3.4 求值顺序产生歧义错误
3.4 综合实例
第4章 指针与数组
4.1 数组与指针的关系
4.2 一维字符串数组与指针
4.3 字符串常量
4.4 指针数组
4.5 配合使用一维数组与指针4.5.1 使用一维数组名简化操作
4.5.2 使用指针操作一维数值数组
4.5.3 使用一维字符数组
4.5.4 指针初始化实例
4.6 动态内存分配与非数组的指针
4.7 二维数组与指针
4.7.1 二维数组
4.7.2 二维数组操作实例
4.7.3 二维数组与指针的关系
4.7.4 二维数组与指向一维数组的指针
4.7.5 二维字符串数组4.8 综合设计实例
4.8.1 使用数组求解
4.8.2 使用动态内存求解
4.8.3 使用二级字符串指针求解
第5章 函数基础知识
5.1 函数
5.1.1 函数和函数原型
5.1.2 函数值和return语句
5.1.3 函数调用形式
5.1.4 函数参数的基础知识
5.1.5 被调用函数的返回位置5.2 C程序的典型结构
5.2.1 单文件结构
5.2.2 一个源文件和一个头文件
5.2.3 多文件结构
5.3 变量的作用域
5.3.1 单文件里的块结构及函数
5.3.2 单文件多函数的变量
5.3.3 多文件变量作用域
5.4 变量的存储地址分配
5.4.1 单文件变量的存储地址分配
5.4.2 多文件变量的存储地址分配5.5 main函数原型及命令行参数
第6章 函数设计
6.1 函数设计的一般原则
6.1.1 函数设计基础
6.1.2 函数设计的注意事项
6.1.3 函数的一般结构
6.2 函数的返回值
6.2.1 无返回值的void类型函数
6.2.2 非void类型的函数必须返回一个值
6.2.3 使用临时变量作为返回值的函数
6.2.4 不能使用临时数组名作为返回值6.2.5 返回临时指针必须是首地址
6.2.6 返回结构的函数
6.2.7 返回结构指针的函数
6.2.8 返回枚举的函数
6.3 函数参数的传递方式
6.3.1 传数值
6.3.2 传地址值
6.4 函数指针
6.5 理解函数声明
6.5.1 词法分析中的“贪心法”
6.5.2 克服语法“陷阱”读懂函数6.6 函数设计举例
6.6.1 完璧归赵
6.6.2 多余的参数
6.6.3 传递的参数与函数参数匹配问题
6.6.4 等效替换参数
6.6.5 设计状态机函数
第7章 函数设计实例
7.1 函数的类型和返回值
7.1.1 函数的类型应力求简单
7.1.2 实参要与函数形参的类型匹配
7.1.3 正确设计函数的返回方式7.1.4 正确区别指针函数和函数指针
7.2 正确选择函数参数
7.2.1 使用结构作为参数
7.2.2 使用键盘为参数赋值
7.2.3 结构的内存分配
7.3 算法基本概念
7.4 使用库函数
7.5 设计实例
7.5.1 递推与递归
7.5.2 递推求解切饼问题
7.5.3 八皇后问题7.5.4 疑案求解
7.5.5 二分查找
7.5.6 汉诺塔问题
7.5.7 青蛙过河
7.5.8 猜数游戏
7.5.9 生死游戏
7.5.10 最短路径
第8章 多文件中的函数设计
8.1 C语言预处理器
8.1.1 宏定义与const修饰符
8.1.2 文件包含8.1.3 条件编译
8.2 模块化程序设计基础
8.2.1 模块化程序设计
8.2.2 分块开发
8.2.3 工程文件
8.2.4 函数设计的注意事项
8.3 使用两个文件的设计实例
8.3.1 设计题目和实现方法
8.3.2 算法和函数设计
8.3.3 完整源程序
8.3.4 组成工程并运行程序8.4 使用3个文件的设计实例
8.4.1 设计思想
8.4.2 算法分析
8.4.3 完整源程序
8.4.4 程序运行
8.5 使用条件编译的多文件设计实例
8.5.1 实现功能
8.5.2 设计思想
8.5.3 参考程序
8.5.4 程序运行
第9章 多文件综合设计实例9.1 使用链表设计一个小型通讯录程序
9.1.1 功能设计要求
9.1.2 设计思想
9.1.3 程序设计
9.1.4 运行示范
9.2 使用数组设计一个实用的小型学生成绩管理程
序
9.2.1 功能设计要求
9.2.2 总体设计
9.2.3 函数设计
9.2.4 参考程序
9.2.5 运行示范第10章 设计游戏程序实例
10.1 剪刀、石头、布
10.1.1 设计思想
10.1.2 参考程序
10.1.3 运行示范
10.2 迷宫
10.2.1 设计思想
10.2.2 参考程序
10.2.3 运行示范
10.3 空战
10.3.1 设计思想10.3.2 参考程序
10.4 贪吃蛇
10.4.1 供改造的源程序
10.4.2 运行示范
10.5 停车场
10.5.1 参考程序
10.5.2 运行示范
10.6 画矩形
10.6.1 用C语言编写Windows程序
10.6.2 Windows的程序结构
10.6.3 用C语言编写画矩形程序10.7 俄罗斯方块
10.7.1 基本游戏规则
10.7.2 基本操作方法
10.7.3 编写游戏交互界面问题
10.7.4 用C语言编写控制台俄罗斯方块游戏
10.7.5 编写Windows俄罗斯方块游戏
10.8 用C语言编写Windows下的贪吃蛇游戏
10.8.1 程序清单
10.8.2 运行示范
附录 7位ASCII码表
参考文献前言
C语言编程仍然是编程工作者必备的技能。本书的基础
版本《C语言解惑》[1]通过比较编程中存在的典型错误,从
而实现像雨珠打在久旱的沙滩上一样滴滴入骨的效果,使学
习者更容易记住编程的要诀,并通过演示如何将一个能运行
的程序优化为更好、更可靠的程序,使读者提高识别坏程序
和好程序的能力。尽管如此,那本书仍然要照顾初学者并兼
顾知识的完整性,所以讨论的深度有所限制。为此,我们决
定推出它的提高版,并将讨论聚焦于函数设计。
本书将集中讨论C语言的核心部分——函数设计。函数
设计涉及函数类型、函数参数及返回值,这就要求读者熟练
掌握指针和数组的知识,此外,还要掌握多文件编程以及多
文件之间的参数传递等知识。
因为本书要求读者已经学过C语言,所以我们可以完
整、系统地论述各个部分的内容,无须赘述基础知识。本书的另一个特点是每一章之间都有知识交叉,进而达到讲透的
目的。如果遇到不清楚的知识点,读者可以自行学习相应参
考资料,也可以与《C语言解惑》配合学习。
本书的落脚点是实现C语言的结构化程序设计。为实现
这一目标,本书专门选择了完整的设计实例。尤其是第10
章,结合趣味游戏程序,综合讲解函数设计和多文件编程。
本书各个部分论述详细,涉及的知识面广,有些知识是
传统教材中所没有的,所以它既可以作为从事教学的老师及
工程技术人员的参考书,也可以作为常备手册。其实,它不
仅对工程技术人员极有参考价值,也能帮助在校生进行编程
训练或作为毕业论文的参考资料。此外,本书对于初学者也
大有帮助,他们可以将它作为课外读物,对目前看不懂的地
方,可以等具备相关知识之后再来研究,彼时将收获更大。
总之,本书能帮助各类人群找到自己需要的知识并有所收
获,而这也将拓宽本书的应用范围。本书共分10章。第1章通过例子说明引入指针变量的必
要性并简单介绍指针变量的基本性质。第2章通过实例解释
指针的基本性质。第3章介绍数组及数组的边界不对称性。
第4章介绍C语言中两个非常重要的概念——数组和指针。第
5章介绍如何掌握函数设计和调用的正确方法。第6章介绍如
何设计合理的函数类型及参数传递方式。第7章先讨论函数
设计的一般原则,然后结合典型算法,用实例说明设计的具
体方法,以便使读者进一步开阔眼界。第8章结合具体实例
详细介绍头文件的编制、多个C语言文件及工程文件的编制
等方法,以提高读者的多文件编程能力。第9章给出两个典
型的多文件编程实例,一个使用链表,另一个使用数组。第
10章中的游戏程序实例将加深读者对一个完整工程项目的理
解。为了学习方便,本书提供全部程序代码。
本书的两位作者分别撰写各章的不同小节,然后逐章讨
论并独立成章。刘燕君负责第1~6章,刘振安负责第7~10
章,最后由刘振安统稿。参与本书工作的还有周淞梅实验师、苏仕华副教授、鲍运律教授、刘大路博士、唐军高级工
程师等。
在编写过程中,我们得到了中国科学院院士、中国科学
技术大学陈国良教授的大力支持,特此表示感谢!对书中所
引用资料的作者及网络作品的作者表示衷心感谢!
作者
zaliu@ustc.edu.cn
2016年6月
[1] 此书已于2014年由机械工业出版社出版,书号
978-7-111-47985-7。第1章 引入指针变量
指针在C语言中具有举足轻重的地位,也是编制C程序的
基本功之一。本章将通过例子说明引入指针变量的必要性并
简单介绍指针变量的基本性质。1.1 变量的三要素
一个变量具有3个要素:数据类型、名字和存放变量的
内存地址。本节将简要回顾变量的3个要素,以便为引入指
针打下基础。
1.基本数据类型
数据类型是C语言中非常重要的一个概念,它将C语言所
处理的对象按其性质不同分为不同的子集,以便对不同类型
的数据规定不同的运算。void是无类型标识符,只能声明函
数的返回类型,不能声明变量,但可以声明指针。
本节只涉及基本数据类型,C语言的基本数据类型有如
下4种。
·char 字符型
·int 整数型·float 浮点数型(又称为单精度数)
·double 双精度浮点数型
另外还有用于整型的限定词short、long、signed和
unsigned。short和long表示不同长度的整型量;
unsigned表示无符号整型数(它的存放值总是正的);可
以省略signed限定词。例如,可以将如下声明
short int x;
unsigned int z;
中的说明符int省略。即它们与如下声明
short x;
unsigned z;
是等效的。上述数据类型的长度及存储的值域也随编译
器不同而变化,ANSI C标准只限定int和short至少要有16
位,而long至少32位,short不得长于int,int不得长于
long。表1-1是数据类型的长度及存储的值域表,表1-1中VC是Visual C++6.0的缩写。表1-2是加了限定词的数据类
型及它们的长度和取值范围。
C语言提供一个关键字sizeof,用来求出对于一个指定
数据类型,编译系统将为它在内存中分配的字节长度。例
如,语句“printf(%d,sizeof(double));”的输
出结果为8。
注意在表1-1中的标注,在VC中int使用4字节,这是本
章计算的依据。
C语言定义的存储类型有4种:auto、extern、static
和register,分别称为自动型、外部型、静态型和寄存器
型。自动型变量可以省略关键字auto。存储类型在类型之
前,即
存储类型 类型
例如auto int和static float等。可以省略auto,其他类型均不可以省略。
表1-1 数据类型的长度及存储的值域
表1-2 加限定词的数据类型及其长度和取值范围
2.变量的名字和变量声明
C语言中大小写字母是具有不同含义的,例如,name和
NAME就代表不同的标识符。原来的C语言中虽然规定标识符
的长度不限,但只有前8个字符有效,所以对定义为
dwNumberRadio
dwNumberTV的两个变量是无法区别的。
现在流行的为32位操作系统配备的C编译器已经能识别
长文件名,不再受8位的限制。另外,在选取时不仅要保证
正确性,还要考虑容易区分,不易混淆。例如,数字1和字
母i在一起,就不易辨认。在取名时,还应该使名字有很清
楚的含义,例如使用area作为求面积函数的名字,area的
英文含义就是“面积”,这就很容易从名字猜出函数的功
能。对一个可读性好的程序,必须选择恰当的标识符,取名
应统一规范,以便使读者能一目了然。
在现在的编译系统中,内部名字中至少前31个字符是有
效的,所以应该采用直观的名字。一般可以遵循如下简单规
律。
1)使用能代表数据类型的前缀。
2)名称尽量接近变量的作用。3)如果名称由多个英文单词组成,每个单词的第一个
字母大写。
4)由于库函数通常使用下划线开头的名字,因此不要
将这类名字用作变量名。
5)局部变量使用比较短的名字,尤其是循环控制变量
(又称循环位标)的名字。
6)外部变量使用比较长且贴近所代表变量的含义。
7)函数名字使用动词,如Get_char(void)。变量使
用名词,如iMen_Number。
变量命名可以参考Windows API编程推荐的匈牙利命名
法。它是通过在数据和函数名中加入额外的信息,既增进程
序员对程序的理解,又方便查错。
所有的变量在使用之前必须声明,所谓声明即指出该变
量的数据类型及长度等信息。声明由类型和具有该类型的变量列表组成。如:
int lower,upper;
char c,name[15];
变量可按任何方式分布在若干个声明中,上述声明同样
可以写成:
int lower; 整数类型
int upper; 整数类型
char c; 字符类型
char name[15]; 字符数组,可连续存放15个字符
后一种形式会使源程序冗长,但便于给每个声明加注
释,也便于修改。
变量的存储类型在变量声明中指定。变量声明的一般形
式为:
存储类型 类型 变量名列表;
应该养成在声明时就为变量赋初值的习惯,但在某些特殊场合则只能声明,如头文件中对外部变量的声明,下面是
一些典型的例子。
auto int a;
static float b, c;
extern double x;
register int i=0;
extern char szClassame[ ];
static int size=50;
const double PI=3.14159;
3.变量的地址
内存地址由系统分配,不同机器为变量分配的地址大小
虽然可以不一样,但都必须给它分配地址。
在C语言中,声明和定义两个概念是有区别的。声明是
对一个变量的性质(如构成它的数据类型)加以说明,并不
为其分配存储空间;而定义则是既说明一个变量的性质,又
为其分配存储空间。定义一个函数,也是为它提供代码。1.2 变量的操作
从三要素可知,既可以通过名字对变量进行操作,也可
以通过地址对存放在该地址的变量进行操作。
1.左值和右值的概念
变量是一个指名的存储区域,左值是指向某个变量的表
达式。“左值”来源于赋值表达式“A=B”,其中左运算分
量“A”必须能被计算和修改。左值表达式在赋值语句中既
可以作为左操作数,也可以作为右操作数,例
如“x=56”和“y=x”,x既可以作为左值(x=56),又可
以作为右值(y=x)。但右值“56”只能作为右操作数,而
不能作为左操作数。由此可见,常量只能作为右值,而普通
变量既可以作为左值,也可以作为右值。如下语句
const int a = 256;定义的a,显然不能作为左值,只能作为右值。
由此可见,值可以作为右值,如整数、浮点数、字符
串、数组的一个元素等。在C语言中,右值以单一值的形式
出现。假设有字符数组a和b,则这两个字符数组的每个元素
均可以作为右值,即“a[0]=b[0]”是正确
的,“b[0]=a[0]”也是正确的。需要注意的是,它们
在“=”号左右两边的含义是不同的。以a[0]为例,在“b[0]=a[0]”中,它是作为值出现的,即a[0]是数组第
1个元素的值;而在“a[0]=b[0]”中,它是作为变量出现
的,即a[0]是数组的第1个元素的变量名,所以a[0]可以作
为左值。即可以使用数组的具体元素作为左值和右值。
a和b都不是字符串的单个元素,所以都不能作为右值。
而因为a和b可以作为数组首地址的值赋给指针变量,所以在
这种情况下它们又都可以作为右值。
由此可见,在C语言中,左值是一个具体的变量,右值一定是一个具体类型的值,所以有些可以既可以作为左值,也可以作为右值,但有些只能作为右值。
2.对变量的基本操作
C语言使用地址运算符“”来取变量存储在内存中的首
地址。假设变量a=55,但不同机器和系统为它分配的地址是
不一样的,这里也假设分配的十六进制地址是
0x0012FF7C。如何从这个地址取出“55”呢?
C语言提供了“”运算符,用来取出地址里的
值。“a”代表地址,显然“a”可以取出55。使用下面
语句
printf(%d,%d\n,a,a,);
可以得到输出结果为“55,55”,即证明a和a是等
价的。1.3 指针变量
1.2节介绍了“”和“”运算符,本节将通过具体的
例子说明它们的用途,从而引入指针变量。
1.对有效地址进行操作
【例1.1】取地址里的值和取地址里存放的地址值的例
子。
include
int main
{
int a = 65;
int addr;
addr = 0x0012ff7c; 将a的首地址存入变量addr
printf(0x%p, 0x%p, 0x%p\n, a, addr, addr); 输出3个地址
printf(%d,%d,0x%p\n, a, a, addr); 输出变量及地址里的值
return 0;
}
语句“int a=65;”定义了整型变量a的值为65,VC使
用4字节存储65。假设存放它的内存首地址为十六进制的“0x0012ff7c”,则可以使用输出格式“%p”来输出这
个地址。“0x”是标注它为十六进制地址,也可以简单地使
用“%p”输出地址。
一个变量具有地址和值,是取地址值运算符。系统为
整型变量a和adder分别分配地
址“0x0012ff7c”和“0x0012ff78”。给整型变量addr赋
十六进制数,这个数可以代表地址,但不一定是有效的地址
(将计算机可以存取的地址称为有效地址)。已经验证
0x0012ff7c是分配给变量a的地址,所以addr是被赋给一个
有效地址。
将“0x0012ff7c”赋给变量addr,addr是系统分给
它的地址“0x0012ff78”,这个地址与a的地址相差4字
节,证明它们是连续存放的。现在这个地址里存放的是地址
0x0012ff7c,也就是变量a的地址。因为addr应该输出a
的地址而不是a的值,所以要使用%p格式。程序输出结果也
验证了如上分析。即输出为0x0012FF7C,0x0012FF7C,0x0012FF78
65,65,0x0012FF7C
既然addr存放的是有效地址,addr也应该能输出这个
地址里的值,也就是变量a的地址。不过,a虽然和addr的
值是一个值,但它们对运算的反应并不一样。a是变量,其
值为65,a是存储地址,所以a是取地址里的值。类似
的,addr应该输出它的存储内容,即地
址“0x0012ff7c”,而addr应该输出地
址“0x0012ff7c”里的内容65。其实这是不行的,因为编
译系统并不知道addr存储的“0x0012ff7c”是地址,所以
将它作为整数,因此编译系统会报错,当然使用addr也
要出错。
但addr里面确实装的是地址,所以可以将这个整数强制
转为地址。addr加上强制转换,应该是“(int)
addr”,它的内容是变量a的地址“0x0012ff7c”。
再对它使用运算符,即(int)addr,输出结果应该是存在这个地址里的变量a的值65。下面的例子验证了如
上分析。
【例1.2】取地址里的整数值和取地址里存放的地址
值。
include
int main
{
int a=65;
int addr;
addr=0x0012ff7c;
printf(0x%p, 0x%p, 0x%p\n, a, addr, (int)addr);
printf(%d,%d,%d\n,a,a, (int)addr);
return 0;
}
输出结果如下:
0x0012FF7C, 0x0012FF7C, 0x0012FF7C
65,65,65
2.引入指针的概念
在【例1.2】中,要使用a的地址直接给addr赋值,必须事先知道这个地址。为了避免这个麻烦,可以直接将地址
表达式“a”赋给变量。即
addr=a;
因为a是地址值,addr是整型变量,所以会给出警告
信息。不过,可以使用强制转换让警告信息“闭嘴”。即
addr=(int)a;
这样一来,使用起来就方便多了。
【例1.3】直接将变量地址赋给另一个变量的例子。
include
int main
{
int a=65;
int addr;
addr=(int)a;
printf(0x%p, 0x%p, 0x%p\n, a, addr, (int)addr);
printf(%d,%d,%d,\n,a,a, (int)addr);
return 0;
}程序运行结果如下:
0x0012FF7C, 0x0012FF7C, 0x0012FF7C
65,65,65,运行结果完全吻合。如果想像对待变量一样对待addr,即使用“”和“”运算符的结果与变量a一样,就必须定
义新的变量类型。分析下述表达式:
addr=(int)a;
(int)addr
(int)addr
由此可见,如果定义一种变量,使它存储的数据类型是
地址,问题就可以迎刃而解了。
要使用(int)addr的addr存储地址,那么就要
用“int”声明“addr”,即
int addr;
这时“addr=a”就无需转换,赋值顺理成章了。“addr”输出地址,则“addr”输出地址里的值。这
就与普通变量的使用方法完全一样了。
暂且将使用“int”定义的变量称为指针变量,下面编
程验证一下这个设想。
【例1.4】使用新的数据类型(指针)的例子。
include
int main
{
int a=65;
int p;
p=a;
printf(0x%p, 0x%p, 0x%p\n, a, p, p);
printf(%d,%d,%d\n,a,a, p);
return 0;
}
输出结果如下:
0x0012FF7C, 0x0012FF78, 0x0012FF7C
65,65,65,输出结果与【例1.3】的完全一样。这种类型称为指针类型,指针类型存储的是地址值。因
为这里使用的地址值是另外一个变量的地址,所以是有效地
址。要明确的是,地址值不一定是有效地址,所以说从指针
的引入开始,也就暗示着它存在着无法预防的错误。
3.引入字符指针再次验证
下面再使用字符来验证一下,看是否与整数的结论相
同。
【例1.5】使用字符的例子。
include
int main
{
char a='B';
int addr;
addr=(int)a;
printf(0x%p, 0x%p, 0x%p\n, a, addr, (char)addr);
printf(%c, %c, %c\n, a,a, (char)addr);
return 0;
}
程序输出结果如下:0x0012FF7C, 0x0012FF7C, 0x0012FF7C
B, B, B
程序验证了“(char)addr”和“(char)
addr”的作用,从而推知,可以定义字符类型的指针。
【例1.6】使用字符指针的例子。
include
int main
{
char c='B';
char p;
p=c;
printf(0x%p, 0x%p, 0x%p\n, c, p, p);
printf(%c, %c, %c\n, c, c, p);
return 0;
}
程序输出结果如下:
0x0012FF7C, 0x0012FF78, 0x0012FF7C
B, B, B
4.声明指针类型的变量
由此可见,声明指针变量是用普通数据类型加“”号。例如:
int p; 整型类型指针
char pc; 字符类型指针
float pf; 浮点类型指针
double pd; 双精度类型指针
struct ps; 结构类型指针
至于“”号的位置,对于整型类型指针p,以下三个位
置均可。
int p; 紧挨着t
int p; 在t与p之间
int p; 紧挨着p
至于哪种写法好,也要根据实际情况,以不造成误会为
准。下面是正确的使用实例。
int a, p, d;
d = 45;
a=d;
p=d;
在上面的声明中,只有a是指针变量,p和d都是整型变
量。使用下面的声明就可以提高可读性。int p, d, a;
系统不管是何种数据类型的指针,一律分配4字节,即
各种类型的指针所占内存的大小是一样的。
【例1.7】演示典型指针长度的例子。
include
struct st{
int a,b;
double f;
} s, ps;
int main
{
double a = 6.8; double pd = a;
float b = 6.5; float pf = b;
char c = 'G'; char pc = c;
int p=NULL;
printf(%d %d %d %d %d\n,sizeof(pd), sizeof(pf), sizeof(pc), sizeof(p), sizeof(ps));
return 0;
}
NULL代表空指针。程序输出结果为:
4 4 4 4 4下面再以整型指针p为例,说明指针的含义。
【例1.8】演示整型指针的例子。
include
int main( )
{
int a=65, p;
p=a;
printf(a的值等于%d, a的首地址是%p, p指向的地址是%p。\n, a, a, p);
printf(通过名字使用%d, 通过p内的地址%p使用%d。\n, a, p, p);
printf(p指向的地址为%p, 存放p的地址是%p。\n, p, p);
return 0;
}
程序输出结果如下:
a的值等于65, a的首地址是0x0012FF7C, p指向的地址是0x0012FF7C。通过名字使用65, 通过p内的地址0x0012FF7C
p指向的地址为0x0012FF7C, 存放p的地址是0x0012FF78。
可以画出变量p和a之间的关系如图1-1所示。图1-1 变量p和a之间的关系示意图
系统为指针变量分配地址0x0012FF78,一般不需要管
它。它存储的地址是变量a的首地址,正是因为这种数据类
型声明的变量代表指向另一个数据类型变量的存储首地址,所以得名为“指针”类型。注意这句话:指向另一个数据类
型变量的存储首地址。
读者有时会对使用如下方法在声明指针的同时初始化指
针的方式感到困惑,即
int p=a;
实际上,选择“intp;”,认为“int”是一种指向
整型的指针类型,用它声明指针变量p,p应该赋予a的地
址,所以应是“p=a”。声明指向整型的指针变量p并同时
初始化,也就顺理成章为“intp=a”。显然,称p为指针
变量(存放的是变量的首地址),而不是称p为指针变量
(p代表指针指向的地址单元所存放的值)。由此可知,p的值是地址,虽然这个地址就是变量a在内
存中的存储首地址,但并不直接说p的值是a的地址,而说成
p指向a的存储首地址,简称p指向a的地址。1.4 指针类型
假设已经知道变量的地址(NULL也算已知),现在将上
一节的构造语法总结如下:
存储类型 数据类型 指针名;指针名=变量地址;
或者采取直接初始化的方法:
存储类型 数据类型 指针名=变量地址;
默认的存储类型为自动存储类型(auto),目前也仅以
自动存储类型为例,以后将通过例子进一步介绍存储类型。
现在假设它们具有如图1-2所示的形式。由关联关系可知,p和a同步变化,即改变任何一个的值,它们的值保持一
致。如果改变p的内容,如使用语句“p=b;”,这使得
p=66,它与b同步,不再与a有任何关系。图1-2 指针操作关系图
【例1.9】说明对p和p进行赋值操作含义的程序。
include
int main ( )
{
int a = 55, b = 66, p = a;
printf ( a:%p,b:%p,p:%p\n, a, b, p );
printf ( %d %d a:%p p:%p\n, a, b, p, p );
a=88;
p=a;
printf ( %d %d a:%p p:%p\n, a, p, p, p );
p=b;
printf ( %d %d b:%p p:%p\n, a, p, p, p );
return 0;
}
程序输出如下:
a:0x0012FF7C,b:0x0012FF78,p:0x0012FF74
55 66 a:0x0012FF7C p:0x0012FF74
88 88 a:0x0012FF7C p:0x0012FF7488 66 b:0x0012FF78 p:0x0012FF74
在【例1.9】中,指针本身的地址不会变化,它反映了
系统需要为指针p分配地址这一概念。正如使用a不要再考虑
a一样,以后也不再考虑p。
【例1.10】下面是一个使用数组b的首地址作为右值的
例子,该程序将数组a的内容复制到数组b中,然后输出两个
数组的内容以便验证。
将数组b的首地d址作为右值的例子
include
void main ( )d
{
char a[]=We are here! Where are you?, b[28], p;
int i=0;
p=b; 数组b的首地址作为右值赋给左值p
while(p[i]=a[i]) 数组a的每个元素值作为右值
i++;
printf(a); printf(\n);
printf(b); printf(\n);
}
程序运行结果如下:
We are here! Where are you?
We are here! Where are you?第2章 指针基础知识
本章将通过实例,解释指针的基本性质以便为用好指针
打下基础。2.1 指针运算符
指针有两种专门的运算符:“”和“”。它们都仅需
要一个操作数,但作用不同。假定已经初始化整型变量a、b
和整型指针变量p。另外,它有一个从“().”简化而来
的“->”运算符,用于存取结构等类型的成员,还可以使用
下标“[]”进行指针操作。
1.&运算符
如前所述,“”仅仅返回这个操作数的地址。而语句
p = &a;
只表示把变量a的地址赋给p,它不改变a的值。假设变
量a的值为56,它存放在内存中的首地址是0x0012FF7C,执
行上述语句后,p的值为0x0012FF7C。
2.运算符“”与“”相反,是返回在这个地址中存储的变量的
值。例如,若p存储了变量a的内存地址,则
b = p;
表示将a的值赋给b,b的值是56,运算符“”理解
为:b接收了在地址p中的值。
可以通过指针间接地存取目标。如上所述,单目运算
符“”将它的操作数作为最终目标的地址来处理,存取的
变量是该地址里的内容。
由此可见,如果b为一个整型变量,在执行语句
p = &a;
之后,下面两条语句
b = p;
b = a;的功能是等价的,都是将p所指向的单元的内容赋给b,第一条语句实际上是对p的间接存取。
3.->运算符
为了书写使用方便,又从“().”运算符演化出“-
>”运算符。
【例2.1】使用结构指针的程序。
struct pencil {
int hardness;
char marker;
int number;
};
include
void main( )
{
struct pencil p[3]; 第9行定义p[3]
struct pencil pen; 第10行定义pen
p[0].hardness=2; p[0].marker='F'; p[0].number=485;
p[1].hardness=0; p[1].marker='G'; p[1].number=38;
p[2].hardness=3; p[2].marker='E'; p[2].number=108;
printf ( Hardness Marker Number\n );
for ( pen=p; pen<=p+2; ++pen )
printf(%4d%8c%8d\n,(pen).hardness, (pen).marker, (pen).number);
}程序输出结果如下:
Hardness Marker Number
2 F 485
0 G 38
3 E 108
已经知道一个结构,例如结构数组pen[0],它的成员
hardness的值可以通过
pen[0].hardness
取得。又知道,指针变量的值是它所指向的数据的地
址,故用结构指针pen来求结构成员,例如hardness的值,可通过语句
(pen).hardness
来取得。这里的圆括号不能省略。因为“.”的运算优
先级高于“”;而在这里首先要求出结构指针所指向的结
构,然后再求这个结构的成员,故必须加圆括号。表达式(pen).hardness
写起来很费事,可以将它表示成如下语句
pen -> hardness
其中->是由负号“-”和大于号“>”组成的。这种表示
方法显得相对简单些。
这样,求结构成员值的一般形式就是:
指向结构变量指针的名字 -> 成员名字
例如在上面程序的for循环中的打印语句参数表
(pen).hardness, (pen).marker, (pen).number);
就可以简化为如下形式:
pen -> hardness, pen -> marker, pen -> number);注意
对结构变量本身进行操作时,必须用“.”运算符。但
若使用结构指针,必须使用箭头运算符。
4.下标运算符
【例2.2】对p使用下标进行操作的程序。
include
int main ( )
{
int a = 36, b = 63, c = 656, i = 0, p = a;
for( i=0; i>-3; i--)
printf ( %4d, p[i]);
printf ( \n );
p=c;
for( i=0; i<3; i++)
printf ( %4d, p[i]);
printf ( \n );
return 0;
}
程序输出结果如下:
36 63 656
656 63 36使用下标时,p是从0开始计数的,即p[0]为起点。从
p[0]可以正数(下标正序增加),也可以反数(下标按负数
递减),即以p[0]为分界点往正负两个方向计数,下面给出
一个例子。
【例2.3】对p使用正负下标进行操作的程序。
include
int main ( )
{
int a = 36, b = 63, c = 656, i=0;
int p = b;
printf ( %d %d %d\n, p[1], p[0], p[-1]);
for( i=1; i>-2; i--)
printf ( %d , p[i]);
printf ( \n );
return 0;
}
程序输出结果如下:
36 63 656
36 63 6562.2 指针移动
指针移动,就是对指针采取++和--操作。VC为整数分
配4字节,所以p+1就是向高地址移动一个整数的长度,即4
字节。字符只分配1字节,p+1就移动1字节。反之,--就代
表地址减少。
1.顺序移动整数类型的指针
下面举例说明指针移动会带来的一些问题。
【例2.4】顺序移动指针的例子。
include
int main
{
int a=88, b=58, c=98, i=0;
int p;
p=a;
printf(0x%p, 0x%p, 0x%p, 0x%p, 0x%p\n,a, b, c, i, p);
for( i=0; i<4; i++, p--)
printf(%d , p);
printf(%p \n, p);
a=0x0012FF6C;
printf(%p\n,(int)a); a=66;
for(p++,i=0; i<4; i++, p++)
printf(%d , p);
printf(%d , p);
printf(%p , p);
c=0x0012FF80;
printf(%d\n,(int)c);
return 0;
}
程序运行输出如下:
0x0012FF7C, 0x0012FF78, 0x0012FF74, 0x0012FF70, 0x0012FF6C
88 58 98 3 0012FF6C
0012FF6C
0 98 58 66 1245120 0012FF80 1245120
变量分配地址以a、b、c、i、p顺序降序分配:
0x0012FF7C~0x0012FF6C。使用p--方式,以降序
0x0012FF7C~0x0012FF74顺序输出“885898”。下一个
0x0012FF70是i的地址,这时的i等于3,所以输出3。再往
下是0x0012FF6C,这时p也指向这个地址,因此p就是
0x0012FF6C。
可以验证一下,将这个地址赋给变量a,再取这个地址的内容,验证结果正确。
现在从i开始往a移动(升序地址),a已经改为66,所
以依次输出
0 98 58 66
这时p出界了,输出是随机数据,这里
是“1245120”。可以将a的地址加4赋给c来验证,这个地
址内的内容也是“1245120”。
由此可见,用一个指针可以到处“跑”,如果用法不正
确,后患无穷。
【例2.5】用顺序移动指针说明危险性的例子。
include
int main
{
int a=55, b=58, c=98, i=0;
int p;
p=(int )0x0012FF78;
printf(0x%p, 0x%p, 0x%p, 0x%p, 0x%p\n,a, b, c, i, p);
printf(%d, %d, %d, %d, %d\n, a, b, c, i, p);
p=(int )0x0012FF76;
printf(0x%p, 0x%p, 0x%p, 0x%p, 0x%p\n,a, b, c, i, p);
printf(%d, %d, %d, %d, %d\n,a, b, c, i, p);
p=12345;
printf(%d, %d, %d, %d, %d\n,a, b, c, i, p);
return 0;
}
程序输出结果如下:
0x0012FF7C, 0x0012FF78, 0x0012FF74, 0x0012FF70, 0x0012FF78
88, 58, 98, 0, 58
0x0012FF7C, 0x0012FF78, 0x0012FF74, 0x0012FF70, 0x0012FF76
88, 58, 98, 0, 3801088
88, 0, 809042018, 0, 13579
使用语句
p=(int )0x0012FF76;
虽然可行,但取出的内容是原来存储的数据,这里已经
不是按分配的地址读取,所以是无意义的数
据“3801088”。不过此时的变量b和c的内容还没有被破坏。
使用语句“p=13579;”则完全破坏了变量b和c的内
容。最后一行的输出验证了这个问题。b变成0,而c变
成“809042018”,这条语句完全破坏了原来的存储内容。
【例2.6】演示使用字符指针存取整数的例子。
include
int main
{
int a=0x12345678, i=0;
char p;
p=(char )0x0012FF7C;
printf(%p, %X\n, a, p);
for( i=0; i<4; i++, p++)
printf(%p, %x\n, p, p);
return 0;
}
程序输出结果如下:
0x0012FF7C, 0x12FF7C
0x0012FF7C, 0x78
0x0012FF7D, 0x56
0x0012FF7E, 0x34
0x0012FF7F, 0x12系统为整型数据分配4字节、字符1字节,用字符指针逐
个读取1字节的内容,可以发现,0x0012FF7C~0x00012FF7F存取0x78~0x12,即指针指向存
储整数的低字节位置,具有连续的4字节。
所以说指针变量存储的是指向变量a的地址值,而不说
是存储变量a的地址。
2.指针基本运算
本节所说的C语言的地址能进行某种运算,即指针可以
与整数相加减。若p为指针,n为整数,则可以使用p+n或p-
n。但这里必须弄清楚,编译程序在具体实现时并不是直接
将n的值加到p上,而是要将n乘上一个“比例因子”,然后
再加上p。这是因为不同类型的数据实际存储所占的单元数
不同,如char类型为1字节,int类型为2字节(VC为4字
节),long和float类型为4字节,double类型为8字节
等,这些数分别为它们的“比例因子”,具体采用哪个作为比例因子,取决于p指向的数据是什么类型。对用户来说,不需要了解编译程序内部的实现,只要将p+n看成将指针p移
动n个数(不必涉及每个数所占的字节数)的位置。下面通
过实例来说明需要注意的几个问题。
【例2.7】演示指针及其运算概念的例子。
include
void main( )
{
int x=56, y=65,p=x; 指针指向x
printf(%u,%u,%u,%u\n,x,y,p,p);
printf(%d,%u,%u,%d\n,x,p,p,p);
p=y; 指针改为指向存放y的首地址
printf(%d,%u,%u,%d\n,y,p,p,p);
p=66; 通过指针改变变量内容
printf(%d,%u,%u,%d\n,y,p,p,p);--p; 对指针进行减1运算,使其指向指针变量的首地址
printf(%d,%u,%u,%d\n,x,p,p,p);
++p; 对指针进行增1运算,使其指向存放y的首地址
printf(%d,%u,%u,%d\n,x,p,p,p);
++p; 对指针进行增1运算,使其指向存放x的首地址
printf(%d,%u,%u,%d\n,x,p,p,p);
++p; 对指针进行增1运算,使其指向程序之外的地址
printf(%d,%u,%u,%d\n,x,p,p,p);--p; 对指针进行减1运算,使其指向存放x的首地址
printf(%d,%u,%u,%d\n,x,p,p,(p-1));
p=(p-1); 通过指针将变量y的值赋给x
printf(%d,%u,%u,%d\n,x,p,p,p);
}上面程序在VC中实现,为了更容易理解,表2-1给出VC
在执行第1条语句之后,为各个变量分配的内存首地址。
为了更容易理解,在程序输出的右方给出输出前的操作
过程及输出序号。
程序输出结果如下:
0x0012FF7C,0x0012FF78,0x0012FF7C,0x0012FF74 (1)
56, 0x0012FF7C,0x0012FF74,56 (2) p=x;
65, 0x0012FF78,0x0012FF74,65 (3) p=y;
66, 0x0012FF78,0x0012FF74,66 (4) p=66;
56, 0x0012FF74,0x0012FF74, 0x0012FF74 (5) --p;
56, 0x0012FF78,0x0012FF74,66 (6) ++p;
56, 0x0012FF7C,0x0012FF74,56 (7) ++p;
56, 0x0012FF80,0x0012FF74, 0x0012FF80 (8) ++p;
56, 0x0012FF7C,0x0012FF74,66 (9) --p;
66, 0x0012FF7C,0x0012FF74,66 (10) p=(p-1);
表2-1 内存分配一览表使用中需要注意如下问题:
1)系统根据变量x和y及指针的声明顺序,为它们分配
一段连续的地址。内存首地址的关系及其所代表的含义如表
2-1所示,第3列0x0012FF80是紧挨x上方的地址(x占4字
节),内容为随机数;其他3个地址分别是第1行变量的存储
首地址。第2列的值分别是第1列变量的值,其中
0x0012FF7C为存储变量x的首地址,即p的值。
2)将p改为指向y,这就改变了p和p的值,p指向y的
地址而p=y。当然p是不会改变的,如第3行输出所示。
3)使用“p=66;”语句也同步改变了y的值,但p的
指向不变,见第4行输出。
4)对p进行--p操作时,因为本程序的y和p顺次存放,所以就使得p指向自己,即第5条输出语句中的p和p均与p
一样,都是输出0x0012FF74。5)当对p进行++p操作时,使指针从指向p变为指向y的
内存存放首地址,p也随之变化并为y的值,操作产生的影
响见第6行的输出。
6)再次对p进行++p操作时,使指针从指向y变为指向x
的内存存放首地址,p也随之变化并为x的值,操作产生的
影响见第7行的输出。
7)如果这时继续执行++p操作,指针指向非程序区的地
址0x0012FF80,其中的内容为随机数。第8行的输出证实了
这一点。此时p所指向的地址虽然唯一,但已经不是所需内
容。如果这个内容很重要,又不慎将它修改,就会造成灾难
性后果。这就是使用指针的危险之处。
8)执行--p使p退回安全区并指向x的地址,这时第9行
的输出就与第2行的一样。
9)对p进行运算,相应的p为p指向地址的内容。也可
以不改变p,将相对p的地址的内容取出,这就是使用(p±n)。程序演示了使用(p-1)输出66,但这并没有改变
p的内容,这可从第10行输出p的结果得到证实。最后一
句使用语句“p=(p-1);”将p指向的内容改变为66,但p仍然指向x。因此要正确区别(p±n)和(p±n)操
作。
可使用下标“[]”描述连续的指针p,这里不再赘述。
3.指针永远指向一个地址
迄今为止都是将指针进行初始化了,在讨论中也是假定
已经将指针初始化了。下面看一个简单的例子。
【例2.8】指针没有赋初值的例子。
include
int main ( )
{
int p;
p=65;
printf ( %d\n, p );
return 0;
}编译给出信息“warning C4700:local
variable'p'used without having been
initialized”。
指针没有指向一个地址,即产生运行时错误。有人认为
改为“p=65;”是正确的,因为这时的输出结果为65。其
实不对,这时输出的是地址,“p=65;”是将一个十进制数
65作为地址,如果将打印语句改为
printf ( %p\n, p );
则输出0x00000041,这就是十六进制地址,41代表十
进制65。这种做法就是将一个无效地址赋给指针,将会产生
灾难性的后果。
由此可见,编译系统给出警告时,首先应该采取有效措
施来消除这个警告。
因为指针变量存放的是地址,所以必须有具体指向。最常见的错误是声明了指针,没有为指针赋值。没有赋值的指
针含有随机地址。可以将上面的赋值语句去掉,直接输出p
以验证这一点。因为指针的破坏性很大,所以尽可能在声明
时同时初始化指针,这种习惯能避免指针的遗漏赋值。由此
可见,不仅要为指针赋一个地址,而且这个地址应是有效地
址。
注意
指针应该指向一个有效的地址。2.3 指针地址的有效性
1.地址的有效性
计算机的内存地址是有一定构成规律的。能被CPU访问
的地址才是有效的地址,除此之外都是无效的地址。
假设有一个指针变量p。可以随便将一个地址赋给p,只
要转换匹配一下即可,p是“来者不拒”,并不“考虑”给
它赋的是什么值,更不“考虑”其后果。声明一个指针,必
须赋给它一个合理的地址值,请看下面的例子。
【例2.9】演示给指针赋有效和无效地址的例子。
include
int main
{
char p,a='A',b='B';
p=a;
printf(0x%p, %c\n, p,p);
p=(char )0x0012FF74;
printf(0x%p, %c\n, p,p);
p=(char )0x0012FF78;
printf(0x%p, %c\n, p,p); p=(char )0x0012FF7C;
printf(0x%p, %c\n, p,p);
p=(char )0x1234;
printf(0x%p\n, p);
printf(%c\n, p);
return 0;
}
编译正确,但运行时出现异常。下面是除最后一条输出
语句之外的输出结果。
0x0012FF78, A
0x0012FF74, B
0x0012FF78, A
0x0012FF7C, |
0x00001234
当指针被赋予字符A的地址时,指针地址不仅有效,且
p具有确定的字符A。当将p改赋地址0x0012FF74时,这个
地址恰恰是系统分给字符B的地址,这个地址不仅有效,且
p具有确定的字符B。有时地址有效,但内容不一定确定,如0x0012FF7C是有效地址,但程序没有使用这个地址,所
以决定不了它的内容,输出字符“|”是无法预知的。地址
0x1234虽然能被指针p接受,也能输出这个地址,但这个地址是无效的,所以执行语句
printf(%c\n, p);
时出错,产生运行时错误。也就是当赋一个无效的地址
给p时,就不能对p进行操作。
结论
使用指针必须对其初始化,并且给指针赋予有效的地
址。
2.指针本身的可变性
编译系统为变量分配的地址是不变的,为指针变量分配
的地址也是如此,但指针变量所存储的地址是可变的。
【例2.10】有如下程序:
include
int main
{
int a=15, b=38, c=35,i=0; 4 int p=a; 5
printf(0x%p,0x%p,0x%p\n, a,b,c); 6
printf(0x%p,0x%p,0x%p,%d\n, p,p, p, p); 7
for(i=0;i<3;i++,p--) 8
printf(%d , p); 9
printf(\n%d,0x%p,0x%p\n, p,p,p); 10
for(i=0,++p;i<3;i++,p++) 11
printf(%d , p); 12
printf(\n%d,0x%p,0x%p\n, p,p,p); 13--p; 14
for(i=0;i<3;i++) 15
printf(%d , (p-i)); 16
printf(\n%d,0x%p,0x%p\n, p,p,p); 17
for(i=0;i<3;i++) 18
printf(%d , (p-2+i)); 19
printf(\n%d,0x%p,0x%p\n, p,p,p); 20
return 0;
}
假设运行后,第6和第7两行给出如下输出信息。
0x0012FF7C,0x0012FF78,0x0012FF74
0x0012FF6C,0x0012FF7C,0x0012FF7C,15
请问能分析出程序后面的输出结果吗?
【解答】因为地址0x0012FF80里存储的值不是由程序
决定的,所以这个输出值不能确定。除此之外,其他的输出
值均可以根据这两行的输出结果,写出确定的输出结果。为了便于分析,首先要清楚所给上述两行输出结果的含
义。
1)从第一行的输出可知,依次是分配给变量a、b和c的
地址。
2)a的地址是0x0012FF7C。注意第2行的输出中,第2
个和第3个的值与它相等。
3)第2行第1个0x0012FF6C对应“p”,是编译系统为
指针分配的地址,用来存放指针p。因为已经给指针变量赋
值(p=a),所以“p”就是输出指针地址0x0012FF6C
里的内容0x0012FF7C。它就是p指向a的地址,即p也输出
0x0012FF7C。也就是说,p、p和a的值相同。
4)“p”就是输出指针p所指向地址0x0012FF7C里所
存储的变量a的值,即15。
要分析输出,需要掌握如下操作含义。1)编译系统为声明的变量a分配存储地址,运行时可以
改变a的数值,但不会改变存储a的地址,即a的地址值不
变。同理,为声明的指针变量p分配一个存储地址,p指向的
地址值可以变化,但p的地址不会变化。
2)可以对指针变量p做加、减操作。由第1行输出结果
知,p=p-1(可记作--p),则p指向的地址是
0x0012FF78,p输出38,再执行p--,则p输出35。如果
再执行p++,则p输出38。这时,对p操作后,不仅p指向的
地址有效,其地址中存储的内容也正确。
3)如果p的操作超出这三个变量的地址,就无法得出输
出结果。
按照上述提示,预测如下。
1)第8~10行中的for语句就是输出三个变量的值
(153835),输出之后,可以预测p指向地址为
0x0012FF70,但不能预测p的内容。在运行过程中p保持为0x0012FF6C。
2)第11~13行中的for语句是反向输出三个变量的值
(353815),输出之后,可以预测p指向地址为
0x0012FF80,但不能预测p的内容(假设它的值为
1245120),当然p仍为0x0012FF6C。
3)第14~17行中的for语句也是输出三个变量的值
(153835),第14行将p调整指向存储a的地址,循环语句
中使用“(p-i)”,因为只是使用p做基准,用i做偏移
量,所以p的值不变,输出之后,p不变,仍为
0x0012FF7C,p=15,p不变。
4)第18~20行中的for语句是反向输出三个变量的值
(353815),循环语句也使用p做基准,即“(p-
2+i)”。输出之后,p不变,仍为0x0012FF7C,p=15,p不变。
由此可见,要小心对p的操作,以免进入程序非使用区或无效地址。如果使用不当,严重时会使系统崩溃,这是使
用指针的难点之一。
程序实际运行结果如下。
0x0012FF7C,0x0012FF78,0x0012FF74
0x0012FF6C,0x0012FF7C,0x0012FF7C,15
15 38 35
3,0x0012FF70,0x0012FF6C
35 38 15
1245120,0x0012FF80,0x0012FF6C 1245120是不可预测的值
15 38 35
15,0x0012FF7C,0x0012FF6C
35 38 15
15,0x0012FF7C,0x0012FF6C
3.没有初始化指针和空指针
【例2.11】没有初始化指针与初始化为空指针的区别。
include
int main
{
int a=456;
int p;
printf(指针没有初始化:\n, p,p);
printf(0x%p,0x%p\n, p,p);
p=NULL;
printf(指针没有初始化为NULL:\n, p,p);
printf(0x%p,0x%p\n, p,p);}
运行结果如下。
指针没有初始化:
0xCCCCCCCC,0x0012FF78指针初始化为NULL:
0x00000000,0x0012FF78
显然,在这两种情况下,不管如何初始化指针,p分配
的地址是一样的,区别是指针变量存放的值。
指针在没有初始化之前,指针变量没有存储有效地址,如果对“p”进行操作就会产生运行时错误。当用NULL初
始化指针时,指针变量存储的内容是0号地址单元,这虽然
是有效的地址,但也不允许使用“p”,因为这是系统地
址,不允许应用程序访问。
为了用好指针,应养成在声明指针时就予以初始化。既
然初始化为NULL也会产生运行时错误,何必要选择这种初始
化方式呢?其实,这是为了为程序提供一种判断依据。例如
申请一块内存块,在使用之前要判断是否申请成功(申请成功才能使用)。
int p=NULL;
p=(int)malloc(100);
if(p==NULL);{
printf(“内存分配错误!\n”);
exit(1); 结束运行
}
注意正确地包含必要的头文件,下面给出一个完整的例
子。
【例2.12】判断空指针的完整例子。
include
include
void main( )
{
char p;
if( (p = (char )malloc(100) ) == NULL) {
printf(内存不够!\n);
exit(1);
}
gets(p);
printf(%s\n, p);
free(p);
}
其实,只要控制住指针的指向,在使用中就可避免出错。
将它与整型变量进行对比,就容易理解指针的使用。整
型变量存储整型类型数据的变量,也就是存储规定范围的整
数。指针变量存储指针,也就是存储表示地址的正整数。由
此可见,一个指针变量的值就是某个内存单元的地址,或称
为某个内存单元的指针。可以说,指针的概念就是地址。
由此可见,通过指针可以对目标对象进行存取(操作
符),故又称指针指向目标对象。指针可以指向各种基本数
据类型的变量,也可以指向各种复杂的导出数据类型的变
量,如指向数组元素等。2.4 指针的初始化
指针初始化,就是保证指针指向一个有效的地址。这有
两层含义,一是保证指针指向一个地址,二是指针指向的地
址是有效的。
1.数值表示地址
为了更深入地理解这一点,首先要记住指针是与地址相
关的。指针变量的取值是地址值,但如何用数值来代表地址
值呢?请看下面的例子。
【例2.13】演示表示地址值的例子。
include
int main
{
int a=256,b=585;
printf (%p,%p\n,a,b);
printf (%d,%d\n,a,b);
printf (%d,%d\n,(int )0x0012FF7C,(int )0x0012FF78);
return 0;
}程序输出结果如下。
0x0012FF7C,0x0012FF78
256,585
256,585
“a”表示变量a的地址,“a”表示存入地址里的
值,也就是变量a的值。既然a代表的是存储变量a的十六进
制地址0x0012FF7C,是否可以使用“0x0012FF7C”呢?
答案是否定的,编译器无法解释“0x0012FF7C”。要
想让“0x0012FF7C”表示地址,必须显式说明,即让它与
指针关联起来,可使用“int”将这个十六进制数字强制转
换成地址。这就是通常说的“地址就是指针”的含义。
由此可见,直接使用数值很麻烦。即使对于验证过的地
址,换一台机器可能就不行了,不具备可移植性,这种赋值
乃是不得已而为之。
2.赋予有效和无效地址【例2.13】不仅演示了如何使用地址值,还演示了有效
地址的概念。该例是在确定变量地址之后才使用它们,所以
是有效的。一般认为,所谓地址有效是指在计算机能够有效
存取的范围内。由此可见,这个有效并不能保证程序正确,指针超出本程序使用的地址范围可能带来不可估计的错误。
为了保证赋予的地址有效,避免像上面例子那样直接使
用强制转换,而是直接使用变量地址赋值。例如:
int a=256,b=585,p=b;
或者使用语句
int a=256,b=585,p;
p=b;
由此可见,对于一个指针,需要赋给它一个地址值。上
面的例子在赋给指针地址时,不是随意的,而是经过挑选
的。如果随便选一个地址,可能是计算机不能使用的地址,也就是无效的地址。【例2.9】演示了无效地址的例子。对于无效的地址,虽然编译没问题,但却产生运行时错误。由此可见,使用指
针的危险性就是赋予它一个无效地址,如果有效地避免了这
一点,就可以运用自如。
3.无效指针和NULL指针
编译器保证由0转换而来的指针不等于任何有效的指
针。常数0经常用符号NULL代替,即定义如下:
define NULL 0
当将0赋值给一个指针变量时,绝对不能使用该指针所
指向的内存中存储的内容。NULL指针并不指向任何对象,但
可以用于赋值或比较运算。除此之外,任何因其他目的而使
用NULL指针都是非法的。因为不同编译器对NULL指针的处
理方式不相同,所以要特别留神,以免造成不可收拾的后
果。如上所述,将指针初始化为NULL,就是用0号地址初始
化指针,而且这个地址不允许程序操作,但可以为编程提供
判别条件。尤其是在申请内存时,假如没有分配到合适的地
址,系统将返回NULL。
C编译程序都提供了内存分配函数,最主要的是malloc
和calloc,它们是标准C语言函数库的一部分,功能都是为
要写的数据在内存中分配一个安全区。一旦找到一个大小合
适的内存空间,就分配给它们,并将这部分内存的地址作为
一个指针返回。malloc和calloc的主要区别是:calloc清
除所分配的内存中的所有字节,即将所有字节置零;
malloc仅分配一块内存,但所有字节的内容仍然是被分配
时所含的随机值。
malloc和calloc所分配的内存空间都可以用free函数
释放。这3个函数的原型在文件stdlib.h中,但很多编译器
又放在头文件malloc.h中,注意查阅手册。在目前所提供的最新C编译程序中,malloc和calloc都
返回一个void型的指针,也就是说,返回的地址值可以假设
为任何合法的数据类型的指针。这个强制转换可以在声明中
进行,如将它们声明为字符型、整型、长整型、双精度或其
他任何类型。
【例2.14】找出程序中的错误并改正。
include
include
int main ( )
{
char p;
p = malloc(200);
gets(p);
printf(p);
free(p);
return 0;
}
malloc所返回的地址并未赋给指针p,而是赋给了指针
p所指的内存位置。这一位置在此情况下也是完全未知的。
下面语句
char p;p = ( char ) malloc(200);
只是将void指针强制转化为char类型的指针,所以它
与上面的等效,都是错误的。如果改为
char p;
p = malloc(200);
的方式,则是可以的,但它也有另外一个更为隐蔽的错
误。如果内存已经用完了,malloc将返回空(NULL)值,这在C语言中是一个无效的指针。正确的程序应该将对指针
的有效性检查加入其中,并及时释放不用的动态内存空间。
下面是一个正确而完整的实例。
include
include
int main ( )
{
char p;
p =(char ) malloc(200);
if(p==NULL) {
printf (内存分配错误!\n);
exit(1);
}
gets(p);
printf(p);
free(p); return 0;
}
在设计C程序时,对指针的初始化有两种方法:使用已
有变量的地址或者为它分配动态存储空间。好的设计方法是
尽可能地早点为指针赋值,以免遗忘造成使用未初始化的指
针。
对于上述程序,如果设置
p =NULL;
则程序执行if语句“{}”里的部分,输出“内存分配错
误!”,然后退出程序。在程序设计中,有时正好利
用“NULL”作为判别条件。下面就是一个典型的例子。
【例2.15】完善下面的程序。
include
char s1[16];
char mycopy(char dest,char src)
{
while(dest++=src++);
return dest;}
void main ( )
{
char s2[16]=how are you?;
mycopy(s1,s2);
printf(s1);
printf(\n);
}
这个程序编译没有错误,但不够完善。这主要是因为
mycopy函数中没有采取措施预防指针为NULL(又称0指针)
的情况。解决的方法很多,下面是简单处理的例子。
char mycopy(char dest,char src)
{
if(dest == NULL || src == NULL)
return dest;
while(dest++=src++);
return dest;
}
由此可见,使用已有变量的地址初始化指针能保证地址
总是有效的。如果使用分配动态存储空间的方法来初始化指
针,确保地址有效的方法是增加判断地址分配是否成功的程
序段。2.5 指针相等
假设有两个指针p1和p2,一定要理解语句
p1=p2;
P1=p1;
的含义。为了说明这个问题,先介绍大端存储和小端存
储的概念。
1.大端存储和小端存储
在CPU内部的地址总线和数据总线是与内存的地址总线
和数据总线连接在一起的。当一个数从内存中向CPU传送
时,有时是以字节为单位,有时又以字(4字节)为单位。
传过来是放在寄存器里(一般是32字节),在寄存器中,一
个字的表示是右边应该属于低位,左边属于高位,如果寄存
器的高位和内存中的高地址相对应,低位和内存的低地址相
对应,这就属于小端存储。反之则称为大端存储。大部分处理器都是小端存储的。
因为十六进制的2位正好是1字节,所以选十六进制
0x0A0B0C0D为例,如图2-1所示,对小端存储,低位是
0x0D,应存入低位地址,所以存入的顺序是
0x0D 0x0C 0x0B 0x0A
反之,对于大端存储则为
0x0A 0x0B 0x0C 0x0D图2-1 图解大端和小端存储
下面利用union的成员共有地址的性质,用一个程序来
具体说明小端存储。
【例2.16】演示小端存储的程序。
include
union s{
int a;
char s1[4];
}uc;
int main( )
{ int i=0;
uc.a=0x12345678;
printf(0x%x\n,uc);
for(i=0;i<4;i++)
printf(0x%x 0x%x\n,uc.s1[i],uc.s1[i]);
return 0;
}
声明十六进制整数a,它与字符串数组共有地址,a的最
低字节是0x0D,按小端存储,则应存入“uc.s1[0]”中,也就是0x4227A8中,最高位地址0x4227AB则应存入0x0A,也就是数据的高位。下面的运行结果证明了这一点。其实,可以在调试环境中直接看到这些结果。
0x4237A8
0x4237A8 0xD
0x4237A9 0xC
0x4237AA 0xB
0x4237AB 0xA
其实,【例2.6】的程序也说明了这个问题。
2.指针相等操作
两个指针变量相等,是指它们指向同一个地址。例如p1=p2;
不仅使得p1和p2都指向原来p1指向的地址,而且保证
p2=p1。注意它们的值是原来p1的值。也就是说,p2放
弃自己原来的指向地址及指向地址里存储的值。而语句
p1=p2;
的作用是使p1放弃自己原来的指向地址里存储的值,但
并没有放弃自己的指向地址。p1和p2仍然保留各自原来的指
向。至于p1里面的值,则要视具体情况而定,不能由此得
出p1指向地址里的值与p2相等。关于这一点,只要用两个
不同结果的例子就可以说明这一点。
【例2.17】演示整数指针相等操作的程序。
include
int main( )
{
int p1, p2;
int s1=0x12345678,s2=0x78;
p1=s1;p2=s2;
printf(0x%x\t0x%x\n,p1,p2);
p2=p1; printf(0x%x\t0x%x\n,p2,p1); 值相等
printf(0x%x\t0x%x\n,p1,p2); 地址不变
p2=p1;
printf(0x%x\t0x%x\n,p1,p2); 值相等
printf(0x%x\t0x%x\n,p1,p2); 地址改变
return 0;
}
这个例子的语句“p2=p1;”使用p1取代p2,但p2
不变。运行结果证明了这一点。
0x12ff74 0x12ff70
0x12345678 0x12345678
0x12ff74 0x12ff70
0x12345678 0x12345678
0x12ff74 0x12ff74
【例2.18】演示字符指针相等操作的程序。
include
int main( )
{
char p1, p2;
char s1[16]=987654321,s2[16]=G;
p1=s1;p2=s2;
printf(0x%x\t0x%x\n,p1,p2);
p2=p1;
printf(%s\t%s\n,p2,p1); 值不相等
printf(0x%x\t0x%x\n,p1,p2); 地址不变
p2=p1;
printf(%s\t%s\n,p2,p1); 值相等
printf(0x%x\t0x%x\n,p1,p2); 地址改变 return 0;
}
这个例子的语句“p2=p1;”并不使用p1取代p2,只是使用字符“1”取代原来的字符“G”,运行结果如下:
0x12ff68 0x12ff58
1 987654321
0x12ff68 0x12ff58
987654321 987654321
0x12ff68 0x12ff68
这是字符操作特征引起的,所以不能只看表面现象。以
后还会做进一步的分析。2.6 对指针使用const限定符
可以用const限定符强制改变访问权限。用const正确
地设计软件可以大大减少调试时间和不良的副作用,使程序
易于修改和调试。
1.指向常量的指针
如果想让指针指向常量,就要声明一个指向常量的指
针,声明的方式是在非常量指针声明前面使用const,例
如:
const int p; 声明指向常量的指针
因为目的是用它指向一个常量,而常量是不能修改的,即p是常量,不能将p作为左值进行操作,这其实是限定
了“p=”的操作,所以称为指向常量的指针。当然,这并
不影响p既可作为左值,也可作为右值,因此可以改变常量指针指向的常量。下面是在定义时即初始化的例子。
const int y=66; 常量y不能作为左值
const int p=y; 因为 y是常量,所以p不能作为左值
指向常量的指针p指向常量y,p和y都不能作为左值,但可以作为右值。
如果使用一个整型指针p1指向常量y,则编译系统就要
给出警告信息。这时可以使用强制类型转换。例如:
const int y=66; 常量y不能作为左值
int p1; p1既可以作为左值,也可以作为右值
p1=(int )y; 因为 y是常量,p1不是常量指针,所以要将y进行强制转换
如果在声明p1时用常量初始化指针,也要进行转换。例
如:
int p1=(int )y; 因为 y是常量,p1不是常量指针,所以要将y进行强制转换
在使用时,对于常量,要注意使用指向常量的指针。2.指向常量的指针指向非常量
因为指向常量的指针可以先声明,后初始化,所以也会
出现在使用时将它指向了非常量的情况。所以这里用一个例
子演示一下如果指向常量的指针p指向普通变量,则会出现
什么结果。
【例2.19】使用指向常量的指针和非常量指针的例子。
include
int main( )
{
int x=55; 变量x能作为左值和右值
const int y=88; 常量y不能作为左值,但可以作为右值
const int p; 声明指向常量的指针
int p1; 声明指针
p=y; 用常量初始化指向常量的指针,p不能作为左值
printf(%d ,p);
p=x; p作为左值,使常量指针改为指向变量x,p不能作为左值
printf(%d ,p);
x=128; 用x作为左值间接改变p的值,使p=x=128
printf(%d ,p);
p1=(int )y; 非常量指针指向常量需要强制转换
printf(%d\n,p1);
return 0;
}
运行结果如下。88 55 128 88
使用指向常量的指针指向变量时,虽然p不能作为左
值,但可以使用“x=”改变x的值,x改变则也改变了p的
值,也就相当于将p间接作为左值。所以说,“const”仅
是限制直接使用p作为左值,但可以间接使用p作为左值,而p仍然可以作为右值使用。
与使用非常量指针一样,也可以使用运算符“”改变
常量指针的指向,这当然也同时改变了p的值。
必须使用指向常量的指针指向常量,否则就要进行强制
转换。当然也要避免使用指向常量的指针指向非常量,以免
产生操作限制,除非是有意为之。
以上结论可以从程序的运行结果中得到验证。
3.常量指针
将const限定符放在号的右边,就使指针本身成为一个const指针。因为这个指针本身是常量,所以编译器要求
给它一个初始化值,即要求在声明的同时必须初始化指针,这个值在指针的整个生存期中都不会改变。编译器
将“p”看作常量地址,所以不能作为左值(即“p=”不成
立)。也就是说,不能改变p的指向,但p可以作为左值。
【例2.20】使用常量指针的例子。
include
int main( )
{
int x = 45, y = 55, p1; 变量x和y均能作为左值和右值
int const sum =100; 常量sum只能作为右值
int const p=x; 声明常量指针并使用变量初始化
int const p2 = (int )sum; 使用常量初始化常量指针,需要强制转换
printf(%d %d ,p,p2);
x = y; 通过左值x间接改变p的值,使p=55
printf(%d ,p);
p = sum; 直接用p作为左值,使p=100
printf(%d ,p);
p2 = p2 + sum + p; p2作为左值,使p2=300
printf(%d ,p2);
p1 = p; p作为右值,使指针p1与常量指针p的指向相同
printf(%d\n,p1);
return 0;
}
运行结果如下。45 100 55 100 300 100
语句“x=y;”和“p=sum;”都可以改变x的值,但p
指向的地址不能改变。
显然,常量指针是指这个指针p是常量,既然p是常量,当然p不能作为左值,所以定义时必须同时用变量对它进行
初始化。对常量而言,需使用指向常量的指针指向它,含义
是这个指针指向的是不能作为左值的常量。不要使用常量指
针指向常量,否则就需要进行强制转换。
4.指向常量的常量指针
也可以声明指针和指向的对象都不能改动的“指向常量
的常量指针”,这时也必须初始化指针。例如:
int x = 2;
const int const p= x;
告诉编译器,p和p都是常量,都不能作为左值。这种指针限制了“”和“”运算符,所以在实际应用中很少用
到这种特殊指针。
5.void指针
在一般情况下,指针的值只能赋给相同类型的指针。
void类型不能声明变量,但可以声明void类型的指针,而
且void指针可以指向任何类型的变量。
【例2.21】演示void指针的例子。
include
int main( )
{
int x=256, y=386, p=x;
void vp = x; void指针指向x
printf(%d,%d,%d\n,vp, p, x);
vp = y; void指针改为指向y
p = (int )vp; 强制将void指针赋值给整型指针
printf(%d,%d,%d\n,vp, p, p);
return 0;
}
虽然void指针指向整型变量对象x,但不能使用vp引
用整型对象的值。要引用这个值,必须强制将void指针赋值给与值相对应的整型指针类型。程序输出如下。
0x0012FF7C,0x0012FF7C,256
0x0012FF78,0x0012FF78,3862.7 使用动态内存
可以自己申请一块内存,且这块内存也由自己释放,这
样分配的内存称为动态内存。
如何使用这种动态内存,也就是如何定位这些动态内存
的地址呢?我们知道,与地址有关的变量是指针变量,所以
可以使用指针对动态内存进行操作。2.7.1 动态内存分配函数
如2.4节所述,C编译程序提供的最主要的内存分配函数
是malloc和calloc,能为要写的数据在内存中分
配一个安全区。前面讨论了这两个函数的区别,这里不再赘
述。
由这两个函数所分配的内存空间都可以用free函数
释放出来。这三个函数的原型在文件stdlib.h中,但很多
编译器又把它放在头文件malloc.h中,注意查阅手册。
如使用如下程序
char p;
p=( char ) malloc (50);
分配50字节的内存。因为指针p指向动态内存的首地
址,所以可以通过它存储变量的值。【例2.22】编写用指针申请一块内存来存储输入的一个
字符串,输出该字符串长度并释放该存储空间的程序。
include
include
void main ( )
{
char s;
int n;
s=(char ) malloc(50);
scanf ( %s,s );
for ( n=0; s!='\0'; ++s )
++n;
s=s-n;
free(s);
printf ( %d, n);
}
运行示范如下:
fedcba987654321
15
下划线标注的是由键盘输入的字符,以后均沿用这种方
式,不再说明。
有时不知道所用系统为int数据分配的字节数,则可以使用sizeof函数计算。下面是分配50个整型量的空间,并
使用sizeof函数来确定整型量所需的字节长度的例子。
int p;
p=( int ) malloc ( 50sizeof ( int ) );
字符串是作为一个整体存入动态内存的,如果申请一块
内存存放数值,因为申请的动态内存是连续的,所以只需要
移动指针,使它每次指向要存储的内存的首地址即可。
【例2.23】编写一个用指针申请一块内存来存储输入的
两个整数值,输出其值并释放该存储空间的程序。
include
include
void main ( )
{
int i, p;
p=(int ) malloc(8);
for ( i=0; i<2; ++i,++p )
scanf ( %d,p );
p=p-2;
for ( i=0; i<2; ++i,++p )
printf ( %d ,p );
printf (\n);
p=p-2;
free(p);
}运行示范如下:
128 256
128 256
循环语句使用“++p”,是为了让指针指向存储下一个
整数的首地址,而程序中的语句“p=p-2;”是使指针返回
开始的起点。
释放内存的语句“free(p);”中的参数p,必须是
申请到的动态内存的首地址。2.7.2 内存分配实例
下面列举几个例子说明如何为变量分配动态内存地址。
【例2.24】使用两个指针进行相减操作求串长度。
C语言允许两个指针进行相减的操作。如果p和q指向同
一数组的成员,p-q实际上就表示p和q之间的元素个数。例
如希望求得一字符串的长度,若得到字符串的首指针及字符
串尾(\0)的指针,那么这两个指针的差就是长度,其实现
如下:
include
include
void main ( )
{
char p, q;
p=q=(char)malloc(100);
scanf ( %s, q );
while ( p != '\0' )
p++;
printf ( %d\n, p-q );
free(q);
}运行示范如下:
nice!
5
但两个指针相加在C语言中是不允许的。从实际意义来
看,两个地址相加是没有意义的,所以C语言不允许p+q的运
算。同样,也不允许乘、除、移位或屏蔽运算,不允许
float或double数与指针量相加。
C语言中允许两个指针进行比较运算,对两个指向同一
数据类型的指针变量可以进行<、>=、==、!=等比较运
算。例如,表达式p
下面的【例2.25】就是演示它们的使用方法。由这几个
例题可见,要为使用的指针分配地址。在使用free时要注
意,指向同一个目标的指针只能释放一次。因为释放后,另
一个已经是空指针,再使用free就要出错。
【例2.25】编写一个程序读进10个float值,用指针将它们存放在一个程序块里,然后输出这些值的和及最小值。
具体要求如下:
1)申请数据内存块并判别是否申请成功。
2)用键盘输入10个数据的内容。
3)在for循环中,使用指针运算完成循环及计算。
【参考程序】编写一个程序读进10个float值,用指针
将它们存放在一个程序块里,然后输出这些值的和及最小
值。
include
include
include
const int SIZE=10;
void main( )
{
float pf, temppf, min, temp,sum=0.0;
if((min = temppf = pf = (float )malloc(SIZEsizeof(float))) == NULL)
printf(内存分配错误\n);
else {
for( ; temppf-pf< SIZE; temppf++){
scanf(%10f, temp);
temppf=temp;
}
for( temppf--; temppf >= pf; temppf--) { min = (min > temppf) ? temppf:min;
sum += temppf; }
}
printf(和是%f, 最小值是%f\n, sum, min);
free(pf);
}
运行示范如下:
-2.5 2.5 5.8 -5.8 -386.5 2.34 -5.34 3.6 -6.6 200和是-192.500000, 最小值是-386.500000
在使用动态内存分配时,还要判别是否分配成功。要保
证返回的值不为零。语句
if((min = temppf = pf = (float )malloc(SIZEsizeof(float))) == NULL);
就是判断内存分配是否成功。因为NULL的定义在
stdio.h中,故除了要包含文件stdlib.h之外,还要包含
stdio.h。
其实,分配的连续内存可以构成一个一维数组,详细的
用途将在后续章节中介绍。2.7.3 NULL指针
假设在内存分配语句
if ( (p=(char)malloc ( 100) == NULL );
中使用了空指针。空指针的表示为
p=NULL;
有时在赋值或比较运算的情况下会使用NULL指针,但在
其他情况下不能使用NULL指针。因为NULL指针并不指向任
何对象,而且空指针也不是空字符串,所以对空指针p而
言,使用如下两个语句会得到什么结果呢?
printf(%s\n, p);
printf(p);
为了代码的文档化,常采取如下定义:define NULL 0
由此可见,p的行为没有定义,这两条语句在不同的机
器上可能有不同的效果。
在禁止读取内存0地址的机器上,语句
printf(%d\n, p);
将会执行失败。在允许的机器上,则会以十进制方式输
出内存位置0中存放的字符内容。
要注意的是,空指针并不是空字符串。无论使用0还是
NULL,效果都是相同的。当将0赋值给一个指针变量时,绝
对不能企图使用该指针所指向的内存中存储的内容。
有些C语言实现对内存位置0只允许读,不允许写。在这
种情况下,NULL指针指向的也是垃圾信息,所以也不能错用
NULL指针。由此可见,对指针进行递增和递减操作必须预防越界。
在达到最后一个边界时,要特别小心谨慎。释放不用的内存
时,必须保证指针指向所申请内存的首地址,否则就会出
错。在某些场合,为了保证释放,甚至需要多申请部分内存
区域。第3章 一维数组
在C语言中,数组是一个非常重要的概念,一定要熟练
掌握数组的用法,尤其是深刻理解数组的边界不对称并避免
数组越界错误。3.1 一维数值数组
利用基本数据类型能构造出相应数据类型的数组,所以
说数组是一种构造型数据类型。本节重点以一维整数数组为
例,讨论它们的特征,然后再推广到其他一维数组。
1.一维数值数组的特征
假如要表示5个连续整数变量1~5,则需要5个整数变量
名称,如用X1~X5表示。这批变量的特点是它们的基本数据
类型一样。现在构造一个新的数据类型,假设它的名称为
A,在符号“[]”内用序号表示为A[0]~A[4],它们的对应
关系如图3-1所示。图3-1 数组构成示意图
后一种表示有很大的进步。X1~X5之间没有内在关系,而A[0]~A[4]之间是通过“[]”内的序号0~4(也就是下
标)构成了唯一的连续对应关系。如果将A[0]~A[4]看作连
续的房间,则可以改变房间里所存放整数的值。暂且将A称
作整数房间。也就是房间里必须都存放整型变量,这样构造
出来的数据类型称为整型数组。
假定使用语法定义如下:
int A[5]={1,2,3,4,5};这条语句的含义是整型数组A的下标从0开始,A[4]的
值为5,A[5]本身不是数组的元素。这个数字5代表数组A共
有5个元素A[0]~A[4]。将若干个同类型变量线性地组合起
来,就构成一维数组。在使用一维数组之前首先必须声明
它,这包括数组的类型、数组名和数组元素的个数。
声明一维数组的一般方式如下:
datatype array_name[n];
其中,datatype是数据类型,可以是基本数据类型,也可以是构造类型。array_name为数组名(标识符),n为
数组中所包含的数组元素的个数。数组与各种基本数据类型
的不同之处是:数组须由datatype、数组标志“[]”及长
度n三者综合描述,它是在基本数据类型基础上导出的一种
数据类型。例如:
int a[10]; 定义整型数组具有10个元素,每个元素是一个整数变量
float b[6]; 定义浮点数组具有6个元素,每个元素是一个实数变量
char c[25]; 定义字符数组具有25个元素,每个元素是一个字符变量数组中各元素总是从0开始连续编号,直到n-1为止。所
以上述定义的数组a、b、c的数组元素分别为
a[0]、a[1]、a[2]、…、a[9]
b[0]、b[1]、b[2]、b[3]、b[4] 、b[5]
c[0]、c[1]、c[2]、…、c[24]
对数组中的任何元素都可单独表示并对它进行访问。假
设a是整型数组,则语句
printf(%d,a[5]);
输出a[5]的值。C语言中规定只能对数组的元素操作,而不能对整个数组操作,即语句
printf(%d,a);
是不允许的,必须使用for语句依次遍历整个数组元
素。例如遍历数组a:
for(int i=0; i<10; i++)
printf(%d\n,a[i]);数组下标可以是变量,这个变量称为下标变量。下标变
量的值原则是从0开始到n-1的整数,但是在C语言中对数组
的下标变量的值是不进行合法性检查的,所以允许数组的下
标越界,对此程序员必须引起注意并避免产生错误。具有丰
富程序设计经验的程序员有时会巧妙地利用数组的下标越界
来进行程序设计。
【例3.1】给出求斐波那契数列中前20个元素值的问
题。所谓斐波那契数列,就是可以将它表示成F0、F1、F2、…,其中除F0和F1以外,其他元素的值都是它们前两个元素
值之和,即Fn=Fn-2+Fn-1,而F0、F1分别为0和1。为此,声
明整数数组fibonacci[20]来依次存放斐波那契数列的前20
个元素值。
include
void main( )
{
int n, fibonacci[20]={0,1}; 初始化
printf(%-5d%-5d,fibonacci[0],fibonacci[1]); 左端对齐
for (n=2; n<20; n++ )
{ 计算后18个元素值
fibonacci[n]=fibonacci[n-2]+fibonacci[n-1];
分4行打印,按每行5个数打印输出,左对齐 if(n%5==0) printf(\n);
printf(%-5d,fibonacci[n]);
}
printf(\n);
}
程序输出结果如下:
0 1 1 2 3
5 8 13 21 34
55 89 144 233 377
610 987 1597 2584 4181
实际上,斐波那契数列在数学和计算机算法研究领域有
许多用途。斐波那契数列起源于“兔子问题”:开始有一对
兔子,假设每对兔子每个月生下一对新的兔子,而每一对新
生下来的兔子在出生后的第2个月月底开始繁殖后代,而且
这些兔子永远不死,那么在1年之后一共会有多少对兔子?
这一问题的答案建立在这样一个事实上,即在第n个月结束
时,有总数为Fn+2的兔子。所以,根据程序输出结果,在第
12个月结束时将一共有377对兔子。
数组的元素可像一般变量那样进行赋值、比较和运算。如同简单变量一样,在说明数组之后就能对它的元素赋值,方法是从数组的第1个元素开始依次给出初始值表,表中各
值之间用逗号分开,并用一对花括号将它们括起来。例如:
int A[]= {1,2,3,4,5};
A数组元素的个数由编译程序根据初始值的个数来确
定。如果数组很大,又不需要对整个数组进行初始化,数组
初始值表可以只对前几个元素置初值,但这时必须指出数组
的长度。例如,在【例3.1】中存放斐波那契数列的数组中
前两个元素是已知的,而后面18个元素是要通过计算产生
的,所以对此可初始化为:
int fibonacci[20] = {0,1};
这样数组中的前两个元素分别为0和1,而后18个元素皆
为0。
如果没有明确进行初始化,则编译程序将外部型和静态型的数组初始化为0,而自动型数组的值不定。如不给自动
数组置初值,程序中又没有使用它,编译系统会给出警告信
息。
C语言允许自动型数组有初始值,它与外部型和静态型
数组初始值都包含在定义语句的花括号里,每个初始值用逗
号分隔。例如:
int d[5]={0,1,2,3,4};
如果定义时只对数组元素的前几个初始化,则其他元素
均被初始化为0;若未进行初始化,则C编译程序使用如下规
则对其进行初始化:
1)外部型和静态型数组元素的初始值为0;
2)自动型和寄存器型数组元素的初始值为随机数。
2.数组元素的地址每个数组元素都有自己的地址,并且是按下标序号顺序
排列的。
数组名就是数组的首地址,也就是第1个元素的地址。
假设有整型数组a[3],则第1个元素的地址有两种表示方
法,即a和a[0]。VC为整数分配4字节,下一个元素a[1]
的首地址与a[0]相差4字节。由指针的知识可知,a+1代表
将a的地址移动一个元素的长度,即移动4字节。a+1就是
a[1]的首地址。由此类推,可以写出如下程序输出它们各
个元素存储的首地址。
【例3.2】演示数组元素的地址。
include
void main
{
int i =0, a[3]={2, 4, 6};
printf(%p %p %p\n,a, a, a[0]); 3种表示方法等效
printf(%p\n, i); 变量i的首地址
for(i=0; i<3; i++) 与a + i表示方法等效
printf(%p ,a[i]);
printf(\n);
for(i=0; i<3; i++) 与a[i]表示方法等效
printf(%p , a + i);
printf(\n); for(i=0; i<3; i++) a[i]+i 表示的含义不同
printf(%p , a + i);
printf(\n);
}
程序运行结果如下:
0x0012FF70 0x0012FF70 0x0012FF70
0x0012FF7C
0x0012FF70 0x0012FF74 0x0012FF78
0x0012FF70 0x0012FF74 0x0012FF78
0x0012FF70 0x0012FF7C 0x0012FF88
在编程中,这三种表示方法都会用到。但在使用时要注
意,虽然a也是数组的首地址,但a+i的含义与a+i的并不
一样,所以不能使用a+i方式输出其他元素的地址。上面的
程序中使用语句
for(i=0; i<3; i++)
printf(%p , a + i);
输出地址,因为a+0就是a,所以第1个地址是对的。
但a+1则不是a[1]的地址,编译系统会在数组a的最后一
个地址+1,即在数组a的第3个元素的首地址0x0012FF78处移动4字节,即存储变量i的首地址0x0012FF7C。同理,a+2应从i的尾部0x0012FF80开始移动8字节,输出
0x0012FF88。即最后这段程序的输出为
0x0012FF70 0x0012FF7C 0x0012FF88
有人说可以使用(a+i),那也是错误的,且在编译时
就会出现错误。
3.存取数组元素的值
a是数组第1个元素存储的首地址,则a就是这个地址存
储的值。同理,数组第i个元素的值可以用下标表示为
a[i],也可以表示为(a+i)。下面的程序演示了两者的
对应关系。
【例3.3】演示数组元素的存取。
include
void main
{
int i =0, a[3], b[3]; for(i=0; i<3; i++)
{
a[i] = i + 50;
(b+i) = i + 50;
}
printf(%d %d\n, a[0], a); 两种表示方法等效
for(i=0; i<3; i++)
printf(%d , a[i]);
printf(\n);
for(i=0; i<3; i++)
printf(%d , (b + i)); 与a[i]等效
printf(\n);
}
程序运行结果如下:
50 50
50 51 52
50 51 52
运行结果也证实了上述结论。这两种方法都要熟练掌
握。3.2 一维字符串数组
字符数组是数组的一个特例。它除了具有一般数组的性
质之外,还具有自己的一些特点。它存储的是一串字符序
列,其中还包括转义字符序列。在字符串的最后位置上存放
一个标记串结束的字符(ASCII字符的0),即空字符。用
转义字符'\0'表示。字符串数组以'\0'结束,它的长度应
是存储字符长度加1。现在分析
int a[3];
char s[3];
这两个定义的区别。字符数组与整型数组的主要不同之
处有如下几点。
1)整型数组a的各个单元的数值长度是可以不相同的,如a[0]=123,a[2]=12345,a[0]是3位数字,a[2]是5位
数字。而字符数组s的各个单元都只能放一个字符,如s[0]='w',s[1]='e',w和e都是一个字符。
2)它们的有效长度不同,a[3]代表有3个数组元素,没有a[3]。而字符数组s中的s[2]='\0',存放的是字符串
结束标志,实际可用的有效上界是s[2],即只能放2个字
符。它的第s[2]个单元是定义的有效上界,当然也没有
s[3]。
3)字符数组的初始化格式比较特殊,可以用字符串来
代替,如:
char str[3] = we; 编译程序将自动加上结束标记'\0'
也可以同整型数组一样初始化。如:
char str[3] = { 'w', 'e', '\0'}; 必须手工以'\0'结束
上述两种方式置初值是等价的。在对字符数组初始化
时,为使程序能觉察到此数组的末尾,在内部表示中,编译
程序要用'\0'来结束这个数组,从而存储的长度比双引号之间的字符的个数多一个。在上面的具体例子中,we字符串
中的字符数是2,而str的长度却是3。
4)与数值数组一样,字符串数组的名字就是字符串的
首地址。不同的是,字符串是顺序存放并以'\0'作为结束标
志。所以字符串既可以按顺序输出,也可以用名字整体输
出。在读入字符串时,单个字符串中不能有空格。
【例3.4】演示字符串数组的例子。
include
void main
{
char s[]=abcd; 定义字符串
int i=4;
printf(%s\n,s); 整体输出内容
for(i=0;i<4; i++) 顺序输出
printf(%c ,s[i]);
printf(\n);
}
字符串顺序存放,字符串名是其首地址。s输出的不是
地址,而是地址里的内容。程序输出结果如下。
abcda b c d
需要注意的是,输出字符串首地址格式可使用“%p”以
十六进制输出,也可以“%u”或者“%d”以十进制格式输
出,但不能使用
printf(%s\n,s);
语句,这个语句仍然是输出字符串的内容。
【例3.5】演示使用不同格式输出字符串数组首地址的
例子。
include
int main
{
char s[]=fish;
printf(%p %p\n,s, s);
printf(%10u %u\n,s, s);
printf(%10d %d\n,s, s);
printf(%10s %s\n,s, s);
return 0;
}
输出结果如下:0x0012FF78 0x0012FF78
1245048 1245048
1245048 1245048
fish fish3.3 使用一维数组容易出现的错误
使用数组最容易犯的错误是数组越界和初始化错误。本
节的分析仅局限于一维数组。3.3.1 一维数组越界错误
【例3.6】使用数组下标越界的例子。
include
int main
{
int i, a[5];
for(i=1;i<=5;i++)
a[i]=i;
return 0;
}
在上述程序中,循环语句
for(i=1;i<=5;i++)
a[i]=i;
有如下两个错误。
1)没有给数组a的第一个元素a[0]赋初值。
2)超出了数组的尾端。一个长度为5的数组,其元素下
标为0~4,即a[4]是最后一个元素。这种错误会造成程序运行时的时断时续的错误。
正确写法应该是:
for ( i=0; i<5; ++i)
a[i]=i;
因为字符数组的最后一个结束标志位是'\0',字符数组
c[5]只能存放4个字符,所以下面的语句
char c[5]=abcde;
也产生数组越界错误。正确的写法是只能有4个字符。
即
char c[5] = abcd;
下面通过讨论C语言的这个特点,以帮助读者杜绝这个
错误。
1.数值数组的边界不对称性记住C语言数值数组采取的是不对称边界,即C语言中一
个具有n个元素的数值数组,它的元素下标是从0到n-1,即
从0开始,没有下标为n的元素,有效元素是n个。
这种不对称边界反而有利于编程。假设定义5个元素的
数组a[5],虽然
for ( i=0; i<=4; ++i)
a[i]=i;
的方法也是正确的,但考虑到0是第1个元素,元素数量
是5,且下标5是不含在下标范围内的,所以推荐使用
for ( i=0; i<5; ++i)
a[i]=i;
的方式,有效的元素数量n=5-0=5。如果定义数组是从
1开始的,显然要包含下标5,元素数量n=5-1+1=5。由于使
用了0,所以就避免了+1的运算。这就是它的优点。
虽然数组没有a[n]这个元素,但是却可以引用这个元素的地址a[n],而且ANSI C标准也明确允许这种用法:数组
中实际不存在的“溢界”元素的地址位于数组所占内存之
后。这个地址可以用来进行赋值和比较,但因为不存在
a[n],所以引用该元素的值就是非法引用。
2.字符数组的边界不对称性
字符数组更为特殊,它的第n-1个元素是法定的'\0',能存储的有效字符为n-1个。
【例3.7】下面的程序对吗?
include
int main
{
int i;
char a[]=abcde,b[6];
for(i=0;i<5;i++)
b[i]=a[i];
printf(b);
printf(\n);
return 0;
}
这个程序是错的。程序的错误是只复制5个元素。语句char a[]=abcde
定义的字符数组是a[6],它具有6个元素,只是第6个
元素是结束符'\0'。这个结束符必须复制到字符数组b,不
然它没有结束符,造成语句
printf(b);
除了输出“abcde”之外,还将其后的字符输出(如果
不是字符代码,则输出乱码),直到遇到空格才能结束。应
将for语句改为
for(i=0;i<6;i++)
可以利用这个结束位编程,下面是一个例子。
【例3.8】利用结束位编程的例子。
include
int main
{
int i=0; char a[ ] = How are you?, b[13];
while (a[i]!='\0')
{b[i]=a[i];++i;}
b[i]='\0';
i=-1;
while (i++,b[i]!='\0')
printf(%c ,b[i]);
printf(\n);
return 0;
}
第1个while语句复制时,因为没有复制结束位,所以
要补一个结束位。因为第2个while语句的循环要用
到“i++”,所以将i的初始值设为-1,输出以结束位为结
束条件。3.3.2 一维数组初始化错误
【例3.9】混淆定义与初始化的例子。
include
int main
{
int i, a[5];
char ch[5];
a[5]={1, 2, 3, 4, 5};
ch[5]=good!;
for(i=0; i<5; i++)
printf(%d ,a[i]);
printf(\n);
printf(ch);
printf(\n);
return 0;
}
上面语句都是在定义数组以后为其赋值。如果定义以后
再赋值,则需要一个一个地赋值。字符数组ch的定义不对,ch的长度不够,将产生数组越界错误。程序其实是用初始化
的方法为元素赋值,本例混淆了定义与初始化。现将它们都
改为在定义时初始化,即
int a[5]={ 1, 2, 3, 4, 5};char ch[]=good!;
这里由编译系统识别ch的下标。如果要直接使用下标,应该定义为
char ch[6]=good!;
修改后的程序如下。
include
int main
{
int i, a[5] = { 1, 2, 3, 4, 5};
char ch[ ] = good!;
for(i=0; i<5; i++)
printf(%d , a[i]);
printf(\n);
printf(ch);
printf(\n);
return 0;
}
运行结果如下。
1 2 3 4 5
good!3.3.3 数组赋值错误
本节指出的错误不涉及指针(涉及指针类的错误将放在
数组与指针关系中讨论),为了熟悉一维数组,本节仍然不
讨论多维数组。
【例3.10】下面的程序错误地使用了赋值运算符,请改
正。
include
int main( )
{
int i, a[5]={ 1, 2, 3, 4, 5},b[5];
char ch[]=good!,st[6];
b=a;
st=ch;
for(i=0;i<5;i++)
printf(%d ,b[i]);
printf(\n);
printf(st);
printf(\n);
return 0;
}
数组没有赋值运算符“=”,必须一个元素一个元素地赋值。
正确的程序如下:
include
int main
{
int i, a[5]={ 1, 2, 3, 4, 5},b[5];
char ch[]=good!,st[6];
for(i=0;i<5;i++)
b[i]=a[i];
for(i=0;i<6;i++)
st[i]=ch[i];
for(i=0;i<5;i++)
printf(%d ,b[i]);
printf(\n);
printf(st);
printf(\n);
return 0;
}
【例3.11】下面的程序使用键盘赋值,改正程序中的错
误。
include
int main
{
int i, a[5];
char ch[5];
for(i=0; i<5; i++)
scanf(%d, a+i);
for(i=0; <5; i++) printf(%d , a+i);
printf(\n);
for(i=0;i<5;i++)
scanf(%c, ch+i);
ch[i] = '\0';
printf(ch); printf(\n);
return 0;
}
程序中对数组a的赋值不对,i=0时,语句
scanf(%d,a);
为第1个元素a[0]赋值。当i=1时,“a+1”的含义是
数组a的最后一个元素的首地址值加1,即数组a后面的地
址,而不是第2个元素a[1]的地址(详见3.1节)。a[1]的
地址应该是“a[1]”,使用语句
scanf(%d,a[i]);
才是正确的。还有一个正确的写法,就是用地址循环。
因为数组名就是数组存储的首地址,所以a+1代表的是首地
址移动到第2个元素存储的首地址,也就是第2个元素的数组名。注意,a和a[0]的含义一样,都是第1个元素a[0]的地
址。a+1就代表a[1]的地址,与a[1]等效,所以如下两条
语句都是正确的。
scanf(%d, a[i]);
scanf(%d, a+i);
scanf要求的是存储元素的首地址,a[i]是变量,不是
地址。a[i]和a+i才是变量的地址,所以上述两条语句是
等效的。
同理,printf输出的是地址里的内容。a存放的是第1
个元素的首地址,a是第1个元素的值。a+1是数组第1个
元素之值加1,显然是不对的,应该是(a+1)。a+1代表
了首地址移动1个元素的地址,即第2个元素的首地址。下述
两条语句是等效的。
printf(%d ,(a+i));
printf(%d ,a[i]);修改后的程序如下:
include
int main
{
int i, a[5];
char ch[5];
for(i=0;i<5;i++)
scanf(%d,a+i); 等效scanf(%d,a[i]);
for(i=0;i<5;i++)
printf(%d ,(a+i)); 等效printf(%d ,a[i]);
printf(\n);
for(i=0;i<5;i++)
scanf(%c,ch+i); 等效scanf(%c,ch[i]);
ch[i]='\0';
printf(ch);
printf(\n);
return 0;
}
输出结果如下:
12345
1 2 3 4 5
abcde
abcd
字符数组ch长度为5,但是要存入一个结束符,所以如
果输入4个字符,则存入正确;如果输入5个,则最终只能存放4个字符。上述字符输入和输出也证明了这一点。
【例3.12】使用键盘为字符数组赋值的例子。
include
void main
{
int i;
char str[256];
i = -1;
do
{
i++;
scanf(%c, str[i]);
}while(str[i] != '');
str[i] = '\0';
printf(%s\n, str);
scanf(%s, str);
printf(%s\n, str);
}
do…while语句可以输入空格,它以“”为结束条件,而且可以输入多行。第2个输入语句是用空格作为分隔符
的,即只接收一个连续字符串,空格之后的字符将被忽略。
下面的运行示范中,第2个输入为“Go home!”,但程序
只接受Go。程序运行示范如下:
Go home!
OK
Go home!
OK
Go home!
Go3.3.4 求值顺序产生歧义错误
不要假设求值顺序。这将会使程序在有些机器上可能是
正常的,但在另一些机器上则不一定正常。
【例3.13】存在求值顺序歧义问题的程序。
include
void main( )
{
int i = 0, n = 5;
int y[5], x[]={1, 2, 3, 4, 5};
while (i < n)
y[i] = x[i++];
for ( i=0; i
printf(%d , y[i]);
}
程序是想将数组x的内容复制到数组y并输出y的内容。
但while语句中假设了求值顺序,这是不可靠的。应该写成
while (i
y[i]=x[i];
i++;
}最好简写成
for ( i = 0; i < n; i++ )
y[i]=x[i];
程序修改为
include
void main( )
{
int i=0, n=5;
int y[5], x[]={1, 2, 3, 4, 5};
for ( i=0; i
y[i] = x[i];
printf(%d , y[i]);
}
printf(\n);
}
使用时一定要注意编译系统的区别,不要在假设的前提
下编写程序。3.4 综合实例
本节列举几个典型的例子,说明数组的使用方法。
【例3.14】计算输入字符串中数码和字符出现次数的程
序。
include stdio.h
void main( )
{
int i, j, nw, nd[10];
char s[100], c;
j=0;
nw=0;
for ( i=0; i <10; i++ )
nd[i]=0;
while ( ( c=getchar( ) )!= '\n' )
{
s[j]=c;
switch ( s[j] ) {
case '0': case '1': case '2':
case '3': case '4': case '5':
case '6': case '7': case '8':
case '9':
nd[s[j]-'0']++;
break;
default:
nw++;
break;
}
j++; }
for ( i=0; i<10; i++ )
printf (%d , i );
printf (\n);
for ( i=0; i<10; i++ )
printf (%d , nd[i] );
printf (\nnw=%d\n, nw );
}
程序运行示范如下:
247909347674e ewfqgq\=.,0 1 2 3 4 5 6 7 8 9
1 0 1 1 3 0 1 3 0 2
nw=12
本例依赖于数字的字符表示,switch语句将数字分离
出来,而且此数字的值是s[j]-'0',这个值刚好落在数值
0~9之间,恰好是nd[]的下标值。非数字则由nw计数。
【例3.15】统计输入串中每个数字、字母和其他字符的
个数。
include
void main( )
{
int i, nother;
char c; int ndigit[10], nchar[26];
nother=0;
for ( i=0; i<10; ++i )
ndigit[i]=0;
for ( i=0; i<26; ++i )
nchar[i]=0;
while( ( c=getchar( ) ) != '\n' )
if ( c >= '0' c<= '9' )
++ndigit[c-'0'];
else if (c >= 'a' c<= 'z' )
++nchar[c-'a'];
else if (c>= 'A' c<= 'Z' )
++nchar[c-'A'];
else
++nother;
printf (digits= );
for ( i=0; i<10; ++i )
printf(%d ,ndigit[i] );
printf (\ncharacters= );
for ( i=0; i<26; ++i )
printf ( %d ,nchar[i] );
printf (\nother= %d\n,nother );
}
程序运行示范如下:
1234567898734abcdeghujxyz=-@^%321
digits=0 2 2 3 2 1 1 2 2 1
characters=1 1 1 1 1 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 1
other= 6
测试语句:if ( c>= 'a' c<= 'z' );
用来确定c中字符是否是小写英文字母,如果是小写英
文字母,则数值c-'a'这个值刚好落在数值0~25之间,恰好
是nchar[]的下标值。大写字母和数字处理方法类似。
通过上述例子可以看出,数组的元素可像一般变量那样
进行赋值、比较和运算。
【例3.16】求解6个元素的整型数组中正元素之和的程
序。
include
void main( )
{
int i,sum;
static int a[ ]={ -15,8, -225, -24, 56, -158};
sum=0;
for ( i=0; i<6; i++ ) {
if(a[i] <0)
continue;
sum=sum+a[i];
}
printf ( sum=%d\n,sum );
}程序运行输出:
sum=64
【例3.17】A、B、C、D、E合伙夜间捕鱼,凌晨时都疲
惫不堪,各自在河边的树丛中找地方睡着了。日上三竿,A
第一个醒来,他将鱼平分作5份,将多余的一条扔回湖中,拿自己的一份回家去了;B第二个醒来,也将鱼平分作5份,扔掉多余的一条,只拿走自己的一份;接着C、D、E依次醒
来,也都按同样的办法分鱼。5人至少合伙捕到多少条鱼?
每个人醒来后看到的鱼数是多少条?
【解题思路】假定A、B、C、D、E5人的编号分别为1、2、3、4、5,为了容易理解,让整数数组的标号直接与这5
个人的序号对应,定义整数数组fish[6]。不使用
fish[0],从而可以使用数组fish[k]表示第k个人所看到的
鱼数。fish[1]表示A所看到的鱼数,fish[2]表示B所看到
的鱼数……显然有如下关系:fish[1]=5人合伙捕鱼的总鱼数
fish[2]=(fish[1]-1)×45
fish[3]=(fish[2]-1)×45
fish[4]=(fish[3]-1)×45
fish[5]=(fish[4]-1)×45
由此可以写出如下一般表达式:
fish[i]=(fish[i-1]-1)×45 i=2,3,4,5
这个公式可用于从已知A看到的鱼数去推算B看到的,再
推算C看到的……现在能否倒过来,先知E看到的再反推D看到
的……直到A看到的。为此将上式改写为
fish[i-1]=fish[i]×54+1 i=5,4,3,2
分析上式如下:1)当i=5时,fish[5]表示E醒来后看到的鱼数,该数
应满足被5整除后余1,即fish[5]%5==1。
2)当i=5时,fish[i-1]表示D醒来后看到的鱼数,该
数既要满足fish[4]=fish[5]×54+1,又要满足
fish[4]%5==1。显然,fish[4]只能是整数,这个结论同
样可以用于fish[3]、fish[2]、和fish[1]。
3)按题意要求5人合伙捕到的最少鱼数,可以从小往大
枚举,先设E所看到的鱼数最少为6条,即fish[5]初始化为
6,之后每次增加5再试,直至递推到fish[1]且所得整数除
以5之后的鱼数为1。根据上述思路,可以将程序分为3个部
分:程序准备(包括声明和初始化)部分、递推部分和输出
结果部分。
程序准备部分包含定义数组fish[6]并初始化为1,定
义循环控制变量i并初始化为0。输出结果部分就是输出计算
结果。以上两个部分都很简单,下面着重介绍递推部分的实现方法。
递推部分使用do…while直到型循环结构,其循环体又
包含两部分:
1)枚举过程中的fish[5]的初值设置,一开始
fish[5]=1+5;以后每次增5。也就是说,第1个边界条件是
fish[5]=6,以后的边界条件是每次递增5。
2)使用一个for循环,i的初值为4,终值为1,步长
为-1,该循环的循环体是一个分支语句,如果fish[i+1]不
能被4整除,则跳出for循环(使用break语句);否则,从
fish[i+1]计算出fish[i]。当由break语句让程序退出循
环时,意味着某人看到的鱼数不是整数,当然不是所求,必
须令fish[5]加5后再试,即重新进入直到型循环do…while
的循环体。当正常退出for循环时,一定是循环控制变量i从
初值4,一步一步执行到终值1,每一步的鱼数均为整数;最
后i=0,表示计算完毕,且也达到了退出直到型循环的条件。
捕鱼问题参考程序
include
void main
{
int fish[6]={1,1,1,1,1,1}; 记录每人醒来后看到的鱼数
int i=0;
do
{
fish[5]=fish[5]+5; 让E看到的鱼数增5
for (i=4; i>=1; i--)
{
if (fish[i+1]%4!=0)
break; 跳出for循环
else
fish[i]=fish[i+1]54+1; 计算第i个人看到的鱼数
}
} while (i>=1); 当i>=1时,继续做do循环
输出计算结果
for (i=1; i<=5; i++)
printf(第%d个人看到的鱼是%d条。\n,i,fish[i]);
}
程序运行的输出结果如下:
第1个人看到的鱼是3121条。第2个人看到的鱼是2496条。第3个人看到的鱼是1996条。第4个人看到的鱼是1596条。第5个人看到的鱼是第4章 指针与数组
在C语言中,数组和指针是两个非常重要的概念,一定
要熟练掌握数组和指针的用法。虽然数组和指针是两个不相
关的概念,但在使用时,却又存在难以割舍的关系,所以一
定要掌握两者配合使用的方法。只有掌握这些内容,才能用
好它们。4.1 数组与指针的关系
C语言的数组名字就是这个数组的起始地址,指针变量
存储地址,数组名就是指针常量,所以指针和数组有密切关
系。任何能由数组下标完成的操作,也能用指针来实现。使
用指向数组的指针,有助于产生占用存储空间小、运行速度
快的高质量的目标代码。
指向数组的指针实际上指的是能够指向数组中任一个元
素的指针。这种指针应当说明为数组元素类型。例如,程序
中声明如下整型数组a
int a[5];
这时只要声明如下一个整型指针变量:
int pa;
就可 ......
刘振安 刘燕君 编著
ISBN:978-7-111-55406-6
本书纸版由机械工业出版社于2016年出版,电子版由华
章分社(北京华章图文信息有限公司,北京奥维博世图书发
行有限公司)全球范围内制作与发行。
版权所有,侵权必究
客服热线:+ 86-10-68995265
客服信箱:service@bbbvip.com
官方网址:www.hzmedia.com.cn
新浪微博 @华章数媒
微信公众号 华章电子书(微信号:hzebook)目录
前言
第1章 引入指针变量
1.1 变量的三要素
1.2 变量的操作
1.3 指针变量
1.4 指针类型
第2章 指针基础知识
2.1 指针运算符
2.2 指针移动
2.3 指针地址的有效性2.4 指针的初始化
2.5 指针相等
2.6 对指针使用const限定符
2.7 使用动态内存
2.7.1 动态内存分配函数
2.7.2 内存分配实例
2.7.3 NULL指针
第3章 一维数组
3.1 一维数值数组
3.2 一维字符串数组
3.3 使用一维数组容易出现的错误3.3.1 一维数组越界错误
3.3.2 一维数组初始化错误
3.3.3 数组赋值错误
3.3.4 求值顺序产生歧义错误
3.4 综合实例
第4章 指针与数组
4.1 数组与指针的关系
4.2 一维字符串数组与指针
4.3 字符串常量
4.4 指针数组
4.5 配合使用一维数组与指针4.5.1 使用一维数组名简化操作
4.5.2 使用指针操作一维数值数组
4.5.3 使用一维字符数组
4.5.4 指针初始化实例
4.6 动态内存分配与非数组的指针
4.7 二维数组与指针
4.7.1 二维数组
4.7.2 二维数组操作实例
4.7.3 二维数组与指针的关系
4.7.4 二维数组与指向一维数组的指针
4.7.5 二维字符串数组4.8 综合设计实例
4.8.1 使用数组求解
4.8.2 使用动态内存求解
4.8.3 使用二级字符串指针求解
第5章 函数基础知识
5.1 函数
5.1.1 函数和函数原型
5.1.2 函数值和return语句
5.1.3 函数调用形式
5.1.4 函数参数的基础知识
5.1.5 被调用函数的返回位置5.2 C程序的典型结构
5.2.1 单文件结构
5.2.2 一个源文件和一个头文件
5.2.3 多文件结构
5.3 变量的作用域
5.3.1 单文件里的块结构及函数
5.3.2 单文件多函数的变量
5.3.3 多文件变量作用域
5.4 变量的存储地址分配
5.4.1 单文件变量的存储地址分配
5.4.2 多文件变量的存储地址分配5.5 main函数原型及命令行参数
第6章 函数设计
6.1 函数设计的一般原则
6.1.1 函数设计基础
6.1.2 函数设计的注意事项
6.1.3 函数的一般结构
6.2 函数的返回值
6.2.1 无返回值的void类型函数
6.2.2 非void类型的函数必须返回一个值
6.2.3 使用临时变量作为返回值的函数
6.2.4 不能使用临时数组名作为返回值6.2.5 返回临时指针必须是首地址
6.2.6 返回结构的函数
6.2.7 返回结构指针的函数
6.2.8 返回枚举的函数
6.3 函数参数的传递方式
6.3.1 传数值
6.3.2 传地址值
6.4 函数指针
6.5 理解函数声明
6.5.1 词法分析中的“贪心法”
6.5.2 克服语法“陷阱”读懂函数6.6 函数设计举例
6.6.1 完璧归赵
6.6.2 多余的参数
6.6.3 传递的参数与函数参数匹配问题
6.6.4 等效替换参数
6.6.5 设计状态机函数
第7章 函数设计实例
7.1 函数的类型和返回值
7.1.1 函数的类型应力求简单
7.1.2 实参要与函数形参的类型匹配
7.1.3 正确设计函数的返回方式7.1.4 正确区别指针函数和函数指针
7.2 正确选择函数参数
7.2.1 使用结构作为参数
7.2.2 使用键盘为参数赋值
7.2.3 结构的内存分配
7.3 算法基本概念
7.4 使用库函数
7.5 设计实例
7.5.1 递推与递归
7.5.2 递推求解切饼问题
7.5.3 八皇后问题7.5.4 疑案求解
7.5.5 二分查找
7.5.6 汉诺塔问题
7.5.7 青蛙过河
7.5.8 猜数游戏
7.5.9 生死游戏
7.5.10 最短路径
第8章 多文件中的函数设计
8.1 C语言预处理器
8.1.1 宏定义与const修饰符
8.1.2 文件包含8.1.3 条件编译
8.2 模块化程序设计基础
8.2.1 模块化程序设计
8.2.2 分块开发
8.2.3 工程文件
8.2.4 函数设计的注意事项
8.3 使用两个文件的设计实例
8.3.1 设计题目和实现方法
8.3.2 算法和函数设计
8.3.3 完整源程序
8.3.4 组成工程并运行程序8.4 使用3个文件的设计实例
8.4.1 设计思想
8.4.2 算法分析
8.4.3 完整源程序
8.4.4 程序运行
8.5 使用条件编译的多文件设计实例
8.5.1 实现功能
8.5.2 设计思想
8.5.3 参考程序
8.5.4 程序运行
第9章 多文件综合设计实例9.1 使用链表设计一个小型通讯录程序
9.1.1 功能设计要求
9.1.2 设计思想
9.1.3 程序设计
9.1.4 运行示范
9.2 使用数组设计一个实用的小型学生成绩管理程
序
9.2.1 功能设计要求
9.2.2 总体设计
9.2.3 函数设计
9.2.4 参考程序
9.2.5 运行示范第10章 设计游戏程序实例
10.1 剪刀、石头、布
10.1.1 设计思想
10.1.2 参考程序
10.1.3 运行示范
10.2 迷宫
10.2.1 设计思想
10.2.2 参考程序
10.2.3 运行示范
10.3 空战
10.3.1 设计思想10.3.2 参考程序
10.4 贪吃蛇
10.4.1 供改造的源程序
10.4.2 运行示范
10.5 停车场
10.5.1 参考程序
10.5.2 运行示范
10.6 画矩形
10.6.1 用C语言编写Windows程序
10.6.2 Windows的程序结构
10.6.3 用C语言编写画矩形程序10.7 俄罗斯方块
10.7.1 基本游戏规则
10.7.2 基本操作方法
10.7.3 编写游戏交互界面问题
10.7.4 用C语言编写控制台俄罗斯方块游戏
10.7.5 编写Windows俄罗斯方块游戏
10.8 用C语言编写Windows下的贪吃蛇游戏
10.8.1 程序清单
10.8.2 运行示范
附录 7位ASCII码表
参考文献前言
C语言编程仍然是编程工作者必备的技能。本书的基础
版本《C语言解惑》[1]通过比较编程中存在的典型错误,从
而实现像雨珠打在久旱的沙滩上一样滴滴入骨的效果,使学
习者更容易记住编程的要诀,并通过演示如何将一个能运行
的程序优化为更好、更可靠的程序,使读者提高识别坏程序
和好程序的能力。尽管如此,那本书仍然要照顾初学者并兼
顾知识的完整性,所以讨论的深度有所限制。为此,我们决
定推出它的提高版,并将讨论聚焦于函数设计。
本书将集中讨论C语言的核心部分——函数设计。函数
设计涉及函数类型、函数参数及返回值,这就要求读者熟练
掌握指针和数组的知识,此外,还要掌握多文件编程以及多
文件之间的参数传递等知识。
因为本书要求读者已经学过C语言,所以我们可以完
整、系统地论述各个部分的内容,无须赘述基础知识。本书的另一个特点是每一章之间都有知识交叉,进而达到讲透的
目的。如果遇到不清楚的知识点,读者可以自行学习相应参
考资料,也可以与《C语言解惑》配合学习。
本书的落脚点是实现C语言的结构化程序设计。为实现
这一目标,本书专门选择了完整的设计实例。尤其是第10
章,结合趣味游戏程序,综合讲解函数设计和多文件编程。
本书各个部分论述详细,涉及的知识面广,有些知识是
传统教材中所没有的,所以它既可以作为从事教学的老师及
工程技术人员的参考书,也可以作为常备手册。其实,它不
仅对工程技术人员极有参考价值,也能帮助在校生进行编程
训练或作为毕业论文的参考资料。此外,本书对于初学者也
大有帮助,他们可以将它作为课外读物,对目前看不懂的地
方,可以等具备相关知识之后再来研究,彼时将收获更大。
总之,本书能帮助各类人群找到自己需要的知识并有所收
获,而这也将拓宽本书的应用范围。本书共分10章。第1章通过例子说明引入指针变量的必
要性并简单介绍指针变量的基本性质。第2章通过实例解释
指针的基本性质。第3章介绍数组及数组的边界不对称性。
第4章介绍C语言中两个非常重要的概念——数组和指针。第
5章介绍如何掌握函数设计和调用的正确方法。第6章介绍如
何设计合理的函数类型及参数传递方式。第7章先讨论函数
设计的一般原则,然后结合典型算法,用实例说明设计的具
体方法,以便使读者进一步开阔眼界。第8章结合具体实例
详细介绍头文件的编制、多个C语言文件及工程文件的编制
等方法,以提高读者的多文件编程能力。第9章给出两个典
型的多文件编程实例,一个使用链表,另一个使用数组。第
10章中的游戏程序实例将加深读者对一个完整工程项目的理
解。为了学习方便,本书提供全部程序代码。
本书的两位作者分别撰写各章的不同小节,然后逐章讨
论并独立成章。刘燕君负责第1~6章,刘振安负责第7~10
章,最后由刘振安统稿。参与本书工作的还有周淞梅实验师、苏仕华副教授、鲍运律教授、刘大路博士、唐军高级工
程师等。
在编写过程中,我们得到了中国科学院院士、中国科学
技术大学陈国良教授的大力支持,特此表示感谢!对书中所
引用资料的作者及网络作品的作者表示衷心感谢!
作者
zaliu@ustc.edu.cn
2016年6月
[1] 此书已于2014年由机械工业出版社出版,书号
978-7-111-47985-7。第1章 引入指针变量
指针在C语言中具有举足轻重的地位,也是编制C程序的
基本功之一。本章将通过例子说明引入指针变量的必要性并
简单介绍指针变量的基本性质。1.1 变量的三要素
一个变量具有3个要素:数据类型、名字和存放变量的
内存地址。本节将简要回顾变量的3个要素,以便为引入指
针打下基础。
1.基本数据类型
数据类型是C语言中非常重要的一个概念,它将C语言所
处理的对象按其性质不同分为不同的子集,以便对不同类型
的数据规定不同的运算。void是无类型标识符,只能声明函
数的返回类型,不能声明变量,但可以声明指针。
本节只涉及基本数据类型,C语言的基本数据类型有如
下4种。
·char 字符型
·int 整数型·float 浮点数型(又称为单精度数)
·double 双精度浮点数型
另外还有用于整型的限定词short、long、signed和
unsigned。short和long表示不同长度的整型量;
unsigned表示无符号整型数(它的存放值总是正的);可
以省略signed限定词。例如,可以将如下声明
short int x;
unsigned int z;
中的说明符int省略。即它们与如下声明
short x;
unsigned z;
是等效的。上述数据类型的长度及存储的值域也随编译
器不同而变化,ANSI C标准只限定int和short至少要有16
位,而long至少32位,short不得长于int,int不得长于
long。表1-1是数据类型的长度及存储的值域表,表1-1中VC是Visual C++6.0的缩写。表1-2是加了限定词的数据类
型及它们的长度和取值范围。
C语言提供一个关键字sizeof,用来求出对于一个指定
数据类型,编译系统将为它在内存中分配的字节长度。例
如,语句“printf(%d,sizeof(double));”的输
出结果为8。
注意在表1-1中的标注,在VC中int使用4字节,这是本
章计算的依据。
C语言定义的存储类型有4种:auto、extern、static
和register,分别称为自动型、外部型、静态型和寄存器
型。自动型变量可以省略关键字auto。存储类型在类型之
前,即
存储类型 类型
例如auto int和static float等。可以省略auto,其他类型均不可以省略。
表1-1 数据类型的长度及存储的值域
表1-2 加限定词的数据类型及其长度和取值范围
2.变量的名字和变量声明
C语言中大小写字母是具有不同含义的,例如,name和
NAME就代表不同的标识符。原来的C语言中虽然规定标识符
的长度不限,但只有前8个字符有效,所以对定义为
dwNumberRadio
dwNumberTV的两个变量是无法区别的。
现在流行的为32位操作系统配备的C编译器已经能识别
长文件名,不再受8位的限制。另外,在选取时不仅要保证
正确性,还要考虑容易区分,不易混淆。例如,数字1和字
母i在一起,就不易辨认。在取名时,还应该使名字有很清
楚的含义,例如使用area作为求面积函数的名字,area的
英文含义就是“面积”,这就很容易从名字猜出函数的功
能。对一个可读性好的程序,必须选择恰当的标识符,取名
应统一规范,以便使读者能一目了然。
在现在的编译系统中,内部名字中至少前31个字符是有
效的,所以应该采用直观的名字。一般可以遵循如下简单规
律。
1)使用能代表数据类型的前缀。
2)名称尽量接近变量的作用。3)如果名称由多个英文单词组成,每个单词的第一个
字母大写。
4)由于库函数通常使用下划线开头的名字,因此不要
将这类名字用作变量名。
5)局部变量使用比较短的名字,尤其是循环控制变量
(又称循环位标)的名字。
6)外部变量使用比较长且贴近所代表变量的含义。
7)函数名字使用动词,如Get_char(void)。变量使
用名词,如iMen_Number。
变量命名可以参考Windows API编程推荐的匈牙利命名
法。它是通过在数据和函数名中加入额外的信息,既增进程
序员对程序的理解,又方便查错。
所有的变量在使用之前必须声明,所谓声明即指出该变
量的数据类型及长度等信息。声明由类型和具有该类型的变量列表组成。如:
int lower,upper;
char c,name[15];
变量可按任何方式分布在若干个声明中,上述声明同样
可以写成:
int lower; 整数类型
int upper; 整数类型
char c; 字符类型
char name[15]; 字符数组,可连续存放15个字符
后一种形式会使源程序冗长,但便于给每个声明加注
释,也便于修改。
变量的存储类型在变量声明中指定。变量声明的一般形
式为:
存储类型 类型 变量名列表;
应该养成在声明时就为变量赋初值的习惯,但在某些特殊场合则只能声明,如头文件中对外部变量的声明,下面是
一些典型的例子。
auto int a;
static float b, c;
extern double x;
register int i=0;
extern char szClassame[ ];
static int size=50;
const double PI=3.14159;
3.变量的地址
内存地址由系统分配,不同机器为变量分配的地址大小
虽然可以不一样,但都必须给它分配地址。
在C语言中,声明和定义两个概念是有区别的。声明是
对一个变量的性质(如构成它的数据类型)加以说明,并不
为其分配存储空间;而定义则是既说明一个变量的性质,又
为其分配存储空间。定义一个函数,也是为它提供代码。1.2 变量的操作
从三要素可知,既可以通过名字对变量进行操作,也可
以通过地址对存放在该地址的变量进行操作。
1.左值和右值的概念
变量是一个指名的存储区域,左值是指向某个变量的表
达式。“左值”来源于赋值表达式“A=B”,其中左运算分
量“A”必须能被计算和修改。左值表达式在赋值语句中既
可以作为左操作数,也可以作为右操作数,例
如“x=56”和“y=x”,x既可以作为左值(x=56),又可
以作为右值(y=x)。但右值“56”只能作为右操作数,而
不能作为左操作数。由此可见,常量只能作为右值,而普通
变量既可以作为左值,也可以作为右值。如下语句
const int a = 256;定义的a,显然不能作为左值,只能作为右值。
由此可见,值可以作为右值,如整数、浮点数、字符
串、数组的一个元素等。在C语言中,右值以单一值的形式
出现。假设有字符数组a和b,则这两个字符数组的每个元素
均可以作为右值,即“a[0]=b[0]”是正确
的,“b[0]=a[0]”也是正确的。需要注意的是,它们
在“=”号左右两边的含义是不同的。以a[0]为例,在“b[0]=a[0]”中,它是作为值出现的,即a[0]是数组第
1个元素的值;而在“a[0]=b[0]”中,它是作为变量出现
的,即a[0]是数组的第1个元素的变量名,所以a[0]可以作
为左值。即可以使用数组的具体元素作为左值和右值。
a和b都不是字符串的单个元素,所以都不能作为右值。
而因为a和b可以作为数组首地址的值赋给指针变量,所以在
这种情况下它们又都可以作为右值。
由此可见,在C语言中,左值是一个具体的变量,右值一定是一个具体类型的值,所以有些可以既可以作为左值,也可以作为右值,但有些只能作为右值。
2.对变量的基本操作
C语言使用地址运算符“”来取变量存储在内存中的首
地址。假设变量a=55,但不同机器和系统为它分配的地址是
不一样的,这里也假设分配的十六进制地址是
0x0012FF7C。如何从这个地址取出“55”呢?
C语言提供了“”运算符,用来取出地址里的
值。“a”代表地址,显然“a”可以取出55。使用下面
语句
printf(%d,%d\n,a,a,);
可以得到输出结果为“55,55”,即证明a和a是等
价的。1.3 指针变量
1.2节介绍了“”和“”运算符,本节将通过具体的
例子说明它们的用途,从而引入指针变量。
1.对有效地址进行操作
【例1.1】取地址里的值和取地址里存放的地址值的例
子。
include
int main
{
int a = 65;
int addr;
addr = 0x0012ff7c; 将a的首地址存入变量addr
printf(0x%p, 0x%p, 0x%p\n, a, addr, addr); 输出3个地址
printf(%d,%d,0x%p\n, a, a, addr); 输出变量及地址里的值
return 0;
}
语句“int a=65;”定义了整型变量a的值为65,VC使
用4字节存储65。假设存放它的内存首地址为十六进制的“0x0012ff7c”,则可以使用输出格式“%p”来输出这
个地址。“0x”是标注它为十六进制地址,也可以简单地使
用“%p”输出地址。
一个变量具有地址和值,是取地址值运算符。系统为
整型变量a和adder分别分配地
址“0x0012ff7c”和“0x0012ff78”。给整型变量addr赋
十六进制数,这个数可以代表地址,但不一定是有效的地址
(将计算机可以存取的地址称为有效地址)。已经验证
0x0012ff7c是分配给变量a的地址,所以addr是被赋给一个
有效地址。
将“0x0012ff7c”赋给变量addr,addr是系统分给
它的地址“0x0012ff78”,这个地址与a的地址相差4字
节,证明它们是连续存放的。现在这个地址里存放的是地址
0x0012ff7c,也就是变量a的地址。因为addr应该输出a
的地址而不是a的值,所以要使用%p格式。程序输出结果也
验证了如上分析。即输出为0x0012FF7C,0x0012FF7C,0x0012FF78
65,65,0x0012FF7C
既然addr存放的是有效地址,addr也应该能输出这个
地址里的值,也就是变量a的地址。不过,a虽然和addr的
值是一个值,但它们对运算的反应并不一样。a是变量,其
值为65,a是存储地址,所以a是取地址里的值。类似
的,addr应该输出它的存储内容,即地
址“0x0012ff7c”,而addr应该输出地
址“0x0012ff7c”里的内容65。其实这是不行的,因为编
译系统并不知道addr存储的“0x0012ff7c”是地址,所以
将它作为整数,因此编译系统会报错,当然使用addr也
要出错。
但addr里面确实装的是地址,所以可以将这个整数强制
转为地址。addr加上强制转换,应该是“(int)
addr”,它的内容是变量a的地址“0x0012ff7c”。
再对它使用运算符,即(int)addr,输出结果应该是存在这个地址里的变量a的值65。下面的例子验证了如
上分析。
【例1.2】取地址里的整数值和取地址里存放的地址
值。
include
int main
{
int a=65;
int addr;
addr=0x0012ff7c;
printf(0x%p, 0x%p, 0x%p\n, a, addr, (int)addr);
printf(%d,%d,%d\n,a,a, (int)addr);
return 0;
}
输出结果如下:
0x0012FF7C, 0x0012FF7C, 0x0012FF7C
65,65,65
2.引入指针的概念
在【例1.2】中,要使用a的地址直接给addr赋值,必须事先知道这个地址。为了避免这个麻烦,可以直接将地址
表达式“a”赋给变量。即
addr=a;
因为a是地址值,addr是整型变量,所以会给出警告
信息。不过,可以使用强制转换让警告信息“闭嘴”。即
addr=(int)a;
这样一来,使用起来就方便多了。
【例1.3】直接将变量地址赋给另一个变量的例子。
include
int main
{
int a=65;
int addr;
addr=(int)a;
printf(0x%p, 0x%p, 0x%p\n, a, addr, (int)addr);
printf(%d,%d,%d,\n,a,a, (int)addr);
return 0;
}程序运行结果如下:
0x0012FF7C, 0x0012FF7C, 0x0012FF7C
65,65,65,运行结果完全吻合。如果想像对待变量一样对待addr,即使用“”和“”运算符的结果与变量a一样,就必须定
义新的变量类型。分析下述表达式:
addr=(int)a;
(int)addr
(int)addr
由此可见,如果定义一种变量,使它存储的数据类型是
地址,问题就可以迎刃而解了。
要使用(int)addr的addr存储地址,那么就要
用“int”声明“addr”,即
int addr;
这时“addr=a”就无需转换,赋值顺理成章了。“addr”输出地址,则“addr”输出地址里的值。这
就与普通变量的使用方法完全一样了。
暂且将使用“int”定义的变量称为指针变量,下面编
程验证一下这个设想。
【例1.4】使用新的数据类型(指针)的例子。
include
int main
{
int a=65;
int p;
p=a;
printf(0x%p, 0x%p, 0x%p\n, a, p, p);
printf(%d,%d,%d\n,a,a, p);
return 0;
}
输出结果如下:
0x0012FF7C, 0x0012FF78, 0x0012FF7C
65,65,65,输出结果与【例1.3】的完全一样。这种类型称为指针类型,指针类型存储的是地址值。因
为这里使用的地址值是另外一个变量的地址,所以是有效地
址。要明确的是,地址值不一定是有效地址,所以说从指针
的引入开始,也就暗示着它存在着无法预防的错误。
3.引入字符指针再次验证
下面再使用字符来验证一下,看是否与整数的结论相
同。
【例1.5】使用字符的例子。
include
int main
{
char a='B';
int addr;
addr=(int)a;
printf(0x%p, 0x%p, 0x%p\n, a, addr, (char)addr);
printf(%c, %c, %c\n, a,a, (char)addr);
return 0;
}
程序输出结果如下:0x0012FF7C, 0x0012FF7C, 0x0012FF7C
B, B, B
程序验证了“(char)addr”和“(char)
addr”的作用,从而推知,可以定义字符类型的指针。
【例1.6】使用字符指针的例子。
include
int main
{
char c='B';
char p;
p=c;
printf(0x%p, 0x%p, 0x%p\n, c, p, p);
printf(%c, %c, %c\n, c, c, p);
return 0;
}
程序输出结果如下:
0x0012FF7C, 0x0012FF78, 0x0012FF7C
B, B, B
4.声明指针类型的变量
由此可见,声明指针变量是用普通数据类型加“”号。例如:
int p; 整型类型指针
char pc; 字符类型指针
float pf; 浮点类型指针
double pd; 双精度类型指针
struct ps; 结构类型指针
至于“”号的位置,对于整型类型指针p,以下三个位
置均可。
int p; 紧挨着t
int p; 在t与p之间
int p; 紧挨着p
至于哪种写法好,也要根据实际情况,以不造成误会为
准。下面是正确的使用实例。
int a, p, d;
d = 45;
a=d;
p=d;
在上面的声明中,只有a是指针变量,p和d都是整型变
量。使用下面的声明就可以提高可读性。int p, d, a;
系统不管是何种数据类型的指针,一律分配4字节,即
各种类型的指针所占内存的大小是一样的。
【例1.7】演示典型指针长度的例子。
include
struct st{
int a,b;
double f;
} s, ps;
int main
{
double a = 6.8; double pd = a;
float b = 6.5; float pf = b;
char c = 'G'; char pc = c;
int p=NULL;
printf(%d %d %d %d %d\n,sizeof(pd), sizeof(pf), sizeof(pc), sizeof(p), sizeof(ps));
return 0;
}
NULL代表空指针。程序输出结果为:
4 4 4 4 4下面再以整型指针p为例,说明指针的含义。
【例1.8】演示整型指针的例子。
include
int main( )
{
int a=65, p;
p=a;
printf(a的值等于%d, a的首地址是%p, p指向的地址是%p。\n, a, a, p);
printf(通过名字使用%d, 通过p内的地址%p使用%d。\n, a, p, p);
printf(p指向的地址为%p, 存放p的地址是%p。\n, p, p);
return 0;
}
程序输出结果如下:
a的值等于65, a的首地址是0x0012FF7C, p指向的地址是0x0012FF7C。通过名字使用65, 通过p内的地址0x0012FF7C
p指向的地址为0x0012FF7C, 存放p的地址是0x0012FF78。
可以画出变量p和a之间的关系如图1-1所示。图1-1 变量p和a之间的关系示意图
系统为指针变量分配地址0x0012FF78,一般不需要管
它。它存储的地址是变量a的首地址,正是因为这种数据类
型声明的变量代表指向另一个数据类型变量的存储首地址,所以得名为“指针”类型。注意这句话:指向另一个数据类
型变量的存储首地址。
读者有时会对使用如下方法在声明指针的同时初始化指
针的方式感到困惑,即
int p=a;
实际上,选择“intp;”,认为“int”是一种指向
整型的指针类型,用它声明指针变量p,p应该赋予a的地
址,所以应是“p=a”。声明指向整型的指针变量p并同时
初始化,也就顺理成章为“intp=a”。显然,称p为指针
变量(存放的是变量的首地址),而不是称p为指针变量
(p代表指针指向的地址单元所存放的值)。由此可知,p的值是地址,虽然这个地址就是变量a在内
存中的存储首地址,但并不直接说p的值是a的地址,而说成
p指向a的存储首地址,简称p指向a的地址。1.4 指针类型
假设已经知道变量的地址(NULL也算已知),现在将上
一节的构造语法总结如下:
存储类型 数据类型 指针名;指针名=变量地址;
或者采取直接初始化的方法:
存储类型 数据类型 指针名=变量地址;
默认的存储类型为自动存储类型(auto),目前也仅以
自动存储类型为例,以后将通过例子进一步介绍存储类型。
现在假设它们具有如图1-2所示的形式。由关联关系可知,p和a同步变化,即改变任何一个的值,它们的值保持一
致。如果改变p的内容,如使用语句“p=b;”,这使得
p=66,它与b同步,不再与a有任何关系。图1-2 指针操作关系图
【例1.9】说明对p和p进行赋值操作含义的程序。
include
int main ( )
{
int a = 55, b = 66, p = a;
printf ( a:%p,b:%p,p:%p\n, a, b, p );
printf ( %d %d a:%p p:%p\n, a, b, p, p );
a=88;
p=a;
printf ( %d %d a:%p p:%p\n, a, p, p, p );
p=b;
printf ( %d %d b:%p p:%p\n, a, p, p, p );
return 0;
}
程序输出如下:
a:0x0012FF7C,b:0x0012FF78,p:0x0012FF74
55 66 a:0x0012FF7C p:0x0012FF74
88 88 a:0x0012FF7C p:0x0012FF7488 66 b:0x0012FF78 p:0x0012FF74
在【例1.9】中,指针本身的地址不会变化,它反映了
系统需要为指针p分配地址这一概念。正如使用a不要再考虑
a一样,以后也不再考虑p。
【例1.10】下面是一个使用数组b的首地址作为右值的
例子,该程序将数组a的内容复制到数组b中,然后输出两个
数组的内容以便验证。
将数组b的首地d址作为右值的例子
include
void main ( )d
{
char a[]=We are here! Where are you?, b[28], p;
int i=0;
p=b; 数组b的首地址作为右值赋给左值p
while(p[i]=a[i]) 数组a的每个元素值作为右值
i++;
printf(a); printf(\n);
printf(b); printf(\n);
}
程序运行结果如下:
We are here! Where are you?
We are here! Where are you?第2章 指针基础知识
本章将通过实例,解释指针的基本性质以便为用好指针
打下基础。2.1 指针运算符
指针有两种专门的运算符:“”和“”。它们都仅需
要一个操作数,但作用不同。假定已经初始化整型变量a、b
和整型指针变量p。另外,它有一个从“().”简化而来
的“->”运算符,用于存取结构等类型的成员,还可以使用
下标“[]”进行指针操作。
1.&运算符
如前所述,“”仅仅返回这个操作数的地址。而语句
p = &a;
只表示把变量a的地址赋给p,它不改变a的值。假设变
量a的值为56,它存放在内存中的首地址是0x0012FF7C,执
行上述语句后,p的值为0x0012FF7C。
2.运算符“”与“”相反,是返回在这个地址中存储的变量的
值。例如,若p存储了变量a的内存地址,则
b = p;
表示将a的值赋给b,b的值是56,运算符“”理解
为:b接收了在地址p中的值。
可以通过指针间接地存取目标。如上所述,单目运算
符“”将它的操作数作为最终目标的地址来处理,存取的
变量是该地址里的内容。
由此可见,如果b为一个整型变量,在执行语句
p = &a;
之后,下面两条语句
b = p;
b = a;的功能是等价的,都是将p所指向的单元的内容赋给b,第一条语句实际上是对p的间接存取。
3.->运算符
为了书写使用方便,又从“().”运算符演化出“-
>”运算符。
【例2.1】使用结构指针的程序。
struct pencil {
int hardness;
char marker;
int number;
};
include
void main( )
{
struct pencil p[3]; 第9行定义p[3]
struct pencil pen; 第10行定义pen
p[0].hardness=2; p[0].marker='F'; p[0].number=485;
p[1].hardness=0; p[1].marker='G'; p[1].number=38;
p[2].hardness=3; p[2].marker='E'; p[2].number=108;
printf ( Hardness Marker Number\n );
for ( pen=p; pen<=p+2; ++pen )
printf(%4d%8c%8d\n,(pen).hardness, (pen).marker, (pen).number);
}程序输出结果如下:
Hardness Marker Number
2 F 485
0 G 38
3 E 108
已经知道一个结构,例如结构数组pen[0],它的成员
hardness的值可以通过
pen[0].hardness
取得。又知道,指针变量的值是它所指向的数据的地
址,故用结构指针pen来求结构成员,例如hardness的值,可通过语句
(pen).hardness
来取得。这里的圆括号不能省略。因为“.”的运算优
先级高于“”;而在这里首先要求出结构指针所指向的结
构,然后再求这个结构的成员,故必须加圆括号。表达式(pen).hardness
写起来很费事,可以将它表示成如下语句
pen -> hardness
其中->是由负号“-”和大于号“>”组成的。这种表示
方法显得相对简单些。
这样,求结构成员值的一般形式就是:
指向结构变量指针的名字 -> 成员名字
例如在上面程序的for循环中的打印语句参数表
(pen).hardness, (pen).marker, (pen).number);
就可以简化为如下形式:
pen -> hardness, pen -> marker, pen -> number);注意
对结构变量本身进行操作时,必须用“.”运算符。但
若使用结构指针,必须使用箭头运算符。
4.下标运算符
【例2.2】对p使用下标进行操作的程序。
include
int main ( )
{
int a = 36, b = 63, c = 656, i = 0, p = a;
for( i=0; i>-3; i--)
printf ( %4d, p[i]);
printf ( \n );
p=c;
for( i=0; i<3; i++)
printf ( %4d, p[i]);
printf ( \n );
return 0;
}
程序输出结果如下:
36 63 656
656 63 36使用下标时,p是从0开始计数的,即p[0]为起点。从
p[0]可以正数(下标正序增加),也可以反数(下标按负数
递减),即以p[0]为分界点往正负两个方向计数,下面给出
一个例子。
【例2.3】对p使用正负下标进行操作的程序。
include
int main ( )
{
int a = 36, b = 63, c = 656, i=0;
int p = b;
printf ( %d %d %d\n, p[1], p[0], p[-1]);
for( i=1; i>-2; i--)
printf ( %d , p[i]);
printf ( \n );
return 0;
}
程序输出结果如下:
36 63 656
36 63 6562.2 指针移动
指针移动,就是对指针采取++和--操作。VC为整数分
配4字节,所以p+1就是向高地址移动一个整数的长度,即4
字节。字符只分配1字节,p+1就移动1字节。反之,--就代
表地址减少。
1.顺序移动整数类型的指针
下面举例说明指针移动会带来的一些问题。
【例2.4】顺序移动指针的例子。
include
int main
{
int a=88, b=58, c=98, i=0;
int p;
p=a;
printf(0x%p, 0x%p, 0x%p, 0x%p, 0x%p\n,a, b, c, i, p);
for( i=0; i<4; i++, p--)
printf(%d , p);
printf(%p \n, p);
a=0x0012FF6C;
printf(%p\n,(int)a); a=66;
for(p++,i=0; i<4; i++, p++)
printf(%d , p);
printf(%d , p);
printf(%p , p);
c=0x0012FF80;
printf(%d\n,(int)c);
return 0;
}
程序运行输出如下:
0x0012FF7C, 0x0012FF78, 0x0012FF74, 0x0012FF70, 0x0012FF6C
88 58 98 3 0012FF6C
0012FF6C
0 98 58 66 1245120 0012FF80 1245120
变量分配地址以a、b、c、i、p顺序降序分配:
0x0012FF7C~0x0012FF6C。使用p--方式,以降序
0x0012FF7C~0x0012FF74顺序输出“885898”。下一个
0x0012FF70是i的地址,这时的i等于3,所以输出3。再往
下是0x0012FF6C,这时p也指向这个地址,因此p就是
0x0012FF6C。
可以验证一下,将这个地址赋给变量a,再取这个地址的内容,验证结果正确。
现在从i开始往a移动(升序地址),a已经改为66,所
以依次输出
0 98 58 66
这时p出界了,输出是随机数据,这里
是“1245120”。可以将a的地址加4赋给c来验证,这个地
址内的内容也是“1245120”。
由此可见,用一个指针可以到处“跑”,如果用法不正
确,后患无穷。
【例2.5】用顺序移动指针说明危险性的例子。
include
int main
{
int a=55, b=58, c=98, i=0;
int p;
p=(int )0x0012FF78;
printf(0x%p, 0x%p, 0x%p, 0x%p, 0x%p\n,a, b, c, i, p);
printf(%d, %d, %d, %d, %d\n, a, b, c, i, p);
p=(int )0x0012FF76;
printf(0x%p, 0x%p, 0x%p, 0x%p, 0x%p\n,a, b, c, i, p);
printf(%d, %d, %d, %d, %d\n,a, b, c, i, p);
p=12345;
printf(%d, %d, %d, %d, %d\n,a, b, c, i, p);
return 0;
}
程序输出结果如下:
0x0012FF7C, 0x0012FF78, 0x0012FF74, 0x0012FF70, 0x0012FF78
88, 58, 98, 0, 58
0x0012FF7C, 0x0012FF78, 0x0012FF74, 0x0012FF70, 0x0012FF76
88, 58, 98, 0, 3801088
88, 0, 809042018, 0, 13579
使用语句
p=(int )0x0012FF76;
虽然可行,但取出的内容是原来存储的数据,这里已经
不是按分配的地址读取,所以是无意义的数
据“3801088”。不过此时的变量b和c的内容还没有被破坏。
使用语句“p=13579;”则完全破坏了变量b和c的内
容。最后一行的输出验证了这个问题。b变成0,而c变
成“809042018”,这条语句完全破坏了原来的存储内容。
【例2.6】演示使用字符指针存取整数的例子。
include
int main
{
int a=0x12345678, i=0;
char p;
p=(char )0x0012FF7C;
printf(%p, %X\n, a, p);
for( i=0; i<4; i++, p++)
printf(%p, %x\n, p, p);
return 0;
}
程序输出结果如下:
0x0012FF7C, 0x12FF7C
0x0012FF7C, 0x78
0x0012FF7D, 0x56
0x0012FF7E, 0x34
0x0012FF7F, 0x12系统为整型数据分配4字节、字符1字节,用字符指针逐
个读取1字节的内容,可以发现,0x0012FF7C~0x00012FF7F存取0x78~0x12,即指针指向存
储整数的低字节位置,具有连续的4字节。
所以说指针变量存储的是指向变量a的地址值,而不说
是存储变量a的地址。
2.指针基本运算
本节所说的C语言的地址能进行某种运算,即指针可以
与整数相加减。若p为指针,n为整数,则可以使用p+n或p-
n。但这里必须弄清楚,编译程序在具体实现时并不是直接
将n的值加到p上,而是要将n乘上一个“比例因子”,然后
再加上p。这是因为不同类型的数据实际存储所占的单元数
不同,如char类型为1字节,int类型为2字节(VC为4字
节),long和float类型为4字节,double类型为8字节
等,这些数分别为它们的“比例因子”,具体采用哪个作为比例因子,取决于p指向的数据是什么类型。对用户来说,不需要了解编译程序内部的实现,只要将p+n看成将指针p移
动n个数(不必涉及每个数所占的字节数)的位置。下面通
过实例来说明需要注意的几个问题。
【例2.7】演示指针及其运算概念的例子。
include
void main( )
{
int x=56, y=65,p=x; 指针指向x
printf(%u,%u,%u,%u\n,x,y,p,p);
printf(%d,%u,%u,%d\n,x,p,p,p);
p=y; 指针改为指向存放y的首地址
printf(%d,%u,%u,%d\n,y,p,p,p);
p=66; 通过指针改变变量内容
printf(%d,%u,%u,%d\n,y,p,p,p);--p; 对指针进行减1运算,使其指向指针变量的首地址
printf(%d,%u,%u,%d\n,x,p,p,p);
++p; 对指针进行增1运算,使其指向存放y的首地址
printf(%d,%u,%u,%d\n,x,p,p,p);
++p; 对指针进行增1运算,使其指向存放x的首地址
printf(%d,%u,%u,%d\n,x,p,p,p);
++p; 对指针进行增1运算,使其指向程序之外的地址
printf(%d,%u,%u,%d\n,x,p,p,p);--p; 对指针进行减1运算,使其指向存放x的首地址
printf(%d,%u,%u,%d\n,x,p,p,(p-1));
p=(p-1); 通过指针将变量y的值赋给x
printf(%d,%u,%u,%d\n,x,p,p,p);
}上面程序在VC中实现,为了更容易理解,表2-1给出VC
在执行第1条语句之后,为各个变量分配的内存首地址。
为了更容易理解,在程序输出的右方给出输出前的操作
过程及输出序号。
程序输出结果如下:
0x0012FF7C,0x0012FF78,0x0012FF7C,0x0012FF74 (1)
56, 0x0012FF7C,0x0012FF74,56 (2) p=x;
65, 0x0012FF78,0x0012FF74,65 (3) p=y;
66, 0x0012FF78,0x0012FF74,66 (4) p=66;
56, 0x0012FF74,0x0012FF74, 0x0012FF74 (5) --p;
56, 0x0012FF78,0x0012FF74,66 (6) ++p;
56, 0x0012FF7C,0x0012FF74,56 (7) ++p;
56, 0x0012FF80,0x0012FF74, 0x0012FF80 (8) ++p;
56, 0x0012FF7C,0x0012FF74,66 (9) --p;
66, 0x0012FF7C,0x0012FF74,66 (10) p=(p-1);
表2-1 内存分配一览表使用中需要注意如下问题:
1)系统根据变量x和y及指针的声明顺序,为它们分配
一段连续的地址。内存首地址的关系及其所代表的含义如表
2-1所示,第3列0x0012FF80是紧挨x上方的地址(x占4字
节),内容为随机数;其他3个地址分别是第1行变量的存储
首地址。第2列的值分别是第1列变量的值,其中
0x0012FF7C为存储变量x的首地址,即p的值。
2)将p改为指向y,这就改变了p和p的值,p指向y的
地址而p=y。当然p是不会改变的,如第3行输出所示。
3)使用“p=66;”语句也同步改变了y的值,但p的
指向不变,见第4行输出。
4)对p进行--p操作时,因为本程序的y和p顺次存放,所以就使得p指向自己,即第5条输出语句中的p和p均与p
一样,都是输出0x0012FF74。5)当对p进行++p操作时,使指针从指向p变为指向y的
内存存放首地址,p也随之变化并为y的值,操作产生的影
响见第6行的输出。
6)再次对p进行++p操作时,使指针从指向y变为指向x
的内存存放首地址,p也随之变化并为x的值,操作产生的
影响见第7行的输出。
7)如果这时继续执行++p操作,指针指向非程序区的地
址0x0012FF80,其中的内容为随机数。第8行的输出证实了
这一点。此时p所指向的地址虽然唯一,但已经不是所需内
容。如果这个内容很重要,又不慎将它修改,就会造成灾难
性后果。这就是使用指针的危险之处。
8)执行--p使p退回安全区并指向x的地址,这时第9行
的输出就与第2行的一样。
9)对p进行运算,相应的p为p指向地址的内容。也可
以不改变p,将相对p的地址的内容取出,这就是使用(p±n)。程序演示了使用(p-1)输出66,但这并没有改变
p的内容,这可从第10行输出p的结果得到证实。最后一
句使用语句“p=(p-1);”将p指向的内容改变为66,但p仍然指向x。因此要正确区别(p±n)和(p±n)操
作。
可使用下标“[]”描述连续的指针p,这里不再赘述。
3.指针永远指向一个地址
迄今为止都是将指针进行初始化了,在讨论中也是假定
已经将指针初始化了。下面看一个简单的例子。
【例2.8】指针没有赋初值的例子。
include
int main ( )
{
int p;
p=65;
printf ( %d\n, p );
return 0;
}编译给出信息“warning C4700:local
variable'p'used without having been
initialized”。
指针没有指向一个地址,即产生运行时错误。有人认为
改为“p=65;”是正确的,因为这时的输出结果为65。其
实不对,这时输出的是地址,“p=65;”是将一个十进制数
65作为地址,如果将打印语句改为
printf ( %p\n, p );
则输出0x00000041,这就是十六进制地址,41代表十
进制65。这种做法就是将一个无效地址赋给指针,将会产生
灾难性的后果。
由此可见,编译系统给出警告时,首先应该采取有效措
施来消除这个警告。
因为指针变量存放的是地址,所以必须有具体指向。最常见的错误是声明了指针,没有为指针赋值。没有赋值的指
针含有随机地址。可以将上面的赋值语句去掉,直接输出p
以验证这一点。因为指针的破坏性很大,所以尽可能在声明
时同时初始化指针,这种习惯能避免指针的遗漏赋值。由此
可见,不仅要为指针赋一个地址,而且这个地址应是有效地
址。
注意
指针应该指向一个有效的地址。2.3 指针地址的有效性
1.地址的有效性
计算机的内存地址是有一定构成规律的。能被CPU访问
的地址才是有效的地址,除此之外都是无效的地址。
假设有一个指针变量p。可以随便将一个地址赋给p,只
要转换匹配一下即可,p是“来者不拒”,并不“考虑”给
它赋的是什么值,更不“考虑”其后果。声明一个指针,必
须赋给它一个合理的地址值,请看下面的例子。
【例2.9】演示给指针赋有效和无效地址的例子。
include
int main
{
char p,a='A',b='B';
p=a;
printf(0x%p, %c\n, p,p);
p=(char )0x0012FF74;
printf(0x%p, %c\n, p,p);
p=(char )0x0012FF78;
printf(0x%p, %c\n, p,p); p=(char )0x0012FF7C;
printf(0x%p, %c\n, p,p);
p=(char )0x1234;
printf(0x%p\n, p);
printf(%c\n, p);
return 0;
}
编译正确,但运行时出现异常。下面是除最后一条输出
语句之外的输出结果。
0x0012FF78, A
0x0012FF74, B
0x0012FF78, A
0x0012FF7C, |
0x00001234
当指针被赋予字符A的地址时,指针地址不仅有效,且
p具有确定的字符A。当将p改赋地址0x0012FF74时,这个
地址恰恰是系统分给字符B的地址,这个地址不仅有效,且
p具有确定的字符B。有时地址有效,但内容不一定确定,如0x0012FF7C是有效地址,但程序没有使用这个地址,所
以决定不了它的内容,输出字符“|”是无法预知的。地址
0x1234虽然能被指针p接受,也能输出这个地址,但这个地址是无效的,所以执行语句
printf(%c\n, p);
时出错,产生运行时错误。也就是当赋一个无效的地址
给p时,就不能对p进行操作。
结论
使用指针必须对其初始化,并且给指针赋予有效的地
址。
2.指针本身的可变性
编译系统为变量分配的地址是不变的,为指针变量分配
的地址也是如此,但指针变量所存储的地址是可变的。
【例2.10】有如下程序:
include
int main
{
int a=15, b=38, c=35,i=0; 4 int p=a; 5
printf(0x%p,0x%p,0x%p\n, a,b,c); 6
printf(0x%p,0x%p,0x%p,%d\n, p,p, p, p); 7
for(i=0;i<3;i++,p--) 8
printf(%d , p); 9
printf(\n%d,0x%p,0x%p\n, p,p,p); 10
for(i=0,++p;i<3;i++,p++) 11
printf(%d , p); 12
printf(\n%d,0x%p,0x%p\n, p,p,p); 13--p; 14
for(i=0;i<3;i++) 15
printf(%d , (p-i)); 16
printf(\n%d,0x%p,0x%p\n, p,p,p); 17
for(i=0;i<3;i++) 18
printf(%d , (p-2+i)); 19
printf(\n%d,0x%p,0x%p\n, p,p,p); 20
return 0;
}
假设运行后,第6和第7两行给出如下输出信息。
0x0012FF7C,0x0012FF78,0x0012FF74
0x0012FF6C,0x0012FF7C,0x0012FF7C,15
请问能分析出程序后面的输出结果吗?
【解答】因为地址0x0012FF80里存储的值不是由程序
决定的,所以这个输出值不能确定。除此之外,其他的输出
值均可以根据这两行的输出结果,写出确定的输出结果。为了便于分析,首先要清楚所给上述两行输出结果的含
义。
1)从第一行的输出可知,依次是分配给变量a、b和c的
地址。
2)a的地址是0x0012FF7C。注意第2行的输出中,第2
个和第3个的值与它相等。
3)第2行第1个0x0012FF6C对应“p”,是编译系统为
指针分配的地址,用来存放指针p。因为已经给指针变量赋
值(p=a),所以“p”就是输出指针地址0x0012FF6C
里的内容0x0012FF7C。它就是p指向a的地址,即p也输出
0x0012FF7C。也就是说,p、p和a的值相同。
4)“p”就是输出指针p所指向地址0x0012FF7C里所
存储的变量a的值,即15。
要分析输出,需要掌握如下操作含义。1)编译系统为声明的变量a分配存储地址,运行时可以
改变a的数值,但不会改变存储a的地址,即a的地址值不
变。同理,为声明的指针变量p分配一个存储地址,p指向的
地址值可以变化,但p的地址不会变化。
2)可以对指针变量p做加、减操作。由第1行输出结果
知,p=p-1(可记作--p),则p指向的地址是
0x0012FF78,p输出38,再执行p--,则p输出35。如果
再执行p++,则p输出38。这时,对p操作后,不仅p指向的
地址有效,其地址中存储的内容也正确。
3)如果p的操作超出这三个变量的地址,就无法得出输
出结果。
按照上述提示,预测如下。
1)第8~10行中的for语句就是输出三个变量的值
(153835),输出之后,可以预测p指向地址为
0x0012FF70,但不能预测p的内容。在运行过程中p保持为0x0012FF6C。
2)第11~13行中的for语句是反向输出三个变量的值
(353815),输出之后,可以预测p指向地址为
0x0012FF80,但不能预测p的内容(假设它的值为
1245120),当然p仍为0x0012FF6C。
3)第14~17行中的for语句也是输出三个变量的值
(153835),第14行将p调整指向存储a的地址,循环语句
中使用“(p-i)”,因为只是使用p做基准,用i做偏移
量,所以p的值不变,输出之后,p不变,仍为
0x0012FF7C,p=15,p不变。
4)第18~20行中的for语句是反向输出三个变量的值
(353815),循环语句也使用p做基准,即“(p-
2+i)”。输出之后,p不变,仍为0x0012FF7C,p=15,p不变。
由此可见,要小心对p的操作,以免进入程序非使用区或无效地址。如果使用不当,严重时会使系统崩溃,这是使
用指针的难点之一。
程序实际运行结果如下。
0x0012FF7C,0x0012FF78,0x0012FF74
0x0012FF6C,0x0012FF7C,0x0012FF7C,15
15 38 35
3,0x0012FF70,0x0012FF6C
35 38 15
1245120,0x0012FF80,0x0012FF6C 1245120是不可预测的值
15 38 35
15,0x0012FF7C,0x0012FF6C
35 38 15
15,0x0012FF7C,0x0012FF6C
3.没有初始化指针和空指针
【例2.11】没有初始化指针与初始化为空指针的区别。
include
int main
{
int a=456;
int p;
printf(指针没有初始化:\n, p,p);
printf(0x%p,0x%p\n, p,p);
p=NULL;
printf(指针没有初始化为NULL:\n, p,p);
printf(0x%p,0x%p\n, p,p);}
运行结果如下。
指针没有初始化:
0xCCCCCCCC,0x0012FF78指针初始化为NULL:
0x00000000,0x0012FF78
显然,在这两种情况下,不管如何初始化指针,p分配
的地址是一样的,区别是指针变量存放的值。
指针在没有初始化之前,指针变量没有存储有效地址,如果对“p”进行操作就会产生运行时错误。当用NULL初
始化指针时,指针变量存储的内容是0号地址单元,这虽然
是有效的地址,但也不允许使用“p”,因为这是系统地
址,不允许应用程序访问。
为了用好指针,应养成在声明指针时就予以初始化。既
然初始化为NULL也会产生运行时错误,何必要选择这种初始
化方式呢?其实,这是为了为程序提供一种判断依据。例如
申请一块内存块,在使用之前要判断是否申请成功(申请成功才能使用)。
int p=NULL;
p=(int)malloc(100);
if(p==NULL);{
printf(“内存分配错误!\n”);
exit(1); 结束运行
}
注意正确地包含必要的头文件,下面给出一个完整的例
子。
【例2.12】判断空指针的完整例子。
include
include
void main( )
{
char p;
if( (p = (char )malloc(100) ) == NULL) {
printf(内存不够!\n);
exit(1);
}
gets(p);
printf(%s\n, p);
free(p);
}
其实,只要控制住指针的指向,在使用中就可避免出错。
将它与整型变量进行对比,就容易理解指针的使用。整
型变量存储整型类型数据的变量,也就是存储规定范围的整
数。指针变量存储指针,也就是存储表示地址的正整数。由
此可见,一个指针变量的值就是某个内存单元的地址,或称
为某个内存单元的指针。可以说,指针的概念就是地址。
由此可见,通过指针可以对目标对象进行存取(操作
符),故又称指针指向目标对象。指针可以指向各种基本数
据类型的变量,也可以指向各种复杂的导出数据类型的变
量,如指向数组元素等。2.4 指针的初始化
指针初始化,就是保证指针指向一个有效的地址。这有
两层含义,一是保证指针指向一个地址,二是指针指向的地
址是有效的。
1.数值表示地址
为了更深入地理解这一点,首先要记住指针是与地址相
关的。指针变量的取值是地址值,但如何用数值来代表地址
值呢?请看下面的例子。
【例2.13】演示表示地址值的例子。
include
int main
{
int a=256,b=585;
printf (%p,%p\n,a,b);
printf (%d,%d\n,a,b);
printf (%d,%d\n,(int )0x0012FF7C,(int )0x0012FF78);
return 0;
}程序输出结果如下。
0x0012FF7C,0x0012FF78
256,585
256,585
“a”表示变量a的地址,“a”表示存入地址里的
值,也就是变量a的值。既然a代表的是存储变量a的十六进
制地址0x0012FF7C,是否可以使用“0x0012FF7C”呢?
答案是否定的,编译器无法解释“0x0012FF7C”。要
想让“0x0012FF7C”表示地址,必须显式说明,即让它与
指针关联起来,可使用“int”将这个十六进制数字强制转
换成地址。这就是通常说的“地址就是指针”的含义。
由此可见,直接使用数值很麻烦。即使对于验证过的地
址,换一台机器可能就不行了,不具备可移植性,这种赋值
乃是不得已而为之。
2.赋予有效和无效地址【例2.13】不仅演示了如何使用地址值,还演示了有效
地址的概念。该例是在确定变量地址之后才使用它们,所以
是有效的。一般认为,所谓地址有效是指在计算机能够有效
存取的范围内。由此可见,这个有效并不能保证程序正确,指针超出本程序使用的地址范围可能带来不可估计的错误。
为了保证赋予的地址有效,避免像上面例子那样直接使
用强制转换,而是直接使用变量地址赋值。例如:
int a=256,b=585,p=b;
或者使用语句
int a=256,b=585,p;
p=b;
由此可见,对于一个指针,需要赋给它一个地址值。上
面的例子在赋给指针地址时,不是随意的,而是经过挑选
的。如果随便选一个地址,可能是计算机不能使用的地址,也就是无效的地址。【例2.9】演示了无效地址的例子。对于无效的地址,虽然编译没问题,但却产生运行时错误。由此可见,使用指
针的危险性就是赋予它一个无效地址,如果有效地避免了这
一点,就可以运用自如。
3.无效指针和NULL指针
编译器保证由0转换而来的指针不等于任何有效的指
针。常数0经常用符号NULL代替,即定义如下:
define NULL 0
当将0赋值给一个指针变量时,绝对不能使用该指针所
指向的内存中存储的内容。NULL指针并不指向任何对象,但
可以用于赋值或比较运算。除此之外,任何因其他目的而使
用NULL指针都是非法的。因为不同编译器对NULL指针的处
理方式不相同,所以要特别留神,以免造成不可收拾的后
果。如上所述,将指针初始化为NULL,就是用0号地址初始
化指针,而且这个地址不允许程序操作,但可以为编程提供
判别条件。尤其是在申请内存时,假如没有分配到合适的地
址,系统将返回NULL。
C编译程序都提供了内存分配函数,最主要的是malloc
和calloc,它们是标准C语言函数库的一部分,功能都是为
要写的数据在内存中分配一个安全区。一旦找到一个大小合
适的内存空间,就分配给它们,并将这部分内存的地址作为
一个指针返回。malloc和calloc的主要区别是:calloc清
除所分配的内存中的所有字节,即将所有字节置零;
malloc仅分配一块内存,但所有字节的内容仍然是被分配
时所含的随机值。
malloc和calloc所分配的内存空间都可以用free函数
释放。这3个函数的原型在文件stdlib.h中,但很多编译器
又放在头文件malloc.h中,注意查阅手册。在目前所提供的最新C编译程序中,malloc和calloc都
返回一个void型的指针,也就是说,返回的地址值可以假设
为任何合法的数据类型的指针。这个强制转换可以在声明中
进行,如将它们声明为字符型、整型、长整型、双精度或其
他任何类型。
【例2.14】找出程序中的错误并改正。
include
include
int main ( )
{
char p;
p = malloc(200);
gets(p);
printf(p);
free(p);
return 0;
}
malloc所返回的地址并未赋给指针p,而是赋给了指针
p所指的内存位置。这一位置在此情况下也是完全未知的。
下面语句
char p;p = ( char ) malloc(200);
只是将void指针强制转化为char类型的指针,所以它
与上面的等效,都是错误的。如果改为
char p;
p = malloc(200);
的方式,则是可以的,但它也有另外一个更为隐蔽的错
误。如果内存已经用完了,malloc将返回空(NULL)值,这在C语言中是一个无效的指针。正确的程序应该将对指针
的有效性检查加入其中,并及时释放不用的动态内存空间。
下面是一个正确而完整的实例。
include
include
int main ( )
{
char p;
p =(char ) malloc(200);
if(p==NULL) {
printf (内存分配错误!\n);
exit(1);
}
gets(p);
printf(p);
free(p); return 0;
}
在设计C程序时,对指针的初始化有两种方法:使用已
有变量的地址或者为它分配动态存储空间。好的设计方法是
尽可能地早点为指针赋值,以免遗忘造成使用未初始化的指
针。
对于上述程序,如果设置
p =NULL;
则程序执行if语句“{}”里的部分,输出“内存分配错
误!”,然后退出程序。在程序设计中,有时正好利
用“NULL”作为判别条件。下面就是一个典型的例子。
【例2.15】完善下面的程序。
include
char s1[16];
char mycopy(char dest,char src)
{
while(dest++=src++);
return dest;}
void main ( )
{
char s2[16]=how are you?;
mycopy(s1,s2);
printf(s1);
printf(\n);
}
这个程序编译没有错误,但不够完善。这主要是因为
mycopy函数中没有采取措施预防指针为NULL(又称0指针)
的情况。解决的方法很多,下面是简单处理的例子。
char mycopy(char dest,char src)
{
if(dest == NULL || src == NULL)
return dest;
while(dest++=src++);
return dest;
}
由此可见,使用已有变量的地址初始化指针能保证地址
总是有效的。如果使用分配动态存储空间的方法来初始化指
针,确保地址有效的方法是增加判断地址分配是否成功的程
序段。2.5 指针相等
假设有两个指针p1和p2,一定要理解语句
p1=p2;
P1=p1;
的含义。为了说明这个问题,先介绍大端存储和小端存
储的概念。
1.大端存储和小端存储
在CPU内部的地址总线和数据总线是与内存的地址总线
和数据总线连接在一起的。当一个数从内存中向CPU传送
时,有时是以字节为单位,有时又以字(4字节)为单位。
传过来是放在寄存器里(一般是32字节),在寄存器中,一
个字的表示是右边应该属于低位,左边属于高位,如果寄存
器的高位和内存中的高地址相对应,低位和内存的低地址相
对应,这就属于小端存储。反之则称为大端存储。大部分处理器都是小端存储的。
因为十六进制的2位正好是1字节,所以选十六进制
0x0A0B0C0D为例,如图2-1所示,对小端存储,低位是
0x0D,应存入低位地址,所以存入的顺序是
0x0D 0x0C 0x0B 0x0A
反之,对于大端存储则为
0x0A 0x0B 0x0C 0x0D图2-1 图解大端和小端存储
下面利用union的成员共有地址的性质,用一个程序来
具体说明小端存储。
【例2.16】演示小端存储的程序。
include
union s{
int a;
char s1[4];
}uc;
int main( )
{ int i=0;
uc.a=0x12345678;
printf(0x%x\n,uc);
for(i=0;i<4;i++)
printf(0x%x 0x%x\n,uc.s1[i],uc.s1[i]);
return 0;
}
声明十六进制整数a,它与字符串数组共有地址,a的最
低字节是0x0D,按小端存储,则应存入“uc.s1[0]”中,也就是0x4227A8中,最高位地址0x4227AB则应存入0x0A,也就是数据的高位。下面的运行结果证明了这一点。其实,可以在调试环境中直接看到这些结果。
0x4237A8
0x4237A8 0xD
0x4237A9 0xC
0x4237AA 0xB
0x4237AB 0xA
其实,【例2.6】的程序也说明了这个问题。
2.指针相等操作
两个指针变量相等,是指它们指向同一个地址。例如p1=p2;
不仅使得p1和p2都指向原来p1指向的地址,而且保证
p2=p1。注意它们的值是原来p1的值。也就是说,p2放
弃自己原来的指向地址及指向地址里存储的值。而语句
p1=p2;
的作用是使p1放弃自己原来的指向地址里存储的值,但
并没有放弃自己的指向地址。p1和p2仍然保留各自原来的指
向。至于p1里面的值,则要视具体情况而定,不能由此得
出p1指向地址里的值与p2相等。关于这一点,只要用两个
不同结果的例子就可以说明这一点。
【例2.17】演示整数指针相等操作的程序。
include
int main( )
{
int p1, p2;
int s1=0x12345678,s2=0x78;
p1=s1;p2=s2;
printf(0x%x\t0x%x\n,p1,p2);
p2=p1; printf(0x%x\t0x%x\n,p2,p1); 值相等
printf(0x%x\t0x%x\n,p1,p2); 地址不变
p2=p1;
printf(0x%x\t0x%x\n,p1,p2); 值相等
printf(0x%x\t0x%x\n,p1,p2); 地址改变
return 0;
}
这个例子的语句“p2=p1;”使用p1取代p2,但p2
不变。运行结果证明了这一点。
0x12ff74 0x12ff70
0x12345678 0x12345678
0x12ff74 0x12ff70
0x12345678 0x12345678
0x12ff74 0x12ff74
【例2.18】演示字符指针相等操作的程序。
include
int main( )
{
char p1, p2;
char s1[16]=987654321,s2[16]=G;
p1=s1;p2=s2;
printf(0x%x\t0x%x\n,p1,p2);
p2=p1;
printf(%s\t%s\n,p2,p1); 值不相等
printf(0x%x\t0x%x\n,p1,p2); 地址不变
p2=p1;
printf(%s\t%s\n,p2,p1); 值相等
printf(0x%x\t0x%x\n,p1,p2); 地址改变 return 0;
}
这个例子的语句“p2=p1;”并不使用p1取代p2,只是使用字符“1”取代原来的字符“G”,运行结果如下:
0x12ff68 0x12ff58
1 987654321
0x12ff68 0x12ff58
987654321 987654321
0x12ff68 0x12ff68
这是字符操作特征引起的,所以不能只看表面现象。以
后还会做进一步的分析。2.6 对指针使用const限定符
可以用const限定符强制改变访问权限。用const正确
地设计软件可以大大减少调试时间和不良的副作用,使程序
易于修改和调试。
1.指向常量的指针
如果想让指针指向常量,就要声明一个指向常量的指
针,声明的方式是在非常量指针声明前面使用const,例
如:
const int p; 声明指向常量的指针
因为目的是用它指向一个常量,而常量是不能修改的,即p是常量,不能将p作为左值进行操作,这其实是限定
了“p=”的操作,所以称为指向常量的指针。当然,这并
不影响p既可作为左值,也可作为右值,因此可以改变常量指针指向的常量。下面是在定义时即初始化的例子。
const int y=66; 常量y不能作为左值
const int p=y; 因为 y是常量,所以p不能作为左值
指向常量的指针p指向常量y,p和y都不能作为左值,但可以作为右值。
如果使用一个整型指针p1指向常量y,则编译系统就要
给出警告信息。这时可以使用强制类型转换。例如:
const int y=66; 常量y不能作为左值
int p1; p1既可以作为左值,也可以作为右值
p1=(int )y; 因为 y是常量,p1不是常量指针,所以要将y进行强制转换
如果在声明p1时用常量初始化指针,也要进行转换。例
如:
int p1=(int )y; 因为 y是常量,p1不是常量指针,所以要将y进行强制转换
在使用时,对于常量,要注意使用指向常量的指针。2.指向常量的指针指向非常量
因为指向常量的指针可以先声明,后初始化,所以也会
出现在使用时将它指向了非常量的情况。所以这里用一个例
子演示一下如果指向常量的指针p指向普通变量,则会出现
什么结果。
【例2.19】使用指向常量的指针和非常量指针的例子。
include
int main( )
{
int x=55; 变量x能作为左值和右值
const int y=88; 常量y不能作为左值,但可以作为右值
const int p; 声明指向常量的指针
int p1; 声明指针
p=y; 用常量初始化指向常量的指针,p不能作为左值
printf(%d ,p);
p=x; p作为左值,使常量指针改为指向变量x,p不能作为左值
printf(%d ,p);
x=128; 用x作为左值间接改变p的值,使p=x=128
printf(%d ,p);
p1=(int )y; 非常量指针指向常量需要强制转换
printf(%d\n,p1);
return 0;
}
运行结果如下。88 55 128 88
使用指向常量的指针指向变量时,虽然p不能作为左
值,但可以使用“x=”改变x的值,x改变则也改变了p的
值,也就相当于将p间接作为左值。所以说,“const”仅
是限制直接使用p作为左值,但可以间接使用p作为左值,而p仍然可以作为右值使用。
与使用非常量指针一样,也可以使用运算符“”改变
常量指针的指向,这当然也同时改变了p的值。
必须使用指向常量的指针指向常量,否则就要进行强制
转换。当然也要避免使用指向常量的指针指向非常量,以免
产生操作限制,除非是有意为之。
以上结论可以从程序的运行结果中得到验证。
3.常量指针
将const限定符放在号的右边,就使指针本身成为一个const指针。因为这个指针本身是常量,所以编译器要求
给它一个初始化值,即要求在声明的同时必须初始化指针,这个值在指针的整个生存期中都不会改变。编译器
将“p”看作常量地址,所以不能作为左值(即“p=”不成
立)。也就是说,不能改变p的指向,但p可以作为左值。
【例2.20】使用常量指针的例子。
include
int main( )
{
int x = 45, y = 55, p1; 变量x和y均能作为左值和右值
int const sum =100; 常量sum只能作为右值
int const p=x; 声明常量指针并使用变量初始化
int const p2 = (int )sum; 使用常量初始化常量指针,需要强制转换
printf(%d %d ,p,p2);
x = y; 通过左值x间接改变p的值,使p=55
printf(%d ,p);
p = sum; 直接用p作为左值,使p=100
printf(%d ,p);
p2 = p2 + sum + p; p2作为左值,使p2=300
printf(%d ,p2);
p1 = p; p作为右值,使指针p1与常量指针p的指向相同
printf(%d\n,p1);
return 0;
}
运行结果如下。45 100 55 100 300 100
语句“x=y;”和“p=sum;”都可以改变x的值,但p
指向的地址不能改变。
显然,常量指针是指这个指针p是常量,既然p是常量,当然p不能作为左值,所以定义时必须同时用变量对它进行
初始化。对常量而言,需使用指向常量的指针指向它,含义
是这个指针指向的是不能作为左值的常量。不要使用常量指
针指向常量,否则就需要进行强制转换。
4.指向常量的常量指针
也可以声明指针和指向的对象都不能改动的“指向常量
的常量指针”,这时也必须初始化指针。例如:
int x = 2;
const int const p= x;
告诉编译器,p和p都是常量,都不能作为左值。这种指针限制了“”和“”运算符,所以在实际应用中很少用
到这种特殊指针。
5.void指针
在一般情况下,指针的值只能赋给相同类型的指针。
void类型不能声明变量,但可以声明void类型的指针,而
且void指针可以指向任何类型的变量。
【例2.21】演示void指针的例子。
include
int main( )
{
int x=256, y=386, p=x;
void vp = x; void指针指向x
printf(%d,%d,%d\n,vp, p, x);
vp = y; void指针改为指向y
p = (int )vp; 强制将void指针赋值给整型指针
printf(%d,%d,%d\n,vp, p, p);
return 0;
}
虽然void指针指向整型变量对象x,但不能使用vp引
用整型对象的值。要引用这个值,必须强制将void指针赋值给与值相对应的整型指针类型。程序输出如下。
0x0012FF7C,0x0012FF7C,256
0x0012FF78,0x0012FF78,3862.7 使用动态内存
可以自己申请一块内存,且这块内存也由自己释放,这
样分配的内存称为动态内存。
如何使用这种动态内存,也就是如何定位这些动态内存
的地址呢?我们知道,与地址有关的变量是指针变量,所以
可以使用指针对动态内存进行操作。2.7.1 动态内存分配函数
如2.4节所述,C编译程序提供的最主要的内存分配函数
是malloc和calloc,能为要写的数据在内存中分
配一个安全区。前面讨论了这两个函数的区别,这里不再赘
述。
由这两个函数所分配的内存空间都可以用free函数
释放出来。这三个函数的原型在文件stdlib.h中,但很多
编译器又把它放在头文件malloc.h中,注意查阅手册。
如使用如下程序
char p;
p=( char ) malloc (50);
分配50字节的内存。因为指针p指向动态内存的首地
址,所以可以通过它存储变量的值。【例2.22】编写用指针申请一块内存来存储输入的一个
字符串,输出该字符串长度并释放该存储空间的程序。
include
include
void main ( )
{
char s;
int n;
s=(char ) malloc(50);
scanf ( %s,s );
for ( n=0; s!='\0'; ++s )
++n;
s=s-n;
free(s);
printf ( %d, n);
}
运行示范如下:
fedcba987654321
15
下划线标注的是由键盘输入的字符,以后均沿用这种方
式,不再说明。
有时不知道所用系统为int数据分配的字节数,则可以使用sizeof函数计算。下面是分配50个整型量的空间,并
使用sizeof函数来确定整型量所需的字节长度的例子。
int p;
p=( int ) malloc ( 50sizeof ( int ) );
字符串是作为一个整体存入动态内存的,如果申请一块
内存存放数值,因为申请的动态内存是连续的,所以只需要
移动指针,使它每次指向要存储的内存的首地址即可。
【例2.23】编写一个用指针申请一块内存来存储输入的
两个整数值,输出其值并释放该存储空间的程序。
include
include
void main ( )
{
int i, p;
p=(int ) malloc(8);
for ( i=0; i<2; ++i,++p )
scanf ( %d,p );
p=p-2;
for ( i=0; i<2; ++i,++p )
printf ( %d ,p );
printf (\n);
p=p-2;
free(p);
}运行示范如下:
128 256
128 256
循环语句使用“++p”,是为了让指针指向存储下一个
整数的首地址,而程序中的语句“p=p-2;”是使指针返回
开始的起点。
释放内存的语句“free(p);”中的参数p,必须是
申请到的动态内存的首地址。2.7.2 内存分配实例
下面列举几个例子说明如何为变量分配动态内存地址。
【例2.24】使用两个指针进行相减操作求串长度。
C语言允许两个指针进行相减的操作。如果p和q指向同
一数组的成员,p-q实际上就表示p和q之间的元素个数。例
如希望求得一字符串的长度,若得到字符串的首指针及字符
串尾(\0)的指针,那么这两个指针的差就是长度,其实现
如下:
include
include
void main ( )
{
char p, q;
p=q=(char)malloc(100);
scanf ( %s, q );
while ( p != '\0' )
p++;
printf ( %d\n, p-q );
free(q);
}运行示范如下:
nice!
5
但两个指针相加在C语言中是不允许的。从实际意义来
看,两个地址相加是没有意义的,所以C语言不允许p+q的运
算。同样,也不允许乘、除、移位或屏蔽运算,不允许
float或double数与指针量相加。
C语言中允许两个指针进行比较运算,对两个指向同一
数据类型的指针变量可以进行<、>=、==、!=等比较运
算。例如,表达式p
下面的【例2.25】就是演示它们的使用方法。由这几个
例题可见,要为使用的指针分配地址。在使用free时要注
意,指向同一个目标的指针只能释放一次。因为释放后,另
一个已经是空指针,再使用free就要出错。
【例2.25】编写一个程序读进10个float值,用指针将它们存放在一个程序块里,然后输出这些值的和及最小值。
具体要求如下:
1)申请数据内存块并判别是否申请成功。
2)用键盘输入10个数据的内容。
3)在for循环中,使用指针运算完成循环及计算。
【参考程序】编写一个程序读进10个float值,用指针
将它们存放在一个程序块里,然后输出这些值的和及最小
值。
include
include
include
const int SIZE=10;
void main( )
{
float pf, temppf, min, temp,sum=0.0;
if((min = temppf = pf = (float )malloc(SIZEsizeof(float))) == NULL)
printf(内存分配错误\n);
else {
for( ; temppf-pf< SIZE; temppf++){
scanf(%10f, temp);
temppf=temp;
}
for( temppf--; temppf >= pf; temppf--) { min = (min > temppf) ? temppf:min;
sum += temppf; }
}
printf(和是%f, 最小值是%f\n, sum, min);
free(pf);
}
运行示范如下:
-2.5 2.5 5.8 -5.8 -386.5 2.34 -5.34 3.6 -6.6 200和是-192.500000, 最小值是-386.500000
在使用动态内存分配时,还要判别是否分配成功。要保
证返回的值不为零。语句
if((min = temppf = pf = (float )malloc(SIZEsizeof(float))) == NULL);
就是判断内存分配是否成功。因为NULL的定义在
stdio.h中,故除了要包含文件stdlib.h之外,还要包含
stdio.h。
其实,分配的连续内存可以构成一个一维数组,详细的
用途将在后续章节中介绍。2.7.3 NULL指针
假设在内存分配语句
if ( (p=(char)malloc ( 100) == NULL );
中使用了空指针。空指针的表示为
p=NULL;
有时在赋值或比较运算的情况下会使用NULL指针,但在
其他情况下不能使用NULL指针。因为NULL指针并不指向任
何对象,而且空指针也不是空字符串,所以对空指针p而
言,使用如下两个语句会得到什么结果呢?
printf(%s\n, p);
printf(p);
为了代码的文档化,常采取如下定义:define NULL 0
由此可见,p的行为没有定义,这两条语句在不同的机
器上可能有不同的效果。
在禁止读取内存0地址的机器上,语句
printf(%d\n, p);
将会执行失败。在允许的机器上,则会以十进制方式输
出内存位置0中存放的字符内容。
要注意的是,空指针并不是空字符串。无论使用0还是
NULL,效果都是相同的。当将0赋值给一个指针变量时,绝
对不能企图使用该指针所指向的内存中存储的内容。
有些C语言实现对内存位置0只允许读,不允许写。在这
种情况下,NULL指针指向的也是垃圾信息,所以也不能错用
NULL指针。由此可见,对指针进行递增和递减操作必须预防越界。
在达到最后一个边界时,要特别小心谨慎。释放不用的内存
时,必须保证指针指向所申请内存的首地址,否则就会出
错。在某些场合,为了保证释放,甚至需要多申请部分内存
区域。第3章 一维数组
在C语言中,数组是一个非常重要的概念,一定要熟练
掌握数组的用法,尤其是深刻理解数组的边界不对称并避免
数组越界错误。3.1 一维数值数组
利用基本数据类型能构造出相应数据类型的数组,所以
说数组是一种构造型数据类型。本节重点以一维整数数组为
例,讨论它们的特征,然后再推广到其他一维数组。
1.一维数值数组的特征
假如要表示5个连续整数变量1~5,则需要5个整数变量
名称,如用X1~X5表示。这批变量的特点是它们的基本数据
类型一样。现在构造一个新的数据类型,假设它的名称为
A,在符号“[]”内用序号表示为A[0]~A[4],它们的对应
关系如图3-1所示。图3-1 数组构成示意图
后一种表示有很大的进步。X1~X5之间没有内在关系,而A[0]~A[4]之间是通过“[]”内的序号0~4(也就是下
标)构成了唯一的连续对应关系。如果将A[0]~A[4]看作连
续的房间,则可以改变房间里所存放整数的值。暂且将A称
作整数房间。也就是房间里必须都存放整型变量,这样构造
出来的数据类型称为整型数组。
假定使用语法定义如下:
int A[5]={1,2,3,4,5};这条语句的含义是整型数组A的下标从0开始,A[4]的
值为5,A[5]本身不是数组的元素。这个数字5代表数组A共
有5个元素A[0]~A[4]。将若干个同类型变量线性地组合起
来,就构成一维数组。在使用一维数组之前首先必须声明
它,这包括数组的类型、数组名和数组元素的个数。
声明一维数组的一般方式如下:
datatype array_name[n];
其中,datatype是数据类型,可以是基本数据类型,也可以是构造类型。array_name为数组名(标识符),n为
数组中所包含的数组元素的个数。数组与各种基本数据类型
的不同之处是:数组须由datatype、数组标志“[]”及长
度n三者综合描述,它是在基本数据类型基础上导出的一种
数据类型。例如:
int a[10]; 定义整型数组具有10个元素,每个元素是一个整数变量
float b[6]; 定义浮点数组具有6个元素,每个元素是一个实数变量
char c[25]; 定义字符数组具有25个元素,每个元素是一个字符变量数组中各元素总是从0开始连续编号,直到n-1为止。所
以上述定义的数组a、b、c的数组元素分别为
a[0]、a[1]、a[2]、…、a[9]
b[0]、b[1]、b[2]、b[3]、b[4] 、b[5]
c[0]、c[1]、c[2]、…、c[24]
对数组中的任何元素都可单独表示并对它进行访问。假
设a是整型数组,则语句
printf(%d,a[5]);
输出a[5]的值。C语言中规定只能对数组的元素操作,而不能对整个数组操作,即语句
printf(%d,a);
是不允许的,必须使用for语句依次遍历整个数组元
素。例如遍历数组a:
for(int i=0; i<10; i++)
printf(%d\n,a[i]);数组下标可以是变量,这个变量称为下标变量。下标变
量的值原则是从0开始到n-1的整数,但是在C语言中对数组
的下标变量的值是不进行合法性检查的,所以允许数组的下
标越界,对此程序员必须引起注意并避免产生错误。具有丰
富程序设计经验的程序员有时会巧妙地利用数组的下标越界
来进行程序设计。
【例3.1】给出求斐波那契数列中前20个元素值的问
题。所谓斐波那契数列,就是可以将它表示成F0、F1、F2、…,其中除F0和F1以外,其他元素的值都是它们前两个元素
值之和,即Fn=Fn-2+Fn-1,而F0、F1分别为0和1。为此,声
明整数数组fibonacci[20]来依次存放斐波那契数列的前20
个元素值。
include
void main( )
{
int n, fibonacci[20]={0,1}; 初始化
printf(%-5d%-5d,fibonacci[0],fibonacci[1]); 左端对齐
for (n=2; n<20; n++ )
{ 计算后18个元素值
fibonacci[n]=fibonacci[n-2]+fibonacci[n-1];
分4行打印,按每行5个数打印输出,左对齐 if(n%5==0) printf(\n);
printf(%-5d,fibonacci[n]);
}
printf(\n);
}
程序输出结果如下:
0 1 1 2 3
5 8 13 21 34
55 89 144 233 377
610 987 1597 2584 4181
实际上,斐波那契数列在数学和计算机算法研究领域有
许多用途。斐波那契数列起源于“兔子问题”:开始有一对
兔子,假设每对兔子每个月生下一对新的兔子,而每一对新
生下来的兔子在出生后的第2个月月底开始繁殖后代,而且
这些兔子永远不死,那么在1年之后一共会有多少对兔子?
这一问题的答案建立在这样一个事实上,即在第n个月结束
时,有总数为Fn+2的兔子。所以,根据程序输出结果,在第
12个月结束时将一共有377对兔子。
数组的元素可像一般变量那样进行赋值、比较和运算。如同简单变量一样,在说明数组之后就能对它的元素赋值,方法是从数组的第1个元素开始依次给出初始值表,表中各
值之间用逗号分开,并用一对花括号将它们括起来。例如:
int A[]= {1,2,3,4,5};
A数组元素的个数由编译程序根据初始值的个数来确
定。如果数组很大,又不需要对整个数组进行初始化,数组
初始值表可以只对前几个元素置初值,但这时必须指出数组
的长度。例如,在【例3.1】中存放斐波那契数列的数组中
前两个元素是已知的,而后面18个元素是要通过计算产生
的,所以对此可初始化为:
int fibonacci[20] = {0,1};
这样数组中的前两个元素分别为0和1,而后18个元素皆
为0。
如果没有明确进行初始化,则编译程序将外部型和静态型的数组初始化为0,而自动型数组的值不定。如不给自动
数组置初值,程序中又没有使用它,编译系统会给出警告信
息。
C语言允许自动型数组有初始值,它与外部型和静态型
数组初始值都包含在定义语句的花括号里,每个初始值用逗
号分隔。例如:
int d[5]={0,1,2,3,4};
如果定义时只对数组元素的前几个初始化,则其他元素
均被初始化为0;若未进行初始化,则C编译程序使用如下规
则对其进行初始化:
1)外部型和静态型数组元素的初始值为0;
2)自动型和寄存器型数组元素的初始值为随机数。
2.数组元素的地址每个数组元素都有自己的地址,并且是按下标序号顺序
排列的。
数组名就是数组的首地址,也就是第1个元素的地址。
假设有整型数组a[3],则第1个元素的地址有两种表示方
法,即a和a[0]。VC为整数分配4字节,下一个元素a[1]
的首地址与a[0]相差4字节。由指针的知识可知,a+1代表
将a的地址移动一个元素的长度,即移动4字节。a+1就是
a[1]的首地址。由此类推,可以写出如下程序输出它们各
个元素存储的首地址。
【例3.2】演示数组元素的地址。
include
void main
{
int i =0, a[3]={2, 4, 6};
printf(%p %p %p\n,a, a, a[0]); 3种表示方法等效
printf(%p\n, i); 变量i的首地址
for(i=0; i<3; i++) 与a + i表示方法等效
printf(%p ,a[i]);
printf(\n);
for(i=0; i<3; i++) 与a[i]表示方法等效
printf(%p , a + i);
printf(\n); for(i=0; i<3; i++) a[i]+i 表示的含义不同
printf(%p , a + i);
printf(\n);
}
程序运行结果如下:
0x0012FF70 0x0012FF70 0x0012FF70
0x0012FF7C
0x0012FF70 0x0012FF74 0x0012FF78
0x0012FF70 0x0012FF74 0x0012FF78
0x0012FF70 0x0012FF7C 0x0012FF88
在编程中,这三种表示方法都会用到。但在使用时要注
意,虽然a也是数组的首地址,但a+i的含义与a+i的并不
一样,所以不能使用a+i方式输出其他元素的地址。上面的
程序中使用语句
for(i=0; i<3; i++)
printf(%p , a + i);
输出地址,因为a+0就是a,所以第1个地址是对的。
但a+1则不是a[1]的地址,编译系统会在数组a的最后一
个地址+1,即在数组a的第3个元素的首地址0x0012FF78处移动4字节,即存储变量i的首地址0x0012FF7C。同理,a+2应从i的尾部0x0012FF80开始移动8字节,输出
0x0012FF88。即最后这段程序的输出为
0x0012FF70 0x0012FF7C 0x0012FF88
有人说可以使用(a+i),那也是错误的,且在编译时
就会出现错误。
3.存取数组元素的值
a是数组第1个元素存储的首地址,则a就是这个地址存
储的值。同理,数组第i个元素的值可以用下标表示为
a[i],也可以表示为(a+i)。下面的程序演示了两者的
对应关系。
【例3.3】演示数组元素的存取。
include
void main
{
int i =0, a[3], b[3]; for(i=0; i<3; i++)
{
a[i] = i + 50;
(b+i) = i + 50;
}
printf(%d %d\n, a[0], a); 两种表示方法等效
for(i=0; i<3; i++)
printf(%d , a[i]);
printf(\n);
for(i=0; i<3; i++)
printf(%d , (b + i)); 与a[i]等效
printf(\n);
}
程序运行结果如下:
50 50
50 51 52
50 51 52
运行结果也证实了上述结论。这两种方法都要熟练掌
握。3.2 一维字符串数组
字符数组是数组的一个特例。它除了具有一般数组的性
质之外,还具有自己的一些特点。它存储的是一串字符序
列,其中还包括转义字符序列。在字符串的最后位置上存放
一个标记串结束的字符(ASCII字符的0),即空字符。用
转义字符'\0'表示。字符串数组以'\0'结束,它的长度应
是存储字符长度加1。现在分析
int a[3];
char s[3];
这两个定义的区别。字符数组与整型数组的主要不同之
处有如下几点。
1)整型数组a的各个单元的数值长度是可以不相同的,如a[0]=123,a[2]=12345,a[0]是3位数字,a[2]是5位
数字。而字符数组s的各个单元都只能放一个字符,如s[0]='w',s[1]='e',w和e都是一个字符。
2)它们的有效长度不同,a[3]代表有3个数组元素,没有a[3]。而字符数组s中的s[2]='\0',存放的是字符串
结束标志,实际可用的有效上界是s[2],即只能放2个字
符。它的第s[2]个单元是定义的有效上界,当然也没有
s[3]。
3)字符数组的初始化格式比较特殊,可以用字符串来
代替,如:
char str[3] = we; 编译程序将自动加上结束标记'\0'
也可以同整型数组一样初始化。如:
char str[3] = { 'w', 'e', '\0'}; 必须手工以'\0'结束
上述两种方式置初值是等价的。在对字符数组初始化
时,为使程序能觉察到此数组的末尾,在内部表示中,编译
程序要用'\0'来结束这个数组,从而存储的长度比双引号之间的字符的个数多一个。在上面的具体例子中,we字符串
中的字符数是2,而str的长度却是3。
4)与数值数组一样,字符串数组的名字就是字符串的
首地址。不同的是,字符串是顺序存放并以'\0'作为结束标
志。所以字符串既可以按顺序输出,也可以用名字整体输
出。在读入字符串时,单个字符串中不能有空格。
【例3.4】演示字符串数组的例子。
include
void main
{
char s[]=abcd; 定义字符串
int i=4;
printf(%s\n,s); 整体输出内容
for(i=0;i<4; i++) 顺序输出
printf(%c ,s[i]);
printf(\n);
}
字符串顺序存放,字符串名是其首地址。s输出的不是
地址,而是地址里的内容。程序输出结果如下。
abcda b c d
需要注意的是,输出字符串首地址格式可使用“%p”以
十六进制输出,也可以“%u”或者“%d”以十进制格式输
出,但不能使用
printf(%s\n,s);
语句,这个语句仍然是输出字符串的内容。
【例3.5】演示使用不同格式输出字符串数组首地址的
例子。
include
int main
{
char s[]=fish;
printf(%p %p\n,s, s);
printf(%10u %u\n,s, s);
printf(%10d %d\n,s, s);
printf(%10s %s\n,s, s);
return 0;
}
输出结果如下:0x0012FF78 0x0012FF78
1245048 1245048
1245048 1245048
fish fish3.3 使用一维数组容易出现的错误
使用数组最容易犯的错误是数组越界和初始化错误。本
节的分析仅局限于一维数组。3.3.1 一维数组越界错误
【例3.6】使用数组下标越界的例子。
include
int main
{
int i, a[5];
for(i=1;i<=5;i++)
a[i]=i;
return 0;
}
在上述程序中,循环语句
for(i=1;i<=5;i++)
a[i]=i;
有如下两个错误。
1)没有给数组a的第一个元素a[0]赋初值。
2)超出了数组的尾端。一个长度为5的数组,其元素下
标为0~4,即a[4]是最后一个元素。这种错误会造成程序运行时的时断时续的错误。
正确写法应该是:
for ( i=0; i<5; ++i)
a[i]=i;
因为字符数组的最后一个结束标志位是'\0',字符数组
c[5]只能存放4个字符,所以下面的语句
char c[5]=abcde;
也产生数组越界错误。正确的写法是只能有4个字符。
即
char c[5] = abcd;
下面通过讨论C语言的这个特点,以帮助读者杜绝这个
错误。
1.数值数组的边界不对称性记住C语言数值数组采取的是不对称边界,即C语言中一
个具有n个元素的数值数组,它的元素下标是从0到n-1,即
从0开始,没有下标为n的元素,有效元素是n个。
这种不对称边界反而有利于编程。假设定义5个元素的
数组a[5],虽然
for ( i=0; i<=4; ++i)
a[i]=i;
的方法也是正确的,但考虑到0是第1个元素,元素数量
是5,且下标5是不含在下标范围内的,所以推荐使用
for ( i=0; i<5; ++i)
a[i]=i;
的方式,有效的元素数量n=5-0=5。如果定义数组是从
1开始的,显然要包含下标5,元素数量n=5-1+1=5。由于使
用了0,所以就避免了+1的运算。这就是它的优点。
虽然数组没有a[n]这个元素,但是却可以引用这个元素的地址a[n],而且ANSI C标准也明确允许这种用法:数组
中实际不存在的“溢界”元素的地址位于数组所占内存之
后。这个地址可以用来进行赋值和比较,但因为不存在
a[n],所以引用该元素的值就是非法引用。
2.字符数组的边界不对称性
字符数组更为特殊,它的第n-1个元素是法定的'\0',能存储的有效字符为n-1个。
【例3.7】下面的程序对吗?
include
int main
{
int i;
char a[]=abcde,b[6];
for(i=0;i<5;i++)
b[i]=a[i];
printf(b);
printf(\n);
return 0;
}
这个程序是错的。程序的错误是只复制5个元素。语句char a[]=abcde
定义的字符数组是a[6],它具有6个元素,只是第6个
元素是结束符'\0'。这个结束符必须复制到字符数组b,不
然它没有结束符,造成语句
printf(b);
除了输出“abcde”之外,还将其后的字符输出(如果
不是字符代码,则输出乱码),直到遇到空格才能结束。应
将for语句改为
for(i=0;i<6;i++)
可以利用这个结束位编程,下面是一个例子。
【例3.8】利用结束位编程的例子。
include
int main
{
int i=0; char a[ ] = How are you?, b[13];
while (a[i]!='\0')
{b[i]=a[i];++i;}
b[i]='\0';
i=-1;
while (i++,b[i]!='\0')
printf(%c ,b[i]);
printf(\n);
return 0;
}
第1个while语句复制时,因为没有复制结束位,所以
要补一个结束位。因为第2个while语句的循环要用
到“i++”,所以将i的初始值设为-1,输出以结束位为结
束条件。3.3.2 一维数组初始化错误
【例3.9】混淆定义与初始化的例子。
include
int main
{
int i, a[5];
char ch[5];
a[5]={1, 2, 3, 4, 5};
ch[5]=good!;
for(i=0; i<5; i++)
printf(%d ,a[i]);
printf(\n);
printf(ch);
printf(\n);
return 0;
}
上面语句都是在定义数组以后为其赋值。如果定义以后
再赋值,则需要一个一个地赋值。字符数组ch的定义不对,ch的长度不够,将产生数组越界错误。程序其实是用初始化
的方法为元素赋值,本例混淆了定义与初始化。现将它们都
改为在定义时初始化,即
int a[5]={ 1, 2, 3, 4, 5};char ch[]=good!;
这里由编译系统识别ch的下标。如果要直接使用下标,应该定义为
char ch[6]=good!;
修改后的程序如下。
include
int main
{
int i, a[5] = { 1, 2, 3, 4, 5};
char ch[ ] = good!;
for(i=0; i<5; i++)
printf(%d , a[i]);
printf(\n);
printf(ch);
printf(\n);
return 0;
}
运行结果如下。
1 2 3 4 5
good!3.3.3 数组赋值错误
本节指出的错误不涉及指针(涉及指针类的错误将放在
数组与指针关系中讨论),为了熟悉一维数组,本节仍然不
讨论多维数组。
【例3.10】下面的程序错误地使用了赋值运算符,请改
正。
include
int main( )
{
int i, a[5]={ 1, 2, 3, 4, 5},b[5];
char ch[]=good!,st[6];
b=a;
st=ch;
for(i=0;i<5;i++)
printf(%d ,b[i]);
printf(\n);
printf(st);
printf(\n);
return 0;
}
数组没有赋值运算符“=”,必须一个元素一个元素地赋值。
正确的程序如下:
include
int main
{
int i, a[5]={ 1, 2, 3, 4, 5},b[5];
char ch[]=good!,st[6];
for(i=0;i<5;i++)
b[i]=a[i];
for(i=0;i<6;i++)
st[i]=ch[i];
for(i=0;i<5;i++)
printf(%d ,b[i]);
printf(\n);
printf(st);
printf(\n);
return 0;
}
【例3.11】下面的程序使用键盘赋值,改正程序中的错
误。
include
int main
{
int i, a[5];
char ch[5];
for(i=0; i<5; i++)
scanf(%d, a+i);
for(i=0; <5; i++) printf(%d , a+i);
printf(\n);
for(i=0;i<5;i++)
scanf(%c, ch+i);
ch[i] = '\0';
printf(ch); printf(\n);
return 0;
}
程序中对数组a的赋值不对,i=0时,语句
scanf(%d,a);
为第1个元素a[0]赋值。当i=1时,“a+1”的含义是
数组a的最后一个元素的首地址值加1,即数组a后面的地
址,而不是第2个元素a[1]的地址(详见3.1节)。a[1]的
地址应该是“a[1]”,使用语句
scanf(%d,a[i]);
才是正确的。还有一个正确的写法,就是用地址循环。
因为数组名就是数组存储的首地址,所以a+1代表的是首地
址移动到第2个元素存储的首地址,也就是第2个元素的数组名。注意,a和a[0]的含义一样,都是第1个元素a[0]的地
址。a+1就代表a[1]的地址,与a[1]等效,所以如下两条
语句都是正确的。
scanf(%d, a[i]);
scanf(%d, a+i);
scanf要求的是存储元素的首地址,a[i]是变量,不是
地址。a[i]和a+i才是变量的地址,所以上述两条语句是
等效的。
同理,printf输出的是地址里的内容。a存放的是第1
个元素的首地址,a是第1个元素的值。a+1是数组第1个
元素之值加1,显然是不对的,应该是(a+1)。a+1代表
了首地址移动1个元素的地址,即第2个元素的首地址。下述
两条语句是等效的。
printf(%d ,(a+i));
printf(%d ,a[i]);修改后的程序如下:
include
int main
{
int i, a[5];
char ch[5];
for(i=0;i<5;i++)
scanf(%d,a+i); 等效scanf(%d,a[i]);
for(i=0;i<5;i++)
printf(%d ,(a+i)); 等效printf(%d ,a[i]);
printf(\n);
for(i=0;i<5;i++)
scanf(%c,ch+i); 等效scanf(%c,ch[i]);
ch[i]='\0';
printf(ch);
printf(\n);
return 0;
}
输出结果如下:
12345
1 2 3 4 5
abcde
abcd
字符数组ch长度为5,但是要存入一个结束符,所以如
果输入4个字符,则存入正确;如果输入5个,则最终只能存放4个字符。上述字符输入和输出也证明了这一点。
【例3.12】使用键盘为字符数组赋值的例子。
include
void main
{
int i;
char str[256];
i = -1;
do
{
i++;
scanf(%c, str[i]);
}while(str[i] != '');
str[i] = '\0';
printf(%s\n, str);
scanf(%s, str);
printf(%s\n, str);
}
do…while语句可以输入空格,它以“”为结束条件,而且可以输入多行。第2个输入语句是用空格作为分隔符
的,即只接收一个连续字符串,空格之后的字符将被忽略。
下面的运行示范中,第2个输入为“Go home!”,但程序
只接受Go。程序运行示范如下:
Go home!
OK
Go home!
OK
Go home!
Go3.3.4 求值顺序产生歧义错误
不要假设求值顺序。这将会使程序在有些机器上可能是
正常的,但在另一些机器上则不一定正常。
【例3.13】存在求值顺序歧义问题的程序。
include
void main( )
{
int i = 0, n = 5;
int y[5], x[]={1, 2, 3, 4, 5};
while (i < n)
y[i] = x[i++];
for ( i=0; i
printf(%d , y[i]);
}
程序是想将数组x的内容复制到数组y并输出y的内容。
但while语句中假设了求值顺序,这是不可靠的。应该写成
while (i
y[i]=x[i];
i++;
}最好简写成
for ( i = 0; i < n; i++ )
y[i]=x[i];
程序修改为
include
void main( )
{
int i=0, n=5;
int y[5], x[]={1, 2, 3, 4, 5};
for ( i=0; i
y[i] = x[i];
printf(%d , y[i]);
}
printf(\n);
}
使用时一定要注意编译系统的区别,不要在假设的前提
下编写程序。3.4 综合实例
本节列举几个典型的例子,说明数组的使用方法。
【例3.14】计算输入字符串中数码和字符出现次数的程
序。
include stdio.h
void main( )
{
int i, j, nw, nd[10];
char s[100], c;
j=0;
nw=0;
for ( i=0; i <10; i++ )
nd[i]=0;
while ( ( c=getchar( ) )!= '\n' )
{
s[j]=c;
switch ( s[j] ) {
case '0': case '1': case '2':
case '3': case '4': case '5':
case '6': case '7': case '8':
case '9':
nd[s[j]-'0']++;
break;
default:
nw++;
break;
}
j++; }
for ( i=0; i<10; i++ )
printf (%d , i );
printf (\n);
for ( i=0; i<10; i++ )
printf (%d , nd[i] );
printf (\nnw=%d\n, nw );
}
程序运行示范如下:
247909347674e ewfqgq\=.,0 1 2 3 4 5 6 7 8 9
1 0 1 1 3 0 1 3 0 2
nw=12
本例依赖于数字的字符表示,switch语句将数字分离
出来,而且此数字的值是s[j]-'0',这个值刚好落在数值
0~9之间,恰好是nd[]的下标值。非数字则由nw计数。
【例3.15】统计输入串中每个数字、字母和其他字符的
个数。
include
void main( )
{
int i, nother;
char c; int ndigit[10], nchar[26];
nother=0;
for ( i=0; i<10; ++i )
ndigit[i]=0;
for ( i=0; i<26; ++i )
nchar[i]=0;
while( ( c=getchar( ) ) != '\n' )
if ( c >= '0' c<= '9' )
++ndigit[c-'0'];
else if (c >= 'a' c<= 'z' )
++nchar[c-'a'];
else if (c>= 'A' c<= 'Z' )
++nchar[c-'A'];
else
++nother;
printf (digits= );
for ( i=0; i<10; ++i )
printf(%d ,ndigit[i] );
printf (\ncharacters= );
for ( i=0; i<26; ++i )
printf ( %d ,nchar[i] );
printf (\nother= %d\n,nother );
}
程序运行示范如下:
1234567898734abcdeghujxyz=-@^%321
digits=0 2 2 3 2 1 1 2 2 1
characters=1 1 1 1 1 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 1
other= 6
测试语句:if ( c>= 'a' c<= 'z' );
用来确定c中字符是否是小写英文字母,如果是小写英
文字母,则数值c-'a'这个值刚好落在数值0~25之间,恰好
是nchar[]的下标值。大写字母和数字处理方法类似。
通过上述例子可以看出,数组的元素可像一般变量那样
进行赋值、比较和运算。
【例3.16】求解6个元素的整型数组中正元素之和的程
序。
include
void main( )
{
int i,sum;
static int a[ ]={ -15,8, -225, -24, 56, -158};
sum=0;
for ( i=0; i<6; i++ ) {
if(a[i] <0)
continue;
sum=sum+a[i];
}
printf ( sum=%d\n,sum );
}程序运行输出:
sum=64
【例3.17】A、B、C、D、E合伙夜间捕鱼,凌晨时都疲
惫不堪,各自在河边的树丛中找地方睡着了。日上三竿,A
第一个醒来,他将鱼平分作5份,将多余的一条扔回湖中,拿自己的一份回家去了;B第二个醒来,也将鱼平分作5份,扔掉多余的一条,只拿走自己的一份;接着C、D、E依次醒
来,也都按同样的办法分鱼。5人至少合伙捕到多少条鱼?
每个人醒来后看到的鱼数是多少条?
【解题思路】假定A、B、C、D、E5人的编号分别为1、2、3、4、5,为了容易理解,让整数数组的标号直接与这5
个人的序号对应,定义整数数组fish[6]。不使用
fish[0],从而可以使用数组fish[k]表示第k个人所看到的
鱼数。fish[1]表示A所看到的鱼数,fish[2]表示B所看到
的鱼数……显然有如下关系:fish[1]=5人合伙捕鱼的总鱼数
fish[2]=(fish[1]-1)×45
fish[3]=(fish[2]-1)×45
fish[4]=(fish[3]-1)×45
fish[5]=(fish[4]-1)×45
由此可以写出如下一般表达式:
fish[i]=(fish[i-1]-1)×45 i=2,3,4,5
这个公式可用于从已知A看到的鱼数去推算B看到的,再
推算C看到的……现在能否倒过来,先知E看到的再反推D看到
的……直到A看到的。为此将上式改写为
fish[i-1]=fish[i]×54+1 i=5,4,3,2
分析上式如下:1)当i=5时,fish[5]表示E醒来后看到的鱼数,该数
应满足被5整除后余1,即fish[5]%5==1。
2)当i=5时,fish[i-1]表示D醒来后看到的鱼数,该
数既要满足fish[4]=fish[5]×54+1,又要满足
fish[4]%5==1。显然,fish[4]只能是整数,这个结论同
样可以用于fish[3]、fish[2]、和fish[1]。
3)按题意要求5人合伙捕到的最少鱼数,可以从小往大
枚举,先设E所看到的鱼数最少为6条,即fish[5]初始化为
6,之后每次增加5再试,直至递推到fish[1]且所得整数除
以5之后的鱼数为1。根据上述思路,可以将程序分为3个部
分:程序准备(包括声明和初始化)部分、递推部分和输出
结果部分。
程序准备部分包含定义数组fish[6]并初始化为1,定
义循环控制变量i并初始化为0。输出结果部分就是输出计算
结果。以上两个部分都很简单,下面着重介绍递推部分的实现方法。
递推部分使用do…while直到型循环结构,其循环体又
包含两部分:
1)枚举过程中的fish[5]的初值设置,一开始
fish[5]=1+5;以后每次增5。也就是说,第1个边界条件是
fish[5]=6,以后的边界条件是每次递增5。
2)使用一个for循环,i的初值为4,终值为1,步长
为-1,该循环的循环体是一个分支语句,如果fish[i+1]不
能被4整除,则跳出for循环(使用break语句);否则,从
fish[i+1]计算出fish[i]。当由break语句让程序退出循
环时,意味着某人看到的鱼数不是整数,当然不是所求,必
须令fish[5]加5后再试,即重新进入直到型循环do…while
的循环体。当正常退出for循环时,一定是循环控制变量i从
初值4,一步一步执行到终值1,每一步的鱼数均为整数;最
后i=0,表示计算完毕,且也达到了退出直到型循环的条件。
捕鱼问题参考程序
include
void main
{
int fish[6]={1,1,1,1,1,1}; 记录每人醒来后看到的鱼数
int i=0;
do
{
fish[5]=fish[5]+5; 让E看到的鱼数增5
for (i=4; i>=1; i--)
{
if (fish[i+1]%4!=0)
break; 跳出for循环
else
fish[i]=fish[i+1]54+1; 计算第i个人看到的鱼数
}
} while (i>=1); 当i>=1时,继续做do循环
输出计算结果
for (i=1; i<=5; i++)
printf(第%d个人看到的鱼是%d条。\n,i,fish[i]);
}
程序运行的输出结果如下:
第1个人看到的鱼是3121条。第2个人看到的鱼是2496条。第3个人看到的鱼是1996条。第4个人看到的鱼是1596条。第5个人看到的鱼是第4章 指针与数组
在C语言中,数组和指针是两个非常重要的概念,一定
要熟练掌握数组和指针的用法。虽然数组和指针是两个不相
关的概念,但在使用时,却又存在难以割舍的关系,所以一
定要掌握两者配合使用的方法。只有掌握这些内容,才能用
好它们。4.1 数组与指针的关系
C语言的数组名字就是这个数组的起始地址,指针变量
存储地址,数组名就是指针常量,所以指针和数组有密切关
系。任何能由数组下标完成的操作,也能用指针来实现。使
用指向数组的指针,有助于产生占用存储空间小、运行速度
快的高质量的目标代码。
指向数组的指针实际上指的是能够指向数组中任一个元
素的指针。这种指针应当说明为数组元素类型。例如,程序
中声明如下整型数组a
int a[5];
这时只要声明如下一个整型指针变量:
int pa;
就可 ......
您现在查看是摘要介绍页, 详见PDF附件(8108KB,975页)。





