About

越俎代庖 - 文件系统权限设置与进程的用户身份

    首先提一个问题, 为何用 sudo 执行需要 root 权限的程序时, 输入的是当前用户的口令而不是 root 的?

    看一下 sudo 的 manpage, 描述是 "execute a command as another user". 注意并不一定是以 root 身份跑程序, 而是可以用任何其他身份. 这做好事不留名干坏事不怕差评利器真是太方便了. 比如, 偷偷输出某个用户的 SSH RSA 私钥之类的. 在桌面机上很少有人会建多个用户, 那么现在创建一个 (建议接下来的试验都新建用户进行, 避免遗留安全隐患; 另外, 当前用户必须已经被添加到 sudo 白名单中)
$ sudo useradd -m furude
$ sudo passwd furude # 这一步最好设置一个与当前用户不同的口令
$ su - furude
furude $
    这样切换到试验用户身份, 然后创建 ssh key, 并确认生成的密钥
$ ssh-keygen
Generating public/private rsa key pair.

Enter file in which to save the key (/home/furude/.ssh/id_rsa): Created directory '/home/furude/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/furude/.ssh/id_rsa.
Your public key has been saved in /home/furude/.ssh/id_rsa.pub.
The key fingerprint is:
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
The keys randomart image is:
+--[ RSA 2048]----+
| ............... |
+-----------------+
$ ls .ssh
id_rsa id_rsa.pub
    现在另开一个 shell, 以非 furude 身份登入, 执行
$ ls /home/furude/.ssh
会看到错误, 不允许列举该目录. 这是当然了, 要是连密钥都能随便看, 就没什么安全感了.

    下面就轮到 sudo 登场了. 还是当前用户身份登入, 执行
$ sudo ls /home/furude/.ssh
id_rsa id_rsa.pub
$ sudo -u furude ls /home/furude/.ssh
id_rsa id_rsa.pub
$ sudo -u `whoami` ls /home/furude/.ssh
ls: cannot open directory /home/furude/.ssh/: Permission denied
    第一个是以 root 身份执行, root 最大嘛, 所以当然可以毫无压力偷看一切文件或目录了; 接下来 -u furude 参数则是指定以 furude 这个用户身份执行, 自己当然可以看自己的东西了; 最后一个则仍然是以当前用户身份执行, 所以被驳回了.
    在上面试验中, 即使以 furude 的身份使用 sudo, 要求输入的口令仍然是当前用户的口令而不是 furude 用户的 (最好新开一个终端试试, 连续使用 sudo 时口令会被缓存下来). 那么, 开头的问题应该是这样的
  • 为何用 sudo 临时以另一用户身份执行程序时, 要求输入的是当前用户的口令, 而不是被指定的那个用户?

Permanent Link: /p/512/

Post tags:

C

文件系统

权限

进程

单链表 给我翻过来

下面是一个简单的单链表节点结构
struct node {
    int value;
    struct node* next;
};
那么如何将这个链表反转过来呢?
void reverse(struct node* head);

下面采用递归的方式实现, 废话少说, 直接上代码
struct node** _reverse(struct node* n)
{
    if (NULL != n->next)
        *_reverse(n->next) = n;
    return &(n->next);
}

void reverse(struct node* head)
{
    *_reverse(head) = NULL;
}
上面的实现假定传入的 head 不会是空节点. 如果要检测, 可以加入一个判断
void reverse(struct node* head)
{
    if (NULL != head)
        *_reverse(head) = NULL;
}

下面来验证一下吧
#include


struct node {
    int value;
    struct node* next;
};

void reverse(struct node* head);

void print_list(struct node const* head);

int main(void)
{
    struct node a = { 0, NULL },
                b = { 1, &a },
                c = { 2, &b }; // c(2) -> b(1) -> a(0) -> NULL
    print_list(&c);
    reverse(&c); //    changed to a(0) -> b(1) -> c(2) -> NULL
    print_list(&a);
    return 0;
}

void print_list(struct node const* head)
{
    printf("[");
    for (; NULL != head; head = head->next) {
        printf(" %d", head->value);
    }
    printf(" ]\n");
}

struct node** _reverse(struct node* n)
{
    if (NULL != n->next)
        *_reverse(n->next) = n;
    return &(n->next);
}

void reverse(struct node* head)
{
    *_reverse(head) = NULL;
}

Permanent Link: /p/475/

Post tags:

Algorithm

C

Data Structure

位运算的优先级

    在现代计算机程序设计语言中, 每一种成熟的语言都支持许多算符. 对于除了像 Lisp 那样表达式本身以中缀式形式书写的语言, 一般语言都会规定表达式的结合顺序, 也就是算符优先级. 由于语言算符数量的庞大, 算符优先级很难整个记下来, 但是通常有迹可循, 比如看到下面的代码
x = y + z
一般会自然地认为 + 将在 = 之前执行, 这是因为很多语言的编译器设计中, 赋值类运算在算术类运算之后进行, 可以认为这是一个常识, 跟乘除在加减之前进行一样自然. 同理下面的代码
x == y and y == z
x == y + z
很自然地能想到, 比较运算 == 会在逻辑运算 and 之前, 而算术运算 + 应该在比较运算之前.

    如果要给算符分个类. 以整数运算为例, 日常二元算符分类的大类应该按照运算结果的类型来分类, 也就是
结果类型算符
整数+ - * / % ^ & | >> <<
boolean== != < > >= <= or and
    从结果类型上也可以看出一些端倪, 比如
x == y + z
这个算式, 如果先计算 ==, 此时 + 左边是 boolean, 而右边是个整数, 这样根本没法圆场了. 所以一般来说, 算术运算拥有较高的优先级完全是天经地义的.

    现在的问题是, 位运算 (^ & |) 到底算啥?
    从之前的说法来看, 它们也应该是算术运算的一种. 不过优先级还真不好说. 最近在某个项目中我将一个后台功能转移到前台, 需要把一段 Python 代码转换成 Javascript 代码, 这时发生了一个悲剧: Python 对于位运算优先级的实现与我的个人观点非常接近, 认为时至今日, 位运算是日常的算术运算之一, 因此它们的优先级要高于比较运算, 也就是说
x + y == x ^ y
在 Python 中等价于
(x + y) == (x ^ y)
而与之相对的, Javascript 认为位运算的优先级要低, 甚至低过了比较运算, 因此上述代码在 V8 引擎眼中等价于
((x + y) == x) ^ y
    这种槽点爆表的设计真是坑死爹了. 好吧, 我去溯源一下, 谁家的语言设计这么疼. 结果原来 C 语言老大哥是这么个整法的, 什么 Javascript 还有个跟这名字差不多不过没有 script 的语言这么承袭的. 哎, 你们这么折腾到底图个啥啊.

Permanent Link: /p/454/

Post tags:

C

Javascript

Operator Precedence

Python

对空结构体求 sizeof

C++ 声称完全兼容了 C, 这一点在某些细节上不尽然. 比如对空结构体 --- 没有成员, 不含虚函数, 虽然 C 还生活在没有虚函数的三叠纪 --- 求 sizeof 的结果. 具体地说就是下面这个表达式

Code Snippet 0-0

struct empty {};

sizeof empty; // 值为 0

在 C 和 C++ 中会得到不同的值: C 中其值为 0 (在主流编译器中如此), 而 C++ 中其值为 1. 这个微妙的不同步源于 C 中的一个指针相减问题, 如下代码

Code Snippet 0-1

#include


struct empty {};

int main()
{
   struct empty x;
   struct empty y;
   printf("%ld", &x - &y);
   return 0;
}

以 C 语言编译并运行, 程序会直接崩溃掉, 因为在 C 中计算表达式 &x - &y 的值等同于 ((char*)&x) - ((char*)&y) / sizeof struct empty. 这个整数除法非常糟糕, 毫无疑问 C 编译器应该了解到危险所在了: 在编译期, 它完全能够发现该除法算式的常数分母是整数 0, 但是它还是义无反顾地生成了代码, 甚至连警告也不给, 将程序推入运行时崩溃的深渊.

本来这种事情应该偷偷改掉拉倒, 可是 C 标准对这个事情讳而不谈, 丢出一张王牌 "对空结构体或联合求 sizeof 将会是*未定义*行为". 对此 C++ 只好吐了个槽, 说*任何对象至少要占用 1 字节空间*. 所以其实 C++ 标准也没有明确说出 "对空结构体或联合求 sizeof 将会是 1" 这样的话, 但是根据前面这个规定, 由编译器厂商演绎出来的结果就是这样的, sizeof 纷纷得到结果 1, 包括下面这样的情况

Code Snippet 0-2

struct empty_base_a {};
struct empty_base_b {};
struct empty_inherit : empty_base_a, empty_base_b {};

sizeof empty_inherit; // 值为 1

即对从空类上 (多重) 继承的空子类求 sizeof 也将得到 1.

这一招看起来很挫, 但还真的管用了, 用 C++ 编译器编译并运行上述程序, 零也不除了, 程序也不会崩了, 还能给出正确地结果.

虽然把两个什么空的东西用继承的方式捏在一起不会产生体积变大, 但是一个数组的什么空的东西则会导致体积累加, 如

struct empty {};

int main()
{
   struct empty x[4];
   printf("%ld", sizeof x); /* 4 */
   return 0;
}

这段 C++ 代码的运行结果将是 4, 也就是 x 占用了 4 个 1 字节. 这又扯到 C++ 另一个核心编程思维 --- 面向迭代器. 例如下面一坨代码

Code Snippet 0-3

struct empty {};

void echo(empty)
{
   std::cout << "echo" << std::endl;
}

int main()
{
   struct empty x[4];
   std::for_each(x, x + 4, echo);
   return 0;
}

如果认为整个数组是一个对象, 打个包求 sizeof 才能得到 1, 而 x[0]x[4] 等等有相同的地址, 那么 std::for_each 中的循环将一次也不被执行. 类似的, 让多个空类对象聚合在一个空类对象中时, 它们占用的空间大小是会累加的, 如

Code Snippet 0-4

struct empty {};
struct twin {
   empty a;
   empty b;
};

sizeof twin; // 2

Permanent Link: /p/438/

Post tags:

C

C++

sizeof

struct 禁区: 内存布局耦合与 C 语言 ABI 基础

    假如现在有个某复杂系统需要存放人员信息. 人员类型定义类似:
struct person_t {
    name_t name;
    age_t age;
};
    这个系统的人员添加功能由两个部分通过动态库集成到一起, 后端的部分只负责拿到人物信息并做诸如存入数据库, 数据统计等工作, 它向前端提供了两种风格的接口, 分别是
void add_person_0(name_t name, age_t age);
void add_person_1(struct person_t const* person);
    两种看上去没有太多区别. 对于前一种接口, 使用该接口的代码很可能写成这样子
add_person_0("hideyosi", 16);
而对于后一种接口, 代码很可能这样写
struct person_t person = { "hideyosi", 16 };
add_person_1(&person);
    现在考察一次扩展性, 而且来最坏的情况: 接口提供方先行更新了动态库, 而使用方在一段时间内并不知情. 更新的内容包括, 在 nameage 之间插入一项 gender (当然前一种人员添加接口也有变动):
struct person_t {
    name_t name;
    gender_t gender;
    age_t age;
};

void add_person_0(name_t name, gender_t gender, age_t age);
void add_person_1(struct person_t const* person);
    这时会怎么样呢?

    显然两种情况都好不到那里去.
    对于 add_person_0 这个接口, 由于参数个数都改变了, 签名对不上号, 那么使用该接口的一方在通过动态库访问此函数时会少压入一个参数, 并且压入的第二个参数 age 对上的还是 gender 这个参数的位置. (在 C++ 中, 由于允许函数重载, 动态库中的函数名会因签名不同而被 name-mangling 成不同的函数, 因此会导致函数找不到错误. 当然这并不意味着 C 和 C++ 孰优孰劣, 因为这本来就是两个不同的语言, 只是在这一个问题上有不同的死法罢了.) 这样一调用函数程序立马崩掉不客气.
    而对于第二种使用, 因为使用方在初始化 person_t 时, 仍然按照旧的逻辑, 即在偏移为 0 的地方放上 name, 在偏移为 sizeof(name_t) 的地方放上 age, 然后将这样初始化的 person 结构体传递给接口提供方; 而此时提供方对这一片内存区的解释有很大不同, 至少它认为的 sizeof(person_t) 都比使用方给出的要多出一个 sizeof(gender_t). 使用这样的 person 就像拿着三年前的武汉市地图到今天的洪山广场游玩结果掉进工地不明不白地挂掉了一样.

    那么有没有什么无痛的方法能够在这种不同模块分离更新的环境下让程序仍然, 至少在新来的人可能都缺少性别的情况下, 能够继续像模像样地跑呢?
    要找到这个方案, 自然应该先看看问题出在什么地方. 这里问题的核心围绕着内存布局, 即双方在 person_t 这一结构体到底包含了什么数据需要达成一致, 换句话说就是因为 person_t, 双方紧耦合了.

    解决方案很自然的就是, 把所有跟 person_t 内存布局相关的代码全部移到一个模块 (当然是接口提供方那边) 里去, 并提供一套操作接口 (为了彻底不让使用方知道 person_t 的内存布局, 干脆只给个前置声明好了)

Permanent Link: /p/390/

Post tags:

ABI

C

码中萌

Source

Qt

Q_Q
Q_D
这两个真的是 Qt 中定义的宏来的.

C

int V = 0, _ = 1, U = 2, o = 3;
int ___ = , l_l = 0;
assert('_');
assert(V^_^V);
assert(-_-U);
assert((o, o));
assert(**U>_
 o <)**``

注释

HTML


Shell 注释

#= o=

C

/* _ */

Permanent Link: /p/349/

Post tags:

ASCII Art

C

Lisp

Qt

Shell

C++ 异常不怎么常见知识点

    之前也反复有人推荐 Google 的 C++ Coding Style, 昨天看到一位老师写的解读 Google C++ code style 谈对 C++ 的理解, 文后的讨论大多集中在 Google 不允许 C++ 异常. 我这里就不多插嘴了, 不过有些关于异常的基本情况还是想略聊一聊.
    文中的例子使用 g++ 4.5.2 和 clang++ 2.9 编译测试通过.

    如果一个函数声明了异常声明与该函数实际抛出的异常不匹配, 那么这个异常不会被捕获到. 也就是说 catch (...) 不是银弹.
#include


void func_throws() throw (std::out_of_range)
{
    throw std::logic_error("danger!");
}

int main()
{
    try {
        func_throws();
    }
    catch (std::out_of_range) {}
    catch (std::logic_error) {}
    catch (...) {}
    return 0;
}
    补丁: std::set_unexpected 函数与不预期的异常

    而一个有趣的 C++ 规定是, 如果一个函数没有任何异常声明, 则该函数有可能抛出任何异常. 这个规定的一个隐喻是, 一切 C 函数都有可能抛出任何异常. 虽然 C 连异常是什么都不知道, 不过没关系. 因为 C 函数是异常透明的.
    比如下面的例子
#include

extern "C" void exception_pass();

void func_throws()
{
    throw std::logic_error("danger!");
}

void upper_cpp_func()
{
    exception_pass();
}

int main()
{
    try {
        upper_cpp_func();
    } catch (std::logic_error) {
        return 0;
    }
    return 1;
}

extern "C" void exception_pass()
{
    func_throws();
}
    在异常发生后的 stack unwind 阶段, C 函数调用栈帧与 C++ 函数栈帧的处理方式相同. 执行结果是一样的. 但这并不意味着一种好的 C/C++ 编程方式, C 是没有 RAII 的!
    另外, 分离编译再链接时行为会不同, 可能出现异常无法被捕获的情况.

Permanent Link: /p/256/

Post tags:

C

C++

Exception Handling

软件翻译与 locale

    看了一篇会 "吐核" 的终端, 了解到 wget 还有烦人的 "eta(英国中部时间)" 提示, 看来软件翻译这档子事还真是很能产生喜感的.

    在 Linux 环境下, 主流的软件翻译方法是通过工具抓取源代码中的需翻译字符串到外部文本文件, 形成一个字典, 翻译之后在运行期加载. 更专业一点就是, 从软件本身要能国际化 (internationalization, 一般简写为 i18n, 因为 i 和 n 之间有 18 个字符), 支持从源代码中提取字符串来翻译; 而翻译这个步骤则成为本地化 (localization, 简称 l10n).
    要让软件具备国际化属性, 这必须程序员们亲自努力, 为需翻译字符串附加一些修饰. 如下面这个程序
int main(int argc, char* argv[])
{
    if (1 == argc || 0 == strcmp("hello", argv[1]))
    {
        puts("Hello");
    }
    else
    {
        puts("World");
    }
    return 0;
}
是很难具备国际化能力的, 因为文本扫描程序无法区分哪些字符串是需要翻译的.
    要加上区分信息很简单, 比如
#define TRANSLATE_THIS(x) x

int main(int argc, char* argv[])
{
    if (1 == argc || 0 == strcmp("hello", argv[1]))
    {
        puts(TRANSLATE_THIS("Hello"));
    }
    else
    {
        puts(TRANSLATE_THIS("World"));
    }
    return 0;
}
    将文件保存为 hello.c, 使用 xgettext 工具提取字符串
$ xgettext hello.c --keyword=TRANSLATE_THIS -o hello.po
那么 xgettext 将提取被宏 TRANSLATE_THIS 所包括的所有字符串到 hello.po 纯文本文件中.
    在 po 文件中, 每个被提取的字符串是一个 msgid, 而翻译则是接下来一行中的 msgstr, 现在所有的 msgstr 都空着, 在 po 文件中直接写入翻译结果并保存即可. 此外还要指定 po 文件头部有文件编码等信息.
    翻译完成后, 在本地创建 locale 相关目录, 再使用 msgfmt 工具编译 po 文件为二进制文件, 并放入 locale 目录中
$ mkdir -p $LANG/LC_MESSAGES && msgfmt hello.po -o $LANG/LC_MESSAGES/hello.mo
这个目录的意义待会儿再解释.
    使用纯文本编辑器盯着 po 文件翻译条目是很累很无趣的事情, 好在有许多工具可以选取. 比较流行的有 poEditQt Linguist. 接下来继续讨论程序的事情.

    到此为止, 程序本身还不具备本地化的能力, 因为宏 TRANSLATE_THIS 什么也没干, 程序本身的逻辑并没有变化, 也不知道如何导入翻译目标字符串替换原有字符串. 现在将宏定义换掉, 使用 locale 中的组件来完成本地化工作

Permanent Link: /p/208/

Post tags:

C

i18n

l10n

Locale Programming

Tutorial

C++ 中捕获整数除零错误

    继承自 C 的优良传统, C++ 也是一门非常靠近底层的语言, 可是实在是太靠近了, 很多问题语言本身没有提供解决方案, 可执行代码贴近机器, 运行时没有虚拟机来反馈错误, 跑着跑着就毫无征兆地崩溃了, 简直比过山车还刺激.
    虽然 C++ 加入了异常机制来处理很多运行时错误, 但是异常机制的功效非常受限, 很多错误还没办法用原生异常手段捕捉, 比如整数除 0 错误. 下面这段代码
#include


int main()
{
    try {
        int x, y;
        std::cin >> x >> y;
        std::cout << x / y << std::endl;
    } catch (...) {
        std::cerr << "attempt to divide integer by 0." << std::endl;
    }
    return 0;
}
输入 "1 0" 则会导致程序挂掉, 而那对 try-catch 还呆在那里好像什么事情都没发生一样. 像 Python 一类有虚拟机环境支持的语言, 都会毫无悬念地捕获除 0 错误.

使用信号

    不过, 底层自然有底层的办法. 这得益于硬件体系中的中断机制. 简而言之, 当发生整数除 0 之类的错误时, 硬件会触发中断, 这时操作系统会根据上下文查出是哪个进程不给力了, 然后给这个进程发出一个信号. 某些时候也可以手动给进程发信号, 比如恼怒的用户发现某个程序卡死的时候果断 kill 掉这个进程, 这也是信号的一种.
    这次就不是 C 标准了, 而是 POSIX 标准. 它规定了哪些信号进程不处理也不会有太大问题, 有些信号进程想处理也是不行的, 还有一些信号是错误中断, 如果程序处理了它们, 那么程序能继续执行, 否则直接杀掉.
    不过, 这些错误处理默认过程都是不存在的, 需要通过调用 signal 函数配置. 方法类似下面这个例子
#include
#include
#include

void handle_div_0(int)
{
    std::cerr << "attempt to divide integer by 0." << std::endl;
    exit(1);
}

int main()
{
    if (SIG_ERR == signal(SIGFPE, handle_div_0)) {
        std::cerr << "fail to setup handler." << std::endl;
        return 1;
    }
    int x, y;
    std::cin >> x >> y;
    std::cout << x / y << std::endl;
    return 0;
}
    可以看出, signal 接受两个参数, 分别是信号编号和信号处理函数. 成功设置了针对 SIGFPE (吐槽: 为什么是浮点异常 FPE 呢?) 的处理函数 handle_div_0, 如果再发生整数除 0 的惨剧, handle_div_0 就会被调用.
    handle_div_0 的参数是信号码, 也就是 SIGFPE, 忽略它也行.

底层机制

Permanent Link: /p/100/

Post tags:

C

C++

Exception Handling

POSIX

Signal

char** 不能赋值给 char const**

C 和 C++ 允许把指向 "某物" 的指针赋值给指向 "不可改变" 的 "某物" 的指针, 如

char* call = "fsck";
char const* call_const = call;

而且, 如果确定不会更改所知向的对象, 那么推荐加上 const, 这是 C++ 的代码编写建议之一. 但是, 要把指向 “某物” 的二级指针赋值给... 见鬼, 简单的说, 下面这种赋值, 任何一个负责任的 C++ 编译器会报出指针不兼容错误:

char* call = "fsck";
char** ptr_to_call = &call
char const** ptr_const_to_call = ptr_to_call; // error

C/C++ 这样做, 目的其实是在于防范将 char const* 赋值给 char* 的意外行为. 这样说也许很难理解, 那么请看下面这段示例代码

Code Snippet 0-0

char const* CALL = "fsck";

void bind(char const** m)
{
   *m = CALL;
}

int main(void)
{
   char* s;
   bind((char const**)&s); // explicit convert
   s[1] = 'u'; // crash on write
   return 0;
}

在调用 bind 时, 采取强制转换来规避编译错误. 而此时调用 bind 就出现了问题, 它把 char const* 类型的 CALL 变量赋值给了实际上是 char*s, 结果就是在紧接下来的那个赋值中, 程序由于尝试去写只读内存区而崩溃. 有崩溃有真相, 这就是对标题的解答.

另一个类似的例子是, 子类对象的二级指针不能赋值给父类对象的二级指针, 如

Code Snippet 0-1

struct base {};
struct inherit : base {};

void f(void)
{
   inherit** i = NULL;
   base** b = i; // error
}

Permanent Link: /p/83/

Post tags:

C

C++

const

Makefile 与 C 语系头文件依赖自动解析

h1. 如何获得源文件依赖的头文件?

这一部分内容与 Makefile 毫无关系, 它由编译器完成.

以 g++ 为例, 可以尝试使用以下方法输出 .cpp 文件依赖的头文件

g++ -M [your cpp file]

乱糟糟的, 不是吗? 这是因为 g++ 把依赖的库文件也给列出来了. 忽略库文件, 则使用这样的命令

g++ -MM [your cpp file]

显然, 这些东西扔到标准输出是毫无用处的, 可以对其进行重定向

g++ -MM [your cpp file] > [some file]

但是这还是有问题, 只把依赖关系放进文件当然不行, 编译源文件的指令还没弄出来呢. 下面就得另外想办法了.

h1. 怎么添加编译指令并运行它们?

添加指令本质上就是追加文件内容, 方法有难有简单, 最简单的无非是写个脚本 echo 并追加重定向到文件中. 在这篇粗陋的文章里就用这招了. 好, 写个 Makefile 脚本

[target.o]:[your cpp file]
....g++ -MM $< > Makefile.tmp
....echo "....g++  $< -c" >> Makefile.tmp
....make -f Makefile.tmp

这里的四个点号 (....) 表示一个制表符, 嗯, 是的, Makefile 只能用万恶的制表符来缩进, 而且还必须缩进; Makefile.tmp 一定不要是当前的 Makefile, 要不然就出现文件覆盖的悲剧了...

结果就是, 一旦 make 到这个目标, 它会生成一串依赖关系和构建方法到另一个 Makefile, 然后把编译这等脏活累活都甩给那个倒霉的家伙.

h1. 怎么转移目标?

如果上述目标仍然写成 xxx.o : xxx.cpp, 那么如果仅仅是依赖的头文件被修改了, 这个目标仍然不会被 make 到. 一个很挫的方法是, 不要把目标定为 xxx.o 文件, 相反, 弄成一个永不会生成的目标比较好, 比如叫 xxx.d 这个名字.

下面上一个完整的例子. 这个例子中有个叫做 DEP 的 Makefile 变量没有被声明过, 嗯, 这将是你的事情. 请将所有需要编译并连接的 cpp 源文件列个表, 然后修改它们的后缀变成 d, 那么 DEP 的值就是这个表.

Code Snippet 0-0

CXX=g++
CXXFLAGS=-Wall -pg
MKTMP=Makefile.tmp

all:object

object:$(DEP)
....$(CXX) $(CXXFLAGS) *.o -o $@

%.d:%.cpp
....$(CXX) -MM $< > $(MKTMP)
....echo "....$(CXX) $< $(CXXFLAGS) -c" >> $(MKTMP)
....make -f $(MKTMP)

clean:
....rm -f *.o
....rm -f test.out
....rm -f $(MKTMP)

再次提醒, 制表符出没注意. (可利用编辑器的全文替换, 4 点换成 1 制表符)

假如现在目录下有 main.cpp, class0.cpp, class1.cpp, 那么定义

DEP=main.d class0.d class1.d

将这句话插进上述源码中的开头位置然后 make 即可.

h1. 怎么收拾掉乱七八糟的输出?

接脏活的临时工经常会反馈出一些无聊的信息, 比如进入这个离开那个的, 如果希望保持控制台的清洁, 那么把不想看的都扔进垃圾桶吧

make > /dev/null

不要担心看不到 warning 和 error, 它们是通过 stderr 输出的, 而上述重定向只会干扰 stdout.

Permanent Link: /p/63/

Post tags:

C

C++

Makefile

已知两圆圆心坐标及半径求两圆交点

在一个二维平面上给定两个圆的圆心横纵坐标、半径共 6 个参数, 求交点. 即实现下面的 C 函数

int intersect(struct circle_t const circles[], struct point_t intersections[]);

其中 point_tcircle_t 的定义分别是

Code Snippet 0-0

struct point_t {
   double x;
   double y;
};

struct circle_t {
   point_t center;
   double r;
};

函数 intersect 的输入参数为两个圆, 返回交点个数, 另外, 交点详细信息将被存入另一个参数数组 intersections 中. 由于两个圆之多有 2 个交点, 因此该函数可以如下形式调用:

Code Snippet 0-1

#include


int main(void)
{
   struct circle_t circles[2];
   struct point_t points[2];

   /* 从 stdin 输入圆参数
    * 按照如下格式
    * $(x_0, y_0, r_0)$
    * $(x_1, y_1, r_1)$
    */
   scanf("%lf%lf%lf%lf%lf%lf",
         &circles[0].center.x, &circles[0].center.y, &circles[0].r,
         &circles[1].center.x, &circles[1].center.y, &circles[1].r);

   // 如果两个圆相同
   // *注意:* 由于 x, y, r 都是浮点数, 使用 == 进行判同可能导致问题
   // 作为示例代码还是姑且这么写了
   if (circles[0].center.x == circles[1].center.x
       && circles[0].center.y == circles[1].center.y
       && circles[0].r == circles[1].r)
   {
      puts("The circles are the same.");
      return 0;
   }

   switch (*intersect(circles, points)*) {
       case 0:
           puts("No intersection.");
           break;
       case 1:
           printf("(%.3lf %.3lf)n", points[0].x, points[0].y);
           break;
       case 2:
           printf("(%.3lf %.3lf) (%.3lf %.3lf)n",
                  points[0].x, points[0].y,
                  points[1].x, points[1].y);
   }
   return 0;
}

用程序实现求交点方法可以有多种, 比较极端的甚至可以用到二分逼近, 反正计算机的计算能力跟笔算是两个世界; 不过本文还是老实一点, 仍试着来解方程. 在求解过程中, 除了用到圆的标准方程

$ (x - x_0)^2 + (y - y_0)^2 = r_0^2

(其中 $(x_0, y_0)$ 是其中一个圆的圆心, $r_0$ 是其半径; 当然对另一圆也是如此, 此处省略之) 圆的参数方程也将会被用到

$ x = r_0 * cos(A) + x_0

$ y = r_0 * sin(A) + y_0

现在, 设其中其中至少有一个交点 (没有交点的情况可以简单利用圆心距大于半径之和来判定), 换言之存在某个 $A_s$ 使得存在对应的点 $(x_s, y_s)$ 于两个圆的方程都成立, 即

Permanent Link: /p/14/

Post tags:

Algorithm

C

Geometry

All 12

. Back to Tobary book
Tobary book - Copyright (C) ZhePlus @ Bit Focus
About