C/C++文档阅读笔记-A Simple Makefile Tutorial解析

Posted IT1995

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C/C++文档阅读笔记-A Simple Makefile Tutorial解析相关的知识,希望对你有一定的参考价值。

Makefile文件可以使得程序编译变得简单。本博文并不是很系统的讲解makefile,本博文的目标是让读者快速编写自己的makefile文件并能应用到中小项目中。

简单实例

举个例子有下面3个文件,分别是hellomake.c,hellofunc.c,hellomake.h这3个文件构成了一个基本的带有main函数的程序。

hellomake.chellofunc.chellomake.h
#include <hellomake.h>

int main()

  //call a function in another file
  myPrintHelloMake();
  return(0);

 
#include <stdio.h>
#include <hellomake.h>

void myPrintHelloMake(void)

  printf("Hello makefiles!\\n");

  return;
/*
example include file
*/

void myPrintHelloMake(void);
 

通常在不使用makefile时,使用如下的命令编译程序:

gcc -o hellomake hellomake.c hellofunc.c -I.

此命令会编译2个.c文件,包括hellomake.c和hellofunc.c并生成hellomake这个可执行程序。-I . 是告知gcc去当前目录(.)找.h文件,也就是找hellomake.h。没用makefile文件,程序员对代码进行修改后,就需要不停按键盘的↑键去找对应的命令进行编译,如果新增了一个.c文件,还需要修改对应的命令。

所以这种方式就存在2个缺点:

1.如果电脑重启,或者新增了.c文件,就需要重新敲边缘命令,这样十分不方便;

2.如果仅仅只修改了一个.c文件,使用编译命令就会把项目所有的.c重新编译,这样编译就会变得十分慢。

创建一个最简单的makefile文件

Makefile1

hellomake: hellomake.c hellofunc.c
     gcc -o hellomake hellomake.c hellofunc.c -I.

文件名可以叫Makefile或makefile,在命名行中输入make执行编译命令,这个make是不需要带参数的。

第一行的hellomake只是一个tab,后面*.c是文件名。关键的命令是在第2行,程序员通常写的makefile,执行make命令时当选择的文件有一个被修改了,才会执行。

Makefile1解决开发时不停使用键盘↑键带来的操作不方便的问题,但他属于把所有文件都编译,所以没有解决效率问题。

注意:在makefile的首行“:”号的前面都要有个tab,这个是必须存在的。

为了提高编译效率,就有了下面这个Makefile2:

Makefile2

CC=gcc
CFLAGS=-I.

hellomake: hellomake.o hellofunc.o
        $(CC) -o hellomake hellomake.o hellofunc.o 

上面定义了CC和CFLAGS两个宏,目的是方便makefile后面的脚本使用,CC=gcc并且后面的$(CC) -o hellomake hellomake.o hellofunc.o说明这个CC=gcc是使用C编译器,CFLAGS列出来标签的list, -I . 编译成.o文件需要依赖当前目录的.h文件。make命令首先会编译每一个.c文件,最后构建成可执行的hellomake文件。

上面这种方式已经可以用于小项目了,但是如果hellomake.h文件改变了,make命令也不会去重编译.c文件。对于这种情况,需要告诉make,哪个.h文件改变了,需要重新编译,这里就有了Makefile3。

Makefile3

CC=gcc
CFLAGS=-I .
DEPS=hellomake.h

%.o: %.c $(DEPS)
        $(CC) -c -o $@ $< $(CFLAGS)

hellomake: hellomake.o hellofunc.o
        $(CC) -o hellomake hellomake.o hellofunc.o

新建了DEPS宏,这个宏里面包含了.cpp代码中需要关注的.h文件,这里需要注意的是,make生成object文件就需要检测这个.h文件是否改变。下面是各个符号的含义:

%.o:当前目录匹配到所有.o结尾的文件;

%.c:当前目录匹配到所有.c结尾的文件;

-c:生成对应的object文件;

-o $@:编译时进行输出,输出时文件的名字放到“:”的左边;

$<:依赖的第一个文件;

下面对上面的Makefile3进行简化,使用了$@和$^。对编译规则重写进行了编写,在下面的示例中所有的include文件都在DEPS宏中,所有的object文件都在OBJ宏中列出。

Makefile4

CC=gcc
CFLAGS=-I .
DEPS=hellomake.h
OBJ=hellomake.o hellofunc.o

%.o: %.c $(DEPS)
        $(CC) -c -o $@ $< $(CFLAGS)

hellomake: $(OBJ)
        $(CC) -o $@ $^ $(CFLAGS)

对符合解释的补充:

$^:所有依赖文件

如果要将.h文件放到include目录,source文件放到src目录,库文件放到lib目录。.o文件也要规范位置,这样就需要编写新的makefile文件,举个例子当下面这个程序依赖m.so库,这个m是math的意思,并且还需要编写make clean的规则,这样的makefile可以这样写:

Makefile5

IDIR =../include
CC=gcc
CFLAGS=-I$(IDIR)

ODIR=obj
LDIR =../lib

LIBS=-lm

_DEPS = hellomake.h
DEPS = $(patsubst %,$(IDIR)/%,$(_DEPS))

_OBJ = hellomake.o hellofunc.o 
OBJ = $(patsubst %,$(ODIR)/%,$(_OBJ))


$(ODIR)/%.o: %.c $(DEPS)
	$(CC) -c -o $@ $< $(CFLAGS)

hellomake: $(OBJ)
	$(CC) -o $@ $^ $(CFLAGS) $(LIBS)

.PHONY: clean

clean:
	rm -f $(ODIR)/*.o *~ core $(INCDIR)/*~ 

解释下参数:

patsubst:

名称:模式字符串替换函数——patsubst。

功能:查找<text>中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式<pattern>,如果匹配的话,则以<replacement>替换。

   这里,<pattern>可以包括通配符“%”,表示任意长度的字串。如果<replacement>中也包含“%”,那么,<replacement>中的这个“%”将是<pattern>中的那个“%”所代表的字串。

   (可以用“\\”来转义,以“\\%”来表示真实含义的“%”字符)

返回:函数返回被替换过后的字符串。

示例:

$(patsubst %.c,%.o, a.c b.c)

把字串“a.c b.c”符合模式[%.c]的单词替换成[%.o],返回结果是“a.o b.o”

实例代码打包下载地址:

https://github.com/fengfanchen/CAndCPP/tree/master/MakeFileExample

编译原理让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 1.)(python/c/c++版)(笔记)

原文:Let’s Build A Simple Interpreter. Part 1.

【编译原理】让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 1.)(python/c/c++版)(part 1)

pascal代码,我们要为这种代码做一个解释器

program factorial;

function factorial(n: integer): longint;
begin
    if n = 0 then
        factorial := 1
    else
        factorial := n * factorial(n - 1);
end;

var
    n: integer;

begin
    for n := 0 to 16 do
        writeln(n, '! = ', factorial(n));
end.

首先我们先来实现pascal编译器的一个小功能,两个数的加法,我们准备用python实现,但是如果你想用其他语言实现也是可行的,这是它的代码:

# -*- coding: utf-8 -*-
"""
@File    : 1.py
@Time    : 2021/5/19 14:44
@Author  : Dontla
@Email   : sxana@qq.com
@Software: PyCharm
"""
# Token types
#
# EOF (end-of-file) token is used to indicate that
# there is no more input left for lexical analysis
INTEGER, PLUS, EOF = 'INTEGER', 'PLUS', 'EOF'


class Token(object):
    def __init__(self, type_, value):
        # token type: INTEGER, PLUS, or EOF
        self.type = type_
        # token value: 0, 1, 2. 3, 4, 5, 6, 7, 8, 9, '+', or None
        self.value = value

    def __str__(self):
        """String representation of the class instance.

        Examples:
            Token(INTEGER, 3)
            Token(PLUS '+')
        """
        return 'Token({type}, {value})'.format(
            type=self.type,
            value=repr(self.value)
        )

    def __repr__(self):
        return self.__str__()


class Interpreter(object):
    def __init__(self, text):
        # 客户端字符串输入, 比如 "3+5"
        self.text = text
        # self.pos is an index into self.text
        self.pos = 0
        # current token instance(当前标记实例)
        self.current_token = None

    def error(self):
        raise Exception('Error parsing input')  # 语法分析输入出错

    def get_next_token(self):
        """Lexical analyzer (also known as scanner or tokenizer)

        This method is responsible for breaking a sentence
        apart into tokens. One token at a time.

        词法分析器(也称为扫描器scanner或标记器tokenizer)
        这个方法负责将一个句子分解成标记tokens。一次一个标记
        """
        text = self.text

        # is self.pos index past the end of the self.text ?
        # if so, then return EOF token because there is no more
        # input left to convert into tokens
        # self.pos索引是否超过self.text的结尾?
        # 如果是,则返回EOF标记,因为没有更多的标记
        # 向左输入以转换为标记
        if self.pos > len(text) - 1:
            return Token(EOF, None)

        # get a character at the position self.pos and decide
        # what token to create based on the single character
        # 在self.pos位置获取一个字符,并根据单个字符决定要创建的标记
        current_char = text[self.pos]

        # if the character is a digit then convert it to
        # integer, create an INTEGER token, increment self.pos
        # index to point to the next character after the digit,
        # and return the INTEGER token
        # 如果字符是数字,则将其转换为整型,创建整型标记,增加self.pos索引以指向数字后面的下一个字符,然后返回整型标记
        if current_char.isdigit():  # isdigit()函数,全是数字返回True,否则返回False
            token = Token(INTEGER, int(current_char))  # 创建一个token
            self.pos += 1
            return token

        if current_char == '+':
            token = Token(PLUS, current_char)
            self.pos += 1
            return token

        self.error()

    def eat(self, token_type):
        # compare the current token type with the passed token
        # type and if they match then "eat" the current token
        # and assign the next token to the self.current_token,
        # otherwise raise an exception.
        if self.current_token.type == token_type:
            self.current_token = self.get_next_token()
        else:
            self.error()

    def expr(self):
        """expr -> INTEGER PLUS INTEGER"""
        # set current token to the first token taken from the input
        self.current_token = self.get_next_token()

        # we expect the current token to be a single-digit integer
        left = self.current_token
        self.eat(INTEGER)

        # we expect the current token to be a '+' token
        op = self.current_token
        self.eat(PLUS)

        # we expect the current token to be a single-digit integer
        right = self.current_token
        self.eat(INTEGER)
        # after the above call the self.current_token is set to
        # EOF token

        # at this point INTEGER PLUS INTEGER sequence of tokens
        # has been successfully found and the method can just
        # return the result of adding two integers, thus
        # effectively interpreting client input
        result = left.value + right.value
        return result


def main():
    while True:
        try:
            # 要在Python3下运行,请将“raw_input”调用替换为“input”

            # text = raw_input('calc> ')
            text = input('calc> ')  # 获取键盘输入,参数为提示信息
        except EOFError:  # 不知是什么异常
            break
        if not text:
            continue
        interpreter = Interpreter(text)
        result = interpreter.expr()
        print(result)


if __name__ == '__main__':
    main()

运行结果:

D:\\python_virtualenv\\my_flask\\Scripts\\python.exe C:/Users/Administrator/Desktop/新建文件夹/1.py
calc> 1+2
3
calc> 

将输入字符串分解为token标记的过程称为词法分析(lexical analysis),词法分析器( lexical analyzer或 lexer )也叫扫描器(scanner )或标记器(tokenizer)。

>>> from calc1 import Interpreter
>>>
>>> interpreter = Interpreter('3+5')
>>> interpreter.get_next_token()
Token(INTEGER, 3)
>>>
>>> interpreter.get_next_token()
Token(PLUS, '+')
>>>
>>> interpreter.get_next_token()
Token(INTEGER, 5)
>>>
>>> interpreter.get_next_token()
Token(EOF, None)
>>>

让我们回顾一下您的解释器如何评估算术表达式:

  • 解释器接受一个输入字符串,比如说“3+5”
  • 解释器调用expr方法在词法分析器get_next_token返回的标记流中查找结构。它试图找到的结构是INTEGER PLUS INTEGER的形式。在确认结构后,它通过添加两个INTEGER标记的值来解释输入,因为此时解释器很清楚它需要做的是添加两个整数 3 和 5。

检查理解:
什么是解释器interpreter?
什么是编译器compiler?
解释器和编译器有什么区别?
什么是标记token?
将输入分解为标记的过程的名称是什么?
进行词法分析lexical analysis的解释器的部分是什么?
解释器或编译器的那部分的其他常见名称是什么?

用c/c++实现

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

struct Interpreter 
{
	char* text;
	int pos;
	struct Token (*get_next_token)(struct Interpreter*);
};

struct Token
{
	int type;
	char value;
};

struct Token get_next_token(struct Interpreter* pipt) {
	if (pipt->pos > (strlen(pipt->text)-1)) {
		struct Token token = {3, '\\0'};//3表示EOF,2表示+,1表示数字
		return token;
	}
	char current_char = pipt->text[pipt->pos];
	if (current_char>='0' && current_char<='9') {
		struct Token token = {1, current_char};
		pipt->pos++;
		return token;
	}
	if (current_char == '+') {
		struct Token token = { 2, current_char };
		pipt->pos++;
		return token;
	}
	printf("输入非法!\\n");
	exit(-1);//如果都不是以上的字符,则报错并退出程序
}

char eat(struct Token* pcurrent_token, struct Interpreter* pipt, int type) {
	char former_token_value = pcurrent_token->value;
	if (pcurrent_token->type == type) {
		*pcurrent_token = pipt->get_next_token(pipt);
	}
	else {
		printf("输入非法!\\n");
		exit(-1);
	}
	return former_token_value;
}

int expr(char* text) {
	struct Interpreter ipt = {text, 0, get_next_token};
	struct Token current_token = ipt.get_next_token(&ipt);
	char temp;
	temp = eat(&current_token, &ipt, 1);//断言第一个字符是数字
	int left = temp - '0';
	eat(&current_token, &ipt, 2);//断言第三个字符是加号
	temp = eat(&current_token, &ipt, 1);//断言第三个字符是数字
	int right = temp - '0';
	int result = left + right;
	return result;

}

int main() {
	char text[10];
	while (1)
	{
		printf("请输入算式:\\n");
		scanf_s("%s", text, sizeof(text));
		int result = expr(text);
		printf("= %d\\n\\n", result);
	}
	return 0;
}

运行结果:

请输入算式:
2+8
= 10

请输入算式:
1+5
= 6

请输入算式:
3+4
= 7

请输入算式:

以上是关于C/C++文档阅读笔记-A Simple Makefile Tutorial解析的主要内容,如果未能解决你的问题,请参考以下文章

Qt文档阅读笔记-Simple Anchor Layout Example解析

编译原理让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 1.)(python/c/c++版)(笔记)

编译原理让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 3.)(python/c/c++版)(笔记)

编译原理让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 4.)(python/c/c++版)(笔记)

编译原理让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 6.)(python/c/c++版)(笔记)

编译原理让我们来构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 2.)(python/c/c++版)(笔记)