最近有一个问题反复出现在我timeline上,问说C语言在这个黑框框里面编程究竟可以做什么?有趣!
一部分人,接触的信息量越大就越容易迷失,一是在于接触到更加广阔的天地,了解到了世界的多元和精彩。二是在于看透了自己的精力有限便对什么都提不起兴趣,总是在寻求"最优解"以避免时间成本沉没,他们会寄希望于在知乎上面发问,让这过来人给自己一个最优路径,诸如"C语言究竟怎么学"。
我觉得喝咖啡和编程类似。当然作为资深穷屌我并没有钱对咖啡进行深入的研究,只是从经常在科技园附近星巴克里面装作写代码看能不能忽悠到投资人或者志同道合之人的有限经历出发,用咖啡来类比。对于大部分科班的人来说,接触到的第一语言是C语言,然后C/C++->Java->python/php/scala/lua等等,就像一开始我们喝美式,苦,酸涩,后来加了一点炼乳牛奶之后发现了新大陆,于是尝试拿铁。尝试卡布奇诺,尝试在咖啡中加入各种能让苦酸涩消失甚至变的齁甜的东西。俗话说借酒消愁愁更愁,借甜浇咖咖不咖,终于有一天意识到自己已经很久没有喝过纯粹的咖啡了,干了一杯美式,发现原来这才是咖啡的味道呀。
对于C语言本身也是类似的感受,它虽然语法简单,简单到简陋,简陋到没有太多基础设施,但是每次从其他语言的沼泽漩涡中脱身回到C语言的怀抱中来的时候又总是有新的感悟。很多人对c保持一种敬畏的态度以至于敬而远之,总是在担心自己写出来的代码会不会太幼稚太low或者不酷炫而迟迟动不了手。这就很没有必要。用文章来比喻,高手写的文章不见得遣词造句很华丽,而引人入胜的是作者独到的见解和思路,就像韩寒的作品。而很多网文,纵然有华丽的遣词,气吞河山的长短句,喘不过气的排比,仍然拯救不了那糟糕的剧情和空洞的想象力,反而给人一种很尴尬,矫揉造作的感觉。
喜欢C,是喜欢那种透明,透明代表容易掌控。而我作为非著名java黑,讨厌的就是java实在太黑盒,特别是那一大堆框架。同事经常为了追新,引入一些新潮的框架,每每搞得连IDE都一片一片的报错,然而无厘头的是部署上去居然能跑!我个人是非常不喜欢这种感觉,相当的不舒服。就像一个过度设计的怪物活像求生之路里面的boomer 臃肿又恶心。
俗话说千里之外之行始于足下,万里之行始于轮子。很多人即便学完了C的基本语法,却依旧感觉无从下笔。书本知识最终要转换成工程实践,工程代码和纯学术算法代码又不太一样,比如你可能会写排序算法。冒泡,堆排,快排溜得飞起,却总是对着一堆int数组排来排去?工程化面临的场景可不太一样,你必须更加关注数据如何存储如何查询,如何做一个可扩展的可实施并且隐藏实现细节的轮子出来,轮子是服务于别的程序员的,写出有服务意识的代码,这是纯书本代码不太关注的细节,也是把old school代码转换成工程可用的很好的方式。
如果你学习了一段时间的C语言,还是有一种无从下笔的无力感,那么不妨从造轮子开始。造轮子的起手式就是这种无力感的"破壁人"。
首先我推荐Linux平台,如果你还没有安装Linux,请关注我另一个专栏弓箭维修指北。
正式进入开发阶段,最头疼的问题就是给自己的项目起个名字,一定要威武雄壮霸气听到就腿软的那种horrible thing。所以我打算起------"姥姥家的锅铲"(grandma's turner aka GT)作为项目名。先建立目录,在项目的根目录下再建两个目录分别用来存放我们的头文件和源码目录。然后让我们先尝试造个简单的轮子吧------栈。

//https://zhida.zhihu.com/search?content_id=5553777&content_type=Article&match_order=1&q=+gttypes&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3Njg1NzM3NzMsInEiOiIgZ3R0eXBlcyIsInpoaWRhX3NvdXJjZSI6ImVudGl0eSIsImNvbnRlbnRfaWQiOjU1NTM3NzcsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.LlTN5VqOYiPYV_6D2CmJ7HyciEvVmwS5HXFmtZnN43s&zhida_source=entity.h
// Created by Rowland@Sjet on 2018/1/28.
//
#ifndef GTLIB_GTTYPES_H
#define GTLIB_GTTYPES_H
#ifdef __cplusplus
extern "C" {
#endif
#define GT_API extern
#define GT_OK (0)
#define GT_ERROR_OUTMEM (-1)
#define GT_ERROR_FULL (-2)
#define GT_ERROR_EMPTY (-3)
#ifdef __cplusplus
}
#endif
#endif
如果要说构造轮子的起手式,这个骨架就是了。最外层的宏ifndef define endif是用来告诉编译器不要重复include我,里面的extern c {}层次是通知c++编译器用c的方式处理我,再里面一层就是我们的代码了。宏应该是服务于可读性越直观越好。
// gtstack.h
// Created by Rowland@Sjet on 2018/1/28.
//
#ifndef GTLIB_GTSTACK_H
#define GTLIB_GTSTACK_H
#ifdef __cplusplus
extern "C" {
#endif
#include "gttypes.h"
typedef struct GtStack GtStack;
GT_API GtStack* gt_stack_create(size_t);
GT_API int gt_stack_push(GtStack*, void*);
GT_API int gt_stack_pop(GtStack*, void**);
GT_API void gt_stack_destroy(GtStack**);
#ifdef __cplusplus
}
#endif
#endif //
如非必要,在头文件中只暴露api,任何和实现相关的具体函数,变量和结构体尽量不要在头文件中暴露。未来如果你想闭源,实现只提供so库,操作灵活性更大。
定义几种基本的栈的操作,包括创建,销毁,push和pop。特别是push和pop的void*和void**可能还很难看懂,在解释他们之前我们先对一些约定达成共识------谁主张,谁举证,谁污染,谁治理。如果你赞同的话,void*就是一个调用方value的引用,我这个结构并不负责对你value的生命周期管理工作,这是你自己应该去管理的,所以我仅仅把你value的地址拿过来并不会复制一份你的value。pop操作就是把你value的地址告诉你,所以你得给我一个能存放地址(void*)的地址(再加一个*)这就变为了void**,那么为什么销毁函数是GtStack**而不是GtSstack*,这跟我在别的地方看到的不一样,你肯定在骗我。如果是GtStack* in如果free掉一次似乎的确没有问题,但是如果再调用一次销毁函数就会出问题,因为已经被free了无法访问了,作为一个有服务意识的serviceboy,我当然不希望程序有这种系统级的异常,即便在调用方粗心的调用了多次的情况下,所以我使用了GtStack指针(GtStack*)的地址(GtStack**)来判断这块地址上面的值是否已经是NULL了,因为销毁后会把这块内存赋值NULL,如果是GtStack*,那么在free之后,下次进入函数if(in)永远是成立的,这个in是指向main函数中那个stack,并不是NULL。如果对此有疑问可以参考我在罗然:实参与形参的值传递问题?回答中提到的方式进行展开。
// gtstack.c
// Created by Rowland@Sjet on 2018/1/28.
//
#include <stdlib.h>
#include "../include/gtstack.h"
struct GtStack{
size_t max;
int index;
void** elems;
};
GtStack* gt_stack_create(size_t max){
GtStack* out = (GtStack*)malloc(sizeof(GtStack));
if(!out) exit(GT_ERROR_OUTMEM);
if(max<=0) max = 16;
out->elems = (void**)calloc(max, sizeof(void*));
if(!out->elems) exit(GT_ERROR_OUTMEM);
out->max = max; out->index = 0;
return out;
}
int gt_stack_push(GtStack* in, void* data){
if(in->index>=in->max) return GT_ERROR_FULL;
in->elems[in->index++] = data;
return GT_OK;
}
int gt_stack_pop(GtStack* in, void** data){
if(in->index<=0) return GT_ERROR_EMPTY;
*data = in->elems[--in->index];
return GT_OK;
}
void gt_stack_destroy(GtStack** in){
if(*in){
GtStack* stack = *in;
free(stack->elems);
free(stack);
*in = NULL;
}
}
我写的代码比较随意,没有推敲,但是我不会写"先申明,再使用"的代码,肯定是一步到位,如果你拥有一个现代编译器,也最好不要写类似int a; a=5;这种先申明后初始化的代码,大多数这么写的代码是为了兼容老的编译器或者维护一个历史包袱沉重的项目,在实际工程当中,有可能会发生脏读。
接下来,在项目根目录写一个main.c来测试一下吧
// main.c
// Created by Rowland@Sjet on 2018/1/28.
//
#include <stdio.h>
#include <stdlib.h>
#include "include/gtstack.h"
int main(){
GtStack* stack = gt_stack_create(10);
gt_stack_push(stack, "顺丰");
gt_stack_push(stack, "韵达");
gt_stack_push(stack, "申通");
gt_stack_push(stack, "圆通");
char* p;
int err;
while((err=gt_stack_pop(stack, (void**)&p))==GT_OK){
printf("pop:%s\n", p);
}
gt_stack_destroy(&stack);
return EXIT_SUCCESS;
}
编译、执行
Sjet/> clang main.c include/gtstack.h src/gtstack.c
Sjet/> ./a.out
pop:圆通
pop:申通
pop:韵达
pop:顺丰
Sjet/> _
验证通过就可以把main.c删掉了,这样第一个轮子就造好了。接下来的篇幅里会再介绍如何使用自动构建工具,如何测试,如何检测内存泄漏,再介绍几个常用数据结构和算法,我们就可以撸一个实际的项目出来了。尽量不使用开源类库而手动实现所需要的各种边边角角。感兴趣的不妨点个赞再关注一下!