基本内置类型
C++定义了 算术类型 (arithmetic type) 和 空类型 (void)。算术类型包括 字符、整型数、布尔值和浮点数。空类型不对应具体的值,仅用于一些特殊的场合,如函数不返回任何值的时候。
算术类型
分为两类:整型(integral type,包括字符和布尔类型在内) 和 浮点型。
类型 | 含义 | 最小尺寸 |
---|---|---|
bool | 布尔类型 | 未定义 |
char | 字符 | 8位 |
wchar_t | 宽字符 | 16位 |
char16_t | Unicode字符 | 16位 |
char32_t | Unicode字符 | 32位 |
short | 短整型 | 16位 |
int | 整型 | 16位 |
long | 长整型 | 32位 |
long long | 长整型 | 64位 |
float | 单精度浮点数 | 6位有效数字 |
double | 双精度浮点数 | 10位有效数字 |
long double | 扩张精度浮点数 | 10位有效数字 |
char:可以存放机器基本字符集中任意字符对应的数字,即 大小和一个机器字节一样
wchar_t:可以存放机器最大扩张字符集中的任意一个字符
char16_t 和 char32_t:为 Unicode 字符集服务(Unicode是用于表示所有自然语言中的标准)。
C++语言规定,一个 int 至少和一个 short 一样大,一个 long 至少和一个 int 一样大,一个 long long 至少和一个 long 一样大。long long 是 C++ 11中新定义的。
C++标准指定了浮点数有效位数的最小值,大多数编译器都实现了更高的精度。float 以一个字表示(32 位),double以2个字表示(64位),long double 以 3 或 4 个字表示(64或128位)。float 和 double 分别有 7 和 16 个有效位; long double 常用于有特殊浮点数要求的硬件。
带符号类型和无符号类型
int、short、long 和 long long可以划分为 带符号的 和 无符号的(前面加unsigned),带符号的可以表示正数、负数和0,无符号的仅能表示大于等于0的值。
char 分了三种:char、signed char 和 unsigned char。尽管分三种,但实际上表现形式只有两种:带符号(-128~127) 和 无符号(0~255),由编译器决定使用哪一种。
类型转换
类型所能表示的值的范围决定了转换的过程:
- 非布尔值的算术值->布尔类型:0->false,非0->true;
- 布尔类型->非布尔类型:false->0,true->1;
- 浮点数->整型:保留浮点数中小数点之前的部分;
- 整型->浮点数:小数部分记为0,超过浮点数容量,精度会有所损失;
- 赋给无符号类型一个超出其范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数;
- 赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。
// 非布尔类型->布尔类型
int i = 42;
if (i) // 条件为true
i = 0;
// 含有无符号类型的表达式
unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl; // 输出-84
// 当一个算术表达式既有无符号数又有int值时,int会被转化成无符号数
std::cout << u + i << std::endl; // 如果int占32位,输出4294967264
unsigned u1 = 42, u2 = 10;
std::cout << u1 - u2 << std::endl; // 正确:输出32
// 当从无符号数中减去一个值时,不管这个值是不是无符号数,我们都必须确保结果不能是一个负值
std::cout << u2 - u1 << std::endl; // 正确,不过是取模后的值
// 正确
for (int i=10; i>=0; i--)
std::cout << i << std::endl;
// 错误:变量u永远也不会小于0,循环条件一直成立
for (unsigned u = 10; u >= 0; --u)
std::cout << u << std::endl;
提示:切勿混用带符号类型和无符号类型。
字面值常量
字面值(literal)
整型和浮点数字面值
0 开头表示八进制,0x开头表示十六进制。
// 20
20 /*十进制*/ 024 /*八进制*/ 0x14 /*十六进制*/
整型字面值数据类型 | 是否带符号 | 决定因素 |
---|---|---|
10进制 | 默认带符号 | 容纳下当前值的前提下,int、long、long long中尺寸最小者 |
8进制和16进制 | 可带可不带 | 容纳其数值,int、unsigned int、long、unsigned long、long long、unsigned long long中的最小者 |
浮点数字面值默认为double。
字符和字符串字面值
'a' // 字符字面值
"Hello World!" // 字符串字面值
字符字面值 ‘a’ 就是表示一个单独的字符 a,字符串“a”代表一个字符的数组,包含字符 a 和一个空字符 ‘\0‘。
转义序列
转义字符 | 含义 |
---|---|
\n | 换行符 |
\t | 横向制表符 |
\a | 报警符 |
\v | 纵向制表符 |
\b | 退格符 |
\” | 双引号 |
\\ | 反斜杠 |
\? | 问号 |
\’ | 单引号 |
\r | 回车符 |
\f | 进纸符 |
泛化的转义序列:\x+16进制数字,+8进制数字。
\7 (响铃) \12 (换行符) \40 (空格)
\0 (空字符) \115 (字符M) \x4d (字符M)
指定字面值的类型
前缀 | 含义 | 类型 |
---|---|---|
u | Unicode16位字符 | char16_t |
U | Unicode32位字符 | char32_t |
L | 宽字符 | wchar_t |
u8 | UTF-8(仅用于字符串字面常量) | char |
后缀 | 类型 |
---|---|
u or U | unsigned |
l or L | long |
ll or LL | long long |
f or F | float |
l or L | long double |
变量
变量定义
类型说明符(type specifier) + 一个或多个变量名组成的列表,变量名之间以逗号隔开,最后以分号结束。
// sum、value和units_sold都是int
int sum = 0, value, units_sold = 0;
// item 是 Sales_item类型
Sales_item item;
// string 是一种库类型,表示一个可变长的字符序列
// book通过一个string字面值初始化
std::string book("0-201-78345-X");
初始化: 对象在创建时获得了一个特定的值,我们说这个对象被初始化了。
// price先被定义并赋值,随后被用于初始化discount
double price = 109.99, discount = price * 0.16;
// 调用函数 applyDiscount,然后用函数的返回值初始化 salePrice
double salePrice = applyDiscount(price, discount);
在C++中,初始化和赋值是两个完全不同的操作。初始化是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。
列表初始化:
int units_sold = 0;
int units_sold = { 0 };
int units_sold{0};
int units_sold(0);
如果使用列表初始化存在丢失信息的风险,编译器会报错:
long double ld = 3.1415926536;
// 错误:转换未执行,因为存在丢失信息的危险
int a{ld}, b = {ld};
// 正确:转换未执行,且确实丢失了部分信息
int c(ld), d = ld;
默认初始化: 内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0。一种例外情况是,定义在函数体内部的内置类型变量将不被初始化。不被初始化的内置类型变量的值是未定义的,试图拷贝或者以其他形式访问此类值将引发错误。类的对象如果没有显式地初始化,则其值由类决定。
变量声明和定义的关系
声明使得名字为程序所知,定义负责创建与名字关联的实体。
// 声明i而非定义i
extern int i;
// 声明并定义j
int j;
任何包含显式化的声明都是定义。
// 定义
extern double pi = 3.1416;
变量能且只能被定义一次,但是可以被多次声明。
标识符
C++标识符(identifier)由字母、数字 和 下划线 组成,必须以字母或下划线开头。长度没有限制,对大小写敏感。
// 定义4个不同的int变量
int somename, someName, SomeName, SOMENAME;
- 关键字不能作为变量名;
- 不能连续出现两个下划线;
- 不能下划线后连大写字母;
- 函数体外的标识符不能以下划线开头。
此外,还有一些规范:
- 要能体现实际含义;
- 变量名一般用小写字母;
- 自定义类大写开头;
- 标识符由多个单词组成,单词间要有明显的区分。
对于命名规范来说,若能坚持,必将有效。
名字的作用域
全局作用域 ( global scope ):整个程序范围都可使用。
块作用域 ( block scope ):花括号体内可以使用。
复合类型
引用
引用( reference ):为对象另外起了一个名字。
int ival = 1024;
int &refVal = ival;
// 报错:引用必须初始化
int &refVal2;
引用非对象,只是为一个已经存在的对象所起的另外一个名字。不能定义引用的引用。
// 把2赋给refVal指向的对象,此处即是赋给了ival
refVal = 2;
// 与ii = ival执行结果一样
int ii = refVal;
- 一般引用类型和与之绑定的对象严格匹配;
- 只能绑定对象,不能绑定字面值。
// 错误:引用类型的初始值必须是一个对象
int &refVal4 = 10;
指针
“指向( point to )”另外一种类型的复合类型。与引用相比:
相同点:都实现了对其他对象的间接访问。
不同点:
- 指针是对象,允许赋值和拷贝,在生命周期内科先后指向几个不同的对象;
- 无须初始化,如果不初始化,会拥有一个不确定的值。
获取对象的地址
使用取址符( 操作符 &)获取对象地址。
int ival = 42;
int *p = &ival;
一般指针类型要和它所指向的对象严格匹配,但有例外。
指针值应属于下列4中状态之一:
- 指向一个对象;
- 指向紧邻对象所占空间的下一个位置;
- 空指针,意味着指针没有指向任何对象;
- 无效指针,也就是上述情况之外的其他值。
访问无效指针会引发错误,编译器不负责检查此类错误。第2和第3种情况,指针的使用也受到限制,由于指针没有指向任何对象,所有试图访问此类指针对象的的行为也不被允许。
利用指针访问对象: 解引用符( 操作符 * )
空指针 (null pointer): 使用指针之前,可以先检查其是否为空。
赋值和指针:
int i = 42;
// pi被初始化,但没有指向任何对象
int *pi = 0;
// pi2被初始化,存有i的地址
int *pi2 = &i;
// 如果pi3定义于块内,则pi3的值无法确定
int *pi3;
// pi3 和 pi2指向同一个对象 i
pi3 = pi2;
// 现在pi2不指向任何对象了
pi2 = 0;
赋值改变的是等号左侧的对象。
// pi的值被改变,现在pi指向了ival
pi = &ival;
// ival的值被改变,指针pi并没有改变
*pi = 0;
其他指针操作:
int ival = 1024;
int *pi = 0;
int *pi2 = &ival;
// pi的值是0,因此条件为false
if (pi)
// ...
// pi2指向ival,因此它的值不是0, 条件是true
if (pi2)
// ...
void* 指针: 是一种特殊的指针类型,可用于存放任意对象的地址。
double obj = 3.14, *pd = &obj;
// 正确,void* 能存放任意类型对象的地址
void* pv = &obj;
// pv可以存放任意类型的指针
pv = pd;
理解复合类型的声明
定义多个变量:
修饰符和类型写在一起
// 合法,但容易产生误导
int* p;
// p1是指向int的指针,p2是int
int* p1, p2;
修饰符和变量标识符写在一起
// p1和p2都是指向int的指针
int *p1, *p2;
选择并坚持一种写法,没有对错之分。
指向指针的指针 :通过 * 可以区分指针的级别。
int ival = 1024;
// pi指向一个int型的数
int *pi = &ival;
// ppi指向一个int型的指针
int **ppi = π
指向指针的引用:引用本身不是一个对象,因此不能定义指向引用的指针。指针是对象,可以定义指针的引用。
int i = 42;
// p 是一个int型指针
int *p;
// r是一个对指针p的引用
int *&r = p;
// r 引用了一个指针,因此r赋值&i就是令p指向i
r = &i;
// 解引用得到i,也是p指向的对象,将i的值改为0
*r = 0;
要理解 r 的类型,需要从由向左阅读 r 的定义。离变量名最近的符号对变量类型有直接的影响。
const限定符
初始化和 const :利用对象去初始化 const修饰的对象时,该对象是不是有 const 修饰都无关紧要。
int i = 42;
const int ci = i; // 正确
int j = ci; // 正确
const的引用
即对常量的引用,不能修改引用所绑定的对象。
const int ci = 1024;
const int &ri = ci;
ri = 42; // 错误,r1是对常量的引用
int &r2 = ci; // 错误,视图让一个非常量引用指向一个常量对象
初始化和对 const 的引用 :初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果可以转换成引用类型即可。
int i = 42;
const int &r1 = i;
const int &r2 = 42; // 正确
const int &r3 = r1 * 2; // 正确
int &r4 = r1 * 2; // 错误,r4是一个普通引用
发生了什么?
double dval = 3.14;
const int &ri = dval;
实际上是创建了一个临时量
const int temp = dval;
const int &ri = temp;
对 const 的引用可能引用一个并非const的对象
int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0;
r2 = 0; // 错误
指针和const
指向常量的指针( pointer to const ) 不能用于改变其所指对象的值。
const double pi = 3.14; // pi是个常量,它的值不能改变
double *ptr = π // 错误
const double *cptr = π // 正确
*cptr = 42; // 错误
Tips: 所谓指向常量的指针或引用,不过是指针或引用“自以为是”罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象的值。
const 指针:指针是对象,因此也可以定义为常量,定义之后不能改变其指向。
int errNumb = 0;
int *const curErr = &errNumb;
const double pi = 3.14159;
const double *const pip = π
顶层const
顶层 const表示指针本身是一个常量对象,底层 const 表示指针所指对象是一个常量。
int i = 0;
int *const p1 = &i;
const int ci = 42;
const int *p2 = &ci;
const int *const p3 = p2;
const int &r = ci;
指向拷贝操作时,顶层 const 不受影响, 底层 const 的限制不能忽视。
constexpr和常量表达式
常量表达式是指值不会改变,在编译过程就能得到结果的表达式。
声明为 constexpr 的变量一定是一个常量,而且必须用常量表达式初始化:
constexpr int mf = 20;
constexpr int limit = mf + 1;
constexpr int sz = size(); // 只有当size()是一个constexpr语句时才正确
constexpr 修饰指针时,仅对指针有效,与其所指对象无关。
处理类型 #
类型别名
两种方法:typedef 和 using
typedef:
typedef double wages;
typedef wages base, *p;
别名声明:
using SI = Sales_Item;
指针、常量和类型别名
typedef char *pstring;
const pstring cstr = 0; // cstr是指向char的指针常量
const pstring *ps; // ps是一个指针,它的对象是指向char的常量指针
auto 类型说明符
引入了 auto 说明符,能让编译器去分析表达式所属的类型。
// 正确,i是整数,p是整型指针
auto i=0, *p = &i;
// 错误,sz 和 pi的类型不一致
auto sz = 0, pi = 3.14
- auto 会忽略顶层 const,同时底层 const 会保留下来。
decltype 类型指示符
选择并返回操作数的类型。编译器分析表达式并得到他的类型,却不实际计算表达式的值。
// 编译器不实际调用函数f
decltype(f()) sum = x;
处理顶层 const 和引用方式与 auto 不同。
const int ci =0, &cj = ci;
// x的类型是 const int
decltype(ci) x = 0;
// y的类型是 const int&, y绑定到变量x
decltype(cj) y = x;
// 错误:z是一个引用,必须初始化
decltype(cj) z;
decltype 和引用
// 引用,必须初始化
decltype(*p) c;
// 引用,必须初始化
decltype((i)) d;
自定义数据结构
定义 Sales_data 类型
struct Sales_data {
std::string bookNo;
// C++11新标准,类内初始化
unsigned units_sold = 0;
// C++11新标准,类内初始化
double revenue = 0.0;
};
定义变量的方式:
struct Sales_data { /* ... */} accum, trans, *salesptr;
或者
struct Sales_data { /* ... */ };
Sales_data accum, trans, *salesptr;
使用Sales_data 类
编写自己的头文件
头文件保护符 ( header guard )
#ifndef xxx
#define xxx
/* ... */
#endif
小结
- 基础内置类型
- 常量和非常量
- 复合类型
- auto 和 decltype