标题:重载函数再论
重载函数是C++提出来的概念,但是在C中却未必没有。比如“1+3”和“1.0+3.0”,虽然都是加法,做的却不是同的操作:编译器要因操作数的不同而调用不同的加法操作。只是C语言中除了内部类型变量可以参与运算以外,没有“类”这么高深的概念。“结构体”也只是内存数据的组织方法,而不涉及对整个结构体的处理。所以,在C语言时代编译器明明做了类似于重载的事情,却可以像雷锋一样“做好事不留名”。
C++发展出了类,并且赋予了“类”很高的期望,类的对象也能像内置类型对象一样参与一切运算。那么,就拿加法运算来说,编译器如何知道对某类对象的加法该调用哪一个详细的操作代码?于是,即使不出现普通函数的重载,至少运算符是要重载的。
林锐博士在《高质量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来重载,比如引用传递和指针传递。
本节开头第一句话就给出了重载函数的定义:重载函数必须符合两个条件:一是出现在相同的作用域中、二是函数名字相同而形参表不同。
其中第一个条件一般人往往是不去想的,其实函数名相同而作用域不同的函数大大存在,比如在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”,可能是出于微软的习惯吧。
说它是管理层面的问题,是因为这些代码的组织往往没有惟一的准则。但是它们还是有一定的规律的。
首先,软件的维护是一个复杂的系统工程。代码的组织应该有利于维护。应该尽量把直接相关的内容放在同一文件、不相关的内容放在不同的文件里。如果这些代码还有亲和疏,那就要分不同的文件夹来存放了。
其次,软件的代码是一个严格的组织体系。不同的内容之间可能是并列的,也可能有必要的先后关系。于是在“#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能表达的范围,对它进行运算也不会产生危险(溢出不能算危险,即使初始化过的数据也不能保证不溢出,而且溢出只是一种逻辑问题)。但是现在的类不这么简单了,忘了初始化往往将带来运行错误。于其每次都要考虑数据的初始化,还不如把这个初始化写成统一的函数,让系统自动调用来得既安全又方便。
笔记:构造函数的确是一类“特殊”的成员函数。它的特殊性至少表现在以下几个方面:一是它的调用不用程序员操心,只要类对象被创建它就会被调用,而且它不允许被程序员显式地调用。二是它们是必需的,如果程序员偷懒,编译器将自动创建简单的构造函数。三是它们的名字不用程序员多考虑,直接与类名相同。四是它们没有返回值。
下面详说这几个特性:
一、它们在类对象被创建时自动调用,创建对象可能有以下方法:程序中用声明变量的语句直接声明创建,或者在程序中用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来修饰的东西了。林锐说“大概是因为其它地方都已经被占用了”并不是根本原因。
类的成员函数与普通函数(全局函数)相比,最根本的区别是实现了类的封装性。封装性的第一个表现是访问权限:都是函数,但是你能访问哪个不能访问哪个却可以设定。第二个表现是直观,通过类成员(或指针)来调用函数,给人的直觉就是“这是类提供的功能”。你好像“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文件中要有函数的实体。既然这样,就放进头文件吧。
C++用内联来取代宏替换,大大提高了安全性。虽然内联函数也是编译时展开的,但是它能进行安全检查,还能处理类的成员函数(原因是内联函数能够处理this指针,宏却不能)。
引用:内联对编译器来说只是一个建议,编译器可以选择忽略这个建议。
笔记:也就是说,有些函数你想内联,编译器也不一定会采纳。因为内联函数虽然减少了函数调用的开销,却增加了程序的体积。
内联函数是唯一允许实体多次被编译的函数。原因是编译器必须先编译这个函数体,才能在编译函数调用的地方进行合理地展开。这就说明在多个CPP文件组成的工程中,可能有不止一个CPP文件中要有函数的实体。既然这样,就放进头文件吧。
对本文本的评论有:
我觉得象这个max()和以前的数组越界一类的事,都可以归纳为一句话,那就是,C为我们提供了强大的工具,那些不会使用的人才会出现这种错误.连个数组越界也管理不好的,还是去写武侠小说比较好.
比如火药发明了以后,我们可以用来炸山开路什么的,难道因为有人用于战争,就怪这个火药功能不够完善吗?
比如火药发明了以后,我们可以用来炸山开路什么的,难道因为有人用于战争,就怪这个火药功能不够完善吗?
是这样的,我们不应该怪C标准不好,
虽然它不能让result = MAX(i++, j);这种问题得到解决,
产生i被自增两次这样的结果,程序员应该自己去避免。
但是,如果标准有进步了,我们倒是因为祝贺它一下。
虽然它不能让result = MAX(i++, j);这种问题得到解决,
产生i被自增两次这样的结果,程序员应该自己去避免。
但是,如果标准有进步了,我们倒是因为祝贺它一下。
标题::局部对象与静态局部对象
本节首先向读者说明了“名字的作用域”和“对象的生命周期”这两个概念,不难,理解了就行了。前者是空间概念:指程序还处在代码阶段的时候这个名字的可见范围,后者是时间概念:指程序运行过程中对象的存在时间。
函数的形参以及函数内部声明的对象都是局部对象,它们的作用域就是函数内部,但是它们的生命周期却未必是函数的执行过程。这看起来有点摸不着头脑,原因在于C++的函数中允许存在以关键字“static”声明的静态对象。
也就是说,静态对象是这样一个对象:它的生命周期很长,可以跨越该函数的每次调用,哪怕该函数每24小时才调用一次,它也是全天候存在的。但是要想访问她,却只有函数正在执行的时候才行。
签于以上特性,我专门写了两个测试函数,该函数试途返回局部对象的引用或指针:
int& GetInt()
{
int t=3;
return t;//警告
}
函数的形参以及函数内部声明的对象都是局部对象,它们的作用域就是函数内部,但是它们的生命周期却未必是函数的执行过程。这看起来有点摸不着头脑,原因在于C++的函数中允许存在以关键字“static”声明的静态对象。
也就是说,静态对象是这样一个对象:它的生命周期很长,可以跨越该函数的每次调用,哪怕该函数每24小时才调用一次,它也是全天候存在的。但是要想访问她,却只有函数正在执行的时候才行。
签于以上特性,我专门写了两个测试函数,该函数试途返回局部对象的引用或指针:
int& GetInt()
{
int t=3;
return t;//警告
}
int* GetInt2()
{
int t = 3;
return &t;//警告
}
以上两个警告产生的原因是函数返回了临时对象的引用或地址。但是如果将t的声明改成“static int t=3;”就不再显示警告。
静态局部对象似乎为节约系统开销做了准备。不过我认为这个特性不应该被滥用。只有确实有必要让对象生命周期跨越多次调用时才应该把它声明为静态(比如统计函数被调用的次数)。否则将提高造成BUG的可能性,使“高效率”的程序成为空中楼阁。
{
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++与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++的“类”,类的声明写进头文件,而实体却写进程序文件。不同的是,类的声明不像函数的声明那样只有一句话,而是一个完整的结构。
函数的实体就是实实在在的函数内容,它规定了这个函数怎样执行,这没有什么好说的。那么函数为什么还要有声明呢?
这样做的目的之一是告诉编译器:虽然你还没有见到函数本身,不知道函数是怎样执行的,但是我先告诉你这个函数的名称、参数与返回值,你就先编译吧。至于这个函数究竟干什么,等到连接的时候再说。
设计合理的程序,其代码存放在不同的文件中,函数的实体只能有一个,存放在某一个源文件中。其它源文件中如果要用到这个函数,就在这个文件中加入函数的声明。
这样做的目的之二是函数的提供者与使用者往往不是同一个人,甚至不是同一个企业。出于种种目的,函数的提供者可能并不想(或不必)让使用者知道这个函数的具体内容,只要使用者能调用就行。这种情况下,函数的提供者只需要提供一个声明给使用者即可。——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);
}
程序二的写法与程序一没有区别,但可以告诉自己递归必须有终止条件。防止一不小心就写了个“永远”。
似乎绝大多数递归函数都可以用循环来解决。这两种方法迁就了不同的对象:循环用少量的计算机资源、大量的人力来解决问题,递归则用大量的计算机资源、少量的人力来解决问题。所以,在计算机速度和存储量都不大的年代,曾有人反对递归。
汉诺塔问题据说是只有用递归才可以解决的问题,其实只有要求解汉诺塔的移动过程才必须用递归,如果只要求解移动次数,那么用循环也不成问题。
引用:递归函数必须定义一个终止条件,否则函数将永远递归下去,这意味着函数会一直调用自身直到程序耗尽。
初识递归的时候,的确有些不容易搞明白。记得当时的教科书为此画一个图,用一组箭头来表示要计算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;
}
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的语句。”
引用: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[]数组之所以不能这样做,是因为第二个条件无法满足。
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必须是局部变量
}
//一个错误的函数
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++到来以后,函数的返回值类型可以是类类型。而类对象的构造、复制与销毁可能很复杂、很占用系统资源。于是“引用传递”再一次发挥了它的威力。
函数的返回值没有类型限制,可以是内置类型变量,也可以是类对象。无论是内置类型还是类对象,都有着一样的规律。但是,这些规律在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中未必改动参数。
但是,天下没有这么便宜的午餐!引用传递不是倒处能用的。举个例子:
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++为什么要有“引用传递”这回事?一种说法是只有引用才能达到操作符重载的目的,这个以后再谈。但是,撇开这个不谈,形参是不是引用,直接影响了程序执行的效率。前面提到过,函数调用时要用实参的值去初始化形参,初始化的过程包含了定义一个变量、然后给它赋一个值两个过程,如果这个变量并不是内部变量,而是一个类对象,那么,定义一个类对象可能很复杂,而初始化这个对象一样会很复杂。而引用只是给对象取一个别名,不涉及定义与初始化,离开作用域时也不用释放。
相比之下,用指针传递可以避免类对象的定义、初始化与释放。只需要付出指针变量的定义、初始化与释放的代价。但是,指针的杀伤力太大。即使是熟练的程序员,也不能保证绝不出现“野指针”,野针的代价几乎无一例外是程序崩溃。
引用也不是吃素的,如果说指针传递是“帮你配了一把我家的钥匙”,那么引用传递就是直接把我家的财产都交给了你。有时,我们使用引用传递仅仅是为了效率,而不希望实参被修改,那就要记得把形参标记为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++有了“引用传递”后,“形参的改变不影响实参”被判无效。这就得提到“引用传递”的概念了,下文再续。
在我发表《函数的定义》一文后,有朋友发表意见,提到了“函数调用过程中的入栈与出栈”,在此首先作个说明:我读的是《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周期.所以,一般调用函数都用入/出栈来备拷贝参数.
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)”则有不同的结果。
以上把“形参”和“实参”提了这么多,主要目的还是讲清“形参的改变不影响实参”这句话。字数不少了,留到下篇文章再续吧。(我觉得我写得不像读书笔记,倒像是教材了。呵呵)
要弄清这个,首先得弄清形参与实参是什么东西。因为函数是一段“可以重用而不必重写”的代码,每次重用当然未必完全相同(不可否认有些函数每次重用都完全相同),那么不同在哪里呢?又怎样产生不同呢?一种方法是依靠随机,随机是个好东西,不要说客户了,连程序员都无法控制每次调用的结果。第二种方法是凭客观条件(比如运行时间、机器配置)。但是这些函数应用很窄,类似于“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迁就了老程序员?
这个说法到了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定义变量做的事完全不同,所以,不按定义变量的方式写,也很正常.
如果偷猫兄一定要写得一样,那就自己做一个编译器吧.
这与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”对应的词,无法定义不返值的函数,才不得已出此下策。
“函数”这个概念在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<<i;”这样的程度,错误处理不是我们要学习的目标。但是,一旦开始编写实用的程序,那么,无论考虑多么周到,无论代码多么精良。意外总是难免的。这些意外可能来自程序员的设计不到位、可能来自用户的错误操作、还可能来自机器与网络的不确定因素。
没有什么比追踪错误更难过的事了,记得有一回我在追踪一个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模式编译。
没有什么比追踪错误更难过的事了,记得有一回我在追踪一个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的日志满了,这个错误显然我在写程序的时候没有想过,丢脸啊...
在网络编程中,有些错误是无法预知的,比如网络连接断了,数据库当了...好象在这些情况下,用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不允许跨函数跳越。),实验二说明在程序段中的标号不允许使用保留字。
如果它们处在由多重循环和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)”了。
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,原因同上,提醒自己和伙伴我没有遗漏。
大体上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()后面加了个分号,结果使循环体被取消了循环的资格,而且有可能出现死循环。
简单语句就是只有一句的语句,“复合语句”也叫语句块,是由多句语句组成的一个整体。虽然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++程序。
笔记:我觉得要提强制类型转换,得从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;
}
隐式转换就是隐式转换,它会出现在你没有注意的地方。参看以下代码:
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++中会有类对象的转换,它们与算术类型的转换差不多:生成一个合适的临时对象,参与运算以后再把临时对象释放掉。
{
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++中会有类对象的转换,它们与算术类型的转换差不多:生成一个合适的临时对象,参与运算以后再把临时对象释放掉。
标题:重载函数再论
重载函数是C++提出来的概念,但是在C中却未必没有。比如“1+3”和“1.0+3.0”,虽然都是加法,做的却不是同的操作:编译器要因操作数的不同而调用不同的加法操作。只是C语言中除了内部类型变量可以参与运算以外,没有“类”这么高深的概念。“结构体”也只是内存数据的组织方法,而不涉及对整个结构体的处理。所以,在C语言时代编译器明明做了类似于重载的事情,却可以像雷锋一样“做好事不留名”。
C++发展出了类,并且赋予了“类”很高的期望,类的对象也能像内置类型对象一样参与一切运算。那么,就拿加法运算来说,编译器如何知道对某类对象的加法该调用哪一个详细的操作代码?于是,即使不出现普通函数的重载,至少运算符是要重载的。
林锐博士在《高质量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来重载,比如引用传递和指针传递。
本节开头第一句话就给出了重载函数的定义:重载函数必须符合两个条件:一是出现在相同的作用域中、二是函数名字相同而形参表不同。
其中第一个条件一般人往往是不去想的,其实函数名相同而作用域不同的函数大大存在,比如在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”,可能是出于微软的习惯吧。
说它是管理层面的问题,是因为这些代码的组织往往没有惟一的准则。但是它们还是有一定的规律的。
首先,软件的维护是一个复杂的系统工程。代码的组织应该有利于维护。应该尽量把直接相关的内容放在同一文件、不相关的内容放在不同的文件里。如果这些代码还有亲和疏,那就要分不同的文件夹来存放了。
其次,软件的代码是一个严格的组织体系。不同的内容之间可能是并列的,也可能有必要的先后关系。于是在“#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能表达的范围,对它进行运算也不会产生危险(溢出不能算危险,即使初始化过的数据也不能保证不溢出,而且溢出只是一种逻辑问题)。但是现在的类不这么简单了,忘了初始化往往将带来运行错误。于其每次都要考虑数据的初始化,还不如把这个初始化写成统一的函数,让系统自动调用来得既安全又方便。
笔记:构造函数的确是一类“特殊”的成员函数。它的特殊性至少表现在以下几个方面:一是它的调用不用程序员操心,只要类对象被创建它就会被调用,而且它不允许被程序员显式地调用。二是它们是必需的,如果程序员偷懒,编译器将自动创建简单的构造函数。三是它们的名字不用程序员多考虑,直接与类名相同。四是它们没有返回值。
下面详说这几个特性:
一、它们在类对象被创建时自动调用,创建对象可能有以下方法:程序中用声明变量的语句直接声明创建,或者在程序中用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来修饰的东西了。林锐说“大概是因为其它地方都已经被占用了”并不是根本原因。
类的成员函数与普通函数(全局函数)相比,最根本的区别是实现了类的封装性。封装性的第一个表现是访问权限:都是函数,但是你能访问哪个不能访问哪个却可以设定。第二个表现是直观,通过类成员(或指针)来调用函数,给人的直觉就是“这是类提供的功能”。你好像“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文件中要有函数的实体。既然这样,就放进头文件吧。
C++用内联来取代宏替换,大大提高了安全性。虽然内联函数也是编译时展开的,但是它能进行安全检查,还能处理类的成员函数(原因是内联函数能够处理this指针,宏却不能)。
引用:内联对编译器来说只是一个建议,编译器可以选择忽略这个建议。
笔记:也就是说,有些函数你想内联,编译器也不一定会采纳。因为内联函数虽然减少了函数调用的开销,却增加了程序的体积。
内联函数是唯一允许实体多次被编译的函数。原因是编译器必须先编译这个函数体,才能在编译函数调用的地方进行合理地展开。这就说明在多个CPP文件组成的工程中,可能有不止一个CPP文件中要有函数的实体。既然这样,就放进头文件吧。
对本文本的评论有:
我觉得象这个max()和以前的数组越界一类的事,都可以归纳为一句话,那就是,C为我们提供了强大的工具,那些不会使用的人才会出现这种错误.连个数组越界也管理不好的,还是去写武侠小说比较好.
比如火药发明了以后,我们可以用来炸山开路什么的,难道因为有人用于战争,就怪这个火药功能不够完善吗?
比如火药发明了以后,我们可以用来炸山开路什么的,难道因为有人用于战争,就怪这个火药功能不够完善吗?
是这样的,我们不应该怪C标准不好,
虽然它不能让result = MAX(i++, j);这种问题得到解决,
产生i被自增两次这样的结果,程序员应该自己去避免。
但是,如果标准有进步了,我们倒是因为祝贺它一下。
虽然它不能让result = MAX(i++, j);这种问题得到解决,
产生i被自增两次这样的结果,程序员应该自己去避免。
但是,如果标准有进步了,我们倒是因为祝贺它一下。
标题::局部对象与静态局部对象
本节首先向读者说明了“名字的作用域”和“对象的生命周期”这两个概念,不难,理解了就行了。前者是空间概念:指程序还处在代码阶段的时候这个名字的可见范围,后者是时间概念:指程序运行过程中对象的存在时间。
函数的形参以及函数内部声明的对象都是局部对象,它们的作用域就是函数内部,但是它们的生命周期却未必是函数的执行过程。这看起来有点摸不着头脑,原因在于C++的函数中允许存在以关键字“static”声明的静态对象。
也就是说,静态对象是这样一个对象:它的生命周期很长,可以跨越该函数的每次调用,哪怕该函数每24小时才调用一次,它也是全天候存在的。但是要想访问她,却只有函数正在执行的时候才行。
签于以上特性,我专门写了两个测试函数,该函数试途返回局部对象的引用或指针:
int& GetInt()
{
int t=3;
return t;//警告
}
函数的形参以及函数内部声明的对象都是局部对象,它们的作用域就是函数内部,但是它们的生命周期却未必是函数的执行过程。这看起来有点摸不着头脑,原因在于C++的函数中允许存在以关键字“static”声明的静态对象。
也就是说,静态对象是这样一个对象:它的生命周期很长,可以跨越该函数的每次调用,哪怕该函数每24小时才调用一次,它也是全天候存在的。但是要想访问她,却只有函数正在执行的时候才行。
签于以上特性,我专门写了两个测试函数,该函数试途返回局部对象的引用或指针:
int& GetInt()
{
int t=3;
return t;//警告
}
int* GetInt2()
{
int t = 3;
return &t;//警告
}
以上两个警告产生的原因是函数返回了临时对象的引用或地址。但是如果将t的声明改成“static int t=3;”就不再显示警告。
静态局部对象似乎为节约系统开销做了准备。不过我认为这个特性不应该被滥用。只有确实有必要让对象生命周期跨越多次调用时才应该把它声明为静态(比如统计函数被调用的次数)。否则将提高造成BUG的可能性,使“高效率”的程序成为空中楼阁。
{
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++与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++的“类”,类的声明写进头文件,而实体却写进程序文件。不同的是,类的声明不像函数的声明那样只有一句话,而是一个完整的结构。
函数的实体就是实实在在的函数内容,它规定了这个函数怎样执行,这没有什么好说的。那么函数为什么还要有声明呢?
这样做的目的之一是告诉编译器:虽然你还没有见到函数本身,不知道函数是怎样执行的,但是我先告诉你这个函数的名称、参数与返回值,你就先编译吧。至于这个函数究竟干什么,等到连接的时候再说。
设计合理的程序,其代码存放在不同的文件中,函数的实体只能有一个,存放在某一个源文件中。其它源文件中如果要用到这个函数,就在这个文件中加入函数的声明。
这样做的目的之二是函数的提供者与使用者往往不是同一个人,甚至不是同一个企业。出于种种目的,函数的提供者可能并不想(或不必)让使用者知道这个函数的具体内容,只要使用者能调用就行。这种情况下,函数的提供者只需要提供一个声明给使用者即可。——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);
}
程序二的写法与程序一没有区别,但可以告诉自己递归必须有终止条件。防止一不小心就写了个“永远”。
似乎绝大多数递归函数都可以用循环来解决。这两种方法迁就了不同的对象:循环用少量的计算机资源、大量的人力来解决问题,递归则用大量的计算机资源、少量的人力来解决问题。所以,在计算机速度和存储量都不大的年代,曾有人反对递归。
汉诺塔问题据说是只有用递归才可以解决的问题,其实只有要求解汉诺塔的移动过程才必须用递归,如果只要求解移动次数,那么用循环也不成问题。
引用:递归函数必须定义一个终止条件,否则函数将永远递归下去,这意味着函数会一直调用自身直到程序耗尽。
初识递归的时候,的确有些不容易搞明白。记得当时的教科书为此画一个图,用一组箭头来表示要计算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;
}
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的语句。”
引用: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[]数组之所以不能这样做,是因为第二个条件无法满足。
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必须是局部变量
}
//一个错误的函数
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++到来以后,函数的返回值类型可以是类类型。而类对象的构造、复制与销毁可能很复杂、很占用系统资源。于是“引用传递”再一次发挥了它的威力。
函数的返回值没有类型限制,可以是内置类型变量,也可以是类对象。无论是内置类型还是类对象,都有着一样的规律。但是,这些规律在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中未必改动参数。
但是,天下没有这么便宜的午餐!引用传递不是倒处能用的。举个例子:
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++为什么要有“引用传递”这回事?一种说法是只有引用才能达到操作符重载的目的,这个以后再谈。但是,撇开这个不谈,形参是不是引用,直接影响了程序执行的效率。前面提到过,函数调用时要用实参的值去初始化形参,初始化的过程包含了定义一个变量、然后给它赋一个值两个过程,如果这个变量并不是内部变量,而是一个类对象,那么,定义一个类对象可能很复杂,而初始化这个对象一样会很复杂。而引用只是给对象取一个别名,不涉及定义与初始化,离开作用域时也不用释放。
相比之下,用指针传递可以避免类对象的定义、初始化与释放。只需要付出指针变量的定义、初始化与释放的代价。但是,指针的杀伤力太大。即使是熟练的程序员,也不能保证绝不出现“野指针”,野针的代价几乎无一例外是程序崩溃。
引用也不是吃素的,如果说指针传递是“帮你配了一把我家的钥匙”,那么引用传递就是直接把我家的财产都交给了你。有时,我们使用引用传递仅仅是为了效率,而不希望实参被修改,那就要记得把形参标记为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++有了“引用传递”后,“形参的改变不影响实参”被判无效。这就得提到“引用传递”的概念了,下文再续。
在我发表《函数的定义》一文后,有朋友发表意见,提到了“函数调用过程中的入栈与出栈”,在此首先作个说明:我读的是《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周期.所以,一般调用函数都用入/出栈来备拷贝参数.
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)”则有不同的结果。
以上把“形参”和“实参”提了这么多,主要目的还是讲清“形参的改变不影响实参”这句话。字数不少了,留到下篇文章再续吧。(我觉得我写得不像读书笔记,倒像是教材了。呵呵)
要弄清这个,首先得弄清形参与实参是什么东西。因为函数是一段“可以重用而不必重写”的代码,每次重用当然未必完全相同(不可否认有些函数每次重用都完全相同),那么不同在哪里呢?又怎样产生不同呢?一种方法是依靠随机,随机是个好东西,不要说客户了,连程序员都无法控制每次调用的结果。第二种方法是凭客观条件(比如运行时间、机器配置)。但是这些函数应用很窄,类似于“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迁就了老程序员?
这个说法到了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定义变量做的事完全不同,所以,不按定义变量的方式写,也很正常.
如果偷猫兄一定要写得一样,那就自己做一个编译器吧.
这与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”对应的词,无法定义不返值的函数,才不得已出此下策。
“函数”这个概念在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<<i;”这样的程度,错误处理不是我们要学习的目标。但是,一旦开始编写实用的程序,那么,无论考虑多么周到,无论代码多么精良。意外总是难免的。这些意外可能来自程序员的设计不到位、可能来自用户的错误操作、还可能来自机器与网络的不确定因素。
没有什么比追踪错误更难过的事了,记得有一回我在追踪一个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模式编译。
没有什么比追踪错误更难过的事了,记得有一回我在追踪一个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的日志满了,这个错误显然我在写程序的时候没有想过,丢脸啊...
在网络编程中,有些错误是无法预知的,比如网络连接断了,数据库当了...好象在这些情况下,用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不允许跨函数跳越。),实验二说明在程序段中的标号不允许使用保留字。
如果它们处在由多重循环和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)”了。
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,原因同上,提醒自己和伙伴我没有遗漏。
大体上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()后面加了个分号,结果使循环体被取消了循环的资格,而且有可能出现死循环。
简单语句就是只有一句的语句,“复合语句”也叫语句块,是由多句语句组成的一个整体。虽然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++程序。
笔记:我觉得要提强制类型转换,得从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;
}
隐式转换就是隐式转换,它会出现在你没有注意的地方。参看以下代码:
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++中会有类对象的转换,它们与算术类型的转换差不多:生成一个合适的临时对象,参与运算以后再把临时对象释放掉。
{
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++中会有类对象的转换,它们与算术类型的转换差不多:生成一个合适的临时对象,参与运算以后再把临时对象释放掉。