C语言学习笔记--函数

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 ——注意不是头文件

#include <stdio.h>

float g_var = 10.0f; //注意,这里定义为float型的

struct Test
{
    int x;
    int y;
};

void f(int i,int j)
{
    printf("i + j = %d\n",i + j);
}

int g(int x)
{
    return (int)( 2 * x + g_var); //在本文件中g_var以float型处理
}

test.c

#include <stdio.h>
#include <malloc.h>

//这里只是声明,告诉编译器该全局变量在外部的global.c文件己经存在!(注意,不是头文件)
extern int g_var;  //注意,这里声明为int。但外部定义为float型。在处理这个.c文件时,编译器会处g_var当成整型来处理,但实际上内存中是以float存储的!
                   //这里声明为int是为了说明,声明和定义是不同的!

struct Test; //在global.c文件中,对于结构体声明无须加extern。

int main()
{
    extern void f(int,int); //这里只是声明,相当于告诉编译器,这个函数在外部文件中己经存在。

    extern int g(int);

    struct Test* p = NULL; //这里合法的

    //这里是错误的,因为结构体是在外部定义的,虽然在编译global.c时编译器是知道这个结构体的大小的。
    //但由于文件是分别编译的,编译只会按本文件中定义的类型来编译。由于本文件中找不到他的定义。
    //所以也就无法知道该结构体实际的大小,因此会报错。这就是声明和定义的区别!
    //struct Test* p = (struct Test*)malloc(sizeof(struct Test));//编译器提示这是一个不完全的类型

    printf("p = %p\n", p);

    //g_var = 10;  //这里可以取消注释来观察g_var值的变化!

    printf("g_var = %d\n", g_var);//会把内存中浮点型的g_var当成int型来处理!

    f(1, 2);

    printf("g(3) = %d\n",g(3)); //g()函数(在global.c中),把g_var当成float型处理。

    free(p);

    return 0;
}

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

4.函数参数

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

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

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

函数参数的求值顺序

#include <stdio.h>

int func(int i, int j)
{
    printf("i = %d, j = %d\n",i, j);
    return 0;
}

int f()
{
    printf("f() Call...\n");
    return 1;
}

int g()
{
    printf("g() Call...\n");
    return 2;
}

int main()
{
    int k = 1;
    int a = 0;

    func(k++,k++); //gcc、vc、bcc:2,1

    printf("k = %d\n", k); //3

    a = f() * g(); //*两侧的操作数顺序也不是固定的,vc、gcc:f()先被调用,然后g()

    return 0;
}

5.程序中的顺序点

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

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

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

6.C 语言中的顺序点

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

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

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

程序中的顺序点

#include <stdio.h>

int main()
{
    int k = 2;
    int a = 1;

    k = k++ + k++;

    printf("k = %d\n", k); //vc:6 = 2 + 2 + 1 + 1
                           //gcc:5 = 2 + 3

    //&&是顺序点,先计算a--并更新a,所以a=0;根据短路原理后面
    //表达式不被判断,所以下面一行不被输出
    if(a-- && a)
    {
        printf("a = %d\n", a); //该行不被输出!
    }
    return 0;
}

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)图解可变参数宏的实现原理

编写函数计算平均值

#include <stdio.h>
#include <stdarg.h>

//一般的求平均值函数
float average(int array[],int size)
{
    int i = 0;
    float avr = 0;

    for(i = 0; i<size; i++)
    {
        avr += array[i];
    }

    return avr / size;
}

//利用可变参数函数来实现
float averageEx(int n, ...)
{
    int i = 0;
    float sum = 0;
    va_list args; //声明一个args指向,指向可变参数列表

    va_start(args, n); //将args指向第1个可变参数

    for(i=0; i<n; i++)
    {
        sum += va_arg(args,int);
    } 

    va_end(args);

    return sum / n;
}

int main()
{
    int array[] = {1, 2, 3, 4, 5};
    printf("%f\n", average(array,5));

    printf("%f\n", averageEx(5, 1,2,3,4,5));
    printf("%f\n", averageEx(4, 1,2,3,4));

    return 0;
}

10.可变参数的限制

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

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

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

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

11.函数与宏

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

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

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

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

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

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

#include <stdio.h>

#define RESET(p,len)               while( len > 0)                  ((char*)p)[--len] = 0

void reset(void* p, int len)
{
    while(len > 0)
      ((char*)p)[--len] = 0;
}

int main()
{
    int array[] = {1, 2, 3, 4, 5};
    int len = sizeof(array);
    int i= 0;

    for(i=0; i<5; i++)
    {
        printf("array[%d] = %d\n",i, array[i]);
    }

    RESET(array, len);
    //reset(array, len);

    for(i=0; i<5; i++)
    {
        printf("array[%d] = %d\n",i, array[i]);
    }

    return 0;
}

12.宏的局限

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

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

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

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

宏的副作用

#include <stdio.h>

#define _ADD_(a, b) a + b
#define _MUL_(a, b) a * b
#define _MIN_(a, b) ((a) < (b) ? (a) : (b))

int main()
{

    int i = 1;
    int j = 10;

    //本意要计算3 * 7
    printf("%d\n", _MUL_(_ADD_(1, 2), _ADD_(3, 4)));//1 + 2 * 3 + 4  ==>11

    //本意要求min(1,10)
    printf("%d\n", _MIN_(i++, j)); //(i++)<(j) ? (i++):(b) //输出2

    return 0;
}

13.宏的妙用

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

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

#include <stdio.h>
#include <malloc.h>

#define MALLOC(type, x)  (type*)malloc(sizeof(type)*x)
#define FREE(p)          (free(p),p = NULL)

//输出格式:变量名 = 变量的值
#define LOG_INT(i)        printf("%s = %d\n", #i,  i)
#define LOG_CHAR(c)       printf("%s = %c\n", #c,  c)
#define LOG_FLOAT(f)      printf("%s = %f\n", #f,  f)
#define LOG_POINTER(p)    printf("%s = %p\n", #p,  p)
#define LOG_STRING(s)     printf("%s = %s\n", #s,  s)

//ForEach函数
#define FOREACH(i, n)    while(1){ int i = 0, l = n;for(i=0;i<l;i++)
#define BEGIN            {
#define END               }break;}

int main()
{
    int* pi = MALLOC(int,5);//己定义好MALLOC的返回类型,无须再强制转换
    char* str = "Hello World!";

    LOG_STRING(str); //打印变量名及变量的值

    LOG_POINTER(pi);

    //k在宏内会被定义,其作用域在很小,此处无须再定义
    FOREACH(k, 5)
    BEGIN
        pi[k] = k + 1;
    END

    //k在宏内会被定义,此处无须再定义
    FOREACH(k, 5)
    BEGIN
        int value = pi[k];
        LOG_INT(value);
    END

    FREE(pi);

    LOG_POINTER(pi);

    return 0;
}

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

时间: 09-25

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

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

1. 函数类型 (1)C 语言中的函数有自己特定的类型,这个类型由返回值.参数类型和参数个数共同决定.如 int add(int i,int j)的类型为 int(int,int). (2)C 语言中通过 typedef 为函数类型重命名 typedef type name(parameter list);//如 typedef int f(int,int); 2. 函数指针 (1)函数指针用于指向一个函数,函数名是执行函数体的入口地址. (2)定义函数指针的两种方法 ①通过函数类型定义:Fun

Perl语言学习笔记 6 哈希

1.哈希的键是唯一的,值可以重复! 2.访问哈希元素 $hashname{"$key"};#哈希为大括号,数组为方括号,键为字符串 $family_name{"fred"} = "firstd";#给哈希元素赋值 3.哈希键支持任意表达式 $foo = "na"; $family_name{$foo."me"};#获取$family_name{"name"}对应的值 4.访问整个哈希 %

R语言学习笔记2——绘图

R语言提供了非常强大的图形绘制功能.下面来看一个例子: > dose <- c(20, 30, 40, 45, 60)> drugA <- c(16, 20, 27, 40, 60)> drugB <- c(15, 18, 25, 31, 40) > plot(dose, drugA, type="b") > plot(dose, drugB, type="b") 该例中,我们引入了R语言中第一个绘图函数plot.pl

R语言学习笔记

參考:W.N. Venables, D.M. Smith and the R DCT: Introduction to R -- Notes on R: A Programming Environment for Data Analysis and Graphics,2003. http://bayes.math.montana.edu/Rweb/Rnotes/R.html 前言:关于R 在R的官方教程里是这么给R下注解的:一个数据分析和图形显示的程序设计环境(A system for data

JavaScript--基于对象的脚本语言学习笔记(三)

事件处理器 1.一个数据校验表单的例程 <html> <head> <title>js练习</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <script type="text/javascript"> String.prototype.trim=function(){ r

Go语言学习笔记(一) [Go语言的HelloWorld]

日期:2014年7月18日 1.简介 Go 编程语言是一个使得程序员更加有效率的开源项目.Go 是有表达力.简 洁.清晰和有效率的.它的并行机制使其很容易编写多核和网络应用,而新奇的类型系统允许构建有性的模块化程序.Go 编译到机器码非常快 速,同时具有便利的垃圾回收和强大的运行时反射.它是快速的.静态类型编译语言,但是感觉上是动态类型的,解释型语言. Go 是第一个实现了简单的(或更加简单的)并行开发,且跨平台的类 C 语言. 2.Go语言文档查看 安装好Go语言之后,其文档可以通过go do

go语言学习笔记

go语言学习笔记 go语言学习笔记(初级) 最近一直在学习go语言,因此打算学习的时候能够记录 一下笔记.我这个人之前是从来没有记录笔记的习惯, 一直以来都是靠强大的记忆力去把一些要点记住. 读书的时候因为一直都是有一个很安静和很专心的环境, 因此很多事情都能记得很清楚,思考的很透彻.但是随着 年纪不断增加,也算是经历了很多的事情,加上工作有时会让人 特别烦闷,很难把心好好静下来去学习,去思考大自然的终极 奥秘,因此需要记录一些东西,这些东西一方面可以作为一种自我激励 的机制,另一方面,也算是自

Go语言学习笔记(二) [变量、类型、关键字]

日期:2014年7月19日 1.Go 在语法上有着类 C 的感觉.如果你希望将两个(或更多)语句放在一行书写,它们 必须用分号分隔.一般情况下,你不需要分号. 2.Go 同其他语言不同的地方在于变量的类型在变量名的后面.例如:不是,int a,而是 a int.当定义了一个变量,它默认赋值为其类型的 null 值.这意味着,在 var a int后,a 的 值为 0.而 var s string,意味着 s 被赋值为零长度字符串,也就是 "". 3.Go语言的变量声明和赋值 在Go中使

JavaScript--基于对象的脚本语言学习笔记(一)

1.两种嵌入js的方式 使用javascript前缀构建url:<a href="javascript:alert('运行JavaScript..')">运行js</a> js脚本放在<style></style>之间: <style type="text/javascript"> alert("运行JavaScript..") </script> 2.如果没有声明变量直接使