目录

  • 1 C++语言导论
    • 1.1 C++语言与问题求解
    • 1.2 人类如何求解问题
      • 1.2.1 过程化思维
      • 1.2.2 面向对象思维
      • 1.2.3 泛型思维
    • 1.3 计算机的工作原理
      • 1.3.1 计算机组成
      • 1.3.2 计算机如何执行程序
      • 1.3.3 计算机如何存储程序和数据
      • 1.3.4 关于存储的约定
    • 1.4 从人机需求到计算机语言
      • 1.4.1 机器语言(只考虑计算机的需求)
      • 1.4.2 汇编语言(考虑一点点人的需求)
      • 1.4.3 高级语言(兼顾人机需求)
    • 1.5 从人机需求到C++语言
      • 1.5.1 过程化编程
      • 1.5.2 面向对象编程
      • 1.5.3 泛型编程
  • 2 动手写个小程序
    • 2.1 求解素数判断问题
    • 2.2 程序的一般结构
    • 2.3 程序的编译和链接
    • 2.4 C++程序是字符序列
    • 2.5 C++程序是单词序列
    • 2.6 C++程序是语句序列
    • 2.7 C++程序是函数集合
    • 2.8 C++程序是文件集合
  • 3 C++过程化编程
    • 3.1 过程化编程的主要工作
    • 3.2 描述简单数据
      • 3.2.1 C++语言的数据类型
        • 3.2.1.1 C++语言的内置数据类型
        • 3.2.1.2 C++语言的复合数据类型
        • 3.2.1.3 定义自己的数据类型
      • 3.2.2 描述字面量
      • 3.2.3 描述变量
    • 3.3 描述数据处理过程
      • 3.3.1 数据的基本运算处理
      • 3.3.2 控制数据处理流程
      • 3.3.3 流程控制实验练习
      • 3.3.4 输入输出处理
        • 3.3.4.1 键盘输入
        • 3.3.4.2 屏幕输出
        • 3.3.4.3 控制输入输出格式
        • 3.3.4.4 文件输入输出处理
    • 3.4 制造数据处理机器:函数
      • 3.4.1 定义函数
      • 3.4.2 调用函数
      • 3.4.3 函数调用的形参生成
      • 3.4.4 函数嵌套和递归调用
      • 3.4.5 函数内联
      • 3.4.6 函数重载
      • 3.4.7 函数的默认形参值
    • 3.5 结构化编程与程序组织
      • 3.5.1 函数声明的组织方式
      • 3.5.2 程序的多文件组织
      • 3.5.3 作用域和生存期
        • 3.5.3.1 作用域
        • 3.5.3.2 生存期
      • 3.5.4 函数和变量的共享:链接性
      • 3.5.5 名字空间域
    • 3.6 描述复杂数据:复合数据类型
      • 3.6.1 常变量
      • 3.6.2 数组变量
      • 3.6.3 指针变量
        • 3.6.3.1 定义指针变量
        • 3.6.3.2 访问指针变量
        • 3.6.3.3 指针与动态变量
        • 3.6.3.4 动态数组
      • 3.6.4 描述字符串变量
      • 3.6.5 引用:给变量取个别名
      • 3.6.6 嵌套复合的数据类型
        • 3.6.6.1 常量数组与枚举类型
        • 3.6.6.2 指针数组与数组指针
        • 3.6.6.3 常量指针与指针常量
        • 3.6.6.4 多维数组和多级指针
        • 3.6.6.5 常变量、指针、数组的引用
        • 3.6.6.6 简化嵌套复合声明:typedef
      • 3.6.7 复合数据类型与函数
        • 3.6.7.1 数组与函数
        • 3.6.7.2 指针与函数
        • 3.6.7.3 引用与函数
  • 4 面向对象编程
    • 4.1 面向对象编程的主要工作
    • 4.2 描述需要什么样的对象
      • 4.2.1 定义类
      • 4.2.2 产生对象
      • 4.2.3 访问对象的成员
      • 4.2.4 描述对象的成员
    • 4.3 约定对象生死时刻的行为
      • 4.3.1 必须编写析构函数
      • 4.3.2 浅拷贝和深拷贝
      • 4.3.3 构造和析构函数的调用时机
    • 4.4 约定对象运算的行为
      • 4.4.1 友元函数和友元类
      • 4.4.2 两种重载操作符的方式比较
      • 4.4.3 重载赋值运算符
      • 4.4.4 深拷贝的赋值运算符
    • 4.5 约定类成员的常量性
      • 4.5.1 常量数据成员
      • 4.5.2 常成员函数
      • 4.5.3 常对象和常成员函数
      • 4.5.4 mutable成员
      • 4.5.5 volatile成员
    • 4.6 约定类成员的静态性
      • 4.6.1 静态数据成员
      • 4.6.2 静态成员函数
      • 4.6.3 const和static成员小结
    • 4.7 对象组合
    • 4.8 对象继承与派生
      • 4.8.1 选择继承方式
      • 4.8.2 改造基类成员
      • 4.8.3 重写构造和析构函数
      • 4.8.4 派生类与基类的赋值兼容
        • 4.8.4.1 派生类对象与基类对象的兼容性
        • 4.8.4.2 派生类对象与基类引用/指针的兼容性
        • 4.8.4.3 针对基类引用/指针进行通用编程
    • 4.9 对象行为的多态:重载和虚函数
      • 4.9.1 编译期多态:重载
      • 4.9.2 运行期多态:虚函数
        • 4.9.2.1 在类层次中定义虚函数
        • 4.9.2.2 使用虚函数实现通用输出操作符
        • 4.9.2.3 虚函数运行期绑定原理
        • 4.9.2.4 运行期绑定的优势与不足
        • 4.9.2.5 虚析构函数
        • 4.9.2.6 不产生对象的类:抽象类
        • 4.9.2.7 只有纯虚函数的类:接口类
    • 4.10 运行期类型识别RTTI
    • 4.11 异常处理机制
  • 5 泛型编程
对象组合

基于组合关系定义类:组合类

组合类,就是基于组合关系定制的类,组合类的某个数据成员是另一种定制数据类型。例如,家庭类CFamily就是组合类,其家庭成员通过CPerson类来描述;而CPerson类同样是组合类,其数据成员“生日”通过CDate类来描述。代码如下,

class CDate { ……};

class CPerson{

private:

      string  name;                 //姓名

      CDate   birthday;             //生日

};

class CFamily {

private:

  CPerson  father, mother, child;  //父亲、母亲、孩子

};

这里,CPerson类的三个对象fathermotherchild作为CFamily类的数据成员,描述了一个家庭的三个成员,组合成“家庭”;而CDate类对象birthday,则作为CPerson类的一个数据成员,描述人的生日,与name等成员组合形成“人”。

可以推断,一个家庭对象必然包含fathermotherchild三个CPerson类对象,称之为对象成员或者子对象。进而,每个CPerson类对象又必然包含一个CDate类的子对象birthday。在 7‑1中,就给出了CFamily对象的存储空间布局。


组合对象的构造和析构

可见,产生一个CFamily对象,必然会产生三个CPerson子对象;而产生每个CPerson子对象,必然会产生一个CDate子对象。

对象的产生会调用构造函数。因此,产生一个CFamily对象,会调用1CFamily类的构造函数,3CPerson类的构造函数以及3CDate类的构造函数。

例如,如下代码会产生家庭对象fm,调用CFamily类对象成员的构造函数以及CFamily类自身的默认构造函数。

CFamily    fm;  //产生一个CFamily对象

构造函数的调用顺序是先别人后自己

  (1)调用CDate类的默认构造函数,产生father子对象的birday子对象;

2)调用CPerson类的默认构造函数,产生father子对象;

3)调用CDate类的默认构造函数,产生mother子对象的birday子对象;

4)调用CPerson类的默认构造函数,产生mother子对象;

  (5)调用CDate类的默认构造函数,产生child子对象的birday子对象;

6)调用CPerson类的默认构造函数,产生child子对象;

7)调用CFamily类的默认构造函数,产生fm对象。



同理,CFamily对象的死亡,则会按照先自己后别人的顺序调用析构函数:

1)调用CFamily类的析构函数,析构CFamily类对象fm

2)调用CPerson类的析构函数,析构child子对象;

3)调用CDate类的析构函数,析构child子对象的birthday子对象;

4)调用CPerson类的析构函数,析构mother子对象;

5)调用CDate类的析构函数,析构mother子对象的birthday子对象;

6)调用CPerson类的析构函数,析构father子对象;

7)调用CDate类的析构函数,析构father子对象的birthday子对象;

总之,本着“产生你先来,送死我先去”的大无畏英雄主义精神,组合类对象在产生时,总是会首先调用对象成员的构造函数,然后再调用自身的构造函数。反之,组合类对象在消亡时,会首先调用自身的析构函数,然后再析构对象成员。

对象成员之间构造和析构的顺序,按照它们在类体中声明的顺序进行。

组合对象中子对象的显式初始化

在上面CFamily类的例子中,子对象都是通过调用默认构造函数进行初始化的,或者是我们定义的默认构造函数,或者是语言自己合成的默认构造函数。

但是,如果对象成员根本就没有默认构造函数,则会导致编译错误:error C2512: 'CXXX': no appropriate default constructor available。例如,

class CPerson{   //CPerson类没有默认构造函数

private:

      string  name;            

      CDate    birthday;            

public:

      CPerson(stringnm) : name(nm) { }

};

class CFamily {

private:

  CPerson father, mother, child;  

};

intmain()  {

  CFamily fm; //错误,没有合适的默认构造函数

  return  0;

}

CFamily对象fm的产生需要调用CPerson类的默认构造函数,然而CPerson类只有一个单参数构造函数,没有给出默认构造函数,而且语言也不会提供合成的默认构造函数,故编译错误。纠正办法是重载CFamily类的默认构造函数,在其初始化列表中显式地调用子对象合适的构造函数。下面就是修正后的CFamily类:

class CFamily {

  ……

public:

  CFamily(): father("Dadi"),mother("Mumi"), child("Kidi") { }

};

intmain()  {

  CFamily fm;       //正确,有了合适的默认构造函数

  return 0;

}

黑体标注的是初始化列表,在初始化列表中显式调用了各个子对象的构造函数。

当然,如果希望在产生CFamily对象时自由设定家庭成员的名字,则可以重载CFamily的构造函数,在初始化列表中显式调用子对象的构造函数。例如,

class CFamily {

  ……

public:

  CFamily() : father("Dadi"),mother("Mumi"), child("Kidi")  {}

  CFamily(string fn, string mn, string cn)

    : child(cn),father(fn), mother(mn)    { }

};

intmain()  { CFamily fm("Leo", "Cristina", "NN");  }

在初始化列表中构造函数的调用顺序是:child(cn)father(fn)mother(mn),但子对象实际的构造顺序却是:father(fn)mother(mn)child(cn)

这是因为,C++语言约定:对象成员构造产生以及析构死亡的顺序,以它们在类体中声明的顺序为准,与它们在初始化列表中的调用顺序没有关系。