第4章 复合类型

4.1 数组

  • 数组大小必须为常量或者常量表达式,即必须编译时已知(可以用 new 操作符避开这种限制)
  • 不会检查下标是否有效
  • 老式c++实现可能会在数组初始化中出现问题,补救措施是在数组声明中使用 static
  • 如果没有初始化,其元素值将是不确定的(值为过去驻留在该内存中的值)
  • sizeof 返回整个数组中的字节数

4.2 字符串

两种实现:来自C语言(C-style string),和基于 string 类库的方法

C语言实现的字符串:

  • 将字符串存储在 char 数组中,以空字符(\0,ASCII码值为0)结尾。

  • 可以以引号括起来的字符串(称为 字符串常量/字符串字面值)进行初始化(char fish[] = "Bubbles" char bird[10] = "Mr. Cheeps"),如果自己指定初始化时数组的长度,记得将结尾的空字符计算在内

  • 同样地,也可以使用 cin >> 来用键盘或文件输入来初始化字符串数组

    • 使用 cin 输入字符串的问题:1. cin 使用空白(空格、制表符和换行符)对字符串定界,也就是说不能将含有空格分隔的字符串作为一个字符串读入。 2. cin 输入的字符串可能比目标数组长
    • 对于上述1. 中的问题,可以考虑使用 istream 中的类提供的面向行的类成员函数:getline()get()
      • cin.getline():通过回车键输入的换行符决定输入结尾。包含两个参数,第一个用于存储的数组名称,第二个时要读取的字符数(如果为20,则函数最多读取19个字符+自动在结尾添加的空字符
      • cin.get():一种实现(cin.get(name,size))与 getline() 类似,但是区别是无法读取并丢弃换行符(即第二次使用开始会因为无法跳过 /n 而读取不到任何内容);另一种(cin.get())可以读取下一个字符(包括换行符,一般用于处理 /n 而为下一行输入做好准备);另一种使用方式是拼接cin.get(name,size).get()),与前后连续调用两个函数效果相同。
      • 如何选择 get() 或 getline() :getline() 使用起来更简单,但是 get() 更容易检查错误(使用第二个 get() 可以判断输入是因为遇到 \n 停止还是因为数组填满而停止
      • 空行与其他问题:get() 将在读取空行后设置失效位,接下来的输入将被阻断,直到使用 cin.clear() 恢复;以及对于输入比指定分配空间长的问题,如果发生,那么 getline() 和 get() 将把余下的字符留在输入队列中,而 getline() 还会设置失效位并关闭后面的输入。具体在 5,6 和 17 章介绍这些内容。
  • 混合使用 cin >> 输入数字与 getline() 输入面向行的字符串的问题:如果先用 cin >> 输入数字再试图用 getline() 输入字符串,那么上一次输入留下的 \n 将在 cin >> 后留在输入队列中,导致 getline() 得到一个空行。这种情况可以使用无参的 get() 来处理换行符

string 类库实现的字符串:

  • 使用前提:包含头文件 string,并使用名称空间 std (using namespace std)
  • 特性
    • 可以使用C-风格字符串初始化:string str = “Dejavu”
    • 可以使用 cin >> 存储键盘输入: cin >> str
    • 可以使用 cout << 显示 string 对象: cout << str
    • 可以使用数组表示法访问 string 对象中的字符:count << str[2]
    • 支持原本数组表示不支持的操作:对象赋值,拼接
    • 在 string 类出现以前,如果需要对 C-style 字符串进行这些操作,一般使用 strcpy() 函数和 strcat() 函数;但是这两个函数容易出现目标数组过小导致内存覆盖的危险(string 类具有自动调节大小的功能所以可以避免这类危险),如果需要在字符数组上规避这类问题,可以使用库提供的类似函数 strncat()strncpy(),它们接受指出目标数组最大允许长度的第三个参数从而保证安全,但是也使得变成更加复杂
      • 使用 str.size() 形式的表示方法来确定字符串查长度,相较于 C-style 字符串的 strlen(char1) 形式的表示方法
  • 在 I/O 细节上 string 类 与 C-style 字符串的一些区别,观察下面的程序
#include <iostream>
#include <string>
#include <cstring>
int main(){
    using namespace std;
    char char1[20];
    string str;
    cout << "Length of string in char1 before input: "
        << strlen(char1) << endl;
    cout << "Length of string in str before input: "
        << str.size() << endl;
    cout << "Enter a line of text:\n";
    cin.getline(char1, 20);     //indicate maximum length
    cout << "You entered: " << char1 << endl;
    cout << "Enter another line of text:\n";
    getline(cin, str);
    cout << "You entered: " << str << endl;
    cout << "Length of string in char1 after input: "
        << strlen(char1) << endl;
    cout << "Length of string in str after input: "
        << str.size() << endl;
    return 0;
}

/*
输出结果:
Length of string in char1 before input: 27
Length of string in str before input: 0
Enter a line of text:
peanut butter
You entered: peanut butter
Enter another line of text:
blueberry jam
Length of string in char1 after input: 13
Length of string in str after input: 13

上述结果可以得出以下结论:

  • 在数组初始化之前,strlen(char1) 返回值为27,因为函数 strlen() 根据数组第一个元素直到遇到空字符的长度计算字符串长度,而未初始化的数组中第一个空字符出现的位置随机,导致数组长度随机;str.size() 的返回值为0,因为未初始化的 string 对象长度被自动设置为 0。

  • 观察将一行输入到数组中的代码: cin.getline(char1, 20) 这说明函数 getline() 是 istream 类的一个类方法(cin 是一个 istream 对象)。而输入到 string 对象中的代码: getline(cin, str) 说明 getline() 不是类方法,而是将 cin 作为参数,指出到哪里去查找输入。那么为什么 前一个 getline 是istream 的类方法,而另一个不是? 因为 istream 类在引入 string 类之前很久就有,所以 istream 的设计考虑到了 double int 等c++基本类型而没有考虑 string 类型。但是 为什么 cin >> str cin >> x 都是可行的?因为后者是 istream 类的一个成员函数,而前面处理 string 对象的代码是 使用 string 类的一个友元函数。有关友元函数以及这种技术为何可行,将在第 11 章介绍

4.4 结构(struct)简介

  • 使用结构来创建包含多种类型元素的一种数据格式,然后进一步使用 结构数组 来保存多份这种内容

  • 变量,结构都可以在函数内部或者函数外部定义。定义在外部(外部声明)表示该内容被所有函数共享

    • C++不提倡使用外部变量,但是提倡使用外部结构声明
  • 初始化:使用与定义结构时类似的方法初始化(逗号分隔的值列表,用花括号括起)

  • 可以将一个结构变量赋给另一个同类型的结构变量。

    • 所以可以将结构定义结构变量声明放在一起,甚至是变量初始化
  • 与C结构不同,C++结构可以含有 成员函数

  • 允许为每个成员定义位字段(指定该成员占用的存储位数),一般在低级编程中使用

4.5 共用体(union)

  • 能偶存储不同的数据类型,但是只能同时存储其中一种类型。
  • 语法与结构类似,不同成员名称不同
  • 用途之一是,当 数据项使用两种或更多格式 时可以节省空间(需要自行设置一个标识符变量来确定到底使用哪个格式)

4.6 枚举(enum)

  • 提供另一种创建符号常量的方式来代替 const

  • 语法与结构相似。

    • 如果不显示为内部符号常量指定整数值,那么其默认值将按定义顺序从 0~length-1 递增
  • 一般来说不能将非 enum 值赋给 enum 变量(比如:band = 7 band = orange + red 等,但有的实现可能没有这种限制,(但是可以在算数表达式中使用 enum ,这时 enum 值将被提升为 int 值)

  • 原本 enum 只能被赋予声明中指出的值,但是现在C++可以通过强制类型转换赋给 enum 变量一些其他的合法值,每个 enum 可以被赋予的取值范围(range)根据声明值定义情况决定(有一套规则)

4.7 指针

  • 一个指针的解除引用表示 *point 与point指向的变量完全等价,可以通过 *point 获得值,也可以将值赋给 *point 来修改所指向的变量

  • 语法:

    • 基本表示:int * p1前后的空格分布任意,int *p1; int* p1 也都是合法表示方法。但是 int* p1,p2 将创建一个指针 p1 和常规int变量 p2,也就是说对于每个指针变量名都需要一个 \
    • 不能将一个基本类型值(即使它可能代表一个地址)赋给一个指针,如果一定要这么做需要用 (int *) 进行强制类型转换
  • 如果使用 int * p1后没有对 p1 进行初始化,p1 可能会指向任何一个地址(p1可能为任何值),如果这个时候使用 *p1 对指针指向的数据进行操作,那么可能会造成严重的bug

4.7.4 使用 new 来分配内存

  • 使用指针的真正作用是实现重要的 OOP 技术——运行时内存分配。与静态分配相对应,运行时内存分配可以将必要的内存分配延迟到运行时再进行

  • C++ 可以使用 C语言中 malloc() 库函数来分配内存,但是更好的方法是使用 new 操作符

  • 语法:

    • 基本表示:int * p = new int。然后可以通过 *p = somevalue 来对p指向的数据对象赋值
    • 如果计算机没有足够的内存,那么 new 将返回 0,还可能引发 bad_alloc 异常 ,值为0的指针被称为空值指针。
  • 使用 delete p 来释放内存

    • 不要释放已经释放的内存块,同时也不要创建两个指向同一个内存块的指针(释放时可能会删除同一个内存块两次)

4.7.6 使用 new 创建动态数组;指针算术

  • 语法

    • 基本表示:int * plist = new int [10],new 操作符将返回第一个元素的地址。
    • 使用delete释放时,也要显示表示是在释放数组:delete [] plist
  • 可以使用与数组表示法同样的方法来访问动态数组中的元素:即,*(plist + 1) = plist[1]这样做的原因是,C 和 C++ 内部都是用指针来处理数组)

  • 在很多情况下可以使用相同的方法使用指针名和数组名,比如可以使用数组方括号表示法,也可以使用解除引用操作符(*)。但是也存在区别

    • 区别1:可以对指针的值进行修改。比如将 plist + 1 将导致表达式 plist[0] 指向数组的第二个值(虽然 int 地址通常相差 2 或 4 个字节,这说明算术指针的特别性)
    • 区别2:对数组应用 sizeof 将得到数组长度,而对指针应用 sizeof 将得到指针的长度,即使指针指向一个数组。
  • 指针算术

  • 根据输入情况动态控制分配的数组大小

    • cin >> temp; char * pn = new char[strlen(temp) + 1]

4.8.2 指针与字符串

  • 指针与字符串(char数组表示的)也具有相似的性质,比如 cout 对象将 char 数组名指向 char 的指针以及用引号括起的字符串常量按照相似的方式处理(都解释为字符串第一个字符的地址)

    • 一般来说,如果给 cout 提供一个指针,将打印地址,但是如果指针的类型为 char *,将显示指向的字符串
    • 考虑如何将一个字符串数组的副本放到一个指针中

4.8.3 使用 new 创建动态结构

  • 语法:

    • somestruct * ps = new somestruct
  • 指向结构的指针无法使用句点方式访问成员,作为代替,使用箭头成员操作符(->)访问成员。另一种方法是,通过 *ps 获得原结构本身,然后再用句点表示法: (*ps).price

4.8.4 自动存储、静态存储和动态存储

根据用于分配内存的方法,C++有 3 种用于管理数据内存的方式:自动存储、静态存储和动态存储(或称作自由存储空间或堆)

  • 自动存储

    • 适用于函数内部定义的常规变量,称为自动变量(automatic variable)
    • 在函数调用时自动产生,函数结束时消亡
    • 是一种局部变量,作用域为包含它的代码块(一般是整个函数,但是函数内也可以有代码块)
  • 静态存储

    • 在整个程序执行期间都存在的存储方式。
    • 有两种方式让一个变量变为静态:一种是在函数外定义,一种是在声明时使用 static
  • 动态存储

    • 使用 new 和 delete 操作符管理一个内存池,C++中称为 自由存储空间(free store)
    • 使得 数据的声明周期不完全受程序或者函数的生存时间控制。程序员对这些数据具有更大的控制权
    • 内存泄漏:如果没有调用 delete 释放创建变量的内存,即使包含指针的内存因为作用域规则或对象生命周期的原因被释放,自由存储空间上动态分配的变量或结构也将继续存在。结果是,(个人理解)代表这些内存的数据结构在C++程序中继续存在(但是在代码层面上不再有访问窗口),C++程序(在生命周期内)认为这些内存仍在被使用而无法将这部分内存回收分配给其他部分使用,极端情况下导致程序可用内存耗尽而崩溃

第5章 循环与关系表达式

5.6 二维数组

  • C++没有提供二维数组类型,但是用户可以创建每个元素本身都是数组的数组。比如:int maxtemps[4][5] 表示一个包含 4 个元素的数组,其中每个元素都是由 5 个整数组成的数组
  • 同样地,从存储空间角度更好地做法是用指针数组

第6章 分支语句和逻辑操作符

6.3 字符函数库 cctype

  • C++从 C 语言继承了一个与字符相关的方便的函数软件包,可以用于确定字符是否为大写字母、数字、标点符号等工作

image.png

6.8 简单文本文件读取/写入

6.8.2 文本文件写入

  • 包含头文件 fstream (I/O 用),指明名称空间 std:
  • 声明一个自己的 ofstream 对象(尽管头文件 iostream 提供一个预先定义好的名为 cout 的 ostream 对象,但是必须声明自己的 ofstream 对象
  • 通过 fout.open(filename) 的形式将 ofstream 对象和特定的文件关联
  • 将 ofstream 对象和文件关联起来之后,就可以像使用 cout 那样的 ofstream 对象一样的使用方式使用它(如 **<<、endl、setf()**等)

6.8.3 文本文件读取

  • 包含头文件 fstream (I/O 用),指明名称空间 std:

  • 声明一个自己的 ifstream 对象(尽管头文件 iostream 提供一个预先定义好的名为 cin 的 ifstream 对象,但是必须声明自己的 ifstream 对象

  • 使用 fin.open(filename) 的形式将 ofstream 对象和特定的文件关联

  • 将 ifstream 对象和文件关联起来之后,就可以像使用 cin 那样的 ofstream 对象一样的使用方式使用它(如 **>>**)

    • 典型的过程是声明一个特定类型变量,然后使用类似 fin >> number 的形式从文件读取一个该类型的数据

第7章 函数

7.3 函数和数组

7.3.5 指针和const

  • 禁止将 const 数据地址赋给非 const 指针

  • 指向const的指针和const指针

    • 指向const的指针: const int \* p = &num
      • 禁止修改 p 指向数据的值
      • p 可以指向另一个变量
    • const 指针:**int \* const p = &num**
      • 可以修改 p 指向数据的值
      • 无法变更 p 指向另一个变量
    • 同样的,可以组合起来声明指向 const 对象的 const 指针: const int * const p = &num,使得 既不能修改 p 指向数据的值,也不能变更其指向另一个变量
  • 如果不想在函数中修改指针或者对象的值,应该在函数参数定义时就应该将其加上 const 参数

7.6 函数与结构

  • 可以将结构作为参数传递,此时与基本类型类似,使用按值传递。也可以传递结构地址,使用指针访问。C++提供了第三种选择——按引用传递(在第 8 章介绍)

7.9 函数指针

  • 函数也有地址。可以编写将另一个函数地址作为参数的函数,使得第一个函数能找到第二个函数,并运行它,这允许其在不同的时间使用不同的函数

7.9.1 函数指针基础

  • 尝试让函数使用另一个函数地址作为参数的典型过程如下:

    • 获取函数地址
    • 声明一个函数指针
    • 使用函数指针调用函数
  • 获取函数地址:

    • 使用原函数名获得函数地址(使用 foo 而不是 foo()
  • 声明函数指针

    • 如果有一个函数原型: double foo (int),则其对应正确的指针类型可表示为:**double (\*pf) (int)**
    • 注意区别:double *pf (int) 代表一个返回指针的函数,而 (*pf) (int) 才代表一个指向函数的指针
    • 正确声明函数指针后,就可以将函数地址赋给它 pf = foo
  • 使用函数指针调用函数

    • 需要使用函数地址(即函数指针)的函数参数声明与指针定义基本相同:estimate(int lines, double (*pf) (int) )
    • (*pf) 的效果与函数名基本等价,所以 使用函数指针调用函数时与原本的表示基本相同:(*pf)(4)

第8章 函数进阶

8.1 C++内联函数

  • 内联函数是C++为了提高程序运行速度所做的一项改进

  • 常规函数调用过程:

    • 执行到函数调用指令
    • 存储指令内存地址,复制参数到堆栈
    • 跳跃到函数起点的内存单位,执行函数代码,(返回值放入寄存器)
    • 回到地址被保存的指令处
    • (可以发现来回跳跃并记录跳跃位置需要一定开销)
  • 内联函数用空间换时间内联函数的编译代码将被“内联”进其他程序代码,用相应函数代码替换函数调用以提高速度,适用于代码执行时间短,调用次数多的场景。

  • 使用内联函数:在 函数定义 与 函数声明前加上关键字 inline

  • 与宏的比较:

    • 宏是内联代码的原始实现。但是宏的限制在于:宏使用文本替换实现而不是按值传递实现,如果将其作用函数使用可能导致程序未按预期效果进行(比如 square(n++)等)

8.2 引用变量

  • 引用是已定义变量的一个别名
  • 主要用途是用作函数的形参。通过使用引用变量作为参数,函数将使用原始数据而不是拷贝。同时也为函数处理大型结构提供一种方便的途径

8.2.1 创建引用变量

  • int & numRef = num。C 和 C++使用 & 符号只是变量地址,但是C++给 & 符号赋予了另外一个含义用于声明引用。

  • numRef 和 num 具有相同的值和内存单位,改变其中任何一个将影响另一方

  • 引用必须在声明时初始化,关联后不再更改引用(类似于 const 指针,无法更换其指向的变量)

    • 如果试图将一个其他变量赋给一个引用,如:numRef = num2 ,将使得 numRef 和 num 的值同时变为 num2 (语句相当于: num = num2

8.2.2 引用作为函数参数

  • 按引用传递允许被调用的函数访问调用函数中的变量,而不是拷贝

  • const 引用参数

    • 如果像让函数使用传递给它的信息,而又不对这些信息进行修改(但同时又想使用引用),可以在参数中使用 const 引用
  • 传递引用的限制更为严格,如果试图将一个表达式(比如 x+3)作为引用参数传递,可能无法在编译器中通过(如果通过,将创建一个临时变量)

  • 临时变量

    • 仅当参数为 const 引用时,C++才允许临时变量。(因为对临时变量的修改是不正确的)
    • 在下面两种情况生成临时变量:
      • 实参类型正确,但不是左值(可被引用的数据对象,字面常量和包含多项的表达式以外)
      • 实参类型不正确,但可以转换为正确的类型
    • 这种情况下,编译器将在类型转换 / 计算表达式结果后,创建一个临时匿名变量,并让引用指向它
  • 引用作为返回值与 const 引用作为返回值

    • 正常情况下,返回机制将返回值复制到临时存储区域,然后调用程序访问该区域。返回引用使得程序直接访问返回值(即自己的一个临时变量)
    • 但是如果选择使用返回引用,应当尽量避免返回当函数终止时不再存在的内存单元引用(比如临时变量)**,这样会导致程序试图引用已经释放的内存导致程序崩溃**(对于返回指针同理
      • 为了避免这种问题,最简单的方法就是返回一个参数引用
      • 或者使用 new 来分配新的存储空间
    • const 引用返回与 const 引用参数机制类似,如果不想让程序使用返回的引用来直接修改它指向的结构,那么应该考虑选择 const 引用返回

8.2.6 对象、继承和引用

  • 基类引用可以指向派生类对象

8.3 默认参数

  • 对于带参数列表的函数,要求必须从右向左添加默认值

8.4 函数重载(多态)

  • C++允许定义名称相同的函数,只要它们的特征标(参数数目和类型相同)

8.5 函数模板

  • C++编译器允许用通用类型来指定函数参数进行编程(模板特性也被称为 参数化类型(parameterized types)
  • 语法:
template <class Any>
void Swap (Any &a, Any &b);
// ...
template <class Any>
// 或者使用 template <typename Any>
void Swap (Any &a, Any &b){
    Any temp;
    temp = a;
    a = b;
    b = temp;
}
  • 注:C++函数模板与Java泛型的对比:

    • Java泛型:存在类型擦除,每个泛型代码只生成一份代码,运行时存在 type cast 开销。
    • C++模板:为每个reified generic type生成一份独立的代码,使得代码量更大,但是执行更快

8.5.1 重载的模板

  • 有时,并非所有的类型都使用相同的算法。为了满足这种要求,可以像重载常规函数定义那样重载模板定义。

8.5.2 显式具体化

  • 对于某种需要特定处理的参数类型,可以提供一个具体化函数定义——称为显式具体化(explicit specialization)
  • 具体化机制随着C++的演变不断变化,这里介绍C++标准定义的形式(第三代具体化(ISO/ANSI C++标准))
// 对于给定的函数名,可以有
// 非模板函数、模板函数和显式具体化模板以及它们的重载版本

// non-template function prototype
void Swap (job &, job &);

// template prototype
template <class Any>
void Swap (Any &, Any &);

// explicit specialization for the job type
template <> void Swap<job> (job &, job &)
  • 如果有多个原型(prototype),编译器选择原型时,优先级为:非模板版本>> 显式具体化 >> 模板版本

8.5.3 实例化和具体化

  • 隐式实例化(impliclit instantiation):代码中包含函数模板时,只是提供一个用于生成函数定义的方案,本身并不会自动生成所有的需要的特定类型代码。当调用给定类型函数(比如:int i, j; swap(i,j))时,编译器将生成 swap 的一个实例,而该实例使用 int 类型。编译器之所以知道需要定义,是因为程序调用 Swap() 函数时提供了 int 参数

  • 同理,也存在显式实例化(expliclit instantiation):直接命令编译器创建特定实例

    • 语法示例:template void Swap<int>(int,int); 或者 template void Swap<>(int,int);
    • 和显式具体化的区别:
      • 显式具体化关注函数模板的重新定义。而显式实例化直接根据原模版的定义与声明生成一个示例函数,只需声明。
      • 同一个编程单元中使用同一种类型的显式实例和显式具体化将出错

8.5.4 编译器选择函数版本策略(重载解析)

过程:

  • 创建候选函数列表(包含所有与被调用函数名称相同的函数和模板函数)

  • 创建可行参数列表(包含所有参数数目正确的函数,包含隐式转换与模板替换等操作)

  • 确定是否有最佳可行函数,判断顺序为:

    • 完全匹配。
      • 常规函数优先于模板
      • 都是模板函数的场合
        • 显式具体化优于隐式具体化
        • 选择发生转换更少的函数(显式指出指针等)
    • 提升转换(char,short => int; float => double
    • 标准转换(int => char; long => double
    • 用户自定义的转换

完全匹配

进行完全匹配时,C++允许某些无关紧要的转换,比如从 int 到 int& 等,见下表

image.png

假设有这样的的函数代码

struct blot {int a; char b[10]; };
blot ink = {25, "spots"};
// ...
recycle(ink);
// can't match a suitable function
void recycle (blot);            // #1
void recycle (const blot);      // #2
void recycle (blot &);          // #3
void recycle (const blot &);    // #4

可以看出下面所有的原型都是匹配的,即不存在最佳的可行函数,则编译器无法完成重载解析。

但是如果只定义 #3 和 #4,则可以匹配成功,会优先选择 #3 ,因为指向非 const 数据的指针和引用优先与非 const 的指针和引用参数匹配

第9章 内存模型与名称空间

9.1 单独编译

  • C++允许并鼓励程序员将组件函数放在独立的文件
  • 使用 include 包含另一个文件时,编译器会分别创建各个源代码文件的对象代码文件,然后由链接程序将对象代码文件与库代码,启动代码结合在一起,生成一个可执行文件
  • 链接编译模块时,应该确保所有对象文件或库是由同一个编译器生成的

9.2 存储持续性、作用域和链接性

C++使用三种不同方案来存储数据,这些方案的区别在于数据保存在内存中的时间

  • 自动存储持续性:在函数中声明的变量(包括函数参数)。程序开始执行所属函数/代码块时被创建,执行完后内存被释放。
  • 静态存储持续性函数外定义的变量和使用 static 定义的变量。在程序整个运行过程都存在
  • 动态存储持续性:用 new 操作符分配的内存,知道使用 deletet 操作符释放或者程序结束

9.2.1 作用域与链接

  • 作用域(scope)描述某个名称在文件的多大范围内可见

  • 链接性(linkage)描述名称如何在不同单元间共享。

    • 链接性为外部的名称可在文件间共享。内部 => 文件内的函数共享。自动变量的名称没有链接性,因为它们不能共享。

9.2.2 自动存储持续性

  • 在 函数/代码块 中声明的变量(包括函数参数),默认情况下:

    • 自动持续性,局部作用域,无链接性

自动变量与堆栈

自动变量使用堆栈管理的实现过程:

  • 流出一段内存作为堆栈
  • 使用两个指针跟踪堆栈(一个指向栈底,一个指向堆顶)
  • 函数被调用时,其自动变量加入到堆栈,栈顶指针移动到变量后的下一个可用的内存单元
  • 函数结束后,栈顶指针被重置为函数被调用前的值,从而释放函数变量使用的内存

寄存器变量

  • C++支持使用 register 来声明寄存器变量
  • 即使不显式请求,编译器也有可能自动在一些地方使用寄存器(比如 for 循环)
  • register 不一定保证变量一定用寄存器来存储

9.2.3 静态持续变量

  • C++ 为静态存储变量提供三种链接性:外部,内部,无链接

  • 创建语法规定:

    • 外部:在代码块外部声明;
    • 内部:在代码块外部声明,且使用 static 限定符;
    • 无链接:在代码块内部声明,且使用 static 限定符;
  • 作用域解析操作符(::)

    • 默认情况下,同名的局部变量将隐藏全局变量,使用 :: 可以访问该同名变量的全局版本
  • 外部静态变量的使用

    • 如果有一个文件试图共享其他文件所定义的外部全局变量,可以在头文件 extern 这个变量,然后再使用
  • 静态无链接变量的特点

    • 仅在代码块内可用,但是在该代码块非活动时仍然存在
    • 同样仅在启动时初始化一次

9.2.4 说明符与限定符

  • 说明符

    • auto, register, static, extern, mutable
    • 同一个声明只能使用一个说明符
    • mutable:即使该成员的结构 / 类为 const,仍然允许对该成员进行单独修改
  • cv限定符

    • const:内存仅能被初始化一次,之后程序不能对其进行修改
    • volatile:禁止使用该内存的缓存,而是直接去获取硬件信息
  • const深入:

    • const 修饰的全局变量的链接性将从外部变为内部
      • 因为当多个文件使用某个头文件时,头文件的内容将包含在每个源文件中,即所有源文件都会包含其 const 常量定义,而不能在多个文件定义同一个外部全局变量,所以为内部
      • 可以使用 extern 覆盖

9.2.5 函数与链接性

  • C++不允许函数内定义另一个函数 => 所有的函数的存储持续性都为静态

  • 函数的默认链接性为外部

    • 可以在函数原型中使用 extern 指出函数时在另一个文件定义的(可选)
    • 要让程序在另一个文件中查找函数,该文件必须作为程序的组成部分被编译,或者是由链接程序搜索的库文件
  • 使用 static 使得函数链接性为 内部 ,同时,使得该 static 函数覆盖同名的外部函数

  • 单定义规则:对于每个非内联函数,程序只能包含一个定义,但是使用该函数的每个文件都应该包含其函数原型。

  • C++如何查找函数

    • 如果该文件中函数原型指出该函数为静态,那么编译器将只在该文件中查找函数定义。
    • 否则,编译器将在所有的程序文件中查找。如果找到两个定义,那么编译器将报错。
    • 程序文件中未找到,那么编译器将在库中搜索。这意味着如果定义了一个与库函数同名的函数,编译器将使用程序员定义版本。一些编译器-链接程序要求显式指出要搜索的库。

9.2.6 语言链接性

  • 链接程序要求每个函数有不同的符号名,由于C++与C在函数名内部翻译上的不一致性。如果要在C++程序中使用C库中预编译的函数,那么需要特殊处理:

    • 假设 C 库中存在一个预编译函数 sniff(),那么 C++ 中应该这样定义函数原型:extern "C" void sniff()。如果时搜寻 C++ 库函数,去掉 “C” 即可

9.3 布局 new 操作符

  • new 负责在堆(heap)中找到一个足以满足要求的内存块
  • 布局 new 操作符使用一个传递给它的**起始位置**地址分配的内存块,不跟踪哪些内存单位被使用,也不查找未使用的内存块
  • 因为布局 new 操作符指定的内存是静态内存,所以无法被 delete 释放

9.4 名称空间

  • 传统C++命名空间

    • 声明区域(declaration region):可以在其中进行声明的区域。函数外声明全局变量的声明区域为声明所在的文件。对于在函数中声明的变量,其声明区域为其声明所在的代码块。
    • 潜在作用域(potential scope):从声明点开始,到其声明区域结尾。潜在作用域 < 声明区域,因为变量必须定义后才能使用。
    • 作用域(scope):变量对于程序而言可见的范围。因为变量可能被另一个嵌套声明的同名变量隐藏,所以作用域 < 潜在作用域。
  • 新的名称空间(namespace)特性

    • C++通过定义一种新的声明区域来创建命名的名称空间,这样做的目的之一是提供一个声明名称的区域,使得一个名称空间中的名称不会与另外一个名称空间的相同名称发生冲突。
namespace Jack {
    double pail;
    void fetch();
    int pal;
    struct Well { ... };
}
namespace Jill {
    double bucket(double n) { ... }
    double fetch();
    int pal;
    struct Hill { ... };
}
    • 名称空间可以全局,也可以位于另一个名称空间,但是不能位于代码块中(所以默认下 namespace 种声明的名称链接性为 外部 (除非引用了常量))
    • 除了用户定义 namespace 外,还存在一个 全局名称空间(global namespace)。对应文件级声明区域,全局变量存在于其中
    • 不同的任何名称空间之间不会发生名称冲突
    • 可以将名称再加入到已有的名称,或者给已定义的原型提供函数代码
    • 使用作用域解析操作符 :: 来访问 namespace 中的名称
  • using 声明和 using 编译指令

    • using 声明 将特定名称添加到其所属的名称空间中,如:在 main() 中的 using 声明:using Jill::fetch 将 fetch 添加到 main() 定义的声明区域中。完成声明后,就可以使用 fetch 代替 Jill::fetch
    • using 编译指令 使得该名称空间内的所有名称可用,不需要解析,如常用的 using namesapce std。如果是在 main() 函数外声明(即全局名称空间),这样使得 std 名称空间的名称全局可用。
    • using 声明与编译指令的区别:如果存在 名称空间和声明区域定义相同名称 的情况。对于 using 声明,则会因为两名称的冲突而报错。对于 using 编译,局部版本将隐藏名称空间名
  • namespace 其他特性

    • 可以嵌套名称空间
    • 嵌套名称空间的场合,using 编译父空间时将同时引入子空间
  • 未命名名称空间

    • 未命名 namespace 的潜在作用域为:从声明点到该声明区域末尾,与全局变量类似。
    • 可以用于代替内部静态变量(全局区域定义的 static 变量)
  • 其他

    • 一些老式头文件(比如math.h, iostream.h)没有使用名称空间。而新式头文件(cmath, iostream)等使用了名称空间

第10章 对象和类

10.2 抽象与类

  • 一个类的范例:
#include <iostream>
#include <cstring>

class Stock
{
private:
    char company[30];
    int shares;
    double share_val;
    double total_val;
    void set_tot() { total_val = shares * share_val; }
public:
    void acquire(const char * co, int n, double pr);
    void buy(int num, double price);
    void sell(int num, double price);
    void update(double price);
    void show();
};
  • 成员函数可以就地定义,也可以用原型表示。(对于描述函数接口而言,原型足够)
  • 类设计尽量将公有接口和实现细节分开
  • 一般将组成类的接口的成员函数放在公有部分;将数据项与私有成员函数(不属于公有接口的实现细节)
  • 在不给定关键字的情况下,类对象默认使用 private 作为关键字

类与结构

  • C++对结构进行了扩展,使其具有与类相同的特性。它们之间的唯一区别是,结构的默认访问类型为 public
  • 一般用结构来表示纯粹的数据对象或者没有私有部分的类。

10.2.3 实现类成员函数

  • 类成员函数与常规函数定义类似,但是还有两个特殊特征

    • 定义成员函数时,使用作用域解析操作符(::)来标识函数所属的类
    • 类方法可以访问类的 private 组件
  • 对类成员函数的实现可以单独放在一个文件中,也可以位于类声明所在的文件中。(最好的方法是将类声明放在头文件中,而将类成员函数定义在一个单独源代码文件中)

void Stock::acquire(const char * co, int n, double pr) {
    std::strncpy(company, co, 29);
    company[29] = '\0';
    // ...
}

内联方法

  • 定义位于类声明中的函数将自动称为内联函数
  • 通常将短小的成员函数作为内联函数
  • 也可以在类声明外定义成员函数并使其成为内联函数,只需在类实现部分使用inline 限定符即可

10.2.4 使用类与对象

  • 不同于java,不一定需要显式创建类对象,声明即可:**Stock joe;**
  • C++的目标是使得使用类与使用基本内置类型尽可能相同:所以,要创建类对象,可以声明类变量,也可以使用 new 分配空间。

10.3 类的构造函数和析构函数

  • 声明与定义构造函数的过程与一般的成员函数类似(先定义,后实现)

  • 不能使用类成员名称作为构造函数参数

  • 构造函数的使用可以有两种方式

    • 显式调用:Stock food = Stock ("World", 250, 1.25);
    • 隐式调用:Stock food ("World", 250, 1.25);

默认构造函数

  • 默认构造函数不会初始化成员

  • 当定义了一个构造函数之后,默认构造函数将被禁用,这将使得默认的对象创造方式:Stock stock1 报错。(可以出于 禁止创建未初始化的对象 的目的这么做)

  • 两种方式来定义默认构造函数:

    • 给已有构造函数的所有参数提供默认值
    • 重载一个无参构造函数。

析构函数

  • 对象过期时,程序将自动调用一个特殊的成员函数,称为 析构函数,用于完成清理工作。

  • 其形式与构造函数类似,没有返回值,但是在类名前加上():`Stock()`

  • 调用时机

    • 如果创建 静态存储类对象,析构函数将在程序结束时自动调用。
    • 如果创建 自动存储类对象,析构函数将在程序执行完代码块时自动被调用。
    • 如果类对象 通过new创建的,析构函数将在使用 delete 释放内存时被自动调用

10.5 对象数组

  • 要创建类对象数组,则这个类必须有默认构造函数

第11章 使用类

11.1 操作符重载

  • C++允许操作符重载扩展到用户定义的类型,例如,使用+将两个对象相加,甚至将两个数组相加等。

  • 适用方法:

    • 格式:operator op (argument-list)。op 是将要重载的操作符
    • 一般作为某个类的类方法被定义,这样使得操作符重载方法的定义内容可以应用到该类本身。
    • 示例:
// mytime1.h
#ifndef MYTIME1_H_
#define MYTIME1_H_

class Time
{
private:
    int hours;
    int minutes;
public:
    Time ();
    Time (int h, int m = 0);
    void AddMin (int m);
    void AddHr (int h);
    void Reset (int h = 0, int m = 0);
    Time operator+ (const Time & t) const;
    void Show () const;
};
#endif

// mytime1.cpp
#include <iostream>
#include "mytime1.h"

Time::Time ()
{
    hours = minutes = 0;
}

Time::Time (int h, int m)
{
    hours = h;
    minuts = m;
}

Time Time::operator+ (const Time & t) const
{
    Time sum;
    sum.minutes = minutes + t.minutes;
    sum.hours = hours + t.hours + sum.minutes / 60;
    sum.minutes %= 60;
    return sum;
}

11.2.2 重载限制

  • 重载后的操作符必须至少有一个操作数是用户定义类型(防止用户重载标准类型)
  • 不能违反操作符原本的句法规则(将二元操作符改为一元等)
  • 不能定义新操作符
  • 一部分操作符不能被重载
  • 大多数操作符可以通过成员或非成员函数重载,但是= () [] ->只能通过成员函数重载

11.3 友元

C++控制对于类对象私有部分的访问,公有类方法提供唯一的访问途径。于是C++提供了另一种形式的访问权限:友元。

友元有三种:

  • 友元函数
  • 友元类
  • 友元成员函数

通过让函数称为类的友元,可以赋予该函数与类的成员函数相同的访问权限

友元函数:

  • 创建一个友元函数:将原型放在类声明中,并在原型声明前加上关键字 friend
  • friend Time operator* (double m, const Time & t)

11.3.2 常用的友元:重载<<操作符

对于类来说,可以对 << 操作符进行重载,使之能与 cout 一起来显示对象的内容

为了实现类似 cout << obj 的效果,可以这样在我们需要使用 cout 的类中定义友元函数:

  • friend void operator<< (ostream & os, const Time & t){ cout << t.hours }

同时,考虑到 cout 可以将输出拼接,所以我们需要使该函数返回 ostream 对象的引用:

  • friend ostream & operator<< (ostream & os, const Time & t)

11.6 类的自动转换和强制类型转换

对于存在接收一个基本类型作为参数的构造函数,可以使用直接赋值的方式来初始化(称为隐式转换):

  • MyClass (double value);
  • MyClass obj; obj = 19.6

如果想要关闭这种自动的类型转化特性,可以在构造函数前添加 explicit 关键字:

  • explicit MyClass (double value);(然而,仍然可以使用显式转换)

如果想要做相反的转换(将自定义对象转换为基本类型值),需要使用特殊C++操作符函数——转换函数

  • 转换函数语法:

    • operator typeName()
  • 限制

    • 必须是类方法
    • 不能指定返回类型
    • 不能有参数
    • 如果定义了多个转换函数,那么需要注意隐式转换时的二义性问题
  • 在同时使用重载函数与转换函数时,可能会出现二义性问题。

第12章 类和动态内存分配

12.1 动态内存和类

  • 在类(对象)方法中使用 new 分配的动态内存数据并不保存在对象中,其数据单独保存在堆内存中,对象中仅保存了指向该数据的信息。
  • 析构函数需要指出自己何时被调用,但是这并不是必不可少的