每文一诗 💪🏼
我见青山多妩媚,料青山见我应如是 ——宋/辛弃疾《贺新郎·甚矣吾衰矣》
译文:我看那青山潇洒多姿,想必青山看我也是一样。
🔥🔥 个人主页 点击🔥🔥
目录
指针的概念
指针存储的是内存地址,这个内存地址指向了存储其他数据的内存位置。借助指针,可以直接对内存中的数据进行访问和操作。
指针的形式
指针用“*”来表示,用“&”来表示取地址,
这里的“*”和数学运算中的“*”号一样,但是这里的“*”表示为他是一个指针变量,
但是当“*”后跟的是一个指针时,就变成了解引用运算符,它的作用是可以得到该指针存储的地址的内存中的值。
方式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;
}
输出(实参已经被改变):
🍎地址传递好处:
- 直接修改实参函数:内部可直接对实参所指向的内存空间进行修改
减少数据拷贝开销:值传递是数据的拷贝,当传递大型数据结构(像数组、结构体或类对象)时,值传递会对整个数据结构进行拷贝,这会消耗大量的时间和内存,而地址传递仅需传递数据的地址,通常为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;
}
显然值传递所传递的内存要比地址传递传递的内存大。