# Chapter 2 Variables and Basic Types
# Primitive Built-in Types
C++ defines a set of primitive types that include the arithmetic types and a special type named void. The arithmetic types represent characters, integers, boolean values, and floating-point numbers. The void type has no associated values and can be used in only a few circumstances, most commonly as the return type for functions that do not return a value.
任何常用的编程语言都具备一组公共的语法特征,最基本的特征包括:
- 整型、字符型等内置类型
- 变量,用来为对象命名
- 表达式和语句,用于操作上述数据类型的具体值
- if 或 while 等控制结构,有选择地执行一些语句或重复地执行一些语句
- 函数,用于定义可供随时调用的计算单元
大多数编程语言通过两种方式来进一步补充其基本特征:
- 自定义数据类型,实现对语言的扩展
- 将一些有用的功能封装成库函数
# 基本内置类型
基本算数类型:
类型 | 含义 | 最小尺寸 |
---|---|---|
bool | 布尔类型 | 8bits |
char | 字符 | 8bits |
wchar_t | 宽字符 | 16bits |
char16_t | Unicode 字符 | 16bits |
char32_t | Unicode 字符 | 32bits |
short | 短整型 | 16bits |
int | 整型 | 16bits (在 32 位机器中是 32bits) |
long | 长整型 | 32bits |
long long | 长整型 | 64bits (是在 C++11 中新定义的) |
float | 单精度浮点数 | 6 位有效数字 |
double | 双精度浮点数 | 10 位有效数字 |
long double | 扩展精度浮点数 | 10 位有效数字 |
# 如何选择类型
- 1. 当明确知晓数值不可能是负数时,选用无符号类型;
- 2. 使用
int
执行整数运算。一般long
的大小和int
一样,而short
常常显得太小。除非超过了int
的范围,选择long long
。 - 3. 算术表达式中不要使用
char
或bool
。 - 4. 浮点运算选用
double
。
# 类型转换
- 非布尔型赋给布尔型,初始值为 0 则结果为 false,否则为 true。
- 布尔型赋给非布尔型,初始值为 false 结果为 0,初始值为 true 结果为 1。
# 字面值常量
一个形如
42
的值被称作字面值常量(literal)。整型和浮点型字面值。
字符和字符串字面值。
使用空格连接,继承自 C。
字符字面值:单引号,
'a'
字符串字面值:双引号,
"Hello World"
分多行书写字符串。
std:cout<<"wow, a really, really long string" "literal that spans two lines" <<std::endl;
转义序列。
\n
、\t
等。布尔字面值。
true
,false
。指针字面值。
nullptr
字符串型实际上时常量字符构成的数组,结尾处以
'\0'
结束,所以字符串类型实际上长度比内容多 1。
# Exercise 2.1
类型 int、long、long long 和 short 的区别是什么?无符号类型和带符号类型的区别是什么?float 和 double 的区别是什么?
解:
C++ 规定 short 和 int 至少 16 位,long 至少 32 位,long long 至少 64 位。 带符号类型能够表示正数、负数和 0 ,而无符号类型只能够表示 0 和正整数。float 和 double 分别是单精度浮点数和双精度浮点数,区别主要是在内存中所占的比特数不同,以及默认规定的有效位数不同。
# Exercise 2.2
计算按揭贷款时,对于利率、本金和付款分别应选择何种数据类型?说明你的理由。
解:
使用 double
。在三种可供选择的浮点类型 float、double 和 long double 中,double 和 float 的计算代价比较接近且表示范围更广,long double 的计算代价则相对较大,一般情况下没有选择的必要。
# Exercise 2.3
读程序写结果。
unsigned u = 10, u2 = 42; | |
std::cout << u2 - u << std::endl; | |
std::cout << u - u2 << std::endl; | |
int i = 10, i2 = 42; | |
std::cout << i2 - i << std::endl; | |
std::cout << i - i2 << std::endl; | |
std::cout << i - u << std::endl; | |
std::cout << u - i << std::endl; |
解:
输出:
32
4294967264
32
-32
0
0
# Exercise 2.4
编写程序检查你的估计是否正确,如果不正确,请仔细研读本节直到弄明白问题所在。
# Exercise 2.5
指出下述字面值的数据类型并说明每一组内几种字面值的区别:
(a) 'a', L'a', "a", L"a"
(b) 10, 10u, 10L, 10uL, 012, 0xC
(c) 3.14, 3.14f, 3.14L
(d) 10, 10u, 10., 10e-2
解:
- (a): 字符字面值,宽字符字面值,字符串字面值,宽字符串字面值。
- (b): 十进制整型,十进制无符号整型,十进制长整型,十进制无符号长整型, 八进制整型,十六进制整型。
- (c): double, float, long double
- (d): 十进制整型,十进制无符号整型,double, double
# Exercise 2.6
下面两组定义是否有区别,如果有,请叙述之:
int month = 9, day = 7; | |
int month = 09, day = 07; |
解:
第一行定义的是十进制的整型,第二行定义的是八进制的整型。但是 month 变量有误,八进制不能直接写 9。
# Exercise 2.7
下述字面值表示何种含义?它们各自的数据类型是什么?
(a) "Who goes with F\145rgus?\012" | |
(b) 3.14e1L | |
(c) 1024f | |
(d) 3.14L |
解:
- (a) Who goes with Fergus?(换行),string 类型
- (b) long double
- (c) 无效,因为后缀
f
只能用于浮点字面量,而 1024 是整型。 - (d) long double
# Exercise 2.8
请利用转义序列编写一段程序,要求先输出 2M,然后转到新一行。修改程序使其先输出 2,然后输出制表符,再输出 M,最后转到新一行。
解:
#include <iostream> | |
int main() | |
{ | |
std::cout << 2 << "\115\012"; | |
std::cout << 2 << "\t\115\012"; | |
return 0; | |
} |
# Variables
A simple variable definition consists of a type specifier, followed by a list of one or more variable names separated by commas, and ends with a semicolon. Each name in the list has the type defined by the type specifier. A definition may (optionally) provide an initial value for one or more of the names it defines.
# 变量
变量提供一个具名的、可供程序操作的存储空间。 C++
中变量和对象一般可以互换使用。
# 变量定义(define)
- 定义形式:类型说明符(type specifier) + 一个或多个变量名组成的列表。如
int sum = 0, value, units_sold = 0;
- 初始化(initialize):对象在创建时获得了一个特定的值。
- 初始化不是赋值!:
- 初始化 = 创建变量 + 赋予初始值
- 赋值 = 擦除对象的当前值 + 用新值代替
- 列表初始化:使用花括号
{}
,如int units_sold{0};
- 默认初始化:定义时没有指定初始值会被默认初始化;在函数体内部的内置类型变量将不会被初始化。
- 建议初始化每一个内置类型的变量。
# 变量的声明(declaration) vs 定义(define)
- 为了支持分离式编译,
C++
将声明和定义区分开。声明使得名字为程序所知。定义负责创建与名字关联的实体。 - extern:只是说明变量定义在其他地方。
- 只声明而不定义: 在变量名前添加关键字
extern
,如extern int i;
。但如果包含了初始值,就变成了定义:extern double pi = 3.14;
- 变量只能被定义一次,但是可以多次声明。定义只出现在一个文件中,其他文件使用该变量时需要对其声明。
- 名字的作用域(namescope)
{}
- 第一次使用变量时再定义它。
- 嵌套的作用域
- 同时存在全局和局部变量时,已定义局部变量的作用域中可用
::reused
显式访问全局变量 reused。 - 但是用到全局变量时,尽量不适用重名的局部变量。
- 同时存在全局和局部变量时,已定义局部变量的作用域中可用
# 变量命名规范
- 需体现实际意义
- 变量名用小写字母
- 自定义类名用大写字母开头:Sales_item
- 标识符由多个单词组成,中间须有明确区分:student_loan 或 studentLoan,不要用 studentloan。
# Exercise 2.9
解释下列定义的含义,对于非法的定义,请说明错在何处并将其改正。
- (a) std::cin >> int input_value;
- (b) int i = { 3.14 };
- (c) double salary = wage = 9999.99;
- (d) int i = 3.14;
解:
(a): 应该先定义再使用。
int input_value = 0; | |
std::cin >> input_value; |
(b): 用列表初始化内置类型的变量时,如果存在丢失信息的风险,则编译器将报错。
double i = { 3.14 }; |
(c): 在这里 wage
是未定义的,应该在此之前将其定义。
double wage; | |
double salary = wage = 9999.99; |
(d): 不报错,但是小数部分会被截断。
double i = 3.14; |
# Exercise 2.10
下列变量的初值分别是什么?
std::string global_str; | |
int global_int; | |
int main() | |
{ | |
int local_int; | |
std::string local_str; | |
} |
解:
global_str
和 global_int
是全局变量,所以初值分别为空字符串和 0。local_int
是局部变量并且没有初始化,它的初值是未定义的。local_str
是 string
类的对象,它的值由类确定,为空字符串。
# Exercise 2.11
指出下面的语句是声明还是定义:
- (a) extern int ix = 1024;
- (b) int iy;
- (c) extern int iz;
解:
(a): 定义
(b): 定义
(c): 声明
# Exercise 2.12
请指出下面的名字中哪些是非法的?
- (a) int double = 3.14;
- (b) int _;
- (c) int catch-22;
- (d) int 1_or_2 = 1;
- (e) double Double = 3.14;
解:
(a), (c), (d) 非法。
# Exercise 2.13
下面程序中 j
的值是多少?
int i = 42; | |
int main() | |
{ | |
int i = 100; | |
int j = i; | |
} |
解:
j
的值是 100,局部变量 i
覆盖了全局变量 i
。
# Exercise 2.14
下面的程序合法吗?如果合法,它将输出什么?
int i = 100, sum = 0; | |
for (int i = 0; i != 10; ++i) | |
sum += i; | |
std::cout << i << " " << sum << std::endl; |
解:
合法。输出是 100 45 。
# Compound Types
# 左值和右值
- 左值(l-value)可以出现在赋值语句的左边或者右边,比如变量;
- 右值(r-value)只能出现在赋值语句的右边,比如常量。
# 复合类型
# 引用
一般说的引用是指的左值引用
- 引用:引用是一个对象的别名,引用类型引用(refer to)另外一种类型。如
int &refVal = val;
。 - 引用必须初始化。
- 引用和它的初始值是绑定 bind 在一起的,而不是拷贝。一旦定义就不能更改绑定为其他的对象
# 指针
int *p; // 指向 int 型对象的指针
是一种
"指向(point to)"
另外一种类型的复合类型。定义指针类型:
int *ip1;
,从右向左读有助于阅读,ip1
是指向int
类型的指针。指针存放某个对象的地址。
获取对象的地址:
int i=42; int *p = &i;
。&
是取地址符。指针的类型与所指向的对象类型必须一致(均为同一类型 int、double 等)
指针的值的四种状态:
1. 指向一个对象;
2. 指向紧邻对象的下一个位置;
3. 空指针;
4. 无效指针。
对无效指针的操作均会引发错误,第二种和第三种虽为有效的,但理论上是不被允许的
指针访问对象:
cout << *p;
输出 p 指针所指对象的数据,*
是解引用符。空指针不指向任何对象。使用
int *p=nullptr;
来使用空指针。指针和引用的区别:引用本身并非一个对象,引用定义后就不能绑定到其他的对象了;指针并没有此限制,相当于变量一样使用。
赋值语句永远改变的是左侧的对象。
void*
指针可以存放任意对象的地址。因无类型,仅操作内存空间,对所存对象无法访问。其他指针类型必须要与所指对象严格匹配。
两个指针相减的类型是
ptrdiff_t
。建议:初始化所有指针。
int* p1, p2;//*是对p1的修饰,所以p2还是int型
# Exercise 2.15
下面的哪个定义是不合法的?为什么?
- (a) int ival = 1.01;
- (b) int &rval1 = 1.01;
- (c) int &rval2 = ival;
- (d) int &rval3;
解:
(b) 和 (d) 不合法,(b) 引用必须绑定在对象上,(d) 引用必须初始化。
# Exercise 2.16
考察下面的所有赋值然后回答:哪些赋值是不合法的?为什么?哪些赋值是合法的?它们执行了哪些操作?
int i = 0, &r1 = i; | |
double d = 0, &r2 = d; |
- (a) r2 = 3.14159;
- (b) r2 = r1;
- (c) i = r2;
- (d) r1 = d;
解:
- (a): 合法。给 d 赋值为 3.14159。
- (b): 合法。会执行自动转换(int->double)。
- (c): 合法。会发生小数截取。
- (d): 合法。会发生小数截取。
# Exercise 2.17
执行下面的代码段将输出什么结果?
int i, &ri = i; | |
i = 5; ri = 10; | |
std::cout << i << " " << ri << std::endl; |
解:
输出:10, 10
# Exercise 2.18
编写代码分别改变指针的值以及指针所指对象的值。
解:
int a = 0, b = 1; | |
int *p1 = &a, *p2 = p1; | |
// change the value of a pointer. | |
p1 = &b; | |
// change the value to which the pointer points | |
*p2 = b; |
# Exercise 2.19
说明指针和引用的主要区别
解:
引用是另一个对象的别名,而指针本身就是一个对象。
引用必须初始化,并且一旦定义了引用就无法再绑定到其他对象。而指针无须在定义时赋初值,也可以重新赋值让其指向其他对象。
# Exercise 2.20
请叙述下面这段代码的作用。
int i = 42; | |
int *p1 = &i; | |
*p1 = *p1 * *p1; |
解:
让指针 pi 指向 i,然后将 i 的值重新赋值为 42 * 42 (1764)。
# Exercise 2.21
请解释下述定义。在这些定义中有非法的吗?如果有,为什么?
int i = 0;
- (a) double* dp = &i;
- (b) int *ip = i;
- (c) int *p = &i;
解:
- (a): 非法。不能将一个指向
double
的指针指向int
。 - (b): 非法。不能将
int
变量赋给指针。 - (c): 合法。
# Exercise 2.22
假设 p 是一个 int 型指针,请说明下述代码的含义。
if (p) // ... | |
if (*p) // ... |
解:
第一句判断 p 是不是一个空指针,
第二句判断 p 所指向的对象的值是不是为 0
# Exercise 2.23
给定指针 p,你能知道它是否指向了一个合法的对象吗?如果能,叙述判断的思路;如果不能,也请说明原因。
解:
不能,因为首先要确定这个指针是不是合法的,才能判断它所指向的对象是不是合法的。
# Exercise 2.24
在下面这段代码中为什么 p 合法而 lp 非法?
int i = 42; | |
void *p = &i; | |
long *lp = &i; |
解:
void *
是从 C 语言那里继承过来的,可以指向任何类型的对象。
而其他指针类型必须要与所指对象严格匹配。
# Exercise 2.25
说明下列变量的类型和值。
(a) int* ip, i, &r = i; | |
(b) int i, *ip = 0; | |
(c) int* ip, ip2; |
解:
- (a): ip 是一个指向 int 的指针,i 是一个 int, r 是 i 的引用。
- (b): i 是 int , ip 是一个空指针。
- (c): ip 是一个指向 int 的指针,ip2 是一个 int。
# const Qualifier
# const 限定符
- 动机:希望定义一些不能被改变值的变量。
# 初始化和 const
- const 对象必须初始化,且不能被改变。
- const 变量默认不能被其他文件访问,非要访问,必须在指定 const 定义之前加 extern。要想在多个文件中使用 const 变量共享,定义和声明都加 const 关键字即可。
# const 的引用
- reference to const(对常量的引用):指向 const 对象的引用,如
const int ival=1; const int &refVal = ival;
,可以读取但不能修改refVal
。 - 临时量(temporary)对象:当编译器需要一个空间来暂存表达式的求值结果时,临时创建的一个未命名的对象。
- 对临时量的引用是非法行为。
# 指针和 const
- pointer to const(指向常量的指针):不能用于改变其所指对象的值,如
const double pi = 3.14; const double *cptr = π
。 - const pointer:指针本身是常量,也就是说指针固定指向该对象,(存放在指针中的地址不变,地址所对应的那个对象值可以修改)如
int i = 0; int *const ptr = &i;
# 顶层 const
顶层const
:指针本身是个常量。底层const
:指针指向的对象是个常量。拷贝时严格要求相同的底层 const 资格。
# constexpr
和常量表达式(▲可选)
- 常量表达式:指值不会改变,且在编译过程中就能得到计算结果的表达式。
C++11
新标准规定,允许将变量声明为constexpr
类型以便由编译器来验证变量的值是否是一个常量的表达式。
# Exercise 2.26
下面哪些语句是合法的?如果不合法,请说明为什么?
解:
const int buf; // 不合法,const 对象必须初始化 | |
int cnt = 0; // 合法 | |
const int sz = cnt; // 合法 | |
++cnt; ++sz; // 不合法,const 对象不能被改变 |
# Exercise 2.27
下面的哪些初始化是合法的?请说明原因。
解:
int i = -1, &r = 0; // 不合法,r 必须引用一个对象 | |
int *const p2 = &i2; // 合法,常量指针 | |
const int i = -1, &r = 0; // 合法 | |
const int *const p3 = &i2; // 合法 | |
const int *p1 = &i2; // 合法 | |
const int &const r2; // 不合法,r2 是引用,引用没有顶层 const | |
const int i2 = i, &r = i; // 合法 |
# Exercise 2.28
说明下面的这些定义是什么意思,挑出其中不合法的。
解:
int i, *const cp; // 不合法,const 指针必须初始化 | |
int *p1, *const p2; // 不合法,const 指针必须初始化 | |
const int ic, &r = ic; // 不合法,const int 必须初始化 | |
const int *const p3; // 不合法,const 指针必须初始化 | |
const int *p; // 合法。一个指针,指向 const int |
# Exercise 2.29
假设已有上一个练习中定义的那些变量,则下面的哪些语句是合法的?请说明原因。
解:
i = ic; // 合法,常量赋值给普通变量 | |
p1 = p3; // 不合法,p3 是 const 指针不能赋值给普通指针 | |
p1 = ⁣ // 不合法,普通指针不能指向常量 | |
p3 = ⁣ // 合法,p3 是常量指针且指向常量 | |
p2 = p1; // 合法,可以将普通指针赋值给常量指针 | |
ic = *p3; // 合法,对 p3 取值后是一个 int 然后赋值给 ic |
# Exercise 2.30
对于下面的这些语句,请说明对象被声明成了顶层 const 还是底层 const?
const int v2 = 0; int v1 = v2; | |
int *p1 = &v1, &r1 = v1; | |
const int *p2 = &v2, *const p3 = &i, &r2 = v2; |
解:
v2 是顶层 const,p2 是底层 const,p3 既是顶层 const 又是底层 const,r2 是底层 const。
# Exercise 2.31
假设已有上一个练习中所做的那些声明,则下面的哪些语句是合法的?请说明顶层 const 和底层 const 在每个例子中有何体现。
解:
r1 = v2; // 合法,顶层 const 在拷贝时不受影响 | |
p1 = p2; // 不合法,p2 是底层 const,如果要拷贝必须要求 p1 也是底层 const | |
p2 = p1; // 合法,int* 可以转换成 const int* | |
p1 = p3; // 不合法,p3 是一个底层 const,p1 不是 | |
p2 = p3; // 合法,p2 和 p3 都是底层 const,拷贝时忽略掉顶层 const |
# Exercise 2.32
下面的代码是否合法?如果非法,请设法将其修改正确。
int null = 0, *p = null; |
解:
合法。指针可以初始化为 0 表示为空指针。
# Dealing with Types
# 处理类型
# 类型别名
- 传统别名:使用 typedef 来定义类型的同义词。
typedef double wages;
- 新标准别名:别名声明(alias declaration):
using SI = Sales_item;
(C++11)
// 对于复合类型(指针等)不能代回原式来进行理解
typedef char *pstring; // pstring是char*的别名
const pstring cstr = 0; // 指向char的常量指针
// 如改写为const char *cstr = 0;不正确,为指向const char的指针
// 辅助理解(可代回后加括号)
// const pstring cstr = 0;代回后const (char *) cstr = 0;
// const char *cstr = 0;即为(const char *) cstr = 0;
# auto 类型说明符 c++11
- auto 类型说明符:让编译器自动推断类型。
- 一条声明语句只能有一个数据类型,所以一个 auto 声明多个变量时只能相同的变量类型 (包括复杂类型 & 和 *)。
auto sz = 0, pi =3.14//错误
int i = 0, &r = i; auto a = r;
推断a
的类型是int
。- 会忽略
顶层const
。 const int ci = 1; const auto f = ci;
推断类型是int
,如果希望是顶层 const 需要自己加const
auto 定义的变量必须有初始值。
使用 auto 也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型类型都必须一样。
# 复合类型、常量和 auto
auto 一般会忽略掉顶层 const,同时底层 const 则会保留下来,比如当初始值是一个指向常量的指针时:
const int ci = i, &cr = ci; | |
auto b = ci; // b is an int (top-level const in ci is dropped) | |
auto c = cr; // c is an int (cr is an alias for ci whose const is top-level) | |
auto d = &i; // d is an int*(& of an int object is int*) | |
auto e = &ci; // e is const int*(& of a const object is low-level const) |
如果希望推断出的 auto 类型是一个顶层 const,需要明确指出:
const auto f = ci; // deduced type of ci is int; f has type const int |
还可以将引用的类型设为 auto,此时原来的初始化规则仍然适用:
auto &g = ci; // g is a const int& that is bound to ci | |
auto &h = 42; // error: we can't bind a plain reference to a literal | |
const auto &j = 42; // ok: we can bind a const reference to a literal |
设置一个类型为 auto 的引用时,初始值中的顶层常量属性仍然保留。
# decltype 类型指示符
- 从表达式的类型推断出要定义的变量的类型。
- decltype:选择并返回操作数的数据类型。
decltype(f()) sum = x;
推断sum
的类型是函数f
的返回类型。- 不会忽略
顶层const
。 - 如果对变量加括号,编译器会将其认为是一个表达式,如 int i-->(i), 则 decltype ((i)) 得到结果为 int & 引用。
- 赋值是会产生引用的一类典型表达式,引用的类型就是左值的类型。也就是说,如果 i 是 int,则表达式 i=x 的类型是 int&。
C++11
# Exercise 2.33
利用本节定义的变量,判断下列语句的运行结果。
解:
a=42; //a 是 int | |
b=42; //b 是一个 int,(ci 的顶层 const 在拷贝时被忽略掉了) | |
c=42; //c 也是一个 int | |
d=42; //d 是一个 int *, 所以语句非法 | |
e=42; //e 是一个 const int *, 所以语句非法 | |
g=42; //g 是一个 const int 的引用,引用都是底层 const,所以不能被赋值 |
# Exercise 2.34
基于上一个练习中的变量和语句编写一段程序,输出赋值前后变量的内容,你刚才的推断正确吗?如果不对,请反复研读本节的示例直到你明白错在何处为止。
# Exercise 2.35
判断下列定义推断出的类型是什么,然后编写程序进行验证。
const int i = 42; | |
auto j = i; const auto &k = i; auto *p = &i; | |
const auto j2 = i, &k2 = i; |
解:
j 是 int,k 是 const int 的引用,p 是 const int *,j2 是 const int,k2 是 const int 的引用。
# Exercise 2.36
关于下面的代码,请指出每一个变量的类型以及程序结束时它们各自的值。
int a = 3, b = 4; | |
decltype(a) c = a; | |
decltype((b)) d = a; | |
++c; | |
++d; |
解:
c 是 int 类型,值为 4。d 是 int & 类型,绑定到 a,a 的值为 4 。
# Exercise 2.37
赋值是会产生引用的一类典型表达式,引用的类型就是左值的类型。也就是说,如果 i 是 int,则表达式 i=x 的类型是 int&。根据这一特点,请指出下面的代码中每一个变量的类型和值。
int a = 3, b = 4; | |
decltype(a) c = a; | |
decltype(a = b) d = a; |
解:
c 是 int 类型,值为 3。d 是 int& 类型,绑定到 a。
# Exercise 2.38
说明由 decltype 指定类型和由 auto 指定类型有何区别。请举一个例子,decltype 指定的类型与 auto 指定的类型一样;再举一个例子,decltype 指定的类型与 auto 指定的类型不一样。
解:
decltype 处理顶层 const 和引用的方式与 auto 不同,decltype 会将顶层 const 和引用保留起来。
int i = 0, &r = i; | |
// 相同 | |
auto a = i; | |
decltype(i) b = i; | |
// 不同 d 是一个 int& | |
auto c = r; | |
decltype(r) d = r; |
# Defining Our Own Data Structures Chapter Summary
# 自定义数据结构
# struct
尽量不要吧类定义和对象定义放在一起。如
struct Student{} xiaoming,xiaofang;
- 类可以以关键字
struct
开始,紧跟类名和类体。 - 类数据成员:类体定义类的成员。
C++11
:可以为类数据成员提供一个类内初始值(in-class initializer)。
# 编写自己的头文件
- 头文件通常包含哪些只能被定义一次的实体:类、
const
和constexpr
变量。
预处理器概述:
- 预处理器(preprocessor):确保头文件多次包含仍能安全工作。
- 当预处理器看到
#include
标记时,会用指定的头文件内容代替#include
- 头文件保护符(header guard):头文件保护符依赖于预处理变量的状态:已定义和未定义。
#indef
已定义时为真#inndef
未定义时为真- 头文件保护符的名称需要唯一,且保持全部大写。养成良好习惯,不论是否该头文件被包含,要加保护符。
#ifndef SALES_DATA_H //SALES_DATA_H未定义时为真
#define SALES_DATA_H
strct Sale_data{
...
}
#endif
# Exercise 2.39
编译下面的程序观察其运行结果,注意,如果忘记写类定义体后面的分号会发生什么情况?记录下相关的信息,以后可能会有用。
struct Foo { /* 此处为空 */ } // 注意:没有分号 | |
int main() | |
{ | |
return 0; | |
}。 |
解:
提示应输入分号。
# Exercise 2.40
根据自己的理解写出 Sales_data 类,最好与书中的例子有所区别。
struct Sale_data | |
{ | |
std::string bookNo; | |
std::string bookName; | |
unsigned units_sold = 0; | |
double revenue = 0.0; | |
double price = 0.0; | |
//... | |
} |
# Exercise 2.41
使用你自己的 Sale_data 类重写 1.5.1 节(第 20 页)、1.5.2 节(第 21 页)和 1.6 节(第 22 页)的练习。眼下先把 Sales_data 类的定义和 main 函数放在一个文件里。
// 1.5.1 | |
#include <iostream> | |
#include <string> | |
struct Sale_data | |
{ | |
std::string bookNo; | |
unsigned units_sold = 0; | |
double revenue = 0.0; | |
}; | |
int main() | |
{ | |
Sale_data book; | |
double price; | |
std::cin >> book.bookNo >> book.units_sold >> price; | |
book.revenue = book.units_sold * price; | |
std::cout << book.bookNo << " " << book.units_sold << " " << book.revenue << " " << price; | |
return 0; | |
} |
// 1.5.2 | |
#include <iostream> | |
#include <string> | |
struct Sale_data | |
{ | |
std::string bookNo; | |
unsigned units_sold = 0; | |
double revenue = 0.0; | |
}; | |
int main() | |
{ | |
Sale_data book1, book2; | |
double price1, price2; | |
std::cin >> book1.bookNo >> book1.units_sold >> price1; | |
std::cin >> book2.bookNo >> book2.units_sold >> price2; | |
book1.revenue = book1.units_sold * price1; | |
book2.revenue = book2.units_sold * price2; | |
if (book1.bookNo == book2.bookNo) | |
{ | |
unsigned totalCnt = book1.units_sold + book2.units_sold; | |
double totalRevenue = book1.revenue + book2.revenue; | |
std::cout << book1.bookNo << " " << totalCnt << " " << totalRevenue << " "; | |
if (totalCnt != 0) | |
std::cout << totalRevenue / totalCnt << std::endl; | |
else | |
std::cout << "(no sales)" << std::endl; | |
return 0; | |
} | |
else | |
{ | |
std::cerr << "Data must refer to same ISBN" << std::endl; | |
return -1; // indicate failure | |
} | |
} |
// 1.6 | |
#include <iostream> | |
#include <string> | |
struct Sale_data | |
{ | |
std::string bookNo; | |
unsigned units_sold = 0; | |
double revenue = 0.0; | |
}; | |
int main() | |
{ | |
Sale_data total; | |
double totalPrice; | |
if (std::cin >> total.bookNo >> total.units_sold >> totalPrice) | |
{ | |
total.revenue = total.units_sold * totalPrice; | |
Sale_data trans; | |
double transPrice; | |
while (std::cin >> trans.bookNo >> trans.units_sold >> transPrice) | |
{ | |
trans.revenue = trans.units_sold * transPrice; | |
if (total.bookNo == trans.bookNo) | |
{ | |
total.units_sold += trans.units_sold; | |
total.revenue += trans.revenue; | |
} | |
else | |
{ | |
std::cout << total.bookNo << " " << total.units_sold << " " << total.revenue << " "; | |
if (total.units_sold != 0) | |
std::cout << total.revenue / total.units_sold << std::endl; | |
else | |
std::cout << "(no sales)" << std::endl; | |
total.bookNo = trans.bookNo; | |
total.units_sold = trans.units_sold; | |
total.revenue = trans.revenue; | |
} | |
} | |
std::cout << total.bookNo << " " << total.units_sold << " " << total.revenue << " "; | |
if (total.units_sold != 0) | |
std::cout << total.revenue / total.units_sold << std::endl; | |
else | |
std::cout << "(no sales)" << std::endl; | |
return 0; | |
} | |
else | |
{ | |
std::cerr << "No data?!" << std::endl; | |
return -1; // indicate failure | |
} | |
} |
# Exercise 2.42
根据你自己的理解重写一个 Sales_data.h 头文件,并以此为基础重做 2.6.2 节(第 67 页)的练习。
# Chapter Summary
🍓:)