《C++ 程序设计》读书笔记

本文联合编辑:小辣辣。向她致以最崇高的敬(爱)意 ❤


第一章 C++的初步认识

在程序进行编译时,先对所有的预处理命令进行处理,将头文件的具体内容代替 #include 指令,然后再对该程序单元进行整体编译。

对函数做声明,它的作用是通知 C++ 编译系统。

第二章 数据的存储、表示形式和基本运算

C++ 没有规定每一种数据所占的字节数,只规定 int 型数据所占的字节数不大于 long 型,不小于 short 型。

符号常量

#define PRICE 30

符号常量虽有名字,但它不是变量。在进行编译预处理时,所有的 PRICE 都被置换为字符 30,在正式进行编译时已经没有 PRICE 这个标识符了。但 C++ 程序员一般更喜欢用 const 定义常变量。

运算符

  • / 除法运算符
    结果是整数,如果有一个是负数,则舍入方向是不固定的。多数编译系统采取向零取整的方法

  • % 模运算符
    两侧均应为整型数据,如 7%4 的值为 3

+,–,*,/ 运算中的两个数中有一个数为 float 型数据,则运算结果是 double 型,因为 C++ 在运算时对所有 float 型数据都按 double 型数据处理。

进行运算时,不同类型的数据都要先转换成同一类型,然后进行运算。

自增(减)运算符

也用于指针变量,使指针指向下一个地址。

赋值运算符和赋值表达式

赋值过程中的类型转换

将浮点型数据赋值给整型变量时,舍弃其小数部分。

不同类型的整型数据间的赋值归根到底就是一条:按存储单元中的存储形式直接传送。

程序初步设计

关系运算和逻辑运算

运算符优先级:

算数运算符 > 关系运算符 >赋值运算符

在 C 和 C++ 中都用数据 1 代表真,0 代表假。编译系统在处理逻辑型数据时,将 false 处理为 0,将 true 处理为 0。逻辑变量在内存中占 1 个字节,用来存放 0 或 1。

用 for 语句构成循环

for(表达式1;表达式2;表达式3)语句

表达式 1 可以省略,但其后得分号不可省略。

如果表达式 2 省略,即不判断循环条件,循环无休止地进行下去。

第四章 利用函数实现指定的功能

函数参数和函数的值

在调用函数时,编译系统临时给形参分配存储单元。调用结束后,形参单元被释放,实参单元仍保留并维持原值。因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调用函数中实参的值。

对被调用函数的声明和函数原型

在函数声明中可以不写形参名,只写形参类型

内置函数(内联函数)

转去被调用函数前,要记下当时执行的指令的地址,还要记下当时有关的信息,以便在函数调用后继续执行,如果有的函数需要频繁使用,则所用时间会很长,从而降低执行效率。可以使用内置函数将所调用的函数的代码直接嵌入到主调函数中,而不是将流程转出去。使用关键字 inline。

函数模板

template < typename T > //类似 Java 的泛型

有默认参数的函数

注意点:如果函数的定义在函数调用之前,则应在函数定义中给出默认值。如果函数的定义在函数调用之后,则在函数调用之前需要有函数声明,此时必须许多函数声明中给出默认值,函数定义时可以不给出默认值。

变量的存储类型

  • auto(默认)

    自动变量,动态局部变量,函数调用结束后释放

  • static

    静态局部变量在静态存储区内分配存储单元,在程序整个运行期间都不释放。
    加上 static 声明,则该变量只能用于本文件

  • register

    寄存器变量,如果有一些变量使用频繁,则为存取变量的值要花不少时间,为提高效率,C++ 允许将局部变量的值放在 cpu 的寄存器中

  • extern

    全局变量(外部变量),作用域从变量的定义开始,到本程序文件的末尾
    注意 extern 是用作变量声明,而不是变量定义。它只是对一个已定义的外部变量做声明,以扩展其作用域。

内部函数和外部函数

外部函数

如果在函数的首部的最左端冠以关键字 extern,则表示此函数是外部函数,可供其他文件调用。

第五章 利用数组处理批量数据

一维数组的初始化

  • 可以只给一部分元素赋值(后面的元素默认为 0)
  • 对全部数组元素赋初值时,可以不指定数组长度

二维数组的初始化

  • 可以对部分元素赋值

用数组作函数参数

数组名作实参和形参,传递的是数组的起始地址。

数组名代表数组首元素的地址。

用数组名作实参,如果改变了形参数组元素的值将同时改变实参数组元素的值。

字符数组

定义和初始化字符数组

如果提供的初值个数大于数组长度,则按语法错误处理。如果小于数组长度,则只将字符赋值给数组中前面的元素,其余的元素自动定义为空字符 \0

字符数组的赋值和引用

只能对字符数组的元素赋值,而不能用赋值语句对整个数组赋值。

结束标志

遇到字符 \0 就表示字符串到此结束。对于一个字符串常量,系统会自动在所在字符的后面加一个 \0 作为结束符,然后再把它存在字符数组中。

可以用字符串常量来初始化字符数组。

字符数组的输入和输出

  • 输出得字符不包含结束符 \0

字符串处理函数

比较函数 strcmp

strcmp(str1,str2)

对两个字符串自左至右逐个字符相比(按 ASCII 值大小比较)

两个字符串比较,不能用以下形式:

if(str1 > str2) {
    ...
}

上面写法表示将两个数组的地址进行比较。

字符串变量的定义和引用

和其他类型变量一样,字符串变量必须先定义后使用。

第六章 善于使用指针和引用

i_pointer 是一个指针变量,* i_pointer 表示 i_pointer 所指向的变量。
* 不是指针变量名的一部分,在变量名前加一个 * 表示该变量是指针变量。

不能用一个整数给指针变量赋值。

int * pointer = 2000;

编译系统并不把 2000 认为是地址,而认为是整数。可以将一个已定义的变量的地址作为指针变量的初值。

一个指针只能指向同一个类型的变量。

引用指针变量

& : 取地址运算符

&a 为变量 a 的地址, *p 为指针变量 p 所指向的存储单元

int * pointer = & a //定义指针变量

pointer:指针变量

* pointer 等效于 a(对指针变量 pointer 做 * 运算,指向 pointer 指向的存储单元,即 a)

&* 两个运算符的优先级别相同,但按自右而左方向结合。例如已知 pointer = &a& * pointer的含义是:先进行 *pointer 的运算,它就是变量 a,再执行 & 运算,因此 & * pointer& a 相同,即变量 a 的地址。

用指针作函数参数

int main() {
    void swap(int * p1, int * p2)
    int * pointer_1, * pointer_2, a, b;
    cin >> a >> b;
    pointer_1 = &a;
    pointer_2 = &b;
    swap(pointer_1, pointer_2)
}

void swap(int * p1, int * p2) { //定义指针变量 p1, p2,即 swap 的形参须是指针
    int temp;
    temp = * p1; // * p1:对指针变量做 * 运算,指向值
    ...
}

不能试图通过改变形参指针变量的值而使实参指针变量的值改变。实参和形参之间的数据传递是单向的“值传递”的方式,指针变量作函数参数也要遵循这一规则,调用函数时不会改变实参指针变量的值,但可以改变实参指针变量指向变量的值。

int main(int argc, char* argv[]) 
{
    void myswap(int * p1, int * p2);
    int * point1, * point2, a, b;
    a = 10, b = 20;
    point1 = &a;
    point2 = &b;
    cout<<"a"<<a<<"b"<<b<<endl;
    if(a < b) myswap(point1, point2);
    cout<<"a"<<a<<"b"<<b<<endl;
    if(a < b) realswap(point1, point2);
    cout<<"a"<<a<<"b"<<b<<endl;
    return 0;
}

//指针变量也是值传递,形参修改并不影响实参 point1 指针
void myswap(int * p1, int * p2) {
    int * temp;
    temp = p1;
    p1 = p2;
    p2 = temp;
}

void realswap(int * p1, int * p2) {
    int temp;
    temp = * p1;
    * p1 = * p2; //* p1,直接修改 p1 指向的值(a)的值
    * p2 = temp;
}

//输出 
a10b20
a10b20
a20b10

//分析
point1 = & a
* point1 即 point1 所指向的变量

数组和指针

指向数组元素的指针

int a[10]; //定义一个数组,有 10 个元素
int *p;
p = &a[0] //将元素 a[0] 的地址赋给指针变量 p,使 p 指向 a[0]

在 C++ 中,数组名代表数组中的第 1 个元素(即序号为 0 的元素)的地址,因此以下语句等价:

p = &a[0];
p = a;

数组名 a 不代表整个数组,p=a 的作用是把 a 数组的首元素的地址赋给指针变量 p。

如果指针变量 p 已指向数组中的一个元素,则 p + 1 指向同一个数组中的下一个元素。

如果 p 的初值为 &a[0],那么 p + ia + i 就是 a[i] 的地址。

*(p+5)
*(a+5)
a[5]

这三种表示方法等价。

可以看出,数组名后面的括号 [],实际上是变址运算符,指向数组元素的指针变量也可以带下标,如 p[i]*(p+i) 等价。

效率比较:

  1. 下标法:a[i]

    int a[10];
    int i;
    for(i = 0; i < 10; i++)
    {
        cout<<a[i]<<endl;
    }
    
  2. 指针法:*(a + i)

    int a[10];
    int i;
    for(i = 0; i < 10; i++)
    {
        cout<<*(a + i)<<endl;
    }
    
  3. 用指针变量指向数组元素

    int a[10];
    int i;
    for(p = a; p < (a + 10); p++)
    {
        cout<<*p<<endl;
    }
    

1 和 2 的执行效率相同,C++ 编译系统是将 a[i] 转换为 *(a + 1) 处理的,对每个 a[i] 都分别计算地址 a + i x d,然后访问该元素。3 比 1 和 2 快,用指针变量直接指向元素,不必每次都重新计算地址。

在使用指针变量指向数组元素是,应切实保证指向数组中有效的元素。

C++ 编译系统将形参数组名一律作为指针变量来处理。

函数与指针

只须将函数名 max 赋给 p,不能写成 p = max(a,b)

int * a(int x, int y);

a 是函数名,调用它以后m能得到一个指向整型数据的指针(地址)。

指针数组和指向指针的指针

指针数组

int * p[4]

由于 [] 比 * 优先级高,因此 p 先与 [4] 结合,形成 p[4] 形式,这显然是数组形式。不要写成 int 这是指向一维数组的指针变量。

* (name + i ++)

表示先求 * 的值,即 name 它是一个地址。将它赋给 p,然后 i 加 1,最后输出以 p 地址开始的字符串。

指向指针的指针

由于 name[i] 的值是地址(即指针),因此 name + i 就是指向指针型数据的指针。

// 指针变量(比如变量 a 的地址)
char * p 

// 存储 a 的地址的指针的地址
char *(* p) 

* p = 'C++'
** p 指向 ‘C++’ 的第一个字符元素的内容

const 指针

用指向常量的指针变量只是限制了通过指针变量改变它指向的对象的值。

  • const 类型名 * p (指向常量的指针变量)

    p 的指向可变,但 p 指向的对象的值不可变。

    int a = 12;
    int b = 15;
    const int * p = & a;
    
    p = &b; //合法,p 的指向可变
    * p = 20; //非法,p 指向的对象的值不可变
    a = 15; //合法,a 不是 const 常量
    

    如果想绝对保证 a 的值始终不变,应当把 a 定义为常变量。

  • int * const p (常指针变量)

    指针变量的指向不可变,但指向变量的值可变

    int a = 12;
    int b = 15;
    int * const p = & a;
    
    p = &b; //非法,p 的指向不可变
    * p = 20; //合法,p 指向变量的值可变
    
  • const int a

    a 的值不可变

    const int a = 12;
    
    a = 20; //非法
    

void 指针类型

该空间尚未使用,其中没有数据,谈不上指向什么类型的数据,故返回一个 void * 类型的指针,表示它不指向确定的具有类型的数据。

指针运算

指针变量可以有空值,即该指针变量不指向任何变量,它可以这样表示:

// iostream 头文件中已定义了符号常量 NULL 代表整数 0
p = NULL; 

如果两个指针不指向同一个数组,则比较是无意义的。如果一定要对不同类型的指针变量赋值,可以用到强制类型转换。

引用

在数据类型名后面出现的 & 是引用声明符号,在其他场合出现的都是地址符:

char &d = c; // 引用的声明符
int * p = & a; //地址符

在声明一个引用后,不能再使之作为另一变量的引用。

int a1, a2;
int &b = a1;
int &b = a2; //非法

引用其实就是一个指针常量,它的指向不可改变。

引用作为函数参数

传递变量的地址:形参是指针变量,实参是一个变量的地址。调用函数时,形参(指针变量)得到实参变量的地址,因此指向实参变量单元。

实参不是地址而是整型变量名,由于形参是引用,系统会自动将实参的地址传递给形参,注意:此时传送的是实参变量的地址而不是实参变量的值。实参是地址,传递的是地址,故仍然是值传递。方式(3)中实参是变量名,而传递的是变量的地址,这才是传址方式。

第七章 用户自定义数据类型

引用结构体变量

. 是成员运算符,它在所有运算符中优先级最高,因此可以把 student.num 作为一个整体来看待。

指向结构体变量的指针

C 和 C++ 提供了指向结构体变量的运算符 ->,形象的表示“指向”关系。例如,p->num 表示指针 p 当前指向的结构体变量中的成员 num。

p->n++ 得到p指向的结构体变量中的成 员 n的值,用完该值后使它加 1。

++p->n 得到 p 指向的结构体变量中的成员 n 的值,并使之加 1,然后再使用它。

用结构体变量和指向结构体变量的指针构成链表

用结构体变量和指向结构体变量的指针构成链表,最有一个元素不再指向其他元素,它成为“表尾”,它的地址部分放到一个“NULL”(表示“空地址”),链表到此结束。

所有节点(结构体变量)都是在程序中定义的,不是临时开辟的,也不能用完后释放,这种链表成为静态链表。

动态链表则是指个结点是可以随时插入和删除的,这些节点并没有变量名,只能先找到一个结点上,才能根据它提供的下一个结点的地址找到下一个结点。

结构体类型数据最为函数参数

调用函数时形参要单独开辟内存单元,如果结构体变量占的存储空间很大,则在虚实结合时控件和时间的开销都比较大,效率是不高的。

用 new 和 delete 运算符进行动态分配和撤销存储空间

new 运算符使用的一般格式:

new 类型 [初值]

注意:用 new 分配数组空间时不能指定初始值

delete 运算符使用的一般格式为:

delete 指针变量
或
delete [] 指针变量

在指针变量前面加一对方括号,表示是对数组空间的操作。

枚举类型

枚举元素按常量处理,故称枚举常量,它们不是常量,不能对它们赋值,即枚举元素的值是固定的。

第八章 类和对象的特性

声明类类型

如果在类的定义中既不指定 private,也不指定 public,则系统就默认为是私有的。

除了 private 和 public之外,还有一种成员访问限定符 protect(受保护的),用protect声明的成员不能被类外访问(这点与私有成员类似),但可以被派生类的成员函数访问。

成员函数的性质

:: 是作用域限定符(field qualifier)或称作用域运算符,用它声明函数是属于哪个子类的。

函数名前既无类名又无作用域运算符,表示函数不属于任何类,这个函数不是成员函数,而是全局函数。

内置成员函数

在类体中定义的成员函数的规模一般都很小,而系统调用函数的过程所花费的时间开销相对是比较大的,为减少时间的开销,如果在类体中定义的成员函数中不包括循环等控制结构,C++ 系统就自动把它们作为内置(inline)函数来处理。

成员函数的存储方式

每个对象所占用的存储空间只是该对象的数据成员所占的存储空间,而不包括函数代码所占用的存储空间。

第九章 怎样使用类和对象

对象的初始化

不能在类生命中对数据成员初始化,因为类并不是一个实体,而是一种抽象类型,并不占存储空间,显然误触容纳数据。

用构造函数实现数据成员的初始化

在类外定义构造成员函数,要加上类名和域限定符。

在建立对象时系统为该对象分配存储单元,此时执行构造函数。

构造函数的重载

无参构造函数应注意正确书写定义对象的语句

请记住:构造函数是不能被用户显式调用的

使用默认参数的构造函数

由于不需要实参也可以调用构造函数,因此全部参数都指定了一个默认值的构造函数也属于默认的构造函数。

编译系统无法识别应该调用那个构造函数,出现歧义性

在一个类中定义了全部是默认参数的构造函数后,不能再定义重载构造函数

析构函数

如果用户没有定义析构函数,C++ 编译系统会自动生成一个析构函数,但它只是徒有析构函数的名称和形式,实际上什么操作都不进行。

调用构造函数和析构函数的顺序

先构造的后析构,后构造的先析构。相当于一个栈,先进后出。

指向对象成员的指针

Time * pt; //定义 pt 是指向 Time 类对象的指针变量
Time tl; //定义 tl 为 Time 类对象
pt = & tl; //将 tl 的起始地址赋给 pt

* pt; //pt 所指向的对象,即 tl;
//以下表示是等价的
(* pt).hour; 
pt -> hour;

指针变量的类型必须与赋值号右侧函数的类型相匹配,要求在以下3方面都要匹配:1.函数参数的类型和参数个数;2、函数返回值类型;3.所属的类

定义指向公用成员函数的指针变量的一般形式为:

数据类型名 (类名 :: * 指针变量名)(参数列表);

//形如:
void(Time:: *p2)()

使指针变量指向一个公用成员函数的一般形式为:

指针变量名 = & 类名 :: 成员函数名
//形如
p2 = &Time::get_time;

常对象

常对象必须有初值,在常对象的生命周期中,对象中的所有数据成员的值都不能被修改

定义常对象的一般形式为:

类名 const 对象名[(实参表)];

也可以把const写在最左边:

const 类名 对象名[(实参表)];

与上面的格式是等价的。

在定义常对象时,必须同时对之初始化,之后不能再改变。

常对象成员

  • 常成员函数

    //注意const的位置在函数名和括号之后
    viod get_time() const;   
    

指向对象的常指针

指向对象的常指针变量的值不能改变,即始终指向同一个对象,但可以改变其所指向的对象的值。

往往常指针作为函数的形参,目的是不允许在函数执行过程中改变指针变量的值,使其始终指向原来的对象。

指向常对象的指针变量

一个对象已经被声明为常变量,只能用指向常变量的指针变量指向它。

指向常变量的指针变量除了可以指向常变量,还可以指向未被声明为const的变量。此时不能通过此指针变量来改变该变量的值。

指向常对象的指针最常用于函数的形参,目的是在白虎形参指针所指向的对象,使它在函数执行过程中不被修改。

以下是非法的:

  • 形参:指向非 const 型变量的指针;实参 const 变量的地址;

因为参数传递本质是值传递/地址传递。过程:形参指向实参。因为非 const 型变量指针只能指向非 const 型变量。所以以上是非法的。

在函数调用时将建立一个新的对象,它是实参对象的拷贝

对象的动态建立和释放

用new运算符动态地分配内存后,将返回一个指向新对象的指针。

对象的赋值

对象的赋值只对其中数据成员赋值,而不对成员函数赋值。

不同对象的成员函数时同一个函数代码段,不需要,也无法对它赋值。

静态成员

静态数据成员可初始化,但只能在类体外进行初始化。

公用静态数据成员与全局变量不同,静态数据成员的作用域只限于定义该类的作用域内(如果是在一个函数中定义类,那么其中静态数据成员的作用域就是在此函数内)

静态数据成员函数

非静态成员函数有 this 指针,而静态成员函数没有this指针。由此决定了静态成员函数不能访问本类中的非静态成员。

在 C++ 程序中最好养成这样的习惯:只用静态成员函数引用静态数据成员,而不引用非静态数据成员。

友元函数

在类外定义的且未用类最限定的函数,是非成员函数,不属于任何类。

友元类

  1. 友元的关系是单向的而不是双向的。
  2. 友元的关系不能传递。

类模板

一般形式为:

类模板名 <实际类型名> 对象名(参数表);

运算符重载

重载运算符的规则

重载的运算符必须和用户定义的自定义类型的对象一起使用,其参数至少应有一个是类对象(或类对象的引用),参数不能全是 C++ 的标准类型

运算符重载函数作为类成员函数和友元函数

将双目运算符重载为友元函数时,由于友元函数不是该类的成员函数,因此在函数的形参表中必须有两个参数,不能省略。

用转换构造函数进行不同类型数据的转换

通常把有一个参数的构造函数作类型转换,所以,称为转换构造函数。

类型转换函数

类型转换函数的作用是将一个类的对象转换成另一个类型的数据。形式如:

operator 类型名() 
{ ... }

在函数名前不能指定函数类型,函数没有参数。类型转换函数只能作为成员函数,因为转换的主体是本类的对象,不能作为友元函数或普通函数。

如果运算符重载函数为成员函数,它的第一个参数必须是本类的对象。当第一个操作数不是类对象时,不能将运算符函数重载为成员函数,如果将运算符“+” 重载为类的成员函数,交换律不适用。

类型转换函数与运算符重载不共存。因为可能出现二义性。

第十一章 继承与派生

派生类的声明方式

基类名前有 public 的称为公用继承。如果不写此项,默认为 privite(私有的)

派生类成员的访问属性

基类的成员函数只能访问基类的成员,而不能派生类的成员。

  • 公用继承

    基类的公用成员和保护成员在派生类中保持原有访问属性,私有成员仍为基类私有

  • 私有继承

    基类的公有成员和保护成员在派生类中成了私有成员,私有成员仍为基类私有

  • 受保护继承

    基类的公有成员和保护成员在派生类中成了保护成员,私有成员仍为基类私有

派生类的构造函数和析构函数

基类的构造函数式不能继承的,对继承过来的基类成员初始化的工作也要由派生类的构造函数承担。解决这个问题的思路是:在执行派生类的构造函数时,调用基类的构造函数。

简单的派生类构造函数

在类中对派生类构造函数作声明时,不包括上面给出的一般形式中的基类的构造函数名(参数表)部分

有子对象的派生类的构造函数

应当在建立对象时对它的数据成员初始化。

派生类构造函数的任务应该包括:

  1. 对基类数据成员初始化
  2. 对子对象数据成员初始化
  3. 对派生类数据成员初始化

定义派生类构造函数的一般形式为:

派生类构造函数名(总参数表):基类构造函数名(参数表),子对象名(参数表)
{ 派生类中新增数据成员初始化语句 }

派生类构造函数的特殊形式

如果在基类中没有定义构造函数,或定义了没有参数的构造函数,那么,在定义派生类构造函数时可以不写基类构造函数。因此此时派生类构造函数没有向基类构造函数传递参数的任务。在调用派生类构造函数时,系统会自动首先调用基类的默认构造函数。

派生类的析构函数

在执行派生类的析构函数时,系统会自动调用基类的析构函数和子对象的析构函数。调用的顺序与构造函数相反:先执行派生类自己的析构函数,然后调用子对象的析构函数,最后调用基类的析构函数。

多重继承

形式如:

派生类构造函数名(总参数表):基类 1 构造函数(参数表),基类 2 构造函数(参数表), 基类 3 构造函数(参数表)
{ 派生类中新增数据成员初始化语句 }

声明基类的顺序决定了基类构造函数的调用顺序

多重继承引发的二义性问题

基类的同名成员在派生类中被屏蔽。成为不可见的。因此如果在定义派生类对象的模块中通过对象名访问同名的成员,则访问的是派生类的成员。请注意:不同的成员函数,只有在函数名和参数个数相同、类型相匹配的情况下才发生同名覆盖。

虚基类

虚基类使得在继间接共同基类时只保留一份成员。

需要注意,为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类,否则仍然会出现对基类的多次继承。

由于虚基类在派生类中只有一份数据成员,所以这份数据成员的初始化必须由派生类直接给出。

C++ 编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。

基类与派生类的转换

A al; //定义基类 A 对象 al
B bl; //B 是 A 的派生类
A& r = al; //定义基类 A 对象的引用 r,并引用 al 对其初始化

这时,r 是 al 的引用(别名),r 和 al 共享同一段存储单元。

A& r = bl;

此时 r 并不是 bl 的别名,也不是与 bl 共享同一段存储单元,它只是 bl 中基类部分的别名,r 与 bl 中基类部分共享同一段存储单元,r 与 bl 具有相同的其实地址。

即基类的引用类型,指向了派生类的变量。指向的也是派生类中从基类继承的部分。

继承与组合

在一个类中以另一个类的对象作为数据成员的,称为类的组合。

第十二章 多态性与虚函数

派生类对象可以替代基类对象向基类对象的引用初始化或赋值。调用的不是在 Circle 中声明的运算符重载函数,而是在 Point 中声明的运算符重载函数,输出的是“点”的信息,而不是“圆”的信息。

这两个 area 函数不是重载函数,它们不仅函数名相同,而且函数类型和参数个数都相同,两个同名函数不在同一个类中,而是分别在基类和派生类中,属于同名覆盖。

虚函数的作用

编译系统按照同名覆盖的原则决定调用的对象。

C++ 的虚函数就是用来解决动态多态的问题的。所谓虚函数,就是在基类声明函数是虚拟的,并不是实际存在的函数,然后在派生类中才正式定义此函数。

本来,基类指针是用来指向基类对象的,如果用它指向派生类对象,则自动进行指针类型转换,将派生类的对象的指针先转换为基类的指针,这样,基类指针指向的是派生类中的基类部分。

有时在基类中定义的非虚函数会在派生类中被重新定义,如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数;如果用派生类指针调用该函数,则系统会调用派生类对象中的成员函数,这并不是多态性行为(使用的是不同类型的指针)

静态关联和动态关联

确定调用的具体对象的过程称为关联。

函数重载和通过对象名调用的虚函数,在编译时即可确定其调用的虚函数属于哪一个类,其过程称为静态关联。

由于是在运行阶段把虚函数和类对象“绑定”在一起的,因此称为动态关联。

在什么情况下应当声明虚函数

一个成员函数被声明为虚函数后,在同一类族中的类就不能定义一个非 virtual 的但与该虚函数具有相同的参数。

虚析构函数

先调用了派生类的析构函数,再调用了基类的析构函数。当基类的析构函数为虚函数,无论指针指的是同一类族中的哪一个类对象。

如果将基类的析构函数声明为虚函数时,由该函数所派生的所有派生类的析构函数有都自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不相同。

纯虚函数与抽象类

virtual 函数类型 函数名(参数类别) = 0;

纯虚函数只有函数的名字而不具备函数的功能,不能被调用。它只是通知编译系统:在这里声明一个虚函数,待派生类中定义。

抽象类

凡是包含纯虚函数的类的都是抽象类。因为纯虚函数是不能被调用的,包含纯虚函数的类是无法建立对象的。抽象类的作用是作为一个类族的共同基类。

抽象基类不能也不必要定义对象。

区别静态关联和动态关联

在编译阶段就能确定调用的是哪一个类的虚函数,所以属于静态关联。如果是通过基类指针调用虚函数,在编译阶段无法从语句本身确定调用哪一个类的虚函数,只有在运行时,指针指向某一类对象后,才能确定调用的是哪一个类的虚函数,故为动态关联。

第十三章 输入输出流

istream 类的其他成员函数

如果到达文件末尾(遇到文件结束符),eof 函数值为非零值(表示真),否则为 0(假)。

推荐阅读更多精彩内容