目录

  • 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 泛型编程
约定对象生死时刻的行为

对象的生死时刻



对象是具有定制行为的变量,因此,与变量一样,对象同样也具有作用域和生存期。不同作用域和生存期的对象,其产生和消亡的时机也不同。其中,

n  局部域 + 自动变量/对象

在变量/对象的定义点产生(从栈区获得存储空间),在离开局部域时消亡。

n  局部域 + 静态变量/对象

在程序第一次执行到其定义点时产生(从静态数据区获得存储空间并进行初始化工作),此后持续存在,直到程序执行结束时消亡。

n  全局变量/对象

在程序开始执行时就产生(从静态存储区获得存储空间并进行初始化工作),此后持续存在,直到程序执行结束时消亡。

n  动态变量/对象

调用new运算符时产生(从堆区获得存储空间),调用delete运算符时消亡。

很自然,我们希望变量或者对象在产生时具有一个可预期的初始状态,在消亡时做一些善后清理工作。例如,通过赋值或者构造函数语法初始化变量,

但是,对象是我们自己定制的数据类型,C++语言不可能预先知道并约定好对象初始化和善后清理的行为,而是需要我们在定义类时自己进行约定。

为此,C++语言提供了两种特殊的成员函数——构造函数析构函数。其中,构造函数专门用于约定对象在产生时的初始化行为,而析构函数则专门用于约定对象在消亡时的善后清理行为。

构造函数:约定对象产生的行为

构造函数(constructor),是专门用于对象初始化的成员函数,由系统在对象产生时自动调用,并且在该对象从产生到消亡的一生中就只会调用这一次。

对象初始化必定会调用构造函数。根据为对象产生提供的初始状态信息,可以将构造函数分为两类:默认构造函数和有参构造函数。有参构造函数又包括普通的有参构造函数、拷贝构造函数、以及转换构造函数。不同的应用场景需要不同的构造函数。

1)默认构造函数:无参数,没有提供任何初始状态信息;例如,

        CDate    now;                //调用默认构造函数

2)普通有参构造函数:有参,提供了一些初始状态信息;例如,

        CDate today(2008, 5, 1);            //调用有参构造函数

3)拷贝构造函数:单参数,提供了另一个同类对象;例如,

        CDate  birthday = today;          //调用拷贝构造函数,或者

        CDate  birthday(today);        

4)转换构造函数:单参数,提供了另一个非同类对象;例如,

        CDate  tomorrow = "2005-11-5";  //调用转换构造函数,或者

        CDate  tomorrow("2005-11-5");  

构造函数的定义语法比较特殊。首先,构造函数的名字就是类名,其次,构造函数无需返回类型说明。具体的语法形式如下,

    类名( 形参列表 ) : 成员初始化列表》 { …… }

其中,成员初始化列表用于设定数据成员的初始值,是可选的。


默认构造函数一种无参数的构造函数。例如,

        CDate::CDate( ) { year=1900; month=1; day=1; }

        CDate::CDate( ) : year(1900), month(1), day(1) { }  //可选方式:初始化列表

对于CDate类而言,通过在构造函数体中给数据成员赋值,或者使用初始化列表进行初始化,两种方式效果相同,但本质不同。其中,

n  在构造函数的函数体中,对象实际上已经产生出来了,只是其初始状态不符合我们的要求,因此,我们通过赋值修改对象的状态。

n  在初始化列表中,对象正在产生且在产生的同时设置其初始状态。

在一些情况下,对象一旦产生,它的某些数据成员就不再允许修改,例如常量数据成员或者引用型数据成员,此时,就必须使用初始化列表的方式。

在初始化列表中,数据成员进行初始化的实际顺序,与数据成员在类体中声明的顺序是一致的,无论在初始化列表中数据成员处于什么样的顺序。

对象初始化必须有构造函数,如果我们没有给出任何构造函数,那么,C++语言会自动生成一个默认构造函数,称为合成的默认构造函数。一般情况下,语言提供的合成默认构造函数,仅负责创建对象,基本上不做什么初始化工作。如果我们自己给出了任意一个构造函数,无论是有参数的构造函数,还是无参数的默认构造函数,那么,C++语言就不再合成默认构造函数。


普通的有参构造函数

CDate::CDate(int y=1900, int m=1,int d=1): year(y), month(m), day(d) { }

此时,这个三参数构造函数的每个参数都设定了默认值。在调用的时候,

1)如果年月日都提供,则无需默认值,直接调用三参数构造函数;

2)如果只提供年月,则默认天数d=1,相当于两参数构造函数;

3)如果只提供年,则默认月份m=1且天数d=1,相当于单参数构造函数;

4)如果年月日都不提供,则默认y=1900m=1d=1,相当于默认构造函数。

此时在调用该构造函数时,需要遵循形式参数表中的参数顺序,从左向右逐个设置实参,默认形参必须按从右向左的次序省略,中间不能遗漏。例如,

CDate today(2008, 5, 1);           //调用三参数构造函数

CDate today(2008, 5);              //调用两参数构造函数

CDate today(2008);                     //调用单参数构造函数

CDate today;                              //调用默认构造函数

CDate today(2008, , 1);            //错误,中间参数遗漏了


拷贝构造函数

在产生对象时,有时候是根据另一个已经存在的同类对象来指定对象的初始状态信息,类似于在另一个对象的基础上克隆或拷贝出一个新对象。例如,

CDate today(2008, 5, 1);

CDate  birthday = today;             //赋值语法调用拷贝构造函数,或者

//CDate  birthday(today);            //构造函数语法调用拷贝构造函数

此时,就需要一种具有特殊参数的有参构造函数——克隆构造函数或者拷贝构造函数copy constructor)。C++语言支持使用赋值语法或者构造函数语法调用拷贝构造函数,例如上面产生birthday对象的代码,二者是等价的。

C++语言约定,拷贝构造函数的第一个参数必须是同类对象的引用,包括常量对象的引用和非常量对象的引用,除此之外,还可以有额外的参数,但是额外参数必须设定其默认值。因此,下面都是有效的CName类的拷贝构造函数:

CName(CName &clone_me){ ……}    //或者,

CName(const CName&clone_me) { ……}  //或者,

CName(CName &clone_me,int n=10) { ……}

很多情况下往往无需定义拷贝构造函数,此时,C++语言会自己合成一个默认的拷贝构造函数。默认拷贝构造函数,其默认行为是递归地为所有基类和对象数据成员调用拷贝构造函数,或者说按成员拷贝的方式(memberwise copy)进行初始化,等效于用源对象的每个数据成员来初始化目标对象的对应数据成员。


转换构造函数

如果在产生对象时提供了一个参数,而且该参数与对象的类型不同。例如:

CDate  tomorrow = "2005-11-5";  //转换构造函数

这里,"2005-11-5"是字符串,不是CDate类对象,从表面上看就像是把一个字符串转换成一个CDate对象一样,此时就需要类型转换构造函数

可以推断,我们需要如下一个单参数构造函数,参数类型是字符串,

CDate::CDate(stringdate) {     //日期字符串格式yyyy-mm-dd

  year = 0; month = 0; day = 0;  int flag = 1;

  const char* p = date.c_str(); //指针指向字符串

  while(*p != '\0') {

    if(*p=='-') { p++; flag++;}

    switch(flag) {

      case 1: year = year*10 + (*p - '0');  break;

      case 2: month = month*10 + (*p-'0');  break;

      case 3: day = day*10 + (*p-'0');  break;

    }

    p++;

  }

}

在该构造函数中,我们从字符串"yyyy-mm-dd"中分别取得年份赋予year,取得月份赋予month,取得天数赋予day,构造出一个日期对象。

该构造函数只有一个参数且参数数据类型不是CDate类,称之为转换构造函数,可以将string类对象转换为CDate类的对象。此后遇到,

CDate tomorrow= "2005-11-5";

则系统会悄悄地调用这个转换构造函数,隐式地从string字符串"2005-11-5"构造产生出一个CDate类对象,然后用该对象初始化tomorrow对象。

总结一下,一个单参数构造函数,如果不是拷贝构造函数,那么它就是转换构造函数,可以将其它数据类型的对象转换为该类的对象。


显式转换构造函数

但是,有些时候我们只是想定义一个构造函数,根本没有预料到或者不希望这种背后偷偷摸摸的隐式转换行为,此时可以禁用隐式转换行为。

C++语言提供了关键字explicit,用于将转换构造函数声明为显式的,表示该转换构造函数必须光明正大地调用,即显式转换,禁止隐式转换。例如,

class CDate {

  ……

public:                         //公有成员函数

  CDate(int=1900,int=1, int=1);  

  CDate(constCDate&);

  explicitCDate(string date);    //转换构造函数(显式)

  ……

};

注意,explicit 关键字只能在类体内修饰构造函数声明。此后,如果需要将string对象转换为CDate类对象时,就必须进行显式地类型转换。例如,

CDate tomorrow= "2005-11-5";   //错误,禁止隐式转换

CDate now =CDate("2005-11-5");//正确


转换成员函数

转换构造函数把一个其它类型的对象转换为类对象。那么,或许我们要问:能否把类对象转换为其它类型的对象呢?可以,需要类型转换成员函数假设要将CDate对象转换为string字符串,则需要定义如下的转换成员函数,

CDate::operator string()  {

  ostringstreamoss;                   //使用字符流对象

  oss<<year<<"-"<<month<<"-"<<day;    //将年--日输出到字符流

  returnoss.str;                  //返回字符流中的字符串

}     //注意,使用字符流需要include <sstream>

转换成员函数的函数名非常特殊:operator+转换目标类型,而且它没有返回类型(因为函数名就指明了返回的目标数据类型)。即,

operator 转换的目标类型名();

CDate类有了上述的转换构造函数以及转换成员函数,就可以书写如下代码,

CDate tomorrow= "2005-11-5";  //隐式调用转换构造函数

string str = tomorrow;              //隐式调用转换成员函数

cout<<str<<endl;                    //输出字符串str


析构函数:约定对象死亡的行为

死生有命,C++语言通过构造函数约定对象产生时的初始化行为,同样,也提供了析构函数(destructor),专门用于约定对象消亡时的善后清理行为。

析构函数与构造函数是成对匹配的。其中,构造函数的名字是类名,而析构函数的名字则是:~类名,同时,析构函数没有返回类型,无任何形参。例如,

CDate::~CDate( ) { …… }

析构函数用于约定对象消亡时的善后清理行为,因此,析构函数通常在对象生命期结束时被系统自动调用。例如,静态对象在程序结束时、局部对象出了局部域时、临时对象生命期结束时、通过delete删除动态对象时,都会自动调用析构函数。

析构函数只有一个,不允许重载。通常如果我们的构造函数没有做什么特别的事情,例如申请某种系统资源,那么,析构函数也不需要做什么事情,甚至可以不定义析构函数,此时,系统会自己合成一个默认的析构函数,其行为与合成的默认构造函数类似,即按成员析构,不过析构顺序与合成默认构造函数的构造顺序相反。