(四)C语言零基础入门 --- C语言之入门课程

        老规矩,在每节课上课之前,我们先来回顾一下上节课的内容,上节课我们讲了函数,数组,局部变量和全局变量,以及C语言中的存储类。 

        一、函数

        函数如何定义?函数如何声明?声明的作用是什么?函数如何调用?函数的传值调用的执行机制又是什么?这些东西,希望大家能够全部理解。

        二、数组

        一维数组如何定义?二维数组如何定义?一维数组在内存的存储方式是怎样的?一维数组和二维数组如何初始化?怎么去调用一维数组和二维数组的元素?数组和函数结合,有什么用法?数组和指针结合又有什么用法?

        三、存储类

        C语言中存储类关键字有哪几个?auto,register,static,extern这几个存储类关键字又有什么作用?C语言的局部变量和全局变量的区别又是什么?函数的形式参数生命周期是什么范围?

以上就是我们上节课讲述的内容。今天我们接着讲剩下一部分,指针,函数指针和回调函数,字符串。

一、指针

        很多初学者,总是觉得指针很难,难以理解,看到指针就觉得头痛。其实,如果真正理解了指针,你就会觉得,指针是个非常好用的工具,能帮助我们简化很多代码。今天,我们就来详细的讲一讲,指针是什么?

        1.指针的概念

        我们在之前已经讲过,计算机的内存存储,是由一个个存储器单元构成的,每个存储器单元能够存储8位数据,为了区分每个不同的存储器单元,所以我们给每个存储器都进行一个编号,大家还记得吗?这个编号就叫做地址,我们可以通过地址直接对某个或者某几个存储器单元进行读写过程。还有,我们在讲运算符一节时,我们跳过了两个运算符没有进行详细讲解,大家都还记得吗?哪两个运算符?大家回去看看就知道了,一个运算符是*,这个运算符用于在指定地址里取值出来,还有一个运算符是&,这个运算符用于取变量地址

        我们首先来介绍取地址运算符&和取地址值的运算符*,我们来看个例子:

#include <stdio.h>

int main(void)
{
    int a = 1;
    
    printf("&a = %p\r\n",&a);
    printf("*(&a) = %d\r\n",a);

    return 0;
}

在这个例子中,我们定义了一个变量,名字叫做a,这个变量的类型是int类型。我们之前说过,定义变量的实质,就是在内存中寻找一块存放地址。我们用变量名,就能够轻易操控这块地址的内容。我们这里的变量a,它有一个在内存中存放的地址,我们怎么获取它到底存放在哪儿的呢?很明显,我们这里使用了一个&符号,这个符号就叫做取地址符号,用于取某个变量的存放地址。在下列输出中,很明显能看到,变量a存放的地址是0x000000000061FE1C,因为本人是64位操作系统,所以系统的地址是64位的,也就是八个字节。通过&运算符,我们就获得了某个变量存储的位置,也就是地址。我们再来看,在下面一句话中,我们使用了另一个运算符*,我们首先通过&运算符得到了a变量的地址,然后使用*运算符,得到了地址里的内容。由此可以看出,*运算符,叫做取值运算符,用于获取某个地址里的内容。这里我们的内容就是我们定义变量时的值。

 好了,&运算符和*运算符我们现在已经讲完了。不是说讲指针吗?怎么还不讲呢?其实,我们已经讲完了,指针实际上就是内存地址,我们利用不同的地址,就能访问不同的内存空间。这就是指针的应用,通过物理地址的访问,对我们的变量进行读写。

        2.指针变量

        在之前,我们已经介绍了C语言中的一些内置变量,整形变量,浮点型变量,数组类型。今天我们又要拓展另外一种变量,叫做指针变量。众所周知,我们之前定义的变量,里面存储的都是实际的变量值;但是,指针变量在C语言中是一种特殊的存在,它保存的不是变量的值,而是变量的地址。也就是说,用指针变量,能够保存变量地址,我们就能用指针变量进行更灵活的操作。首先我们来讲一下指针变量的定义和使用,来看一个例子:

#include <stdio.h>

int main(void) {
	int a = 100;
	int *p;

	p = &a;

	printf("&p = %p\r\n", &p);
	printf("p = %p\r\n", p);
	printf("*p = %d\r\n", *p);

	return 0;
}

        在这个例子中,我们定义变量时,出现了一种新的定义方式: int *p,这种定义方式,就是用于定义指针变量。我们在指定变量类型后,添加一个*号,代表我们定义的这个变量为指针变量,指向的数据类型是你定义的类型。指针变量也有对应的数据类型,定义了类型之后,这个指针变量,只能指向相同类型的变量地址。不能让其指向其他类型的变量,否则编译器会报错。为什么不行,用一张图就能搞懂,如下图所示:

        图中清晰的展示了为什么,使用指针变量指向不同类型是不允许的,指针变量应该指向对应类型的变量地址。

        3.NULL指针

        NULL指针又被称为空指针,说明指针现在不指向任何地方。为什么要有NULL指针呢?我们说了,我们在定义变量时候,必须要对变量进行初始化,除非你有十足的把握,保证定义变量的数据为你想要的数据。如果不对变量的值进行初始化,则变量的值是不可预测的,如果我们定义了一个指针,则指针的指向也是不可预测的,我们将其称为野指针。野指针,就是指向不定的指针变量,使用野指针有很大可能会造成程序的崩溃。所以,现在有个特殊的指针,就叫做NULL指针,即空指针,表示这个指针变量不指向任何地方,实际上,这个空指针的地址为0,也就是说,我们把地址为0的指针也称为空指针。NULL指针的出现,就是为了在你初始化的时候,如果不确定指针的指向,我们就把NULL指针赋予给它,防止后面的程序设计疏忽使用野指针,导致程序崩溃。下面是赋予NULL指针的一个例子:

int *p = NULL;

        在这里,我们定义了一个p指针,指向int类型变量,初始化,我们让这个指针指向NULL,代表现在这个指针不指向任何地方,是个空指针。

        4.指针变量的算术运算

        我们定义的整形变量、浮点型变量,都可以进行加减乘除的运算,那指针变量可以进行算术运算吗?当然可以,但是指针变量的运算只有加减运算,没有其他的运算。我们来看看指针运算的例子:

#include <stdio.h>

int main(void)
{
    int i = 0;
    int *p;
    int a[10] = {
                    1,2,3,4,5,6,7,8,9,10
                };
    p = a;
    
    for(i=0;i<10;i++)
    {
        printf("*p = %d\r\n",*p);
        p++;
    }

    return 0;
}

        执行结果如下图所示

        通过以上一个例子,充分展示了指针的加减运算,在上面的例子中,我们定义了一个指针p,其指向的类型为int类型,那么p一次所指向的是4字节内存空间。这里,初始化,我们让p指向int类型的数组a,那为什么可以直接使用p = a;语句呢?我们在这里解释一下,C语言中我们所定义的数组,其实也是一个指针,数组名其实就是一个指针,也就是一个地址,只不过数组名属于指针常量,一旦定义了数组后,这个数组指针的指向就是确定的,不能再发生改变。因此我们在这里可以直接将a(也就是数组名)赋值给对应类型的指针变量。也就是说,现在p指向a的第一个元素,在for循环中,每次打印出p指针指向地址的值,然后进行一次p++操作,也就是说,让我们的指针p指向下一个四字节地址,这样,每次指向一次p++操作后,p就指向a数组的下一个元素。我们使用p--操作就是指向数组上一个元素。这个就是指针的算术运算,只能进行加减运算,当然,不限于p++,我们还可以这样执行p += 9;也就是说,p指针现在的指向向后移动九个单位(这里的单位要根据数据类型决定)

        5.指针数组

        什么是数组?数组就是保存多个同一类型变量的数据类型,那指针数组又是什么呢?顾名思义,数组保存的数据类型是指针类型,我们把这种数组叫做指针数组。指针数组定义方式如下:

int* a[10];

        这样我们就定义了一个数组,这个数组比较特殊,它保存的数据类型是指针类型的数据,这个指针类型指向的是int类型的变量。这个数组一共能够保存10个指针变量。

        同样,指针数组也可以进行初始化,初始化的时候注意,要把对应的地址赋予给它才行。

        6.函数的地址调用

        我们之前说了,函数的参数执行机制,是在进入函数前,将参数进行拷贝一份,然后将代入的值给拷贝的参数。这种方式,叫做传值调用。我们这里的地址调用,其实就是将指针作为函数的参数。我们同样拿上次的两数交换的函数做例子:

#include <stdio.h>

void swarp(int*,int*);

int main(void)
{
    int a = 10,b = 20;

    printf("交换前: a = %d,b = %d\r\n",a,b);

    swarp(&a,&b);    

    printf("交换后: a = %d,b = %d\r\n",a,b);

    return 0;
}

void swarp(int* a,int* b)
{
    int temp;

    temp = *a;
    *a = *b;
    *b = temp;

}

上述代码执行结果如下图所示:

         这次执行结果,怎么和我们上次讲的执行结果不一样呢?让我们来看看swarp函数是如何定义的,swarp函数,传入的参数是什么?是指针,也就是地址,我们将各个变量的地址传递进去。那函数中的运算,针对的都是指定地址里的数据进行运算。所以,在执行完之后,外部代入的变量,它们的地址是不变的,而在函数内,对应地址里的值已经发生了改变,这才达到了两个数交换的目的。这就是传值调用和地址调用的区别,请大家,务必搞清楚这一点。

         同时,上述例子,也讲述了一种指针的用法,就是将指针作为函数的参数代入,接下来我们讲另一种指针的用法。

        7.指针作为函数的返回值

        指针也是一种数据类型,当然可以作为函数的返回值,我们来看看,一个函数如何返回一个指针变量出来。

int* function(void)
{
    int* p = NULL;
    return p;
}

        这是一个简单的例子,如果想要返回指针,我们直接在返回的数据类型后加一个*就好,这里的*号为标识符号,表示返回的数据类型是指针类型的数据。这个函数返回了一个NULL指针。

        8.函数指针与回调函数

        指针在C语言的应用中及其广泛,原因之一也是因为C语言中有函数指针。那什么叫做函数指针呢?函数指针指的是,一个指针指向一个函数,我们调用这个指针的时候,会自动执行这个函数的内容。我们来看个函数指针的例子:

#include <stdio.h>

int (*point_add)(int *, int *);
int add(int *, int *);

int main(void) {
	int result, a, b;
	point_add = add;

	a = 10;
	b = 20;
	result = point_add(&a, &b );

	printf("result = %d\r\n", result);
	return 0;
}

int add(int *a, int *b) {
	return *a + *b;
}

        在这个例子中,我们编写了一个add加法函数,其两个参数均为int类型的指针,返回值为int类型的数据。但是,我们来看主函数,我们并没有用到add加法函数,而是用到了point_add函数,我们暂时先把它说成函数,我们先来看看结果。

        结果等于a和b的和,同样执行的是加法运算,为什么呢?这是因为,我们在这里,定义了一个函数指针,point_add,这个指针指向了add函数,我们在调用这个指针的时候,就自动执行add函数中的内容。那这个指针指向的是什么呢?我们来看指向的函数的参数,两个都为int*类型,返回值为int类型。这个不正好与我们编写的add函数的参数和返回值都是一样的吗?的确是这样的,想要定义一个函数指针,首先需要确定,我们们需要这个指针指向哪个函数,把指向的函数的参数类型以及返回值类型都确定,然后按照如下格式声明

return_type (* name)(par_type,.....);

return_type为我们需要指向函数的返回值;

name为我们需要定义函数指针的名字;

par_type为函数的参数列表,一定要和指向的函数参数类型一一对应;

另外别忘了,在函数名字前加一个*符号,然后将*和名字一起用小括号括起来。

这样,我们就定义了一个函数指针,用于指向某个函数。

那么函数指针有什么用呢?我们来看一个简单的例子:

#include <stdio.h>

float (*pointToType)(float, float );

float add(float a, float b);
float decrease(float a, float b);
float multiply(float a, float b);
float divide(float a, float b);

int main(void) {
	float input_a, input_b;
	char input_type;
	float result;
	printf("请按格式输入: 数字a,数字b,执行符号\r\n");
	scanf("%f,%f,%c", &input_a, &input_b, &input_type);

	if (input_type == '+') {
		pointToType = add;
	} else if (input_type == '-') {
		pointToType = decrease;
	} else if (input_type == '*') {
		pointToType = multiply;
	} else if (input_type == '/') {
		pointToType = divide;
	}

	result = pointToType(input_a, input_b);

	printf("result = %f\r\n", result);

	return 0;
}

float add(float a, float b) {
	return a + b;
}

float decrease(float a, float b) {
	return a - b;
}

float multiply(float a, float b) {
	return a * b;
}

float divide(float a, float b) {
	if (b == (0.0f)) {
		return 0;
	}
	return a / b;
}

 这个例子,充分说明了,函数指针的用处,在这个例子中,虽然我们只调用了一个函数指针,但是函数指针的指向是可以改变的,我们可以在程序中,随意改变函数指针的指向,从而达到一个函数名就能执行不同的逻辑的目的。这在C语言中是可以相当灵活的,因为我们不再拘泥于,逻辑代码写完后,所有的函数执行代码都定死了,我们可以通过函数指针,灵活的切换函数的执行代码。以下是我们的执行结果。

 另外,函数指针和普通指针一样,可以当作函数的参数使用。我们举一个例子

void function(int (*f)(int,int))
{

}

        这个函数中,我们只有一个参数,那就是一个函数指针,参数名叫做f,指针指向的是一个由两个int类型参数和int类型返回值的函数。我们把这种用法,叫做回调函数调用,就是说,我们在另一个函数中,调用了函数指针参数指向的函数,回调函数的使用,在嵌入式开发应用及其广阔。请务必掌握函数指针的用法。

      二、字符串

        字符串的用法相对来说比较简单,大家可以看C 字符串 | 菜鸟教程 (runoob.com)自行理解。字符串,实际上就是一个字符型数组。

 

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值