1. C++ 基本

C/C++ Difference

C 提供了低级硬件访问。
C++提供了 OOP,即提供了高级抽象。

可移植性和标准

  • C++98
  • C++03
  • C++11

    头文件名

    image.png

    命名空间

    1. using namespace std;

类、函数和变量是C++编译器的标准组件,它们都被放置在命名空间 std 中。仅当头文件没有扩展名 h 时,情况才是如此。

区别

是不一样,前者没有后缀,实际上,在你的编译器include文件夹里面可以看到,二者是两个文件,打开文件就会发现,里面的代码是不一样的。

后缀为.h的头文件c标准已经明确提出不支持了,早些的实现将标准库功能定义在全局空间里,声明在带.h后缀的头文件里,c标准为了和C区别开,也为了正确使用命名空间,规定头文件不使用后缀.h。

因此,当使用时,相当于在c中调用库函数,使用的是全局命名空间,也就是早期的c++实现;当使用的时候,该头文件没有定义全局命名空间,必须使用namespace std;这样才能正确使用cout。

namespace 的使用

C++标准程序库中的所有标识符都被定义于一个名为std的namespace中。

由于namespace的概念,使用C++标准程序库的任何标识符时,可以有三种选择:

1、直接指定标识符。例如std::ostream而不是ostream。完整语句如下:

  1. std::cout << std::hex << 3.4 << std::endl;

2、使用using关键字。

  1. using std::cout;
  2. using std::endl;

以上程序可以写成

  1. cout << std::hex << 3.4 << endl;

3、最方便的就是使用using namespace std;
例如:

  1. #include <iostream>
  2. #include <sstream>
  3. #include <string>
  4. using namespace std;

这样命名空间std内定义的所有标识符都有效(曝光)。就好像它们被声明为全局变量一样。那么以上语句可以如下写:

  1. cout << hex << 3.4 << endl;

因为标准库非常的庞大,所程序员在选择的类的名称或函数名时就很有可能和标准库中的某个名字相同。所以为了避免这种情况所造成的名字冲突,就把标准库中的一切都被放在名字空间std中。但这又会带来了一个新问题。无数原有的C代码都依赖于使用了多年的伪标准库中的功能,他们都是在全局空间下的。 所以就有了和等等这样的头文件,一个是为了兼容以前的C代码,一个是为了支持新的标准。命名空间std封装的是标准程序库的名称,标准程序库为了和以前的头文件区别,一般不加”.h”。

2. C++ 语法

基本数据类型

  1. bool
  2. char
  3. signed char
  4. unsigned char
  5. short
  6. unsigned short
  7. int
  8. unsigned int
  9. long
  10. unsigned long
  11. long long
  12. unsigned long long

const 限定符

算术运算符

3. 复合类型

数组

字符串

string 类

struct

union

enum

指针

基本

在 C++ 中,int* 是一种复合类型,是指向 int 的指针。

new 分配的内存块通常与常量变量声明分配的内存块不同。变量 的值都存储在被称为栈(stack) 的内存区域中,而new从被称为堆(heap)或自由存储区(free store)的内存区域中分配内存。

new 和 delete 的使用原则:

  • 不要使用 delete 来释放不是 new 分配的内存
  • 不要使用 delete 来释放同一个内存块两次
  • 如果使用 new [] 为数组分配内存,则应使用 delete [] 来释放
  • 如果使用 new [] 为一个实体分配内存,则应使用 delete (没有方括号) 来释放
  • 对空指针使用 delete 是安全的

    指针、数组和指针和算术

    C++ 将数组名解释为 地址。

数组名是常量。

对数组应用 sizeof 得到的是数组的长度,对指针使用 sizeof 得到的是指针的长度,即使指针指向的是一个数组。

  1. #include <iostream>
  2. #include <stdlib.h>
  3. using namespace std;
  4. int main (int argc, char *argv[]) {
  5. short tell[10];
  6. short* ptell = tell;
  7. cout << tell << endl;
  8. cout << &tell[0] << endl;
  9. cout << &tell << endl;
  10. // ptell += 1;
  11. cout << tell + 1 << endl;
  12. cout << &tell + 1 << endl;
  13. return EXIT_SUCCESS;
  14. }

输出:

  1. 0x7ffee2e87420
  2. 0x7ffee2e87420
  3. 0x7ffee2e87420
  4. 0x7ffee2e87422
  5. 0x7ffee2e87434

解释:
tell + 1 是将地址值加2
&tell + 1 是将将地址值加20
tell 是一个 short 指针;而 &tell 指向包含20个元素的 short 数组(short(*)[20])。

指针常量和常量指针

参见:https://blog.csdn.net/chen1083376511/article/details/78442059

指针常量

指向常量的指针,这里所说的 “常量” 其实在解引用时起到所谓常量的作用,并非是指向的变量就是常量,指向的变量既可以是非常量也可以是常量。总体意思是不能修改指向某变量的值。

  1. // 两者等价
  2. const int *PtrConst;
  3. int const *PtrConst;

常量指针

不能改变指向的变量(即不能改变指针保存的内容)。

  1. //必须初始化,只能指向一个变量,绝不可再改变指向另一个变量
  2. int *const ConstPtr=&a;

C++ 3 种管理数据内存的方式

根据用于分配内存的方法,C++ 有 3 种管理数据内存的方式:自动存储、静态存储和动态存储(有时也叫作自由存储空间或堆)。C++ 11 新增了第四种类型:线程存储。

自动存储

在函数内部定义的常规变量使用自动存储空间,被称为自动变量(automatic variable),这意味着它们在所属的函数被调用时自动产生,在该函数结束时消亡。

实际上,自动变量是一个局部变量,其作用域为包含它的代码块。

自动变量通常存储在栈种。

静态存储

静态存储是整个程序执行期间都存在的存储方式。使变量称为静态的方式有两种:一种是在函数外面定义它;另一种是在声明变量时使用关键字 static。

动态存储

new 和 delete 运算符提供了一种比自动变量和静态变量更灵活的方法。它们管理了一个内存池,称为自由存储空间(free store)或 堆(heap)。该内存池同用于自动变量和静态变量的内存是分开的。

类型组合

4. 函数

函数指针

获取函数的地址

使用函数名即可

声明一个函数指针

声明指向某种数据类型的指针时,必须指定指针指向的类型。同样,声明指向函数的指针时,也必须指定指向的函数类型。这意味着声明应指定函数的返回类型以及函数的参数列表。

使用函数指针来调用函数

使用 pf 和 (*pf) 均可。

Question:指针函数是什么,跟函数指针有何异同?

内联函数

内联函数是 C++ 为提高程序运行速度所做的一项改进。常规函数和内联函数之间的主要区别不在于编写方式,而在于 C++ 编译器如何将它们组合到程序中。

内联函数的编译代码与其他程序代码“内联”起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再挑回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。如果程序在10个不同的地方调用同一个内联函数,则该程序将包含该函数代码的10个副本。

要使用内联函数,必须采取下述措施之一:

  • 在函数声明前加上关键字 inline
  • 在函数定义前加上关键字 inline

通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应提供原型的地方。

程序员请求将函数作为内联函数时,编译器不一定会满足这种要求。它可能认为该函数过大或注意到函数调用了自己(内联函数不能递归),因此不将其作为内联函数;而有些编译器没有启用或实现这种特性。

内联和宏

Inline 是 C++ 新增的特性。

C 语言使用预处理语句 #define 来提供宏——内联代码的原始实现。

宏是通过文本替换来实现的。

引用变量

引用是已定义的变量的别名。引用变量的主要用途是用作函数的形参。通过将引用变量用作参数,函数将使用原始数据,而不是其副本。这样除了指针之外,引用也为函数处理大型数据结构提供了一种非常方便的途径。

注意:必须在声明引用变量时进行初始化。

引用更接近 const 指针,即:

  1. int & rodents = rats;

实际上是下面代码的伪装表示:

  1. int * const pr = &rats;

引用经常被用作函数参数,使得函数中的变量名称为调用程序中的变量的别名。这种传递参数的方法称为按引用传递。按引用传递允许被调用的函数能够访问函数中的变量。C++新增的这种特性是对 C 语言的超越,C 语言只能按值传递。按值传递导致被调用函数使用调用程序的值的拷贝。当然,C 语言也允许避开按值传递的限制,采用按指针传递的方式。

尽可能使用 const

  • 使用 const 可以避免无意中修改数据的编程错误
  • 使用 const 使函数能够处理 const 和 非 const 实参,否则将只能接否非 const 数据
  • 使用 const 引用使函数能够正确生成并使用临时变量

返回引用

  • 返回引用的函数实际上是被引用的变量的别名
  • 应避免返回函数终止时不再存在的内存单元引用:最简单的方法是返回一个作为参数传递给函数的引用;另一种方法是用 new 来分配新的存储空间

默认参数

当函数调用中省略了实参时自动使用的一个值。

对于带参数列表的函数,必须从右向左添加默认值。也就是,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值。

函数模板

  1. template <typename T>
  2. void Swap(T &a, T &b) {
  3. T temp = a;
  4. a = b;
  5. b = temp;
  6. }

5. 内存模型和名称空间

单独编译

C++ 鼓励程序猿将组件函数放在独立的文件中。可以单独编译这些文件,然后将它们链接成可执行的程序。如果只修改了一个文件,则可以只重新编译该文件,然后将它与其他文件的编译版本链接。

可将程序分成三部分

  • 头文件:包含结构声明和使用这些结果的函数的原型
  • 源代码文件:包含与结构有关的函数的代码
  • 源代码文件;包含调用与结构相关的函数的代码

头文件中常包含的内容

  • 函数原型
  • 使用#defineconst定义的符号常量
  • 结构声明
  • 类声明
  • 模板声明
  • 内联函数

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

名称空间

6. 对象和类

  • 抽象
  • 封装
  • 多态
  • 继承

不必在类声明中使用关键字private,因为这是类对象的默认访问控制。

类和结构

  1. C++ 对结构进行了扩展,使之具有与类相同的特性。它们之间唯一的区别是,结构的默认访问类型是 public,而类为 private
  2. C++ 通常使用类来实现类描述,而把结构限制为之表示纯粹的数据对象。

内联方法

定义位于类声明中的函数都将自动成为内联函数。

构造函数和析构函数

什么时候调用析构函数呢?这由编译器决定,通常不应在代码中显示地调用析构函数。

抽象数据类型

Abstract Data Type, ADT

运算符重载

基本

运算符重载是一种形式的 C++ 多态。C++ 根据操作数的数目和类型来决定采用哪种操作。

C++ 允许将运算符重载扩展到用户定义的类型。

运算符函数的格式如下:

  1. operatorop(argument-list)

例如:operator+() 重载+运算符,operator*() 重载 *运算符。 op 必须是有效的 C++ 运算符,不能虚构一个新的符号。

作为成员函数还是非成员函数

对于很多运算符来说,可以选择使用成员函数或非成员函数来实现运算符重载。一般来说,非成员函数应是友元函数,这样它才能直接访问类的私有数据。

友元函数

友元有3种:

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

创建友元

  1. friend Time operator*(double m, const Time & t);

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

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

C ++ 新增关键字 explicit 用于关闭自动类型转换,即可以这样声明构造函数:

  1. explicit Stonewt(double lbs); // no implicit conversions allowed

这将关闭隐式转换,但仍然允许显示转换,即显示强制类型转换。

转换函数

构造函数只用于从某种类型到类类型的转换,要进行相反的转换,必须使用特殊的 C++ 运算符函数——转换函数。

转换函数是用户定义的强制类型转换,可以像使用强制类型转换那样使用它们。

如何创建转换函数呢?

  1. operator typeName();

注意

  • 转换函数必须是类方法
  • 转换函数不能指定返回类型
  • 转换函数不能有参数

例如:转换为 double 类型的函数的原型如下:

  1. operator double();

typeName 指出了要转换成的类型,因此不需要指定返回类型。

转换函数是类方法意味着,它需要通过类对象来调用,从而告知函数要转换的值。因此函数不需要参数。

使用转换函数要小心。可以在声明构造函数时使用关键字 explicit ,以防止它被用于隐式转换。

7. 类和动态内存分配

动态内存和类

C++ 使用 new 和 delete 运算符来动态控制内存。

  1. 在构造函数中使用 new 来分配内存时,必须在相应的析构函数中使用 delete 来释放内存。如果使用 new[] 来分配内存,则应使用 delete[] 来释放内存。

自动成员函数

C++ 自动提供以下成员函数:

  1. 默认构造函数,如果没有定义构造函数
  2. 默认析构函数,如果没有定义
  3. 复制构造函数,如果没有定义
    复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。类的复制构造函数原型通常如下:
    1. Class_name(const Class_name &);
    它接受一个指向类对象的常量引用作为参数。

3.1 何时调用
新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。最常见的情况时将新对象显示地初始化为现有的对象。例如,以下4种声明

  1. StringBad ditto(motto);
  2. StringBad metto = motto;
  3. StringBad also = StringBad(motto);
  4. StringBad * pStringBad = new StringBad(motto);
  5. // All will call StringBad(const StringBad &);

其中,中间的2种声明可能会使用复制构造函数直接创建 metoo 和 also,有可能使用复制构造函数生成一个临时对象,然后将临时对象的内容赋给 metoo 和 also,这取决于具体实现。最后一种声明使用 motto 初始化一个匿名对象,并将新对象的地址赋给 pStringBad 指针。
每当程序生成了对象副本时,编译器都将使用复制构造函数。具体地说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。按值传递意味着创建原始变量的一个副本。
何时生成临时对象随编译器而异,但无论是哪种编译器,当按值传递或返回对象时,都将调用复制构造函数。
由于按值传递对象将调用复制构造函数,因此应该按引用传递对象。这样可以节省调用构造函数的时间以及存储新对象的空间。
3.2 默认复制构造函数的功能
默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。
注意:如果类中包含了使用 new 初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制(Deep copy)。复制的另一种形式(成员复制或浅复制)只是复制指针值。浅复制仅浅浅地复制指针信息,而不会深入“挖掘”以复制指针引用的结构。

  1. 赋值运算符,如果没有定义
    赋值运算符原型如下:

    1. StringBad & StringBad::operator=(const StringBad &);

    它接受并返回一个指向类对象的引用。
    4.1 何时调用
    将已有的对象赋给另一个对象时,将使用重载的赋值运算符:

    1. StringBad headline1("Hello world");
    2. ...
    3. StringBad knot;
    4. knot = headline1; // assignment operator invoked
    5. String metoo = knot; // use copy constructor, possibly assignment too

    上例最后一行,实现可能分两步来处理:使用复制构造函数创建一个临时对象,然后通过赋值降临时对象的值复制到新对象中。
    这就是说,初始化总是会调用复制构造函数,而使用 = 运算符时有可能调用复制运算符。
    与复制构造函数类似,赋值运算符的隐式实现也对成员进行逐个复制。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态成员不受影响。
    4.2 解决赋值的问题
    办法是提供赋值运算符(进行深复制)定义。

    • 由于目标对象可能引用了以前分配的数据,所以函数应使用 delete[] 释放这些数据
    • 函数应当避免降对象赋给自身,否则,给对象重新赋值前,释放内存操作可能删除对象的内容
    • 函数返回一个指向对象的引用
  2. 地址运算符,如果没有定义

C++11 还提供了另外两个特殊成员函数:

  • 移动构造函数(move constructor)
  • 移动赋值运算符(move assignment operator)

    C++11 空指针

    在 C++98 中,字面值0有两个含义:可以表示数字值零,也可以表示空指针。有人使用 (void *)0 来标识空指针(空指针本身的内部表示可能不是零),还有人使用 NULL ,其实这是一个表示空指针的 C 语言宏。
    C++11 提供了更好的解决方案:引入新关键字 nullptr 表示空指针。

    构造函数中使用 new 的注意事项

  • 如果在构造函数中使用 new 来初始化指针成员,则应在析构函数中使用 delete

  • new 和 delete 必须相互兼容。new 对应于 delete,new[] 对应于 delete[]
  • 如果有多个构造函数,则必须以相同的方式使用 new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。然而,可以在一个构造函数中使用 new 初始化指针,而在另一个构造函数中将指针初始化为空,这是因为 delete 可以用于空指针。
  • 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。
  • 应定义个赋值运算符,通过深度复制将一个对象复制给另一个对象。通常,方法如下:

    1. // assign a String to a String
    2. String & String::operator=(const String & st) {
    3. if (this == &st) {
    4. return *this;
    5. }
    6. delete [] str;
    7. len = st.len;
    8. str = new char[len+1];
    9. std::strcpy(str, st.str);
    10. return *this;
    11. }

    返回对象

    当成员函数或独立的函数返回对象时,有几种返回方式可供选择。

    返回指向 const 对象的引用

    1. const Vector & Max(const Vector & v1, const Vector & v2) {
    2. if (v1.magval() > v2.magval()) {
    3. return v1;
    4. } else {
    5. v2;
    6. }
    7. }
  1. 返回对象将调用复制构造函数,而返回引用不会,因此返回引用效率更高
  2. 引用指向的对象应该在调用函数执行时存在
  3. v1 和 v2 都被声明为 const 引用,因此返回类型必须为 const,这样才匹配

    返回指向非 const 对象的引用

    两种常见的返回非 const 对象的情形是,重载赋值运算符和重载与cout一起使用的 << 运算符。前者是为了提高效率,而后者必须这样做。

    返回对象

    如果被返回的对象是被调用函数中的局部变量,则不应该按引用方式返回它,因为在被调用函数执行完毕时,局部对象将调用其析构函数。因此,当控制权回到调用函数时,引用指向的对象将不再存在。这种情况应返回对象而不是引用。
    通常,被重载的算法运算符属于这一类。如下:
    1. Vector Vector::operator+(const Vector & b) const {
    2. return Vector(x+b.x, y+b.y);
    3. }
    使用示例:
    1. Vector force1(50, 60);
    2. Vector force2(10, 20);
    3. Vector net;
    4. net = force1 + force2;

这种情况下,存在调用复制构造函数创建被返回的对象的开销,但是这是无法避免的。

返回 const 对象

为了避免:

  1. force1 + froce2 = net;

将返回类型声明为: const Vector

总结:如果方法或函数要返回局部对象,则应返回对象,而不是引用;如果方法或函数要返回一个没有公有复制构造函数的类(如 ostream)的对象,它必须返回一个指向这种对象的引用;如果有些方法或函数(如重载的赋值运算符)可以返回兑现过,也可以返回指向对象的引用,应首选引用,因为其效率高。

使用指向对象的指针

使用 new 初始化对象

  1. Class_name * pclass = new Class_name(value);

将调用如下构造函数:

  1. Class_name(Type_name);

这里可能还有一些琐碎的转换,例如:

  1. Class_name(const Type_name &);

另外,如果不存在二义性,则将发生由原型匹配导致的转换(如从 int 到 double)。

析构函数被调用情况

  • 如果对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数。
  • 如果对象是静态变量(外部、静态、静态外部或来自名称空间),则在程序结束时将调用对象的析构函数。
  • 如果对象是用 new 创建的,则仅当显示是用 delete 删除对象时,其析构函数才会被调用。

8. 类继承

主要内容

面向对象编程的主要目的之一是提供可重用的代码。

  1. 1. is-a 关系的继承
  2. 2. 保护访问
  3. 3. 构造函数成员初始化列表
  4. 4. 向上和向下强制转换
  5. 5. 虚成员函数
  6. 6. 早期(静态)联编与晚期(动态)联编
  7. 7. 抽象基类
  8. 8. 纯虚函数

p 497

传统的 C函数库通过预定义、预编译的函数提供了可重用性。

C++ 类提供了跟高层次的重用行。很多厂商提供了类库,类库由类声明和实现构成。因为类组合了数据表示和类方法,因此提供了比库函数更加完整的程序包。

  • 可以在已有类的继承上添加功能
  • 可以给类添加数据
  • 可以修改类方法的行为

注意:创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。派生类的构造函数总是调用一个基类的构造函数。可以使用初始化器列表语法指明要使用的基类构造函数,否则将使用默人的基类构造函数。派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。

派生类和基类之间的关系

  • 派生类可以使用基类的方法,条件是方法不是私有的
  • 基类指针可以在不进行显示类型转换的情况下指向派生类对象;基类引用可以在不进行显示类型转换的情况下引用派生类对象
  • 不可以将基类对象和地址赋给派生类引用和指针

继承:is-a 关系

C++ 有3种继承方式:公有继承、保护继承和私有继承。

公有继承是最常用的方式,它建立一种 is-a 关系,即派生类对象也是一个基类对象。

多态公有继承

有两种重要的机制可以实现多态公有继承:

  • 在派生类中重新定义基类的方法
  • 使用虚方法

注意:如果方法是通过引用或指针而不是对象调用时,它将确定使用哪一种方法。如果没有使用关键字 virtual ,程序将根据引用类型或指针类型选择方法;如果使用了 virtual,程序将根据引用或指针指向的对象的类型来选择方法。

  • 基类声明一个虚析构函数,是为了确保释放派生对象时,按正确的顺序调用析构函数。
  • 如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的。这样,程序将根据对象类型而不是引用或指针的类型来选择方法版本。为基类声明一个虚析构函数也是一种惯例。使用虚析构函数可以确保正确的析构函数序列被调用。

静态联编和动态联编

简介

程序调用函数时,将使用哪个可执行代码块呢?编译器负责回答这个问题。将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding)。在 C 语言中,非常简单,因为每个函数名都对应一个不同的函数。在 C++ 中,由于函数重载的缘故,这项任务更复杂。编译器必查看函数参数以及函数名才能确定使用哪个函数。然而, C/C++ 编译器可以在编译过程完整这种联编。在编译过程中进行联编被称为静态联编(static binding),又称为早期联编(early binding)。然而,虚函数使这项工作变得更苦难。因为,使用哪一个函数是不能在编译时确定的,因为编译器不知道用户将选择哪种类型的对象。所以,编译器必须生成能够在程序运行时选择正确的方法的代码,这被称为动态联编(dynamic binding),又称为晚期联编(late binding)。

编译器对虚方法使用动态联编。

几个问题

为了有两种类型对联编以及为什么默人为静态联编
  • 效率:为使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销。
  • 概念模型:在设计类时,可能包含一些不在派生类重新定义的成员函数。不将该函数设置为虚函数,有两方面的好处:首先效率更好,其次,著出不要重新定义该函数。

Tips: 如果要在派生类中重新定义基类的方法,则将它设置为虚方法;否则,设置为非虚方法。

虚函数的工作原理

C++ 规定了虚函数的行为,但将实现方法留给了编译器作者。

通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table, vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。

总结

使用虚函数时,在内存和执行速度方面有一定的成本:

  • 每个对象都将增大,增大量为存储地址的空间
  • 对于每个类,编译器都创建一个虚函数地址表(数组)
  • 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址

虽然非虚函数的效率比虚函数稍高,但不具备动态联编功能。

注意事项

  • 在基类方法的声明中使用关键字 virtual 可使该方法在基类以及所有的派生类中是虚的。
  • 如果使用指向对象的引用或指针来调用虚函数,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。
  • 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
  1. 构造函数不是虚函数。
  2. 析构函数应当是虚函数,除非不用做基类。
    Tips: 通常应给基类提供一个虚析构函数,即使它并不需要析构函数。
  3. 友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。如果由于这个问题原因引起了设计问题,可以通过让友元函数使用虚成员函数来解决。
  4. 如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生联中,则将使用最新的虚函数版本。
  5. 重新定义将隐藏方法

抽象基类

Abstract base class, ABC

C++ 通过使用纯虚函数(pure virtual function)提供未实现的函数。纯虚函数声明的结尾处为 =0。

当类声明中包含纯虚函数时,则不能创建该类的对象。这里的理念是,包含纯虚函数的类只用作基类。要成为真正的 ABC,必须至少包含一个纯虚函数。原型中的=0使虚函数称为纯虚函数。

可以将 ABC 看作是一种必须实时的借口。ABC 要求具体派生类覆盖其纯虚函数——迫使派生类遵循 ABC 设置的接口规则。这种模型在基于组件的编程模式中很常见,在这种情况下,使用 ABC 使得组件设计人员能够制定“接口约定”,这样确保了从 ABC 派生的所有组件都至少支持 ABC 指定的功能。

继承和动态内存分配

类设计回顾

编译器生成的成员函数

  1. 默认构造函数
    如果没有定义任何构造函数,编译器将定义默认构造函数。自动生成的默认构造函数的另一项功能是,调用基类的默认构造函数以及调用本身是对象的成员所属类的默认构造函数。
  2. 复制构造函数
    复制构造函数接受其所属类的对象作为参数。例如,Star 类的复制构造函数原型如下:

    1. Star(const Star &)
  3. 在下述情况下,将使用复制构造函数:

    • 将新对象初始化为一个同类对象
    • 按值将对象传递给函数
    • 函数按值返回对象
    • 编译器生成临时对象

如果程序没有使用(显示或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;否则,程序将定义一个执行成员初始化的复制构造函数。也就是说,新对象的每个成员都被初始化为原始对象相应成员的值。如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数。

  1. 赋值运算符
    默认的赋值运算符用于处理同类对象之间的赋值。不要将赋值和初始化混淆了。如果语句创建新的对象,则使用初始化;如果语句修改已有对象的值,则是赋值。

    1. Star sirius;
    2. Star alpha = sirius; // initialization (one notation)
    3. Star dogstar;
    4. dogstar = sirius; // assigment
  2. 默认赋值为成员赋值。如果成员为类对象,则默认成员赋值将使用相应类的赋值运算符。如果需要显示定义复制构造函数,则基于相同原因,也需要显示定义赋值运算符。

  3. 构造函数
  4. 析构函数
    对于基类,即使它不需析构函数,也应提供一个虚析构函数。
  5. 转换
    使用一个参数就可以调用的构造函数定义了从参数到类类型的转换。
    在带一个参数的构造函数原型中使用 explicit 将禁止进行隐式转换,但仍允许显示转换。
  6. 按值传递对象和传递引用
    通常,编写使用对象作为参数的函数时,应按引用而不是按值来传递对象。
  7. 返回对象和返回引用
    如果函数返回在函数中创建的临时对象,则不要使用引用。
  8. 使用 const

类函数小结

image.png

9. C++ 中的代码重用

C++ 的一个主要目标是促进代码重用。公有继承是实现这种目标的机制之一,但并不是唯一的机制。其中之一是使用这样的类成员:本身是另一个类的对象。这种方称为包含(containment) 、组合(composition)或层次化(layering)。另一种方法是使用私有或保护继承。

私有继承

使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。

包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。使用属于子对象(subobject)来表示通过继承或包含添加的对象。
image.png

多重继承

C++ 引入多重继承的同时,引入了一种新技术——虚基类(Virtual Base class),使 MI 成为可能。

虚基类

虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。例如,通过在类声明中使用关键字 virtual,可以使 Worker 被被用作 SingerWaiter 的虚基类(virtual 和 public 的次序无关紧要):

  1. class Singer : virtual public Worker {...};
  2. class Waiter : public virtual Worker {...};
  3. class SingingWatier : public Singer, public Watier {...};

SingingWaiter 对象将只包含 Worker 对象的一个副本。从本质上说,继承的 SingerWaiter 对象共享一个 Worker 对象,而不是各自引入自己的 Worker 对象副本。

如果一个类从两个不同的类那里继承了两个同名的成员,则需要在派生类中使用类限定符来区分它们。

MI会增加编程的复杂程度。然而,这种复杂性主要是由于派生类通过多条途径继承同一个基类引起的。

类模板

C++ 的类模板为生成通用的类声明提供了一种更好的方法。模板提供参数化(parameterized)类型,即能够将类型名作为参数传递给接收方来建立类或函数。

定义类模板

  1. template <class Type>
  2. template <typename Type>

总结

  1. C++ 提供了几种重用代码的手段。

公有继承能够建立 is-a 关系,这样派生类可以重用基类的代码。

私有继承和保护继承也使得能够重用基类的代码,但建立的是 has-a 关系。使用私有继承时,基类的公有成员和保护成员将成为派生类的私有成员;使用保护继承时,基类的公有成员和保护成员将成为派生类的保护成员。无论使用哪种继承,基类的公有接口都将成为派生类的内部接口。

还可以通过开发包含对象成员的类来重用类代码。这种方法被称为包含、层次化或组合,它建立的也是 has-a 的关系。

  1. 多重继承

多重继承(Multiple Inheritance, MI)使得能够在类设计中重用多个类的代码。

私有 MI 或保护 MI 建立 has-a 关系,而公有 MI 建立 is-a 关系。

MI 会带来一些问题,即多次定义同一个名称,继承多个基类对象。可以使用类限定符来解决名称二义性的问题,使用虚基类来避免继承多个基类对象的问题。但使用虚基类后,就需要为编写构造函数初始化列表以及解决二义性问题引入新的规则。

  1. 类模板

类模板使得能够创建通用的类设计,其中类型(通常是成员类型)由类型参数表示。典型的模板如下:

  1. template <class T>
  2. class Ic
  3. {
  4. private:
  5. T v;
  6. public:
  7. Ic(const T &val) : v(val) {}
  8. };

10. 友元、异常和其他

友元

前面介绍过将友元函数用于类的扩展接口中,类并非只能拥有友元函数,也可以将类作为友元。在这种情况下,友元类的所有方法都可以访问原始类的私有成员和保护成员。另外,也可以做更严格的限制,只将特定的成员函数指定为另一个类的友元。哪些函数、成员函数或类为友元是由类定义的,而不能从外部强加友情。因此,尽管友元被授予从外部访问类的私有部分的权限,但它们并不与面向对象的编程思想相悖;相反,它们提高了公有接口的灵活性。

友元类

定义一个友元类:

  1. friend class Remote;

友元成员函数

只声明某些需要的函数为友元函数。

问题:声明的顺序?

共同的友元

需要使用友元的另一种情况:函数需要访问两个类的私有数据。

嵌套类

访问权限

类的默认访问权限是私有的。
image.png

异常

异常提供了将控制权从程序的一个部分传递到另一部分的途径。对异常的处理有3个组成部分:

  • 引发异常
  • 使用处理程序捕获异常
  • 使用 try 块

RTTI

RTTI 是运行阶段类型识别(Runtime Type Identification)。RTTI 旨在为程序在运行阶段确定对象的类型提供一种标准方式

RTTI 工作原理

p 660

C++ 有3个支持 RTTI 的元素:

  • 如果可能的话,dynamic_cast 运算符将使用一个指向基类的指针来生成一个指向派生类的指针;否则,该运算符返回0——空指针。
  • typeid 运算符返回一个指出对象的类型的值。
  • type_info 结构存储了有关特定类型的信息。

类型转换运算符

总结

  1. 友元使得能够为类开发更灵活的接口。类可以将其他函数、其他类和其他类的成员函数作为友元。在某些情况下,可能需要使用前向声明,需要特别注意类和方法声明的顺序,以正确的使用友元。
  2. 嵌套类是在其他类中声明的类,它有助于设计这样的助手类,即实现其他类,但不必是公有接口的组成部分。
  3. C++ 异常机制为处理拙劣的编程事件,提供一种灵活的方式。引发异常将终止当前执行的函数,将控制权传给匹配的 catch 块。
  4. RTTI(运行阶段类型信息)特性让程序能够检测对象的类型。

11. string 类和标准模板库

string 类

构造字符串

image.png

智能指针模板类

智能指针是行为类似于指针的类对象,但这种对象还有其他功能。

auto_ptr, unique_ptr, shared_ptr 都定义了类似指针的兑现过,可以将 new 获得的地址赋给这种对象。当智能指针过期时,其析构函数将使用 delete 来释放内存。因此,如果将 new 返回的地址赋给这些对象,将无需记住稍后释放这些内存:在智能之臣过期时,这些内存都自动被释放。
image.png
要创建智能指针对象,必须包含头文件 memory ,该文件模板定义。

  1. auto_ptr<double> pd(new double);
  2. auto_ptr<string> ps(new string);

new double 是 new 返回的指针,指向新分配的内存块。

选择智能指针

如果程序要使用多个指向同一个对象的指针,应选择 shared_ptr

如果程序不需要多个指向同一个对象的指针,则可使用 unique_ptr

标准模板库

STL 提供了一组表示容器、迭代器、函数对象和算法的模板。

  • 容器是一个与数组类似的单元,可以存储若干个值。STL 容器是同质的,即存储的值的类型相同。
  • 迭代器能够用来遍历容器的对象,与能够遍历数组的指针类似,是广义指针。
  • 函数对象是类似于函数的对象,可以是类对象或函数指针(包括函数名,因为函数名被用作指针)。
  • 算法是完成特定任务(如对数组进行排序或在链表中查找特定值)的处方。

STL 使得能够构造各种容器(包括数组、队列和链表)和执行各种操作(包括搜索、排序和随机排列)。

Alex Stepanov 和 Meng Lee 在 Hewlett-Packard 实验室开发了 STL,并于 1994 年发布其实现。ISO/ANSI C++ 委员会投票统一将其作为 C++ 标准的组成部分。

STL 不是面向对象的编程,而是一种不同的编程模式——泛型编程(generic programming)。

模板类 vector

p 692

泛型编程

STL 是一种泛型编程(generic programming)。面向对象编程关注的是编程的数据方面,而泛型编程关注的是算法。它们之间的共同点是抽象和创建可重用代码,但它们的理念决然不同。

泛型编程旨在编写独立于数据类型的代码。

为何使用迭代器

理解迭代器是理解 STL 的关键所在。模板使得算法独立于存储的数据类型,而迭代器使算法独立于使用的容器类型。因此,它们都是 STL 通用方法的重要组成部分。

总结

首先是处理容器的算法,应尽可能用通用的术语来表达算法,使之独立于数据类型和容器类型。为使通用算法能够适用于具体情况,应定义能够满足算法需求的迭代器,并把要求加到容器设计上。即基于算法的要求,设计基本迭代器的特征和容器特征。

迭代器类型

STL 定义了5中迭代器,并根据所需的迭代器乐行对算法进行了描述。

p 705

  • 输入迭代器
  • 输出迭代器
  • 正向迭代器
  • 双向迭代器
  • 随机访问迭代器

迭代器层次结构

image.png

容器种类

STL 具有容器概念和容器类型。概念是具有名称(如容器、序列容器、关联容器等)的通用类别;容器类型是可用于创建具体容器对象的模板。

以前的11个容器类型分别是:

  1. deque
  2. list
  3. queue
  4. priority_queue
  5. stack
  6. vector
  7. map
  8. multimap
  9. set
  10. multiset
  11. biset // 比特级处理数据的容器

C++11 新增了

  1. forward_list
  2. unordered_map
  3. unordered_multimap
  4. unordered_set
  5. unordered_multiset

函数对象

p 724

很多 STL 算法都适用函数对象——也叫函数符(functor)。

算法

p 730

算法组

STL 将算法库分成4组:

  • 非修改式序列操作
  • 修改式序列操作
  • 排序和相关操作
  • 通用数字运算

12. 输入、输出和文件

p 748

13. C++11 新标准

14. 问题

  1. C++ 怎么使用像Java中的接口?

参考

  1. cpp-cheat-sheet
  2. The C++ Standard Template Library (STL)
  3. C Language Library
  4. http://www.cplusplus.com/
  5. C++ Standards Support in GCC

    附录