《C语言进阶剖析》5.函数与作用域

文章详细介绍了C语言中函数的重要性和作用,包括模块化编程、面向过程思想的体现,以及函数参数的特性和求值顺序。同时,提到了宏的使用,包括其效率、副作用和应用场景,并强调了递归函数和良好的函数设计原则,如保持函数小巧、避免全局变量、参数有效性检查等。
摘要由CSDN通过智能技术生成

一.《第43课 - 函数的意义》

 

1. C语言中的函数

追溯一下C语言发展的历史,我们知道C语言在早期是用在科学计算上的,而科学计算就是使用各种算法处理数据

在C语言中就是使用函数实现算法。

 

2. 函数的意义

(1)模块化程序设计

      

(2)C语言中的模块化   ==>  使用函数完成模块化编程

     

3. 面向过程的程序设计

(1)面向过程是一种以过程(过程即解决问题的过程)为中心的编程思想。首先将复杂的问题分解为一个个容易解决的问题,分解过后的问题可以按照步骤一步步完成。

(2)函数是面向过程在C语言中的体现,解决问题的每个步骤可以用函数来实现。

4. 声明和定义

(1)声明的意义在于告诉编译器程序单元的存在

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

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

(4)一些程序单元在声明时可以省略extern,比如声明结构体的类型,extern struct Test  <==> struct test

【声明和定义不同】

// global.c

#include <stdio.h>

int g_var = 10;
// float g_var = 10;

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型处理
}

// 43-1.c

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

// 声明,告诉编译器g_var在外部的文件中
extern int g_var;
// extern float g_var;


struct Test;  // extern struct Test;  extern可以省略

int main()
{
    // 声明两个函数
    extern void f(int,int);
    extern int g(int);

    struct Test* p = NULL; // 可以这样定义指针,,但是不能定义该结构体对用的变量

    // struct Test* p = (struct Test*)malloc(sizeof(struct Test));  // error,因为在编译该文件时不知道struct Test的具体成员,也就不知道它的大小

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

    //g_var = 10;

    printf("g_var = %d\n", g_var);  // 如果前面是 extern float g_varl; 这里打印垃圾值,浮点数和整型在内存中的存储方式不同

    f(1, 2);  // i + j = 3

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

    free(p);

    return 0;
}


二.《第44课 - 函数参数的秘密(上)》

 

1. 函数的参数

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

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

         

 (3)C标准只规定了 必须要将每个实参的具体值求出来之后才能进行函数调用,并没有规定函数参数的求值顺序,求值顺序依赖于编译器的实现

         比如 void func(参数表达式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;

    func(k++, k++); // 参数的求值顺序取决于编译器,gcc先计算右边的再计算左边的

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

    a = f() * g(); // C语言中的乘法操作也是这样,左右操作数哪个先被求值依赖于编译器

    return 0;
}

gcc编译器的输出结果:

2. 程序中的顺序点

(1)

(2)

(3)

3. C语言中的顺序点


三.《第45课 - 函数参数的秘密(下)》

 

四.《第46课 - 函数与宏分析》

1. 函数与宏

(1)宏是由预处理器直接替换展开的,编译器不知道宏的存在,因此参数无法进行类型检查

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

(2)多次使用宏会增大代码量,最终导致可执行程序的体积增大,对于嵌入式设备而言,设备资源有限,这个还是比较重要的

        函数是跳转执行的,内存中只有一份函数体存在,不存在宏的问题

(3)宏的效率比函数高,因为宏是文本替换,没有调用开销       //  虽然宏的效率比函数稍高但副作用很大,因此,可以用函数完成的功能绝对不用宏

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

(4)函数可以递归调用,但宏的定义中不能出现递归定义

【函数与宏】

#include <stdio.h>

// 使用宏将一片内存区域置0
#define RESET(p, len) while(len > 0) \
                        ((char *)p)[--len] = 0

// 使用函数将一片内存区域置0
void reset(void *p, int len)
{
    while(len > 0)
        ((char *)p)[--len] = 0;

}

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

    /*
        下面的宏和函数都可以实现置0的功能
        但是假如使用 RESET(10, len),这个在编译期间是不错报错的,宏不会检查参数的类型
        使用reset(10, len)函数在编译时就会有warning,提示传参类型不符
    */
    // reset(a, len);
    // RESET(a, len);

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

    return 0;
}

 

【宏的副作用】

#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;

    // 预处理结果:printf("%d\n", 1 + 2 * 3 + 4);
    // 预期: (1 + 2) * (3 + 4) ==> 21
    // 实际: 1 + 2 * 3 + 4 ==> 11
    printf("%d\n", _MUL_(_ADD_(1, 2), _ADD_(3, 4)));

    // 预处理结果:printf("%d\n", ((i++) < (j) ? (i++) : (j)));
    // 预期: 1 < 10? 1 : 10 ==> 1
    // 实际: (i++) < (j) ? (i++) : (b) ==> 2
    printf("%d\n", _MIN_(i++, j));

    return 0;
}

2. 宏的妙用 

 前面讲了宏的很多副作用和缺点,那宏是不是一无是处呢?绝对不是这样的,在C语言中宏有很多妙用。

(1)用于生成一些常规性的代码,比如下面代码中的LOG_INT、LOG_CHAR、LOG_FLOAT、LOG_POINTER

(2)封装函数,加上类型信息,比如下面代码中的MALLOC的实现

【宏的妙用】

#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)

#define FOREACH(i, n)    while(1){ int i = 0, l = n;for(i = 0; i < l; i++)    // 使用while是为了定义一个scope,局部变量都在其中
#define BEGIN            {
#define END              }break;}

int main()
{
    int* pi = MALLOC(int, 5);  // ①可以指定数据类型,代码可读性更强 ②返回的void *指针已经进行了强制类型转换
    char* str = "2020-01-14 22:38:27";

    LOG_STRING(str);  // str = 2020-01-14 22:38:27

    LOG_POINTER(pi);  // pi = 0x74f010

/*
    while(1){ int k = 0, l = 5;for(k = 0; k < l; k++)
    {
        pi[k] = k + 1;
    }break;}
*/
    FOREACH(k, 5)
    BEGIN
        pi[k] = k + 1;  // 对pi对应的堆空间赋值
    END

/*
    while(1){ int k = 0, l = 5; for(k = 0; k < l; k++)
    {
        int value = pi[k];
        printf("%s = %d\n", "value", value);
    }break;}
*/
    FOREACH(k, 5)
    BEGIN
        int value = pi[k];  // 遍历pi对应的堆空间
        LOG_INT(value);
    END

    FREE(pi);   // 安全的释放动态内存

    LOG_POINTER(pi);    // pi = nil

    return 0;
}

 

五.《第47课 - 递归函数分析》

 


六.《第48课 - 函数设计原则(完结)》

 

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

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

3. 函数参数要能够体现参数的意义

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

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

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

7. 不能省略返回值的类型

如果函数没有返回值,那么应声明为void类型

8. 对参数进行有效性检查

9. 不要返回指向“栈内存”的指针

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

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

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

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

14. 函数名与返回值类型在语义上不可冲突

【优秀代码赏析】

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值