带着问题玩转指针

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字节。
总结下全篇:指针是内存地址,这个内存地址对应的存储空间可以存放数据,至于可以存放什么样的东西,别急别急。
关于二进制、十进制、十六进制,有时间我会写篇博客。(拖更。。。)
到了这里指针的硬件知识就全部讲解完毕了,理解了指针以后我们就可以开始正题了。

有哪些指针类型?

  • 基础数据类型指针:char*、int*等
  • 结构体指针:struct 结构体类型 *
  • 函数指针:返回值类型 *()
  • 数组指针(指向数组的指针又叫行指针)
  • 对象指针
  • 智能指针this
  • 其他的后续补充

指针的具体使用

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

指针和函数的纠缠

指针作为函数的参数

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

代码:

#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和指针书中指出数组名是特殊的指针,可见数组和指针的关系。

数组名是指针常量,指向了数组的首元素地址,即num等同于&num[0]

它和普通指针不同在三个地方

  1. 数组名的sizeof返回的是整个数组空间大小;指针的sizeof返回的指针的大小4/8。
  2. 数组名使用取地址符返回的是指向整个数组的内存区域地址代表了整个数组;而指针取地址返回的是存储指针的那片内存区域的地址。
  3. 数组名不可修改,指针可以修改。

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

一维数组和指针

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

这里的p等同于数组名num、&num[0],所以表示元素的方法有两种。
数组表示法:p[1],这里认为p是数组名
指针表示法:*(p+1),因为p指向数组首元素

代码:

#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 

指针数组和数组指针

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

那么在表达式中该如何去区分呢?
指针数组:char *p[3]; // []的运算符优先级高,所以p[3]首先被认为是个数组。
数组指针:int (*p)[3]; // ()和[]优先级相同,根据结合律从左向右运算。所以(*p)先计算p被认为是个指针。被认为是个指向一个拥有3个元素数组的指针。

这里详细说明下数组指针。
这里的*p等同于数组名num。因为int (*p)[3] = &num; 元素的数组表示法:(*p)[1]。
指针表示法比较特殊。如果能把上面数组名和指针和数组理解了,那么这里的两种写法很容搞懂。
p+1代表什么?
p指向了整个num数组,所以p+1表示跨越一个数组的长度,这也就是二位数组中的行指针。
*p+1又代表什么?
p等同于num,num是数组元素的首地址。那么p+1表示指针偏移的大小是一个元素的内存大小。

代码:

#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 

结论:

1、指针数组是用来存放指针的数组
2、数组指针是指向了一个数组,注意赋值一定义是数组名取地址,而不是数组名。
这就是我们上面谈到的指针和数组名的不同点。所以数组又叫行指针,+1就是跳过了整个数组
3、数组指针两种访问方式需要记忆

指针和二维数组

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

代码:

#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 

总结:

1、对二维数组的单行进行操作的时候,利用数组指针去访问很方便

二级指针

一级指针通过形参把变量带入到函数内部
二级指针可以把函数内部变量带出去,注意不能带出局部变量。

代码:

#include <iostream>
#include <windows.h>

using namespace std;

void ret(int** p2);

int main() {

	int* p1 = NULL;  // 指针置空,防止出现野指针

	ret(&p1);

	cout << *p1 << endl;

	system("pause");
	return 0;
}

void ret(int** p2) {

	static int num = 13;  // 静态变量全局变量存储在内存的全局区中,不在函数栈帧中。

	*p2 = &num;

}

结果:

13
请按任意键继续. . .

在这里插入图片描述

指针引用

指针引用的作用类似于二级指针可以把被调用函数内存的变量给带到调用函数中。
但是要注意不要带出auto存储类的局部变量。

代码:

#include <stdio.h>
#include <windows.h>

void test(int* &p2);

int main() {
	int* p1 = NULL;

	test(p1);
	printf("*p = %d\n",*p1);

	system("pause");
	return 0;
}

// 函数参数是指针引用
void test(int* &p2) {  //这里int * &p2 = p1等同于int** p2 = &p1
	static int num = 12;
	p2 = &num;  // 等同于*p2 = &num
}

输入、输出:

*p = 12
请按任意键继续. . .

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

划水猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值