第 2 章:变量和基本类型
2.1 基本内置类型
short、int、long、long long 都是带符号的,添加 unsigned 可以得到无符号类型。
常见数据范围
更多可以参考 C++ 数据类型范围
类型名 | 其他名 | 值的范围 | 相当于 |
---|---|---|---|
short 、__int16 | short int , signed short int | -32768 到 32767 | -2**15 到 2**15-1 |
int 、__int32 | signed int signed | -2147483648 到 2147483647 | -2**31 到 2**31-1 |
long | long int 、signed long int | -2147483648 到 2147483647 | -2**31 到 2**31-1 |
long long 、__int64 | signed long long | -9223372036854775808 到 9223372036854775807 | -2**63 到 2**63-1 |
PS: long
实际上在 linux 上是 64 位,在 windows 上才是 32 位,为了更好的兼容,一般都认为是 32 位。
如何选择类型
- 当明确知晓数值不可能为负时,选用无符号类型。
- 使用 int 执行整数运算。在实际应用中,short 常常显得太小而 long 一般和 int 有一样的尺寸。如果你的数值超过了 int 的表示范围,选用 long long。
- 在算术表达式中不要使用 char 或 bool,只有在存放字符或布尔值时才使用它们。
- 执行浮点数运算选用 double,这是因为 float 通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。
类型转换
- 非布尔转布尔,
0
=>false
,其他 =>true
- 布尔转非布尔,
false
=>0
,true
=>1
- 浮点数转整型,仅保留小数点前的数
3.14
=>3
- 整型转浮点数,小数记为 0,整型过大会丢失精度
3
=>3.0
- 无符号类型超出范围,数值总数取模后的余数 如 unsigned char 表示 0 - 255,即对 256 取模,
-1
=>255
,257
=>1
- 有符号类型超出范围,结果是未定义
undefined
提示:切勿混用带符号类型和无符号类型
字面值常量
进制
0 开头表示八进制,0x 或 0X 表示十六进制
int a = 20; // 十进制
int b = 024 // 八进制
int c = 0x14 // 十六进制
引号
单引号括起来为 char 型字面值 'h'
双引号括起来为字符串字面值 "hello world"
,编译器会在结尾处添加一个空字符串\0
转义序列
指定字面值的类型
指针
nullptr是指针字面值
2.2 变量
变量定义
变量、对象、初始化
变量提供一个具名的、可供程序操作的存储空间。
对象是指一块能存储数据并具有某种类型的内存空间。
初始化不是赋值:
初始化:创建变量时赋予其一个初始值 赋值:把对象的当前值擦除,而以一个新值来替代。
列表初始化
如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错。
int a{0};
int a = {0};
默认初始化
定义于任何函数体之外的变量被初始化为 0。
定义在函数体内部的内置类型变量将不被初始化。
定义于函数体内的内置类型的对象如果没有初始化,则其值未定义。
类的对象如果没有显式地初始化,则其值由类确定。
建议初始化每一个内置类型的变量。虽然并非必须这么做,但如果我们不能确保初始化后程序安全,那么这么做不失为一种简单可靠的方法。
变量的声明和定义
- 变量声明:规定了变量的类型和名字
- 变量定义:规定了变量的类型和名字,申请存储空间,也可能会为变量赋一个初始值。
变量能且只能被定义一次,但是可以被多次声明。
extern int i; // 声明
int j; // 定义
extern int k = 1; // 定义,= 抵消了 extern
C++是一种静态类型(statically typed)语言,其含义是在编译阶段检查类型。其中,检查类型的过程称为类型检查(type checking)。
标识符
常见规范:
- 要能体现含义
- 变量名小写 camel_case、camelCase
- 自定义类名 Camel_case、CamelCase
名字的作用域
建议:当你第一次使用变量时再定义它。
如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。
2.3 复合类型
引用
定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。
引用并非对象
- 只是为一个已经存在的对象所起的另外一个名字。
- 不能定义引用的引用
引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起。
int iVal = 2014;
int &refVal1 = iVal;
int &refVal2; // 错误,必须初始化
int &refVal3 = &refVal1; // 错误,不能定义引用的引用
int & refVal4 = 12; // 错误,必须跟对象绑定
指针
指针与引用的不同点:
- 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
- 指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
int *ip1; // ip1 是一个 int 型对象的指针
double *dp1; // dp1 是一个 int 型对象的指针
获取对象地址
指针存放某个对象的地址,通过 &
取地址符使用
int val = 42;
int *P = &val;
引用不是对象,没有实际地址,所以不能定义指向对象的指针。
指针值
- 指向对象
- 指向紧邻对象所占空间的下一个位置
- 空指针,指针没有指向任何对象
- 无效指针,上述情况之外的其他值
使用解引用符号
int val = 42;
int *p = &val; // p 存放变量 val 的地址,或者说 p 是指向变量 val 的指针
cout << *p; // 解引用符号,得到指针 p 所指的对象,得到结果 42
解引用操作仅适用于那些确实指向了某个对象的有效指针。
某些符号拥有多重含义
int i = 42;
int &r = i; // 引用
int *p = &i; // 指针 取地址
int &r2 = *p; // 引用 解
空指针
nullptr
(c++ 11 新标准引入的一种方法,可以转换成任意其他的指针类型)0
(字面量 0)NULL
(cstdlib 定义的预处理变量,值就是 0)
得到空指针最直接的办法就是用字面值 nullptr
来初始化指针(建议使用这个,而不是 NULL
,NULL 在面对非 int 类型可能会有问题)
建议初始化所有的指针,不确定时指向 nullptr
,程序就能知道它没有指向任何具体对象。
赋值和指针
引用无法重新绑定别的对象,指针可以赋值新的地址。
记住赋值永远改变的是等号左侧的对象
其他指针操作
任何非0指针对应的条件值都是true。
void *
void *
是一种特殊的指针类型,可用于存放任意对象的地址
2.4 const 限定符
初始化和 const
默认,const 对象仅在文件内有效。
想要多文件共享,需要添加 extern 关键字。
const int bugSize = 512;
extern const int bugSize2 = 618;
在编译过程中,用到该变量的地方会被替换成对应的值。
const 的引用
常量引用是对 const 的引用
初始化
常量引用允许任意表达式作为初始值。
int i = 1;
const int &r1 = i;
const int &r2 = 22;
const int &r3 = i + 1;
int &r4 = i + 1; // 错误 ❌ ,引用初始值必须是一个对象。
double val = 3.14;
const int &rVal = val;
// 这种情况常量引用 rVal 绑定了一个临时量对象。可以理解为做了如下的过程
const int temp = val; // 从 double 类型生成一个临时的整型常量
const int &rVal = temp;
指针和 const
指针也可以指向常量或非常量。
常量的指针仅要求不能通过该指针改变对象的值,但是对象的值可能通过其他方式改变。
const int i = 1; // i 不能改变
const int *pi = &i; // pi 不能改变
double d = 1.5; // d 可以改变,改变后会影响下面的 pd
const double *pd = &d; // pd 不可以改变
const 指针
常量指针(const pointer)必须初始化,一旦初始化,它的值(地址)就不能改变了。
int num = 1;
int *const pNum = #
顶层 const
顶层 const(top-level const)表示指针本身是个常量,可以表示任意的对象是常量。
底层const(low-level const)表示指针所指的对象是一个常量,指针和引用等复合类型的基本类型有关。
指针类型即可以是顶层 const 也可以是底层 const。
int i = 0;
int *const p1 = &i; // 顶层 const
const int ci = 42; // 顶层 const
const int *p2 = &ci; // 底层 const
const int *const p3 = p2; // 右侧是顶层 const,左侧是底层 const
const int &r = ci; // 底层 const
constexpr 和 常量表达式
常量表达式(const expression):指值不会改变并且在编译过程就能得到计算结果的表达式。
将变量声明为 constexpr 类型以便由编译器来验证变量的值是否是一个常量表达式。
const int a = 1; // 常量表达式
const int b = a + 1; // 常量表达式
constexpr int c = 3; // 常量表达式
constexpr int d = c + 1; // 常量表达式
constexpr int e = getE(); // 当 geeE() 是一个 constexpr 函数时,才是常量表达式
如果你认定变量是一个常量表达式,那就把它声明成 constexpr 类型。
指针和 constexpr
限定符 constexpr 仅对指针有效,与指针所指的对象无关
const int *p = nullprt; // p 是一个指向整型常量的指针
constexpr int *q = nullprt; // q 是一个指向整数的常量指针
2.5 处理类型
类型别名
类型别名(type alias)是一个名字,它是某种类型的同义词
typedef double wages; // wages 是 double 的同义词
typedef wages base, *p; // base 是 double 的同义词,p 是 double* 的同义词
别名声明(alias declaration)来定义类型的别名
using SI = Sales_item;
auto 类型说明符
auto 让编译器通过初始值来推算变量的类型。所以 auto 定义的变量必须有初始值。
auto 一般会忽略掉顶层 const,保留底层 const。
auto a = b + c;
decltype 类型指示符
decltype 是选择并返回操作数的数据类型
decltype(f()) sum = x; // sum 的类型是函数 f 的返回类型
decltype((v))
双层括号的结果是引用
decltype(v)
是 v 的类型,只有 v 是引用结果才是引用。
2.6 自定义数据类型
定义 Sales_data 类型
注意 struct 后面需要封号
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
编写自己的头文件
头文件通常包含那些只能被定义一次的实体,如类、const和constexpr变量。
头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。
预处理器
预处理器:确保头文件多次包含仍能安全工作。
预处理变量有两种状态:已定义和未定义。
#define
指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:
#ifdef
当且仅当变量已定义时为真。
#ifndef
当且仅当变量未定义时为真。
将会一直执行到 #endif
指令为止。