第一章:预备知识
- C++融合了三种不同的编程方式:C语言代表的
过程性语言,C++在C语言基础上添加的类代表的面向对象语言、C++模板支持的泛型编程。 - 面向对象编程(
OOP) - 一种被称为
编译器的特殊程序将高级语言翻译成特定计算机的内部语言. - 集成开发环境(integrated development environments,
IDE) OOP是一个管理大型项目的工具,而泛型编程提供了执行常见任务(如对数据排序或合并链表)的工具.- 编程步骤:
第二章:开始学习C++
main函数可以省略最后的return 0;语句(只有main函数可以省略)- C++ 和 C一样,也使用一个
预处理器,该程序在进行主编译之前对源文件进行处理。 #include <iostream>预处理器编译指令是做什么用的? 这将导致在最终编译之前,使用iostream中的文件的内容替换该编译指令。using namespace std;(命名空间编译指令 , 这里的是using编译指令.)是做什么用的? 它使得程序可以使用std名称空间中的定义。<<符号表示该语句将把这个字符串发送给cout,该符号指出了信息流动的路径。- 类描述了一种数据类型的全部属性(包括可使用它执行的操作),对象是根据这些描述创建的实体。
endl是一个特殊的C++符号,表示一个重要的概念:重起一行。endl确保程序继续运行前刷新输出(将其立即显示在屏幕上)。而\n不能提供这样的保证,这意味着在有些系统中,有可能在您输入信息后才会出现提示。- 头文件命名的约定
| 头文件类型 | 约定 | 示例 | 说明 |
|---|---|---|---|
| C++旧式风格 | 以.h结尾 | iostream.h | C++程序可以使用 |
| C旧式风格 | 以.h结尾 | math.h | C、C++程序可以使用 |
| C++新式风格 | 没有扩展名 | iostream | C++程序可以使用,使用 namespace std |
| 转换后的C | 加上前缀C,没有扩展名 | cmath | C++程序可以使用,可以使用不是C的特性,如 namespace std |
第三章:处理数据
3.1.2 整形
C++的基本整形(按宽度递增的顺序排列)分别是char、short、int、long 和C++11新增的long long .其中每种类型都有符号版本和无符号版本,因此总共有10种类型可供选择。
3.1.3 整形short、int、long 和 long long
- 位与字节:
sizeof计算有多少字节,CHAR_BIT表示字节的位数
- 计算机内存的基本单位是位(
bit)。可以将位看作电子开关,可以开,也可以关。开表示值1,关表示值0。8位的内存块可以设置出256(2 ^ 8)种组合.- 字节(
byte)通常指的是8位的内存单元。从这个意义上说,字节指的就是描述计算机内存量的度量单位,1KB等于1024字节,1MB等于1024KB。然而,C++对字节的定义与此不同。C++字节由至少能够容纳实现的基本字符集的相邻位组成,也就是说,可能取值的数目必须等于或超过字符数目。如Unicode,有些实现可能使用16位甚至32位的字节。
2.计算机内存由一些叫做位(bit)的单元组成。C++的short、int、long 和 long long 类型通过使用不同数目的位来存储值,最多能够表示表示4种不同的整数宽度。由于系统位数不同,没有一种选择能满足所有计算机设计要求。C++ 提供了一种灵活的标准,它确保了最小长度(从C语言借鉴而来),如下所示:
- short 至少16位;
- int 至少与 short 一样长;
- long 至少32位,且至少和int一样长;
- long long 至少64位,且至少与long一样长;
C++的short、int、long和long long都是符号类型,这意味着每种类型的取值范围中,负值和正值几乎相同。例如,16位的int的取值范围为-32768到+32767.- 符号常量---预处理器方式(
P42)
在
C++编译过程中,首先将源代码传递给预处理器。在这里#define和#include一样,也是一个预处理器编译指令。该编译指令告诉预处理器:在程序中查找INT_MAX,并将所有的INT_MAX都替换为32767.#define编译指令是C语言遗留下来的,C++通常使用关键词const.
- C++ 初始化方式
int week = 7;
int wrens(32);
int age = {10};
int year = {2018};
cout << wrens << endl;
cout << age << endl;
cout << year << endl;
cout << week << endl;
3.1.6 整型字面值
- 整型字面值(常量)是显式地书写的常量,与
C相同,C++能够以三种不同的计数方式来书写整数:十进制、八进制、十六进制。C++使用前一(两)位来标识数字常量的基数。
- 如果第一位为
1~9,则基数为10(十进制)。- 如果第一位为
0,第二位为1~7,则基数为8(八进制);因此042的基数是8,它相当于十进制的34- 如果前两位为
0x或0X,则基数为16(十六进制);因此0x42为十六进制数,相当于十进制的66.对于十六进制,字符a~f和A~F表示了十六进制位,对应于10~15.0xA5为165(10个16加5个1)。
- 头文件
iostream提供了控制符dec、hex、oct分别用于指示cout以十进制、十六进制和八进制格式显示整数.
3.1.8 字符和小整数
- char类型接收和显示的是字符
char ch;
cin >> ch;
// 输入5,会被系统识别成字符“5”,并将其对应的字符编码(ASCII编码53)存储到变量ch中
int ch ;
cin >> ch ;
// 输入5,上述代码将读取字符“5”,将其转换为相应的数字值5,并存储到变量n中
- 与
int不同的是,char在默认情况下既不是没有符号,也不是有符号。是否有符号由C++实现决定。如果char有某种特定的行为对您来说非常重要,则可以显式地将类型设置为signed char或unsigned char.
- 如果将
char用作数值类型,则unsigned char和signed char的差异非常重要。unsigned char类型的通用范围是0~255,而signed char的表示范围是-128~127.- 如果使用
char变量来存储标准ASCII字符,则char有没有符号都没关系,在这种情况下,可以使用char。
3.1.9 bool类型
C++的bool类型,非零即真
3.2 const限定符
const声明常量,常量被初始化后,其值就被固定了,编译器将不允许再修改该常量的值。- const比#define好
- 能够明确的指定类型.
- 可以使用C++的作用域规则将定义限制在特定的函数或文件中.
- 可以将const用于更复杂的类型.
3.3浮点数
- 计算机将浮点数分两部分存储。一部分表示值,另一部分用于对值进行放大或缩小。
- E表示法: d.dddE+n 指的是小数点向右移n位(可以简写为7E5),而d.dddE-n指的是将小数点向左移n位(7E-5)。
3.3.4 浮点数的优缺点
浮点数有两大优点:
- 可以表示整数之间的值
- 由于有缩放因子,它们可以表示的范围大的多。
缺点:
- 浮点运算的速度通常比整数运算
慢- 精度将降低
3.4.2 除法分支
除法运算符(/)的行为取决于操作数的类型。如果两个操作数都是整数,则C++将执行整数除法。这意味着结果的小数部分将被丢弃,使得最后的结果是一个整数。如果其中有一个(或两个)操作数是浮点数,则小数部分将保留,结果是浮点数。
3.4.3类型转换
11种整形:
char、unsigned char、signed char、short、unsigned short、int、unsigned int、long、unsigned long、long long、unsigned long long. 3种浮点型: float、double、long double.
由于有11种整形和3种浮点类型,C++自动执行很多类型转换:P62
- 将一种算术类型的值赋给另一种算术类型的变量时
- 表达式中包含不同的类型时
- 将参数传递给函数时
3.6 复习题
- 下面两条C++语句是否等价?
char grade = 65;
char grade = 'A';
这两条语句并不真正等价,虽然对于某些系统来说,它们是等效的。最重要的是,只有使用ASCII码的系统上,第一条语句才将得分设置为字母A,而第二条语句还可用于使用其他系统编码的系统。其次65是一个int常量,而‘A’是一个char常量。
2. 如何使用C++找出编码88表示的字符?
char c = 88;
cout<< c << endl;
cout.put(char(88));
cout << char(88) << endl;
cout << (char)88 << endl;
第四章 复合类型
4.1 数组
数组(Array) 是一种数据格式,能够存储多个同类型的值.
4.1.2 数组的初始化规则
1.只有在定义数组的时才能使用初始化,此后就不能使用了。
int cards[4] = {1,2,3,4}; // okay
itn hand[4]; // okay
hand[4] = {1,2,3,4}; // not allowed
hand = cards // not allowed
- 如果只对数组的一部分初始化,则编译器将其他元素设置为0.
// 初始化了包含500个0的数组
int totals[500] = {0}
4.2 字符串
- C++处理字符串的方式有两种
- 来自C语言,常被称为C-风格字符串(
C-Style string)- 基于
string类库的方法
C-style字符串具有一种特殊的性质:以空字符结尾,空字符被写作\0,其ASCII码为0,用来标记字符串的结尾。- "AAAA" - 被称为字符串常量(
string constant)或字符串字面值(string literal) - 处理字符串的函数会根据空字符的位置,而不是数组的长度来进行处理。
- 在确定存储字符串所需的最短数组时,别忘了将结尾的空字符计算在内。
- 字符串常量(使用双引号)不能与字符常量(使用单引号)互换。字符常量(如's')是字符串编码的简写表示。在ASCII系统上,'s'只是83的另一种写法。"s"不是字符常量,它表示两个字符(字符S和\0)组成的字符串。更糟糕的是,"s"实际上表示的是字符串所在的内存地址。
sizeof计算出整个数组的长度。strlen()返回的可见字符串的长度(不包含\0)
4.2.4 每次读取一行字符串输入
- 面向行的输入:getline().
通过换行符确定结尾,但不保存换行符
// 读取20个字符到数组name中(实际上只能读取19个,余下一个放空白字符)
cin.getline(name,20)
- 面向行的输入:get()
P80
4.3.3 string类的其他操作
strcpy(A,B) //copy B to A
strcat(A,B) // append contents of B to A
4.5 共用体
- 共用体(
union)是一种数据格式,它能够存储不同的数据类型,但只能同时存储其中的一种类型。 - 由于共用体每次只能存储一个值,因此它必须有足够的空间来存储最大的成员,所以,共用体的长度为其最大成员的长度。
- 共用体常用于(但并非只用于)节省内存。
4.6.2 枚举的取值范围 (P97)
取值范围定义如下。首先要找出上限,需要知道枚举量的最大值,找到大于这个最大值的、最小的2的幂,将它减去1 。最小值如果不下于0,取值范围下限为0,否则是比最小值还小的负数,如-6,最小范围就是-(2^3 - 1) = -7
4.7 指针和自由存储空间
- 使用常规变量时,值是指定的量,而地址为派生量。而使用指针时,将地址视为指定的量,而将值视为派生量
- 指针名表示的是地址
*运算符被称为间接值(indirect value) 或解除引用 (dereferencing)->也是解除引用运算符
4.7.1 声明和初始化指针
int *p1,p2 // 声明创建一个指针(p1) 和 一个int变量(p2),对于每个指针变量名,都需要使用一个*
4.7.2 指针的危险
- 在C++中创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存 (
P101) - 一定要在对指针应用解除引用运算符(
*)之前,将指针初始化为一个确定的、适当的地址
4.7.6 使用new来创建动态数组
使用new和delete时,应遵守以下规则
- 不要使用
delete来释放不是new分配的内存- 不要使用
delete释放同一个内存块两次- 如果使用
new[]为数组分配内存,则应使用delete[]来释放- 如果使用
new为一个实体分配内存,则应该用delete(没有方括号)来释放- 对空指针应用
delete是安全的
4.8 指针、数组和指针算术
- 指针和数组基本等价的原因在于指针算术(
pointer arithmetic) 和C++内部处理数组的方式。 - 指针算术:将指针变量加
1后,增加的量等于它指向的类型的字节数。将指向double的指针加1后,如果系统对double使用8个字节存储,则数值将增加8.
4.8.1 数组的地址
(P109)
int main() {
int age[] = {1,2,3,4};
int *nextAge = (int *)(&age + 1);
cout << *(nextAge - 1) << endl;
return 0;
}
// 输出4
4.8.3 指针和字符串
一般来说,如果给cout提供一个指针,它将打印地址。但如果指针的类型为char*,则cout将显示指向的字符串。如果要显示的是字符串的地址,则必须将这种指针强制转换为另一种指针类型,如int*
4.8.5 自动存储、静态存储和动态存储
- 自动存储到栈
- 静态存储是整个成熟知性期间都存在的存储方式。使变量称为静态的方式有两种:一种是在函数外面定义它,另一种是在声明变量时使用关键字static
static double free = 56.50
- 动态存储 (有时也叫做自由存储空间或堆)
第五章 循环和关系表达式
5.1.1 for循环的组成部分
-
for循环的组成部分完成下面这些步骤- 设置初始值
- 执行测试,看看循环是否应当继续进行
- 执行循环操作
- 更新用于测试的值
-
C++表达式是值或值与运算符的结合,每个C++表达式都有值 -
从
表达式到语句的转变很容易,只要加分号即可。
5.1.7 前缀格式和后缀格式(P135)
对于内置类型和当代编译器而言,速度没什么问题。然而C++允许您针对类定义这些运算符,在这种情况下,用户这样定义前缀函数:将值加1 ,然后返回结果;但后缀版本首先复制一个副本,将其加1,然后将复制的副本返回。因此,对于类而言,前缀版本的效率比后缀版本高。
5.1.8 递增/递减运算符和指针(P135)
int main() {
int arr[5] = {1,4,7,10,13};
int *pt = arr;
int x = *++pt;
cout << x << endl; // 4
x = *--pt;
cout << x << endl; // 1
int m = ++*pt; // 2
cout << m << endl;
int d = *pt; // 2
cout << d << endl;
int u = *pt++;
cout << u << endl; // 2
cout << *pt << endl; //-----注意这里------ // 4
return 0;
}
5.1.9 组合赋值运算符
代码块由一对花括号和它们包含的语句组成,被视为一条语句。
5.1.11 其他语法技巧--逗号运算符
逗号表达式的值是第二部分的值
int m = i=20,j=2*i; // m 的值是40
5.1.12 关系表达式
关系运算符的优先级比算术运算符低
x+3 > y-2
对应于
(X+3) > (y-2)
5.2.2 等待一段时间:编写延时循环
clock()返回程序开始执行后所用的系统时间。头文件ctime定义了一个符号CLOCKS_PER_SEC,该常量等于每秒钟包含的系统时间单位数。因此,将系统时间除以这个值,可以得到秒数。或者将秒数乘CLOCKS_PER_SEC,可以得到以系统时间单位为单位的时间。其次ctime将clock_t作为clock()返回类型的别名。
5.3 do while 循环
do while 是出口条件循环,意味着这种循环首先执行循环体,然后再判断测试表达式,决定是否继续执行循环。
for 和 while 是入口条件循环。
第六章 分支语句和逻辑运算符
6.2 逻辑表达式
逻辑表达式||、&&优先级比关系运算符低
逻辑表达式!的优先级高于所有关系运算符和算术运算符
逻辑&&运算符的优先级高于逻辑||运算符
6.3 字符函数库(CCType) p177
6.6 break 和 continue 语句
continue 语句用于循环中,让程序跳过循环体中余下的代码,并开始新一轮循环 (跳过本次循环)
break跳过循环体的剩余部分,到达下一条语句(跳过循环)
第七章 函数——C++的编程模块
7.1.2 函数原型和函数调用
原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型(如果有的话)以及参数的类型和数量告诉编译器。- 函数原型的参数列表,不需要提供变量名,有类型列表就足够了。
7.3.5 指针和const
如果数据类型本身并不是指针,则可以将const数据或非const数据的地址赋给指向const的指针,否则只能将非const数据的地址赋给非const指针.p222
7.6 函数和结构
结构作为函数参数传递时,在C++ 中有三种传递方式
- 将结构作为参数传递
- 传递结构的地址
- C++提供了第三种选择--按引用传递
7.9.1 包含一个递归调用的递归
void recurs(argumentlist)
{
statements1
if (test)
recurs(arguments)
statements2
}
递归调用相当于把recurs(arguments)这一行拉高了,内容类似下面这样
void recurs(argumentlist)
{
statements1
if (test)
statements1
if (test)
recurs(arguments)
·
·
·
statements2
statements2
}
test最终将为false,将调用链断开。
递归调用将导致一系列有趣的事件。只要if语句为true,每个recurs()调用都将执行statements1,然后再调用recurs(),而不会执行statements2.当if语句为false时,当前调用将执行statements2.当前调用结束后,程序控制权将返回给调用它的recurs(),而该recurs()将执行其stataments2部分,然后结束,并将控制权返回给前一个调用,依此类推。因此,如果recurs()进行了5次递归调用,则第一个statements1部分将按照函数调用顺序执行5次,然后statements2部分将以与函数调用相反的顺序执行5次。进入5层递归后,程序将沿进入的路径返回。
下面的程序演示了这种现象
#include <iostream>
void countdown(int n);
int main() {
using namespace std; // 命名空间编译指令
countdown(4);
return 0;
}
void countdown(int n) {
using namespace std;
cout << "Counting down ... " << n << " (n at " << &n << ")" << endl;
if (n > 0)
countdown(n-1);
cout << n << ": Kaboom!" <<" (n at " << &n << ")" << endl;
}
注意:每个递归调用都创建自己的一套变量,因此当程序到达第5次调用时,将有5个独立的n变量
下面是该程序的输出
Counting down ... 4 (n at 0x7ffeec8f657c)
Counting down ... 3 (n at 0x7ffeec8f654c)
Counting down ... 2 (n at 0x7ffeec8f651c)
Counting down ... 1 (n at 0x7ffeec8f64ec)
Counting down ... 0 (n at 0x7ffeec8f64bc)
0: Kaboom! (n at 0x7ffeec8f64bc)
1: Kaboom! (n at 0x7ffeec8f64ec)
2: Kaboom! (n at 0x7ffeec8f651c)
3: Kaboom! (n at 0x7ffeec8f654c)
4: Kaboom! (n at 0x7ffeec8f657c)
7.9.2 包含多个递归调用的递归
#include <iostream>
using namespace std; // 命名空间编译指令
const int Len = 66;
const int Divs = 6;
void subdivide(char ar[], int low, int high, int level);
int main() {
char ruler[Len];
int i;
for (i = 1;i < Len - 2; i++)
ruler[i] = ' ';
ruler[Len - 1] = '\0';
int max = Len - 2;
int min = 0;
ruler[min] = ruler[max] = '|';
cout << ruler << endl;
for (i = 1;i <= Divs; i++) {
subdivide(ruler,min,max,i);
cout << ruler << endl;
for (int j = 1; j < Len - 2; j++)
ruler[j] = ' '; // reset to blank ruler(重置为空白标尺)
}
return 0;
}
void subdivide(char ar[], int low, int high, int level) {
// cout << level << " --- "<< &level << endl;
if (level == 0)
return;
int mid = (high + low) / 2;
ar[mid] = '|';
subdivide(ar,low,mid,level-1);
subdivide(ar,mid,high,level-1);
}
7.10.1 函数指针的基础知识
- 函数的地址就是函数名,如果
think()是一个函数,则think就是该函数的地址 - 通常,要声明指向特定类型的函数的指针,可以首先编写这种函数的原型,然后用
(*pf)替换函数名。这样pf就是这类函数的指针。
第八章 函数探幽
8.5 函数模板
template <typename AnyType>
void Swap(AnyType &a, AntType &b) {
Anytype temp;
temp = a;
a = b;
b = temp;
}
第九章 内存模型和名称空间
9.1 单独编译
在同一个文件中只能将同一个头文件包含一次。有一种标准的C/C++技术可以避免多次包含同一个头文件。它是基于预处理器编译指令#ifndef(if not define)。下面的代码片段意味着仅当以前没有使用预处理器编译指令#define定义名称COORDIN_H_时,才处理#ifndef和endif之间的语句:
#ifndef COORDIN_H_
...
#endif
9.2.3 静态持续变量
1.作用域解析运算符 ::,放在变量名前面时,该运算符表示使用变量的全局版本。
2.下表总结了引入名称空间之前使用的存储特性。
| 存储描述 | 持续性 | 作用域 | 链接性 | 如何声明 |
|---|---|---|---|---|
| 自动 | 自动 | 代码块 | 无 | 在代码块中 |
| 寄存器 | 自动 | 代码块 | 无 | 在代码块中,使用关键字register |
| 静态,无链接性 | 静态 | 代码块 | 无 | 在代码块中,使用关键字static |
| 静态,外部链接性 | 静态 | 文件 | 外部 | 不在任何函数内 |
| 静态,内部链接性 | 静态 | 文件 | 内部 | 不在任何函数内,使用关键字static |
9.2.8 函数和链接性
- 所有函数的存储持续性都自动为静态的,默认情况下函数的链接性为外部的,即可以在文件间共享。
- 可以在函数原型中使用关键字
extern来指出函数是在另一个文件中定义的。 static将函数的链接性设置为内部的。
9.2.10 存储方案和动态分配
通常,编译器使用三块独立的内存:一块用于静态变量(可能再细分),一块用于自动变量,另外一块用于动态存储。
9.3.2 新的名称空间特性
using声明和using编译指令
第十章 对象和类
10.2.3 实现类成员函数
- 成员函数顾名思义就是类的成员
- 成员函数的两个特征
- 定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类
- 类方法可以访问类的private组件
- 定义位于类声明中的函数都将自动成为内联函数。
10.3.3 默认构造函数
stock.h
#ifndef SECOND_STOCK_H
#define SECOND_STOCK_H
#include <string>
class Stock
{
private:
std::string company;
long shares;
double share_val;
double total_val;
void set_tot() { total_val = shares * share_val; }
public:
void acquire(const std::string &co, long n, double pr);
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
};
#endif //SECOND_STOCK_H
stock.cpp
#include "Stock.h"
#include <iostream>
void Stock::acquire(const std::string &co, long n, double pr) {
company = co;
if (n < 0) {
std::cout<< "Number of shares can't be negative; "
<< company << "shares set to 0.\n";
shares = 0;
} else
shares = n;
share_val = pr;
set_tot();
}
void Stock::buy(long num, double price) {
if (num < 0) {
std::cout << "Number of shares purchased can't be negative. "
<< "Transaction is aborted.\n";
} else {
shares += num;
share_val = price;
set_tot();
}
}
void Stock::sell(long num, double price)
{
using std::cout;
if (num < 0)
{
cout << "Number of shares sold can't be negative. "
<< "Transaction is aborted.\n";
}
else if (num > shares)
{
cout << "You can't sell more than you have! "
<< "Transaction is aborted.\n";
}
else
{
shares -= num;
share_val = price;
set_tot();
}
}
void Stock::update(double price)
{
share_val = price;
set_tot();
}
void Stock::show()
{
std::cout << "Company: " << company
<< " Shares: " << shares << '\n'
<< " Share Price: $" << share_val
<< " Total Worth: $" << total_val << '\n';
}
- 定义一个构造函数
构造函数声明:
Stock(const string & co, long n, double pr);
实现
Stock::Stock(const string & co, long n, double pr) {
company = co;
shares = n;
share_val = pr;
}
定义一个带初始化的默认构造函数
Stock::Stock()
{
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
- 当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。
- 隐式地调用默认构造函数时,不要使用圆括号。
Stock first; // right
Stock second() ; // wrong
10.3.4 析构函数
- 对象过期时,程序将自动调用一个特殊的成员函数,该函数的名称令人生畏 --- 析构函数。
- 析构函数的名称也很特殊:在类名前加上~。
- 析构函数的声明和实现 (析构函数没有参数)
- 声明
~Stock()
- 实现
Stock::~Stock()
{
}
- 如果构造函数使用了
new,则必须提供使用delete的析构函数。 - 在默认情况下,将一个对象赋给同类型的另一个对象时,C++将源对象的每个数据成员的内容复制到目标对象中相应的数据成员中。
10.4 this指针
this指针指向用来调用成员函数的对象(this被作为隐藏参数传递给方法)
第十一章 实用类
11.1 运算符重载
- 运算符重载是一种形式的
C++多态。
第十二章 类和动态内存分配
12.2.1 修订后的默认构造函数
NULL这是一个表示空指针的C语言宏
12.7.1 队列类
成员初始化列表
- 从概念上说,调用构造函数时,对象将在括号中的代码执行之前被创建。对应
const数据成员,必须在执行到构造函数体之前,即创建对象时进行初始化。C++提供了一种特殊的语法来完成上述工作,它叫做成员初始化列表(member initializer list). - 只有
构造函数可以使用这种初始化列表语法。 - 对于
const类成员和被声明为引用的类成员,必须使用这种语法 - 数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关
- 成员初始化列表将覆盖函数体中的赋值(待验证)。