The C++ Programming Language 第七章 笔记

The C++ Programming Language 第七章 笔记
james chen
050311

*********************
7.1 函数声明
*********************

在一个函数声明中,需要给出函数的名字,这个函数返回的值的类型(如果有的话),以及在调用这个函数时必须提供的参数的个数和类型。
在函数声明中可以包含参数的名字,这样做可能对读程序的人有所帮助,但编译器将简单地忽略掉这样的名字。

7.1.1 函数定义

在程序里调用的某个函数都必须在某个地方定义,仅仅一次。一个函数定义也就是给出了函数体的函数声明。

void swap(int* p,int* q);  //声明

void swap(int* p,int* q)
{
 int t=*p;
 *p=*q;
 *q=t;
}

一个函数的定义和对它的所有声明必须都描述了同样的类型。不过,这里并不把参数名字作为类型的一部分,因此参数名不必保持一致。如:
void swap(int* p,int* q);  //声明
void swap(int* a,int* b){}  //函数定义的参数名可与声明不一样,但类型必须一样。
void swap(int*,int*);   //声明亦可无参数名

inline(内联,在线)函数即在编译时将函数展开直接嵌入程式之中,直接运行,跳过调用函数的过程。定义:
inline int max(int* a,int* b)
{return a>b?a:b;}
例:
int a=22,b=11,c;
c=max(a,b);    //在编译时这行将变成:c=a>b?a:b;

7.1.2 静态变量

局部变量将在运行线程达到其定义时进行初始化。按照默认方式,这件事发生在函数的每次调用中,且函数的每个调用有自己的一份局部变量副本。如果一局部变量被声明为static,那么将只有惟一的一个静态分配的对象,它被用于在该函数的所有调用中表示这个变量,这个对象将只在执行线程第一次到达它的定义时初始化。


void f(int a)
{
while(a--)
static int n=0;    //第一次调用该函数时初始化,再次调用将略过此句
int x=0;    //每次调用函数时都初始化
cout<<"n=="<<n++<<",x=="<<x++<<endl;
}
void main()
{f(3);}
结果是:
n==0,x==0
n==1,x==0
n==2,x==0    //静态变量n的每次运行值都得到了保持,而常规x变量则不是

静态变量为函数提供了一种“存储器”,使我们不必去引进可能被其他函数访问或破坏的全局变量。


*********************
7.2 函数传递
*********************

当一个函数被调用时,将安排好其形式参数所需要的存储,各个形式参数将用对应的实际参数进行初始化。参数传递的语义与初始化的语义完全相同。特别是,需要对照每个形式参数检查与之对应的实际参数的类型,交执行所有标准的和用户自定义的类型转换。有一些特殊规则处理数组参数的传递,一种传递不加检查的参数的机制以及一种刻画默认参数的机制。考虑:

void f(int a,int& b)
{a++;b++;}

当f被调用时,a++增加的是第一个实际参数的一个副本,而b++增加的却是第二个实际参数本身,因为b是第二个实际参数的一个引用。
通过引用方式传递大的对象,比通过值传递的速度更高,在这种情况下,可以将参数声明为const,以指明使用这种参数仅仅是为了效率而使用,而不是让函数修改实际参数的值。
如果在一个引用参数的声明中没有const,就应该认为,这是想说明该参数将被修改。与此类似,将指针参数声明为const,也就是告知读者,函数将不修改由这个参数所指的对象,如:
int strlen(const char*);  //求c风格字串的长度
char* strcpr(char* to,const char* from);//复制c风格字串
int strcmp(const char*,const char*); //比较两个c风格字串
const参数的重要性将随着程序的规模增大而进一步增长。

文字量、常量和需要转换的参数都可以传递给const&参数,但不能传递给非const的引用参数。允许对const t&参数进行转换,就保证了对这种参数所能提供的值集合,正好与通过一个临时量传递T参数的集合相同。

float f(const float&);
void g(double d)
{
float r=f(2.0f);   //传递的是保存2.0f的临时量的引用
r=f(r);     //传递的是r的引用
r=f(d);     //传递的是d的引用,在传入时会进行double->float转换
}

对于非const引用参数不允许做类型转换,这种规定能帮助我们避免了一种由于临时量而产生的可笑错误,如:
void update(float& i);
void f(double d,float r)
{
update(2.0f);    //错误,2.0f是临时量,不能传递给T&,只能传递给const T&
update(r);    //传递r的引用
update(d);    //错误,形参是float,而实参是double,没有进行type转换

7.2.1 数组参数

如果将数组作为函数的参数,传递的就是到数组的首元素的指针。如:
int strlen(const char*)
void f()
{
char v[]="how are you";
int i=strlen(v);
int j=strlen("hahaha");   //相当于临时数组
}


也就是说,类型t[]作为参数传递时将被转换为一个t*。这也就意味着,对数组参数的某个元素的赋值,将改变实际参数数组中那个元素的值。换句话说,数组与其他类型不同,数组不会也不能按值的方式传递。
也就是说从t[]到t*,是用的按址传递。

对于数组而言,数组参数的大小是不可用的。c风格的字串是以0结束的,它们的大小就很容易计算,对于其他数组,可以传递第二个参数,用它来标识数组的大小,如3:

void f(int* c_ptr,int c_size);

换一种方式,我们也可以不用数组,而用vector等类型。

多维数组的情况更加诡异,但通常可以改用指针的数组,这样做并不需要特别的处理,如:
char* day[]={"mon","tue","wed","thu","fri","sat","sun"};

再提一次,vector等类型可以用于代替内部的、低级的数组和指针等一类的东西。


*********************
7.3 返回值
*********************

若一函数为非void类型的,则必须返回一个值。当然,void类型之函数就不能返回值。
int f(){}    //error,无返回值
void f(){}    //ok
int f(){return 1;}   //ok
void f(){return 1;}   //错,不能有返回值
int f(){return;}   //错,无返回值
void f(){return;}   //ok,虽有return,但无返回值

返回值亦可由返回语描述,如:
int fac(int n)
{return (n>1)?n*fac(n-1):1;}  //一个调用自己的函数称为递归

就像参数传递的语义一样,函数返回值的语义也与初始化的语义相同,可以认为返回语句所做的就是去初始化一个具有返回类型的匿名变量。这时将对照函数的返回类型检查返回表达式的类型,并执行所标准的或用户定义的转换。如:
double f(){return 2;}   //2被隐式地转换为double(2)

每当一个函数被调用时,就会建立起它的所有参数和局部(自动)变量的一个副本。在该函数返回后,这些存储又会被另做他用。所以,绝不能返回指向局部变量的指针,因为被指位置中内容的改变情况是无法预料的:
int* f(){int local=1;return &local;} //错

这种错误一般没有通过引用造成的类似错误那么普遍:
int& f(){int local=1;return &local;} //错得更严重,还好,大多数编译器都可提出警告

一个void函数可以将另一个void函数作为它的return语句中的表达式。如:
void g(int* p);
void h(int* p){return g(p);}  //ok,返回无值
其实就相当于void h(int* p){g(p);}

这种返回形式有其重要性,如果要写的模板函数的返回类型是模板参数,就很可能需要用这种东西。


*********************
7.4 重载函数名
*********************

一般来说,不同的函数应有不同的名字。但是,当某些函数在不同类型的对象上执行相同概念的操作时,能给它作取相同的名字就很方便了。
将同一个名字用于不同类型上操作的函数的情况叫重载。如

void print(int);   //打印int
void print(const char*);  //打印char*

它们的功能都是将传入参数打印出来,只是参数类型不一样罢了,所以可以函数名相同,即函数重载。
重载函数名从根本上说就是一种记法上的方便,对于那些常用的函数,如open,add,print等,这种方便性是很重要的。
当一个重载函数f()被调用时,编译器就必须弄清楚究竟应该调用具有名字f的哪一个函数,为了完成这项工作,它需要将实际参数的类型与所有名为f的函数的形参的类型作比较,直到找到与实参最为匹配的f(),便执行此f(),如果不存在匹配的函数,就给一个编译错误。如:

void print(double);
void print(long);
void f()
{
print(1L);    //print(long)
print(1.0);    //print(double)
print(1);    //错误,有歧义,可能是print(long(1))或是print(double(1))
}

要从若干个重载函数中找到应实际调用的那个正确版本,就需要找到在参数表达式的类型和函数的形参类型之间的最好匹配。为了尽可能接近我们关于怎样做最合理的观念,需要按顺序检查下面的一系列判断准则:

[1]准确匹配,也就是说,无须任何转换或者只须做平凡转换(如,数组名到指针,函数名到函数指针,T到const T等)的匹配。
[2]利用提升的匹配,即包括整数提升(bool到int,char到int,short到int以及它们的无符号版本;以及float到double的提升)。
[3]利用标准转换,(int到double,double到int,double到long double,T*到void*,int到unsigned int)
[4]利用用户自定义转换的匹配。
[5]利用函数声明中的省略号的匹配。

如果在能找到匹配的某个最高层次上同时发现两个匹配,这个调用将作为歧义而被拒绝,如:

void print(int);
void print(const char*);
void print(double);
void print(long);
void print(char);
void f(char r,int i,short s,float f)
{
print(c);    //ok,print(char)
print(i);    //ok,print(int)
print(s);    //ok,整数提升,print(int(s))
print(f);    //ok,float到double的提升,print(double(f))
print('a');    //ok,print(char)
print(49);    //ok,print(int)
print(0);    //ok,print(int)
print("addf");    //ok,print(char*)
}

重载解析与被考虑的函数声明的顺序无关。
相对而言,重载所依赖的这组规则是比较复杂的,但程序员却很少会对哪个函数被调用感到吃惊。那么,为什么呢?请考虑一下重载的替代方式。我们经常需要对几个类型的对象执行类似操作。如果没有重载,我们就必须用几个不同的名字去定义几个函数:

void print_int(int);
void print_char(char);
void print_string(const char*);
void f(int i,char c,const char* p,double d)
{
print_int(i);    //ok
print_char(c);    //ok
print_string(p);   //ok

print_int(c);    //可以用,但调用print_int(int(c))
print_char(i);    //可以用,但调用print_char(char(i))
print_string(i);   //错误
print_int(d);    //ok
}

与重载的print()相比,我们必须同时记住几个名字,而且要记住如何正确地使用它们。这会令人厌倦,也会挫败通用开型程序设计的企图,并促使程序员去注意相对低级的类型问题。因为不存在重载函数,所有标准转换都将被用于这些函数的参数。这样做也可能引起错误。在上面的例子里,可以看出,在这里的四个采用了“错误”参数的调用中,编译器只能捕捉到其中一个。由此看,重载还能增加编译器拒绝不合适参数的机会。

7.4.1 重载和返回类型

重载解析中将不考虑返回类型。这样规定的理由就是要保持对重载的解析只是针对单独的运算符或函数调用,与调用的环境无关。考虑:
float f(float);
double f(double);
void f(double d,float f)
{
float f=f(d);    //调用f(double)
double d=f(d);    //调用f(double)
f=f(f);     //调用f(float)
d=f(f);     //调用f(float)
}

如果把返回类型也考虑在内,我们就无法继续去孤立地去看一个f()调用,并由此确定到底应该调用哪个函数了。


7.4.2 重载和作用域
在不同的非名字空间作用域里声明的函数不算是重载。如:
void f(int);
void g()
{
void f(double);
f(1);     //调用f(double)
}

f(int)应该是对f(1)的最好匹配,但只有f(double)在作用域里。对于这种类情况,可以通过加入或者去除局部声明的方式去取得所需要的行为。与其他地方一样,有意识的遮蔽可以成为一种很有用的技术,但无意识的遮蔽则是产生令人吃惊情况的一个根源。如果希望重载能够跨越类作用域或名字空间作用域,那么可以利用使用声明或者使用指令。

7.4.3 手工的歧义性解析
对一个函数,声明的重载版本过少或者过多都有可能导致歧义性,如:
void f1(char);
void f1(long);
void f2(char*);
void f2(int*);

void f(int i)
{
f1(i);     //歧义的,找不到f1(int),就可能是f1(char)或f1(long)
f2(0);     //歧义的,f2(char*)或者f2(int*)
}

只要可能,在这种情况下应该做的就是将该函数的重载版本集合作为一个整体来考虑,看看对于函数的语义而言它们是否有意义。有关问题经常可以通过增加一个消解歧义的版本而得到解决。例如,加进
inline void f1(int a){f1(long(a));}
inline void f2(int a){f2(&a);}
就能以偏向更大类型long int的方式来消解所有类似f1(i)的歧义性的情况。
也可以通过增加一个显式类型转换的方式去解决某个特定调用的问题。例如:
f2(static_cast<int*>(0));
然而这只是一种权宜之计,很快就可能遇到另一个必须处理的类似调用。

7.4.4 多参数的解析
有了上述重载解析规则之后,我们就可以保证:当所涉及到的不同类型在计算效率或者精度方面存在明显差异时,被调用将会是最简单的算法。例如:
int pow(int,int);
double pow(double,double);

complex pow(double,complex);
complex pow(complex,int);
complex pow(complex,double);
complex pow(complex,complex);

void f(complex z)
{
int i=pow(2,2);     //pow(int,int)
double d=pow(2.0,2.0);    //pow(double,double)
complex z2=pow(2,z);    //pow(double,complex)
complex z3=pow(z,2);    //pow(complex,int)
complex z4=pow(z,z);    //pow(complex,complex)
}

如果选择过程牵涉到两个或者更多的参数,那么将根据7.4节的规则为每个参数找到最佳匹配。如果有一个函数在某个参数上具有最佳的匹配,而在其他参数的匹配上都优于或者等于其他可能被调用的函数,那么它就会被调用。如果没有这样的函数,这个调用将被看做有歧义而予以拒绝。例如:
void f()
{
double d=pow(2.0,2);    //错误,pow(int(2.0),2)或者pow(2.0,double(2))
}
这个调用具有歧义性,因为对2.0的最佳匹配是pow(double,double),而对2的最佳匹配却是pow(int,int)。


*********************
7.5 默认参数
*********************

一个通用函数所需要的参数常常比处理简单情况时所需要的参数更多一些。特别地,那些为对象构造所用的函数通常都为灵活性而提供了一些选项。考虑一个打印整数的函数,给用户一个关于以什么为基数去打印它的选项是很合理的,但是在大部分程序里,整数都会按十进制的形式打印。例如:
void print(int value,int base=10);  //默认基数为10,即十进制
void f()
{
print(31);     //以默认十进制打印
print(31,10);     //指定十进制打印
print(31,16);     //指定十六进制打印
print(31,2);     //指定二进制打印
}
可能产生
31 31 1f 11111

也可以通过重载得到这种默认参数的效果:
void print(int value,int base);
inline void print(int value){print(value,10);} //默认打印十进制
当然,采用重载使读者不容易看到原来的意图:用一个函数加上一种函数简写形式。

默认参数的类型将在函数声明时检查,在调用时求值。只可能对排列在最后的那些参数提供默认参数,例如:
int f(int,int =0,char* =0);   //ok
int g(int =0,int =0,char*);   //错误
int g(int =0,int,char* =0);   //错误
请注意,在*和=之间的空格是重要的,否则就变成了*=运算符。

在同一个作用域中随后的声明里,默认参数都不能重复或者改变。例如:
void f(int x=7);
void f(int =7);     //错误,默认参数不能重复
void f(int =8);     //错误,默认参数不能改变

void g()
{
void f(int x=9);    //ok,不同作用域,可屏蔽外层的声明
}
在嵌套作用域里声明一个名字,让它去屏蔽在外层作用域里同一名字的声明,这种做法很容易出错。

*********************
7.6 未确定数目的参数
*********************
对于有些函数而言,我们没办法确定在各个调用中所期望的所有参数的个数和类型。声明这种函数的方式就是在参数表的最后用省略号(...)结束,省略号表示"还可能有另外一些参数"。例如:
int printf(const char* ...);
妈的,难懂,略过先。。。。

*********************
7.7 指向函数的指针
*********************
对于一个函数只能做两件事:调用它,或者取得它的地址。通过取一个函数的地址而得到的指针,可以在后面用于调用这个函数。例如:
void error(string s){}
void (*efct)(string);    //指向函数的指针
void f()
{
efct=&error;     //将error函数地址送给efct函数指针
efct("error!");     //通过efct调用error函数
}

编译器知道efct是一个指针,并会去调用被指的函数,这也就是说,可以不写从指针得到函数的间接运算*。与此类似,取得函数地址的&也可以不写:
void (*f1)(string)=&error;   //ok
void (*f2)(string)=error;   //ok,也可以,与&error的意思一样
void g()
{
f1("afjlsfd");     //ok
(*f1)("sdfsfsdf");    //ok
}
也就是说,取得指针指向的对象本来用*,但如果是函数指针,想取所指函数可用*,也可省去。

在指向函数的指针的声明中也需要给出参数类型,就像函数声明一样。在指针赋值时,完事的函数类型必须完全匹配。例如:
void (*pf)(string);    //指向void(string)
void f1(string);    //void(string)
int f2(string);     //int(string)
void f3(int*);     //void(int*)

void f()
{
pf=&f1;      //ok
pf=&f2;      //错误,void(string)不能指向int(string)
pf=&f3;      //错误,void(string)不能指向void(int*)
pf("sdfsdf");     //ok
pf(1);      //错,参数类型不对,需要是string,而不是int
int i=pf("sdfsdf");    //错,pf为void,无返回值
}

人们常常为了方便而为指向函数的指针类型定义一个名字,这样可以避免到处去写意义不太明显的语法形式。可以将一个函数指针定义为一个类型:
typedef void (*sig_typ)(int);
typedef void (*sig_arg_typ)(int);
sig_typ signal(int,sig_arg_typ);

指向函数的指针的数组非常有用,下面是一组关于菜单采用函数指针数组的例子:
typedef void(*PF)();
PF edit_ops={&cut,&pause,&copy,&search};
PF file_ops={&open,&append,&close,&write};
然后我们就可以定义并初始化一些指针,由它们去控制各种操作,通过关联于鼠标键的菜单去选择那些操作:
PF* button2=edit_ops;
PF* button3=file_ops;
button2[2]();     //调用copy函数
button3[1]();     //调用append函数

对于指向重载函数的指针应与重载函数的参数类型匹配:
void f(int);
void f(char);
void (*f1)(int)=&f;    //void f(int)
void (*f2)(char)=&f;    //void f(char)
int (*f3)(int)=&f;    //错,无int f(int);

7.8 宏
c++里建议inline函数代替宏,尽量少用宏。。。。为了时间,暂略过吧。。有需要再来回顾。

*********************
7.9 忠告
*********************
[1]质疑那些非const的引用参数;如果你想要一个函数去修改其参数,请使用指针或者返回值。
[2]当你需要尽可能减少参数复制时,应该使用const引用参数。
[3]广泛一致的使用const。
[4]避免宏。
[5]避免不确定数目的参数。
[6]不要返回局部变量的指针或者引用。
[7]当一些函数对不同类型的对象执行相同概念的操作时请使用重载。
[8]在各种整数上重载时,请提供函数去消除常见的歧义性。
[9]在考虑使用指向函数的指针时,请考虑虚函数和模板是不是最好的选择。
[10]如果你必须使用宏,请使用带有大写字母的名字。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xchenbb

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值