七、更灵活的内存管理方式
- malloc
申请动态内存空间
- free
释放动态内存空间
- calloc
申请并初始化一系列内存空间
- realloc
重新分配内存空间
1、mallloc
函数原型:
void *malloc(size_t size);
malloc函数向系统申请分配size个字节的内存空间,并返回一个指向这块空间的指针。
如果函数调用成功,返回一个指向申请的内存空间的指针,由于返回类型是void 指针(void *),所以它可以被转换成任何类型的数据;如果函数调用失败,返回值是NULL。另外,如果size参数设置为0,返回值也可能是NULL,但并不意味着函数调用失败。
- 例:
# include<stdio.h> # include<stdlib.h> int main() { int *ptr; ptr = (void *)malloc(sizeof(int)); // ptr = (int *)malloc(sizeof(int)); //因为viod类型,可以转换为任意类型,写成int 可以提高可读性 if (ptr == NULL) { printf("分配内存失败!\n"); exit(1); } printf("请输入一个整数:"); scanf("%d", ptr); printf("你输入的整数是:%d", *ptr); return 0; }
输出:
请输入一个整数:6 你输入的整数是:6
2、free
函数原型:
void free(void *ptr);
free函数释放ptr参数指向的内存空间。该内存空间必须是由malloc、calloc或realloc函数申请的。否则,该函数将导致未定义行为。如果ptr参数是NULL,则不执行任何操作。
注意:该函数不会修改ptr参数的值,调用它后仍然指向原来的地方。(非法空间)
- 例:
# include<stdio.h> # include<stdlib.h> int main() { int *ptr; ptr = (void *)malloc(sizeof(int)); // ptr = (int *)malloc(sizeof(int)); //因为viod类型,可以转换为任意类型,写成int 可以提高可读性 if (ptr == NULL) { printf("分配内存失败!\n"); exit(1); } printf("请输入一个整数:\n"); scanf("%d", ptr); printf("free前,整数是:%d\n", *ptr); free(ptr); //比较释放前后的区别 printf("free后,整数是:%d\n", *ptr); return 0; }
输出:
请输入一个整数: 6 free前,整数是:6 free后,整数是:13435216
释放后就出现了“错误数值”。
内存泄漏
- 隐式内存泄漏
(用完内存块没有及时使用free函数释放)
- 丢失内存块地址
3、calloc
函数原型:
void *calloc(size_t nmemb, size_t size);
calloc函数在内存中动态地申请nmemb个长度为size的连续内存空间(申请的总空间尺寸为nmemb*size),这些内存空间全部被初始化为0。
- calloc函数与malloc函数的区别:
calloc函数在申请完内存后,自动初始化该内存空间为零。
malloc函数不进行初始化操作,里边的数据是随机的。
- 例:以下两种写法是等价的
//calloc()分配内存空间并初始化 int *ptr = (int*)calloc(8, sizeof(int)); //malloc() 分配内存空间并用meset()初始化 int *ptr = (int *)malloc(8 * sizeof(int)); memset(ptr, 0, 8 * sizeof(int));
4、realloc
函数原型:
void *realloc(void *ptr, size_t size);
以下几点是需要注意的:
- realloc函数修改ptr指向的内存空间大小为size字节
- 如果新分配的内存空间比原来的大,则就内存块的数据不会发生变化;如果新的内存空间大小小于旧的内存空间,可能会导致数据丢失。
- 该函数将移动内存空间的数据并返回新的指针
- 如果ptr参数为NULL,那么调用该函数就相当于调用malloc
- 如果调用size参数为0,并且ptr参数不为NULL,那么调用该函数就相当于调用free(ptr)
- 除非ptr参数为NULL,否则ptr的值必须由先前调用malloc、calloc或者realloc函数返回。
- 例:
# include <stdio.h> # include <stdlib.h> int main() { int i,num; int count = 0; int *ptr = NULL; // 注意,这里必须初始化为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); //直到num=-1,循环停止 printf("输入的整数分别是:"); for (i = 0;i < count;i++) { printf("%d ", ptr[i]); } putchar('\n'); //这里是单引号 return 0; }
输出:
请输入一个整数(输入-1代表结束):9 请输入一个整数(输入-1代表结束):0 请输入一个整数(输入-1代表结束):7 请输入一个整数(输入-1代表结束):-1 输入的整数分别是:9 0 7 -1
八、C语言的内存布局规律
- 代码段
代码段通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
- 数据段
通常用来存放已经初始化的去那句变量和局部静态变量。
- BSS段
通常是指用来存放程序中未初始化的全局变量的一块内存区域。这个区段中的数据在程序运行前将被自动初始化为数字0。
1、堆
堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩展或缩小。当进程调用malloc等函数分配内存时,新分配的内存就动态添加到堆上;当利用free等函数释放内存时,被释放的内存从堆中被删除。
2、栈
栈是函数执行的内存区域,通常和堆共享一片区域。
3、堆和栈的区别
- 申请方式:
堆由程序员手动申请
栈由系统自动分配
- 释放方式:
堆由程序员手动释放
栈由系统自动释放
- 生存周期:
堆的生存周期由动态申请到程序猿主动释放为止,不同函数之间均可自由访问
栈的生存周期由函数调用开始到函数返回时结束,函数之间的局部变量不能互相访问
- 发展方向:
堆是从低地址向高地址发展
栈是由高地址向低地址发展
九、宏定义
1、不带参数的宏定义
- 为了和普通的变量进行区分,宏的名字全部由大写字母组成。
- 宏定义只是简单地进行替换,并且由于预处理是在编译之前进行,而编译工作的任务之一就是语法检查,所以编译器不会堆宏定义进行语法检查。
- 宏定义不是说明或语句,在末尾不必加分号。
- 宏定义的作用域是从定义的位置开始到整个程序结束。
例:计算一个圆的面积
# include <stdio.h> #define PI 3.14 int main(){ int r = 4; float s; s = r * r * PI; printf("圆的面积是:%.2f", s); return 0; }
输出结果为:
圆的面积是:50.24
- 可以用#undef 来终止宏定义的作用域。
例:将上例加上#undef
# include <stdio.h> #define PI 3.14 int main(){ int r = 4; float s; #undef PI s = r * r * PI; printf("圆的面积是:%.2f", s); return 0; }
编译结果:
[Error] 'PI' undeclared (first use in this function) [Note] each undeclared identifier is reported only once for each function it appears in
可见,到计算面积的时候,PI并没有被定义。原因是#undef终结了作用域。
6. 宏定义允许嵌套。
2、带参数的宏定义
- 例:输出两个数中较大的那个
# include <stdio.h> #define MAX(x, y) (((x) > (y)) ? (x) : (y)) int main(){ int x, y; printf("请输入两个数:"); scanf("%d%d", &x, &y); printf("两个数中较大的那个是:%d", MAX(x,y)); return 0; }
输出结果为:
请输入两个数:9 8 两个数中较大的那个是:9
注意:MAX(x, y)这里的MAX和(x,y)中间不能有空格。
((x) > (y)) ? (x) : (y) //这里是条件语句,是不是有点忘记了呢?
3、补充:#和##运算符
- 在带参数的宏定义中,#运算符后面应该跟一个参数,预处理器会把这个参数转换成一个字符串
- 例:
# include <stdio.h> # define STR(s) #s int main(){ printf(STR(Hello %s), STR(World)); return 0; }
运算结果:
Hello World
- ##运算符被称为记号连接运算符,比如我们可以使用##运算符连接两个参数。
- 例:
# include <stdio.h> # define LINK(x, y) x ## y int main(){ printf("%d\n", LINK(5, 21)); return 0; }
输出结果:
521
十、内联函数
引入内敛函数来解决程序中函数调用的效率问题。
内联函数虽然节省了函数调用的时间消耗,但由于每一个函数出现的地方都要进行替换,因此增加了代码编译的时间。另外,并不是所有的函数都能变化曾内联函数。
现在的编译器也很聪明,就算你不写inline,它也会自动将一些函数优化成内联函数。
所以说,内联函数了解即可!
十一、结构体
1、结构体声明、定义、变量访问及初始化
可以使用结构体(Struct)来存放一组不同类型的数据。
- 结构体声明
struct 结构体名称 { 结构体成员1; 结构体成员2; 结构体成员3; ...... };
- 定义结构体类型变量
struct 结构体名称 结构体变量名
- 访问结构体变量
需要用到 .
运算符。比如book.title就是引用book结构体的title成员,它是一个字符数组。
- 例:设计一个简单的图书录入系统
# include <stdio.h> struct Book { char title[120]; char author[40]; float price; } ; int main(){ struct Book book; printf("请输入书名:"); scanf("%s", book.title); printf("请输入作者:"); scanf("%s", book.author); printf("请输入价格:"); scanf("%f", &book.price); printf("数据录入完成\n"); printf("书名:%s\n", book.title); printf("作者:%s\n", book.author); printf("价格:%.2f\n", book.price); return 0; }
输出结果:
请输入书名:《从容的底气》 请输入作者:林清玄 请输入价格:39.90 数据录入完成 书名:《从容的底气》 作者:林清玄 价格:39.90
- 初始结构体的指定成员值
例如,上例中,我们只初始化Book的price成员:
struct Book book = {.price = 50};
2、结构体数组
- 定义的第一种方法,在声明结构体的时候进行定义
struct 结构体名称 { 结构体成员; } 数组名[长度];
- 定义的第二种方法,先声明一个结构体类型,再用此类型定义一个结构体数组
struct 结构体名称 { 结构体成员; }; struct 结构体名称 数组名[长度];
- 初始化结构体数组
- 例如:
struct Book book[3] = { {"三体Ⅰ", "地球往事"}, {"三体Ⅱ", "黑暗森林"}, {"三体Ⅲ", "死神永生"} };
3、结构体指针
- 通过结构体指针访问结构体成员的两种方法
第一种:(*结构体指针).成员名 第二种:结构体指针->成员名
4、传递结构体变量
两个结构体变量能直接赋值,但要求数据类型要一致。
- 例:
# include <stdio.h> int main(){ struct Test{ int x; int y; }t1, t2; t1.x = 3; t1.y = 10; t2 = t1; // 将t1的值赋给t2 printf("t2.x = %d, t2.y = %d\n", t2.x, t2.y); return 0; }
输出:
t2.x = 3, t2.y = 10
- 例:一个小的图书录入系统
#include <stdio.h> struct Date //结构体声明 { int year; int month; int day; }; struct Book //结构体声明 { char title[120]; //定义一个字符数组 char author[40]; struct Date data; }; struct Book getInput(struct Book book); void printBook(struct Book book); //函数声明 struct Book getInput(struct Book book) //录入信息子函数,该函数的返回值是一个结构体,参数也是一个结构体 ,只要是机构提,就要有struct关键字 { printf("请输入书名:"); scanf("%s", book.title); printf("请输入作者:"); scanf("%s", book.author); printf("请输入购买日期:"); scanf("%d-%d-%d", &book.data.year, &book.data.month, &book.data.day); return book; } void printBook(struct Book book) //打印子函数 { printf("书名:%s\n", book.title); printf("作者:%s\n", book.author); printf("购买日期:%d-%d-%d\n", book.data.year, book.data.month, book.data.day); } int main() { struct Book b1, b2; //定义结构体变量 printf("请输入第一本书的信息:\n"); b1 = getInput(b1); putchar('\n'); printf("请输入第二本书的信息:\n"); b2 = getInput(b2); printf("\n\n现在信息已录入完毕,开始打印验证\n\n"); printf("打印第一本书的信息:\n"); printBook(b1); printf("打印第二本书的信息:\n"); printBook(b2); return 0; }
输出结果为:
请输入第一本书的信息: 请输入书名:《远方的星》 请输入作者:星 请输入购买日期:2021-7-27 请输入第二本书的信息: 请输入书名:《星》 请输入作者:远方的星 请输入购买日期:2021-7-27 现在信息已录入完毕,开始打印验证 打印第一本书的信息: 书名:《远方的星》 作者:星 购买日期:2021-7-27 打印第二本书的信息: 书名:《星》 作者:远方的星 购买日期:2021-7-27
5、传递指向结构体变量的指针。
- 例:将上例进行简单修改
#include <stdio.h> struct Date { int year; int month; int day; }; struct Book { char title[120]; char author[40]; struct Date data; }; void getInput(struct Book *book); void printBook(struct Book *book); void getInput(struct Book *book) //由于用到指针,这里就不需要返回值了 { printf("请输入书名:"); scanf("%s", book->title); printf("请输入作者:"); scanf("%s", book->author); printf("请输入购买日期:"); scanf("%d-%d-%d", &book->data.year, &book->data.month, &book->data.day); //因为前面没有把data定义为指针,所以data.year等地方不需要修改 } void printBook(struct Book *book) { printf("书名:%s\n", book->title); printf("作者:%s\n", book->author); printf("购买日期:%d-%d-%d\n", book->data.year, book->data.month, book->data.day); } int main() { struct Book b1, b2; //定义结构体变量 printf("请输入第一本书的信息:\n"); getInput(&b1); //传入地址 putchar('\n'); printf("请输入第二本书的信息:\n"); getInput(&b2); printf("\n\n现在信息已录入完毕,开始打印验证\n\n"); printf("打印第一本书的信息:\n"); printBook(&b1); printf("打印第二本书的信息:\n"); printBook(&b2); return 0; }
结果与上例一致。
6、动态申请结构体
使用malloc函数为结构体分配存储空间
例:对上例继续做简单的改变
#include <stdio.h> #include <stdlib.h> //exit需要的头文件 struct Date { int year; int month; int day; }; struct Book { char title[120]; char author[40]; struct Date data; }; void getInput(struct Book *book); void printBook(struct Book *book); void getInput(struct Book *book) //由于用到指针,这里就不需要返回值了 { printf("请输入书名:"); scanf("%s", book->title); printf("请输入作者:"); scanf("%s", book->author); printf("请输入购买日期:"); scanf("%d-%d-%d", &book->data.year, &book->data.month, &book->data.day); //因为前面没有把data定义为指针,所以data.year等地方不需要修改 } void printBook(struct Book *book) { printf("书名:%s\n", book->title); printf("作者:%s\n", book->author); printf("购买日期:%d-%d-%d\n", book->data.year, book->data.month, book->data.day); } int main() { struct Book *b1, *b2; //定义结构体变量 b1 = (struct Book *)malloc(sizeof(struct Book)); //强制转换为一个指向Book的指针 b2 = (struct Book *)malloc(sizeof(struct Book)); if (b1 == NULL || b2 == NULL) { printf("内存分配失败!\n"); exit(1); } printf("请输入第一本书的信息:\n"); getInput(b1); putchar('\n'); printf("请输入第二本书的信息:\n"); getInput(b2); printf("\n\n现在信息已录入完毕,开始打印验证\n\n"); printf("打印第一本书的信息:\n"); printBook(b1); printf("打印第二本书的信息:\n"); printBook(b2); free(b1); free(b2); //最后不能忘记释放 return 0; }
结果仍然一致!
十二、未完待续
这一阶段的笔记就先记录到这里,下一篇,应该是最后一篇啦,加油!
路漫漫其修远兮,吾将上下而求索。
大家一起加油吧!