C++系列第八篇 数据类型下篇 - 复合类型(指针及动态内存申请)

系列文章

C++ 系列 前篇 为什么学习C++ 及学习计划-CSDN博客

C++ 系列 第一篇 开发环境搭建(WSL 方向)-CSDN博客

C++ 系列 第二篇 你真的了解C++吗?本篇带你走进C++的世界-CSDN博客

C++ 系列 第三篇 C++程序的基本结构-CSDN博客

C++ 系列 第四篇 C++ 数据类型上篇---基本类型-CSDN博客

C++ 系列 第五篇 C++ 算术运算符及类型转换-CSDN博客

C++系列第六篇 数据类型下篇 - 复合类型(数组及字符串)-CSDN博客

C++系列第七篇 数据类型下篇 - 复合类型(结构体、共用体及枚举)-CSDN博客

前言

这一章节进行复合类型最后一部分指针的介绍,包含指针的概念及一些基本的使用注意事项,顺带会介绍动态内存申请。熟悉C编程的都知道,指针是最难理解也最容易使用出错的一种数据类型,所以我们本章尽量先把概念介绍清楚,后边会再出一章,介绍指针的一些高级应用,比如指针和数组的一致及不一致性。

什么是指针

指针是一个变量,其存储的是值的地址,而不是值本身。常规变量的地址,只需对变量应用地址运算符(&),就可以获得它的位置;例如,如果 home 是一个变量,则&home 是它的地址。使用常规变量时,值是指定的量,而地址为派生量。而指针将地址视为指定的量,而将值视为派生量。

指针用于存储值的地址。因此,指针名表示的是地址。*运算符被称为间接值或解除引用运算符,将其应用于指针,可以得到该地址处存储的值。间接值运算符和乘法使用的符号相同;C++根据上下文来确定所指的是乘法还是解除引用。

指针的声明和初始化

我们看概念之前,先通过示例直观的感受下,示例中value_ptr 是一个指针,指向了char 类型(输出时做强制转换是因为cout 默认会把char * 指针按字符串进行输出,而不是打印地址,所以我们转换成void * 输出地址)

声明

计算机需要跟踪指针指向的值的类型。例如,char 的地址与 double 的地址看上去没什么两样,但char 和 double使用的字节数是不同的,它们存储值时使用的内部格式也不同。因此,指针声明必须指定指针指向的数据的类型。例如,int *p_updates;这表明,*p_updates 的类型为 int。由于*运算符被用于指针,因此p_updates 变量本身必须是指针。我们说p_updates 指向int 类型,我们还说p_updates 的类型是指向int 的指针,或int*。可以这样说,p_updates是指针(地址),而*p_updates 是 int。

*运算符两边的空格是可选的。传统上,C 程序员使用这种格式:int *ptr;这强调*ptr 是一个int 类型的值。而很多C++程序员使用这种格式:int* ptr:这强调的是:int*是一种类型--指向 int 的指针。在哪里添加空格对于编译器来说没有任何区别,甚至可以这样做:int*ptr。 我个人还是喜欢使用C语言的风格方式,因为平常做项目基本都是C主导,C++可能是其中的一些模块代码,要遵守整个项目的编程规范。

可以用同样的句法来声明指向其他类型的指针 ,如 double * tax_ptr;由于已将tax_ptr 声明为一个指向double 的指针,因此编译器知道*tax_ptr 是一个 double 类型的值。也就是说,它知道*tax_ptr 是一个以浮点格式存储的值,这个值占据 8 个字节(在大多数系统上)。指针变量不仅仅是指针,而且是指向特定类型的指针。tax_ptr 的类型是指向 double 的指针(或double *类型),ptr是指向 int 的指针类型(或int *)。尽管它们都是指针,却是不同类型的指针。和数组一样,指针都是基于其他类型的。

初始化

可以在声明语句中初始化指针。在这种情况下,被初始化的是指针,而不是它指向的值。如开始的示例中 char *value_ptr; value_ptr = &value; 这两句代码,可以合并为 char *value_ptr = &value; 这种声明时初始化的方式。

指针很危险

创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存。为数据提供空间是一个独立的步骤,C语言使用malloc 为数据存储申请空间,C++使用new 为数据存储申请空间。 不管是哪种申请方式,切记,一定要让先给指针指向正确、可用的地址空间,才能操作指针,进行写值操作,否则就是访问野指针,很容易造成进程crash。

复制代码
long *ptr;
*ptr = 2233;

如上边的代码,ptr 确实是一个指针,但它指向哪里呢?上述代码没有将地址赋给ptr。那么 2233 将被放在哪里呢? 我们不知道。由于ptr没有被初始化,它可能有任何值。不管值是什么,程序都将它解释为存储223323 的地址。如果fellow 的值碰巧为 1200,计算机将把数据放在地址1200 上,即使这恰巧是程序代码的地址。fellow 指向的地方很可能并不是所要存储 223323 的地方。这种错误可能会导致一些最隐匿、最难以跟踪的bug。

指针和数字

指针不是整型,虽然计算机通常把地址当作整数来处理。从概念上看,指针与整数是截然不同的类型。整数是可以执行加、减、除等运算的数字,而指针描述的是位置,将两个地址相乘没有任何意义。从可以对整数和指针执行的操作上看,它们也是彼此不同的。因此,不能简单地将整数赋给指针。

复制代码
int *pt;
pt = 0xB8000000; // type mismatch

上边的示例中,左边是指向int的指针,因此可以给它赋地址值,但右边是一个整数,这条语句没有个告诉程序,这个数字是一个地址,C99之前,C语言允许这样赋值。但C++在类型一致方面要求更严格,编译器将显示一条错误消息,通告类型不匹配。要将数字值作为地址来使用,应通过强制类型转换将数字转换为适当的地址类型,即 "pt = (int *)0xB8000000;" 。

动态申请及释放内存

上边声明和初始化章节,我们是将指针初始化为变量的地址;变量是在编译时分配的有名称的内存,而指针只是为可以通过名称直接访问的内存提供了一个别名。

指针真正的用武之地在于,在运行阶段动态分配未命名的内存以存储值。在这种情况下,只能通过指针来访问内存。在 C 语言中,可以用库函数malloc()来分配内存;在C++中仍然可以这样做,但C++还有更好的方法--new运算符。代码要告诉new,需要为哪种数据类型分配内存;new将找到一个长度正确的内存块,并返回该内存块的地址。代码的责任是将该地址赋给一个指针 。

复制代码
int *pn= new int;

new int 告诉程序,需要适合存储int 的内存。new运算符根据类型来确定需要多少字节的内存。然后,它找到这样的内存,并返回其地址。接下来,将地址赋给 pn,pn 是被声明为指向 int 的指针。现在,pn 是地址,而*pn 是存储在那里的值。

需要指出的另一点是,new分配的内存块通常与常规变量声明分配的内存块不同。我们上边举例中 函数中的 变量 值都存储在栈(stack)的内存区域中,而new从被堆(heap)或自由存储区(free store)的内存区域分配内存。

当需要内存时,使用new 来请求。不需要的时候,使用 delete 运算符,将new 申请的内存归还给内存池。归还或释放(free)的内存可供程序的其他部分使用。使用 delete 时,后面要加上指向内存块的指针(这些内存块最初是用new 分配的)。一定要配对地使用new 和delete;否则将发生内存泄漏(memory leak),也就是说,被分配的内存再也无法使用了。如果内存泄漏严重,则程序将由于不断寻找更多内存而终止。不要尝试释放已经释放的内存块,C++标准指出,这样的结果将是不确定的,这意味着什么情况都可能发生。另外,不能使用 delete 来释放声明变量所获得的内存。

只能用 delete 来释放使用 new 分配的内存。对空指针使用delete 也是安全的。注意,使用 delete 的关键在于,将它用于new 分配的内存。这并不意味着要使用用于new 的指针,而是用于new的地址。

使用new创建动态数组

如果程序只需要一个值,则可能会声明一个简单变量,因为对于管理一个小型数据对象来说,这样做比使用 new 和指针更简单。使用new 去动态申请内存更适合大型数据结构。

在 C++中,创建动态数组很容易;只要将数组的元素类型和元素数目告诉new 即可。必须在类型名后加上方括号,其中包含元素数目。例如,要创建一个包含 10个int 元素的数组,可以如下这样做,new 运算符返回申请的容纳10个int 的数组空间的地址,或者说是第一个int元素的地址。

复制代码
int *array = new int [10];

要删除上边申请的内存,如下使用如下语句,方括号告诉程序,应释放整个数组,而不仅仅是指针指向的元素。

复制代码
delete [] array;

使用 new 和 delete 时,应遵守以下规则。

1)不要使用delete来释放不是new分配的内存。

2)不要使用 delete释放同一个内存块两次。

3)如果使用new[]为数组分配内存,则应使用delete []来释放。

4)如果使用new 为一个实体分配内存,则应使用delete(没有方括号)来释放。

5)对空指针应用 delete 是安全的。

6)不能使用sizeof 来确定动态分配的数组包含的字节数。

相关推荐
zhangfeng11331 小时前
openclaw skills 小龙虾技能 通讯仿真 matlab skill Simulink Agentic Toolkit,通过kimi找到,mcp通讯
开发语言·matlab·openclaw·通讯仿真
chao1898447 小时前
基于 SPEA2 的多目标优化算法 MATLAB 实现
开发语言·算法·matlab
赏金术士8 小时前
Kotlin 习题集 · 高级篇
android·开发语言·kotlin
楼兰公子8 小时前
buildroot 在编译rust时裁剪平台类型数量的方法
开发语言·后端·rust
知识领航员9 小时前
蘑兔AI音乐深度实测:功能拆解、实测表现与适用场景
java·c语言·c++·人工智能·python·算法·github
吴声子夜歌9 小时前
Go——并发编程
开发语言·后端·golang
ooseabiscuit9 小时前
Laravel4.x:现代PHP框架的奠基之作
java·开发语言·php
c1s2d3n4cs10 小时前
Qt模仿nlohmann::json进行序列化和反序列化
开发语言·qt·json
AiTop10010 小时前
Claude Code 推出 Agent View:命令行编程正式进入“多线程并发“时代
开发语言·人工智能·ai·aigc
jf加菲猫11 小时前
第21章 Qt WebEngine
开发语言·c++·qt·ui