标题:
重
载
函数再
论
重
载
函数是
C++
提出来的概念,但是在
C
中却未必没有。比如
“1+3”
和
“1.0+3.0”
,
虽
然都是加法,做的却不是同的操作:
编译
器要因操作数的不同而
调
用不同的加法操作。只是
C
语
言中除了内部
类
型
变
量可以参与运算以外,没有
“
类
”
这么
高深的概念。
“
结
构体
”
也只是内存数据的
组织
方法,而不
涉
及
对
整个
结
构体的
处
理。所以,在
C
语
言
时
代
编译
器明明做了
类
似于重
载
的事情,却可以像雷
锋
一
样
“
做好事不留名
”
。
C++
发
展出了
类
,并且
赋
予了
“
类
”
很高的期望,
类
的
对
象也能像内置
类
型
对
象一
样
参与一切运算。那
么
,就拿加法运算来
说
,
编译
器如何知道
对
某
类对
象的加法
该调
用哪一个
详细
的操作代
码
?于是,即使不出
现
普通函数的重
载
,至少运算符是要重
载
的。
林
锐
博士在《高
质
量
C++/C
编
程指南》中
为
重
载
函数的必要性提了另一个理由:
类
的构造函数名称必
须
与
类
名相同,而
类
却
经
常要定
义
多个不同的构造函数。那就只好重
载
了。
对
于普通程序
员
来
说
,我
们
完全可以不用考
虑
得
这么
深。重
载
函数
给
我
们
至少
还带
来了另一个好
处
:不用
记忆
多个不同的函数名了,也不用
为
了
给
函数起名而
绞
尽
脑
汁了。不
过
本
书还给
出了一个建
议
:并不是任何
时
候都有必要重
载
函数的,有的
时
候不同的函数名可
以直
观
地
带
来好多信息,
滥
用重
载
只是
牺
牲了名称中的信息。
标题:
:重
载
函数的概念
引用:出
现
在相同作用域中的两个(可以是两个以上
——
偷
猫注)函数,如果具有
相同的名字而形参表不同,
则
称
为
重
载
函数。
本
节开头
第一句
话
就
给
出了重
载
函数的定
义
:重
载
函数必
须
符合两个条件:一是出
现
在相同的作用域中、二是函数名字相同而形参表不同。
其中第一个条件一般人往往是不去想的,其
实
函数名相同而作用域不同的函数大大存在,比如在
MFC
中就有。它
们
是完全不相干的函数。
第二个条件
还
可以
详说
一下:函数名字相同当然不在
话
下,
这
是函数被称
为
“
重
载
”
的根源。之于形参表不同,可能表
现
在形参个数不同、可能表
现
在形参
类
型不同、
还
可能表
现
在形参
顺
序不同。
如果要
扩
展
开
来
说
,
还
可以
举
出
许
多
不是重
载
函数的情况。
一、如果既在同一作用域下、名称也相同、形参表也相同,
则
后者被
视为
前者的重
复
声明。
——
函数可以重
复
声明,因
为
函数的声明并不
产
生目
标
代
码
,但是函数的定
义
不允
许
重
复
出
现
。
二、如果既在同一作用域下、名称也相同、形参表也相同,但是返回
值
不同,
则
后者被
视为错误
的声明。函数不可以只凭返回
值
来区分,因
为调
用函数的
时
候只凭名称和形参来
选择
函数,而不凭返回
值
。再究其原因,一是因
为
函数的返回
值
可以被
丢
弃;二来即使不
丢
弃,将返回
值赋
予另一个
变
量之前没必要
检查
我需要什
么样
的返回
值
,而能否
赋值
也与函数本
身无
关
。
三、有些
时
候看起来形参表不同,
实际
上是完全相同的,
书
本第
229
页讲
了四
组这样
的例子:
Record lookup(const Account &acct);
Record lookup(const Account &);//
区
别
在于有没有
给
形参命名
typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&);//
只是
给类
型取了个
别
名
Record lookup(const Phone&, const Name&);
Record lookup(const Phone&, const Name& = "");//
区
别
在于
给
形参提供了默
认值
Record lookup(Phone);
Record lookup(const Phone);//
区
别
在于是否
const
其中第三
组
可能会
让
人
产
生函数的形参个数不同的假像,其
实
可缺省的形参并没有减少形参的个数。第四
组
有点不容易搞清:因
为
有的
时
候可以凭是否
const
来重
载
,比如引用
传递
和指
针传递
。
标题:
:文件的
组织
一个程序往往由多个源文件
组
成,
这
些代
码
究竟
应该
放在哪个源文件里、哪些代
码
可以放在同一个源文件里、哪些代
码
必需分
开
放。
这
是一个管理
层
面的
问题
。
说
它是管理
层
面的
问题
,是因
为这
些代
码
的
组织
往往没有惟一的准
则
。但是它
们还
是有一定的
规
律的。
首先,
软
件的
维护
是一个
复杂
的系
统
工程。代
码
的
组织应该
有利于
维护
。
应该
尽量把直接相
关
的内容放在同一文件、不相
关
的内容放在不同的文件里。如果
这
些代
码还
有
亲
和疏,那就要分不同的文件
夹
来存放了。
其次,
软
件的代
码
是一个
严
格的
组织
体系。不同的内容之
间
可能是并列的,也可能有必要的先后
关
系。于是在
“#include”
的
时
候要注意
顺
序。
最后,也是最重要的一点,有些代
码
在同一工程中可以重用(或必
须
重用),有些代
码
在同一个工程中只能出
现
一次。可以重用的有
类
的声明、函数的声明、
变
量的声明等,不可以重用的是
类
的
实
体、函数的
实
体、
变
量的定
义
等。那
么
,把可以重用的内容放在
h
文件中,把不可以重用的放在
cpp
文件中是一个好
办
法。
拿
类
的声明和
类
的
实
体
为
例,如果把一个
类
的所有内容一古
脑
放在同一个文件中,将可能出
现问题
。因
为
在其它用到
类实
例的地方都必
须让类
的声明
“
可
见
”
,所以我
们
往往在文件
头
部加个
“#include”
,
结
果
类
的
实
体也被
编译
多次,在
连
接
时产
生冲突。
在前文中曾提到
过
,内
联
函数是惟一允
许
(也是必
须
)在
编译时让
函数
实
体可
见
的的函数。所以内
联
函数可以放在
h
文件中。
C++
规则
中有一句正好与此照
应
:在
类
的声明中直接写出的函数被
认为
是内
联
函数。
Visual C++
给类
的文件起默
认
名
时
,文件名往往与
类
名一致。如果
类
名由
“C”
开头
,
则
文件会是除去
开头
的
“C”
字以外的其它文字。如
类
“CMyClass”
,它的代
码
存放在以下两个文件中:
“MyClass.h”
和
“MyClass.cpp”
中。原因是
VC++
建
议类
名以
C
开头
,至于
为
什
么
在文件名中不出
现开头
的
“C”
,可能是出于微
软
的
习惯
吧
。
标题:
:
类
的构造函数
引用:构造函数是特殊的成
员
函数。
笔
记
:构造函数的确是一
类
“
特殊
”
的成
员
函数。它的特殊性至少表
现
在以下几个方面:一是它的
调
用不用程序
员
操心,只要
类对
象被
创
建它就会被
调
用,而且它不允
许
被程序
员显
式地
调
用。二是它
们
是必需的,如果程序
员偷懒
,
编译
器将自
动创
建
简单
的构造函数。三是它
们
的名字不用程序
员
多考
虑
,直接与
类
名相同。四是它
们
没有返回
值
。
下面
详说这
几个特性:
一、它
们
在
类对
象被
创
建
时
自
动调
用,
创
建
对
象可能有以下方法:程序中用声明
变
量的
语
句直接声明
创
建,或者在程序中用
new
关键
字
动态创
建。
这
两
种
方法都可以
创
建
单
个
对
象,也都可以
创
建
对
象数
组
。只要有一个
对
象被
创
建,构造函数就被
调
用一次。
如果程序
员
想
显
式地
调
用构造函数那是不行的。正因
为
如此,构造函数中
还
有一
种
特定的部分叫
“
初始化列表
”
,通
过
它程序
员
可以
调
用基
类
或成
员
的构造函数。必竟
类
的
设计
千差万
别
,如果某个
类
的基
类
或(和)成
员
有多个构造函数,那
么
,
该类
必
须
能
够
指定用哪一个构造函数,否
则类
的功能将大打折扣。
调
用构造函数不是程序
员
的事,程序
员
不
应该
管也管不了。初始化列表
为
解决
这
个
问题
而生,所以只有构造函数才有初始化列表,其它函数不能有。
上面
说
到的
“
大打折扣
”
究竟是怎
样
的折扣呢?如果
不能指定基
类
和成
员
用哪一个构造函数,那就只好
让编译
器去挑了,构造出来的
对
象往往不符合要求,只好
调
用基
类
和成
员
的其它函数,比如
赋值
函数或其它
进
行参数
设
定的函数
——
当然,基
类
和成
员
必
须
包含
这样
的函数。
这样
就浪
费
了
资
源。
二、
类
必
须
包含构造函数
——
确切地
说
是必
须
包含无参数构造函数和拷
贝
构造函数
——
原因是因
为
它
们
的
调
用是自
动
的。如果
这
两个函数根本就没有,你
让
系
统
如何
调
用?所以,
C++
也不含糊,你要是
懒
得写,它就帮你写一个
简单
的。
简单
就意味着至少要
丧
失一些功能,如果
类设计
得比
较复杂
(比如包含指
针
操作)
还
可能引起灾
难
性事故。
三、函数名与
类
名一致。构造函数的名称是必
须
特殊的,即使
这
个特殊不表
现
在与
类
名相同,也必
须
找到另一个
规则
来
实现
。因
为
系
统
要自
动调
用
这
些函数,你就必
须让
系
统
知道哪些函数是构造函数。
第四个特性直接改
变
了
C/C++
语
言的一条
规则
:
C
语
言
规
定,如果函数没有明
显
指出返回
类
型,那
么
C
语
言
认为
返回
值
是
int
型。
C
语
言之所以可以有
这
条
规则
,一是因
为
返回
int
的函数很多,二是因
为
即使没有返回
值
,也必
须
指明
void
。当
时
制定
规则
的人无法
预
料到,
C++
中居然会出
现
“
连
void
都不是的返回
值
”
的函数,
void
虽
然表示不返回任何
值
,必竟与
类
构造函数的
“
没有返回
值
”
是两
码
事。于是,
C++
新
标
准
规
定:在定
义
或声明函数
时
,没有
显
式指定返回
类
型中不合法的。当然
类
的构造函数除外。
构造函数的出
现
有它的可行院
捅
厝恍浴?尚行允怯捎
贑
++
的
类
允
许
包含成
员
函数,既然
类
可以包含普通的成
员
函数,那
么
包含特殊的函数自然也不在
话
下。必然性是由于
类
的
对
象往往必
须经过
特定的初始化。
C++
到来之前,
C
语
言中的数据
类
型只是内置
类
型。
对
于内置
类
型
对
象,如果忘了初始化,大不了
这
个
对
象失去作用,但是不会
导
致大的
问题
。比如一个
int
型
值
,无
论
内存如何随
机,它的取
值
范
围
都不会超
过
int
能表达的范
围
,
对
它
进
行运算也不会
产
生危
险
(溢出不能算危
险
,即使初始化
过
的数据也不能保
证
不溢出,而且溢出只是一
种逻辑问题
)。但是
现
在的
类
不
这么简单
了,忘了初始化往往将
带
来运行
错误
。于其
每
次都要考
虑
数据的初始化,
还
不如把
这
个初始化写成
统
一的函数,
让
系
统
自
动调
用来得既安全又方便
。
标题:
:
类
的成
员
函数
类
与
C
语
言中的
结
构体最大的区
别
就是
类
可以
带
函数,而
结
构体只是一个内存
组
合
。所以,要提
类
就不得不提成
员
函数。
类
的成
员
函数与普通函数(全局函数)相比,最根本的区
别
是
实现
了
类
的封装性。封装性的第一个表
现
是
访问权
限:都是函数,但是你能
访问
哪个不能
访问
哪个却可以
设
定。第二个表
现
是直
观
,通
过类
成
员
(或指
针
)来
调
用函数,
给
人的直
觉
就是
“
这
是
类
提供的功能
”
。你好像
“Bird.Fly();”
一
样
一目了然。
在理解
this
指
针
以前要想
彻
底理解成
员
函数是有困
难
的,我就曾以
为
在
类
的
实
例中保存了函数的副本。要不然,
为
什
么
同一个
类
的不同
对
象
调
用
这
个函数有不同的效果呢?原来,在函数所有的形参之外,
还
有一个不用你操心的参数
this
,它是一个指
针
,
该
指
针
的目
标
就是函数的
调
用者。
这么
一
说
就明白了。
函数形参表后加入
const
就成了
“const
成
员
函数
”
,
这样
的函数保
护
了
调
用者自身不被修改。如
CString
的
GetLength()
函数,你只能
获
取它的
长
度,不能修改它的内容或
长
度。加入
const
的作用倒不是怕
调
用者修改,而是防止
编
写函数的人不小心改
动
了
对
象。因
为
百密
总
有一疏,万一在某个不
该
修改数据的函数中改
变
了数据(比如将
“==”
写成
“=”
),或者万一
调
用了另一个非
const
的成
员
函数都将可能引起
错误
。在
编
写函数前就先加上
const
可以
记编译
器来帮你
检查
。
这
个
const
加在形参表的后面
显
得有些怪怪的,造成
“
怪怪的
”
原因就是因
为
函数的形参表中没有
this
,也就没有能用
const
来修
饰
的
东
西了。林
锐说
“
大概是因
为
其它地方都已
经
被占用了
”
并不是根本原因。
标题:
:内
联
函数
内
联
函数
应该
是
为
了改善
C
语
言中的宏替
换
的不足而
产
生的吧。因
为
宏替
换
是
预编译
中直接展
开
的,展
开过
程中将
产
生意想不到的
结
果。
典型的有
“#define MAX(a, b) (a) > (b) ? (a) : (b)”
。
“result = MAX(i, j)+2;”
将被展
开为
“result = (i) > (j) ? (i) : (j) + 2;”
。
虽
然外面再加一
对
括号可以解决以上
问题
,但是
“result = MAX(i++, j);”
被展
开
后将
导
致
i
被自增
1
了两次。
(以上例子摘自林
锐
博士的《高
质
量
C++/C
编
程指
南》第
66
页
,林
锐
管
这
叫做
“
边际
效
应
”
)
C++
用内
联
来取代宏替
换
,大大提高了安全性。
虽
然内
联
函数也是
编译时
展
开
的,但是它能
进
行安全
检查
,
还
能
处
理
类
的成
员
函数(原因是内
联
函数能
够处
理
this
指
针
,宏却不能)。
引用:内
联对编译
器来
说
只是一个建
议
,
编译
器可以
选择
忽略
这
个建
议
。
笔
记
:也就是
说
,有些函数你想内
联
,
编译
器也不一定会采
纳
。因
为
内
联
函数
虽
然减少了函数
调
用的
开销
,却增加了程序的体
积
。
内
联
函数是唯一允
许实
体多次被
编译
的函数。原因是
编译
器必
须
先
编译这
个函数体,才能在
编译
函数
调
用的地方
进
行合
理地展
开
。
这
就
说
明在多个
CPP
文件
组
成的工程中,可能有不止一个
CPP
文件中要有函数的
实
体。
既然
这样
,就放
进头
文件吧
。
对
本文本的
评论
有:
我
觉
得象
这
个
max()
和以前的数
组
越界一
类
的事
,
都可以
归纳为
一句
话
,
那就是
,C
为
我
们
提供了
强
大的工具
,
那些不会使用的人才会出
现这种错误
.
连
个数
组
越界也管理不好的
,
还
是去写武侠小
说
比
较
好
.
比如火
药发
明了以后
,
我
们
可以用来炸山
开
路什
么
的
,
难
道因
为
有人用于
战
争
,
就怪
这
个火
药
功能不
够
完善
吗
?
是这样的,我们不应该怪C标准不好,
虽然它不能让result = MAX(i++, j);这种问题得到解决,
产生i被自增两次这样的结果,程序员应该自己去避免。
但是,如果标准有进步了,我们倒是因为祝贺它一下。
标题:
:局部
对
象与静
态
局部
对
象
本
节
首先向
读
者
说
明了
“
名字的作用域
”
和
“
对
象的生命周期
”
这
两个概念,不
难
,理解了就行了。前者是空
间
概念:指程序
还处
在代
码阶
段的
时
候
这
个名字的可
见
范
围
,后者是
时间
概念:指程序运行
过
程中
对
象的存在
时间
。
函数的形参以及函数内部声明的
对
象都是局部
对
象,它
们
的作用域就是函数内部,但是它
们
的生命周期却未必是函数的
执
行
过
程。
这
看起来有点摸不着
头脑
,原因在于
C++
的函数中允
许
存在以
关键
字
“static”
声明的静
态对
象。
也就是
说
,静
态对
象是
这样
一个
对
象:它的生命周期很
长
,可以跨越
该
函数的
每
次
调
用,哪怕
该
函数
每
24
小
时
才
调
用一次,它也是全天候存在的。但是要想
访问
她,却只有函数正在
执
行的
时
候才行。
签
于以上特性,我
专门
写了两个
测试
函数,
该
函数
试
途返回局部
对
象的引用或指
针
:
int& GetInt()
{
int t=3;
return t;//
警告
}
int* GetInt2()
{
int t = 3;
return &t;//
警告
}
以上两个警告
产
生的原因是函数返回了
临时对
象的引用或地址。但是如果将
t
的声明改成
“static int t=3;”
就不再
显
示警告。
静
态
局部
对
象似乎
为节约
系
统开销
做了准
备
。不
过
我
认为这
个特性不
应该
被
滥
用。只有确
实
有必要
让对
象生
命周期跨越多次
调
用
时
才
应该
把它声明
为
静
态
(比如
统计
函数被
调
用的次数)。否
则
将提高造成
BUG
的可能性,使
“
高效率
”
的程序成
为
空中楼
阁
。
标题:
:默
认实
参
没什
么
比
偷懒
更舒服的了,所以我喜
欢
允
许
默
认实
参的函数,我
还
喜
欢
写允
许
默
认实
参的函数。
在形参表中,如果允
许
某些形参具有默
认值
,
则
它
们
必
须
按从右到左的方向排列。以上
这
个
规
定
C++
与
BASIC
是一
样
的,但是
C++
与
BASIC
还
有一点区
别
,就是在函数
调
用
时
,
C++
必
须
从右
边开
始缺省
实
参,而
BASIC
却可以任意缺省而不
顾
次序(只要有逗号表示那里缺了个
东
西即可)。所以,同
样设计
函数,
C++
比
BASIC
要多考
虑
一个
问题
:
“
设计带
有默
认实
参的函数,其中部分工作就是排列形参,使最少使用默
认实
参的表参排在最前,最可能使用默
认实
参的形参排在最后。
”
形参的默
认值
竟究写在声明中
还
是
实
体中?我曾
经试过
,在某
些情况下写在声明中或
实
体中一
样
可行。但是,事
实
上写在
实
体中是
错误
的做法。只有当函数
实
体和函数
调
用在同一个源文件中,而且函数
实
体在
调
用前被
编译时
,将形参的默
认值
写在
实
体中才可通
过编译
。
实际
上
对
于
这种
情况,函数根本就不用声明。
将默
认值
写在
实
体中不
仅仅
是能否通
过编译
的
问题
,
还关
系到程序
设计
的理念。
“
一是函数的
实现
本来就与参数是否有缺省
值
无
关
,所以,没有必要
让
缺省
值
出
现
在函数的定
义
体中。二是参数的缺省
值
可能会改
动
,
显
然修改函数的声明比修改函数的定
义
要方便。
”
(《高
质
量
C++/C
编译
指南》第
63
页
)
读
到
这
里,本
书给
了我一个大大的惊
诧
:原来默
认实
参的默
认值还
可以是任何表达式。以前,我一直是
这样
写的:
“int GetInt(int i=3);”
虽
然没人跟我
这样说过
,但是我始
终
以
为
后面的默
认值
只能是常量。想不到
还
可以是需要求
值
的
变
量甚至是更
复杂
的表达式:
int GetInt(const int i = 3);
int GetInt2(const int j = GetInt());//
居然可以
这样
写
学
习
了,感
谢
《
C++ Primer
》!
标题:
:函数的声明与
实
体
注:本
书
中提到了
“
声明
”
与
“
定
义
”
两个
词
。我倒是
认为
将后者改
为
“
实
体
”
更好。
函数的
实
体就是
实实
在在的函数内容,它
规
定了
这
个函数怎
样执
行,
这
没有什
么
好
说
的。那
么
函数
为
什
么还
要有声明呢?
这样
做的目的之一是告
诉编译
器:
虽
然你
还
没有
见
到函数本身,不知道函数是怎
样执
行的,但是我先告
诉
你
这
个函数的名称、参数与返回
值
,你就先
编译
吧。至于
这
个函数究竟干什
么
,等到
连
接的
时
候再
说
。
设计
合理的程序,其代
码
存放在不同的文件中,函数的
实
体只能有一个,存放在某一个源文件中。其它源文件中如果要用到
这
个函数,就在
这
个文件中加入函数的声明。
这样
做的目的之二是函数的提供者与使用者往往不是同一个人,甚至不是同一个企
业
。出于
种种
目的,函数的提供者可能并不想(或不必)
让
使用者知道
这
个函数的具体内容,只要使用者能
调
用就行。
这种
情况下,函数的提供者只需要提供一个声明
给
使用者即可。
——C
语
言的
库
函数就是
这样
的。
然而
“
在需要用到函数的文件中加入函数的声明
”
也有好
办
法与笨
办
法。将声明
语
句重写一遍自然
不
难
,但是
这样
做有两个明
显
的缺点:一是
烦琐
易
错
、二是不易修改。所以,函数的声明
应该
放在
头
文件中,哪儿要,就在哪儿包含。
这
就好像我家没有
摆许
多盆
鲜
花而是
摆
了
许
多面
镜
子。我在哪儿都能看到
鲜
花,
浇
水却只要
浇
一盆。
这
个理
论
也适用于
C++
的
“
类
”
,
类
的声明写
进头
文件,而
实
体却写
进
程序文件。不同的是,
类
的声明不像函数的声明那
样
只有一句
话
,而是一个完整的
结
构
。
标题:
:
递归
引用:直接或
间
接
调
用自己的函数称
为递归
函数。
引用:
递归
函数必
须
定
义
一个
终
止条件,否
则
函数将永
远递归
下去,
这
意味着函数会一直
调
用自身直到程序耗尽。
初
识递归
的
时
候,的确有些不容易搞明白。
记
得当
时
的教科
书为
此画一个
图
,用一
组
箭
头
来表示要
计
算
A
必
须
先
计
算
B
、要
计
算
B
又要先
计
算
C
、
……
,用另一
组
箭
头
表示算好了
C
就可以算
B
、算好了
B
就可以算
A
。
……
实
例程序与一个
图结
合,如此
摆
事
实讲
道理,要
说
明
递归
自然稍容易些。
要写
递归
函数就得
领
悟
递归
的妙用,要写没有
错误
的
递归
函数
则
要
领
悟其数学原理。我倒是
觉
得
这样
的函数与
“
数学
归纳
法
”
有些相通之
处
。不同的是,数学
归纳
法
总
是先求
边
界条件,再去往无
穷
方向
归纳
。而
递归
是从无
穷
方向向
边
界
计
算的。函数如何
执
行,与我
们
如何写没有必然的
关
系,于是,我
们
在写程序的
时
候也可以先写
边
界条件。
这样
做可以在程序
开头
先把可能的
问题给
排除掉。
“
永
远递归
下去
”
的可能性自然被降低。比如求
阶
乘的函数:
//
程序一、
书
上的例子
int factorial(int val)
{
if (val > 1)
return factorial(val-1);
return 1;
}
//
程序二
int factorial2(int val)
{
if (val <= 1)
return 1;
return factorial2(val-1);
}
程序二的写法与程序一没有区
别
,但可以告
诉
自己
递归
必
须
有
终
止条件。防止一不小心就写了个
“
永
远
”
。
似乎
绝
大多数
递归
函数都可以用循
环
来解决。
这
两
种
方法迁就了不同的
对
象:循
环
用少量的
计
算机
资
源、大量的人力来解决
问题
,
递归则
用大量的
计
算机
资
源、少量的人力来解决
问题
。所以,在
计
算机速度和存
储
量都不大的年代,曾有人反
对递归
。
汉诺
塔
问题
据
说
是只有用
递归
才可以解决的
问题
,其
实
只有要求解
汉诺
塔的移
动过
程才必
须
用
递归
,如果只要求解移
动
次数,那
么
用循
环
也不成
问题
。
对本文本的评论有:
阶乘的函数写错了.
int factorial(int val)
{
if (val > 1)
return val* factorial(val-1);
return 1;
}
晕,我忘了相乘了,哈哈。
标题:
:
return
语
句
引用:
return
语
句用于
结
束当前正在
执
行的函数,并将控制
权
返回
给调
用此函数的函数。
引用:
return
语
句有两
种
形式:
reutrn; return expression;……
第二
种
形式提供了函数的
结
果。
笔
记
:以上第一句
话说
了
return
的两个作用之一:
结
束函数。
return
的作用之二是提供函数的返回
值
。
对
于
return
语
句的两
种
形式,情式一只能用于无返回
值
的函数,情式二可以用于有返回
值
的函数也可用于无返回
值
的函数。
如果函数有返回
值
,就必
须
用形式二来
结
束,
这
是
显
而易
见
的。
对
于没有返回
值
的函数,可以不写
return
语
句,
“
隐
式的
return
发
生在函数的最后一个
语
句完成
时
”
。也可以用形式一来
结
束,
这种
用法一般用在函数中
间
,判断某些条件之后就立即
结
束,后面的
语
句不再
执
行。如果用形式二来返回,那
么
express
必
须
是另一个没有返回
值
的函数。如:
void FuncA();
void FuncB()
{
return FuncA();
}
个人
认为这种
写法不是好
习惯
,因
为
看起来
FuncB
有了返回
值
,如果
逻辑
上有
这
需要,我
认为
写成以下格式更好:
void FuncB()
{
FuncA();
return;
}
在
BASIC
中,函数的返回
值
与
结
束是由两个不同的
语
句
实现
的。前者是一个
给
函数名
赋值
的
语
句,后者
则
是
“Exit Function”
语
句。
这种设计
除了不如
C++
精
练
以外,
还
容易出事。比如在函数
开头
先
给
函数名
赋
一个默
认值
,然后根据某些条件
给
它
赋
其它特定的
值
并
Exit
。如果写函数
时
不小心漏了某个
赋值语
句,函数将
产
生
BUG
。
C++
则
不会
产
生
这种类
型的
BUG
。
引用:千万不要返回局部
对
象的引用。
引用:千万不要返回局部
对
象的指
针
。
笔
记
:以上两句是黑体的
标题:
,
书
中
专门进
行了
讨论
。不
过这
个
错误虽
然
严
重,却不
难
理解。知道了就好了。
main()
是一个很特殊的函数,它的特殊性在
这
里
还
有体
现
。引用:
“
返回
类
型不是
void
的函数必
须
返回一个
值
,但此
规则
有一个例外的情况:允
许
主函数
main
没有返回
值
可
结
束。
……
编译
器会
隐
式地插入返
回
0
的
语
句。
”
标题:
:
传递
数
组
的函数与字符串函数
如果将数
组
作
为实
参来
调
用函数,函数接收到的形参其
实
是一个指
针
。数
组
名是可以
转换为
指
针
的,但是数
组
名和指
针毕
竟不等价。所以,
这样传递
的
结
果是
丢
失了数
组
原有的一些特性。最大的
损
失莫
过
于
sizeof
对
数
组
大小的
测试
。
试
看以下程序:
void FuncA(int *temp)
{
cout << sizeof(temp) << endl;
}
void FuncB(int temp[])
{
cout << sizeof(temp) << endl;
}
void FuncC(int temp[20])
{
cout << sizeof(temp) << endl;
}
int main()
{
int a[10];
cout << sizeof(a) << endl;
FuncA(a);
FuncB(a);
FuncC(a);
return 0;
}
三个函数的写法各有不同,但是
结
果却是一
样
的。其中
FuncC
的写法尤其容易
产
生
误
解。因
为编译
器不管你
传递
的是多大的数
组
(甚至不管是不是数
组
),但是函数的写法却在暗示程序
员这
个数
组
有
20
个成
员
。如果
实
参成
员
超
过
20
个,
结
果就是
没有起到完全的作用,如果
实
参成
员
不到
20
,那就指
针
越界了。
为
避免
这样
的
尴
尬,有
时
我
们
将指
针
与容量一起
传
入函数:
“void FuncD(int temp[], _size_t Size);”
,或者
传递
两个指
针
:
“void FuncE(int* Begin, int* End);”
。
这样
做当然好,不
过
C++
还
有另一
种办
法可以不用
这么
麻
烦
,那就是引用
传递
:
“void FuncF(int (&temp)[10]);”
。
这样
的函数只允
许
将
int[10]
实
参
传
入,大小不符的数
组
或非数
组
的指
针
都无法
传
入。
这样
就保
证
了
10
这
个
值
的正确性,
连
sizeof
都省了。
C
语
言的字符串
处
理函数大概是
仅
有的可以不受此
约
束的函数了。字符串就是字符数
组
,但是在
传递
字符数
组时
,可以只
传
指
针
而不管大小。因
为
C
语
言中的字符串都是以
NULL
尾的。前
阵
子有人在
论坛
提
问
,
问
及字符串和字符指
针
的
关
系。回答是:
C
语
言的字符串是用字符数
组
存放的,而
处
理
则
是借助于字符指
针
。但是,要能
进
行
这样
的操作,有两个条件必
须满
足:一是所有字符
连续
放置在以指
针开头
的内存中、不跳
跃
,二是有一个
规
定的
结
束符。
int[]
数
组
之所以不能
这样
做,是因
为
第二个条件无法
满
足
。
标题:
:函数的引用返回
值
引用是
给变
量取一个
别
名,所以引用
传递
会直接
进
行
变
量本身的
传递
。它的最大好
处
是可以把
别处对变
量的改
变
保留下来,第二好
处
是它提高了性能:如果函数的返回
值
是一个引用,那
么
,如上文所
说
,它会
节约
一
组
构造、
赋值
和析构
过
程。但是,函数返回引用往往会
带
来一些意想不到的
错误
:比如返回
临时变
量的引用。
//
一个
错误
的函数
int &Max(int i, int j)
{
return i>j ? i : j;
}
以上函数的
错误
在于,
i
和
j
在函数
结
束后会被
释
放。
对
它
们
的引和也将失效。如果用
这
个返回
值给别
的
变
量
赋值
,将会
获
得一个垃圾。
VC++.Net
会
对
以上
return
语
句
显
示警告。
那
么
,如果返回一个全局
变
的引用呢?
这
当然是可以的,但是,一来程序
设计
中不建
议
使用
过
多的全局
变
量,二来全局
变
量即使不返回也可以
访问
。
这样
做的唯一用途就是把函数做右
值
来
给
其它
变
量
赋值
。
int m;//
全局
变
量
int &MaxByGlobal(int i, int j)
{
return m = i>j ? i : j;
}
int a, b, c;
c = MaxByGlobal(a, b);//
用法一、用返回
值赋值
MaxByGlobal(a, b); c = m;//
用法二、不用返回
值赋值
当然,以上
这
个
MaxByGlobal
函数也不是一无是
处
,能用返回
值
来
进
行
赋值
会
给
程序
带
来更好的可
读
性。
只是
这样
的函数
设计
本身不被建
议
。
那
么
,函数返回引用用得最多的就是返回形参了。因
为
形参可以用引用
传递
,引用的形参不是函数内部的局部
变
量,
这样
做是可取的:
int &MaxByRef(int &i, int &j)
{
return i>j ? i : j;
}
上面
这
个函数和上文中的
“int Max(int i, int j)”
函数如此相似,但是它省去了三次构造、
赋值
和析构。
另外一
种
用法就是在
类
的成
员
函数中返回
类对
象自身了,典型的是
“operator +=”
函数之
类
。
MyClass &MyClass::operator +=(const MyClass &other)
{
//
某些
语
句
return *this;
}
以上函数返回的是自身的引用。因
为类
的成
员
函数也可以写成全局函数
“MyClass &operator +=(MyClass &Left, const MyClass &right)”
,而且在
类
成
员
函数的
调
用中
实际
存在着
this
指
针
的
传递
。所以,以上
这
个函数依然可以看作返回了形参的引用。
对
于返回引用的函数,
还
有一个好玩的
现
像。即返回
值还
可能可以被
赋值
。如
“(a += b) = c;”
这样
的形式。
这种
写法明
显
不
伦
不
类
,但是如果函数返回了非
const
的引用,
这
个表达式的确是合理的。所以,上面的
“operator +=”
函数
还
要修改一下,将返回
值
由
“MyClass&”
改
为
“const MyClass&”
。
返回引用并不是
处处
可用的,正如《引用
传递
的
应
用范
围
》中提到的一
样
:不能用引用来
传递临时值
。有
时
候我
们
的确要
产
生一个
临时对
象并返回它,那就不能返回引用。典型的有
“operator +”
函数:
const MyClass MyClass::operator +(const MyClass &other) const
{
MyClass Temp;
//
某些
语
句
return Temp;//
这
里只能返回
对
象,因
为
Temp
必
须
是局部
变
量
}
标题:
:函数的非引用返回
值
函数最多可以返回一个
值
,也可以不返回任何
值
(也有
“
返回
void”
的
说
法)。之所以最多只能返回一个
值
,因
为
只有
这样
才能在表达式中使用。比如
“y=Sin(x);”
,如果
Sin
函数返回多个
值
,
这
个表达式就失去了意
义
。之于
为
什
么
可以不返回任何
值
,
经历过
BASIC
的人
应该
更能理解。因
为
BASIC
中把有返回
值
的程序段叫函数,没有返回
值
的程序段
则
叫做
“
子程序
”
。很
显
然,
“
子程序
”
就是完成一个特定的功能后
结
束的程序段。
函数的返回
值
没有
类
型限制,可以是内置
类
型
变
量,也可以是
类对
象。无
论
是内置
类
型
还
是
类对
象,都有着
一
样
的
规
律。但是,
这
些
规
律在
C++
到来之前很少有人去理会,
毕
竟内置
变
量
类
型太
复
通,以至于程序
员
根本不去考
虑
那
么
多
“
为
什
么
”
。
在
C
时
代,所有的返回
值
都是局部
变
量。如下列程序:
//
程序一:
int Max(int i, int j)
{
return i>j ? i : j;
}
//
程序二:
char *StrCpy(char *Target, const char *Source)
{
char *Temp=Target;
while(*Source)
{
*Temp++ = *Source++;
}
return Target;
}
程序二
给
人一个
错觉
:
认为该
函数返回的不是函数内部的局部
变
量。
错误
原因在于没有理解指
针
的本
质
。其
实
程序二和程序一一
样
,返回
值
是形参之一。而形参就是作用域
为
函数内部的局部
变
量。
理解了
“
返回
值
是局部
变
量
”
还
不
够
。因
为还
有一个很重要的概念没弄清。比如:
int a, b, c;
char d[10], e[10], *f;
//
其它
语
句
c = Max(a, b);//
语
句一
f = StrCpy(d, e);//
语
句二
以上注
释
的
两行
语
句都有同一个
问题
:如果返回的
变
量作用域
仅
限于函数内部,那
么
函数
结
束以后
该变
量就已
经
不存在了,那
么给
c
和
f
赋值
的是什
么
?
C
和
C++
有一个机制保
证
以上
赋值
正常
进
行:在函数
结
束前,先将要返回的局部
变
量
临时
拷
贝
一份到
栈
内存(
这
个内存程序
员
无
须
知道,也无法知道)。然后将局部
变
量
销毁
,函数正常
结
束。接下来用
栈
中的
临时变
量
对
目
标变
量
进
行
赋值
,
赋值结
束后再把
临时变
量
销毁
。
以上
这
个
过
程凭空多出一次
变
量构造、
复
制与
销毁过
程,好在
对
于内置
类
型
变
量来
说
,
这样
的
过
程所需的性能
赋
出并不太多。但是
C++
到来以后,函数的返回
值类
型可以是
类类
型。而
类对
象的构造、
复
制与
销毁
可能很
复杂
、很占用系
统资
源。于是
“
引用
传递
”
再一次
发挥
了它的威力
。
标题:
:引用
传递
的
应
用范
围
经过
三篇文章的
细
述,函数的参数
传递应该
比
较
明朗了,
经过
一番
对
比,似乎引用
传递
是最
优
秀的一
种传递
方式。第一、它用法很
简单
,
类
似于
值传递
,第二、它功能很
强
大,
类
似于指
针传递
,第三、它很安全,可以避免指
针传递带
来的危
险
,第四、它效率高,函数中不必要
进
行
对
象的
创
建、
赋值
与
释
放。第五、如果不希望
实
参被改
变
,可以使用
const
修
饰
形参
……
但是,天下没有
这么
便宜的午餐!引用
传递
不是倒
处
能用的。
举
个例子:
void Swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
以上函数可以
进
行两个
int
变
量的交
换
。但是,很多情况下
该
函数不能
调
用:
int ia = ib = 1;
short sa = sb = 2;
const int cia = cib = 3;
Swap(ia, ib);//
正确
Swap(sa, sb);//
错误
,
short
不是
int
,
虽
然可以
隐
式
转换为
int
,但是
这
个
变
量不存在
Swap(cia, cib);//
错误
,
这
两个参数是
const
的
Swap(4, 5);//
常量不是
变
量,
类
似于将
short
变
量
传递给
函数
Swap(ia+ib, ia-ib);//
错误
,表达式求
值
后
产
生的
临时值
不是
变
量
其中将
const
参数
传递进
函数的做法,
虽
然看起来有些荒
诞
,
实际
上某些
时
候会不
经
意
间
做的。某个
变
量在定
义
的
时
候并不是
const
的,但是在
调
用某个函数的
时
候将它作
为
const
形参
传
入,而
该
函数内部再
调
用
Swap()
函数
时
,
这
个
变
量已
经
成了局部的
const
变
量。
以上
这
个特性反
过
来
应
用是很有用的。在多人
协
作写程序的
时
候,或者写一个大型程序的
时
候。你不知道某函数是否用
const
来保
护
参数,但是你想保
护
参数。那
么
,你就在自己写的原
调
函数中将
该
参数保
护
起来。
这样
,当你
调
用某个没有
显
式指定
const
引用参数的函数
时
,
编译
器就会
报错
。
void funca(const int& a)
{
funcb(a);//
发
生
错误
}
void funcb(int& b)
{
...;
}
int t;
funca(t);
以上程序会在注
释
的那行停止
编译
。因
为
在它
调
用了函数
b
,而
b
没有声明参数
为
const
。
虽
然函数
b
中未必改
动
参数
。
标题:
:形参与
实
参的
关
系之引用
传递
C++
有了
“
引用
传递
”
后,
“
形参的改
变
不影响
实
参
”
被判无效。因
为传递给
函数的并不是一个
值
,而是
变
量自身。在函数中定
义
的形参
虽
然
还
是局部
变
量,但却是一个引用。
虽
然
这
个引用的作用域
仅
限于函数内部,但是由于它与
实
参就是同一回事,所以
对
它的操作完全等同于
对实
参的操作。比如你叫
“
黑旋
风
”
去
买鱼
,或者叫
“
铁
牛
”
去
买鱼
,去的都是同一个人。
C++
为
什
么
要有
“
引用
传递
”
这
回事?一
种说
法是只有引用才能达到操作符重
载
的目的,
这
个以后再
谈
。但是,撇
开这
个不
谈
,形参是不是引用,直接影响了程序
执
行的效率。前面提到
过
,函数
调
用
时
要用
实
参的
值
去初始化形参,初始化的
过
程包含了定
义
一个
变
量、然后
给
它
赋
一个
值
两个
过
程,如果
这
个
变
量并不是内部
变
量,而是一个
类对
象,那
么
,定
义
一个
类对
象可能很
复杂
,而初始化
这
个
对
象一
样
会很
复杂
。而引用只是
给对
象取一个
别
名,不
涉
及定
义
与初始化,离
开
作用域
时
也不用
释
放。
相比之下,用指
针传递
可以避免
类对
象的定
义
、初始化与
释
放。只需要付出指
针变
量的定
义
、初始化与
释
放的代价。但是,指
针
的
杀伤
力太大。即使是熟
练
的程序
员
,也不能保
证绝
不出
现
“
野指
针
”
,野
针
的代价几乎无一例外是程序崩
溃
。
引用也不是吃素的,如果
说
指
针传递
是
“
帮你配了一把我家的
钥
匙
”
,那
么
引用
传递
就是直接把我家的
财产
都交
给
了你。有
时
,我
们
使用引用
传递仅仅
是
为
了效率,而不希望
实
参被修改,那就要
记
得把形参
标记为
const
,如
“UINT GetLength(const CString&)”
。
顺
便
说
一句,指
针传递
也可以
这样
做。把形参定
义为
指向
const
对
象的指
针
(而不是
const
指
针
),可以降低
杀伤
力,保
护实
参所
对应
的内存。如果是普通的
值传递
,那
么
有没有
const
对
函数外
部并不影响。但是,我个人
认为
,有
时
候加上
const
也是一件好事。如果程序的
逻辑
并不需要改
变
参数,而
实际
上
误
写了代
码
,加上
const
可以
让编译
器帮我
们
找出
BUG
,如:
int Max(const int a, const int b)
{
return a>b?a:b;
}
VB
没有指
针
的概念,却有
“
值传递
”
和
“
地址
传递
”
两个概念。比如
“Function Func(ByRef i As Integer) As Integer”
,
变
量
i
接受了
实
参后,它的改
变
能影响
实
参。它的
实质
就
类
似于
C++
中的引用
传递
。
标题:
:形参与
实
参的相互
关
系
“
形参的改
变
不影响
实
参
”
这
句
话说
起来
轻
巧,但是要完全理解,似乎
还
有几个玄机。
在我
发
表《函数的定
义
》一文后,有朋友
发
表意
见
,提到了
“
函数
调
用
过
程中的入
栈
与出
栈
”
,在此首先作个
说
明:我
读
的是《
C++ Primer
》,而不是《
编译
原理》,入
栈
与出
栈
不
归
我
讨论
。在
现
在
讨论
的尺度内,我
们
可以
这么认为
:形参是函数内部的一个局部
变
量,
该
局部
变
量在函数
开
始
执
行
时
被初始化,而初始化它的
值则
来自
实
参的
值
。也就是
说
,它的定
义
与初始化
类
似于
“int i=3;”
。只是被分成两行写了,形参的定
义
写在函数的定
义
中,如:
“int ttt(int b)”
,初始化写在了
调
用中
“cout << ttt(a) << endl;”
。
——
参看上一篇文章《形参与
实
参概念》。
那
么
,在函数中无
论
怎
样
改
动
b
的
值
,被改的始
终
是形参
这
个局部
变
量,函数
结
束
时
,离
开这
个局部
变
量的作用域,
变
量被
释
放。
但是,
C
语
言的
“
指
针传递
”
总
是
给
人
“
形参能改
变实
参
”
的感
觉
,其
实这
是一个
误
解。
对
于指
针传递
来
说
,函数的形参是一个指
针
,
传给
它的
实
参也
应该
是指
针
(或者能
转为
指
针
的
值
,比如数
组
名、能
转换为
指
针
的
类
等)。在函数中,如果改
变
了
该
指
针
(
对
指
针
的改
变
就等同于
让这
个指
针
指向
别处
),不会影响主
调
函数中的
实
参。但是,由于指
针对应
着一个内存地址,通
过
它可以改
变
内存的内容。所以,无
论
在函数内部的形参
还
是外部的
实
参,它
们
都可以影响同一内存的
值
。所以,指
针传递
可以把函数内部的影响
带
到函数外,但是,
带
到函数外的
绝
不是形参,而是形参所指的内存。
这
就好比我把我家的
钥
匙
给
你配了一把,我手里的
钥
匙是
实
参,你手里的
钥
匙是形参。你无
论
是把
钥
匙折断
还
是磨短,都与我的
钥
匙无
关
,但是你用它
开
了我家的
门
却可以把我家洗劫一空。你影响的不是我的
钥
匙,而是我的
财产
。
上文
说
到,
C++
有了
“
引用
传递
”
后,
“
形参的改
变
不影响
实
参
”
被判无效。
这
就得提到
“
引用
传递
”
的概念了,下文再
续
。
对
本文本的
评论
有:
简单
地
说
,
每
次
调
用函数的
时
候
,
形参把
实
参克隆了一次
,
你再怎
么
折
腾
形参
,
也与
实
参无
关
.
TNND
就是一个入
栈
与出
栈过
程嘛
,
你可以去学学
汇编
.
举
例
:
mov cs1,100 //cs1=100;
push cs1 //
把
cs1
入
栈
;
pop cs2 //
把
栈
中的内容出
栈给
cs2;
这
与另一句
话
等价
:
mov cs1,100
mov cs2,cs1
为
什
么
会使用上面的那
种
用法呢
?
因
为
push
和
pop
占用更少的
CPU
周期
.
所以
,
一般
调
用函数都用入
/
出
栈
来
备
拷
贝
参数
.
标题:
:形参与
实
参概念
说
到形参与
实
参,在
C++
出来之前其
实
很
简单
,就一句
话
:形参的改
变
不影响
实
参。
这
个状
态
直到
C++
有了
“
引用
传递
”
才有改
变
。
要弄清
这
个,首先得弄清形参与
实
参是什
么东
西。因
为
函数是一段
“
可以重用而不必重写
”
的代
码
,
每
次重用当然未必完全相同(不可否
认
有些函数
每
次重用都完全相同),那
么
不同在哪里呢?又怎
样产
生不同呢?一
种
方法是依靠随机,随机是个好
东
西,不要
说
客
户
了,
连
程序
员
都无法控制
每
次
调
用的
结
果。第二
种
方法是凭客
观
条件(比如运行
时间
、机器配置)。但是
这
些函数
应
用很窄,
类
似于
“y=Sin(x)”
这样
的函
数就
绝
不能
这样
做。
那
么
,从
“y=sin(x)”
的形式看来,能决定函数怎
样
运行的唯一因素就是
x
的
值
了。函数的某次运行是受某一个
x
值
的影响并控制的,而下一次运行,
则
会受另一个
x
值
的影响。那
么
,
调
用函数者就有必要告
诉
函数:我要用哪个
值
来控制你,而函数自己
则
有必要保存
这
个
值
,直到函数
结
束。
为
此,在函数内部建立一个
临时
的、局部的
变
量,
该变
量的作用域就是函数内部,
该变
量的作用
时间
就是从函数
开
始
执
行到
结
束
执
行。如果同一函数在同一
时间
有几个副本在
执
行(
这种
情况在多
线
程程序中会出
现
),那
么
它
们
是互不相干的,它
们
内
部的
变
量也是互不相干的。
这
个
变
量就叫做
“
形参
”
,全称形式参数。
“
形式
”
是跟
“
实际
”
相
对
的,另一个参数就是
实际
参数,叫
“
实
参
”
,在
调
用函数
时
,
这
个
值
将决定函数内部的形参的
值
。
实
参在函数中是否可
见
?
这
要取决于两个因素:一是
实
参的作用域,二是有没有被形参覆盖。先
说
第一个因素,如果只
谈
C
语
言,那
么
所
谓
的作用域就是全局与局部两
种
,但是
C++
中
还
有
“
类
作用域
”
这
一概念,由此第一个因素
变
得
复杂
了。第二个因素本身并不
复杂
,但是如果没有引起程序
员
的注意,那
么
造成的
问题
是很
难发现
的。
试
看下以下程序:
int a;//
全局
变
量
int ttt(int a)//
该
函数的形参也叫
a
{
cout << ++a << endl;
return a;
}
int main()
{
a = 3;
cout << a << endl;
cout<< ttt(a) << endl;
cout << a << endl;
return 0;
}
该
程序中有一个全局的
a
变
量,但是在
ttt()
函数中却被另一个
a
覆盖了,所以,
++a
没有影响到全局的
a
,如果把函数定
义
改
为
“int ttt(int b)”
则
有不同的
结
果
。
以上把
“
形参
”
和
“
实
参
”
提了
这么
多,主要目的
还
是
讲
清
“
形参的改
变
不影响
实
参
”
这
句
话
。字数不少了,留到下篇文章再
续
吧。(我
觉
得我写得不像
读书
笔
记
,倒像是教材了。
呵呵
)
标题:
:函数的定
义
不
记
得在哪本
书
上看到
过
,函数的定
义为
“
有名称的一段代
码
”
。
这
大概地
说
明了函数的
实质
:首先、它是一段代
码
,其次、
这
段代
码
可以被重
复
使用而不必重
复编
写,第三、它是有名字的,在需要重用的
时
候凭名字来
调
用。
这
个
说
法到了
C++
中
变
得
复杂
了。原因之一是
C++
支持函数重
载
,也就是
说
出
现
了同名函数。
虽
然
编译
器在
编译时产
生不同的函数名,但那必竟是
编译
器的事,
对
于程序
员
来
说
就是同一个函数名。原因之二是
C++
支持运算符重
载
,可以用一个
类
似于
“+”
号的运算符来
调
用函数。运算符重
载
明
摆
着是
为
了配合
类对
象的运算,因
为
如果没有
类
,
仅针对
内置
类
型,运算符是没必要重
载
的。
——
我
试验
了一下,自定
义
了一个
“int operator +(int i, int j)”
函数,
结
果没有通
过编译
。
于是,到了
C++
中,函数的概念被修改
为
“
函数由函数名以及一
组
操作数
类
型唯一地表示
”
,依我看,
这样说还
不
够
。
严
格
说
来,
应该说
“
函数由作用域、函数名以及一
组
操作数
类
型唯一地表示
”
,理由很
简单
,因
为
在不同的作用域中可以出
现
名称相同、参数
类
型也相同的函数,除非把
“
作用域
::
函数名
”
合起来看作一个函数名。
函数
对
函数体没有任何
强
制性要求,哪怕函数体
为
空也可以。不
过
,无
论
是空、一句
语
名,
还
是多句
语
句,花括号一定不可少。在
这
里,包括在花括号内的若干行
语
句不能再
视为
一个
复
合
语
句了
——
因
为
能放
复
合
语
句的地方也能放
简单语
句,而
简单语
句可以不使用花括号。
不管你如何看待
这组
花括号,有一点是肯定的:花括号内部是一个作用域。那
么
,内部定
义
的
变
量就只有在内部使用了。
这
就是局部
变
量,在任何函数(包括
main()
)内部定
义
的
变
量都是局部
变
量
——
初学者可能以
为
在
main()
内部定
义
的
变
量是全局
变
量。
有一
种
内部
变
量的定
义
与以往的定
义
方式不一
样
,那就是函数的参数。不同之
处
在于:一是它
们
用逗号分隔,二是不允
许
用
“int i,j”
这样
的方式定
义
一
组变
量。我想,也
许
正是因
为
所有定
义
用逗号分隔,才造成不允
许
后者的吧,
毕
竟
这样
会
带
来歧
义
——j
没有指定
类
型。如果用分号来分隔,那
么
后者的
方式也
许
就可以了。
这
是
C++
标
准的事,我没有能力来
为标
准出
谋
划策,只能妄加猜
测
了。
函数的返回
值
也是一个
类
型,与
变
量的
类
型一
样
,它可以是内置
类
型,也可以是
类类
型,
还
可以是引用和指
针
。
引用:在
C++
标
准化之前,如果缺少
显
式返回
类
型,函数的返回
值
将被假定
为
int
型。
笔
记
:据我
测试
,在
VC++.NET
中,
这样
做是可以的。照
这么说
,
VC++.NET
仍然没有按照
C++
标
准做?或者
说
VC++.NET
迁就了老程序
员
?
对本文本的评论有:
函数的参数当然不能使用类似int i,j的方式,因为调用函数的时候,涉及到的不仅仅是定义参数,还有把要处理的变量入栈,调用的函数运行前的第一件事,是把被入栈的变量出栈.
这与int i,j定义变量做的事完全不同,所以,不按定义变量的方式写,也很正常.
如果偷猫兄一定要写得一样,那就自己做一个编译器吧.
标题:
:函数概念
进
入第七章学
习
。
“
函数
”
这
个概念在
C/C++
里
头
是很
烦
人的。原因在于,好多
C
语
言入
门书
的第一章第一
节
都
说
“C
语
言是由函数
组
成的
”
,初学者学到
这
里,就好像是
刚
推
开
C
的大
门
就被一个麻袋套在
头
上,什
么
也看不
见
了。那些
书
本
还举
了一个例子,然后
对
照着例子
说
“
这
个程序是由
main()
、
scanf()
、
printf()
函数
组
成的
……”
。我
晕
啊,初学者第一天上
C
的
课
,哪里会管什
么
函数不函数的。
这
点
BASIC
做得不
错
,倒不是
说
BASIC
比
C++
好,而是
BASIC
容易入
门
。在
开头
几
节课
不必理会
这么复杂
的
东
西,学了
“Let
语
句
”
、
“Print
语
句
”
就可以
涉
足
简单
的算法了。然后提到的
“
函数
”
是包括数学函数在内的
“
内部函数
”
。我
们
在数学里学
过
“
函数
”
概念,知道
“y=Sin(x)”
是一个函数,
现
在在
BASIC
里学到一
样
的函数,自然容易入
门
。等
这
一切都熟悉了,再去学
习
自己写的函数
——
自定
义
函数,会更加理解程序中的
“
函数
”
概念。
VB
与早期的
BASIC
相比,使用了
“
事件
驱动
”
原理。画完界面就得面
对
函数了
,但是
VB
用
“
事件
”
这
个
说
法来回避了。初学者可以不知道
“Private Sub Command1_Click()”
究竟代表什
么
,只要知道那是
“
按
钮
控件被
单击
后
执
行的代
码
”
就
够
了。等到后来,学
习
了
“
自定
义
函数
”
后,必然会恍然大悟。
回到
C++
中,学
习
之初用到的函数的确是
现
成的
库
函数,但是正因
为过
早地提到了函数概念,
导
致了初学者无所适从。有没有
别
的
办
法呢?当然有了,至少《
C++ Primer
》
这
本
书
一直到第七章才
开
始提
“
函数
”
二字。
另外:
VB
中有
“
函数
”
和
“
子程序
”
两个不同的概念,如今
“
子程序
”
又叫
“
过
程
”
,除了使用不同的
关键
字以外,它
们
的惟一区
别
是有没有返回
值
。
C
将它
们
合并了,都叫函数。其
实
,
VB
里的函数也可以
丢
弃返回
值
,只是
VB
里没有与
“void”
对应
的
词
,无法定
义
不返
值
的函数,才不得已出此下策
。
标题:
:
try
、
catch
和
assert
程序
员
是要慢慢成
长
的,比如
错误处
理
这种
事情,就不是一
开
始就面
对
的。当我
们编
的程序
还
很小,小到
“cin>>i; cout<
这样
的程度,
错误处
理不是我
们
要学
习
的目
标
。但是,一旦
开
始
编
写
实
用的程序,那
么
,无
论
考
虑
多
么
周到,无
论
代
码
多
么
精良。意外
总
是
难
免的。
这
些意外可能来自程序
员
的
设计
不到位、可能来自用
户
的
错误
操作、
还
可能来自机器与网
络
的不确定因素。
没有什
么
比追踪
错误
更
难过
的事了,
记
得有一回我在追踪一个
VB
程序的
错误
。
经过长时间测试
,我
发现
程序在运行中突然
发
生很大的跳
跃
:函数
A
调
用
B
,
B
调
用
C
,在
C
的
执
行
过
程中,居然会突然跳到
A
中。后来追
查发现
,原来
A
中有一行
“On Error Goto”
语
句。
这
一个
语
句,影响了我
调试
C
函数。从那以后,我明白了,除非程序要
发
布了,否
则别
启
动错误处
理。
C++
与
VB
不一
样
,
VB
用一句
“On Error Goto”
启
动
了
错误处
理后,在
该
函数
结
束之前一直有效(除非
显
式地
关闭
它)。如果
发
生了异常,
处
理代
码
要根据异常的
值
来分析异常的
类
型。而
C++
可以
选择
可能出
现
异常的内容放
进
try
后的
块
中。一个函数内部可以有多个
try
块
,而
每
个
try
块
又可以附
带
多个
catch
来
处
理。
应该说
,
C++
中的异常
处
理更灵活,当然也更容易出
错
。我前
阵
子
发
生的
错误
就是在
ADO
处
理后只有
“catch(_com_error *e)”
,但是
实际
上出
现
的异常却不是
“_com_error”
类
的,
结
果仍然抓不往异常。
异常
处
理和
assert
之
间
的
关
系有些
让
人
难
以捉摸。一方面它
们
各有各的作用,另一方面它
们
有
时
会互相影响。我就曾
经
在
这
上面吃
过亏
:我的程序是在服
务
器上运行的,从来没人会
盯
着服
务
器看,所以我的程序不允
许弹
出
对话
框。我写了比
较
完善的异常
处
理,无
论
出
现
什
么错误
,都
记录进
LOG
文件,然后
继续
运行。但是我却
是用
DEBUG
模式
编译
的,
结
果异常到来
时
,
try
没起作用,倒是
assert
起作用了,
弹
了个
对话
框在那儿。
这
件事
给
我的启
发
是:
别
以
为
自己是程序的客
户
就可以用
DEBUG
模式
编译
。
对本文本的评论有:
错误捕捉是很烦人,我的感觉是能在try代码段外解决的错误,就尽量在外头自己解决,尽量少依靠try来处理捕获错误.
在网络编程中,有些错误是无法预知的,比如网络连接断了,数据库当了...好象在这些情况下,用try比较好.
我有一次写的一个服务程序,用户用了一段时间后,经常会异常中止,查来查去查不出原因,后来才发现是ORACLE的日志满了,这个错误显然我在写程序的时候没有想过,丢脸啊...
标题:
:
break
、
continue
和
goto
break
和
continue
的使用范
围
比
较
一致,两都可以用于循
环
,其中
break
还
可以用于
switch
。功能上也有一定的相似性,
break
就相当于退学,
continue
则
相当于跳
级
。
对
于
break
,程序究竟跳到哪儿比
较
好理解。
但是
continue
究竟跳到哪儿去了,初学者可能有些疑惑,不妨就当它跳到了循
环
体最后一句
语
句的后面。
如果它
们处
在由多重循
环
和
switch
组
成的圈圈里,那
么
它
们
就
对
包括它
们
的最里
层
起作用。于是,
设
想一下子跳出多重循
环
的人可能忘不了
goto
。
引用:从上世
纪
60
年代后期
开
始,不主
张
使用
goto
语
句。
……
所有使用
goto
的程序都可以改写成不用
goto
。
笔
记
:
goto
是一个很有争
议
的
语
句,
语
多
书
本建
议
少用或不用它,我个人的
习惯
是
坚
决不用。不
过
,至于
“
上世
纪
60
年代
”
这
个
说
法,我倒是一直不知道。因
为
我自己学
习
BASIC
已
经
是
1994
年,那
时
候学的是
带
行号的
GW-BASIC
,
goto
是必
须
用到的
语
句。莫非当
时
我
们
学校
开设
的
课
程居然是落后二十年的内容?
林
锐
博士
对
goto
另有看法,他
说
:
“
错误
是
程序
员
自己造成的,不是
goto
的
过错
。
goto
至少有一
处
可
显
神通,它能从多重循
环
中咻地一下子跳到外面,
……
就像房子着火了,来不及从楼梯一
级
一
级
往下走,可从窗口跳出火坑。
……”
(《高
质
量
C++/C
编
程指南》第
32
页
)
我写的程序目前
还
没有超越三
级
循
环
。从最里
层
往外跳,如果跳一
层
,就
break
,如果跳两
层
或三
层
,一是
这种
可能性很小,二是如果真的碰到了,我就用其它条件来控制外
层
循
环
是否
继续
break
,自从
1997
年
进
入
结
构化的程序
设计
以来,我的确完全抛弃了
goto
。
——VB
中的
“On Error Goto”
除外,出
现错误
,自然不管在哪一
层
,都
给
我跳
进错误处
理中。
goto
的目
标
是一个
标
号,
这
个
标
号的起名倒有点意思,因
为标
号只用于
goto
,所以它的名字可以与任何
变
量名以及其它
标识
符一
样
而不
产
生重名。以前的程序是
带
行号的,所以就
“goto
行号
”
,
现
在程序不
带
行号了,但是允
许
在任何地方加
标
号。
编译
器在碰到它
们
的
时
候,大概就是凭其后
头
的冒号来判断
这
个名字不需要
检验
合法性。那
么
,
C++
中已有的
“public:”
算不算
标
号呢?
为
此,我做了个
实验
:
实验
内容一是我在
类
的声明里加入了一行
“pub:”
,二是我在程序段中加入了一行
“public:”
。
结
果
发现
两都都不能通
过编译
。也就是
说
,
实验
一
说
明在
类
定
义这样
的地方不允
许
使用
标
号(也用不着,因
为
它不在任何函数内部,
goto
是运行
时
的事,与
编译
无
关
,而且
goto
不允
许
跨函数跳越。),
实验
二
说
明在程序段中的
标
号不允
许
使用保留字
。
对本文本的评论有:
不主张使用GOTO语句是为了让程序看起来顺眼而己.看:模块化的代码.其实的确没什么大不了的.记住,当你的程序被编译成机器代码以后,里面的跳转全是JMP,相当于GOTO.
自从和草莓对骂以来,你就学会了狡辩。
我有跟你讨论机器码吗?
程序设计的风格是为了程序维护,
不是为了编译。
标题:
:
while
、
for
语
句
while
中有一个怪事:
类
似于
“while (int i = GetInt())”
这样
的
语
句,在条件中定
义
一个
变
量,在
for
中非常常
见
,也很好理解。但是用在
while
中却有所不同,如果用在
while
中,那
么每
次循
环
都会
经历
一次
创
建和撤
销
的
过
程。
——
天,
还
是不要
这样
写吧。幸
亏
我
总
是在
while
前面定
义
并初始化
变
量的。
do-while
与
while
有着不一般的
关
系,所以几乎所有的
书
本都是把它
们
放一起
讲
的。当年学
BASIC
时
,花了不少的功夫去学
习
“
当型循
环
”
和
“
直到型循
环
”
。的确,当型和直到型都有存在的必要,因
为
程序的确有
这
两
种逻辑
需要。于是
C
、
BASIC
以及
PASCAL
等程序
语
言都提供了
这
两
种
循
环
。不
过
提供
归
提供,怎
么
用却是程序
员
自己的事。就我个人而言,我
还
是喜
欢
用当型循
环
。因
为
当型循
环
可以模
拟
出直到型循
环
的效果来。比如以下四段代
码
,它
们
是完全一致的:
//
代
码
1
do
{
循
环
体
;
BoolVal =
表达式
;
}while (BoolVal);
//
代
码
2
BoolVal = 1;//
先
赋
True
值
while(BoolVal)
{
循
环
体
;
BoolVal =
表达式
;
}
//
代
码
3
do
{
循
环
体
;
}while (
表达式
)
//
代
码
4
while(1)
{
循
环
体
;
if (!
表达式
) break;
}
for
语
句的
执
行
顺
序和
执
行
逻辑
是最
难讲
清的了。如果知道了,就是
这么
回事。如果不知道,不
费
上半天口舌是
说
不清的。原因在于
for
包括四个互相
关联
的
语
句,其中三个在
“for”
后面的括号里,另一个作
为
循
环
体存在。
这
也
难
怪
BASIC
要将
for
语
句定
义为
“For i=M To N Step t”
的格式。
for
括号里的三个
语
句是可以省略的,最牛
B
的省略莫
过
于
“for (;;)”
了。会
这样
写的人,要
么
是
彻彻
底底地明白了
for
的
逻辑
的人,要
么
是一点不懂的人。我
觉
得,如果要我
这样
写,我不如写
“while(1)”
了。
标题:
:
if
、
switch
语
句
本
书
不愧
为经
典
书
,在
if
这
地方能避免
说
教,
讲
得
绘
声
绘
色,真叫人佩服。
大体上
if
要注意的就只有
else
的配
对问题
了。如果在
else
前方有多个没有配
对
的
if
,那就找最近的一个配
对
。如果要改
变这种
默
认
的
“
拉郎配
”
,就加上花括号。
还
是引用林
锐
博士的一句
话
吧:
“if
、
for
、
while
、
do
等
……
不
论执
行
语
句有多少都要加
{}
。
这样
可以防止
书
写失
误
。
”
(《高
质
量
C++/C
编
程指南》第
16
页
)
if
语
句曾有一个令我疑惑
了好久的
东
西:
“else if”
究竟算什
么
?因
为
BASIC
里有
“ElseIf”
这
个
关键词
,而
C++
中所
谓
的
“else if”
是两个
关
健
词组
成的。中
间
插了个空格。我
们
都知道,
C++
的
语
句与
语
句之
间
插入若干个(包括
0
个)空格、
TAB
、回
车
都是一
样
的,那
么
,如果我把
else
后插入一个回
车
,不成了另一
种结
构的
if
语
句了
么
?后来我仔
细
地分析一下
逻辑关
系,才豁然
开
朗:原来是
BASIC
的
“ElseIf”
干
扰
了我的理解。
C++
中用哪
种
方法去理解都没区
别
。
都
说
switch
是
为
了
简
化
if
而出
现
的,但是
switch
虽
然可以
简
化
if
,却并不是任何
时
候都能使用。使用
switch
有两个先决因素:一是所有的条件都必
须
是
编译时
常量。也就是
说
如果要在程序运行
时
再决定
case
后的条件,那是不行的。另一个因素是只能拿出若干个整数
值
来比
较
是否相等,既不能是浮点数,也不能比
较
大于或小于。
switch
最容易出
错
的就是
丢
失
break
语
句了。因
为
按常
规
思路,人
们总
以
为
两个
标
号之
间
的
语
句才是
应该执
行的。从
BASIC
过
来的人更加痛苦,因
为
BASIC
里不需要
类
似于
break
这样
的
语
句来表示
结
束。
我的做法是,在打程序框架
时
,先把
case
标
号和
break
写了,其余的再去完善。即使
逻辑
上不需要
break
语
句,也要写上
“//break;”
,
这样
可以提醒自己和
团队
的伙伴:此
处
并未
丢
失
break
,而是的确不需要。
丢
失
default
是最理直气壮的了。因
为
的确有
许
多
时
候并不需要
default
,但是我的
经验
是要加上
default
以及它后面的
break
,原因同上,提醒自己和伙伴我没有
遗
漏
。
标题:
:
简单语
句与
复
合
语
句
祝
贺进
入第
6
章的学
习
。
简单语
句就是只有一句的
语
句,
“
复
合
语
句
”
也叫
语
句
块
,是由多句
语
句
组
成的一个整体。
虽
然
BASIC
也有
语
句
块
的概念,但是它
们
却是不同的概念:
BASIC
将
简单语
句
视为
特殊的
语
句
块
,而
C++
则
将
语
句
块视为
特殊的
简单语
句。个人
认为
,
C++
中
复
合
语
句的存在是
为
了
补
充
C++
没有
“end if”
之
类语
句的缺陷。
BASIC
中,
if
有
end if
(行
if
除外)、
while
有
wend
,
do
有
loop
。也就是
说
,有
头
就有尾,所以,
BASIC
编译
器不担心无法确定
语
句
块
的大小。
C++
则
不同,它的
这
些
关键
字都没有
结
束
语
句。没有
结
束
标记
,
谁
知道它的主体究竟是几行呢?所以,
C++
只好
规
定:所有
这
些
结
构的
语
句体都只能包含一句,而且必
须
包含一句(有且
仅
有一句)。
换
句
话说
,如果要多句,你也得做成一句的
样
。
将多行做成一行,就是所
谓
的
“
复
合
语
句
”
了。
说
到
简单语
句,空
语
句和空
块
是不能不提的。空
语
句(
块
)也是
语
句(
块
),只是它
啥
也不干。空
语
句存在的原因,无非也是因
为
C++
中
规
定了
语
句体必
须
是一句。
刚
才
说
了,那些
结
构的
语
句体是
“
有且
仅
有一句
”
,不
仅仅
“
多于一句要写成一句的
样
”
,反
过
来
说
,如果没有任何内容,你
也得
伪
造一句出来。于是
“
空
语
句
”
问
世了。
以下
语
句就是一个典型的例子:
int s = 0;
for (int i=1,s=0; i<101; s+=i,++i) ;//
空
语
句
空
语
句的存在
为
C++
徒增了
难
度与危
险
性,很多初学者弄不清哪些
语
句要以分号
结
尾,哪些
语
句不要,
错误
地在
for()
后面加了个分号,
结
果使循
环
体被取消了循
环
的
资
格,而且有可能出
现
死循
环
。
标题:
:
显
式
转换
引用:
显
式
转换
也称
为强
制
类
型
转换
。
笔
记
:我
觉
得要提
强
制
类
型
转换
,得从
C
风
格的
说
起。
这
里面可能有我个人的原因。因
为
我个人
习惯
了
C
风
格的
强
制
类
型
转换
。
在
C
语
言中,
强
制
类
型
转换
就是用借助一
对
括号同
时
把
类
型名和表达式列出来,比如
“(int)t”
和
“int(t)”
就是把
t
转为
int
型。
引用:因
为
要覆盖通常的
标
准
转换
,所以需
显
式使用
强
制
类
型
转换
。
……
显
式使用
强
制
类
型
转换
的另一个原因是:可能存在多
种转换时
,需要
选择
一
种
特定的
类
型
转换
。
笔
记
:从外文
图书
翻
译过
来的中国
图书
有个通病,就是
语
言不
伦
不
类
。本
书
算是翻
译
得非常好的了,依然无法
摆
脱
这种
影响。上文的意思无非是
说
:我不希望使用默
认
的
转换规则
的
时
候,就可以
显
式地
规
定按我的要求
转换
。如果要
举
个例子,可以拿上文《
类
型
转换
之
隐
式
转换
》中一个
现
成的例子:
int a = -3;
unsigned b = 3;
if (a == b)//
隐
式
转换
将
转为
unsigned int
if (a == (int)b)//
显
式指定
转换为
int
这种
用法更多地用于指
针类
型的
转换
。因
为
指
针类
型就是指
针
所指向
对
象的
类
型,而指
针
本身是没有
类
型区
别
的。所以,指向任何
类
型的指
针
可以互相
转换
。最典型的就是
void*
和其它
类
型之
间
的互
换
了,比如:
“int* p = (int*)malloc(sizeof(int) * MaxSize);”
还
有一
种
用法就是在
编译
器不允
许进
行
隐
式
转换
的
时
候,比如将
const
对
象
转为
非
const
对
象。如:
const int t = 3;
int* p = (int*)&t;//
本来要写作
const int* p = &t;
这种
用法
还
是少用
为
好,理由很
简单
,
编译
器之所以不允
许进
行
转换
,就是
为
了保
护
数据,你非要破坏
这种
安全性自然不好。即使能确信
这样
做不
产
生
恶
果,
这样
做至少是没有良好
风
格的。
C++
中
为显
式
类
型
转换
提供了四
种
不同的操作符:
static_case
、
dynamic_cast
、
const_cast
、
reinterpret_cast
。个人
认为
与
C
风
格的相比似乎都没有什
么进步
。
引用:
强
制
类
型
转换关闭
或挂起了正常的
类
型
检查
。
强
烈建
议
程序
员
避免使用
强
制
类
型
转换
,不依
赖强
制
类
型
转换
也能写好很好的
C++
程序。
标题:
:
类对
象的
隐
式
转换
与算
术类
型相比,
类
的
转换
更
复杂
。因
为
算
术转换
只
涉
及到精度的
问题
,而
类对
象的
转换
却
涉
及到能否
转换
以及怎
样转换
的
问题
。
隐
式
转换
就是
隐
式
转换
,它会出
现
在你没有注意的地方。参看以下代
码
:
class CMyInt
{
public:
CMyInt();
CMyInt(int i);
~CMyInt();
private:
int m_i;
};
CMyInt::CMyInt()
{
m_i = 0;
cout << "
无参数构造
(
默
认
0)" << endl;
}
CMyInt::CMyInt(int i)
{
m_i = i;
cout << "
从整数构造
,
值为
" << i << endl;
}
CMyInt::~CMyInt()
{
cout << "
析构
" << m_i << endl;
}
int _tmain(int argc, _TCHAR* argv[])
{
CMyInt a;
a = 3;
return 0;
}
执
行以上代
码
会
发现
,程序中有两次构造与两次析构,原因是
“a = 3;”
这
个
赋值
表达式要将右
边
的
类
型
转换为
左
边
的
类
型。
而
这
个
转换
是通
过调
用构造函数来
进
行的。
不
过
,以上代
码
只是
测试
用的,
读
者
们
千万不要在
实际
工作中写
这样
的代
码
。因
为这样
做是很不科学的。
“a = 3;”
这样
的表达式,只要
CMyInt
类
提供了右
值类
型
为
int
的
赋值
操作符重
载
,就可以避免使用构造函数来
进
行
转换
。操作符重
载
代
码
如下:
在
类
定
义
的
public
段添加一行:
“CMyInt& operator = (const int i);”
然后在
类
定
义
外部添加以下代
码
:
CMyInt& CMyInt::operator = (const int i)
{
m_i = i;
cout << "operator =" << i << endl;
return *this;
}
加了以上代
码
,同
样
是
“a = 3;”
就不使用
类
型
转换
了,改
为
使用
赋值
操作。我写下以上
这
段程序的目的只是
验证
VC++
中会有
类对
象的
转换
,它
们
与算
术类
型的
转换
差不多:生成一个合适的
临时对
象,参与运算以后再把
临时对
象
释
放掉
。