系列文章
C++ 系列 前篇 为什么学习C++ 及学习计划-CSDN博客
C++ 系列 第一篇 开发环境搭建(WSL 方向)-CSDN博客
C++ 系列 第二篇 你真的了解C++吗?本篇带你走进C++的世界-CSDN博客
C++ 系列 第三篇 C++程序的基本结构-CSDN博客
C++ 系列 第四篇 C++ 数据类型上篇—基本类型-CSDN博客
C++ 系列 第五篇 C++ 算术运算符及类型转换-CSDN博客
C++系列第六篇 数据类型下篇 - 复合类型(数组及字符串)-CSDN博客
C++系列第七篇 数据类型下篇 - 复合类型(结构体、共用体及枚举)-CSDN博客
C++系列第八篇 数据类型下篇 - 复合类型(指针及动态内存申请)-CSDN博客
前言
这一章节进行复合类型最后一部分的介绍,主要是指针的一些高级应用,包括指针和数组的的关系,指针和字符串的关系,指针和结构的关系, 变量基于内存位置进行的分类。 还会简单介绍下 C++特有的动态数组,vector 和 array。
指针和数组
进行过C编程的人一定进行过指针和数组等价使用,即申请的是数组,按指针操作,或者一个指针指向一块申请的内存,按数组进行操作。 C++中指针和数组也可以进行等价使用。指针和数组基本等价的原因在于指针算术和C++内部处理数组的方式。
首先,将整数变量加 1 后,其值将增加1:但将指针变量加1 后,增加的量等于它指向的类型的字节数。将指向 double 的指针加1 后,如果系统对 double 使用 8 个字节存储,则数值将增加 8;将指向short 的指针加1 后,如果系统对 short 使用 2 个字节存储,则指针值将增加 2。
如上的示例除了说明指针的数值计算外,还有一个比较重要的点就是将数组名解释为地址。大部分情况两者可以等价操作,可以像示例中一样使用指针的数值计算一样进行数组的数值计算,同时也能像取数组元素一样操作指针。
我们为什么说大部分情况可以等价操作,那就是有不等价的情况,两种情况,一是 指针是一个变量,可以像示例中 ptr = ptr+1; 即可以修改指针的值,而数组名不可以修改,它是常量;二是使用sizeof 进行计算,对数组应用 sizeof 运算符得到的是数组的长度,而对指针应用 sizeof 得到的是指针的长度,即使指针指向的是一个数组。
还有一个平常容易忽略的点是数组的地址 ,对数组取地址时,数组名不会被解释为其地址。我们上边说了数组名被解释为数组的地址,相当于第一个数组元素的地址,当对数组名应用地址运算符时,得到的是整个数组的地址,从数字上说,这两个地址相同;但从概念上说,&ptr[0](即 ptr)是一个4 字节内存块的地址,而&ptr是一个20 字节内存块的地址。因此,表达式ptr+1将地址值加4,而表达式&ptr+1 将地址加 20。换句话说,ptr是一个int 指针,而&ptr是一个这样的指针,即指向包含5个元素的int数组(int(*)[5])。具体使用的时候一定要注意。
指针和字符串
有三个特别需要注意的点
1、在cout 和多数 C++表达式中,char 数组名、char 指针以及用引号括起的字符串常量都被解释为字符串第一个字符的地址。
如果给 cout 提供一个字符的地址,则它将从该字符开始打印,直到遇到空字符为止。这意味着可以将指向char 的指针变量作为cout 的参数,因为它也是char 的地址。
cstring 中的strcpy 具体实现如图,可以看到从第一个字符的地址开始进行处理,直到碰到结束符为止
2、在将字符串读入程序时,应使用已分配的内存地址。该地址可以是数组名,也可以是使用 new初始化过的指针。
字符串读入一定是要有具体内存去存储它的,否则胡乱存储到未知内存的话,就会造成内存访问异常,进而出现程序挂死。而数组本身是一个已经由编译器分配好内存的的变量,new初始化过的指针也指向了一个动态分配好的内存。但是要特别注意,数组或者new 分配的内存要足够大,能够容纳需要读入的字符串,否则会造成内存访问溢出。
3、字符串赋值给字符数组时要使用strcpy 或者 strncpy 等拷贝函数, 直接 "array = ptr" 这种操作是不可取的,因为ptr 是一个指针,其值是一个地址,直接赋值相当于 把ptr 指向的地址 赋值给array 数组了,但是array 数组本身是有一个地址的,编译器不允许修改 数组变量的地址
指针和结构体
可以运行时创建动态结构,“动态”意味着内存是在运行时,而不是编译时分配的。由于类与结构非常相似,结构的大部分技术也适用于类。将new 用于结构由两步组成:创建结构和访问其成员。要创建结构,需要同时使用结构类型和new。例如,要创建一个未命名的 inflatable 类型,并将其地址赋给一个指针,可以这样做:inflatable *ps = new inflatable; 这将把足以存储 inflatable 结构的一块可用内存的地址赋给 ps。
动态创建的结构,不能将成员运算符句点用于结构名,因为这种结构没有名称,只是知道它的地址。需要使用箭头成员运算符(->) 来访问成员,这点和C 语言完全一眼。。例如,如果ps 指向一个inflatable 结构,则 ps->price 是被指向的结构的 price 成员。
另一种访问结构成员的方法是,如果 ps 是指向结构的指针,则*ps 就是被指向的值--结构本身。由于*ps 是一个结构,因此(*ps).price 是该结构的 price 成员。但基本没这么用的,平白增加程序的复杂度。
局部变量、静态变量及动态变量
根据分配内存的方法,或者是变量所属内存位置的不同, C及 C++有 3 种管理数据内存的方式:自动存储、静态存储和动态存储(有时也叫作自由存储空间或堆), 对应的变量分别也叫局部变量、静态变量及动态变量。
在函数内部定义的常规变量使用自动存储空间,被称为自动变量(automatic variable),这意味着它们在所属的函数被调用时自动产生,在该函数结束时消亡。自动变量是一个局部变量,其作用域为包含它的代码块。代码块是被包含在花括号中的一段代码。注意是代码块,而不是函数,我们目前为止举例都是整个函数举例,但其实一个函数里也可以分不同代码块的,如下所示, value 整形变量定义时使用{} 进行了包含, 所以属于一个单独的代码块,在代码快外使用,提示未定义。
静态存储是整个程序执行期间都存在的存储方式。使变量成为静态的方式有两种: 一种是在函数外面定义它;另一种是在声明变量时使用关键字static, 如 static double fee=56.50;。
自动存储和静态存储的关键在于,这些方法严格地限制了变量的寿命。变量可能存在于程序的整个生命周期(静态变量),也可能只是在特定函数被执行时存在(自动变量)。
动态存储new 和delete 运算符提供了一种比自动变量和静态变量更灵活的方法。它们管理了一个内存池,这在 C++中被称为自由存储空间(free store)或堆(heap)。该内存池同用于静态变量和自动变量的内存是分开的。new 和delete 使得一个变量能够在一个函数中分配内存,而在另一个函数中释放它。因此,数据的生命周期不完全受程序或函数的生存时间控制。
与使用常规变量相比,使用 new 和delete 让程序员对程序如何使用内存有更大的控制权。然而,内存管理也更复杂了。在栈中,自动添加和删除机制使得占用的内存总是连续的,但new和delete的相互影响可能导致占用的自由存储区不连续,这使得跟踪新分配内存的位置更困难。
如果使用new 运算符在自由存储空间(或堆)上创建变量后,没有调用 delete,则即使包含指针的内存由于作用域规则和对象生命周期的原因而被释放,在自由存储空间上动态分配的变量或结构也将继续存在。实际上,将会无法访问自由存储空间中的结构,因为指向这些内存的指针无效。这将导致内存泄漏。被泄漏的内存将在程序的整个生命周期内都不可使用;这些内存被分配出去,但无法收回。
我们在上一篇也提到过,指针时危险的,因为它们允许执行对计算机不友好的操作,如使用未经初始化的指针来访问内存或者试图释放同一个内存块两次,或者向上边提到的,因为未进行delete 操作,造成内存泄漏。大家使用的时候一定要特别注意。
数组的替代品
模板类 vector 和array是数组的替代品
模板类 vector 类似于string 类,是一种动态数组。可以在运行阶段设置vector 对象的长度,可在末尾附加新数据,还可在中间插入新数据。基本上,它是使用 new 创建动态数组的替代品。实际上,vector类确实使用 new 和 delete 来管理内存,但这种工作是自动完成的。
要使用vector 对象,必须包含头文件vector。其次,vector 包含在名称空间std 中,因此可使用using 编译指令、using 声明或std::vector。第三,模板使用不同的语法来指出它存储的数据类型。第四,vector 类使用不同的语法来指定元素数。
如下vi 是一个空的vector<int>对象 。 使用时可以使用对应的方法 resize 重新指定需要的大小并 按正常数组方式进行使用。
vd 是一个有三个元素的 vector<double> 对象,定义时分配了空间,所以可以直接访问范围内元素。
vector 类的功能比数组强大,但付出的代价是效率稍低。如果需要的是长度固定的数组,使用数组是更佳的选择,但代价是不那么方便和安全。鉴于此,C++11 新增了模板类array,它也位于名称空间std中。与数组一样,array 对象的长度也是固定的,也使用栈(静态内存分配),而不是自由存储区,因此其效率与数组相同。 好多学习资料上说array 有边界检查,不够准确,不是所有情况下都有边界检查,如下所示,使用max_size方法 ,获取到的确实 是定义时候的大小,但是 可以像C数组一样,超边界赋值,编译器并没有报错。只有通过at() 方法 时 才会捕捉这种越界错误,并进行停止,但是效率会 降低。
到这里为止,符合类型就都总结完了,意味着数据类型也都学完了,下一篇我们将开始表达式和语句的总结学习。