2.2 程序设计范式
编程范式:指的是计算机编程的基本风格或典范模式。设计C++就是为了支持数据抽象、面向对象的程序设计和通用型程序设计。
2.3 过程式程序设计
原始的程序设计范式是:
Decide which procedures you want; use the best algorithms you can find. |
2.3.1 变量和算术
一个声明是一个语句,它为程序引入一个名字,还为这个名字确定了一个类型。类型则定义了名字或者表达式的正确使用方式。
C++提供了一批各种各样的基本类型,它们都直接对应于一些硬件功能。例如:bool、char、int、double。算术运算可以用于这些类型的任意组合:+、-、*、/、%; 比较运算符也是这样:==、!=、<、>、<=、>=。
2.3.2 检测和循环(PSword使用:如果从vs贴过来的数据,得用行距使用固定值)
一个检测的小程序,检测了一个yes的情况
bool accept()
{
cout<<"Do you want to proceed (y or n)? "<<endl;
char answer = 0;
cin>>answer;
if (answer == 'y')
return true;
return false;
}
如果将回答‘n’也纳入考虑的范围,可以对这个例子做一点改进:
bool accept2()
{
cout<<"Do you want to proceed (y or n)? "<<endl;
char answer;
cin>>answer;
switch(answer)
{
case 'y':
return true;
case 'n':
return false;
default:
cout<<"I `ll take that for a no"<<endl;
return false;
}
}
加入endl会立即显示,cout<<"Do you want to proceed (y or n)? ";,这样的输入不会立即输出到屏幕上, 会写入缓冲区中。除了具备“ ”的换行功能外,还调用输出流的flush函数,刷新缓冲区,让数据直接文件或屏幕上。这两种都可以用的,不过不需要立即显示,并且要兼顾代码的执行效率的时候,可以考虑“ ”,这样不刷新缓冲区,会更快。
2.3.3 指针和数组
数组可以如下定义:
char v[10];
指针的定义与此类似:
char *p;
在声明里,[]表示”的数组”,而*表示”指向”。
C++语言强制要指向const对象的指针也必须具有const特性。本质上来说,系统是没有办法分辨常量指针所指向的对象是否为const,系统会把它所指的所有对象都是为const。如果指向const的指针所指的对象并非const,则可直接给该对象赋值或间接地利用普通的非const指针来修改:毕竟这个值不是const。
const double *cptr;
如果把指向const的指针理解为“自以为指向const的指针”。这个可能会对理解有所帮助。
Const指针
int errNumb = 0;
int *const curErr = &errNumb;//error
const指针的值不能修改, 这就意味着不能使curErr指向其他对象。
2.4 模块程序设计
设计程序的重点已经从有关过程的设计转移到数据的组织了。除了其他的因素外,这种转移也反映了程序规模增大的情况。
C++提供了一种机制,可以把相关的数据、函数等组织到一个独立的名字空间里。例如,模块Stack的接口可以按如下的方式声明和使用:
namespace Stack //interface
{
void push(char);
char pop();
}
void f()
{
Stack::push('c');
if (Stack::pop() != 'c') error("impossible");
}
这里的Stack::限定词表明push()和pop()都是Stack名字空间。“::”域操作符。A::B表示A范围内的B。
namespace Stack //implement
{
const int max_size = 200;
char v[max_size];
int top = 0;
void push(char c){/*检查上溢°并压入c*/}
char pop(){/*/检查下溢出并弹出*/}
}
有关这个Stack 模块的关键点是, 用户代码完全隔离于Stack的数据表示之外,隔离的方式是通过写出的Stack::push() 和Stack::pop()代码来实现。用户不用知道Stack是数组实现的。
2.4.1 分别编译
C++支持C语言中有关分别编译的概念。这种机制可以将程序组织为一组部分独立的片段。将接口放在.h 文件内、而将实现放在.cpp内。
第三章标准库预览
要是即刻就忘,何必费时去学?
1.1 引言
没有任何一个重要程序只是用某种赤裸裸的程序设计语言写出的。首先总是要开发出一组支撑库,这也就形成了进一步工作的基础。
3.2 Hello World!
最小的C++程序
int main { }
main函数返回的int值, 如果有的话就是这个程序返回给“系统”的值。如果没有值返回,系统将接到一个表示程序成功完成的值。也就是说main函数末尾的return 0可有可无。 由反斜线字符后跟一个字符表示的是某个特殊字符。
#include <iostream>//input/output stream
int main()
{
std::cout<<"Hello world "<<endl;
}
3.3 标准库名字空间
标准库定义在一个称为std的名字空间里,这也就是为什么写的是std::cout,而不直接写cout的原因。这样做的目的是明确说出要使用的是标准库里的cout。
标准库的每个功能都是通过某个像<iostream>这样的头文件提供。例如
#include <iostream>
#include <list>
iostream.h是C语言格式的标准输入输出流文件,为非标准。由于当时没有命名空间这个说法,所以也就不存在std这个命名空间标示符。所以用iostream.h也就用不着std或者using namespace std。(参见范磊 书籍)
iostream为标准输入输出流,它是C++规范的带有命名空间的头文件,它包含在std命名空间内。
3.5 字符串
标准库提供了一个string类型,作为前面所用的字符串文字量的补充,常用的操作有+, substr, replace,c_str。
3.6 输入
标准库提供了istream。与ostream一样,istream能处理内部数据类型的字符串序列。运算符>>(“取出”)被用于输入运算符。>> 右边的运算对象的类型决定可以接受什么输入。
获得输入的方式有两种:
int main()
{
string name;
cout<<"请输入您的姓名:"<<endl;
cin>>name;
cout<<"hello, "<<name<<endl;
return 0;
}
按照默认方式,一个空白字符,例如空格符,将结束一次输入。如果你输入的是
Eric Boolaxe
回答仍然是
Hello , Eric
但是你可以使用函数getline读入一个完整的行。例如:
int main()
{
string name;
cout<<"请输入您的姓名::"<<endl;
getline(cin, name);
cout<<"hello, "<<name<< endl;
fflush(stdin);
getchar();
return 0;
}
3.7 容器
一个可以保存一批对象为主要用途的类通常被称为一个容器。C语言内部数组具有固定的规模。如果我们选择了很大的规模,那么就会浪费存储空间;而如果选择了过小的规模,数组又会溢出。这两种情况不得不去写低级的存储管理代码。标准库提供的vector能关照好所有这些情况。
vector<int> a1(10); //10元素的向量
4.3 字符类型
每个字符常量都有一个整数值,例如在ASCII字符集里,‘b’的值是98。
int main()
{
char c;
cout<<"请输入一个字符:"<<endl;
cin>>c;
cout<<"the value of "<<c<<" is"<<int(c)<<endl;
system("pause");
return 0;
}
int(c)将给出字符c的整数值。
还提供了另一个类型wchar_t,用于保存更大的字符集的字符。例如Unicode的字符。在C语言里wchar_t是一个typedef而不是一个内部类型。加上后缀_t,就是为了区分标准类型和typedef。
例如你在使用ASCII字符集的机器上运行程序,‘0’的值就是48。采用字符常量而不用十进制写法能使程序更具有移植性。宽字符常量的形式是L’ab’(这里放在引号间的字符个数及意义由显示根据wchar_t类型确定)。
4.4 整数类型
与char一样,每个整数类型也有三种形式,普通的“int”,signed int ,和unsigned int 。此外,整数还有三种大小,short int(short)、 int、long int(long)。
4.5 浮点类型
浮点数也有三种大小:float、double和long double。
int main()
{
cout<<"largest float == "<<numeric_limits<float>::max()
<<", char is signed == "<<numeric_limits<char>::is_signed<<' ';
system("pause");
return 0;
}
注: 如果不能编译, 则需要加入头文件#include <limits>
4.8 枚举
一个枚举是一个类型,它可以保存一组由用户刻画的值。枚举的定义:enum其后是一个可选枚举类型名, 和一个用花括号括起来,用逗号分开的枚举成员。
enum open_modes{input, output, append};
默认下,第一个枚举子被赋值0,接下来的枚举子取值是前面一个枚举子的取值+1,例如
enum weather {SUNNY, CLOUDY, RAINY, WINDY};
其中
sunny == 0,
cloudy == 1,
rainy == 2,
windy == 3;
4.8.1枚举变量的定义、初始化和赋值
既然每个枚举都是一个类型,那么由这个类型自然可以声明变量,枚举类型变量的大小是4。例如,由前面定义的some_big_cities:
enum some_big_cities {
Guangzhou = 4,
Shenzhen = 4,
Hongkong = 4,
Shanghai = 2,
Beijing = 3,
Chongqi = 5
};
int main()
{
some_big_cities wh = Guangzhou; //枚举变量定义的时候最好初始化
cout<<"the value is: "<<wh<<endl; // 程序的输入结果是4
system("pause");
return 0;
}
4.9.1 声明的结构
一个声明由4个部分组成:一个可选的“描述符”,一个基础类型,一个声明符,还有一个可选的初始式。*,*const,&,[],()。后缀([],())的声明运算符比前缀的那些声明符约束力更强。因此,*kings[]就是一个指向什么东西的指针数组
4.9.4 作用域
一个声明将一个名字引进一个作用域;也就是说,这个名字只能在程序的一个特定部分内使用。被遮蔽的全局名字可以通过作用域解析运算符::去引用。
int x;
void f2()
{
int x = 1; //遮蔽了全局的x
::x = 2; //给全局的x赋值
}
4.9.5 初始化
如果没有提供初始式,全局的、名字空间和局部静态对象(统称为静态对象)将自动初始化为适当的0。
更复杂的对象需要多于一个值做为初始式。数组和结构的C风格初始化采用的是{ } 初始式列表描述。
int a[] = {1,2};
4.9.7 typedef
如果一个声明以typedef为前缀,它就为类型声明了一个新名字,而不是声明一个指定类型的对象。例如:typedef char *PCHAR;
这样一个可以方便地将原来笨拙的类型名缩写。还有使用typedef的作用就是易移植。例如:
typedef int int32;
typedef short int16;
那么很容易将我们的程序一直到一个sizeof(int)是2的机器上。
第五章 指针、数组和结构
5.1.1 零
没有任何对象会被分配到地址0,因此,0也被当做一个指针常量,表明指针并没有指向任何对象。在C中流行的是用一个宏NULL表示0指针。由于C++收紧的类型检查规则,采用普通的0而不是一些人建议的NULL宏。带来的问题会更少一些。如果感到必须定义NULL,请采用:
const int NULL = 0;
5.2 数组
float v[3];
char * a[32];
5.3 数组的初始化
数组可以用一系列值初始化。如:
int vl [] = {1, 2, 3, 4};
如果明确给出了大小,在初始化列表中给了多余的元素就是错误。
char v3[2] = {1, 2, 3};
5.2.2 字符串常量
“this is a string”,一个字符串常量里包含的字符个数比它看起来的字符多一个。它总是由一个空字符’’结束。空字符的值是0。字符串常量的类型是:“适当个数的const字符的数组”,所以像”Bohr”的类型就是const char [5]。
cout<<sizeof("Bohr");
char *a = "chen";
a[2] = 'e'; //error:assignment to const;
如果希望一个字符串保证能够被修改,那么就必须将有关的字符复制到数组里:
char a[] = "dour";
a[2] = 'r';
cout<<a<<endl;
两个相同的字符串常量是否分配在一起,这一点由实现确定,例如
const char *p = "Heraclitus";
const char *q = "Heraclitus";
void g()
{
if (p == q)
cout<<"one! ";
...
}
在vs中却是是同一个地址。为表示非打印字符提供的反斜线约定也可以放在字符串里。最常见的就是换行符’ ’。
cout<<"aa"<<endl;
5.3 指向数组的指针
char v[] = "chian";
char *p = v;//隐式地将char[] 转换到char *
指针可以减去一个指针,结果就是这两个指针间的元素个数。一个指针加上一个整数或者减去一个整数,得到的结果还是一个指针值。但是指针相加没有任何意义,因此是不允许的。
5.4 常量
关键词const可以加到一个对象的声明上,将这个对象声明为一个常量。因为不允许赋值,所以常量必须初始化。
const限制了常量的使用方式,并没有描述常量应该如何分配。如果编译器知道了某const的所有使用,它甚至可以不为该const分配空间(???不分配空间,该怎么理解呢)。最简单的常见情况就是常量的值在编译时已知,而且不需要分配存储。从右向左读的记忆方式
char const *cp ; //cp is a const pointer to char
char s[] = "Gorm";
const char *pc = s;//pc是一个指针,指向的是const char;指向常量
//p[3] = 's'; //error
char *const cp = s; //cp是一个常量, 类型是char *;常量指针
cp[3] = 'c';
//cp = p;//error
5.5 引用
一个引用就是某对象的另一个名字,引用的主要用途是为了描述函数的参数和返回值(C++ primer 这个地方讲解的比较好),特别是为了运算符的重载。为了确保一个引用总能是某个东西的名字,我们必须对饮用做初始化。
对const T&的初始式不必是一个左值,甚至可以不是T类型的
double& dr = 1; //error
const double &dr = 1; //ok
对后一个初始化的解释是:
double temp = double (1);
const double &cdr = temp;
利用const引用避免复制,在向函数传递大型对象时,需要引用参数,这是引用参数使用的一种情况。
void swap (int &v1, int &v2)
{
int tmp = v1;
v1 = v2;
v2 = tmp;
}
5.6 指向void的指针
void*的最重要用途是需要向函数传递一个指针,而又不能对对象的类型做任何假设。还有就是从函数返回一个无类型的对象。要使用这样的对象必须通过显式的类型转换。采用void*的函数通常存在于系统中很低的层次里,在哪里需要操作某些真实的硬件资源。在系统的较高层次上出现应该void*是可疑的,它们就像是设计错误的指示器。
5.7 结构
数组是相同类型的元素的一个聚集。一个struct则是任意类型元素的聚集。
struct address {
char *name;
long int number;
char *street;
char *town;
char state[2];
long zip;
};
这是C++里很少有的几处在{}之后还需要分号的地方,因此人们容易忘记它。
address jd = {
"Jim Dandy",
61,"South st",
"New Providence",
{'N','J'},7974
};
注意,jd.state不能用字符串”NJ”去初始化。字符串总是以字符’’结束,这样”NJ”实际上就有三个字符。
忠告:
1、避免非平凡的指针算术;
2、当心,不要超出数组的界限去读写;
3、尽量使用0而不是NULL;
4、尽量使用vector和valarray而不是内部的数组;
5、尽量使用string而不是以0结尾的char数组;
6、尽量少用普通的引用参数;如果用了引用,那么尽量加上const;
7、避免void*,除了在某些低级代码里。
8、避免在代码中使用非平凡的文字量,相反,应该定义和使用各种符号常量。
以后要将下面的笔记加到适当的地方:
但是在C++中,const定义的常量要具体情况具体对待:对于基本数据类型的常量,编译器会把它放到符号表中而不分配存储空间。
第七章 函数
7.1 内联 (一)inline函数(摘自C++ Primer的第三版)
在函数声明或定义中函数返回类型前加上关键字inline即把min()指定为内联。
inline int min(int first, int secend) {/****/};
inline 函数对编译器而言必须是可见的,以便它能够在调用点内展开该函数。与非inline函数不同的是,inline函数必须在调用该函数的每个文本文件中定义。当然,对于同一程序的不同文件,如果inline函数出现的话,其定义必须相同。对于由两个文件compute.C和draw.C构成的程序来说,程序员不能定义这样的min()函数,它在compute.C中指一件事情,而在draw.C中指另外一件事情。如果两个定义不相同,程序将会有未定义的行为:
为保证不会发生这样的事情,建议把inline函数的定义放到头文件中。在每个调用该inline函数的文件中包含该头文件。这种方法保证对每个inline函数只有一个定义,且程序员无需复制代码,并且不可能在程序的生命期中引起无意的不匹配的事情。
(二)内联函数的编程风格(摘自高质量C++/C 编程指南)
关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。
如下风格的函数Foo 不能成为内联函数:
inline void Foo(int x, int y); // inline 仅与函数声明放在一起
void Foo(int x, int y){}
而如下风格的函数Foo 则成为内联函数:
void Foo(int x, int y);
inline void Foo(int x, int y) // inline 与函数定义体放在一起{}
所以说,inline 是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。一般地,用户可以阅读函数的声明,但是看不到函数的定义。尽管在大多数教科书中内联函数的声明、定义体前面都加了inline 关键字,但我认为inline 不应该出现在函数的声明中。这个细节虽然不会影响函数的功能,但是体现了高质量C++/C 程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。
定义在类声明之中的成员函数将自动地成为内联函数
例如
class A
{
public:void Foo(int x, int y) { } // 自动地成为内联函数
}
将成员函数的定义体放在类声明之中虽然能带来书写上的方便,但不是一种良好的编程风格,上例应该改成:
// 头文件
class A
{
public:
void Foo(int x, int y);
}
// 定义文件
inline void A::Foo(int x, int y){}
慎用内联
内联能提高函数的执行效率,为什么不把所有的函数都定义成内联函数?如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了 inline 不应该出现在函数的声明中)。
注意点:
内联函数既能够去除函数调用所带来的效率负担又能够保留一般函数的优点。然而,内联函数并不是万能药,在一些情况下,它甚至能够降低程序的性能。因此在使用的时候应该慎重。
1.我们先来看看内联函数给我们带来的好处:从一个用户的角度来看,内联函数看起来和普通函数一样, 它可以有参数和返回值,也可以有自己的作用域,然而它却不会引入一般函数调用所带来的负担。另外, 它可以比宏更安全更容易调试。
当然有一点应该意识到,inline specifier仅仅是对编译器的建议,编译器有权利忽略这个建议。那么编译器是如何决定函数内联与否呢?一般情况下关键性因素包括函数体的大小,是否有局部对象被声明,函数的复杂性等等。
2.那么如果一个函数被声明为inline但是却没有被内联将会发生什么呢?理论上,当编译器拒绝内联一个 函数的时候,那个函数会像普通函数一样被对待,但是还会出现一些其他的问题。例如下面这段代码:
// filename Time.h
#include<ctime>
#include<iostream>
using namespace std;
class Time
{
public:
inline void Show()
{
for (int i = 0; i<10; i++)
cout<<time(0)<<endl;
}
};
因为成员函数Time::Show()包括一个局部变量和一个for循环,所以编译器一般拒绝inline,并且把它当作一个普通的成员函数。但是这个包含类声明的头文件会被单独的#include进各个独立的编译单元中:
// filename f1.cpp
#include "Time.h"
void f1()
{
Time t1;
t1.Show();
}
// filename f2.cpp
#include "Time.h"
void f2()
{
Time t2;
t2.Show();
}
结果编译器为这个程序生成了两个相同成员函数的拷贝:
void f1();
void f2();
int main()
{
f1();
f2();
return 0;
}
当程序被链接的时候,linker将会面对两个相同的Time::Show()拷贝,于是函数重定义的连接错误发生。但是老一些的C++实现对付这种情况的办法是通过把一个un-inlined函数当作static来处理。因此每一份函数拷贝仅仅在自己的编译单元中可见,这样链接错误就解决了,但是在程序中却会留下多份函数拷贝。在这种情况下,程序的性能不但没有提升,反而增加了编译和链接时间以及最终可执行体的大小。但是幸运的是,新的C++标准中关于un-inlined函数的说法已经改变。一个符合标准C++实现应该只生成一份函数拷贝。然而,要想所有的编译器都支持这一点可能还需要很长时间。
另外关于内联函数还有两个更令人头疼的问题。第一个问题是该如何进行维护。一个函数开始的时候可能以内联的形式出现,但是随着系统的扩展,函数体可能要求添加额外的功能,结果内联函数就变得不太可能,因此需要把inline specifier去除以及把函数体放到一个单独的源文件中。另一个问题是当内联函数被应用在代码库的时候产生。当内联函数改变的时候,用户必须重新编译他们的代码以反映这种改变。然而对于一个非内联函数,用户仅仅需要重新链接就可以了。
这里想要说的是,内联函数并不是一个增强性能的灵丹妙药。只有当函数非常短小的时候它才能得到我们想要的效果,但是如果函数并不是很短而且在很多地方都被调用的话,那么将会使得可执行体的体积增大。最令人烦恼的还是当编译器拒绝内联的时候。在老的实现中,结果很不尽人意,虽然在新的实现中有很大的改善,但是仍然还是不那么完善的。一些编译器能够足够的聪明来指出哪些函数可以内联哪些不能,但是,大多数编译器就不那么聪明了,因此这就需要我们的经验来判断。如果内联函数不能增强行能,就避免使用它!
如果以局部变量被声明为static, 那么将只有唯一的一个静态分配的对象,这个对象将只在执行线程第一次到达它的定义时初始化。
void f(int a)
{
while (a--)
{
static int n = 0;
int x = 0;
cout<<"n =="<<n++<<",x =="<<x++<<' ';
}
}
f(3);
n ==0,x ==0
n ==1,x ==0
n ==2,x ==0
静态局部变量为函数提供了一种“存储器”, 使我们不必去引用可能被其他函数访问或破坏的全局变量。
7.2 参数传递
在一些情况下,可以将有关的参数声明为const,可以使用这种参数仅仅是为了效率的原因,而不是想让调用函数能够修改对象的值:
void f(const Large& arg)
{
}
如果一个引用参数的声明中没有const,就应该认为,该参数将被修改。
7.4 重载函数名
函数重载是指在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处。基本想法就是去调用其中那个在参数上匹配得最好的函数。
void print(int i )
{
cout<<"print a integer:"<<i<<endl;
}
void print(string str )
{
cout<<"print a string:"<<str<<endl;
}
7.4.1为什么需要函数重载(why)?
l 试想如果没有函数重载机制,如在C中,你必须要这样去做:为这个print函数取不同的名字,如print_int、print_string。这里还只是两个的情况,如果是很多个的话,就需要为实现同一个功能的函数取很多个名字,如加入打印long型、char*、各种类型的数组等等。这样做很不友好!
l 类的构造函数跟类名相同,也就是说:构造函数都同名。如果没有函数重载机制,要想实例化不同的对象,那是相当的麻烦!
l 操作符重载,本质上就是函数重载,它大大丰富了已有操作符的含义,方便使用,如+可用于连接字符串等!
我们可以发现编译之后,重载函数的名字变了不再都是print!这样不存在命名冲突的问题了,但又有新的问题了——变名机制是怎样的,即如何将一个重载函数的签名映射到一个新的标识?我的第一反应是:函数名+参数列表,因为函数重载取决于参数的类型、个数,而跟返回类型无关。但看下面的映射关系:
void print(int i) --> _Z5printi
void print(string str) --> _Z5printSs
进一步猜想,前面的Z5表示返回值类型,print函数名,i表示整型int,Ss表示字符串string,即映射为返回类型+函数名+参数列表。最后在main函数中就是通过_Z5printi、_Z5printSs来调用对应的函数的:
7.4.3 重载函数的调用匹配
现在已经解决了重载函数命名冲突的问题,在定义完重载函数之后,用函数名调用的时候是如何去解析的?为了估计哪个重载函数最适合,需要依次按照下列规则来判断:
- 精确匹配:参数匹配而不做转换,或者只是做微不足道的转换,如数组名到指针、函数名到指向函数的指针、T到const T;
- 提升匹配:即整数提升(如bool 到 int、char到int、short 到int),float到double
- 使用标准转换匹配:如int 到double、double到int、double到long double、Derived*到Base*、T*到void*、int到unsigned int;
- 使用用户自定义匹配;
- 使用省略号匹配:类似printf中省略号参数
7.4.4 多参数解析
如果有一个函数牵涉到两个或者更多的参数,那么将根据上面的规则为每个参数找到最佳匹配。如果有一个函数在某个参数具有最佳匹配,而在其他参数的匹配上都优于或者其他可能配调用的函数, 那么他就会被调用。
7.5 默认参数
一个通用的函数需要的参数通常比处理简单情况是所需要的参数更多一些。默认参数的类型将在函数声明时检查,在调用时求值。一般默认参数只能排列在最右边。例如:
int f(int, int=0);
7.6 未确定数目的参数
由于在C语言中没有函数重载,解决不定数目函数参数问题变得比较麻烦,即使采用C++,如果参数个数不能确定,也很难采用函数重载。对这种情况,提出了指针参数来解决问题。
如printf()函数,其原型为:
int printf(const char* format, ...);
它除了有一个参数format固定以外,后面跟的参数的个数和类型是可变的,例如我们可以有以下不同的调用方法:
printf( "%d ",i);
printf( "%s ",s);
printf( "the number is %d,string is:%s ", i, s);
如何实现其功能?
我们需要以下几个宏定义:
(1)va_list
定义了一个指针arg_ptr, 用于指示可选的参数.
(2)va_start(arg_ptr, argN)
使参数列表指针arg_ptr指向函数参数列表中的第一个可选参数,argN是位于第一个可选参数之前的固定参数, 或者说最后一个固定参数.如有一va函数的声明是void va_test(char a, char b, char c, ...), 则它的固定参数依次是a,b,c, 最后一个固定参数argN为c, 因此就是va_start(arg_ptr, c).
(3)va_arg(arg_ptr, type)
返回参数列表中指针arg_ptr所指的参数, 返回类型为type. 并使指针arg_ptr指向参数列表中下一个参数.返回的是可选参数, 不包括固定参数.
(4)va_end(arg_ptr)
清空参数列表, 并置参数指针arg_ptr无效.
(注:va在这里是variable-argument(可变参数)的意思. 这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件)
也需你现在还是不能理解,别着急,现在从一个实例着手.定义这么一个函数,函数的第一个参数是固定的,其余参数是可变的。定义为:
void simple_va_fun(int i,...); 其代码为:
1 #include <iostream>
2 #include <stdarg.h>
3 using namespace std;
4 void simple_va_fun(int i,...);
5
6 int main(int argc,char *argv[])
7 {
8 simple_va_fun(100);
9 simple_va_fun(100,200);
10 simple_va_fun(100,200,'a');
11 return 0;
12 }
13
14 void simple_va_fun(int i,...)
15 {
16 va_list arg_ptr; //定义可变参数指针
17 va_start(arg_ptr,i); // i为最后一个固定参数
18 int j=va_arg(arg_ptr,int); //返回第一个可变参数,类型为int
19 char c=va_arg(arg_ptr,char); //返回第二个可变参数,类型为char
20 va_end(arg_ptr); // 清空参数指针
21 printf( "%d %d %c
",i,j,c);
22 return;
23 }
代码运行解释:
(1)首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变量是指向参数的指针.
(2)然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数.
(3)然后用va_arg返回第一个可变的参数,并赋值给整数j。va_arg的第二个参数是你要返回的参数的类型,这里是int型. 返回第一个可变参数后arg_ptr指向第二个可变参数,用同样的方法返回并赋值给c,类型为char类型。
(4)最后用va_end宏结束可变参数的获取。
小结:
可变参数的函数原理其实很简单,而va系列是以宏定义来定义的,实 现跟堆栈相关.我们写一个可变函数的C函数时,有利也有弊,所以在不必要的场合,我们无需用到可变参数.如果在C++里,我们应该利用C++的多态性来实现可变参数的功能,尽量避免用C语言的方式来实现。
附加:
参数在堆栈中分布:
在进程中,堆栈地址是从高到低分配的.当执行一个函数的时候,将参数列表入栈,压入堆栈的高地址部分,然后入栈函数的返回地址,接着入栈函数的执行代码,这个入栈过程,堆栈地址不断递减,一些黑客就是在堆栈中修改函数返回地址,执行自己的代码来达到执行自己插入的代码段的目的. 总之,函数在堆栈中的分布情况是:地址从高到低,依次是:函数参数列表,函数返回地址,函数执行代码段. 堆栈中,各个函数的分布情况是倒序的.即最后一个参数在列表中地址最高部分,第一个参数在列表地址的最低部分.参数在堆栈中的分布情况如下:
最后一个参数
倒数第二个参数
...
第一个参数
函数返回地址
函数代码段
7.7 指向函数的指针
对于一个函数只能做两件事:调用它,或者取得它的地址。
void error(string s){/* ....*/}
void (*efct)(string s);
void f()
{
efct = &error;
efct("error");
*efct("error");
}
通常讲,typedef要比#define要好,特别是在有指针的场合。请看例子:
typedef char *pStr1; 我的理解是这种相当于是起别名。
#define pStr2 char *; 而这一种相当于是宏的拓展,例如以后如果遇见pStr2就用后面的那种方式扩展开来。
用途三:
用typedef来定义与平台无关的类型。
比如定义一个叫 REAL 的浮点类型,在目标平台一上,让它表示最高精度的类型为:
typedef long double REAL;
在不支持 long double 的平台二上,改为:
typedef double REAL;
在连 double 都不支持的平台三上,改为:
typedef float REAL;
也就是说,当跨平台时,只要改下 typedef 本身就行,不用对其他源码做任何修改。
标准库就广泛使用了这个技巧,比如size_t。另外,因为typedef是定义了一种类型的新别名,不是简单的字符串替换,所以它比宏来得稳健。
这个优点在我们写代码的过程中可以减少不少代码量哦!
用途四:
为复杂的声明定义一个新的简单的别名。方法是:在原来的声明里逐步用别名替换一部
分复杂声明,如此循环,把带变量名的部分留到最后替换,得到的就是原声明的最简化
版。举例:
原声明:void (*b[10]) (void (*)());
变量名为b,先替换右边部分括号里的,pFunParam为别名一:
typedef void (*pFunParam)();
再替换左边的变量b,pFunx为别名二:
typedef void (*pFunx)(pFunParam);
原声明的最简化版:
pFunx b[10];
typedef void (*PFT) ( char ,int );
void bar(char ch, int i)
{
cout<<"bar "<<ch<<' '<<i<<endl;
return ;
}
PFT pft;
pft = bar;
pft('e',91);
例子中函数指针pft指向了一个已经声明的函数bar(),然后通过pft来实现输出字符和整型的目的。
函数指针另一个作用便是作为函数的参数,我们可以在一个函数的形参列表中传入一个函数指针,然后便可以在这个函数中使用这个函数指针所指向的函数,这样便可以使程序变得更加清晰和简洁,而且这种用途技巧可以帮助我们解决很多棘手的问题,使用很小的代价就可获得足够大的利益(速度+复杂度)。