C++ Primer(第五版)读书笔记 & 习题解答 --- Chapter 2

Chapter 2.1

1. 数据类型决定了程序中数据和操作的意义。

2. C++定义了一套基本数据类型,其中包括算术类型和一个名为void的特殊类型。算术类型包含了字符、整型、布尔值以及浮点数。void无值也无操作,我们不能定义一个void类型的变量。

3. 算术类型的尺寸,也就是该类型所占的比特数,在不同机器上有所差别。下表列出了C++标准规定的最小尺寸,同时允许编译器赋予这些类型更大的尺寸:

C++ Primer(第五版)读书笔记 & 习题解答 --- Chapter 2

由于比特数的不同,一个类型所能表达的最大(最小)值也是不同的。

4. C++语言规定,一个int至少和一个short一样大,一个long至少和一个int一样大,一个long long至少和一个long一样大。

5. 基本字符类型是char,一个char的尺寸应确保可以存放机器基本字符集中任意字符对应的数字值,也就是说,一个char的尺寸和一个机器字节相同。

6. 其他字符类型,如wchar_t、char16_t、char32_t用于扩展字符集。wchar_t类型确保可以存放机器最大扩展字符集中的任意一个字符。char16_t和char32_t用于Unicode字符集(Unicode是用于表示所有自然语言中字符的标准)。

7. 计算机以比特序列存储数据。大多数计算机以2的整数次幂个比特作为块来处理内存,可寻址的最小内存块称为“字节”。在C++语言中,一个字节要至少能容纳机器基本字符集中的字符。存储的基本单元称为“字”,通常由几个字节组成。大多数机器的字节由8比特构成,字则由32比特或64比特构成。

8. 大多数计算机将内存中的每个字节与一个数字(被称为地址)关联起来。我们能够使用某个地址来表示从这个地址开始的大小不同的比特串。为了赋予内存中某个地址明确的含义,必须首先知道存储在该地址的数据的类型。类型决定了数据所占的比特数以及该如何解释这些比特的内容。

9. 除去布尔型和扩展的字符型之外,其他整型可以划分为带符号的和无符号的。字符型与其他整型不同,被分为三种:char、unsigned char和signed char。尽管有三种,但是其表现形式仍就只有两种:带符号的和无符号的。类型char会表现为signed char还是unsigned char是由编译器决定的。

10. 无符号类型中所有比特都用来存储值。对于有符号类型,C++标准并没有规定应如何表示,但是约定了在表示范围内正值和负值应该均匀分布。

11. 下面是选择使用哪种类型的一些有用的准则:

(1). 当明确知晓数值不可能为负时,选用无符号类型
(2). 使用int来执行整数运算。short通常太小了而long一般和int的尺寸一样。如果数值超出int的表示范围,就使用long long
(3). 在算术表达式中不要使用char或bool。只有在存放字符或布尔值时才使用它们。因为char在一些机器上是有符号的,而在另一些机器上却是无符号的,所以如果使用char进行运算特别容易出问题。如果你需要一个不大的整数,就显示指定为signed char或unsigned char
(4). 选用double来执行浮点运算。float的精度通常不够,并且双精度浮点和单精度浮点的计算在性能开销上相差无几。事实上,在一些机器上,双精度运算甚至比单精度还快。long double提供的精度通常是没有必要的,并且它带来的性能开销也不容忽视

12. 当在程序的某处我们使用了一种类型而对象应该取另一种类型时,程序会自动进行类型转换:

bool b = ;             // b为真
int i = b; // i的值为1
i = 3.14; // i的值为3
double pi = i; // pi的值为3.0
unsigned char c = -; // 假设char占8比特,c的值为255
signed char c2 = ; // 假设char占8比特,c2的值是未定义的

类型所能表示的值的范围决定了转换的过程:

(1). 当我们把一个非布尔算术类型赋值给一个布尔类型对象时,如果值是0,则结果为false,否则结果为true
(2). 当我们把一个布尔类型赋值给其他算术类型时,如果布尔值为true,那么结果就是1,否则结果就是0
(3). 当我们把浮点类型赋值给整型对象时,值会被截断。只会保留小数点之前的值
(4). 当我们把整型类型赋值给浮点型对象时,小数部分将是0。如果该整数的比特数超过了浮点对象可以容纳的比特数的话,精度可能会丢失
(5). 当我们给无符号类型赋值一个超出它表示范围的数值时,结果是该值对无符号类型能够表示的数值的总数取模后的余数。例如,8比特大小的unsigned char可以表示0到255,总共256个数。因此,把-1赋值给它所得的结果是255
(6). 当我们给带符号类型赋一个超出它表示范围的值时,结果是未定义的

13. 如果表达式里既有带符号类型又有无符号类型,带符号数会自动地转换成无符号数。

14. 一个形如42的值被称为字面值常量。每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。

15. 我们可以将整型字面值写作十进制数、八进制数或十六进制数的形式:

     // 十进制
// 八进制,以0开头
0x14 // 十六进制,以0x或0X开头

默认情况下,十进制字面值是带符号数,它的类型是int、long和long long中能容纳字面值数值的尺寸最小的那个。而八进制和十六进制字面值既可能是带符号的也可能是无符号的,它的类型是int、unsigned int、long、unsigned long、long long和unsigned long long中能容纳字面值数值的尺寸最小的那个。尽管整型字面值可以存储在带符号类型中,但技术上来说,十进制字面值不会是负数。如果我们写-42,那么负号其实不是字面值的一部分,它只是一个操作符,作用是对字面值取负。

浮点型字面值表现为一个小数或以科学计数法表示的指数:
3.14159
3.14159E0
.
0e0
.

默认情况下,浮点型字面值是一个double。

16. 由单引号括起来的一个字符称为char型字面值,双引号括起来的零个或多个字符则构成字符串型字面值。编译器会在每个字符串型字面值的结尾处添加一个空字符('\0'),因此,字符串型字面值的实际长度要比它的内容多1。如果两个字符串型字面值位置紧邻并且仅由空格、缩进和换行符分隔,那么它们实际上是一个整体:

std::cout << "一个很长很长很长很长再长一点的字符串 "
"跨越了两行噢" << std::endl;

17. C++语言规定的转义序列包括:

C++ Primer(第五版)读书笔记 & 习题解答 --- Chapter 2

我们也可以使用泛化的转义序列,其形式是\x后紧跟1个或多个十六进制数字,或者\后紧跟1个、2个或3个八进制数字。其值表示的是字符对应的数值。

18. 我们可以通过下表中所列的前缀和后缀,来改变整型、浮点型和字符型字面值的默认类型:

C++ Primer(第五版)读书笔记 & 习题解答 --- Chapter 2

Chapter 2.2

1. C++中的每个变量都有其数据类型,数据类型决定着变量所占内存空间的大小和布局方式、该内存空间能够存储的值的范围、以及变量能参与的运算。

2. 当一次定义了两个或多个变量时,对象的名字随着定义也就马上可以使用了。因此,在同一条定义语句中,可以用先定义的变量值去初始化后定义的其他变量:

double price = 109.99, discount = price * 0.16;

3. 初始化与赋值是两个完全不同的操作。初始化的含义是创建变量时赋予其一个初始值。而赋值的含义是把对象的当前值擦除,用一个新值来替代。

4. 在C++11标准中,可以用花括号来初始化变量,这种初始化形式被称为列表初始化:

int units_sold = {};
int units_sold{};

当列表初始化用于内置类型的变量时,有一个重要特点:如果初始值存在丢失信息的风险,则编译器会报错:

long double ld = 3.1415926536;
int a{ld}, b = {ld}; // 编译错误:存在丢失信息的风险
int c(ld), d = ld; // 编译正确:但值被截断了

5. 如果定义变量时没有指定初始值,则变量会默认初始化:

(1). 如果是内置类型的变量,当它定义于任何函数体之外时,将被初始化为0。当它定义在函数体内部时,将不被初始化
(2). 每个类各自决定其初始化对象的方式

6. 变量声明规定了变量的类型和名字,在这一点上定义与之相同。但除此之外,定义还申请存储空间,并且可能会为变量赋一个初始值。

7. 声明一个变量而非定义它,就在变量名前添加关键字extern:

extern int i;  // 声明
int j; // 定义

8. 任何包含了显示初始化的声明即成为定义。因此,extern语句如果包含初始值也就不再是声明了:

extern double pi = 3.1416;  // 定义

在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误。

9. 变量能且只能被定义一次,但是可以被多次声明。

10. C++中的作用域有如下几级:

全局作用域:全局作用域内的名字在整个程序的范围内都可使用
类作用域:名字定义在类的内部
命名空间作用域:名字定义在命名空间内部
块作用域:名字定义在块的内部。从声明位置开始直至声明语句所在的作用域末端为止都是可用的

11. 作用域能彼此包含,被包含的作用域称为内层作用域,包含着别的作用域的作用域称为外层作用域。作用域中一旦声明了某个名字,它所嵌套着的所有作用域都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字。

Chapter 2.3

1. 我们无法令引用重新绑定到另外一个对象,因此引用必须初始化。

2. 引用并非对象,它只是为一个已经存在的对象所起的另外一个名字。因为引用本身不是一个对象,所以不能定义引用的引用。

3. 引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起。

4. 指针与引用相比有几个不同点:其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。其二,指针无须在定义时赋初值。

5. 和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。

6. 因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。

7. 指针存储的值可以是以下四种状态之一:

(1). 指向一个对象
(2). 指向一个对象所占空间末尾的下一个位置
(3). 空指针
(4). 无效指针

8. C++11引入了nullptr,可以初始化一个指针,表示空指针。它是一种特殊类型的字面值,可以被转换成任意其他的指针类型。

9. void*是一种特殊的指针类型,可用于存放任意对象的地址。

10. 变量的定义包括一个基本数据类型和一组声明符。在同一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式却可以不同:

// i是一个int类型的变量,p是一个int类型的指针,r是一个int类型的引用
int i = , *p = &i, &r = i;

11. 指针是对象,所以存在对指针的引用:

int i = ;
int *p = nullptr;
int *&r = p; // r是一个对指针p的引用
r = &i; // r引用了指针p,所以就是令p存放i的地址
*r = ; // r引用了指针p,所以就是解引用指针p,得到i,将i的值改为0

Chapter 2.4

1. 引用的类型必须与其所引用对象的类型一致,但是有两个例外。第一个就是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。特别是,允许为一个常量引用绑定非常量的对象、字面值,甚至是一个表达式:

int i = ;
const int &r1 = i;
const int &r2 = ;
const int &r3 = r1 * ;
double d = 3.14;
const int &r4 = d;

2. 指针的类型必须与其所指对象的类型一致,但是有两个例外。第一个就是允许令一个指向常量的指针指向一个非常量对象:

double dval = 3.14;
const double *ptr = &dval;

3. 我们使用名词顶层const(top-level const)表示一个对象本身是常量,顶层const对任何数据类型都适用,如算术类型、类、指针等。而名词底层const(low-level const)用于指针或引用这些复合类型的基本类型,例如表示指针所指的对象是一个常量,引用所绑定的对象是一个常量。

当执行对象的拷贝操作时,顶层const会被忽略:
int i = ;
const int ci = ;
i = ci; // ci是顶层const,被忽略
拷贝操作并不会改变被拷贝对象的值,因此,拷入和拷出的对象是不是常量都没什么影响。
另一方面,底层const却不容忽视。当我们拷贝一个对象时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换,通常来说,非常量可以转换为常量,反之则不行:
int i = ;
const int* const p = &i; // p3既是顶层const,又是底层const
int *p1 = p; // 错误:p包含底层const,但p1却没有
const int &r = i; // 正确:const int&可以绑定到普通int上,反之int&却不可以绑定到const int上

4. 常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定:

const int max_files = ; // 常量表达式
const int limit = max_files + ; // 常量表达式
int staff_size = ; // 不是常量表达式
const int sz = get_size(); // 不是常量表达式

C++11中,我们可以将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。

5. 尽管指针可以定义成constexpr,但是它的初始值却受到严格的限制,必须是nullptr或者0,或者是存储于某个固定地址中的对象。还必须明确的一点事,限定符constexpr仅对指针有效,与指针所指的对象无关:

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

Chapter 2.5

1. C++11引入了一种新的定义类型别名的方法:

using MyInt = int;

2. 当指代复合类型的类型别名与const一起使用时,可能会产生意想不到的结果:

using pstring = char*;
const pstring cstr = nullptr; // cstr是指向char的常量指针

来看上述代码,当遇到一条使用了类型别名的声明语句时,很多人常常会错误的把类型别名替换成它本来的样子来理解它:

const char *cstr = nullptr;

这种理解是错误的。声明语句中用到pstring时,其基本数据类型是char*,是一个指针。而用char*替换重写了之后,基本数据类型就变成了char。所以,在理解使用类型别名的声明语句时,要特别注意这一点。

3. C++11引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。auto定义的变量必须有初始值。使用auto也能在一条语句中声明多个变量,但因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一致:

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

4. 使用auto的时候要注意以下一些规则:当引用被用作初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为auto的类型:

int i = , &r = i;
auto a = r; // a是一个整数

auto一般会忽略掉顶层const,而保留底层const:

const int ci = ;
auto b = ci; // b是一个整数(ci的顶层const特性被忽略)
auto e = &ci; // e是一个指向整数常量的指针(对常量对象取地址是一种底层const)

设置一个类型为auto的引用时,初始值中的顶层const会被保留:

const int ci = ;
auto &r = ci; // r是一个常量引用

5. C++11标准引入了类型说明符decltype,它的作用是返回它的操作数的类型:

decltype(f()) sum = x;

在上面的代码中,编译器并不会调用函数f,而是使用当函数f被调用时的返回值类型作为sum的类型。decltype处理顶层const和引用的方式与auto不同,如果decltype使用的表达式是一个变量,则它会返回该变量的类型(包括顶层const和引用):

const int ci = , &cj = ci;
decltype(ci) x = ; // x的类型是const int
decltype(cj) y = x; // y的类型是const int&,绑定到x

如果decltype使用的表达式不是一个变量,则它将返回表达式结果对于的类型。一些表达式会导致decltype产生一个引用类型,通常来说,返回引用类型的表达式是那种能产生一条赋值语句的左值的表达式:

int i = , *p = &i, &r = i;
decltype(r + ) b; // b是int类型
decltype(*p) c; // 错误:c是int&类型,必须初始化

正如我们所看到的,*p得到指针p所指的对象,并且该对象可以赋值,所以,decltype(*p)产生的是int&,而不是int。

有一种情况需要特别注意,如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型。如果给变量加上了一层或多层括号,编译器就会把它当成是一个表达式,而变量是一种可以作为赋值语句左值的特殊表达式,因此,这样decltype就会得到引用类型: 
int i = ;
decltype((i)) d = i; // d是int&

Chapter 2.6

1. C++11标准规定,可以为类的数据成员提供一个类内初始值:

class CItem
{
unsigned unitsSold = ;
};

2. 预处理器是在编译之前执行的一段程序。

3. 预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变量。预处理变量不遵循C++语言中关于作用域的规则,所以预处理变量在整个程序中必须唯一。

Exercises Section 2.1.3

Q_1. Using escape sequences, write a program to print 2M followed by a newline. Modify the program to print 2, then a tab, then an M, followed by a newline.

A_1.

#include <iostream>

int main()
{
std::cout << "2\115" << '\12'; return ;
}
#include <iostream>

int main()
{
std::cout << '' << '\t' << '\115' << '\n'; return ;
}

Exercises Section 2.3.2

Q_1. Write code to change the value of a pointer. Write code to change the value to which the pointer points.

A_1.

#include <iostream>

int main()
{
int value = ;
int *pValue = &value; // 更改指针所指对象的值
*pValue = ; // 更改指针的值
pValue = nullptr; return ;
}

Exercises Section 2.4.4

Q_1. Is the following code legal or not? If not, how might you make it legal?

A_1.

int null = , *p = null; // 非法,不能用整型变量null来初始化指向整型变量的指针p
// 修正:
int null = , *p = &null;
上一篇:C++ primer chapter 13


下一篇:C++ Primer(第五版)读书笔记 & 习题解答 --- Chapter 3