C++ chapter2 变量和基本类型


基本内置类型

C++定义了一组表示整数、浮点数、单个字符和布尔值的算术类型(arithmetic type),另外还定义了一种称为void的特殊类型。void类型没有对应的值,仅用于在有限的一些情况下,通常用作无返回值函数的返回类型。

类型 含义 最小存储空间
bool 布尔型
char 字符串型 8位
wchar_t 宽字符型 16位
short 短整型 16位
int 整型 16位
long 长整型 32位
float 单精度浮点数 6位有效数字
double 双精度浮点数 10位有效数字
long double 扩展精度浮点数 10位有效数字

因为位数的不同,这些类型所能表示的最大(最小)值也因机器的不同而有所不同。

整型

表示整数,字符和布尔值的算术类型合称为整型(integral type)

字符类型有两种:charwchar_tchar类型保证了有足够的空间,能够存储机器基本字符集中任何字符的相应的数值,因此char类型通常是单个机器字节(byte)。wchar_t类型用于扩展字符集,比如汉字和日语。

bool类型表示真值truefalse。可以将算术类型的任何值赋给bool对象。0值算术类型代表false,任何非0的值都代表true

1.带符号和无符号类型

bool类型外,整型可以是带符号的(signed) ,也可以是无符号的(unsigned) 。顾名思义,带符号类型可以表示整数也可以表示负数(包括0),而无符号类型只能表示大于或等于0的数。

2.整型值的表示

无符号型中,所以的位都表示数值。如果在某种机器中,定义一种类型使用8位表示,那么这种类型的unsigned型可以取值0到255。

C++标准并未定义signed类型如何用位来表示,而是由每个编译器自由决定如何表示signed类型。这些表示方式会影响signed类型的取值范围。8位signed整型取值是从-128到127.

3.整型的赋值
对象的类型决定对象的取值。

当将超过取值范围的值赋给signed类型时,由编译器决定实际赋的值。在实际操作中,很多的编译器处理signed的方式和unsigned类型类似。也就是说,赋值时是取该值对该类型取值数目求模后的值。

注意:C++中,把负值赋给unsigned对象是完全合法的,其结果是该负数对该类型的取值个数求模后的值。所以,如果把-1赋给8位的unsigned char,那么结果是255,因为255是-1对256求模后的值。

浮点型

类型floatdoublelong double分别表示单精度浮点数、双精度浮点数和扩展精度浮点数。一般float类型用一个字(32位)表示,double类型用两个字(64位)来表示,long double类型用三个或四个字(96或128位)来表示。类型的取值范围决定了浮点数所含的有效数字位数。

注意:对于实际的程序来说,float类型精度通常是不够的——float型只能保证6位有效数字,而double型可以保证10位有效数字,能满足大多数计算的需要。

字面值常量

像 42 这样的值,在程序中被当作字面值常量。称之为字面值是因为只能用它的值称呼它,称之为常量是因为它的值不能修改。每个字面值都有相应的类型。只有内置类型存在字面值,没有类类型的字面值。因此,也没有任何标准库类型的字面值。

1.整型字面值规则

定义字面值整数常量可以使用以下三种进制中的任一种:十进制、八进制和十六进制。当然这些进制不会改变其二进制位的表示形式。

1
2
3
20
024
0x14

字面值整数常量的类型默认为intlong类型。其精度类型决定于字面值——其值适合int就是int类型,比int大的值就是long类型。通过增加后缀,能够强制将字面值整数常量转换为longunsignedunsigned long类型。通过在数值后面加L或者l指定常量为long类型。

提示:定义长整型时,应该使用大写字母L。小写字母l很容易和数值1混淆。

类似地,可通过在数值后面加Uu定义unsigned类型。同时加LU就能够得到unsigned long类型的字面值常量。但其后缀不能有空格:

1
2
128u      1024UL
1L 8Lu

没有short类型的字面值常量。

2.浮点字面值规则

通常可以使用十进制或者科学计数法来表示浮点字面值常量。使用科学计数法时,指数用E或者e表示。默认的浮点字面值常量为double类型。在数值的后面加上Ff表示单精度。同样加上L或者l表示扩展精度。

1
2
3.14159F          .001f        12.345L        0.
3.14159E0f 1E-3F 1.2345E1L 0e0

3.布尔字面值和字符字面值
单词truefalse是布尔型的字面值:

1
bool test=false

可打印的字符型字面值通常用一对单引号来定义:

1
'a'    '2'    ','         ''

这些字面值都是char类型的,在字符字面值前加L就能够得到wchar_t类型的宽字符字面值。

1
L'a'

4.非打印字符的转义序列
有些字符是不可打印的。不可打印字符实际上是不可显示的字符,比如推个或者控制符。还有一些在语言中有特殊意义的字符,例如单引号、双引号和反斜杠符号。不可打印字符和特殊字符都用转义字符书写。转义字符都以反斜线符号开始,C++中定义了如下转义字符:

换行符 \n 水平制表符 \t
纵向制表符 \v 退格符 \b
回车符 \r 进纸符 \f
响铃符 \a 反斜线 \\
疑问号 \? 单引号 \'
双引号\"

我们可以将任何字符表示为以下形式的同样转义字符:

1
\ooo

这里ooo表示三个八进制数字,这三个数字表示字符的数字值。

下面是用ASCII码字符集表示字面值常量:

1
2
\7(响铃符) \12(换行符) \40(空格符)
\0(空字符) \062'2') \115'M'

字符\0通常表示“空字符”。
同样也可以使用十六进制转义字符来定义字符:

1
\xddd

它由一个反斜线符、一个x和一个或者多个十六进制数字组成。

5.字符串字面值
之前见过的所有字面值都有基本内置类型。还有一种字面值(字符串字面值)更加复杂。字符串字面值是一串常量字符。

字符串字面值常量用双引号括起来的零个或者多个字符表示。不可打印字符表示成相应的转义字符。

1
“hello world\n”

为了兼容C语言,C++中所有的字符串字面值都由编译器自动在末尾加一个空字符。

字符字面值表示单个字符A,

1
'A'

然而

1
"A"

表示包含字符A和空字符两个字符的字符串。

也存在宽字符串字面值,一样在前面加“L”,如

1
L"hello world"

宽字符串字面值是一串常量宽字符,同样以一个宽空字符结束。

6.字符串字面值的连接

两个相邻的仅由空格、制表符或换行符分开的字符串字面值(或宽字符串字面值),可连接成一个新字符串字面值。这使得多行书写字符串字面值变得简单:

1
2
3
std::cout<<"hello"
"world"
<<std::endl;

如果连接字符串字面值和宽字符串字面值,其结果是未定义的,也就是说,连接不同类型的行为标准没有定义。这个程序可能会执行,也可能会崩溃或者产生没有用的值,而且在不同的编译器下程序的动作可能不同。

7.多行字面值

处理长字符串有一个更基本的(但不常使用)方法,这个方法依赖于很少使用的程序格式化特性:在一行的末尾加一反斜线符号可将此行和下一行当作同一行处理。

1
2
3
std::cou\
t<<"Hi"<<st\
d::endl;

等价于

1
std::cout<<"Hi"<<std::endl;

注意反斜线符号必须是该行的尾字符——不允许其后面有注释或空格。同样,后继行行首的任何空格和制表符都是字符串字面值的一部分。正因如此,长字符串字面值的后继行才不会有正常的缩进。

变量

什么是变量

变量提供了程序可以操作的有名字的存储区。C++中的每一个变量都有特定的类型,该类型决定了变量的内存大小和布局、能够存储与该内存中的值的取值范围以及可应用在该变量上的操作集。C++程序员常常把变量称为 “变量” 或 “对象”。

左值和右值

  1. 左值(lvalue):左值可以出现在赋值语句的左边或右边。
  2. 右值(rvalue):右值只能出现在赋值的右边,不能出现在赋值语句的左边。

变量是左值,因此可以出现在赋值语句的左边。数字字面值是右值,因此不能被赋值。给定以下变量:

1
2
int units_sold=0;
double sales_price=0

有些操作符,比如赋值,要求其中的一个操作数必须是左值。结果,可以使用左值的上下文比右值更广。左值出现的上下文决定了决定了左值是如何使用的。

1
units_sold=uints_sold+1;

uints_sold变量被用作两种不同操作符的操作数。+操作符仅关心其操作数的值。变量的值是当前存储在和该变量关联的内存中的值。加法操作符的作用是取得变量的值并加1。

变量units_sold也被用作=操作符的左操作数。=操作符读取右操作数并写到左操作数。

变量名

变量名,即变量的标识符(identifier),可以由字母、数字和下划线组成。变量名必须以字母或下划线开头,并且区分大小写字母:C++中的标识符都是大小写敏感的。下面定义了4个不同的标识符:

1
int somename,someName,SomeName,SOMENAME;

1.C++关键字

C++关键字
asm do if return try
auto double inline short typedef
bool dynamic_cast int signed typeid
break else long sizeof typename
case enum mutable static union
catch explicit namespace static_cast unsigned
char export new struct using
class extern operator switch virtual
const false private template void
continue for public throw wchar_t
default friend register true while
delete goto reinterpret_cast

C++还保留了一些词用作各种操作符的替代名。这些替代名用于支持某些不支持标准C++操作符符合集的字符集。它们也不能用作标识符。

C++ 操作符 替代名
and bitand compl not_eq or_eq xor_eq
and_eq bitor not or xor

除了关键字,C++标准还保留了一组标识符用于标准库。标识符不能包含两个连续的下划线,也不能以下划线开头后面紧跟一个大写字母。有些标识符(在函数外定义的标识符)不能以下划线开头。

2.变量命名习惯

变量命名有许多被普遍接受的习惯,遵循这些习惯可以提供程序的可读性。

  • 变量名一般用小写字母。
  • 标识符应使用能帮助记忆的名字。
  • 包含多个词的标识符书写为在每个词之间添加一个下划线,或者每个内嵌的词的第一个字母都大写。

注意:命名习惯最重要的是保持一致。

定义对象

下列语句定义了5个变量:

1
2
3
4
int units_sold;
double sales_price,avg_prive;
std::string title;
Sales_item curr;

每个定义都是以类型说明符(type specifier) 开始,后面紧跟着以逗号分开的含有一个或多个说明符的列表。分号结束定义。类型说明符指定与对象相关联的类型:int、double、std::string和Sales_item都是类型名。其中int和double是内置类型,std::string是标准库定义的类型。

类型决定了分配给变量的存储空间的大写和可以在其上执行的操作。

多个变量可以定义在同一条语句中:

1
2
3
4
double salary,wage;
int month,
day,year;
std::string address;

1.初始化

变量定义指定了变量的类型和标识符,也可以为对象提供初始值。定义是指定了初始值的对象被称为已初始化的(initialized)。C++支持两种初始化变量的形式:复制初始化(copy-initialization)直接初始化(direct-initialization)。复制初始化语法用等号(=),直接初始化则是把初始化式放在括号中:

1
2
int ival(1024);  //直接初始化
int ival=1024; //复制初始化

使用=来初始化变量使得许多C++编程新手感到迷惑,他们很容易把初始化当成是赋值的一种形式。但是在C++中初始化和赋值是两种不同的操作

2.使用多个初始化式

初始化内置类型的对象只有一种方法:提供一个值,并且把这个值复制到新定义的对象中。对内置类型来说,复制初始化和直接初始化几乎没有差别。

对类类型的对象来说,有些初始化仅能用直接初始化完成。要想理解其中缘由,需要初步了解类是如何控制初始化的。

每个类都可能会定义一个或几个特殊的成员函数来告诉我们如何初始化类类型的变量。定义如何进行初始化的成员函数称为构造函数(constructor)。和其他函数一样,构造函数能接受多个参数。一个类可以定义几个构造函数,每个构造函数必须接受不同数目或者不同类型的参数。

我们以string类为例。string类型在标准库中定义,用于存储不同长度的字符串。使用string时必须包含string头文件。和IO类型一样,string定义在std命名空间中。

string类定义了几个构造函数,使得我们可以用不同的方式初始化string对象。其中一种初始化string对象的方式是作为字符串字面值的副本:

1
2
3
#include<string>
std::string titleA="C++ Primer";
std::string titleB("C++ Primer");

本例中,两种初始化方式都可以使用。两种定义都创建了一个string对象,其初始值都是指定的字符串字面值的副本。

也可以通过一个计数器和一个字符初始化string对象。这样创建的对象包含重复多次的指定字符,重复次数由计数器指定。

1
std::string all_nines(10,'9');

本例中,初始化 all_nines 的唯一方法是直接初始化。有多个初始化式时不能使用复制初始化。

3.初始化多个变量

当一个定义中定义了两个以上变量的时候,每个变量都可能有自己的初始化式。对象的名字立即变成可见,所以可以用同一个定义中前面已定义变量的值初始化后面的变量。已初始化变量和未初始化变量可以在同一个定义中定义。两种形式的初始化文法可以相互混用。

1
2
3
4
5
6
7
#include <string>
double salary=999.99,
wage(salary+0.01);
int interval,
month=9,day=7,year=1955;
std::string title("C++ Primer"),
publisher="A-W";

对象可以用任意复杂的表达式(包含函数的返回值)来初始化:

1
double price=109.99,discount=0.16;

变量初始化规则

当定义没有初始化式的变量时,系统有时候会帮我们初始化变量。这时,系统提供什么样的值取决于变量的类型,也取决于变量定义的位置。

1.内置类型变量的初始化

内置类型变量是否自动初始化取决于变量定义的位置。在函数体外定义的变量都初始化成0,在函数体里定义的内置类型变量不进行自动初始化。除了用作赋值操作符的左操作数,未初始化变量用作任何其他用途都是没有定义的。未初始化变量引起的错误难以发现。

注意:建议每个内置类型的对象都要初始化。

2.类类型变量的初始化

每个类都定义了该类型的对象可以怎样初始化。类通过定义一个或多个构造函数来控制类对象的初始化。

如果定义某个类的变量时没有提供初始化式,这个类也可以定义初始化时的操作。它是通过定义一个特殊的构造函数即默认构造函数(default constructor)来实现的。这个构造函数之所以被称作默认构造函数,是因为它是 “默认” 运行的。如果没有提供初始化式,那么就会使用默认构造函数。不管变量在哪里定义,默认构造函数都会被使用。

大多数类都提供了默认构造函数。如果类具有默认构造函数,那么就可以在定义该类的变量时不用显示地初始化变量。例如,string类定义了默认构造函数来初始化string变量为空字符串,既没有字符的字符串:

1
std::string empty;

有些类类型没有默认构造函数。对于这些类型来说,每个定义都必须提供显示的初始化式。没有初始值是根本不可能定义这种类型的变量的。

声明和定义

正如前面所看到的那样,C++程序通常由许多文件组成。为了让多个文件访问相同的变量,C++区分了声明和定义。

变量的定义(definition) 用于为变量分配存储空间,还可以为变量指定初始值。在一个程序中,变量有且仅有一个定义。

声明(declaration) 用于向程序表明变量的类型和名字。定义也是声明:当定义变量时我们声明了它的类型和名字。可以通过使用extern关键字声明变量名而不定义它。不定义变量的声明包括对象名、对象类型和对象类型前的关键字extern

1
2
extern int i;  //声明
int i; //定义

extern声明不是定义,也不分配存储空间。事实上,它只是说明变量定义在程序的其他地方。程序中变量可以声明多次,但只能定义一次。

只有当声明也是定义时,声明才可以有初始化式,因为只有定义才分配存储空间。初始化式必须要有存储空间来进行初始化。如果声明有初始化式,那么它可被当作是定义,即使声明标记为extern

1
extern double pi=3.1416;

虽然使用了extern,但是这条语句还是定义了pi,分配并初始化了存储空间。只有当extern声明位于函数外部时,才可以含有初始化式。

因为已初始化的extern声明被当作是定义,所以该变量任何随后的定义都是错误的。

同样,随后的含有初始化式的extern声明也是错误的。

在C++语言中,变量必须且仅能定义一次,而且在使用变量之前必须定义或声明变量。

任何在多个文件中使用的变量都需要有与定义分离的声明。在这种情况下,一个文件含有变量的定义,使用该变量的其他文件则包含该变量的声明(而不是定义)。

名字的作用域

C++程序中,每个名字都与唯一的实体(比如变量、函数和类型等)相关联。尽管有这样的要求,还是可以在程序中多次使用同一个名字,只要它用在不同的上下文中,且通过这些上下文可以区分该名字的不同意义。用来区分名字的不同意义的上下文称为作用域(scope)。作用域是程序的一段区域。一个名称可以和不同作用域中的不同实体相关联。

C++语言中,大多数作用域是用花括号来界定的。一般来说,名字从其声明点开始直到其声明所在的作用域结束处都是可见的。

1
2
3
4
5
6
7
8
9
#include <iostream>
int main(){
int sum=0;
for(int val=1;val<=10;val++){
sum+=val;
std::cout<<"hello"<<endl;
return 0;
}
}

这个程序定义了三个名字,使用了两个标准库的名字。程序定义了一个名为main的函数,以及两个名为sumval的变量。名字main定义在所有花括号之外,在整个程序都可见。定义在所有函数外部的名字具有全局作用域(global scope) ,可以在程序中的任何地方访问。名字sum定义在main函数的作用域中,在整个main函数中都可以访问,但在main函数外则不能。变量sum局部作用域(local scope) 。名字val更有意思,它定义在for语句的作用域中,只能在for语句中使用,而不能用在main函数的其他地方。它具有语句作用域(statement scope)

C++中作用域可嵌套

定义在全局作用域中的名字可以在局部作用域中使用,定义在全局作用域中的名字和定义在函数的局部作用域中的名字可以在语句作用域中使用,等等。名字还可以在内部作用域中重新定义。理解和名字相关联的实体需要明白定义名字的作用域:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <string>
std::string s1="hello";
int main(){
std::string s2="world";
std::cout<<s1<<" "<<s2<<std::endl;
int s1=42;
std::cout<<s1<<" "<<s2<<std::endl;
return 0;
}

这个程序中定义了三个变量:string类型的全局变量s1、string类型的局部变量s2int类型的局部变量s1。局部变量s1的定义屏蔽(hide)了全局变量s1。

变量从声明开始才可见,因此执行第一次输出时局部变量s1不可见,输出表达式中的s1是全局变量s1,输出 “hello world”。第二条输出语句跟在s1的局部定义后,现在局部变量s1在作用域中。第二条输出语句使用的是局部变量s1而不是全局变量s1,输出“42 world”。

注意:在函数内定义一个函数可能会用到的全局变量同名的局部变量总是不好的。局部变量最好使用不同的名字。

在变量使用处定义变量

一般来说,变量的定义或声明可以放在程序中能摆放语句的任何位置。变量在使用前必须先声明或定义。

在对象第一次被使用的地方定义对象可以提高程序的可读性。读者不需要返回到代码段的开始位置去寻找某一特殊变量的定义,而且,在此处定义变量,更容易给它赋以有意义的初始值。

放置声明的一个约束是,变量只在从其定义处开始到该声明所在的作用域的结束处才可以访问。必须在使用该变量的最外层作用域里面或之前定义变量。

const限定符

1.定义const对象

定义一个变量代表某一常数的方法仍然有一个严重的问题。即变量是可以被修改的。

变量可能被有意或无意地修改。const限定符提供了一个解决办法,它把一个对象转换成一个常量。

1
const int bufsize=512;

利用const关键字定义一个常量,常量是不可修改的,任何修改常量的尝试都会报错。

因为常量在定义后就不能被修改,所以定义时必须初始化。

2.const对象默认为文件的局部变量

在全局作用域力定义非const变量时,它在整个程序中都可以访问。我们可以把一个非const变量定义在一个文件中,假设已经做了合适的声明,就可在另外的文件中使用这个变量:

1
2
3
4
5
//file1.cc
int counter;
//file2.cc
extern int counter;
counter++;

与其他变量不同,除非特别说明,在全局作用域声明的const变量是定义该对象的文件的局部变量。此变量只存在于那个文件中,不能被其他文件访问。

通过指定const变量为extern就可以在整个程序中访问const对象:

1
2
3
4
//file1.cc
extern const int buf=fcn();
//file2.cc
extern const int buf;

注解:非const变量默认为extern。要使const变量能够在其他的文件中访问,必须显式地指定它为extern

引用

引用(reference) 就是对象的另一个名字。在实际程序中,引用主要作函数的形式参数。

引用是一种复合类型(compound type),通过在变量名前添加 “&” 符号来定义。符号类型是指用其他类型定义的类型。在引用的情况下,每一种引用类型都 “关联到” 某一其他类型。不能定义引用类型的引用,但可以定义任何其他类型的引用。

引用必须用与该引用同类型的对象初始化:

1
2
int ival=1024;
int &refVal=ival;

1.引用是别名

因为引用只是它绑定的对象的另一名字,作用在引用上的所有操作事实上都是作用在该引用绑定的对象上:

1
refVal+=2;

注意:当引用初始化后,只要该引用存在,它就保持绑定到初始化时指向的对象。不可能将引用绑定到另一个对象。

要理解的重要概念是引用只是对象的另一名字。事实上,我们可以通过ival的原名访问ival,也可以通过它的别名refVal访问。赋值只是另外一种操作。

初始化是指明引用指向哪个对象的唯一方法。

2.定义多个引用
可以在一个类型定义行中定义多个引用。必须在每个引用标识符前添加 “&” 符号:

1
2
3
4
int i=1024,i2=2048;
int &r=i,r2=i2;
it i3=1024,&ri=i3;
int &r3=i3,&r4=i2;

3.const引用

const引用是指向const对象的引用:

1
2
3
const int ival=1024;
const int &refVal=ival;
int &ref2=ival; //error

可以读取但不能修改refVal,因此,任何对refVal的赋值都是不合法的。这个限制有其意义:

不能直接对ival赋值,因此不能通过使用refVal来修改ival。

同理,用ival初始化ref2也是不合法的:ref2是普通的非const引用(nonconst reference),因此可以用来修改ref2指向的对象的值。

const引用可以初始化为不同类型的对象或者初始化为右值,如字面值常量:

1
const int &r=42;

const引用只能绑定到与该引用同类型的对象。
const引用则可以绑定到不同但相关的类型的对象或绑定到右值。

typedef名字

typedef可以用来定义类型的同义词:

1
2
typedef double wages;
typedef int exam_score;

typedef定义以关键字typedef开始,后面是数据类型和标识符。标识符或类型名并没有引入新的类型,而只是现有数据类型的同义词。typedef名字可出现在程序中类型名可出现的任何位置。

typedef通常被用于以下三种目的:

  • 为了隐藏特定类型的实现,强调使用类型的目的。
  • 简化复杂的类型定义,使其更易理解。
  • 允许一种类型用于多个目的,同时使得每次使用该类型的目的明确。

枚举

我们需要为某些属性定义一组可选择的值。例如,文件打开的状态可能会有三种:输入、输出和追加。记录这些状态值的一种方法是每种状态都与一个唯一的常数值相关联。

1.定义和初始化枚举
枚举的定义包括关键字enum,其后是一个可选的枚举类型名,和一个花括号括起来、用逗号分开的枚举成员(enumerator) 列表。

1
enum open_modes{input,output,append};

默认地,第一个枚举成员赋值为0,后面的每个枚举成员赋的值比前面的大1。

2.枚举成员是常量

可以为一个或多个枚举成员提供初始值,用来初始化枚举成员的值必须是一个常量表达式(constant expression) 常量表达式是编译器在编译时就能够计算出结果的整型表达式。整型字面值常量是常量表达式,正如一个通过常量表达式自我初始化的const对象也是常量表达式一样。

1
enum Forms{shape=1,sphere,cylinder,polygon};

在枚举类型Forms中,显示将shape赋值为1。其他枚举成员隐式初始化:sphere初始化为2,cylinder初始化为3,polygon初始化为4.

枚举成员值可以是不唯一的。

1
2
enum Points{ point2d=2,point2w,
point3d=3,pint3w};

本例中,枚举成员pint2d显示初始化为2.下一个枚举成员point2w默认初始化,即它的值比前一枚举成员的值大1,因此point2w吹时候为3。枚举成员pint3d显示初始化为3。一样,point3w默认初始化,结果为4。

不能改变枚举成员的值。枚举成员本书就是一个常量表达式,所以也可用于需要常量表达式的任何地方。

3.每个enum都定义一种唯一的类型

每个enum都定义了一种新的类型。和其他类型一样,可以定义和初始化Points类型的对象,也可以以不同的方式使用这些对象。枚举类型的对象的初始化或赋值,只能通过其枚举成员或同一枚举类型的其他对象来进行。

1
2
3
4
Points pt3d=point3d;
Points pt2w=3; //error
pt2w=polygon; //error
pt2w=pt3d;

注意把3赋给Points对象是非法的,即使3与一个Points枚举成员相关联。

类类型

C++中,通过定义类(class)来自定义数据类型。类定义了该类型的对象包含的数据和该类型的对象可以指向的操作。标准库类型stringistreamostream都定义成类。

1.从操作开始设计类

每个类都定义了一个接口(interface) 和一个实现(implementation)。接口由使用该类的代码需要执行的操作完成。实现一般包括该类所需要的数据。实现还包括定义该类需要的但又不供一般性使用的函数。

定义类时,通常先定义该类的接口,即该类所提供的操作。通过这些操作,可以决定该类完成其功能所需要的数据,以及是否需要定义一些函数来支持该类的实现。

2.定义Sales_item类

定义类:

1
2
3
4
5
6
7
class Sales_item{
public:
private:
std::string isbn;
unsigned units_sold;
double revenue;
};

类定义以关键字class开始,其后是该类的名字标识符。类体位于花括号里面。花括号后面必须要跟一个分号。

类体可以为空。类体定义了组成该类型的数据和操作。这些操作和数据是类的一部分,也称为类的成员(member)。操作称为成员函数,而数据则称为数据成员(data member)

类也可以包含0个到多个privatepublic访问标号(access label)。访问标号控制类的成员在类外部是否可访问。使用该类的代码可能只能访问public成员。

定义了类,也就定义了一种新的类型。类名就是该类型的名字。通过命名Sales_item类,表示Sales_item是一种新的类型,而且程序也可以定义该类型的变量。

每一个类都定义了它自己的作用域。也就是说,数据和操作的名字在类的内部必须唯一,但可以重用定义在类外的名字。

3.类的数据成员

定义类的数据成员和定义普通变量有些相似。我们同样是指定一种类型并给该成员一个名字。

定义变量和定义数据成员存在非常重要的区别:一般不能把类成员的初始化作为其定义的一部分。当定义数据成员时,只能指定该数据成员的名字和类型。类不是在类定义里定义数据成员时初始化数据成员,而是通过称为构造函数的特殊成员函数控制初始化。

4.访问标号

访问标号负责控制使用该类的代码是否可以使用给定的成员。类的成员函数可以使用类的任何成员,而不管其访问级别。访问标号publicprivate可以多次出现在类定义中。给定的访问标号应用到下一个访问标号出现时为止。

类中public部分定义的成员在程序的任何部分都可以访问。一般把操作放在public部分,这样程序的任何代码都可以执行这些操作。

不是类的组成部分的代码不能访问private成员。通过设定Sales_item的数据成员为private,可以保证对Sales_item对象进行操作的代码不能直接操纵器数据成员。

5.使用struct关键字

C++支持另一个关键字struct,它也可以定义类类型。struct关键字是从C语言中继承过来的。

如果使用class关键字来定义类,那么定义在第一个访问标号前的任何成员都隐式指定为private;如果使用struct关键字,那么这些成员都是public。使用class还是struct关键字来定义类,仅仅影响默认的初始访问级别。

1
2
3
4
5
6
struct Sales_item{
private:
std::string isbn;
unsigned units_sold;
double revenue;
};

classstruct关键字定义类的唯一差别在于默认访问级别:默认情况下,struct的成员为public,而class的成员为private

编写自己的头文件

一般类定义都会放入头文件(header file)

事实上,C++使用头文件包含的不仅仅是类定义。

由多个文件组成的程序需要一种方法连接名字的使用和声明,在C++中这时通过头文件实现的。

为了允许把程序分成独立的逻辑块,C++支持所谓的分别编译(separate compilation)。这样程序可以由多个文件组成。为了支持分别编译,我们的类的定义放在一个头文件里面。我们将定义的成员函数放在单独的源文件中。任何使用类的源文件都必须包含类的头文件。

设计自己的头文件

头文件为相关声明提供了一个集中存放的位置。头文件一般包含类的定义、extern变量的声明和函数的声明。使用或定义这些实体的文件要包含适当的头文件。

头文件的正确使用能够带来两个好处:保证所有文件使用给定实体的同一声明;当声明需要修改时,只有头文件需要更新。

设计头文件还需要注意以下几点:头文件中所做的声明在逻辑上应该是适于放在一起的。编译头文件需要一定的时间。如果头文件太大,程序员可能不愿意承受包含该头文件所带来的编译时代价。

为了减少处理头文件的编译时间,有些C++的实现支持预编译头文件。

1.头文件用于声明而不是用于定义

当设计头文件时,记住定义和声明的区别是很重要的。定义只可以出现一次,而声明则可以出现多次。

下列语句是一些定义,所以不应该放在头文件里:

1
2
extern int ival=10;
double fica_rate;

注意:因为头文件包含在多个源文件中,所以不应该含有变量或函数的定义。

对于头文件不应该含有定义这一规则,有三个例外。头文件可以定义类、值在编译时就已知道的const对象和inline函数。这些实体可以在多个源文件中定义,只要每个源文件中的定义是相同的。

在头文件中定义这些实体,是因为编译器需要它们的定义(不只是声明)来产生代码。为了产生能定义或使用类的对象的代码,编译器需要指定组成该类型的数据成员。同一还需要知道能够在这些对象上执行的操作。类定义提供所需要的信息。在头文件中定义const对象则需要更多的解释。

2.一些const对象定义在头文件中

const变量默认时是定义该变量的文件的局部变量。正如我们现在所看到的,这样设置默认情况的原因在于允许const变量定义在头文件中。

预处理器的简单介绍

#include设施是C++预处理器(preprocessor) 的一部分。预处理器处理程序的源代码,在编译器之前运行。C++继承了C的非常精细的预处理器。现在的C++程序以高度受限的方式使用预处理器。

#include指示只接受一个参数:头文件名。预处理器用指定的头文件的内容替代每个#include。我们自己的头文件存储在文件中。系统的头文件可能用特定于编译器的更高效的格式保存。无论头文件以何种形式保存,一般都含有支持分别编译所需的类定义及变量和函数的声明。

1.头文件经常需要其他头文件

头文件经常#include其他头文件。头文件定义的实体经常使用其他头文件的设施。

包含其他头文件是如此司空见惯,甚至一个头文件被多次包含进同一源文件也不稀奇。

设计头文件时,应使其可以多次包含在同一源文件中,这一点很重要。我们必须保证多次包含同一头文件不会引起该头文件定义的类和对象被多次定义。使得头文件安全的通用做法,是使用预处理器定义头文件保护符(header guard)。头文件保护符用于避免在已经见到头文件的情况下重新处理该头文件的内容。

2.避免多重包含

在编写头文件之前,我们需要引入一些额外的预处理器设施。预处理器运行我们自定义变量。

注意:预处理器变量的名字在程序中必须是唯一的。任何与预处理器变量相匹配的名字的使用都关联到该预处理器变量。

为了避免名字冲突,预处理器变量经常用全大写字母表示。

预处理器变量有两种状态:已定义或未定义。定义预处理器变量和检测其状态所用的预处理器指示不同。#define指示接受一个名字并定义该名字为预处理器变量。#ifndef指示检测指定的预处理器变量是否未定义。如果预处理器变量未定义,那么跟在其后的所有指示都被处理,直到出现#endif

可以使用这些设施来预防多次包含同一头文件:

1
2
3
#ifndef SALESITE_H
#define SALESITEM_H
#endif

为了保证头文件在给定的源文件中只处理过一次,我们首先检测#ifndef。第一次处理头文件时,测试会成功,因为SALESITEM_H还未定义。下一条语句定义了SALESITEM_H。那样的话,如果我们编译的文件恰好又一次包含了该头文件。#ifndef指示会发现SALESITEM_H已经定义,并且忽略该头文件的剩余部分。

头文件应该含有保护符,即使这些头文件不会被其他头文件包含。编写头文件保护符并不困难,而且如果头文件被包含多次,它可以避免难以理解的编译错误。

当没有两个头文件定义和使用同名的预处理器变量时,这个策略相当有效。我们可以用定义在头文件里的实体(如类)来命名预处理器变量来编码预处理器变量重名的问题。一个程序只能含有一个名为Sales_item的类。通过使用类名来组成头文件和预处理器变量的名字,可以使得很可能只有一个文件将会使用该预处理器变量。

3.使用自定义的头文件

如果头文件名括在尖括号(<>)里,那么认为该头文件是标准头文件。编译器将会在预定义的位置查找该头文件,这些预定义的位置可以通过查找路径环境变量或者通过命令行选项来修改。

如果头文件名括在一对引号里,那么认为它是非系统头文件,非系统头文件的查找通常开始于源文件所在的路径。