C语言---函数概念深入学习基础(3)

函数

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) {
  // ...
}
  • 7
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值