浅谈指针

笔记记录于小甲鱼的课程,十分感谢!

指针是C语言的精髓部分。有一句话说:对于指针的掌握程度决定了你对C语言的编程能力。

数据在内存中是如何存取的

我们知道如果我们在程序中定义一个变量,那么程序在进行编译的时候,系统就会给不同的变量类型分配相应长度的内存空间。我们如何访问这个变量呢?通过变量名!这是我们学习编程开始对内存的最基本认知,但这对于内存的知识还是不够的。

那么在内存中是如何存储数据的呢?

内存的最小索引单元是一个字节。
内存中每一个最小单元的索引叫做存储地址
每个存储地址可以存放一个字节的数据。

如:整型变量在内存中需要4个字节的存储单元,所以这四个存储单元都会有一个地址。看下图应该会有点感悟:

看到这里会不会有疑问?那么变量名是怎么回事呢?

其实在内存中是完全没有必要存放变量名的,变量名是为了方便程序员的使用而定义的。而编译器又知道具体的变量名对应的存储地址。

所以当你读取每个变量的时候。编译器就会通过变量名,并根据变量的类型先找到变量名对应的首地址(像是查表似的,编译器知道变量名对应的地址表是什么),然后读取相应长度的数据(即首地址往下连续的长度数据)

指针和指针变量

我们平时说的指针实际上就是地址的意思!

指针变量:专门存储地址的变量

普通变量:存放的是数据

指针变量也有类型,它的类型是存放地址指向的内存单元中的数据的类型
PS:因为不同数据类型所占的内存空间不同,如果指针变量的类型错误就会以为着在访问指向的数据时候读取的内存最小单元的长度就会不同,导致获取的结果出错。

注意:一个指针在编译系统里占四个或八个(根据编译系统而定)字节的存储单元

如何定义指针变量呢?

格式:类型名 *指针变量名

如:char *a; // 定义一个指向字符型的指针变量

取址运算符和取值运算符

  • 如果需要获取某个变量的地址,可以使用取址(地址)运算符(&)
    如:char *b = &a;

  • 如果需要访问指针变量指向的数据,可以用取值运算符(),也叫间接操作符(因为是通过地址间接去取值)
    ps:跟定义指针时运用的是同一个符号,属于符号的重用,在不同的地方有不同的意义
    如:printf("%c, %d\n", *pa, *pb); // 这里的
    就是取值运算符,取指针变量指向的那个值

    代码示例:

输出结果:

注意:要避免访问为初始化的指针,也叫野指针。因为在局部变量在栈中的地址是随机分配的,那么野指针指向的地址就是随机的,你往一个随机的地址进行赋值操作,是有可能覆盖掉系统的关键代码而出现意想不到的错误!

指针和数组的关系

  • 数组名其实是数组第一个元素的地址
  • 当指针指向数组元素的时候,我们可以对指针变量进行加减运算,这样做的意义相当于指向距离指针所在位置向前或向后第n个元素(对比常规的下表访问数组的元素方法,这种使用指针进行间接访问的方法叫做指针法)
    ps:p+1并非简单地将地址加1,而是指向数组的下一个元素

指针和数组的区别:

  • 指针和数组名同样指向的是一个地址
  • 指针是一个左值(lvalue,即可改变的值)
  • 数组名是一个地址常量,它不是一个左值(lvalue),即不可改变的值。

指针数组和数组指针

数组的运算符[]的优先级大于指针运算符*

故:

指针数组:是一个数组,每个数组元素存放一个指针变量
ps:数组中的每一个元素变量因为是指针类型,所以一定要注意初始化,指针数组的初始化可以直接使用字符串作为元素,因为字符串变量实际上就是字符数组的指针

数组指针:是一个指针,它指向的是一个数组
ps:要注意的是,数组名和数组地址以及数组的首元素地址实际上都等于首元素地址,但是它们的概念不一样。在给数组指针初始化的时候需要赋给它的是数组的地址,虽然平时我们直接使用数组名就表示数组的地址了,但是编译器在初始化数组指针的时候需要给数组地址一个明确身份,所以需要给数组名前面加上“&”符号(这就给了数组地址一个身份)。这里因为是重复取址了,故在取值的时候,需要使用两个"*"号才能成功取值

指针和二维数组

二维数组实际上就是一维数组的元素也是数组。

定义了一个二维数组int array[5][5]:

  • *(array+1)我们叫做array+1的和解引用(取出对应地址所存储的值)
  • *(array+1) == array[1],这是C语言的语法糖(对于编程语言来说没有增加新的技能,通过增加一个语法来方便程序员的使用,就是一种写法的另外一种便捷形式)

结论:无论是一维数组还是二维数组甚至多维数组也罢,都满足:下图所示的公式:

即:我们的下标索引取值的形式都可以转换为指针取值的形式。因为在C语言中多维数组实际上都是由一维数组进行线性扩展而得来的。

void指针和Null指针

void指针:也称为通用指针,可以指向任意类型的数据。也就是说,任意类型的指针都可以赋值给void指针
ps:不要直接对void类型指针进行解引用,因为编译器并不聪明,搞不懂void指针指向的数据是什么类型的,就无法按指定类型的大小取数据

Null指针:在C语言中一个指针不知向任何数据我们就把这样的指针成为空(Null)指针。
ps:Null在C语言中是一个宏定义,它指向的是系统的0地址(该地址一般不被使用)#define Null((void *)0)。当你还不清楚要将指针初始化为什么地址的时候,请将它初始化为Null;在对指针进行解引用时,先检查该指针是否为Null。这种策略可以为今后编写大型程序节省大量的调试时间(程序会报错,野指针可能会随便指向一个合法的地址,导致程序不报错,给排查造成很大的困难),这是一种良好的变成习惯。

注意:

指向指针的指针

可以发现C语言在定义的时候就已经交代了如何解引用了,如上图*p即可得到int的值,而**p才能得到int的值。

用处:指向指针的指针用于指向指针数组的元素有下面几个好处:

  • 避免重复分配内存
  • 只需要进行一处修改
  • 代码的灵活性和安全性都有了显著地提高!

常量和指针

变量和常量最大的区别是值是否可以改变。变量标志着内存位置的名称,通过这个名称可以找到该位置,然后把里面值换成其他值。而常量定义后该值就不可以被改变

在C语言中,可以使用const关键字去修饰变量,使具有常量一样的特性,即把变量变成只可读,不可写与修改

当我们定义一个指向常量的指针(例:const int *p = #),即指针指向一个被const修饰的变量时,该指针不能通过解引用的形式修改变量的值,这与不能直接对变量进行值修改是一样的,但是我们可以修改指针的指向,指向别的值。简单来说,就是指针的指向可以改变,指针的指向的值不可以改变。
ps:改变指向的值可以不是一个被const修饰的变量,但同样通过解引用的方式修改值是被禁止的。

指向常量的指针总结:有以下特点:

  • 指针可以修改为指向不同的常量
  • 指针可以修改为指向不同的变量
  • 可以通过解引用来读取指针指向的数据
  • 不可以通过解引用修改指针指向的数据

指向常量的指针不能改变的是指针指向的值,但指针本身可以被修改。如果我们想让指针也不可以变,那我们可以使用常量指针。同样使用的是const关键字,但是位置发生了一些变化,它放在了“*”号的后边,如:
int * const p = #

所以,如果把const放在*号的左边,则是一个指向常量的指针,放在右边则是一个常量指针。常量指针的指针本身不能改本,而指针所指向的值由变量类型决定是否能改变(这一点与指向常量的指针不一样,指向常量的指针规定无论指针指向的指是否为const修饰,都不可改变)

常量指针总结:有以下特点:

  • 指向非常量的常量指针
    • 指针自身不可以被修改
    • 指针指向的值可以被修改
  • 指向常量的常量指针
    • 指针自身不可以被修改
    • 指针指向的值也不可以被修改

指针函数和函数指针

函数的类型事实上就是函数的返回值

当一个函数返回值是指针类型的数据,该函数就叫做指针函数,它的定义格式:int *p();看下面代码例子:

上图中可以看到,通常情况下,我们是没有一个类型来定义字符串的。都是使用char类型的指针来定义字符串,因为char类型的指针实际上指向的是一个字符。我们用char类型的指针指向字符串的第一个字符,而C语言中约定俗成,字符串的读取截止于\0这个空字符。所以知道第一个字符就知道了整个字符串。图中使用指针变量作为函数的返回值,就是指针函数了。

PS:不要返回局部变量的指针(地址)
函数内部定义的变量我们就叫做局部变量,局部变量的作用域仅限于函数内部,所以当我们返回局部变量的地址的时候,我们main函数中调用完指针函数后,指针函数中定义的变量已经销毁掉了,这是就是函数的生命周期。故外部接收到如果是局部变量也会跟着函数烟消云散。
但是如上图所示,我们返回的是一个字符串,没有定义到一个变量里面去。这是可以成功返回的,这是因为字符串在C语言里面是比较特殊的,它会去找到固定的一个存储区域,而非函数里面。


函数指针顾名思义就是一个指向函数的指针,定义格式:int (*p)()

C语言中函数名是经过运算后得到的一个地址,通过该地址就可以调用该函数,所以当一个函数指针的类型和参数列表类型一致时,就可以把函数名直接赋值给函数指针。简单理解就是函数名相当于函数的地址。

我们也可以把函数指针作为函数参数使用,这样的话,用同一个函数,通过传递不同的函数指针就可以实现不同的功能,如下图所示:

结构体数组和结构体指针

结构体数组

结构体数组顾名思义就是每个数组的元素不是简单的基础类型,而是一个个的结构体类型的数据,故每个数组元素都包含该结构体的所有成员。

定义方式:

  • 第一种方法是在声名结构体的时候进行定义:

    struct 结构体名称
    {
        结构体成员;
    } 数组名[长度];
    
  • 第二种方法是先是先声明一个结构体类型(比如上面Book),再用此类型定义一个结构体数组:

    struct 结构体名称
    {
        结构体成员;
    };
    struct 结构体名称 数组名[长度];
    

初始化结构体数组(在定义的同时也可以进行初始化):

struct Book = {
    char name[128];
    char author[24];
    float price;
}
struct Book book[2] = {
    {"book1","大黄",100},
    {"book2","小黄",200}
}

结构体指针

结构体指针:指向结构体变量的指针

声明:struct Book * pt;

我们知道,数组名实际上是指向一个数组第一个元素的地址,所以我们可以直接将数组名赋值给一个指针变量。但是,结构体变量不一样,结构体的变量名并非指向结构体的地址,所以要对结构体结构体变量进行取址,必须使用取址运算符pt = &book;

通过结构体指针访问结构体成员有两种方法:

  • 先使用*号对结构体指针进行解引用然后再使用.号运算符去访问结构体的成员:(*结构体指针).成员名
    ps:要使用()进行先解引用,这是因为.号成员选择运算符的优先级是要比取值运算符*的优先级要高的。
  • 第二种是直接使用指针专用的成员选择符号->,它的优先级和.号一样高。结构体指针->成员名

巧计:箭头有指针所以用于指针,点号用于对象。

传递结构体变量和结构体指针

我们知道函数调用的时候,参数的传递是一个值传递的过程,即实参赋值给形参这样的一个过程。所以,结构体变量如果能作为函数的参数来传递的话,那么两个相同结构体变量之间应该是可以使用赋值号直接赋值才对。

那么两个结构体变量是否能够直接赋值?答案是可以的!但是前提是两个结构体变量的结构体类型必须是一致的。

在函数中传递结构体变量可能会导致程序的开销变大(因为结构体可以定义的很庞大),为了效率,我们应该传递指向结构体变量的指针(一个指针也就四个字节或者八个),而不是直接传递结构体变量。
在操作上,我们要访问结构体成员只需把.号换为->就可以了。

原文地址:https://www.cnblogs.com/deehuang/p/14394804.html