《C++ Primer》读书笔记 第二章 变量和基本类型

第二章 变量和基本类型

2.1 基本内置类型

2.1.1 算术类型

  • 包括整型浮点型
  • C++标准规定的算术类型允许的最小尺寸(编译器实际可以赋予它们更大的尺寸):

  • int至少和short一样大。
  • long至少和int一样大。
  • long long至少和long一样大。(C++11)

  • 字节byte:可寻址的最小内存块,通常是8 bits。一个字节要至少能容纳机器基本字符集中的字符。

  • word:存储的基本单元,通常是32 bits或64 bits,即4 bytes或8 bytes。

  • 计算机以比特序列存储数据,大多数计算机以2的整数次幂作为块来处理内存。为了赋予内存中某个地址明确的含义,必须首先知道存储在该地址的数据的类型。类型决定了数据所占的比特数以及该如何解释这些比特的内容。

  • intshortlonglong long区分带符号类型(signed)和无符号类型(unsigned)。

  • char区分charsigned charunsigned char三种。由具体编译器决定char属于另外两种中的哪一种,所以一般不要使用char进行算术运算。

  • C++标准没有规定带符号类型应如何表示,但是约定了正负范围应该平衡。

使用建议:

  1. 明确知道数值不可能为负值时,使用无符号。
  2. 使用int执行整数运算。
  3. 不适用charbool进行算术运算。
  4. 浮点运算使用double。它与float相比精度更高,计算代价相差无几甚至能更快,long double代价比较大。

2.1.2 类型转换

  • 给无符号类型一个超过范围的值,将得到取模后的余数。
  • 给带符号类型一个超过范围的值,结果是未定义的。

  • 有符号数与无符号数一起运算,有符号数会先转化成无符号数。

  • 无符号减去一个数,无论这个数是不是无符号,都必须确保结果不能是个负值。

  • 切勿混用带符号类型和无符号类型。

2.1.3 字面值常量

  • 十进制字面值是带符号的,具体类型是intlonglong long中能容纳的前提下尺寸最小的那个。
  • 然而十进制字面值一般不会是负数,负号看作运算符。
  • 八进制和十六进制不一定带符号。
  • 若都容纳不下,会出现错误。

  • 浮点型字面值是一个double,表示方法有:3.143.14E00.0e0.001

  • 字符字面值:a

  • 字符串字面值:abc
  • 字符串可以自动连接:
1
2
std::cout << "a really, really long string literal "
"that spans two lines" << std::endl;
  • 转义序列:\n\t\a\v\b\"\\\?\'\r\f
  • 泛化的转义序列:\x+1个或多个十六进制数字,或者\+1~3个八进制数字。注意,前者会判定所有的数字,可能造成超过范围而报错,一般是配合扩展字符集使用的。后者最多只判定3个数字。

  • 指定字面值类型的前缀和后缀:

  • 字面值还包括truefalsenullptr

2.2 变量

变量提供一个具名的、可供程序操作的存储空间。

2.2.1 变量定义与初始化

1
2
3
4
int sum = 0, value,
units_sold = 0;
Sales_item item;
std::string book("abcd");
  • 本书不严格区分变量和对象,对象是一块能存储数据并具有某种类型的内存空间。
  • 初始化和赋值是两个完全不同的操作。初始化是创建变量时赋予其一个初始值。赋值是把对象的当前值擦除,用一个新值替代。

  • 初始化的几种形式:

1
2
3
4
int a = 0;
int a = {0}; // C++11
int a{0}; // C++11
int a(0);
  • 列表初始化:如果会丢失精度,编译器会报错。

  • 默认初始化:定义于任何函数体之外的变量被初始化为0;定义在函数体内的内置类型变量将不被初始化(值未定义)。

  • 类的对象如果没有显式初始化,其值将取决于类。如std::string默认为空字符串。

2.2.2 变量声明和定义的关系

  • C++支持分离式编译。
  • 变量声明规定了变量的类型和名字。变量定义还申请了存储空间,也可能进行初始化。
  • 变量声明的方式:使用extern,且不要显式初始化,否则就是定义了。并且如果在函数体内试图初始化一个extern变量,还会报错。
  • 变量的定义必须出现在且只能出现在一个文件中。

2.2.3 标识符

2.2.4 名字的作用域

  • 全局作用域:如main函数,声明后在整个程序的范围内都可使用。
  • 块作用域:如main函数中定义的变量,从声明开始到函数结束为止都可以访问。
  • 嵌套作用域:内层能访问外层的名字,也可以对该名字进行重新定义。如果重新定义后想获取全局作用域中声明的变量,可使用作用域操作符(全局作用域本身没有名字):
1
std::cout << ::reused << std::endl;
  • 建议:当你第一次使用变量时再定义它。

2.3 复合类型

2.3.1 引用

  • 引用和初始值是绑定的,而不是对初始值的拷贝,且无法重新绑定,所以引用必须初始化。
  • 引用即别名。赋值和取值使用的都是绑定的对象的值。
  • 引用本身不是一个对象,因此不能定义引用的引用,也不能与字面值或表达式的计算结果相绑定。
  • 除了两种例外情况,其它所有的引用的类型都要和与之绑定的对象类型严格匹配。(2.4.1、15.2.3

2.3.2 指针

  • 指针本身就是一个对象。
  • 定义时无须赋初值,这样如果在块作用域内定义的话会拥有不确定的值。
  • 获取某个对象的地址使用取地址符&,根据指针获取指向的对象使用解引用符*
  • 除了两种例外情况,指针类型也要和所指向的对象严格匹配。(2.4.2、15.2.3
  • 使用空指针:
1
2
3
4
5
6
int *p1 = nullptr;  // C++11,推荐
int *p2 = 0;
int *p3 = NULL; // <cstdlib>,不推荐

int zero = 0;
pi = zero; // 错误
  • void*指针可以存放任意对象的地址,但是对对象的类型不了解。

2.3.3 理解复合类型的声明

  • 一个语句中定义多个变量时要注意类型修饰符*&不能省略:
  • 指针和应用声明的两种写法:
1
2
int *p; 
int* p;
  • 存在指向指针的指针,不存在指向引用的指针。
  • 存在指针的引用:
1
2
int *p;
int *&r = p; //从右向左阅读,最近的符号对变量的类型有最直接的影响。

2.4 const限定符

  • const对象一旦创建后其值就不能再改变,所以它一定要初始化。
  • 常量特征仅仅在执行改变该const变量时才会发挥作用。

如果多个文件共享一个const对象,即在一处定义,其余地方使用extern引用:

  • 默认情况下,编译器必须知道该变量的初始值,并在编译过程中把用到该变量的地方都替换成对应的值。这就要求每个文件都必须得能访问到它的初始值才行。所以const对象被设定为仅在文件内有效,等同于在不同文件中分别定义了独立的变量。
  • 如果不希望这样,即const变量初始值不是常量表达式,且希望在文件中共享,又不希望为每个文件生成独立的变量。可以在定义变量时也加上extern
1
2
3
4
// file_1.cc
extern const int bufSize = fcn();
// file_1.h
extern const int bufSize;

2.4.1 const引用

  • 对常量的引用:把引用绑定到const对象上,不能被用作修改它所绑定的对象。
1
2
3
4
const int ci = 1024;
const int &r1 = ci; // 正确
r1 = 42; // 错误
int &r2 = ci; // 错误
  • 引用类型可以与所引用对象的类型不同的一种例外情况:初始化常量引用时允许用任意表达式作初始值,只要表达式的结果能转换成引用类型。
1
2
3
int i = 42;
int &r1 = i * 2; // 错误
const int &r2 = i * 2; // 正确
  • 常量引用可以引用非const的对象,这意味着虽然不能通过常量引用改变该对象的值,但还是可以通过其他方式对其修改的。

2.4.2 指针和const

  • 指向常量的指针:类似常量引用,不能被用作修改它所指向的对象的值。
  • 使用指向常量的地址只能使用指向常量的指针。
1
2
3
const double pi = 3.14;
double *ptr = &pi; // 错误
const double *cptr = &pi; // 正确
  • 指向常量的指针同样可以指向非const对象,同样意味着还是可以通过其他方式对该对象进行修改的。

  • 常量指针:因为指针是对象而引用不是,因此允许把指针本身定为常量,常量指针必须初始化。

1
2
3
int err = 0;
int *const curErr = &err;
const int *const constCurErr = &err; // 记得从右向左阅读

2.4.3 顶层const

  • 顶层const:指针本身是一个常量。
  • 底层const:指针所指对象是一个常量。

当执行拷贝操作时:

  • 顶层const不受什么影响。
  • 底层const必须具有相同的底层const资格,或者能够转换(非常量可以转换成常量)。
1
2
3
4
5
6
7
8
9
10
11
12
int i = 0;
int *const p1 = &i; // 顶层

const int ci = 42; // 顶层
const int *p2 = &ci; // 底层

const int *const p3 = p2; // 底层 顶层

int *p = p3; // 错误
int &r = ci; // 错误
p2 = p3; // 正确,底层const资格一样,顶层不受影响
p2 = &i; // 正确,int *转换成const int*
  • 总结:对指针,不能改变所指向对象的值的指针不能赋值给能改变指向对象的值的指针。

2.4.4 constexpr和常量表达式

  • 常量表达式:值不会改变并且在编译过程就能得到计算结果的表达式。
1
2
3
const int max = 20;         // max是常量表达式。
const int limit = max + 1; // limit是常量表达式。
const int sz = get_size(); // sz不是常量表达式,因为在运行时才能确定值。
  • 为了避免const修饰的常量在编译阶段不能确定值得情况,引入constexpr
1
2
constexpr int mf = 20;
constexpr int sz = get_size(); // get_size()是一个constexpr函数时才正确。
  • 声明为constexpr的对象类型不能太复杂,需要是显而易见、容易得到的,称为“字面值类型”。目前接触的类型中包括:算术类型、引用和指针。不包括自定义类、IO库、string。其它的字面值类型还有7.5.6、19.3
  • 定义为constexpr的指针初始值必须是0nullptr或存储于固定地址的对象(6.1.1,函数体外的对象,或者函数体内定义的有效范围超过函数本身的变量)。

  • 注意:

1
2
3
4
5
6
7
8
const int *p = nullptr;         // 指向整型常量的指针,底层const
constexpr int *q = nullptr; // 指向整数的常量指针,相当于顶层const

int j = 0; // 函数体外定义
constexpr int i = 42; // 函数体外定义

constexpr const int *p = &i; // 指向整型常量的常量指针
constexpr int *p1 = &j; // 指向整数j的常量指针

2.5 处理类型

2.5.1 类型别名

  • 使用关键字typedef
1
2
typedef double wages;
typedef wages base, *p;
  • 使用别名声明:(C++11)
1
using SI = Sales_item;

注意:

1
2
3
typedef char *pstring;
const pstring cstr = 0; // 指向char的常量指针
const char *cstr = 0; // 指向const char的指针

2.5.2 auto类型说明符

  • auto:让编译器根据初始值推算变量类型,所以必须初始化。
  • 如果要在一个语句中声明多个变量,那么变量类型要相同。
1
auto sz = 0, pi = 3.14;     // 错误
  • 如果使用引用,起作用的是引用对象的值。
  • auto一般会忽略顶层const,除非使用const auto,而底层const会被保留下来。
1
2
3
4
5
6
7
int i = 0;
const int ci = i, &cr = ci;
auto b = ci; // 相当于int
auto c = cr; // 相当于int
auto d = &i; // 相当于int*
auto e = &ci; // 相当于const int*
const auto f = ci; // 相当于const int
  • 如果要在一条语句中定义多个变量,要注意类型一致问题。
1
2
3
auto k = ci, &l = i;    // int
auto &m = ci, *p = &ci; // const int
auto &n = i, *p2 = &ci; // 错误,int 和 const int

2.5.3 decltype类型指示符

  • 返回操作数的数据类型,如果表达式是函数调用,并不会真正调用这个函数。
1
decltype(f()) sum = x;
  • 如果该表达式是个变量,和auto不同的是,它返回的类型是包括顶层const和引用在内的。所以在这里和通常情况不一样,引用并不能作为所指对象的同义词。
1
2
3
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // const int
decltype(cj) y = x; // const int&,所以必须初始化。
  • 如果该表达式不是变量,则返回表达式结果对应的类型。
1
2
3
4
int i = 42, *p = &i, &r = i;
decltype(r) a = i; // int&,必须初始化
decltype(r + 0) b; // int
decltype(*p) c = i; // int&,解引用得到的类型是引用
  • 如果表达式是个变量,加上括号就是表达式了,可能会改变最后的值的类型。(双层括号永远是引用)
1
2
decltype((i)) d;            // int&,所以错误,必须初始化
decltype(i) e; // int

2.6 自定义数据结构

2.6.1 定义Sales_data类型

1
2
3
4
5
struct Sales_data{
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
  • 不推荐类的定义和对象的定义写在一起。
  • 可以为数据成员提供一个类内初始值。没有初始值的成员将被默认初始化。
  • 类内初始值和之前(2.2.1)介绍的一样,要么放在花括号中,要么放在等号后面,不能用圆括号。

2.6.2 使用Sales_data类型

2.6.3 编写自己的头文件

使用头文件保护符。

1
2
3
4
5
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
....
#endif

作者说

避免无法预知和依赖于实现环境的行为。

习题

  • 2.1 认识变量类型。
  • 2.2 认识变量类型。
  • 2.3 整型计算。注意溢出
  • 2.4 整型计算。
  • 2.5 识别字面值的类型。
  • 2.6 识别字面值的类型。
  • 2.7 识别字面值的类型。
  • 2.8 转义字符。
  • 2.9 变量定义。
  • 2.10 变量初值。
  • 2.11 区分声明还是定义。int i;extern int i = 0;都是定义
  • 2.12 变量名规则。
  • 2.13 变量作用域。
  • 2.14 变量作用域。
  • 2.15 引用的初始化。
  • 2.16 赋值语句。
  • 2.17 赋值语句。
  • 2.18 指针的理解。
  • 2.19 指针和引用的区别。
  • 2.20 *的理解。
  • 2.21 指针的定义。
  • 2.22 指针的理解。
  • 2.23 指针的合法性。
  • 2.24 指针类型。
  • 2.25 变量、指针、引用的定义。
  • 2.26 const 变量的理解。
  • 2.27 const 变量的初始化。Review
  • 2.28 顶层、底层 const 的定义。Review
  • 2.29 顶层、底层 const 的使用。
  • 2.30 顶层、底层 const 的区分。Review
  • 2.31 顶层、底层 const 的使用。
  • 2.32 null的理解。
  • 2.33 autoauto&
  • 2.34 autoauto&
  • 2.35 auto类型推断。
  • 2.36 decltype()decltype(())
  • 2.37 decltype()decltype(())
  • 2.38 decltype()auto的区别。
  • 2.39 定义类要写;
  • 2.40 自己实现一个Sales_data类。
  • 2.41 使用自己的Sales_data类。
  • 2.42 自己写一个Sales_data.h文件并使用。