【C++指针】搭建起程序与内存深度交互的桥梁(上)

发布于:2025-03-24 ⋅ 阅读:(30) ⋅ 点赞:(0)


每文一诗  💪🏼

        我见青山多妩媚,料青山见我应如是 ——宋/辛弃疾贺新郎·甚矣吾衰矣

        译文:我看那青山潇洒多姿,想必青山看我也是一样。

       🔥🔥 个人主页 点击🔥🔥


目录

指针的概念

指针的形式

解引用间接修改变量的值

指针占有内存

程序存储数据三属性

空指针/野指针

指针类型

常量指针与指针常量

值传递/地址传递


 

指针的概念

        指针存储的是内存地址,这个内存地址指向了存储其他数据的内存位置。借助指针,可以直接对内存中的数据进行访问和操作。

指针的形式

指针用“*”来表示,用“&”来表示取地址,

这里的“*”和数学运算中的“*”号一样,但是这里的“*”表示为他是一个指针变量,

但是当“*”后跟的是一个指针时,就变成了解引用运算符,它的作用是可以得到该指针存储的地址的内存中的值。

方式1

int m = 100;
int *p;
p = &m;

方式2

int m = 100;
int *p = &m;

为了便于理解,可以将int* 作为一个新的数据类型,表示指向一个int类型变量的指针。

🧐为了理解变量指向变量的指针:可以借助寄快递的比喻

        寄快递时,我们有三个重要的信息,一个是快递要寄到哪里,即地址(这里假如说要寄给“小ri子”);二是寄的什么东西(这里假如说是“东风”);三是所寄东西的类型(这里假如说是“会飞的”)。此外,我们用“”来代表“小ri子”。


所以说这里我们可以形象的说:

        会飞的 快递 = 东风;

        会飞的* 岛 = &快递;

       (地址:小ri子)

由此可以得出:&快递(取地址) = 岛 = 小ri子

                          快递 = 东风 = *岛(解引用,“炸开”,谁炸的)


再来看看代码:

        std::string a = "东风";

        std::string* b = &a;

       (地址:0x0011)

由此可以得出:&a(取地址) = b = 0x0011

                          a = "东风" = *a(解引用)


        综上,变量存储的是具体的值(就像快递存储的是"东风"),而指向变量的指针存储的是该变量的地址(就像“岛”存储的是"小ri子"),通过“*”加指针就可以得到具体的值(就像*岛=“东风”)可形象的说“炸开”。即东风快递,使命比达!

测试代码

#include<iostream>

int main(int argc, char const *argv[])
{
    int m = 100;
    int *p = &m;
    std::cout<<"指针:"<<p<<std::endl;
    std::cout<<"指针指向的值:"<<*p<<std::endl;
    return 0;
}

输出(采用cmake)

解引用间接修改变量的值

😈代码:

使用*p解引用来存储的值

#include<iostream>

int main(int argc, char const *argv[])
{
    int m = 100;
    int *p = &m;
    *p = 120;
    std::cout<<"指针:"<<p<<std::endl;
    std::cout<<"指针指向的值:"<<*p<<std::endl;
    return 0;
}

指针占有内存

在32位操作系统中,地址总线是 32 位,一个字节是8位,32/8=4指针占4个字节;

在64位操作系统中,地址总线是 64 位,一个字节是8位,64/8=8,指针占8个字节。

程序存储数据三属性

程序在存储数据时,必须跟踪以下三个属性

  • 数据的值:即变量所存储的具体内容。
  • 数据类型:数据类型决定了数据的存储方式、取值范围以及可以对数据进行的操作。
  • 数据存储的位置:计算机中每个数据都有其特定的存储位置,通常用地址来表示。

这就好比在

注意:指针存储的是一块连续内存空间的起始位置,即首地址,而不是首尾地址,但是怎么知道变量被分配在哪一快内存呢?这就是需要数据类型了,因为数据类型决定了占用内存的大小(int占用4个字节,每个字节都有唯一的地址),再加上指针存储的首地址,就可以知道占用了那块内存了。

        普通变量:在声明一个普通变量时,系统会根据变量的类型来分配相应内存空间,int型分配4个内存空间,float分配4个字节存储空间,而double是8个。普通变量直接将数据的值存储在分配的内存空间中

        指针变量:在 32 位系统中,指针变量通常会分配 4 个字节的内存空间;在 64 位系统中,指针变量通常会分配 8 个字节的内存空间。针变量存储的是其他变量的内存地址,而不是数据的值。

空指针/野指针

空指针是一个特殊的指针值,它不指向任何有效的内存地址,可以用NULL来初始化一个空指针

int *p = NULL;

作用:当定义一个指针但暂时不需要它指向任何具体的对象时,可以将其初始化为空指针,以表明它当前不指向任何有效内存。

注意:

  • 空指针被解引用时,程序会崩溃,如
int* p = NULL;
std::cout<<"指针指向的值:"<<*p<<std::endl;
  • 如果指针是空指针,对一个空指针使用delete时,程序会正常退出,但是delete是释放动态分配的内存的(new),所以这里也没必要
int* p = NULL;
delete p;
  • 在函数的形参中使用指针时,在函数中应该有判断指针是否为空的代码。
void func(int *a,std::string *b)
{
    if(a==NULL||b==NULL) return;
    std::cout<<"地址传递 "<<*a<<" "<<*b<<std::endl;
}


野指针是指针不指向一块有效的内存地址,而是指向一个非法的内存地址。

产生原因:

  • 当delete了一块动态分配内存后,如果还要使用这个指针,但是没有将这块内存指指针设为空或其他值,那么就会变位野指针
int* p = new int(2);
delete p;
std::cout<<"指针:"<<p<<std::endl;//错误,地址现在已经是无效
std::cout<<"指针指向的值:"<<*p<<std::endl;//错误,内存已经被释放
  • 初始化指针变量时,没有将该指针设为空或者赋予地址,那么这个指针就会指向一个随机的地址,可能会变成野指针。
int* p ;//错误
std::cout<<"指针:"<<p<<std::endl;
std::cout<<"指针指向的值:"<<*p<<std::endl;

野指针非常危险,会导致程序崩溃,即段错误 (核心已转储)

指针类型

在C++中,指针的类型有:

  • 基本数据类型指针:如int*float*char*等
  • 数组指针
  • 函数指针
  • 类指针

当我们对指针赋值时,也就是在说“指向某变量”,被指向的变量的数据类型就是“基类型”

常量指针与指针常量

记法:const可翻译为“常量”,const在前面则为常量指针,不在前面则为指针常量

常量指针(开发中常用):

指针的指向可以改变,指针指向的的值不可以改变。

int m = 100;
const int *p = &m;

以下代码是错误的(指针指向的值不可以改变,不可以用解引用的方法修改值,但是可以通过修改原始变量来改变)

#include<iostream>
int main(int argc, char const *argv[])
{

    int m = 100;
    const int *p = &m;
    
    *p = 120;//错误
     m = 120;//正确,可以通过修改原始变量来改变。
    std::cout<<"指针:"<<p<<std::endl;
    std::cout<<"指针指向的值:"<<*p<<std::endl;
    return 0;
}

以下代码是正确的(常量指针可以修改指针的指向)

#include<iostream>
int main(int argc, char const *argv[])
{

    int m = 100;
    int n = 120;
    const int *p = &m;
    
    p = &n;//正确

    std::cout<<"指针:"<<p<<std::endl;
    std::cout<<"指针指向的值:"<<*p<<std::endl;
    return 0;
}

用途:

常用在函数的形参中,指的是不希望在函数内部修改内存地址中的值,若修改则会报错。

void func(const int* a)
{
    *a = 1;//错误
}

指针常量:

指针的指向不可以改变,指针指向的的值可以改变。

int m = 100;
int* const p = &m;

以下代码是错误的(指针的指向不可以改变)

#include<iostream>
int main(int argc, char const *argv[])
{

    int m = 100;
    int n = 120;
    int* const p = &m;
    
    p = &n;//错误

    std::cout<<"指针:"<<p<<std::endl;
    std::cout<<"指针指向的值:"<<*p<<std::endl;
    return 0;
}

以下代码是正确的(指针指向的值可以改变)

#include<iostream>
int main(int argc, char const *argv[])
{

    int m = 100;
    int n = 120;
    int* const p = &m;
    
    *p = n;

    std::cout<<"指针:"<<p<<std::endl;
    std::cout<<"指针指向的值:"<<*p<<std::endl;
    return 0;
}

值传递/地址传递

值传递:

        在函数参数中,传递的是普通变量,是形参,而形参是函数的局部变量,形参中存放的是实参的拷贝,所以修改形参的值,不会改变实参(如下面代码中的函数第二次)。

代码

#include<iostream>

void func(int a,int b)
{
    std::cout<<"函数第一次 "<<a<<" "<<b<<std::endl;
    a = 20;
    b = 30;
    std::cout<<"函数第二次 "<<a<<" "<<b<<std::endl;

}
int main(int argc, char const *argv[])
{
    int m = 100;
    int n = 130;
    func(m,n);
    std::cout<<"主函数 "<<m<<" "<<n<<std::endl;
    return 0;
}

输出:

地址传递:

        在函数参数中,传递的是指针变量,是形参,形参中存放的是变量的地址,而通过上文知道,通过*加指针变量解引用可以间接修改变量的值,而这则会改变实参的值(如下面代码中的主函数)。

 代码:

#include<iostream>
void func(int *a,int *b)
{
    std::cout<<"函数第一次 "<<*a<<" "<<*b<<std::endl;
    *a = 20;
    *b = 30;
    std::cout<<"函数第二次 "<<*a<<" "<<*b<<std::endl;

}
int main(int argc, char const *argv[])
{
    int m = 100;
    int n = 130;
    func(&m,&n);
    std::cout<<"主函数 实参"<<m<<" "<<n<<std::endl;
    return 0;
}

输出(实参已经被改变):

🍎地址传递好处:

  1. 直接修改实参函数:内部可直接对实参所指向的内存空间进行修改
  2. 减少数据拷贝开销:值传递是数据的拷贝,当传递大型数据结构(像数组、结构体或类对象)时,值传递会对整个数据结构进行拷贝,这会消耗大量的时间和内存,而地址传递仅需传递数据的地址,通常为4字节。

对2解释:

#include<iostream>

void func(int *a,std::string *b)
{
    std::cout<<"地址传递 "<<*a<<" "<<*b<<std::endl;
}
void func2(int a,std::string b)
{
    std::cout<<"值传递 "<<a<<" "<<b<<std::endl;
}
int main(int argc, char const *argv[])
{
    int m = 100;
    std::string n = "i love you";
    func(&m,&n);
    std::cout<<"地址传递 传递内存 "<<sizeof(int*)+sizeof(std::string*)<<" "<<n<<std::endl;
    func2(m,n);
    std::cout<<"值传递 传递内存 "<<sizeof(int)+sizeof(std::string)<<" "<<n<<std::endl;

    return 0;
}

显然值传递所传递的内存要比地址传递传递的内存大。