【C++基础】第十二课:自定义数据结构

struct,预处理器,头文件保护符

Posted by x-jeff on November 26, 2019

【C++基础】系列博客为参考《C++ Primer中文版(第5版)》C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.前言

C++语言允许用户以类的形式自定义数据类型。

2.定义Sales_data类型

我们可以尝试定义一个Sales_data类,把书本的ISBN编号、售出量及销售收入等数据组织在一起,并使用户能直接访问其中的数据元素。这个Sales_data类就是一个数据结构。例如其定义可为:

1
2
3
4
5
struct Sales_data {
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

我们的类以关键字struct开始,紧跟着类名类体(其中类体部分可以为空)。类体由花括号包围形成了一个新的作用域。类内部定义的名字必须唯一,但是可以与类外部定义的名字重复。

⚠️类体右侧的表示结束的花括号后必须写一个分号,这是因为类体后面可以紧跟变量名以示对该类型对象的定义,所以分号必不可少:

1
2
3
4
struct Sales_data { /*......*/ } accum,trans,*salesptr;
//与上一条语句等价,但可能更好一些
struct Sales_data { /*......*/ };
Sales_data accum,trans,*salesptr;

2.1.类数据成员

类体定义类的成员,在上述例子中,我们的类只有数据成员,并且共有3个数据成员。

❗️C++11新标准规定,可以为数据成员提供一个类内初始值。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化。

C++语言还提供另外一个关键字class来定义自己的数据结构,目前我们先使用struct来定义自己的数据结构,后续的博客中我们会详细介绍class,以及其与struct的异同。

3.使用Sales_data类

【C++基础】第四课:类简介一文中,我们定义了一个Sales_item类。和Sales_item类不同的是,本文中我们自定义的Sales_data类没有提供任何操作,所以Sales_data类的使用者如果想执行什么操作就必须自己动手实现。

如果我们想使用Sales_data类实现【C++基础】第四课:类简介中求两次交易相加结果的功能。程序的输入是下面这两条交易记录:

0-201-78345-X 3 20.00
0-201-78345-X 2 25.00

每笔交易记录着图书的ISBN编号、售出数量和售出单价。

3.1.添加两个Sales_data对象

假设已知Sales_data类定义于Sales_data.h文件内。

1
2
3
4
5
6
7
#include <iostream>
#include <string>
#include "Sales_data.h"
int main()
{
	Sales_data data1, data2;
}

3.2.Sales_data对象读入数据

在上一段程序中,包含了string头文件,因为我们的代码中将用到string类型的成员变量bookNo。在后续的博客中将会详细介绍string类型的细节。

现在我们只需知道string类型其实就是字符的序列,它的操作有<<>>==等,功能分别是读入字符串、写出字符串和比较字符串。

这样我们就能书写代码读入第一笔交易了:

1
2
3
4
5
6
7
8
double price = 0;//书的单价,用于计算销售收入
//读入第1笔交易:ISBN、销售数量、单价
std::cin >> data1.bookNo >> data1.units_sold >> price;
//计算销售收入
data1.revenue = data1.units_sold * price;
//读入第2笔交易
std::cin >> data2.bookNo >> data2.units_sold >> price;
data2.revenue = data2.units_sold * price;

3.3.输出两个Sales_data对象的和

剩下的工作就是检查两笔交易涉及的ISBN编号是否相同了。如果相同输出它们的和,否则输出一条报错信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (data1.bookNo == data2.bookNo){
	unsigned totalCnt = data1.units_sold + data2.units_sold;
	double totalRevenue = data1.revenue + data2.revenue;
	//输出:ISBN、总销售量、总销售额、平均价格
	std::cout << data1.bookNo << " " << totalCnt << " " << totalRevenue << " ";
	if (totalCnt != 0)
		std::cout << totalRevenue/totalCnt << std::endl;
	else
		std::cout << " (no sales) " << std::endl;
	return 0; //标示成功
}else { //两笔交易的ISBN不一样
	std::cerr << "Data must refer to the same ISBN" << std::endl;
	return -1; //标示失败
}

4.编写自己的头文件

为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。

例如,库类型string在名为string的头文件中定义。又如,我们应该把Sales_data类定义在名为Sales_data.h的头文件中。

❗️头文件通常包含那些只能被定义一次的实体,如类、const和constexpr变量等。头文件也经常用到其他头文件的功能。

4.1.预处理器概述

Sales_data类包含有一个string成员,所以Sales_data.h必须包含string.h头文件。同时,使用Sales_data类的程序为了能操作bookNo成员需要再一次包含string.h头文件。这样,事实上使用Sales_data类的程序就先后两次包含了string.h头文件。

❗️确保头文件多次包含仍能安全工作的常用技术是预处理器(preprocessor)

👉预处理器是在编译之前执行的一段程序。之前已经用到了一项预处理功能#include,当预处理器看到#include标记时就会用指定的头文件的内容代替#include

👉C++程序还会用到的一项预处理功能是头文件保护符,头文件保护符依赖于预处理变量

‼️预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。

使用这些功能就能有效地防止重复包含的发生:

1
2
3
4
5
6
7
8
9
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data {
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};
#endif

如果#ifndef的检查结果为假,则编译器将忽略#ifndef#endif之间的部分。

⚠️预处理变量无视C++语言中关于作用域的规则。

⚠️要确保保护符名字的唯一性。为了避免与程序中的其他实体发生名字冲突,一般把预处理变量的名字全部大写