《C++ Primer(第五版)》第二章笔记
基本内置类型
类型 | 含义 | 最小尺寸 |
---|---|---|
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位有效数字 |
可寻址最小内存块称为字节(Byte),存储的基本单元称为字(Word)。在一台32位的计算机上,32位即字长。
- 一个int至少和一个short一样大,一个long至少和一个int一样大,一个long long之上和一个long一样大
- float 以1个字来表示,double用2个字来表示,long double以3或4个字来表示
如何选择类型:
- 当明确知晓数值不可能为负时,选用无符号类型。
- 使用int执行整数运算。在实际应用中,short常常显得太小而long一般和int有一样的尺寸。如果你的数值超过了int的表示范围,选用long long。
- 在算数表达式中不要使用char或bool,只有在存放字符或布尔值时才使用它们。原因在于char类型在一些机器上是有符号的,而在另一些机器上又是无符号的。如果你需要使用一个不大的整数,那么明确指定它的类型是signed char还是unsigned char。比如一些数字char的操作。
- 执行浮点数运算选用double,这是因为floa通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。
类型转换
- 当我们赋予无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。
- 当我们赋予带符号一个超出它表示范围的值时,结果是未定义的。
- 当一个算数表达式中既有无符号数又有int值时,那个int值就会转换成无符号数。
字面值常量(literal)
1 | 20 /* 十进制 */ |
转义序列:‘\’
后跟八进制数字,\x
后跟十六进制数字转义字符。如果反斜线\
后面跟着的数字超过3个,只有前3个数字构成转义序列u8"\1234"="S4"
。
指定字面值类型(整型和浮点型):
后缀 | 最小匹配类型 | 后缀 | 类型 |
---|---|---|---|
u/U | unsigned | f/F | float |
l/L | long | l/L | long double |
ll/LL | long long |
变量
对象(object)
: 一块能存储数据并具有某种类型的内存空间
初始化
初始化不是赋值,初始化的含义式创建变量时赋予其一个初始值,而赋值的含义式把对象的当前值擦除,而以一个新值来替代。
默认初始化
如果内置类型的变量未被显式初始化,它的值由定义的位置决定。定义域仍和函数体之外的变量被初始化为0,而定义在函数体内部的内置类型变量将不被初始化。一个未被初始化的内置类型变量的值式未定义的,如果试图拷贝或以其他形式访问此类值将引发错误。
未初始化的变量含有一个不确定的值,使用未初始化变量的值是一种错误的编程行为且很难调试。
变量声明和定义的关系
1 | extern int i; // 声明i而非定义i |
变量能且只能被定义一次,但是可以被多次声明。
声明和定义的区别看起来也许微不足道,但实际上却非常重要。如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。
标识符
C++的标识符(identifier)由字母、数字和下划线组成,其中必须以字母或下划线开头。
- 用户自定义的标识符中不能连续出现两个下划线,也不能以下划线紧连大写字母开头
- 函数体外的标识符不能以下划线开头
作用域
1 |
|
复合类型
引用
- 引用并非对象而是为一个就已经存在地对象所起的别名
- 不能定义引用的引用
- 引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起(除非是常量引用)
- 必须初始化
- 一旦引用完成初始化,该引用将无法重新绑定到另外一个对象上
指针
- 指针本身是一个对象,允许对指针赋值和拷贝
- 指针无需在定义时赋值
- 指针存放某个对象的地址,需要使用取地址符&
- 引用不是对象,没有实际地址,所以不能定义指向引用的指针
*p
解引用符访问对象,解引用符仅适合于那些确实指向了某个对象的有效指针
空指针(null pointer)不指向任何对象,在试图使用一个指针之前代码可以首先检擦它是否为空。
1 | int *p1 = nullptr; //等价于int *p1 = 0; |
建议:初始化所有指针。在大多数编译器环境下,如果使用了未经初始化的指针,则该指针所占内存空间的当前内容将被看作一个地址值。访问该指针,相当于去访问一个本不存在的位置上的本不存在的对象。
void* 指针
void*是一种特殊的指针类型,可用于存放任意对象的地址。然而对于 void*来说,内存空间仅仅是内存空间,没办法访问内存空间中所存的对象。(对void*指针使用解引用符*
会报错)
理解复合指针的声明
1 | int ival = 1024; |
以上可以改写为:
1 | int ival=1024, *pi=&ival, **ppi=&pi, *&r=pi; |
面对一条比较复杂的声明语句时,从右向左读,离变量名最近的符号对变量的类型右最直接的影响。
const 限定符
const大法:
- const对象一旦创建后其值就不能再改变,所以const对象必须初始化
- 如果利用一个对象去初始化另外一个对象,则它们是不是const都无关紧要
- 如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字
const引用
常量引用
: 对const常量的引用,不能被用作修改它所绑定的对象
常量引用初始化:
1 | int i=42; |
为啥C++这样设计?
1 | double dval = 3.14; |
编译器把上述代码变成如下:
1 | double dval = 3.14; |
如果不是常量引用,就允许对赋值,并且丢弃精度,这样会改变所引用对象的值。否则,我们绑定的对象就是一个临时量而非dval。我们既然用引用dval,就肯定想通过ri改变dval的值,否则干什么给赋值呢?既然大家不会想着把引用绑定到临时量上,C++也就把这种行为归为非法。
常量引用值对引用做限制而对引用的对象是否为常量不做限制。
指针和const
指向常量的指针
: 令指针指向常量,存放常量对象的地址。
允许一个指向常量的指针指向一个非常量对象(普通指针不允许)
常量指针
: const pointer,必须初始化,而且一旦初始化完成,则它的值(存放在指针中的那个地址)就不能再改变了。把*放在const关键字之前用以说明指针是一个常量。
1 | const double pi = 3.1415; |
顶层const
顶层const
: (top-level const) 表示指针本身是个常量;表示任意对象都可以是一个常量。
底层const
: (low-level const) 表示指针所指的对象是一个常量;表示所指对象是常量像引用和指针。
只有指针既可以是顶层const也可以是底层const
拷贝操作
-
顶层const不受拷贝操作影响
-
而底层从上图的拷贝需要拷入和拷出的对象必须具有相同的底层const的资格,或者两个对象的数据类型必须能够转换。非常量可以转换成常量,反之则不行。
1 | const int ci = 42; |
常量表达式
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。比如,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
1 | const int sz = get_size(); // sz 不是常量表达式 |
C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。申明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化:
1 | constexpr int mf = 20; |
如果在constexpr声明中是一个指针,那constexpr只对针有效:
1 | const int *p = nullptr; // p是一个指向整型常量的指针 |
处理类型
类型别名
两种方法定义类型别名,一是typedef,二是using。
typedef
定义类别别名,让复杂的类型名字变得简单明了、简单易用(更复杂(雾))。
1 | typedef double wages; // wages 是 double 的同义词 |
using
1 | using SI = Sales_item; // SI是Sales_item的同义词 |
这样声明别名后就可以用别名来定义对象了。
指针、常量和类别别名
typedef
就是晦涩难懂的代名词
1 | typedef char *pstring; // 为指向char的指针做别名 |
auto和decltype
auto: 类型说明符,用它能让编译器代替我们去分析表达式所属的类型。auto定义的变量必须有初始值,auto在一条语句中声明多个变量必须保证同样的基本数据类型。
decltype: 类型指示符,为了从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。
decltype
处理顶层const
和引用的方式与auto
不同,auto
会忽略顶层const和直接推演引用所指的类型,decltype
会将顶层const
和引用保留起来。
1 | const int ci = 0, &cj = ci; |
decltype
处理(对指针)解引用操作得到引用类型,而auto
得到指针所指对象的类型
1 | int i = 1, *p = &i; |
decltype
结果类型与表达式形式密切相关
如果decltype
使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,编译器就会将把它当成一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这样的decltype
就会的得到引用类型:
1 | int a = 1, b = 0; |
自定义数据结构
struct
:通篇只讲这个。
- 类内定义的名字必须唯一,但是可以和类外部定义的名字重复
- 类体右侧的表示结束的花括号必须写一个分号,这是因为类体后面可以紧跟变量名以实对该类型对象的定义(但是不推荐这样做)
- 类内允许有类内初始值来初始化类
1 | struct Sales_data {/*...*/} accum, trans, *sp; |
一般来说最好不要把对象的定义和类的定义放在一起。这样做无异于把两种不同实体的定义混在了一条语句离,一会儿类,一会儿变量,非常不合适。
头文件保护符
1 |
|
SALES_DATA_H为头文件保护符,如此的预处理变量必须唯一,通常的做法是基于头文件的名字来构建保护符的名字。
#defube
: 把一个名字设定为预处理变量
#ifdef
: 当且仅当变量已定义为真
#ifndef
: 当且仅当变量未定义为真
一旦检查结果为真,则执行后续操作直至遇到#endif
为止。