1. 栈
通常我们说的堆栈有两种形式,即:
- 数据结构场景下,堆与栈表示两种常用的数据结构
- 程序内存布局场景下,堆与栈表示两种内存管理方式
这两种栈在含义上略有不同,但是其核心思想和理念是相同的,即先进后出,如下图所示:
1.1 数据结构中的栈
具有先进后出的性质,有两种实现方式,一种是静态栈,一种是动态栈。
静态栈是一种连续储存的数据结构(数组实现),动态栈是非连续存储的数据结构(链表实现)。通常在数据结构中的操作有入栈(压栈),出栈,初始化,清空栈。想要读取栈
中的某个元素,就是将其之间的所有元素出栈才能完成。
1.1.1 静态栈
静态栈的顺序存储方式使它成为一种运算受限的顺序表,因为栈的大小是固定的,静态栈的示意图如下:
利用一维数组依次从栈底存放,由于对栈的操作只能在栈顶进行,所以必须要有栈顶的标志。 首先,定义一个有固定容量的栈空间,其结构描述为
#define maxsize 100
typedef struct
{
/* 栈顶标志 */
int top;
/* 栈中的结点存储在一维数组中 */
int data[maxsize];
}stack;
对静态栈的基本操作为置空栈,判空栈,返回栈顶数据,入栈,出栈等
/* -置空栈 */
void SetEmpty(stack var_s)
{
var_s.top = -1;
}
/* -判空栈 */
int IsEmpty(stack var_s)
{
if (var_s.top == -1)
{
return TRUE; // 空栈返回真
}
else
{
return FALSE; // 非空返回假
}
}
/* 取栈顶结点值 */
int GetTopValue(stack var_s)
{
if(IsEmpty(var_s))
{
printf("当前栈为空\n");
}
else
{
return var_s.data[var_s.top];
}
}
/* -入栈 */
int Push(stack var_s,int var_data)
{
if (var_s.top == maxsize-1)
{
printf("栈已满!\n");
return FALSE;
}
else
{
var_s.data[++var_s.top] = var_data;
return TRUE;
}
}
/* -出栈 */
int Pop(stack var_s)
{
if(IsEmpty(var_s))
{
printf("当前栈为空\n");
}
else
{
return var_s[var_s.top--];
}
}
简单的举个例子,利用栈的数据结构可以实现逆序,例如可以实现十进制转化为二进制
1.1.2 动态栈
动态栈简称链表栈,栈中每个结点都是动态分配的,入栈相当于在单链表的尾插,出栈相当于单链表的尾删,可以理解为尾部就是栈顶。(用visio画图太麻烦了,手画了一下,区分头指针与头结点,在链表中通常的输入参数为头指针)
不理解链表的同学,请网上自行查找相关资料,这里直接上代码(静态栈的代码,我没验证,动态栈的我验证了)。
link_stack.h为下面代码
#include <stdio.h>
#include <stdlib.h>
/* -SECTION_1 自定义类型 */
#define uint_8 unsigned char
#define uint_16 unsigned int
#define uint_32 unsigned long
#define TRUE 1
#define FALSE 0
/* -SECTION_2 动态栈相关定义 */
typedef struct node
{
uint_16 data; // 结点中的数据
struct node *pnext; // 结点中的指针域
}link_stack;
extern link_stack *init_stack(link_stack *phead);
extern link_stack * set_null(link_stack *phead);
extern uint_8 is_null(link_stack *phead);
extern void showall(link_stack *phead);
link_stack.c为下面代码
#include <stdio.h>
#include <stdlib.h>
#include "list_stack.h"
//link_stack *phead = NULL; // 头指针
/* -初始化 */
link_stack* init_stack(link_stack *phead)
{
return NULL;
}
/* -置空,输入参数:头指针(头部删除不符合动态栈的描述,仅仅是为了置空) */
link_stack * set_null(link_stack *phead)
{
link_stack *p1=phead,*p2=NULL;
while(p1 != NULL)
{
p2 = p1;
p1 = p1->pnext;
free(p2);
}
return p1;
}
//void set_null(link_stack **phead) // 也可以使用二级指针
//{
// link_stack *p1=NULL;
//
// while(*phead != NULL)
// {
// p1 = *phead;
// *phead = (*phead)->pnext;
// free(p1);
// }
//}
/* -判空 */
uint_8 is_null(link_stack *phead)
{
if (phead == NULL)
{
return TRUE;
}
else
{
return FALSE;
}
}
/* -入栈,因为要改变头结点,所以返回的是指针 */
link_stack* push(link_stack *phead,uint_16 var_data)
{
link_stack *pnewnode = (link_stack*)malloc(sizeof(link_stack));
pnewnode->data = var_data;
pnewnode->pnext = NULL;
if(is_null(phead))
{
phead = pnewnode;
}
else
{
link_stack *p = phead;
while(p->pnext != NULL )
{
p = p->pnext;
}
p->pnext = pnewnode;
}
return phead;
}
/* -出栈 */
link_stack* pop(link_stack *phead,link_stack *p_out_node)
{
if (is_null(phead))
{
printf("空栈\n");
return NULL;
}
else if(phead->pnext == NULL)
{
p_out_node->data = phead->data;
free(phead);
phead = NULL;
return phead;
}
else
{
link_stack *p =phead;
while (p->pnext->pnext != NULL)
{
p = p->pnext;
}
p_out_node->data = p->pnext->data;
free(p->pnext);
p->pnext = NULL;
return phead;
}
}
/*****
** 函 数 名:显示结点的所有数据
** 输入参数:
** 1,ST *phead,头结点,即第一个结点的地址
** 输出参数:无
** 备 注:无
*****/
void showall(link_stack *phead)
{
uint_16 i = 1;
/* 遍历所有结点 */
while(phead != NULL)
{
printf("\n结点_%d, 数据为%d",i,phead->data);
printf("\t本结点地址%p,下一结点地址%p",phead,phead->pnext);
phead = phead->pnext;
i++;
}
}
main.c为
void test_1(void)
{
/* -初始化 */
link_stack *head = NULL; //头指针
//对数据操作有分配空间
link_stack *pop_data = (link_stack*)malloc(sizeof(link_stack));
head = init_stack(head);
head = push(head,1);
head = push(head,2);
head = push(head,3);
head = push(head,4);
/* 显示所有结点 */
showall(head);
printf("\n\n");
//pop_data = head;
head = pop(head,pop_data);
showall(head);
printf("\n\n");
head = set_null(head);
/* 显示所有结点 */
showall(head);
}
/* -链表栈练习 */
void main()
{
test_1();
system("pause"); // 暂停
}
写代码的时候,因为输入参数的头指针属于一级指针,在函数内部尽管我改变了指针的指向,但是实参并没有发生改变,因此有两种方法可以做,一种是属于二级指针(一级指针的地址),一直中返回地址,然后直接改变实参的指向。一级指针和二级指针做形参可以参考我的博客:c语言_指针的理解
1.2 内存管理的栈
栈的生成方式是从高地址到低地址,即先分配的变量存在高地址,后分配的变量在高地址,请允许我测试时,用递归小浪一下,
void stackDrec()
{
static char *addr = NULL;
char dummy;
if (addr == NULL)
{
addr = &dummy;
stackDrec();
}
else
{
if (&dummy > addr)
{
printf("向高地址方向生长,dummy: %d,addr: %d\n",&dummy,addr);
}
else
{
printf("向低地址方向生长,dummy:%d,addr:%d\n", &dummy, addr);
}
}
}
void main()
{
stackDrec();
system("pause");
}
所以,我们当我们写程序时,例如我们定义了N个局部变量;
void test(void)
{
int i_1; // 第一个入栈,在栈底
int i_2; // 第2个入栈
int i_3; // 第3个入栈
.
.
.
int i_n; // 第n个入栈,当程序运行完成,第一个被释放,即出栈
}
典型的存储结构如图所示:
2. 大小端
在我们的计算机系统中,数据的存储是以字节为单位的,每个地址单元都对应着一个字节,所以我们可以理解成为一个字节的内存空间绑定一个地址。
在C语言中,我们定义的变量存储在内存中,如我们定义int i =1,编译器会在栈中分配一段4字节的内存空间给这个局部变量使用,我们不禁会想,在内存中存储模型是下面两者中的哪一个呢?
由此引入了大小端的问题,大端就是低字节排放在内存的低端,高位字节排放在内存的高端; Big Endian就是高位字节排放在内存的低端,低位字节排放在内存的高端。大小端是由处理器决定的。
/* -内存管理 */
void main()
{
int a =0xaabbccdd;
printf("%p",&a);
system("pause"); // 暂停
}
可以看到我们显示的结果是小端
有时候我们用a的首地址&a对变量a进行操作,我们可能要问a的首地址是指-0x0032FE28还是0x0032FE2B呢?要记住一个结论:无论为大端、小端,变量的首地址都为低内存地址
故当我们定义变量时,变量的首地址是低内存地址,所以通常面试中,我们可以通过首地址(指针)来测试大小端的代码:
int Check_sys()
{
int a = 1;
//char* p = (char*)&a;
//return *p; //大端返回0,小端返回1
//还可以写成下面的方式:
return *(char*)&a;
}
int main()
{
int ret=Check_sys(); //写一个测试的函数
if (1 == ret)
{
printf("当前模式为小端存储\n");
}
else
{
printf("当前模式为大端存储\n");
}
return 0;
}
还有一种方法是,根据共用体来测试大小端,代码为:
int checkCPU()
{
union w
{
int a;
char b;
}c;
c.a = 1;
return (c.b == 1); // 小端返回TRUE,大端返回FALSE
}
附加:
在 C 语言中,sizeof() 是一个判断数据类型或者表达式长度的运算符。不如下面的代码,我们定义int a,那么编译器会自动根据我们定义的类型给我们分配4字节的内存空间,这是编译器自动完成的,不需要我们操作。这里需要注意的一点是&a分配的内存大小一直是4字节。
void main(void)
{
/*int a;*/
double a;
/*char a; */
double* p = &a; // p的类型为double*型的
printf("%d\n", sizeof(a)); // 结果依次为 4,8,1
printf("%d\n", sizeof(&a)); // 结果依次为 4,4,4
printf("%d\n", sizeof(p)); // 结果依次为 4,4,4,
system("pause");
}