最全C语言个人笔记【第四章节-函数】

函数入门

在C语言中,函数意味着功能模块。一个典型的C语言程序,就是由一个个的功能模块拼接而成的整体。也因为如此,C语言被称为模块化语言。

对于函数的使用者,可以简单的将函数理解为黑箱子,使用者只管按照规定给黑箱一些输入,就会得到一些输出,而不必要理会黑箱子里面的运行细节。在这里插入图片描述
​ 黑箱的输入和输出

日常使用的电视机可以被理解为一个典型的黑箱子,它有一些公共的接口提供给使用者操作,比如开关、音量、频道等,使用者不需要理会其内部电路,更不需要管电视机的工作原理,只需要按照规定的接口操作得到结果即可。
在这里插入图片描述
​ 电视机

对于函数的设计者,最重要的工作是封装,封装意味着对外提供服务并隐藏细节。对于一个封装良好的函数而言,其对外提供服务的接口应当是简洁的,内部功能应当是明确的。

函数的定义

  • 函数头:函数对外的公共接口
    1. 函数名称:命名规则与变量一致,一般取与函数实际功能相符合的、顾名思义的名称。
    2. 参数列表:即黑箱的输入数据列表,一个函数可有一个或多个参数,也可以不需要参数。
    3. 返回类型:即黑箱的输入数据类型,一个函数可不返回数据,但最多只能返回一个数据
  • 函数体:函数功能的内部实现
  • 语法说明
返回类型 函数名称(参数1,参数2.......)
{
    函数体
    return 返回值;
}
  • 场景1:没有返回值没有参数的函数实现

在这里插入图片描述

demo1:

#include <stdio.h>

// 植发医院,子函数的函数体实现
// 没有返回值没有参数的函数
void huamei(void)
{
    printf("植发进行中,请勿打扰\n");
}

void basketGround(void)
{
    printf("小明打完篮球准备去植发\n");
    huamei();
}

// 主人的家,主函数
int main(int argc, char const *argv[])
{
    // 进入huamei函数并执行
    // huamei为函数名即是函数的首地址
    // ()表示进入此函数并执行
    huamei();
    basketGround();
    return 0;
}
  • 场景2:没有返回值有参数的函数
    在这里插入图片描述
#include <stdio.h>

// 子函数
void basketGround(char *tName)
{
    printf("%s正在打篮球\n",tName);
}

// 设计一个接口函数,实现两个数值相加
void Add(int a, int b)
{
    printf("%d+%d=%d\n",a,b,a+b);
}

// 主函数
int main(int argc, char const *argv[])
{
    char Name[] = "jack";
    // Name为实参,参数个数没有限制,用逗号隔开
    basketGround(Name); 
    Add(10,20);
    
    return 0;
}

  • 场景3:有返回值有参数
    在这里插入图片描述
#include <stdio.h>

// 实现两个数值相加,并将结果返回
float Add(int a, float b)
{
    return a+b;
}

int main(int argc, char const *argv[])
{
    int a = 10;
    float f = 10.5;
    float ret = Add(a,f);
    printf("%.2f\n",ret);
    return 0;
}

demo:

#include <stdio.h>

// 函数是一个独立的模块,所以不能定义在main里面
void TV(void)
{
    printf("我在看还珠格格第一部\n");
    return;
} 

int add(int a, int b)
{
    printf("a+b=%d\n",a+b);
    return a+b; // 30
}

void sub(int a, int b,char *vName)
{
    printf("%s 计算结果是:%d\n",vName,a-b);
}

char* showName( char(*name)[5] )
{
    printf("%s\n",name[1]);
    return name[1];
}

int main(int argc, char const *argv[])
{
    // 函数名相当于这个函数的入口地址
    printf("%p\n",TV);
    // 函数名()表示执行此函数
    TV();
    //
    int ret = add(10,20);
    printf("ret=%d\n",ret);
    printf("%d\n",add(15,30));

    sub(30,15,"jack");

    char name[3][5] = {"jack","rose","ken"};
    char * ret_name = showName(name);
    printf("%s\n",ret_name);

    return 0;
}

作业1:设计加减乘除取余接口函数,并实现将结果返回到主函数,并输出

#include <stdio.h>
#include <stdbool.h>

float add(float a, float b)// 形参,空间内存
{
    return a+b; // return后函数空间被释放
}

float sub(float *a, float *b)// 形参,空间内存
{
    return *a-*b; // return后函数空间被释放
}

float data; // 全局变量,程序不结束空间不释放

float *mul(float *a, float *b)// 形参,空间内存 
{
    data = (*a)*(*b);
    return &data; // return后函数空间被释放
}

bool div(float *a, float *b, float *rdata)// 形参,空间内存 
{
    *rdata = (*a)/(*b);
    return true;
}

//sur(int pbuf[])
void sur(int *pbuf,int *ret)
{
    *ret = pbuf[0] % pbuf[1];   
}

int main(int argc, char const *argv[])
{
    float a = 15.6;
    float b = 10.6;

    int a1 = 3;
    int b1 = 4;

    float ret = add(10.5,15.6);// 实参
    float ret1 = add(a1,b1);// 实参
    printf("%.2f,%.2f\n",ret, ret1);

    printf("%.2f\n", sub((float *)&a1,(float *)&b1));

    float *p = mul(&a,&b);
    printf("%.2f\n",*p);

    float rdata = 0;
    div(&a,&b,&rdata);
    printf("%.2f\n",rdata);


    int buf[2] = {a1,b1};
    int ret2;
    sur(buf,&ret2);
    printf("%d\n",ret2);

    return 0;
}

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

#include <stdio.h>

// int *swap(int a, int b,int *buf)
// {
//     int temp = a;
//     a = b;
//     b = temp;
//     buf[0] = a;
//     buf[1] = b;
//     return buf;
// }

void swap(int a, int b,int *buf)
{
    int temp = a;
    a = b;
    b = temp;
    buf[0] = a;
    buf[1] = b;
    return;
}

void swap1(int *a, int *b) // 形参
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(int argc, char const *argv[])
{
    int a = 10;
    int b = 20;
    int buf[2] = {a,b};
    //int *p = swap(a,b,buf);
    // 数组名相当于整个数组的首元素地址
    swap(a,b,buf);
    printf("(%d,%d)\n",buf[0],buf[1]);

    swap1(&a,&b); // 实参
    printf("(%d,%d)\n",a,b);
    return 0;
}

在这里插入图片描述

  • 函数示例1:求两个给定整数的最大值
int max(int x, int y)
{
    int z;
    z = x > y ? x : y;
    return z;
}
  • 函数示例2:交换两个浮点数
// 该函数接收两个浮点指针参数,不返回数据
void swap(double *p1,double *p2)
{
    if(p1 == NULL || p2 == NULL)
    {
        return;
    }
    double tmp;
    tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}
  • 语法汇总:

    1. 当函数的参数列表为void时,表示该函数不需要任何参数。
    2. 当函数的返回类型为void时,表示该函数不返回任何数据。
    3. 关键字return表示退出函数。①若函数头中规定有返回数据类型,则 return 需携带一个类型与之匹配的数据;②若函数头中规定返回类型为 void,则 return 不需携带参数。
  • 总结

    函数名前面为返回值的类型,哪怕此函数没有返回值,也要写上void 
    	 函数的返回类型要顶格
    	 函数返回类型与函数名空一格
    	 函数参数要写(),哪怕里面没有任何内容,如果没有内容最好这样写void func(void)
    	 函数调用者调用子函数的时候,可以不接收返回值
    	
    
  • 注意

注意: 大括号表示函数的工作范围,如果离开此范围,数据就不属于此函数管辖
	 函数不能在main(){}里面实现,虽然不会报错,如果不调用,这个子函数是不会被执行的
	 所有函数的实现都在主函数外面实现
// 语法没问题,编程规范有问题,没有人这么写
	int main()// 主函数
	{
		void func() // 子函数
		{
			printf("func\n");
		}
		
		func();
	}
	
	// 正确的写法是将子函数放到主函数外面实现
	void func()
	{
		
	}
	
	int main()
	{
		func();
	}

实参与形参

  • 概念:

    • 函数调用中的参数,被称为实参,即arguments
    • 函数定义中的参数,被称为形参,即parameters
  • 实参与形参的关系:

    • 实参与形参的类型和参数个数必须一一对应。
    • 形参的值由实参初始化。
    • 形参与实参位于不同的内存区域,彼此独立。
  • 示例:

// 函数定义中,x、y都属于形参,位于函数 max 的栈内存中
// 它们的值由实参一一对应初始化
int max(int x, int y)
{
    int z;
    z = x>y ? x : y;
    return z;
}

int main(void)
{
    int a = 1;
    int b = 2;
    int m;
        
    // 函数调用中,a、b都属于实参,存储于主函数 main 的栈内存中
    m = max(a, b);    
}

函数调用的流程

函数调用时,进程的上下文回切换到被调函数,当被调函数执行完毕之后再切换回去。
在这里插入图片描述

​ 函数调用时的执行流程

作业:

作业2:
	实现MyStrcat()接口

作业2:
输入一个数,求此数最近相邻最小质数(只能被1或者自身整除的数称为质数),如果本身是质数则输出自身。
    #include <stdio.h>
#include <stdbool.h>

bool isPriNumber(int n)
{
    if(n < 2)
        return false;

    // 查找质数
    for(int i = 2; i < n; i++)
    {
        if(n % i == 0) // 非质数
        {
            return false;
        }
    }

    return true;
}

int main(int argc, char const *argv[])
{
    int n;
    scanf("%d",&n);

    // 计数
    int count = 0;
    while (1)
    {
        // 判断是否是质数
        if(isPriNumber(n-count))
        {
            printf("%d\n",n-count);
            break;
        }

        // if(isPriNumber(n+count))
        // {
        //     printf("%d\n",n+count);
        //     break;
        // }

        count++;
    }
    
    return 0;
}


  • 冒泡函数

在这里插入图片描述

作业2:设计一个函数接口实现冒泡排序
输入15432  输出 12345
#include "bubble.h"

void bubble_sort(int *array, int length)
{
    for(int i = 0; i < length-1; i++) // 遍历length遍
    {
        for(int j = 0; j < length-i-1; j++)// 每次遍遍历length-i
        {
            if(array[j] > array[j+1])
            {
                int temp = array[j];
                array[j] = array[j+1];
                array[j+1] = temp;
            }
        }
    }
}
练习1:设计接口函数,实现冒泡void bobble_sort()算法,将冒泡函数放到一个独立的c文件,主函数将数据有序输出
练习2:设计接口函数,自行封装Mystrlen()
练习3: 设计接口函数,自行封装MyStrcpy()
  • gcc 编译器编译原理

1.将c文件进行预编译,作用是将头文件里面的内容进行展开,就是将头文件的内容拷贝到main函数上面
	gcc main.c -o main.i -E

2.生成汇编程序,作用是将c文件编译成更加底层的编程语言汇编程序
	gcc main.i -o main.s -S

3.将汇编程序生成未链接的可执行程序
	gcc main.s -o main.o -c

4.将未连接的可执行程序生成已链接的可执行程序
	gcc main.o -o main.exe

在这里插入图片描述

  • 函数多文件封装

将自行的公共接口进行独立放到单独的c文件,编译存储,如果想直接使用,主函数直接调用即可,但是编译的时候需要添加此文件一起编译例如:
// 1.创建MyString.h头文件,添加以下内容
typedef int Mc_Size_t;

Mc_Size_t MC_Strlen(const char *str);
char *MC_Strncpy(char *dest, const char *src, Mc_Size_t len);
char *MC_Strcat(char *dest, const char *src);


// 2.创建MyString.c文件,添加以下内容
#include <stdio.h>

typedef int Mc_Size_t;

Mc_Size_t MC_Strlen(const char *str)
{
    if(str == NULL)
        return -1;

    int len = 0;
    while (*(str++) != '\0')
    {
        len++;
    }
    return len;
}

/*
* strncpy : 字符串拷贝,安全版
* 参数:
    dest : 拷贝的目标位置
    src  :需要拷贝的字符串
    len  : 需要拷贝的长度 
* 返回值
    成功  : 返回拷贝后的字符串
    失败  :  NULL
*/
char *MC_Strncpy(char *dest, const char *src, Mc_Size_t len)
{
    if(dest == NULL || src == NULL)
        return NULL;

    int i;
    for(i = 0; i < len && src[i] != '\0'; i++)
    {
        dest[i] = src[i];
    }
    // 拷完后,末尾添加'\0'
    dest[i] = '\0';

    return dest;
}

/*
* strcat : 字符串拼接
* 参数:
    dest : 拼接后的目标位置
    src  :需要拼接的字符串

* 返回值
    成功  : 返回拼接后的字符串
    失败  :  NULL
*/
char *MC_Strcat(char *dest, const char *src)
{
    if(src == NULL || dest == NULL)
        return NULL;

    int i = 0;
    // 定位到dest最末尾
    while(dest[i]){++i;}

    // 将src放到dest末尾
    while(*src)
    {
        *(dest+i) = *(src++);
    }
    dest[i] = '\0';
    return dest;
}
// 创建main.c添加以下内容
#include <stdio.h>
//#include <MyString.h> // 在系统里面查找MyString.h文件发现找不到
#include "MyString.h" // 在当前目录查找MyString.h发现找到了

int main(int argc, char const *argv[])
{
    char *str = "kenjack";
    printf("len : %d\n",MC_Strlen(str));

    char dest[100];
    MC_Strncpy(dest,str,7);

    char *ret = MC_Strcat(dest,str);
    printf("%s\n",dest);
    printf("ret = %s\n",ret);
    return 0;
}

3.编译
	1.先打开终端输入
		gcc main.c MyString.c -o a.exe
	2.执行程序
		./a.exe

在这里插入图片描述
在这里插入图片描述

注意:编译带头文件的工程程序,千万不要和.h头文件一起编译,头文件是不需要编译的,只需要将头文件和需要编译的程序放在同一个文件夹即可
  • 主函数命令行参数

#include <stdio.h>

int main(int argc, char const *argv[])
{
    // char const *buf[] = {"jack","rose"};
    // printf("%s\n",buf[1]);
    
    // argc : 参数的个数,默认为1
    printf("argc : %d\n",argc);
    // argv[0]表示程序a.exe自身
    //printf("argv[0] : %s\n",argv[0]);
    // argv[1]表示传递的第一个参数,注意传递的参数要用空格隔开
    //printf("argv[1] : %s\n",argv[1]);

    // 输出外部参数 :"jack" "rose" "ken" "123"

    // 判断是否有输入4个外部参数
    if(argc != 5)
    {
        printf("请输入对应的参数个数:\n");
        return -1;
    }

    printf("argv[1] : %s\n",argv[1]);
    printf("argv[2] : %s\n",argv[2]);
    printf("argv[3] : %s\n",argv[3]);
    // 注意 所有外部输入的参数都是以字符串方式传输
    printf("argv[4] : %s\n",argv[4]);

    return 0;
}

在这里插入图片描述

C进程内存布局

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

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

  • PM:Physical Memory,物理内存。
  • VM:Virtual Memory,虚拟内存

在这里插入图片描述

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

  • 栈(stack)
  • 堆(heap)
  • 数据段
  • 代码段
    在这里插入图片描述

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

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

在这里插入图片描述
在这里插入图片描述

栈内存

  • 什么东西存储在栈内存中?

    • 环境变量
    • 命令行参数
    • 局部变量(包含形参)
  • 栈内存有什么特点?

    • 空间有限,尤其在嵌入式环境下,尤其不可以用来存储尺寸太大的变量,在Linux栈内存大小为8M
    • 每当一个函数被调用,栈就会向下增长一段,用以存储该函数的局部变量
    • 每当一个函数退出,栈就会向上缩减一段,将该函数的局部变量所占内存归还给系统。
    • 栈空间申请的变量随着函数结束,空间自动释放
  • 注意:

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

    在这里插入图片描述

  • 示例代码:

void func(int a, int *p) // 在函数 func 的栈内存中分配
{
    double f1, f2;        // 在函数 func 的栈内存中分配
    ...           // 退出函数 func 时,系统的栈向上缩减,释放内存
}

int main(void)
{
    int m  = 100;  // 在函数 main 的栈内存中分配
    func(m, &m);  // 调用func时,系统的栈内存向下增长
}

数据段与代码段

  • 数据段细分成如下几个区域:
    • .bss 段:存放未初始化的静态数据,它们将被系统自动初始化为0
    • .data段:存放已初始化的静态数据
    • .rodata段:存放常量数据
  • 代码段细分成如下几个区域:
    • .text段:存放用户代码
    • .init段:存放系统初始化代码

在这里插入图片描述

int a;       // 未初始化的全局变量,放置在.bss 中
int b = 100; // 已初始化的全局变量,放置在.data 中

int main(void)
{
    static int c;       // 未初始化的静态局部变量,放置在.bss 中
    static int d = 200; // 已初始化的静态局部变量,放置在.data 中
    
    // 以上代码中的常量100、200防止在.rodata 中
}
  • 注意:数据段和代码段内存的分配和释放,都是由系统规定的,我们无法干预,所以尽量不要使用数据段,除非没办法。

局部变量与栈内存

  • 局部变量概念:凡是被一对花括号包含的变量,称为局部变量
  • 局部变量特点:
    • 某一函数内部的局部变量,存储在该函数特定的栈内存中
    • 局部变量只能在该函数内可见,在该函数外部不可见
    • 当该函数退出后,局部变量所占用的内存立即被系统回收,因此局部变量也称为临时变量
    • 函数的形参虽然不被花括号包含,但依然属于该函数的局部变量
  • 栈内存特点:
    • 每当一个函数被调用时,系统将自动分配一段栈内存给该函数,用于存放其局部变量
    • 每当一个函数有退出时,系统将自动回收其栈内存
    • 系统为函数分配栈内存时,遵循从上(高地址)往下(低地址)分配原则
  • 示例代码:
int max(int x, int y)// 变量x和y存储在max()函数的栈中
{
    int z;//变量z存储在max()函数的栈中
    z = x > y ? x : y;
    return z; // 函数退出后,栈中的x、y和z被系统回收
}
int main(void)
{
    int a = 1;// 变量a存储在main()函数的栈中
    int b = 2;// 变量b存储在main()函数的栈中
    int m;// 变量m存储在main()函数的栈中,未赋值所以其值为随机值
    m = max(a,b);
}

​ 函数调用时代码的执行流程

  • 技术要点:
    • 栈内存相对而言是比较小的,不适合用来分配尺寸太大的变量
    • return之后不可再访问函数的局部变量,因此返回一个局部变量的地址通常是错误的。

静态数据

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

  • 全局变量:定义在函数外的变量
  • 静态局部变量:定义在函数内部,且被static修饰的变量
  • 示例:
int a; // 全局变量,退出整个程序之前不会释放
void f(void)
{
    static int b; // 静态局部变量,退出整个程序之前不会释放
    printf("%d\n", b);
    b++;
}

int main(void)
{
    f();
    f(); // 重复调用函数 f(),会使静态局部变量 b 的值不断增大
}
  • 为什么需要静态数据
    1. 全局变量在默认的情况下,对所有文件可见,为某些需要在各个不同文件和函数之间访问的数据提供操作上的方便。
    2. static修饰的全局变量,只能在本文件使用,如果未被static修饰的全局变量,所有的文件都能使用,会出现命名污染。
    3. 当我们希望一个函数退出后依然能保留局部变量的值,以便于下一次调用时还能用,静态局部变量可以帮助实现这样的功能。
  • 注意1:
    • 若定义时未初始化,则系统会将所有的静态数据自动初始化为0
    • 静态数据初始化语句,只会执行一遍。
    • 静态数据从程序开始运行时便已存在,直到程序退出时才释放。
  • 注意2:
    • static修饰局部变量:使之由栈内存临时数据,变为静态数据
    • static修饰全局变量:使之由个文件可见的静态数据,变成为本文件可见的静态数据
    • static修饰的函数:使之由各文件可见的函数,变成为本文件可见的静态函数。
  • 练习
创建一个工程main.c add.c add.h,在main.c和add.c分别写两个同名的子函数实现两个数值相加,add.h声明子函数,通过static解决命名冲突的问题?
// add.h

// static修饰函数也是讲次函数固定在本文件有效
// 其它文件无法调用
// 防止变量名字污染
static int add(int, int);
// add.c

#include <stdio.h>

static int a = 10;

int add(int a1, int b1)
{
    return a+b1;
}
// main.c
#include <stdio.h>
#include "add.h"

// static 的作用是将变量固定在本文件有效
// 防止变量名字污染
static int a = 10;


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

int main(int argc, char const *argv[])
{
    printf("%d\n",add(a,20));
    return 0;
}

编译:
gcc main.c add.c -o a.exe
执行程序
./a.exe

练习:

从键盘输入一个字符串,然后按照下面要求输出一个新字符串。
新串是在原串中,每两个字符之间插入一-个空格,如原串为abcd,
则新串为ab cd。要求在函数insert中完成新串的产生﹔
#include <stdio.h>
#include <string.h>

void insert_space(char *newStr,char *str)
{
    int cnt = 0;
    for(int i = 0; i < strlen(str); i++)
    {
        newStr[cnt++] = str[i];
        if((i+1) % 2 == 0) // 添加空格
        {
            newStr[cnt++] = ' ';
        }
    } 
}

int main(int argc, char const *argv[])
{
    char str[] = "jackroseken";
    char newStr[strlen(str)*2];
    // 清空数组
    memset(newStr,0,strlen(str)*2);

    insert_space(newStr,str);
    printf("%s\n",newStr);
    return 0;
}

堆内存(重点,一定要掌握)

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

  • 堆内存基本特征:
    • 相比栈内存,堆的总大小仅受限于物理内存,在物理内存允许的范围内,系统对堆内存的申请不做限制。
    • 相比栈内存,堆内存从下往上增长。
    • 堆内存是匿名的,只能由指针来访问。
    • 自定义分配的堆内存,除非开发者主动释放,否则永不释放,直到程序退出。

在这里插入图片描述

  • 相关API:
    • 申请堆内存:malloc() / calloc()/realloc
    • 清零堆内存:bzero()
    • 释放堆内存:free()

在这里插入图片描述

  • 示例:
int *p = (int *)malloc(sizeof(int)*100); // 申请1块大小为 sizeof(int) 的堆内存
p+1;
bzero(p, sizeof(int));        // 将刚申请的堆内存清零

char *q = malloc(sizeof(char)*100);
q+1;

*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);  // 释放堆内存
  • 注意:

    • malloc()申请的堆内存,默认情况下是随机值,一般需要用 bzero() 戳中memset()来清零。
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(int argc, char const *argv[])
    {
        // 手动申请堆空间
        int *ptr = malloc(5*sizeof(int));
        if(ptr == NULL)
        {
            // 将错误信息输出
            perror("malloc fialed:");
            return -1;
        }
        
        // 清空堆
        //bzero(ptr,5*sizeof(int));
        memset(ptr,0,5*sizeof(int));
    
        ptr[0] = 10;
        ptr[1] = 22;
        ptr[2] = 33;
        ptr[3] = 44;
        ptr[4] = 55;
    
        printf("%d\n",ptr[1]);
    
        int buf[5] = {100,200,300,400,500};
        //ptr[0] = buf[0];
        // strcpy拷贝的内容一定是字符串,非字符串不能使用
        //strcpy(ptr,buf);
    
        // 内存拷贝memcpy
        memcpy(ptr,buf,5*sizeof(int));
        printf("%d\n",ptr[2]);
    
        // 手动释放空间
        free(ptr);//将ptr与堆空间断开
        ptr = NULL; // 防止ptr成为野指针
    
        printf("%d\n",ptr[2]);
    
        return 0;
    }
    
    • calloc()申请的堆内存,默认情况下是已经清零了的,不需要再清零。

    在这里插入图片描述

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(int argc, char const *argv[])
    {
        // 申请堆空间
        char *p = calloc(5,sizeof(char));
        if(p == NULL)
        {
            perror("calloc failed:");
            return -1;
        }
        // 错误,不能直接赋值,会改变p所指向的地址
        //p = "jack";
        //printf("%s\n",p);
    
        // 清空堆空间
        memset(p,0,5);
        
        strcpy(p,"rose");
        printf("%s\n",p);
    
        // 清空堆空间
        memset(p,0,5);
    
        memcpy(p, "ken",3);
        printf("%s\n",p);
    
        // 释放空间
        free(p);
        p = NULL;
    
        return 0;
    }
    
    
    • realloc()申请的堆内存,在原来内存基础上可进行扩容。
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(int argc, char const *argv[])
    {
        // 申请堆空间
        char *p = calloc(1,sizeof(char));
        if(p == NULL)
        {
            printf("calloc failed:");
            return -1;
        }
    
        char buf[] = "afjoiajfajflajlfjalflafjslfjsljfls";
    
        // 扩容,如果不扩容会溢出
        char *ptr = realloc(p,100);
        strcpy(ptr,buf);
        printf("%s\n",ptr);
    
    
        return 0;
    }
    

    在这里插入图片描述

    • free()只能释放堆内存,并且只能释放整块堆内存,不能释放别的区段的内存或者释放一部分堆内存。
  • 释放内存的含义:

    • 释放内存意味着将内存的使用权归还给系统。
    • 释放内存并不会改变指针的指向。
    • 释放内存并不会对内存做任何修改,更不会将内存清零。

    demo:

    #include <stdio.h>
    #include <stdlib.h> // malloc
    #include <string.h>
    
    
    int main(int argc, char const *argv[])
    {
        // buf所指向的空间为随机数
        char *buf = (char *)malloc(100);
        memset(buf,0,100);
        strcpy(buf,"jack");
     
        printf("-------------------------\n");
    
        // 申请100块,每块4字节
         // buf1所指向的空间数据为0
        char *buf1 = calloc(100,1);
        memset(buf1, 0, 100);
        strcpy(buf1,"jack");
    
        // 对buf1堆空间进行拓展
        // 200是扩展后的总结大小,包含原来的大小
        // buf2在buf1的基础上进行内存的拓展
        // 如果没办法拓展,它会先释放原来的空间,然后再申请一块新的空间
        char *buf2 = realloc(buf1,200);
        printf("%s\n",buf2);
    
        printf("%p\n",buf1);
        printf("%p\n",buf2);
    
        free(buf);
        free(buf1);
    
        return 0;
    }
    
    

静态函数

  • 背景知识:普通函数都是跨文件可见,即在文件a.c中定义的函数可以在b.c中使用。
  • 静态函数:只能在定义的文件内可见的函数,称为静态函数。
  • 语法:
// 在函数头前面增加关键字static,使之成为静态函数
static void f(void)
{
    // 函数体
}
  • 要点:
    • 静态函数主要是为了缩小函数的可见范围,减少与其他文件中重命名函数冲突的概率。
    • 静态函数一般被定义在头文件中,然后被各个源文件包含。

函数指针(非常重要一定要掌握)与指针函数数组

  • 概念
    • 函数指针变量,就是用来保存函数地址的变量
int func(int a, int b);
// 函数指针,指向的函数类型为返回值为int 参数为(int,int)的函数
int (*pfunc)(int a, int b);
//demo:
#include <stdio.h>

typedef int int32_t;

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

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

int max(int a, int b)
{
    return a > b ? a : b;
}

int main(int argc, char const *argv[])
{
    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函数,实现比较最大值
    fMaxPtr fmax = max;
    printf("max = %d\n",fmax(19,29));

    return 0;
}

  • 函数指针数组
// 函数指针数组,数组存放的是指向返回值为 int 参数为int int类型的函数地址
int (*pfbuf[]) (int,int);
//dmeo:
#include <stdio.h>
#include <string.h>

// 设计函数指针数组类型,增加易用性
typedef int (*fpbuf[2]) (int,int);


int Max(int a, int b)
{
    return a > b ? a : b;
}

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

int main(int argc, char const *argv[])
{
    // 定义函数指针数组
    int (*pbuf[2]) (int,int) = {Max,Min};
    // 输出最小值
    printf("min = %d\n",pbuf[1](10,20));

    fpbuf fbuf = {Max,Min};
    // 输出最小值
    printf("Max = %d\n",fbuf[0](10,20));

    return 0;
}

作业:

​ typedef 定义函数指针数组,初始化 sub(减法) div(除法) rem(取余数)

#include <stdio.h>

typedef int (*pfunc[3])(int a, int b); 

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

int sub(int a, int b)
{
    return a-b;
}

int div(int a, int b)
{
    return a/b;
}

int main(int argc, char const *argv[])
{
    int (*p[3])(int a, int b) = {add, sub, div};
    printf("%d\n",p[0](10,20));
    printf("%d\n",p[1](30,20));
    printf("%d\n",p[2](40,20));

    pfunc func_buf = {add,sub,div};
    printf("%d\n",func_buf[0](10,20));
    printf("%d\n",func_buf[1](30,20));
    printf("%d\n",func_buf[2](40,20));

    return 0;
}

  • 回调函数(必须掌握)

    • 调用一个函数,这个函数传递函数指针,被调用的的这个函数通过这个函数指针进行调用其他函数,我们把这种方式称为回调函数
    // 1. callback.h
    /*
    *接口说明:
    *功能 : 实现两个数相加
    *参数 : 返回值是int 参数是int,int类型的函数
    */
    typedef int (*CALLCAK_ADDFUNC)(int,int);
    extern int callback(CALLCAK_ADDFUNC ptr);
    
    // 2. callback.c
    #include "callback.h"
    /*
    *接口说明:
    *功能 : 实现两个数相加
    *参数 : 返回值是int 参数是int,int类型的函数
    */
    // 开发者
    int callback(CALLCAK_ADDFUNC ptr)
    {
        return ptr(10,20);
    }
    
    // 2. main.c
    #include <stdio.h>
    #include "callback.h"
    
    // 回调后的执行者
    int add(int a, int b)
    {
        return a+b;
    }
    
    // 使用者
    int main(int argc, char const *argv[])
    {
        int ret = callback(add);
        printf("%d\n",ret);
        return 0;
    }
    
    
    // 编译
    gcc main.c callback.c -o main
    

    练习: 将冒泡排序设计成回调函数—>回调接口要考虑升序降序功能,因为接口一旦定下来一般不改
    main函数调用callback函数,callback实现冒泡算法,然后将
    结果发送到客户函数printSort(),printSort()将排序结果打印出来

    分成3个文件,将call_bubble_back进行封装
    
    //1.先创建sort.h
    
    #ifndef _SORT_H
    #define _SORT_H
    
    #include <stdio.h>
    #include <stdbool.h>
    
    // 函数指针
    typedef bool (*pfunc)(int,int);
    
    /*
    * 功能 :冒泡算法实现升降序
    * 参数
    * Array : 排序的数据
    * n : 数据个数
    * func : 升降序函数方法
    * 返回值 : 无返回值
    */
    #include <stdio.h>
    #include <stdbool.h>
    
    void bubble_sort(int *array, int n, pfunc func);
    
    #endif
    
    // 2.创建sort.c
    #include "callback.h"
    // 回调
    void bubble_sort(int *array, int n, pfunc func) 
    {
        for(int i = 0; i < n-1; i++)
        {
            for(int j = 0; j < n-i-1; j++)
            {
                if(func(array[j],array[j+1]))
                {
                    int temp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = temp;
                }
            }
        }
    }
    
    
    // 3. main.c
    #include <stdio.h>
    #include "callback.h"
    #include <stdbool.h>
    
    // 升序
    bool ascend(int a, int b)
    {
        return (a-b)>0 ? true : false;
    }
    
    // 降序
    bool descend(int a, int b) // 5 4
    {
        return (a-b)<0 ? true : false;
    }
    
    int main(int argc, char const *argv[])
    {
        int array[] = {5,4,3,2,1};
        // 升序
        bubble_sort(array,5,ascend);
    
        for(int i = 0; i < 5; i++)
            printf("%d\t",array[i]);
        return 0;
    }
    
    
    编译方法:
    gcc *.c
    

回调函数(钩子函数)

  • 概念:函数实现方不调用该函数,而由函数接口提供方间接调用的函数,称为回调函数。
  • 示例:系统中的信号处理,是一个典型的利用回调函数的情形。

在这里插入图片描述

  • 要点:
    • 示例中函数 sighandler 是回调函数。
    • signal() 将函数回调函数传递给内核,使得内核可以在恰当的时机回调 sighandler。
    • 应用开发者和内核开发者只要约定好回调函数的接口,即可各自开发,进度互不影响。

内联函数 inline

  • 内联的特性 :以空间换时间
  • 当编译器发现某段代码有inline关键字的时候就会将这段代码插入到当前的位置,加快运行效率,但是也会消耗一定的运行空间
  • 什么时候用inline
    1. 函数需要频繁被调用,代码最好不要超过5行
  • inline注意事项
    • 内联函数在头文件实现,其它函数不要在头文件实现
    • 函数声明和函数实现都需要添加关键字inline,如果函数声明没有添加extern 和 inline 关键字,会报错
// 创建main.h头文件

// 内联函数必须要声明,声明不需要加inline关键字
// 内联函数不需要单独放到c文件,直接放到头文件即可
// 切记其他函数实现一定要放在独立的c文件
int max(int a, int b);

// 将max函数声明为内联函数
inline int max(int a, int b)
{
    return a > b ? a : b;
}
// 创建main.c

#include <stdio.h>
#include "main.h"

int main(int argc, char const *argv[])
{
    int a = 10, b = 20;
    printf("max = %d\n",max(a,b));
    printf("max = %d\n",max(a,b));
    printf("max = %d\n",max(a,b));
    printf("max = %d\n",max(a,b));
    printf("max = %d\n",max(a,b));
    printf("max = %d\n",max(a,b));

    return 0;
}

编译 
gcc main.c

练习 : 通过内联函数实现获取两个数的最小值

递归函数

  • 递归概念:如果一个函数内部,包含了对自身的调用,则该函数称为递归函数。
自己调用自己,注意需要有结束条件,否则会出现内存溢出(段错误)
	什么时候用到递归  : 需要满足有规律递减条件,递减到某个程度是可以退出的
	func()
	{
		func()
	}
#include <stdio.h>

void func(int n)
{
    // 退出条件
    if(n == 5)
        return;

    func(++n);
    printf("%d\n",n);
}

int main(int argc, char const *argv[])
{
    int n = 0;
    func(n);
    return 0;
}
  • 递归问题:

    • 阶乘
    #include <stdio.h>
    
    int func(int n)
    {
        // 退出条件
        if(n == 1)
            return 1;
        return func(n-1)*n;
    }
    
    int main(int argc, char const *argv[])
    {
        int n = func(4);
        printf("%d\n",n);
        return 0;
    }
    
    
    • 幂运算(非递归实现,递归实现)
    //非递归
    float MyPower(float base, int exp)
    {
        int temp = base;
        for(int i = 0; i < exp-1; i++)
        {
            base *= temp;
        }
        return base;
    }
    
    // 递归
    float MyPower(float base, int exp)
    {
        if(exp == 0)
            return 1;
        
        return MyPower(base,exp-1)*base;
    }
    
    float MyPower(float base, int exp)
    {
        if(exp == 0)
            return 1;
        
        return MyPower(base,exp-1)*base;
    }
    
    int main(int argc, char const *argv[])
    {
        float base; // 底数
        int exp; // 幂指数
    
        printf("请输入底数和幂指数(2^3): ");
        int ret = scanf("%f^%d",&base,&exp);
        if(ret != 2)
        {
            printf("输入有误,请重新输入\n");
            while(getchar() != '\n');
            return -1;
        }
    
        if(base == 0 && exp == 0)
        {
            printf("0^0次方没有意义\n");
        }
        else if(base != 0 && exp == 0) // n^0
        {
            printf("%.2f^0=1\n",base);
        }
        else if(base == 0 && exp != 0) // 0^n
        {
            printf("0^%d=0\n",exp);
        }
        else if(exp > 0) // 正幂指数
        {
            printf("%.2f^%d = %f\n",base,exp,MyPower(base,exp));
        }
        else // 负幂指数
        {
            printf("%.2f^%d = %f\n",base,exp,1/MyPower(base,-exp));
        }
    
        return 0;
    }
    
    
    • 字符串翻转
    #include <stdio.h>
    
    void reverseToStr(void)
    {
        char ch = getchar();
        if(ch == '\n')
            return;
    
        reverseToStr();
        printf("%c",ch);
    }
    
    int main(int argc, char const *argv[])
    {
        reverseToStr();
        return 0;
    }
    
  • 要点:

    • 只有能被表达为递归的问题,才能用递归函数解决
    • 递归函数必须有一个可直接退出的条件,否则会无限递归
    • 递归函数包含两个过程,一个逐渐递进的过程,和一个逐渐回归的过程。
  • 示例:依次输出 n 个自然数。

  • 思路:先输出前面的n-1个自然数,再输出最后一个自然数n,而要输出前面的n-1个自然数,递归调用自身即可。

  • 递归调用时,函数的栈内存的变化如下图所示。可见,随着递归函数的层层深入,栈空间逐渐往下增长,如果递归的层次太深,很容易把栈内存耗光。

  • 层层递进时,问题的规模会随之减小,减小到可直接退出的条件时,函数开始层层回归。

函数strstr

在这里插入图片描述

  • 示例:
char *s = "abcd.txt";
char *p = strstr(s, ".wps");

if(p == NULL)
    printf("文件[%s]不是WPS文件\n", s);
else
    printf("文件[%s]是WPS文件\n", s);

函数strlen

在这里插入图片描述

  • 示例:
char *s = "www.vstc.com.cn";
printf("vstc地址的长度是:%d\n", strlen(s));

函数strtok

在这里插入图片描述

  • 注意:

    1. 该函数会将改变原始字符串 str,使其所包含的所有分隔符变成结束标记 ‘\0’ 。
    2. 由于该函数需要更改字符串 str,因此 str 指向的内存必须是可写的。
    3. 首次调用时 str 指向原始字符串,此后每次调用 str 用 NULL 代替。
  • 示例:

char s[20] = "vstc.com.cn";

char *p = strtok(s, ".");  // 首次调用时,s 指向需要分割的字符串
while(p != NULL)
{
    printf("%s\n", p);
    p = strtok(NULL, "."); // 此后每次调用,均使用 NULL 代替。
}

注:上述代码的运行结果就是将字符串 s 拆解为"www"、“yueqian”、“com” 和 “cn”

函数strcat与strncat

在这里插入图片描述

  • 注意:
    1. 这两个函数的功能,都是将 src 中的字符串,复制拼接到 dest 的末尾。
    2. strcat() 没有边界控制,因此可能会由于 src 的过长而导致内存溢出。
    3. strncat() 有边界控制,最多复制 n+1 个字符(其中最后一个是 ‘\0’ )到 dest 的末尾。
  • 示例:
char s1[10] = "abc";
strcat(s1, "xyz");
printf("%s\n", s1); // 输出 "abcxyz"

char s2[10] = "abc";
strcat(s3, "123456789"); // 此处操作内存溢出,可能会发生内存崩溃


char s[10]  = "abc";
strncat(s, "123456789", sizeof(s)-strlen(s)-1);
printf("%s\n", s); // 输出 "abc123456",两个字符串被拼接到了一起,且不会溢出
  • 注意:strncat()是具备边界检查的安全版本,推荐使用。

函数strcpy与strncpy

在这里插入图片描述

  • 注意:
    1. 这两个函数的功能,都是将 src 中的字符串,复制到 dest 中。
    2. strcpy() 没有边界控制,因此可能会由于 src 的过长而导致内存溢出。
    3. strncpy() 有边界控制,最多复制 n+1 个字符(其中最后一个是 ‘\0’ )到 dest 中。
  • 示例:
char s1[5] = "abc";
strcpy(s1, "xyz);
printf("%s\n", s1); // 输出 "xyz",原有的"abc"被覆盖

char s2[5] = "abc";
strcpy(s2, "123456789"); // 此处操作内存溢出,可能会发生内存崩溃
char s[5] = "abc";
strncpy(s, "123456789", sizeof(s)-1);
printf("%s\n", s); // 输出 "1234",有边界保护,不会溢出
  • 注意:strncpy()是具备边界检查的安全版本,推荐使用。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    // 将匿名数组的首地址存放到buf指针变量
    char *buf = "jack_rose";
    
    // 错误的,是将匿名数组的数据拷贝到buf指针
    // buf是没有实际存放数据的能力,所以需要分配空间
    //strcpy(buf,"jack,rose");

    // 1.用strcpy拷贝数据,内存必须存在
    // 栈空间
    char sbuf[5] = {0};
    // 将字符串拷贝到sbuf,拷贝5个字符
    strncpy(sbuf,"jack,rose",4);

    // 2.手动分配堆空间
    char *pbuf = malloc(5);
    // 清空堆空间
    memset(pbuf,0,5);
    
    strncpy(pbuf,"jack_rose",4);

    printf("%s,%s\n",sbuf,pbuf);

    // 释放空间
    free(pbuf);
    pbuf = NULL;

    return 0;
}

函数strcmp与strncmp

在这里插入图片描述

  • 作用: 判断两个字符串是否相等
  • 注意:
    • 比较字符串大小,实际上比较的是字符的 ASCII码值的大小。
    • 从左到右逐个比较两个字符串的每一个字符,当能“决出胜负”时立刻停止比较。
  • 示例:
printf("%d\n", strcmp("abc", "abc")); // 输出0,两个字符串相等
printf("%d\n", strcmp("abc", "aBc")); // 输出1,"abc" 大于 "aBc"
printf("%d\n", strcmp("999", "aaa")); // 输出-1,"999" 小于 "aaa"
#include <stdio.h>
#include <string.h>

int main(int argc, char const *argv[])
{
    char buf[] = "jackkenrose";
    // 字符串匹配主要是用于匹配内容是否相等
    // 判断大小没有意义
    if(strcmp(buf,"jackkenrose") == 0)
    {
        printf("匹配成功\n");
    }

    // 指定匹配4个字符
    if(strncmp(buf,"jack",4) == 0)
    {
        printf("匹配成功\n");
    }
    return 0;
}

函数strchr与strrchr

在这里插入图片描述

  • 注意:
    1. 这两个函数的功能,都是在指定的字符串 s 中,试图找到字符 c。
    2. strchr() 从左往右找,strrchr() 从右往左找。
    3. 字符串结束标记 ‘\0’ 被认为是字符串的一部分。
  • 示例:
#include <stdio.h>
#include <string.h>

int main(int argc, char const *argv[])
{
    char str[] = "www.qq.com";
    // 从左往右查找第一个.将后面的内容输出
    char *p = strchr(str,'.');
    printf("%s\n",p);

    // 从右往左查找第一个.将后面的内容输出
    p = strrchr(p,'.');
    printf("%s\n",p);

    return 0;
}


```c

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值