1. C 语言中的函数

(1)函数的由来:

程序 = 数据 + 算法→C 程序 = 数据 + 函数

(2)模块化程序设计

(3)C 语言中的模块

2. 面向过程的程序设计

(1)面向过程是一种以过程为中心的编程思想

(2)首先将复杂的问题分解为一个个容易解决的问题

(3)分解过后的问题可以按照步骤一步步完成

(4)函数是面向过程在 C 语言中的体现

(5)解决问题的每个步骤可以用函数来实现

3. 声明和定义

(1)声明的意义在于告诉编译器程序单元(以下均指变量或函数)的存在

(2)定义则明确指示程序单元的意义

(3)C 语言中通过 extern 进行程序单元的声明。

(4)一些程序单元在声明时可以省略 extern(如结构体)

(5)严格意义上的声明和定义并不相同!

global.c ——注意不是头文件

  1. #include <stdio.h>
  2.  
  3. float g_var = 10.0f; //注意,这里定义为float型的
  4.  
  5. struct Test
  6. {
  7. int x;
  8. int y;
  9. };
  10.  
  11. void f(int i,int j)
  12. {
  13. printf("i + j = %d\n",i + j);
  14. }
  15.  
  16. int g(int x)
  17. {
  18. return (int)( * x + g_var); //在本文件中g_var以float型处理
  19. }

test.c

  1. #include <stdio.h>
  2. #include <malloc.h>
  3.  
  4. //这里只是声明,告诉编译器该全局变量在外部的global.c文件己经存在!(注意,不是头文件)
  5. extern int g_var; //注意,这里声明为int。但外部定义为float型。在处理这个.c文件时,编译器会处g_var当成整型来处理,但实际上内存中是以float存储的!
  6. //这里声明为int是为了说明,声明和定义是不同的!
  7.  
  8. struct Test; //在global.c文件中,对于结构体声明无须加extern。
  9.  
  10. int main()
  11. {
  12. extern void f(int,int); //这里只是声明,相当于告诉编译器,这个函数在外部文件中己经存在。
  13.  
  14. extern int g(int);
  15.  
  16. struct Test* p = NULL; //这里合法的
  17.  
  18. //这里是错误的,因为结构体是在外部定义的,虽然在编译global.c时编译器是知道这个结构体的大小的。
  19. //但由于文件是分别编译的,编译只会按本文件中定义的类型来编译。由于本文件中找不到他的定义。
  20. //所以也就无法知道该结构体实际的大小,因此会报错。这就是声明和定义的区别!
  21. //struct Test* p = (struct Test*)malloc(sizeof(struct Test));//编译器提示这是一个不完全的类型
  22.  
  23. printf("p = %p\n", p);
  24.  
  25. //g_var = 10; //这里可以取消注释来观察g_var值的变化!
  26.  
  27. printf("g_var = %d\n", g_var);//会把内存中浮点型的g_var当成int型来处理!
  28.  
  29. f(, );
  30.  
  31. printf("g(3) = %d\n",g()); //g()函数(在global.c中),把g_var当成float型处理。
  32.  
  33. free(p);
  34.  
  35. return ;
  36. }

面向过程是由上至下分解问题的设计方法

4.函数参数

(1)函数参数在本质上与局部变量相同,都在栈上分配空间

(2)函数参数的初始值是函数调用时的实参值

(3)函数参数的求值顺序依赖于编译器的实现(注意:这里指求值顺序而不是入栈顺序!)

函数参数的求值顺序

  1. #include <stdio.h>
  2.  
  3. int func(int i, int j)
  4. {
  5. printf("i = %d, j = %d\n",i, j);
  6. return ;
  7. }
  8.  
  9. int f()
  10. {
  11. printf("f() Call...\n");
  12. return ;
  13. }
  14.  
  15. int g()
  16. {
  17. printf("g() Call...\n");
  18. return ;
  19. }
  20.  
  21. int main()
  22. {
  23. int k = ;
  24. int a = ;
  25.  
  26. func(k++,k++); //gcc、vc、bcc:2,1
  27.  
  28. printf("k = %d\n", k); //
  29.  
  30. a = f() * g(); //*两侧的操作数顺序也不是固定的,vc、gcc:f()先被调用,然后g()
  31.  
  32. return ;
  33. }

5.程序中的顺序点

(1)程序中存在一定的顺序点。

(2)顺序点指的是执行过程中修改变量值的最晚时刻

(3)在程序到达顺序点的时候,之前所做的一切操作必须完成。

6.C 语言中的顺序点

(1)每个完整表达式结束时,即分号处

(2)&&、||、?:、逗号表达式的每个参数计算之后

(3)函数调用时所有实参求值完成后(进入函数体之前)

程序中的顺序点

  1. #include <stdio.h>
  2.  
  3. int main()
  4. {
  5. int k = ;
  6. int a = ;
  7.  
  8. k = k++ + k++;
  9.  
  10. printf("k = %d\n", k); //vc:6 = 2 + 2 + 1 + 1
  11. //gcc:5 = 2 + 3
  12.  
  13. //&&是顺序点,先计算a--并更新a,所以a=0;根据短路原理后面
  14. //表达式不被判断,所以下面一行不被输出
  15. if(a-- && a)
  16. {
  17. printf("a = %d\n", a); //该行不被输出!
  18. }
  19. return ;
  20. }

7.函数参数入栈顺序

(1)函数参数的计算次序是依赖编译器实现的

(2)但入栈次序与调用约定有关

8.调用约定

(1)当函数调用发生时,参数会传递给被调用的函数,而返回值会返回函数的调用者。

(2)调用约定描述参数如何传递到栈中以及栈的维护方式(即参数传递顺序和调用栈清理)

(3)调用约定是预定义的,可理解为调用协议

(4)调用约定通常用于库调用和库开发的时候

①从右到左依次入栈:_stdcall、_cdecl、_thiscall

②从左到右依次入栈:_pascal、_fastcall

9.可变参数

(1)C 语言中可以定义参数可变的函数

(2)参数可变函数的实现依赖于 stdarg.h 头文件

①va_list:指向可变参数列表

②va_arg: 取具体参数值

③va_start:标识参数访问的开始

④va_end:标识参数访问的结束

(3)相关宏的定义

①va_list 宏:typedef char * va_list;

②_ADDRESSOF 宏:#define _ADDRESSOF(v)  ( &(v) ) //用来取得变量的地址

③_INTSIZEOF 宏:#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

A.主要是用来计算类型大小(取整到 sizeof(int)的整数倍)。比如 sizeof(int)为4,1,2,3,4 就取 4。而 5,6,7,8 就取 8。

对 x 向 n 取整用 C 语言的算术表达就是((x+n-1)/n)*n,当 n 为 2 的幂时可以将最后二步运算换成位操作将最低 n - 1 个二进制位清 0 就可以了。

B.取整的主要目的是进行内存对齐。

④va_start 宏:#define va_start(ap,v) (ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)),将第 1 个可变参数的起始地址保存在 ap 变量中。

⑤va_arg 宏:#define va_arg ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )。每次调用 va_arg(ap,v)就是取出当前 ap 指向的变量,然后移到下一个

变量(移动距离由 v 的类型来决定)。

⑥va_end 宏:#define va_end ( ap = (va_list)0 )。即,将 ap 设为 NULL

(4)图解可变参数宏的实现原理

编写函数计算平均值

  1. #include <stdio.h>
  2. #include <stdarg.h>
  3.  
  4. //一般的求平均值函数
  5. float average(int array[],int size)
  6. {
  7. int i = ;
  8. float avr = ;
  9.  
  10. for(i = ; i<size; i++)
  11. {
  12. avr += array[i];
  13. }
  14.  
  15. return avr / size;
  16. }
  17.  
  18. //利用可变参数函数来实现
  19. float averageEx(int n, ...)
  20. {
  21. int i = ;
  22. float sum = ;
  23. va_list args; //声明一个args指向,指向可变参数列表
  24.  
  25. va_start(args, n); //将args指向第1个可变参数
  26.  
  27. for(i=; i<n; i++)
  28. {
  29. sum += va_arg(args,int);
  30. }
  31.  
  32. va_end(args);
  33.  
  34. return sum / n;
  35. }
  36.  
  37. int main()
  38. {
  39. int array[] = {, , , , };
  40. printf("%f\n", average(array,));
  41.  
  42. printf("%f\n", averageEx(, ,,,,));
  43. printf("%f\n", averageEx(, ,,,));
  44.  
  45. return ;
  46. }

10.可变参数的限制

(1)可变参数必须从头到尾按照顺序逐个访问(1)可变参数必须从头到尾按照顺序逐个访问

(2)参数列表中至少要存在一个确定的命名参数

(3)可变参数函数无法确定实际存在的参数的数量

(4)可变参数无法确定参数的实际类型。(注意,如果 va_arg 中指定了错误的类型,那么结果是不可预料的)

11.函数与宏

(1)宏是由预处理直接替换展开的,编译器不知道宏的存在

(2)函数是由编译器直接编译的实体,调用行为由编译器决定

(3)多次使用宏会导致最终可执行程序的体积增大 ☆

(4)函数是跳转执行的,内存中只有一份函数体存在  ☆

(5)宏的效率比函数要高,因为是直接展开,无调用开销 ☆

(6)函数调用时会创建活动记录,效率不如宏

  1. #include <stdio.h>
  2.  
  3. #define RESET(p,len) \
  4. while( len > ) \
  5. ((char*)p)[--len] =
  6.  
  7. void reset(void* p, int len)
  8. {
  9. while(len > )
  10. ((char*)p)[--len] = ;
  11. }
  12.  
  13. int main()
  14. {
  15. int array[] = {, , , , };
  16. int len = sizeof(array);
  17. int i= ;
  18.  
  19. for(i=; i<; i++)
  20. {
  21. printf("array[%d] = %d\n",i, array[i]);
  22. }
  23.  
  24. RESET(array, len);
  25. //reset(array, len);
  26.  
  27. for(i=; i<; i++)
  28. {
  29. printf("array[%d] = %d\n",i, array[i]);
  30. }
  31.  
  32. return ;
  33. }

12.宏的局限

(1)宏的效率比函数稍高,但是其副作用巨大

(2)宏是文本替换,参数无法进行类型检查

(3)可以用函数完成的功能,绝对不用宏

(4)宏的定义中不能出现递归定义!!!!

宏的副作用

  1. #include <stdio.h>
  2.  
  3. #define _ADD_(a, b) a + b
  4. #define _MUL_(a, b) a * b
  5. #define _MIN_(a, b) ((a) < (b) ? (a) : (b))
  6.  
  7. int main()
  8. {
  9.  
  10. int i = ;
  11. int j = ;
  12.  
  13. //本意要计算3 * 7
  14. printf("%d\n", _MUL_(_ADD_(, ), _ADD_(, )));//1 + 2 * 3 + 4 ==>11
  15.  
  16. //本意要求min(1,10)
  17. printf("%d\n", _MIN_(i++, j)); //(i++)<(j) ? (i++):(b) //输出2
  18.  
  19. return ;
  20. }

13.宏的妙用

(1)用于生成一些常规性的代码

(2)封装函数,加上类型信息

  1. #include <stdio.h>
  2. #include <malloc.h>
  3.  
  4. #define MALLOC(type, x) (type*)malloc(sizeof(type)*x)
  5. #define FREE(p) (free(p),p = NULL)
  6.  
  7. //输出格式:变量名 = 变量的值
  8. #define LOG_INT(i) printf("%s = %d\n", #i, i)
  9. #define LOG_CHAR(c) printf("%s = %c\n", #c, c)
  10. #define LOG_FLOAT(f) printf("%s = %f\n", #f, f)
  11. #define LOG_POINTER(p) printf("%s = %p\n", #p, p)
  12. #define LOG_STRING(s) printf("%s = %s\n", #s, s)
  13.  
  14. //ForEach函数
  15. #define FOREACH(i, n) while(1){ int i = 0, l = n;for(i=0;i<l;i++)
  16. #define BEGIN {
  17. #define END }break;}
  18.  
  19. int main()
  20. {
  21. int* pi = MALLOC(int,);//己定义好MALLOC的返回类型,无须再强制转换
  22. char* str = "Hello World!";
  23.  
  24. LOG_STRING(str); //打印变量名及变量的值
  25.  
  26. LOG_POINTER(pi);
  27.  
  28. //k在宏内会被定义,其作用域在很小,此处无须再定义
  29. FOREACH(k, )
  30. BEGIN
  31. pi[k] = k + ;
  32. END
  33.  
  34. //k在宏内会被定义,此处无须再定义
  35. FOREACH(k, )
  36. BEGIN
  37. int value = pi[k];
  38. LOG_INT(value);
  39. END
  40.  
  41. FREE(pi);
  42.  
  43. LOG_POINTER(pi);
  44.  
  45. return ;
  46. }

14.函数设计原则

(1)函数从意义上应该是一个独立的功能模块

(2)函数名要在一定程度上反映函数的功能

(3)函数参数名要能够体现参数的意义

(4)尽量避免在函数中使用全局变量

void sc(char *s1, char* s1);×

void str_copy(char* dest, char* src); √

(5)当函数参数不应该在函数体内部被修改时,应加上 const 声明

(6)如果参数是指针,且仅作输入参数,则应加上 const 声明

void str_copy(char* dest, const char* src);

(7)不能省略返回值的类型。如果没有返回值,应声明为 void。

(8)对参数进行有效性检查,特别是指针参数的检查尤为重要

(9)不要返回指向“栈内存”的指针,因为栈内存在函数体结束时被自动释放

(10)函数体的规模要小,尽量控制在 80 行代码之内

(11)相同的输入对应相同的输出,避免函数带有“记忆”功能

(12)避免函数有过多的参数,参数个数尽量控制在 4 个以内

(13)有时候函数不需要返回值,但为了增加灵活性,如支持链式表达,可以附加返回值

char s[64];

int len = strlen(strcpy(s, "Hello")); //当中的 strcpy 返回缓冲区 s 的地址。

(14)函数名和返回值类型在语义上不可冲突

char c = getchar(); //getchar 的返回值实际上是 int 类型,而不是 char。与函数名不符。

参考资料:
www.dt4sw.com
http://www.cnblogs.com/5iedu/category/804081.html

C语言学习笔记--函数的更多相关文章

  1. C语言学习笔记--函数与指针

    1. 函数类型 (1)C 语言中的函数有自己特定的类型,这个类型由返回值.参数类型和参数个数共同决定.如 int add(int i,int j)的类型为 int(int,int). (2)C 语言中 ...

  2. c语言学习笔记 函数数组传递笔记

    今天学习c语言的一个小例子,果然还是陷入了php的编程习惯里,这里记录一下. #include <stdio.h> //例子很简单,就是编写一个函数把传递进来的数组里的值都赋值为1而已 / ...

  3. Go语言学习笔记-函数部分(三)

    函数部分 函数基本组成:关键字func.函数名.参数列表.返回值.函数体.返回语句 例子: func Add(int a, int b) (return int, err error){ ....函数 ...

  4. Go语言学习笔记七: 函数

    Go语言学习笔记七: 函数 Go语言有函数还有方法,神奇不.这有点像python了. 函数定义 func function_name( [parameter list] ) [return_types ...

  5. 大一C语言学习笔记(5)---函数篇-定义函数需要了解注意的地方;定义函数的易错点;详细说明函数的每个组合部分的功能及注意事项

    博主学习C语言是通过B站上的<郝斌C语言自学教程>,对于C语言初学者来说,我认为郝斌真的是在全网C语言学习课程中讲的最全面,到位的一个,这个不是真不是博主我吹他哈,大家可以去B站去看看,C ...

  6. 2017-04-21周C语言学习笔记

    C语言学习笔记:... --------------------------------- C语言学习笔记:学习程度的高低取决于.自学能力的高低.有的时候生活就是这样的.聪明的人有时候需要.用笨的方法 ...

  7. GO语言学习笔记(一)

    GO语言学习笔记 1.数组切片slice:可动态增长的数组 2.错误处理流程关键字:defer panic recover 3.变量的初始化:以下效果一样 `var a int = 10` `var ...

  8. Go语言学习笔记十三: Map集合

    Go语言学习笔记十三: Map集合 Map在每种语言中基本都有,Java中是属于集合类Map,其包括HashMap, TreeMap等.而Python语言直接就属于一种类型,写法上比Java还简单. ...

  9. Go语言学习笔记十二: 范围(Range)

    Go语言学习笔记十二: 范围(Range) rang这个关键字主要用来遍历数组,切片,通道或Map.在数组和切片中返回索引值,在Map中返回key. 这个特别像python的方式.不过写法上比较怪异使 ...

随机推荐

  1. External (and Live) snapshots with libvirt

    list all the block devices associated with the guest $ virsh domblklist testvm --details Type Device ...

  2. 【新手专属】IntelliJ IDEA删除项目

    这两天刚从Eclipse转手IDEA,每次都是直接删项目文件,后来百度一下才明白原来应该这样~~~ IntelliJ IDEA 删除项目,共三步: 第一步:记住当前项目文件路径1,然后点击file-- ...

  3. 使用ES6的Promise 解决回调函数。

    //创建一个Promise实例,获取数据.并把数据传递给处理函数resolve和reject.需要注意的是Promise在声明的时候就执行了. var getUserInfo=new Promise( ...

  4. SQL-表的操作(创建表,删除表,更改列,插入新行,更改行的值,删除表中数据)

    一,操作表及列 1.创建表: CREATE TABLE test (ID int  PRIMARY KEY IDENTITY,Name varchar(20) ) 2.删除表 DROP TABLE t ...

  5. tag问题

  6. HihoCoder1182 欧拉路(Fleury算法)

    描述 小Hi和小Ho破解了一道又一道难题,终于来到了最后一关.只要打开眼前的宝箱就可以通关这个游戏了. 宝箱被一种奇怪的机关锁住: 这个机关是一个圆环,一共有2^N个区域,每个区域都可以改变颜色,在黑 ...

  7. 性能测试工具BenchmarkDotnet

    .NET Core中的性能测试工具BenchmarkDotnet https://www.cnblogs.com/lwqlun/p/9671611.html 背景介绍 之前一篇博客中,我们讲解.NET ...

  8. Docker-安装与部署

    本文在CentsOS下安装Docker 1.安装前准备工作 系统要求: 在CentOS下需要64位的CentsOS 7  OS requirements To install Docker, you ...

  9. vim直接编辑远程文件

    本文由Suzzz原创,发布于http://www.cnblogs.com/Suzzz/p/4116341.html,转载请保留此声明. Linux环境下 vim scp://user@hostIP/P ...

  10. hl7中V2版本的ACK消息的构造

    hl7 v2的ack消息即应答消息构造时有几个注意的地方. 首先,我们看下2个ack的例子: Send: MSH|^~\&|NIST_SENDER^^|NIST^^|NIST_RECEIVER ...