3.3 Linux系统下C语言的进阶编程
C语言进阶编程主要是围绕数组、指针、函数、构造数据类型展开的,这部分内容和上一小节相比,在内容理解方面提升了一点难度,尤其是指针,因此指针是本小节的重点内容,也是学好C语言、用好C语言、理解C语言的关键。进阶编程会配合示例代码讲解,这样理解起来更容易些。
3.3.1 C语言的数组
C语言中数组的命名和标识符命名规则一致,在这里我们需要关注数组的初始化、数组类型、数组的访问方法、一维数组和二维数组。
在创建数组时,我们必须定义数组的类型和大小,数组的大小不能为0,数组中的元素类型都是相同的。
数组的初始化有多种方式,一般数组定义好后就要初始化。
一维数组初始化和赋值操作,具体如下。
二维数组初始化和赋值操作,具体如下。
数组是在内存中连续的一段地址空间,因此在访问数组时常用下标来访问。由于数组名本身就是数组首地址,因此下标总是从零开始,数组名[下标]就是数组对应元素的内存地址。
小白成长之路:使用数组的注意事项
1)数组下标越界编译器不会报错,因为数组的外部内存空间,不确定是否有权限,如果越界访问程序可能奔溃。
2)数组采用下标来访问数组元素,数组下标总是从0开始,到数组元素个数的n-1结束。
3)数组作为函数参数的时候,传递的是地址。
4)字符数组不等于字符串,因为字符串中必须包含‘\ 0’字符串结束转义字符,但是字符数组可以不包含,也可以包含多个。其次,对于相同字符的字符串和字符数组来说,字符串数组初始化要比字符数组多一个字节的存储单元。字符串数组是字符数组的一个子集。
3.3.2 C语言的指针
指针是C语言中最为重要的一个数据类型,它也是用来区别C语言和其他编程语言与众不同的地方。指针使用非常灵活方便,能够让编程人员高效便捷地进行编程。尽管指针功能强大,但有时也会让程序员苦恼。原因在于C语言不对指针操作进行限制。比如定义一个整型变量,那么这个变量在初始化阶段就在内存中占据了一个固定的位置,然而每个变量的内存位置都是有CPU地址总线唯一寻址得到的,如果想访问这个变量,一是可以直接使用声明好的变量名来访问,二是可以使用相同类型的指针指向它,然后使用该指针来访问变量。变量的地址就相当于人们的身份证,是唯一的。指针只是地址的一个别名,所以指针就是地址。
1.指针的定义
指针就是一个用来存储地址的变量,指针里面储存的不是内容,是存放的内存地址。在讨论指针之前,需要先讨论一下计算机的内存地址,我们知道计算机内存地址是一个连续的存储空间,这里暂且比作有一排100间的房子,假定一个变量就占用一个房间,给这100个房间从0~99编号,这里定义房间钥匙对应内存地址。首先声明一个变量,如int p=3,这个时候假设系统给p这个变量分配第0号房间。然后再定一个指针类型的变量int∗q,假设系统给指针变量q分配房间号为第23号,如果执行q=&p操作,这时我们在第23号房间里能够发现0号房间的钥匙,这样q指针变量就能使用0号房间里的东西。
2.指针变量
为了更好地说明指针变量这个概念,我们看一个例子,具体如下。
从上面的例子可以得出指针变量存储的是地址的结论。之所以称为变量,是因为地址的值是可以动态变化的。在定义指针时要声明指针所指向的类型,指针在初始化时规范操作要让指针指向确定的地址空间,防止出现“野指针”导致内存泄漏。
在学习的过程中,我们要搞清指针四方面的内容:指针的类型、指针所指向的类型、指针的值或者叫指针所指向的内存区,以及指针本身所占据的内存区。
3.指针类型
既然指针变量所占内存地址都是一样的(这是由CPU的位数决定),为什么还要在定义指针变量的同时声明基本数据类型呢?因为不同类型的数据在内存中所占的字节数是不同的,比如int型数据占4字节、char型数据占1字节。而每个字节都有一个地址,比如一个int型数据占4字节,就有4个地址。指针变量所指向的是这4个地址中的第一个地址,即它里面保存的是其所指向的变量的首地址。通过所指向变量的首地址和该变量的类型,就能知道该变量的所有信息。
从语法的角度看,只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型,让我们看看下面几个例子中各个指针的类型。
4.指针指向类型
当我们通过指针来访问指针所指向的内存时,指针所指向的类型决定了编译器将把那段内存区里的内容当做什么来看待。从语法上看,只需把指针声明语句中的指针名字和名字左边的指针声明符∗去掉,剩下的就是指针所指向的类型。
5.指针所指向的内存区
指针的值是指针本身存储的数值,这个值将被编译器当成一个地址,而不是一个一般的数值。在32位程序里,所有类型的指针的值都是一个32位整数,因为32位程序里内存地址全都是32位长。指针所指向的内存区就从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。以后我们说一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。指针所指向的内存区和指针所指向的类型是两个完全不同的概念,示例如下。
6.指针本身所占据的内存区
指针本身占了多大的内存?我们只要用函数sizeof()(指针的类型)测一下就知道了。在32位平台里,指针本身占据了4个字节的长度。结果是一个指针的表达式就是指针表达式,当一个指针表达式有了自身明确占据的内存区就是一个左值(即一个能用于赋值运算左边的表达式)。
小白成长之路:指针数组和数组指针的区别
1)指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身的大小决定,每一个元素都是一个指针,在32位系统下任何类型的指针永远是占4个字节。它是“储存指针的数组”的简称。
2)数组指针:首先它是一个指针,它指向一个数组。在32位系统下任何类型的指针永远是占4个字节,至于它指向的数组占多少字节,具体要看数组大小。它是“指向数组的指针”的简称。
3.3.3 C语言的函数
在学习C语言编程中,无法避免的是学习使用和设计C语言的函数,因为函数是面向过程编程中的基本组成部分。当设计一个程序的时候,都是采用模块化的思想,自顶向下进行分解,最终分解到一个个小的模块。这一个个小的模块可以设计封装成一个函数,比如输出函数printf()。函数的实现则是由一个个语句表达式完成,C语言中引入函数的原因,一是可以将系统进行分解模块化,二是这些函数方便移植使用,不需要每个IT工程师重复地设计相同的函数。
1.函数分类
函数总体上分为两大类,一类是库函数,另一类是用户自定义函数。库函数主要是在进行编程中调用C标准库的函数,比如printf()输出函数、scanf()输入函数等。自定义函数主要是用户自己设计或者是移植三方的函数(非标准库函数)。函数设计的总体思想是“高内聚,低耦合”,这个思想就是在函数设计时尽可能保证在程序功能发生变化时,修改的代码量最少。
2.函数定义
函数必须要先定义后使用,函数定义的一般格式如下。
在函数定义的一般格式中,类型名就是函数的返回值,如果这个函数不准备返回任何数据,那么需要声明void类型(void就是无类型,表示没有返回值)。函数名就是函数的名字,一般我们根据函数实现的功能来命名,比如print_C就是“打印C”的意思,一目了然。参数列表指定了参数的类型和名字,如果这个函数没有参数,那么这个位置直接写上小括号或是使用void关键字声明即可。
函数声明方式很简单,即在函数体定义处复制函数名并在最后加上分号。注意函数声明必须要放在函数使用相关的头文件中,并且要在使用前定义和声明。
小白成长之路:什么是指针函数、函数指针和回调函数
1)指针函数本质是一个函数,只不过返回值为某一类型的指针(地址值)。函数返回值必须用同类型的变量来接受,也就是说,指针函数的返回值必须赋值给同类型的指针变量。
2)函数指针本质是一个指针,只不过这个指针指向一个函数。常见的函数都有其入口,比如main()函数是整个程序的入口。我们调用的其他函数都有其特定的入口,正如可以通过地址找到相应的变量一样,也可以通过地址找到相应的函数。而这个存储函数地址的指针就是函数指针。
3)我们知道,函数指针变量也是一个变量,那么作为变量当然也可以当作参数来使用,回调函数就是函数指针作为某个函数的参数。回调函数是利用函数指针实现的一种调用机制,其调用机制原理如下:调用者不知道具体事件发生时需要调用的具体函数;被调用函数不知道何时被调用,只知道被调用会完成需要的任务;当具体事件发生时,调用者通过函数指针调用具体函数。
3.3.4 C语言的构造数据类型
在实际编程过程中,如果基本数据类型(整型、浮点型、字符型、指针型)无法满足编程需要,就需要设计出一种能够满足编程结构的类型,我们将其称之为构造数据类型。常用的构造数据类型有结构数据类型、共用体数据类型以及枚举数据类型。
1.结构体数据类型
结构体是由几个不同数据类型的元素组合成的复杂数据类型,这些基本的数据类型称为结构体的成员。定义一个结构体需要给出各个成员的类型及名称。结构声明描述了一个结构的组织形式,结构体定义需要声明一个结构体变量。
在结构体类型声明的一般格式中,结构体成员是该结构体类型所包含的变量或数组。成员类型由程序员自己定义,在结构体类型声明中成员变量不需要初始化。结构体类型声明完成后,使用结构体类型和常规的基本数据类型是一样的,因此在结构体初始化之前是不分配内存空间的,具体如下。
下面以一个学生结构体类型为例,介绍结构体的定义和初始化操作。
typedef属于C语言中的关键字,常常用来重定义结构体的类型名。typedef本身是一种存储类的关键字,与auto、extern、static、register等关键字不能出现在同一个表达式中。ty-pedef作用是为一种数据类型定义一个新名字,这里的数据类型包括内部数据类型(int和char等)和自定义的数据类型(struct等)。下面将对比使用typedef重新定义结构体类型名。
小白成长之路:typedef与#define的区别
1)#define一般用来定义常量,也可以定义类型,但是不常用。#define宏定义不仅可以配合#ifdef、#ifndef等进行逻辑判断,还可以使用#undef取消定义。
2)typedef符合(C语言)范围规则,使用typedef定义的变量类型,其作用范围限制在所定义的函数或者文件内(取决于此变量定义的位置),而宏定义则没有这种特性。
2.共用体数据类型
在进行嵌入式系统的某些编程中,需要使几种不同类型的变量存放在同一段内存单元中。这几种变量可以共享这一段内存单元,几个变量可以互相覆盖。这种几个不同的变量共同占用一段内存的结构,在C语言中被称作“共用体”类型结构,简称共用体,也称为联合体。
共用体类型数据的特点如下。
1)同一个内存段可以用来存放几种不同类型的成员,但是在每一瞬间只能存放其中的一种,而不是同时存放几种。换句话说,每一瞬间只有一个成员起作用,其他的成员不起作用,即不是同时都在存在和起作用。
2)共用体变量中起作用的成员是最后一次存放的成员,在存入一个新成员后,原有成员失去作用。
3)共用体变量和它的各成员都是同一地址。
4)不能对共用体变量名赋值,也不能企图引用变量名来得到一个值。
5)共用体类型可以出现在结构体类型的定义中,也可以定义共用体数组。反之,结构体也可以出现在共用体类型的定义中,数组也可以作为共用体的成员。
小白成长之路:结构体和共用体的区别
结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。结构体占用的内存大于等于所有成员占用的内存总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
3.枚举数据类型
enum是C语言中的一个关键字,enum叫枚举数据类型,描述的是一组整型值的集合,相比结构体类型,枚举是常量的集合,不允许枚举变量中有变量。枚举型是预处理指令#de-fine的替代,枚举和宏类似,宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。枚举简单地说也是一种数据类型,只不过这种数据类型只包含自定义的特定数据,是一组有共同特性的数据集合。举个例子,颜色也可以定义成枚举类型,它可以包含我们定义的任何颜色,当需要的时候,只需通过枚举调用即可;又比如季节(春夏秋冬)、星期(星期一到星期日)等具有共同特征的数据,都可以定义枚举,举例如下。
小白成长之路:枚举类型的注意事项
1)在没有显示说明的情况下,默认第一个枚举常量(也就是花括号中的常量名)的值为0,往后每个枚举常量依次递增1。
2)在部分显示说明的情况下,未明确数值的枚举常量将依附前一个有明确数值的枚举常量依次顺序递增。
3)一个整数不能直接赋值给一个枚举变量,必须用该枚举变量所属的枚举类型进行类型强制转换后才能赋值。
4)同一枚举类型中不同的枚举成员可以具有相同的值。
5)同一个程序中不能定义同名的枚举类型,不同的枚举类型中也不能存在同名的枚举成员(枚举常量)。