C++ 函数指针与回调函数深度解析

C++ 函数指针与回调函数深度解析

第32篇:C++ 函数指针与回调函数深度解析

在这里插入图片描述

一、学习目标与重点

  • 掌握函数指针的定义、声明、初始化及调用方式
  • 理解函数指针的核心应用场景,能够灵活运用函数指针优化代码
  • 掌握回调函数的概念、实现原理及注册机制
  • 能够独立编写回调函数案例,解决实际开发中的解耦需求
  • 理解函数指针与typedef、std::function的结合使用技巧
  • 规避函数指针使用中的常见错误(类型不匹配、空指针调用等)

💡 核心重点:函数指针的类型匹配规则、回调函数的注册与执行流程、函数指针与现代C++特性的结合

二、函数指针基础认知

2.1 什么是函数指针

函数指针是指向函数的指针变量,本质是指针,但它存储的不是普通数据的地址,而是函数在内存中的入口地址。通过函数指针,我们可以间接调用函数,实现“以指针方式操作函数”的灵活编程模式。

🗄️ 类比理解:

  • 普通指针:int* p 指向int类型数据,通过*p访问数据
  • 函数指针:int (*p)(int, int) 指向“参数为两个int、返回值为int”的函数,通过p(1,2)(*p)(1,2)调用函数

2.2 函数指针的声明与语法规则

函数指针的声明需明确指定函数的返回值类型、参数列表类型,语法格式如下:

// 格式:返回值类型 (*指针变量名)(参数类型1, 参数类型2, ...); 返回值类型 (*func_ptr)(参数类型列表);

💡 语法解析:

  • (*func_ptr):括号必须存在,表明func_ptr是指针变量(若无括号,int* func_ptr(int) 是返回int*的函数声明)
  • 括号内的参数类型列表:必须与指向的函数参数类型、个数完全一致
  • 前面的返回值类型:必须与指向的函数返回值类型完全一致

2.3 函数指针的初始化与调用

函数指针的初始化有两种方式:直接赋值函数名(函数名本质是函数入口地址),或使用&函数名(取地址符可省略)。调用时可通过指针变量名(参数)(*指针变量名)(参数)两种方式。

💡 示例:基础函数指针的声明、初始化与调用

#include<iostream>usingnamespace std;// 定义一个加法函数intadd(int a,int b){return a + b;}// 定义一个减法函数intsubtract(int a,int b){return a - b;}intmain(){// 1. 声明函数指针:指向“参数为两个int、返回值为int”的函数int(*calc_ptr)(int,int);// 2. 初始化:赋值为add函数(&可省略) calc_ptr = add;// 等价写法:calc_ptr = &add;// 3. 调用方式1:指针变量名(参数)int result1 =calc_ptr(10,5); cout <<"10 + 5 = "<< result1 << endl;// 输出:10 + 5 = 15// 调用方式2:(*指针变量名)(参数)(兼容C语言风格,C++中两种方式等价)int result2 =(*calc_ptr)(20,8); cout <<"20 + 8 = "<< result2 << endl;// 输出:20 + 8 = 28// 4. 重新赋值为subtract函数,实现灵活切换 calc_ptr = subtract;int result3 =calc_ptr(15,6); cout <<"15 - 6 = "<< result3 << endl;// 输出:15 - 6 = 9return0;}

✅ 运行结果:

10 + 5 = 15 20 + 8 = 28 15 - 6 = 9 

2.4 函数指针的类型匹配规则(关键!)

函数指针的类型必须与指向的函数完全匹配,包括:

  1. 返回值类型一致(如int不能匹配void,double不能匹配int)
  2. 参数类型一致(如int不能匹配double,参数个数必须相同)
  3. const修饰一致(如指向const函数的指针不能指向非const函数)

⚠️ 警告:类型不匹配的赋值会导致编译错误,或运行时不可预期的行为(如内存访问错误)。

💡 错误示例(类型不匹配):

// 错误1:返回值类型不匹配(函数返回void,指针期望int)voidprintHello(){ cout <<"Hello"<< endl;}int(*func1_ptr)()= printHello;// 编译错误:返回值类型不匹配// 错误2:参数个数不匹配(函数需2个参数,指针期望1个)intmultiply(int a,int b){return a * b;}int(*func2_ptr)(int)= multiply;// 编译错误:参数个数不匹配// 错误3:参数类型不匹配(函数参数为double,指针期望int)doubledivide(double a,double b){return a / b;}int(*func3_ptr)(int,int)= divide;// 编译错误:参数类型不匹配

2.5 typedef简化函数指针声明

复杂的函数指针声明(如参数较多或嵌套)可读性差,可使用typedef为函数指针类型定义别名,简化代码。

💡 示例:typedef简化函数指针

#include<iostream>usingnamespace std;// 定义函数intadd(int a,int b){return a + b;}intsubtract(int a,int b){return a - b;}// 使用typedef定义函数指针类型别名:CalcFunc 是“int(int, int)”类型的函数指针别名typedefint(*CalcFunc)(int,int);intmain(){// 直接使用别名声明函数指针,语法更简洁 CalcFunc func_ptr; func_ptr = add; cout <<"3 + 4 = "<<func_ptr(3,4)<< endl;// 输出:7 func_ptr = subtract; cout <<"9 - 2 = "<<func_ptr(9,2)<< endl;// 输出:7return0;}

💡 C++11及以上也可使用using定义别名(更直观,推荐):

// 等价于typedef int (*CalcFunc)(int, int);using CalcFunc =int(*)(int,int);

三、函数指针的核心应用场景

函数指针的核心价值是“解耦”和“灵活切换”,以下是C++开发中最常见的应用场景:

3.1 场景1:实现函数回调(核心场景)

回调函数是指通过函数指针将函数作为参数传递给另一个函数,在合适的时机由被调用函数触发执行的函数。函数指针是回调机制的底层实现基础,广泛用于事件处理、框架设计、算法注入等场景。

💡 示例:回调函数实现通用计算器

#include<iostream>usingnamespace std;// 定义函数指针类型别名using CalcFunc =int(*)(int,int);// 通用计算函数:接收两个操作数和一个回调函数,通过回调函数实现不同运算intcalculate(int a,int b, CalcFunc callback){// 校验回调函数指针非空(避免空指针调用)if(callback ==nullptr){ cout <<"错误:回调函数指针为空!"<< endl;return0;}// 调用回调函数,实现灵活运算returncallback(a, b);}// 具体运算函数(回调函数)intadd(int a,int b){return a + b;}intsubtract(int a,int b){return a - b;}intmultiply(int a,int b){return a * b;}intdivide(int a,int b){if(b ==0){ cout <<"错误:除数不能为0!"<< endl;return0;}return a / b;}intmain(){int x =20, y =5;// 传递add作为回调函数 cout << x <<" + "<< y <<" = "<<calculate(x, y, add)<< endl;// 25// 传递subtract作为回调函数 cout << x <<" - "<< y <<" = "<<calculate(x, y, subtract)<< endl;// 15// 传递multiply作为回调函数 cout << x <<" * "<< y <<" = "<<calculate(x, y, multiply)<< endl;// 100// 传递divide作为回调函数 cout << x <<" / "<< y <<" = "<<calculate(x, y, divide)<< endl;// 4// 传递空指针(测试错误处理)calculate(x, y,nullptr);// 输出错误提示return0;}

✅ 运行结果:

20 + 5 = 25 20 - 5 = 15 20 * 5 = 100 20 / 5 = 4 错误:回调函数指针为空! 

3.2 场景2:实现函数表(跳转表)

函数表是存储函数指针的数组,通过索引快速切换并调用不同函数,适用于多分支场景(替代switch-case,提高代码可维护性)。

💡 示例:函数表实现菜单命令处理

#include<iostream>usingnamespace std;// 定义函数指针类型:无参数、无返回值using CommandFunc =void(*)();// 具体命令函数voidcmd_new(){ cout <<"执行【新建文件】命令"<< endl;}voidcmd_open(){ cout <<"执行【打开文件】命令"<< endl;}voidcmd_save(){ cout <<"执行【保存文件】命令"<< endl;}voidcmd_exit(){ cout <<"执行【退出程序】命令"<< endl;}intmain(){// 函数表:存储命令函数指针的数组 CommandFunc cmd_table[]={ cmd_new,// 索引0:新建 cmd_open,// 索引1:打开 cmd_save,// 索引2:保存 cmd_exit // 索引3:退出};int choice;while(true){// 显示菜单 cout <<"\n===== 菜单 ====="<< endl; cout <<"0. 新建文件"<< endl; cout <<"1. 打开文件"<< endl; cout <<"2. 保存文件"<< endl; cout <<"3. 退出程序"<< endl; cout <<"请输入选择(0-3):"; cin >> choice;// 校验输入合法性if(choice <0|| choice >=sizeof(cmd_table)/sizeof(cmd_table[0])){ cout <<"输入错误,请重新选择!"<< endl;continue;}// 通过函数表索引调用对应函数 cmd_table[choice]();// 退出程序if(choice ==3){break;}}return0;}

✅ 运行结果:

===== 菜单 ===== 0. 新建文件 1. 打开文件 2. 保存文件 3. 退出程序 请输入选择(0-3):0 执行【新建文件】命令 ===== 菜单 ===== 0. 新建文件 1. 打开文件 2. 保存文件 3. 退出程序 请输入选择(0-3):3 执行【退出程序】命令 

3.3 场景3:结合类成员函数(注意事项)

普通函数指针不能直接指向类的非静态成员函数,因为非静态成员函数隐含一个this指针参数(指向类实例),导致函数指针类型不匹配。需使用“成员函数指针”专门指向类成员函数。

💡 示例:类成员函数指针的使用

#include<iostream>usingnamespace std;classMathUtil{public:// 非静态成员函数(隐含this指针)intadd(int a,int b){return a + b;}intmultiply(int a,int b){return a * b;}// 静态成员函数(无this指针,可直接用普通函数指针指向)staticintsubtract(int a,int b){return a - b;}};intmain(){// 1. 非静态成员函数指针:需指定类名,语法格式:返回值类型 (类名::*指针名)(参数列表)int(MathUtil::*mem_func_ptr)(int,int);// 初始化:指向MathUtil的add成员函数 mem_func_ptr =&MathUtil::add;// 类成员函数必须加&// 调用:必须通过类实例(this指针需要绑定实例) MathUtil math;int result1 =(math.*mem_func_ptr)(10,3);// 语法:(实例.*指针名)(参数) cout <<"10 + 3 = "<< result1 << endl;// 13// 重新赋值为multiply成员函数 mem_func_ptr =&MathUtil::multiply;int result2 =(math.*mem_func_ptr)(10,3); cout <<"10 * 3 = "<< result2 << endl;// 30// 2. 静态成员函数指针:普通函数指针即可(无this指针)int(*static_func_ptr)(int,int)=&MathUtil::subtract;int result3 =static_func_ptr(10,3); cout <<"10 - 3 = "<< result3 << endl;// 7return0;}

⚠️ 注意事项:

  • 非静态成员函数指针的声明必须包含类名::,调用时必须绑定类实例((实例.*指针名)(参数)(指针->*指针名)(参数)
  • 静态成员函数无this指针,可直接用普通函数指针指向,无需绑定实例

3.4 场景4:函数指针与动态库(进阶应用)

在Windows/Linux平台下,可通过函数指针调用动态库(.dll/.so)中的导出函数,实现“运行时加载动态库”的灵活架构(插件化开发核心)。

💡 示例:Windows平台动态库函数调用(简化版)

#include<iostream>#include<windows.h>// Windows动态库相关头文件usingnamespace std;// 定义函数指针类型(与动态库导出函数匹配)typedefint(*AddFunc)(int,int);intmain(){// 1. 加载动态库(.dll文件路径) HMODULE hDll =LoadLibraryA("MathLib.dll");if(hDll ==nullptr){ cout <<"加载动态库失败!"<< endl;return1;}// 2. 获取动态库中导出函数的地址(函数名必须与导出时一致) AddFunc add =(AddFunc)GetProcAddress(hDll,"add");if(add ==nullptr){ cout <<"获取函数地址失败!"<< endl;FreeLibrary(hDll);// 释放动态库return1;}// 3. 通过函数指针调用动态库函数int result =add(100,200); cout <<"100 + 200 = "<< result << endl;// 300// 4. 释放动态库(避免内存泄漏)FreeLibrary(hDll);return0;}

⚠️ 说明:动态库需导出函数(如Windows中用__declspec(dllexport),Linux中用__attribute__((visibility("default")))),函数指针类型必须与导出函数严格匹配。

四、回调函数的进阶实现:从函数指针到std::function

C++11引入的std::function是通用的函数包装器,支持封装函数指针、函数对象、lambda表达式等,相比原生函数指针更灵活、类型安全,且支持捕获上下文(如lambda捕获变量),是现代C++中实现回调的首选方式。

4.1 std::function的基本用法

std::function的声明格式:std::function<返回值类型(参数类型列表)> 变量名;

💡 示例:std::function替代函数指针实现回调

#include<iostream>#include<functional>// 包含std::function头文件usingnamespace std;// 通用计算函数:使用std::function作为回调参数intcalculate(int a,int b, function<int(int,int)> callback){if(!callback){// 校验回调是否有效(比nullptr更直观) cout <<"错误:回调函数无效!"<< endl;return0;}returncallback(a, b);}// 普通函数intadd(int a,int b){return a + b;}// 函数对象(仿函数)structMultiplyFunc{intoperator()(int a,int b){return a * b;}};intmain(){int x =15, y =3;// 1. 封装普通函数 function<int(int,int)> func1 = add; cout << x <<" + "<< y <<" = "<<calculate(x, y, func1)<< endl;// 18// 2. 封装函数对象 MultiplyFunc multiply_obj; function<int(int,int)> func2 = multiply_obj; cout << x <<" * "<< y <<" = "<<calculate(x, y, func2)<< endl;// 45// 3. 封装lambda表达式(最灵活,支持捕获上下文)int factor =2;auto lambda_divide =[factor](int a,int b){if(b ==0)return0;return(a / b)* factor;// 捕获外部变量factor}; cout <<"("<< x <<" / "<< y <<") * "<< factor <<" = "<<calculate(x, y, lambda_divide)<< endl;// 10return0;}

✅ 运行结果:

15 + 3 = 18 15 * 3 = 45 (15 / 3) * 2 = 10 

4.2 std::function vs 原生函数指针

对比维度原生函数指针std::function
灵活性低(仅支持普通函数/静态成员函数)高(支持函数、函数对象、lambda、成员函数)
类型安全一般(编译时检查,报错信息不直观)高(编译时严格检查,报错信息清晰)
上下文捕获不支持(无法捕获外部变量)支持(通过lambda捕获变量)
空值处理需手动检查nullptr支持!callback直接判断有效性
语法复杂度低(简单场景易用)中(需包含头文件,声明格式类似)

💡 推荐用法:

  • 简单场景(无上下文捕获,仅普通函数):可使用原生函数指针
  • 现代C++开发(需lambda、函数对象,或需要捕获变量):优先使用std::function

五、函数指针使用的常见错误与规避方案

5.1 错误1:空指针调用(崩溃风险)

原因:

函数指针未初始化(默认是野指针)或被赋值为nullptr,直接调用会导致内存访问错误(程序崩溃)。

规避方案:
  1. 函数指针声明后立即初始化(如赋值为具体函数,或显式赋值为nullptr)
  2. 调用前必须检查指针是否非空(if (func_ptr != nullptr)if (callback)

💡 示例:安全调用函数指针

int(*func_ptr)(int,int)=nullptr;// 显式初始化if(func_ptr !=nullptr){// 调用前检查func_ptr(1,2);}else{ cout <<"函数指针未初始化!"<< endl;}

5.2 错误2:类型不匹配(编译/运行错误)

原因:

函数指针的返回值类型、参数类型/个数与指向的函数不匹配,或非静态成员函数用普通函数指针指向。

规避方案:
  1. 使用typedefusing定义函数指针类型别名,避免手动书写错误
  2. 严格遵循“类型完全匹配”原则,非静态成员函数使用专门的成员函数指针
  3. 现代C++中优先使用std::function,编译错误提示更清晰

5.3 错误3:函数指针生命周期问题(悬垂指针)

原因:

函数指针指向的函数已被销毁(如动态库已卸载、局部函数指针指向栈上的函数对象),后续调用会导致非法访问。

规避方案:
  1. 确保函数指针指向的函数生命周期长于指针的使用周期(如指向全局函数、静态函数)
  2. 动态库卸载前,确保不再使用库中的函数指针
  3. 避免函数指针指向局部函数对象(栈上对象,超出作用域后销毁)

5.4 错误4:忽略const修饰(编译错误)

原因:

函数指针未加const修饰,却指向const函数(如int (*func_ptr)(int) 指向 int func(const int a))。

规避方案:

函数指针的参数const修饰需与指向的函数一致:

intfunc(constint a){return a *2;}// 正确:参数类型包含constint(*func_ptr)(constint)= func;// 错误:参数类型无const,与函数不匹配// int (*func_ptr)(int) = func;

六、实战案例:函数指针+回调函数实现排序算法注入

6.1 问题描述

实现一个通用排序函数,支持对整数数组按升序或降序排序,排序规则通过回调函数注入(函数指针实现),提高代码复用性。

6.2 实现思路

  1. 定义排序回调函数类型:接收两个int参数,返回bool(表示第一个参数是否应排在第二个参数之前)
  2. 实现通用排序函数(如冒泡排序),接收数组、数组长度、排序回调函数
  3. 实现升序、降序两个具体的排序规则函数,作为回调函数传递给通用排序函数

6.3 代码实现

#include<iostream>usingnamespace std;// 1. 定义排序回调函数类型:bool(int a, int b),返回true表示a应排在b前面using CompareFunc =bool(*)(int,int);// 2. 通用冒泡排序函数:通过回调函数注入排序规则voidbubble_sort(int arr[],int length, CompareFunc compare){// 校验参数合法性if(arr ==nullptr|| length <=1|| compare ==nullptr){return;}for(int i =0; i < length -1;++i){for(int j =0; j < length -1- i;++j){// 调用回调函数,判断是否需要交换元素if(!compare(arr[j], arr[j +1])){// 交换元素int temp = arr[j]; arr[j]= arr[j +1]; arr[j +1]= temp;}}}}// 3. 具体排序规则函数(回调函数)// 升序排序:a < b 时,a应排在b前面boolascending(int a,int b){return a < b;}// 降序排序:a > b 时,a应排在b前面booldescending(int a,int b){return a > b;}// 辅助函数:打印数组voidprint_array(int arr[],int length){for(int i =0; i < length;++i){ cout << arr[i]<<" ";} cout << endl;}intmain(){int arr[]={5,2,9,1,5,6};int length =sizeof(arr)/sizeof(arr[0]); cout <<"原始数组:";print_array(arr, length);// 5 2 9 1 5 6// 4. 传递升序回调函数bubble_sort(arr, length, ascending); cout <<"升序排序后:";print_array(arr, length);// 1 2 5 5 6 9// 5. 传递降序回调函数bubble_sort(arr, length, descending); cout <<"降序排序后:";print_array(arr, length);// 9 6 5 5 2 1return0;}

6.4 运行结果

原始数组:5 2 9 1 5 6 升序排序后:1 2 5 5 6 9 降序排序后:9 6 5 5 2 1 

✅ 扩展:若需自定义排序规则(如按绝对值大小排序),只需新增一个回调函数,无需修改排序核心逻辑:

// 按绝对值升序排序boolabs_ascending(int a,int b){returnabs(a)<abs(b);}// 使用新的排序规则int arr2[]={-3,1,-5,2};bubble_sort(arr2,4, abs_ascending);print_array(arr2,4);// 1 2 -3 -5

七、总结

  1. 函数指针是指向函数的指针变量,核心语法需遵循“返回值类型、参数类型完全匹配”原则,typedef/using可简化声明。
  2. 函数指针的核心应用是回调函数和函数表,实现代码解耦和灵活切换,广泛用于事件处理、框架设计、动态库调用等场景。
  3. 类非静态成员函数需使用“成员函数指针”,且调用时必须绑定类实例;静态成员函数可直接用普通函数指针指向。
  4. 现代C++中,std::function是更灵活的函数包装器,支持封装函数、lambda、函数对象,推荐替代原生函数指针实现回调。
  5. 使用函数指针需规避空指针调用、类型不匹配、生命周期问题等常见错误,确保代码安全可靠。

通过本文学习,你应能熟练运用函数指针和回调函数解决实际开发中的解耦需求,并掌握现代C++中std::function的使用技巧。下一篇将深入探讨C++的模板编程基础,开启泛型编程的大门!

Read more

【华为OD机试真题 双机位A卷】937、正则表达式替换 | 机试真题+思路参考+代码解析 (C++、Java、Py、C语言、JS)

【华为OD机试真题 双机位A卷】937、正则表达式替换 | 机试真题+思路参考+代码解析 (C++、Java、Py、C语言、JS)

文章目录 * 一、题目 * 🎃题目描述 * 🎃输入输出 * 🎃样例1 * 🎃样例2 * 二、代码与思路参考 * 🎈C++语言思路 * 🎉C++代码 * 🎈Java语言思路 * 🎉Java代码 * 🎈Python语言思路 * 🎉Python代码 * 🎈C语言思路 * 🎉C代码 * 🎈JS语言思路 * 🎉JS代码 * 作者:KJ.JK 订阅本专栏后即可解锁在线OJ刷题权限   🍂专栏介绍:最新的华为OD机试题目总结,使用C++、Java、Python、C语言、JS五种语言进行解答,每个题目的思路分析都非常详细,支持在线OJ评测刷题!!!!订阅后获取权限,新增图解思路,问题解疑,多样例测试,超过百字的思路参考解析,持续更新,代码仅供学习参考   题库学习: 华为OD技术面试手撕真题 一、题目 🎃题目描述 为了便于业务互交,约定一个对输入的字

By Ne0inhk
JavaSE重点总结后篇

JavaSE重点总结后篇

🔥个人主页:寻星探路 🎬作者简介:Java研发方向学习者 📖个人专栏:JAVA(SE)----如此简单 从青铜到王者,就差这讲数据结构!!数据库那些事!!JavaEE 初阶启程记:跟我走不踩坑测试开发漫谈 ⭐️人生格言:没有人生来就会编程,但我生来倔强!!! 目录 一、面向对象 1、深拷贝和其那拷贝的区别 2、Java创建对象有哪几种方式? 二、String 1、String 和StringBuilder、StringBuffer 的区别? 2、String 是不可变类吗? 三、异常处理 1、Java中的异常体系? 2、异常的处理方式 四、I/O 1、Java中IO流分为几种? 2、有了字节流为什么还要有字符流? 3、BIO、NIO、

By Ne0inhk

JavaQuestPlayer终极指南:简单快速的QSP游戏完整解决方案

JavaQuestPlayer终极指南:简单快速的QSP游戏完整解决方案 【免费下载链接】JavaQuestPlayer 项目地址: https://gitcode.com/gh_mirrors/ja/JavaQuestPlayer 想要轻松畅玩各类QSP游戏却苦于复杂的配置过程?JavaQuestPlayer为你提供了最简单快捷的解决方案,这款基于Java开发的智能游戏运行器让新手也能快速上手,享受流畅的游戏体验。无论你是游戏爱好者还是开发者,都能在这里找到适合你的运行方式。 🎯 从零开始:快速入门指南 环境准备与项目获取 首先确保你的系统已安装Java运行环境,支持Oracle JDK1.8或OpenJDK JDK 11及以上版本。通过以下命令获取项目: git clone https://gitcode.com/gh_mirrors/ja/JavaQuestPlayer 项目结构清晰,主要源码位于src/main/java/com/baijiacms/qsp目录下,包含游戏控制器、资源管理、核心逻辑等模块。 两种运行模式详解 桌面应用模式提供原生的游戏体验

By Ne0inhk
使用 HTML + JavaScript 实现滑动验证码

使用 HTML + JavaScript 实现滑动验证码

文章目录 * 一、滑动验证码 * 二、效果演示 * 三、系统分析 * 1、页面结构 * 2、核心功能实现 * 2.1 初始化流程 * 2.2 拖拽交互处理 * 四、扩展建议 * 五、完整代码 一、滑动验证码 在现代网络安全体系中,人机验证机制扮演着至关重要的角色。传统的文本验证码由于识别困难、用户体验差等问题逐渐被更先进的验证方式取代。滑动验证码作为一种新型的人机验证手段,凭借其直观的操作体验和良好的安全性,广泛应用于各类网站和应用程序中。本文将详细介绍如何使用 HTML、CSS 和 JavaScript 构建一个完整的滑动验证码系统。 二、效果演示 滑动验证码的核心交互流程包括图像加载、拼图生成、用户拖拽和验证判断四个阶段,用户通过拖拽右侧滑块向右移动,使拼图块与背景图像中的缺口对齐,验证成功时显示绿色成功提示,失败则显示红色错误信息并自动重置。 三、系统分析 1、页面结构 整个滑动验证码系统采用简洁清晰的

By Ne0inhk