数组参数、指针参数
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计。
一维数组传参
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void test(int arr[])//ok? OK
{}
void test(int arr[10])//ok? OK
{}
void test(int arr)//ok? OK
{}
void test2(int *arr[20])//ok? OK 20可以省略
{}
void test2(int **arr)//ok? OK
{}
int main()
{
int arr[10] = { 0 };
int *arr2[20] = { 0 };
test(arr);
test2(arr2);
return 0;
}
二维数组传参
void test(int arr[3][5]) //OK
{}
void test(int arr[][5]) //OK
{}
void test(int arr[3][]) //err
{}
void test(int arr[][]) //err
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr) //err
{}
void test(int **arr) //err
{}
void test(int *arr[5]) //err
{}
void test(int (*arr)[5]) //OK
{}
int main()
{
int arr[3][5]={0};
test(arr);//二维数组传参
return 0;
}
一级指针传参
void print(int * p,int sz)
{
int i=0;
for(i=0;i<sz;i++)
{
printf("%d\n", *(p+i));
}
}
int main()
{
int arr[10]={1,2,3,4,5,6,7,8,9};
int* p=arr;
int sz=sizeof(arr)/sizeof(arr[0]);//一级指针p,传给函数
print(p,sz);
return 0;
}
思考:
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
比如:
void test1(int* p)
{}
void test2(char* p)
{}
&a, p
二级指针传参
void test(int** ptr)
{
printf("num=%d\n",**ptr);
}
int main()
{
int n=10;
int* p=&n;
int** pp=&p;
test(pp);
test(&p);
return 0;
}
思考:
当一个函数的参数部分为二级指针的时候,函数能接收什么参数?
void test(char** p)
{}
int main()
{
char c='b';
char* pc=&c;
char** ppc=&pc;
char* arr[10];
test(&pc);
test(ppc);
test(arr);
return 0;
}
函数指针
函数指针-是指向函数的指针
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<Windows.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
printf("%d\n", Add(a, b));
printf("%p\n", &Add);
printf("%p\n", Add);//&函数名和函数名都是函数的地址
int(*pa)(int, int) = Add;
printf("%p\n", pa);
printf("%d\n", (*pa)(2,3));
return 0;
}
输出:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<Windows.h>
void Print(char* str)
{
printf("%s\n",str);
}
int main()
{
void (*P)(char*) = Print;
(*P)("Chung");
return 0;
}
例题:
(*(void(*)())0)();
void(*signal(int,void(*)(int)))(int);
第一句:这段代码实际上是一种C语言中的函数指针调用方式,意图是将地址为0的位置视为函数地址,并尝试跳转到那个地址执行代码。
首先,void(*)()表示一个函数指针类型,它指向一个没有参数和返回值的函数。在这个类型名称中,*表示指针,括号中的void表示函数没有返回值,而另一组括号中的空参数列表表示该函数没有接受参数。
其次,(void(*)())0将整数0的值强制类型转换为一个函数指针类型,它会发生未定义的行为,因为内存地址0通常是未映射的或保留的,而不是可执行代码所在的地址。这种类型转换会将指针设置为0,它类似于NULL指针,但它不是一个有效的函数指针,因为它指向无效的内存地址。
最后,整个表达式中的星号*是解引用符号,它会将指针转换为函数并执行它。但因为上面所述的原因,尝试执行地址为0的函数会导致系统崩溃,因此这段代码极其危险而不推荐使用。
第二句:这是一条函数声明语句,它可以读作:
“signal是一个函数,它有两个参数:一个整型参数和一个指向以整型参数为参数且没有返回值的函数指针。该函数会返回一个指向以整型参数为参数且没有返回值的函数指针。”
让我们逐个来看这个声明语句的不同部分。
void
表示函数的返回类型为空,因此函数并不返回任何值。这意味着 function call signal(a, b)
将不会返回任何值。
(int, void(*)(int))
是函数declaration(声明函数的参数)。它说明函数signal接受两个参数。第一个参数是一个整数类型,第二个参数是一个函数指针类型。对于这个函数指针类型 void()(int) ,可以理解为函数指针类型 (Function Pointer Type),该函数指针名字为 ‘’ 符号左边的void,包含一个int类型的参数表,其中* 符号代表一个函数指针。
最后,(*) (int)
是函数的返回类型。这里实际上定义了一个指向函数的指针,这个指针指向一个没有返回值的函数,且该函数接受一个整数类型的参数。
综合起来,这条函数声明语句的意义是:
signal
函数是指一个函数,这个函数的第一个参数是一个整数,第二个参数是一个指向一个以整数参数作为输入且没有返回值的函数的指针。该函数最终返回一个指向一个以整数为参数且没有返回值的函数指针。
顺便提一下signal
函数是一个内置函数,用于信号处理,允许进程在如同中断意外地被中断时,能够采取措施来处理。
自问:我发现,当这样给函数Print一个指针void ( * P)(char * ) = Print; 这样写:( * P)(“Chung”)和这样写:P(“Chung”)是等价的,这是为什么?
ChatAI回答:
(*P)
和P
是等价的,因为它们本质上都是函数指针,指向相同的函数。
在第一行代码中,void(*P)(char*)
声明了一个名为P
的函数指针,指向一个函数,该函数接受一个char*
类型的参数,没有返回值。Print
是一个函数名,它代表指向函数的指针。
在第二行代码中,(*P)("Chung")
使用了解引用符号*
来将P
转换为函数,并将字符串常量"Chung"
作为参数传递给该函数。正如你所看到的,你可以通过解引用函数指针来调用函数,这和直接使用函数名称是等效的。
在第三行代码中,使用P("Chung")
的语法,函数指针P
被隐式解引用为指向函数的指针,并将字符串常量"Chung"
作为参数传递给该函数。这种语法适用于函数指针变量,因为它具有函数名称的所有特性。在这种情况下,函数指针名称P
类似于函数名称,因此可以像函数名称一样使用。
综上所述,(*P)("Chung")
和P("Chung")
的功能是等效的,它们都将字符串参数传递给指向同一个函数的指针,并调用该函数。
老师解释:
第二个可以这样简化:
typedef void(*pfun_t)(int);
pfun_t signal(int,pfun_t);
signal是一个函数声明
signal函数的参数有2个,第一个是int.第二个是函数指针,该函数指针指向的函数的参数是int,返回类型是void。signal函数的返回类型也是一个函数指针:该函数指针指向的函数的参数是int,返回类型是void。
自行探索:
问ChatAI:
能否用void(func(int,void()(int)))(int);在实际的代码中进行运用举例?
ChatAI回答:
如果将函数名还原为func,则该函数原型的定义为:
void (*func(int, void(*)(int)))(int)
这是一个函数原型,该函数名为 func ,返回类型是指向函数指针的指针,并且接受两个参数,一个是 int 型,另一个是指向函数指针的指针。
该函数的返回值是一个指向函数指针的指针,该指针指向一个类型为void (*)(int) 的函数指针,即指向一个不返回值的函数,该函数只接受一个int类型参数。
下面给出一个简单的示例来演示如何声明、定义以及调用函数原型的方式:
#include <stdio.h>
void my_handler(int signal) {
printf("Received signal with value %d\n", signal);
}
void (*func(int, void (*)(int)))(int) {
printf("func is called.\n");
return my_handler;
}
int main(void) {
void (*fp)(int);
fp = func(0, &my_handler);
if(fp) {
printf("Got function pointer\n");
(*fp)(5);
}
return 0;
}
该代码定义了一个名为func的函数,它的功能是打印一条消息并返回一个指向函数指针的指针,该指针指向名为my_handler的函数。
在main()函数中,我们使用func()函数获取一个指向函数指针的指针,然后调用该函数指针,使它指向一个函数(也就是my_handler函数),最后通过该函数指针调用my_handler函数并向它传递一个值。
需要注意的是,需要严格按照函数原型的定义方式来声明、定义和调用该函数,这样才能保证函数的正确运行。
如果直接用ChatAI写的代码拷贝到VS2015在第九行会报错,于是继续问:
这段代码在VS2015会报错,无法成功运行,报错位置在第9行。错误是:错误:存在正文时不允许未命名的原型参数
ChatAI回答:
你好,看到你的反馈,错误信息是“存在正文时不允许未命名的原型参数”。这个错误的原因是你在函数定义的时候,没有对函数的形参进行命名。
你可以尝试将函数定义中的形参 int 设置一个名称,例如:
void (*func(int len, void (*handler)(int)))(int) {
printf("func is called.\n");
return my_handler;
}
这样,在函数定义中对参数int 命名为 len,就可以解决该问题了。 修改后的代码如下:
#include <stdio.h>
void my_handler(int signal) {
printf("Received signal with value %d\n", signal);
}
void (*func(int len, void (*handler)(int)))(int) {
printf("func is called.\n");
return my_handler;
}
int main(void) {
void (*fp)(int);
fp = func(0, &my_handler);
if(fp) {
printf("Got function pointer\n");
(*fp)(5);
}
return 0;
}
这样即可解决“存在正文时不允许未命名的原型参数”的问题了。
输出:
最后,通过自行探索一个代码:
(*(void(*)())0)();
因为用0是一个极其危险的操作,因此笔者将代码改良了一下,可以这样使用:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void Print()
{
printf("该操作极度危险!!\n");
}
int main()
{
void(*P)() = Print;//获取Print函数的地址,确保将Null替换为一个有效的地址
int a = P;//为了方便探索上述代码,特此用一个非地址的变量
(*(void(*)())a)();//将非地址变量强制转换成地址变量,并用该变量来执行该地址的函数
return 0;
}
输出:
我将自己写的这段代码拿给AI看:
可以这样写出来。把0替换成有效地址就行了。
没想到AI反怼我了:
虽然替换0为有效地址确实可以避免空指针引用错误,但这种写法仍然是非常危险的。因为我们将函数指针硬编码到了程序中,这意味着我们的程序依赖于特定的内存布局和地址空间分配,一旦环境有变化,这段代码可能会引发未知的错误。此外,这种写法也与跨平台编程的理念相悖,因为不同的操作系统和编译器可能会将相同的函数分配到不同的内存位置。因此,在编写跨平台应用程序时,最好避免硬编码内存地址。
如果我们必须要直接调用某个函数,我们可以使用动态链接库,将函数的实现打包成独立的库文件。然后,我们可以在程序运行时动态加载这个库,并通过库函数调用其中的函数。这种方式可以跨平台使用,并且在不需要的时候可以随时卸载库文件,避免空指针引用等错误。例如:
#include <stdio.h>
#include <dlfcn.h>
void Print()
{
printf("该操作极度危险!!\n");
}
int main()
{
// 打开动态链接库
void* handle = dlopen("./libdangerous.so", RTLD_NOW);
// 获取函数指针
void (*P)() = dlsym(handle, "Print");
// 调用函数
(*P)();
// 卸载动态链接库
dlclose(handle);
return 0;
}
在这个例子中,我们定义了一个函数Print(),并将它打包成独立的动态链接库“libdangerous.so”。在主函数中,我们使用dlopen()函数动态加载这个库,并使用dlsym()函数获取Print()函数的地址,然后通过函数指针调用这个函数。这种方式不仅保证了程序的安全性和可靠性,还允许我们在运行时动态加载和卸载库文件。