C语言菜鸟学习(函数)

引入

C语言本身就是由多个函数模块组成,在C语言本身自带的头文件中,也有很多被封装好的函数,在初学C语言时,我们最先使用的就是使用printf()函数输出一个“helloworld”;而printf()函数就是被封装在#include<stdio.h>头文件中的。

但是经过封装的函数我们无法看到源代码,在实际开发中,使用别人封装好的代码是具有一定风险的,所以我们必须学会自己去实现并调用函数,减少不必要的麻烦~~~

函数的定义

  • 函数头:函数对外的公共接口

    1. 函数名称:命名规则与变量一致,一般取与函数实际功能相符合的、顾名思义的名称。

    2. 参数列表:即黑箱的输入数据列表,一个函数可有一个或多个参数,也可以不需要参数。

    3. 返回类型:即黑箱的输入数据类型,一个函数可不返回数据,但最多只能返回一个数据

  • 函数体:函数功能的内部实现

  • 语法说明

返回类型 函数名称(参数1,参数2,.......)
{
    函数体
    return 返回值;
}

实参与形参

  • 概念:

    • 函数调用中的参数,被称为实参,即arguments

    • 函数定义中的参数,被称为形参,即parameters

  • 实参与形参的关系:

    • 实参与形参的类型和参数个数必须一一对应。

    • 形参的值由实参初始化。

    • 形参与实参位于不同的内存区域,彼此独立。

 

函数调用的流程

函数调用时,进程的上下文回切换到被调函数,当被调函数执行完毕之后再切换回去。

 例子:

 例如:封装接口函数实现两个数值交换,主函数将交换后的内容打印出来

#include <stdio.h>
//交换两个数
void swap(int *a,int *b)
{
    int t = *a;
    *a = *b;
    *b = t;
}
int main()
{

    int a = 10, b = 20;
    swap(&a,&b);
    printf("%d %d\n",a,b);

    return 0;
}
//主函数传参不会改变原来的数据,地址传参可以改变

 如以上代码所示,函数在调用后会将定义的函数空间全部释放,所以如果不利用存储地址的方法调用函数,主函数内的实参是不会发生任何变换。

地址传参的好处在于,传入函数的是实参的地址,即便函数内存被释放,但是实参的空间是伴随主函数的进程不会发生变化,从而实现我们想实现的功能。

C进程内存布局

任何一个程序,正常运行都需要内存资源,用来存放诸如变量、常量、函数代码等等。这些不同的内容,所存储的内存区域是不同的,且不同的区域有不同的特性。俗话所说知己知彼方能百战百胜,因此我们需要研究内存布局,逐个了解不同内存区域的特性。

每个C语言进程都拥有一片结构相同的虚拟内存,所谓的虚拟内存,就是从实际物理内存映射出来的地址规范范围,最重要的特征是所有的虚拟内存布局都是相同的,极大的方便内核管理不同的进程。例如三个完全不相干的进程p1,p2,p3,他们很显然会占据不同区域的物理内存,但是经过系统的变换和映射,它们的虚拟内存的布局是完全一样的。

  • PM:Physical Memory,物理内存。

  • VM:Virtual Memory,虚拟内存

 

将其中一个进程的虚拟内存放大来看,会发现其内部包含如下区域:

  • 栈(stack)

  • 堆(heap)

  • 数据段

  • 代码段

 

虚拟内存中,内核区段对于应用程序而言是禁闭的,它们用于存放操作系统的关键性代码,另外由于 Linux 系统的历史性原因,在虚拟内存的最底端 0x0 ~ 0x08048000 之间也有一段禁闭的区段,该区段也是不可访问的。

虚拟内存中各个区段详细内容:

而函数则是存储在栈内存中的。

栈内存

栈内存中存储的东西有:

  • 环境变量

  • 命令行参数

  • 局部变量(包含形参)

栈内存的特点有:

  • 空间有限,尤其在嵌入式环境下,尤其不可以用来存储尺寸太大的变量,在Linux栈内存大小为8M

  • 每当一个函数被调用,栈就会向下增长一段,用以存储该函数的局部变量

  • 每当一个函数退出,栈就会向上缩减一段,将该函数的局部变量所占内存归还给系统。

  • 栈空间申请的变量随着函数结束,空间自动释放

注意:栈内存的分配和释放都是由系统规定的,我们无法干预。

数据段与代码段

数据段细分成如下几个区域:

  • .bss 段:存放未初始化的静态数据,它们将被系统自动初始化为0

  • .data段:存放已初始化的静态数据

  • .rodata段:存放常量数据

  • 代码段细分成如下几个区域:

    • .text段:存放用户代码

    • .init段:存放系统初始化代码

 

  • 注意:数据段和代码段内存的分配和释放,都是由系统规定的,我们无法干预,所以尽量不要使用数据段,除非没办法。

以下代码体现栈内存与数据段的区别:

#include <stdio.h>
//数据段和代码段内存的分配和释放,都是由系统规定的,我们无法干预,所以尽量不要使用数据段,除非没办法。
//数据段的空间不会轻易被释放,程序结束时才被释放
int a1[8*1024*1024];//全局变量,存储在数据段
int main()
{
    //栈内存相对而言是比较小的,不适合用来分配尺寸太大的变量
    //栈空间能使用的最大内存为8M,但开8M空间会导致数据溢出,从而输出为段错误
    int a[8*1024*1024] = {};//局部变量,存储在栈空间
    printf("%p\n",a);
    //若局部变量与全局变量重名,不会报错,系统会根据就近原则,选择局部变量的值输出
    int a1 = 20;
    printf("%d\n",a1);

    //栈区的特点是系统自动分配空间,{}结束自动释放内存
    {
        int b = 10;
        printf("%d\n",b);
    }
   // printf("%d\n",b);//b的空间被释放,对b操作会报错
    return 0;
}
静态数据

c语言中,静态数据有两种:

  • 全局变量:定义在函数外的变量

  • 静态局部变量:定义在函数内部,且被static修饰的变量

为什么需要静态数据:

  1. 全局变量在默认的情况下,对所有文件可见,为某些需要在各个不同文件和函数之间访问的数据提供操作上的方便。

  2. static修饰的全局变量,只能在本文件使用,如果未被static修饰的全局变量,所有的文件都能使用,会出现命名污染。

  3. 当我们希望一个函数退出后依然能保留局部变量的值,以便于下一次调用时还能用,静态局部变量可以帮助实现这样的功能。

  • 注意1:

    • 若定义时未初始化,则系统会将所有的静态数据自动初始化为0

    • 静态数据初始化语句,只会执行一遍。

    • 静态数据从程序开始运行时便已存在,直到程序退出时才释放。

  • 注意2:

    • static修饰局部变量:使之由栈内存临时数据,变为静态数据

    • static修饰全局变量:使之由个文件可见的静态数据,变成为本文件可见的静态数据

    • static修饰的函数:使之由各文件可见的函数,变成为本文件可见的静态函数。

 

静态函数
  • 背景知识:普通函数都是跨文件可见,即在文件a.c中定义的函数可以在b.c中使用。

  • 静态函数:只能在定义的文件内可见的函数,称为静态函数。

  • 语法:

// 在函数头前面增加关键字static,使之成为静态函数
static void f(void)
{
    // 函数体
}
  • 要点:

    • 静态函数主要是为了缩小函数的可见范围,减少与其他文件中重命名函数冲突的概率。

    • 静态函数一般被定义在头文件中,然后被各个源文件包含。

★堆内存

堆内存(heap)又被称为动态内存、自由内存,简称堆。堆是唯一可被开发者自定义的区段,开发者可以根据需要申请内存的大小、决定使用的时间长短等。但又由于这是一块系统“飞地”,所有的细节均由开发者自己把握,系统不对此做任何干预,给予开发者绝对的“自由”,但也正因如此,对开发者的内存管理提出了很高的要求。对堆内存的合理使用,几乎是软件开发中的一个永恒的话题。

  • 堆内存基本特征:

    • 相比栈内存,堆的总大小仅受限于物理内存,在物理内存允许的范围内,系统对堆内存的申请不做限制。

    • 相比栈内存,堆内存从下往上增长。

    • 堆内存是匿名的,只能由指针来访问。

    • 自定义分配的堆内存,除非开发者主动释放,否则永不释放,直到程序退出。

  • 相关API:

    • 申请堆内存:malloc() / calloc()/realloc

    • 清零堆内存:bzero()

    • 释放堆内存:free()

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
    int *p = malloc(sizeof(int)); // 申请1块大小为 sizeof(int) 的堆内存
    bzero(p, sizeof(int));        // 将刚申请的堆内存清零

    *p = 100; // 将整型数据 100 放入堆内存中
    free(p);  // 释放堆内存

    // 申请3块连续的大小为 sizeof(double) 的堆内存
    double *k = calloc(3, sizeof(double));

    k[0] = 0.618;
    k[1] = 2.718;
    k[2] = 3.142;
    free(k);  // 释放堆内存
}

★函数指针与指针函数数组

  • 函数指针

 int func(int a, int b);
// 函数指针,指向的函数类型为返回值为int 参数为(int,int)的函数
int (*pfunc)(int a, int b);

#include <stdio.h>

// 给函数指针该别名,方便使用,增加指针的易用性
// 此时fptr就相当于void (*fptr)(int *a, int *b)的类型
typedef void (*fptr)(int *a, int *b);// 类似于int类型 int a = 10
typedef int (*fMinPtr)(int,int);// 函数指针的参数可以只写变量类型

void swap(int *a, int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

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

int main()
{
    int a = 10 , b = 20;
    // 定义函数指针指向swap
    // 注意pfunc指针的类型与所指向的函数类型一致
    // 将函数名去掉,剩下的部分为函数的类型
    void (*pfunc)(int *a, int *b) = swap;
    printf("%p\n",swap);
    printf("%p\n",pfunc);//两个指向的地址一致

    // 2. 将函数指针改别名
    fptr p = swap;
    printf("%p\n",p);

    // 通过函数指针执行swap函数
    //p(&a,&b);
    pfunc(&a,&b);
    printf("%d,%d\n",a,b);

    // 定义函数指针指向max函数,实现比较最大值
    fMinPtr fmin = min;
    printf("min = %d\n",fmin(19,29));

    return 0;
}
  • 函数指针数组

// 函数指针数组,数组存放的是指向返回值为 int 参数为int int类型的函数地址
int (*pfbuf[]) (int,int);

#include <stdio.h>
typedef int (*fpbuf[3]) (int,int);
//减法
int sub(int a,int b)
{
    return a - b;
}
//除法
int div(int a,int b)
{
    return a / b;
}
//取余数
int rem(int a,int b)
{
    return a % b;
}
int main()
{
    int (*fpbuf[3]) (int,int) = {sub, div, rem};
    int a,b;
    scanf("%d%d",&a,&b);
    for(int i = 0; i < 3; i++)
    {
        printf("%d\n",fpbuf[i](a,b));
    }
    return 0;
}

回调函数(钩子函数)

  • 概念:函数实现方不调用该函数,而由函数接口提供方间接调用的函数,称为回调函数。

  • 示例:系统中的信号处理,是一个典型的利用回调函数的情形。

  • 要点:

    • 示例中函数 sighandler 是回调函数。

    • signal() 将函数回调函数传递给内核,使得内核可以在恰当的时机回调 sighandler。

    • 应用开发者和内核开发者只要约定好回调函数的接口,即可各自开发,进度互不影响。

 例如:callback.h

#ifndef __CALLBACK_H__
#define __CALLBACK_H__
typedef int (*pFunc) (int,int);

extern int callback(pFunc ptr);
#endif

#include "callback.h"
#include <stdio.h>
int callback(pFunc ptr)
{
    return ptr(10,20);
}

#include <stdio.h>
#include "callback.h"
int add(int a,int b)
{
    return a+b;
}
int main()
{
    
    printf("%d\n",callback(add));
    return 0;
}

内联函数 inline

  • 内联的特性 :以空间换时间

  • 当编译器发现某段代码有inline关键字的时候就会将这段代码插入到当前的位置,加快运行效率,但是也会消耗一定的运行空间

  • 什么时候用inline

    1. 函数需要频繁被调用,代码最好不要超过5行

  • inline注意事项

    • 内联函数在头文件实现,其它函数不要在头文件实现

    • 函数声明和函数实现都需要添加关键字inline,如果函数声明没有添加extern 和 inline 关键字,会报错

例如:main.h 

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

#include <stdio.h>
#include "main.h"
int main()
{
    int a,b;
    scanf("%d%d",&a,&b);
    printf("%d\n",min(a,b));
    return 0;
}

递归函数

  • 递归概念:如果一个函数内部,包含了对自身的调用,则该函数称为递归函数。

自己调用自己,注意需要有结束条件,否则会出现内存溢出(段错误)
    什么时候用到递归 :需要满足有规律递减条件,递减到某个程度是可以退出的
    func()
    {
        func()
    }

 常见递归问题:爬楼梯

 以下是运用递归算法实现的,可见代码十分简洁明了,但是递归是在函数内部不断调用本身,这就导致它会不断开辟栈空间,从栈内存的知识点中我们知道,栈空间的内存是十分有限的,如果在用户需要输入一个很大的数据时,使用递归的算法便会数据溢出,导致段错误。

int climbStairs(int n) {
    if(n == 1)
    return 1;
    if(n == 2)
    return 2;
    return climbStairs(n-1)+climbStairs(n-2);
}

以下是运用非递归算法实现的,很明显代码比递归算法长出一截,但是在运行时却不会出现段错误,这是因为每次的结果我都用一个变量存储了下来,这样函数在调用时就不需要不断开辟新的空间。

int climbStairs(int n) {
    if(n == 1)
    return 1;
    if(n == 2)
    return 2;
    int t;
    int a = 1;
    int b = 2;
    while(n-2)
    {
        t = a + b;
        a = b;
        b = t;
        n--;
    }
    return t;
}

总结:在使用递归算法时,需要考虑到内存的大小,在内存足够的情况下,递归运算简洁明了,不会浪费空间是很好的一个算法~

  • 34
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值