C函数高级
一、函数返回
函数的返回值
- 函数的返回值是指函数被调用后,执行函数体中的程序段所取得的并返回给主调函数的值。
- 函数的返回值只能通过return语句返回主函数。
- return语法:
- return 表达式;
- 或者return (表达式);
return语句的实现机制
- 执行return语句时,先计算return后面的表达式的值,再将值返回给主调函数。
- 函数中允许有多个return语句,但每次调用只能有一个return语句被执行,因此只能返回一个函数值。实际上,函数的右花括号具有return功能。
- 返回值的数据类型应该与函数原型中返回值的数据类型匹配,若两者不一致,则以函数原型中的返回值类型为准,自动进行类型转换,但可能会出现精度损失。
- 当遇到return语句时,函数执行将终止,程序控制流将立即返回调用函数。
- 不返回函数值的函数,可以明确定义为“空类型”,类型说明符为“void”。若要终止函数执行可用“ return;”语句
二、Linux的内存管理机制
冯诺依曼体系结构
- 五个设备:控制器、运算器、存储器、输入设备、输出设备。
- 三条总线:控制总线、地址总线、数据总线
Linux的内存
地址从高到低的四种虚拟内存:
栈:存储局部变量、参数、函数返回地址等
堆:也称动态内存分配,由程序员用malloc等函数向系统申请任意指定大小的内存,并由程序员自己调用free等函数来释放内存。
数据段
- 静态存储区:存储全局变量和static变量,分为初始化区和未初始化区。
- 常量区
代码段(正文段)
- 存储程序的函数代码
内存的管理
进程隔离
- 保护独立的进程,防止互相干涉数据和存储空间
- 进程中使用的地址是虚拟地址,分配有4个G
段页式内存管理
- 进程在虚拟内存中分为代码段、数据段、堆栈段。
- 进程在段中由许多固定大小的块组成,这些块称为页。
- 虚拟地址由段号、页号、页中偏移量构成。
- 虚拟地址和内存中物理地址的动态映射。由MMU完成交换
- 按需调页,消除了进程全部载入内存中。称为LPU
C程序的运行流程
- 系统将可执行文件加载进内存,函数代码读入代码段中,程序中的全局变量、静态变量读入数据段中并初始化。
- 内核启动特殊例程,此例程从程序入口main开始执行。
- 启动例程
- 在进程的main函数执行之前内核会启动
- 编译器在编译时会将启动例程编译进可执行文件中
- 搜集命令行的参数传递给main函数中的参数
三、变量的存储类型和生命周期
作用域的分类
文件作用域
- 变量从它定义的位置开始直到这个程序文件的末尾都有效,这种变量称为全局变量。
函数作用域
- 变量仅在一个函数中都有效,是一个局部变量
代码块作用域
- 变量位于一对花括号{}中(函数体或语句块),从它定义的位置开始到右}括号之间有效。代码块还包括由for、while、dowhile、if语句所控制的代码。
函数原型作用域
- 变量出现在函数原型中,这个函数原型只是一个声明而不是定义(没有函数体),那么变量从声明的位置开始到末尾之间有效。
注意:以上作用域范围由大到小排列,变量的取值遵守_就近原则_。
变量的存储类型
存储类型 | 变量 | 说明 |
---|---|---|
auto | 自动变量 | 局部变量在缺省存储类型的情况下归为自动变量,默认不会初始化,会产生随机值。 |
static | 静态变量 | 在程序执行时存在,并且只要整个程序在运行,就可以继续访问该变量。默认会初始化为0且仅初始化一次。可以是全局也可以是局部变量。 |
extern | 外部变量(全局变量) | 作用域是整个程序,包含该程序的各个文件。生存期非常长,它在该程序运行结束后,才释放内存。默认会初始化并仅初始化一次。 |
register | 寄存器变量 | 一般变量存储在内存中,而寄存器变量存放在CPU的寄存器中。对于循环次数较多的循环控制变量及循环体反复使用的变量均可定义为寄存器变量。只能是局部变量不能是全局变量并且不能加static修饰,默认不会初始化。会占用CPU所以不推荐使用。 |
寄存器变量运算起来更快,因为直接在cpu内部计算,而其他内存变量需要先从内存提出到cpu计算后再返回到内存,所以没有寄存器变量运算快。
- 内存分配地址在编译时完成
反编译查看:objdump -xd file
主要看symbol table(符号表)
.data:初始化区
.bss:未初始化区
.text:代码段
#include<stdio.h>
void func();
int a1; //全局变量,省略extern
static int a2;
int *p;
int main()
{
printf("p:%p\n",p); //指针默认初始化NULL
printf("a1:%d a2:%d\n",a1,a2); //extern变量默认初始化0
a1++;a2++;
func();func();func(); //重复执行函数3次
return 0;
}
void func()
{
printf("a1:%d a2:%d\n",a1,a2);
int a=10; //同auto,省略auto
a++;
auto int b=20;
b++;
register int r=30;
r++;
printf("a:%d b:%d r:%d\n",a,b,r);
static int count; //只初始化第一次,缺省初始化为0
count++;
printf("static count:%d\n",count);
}
变量的内存分配方式
静态存储区(属于数据段)上分配
- 针对全局变量和static变量,程序在编译时编译器就已对其分配好内存地址,这块内存在程序的整个运行期间都存在,这些变量的值在程序运行期间一直存在,直到程序运行结束才会被释放。
栈上分配 - 针对局部变量(包括参数),在每次调用函数时,系统会动态为局部变量在栈中分配内存空间,在函数调用后这些局部变量占有的内存空间会被自动释放。
堆上分配 - 或称动态内存分配,由程序员主动使用malloc,free等函数向系统申请指定大小内存或释放,生命周期由程序员决定。
变量的存储方式
静态存储方式
- 程序在编译时分配的固定存储空间(静态存储区)的方式,程序执行完毕就释放。全局变量和静态变量属于静态存储方式,存放在静态存储区中。
动态存储方式
- 程序运行期间根据需要进行动态的分配存储空间(动态存储区)的方式
- 属于动态存储方式的包括:
- 函数的形参
- 自动变量(带auto或者不带auto的局部变量)
- 函数调用时的现场保护和返回地址等
- 对以上数据,在函数调用开始时分配动态存储空间,函数调用结束时释放这些空间
变量的作用域和生命周期
变量 | 作用域 | 生命周期 |
---|---|---|
自动变量 | 限于一个函数内有效 | 限于一个函数,函数调用结束后被释放 |
寄存器变量 | 同自动变量 | 同自动变量 |
静态局部变量 | 限于一个函数内有效 | 限于整个程序的生命周期,程序运行结束才会被释放 |
静态全局变量 | 限于一个源文件 | 同静态局部变量 |
全局变量(外部变量) | 一个源文件范围,也可以通过extern声明扩展到程序中的多个源文件 | 同静态局部变量 |
链接属性
External
- 存储类型为extern
- 可以在当前文件和其他文件使用
Internal
- 存储类型为static
- 仅限于当前文件或函数使用
None
- 存储类型为auto
- 仅限于函数使用
注意
- 链接属性适用于变量和函数
- static可以修改链接属性,存储类型,作用域和生命周期
内部函数和外部函数
内部函数
- 一个函数只能被本文件中的其他函数调用称为内部函数
- 内部函数的声明:static 类型说明符 函数名(型参列表);
- 内部函数也称为静态函数,仅限于本文件内调用
- 若在不同文件中有同名的内部函数也互不干扰
外部函数
- 函数声明时在函数前面使用extern修饰称为外部函数,省略extern默认就是外部函数。
四、extern
extern的使用
利用extern来声明外部变量(全局变量)可以扩展外部变量的作用域,分为两种方式
-
在一个文件内声明外部变量
- 在外部变量定义之前若函数想使用该变量,则应在使用之前用extern对该变量作外部变量的声明,那么就可以从使用extern声明处起使用该外部变量。
-
在多个文件的程序中声明外部变量。
- 一个c程序可以由多个源文件组成,在一个文件中通过extern可以使用另外一個文件中已经定义的外部变量。
- 若c程序中的两个源文件都要用到同一个外部变量,那就不能分别在兩個文件中各自定义这个外部变量,否则在程序链接时会出现重复定义的错误。正确做法:在一个文件中用extern作为外部变量声明,这样此文件就可以使用该外部变量
extern使用注意点
- 外部变量值的修改对所有使用该变量的文件都有影响。
- 编译器遇到extern时,先从本文件中找到外部变量的定义,若找到就在本文件中扩展作用域。若没有找到就在链接时从其他文件中找到外部变量定义,若找到就将作用域扩展到本文件,若找不到按出错处理。
/*
*文件extern1.c
*定义函数
*/
#include<stdio.h>
//int a=2; 此处做定义会报错,不可重复定义,extern2已经定义了
int power(int n)
{
extern int a; //此处的a声明了extern2.c的a,可以使用
int result=1,i;
for(i=1;i<=n;i++)result*=a; //此处的a使用到了extern2.c的a
return result;
}
/*
*文件extern2.c
*使用函数
*/
#include<stdio.h>
extern int power(int); //使用extern1.c的函数,先进行声明
int a=2;
//static int a=2; 使用了static就不可以再被其他文件引用。
int main(void)
{
int result = power(5);
printf("result:%d\n",result);
}
五、函数栈
主调函数和被调函数
- 在函数调用时,被调函数名后面必须有括号
- 一个函数只能返回一个值
函数栈的调度流程
- 函数的调用过程实际上是对栈空间的操作过程,调用函数是使用对栈空间来保存信息,过程如下:
- 建立被调用函数的栈空间
- 保护调用函数的运行状态和返回地址
- 传递函数的实参给形参
- 执行被调用函数的函数体内语句
- 将控制权或返回值转交给调用函数
- 函数调用完毕,释放被调用函数的栈空间。
- 不同函数的栈区互相独立,函数间通过参数传递、返回值等方式进行数据传递。
六、递归调用 recursive
递归调用概念
概念
- 一个函数在它的函数体内调用它自身称为递归调用,这种函数称为递归函数
- C语言中支持运行时通过堆栈支持递归函数的实现。
- 在递归调用中,主调函数又是被调函数。执行递归函数将反复调用其自身,每调一次就进入新的一层。调用太深会造成溢出。
- 尾部递归最好采用迭代方式以提高效率
递归所需特性
- 存在限制条件,当符合限制条件递归不再继续。
- 每次递归调用后越来越接近该限制条件。
//将数值转换成字符
void bin2asc(int value){
int temp;
temp=value/10;
if(temp!=0)bin2asc(temp);//递归调用
putchar(value%10+'0');
}
int main(void){
bin2asc(4267);
printf("\n");
return 0;
}
//4267-->'4268'
/*使用递归输出斐波那契数列*/
long fibonacci(int n){
if(n<2) return 1;
return fibonacci(n-1)+fibonacci(n-2);
}
/*使用递归计算阶乘*/
long factorial(int n){
if(n<=0) return 1;
else return n*factorial(n-1);
}
七、函数指针与指针函数
指针函数
指针函数是指带指针的函数,即本质是一个函数。
指针函数返回类型是某一类型的指针。
指针函数的定义
返回类型标识符*函数名(形参列表){函数体}
//int sum=0;也可以作为全局变量
int *fun(int n)
{
static int sum=0; //局部静态变量,将sum存储在数据段,所以在函数清空后还得以保存
//int sum=0; 总之,不可以指向局部变量。
int *p=∑
int i;
for(i=1;i<=n;i++)sum+=i;
return p; //返回的指针指向的是局部变量,函数释放后局部变量会清空,不推荐使用
}
int main()
{
int *p_sum=fun(100); //指针赋值操作
sleep(1); //函数调用完毕,这1秒内fun()函数栈空间被清空,使得其中的值都变随机值。但是若使用static变量则避免了随机值。
printf("sum:%d\n",*p_sum);
return 0;
}
指针函数注意:
- 要是返回的指针指向的是函数内的局部变量,在函数释放后没有立即调用指向的值,这个值就会被清除,非常危险。
- 返回的指针最好是指向一个静态局部变量,或者一个全局变量,这样可以避免以上情况。
函数指针
C语言在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针。
通过函数指针可以调用它所指向的函数。
函数指针也称函数指针变量。
函数指针的定义和初始化:
返回类型说明符 (*函数指针变量名)(参数列表);
- 返回类型说明符为函数指针所指向的函数的返回类型
- 参数列表为函数指针所指向函数的形参列表
- 函数指针初始化:函数指针变量=函数名;//函数名即函数入口地址
通过函数指针调用函数的方式:
函数指针变量名(实参列表);
int max(int x,int y);//函数声明
/*定义函数指针*/
int (*p)(int,int);
//int (*p)();
//int (*p)(int x,int y);
/*函数指针初始化*/
p=max; //函数名即为函数入口地址
/*函数指针调用所指函数*/
result=p(a,b);
//result=max(a,b);
//result=(*p)(a,b);
回调函数 callback
概念
- 回调函数就是一个通过函数指针调用的函数。
- 把函数指针作为参数传递给另一个函数,当这个指针被用来调用其他所指向的函数时,我们就说这是回调函数。
- 回调函数不是由该函数的实现方(如main函数)直接调用,而是在特定的时间或条件发生时由另外的一方(如下process函数)调用的,用于对该事件或条件进行响应。
回调函数的实现流程
- 定义一个回调函数(如下get_grade函数)
- 提供函数调用的另一方(如下process函数)在初始化的时候,将回调函数的函数指针(如下get_grade)传递给调用者(process(score,get_grade))。
- 当特定的事件或条件发生的时候,调用者使用函数指针调用所指向的回调函数对事件进行处理。
优点:实现了代码的通用性。可以方便地调用各种算法。
/*文件名:callback.h*/
char process(int score,char (*p)(int));
/*文件名:callback.c*/
#include "callback.h"
char process(int score,char (*p)(int))
{
char result=p(score);
return result;
}
/*文件名:callback_test.c*/
void get_grade(int score)
{
}
int main(void)
{
int score;
printf();
scanf();
printf("result:%c\n",process(score,get_grade));
}
案例–使用回调函数选择排序算法排序
/*文件:callback_sort.h*/
extern void data_sort(int data[],int n,
void (*p)(int*,int),void (*out)(int*,int));
/*文件:callback_sort.c*/
void data_sort(int data[],int n,
void (*p)(int*,int),void (*out)(int*,int))
{
/*通过函数指针p去调用不同的实现排序算法的回调函数*/
p(data,n);
/*通过函数指针out去调用输出数组元素的回调函数*/
out(data,n);
}
/*文件:callback_sort_test.c*/
#include <stdio.h>
#include "callback_sort.h"
void bubble_sort(int *p,int n)
{
int i,j;
for(i=0;i<n-1;i++){
for(j=n-1;j>i;j--){
if(*(p+j)<*(p+j-1)){
int temp;
temp=*(p+j);
*(p+j)=*(p+j-1);
*(p+j-1)=temp;
}
}
}
}
void selection_sort(int *p,int n)
/*选择排序法:选择最小的一个数到排好序的数列前面*/
{
int i,j;
for(i=0;i<n-1;i++){
int pos=i;
for(j=i+1;j<n;j++){
if(*(p+j)<*(p+pos)){
pos=j;
} //找到最小的一个数的下标,然后将其值和下标i的值兑换
if(pos!=i){
int temp;
temp=*(p+j);
*(p+j)=*(p+j-1);
*(p+j-1)=temp;
}
}
}
void out_data(int data[],int n)
/*输出数组*/
{
int i;
for(i=0;i<n;i++)
printf("%d ",data[i]);
printf("\n");
}
int mian(void)
{
int data1[]={6,8,3,4,5};
int n=sizeof(data1)/4; //求数组的长度,总字节除以int字节数4
return 0;
}
八、可变参数列表
概念
- 参数个数可变的函数,如scanf()其函数原型
int scanf(const cahr* format,…);- 除了参数format固定外,后面跟的参数个数和类型是可变的(用三个点"…"做参数占位符)。
- “…”称为可变参数列表,可以用来接受个数和类型不确定的实参。
使用
可变参数列表在C语言中可通过三个宏(va_start,va_arg,va_end)和一个类型(va_list)实现的,它们都定义在头文件stdarg.h中。
-
宏va_stsrt的原型:
void va_start(va_list ap,paramN)- 参数
- va_list:存储参数的类型
- ap:可变参数列表地址
- paramN:确定的参数
- 功能:初始化可变参数列表
- 参数
-
宏va_arg的原型:
type va_arg(va_list ap,type)- 功能:返回下一个参数的值
-
宏va_end的原型:
type va_end(va_list ap,type)- 功能:关闭初始化列表(将可变参数列表清空)
-
可变参数列表的使用方式
- 用va_start初始化可变参数列表,用va_arg逐个获取参数的值,最后用va_end将可变参数列表清空。
#include<stdarg.h>
#include<stdio.h>
float average(int n_values,...);
int main()
{
float aver=average(5,1,2,3,4,5);
printf("aver:%.2f\n",aver);
return 0;
}
float average(int n_values,...)
/*第一个数n_values为项数,"..."开始为输入值,最后输出"..."所有值加起来的平均值*/
{
/*定义一个va_list类型的变量用于访问可变参数列表*/
va_list vary_arg;
int count;
float sum=0;
/*初始化可变参数列表*/
va_start(vary_arg,n_values);
/*通过循环获取可变参数列表中的参数*/
for(count=0;count<n_values;count++)
sum+=va_arg(vary_arg,int);//int为可变参数类型
/*关闭初始化列表*/
va_end(vary_arg);
return sum/n_values;
}
根据函数压栈的思想,使用指针实现可变参数
#include<stdio.h>
float average2(int n_values,...);
int main()
{
float aver=average2(5,1,2,3,4,5);
printf("aver:%.2f\n",aver);
return 0;
}
float average2(int n_values,...)
{
int *p=&n_values;
float sum=0.0f;
int i;
for(i=1;i<n_values;i++) //i从1开始,跳过n_values指针
sum += *(p+i);
return sum/n_values;
}
九,函数指针数组
函数指针数组概念
函数指针数组
- 数组元素是函数指针的数组称为函数指针数组,也称为__转移表__
函数指针数组的定义和初始化
返回类型说明符(*函数指针数组名[])(参数列表)
={函数指针/函数名1,……,函数指针/函数名n}
- 返回类型说明符为函数指针数组中的元素即函数指针所指向的函数的返回类型
- 参数列表为函数指针所指向的形参列表
- 用来初始化函数指针数组的元素可以是函数指针或函数名
函数指针数组使用
函数指针数组的使用方式
函数指针数组名下标;
或者
(*函数指针数组名[下标])(实参列表);
/*实现一个计算器*/
int add(int i,int j){return i+j;}
int sub(int i,int j){return i-j;}
int mul(int i,int j){return i*j;}
int div(int i,int j){return i/j;}
void make_menu();
int (*fun_array[])(int,int)={add,sub,mul,div};
int main(void)
{
int i,j,cmd;
while(1)
{
make_menu();
scanf("%d",&cmd);
if(cmd==0)break;
if(cmd>=1&&cmd<=4){
printf("输入两个数:");
scanf("%d%d",&i,&j);
/*通过函数指针数组去调用对应的函数*/
//int result = fun_arry[cmd-1](i,j); 直接写,常用
//int result = (*fun_arry[cmd-1])(i,j);
//也可以如下拆解写
int (*p)(int,int)=fun_array[cmd-1];
int result=p(i,j);
printf("%d\n",result);
}
return 0;
}
void make_menu()
{
printf("please chose a num:\n");
printf("0:quit\n");
printf("1:plus two num\n");
printf("2:subtract two num\n");
printf("3:multiply two num\n");
printf("4:divide two num\n");
}
函数指针数组的应用场合
-
通过使用switch语句可以获得类似的效果,但是使用函数指针数组可以有更大的灵活性,因为数组元素可以在程序运行时发生改变
-
可应用在命令菜单的选择处理
-
注意区分
int (*p[])(int); //这个是函数指针数组
int (*p)[4]; //这是数组指针,或称行指针
int (*p)(int); //一个函数指针
int *p(int); //指针函数声明