(萌新笔记)C语言有关函数的概念(2)

目前正在学习函数的相关概念,期间将此博客当做笔记本来用,因此本文可能会出现很多基本知识点,但是也可能会有 只有初学者会关注到的知识点,所以各位伙伴们斟酌观看吧 ̋(๑˃́ꇴ˂̀๑)

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~

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值