C/C++语言基础--指针三大专题详解1(包含常见错误,代码均可运行)

本专栏目的

  • 更新C/C++的基础语法,包括C++的一些新特性

前言

  • 指针是C/C++的灵魂,和内存地址相关联,运行的时候速度快,但是同时也有很多细节和规范要注意的,毕竟内存泄漏是很恐怖的
  • 指针打算分三篇文章进行讲解,基本涵盖了常见的用法和注意事项
  • 制作不易,欢迎收藏+点赞+关注,本人会持续更新

初识指针

地址/存储单元

字节(Byte):计算机将内存分成一格一格,每一格用来存储一个数字,每一格对应的专业术语叫字节。

地址(Address):计算机给内存中的每个字节都指定一个唯一的编号,编号从0开始,后续字节编号依次加1.

在这里插入图片描述

存储区(Buffer):计算机将1字节或多个连续的字节形成一个存储单元,简称存储区,又称缓存区。

首地址(Base Address):又称起始地址,存储区中第一个字节的地址用来当存储区的首地址,又称基地址。

原则:任何程序访问内存前先分配。(操作系统负责分配)

我们原先学过的变量、数组、函数等都是放在内存中的,我们可以通过名字去使用变量、数组、函数等,但实际运行时,系统使用得是内存地址,而不是变量名,变量名只是方便我们程序员使用的。

怎么获得变量的地址呢?

  • &符号,这个符号就是获取变量的地址的符号。

    int age = 18;
    //输出age变量的地址
    printf("addr:%p\n",&age);
    

注意:

  • **连续性:**每个内存单元之间地址是连续的
  • 唯一性:在同一台机器上每个内存单元的地址是唯一
  • **随机性:**每次运行程序,变量的地址不一定一样,这是由操作系统随机分配的

指针

定义

指针描述了数据在内存中的位置,标示了一个占据存储空间的实体,在 C/C++语言中,指针一般被认为是指针变量,指针变量的内容存储的是其指向的对象的首地址,指向的对象可以是变量,数组,函数等占据存储空间的实体。

总的来说

  • 指针实际上是一种特殊的数据类型,用来存储的首地址
  • 一种特殊的数据类型,用来描述数据在内存中的位置

指针定义如下

int *p;
int *p1,*p2,*p3;	

同时定义多个指针变量时,每个标识符前面都要加*号,否则后面的会被定义成int型变量。

指针大小

指针作为一个变量是有大小的,其大小在32位平台是4个字节,64位平台上是8个字节,大小与指针的类型无关。

案例如下:

printf("<char*> size is %llu byte\n", sizeof(char*));
printf("<int*> size is %llu byte\n", sizeof(int*));
printf("<double*> size is %llu byte\n", sizeof(double*));
printf("<char**> size is %llu byte\n", sizeof(char**));
printf("<int**> size is %llu byte\n", sizeof(int**));

结果如下:

<char*> size is 8 byte
<int*> size is 8 byte
<double*> size is 8 byte
<char**> size is 8 byte
<int**> size is 8 byte

通过分析发现:

  • 指针大小都是 8 byte,与指针类型无关(在x64平台是8字节)

指针使用

既然指针保存的是对象的地址,那么如何通过指针操作对象呐?

  • &:取地址符。用于获取变量所在的首地址
  • *:间接访问运算符,也叫作解引用运算符。用于获取地址对应的值。

这两个运算符的优先级一样,结合起来使用也非常简单!(如果不理解,最好加上括号;如:*(&p)),我们来看一下这个案例:

int a = 520;
int* p = &a;
printf("%d, %p, %p, %p\n", *&a, &*p, *&p, &a);

结果:

520, 0000001B416FFAB4, 0000001B416FFAB4, 0000001B416FFAB4

分析:

  • &a:变量a的地址,*:解应用,解(&a),所以输出还是a
  • *p:解引用,输出变量a,&:取地址,即输出a的地址
  • &p:指针p的地址,*:解引用,即输出指针变量p,也就是a的地址
  • &a:很明显,就是输出a的地址

使用指针的好处:

  • 直接访问硬件
  • 快速传递数据(指针表示地址)
  • 返回一个以上的值,返回一个(数组或者结构体的指针)
  • 方便处理字符串

万能指针

万能指针:即void* 类型的指针,因为它既可以保存任意类型的地址,还可以再任意类型的指针类型之间进行转换

  • 可以指向任何类型地址

    int maye = 20;
    void* p = &maye;
    
  • 可以隐式自动转换为其他类型指针

    int* pi = p;
    
  • **不能对void*取值操作,因为它没有类型,**或者说不能判断存储的是什么类型,需要强转指定一个确定的类型才能使用

    printf("%d\n",*p);		//error
    printf("%d\n",*(int*)p);//right
    

指针的特殊状态

我们在使用指针的时候,总是会遇到各种稀奇古怪的问题,但万变不离其宗,下面是一些指针常见的问题:

野指针

野指针(wild pointer)就是没有被初始化过的指针。

【示例:】

#include<stdio.h>
int main()
{
    int *p;
    printf("%d\n",*p);  // 没有初始化,那他就是随机的
    return 0;
}

如果用Vs编译,会直接报错error C4700: 使用了未初始化的局部变量“p”,还是比较人性的,从根本上避免了野指针。没有初始化,那他就是随机的,又因为指针和内存相挂钩,所以随机内存是很危险的。

空指针

空指针就是被赋值为NULL的指针,它不指向任何的对象或者函数。(坚决不能使用空指针,否则程序就会崩)

空指针的出现是为了避免错误的引用指针而导致的难以排查的问题,不过空指针也不能直接访问,但是可以用来判断。

【示例:】

#include<stdio.h>
int main()
{
    int* p = NULL;
    //判断指针是否为NULL
    if (p != NULL)
    {
        printf("%d\n", *p);
    }
    return 0;
}

如果把指针值为空,则可以进行判断,就算没有判断,直接对空指针进行引用,产生的报错也非常好理解。

悬空指针

悬空指针是指针**最初指向的内存已经被释放了的一种指针。 **

【示例:】从函数中返回临时变量的地址

#include<stdio.h>

int* foo()
{
    int age = 18;   //函数执行完毕,age的内存会被自动释放
    return &age;   
}
int main()
{
    int* p = foo();
    //getchar();
    printf("%d\n", *p);
    return 0;
}

运行上面的代码,貌似没有任何问题,的确如此,但不代表这个代码是正确的。那是因为释放内存需要时间,现在我们把main函数中的getchar的注释放开,然后重新运行程序,等待几秒之后按下任意键,发现输出的结果已经不对了,因为这个时候内存已经释放了

**注意:**悬空指针是编码过程中最容易出现问题的,切记,认真检查!

指针的运算

算数运算:

  • 指针是存储的地址,地址本质就是一个整数,因此,我们可以对指针执行四种算术运算:++、–、+、-(其他运算没有意义),代表内存位置的移动。

    char* pc = NULL;
    printf("%p %p\n",pc,pc+1);	//0 1
    int* pi = NULL;
    printf("%p %p\n",pi,pi+1);	//0 4
    double* pd = NULL;
    printf("%p %p\n",pd,pd+1);	//0 8
    
  • 总结:

    • 指针的每一次递增,它其实会指向下一个元素的存储单元。
    • 指针的每一次递减,它都会指向前一个元素的存储单元。
    • 指针在递增和递减时跳跃的字节数(步长)取决于指针所指向变量数据类型长度,比如 int 就是 4 个字节
    • 不同类型的指针所占内存大小都是一样的(32位计算机4个字节,64位8个字节)

关系运算:

  • 指针可以用关系运算符进行比较,如 ==、< 和 >。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。
  • 总结:
    • 相关变量的指针进行比较,才有意义
    • 大于小于常用在数组中,全等一般是判断指针是否为NULL

指针的类型

指针使用来间接访问内存的,那么就需要知道指针指向的是什么样的数据类型,应该如何解析它。

所以我们需要一个特定类型的指针变量来存放特定类型变量的地址。

  • int* -> int
  • char* - > char
  • double* -> double
  • int* - >char 错误!错误!错误!

为什么这么麻烦呢?

因为,我们不仅仅使用指针来存储内存地址;同时也是用它来解引用那些地址的内容,这样我们就可以访问和修改这些地址对应的值了,故需要指定指针的类型。

地址分析

不同的数据类型有不同的大小,char 1个字节,int 4个字节,double 8个字节。不仅仅是大小方面的差异,这些变量或数据类型在存储信息的方式上也有所不同。

在这里插入图片描述

上图所示,如果是int类型,那么最高位表示符号位,剩下的31个位用来存储值,接下来让我们探讨一下几种情况:

  • 现在如果声明一个指针p指向整型变量,然后用取地址符把a的地址存放在p中,然后打印p,p的值会是多少呢?0x200,也就是byte0的地址,也就是说整型变量a的起始地址是0x200;

  • 如果我们想知道那个地址的内容(值),就使用*p去解引用这个地址。然后编译器看到之后就会觉得没有问题,p是一个指向整型的指针,因此我们需要看4个字节。从地址0x200开始,编译器就知道如何去提取一个整型数据。所以,它从这四个字节中得到了1025这个值。

  • 如果p是一个字符类型的指针,那么在解引用的时候编译器只会看一个字节,因为一个字符类型只有一个字节。如果p是一个浮点型指针,尽管浮点型也是4个字节,但是浮点数4字节的信息表示缺与整型不一样。打印出来的值也会是不一样的。

const 与 指针

const是constant的简写,只要一个变量前面用const来修饰,就意味着该变量里的数据可以被访问,不能被修改。也就是说const意味着“只读”。任何修改该变量的尝试都会导致编译错误。const是通过编译器在编译的时候执行检查来确保实现的,也就是说const类型的变量不能改是编译错误,不是运行时错误。

所以我们只要想办法骗过编译器,就可以修改const定义的常量,而运行时不会报错。

const与指针可以搭配出三种不同的含义:

  • 指针指向的内存不可修改,但指针的指向可以修改

    int age = 18;
    const int* ptr = &age;		//常量指针
    int const* ptr = &age;		//和上面一句是等价的
    
    *ptr = 20;					//err,不能修改
    ptr = NULL;					//ok,可以修改指向
    
  • 指针的指向不可以修改,但指向的内存可以修改

    int* const ptr = &age;
    *ptr = 20;	//ok
    ptr = NULL;	//err
    
  • 指针的指向不可以修改,指向的内存也不可以修改

    const int* const ptr = &age;
    *ptr = 20;	//err
    ptr = NULL;	//err
    

常量指针(指向常量的指针)是指指向常量的指针,顾名思义,就是指针指向的是常量,即,它不能指向变量,它指向的内容不能被改变,不能通过指针来修改它指向的内容,但是指针自身不是常量,它自身的值可以改变,从而指向另一个常量。

指针常量(指针是常量)是指指针本身是常量。**它指向的地址是不可改变的,但地址里的内容可以通过指针改变。**有一点需要注意的是,指针常量在定义时必须同时赋初值。

typedef陷阱(易错)

示例:

typedef char* IntPtr;
const IntPtr p;

const IntPtr 相当于 const char*” 呢?还是char* const呢?

  • 答案是相当于char* const,原因很简单,typedef 是用来定义一种类型的新别名的,它不同于宏,不是简单的字符串替换。
  • 因此,const IntPtr中的 const 给予了整个指针本身常量性,也就是形成了常量指针char* const(一个指向char的常量指针),而不是const char*(指向常量 char 的指针)。
  • 当然,要想让 const IntPtr相当于 const char* 也很容易。如下面的代码所示:
typedef const int* const_IntPtr;
const_IntPtr p;

还值得注意的是,虽然 typedef 并不真正影响对象的存储特性,但在语法上它还是一个存储类的关键字,就像 auto、extern、static 和 register 等关键字一样。因此,像下面这种声明方式是不可行的:

typedef static int Static_Int;	//错误

不可行的**原因是不能声明多个存储类关键字,由于 typedef 已经占据了存储类关键字的位置,**因此,在 typedef 声明中就不能够再使用 static 或任何其他存储类关键字了。

指针做函数参数

学习函数的时候,讲了函数的参数都是值拷贝,在函数里面改变形参的值,实参并不会发生改变。

在这里插入图片描述

如果想要通过形参改变实参的值,就需要传入指针了。

在这里插入图片描述

注意: 虽然指针能在函数里面改变实参的值,但是函数传参还是值拷贝。**不过指针虽然是值拷贝,但是却指向的同一片内存空间,**即拷贝指针

在这里插入图片描述

指针做函数返回值

返回指针的函数,也叫作指针函数。

和普通函数一样,只是返回值类型不同而已,先看一下下面这个函数:

int fun(int x,int y);

接下来看另外一个函数声明

int* fun(int x,int y);

这样一对比,发现所谓的指针函数也没什么特别的。

注意:

  • 不要返回临时变量的地址
  • 可以返回动态申请的空间的地址
  • 可以返回静态变量和全局变量的地址

大、小端模式

  • 大端(Big-endian)和小端(Little-endian)是什么?
    • 在计算机业界,Endian表示数据在存储器中的存放顺序
  • 大端模式:数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中
    • 这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这种存放方式符合人类的正常思维
  • 小端模式:数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中
    • 这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。

在这里插入图片描述

  • 总结:采用大小模式对数据进行存放的主要区别在于在存放的字节顺序,大端方式将高位存放在低地址,小端方式将高位存放在高地址。采用大端方式进行数据存放符合人类的正常思维,而采用小端方式进行数据存放利于计算机处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值