首页 > 代码库 > C语言中的函数

C语言中的函数

一、认清函数的真相

1、函数的由来

程序  =  数据  + 算法

C程序 = 数据 + 函数

2、函数的意义

模块化程序设计


C语言中的模块化

3、面向过程的程序设计

# 面向过程是一种以过程为中心的编程思想

# 首先将复杂问题分解为一个个容易解决的问题

# 分解过后的问题可以按照步骤一步步完成

# 函数是面向过程在C语言中的体现

# 解决问题的每一个步骤可以用函数来实现

3、声明和定义

# 程序中的声明可以理解为预先告诉编译器实体的存在,如:变量,函数,等等

# 程序中的定义明确指示编译器实体的意义

声明和定义并不相同!!!

实例:

// global.c
// int g_var = 0; // 定义


#include <stdio.h>


extern int g_var; //声明一个外部变量

void f(int i, int j); //声明一个外部函数

int main()
{
    int g(int x);
    
    g_var = 10;
    
    f(1, 2);
    
    printf("%d\n", g(3));
    
    return 0;
}

void f(int i, int j) // 定义
{
    printf("i + j = %d\n", i + j);
}

int g(int x)
{
    return 2 * x + g_var;
}
4、函数参数

# 函数参数在本质上与局部变量相同,都是在栈上分配空间

# 函数参数的初始值是函数调用时的实参值


实例:
#include <stdio.h>

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

int main()
{
    int k = 1;
    
    f(k, k++);
    
    printf("%d\n", k);
    
    return 0;
}
函数参数的求值顺序依赖于编译器的实现!!!
C语言中大多数运算符对其操作数求值的顺序都是依赖于编译器的实现的!!!
int i = f() * g(); //这里可不要盲目的认为先计算f()再计算g()???
5、程序中的顺序点
# 程序中存在一定的顺序点
# 顺序点指的是执行过程中修改变量值的最晚时刻
# 在程序达到顺序点的时候,之前所做的一切操作必须反映到后续的访问中
# 每个完整表达式结束时
# &&,||, ?:,以及逗号表达式的每个运算对象计算之后
# 函数调用中对所有实际参数的求值完成之后(进入函数体之前)
#include <stdio.h>

int main()
{
    int k = 2;
    int a = 1;
    
    k = k++ + k++;
    
    printf("k = %d\n", k);
    
    if( a-- && a )
    {
        printf("a = %d\n", a);
    }
    
    return 0;
}
函数的缺省认定
# C语言会默认没有类型的函数参数为int
f(i, j)
{
return i + j;
}
等价于
int f(int i, int j)
{
return i + j;
}
小结:
1、C语言是一种面向过程的语言
2、函数可理解为解决问题的步骤
3、函数的实参并没有固定的计算次序
4、顺序点是C语言中变量改变的最晚时机
5、函数定义时参数和返回值得缺省类型为int
三、可变参数分析与宏分析
问题:如何编写一个可以计算n个数平均值的函数?
下面是大家都能想到的方法,其实C语言中还有另外一种实现方法。
#include <stdio.h>

float func(int array[], int size)
{
    int i = 0;
    float avr = 0;
    
    for(i=0; i<size; i++)
    {
        avr += array[i];
    }
    
    return avr / size;
}

int main()
{
    int array[] = {1, 2, 3, 4, 5};
    
    printf("%f\n", func(array, 5));
    
    return 0;
}
1、可变参数
# C语言中可以定义参数可变的函数
# 参数可变函数的实现依赖于stdarg.h头文件
# va_list变量与va_start,va_end和va_arg配合使用能够访问参数值

可变参数的定义与使用实例:
#include <stdio.h>
#include <stdarg.h> //可变参数头文件,一定要加上。

float average(int n, ...)
{
    va_list args; //专用类型,可变列表
    int i = 0;
    float sum = 0;
    
    va_start(args, n);//初始化列表
    
    for(i=0; i<n; i++)
    {
        sum += va_arg(args, int);
    }
    
    va_end(args); //结束列表
    
    return sum / n;
}

int main()
{
    printf("%f\n", average(5, 1, 2, 3, 4, 5));
    printf("%f\n", average(4, 1, 2, 3, 4));
    
    return 0;
}
2、可变参数的限制
# 可变参数必须从头到尾按照顺序逐个访问
# 参数列表中至少要存在一个确定的命名参数
# 可变参数宏无法判断实际存在的参数的数量
# 可变参数宏无法判断参数的实际类型
warning:va_arg如果指定了错误的类型,那么结果是不可预测的。
小结:
# 可变参数是C语言提供的一种函数设计技巧
# 可变参数的函数提供了一种更方便的函数调用方式
# 可变参数必须顺序的访问
# 无法直接访问可变参数列表中间的参数值
3、李逵和李鬼
#include <stdio.h>
#define RESET(p, len) while( len > 0) ((char*)p)[--len] = 0

void reset(void* p, int len)
{
    while( len > 0 ) 
    {
        ((char*)p)[--len] = 0;
    }
}


int main()
{
    int array[] = {1, 2, 3, 4, 5};
    int len = sizeof(array);
    
    reset(array, len);
    RESET(array, len);
    
    return 0;
}
4、函数 VS 宏
# 宏是由预处理直接替换展开的,编译器不知道宏的存在
# 函数是由编译器直接编译的实体,调用行为由编译器决定
# 多次使用宏会导致程序代码量增加
# 函数是跳转执行的,因此代码量不会增加
# 宏的效率比函数要高,因为是直接展开,无调用开销
#函数调用时会创建活动记录,效率不如宏
5、宏的优点和缺点
# 宏的效率比函数稍高,但是其副作用巨大,容易出错
#include <stdio.h>

#define ADD(a, b) a + b
#define MUL(a, b) a * b
#define _MIN_(a, b) ((a) < (b) ? (a) : (b))

int main()
{
    int i = 1;
    int j = 10;
    
    printf("%d\n", MUL(ADD(1, 2), ADD(3, 4)));//宏的副作用
    printf("%d\n", _MIN_(i++, j));
    
    return 0;
}
函数存在实参到形参的传递,因此无任何副作用,但是函数需要建立活动对象,效率受影响
#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

int mul(int a, int b)
{
    return a * b;
}

int _min_(int a, int b)
{
    return a < b ? a : b;
}

int main()
{
    int i = 1;
    int j = 10;
    
    printf("%d\n", mul(add(1, 2), add(3, 4)));
    printf("%d\n", _min_(i++, j));
    
    return 0;
}
宏无可替代的优势
宏参数可以是任何C语言实体
# 宏编写的_MIN_参数类型可以是int,float等等
# 宏的参数可以是类型名
# define MALLOC(type, n) (type*)malloc(n * sizeof(type))
int *p = MALLOC(int, 5);//这是函数完成不了的
小结:
1、宏和函数并不是竞争对手
2、宏能够接受任何类型的参数,效率高,易出错
3、函数的参数必须是固定类型,效率稍低,不易出错
4、宏可以实现函数不能实现的功能
五、函数调用行为
1、活动记录
# 活动记录是函数调用时用于记录一系列相关信息的记录
 临时变量域:用来存放临时变量的值,如k++的中间结果
解释:对于k++,函数是这样来处理的
1、生产临时变量temp
2、将k赋值与temp
3、k = k+1
所以对于f(k, k++)中k++实际上取得是temp的值。
局部变量域:用来存放函数本次执行中的局部变量
机器状态域:用来保存调用函数之前有关机器状态的信息,包括各种寄存器的当前值和返回地址等;
实参数域:用于存放函数的实参信息
返回值域:为调用者函数存放返回值
2、参数入栈
既然函数参数的计算次序是依赖编译器实现的,那么函数参数的入栈次序是如何确定的呢?
那就是根据调用约定:
# 当一个函数被调用时,参数会传递给被调用的函数,而返回值会被返回给调用函数。函数的调用约定就是描述参数是怎么传递到栈空间的,以及栈空间由谁维护。
# 参数传递顺序
从右到左依次入栈:_stdcall, _cdecl, _thiscall
从左到右依次入栈:_pascal, _fastcall
# 调用堆栈清理
调用者清除栈
被调用函数返回后清除栈
所以函数参数的入栈次序,可以有自己确定。还有函数的调用和返回释放都是有编译器编译好代码有调用函数来操作,可以说函数的释放依赖于编译器。
那什么时候会用到调用约定呢?
当我们使用别人给的库函数,当然如果是别人给的源码,那使用同一个编译器进行编译,自然就是是同样的调用约定。而调用别人动态库中的函数时就必须要约定好调用规则了。如果使用了不同的调用规则,就会发生莫名其妙的调用失败,或者会产生荒谬的结果。
别人提供的动态库,有可能不是用C语言编写的,所以这个时候,我们调用动态库时,就要小心定义调用约定了。
小结:
# 函数调用是C语言的核心机制
# 活动记录中保存了函数调用以及返回所需要的一切信息
# 调用约定是调用者和被调用者之间的调用协议,常用于不同开发者编写的库函数之间。
五、函数递归和函数设计技巧
1、递归概述
# 递归是数学领域中概念在程序设计中的应用
# 递归是一种强有力的程序设计方法
# 递归的本质为函数内部在适当的时候调用自身。
# C递归函数有两个主要的组成部分:
递归点-以不同参数调用自身
出口-不在递归调用
f(x) =  1;  (x = 1)   x*f(x-1);(x>1)
利用递归函数求解n!
#include <stdio.h>

int func(int x)
{
    if( x > 1 )
    {
        return x * func(x - 1);
    }
    else
    {
        return 1;
    }
}

int main()
{
    printf("x! = %d\n", func(4));
    
    return 0;
}
递归的一大优点就是它非常符合我们的思考方式。
缺点就是它占用较大的栈空间,如上程序中,如果n较大,可能会产生栈溢出的结果。
小结:
1、C语言中的递归函数必然会使用判断语句
2、递归函数在需要编写的时候定义函数的出口,否则栈会溢出
3、递归函数是一种分而治之的思想
思考:编写一个函数打印一个字符数组的全排列
函数设计技巧
# 不要在函数中使用全局变量,尽量让函数从意义上是一个独立的功能模块
# 参数名要能够体现参数的意义
void str_copy(char *str1, char *str2);
void str_copy(char *str_dest, char *str_src);
# 如果参数是指针,且仅作输入参数用,则应在类型前加const,以防止该指针在函数体内被意外修改
void str_copy(char *str_dest, const char *str_src);
# 不要省略返回值的类型,如果函数没有返回值,那么应声明为void类型
# 在函数体的“入口处”,对参数的有效性进行检查,对指针的检查尤为重要
# 语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时被自动销毁
# 函数体的规模要小,尽量控制在80行代码之内
# 相同的输入应当产生相同的输出,尽量避免函数带有“记忆性”功能
# 避免函数有太多的参数,参数个数尽量控制在4个以内
# 有时候函数不需要返回值,但为了增加灵活性,如支持链式表达,可以附加返回值。链式表达就是指函数的参数使用函数返回值。
char s[54];
int len = strlen(strcpy(s, "android"));
# 函数名与返回值类型在语义上不可冲突
char c;
c = getchar();//getchar的返回值为int型
if(EOF == c)
{
//...
}
六、进军C++的世界
1、C语言已经很强大了,为什么还要有C++?
答:C语言是面向过程的高级语言,但它有一个问题就是不能把生活中的问题直接映射到C语言程序中,所以用C开发应用程序的效率不高。而C++是在C语言的基础上进行了两个加强,一个是类型加强,一个是引进了面向对象的概念,所以C++更容易用来描述现实生活中的问题。
我们认识世界的方式就是通过分类;而C++同样加强了类的概念,之前的C语言中也使用了类,但没有具体提出。
类一直存在,但不一定有对象。C++中的类实际为struct结构的升级版,它在结构体的基础上添加了函数,C++中称为对象行为。
2.初识OOP(Object Oriented Programmingt)面向对象编程
# 类和对象是面向对象中的两个基本概念
# “类”指的是一类事物,是一个抽象的概念
# “对象”指的是属于某个类的一个实体,是一个具体存在的事物
类是一种“模板”,可以通过这种模板创建出不同的对象“实例”
对象“实例”是类“模板”的一个具体实现
一个类可以有很多对象,而一个对象必然属于某个类
3、生活中的类和对象
4、抽象
# 抽象的意义是观察一群“事物”,并认识它们所具有的一些共同特性
# 抽象的本质是忽略不重要的区别,只记录能表现事物特征的关键数据项
# 类是抽象在程序设计领域的概念
# 类用于抽象的描述一类事物所特有的属性和行为
如:电脑类的每个对象都有CPU,内存和硬盘,电脑类的每个对象都可以开机和运行程序
#对象时一个具体的事物,拥有其所属类的所有属性,并且每个属性都是一个特有的值
如:老虎的每个对象(也就是每只老虎),都有不同的体重,不同食量以及不同的性情
5、封装
# 类中描述的事物属性和行为往往是相关的
# 在C++中属性通过变量来表示,行为通过函数来模拟
# 封装指的是类中的变量只能通过类的函数来访问


6、C++中的第一个类展示
#include <stdio.h>

struct Student
{
    const char* name;
    int number;
    void info()
    {
        printf("Name = %s, Number = %d\n", name, number);
    }
};

int main()
{
    Student s;
    
    s.name = "Delphi";
    s.number = 100;
    s.info();
    
    return 0;
}
可以看出它的类就是C中的struct加上了一个函数(代码)
7、C++类中有三种访问权限
public--类的外部可以自由的访问
protected--类自身和子类中可以访问
private--类自身中可以访问

#include <stdio.h>
struct Student
{
protected:
    const char* name;
    int number;
public:
    void set(const char* n, int i)
    {
        name = n;
        number = i;
    }
    
    void info()
    {
        printf("Name = %s, Number = %d\n", name, number);
    }
};

int main()
{
    Student s;
    
    s.set("Delphi", 100);
    s.info();
    
    return 0;
}
8、你也能做富二代
# 在C语言中struct有了自己的含义,虽然在C++中扩展成为了类,但一般情况还是遵循C中的用法
# C++一般情况下用class来做类的关键字声明
# 继承是C++中代码复用的方式,通过继承,在子类中可以使用父类中的代码
# 子类可以完全继承父类中所有的变量和函数,在可以使用父类的地方就可以用子类代替
# 子类从概念上而言是一种特殊的父类
#include <stdio.h>

struct Student
{
protected:
    const char* name;
    int number;
public:
    void set(const char* n, int i)
    {
        name = n;
        number = i;
    }
    
    void info()
    {
        printf("Name = %s, Number = %d\n", name, number);
    }
};

class Master : public Student
{
protected:
    const char* domain;
public:
    void setDomain(const char* d)
    {
        domain = d;
    }
    
    const char* getDomain()
    {
        return domain;
    }
};

int main()
{
    Master s;
    
    s.set("Delphi", 100);
    s.setDomain("Software");
    s.info();
    
    printf("Domain = %s\n", s.getDomain());
    
    return 0;
}

小结:
1、面向对象是一种新型的软件开发思想
2、面向对象将生活中的事物完全映射到程序中
3、抽象,封装和继承是面向对象程序设计的重要特征
4、继承能够很好的复用已有类的特性
5、子类是一种特殊化的父类

C语言中的函数