第04章 复合类型

本章内容包括:

  • 创建和使用数组
  • 创建和使用C-风格字符串
  • 创建和使用string类字符串.
  • 使用方法getline()和get()读取字符串.
  • 混合输入字符串和数字.
  • 创建和使用结构.
  • 创建和使用共用体.
  • 创建和使用指针.
  • 使用new和delete管理动态内存.
  • 创建动态数组.
  • 创建动态结构.
  • 自动存储,静态存储和动态存储.
  • vector和array类简介.

4.1 数组

  • 声明数组的通用格式:typeName arrayName[arraySize];
  • C++数组从0开始编号.
  • 有效下标值的重要性:编译器不会检查使用的下标是否有效.

4.1.1 程序说明:程序清单4.1 arrayone.cpp 
4.1.2 数组的初始化规则

  • 只有在定义数组时才能使用初始化,此后就不能使用了,也不能将一个数组赋给另一个数组.
  • 通常,让编译器计算元素个数是种很糟的做法,因为其计数可能与您想象的不一样.

4.1.3 C++11数组初始化方法

  • 首先,初始化数组时,可省略等号(=);
  • 其次,可不在大括号内包含任何东西,这将把所有元素都设置为零. 
    列表初始化禁止缩窄转换.

4.2 字符串

  • 让数组比字符串长没有什么害处,只是会浪费一些空间而已.这是因为处理字符串的函数根据空字符的位置,而不是数组长度来进行处理.
  • C++对字符串长度没有限制.
  • 警告:在确定存储字符串所需的最短数组时,别忘了将结尾的空字符计算在内.

4.2.1 拼接字符串常量 
4.2.2 在数组中使用字符串

  • sizeof运算符指出整个数组的长度:xx个字节,但strlen()函数返回的是存储在数组中的字符串的长度,而不是数组本身的长度.另外,strlen()只计算可见的字符,而不把空字符计算在内.

4.2.3 字符串输入 
4.2.4 每次读取一行字符串输入

  • 1.面向行的输入:getline()
  • 2.面向行的输入:get()
  • 为什么要使用get(),而不是getline()呢?首先,老式实现没有getline().其次,get()使输入更仔细.总之,getline()使用起来简单一些,但get()使得检查错误更简单些.
  • 3.空行和其他问题

4.2.5 混合输入字符串和数字

  • 程序清单4.6 numstr.cpp
  • C++程序常使用指针(而不是数组)来处理字符串.

4.3 string类简介

  • ISO/ANSI C++98标准通过添加string类扩展了C++库,因此现在可以string类型的变量(使用C++的话说是对象)而不是字符数组来存储字符串.
  • 程序清单4.7 strtype1.cpp
  • 在很多方面,使用string对象的方式与使用字符数组相同. 
    • 可以使用C风格字符串来初始化string对象
    • 可以使用cin来将键盘输入存储到string对象中.
    • 可以使用cout来现实string对象
    • 可以使用数组表示法来访问存储在string对象中的字符.
  • string对象和字符数组之间的主要区别是,可以将string对象声明为简单变量,而不是数组.

4.3.1 C++11字符串初始化

  • C++11也允许将列表初始化用于C风格字符串和string对象.

4.3.2 赋值,拼接和附加

扫描二维码关注公众号,回复: 4288180 查看本文章
  • 程序清单4.8 strtype2.cpp

4.3.3 string类的其他操作

  • 程序清单4.9 strtype3.cpp

4.3.4 string类I/O

  • 程序清单4.10 strtype4.cpp 
    • 首先,为初始化的数组的内容是未定义的;对于未被初始化的数据,第一个空字符的出现位置是随机的.
    • 其次,函数strlen()从数组的第一个元素开始计算字节数,直到遇到空字符.
  • 为何一个getline()是istream的类方法?在引入string类之前很久,C++就有istream类.因此istream的设计考虑到了注入double和int等基本C++数据类型,但没有考虑string类型,所以istream类中,有处理double,int和其他基本类型的类方法,但没有处理string对象的类方法.

4.3.5 其他形式的字符串字面值

  • C++11还支持Unicode字符编码方案UTF-8.在这种方案中,根据编码的数字值,字符可能存储为1~4个八位组.C++使用前缀u8来表示这种类型的字符串字面值.
  • C++11新增的另一种类型是原始(raw)字符串.在原始字符串中,字符表示的就是自己.原始字符串将”(“和”)”用作定界符,冰食用前缀R来标识原始字符串.输入原始字符串时,按回车键不仅会移到下一行,还将在原始字符串中添加回车字符.
  • 如果要在原始字符串中包含)”,该如何办呢?使用R”+(标识原始字符攒的开头时,必须使用)+“标识原始字符串的结尾.总之,这使用”+(和)+“替代了默认定界符”(和)”.

4.4 结构简介

  • 结构也是C++ OOP堡垒(类)的基石.
  • C++允许在声明结构变量时省略关键字struct.

4.4.1 在程序中使用结构

  • 结构声明的位置很重要.
  • 外部声明可以被其后面的任何函数使用,而内部声明只能被该声明所数的函数使用.通常应使用外部声明,这样所有函数都可以使用这种类型的结构.
  • 变量也可以在函数内部和外部定义,外部变量由所有的函数共享.C++不提倡使用外部变量,但提倡使用外部结构声明.
  • 另外,在外部声明符号常量通常更合理.

4.4.2 C++11结构初始化

  • 与数组一样,C++11也支持将列表初始化用于结构,且等号(=)是可选的.最后,不允许缩窄转换.

4.4.3 结构可以将string类作为成员吗?可以 
4.4.4 其他结构属性

  • C++使用户定义的类型与内置类型尽可能相似.
  • 还可以使用赋值运算符(=)将结构赋给另一个同类型的结构,这样结构中每个成员都将被设置为另一个结构中相应成员的值,即使成员是数组.这种赋值被称为成员赋值.
  • 程序清单4.12 assgn_st.cpp

4.4.5 结构数组

  • 程序清单4.13 arrstruct.cpp

4.4.6 结构中的位字段

  • 与C语言一样,C++也允许制定占用特定位数的结构成员,这使得创建与某个硬件设备上的寄存器对应的数据结构非常方便.字段的类型应为整型或枚举,接下来是冒号,冒号后面是一个数字,它制定了使用的位数.可以使用没有名称的字段来提供间距.每个成员都被称为位字段.
  • 位字段通常用在低级编程中.

4.5 共用体

  • 共用体union是一种数据格式,它能够存储不同的数据类型,但只能同时存储其中的一种类型.
  • 共用体的长度为其最大成员的长度.
  • 共用体常用语(但并非只能用于)节省内存.

4.6 枚举*

  • C++的enum工具提供了另一种创建符号常量的方式,这种方式可以代替const.
  • 对于枚举,制定已了赋值运算符.具体地说,没有为枚举定义算术运算.
  • 如果打算只适用常量,而不创建枚举类型的变量,则可以省略枚举类型的名称.

4.6.1 设置枚举量的值

  • 在C++早期的版本种,只能将int值(或提升为int的值)赋给枚举量,但这种限制取消了,因此可以使用long甚至long long类型的值

4.6.2 枚举的取值范围

  • 取值范围的定义: 
    • 首先,要找出上限,需要指导枚举量的最大值.
    • 找到大于这个最大值的,最小的2的幂,将它减去1,得到的便是取值范围的上限.
  • 选择用多少空间来存储枚举由编译器决定.
  • C++11扩展了枚举,增加了作用域内枚举.

4.7 指针和自由存储空间

  • 使用常规变量时,值是指定的量,而地址为派生量.
  • 指针与C++基本原理 
    • 面向对象编程与传统的过程性编程的区别在于,OOP强调的是在运行阶段(而不是编译阶段)进行决策.运行阶段指的是程序正在运行时,编译阶段指的是编译器将程序组合起来时.运行阶段决策就好比度假时,选择参观哪些景点取决于天气和当时的心情;而编译阶段决策更像不管在什么条件下,都坚持预先设定的日程安排.
    • 运行阶段决策提供了灵活性,可以根据当时的情况进行调整.例如,考虑为数组分配内存的情况.传统的方法是声明一个数组.要在C++中声明数组,必须制定数组的长度.因此,数组长度在程序编译时就设定好了;这就是编译阶段决策.您可能认为,在80%的情况下,一个包含20个元素的数组足够了,但程序有时需要处理200个元素.为了安全起见,使用了一个包含200个元素的数组.这样,程序在大多数情况下都浪费了内存.OOP通过将这样的决策推迟到运行阶段进行,使程序更灵活.在程序运行后,可以这次高速它只需要20个元素,而还可以下次高速它需要205个元素.
    • 总之,使用OOP时,您可能在运行阶段确定数组的长度.为使用这种方法,语言必须允许在程序运行时创建数组.稍后您会看到,C++采用的方法是,使用关键字new情况正确数量的内存以及使用指针来跟踪新分配的内存的位置.
    • 在运行阶段做决策并非OOP独有的,但使用C++编写这样的代码比使用C语言简单.
  • 程序清单4.15 pointer.cpp

4.7.1 声明和初始化指针

  • 指针声明必须制定指针指向的数据的类型.
  • *运算符两边的空格是可选的.传统上,C程序员使用这种格式:int ptr;这强调*ptr是一个int类型的值.而很多C++程序员使用这种格式:int ptr;这强调的是:int*是一种类型–指向int的指针.
  • int* p1,p2;声明创建一个指针p1和一个int变量;对每个指针变量名,都需要使用一个*
  • 注意:在C++中,int*是一种符合类型,是指向int的指针.
  • 和数组一样,指针都是基于其他类型的.
  • 程序清单4.16 init_ptr.cpp

4.7.2 指针的危险

  • 极其重要的一点是:在C++中创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存.为数据提供空间是一个独立的步骤,忽略这一步无疑是自找麻烦.
  • 警告:一定要在对指针应用接触引用运算符(*)之前,将指针初始化为一个确定的,适当的地址.这是关于使用指针的金科玉律.

4.7.3 指针和数字 
4.7.4 使用new来分配内存

  • 指针真正的用武之地在于,在运行阶段分配未命名的内存以存储值.在这种情况下,只能通过指针来访问内存.
  • 在C语言中,可以用库函数malloc()来分配内存;在C++中仍然可以这样做,但C++还有更好的方法-new运算符.
  • 为一个数据对象(可以是结构,也可以是基本类型)获得并制定分配内存的通用格式:typeName * pointer_name = new typeName;
  • 程序清单4.17 use_new.cpp 
    • 对于指针,需要指出的是,new分配的内存块通常与常规变量声明分配的内存块不同.变量nights和pd的值都存储在被称为栈stack的内存区域中,而new从被称为堆heap或自由存储区free store的内存区域分配内存.
    • 内存被耗尽?计算机可能会由于没有足够的内存而无法满足new的请求.在这种情况下,new通常会引发异常;而在较老的实现中,new将返回0.在C++中,值为0的指针被称为空指针null pointer.C++确保空指针不会指向有效的数据,因此它常被用来表示运算符或函数失败(如果成功,他们将返回一个有用的指针).就目前而言,只需如下要点:C++提供了检测并处理内存分配失败的工具.

4.7.5 使用delete释放内存

  • 一定要配对地使用new和delete;否则将发生内存泄漏.
  • 不要尝试释放已经释放的内存快,C++标准指出,这样做的结果将是不确定的,这意味着什么情况都可能发生.另外,不能使用delete来释放声明变量所获得的内存.
  • 警告:只能用delete来释放使用new分配的内存.然而,对空指针使用delete是安全的.
  • 注意,使用delete的关键在于,将它用于new分配的内存.这并不意味着要使用用于new的指针,而是用于new的地址.
  • 一般来说,不要创建两个指向同一个内存块的指针,因为这将增加错误地删除同一个内存块两次的可能性.

4.7.6 使用new来创建动态数组

  • 通常,对于大型数据(如数组,字符串和结构),应使用new,这正是new的用武之地.
  • 关于动态数组的两个基本问题:如何使用C++的new运算符创建数组以及如何使用指针访问数组元素. 
    • 1.使用new创建动态数组 
      • C++早起版本无法识别方括号表示法.然而,对于ANSI/ISO标准来说,new与delete的格式不匹配导致的后果是不确定的,这意味着程序员不能依赖于某种特定的行为.
      • 使用new和delete时,应遵守以下规则: 
        • 不要使用delete来释放不是new分配的内存.
        • 不要使用delete释放同一个内存块两次
        • 如果使用new[]为数组分配内存,则应使用delete[]来释放.
        • 如果使用new[]为一个实体分配内存,则应使用delete(没有方括号)来释放.
        • 对空指针应用delete是安全的.
      • 为数组分配内存的通用格式如下:type_name*pointer_name = new type_name [num_elements];
    • 2.使用动态数组 
      • C和C++内部都使用指针来处理数组.数组和指针基本等价是C和C++的优点之一(这在有时候也是个问题,但这是另一码事).
      • 程序清单4.18 arraynew.cpp 
        • 不能修改数组名的值.但指针是变量,因此可以修改它的值.

4.8 指针,数组和指针算术

  • 指针和数组基本等价的原因在于指针算术pointer arithmetic和C++内部处理数组的方式.
  • C++将数组名解释为地址.
  • 程序清单4.19 addpntrs.cpp

4.8.1 程序说明

  • 注意:将指针变量加1后,其增加的值等于指向的类型占用的字节数.
  • 对数组应用sizeof运算符得到的是数组的长度,而对指针应用sizeof得到的是指针的长度,即使指针指向的是一个数组.
  • 数组的地址:对数组取地址时,数组名也不会被解释为其地址.等等,数组名难道不被解释为数组的地址吗?不完全如此:数组名被解释为其第一个元素的地址,而对数组名应用地址运算符时,得到的是整个数组的地址.
  • 总之,使用new来创建数组以及使用指针来访问不同的元素很简单.只要把指针当做数组名对待即可.然而,要理解为何可以这样做,将是一种挑战.

4.8.2 指针小结

  1. 声明指针 
    • 要声明指向特定类型的指针,格式:typeName * pointerName;
  2. 给指针赋值 
    • 应将内存地址赋给指针.
    • 可以对变量名应用&运算符,来获得被命名的内存的地址,new运算符返回未命名的内存的地址.
  3. 对指针解除引用 
    • 对指针解除应用意味着获得指针指向的值.对指针应用解除引用或间接值运算符(*)来解除应用.
    • 另一种对指针解除引用的方法是使用数组表示法,例如,pn[0]与*pn是一样的.绝不要对未被初始化为适当地址的指针解除引用.
  4. 区分指针和指针所指向的值
  5. 数组名 
    • 在多数情况下,C++将数组名视为数组的第一个元素的地址.一种例外情况是:将sizeof运算符用于数组名用时,此时将返回整个数组的长度(单位为字节).
  6. 指针算术
  7. 数组的动态联编和静态联编 
    • 使用数组声明来创建数组时,将采用静态联编,即数组的长度在编译时设置
    • 使用new[]运算符创建数组时,将采用动态联编(动态数组),即将在运行时为数组分配空间,其长度也将在运行时设置.使用完这种数组后,应使用delete[]释放其占用的内存.
  8. 数组表示法和指针表示法 
    • 使用方括号数组表示法等同于对指针解除引用.

4.8.3 指针和字符串

  • 数组和指针的特殊关系可以扩展到C风格字符串.
  • 在C++中,用引号括起的字符串像数组名一样,也是第一个元素的地址.
  • 注意:在cout和多数C++表达式中,char数组名,char指针以及用引号括起的字符串常量都被解释为字符串第一个字符的地址.
  • 程序清单4.20 ptrstr.cpp 
    • 警告:在将字符串读入程序时,应使用已分配的内存地址.该地址可以是数组名,也可以四使用new初始化过的指针.
    • 警告:应使用strcpy()或strncpy(),而不是赋值运算符来将字符串赋给数组.

4.8.4 使用new创建动态结构

  • 创建动态结构时,不能将成员运算符句点用于结构名,因为这种结构没有名称,知识指导它的地址.C++专门为这种情况提供了一个运算符:箭头成员运算符(->).
  • 提示:有时,C++新手在制定结构成员时,搞不清楚何时应使用句点运算符,何时应使用箭头运算符.规则非常简单.如果结构标识符是结构名,则使用句点运算符;如果标识符是指向结构的指针,则使用箭头运算符.
  • C++的运算符优先规则要求使用括号,如:(*ps).price
  • 程序清单4.21 newstrct.cpp
  • 1.一个使用new和delete的示例 
    • 程序清单4.22 delete.cpp
  • 2.程序说明 
    • 将new和delete放在不同的函数中通常并不是个好方法,因为这样很容易忘记使用delete.

4.8.5 自动存储,静态存储和动态存储

  • 根据用于分配内存的方法,C++有3种管理数据内存的方式:自动存储,静态存储和动态存储(有时也叫作自由存储空间或堆).(C++11新增了第四种类型—线程存储).
  • 1.自动存储 
    • 在函数内部定义的常规变量使用自动存储空间,被称为自动变量,这意味着它们在所属的函数被调用时自动产生,在该函数结束时消亡.
    • 实际上,自动变量是一个局部变量,其作用域为包含它的代码块.
    • 自动变量通常存储在栈中.这意味着执行代码块时,其中的变量将依次加入到栈中,而在离开代码块时,将按相反的顺序释放这些变量,这被称为后进先出LIFO.因此,在程序执行过程中,栈将不断的增大和缩小.
  • 2.静态存储 
    • 使变量称为静态的方式有两种:一种是在函数外面定义它;另一种是在声明变量时使用关键字static.
  • 3.动态存储 
    • 栈,堆和内存泄漏:如果使用new运算符在自由存储空间(或堆)上创建变量后,没有调用delete,将发生什么情况呢?如果没有调用delete,则即使包含指针的内存由于作用域规则和对象声明周期的原因而被释放,在自由存储空间上动态分配的变量或结构也将继续存在.实际上,将会无法访问自由存储空间中的结构,因为指向这些内存的指针无效.这将导致内存泄漏.被泄漏的内存将在程序的整个证明周期内都不可使用;这些内存被分配出去,但无法收回.极端情况(不过不常见)是,内存泄漏可能会非常严重,以致于应用程序可用的内存被耗尽,出现内存耗尽错误,导致程序崩溃.另外,这种泄漏还会给一些操作系统或在相同的内存空间中运行的应用程序带来负面影响,导致他们崩溃.即使是最好的程序员和软件公司,也可能导致内存泄漏.要避免内存泄漏,最好是养成这样一种习惯,即同时使用new和delete运算符,在自由存储空间上动态分配内存,随后便释放它.C++智能指针有助于自动完成这种任务.
    • 注意:指针是功能最强大的C++工具之一,但也最危险,因为他们与女婿执行对计算机不友好的操作,如使用未经初始化的指针来访问内存或者试图释放同一个内存块两次.另外,在通过实践习惯指针表示法和指针概念之前,指针是容易引起迷惑的.

4.9 类型组合

  • 程序清单4.23 mixtypes.cpp

4.10 数组的替代品

  • 模板类vector和array是数组的替代品.

4.10.1 模板类vector

  • 模板类vector类似于string类,也是一种动态数组.基本上,它是使用new创建动态数组的替代品.实际上,vector类确实使用new和delete来管理内存,但这种工作是自动完成的.
  • 一般而言,下面的声明创建一个名为vt的vector对象,它可存储n_elem个类型为typeName的元素:C++ vector<typeName> vt(n_elem);其中参数n_elem可以是整型常量,也可以是整型变量.

4.10.2 模板类array(C++11)

  • vector类的功能比数组强大,但付出的代价是效率稍低.如果需要的是长度固定的数组,使用数组是更加的选择,但代价是不那么方便和安全.
  • 声明创建一个名为arr的array对象,它包含n_elem个类型为typename的元素:C++ array<ttypeName>,n_elem> arr;与创建vector对象不同的是,n_elem不能是变量.
  • 在C++11中,可将列表初始化用于vector和array对象,但在C++98中,不能对vector对象这样做.

4.10.3 比较数组,vector对象和array对象

  • 程序清单4.24 choices.cpp
  • array对象和数组存储在相同的内存区域(即栈)中,而vector对象存储在另一个区域(自由存储区或堆)中.
  • 将一个array对象赋给另一个array对象;而对于数组,必须逐元素赋值数据.
  • 与C语言一样,C++也不检查数组下标越(超)界的错误.
  • vector和array对象的成员函数at();

4.11 总结

  • C++98新增的标准模板库STL提供了模板类vector,它是动态数组的替代品.C++11提供了模板类array,它是定长数组的替代品.

4.12 复习题 
4.13 编程练习

附件:本章源代码下载地址

猜你喜欢

转载自blog.csdn.net/weixin_39345003/article/details/82118356