return0;
}
intcheck()
{
staticinti=1;
i++;
returni;
}
输出结果为:
2
3
4
5
6
在本程序运行中,除第一次staticinti=1;声明语句有效外,其他时刻都被忽略。
而auto型局部变量,其生命期只在该函数执行至其声明语句开始至函数一次调用结束。
B、使用static声明的模块层变量能被模块内所有函数访问。
C、使用static声明的模块层变量不能被模块外其他函数访问。
(主要应用于文件内部变量和内部函数的声明,用以防止其他文件通过头文件引用和外部声明方式来使用它)
6、存储区之堆和栈(摘录)
一般认为在c中分为这几个存储区:
1栈-有编译器自动分配释放
2堆-一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收
3全局区(静态区),全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束时释放
4一个专门放常量的地方,在程序结束时释放
在函数体中定义的变量通常是存储在栈上,用malloc,calloc,realloc等分配内存的函数分配得到的就是在堆上。
在所有函数体外定义的是全局量,加了static修饰符后不管在哪里都存放在全局区(静态区)。
另外,函数中的"adgfdf"这样的字符串存放在常量区。
比如:
inta=0;全局初始化区
char*p1;全局未初始化区
main()
{
intb;栈
chars[]="abc";栈
char*p2;栈
char*p3="123456";123456\0在常量区,p3在栈上。
staticintc=0;全局(静态)初始化区
p1=(char*)malloc(10);
p2=(char*)malloc(20);分配得的10和20字节的区域就在堆区。
strcpy(p1,"123456");123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一块。
}
还有就是函数调用时会在栈上有一系列的保留现场及传递参数的操作。
栈的空间大小有限定,vc的缺省是2M。
栈不够用的情况一般是程序中分配了大量数组和递归函数层次太深。
有一点必须知道,当一个函数调用完返回后它会释放该函数中所有的栈空间。
栈是由编译器自动管理的,不用你操心。
堆是动态分配内存的,并且你可以分配使用很大的内存。
但是用不好会产生内存泄漏,并且频繁地malloc和free会产生内存碎片(有点类似磁盘碎片),因为c分配动态内存时是寻找匹配的内存的。
而用栈则不会产生碎片。
在栈上存取数据比通过指针在堆上存取数据快些。
一般大家说的堆栈和栈是一样的,就是栈(stack),而说堆时才是堆heap。
栈是先入后出的,一般是由高地址向低地址生长。
2、函数简介
1、函数的声明与定义
对某种功能用若干语句进行实现所获得的抽象即为函数,如fabs()即为绝对值求值实现的抽象。
A、函数原型
函数原型声明一般包括类型说明、函数名、参数列表,如下形式:
(返回值类型)<函数名>(参数表);
如果函数仅为若干过程语句,不需返回值,可声明返回值类型为void型。
注:
在C语言中,自定义函数如未声明返回值类型,则默认其为int型。
C++则并须声明有返回值类型。
B、函数定义
函数定义格式:
(返回值类型)<函数名>(参数表)
{
函数体;//用于实现特定功能的语句块
}
注:
函数不可以嵌套定义。
2、函数参数
一般函数参数在声明时可允许仅说明参数类型,如:
Intf(int[],float);
但在函数定义时如果要在函数体中调用形参,必须为其添加变量名,如:
Intf(ints[],floati)
{
函数体;
}
函数的形参允许设置默认值,但需注意以下几点:
A、指定默认值时,要从参数表有右端开始,在制定了默认值的参数右边不允许有未指定默认值的形参出现,如:
Intf(inta,intm=0,intn);//该声明将使编译器报错
Intf(inta,intm=0,intn=2);//此声明正确
B、在函数调用时,给定的实参值将替换默认值,为给定实参值的形参将使用默认值。
C、如果一个函数需要说明,默认的参数值应设置在函数的说明中,而不是在函数的定义中。
当没有函数说明时,默认的参数值可设在函数的定义中。
D、再给形参设置默认值时,可以是数值,也可以是表达式。
对于表达式,默认值一般由全局变量组成,也可以是函数,但不可以是局部变量。
因为默认参数的函数调用是在编译时确定的,而局部变量在编译时无法确定。
3、动态分配的局部变量之异位引用
在变量类型中讲到过,函数形参属于动态存储类,是局部变量。
而对于函数体中所有局部变量,都是采取动态分配的形式,且自定义函数与主函数的内存分配空间相分离。
每次调用函数结束后,函数内部所有动态存储类变量名都将被销毁,但其原来所在的地址中的值是被保存下来的,这些内存此时已被释放(释放并不代表值被清空),供下一个自定义函数调用时分配使用。
由此,在使用多个指针函数(会在下文提到)时,会出现一个有趣的现象(我仅在此称它为异位引用,下文好叙述一些),借助这个例子可以更理解关于函数调用时内存分配的问题,接下来我们认识一下它的奇特之处。
先看一个代码:
#include
usingnamespacestd;
int*f(inta,intb)//a>b,返回1;否,返回0
{
intj=a>b;
return&j;
}
int*f1(floatc,floatd)//c>d,返回1;否,返回0
{
intk=c>d;
return&k;
}
main()
{
inta=10,b=1,*p,*p1;
floatc=1.1,d=3.1;
p1=f1(c,d);
p=f(a,b);
cout<<*p1<}
这段代码很简单,从实际应用来看,确实没多大价值,而且采用这种函数体内局部变量地址返回还会产生警告。
先不管这些不足,先分析一下,结果。
从运算上来看,输出值应该为0.相信没有前面的铺垫叙述,很多人对此都深信不疑,难道判断不对么?
不是的,判断是对的,在执行p1=f1(c,d);结束后,p1指向地址所存入的值确实应为0,但在p=f(a,b);之后,p1指向地址的值发生变化,被修改为1,即输出为1;如果在主函数末尾加上“cout<
由于函数f1()内部局部变量k在其调用结束后被销毁,其内存被系统在调用f()函数时重新分配给了j,才会发生这样的结果。
另外,注意到参数类型没有影响分配函数体中局部变量内存时对应的位置,是因为参数的内存分配位置与其内部变量略有不同,互不影响。
那是因为系统会先识别不同函数形参的类型和个数,并为其分配替换地址。
对于异位引用现象,在平常编程中不常遇到,只有在定义了多个结构相似的函数并做地址返回操作时,发生的可能性较大,这种错误一旦发生不易检查出来。
除了函数体中局部变量外,对参数列表中的局部变量进行地址返回操作也同样能导致异位引用的发生。
3、传值调用及引用调用
1、函数调用
一个构造完成的函数可供我们调用来实现特定功能。
函数的调用过程实际上是对栈空间的操作过程,因为调用函数是使用栈空间来保存信息的。
其调用过程大致描述如下:
A、建立被调用函数的栈空间;
B、保护调用函数的运行状态和返回地址;
C、传值函数实参值给形参;反过来说,我们通常认为不成立。
D、执行被调用函数的函数体内语句;
E、将控制权和返回值转交给调用函数。
函数将一分独立的代码封装,完成特定的功能,增加了源文件可读性,实践模块化编程的思想,但是也产生了调用函数带来的额外的时间开销和空间开销。
2、传值调用
传值调用又分为值传值和地址传值,用于不同的传值需要。
A、值传值
这种传值方式是将实参的数据值通过复制获得其副本并将副本存放在被调用函数的栈区中,通过这种方式将值传给形参。
B、地址传值
传值调用方式是将地址值直接传递给形参,即形参为指针,使之指向实参地址,这里便不需要复制实参副本,所有对形参指针指向地址的值操作都将反馈到形参中。
(数组形参类似,真正要修改的值(包括地址值)需在形参中设置高一级的指针来同步指向)
3、引用调用
引用调用是在函数形参列表中设置形参引用,该形参名即为对应实参的别名。
用这种传值方式,不仅省去实参副本赋值,还省去指针调用的麻烦,可直接进行地址调用。
(实际上,引用调用与地址传值相类似,都是地址传递,只是更为简便些)
用返回值方式,只能做到传递一个值,而引用调用和地址传值能返还多个值,地址调用通常用于数组、指针,而引用调用则可包含传值调用的作用范围,应用更为广泛。
4、Main函数与自定义函数
1、main函数
在C/C++的程序中,必定有唯以一个主函数,它具有一些特性:
A、主函数只能被系统调用
B、主函数具有形参,只能通过系统为其传值
主函数的形参类型已被固定,argc为命令行中参数个数,argv为指向字符指针数组的指针:
Main(intargc,char**argv)
Main(intargc,char*argv[])
以上两种形式皆合法。
另外,对于形参名是可以随用户自定义的,如:
Main(inta,char**c)
注:
这样声明也可以:
Main(inta,char*c[1])
C、一个源程序在编译时是从主函数开始编译的
D、C/C++主函数的默认返回值为int型。
2、自定义函数
自定义函数一般由用户自己编辑定义,用以实现某种特定功能。
其声明如上所述。
不同于主函数,自定义函数在一般情况下可供其他任意函数嵌套调用(包括主函数),也可自身调用自身,即递归。
自定义函数在被调用之前,必须在调用函数之前进行声明或直接定义。
5、指针函数与函数指针
1、指针函数
指针函数本质为函数,返回指针类型的值,其声明如下:
(指针指向值类型)*函数名(形参列表);
2、函数指针
函数指针本质为指针,该指针指向一个函数,其声明如下:
(指针指向函数返回值类型)(*指针名)(函数形参列表)
也可以在函数定义之后声明一个指针指向它,如:
Intf(int,int);
在主函数中作如下操作:
Int*p=f;
这样,指针p便指向函数f();
调用时便有两种形式:
Inta=p(1,2);或inta=f(1,2);
然而事实上,采用前者并不能提升效率,反而比后者多增了一个取值操作(先取得函数名的常量地址)。
然而既然存在则必有其原因。
3、函数指针的功能
有如下代码:
#include
usingnamespacestd;
int*f(inta,intb)
{
intj=a>b;
return&j;
}
int*f1(intc,intd)
{
intj=!
(c>d);
return&j;
}
typedefint*(*pn)(inta,intb);
pnff(ints)
{
switch(s)
{
case0:
returnf;
case1:
returnf1;
}
}
intmain()
{
inta=10,b=1,c,*p;
int*p1;
cin>>c;
p=ff(c)(a,b);
cout<<*p<return0;
}
这份代码由异位引用一讲的代码改编过来,实现的功能相同,不过代码中多插入了一段,主函数和函数f1()中也做了点修改。
函数f()在a>b时返回1,f1()则返回0;
当我们输入:
1
则输出:
0
此时调用了f1();
当我们输入:
0
则输出:
1
此时调用了f()。
我们通过函数指针将f()和f1()相关联,依据不同指令调用不同函数,在多个功能相关的返回值类型、参数类型及个数相同的函数应用中,可以使用函数指针实现动态联编的效果。
6、函数递归、函数重载和inline函数
1、inline函数
inline关键字用来定义一个类的内联函数,引入它的主要原因是用它替代C中表达式形式的宏定义。
用inline声明的函数也是真正的函数,与其他函数的区别在于编译时其代码直接内嵌到调用函数源文件代码中,从而在一定程度上减去了调用函数产生的额外时间和空间开销。
主要适用于代码较少,使用频繁而不涉及复杂运算和循环结构的函数。
所以一般inline函数代码少,不涉及复杂结构(如,循环语句,switch语句)。
主要是因为在编译时内联函数带有太多代码会产生累赘。
内联函数产生过多代码会被编译器视为普通函数,忽略其inline关键字
除此外,内联函数满足一般函数调用的一般性质,只要在被调用函数调用前检测到inline函数声明,则该函数就可被调用。
例子(略)
2、函数递归
递归的实现类似于迭代,是迭代的模块化呈现。
即将一个问题分解为相似的一个个具有嵌套层次的小问题,如:
#include
usingnamespacestd;
intFibonacc(inti)
{
if(i==0)
return0;
elseif(i==1)
return1;
else
returnFibonacc(i-1)+Fibonacc(i-2);
}
intmain()
{
intn;
while(cin>>n&&n>=0)
cout<return0;
}
这个代码是实现对Fibonacc数列的求值(数列公式:
f[0]=0,f[1]=1,f[n]=f[n-1]+f[n-2],n>=2),在n不算大时可以实现。
这段程序中的自定义函数采用了递归思想,函数即为递归函数,是对求值中的f[i]=f[i-1]+f[i-2]的单一实现(2<=i<=n),即将求值问题分成相关似的小问题求解.
注意到自定义函数中的条件语句,实现一个递归,至少需要三个条件:
A、递归起点
B、递归终结点
C、递归语句
只有满足这三个条件,递归才完整。
以上递归函数中的前两个判断条件即为递归终结点;第三个条件语句为递归语句,是利用调用自身实现内嵌式递归调用的,调用的函数和被调用的其他自身函数是分别独立的,构成类似塔形结构,由下至上延伸之终结点。
3、递归函数的形式转换
递归可以转换,用其他形式实现相同功能,以下用求两个整数的最大公约数的程序来说明。
A、递归求解
#include
usingnamespacestd;
intgcd(inta,intb)
{
if(a%b==0)
returnb;
returngcd(b,a%b);
}
intmain()
{
intm,n;
cin>>m>>n;
cout<return0;
}
B、栈解法
#include
#include
usingnamespacestd;
intgcd(inta,intb)
{
stackx;
x.push(a),x.push(b);
while
(1)
{
inty=x.top();
x.pop();
intz=x.top();
x.pop();
if(z%y==0)
returny;
x.push(y);
x.push(z%y);
}
}
intmain()
{
intm,n;
cin>>m>>n;
cout<return0;
}
C、迭代
#include
#include
usingnamespacestd;
intgcd(inta,intb)
{
for(inttemp;b;a=b,b=temp)
temp=a%b;
returna;
}
intmain()
{
intm,n;
cin>>m>>n;
cout<return0;
}
仅以上三种替换方式来说明递归思想,其原理相同,只是实现过程略有不同而已。
4、函数重载
函数重载是C++引入的用于实现多态性的编程机制,即对于同一问题可以有不同处理。
重载函数的一个重要特征在于他们有相同函数名,而在其他方面有所不同,主要体现在函数原型上,与相应的函数体无关。
函数重载必须满足条件:
A、函数名相同
B、形参列表有所不同(形参类型,形参个数,只要有一点不同即可),这是因为系统在确定调用函数时是通过类型匹配来判断的,如果没有完全精确地匹配对象,系统会通过系统内置的标准类型转换来匹配。
注:
返回值类型不同不能成为函数重载的条件。
以下用一个简单的求和例子说明:
#include
#include
usingnamespacestd;
intsum(inta,intb)
{
returna+b;
}
doublesum(doublea,doubleb)
{
returna+b;
}
intmain()
{
doublea=1.2,b=5.7;
intm=3,n=8;
cout<cout<return0;
}
输出结果:
6.9
11
使用sum()函数来实现两种类型的求和运算。
4、函数压轧
在不同的程序语言中,内部都制定了严格的命名机制,对于我们在源代码编辑时为自定义函数所起的函数名在系统中会加上附缀名,使其与其他文件里的函数名不相同。
于是,当我们用C++编译C需加externc修饰,以阻止函数压轧。
7、常见三种排序方式
1、选择排序
例:
找出第i个数及之后数的最小项(以下标访问数组的形式)并交换,对第i+1个数做同样操作,最后完成递增排序(0<=i<=n,n为数组维数)至多交换次数(n-1),比较次数1/2*n*(n-1).
函数代码如下:
VoidselectionSort(Datatypeea[n],inti)
{
IntsmallIndex;//下标访问
Inti,j;
For(i=0;i{