IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C语言入门篇 -> 正文阅读

[C++知识库]C语言入门篇

文章目录

C语言入门篇

1. C程序的构成

1.1 头文件

? 在C语言中,头文件被大量的使用。头文件就像程序的大脑一样,里面记录了大量用户不需要的函数库。一般而言,每个C/C++程序通常都由头文件和定义文件组成。头文件作为一种包含功能函数、数据接口声明的载体,主要用于保存程序的声明,而定义文件用于保存程序的实现,.c就是你所编写的程序文件。

? 一般在一个应用开发体系中功能的真正逻辑实现是以硬件层为基础,在驱动程序、功能层程序以及用户的应用程序中完成的。头文件的主要作用在于多个代码文件全局变量的重用、防止定义的冲突。对各个被调用函数给出一个描述,其本身不需要包含程序的逻辑实现代码,它只起到描述性作用,用户程序只需要按照头文件中的接口声明来调用相关函数和变量,链接器就会从库中寻找相应的实际定义代码。

? C/C++的头文件以".h"为扩展名。

1.2 主函数

注意:C程序的设计原则设计把函数作为程序的构成模块,main()函数一般被称为主函数,一个C程序总是从main()函数开始执行的。

1.2.1 main()函数的形式

在C99的标准中,只有以下两种定义方式设计正确的:

int main(void)    /*无参数形式*/
{
    ...
    return 0;
}

int main(int argc, char *argv[])   /*带参数形式*/
{
    ...
    return 0;
}

int指明了main()函数的返回类型,传输给函数的信息一般被包含在函数名的后面的括号中,void表示没有给函数传递参数。

1.2.2 main()函数的返回值

main()函数的返回值类型是整数,而程序最后的return 0;说明0就是函数的返回值。那么这个0表示返回哪里呢?返回操作系统,表示程序正常退出。return语句常常写在程序的最后面,也就是说,只要程序最后执行了这个return语句,那么这个程序就结束了。所以,return语句的作用不只是返回一个值,还有结束函数。

1.3 其他组成部分

以下面这段代码为例:

#include<stdio.h>
#define _CRT_SECURE_NO_WARNINGS
#define PI 3.1415926  //定义常量

float area(float r);  // 声明计算面积饿函数
float perimeter(float r); //声明计算周长的函数

void main(void) {
    float r;  // 定义变量r来保存圆的半径
    float s, l;  // 定义变量来保存圆的面积和周长

    printf("请输入圆的半径:");  // 显示提示信息
    scanf_s("%f",&r);  // 接收用户输入的半径

    s = area(r);  //调用area()函数计算面积
    l = perimeter(r);  //调用perimeter()函数计算周长

    printf("半径R=%.2f, 面积S=%.2f \n", r, s); //输出圆的面积
    printf("半径R=%.2f, 周长L=%.2f \n", r, l); //输出圆的周长
}
/*函数area()计算圆的面积*/
float area(float r) {
    float s;

    s = PI * r * r;  // 计算圆的面积
    return s;    // 返回圆的面积
}
/*函数perimeter()计算圆的周长*/
float perimeter(float r) {
    float l;

    l = 2 * PI * r;   // 计算圆的周长
    return l;      // 返回圆的周长
}

1.定义常量

? 使用#define定义一个符号为PI,值为3.1415926。当程序被编译时,程序后面的PI都会被替换成3.1415926。

? 使用符号常量的好处就在于其名称可以给程序员起到提示作用,另一个好处就是方便修改,如果需要修改圆周率的精度,只需要修改符号常量的定义语句即可。若不使用符号常量,若修改则需要修改所有的PI变量的值

2.声明函数原型

? 在C语言中,函数声明称为函数原型。它的主要作用就是利用它在程序的编译阶段对调用函数的合法性进行全面的检查。

? 如果函数调用前没有对函数做出声明,则编译系统会把第一次遇到的该函数的形式(函数定义或者函数调用)作为函数的声明,并将函数的类型默认为整型。使用这种方法时,系统无法对参数的类型进行检查,或者调用函数时参数使用不当,在编译时也不会报错。因此,为了程序的清晰和安全,建议对函数做出声明。

3.程序语句

? C程序是由多条语句构成的,不同的语句完成不同的功能。在C程序中,一般一条语句占用一行,语句以分号结束。C程序一般使用以下语句。

(1)变量定义

? 程序对半径、面积、周长的变量的定义为变量定义语句。在C语言中,在使用变量之前必须先进行定义。编译器遇到变量定义语句时,按变量类型为其分配内存空间。

(2)输出语句

? 程序使用printf()库函数向屏幕上输出信息。

(3)输入语句

? scanf()库函数用于从键盘接收用户的输入,并将结果保存到指定的变量中

(4)运算语句

? 调用面积函数和周长函数,函数中计算面积与周长的语句。

(5)返回语句

? 调用面积函数和周长函数,最后return返回面积和周长的语句。

1.4 函数定义

? C语言中的函数相当于BASIC中的子程序、Pascal中的过程。通过函数将相关功能进行封装,调用函数时不必了解函数的实现细节。在ANSI C中有数量众多的库函数,这些函数都是程序中常用的功能,如上面代码中的scanf()和printf()两个库函数。

? 在更多的情况下用户需要编写自己的函数,以此来完成更多的特定功能。函数定义包括两个部分:函数头和函数体。函数头定义函数的名称、参数和函数返回值的类型;而函数体则定义函数具体完成的工作。

? 在上面代码中的main()、area()和perimeter()三个函数。从函数头定义中可以看出main()函数无参数、无返回值;area()和perimeter()函数需要一个float类型的参数,并返回一个float类型的返回值。

1.5 注释

? 为程序添加注释是一个非常好的编程习惯。C语言编译器将忽略所有的注释内容,因为注释不是写给编译器的,而是写给程序员阅读的。当一个程序比较简单时,注释显得有点多余,但是随着程序的变大、参与开发的人员变多,在程序中通过注释说明该程序的逻辑和结构,可以使代码的可读性大大的提高。

/* */ 为块注释,注释一段代码块

// 行注释 ,注释一行的代码

1.6 拓展训练

1.6.1 打印字母C

#include<stdio.h>
void main(void) {
	printf("*****\n");
	printf("*\n");
	printf("*\n");
	printf("*****\n");
}

1.6.2 scanf()函数的应用

#include<stdio.h>
void main(void) {
	int a;
	scanf_s("%d", &a);
	printf("The number is %d \n", a);
}

在VS中直接调用scanf()函数会报错,因此可以在头文件中加入#define _CRT_SECURE_NO_WARNINGS,或者是应用如上的scanf_s()来解决问题。

1.7 C语言的一些基础规范

1.7.1 规范命名

在C语言中,一般被命名的名字都被称作标识符。标识符是指一个字符组成的序列,通常包括变量名、常量名、函数名、程序名等。这些命名都必须符合C语言的规范,否则程序运行时会出现错误。

命名是必须符合以下C语言的规范:

  • C语言中严格区分大小写,如A和a分别表示两个不同的命名,意义完全不一样
  • C语言命名必需以下划线或者字母开头,不能以数字开头
  • C语言中命名的名字长度不限,但一般只有前8位有效,不同的命名前8位一定要不相同

C语言中的标识符可以分为3类:

(1)关键字:指C语言中有固定含义的标识符,不能表示其他的含义。包含32个:auto、extern、register、static、typedef、char、const、double、enum、float、int、long、short、signed、struct、union、unsigned、void、volatile、break、case、continue、default、do、else、for、goto、if、return、switch、while和sizeof。

(2)特定字:指C语言中有特定含义的标识符,不能表示其他的含义,与关键字并无很大区别。

特定字包括:include、define、under、ifdef、ifndef、endif、main

(3)用户标识符:值用户自己定义的一些标识符,如程序中的变量名、函数名等注意:用户标识符由下划线、数字和字母组成,首字符不能为数字,并且命名的名字不能与关键字和特定字相冲突

1.7.2 美观对称

? 在C语言中,代码讲究规范、对称和美观。通常从一个程序中就可以看出一名程序员的编程风格,好的程序员编写的代码都很简洁、美观和对称。因此开始学习C语言的时候必须注意养成良好的编程习惯。

? 建议如下:

  1. 空行:空行虽然不会浪费内存,但是会浪费纸张。因此,需要根据实际情况来判断是否加空行,必要时加上空行。
  2. 一行代码最好只做一件事,不要都挤在一行。
  3. 在定义变量时就对该变量进行初始化,可以避免变量未初始化引发的问题。
  4. 编译代码时“{” 和 “}“要对齐,可以使程序简洁,尤其是出现多队{}符号时,对齐的效果非常明显。
  5. 修饰符应该紧靠变量,不容易让人产生误解
char* a,b;
char *a,b;  //不容易产生误解

1.7.3 注释形式

? 在C语言中添加注释,可以用来对程序进行分析说明以及提示需要注意的问题。注释就像旁白一样,是独立于程序本身的,不会影响程序的运行。注释虽然有助于代码的理解,但是也不能滥用。

? 建议如下:

  1. 一些简单的语句不用添加注释
  2. 注释应与源代码相近,不可以太远,放在代码的左边、右边、上边都可以
  3. 注释应适量,不可以太多,毕竟注释只起到辅助的作用
  4. 修改代码的同时应该修改注释,以保证代码和注释的一致性
  5. 注释应该尽可能准确和简洁
  6. 对于结构化的部分,应在该结构的开头或末尾添加注释,以便于阅读和理解

2. 数据是一切程序存在的基础

2.1 C语言中的数据类型

数据类型是按规定的形式表示数据的一种方式,不同的数据类型将占用不同的存储空间,就像我们要建造的房屋的地基,只有使用正确的数据类型,才能将这个房屋建造好。C语言提供了许多的数据类型,可以分为基本类型、构造类型、指针类型、空类型四个大类。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XHjbh10J-1631880972539)(…/C-数据类型.png)]

2.1.1 基本类型

? 基本类型不可以在分解为其他的类型,包含浮点型、整数型、字符型、枚举类型。

? 注意:其中整型包括长整型和短整型

? 整型一般占用4字节(32位),最高位代表符号位,整数用0表示,负数用1表示,取值的范围是-2147483648~2147483647,它在内存中的存储顺序为地位在前、高位在后。

  • 整型的定义:
int a=6;
  • 长整型的定义:
long a=6;
  • 短整型的定义:
short a=6

注意:浮点型也包括包括单精度和双精度型

  • 浮点型,又称实型,也称单精度型,一般占4字节(32位),定义如下:
float a=4.5;
  • 双精度型一般站8个字节(64位),定义如下:
double a=4.5;
  • 字符型在各类系统中都只占1字节(8位),定义如下:
char c='a';
  • 字符型也可以通过字符对应的ASCII码赋值,定义如下:
char c=97;

注意:以上指定是一般情况,不同的平台可能还会有所不同,具体的平台可以用sizeof关键字进行测试

2.1.2 构造类型

? 构造类型是指使用基本类型或其他已定义的一个或多个数据类型来构造一个新的数据类型。构造类型就像一节车厢,里面包含了各种不同类型的乘客。即一个构造类型可以分解为若干个成员,每个成员都是一个基本类型或一个已定义的构造类型,比如结构类型、联合类型、数组类型等。

struct point
{
    //包含4个变量成员
    int x;      // 包含整型变量x
    int y;      // 包含整型变量y
    char z;     // 包含字符型变量z
    int A[10];  // 包含整型数组A
} // 自定义的构造类型

2.1.3 指针类型

? C语言的精华实在程序中使用指针。指针是一种特殊的、具有重要作用的数据类型。他就像内存的门牌号,指针的值表示某个地址,有了地址就能方便的访问内存。

? 指针类型跟指针的值有关,指针在内存中占4个字节,但是指针的类型却是各不相同的。例如,可以定义指针:

int *a;
char *b;
float *c;

2.1.4 空类型

? 空类型主要用来设置函数的返回值。函数在结束的时候一般都会要求调用者返回一个类型,如整型、指针类型等,编程者在设计函数的时候就要构想好函数的返回值是什么,如果函数不返回值,就设置函数返回值为空。具体设置方法如下:

void main(void)
{
    
}

2.2 数据的存储原理

2.2.1 内存单元

? 我们把计算机的内存看做一条长长的火车,一般来说,每一节火车由8个车厢构成,也就是说,在个人计算机中,一般由8个二进制“位”组成一个字节。如下图所示:

![img](file:///C:\Users\26217\Documents\Tencent Files\2621715498\Image\C2C\AC]46RTR$2SW4L2Y4`AQT.png)

? 计算机可以处理海量的信息,也需要具有巨大的容量的内存系统。为了适应海量内存的表示,通常对内存可以有多个表示单位,各个度量单位之间的转换关系如下:

1Byte = 8 bit
1KB = 1024 Byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB

注意:C程序可以处理内存的最小单位为位(bit)

? 现在计算机有512MB、1GB甚至2GB以上的内存,对于这些海量的内存单元,计算机如何做到在指定的内存单元中存取数据呢?在计算机中,每个内存单元都有一个地址,通过这个地址即可对内存中的数据进行保存和读取操作。

? 下图显示了4个字节的内容,左侧为每个字节的地址,如地址是8100内存单元中保存的数据就是97,实际上保存在计算机中的是二进制数“01100001”,在图中显示的十进制数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NjcjjXr8-1631880972543)(…/内存地址.png)]

? 计算机中的内存是按字节顺序排列的,内存地址也依次编码。在C程序中,程序员一般不要关系具体的地址值,这些都是C编译器自动处理的。

2.2.2 字符的存储

? 要将数据保存到计算机,必须使用二进制形式来表示数据。对于字符和数值将按不同的方式进行保存。

? 对于字符,可以使用ASCII来表示。ASCII码将字母、数字、特殊符号都进行了编码,如编号97表示小写字母a,在计算机中保存字母a时,实际上是保存数字编码97(97转换为二进制编码为“01100001”)。

2.2.3 数值的存储

? 1字节为8位二进制位,正好可以存储一个字符ASCII码。而如果保存数值,则8位二进制数最大只能表示256。如果要表示更大的数,1字节就不够了。

? 在计算机中,一般整数用2字节来表示存放,而实数则用4字节或更多的字节来存放。

? 在计算机中,保存到每个字节的都是8位二进制位,那么计算机怎么区分保存的是字符还是数值呢?这就需要在程序中指明将要保存或读取的数据类型。如果设置保存字符,则只将数据写入1字节,如果设置保存整数,则将内存中相邻的2字节中写入数据。同样,在读出数据的时候,若指定读取的输出为一个字符,则只从内存中读取1字节的数据,若指定读取一个整数值,则将读取的内存中相邻的2字节,并组合为一个整数值。

2.3 标识符和关键字

2.3.1 标识符

? 在C语言中,为了引用常量、变量、数组、函数、宏名等,需要给元素设置一个名称,这些名称就是标识符。标识符就行我们为自己创造的东西起的名字,这些名字不能与已有的名字重复。每一种程序设计语言对标识符都有一定的限制,C语言标识符一般遵循以下的命名规则:

  • 标识符必须以字母或下划线开头,后面可跟任意多个(可为0个)字符,这些字符可以是英文字母、下划线、数字,其他字符不允许出现在标识符中。
  • 对于标识符的长度,C89规定,编译器至少能够处理31个字符(包括31)以内的标识符,C99规定,编译器应该至少能够处理63个字符(包括63)以内的标识符。在程序中,如果使用超出最大数目限制的字符来命名标识符,则编译器会忽略超出的那部分字符。
  • C语言中的关键字具有特殊含义,不能作为用户自定义的标识符。
  • 自定义的标识符最好取具有一定意义的字符串(见名知意),以方便阅读,记忆和使用

注意:标识符区分大小写name、Name、NAME是3个不同的标识符。

? 了解标识符的相关规定后,即可在程序中为相关的元素设置符合要求的名称,以下的标识符是正确的,可以在程序中作为符号变量、变量、函数等的名称使用。

name
Age
address_home
telephone2
_frequency
__X

? 而下面这些标识符就是错误的

2telephone  // 以数字开头
First name  // 含有空格
return      // C语言的关键字
a+b         // 包含+号

2.3.2 关键字

? C语言简洁、紧凑,使用方便、灵活。ANSI C标准C语言共有32个关键字,9种控制语句,程序书写形式自由,区分大小写。把高级语言的基本结构和语句与低级语言的实用性结合起来。C语言可以像汇编语言一样对位、字节和地址进行操作,而这三者是计算机中最基本的工作单元。

? C语言中关键字如下:

auto、extern、register、static、typedef、char、const、double、enum、float、int、long、short、signed、struct、union、unsigned、void、volatile、break、case、continue、default、do、else、for、goto、if、return、switch、while和sizeof。

? 1999年12月16日,ISO推出了C99标准,新增了5个C语言关键字,如下:

inline、restrict、 _Bool_ Complex_Imaginary

? 2011年12月8日,ISO发布C语言的新标准C11,该标准新增了7个C语言关键字。如下所示:

_Alignas_Alignof_Atomic_Static_assert_Noreturn_Thred_iocal_Generic

注意:程序中只能使用关键字的规定作用,而不能给它们赋予新的含义。例如,关键字int规定的作用是说明整形数据,而不能用于其他的用途。

2.4 常量

? 在C语言中,程序执行时其值不发生改变的值称为常量,其值可变的称为变量,常量可以直接使用,而变量必须先定义后使用。

2.4.1 直接常量

? 直接常量又称字面常量,通过数据直接表现的形式来确定其数据类型,比如:

s=3.14*r*r;

? 在以上代码中,3.14表示为小数形式,该常量的数据类型为浮点数类型。

l=2*3.14*r

? 在以上代码中,2表示为整数,该常量的数据类型为整数类型。

2.4.2 符号常量

? 在C语言中经常用一个与常量相关的标识符来代替常量出现在程序中,这种相关的标识符称为符号常量。

? 使用符号常量的优点:

  1. 使程序的常量含义清楚
  2. 若修改常量,则只需修改定义符号常量的语句即可

提示:符号常量不是变量,它所代表的值在程序运行过程中不能改变。也就是说,在程序运行中,不能再使用赋值语句对它重新赋值。

可以用以下两种方法定义符号常量

  • #define宏定义
#define 符号常量名 宏表达式

警告:使用#define进行宏定义的时候,最后不能添加分号。若有分号,则在进行宏替换的时候将连分号一起替换,将得到错误的结果。

  • const语句定义

    另一种定义符号常量的方法是使用const语句,其语法格式如下:

    const 类型 符号常量=表达式;
    

注意:const是一条C语句,需要在语句末尾添加分号作为结束

const float PI=3.14;

2.5 变量

? 变量是常量的兄弟,不同的是,变量需要先定义再使用,否则编译器会报错。

注意:习惯上,符号常量的标识符用大写字母,变量的标识符用小写字母,以示区别。

2.5.1 变量声明

? 在C语言中,变量需要先声明再使用,这样做的好处在于:

  • 确保程序中引用的变量名正确。凡引用未声明的变量,在编译时都会提示报错,这样可以减少因为输入错误而导致的变量引用错误。
  • 编译器按声明的变量类型分配相应的内存单元
  • 在C语言中,不同的数据类型可以进行的运行不同,在变量声明时指定了变量的类型,在编译时可以检查变量所进行的运算是否合法。

声明变量的简单格式如下:

类型说明符 变量名表;

2.5.2 变量初始化

? 编译器给声明的变量分配内存空间时,并不会对分配的内存单元清零。就像在高铁上,每个座位都有可能入座,尽管他们不一定有票,当你出示你的车票的时候,这个座位就是你的了,你便可以将霸占你座位的人赶走,也就是说,这时的内存单元是一个不确定的值。为了程序的不会因为使用一个不确定值而出现数据混乱,应在使用变量前对其进行赋初值,使其有确定的值,这种方法被称为初始化。

int age, num, count;
age=18;
count=50;

? 后面两条语句就是赋值语句,分别将常量18保存age变量中,将常量50保存count变量中。如果多个变量具有相同的值,还看使用以下的连续赋值的方式来快速初始化变量:

a=b=c=d=8;  // 执行这条语句后a、b、c、d的值都为8

? 最方便的初始化方法是在声明变量的同时为其赋值,其语法格式如下:

类型说明符 变量1=1,变量2=2,···;

例如:

int age=18;
int num,count=50;  //只给count变量赋初值,对变量num未进行处理

2.6 整数类型

C语言提供了多种整数类型,分别用来表示不同范围的整数。其中int类型是最基本的整数类型,程序员可根据需要选择其他类型。

2.6.1 整数类型及其存储

1. 整数类型

? 在C语言中,根据计算机字长的不同,为int分配的存储空间也不一样。如果要查看自己的计算机中int类型占用即字节的内存,可以使用以下的语句:

printf("int: %d Bytes. \n",sizeof(int));

? 其中,sizeof为一个运算符,用来计算指定数据类型或变量占用的存储空间大小,以字节为单位。

? 在声明int类型的变量时,还可以给int添加各种修饰符。通过添加修饰符可改变int类型的含义,使其更加精确的适合程序的需要。共有以下4个修饰符。

  • signed:有符号,默认值

  • unsigned:无符号

  • long:长型

  • short:短型

    在默认情况下,直接使用int关键字声明变量,该变量用来表示有符号数。以下两种声明方式是相同的:

    signed int i;
    int i;
    

    ? 在计算机中,若要表示负数,则必须使用有符号数。在有符号数中,使用一位二进制位表示符号位,如果int类型为16位字长,那么这时只能用15位来表示数值,所有其表示的数据范围将缩小。有符号int类型的取值范围为-3276832767。若使用unsigned来修饰int类型,用来保存无符号数据,则可以使用16位二进制位来全部表示数据,其取值范围为065535.

    ? 使用long修饰int,可以直接简写为long。这种类型可能占用比int类型更多的存储空间,比如在16位的计算机中,int类型为16位,long类型则为32位;在字长为32的计算机中,int类型和long类型都是32位。

    ? 使用short类型修饰int,可以之间简写为short。这种类型可能占用比int类型更少的存储空间,比如在16位的计算机中,short类型和int类型都是16位,而在字长32位的计算机中,short类型为16位,int类型为32位。

    ? 如果要表示比较大的整数,还可以使用两个long关键字来修饰int类型,如下:

    long long int i;
    

    ? 其中,关键字int可以省略,直接写为long long。这种类型占用比long类型更多的存储空间。

2. 整数的存储

? C语言提供的整数类型以及每种类型的占用的存储空间上面已经介绍。在计算机中,负数的保存比较特殊,并不是简单的将符号位位置改为1即可。为了提高计算机加减运算的速度,现代计算机都采用补码来保存数据。

? 补码的转换非常简单,可使用下面两个规则来求一个数的补码:

  • 整数的补码就是本身
  • 负数的补码,除符号位外逐位取反,最后再加1

2.6.2 整型常量的表示

? 在C语言中,编译器根据字面常量的表达形式确定其类型。如果程序中的字面常量看起来像整型,编译器就会将其作为整型。例如:

f=32+c;

? 在以上代码中,32将被作为int类型的常量,而如果写作如下形式:

f=32.0+c;

? 因为有小数点,则编译器会将其作为float类型的常量。

1. 整型常量的类型

? 通常,C编译器将智能识别整型常量的类型。例如,在程序中使用数字1200,编译器会将该数字以int类型存储到内存中。如果程序中有数字268263823849,这时使用int类型的将不能完整的保存该数值,那么编译器将会自动的选择unsigned long类型,如果仍不能完整的保存数据,那么编译器将会选择long long类型保存该数,甚至使unsigned long long 类型。

? 在一个小的整数后面添加一个字母l或者L,则该数被看做一个long类型的常数。在16位的计算机中,编译器将128作为2字节的int类型存储,而128L则会告诉编译器将该数保存为4字节的long类型。

? 若需要将小的整数按long long类型存储,则可以在整型常量的后面添加后缀LL(或ll)。

? 若需要将整型常量作为无符号数据存储,则可以在整型常量的后面添加后缀u(或U)。后缀U和L或LL可以一起使用。

2. 整型常量表示为十进制

? 在C语言中,可以将整型常量按十进制、十六进制、八进制来写。

十进制整型常量不需要前缀,直接由0~9的数字组成。以下各数都是合法的十进制整型常量:225、-512、65535、156472。

而以下各数不是合法的十进制整型常量:

  • 0735:使用了前导0,编译器会将其认为是八进制数,相当于十进制的477
  • 0818:使用了前导0,编译器会将其认为是八进制数,在八进制数中使用超过7的编码,编译器会提示出错
  • 5F:含有非十进制数码

提示:在C程序中,编译器将根据数字前缀来区分各种进制,因此在书写常数时不能把前缀弄错,以免造成结果不正确。

3. 整型常量表示为十六进制

十六进制整型常量前缀为0X或0x。其数码取值为09,AF或a~f共16个数码。

以下是合法的十六进制整型常量:

  • 0XFF:十进制的255

  • 0x10000:十进制的65535

  • 0x3a4f:十进制的14927

而以下的各数不是合法的十六进制整型常量:

  • FFFF:无前缀0X或者0x
  • 0x5HB:H为非十六进制数码
4 .整型常量表示为八进制

八进制整型常量必须以0为前缀。其数码取值为0~7.

以下各数是合法的八进制整型常量:

  • 0735
  • 0101
  • 010000
  • 0777

而以下各数不是合法的八进制整型常量:

  • 254:无前缀0,编译器会将其认为是十进制的数值进行保存
  • 0818:包含非八进制的数码

2.6.3 整形数据输出

? 使用printf()库函数可以将整型数据显示到屏幕上,一般情况下,可在printf()中使用%d输出整型数据。但对于unsigned、long long类型的数据,直接使用%d输出数据时可能会出现问题。另外,为了方便用户,有时还可能需要将整型数据输出为八进制或十六进制数。

1. 输出不同类型的数据

在printf()函数中,使用%d控制符可输出带符号的十进制整数。还可以使用以下控制符输出整数。

  • %ld:输出signed long数值(使用%d也可以输出long数值,但若移植其他系统则可能出现问题)
  • %hd:输出signed short数值
  • %lld:输出signed long long数值。在有的系统中,需要使用%I64d输出该类型(是i的大写字符I,不是小写字母l)
  • %u:输出unsigned int数值
  • %lu:输出unsigned long数值
  • %llu:输出unsigned long long数值
#include<stdio.h>
int main() {
	unsigned int ui = 2147483649U;
	long l = 65537;
	short s = 128;
	long long vl = 13198317000;

	printf("无符号整型数:%u, 显示为有符号整型数:%d\n\n", ui, ui);
	printf("短整型数:%hd, 显示为无符号短整型数:%hu, 显示为整型数:%d\n\n", s, s, s);
	printf("长整型数:%ld, 显示为整型数:%d, 显示为短整型数:%hd\n\n", l, l, l);
	printf("特长整型数:%lld, 显示为整型数:%d\n\n", vl, vl);
	getch();
	return 0;
}

运行代码得到以下结果。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6UJXbnLd-1631880972580)(…/整型数据输出实例.png)]

2. 输出十六进制数和八进制数

编写C源代码时可按3种数制(十进制、十六进制、八进制)书写数字,也允许按照这3种数制输出整数。在默认情况下输出十进制数,可在printf()函数中使用控制符来控制输出数据的数制。

  • %x:将整数输出为十六进制数
  • %o:将整数输出为八进制数
#include<stdio.h>
int main() {
	int i;

	printf("请输入一个十进制数:");
	scanf_s("%d", &i);

	printf("\n你输入的十进制数是:%d\n\n", i);
	printf("转换为十六进制数:%x\n\n", i);
	printf("转换为八进制数:%o\n\n", i);
	getchar();
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A6jG02Rs-1631880972584)(…/十六进制和八进制输出.png)]

2.7 字符类型

2.7.1 字符常量

与整型常量不同,字符常量必须使用指定的定界符来进行限制。字符常量是用单引号括起来的一个字符。

'c''w''y''0''#'

在程序中输入字符常量的时候,需要注意以下几点:

  • 字符常量的定界符为单引号,只能用单引号来括起来,不能用双引号或其他符号
  • 一对单引号只能包括一个字符,不能是多个字符
  • 字符可以是ASCII码中的任何字符
  • 数字被定义为字符就不能代表数字字面的量,若参与数值运算,其值将是对应的ASCII码。

2.7.2 字符变量及其初始化

与声明其他类型的变量类似,使用char关键字声明字符变量

char c1,c2;
char answer;

以上两条语句定义了三个字符变量,编译器将为每个字符变量分配1个字节的存储空间。可以使用以下的语句为字符变量赋初值:

c1 = 'w';

编译器处理该语句的时候,将字符常量w对应的ASCII码值119保存到为变量c1分配的内存单元中。

使用以下语句赋初值是不对的:

c1 = "w";
c1 = w;

因为字符常量是作为一个整数进行保存的,所以也可以使用下面的语句进行赋值

c1 = 119;

使用整数值给变量赋值时,如果整数值超过取值范围,则编译将会出现错误提示。有的C编译器将char作为有符号类型,这样其取值范围就是-128127;而另一些C编译器将char作为无符号类型,其取值范围就是0255。

2.7.3 转义字符

除英文字母、数字和一些特殊符号能在屏幕上显示之外,还有许多不能显示的字符,如果让计算机蜂鸣的ASCII码值为7,怎么将这个字符赋值给一个变量呢?可以使用两种方法。

第一种方法就是将ASCII码值直接赋值给char变量,例如:

char beer=7;

这种方法可能带来一些问题,首先是不直观,其次是给程序的移植带来一些问题。如果将这个程序移植到不支持ASCII码的系统中,那么计算机蜂鸣的代码可能就不是7了。

第二种方法就是转义字符。转义字符是一种特殊的字符常量,以反斜线"\"开头,后面跟一个或几个字符。反斜线后面的字符不在具有原来的意义,所以称为转义字符。例如我们经常看见的"\n",用来表示回车换行,下表列举了C语言中的转义字符及其意义。

转义字符转义字符的意义ASCII码值
\a蜂鸣警告7
\b退格8
\f走纸换页12
\n回车换行10
\r回车13
\t水平制表符9
\v垂直制表符11
\\反斜线符92
\'单引号符39
\"双引号符34
?问号63
\ddd1~3位八进制所代表的的字符
\xhh1~2位十六进制所代表的字符

广义的讲,C语言字符集合中的任何一个字符都可以用转移字符来表示,上表中最后两个就是为此提出的。ddd和xhh分别是八进制和十六进制的ASCII码值,如\111表示的字符“I”(大写的i),\112表示的是字母"J",\x2a表示的是字符"*"等。

转义字符中的\t产生一个制表符,制表符常用来生成一定排列规则的数据。

#include<stdio.h>
int main() {
	int i = 1, j = 2, k = 3;
	printf("输出第一种形式:\n");
	printf("变量i\t变量j\t变量k\n");
	printf("%d\t%d\t%d\n", i, j, k);
	printf("\n输出第二种形式:\n");
	printf("变量i\t%d\n", i);
	printf("变量j\t%d\n", j);
	printf("变量k\t%d\n", k);
	getch();
	return 0;
}

运行以上代码的结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sNMWDUyq-1631880972587)(…/转义字符实例.png)]

2.7.4 字符类型数据的输出

在C语言中,可以对char类型变量赋予初值,同样也允许对整型变量赋予字符常量,此时,编译器会将字符常量对应的ASCII码值保存到整型变量中。

既然char类型变量保存的也是整型数,那么也对char类型变量进行加减运算。当运算结果超过char类型的取值范围时,程序会自动舍弃高位的值,而只保存低八位的值。

在输出时,既允许把字符变量按整型数输出,也允许把整型量按字符量输出。在C语言中,printf()函数使用控制符"%c"输出字符。若要将字符量输出为ASCII码值,则可以使用输出整数的控制符"%d"。

在程序当中int类型变量中的值作为字符输出时,因为int类型占4个字节,而char类型只占了1个字节,所以此时只有低八位的值参与处理。

例如,下面的程序演示将int类型输出为字符,对char类型进行加法运算的效果:

#include<stdio.h>
int main()
{
	int i = 65, j;
	char c = 'a';
	j = i + 1;
	printf("整数i=%d,显示为字符:%c\n", i, i);
	printf("整数j=%d,显示为字符:%c\n", j, j);
	printf("字符c=%c,显示为整数:%d\n", c, c);
	c = c + 1;
	printf("字符c=%c,显示为整数:%d\n", c, c);
	getch();
	return 0;
}

编译以上程序,结果如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ekpaXlRi-1631880972588)(…/字符型数据输出实例.png)]

2.8 实数类型

在大多数程序中,使用整数就能完成任务。但是,实际中,经常会遇到处理财务数据等需要使用小数的情况,这时就需要使用C语言中的实数类型(浮点数)。

2.8.1 实数类型及其存储

1. 实数类型及其取值范围

C语言中实数包括float、double、long double三种类型。

  • float类型:C语言标准规定,float类型至少能表示6位有效数字,取值范围为1十的负三十七次方~十的三十七次方。六位有效数字指float至少能表示数据的六位有效值,超出该有效值时系统会采用近似的数表示。
  • double类型:称为双精度实数。double类型与float类型相比,增加了数据的精度,至少能表示10位有效数字。
  • long double类型:该类型可以满足比double类型更高的精度要求。不给C语言要求long double类型至少应于double类型具有一样的精度。
2. 实数的存储

实数类型的数据与整数类型的数据存储方式不同。实数保存到内存中时会将数据分为三部分。如图所示,其中,符号位用来表示正负数,小数部分用来表示有效位,指数部分表示10的几次冥。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OdGVSQO6-1631880972589)(…/实数存储.png)]

2.8.2 实型常量的表示

与整数类型相比,实数类型可以表示更大或更小的数据。在C语言中,整数是不含小数点的数,只要数据包含小数点,就将其表示为实数。比如,5是一个整数,5.0却是一个实数。

1. 实型常量的表示方法

实型常量一般由两种表示方式:

一种是直接书写的、包含小数点的数。比如:

+0.618
.618   // 省略整数部分
95.    // 省略小数部分
-6.66

对于正号,可以省略正号。对于只有小数部分的数可以省略整数部分的0,对于无小数部分的数可以省略小数点后面的0,但是都不能省略小数点,否则会被当成整数处理。

另一种表示方法就是科学计数法,这种方法可以表示很大或者很小的数据。

对比两种形式,显示是科学计数法的形式更直观,要知道第一种表现形式数据的大小需要去仔细的数后面有多少个0。

在C语言中,科学计数法使用以下形式来表示:

尾数E阶码

尾数和阶码都可以包含符号,字母E也可以小写

注意:尾数、字母E、阶码之间不能有空格。

例如,6乘以十的九次幂可以编写成以下几种形式:

6.0E9
6.E+9
+6.E+9
2. 强制类型

在默认情况下,C编译器将实数按照double类型存储。之所以用double类型,是因为该类型的有位数多一些,能够保证计算的精度。但是double类型要占用8字节,比float多占一倍的控件,所以降低程序的运行速度。

2.8.3 实型变量

实型变量的声明和初始化方法与整型变量相同。例如:

float price = 18;
double star,end;
long double id_star = 5.833333L;

警告:实型变量和整型变量不同,整型变量能够精确的表示数据,而实型变量受有效位数的限制,在超过有效位数后将采取近似值来表示数据。

2.8.4 实型数据的输出

在库函数printf()中,可以使用以下控制符输出实型数据:

  • %f:该控制符可以用来按十进制计数法输出float和double类型的数据。
  • %e:该控制符可以按指数计数法输出float和double类型的数据。
  • %Lf、%Le:用来输出long double类型的数据。

下面程序演示输出实型数据的方法:

#include<stdio.h>
int main() {
	float f;
	double d;

	f = 1234567890123.456789;
	d = 1234567890123.456789;

	printf("f=%f\t科学计数法f=%e\n", f, f);
	printf("d=%f\t科学计数法d=%e\n", d, d);
	getch();
	return 0;
}

运行以上程序,等到以下结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xDh2eKw9-1631880972590)(…/实型数据输出实例.png)]

在程序中给f和d初始化为通用的值。因为float和double类型的保存数据的精度不洗头,所以在输出数据的时候,显示的结果也就不同。因为float类型只能保存6~7位的有效数,所以从第8位开显示近似数。double类型的数据因其有效位数是16位,所以从第17位开始显示近似数。

2.9 混合运算及类型转换

2.9.1 混合运算

在不同的数据类型混合在一起运算时,就要进行类型转换。其转换规则很简单,各基本数据类型可以排出以下等级:

long double>double>float>long long>long>int>short>char

左边的类型比右边的类型等级高。

在不同的数据类型参与运算的时候,C语言总是将等级低的类型转换为等级高的类型,这种类型转换称为自动转换,是由编译器自动完成的。

自动转换遵循以下规则:

  • 若参与计算的数据类型不同,则先转换成同一类型,然后再进行运算。
  • 转换按数据长度增加的方向进行,以保证精度不降低。
  • 所有的浮点运行都是双精度进行的,即使仅包含float单精度运算的表达式,也要先转换为double类型,再进行运算。
  • char类型和short类型参与运算,都必须先转换为int型。

注意:在赋值运算时,无论等号右侧的类型如何,都转换为左侧变量的类型,若右侧数据的精度具有较高等级,则将被截短或舍入,使结果与左侧的变量的类型相同。

以下演示char类型和int类型的转换:

#include<stdio.h>
int main() {
	int i1;
	char c1 = 'a', c2;

	i1 = c1;
	c2 = i1;
	printf("c1=%d\tc2=%d\ti1=%d\n", c1, c2, i1);
	getch();
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3IsGtexR-1631880972591)(…/char和int类型转化实例.png)]

以下代码将float类型转换为int类型:

#include<stdio.h>
int main() {
	int i1;
	float f1 = 6.18, f2;

	i1 = f1;
	f2 = i1;
	printf("f1=%f\tf2=%f\ti1=%d\n", f1, f2, i1);
	getch();
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BsdRczis-1631880972592)(…/float类型转换为int类型.png)]

注意:代码中将float类型数据赋值给int类型时,小数部分被截掉,然后将int类型数据赋值给float类型数据时,变量i1中只存在整数部分数据。

2.9.2 强制数据类型转换

在C语言表达式中,强制数据类型转换就行司令一样,可使用强制类型转换来显示转换数据类型。强制类型转换是通过类型转换运算来实现的。表达式为:

(类型名) (表达式)

以上格式将表达式的运行结果转换成类型名所规定的类型。类似于先将表达式的结果指定给一个指定类型的变量,然后再用该变量来参与运算。

以下演示强制类型转换实例:

#include<stdio.h>

int main()
{
	float f = 6.18;
	printf("f=%f\t(int)f=%d\n", f, (int)f);
	getch();
	return 0;
}

第6行代码通过(int)f将变量f强制转换为整数输出。转换过后并不会改变f的值,仍然为原来的6.18。

2.10 逻辑型

C99中增加了_BOOl类型,可以用来存储数据值0和1。_Bool类型是一个整数类型。

2.11 复数类型(_Complex和__Imaginary)

许多科学和工程计算都需要复数和虚数,复数是具有一个实部和一个虚部,实部就是普通的实数,虚部就是一个虚数,虚数是-1的平方根的倍数,复数常写做以下格式:

2.2+1.8i;

其中i表示-1的平方根。

C99标准支持这些类型,提供了关键字_Complex和__Imaginary,以及附加的头文件和几个新的库函数。

复数类型有3种,分别是:

  • float _Complex
  • double _Complex
  • long double _Complex

注意:基本数据类型和_Complex之间有一个空格。

float _Complex类型的数据在保存时需要采用两个相邻的float内存空间,实部的值保存在第一个元素中,虚部的值保存在第2个元素中。

C99标准也支持3种虚数类型,分别是:

  • float __Imaginary
  • double __Imaginary
  • long double __Imaginary

2.12 拓展训练

2.12.1 训练一:变量初始化

#include<stdio.h>

int main()
{
	int a, b;
	a = 2;
	b = 1;
	printf("a=%d,b=%d\n", a, b);
	return 0;
}

2.12.2 不同数据类型转换为十进制

#include<stdio.h>

void main()
{
	float a = 3.14;
	char b = 'a';
	printf("a=%d,b=%d\n", (int)a, (int)b);
}

3. C语言代码的基本组成

3.1 表达式

表达式是由操作数和运算符共同组成的,能够返回一个具体数值的式子。表达式中作为运算的数据称为操作数。操作数可以是常量、变量、函数表达式;运算符是介于操作数之间的运算符号,如+、-都是典型的操作符。

3.1.1 简单表达式

C语言中,最简单的表达式就是单独的一个变量、常量。例如:

A
1o
MAX
'c'

在简单的表达式中,字面常量返回的值就是其本身,变量返回的值为该变量的值,符号常量返回的为其定义的值。

注意:在C语言中,更多的表达式是由一个或多个运算符连接的。

例如:

3.14*2*2
PI*2*r

3.1.2 逗号表达式

在C语言中,逗号既可以作为分隔符,又可以用在表达式中。逗号作为分隔符使用时,用于间隔说明语句中的变量或函数中的参数。例如,在变量的声明中或者函数中作为参数的分隔符。

将逗号用在表达式中,可将若干个独立的表达式连接在一起,组成逗号表达式。逗号表达式的一般形式是:

表达式1,表达式2,...,表达式n

逗号表达式的运算过程是,先计算表达式1的值,然后计算表达式2的值,一直计算到最后一个表达式n,并将表达式n的值作为逗号表达式的值返回。逗号表达式的经典型的应用是在for循环中。

以下是逗号表达式的演示:

#include<stdio.h>

int main()
{
    int i;

    printf("%d\n", ((i = 5 * 2, i * 3), i * 5));

    return 0;
}

运行以上代码输出结果为50。

3.2 运算符

运算符是表示一个特定的数学或逻辑运算符号。C语言提供了非常多的运算符,通过数量众多的运算符构成丰富的表达式,使C语言的功能十分完善,这是C语言的主要特点之一。

3.2.1 运算符概括

在数学运算中,运算符是由优先级的,如在同一算式中先乘除后加减。与此类似的,C语言中运算符也是有优先级的。C语言中除算术运算符之外,还有其他很多独立的运算符,一些不同性质的运算采用了相同的运算符,这就为掌握运算符的运算规律带来了一定的难度。

C语言中的运算符不仅具有不同的优先级,而且还有一个特点,就是它的结合性。在表达式中,各运算量参与运算的先后顺序不仅要遵循运算符的优先顺序的规定,还要受运算符结合性的制约,以便确定是从左到右进行运算还是从右到左进行运算。这种结合性是其他高级语言的运算符所没有的,因此也增加了C语言的复杂性。

C语言运算符按其功能分为算数运算符、赋值运算符、逗号运算符、逻辑运算符、关系运算符、位逻辑运算符、条件运算符、括号运算符、地址运算符、成员访问运算符、sizeof()运算符等;按其要求参与运算的操作数的数目可以分为单目运算符、双目运算符、三目运算符(也称多目运算符)。

3.2.2 算数运算符

目数运算符功能操作示例
双目+加法两个操作数相加a+b
双目-减法第一个操作数减去第二个操作数a-b
双目*乘法两个操作数相乘a*b
双目/除法第一个操作数除以第二个操作数a/b
双目%取余第一个操作数除以第二个操作数所得余数a%b
单目-取负操作数乘以-1-a
单目++自增1操作数加1a++,++a
单目自减1操作数-1a–,--a
1. 双目运算符

在C语言的5个双目算数运算符中,加减乘除运算符的操作数都与数学中的含义相同,只是使用*表示乘号,使用/表示除号,这种表示方法在所有程序设计语言中都是相同的。

C语言中增加了一个号求模运算符(%)。所谓的求模运算,是指将第一个操作数除以第二个操作数得到的余数。

注意:求模运算符的两个操作数都必须是整型数据,不能是实型数据。

当除号两侧的操作室整型时,结果也是整型,若是不能整除,那么余数将被舍弃。

若运算符两边的操作室的类型不同,则系统会自动进行转换,使操作数具有相同的类型后进行运算。

十进制数转换为二进制数的方法是除2取余法,即将十进制数反复除以2,取余数作为相应的二进制数最低位,再除以2得到余数作为二进制数的第二位,循环想除即可将其转换为一个二进制数。下面程序将使用求模和整除的方法将十进制数转换为二进制数。

#include<stdio.h>

int main()
{
	int t, t1, b0, b1, b2, b3, b4, b5, b6, b7;
	printf("请输入0-255之间的数字:");
	scanf_s("%d", &t);
	t1 = t;
	b0 = t % 2; //第1位二进制位
	t = t / 2;
	b1 = t % 2; //第2位二进制位
	t = t / 2;
	b2 = t % 2; //第3位二进制位
	t = t / 2;
	b3 = t % 2; //第4位二进制位
	t = t / 2;
	b4 = t % 2; //第5位二进制位
	t = t / 2;
	b5 = t % 2; //第6位二进制位
	t = t / 2;
	b6 = t % 2; //第7位二进制位
	t = t / 2;
	b7 = t % 2; //第8位二进制位
	t = t / 2;
	printf("十进制数:%d", t1);
	printf("转换为二进制数为:%d%d%d%d%d%d%d%d\n", b7, b6, b5, b4, b3, b2, b1, b0);
	return 0;
}
2. 单目运算符

单目运算符只需要一个操作数。比如取负运算符操作数设置为负数,如果操作数为负数,则得到的结果为正数。

C语言中更常用的两个单目运算符是其他高级语言中通常没有的,就是自增运算符和自减运算符。

自增运算符使变量的值自增1,自减运算符使变量的值自减1。例如:

i++;
J--;

使用自增自减运算符可以使代码更加的简单。使用自增自减运算符可以节省代码行,例如:

j=i++;

相当于:

j=i;
i=i+1;

注意:自增运算符和自减运算符的运算对象都只能是变量,不能是常量或表达式。例如:5++或++(i+1)。

自增自减运算符可以通过两种方式来使用:一种是后缀形式,即运算符出现在变量的后面,另一种是前缀模式,即预算符出现在变量前面。这两种模式的区别在于值的增减的时间不同。

  • 采用前缀模式时,先执行变量的自增或自减操作,再用计算的结果参与表达式的运算。
  • 采用后缀模式时,先使用变量参与表达式的计算,再对变量执行自增或自减的操作。

以下代码演示自增运算符的后缀模式和前缀模式的计算结果:

#include<stdio.h>
int main()
{
	int i = 0, j = 0;
	printf("变量i\t变量j\n");
	printf("%d\t%d\n", i++, ++j);
	printf("%d\t%d\n", i++, ++j);
	printf("%d\t%d\n", i++, ++j);
	printf("%d\t%d\n", i++, ++j);
	return 0;

}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J5xX4GvQ-1631880972593)(…/自增运算符的前缀后缀.png)]

3. 算数运算符的优先级
目数运算符操作优先级
双目+两个操作数相加3
双目-第一个操作数减去第二个操作数3
双目*两个操作数相乘2
双目/第一个操作数除以第二个操作数2
双目%第一个操作数除以第二个操作数所得余数2
单目-操作数乘以-11
单目++操作数加11
单目操作数-11

3.2.3 赋值运算符

C语言将赋值作为一种运算,并定义了赋值运算符。他的作用就是把一个表达式的值赋予一个变量。

注意:赋值运算符的左边必须是一个变量。

C语言的赋值运算符分为两类:简单赋值运算符;复合赋值运算符。下面分别介绍这2两类赋值运算符。

1. 简单赋值运算符

简单赋值运算符(=)的语法格式如下:

变量 = 表达式;

赋值运算符的作用是对右侧表达式进行计算,然后将计算结果保存到左侧指定的变量之中。左侧必须是变量,不能是表达式或者常量

赋值运算符“=“与在数学表达式中的符号"="的意义是不同的,赋值运算符表示的是内存空间的复制操作。

在C语言中,赋值运算符是一个运算符,因此可以出现在表达式中。例如:

count = (start = 98) + (end = 208);

以上是现将常量98赋值给start、常量208赋值给end,再将end和start相减的结果赋值给count,最后count的值是306。

2. 复合赋值运算符

C语言提供了许多的精简代码的运算符,在赋值运算符中,又将其他的运算符和赋值运算符组合,提供了复合赋值运算符。

复合赋值运算符是由各种常用运算符与赋值运算符组合而成的运算符,共有十种形式,如下表所示:

运算符名称等价关系
+=加赋值a+=b等价于a=a+b
-=减赋值a-=b等价于a=a-b
*=乘赋值a*=b等价于a=a*b
/=除赋值a/=b等价于a=a/b
%=取余赋值a%=b等价于a=a%b
&=位与赋值a&=b等价于a=a&b
|=位或赋值a|=b等价于a=a|b
^=位异或赋值a=b等价于a=ab
<<=位左移赋值a<<=b等价于a=a<<b
>>=位右移赋值a>>=b等价于a=a>>b

每个赋值运算符都由两个或三个字符组成,除赋值号以外,前面的字符都是一个运算符。

提示:在使用符合赋值运算符时,首先将右侧的表达式计算得到一个结果,再用这个结果与左侧变量的值进行相关运算。

复合赋值运算符实例:

#include<stdio.h>
int main()
{
	int i, j;

	i = 3;
	j = 2;
	i *= j + 2;

	printf("%d\t%d\n", i, j);
	return 0;
}

编译这个程序,变量i的值变为12,变量j的值还是初值2。

3.2.4 关系运算符

关系运算符用来判断两个操作数的大小关系。

在数学中,有6种关系运算符:小于、大于、小于或等于、大于或等于、等于、不等于。

在C语言中这六种关系运算符有所改变,如下表所示:

运算符含义实例
<小于a<b
<=小于或等于a<=b
==等于a==b
>=大于或等于a>=b
>大于a>b
!=不等于a!=b

通过关系运算符计算得到的结果只有两种:true或false。其中true表示指定的关系成立,而false表示指定的关系不成立。

在C语言中,true与yes或1相同,false与no或0相同。在以后的程序中可以看出,只要是非0值,C语言都将其解释为true,为0解释为false。

在使用关系运算符进行比较时,还需要注意等于和不等于这两个运算符,一般都只用在整型数据中,而不将其应用到实型数据中。这是因为实型数据保存的值存在误差,有可能两个应该相等的数不相等。

关系运算符的实例:

#include<stdio.h>
int main()
{
	float f;

	f = 1.0 / 3;
	printf("%f\n", 3 * f);
	printf("%d\n", (3 * f == 1));
	return 0;
}

3.2.5 逻辑运算符

使用关系运算符可以对两个操作数进行比较,如果需要多个关系表达式的结果合并在一起,则需要使用逻辑运算符。

i>=0 && i<=10;

如果变量i的值在0-10之间,那么以上表达式的结果就是true,否则为false。

C语言提供了三种逻辑运算符,如下表所示。与关系运算一样,逻辑运算的结果为true或false。

逻辑运算符含义解释示例
&&逻辑与若两个操作数都是true,则结果为true5>3&&i<5
||逻辑或若两个操作数有一个是true,则结果为true5>3||i<5
逻辑非操作数为true,结果为false;操作数为false,结果为true!(i<5)

注意:在逻辑运算符中,逻辑与和逻辑或都是双目运算符,逻辑运算的结果为true或false。

设有两个变量a和b,用来保存逻辑量true和false,变量a和b可以进行上表的所列的3种逻辑运算。因逻辑量只有两个值,所以3种逻辑运算的结果也能确定。将这些内容用一张表格来表示,就是逻辑运算的“真值表”,如下表所示:

ab!aa&&ba||b
truetrueflasetruetrue
trueflaseflaseflasetrue
flasetruetrueflasetrue
flaseflasetrueflaseflase

由此表可以看出:

  • 对于逻辑非运算,结果总是与操作数相反。
  • 对于逻辑与运算,只有两个操作数都为true时,结果才会是true。
  • 对于逻辑或运算,只有两个操作数都是flase时,结果才会是false。

注意:3个逻辑运算符的优先级:! > && >||。

下面程序演示逻辑运算符的使用:

#include<stdio.h>
int main()
{
	float f = 6.18;
	char c = 'h';
	int i = 3, j = 0;

	printf("i&&j=%d\n", i && j);
	printf("i||j+2&&c=%d\n", i || j + 2 && c);
	printf("!f=%d\n", !f);

	return 0;
}

3.2.6 位运算符

与其他高级语言相比较,位运算是C语言一个比较有特色的地方,利用位运算可以实现许多汇编语言才能实现的功能。

所谓位运算,是指进行二进制的运算。C语言提供的位运算符如下表所示:

位运算名称位运算名称
&按位与~取反
|按位或<<左移
^按位异或>>右移

位运算符中除了~是单目运算符以外,其余都是双目运算符。

警告:位运算所操作的操作数只能是整型或字符型数据以及它们的变体。位运算不能用于float、double、long或其他更复杂的数据类型。

3.2.7 条件运算符

通过条件运算符把3个表达式连接在一起,组成条件表达式。条件表达式的语法格式如下:

表达式1?表达式2:表达式3

由以上格式可以看出这是一个三目运算符,通过问号和冒号将三个表达式分开,这是C语言中唯一一个三目运算符。

条件表达式的运算过程是:先计算表达式1的值,如果它的值为true,则将表达式2的值作为条件表达式的值,若表达式1的值为false,则表达式3的值作为条件表达式的值。

使用条件表达式可以替代简单的if语句,下面程序让用户输入3个整数,求出这3个整数的最大数:

#include<stdio.h>
int main()
{
	int a, b, c;
	int max;

	printf("请输入第一个整数:");
	scanf_s("%d", &a);
	printf("请输入第一个整数:");
	scanf_s("%d", &b);
	printf("请输入第一个整数:");
	scanf_s("%d", &c);

	max = (a > b ? a : b);
	max = (max > c ? max : c);

	printf("最大数是:%d\n", max);
	return 0;
}

在条件表达式中,可能表达式2和表达式3结果值的类型不同,这时C编译器会自动进行转换。例如:

a>0?6.18:3

在以上表达式中,表达式2的类型为实型,而表达式3的类型为整型。这时,C编译器将整数3转换为3.0,最后计算结果返回值为实型。

3.2.9 其他运算符

除了上面介绍的各种运算符,C语言还有以下的运算符。

  • sizeof:长度运算符(单目运算符)
  • []:下标运算符
  • .:结构体成员运算符
  • ->:指向结构体成员运算符
  • *:指针运算符(单目运算符)
  • &:取地址运算符(单目运算符)
1.5 sizeof长度运算符

sizeof长度运算符是C语言中一个比较特殊的运算符。大部分程序设计语言中的运算符都是由一个或几个特殊符号构成的,看起来sizeof不像一个运算符,更像一个函数。但是sizeof确实是C语言中的一个运算符。

简单地说,使用sizeof运算符可以返回一个数据类型所占的内存字节数。其操作数可以是数据类型、变量、常量。例如,以下程序是分别使用sizeof计算数据类型、常量、变量的字节长度。

#include<stdio.h>
int main()
{
	float f = 5;

	printf("%d\n", sizeof(float));
	printf("%d\n", sizeof(5.0f));
	printf("%d\n", sizeof(f));

	return 0;
}

编译运行以上的程序,可以看到输出的数字都是4。

2. []下标运算符

下标运算符在数组中使用。

3. .结构体成员运算符和->指向结构体成员运算符

这两个运算符将在结构总使用。

4. *指针运算符

指针运算符主要对指针进行操作。

5. &取地址运算符

在使用scanf()函数的时候接收用户的输入时,需要在变量名前添加&运算符。

3.3 表达式的运算顺序

在C语言中,表达式可以由各种类型的操作数和运算符组成。对应这种由多个运算符连接的表达式,必须知道C对运算符操作的顺序,才能确切的知道表达式最终的结果。

3.3.1 运算符优先级

前面在介绍算数运算符时提供了运算符的优先级,当表达式中有多种类型的运算符混合在一起时,就需要对C语言的所有运算统一规定优先级。

在C语言中,运算符的运算优先级分为15级,1级最高,15级最低。在表达式中,优先级高的先运算。运算符的优先级如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wIB6tOMs-1631880972593)(file:///C:\Users\26217\AppData\Roaming\Tencent\Users\2621715498\QQ\WinTemp\RichOle\OQFA0@F7T]VQK~Y9YA}YQVD.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sVbolNvl-1631880972595)(file:///C:\Users\26217\AppData\Roaming\Tencent\Users\2621715498\QQ\WinTemp\RichOle\20$4Y@4C00@EW5CVZBK}FUU.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MAXOR0Q9-1631880972596)(file:///C:\Users\26217\AppData\Roaming\Tencent\Users\2621715498\QQ\WinTemp\RichOle%FSUWRVIR(ASU_R)]N84E4.png)

优先级为1的圆括号就是用来改变其他运算符优先级的,即如果需要将具有较低有优先级的运算符先运算,则可以将该运算符及其操作数用圆括号括起来。

3.3.2 运算符的结合性

在表达式中,当一个操作数两侧都有运算符,且两个运算符的优先级相同时,C语言按照什么顺序来运算表达式呢?例如:

a+b-c

操作数b的左侧为加号,右侧为减号,具有相同的优先级。对于这个表达式,都知道是从左到右顺序,先算加法再算减法,这种方式称为左结合,大部分都采用这种方式。

而以下这种表达式:

a=b=c

操作数b的左右两侧都是赋值运算符,在C语言中规定将先运算右侧的赋值操作,这种从右到左的顺序进行运算的方式称为右结合。

3.3.3 自增、自减运算符注意事项

1. 操作数的类型

自增、自减运算符的操作数只能是变量,而不能是常量或表达式。因为自增、自减运算符具有对运算量重新赋值的功能。整型、实型、字符型、枚举型都可以作为这两个运算符的操作数。

2. 操作符产生歧义

在C语言中,操作符的使用想当灵活。但是由于这种灵活性也可能使程序产生歧义,甚至出现在不同的C编译器中运算的结果不相同的现象。

int a, b;
a = 4;
b = a+++a+++a++;

执行以上程序后,变量a和变量b的值各为多少?

  • 第一种情况:变量b的值为12(4+4+4),变量a的值为7。
  • 第二种情况:变量b的值为15(4+5+6),变量啊的值为7。

警告:在程序中,类似于a+++a+++a这种连续自增、自减的运算最好不要使用,以免出现歧义,同时也减少程序出错的可能性。

3.4 语句

语句是C程序的基本成分,一条语句是一条完整的计算机指令。在C语言中,每条语句都必须使用一个分号作为结束符。

3.4.1 语句书写方式

C语言的语句限制条件很少,最基本的一条就是每条语句都必须以一个分号结束。用户可以很随意的、采用符号自己风格的方式排列C语言。与其他的程序设计不同,一条C语句可以排列在多行中。例如,有以下语句:

s=(a+b)*h/2;

也可以写成以下格式:

s=
(a+b)*h/2;

甚至可以写成以下格式:

s=
(a+b)
*h/2;

在C语言中,通过分号标识一条语句的结束,所以上面的两行或三行没有分号,表示这两行或三行为一条语句。

将一行语句按多行写不是一个好习惯,但是这样的语句是合法的。

同样,因语句使用分号作为分隔符,将多条语句写在一行中也是合法的。例如:

a=b;b=c;c=a;

一般情况下,C编译器将忽略语句中的空格、制表符和空行。因此为了使源程序的排列更加的美观,更利于程序的排错,一般使用以下规则输入源代码:

  • 在一行内只写一条语句,并采用空格、空行保证清楚的视觉效果
  • 每条复合语句使用一个Tab键缩进,复合语句的大括号放在最新的一行,单独成一行,便于检查源程序时进行匹配。

3.4.2 表达式语句

大部分高级程序设计语言中专门有赋值语句,而在C语言中将赋值作为一个操作符,因此只有赋值表达式。在赋值表达式后面添加分号,就成了独立的语句。

以下内容可以作为表达式语句:

10;
3.14*2*2;
2*a*b+3;

以上表达式语句从语法上讲是正确的,能被C编译器顺利编译,但是这些语句不对程序执行有任何有用的操作。一般表达式语句应能完成一个相应的操作,如修改变量的值、给变量赋初值、调用某个函数等。如以下的语句:

a+=10;
s=3.14*2*2;
printf("%d\n",a);

执行以上表达式语句,将对表达式进行求值,并将其赋值给某个变量。

3.4.3 空语句

在C语言中,如果一条语句只有一个分号,则称该语句为空语句。

空语句是什么也不执行的语句。在程序中的主要作用作为空循环体。例如:

while(getch() != '\n');

以上代码的功能是,只要从键盘输入的字符不是回车就要求用户重新输入。即要求用户按回车键才能继续执行后面的程序。在该部分代码中,接收用户按键、判断按键的内容都集中在while的判断中,因此,循环体中不再需要任何功能,就在循环体重输入一个空语句作为循环体。

3.4.4 复合语句

复合语句是由大括号中的0个或多个声明和语句共同构成的。在C99标准中语句和声明可以混合,在早期的C语言中则要求把声明放在语句之前。复合语句也被称为代码块。

复合语句可以放在能够使用语句的任何地方,大多数时候用在if或while等程序控制结构中。例如:

if (a[i]<a[j])
{
    temp=a[i];
    a[i]=a[j];
    a[j]=temp;
}

3.4.5 标号语句

C语言允许用户在每条语句的前面添加一个冒号,其格式如下:

标号:语句;

标号是C语言中的一个标识符,标号后面是一个冒号,冒号后面才是C语言的语句。

警告:标号不能单独出现,而要和语句相联系。

可以使用goto语句或switch分支语句将程序控制转移到编有标号的语句。标号语句有3种:

  • 命名标号:可在任何语句前面添加命名标号,使用goto语句可跳转到命名标号处。
  • case标号:只能出现在switch语句体中进行分支处理。
  • default标号:只能出现在在switch语句中进行分支处理。

3.5 拓展训练

3.5.1 加1和减1运算符

#include<stdio.h>

int main()
{
	int x = 3, y = 5;
	int a = x++;
	int b = y--;
	int c = ++x;
	int d = --y;
	printf("%d,%d,%d,%d\n", a, b, c, d);
	return 0;
}

3.5.2 三目运算符

#include<stdio.h>

int main()
{
	int x = 5;
	int y = ++x ? 0 : 1;
	printf("%d,%d\n", x, y);
	return 0;
}

5.2.3 逗号运算符

注意:逗号运算符可以将多个表达式组合在一起,其返回值为逗号右边的值。

例如:

y=(x=1,x+2);

首先将1赋值给x,再进行加2操作,最后将逗号右边的值赋值给y,即3。

逗号运算符本质上是将多个运算式组合在一起,从左至右逐个运算,然后将逗号右边的值赋给左边的变量。例如

x=1;
y=(x=x+2,x=x*3,x-5);

根据给出的数据,对其进行算术运算和逻辑运算。

#include<stdio.h>

void main()
{
	int x = 0, y = 1, z;
	float a = 5.7;
	char b = 'z';
	z = (++x) && (--y);
	printf("%d\n", z);
	z = a - y;
	printf("%d\n", z);
	z = b;
	printf("%d\n", z);

}

4. 控制输入/输出的形式

4.1 格式化输出----printf()函数

C程序运算的结果保存在内存中,必须将其输出到指定设备,才能让用户了解程序的运行结果。显示器就是计算机的标配设备,将程序的运行结果输出到显示器是最常用的方法。

4.1.1 printf()函数输出格式

在前面已经多次使用到了printf()函数,它的作用是向显示器输出多个任意类型的数据。

该函数包括两部分参数,其作用如下:

  • 格式字符串:用双引号括起来的字符串,在该字符串中可以只包含普通字符,这时printf()函数将其原样输出在屏幕上;还可以包含以%开头的格式字符,他它的作用就是将输出的数据转换为指定的格式输出。格式说明总是由%字符开始的,printf()函数的格式字符有很多。
  • 输出列表:是需要输出到屏幕的数据,可以是常量、变量或表达式。若为表达式,则将输出表达式的值。

4.1.2 printf()函数的格式化字符

在printf()函数的第一个参数必须是以%开头的格式化字符,才能将输出中的表达式按规定的格式输出。对于输出同一个值,使用不同的格式字符将输出不同的结果。

格式字符功能
%a使用科学计数法将实数按十六进制输出,阶码前字符为p
%A使用科学计数法将实数按十六进制输出,阶码前字符为P
%c输出一个字符
%d输出有符号十进制整数
%e使用科学计数法将实数输出,阶码前字符为e
%E使用科学计数法将实数输出,阶码前字符为E
%f输出十进制实数
%g阶码小于-4或者超过指定精度时使用%e,否则使用%f格式输出
%G阶码小于-4或者超过指定精度时使用%E,否则使用%f格式输出
%i输出有符号十进制整数
%o输出无符号八进制整数
%p输出指针
%s输出字符串
%u输出无符号十进制整数
%x输出十六进制整数(字母小写)
%X输出十六进制整数(字母大写)
%%输出百分号

4.1.3 修饰符

printf()函数的格式字符串用于指定输出格式。格式字符是以%开头的字符串,在%后面接着各种格式字符,以说明输出数据的类型。在%和格式字符之间还可以使用一个或多个修饰符,对输出的格式进行进一步控制,如控制对齐方式、输出长度、小数位数等。

对printf()函数的格式字符进行拓展,加入修饰符后的一般形式如下:

[标志][输出最小宽度][.精度][长度]类型
1. 标志

共有五种标志,可以使用0个或者多个标志。

标志功能
-输出左对齐,右边填空格
+输出数据项的符号
(空格)输出数据为正数时,则显示为空格;为负数时,则显示为负号
#使用格式字符的可选形式。对格式字符%o,在输出时加前缀0;对格式字符%x或%X,在输出时加前缀0x或0X;对所有实数,#保证计时不跟任何数字,也输出一个小数点
0对所有数字格式,用前导0而不是空格填充字段宽度。若有标志-标志或指定了精度,则忽略本标志。
2. 输出最小宽度

用十进制整数来表示输出的最少位数,有实际位数多余定义的宽度,则按实际位数输出,若实际位数少于定义的宽度,则补以空格或0。

printf()函数输出最小宽度控制实例:

#include<stdio.h>

int main()
{
	int i = -78, j = 65535;

	printf("%d %d\n", i, j);
	printf("%4d %4d\n", i, j);      // 输出至少4个字符宽的整型变量
	printf("%-4d %-4d\n", i, j);    // 输出左对齐的4个字符宽的整型变量
	printf("%+4d %+4d\n", i, j);    // 输出带符号的4个字符宽的整型变量
	printf("%#x %#X\n", i, j);      // 输出带前缀的十六进制整数
	printf("%04d %04d\n", i, j);    // 输出至少4个字符的宽度的整型变量,不足补0
	printf("% d % d\n", i, j);      // 输出修饰符空格输出变量
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VMHqhzM1-1631880972598)(…/printf函数输出最小宽度实例.png)]

3. 精度

精度修饰符以“.”开头,后跟十进制整数。该修饰符的意义:如果输出的是整数,则表示输出的最小位数,若输出的位数小于该值,则将添加前置0;如果输入的是实数,则表示小数点的位数;如果输出的是字符,则表示输出字符的个数,若实际位数大于所定义的位数,则截取超过的部分。

#include<stdio.h>

int main()
{
	int j = 65535;
	float f = 3.1415926;
	printf("j = %.8d\n", j);
	printf("f = %.2f\n", f);
	printf("f = %07.2f\n", f); // 输出至少7个字符宽度且精度为2的实型变量f
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AM2KX813-1631880972599)(file:///C:\Users\26217\AppData\Roaming\Tencent\Users\2621715498\QQ\WinTemp\RichOle\55]SL0SHH1{AV814}MOVD.png)]

4. 长度

这里所说的长度是指输出数据所占用的内存的长度,其实就是输出指定类型的数据。在正常情况下,使用%d格式字符输出int类型的数据。若要输出long类型的数据,则需要使用%ld,其中l是长度修饰符。可以使用如下表所示的长度修饰符:

长度继续师傅意义
h和整数格式符一起使用,表示一个short int或者unsigned short int类型的数据
hh和整数格式符一起使用,表示一个signed char或者unsigned char类型数据
l和整数格式符一起使用,表示一个long int或者unsigned long int类型的数据
ll和整数格式符一起使用,表示一个long long int或者unsigned long long int类型的数据
L和实数格式符一起使用,表示一个long double类型数据

现在大部分是32位字长的计算机系统,int和long类型使用相同的字节数,在printf()函数中可以使用%d输出这两种类型的数据,也可以使用%ld输出long类型的数据。使程序在移植到16位字长的计算机系统中也可以正常使用。

对于short类型和char类型的数据,使用%d也可以正常输出。

3.1.4 printf()函数实例

1. 输出字符

使用格式字符"%c"可将整数类型的数据输出为一个对应的ASCII字符。char类型的取值范围为-128~127,正好对应一个ASCII字符。

printf()函数输出字符实例:

#include<stdio.h>

int main()
{
	int i = 75, j = 65535;
	char c = 'w';

	printf("整型数:%d,输出字符:%c\n", i, i);
	printf("整型数:%d,输出字符:%c\n", j, j);
	printf("字符:%c,输出为整型数:%d\n", c, c);

	return 0;

}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t0w3dFTJ-1631880972600)(…/…/AppData/Roaming/Typora/typora-user-images/image-20210715210239838.png)]

2. 输出整数

printf()函数输出整数的格式字符有很多个,如%d,%u,%o,%x或%X,对这些格式字符还可以添加标志、宽度、长度等修饰符。

#include<stdio.h>

int main()
{
	int i = 75, j = -65355;

	printf("整型数:%6d和%6d,输出为无符号数:%12u和%12u\n", i, j, i, j);
	printf("整型数:%6d和%6d,输出为八进制数:%#12o和%#12o\n", i, j, i, j);
	printf("整型数:%6d和%6d,输出为十六进制数:%#10x和%#10x\n", i, j, i, j);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3jU4Yhie-1631880972601)(…/printf函数输出整数.png)]

3. 输出实例

printf()函数输出实数的格式字符有很多个,最常用的就是%f、%e(或%E)、%g(%G)。对于这些格式字符也可以添加标志、宽度、长度等修饰符。

#include<stdio.h>

int main()
{
	float f = 130.79069388;
	double d = 13198317000.2682882;
	printf("f=%f,%5.4f,%e,%g\n", f, f, f, f);
	printf("d=%f,%8.4f,%E,%G\n", d, d, d, d);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e84oLFlQ-1631880972603)(…/printf函数输出实数.png)]

4. 输出字符串

在printf()函数中使用%s将字符串原样输出,如果字符串中包含转义字符,则将转义字符的意义进行操作,如果字符串中包含\n则进换行、包含\f则后面的内容将在下一个制表位输出。

可以添加以下修饰字符来控制字符串的输出:

  • %ms:输出的字符串占m列。若字符串本身长度大于m,则突破m的限制,将字符串全部输出;若字符串本身长度小于m,则在左侧补空格。
  • %-ms:如果字符串本身长度小于m,则在m范围内,字符串向左对齐,右侧补空格。
  • %m.ns:输出占m列,但值输出字符串中左侧的n个字符。这n个字符输出在m列右侧,左侧补空格。
  • %-m,ns:其中m、n的含义同上,即n个字符输出在m列范围的左侧,右侧补空格。如果n>m,则m自动取n值,即保证n个字符正常输出。
#include<stdio.h>
#define STR "Hello,World!"

int main()
{
	printf("%s*\n", STR);
	printf("%15s*\n", STR);
	printf("%-15s*\n", STR);
	printf("%15.5s*\n", STR);
	printf("%-15.5s*\n", STR);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lKSQ9HL2-1631880972604)(…/…/AppData/Roaming/Typora/typora-user-images/image-20210715215802141.png)]

4.1.5 动态设置输出宽度和精度

在printf()函数中,通过在格式字前面添加相应的修饰符可以控制输出最小宽度和精度。一般情况下,在程序编码阶段就设置好这两个参数,例如:

printf("%15.5f\n",f);

以上语句中设置输出变量f时,至少占用15个字符的宽度,小数位数保留5位。这是常用的表示形式。其实,对于输出宽度和精度,还可以使用"*"来代替,这时,printf()函数将使用输出列表中对应的数据作为输出宽度和精度,格式入下:

printf("%*.*f*\n",m,n,f);

其中的变量m对应第一个星号,设置输出宽度,变量n对应第二个星号,设置输出精度。

#include<stdio.h>

int main()
{
	int m, n;
	float f = 130.79069388;
	
	printf("设置数据最少输出宽度值:");
	scanf_s("%d", &m);
	printf("设置数据输出的小数位数:");
	scanf_s("%d", &n);
	printf("%*.*f\n", m, n, f);
	return 0;

}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E5dEhSaw-1631880972605)(…/…/AppData/Roaming/Typora/typora-user-images/image-20210715221518312.png)]

4.1.6 printf()函数的返回值

提示:大多数程序都不处理printf()函数的返回值。在一些特殊情况下,需要检测printf()函数是否执行成功,可使用一个变量来保存其返回值。

C函数一般都只有一个返回值,返回值使用由函数返回给调用程序的一个值。printf()函数也有一个返回值,正常情况下返回打印输出的字符数目。如果printf()函数执行出错,则返回一个负数。

#include<stdio.h>
#define STR "Hello,World!"

int main()
{
	int i;

	i = printf("%s\n", STR);
	printf("上一行代码输出了%d个字符\n", i);
	getch();
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-grmpTiZ9-1631880972606)(…/…/AppData/Roaming/Typora/typora-user-images/image-20210715222252204.png)]

调用printf()函数输出字符串常量STR,并返回输出的字符数量。打印的上一行代码的输出的字符数量。字符常量STR保存的可见字符为12个,C将自动为字符串添加一个结束标志,因此共有13个字符。

4.1.7 理解输出列表

printf()函数的输出列表可以有多个表达式,其数量至少应该与格式字符串中的格式字符相匹配。若多余格式字符的数量,则多余表达式的值不会被输出;若少于格式字符的数量,则可能输出一些无意义的值。

注意:使用printf()函数的时候,编译器将传递给函数printf()的参数存入一个称为堆栈的内存区域。堆栈中的数据采用“后进先出”的方式,即最后保存的数据将最先被读取出来。因此,printf()函数总是将参数按从后往前的顺序放到堆栈中。

例如:

printf("%f,%d\n",f,i,j);

调用以上语句的时候,先将变量j的值存入堆栈,再将变量i的值存入堆栈,再将变量f的值存入堆栈,最后将第一个参数(格式字符串)存入堆栈。进入函数printf()执行其代码时,首先对第一个参数(格式字符串)进行分析,每找到一个格式字符就从堆栈中取一个输出值。这样,若格式字符的数量和输出表达式的数量匹配,则函数printf()能够顺利输出所有值;若格式字符的数量比输出表达式的数量多,则函数printf()取完装入堆栈中的值后,还将继续从堆栈中取值,所以将得到一个无意义的值;若格式字符的数量比输出表达式的数量少,则多余的值将被留在堆栈中不进行处理。例如以下程序输出i的值:

#include<stdio.h>

int main()
{
	int i = 3;
	printf("%d\t%d\t%d\t%d\n", ++i, --i, i--, i++ );
	return 0;
}

当格式字符与输出列表中的值的类型不匹配时会输出杂乱的毫无意义的值:

#include<stdio.h>

int main()
{
	int i = 10;
	long l = 90;
	float f = 6.18;
	double d = 9.18;

	printf("%e,%e,%e,%e\n", i, l, f, d);
	printf("%ld,%ld\n", i, l);
	printf("%ld,%ld,%ld,%ld\n", f, d, i, l);

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BrtLztMe-1631880972607)(…/…/AppData/Roaming/Typora/typora-user-images/image-20210716104004112.png)]

4.2 格式化输入scanf()函数

4.2.1 scanf()函数的格式

scanf()函数被称为格式输入函数,使用该函数可以按指定的格式从键盘上把数据输入到指定变量中。

scanf()函数是一个标准的库函数,他的函数原型在头文件“stdio.h”中。scanf()函数的一般形式为:

scanf("格式字符串",地址列表);

其中格式字符串的作用与printf()函数相同;地址列表中给出各变量的地址。地址是由地址运算符&后面跟上变量名组成的。这个地址是编译系统在内存中给变量分配的地址,用户不用关心具体的地址是多少,只需要在scanf()函数的变量名前面加上&即可。

#include<stdio.h>

int main()
{
	int a, b;

	printf("请输入两个整数:");
	scanf_s("%d %d", &a, &b);

	printf("你输入的两个整数是:%d %d\n", a, b);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-se9Stdgv-1631880972609)(…/…/AppData/Roaming/Typora/typora-user-images/image-20210716110849507.png)]

4.2.2 scanf()函数的格式字符

scanf()函数的格式字符作用与printf()函数的相同,但比printf()函数相比,少了一个精度控制修饰符。

%[*][输入数据宽度][长度]类型
1. 类型

表示输入数据的类型,其格式字符和意义如下:

  • d或i:输入十进制数
  • o:输入八进制数
  • x或X:输入十六进制数
  • u:输入无符号十进制数
  • f、e、g:输入实型数
  • c:输入单个字符
  • s:输入字符串
2. 长度

长度格式为l和h,l表示输入长整型数据(如%ld)和双精度浮点数(如%lf),h表示输入短整型数据。

3. 宽度

注意:用十进制整数指定输入的宽度(字符数)。达到指定宽度后,后面数据将赋值给下一个变量。

例如:

scanf("%4d",a);

如果输入以下内容:

123456789

则scanf()函数只会将1234保存到变量a中,其余部分被舍弃。

如果有多个字符使用宽度限制,如以下代码:

scanf("%3d%3d",&a,&b);

同样输入123456789,则scanf()函数将123保存到变量a当中,456保存到变量b当中,其余部分将被舍弃。

4. 星号

在格式字符中使用星号,表示从输入缓冲区读取该数据项后不赋予相应的变量,即跳过该输入值:

scanf("%d%*d%d",&a,&b);

当输入为“12 34 56”时,scanf()函数将输入的第一个数12保存到变量a当中,将第2个数34跳过,将第3个数56保存到变量b当中。这种方式在处理已有的一批数据而又需要跳过部分数据时很有用。

4.2.3 scanf()函数的注意事项

1. 不能控制输入精度
#include<stdio.h>

int main()
{
	float f1, f2;

	printf("请输入实数f1:");
	scanf_s("%5f", &f1);

	printf("请输入实数f2:");
	scanf_s("%5.5f", &f2);

	printf("f1=%f,f2=%f\n", f1, f2);
	return 0;
}

编译以上代码VS2019在输入第2个变量时直接报错。如果换成其他的编译器可能输入完f1的值之后直接跳过f2的输入,直接printf()输出f1,f2的值,变量f1的值输出可能会有误差,f2的值输出可能为0或者其他不确定的值。

2.在格式字符串中包含非格式字符

前面介绍了在输入多个数值数据时,若格式字符串中没有非格式字符作为输入数据之间的间隔,则可以用空白字符(空格、Tab、回车符)作为间隔。scanf()在碰到空白字符或非法数据时即认为该数据结束。

警告:当格式字符串中包含非格式字符,则要求用户输入数据时也必须原样输入这些非格式字符。

如下:

#include<stdio.h>

int main()
{
	int a, b;
	
	printf("请输入两个整数:");
	scanf_s("a=%d b=%d", &a, &b);

	printf("你输入的整数是:a=%d,b=%d\n",a,b);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DLfcjLof-1631880972610)(…/…/AppData/Roaming/Typora/typora-user-images/image-20210716151225617.png)]

4.2.4 scanf()函数的返回值

scanf()函数的返回值,其返回值为成功读入数据的项目数。若在scanf()函数中需要读取一个整数,而用户却输入一整个字符串时,则scanf()函数将不能获得一个有效的数据,则返回值为0。

#include<stdio.h>

int main()
{
	int a, b, c, i;

	printf("请输入三个整数:");
	i = scanf_s("%d%d%d", &a, &b, &c);

	printf("你输入的整数是:a=%d,b=%d,c=%d\n", a, b, c);
	printf("你一共输入了%d个有效数据\n", i);

	return 0;

}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5FLjoyJv-1631880972611)(…/…/AppData/Roaming/Typora/typora-user-images/image-20210716152253489.png)]

4.3 其他常用输入/输出函数

4.3.1 getchar()函数

getchar()函数的功能是接收键盘输入的一个字符。函数原型如下:

int getchar(void);

该函数不需要任何的参数,从输入缓冲区中获取一个字符,并将其ASCII码作为返回值。通常定义一个字符变量来接收getchar()函数的返回值,例如:

char c;
c = getchar();

提示:当调用getchar()函数时,程序就等待用户输入字符,用户输入的字符被存放在输入缓冲区中,直到用户按回车键才结束。

getchar()函数返回用户输入的第一个字符的ASCII码,若执行过程中出错,则该函数返回-1,且将用户输入的字符返回到屏幕上。如果用户在按回车键之前输入了不止一个字符,那么其他的字符会保留在输入缓冲区,供后续函数读取。也就是说,当输入缓冲区中有未被读取完的字符时,后续的函数将不会等待用户输入字符,而直接读取输入缓冲区中的字符,直到输入缓冲区中的字符被读取完,才等待用户输入字符。

例如下面这个程序,等待用户输入字符,按回车键后在显示一个字符。若用户输入多个字符,则也只是显示一个字符。

#include<stdio.h>

int main()
{
	char c;

	printf("输入一个字符:");
	c = getchar();

	printf("你输入的字符是:%c\n", c);
	return 0;
}

4.3.2 getch()函数

getch()函数与getchar()函数的功能基本相同,都是接收用户输入的一个字符。其函数原型如下:

int getch(void);

提示:getch()函数直接从键盘获取键值,不等待用户按回车键,只要用户输入一个键,getch()函数就立刻返回。

getch()函数的返回值是用户输入的ASCII码,出错则返回-1。

使用getch()函数输入的字符不会显示在屏幕上。因此getch()函数常用与程序调试,在调试中,在关键位置显示有关结果以待查看,然后调用getch()函数暂停程序运行,当按任意键后程序继续运行。

注意:getchar()函数的原型位于stdio.h中,而getch()函数的原型位于conio.h头文件中。

#include<stdio.h>
#include<conio.h>

int main()
{
	char c;

	printf("按任意键继续\n");
	c = _getch();

	printf("按下的键是%c\n", c);
	return 0;
}

在vs2019中,getch()使用报错,此段代码中使用_getch()替代了getch()。

4.3.3 gets()函数

gets()函数用来从输入缓冲区中读取字符串,直至接收到换行符时停止,并将读取的结果存放到str指针所指向的字符数组中。换行符不作为读取字符串的内容,读取的换行符被转换为null值,并由此来结束字符串。其函数原型如下:

char *gets(char *string);

函数的参数string可以是一个字符指针或一个字符数组。如果是字符数组,则应该确保string的空间足够大,以便在执行读操作的时候发生溢出。

#include<stdio.h>

int main()
{
	char string[80];

	printf("请输入一个字符串:");
	gets(string);

	printf("输入的字符串是:%s\n", string);
	return 0;
}

用scanf函数的格式字符%s也可以接收用户输入的字符串。但是,如果输入的字符串中包含空格,则scanf()函数将只能接收到空格之前的数据,空格后的数据仍将保留在输入缓冲区中,而gets()函数将接收全部的数据保存到字符数组中。

提示:一般接收用户输入的字符串时,使用gets()函数更好。

4.3.4 putch()函数

putch()函数用于向屏幕中输出字符,其函数原型包含中conio.h头文件中,具体格式如下:

int putch(int ch);

例如:下面的语句输出小写字母a:

putch('a');

而下面的语句输出字符变量c所保存的字符:

putch(c);

使用putch()函数还可以输出控制字符(执行控制功能)。下面的语句执行换行操作

putch('\n');

对于控制字符,putch()函数将执行控制功能,不在屏幕上显示。例如。下面的程序使用多个putch()函数输出一个字符串:

#include<stdio.h>

int main()
{
	char c1 = 'G', c2 = 'o', c3 = 'o', c4 = 'd';

	putch(c1);
	putch(c2);
	putch(c3);
	putch(c4);

	return 0;
}

提示:在stdio.h头文件中还有一个putchar()的函数,也可以用来向屏幕输出一个字符,其在使用上与putch()函数没有什么区别。

4.3.4 puts()函数

puts()函数用来输出一个字符串到屏幕上,使用与printf()函数的格式字符%s完成相同的功能。其函数原型如下:

int puts(char *string);

与gets()函数类似,参数string可为字符数组或字符指针。

#include<stdio.h>

int main()
{
	char str[80];

	printf("请输入一个字符串:");
	gets(str);

	puts("输入的字符串是:");
	puts(str);
	return 0;
}

一个经常容易犯的错如下:

char c='w';
puts(c);

警告:在上面的程序中,打算使用puts()函数输出字符变量c的值。当运行上面的代码时,程序将出现错误。这是因为字符串必须以结束字符’\0’作为结束标志,而字符变量c只包含一个字符,没有结束标志,所以不能使用puts()函数来输出。

4.4 拓展训练

4.4.1 鸡兔同笼问题

鸡兔同笼问题:鸡、兔子关在同一只笼子里面,该笼中有35个头、94只脚,要求编程求出[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dZ2ME1AZ-1631880972613)(file:///C:\Users\26217\AppData\Local\Temp\SGPicFaceTpBq\2440\01906D56.png)]和[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5x4DogJB-1631880972614)(file:///C:\Users\26217\AppData\Local\Temp\SGPicFaceTpBq\2440\01909B0D.png)]各有多少只。

#include<stdio.h>

int main()
{
	int head = 35, feet = 94;
	int Chicken, Rabbit;

	Chicken = (4 * head - feet) / 2;
	Rabbit = (feet - 2 * head) / 2;

	printf("Chicken=%d,Rabbit=%d\n", Chicken, Rabbit);
	return 0;
}

4.4.2 格式化输入

格式化输入/输出函数的使用:从键盘输入圆柱体的底面半径和高,求圆柱体的体积。

#include<stdio.h>

int main()
{
	float r, h, V;

	printf("请输入圆柱体的底面半径:");
	scanf_s("%f", &r);
	printf("请输入圆柱体的高:");
	scanf_s("%f", &h);

	V = 3.14 * r * r * h;
	printf("圆柱体的体积为:%f\n", V);

	return 0;
}

4.4.3 输入两个变量的值,交换输出

#include<stdio.h>

int main()
{
	int a, b, temp;

	printf("请输入两个数:");
	scanf_s("%d%d", &a, &b);
	printf("交换之前两个变量的值为:a=%d,b=%d\n", a, b);
	temp = a;
	a = b;
	b = temp;
	printf("交换之后两个变量的值为:a=%d,b=%d\n", a, b);
	return 0;
}

4.4.4 大小写字母转换器

将键盘输入的小写字母转换为大写字母,并将其输出至屏幕。

分析:字符AZ的ASCII码值为6590,字符az的ASCII码值为97122。

#include<stdio.h>

int main()
{
	char c;
	int a;
	printf("请输入一个小写字符:");
	scanf_s("%c", &c);
	a = c - 32;
	printf("字符%c对应的大写字母是%c\n", c, a);
	return 0;
}

5. 程序的三大流程控制结构

5.1 顺序结构

顺序结构的程序流程设计是最简单的,只需要按照解决问题的顺序写出相应的语句即可,它的执行顺序是自上而下、依次执行。

顺序结构就是一条一条地从上到下执行语句,它会执行所有语句,而且只要是已经执行过的语句就不会再次执行。

顺序结构就行工厂流水线一样,不需要条件的判断,只会顺序执行。

5.2 条件语句

在前面的程序中,所有程序都是按语句在源代码中排列顺序执行的,写在前面的语句先执行,写在后面的语句后执行。在很多情况下,需要根据用户输入的值决定执行程序的哪一部分,而跳过另一部分,这样的程序结构被称为分支结构

C语言提供了if语句和switch语句来完成这样的功能,他们可以根据条件判断的结果选择所要执行的程序语句。它们就像程序的运行铁路上不同的岔路口,前进的方向是多样的,通过设定条件,在执行程序的时候便可以按照我们设想的方向运行。

5.2.1 if语句

C语言最常用的分支语句就是if语句,它根据给定的表达式的值进行判断,以决定执行某个分支程序段。

if语句的基本语法格式如下:

if (表达式) 语句:

以上语句的含义是:如果表达式的值为真,则执行其后面的语句;否则不执行该语句。

注意:if关键字后面的表达式通常是逻辑表达式或关系表达式,也可以是其他表达式,如赋值表达式,甚至可以是一个变量。

在if语句中,表达式即使是一个变量也必须用括号括起来。又来括号作为分隔符,if关键字与括号之间可以使用空格间隔,也可以不输入空格分隔。表达式与后面的语句之间的间隔与此类似,例如,以下两行语句都是正确的:

if(max<j)max=j;
if (max<j) max=j;

以上两种书写方式都没有语法错误,建议使用后一种方式,使程序的格式更规范。

用if语句求最大值:

#include<stdio.h>

int main()
{
	int i, j, max;

	printf("请输入两个整数:");
	scanf_s("%d %d", &i, &j);

	max = i;
	if (max < j) max = j;
	printf("你输入的数是%d和%d,最大值是%d\n", i, j, max);
	return 0;
}

上面程序的if语句表达式后面是一条语句,在更多情况下,当表达式的值为true时,需要执行读条语句,这时就需要使用代码块。

#include<stdio.h>

int main()
{
	int i, j, temp;

	printf("请输入两个整数:");
	scanf_s("%d %d", &i, &j);

	if (i > j)
	{
		temp = i;
		i = j;
		j = temp;
	}
	printf("你输入的数字从小到大的排序为%d %d\n", i, j);
	return 0;
}

5.2.2 if-else语句

5.2.1介绍的if语句在表达式的值为true时,先执行其后的语句或语句块,然后执行后面的语句,若表达式的值为false,则跳过其后面的语句或语句块,直接执行后面的语句,在更多的时候,针对表达式的值为true或flase的情况,需要先分别执行不同的语句,然后再执行后面的代码。这时,可以使用if-else语句,语法格式如下:

if (表达式) 语句1;
else 语句2;

以上的语句的含义是:如果表达式的值为true,则执行语句1,否则执行语句2。

通过if-else语句可以更方便的对条件进行处理。例如使用if-else语句对上面if语句的排程序进行修改:

#include<stdio.h>

int main()
{
	int i, j;

	printf("请输入两个整数:");
	scanf_s("%d %d", &i, &j);

	if (i>j)
		printf("你输入的数字从小到大的排序为%d %d\n", j, i);
	else
		printf("你输入的数字从小到大的排序为%d %d\n", i, j);

	return 0;
}

5.2.3 if-else-if语句

在5.2.2中使用了if嵌套的范式进行多个不同条件的组合。但如果嵌套的层次太多,则在代码执行的时候很容易出错。对于多个分支结构的情况,可以使用if-else-if语句。

使用前面介绍的if语句,主要用于处理两个分支结构的情况。使用嵌套形式也可以处理多个分支结构。C语言中专门提供了处理多个分支的if-else-if语句(此外还有一个switch语句也可以处理多个分支结构),表达式如下:

if (表达式1)
    语句1;
else if (表达2)
    语句2;
else if (表达式3)
    语句3;
...
    ...;
else if (表达式n)
    语句n;
else
    语句z;

其执行过程是:从上到下依次判断各表达式的值,当某个表达式的值为true时,则执行其对应的语句,然后跳到整个if-else-if语句之外继续执行程序;如果所有表达式的值都为false,则执行语句z,然后继续执行后续语句。

下面的例子检查用户输入的字符是大写字母、小写字母、数字还是其他字符。用户的输入有四种可能,可以通过字符的ASCII码值进行判断,确定输入字符属于哪种类型。

#include<stdio.h>

int main()
{
	char c;
	
	printf("请输入一个字符:");
	scanf_s("%c", &c);

	if (c >= 65 && c <= 90)
		printf("你输入的是大写字母%c\n",c);
	else if (c >= 97 && c <= 122)
		printf("你输入的是小写字母%c\n",c);
	else if (c >= 48 && c <= 57)
		printf("你输入的是数字%c\n",c);
	else
		printf("你输入的是其他字符%c",c);

	return 0;

}

5.2.4 if语句的嵌套

使用if-else语句语句可以处理有两个分支结构的情况,如果有多个分支结构要处理,就需要使用if语句的嵌套格式。

所谓嵌套,就是在if语句后面执行的语句中又包含if语句。if语句嵌套的一般形式表示如下:

if (表达式1)
{
    if (表达式2) 语句1;
}
else
    if (表达式3) 语句2;

在更加复杂的情况下,在嵌套内的语句又是if-else语句,其结构如下:

if (表达式1)
{
    if (表达式2) 语句1;
	else 语句2;
}
else
    if (表达式3) 语句3;
	else 语句4;

接下来是一个复杂的例子。用户输入3个整数,使用if嵌套的方式对这3个数进行排序,并按从大到下的书序输出。

用户输入3个数字有6种组合:

  • 大、中、小
  • 大、小、中
  • 中、小、大
  • 中、大、小
  • 小、中、大
  • 小、大、中

假设输入的3个整数分别保存在3个变量a、b、c中,使用if嵌套分别判断两个数的大小,的到以上6种组合。

#include<stdio.h>

int main()
{
	int a,b,c;

	printf("请输入3个整数:");
	scanf_s("%d %d %d", &a, &b, &c);

	if (a > b)
		if (b > c)
			printf("%d,%d,%d\n", a, b, c);
		else
			if (a>c)
				printf("%d,%d,%d\n", a, c, b);
			else
				printf("%d,%d,%d\n", c, a, b);
	else
		if (b<c)
			printf("%d,%d,%d\n", c, b, a);
		else 
			if (c>a)
				printf("%d,%d,%d\n", b, c, a);
			else
				printf("%d,%d,%d\n", b, a, c);

	return 0;
}

编译以上程序,输入之前分析的6种输入数据组合,可以看到最后都是输出从大到小排列的3个数据。

以上程序主要为了演示if嵌套语句而编写。在实际过程中,使用一个中间变量对数据进行交换即可,可以很简单的完成以上程序。

#include<stdio.h>

int main()
{
	int a, b, c, temp;

	printf("请输入3个整数:");
	scanf_s("%d %d %d", &a, &b, &c);

	if (a < b)
	{
		temp = a;
		a = b;
		b = temp;
	}
	if (a < c)
	{
		temp = a;
		a = c;
		c = temp;
	}
	if (b < c)
	{
		temp = b;
		b = c;
		c = temp;
	}

	printf("%d > %d > %d\n", a, b, c);

	return 0;
}

提示:以上程序中,也可以将每个语句块中的3条交换数据的语句写在同一行。

5.2.5 布尔表达式

等式是最简单的布尔表达式。这种布尔表达式用于测量两个值是否相等。

布尔表达式是逻辑运算符和布尔运算量按照一定语法规则组成的式子。逻辑运算符通常有与、或、非。在某些语言中,还包括等价(≡)以及蕴含(→)等。

逻辑运算对象可以是逻辑值(true或false)、布尔变量、关系表达式以及由括号括起来的的布尔表达式。

不论是布尔变量或是布尔表达式,都只能去逻辑值true或false。在计算机内真值通常用1来表示,假值通常用0来表示。

关系表达式是形如E1 Rop E2的式子,其中E1、E2为简单算术表达式。Rop为关系表达运算符(<、>、=、<=、>=等等)。若E1和E2的值使该关系式成立,则此关系表达式的值为true,否则为false。

5.2.6 增强后的switch分支语句

一般的if语句只有两个分支可供选择,而实际工作中常常要用到多个分支的选择。使用if-else-if语句也可以处理多个分支的情况,但是多数情况下,使用switch语句处理多个分支更加的方便。

switch语句就像许多扇大门一样,上面标注了符合什么条件才能通过。

使用switch语句可以方便处理同一表达式有多个分支的情况。语法格式如下:

switch (表达式)
{
    case 常量表达式1:
        语句1;
        break;
    case 常量表达式2:
        语句2;
        break;
        ...
    case 常量表达式m:
        语句m;
        break;
    default:
        语句n;
}

其执行过程为:首先计算表达式的值,然后将该值与后面的每个case关键字的常量表达式进行比较,当表达式的值与某个常量表达式的值相等时,即执行其后的语句,然后不再进行判断,继续执行所以case后的语句,直到遇到break语句后退出switch语句。如果表达式的值与所有case后的常量表达式都不等,则执行default后的语句。

switch语句主要应用在同一个表达式有多个分支的情况下。

注意:

  • switch关键字后面括号里面的表达式的值必须为整型,不能为实型。

  • 每个case关键字的常量表达式后面都添加冒号。

  • case关键字后面的值可以是常量,也可以是常量表达式,但表达式中不能有变量。因为case关键字后面的值必须在编译阶段被确定。

  • 每个case后的常量表达式值要与switch后面括号内的表达式中的类型一直,且各个case后的常量表达式必须各不相同,否则会出现执行两个分支的矛盾。

  • 执行完一个case后面的语句后,看流程控制转移到下一个case继续执行。"case常量表达式"只起语句标号作用,并不是该处进行条件判断。因此,如果在执行完当前case语句后需要跳到switch语句后执行,就需要在case分支的最后加上一条break语句。

  • default分支可以省略。如果省略这个分支,则当所有常量表达式与switch后面括号中的表达式值不相等时,switch结构中将没有语句可以执行。

#include<stdio.h>

int main()
{
	int w;

	printf("请输入1个整数:");
	scanf_s("%d", &w);

	switch (w)
	{
	case 0:
		printf("星期日\n");
		break;
	case 1:
		printf("星期一\n");
		break;
	case 2:
		printf("星期二\n");
		break;
	case 3:
		printf("星期三\n");
		break;
	case 4:
		printf("星期四\n");
		break;
	case 5:
		printf("星期五\n");
		break;
	case 6:
		printf("星期六\n");
		break;
	default:
		printf("输入0-6之间的数字");
	}

	return 0;

}

从以上程序可以看出,与if语句不同,在case后面虽然可以执行很多条语句,但可以不用大括号将这些语句括起来组成语句块,程序会自动顺序执行case语句后面所有的执行语句。这是因为,在switch结构中,每个case相当于一个标号,标号后面可以有很多语句,程序会自动执行这些语句,直到语句break语句才跳出switch结构。

对于switch结构中的最后一个分支,可以不用加break语句。当执行到最后一个分支后,按顺序本来就应该执行switch结构后面的一条语句。当然最后一个分支加上一个break语句也是正确的。

5.3 循环语句

5.3.1 while循环结构

while语句可用于实现入口条件循环,即当循环条件满足时执行循环体内的语句。这是C语言中常用的循环语句之一。

while语句的语法格式很简单,一般形式为:

while (表达式)
    语句;

其中表达式就是循环条件,语句为循环体。

在C语言中,循环条件非常灵活,可以是各种表达式,并不仅限于条件表达式或逻辑表达式,只要表达式的返回值为非0,即认为循环条件为true;表达式的返回值为0,即认为循环条件为false,结束循环。

提示:循环体可以是一条语句,也可以是多条语句。如果是多条语句,则需要使用大括号{}将这些语句括起来,作为一个语句块。

#include<stdio.h>

int main()
{
	int i, n, sum;

	printf("请输入一个整数:");
	scanf_s("%d", &n);

	sum = 0;
	i = 1;

	while (i <= n)
	{
		sum += i;
		i++;
	}

	printf("从1到%d所有自然数之和为%d\n", n, sum);

	return 0;
}

警告:循环条件一定要设置无误,否则程序进入死循环,无法停止。

1. 使用复合语句

在while结构中,循环体可以是一条单独的语句,也可以是一个语句块。

当循环体一条单独的语句时,循环执行的过程不容易出现问题。例如:

while (i)
    printf("执行循环体,变量的值为%d\n",i--);

当i的值不为0的时候,将执行循环体。

当循环体有多条语句的时候,例如:将上面程序的中的循环体拆分为两条语句,写成如下形式:

while (i)
    printf("执行循环体,变量的值为%d\n",i);
	i--;

从书写程序的缩进表示理解,以上3条语句是希望i的值大于0的时候,执行printf()函数输出i的值,再让变量i自减1,然后返回while循环判断是否继续执行循环体。

但是,C编译器将忽略所有空白字符,也就是说,编译器不会按照用户缩进的格式来决定程序执行,只会通过分号来确定语句,缩进只是写给程序员看的。

实际情况是,在以上3行代码中,循环体只是第2行语句,因此第1、2行语句将构成一个死循环,执行printf()函数输出变量i的值,然后又返回第1行用while循环判断条件,第3行语句将不会被执行。

因此,只要循环体中有两条或两条以上一句,必须使用大括号{}将这些语句括起来,构成一条复合语句。当然,只有一条语句也是可以使用大括号将该语句括起来。

2. 空循环体

在while结构中还需要注意分号的用法。例如,有以下循环语句:

while (i--) ;
    sum += i;

在第1行while最后有一个分号,表示while语句语句结束,其循环体为一条空语句,不会将第2行作为循环体。

其实,while语句本身在语法上是作为一条单独的语句,由while判断循环条件和一个循环体组成。对于当行循环体,从while开始,到第一个分号结束为一条语句;对于一个复合语句循环体,从while开始,到复合语句终结的大括号为一条语句。这样就不难理解以上的程序了。

因为C语言的灵活性,所以在while的循环条件表达式中可以完成很多的工作,如将循环体内的语句写到此处,循环体无语句可写,就可以使用一条空语句。例如:

while (sum += i,i--);

在循环条件表达式中使用了一个逗号表达式,逗号表达式从左到右的顺序计算,返回最右侧的值。因此,以上代码现将变量i累加到sum中,再执行i自减1的操作。整个逗号表达式的返回值就是变量i的值,当i等于0时,循环结束。

3. 入口条件循环

此处介绍的while的入口条件循环,即先判断条件,再根据条件决定是否执行循环。例如:

i = 10;

while (i<10)
{
    sum += i;
    i++;
}

在以上代码中,因进入循环测试时,循环的条件为false,所以循环体一次都不执行。

4. 循环条件表达式

因为C语言的灵活性,在while语句的循环条件中,还可以使用赋值表达式,例如:

while (i=1)

在以上语句中,循环条件表达式是一个赋值表达式,将常量1赋值给变量i,同时表达式的值也是1,就与while(1)的意义不同了。

以上语句要与以下语句区分开:

while (i==1)

5.3.2 do-while循环结构

while语句是在循环前判断条件,就像火车选择铁路一样,只有符合路线要求的火车才能进入铁路,而while语句也只有满足条件才能进入循环体。如果一开始条件就不满足,那么循环体一次也不被执行。

而do-while语句适合先循环,然后在循环过程中产生控制条件,第一次循环结束后,再判断条件,以决定是否进行下一次循环。

下面看一个while循环显示菜单的例子:

#include<stdio.h>

int main()
{
    int i;

    printf("1.录入数据\n");
    printf("2.查询资料\n");
    printf("3.输出资料\n");
    printf("0.退出系统\n");
    scanf_s("%d", &i);

    while (i)
    {
        switch (i)
        {
        case 1:
            printf("调用录入数据函数\n\n");
            break;
        case 2:
            printf("调用查询资料函数\n\n");
            break;
        case 3:
            printf("调用输出资料函数\n\n");
            break;
        default:
            printf("输入错误,请选择对应的菜单\n");
            break;
        }

        printf("1.录入数据\n");
        printf("2.查询资料\n");
        printf("3.输出资料\n");
        printf("0.退出系统\n");
        scanf_s("%d", &i);
    }

    return 0;
}

do-while语句先执行循环体,再判断循环条件,其语法格式如下:

do
    语句;
while(表达式);

其中语句是循环体。如果循环体内有多条语句,则需要将这些语句用大括号{}括起来,组成一条复合语句。while后面的表达式是循环条件。

注意:while(表达式)后面必须有一个分号。

do-while语句和while语句的区别在于:do-while语句是先执行后判断,因此do-while语句至少要执行一次循环体;而while语句是先判断后执行,如果条件不满足,则循环体语句一次也不执行。

使用do-while语句改写while语句编写的菜单显示程序如下:

#include<stdio.h>

int main()
{
	int i;

	do
	{
		printf("1.录入数据\n");
		printf("2.查询资料\n");
		printf("3.输出资料\n");
		printf("0.退出系统\n");
		scanf_s("%d", &i);

		switch (i)
		{
		case 0:
			printf("退出系统");
			break;
		case 1:
			printf("调用录入数据函数\n\n");
			break;
		case 2:
			printf("调用查询资料函数\n\n");
			break;
		case 3:
			printf("调用输出资料函数\n\n");
			break;
		default:
			printf("输入错误,请选择对应的菜单\n");
			break;
		}
	} while (i);

	return 0;
}

从以上程序可以看出,使用do-while语句可以节约重复显示的语句,使程序更加简洁。

在do-while循环中,循环体至少要执行一次:而在while和for循环中,循环体可能一次也不执行。除此以外,do-while语句还应注意以下几点:

  • 在if语句、while语句中,在表达式后面都不能加分号;而在do-while语句的表达式后面必须加分号,表示该语句的结束。
  • 在do和while之间的循环体由多条语组成时,也必须使用大括号括起来组成一条复合语句。

5.3.3 for循环结构

各种高级程序设计语言都提供有for语句(或类似的语句),但都没有C语言中for语句灵活。C语言中的for语句可有各种各样的用法,下面列出常用的使用样式。

1. 省略初始代码

for语句的“表达式1”用来初始化变量,也可以在表达式中使用逗号表达式来完成多个变量的初始化。

如果在进入for语句之前,各变量已初始化,则在for语句中和可以省略"表达式1"。但是,用来分隔的分号不能省略。例如,以下程序用来计算自然数的一个连续区间内的各数的平方,要求用户输入连续区间的起始数和终止数。

#include<stdio.h>

int main()
{
	int i, j;

	printf("请输入起始数和终止数:");
	scanf_s("%d %d", &i, &j);

	for (; i <= j; i++)
		printf("%d的平方是%d\n", i, i * i);

	return 0;
}
2. 省略判断代码

for语句的“表达式2”作为循环的入口判断条件,决定是否执行循环体。按一般的理解,该表达式是必须给出的,但C语言中规定该表达式也可以省略。如果省略该表达式,则for认为其值为1,与使用while(1)相似,构成一个死循环。例如,以下代码将一直在屏幕上显示信息,直到用户强行终止才结束。

for(i=1;;i++)
    printf("死循环,一直打印这条信息");

在以上程序中,for语句的初始化部分后面跟两个分号,省略了判断部分,此时无论初始化部分和修改部分写的是什么语句,以上程序都是一个死循环。更常见的用for写的死循环就是3个表达式全部省略,如下:

for(;;)
    printf("死循环,一直打印这条信息");

注意:for语句中的两个分号不能省略。

3. 省略修改代码

for语句的“表达式3”用来修改计数器,使判断部分的值在一定条件下能为false,以便退出循环。如果for语句中省略该代码,则需要在循环中修改计数器的值,例如:

for(i=1;i<j;)
{
    printf("%d的平方是%d",i,i*i);
    i++;
}

当然,这种写法就无法体现for语句的简介性,所以很少使用这种方法,而以下的表示方法在C语言中更加常见:

for(i=1;i<j;)
    printf("%d的平方是%d",i++,i*i);
4. 只有判断代码

如果for语句中只写出判断部分的代码,则和while语句的功能完全相同。例如:

i=1;
for(;i<10;)
    sum+=i++;

改写为while格式如下:

i=1;
while(i<10)
    sum+=i++;
5. 循环变量递增方式

在省略初始代码的的程序中,for语句的表达式3为依次递增,最终使表达式2的值变为false,退出循环。也可对表达式3使用递减运算,修改程序代码如下:

for(sum=0,i=n;i>0;i--)

程序依然可以得到正确的结果。

在上面的语句中,for语句的表达式1将变量i的值初始化为累加的最大值n,在表达式3中对变量i进行递减,当变量i的值递减到0时,结束循环。与while语句后的表达式类似,若变量i为0时退出循环。也可以在表达式2中直接写上变量。因此以上语句还可以写成以下形式:

for(sum=0,i=n;i;i--)

因为for修改循环变量的表达式3为一个表达式,因此,可以灵活地修改计数器每次的增量,如下代码使循环变量每次增2,可对奇数或偶数进行累加求和。

for(sum=0,i=1;i>=n;i+=2)
    sum+=i;

同理还可以是计数器每次变动一个比率。比如,计算投资100万元,按年收益12%计算,多少年能达到1000万元,可以用以下代码来实现。

#include<stdio.h>

int main()
{
    float s;
    int i = 1;
    
    for(s=100.0;s<=1000.0;s=s*1.12,i++)
        printf("第%d年收益为%.2f\n", i, s);
    
    return 0;
}
6. 将循环体语句写到for语句中

将上面这个程序的第9行代码改写成如下样式:

for(s=100.0;printf("第%d年收益为%.2f\n", i, s), s<=1000.0;s=s*1.12,i++)

提示:与while语句相似,也可以将循环体语句写到for语句中,从而使循环体变为一条空语句。

在C语言中,保存字符时是保存该字符的ASCII码值,所以可以将字符作为整型数据处理。因此在循环中,也可以使用字符变量作为循环变量。例如以下程序可以输出指定两个字符之间的所有字符:

#include<stdio.h>
#define _CRT_SECURE_NO_WARNINGS

int main()
{
	char c, d;

	printf("请输入起始字符和结束字符:");
	c = getchar();
	d = getchar();

	printf("%c\n", c);
	printf("%c\n", d);

	printf("在ASCII码表中,字符%c和%c之间所有字符如下:\n", c, d);
	for (; c <= d; c++)
	{
		printf("%c", c);
	}

	return 0;
}

用for循环输出ASCII码表:

#include<stdio.h>

int main()
{
	char c;
	int i;

	printf("ASCII表\n");

	for (c = 32, i = 1; c <= 126; c++, i++)
	{
		printf("%4d %2c", c, c);

		if (i == 9)
		{
			printf("\n");
			i = 0;
		}
	}

	return 0;
}

5.3.4 循环嵌套

分支结构也可以嵌套。即一个分支里面又包含一个分支或多个分支。同样,循环结构也可与支持嵌套,即一个循环里面又包含一个循环,也称为多重循环。前面介绍的3种循环语句,每种循环体部分都可以再含有循环语句,所以编写多重循环的程序很简单。

下面是一个例子,要求编写程序输出3行星号,每行20个。

看到这个要求,首先是想到就是使用3个printf()函数分别输出,每个printf()里面20个星号,并换行。

printf("********************\n");
printf("********************\n");
printf("********************\n");

以上的程序可以完成例子中所提出的要求。但是如果对程序的要求进行改变,如果要求输出50个星号,一共输出20行,就需要对以上代码进行大幅度修改,相当于重写代码。因此,使用循环编写以上程序应该是最好的办法。首先想到使用循环输出每一行的星号。例如:

#define COL 20

for(i=1;i<=COL;i++)
{
    printf("*");
}
printf("\n");

以上程序定义每一行输出的星号的数量,下面3行完成输出一行星号的过程。

将以上程序的3-7行重复出入3次就能完成3行星号的输出。如果想要输出50行星号,则将第3-7行重复输入五十次,这样显然是不太理想的。这时就又可以考虑到循环的使用了。

假设有一段程序能够在每一行输出规定的星号,则使用以下程序来输出指定行的星号:

#define ROW 50

for(j=1;j<=ROW;j++)
{	
	for(i=1;i<=COL;i++)
	{
    	printf("*");
	}
	printf("\n");
}

以上程序并不难理解,但上面的代码要真正的执行,需要将第4行代码进行替换。替换的代码如下:

#define ROW 50
#define COL 20

for(j=1;j<=ROW;j++)
{
	输出第j行;
}

对以上程序进行修改,让用户在程序运行时控制每行星号的数量,以及输出显示多少行,程序如下:

#include<stdio.h>

int main()
{
	int r, c, i, j;

	printf("输入每行星星的数量:");
	scanf_s("%d", &c);
	printf("输入行数:");
	scanf_s("%d", &r);

	for (i = 0; i < r; i++)
	{
		for (j = 0; j < c; j++) {
			printf("*");
		}
		printf("\n");
	}

	return 0;
}

循环嵌套的层数是任意的,按照嵌套的层数可以分别叫做双重循环、三重循环、四重循环等。

在多重循环中,处于内部的循环叫做内循环,处于外部的叫做外循环。内循环和外循环可以分别为不同的循环语句。例如:

for()
{
    语句1;
    while()
        语句2;
    语句3;
}

在for循环中嵌入一个while循环。

do
{
    语句1;
    while()
        for()
            语句2;
    语句3;
}

以上程序为一个三重循环,最外层为一个do-while循环,里面为一个while循环和一个for循环。

通常双重循环用的比较多。例如用外循环来控制按行显示数据,用内循环处理每一行的所有列。小学生用的九九乘法表也可以用双重循环来输出,如下程序:

#include<stdio.h>

int main()
{
	int i, j;

	for (i = 1; i <= 9; i++)
	{
		for (j = 1; j <= i; j++)
		{
			printf("%d * %d = %-2d ", j, i, i * j);
		}
		printf("\n");
	}
	return 0;
}

再看一个经典的百钱白鸡问题。中国古代数学家张丘建在他的《算经》中提出了著名的百钱买百鸡问题:公鸡一只价钱为5钱,母鸡一只价钱为3钱,小鸡三只价钱为1钱,用100钱能买多少公鸡、母鸡、小鸡各多少只?

#include<stdio.h>

int main()
{
	int x, y, z;

	for (x = 0; x <= 100; x++)
	{
		for (y = 0; y <= 100; y++)
		{
			for (z = 0; z <= 100; z++)
			{
				if (z % 3 == 0 && x + y + z == 100 && 5 * x + 3 * y + z / 3 == 100)
					printf("公鸡:%d,母鸡:%d,小鸡:%d\n", x, y, z);
			}
		}
	}
	return 0;
}
#include<stdio.h>

int main()
{
	int x, y, z;

	for (x = 0; x <= 12; x+=4)
	{
		y = 25 - (x / 4) * 7;
		z = 100 - x - y;
		printf("公鸡:%d,母鸡:%d,小鸡:%d\n", x, y, z);
	}
	return 0;
}

由以上两个程序可知,在循环嵌套中,尽量减少嵌套的层数,可提高程序的效率。但是由于计算机的运行速度越来越快,程序员也应该注意不要过度追求程序效率,而使程序很难懂。例如,将程序改写成上面第二个程序的形式,虽然也能得到正确的结果,并且只有一层循环,但是看着这个程序能轻松的弄懂其算法吗。

5.4 转向语句

循环语句就像一个圈,,如果没有一个退破口,程序就容易进入死循环,所以我们需要一些语句来跳出循环。对于3种循环语句,C语言都提供了退出循环或短路U型能换的break语句和continue语句。

5.4.1 break中断语句

break语句在switch结构中使用过,用来中断分支的执行,跳到switch的最后一条语句执行。

当循环结构中使用break语句后,可使程序终止循环而执行循环后面的语句。通常break语句和if语句连在一起,即满足条件便跳出循环。

注意:对于多重循环嵌套,使用break语句只能跳出当前循环,而不会跳出所有外层循环。

在某些特殊情况下,需要创建一个死循环结构,这时可能在该结构内部添加break语句,用来跳出该死循环。例如,以下程序要求用户输入多个数据,程序对这些数据进行累加,并输出累加结果。

#include<stdio.h>

int main()
{
	float n, sum = 0;
	int i = 0;

	do
	{
		printf("请输入累加的数(若为0,则完成累加):");
		scanf_s("%f", &n);
		if (n == 0)
			break;
		i++;
		sum += n;
	} while (1);
	printf("\n你输入了%d个数,所有数之和为:%.2f\n", i, sum);

	return 0;
}

以上程序中,因为不知道用户输入的数据的数量,所以使用了一个do-while的死循环。在死循环中若不设置退出条件,程序将一直循环:若用户强行中断程序的运行,便不能执行第17行的代码输出运行结果。

注意:在死循环结构中都必须设置退出条件。在上面程序中第12、13行对用户输入的数据进行判断,若用户输入的数据为0,则执行break语句跳出循环,转到第17行执行输出操作。

5.4.2 continue条件继续语句

使用break语句可跳出循环,而continue语句的作用则是跳过循环体中剩余的语句,而直接去执行下一次循环。continue语句只能在for、while、do-while等循环体中使用。在循环体中添加break语句后,

例如:对以上break实例程序进行修改,要求只对用户输入的正数进行累加,若用户输入的是负数,则不累加。同样,若用户输入的值为0,则跳出循环,输出累加结果。

#include<stdio.h>

int main()
{
	float n, sum = 0;
	int i = 0;

	do
	{
		printf("请输入累加的数:");
		scanf_s("%f", &n);

		if (n < 0)
			continue;
		if (n == 0)
			break;
		i++;
		sum += n;
	} while (1);
	printf("\n你输入了%d个正数,所有正数之和为:%.2f\n", i, sum);

	return 0;
}

编译运行以上程序,按提示随机输入正数、负数,最后输入0。根据最后的结果可以看出,所输入的复数都被放弃了,程序只统计了用户输入的正数的数量,只累加正数的值。

5.4.3 标签语句

goto语句的语义是改变程序流向,转去执行标号所标识的语句。它通常与条件语句配合使用,可用来实现条件转移、构成循环、跳出循环体等功能。

goto 语句标号;

其中语句标号是按标识符规定书写的符号,放在某一语句行的前面,标号后面加冒号。语句标号起标识语句的作用,与goto语句配合使用。

提示:在switch结构中,case关键字后面就是的常量就是一个标号。

在程序中定义标号的时候,整个程序的标号不能重复。例如:

loop1: if(i<=n)
labe1: sum+=1;

以上两条语句分别为两行程序定义标号,使goto语句能够跳转到该行程序处执行。

不使用for、while、或do-while语句,只使用goto语句也可以构造循环结构。例如:使用goto语句改写之前的程序,累加自然数之和。

#include<stdio.h>

int main()
{
	int n, i, sum = 0;

	printf("请输入一个整数:");
	scanf_s("%d", &n);

	i = 1;
loop: if (i <= n)
{
	sum += i;
	i++;
	goto loop;
}
printf("1到%d的所有自然数的和为:%d\n", n, sum);

return 0;
}

在以上程序中,在11行定义了标号之后,在程序的任何位置都能使用goto语句跳转到该行。这样有可能使程序的流程发生混乱,使理解和调试程序都产生困难。因此,建议在程序中不要使用goto语句。在此简单介绍,只是让大家知道C语言提供了此语句,在一些特殊情况下,可以使用goto语句提高程序的效率。例如,在一个很多层循环嵌套的结果中,要直接跳出最外层循环,就要使用goto语句。若使用break语句来退出,因break语句只能跳出所在层的循环,还需要在每层添加条件判断和break语句,比较麻烦。实例代码如下:

for(a=0;a<10;a++){
    for(b=0;b<10;b++){
        for(c=0;c<10;c++){
            for(d=0;d<10;d++){
                for(e=0;e<10'e++){
                    for(f=0;f<10;f++){
                        if(a+b+c+d+e==50)
                            goto end;
                    }
                }
            }
        }
    }
}
end: printf("ok");

这种跳出多重循环的情况下,goto语句的功能是其他语句所不能替代的。

5.5 返回语句

return表示从被函数返回主函数继执行,其返回值是由return后面的参数所决定的。一般来说,返回一个值是必要的操作,因为函数调用的时候计算结果通常是通过返回值带出的。如果函数执行不需要返回计算结果,则也经常返回一个状态码来表示函数执行是否成功,主函数可以通过返回值来判断被调函数是否已经顺利执行。

如果实在不需要返回什么值,就需要用void声明其类型。

还有一点就是,你的函数是什么类型,就需要返回什么类型的值,比如int类型的函数则需要返回int类型的数据。

1. 非void型

int f1()
{
    int i=1;
    return 1;
    //return (i); 这样也可以
}

2.void型

void f2()
{
    int i=1;
    //return;  这样也可以,不要这一句也可以
}

有时计时被调用函数是void类型,被调函数中的return也不是毫无意义的。

例如:

#include<stdio.h>
void function()
{
    printf("1111");
    return;
    printf("2222");
}
main()
{
    function();
}

运行结果为:屏幕上只输出一串数字1而没有2。但是如果去掉function函数中的return语句,就可以同时输出一串2。

这里的return其实还有一个退出该程序的作用。也就是说,在printf(“1111”)后面加一个return,就表示结束该函数,返回主函数。

5.6 拓展训练

5.6.1 水仙花数

水仙花数是指一个三位数中各个位的数值的立方和等于该数。

#include<stdio.h>

int main()
{
	int i, ge, shi, bai;

	for (i = 100; i <= 999; i++)
	{
		ge = i % 100 % 10;
		shi = i % 100 / 10;
		bai = i / 100;

		if (ge * ge * ge + shi * shi * shi + bai * bai * bai == i)
			printf("%d\n", i);
	}
	return 0;
}

5.6.2 工资提成问题

现有一家公司发放的奖金根据年工资提成,其提成规定如下:

  • 工资低于或等于3万元,奖金提成%8。
  • 工资高于3万元,低于或等于7万元,奖金提成5%。
  • 工资高于7万元,低于或等于10万元,奖金提成3%。
  • 工资高于10万元,奖金提成1%。
#include<stdio.h>

int main()
{
	long int a;
	int b;

	printf("请输入年工资:");
	scanf_s("%ld", &a);

	if (a <= 30000)
		b = a * 0.08;
	else if (a <= 70000)
		b = a * 0.05;
	else if (a <= 100000)
		b = a * 0.03;
	else if (a > 100000)
		b = a * 0.01;
	else
		printf("输入错误!");

	printf("奖金为%d\n", b);

	return 0;

}

5.6.3 学生成绩评分等级

输入学生的成绩的等级(A、B、C、D、E),输出相应的评价文字(优秀、良好、中等、及格、不及格)。

#include<stdio.h>

int main()
{
	char c;
	printf("输入学生成绩等级:");
	c = getchar();

	switch (c)
	{
	case 'A':
		printf("优秀\n");
		break;
	case 'B':
		printf("良好\n");
		break;
	case 'C':
		printf("中等\n");
		break;
	case 'D':
		printf("及格\n");
		break;
	case 'E':
		printf("不及格\n");
		break;
	default:
		printf("输入有误");
	}
	return 0;
}

5.6.4 商店找零问题

现有一张100元的钞票,专门用来买3元、4元、5元的商品,求共有多少种买法恰好可以将100元用完。

#include<stdio.h>

int main()
{
	int x, y, z;

	for (x = 0; x <= 33; x++) {
		for (y = 0; y <= 25; y++) {
			for (z = 0; z <= 20; z++) {
				if (3 * x + 4 * y + 5 * z == 100)
					printf("%4d\t%4d\t%4d\n", x, y, z);
			}
		}
	}
	return 0;
}

5.6.5 斐波那契数列问题

#include<stdio.h>

void main()
{
	int f1 = 1, f2 = 1, f, i, x, y;

	printf("请输入要求第几个数:");
	scanf_s("%d", &x);
	y = x - 2;

	for (i = 1; i <= y; i++) {
		f = f1 + f2;
		f1 = f2;
		f2 = f;
	}
	printf("要求的第%d个数是%d", x, f);
}

5.6.7 简单计算器

#include<stdio.h>

int main()
{
	int a, b, d=0;
	char c;

	printf("请输入a、b的值:");
	scanf("%d%c%d", &a, &c, &b);

	switch (c)
	{
	case '+':
		d = a + b;
		break;
	case '-':
		d = a - b;
		break;
	case '*':
		d = a * b;
		break;
	case '/':
		d = a / b;
		break;
	default:
		printf("输入的操作有误");
	}
	printf("%d %c %d = %d\n", a, c, b, d);
	return 0;
}

5.6.8 判断一个数是否为素数

#include<stdio.h>

int main()
{
	int i, j, k = 0;

	for (i = 2; i < 101; i++)
	{
		for (j = 2; j <= i; j++)
		{
			if (i % j == 0)
				break;
		}
		if (j >= i)
		{
			printf("%4d", i);
			k++;
		}
		if (!(k % 10))
			printf("\n");
	}
	return 0;
}

5.6.9 判断三角形的构成问题

输入三角形的3条边的大小,判断这3条边是否能够构成三角形,若能构成则输出这个三角形的面积,否则输出"不能构成三角形"的信息。

a=(x+y+z)*0.5;
s=sqrt(a*(a-x)*(a-y)*(a-z));
#include<stdio.h>
#include<math.h>

int main()
{
	float x, y, z, a, s;

	printf("请输入三边:");
	scanf("%f %f %f", &x, &y, &z);

	if (x + y > z && x + z > y && y + z > x)
	{
		a = (x + y + z) * 0.5;
		s = sqrt(a * (a - x) * (a - y) * (a - z));
		printf("三角形的面积为:%f\n", s);
	}
	else
		printf("不满足构成三角形的条件");

	return 0;

}

6. 数组管理更复杂的数据

6.1 了解数组

电影院的一排排的座位有很多的编码,一维数组就像是一排座位一样,它是由很多具有相同数据类型的变量组成的集合。将数据和循环结构相结合,可提高程序的灵活性,使设计的程序更具有通用性。

6.1.1 使用数组的好处

先看一个例子,编写一个程序,计算学生的平均成绩。如果学生只修了五门课,则可以使用以下的程序:

#include<stdio.h>

int main()
{
	float socre1, score2, score3, score4, score5, avg, sum;

	printf("请输入学生的五门课程成绩:");
	scanf("%f %f %f %f %f", &socre1, &score2, &score3, &score4, &score5);

	sum = socre1 + score2 + score3 + score4 + score5;
	avg = sum / 5.0;

	printf("总分:%.2f,平均分:%.2f\n", sum, avg);

	return 0;
}

注意:以上程序在学生所学课程不多的时候可以使用,但是当有几十门课程的成绩需要统计时,采用这种方法,编写的程序就显得很笨重了。

这时可以在程序中使用数组来保存成绩,代码改写成如下形式:

#include<stdio.h>
#define NUM 5

int main()
{
	float score[NUM], avg, sum;
	int i;

	printf("请输入学生各课程成绩:");
	for (i = 0,sum = 0; i < NUM; i++)
	{
		scanf("%f", &score[i]);
		sum += score[i];
	}
	avg = sum / NUM;
	printf("总分:%.2f,平均分:%.2f\n", sum, avg);

	return 0;
}

6.1.2 数组的概念

数组是一组相同类型的数据集保存的一种方式。数组中的每一个数据被称为一个数组元组,所有数组元素具有相同的数组名称,通过中括号中的数字序号来区分不同的数据元素。在程序中,可使用与普通变量相同的方法对数据进行赋值、计算、输出等操作。

在上面程序中,使用了一个名为score的数组,该数组是由5个float数据类型的数组元素构成,分别为score[0]、score[1]、score[2]、score[3]、score[4]。在程序中也可以使用以下语句将用户输入的值保存到score[0]中:

scanf("%f",&score[0]);

也可以使用以下语句在程序中对数组元素赋值

score[1]=score[0]
score[2]+=score[0]

在输出数组的时候,必须逐个元素进行输出。一般使用循环完成所有数组元素的输出。例如:

for(i=0;i<sizeof(score)/score[0];i++)
    printf("第%d个元素的值为:%.2f\n",i+1,score[i]);

6.1.3 数组的维数

按数组元素的类型不同,数组可以分为数值数组、字符数组、指针数组、结构数组等类别。这里介绍数值数组、字符数组的使用,指针数组和结构数组在介绍指针和结构的时候介绍。

还有一种数组分类的方法,即按数组的维数来分,可以分为一维数组、二维数组等。想之前的比喻,一维数组就是电影院一排作为,那么二维数组就是整个电影院的座位了,不仅有行,还有列。

多维数组有多个下标。二维数组有两个下标、三维数组有三个下标、四维数组有四个下标,一次类推。C语言对数组下标数量没有限制。

例如,上面的程序使用的数组score只能表示一位同学的成绩,如果一个班有45个同学,则可以使用一个二维数组来表示。

float score[45][5];

如果需要处理全校所有班级的学生的成绩,则又可以定义一个三维数组。

float score[15][45][5];

这个三维数组中,第一维表示班级,第2维表示班级的某个学生,第3维表示的是该学生的某门课程成绩。

三维数组可以看成一个立方体。

当然,数组还可以有更多的维数,但太多的维数会导致数据的结构很复杂。同时,数组的维数增加,将占用大量的内存。

6.2 一维数组

6.2.1 一维数组的声明

在C语言中,与普通变量一样,数组也必须先声明再使用。数组声明的一般形式为:

类型说明符 数组名[常量表达式],...;

其中类型说明符是任意一种基本数据类型或构造数据类型;数组名是用户定义的数组标识符;方括号中的常量表达式表示数据元素的个数,也称为数组的长度。

例如,以下语句用来声明一维数组:

flaot score[5];
int arr[40];

在同一条语句中可以声明多个数组或变量。例如:

int i, score[30], name[30];

在C89标准的编译器声明数组时,不能在方括号中用变量来表示元素的个数,只能用常数或常量表达式来声明数组元素的个数。

例如:

#define NUm 5
int main()
{
    float score[NUM],name[NUm+1],arr[8+9];
    ...
}

以上代码通过#define定义符号常量,再使用符号常量声明数组的元素个数。在声明数组的语句中,中括号也可以为一个常量表达式。如“8+9“,将声明拥有17个元素的数组。

而以下代码是不合法的:

int main()
{
    int j=5;
    float arr[j];
}

如果是在支持C99标准的编译器中,则以上代码是合法的。使用以上代码创建一种新数组,称为变长数组。变长数组允许动态分配内存单元。这表示可以在程序运行时指定数组大小。常规的数组在编译时就确定了其存储空间的大小。

例如,以下程序就是对变长数组的应用。在程序执行时先由用户输入一个整数,用来决定数组的元素个数;然后要求用户逐个输入数组元素的值,并进行汇总;最后打印用户输入的个数和汇总结果。(C99标准)

#include<stdio.h>

int main()
{
	int i, j;
	float g[j], sum;

	printf("请输入数组元素的个数:");
	scanf("%d", &j);

	for (i = 0, sum = 0; i < j; i++)
	{
		printf("第%d个数:", i + 1);
		scanf("%f", &g[i]);

		sum += g[i];
	}

	for (i = 0; i < j; i++)
	{
		printf("第%d个数:%.2f\n", i + 1, g[i]);
	}
	printf("合计:%.2f\n", sum);

	return 0;
}

在编译以上程序之后,输入数组的个数为5,接着程序将要求用户输入5个数组元素的值。输入完毕后,程序还将显示输入的值,同时显示汇总结果。

再次执行程序,输入数组元素的个数为10,则要求用户输入10个数组元素的值。

还要注意,在声明数组的时候,常量表达式必须为大于0的整数值。例如,以下声明方式将出现错误:

int a[8.8];
int b[-9];

在以上声明中,第1行使用了实数,所以不能通过编译;第2行使用的是负数,也不能通过编译。而以下的方式是能顺利执行的:

int a[(int)8.8];

以上语句使用了强制类型转换,将实数8.8转换成整型8。

注意:类型说明符指的是数组中每个元素所能保存的数据类型。对于一个数组,其所有元素的数据类型都是相同的

注意:数组名也是一种标识符,因此必须符合标识符的相关规定。同时,数组名不能与其他变量名相同。

6.2.2 一维数组的存储

C编译器在遇到数组的声明语句的时候,将按照数组的长度的要求为数组分配一片连续的存储空间。在分配存储空间时,将以声明数组时指定的数据类型为基本单位,再乘以数组的元素的个数。例如,对于10个元素的int型数组a,在32位系统中,将分配连续的40个字节来保存对于的十个数组元素,每个数组元素与单个的int类型变量一样占用4字节。

6.2.3 引用一维数组

定义数组之后就可以使用它了。程序中只能引用数组的单个元素,打个比方,电影院的一排座位一次只能引用一个座位,而不能一次性的引用整个数组的数据。

数组元素的引用要指定其下标,引用形式:

数组名[下标];

其中的下标必须是整型表达式或者整型常量。若下标为小数,则在有的编译器中对其自动取整,在有的编译器中提示错误。为了使程序在任何编译器中都编译通过,最好在下标为小数时添加强制类型转换。

在程序中,凡是能够出现普通变量的地方,都可以使用数组元素。在引用数组元素时,其下标还可以是变量或表达式。例如:

printf("%d\n",a[i]);
scanf("%d",&a[i]);
i=a[3];
a[i]=a[j];
a[i++]=3;

在以上的示例中,第1行是输出数组a[i]的值;第2行将用户输入的整数保存到数组元素a[i]中;第3行将数组元素a[3]的值赋给变量i;第4行将数组元素a[j]的值赋给数组元素a[i];第5行将常数3赋值给数组元素a[i],同时变量i自增1。

声明数组和引用数组时使用的字符相同,但是要注意二者具有不同的意义。

  • 在声明数据时,方括号内是常量表达式,代表数组的长度。由编译器为声明的数据分配指定长度的内存单元。一个数组只需要在程序中声明一次。
  • 在引用数组的元素时,方括号内是表达式,代表下标,可以包括变量或表达式,表示指定序号的单个数组元素。该数组元素与一个普通变量相同,可以对其进行赋值、参加表达式的运算、输出等操作。在程序中引用数组元素的次数不受限制。

注意:与其他高级程序设计语言不同,C语言编译器不会检查数组的下标是否越界。

#include<stdio.h>

int main()
{
	int i, a[10];

	for (i = 0; i < 10; i++)
		a[i] = i + 1;
	
	for (i = 0; i < 15; i++)
		printf("第%d个数:%d\n", i, a[i]);

	return 0;
}

编译以上程序,数组前十个数组正常输出,第11到15个元素超出数组定义的长度,将输出一些无意义的数。

程序第5行定义了一个数组a,具有10个元素。第7、8行使用for循环对数组进行了赋值,第10、11行对数组元素进行了循环输出。第5行只为数组定义了10个元素,并且只对这10个元素进行了赋值,但是输出的时候却输出了15个数。

在以上程序中对读取超越下标的界限情况,程序能够正常运行。如果向常规界限的数组元素赋值,将会将数据写到其他变量所占的内存单元中,甚至写入程序代码段中,这样有可能造成不可预料的运行结果,甚至使计算机系统出现崩溃。

6.2.4 一维数组的初始化

在前面已经演示了通过循环向数组中各元素进行赋值的方法。其实在声明数组时就可以对数组进行初始化,使元素具有一个确定的值。

注意:与普通变量相似,在声明数组时,如果未对数组元素进行初始化,则数组中各元素的值都是不确定的,编译器分配内存空间时并不会对内存单元进行清零操作。

#include<stdio.h>
int main()
{
	int i,a[10];
	
	for(i=0;i<10;i++)
		printf("第%d个数:%d\n",i+1,a[i]);
		
	return 0;
}

编译以上程序可以看到输出的结果为一些无意义的数字。

为了使各元素具有一个确定的值,可在声明数组的时对齐进行初始化。数组初始化赋值是指在声明数组的时候就对齐进行赋值。数组初始话是在编译阶段进行的。这样将减少程序的运行时间,提高运行效率。

初始化赋值一般形式为:

类型说明符 数组名 [常量表达式]={1,2,...,3};

在定义数组的时候,可以对元素进行赋初值。将数组元素的初值依次放在一对大括号中,各数据之间使用逗号隔开。例如:

int main()
{
    int a[5] = {23,45,67,89,12};
    ...
}

在以上程序中,在声明数组a的时候对齐进行了初始化,使a[0]=23,a[1]=45,a[2]=67,a[3]=89,a[4]=12。

在以上程序中对各元素都进行了初始化。也可以只对一部分元素赋初值。例如:

a[5]={12,34};

以上声明语句中,数组a具有5个元素,后面的初始化大括号中只给出了2个数据。这时编译器将按顺序将这两个元数据赋值个数组的前2个元素。这时后面三个元素的值将不再是无意义的数,而是0值。

在声明数组是对数组中的部分元素进行了赋值,则未赋值的数组元素都初始化为0。而不再是不可预知、毫无意义的数据。

所以,使用以下方式可将数组的所有元素都初始化为0:

int a[10]={0};

注意:当初始化数据少于数组元素长度时,编译器将按顺序给对应的数组元素赋值,多余的元素则会被初始话为0。但是,如果初始化列表中的数量超过数组元素个数的时候,则编译器会提示出错。

这时就可以使用另一种初始化数组的方法,即声明数组的时候不定义其元素的长度。这时,需要用大括号为数组初始化,C编译器将根据初始化的数据的数量自动设置数组的长度。

int a[5]={1,2,3,4,5};
int a[]={1,2,3,4,5};

以上两个数组初始化是一样的。

如果被定义的数组长度与提供的初值的个数不同,则数组长度不能省略。例如,想定义数组长度为10,而初始化数据只有3个,就不能省略数组长度的定义。

在支持C99标准的编译器中还可以使用另一种方式初始化数组,有选择的对数组中的某些元素进行初始化。例如,要将数组中的第3个元素设置为3,按传统方式只能以以下方式来初始化:

int a[10]={0,0,3};

这样编译器将数组的第3个元素初始化为3,其他元素都设置为0。

按C99标准可以将以上语句改写成以下形式:

int a[10]={[2]=3};

在初始化列表中,使用中括号加序号的方式为指定序号的元素赋值。这对于一些元素较多,而只需要初始化其中极个别元素的数组来说,使用起来非常的方便。例如:

int arr[50]={23,[5]=90,[30]=56};

6.3 二维数组

6.3.1 二维数组的声明

声明二维数组的形式与声明一维数组相似,只是多加一个二维下标的常量表达式,例如:

int a[4][5];

以上代码声明了一个二维数组,该数组具有4行5列,共20个数组元素,其二维结构如下所示:

a[0][0]a[0][1]a[0][2]a[0][3]a[0][4]
a[1][0]a[1][1]a[1][2]a[1][3]a[1][4]
a[2][0]a[2][1]a[2][2]a[2][3]a[2][4]
a[3][0]a[3][1]a[3][2]a[3][3]a[3][4]

在C语言中,二维数组可以看做由一维数组嵌套组成,假设一维数组的每个元素又都是一个数组,就成了二维数组。可以将每一行看做一个数据类型。例如上面声明的a数组,如果按行列排列,则将每一列看做一个数据类型,然后又定义了4个该类型的数据的数组。

6.3.2 二维数组的存储

二维数组在逻辑上是二维的,但是,在存储二维数组的时候,C编译器将其转换为一维线性排列,也与存储一维数组相似,将所有数据按顺序放在内存中。将二维的数据转换为一维的形式来进行存放可以有两种方式进行选择:一种是按行排列,即保存完第一行再保存第二行,以此类推;另一种是按列排列,即保存完第一列之后再保存第二例,以此类推。

C语言中的二维数组采用按行排列的方式进行保存。

下面的程序演示二维数组转换为一维线性方式存放的结果:

#include<stdio.h>

int main()
{
	int score[4][5], i, j;

	for (i = 0; i < 4; i++)
		for (j = 0; j < 5; j++)
			score[i][j] = i * 4 + j * 5;

	printf("按二维数组输出数据:\n");
	for (i = 0; i < 4; i++)
	{
		for (j = 0; j < 5; j++)
			printf("%3d ", score[i][j]);
		printf("\n");
	}
	printf("按一维数组输出数据:\n");
	for(i=0;i<20;i++)
		printf("%3d ", score[0][i]);

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VJXpvGQs-1631880972617)(…/…/AppData/Roaming/Typora/typora-user-images/image-20210722114259891.png)]

编译以上程序得到所示结果。

第19、20行使用一个for循环,以score[0][0]`score[0][19]`的方式输出数组中的值。这时将score[0]作为数组的名称,后面跟着019作为数组的下标,得到一个一维数组。score[0]只有5个元素score[0][0]~score[0][4],而score[0][5]将引用score[1][0]中的数据,score[0][6]将引用score[1][1]中的数据,以此类推,可得到如上所示的最后一行的输出结果。

二维数组和一维数组相似,只是需要指定两个下标的值。如上所示的程序中,score[0][0]表示第一行第一个元素,score[1][0]表示第二行第一个元素。

在以上程序中的第20行中,将二维数组作为一维数组的形式输出数据时,然后需要使用两个下标,只是第1个一直为0。如果将第20行改为如下形式:

printf("%3d",score[i]);

程序将暑促score[0],…,score[19]的值。但是其输出结果并不是二维数组中保存的值。结果如下所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X12mz3Bc-1631880972618)(…/…/AppData/Roaming/Typora/typora-user-images/image-20210722121118103.png)]

从后面的输出结果可以看出,最后输出的20个数,每个数据都相差80。要了解这些数据的含义,需要介绍一个新的概念。在C语言的数组中,数据名代表一个内存地址。如果运行以下程序,则将输出数组a在内存中的起始地址。

int main()
{
	int a[5];
	printf("%d",a);
	return 0;

对于二维数组score,其完整的引用形式为score[0][0],如果省略后一个下标,则也将返回其地址。因此,以上运行结果输出的数据是二维数组每一行的首地址。因为每一行由5个int类型数据组成,所以每行占用20字节。这就是为什么输出的每个值都相差20的原因。在以上程序中只定义了4行数据,输出score[0]~score[19]的地址范围超出了数组的内存区域。

使用取地址运算符’&’,可以很方便的查看数组中各元素的地址,具体程序如下:

#include<stdio.h>

int main()
{
	int score[4][5], i;

	printf("数组首地址:%d \n", &score);
	printf("数组首地址:%d \n", score);
	printf("数组首地址:%d \n", score[0]);
	printf("数组首地址:%d \n", &score[0][0]);
	printf("\n");

	for (i = 0; i < 4; i++)
	{
		printf("输出第%d行的首地址:%d \n", i + 1, &score[i][0]);
		printf("输出第%d行的首地址:%d \n", i + 1, score[i]);
		printf("输出第%d行的首地址:%d \n", i + 1, &score[i]);
		printf("\n");
	}

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7MufdRty-1631880972619)(…/…/AppData/Roaming/Typora/typora-user-images/image-20210722121342008.png)]

编译以上程序得到结果。这是在截图时计算机的运行情况,在不同的计算机中、不同时间执行改程序,得到的结果不同。但是相邻的数据的差值始终不变。

总结一下二维数组中地址的相关内容:

  • 使用取地址运算符’&’,可获取数组名、数组行名、数组元素名的地址。
  • 对于数组名、数组行名,其标识符本身就代表其首地址。
  • 对二维数组的每一行,位于该行的第1个元素的地址也就是该行的首地址。位于第1行第1个元素的地址也就是整个数组的首地址。

6.3.3 二维数组的初始化

与一维数组相同,在声明二维数组的时候,也可以对数组元素赋初值。因为二维数组在逻辑上是二维平面的,在内容中又是按一维线性方式存储的,即可以将二维数组看做多个一维数组组合而成的,所以可以使用两种方法进行初始化。

1. 按行列进行初始化

在二维数组初始化时,通过大括号的嵌套,使数据按行列逻辑关系进行初始化。其具体形式如下:

类型名 数组名[行长度][列长度]={{1行初始值},{2行初始值},...,{第n行初始值}};

上面的数组声明语句中,右侧使用大括号嵌套进行初始化,内层的每一个大括号表示二维数组中的一行数据。

**注意:内嵌大括号中最后一个大括号不能添加逗号,逗号要添加在两个大括号之间。**例如:

int a[2][5]={{1,2,3,4,5},{6,7,8,9,10}};

以上语句声明一个二维数组,同时对该数组进行了初始化,将a[0]初始化为{1,2,3,4,5},将a[1]初始化为{6,7,8,9,10}。这样,数组元素a[0][0]=1,a[1][0]=6。

也可以只初始化部分数组元素,与一维数组相比,二维数组中初始化部分元素有多种情况,例如:

int a[3][5]={{1,2,3,4,5},{6,7,8,9,10}};

以上语句声明了一个3行5列的二维数组,初始化列表总只列出了2行数据,因为第3行中的5个元素值都为0。

int a[3][5]={{1,2,3,4,5},{},{6,7,8,9,10}};

以上语句声明了一个3行5列的二维数组,初始化列表总第2行为一对空的大括号,因此第2行中的5个元素值都为0。

int a[3][5]={{1,2,3,4,},{6,7,8,9,10},{11,12,13,14,15}};

以上语句声明了一个3行5列的二维数组,初始化列表中第1行中的数据只列出了4个,因此,这一行最后一列的元素值为0.

在支持C99标准的C系统中,还可以使用以下方式初始化指定元素:

int a[3][5]={[2][2]=4};

以上语句为第3行第3列的元素赋初值为4。

int score[4][5]={{[1]=1},{[2]=3}};

以上语句第1行第2列的元素赋初值为1,第2行第3列的元素赋初值为3。用内嵌的大括号区别行数。

int score[4][5]={[1]=1,[2]=3};

以上语句赋初值很特殊,需要注意。大括号中的[1]表示第2行的起始位置(相当于[1][0]),而[2]则表示第3行的起始位置(相当于[2][0])。因此,以上程序将为第2行第1列的元素赋初值为1,为第3行第1列的元素赋初值为3。

2. 按存储顺序赋初值

二维数组在内存中仍然是按一维线性方式存储的。对二维数组初始化时,也可以按这种存储方式顺序为各元素赋初值。具体如下:

类型名 数组名[行长度][列长度] =[初值表];

例如:

int a[2][5]={1,2,3,4,5,6,7,8,9,10};

等价于:

int a[2][5]={{1,2,3,4,5},{6,7,8,9,10}};

如果只对部分元素赋初值,则要注意初值表中数据的书写顺序,例如:

int a[3][5]={1,2,3,4,5,0,0,0,0,0,6,7,8,9,10};

等价于:

int a[3][5]={{1,2,3,4,5},{},{6,7,8,9,10}};

另外,初始化二维数组时,如果对全部的元素赋了初值,或分行赋初值时在初值表中列出了全部行,那么可以省略行长度,例如

int a[][5]={{1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15}};

在给数组赋初值的时候,尽量使用按行列赋值的方式,不使用按存储顺序赋值的方式。另外,也要尽量少用省略数组元素长度的方式初始化数组。

6.4 字符数组和字符串

在C语言中,可定义类型为char类型的数组,用来存储字符串。每个数组元素保存一个字符。在字符数组后面添加一个’\0’作为结尾,C语言就可以将该数组作为字符串进行处理。定义一个二维的char型数组,就可以处理多个字符串。例如,我们无法使用一个char类型的变量表示一个包含了多个字母的单词,而char型数组则可以做到。

6.4.1 字符数组

1. 声明字符数组

字符数组的声明方法与前面的方法相同,只是类型声明符指定为char。例如:

char name[10],char address[15];

以上语句声明了两个字符数组:一个为名为name,有10个元素,另一个名为address,有30个元素。

当然也可以声明二维数组。例如:

char name[5][10];

以上语句声明了一个名为name的二维数组,有5行10列,可以保存50个字符。

2. 初始化字符数组

字符数组各元素可以保存字符或0~255范围内的整数,可以使用前面的方法来初始化字符数组。例如:

char name[10]={'a','b','c','d','e','f','g','h','i','j'};

以上语句将10个字符分别赋值给数组的10个元素。

在初始化字符数组时,大括号里面的字符都必须使用单引号括起来。

二维字符数组的初始化与此类似。

3. 引用字符数组

引用字符数组中的元素,也需要通过数组下标的方法。引用字符数组的一个元素,可操作其中的一个字符。

#include<stdio.h>

int main()
{
	char name[10];

	printf("输入你的姓名(10个字符以内):");
	for (int i = 0; i < 10; i++)
		scanf("%c", &name[i]);

	printf("你的姓名是:");
	for (int i = 0; i < 10; i++)
		printf("%c", name[i]);

	return 0;
}

编译运行以上程序,在提示文字之后输入一串字符,并按回车键。如果输入的字符数量不够,可以需要多按几次回车键。

6.4.2 了解字符串

在程序中经常要用到字符串。要处理字符串,可以通过定义字符串数组,然后逐个字符串初始化、逐个字符输入和输出。采用这种方式时,因为要逐个字符地进行处理,特别不方便

在C语言中,字符和字符串是两个概念。用单引号括起来的是字符,用双引号括起来的才是字符串。为了方便字符串的处理,C语言规定每个字符串的结束位置都添加一个字符’\0’,表示字符串的结束。例如,有以下字符串:

"C language"

以上为一个字符串常量,有9个英文字符和一个空格,共10个字符。但C编译器在保存这个字符串常量的时候,将占用11个内存单元,因为最后要添加一个字符’\0’作为结尾。

字符’\0’代表ASCII码为0的字符,该字符是一个不可显示的字符,用它来作为字符串结束标志不会产生附加的操作或增加有效字符。

C编译器为什么要在字符串后面添加一个结束标志呢?在C语言中,将字符串按顺序存储在内存中,与数组的存储方式相同,其实就是该字符串按字符数组的方式来存储。当输出字符串的时候,怎么判断是否到了字符串的结尾呢?如果不在最后添加一个结束标志,就需要知道字符数组的长度。并且在大多数情况下,字符串并不会将字符数组各元素使用完。

为了简化程序,C编译器规定字符’\0’为字符串的结束标志。在遇到字符’\0’时,表示字符串结束,由它前面的字符组成字符串,不再处理后面的内容。

有了结束标志’\0’之后,字符数组的长度就显得不是很重要了。C语言提供的处理字符串的库函数基本上都是靠检测’\0’的位置来判断字符串是否结束的,而不是根据数组的长度来决定字符串长度的。

了解了字符串的结束标志以后,就可以通过在字符数组的最后添加一个结束标志得到一个字符串,例如:

char name[10]={'w','u','y','u','n','h','u','i','\0'};

对于字符串常量,C编译器自动添加结束标志。因此,上面的语句改写为以下形式:

char name[10]={"wuyunhui"};

还可以省略大括号,改写成以下形式:

char name[10]="wuyunhui";

使用以下方式为字符数组赋初值时,要确保字符数组定义的长度比字符串常量中字符数目多1;否则编译器将提示错误。为了避免出现这种不必要的错误提示,一般在定义字符数组并使用字符串常量进行初始化时,可省略数组长度的常量表达式,使用以下形式:

char name[]="wuyunhui";

这样,编译器将跟根据字符串常量的字符量自动匹配内存空间。

如果声明的字符数组在程序中还需要保存更长的字符串,在声明时就必须定义一个足够大的数组长度。

6.4.3 字符的输入/输出

在前面的程序中,在scanf函数中使用格式化字符“%c"可逐个接收用户输入的字符;在printf函数中使用格式字符"%c"可逐个输出字符数组中的字符。

了解字符串的知识后,还可以有更好的方法输入或输出字符串,就是使用格式字符"%s"将整个字符串一次性输入或输出。

#include<stdio.h>

int main()
{
    char name[10];
    int i;
    
    printf("请输入你的姓名(10个字符以内):");
    scanf("%s",name);
    
    printf("你的姓名是:%s",name);
    return 0;
}

编译运行以上程序,在提示字符之后输入一个字符串,按回车键后,程序马上将输入的内容输出在屏幕上。

在printf()函数汇总用格式字符"%s"输出字符时,输出项必须是字符串或字符数组名,而不能是数组元素名。例如,以下程序是错误的:

printf("%s\n",name[3]);

6.5 拓展训练

6.5.1 数组求解斐波那契数列

用数组实现输出斐波那契数列数列的前20项。

#include<stdio.h>

int main()
{
	int i;
	int x[20] = { 1,1 };

	for (i = 2; i < 20; i++)
	{
		x[i] = x[i - 1] + x[i - 2];
	}
	for (i = 0; i < 20; i++)
		printf("%d ", x[i]);

	return 0;
}

6.5.2 采用冒泡排序法对数据进行排序

从键盘输入10个数,将这10个数从小到大排列输出至屏幕。

#include<stdio.h>

int main()
{
	int x[10], i, j, k;
	printf("输入10个数:");
	for (i = 0; i < 10; i++)
		scanf("%d", &x[i]);

	for(i=0;i<9;i++)
		for(j=9;j>i;j--)
			if (x[i] > x[j])
			{
				k = x[i];
				x[i] = x[j];
				x[j] = k;
			}
	printf("从小到大排序输出为:\n");
	for (i = 0; i < 10; i++)
		printf("%d ", x[i]);

	return 0;
}

6.5.3 通过数组计算学生的平均成绩

现有5个学生4门科目,已知所有学生的各科成绩,计算每个学生的平均成绩和每门科目的平均成绩。

#include<stdio.h>

int main()
{
	float score[5][4] = { {77,83,84,79},{70,80,84,90},{76,91,88,84},{60,76,69,71},{76,77,80,70} };
	float x[5] = { 0,0,0,0,0 }, y[4] = { 0,0,0,0 };
	int i, j;

	for (i = 0; i < 5; i++)
		for (j = 0; j < 4; j++)
			x[i] = x[i] + score[i][j];

	for (j = 0; j < 4; j++)
		for (i = 0; i < 5; i++)
			y[j] = y[j] + score[i][j];

	for (i = 0; i < 5; i++)
		printf("第%d个学生的平均成绩为%.3f\n", i+1, x[i] / 4);
	for (j = 0; j < 4; j++)
		printf("第%d个科目的平均成绩为%.3f\n", j+1, y[j] / 5);

	return 0;


}

6.5.4 字符串反转

将用户输入的一个字符串反转。

由于用户输入的字符串的长度是不确定的,首先要确定用户输入字符串中字符的数量,然后才能开始进行字符交换。在C语言的函数库中,有一个strlen()函数可以获取字符串的长度,在此先不使用这个方法。

#include<stdio.h>

int main()
{
	char arr[80], temp;
	int i, j;

	printf("请输入一个字符串(80个字符以内):");
	scanf("%s", arr);

	printf("输入的字符串为:%s\n\n", arr);

	for (i = 0; arr[i]; i++)   //使用for循环查找字符串的结束标志确定字符串的长度
		;

	for (j = 0; j < i / 2; j++) // 使用for循环实现数组首尾交换
	{
		temp = arr[j];
		arr[j] = arr[i - j - 1];
		arr[i - j - 1] = temp;
	}

	printf("反转之后的字符串为:%s\n", arr);

	return 0;
}

7. 用函数模块化代码结构

7.1 函数概述

C语言的源程序是由一个或多个函数组成的。C语言的所有工作都是由各种各样的函数来完成的,所以也把C语言称为函数式语言。

7.1.1 函数的概念

在数学领域,函数是一种关系,这种关系使一个集合的每个元素对应到另一个集合里的唯一元素。在程序设计中也引入了函数这个概念,将一段程序定义为函数,给这个函数0个或多个参数,函数通过运算将返回或输出结果。因此可以说函数是用于完成特殊任务的程序代码的集合。

C语言的函数,使用者只需要知道给函数提供哪些参数,以及函数执行后可以得到什么结果。例如,在使用printf()函数时,向其提供格式字符串和输出列表,printf()函数就会将这些数据输出到屏幕上。在使用printf()函数时,使用者不需要知道完成该函数的程序是怎么编写的,也不需要了解其实现细节。

通过上面的介绍可知道函数的第一个功能就是封装代码。所谓封装代码,就是将完成某一功能的代码封装为一个函数。供其他函数或以后的程序调用。使用者不需要再去重复编写完成类似功能的代码,从而提高开发效率。

使用函数的第二个功能就是进行结构化程序设计。“结构化程序设计”是一种强调程序模块化和应用程序内的分层结构的方法。在C语言中,实现结构化程序设计的最直接方法是合理地使用函数将应用分解为离散的逻辑单元。调试各个单独的单元比调试整个程序更容易一点。还可以在其他程序中使用为某个程序开发的函数,而通常只需要少量修改甚至不修改。

7.1.2 函数的分裂

按照不同的分类方法,C语言的函数可以进行多种分类:

1. 按函数定义分类

按函数的定义分类,C语言的函数可以分为库函数和自定义函数两类。

  • 库函数:是随C编译器一起提供给用户的函数,这些函数不需要用户编写代码,已经由系统提供编译好的链接库文件。要调用库函数,只需要在程序代码的最前面使用包含指令#include引用含有该库函数原型的头文件即可。
  • 自定义函数:是由用户根据需要编写的函数。对于用户自定义函数,不仅要在程序中定义函数本身,而且在主调函数模块中还必须对该函数进行类型说明才能使用。

ANSI C随编译器提供了丰富的库函数,包括字符类型分类函数、转换函数、目录路径函数、诊断函数、图形函数、输入/输出函数、接口函数、字符串函数、内存管理函数、数学函数、日期和时间函数、进程控制函数、其他函数等等。不同的编译器可能还提供了一些独有的函数,用户可以通过查看编译器使用手册了解相关的函数。

2. 按功能分类

C语言的函数具有其他程序设计语言中函数和过程的双重功能。在大部分程序设计语言中,有返回值的被称为函数,无返回值的被称为过程。在C语言中,这两者统称为函数,从这个角度看可以把函数分为有返回值和无返回值两种。

  • 有返回值函数:此类函数被调用执行完之后将向调用者返回一个运行结果,称为函数返回值。如数学函数即此类函数。由用户定义的这种要返回函数值的函数,必须在函数定义和函数说明汇总明确返回值的类型。
  • 无返回值函数:此类函数用于完成某项特定的处理任务,执行完成后不向调用者返回函数值。这类函数类似于其他语言的过程。由于函数无须返回值,所以用户在自定义此类函数时可以指定它的类型为空类型。空类型说明符为"void"。

7.1.3 定义函数

注意:对于库函数,用户可以在程序中随时调用。而自定义函数则必须由用户对齐进行定义,编写好完成相应功能的代码,才能被其他函数调用。

定义函数的语法格式如下:

返回类型 函数名(参数列表)
{
    函数实现过程(函数体)
    return 表达式;
}

其中:

  • 返回类型:说明函数的返回的数据类型,可以是除数组以外的任何数据类型。如果函数不返回值,则返回类型设置为void。
  • 函数名:标识函数的名称。其命名规则必须符号标识符的相关规定,且不能和其他标识符重名。
  • 参数列表:是用逗号分割的一系列变量名和他们相关类型的列表。参数接收该函数调用时由调用者传入的变量值。函数可以没有参数,此时参数列表为空。可以将关键字void放在括号中,来明确指定函数无参数。
  • 函数体:在大括号内的为函数体。可使用C的各种语句编写完成指定功能的代码,如声明变量、执行表达式、调用其他函数等等。最后使用return返回函数的运行结果。如果函数没有返回值,则不用return语句。

在处理数值数据时,经常遇到比较大小的情况,编写一个返回最大值的函数,具有一定的通用性。为了测试自定义函数的功能,需要在主函数main()中编写相应的测试代码。

#include<stdio.h>

int int_max(int m, int n);

int main()
{
    int i,j,m;
    
    printf("请输入两个整数:");
    scanf("%d %d",&i,&j);
    
    m=int_max(i,j);
    printf("最大整数为:%d\n",m);
    
    return 0;
}

int int_max(int m, int n)
{
    int max=m;
    
    if(max<n)
        max=n;
    return max;
}

在以上程序中,共有两个函数:一个是测试用的主函数main(),由516行构成;一个是返回最大值的函数int_max(),由1825行构成。

7.1.4 main()函数

main()函数是C语言中的一个特殊函数。作为程序的入口,在大部分程序中,main()函数中都是一些调用其他函数的代码,例如:

int main()
{
    welcome();
    getinput();
    procdata();
    outdata();
    reurn 0;
}

在以上示意代码中,将程序的功能分到各自定义函数中,main()函数只是作为一个集中调用的地方。

在C语言中,所有函数的定义都是平等的。在一个函数的函数体内,可以调用已经定义的函数,但不能再定义另一个函数,即不能嵌套定义。main()函数作为入口程序,可以调用其他函数,而不允许被其他函数调用。C程序的执行总是从main()函数开始的,完成对其他函数的调用后再返回到main()函数,最后由main()函数结束整个程序。

7.2 函数的工作过程

7.2.1 程序结构

提示:C语言程序是由一个或多个函数组成的,这些函数可以全部保存在一个扩展名为.c的文件中,也可以分别保存在不同的文件中。

假设一个C源程序有3个函数,分别是:main()、func_a()、func_b(),其结构代码可能如下:

#include<stdio.h>

int func_a();
int func_b();

int main()
{
    ...
    func_a()
    ... 
    return 0;
}

int func_a()
{
    ...
    func_b()
    ...
    return 0;
}

int func_b()
{
    完成相应的功能的语句;
    return 0;
}

以上为示意代码,不能直接输入到计算机去编译运行。在以上示意代码中,第612行为main()函数代码,第1420行为func_a()函数代码,第22~26行为func_b()函数代码。

第3、4行为函数原型声明,用于告诉编译器程序后面调用的自定义函数的格式。

C程序中所有函数在逻辑上都是平行的,一个函数不包含另一个函数的定义,在这个基础上,定义每个函数的位置没有具体要求。

7.2.2 函数执行过程

在C语言中,程序语句都包含在各个函数之中。这些函数中的语句不会主动执行,必须通过另一个函数调用,程序才会将控制权转移到该函数的内部,执行函数体中的语句。

上面介绍的示意代码有3个函数。其执行过程如下:

程序首先进入第6行的main()函数,执行其中的语句;当执行到第9行的func_a()函数时,程序将会跳转到第14行,从func_a()函数的入口开始执行该函数。

在func_a()函数中执行到第17行的时候,调用func_b()函数。这时,程序将跳转到第22行,从func_b()函数的入口开始执行该函数。

在func_a()函数中执行到第19行的时,执行return语句结束func_a()函数的执行,返回其调用函数main()中,执行main()中调用func_a()函数的下一条语句(第10行)。然后顺序执行main()函数中后面的语句,执行到return语句时,结束main()函数的执行,返回操作系统。

7.3 编写函数

7.3.1 函数头

函数头用来标识一个函数代码的开始,是函数的入口。函数体就像是一个工厂的大门,上面需要标准你的工厂是干什么的、类型是什么、工厂名是什么、员工都有谁。函数头由返回类型、函数名、参数列表三部分组成。

7.3.2 返回类型

函数代码执行可将运行结果返回给调用程序。函数的返回类型确定了函数执行后返回值的类型,可设置为各种数据类型,比如char、int、long、long long、float、double、long double等类型,但是不能返回数组类型。

如果函数没有返回值,则应该将返回值类型设置为void。如果省略返回值类型,根据C语言的规定,就会被编译器作为返回int型处理,而不是void类型。例如:

#include<stdio.h>

add(int a,int b)
{
    return a+b;
}

itn main()
{
    printf("1+2=%d",add(1,2));
    reurn 0;
}

在上面程序中,第3-6行定义了一个函数add()。用来将两个函数相加。在第3行函数体部分,省略了函数的返回值类型,与给函数设置的返回类型为int相同。

在8~12行为main()函数,在该函数的第10行语句中,使用printf()函数输出值,其输出列表为add(1,2)。将调用add()函数进行运算,并返回运算结果为3。

在以上实例可以看出,省略函数的返回类型时,将使用int型作为返回类型。

在上面的程序中,如果将第3行语句改写为以下形式:

void add(int a,int b)

在编译程序时,将提示第5行出错,因为对无返回值的函数使用了return语句。

提示:为了避免混乱,对于任何函数都必须一个不漏的指定其类型。如果函数没有返回值,则一定要声明为void类型。这既是对程序良好可读性的要求,也是对编程规范性的要求。

7.3.3 参数列表

函数中的代码用来对数据进行加工处理,最后返回处理结果。在调用函数时,通过参数将数据交给函数,再由函数进行加工处理。

函数参数列表是用逗号分隔的一系列数据类型和变量名列表。函数可以有多个参数,也可以没有参数。

当函数参数列表为多个时,ANSI C要求在每个变量名前声明其类型。例如,一下代码为函数func_a()声明3个int型参数。

int func_a(int a,b,c)

这种写法是错误的,应该写位以下的格式:

int func_a(int a,int b,int c)

如果函数没有参数,则函数名后面的括号也不能省略,可写位以下样式:

int func_a()

与函数的返回类型一样,如果函数无参数,则最好在参数列表中使用关键字void。可将上面的代码改写为以下样式:

int func_a(void)

在参数列表中列出的变量名称为形式参数,简称形参,其变量名减没试过在函数中被引用,在调用函数时给出了每个参数的具体值,这些值被称为实参。

7.3.4 函数体

函数体位于函数头下方,用一对大括号括起来。函数要实现的具体功能依靠函数体中的语句来完成,最后通过return语句返回函数体的运行结果。

在函数体中,可使用C语言的各种语句,但是不能在函数体内定义另外一个函数。

如果函数要完成的功能很复杂,代码行数很多,导致函数体过长,则可将一个函数的功能进行细分,对每个小功能编写一个函数。一般情况下,每个函数的代码行在30行左右为好。

在函数体内,可根据需要声明变量。函数体内声明的变量被称为局部变量,只在函数体内有效。

函数体执行到一个return语句或函数体最外层大括号时,将结束执行,返回调用程序,并从调用本函数的下一条语句开始执行。

7.3.5 函数原型

函数原型可理解为对函数的声明,与变量声明类似。在使用函数之前,函数原型的作用是讲有关函数的信息告知编译器,在调用函数时由编译器检查函数的返回值、参数是否正确。

函数原型与函数头相同,只是后面多了一个分号。如果没有这个分号,则表示紧随其后的是函数定义代码。

传统的声明函数的方式只声明了函数的返回类型,这是不够准确的,容易导致运行错误,例如,使用之前编写int_max()函数输出以下数据:

#include<stdio.h>

int int_max1();

int main()
{
	printf("数据8和10的最大数为:%d\n", int_max1(8));
	printf("数据8和10的最大数为:%d\n", int_max1(8.0, 10.0));
	printf("数据8和10的最大数为:%d\n", int_max1(8, 10));

	return 0;
}

int int_max1(int m, int n)
{
	int max = m;

	if (max < n)
		max = n;
	return max;
}

在以上程序中,第3行使用了传统的方式声明了函数类型,只给出了函数返回类型和函数名,而没有给出函数的参数列表。这样的函数原型,编译器不知道参数的数量和类型,所以在调用函数时,将不会检查参数的数量和类型。

在第7行的printf()语句中,只给出了int_max1()函数的一个参数8,。由于编译器不检查参数的数量和类型,一次你,虽然函数int_max1()需要两个参数,但是该程序仍然能顺利的编译。

在第8行调用int_max1()函数时传入了两个参数,但其类型为double型(实型常量默认是double型,而不是float型)。而int_max1()函数需要的是两个整型参数,这时因为C编译器不知该函数参数的类型,所以并不会对其进行类型转换。

第9行按照int_max1()函数的期望传入了输了和类型都相符的两个整型参数。

编译以上程序只有第9行得到了正确的结果,第7、8行输出的都是无意义的数。

7.4 函数的参数

7.4.1 参数传递过程

在定义函数时,在函数头中列出的参数列表为形参列表;而在调用函数时,需要将实际的参数值传递给函数,这些被传递的值被称为实参。

在调用函数时,将实参放在函数名后面的括号中,这些数据将传递给函数的形参,供函数内部的代码使用。参数传递过程就是将实参赋值给形参的过程。

如果函数有多个参数,则参数必须按照函数定义时的参数的顺序依次书写。编译器会将这些实参按顺序赋值给对应的形参。

注意:实参可以是常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传递给形参。实参和形参的数量、类型、书写顺序等必须保持一致,否则会发生类型不匹配的错误。

7.4.2 值调用

在调用函数时,形参变量只有函数被调用时才被分配到内存单元,函数执行结束后将释放所分配的内存单元。因此,形参只有在函数内部才有效,函数调用结束返回主调函数后将不能再使用该形参变量。

另外,函数调用中发生的数据传递是单向的。即只能把实参的值传递给形参,不能把形参的值反向的传递给实参。因此,在函数调用过程中,形参的值发生改变,而实参的值不会改变。

这种方式称为值调用,即把实参的值复制一份到函数对应的形参中。在这种情况下,函数对形参的修改不会影响实参的值。这是最常用的参数传递方式。

例如,以下程序演示这种效果:

#include<stdio.h>

int add1(int a, int b);

int main()
{
	int i = 5, j = 10;

	printf("调用函数前:i=%d,j=%d\n", i, j);
	printf("i+j=%d\n", add1(i, j));
	printf("调用函数后:i=%d,j=%d\n", i, j);

	return 0;
}

int add1(int a, int b)
{
	a += b;
	return a;
}

编译运行以上程序,从程序的运行结果可以看出,调用函数add1()对两个变量到的值进行累加后,作为实参的两个变量的值并没有改变。

在内存中实参与形参会占用不同的内存单。当main()函数调用add1()函数时,将变量i和j的值复制到add1()函数的形参所在的内存单元;在add()函数内部对变量a进行累加时,只有变量a的值变化,main()函数中的变量i和j不会改变。

7.4.3 引用调用

注意:在C语言中,大部分函数都是通过只调用的方法来传递参数的,并将形参和实参分割开来。这样,函数一般无法修改主函数中的变量。

在极少数的情况下,可能需要函数修改主函数中的值。首先来看一个例子。在对数据进行排序时,经常需要进行两个数的交换。编写一个交换函数,用于将两个数进行互换。

在两个数进行交换时,一般需要引进一个临时变量。使用以下代码可将变量a和变量b中的值互换。

t=a;
a=b;
b=t;

根据以上思路,编写交换函数swap(),并编写main()函数测试swap()函数的运行结果:

#include<stdio.h>

void swap(int a, int b);

int main()
{
	int i = 5, j = 10;

	printf("调用函数前:i=%d,j=%d\n", i, j);
	swap(i, j);
	printf("调用函数后:i=%d,j=%d\n", i, j);

	return 0;
}

void swap(int a, int b)
{
	int t;

	t = a;
	a = b;
	b = t;
}

在以上代码中,第3行声明函数原型,swap()函数无返回值,需要两个int型参数。

swap()函数的定义代码在16~23行。

在main()函数中,首先在第9行中输出调用swap()函数前变量i和变量j的值,接着在调用函数swap()交换变量i和变量j的值之后,在第11行再次输出调用swap()函数后变量i和变量j的值。

编译运行以上程序,从运行结果观察,swap()函数没有完成数据交换的任务。检查函数swap()中的程序,没有发现错误。为什么会出现这种情况呢?这是因为,在C语言中是采用值调用的方法传递参数的,在第10行中调用swap()函数时,将变量i和j的值分别复制给swap()函数中的形参a和b,在swap()函数中将变量a和b的值进行了互换,但不会返回到调用函数中。

这时就需要用到引用调用了。所谓引用调用,就是将实参的地址传递给函数的形参,使形参和实参共用一个地址。这样,在函数中使用代码修改变量的值,返回调用函数后,可以同样访问到该地址的值。

在C语言中,使用指针来表示变量的地址。

#include<stdio.h>

void swap(int* a, int* b);

int main()
{
	int i = 5, j = 10;

	printf("调用函数前:i=%d,j=%d\n", i, j);
	swap(&i, &j);
	printf("调用函数后:i=%d,j=%d\n", i, j);

	return 0;
}

void swap(int *a, int *b)
{
	int t;

	t = *a;
	*a = *b;
	*b = t;
}

注意:使用取地址运算符&可以取得一个变量的地址,使用指针运算符*可以取得指定地址的值。

编译运行以上程序,可以观察到结果,该程序完成了数据的交换功能。

7.4.4 数组调用

在函数需要处理大量数据的时候,如果逐个变量传递,则函数的参数列表需要很长,并且有时需要处理的数据数量不确定。这时,可使用数组作为参数,将数组传递到函数中进行处理。将数组用作函数参数有两种形式:一种是把数组中的某个元素当做实参使用,这与使用简单变量的方式相同;另一种是把数组名作为函数的形参和实参使用。

数组中的单个元素与普通变量并无区别,因此他作为函数实参使用时,与普通变量是完全相同的。在发生函数调用时,把作为实参的数组元素的值传递给形参,实现单向的值传递。

警告:在用数组名作为函数参数时,要求形参和相对应的实参都必须是类型相同的数组,都必须要有明确的数组说明。当形参和实参不一致时,将会发生错误。

例如,要编写一个对数组排序的函数sort(),可使用以下函数头():

void sort(int a[5])

在调用以上函数时,需要在函数中定义一个数组,并将数组名作为参数进行调用,具体代码如下:

int arr[5]={3,35,34,29,78};
sort(arr);

用上面的方式定义的函数sort()只能对包含5个元素的数组进行排序。为了使函数具有更好的通用性,可将函数编写为能处理任何元素长度的数组。可以使用以下函数头定义函数:

void sort(int a[],int n)

此时省略了数组元素长度,同时在函数参数列表中增加了一个变量n,用来表示要处理的数组的元素个数。在之前说过,C编译器不会检测数组下标是否越界。因此,在函数头中接收数组的首地址,并知道要处理的元素个数,即可对数组中的数据进行处理。

在用数组名作函数参数时,不是进行值的传递,既不是把实参数组的每一个元素的值赋予形参数组的各个元素。因为实际上形参数组并不存在,编译系统不为形参数组分配内存。之前介绍过,数组名就是数组的首地址。因此在用数组名作为函数参数时所进行的传递只是地址的传递,也就是相当于有了实在的数组。实际上是形参数组和实参数组为同一数组,共同拥有一段内存空间。即数组调用也是按引用调用的方式传递数据。

为了检验数组调用的方式,编写一个数据排序的函数。数据排序一般需要针对大量的数据进行,这时使用数组保存需要排序的数据,调用排序函数对该数组进行排序,然后在主调函数中输出排序后的结果。具体程序如下:

#include<stdio.h>
#define M 10

void sort(int a[], int n);

int main()
{
	int i, arr[M] = { 3,35,34,29,78,22,33,44,66,55 };

	for (i = 0; i < M; i++)
		printf("%d ", arr[i]);
	printf("\n");

	sort(arr, M);
	for (i = 0; i < M; i++)
		printf("%d ", arr[i]);
	printf("\n");

	return 0;
}

void sort(int a[], int n)
{
	int i, j, k;

	for(i=0;i<n-1;i++)
		for (j = i; j < n; j++)
		{
			if (a[i] > a[j])
			{
				k = a[i];
				a[i] = a[j];
				a[j] = k;
			}
		}
}

在以上程序中,第4行声明函数原型,第22~36行定义函数sort()。在该函数中,通过双重循环对数组a中的元素进行排序,使排序结果按从小到大顺序排列。在该函数中,使用数组a接收实参传过来的数组地址,变量n表示数组的长度。有了这两个数据后,即可对数组中的元素进行处理。

在main()函数中,第8行声明并处理数组arr;第10、11行输出数组arr最初的值;第14行调用sort()函数对数组arr中的元素进行排序;第15、16行输出排序后数组arr中各元素的值。

注意:形参数组和实参数组类型必须一致,否则将会引起错误。形参数组和实参数组的长度可以不相同,因为在调用时,只传递首地址而不检查形参数组的长度。当形参数组的长度与实参数组不一致时,虽不至于出现语法错误(编译能够通过),但是程序运行结果将于实际不符。

7.4.5 main()函数的参数

main()函数可以看成一个工厂的总公司,在通常情况下,他可能不生产产品,但是它也是具备生产产品的能力的。在大部分时间里,main()函数始终作为主调函数,也就是说,允许main()函数调用其他函数,并向函数传递参数。事实上,作为C程序入口的main()函数也可以接收云狐输入的参数。但是由于程序中的任何函数均不能调用main()函数,怎么向main()函数传递参数呢。

main()函数的参数只由命令行传递。在操作系统环境下,一条完整的运行命令应包括两部分;命令与相应的参数。其格式为:

命令 参数1 参数2 ... 参数n

此格式也被称为命令行。命令行中的命令就是可执行文件的文件名,其后所跟参数需要用空格分隔,这里的参数即main()函数的参数。

设有以下命令行:

filename str1 str2 str3 ...

命令行与main()函数的参数存在如下关系:

其中,filename为文件名,也就是由C语言源程序程序经编译、链接后生成的可执行文件,其后跟了n个参数。对于main()函数来说,他可以使用两个参数来记录这些数据,其中一个名为argc的参数记录了命令行中命令与参数的个数,在上面的代码中共有四个;另一个参数是一个指针数组char*argv[4],用来记录命令行中命令和参数的字符串。在上面代码总,数组中保存的字符串如下:

argv[0]="filename"
argv[1]="str1"
argv[2]="str2"
argv[3]="str3"

main()函数带参数的形式如下:

int main(int argc,char *argv[])
{
    ...
}

从函数参数的形式上,包含一个整型和一个指针数组。数组的各指针分别指向一个字符串。应当引起注意的是,接收到的指针数组的各指针是从命令行的开始接收的,首先接收到的是命令,其后才是参数。

下面用实例来说明带参数的main()参数的正确使用。下面的实例要求用户在命令行运行程序,在程序名后面跟上用户的名称,将显示一个欢迎信息。

#include<stdio.h>

int main(int argc, char* argv[])
{
	if (argc != 2)
	{
		printf("使用格式:程序名 你的名称!\n");
		exit(1);
	}
	printf("你好,%s\n", argv[1]);
	return 0;
}

编译以上程序,转到windows的命令窗口,切换到程序所在位置,输入命令,得到一个输出结果。

第1次在命令提示符后值输入程序名称,程序将会显示用格式的提示;第2次在命令提示符后输入正确的格式,将在下方输出程序的运行结果。

使用以下程序,可以输出用户输入的所有命令行语句:

#include<stdio.h>

int main(int argc,char *argc[])
{
    int i;
    
    for(i=0;i<argc;i++)
        printf("第%d个参数为:%s\n",i,argv[i]);
    
    return 0;
}

7.5 函数调用

在程序中,通过对函数的调用来执行函数体。前面说过,C语言中所有的函数定义都是平行的,即函数不能嵌套定义。但是函数之间允许相互调用,也允许嵌套调用。习惯上把调用者称为主调函数。函数还可以自己调用自己,称为递归调用。

main()函数时C程序的入口,它可以调用其他函数,而不允许被其他函数调用。

7.5.1 函数调用方式

在C语言中,可以使用很多种方法调用函数:作为单独语句调用、将函数嵌入表达式、将函数作为另一个函数的参数等。

1. 作为单独语句调用

将函数名和参数列表作为一条单独语句,需要在后面加上分号,构成函数语句。例如:

printf("i=%d",i);

以上是最常用的一条语句,使用printf()函数输出一个变量的值。printf()函数将返回一个整型值,表示输出的字符数量。按上面的语句调用该函数,则将舍弃其返回值。

2. 将函数嵌入表达式

如果函数有返回值,则可使用其返回值参与表达式的运算。因此,可将函数嵌入表达式中。最简单的就是赋值表达式。例如,有一个求最大值的函数int_max(),该函数返回两个数中的最大者,可以使用以下表达式:

m=int_max();

以上语句将int_max()函数的返回值保存到变量m当中。

也可以用int_max()函数的返回值直接参加表达式的运算。例如:

m=int_max(a,b)*c+10;

以上语句将int_max()函数的返回值与变量c相乘再加上10的结果保存到变量m中。

注意:如果函数的返回类型是void,则该函数不能嵌入表达式中。

3. 将函数作为另一个函数的参数

将函数书写在另一个函数调用的实参位置,这种情况是把该函数的返回值作为实参进行传递,因此要求该函数必须有返回值。例如:

printf("最大值为:%d\n",int-max(a,b));

以上语句调用函数int_max()求出变量a、b的最大值,使用printf()函数将其输出。在上面的语句中,int_max()函数调用的返回值又作为printf()函数的实参使用。

注意:如果函数的返回类型是void,则该函数不能出现在另一个函数的实参中。

7.5.2 被调函数的说明

被调函数的说明,其实就是声明函数原型。前面已经介绍了函数原型的相关内容,下面再介绍集中可以省略函数原型声明的情况。

  • 若被调函数的返回值设计整型或字符型,则可以不对被调函数进行说明而直接调用。这时系统将对被调函数返回值按整型处理。
  • 当被调函数的函数定义出现在主调函数之前时,在主调函数中也可以不对被调函数进行声明而直接调用。例如,在使用main()函数调用个各函数时,若将各函数的定义放在main()函数之前,则可以省略对函数原型的声明。
  • 对库函数的调用不需要编写代码声明其函数原型,因为库函数的函数原型包含在相应的头文件中。在使用库函数的时候,只需要使用#include命令包含相应的头文件即可。

被调函数的说明既可以在主调函数内部进行,也可在所有函数之外进行。例如,以下两种方式程序都能正确运行。

提示:虽然在有的情况下可以省略函数说明,但是这不是一个好习惯,建议对每个自定义函数都进行函数原型声明。

以下程序在主调函数内部声明被调函数:

int main()
{
    int i,arr[M]={1,2,3,4,5,6};
    
    void sort(int a[],int n);
    ...
    sort(arr,M);
    ...
    return 0;
}

void sort(int a[],int n)
{
    ...
}

以下程序在所有函数外部声明被调函数:

void sort(int a[],int n);

int main()
{
    int i,arr[M]={1,2,3,4,5,6};
    
    ...
    sort(arr,M);
    ...
    return 0;
}

void sort(int a[],int n)
{
    ...
}

7.5.3 返回函数结果

函数的返回类型若为void,则函数不需要返回结果。也就是说,这类函数不返回值给主调函数,而只是完成某项指定功能。

若函数的返回类型不为void,则必须在函数体中使用return语句返回一个值。

函数结果返回的形式如下:

return 表达式;

return (表达式);

return语句有两方面的作用:一是结束函数的运行;二是带着表达式的运算结果返回主调函数。

return语句后的表达式为要返回的值,return语句后面也可为空表达式,即不返回值。在C89标准中,如果非void函数执行不含值的return语句,则将返回一个无用值。在C99标准中,非void函数必须要使用有返回值的return语句。

在函数体中,也可以使用多个return语句:

int int_max(int m,int n)
{
    if(m>n)
        return m;
    else
        return n;
}

在以上代码中使用了两个return语句。当m大于n,范湖变量m的值,否则返回变量n的值。

无论函数体中有多少个return语句,函数只有执行到第一个return语句,就会结束函数体的执行,返回调用函数。

在函数体中使用多个return语句,使函数具有多个出口,这不是一个好习惯。一般情况下,尽量使函数只有一个return语句,使函数具有唯一的入口和唯一的出口。

一般情况下,return语句返回的结果类型应于函数类型一致,如果两者不一致,则编译器会把return语句后的表达式转换为函数类型。例如:

int func_a(double b)
{
    ...
    return b;
}

return语句后面是一个double类型的数据,但是函数返回类型为int型,这时编译器就会将变量b转换为int型返回。

return语句只能返回一个值,而不能同时返回多个值。因此,以下语句想让return语句返回两个值是错误的:

return a,b;

若改写为以下形式:

return a;
return b;

也达不到返回两个值的效果,因为执行到第一个return语句,返回变量a的值之后,函数就也结束了,将返回到主调函数,不可能再去执行第二个return语句。

7.5.4 函数的嵌套使用

C语言中不允许定义嵌套的函数,即在一个函数中定义另一个函数。但是C语言允许在一个函数的函数体中调用另一个函数,就如果main()函数中调用其他各函数一样。这种情况称为函数的嵌套调用,即在被调用函数中又调用其他函数。

例如,编写一个函数求三个数的最大值:

#include<stdio.h>

int int_max2(int a, int b);
int max(int a, int b, int c);

int main()
{
	int i, j, k;

	printf("输入3个整数:");
	scanf("%d %d %d", &i, &j, &k);

	printf("最大数为:%d\n", max(i, j, k));
	
	return 0;
}

int max(int a, int b, int c)
{
	int m;

	m = int_max2(a, b);
	m = int_max2(m, c);
	return m;
}

int int_max2(int a, int b)
{
	return (a > b) ? a : b;
}

在以上程序中定义了3个函数,分别是:入口函数main()、求3个数中最大值函数max()、求两个数中最大值函数int_max2()。

在第13行调用了max()函数,得到3个数中的最大值。

7.6 递归函数

所谓的递归函数,即自调函数,在函数体内直接或间接的调用自己,即函数的嵌套是函数本身。就像在一条流水线中,一件产品可能会被重复的加工多次。C语言中的函数支持递归调用。

7.6.1 函数的递归调用

函数的递归调用分两种情况:直接递归和间接递归。

  • 直接递归:即在函数中调用函数本身。
  • 间接递归:即间接的调用一个函数,如func_a()函数调用func_b(),func_b()又调用func_a()函数。间接递归用的不多。

理解递归的最常见的一个例子就是编写程序求阶乘。所谓阶乘就是从1到指定数之间的所有自然数相乘的结果。n的阶乘为:

n*(n-1)*(n-2)*...*2*1

例如,5的阶乘为:

5*4*3*2*1=120

编写该程序,能根据用户输入的值计算出阶乘。

#include<stdio.h>

int fact(int n);

int main()
{
	int i;

	printf("输入要求阶乘的一个整数:");
	scanf("%d", &i);

	printf("%d的阶乘为:%d\n", i, fact(i));

	return 0;
}

int fact(int n)
{
	if (n == 1)
		return 1;
	else
		return n * fact(n - 1);
}

注意:递归一定要选择好判断的条件,否则程序会无限递归而出错。

在以上程序中,函数fact()定义在17~23行,是一个递归函数。在22行中,程序又调用了自身。

7.6.2 递归的基本原理

要理解递归,首先应理解一种数据结构–堆栈的概念。

栈是一个后进先出的压入和弹出式数据结构。在程序运行时,系统每次向栈中压入一个对象,然后栈指针向下移动一个位置。当系统从栈中弹出一个对象时,最近进栈的对象会被弹出,然后栈指针向上移动一个位置。栈在每个程序中都是存在的,它不需要程序员编写代码去维护,而是又系统自动处理。

C编译器处理函数调用时,就是使用栈来保存数据的。当主调函数调用另一个函数的时候,C编译器将主调函数的所有参数和返回地址压入栈中,栈指针将移到合适的位置来容纳这些数据。

当执行被调函数时,编译器将栈中的实参数据弹出,赋值给函数的形参。在被调函数执行期间,还可以利用栈来保存函数执行时的局部变量。当被调函数准备返回时,系统将弹出栈中所有当前函数压入栈中的值,这时栈指针移动到被调函数刚开始执行时的位置。接着被调函数返回,系统从栈中弹出返回地址,主调函数就可以继续执行了。

递归之所以能实现,是因为函数的每个执行过程都在栈中有自己的形参和局部变量的拷贝,这些拷贝和函数的其他执行过程毫不相干。这种机制是其他大多数程序设计语言实现子程序结构的基础,使得递归称为可能。嘉定某个主调函数调用了一个被调函数,再假定被调函数又反过来调用了主调函数,这二个调用就被称为调用函数的递归,因为它发生在调用函数的当前执行过程运行完毕之前。而且因为这个原先的主调函数、现在被调函数在栈中较低的位置有它独立的一组参数和自变量,原先的参数和变量将不受影响,所以递归能正常工作。

7.6.3 递归函数的设计

在设计递归函数的时候,因为函数将调用自身,如果设置不当,则可能导致程序将计入死循环状态。因此,在设置递归函数的时候,必须为函数设置终止递归的条件。在设计一个递归函数时,在函数内至少有可以终止此递归的条件。如果没有一个正常情况下可以满足的条件,则将程序陷入执行无限循环的高度危险之中。如在递归求阶乘的程序中设置当函数变量n的值等于1时,终止递归,返回上层主调函数。

再看一个经典问题:汉诺塔问题。这是印度的一个古老的传说。开天辟地的神勃拉玛在一座庙里留下了3个金刚石的棒,第1跟上面套着64个圆盘,最大的圆盘在最底下,其余一个比一个小,一次叠上去。众僧将该金刚石棒中的圆盘逐个移动到另一个棒上。在移动过程中,规定一次只能移动一个圆盘,且圆盘放在棒上时,大的不能放在小的上面。可利用中间一根棒作为辅助。按照这个规则,众僧耗尽毕生精力也不可能完成圆盘的移动,因为移动的圆盘的次数是一个天文数字(64个圆盘需要移动的次数为2的64次方)。

用计算机编写程序来解决该问题,假设A、B、C3根棒,初始状态时,A棒上放着若干个圆盘,将其移动到C棒上,中途可在B棒上暂时放置圆盘。

解题思路:

(1)如果只有1个圆盘,则把圆盘直接放到C棒上,完成任务

(2)如果圆盘的数量>1,则移动圆盘可以分为3步。

  • 第一步:把A棒上的n-1个圆盘放到B棒上。
  • 第二步:把A棒上的一个圆盘放到C棒上。
  • 第三步:把B棒上的n-1个圆盘放到C棒上。

其中第1步和第3步又是移动多个圆盘的过程,又可以重复上面的3个步骤来完成这两步中多个盘片的移动,这样就完成了一个递归的过程。

#include<stdio.h>

void hanoi(int n, char a, char b, char c);
long count;

int main()
{
	int h;

	printf("输入圆盘的数量:");
	scanf("%d", &h);

	count = 0;
	hanoi(h, 'A', 'B', 'C');

	return 0;
}

void hanoi(int n, char a, char b, char c)
{
	if (n == 1)
	{
		printf("第%d次,%c棒-->%c棒\n", ++count, a, c);
	}
	else
	{
		hanoi(n - 1, a, c, b);
		printf("第%d次,%c棒-->%c棒\n", ++count, a, c);
		hanoi(n - 1, b, a, c);
	}
}

大多数的递归都可以使用循环来代替。循环程序在一个程序内部完成,不会产生调用函数时的压入栈、返回时的弹出栈等操作,课节约系统开销,因此使用循环相对于递归调用可以大幅度提高系统性能。

#include<stdio.h>

int fact(int n);

int main()
{
    int i;
    
    printf("输入要求阶乘的一个整数:");
    scanf("%d",&i);
    
    printf("%d的阶乘结果为:%d\n",i,fact(i));
    
    return 0;
}

int fact(int n)
{
    int i,m;
    
    for(m=1,i=n;i;i--)
        m*=i;
    return m;
}

7.6.4 递归的优缺点

了解递归函数的设计方法和工作原理后,下面对递归的优缺点进行总结。

在函数中使用递归的好处有:程序代码更加简洁清晰、可读性好;有的算法用递归表示要比用循环表示简洁精炼,而且某些问题,特别是与人工智能相关的问题,更适宜用递归方法,如八皇后问题、汉诺塔问题;有的算法用递归实现,而用循环却不一定能实现。

递归的缺点:递归的内部实现要消耗额外的空间和时间,需要执行一些列的压栈和出栈等操作。如果递归层次太深,则还看导致堆栈溢出。

7.7 拓展训练

7.7.1 训练1:计算1到x的五次方累加和

#include<stdio.h>

long f(int x)
{
	long p = x;
	int i;
	for (i = 1; i < 5; i++)
		p *= x;
	return p;
}
long s(int x)
{
	long s = 0;
	int i;
	for (i = 1; i <= x; i++)
		s = s + f(i);
	return  s;
}
void main()
{
	int x;
	printf("输入数字:");
	scanf("%d", &x);
	printf("the number is %d\n", s(x));
}

7.7.2 训练2:统计字符出现次数

编写一个程序,统计一个字符串中一个字符出现的次数。

#include<stdio.h>

void f1(char* s)
{
	char c = 's';
	int i = 0, m = 0;
	while (s[i] != '\0')
	{
		if (s[i] == 's')
			m++;
		i++;
	}
	printf("the num is %d\n", m);
}

void main()
{
	char s[50];
	gets(s);
	f1(s);
}

7.3.3 训练3:通过函数计算两个数的差值

编写一个函数,计算两个数的差值并输出结果。

#include<stdio.h>

int f2(int a, int b)
{
	int c;
	if (a > b)
		c = a - b;
	else
		c = b - a;
	return c;
}

void main()
{
	int x, y, z;

	printf("输入两个数:");
	scanf("%d %d", &x, &y);

	z = f2(x, y);

	printf("两个数的差值为:%d\n", z);
}

7.4.4 训练4:十进制数转换为二进制数

#include<stdio.h>

void s1(int x)
{
	int i = 0, j, s[100];
	while (1)
	{
		s[i++] = x % 2;
		x = x / 2;
		if (x == 0)
			break;
	}
	for (j = i - 1; j >= 0; j--)
		printf("%d", s[j]);
}
void main()
{
	int a;

	printf("输入要转换的十进制数:");
	scanf("%d", &a);

	s1(a);
	printf("\n");
}

7.8 函数传址方式

传址方式是指将实参的地址传递给形参,函数将对这一地址进行操作,因此形参值的变化会影响实参的值。在传址方式中,实参可以是变量地址或指针名、一维数组、字符数组或多维数组等。

函数的地址就像是门牌号,我们无法通过人脑直接记住,需要通过一个工具来记录地址的值,这个工具就像是一个记录本,有了记录本我们便可以传递地址了,这个记录本就是指针。

7.8.1 变量地址做实参

通过传址方式交换两个数的数值。

提示:传址方式是指将变量的地址传递给形参,对变量的地址进行操作。因此,传值方式中的形参值的变换会影响实参的值。

#include<stdio.h>
void ff(int *m,int *n)
{
    int t;
    t=*m;
    *m=*n;
    *n=t;
    printf("m=%d,n=%d\n",*m,*n);
}
void main()
{
    int m=30,n=40;
    ff(&m,&n);
    printf("m=%d,n=%d\n",m,n);
}

7.8.2 一维数组作为实参

一维数组名表示数组的首地址,因此可以作为实参传递给参数。

从键盘输入两个字符串,比较这个两个字符串是否相等。若两个字符串相等则输出相等,否则输出不相等。

提示:用两个字符数组来保存输入的字符串,对两个字符串进行比较,若相等,则输出相等。若不相等,则输出不相等。

#include<stdio.h>
void ff(char s1[], char s2[])
{
    int i = 0;
    while (s1[i] == s2[i] && s1[i] != 0)
        i++;
    if (s1[i] == '\0' && s2[i] == '\0')
        printf("相等\n");
    else
        printf("不相等\n");
}
void main()
{
    char s1[30], s2[30];
    printf("请输入两个字符串:\n");
    gets(s1);
    gets(s2);
    ff(s1, s2);
}

7.8.3 字符串作为实参

字符串作为实参,形参应为字符串型的指针变量。

提示:计算一个字符串中字符s出现的次数,可以设置一个计数器。计数器初值为0,将字符s和字符串中的字符逐个比较,若相等则计数器加1,否则计数器的值不变。

#include<stdio.h>
void fff(char* s)
{
    char c = 's';
    int i = 0, m = 0;
    for (; s[i] != 0; i++)
    {
        if (s[i] == 's')
            m++;
    }
    printf("出现次数为:%d\n",m);
}
void main()
{
    char s[100];
    gets(s);
    fff(s);
}

7.9 形参和实参

函数的参数分为实参和形参。在函数的调用过程中,实参的值将会传递非形参,但形参不能传递个实参。函数参数的传递分为传值方式和传址方式两种,之前介绍了传址方式,这里将介绍传值方式。

当要将函常量、变量传递给函数时,即可采用传值方式。采用传值方式时,形参值的改变不会影响实参的值,其中形参的类型和个数应于实参一致。

提示:传值方式是直接将实参传递给函数中的形参,然后通过函数中的变量进行操作计算得出结果。

#include<stdio.h>
void z(int m,int n)
{
    int t;
    t=m;
    m=n;
    n=t;
    printf("m=%d,n=%d\n",m,n);
}
void main()
{
    int m=30,n=40;
    z(m,n);
    printf("m=%d,n=%d\n",m,n);
}

注意:

  1. 传值方式是将实参的值传递给形参。形参可以与实参同名,形参的变化不会影响实参值的变化。
  2. 传值方式中return语句只能返回一个值。

8. 算法在C语言中的应用

算法是对特定的问题求解步骤的描述。对于同一个问题可以用不同的算法来求解,而不同的算法,解决问题的时间和复杂性都会不同,程序员可以根据算法的可读性和效率等进行取舍。针对不同的数据保存方式也会有不同的算法。

8.1 算法的定义

C语言中的程序其实就是由数据结构和算法组成的。我们先构建一个用于表示问题的模型,然后用算法改进这个模型,就能解决我们想要解决的问题。算法就像程序的灵魂,程序的好与坏是由是由的算法的好坏所决定的。一个好的程序必定有一个好的算法,能够方便的解决问题。评价一个算法的好坏,可以通过空间复杂度和时间复杂度进行判断。

8.2 常见算法

8.2.1 冒泡排序法

冒泡排序法就像它的名字一样,每次比较后较大的数就像泡泡一样冒到前面。冒泡排序法使用的是交换排序。基本思路就是:对数组中尚未排序的元素,依次比较相邻两个元素的大小,若前面的元素大于后面的元素,那么就交换这两个元素,经过第一轮排序后便可把最小的元素拍好;然后再用通样的方法把剩下的元素进行逐个比较,就可以排好数组各元素的顺序。

为了更好的演示数组的排序,以下程序使用main()函数生成了一个随机数组,再调用相关的排序算法进行排序。

#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define N 10

void maopao(int *a, int n);

int main()
{
	int arr[N], i;

	srand(time(NULL));  //使用时间函数生成一个随机种子,控制rand函数生成随机数

	for (i = 0; i < N; i++)  //循环生成随机数,并将生成的数据保存到数组中
		arr[i] = rand() / 1000 + 100;

	for (i = 0; i < N; i++)
		printf("%d ", arr[i]);
	printf("\n");

	maopao(arr, N);

	for (i = 0; i < N; i++)
		printf("%d ", arr[i]);

	return 0;
}

void maopao(int* a, int n)
{
	int i, j, t;

	for(i=0;i<n;i++)
		for(j=n-1;j>=i;j--)
			if (a[j - 1] > a[j])
			{
				t = a[j - 1];
				a[j - 1] = a[j];
				a[j] = t;
			}
}

8.2.2 折半查找

折半查找就像在数据两端设了障碍,通过不断缩小障碍的范围提高效率,具体的做法就是每次都比较数据中间的值,再缩小查找范围,这样就能提高查找范围。对于经过排序的数据就可以使用折半查找法进行查找,可以提高查找速度。折半查找的算法如下:

首先要确定三个变量low、high、mid,可分别保存数组元素的开始、结尾和中间的序号。假定有10个元素,开始时令low=0,high=9,mid=(low+high)/2=4。接着进行以下判断:

  1. 如果序号为mid的数据组元素与x相等,则表示查找到数据,返回该序号mid。
  2. 如果x<a[mid],则表示要查找的数据x位于low和mid-1的序号之间,就不需要再去查找mid和high序号之间的元素了。因此,将high变量的值改为mid-1,重新查找low和mid-1之间的数据。
  3. 如果x>a[mid],则表示要查找的数据位于mid+1和high之间,就不需要再去查找low和mid之间的元素了。因此将low变量的值改为mid+1,重新查找mid+1与high之间的数据。
  4. 逐步循环,如果到low>high时还未找到目标数据x,则表示数组中无此数据。

根据以上算法编写的程序如下,这里只有查找函数的代码,先调用之前的冒泡排序函数对函数进行随机生成的数据排序,然后即可调用下面的函数进行查找。

int isearch(int a[],int m,int x)
{
    int mid,low,high;
    
    low=0;
    high=n-1;
    
    while(low<=high)
    {
        mid=(low+high)/2;
        if(a[mid]==x)
            return mid;
        else if(a[mid]>x)
            high=mid-1;
        else
            low=mid+1;
    }
    return -1;
}

8.2.3 堆栈的实现

与队列不同,堆栈采用的是先进后出的形式存取数据,只能在一端(称为栈顶)对数据项进行插入和删除操作。

堆栈的实现与队列相似,只需要管理一个栈顶指针st,当该值小于0时,表示已到栈底;当该值超过堆栈空间最大值MAX_STACK时,表示堆栈已满,不能再像堆栈中保存数据。

注意:堆栈主要有两个操作—进栈和出栈,另外需要编写一些辅助的操作函数,如初始化堆栈、显示堆栈中各元素的值等。

下面列出初始化、进栈、出栈3个函数的代码。

首先定义一个堆栈的结构,其中data为保存堆栈数据的内存起始地址,st为栈顶讯号(后面将其称为指针,用来指向堆栈内的某一个元素)。用typedef将该结构定义STACK,方便后面使用。

#include<stdio.h>
#define MAX_STACK 100

typedef struct   //定义数据结构
{
	char *data;
	int st;
}STACK;
STACK *createstack()  //定义函数创建一个栈
{
	STACK *s;

	s = (STACK *)malloc(sizeof(STACK)); //分配一个保存堆栈结构的内存地址
	if (!s) return 0;

	s->data = (char *)malloc(MAX_STACK*sizeof(int)); // 分配一个保存堆栈数据的内存地址
	if (!s->data)  // 数据结构为空则释放节点
	{
		free(s);
		return 0;
	}
	s->st = 0;
	return s;
}
int push(STACK *s, int e)  //函数push将一个数据压入堆栈
{
	if (!s) return 0;
	if (!s->data) return 0;
	if (s->st > MAX_STACK) return 0;
	s->data[s->st++] = e;
	return 1;
}
int pop(STACK *s)  //函数pop用来从堆栈中弹出数据
{
	if (!s) return 0;
	if (!s->data) return 0;
	if (s->st<0) return 0;
	return s->data[--s->st];
}

int main()
{
	int i, n;
	STACK *s = createstack();

	for (i = 0; i < 10; i++)
	{
		printf("输入进栈的数据:");
		scanf("%d", &n);
		push(s, n);
	}

	printf("出栈的顺序:");
	for (i = 0; i < 10; i++)
		printf("%d", pop(s));
	printf("\n");

	return 0;
}

8.2.4 插入排序法

插入排序法首先对数组的前两个元素排序,然后将第3个元素与排的元素进行比较,插入合适的位置,接着将第4个元素插入已经排序好的前3个元素中,不断重复这个过程,直到把最后一个元素插入合适的位置。

void charu(int *a, int n)
{
	int i, j, t;

	for (i = 1; i < n; i++)  //使用for循环进入插入排序
	{
		t = a[i];   //将需要插入的元素保存到变量t中
		j = i - 1;  //插入位置为t-1
		//循环判断,如果序号为1的数据大于需要插入的数据,则序号为j的数据后移
		while (j >= 0 && t < a[j])
		{
			a[j + 1] = a[j];
			j--;
		}

		a[j + 1] = t;  //在序号为j的下一个元素处插入数据
	}
}

提示:插入排序法在数据已有一定顺序的情况下效率很高。如果数据无规则,则需要移动大量的数据,其效率就与冒泡排序法和选择排序法一样差了。

8.3 拓展训练

8.3.1 训练一:求最大值算法

求最大值是指出某些数据中的最大值。设现将10个数据保存到数组中,可以通过设置一个临时变量赋值1个元素,然后将该临时变量与后面的剩余的元素进行比较,若小于后面的元素则临时变量赋值为该元素;否则不变。如此,所有的元素比较完成后,临时变量中保存的就是数据中最大值。

#include<stdio.h>
void main()
{
	int x[10], t, i;
	for (i = 0; i < 10; i++)
	{
		printf("请输入第%d个数:", i + 1);
		scanf("%d", &x[i]);
	}
	t = x[0];
	for (i = 1; i < 10; i++)
		if (t < x[i])
			t = x[i];
	printf("最大值为:%d\n", t);
}

8.3.2 训练二:希尔排序

希尔排序是经过改进后的一种算法,其思想为:先将要排序的序列分为若干块:然后对各个快进行排序,使每个块中的数据都为有序序列;最后对所有的元素进行一步插入排序,使得所有元素为有序序列。

希尔排序先将要排序的序列分为d组,序列中每隔x个数据为一组。然后对每组的数据进行直接插入排序,再将新的序列按照增量x1进行分组后进行比较,直至增量d的值为1,即全体元素进行比较后得到的有序序列。

d一般取n的值的一半,d1取d的值的一半,增量的值按规律递减直至最后为1。

#include<stdio.h>
#include<stdlib.h>
void sort1(int s[], int n, int d)
{
	int i, j;
	for(i=d+1;i<n;i++)
		if (s[i] < s[i - d])
		{
			s[0] = s[i];
			j = i - d;
			do{
				s[j + d] = s[i];
				j -= d;
			} while (j > 0 && s[0] < s[j]);
			s[j + d] = s[0];
		}
}
void shellsort(int r[], int n)
{
	int d = n;
	do {
		d = d / 3 + 1;
		sort1 (r, n, d);
	} while (d > 1);
}
void main()
{
	int i, x[10];
	printf("请输入10个整数:\n");
	for (i = 1; i <= 10; i++)
		scanf("%d", &x[i]);
	puts("你输入的序列是:");
	for (i = 1; i <= 10; i++)
		printf("%3d", x[i]);
	shellsort(x, 10);
	printf("\n希尔排序的结果如下所示:");
	for (i = 1; i <= 10; i++)
		printf("%3d", x[i]);
	printf("\n");
}

8.4 算法的必要性

算法不仅在C语言中起着很大的作用,而且在科学研究中心也至关重要。算法相比其他方法来说更具体更精确,具有可行性、可操作性等特性。某个问题如果有解,则该问题有相应的算法来解决该问题。

计算机是通过数学创造出来的,同时它又是数学的创造者。算法是计算机解决问题的步骤,它与数学有着很大的联系。随着信息化的发展,算法的思想、方法已经融入人们的生活中,影响着人们的生活

学习算法也有以下两方面的意义:

  1. 帮助人们全面理解运算能力。很多时候人以运算就是加减乘除运算,通过自己熟悉的运算法则来计算得出结果。而实际上,一个问题的解往往有很多种。在计算机中通过不同的算法都可以解决问题。人们可以比较这些算法的好坏,选择好的算法从而提高运算效率。因此算法可以帮助人们更全面的理解运算能力。
  2. 培养人们的思维能力。算法是数学的基本内容,可以有效培养人么的逻辑思维能力。算法是解决问题的步骤,它有具体化、程序化、精确性、可行性等特点。通过算法来描述解决问题的方法,能够有效提高人们的逻辑思维能力。

8.5 当下流行的算法

8.5.1 基本算法

  1. 交换(两量交换借助第3者)
  2. 累加。累加算法的要领是形如"s=s+a"的累加式,有了这样的式子我们便可以通过循环来计算得出结果。a通常是有规律变化的表达式,s在进入循环前必须获得一个合适的初值,一般为0。
  3. 累乘。累乘算法的要领是形如"s=s*a"的累乘式,此式必须出现在循环中才能被反复的执行,从而实现累乘的功能。a通常是有规律变化的表达式,s在进入循环前必须获得一个合适的初值,一般为1。

8.5.2 非数值计算通常用经典算法

1. 穷举

也称为枚举法,也就是说将要计算的内容一次一次的进行判断,符合条件的则继续执行。一般采用循环来实现。

2. 排序
  • 冒泡排序:冒泡排序就像他的名字一样,每次将最大的值冒出就可以得出结果。

  • 选择法排序:选择法排序是相对好理解的排序算法。假设要对含有n个数的序列进行升序排序,算法步骤如下:

    • 从数组存放的n个数中找出最小数的下标,然后将第1个数和最小数交换位置。
    • 除第1个数之外,再从n-1个数中找出最小数的下标,将此数和最小数交换位置
    • 重复第1步n-1次,即可完成所求。
  • 插入法排序 :插入法排序的要领就是每次插入即有序,插入排序每次插入一个数立即插入它最终存放的数组中。

  • 归并排序:即将两个数都降序或升序排列的数据序列合并为一个仍然按原序列排列的序列

3. 查找
  • 顺序查找:顺序查找的思路是:将待查找的量与数组中的每一个元素进行比较,若有一个元素与之相等则找到;若没有一个元素与之相等则找不到。
  • 折半查找:顺序查找的效率太低,当有很多的数据时,用二分法查找可以提高效率。但是必须有序的的数列才能使用二分法查找。二分法查找的思路是:先将要查找的值与同数组的中间值做比较,若相同则查找成功,结束;否则判别关键值落在数组的哪半部分,若它比中间值大,则在上半部分查找,否则在下半部分查找,直到找到或者数组中没有这样的元素为止。

8.5.3 数值计算常用经典算法

  • 级数计算
  • 一元非线性方程求根
    • 牛顿迭代法
    • 二分法
  • 梯形法计算定积分

8.5.4 其他常见算法

  • 迭代法:迭代法的基本思想是把一个复杂的计算过程转换为简单过程的多次重复。每次重复都在旧值的基础上递推出新值,并由新值替代旧值。
  • 进制转换
    • 十进制转换为其他进制数
      • 一个十进制正整数m转换为r进制数的思路是:将m不断除以r以取余数,直到商为0为止,以反序输出序列即得到结果。
    • 其他进制数转换为十进制数
      • 其他进制转换为十进制整数的要领是按权展开。例如二进制数101011,则其十进制数为1*2^5+0*2^4+1*2^3+0*2^2+1*2^1+1*2^0。其他进制数转换为十进制数与其类似。
  • 矩阵转置
    • 矩阵转置的算法要领是:将一个m行n列的矩阵的每一行转置成一个n行m列的相应列。
  • 字符处理
    • 字符统计:对字符串中各个字符出现的次数进行统计。
    • 字符加密
  • 整数各数位上数字的获
    • 该算法的核心是利用任何整数整除10的余数即得到该数个位上的数的特点,用循环来从低位到高位依次取出正整数的每一位数字。
  • 辗转相除法求两个正整数的最大公约数
    • 该算法的要领是:假设两个正整数为a和b,将a除以b得到的余数赋予r,若r不为0,则将b赋予a,r赋予b;再求出a除以b的余数,仍然存放到r中,如此反复,直至r为0为止。此时存放到在b中的即为原来两数的最大公约数。
  • 求最值
    • 即求若干个数中的最大值或最小值。该算法的要领是:首先将若干数据存放到数组中,通常假设第一个数为最大值或最小值,赋值给最终存放最大值或最小值的变量,然后将该变量的值与数组其余每个元素进行比较,一旦比该变大或小,则将该元素的值赋值给该变量,直到将所有元素全部比较完毕,即可求得最大值或最小值。
  • 判断素数
    • 素数也叫作质数,它的定义是只能被1和自身整除的大于1的整数,判断素数的算法要领就是依据数学定义,若该数为大于1的正整数不能被2至自身减1的实数整除,则是素数。
  • 数组元素的插入和删除
    • 数组元素的插入。此算法一般是已经有序的数组中再插入一个数据,使数组中的数据依然有序。该算法的要领就是:假设待插入的数据为x,数组a中的数据为升序序列。
      • 先将x与a数组当前最后一个元素进行比较,若比最后一个元素还大,则将x放入其后一个元素中,否则进行以下步骤。
      • 先查找到带插入的位置。从数组a的第1个元素开始找到不比x小的第1个元素,设其下标为i。
      • 将数组a中原最后一个元素至第i个元素依次后移一一位,让出待插入数据的位置,即下标为i的位置。
      • 将数据存入a[i]中。
    • 数组元素的删除。此算法的要领是:首先找到需要删除的元素在该数组中的位置下标,然后让它后面的下标都往前移一位,即可完成删除操作。
  • 二维数组的其他典型问题
    • 方阵的特点
    • 杨辉三角形:杨辉三角形的每一行都是(x+y)^n的展开式的系数。

9. 用指针控制计算机的虚拟地址

指针是C语言中广泛使用的一种数据类型。正确理解和使用指针对于成功进行C语言程序设计是至关重要的。使用指针可访问简单变量、数组、字符串,甚至可以访问函数。指针是C语言最显著的特征;同时又是C语言最危险的特征。例如,在使用指针的程序中,常常因为用错指针导致程序出错,而这类错误却很难发现。更严重的是对未初始化的指针进行操作可能会导致系统崩溃。

9.1 内存和变量

要理解C语言的指针,首先一定要理解C语言中的变量的存储实质,在此主要介绍变量存储这方面的内容。

9.1.1 计算机内存

在体育馆中有上万个座位,为了让观众快速找到自己的座位,需要按照一定的规则对座位进行编号。于此类似,计算机的内存是由一系列连续的内存单元组成,64kb的内存容量就有多达6万个内存单元,现在大多数计算机更是配备了几百MB、上千MB的内存单元,也需要像体育馆的座位一样进行编号。这就是内存编址,每个内存单元都有一个唯一的地址,系统根据这个地址来识别内存单元,在地址所标识的内存单元中存取数据。

有两个概念要分清楚:

  • 一个是内存单元地址。通过引用不同的地址编号,可使用不同的内存单元中的数据。内存单元的地址是固定的,即某个内存单元始终都是设定的那个地址,在程序中不能修改。
  • 另一个就是内存单元中的数据。内存单元中的数据是可以修改的。

9.1.2 变量的存储

在C语言中一般使用以下方式定义变量:

int i;
char c;

以上语句的作用是:要求系统在内存中分配一个类型为int型的存储空间和一个类型为char类型的存储空间。

在高级程序设计语言当中,为了方便程序的阅读,引进了变量的概念。当用户使用变量时,其本质就是访问该变量所对应的内存单元。

例如,执行以下语句:

i=10;
c='w';

在使用scanf()函数时介绍过运算符’&’,使用该运算符可获得变量的内存地址。下面的程序使用该运算符得到变量的地址。

#include<sdtio.h>

int main()
{
    int i;
    char c;
    
    i=10;
    c='w';
    printf("变量i对应的内存单元:%d\t变量i的值为:%d\n",&i,i);
    printf("变量c对应的内存单元:%d\t变量c的值为:%d\n",&c,c);
    
    return 0;
}

9.2 指针和简单变量

了解变量的存储原理之后,就可以开始使用指针了。这里先介绍指针的概念,接着介绍指针操作简单变量。

9.2.1 指针的概念

在生活中有很多的使用指针的例子,例如,方便通信的移动电话就是一个典型的指针实例。如图计算机内存单元的编号一样,手机的号码并没有实际的意义。当拨打手机号码时,我们并不是想对电话号码进行操作,而是想找到手机号码的拥有者。这时,手机号码就是一个指针,它指向了一个具体的使用者。

在C语言中,将内存单元的编号或者或者地址称为指针。可以通过一个变量来存放指针,这种变量称为指针变量。因此,一个指针变量的值就是某个内存单元的地址,或称为某内存单元的指针。

例如,执行以下语句将变量i的地址保存到变量p中:

p=&i;

变量p就是一个指针变量,该变量保存的值为一个指针(内存地址)

提示:指针类型并非多此一举的设计,可以设想,如果你家的地址只有你持有,那么人和人想要拜访你,都需要通过询问你才能到你家,这或多或少有不便之处。但是有些是必须知道的,比如你的父母、你的妻子也保存你家的地址,这样,他们就不要再询问你,可以直接找到你住的地方。

9.2.2 创建指针

在32位系统的计算机中,内存单元的地址用4直接来表示(在16位系统中用2字节来表示)也就是说,保存一个指针需要占用4个内存单元。

需要注意的是,如果将内存单元的地址保存到一个int型变量中,则将不能使用“间接访问”方式去访问该地址所对应单元中的值。例如,以下代码:

int i,j;
j=&i;

编译后,变量j中将保存变量i的地址。但在这里,变量j也就只是将该地址保存为一个整数,而不能作为一个指针。在编译以上代码时,将出现以下的注意信息:

[warning] assigment makes integer from pointer without a cast

而在有的编译器中则不允许将指针赋值给一个整型变量,编译时将出现以下错误提示:

error: Cannot convert 'int *' to 'int'

在C语言中必须将指针保存在一种特殊的变量中,这种变量也称为指针变量,需要用特殊的的方式定义指针变量。一般形式如下:

类型说明符 *变量名1,*变量名2;

其中,星号表示这是一个指针变量,变量名即为定义的指针变量名,类型说明符表示本指针变量所指向的变量的数据类型。

对指针变量的类型说明包括两个方面:

  • 指针所指向的变量的数据类型
  • 指针变量名

例如,以下定义几个不同数据类型的指针:

int *pi;
float *pf;
char *pc;

第一条语句定义指针变量名为pi,指针所指向的数据类型为int型。即变量pi保存的值为一个内存地址,该内存地址中保存的是一个int型的数据。

第二条语句定义指针变量名为pf,指针所指向的数据类型为float型。即变量pf保存的值为一个内存地址,该内存地址中保存的是一个float型的数据。

第三条语句定义指针变量名为pc,指针所指向的数据类型为char型。即变量pc保存的值为一个内存地址,该内存地址中保存的是一个char型的数据。

必须注意,一个指针变量只能指向同类型的变量,如pf只能指向float类型变量,不能指向一个字符变量。

9.2.3 初始化指针变量

与普通变量相比,指针变量必须经过初始化才能使用。

警告:如果对未初始化的指针进行操作,则可能导致系统混乱,严重的将会使系统崩溃。

因为刚定义的指针变量中可能有一个随机的值,在指针变量中,该值表示一个内存地址。如果该内存地址刚好是操作系统的代码区域,修改该内存地址中的值就会使系统崩溃。

因此,定义指针变量之后,必须为其赋予一个具体的值。指针变量的赋值只能赋予地址,决不能赋予其他任何数据,否则也看引起错误。

未用指针应当赋值NULL,以表明它未指向任何地方。当指针的值为NULL时,称该指针为空指针。NULL是一个符号常量,表示0值。使用以下代码可将指针变量初始化为一个空指针。

int *p;
p=NULL;

int *p=NULL;

在程序中,变量的地址是由编译系统分配的,用户不知道变量的具体地址。C语言中提供了地址运算符&来获取变量的地址。初始化指针一般就是使用该运算符取得一个变量的地址,并将其赋值给指针变量。例如:

int i;
int *pi;
pi=&i;

在以上三条语句中,首先定义了两个变量,接着将变量i的地址保存到了变量pi中。需要注意的是,变量i的地址是保存在变量pi中的,最后一行不用在pi前面加上星号。

不允许把一个常数赋值给指针变量。下面的语句在有的编译器中可以编译,但将提示用户注意;在有的编译器中不能编译,而直接提示出错。

int *p;
p=2000;

给指定变量赋初值为0(NULL)是允许的。

在初始化指针变量时,必须保证指针变量所指向的数据类型是正确的。例如,当把一个指针定义为float型时,编译器便认为该指针变量存放的地址都是指向float型变量的。当初始化指针时,C语言允许将任何地址赋值给指针变量。因此下面的程序在编译时不会出错,但在运行时得不到想要的结果。

#include<stdio.h>

int main()
{
    int i=10,j;
    float *pf;
    
    pf=&i;
    j=*pf;
    printf("i=%d\tj=%d\n",i,j);
    
    return 0;
}

在以上程序中,在第五行定义int型变量i和j,并给变量i赋值为10;第六行定义一个float类型的指针变量pf;第八行将int类型变量i的地址赋值给float类型的指针变量pf,第九行使用间接寻址运算符*从指针变量pf所指向的内存单元中取出值,然后赋值给变量j,其意义相当于以下语句:

j=i;

这时的设想是将变量i的值赋值给变量j。但是因为pf指针变量是float类型的,编译器认为其指向的是内存单元中保存的是float型数据,因此将按float类型数据格式从内存中读取数据,然后赋值给变量j。

**注意:**第9行赋值号右边是float类型,左边是int类型,虽然类型不匹配,但可以强制转换,小数点后的数值将会被删除,精确度降低。

9.2.4 指针变量的引用

对指针变量赋值后,就可以使用指针变量参与操作了。对指针的操作最常用的两个运算符就是&和*。

取地址运算符&:是单目运算符,其结合性为自右至左,其功能是取变量的地址。在scanf()函数及前面所介绍指针变量赋值时已经使用到了&运算符。

取内容运算符*:是单目运算符,其结合性为自右至左,用来表示指针变量所指向的变量。在*运算符之后跟的变量必须是指针变量。需要注意的是,指针运算符*和指针变量说明中的指针说明运算符*不是一回事。在指针变量说明中,*是类型说明符,表示其后的变量是指针类型;而表达式中出现的*则是一个运算符,用来表示指针变量所指向的变量。

其实运算符*不仅能从指定的内存地址中取出内容,也可以修改指定内存地址中的内容。

例如,在程序中有以下的语句:

int i;
int *pi=&i;

当执行以上语句后,指针变量pi中保存的就是变量i的地址,这时可使用*pi来访问变量中i中的值。取内容运算符*将指针变量pi保存的地址所对应的内存单元中的值取出来。使用以下语句就可以输出变量i的值:

printf("%d\n",*pi);

这样,*pi和变量i都指向完全相同的内存地址,可以互相取代。指针变pi、*pi和变量i、&i之间的关系如图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5sL0K5Wo-1631880972621)(…/指针和变量的关系.png)]

在上图中,变量i保存的值为10,指针变量pi中保存的变量是i的地址。使用*pi可得到变量i的值,*pi和i对应的同一个内存地址。

取地址符和取内容运算符的实例:

#include<stdio.h>

int main()
{
    int i;
    int* pi;

    pi = &i;
    i = 10;
    printf("i=%d\t*pi=%d\n", i, *pi);

    *pi = 5;
    printf("i=%d\t*pi=%d\n", i, *pi);

    return 0;
}

在以上程序中,第8行将变量i的地址赋值给指针变量pi;第9行将变量i的值设置为10,因为指针变量pi是指向变量i的,所以*pi得到的值也是10;第10行程序输出结果将会验证这一点,第12行程序将常量5赋值给指针变量pi所指向的内存单元(变量i),因此i的值也随着改变。

**提示:**可将*pi看做变量i的一个别名,即这两个名称可以访问同一个内存单元。实质上*pi是经过一次间接寻址才访问到变量i的内存单元的。其执行过程是:首先从指针变量pi中获取一个内存地址,再去访问该地址对应的内存单元。

在程序运行过程中修改指针变量所指向的变量:

#include<stdio.h>

int main()
{
    int i=10,j=5;
    int* pi;

    pi = &i;
    printf("i=%d\tj=%d\t*pi=%d\n", i,j, *pi);

    *pi = &j;
    printf("i=%d\tj=%d\t*pi=%d\n", i,j, *pi);

    return 0;
}

再看一个指针应用的例子。对用户输入的两个数,要求按大小排序输出,不允许交换两个数的位置。

要将两个数按照从大到小顺序输出,首先想到的是把大的数交换到前面来。但是本次不允许交换两个数的位置,这时,可使用指针来完成该任务。

#include<stdio.h>

int main()
{
	int i, j, * pmax, * pmin, * pt;
	
	printf("请输入两个整数:");
	scanf("%d %d", &i, &j);

	pmax = &i;
	pmin = &j;
	if (*pmax < *pmin)
	{
		pt = pmax;
		pmax = pmin;
		pmin = pt;
	}
	printf("输入的值:i=%d,j=%d\n", i, j);
	printf("最大值为:%d,最小值为:%d\n", *pmax, *pmin);

	return 0;
}

注意:第14-16行的代码是用于交换指针变量的值,即交换指针变量保存的内存地址。如果写位以下形式则交换变量i和变量j的值。

t=*pmax;
*pmax=*pmin;
*pmin=t;

上面的代码没有使用*pt,因为指针变量没有被初始化。若使用以下代码将一个值赋值给没有初始化的指针,将给系统带来非常大的危险。

*pt=*pmax;
9.2.5 给函数传递指针

在第7章中使用了指针作为形参的函数swap(),下面再次列出函数swap()的代码:

void swap(int *a, int *b)
{
    int t;
    t=*a;
    *a=*b;
    *b=*a;
}

以上函数使用两个指针变量作为形参,变量a保存一个实参变量的地址,变量b保存另一个实参变量的地址。调用该函数以以下的形式:

swap(&i,&j);

在调用函数swap()函数的时候,将变量i的地址传递给指针变量a,将变量j传递给指针变量b。这样,变量i和a指向同一个内存地址,变量j和指针b指向同一个内存地址。这样,在swap()函数中修改(交换)数据后,在主调用函数中通过变量i和j也能访问到。

通过给函数传递指针,可将函数的运行结果返回到主调函数中。

**提示:**在处理字符串时,基本上都是以指针的形式将字符串首地址 传递到函数中,再由函数对字符串进行处理的。

9.3 指针变量的赋值

9.3.1 初始化赋值

初看起来,指针的初始化和赋值好像很混乱,有时候用&,有时候又用*,时不时又出来一个数组。其实总结起来很简单,只有以下几种形式:

int *p;
intt a=25;
int b[10];
int *m=&a;
int *n=b;
int *r=&b[0];

指针的定义如上所示,以*开头的变量代表该变量为指针变量。

注意:在初始化指针时,=的右操作符必须是内存中数据的地址,不可以是变量,也不可以直接用整型地址值(int *p=0除外,该语句表示指针为空)。

此时,*p仅仅表示定义的是一个指针变量,并没有间接取值的意思。

int *s=15;
int *s={2,3,5};
int *s=a;

以上3种初始化方式都是错误的。

对指针的赋值,=的左操作数可以是*p,也可以是p。

p=m;
p=&a;
p=b;
*p=25;
*p=a;
*p=b[4];

当=的左操作数是*p时,改变的是p所指向的地址存放的数据;当=左边的操作数是p时,改变的是p所指向的地址。

该数组的首地址用数组变量名b表示,因此p=b也是正确的。

9.3.2取地址赋值

通过具体的例子来分析,例如以下的幅值语句:

i=30;
a='t';

这两条赋值语句的含义是把30存放进i变量的内存空间,以及把字符’t’存进a变量的内存空间中。

那么,变量在内存的那里呢,变量的地址在那里呢。

我们需要一个&符号,那么&i是什么意思呢?&i即为取变量i所在的地址编号。我们可以这样理解:它的作用是返回i变量的地址编号。

&符号就像一名警察,通过调用这个符号,我们便可以记录下门牌号(地址值)。

如果要在屏幕上显示变量的地址值,则可以写如下的代码:

printf("%x",&i);

屏幕上显示的将不会再是30,而是将它的地址编号输出到屏幕上。当然实际操作中,变量i的地址一定会是哪个数。

最后总结代码如下:

#include<stdio.h>

void main()
{
    int i=5;
    printf("%d\n",i);
    printf("%d\n",&i);
}

9.3.3 指针之间的赋值

我们可以给同类的指针赋值,例如:

int x1 = 18, x2 = 19;
int *p1, *p2;
p1 = &x1;
p2 = &x2;
p1 = p2;

注意:p1指向了x2,而没有指向x1,这就是指针之间的赋值。

9.3.4 数组赋值

示例:

array[0][0]=1;
array[0][1]=2;
array[0][2]=3;
array[0][3]=4;
array[0][4]=5;
array[0][5]=6;

上面操作的是定义一个2x3的int型的二维数组int array[2][3],并且给这个二维数组赋值为1,2,3,4,5,6;array[0]代表第一行的地址,也就是第一行第一个元素的地址,即&array[0][0],所以array[0]==&array[0][0],其实&array[0]array[0]&array[0][0],都表示第一行的地址。

array[1]为第二行的首地址,也就是第二行第一个数的地址,即&array[1][0],所以

&array[1]array[1]&array[1][0]

定义一个指针变量*p,将第一行的首地址赋给p的3种方式。

p=array[0];
p=&array[0];
p=&array[0][0];

同理,第二行的首地址赋值给p的方式也有3种。

p=array[1];
p=&array[1];
p=&array[1][0];

9.3.5 字符串赋值

字符串并不是C语言中的一个特定的类型,所以我们通常将字符串放在一个字符数组中,如下示例所示:

#include<stdio.h>
int main()
{
    char str[]="http:/c/biancheng.net";
    int len=strlen(str),i;
    printf("%s\n",str);
    for(i=0;i<len;i++)
    {
        printf("%c",str[i]);
    }
    printf("\n");
    
    return 0;
}

**提示:**字符数组归根结底还是一个数组,之前讲到的关于指针和数组的规则通用适用于字符数组。

更改以上的代码,使用指针的方式来输出字符串:

#include<stdio.h>

int main()
{
    char str[]="http:/c/biancheng.net";
    char *pstr=str;
    int len=strlen(str),i;
    for(i=0;i<len;i++)  //使用指针输出字符串
    {
        printf("%c",*(pstr+i));
    }
    printf("\n");
    for(i=0;i<len;i++)   // 使用数组输出字符串
    {
        printf("%c",pstr[i]);
    }
    printf("\n");
    for(i=0;i<len;i++)
    {
        printf("%c",*(str+i));
    }
    printf("\n");
    
    return 0;
}

除了字符数组,C语言还支持另一种表示字符串的方法,就是直接使用一个指针指向字符串,例如:

char *str="http:/c/biancheng.net";

或者:

char *str;
str = "http:/c/biancheng.net";

**注意:*字符串中的所有字符在内存中都是连续排列的,str指向的是字符串的第0个字符;我们通常将第0个字符的地址称为字符串的首地址。字符串中每个字符的类型都是char,所以str的类型也必须是char

下面的实例演示了怎么输出这种字符串:

#include<stdio.h>
int main()
{
    char *str="http:/c/biancheng.net";
    int len=strlen(str),i;
    printf("%s\n",str);
    for(i=0;i<len;i++){
        printf("%c",*(str+i));
    }
    printf("\n");
    for(i=0;i<len;i++)
    {
        printf("%c",str[i]);
    }
    return 0;
}

看起来这和字符串数组十分相似,他们都可以使用%s输出整个字符串,都可以使用*或[]获取单个字符,那么这两种方式的区别在哪里。

它们最根本的区别就是在内存中的存储区域不一样,字符数组存储在全局数据区域或栈区,第二种形式的字符串主要存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而 常量区的字符串只有读取权限,没有写入权限。

由于内存权限的不同而导致的其中一个后果就是,字符数组在定义后可以读取和修改每个字符,但是第二种形式只能读取、不能修改,常见的错误操作就是对它赋值。

第二种形式的字符串称为字符串常量,意思很明显,常量只能读取,不能写入。例如:

#include<stdio.h>
int main()
{
    char *str="hello world";
    str="I LOVE C";  // 正确
    str[3]='p';      // 错误
    
    return 0;
}

虽然上述代码能在编译器中正常编译,但是运行后会出现段错误或者写入位置错误。

第4行代码是正确的,可以更改指针本身的指向,第5行代码是错误的,不能修改字符串中的字符。

那么,到底是使用字符数组还是使用字符串常量呢?在编程过程中如果只涉及对字符串的读取,那么字符数组和字符串常量都能够满足要求;如果需要写入(修改操作),那么只能使用字符数组,不能使用字符串 常量。

获取用户输入的字符串就是一个典型的写入操作,只能使用字符数组,不能使用字符串常量。看看下面的代码:

#include<stdio.h>
int main()
{
    char str[30];
    gets(str);
    printf("%s\n",str);
    return 0;
}

最后总结一下,C语言有两种表示字符串的方法,一种是字符数组,另一种就是字符串常量,它们在内存中的存储位置不同。字符数组不但能读取,而且还能对它进行修改,但是字符串常量只能读取,却不能对它进行修改。

9.3.6 函数入口赋值

就像指针变量可以存储某一数据变量的内存地址一般,函数的首地址也可以存储在某个函数指针变量里。这样,我们就可以通过这个函数指针变量来调用所指向的函数了。

在C语言中,我们在使用变量之前要先声明它。同理,我们在使用函数指针变量之前也有个先声明。那又该如何来声明呢?例如,我们声明一个可以指向MyFun()函数的函数指针变量FunP()。下面就是声明FunP()变量的方法:

void (*FunP)(int);

也可以写成

void (*FunP)(int x);

有了FunP指针变量后,我们就可以对它进行赋值,使其指向MyFun()函数,然后通过该指针变量来调用MyFun()函数。

下面通过一个实例来实现对FunP指针变量来调用MyFun()函数。

#include<stdio.h>
void MyFun(int x);
void (*FunP)(int );// 也可以写成void (*FunP)(int x),但是习惯上一般不这么写
int main(int argc, char* argv[])
{
    MyFun(10);
    FunP=&MyFun;
    (*FunP)(20);
    
}
void MyFun(int x)
{
    printf("%d\n",x);
}

以上程序就是通过指针变量调用函数的方法。

9.4 指针和数组的关系

通过指针变量可以指向一个简单的变量,而数组元素也可以被当做简单变量使用,因此指针变量也可以指向数组元素。因为数组是存储在一片连续的内存单元中,所以当指针指向某个数组元素后,可以通过向前或向后移动指针来访问数组中的其他元素。

9.4.1 指针、数组和地址的关系

数组是存储在一片连续的内存单元中,数组名是这片连续内存单元中的首地址,内存单元的地址就是指针,因此数组名也是一个指针。

数组由多个数组元素组成,每个数组元素按其类型不同占有几个连续的内存单元。一个数组元素的首地址就是它所使用的的几个内存单元的首地址。

指针变量既可以指针一个数组,也可以指向一个数组元素。将数组名或数组的第一个元素的地址赋给指针,则指针指向一个数组。如果要使指针变量指向第i个元素,则可以把第i个元素的首地址赋给它。

假设用以下代码定义一个int型的数组a,使用一个指针指向该数组。

int a[5];
int *p;
p=a;

上述最后一条语句的作用只是把数组a的首地址保存到指针变量p中,而不是将数组a的各元素赋给p。

因为数组的第1个元素的地址与数组首地址相同,因此,也可以使用以下方法将数组的首地址保存到指针变量p中:

p=&a[0];

下面程序演示指针、数组和地址之间的关系:

#include<stdio.h>

int main()
{
    int a[5]={5,10,15,20,25};
    int *p=a;
    
    printf("指针变量p的值:%d\n",p);
    printf("数组a的首地址:%d\n",a);
    printf("数组a的第一个元素的值:%d\n",a[0]);
    printf("指针指向数组元素的值:%d\n",*p);
    
    return 0;
}

在以上程序中,第5行定义初始化数组a,第6行定义指针变量,并使其指向数组a,第8~11行分别输出指针变量的值、数组a的首地址、数组元素a[0]的值、指针指向数组元素的值。

指针变量p和数组名a指向同一个内存地址。同时,指针指向的地址也是数组元素的首地址,因此输出结果两两相同。

使用*p可以访问数组的第1个元素,能不能通过指针的运算访问数组的其他元素呢?答案是肯定的,接下来将会介绍通过指针访问数组元素的方法。

9.4.2 指针变量的运算

要使用指针访问·数组中的元素,需要对指针进行运算。下面介绍指针运算的规则。

常见的指针运算有:指针加/减一个数,指针自增、指针自减、指针比较等操作。

假设有以下指针:

int *p=&i;

设指针变量i的首地址问20000。

1. 自增/自减运算

对指针变量可以进行自增/自减运算。例如:执行以下操作:

p++;

指针变量p的值应该是多少?

如果变量p不是指针变量,那么这时候p的值应该自增1,变为20001;如果p是指针变量那么执行p++的结果就不一定了。对于本例,执行以上语句后,p的值变为20004,即p每自增一次,指针就指向后一个int型数。

C语言规定,指针每递增1次,将指向后一个基类型元素的内存单元(所谓的基类型元素,是数组中元素的类型,而指向该数组元素的指针早定义之前就确定了该基类型,即数组元素如果是int型,则指针为int类型指针,如果是结构体,则指着为结构体指针类型),指针每递减1次,将指向前一个基类型的元素的内存单元。也就相当于执行以下操作:

p=p+sizeof(*p);
p=p-sizeof(*P);

**警告:**在定义指针变量时,指针变量的类型十分重要。若使用类型不匹配的指针变量,则将得到不可预测的结果。

2. 指针加/减一个整数

指针还可以加/减一个整数n。例如,执行以下操作:

p+=3;

与自增/自减类似,这时的指针变量p并不是将内存单元地址增加3字节,而是将p增加3个基类型元素的字节宽度,相当于执行以下语句:

p=p+sizeof(*p);

因此,此时p的值为20000+3*4=20012。

对于指针减去一个整数n的操作于此类似。

需要注意的是,除了对指针加上或减去一个整数,其他任何算术运算都是非法的。例如不允许对指针做乘除法、不允许两个指针相加、不允许对指针加上或减去一个float类型或double类型的数据。

3. 两指针变量相减

因为指针变量保存的是内存地址,将两个内存地址相加没有什么意义,所以不允许两个指针变量相加。但是大部分编译器支持两个指针变量相减的运算,两个指针变量相减所得之差就是两个指针所指向变量之间相差的元素个数。就像我们生活中的两个门牌号相加得到的数字是没任何意义的,但是可以将量门牌号相减得到两个房子之间还有几个房子,实际上是两个指针值(地址)相减之差再除以该指针变量的类型长度。例如,p1和p2是两个指向int型变量的指针,当运算p1-p2时得到的结果如下:

d=(p1-p2)/4;

即p1的地址值减去p2的地址值,再除以int型数据所占的字节数。得到的值为两个变量相隔多少个int型数据位置。

对于两个普通变量的指针,使用这种运算没有任何意义。但是,如果两个指针指向同一个数组,那么这时计算结果d就表示两个指针之间相差的数组元素个数。例如,有一个数组a,两个指针p1和p2分别指向数组的不同元素,这时p1-p2就能表示之间相差多少个数组元素。

4. 指针比较

指针比较可以用来判断两个指针在内存中的位置关系。例如:

p1>p2

如果以上表达式为True,则表示p1处于高位地址(地址值更大)。

p1==p2

如果以上表达式为True,则表示两个指针指向同一个地址。

另外,还可以用以下表达式判断指针是否为空指针。

p1==NULL

如果以上表达式为True,则表示p1为空指针。

9.4.3 指针操作数组元素

#include<stdio.h>

int main()
{
    int i,a[5]={5,10,15,20,25};
    int *p=a;
    
    for(i=0;i<5;i++)
        printf("a[%d]=%d\n",i,a[i]);
    printf("\n");
    
    for(i=0;i<5;i++)
        printf("*(p+%d)=%d\n",i,*(p+i));
    
    return 0;
}

在以上程序中,第5行定义并初始化了一个数组a;第6行将数组a的起始地址保存到指针变量p当中第8、9行使用for循环逐行输出数组中各元素的值,第12、13行使用for循环输出指针p及与指针p的距离为1~4的各元素的值。

指针p和p+0和数组名a都指向同一个内存单元,是数组的首地址;p+1表示将指针p增加一个元素(p+sizeof(*p)),使指针指向数组的下一个元素a[1];以此类推,可知道p+i和a[i]指向同一个元素。

因此,引入指针变量后,就可以使用两种方法来访问数组元素。

  • 第一种为下标法,即用a[i]形式访问数组元素。
  • 第二种方法为指针发,即采用*(p+i)的形式,用间接法的方式来访问数组元素。

**注意:**当使用指针p指向数组的首地址后,也可以使用p[0]、p[1]、p[2]来访问数组元素。这样,指针p就相当于数组a的一个别名。

下面的程序演示这种效果:

#include<stdio.h>

int main()
{
    int i,a[5]={5,10,15,20,25};
    int *p=a;
    
    for(i=0;i<5;i++)
        printf("*(a+%d)=%d\n",i,*(a+i));
    printf("\n");
    
    for(i=0;i<5;i++)
        printf("p[%d]=%d\n",i,p[i]);
    
    return 0;
}

编译运行以上程序,从运行结果可以看出,当指针指向数组后,对指针变量也可以使用下标方式访问数组中的元素。如果只是使用指针来代替数组名,则显然没有任何意义,因为可以直接使用数组名加下标来访问。

使用指针操作数组的优点主要体现在对数组元素进行顺序操作时,可使用指针的自增/自减运算,快速的对数组各元素进行操作。例如,上面的第13行代码就可以改写为如下形式:

printf("*p++=%d\n",*p++);

在*p++表达式中,首先使用 *p返回指针指向单元的值,供printf()函数输出,接着执行p++使指针向后移动,每执行一次循环体,指针就会向后移动一个元素,至循环体执行结束后,指针将指向最后面的一个内存单元。

用类似的方法,将以上的第9行程序的代码改写为以下形式:

printf("*(a+%d)=%d\n",i,*a++");

这时将会发现程序编译出错,表示不能将数组改写为这种形式。

这时将会发现,指针和数组名还是有区别的。其实指针是指针变量,可通过运算改变变量保存的值;而数组名是一个指针常量,常量的值是不允许改变的。将数组名定义为一个指针常量是可以理解的,因为数组名指向数组的首地址,如果其为变量,那么在程序中可改变其值,这样,数组的首地址也会跟随者改变。但数组在编译程序时已经被分配到了固定的内存区域。因此,数组的首地址是不允许改变的,数组名就必须被定义为一个指针常量。

**注意:**在操作数组名的时候,不能使用类似a++或a–这样的操作,而使用a[i]、*(a+i)等方式访问数组元素,数据名a的值是始终保持不变,因此可以按照这种方式使用。

在指针运算中,使用的最多的是自增/自减运算,而自增/自减运算符和指针运算符优先级相同,结合方向是自左向右。假设指针指向数组a的首地址,使用printf()函数输出指针所指向的值,可能有以下几种情况:

  • *p++:相当于 *(p++),因为++是后置运算,所有先得到p指向变量的值,然后再使用p自增。执行该表达式后,将输出a[0]的值,然后指向a[1]。
  • *(++p):是使用指针进行增加,再取增加过后所指向的位置的值,指向该表达式之后,将输出a[1]的值,指针指向[1]。
  • (*p)++:先取得指向元素的值,然后将该值累加1,。这时指针指向的变量不会变,仍然指向a[0]。

9.5 指向多维数组的指针

上面介绍了指针指向一维数组,然后用指针操作数组元素的情况。当然,也可以使用指针来指向多维数组,只是使用指针处理多维数组时的概念要复杂很多,下面操作二维数组来进行介绍。

9.5.1 理解二维数组的地址

假设定义二维数组如下:

int a[4][5];

**注意:**由于数组名就代表着数组的起始地址,所以a和第一个元素a[0][0]地址的数字是相同的,但是意义却不相同。

二维数组在逻辑上是由行和列组成的,因此,对于二维数组,可以加其分为3层来理解:

  • 第1层将数组看做一个变量。
  • 第2层将二维数组a看做一个一维数组,有a[0]、a[1]、a[2]、a[3]4个元素组成。
  • 第3层就是将第2层中的每个数组元素看做一个单独的数组。

下面编写程序来查看每一层的地址情况。

首先,在第1层中将数组看做是一个变量,改变量的地址为&a,长度为sizeof(a),使用以下程序可以得到变量a的地址和长度。

#include<stdio.h>

int main()
{
    int a[4][5];
    
    printf("变量a的地址:%d\n",&a);
    printf("变量a的长度:%d\n",sizeof(a));
    
    return 0;
}

其次,第2层中将数组a看做是一个一维数组,它有4个元素a[0]、a[1]、a[2]、a[3]。数组的首地址为a或&a[0]。使用sizeof(a[0]),可以得到数组元素的长度。

#include<stdio.h>

int main()
{
    int a[4][5];
    
    printf("数组a的首地址:%d\n",a);
    for(int i=0;i<4;i++)
        printf("数组a的第%d个元素的地址:%d\n",i+1,&a[i]);
    printf("数组元素的长度为:%d\n",sizeof(a[0]));
    
    return 0;
}

编译运行以上程序,得到的结果在不同的计算机中地址值可能不同,但是每个元素的地址之间的差值肯定是相同的。

从结果也可以看出将二维数组a作为一个一维数组看待时,其每个元素的长度就是二维数组中一行的元素的长度之和。

最后,在第3层之中,第2层的元素又被看成是由5个元素构成。

**注意:**当将a[0]看做一个数组名时,该数组的首地址也就保存在a[0]中(这里a[0]作为一个整体,看做是数组名,而不是一个数组的元素),不用取地址运算符,直接输出a[0]的值就可以得到数组的首地址。

#include<stdio.h>

int main()
{
    int a[4][5],i;
    
    printf("数组a的首地址:%d\n",a);
    for(i=0;i<4;i++)
        printf("数组a[%d]的首地址:%d\n",i,a[i]);
    printf("数组a[0]中的元素a[0][0]的长度:%d\n",sizeof(a[0][0]));
    
    return 0;
}

9.5.2 多维数组的指针表示

多维数组也可以方便的使用指针操作。前面介绍了二维数组的地址,下面介绍二维数组的指针表示方法。

既然可以将二维数组a看成一个一维数组,也就可以使用前面介绍的方法来表示这个一维数组,如a+1就是数组a的第2个元素的地址,a+i就是它的第i个元素的地址,这样,使用*(a+i)就可以访问该元素的值了(等同于a[i])。

进一步,在二维数组中,*(a+i)又指向一个数组, *(a+i)+j表示这个数组的j+1个元素的地址,要访问该元素的值可以使用 *( *(a+i)+j),其实也就是a[i][j]

在一维数组a中,数组名a表示数组的首地址,a[i]表示数组a的i+1个元素的值,&a[i]表示第i+1个元素的地址,也可以使用*(i+1)这种指针方式来表示第i+1个元素的值。

在二维数组中,数组名a表示数组的首地址,a[i]表示一个一维数组的名称,其本身并不占用内存单元(也就是说不保存元素值),它只是一个地址(想一想,一个一维数组b,其数组名b也只是代表一个数组的首地址,并不保存元素值)。a[i]表示数组的首地址,a[i][j]表示数组a[i]的第j+1个元素的值,&a[i][j]表示返回数组a[i]的第j+1个元素的地址。也可以使用*(a[i]+j)的方式来表示a[i]的第j+1个元素的值,而 *(a[i]+j) 又可以表示为 *( *(a+i)+j)。

对于二维数组a,以下描述都是正确的:

  • a是一个数组,*a指向一个数组
  • a+1指向一个数组
  • a、*a、&a、&a[0][0]
  • a[i]+j是一个地址,*(a+i)+j与其相同
  • a[i][j]是数组的元素,*( *(a+i)+j)与其相同

9.5.3 指向多维数组的指针变量

接下来看一个程序,不使用指针,知己初始化和输出二维数组。

#include<stdio.h>

int main()
{
    int a[4][5],i,j;
    
    for(i=0;i<4;i++)
        for(j=0;j<5;j++)
            a[i][j]=i*5+j;
    
    printf("二维数组的值为:\n");
    for(i=0;i<4;i++)
    {
        for(j=0;j<5;j++)
            printf("%4d ",a[i][j]);
        printf("\n");
    }
    
    return 0;
}

**提示:**二维数组从逻辑上可以分为列和行。因此,一般使用双重循环的形式来处理二维数组,外循环控制行的变化,内循环控制列的变化。

以上程序是一个比较常见的、用双重循环处理数组各元素的代码结构。下面用指针变量来处理该程序当中的数组。

注意:二维数组虽然在逻辑上是一个平面结构,但其保存在内存中的时候采用的是一维线性结构。

如果要按顺序将用户输入的值逐个保存到二维数组各元素中,或逐个输出二维数组中的元素,则可以使用指针处理一维数组相似的方法来处理二维数组。

#include<stdio.h>

int main()
{
    int a[4][5],i;
    int *p;
    
    p=a[0];
    for(i=0;i<20;i++)
        *p++=i;
    
    printf("二维数组的值为:\n");
    p=a[0];
    for(i=0;i<20;i++)
    {
        if(i%5==0)
            printf("\n");
        printf("%4d ",*p++);
    }
    
    return 0;
}

**注意:**在以上程序中,如果删除第13行将不会得到正确的结果。

2. 用指针变量指向一行数据

在上面例子中,使用int型指针指向一个整型变量,这样每移动一次指针就可以获取下一个数组元素。但是这种方法将二维数组按一维数组的形式来处理,如果需要获取第i行第j列的数据就需要进行一换算才能计算出其存储位置。

二维数组在逻辑上是按行列构成的,每一行可作为一个独立的部分,这样就可以将二维数组看做是一个以行为单位的一维数组。因此,可以创建一个只想该一维数组的指针。数组中的每个元素都是int型(或者其他简单数据类型,如float、double、char) ,使用以下方式就可以定义指针变量即可:

int *p;

这样,执行p+1时将指向下一个int型元素(数组的下一个元素)。

但是,当a[0]不是int型或者其他简单数据类型,而是一个一维数组时,指针的类型应该怎么定义呢?

从存储字节的观点来看,当使用以下方式定义指针时:

int *p;

表示指针指向一个占用4字节数据的首地址,当执行p++时,p的值将加4。

当指针p指向一个数组时,执行p++要使其指向下一行,则指针p必须是其所指向的数组。

假如指针p指向的数据类型是一个具有5个int元素的数组,因此,应该使用以下方式定义指针变量:

int (*p)[5];

这里需要理解一个概念,就是指针所指向的类型。当通过指针来访问所指向的内存区时,指针所指向的类型决定了编译器把那片内存区的内容当做什么来看待。

例如:

int *p;

表示指针所指向区域的数据类型为int型,指针每次向前或向后移动时,都是以int类型所占字节的倍数来进行。即p++使指针向后移动4字节,指向下一个int型的数据,p–使指针向前移动4字节,指向上一个的数据。

怎么确定指针所指向数据的类型呢?最简单的方法就是把指针变量及其前面的*号去掉,剩下的就是所指向的类型。例如:

char *pc;
float *pf;

第1条语句去掉*pc后为char,表示指针pc指向的数据类型为char类型。第2条语句去掉 *pf后为float,表示指针pf指向的数据类型为float型。

那么,下面的语句:

int (*p)[5];

去掉*p 后为int()[5],表示指针p所指向的数据类型为int()[5] (5个连续的int型数据,即含有5个int型元素的数组,共占用20字节的内存空间)。指向p++时,指针将向后移动20个字节,指向下一行数据。

这种方式称为数组指针,即定义的变量是一个指针,其指向的数据类型是数组。

这还要注意的是,*p必须括号括起来。如果写成以下形式:

int *p[5];

由于运算符[]的级别高于运算符*,所以在上面语句,变量p先和[5]结合成数组,然后再与前面的 *结合,这样就成为指针数组(每个数组元素保存一个指针)。

**注意:**数组指针和指针数组,只是把词语的位置交换了一下,但是其意义完全不同。

#include<stdio.h>

int main()
{
    int a[4][5], i, j;
    int(*p)[5];

    p = a;
    for (i = 0; i < 4; i++)
    {
        for (j = 0; j < 5; j++)
            *(*p + j) = i * 5 + j;
        p++;
    }
    printf("二维数组的值为:\n");
    p = a;
    for (i = 0; i < 4; i++)
    {
        for (j = 0; j < 5; j++)
            printf("%4d ", *(*p + j));
        printf("\n");
        p++;
    }

    return 0;
}

第6行定义了一个指针变量,其指向数据类型为5个int类型的数组。第8行将数组a的首地址赋给指针p。注意这里不是使用以下这种形式初始化指针:

p=a[0];

这种方式是将数组a[0]的首地址赋值给指针变量p,这时指针变量p所指向的数据类型应该为a[0][0]的类型(int型)。

第13行和第23行语句使指针指向二维数组的下一行。如果省略这两行语句,则第12行和第21行中的获取数组元素的部分就需要改写为以下形式:

*(*(p+i)+j);

在执行到第16行时,指针p已经指向数组a以外,所以需要用第17行的语句重新使指针指向数组a的起始位置。

9.5.4 数组名作为函数的参数

在前面已经介绍过用数组名作为函数的实参和形参的问题。在学习了指针变量之后,对这种参数的传递就更加的容易理解了。

数组名就是数组的首地址,当实参向形参传递数组名时,实际上就是传递数组的地址,形参得到该地址后就与主调函数中的实参指向同一数组。这样,在被调函数中对数组元素的值进行修改,返回主调函数后,通过实参通用可以访问到。这样就解决了C语言只能使用return语句返回一个值的问题。当函数需要返回多个值给主调函数时,可使用数组作为函数的形参。

同样,指针变量的值也是地址。在调用形参为数组的函数时,也可以将指针变量作为实参传递给函数。

1. 一维数组作为参数

**提示:**当向函数传递一维数组作为实参时,只需要将数组名填入函数的形参部分就可。

例如:编写一个函数,用于向屏幕中输出一个字符串。

#include<stdio.h>

int main()
{
    void puts1(char s[]);
    char s1[]="hello,c programmer";
    
    puts1(s1);
    
    return 0;
}

void puts1(char s[])
{
    int i;
    
    for(i=0;s[i]!='\0';i++)
        putchar(s[i]);
}

在以上程序中编写函数puts1(),用来输出一个字符串。在函数的定义部分,第12行定义该函数的形参为一个一维数组,该一维数组中保存着一个字符串。在该函数中使用putchar()库函数逐个输出该数组中的字符,直到字符串结束为止。字符串的最后一个字符为’\0’,所以第17行在循环中以s[i]!=’\0’作为条件。

main()函数的第8行调用puts1()函数,将字符数组名s1作为实参传递给函数,参数的传递过程相当于一条赋值语句:

s=s1;

即将main()函数中的数组名s1(数组的首地址)传递给形参中的数组名s。在前面介绍过,数组名就是指针常量,不允许修改其值。在C语言中,当函数的形参设置为数组时,C编译器会将其转换为指针。例如,本例中的函数puts1()的函数头中将形参定义为数组s,是指是函数头与以下形式等价:

void puts1(char *s);

为了检验这种情况,可将函数puts1()的代码改写为以下形式:

void puts1(char s[])
{
    for(;*s='\0';)
        putchar(*s++);
}

**注意:**在函数头部分,形参仍然采用数组形式,但在函数体的代码中,可以使用s++来修改数组的首地址,逐个输出其值。如果数组s不是函数的形参,而是函数内部定义的数组,则不允许使用这种方式。

其实C语言程序设计中,程序员更喜欢用指针方式来编写这类程序。例如:将puts1()函数改写为以下形式,应该更易读懂:

void puts1(char s[])
{
    while(*s='\0')
        putchar(*s++);
}

而表达式*s=’\0’更可以精简为 *s,因为当 *s的值为’\0’时,表达式的值也为0,循环结束;当 *s的值不为0时,循环继续执行。在C语言中,类似这样的比较运算都可以简写,省去关系表达式运算的环节,可以提高程序的效率,同时使程序更简洁。修改后的程序如下:

void puts1(char s[])
{
    while(*s)  putchar(*s++);
}

修改后的程序与之前的函数具有完成相同的功能,main()函数不需要进行任何修改,即可得到同样的结果。

2. 二维数组作为参数

一维数组作为参数时比较简单,上面演示了其使用方法。在使用一维数组作为参数时,也可以使用指针的方式访问数组中的数据。

也可以根据需要向函数传递二维数组作为实参,与两种方法:一种是使用指向数组元素的指针变量,其本质就是将二维数组作为一维数组来使用;二是使用指向一维数组的指针变量。

下面的程序演示使用指向一维数组的指针变量传递参数的形式。该程序让用户输入一个矩阵,然后计算该矩阵的对角线数据之和。

#include<stdio.h>
#define M 4

int main()
{
    void read(int a[][M], int n);
    int sum(int a[][M], int n);

    int a[M][M];

    read(a, M);

    printf("矩阵对角线元素之和为:%d\n", sum(a, M));

    return 0;
}

void read(int a[][M], int n)
{
    int i, j;

    for (i = 0; i < n; i++)
    {
        printf("请输入矩阵第%d行的%d个数据:", i + 1, n);
        for (j = 0; j < n;j++)
            scanf("%d", &a[i][j]);
    }
}

int sum(int a[][M], int n)
{
    int i,s = 0;

    for (i = 0; i < n; i++)
        s += a[i][i];
    return s;
}

第9行定义了一个二维数据,用来保存矩阵。函数read()用来获取矩阵,函数sum()用来计算矩阵对角线元素之和。因为保存矩阵的二维数组是在main()函数中定义的,并且该数组需要在这两个函数中进行处理,所以需要将该二维数组传递到函数中。

**提示:**查看两个函数的函数头可以看出,当函数中形参为二维数组时,可以只设置最后一维的长度(设置二维数组每行需要占用的内存单元),另外设置一个参数n来表示二维数组的行数,然后再函数中就可以直接引用该二维数组了。

与一维数组做函数实参相似,二维数组作为实参时,也就是将二维数组的首地址传递到函数中,所以在函数中对数组进行操作,返回主调函数后可以通过访问数组得到修改结果。

也可以使用数组指针的形式来定义函数的形参。例如,将read()函数修改为以下形式:

void read(int (*a)[M], int n)
{
    int i,j;
    
    for(i=0;i<n;i++)
    {
        printf("请输入矩阵第%d行的%d个数据:", i + 1, n);
        for (j = 0; j < n;j++)
            scanf("%d", *a+j);
        a++;
    }
}

在函数头部分,使用数组指针的形式int(*a)[M]定义形参,可接收二维数组的地址作为实参。调用该函数时,使指针a指向实参数组,因为该指针为数组指针,所以在第10行中执行指针自增操作运算时,将处理二维数组中的下一个数据。在第9行中使用 *a+j的方式返回二维数组元素的地址(其中 *a取得二维数组行的首地址,再加上j就得到指定列的地址)。

同样,sum()函数也可以将其形式改写为数组指针形式,修改后的代码如下:

int sum(int (*a)[M],int n)
{
    int i,s=0;
    
    for(i=0;i<n;i++)
        s+=*(*(a+i)+j);
    return s;
}

第6行使用*(a+i)取得二维数组第i行的首地址,再使用 *(a+i)+j得到的第i行第j列的地址,最后使用运算符 *取得改地址保存的数据。

9.5.5 指向数组的指针小结

前面介绍了指向数组的指针,主要介绍了指向一维数组和二维数组的指针。指向一维数组的指针比较简单,在概念上也比较容易理解,一般只需要使用以下形式定义指针即可:

类型说明符 *指针变量名;

其中,类型说明符为指针指向数据的类型;*表示其后的变量是指针类型。

定义好指针变量后,将数组名赋给指针变量,就可以使用指针变量和数组名两种方式操作数组。例如,有数组a和一个指向该数组的指针变量p,使用a[1]和p[1]都可以引用数组中的第2个元素,使用*(a+1)和 *(p+1)也可以引用数组中的第2个元素。因为数组名a是一个指针常量,所以不能执行a++、a–之类的自增/自减操作,而指针p则可以使用p++、p–之类的运算改变自身的值。

对于指向二维数组的指针,情况就要复杂一些。定义二维数组指针变量的一般形式如下:

类型说明符 (*指针变量名)[长度];

其中,类型说明符为所指向数组的数据类型;*表示其后的变量是指针类型;长度表示二维数组分解为多个一维数组时一维数组的长度,也就是二维数组的列数。注意, ( *指针变量名)两边的括号不可少,如果缺少括号则表示是指针数组。

**注意:**如果是三维或者更多维的数组,那么情况将更加的复杂。所幸的是,在实际应用中很少使用超过二维的数组。

9.6 指针和字符串

在字符数组的最后添加一个结束字符’\0’就是一个字符串。使用指针处理普通的一维数组,还需要知道数组的长度。因为字符串有明显的结束字符,所以,用指针处理字符串将非常方便。

9.6.1 字符串的指针表示

前面介绍字符串使用以下方式定义:

char name[]="wuyunhui";

以上的语句的实质就是定义一个字符数组,数组的每一个元素保存一个字母,并在最后面一个元素中保存字符串的结束标志’\0’。这样就可以使用printf()函数的格式"%s"来输出字符串name。

在以上语句中,数组名name表示数组的首地址,name[0]为第1个元素的内容,name[3]为第4个元素的内容。

注意:在以上语句中,数组长度显得不重要,将由编译器自动进行填充。在使用printf()函数进行输出操作时,只需要知道数组的首地址就行了。按照这种要求,使用指针也可以达到同样的效果。

可使用以下格式定义字符串指针变量:

char *指针变量名;

其中,类型说明符也必须为char。例如:

char *p_name="wuyunhui";

以上语句就定义了一个指向char类型的指针变量p_name,该指针变量初始化指向字符串常量"wuyunhui"的首地址(字符串常量由编译器自动分配一片连续的区域保存)。这样,通过指针就可以访问到保存字符串常量的首地址,也就可以方便的使用printf()函数按照以下方式输出其内容:

printf("%s\n",p_name);

在printf()函数的输出列表中,只需要给出指着变量名即可。与输出字符数组相似,使用格字符“%s”,只需要通过知道字符串的首地址,就可以顺序输出该字符中的所有字符。

在本章节前面介绍过,在一个指针变量指向一维数组后,也可以在指针变量后使用序号的方式访问数组中指定的元素。在字符串中也可以同样处理单个字符,下面的程序将演示字符数组、字符串指针的使用。

#include<stdio.h>

int main()
{
    char a[] = "The Beijing 2008 Olympic Games";
    char* pc = "One World,one Dream";

    printf("字符数组a:%s\n", a);
    printf("字符串指针pc:%s\n", pc);

    printf("a[4]=%c\t*(a+4)=%c\n", a[4], *(a + 4));

    printf("pc[4]=%c\t*(pc+4)=%c\n", pc[4], *(pc + 4));
    printf("使用&a[4]方式输出子串:%s\n", &a[4]);

    printf("使用a+4的方式输出子串:%s\n", a + 4);

    printf("使用&pc[4]方式输出子串:%s\n", &pc[4]);

    printf("使用pc+4方式输出子串:%s\n", pc + 4);

    return 0;

}

9.6.2 字符串指针作为函数参数

C语言中许多字符串操作通常是由指针运算实现的。因为对于字符串来说,一般按存储顺序进行操作,所以使用指针可以明显的提高效率。

将字符串作为实参传递给函数的形参,可以将字符数组名或指向字符串的指针变量作为参数。

C语言的库函数提供了许多字符串函数,下面的例子完成cmpstring()函数的功能,用来比较两个字符串的大小。

**提示:**其比较过程是:首先取出两个字符串的第1个字母,比较器ASCII码,ASCII码大的字符串大;如果第一个字符相同,则比较第二个字符…直到遇到不同的字符,得出其大小。如果第一个字符串的所有字符和第二个字符串的对应字符相同,且第2个字符串更长,则也认为两个字符串相等。

根据这个规则编写函数cmpstring()的代码如下,其中main()函数用来测试cmpstring()函数的工作情况:

#include<stdio.h>

int main()
{
    int cmpstring(char* s1, char* s2);
    int ret;
    char str1[80];
    char str2[80];

    printf("请输入一个字符串:\n");
    scanf("%s", str1);

    printf("请输入另一个字符串:\n");
    scanf("%s", str2);

    ret = cmpstring(str1,str2);
    if (ret > 0)
        printf("第1个字符串大于第2个字符串\n");
    else if (ret < 0)
        printf("第2个字符串大于第1个字符串\n");
    else
        printf("第1个字符串等于第2个字符串\n");

    return 0;
}

int cmpstring(char *s1, char* s2)
{
    while (*s1)
        if (*s1 - *s2)
            return *s1 - *s2;
        else
        {
            s1++;
            s2++;
        }
    return 0;
}

9.6.3 字符数组和字符串指针变量的区别

从前面的例子可以看出,使用字符数组和字符串指针都也可以实现对字符串的处理,感觉两者之间好像没有什么区别。但是实际情况并非如此,字符数组和字符串指针变量之间是有重要区别的,下面介绍这些区别。

字符数组是由若干个数组元素组成的,每个元素占用一个内存单元,保存一个字符。而字符串指针变量本身是一个变量,用来保存字符串常量的首地址。如果字符串指针变量未初始化而使用,则将产生不可预料的后果。

例如:

char s[50];
char *ps;

scanf("%s",s);
scanf("%s",ps);

在以上的程序中,第1行定义了一个字符数组,编译器将会分配50个连续的内存单元。而第2行定义字符串指针变量,编译器只会分配4个字节用来存放字符串常量的首地址,由于指针变量的值还未初始化,所以ps变量中的值还是未知的。第3行接收用户输入,并保存到数组s中,这时正确的。而第4行却会带来问题,因为ps的值是未知的,而该语句将会将用户输入的内容保存到一个未知的内存区域,可能会使系统崩溃。因此,必须先对字符串指针变量进行初始化,使其指向一个确定的内存区域,然后才能使用。例如,可以使用以下方式进行初始化:

char s[80];
char *ps=s;
scanf("%s",ps);

**注意:**前面反复提到,数组名就是数组的首地址,是一个指针常量,确定存储位置后是不能改变的。而字符串指针变量是一个变量,该变量保存的字符串常量首地址是可以改变的,即字符串指针变量可以指向不同的字符串常量。当字符串指针变量ps指向第1个字符串常量时,变量ps就表示第1个字符串;指向第2个字符串常量时,变量ps就表示第2个字符串。

例如:

char s[]="The Beijing 2008 Olympic Games";

是正确可行的,但是以下语句却不能编译:

char s[80];
s="The Beijing 2008 Olympic Games";

字符串常量只能在初始化数组时一次性保存到数组的各元素中。数组定义后,要改变字符数组的值,就只能逐个修改数组元素的值。例如,可以按以下方式对数组各元素分贝赋值:

char s[80];
s[0]='T';
s[1]='h';

而对于字符串指针变量,可以使用以下方式修改指针变量所指向的字符串常量:

char *s;
s="The Beijing 2008 Olympic Games";
s="One World One Dream";

在程序运行过程中,可以随时改变字符串指针变量的值,实质是将另一个字符串常量的首地址填入字符串指针变量中,使其指向新的字符串常量。

在以上几点可以看出字符串指针变量与字符数组在使用时的区别,同时也可以看出使用指针变量更加的方便。

9.7 指针数组

在前面使用数组指针的时候就已经提到过指针数组,两个词语很容易混淆。数组指针的本质是一个指针,其指向的数据类型是一个数组构成的(将数组作为一个数据类型来看待);而指针数组的本质是一个数组,数组中的每一个元素用来保存一个指针变量。

9.7.1 指针数组的概念

一个数组的各元素值为指针类型数据时,这个数组就称为指针数组。指针数组的所有元素都必须是具有相应存储类型好指向相同数据类型的指针变量。

我们可以打个生动的比喻,指针数组就像是一堆门牌号的集合,它专门用来存放门牌号。

使用以下的形式来定义指针数组:

类型说明符 *数组名[数组长度];

其中,类型说明符为指针所指向变量的类型。

例如:

char *p[5];

定义了5个元素的指针数组,每个数组元素可以保存一个类型为char *的指针变量的值。也就是说,每个数组元素都是一个char *字符指针。

在上面的语句中,由于运算符[]比运算符*的优先级高,因此p与[5]先结合,形成p[5]形式,表示是一个具有5个元素的数组;然后再与p前面的 *结合, *表示此数组是指针类型。所以

*p[5]表示p是一个指针数组,它有5个元素,每个元素值都是一个指针,指向char变量。

要注意指针数组和二维数组指针的区别。二维数组指针变量是单个的变量,其定义形式如下:

类型说明符 (*指针变量名)[数组长度];

其中,(*指针变量名)两边的括号不能少。而指针数组类型表示的是多个指针的集合,其中定义形式中," *数组名"两边不能有括号。

例如:

int (*p)[5];

表示个指向二维数组的指针变量,该二维数组的列数为5,而下面的写法:

int *p[5];

表示p是一个指针数组,有5个下标变量,均为指针变量。

**提示:**通常可以用一个指针数组来指向一个二维数组。指针数组中的每个元素都被赋予二维数组每一行的首地址,因此也可以理解为指向一个一维数组。

例如,下面的程序用一个指针数组指向二维数组的每一行的首地址。

#include<stdio.h>

int main()
{
    int a[3][3] = { {1,2,3},{4,5,6},{7,8,9} };
    int *p[3] = { a[0],a[1],a[2] };
    int i;

    for (i = 0; i < 3; i++)
        printf("%d %d %d\n", *(p[i] + 0), *(p[i] + 1), *(p[i] + 2));

    return 0;
}

在上面的程序中,第6行的代码定义了一个指针数组,并为数组的每个元素赋初值,使其指向二维数组的每一行首地址。a[0]、a[1]、a[2]分别表示二维数组a每一行的首地址。

数组元素p[0]的值为二维数组a第1行的首地址,因为数组元素p[0]是一个指针,所以p[0]加上或减去一个整数n时,指针将指向后n个或前n个相同类型的元素。p[0]+0的值将是二维数组a第1行第1列的地址,p[0]+1的值就是二维数组a第1行第2列的地址,p[0]+2的值就是二维数组a第1行第3列的地址。

下面将以上程序改写为二维数组指针方式,具体程序如下:

#include<stdio.h>

int main()
{
    int a[3][3] = { {1,2,3},{4,5,6},{7,8,9} };
    int(*p)[3];
    int i;

    p = a;
    for (i = 0; i < 3; i++)
        printf("%d %d %d\n", *(p[i] + 0), *(p[i] + 1), *(p[i] + 2));

    return 0;
}

**警告:**二维数组指针名p是一个变量,所以可以对其执行自增/自减运算;而指针数组名p只是表示该数组的首地址。是一个指针常量,不能对其进行自增/自减运算。

9.7.2 用指针数组处理字符串

指针数组可以非常方便的用来指向多个字符串,使字符串的处理更加的方便、灵活。

前面介绍了使用数组保存字符串的方法,使用一维数组可以保存一个字符串,或使用一个字符串指针可以指向一个字符串常量。例如:

char s1[]="String1";
char *ps="String2";

在程序中,若需要同样处理多个字符串,则可以通过定义一个二维数组,分别存放多个祝福词。例如:

char s1[][14]={"C",|"Basic","Foxpro","Visual Studio"};

有了以上定义,就可以将s1[0]、s1[1]、s1[2]、s1[3]分别当做一个字符串来处理。

用以上语句定义二维数组s1后,编译器将在内存中分配一片连续的控件来存储这4个字符串。对于二维数组,每一行所占的字节数必须相等,因此,保存以上语句中的4个字符串需要使用4x14=56个字节(每个字符串后自动添加结束字符’\0’)。

**提示:**用二维数组保存多个字符串比较浪费内存空间。二维数组的行长度必须以最长字符串来定义。这时,如果使用指针数组,则可以很方便的解决内存浪费的问题。

例如:使用以下程序来定义4个字符串:

#include<stdio.h>

int main()
{
    int i;
    char *s[]={
        "C",
        "Basic",
        "Foxpro",
        "Visual Studio"
    };
    
    for(i=0;i<sizeof(s)/4;i++)
        printf("%s\n",s[i]);
    
    return 0;
}

在以上程序中,第6行定义了一个指针数组。在定义时未指定该数组的长度(元素数量),而是通过初始化字符串由编译器自动决定字符串的长度。在初始化字符串时,为了方便阅读,将每一个字符串单独排在一起。在编程时也应该养成这种习惯,使代码的排列尽量方便阅读。第13、14行使用循环输出每个字符的内容。第13行使用sizeof(s)/4得出数组的元素个数,其中sizeof(s)将得出数组s占用的内存字节数,因为一个指针将占用4字节,所以将其除以4,即可得到数组元素数量。使用这种方式,如果需要对更多的字符串处理,只需要在第6行中添加更多新的字符串,第13行的代码不需要修改。

在以上代码中,将指针数组中的每个元素指向不同的字符串。这时4个字符串不需要保存在一片连续的内存单元,只需要将个字符粉笔保存到内存单元,然后将首字符地址保存到指针数组中即可。

使用指针数组处理多个字符串的优点就是节约内存空间,因为每个字符串是单独存放的。不要求每个字符串占用同样的字节数。

9.7.3 用指针数组作为函数参数

从程序设计的角度来看,将多个字符串传递给函数处理时,因为传递的是一维数组,所以形参的定义比较简单。例如,strsort()可以对多个字符串进行排序,函数头的定义形式如下:

void strsort(char *s[],int n);

形参char *s[]为字符串指针数组,将要处理的指针数组的首地址作为实参传入。形参int n表示要处理的字符串的数量。

如果使用二维数组来保存多个字符串,要将其传入函数中进行修复,则比较麻烦。首先要确定二维数组每一行的长度,再定义二维数组指针作为形参。其函数头的定义形式如下:

void strsort(char (*s)[14],int n);

这样的函数就不具有通用性。因为函数要处理的字符串长度是不会相同的,而以上函数头定义的形式要求二维数组每一行的长度都必须为14.

下面用实例编写字符串排序函数strsort():

#include<stdio.h>


#include<string.h>
int main()
{
    void strsort(char *s[],int n);
    int i;
    char *s1[]={
        "C",
        "Basic",
        "Foxpro",
        "Visual Studio"
    };
    
    strsort(s1,sizeof(s1)/4);
    
    for(i=0;i<sizeof(s1)/4;i++)
        printf("%s\n",s1[i]);
    
    return 0;
}

void strsort(char *s[],int n)
{
    int i,j;
    char *t;
    
    for(i=0;i<n-1;i++)
        for(j=i+1;j<n;j++)
        {
            if(strcmp(s[i],s[j])>0)
            {
                t=s[i];
                s[i]=s[j];
                s[j]=t;
            }
        }
}

9.8 指向指针的指针

如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。

指针的指针就像一个地图一样,详细的说明了每个门牌号所在的具体位置。

9.8.1 理解指向指针的指针

一个指针变量可以保存整型变量、实型变量、字符串变量的地址(也称为指向这些变量),也可以指向指针类型的变量。将一个指针变量指向另一个指针变量,称为指向指针的指针变量,读起来有点拗口。

假设变量i的值为10,保存在首地址为20000的内存区域中。还有一个指针变量p,使用以下语句使其指向变量i:

int *p=&i;

即指针变量的值为变量i的地址20000.

若再定义一个指针变量pp,使该变量指向变量p。这时,变量pp就是指向指针变量的指针变量,简称为指向指针的指针。为了便于理解,一般将直接指向变量的指针变量称为一级指针,将指向指针变量的指针变量称为二级指针。p为一级指针,pp为二级指针。

在介绍定义指向指针的指针变量之前,先回忆一下定义指向变量的指针变量的格式。

类型说明符 *指针变量名;

其中*号表示定义的是一个指向“类型说明符”的指针变量。例如:

char *pc;

定义一个指向字符变量的指针变量,即在变量pc中保存一个字符变量的地址。

要定义指向指针的指针变量,与上面的格式有些类似,只是多使用一个*号,具体的格式如下:

类型说明符 **指针变量名;

在指针变量名的前面使用了两个*号,右边的 *号表示该指针变量收集指针类型的变量,左边的 *表示该变量所指向的对象是指针变量。这里的指针变量名为二级指针变量,将指向一个一级指针变量;类型说明符为一级指针变量所指向的变量的数据类型。

二级指针变量也用来保存一个变量的地址,一般使用取地址运算符&将一级指针变量的地址保存到该变量中。

使用以下语句可以完成之前描述的二级指针;

int i=10;
int *p=&i;
int **pp=&p;

第2条语句定义了一个一级指针变量,指向变量i,第3条语句定义了二级指针变量,指向了变量p。

**注意:**在表达式中,使用*p表示从指针变量p保存的内存地址中取出数据,即得到变量i的值。按照这种规则,对于二级指针变量,使用 *pp得到的是一级指针变量p的地址,对改地址再次使用运算符 * ( *pp)即可得到一级指针变量p所指向的变量的值。

下面的实例演示这种引用关系:

#include<stdio.h>

int main()
{
    int i = 0;
    int *p = &i;
    int **pp = &p;

    printf("二级指针变量pp所指向的地址:%d\n", pp);
    printf("变量p的地址:%d\n",&p);
    printf("一级指针变量p指向地址:%d\n",p);
    printf("变量i的地址:%d\n",&i);
    printf("i=%d\n", i);
    printf("p=%d\n", p);
    printf("*p=%d\n", *p);
    printf("pp=%d\n", pp);
    printf("*pp=%d\n", *pp);
    printf("**pp=%d\n", **pp);

    return 0;
}

下面分别介绍各输出语句的功能:

  • 第9行输出变量pp的值,即其指向的一级指针变量p的地址。
  • 第10行使用取地址运算符&,也可以得到一级指针变量p的地址。
  • 第11行输出变量p的值,即其指向的变量i的地址。
  • 第12行使用取地址运算符&,也可以得到变量i的地址。
  • 第13行输出变量i的值。
  • 第14行输出变量p的值,与请求行的输出结果一致。
  • 第15行输出表达式*p的值,即一级指针变量所指向的值,也就是变量i的值。
  • 第16行输出变量pp的值,与第9行的输出结果一致。
  • 第17行输出表达式*pp的值,取二级指针变量所指向的一级指针变量的值,也就是一级指针变量p的值—变量i的内存地址。
  • 第18行输出表达式**pp的值,由于运算符 *是右结合,该表达式可以理解为 *( *pp), *pp从第17行的输出可知道是一级指针变量p的值,因此 * *pp可以看做是 *p,即和第13行的输出结果一致,输出变量i的值。

从以上程序可知,对于二级指针变量,使用一个*运算符得到的是一个内存地址(表示一级指针变量的值),使用两个 *运算符可以得到所指向变量的值。

9.8.2 二级指针变量和数组

在C语言中,数组名就是一个指针常量,保存数组的首地址。因为数组名是一个指针常量,不能修改其指向的值,因此可以定义一个指针变量指向指针数组。这样。使用数组名加下标就可以访问数组中的元素了,使用指针名加下标也可以访问数组中的元素。

现在如果定义一个二级指针变量,让其指向一级指针,就可以使用二级指针变量操作数组。例如,使用以下语句定义二级指针变量:

int a[10];
int *p=a;
int **pp=&p;
1. 用二级指针变量操作一维数组

可以用以下代码来输出数组a中的各元素的值:

#include<stdio.h>

int main()
{
    int i, a[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int *p, ** pp;

    printf("用数组的方式输出数组各元素的值:\n");
    for (i = 0; i < 10; i++)
        printf("%4d ", a[i]);

    printf("\n使用一级指针变量输出数组各元素的值:\n");
    p = a;
    for (i = 0; i < 10; i++)
        printf("%4d ", *p++);

    printf("\n使用二级指针变量输出数组各元素的值:\n");
    p = a;
    pp = &p;
    for (i = 0; i < 10; i++)
        printf("%4d ", *(*pp + i));

    printf("\n使用二级指针变量**p方式输出数组各元素的值:\n");
    p = a;
    for (i = 0; i < 10; i++, p++)
        printf("%4d ", **pp);

    return 0;
}

从上面的情况可以看出,对于一维数组,使用一级指针变量就可以方便地操作数组元素,而使用二级指针变量只会让情况更加的复杂。

2. 用二级指针操作二维数组

用二级指针变量操作二维数组,同样,在二级指针变量pp中保存一级指针变量的地址,修改一级指针变量p的指向,使用**pp就可以访问到不同的数组元素。

下面演示用二级指针变量输出二维数组的数据:

#include<stdio.h>

int main()
{
    int i,j,a[3][4]=
    {
        {1,2,3,4},
        {5,6,7,8},
        {9,10,11,12}
    }
    int *p,**pp;
    
    pp=&p;
    printf("用二级指针变量的方式来输出二维数组:\n");
    pp=&p;
    for(i=0;i<3;i++)
    {
        p=a[i];
        for(j=0;j<4;j++)
            printf("%4d ",*(*pp+j));
        printf("\n");
    }
    
    printf("用二级指针变量**pp的方式来输出二维数组:\n");
    for(i=0;i<3;i++)
    {
        p=a[i];
        for(j=o;j<4;j++,p++)
            printf("%4d ",**pp);
        printf("\n");
    }
    
    return 0;
}

在程序的第13行,使用了二级指针变量pp指向了一级指针变量p,因为p的内存地址不会再发生再变化,所以可将该语句放在程序的前面,在后面的循环中不需要修改其值。

**注意:*在第20行使用 (*pp+j)的方式输出数组中的值,这种方式在前面层多次的使用,首先通过 *pp得到一级指针变量p保存的值,由第18行可知,变量p中保存着二维数组中某一行的首地址,然后再在后面加上一个整数j,得到该行第j个元素的地址,最后使用运算符 *得到改地址的值。

第29行使用**pp方式输出二维数组元素的值,该表达式可以看做 *( *pp), *pp得到变量p中的值。由26行可知,变量p中保存着二维数组中某行的首地址,该首地址也就是该行的第1个元素的地址,因此使用 *( *pp)的方式将输出某行第1个元素的值。接着执行循环种中的p++,是一级指针变量的值增加1,因为该变量保存的值是一个指针,所以对其自增运算将指向下一个元素,该行的第2个元素,再使用 * *pp即可输出第2个元素的值。通过第28行的循环,即可将二维数组一行中的每个元素都输出。最后通过第25行的外循环,即可输出整个二维数组中的元素值。

3. 用二级指针变量操作指针数组

在前面的程序中还是用指针数组来操作多个字符串。现在还可以通过二级指针变量的方法来操作进行这些操作。首先定义一个字符串指针数组s,用来指向多个字符串常量,再定义一个二级指针变量p,使其指向数组s。因为数组s中的每个元素都是数组,所以指针变量p必须定义为指向指针的指针(二级指针变量)。

#include<stdio.h>
#include"string.h"

void strsort(char **p, int n);

int main()
{
    int i;
    char* s[] = {
        "C",
        "Basic",
        "Foxpro",
        "Visual studio"
    };

    strsort(s, sizeof(s) / 4);

    printf("\n排序后的数据:\n");
    for (i = 0; i < sizeof(s); i++)
        printf("%s\n", s[i]);

    return 0;
}

void strsort(char **p, int n)
{
    int i, j;
    char* pstr;

    for (i = 0; i < n - 1; i++)
        for (j = i+1; j < n; j++)
            if (strcmp(*(p + i), *(p + j)) > 0)
            {
                pstr = *(p + i);
                *(p + i) = *(p + j);
                *(p + j) = pstr;
            }
}

9.9 指针和函数

前面介绍了将函数的形式参数设置为指针的情况。另外,函数的返回值也可以是一个指针(地址),还可以将函数首地址赋值给指针,然后通过指针变量来调用该函数。

9.9.1 返回指针的函数

每个函数都可以返回一个值,返回值可以是char、int、float、double等类型,当其返回值类型设置为void时,表示函数没有返回值。在C语言中,还允许一个函数的返回值是一个指针,这种返回指针的函数称为指针型函数。

定义指针型函数的形式如下:

类型说明符 *函数名(形参表)
{
    ...
}

其中,函数名之前加了*号表面这是一个指针型函数,其返回值是一个指针。

类型说明符表示返回的指针值所指向的数据类型。一般用这种函数返回一个字符串常量的首地址。

编写一个函数,用于阿拉伯数组表示的月份转换为对应的英文名称。函数一次只能返回一个值,若要返回一个字符串(由多个字符组成),则用前面介绍的方法可以通过函数的形参返回多个字符。例如,用以下的函数头:

void cmonth(int month,char s[]);

要调用以上形式的函数,首先要定义一个数组,再将数组作为实参传递给函数,最后将函数处理的结果用另一条语句输出。使用类似以下的程序:

char s[20];
cmonth(5,s);
printf("月份:%2d-->英文名称:%s\n",5,s);

如果函数返回字符串,则可以使用以下方式来调用函数,并输出返回值:

printf("月份:%2d-->英文名称:%s\n",i,cmonth(i));

编写指针类型函数可返回字符串的首地址。下面的程序演示指针类型函数的编写方法:

#include<stdio.h>

char *cmonth(int month);

int main()
{
    int i;

    printf("请输入月份数字:");
    scanf("%d", &i);

    printf("月份:%2d-->英文名称:%s\n", i, cmonth(i));

    return 0;
}

char *cmonth(int month)
{
    char* str_month[] = {
        "Illegal Month",
        "January",
        "February",
        "March",
        "April",
        "May",
        "June",
        "July",
        "August",
        "September",
        "October",
        "November",
        "December"
    };
    char* p;

    if (month >= 1 && month <= 12)
        p = str_month[month];
    else
        p = str_month[0];
    return p;
}

在以上程序中,第17-41行定义了函数cmonth(),该函数需要一个整型变量作为实参,返回一个字符串指针。在函数体内部,第19行定义了指针数组,该数组中的每个指针都指向一个字符串常量,第36行判断实参month是否合法,弱不合法则将第1个元素赋给指针变量p,这样,字符串指针变量中的值就与指针数组第1个元素中的值相同了,即指向字符串常量"Illegal Month"。

在main()函数的第12行中printf()函数输出列表中包括cmonth()函数的返回值(其返回值是一个字符串的首地址),printf()函数的格式字符%s从该首地址开始输出字符串。

9.10 指针和const变量

使用const关键字可以定义一个符号常量。例如,程序中有以下的语句:

const float PI=3.14;

有了这句定义后,程序中就不允许再对PI重新赋值了。以上语句也可以写为以下形式:

float const PI=3.14;

即const关键字与变量类型说明符的前后位置可换。

**提示:**const就像上司的指令一样,可以理解为当变量接收了这个命令后就变成了一个常量,无法再对它的值进行修改。

9.10.1 用const控制指针

在定义指针变量的时候,也可以通过const关键字限制对指针变量的值进行修改,或限制对指针变量指向的变量的值进行修改。下面介绍这两种情况:

首先看看以下的定义指针变量的语句:

const int *p;

可写为以下的形式:

int const *p;

在这种写法中,const关键字控制*p的值不能修改。 *p表示指针变量指向的变量,也就是说,这种方式定义的指针,不允许使用 *p的方式修改指向变量的值。例如,有以下的代码:

int i=3,j;
int const *p=&i;
*p=5;

一般情况下,第3行语句是修改变量p所指向变量的值,但是,在第2行中定义指针变量p的时候使用了const关键字,所以,第3行将出错。

需要注意的是,使用const关键字控制的是*p,所以不能通过 *p来修改变量i的值,但是可以直接对变量i赋值。例如,紧接着上面的程序。下面的语句也能成立。

i=8;

同时,因为关键字const没有控制变量p,所以变量p的值也可以修改,即可以让其指向其他变量,例如,以下代码可以让指针变量指向变量j。

p=&j;

但是不能使用*p来修改j的值。

接下来看另一种情况:

int * const p;

在以上语句中,关键字const将*和p分割开了。这时,使用const关键字控制变量p,所以变量p的值不允许修改,即该指针是一个指针常量,与数组名相同。例如:

int i=3,j=4;

int * const p=&i;
*p=5;
p=&j;

在以上程序中,使用const关键字控制变量p,对于这种类型的指针变量,必须在定义的同时为其赋初值。在第2行定义后,在以后的程序中江滨再修改指针变量的指向。如上程序中的第4行就将出错,不能通过编译。

虽然指针变量的值不允许修改,但其指向的变量的值是允许修改的,如第3行使用*p将其指向的变量i的值修改为5是正确的。

9.10.2 const的几种特殊的用法

对于指针和const的组合,下面看一些特殊情况,在有的编译器中无法通过编译。

前面说过,使用const关键字控制的变量是不允许修改其值的。下面看程序:

#include<stdio.h>

int main()
{
    const int k=3;
    int *p;
    
    p=&k;
    *p=5;
    printf("*p=%d\n",*p);
    printf("k=%d\n",k);
    
    return 0;
}

在第5行定义了符号常量k,第8行将符号常量的地址保存到指针变量p中(使指针变量p指向k),这句在有的编译器是不能通过的。在第9行通过*p的方式修改指针变量p指向的值,即将符号常量k的值改为5。

如果要杜绝以上的情况,则在定义指针时也需要使用关键字进行控制。例如:

const int k=3;
const int *p;
p=&k;

在运行完以上的3行语句后,在程序中既不能直接修改k的值,也不能间接通过修改*p修改k的值,但可以是指针p指向别的变量或符号常量。

如果既要控制修改指针变量指向的值,又要限制修改指针变量的值,则可以使用以下的方式:

const int k=3;
const int * const p=&k;

9.11 拓展训练

9.11.1 训练一:学生成绩管理

现有5名学生,4门科目:语文、数学、英语、化学。考试之后要对这5名学生做以下的统计:

  • 求第一门课语文成绩的平均成绩
  • 求每一名学生的平均成绩,并将其输出。
#include<stdio.h>
void avcourse(float* p1)
{
    int i;
    float sum = 0, average;
    for (i = 0; i < 5; i++)
        sum += *(p1 + 4 * i +1);
    average = sum / 5;
    printf("course 1 average is %f\n", sum);
}
void aver(float* p1)
{
    int i, j;
    float sum, average;
    for (i = 0; i < 5; i++) {
        sum = 0;
        for (j = 0; j < 4; j++)
            sum += *(p1 + 4 * i + j);
        average = sum / 4;
        printf("student%d: average is %f\n", i + 1, average);
    }
}
void main()
{
    int i;
    float score[5][4], * p1, * p2;
    for (i = 0; i < 5; i++)
    {
        printf("input student %d score: \n", i + 1);
        scanf("%f %f %f %f", &score[i][0], &score[i][1], &score[i][2], &score[i][3]);
    }
    p1 = &score[0][0];
    avcourse(p1);
    aver(p1);
}

9.11.2 训练二:输出两个数中的大者

从键盘输入两个数x和y,输出两个数中的较大的数

#include<stdio.h>

void main()
{
    int MAX();
    int x, y, z, (*p)();
    p = MAX;
    scanf("%d %d", &x, &y);
    z = (*p)(x, y);
    printf("the max is %d\n", z);
}

int MAX(int a, int b)
{
    int c;
    c = a > b ? a : b;
    return c;
}

9.12 技术解惑

9.12.1 指向指针的指针有什么用处

在前面介绍的指针都是直接指向变量的指针。在C语言中,指针还可以指向指针。若一个指针不是指向变量,而是指向另外一个指针,前一个指针才指向变量的地址,那么这种指针称为指向指针的指针,也称为二级指针。指针的指针就像是一幅地图,详细的说明了每个门牌号的所在的具体位置。

二级指针的定义x形式如下:

类型说明符 **指针名;

例如:

char **P;

上述语句定义了一个字符串的二级指针。下面通过一个实例来了解二级指针的应用。

编写程序,通过二级指针输出多个字符串。

#include<stdio.h>

int main()
{
    int i,j;
    char **p;
    char* s[] = { "Hello","Student","Teacher","Team" };
    p = s;
    for (i = 0; i < 4; i++)
        printf("%s\n", p[i]);
    for (j = 0; j < 4; j++)
        printf("%d\n",(*p)++);
}

9.12.2 数组指针和指针数组的区别

数组指针的定义为:

int (*p)[n];

()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说指向p+1时,p要跨过n个整型数据的长度。

如果要将二维数组赋给一个数组指针,则应该这样赋值:

int a[3][4];
int (*p)[4];
p=a;
p++;

所以数组指针也称为指向一维数组的指针,也称为行指针。

而指针数组的定义:

int *p[n];

[]优先级高,先与p结合成为一个数组,再由int *说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,p指向下一个元素。这样赋值是错误的:p=a,因为p是一个不可知的表示,只存在p[0]、p[1]、p[2]…p[n-1],而且它们分别是指针变量,可以用来存放变量地址。但是这样赋值: *p=a,这样的 *p表示指针数组的第一个元素的值,也就是a数组的首地址的值。

如果要将二维数组赋值给指针数组,则应该这样赋值:

int *p[3];
int a[3][4];
p++;
for(i=0;i<3;i++)
    p[i]=a[i];

10. 不同形式的存储----结构、联合和枚举

10.1 结构的概念

在实际的程序中,一般不会只处理孤立的数据。例如,在管理员工信息时,可能需要记录姓名、出生日期、电话号码、家庭住址、聘用日期、基本工资等信息。如果分别定义变量来保存这些数据,虽然可以处理,但数据很散乱,不方便管理。从习惯上来说,一般将一名员工的所有数据作为一个整体来处理。为了解决这类问题,C语言中给出了另一种构造数据类型—结构。它相当于其他高级语言中的记录。

10.1.1 定义结构类型

在第6节中介绍的数组就属于构造数据类型。在数组中可以保存多个不同类型的数据。如果要保存多个不同类型的数据,就不能使用数组了。

结构就是一种构造数据类型,它就像一个大的课堂,若由若干不相同类型的成员构成。与数组不同,结构中的成员可以是不同的数据类型,每个成员可以是基本数据类型或者构造数据类型。对于构造类型的数据,在使用之前必须定义其构成方式。如在使用数组前必须定义数组每一维的长度。对于结构,在使用之前也必须定义它的逻辑形式。

一般使用以下语句来定义结构的逻辑形式:

struct 结构名
{
    类型说明符 成员名1;
    类型说明符 成员名2;
    
    类型说明符 成员们n;
};

其中关键字struct实际上是英文structure的缩写。紧随其后的是结构名,也称为结构的标志名,应是一个合法的标识符,用于表示所定义的结构。由关键字struct加上结构名就构成了一种称为结构类型的数据类型。

在结构定义语句中,包含在一对大括号中的就是结构的成员列表,对每个成员都使用与变量相似的方法进行说明。

**注意:**定义结构体的最后必须要有分号,以表示结构定义语句结束。

在第6章介绍过的,在程序中定义数组后,编译器将给数组分配能保存指定数量的元素的内存空间。但是对于结构定义语句,在定义时只是定义了一种结构类型的逻辑结构,编译器并不会给其分配内存空间。只有使用新定义的结构数据类型定义一个变量时,编译器才会为该变量分配空间。

例如:在C语言中没有日期类型,为了方便处理日期,可以定义一个日期结构,将年月日分别用三个整数表示:

struct date
{
    int year;
    int month;
    int day;
};

在以上定义结构的语句中,结构名为date,在该结构体当中包含3个成员:year、month、day,分别表示年月日。在实际的使用过程中,年为四位数字,日和月都是2位数字,如果将3个成员都定义为int类型,则将占用12字节。而int型表示的数据范围很大,因此可以将其定义为表示范围更小的整数,如用short int表示年份是完全够用的(无符号short int型的取值范围是065535);同样,因为月和日只需要2位整数就能表示,所以char型完全可以作为整数使用(char型的取值范围为0255)。

因此,还可以使用short int和char类型来定义这3个成员,使其占用更少的内存空间。例如。使用以下的成员定义date结构,将只占用4字节。

struct date
{
    short int year;
    char month;
    char day;
};

10.1.2 定义结构变量

使用struct定义了结构类型后,就可以使用该类型定义对应的变量,然后在程序中引用结构中的成员。用结构类型定义的变量称为结构变量。

使用结构类型定义变量有3种方法。下面以前面定义的date结构为例演示具体方法。

  • 第1种方法是先定义结构,再使用结构类型定义结构变量:
struct date birthday,entrydate;

以上语句使用struct date定义了两个变量birthday和entrydate。

  • 第2种方法计时在定义结构类型的同时定义结构变量。例如:
struct date
{
    short int year;
    char month;
    char day;
}birthday,entrydate;

以上为一条语句,在定义结构类型的同时定义了birthday和entrydate两个变量。在后面的程序中,还可以使用struct date类型将其他变量定义为该类型。

  • 第3中方法是在定义结构类型时不设置结构名,而直接说明结构变量。例如:
struct
{
    short int year;
    char month;
    char day;
}birthday,entrydate;

这种方法与第2种方法的区别在于省略了结构名,而直接给出了结构变量。使用这种方法只能在定义结构变量是定义需要的变量,在以后的程序中无法再次使用该类型,所以一般不使用这种方法。

**提示:**以上的3种方法都定义了birthday和entry两个变量,这两个变量具有相同的结构类型。定义好这两个结构变量后,即可向两个结构变量中的各个成员赋值。

10.1.3 使用结构变量

结构类型一般由多个成员变量组成,对于每个成员变量,与使用同类型的变量一样,可对其进行赋值、参加运算、输出其保存的值等操作。

要访问结构变量中的成员,需要用以下的方式:

结构变量.结构成员

小数点称为句点运算符,也称结构体成员运算符。可以从指定的结构变量中访问到指定的成员变量。

例如,可使用后以下语句来设置birthday变量各成员变量的值:

birthday.year=1976;
birthday.month=10;
birthday.day=1;

可以使用printf()函数分别输出各成员变量的值。例如:

printf("%4d%02d%02d\n",birthday.year,birthday.month,birthday.day);

以上语句将输出3个成员变量的数据,得到一个按日期排列的数据。在printf()函数的格式字符串中使用%02d来控制当输出数据不足2位时,在前面添加0。

在以上的幅值和输出语句中还看不出使用结构有什么好处,反而增加了程序代码的录入量。

使用结构组织数据,首先,可以使用相关数据更符合人们的思维习惯。例如,一般情况下,说到日期就会想到表示年月日三部分,使用结构来表示日期,将其看成一个整体,符合日常的思维习惯。其次,可以使用简单的幅值语句将一个结构变量所有的成员变量都复制到另外一个结构变量对应的成员变量中。例如:

entrydate=birthday;

以上语句将结构变量birthday中3个成员变量的值复制到结构变量entrydate对应的成员变量中。相当于下面3条语句:

entrydate.year=birthday.year;
entrydate.month=birthday.month;
entrydate.day=birthday.day;

如果结构变量中包含很多的成员变量,则使用结构变量之间的赋值可以节省大量的时间和代码量。

在学习完这里的内容之后,将会了解很多结构的优点。下面先通过一个程序了解结构和结构变量的使用。

#include<stdio.h>

int main()
{
    struct date
    {
        short int year;
        char month;
        char day;
    };
    struct date birthday;
    
    printf("请输入出生日期(按照yyyy/mm/dd格式输入):");
    
    scanf("%4d %02d %02d",&birthday.year,&birthday.month,&birthday.day);
    
    if(birthday.year<1900||birthday.year>2021||birthday.month<1||birthday.month>12||birthday.day<1||birthday.day>31)
        printf("日期输入错误!");
    else
        printf("您的生日是:%4d/%02d/%02d\n",birthday.year,birthday.month,birthday.day)return 0;
}

在以上的程序中,第510行中定义了一个结构,用来保存日期。第5行使用struct关键字定义了一个结构名为date,第79行定义表示了年月日等3个数据的成员变量。

第11行使用struct date结构类型定义了一个结构变量birthday,通过在该变量后使用据点运算符可访问结构中的成员变量。

第17行使用if语句判断用户输入的值是否合法,要求在年份19002021之间,月份在112月之间,日在1~31之间。在该语句需要判断多个条件,按照分行格式来书写,方便查看。

第20行使用printf()函数输出结构变量中各成员变量的值。与输出普通整型变量的方式相同。

10.2 结构的嵌套

在上一节介绍过结构的定义和使用方法,结构中的成员都设置为基本数据类型。其实,结构中的成员允许是各种数据类型,比如指针、数组,甚至可以是另一个结构。

10.2.1 包含数组的结构

结构中的成员可以是一个数组。例如,在定义保存员工信息的结构体中,需要使用数组来保存院的姓名、地址等数据,其结构如下:

struct emp
{
    char name[10];
    char address[20];
    char sex;
};

使用以上的结构类型定义一个结构变量:

struct emp emp1;

编译器将会为结构变量emp1中的成员分配对应的存储空间。

可以使用sizeof运算符得到结构类型的长度。如果结构emp中只包含上面列出的3个成员,则其 长度为31字节。

要访问结构中的数组元素,可以使用以下的形式:

emp1.name[0]='w';

使用结构中的数组与使用普通的数组相同。例如,一般使用数组来保存字符串,数组名为数组的首地址。可以使用以下程序来让用户输入字符串,并将其保存到数据组各元素当中。

scanf("%s",emp1.name);

但不能使用以下方式给数组中的每个元素赋值:

emp1.name='wuyunhui';

因为数组名只代表数组的首地址,是无法将一个字符串赋值到数组对应的各元素中的,与普通数组是一样的。

使用以下程序可以输出数组中保存的字符串:

printf("%s\n",emp1.name);

10.2.2 包含指针的结构

结构中的成员还可以是指针,在结构中包含的指针主要是用来处理字符串。例如:

struct emp
{
    char *name;
    char *address;
    char sex;
};

使用以上结构类型定义一个结构变量。

struct emp emp1;

编译器将会为结构变量emp1中的成员分配相应的存储空间。每个指针只占用4字节,用来存放指向目标的地址。每个指针指向一个字符串常量。

**提示:**可以使用sizeof运算符得到结构类型的长度。如果emp中只包含上面列出的3个成员,则其长度为9字节,并不包括指向字符串的长度。

使用结构中的指针和普通指针一样。例如,可以使用以下程序让用户输入字符串:

scanf("%s",emp1.name);

也可以使用以下的方式将字符串常量的首地址赋值给指针,这就比数组方便:

emp1.name="wuyunhui";

使用以下程序可输出指针指向的字符串常量:

printf("%s\n",emp1.name);

10.2.3 包含结构的结构

结构的成员还可以是另外一个结构。例如,在员工信息的结构中可以包含作为日期结构的成员:

struct emp
{
    char name[10];
    char address[20];
    char sex;
    struct date birthday;
    struct date entryday;
}

在以上的结构struct emp中,成员birthday又是一个结构类型,它又有自己的成员year、month、day,形成了结构的嵌套定义。

**提示:**C语言没有限制结构嵌套的层数,但是嵌套层数一般很少超过3层。

使用以上结构类型定义一个结构变量:

struct emp emp1;

要访问结构中嵌套结构的成员,必须使用两个句点运算符。可以使用以下的方式:

emp1.birthday.year=1999;
emp1.birthday.month=07;
emp1.birthday.day=26;

在嵌套定义的结构变量中,每个变量从左到右、从外到内的方式引用。

下面的程序演示给嵌套结构中的成员赋值、输出的方法:

#include<stdio.h>

struct date
{
    short int year;
    char month;
    char day;
};

struct emp
{
    char name[20];
    char address[20];
    char sex;
    struct date birthday;
    struct date entryday;
};

int main()
{
    struct emp emp1;

    printf("请输入员工姓名:\n");
    scanf("%s", emp1.name);

    printf("请输入员工地址:\n");
    scanf("%s", emp1.address);

    printf("请输入员工的性别(男-1,女-0):\n");
    scanf("%d", &emp1.sex);

    printf("请输入出生日期:\n");
    scanf("%4d %2d %2d", &emp1.birthday.year, &emp1.birthday.month,&emp1.birthday.day);

    printf("请输入聘用日期:\n");
    scanf("%4d %2d %2d", &emp1.entryday.year, &emp1.entryday.month,&emp1.entryday.day);

    printf("\n员工基本信息:\n");
    printf("姓名:%s\n", emp1.name);
    printf("住址:%s\n", emp1.address);
    printf("性别:%d\n", emp1.sex);
    printf("出生日期:%4d %02d %02d\n", emp1.birthday.year,emp1.birthday.month,emp1.birthday.day);
    printf("聘用日期:%4d %02d %02d\n",emp1.entryday.year,emp1.entryday.month,emp1.entryday.day);

    return 0;
}

**注意:**被嵌套的结构必须先定义。

10.3 初始化结构变量

结构变量中包含多个成员,在程序当中可以分别对每个成员赋值。与数组的初始化类似,在定义结构变量是,也可以对其进行初始化。例如,使用以下形式,在定义结构类型的同时定义结构变量,同时为结构变量设置初值。

struct date
{
    short int year;
    char month;
    char day;
}birthday(1976,10,1);

以上语句相当于下面的多条语句:

struct date
{
    short int year;
    char month;
    char day;
};
struct date birthday;
birthday.year=1976;
birthday.month=10;
birthday.day=1;

也可以在定义结构类型后,使用以下代码,在使用struct date结构类型定义变量的同时初始化结构变量:

struct date birthday={1976,10,1};

在初始化数据时,编译器将按照顺序将逗号分隔的数据逐项赋给结构中的成员。当大括号中的数据数量少于结构中的成员数时,后面的成员将被赋值为0。例如:

struct date birthday={1976,10};

以上的初始化相当于下面的3条语句:

birthday.year=1976;
birthday.month=10;
birthday.day=1;

对于嵌套的结构,可以使用大括号的嵌套方式初始化结构变量各成员的值。

#include<stdio.h>

struct date1
{
    short int year;
    char month;
    char day;
};

struct employee
{
    char name[20];
    char address[20];
    char sex;
    struct date1 birthday;
    struct date1 entryday;
};

int main()
{
    struct employee emp1 =
    {
        "wuyunhui",
        "No.609 Street wanda",
        1,
        {1997,10,1},
        {2000,7,1}
    };

    printf("姓名:%s\n", emp1.name);
    printf("住址:%s\n", emp1.address);
    printf("性别:%s\n", (emp1.sex == 1) ? "男" : "女");
    printf("出生日期:%4d %02d %02d\n", emp1.birthday.year, emp1.birthday.month, emp1.birthday.day);
    printf("聘用日期:%4d %02d %02d\n", emp1.entryday.year, emp1.entryday.month, emp1.entryday.day);
    return 0;

}

以上大部分代码与上一个程序相同。第21行使用的struct date1结构类型定义变量并进行初始化,第23-27行为初始化结构变量的数据。

10.4 结构数组

一个结构变量可将具有一定逻辑意义的多个不同类型的数据组合在一起。例如,本章前面所使用的emp结构将字符指针、字符、整型等不同的数据类型组合在一起。这样,每一个结构emp定义的变量都可以表示一个员工的信息。

如果需要处理多个员工的信息,则可以用结构emp定义多个结构变量。更好的方法便是定义结构数组。数组每个元素就是一个结构变量。在实际应用中,经常用结构数组来表示具有相同数据结构的一个群体,比如一家公司的员工资料、员工工资表等等。

结构也可以看成一间屋子,里面包含了许多不同类型的住户,那么结构数组便可以看成一栋楼,由许多的屋子组成,但里面的住户又各不相同。

10.4.1 结构数组的定义和引用

结构数组的定义方法与基本数据类型的数组定义方法类似,只是结构数组中的每个元素的数据类型都是一个结构。因此,要定义一个结构数组,首先要定义一个结构类型,然后再把数组元素说明为这种结构类型。

例如,在用电脑管理的低保数据中,找出年龄最大的低保人员,并输出其所有信息。

在管理低保户信息时,需要了解低保户的许多信息,如姓名,性别,年龄,销售低保金额等,可以使用结构来表示这些类型不同的数据。当要管理多个低保户的信息时,可以通过结构数组,让数组的每个元素保存一个低保户信息。

首先使用以下代码定义结构:

struct minneed
{
    char *name;
    char sex;
    unsigned short age;
    float amount;
};

接着使用以下代码定义结构数组:

struct minneed needs[2];

编译器将自动为所有结构数组元素分配足够的内存单元,结构数组的元素时连续存放的。

由于每个结构数组元素的类型是结构,所以其使用方法和相同类型的结构变量一样,既可以引用数组的元素,如needs[0],也可以引用结构数组元素的分量,如needs[0].name。像所有数组一样,结构数组的元素的下标也是从0开始的。结果数组分量的引用是通过使用数组下标和结构分量操作符"."来完成的,其一般形式为:

结构数组名[下标].分量名;

对结构数组的各元素可直接赋值。例如,以下代码将第2个结构数组元素赋给第1个结构数组元素:

needs[0]=needs[1];

10.4.2 结构数组的初始化

一个结构变量可以包含多个成员,而结构数组又是由多个结构变量组成的,因此,一个结构数组包含非常多的数据。如果每次都要由用户在程序运行时输入这些数据,那么工作量将十分的巨大。为调试程序,一般可在程序代码中对结构数组进行初始化。而实际运行程序时,可以通过文件保存数据,需要时从文件中读取数据保存到结构数组中。

对结构数组的初始化,与对单个结构的初始化类似,只需要将多个结构元素中各成员的值分别给出即可。例如,下面首先定义一个结构minneed,在使用该结构定义一个结构数组needs[4],并将该结构数组进行初始化。

struct minneed
{
    char *name;
    char sex;
    unsigned short age;
    float amount;
};
struct minneed needs[4]=
{
    {"zhangjun",1,55,150.0},
    {"wumei",0,48,120.0},
    {"duli",0,55,280.0},
    {"liping",1,56,234.0}
};

**提示:**在初始化过程中,要注意初始化值与各个结构成员的数据类型相匹配。

10.4.3 结构数组实例

下面实例演示结构数组的使用方法。要求对低保用户数据进行比较,找出年龄最大的低保户,并输出其相应的资料。

#include<stdio.h>
#define N 4

struct minneed
{
    char* name;
    char sex;
    unsigned short age;
    float amount;
};

int main()
{
    struct minneed needs[4] =
    {
        {"zhangjun",1,55,150.0},
        {"wumei",0,48,120.0},
        {"duli",0,57,280.0},
        {"liping",1,56,234.0}
    };
    int i, m = 0;

    for (i = 1; i < N; i++)
        if (needs[m].age < needs[i].age)
            m = i;

    printf("年龄最大的低保户的资料为:\n");
    printf("姓名:%s\n", needs[m].name);
    printf("性别:%s\n", (needs[m].sex == 1) ? "男" : "女");
    printf("年龄:%d\n", needs[m].age);
    printf("低保金额:%.2f\n", needs[m].amount);

    return 0;
}

10.5 结构指针

指针也可以指向结构变量。当一个指针变量直用来指向一个结构变量时,称之为结构变量指针。结构变量指针中的值是所指向的结构变量的首地址。通过结构指针即可访问该结构变量,这里与数组指针和函数指针的情况是相同的。

这个指针就像每个结构体屋子的门牌号,当我们知道了门牌号,对于屋子里面的信息也就十分清楚了。

10.5.1 定义结构指针

定义结构指针变量的形式如下:

struct 结构名 *结构指针变量;

例如,在本章的定义的minneed结构,可以使用以下的语句定义一个指向minneed结构类型的指针变量pneed。

struct minneed *pneed;

当然也可在定义minneed结构的同时定义指针变量pneed。

与上一章介绍的各类指针变量相同,结构指针变量也必须先赋值,然后才能使用。赋值就是把结构变量的首地址赋予该指针变量。

例如:

struct minneed need1;
strucr stu *pneed;
pneed=&need1;

以上语句就是将结构变量need1的首地址保存到指针变量pneed中。

以上语句定义的指针变量pneed指向一个用struct minneed结构类型定义的结构变量,而不是指向minneed结构。因为minneed结构只是定义了一个数据的逻辑组合形式,而编译器并未为其分配内存空间,所以指针不能指向结构的定义,而必须指向通过结构类型定义的变量,因为编译器为变量分配了空间。例如:

pneed=&minneed;

以上语句是错误的,因为结构类型minneed是没有内存地址的。

10.5.2 使用结构指针

有了结构指针变量,就能更方便的访问结构变量的各个成员。可以使用以下形式访问结构指针变量所指向的结构变量的成员:

(*结构指针变量名)).成员名;

例如,对于前面定义的指针变量pneed,可以使用以下形式访问结构变量need1的成员name。

(*pneed).name;

就相当于:

*(pneed.name);

其意义为从pneed结构变量的成员name保存的地址中获取数据。如果成员name保存的不是一个地址,则执行以上表达式将会发生错误。

C语言还提供了一个专门指向结构成员的运算符(->),该运算符由两个字符组成,但在C语言中将其作为一个运算符处理。其使用形式如下:

pneed->name;

这样,对于结构变量need1,就有了以下3种方式可以访问到成员name:

need1.name;
(*pneed).name;
pneed->name;

第1种方式是直接使用结构变量的句点运算符访问成员;第2种和第3种方式是使用指向结构变量need1的指针变量访问结构的成员。

10.5.3 用指针处理结构数组

结构指针可以指向结构变量,当然也可以指向结构数组。与指针指向其他类型的数组一样的,用指针可以方便的遍历数组中的每一个元素。

可以会讲结构数组的第1个元素的地址赋给结构指针变量,因为数组名就表示数组的首地址,所以也可将数组名赋给结构指针变量。例如,在程序中有以下代码:

struct minneed needs[N];
struct minneed *pneed;

则可以使用以下代码将结构数组的首地址保存到结构指针变量pneed中:

pneed=needs;

以下形式等价:

pneed=&needs[0];

当结构指针变量pneed指向结构数组后,就可以使用该指针变量处理结构数组中的一个元素。如果要访问结构数组中的下一个元素,则可以使用指针变量pneed自增1。

pneed++;

与指向其他数据类型的指针变量相同,当指针变量自增1时,相当于执行以下的语句:

pneed=pneed+sizeof(minneed);

下面的程序通过结构指针变量结构数组,将结构数组中的各元素的值输出到屏幕上:

#include<stdio.h>
#define N 4

struct minneed
{
    char *name;
    char sex;
    unsigned short age;
    float amount;
};


int main()
{
    struct minneed needs[N]=
    {
        {"zhangjun",1,55,150.0},
        {"wumei",0,48,120.0},
        {"duli",0,57,280.0},
        {"liping",1,56,234.0}
    };
    struct minneed *pneed;
    pneed=needs;
    int i;
    
    for(i=0;i<N;i++)
    {
        printf("\n姓名:%s\n",pneed->name);
        printf("性别:%s\n",(pneed->sex==1)?"男":"女");
        printf("年龄:%d\n",pneed->age);
        printf("地址:%.2f\n",pneed->amount);
        pneed++;
    }
    
    return 0;
}

10.6 向函数传递结构

结构作为一种结构数据类型,每定义一种结构就相当于新增了一种数据类型。通过该数据类型可以定义相应的变量,在函数之间也可传递结构变量。向函数传递结构变量有两种方式:一是传递结构变量的值,二是传递结构指针。

10.6.1 传递结构变量的值到函数

可以将结构变量的形参设置为已经定义的结构类型。例如,以下的函数头定义了一个形参need1,其数据类型为struct minneed。

void outdata(struct minneed need1);

在函数outdata()中,可以通过形参need1引用结构minneed中的成员。而调用该函数的实参必须是一个struct minneed结构类型的数据或变量。

例如,以下程序将调用outdata函数输出minneed结构成员的数据。

#include<stdio.h>
#define N 4

struct minneed
{
    char *name;
    char sex;
    unsigned short age;
    float amount;
};

void outdata(struct minneed);

int main()
{
    struct minneed needs[4]={
        
        {"zhangjun",1,55,150.0},
        {"wumei",0,48,120.0},
        {"duli",0,57,280.0},
        {"liping",1,56,234.0}
    };
    int i;
    
    for(i=0;i<N;i++)
        outdata(needs[i]);
    
    return 0;
}

void outdata(struct minneed need1)
{
    printf("姓名:%s\n", need1.name);
    printf("性别:%s\n", (need1.sex == 1) ? "男" : "女");
    printf("年龄:%d\n", need1.age);
    printf("低保金额:%.2f\n", need1.amount);
    printf("\n");
}

函数传递过程为传值过程,即将实参结构变量中的各成员对应复制到形参中。

10.6.2 传递结构指针到函数。

在上面的这个程序中,函数outdata()的形参是一个struct minneed类型的变量。编译器将为函数outdata()形参need1分配存储空间,在调用函数时将实参中的数据复制到形参内存空间当中。

但更多的时候是减法指向结构变量的指针变量作为参数传递到函数中。这样,调用函数时只需要将指针变量传递到函数即可。同时,对结构中的数据进行修改的结果也会返回到调用函数中。

下面的代码改写上面的程序中的outdata()函数,使用指针的范式来传递结构变量:

void outdata(struct minneed *pneed)
{
    printf("姓名:%s\n", pneed->name);
    printf("性别:%s\n", (pneed->sex== 1) ? "男" : "女");
    printf("年龄:%d\n", pneed->age);
    printf("低保金额:%.2f\n", pneed->amount);
    printf("\n");
}

修改上面程序的主调函数。在主调函数中,首先定义一个结构指针变量,使其指向结构数组,然后使用循环逐个数出数组中的元素。这部分代码如下:

struct minneed *pneed;
pneed=needs;
for(i=0;i<N;i++)
    outdata(pneed++);

10.7 联合

联合是一种类似于结构的构造数据类型。和结构一样,首先定义联合的数据类型,然后再用这个新建的数据类型定义变量,即可访问其成员。本节介绍联合的定义和使用。

10.7.1 结构和联合的区别

虽然结构和联合有一定的相似之处,但是它们有着最本质的区别。在结构中各成员有着各自的内存空间,除空结构外,各个成员的长度之和就是一个结构变量的总长度。而在联合中,各成员享有一段内存空间,一个联合变量的长度等于各个成员中最长的长度。应该说明的是这里所谓的共享不是指把多个成员同时装入一个联合变量内,而是指该联合变量可被赋予任意成员的值,但是每次只能赋一种值,赋入新值则冲去旧值。就向结构和联合都是一个房子,里面住着各种各样的成员,但是结构这个房子里的每个成员都有自己独立的房间,而联合这个房子的全部成员挤在一个房间里。

下面通过一个实例加深对联合的理解:

#include<stdio.h>

int main()
{
    union number
    {
        int i;
        struct
        {
            char first;
            char second;
        }half;
    }num;
    num.i=0x4241;
    printf("%c %c\n",num.half.first,num.half.second);
    num.half.first='a';
    num.half.first='b';
    printf("%x\n",num.i);
}

运行以上的程序,从它的结果分析,当给i赋值后,其低八位也就是first和second的值,当给first和second赋字符后,这两个字符的ASCII码也将作为i的低八位和高八位。

10.7.2 定义联合类型

定义联合类型的形式与定义结构类型相似,其结构如下:

union 联合名
{
    类型说明符 成员名1;
    类型说明符 成员名2;
    ...
    类型说明符 成员名n;
}

联合的成员表中含有若干成员,成员名的命名应符合标识符的规定。例如,以下代码定义了一个联合类型:

union u_tag
{
    char cval;
    int ival;
    float fval;
};

定义了联合类型后,就可以使用该数据类型定义变量。例如:

union u_tag u;

**注意:**在结构类型中,每个成员占用的独立的内存空间。于此不同,联合类型中所有成员使用同一片内存空间。编译程序将按照联合中需要占用内存最大的那个成员分配内存空间。

10.7.3 使用联合变量

定义联合变量的方式与定义结构变量的方式相同,也有3种形式:在定义联合类型的同时定义变量;先定义联合类型,在使用该类型定义变量;在定义联合类型的同时定义变量,而不给联合类型设置名称。

例如:

union u_tag
{
    char cval;
    int ival;
    float fval;
}u;

上述代码在定义联合类型u_tag各成员的同时定义了一个联合类型的变量u。

也可以先定义联合类型u_tag,然后在使用以下形式定义变量u。

union u u_tag;

如果某个联合类型只使用一次,则也可以不给联合类型设置名称,而直接为其定义变量名,如下代码;

union 
{
    char cval;
    int ival;
    float fval;
}u;

定义联合类型的变量后,即可向该变量的成员赋值。与结构变量的赋值相同,可以使用句点运算符访问联合中的成员。例如:

u.cval='w';

与结构不同,一次只能给联合变量的一个成员赋值,当向联合变量的另一个成员赋值时,其原有的值将被覆盖。例如,向变量u的成员ival赋值。

u.ival=10;

**注意:**变量u中的成员ival原来保存的值将会被覆盖,因为联合类型中的3个成员共用4字节的内存空间,只要为一个赋值,就将覆盖其他的值。

10.7.4 在结构嵌套联合类型

在单独使用联合类型时,需要程序员记住联合变量中当前是哪个成员保存的值,然后按该成员的数据类型进行处理。如果该程序中将保存为一种成员的数据用另一个成员名进行输出,则将得到一个无意义的数据。例如:

#include<stdio.h>

union u_tag()
{
    char cval;
    int ival;
    float fval;
};

int main()
{
    union u_tag u;
    
    u.cval='w';
    printf("u.cval=%c\n",u.cval);
    printf("u.fval=%f\n",u.fval);
    printf("u.ival=%d\n",u.ival);
    
    return 0;
}

在以上程序中,第3-8行定义了一个名为u_tag的联合类型,第12行以该联合类型定义变量u,第14行为变量u的成员cval赋初值为字符w,第15行输出联合变量u的的成员cval1的值,将得到真实值,第16行输出联合变量u的成员fval的值,将输出一个不正确的结果,第17行输出联合变量u的成员ival的值,将输出字符w的ASCII码的值119,这只是一个特例,若成员cval为其他类型,则第17行输出的结果也是无意义的值。

像以上这样,需要用户一直记住联合类型中哪个成员保存着数据,以免导致出错。这时,可以将联合类型定义在结构中,通过结果中的式成员来记录联合类型中的保存的数据的类型。下面的程序就是演示这种使用方法。

#include<stdio.h>

struct dtype()
{
    char type;
    union
    {
        char cval;
        int ival;
        float fval;
    }u;
};

void outdata(struct dtype);

int main()
{
    struct dtype test;
    
    test.type='c';
    test.u.cval='w';
    outdata(test);
    
    test.type='i';
    test.u.ival=88;
    outdata(test);
    
    test.type='f';
    test.u.fval=6.18;
    outdata(test);
    
    return 0;
}

void outdata(struct dtype sd)
{
    switch(sd.type)
    {
        case 'c':
            printf("sd.u.cval=%c\n",sd.u.cval);
            break;
        case 'i';
            printf("sd.u.ival=%d\n",sd.u.ival);
            break;
        case 'f';
            printf("sd.u.fval=%.2f\n",sd.u.fval);
            break;
        default:
            printf("数据类型错误!");
    }
}

10.7.5 联合数组

联合变量与结构变量相似,也可以定义数组。例如:

union day
{
    char c;
    int i;
    float j;
}t[5];

以上代码定义了一个联合数组,可以使用相同的的赋值方法为联合数组赋值。

下面来看一个例子。设有一个教室与学生通用的表格,教师数据有姓名、年龄、职业、教研室等4项。学生数据有姓名、年龄、职业、班级4项。实例代码如下:

char c;

main()
{
    struct
    {
        char name[10];
        int age;
        char job;
        union
        {
            int class;
            char office[10];
        }depa;
    }body[2];
    int n,i;
    for(i=0;i<2;i++)
    {
        printf("input name.age,job and department\n");
        scanf("%s %d %c\n",body[i].name,&body[i].age,&body[i].job);
        if(body[i].job=='s')
            scanf("%d",&body[i].depa.class);
        else
            scanf("%s",&body[i].depa.office);
    }
    printf("name\tage job class/office\n");
    for(i=0;i<2;i++)
    {
        if(body[i].job=='s')
            printf("%s\t%3d%3c%d\n",body[i].name,&body[i].age,&body[i].job,&body[i].depa.class);
        else
            printf("%s\t%3d%3c%s\n",body[i].name,&body[i].age,&body[i].job,&body[i].depa.office);
    }
}

本程序用一个结构数组body来存放人员数据,该结构共有4个成员。其中成员项depa是一个联合类型,其又由两个成员组成,一个为整型量class,另一个为字符数组office。在程序的第1个for语句,输入人员的各项数据,先输入结构的前3个成员项name、age、job,然后判别job成员项,如为s则对联合depa.class输入班级编号,否则输入教研组名。

**注意:**在用scanf语句输入时要注意,凡是数组类型的成员,无论是结构成员还是联合成员,在该项前不能再加取地址运算符&。

10.7.6 联合指针

联合指针的定义格式如下:

union day
{
    char c;
    int i;
    float j;
}*联合指针变量名;

使用联合指针变量引用联合体的成员,引用格式如下:

(*联合指针变量).成员名
联合指针变量名->成员名

例如:

(*minner).name
minner->name

联合指针的使用方式和结构指针的使用方式几乎一样,因此不再多介绍。

10.8 枚举

枚举是一个被命名的整型常数的集合。枚举在日常生活中很常见,比如表示星期的sunday,monday,tuesday,wednesday,thursday,friday,saturday就可以表示枚举类型。

10.8.1 定义枚举类型

枚举也是一种用户自定义的数据类型,但不是构造数据类型,因为它不能再分解为任何基本数据类型。

与定义结构或联合类似,枚举类型也需要先定义,再用来定义相应的变量。定义枚举类型的形式如下:

enum 枚举名
{
    标识符[=整型常数],
    标识符[=整型常数],
    ...
    标识符[=整型常数],
}枚举变量;

在定义枚举类型时,如果没有对标识符进行初始,即声调"=整型常数"部分,则默认第一个标识符的值将为0,然后给每个标识符赋值为1,2,3…

例如:

enum weekday
{
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
};

在以上的枚举类型中,标识符SUNDAY的值为0,MONDAY的值为1,SATURDAY的值为6,编译器将会自动按照依次加1的规则为每个标识符设置值。

在定义枚举类型时,也可以指定有间隔的值。例如:

enum color
{
    BLACK;
    RED=4,
    YELLOW=14,
    WHITE
};

在以上的枚举类型中BLACK为设置值,排列在第1个位置,因此其值为0,RED的值为4,YELLOW的值为14,WHITE的值则为15(为设置值,在上一个标识符YELLOW的基础上加1)。

**提示:**另外,还可以将标识符设置为负数,其后续标识符仍按照一次加1的规则排列。

10.8.2 使用枚举变量

定义枚举变量后,就可以使用该类型来定义变量了。与结构和联合相同,也可以在定义枚举类型的同时定义枚举变量。

可以使用以下方式来定义枚举变量:

enum weekday wk;

以上方式定义枚举变量后,该变量就只能被赋值枚举类型中列出的值。例如:

wk=FRIDAY;

若需要将一个整数值保存到枚举变量中,则必须使用强制类型转换。例如:

wk=(enum weekday)5;

**提示:**在有的编译中,可以直接的将整数值保存到枚举变量中,而编译器不会提示错误。

在使用枚举变量时需要注意,枚举值是常量,而不是变量,不能在程序中用赋值语句再对它赋值。例如,对枚举weekday的元素再进行以下的赋值:

SUNDAY=7;

上述语句准备将标识符SUNDAY的值改为7,这是错误的。

10.8.3 枚举类型变量的赋值

  • 首先要了解的就是枚举元素为常量,不能赋值,从起始位置开始值为0,1,2,…。例如:
enum color(red,blue,green,white,black);
enum color c;
c=green;
printf("%d\n",c)

上述程序的输出结果为2,。因为green在枚举变量的第3个位置,所以其默认值为2。

  • 枚举变量为常量,但可以在定义时为其赋值。例如:
enum color(red,blue,green,white,black);
red=5;
green=0;
  • 枚举常量也可以用来比较。例如:
if(c>green)...
if(c<red)...

之所以有采用这样的比较方法,是因为每个枚举变量都拥有一个默认的值。

  • 枚举变量不能直接赋值,但可以强制转换进行赋值。例如:
c=4;

以上代码是错误的。

c=(enum color)4;

以上代码代表将数字强制转换再进行赋值,是正确的。

10.9 使用typedef

C语言提供了一种称为typedef的机制,允许由用户为数据类型取别名。有了这个机制,我们便成为了我们创造的数据类型的“父母”,有了给它起名字的资格。其语法格式如下:

typedef 原数据类型说明 新数据类型说明;

例如,关键字int表示整数类型,可以通过以下的语句为int数据设置一个别名。

typedef int INTEGER;

以后就可以用INTEGER来代替int表示整数类型变量的类型声明了。

例如:

INTEGER i,j;

与下面的等价:

int i,j;

**提示:**使用typedef定义结构、联合等类型,不仅可以减少程序代码量,而且使程序的意义更加明确,因而增加了可读性。

例如:

typedef struct minneed
{
    char *name;
    char sex;
    unsigned short age;
    float amount;
}MINNEED;

在上面定义结构使用了typedef关键字,在结构体的结束的大括号后面跟上一个大写的MINNEED的标识符,即将结构的定义设置为一个新的别名。以后可以使用以下的形式定义结构变量。

MINNEED needs[10];

与下面的语句等价:

struct minneed needs[10];

还可以使用以下方式为数组设置一个别名:

typedef char ARR[10];

以上代码表示ARR是字符数组类型,数组长度为10,然后可以用ARR说明变量,如以下代码:

ARR s1;

等价于以下语句:

char s1[10];

同样使用以下语句:

typedef char *STRING;

表示用STRING代替char *,然后可以使用STRING定义字符指针,例如:

STRING ps1,ps2;

与下面的语句等价:

char *ps1,*ps2;

在使用typedef为数据类型取别名的时候,一般新类型用大写表示,以便于区分。

**注意:**使用typedef语句并不会创建一个新的类型,仅仅是对现有类型设置的一个新的名字。

使用typedef语句为数据类型设置新的名称,可以方便程序的移植。在头文件中使用typedef定义与机器有关的数据类型,当移植程序时只需要改变typedef的定义,就可以使程序适应新的计算机特性。如在不同的字长的计算机中,int类型所占的字节数不同,就可以通过typedef来定义。打开C系统的头文件,可以看到许多的typedef语句。

10.10 拓展训练

10.10.1 训练一:统计并输出学生和老师的信息

现在假设有若干人的信息要记录,其中包括学生和老师。学生的信息包括姓名、性别、编号、职业、平均成绩,老师的信息包括姓名、性别、编号、职业、任课科目。编写一个程序,从键盘输入数据,并将结果输出至屏幕。

**提示:**学生与老师要记录的信息只有平均成绩和任课科目不同,因此可以将其定义为联合类型,其余的用结构体来表示。

#include<stdio.h>

struct
{
    char name[10];
    char sex;
    char num[20];
    char job;
    union
    {
        float score;
        float course[10];
    }a;
}s[3];

void main()
{
    int i;
    for (i = 0; i < 3; i++)
    {
        scanf("%s %c %s %c\n", &s[i].name, &s[i].sex, &s[i].num, &s[i].job);
        if(s[i].job == 's')
            scanf("%f", &s[i].a.score);
        else
            scanf("%s", &s[i].a.course);
    }
    for (i = 0; i < 3; i++)
    {
        if (s[i].job == 's')
            printf("%s %c %s %c %f", s[i].name, s[i].sex, s[i].num, s[i].job, s[i].a.score);
        else
            rintf("%s %c %s %c %f", s[i].name, s[i].sex, s[i].num, s[i].job, s[i].a.course);
    }
}

10.10.2 训练二:结构变量地址作为函数参数实例

编写一个程序,将结构变量地址作为实参传递给其他函数。

**提示:**将结构变量地址传递给函数时传址调用,因此形参值的变化会影响实参的值。

#include<stdio.h>
#include<stdlib.h>

struct st
{
    char name[10];
    char age[5];
    char sex[6];
    float score;
};
void out(struct st stu)
{
    printf("姓名:%s\n", stu.name);
    printf("性别:%s\n", stu.sex);
    printf("年龄:%s\n", stu.age);
    printf("分数:%.1f\n", stu.score);
}
void in(struct st *stu)
{
    char score[10];
    printf("请输入名字:");
    gets(stu->name);
    printf("请输入年龄:");
    gets(stu->age);
    printf("请输入性别:");
    gets(stu->sex);
    printf("请输入分数:");
    gets(score);
    stu->score = atof(score);
}

void main()
{
    struct st s = { "Li Ming","20","man",87.5 };
    out(s);
    in(&s);
    out(s);
}

10.10.3 候选人票数统计

现在有3个候选人,10个人进行投票,统计每一个候选人的票数。

**提示:**可以将这3个候选人定义为结构类型,10个人进行投票,将每个人的投票与结构体中的名字进行比较,若相等则其票数加1,最后输出3个候选人的票数。

#include<stdio.h>
#include<string.h>
struct 
{
    char name[10];
    int c;
}p[3]={"zhang","0","li","0","hu","0"};

void main()
{
    int i,j;
    char n[10];
    for(i=0;i<10;i++)
    {
        scanf("%s",n);
        for(j=0;j<3;j++)
            if(strcmp(n,p[j]).name==0)
                p[j].c++;
    }
    printf("最终结果:\n");
    for(j=0;j<3;j++)
        printf("%s: %d票\n",p[j].name,p[j].c);
}

10.11 技术解惑

10.11.1 结构变量地址作为函数参数

将结构变量地址作为参数传递给函数,则形参应为相同类型的指针。这种方式为传址方式,因此函数内结构变量值的变化也会影响实参。

通过这样的操作,我们在调用函数的时候就像拎了一个袋子,我们修改函数的值便能通过指针带回数据。

**提示:**在C语言中,所有的变量都存储在内存中,可以通过取地址运算符获得变量的内存地址。在进行函数调用时,只需要在普通变量前加上取地址运算符,就可以达到传递变量地址的目的。

例如:先定义一个结构类型。

struct Yearmonthday
{
    int year,month,day;
};

再定义相关的函数,参数类型指定为结构指针。

void input(struct Yearmonthday *p1)
{
    printf("请输入日月年:");
    scanf("%d %d %d",&p1->year,&p1->month,&p1->day);
}

最后在主函数中定义一个结构变量,调用相关的函数,传递变量地址。

int main()
{
    struct Yearmonthday ymd;
    input(&ymd);
    printf("data=%d/%d/%d\n",ymd.year,ymd.month,ymd.day);
}

这时候就可以将结构变量地址作为函数形参,以便将指向结构变量的指针传递给它。

10.11.2 用户自定义类型

C语言中可以用关键字typedef来为已有类型重新定义名称,其定义形式如下:

typedef 类型名 标识符;

其中,类型名可以为C语言中的任意类型,标识名为用户自己定义的名字。例如:

typedef int INTEGER;

上述程序用INTERGER表示int类型。用typedef重新定义类型之后,即可使用该标识符来定义相应类型的变量,例如:

INTERGER x;
int x;

上述两种定义变量的方式是等价的,都表示定义一个整型变量x。

下面用一个例子来了解用户自定义类型的应用:

#include<stdio.h>
typedef union
{
    int a[5];
    float b[5];
    char c[5];
}N5;

void main()
{
    N5 x;
    printf("联合体所占的控件:%d\n",sizeof(x));
}

r cval;
int ival;
float fval;
}u;


定义联合类型的变量后,即可向该变量的成员赋值。与结构变量的赋值相同,可以使用句点运算符访问联合中的成员。例如:

```c
u.cval='w';

与结构不同,一次只能给联合变量的一个成员赋值,当向联合变量的另一个成员赋值时,其原有的值将被覆盖。例如,向变量u的成员ival赋值。

u.ival=10;

**注意:**变量u中的成员ival原来保存的值将会被覆盖,因为联合类型中的3个成员共用4字节的内存空间,只要为一个赋值,就将覆盖其他的值。

10.7.4 在结构嵌套联合类型

在单独使用联合类型时,需要程序员记住联合变量中当前是哪个成员保存的值,然后按该成员的数据类型进行处理。如果该程序中将保存为一种成员的数据用另一个成员名进行输出,则将得到一个无意义的数据。例如:

#include<stdio.h>

union u_tag()
{
    char cval;
    int ival;
    float fval;
};

int main()
{
    union u_tag u;
    
    u.cval='w';
    printf("u.cval=%c\n",u.cval);
    printf("u.fval=%f\n",u.fval);
    printf("u.ival=%d\n",u.ival);
    
    return 0;
}

在以上程序中,第3-8行定义了一个名为u_tag的联合类型,第12行以该联合类型定义变量u,第14行为变量u的成员cval赋初值为字符w,第15行输出联合变量u的的成员cval1的值,将得到真实值,第16行输出联合变量u的成员fval的值,将输出一个不正确的结果,第17行输出联合变量u的成员ival的值,将输出字符w的ASCII码的值119,这只是一个特例,若成员cval为其他类型,则第17行输出的结果也是无意义的值。

像以上这样,需要用户一直记住联合类型中哪个成员保存着数据,以免导致出错。这时,可以将联合类型定义在结构中,通过结果中的式成员来记录联合类型中的保存的数据的类型。下面的程序就是演示这种使用方法。

#include<stdio.h>

struct dtype()
{
    char type;
    union
    {
        char cval;
        int ival;
        float fval;
    }u;
};

void outdata(struct dtype);

int main()
{
    struct dtype test;
    
    test.type='c';
    test.u.cval='w';
    outdata(test);
    
    test.type='i';
    test.u.ival=88;
    outdata(test);
    
    test.type='f';
    test.u.fval=6.18;
    outdata(test);
    
    return 0;
}

void outdata(struct dtype sd)
{
    switch(sd.type)
    {
        case 'c':
            printf("sd.u.cval=%c\n",sd.u.cval);
            break;
        case 'i';
            printf("sd.u.ival=%d\n",sd.u.ival);
            break;
        case 'f';
            printf("sd.u.fval=%.2f\n",sd.u.fval);
            break;
        default:
            printf("数据类型错误!");
    }
}

10.7.5 联合数组

联合变量与结构变量相似,也可以定义数组。例如:

union day
{
    char c;
    int i;
    float j;
}t[5];

以上代码定义了一个联合数组,可以使用相同的的赋值方法为联合数组赋值。

下面来看一个例子。设有一个教室与学生通用的表格,教师数据有姓名、年龄、职业、教研室等4项。学生数据有姓名、年龄、职业、班级4项。实例代码如下:

char c;

main()
{
    struct
    {
        char name[10];
        int age;
        char job;
        union
        {
            int class;
            char office[10];
        }depa;
    }body[2];
    int n,i;
    for(i=0;i<2;i++)
    {
        printf("input name.age,job and department\n");
        scanf("%s %d %c\n",body[i].name,&body[i].age,&body[i].job);
        if(body[i].job=='s')
            scanf("%d",&body[i].depa.class);
        else
            scanf("%s",&body[i].depa.office);
    }
    printf("name\tage job class/office\n");
    for(i=0;i<2;i++)
    {
        if(body[i].job=='s')
            printf("%s\t%3d%3c%d\n",body[i].name,&body[i].age,&body[i].job,&body[i].depa.class);
        else
            printf("%s\t%3d%3c%s\n",body[i].name,&body[i].age,&body[i].job,&body[i].depa.office);
    }
}

本程序用一个结构数组body来存放人员数据,该结构共有4个成员。其中成员项depa是一个联合类型,其又由两个成员组成,一个为整型量class,另一个为字符数组office。在程序的第1个for语句,输入人员的各项数据,先输入结构的前3个成员项name、age、job,然后判别job成员项,如为s则对联合depa.class输入班级编号,否则输入教研组名。

**注意:**在用scanf语句输入时要注意,凡是数组类型的成员,无论是结构成员还是联合成员,在该项前不能再加取地址运算符&。

10.7.6 联合指针

联合指针的定义格式如下:

union day
{
    char c;
    int i;
    float j;
}*联合指针变量名;

使用联合指针变量引用联合体的成员,引用格式如下:

(*联合指针变量).成员名
联合指针变量名->成员名

例如:

(*minner).name
minner->name

联合指针的使用方式和结构指针的使用方式几乎一样,因此不再多介绍。

10.8 枚举

枚举是一个被命名的整型常数的集合。枚举在日常生活中很常见,比如表示星期的sunday,monday,tuesday,wednesday,thursday,friday,saturday就可以表示枚举类型。

10.8.1 定义枚举类型

枚举也是一种用户自定义的数据类型,但不是构造数据类型,因为它不能再分解为任何基本数据类型。

与定义结构或联合类似,枚举类型也需要先定义,再用来定义相应的变量。定义枚举类型的形式如下:

enum 枚举名
{
    标识符[=整型常数],
    标识符[=整型常数],
    ...
    标识符[=整型常数],
}枚举变量;

在定义枚举类型时,如果没有对标识符进行初始,即声调"=整型常数"部分,则默认第一个标识符的值将为0,然后给每个标识符赋值为1,2,3…

例如:

enum weekday
{
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
};

在以上的枚举类型中,标识符SUNDAY的值为0,MONDAY的值为1,SATURDAY的值为6,编译器将会自动按照依次加1的规则为每个标识符设置值。

在定义枚举类型时,也可以指定有间隔的值。例如:

enum color
{
    BLACK;
    RED=4,
    YELLOW=14,
    WHITE
};

在以上的枚举类型中BLACK为设置值,排列在第1个位置,因此其值为0,RED的值为4,YELLOW的值为14,WHITE的值则为15(为设置值,在上一个标识符YELLOW的基础上加1)。

**提示:**另外,还可以将标识符设置为负数,其后续标识符仍按照一次加1的规则排列。

10.8.2 使用枚举变量

定义枚举变量后,就可以使用该类型来定义变量了。与结构和联合相同,也可以在定义枚举类型的同时定义枚举变量。

可以使用以下方式来定义枚举变量:

enum weekday wk;

以上方式定义枚举变量后,该变量就只能被赋值枚举类型中列出的值。例如:

wk=FRIDAY;

若需要将一个整数值保存到枚举变量中,则必须使用强制类型转换。例如:

wk=(enum weekday)5;

**提示:**在有的编译中,可以直接的将整数值保存到枚举变量中,而编译器不会提示错误。

在使用枚举变量时需要注意,枚举值是常量,而不是变量,不能在程序中用赋值语句再对它赋值。例如,对枚举weekday的元素再进行以下的赋值:

SUNDAY=7;

上述语句准备将标识符SUNDAY的值改为7,这是错误的。

10.8.3 枚举类型变量的赋值

  • 首先要了解的就是枚举元素为常量,不能赋值,从起始位置开始值为0,1,2,…。例如:
enum color(red,blue,green,white,black);
enum color c;
c=green;
printf("%d\n",c)

上述程序的输出结果为2,。因为green在枚举变量的第3个位置,所以其默认值为2。

  • 枚举变量为常量,但可以在定义时为其赋值。例如:
enum color(red,blue,green,white,black);
red=5;
green=0;
  • 枚举常量也可以用来比较。例如:
if(c>green)...
if(c<red)...

之所以有采用这样的比较方法,是因为每个枚举变量都拥有一个默认的值。

  • 枚举变量不能直接赋值,但可以强制转换进行赋值。例如:
c=4;

以上代码是错误的。

c=(enum color)4;

以上代码代表将数字强制转换再进行赋值,是正确的。

10.9 使用typedef

C语言提供了一种称为typedef的机制,允许由用户为数据类型取别名。有了这个机制,我们便成为了我们创造的数据类型的“父母”,有了给它起名字的资格。其语法格式如下:

typedef 原数据类型说明 新数据类型说明;

例如,关键字int表示整数类型,可以通过以下的语句为int数据设置一个别名。

typedef int INTEGER;

以后就可以用INTEGER来代替int表示整数类型变量的类型声明了。

例如:

INTEGER i,j;

与下面的等价:

int i,j;

**提示:**使用typedef定义结构、联合等类型,不仅可以减少程序代码量,而且使程序的意义更加明确,因而增加了可读性。

例如:

typedef struct minneed
{
    char *name;
    char sex;
    unsigned short age;
    float amount;
}MINNEED;

在上面定义结构使用了typedef关键字,在结构体的结束的大括号后面跟上一个大写的MINNEED的标识符,即将结构的定义设置为一个新的别名。以后可以使用以下的形式定义结构变量。

MINNEED needs[10];

与下面的语句等价:

struct minneed needs[10];

还可以使用以下方式为数组设置一个别名:

typedef char ARR[10];

以上代码表示ARR是字符数组类型,数组长度为10,然后可以用ARR说明变量,如以下代码:

ARR s1;

等价于以下语句:

char s1[10];

同样使用以下语句:

typedef char *STRING;

表示用STRING代替char *,然后可以使用STRING定义字符指针,例如:

STRING ps1,ps2;

与下面的语句等价:

char *ps1,*ps2;

在使用typedef为数据类型取别名的时候,一般新类型用大写表示,以便于区分。

**注意:**使用typedef语句并不会创建一个新的类型,仅仅是对现有类型设置的一个新的名字。

使用typedef语句为数据类型设置新的名称,可以方便程序的移植。在头文件中使用typedef定义与机器有关的数据类型,当移植程序时只需要改变typedef的定义,就可以使程序适应新的计算机特性。如在不同的字长的计算机中,int类型所占的字节数不同,就可以通过typedef来定义。打开C系统的头文件,可以看到许多的typedef语句。

10.10 拓展训练

10.10.1 训练一:统计并输出学生和老师的信息

现在假设有若干人的信息要记录,其中包括学生和老师。学生的信息包括姓名、性别、编号、职业、平均成绩,老师的信息包括姓名、性别、编号、职业、任课科目。编写一个程序,从键盘输入数据,并将结果输出至屏幕。

**提示:**学生与老师要记录的信息只有平均成绩和任课科目不同,因此可以将其定义为联合类型,其余的用结构体来表示。

#include<stdio.h>

struct
{
    char name[10];
    char sex;
    char num[20];
    char job;
    union
    {
        float score;
        float course[10];
    }a;
}s[3];

void main()
{
    int i;
    for (i = 0; i < 3; i++)
    {
        scanf("%s %c %s %c\n", &s[i].name, &s[i].sex, &s[i].num, &s[i].job);
        if(s[i].job == 's')
            scanf("%f", &s[i].a.score);
        else
            scanf("%s", &s[i].a.course);
    }
    for (i = 0; i < 3; i++)
    {
        if (s[i].job == 's')
            printf("%s %c %s %c %f", s[i].name, s[i].sex, s[i].num, s[i].job, s[i].a.score);
        else
            rintf("%s %c %s %c %f", s[i].name, s[i].sex, s[i].num, s[i].job, s[i].a.course);
    }
}

10.10.2 训练二:结构变量地址作为函数参数实例

编写一个程序,将结构变量地址作为实参传递给其他函数。

**提示:**将结构变量地址传递给函数时传址调用,因此形参值的变化会影响实参的值。

#include<stdio.h>
#include<stdlib.h>

struct st
{
    char name[10];
    char age[5];
    char sex[6];
    float score;
};
void out(struct st stu)
{
    printf("姓名:%s\n", stu.name);
    printf("性别:%s\n", stu.sex);
    printf("年龄:%s\n", stu.age);
    printf("分数:%.1f\n", stu.score);
}
void in(struct st *stu)
{
    char score[10];
    printf("请输入名字:");
    gets(stu->name);
    printf("请输入年龄:");
    gets(stu->age);
    printf("请输入性别:");
    gets(stu->sex);
    printf("请输入分数:");
    gets(score);
    stu->score = atof(score);
}

void main()
{
    struct st s = { "Li Ming","20","man",87.5 };
    out(s);
    in(&s);
    out(s);
}

10.10.3 候选人票数统计

现在有3个候选人,10个人进行投票,统计每一个候选人的票数。

**提示:**可以将这3个候选人定义为结构类型,10个人进行投票,将每个人的投票与结构体中的名字进行比较,若相等则其票数加1,最后输出3个候选人的票数。

#include<stdio.h>
#include<string.h>
struct 
{
    char name[10];
    int c;
}p[3]={"zhang","0","li","0","hu","0"};

void main()
{
    int i,j;
    char n[10];
    for(i=0;i<10;i++)
    {
        scanf("%s",n);
        for(j=0;j<3;j++)
            if(strcmp(n,p[j]).name==0)
                p[j].c++;
    }
    printf("最终结果:\n");
    for(j=0;j<3;j++)
        printf("%s: %d票\n",p[j].name,p[j].c);
}

10.11 技术解惑

10.11.1 结构变量地址作为函数参数

将结构变量地址作为参数传递给函数,则形参应为相同类型的指针。这种方式为传址方式,因此函数内结构变量值的变化也会影响实参。

通过这样的操作,我们在调用函数的时候就像拎了一个袋子,我们修改函数的值便能通过指针带回数据。

**提示:**在C语言中,所有的变量都存储在内存中,可以通过取地址运算符获得变量的内存地址。在进行函数调用时,只需要在普通变量前加上取地址运算符,就可以达到传递变量地址的目的。

例如:先定义一个结构类型。

struct Yearmonthday
{
    int year,month,day;
};

再定义相关的函数,参数类型指定为结构指针。

void input(struct Yearmonthday *p1)
{
    printf("请输入日月年:");
    scanf("%d %d %d",&p1->year,&p1->month,&p1->day);
}

最后在主函数中定义一个结构变量,调用相关的函数,传递变量地址。

int main()
{
    struct Yearmonthday ymd;
    input(&ymd);
    printf("data=%d/%d/%d\n",ymd.year,ymd.month,ymd.day);
}

这时候就可以将结构变量地址作为函数形参,以便将指向结构变量的指针传递给它。

10.11.2 用户自定义类型

C语言中可以用关键字typedef来为已有类型重新定义名称,其定义形式如下:

typedef 类型名 标识符;

其中,类型名可以为C语言中的任意类型,标识名为用户自己定义的名字。例如:

typedef int INTEGER;

上述程序用INTERGER表示int类型。用typedef重新定义类型之后,即可使用该标识符来定义相应类型的变量,例如:

INTERGER x;
int x;

上述两种定义变量的方式是等价的,都表示定义一个整型变量x。

下面用一个例子来了解用户自定义类型的应用:

#include<stdio.h>
typedef union
{
    int a[5];
    float b[5];
    char c[5];
}N5;

void main()
{
    N5 x;
    printf("联合体所占的控件:%d\n",sizeof(x));
}
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-09-18 09:54:07  更:2021-09-18 09:56:41 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 23:14:06-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码