【c++ Prime 学习笔记】第2章 变量和基本类型
阅读原文时间:2023年07月08日阅读:1

2.1 基本内置类型

基本数据类型包含了算术类型(arithmetic type)空类型(void)

  • 算数类型,包含了字符、整型数、布尔值和浮点数

  • 空类型,不对应具体的值

  • 算术类型分为两类:整型(integral type,包含字符和布尔)和浮点型

  • bool 取值true或false

  • char 大小和机器字节一样

  • 其他字符类型用于扩展字符集,wchar_t用于确保可存放机器最大扩展字符集中的任意字符,char16_tchar32_t为Unicode字符集服务。

  • short ≤ int ≤ long ≤ long long。其中,数据类型 long long 是在 C++11 中新定义的

  • 浮点型可表示单精度、双精度、扩展精度。

    • float 表示单精度浮点数,格式为1位符号,8位指数,23位小数

    • double 表示双精度浮点数,格式为1位符号,11位指数,52位小数。

      long double 提供的精度在一般情况下是没有必要的,况且它带来的运算时消耗也不容忽视。

带符号类型和无符号类型

  • 带符号(signed)

    • int、short、long、long long
  • 无符号(unsigned)

    • unsigned long、unsigned int(可以缩写unsigned)
  • 字符型

    • char 和signed char 并不一样;有无符号由编译器决定

    • signed char 8位,表示0~255

    • unsigned char 理论表示-127127,大多是现代计算机将实际的表示范围定位-128127


自动转换过程

  • 非bool → bool,值为0则返回false,否则返回true

  • bool → 非bool,值为false则返回0,值为true则返回1

  • 浮点 → 整型,只保留整数部分

  • 整型 → 浮点,小数部分记为0,若整数所占空间大于浮点型容量,则损失精度

  • 给unsigned赋超出范围的值,结果为该值对unsigned表示数值总数取模后的余数,如-1赋值unsigned char得到255

  • 给signed赋超出范围的值,结果为undefined

含有无符号类型的表达式

算术表达式中既有signed又有unsigned时,signed会转成unsigned 。因此,切勿混用unsigned和signed


   一个形如42的值被称作字面值常量。字面值常量的形式和值决定了它的数据类型。

整型和浮点型的字面值

  • 默认十进制字面值是signed,八进制和十六进制字面值可能是signed或unsigned

    20/* 十进制 /、024/ 八进制 /、0x14/ 二进制 */

  • 十进制字面值的类型是int、long、long long中的最小者,八进制和十六进制字面值的类型是int、long、long long、unsigned int、unsigned long、unsigned long long中的最小者。因此short没有对应的字面值。

  • 浮点型字面值表现为小数或以科学计数法表示的指数,默认浮点字面值是double类型

    3.14159 、3.14159E0、0.、0e0、.001

字符和字符串字面值

'a'  //char型字面值
"Hello World!"     //字符串字
  • 字符串型字面值的类型实际是常量字符构成的数组。编译器在字符串结尾处添加空字符'\0',因此字符串的长度比内容多1

  • 分行书写字符串

    std::cout << "a really, really long string literal "
    "that spans two lines" << std::endl;

转义序列

   在C++中有特殊含义的字符(如单双引号、问号、反斜线等)出现在字符串中时要转义,转义序列均以反斜线作为开始。

  • 泛型转义字符

    形式是\x后紧跟1个或多个十六进制数,或\后紧跟1、2、3个八进制数,其中数字部分表示的是字符对应的数值。如果超过3个,八进制只看前3个,十六进制看所有数字

    std::cout<< "Hi \x4dO\115!\n"; //输出Hi MOM!,换行 \115 \x4d代表字符M

指定字面值的类型

布尔字面值 true 和 false

指针字面值 nullptr


2.2 变量

  • 变量提供一个具名的、可供程序操作的存储空间。
  • 变量都有其数据类型,决定着其所占内存空间的大小和布局方式、该控件能存储的值的范围。
  • “变量(variable)”和“对象(object)”一般可以互换使用。

基本形式

类型说明符(type specifier)后紧跟一个或多个变量名组成的列表,变量名以逗号分隔,最后分号结束

int sum=0, value, units_sold=0;
Sales_item item;
std::string book("0-201-78345-X");

对象(object):指一块能存储数据并具有某种类型的内存空间。

初始值

  • 初始化:创建变量时赋予一个初始值,有别于赋值
  • 赋值 :把对象的当前值擦除,而以一个新值来代替

列表初始化

  • C++11新标准:用花括号来初始化变量

  • 如果使用列表初始化且初始值存在丢失信息的风险,则编译器报错

    int a=0;
    int a={0};
    int a{0};
    int a(0);

    long double ld=3.141592653;
    int a{ld}, b={ld}; //错误,转换未执行,因为存在丢失信息的危险
    int c(ld), d=ld; //正确:转换执行,且确实丢失了部分值

默认初始化

  • 定义变量时未指定初值,则是默认初始化,此时变量被赋予“默认值”。

  • 默认值由变量类型决定,同时定义变量的位置也会对此有影响

    • 内置类型的默认值由定义的位置决定,定义函数之外的变量被初始化为0

      定义于函数之内的变量不被初始化 ,如果试图拷贝或以其他形式访问此类值将引发错误。

    • 各个类各自决定其初始化对象的方式。绝大多数类无须显示初始化就提供默认值;一些类则要求显示初始化,否则引发错误。

分离式编译(sparate compilation)

  • 该机制允许将程序分割为若干个文件,每个文件可被独立编译。为支持该机制,C++将声明和定义区分开来

声明(declaration)

  • 使名字为程序所知
  • 规定了对象的类型和名字
  • 若只想声明而不定义,就使用关键字extern,且不要显式初始化。可以给extern显式初始化,一旦这样做,就成了定义。在函数体内部初始化extern标记的变量,将引发错误
  • 变量只能定义一次,但可以被多次声明
  • 定义必须出现且只能出现在一个文件中,其他用到改变量的文件必须对其声明但不可重复定义。

定义(definition)

  • 负责创建与名字相关联的实体

  • 申请存储空间,也可能赋予初值

    extern int i; //声明
    int j; //声明并定义
    extern double pi-3.1416 //定义

定义

由字母、数字、下划线组成,且必须以字母或下划线开头。长度没有限制,但大小写敏感。

C++为标准库保留了命

用户自定义标识符要求

  • 不能连续出现两个下划线
  • 不能以下划线紧接大写字母开头
  • 定义在函数体外的标识符不能以下划线开头

变量命名规范

一下规范能提高程序可读性

  • 标识符体现实际含义

  • 变量名一般用小写字母

  • 自定义类名一般大写字母开头

  • 表示符由多个单词组成时,单词间应有明显区分,如student_loan或studentLoan,不要使用studentloan

  • 作用域(scope)是程序的一部分,以花括号分隔

  • 同一个名字在不同作用域中可能指向不同实体,名字的有效区域始于声明语句,结束于声明语句所在作用域的末端。

  • 全局作用域(global scope)是定义在所有函数之外的名字的作用域,块作用域( block scope)是花括号内部的作用域。

嵌套的作用域

  • 作用域能彼此包含,包括外层作用域内层作用域

    • 内层作用域可以使用外层作用域声明的某个名字
    • 允许在内层作用域中重新定义外层作用域已有的名字。
  • 域操作符::手动指定作用域,左侧是作用域的名称,全局作用域的名称为空。


2.3 复合类型

  • 复合类型是指基于其他类型而定义的类型,包括引用指针等。

  • 声明:一条声明语句由一个基本数据类型和紧随其后的一个声名符列表组成,每个声名符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。

  • 我们使用术语引用时,通常是指左值引用(C++11中新增右值引用

  • 引用为对象起了另外一个名字。将声名符写为&d来定义变量d为引用。

  • 初始化变量和定义引用的区别:

    • 初始化变量时,初始值会被拷贝到新建的对象中
    • 定义引用时,引用和初始值绑定在一起,而不是拷贝初始值。
  • 一旦引用被绑定到一个初始值对象后,便无法令引用重新绑定到另外的对象,因此引用必须被初始化

    int ival = 1024;
    int &refVal = ival;
    int &refVal2; //报错:引用必须被初始化

引用即别名

  • 引用并非对象,他只是一个已存的对象的别名,所以不能定义引用的引用

  • 定义引用之后,对其进行的所有操作都是在与之绑定的对象上进行的

引用的定义

  • 允许一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头

  • 引用只能绑定在对象上,不能绑定到字面值或表达式的计算结果

  • 引用类型要与其被绑定的对象类型严格匹配

  • 指针是“指向”另一种类型的复合类型。

  • 指针与引用的区别:

    • 指针本身是对象,允许对指针赋值或拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象
    • 指针存放对象的地址,引用是对象的别名
    • 指针无需在定义时赋初值。
    • 块作用域内的指针如果没有初始化,其值将不确定
  • 定义指针类型的方法将声名符写成*d的形式,其中d时变量名。

获取对象的地址

  • 指针存放某个对象的地址,获取该地址需使用取地址符&

    int ival=42;
    int *p=&ival; //p存放ival的地址。或者说p时指向ival的指针

  • 指针的类型要和它所指向的对象严格匹配,除了两种例外情况

    double dval;
    double *pd=&dval; //正确,指向double型对象的指针
    double *pd2=pd; //正确,指向double型对象的指针

    int *pi=pd; //错误,指针pi和pd的类型不匹配
    pi=&dval; //错误,试图把double型对象的地址赋给int型指针

指针值

指针的值(地址)应属于下列4中状态之一:

  1. 指向一个对象
  2. 指向紧邻对象所占空间的下一个位置
  3. 空指针,指针没有指向任何对象
  4. 无效指针,即上述情况之外的其他值
  • 试图拷贝或以其他方式访问无效指针的值将引发错误
  • 访问无效指针后果无法预计,因此程序员必须清楚任意给定的指针是否有效
  • 2、3形式的指针虽然有效,但没有指向任何具体对象,所以对其访问的行为是不被允许的。

利用指针访问对象

  • 如果指针指向一个对象,则允许使用解引用符*来访问该对象

  • 对解引用的结果赋值,实际上也就是给指针所指的对象赋值

    int ival=42;
    int p=&ival; cout<<p;//输出42
    p=0; cout<<p;//输出0

空指针

  • 空指针不指向任何对象,使用指针前可以先检查它是否为空。

  • 生成空指针的方法

    //C++11引入
    int *p1=nullptr; //等价于int *p1=0;

    int *p2=0;

    //#include cstdlib
    //预处理变量NULL,由预处理器负责管理,不属于命名空间std,可直接使用,无需std::
    //预处理其会自动将预处理变量替换为实际值
    int *p3=NULL //等价于int *p3=0;

  • 尽量使用nullptr,尽量避免使用NULL

  • 不能把int变量直接赋给指针,即使int变量值刚好等于0也不行

  • 建议:初始化所有指针

    尽量在定义对象之后再定义指针。如果实在不清楚指针指向何处,就初始化为nullptr

赋值和指针

给指针赋值就是存放一个新的地址,从而指向一个新的对象

int i=42;
int *pi=0;   //pi初始化空指针
int *pi2=&i; //pi2初始化,指向i
int *pi3;    // pi3如果定义块内,值无法确定
pi3=pi2;     //pi3和pi2指向i
pi2=0;       //pi2为空指针

pi=&ival;    //pi指向ival
*pi=0;       //ival值改变,pi依然指向ival

其他指针操作

  • 空指针对应的条件值为false,非0指针对应的条件值为true
  • 两个指针地址相同,则指针相等。三种情况
    • 都为空指针
    • 都指向同一个对象
    • 一个指针指向某对象,另一个指针指向此对象的下一个地址,也可能两指针相等

void* 指针

  • void* 指针是一种特殊的指针类型,可存放任意类型对象的地址
  • void* 指针能做的事:
    • 与其他指针比较大小
    • 作为函数的输入输出传递
    • 赋值给另一个void *指针
  • 不能直接操作void*指针所指的对象,因为不知对象类型

同一条声明语句中,基本数据类型只有一个,但声名符形式可以不同,即可定义多种不同的复合类型

定义多个变量

int* p;  //合法但容易产生误导
int* p1,p2;  //p1是指向int的指针,p2是int
int *p1,*p2; //都是指向int的指针

指向指针的指针

  • 名符中的修饰符(&和*)的数量没有限制

  • 指针本身是对象,也有自己的地址,允许指针的地址再存到另一个的指针当中

    int ival=1024;
    int *pi=&ival; //pi指向int型的数
    int **ppi=&pi; //ppi指向int型的指针

指向指针的引用

int i=42;
int *p;     //指针可不用初始化
int *&r=p;  //绑定到指针p的引用。
            //r是一个引用,引用的对象是指针,该指针是int型,该引用r被绑定到对象p。
r=&i;       //给r赋值就是给指针p赋值
*r=0;       //解引用r就是解引用指针p

  • 关键字const使变量的值不能被改变
  • const对象创建后其值不能改变,所以必须初始化

初始化和const

  • 如果用一个对象初始化另一个对象,则它们是不是const都无所谓。右边即使是const也可拷贝其内容

默认状态下,const对象仅在文件内有效

  • 在编译期将const变量都替换为对应的值

  • 当多个文件出现了同名的const变量时,等同于在不同文件中分别定义了独立的变量。

  • 如果要在文件间共享const对象,就必须在声明和定义中都用extern,即将所有该对象都声明为文件外可见。

    //file_1.cc 定义并初始化一个常量,其他文件可访问
    extern const int bufSize=fcn();

    //file_1.h
    extern const int bufSize;//与.h中的bufSize是同一个

对常量的引用:将引用绑定到const对象上。对常量的引用不可修改其绑定的对象。

const int ci = 1024;
const int &r1=ci;  //正确,引用及其绑定对象都是常量
r1=42;             //错误r1是对常量的引用
int &r2=ci;        //错误,试图让非常量引用指向一个常量引用

初始化和对const的引用

  • 初始化普通引用,其类型必须严格匹配其所引用的对象类型。

  • 初始化常量引用时允许任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。

    补充:意味着可以绑定非常量的对象、字面值和一般表达式 详解)

  • 这两者差异的原因在于::类型不匹配时,编译器定义一个临时量用于类型转换。如果允许非常量引用的类型不匹配,就会产生临时量,修改引用时修改的是临时量而不是想要绑定的对象。常量引用不可修改,因此不存在这个问题。如下例:

    //手写的代码
    double dval=3.14;
    const int &ri=dval;

    //编译器转化的代码
    double dval=3.14;
    const int temp=dval; //定义与引用类型匹配的中间量,类型转换
    const int &ri=temp; //将常量引用绑定到中间量

  • 可将常量引用绑定到非常量对象。此时对象仍可修改,只是不能被这个引用修改。

    int i = 42;
    int &r1 = i;
    const int& r2 = i;
    std::cout << r2 << std::endl; //输出42
    r1 = 0;
    std::cout << r2 << std::endl; //输出0
    i = 10;
    std::cout << r2 << std::endl; //输出10

指向常量的指针

  • 不能用于改变其所指对象的值

  • 要想存放常量对象的地址,只能使用指向常量的指针

  • 没有规定其所指的对象必须时常量

    const double pi=3.14;
    double *ptr=π //错误,ptr是普通指针
    const double *cptr=π //正确,cptr可指向双精度常量
    cptr=42; //错误,不能给cptr赋值

    double dval=3.14;
    cptr=&dval; //正确,但不能通过cptr改变dval的值

const 指针 —— 常量指针

  • 指针本身是常量*const ,即存放的地址不可改,但其所指对象的值可以修改

  • 常量指针必须初始化

    //定义“常量指针”
    int errNumb=0;
    int *const curErr=&errNumb; //常量指针。curErr是const,是指针,指向int对象
    //定义“指向常量的指针”和“指向常量的常量指针”
    const double pi=3.14;
    const double *cptr=π //指向常量的指针。cptr是指针,指向const double对象
    const double *const pip=π //指向常量对象的常量指针。pip是const,是指针,指向const double对象

  • 顶层const

    • 顶层const表示指针本身是个常量
    • 顶层const可表示任意对象是常量,对任意数据类型(算术类型、类、指针)适用。
  • 底层const

    • 底层const表示指针所指的对象是个常量
    • 底层const与复合类型(指针、引用等)的基本类型部分有关。
      • 引用的const都是底层const。
      • 指针既可以是顶层const也可以是底层const,也可以同时是两种const
  • 拷贝时,顶层const没有影响。

  • 拷贝时,拷入拷出的对象必须有相同的底层const资格,或者必须能类型转换。一般来说非常量可以转成常量,反之不行

    int i=0;
    int *const p1=&i; //指针不可变,顶层const
    const int ci=42; //基本类型的const都是顶层const
    const int *p2=&ci; //指针可变,指向对象不可变,底层const
    const int *const p3=p2; //既是顶层const又是底层const
    const int &r=ci; //引用的const都是底层const

    //顶层const不受影响
    i=ci; //正确,ci是顶层const,可拷贝给顶层非const的i
    p2=p3; //正确,底层指向对象的类型严格匹配,可将顶层const的p3拷贝给顶层非const的p2
    //底层const的限制
    int *p=p3; //错误,p3是底层const,而p底层非const。不可将底层const拷贝给底层非const
    p2=p3; //正确,p2和p3底层指向对象的类型严格匹配,都是底层const。
    p2=&i; //正确,i是int,p2是底层const,可将底层非const拷贝给底层const(类型转换)。
    int &r=ci; //错误,普通int& 不能绑定到int常量上。
    const int &r2=i; //正确,const int&可以绑定到普通int上。

常量表达式

  • 指不会改变,且在编译期就能得到结果的表达式(两个条件缺一不可)。用常量表达式初始化的const对象也是常量表达式。

  • 一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定

    const int max_files=20; //是常量表达式
    const int limit =max_files+1;//是常量表达式
    int staff_size=27; //不是,因为类型不是const int
    const int sz =get_size(); //不是,因为get_size()运行时才确定

constexpr 变量

C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量是否是一个常量表达式

constexpr int mf=20;  //是常量表达式
constexpr int limit=mf+1;//是常量表达式
constexpr int sz=size();//只有当函数size()是constexpr函数时,才是初始化constexpr变量

字面值类型

  • 算术类型、引用和指针属于字面值类型

    • constexpr指针初始值必须是nullptr或0,或者是存储与某个固定地址中的对象。
    • 函数体类的变量并非存放在固定地址,因此constexpr不能指向这样的变量
    • 函数体外的对象其地址固定不变,能用来初始化consexpr指针
    • 允许函数定义一类有效范围超出函数本身的变量,这类变量也有固定地址,可以用constexpr引用绑定或constexpr指针指向
  • 自定义类Sales_item、IO库、string类型不属于字面值类型

指针和constexpr

  • constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关

  • constexpr指针既可以指向常量也可以指向非常量

    const int *p=nullptr; //p是一个指向整型常量的指针
    constexpr int *q=nullptr;//q是一个指向整数的常量指针


2.5 处理类型

  • 类型别名是一个名字,它是某种类型的同义词。类型别名和类型的名字等价

  • 定义类型别名:

    • 传统方法:关键字typedef
    • 新标准:别名声明,关键字using

    typedef double wages; //wages是double的同义词
    typedef wages base, *p; //base是double的同义词,p是double *的同义词

    using SI=Sales_item; //SI是Sales_item的同义词

指针、常量和类型别名

typedef char *pstring;  //pstring类型是指向char的指针
const pstring cstr=0;   //cstr是const pstring类型,即是常量指针,指向char,
                        //此处易误解成“const char *cstr=0”,即指向const char的指针
const pstring *ps;      //ps是指针,指向的对象是“指向char的常量指针”
  • 类型说明符auto:让编译器通过初始值来推算变量的类型。因此auto类型的变量必须初始化

  • auto可以在一条语句中声明多个变量,但所有变量的初始基本数据类型都必须一样。

    auto i=0, *p=&i; //正确,i是整型,p是指向整型的指针
    auto sz=0, pi=3.14; //错误,sz和pi类型不一样

复合类型、常量和auto

  • 编译器推出的auto有时候和初始值不完全一样,编译器会适当改变结果类型十七更符合初始化规则

    • 使用引用时,auto的类型是引用对象的类型
    • auto会忽略顶层const,保留底层const。
    • 引用类型设为auto,auto & ,原来初始化规则仍然保留
    • auto 前使用const:const auto

    int i=0, &r=i;
    auto a=r; //a是int型

    const int ci=i, &cr=ci;
    auto b=ci; //ci是顶层const,忽略。b是int型
    auto c=cr; //cr是ci的引用,忽略。ci是顶层const,忽略。c是int型
    auto d=&i; //i是int,取地址得到指向int的指针。d是指向int的指针
    auto e=&ci; //ci是const int,取地址得到指向const int的指针,是底层const不可忽略。e是指向const int的指针

    const auto f=ci; //f是const int
    auto &g=ci; //手动指定g的类型为引用。引用的对象是ci,即g是const int。
    auto &h=42; //错,h是非常量引用。不能为非常量引用绑定字面值,只能绑定对象
    const auto &j=42 //j是引用,绑定到const auto类型,即j是常量引用。可为常量引用绑定字面值

    auto k=ci, &l=i; //k是整型,l是整型引用
    auto &m=ci, *p=&ci; //m是整型常量的引用,p是指向整型常量的指针
    auto &n=i, *p2=&ci; //错误,i是int型,&c是const int型

  • decltype:返回操作数的类型。

    auto必须初始化,而decltype只需推导类型,不需初始化。C++11新标准引入

    const int ci=0, &cj=ci;
    decltype(ci) x=0; //x的类型是const int
    decltype(cj) y=x; //y的类型是const int&,y绑定到x
    decltype(cj) z; //错误,z是引用,必须初始化

decltype 和引用

  • 如果decltype使用的表达式是一个变量,则返回该变量的类型

  • 如果decltype使用的表达式不是一个变量,则返回表达式结果对应的类型

  • 如果decltype内表达式的内容可作为赋值语句的左值,则decltype得到引用类型。

  • 如果decltype内表达式的内容是解引用操作,则decltype得到引用类型。(解引用指针得到的是所指对象的引用,而不是值)

  • 如果decltype内表达式的内容是变量加上一层或多层括号,编译器把它当成一个表达式。变量是可作为左值的特殊表达式,故此时得到引用类型。

    int i=42, p=&i, &r=i; decltype(r) a; //错,r的推导结果是引用,必须初始化 decltype(r+0) b; //对,r+0的结果是int,故b是未初始化的int decltype(p) c; //错,解引用得到的结果是引用,c是绑定到int的引用,必须初始化

    //decltype内表达式的内容是变量加上一层或多层括号,结果是引用
    decltype((i)) d; //错,d是绑定到int的引用,必须初始化
    decltype(i) e; //对,e是未初始化的int

auto 和 decltype 的区别主要有三方面:

  1. auto 类型说明符用编译器计算变量的初始值来推断其类型,而 decltype 虽然也让编译器分析表达式并得到它的类型,但是不实际计算表达式的值。
  2. 编译器推断出来的 auto 类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。例如,auto 一般会忽略掉顶层 const,而把底层 const 保留下来。与之相反,decltype 会保留变量的顶层 const。
  3. 与 auto 不同,decltype 的结果类型与表达式密切相关,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。如果 decltype 使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,则编译器将推断得到引用类型。

2.6 自定义数据结构

  • 类以关键字struct开始,紧跟类名类体(类体可为空)

  • 类内部定义的名字必须唯一

  • 类体后面可以紧跟变量名,故结束的花括号后必须写一个fen'ha

    struct Sales_data{
    std::string bookNo;
    unsigned units_sold=0;
    double revenue=0.0;
    };

    struct Sales_data{/……/} accum,trans,*salesptr;

    struct Sales_data{/……/};
    Sales_data accum,trans,*salesptr;

类数据成员

  • 每个对象有自己的一份数据成员拷贝,修改一个对象的数据成员不影响其他对象。
  • 可为数据成员提供类内初始值。创建对象时,类内初始值用于初始化数据成员,未初始化的成员执行默认初始化。
  • 类内初始值进行数据成员的初始化时,只能用等号或花括号,不能用圆括号

预处理功能:头文件保护符

防止重复包含的发生

#ifndef SALES_DATA_H
#define SALES_DATA_H
#endif