抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

C++ 是 C 语言的超集,它是一种使用非常广泛的计算机编程语言。C++ 作为一种静态数据类型检查的、支持多范型的通用程序设计语言,能够支持过程化程序设计、数据抽象化、面向对象程序设计、泛型程序设计、基于原则设计等多种程序设计风格。

变量类型

变量声明

当使用多个文件且只在其中一个文件中定义变量时(定义变量的文件在程序连接时是可用的),变量声明就显得非常有用。可以使用 extern 关键字在任何地方声明一个变量。虽然可以在 C++ 程序中多次声明一个变量,但变量只能在某个文件、函数或代码块中被定义一次。

#include <iostream>
using namespace std;

// 变量声明
extern int a, b;
extern int c;
extern float f;

int main ()
{
  // 变量定义
  int a, b;
  int c;
  float f;

  // 实际初始化
  a = 10;
  b = 20;
  c = a + b;
  f = 70.0/3.0;

  cout << c << endl;
  cout << f << endl;

  return 0;
}

变量作用域

在程序中,局部变量和全局变量的名称可以相同。但是在函数内,局部变量会覆盖全局变量的值。

常量

整数常量

整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数:0x0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。

212     // 合法的
215u    // 合法的
0xFeeL  // 合法的
078     // 非法的:8 不是八进制的数字
032UU   // 非法的:不能重复后缀

字符常量

字符常量括在单引号(’’)中。如果常量以‘L’(仅当大写时)开头,则表示它是一个宽字符常量(例如 L'x'),此时它必须存储在 wchar_t 类型的变量中。否则,它就是一个窄字符常量(例如 'x'),此时它可以存储在 char 类型的简单变量中。

字符串常量

字符串常量括在双引号("")中。

可以使用空格做分隔符,把一个很长的字符串常量进行分行。

下面的实例显示了一些字符串常量。

"hello, dear"
"hello, \
dear"
"hello, " "d" "ear"
hello, dear
hello, dear
hello, dear

定义常量

在 C++ 中,有两种简单的定义常量的方式:

  • 使用 #define 预处理器。
  • 使用 const 关键字。

把常量定义为全大写字母形式,是一个很好的编程习惯。

修饰符类型

类型限定符

限定符 含义
const const 类型的对象在程序执行期间不能被修改
volatile 修饰符 volatile 告诉编译器,变量的值可能以程序未明确指定的方式被改变。
restrict restrict 修饰的指针是唯一一种访问它所指向的对象的方式。只有 C99 增加了新的类型限定符 restrict。

存储类

存储类定义 C++ 程序中变量/函数的范围(可见性)和生命周期。这些说明符放置在它们所修饰的类型之前。下面列出 C++ 程序中可用的存储类:

  • auto
  • register
  • static
  • extern
  • mutable

auto 存储类

auto 存储类是所有局部变量默认的存储类。

auto 只能修饰局部变量。

register 存储类

register 存储类用于定义存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个词),且不能对它应用一元的‘&’运算符以取址(因为它没有内存位置)。

寄存器只用于需要快速访问的变量,比如计数器。还应注意的是,定义‘register’并不意味着变量将被存储在寄存器中,它意味着变量仅仅只是可能存储在寄存器中,这取决于硬件和实现的限制。

static 存储类

static 存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。

static 修饰符也可以应用于全局变量。当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。

在 C++ 中,当 static 用在类数据成员上时,会使仅有一个该成员的副本被类的所有对象共享。

#include <iostream>

// 函数声明
void func(void);

static int count = 10; // 全局变量

int main()
{
    while (count--)
        func();
    return 0;
}

// 函数定义
void func(void)
{
    static int i = 5; // 局部静态变量
    i++;
    std::cout << "i = " << i;
    std::cout << "\t count = " << count << std::endl;
}

extern 存储类

extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当使用 extern 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。

当有多个文件且定义了一个可以在其它文件中使用的全局变量或函数时,可以在其它文件中使用 extern 来得到已定义的变量或函数的引用。可以这么理解,extern 是用来在另一个文件中声明一个全局变量或函数。

extern 修饰符通常用于当有两个或多个文件共享相同的全局变量或函数的时候,如下所示:

第一个文件:main.cpp

#include <iostream>

int count;
extern void write_extern();

int main()
{
    count = 5;
    write_extern();
    return 0;
}

第二个文件:support.cpp

#include <iostream>

extern int count;

void write_extern(void)
{
    std::cout << "Count is " << count << std::endl;
}

在这里,第二个文件中的 extern 关键字用于声明已经在第一个文件 main.cpp 中定义的 count。

Count is 5

mutable 存储类

mutable 说明符仅适用于类的对象,这将在后面讲解。它允许对象的成员替代常量。也就是说,mutable 成员可以通过 const 成员函数修改。

运算符

位运算符

运算符 描述
& 二进制运算
| 二进制运算
^ 二进制异或运算
~ 二进制补码运算,所有位取反
<< 二进制左移运算
>> 二进制右移运算

逗号运算符

逗号运算符会顺序执行一系列运算。整个逗号表达式的值是以逗号分隔的列表中的最后一个表达式的值。逗号表达式中的括号是必需的,因为逗号运算符的优先级低于赋值操作符。

var = (count = 19, incr = 10, count + 1);

变量 var 的值为 20。

下面的例子

#include <iostream>
using namespace std;

int main()
{
    int i, j;

    j = 10;
    i = (j++, j + 100, 999 + j);

    cout << i << endl;

    return 0;
}
1010

函数

函数声明

在函数声明中,参数的名称并不重要,只有参数的类型是必需的,因此下面也是有效的声明:

int max(int, int);

在同一个源文件中使用时,函数声明是可选的。当在一个源文件中定义函数且在另一个文件中调用函数时,函数声明是必需的。在这种情况下,应该在调用函数的文件顶部声明函数。

参数的默认值

当定义一个函数时,可以为参数列表中的每一个参数指定默认值。当调用函数时,如果实际参数的值留空,则使用这个默认值。

这是通过在函数定义中使用赋值运算符来为参数赋值的。调用函数时,如果未传递参数的值,则会使用默认值;如果指定了值,则会忽略默认值,使用传递的值。

#include <iostream>
using namespace std;

int sum(int a, int b = 20)
{
    return a + b;
}

int main()
{
    // 局部变量声明
    int a = 100;
    int b = 200;
    int result1, result2;

    // 调用函数来添加值
    result1 = sum(a, b);
    result2 = sum(a);

    cout << "Total value: " << result1 << endl;
    cout << "Total value: " << result2 << endl;

    return 0;
}
Total value: 300
Total value: 120

C++ 不支持在函数外返回局部变量的地址和引用,除非定义局部变量为 static 变量。

数字

随机数

在许多情况下,需要生成随机数。关于随机数生成器,有两个相关的函数。一个是 rand(),该函数只返回一个伪随机数。生成随机数之前必须先调用 srand() 函数。

示例 1 与示例 2 分别是不设置种子和设置种子时的随机数:

#include <iostream>
#include <cstdlib>

using namespace std;

int main()
{
    /* 生成 10 个随机数 */
    for (int i = 0; i < 10; i++)
        cout << "随机数:" << rand() << endl;

    return 0;
}
#include <iostream>
#include <ctime>
#include <cstdlib>

using namespace std;

int main()
{
    srand((unsigned)time(NULL));

    /* 生成 10 个随机数 */
    for (int i = 0; i < 10; i++)
        cout << "随机数:" << rand() << endl;

    return 0;
}

通过多次执行发现,示例 1 会输出完全一样的结果,而示例 2 的多次结果都不同。这也证明了单独的 rand() 输出的是伪随机数。

引用

引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。通过使用引用来替代指针,会使 C++ 程序更容易阅读和维护。

引用 V.S. 指针

引用很容易与指针混淆,它们之间有三个主要的不同:

  • 不存在空引用。引用必须连接到一块合法的内存。
  • 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
  • 引用必须在创建时被初始化。指针可以在任何时间被初始化。

创建引用

试想变量名称是变量附属在内存位置中的标签,可以把引用当成是变量附属在内存位置中的第二个标签。因此,可以通过原始变量名称或引用来访问变量的内容,访问方法一样。

#include <iostream>

using namespace std;

int main()
{
    // 声明简单的变量
    int i;
    double d;

    // 声明引用变量
    int &r = i;
    double &s = d;

    i = 5;
    d = 11.7;

    cout << "i = " << i << endl;
    cout << "i reference = " << r << endl;

    cout << "d = " << d << endl;
    cout << "d reference = " << s << endl;

    return 0;
}
i = 5
i reference = 5
d = 11.7
d reference = 11.7

日期与时间

C++ 标准库没有提供所谓的日期类型。C++ 继承了 C 用于日期和时间操作的结构和函数。为了使用日期和时间相关的函数和结构,需要在 C++ 程序中引用 <ctime> 头文件。

有四个与时间相关的类型:clock_ttime_tsize_ttm。类型 clock_t、size_t 和 time_t 能够把系统时间和日期表示为某种整数,结构类型 tm 把日期和时间以 C 结构的形式保存。

tm 结构的定义如下:

struct tm
{
    int tm_sec;   // 秒,正常范围从 0 到 59,但允许至 61
    int tm_min;   // 分,范围从 0 到 59
    int tm_hour;  // 小时,范围从 0 到 23
    int tm_mday;  // 一月中的第几天,范围从 1 到 31
    int tm_mon;   // 月,范围从 0 到 11
    int tm_year;  // 自 1900 年起的年数
    int tm_wday;  // 一周中的第几天,范围从 0 到 6,从星期日算起
    int tm_yday;  // 一年中的第几天,范围从 0 到 365,从 1 月 1 日算起
    int tm_isdst; // 夏令时
}

当前日期和时间

下面的实例获取当前系统的日期和时间,包括本地时间和协调世界时(UTC)。

#include <iostream>
#include <ctime>

using namespace std;

int main()
{
    // 基于当前系统的当前日期/时间
    time_t now = time(0);

    // 把 now 转换为字符串形式
    char *dt = ctime(&now);

    cout << "本地日期和时间:" << dt;

    // 把 now 转换为 tm 结构
    tm *gmtm = gmtime(&now);
    dt = asctime(gmtm);
    cout << "UTC 日期和时间:" << dt;
}
本地日期和时间:Mon Feb 12 21:01:14 2018
UTC 日期和时间:Mon Feb 12 13:01:14 2018

使用结构 tm 格式化时间

tm 结构在 C/C++ 中处理日期和时间相关的操作时,显得尤为重要。tm 结构以 C 结构的形式保存日期和时间。大多数与时间相关的函数都使用了 tm 结构。下面的实例使用了 tm 结构和各种与日期和时间相关的函数。

#include <iostream>
#include <ctime>

using namespace std;

int main()
{
    // 基于当前系统的当前日期/时间
    time_t now = time(0);

    cout << "Number of sec since January 1, 1970: " << now << endl;

    tm *ltm = localtime(&now);

    // 输出 tm 结构的各个组成部分
    cout << "Year: " << 1900 + ltm->tm_year << endl;
    cout << "Month: " << 1 + ltm->tm_mon << endl;
    cout << "Day: " << ltm->tm_mday << endl;
    cout << "Time: " << 1 + ltm->tm_hour << ":";
    cout << 1 + ltm->tm_min << ":";
    cout << 1 + ltm->tm_sec << endl;

    return 0;
}
Number of sec since January 1, 1970: 1518441081
Year: 2018
Month: 2
Day: 12
Time: 22:12:22

基本的输入输出

I/O 库头文件

下列头文件在 C++ 编程中很重要。

头文件 函数和描述
<iostream> 该文件定义了 cincoutcerrclog 对象,分别对应于标准输入流、标准输出流、非缓冲标准错误流和缓冲标准错误流。
<iomanip> 该文件通过所谓的参数化的流操纵器(比如 setwsetprecision),来声明对执行标准化 I/O 有用的服务。
<fstream> 该文件为用户控制的文件处理声明服务。后面将在文件和流的相关章节讨论它的细节。

标准输出流(cout)

预定义的对象 coutostream 类的一个实例。cout 对象“连接”到标准输出设备,通常是显示屏。cout 是与流插入运算符 << 结合使用的。

#include <iostream>

using namespace std;

int main()
{
    char str[] = "Hello C++";

    cout << "Value of str is : " << str;

    return 0;
}
Value of str is : Hello C++

C++ 编译器根据要输出变量的数据类型,选择合适的流插入运算符来显示值。<< 运算符被重载来输出内置类型(整型、浮点型、double 型、字符串和指针)的数据项。

标准输入流(cin)

预定义的对象 cinistream 类的一个实例。cin 对象附属到标准输入设备,通常是键盘。cin 是与流提取运算符 >> 结合使用的。

#include <iostream>

using namespace std;

int main()
{
    char name[50];

    cout << "请输入您的名称:";
    cin >> name;
    cout << "您的名称是:" << name << endl;
}
请输入您的名称:CPlusPlus
您的名称是:CPlusPlus

C++ 编译器根据要输入值的数据类型,选择合适的流提取运算符来提取值,并把它存储在给定的变量中。流提取运算符 >> 在一个语句中可以多次使用,如果要求输入多个数据,可以使用如下语句:

cin >> name >> age;

这相当于下面两个语句:

cin >> name;
cin >> age;

标准错误流(cerr)

预定义的对象 cerrostream 类的一个实例。cerr 对象附属到标准错误设备,通常也是显示屏,但是 cerr 对象是非缓冲的,且每个流插入到 cerr 都会立即输出。cerr 也是与流插入运算符 << 结合使用的。

#include <iostream>

using namespace std;

int main()
{
    char str[] = "Unable to read…";

    cerr << "Error message: " << str;

    return 0;
}
Error message: Unable to read…

标准日志流(clog)

预定义的对象 clogostream 类的一个实例。clog 对象附属到标准错误设备,通常也是显示屏,但是 clog 对象是缓冲的。这意味着每个流插入到 clog 都会先存储在缓冲在,直到缓冲填满或者缓冲区刷新时才会输出。clog 也是与流插入运算符 << 结合使用的。

#include <iostream>

using namespace std;

int main()
{
    char str[] = "Unable to read…";

    clog << "Error message: " << str;

    return 0;
}
Error message: Unable to read…

通过这些小实例,我们无法区分 cout、cerr 和 clog 的差异,但在编写和执行大型程序时,它们之间的差异就变得非常明显。所以良好的编程实践告诉我们,使用 cerr 流来显示错误消息,而其它的日志消息则使用 clog 流来输出。

结构类型

结构的声明

  • 使用 struct 关键字+结构名+结构变量名。
struct structure_Tag
{
    // 结构成员列表
};

struct structure_Tag structure_variable1, structure_variable2;

struct structure_Tag
{
    // 结构成员列表
} structure_variable1, structure_variable2;
  • 使用 typedef 为结构创建“别名”之后,使用结构别名+结构变量名。
typedef struct
{
    // 结构成员列表
} structure_alias;

structure_alias structure_variable1, structure_variable2;

如以下示例。

#include <iostream>

using namespace std;

struct Books
{
    char title[50];
    char author[50];
    char subject[100];
    int book_id;
};

typedef struct
{
    char title[80];
    char author[80];
    char subject[120];
    int note_id;
} Notes;

int main()
{
    // 直接使用 struct 加结构名来声明结构变量
    struct Books Book1, Book2;

    // 使用 typedef 为结构创建“别名”之后,声明结构变量
    Notes Note1, Note2;

    /* 其它代码 */

    return 0;
}

类与对象

类定义

类定义以关键字 class 开头,后跟类的名称。类的主体包含在一对花括号中。类定义后必须跟着一个分号或一个声明列表。

class Box
{
  public:
    double length;  // Length of a box
    double breadth; // Breadth of a box
    double height;  // Height of a box
};

类成员函数

类成员函数的定义有多种方式。

  • 直接定义在类定义内部。在类定义中定义的成员函数把函数声明为内联的,即便没有使用 inline 标识符。
class Box
{
  public:
    double length;  // 长度
    double breadth; // 宽度
    double height;  // 高度

    // 成员函数定义
    double getVolume(void)
    {
        return length * breadth * height;
    }
};
  • 在类的外部单独使用范围解析运算符(::)来定义。注意在 :: 运算符之前必须使用类名。
class Box
{
  public:
    double length;  // 长度
    double breadth; // 宽度
    double height;  // 高度

    // 成员函数声明
    double getVolume(void);
};

// 成员函数定义
double Box::getVolume(void)
{
    return length * breadth * height;
}

构造函数

构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。

使用初始化列表来初始化字段

Line::Line(double len, double wid) : length(len), width(wid)
{
    //
}

相当于

Line::Line(double len, double wid)
{
    //
    length = len;
    width = wid;
}

拷贝构造函数

拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:

  • 通过使用另一个同类型的对象来初始化新创建的对象。

  • 复制对象把它作为参数传递给函数。

  • 复制对象,并从函数返回这个对象。

如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。

class_name (const class_name &obj) {
    // 构造函数的主体
}

在这里,obj 是一个对象引用,该对象是用于初始化另一个对象的。

#include <iostream>
using namespace std;

class Line
{
  public:
    Line(int len);            // 简单的构造函数
    Line(const Line &object); // 拷贝构造函数

  private:
    int *ptr;
};

// 构造函数
Line::Line(int len)
{
    ptr = new int;
    *ptr = len;
}

// 拷贝构造函数
Line::Line(const Line &object)
{
    ptr = new int;
    *ptr = *object.ptr; // copy the value
}

int main()
{
    Line line1(10);

    Line line2 = line1; // 这里调用了构造函数和拷贝构造函数

    // 其它代码
}

友元函数

类的友元函数定义在类外部,但有权访问类的所有私有成员和保护成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数

友元可以是一个函数,该函数称为友元函数;友元也可以是一个类,该类称为友元类,在这种情况下,整个类及其所有成员都是友元。

如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend

class Box
{
    double width;

  public:
    double length;
    friend void printWidth(Box box); // 友元函数
    void setWidth(double wid);
};

声明类 ClassTwo 为类 ClassOne 的友元类,需要在类 ClassOne 的定义中放置如下声明:

friend class ClassTwo;
#include <iostream>
using namespace std;

class Box
{
    // 没有访问修饰符则默认为私有类型
    double width;

  public:
    friend void printWidth(Box box);
    void setWidth(double wid);
};

// 成员函数定义
void Box::setWidth(double wid)
{
    width = wid;
}

// 注意,printWidth() 不是任何类的成员函数
void printWidth(Box box)
{
    // 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员
    cout << "Width of box : " << box.width << endl;
}

int main()
{
    Box box;

    // 使用成员函数设置宽度
    box.setWidth(10.0);

    // 使用友元函数输出宽度
    printWidth(box);

    return 0;
}

静态成员

静态数据成员

静态数据成员在类的外部通过使用范围解析运算符 :: 来初始化。

静态函数成员

静态函数使用类名加范围解析运算符 :: 加以访问。

静态成员函数只能访问静态数据成员,不能访问其它静态成员函数和类外部的其它函数。

静态成员函数有一个类范围,不能访问类的 this 指针。可以使用静态成员函数来判断类的某些对象是否已被创建。

继承

基类与派生类

一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。

class derived-class: private base-class1, public base-class2

访问控制和继承

派生类可以访问基类中所有的非私有成员。

一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

继承类型

当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。

我们几乎不使用保护或私有继承,通常使用公有继承。当使用不同类型的继承时,遵循以下几个规则:

  • 公有继承:当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员;基类的保护成员也是派生类的保护成员;基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
  • 保护继承: 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
  • 私有继承:当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。

重载运算符和重载函数

运算符重载

可以重定义或重载大部分内置的运算符。这样,就能使用自定义类型的运算符。

重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其它函数一样,重载运算符有一个返回类型和一个参数列表。

Box operator+(const Box &b)

可重载运算符与不可重载运算符

下面是可重载的运算符列表:

+ - * / % ^
& | ~ ! , =
< > <= >= ++
<< >> == != && ||
+= -= /= %= ^= &=
|= *= <<= >>= [] ()
-> ->* new new [] delete delete []

下面是不可重载的运算符列表:

:: .* . ?:

多态

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

#include <iostream>
using namespace std;

class Shape
{
  protected:
    int width, height;

  public:
    Shape(int a = 0, int b = 0)
    {
        width = a;
        height = b;
    }
    int area()
    {
        cout << "Parent class area:" << endl;
        return 0;
    }
};
class Rectangle : public Shape
{
  public:
    Rectangle(int a = 0, int b = 0) : Shape(a, b) {}
    int area()
    {
        cout << "Rectangle class area:" << endl;
        return (width * height);
    }
};
class Triangle : public Shape
{
  public:
    Triangle(int a = 0, int b = 0) : Shape(a, b) {}
    int area()
    {
        cout << "Triangle class area:" << endl;
        return (width * height / 2);
    }
};

int main()
{
    Shape *shape;
    Rectangle rec(10, 7);
    Triangle tri(10, 5);

    // 存储矩形的地址
    shape = &rec;
    // 调用矩形的求面积函数 area
    shape->area();

    // 存储三角形的地址
    shape = &tri;
    // 调用三角形的求面积函数 area
    shape->area();

    return 0;
}
Parent class area:
Parent class area:

导致错误输出的原因是,调用的函数 area() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接——函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了。

但现在,让我们对程序稍作修改,在 Shape 类中,area() 的声明前放置关键字 virtual

class Shape
{
  protected:
    int width, height;

  public:
    Shape(int a = 0, int b = 0)
    {
        width = a;
        height = b;
    }
    virtual int area()
    {
        cout << "Parent class area:" << endl;
        return 0;
    }
};
Rectangle class area:
Triangle class area:

此时,编译器看的是指针的内容,而不是它的类型。因此,由于 tri 和 rec 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。

正如所看到的,每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。有了多态,可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。

虚函数

虚函数是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。

我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定

纯虚函数

有时可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。

可以把基类中的虚函数 area() 改写如下:

class Shape
{
  protected:
    int width, height;

  public:
    Shape(int a = 0, int b = 0)
    {
        width = a;
        height = b;
    }

    // pure virtual function
    virtual int area() = 0;
};

= 0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数

接口(抽象类)

接口描述了类的行为和功能,而不需要完成类的特定实现。

C++ 接口是使用抽象类来实现的。如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 = 0 来指定的。

class Box
{
  public:
    // 纯虚函数
    virtual double getVolume() = 0;

  private:
    double length;  // 长度
    double breadth; // 宽度
    double height;  // 高度
};

设计抽象类的目的,是为了给其它类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。

因此,如果一个抽象类的子类需要被实例化,则必须实现每个虚函数,这也意味着 C++ 支持使用抽象类声明接口。如果没有在派生类中重载纯虚函数,就尝试实例化该类的对象,会导致编译错误。

可用于实例化对象的类被称为具体类,是与抽象类相对的。

文件和流

从文件读取流和向文件写入流需要用到 C++ 中另一个标准库 fstream,它定义了三个新的数据类型:

数据类型 描述
ofstream 该数据类型表示输出文件流,用于创建文件并向文件写入信息。
ifstream 该数据类型表示输入文件流,用于从文件读取信息。
fstream 该数据类型通常表示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件、向文件写入信息、从文件读取信息。

要在 C++ 中进行文件处理,必须在 C++ 源代码文件中包含头文件 <iostream><fstream>

打开文件

在从文件读取信息或者向文件写入信息之前,必须先打开文件。ofstreamfstream 对象都可以用来打开文件进行写操作,如果只需要打开文件进行读操作,则使用 ifstream 对象。

下面是 open() 函数的标准语法,open() 函数是 fstream、ifstream 和 ofstream 对象的一个成员。

void open(const char *filename, ios::openmode mode);

在这里,open() 成员函数的第一参数指定要打开的文件的名称和位置,第二个参数定义文件被打开的模式。

模式标志 描述
ios::app 追加模式。所有写入都追加到文件末尾。
ios::ate 文件打开后定位到文件末尾。
ios::in 打开文件用于读取。
ios::out 打开文件用于写入。
ios::trunc 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。

可以把上面的两种及以上的模式结合使用。例如,如果想要以写入模式打开文件,并希望截断文件,以防文件已存在,那么可以使用下面的语法:

ofstream outfile;
outfile.open("file.dat", ios::out | ios::trunc);

类似地,如果想要打开一个文件用于读写,可以使用下面的语法:

fstream afile;
afile.open("file.dat", ios::out | ios::in);

关闭文件

当 C++ 程序终止时,它会自动关闭刷新所有流,释放所有分配的内存,并关闭所有打开的文件。但程序员应该养成一个好习惯,在程序终止前关闭所有打开的文件。

下面是 close() 函数的标准语法,close() 函数是 fstream、ifstream 和 ofstream 对象的一个成员。

void close();

写入文件

在 C++ 编程中,我们使用流插入运算符(<<)向文件写入信息,就像使用该运算符输出信息到屏幕上一样。唯一不同的是,在这里使用的是 ofstreamfstream 对象,而不是 cout 对象。

读取文件

在 C++ 编程中,我们使用流提取运算符(>>)从文件读取信息,就像使用该运算符从键盘输入信息一样。唯一不同的是,在这里使用的是 ifstreamfstream 对象,而不是 cin 对象。

读取与写入实例

下面的 C++ 程序以读写模式打开一个文件。在向文件 afile.dat 写入用户输入的信息之后,程序从文件读取信息,并将其输出到屏幕上:

#include <fstream>
#include <iostream>
using namespace std;

int main()
{

    char data[100];

    // 以写模式打开文件
    ofstream outfile;
    outfile.open("afile.dat");

    cout << "Writing to the file" << endl;
    cout << "Enter your name: ";
    cin.getline(data, 100);

    // 向文件写入用户输入的数据
    outfile << data << endl;

    cout << "Enter your age: ";
    cin >> data;
    cin.ignore();

    // 再次向文件写入用户输入的数据
    outfile << data << endl;

    // 关闭打开的文件
    outfile.close();

    // 以读模式打开文件
    ifstream infile;
    infile.open("afile.dat");

    cout << "Reading from the file" << endl;
    infile >> data;

    // 在屏幕上写入数据
    cout << data << endl;

    // 再次从文件读取数据,并显示它
    infile >> data;
    cout << data << endl;

    // 关闭打开的文件
    infile.close();

    return 0;
}
Writing to the file
Enter your name: Roger
Enter your age: 19
Reading from the file
Roger
19

上面的实例中使用了 cin 对象的附加函数,比如 getline()函数从外部读取一行,ignore() 函数会忽略掉之前读语句留下的多余字符。

文件位置指针

istreamostream 都提供了用于重新定位文件位置指针的成员函数。这些成员函数包括关于 istream 的 seekg(“seek get”)和关于 ostream 的 seekp(“seek put”)。

seekg 和 seekp 的参数通常是一个长整型。第二个参数可以用于指定查找方向。查找方向可以是 ios::beg(默认,从流的开头开始定位),也可以是 ios::cur(从流的当前位置开始定位),也可以是 ios::end(从流的末尾开始定位)。

文件位置指针是一个整数值,指定了从文件的起始位置到指针所在位置的字节数。下面是关于文件位置指针 seekg 的实例:

// 定位到 fileObject 的第 n 个字节(假设是 ios::beg)
fileObject.seekg(n);

// 把文件的读指针从 fileObject 当前位置向后移 n 个字节
fileObject.seekg(n, ios::cur);

// 把文件的读指针从 fileObject 末尾往回移 n 个字节
fileObject.seekg(n, ios::end);

// 定位到 fileObject 的末尾
fileObject.seekg(0, ios::end);

模板

模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。

函数模板

模板函数定义的一般形式如下所示:

template <class type>
ret_type func_name(parameter_list)
{
    // 函数的主体
}

在这里,type 是函数所使用的数据类型的占位符名称。这个名称可以在函数定义中使用。

#include <iostream>
#include <string>

using namespace std;

template <typename T>
inline T const &Max(T const &a, T const &b)
{
    return (a < b) ? b : a;
}

int main()
{

    int i = 39;
    int j = 20;
    cout << "Max(i, j): " << Max(i, j) << endl;

    double f1 = 13.5;
    double f2 = 20.7;
    cout << "Max(f1, f2): " << Max(f1, f2) << endl;

    string s1 = "Hello";
    string s2 = "World";
    cout << "Max(s1, s2): " << Max(s1, s2) << endl;

    return 0;
}
Max(i, j): 39
Max(f1, f2): 20.7
Max(s1, s2): World

类模板

泛型类声明的一般形式如下所示:

template <class type>
class class_name
{
    // 类的主体
}

在这里,type 是占位符类型名称,可以在类被实例化的时候进行指定。您可以使用一个逗号分隔的列表来定义多个泛型数据类型。

下面的实例定义了类 Stack<>,并实现了泛型方法来对元素进行入栈出栈操作:

#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>

using namespace std;

template <class T>
class Stack
{
  private:
    vector<T> elems; // 元素

  public:
    void push(T const &); // 入栈
    void pop();           // 出栈
    T top() const;        // 返回栈顶元素
    bool empty() const
    {
        // 如果为空则返回真。
        return elems.empty();
    }
};

template <class T>
void Stack<T>::push(T const &elem)
{
    // 追加传入元素的副本
    elems.push_back(elem);
}

template <class T>
void Stack<T>::pop()
{
    if (elems.empty())
        throw out_of_range("Stack<>::pop(): empty stack");

    // 删除最后一个元素
    elems.pop_back();
}

template <class T>
T Stack<T>::top() const
{
    if (elems.empty())
        throw out_of_range("Stack<>::top(): empty stack");

    // 返回最后一个元素的副本
    return elems.back();
}

int main()
{
    try
    {
        Stack<int> intStack;       // int 类型的栈
        Stack<string> stringStack; // string 类型的栈

        // 操作 int 类型的栈
        intStack.push(7);
        cout << intStack.top() << endl;

        // 操作 string 类型的栈
        stringStack.push("hello");
        cout << stringStack.top() << std::endl;
        stringStack.pop();
        stringStack.pop();
    }
    catch (exception const &ex)
    {
        cerr << "Exception: " << ex.what() << endl;
        return -1;
    }
}
7
hello
Exception: Stack<>::pop(): empty stack

预处理器

预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。

所有的预处理器指令都是以井号(#)开头,只有空格字符可以出现在预处理指令之前。预处理指令不是 C++ 语句,所以它们不会以分号(;)结尾。

# 和 ## 运算符

  • # 运算符会把 replacement-text 令牌转换为用引号引起来的字符串。
#include <iostream>
using namespace std;

#define MKSTR(x) #x

int main()
{
    cout << MKSTR(Hello C++) << endl;

    return 0;
}
Hello C++

让我们来看看它是如何工作的。不难理解,C++ 预处理器把下面这行:

cout << MKSTR(Hello C++) << endl;

转换成了:

cout << "Hello C++" << endl;
  • ## 运算符用于连接两个令牌。
#include <iostream>
using namespace std;

#define concat(a, b) a##b
int main()
{
    int xy = 100;

    cout << concat(x, y);
    return 0;
}
100

让我们来看看它是如何工作的。不难理解,C++ 预处理器把下面这行:

cout << concat(x, y);

转换成了:

cout << xy

预定义宏

C++ 提供了下表所示的一些预定义宏:

描述
__LINE__ 包含当前行号。
__FILE__ 包含当前文件名。
__DATE__ 包含一个形式为 month/day/year 的字符串,它表示把源文件转换为目标代码的日期。
__TIME__ 包含一个形式为 hour:minute:second 的字符串,它表示程序被编译的时间。
#include <iostream>
using namespace std;

int main()
{
    cout << "Value of __LINE__: " << __LINE__ << endl;
    cout << "Value of __FILE__: " << __FILE__ << endl;
    cout << "Value of __DATE__: " << __DATE__ << endl;
    cout << "Value of __TIME__: " << __TIME__ << endl;

    return 0;
}
Value of __LINE__: 6
Value of __FILE__: test.cpp
Value of __DATE__: Feb 14 2018
Value of __TIME__: 13:53:03

信号处理

信号是由操作系统传给进程的中断,会提早终止一个程序。在 Unix、Linux、macOS、Windows 操作系统上,可以通过按 Ctrl+C 产生中断。

有些信号不能被程序捕获,但是下表所列信号可以在程序中捕获,并可以基于信号采取适当的动作。这些信号定义在 C++ 头文件 <csignal> 中。

信号 描述
SIGABRT 程序的异常终止,如调用 abort
SIGFPE 错误的算术运算,比如除以零或导致溢出的操作。
SIGILL 检测非法指令。
SIGINT 接收到交互注意信号。
SIGSEGV 非法访问内存。
SIGTERM 发送到程序的终止请求。

signal() 函数

C++ 信号处理库提供了 signal() 函数,用来捕获突发事件。

void (*signal(int sig, void (*func)(int)))(int);

这个函数接收两个参数:第一个参数是一个整数,代表了信号的编号;第二个参数是一个指向信号处理函数的指针。

接下来编写一个简单的 C++ 程序,使用 signal() 函数捕获 SIGINT 信号。不管想在程序中捕获什么信号,都必须使用 signal 函数来注册信号,并将其与信号处理程序相关联。

#include <iostream>
#include <csignal>
#include <windows.h>

using namespace std;

void signalHandler(int signum)
{
    cout << "Interrupt signal (" << signum << ") received.\n";

    // 清理并关闭
    // 终止程序

    exit(signum);
}

int main()
{
    // 注册信号 SIGINT 和信号处理程序
    signal(SIGINT, signalHandler);

    while (true)
    {
        cout << "Going to sleep..." << endl;
        Sleep(1);
    }

    return 0;
}
Going to sleep...
Going to sleep...
Going to sleep...

现在,按 Ctrl+C 来中断程序,会看到程序捕获信号,程序打印如下内容并退出:

Going to sleep...
Going to sleep...
Going to sleep...
Interrupt signal (2) received.

raise() 函数

可以使用函数 raise() 生成信号,该函数带有一个整数信号编号作为参数。

int raise(signal sig);

在这里,sig 是要发送的信号的编号,这些信号包括:SIGINT、SIGABRT、SIGFPE、SIGILL、SIGSEGV、SIGTERM、SIGHUP。以下使用 raise() 函数内部生成信号。

#include <iostream>
#include <csignal>
#include <windows.h>

using namespace std;

void signalHandler(int signum)
{
    cout << "Interrupt signal (" << signum << ") received.\n";

    // 清理并关闭
    // 终止程序

    exit(signum);
}

int main()
{
    int i = 0;
    // 注册信号 SIGINT 和信号处理程序
    signal(SIGINT, signalHandler);

    while (++i)
    {
        cout << "Going to sleep..." << endl;
        if (i == 3)
            raise(SIGINT);

        Sleep(1);
    }

    return 0;
}
Going to sleep...
Going to sleep...
Going to sleep...
Interrupt signal (2) received.

多线程

多线程是多任务处理的一种特殊形式,多任务处理允许让电脑同时运行两个或两个以上的程序。在一般情况下,有两种类型的多任务处理:基于进程基于线程

基于进程的多任务处理处理的是程序的并发执行;基于线程的多任务处理的是同一程序的片段的并发执行。

多线程程序包含可以同时运行的两个或多个部分。这样的程序中的每个部分称为一个线程,每个线程定义了一个单独的执行路径。

C++ 不包含多线程应用程序的任何内置支持。相反,它完全依赖于操作系统来提供此功能。

标准模板库

C++ STL(Standard Template Library,标准模板库)是一套功能强大的 C++ 模板类,提供了通用的模板类和函数,这些模板类和函数可以实现多种流行和常用的算法和数据结构,如矢量、链表、队列、栈。

除了函数对象之外,C++ 标准模板库的核心包括以下三个组件:

组件 描述
容器(containers) 容器是用来管理某一类对象的集合。C++ 提供了各种不同类型的容器,比如双端队列(deque)、双向链表(list)、计算矢量(vector)、关联数组(map)等。
算法(algorithms) 算法作用于容器。它们提供了执行各种操作的方式,包括对容器内容执行初始化、排序、搜索和转换等操作。

这三个组件都带有丰富的预定义函数,帮助我们通过简单的方式处理复杂的任务。

下面的程序演示了矢量容器(一个 C++ 标准的模板),它与数组十分相似,唯一不同的是,矢量在需要扩展大小的时候,会自动处理它自己的存储需求:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    // 创建一个矢量存储 int
    vector<int> vec;
    int i;

    // 显示 vec 的原始大小
    cout << "vector size = " << vec.size() << endl;

    // 推入 5 个值到矢量中
    for (i = 0; i < 5; i++)
    {
        vec.push_back(i);
    }

    // 显示 vec 扩展后的大小
    cout << "extended vector size = " << vec.size() << endl;

    // 访问矢量中的 5 个值
    for (i = 0; i < 5; i++)
    {
        cout << "value of vec [" << i << "] = " << vec[i] << endl;
    }

    // 使用迭代器 iterator 访问值
    vector<int>::iterator v = vec.begin();
    while (v != vec.end())
    {
        cout << "value of v = " << *v << endl;
        v++;
    }

    return 0;
}
vector size = 0
extended vector size = 5
value of vec [0] = 0
value of vec [1] = 1
value of vec [2] = 2
value of vec [3] = 3
value of vec [4] = 4
value of v = 0
value of v = 1
value of v = 2
value of v = 3
value of v = 4

关于上面实例中所使用的各种函数,有几点要注意:

  • push_back() 成员函数在矢量的末尾插入值,如果有必要会扩展矢量的大小。
  • size() 函数显示矢量的大小。
  • begin() 函数返回一个指向矢量开头的迭代器。
  • end() 函数返回一个指向矢量末尾的迭代器。

评论