C语言简明教程(十一)(函数和指针)

接上一节:输入输出和文件
C语言有两种很特别的数据类型:函数类型和指针类型,必须说明,函数以函数类为基础,指针同样以指针类型为基础,不然的从各个角度都能理解方式和指针的话那恐怕很难掌握C语言的核心。以数据类型为基础可以更好理解C语言的函数和指针,这样在其它地方或者linux内核中看到更复杂的写法就能很好看懂,并且也能写了,函数类型和指针类型跟int整型或char字符类型一样,同样有类型限制,不同的是,我们需要问对应的类型创建的变量的数据对象是怎么样的?

C语言简明教程(十一)(函数和指针)

文章图片
如上图,函数是存放在C程序内存中的代码区中的,函数类型的变量存储的数据是函数内部的执行指令,而指针类型的变量一般存放在栈区,存放的是一个地址,下面我们先来解释一下什么是指针。
一、C语言的指针类型严格的说,说指针指的是一个值,如0XFFABC10,这就是一个数值,甚至可以写出十进制的形式:268090384,一般指针由无符号整型unsigned int表示,但是记得指针是一个数值,无符号整数值。
指针值和整数值不同的是类型的不同,指针值是指针类型的,指针类型用于存储内存地址,指针类型的写法是:(类型名 *)如int*是一个整型指针,char*是一个字符型指针,所以你看指针类型是有很多的,既然有类型了,那么声明一个指针类型的变量自然就可以写作:(类型名 *)pointer,例如int *p,char *str;
和其它数据类型一样,指针类型只能存储指针值,指针值就是一个整数值如0XFFABC10,虽然它是一个整数值,但是它是一个指针类型的值和同样值的整型值不同的是类型不同,你可以将一个指针值强制转为int,或者将一个int强制转为指针类型,前提是转换后可以有意义地使用(实际上这种做法还是很有用的,很多人说不能显式给指针赋值是错误的,必须指出你可以给指针变量赋值为一个整数,不要被这个限制了,不然遇到一些奇怪的写法可能就看不懂了,它是一个类型变量,只要符合就能赋值)。
另外,一般来说某类型的指针变量只能存储对应类型数据的地址,例如int *p只能存储int类型数据的地址。& 是地址运算符,& a表示取出a的地址,*是间接运算符,*p表示取出指针p中的数据对象。
下面是指针类型的代码实例:
int a = 15; int *b = &a; // 创建一个指针类型的变量b,并且将b的值初始化为a的 int c = 0x0028FEA4; // int类型的值(这里的地址值取值下面b的输出) printf("%d\n", *b); // 取地址b对应的数据对象 printf("%p\n", b); // 输出地址值,转换说明使用%p printf("%d\t%d\n", (int*)c == b, *((int*)c)); // c是一个整型变量,将其强制转为指针类型就可以进行指针的使用

下面是关于指针变量和普通变量的内存结构图:
C语言简明教程(十一)(函数和指针)

文章图片
因为变量地址是动态改变的,所以一般指针变量不是显式赋值,而是使用一个已存在的变量进行取址赋值。
二、函数类型和指针一样为什么不说函数而说函数类型?它就是一个数据类型,这是为了对待指针一样更全面地理解C语言的函数,它首先得是一个数据类型,然后我们再讨论这个数据类型声明的变量存储什么样的数据,另外函数的功能主要是为了封装某一功能的具体实现,实现模块化开发,同时设计可重用的代码。
关键是函数类型的类型名是如何书写的?首先变量有声明和初始化,函数当然也有,标准的使用函数的方式是先声明函数(即函数原型),然后定义函数实现,声明的方式有两种:
// 声明函数的两种方式 int (get_count)(int, int); // 参数列表中省略参数变量名 int (get_count)(int a, int b); // 参数列表中带有参数变量名

注意以上声明中函数名都使用括号括起来了,为什么?这种写法更清楚一些,平时可以省去,这是为了说明函数名就是函数类型的变量名,而除了函数名的其它部分就是一个函数类型的书写形式即: 返回值 ()(参数列表),也称为函数签名,这种写法有点特殊,但是事实它就是这样的,上面声明的函数类型是int ()(int, int),变量名是get_count,下面是用typedef定义函数类型的别名和函数指针类型的别名:
typedef int (Count)(int, int); // 定义一个函数类型的别名,函数类型为int ()(int, int) typedef int (*PCount)(int, int); // 定义一个函数指针类型的别名Count f; // f函数变量声明

函数有点特别的地方是函数名是一个地址,对函数名取址同样是该函数的地址,这是为了写法上的方便而设计的,两种方式都可以使用。
下面就是常见的函数实现的写法:
// 函数实现定义 int get_count(int a, int b){ return a + b; }// 函数实现定义 int f(int a, int b){ // f函数定义,初始化 return a + b; }

变量必须在使用前先声明,所以函数也必须在调用前先声明,所以如果没有函数原型声明,直接定义函数,视乎该函数在主函数之前还是之后,如果是之后那会报错,如果之前那就是没问题,因为直接定义函数相当于int a = 9同时声明一个变量并且赋值。方便起见,所有要使用的函数一般先声明后实现,并且函数原型全部放在一个头文件中。函数原型声明的主要目的是告诉编译器函数的类型以及声明的函数变量,空参数要写成void,否则编译器可能不能做正确的类型检查。
函数的参数:带不定或变长参数的函数声明方式如下:
void show(char *message, ...); // 带不定参数的函数声明形式

不定参数的实现需要使用到stdarg.h头文件。
C函数参数都是复制值传递的,意思就是函数调用传参的值都是复制一份给函数的,一般的数据变量是复制对应的数据对象,指针同样是复制数据对象,只不过是复制一个简单的地址值,复制地址值消耗内存比复制大数据对象更少,所以C函数参数一般都是使用指针类型的参数,C++中有引用类型,引用类型同样是复制值传递,只不过引用传递复制的是变量名。
【C语言简明教程(十一)(函数和指针)】函数调用:函数调用的时候如果有参数需要传入实际参数,其中有一种调用自身的函数:递归函数,由于是递归调用,所以造成内存消耗大,一般不推荐用,递归调用的一个例子就是实现n的阶乘,不过使用循环实现也更为简单也不用消耗太多内存。

    推荐阅读