2.1 基本内置类型
算术类型分为两类:整型 和 浮点型
C++:算术类型 | ||
类型 | 含义 | 最小尺寸 |
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位有效数字 |
如何选择类型:
类型所能表示的值的范围决定了转换的过程:
- 当明确知晓数值不可能是负数时,选用无符号类型;
- 使用int执行整数运算。一般long的大小和int一样,而short常常显得太小。除非超过了int的范围,选择long long。
- 算术表达式中不要使用char或bool。(因为类型char在一些机器上是有符号的,而在另一些机器上又是无符号的)
- 浮点运算选用double。(因为float通常精度不够,而且双精度浮点数和单精度浮点数的计算代价相差无几)
- 当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为0则结果为false,否则结果为true。
- 当我们把一个布尔值赋给非布尔类型时,初始值为false 则结果为0,初始值为true则结果为1。
- 当我们把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分。
- 当我们把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
- 当我们赋给无符号类型一个超出它表示范围的值时,结果是 初始值 对无符号类型表示数值总数 取模后的余数。(例如,8比特大小的unsigned char 可以表示0至255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1 赋给8比特大小的unsigned char 所得的结果是255)
- 当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。
注:当一个算术表达式中 既有无符号数又有无符号数 时,有符号数就会转换成无符号数。
字面值常量:
- 整型字面值(20、024、0x24)
- 浮点数字面值(3.14、3.14f、3.14l)
- 字符字面值和字符串字面值('A'、“AAAA”)
- 转义序列(\n、\t、\b)
- 布尔字面值和指针字面值(true、false;nullptr)
2.2 变量
- 定义形式:类型说明符 + 一个或多个变量名组成的列表。(如int sum = 0, value, units_sold = 0)
- 初始化:对象在创建时获得了一个特定的值。
- 列表初始化:使用花括号{ }。(如int units_sold{0})
- 默认初始化:定义时没有指定初始值会被默认初始化;在函数体内部的内置类型变量将不会被初始化。(建议初始化每一个内置类型的变量)
注:初始化不是赋值!(初始化 = 创建变量 + 赋予初始值;赋值 = 擦除对象的当前值 + 用新值代替)
变量的 声明 vs 定义:
为了支持分离式编译,C++将声明和定义区分开。声明 使得名字为程序所知。定义 负责创建与名字关联的实体。使用 extern 只是说明变量定义在其他地方。如果只声明而不定义,则在变量名前添加 关键字extern (如extern int i);但如果包含了初始值,就变成了定义。(extern double pi = 3.14(),也就抵消了extern的作用)变量只能被定义一次,但是可以多次声明。
C++是一种静态类型语言,其含义是在编译阶段检查类型。
- 左值:可以出现在赋值语句的左边或者右边,比如变量。
- 右值:只能出现在赋值语句的右边,比如常量。
- 名字的作用域:同一个名字在不同的作用域中可能指向不同的实体。名字的有效区域 始于名字的声明语句,以声明语句所在的作用域末端为结束。
2.3 复合类型
引用:引用是对象的别名(引用本身不是对象)
- 必须初始化
- 无法重新绑定另一个对象(引用初始化后就和初始值 绑定 在一起了,而不是简单赋值)
- 只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起
指针:指针是指向任何其他类型对象的对象(指针本身就是对象,存放被指向对象的地址)
- 不能定义指向引用的指针(引用不是对象,没有实际地址)
- 不能访问一个没被初始化指针的值(没初始化的指针其值是不确定的)
void*指针可以存放任意对象的地址,其他指针类型必须要与所指对象严格匹配。
空指针不指向任何对象。(被 字面值nulltptr 初始化的指针就是空指针)
两个指针相减的类型是ptrdiff_t。
建议:初始化所有指针。
空指针不指向任何对象。(被 字面值nulltptr 初始化的指针就是空指针)
两个指针相减的类型是ptrdiff_t。
建议:初始化所有指针。
int i = 42; int &r = i; // &紧随类型名出现,因此是声明的一部分,r是一个引用 int p; // *紧随类型名出现,因此是声明的一部分,P是一个指针 P = &i; // &出现在表达式中,是一个取地址符 *p = i; // *出现在表达式中,是一个解引用符 int &r2 = *p; // &是声明的一部分,*是一个解引用符注:在声明语句中,&和*用于组成复合类型;在表达式中,它们的角色又转变成运算符。
int i = 1; int *p; // p是一个int型指针 int *&r = p; // r是一个对指针p的引用(离变量名最近的符号(&)对变量类型有最直接的影响,因此r是一个引用,声明符的剩余部分(*)用以确定r的类型是什么,所以最终可知r引用的是一个指针) r = &i; // r引用了一个指针,因此给r赋值&i就是令p指向i *r = 0; // 解引用r得到i,也就是p指向的对象,将i的值改为0
注:面对一条比较复杂的指针或引用的声明语句时,从右向左阅读有助于弄清楚它的真实含义。
2.4 const限定符
动机:希望定义一些不能被改变值的变量。
(1) 初始化和const:
- const对象必须初始化,且不能被改变
- 默认情况下const变量不能被其他文件访问;若非要访问,则对于const变量不管是声明还是定义都要添加extern关键字
(2) const的引用:
- 对常量的引用:指向const对象的引用。(如 const int ival=1; const int &refVal = ival; 可以读取但不能修改refVal)
- 临时量对象:当编译器需要一个空间来暂存表达式的求值结果时,临时创建的一个未命名的对象。(对临时量的引用是非法行为)
引用的类型必须与其所引用对象的类型一致,但是有两个例外。
int i = 42;. const int &r1 = i; // 允许将 const int&绑定到一个普通 int对象上 const int &r2 = 42; // 正确: r2是一个常量引用 const int &r3 = r1 * 2; // 正确: r1、r3都是常量引用 int &r4=r1 * 2; // 错误: r4是一个普通的非常量引用 const double& r5 = i; // 正确: int可以转换为double类型
- 在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。
- 允许为一个常量引用绑定非常量的对象、字面值、甚至是个一般表达式。
注:常量引用并不是说引用本身是常量,因为引用都不是一个对象!所以常量引用指的是 引用的对象是个常量。
对const的引用可能引用一个并非const的对象
int i = 42; int &r1 = i; // 引用r1绑定对象i const int &r2 = i; // r2也绑定对象i,但是不允许通过r2修改i的值 r2 = 0; // 错误: r2是一个常量引用 r1 = 0; // 正确: r1并非常量,i的值修改为0(不允许通过r2修改i的值。尽管如此,i的值仍然允许通过其他途径修改)注:必须认识到,常量引用仅对 引用可参与的操作 做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值。
(3) 指针和const:
- 指针常量:指针指向一个常量对象,不能用于改变其所指对象的值。(如 const double i = 3.14; const double *p = &i;)
- 常量指针:指针本身是常量,指针本身不可改变。(如 int i = 0;int *const p = &i;)
double i = 3.14; const double* p = &i; // 正确允许一个指向常量的指针指向一个非常量对象。
注:对除指针以外的其他变量声明语法来说 const type name 与 type const name 效果是相同的,即都表示声明一个 类型为type名为name 的常量。
(4) 顶层const
顶层const:指针本身是个常量
底层const:指针指向的对象是个常量。
底层const:指针指向的对象是个常量。
拷贝时严格要求相同的底层const资格
int i = 1;
const int* p = &i;
int* ptr1 = p; // 错误: "const int *"类型的值不能用于初始化"int *"类型的实体
const int* ptr2 = p; // 正确
const int n = 1;
int &r1 = n; // 错误: 将"int &"类型的引用绑定到"const int"类型的初始值设定项时,限定符被丢弃。
const int& r2 = n; // 正确
(5) constexpr和常量表达式
常量表达式:指值不会改变,且在编译过程中就能得到计算结果的表达式。C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量的表达式。
一个对象(或表达式)是不是常量表达式由它的 数据类型和初始值 共同决定。
声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。
constexpr把它所定义的对象置为顶层const。
const int max_files = 20; // max_files是常量表达式, const int limit = max_files + 1; // limit是常量表达式 int staff_size = 27; // staff_size不是常量表达式(尽管staff_ size 的初始值是个字面值常量,但它的数据类型只是一个普通 int而非const int) const int sz = get_size() ; // sz不是常量表达式(尽管sz本身是一个常量,但它的具体值直到运行时才能获取到)
constexpr int mf = 20; // 20是常量表达式. constexpr int limit = mf + 1; // mf + 1是常量表达式 constexpr int sz = size(); // 只有当size是一个constexpr函数时,才是一条正确的声明语句
int j = 0; constexpr int i = 42; // i的类型是整型常量 // 注: i和j都必须定义在函数体之外。 constexpr const int *p = &i; // p是常量指针,指向整型常量i constexpr int *p1 = &j; // pl是常量指针,指向整数j
2.5 处理类型
(1) 类型别名
- 传统别名:使用typedef来定义类型的别名。(如:typedef double wages;)
- 新标准别名:使用using来定义类型的别名。(如: using SI = Sales_item;(C++11))
注:后面俩句并不等价!
typedef char *pstring; const patring cstr=0; // cstr是指向char的常量指针(const修饰pstring这个指针(char *),所以数据类型为 const char*,因此cstr是指向char常量指针) const char *cstr=0; // cstr是指向一个常量对象的指针(const修饰char,所以数据类型为const char ,因此cstr是指向一个常量对象的指针)
(2)auto类型说明符
auto让编译器通过 计算初始值 来推算变量的类型。
1. 用auto进行类型推导时(非引用)会忽略顶层const、保留底层const。
const int a = 1; auto b = a; // b为 int 类型(忽略顶层const属性) auto c = &a; // c是 const int* 类型(保留底层const属性)2. 用auto推导引用类型时,实际推导的是引用所绑定的对象;而被绑定的对象即使包含顶层const,也会被忽略掉。
const int a = 1; const int& y = a; auto b = y; // b为 int 类型(实际推导的是引用所绑定的对象,并忽略顶层const属性)3. 用auto类型推导,定义引用时,需要显示地定义引用,并且会保留顶层const。
const int a = 1; auto& y = a; // y为const int类型的引用(保留了a的顶层const,而y本身变成了底层const)4. 用auto定义多个变量时,*和&只属于声明符,而不是数据类型的一部分;因此类型必须一致。
auto &q = a, *p = &a; // 正确: q是对const int的引用,p是指向const int的指针 auto &c = b, *d = &a; // 错误: c的数据类型为int,d的数据类型为const int
(3)decltype类型指示符
decltype 分析表达式的类型 推断并返回操作数的数据类型。
1. 当使用 decltype(var) 的形式时,decltype会直接返回变量的类型(包括顶层const和引用)。
const int i = 0, &j = i; // i为const int,j为const int& decltype(i) x = 0; // x的类型是const int decltype(j) y = x; // y的类型是const int&2. 当使用 decltype(expr) 的形式时,decltype会返回表达式结果对应的类型。
int i = 42, *p = &i, &r = i; // i是int类型变量,p是int类型指针,r是int类型引用(它们类型都为int,只是声明符不同而已) decltype(r + 0) b; // b为int(算术表达式返回右值,即int) decltype((i)) ri = i; // ri为int&(加了括号变量i变成了表达式,因此返回的是i的左值形式,即int&) decltype(*p) c = i; // c是一个int类型引用
- 解引用运算符*作用于指针类型,得到了p指向的对象的左值。因此decltype(*p)得到的类型是int &。
- 当变量i 加一个括号后,是作为表达式 。返回的是该变量的一个左值形式。(因为该表达式的结果可以作为赋值语句的左侧的值,所以是左值,因此得到类型的左值引用)
3. 当使用 decltype(函数指针) 的形式时,decltype会返回函数对应的类型。
int fun(int a,int b); decltype(fun) *pf = fun; // pf为int(*)(int,int)类型
decltype加指针也会返回指针的类型。
decltype加数组,不负责把数组转换成对应的指针,所以其结果仍然是个数组。
总之decltype(var)完美保留了变量的类型。
decltype加数组,不负责把数组转换成对应的指针,所以其结果仍然是个数组。
总之decltype(var)完美保留了变量的类型。
2.6 自定义数据结构
- 可以为类数据成员提供一个 类内初始值(C++11)。
- 当预处理器看到#include标记时,会用指定的头文件内容代替#include
- 头文件:通常包含那些只能被定义一次的实体:类、const和constexpr变量。
- 预处理器:确保头文件多次包含仍能安全工作。
- 头文件保护符:依赖于预处理变量的状态:已定义和未定
#ifndef SALES_DATA_H #define SALES_DATA_H strct Sale_data{ ... } #endif#define指令:把一个名字设定为预处理变量
#ifdef指令:当且仅当预处理变量已定义时为真
#ifndef指令:当且仅当变量未定义时为真
#endif指令:一旦检查结果为真,则执行后续操作直至遇到#endif指令为止
#endif指令:一旦检查结果为真,则执行后续操作直至遇到#endif指令为止