如何构建和管理自己的知识系统

1. 背景

三年前,我在做硕士毕业设计的时候第一次使用Evernote(印象笔记)收集资料,之后就一直使用Evernote记录一些备忘和生活琐事,但也没有形成相对完整的系统。开始读博士之后,我习惯随手在论文上记批注和笔记,觉得这样个人观点和原文的耦合度会比较高。但是等到要检索和引用资料时,脑中对这些知识点有些印象,可很多资料就是找不到出处,这时又要重新谷歌或者去翻文件夹。这让我思考是否需要将笔记做成电子形式,这样检索和使用的时候会比较方便,知识也能线性积累。去纸化的过程是比较痛苦的,因为我们从小到大习惯了纸质笔记这种记录方式。我们的工具十分简单(纸和笔),却可以记录各种类别的信息(文字、结构图、流程图、批注)。然而到了电子时代,我们可用的工具很多,但却很难形成和纸质笔记相比的“灵活性”。当然,相比传统的笔记方式,电子笔记可以多次修改、任意排序和整理、发布和分享、互相引用与检索,但有的时候却不如传统笔记那么“直观”和“简单”(想想用pdf做标注 v.s. 在书上直接做批注 或是 用画图软件画一个流程图 v.s. 在纸上随手画一个流程图)。在过去的一年时间里,我一直在思考和实践电子化笔记系统的可行性,以及如何凸显和最大程度发挥电子笔记的优势。而我发现,电子笔记不仅有上述提到的几个优势,还可以成为构建和强化知识系统的有力工具。

这里,我将一切广义上可以提高人对某一事物或领域的认知或记忆的信息(以文字为主)称为知识,而将通过计算机、平板、手机等手段记录个性化知识的方式称为电子笔记。此文分享了我对于知识和记笔记的理解,以及通过电子化工具构建知识系统的思考和经验。

2. 为什么需要记笔记?

上一节聊了我为什么需要将笔记电子化的动机,现在简单说一下我们为什么需要记笔记。每个人都有这样的经历:和别人聊天时,总是有一些感觉在嘴边的东西,大脑却无法回忆起来;或者某天去超市购物时,明明觉得需要买什么,却怎么也想不起来。不管我们认不认同,人脑的潜力不是无限的。理解我们自身局限性的其中之一,就是认识到人脑是有局限性的,并且记忆的容量是有限的。所以构建一个笔记/知识信息系统,就是要补偿人脑的这种局限性,将笔记系统作为我们的第二大脑。这个观点已经得到了主流大众的认可。我们知道记笔记需要花费时间,这种时间实际上是一种对未来的投资。我们一般记录事情的动机有两个:一是为了增强对摄取信息的认知;二是为了日后需要的时候可以回想起来。大部分人也应该可以认同记笔记是强化知识的重要手段。除此之外,记笔记的核心价值是“使用”。也就是说,一个好的笔记应该是有使用价值的。一个shopping list可以让你在逛超市的时候买齐所有的东西;一篇日记让你可以回忆过去自己经历的事情;一篇论文笔记让人在需要的时候可以快速回忆起论文的核心观点;一个programming reference可以让你在编程的时候快速想起API的用法,而不用每次都去Google. 这些都是使用价值,是你记录时投资时间换来的回报。记录信息的本质是提供使用价值,所以评价一个笔记系统也应该用”可使用性”为评价方式。有一种观点是:现在(几乎)所有的信息都能在因特网上搜索到,所以需要信息的时候都可以即刻去搜索。这种观点本身没有问题,但是以目前的搜索技术和因特网的归档程度而言,这种方式的时间开销更高:因为你要从海量信息中筛选、判断、提炼出你想要的那一部分信息。而笔记则是“一次记录,多次使用”,一劳永逸。并且有新的知识点时,可以修改、完善之前的知识(另一点电子笔记的优势就是修改起来十分简单)。我们记笔记,就是让笔记成为知识的载体,也是为了日后需要这个知识时提供便捷。

Read more »

【C语言深入】陷阱:数组溢出导致内存被意外修改

C语言的指针在提供编程便利的同时,却带来了很多潜在的内存安全问题。见以下例子:

#include <stdio.h>

int main() {

	char string_buff[12];
	unsigned int i_not_zero = 0xFF;

	sprintf(string_buff, "Hello,world!");

	printf("i = %x\r\n", i_not_zero);

	return 0;
}

该程序(不正确)的输出为:

i = 0

上述代码使用sprintf修改了string_buff指针所指向的char型数组。但是由于在申明数组长度的时候,没有考虑到字符串结束符’\0’,所以实际写入时不慎篡改了下一位内存地址的内容(此例中为i_not_zero, 使用MinGW gcc)。在实际程序中,此类bug一般很难被发现,尤其是还有其他程序在正常修改该值时,一般先会去排查和该变量有关的程序。

这只是一个因为不慎所导致的内存溢出问题,而在一些极端的黑客代码中,经常会见到通过内存变量和函数的指针地址反向访问、修改堆栈,从而获得系统的权限。可见指针作为C语言的一个重要(但是晦涩的)组成部分,无形中降低了系统的可靠性和安全性,需要挑战programmer的debug能力。

你好,2017!

又是新的一年。岁月匆匆,不给人时间回头看看,就又让人上路了。

今年更新了10篇左右的博文:

  • 新增的”C语言深度”专题来源于我在嵌入式课程助教过程中发现的C语言的错误使用。我觉得C和C++是机器人领域最重要的两个语言,所以希望在这方面增加一些内容。
  • 另几篇博文是介绍新发布的树莓派3代的。今年除了树莓派3,还入手了若干树莓派Zero。因为Zero很难买到,所以一下屯了5、6个。这批Zero准备用在智能家庭的节点中,但是应用场景目前还不明确,所以没有给大家做专题介绍。

明年的工作计划:

  • 承诺的智能家庭系统还没有完全开发完成,全部完成后会找时间公布。目前已完成的部分:中心服务器的部署,NAS,多媒体中心,一个传感器节点部署好了(已经上报了半年的温湿度数据)。二氧化碳、PM2.5传感器和无线组网模块选购好了,但还没有时间调试。另一个困难是控制数据的下发和传感器数据、系统参数的展现,我想基于BS架构(Flask + socket.io / Node.js + Ajax)。我没有网页编程的基础,而且中间涉及Real-time和asynchronous的问题,所以还没有时间解决。
  • More topics on Robotics. 实验室还是以机器人为主题的,去年有点跑偏了(嵌入式系统),今年重新回归到主题上。重点我想要关注的内容点有:增强学习、概率决策、机器视觉、ROS、机器(深度)学习。
  • 博主现在长期在国外生活,关于将网站转换为全英文的想法已有很久。但是还是一直很挣扎,考虑到很多内容对国内的读者会有帮助,所以今年还是保留双语写作。

最后祝大家2017年工作、学习顺利!

【C语言深入】陷阱:数组指针作为函数参数返回

再来看一个指针问题,同样的来自一个本科生的代码。这段代码想要实现将一个全是小写字母的字符串转换成对应的大写字母字符串:

char *covert_to_upper_case(char *string) {
    char p[100];
    int i = 0;
    
    for(; i < strlen(string); i++ ) {
            p[i] = string[i] - ('a' - 'A'); 
    }
    p[i] = '\0';
    
    return p;
    
}

然而这段代码没有能实现期望的功能。原因如下:

  1. 主程序调用convert_to_upper_case()函数后,堆栈为p分配了内存空间;
  2. 函数体正确修改了p对应字符数组的内容,并将p的首地址作为指针返回;
  3. 函数返回后,所有临时变量从堆栈中弹出,包括p[100];
  4. 主程序得到返回的指针,对其进行解析。然而指针指向的字符数组此时已经从堆栈中弹出,解析后的数据无法被定义。

要想正确实现对应的功能,应该将目标指针作为额外参数传递给该函数,并由上层调用者提供内存空间的创建。当然也可以使用malloc()将内存分配在堆中,但是需要注意使用对应的free()释放空间,否则会有内存泄露的问题。

【C语言深入】指针的一个错误赋值

关于指针总是有说不完的故事。

最近给本科的学生带Embedded System课程设计,遇到了一个非常奇怪的bug。有一段代码需要实现I2C通信,核心代码已经由软件库提供了,学生只需要设置结构体后调用API即可。一个学生的代码是这样的:

struct I2C_CONFIG {
  // ...
  char *i2c_buff;
  int length;
  // ...
};

struct I2C_CONFIG cfg;
char *i2c_buff;

void I2C_init() 
{
  // ...
  cfg.buff = i2c_buff;
  cfg.length = sizeof(buff);
  // ...
}

void I2C_send(new_buff)
{
  // ...
  i2c_buff = new_buff;
  I2C_MasterTransferData(LPC_I2C1, cfg);
  // ...
}

初看一下没有什么问题:在I2C_init()函数中首先对结构体cfg进行初始化,而在I2C_send()函数中设置了需要发送的数据指针,之后使用I2C的API发送数据。

因为代码一直无法实现期望的功能,我又仔细看了一下其中的蹊跷。我注意到,这段代码中使用了一个中间变量:char *i2c_buff。在I2C_init()中虽然将cfg.buff指向了i2c_buff,但是因为cfg.buff本身也是指针变量,而非”指向指针的指针”,所以这里只实现了简单的按值传递,即将i2c_buff的值 (初始值为0) 赋给了cfg.buff。之后虽然在I2C_send()中修改了临时变量i2c_buff指向的位置,但却没有影响到cfg.buff中的内容,cfg.buff依然指向之前i2c_buff初始化时指向的内存地址,所以需要发送的缓冲指针new_buff其实并没有传递给之后的I2C_MasterTransferData()函数!为了解决这个问题,必须将更改后的i2c_buff的值再次赋给cfg.buff,即:

void I2C_send(new_buff)
{
  // ...
  i2c_buff = new_buff;
  cfg.buff = i2c_buff;
  I2C_MasterTransferData(LPC_I2C1, cfg);
  // ...
}

另外这段代码还有一个不容易注意的bug,就是在I2C_init()中使用了sizeof()来判断buffer的大小。因为sizeof()函数得到的只是数据类型的大小,所以对于指针char *i2c_buff来说,sizeof(i2c_buff) = 4,而不会返回buffer的实际大小。指针的大小并不等于指针指向缓冲的大小!