二. 征服C指针:C如何使用内存

虚拟内存

现代OS都会给应用程序每个进程分配独立的虚拟地址空间。这样做的目的是为了保证安全,防止应用程序破坏内存空间。。这和 C 语言本 身并没有关系,而是操作系统和 CPU 协同工作的结果。即:应用程序面对的是虚拟地址空间。

在这里插入图片描述

作用域

有如下三种:

  1. 全局变量
    在函数外定义的变量,默认就是全局变量。全局变量在任何地方都是可见的。当程序被分割到多个源码文件进行编译时,声明为全局变量的变量也是可以从其他源码文件中引用。

  2. 文件内部的静态变量
    同样是定义在函数外部,一旦有static修饰,就变成了文件内部的静态变量,作用域就仅限定在当前所在的源代码文件中。通过static修饰的变量(包括函数),对于其他源代码是不可见的。

  3. 局部变量
    指在函数中声明的变量。通常在语句块结束的时候释放。

存储期(storage duration)

变量除了作用域的不同,还有存储期的不同。

  1. 静态存储期(static storage duration)
    全局变量,文件内的static变量以及用static修饰的局部变量都是静态存储期,都可以称为静态变量。注意:静态变量的寿命从程序运行时开始,到程序关闭时结束,即静态变量一直存在于内存的同一个地址上。

  2. 自动存储期(auto storage duration)
    没有指定static的局部变量,持有自动存储期。这样的变量称为自动变量,这样的变量是程序进入函数时统一进行内存区域分配的。这个特性使用“栈”来实现。

  3. 通过malloc()动态分配的内存
    它的寿命一直延续到使用free()释放它为止。

print_addr.c示例,演示各种不同作用域,存储期变量的指针地址

/**
 * 演示不同作用域、存储期的变量在内存中的地址分布
 * @author liu
 */

#include <stdio.h>
#include <stdlib.h>
  
// 全局变量
int global_variable;

// 文件内静态变量
static int file_static_variable;

void func1()
{
  // 局部变量
  int func1_variable;

  // 局部静态变量
  static int func1_static_variable;

  printf("&func1_variable.. %p\n", &func1_variable);
  printf("&func1_static_variable.. %p\n", &func1_static_variable);

}


void func2()
{
  int func2_variable;
  printf("&func2_variable..%p\n", &func2_variable);
}


int main(int argc, char *argv[])
{
  int *p;

  // 打印两个函数指针
  printf("&func1..%p\n", func1);
  printf("&func2..%p\n", func2);

  // 打印字符常量指针
  printf("string literal..%p\n", "abc");

  // 打印全局变量指针
  printf("&global_variable..%p\n", &global_variable);

  // 打印文件内静态变量指针
  printf("&file_static_variable...%p\n", &file_static_variable);

  // 执行两个函数,打印局部变量和局部静态变量
  func1();
  func2();

  // 用malloc函数分配内存块
  p = malloc(sizeof(int));
  
  // 打印malloc分配内存块指针
  printf("malloc address..%p\n", p);

  return 0;
}

result:

&func1..0x4005d6
&func2..0x40060b
string literal..0x4007d8
&global_variable..0x601038
&file_static_variable...0x601030
&func1_variable.. 0x7ffc1d1fd04c
&func1_static_variable.. 0x601034
&func2_variable..0x7ffc1d1fd04c
malloc address..0xd246b0


可以看出函数体,字符串分配在一起, 静态变量(全局变量,文件内静态变量,静态局部变量)分配在一起, 局部变量分配在一起,最后malloc内存块再单独的位置。

其中func1_variable和func2_variable完全一样,正好在同一个地址分配,因为func1()和func2()是前后调用,func1_variable是先释放完再分配给func2_variable。

它们在内存空间的分布大概是这样:

在这里插入图片描述

函数和字符串常量
只读内存区域

函数(即程序自身)和字符串常量被配置在内存里相邻的地址上。如今大多数OS都是将函数和字符串常量存储在一个只读内存区域。原因是两者本身都不需要再改写。另外在同一份程序被启动多次时,这样能能不同进程之间共享以实现节约物理内存。

指向函数的指针

函数在表达式中可以解读为指向函数的指针。所以本质上也是指针,可以赋给指针型变量。示例如下:

#include <stdio.h>
  
/**
 * 演示函数型指针的赋值,使用
 *
 */

void printhello()
{
  puts("hello world c programming");
}


int main(int argc, char *argv[])
{
  void (*fun)() = printhello;


  fun();
  return 0;
}

静态变量
什么是静态变量

即从程序开始运行到结束持续存在的变量,所以静态变量总是在虚拟地址空间上占据固定的区域。静态变量前一节已经描述过,即:全局变量,文件内static变量以及static局部变量。这些静态变量作用域各不相同,编译和连接时具有不同的意义,但是在运行时都是以相似的方式被使用。

分割编译和连接

一定规模的C语言工程源代码一定是分割到多个文件中,这些源文件可以各自编译最后连接起来。函数和全局变量,如果名称相同,即使跨越了多个源文件也被作为相同的对象来看待,进行这项工作的是一个被称为“链接器”的程序。

在这里插入图片描述
为了在链接器中将名称结合起来,各目标代码大多都具备一个符号表(symbol table)。如在UNIX中,可以使用nm命令窥视符号表的内容。
可以使用 gcc -c xx.c 生成名称为xx.o的目标代码,再使用nm xx.o窥探符号表。示例如下:

#include <stdio.h>
  
int global;
int static static_global;

int main(int argc, char *argv[])
{
  int local;
  int static static_local;

  return 0;
}

在这里插入图片描述

可以看出全局变量使用C标记,文件内静态变量, static局部变量标记为b, 局部变量local符号表中没有出现。

局部变量local没有出现好理解,它的作用域是main函数范围,不需要跟其他文件链接,并且它的地址是在运行时决定,不属于链接器的范围。

文件内static变量和static局部变量它们的作用域也限制在文件内为什么也出现在符号表中?原因是静态变量必须提前要给它们分配一些地址。

链接器就是根据这些符号表信息,给这些只是名称的对象分配地址。

自动变量(stack: 栈)
内存区域重复使用
&func1_variable.. 0x7ffc1d1fd04c
&func2_variable..0x7ffc1d1fd04c

前面的示例演示了两个局部变量分配在了同样的内存地址,所以该区域是重复利用的。当局部变量所在的函数执行完成之后,自动变量所在的内存空间就释放了。每次分配给自动变量的内存地址都不是固定的。

函数调用

函数调用本质上一个栈结构,在一个函数中调用另一个函数即压栈,函数调用完即出栈。

示例如下:
在这里插入图片描述

参数在栈中堆积方式:
在这里插入图片描述

基本的思想是:

  1. 在调用方,函数参数从后向前堆积在栈中,即假设函数有两个参数,第二个参数先入栈,第一个参数后入栈。原因后面会讲。
  2. 和函数相关联的返回信息(返回地址等)也被堆积在栈中,返回地址即函数返回后应该返回的地址。
  3. 跳转到作为被调用对象的函数地址。
  4. 栈为当前函数所使用的自动变量增长所需大小的内存区域。
  5. 在函数的执行过程中,为了进行复杂的表达式运算,有时候会将计算过程中的值 放在栈中。
  6. 一旦函数调用结束,局部变量占用的内存区域就被释放,并且使用返回信息返回 到原来的地址。
可变长参数

C采用函数参数从后往前往栈中堆积,这个做法跟java语言相反,看上去也违反习惯。 C这么实现的目的是实现可变参数列表的功能,如最常见的printf函数,参数列表是可变的。如:prinf("%d, %s\n", 100, “hello”);

调用支持可变参数列表函数的栈结构:
在这里插入图片描述
这样即使可变参数列表再长,也能顺利找到第一个参数的地址,再依次取出可变参数列表。

自定义一个可变列表函数
#include <stdio.h>
#include <stdarg.h>
#include <assert.h>

/**
 * 自定义的printf,实现可变参数列表效果
 */
void tiny_printf(char *format, ...)
{
  va_list ap;

  va_start(ap, format);

  for (int i = 0; format[i] !='\0'; i++) {
    switch (format[i]) {
      case 's':
        printf("%s", va_arg(ap, char*));
        break;

      case 'd':
        printf("%d", va_arg(ap, int));
        break;

      default:
        assert(0);
    }
  }

  va_end(ap);

  putchar('\n');
}


int main(int argc, char *argv[])
{
  tiny_printf("sddddd", "result:", 1, 2, 3, 4, 5);
  return 0;
}
递归调用

递归调用是指对函数自身的调用。C在栈中分配自动变量内存,该区域可重复使用,采用该结构可方便实现递归调用。

现实中递归调用常常用来实现树结构遍历,图结构遍历,快速排序等。

快速排序(quicksort)基本思路:

  1. 从需要排序的序列中找到一个基准值(pivot)
  2. 将需要排序的数据按照小于pivot和大于pivot进行分类
  3. 对分类后的两组数据再进行1步骤和2步骤的处理

大体算法思路是:

  1. 从左到右,检索比pivot大的数据
  2. 从右到左,检索比pivot小的数据
  3. 如果两个方向都能检索到数据,执行交换
  4. 重复1-3的步骤,直到左右检索下标冲突为止

实现快速排序的源代码如下:

#include <stdio.h>
  
/**
 * 快速排序算法C语言实现
 *
 */
void quick_sort_sub(int *data, int left, int right)
{
  int left_index = left;
  int right_index = right;

  int pivot = data[(left + right) / 2];

  while (left_index <= right_index) {
    // 两边开始遍历
    for (; data[left_index] < pivot; left_index ++)
      ;
    for (; data[right_index] > pivot; right_index --)
      ;

    // 符合条件的交换顺序
    if (left_index <= right_index) {
      int tmp = data[left_index];
      data[left_index] = data[right_index];
      data[right_index] = tmp;

      left_index ++;
      right_index --;
    }
  }

  // 基准点左侧的递归执行排序
  if (right_index > left) {
    quick_sort_sub(data, left, right_index);
  }

  // 基准点右边的递归执行排序
  if (left_index < right) {
    quick_sort_sub(data, left_index, right);
  }
}


void quick_sort(int *data, int size)
{
  quick_sort_sub(data, 0, size - 1);
}

int main(int argc, char *argv[])
{
  int a[] = {4, 3, 67, 1, 0, -5, 45, 23, 11, 34, 10};


  quick_sort(a, 11);

  for (int i = 0; i < 11; i ++) {
    printf("a[%d]: %d, ", i, a[i]);
  }

  puts("");

}
利用malloc()来进行动态内存分配(heap: 堆)

malloc()可以根据参数指定的尺寸来分配内存块,它将返回指向内存块初始位置的指针,通常用于动态分配结构体的内存。

int *p = malloc(size);
free(p);

以上是mallc使用的基本方式。

像这样能动态的进行内存分配,并且可以通过任意的顺序释放的内存区域,称为heap。在英文中heap是指像山一样堆得高高的东西(如干草堆),malloc()就像是从内存堆成的山上扣了一块内存。

在链表结构中malloc用的很广泛,示例:

#include <stdio.h>
#include <stdlib.h>

typedef struct struct_node {
 char *name;
 struct struct_node *next;
} Node;


void printLinkedlist(Node *head)
{

  Node *next = head -> next;

  while(next) {
    printf("Node: %s\n", next -> name);
    next = next -> next;
  }
}





int main(int argc, char *argv[])
{
  Node *head = malloc(sizeof(Node));

  Node *node = malloc(sizeof(Node));
  node -> name = "1";

  head -> next = node;

  printLinkedlist(head);

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值