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

一、 函数

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);
  else
    return 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();  // 1
  counter();  // 2
  counter();  // 3
  counter();  // 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作为数组名,本身就是地址
  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值