c指針進階

轉載:https://blog.csdn.net/u012741741/article/details/66478883

指针数组&数组指针

先理解一下什么是指针数组和数组指针,指针数组的实质是个数组,这个数组中存储
的元素都是指针变量,而数组指针实际上是一个指针,这个指针指向一个数组。通过
表达式区分不同

指针数据和数组指针的表达式有一定的区别,关键在于号的位置,如果号和变量结合
性强,表示是指针,否则,就是数组,所以,符号的优先级在这里非常重要,决定了
两个符号同时和变量结合时,谁先运算的问题。

查优先级表可以知道,[]的优先级高于号,所以,举例来说:int *p[5];这段代码中,
p就是一个指针数组,而int (*p)[5];中,p由于先和结合,所以p是一个数组指针;
函数指针和typedef

函数指针,实质上还是一个指针变量,所以函数指针变量也占4个字节,和普通指针区
别就在于指针指向的变量类型不同。
函数指针的实质

函数实质上是一段代码,这段代码在内存中是连续的,一个函数体中的所有语句编译后
生成的可执行程序在内存中是连续分布的,所以函数中第一句代码的地址,就是函数的
地址,在C语言中使用函数名来表示,由于函数指针实质就是一个普通的变量,类型是
函数指针,值就是某个函数的地址,也就是函数名这个符号在编译器中的值。
函数指针的书写和分析

由于C语言本身是强类型语言,所以需要明确指针的类型,所以在书写函数指针的时候
要按照要求来,这样编译器才会知道这个指针所指向的变量的类型,函数指针一般书写
为“返回值类型 (*指针变量名) (参数类型)”,例如int (*p) (double);。
typedef

在C语言中,可以使用typedef来定义类型,C语言可以分为两种类型,一种是C语言自带
的原生类型,一种是使用typedef定义的自定义类型,函数指针也是一种自定义类型,
如果函数指针定义非常繁琐,为了简化书写,可以使用typedef来自定义该函数指针类型
为一个简单易写的名称,需要注意的是,typedef定义出来的都是类型,而不是变量,
typedef的使用如下:

typedef char* (*pType)(char *, const char *);


上面定义了一个pType类型的函数指针。
函数指针

函数指针是如何使用的呢?
函数指针的调用

定义如下函数指针:

typedef int *(pFunc)(int,int);


指向一个返回值为int,参数为两个int的函数,则如果有函数是:

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


则可以定义函数指针变量,并进行调用:

// 创建函数指针变量
pFunc p1 = NULL;
// 赋值
p1 = add;
// 调用
int result = p1(1,1);


这样相当于调用了add函数进行加法运算,如果用该函数指针指向不同的函数,就可以实现
不同的结果,只需要该函数的参数类型和返回值类型符合该函数指针的定义即可。
结构体内嵌指针

使用结构体内嵌指针,可以实现程序的分层设计。
程序分层

我们拿计算器程序为例,先创建一个框架文件,命名为framework.c,再创建具体的计算器实
现文件cal.c,这就分成了两个层次,一个是框架层,一个是实现层,两个层次可以由不同的
人员完成,通过调用组合来共同工作,cal.c直接完成计算器的工作,但是其中的关键部分
是调用framework.c中的函数来完成的,framework.c的代码如下:
头文件定义

#include "cal.h"
// framework.c中应该写实际业务关联的代码

// 计算器函数
int calculator(const struct cal_t *p){
    return p->p(p->a, p->b);
}

定义cal.c的头文件cal.h,作为头文件的联系纽带:

#ifndef __CAL_H__
#define __CAL_H__

typedef int (*pFunc)(int, int);

// 结构体是用来做计算器的,计算器工作时需要计算原材料
struct cal_t
{
    int a;
    int b;
    pFunc p;
};

// 函数原型声明
int calculator(const struct cal_t *p);

#endif

实现文件

实现层包含具体实现计算步骤的函数:

#include "cal.h"
#include <stdio.h>


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

int sub(int a, int b)
{
    return a - b;
}

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

int divide(int a, int b)
{
    return a / b;
}

int main(void)
{
    int ret = 0;
    struct cal_t myCal;

    myCal.a = 12;
    myCal.b = 4;
    myCal.p = divide;

    ret = calculator(&myCal);
    printf("ret = %d.\n", ret);

    return 0;
}

 
在结构体中,pFunc是一个函数的指针,将这个结构体变量传入到 calculator这个函数中之
后,会被调用,从而完成运算。

经过分层设计实现了解耦,每个层次专注各自的领域和任务,不同层次之间使用头文件交互,
上层为下层提供服务,上层的代码在下层中被调用,上层注重实现业务逻辑,与目标相关联但
是没有具体实现,下层注重直接工作,为上层填充实例并将实例传递到上层中的接口当中,来
调用接口函数,下层代码中核心是结构体变量的填充,将结构体变量传到接口中即可实现对应
的功能。
再论typedef

C语言中有两种数据类型,內建类型和用户自定义类型,使用typedef就可以实现用户自定义类
型,typedef操作的是类型,而不是变量,typedef和#define是有区别的,#define定义出来的
是一个宏,在宏扩展阶段进行文本替换,而typedef是在编译阶段,进行类型的重新定义,例
如typedef char *pChar;,与#define pChar char *,就是这种区别。
typedef和结构体

结构体在定义和使用时,需要先定义结构体类型,然后使用结构体类型来定义变量,例如,有
如下结构体:

struct student {
    char name[20];
    int age;
}

则可以定义student的结构体变量struct student s1;,struct student合在一起是变量s1的类型。

为了简化书写,我们可以在定义结构体的时候,使用typedef,

typedef struct student {
    char name[20];
    int age;
} student_t;

   

这样,就把这个结构体重新定义成了student_t类型,以后再定义变量的时候,就可以这样写了
student_t s1;,可见,typedef简化了我们编程时类型的书写。
定义结构体指针

有时我们也需要直接定义结构体变量的指针,如下

typedef struct teacher {
    char name[20];
    int age;
    int mager;
} teacher, *pTeacher;

   

通过这样,一次定义了三个类型,结构体类型struct teacher,teacher以及结构体指针类型
struct teacher *pTeacher;则变量可以如下定义:

teacher t1;
pTeacher p1 = &t1;


可见指针类型是可以被结构体变量赋值的。
typedef和const

假设有typedef int *pInt;,我们定义了一个pInt类型,如果有以下定义:

int a = 1;
pInt p1 = &a;

    

则是完全可以理解的,但是,如果是写成这样const pInt p2= &a;,则结果是怎么样呢?这相当于
int *const p2;,也就是说,指针p2为const的,但是其指向的变量的值是可以变的,这点需要搞
清楚;

那么如果写成这样呢?pInt const p2 = &a;,效果其实和上边是一样的。

typedef通常用于简化类型的描述和书写,产经就是上面第一个例子的场景,来重命名或者叫做重定
义一个比较复杂的类型为一个比较简短或者容易书写理解的类型,另外,typedef也通常用于定义与
平台无关的类型,C语言內建类型通常和平台紧密相关,例如int在32位机器上是32bit,在64位的机
器上是64bit,为代码的移植造成了很大的影响,为了解决这样的问题,可以使用typedef来定义一
些中间类型来进行转换,例如C语言中就有这种定义:

typedef int size_t;


使用size_t来替代int,以屏蔽不同平台上int所占字节不同的差异。
二重指针

二重指针本质上来说,和一重指针没有什么不同,本质都是指针变量,而指针变量的本质就是变量,
所以追根究底是一样的。
二重指针概念

一重指针和二重指针本身都是占用4个字节的内存空间,都可以指向另外一个变量,例如char **p1;
就是一个二重指针,表示的是指针指向的变量是一个char *类型,也就是说指针指向了另外一个指
针,在由另外一个指针指向一个变量。

二重指针和普通指针的差别就在这里,它所指向的变量类型是一重指针,二重指针其实也是一种数据
类型,编译器在编译时会根据二重指针来做静态类型检查,如果发现数据类型不匹配,编译器就会报错。
C语言中的二重指针

C语言中的一重指针完全可以承担二重指针的任务,但是某些任务或者变量,使用二重指针会更加灵活
和方便,指针被定义时,使用数据类型来标记,编译器通过该数据类型进行类型检查,可以帮助编程者
发现潜在错误
二重指针的用法

二重指针必须指向一重指针的地址,也可以指向指针数组,我们知道数组名做右值的时候,实质就是数
组首元素的地址,就是一个指针,所以数组指针可以被看做是一个二重指针,因为数组首元素就是一个
int*类型,再用一个指针指向它,就是二重指针了。
二维数组

单从内存角度来看的话,二维数组和一维数组本质上没有任何区别,甚至二维数组都可以用一维数组来
表示,而且在内存使用和访问效率上是一样的,但是在某些情况下,二维数组比较容易理解,代码利于
组织。例如进行矩阵操作,平面坐标轴操作等等。

二维数组的第一维本身也是个数组,它的元素的数据类型是数组类型,第二维本身也是个数组,它的元
素类型是普通元素,第二维的数组本身作为第一维数组的元素存在于第一维数组中。
二维数组的访问方式

二维数组中的元素可以通过下标以及指针两种方式来访问,假设有二维数组:int [2][5];则可以定义
指针int **p = a;,访问等价表示如下:

    a[0][0]等价于*(*(p + 0) + 0)
    所以有a[i][j]等价于*(*(p + i)+ j)

二维数组的指针及其运算

二维数组的运算分为三种情况。
指针指向数组名

假设如下代码:

int[2][5] a= {{1,2,3,4,5},{6,7,8,9,0}};
int (*p)[5] = a;

    
则二维数组的指针书写如上所示,表示p是一个数组指针,该数组指针指向一个数组,数组中有5个int类
型的元素。

a是二维数组的数组名, 做为右值时表示第一维数组首元素的首地址(其实就是数组的地址),也就是
&a[0],所以其实可以写成int (*p)[5] = &a[0];,但是&a[0]又等同于a,所以就写成那样了。其实都代表
数组的首地址,至此,我们就可以使用指针的方式来访问数组中的各个元素,从而获取其中的元素来参
与运算。

由此可见,二维数组的数组名表示的是第一维数组首元素的地址,第一维数组首元素也就是第二维数组的
地址,和一维数组的符号含义是相同的,所以不要认为我们是使用一维数组的指针来指向二维数组,其实
本质上还是指向的一个数组,只不过这个数组中的元素是数组罢了。
指向二维数组的第一维

再进一步,如果想要表示第二维中某个元素的地址,可以这样写int *p1 = a[0];,因为我们知道a[0]表示
第二维整体数组的数组名,又等于第二维整体数组的首元素地址,也就等价于&a[0][0],同理第二维中第
一个数组的第四个元素表示为*(p1 + 4);。
指向二维数组的第二维

如果想用指针指向二维数组的第二维,严格来说是不可以的,因为二维数组的第二维中的元素其实就是基
本类型了,不能用指针类型进行赋值了,除非对该元素进行取地址操作,例如int *p = &a[1][3];,这样
就和直接用下标访问没什么区别了。

二维数组和指针关键在于:

    .数组中各个符号要弄清楚。
    .二维数组的指针式访问要弄清楚,不要从简单的书写方式上来理解,要从指针的特性上来理解以及数
    组地址,数组首地址,二维数组和一维数组的地址来理解。

猜你喜欢

转载自blog.csdn.net/qq_40618124/article/details/81189344
今日推荐