C语言---函数指针基础总结万字(4)

server/2024/12/22 19:50:15/

一、 函数

1.函数是一段可以重复执行的代码。

它可以接受不同的参数,
完成对应的操作。

下面的例子就是一个函数

int plus(int n) {return n;
}

上面的代码声明了一个函数plus()

2.函数声明的语法有以下几点,需要注意。
  • 返回值类型。
    函数声明时,
    首先需要给出返回值的类型,
    上例是int
    表示函数plus()返回一个整数。

  • 参数。
    函数名后面的圆括号里面,
    需要声明参数的类型和参数名,
    plus(int n)表示这个函数有一个整数参数n

  • 函数体。
    函数体要写在大括号里面,
    后面(即大括号外面)不需要加分号。
    大括号的起始位置,
    可以跟函数名在同一行,
    也可以另起一行。

  • return语句。
    return语句给出函数的返回值,
    程序运行到这一行,
    就会跳出函数体,
    结束函数的调用。
    如果函数没有返回值,
    可以省略return语句,
    或者写成return;

3.调用函数时,

只要在函数名后面加上圆括号就可以了,
实际的参数放在圆括号里面,
就像下面这样。

int a = plus(13);
// a 等于 14
4.函数调用时,

参数个数必须与定义里面的参数个数一致(一一对应),
参数过多或过少都会报错。

int plus(int n) {return n + 1;
}plus(2, 2); // 报错
plus();  // 报错

上面示例中,函数plus()只能接受一个参数,传入两个参数或不传参数,都会报错。

5.函数必须声明后使用,

否则会报错。
也就是说,
一定要在使用plus()之前,声明这个函数。
如果像下面这样写,编译时会报错。

int a = plus(13);int plus(int n) {return n + 1;
}

上面示例中,在调用plus_one()之后,才声明这个函数,编译就会报错。

6.C 语言标准规定,

函数只能声明在源码文件的顶层,
不能声明在其他函数内部。

7.没有返回值的函数,

使用void关键字表示返回值的类型。
没有参数的函数,
声明时要用void关键字表示参数类型。

void myFunc(void) {// ...
}

上面的myFunc()函数,
既没有返回值,
调用时也不需要参数。

8.函数可以调用自身,

这就叫做递归(recursion)
下面是斐波那契数列的例子。

unsigned long Fibonacci(unsigned n) {if (n > 2)return Fibonacci(n - 1) + Fibonacci(n - 2);elsereturn 1;
}

上面示例中,
函数Fibonacci()调用了自身,
这样做可以简化算法。

9.main()

C 语言规定,
main()是程序的入口函数,
即所有的程序一定要包含一个main()函数。
程序总是从这个函数开始执行,
如果没有该函数,
程序就无法启动。
其他函数都是通过它引入程序的。

main()的写法与其他函数一样,
要给出返回值的类型和参数的类型,
就像下面这样。

int main(void) {printf("Hello World\n");return 0;
}

上面示例中,
最后的return 0;表示函数结束运行,返回0

11.C 语言约定,

返回值0表示函数运行成功,
如果返回其他非零整数,
就表示运行失败,
代码出了问题。
系统根据main()的返回值,
作为整个程序的返回值,
确定程序是否运行成功。

正常情况下,
如果main()里面省略return 0这一行,
编译器会自动加上,
main()的默认返回值为0。
所以,写成下面这样,
效果完全一样。

int main(void) {printf("Hello World\n");
}

由于 C 语言只会对main()函数默认添加返回值,
对其他函数不会这样做,
建议总是保留return语句

12.参数的传值引用

如果函数的参数是一个变量,
那么调用时,
传入的是这个变量的值的拷贝,
而不是变量本身。

void increment(int a) {a++;
}int i = 10;
increment(i);printf("%d\n", i); // 10

上面示例中,
调用increment(i)以后,
变量i本身不会发生变化,
还是等于10
因为传入函数的是i的拷贝,
而不是i本身,
拷贝的变化,
影响不到原始变量。
这就叫做“传值引用(单向值传递)”。
还有一种方法是是双向传递(也叫地址传递)下面会讲解

所以,
如果参数变量发生变化,
最好把它作为返回值传出来。

int increment(int a) {a++;return a;
}int i = 10;
i = increment(i);printf("%d\n", i); // 11

再看下面的例子,Swap()函数用来交换两个变量的值,由于传值引用,下面的写法不会生效。

void Swap(int x, int y) {int temp;temp = x;x = y;y = temp;
}int a = 1;
int b = 2;
Swap(a, b); // 无效

上面的写法不会产生交换变量值的效果,
因为传入的变量是原始变量ab的拷贝,
不管函数内部怎么操作,
都影响不了原始变量。

13…如果想要传入变量本身,

有一个办法,
就是传入变量的地址(地址传递是双向的)。

void Swap(int* x, int* y) {int temp;temp = *x;*x = *y;*y = temp;
}int a = 1;
int b = 2;
Swap(&a, &b);

上面示例中,
通过传入变量xy的地址,
函数内部就可以直接操作该地址,
从而实现交换两个变量的值。

虽然跟传参无关,
这里特别注意下,
函数不要返回内部变量的指针。

int* f(void) {int i;// ...return &i;
}

上面示例中,
函数返回内部变量i的指针,
这种写法是错的。
因为当函数结束运行时,
内部变量就消失了,
这时指向内部变量i的内存地址就是无效的,
再去使用这个地址是非常危险的。

14.函数指针

函数本身就是一段内存里面的代码,
C 语言允许通过指针获取函数。

void print(int a) {printf("%d\n", a);
}void (*print_ptr)(int) = &print;

上面示例中,
变量print_ptr是一个函数指针,
它指向函数print()的地址。
函数print()的地址可以用&print获得。
注意,
(*print_ptr)一定要写在圆括号里面,
否则函数参数(int)的优先级高于*
整个式子就会变成void* print_ptr(int)

有了函数指针,
通过它也可以调用函数。

(*print_ptr)(10);
// 等同于
print(10);

比较特殊的是,
C 语言还规定,
函数名本身就是指向函数代码的指针,
通过函数名就能获取函数地址。
也就是说,
print&print是一回事。

if (print == &print) // true

因此,上面代码的print_ptr等同于print

void (*print_ptr)(int) = &print;
// 或
void (*print_ptr)(int) = print;if (print_ptr == print) // true

所以,对于任意函数,
都有五种调用函数的写法。

// 写法一
print(10)// 写法二
(*print)(10)// 写法三
(&print)(10)// 写法四
(*print_ptr)(10)// 写法五
print_ptr(10)

为了简洁易读,
一般情况下,
函数名前面都不加*&

15.这种特性的一个应用是,

如果一个函数的参数或返回值,
也是一个函数,
那么函数原型可以写成下面这样。

int compute(int (*myfunc)(int), int, int);

上面示例可以清晰地表明,
函数compute()的第一个参数也是一个函数。

16.函数原型

前面说过,
函数必须先声明,后使用。
由于程序总是先运行main()函数,
导致所有其他函数都必须在main()函数之前声明。

void func1(void) {
}void func2(void) {
}int main(void) {func1();func2();return 0;
}

上面代码中,
main()函数必须在最后声明,
否则编译时会产生警告,
找不到func1()func2()的声明。

但是,
main()是整个程序的入口,
也是主要逻辑,
放在最前面比较好。
另一方面,
对于函数较多的程序,
保证每个函数的顺序正确,
会变得很麻烦。

C 语言提供的解决方法是,
只要在程序开头处给出函数原型,
函数就可以先使用、后声明。
所谓函数原型,
就是提前告诉编译器,
每个函数的返回类型和参数类型。
其他信息都不需要,
也不用包括函数体,
具体的函数实现可以后面再补上。

int twice(int);int main(int num) {return twice(num);
}int twice(int num) {return 2 * num;
}

上面示例中,
函数twice()的实现是放在main()后面,
但是代码头部先给出了函数原型,
所以可以正确编译。
只要提前给出函数原型,
函数具体的实现放在哪里,
就不重要了。

17.函数原型包括参数名也可以,

虽然这样对于编译器是多余的,
但是阅读代码的时候,
可能有助于理解函数的意图。

int twice(int);// 等同于
int twice(int num);

上面示例中,
twice函数的参数名num
无论是否出现在原型里面,
都是可以的。

注意,
函数原型必须以分号结尾。

一般来说,
每个源码文件的头部,
都会给出当前脚本使用的所有函数的原型。

18.函数说明符

C 语言提供了一些函数说明符,
让函数用法更加明确。

(1)extern 说明符

对于多文件的项目,
源码文件会用到其他文件声明的函数。
这时,当前文件里面,
需要给出外部函数的原型,
并用extern说明该函数的定义来自其他文件。

extern int foo(int arg1, char arg2);int main(void) {int a = foo(2, 3);// ...return 0;
}

上面示例中,
函数foo()定义在其他文件,
extern告诉编译器当前文件不包含该函数的定义。

不过,
由于函数原型默认就是extern
所以这里不加extern
效果是一样的。

(2)static 说明符

默认情况下,
每次调用函数时,
函数的内部变量都会重新初始化,
不会保留上一次运行的值。
static说明符可以改变这种行为。

static用于函数内部声明变量时,
表示该变量只需要初始化一次,
不需要在每次调用时都进行初始化。
也就是说,
它的值在两次调用之间保持不变。

#include <stdio.h>void counter(void) {static int count = 1;  // 只初始化一次printf("%d\n", count);count++;
}int main(void) {counter();  // 1counter();  // 2counter();  // 3counter();  // 4
}

上面示例中,
函数counter()的内部变量count
使用static说明符修饰,
表明这个变量只初始化一次,
以后每次调用时都会使用上一次的值,
造成递增的效果。

注意,
static修饰的变量初始化时,
只能赋值为常量,
不能赋值为变量。

int i = 3;
static int j = i; // 错误

上面示例中,
j属于静态变量,
初始化时不能赋值为另一个变量i

另外,
在块作用域中,
static声明的变量有默认值0

static int foo;
// 等同于
static int foo = 0;

static可以用来修饰函数本身。

static int Twice(int num) {int result = num * 2;return(result);
}

上面示例中,
static关键字表示该函数只能在当前文件里使用,
如果没有这个关键字,
其他文件也可以使用这个函数(通过声明函数原型)。

static也可以用在参数里面,
修饰参数数组。

int sum_array(int a[static 3], int n) {// ...
}

上面示例中,static对程序行为不会有任何影响,
只是用来告诉编译器
该数组长度至少为3,
某些情况下可以加快程序运行速度。
另外,
需要注意的是,
对于多维数组的参数,
static仅可用于第一维的说明。

(3)const 说明符

函数参数里面的const说明符,
表示函数内部不得修改该参数变量。

void f(int* p) {// ...
}

上面示例中,
函数f()的参数是一个指针p
函数内部可能会改掉它所指向的值*p
从而影响到函数外部。

为了避免这种情况,
可以在声明函数时,
在指针参数前面加上const说明符,
告诉编译器,
函数内部不能修改该参数所指向的值。

void f(const int* p) {*p = 0; // 该行报错
}

上面示例中,声明函数时,
const指定不能修改指针p指向的值,
所以*p = 0就会报错。

但是上面这种写法,
只限制修改p所指向的值,
p本身的地址是可以修改的。

void f(const int* p) {int x = 13;p = &x; // 允许修改
}

上面示例中,
p本身是可以修改,
const只限定*p不能修改。

如果想限制修改p
可以把const放在p前面。

void f(int* const p) {int x = 13;p = &x; // 该行报错
}

如果想同时限制修改p*p
需要使用两个const

void f(const int* const p) {// ...
}

二、指针

几乎任何C语言资料都会提到一句话:

指针是C语言的精华。
出去说会C语言,
但不会指针,
还不如直接说不会C语言。

指针的重要性不言而喻。
然而,
指针重要,
但是又很难,
不容易学好,
那么本章,
我将以更易懂的方式,
让大家认识并理解C语言指针。

关于指针,
我一直认为:
其实每一门语言都有指针,
在学其他语言的时候没听说过,
不过是因为其他语言弱化了这个概念而已,
但实际上指针是存在的。

那么
什么是指针?
为什么要用指针?
怎么用指针?
图解
学习每一个知识点,
都躲不过这三个问题,

1.什么是指针?

首先,需要明确两个概念:

  • 指针是地址,是一个常量。
  • 平时所提到的“指针”,
    其实指的是存储地址的变量,
    准确的来说,
    应该叫做指针变量。

指针变量在定义方面,
和普通变量没什么区别,
但是它存的是变量的地址。

2.为什么要用指针?

在计算机当中,
任何一步操作都是在对内存进行访问。
为了能正确的访问这些存储单元,
就需要给它们编号,
这个编号,
就是地址。

  • 使用指针进行访问,
    即直接访问地址,
    这样效率更高。
  • C语言当中,只有传值,没有引用。
    想要对值进行传递,就必须要通过指针。(在函数部分体会最深)
  • C语言的函数,其实是一个指针。
    没有指针,就没有C语言函数。

3.怎么用指针?

说道怎么用,
那这肯定是这一章都说不完的话题了。
从这四个方面建立对指针的认识

  • (1)指针的定义、初始化和引用方式
  • (2)指针与函数
  • (3)指针与数组
(1)指针的定义、初始化和引用方式

在C语言中,指针是一种特殊的变量,它存储的是另一个变量的内存地址。以下是指针的定义、初始化和引用方式的详细说明:

1)指针的定义
int *p;

其中 int 是指针指向的变量的类型,
*p 是指针的名称。

2)初始化的方式

指针的初始化是指在定义指针的同时给它赋一个初始值。
初始化指针的方式有以下几种:

  1. 初始化为NULL

    int *ptr = NULL;
    

    指针被初始化为NULL,
    表示它不指向任何有效的内存地址。

  2. 初始化为一个变量的地址

    int var = 10;
    int *ptr = &var;
    

    指针 ptr 被初始化为变量 var 的地址。

  3. 初始化为一个常量的地址

    const int const_var = 20;
    int *ptr = &const_var;
    

    指针 ptr 被初始化为常量 const_var 的地址。

  4. 初始化为另一个指针的值

    int *ptr1 = &var;
    int *ptr2 = ptr1;
    

    ptr2 被初始化为 ptr1 的值,即 var 的地址。

  5. 动态内存分配

    int *ptr = (int *)malloc(sizeof(int));
    

    使用 malloc 函数动态分配了一个整型大小的内存,并让指针 ptr 指向这块内存。

3)引用方式
引用指针指向的值,可以使用解引用操作符 `*`。以下是几种引用指针的方式:
  1. 直接解引用

    int value = *ptr;
    

    value 被赋值为指针 ptr 指向的值。

  2. 在表达式中解引用

    *ptr = 30;
    

    将指针 ptr 指向的值设置为30。

  3. 作为函数参数

    void update(int *ptr) {*ptr = 40;
    }
    

    函数 update 接受一个整型指针作为参数,并修改它指向的值。

  4. 数组和指针

    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;
    int value = *(ptr + 2); // 引用数组第三个元素
    

    value 被赋值为数组 arr 的第三个元素。

  5. 字符串和指针

    char str[] = "Hello";
    char *ptr = str;
    char ch = *ptr; // 'H'
    

    ch 被赋值为字符串 str 的第一个字符。

通过这些方式
可以有效地使用指针来访问和操作内存中的数据。

(2)指针与函数

之前在介绍指针时,
提到过一句话:

C语言当中,
只有传值,
没有引用。
想要对值进行传递,
就必须要通过指针。

这句话什么意思?

1.传值

之前学习函数的时候遇到一个问题,
实参与形参都有自己独立的内存空间,
所以不通过指针,
进行交换两个数的操作,
达不到想到的结果。
下面是以前的例子:

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

因为在这个步骤中,只有传值,
并没有真正修改到实参的内容。
对于这个问题,
现在我们用指针来解决:

void swap(int *a, int *b) {int temp = *a;*a = *b;*b = temp;
}int main() {int a = 4, b = 6;swap(&a, &b);
}

思考:如果将上面的swap函数修改为一下形式,
能达到交换效果吗?

void swap(int *a, int *b) {int *temp = a;a = b;b = temp;
}
(3)指针与数组

数组是由若干个元素组成的,
每一个元素都有独立的存储空间,
并且地址连续。
而数组名,
就是这一块连续内存的首地址。
既然如此,
我们也可以使用指针来指向这一块地址。

int a[10] = {1, 2, 3, 4, 5, 6};
int *p = a;  // a不用写成&a,因为a作为数组名,本身就是地址

http://www.ppmy.cn/server/118480.html

相关文章

Ready Go

本文首发在这里 温馨提示 XX年&#xff0c;指的是20XX年&#xff0c;后跟以前、以后之类&#xff0c;均包含本数链接较多&#xff0c;只是想言之有物&#xff0c;已拒绝相同外链&#xff0c;仅看关心的即可已尽量只引用自己的东西&#xff0c;16年后仓库(11/13)&#xff0c;2…

Java项目实战II基于Java+Spring Boot+MySQL的图书管理系统的设计与实现 (源码+数据库+文档)

目录 一、前言 二、技术介绍 三、系统实现 四、论文参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 在信息爆炸…

LabVIEW机械手视觉引导系统

开发了LabVIEW软件和硬件工具开发的高精度机械手视觉引导系统。系统通过高效的视觉识别和精确的机械操作&#xff0c;提升工业自动化领域的生产效率和操作精度。 项目背景&#xff1a; 随着工业自动化的不断发展&#xff0c;对生产效率和精确度的要求也日益增高。传统的机械手…

开源 AI 智能名片链动 2+1 模式 O2O 商城小程序在社群活动中的应用与时机选择

摘要&#xff1a;本文探讨了开源 AI 智能名片链动 21 模式 O2O 商城小程序在社群经济中的重要性&#xff0c;着重分析了如何借助该小程序适时举办大型活动以维持和引爆社群活跃度。通过对活动时机选择的研究&#xff0c;强调了针对社群用户量身定制活动时机的必要性&#xff0c…

DesignPattern设计模式

1 前言 1.1 内容概要 理解使用设计模式的目的掌握软件设计的SOLID原则理解单例的思想&#xff0c;并且能够设计一个单例理解工厂设计模式的思想&#xff0c;能够设计简单工厂&#xff0c;理解工厂方法了解建造者模式的思想&#xff0c;掌握其代码风格理解代理设计模式的思想&a…

编写注册接口与登录认证

编写注册接口 在UserController添加方法 PostMapping("/login")public Result login(Pattern(regexp "^\\S{5,16}$") String username,Pattern(regexp "^\\S{5,16}$") String password){ // 根据用户名查询用户User loginUser userS…

python数据分析 pandas库-数据的读取和保存

python数据分析 pandas库-数据读取和保存 一、数据文件 在数据分析中&#xff0c;数据的读取是非常重要的一步。Pandas 提供了丰富的接口来读取各种格式的数据文件&#xff0c;例如 CSV、Excel、JSON、SQL 数据库等。接下来我们将详细说明如何使用 Pandas 读取不同格式的数据…

快速入门Vue

Vue是什么 Vue.js&#xff08;通常简称为Vue&#xff09;是一个开源的JavaScript框架&#xff0c;用于构建用户界面和单页应用程序&#xff08;SPA&#xff09;。它由尤雨溪&#xff08;Evan You&#xff09;在2014年开发并发布。Vue的核心库只关注视图层&#xff0c;易于上手…