第1章 文件结构
1.1 版权和版本的声明
/*
* Copyright (c) 2001,上海贝尔有限公司网络应用事业部
* All rights reserved.
*
* 文件名称:
filename.h
* 文件标识:
见配置管理计划书
* 摘 要:
简要描述本文件的内容
*
* 当前版本:
1.1
* 作 者:
输入作者(或修改者)名字
* 完成日期:
2001年7月20日
*
* 取代版本:
1.0
* 原作者 :
输入原作者(或修改者)名字
* 完成日期:
2001年5月10日
*/
|
1.2 头文件的结构
// 版权和版本声明见示例1-1,此处省略。
#ifndef GRAPHICS_H // 防止graphics.h被重复引用
#define GRAPHICS_H
#include <math.h> // 引用标准库的头文件
…
#include “myheader.h” // 引用非标准库的头文件
…
void Function1(…); // 全局函数声明
…
class Box // 类结构声明
{
…
};
#endif
|
1.3 定义文件的结构
// 版权和版本声明见示例1-1,此处省略。
#include “graphics.h” // 引用头文件
…
// 全局函数的实现体
void Function1(…)
{
…
}
// 类成员函数的实现体
void Box::Draw(…)
{
…
}
|
1.4 头文件的作用
1.5 目录结构
第2章 程序的版式
2.1 空行
// 空行
void Function1(…)
{
…
}
// 空行
void Function2(…)
{
…
}
// 空行
void Function3(…)
{
…
}
|
// 空行
while (condition)
{
statement1;
// 空行
if (condition)
{
statement2;
}
else
{
statement3;
}
// 空行
statement4;
}
|
2.2 代码行
int width; // 宽度
int height; // 高度
int depth; // 深度
|
int width, height, depth; // 宽度高度深度
|
x = a + b;
y = c + d;
z = e + f;
|
X = a + b; y = c + d; z = e + f;
|
if (width < height)
{
dosomething();
}
|
if (width < height) dosomething();
|
for (initialization; condition; update)
{
dosomething();
}
// 空行
other();
|
for (initialization; condition; update)
dosomething();
other();
|
2.3 代码行内的空格
void Func1(int x, int y, int z); // 良好的风格
void Func1 (int x,int y,int z); // 不良的风格
|
if (year >= 2000) // 良好的风格
if(year>=2000) // 不良的风格
if ((a>=b) && (c<=d)) // 良好的风格
if(a>=b&&c<=d) // 不良的风格
|
for (i=0; i<10; i++) // 良好的风格
for(i=0;i<10;i++) // 不良的风格
for (i = 0; I < 10; i ++) // 过多的空格
|
x = a < b ? a : b; // 良好的风格
x=a<b?a:b; // 不好的风格
|
int *x = &y; // 良好的风格
int * x = & y; // 不良的风格
|
array[5] = 0; // 不要写成 array [ 5 ] = 0;
a.Function(); // 不要写成 a . Function();
b->Function(); // 不要写成 b -> Function();
|
2.4 对齐
void Function(int x)
{
… // program code
}
|
void Function(int x){
… // program code
}
|
if (condition)
{
… // program code
}
else
{
… // program code
}
|
if (condition){
… // program code
}
else {
… // program code
}
|
for (initialization; condition; update)
{
… // program code
}
|
for (initialization; condition; update){
… // program code
}
|
While (condition)
{
… // program code
}
|
while (condition){
… // program code
}
|
如果出现嵌套的{},则使用缩进对齐,如:
{
…
{
…
}
…
}
|
|
2.5 长行拆分
if ((very_longer_variable1 >= very_longer_variable12)
&& (very_longer_variable3 <= very_longer_variable14)
&& (very_longer_variable5 <= very_longer_variable16))
{
dosomething();
}
|
virtual CMatrix CMultiplyMatrix (CMatrix leftMatrix,
CMatrix rightMatrix);
|
for (very_longer_initialization;
very_longer_condition;
very_longer_update)
{
dosomething();
}
|
2.6 修饰符的位置
2.7 注释
/*
* 函数介绍:
* 输入参数:
* 输出参数:
* 返回值 :
*/
void Function(float x, float y, float z)
{
…
}
|
if (…)
{
…
while (…)
{
…
} // end of while
…
} // end of if
|
2.8 类的版式
class A
{
private:
int i, j;
float x, y;
…
public:
void Func1(void);
void Func2(void);
…
}
|
class A
{
public:
void Func1(void);
void Func2(void);
…
private:
int i, j;
float x, y;
…
}
|
第3章 命名规则
3.1 共性规则
几十年前老ANSI C规定名字不准超过6个字符,现今的C+ +/C不再有此限制。一般来说,长名字能更好地表达含义,所以函数名、变量名、类名长达十几个字符不足为怪。那么名字是否越长约好?不见得! 例如变量名maxval就比maxValueUntilOverflow好用。单字符的名字也是有用的,常见的如i,j,k,m,n,x,y,z等,它们通常可用作函数内的局部变量。
3.2 简单的Windows应用程序命名规则
3.3 简单的Unix应用程序命名规则
第4章 表达式和基本语句
4.1 运算符的优先级
优先级
|
运算符
|
结合律
|
从
高
到
低
排
列
|
( ) [ ] -> .
|
从左至右
|
! ~ ++ -- (类型) sizeof
+ - * &
|
从右至左
| |
* / %
|
从左至右
| |
+ -
|
从左至右
| |
<< >>
|
从左至右
| |
< <= > >=
|
从左至右
| |
== !=
|
从左至右
| |
&
|
从左至右
| |
^
|
从左至右
| |
|
|
从左至右
| |
&&
|
从左至右
| |
||
|
从右至左
| |
?:
|
从右至左
| |
= += -= *= /= %= &= ^=
|= <<= >>=
|
从左至右
|
4.2 复合表达式
4.3 if 语句
有时候我们可能会看到 if (NULL == p) 这样古怪的格式。不是程序写错了,是程序员为了防止将 if (p == NULL) 误写成 if (p = NULL),而有意把p和NULL颠倒。编译器认为 if (p = NULL) 是合法的,但是会指出 if (NULL = p)是错误的,因为NULL不能被赋值。
4.4 循环语句的效率
for (row=0; row<100; row++)
{
for ( col=0; col<5; col++ )
{
sum = sum + a[row][col];
}
}
|
for (col=0; col<5; col++ )
{
for (row=0; row<100; row++)
{
sum = sum + a[row][col];
}
}
|
l 【建议4-4-2】如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。示例4- 4(c)的程序比示例4-4(d)多执行了N-1次逻辑判断。并且由于前者老要进行逻辑判断,打断了循环“流水线”作业,使得编译器不能对循环进行优化处理,降低了效率。如果N非常大,最好采用示例4-4(d)的写法,可以提高效率。如果N非常小,两者效率差别并不明显,采用示例4-4(c)的写法比较好,因为程序更加简洁。
for (i=0; i<N; i++)
{
if (condition)
DoSomething();
else
DoOtherthing();
}
|
if (condition)
{
for (i=0; i<N; i++)
DoSomething();
}
else
{
for (i=0; i<N; i++)
DoOtherthing();
}
|
4.5 for 语句的循环控制变量
for (int x=0; x<N; x++)
{
…
}
|
for (int x=0; x<=N-1; x++)
{
…
}
|
4.6 switch语句
4.7 goto语句
第5章 常量
5.1 为什么需要常量
5.2 const 与 #define的比较
5.3 常量定义规则
5.4 类中的常量
有时我们希望某些常量只在类中有效。由于#define 定义的宏常量是全局的,不能达到目的,于是想当然地觉得应该用const修饰数据成员来实现。const数据成员的确是存在的,但其含义却不是我们所期望的。const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。
第6章 函数设计
6.1 参数的规则
6.2 返回值的规则
6.3 函数内部实现的规则
6.4 其它建议
6.5 使用断言
void *memcpy(void *pvTo, const void *pvFrom, size_t size)
{
assert((pvTo != NULL) && (pvFrom != NULL)); // 使用断言
byte *pbTo = (byte *) pvTo; // 防止改变pvTo的地址
byte *pbFrom = (byte *) pvFrom; // 防止改变pvFrom的地址
while(size -- > 0 )
*pbTo ++ = *pbFrom ++ ;
return pvTo;
}
|
很少有比跟踪到程序的断言,却不知道该断言的作用更让人沮丧的事了。你化了很多时间,不是为了排除错误,而只是为了弄清楚这个错误到底是什么。有的时候,程序员偶尔还会设计出有错误的断言。所以如果搞不清楚断言检查的是什么,就很难判断错误是出现在程序中,还是出现在断言中。幸运的是这个问题很好解决,只要加上清晰的注释即可。这本是显而易见的事情,可是很少有程序员这样做。这好比一个人在森林里,看到树上钉着一块“危险”的大牌子。但危险到底是什么?树要倒?有废井?有野兽?除非告诉人们“危险”是什么,否则这个警告牌难以起到积极有效的作用。难以理解的断言常常被程序员忽略,甚至被删除。[Maguire, p8-p30]
6.6 引用与指针的比较
第7章 内存管理
7.1内存分配方式
7.2常见的内存错误及其对策
7.3指针与数组的对比
char a[] = “hello”;
a[0] = ‘X’;
cout << a << endl;
char *p = “world”; // 注意p指向常量字符串
p[0] = ‘X’; // 编译器不能发现该错误
cout << p << endl;
|
不能对数组名进行直接复制与比较。示例7-3-2中,若想把数组a的内容复制给数组b,不能用语句 b = a ,否则将产生编译错误。应该用标准库函数strcpy进行复制。同理,比较b和a的内容是否相同,不能用if(b==a) 来判断,应该用标准库函数strcmp进行比较。
语句p = a 并不能把a的内容复制指针p,而是把a的地址赋给了p。要想复制a的内容,可以先用库函数malloc为p申请一块容量为strlen(a)+1个字符的内存,再用strcpy进行字符串复制。同理,语句if(p==a) 比较的不是内容而是地址,应该用库函数strcmp来比较。
// 数组…
char a[] = "hello";
char b[10];
strcpy(b, a); // 不能用 b = a;
if(strcmp(b, a) == 0) // 不能用 if (b == a)
…
|
// 指针…
int len = strlen(a);
char *p = (char *)malloc(sizeof(char)*(len+1));
strcpy(p,a); // 不要用 p = a;
if(strcmp(p, a) == 0) // 不要用 if (p == a)
…
|
char a[] = "hello world";
char *p = a;
cout<< sizeof(a) << endl; // 12字节
cout<< sizeof(p) << endl; // 4字节
|
void Func(char a[100])
{
cout<< sizeof(a) << endl; // 4字节而不是100字节
}
|
7.4指针参数是如何传递内存的?
void GetMemory(char *p, int num)
{
p = (char *)malloc(sizeof(char) * num);
}
|
void Test(void)
{
char *str = NULL;
GetMemory(str, 100); // str 仍然为 NULL
strcpy(str, "hello"); // 运行错误
}
|
毛病出在函数GetMemory 中。编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把 _p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。
void GetMemory2(char **p, int num)
{
*p = (char *)malloc(sizeof(char) * num);
}
|
void Test2(void)
{
char *str = NULL;
GetMemory2(&str, 100); // 注意参数是 &str,而不是str
strcpy(str, "hello");
cout<< str << endl;
free(str);
}
|
char *GetMemory3(int num)
{
char *p = (char *)malloc(sizeof(char) * num);
return p;
}
|
void Test3(void)
{
char *str = NULL;
str = GetMemory3(100);
strcpy(str, "hello");
cout<< str << endl;
free(str);
}
|
char *GetString(void)
{
char p[] = "hello world";
return p; // 编译器将提出警告
}
|
void Test4(void)
{
char *str = NULL;
str = GetString(); // str 的内容是垃圾
cout<< str << endl;
}
|
char *GetString2(void)
{
char *p = "hello world";
return p;
}
|
void Test5(void)
{
char *str = NULL;
str = GetString2();
cout<< str << endl;
}
|
函数Test5 运行虽然不会出错,但是函数GetString2的设计概念却是错误的。因为GetString2内的“hello world”是常量字符串,位于静态存储区,它在程序生命期内恒定不变。无论什么时候调用GetString2,它返回的始终是同一个“只读”的内存块。
7.5 free和delete把指针怎么啦?
char *p = (char *) malloc(100);
strcpy(p, “hello”);
free(p); // p 所指的内存被释放,但是p所指的地址仍然不变
…
if(p != NULL) // 没有起到防错作用
{
strcpy(p, “world”); // 出错
}
|
7.6 动态内存会被自动释放吗?
void Func(void)
{
char *p = (char *) malloc(100); // 动态内存会自动释放吗?
}
|
7.7 杜绝“野指针”
7.8 有了malloc/free为什么还要new/delete ?
class Obj
{
public :
Obj(void){ cout << “Initialization” << endl; }
~Obj(void){ cout << “Destroy” << endl; }
void Initialize(void){ cout << “Initialization” << endl; }
void Destroy(void){ cout << “Destroy” << endl; }
};
|
void UseMallocFree(void)
{
Obj *a = (obj *)malloc(sizeof(obj)); // 申请动态内存
a->Initialize(); // 初始化
//…
a->Destroy(); // 清除工作
free(a); // 释放内存
}
|
void UseNewDelete(void)
{
Obj *a = new Obj; // 申请动态内存并且初始化
//…
delete a; // 清除并且释放内存
}
|
7.9 内存耗尽怎么办?
void main(void)
{
float *p = NULL;
while(TRUE)
{
p = new float[1000000];
cout << “eat memory” << endl;
if(p==NULL)
exit(1);
}
}
|
7.10 malloc/free 的使用要点
为什么free函数不象malloc函数那样复杂呢?这是因为指针p的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。如果p是NULL指针,那么free对p无论操作多少次都不会出问题。如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误。
7.11 new/delete 的使用要点
7.12 一些心得体会
第8章 C++函数的高级特性
8.1 函数重载的概念
在C ++程序中,可以将语义、功能相似的几个函数用同一个名字表示,即函数重载。这样便于记忆,提高了函数的易用性,这是C++语言采用重载机制的一个理由。例如示例8-1-1中的函数EatBeef,EatFish,EatChicken可以用同一个函数名Eat表示,用不同类型的参数加以区别。
void EatBeef(…); // 可以改为 void Eat(Beef …);
void EatFish(…); // 可以改为 void Eat(Fish …);
void EatChicken(…); // 可以改为 void Eat(Chicken …);
|
所以只能靠参数而不能靠返回值类型的不同来区分重载函数。编译器根据参数为每个重载函数产生不同的内部标识符。例如编译器为示例8-1-1中的三个Eat函数产生象_eat_beef、_eat_fish、_eat_chicken之类的内部标识符(不同的编译器可能产生不同风格的内部标识符)。
示例8-1-3中,第一个output函数的参数是int类型,第二个output函数的参数是float类型。由于数字本身没有类型,将数字当作参数时将自动进行类型转换(称为隐式类型转换)。语句output(0.5)将产生编译错误,因为编译器不知道该将0.5转换成int还是float类型的参数。隐式类型转换在很多地方可以简化程序的书写,但是也可能留下隐患。
# include <iostream.h>
void output( int x); // 函数声明
void output( float x); // 函数声明
void output( int x)
{
cout << " output int " << x << endl ;
}
void output( float x)
{
cout << " output float " << x << endl ;
}
void main(void)
{
int x = 1;
float y = 1.0;
output(x); // output int 1
output(y); // output float 1
output(1); // output int 1
// output(0.5); // error! ambiguous call, 因为自动类型转换
output(int(0.5)); // output int 0
output(float(0.5)); // output float 0.5
}
|
8.2 成员函数的重载、覆盖与隐藏
#include <iostream.h>
class Base
{
public:
void f(int x){ cout << "Base::f(int) " << x << endl; }
void f(float x){ cout << "Base::f(float) " << x << endl; }
virtual void g(void){ cout << "Base::g(void)" << endl;}
};
|
class Derived : public Base
{
public:
virtual void g(void){ cout << "Derived::g(void)" << endl;}
};
|
void main(void)
{
Derived d;
Base *pb = &d;
pb->f(42); // Base::f(int) 42
pb->f(3.14f); // Base::f(float) 3.14
pb->g(); // Derived::g(void)
}
|
#include <iostream.h>
class Base
{
public:
virtual void f(float x){ cout << "Base::f(float) " << x << endl; }
void g(float x){ cout << "Base::g(float) " << x << endl; }
void h(float x){ cout << "Base::h(float) " << x << endl; }
};
|
class Derived : public Base
{
public:
virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }
void g(int x){ cout << "Derived::g(int) " << x << endl; }
void h(float x){ cout << "Derived::h(float) " << x << endl; }
};
|
void main(void)
{
Derived d;
Base *pb = &d;
Derived *pd = &d;
// Good : behavior depends solely on type of the object
pb->f(3.14f); // Derived::f(float) 3.14
pd->f(3.14f); // Derived::f(float) 3.14
// Bad : behavior depends on type of the pointer
pb->g(3.14f); // Base::g(float) 3.14
pd->g(3.14f); // Derived::g(int) 3 (surprise!)
// Bad : behavior depends on type of the pointer
pb->h(3.14f); // Base::h(float) 3.14 (surprise!)
pd->h(3.14f); // Derived::h(float) 3.14
}
|
class Base
{
public:
void f(int x);
};
|
class Derived : public Base
{
public:
void f(char *str);
};
|
void Test(void)
{
Derived *pd = new Derived;
pd->f(10); // error
}
|
8.3 参数的缺省值
#include <iostream.h>
void output( int x);
void output( int x, float y=0.0);
|
void output( int x)
{
cout << " output int " << x << endl ;
}
|
void output( int x, float y)
{
cout << " output int " << x << " and float " << y << endl ;
}
|
void main(void)
{
int x=1;
float y=0.5;
// output(x); // error! ambiguous call
output(x,y); // output int 1 and float 0.5
}
|
8.4 运算符重载
运算符
|
规则
|
所有的一元运算符
|
建议重载为成员函数
|
= () [] ->
|
只能重载为成员函数
|
+= -= /= *= &= |= ~= %= >>= <<=
|
建议重载为成员函数
|
所有其它运算符
|
建议重载为全局函数
|
8.5 函数内联
在C 程序中,可以用宏代码提高执行效率。宏代码本身不是函数,但使用起来象函数。预处理器用复制宏代码的方式代替函数调用,省去了参数压栈、生成汇编语言的 CALL调用、返回参数、执行return等过程,从而提高了速度。使用宏代码最大的缺点是容易出错,预处理器在复制宏代码时常常产生意想不到的边际效应。例如
让我们看看C++ 的“函数内联”是如何工作的。对于任何内联函数,编译器在符号表里放入函数的声明(包括名字、参数类型、返回值类型)。如果编译器没有发现内联函数存在错误,那么该函数的代码也被放入符号表里。在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样)。如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。这个过程与预处理有显著的不同,因为预处理器不能进行类型安全检查,或者进行自动类型转换。假如内联函数是成员函数,对象的地址(this)会被放在合适的地方,这也是预处理器办不到的。
所以说,inline是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。一般地,用户可以阅读函数的声明,但是看不到函数的定义。尽管在大多数教科书中内联函数的声明、定义体前面都加了inline关键字,但我认为inline不应该出现在函数的声明中。这个细节虽然不会影响函数的功能,但是体现了高质量C++/C程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联:
8.6 一些心得体会
C++ 语言中的重载、内联、缺省参数、隐式转换等机制展现了很多优点,但是这些优点的背后都隐藏着一些隐患。正如人们的饮食,少食和暴食都不可取,应当恰到好处。我们要辨证地看待C++的新机制,应该恰如其分地使用它们。虽然这会使我们编程时多费一些心思,少了一些痛快,但这才是编程的艺术。第9章 类的构造函数、析构函数与赋值函数
9.1 构造函数与析构函数的起源
作为比C更先进的语言,C++提供了更好的机制来增强程序的安全性。C++编译器具有严格的类型安全检查功能,它几乎能找出程序中所有的语法问题,这的确帮了程序员的大忙。但是程序通过了编译检查并不表示错误已经不存在了,在“错误”的大家庭里,“语法错误”的地位只能算是小弟弟。级别高的错误通常隐藏得很深,就象狡猾的罪犯,想逮住他可不容易。
9.2 构造函数的初始化表
B::B(const A &a)
: m_a(a)
{
…
}
|
B::B(const A &a)
{
m_a = a;
…
}
|
F::F(int x, int y)
: m_x(x), m_y(y)
{
m_i = 0;
m_j = 0;
}
|
F::F(int x, int y)
{
m_x = x;
m_y = y;
m_i = 0;
m_j = 0;
}
|
9.3 构造和析构的次序
一个有趣的现象是,成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。这是因为类的声明是唯一的,而类的构造函数可以有多个,因此会有多个不同次序的初始化表。如果成员对象按照初始化表的次序进行构造,这将导致析构函数无法得到唯一的逆序。[Eckel, p260-261]
9.4 示例:类String的构造函数与析构函数
9.5 不要轻视拷贝构造函数与赋值函数
9.6 示例:类String的拷贝构造函数与赋值函数
// 内容自赋值
b = a;
…
c = b;
…
a = c;
|
// 地址自赋值
b = &a;
…
a = *b;
|
9.7 偷懒的办法处理拷贝构造函数与赋值函数
9.8 如何在派生类中实现类的基本函数
9.9 一些心得体会
第10章 类的继承与组合
10.1 继承
10.2 组合
class Eye
{
public:
void Look(void);
};
|
class Nose
{
public:
void Smell(void);
};
|
class Mouth
{
public:
void Eat(void);
};
|
class Ear
{
public:
void Listen(void);
};
|
// 正确的设计,虽然代码冗长。
class Head
{
public:
void Look(void) { m_eye.Look(); }
void Smell(void) { m_nose.Smell(); }
void Eat(void) { m_mouth.Eat(); }
void Listen(void) { m_ear.Listen(); }
private:
Eye m_eye;
Nose m_nose;
Mouth m_mouth;
Ear m_ear;
};
|
// 功能正确并且代码简洁,但是设计方法不对。
class Head : public Eye, public Nose, public Mouth, public Ear
{
};
|
第11章 其它编程经验
11.1 使用const提高函数的健壮性
为了提高效率,可以将函数声明改为void Func(A &a),因为“引用传递”仅借用一下参数的别名而已,不需要产生临时对象。但是函数void Func(A &a) 存在一个缺点:“引用传递”有可能改变参数a,这是我们不期望的。解决这个问题很容易,加const修饰即可,因此函数最终成为void Func(const A &a)。
对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const引用传递”,目的是提高效率。例如将void Func(A a) 改为void Func(const A &a)。
|
对于内部数据类型的输入参数,不要将“值传递”的方式改为“const引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void Func(int x) 不应该改为void Func(const int &x)。
|
如果返回值不是内部数据类型,将函数A GetA(void) 改写为const A & GetA(void)的确能提高效率。但此时千万千万要小心,一定要搞清楚函数究竟是想返回一个对象的“拷贝”还是仅返回“别名”就可以了,否则程序会出错。见6.2节“返回值的规则”。
++ m_num; // 编译错误,企图修改数据成员m_num