open-courses
  • 公开课笔记
  • CMU 15-445/645 Database Systems
    • Relational Data Model
    • Advanced SQL
    • Database Storage
    • Buffer Pools
    • Hash Tables
    • Tree Indexes
    • Index Concurrency Control
    • Query Processing
    • Sorting&Aggregations
    • Join Algorithms
    • Query Optimization
    • Parallel Execution
    • Embedded Database Logic
    • Concurrency Control Theory
    • Two Phase Locking
    • Timestamp Ordering Concurrency Control
    • Multi-Version Concurrency Control
    • Logging Schemes
    • Database Recovery
    • Introduction to Distributed Databases
    • Distributed OLTP Databases
    • Distributed OLAP Databases
  • UCB - CS162
    • OS intro
    • Introduction to the Process
    • Processes, Fork, I/O, Files
    • I/O Continued, Sockets, Networking
    • Concurrency: Processes & Threads
    • Cooperating Threads, Synchronization
    • Semaphores, Condition Variables, Readers/Writers
    • Scheduling
    • Resource Contention & Deadlock
    • Address Translation, Caching
    • File System (18,19,20)
    • Distributed Systems, Networking, TCP/IP, RPC (21,22)
    • Distributed Storage, Key-Value Stores, Security (23)
    • Security & Cloud Computing (24)
    • Topic: Ensuring Data Reaches Disk
  • MIT - 6.006
    • Sequence and Set Interface
    • Data Structure for Dynamic Sequence Interface
    • Computation Complexity
    • Algorithms and Computation
    • Structure Of Computation
    • Graph & Search
    • Tree & Search
    • Weighted Shortest Paths
    • String Matching, Karp-Rabin
    • Priority Queue Interface & Implementation
    • Dictionary Problem & Implementation
    • Sorting
    • Dynamic Programming
    • Backtracking
    • Self-Balancing Tree
  • MIT - 6.824
    • 2PC & 3PC
    • Introduction and MapReduce
    • RPC and Threads
    • Primary/Backup Replication
    • Lab: Primary/Backup Key/Value Service
    • Google File System (GFS)
    • Raft
    • Lab: Raft - Leader Election
    • Lab: Raft - Log Replication
  • Stanford-CS107
    • 原始数据类型及相互转化
    • 指鹿为马
    • 泛型函数
    • 泛型栈
    • 运行时内存结构
    • 从 C 到汇编
    • 函数的活动记录
    • C 与 C++ 代码生成
    • 编译的预处理过程
    • 编译的链接过程
    • 函数的活动记录续、并发
    • 从顺序到并发和并行
    • 信号量与多线程 1
    • 信号量与多线程 2
    • 复杂多线程问题
    • 函数式编程 - Scheme 1
    • 函数式编程 - Scheme 2
    • 函数式编程 - Scheme 3
    • 函数式编程 - Scheme 4
    • 函数式编程 - Scheme 5
    • Python 基础
  • MIT - 6.001 - SICP
    • 什么是程序
    • 程序抽象
    • 替代模型
    • 时间/空间复杂度
    • 数据抽象
    • 高阶函数
    • Symbol
    • 数据驱动编程与防御式编程
    • 数据抽象中的效率与可读性
    • 数据修改
    • 环境模型
    • 面向对象-消息传递
    • 面向对象 - Scheme 实现
    • 构建 Scheme 解释器
    • Eval-Apply Loop
    • Normal Order (Lazy) Evaluation
    • 通用机
    • 寄存器机器
    • 子程序、栈与递归
    • 在寄存器机器中执行
    • 内存管理
  • MIT - 6.046
    • Randomized Algorithms
    • Skip Lists
  • System Design
    • Twitter
    • Cache Consistency & Coherence
  • DDIA 笔记
    • Replication
    • Transactions
    • The Trouble with Distributed Systems
    • Consistency & Consensus
  • Papers We Love
    • Consistent Hashing and Random Trees (1997)
    • Dynamic Hash Tables (1988)
    • LFU Implementation With O(1) Complexity (2010)
    • Time, Clocks, and the Ordering of Events in a Distributed System (1978)
    • Dapper, a Large-Scale Distributed Systems Tracing Infrastructure (2010)
    • Gorilla: A Fast, Scalable, In-Memory Time Series Database (2015)
  • Release It 笔记
    • Anti-patterns & Patterns in Microservice Architecture
  • Database Design
    • Log Structured Merge (LSM) Tree & Usages in KV Stores
    • Prometheus
Powered by GitBook
On this page
  1. Stanford-CS107

泛型栈

第六课

泛型栈的接口及实现

栈是最基本的抽象数据类型之一,这一节探讨了在 C 语言环境下,如何利用无类型指针和内存操作实现一个通用的栈 (Generic Stack) — 即可以放进任何类型数据的栈。

Interfaces

typedef struct {
  void *elems;       // 指向栈内数据的无类型指针
  int elemSize;     // 每个数据单元的大小 (字节数)
  int loglength;    // 栈内数据的个数
  int alloclength;    // 为栈分配的动态内存空间能够储存的数据的个数
} stack;

void StackNew(stack *s, int elemSize);     // 创建栈
void StackDispose(stack *s);            // 回收栈
bool StackEmpty(stack *s);            // 查询栈是否为空
void StackPush(stack *s, void *elemAddr);    // 入栈
void StackPop(stack *s, void *elemAddr);    // 出栈

StackNew、StackDispose、StackEmpty、StackPush、StackPop 定义了栈最基本的接口,保证了栈实现的隔离 (Information Hiding)

Implementations

StackNew

创建栈时,使用者需要指定栈内数据单元的大小。知道了数据单元的大小后,只需要直接按照相应大小对内存中的字节直接操作即可完成对不同数据类型的支持

void StackNew(stack *s, int elemSize) {
  s->elemSize = elemSize;
  s->loglength = 0;
  s->alloclength = 4;
  s->elems = malloc(4 * elemSize);
}

StackDispose

StackDispose 只是回收栈中为存储数据动态分配的内存空间,而不是回收栈结构本身。

void StackDispose(stack *s) {
  free(s->elems);
}

StackEmpty

bool StackEmpty(stack *s) {
  return s->loglength == 0;
}

StackPush

StackGrow 采用每次倍增栈内数据的存储空间。由于分配存储空间的复杂度为 O(n) ,因此这种操作应当尽量避免或者能被均摊 (amortized),而倍增策略在均摊情形下是可以达到 O(1) 的,因此这是一种广泛被采用的扩容策略。

StackPush 利用 “指鹿为马” 的黑科技直接将数据源地址中的字节直接拷贝到栈顶。

static void StackGrow(stack *s) {
  s->alloclength *= 2;
  s->elems = realloc(s->elems, s->alloclength * s->elemSize);
}

void StackPush(stack *s, void *elemAddr) {
  if (s->loglength == s->alloclength) {
    StackGrow(s);
  }
  void *destAddr = (char *) s->elems + s->loglength * s->elemSize;
  memcpy(destAddr, elemAddr, s->elemSize);
  s->loglength++;
}

StackPop

基本可以理解为 StackPush 实现的逆操作。值得一提的是,这里的接口设计要求使用者负责创建和回收存储出栈数据的内存空间,从而减少犯错的几率。

void StackPop(stack *s, void *elemAddr) {
  s->loglength--;
  void *sourceAddr = (char *) s->elems + s->loglength * s->elemSize;
  memcpy(elemAddr, sourceAddr, s->elemSize);
}

例子:字符串栈

int main(int argc, char** argv) {
  const char *friends[] = {"Al", "Bob", "Carl"};
  stack stringStack;


  StackNew(&stringStack, sizeof(char *));
  for (int i = 0; i < 3; i++) {
    char *copy = strdup(friends[i]);
    StackPush(&stringStack, &copy);
  }
  // 1. 
  char *name;
  for (int i = 0; i < 3; i++) {
    StackPop(&stringStack, &name);
    printf("%s \n", name);
    free(name);
  }
  StackDispose(&stringStack);

  // 2. 
  // StackDispose(&stringStack);
}

使用通用栈来存放字符串数据与原始类型数据有所不同。存放原始类型数据不涉及动态类型的数据,压入栈中的是该原始数据本身,而存放字符串数据、以及其它自定义结构数据时,压入栈中的是指向字符串或自定义结构数据的指针。如示例中的*copy所示,传入StackPush的是指向字符串的指针的指针,压入栈中的是指向字符串的指针,同理,传入StackPop的是指向字符串的指针的指针,从栈中弹出的是指向字符串的指针。

通用栈中存放指针时,通常还涉及到动态分配内存空间的释放。当指向字符串的指针被压入栈中时,字符串空间释放的责任就从栈的使用方转移到了栈的实现方,而当指向字符串的指针从栈中被弹出时,字符串空间释放的责任又从栈的实现方转移回到了栈的使用方。由于我们无法保证栈的使用者每次都在栈为空的情况下调用StackDispose,因此,栈本身需要知道如何释放栈中元素 — 某指针指向的动态空间。

为了达到上述目的,我们需要修改通用栈的部分接口:

// stack.h
typedef struct {
  void *elems;
  int elemSize;
  int loglength;
  int alloclength;
  void (*freefn)(void *); // 空间释放函数,由栈的使用方提供
} stack;

void StackNew(stack *s, int elemSize, void(*freefn)(void *)); // 传入空间释放函数

// stack.c
void StackNew(stack *s, int elemSize, void (*freefn)(void *)) {
  s->elemSize = elemSize;
  s->loglength = 0;
  s->alloclength = kInitialAllocationSize;
  s->elems = malloc(kInitialAllocationSize * elemSize);
  s->freefn = freefn;
}

void StackDispose(stack *s) {
  // s->freefn == NULL, means there is no dynamically allocated resources
  // s->freefn != NULL, means there is dynamically allocated resources which
  // should be handled properly by s->freefn provided by clients
  if (s->freefn != NULL) {
    for (int i = 0; i < s->loglength; i++) {
      s->freefn((char *)s->elems + i * s->elemSize);
    }
  }
  free(s->elems);
}

这时候,使用 stringStack 就需要传入释放函数:

void StringFree(void *elem) {
  free(*(char **) elem);
}

StackNew(&stringStack, sizeof(char *), StringFree);

参考

Previous泛型函数Next运行时内存结构

Last updated 6 years ago

Stanford CS107: lecture 6
Github: ZhengHe-MD - lecture codes