一、 函数
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); // 无效
上面的写法不会产生交换变量值的效果,
因为传入的变量是原始变量a
和b
的拷贝,
不管函数内部怎么操作,
都影响不了原始变量。
13…如果想要传入变量本身,
有一个办法,
就是传入变量的地址(地址传递是双向的)。
void Swap(int* x, int* y) {
int temp;
temp = *x;
*x = *y;
*y = temp;
}
int a = 1;
int b = 2;
Swap(&a, &b);
上面示例中,
通过传入变量x
和y
的地址,
函数内部就可以直接操作该地址,
从而实现交换两个变量的值。
虽然跟传参无关,
这里特别注意下,
函数不要返回内部变量的指针。
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)初始化的方式
指针的初始化是指在定义指针的同时给它赋一个初始值。
初始化指针的方式有以下几种:
-
初始化为NULL:
int *ptr = NULL;
指针被初始化为NULL,
表示它不指向任何有效的内存地址。 -
初始化为一个变量的地址:
int var = 10; int *ptr = &var;
指针
ptr
被初始化为变量var
的地址。 -
初始化为一个常量的地址:
const int const_var = 20; int *ptr = &const_var;
指针
ptr
被初始化为常量const_var
的地址。 -
初始化为另一个指针的值:
int *ptr1 = &var; int *ptr2 = ptr1;
ptr2
被初始化为ptr1
的值,即var
的地址。 -
动态内存分配:
int *ptr = (int *)malloc(sizeof(int));
使用
malloc
函数动态分配了一个整型大小的内存,并让指针ptr
指向这块内存。
3)引用方式
引用指针指向的值,可以使用解引用操作符 `*`。以下是几种引用指针的方式:
-
直接解引用:
int value = *ptr;
value
被赋值为指针ptr
指向的值。 -
在表达式中解引用:
*ptr = 30;
将指针
ptr
指向的值设置为30。 -
作为函数参数:
void update(int *ptr) { *ptr = 40; }
函数
update
接受一个整型指针作为参数,并修改它指向的值。 -
数组和指针:
int arr[5] = {1, 2, 3, 4, 5}; int *ptr = arr; int value = *(ptr + 2); // 引用数组第三个元素
value
被赋值为数组arr
的第三个元素。 -
字符串和指针:
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作为数组名,本身就是地址