C 语言高级3--函数指针回调函数,预处理,动态库的封装

这篇具有很好参考价值的文章主要介绍了C 语言高级3--函数指针回调函数,预处理,动态库的封装。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

1.函数指针和回调函数

1.1 函数指针

1.1.1 函数类型

1.1.2 函数指针(指向函数的指针)

1.1.3 函数指针数组

       1.1.4 函数指针做函数参数(回调函数)

 2.预处理

2.1 预处理的基本概念

2.2 文件包含指令(#include)

2.2.1 文件包含处理

 2.2.2 #incude<>和#include""区别

2.3 宏定义

2.3.1 无参数的宏定义(宏常量)

2.3.2 带参数的宏定义(宏函数)

2.4 条件编译

2.4.1 基本概念

 2.4.2 条件编译

2.5 一些特殊的预定宏

3.动态库的封装和使用

3.1 库的基本概念

3.2 windows下静态库创建和使用

3.2.1 静态库的创建

3.2.2 静态库的使用

3.3 静态库优缺点

3.4 windows下动态库创建和使用

3.5 动态库的创建

3.6  动态库的使用

4. 递归函数

4.1 递归函数基本概念

4.2 普通函数调用

 4.3 递归函数调用

 4.4 递归实现字符串反转


1.函数指针和回调函数

1.1 函数指针

1.1.1 函数类型

通过什么来区分两个不同的函数?

一个函数在编译时被分配一个入口地址,这个地址就称为函数的指针,函数名代表函数的入口地址。

函数三要素: 名称、参数、返回值。C语言中的函数有自己特定的类型。

c语言中通过typedef为函数类型重命名:

typedef int f(int, int); // f 为函数类型

typedef void p(int); // p 为函数类型

这一点和数组一样,因此我们可以用一个指针变量来存放这个入口地址,然后通过该指针变量调用函数。

注意:通过函数类型定义的变量是不能够直接执行,因为没有函数体。只能通过类型定义一个函数指针指向某一个具体函数,才能调用。

typedef int(p)(int, int);

void my_func(int a,int b){
	printf("%d %d\n",a,b);
}

void test(){

	p p1;
	//p1(10,20); //错误,不能直接调用,只描述了函数类型,但是并没有定义函数体,没有函数体无法调用
	p* p2 = my_func;
	p2(10,20); //正确,指向有函数体的函数入口地址
}

 

1.1.2 函数指针(指向函数的指针)

  1.  函数指针定义方式(先定义函数类型,根据类型定义指针变量);
  2.  先定义函数指针类型,根据类型定义指针变量;
  3.  直接定义函数指针变量;
int my_func(int a,int b){
	printf("ret:%d\n", a + b);
	return 0;
}

//1. 先定义函数类型,通过类型定义指针
void test01(){
	typedef int(FUNC_TYPE)(int, int);
	FUNC_TYPE* f = my_func;
	//如何调用?
	(*f)(10, 20);
	f(10, 20);
}

//2. 定义函数指针类型
void test02(){
	typedef int(*FUNC_POINTER)(int, int);
	FUNC_POINTER f = my_func;
	//如何调用?
	(*f)(10, 20);
	f(10, 20);
}

//3. 直接定义函数指针变量
void test03(){
	
	int(*f)(int, int) = my_func;
	//如何调用?
	(*f)(10, 20);
	f(10, 20);
}

1.1.3 函数指针数组

函数指针数组,每个元素都是函数指针。

void func01(int a){
	printf("func01:%d\n",a);
}
void func02(int a){
	printf("func02:%d\n", a);
}
void func03(int a){
	printf("func03:%d\n", a);
}

void test(){

#if 0
	//定义函数指针
	void(*func_array[])(int) = { func01, func02, func03 };
#else
	void(*func_array[3])(int);
	func_array[0] = func01;
	func_array[1] = func02;
	func_array[2] = func03;
#endif

	for (int i = 0; i < 3; i ++){
		func_array[i](10 + i);
		(*func_array[i])(10 + i);
	}
}

       1.1.4 函数指针做函数参数(回调函数)

函数参数除了是普通变量,还可以是函数指针变量。

//形参为普通变量
void fun( int x ){}
//形参为函数指针变量
void fun( int(*p)(int a) ){}

函数指针变量常见的用途之一是把指针作为参数传递到其他函数,指向函数的指针也可以作为参数,以实现函数地址的传递。

int plus(int a, int b)
{
	return a + b;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a * b;
}
int division(int a, int b)
{
	return a / b;
}

//函数指针 做函数的参数 --- 回调函数
void Calculator(int(*myCalculate)(int, int), int a, int b)
{
	int ret = myCalculate(a, b); //dowork中不确定用户选择的内容,由后期来指定运算规则
	printf("ret = %d\n", ret);
}

void test01()
{
	printf("请输入操作符\n");
	printf("1、+ \n");
	printf("2、- \n");
	printf("3、* \n");
	printf("4、/ \n");

	int select = -1;
	scanf("%d", &select);

	int num1 = 0;
	printf("请输入第一个操作数:\n");
	scanf("%d", &num1);

	int num2 = 0;
	printf("请输入第二个操作数:\n");
	scanf("%d", &num2);

	switch (select)
	{
	case  1:
		Calculator(plus, num1, num2);
		break;
	case  2:
		Calculator(sub, num1, num2);
		break;
	case 3:
		Calculator(mul, num1, num2);
		break;
	case 4:
		Calculator(division, num1, num2);
		break;
	default:
		break;
	}

}

注意:函数指针和指针函数的区别:

        1函数指针是指向函数的指针;

        2指针函数是返回类型为指针的函数;

 2.预处理

2.1 预处理的基本概念

C语言对源程序处理的四个步骤:预处理、编译、汇编、链接。

预处理是在程序源代码被编译之前,由预处理器(Preprocessor)对程序源代码进行的处理。这个过程并不对程序的源代码语法进行解析,但它会把源代码分割或处理成为特定的符号为下一步的编译做准备工作。

2.2 文件包含指令(#include)

2.2.1 文件包含处理

“文件包含处理”是指一个源文件可以将另外一个文件的全部内容包含进来。C语言提供了#include命令用来实现“文件包含”的操作。

C 语言高级3--函数指针回调函数,预处理,动态库的封装,c语言,开发语言

 2.2.2 #incude<>和#include""区别

  1. "" 表示系统在file1.c所在的当前目录找file1.h,如果找不到,按系统指定的目录检索。
  2. < > 表示系统直接按系统指定的目录检索。

注意:

1. #include <>常用于包含库函数的头文件;

2. #include ""常用于包含自定义的头文件;

3. 理论上#include可以包含任意格式的文件(.c .h等) ,但一般用于头文件的包含;

2.3 宏定义

2.3.1 无参数的宏定义(宏常量)

如果在程序中大量使用到了100这个值,那么为了方便管理,我们可以将其定义为:

const int num = 100; 但是如果我们使用num定义一个数组,在不支持c99标准的编译器上是不支持的,因为num不是一个编译器常量,如果想得到了一个编译器常量,那么可以使用:

#define num 100

在编译预处理时,将程序中在该语句以后出现的所有的num都用100代替。这种方法使用户能以一个简单的名字代替一个长的字符串,在预编译时将宏名替换成字符串的过程称为“宏展开。宏定义,只在宏定义的文件中起作用。

#define PI 3.1415
void test(){
	double r = 10.0;
	double s = PI * r * r;
	printf("s = %lf\n", s);
}

 

说明:

  1. 宏名一般用大写,以便于与变量区别;
  2. 宏定义可以是常数、表达式等;
  3. 宏定义不作语法检查,只有在编译被宏展开后的源程序才会报错;
  4. 宏定义不是C语言,不在行末加分号;
  5. 宏名有效范围为从定义到本源文件结束;
  6. 可以用#undef命令终止宏定义的作用域;

在宏定义中,可以引用已定义的宏名;

2.3.2 带参数的宏定义(宏函数)

在项目中,经常把一些短小而又频繁使用的函数写成宏函数,这是由于宏函数没有普通函数参数压栈、跳转、返回等的开销,可以调高程序的效率。宏通过使用参数,可以创建外形和作用都与函数类似地类函数宏(function-like macro). 宏的参数也用圆括号括起来。

#define SUM(x,y) (( x )+( y ))
void test(){
	
	//仅仅只是做文本替换 下例替换为 int ret = ((10)+(20));
	//不进行计算
	int ret = SUM(10, 20);
	printf("ret:%d\n",ret);
}

注意:

  1. 宏的名字中不能有空格,但是在替换的字符串中可以有空格。ANSI C允许在参数列表中使用空格;
  2. 用括号括住每一个参数,并括住宏的整体定义。
  3. 用大写字母表示宏的函数名。
  4. 如果打算宏代替函数来加快程序运行速度。假如在程序中只使用一次宏对程序的运行时间没有太大提高。

2.4 条件编译

2.4.1 基本概念

一般情况下,源程序中所有的行都参加编译。但有时希望对部分源程序行只在满足一定条件时才编译,即对这部分源程序行指定编译条件。

C 语言高级3--函数指针回调函数,预处理,动态库的封装,c语言,开发语言

 2.4.2 条件编译

防止头文件被重复包含引用;

#ifndef _SOMEFILE_H
#define _SOMEFILE_H

//需要声明的变量、函数
//宏定义
//结构体

#endif

2.5 一些特殊的预定宏

C编译器,提供了几个特殊形式的预定义宏,在实际编程中可以直接使用,很方便。

//	__FILE__			宏所在文件的源文件名 
//	__LINE__			宏所在行的行号
//	__DATE__			代码编译的日期
//	__TIME__			代码编译的时间

void test()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
}

3.动态库的封装和使用

3.1 库的基本概念

库是已经写好的、成熟的、可复用的代码。每个程序都需要依赖很多底层库,不可能每个人的代码从零开始编写代码,因此库的存在具有非常重要的意义。在我们的开发的应用中经常有一些公共代码是需要反复使用的,就把这些代码编译为库文件。库可以简单看成一组目标文件的集合,将这些目标文件经过压缩打包之后形成的一个文件。像在Windows这样的平台上,最常用的c语言库是由集成按开发环境所附带的运行库,这些库一般由编译厂商提供。

3.2 windows下静态库创建和使用

3.2.1 静态库的创建

1. 创建一个新项目,在已安装的模板中选择“常规”,在右边的类型下选择“空项目”,在名称和解决方案名称中输入staticlib。点击确定。

2.在解决方案资源管理器的头文件中添加,mylib.h文件,在源文件添加mylib.c文件(即实现文件)。

3.在mylib.h文件中添加如下代码:

#ifndef TEST_H
#define TEST_H

int myadd(int a,int b);
#endif

4.在mylib.c文件中添加如下代码:

        

#include"test.h"
int myadd(int a, int b){
	return a + b;
}

5. 配置项目属性。因为这是一个静态链接库,所以应在项目属性的“配置属性”下选择“常规”,在其下的配置类型中选择“静态库(.lib)。

6.编译生成新的解决方案,在Debug文件夹下会得到mylib.lib (对象文件库),将该.lib文件和相应头文件给用户,用户就可以使用该库里的函数了。

3.2.2 静态库的使用

方法一:配置项目属性

A、添加工程的头文件目录:工程---属性---配置属性---c/c++---常规---附加包含目录:加上头文件存放目录。

B、添加文件引用的lib静态库路径:工程---属性---配置属性---链接器---常规---附加库目录:加上lib文件存放目录。

C  然后添加工程引用的lib文件名:工程---属性---配置属性---链接器---输入---附加依赖项:加上lib文件名。

 方法二:使用编译语句

#pragma comment(lib,"./mylib.lib")

 方法三:添加工程中

就像你添加.h和.c文件一样,把lib文件添加到工程文件列表中去.

切换到"解决方案视图",--->选中要添加lib的工程-->点击右键-->"添加"-->"现有项"-->选择lib文件-->确定.

3.3 静态库优缺点

  1. 静态库对函数库的链接是放在编译时期完成的,静态库在程序的链接阶段被复制到了程序中,和程序运行的时候没有关系;
  2. 程序在运行时与函数库再无瓜葛,移植方便。
  3. 浪费空间和资源,所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。

内存和磁盘空间

静态链接这种方法很简单,原理上也很容易理解,在操作系统和硬件不发达的早期,绝大部门系统采用这种方案。随着计算机软件的发展,这种方法的缺点很快暴露出来,那就是静态链接的方式对于计算机内存和磁盘空间浪费非常严重。特别是多进程操作系统下,静态链接极大的浪费了内存空间。在现在的linux系统中,一个普通程序会用到c语言静态库至少在1MB以上,那么如果磁盘中有2000个这样的程序,就要浪费将近2GB的磁盘空间。

 

程序开发和发布

空间浪费是静态链接的一个问题,另一个问题是静态链接对程序的更新、部署和发布也会带来很多麻烦。比如程序中所使用的mylib.lib是由一个第三方厂商提供的,当该厂商更新容量mylib.lib的时候,那么我们的程序就要拿到最新版的mylib.lib,然后将其重新编译链接后,将新的程序整个发布给用户。这样的做缺点很明显,即一旦程序中有任何模块更新,整个程序就要重新编译链接、发布给用户,用户要重新安装整个程序。

3.4 windows下动态库创建和使用

要解决空间浪费和更新困难这两个问题,最简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不是将他们静态的链接在一起。简单地讲,就是不对哪些组成程序的目标程序进行链接,等程序运行的时候才进行链接。也就是说,把整个链接过程推迟到了运行时再进行,这就是动态链接的基本思想。

3.5 动态库的创建

1. 创建一个新项目,在已安装的模板中选择“常规”,在右边的类型下选择“空项目”,在名称和解决方案名称中输入mydll。点击确定。

2.在解决方案资源管理器的头文件中添加,mydll.h文件,在源文件添加mydll.c文件(即实现文件)。

3.在test.h文件中添加如下代码:

#ifndef TEST_H
#define TEST_H

__declspec(dllexport) int myminus(int a, int b);

#endif

test.c文件中添加如下代码:

#include"test.h"
__declspec(dllexport) int myminus(int a, int b){
	return a - b;
}

5. 配置项目属性。因为这是一个态链接库,所以应在项目属性的“配置属性”下选择“常规”,在其下的配置类型中选择“态库(.dll)。

6.编译生成新的解决方案,在Debug文件夹下会得到mydll.dll (对象文件库),将该.dll文件、.lib文件和相应头文件给用户,用户就可以使用该库里的函数了。

疑问一:__declspec(dllexport)是什么意思?

动态链接库中定义有两种函数:导出函数(export  function)和内部函数(internal  function)。 导出函数可以被其它模块调用,内部函数在定义它们的DLL程序内部使用。

疑问二:动态库的lib文件和静态库的lib文件的区别?

在使用动态库的时候,往往提供两个文件:一个引入库(.lib)文件(也称“导入库文件”)和一个DLL(.dll)文件。虽然引入库的后缀名也是“lib”,但是,动态库的引入库文件和静态库文件有着本质的区别,对一个DLL文件来说,其引入库文件(.lib)包含该DLL导出的函数和变量的符号名,而.dll文件包含该DLL实际的函数和数据。在使用动态库的情况下,在编译链接可执行文件时,只需要链接该DLL的引入库文件,该DLL中的函数代码和数据并不复制到可执行文件,直到可执行程序运行时,才去加载所需的DLL,将该DLL映射到进程的地址空间中,然后访问DLL中导出的函数。

3.6  动态库的使用

方法一:隐式调用

     创建主程序TestDll,将mydll.h、mydll.dll和mydll.lib复制到源代码目录下。

(P.S:头文件Func.h并不是必需的,只是C++中使用外部函数时,需要先进行声明)

在程序中指定链接引用链接库 : #pragma comment(lib,"./mydll.lib")

方法二:显式调用

HANDLE hDll; //声明一个dll实例文件句柄

hDll = LoadLibrary("mydll.dll"); //导入动态链接库

MYFUNC minus_test; //创建函数指针

//获取导入函数的函数指针

minus_test = (MYFUNC)GetProcAddress(hDll, "myminus");

 

4. 递归函数

4.1 递归函数基本概念

C通过运行时堆栈来支持递归函数的实现。递归函数就是直接或间接调用自身的函数。

4.2 普通函数调用

void funB(int b){
	printf("b = %d\n", b);
}

void funA(int a){
	funB(a - 1);
	printf("a = %d\n", a);
}

int main(void){
	funA(2);
    printf("main\n");
	return 0;
}

函数的调用流程如下:

C 语言高级3--函数指针回调函数,预处理,动态库的封装,c语言,开发语言

 4.3 递归函数调用

void fun(int a){
	
	if (a == 1){
		printf("a = %d\n", a);
		return; //中断函数很重要
	}

	fun(a - 1);
	printf("a = %d\n", a);
}

int main(void){
	
	fun(2);
	printf("main\n");

	return 0;
}

函数的调用流程如下:

C 语言高级3--函数指针回调函数,预处理,动态库的封装,c语言,开发语言

 

作业:

递归实现给出一个数8793,依次打印千位数字8、百位数字7、十位数字9、个位数字3。

void recursion(int val){

if (val == 0){

return;

}文章来源地址https://www.toymoban.com/news/detail-627691.html

int ret = val / 10;

recursion(ret);

printf("%d ",val % 10);

}

 4.4 递归实现字符串反转

int reverse1(char *str){
	if (str == NULL)
	{
		return -1;
	}

	if (*str == '\0') // 函数递归调用结束条件
	{
		return 0;
	}
	
	reverse1(str + 1);
	printf("%c", *str);

	return 0;
}

char buf[1024] = { 0 };  //全局变量

int reverse2(char *str){
	if (str == NULL) 
	{
		return -1;
	}

	if ( *str == '\0' ) // 函数递归调用结束条件
	{
		return 0;
	}

	reverse2(str + 1);
	strncat(buf, str, 1);

	return 0;
}

int reverse3(char *str, char *dst){
	if (str == NULL || dst == NULL) 
	{
		return -1;
	}

	if (*str == '\0') // 函数递归调用结束条件
	{
		return 0;
	}

	reverse3(str + 1);

	strncat(dst, str, 1);

	return 0;
}

到了这里,关于C 语言高级3--函数指针回调函数,预处理,动态库的封装的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C语言——程序环境和预处理(再也不用担心会忘记预处理的知识)

    先简单了解一下程序环境,然后详细总结翻译环境里的编译和链接,然后在总结编译预处理。 在 ANSI C 的任何一种实现中,存在两个不同的环境 翻译环境:这个环境中源代码被转换为可执行的机器指令。 执行环境:执行二进制代码。 计算机如何执行二进制指令? 我们写的C语

    2024年02月09日
    浏览(44)
  • 【C语言】预处理

    在ANSI C的任何一种实现中,存在两个不同的环境。 第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。 第2种是执行环境,它用于实际执行代码。 1.翻译环境 组成一个程序的每个源文件通过编译过程分别转换成目标代码 每个目标文件由链接器(linker)捆绑在

    2024年02月17日
    浏览(56)
  • OpenCV图像预处理常用函数及流程

    在PyCharm终端中,运行如下命令 由于默认使用的为外网资源,下载速度和稳定性较差,具体看网络状态。如下命令为使用清华镜像下载安装相应的包 在终端中运行命令时,Windows10系统可能会存在如下报错:无法加载激活文件,因此在此系统上禁止运行脚本。此情况是因为win

    2024年02月05日
    浏览(41)
  • 详解C语言—预处理

    目录 一、预处理 1、预定义符号介绍 2、预处理指令 #define #define 定义标识符:  #define 定义宏: #define 替换规则 3、预处理操作符# 4、预处理操作符## 5、带副作用的宏参数 6、宏和函数对比 二、命名约定 三、预处理指令 #undef 四、命令行定义 五、条件编译  1、单分支#if:

    2024年02月08日
    浏览(46)
  • C语言预处理详解

    上一篇博客中我们讲了C语言的编译与链接,在编译过程中有三个小阶段:预处理、编译、汇编。 本篇博客将详细讲述预处理部分的有关知识点 。 在C语言中,C语言本身设置了⼀些预定义符号,可以直接使⽤ ,预定义符号的处理也是在预处理期间进行的。 在这里介绍几个常

    2024年04月15日
    浏览(45)
  • C语言:预处理详解

    创作不易,来个三连呗! C语⾔设置了⼀些预定义符号, 可以直接使⽤ ,预定义符号也是在预处理期间处理的。 __FILE__ //进⾏编译的源⽂件 __LINE__ //⽂件当前的⾏号 __DATE__ //⽂件被编译的⽇期 __TIME__ //⽂件被编译的时间 __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义

    2024年01月19日
    浏览(34)
  • C语言【预处理器】

    1、一些关于预处理的知识 ​C代码中,一般带 # 的都是预处理指令,包括 宏替换、文件包含、条件编译 等。 ​为兼容一些老编译器, # 前后一般不写空格 ​预处理指令后面不加分号。 2、宏定义 3、文件包含 ​自定义头文件,用\\\" \\\" 。 引号里填相对路径或绝对路径。基于当

    2024年02月05日
    浏览(37)
  • C语言·预处理详解

            C语言设置了一些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的                 __FILE__  进行编译的源文件                 __LINE__  文件当前的行号                 __DATE__  文件被编译的日期                 _

    2024年01月21日
    浏览(37)
  • 【C语言】预处理详解

             本文目录 1 预定义符号 2 #define 2.1 #define 定义标识符 2.2 #define 定义宏 2.3 #define 替换规则 2.4 #和## 2.5 带副作用的宏参数 2.6 宏和函数对比 2.7 命名约定 3 #undef 4 命令行定义 5 条件编译 6 文件包含 6.1 头文件被包含的方式 6.2 嵌套文件包含 这些预定义符号都是语言内置

    2024年02月14日
    浏览(32)
  • <C语言> 预处理和宏

    这些预定义符号都是C语言内置的。 举个例子: #define 定义标识符形式: 其中, 标识符 是你希望定义的名称,而 值 可以是一个数值、一个字符串或一个表达式。 例子: #define 只是进行简单的文本替换,没有类型检查和错误检查。 建议 #define 后面不要加分号 #define机制包括

    2024年02月14日
    浏览(27)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包