第2章 变量和基本类型
- 什么是对象: 内存中具有类型的区域。
- 初始化不是赋值: 初始化指创建变量并给它赋初始值,而赋值则是擦除对象的当前值并用新值代替。
- extern: 当碰到它搞不清楚时,想一想声明和定义的关系,声明可以有多份,但定义必须有且只能有一份。
- const 和 extern: 定义全局变量时,隐式包含 extern,但如果还加了 const 的话(被限制了本文件使用),就要显示加上 extern 了,否则别的文件无法引用。有一种情况例外:声明在头文件中的常量表达式(比如enum),包含它的每个文件都会自带一份(名称和值都一样),大部分编译器编译时便会将使用的地方替换成常量表达式,所以并不会为const变量开辟存储空间。(如果此时加了extern,反而会出现重复定义的报错)
- struct 和 class: 二者效果相同,唯一的区别在于默认访问级别不同。
趣味题
union Un
{
int8_t a[4];
int32_t b;
};
Un u;
u.b = 1;
if (u.a[0] == 1)
cout << u.a[0] << endl; // 有输出,但是输出“笑脸”
// 思考:明明等于1,为什么输出不等于1?
// 猜测:== 是根据内存对比的,cout 却是根据内存类型(int)输出的。
第3章 标准库类型
- vector: 下标操作不能用来新增元素;for 循环的判断条件用 != 而不是用 < ;界限用 .size() 不需要提前保存。
- const_iterator:
const ivec<int>::iterator iter
和const int *p
不一样,后者不可改变 *p 的值,但前者是不能 iter++。要想限定所指向的值不能改变,有专门的定义方式ivec<int>::const_iterator iter
。 - size_type 和 difference_type: 一个 unsigned 一个 signed ,足够大来存储大小,应用于 string 、vector 当中。
- 任何改变 vector 长度的操作都会使已存在的迭代器失效。
- bitset 和 vector: 二者同为类模板,vector 抽离类型,bitset 抽离长度,在尖括号内给出长度值。
- bitset 初始化: bitset 对象内存从0开始是低位到高位,但是输出时从高位到低位。string 对象和 bitset 对象之间是反向转化的(高低位颠倒)。
第4章 数组和指针
- 概念: 类似于 vector 和迭代器的内置数据类型,尽量避免使用(容易出错难于调试),除非设计强调速度的良好程序。
- 指针: 提供对其所指对象的间接访问,相对于迭代器,结构更通用一些。
- 取地址操作符 &: 只有当变量用作左值时,才能取其地址。P.115
- 指针注意事项: 最好在定义指针时就初始化,如果一定要分开,也要初始化为0。这样编译器才能检测出(未初始化、指向不可控地址,是无法检测出来的)。
- void 指针:* 可以保存任何类型对象的地址。操作有限:1.与另一个指针进行比较;2.向函数传递 void* 指针或从函数返回 void* 指针;3.给另一个 void* 指针赋值。
- 怎么理解? 如果指针指向一对象,可以在指针上加1从而获取指向相邻的下一个对象的指针。
- 指针相减: 只要两个指针指向同一数组或有一个指向数组末端的下一单元,C++还支持对这两个指针做减法操作。得到的数据是标准库类型 ptrdiff_t,与 size_t 类型一样,是在 cstddef 头文件中定义的一种与机器相关的类型(可以和 vector中的 size_type、difference_type 做类比)。
- 下标操作符 []: 使用下标访问数组时,实际上是对指向数组元素的指针做下标操作(没错,指针也可以用下标操作:p[i] 等效于 *(p+i),并且支持负数)。P.122
- for 循环新知识: 只要定义的多个变量具有相同的类型,就可以在 for 循环的初始化语句中同时定义它们。(常规只能写一条)。
- 指针是数组的迭代器: 循环遍历时,可以像迭代器一样,指针 p 等效于begin(),p + size 等效于end()。
- 疑问: 为什么指向 const 对象的指针,在定义时不需要对它进行初始化?P.124
- 解答: 和 const 指针作对比,与任何 const 量一样,const 指针也必须在定义时初始化。
- 指针和 typedef:
typedef string *pstring; const pstring cstr;
如果把 typedef 当做文本扩展,就会错误的理解为 cstr是一种指针,指向 string 类型的 const 对象。正确答案是:cstr 是指向 string 类型对象的 const 指针。 - cstring 的关系: cstring是 string.h 头文件的 C++ 版本,而 string.h 则是 C 语言提供的标准库。
- 动态数组:
int *pia = new int[10];
1、这样创建的数组没有名字,只能通过地址间接访问堆中对象。2、类类型会调用默认构造函数,内置类型无初始化。可以跟一对圆括号统一初始化,但无法像数组变量一样,用初始化列表给元素提供各不相同的初值。 - const 对象的动态数组: 必须在定义时就初始化,因为常量元素不允许被修改。正因如此,这样的数组实际上用处不大。
- 允许动态分配空数组: 之所以要动态分配数组,往往是由于编译时并不知道数组的长度。
size_t n = get_size(); int *p = new int[n]; for (int *q = p; q != p + n; ++q)
有趣的是get_size()返回0的时候,也能正常运行,只是指针不能取引用。 - 动态空间的释放: delete [] 表达式,如果遗漏了空方括号,会导致运行时少释放了内存空间,从而产生内存泄露。
- 新旧代码兼容: C 风格的可以初始化 string 类型,但反之不行。必须采取以下方式,并且加上 const。还要注意中途修改了 str 后,该指针可能失效,所以最好是用之前拷贝一份。
const char *pstr = str.c_str();
第5章 表达式
- 除以和求模: 求模操作符号不同时,结果依赖于机器。但规律一定:如果求模的结果随分子的符号,则除出来的值向零一侧取整;如果求模与分母的符号匹配,则除出来的值像负无穷一侧取整。
- 短路求值: 逻辑与和逻辑或操作符总是先计算其左操作数,当仅靠左操作数的值无法确定结果时,才会求解其右操作数。具有危险边界时适用。P.146
- 位操作符: 对于符号位的处理依赖于机器,所以强烈建议使用 unsigned 整型操作数。
- 位运算与 bitset: 标准库提供的 bitset 操作更直接、更容易阅读和书写、正确使用的可能性更高,并且对象的大小不受 unsigned 数的位数限制。
- 移位操作符:中等优先级。
- 赋值操作符: 赋值操作符的左操作数必须是非 const 的左值。数组名是不可修改的左值,因此不能作为赋值操作的目标。而下标和解引用操作符都返回左值,因此当作用于非 const 数组时,其结果可作为赋值操作的左操作数:
int ia[10]; ia[0] = 0; *ia = 0;
- 赋值操作符:低优先级、右结合性(与常规二元运算符不同)
- 复合赋值操作符: 可以是以下十种:
+= -= *= /= %= <<= >>= &= ^= |=
。使用复合赋值操作时,左操作数只计算了一次;而使用相似的长表达式时,该操作数则计算了两次,第一次作为右操作数,而第二次则用作左操作数。 - 自增和自减操作符: 前置操作返回对象本身,是左值。而后置操作返回的则是右值。对于 int 型对象和指针,编译器可优化掉这项额外工作,但对于更多的复杂迭代器类型,可能会花费更大的代价。因此,养成使用前置操作这个好习惯,就不必操心性能差异的问题。
- 箭头操作符: 对于指向类类型的指针变量,访问其成员时可用
p->foo;
取代(*p).foo;
。 - 条件操作符: 是C++中唯一的三元操作符,它允许将简单的 if-else 判断语句嵌入表达式中。
- 逗号操作符: 逗号表达式是一组由逗号分隔的表达式,这些表达式从左向右计算。逗号表达式的记过是其右边表达式的值,如果是左值结果也是左值。
- 优先级和结合性: 优先级决定操作数的结合方式,结合性决定操作数的计算顺序。P.161(但这二者都不能定义求值顺序P.163)
- 动态创建对象 new: 定义变量时,必须指定其数据类型和名字。而动态创建对象时,只需指定其数据类型,而不必为对象命名。取而代之的是,new 表达式返回指向新创建对象的指针。
- 动态创建对象的默认初始化:
int *pi = new int;
和int *pi = new int();
是不一样的,前者没有定义,后者初始值为0。 - 撤销动态创建的对象 delete: 如果指针指向不是用 new 分配的内存地址,则在该指针上使用 delete 是不合法的。(编译器甚至无法发现错误。例如:
int i; int *pi = &i; delete pi;
)在 delete 之后,重设指针的值 - 零值指针的删除: C++保证:删除0值的指针时安全的。(虽然这样做没有任何意义。)
- 动态内存的管理容易出错: 1、删除失败,该块内存无法返还给自由存储区,导致内存泄漏(不易发现,程序运行一段时间后内存不足才知道);2、读写已删除的对象。(删除后立即置0可避免);3、同一块内存空间连续两次delete。(自由存储区可能会被第二次删除破坏)
- 隐式类型转换: 1、在混合类型的表达式中,操作数被转换为相同类型;2、用作条件的表达式被转换为 bool 类型;3、表达式初始化或者赋值某个变量,被转换为该变量的类型。
- **算术转换:**研究大量例题是帮助理解算术转换的最好方法。P.170
- 其他隐式转换: 1、**指针转换:**数组大多情况自动转换为指向第一个元素的指针;任意数据类型的指针都可转换为void*类型;整型数值常量0可转换为任意指针类型。
- 命名的强制类型转换: dynamic_cast、const_cast、static_cast、reinterpret_cast。
趣味题:
if (val == true)
if (val)
// val 为bool类型时,二者等价。
// 如果不是,上面等于1满足,下面非0即满足。
第6章 语句
- 复合语句: 用花括号括起来的块语句。
- if 语句: 条件中可以是表达式,或者是一个初始化声明。必须初始化,然后转化为 bool 型的类型。类类型能否用在条件表达式中取决于类本身。IO通常可以,vector 和 string 一般不可用。
- 悬垂 else 问题: 在 if 语句后加花括号是一个好习惯,可以避免这种二义性。P.185
- switch 表达式: 求解的表达式可以非常复杂,并可以定义和初始化一个变量,但只能在结构内使用。
- case 标号: 必须是整型常量表达式。
- switch 内部的变量定义: 最好用花括号括起来行程块语句,不然下一个 case 能使用该变量,但有可能被该 case 标签跳过初始化。
- while 和 do while: 后者的判断条件中不可以定义变量。
- 使用预处理器进行调试:
#ifndef NDEBUG #endif
- 四种在调试时非常有用的常量:
__FILE__
文件名、__LINE__
当前行号、__TIME__
文件被编译的时间、__DATE__
文件被编译的日期。 - assert: 用来测试“不可能发生”的条件,只对程序的调试有帮助,但不能用来代替运行时的逻辑检查,也不能代替对程序可能产生的错误的检测(异常处理)。
第7章 函数
- **函数与操作符:**以前看操作符重载的时候,觉得操作符就是函数,实际二者的目的确实是一样的。
- **调用操作符:**C++语言使用调用操作符()(一对圆括号)实现函数的调用。把()当做操作符,参数当做操作数。
- **函数必须指定返回类型:**早期的C++版本可以不定义,会隐式返回 int 类型。
- 引用形参的两个作用: 1、避免复制类类型或者大型数组,造成效率低下。2、可以返回想要的,额外的信息。
- **const 引用:**如果函数唯一的目的是避免复制实参,则应将形参定义为 const 引用,避免让函数的使用遭到限制(const 实参无法传递)。
- **数组形参:**使用数组类型形参的函数,会被自动转化为指针。
int*; int[]; int[10]
三者完全相同。编译器只会检查实参是不是指针,类型是否匹配,不会检查数组的长度。P.221 - **通过引用传递数组:**如果形参是数组的引用,编译器不会将数组实参转化为指针,而是传递本身,此时数组大小成为类型的一部分,编译器会检查是否匹配。
int (&arr)[10]
- **多维数组的传递:**与一维数组一样,编译器忽略第一维的长度。
int matrix[][10]; int (*matrix)[10]
这二者等效。 - 传递给函数的数组的处理: 1、C风格字符串,末尾null字符作为结束标记;2、显示传递数组大小
int j[] = {0, 1}; print(j, sizeof(j)/sizeof(*j));
3、使用标准库规范,传递两个指针,一个指向第一个元素,一个指向最后一个元素的下一个位置。print(j, j+2);
- **main 处理命令行选项:**第一个为参数个数(包含程序名),第二个有两种等效写法:
char *argv[]; char **argv;
- **return 语句:**void 函数不允许返回表达式,但是可以返回同为 void 的函数。隐式的 return 发生在函数的最后一个语句完成时。
- **主函数 main 的返回值:**所有的非 void 函数都要有返回值,main 函数除外。它的返回值通常视为状态指示器,0代表成功,非0的意义因机器而不同。建议使用 cstdlib.h 中的宏定义 EXIT_SUCCESS、EXIT_FAILURE。
- 可以返回引用(比如返回两个字符串中较长的那个),但千万不要返回局部对象的引用,返回局部对象的指针也一样不行。
- **引用返回左值:**可以对返回值进行赋值。标准输出运算符
<<
返回的也是引用值。 - 递归:递归函数必须定义一个终止条件,否则会一直调用自身知道程序栈耗尽。这种现象称为“无限递归错误”。满足了终止条件后,依次返回前面每个调用的返回值,这个过程称为此值向上回渗。主函数 main 不能调用自身。
- **函数声明:**一个函数只能定义一次,但是可声明多次。
- **函数原型:**函数原型描述了函数的接口,包括函数返回