0
点赞
收藏
分享

微信扫一扫

带着问题玩转指针

无聊到学习 2022-04-27 阅读 93
c++c语言

0 前言

C/C++中的指针一直是很多初学者的噩梦,特别是涉及到的一些硬件知识的时候,再加上人云亦云。其实指针并不难,不要抱着畏惧和目的去学习,其次要善于去总结多看看别人写的一些经验。说这么多的目的是让大家带着兴趣来学习,也不要去担心顾虑深什么的。本文的代码篇不是纯小白文,需要读者有一些C基础。
我希望大家可以带着问题去学习,做个好奇宝宝。也不要去做本只有十万个为什么,但是答案都是空白的书,要学会自己去解决问题。

指针理解(什么是指针?)

内存(内存条)

关键词:存储数据、内存条、内存大小
在学指针前先要搞懂指针是什么。指针是内存地址。那么什么是内存地址呢?要知道这个我们引入个硬件:内存。 什么是内存?有木有熟悉的感觉?跟什么很像?对没错就是内存条。熟悉电脑的人可能会先反应过来,其实它就是笔记本的内存条,通常我们遇到的内存不足,指的就是这个内存条剩余使用空间不足。内存条是用来存放数据的,所以内存是有大小的,内存的大小一般是GB为单位。内存条其实很像硬盘。下图就是内存条

内存条关键词:内存地址、内存基本单元、字节
我们知道了内存是有大小的,那么我们怎么去利用内存呢?总不能存个数据直接塞满8G,或者说数据随便丢在内存里面,当我们需要使用数据的时候该去内存什么位置去找到这个数据呢?所以为了解决这些问题,内存在硬件上划分了地址,并且划分了很多基本存储单元,这些基本单元是以字节为单位的。(字节:由8个二进制位组成)

操作系统眼中的内存 通过上面的的描述大家可能对内存有了一定的认识。那么回到刚开始的问题什么是指针?指针就是内存地址。这样大家应该理解了吧。那么指针是干什么的呢?这问题可能会问的大家有点懵逼😈,我换个问题。上一段文字中说了,我们为什么会划分内存地址,基本存储单元呢,不就是为了去访问内存中的数据吗。卧槽豁然开朗啊,有没有有没有。
内存地址是为了存储数据和读取数据的方便,而指针就是内存地址。

下面还要引出一个概念,最后一个了。看到了现在还担心不会吗?毛线呢,那我不是白写了吗?

关键词:寻址能力、指针大小
一开始我说了个问题不知道大家有没思考过。电脑很容易遇到内存不足,软件运行卡顿的问题。 那么怎么去解决这个问题呢。你还想还想。手机没话费了咋办,充话费啊,土豪直接梭哈。这里也是同样的加内存条就可以解决这个问题,普通人可能加个4G、8G的内存,但是土豪不一样上来就梭哈。
解决上面的问题这里有出现了新问题,就是土豪内存最多可以加多少。内存的大小被三个因素限制主板、CPU、操作系统版本、操作系统的位(后面两个可以归于操作系统)。我们这里主要谈操作系统位对内存大小的限制。不同位的操作系统寻址能力是不同的,32位操作系统的寻址能力是4个字节,64位操作系统的寻址能力是8个字节。那也就是说指针是有大小的,32位4字节,64位8字节。我又有问题了,读者别打我。 寻址能力是什么?寻址能力是操作系统所能访问到内存的最大地址。 也就是32位操作系统最大访问的内存地址是0xFFFFFFFF,换算成十进制约等于4G,64位操作系统理论上最大访问的内存地址是0xFFFFFFFFFFFFFFFF,换算成十进制约等于17179869184G,但是目前windows64位系统最大只能达到128G内存。
上面啰嗦了一堆,其实想要引出的就是指针有大小,32位系统上指针是4字节,64位系统上指针是8字节。
总结下全篇:指针是内存地址,这个内存地址对应的存储空间可以存放数据,至于可以存放什么样的东西,别急别急。
关于二进制、十进制、十六进制,有时间我会写篇博客。(拖更。。。)
到了这里指针的硬件知识就全部讲解完毕了,理解了指针以后我们就可以开始正题了。

指针对应的存储空间可以存什么?

  • 基本数据类型(int、double、char…)
  • 内存地址
  • 函数的地址
  • 数组的地址
  • 其他的后续补充

指针的具体使用

我尽量用最简洁的代码和少量的新知识去描述出指针的各种用法,如果出现了一些新的东西我会注释的。

指针和函数的纠缠

指针作为函数的参数

指针作为函数的参数有什么作用呢?我们带着这个问题来看下面的代码。

代码:

#include <stdio.h>

void funcP(int *p2);  // 函数声明

// 在被调用函数里面去改变调用函数变量值
// 调用函数
int main() {
    int num = 12;
    
    printf("num=%d\n", num);
    funcP(&num);  // 把num变量的地址作为函数参数传递
    printf("num=%d\n", num);

    return 0;
}

// 被调用函数
void funcP(int *p2) {
    *p2 = 13;  // 访问指针对应的内存空间,并赋值。也就是访问num变量的存储空间
}

输入、输出:

➜  code ./a  # 执行程序的脚本指令
num=12  # 程序输出
num=13  # 程序输出
➜  code 

总结:
1、int *p; :指向int类型的指针
2、指针作为函数参数可以修改调用函数内的变量值

指针作为函数的返回值

指针作为函数的返回值有什么用呢?

代码:

#include <stdio.h>

int * funcP(void);

// 通过调用函数去访问、修改被调函数的变量
// 调用函数
int main() {
    int *p = NULL;  // 指针创建后使用NULL空指针初始化。NULL被宏定义为0

    p = funcP();
    printf("main b=%d", *p);  // 外部去访问这个值

    *p = 14;  // 外部去修改这个值
    p = funcP();  // 通过funcP函数打印验证

    return 0;
}

// 被调用函数
int * funcP(void) {
    // static静态变量的生存周期是整个程序,普通的局部变量当funcP函数结束就会被释放
    // b = 12只会初始化一次,以后不管执行多少次都不会再初始化
    static int b = 12;  

    printf("funcP b=%d", b);

    return &b;
}

输入、输出:

➜  code ./a
funcP b=12
main b=12
funcP b=14

总结:
1、指针变量定义的时候要使用空指针来初始化。
2、指针作为函数返回值的时候,可以通过调用函数去访问、修改被调用函数的变量。
3、这个可被修改的变量绝对不能是auto存储类型的局部变量。因为它的生存周期只在该函数中,函数结束后,再去访问这个内存空间得到的数据可能不是你想要的。

函数指针和void指针

函数也有地址吗?当然
函数指针一般用在qsort数组排序算法中
这节比较难,会涉及到void *指针和函数指针

代码:

#include <stdio.h>
#include <cstdlib>  // qsort

int funcP(const void *num1, const void *num2);

int main() {
    // 函数指针,如果不加括号那么这就是一个返回值是指针的函数了
    // 假设不加括号,因为括号优先级比*高,所以fp()会被认为是一个函数
    int (*fp)(const void *num1, const void *num2) = &funcP;  // 创建一个函数指针并且初始化
    int num[] = {1, 3, 2, 11, 12, 23};

    // 排序qsort第一个参数数组,第二个参数数组个数,第三个参数单个元素字节,第四个比较函数的函数指针
    qsort(num, sizeof(num)/sizeof(int), sizeof(int), fp);

    // 打印数据
    for(int i=0; i<sizeof(num)/sizeof(int); i++) {
        printf("num[%d] = %d\n", i, num[i]);
    }

    return 0;
}


// 比较函数
// void *num1这是vodi类型指针
// 该指针指向一个内存空间,但是却不指定类型。
// int *指针到void *指针可以隐式转换,但是反过来需要强制转换类型
// 好处是调用函数可以传递任意类型,我们只要此函数中修改下就行。
// const vodi *num1;表示void指针指向的内容不可以更改
int funcP(const void *num1, const void *num2) {
    int *p1 = (int *)num1;  // void指针强制转成int类型
    int *p2 = (int *)num2;
    
    return *p1-*p2;
}

输入、输出:

➜  code ./a
num[0] = 1
num[1] = 2
num[2] = 3
num[3] = 11
num[4] = 12
num[5] = 23
➜  code 

补充:如何去访问函数指针呢?
1、(*fp)(实参); // 贝尔实验室写法
2、fp(实参); // 伯克利写法
ANSI规定两种都是可以使用的

常量指针、指针常量、两者都有

常量指针

记忆窍门:因为const离数据类型近,所以他限制的是类型,而不是指针。
指针指向的那片内存里面的数据不能更改,也就是指针解引用后的内存区域不能改
声明有两种:
const int *p;
int const *p;

指针常量

记忆窍门:因为const离指针近,所以他限制的是指针,而不是数据类型。
指针指向不能更改,也就是指针变量存的地址不能改
声明:
int * const p;

两者皆有

指针指向不能改,指针指向的值也不能改
声明:
const int * const p;

指针和数组

数组名

数组名是什么?
在C和指针书中指出数组名是特殊的指针。
数组名是指针常量,指向了数组的首元素地址,它和普通指针不同在两个地方,sizeof返回的是整个数组的空间大小使用取地址符返回的是指向数组的指针代表了整个数组,而不是存储数组指针变量的地址。

这里有个关键点需要注意:数组名代表的数组首元素的地址,重点在元素,所以对它的加减操作跨度是数组元素;而&数组名地址返回的是数组的地址,重点在数组,所以对它的加减操作跨度是整个数组。

一维数组和指针

数组名是特殊的指针:指针常量

代码:

#include <stdio.h>

void funcP(int * const p);

// 利用一级指针改变调用函数的变量值
int main() {
    int num[] = {1, 2, 3, 4};

    for(int i=0; i<sizeof(num)/sizeof(int); i++) {
        printf("num[%d]=%d\n", i, num[i]);
    }

    funcP(num);  // 需要注意的是传递数组名实际上传递的是指针
    // 思考为什么使用数组名取传递?
    // 因为函数形参类型不对,所以没法接受。
    // 那么&num到底是什么?其实我们在数组名描述已经讲了,他是指向整个数组的指针
    // 代表的是整个数组,所以需要数组指针去接受
    for(int i=0; i<sizeof(num)/sizeof(int); i++) {
        printf("num[%d]=%d\n", i, num[i]);
    }

    return 0;
}

void funcP(int * const p) {  // 数组名是一个指针常量
    // 两种访问方式
    // 1、数组访问法
    p[0] = 11;

    // 2、指针访问法
    *(p+1) = 12; // 等同于p[1] = 12;
}

输入、输出:

➜  code ./a
num[0]=1
num[1]=2
num[2]=3
num[3]=4
num[0]=11
num[1]=12
num[2]=3
num[3]=4
➜  code 

结论:
1、数组名是特殊的指针,但是不等同于指针。不同点在于sizeof测出的是整个数组的长度。
2、数组作为函数参数传参意味着数组退化成指针去传递,相当于参数是个数组名,而不是拷贝一份数组。这样的好处是可以节约内存开销。
3、为什么传递的参数用的是数组名num,而不是&num?

指针数组和数组指针

指针数组:是存储指针的数组。
数组指针:指向数组的指针

那么在表达式中该如何去区分呢?
char *p[3]; // []的运算符优先级高,所以p[3]首先被认为是个数组,然后char * 被认为是一个char指针。指针数组

int (*p)[3]; // ()优先级更高,所以(*p3)被认为是个指向一个拥有3个元素数组的指针。数组指针

代码:

#include <stdio.h>

int main() {
    int num[] = {1, 2, 3, 4, 5};
    char ch[] = "abcd123";
    int len = sizeof(ch)/sizeof(char)-1;

    char *cp[len];  // 指针数组
    int (*p)[5] = NULL;  // 数组指针


    // 指针数组
    for(int i=0; i<len; i++) {
        cp[i] = ch+i;
        // cp[i] = &ch[i]

        printf("ch[%d] = %c\n", i, *cp[i]);
    }


    // 指针数组
    {
    p = &num;  // 这里要注意是取数组名的地址,这样代表了整个数组

    // 数组表示法
    (*p)[1] = 1;  // 修改num[1] = 1;
    
    // 指针表示法
    *((*p)+2) = 2;  // 修改num[2] = 2;

    printf("num[1] = %d, num[2] = %d\n", num[1], num[2]); 

    // 数组指针又叫行指针,在后面指针和二维数组中我会再讲
    // 从p+1的跨度可以看出是直接跳过了整个数组的地址
    printf("p addr = %p, *p+1 addr = %p, p+1 addr = %p\n", p, *p+1, p+1);
    }

    return 0;
}

输入、输出:

➜  code ./a
ch[0] = a
ch[1] = b
ch[2] = c
ch[3] = d
ch[4] = 1
ch[5] = 2
ch[6] = 3
num[1] = 1, num[2] = 2
p addr = 0x16d2a3520, *p+1 addr = 0x16d2a3524, p+1 addr = 0x16d2a3534
➜  code 

结论:

指针和二维数组

二维数组的内存存储模型从上图中我们不难发现,二维数组本质上还是一维数组,因为他们在内存上是连续的。

代码:

#include <stdio.h>

int main() {
    // 利用行指针找出数组每行里面最小的
    // 这里的二维数据可以理解成三个一维数组
    int num[3][3] = {
        {3, 2, 12},
        {2, 3, 6},
        {9, 32, 1}
    };

    int * little = num[0];
    // 数组指针
    int (*p)[3] = &num[0];
    
    for(int i=0; i<3; i++) {
        for(int j=0; j<3; j++) {

            if(*little>(*p)[j]) {
                little = *p+j;
            }
            
        }
        printf("little num = %d\n", *little);
        p++;  //P+1表示跳了一行数组  
    }


}

输入、输出:

➜  code ./a
little num = 2
little num = 2
little num = 1
➜  code 

总结:

指针篇先到这里,后续有时间我再更新。

举报

相关推荐

0 条评论