目前正在学习函数的相关概念,期间将此博客当做笔记本来用,因此本文可能会出现很多基本知识点,但是也可能会有 只有初学者会关注到的知识点,所以各位伙伴们斟酌观看吧 ̋(๑˃́ꇴ˂̀๑)
PS:本文仅供有些C语言基础,想回顾知识点的伙伴阅读
———————————————————————————————————————
1、递归
One、初始递归
void fun()
{
static int count = 5;//用 static 修饰 count 变量保证其被调用时不会被重复定义为 5
printf("Hi!\n");
if (--count)
{
fun();//调用本身
}
}
output:Hi!
Hi!
Hi!
Hi!
Hi!
递归的实质:函数调用本身
Two、递归求阶层
long fun(int);
long fun(int num)
{
long result;
if (num > 0)
{
result = num * fun(num - 1);
}
else
{
result = 1;
}
return result;
}
int main(void)
{
int num;
printf("请输入一个整数:");
scanf("%d", &num);
printf("%d的阶层是:%ld\n", num, fun(num));
return 0;
}
分析:
因为求阶层时输出的值是比较大的,所以函数最好 return long 类型
fun()循环中递归过程分为:顺序和回溯,先把 if 语句内的条件执行完再执行 else 条件内的语句,因为 else 内语句的 result = 1,所以与 if 语句内 num 相乘得到的还是 num 的值,故最后返回的是 num 的阶层
Three、汉诺塔
没玩过汉诺塔的可以去百度搜小游戏玩玩,本题要求列出移动汉诺塔的顺序(无论多少层)
直接上代码
void hanoi(int n, char x, char y, char z);
void hanoi(int n, char x, char y, char z)
{
if (n == 1)
{
printf("%c --> %c\n", x, z);
}
else
{
hanoi(n - 1, x, z, y);//将n-1个圆盘从x移到y
printf("%c --> %c\n", x, z);
hanoi(n - 1, y, x, z);//将n-1个圆盘从y移到z
}
}
int main(void)
{
int n;
printf("请输入汉诺塔的层数:");
scanf("%d", &n);
hanoi(n, 'X', 'Y', 'Z');
return 0;
}
input:请输入汉诺塔的层数:2
output:x --> y
x --> z
y --> z
以我的看法就是:hanoi(n, a, b, c)就是将 a 借助 b 移动到 c ,再递归调用本身,直到n == 1,再执行回溯,就是2An + 1步操作,但是具体else语句内的 printf 夹在两个hanoi()函数中间是怎么实现的,或借助是如何实现的还是不太理解
毕竟我现在也不太理解这种抽象的原理,所以先推荐一篇博客汉诺塔问题——递归,了解一下她的见解,等我学到算法我再回来详细补坑
Four、排序的算法:快速排列
若有 73, 108, 99, 56, 147, 94这几个数,如何快速排序呢?
直接上代码
void fun(int a[], int left, int right)
{
int val, temp;
int i = left;
int j = right;
val = a[(left + right) / 2];//基准点
while (i <= j)
{
while (a[i] < val)//满足就加一
{
i++;
}
while (a[j] > val)//满足就减一
{
j--;
}
if (i <= j)
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
i++;
j--;
}
}
if (left < j)
{
fun(a, left, j);
}
if (right > i)
{
fun(a, i, right);
}
}
int main(void)
{
int a[] = { 73,108,99,56,147,94 };
int length = (sizeof(a) / sizeof(a[0]));
fun(a, 0, length - 1);
int p;
for (p = 0; p < length; p++)
{
printf("a[%d] 是 %d\n", p, a[p]);
}
putchar('\n');
return 0;
}
output:a[0] 是 56
a[1] 是 73
a[2] 是 94
a[3] 是 99
a[4] 是 108
a[5] 是 147
解析:
查看具体思路与解法
再做个补充:
这种方法目前只适用于已知数组(即全部元素都已知),而不适用于动态数组(即可输入元素)
fun()函数的声明是(数组,数值,数值),是为末尾的递归做准备的,因为递归时带入的参数与前一次的是不一样的
fun()函数中两个内部的while的循环用 < 和 > ,而不用 <= 和 >= 是因为当元素与基准点的值一样时也要替换,不然若数组的元素比较少时会出现Bug,如{ 7,9,8 }
还有,我认为这种长篇代码对于初学者知其然就行了,等以后用多了自然就知其所以然
2、动态内存管理
- malloc —— 申请动态内存空间
- free —— 释放动态内存空间
- calloc —— 申请并初始化一系列内存空间
- realloc —— 重新分配内存空间
注意:这些函数都包含在 <stdlib.h> 这个头文件中
简单介绍 malloc 和 free
malloc()函数向系统申请分配size个字节的内存空间,并返回一个指向这块空间的指针,若函数申请失败则返回NULL(但这并不意味着函数调用失败)
free()函数释放ptr参数指向的内存空间(该内存空间必须是由malloc、calloc、realloc函数申请的)
举例
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *ptr;
ptr = (int *)malloc(sizeof(int));//(int*)是用来强调 malloc 该地址是用来储存整型的
if(ptr == NULL)//检测是否申请成功
{
printf("分配内存失败!\n");
exit(1);//退出整个程序
}
printf("请输入一个整数:");
scanf("%d",ptr);//这里相当于 &(*ptr),效果都一样
printf("你输入的整数是:%d\n",*ptr);
free(ptr)//释放 ptr 指针指向的内存空间
return 0;
}
若申请动态内存成功
intput:请输入一个整数:520
output:你输入的整数是:520
若申请动态内存失败
intput:520
output:分配内存失败!//并 return 1(从此看出返回 1 是为了区别是否申请成功)
exit()函数也包含在 <stdlib.h> 这个头文件中
free()函数释放的只是参数所指向的内存空间,并不是指针本身,释放完后指针仍然指向原来的地方,只是所指向的内存已变为非法空间
危:若申请的动态内存没有及时释放会造成 内存泄露,当大量内存空间未被释放时会造成程序崩溃
再举一个实现字符串的例子
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int i, length;
char *buffer;
printf("请输入字符串的长度:");
scanf("%d", &length);
buffer = (char *)malloc(length+1); // 还要存放'\0'字符
if (buffer == NULL)
{
printf("内存空间不足!\n");
exit(1);
}
printf("请输入%d个字符的字符串:", length);
getchar(); // 清除上一次输入残留的'\n'字符
for (i = 0; i < length; i++)
{
buffer[i] = (char)getchar();
}
buffer[length] = '\0';
printf("您输入的字符串是:%s\n", buffer);
free(buffer);
return 0;
}
input:请输入字符串的长度:5
请输入5个字符的字符串:12345
output:您输入的字符串是:12345
有没有发现,这个代码片可以实现动态数组!其核心思想就是 malloc 一块内存,转换成期望的指针类型,那么就可以该指针通过动态往这个内存中存数据了。 补充:在 Dev-C++ 和 VS2019 中还不支持C99标准的动态数组,只能用上述方法进行间接实现
补充:导致内存泄漏主要有两种情况
1、隐式内存泄漏(即用完内存块没有及时使用free()函数释放)
2、丢失内存块地址(即改变指针指向,导致没有指向内存块的指针从而释放不了)
更高效的管理内存
在头文件 <string.h> 与 <memory.h> 中,都有几个mem开头的函数
- memset —— 使用一个常量字节填充内存空间
- memcpy —— 复制内存空间
- memmove —— 复制内存空间
- memcmp —— 比较内存空间
- memchr —— 在内存空间中搜索一个字符
各函数原型
#include <string.h>//或#include <memory.h>
...
void* memset(void* s, int c, size_t n);
void* memcpy(void* dest, const void* str, size_t n);
void* memmove(void* dest, const void* str, size_t n);
int memcmp(const void* str1, const void* str2, size_t n);
void* memchr(const void* str, int c, size_t n);
以memset举例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>//或#include <memory.h>
int main(void)
{
int* ptr = NULL;
ptr = (int*)malloc(5 * sizeof(int));
memset(ptr, 0, 5 * sizeof(int));
for(int i = 0; i < 5; i++)
{
printf("%d ", ptr[i]);
}
free(ptr);
return 0;
}
output:0 0 0 0 0
memset 的作用是将某一块内存中的内容全部设置为指定的值(可以是int、char、double等), 这个函数通常为新申请的内存做初始化工作
memcpy()函数和 strcpy()函数很类似,都可以用来复制
strcpy 只用于字符串复制,并且它不仅复制字符串内容之外,还会复制字符串的结束符
memcpy 提供了一般内存的复制,即可以复制任意内容,例如字符数组、整型、结构体、类等,因此用途更广
由此类比,上述五种 mem 开头的函数都适用于内存空间的管理,而不单只用于某一类型
补充:这几种函数只了解,不做补充,等以后需要用时再说
简单介绍 calloc 和 realloc
calloc很好理解,calloc()函数和 malloc()函数的一个重要区别是:calloc函数在申请完内存后,自动初始化该内存空间为0,举个例子(下面两种写法是等价的)
//calloc()分配内存空间并初始化
int* ptr = (int*)calloc(8, sizeof(int));
//malloc()分配内存空间并用 memset()初始化
int* ptr = (int*)malloc(8 * sizeof(int));
memset(ptr, 0, 8 * sizeof(int))
relloc其实也很好理解,只是有一些注意事项,relloc()函数的作用是对原来分配的内存空间进行扩展,也举个例子(下面两种写法是等价的)
//malloc()函数分配内存空间发现不够用,再申请一次大的并用 memcpy()函数进行数据转移
int* ptr1 = NULL;
int* ptr2 = NULL;
ptr1 = (int*)malloc(10 * sizeof(int));
//此处省略一系列赋值等操作,然后发现申请的空间不够用
ptr2 = (int*)malloc(20 * sizeof(int))//这是第二次申请内存空间,申请的空间比第一次多
memcpy(ptr2,ptr1,10);//将 ptr1 的数据复制到 ptr2 中
free(ptr1);//及时释放 ptr1 所指向的内存空间
//relloc()函数直接进行封装
int* ptr1 = NULL;
ptr1 = (int*)malloc(10 * sizeof(int));
//此处省略一系列赋值等操作,然后发现申请的空间不够用
ptr1 = (int*)realloc(ptr,20 * sizeof(int));//内存扩大了并且数据已经转移好了
最后!那么调用 realloc()函数的注意事项是什么呢?我把其列举为四点:
1、(10 * sizeof(int))这个意思是将原先的内存空间扩展到10个 int 型,而不是再申请10个 int 型内存空间
2、如果新分配的内存空间小于原先的内存空间,则可能导致数据丢失,请慎用
3、如果 ptr 参数为NULL(即没有进行赋值等一系列操作),则调用realloc()函数相当于调用malloc()函数
4、除非 ptr 参数为NULL,否则 ptr 的值必须是由先前的malloc()函数、calloc()函数、relloc()函数调用而来,若不满足则不能调用relloc()函数
小试牛刀:
编写一个程序,不断地接收用户输入的整数,直到用户输入-1表示结束,将所有的数据打印出来
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int i, num = 0;
int count = 0;
int* ptr = NULL;
do
{
printf("请输入一个整数(输入-1表示结束):");
scanf("%d",&num);
count++;
ptr = (int*)realloc(ptr, count * sizeof(int));
if (ptr == NULL)
{
exit(1);
}
ptr[count - 1] = num;
} while (num != -1);
printf("输入的整数分别是:");
for (i = 0; i < count; i++)
{
printf("%d ", ptr[i]);
}
free(ptr);
return 0;
}
input: 请输入一个整数(输入-1表示结束):5
请输入一个整数(输入-1表示结束):89
请输入一个整数(输入-1表示结束):123
请输入一个整数(输入-1表示结束):49
请输入一个整数(输入-1表示结束):-1
output:输入的整数分别是:5 89 123 49 -1
我之所以放出这道题是因为,这道题里面也包含动态数组,而且是用户输多少就是多少的动态数组,相比于之前 malloc()函数那到题的动态数组更为好用,原理都是利用指针往内存空间储存数据,所以对于新知识还是需要 多练 多探索 多发现 ( • ̀ω•́ )✧
Five、C语言的内存分布
先来看一下C语言中各种变量内存地址由底到高的分布规律 ↓
内存地址 | 类型 |
---|---|
高 | 局部变量 |
↑ | 动态申请的内存空间 |
| | 全局变量(未初始化) |
| | 静态变量(未初始化) |
| | 静态变量(初始化) |
| | 全局变量(初始化) |
| | 字符串常量 |
低 | 函数 |
接下来看一下典型C语言程序的内存空间是如何划分的 ↓
根据内存地址从高到低做如下划分:
- 堆(heap)
- 栈(stack)
- BSS(Block started by system)
- 数据段(data)
- 代码段(text)
1、代码段
代码段通常是指用来存放程序执行代码的一块内存区域
这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读
在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等
2、数据段
数据段通常用来存放已经初始化的全局变量和静态变量(全局、局部)
3、BSS
BSS 段通常是指用来存放程序中未初始化的全局变量的一块内存区域
这个区段中的数据在程序运行前将被自动初始化为数字 0
4、堆
前边我们学习了动态内存管理函数,使用它们申请的内存空间就是分配在这个堆里边
所以,堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩展或缩小
当进程调用 malloc 等函数分配内存时,新分配的内存就被动态添加到堆上;当利用 free 等函数释放内存时,被释放的内存从堆中被剔除
5、栈
大家平时可能经常听到堆栈这个词,一般指的就是这个栈
栈是函数执行的内存区域,通常和堆共享同一片区域
6、堆和栈的对比
堆和栈则是 C 语言运行时最重要的元素,下面我们将两者进行对比
申请方式:
堆由程序员手动申请
栈由系统自动分配
释放方式:
堆由程序员手动释放
栈由系统自动释放
生存周期:
堆的生存周期由动态申请到程序员主动释放为止,不同函数之间均可自由访问
栈的生存周期由函数调用开始到函数返回时结束,函数之间的局部变量不能互相访问
以堆举个例子
#include <stdio.h>
#include <stdlib.h>
int* fun(void)
{
int* ptr = NULL;
ptr = (int*)malloc(sizeof(int));
if (ptr == NULL)
{
exit(1);
}
*ptr = 520;
return ptr;
}
int main(void)
{
int* ptr = NULL;
ptr = fun();
printf("%d\n", *ptr);
free(ptr);
return 0;
}
output:520
印证了不同函数间都可以访问动态数组(堆)
注意:上述代码片中,在fun()函数中申请堆,在main函数中释放,这种做法在实际开发中是不可取的,应该在一个函数中进行申请和释放,上述这么用只是用来演示
发展方向:
堆和其它区段一样,都是从低地址向高地址发展
栈则相反,是由高地址向低地址发展
举个例子
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int* ptr1 = NULL;
int* ptr2 = NULL;
ptr1 = (int*)malloc(sizeof(int));
ptr2 = (int*)malloc(sizeof(int));
printf("stack:%p --> %p\n", &ptr1, &ptr2);//栈
printf("heap:%p --> %p\n", ptr1, ptr2);//堆
return 0;
}
output:stack:000000000062FE18 --> 000000000062FE10//地址变低了(栈)
heap:0000000000081400 --> 0000000000081420//地址变高了(堆)
//这是Dec—C++输出的结果,若是VS2019结果会不同(可能与其配置有关系)
打印栈用 & 号是因为局部变量在 栈 中,直接获取 栈 的地址
打印堆不用是因为此变量指向 堆,故获取 堆 的地址
打印结果印证了它们的发展方向
仔细看一下,会发现栈的地址减少了8个字节(一个指针的字节),而堆的地址增加了16个字节(地址按字节计数,是16进制),所有如果有环保性能这么一个评测标准的话,栈应该是更为 “环保” 的
再想一下,若调用 realloc()函数使指针变量重新分配内存空间,那么指针变量所指的位置会发生变化吗?如下
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int* ptr1 = NULL;
int* ptr2 = NULL;
int* ptr3 = NULL;
int* ptr4 = NULL;
ptr1 = (int*)malloc(sizeof(int));
ptr2 = (int*)malloc(sizeof(int));
printf("heap:%p --> %p\n", ptr1, ptr2);
ptr3 = (int*)realloc(ptr2, 2 * sizeof(int));
printf("heap:%p --> %p\n", ptr1, ptr3);
ptr4 = (int*)realloc(ptr3, 5 * sizeof(int));
printf("heap:%p --> %p\n", ptr1, ptr4);
free(ptr1);
free(ptr2);
free(ptr3);
free(ptr4);
return 0;
}
output: heap:00000000001D60D0 --> 00000000001D6120
heap:00000000001D60D0 --> 00000000001D6120//不变
heap:00000000001D60D0 --> 00000000001D7180//乘 4 的时候还不变,乘 5 时开始变
之所以检测 堆,是因为指针变量指向的地址是储存在堆里
可以看出:
如果新分配的空间足够存放,ptr2则不需要指向新的位置
如果新分配的内存比较大,则 ptr2 需要指向新的位置
注意是下面的区别!(新申请内存空间和扩展内存空间)
int* ptr1 = NULL;
int* ptr2 = NULL;
int* ptr3 = NULL;
int* ptr4 = NULL;
ptr1 = (int*)malloc(sizeof(int));
ptr2 = (int*)malloc(sizeof(int));
ptr3 = (int*)malloc(sizeof(int));
printf("heap:%p --> %p\n", ptr1, ptr2);
printf("heap:%p --> %p\n", ptr2, ptr3);
ptr4 = (int*)realloc(ptr2, 2 * sizeof(int));
printf("heap:%p --> %p\n", ptr1, ptr2);
output: heap:00000000009B60D0 --> 00000000009B6120//---->|
heap:00000000009B6120 --> 00000000009B6170// |往下(打印的内容一样)
heap:00000000009B60D0 --> 00000000009B6120//<----↓
得出结论:
新申请内存空间是一定要占用地址的
而扩展内存空间不一定会占用地址的
Six、高级宏定义
嵌套的宏定义
#include <stdio.h>
#define R 2.0
#define PI 3.14
#define S (PI * R)
int main(void)
{
printf("园的面积是:%f\n",S);
return 0;
}
output:园的面积是:6.280000
一般用大写字母组成宏的名字
带参数的宏定义
#include <stdio.h>
#define MAX(x,y) (((x) > (y) ? (x) : (y)))
int main(void)
{
int x = 5,y = 6;
printf("两个整数比较大的数是:%d",MAX(x,y))
return 0;
}
output:两个整数比较大的数是:6
下面这么写是错误的
#define MAX (x,y) (((x) > (y) ? (x) : (y))) //MAX 和(x,y)中隔了一个空格
注意:宏定义的参数部分一定要加括号,不然会出现下面情况
#include <stdio.h>
#define SQUARE(x) x * x
int main(void)
{
int x = 5;
printf("x的平方是:%d\n",SQUARE(x));
printf("x + 1 的平方是:%d\n",SQUARE(x + 1));
return 0;
}
output: x的平方是:25
x + 1 的平方是:11
因为宏定义是简单的替换,所以运行时是:5 + 1 * 5 + 1 == 5 + 5 + 1 == 11
Seven、内联函数
学了高级宏定义后,仔细看下面的代码片,看看有没有什么问题?
#include <stdio.h>
#define SQUARE(x) (x * x)
int main(void)
{
int i = 1;
while (i <= 10)
{
printf("打印%d的平方:%d\n", i - 1, SQUARE(i++));
}
return 0;
}
output: 打印2的平方:1
打印4的平方:9
打印6的平方:25
打印8的平方:49
打印10的平方:81
这是因为在宏定义中 i++ 用了两次,那么有什么解决方法吗?
#include <stdio.h>
inline int square(int x);
inline int square(int x)
{
return (x * x);
}
int main(void)
{
int i = 1;
while (i <= 10)
{
printf("打印%d的平方:%d\n", i - 1, square(i++));
}
return 0;
}
output:打印1的平方:1
打印2的平方:4
打印3的平方:9
打印4的平方:16
打印5的平方:25
打印6的平方:36
打印7的平方:49
打印8的平方:64
打印9的平方:81
打印10的平方:100
上面代码片定义了内联函数,即在函数定义的头前加上 inline 关键字
但是!!不加也行!因为编译器比我们更懂哪些函数应该内联,所以这个知识点我们知道就好了
Eight、一些花里胡哨的技巧
前提:符号 # 和 ## 都是预处理运算符
先举例说明
#include <stdio.h>
#define STR(s) # s
int main(void)
{
printf(STR(Hello %s num = %d\n),STR(CL),520);
return 0;
}
output: Hello CL num = 520
在带参数的宏定义中,符号 # 的作用就是把这个参数转换成一个字符串
若实参中存在多个空白字符则被替换成一个空格
若存在 " 符号则被替换 \ " 符号
若存在 \ 符号则被替换成 \ \ 符号
目的都是为了确保其变成字符串后内容原封不动
继续举例
#include <stdio.h>
#define LINK(s) x ## s
int main(void)
{
printf("%d\n",LINK(5,20));//在一个 %d 中完成格式化
return 0;
}
output:520
##被记作连接运算符
可变参数(这一节了解就好,我实在觉得多余
//原型
#define MORE(...) printf(#__VA_ARGS__)
. . . 代表使用可变参数,__VA_ARGS__在预处理中被实际的参数所替换
#include <stdio.h>
#define MORE(...) printf(#__VA_ARGS__)
int main()
{
MORE(Fish, 520, 3.14)
return 0;
}
output:Fish, 520, 3.14
没错!原型就是 . . . 和 #VA_ARGS ,一个符号都没变!多复杂啊(╯>д<)╯⁽˙³˙⁾
//原型
#define PRINT(format, ...) printf(#format, ##__VA_ARGS__)
若可变参数是空参数,##会将 format 后面的逗号 “吃掉” ,从而避免参数数量不一致的错误
#include <stdio.h>
#define PRINT(format, ...) printf(#format, ##__VA_ARGS__)
int main()
{
PRINT(num = %d\n, 520);
PRINT(Hello CL\n);
return 0;
}
output: num = 520
Hello CL
没错!原型还是. . . 和 ##VA_ARGS ,只是多了一个 # 符号啊(╯’ - ')╯︵ ┻━┻
如果你看不懂这一节(或懒得看,其实没什么关系,毕竟谁开发用这个花里胡哨的东西啊(╯°Д°)╯︵ ┻━┻
————————————————————————————————————————————
终于,终于写完了୧(๑•̀⌄•́๑)૭✧(笑
C语言有关函数的概念(2)算是正式结束了(੭•̀ω•́)੭ ̸*✩⁺˚,如果大家在阅读的过程中发现了什么问题,或者想补充些知识点,也请大家在评论区里面留言斧正,互相交流学习,我不胜感激~
最后再一次感谢大家ヾ( ̄▽ ̄)Bye ~ Bye~