Chapter2 变量和基本类型
2.1 基本内置类型
- 基本数据类型包括算术类型(arithmetic type)和空类型(void),算术类型包含字符、整型数、布尔值和浮点数
2.1.1 算术类型
-
算术类型分为整型(integral type, 包括整型数、字符和布尔值)和浮点型
(1) C++规定int至少和short一样大,long至少和int一样大,long long至少和long 一样大
(2) 通常,float以32 bits表示,double以64 bits表示 -
可寻址的最小内存块为"字节(byte)",存储的基本单元称为"字(word)"。大多数机器的字节由8 bits组成,字则由32或64 bits组成
-
符号/无符号
(1) 除布尔型和扩展字符型外,整型可划分为带符号的(signed)和无符号的(unsigned)两种
(2) 其中字符型分为char、signed char和unsigned char,类型char会表现为另外的其中一种(编译器决定)
-
类型选择
(1) 明确数值不可能为负时,选用无符号类型
(2) 一般使用int执行整数运算
(3) 在算术表达式中不要使用char和bool,只在存放字符或布尔值时使用
(4) 执行浮点数运算一般使用double,float精度不够且两者计算代价相差无几
2.1.2 类型转换
-
类型转换:将对象从一种给定的类型转换(convert)为另一种相关类型
(1) 把一个非布尔类型的算术值赋给布尔类型,初始值为0则结果为false,否则结果为true
(2) 把一个布尔值赋给非布尔类型,初始值为false则结果为0,初始值为true则结果为1
(3) 把一个浮点数赋给整数类型,只取整数部分
(4) 把整数值赋给浮点数类型,小数部分为0
(5) 赋给无符号类型一个超出其表示范围的值,结果是初始值对其可表示数值总数取模后的余数,如给8 bit的无符号类型赋值256,结果为
256 % 255 = 1
(6) 赋给带符号类型一个超出其表示范围的值,结果为未定义(undefined)
-
含无符号类型的表达式
(1) 算术表达式既有无符号数又有int值时,int值会转换为无符号数
(2) 把负数转换成无符号数相当于直接给无符号数赋一个负值,结果为负数加上无符号数的模
(3) 无符号数不会小于0可能影响到循环条件
for(unsigned i = 10; i >= 0; --i) cout << i << endl; // 循环条件永远成立,死循环
2.1.3 字面值常量
-
整型字面值
(1) 以0开头的整数代表八进制数,以0x或0X开头的代表十六进制数
20,024,0x14均表示20
(2) 十进制字面值的类型是int, long和long long中尺寸最小的,八进制和十六进制字面值是能容纳其值的int, unsigned int, long, unsigned long, long long和unsigned long long中的尺寸最小者
-
浮点型字面值
3.14159,3.14159E0,0.,0e0
(1) 浮点型字面值默认是一个double
(2) 使用科学计数法表示时,指数部分用E或e标识
-
字符和字符串字面值
(1) 由单引号括起来的字符称为char型字面值
(2) 双引号括起来的零或多个字符构成字符串字面值,字符串字面值实际上是由常量字符构成的数组,总由空字符
'\0'
结尾(3) 如果两个字符串字面值紧邻且仅由空格、缩进和换行符分隔,则它们实际上是一个整体,如
cout << "a really, really long string literal " "tahat spans two lines" << endl;
-
转义序列
(1) 不能直接使用的两类字符:不可打印字符(退格等),在C++有特殊含义的字符(单引号、双引号等)
(2) 使用转义序列,以反斜线开始
-
指定字面值的类型
通过添加前缀和后缀,可以改变整型、浮点型和字符型字面值的默认类型
L'a' // 宽字符型字面值,wchar_t u8"hi!" // utf-8字符串字面值 42ULL // unsigned long long 1E-3F // float 3.14159L// long double
-
布尔字面值和指针字面值
(1) 布尔字面值:true和false
(2) 指针字面值:
nullptr
2.2 变量
2.2.1 变量定义
-
变量定义的基本形式,类型说明符(type specifier)+一个或多个变量名组成的列表
-
初始化(initialized),对象在创建时获得了一个特定的值,则称这个对象被初始化了
-
区分初始化和赋值,在C++中是两个不同的概念
(1) 初始化:创建变量时赋予其一个初始值
(2) 赋值:把对象的当前值擦除,而以一个新值来代替
-
列表初始化
(1) 多种初始化形式,如定义整型变量并初始化为0
int a = 0; int a = {0}; // 列表初始化 int a{0}; // 列表初始化 int a(0);
(2) 使用列表初始化且初始值存在丢失信息风险(如将浮点数赋给整型变量)时,编译器将报错
double pai = 3.1415926; int a{pai}, b={pai}; // 编译器报错 int c(pai), d = pai; // 编译器不报错,但部分信息丢失
-
默认初始化
(1) 定义变量时没有指定初值,变量被默认初始化
(2) 内置类型的变量,定义于函数体外的变量默认初始化为0;定义于函数体内的变量不被初始化,值为未定义的
(3) 类的对象没有被显式地初始化,其值由类决定,若类不允许创建对象而不初始化,则会引发错误
2.2.2 变量声明和定义的关系
-
分离式编译(separate compilation)机制:允许将程序分割为若干个文件,每个文件可被独立编译
-
声明(declaration)使得名字为程序所知,程序使用别处定义的名字必须包含对那个名字的声明
-
定义(definition)负责创建于名字关联的实体,申请存储空间,还可能会赋初始值
extern int i; // 声明i int j; // 声明并定义j
-
给extern标记的变量赋初始值,则抵消了extern的作用,变为定义而不是声明,如
extern double pi = 3.14; // 声明
-
C++是一种静态类型语言,即在编译阶段检查类型
2.2.3 标识符
-
C++的标识符(identifier)由字母、数字和下划线组成,只能由字母或下划线开头,大小写字母敏感
-
规则
(1) 不能使用保留的关键字
(2) 不能以连续两个下划线开头
(3) 不能以下划线紧跟大写字母开头
(4) 定义在函数体外的标识符不能以下划线开头
-
规范
(1) 标识符要体现实际含义
(2) 变量名一般用小写字母
(3) 自定义类名一般以大写字母开头
(4) 由多个单词组成时,使用驼峰式或下划线式分割,但不要两者混用
2.2.4 名字的作用域
-
名字的有效区域始于名字的声明语句,以声明语句所在作用域(scope)末端为结束
-
定义在函数体外的名字(包括main)拥有全局作用域(global scope)
-
定义在函数体内的变量,拥有块作用域(block scope)
-
嵌套的作用域
(1) 被包含的作用域称为内层作用域,包含别的作用域的作用域称为外层作用域
(2) 内层作用域能访问外层作用域声明的名字
(3) 内层作用域可以重新定义外层作用域已有的名字
int a = 42; int main(){ int a = 1; // 新建局部变量a,覆盖了全局变量a cout << a << endl; // 输出1 cout << ::a << endl; // 输出42 // 使用作用域操作符(::)覆盖默认的作用域规则, 访问全局作用域中的a return 0; }
(4) 可使用作用域操作符
::
覆盖默认的作用域规则, 左侧为空时代表全局作用域(没有名字)
2.3 复合类型
- 复合类型(compound type)是指基于其他类型定义的类型,如引用和指针
- 一条声明语句由一个基本数据类型(base type)和其后的一个声明符(declarator)列表组成,每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型
2.3.1 引用
-
C++11增加了右值引用,主要用于内置类,一般情况下引用指的是左值引用
-
引用(reference)为对象起了另外一个名字,引用类型refers to另外一种类型,通过将声明符写成
&variable
的形式定义引用类型int val = 1024; int &refVal = val; // refVal指向val,作为val的另一个名字
-
定义引用时,程序把引用和其初始值绑定(bind)且此后将始终绑定在一起,而不是将初始值拷贝给引用,引用必须初始化
int &refVal; // 错误,引用必须初始化
-
引用本身不是对象,而是一个已存在对象的另一个名字,不能定义引用的引用
-
一般地,引用的类型要和与之绑定的对象严格匹配
-
引用只能绑定在对象中,不能与字面值或某个表达式的计算结果绑定在一起
int a = 1; double b = 3.14; int &refVal = 10; // error,不能绑定字面值常量 int &refVal2 = a + 3; // error,不能绑定表达式的计算结果 int &refVal3 = b; // eroor, 引用类型与绑定对象不匹配
2.3.2 指针
-
指针(pointer)是point to另外一种类型的复合类型
-
与引用的区别
(1) 指针本身是一个对象,允许对指针赋值和拷贝,且在指针的生命周期内可以先后指向不同的对象
(2) 指针无须再定义时赋初值,在块作用域内定义的指针没有被初始化,将拥有一个不确定的值
-
获取对象的地址
指针存放某个对象的地址,可使用取地址符
&
获取地址引用不是对象,不能定义指向引用的指针
int val = 42; int *p = &val;
-
一般地,指针的类型要和其所指向的对象严格匹配
-
指针值
指针的值有4个状态
(1) 指向一个对象
(2) 指向紧邻对象所占空间的下一个位置
(3) 空指针
(4) 无效指针
-
利用指针访问对象
(1) 可使用解引用符
*
访问指针指向的对象int val = 42; int *p = &val; cout << *p << endl; // 打印p指向的对象,即val,结果为42
(2) 给解引用的结果赋值,相当于给指针指向的对象赋值
*p = 40; cout << val << endl; // 结果为40
&
和*
在声明语句中用于复合类型,在表达式中用作运算符
-
空指针(null pointer)
(1) 生成空指针的方法
int *p1 = nullptr; // C++11新标准引入,推荐使用 int *p2 = 0; int *p3 = NULL; // NULL为预处理变量,在头文件cstdlib中定义,值为0 // 预处理器是运行于编译过程之前的一段程序
(2) 不允许通过int变量给指针赋值
int val = 0; int *p = val; // error
- 建议初始化所有指针,若暂不确定其指向何处,则初始化为
nullptr
-
其他指针操作
(1) 指针用于条件表达式,若为空指针则取false,否则取true
(2) 两个类型相同的合法指针,可以用相等操作符
==
或不相等操作符!=
比较它们 -
void* 指针
(1) 特殊的指针类型,可以存放任意对象的地址(没有类型限制)
// 下列语句均正确 int a = 0, *pa = a; double b = 3.14, *pb = b; void *pv = &a; // 存放int型对象a的地址 pv = &b; // 存放double型对象b的地址 pv = &pa; // 存放int型指针对象pa的地址 pv = &pb; // 存放double型指针对象pb的地址
(2) 不能直接操作void* 指针所指对象,因为其类型不确定
2.3.3 理解复合类型的声明
-
变量的定义包括一个基本数据类型(base type)和一组声明符(如类型修饰符
*
和&
) -
指向指针的指针
int a = 0; int *p = &a; int **pp = &p; // 指向指针的指针 cout << **pp << endl; // 两次解引用得到对象a的值
-
指向指针的引用
int a = 0; int *p = &a; int *&r = p; // r是对指针的引用 cout << *r << endl;
-
对于复杂的指针或引用声明语句,可从右向左阅读,如
int *&r = p; // r是引用,指向指针,该指针指向int类型对象
2.4 const限定符
-
const对象一旦创建后其值就不再改变,const对象必须初始化
const int buffSize = 1024;
-
默认情况,const对象仅在文件内有效,若多个文件出现了同名的const变量,相当于在不同文件中分别定义了独立的变量
-
在不同文件共享const变量的方法:对于const变量不管是声明还是定义都添加extern关键字
// 1.cpp, 定义const变量bufSize extern const int bufSize = 0; // 2.cpp, 声明bufSize, 与1.cpp中定义的bufSize是同一个 extern const int bufSize;
-
利用一个对象去初始化另一个对象,则它们是不是const无关紧要
一旦拷贝完成,新的对象就和原来的对象没有关系了
int i = 24; const int ci = i; // 合法 int j = ci; // 合法
2.4.1 const的引用
-
把引用绑定到const对象,称为对常量的引用(reference to const),对常量的引用不能用作修改它所绑定的对象
-
常量对象必须由常量引用指向,让非常量引用指向常量对象,将引起编译错误
但常量引用可以指向非常量对象
const int a = 0; int &ref = a; // 错误,不能将非常量引用指向常量对象
-
一般地,引用的类型必须与其所引用对象的类型一致
例外情况:初始化常量引用时允许用任意表达式作为初始值,只要能转换成引用的类型即可
int i = 42; const int &r1 = i; // 正确 const int &r2 = 42; // 正确 const int &r3 = r1 * 2; // 正确 int &r4 = r1 * 2; // 错误,非常量引用不能指向常量对象
-
临时量(temporary)对象
double val = 3.14; const int &ri = val;
编译器把上述代码转换为
double val = 3.14; const int temp = val; // 双精度浮点数生成一个临时的整型常量 const int &ri = temp; // ri绑定此临时量
若ri为非常量引用,则ri被绑定到临时量temp上,修改ri无法改变val,因此C++直接将非常量引用指向常量对象当作非法行为
-
对const的引用可能引用一个并非const的对象
常量引用并未对引用对象本身是不是一个常量作限定
int i = 42; const int &ri = i; ri = 41; // 错误, 不能通过从常量引用改变引用的对象 i = 40; // 正确 cout << ri << endl; // 结果为40
2.4.2 指针和const
- 指向常量的指针并非常量对象,而是其自认为指向的是常量对象,可以不初始化
- const指针是常量对象,必须初始化
-
指向常量的指针不能用于改变其所指对象的值
-
存放常量对象的地址,只能使用指向常量的指针
但指向常量的指针,也可以存放非常量对象的地址
-
一般地,指针的类型必须与其所指向对象的类型一致
例外情况:允许一个指向常量的指针指向一个非常量对象
double val = 3.14; const double *ref = &val;
- 所谓指向常量的指针和引用,是其自以为指向了常量,所以不去改变所指对象的值,但不保证所指对象为常量
-
const指针
常量指针(const pointer)必须初始化,其值(即存放在指针中的地址)不再改变,而非指向的对象不再改变
double pi = 3.14; double *const pip = π *pip = 3.14159; // 正确, 没有改变指针的值 pip = nullptr; // 错误,改变了指针的值
-
判断语句是否合法
const int ic, &r = ic; // 不合法,ic必须初始化
const int a = 0; const int *ref1 = &a; int *ref2 = ref1; // 不合法,不能用指向常量指针初始化普通指针
2.4.3 顶层const
-
顶层const表示指针本身是常量,底层const表示指针所指对象是常量对象
-
进一步地,顶层const表示任意对象是常量,底层const表示引用、指针等复合类型的基础类型部分为常量
-
执行拷贝操作时,拷入和拷出对象必须具有相同的底层const,或两个对象的数据类型能相互转换,一般非常量可转换为常量,反之则不行
int a = 10; int *r2 = &a; const int *r1 = r2; // 合法,r2底层const为非常量,可转换为常量 r2 = r1; // 非法,r1底层const为常量,不可转换为非常量
2.4.4 constexpr和常量表达式
-
常量表达式(const expression)指值不会改变并且在编译过程就能得到计算结果的表达式
const int sz = get_size(); // sz不是常量表达式, 只有在运行时才能取到该值
-
声明为constexpr类型的变量,一定是个常量,且必须用常量表达式初始化
-
字面值类型包含算术类型、引用和指针, 其中constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象(一般是定义在函数体外的对象)
-
指针和constexpr
const int *p = nullptr; // p是一个指向整型常量的指针 int *const a = nullptr; // a是一个指向整数的常量指针 constexpr int *q = nullptr; // q是一个指向整数的常量指针 constexpr i = 0; // i是定义在函数体外的整型常量 constexpr const int *p1 = &i;// p1是常量指针,指向整型常量
2.5 处理类型
2.5.1 类型别名
-
类型别名(type alias)是一个名字,是某种类型的同义词
-
定义类型别名的方法
(1) 使用关键字typedef
typedef double wages; // wages是double的同义词 typedef wages base, *p; // base是wages的同义词,p是double*的同义词 wages jan = 1000.5; p a = &jan; // a为指向double类型变量的指针 cout << *a << endl;
(2) 别名声明(alias declaration)
using SI = Sales_item; // SI是Sales_item的别名
-
若某个类型别名指代复合类型或常量,则用于声明语句时应谨慎
using pstring = char*; // pstring是指向char类型的指针类型 const pstring cstr = 0; // cstr是指向char类型的常量指针 // 不可理解为const char *cstr = 0, 此时cstr为指向char型常量的指针 const pstring *ps; // ps是指针, 指向常量指针, 该常量指针指向char变量
2.5.2 auto类型说明符
-
auto类型说明符让编译器通过初始值推算变量的类型
auto item = val1 + val2; // 根据val1和val2相加结果推断item类型
-
auto可在一条语句声明多个变量,但其初始值数据类型必须一致
auto i = 0, *p = i; // 正确,均为int类型 auto a = 1, b = 3.14; // 错误 const int ci = i; auto &n = i, *p2 = &ci; // 错误, i为int类型, ci为const int类型
-
使用引用初始化auto类型时,编译器以引用对象的类型作为auto的类型
int i = 0, &r = i; auto a = r; // a为int型变量
-
auto一般忽略顶层const,但保留底层const
int i = 0; const int ci = i, &cr = ci; auto a = ci; // a为int型 auto b = cr; // b为int型 auto c = &i; // c为指向int型的指针 auto d = &ci; // d为指向int型常量的指针(保留底层const)
-
可通过const auto声明顶层const
const auto f = ci; // ci的推演类型为int, f为const int型
-
设置类型为auto的引用时,初始值中的顶层常量属性仍然保留
const int i = 0; auto &r = i; r = 42; // 错误, r为指向常量的引用
2.5.3 decltype类型指示符
-
希望从表达式的类型推断出要定义的变量的类型,但并不想用该表达式的值初始化变量,可使用decltype类型说明符
-
decltype类型说明符选择并返回操作数的数据类型
decltype(f()) sum = x; // sum的类型为函数f的返回类型
-
decltype使用表达式时,返回表达式结果对应的类型
int i = 42, &r = i; decltype(r + 0) b; // r+0为表达式, 其结果为int型, 故b为int型 decltype(r) c; // r为指向int的引用, 故c为int&型, 必须初始化
-
如果表达式的内容是解引用操作,则decltype将得到引用类型
int i = 0, *p = i; decltype(*p) a = i; // a为int&型, 必须初始化
-
如果给变量加上了一层或多层括号,编译器将把它当作一个表达式,decltype会得到引用类型
int i = 10; decltype((i)) a = i; // a为int&型
decltype((variable))的结果永远是引用, decltype(variable)的结果只有当varibale为引用时才是引用
-
decltype只会返回表达式的类型,而不会计算表达式
int a = 3, b = 4; decltype(a = b) c = a; // 赋值表达式会产生引用, 引用类型为左值的类型, 故c为int&型 // decltype不会执行表达式, 故a = b并未执行, 程序结束后a = 3, b = 4
2.6 自定义数据结构
-
类以关键字struct开始,紧跟着类名和类体,以分号结尾
-
数据成员(data member),可以为数据成员提供一个类内初始值(in-class initializer)
struct SalesData{ string isbn; unsigned unitsSold = 0; double revenue = 0.0; }; int main() { SalesData book1 = {"1", 1, 30}; // 或SalesData book1{"1", 1, 30}; // 不能写成SalesData book1("1", 1, 30); return 0; }
2.6.3 编写自己的头文件
-
为了确保各个文件中类定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一致
-
预处理器(preprocessor)确保头文件多次包含仍能安全工作,包含#include,头文件保护符等
-
头文件保护符(header guard)依赖于预处理变量,预处理变量有已定义和未定义两种状态
(1)
#define
指令把一个名字设定为预处理变量(2)
#ifdef
当且仅当预处理变量已定义时为真(3)
#ifndef
当且仅当预处理变量未定义时为真(4)
#endif
结束对应的#ifdef
或#ifndef
#ifndef SALES_DATA_H #define SALES_DATA_H #include <string> using namespace std; struct SalesData{ string isbn; unsigned unitsSold = 0; double revenue = 0.0; }; #endif
预处理变量无视作用域规则