C语言万字基础笔记总结(三)下

简介: 简介:C语言万字基础笔记总结(三)下

七、更灵活的内存管理方式


  • 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语言的内存布局规律

image.png

image.png

  • 代码段

代码段通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

  • 数据段

通常用来存放已经初始化的去那句变量和局部静态变量。

  • BSS段

通常是指用来存放程序中未初始化的全局变量的一块内存区域。这个区段中的数据在程序运行前将被自动初始化为数字0。

1、堆

堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩展或缩小。当进程调用malloc等函数分配内存时,新分配的内存就动态添加到堆上;当利用free等函数释放内存时,被释放的内存从堆中被删除。

2、栈

栈是函数执行的内存区域,通常和堆共享一片区域。

3、堆和栈的区别

  • 申请方式:

堆由程序员手动申请

栈由系统自动分配

  • 释放方式:

堆由程序员手动释放

栈由系统自动释放

  • 生存周期:

堆的生存周期由动态申请到程序猿主动释放为止,不同函数之间均可自由访问

栈的生存周期由函数调用开始到函数返回时结束,函数之间的局部变量不能互相访问

  • 发展方向:

堆是从低地址向高地址发展

栈是由高地址向低地址发展


九、宏定义


1、不带参数的宏定义

  1. 为了和普通的变量进行区分,宏的名字全部由大写字母组成。
  2. 宏定义只是简单地进行替换,并且由于预处理是在编译之前进行,而编译工作的任务之一就是语法检查,所以编译器不会堆宏定义进行语法检查。
  3. 宏定义不是说明或语句,在末尾不必加分号。
  4. 宏定义的作用域是从定义的位置开始到整个程序结束。
    例:计算一个圆的面积
# 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
  1. 可以用#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; 
}

结果仍然一致!


十二、未完待续


这一阶段的笔记就先记录到这里,下一篇,应该是最后一篇啦,加油!

路漫漫其修远兮,吾将上下而求索。

大家一起加油吧!

相关文章
|
6月前
|
存储 人工智能 算法
【C语言】自学终极笔记
【C语言】自学终极笔记
107 0
|
3月前
|
测试技术 C语言 C++
【C语言刷题训练——6】鹏哥C语言刷题训练营笔记,含代码讲解改进
【C语言刷题训练——6】鹏哥C语言刷题训练营笔记,含代码讲解改进
|
3月前
|
存储 C语言
【C语言】鹏哥C语言刷题训练营——第5节内容笔记(含代码全面分析和改进,讲解)
【C语言】鹏哥C语言刷题训练营——第5节内容笔记(含代码全面分析和改进,讲解)
|
5月前
|
C语言
|
5月前
|
C语言
|
6月前
|
C语言
C语言(指针详解)重点笔记:指针易错点,都是精华
C语言(指针详解)重点笔记:指针易错点,都是精华
75 0
|
存储 编译器 C语言
C语言笔记第03章:数组(四)
C语言笔记第03章:数组
97 0
|
编译器 C语言 索引
C语言笔记第03章:数组(一)
C语言笔记第03章:数组
104 0
|
6月前
|
NoSQL Redis C语言
|
C语言
C语言初阶 牛客网刷题笔记(将持续更新..)
C语言初阶 牛客网刷题笔记(将持续更新..)
C语言初阶 牛客网刷题笔记(将持续更新..)