《C语言高级》(二)------ 函数与指针 篇

目录

一、函数

1.1、创建和使用函数

1.2、全局变量与局部变量

1.3、函数的参数和返回

1.4、递归调用

1.5、斐波那契数列解法其三

1.6、汉诺塔

1.7、快速排序算法

二、指针

        2.1、初识指针

        2.2、指针与数组

        2.3、多级指针

        2.4、指针数组与数组指针

        2.5、指针函数与函数指针


一、函数

其实函数我们在一开始就在使用了:

int main() { //这是定义函数
    ...
}

我们程序的入口点是main函数,我们只需要将我们的程序代码编写到主函数中就可以运行了,不过这个函数只是由我们来定义,而不是我们自己来调用。当然,除了主函数之外,我们一直在使用的printf也是一个函数,不过这个函数是标准库中已经实现好了的,现在是我们在调用这个函数:

printf("hello world"); //直接通过 函数名称(参数...)的形式调用函数

那么,函数的具体定义是什么呢?

函数是完成特定任务的独立程序代码单元


1.1、创建和使用函数

首先我们来看看如何创建一个函数,其实创建一个函数是很简单的,格式如下:

返回值类型 函数名称([函数参数...]);

其中函数名称也是有要求的,并不是所有的字符都可以用作函数名称,它的命名规则与变量的命名规则基本一致,所有这里就不一一列出了。

函数不仅仅需要完成我们的任务,可能某些函数还需要告诉我们结果,比如我们之前学习的getchar函数,这个函数实际上返回了一个int值作为结果(也就是我们输入的字符)我们同样可以将函数返回的结果赋值给变量或是参与运算等等。

当然如果我们的函数只需要完成任务,不需要告诉我们结果,返回值类型可以写成void表示为空

#include <stdio.h>
void test(void) {
    printf("hi");
}

int main(){
 test();

}

1.2、全局变量与局部变量

现在我们已经了解了如何创建和调用函数,在继续学习后续内容之前,我们需要先认识一下全局变量和局部变量这两个概念

我们首先来看看局部变量,实际上我们之前使用的都是局部变量,比如

int main() {
    int i = 10;
}

局部变量只会在其作用域中生效:

int main() {
    int i = 10;
}

void test() {
    i += 10;
}

可以看到在其他函数中,无法使用main函数中的变量,因为局部变量的作用域是有限的,比如位于某个函数内部的变量,那么它的作用域就是整个函数内部,而在其他位置均无法访问。


1.3、函数的参数和返回

我们的函数可以接受参数来完成任务,比如我们现在想要实现用一个函数计算两个数的和并输出到控制台。这种情况我们就需要将我们需要进行加法计算的两个数,告诉函数,这样函数才能对这两个数求和,那么怎么才能告诉函数呢?我们可以通过设定参数。

#include <stdio.h>
void add(int x, int y) {
    int z;
    z = x + y;
    printf("%d", z);
}
int main() {
    add(5, 4);
}

如果我们修改形式参数的值,外面的实参值会跟着发生修改吗

# include <stdio.h>

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

int main() {
    int a=10, b=20;
    swap(a, b);
    printf("a=%d,b=%d", a, b);

}

通过结果发现,虽然调用了函数对a和b的值进行交换,但缺并没有什么用

函数的形参实际上就是函数内的局部变量,它的作用域仅仅是这个函数,而我们外面传入的实参,仅仅只是将值赋值给了函数内的形参而已,并且外部的变量跟函数内部的变量作用域都不同,这里交换的仅仅是函数内部的两个形参变量值,跟外部作实参的变量没有任何关系。

 如何在每次调用函数时保留变量的值呢,我们可以使用静态变量:

# include <stdio.h>

void swap(){
    static int count = 0;
    count++;
    printf("Appied:%d", count);

}

int main() {
    swap();
    swap();

}

我们接着来看函数的返回值,并不是所有的函数都是执行完毕就结束了的,可能某些时候我们需要函数告诉我们执行的结果如何,这时我们就需要用到返回值了,比如现在我们希望实现一个函数计算a+b的值:

#include <stdio.h>
int add(int x, int y) {
    int z;
    z = x + y;
    return z;

}
int main() {
    int z = add(5, 4);
    printf("%d", z);
}

1.4、递归调用

我们的函数除了在其他地方被调用之外,也可以自己调用自己(递归)

#include <stdio.h>
void fun() {
    printf("hi");
    fun();
}
int main() {
    fun();
}

我们可以尝试运行一下上面的程序,会发现程序直接无限打印hi,这是因为函数在自己调用自己,不断地重复进入到这个函数,理论情况下,它将永远都不会结束,而是无限地执行这个函数的内容。

比如现在要求求某个数的阶乘

#include <stdio.h>
int fun(int n) {
    if(n == 1) return 1;
    return fun(n-1) * n;
}
int main() {
    printf("%d", fun(5));
}

1.5、斐波那契数列解法其三

前面我们介绍了函数递归调用,现在来看一个具体的实例,还是以斐波那契数列为例。

既然每个数都是前两个数之和,那么我们是否也可以通过递归的形式不断划分进行计算呢,我们依然可以借鉴之前动态规划的思想,通过划分子问题,分而治之来完成计算。

#include <stdio.h>
int F(int n) {
    if(n == 1 || n == 2) return 1;
    return F(n-1) + F(n-2);
}
int main() {
    printf("%d", F(7));
}

1.6、汉诺塔

什么是汉诺塔?

汉诺塔(Tower of Hanoi),又称河内塔,是一个源于印度古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始

按照大小顺序重新放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

这三根柱子我们依次命名为A、B、C,现在请你设计一个C语言程序,计算N阶(n片圆盘)汉诺塔移动操作的每一步。

我们可以对每一步进行推理

当汉诺塔只有1阶的情况下:直接把A上的圆盘移动到C

当汉诺塔只有2阶的情况下:我们的最终目标还是需要将A柱最下面的圆盘丢到C,不过现在多了圆盘,我们得先把这个圆盘给处理了,所以我们得把这上面的1个圆盘丢到B上去,这样才能把A最下面的圆盘丢给C。然后再把B上面的1个圆盘丢到C上去

当汉诺塔只有3阶的情况下:我们的最终目标还是需要将A柱最下面的圆盘丢到C,不过现在多了圆盘,我们得先把这个圆盘给处理了,所以我们得把这上面的2个圆盘丢到B上去,这样才能把A最下面的圆盘丢给C。然后再把B上面的2个圆盘丢到C上

#include <stdio.h>

void move(char start, char end, int n){
    printf("第%d个圆盘:%c --> %c\n", n, start, end);
}

void hanoi(char a, char b, char c, int n){
    if (n==1){
        move(a, c, n);
    } else {
        hanoi(a,c,b,n-1);
        move(a,c,n);
        hanoi(b, a, c, n-1);
    }
}

int main(){
    hanoi('A', 'B', 'C', 10);
}

1.7、快速排序算法

有一个数组:

int arr[] = {4, 3, 8, 2, 1, 7, 5, 6, 9, 0};

        现在请你设计一个C语言程序,对数组按照从小到大的顺序进行排序。这里我们使用冒泡排序的进阶版本--“快速排序”来完成,它的核心思想是分而治之,每一轮排序都会选出一个基准,一轮排序完成后,所以比基准小的数一定在左边,比基准大的数一定在右边,在分别通过同样的方法对左右两边的数组进行排序,不断划分,最后完成整个数组的排序。它的效率比冒泡排序的双重for循环有所提升。

#include <stdio.h>
//arr是数组,left是起始下标,right是结束下标
void quickSort(int arr[], int left, int right){
    if(left >= right) return;

    int base = arr[left], l = left, r = right;
    while(l < r) {
        while (l < r && arr[r] >= base) r--;
        arr[l] = arr[r];
        while (l < r && arr[l] <= base) l++;
        arr[r] = arr[l];
    }
    arr[r] = base;
    quickSort(arr, left, r-1);
    quickSort(arr, r+1, right);
}

int main() {
    int arr[] = {4, 3, 8, 2, 1, 7, 5, 6, 9, 0};
    quickSort(arr, 0, 9);
    for (int i = 0; i < 10; ++i) {
        printf("%d", arr[i]);
    }
}

二、指针

        2.1、初识指针

        我们知道,程序中使用的变量实际上都在内存中创建的,每个变量都会被保存在内存的某一个位置上(具体在哪个位置是由系统分配的),就像我们最终会在这个世界上的某个角落安家一样,所有的变量在对应的内存位置上都有一个地址(地址是独一无二的),而我们可以通过这个地址寻找到这个变量本体,比如int占据4字节,因此int类型变量的地址就是这4个字节的起始地址,后面32个bit位全部是用于存放此变量的值的。

我们来看看如何创建一个指针变量用于保存变量的内存地址

#include <stdio.h>

int main(){
    int a = 10;
    //指针类型需要与变量的类型相同,且后面需要添加一个*符号(注意这里不是乘法运算)表示是对于类型的指针
    int *p = &a;
    printf("a在内存中的地址为:%p", p);  //地址用%p表示
}

 拿到指针之后,我们可以很轻松的获取指针所指地址上的值:

#include <stdio.h>

int main(){
    int a = 10;
    //指针类型需要与变量的类型相同,且后面需要添加一个*符号(注意这里不是乘法运算)表示是对于类型的指针
    int *p = &a;
    printf("a在内存中的地址为:%p,值为:%d", p, *p);  //地址用%p表示
}

 注意这里访问指针所指向地址的值时,是根据类型来获取的,比如int类型占据4个字节,那么就读取地址后面4个字节的内容作为一个int值,如果指针是char类型的,那么就只读取地址后面1个字节作为char类型的值。


实际上拿到一个变量的地址后,我们完全不需要再使用这个变量,而是可以通过它的指针来对其进行各种修改。因此,现在我们想要实现两个变量的值进行交换的函数就很简单了

#include <stdio.h>

void swap(int *a, int *b){
    int mid = *a;
    *a = *b;
    *b = mid;

}
int main(){
    int a = 10, b = 20;
    swap(&a, &b);
    printf("a=%d, b=%d", a,b);
    
}

 当然,和变量一样,要是不给指针变量赋初始值的话,就不知道指的哪里了,因为指针变量也是变量,存放的其他变量的地址值也在内存中保存,如果不给初始值,那么存放别人地址的这块内存可能在其他地方使用过,这样就不知道初始值是多少了,所以一定要给个初始值或者将其设定为NULL,表示空指针,不指向任何内容。

下面我们来看看const标记指针

#include <stdio.h>

int main() {
    int a = 9, b = 10;
    const int * p = &a;
    *p = 20;  //这里直接报错,因为被const标记的指针,所指地址上的值不允许发生修改
    p = &b;  //但是指针指向的地址是可以发生改变的    

}

我们再来看另一种情况

#include <stdio.h>

int main() {
    int a = 9, b = 10;
     int * const p = &a; //const关键字被放在了类型后面
    *p = 20;  //允许修改地址上的值
    p = &b;  //但是不允许修改指针存储的地址值,其实就是反过来了。  

}

  


    2.2、指针与数组

前面我们介绍了指针的基本使用,我们来回顾一个问题,为什么数组可以以原身在函数之间进行传递呢?先说结论,数组表示法实际上是在变相地使用指针,你甚至可以将其理解为数组变量其实就是一个指针变量,它存放的就是数组中第一个元素的起始地址。

#include <stdio.h>
int main(){
    char str[] = "hello world";
    char *p = str;
    printf("%c", *p[0]);

}

        数组在内存中是一块连续的空间,所以为什么声明数组一定要明确类型和大小,因为这一块连续的内存空间生成后就固定了。

        而我们的数组变量实际上存放的就是首元素的地址,而实际上我们之前一直使用的都是数组表示法来操作数组,这样可以很方便地让我们对内存中的各个元素值进行操作

#include <stdio.h>
int main(){
    char str[] = "hello world";
    printf("%c", str[0]);

}

而我们知道实际上str表示的就是数组的首地址,所以我们完全可以将其赋值给一个指针变量,因为指针变量也是存放的地址:

char str[] = "hello world";
char *p = str;

 而使用指针后,实际上我们可以使用另一种表示法来操作数组,这种表示法叫做指针表示法

#include <stdio.h>
int main(){
    char str[] = "hello world";
    char *p = str;
    printf("第一个元素为:%c,第二个元素为:%c", *p, *(p+1));

}

比如我们现在需要表示数组中的第二个元素:

· 数组表示法:str[1]

· 指针表示法:*(p+1)

虽然写法不同,但是他们表示的意义是完全相同的,都代表了数组中的第二个元素,其中指针表示法使用了 p+1 的形式表示第二个元素,这里的 +1 操作并不是让地址 +1,而是让地址 + 一倍的对应的类型大小,也就是说地址后移一个char的长度,所以正好指向了第二个元素,然后通过 * 取到对应的值

 *p   //数组的第一个元素

p  //数组的一个元素的地址

p == str  //为真,都是数组首元素地址

*str  //因为str就是首元素的地址,所以这里对地址加*就代表第一个元素,使用的是指针表示法

&str[0]  //这里得到的实际上还是首元素的地址

*(p+1)  //代表第二个元素

p + 1  //第二个元素的内存地址

*p + 1  //注意*的优先级比+要高,所以这里代表的是首元素的值 + 1, 得到字符'K'

指针也可以进行自增和自减操作,比如:

#include <stdio.h>
int main(){
    char str[] = "hello world";
    char *p = str;
    p++;  //自增后相当于指针指向了第二个元素的地址
    printf("%c", *p);  //这里打印的就是第二个元素的值

}

   


    2.3、多级指针

        我们知道,实际上指针本身也是一个变量,它存放的是目标的地址,但是它本身作为一个变量,它也要将地址信息保存到内存中。我们还可以继续创建一个指向指针变量地址的指针,甚至可以创建更多级(比如指向指针的指针的指针)

int main(){
    int a = 20;
    int * p = &a; //指向普通变量的指针
    int ** pp = &p; //二级指针
    int *** ppp = &pp; // 三级指针
}

 


      2.4、指针数组与数组指针

前面我们了解了指针的一些基本操作,包括它与数组的一些关系。我们接着来看指针数组和数组指针,这两词语看着就容易搞混,不过哪个词在后面就是哪个,我们先来看指针数组,虽然名字很像数组指针,但是它本质上是一个数组,不过这个数组是用于存放指针的数组

int main(){
    int a, b, c;
    int * arr[3] = {&a, &b, &c};
}

 因为这个数组中全都是指针,比如现在我们想要访问数组中第一个指针指向的地址:

#include <stdio.h>
int main(){
    int a, b, c;
    int * arr[3] = {&a, &b, &c};

    *arr[0] = 999;  //[]运算符优先级更高,所以这里先通过[0]取出地址,然后再使用*将值赋到对应的地址上
    printf("%d", a);
}

数组指针,可以看到指针在后,说明本质是一个指针,不过这个指针比较特殊,它是一个指向数组的指针(注意它的目标是整个数组,和我们之前认识的指针不同,之前认识的指针是指向某种类型变量的指针)

int * p;   //指向int类型的指针
int (*p)[3];   //数组指针,这里需要将*p括起来,因为[]的优先级更高
int arr[3] = {111, 222, 333};
int (*p)[3] = &arr;   //直接对整个数组再取一次地址
#include <stdio.h>
int main(){
    int arr[3] = {111, 222, 333};
    int (*p)[3] = &arr;   //直接对整个数组再取一次地址

    printf("%d, %d, %d", *(*p + 0), *(*p + 1), *(*p + 2));
}

· p代表整个数组的地址

· *p表示所指向数组中首元素的地址

· *p+i 表示所指向数组中第 i 个(0开始)元素的地址(实际上这里的 *p 就是指向首元素的指针)

· *(*p + i)就是取对应地址上的值

      


  2.5、指针函数与函数指针

我们的函数可以返回一个指针类型的结果,这种函数我们就称为指针函数

#include <stdio.h>
//函数返回值类型是int *指针类型的
int * test(int * a){
    return a;
}

int main(){
    int a = 10;
    int * p = test(&a);  //使用指针去接受函数的返回值
    printf("%d", *p);
    printf("%d", *test(&a));  //当然也可以直接把间接运算符在函数调用前面表示直接对返回的地址取地址上的值
}

函数指针,实际上指针除了指向一个变量之外,也可以指向一个函数,当然函数指针本身还是一个指针,所以依然是用变量表示,但是它代表的是一个函数的地址(编译时系统会为函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址)

#include <stdio.h>
//函数返回值类型是int *指针类型的
int sum(int a, int b){
    return a + b;
}

int main(){
    // 类型(*指针变量名称)(函数参数....) 注意一定要把*和指针变量名称括起来,不然优先级不够
    int (*p)(int, int) = sum;
    int result = p(1, 2);
    printf("%d", result);
}

有了函数指针,我们就可以编写函数回调了(所谓回调就是让别人去调用我们提供的函数,而不是我们主动来调别人的函数),比如现在我们定义了一个函数,不过这个函数需要参数通过一个处理的逻辑才能正常运行:

#include <stdio.h>
int sum(int (*p)(int, int), int a, int b){
    return p(a, b);
}
//函数返回值类型是int *指针类型的
int sumImpl(int a, int b){
    return a + b;
}

int main(){
    // 类型(*指针变量名称)(函数参数....) 注意一定要把*和指针变量名称括起来,不然优先级不够
    int (*p)(int, int) = sumImpl;
    printf("%d", sum(p, 10, 20));
}

     

猜你喜欢

转载自blog.csdn.net/m0_60964321/article/details/128679632