一.《第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. 函数名与返回值类型在语义上不可冲突
【优秀代码赏析】