65.9K
CodeProject 正在变化。 阅读更多。
Home

在 C 中创建和按值传递对象

starIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIconemptyStarIcon

1.50/5 (6投票s)

2021年7月9日

GPL3

19分钟阅读

viewsIcon

5849

downloadIcon

71

C 语言、纯函数、多文件、栈和堆

引言

函数的最佳大小是多少?一个函数应该做多少事情?应该有的最小尺寸是多少?一行代码有意义吗?我根据什么标准将函数归类到文件里?

这适合那些有比较扎实 C 语言学术背景的人。熟悉中等难度的算法任务,但缺乏实际编码经验。

纯函数

没有数据的函数是没有意义的。有些函数处理整个数据类型,有些则处理它的每一个基本方面。如果你有一个Person数据类型;前者操作的是Persons(复数),后者操作的是Person(单数)的属性,例如:mass(质量)、age(年龄)、name(姓名)、address(地址)、picture(图片)......

我发现了一个更间接的解决方案来解决我的“函数大小问题”,那就是纯函数。纯函数没有副作用。它们通过参数复制数据,进行任何必要的计算,然后返回结果。给定相同的参数,纯函数总是返回相同的结果。

纯函数只向外部世界返回一个值,它在不提及代码行数之类数量的情况下就说明了它的大小。你应该只有一个作用。你不应该在一个函数中使用指针来修改 5 个不同类型的外部对象。

一些基本值与其他值有关联,单独没有意义。它们代表一个逻辑实体。例如,一个三维点可以代表Person的位置。将对象的point位置作为point返回,比编写三个不同的函数来返回它的xyz坐标更实用。

Python 等语言能够从函数返回多个值,我认为 C 语言比这更合适。你为什么想从一个函数中返回Person的:masspictureaddress?你应该将一组代表逻辑实体的基本值,以struct的形式返回。就像一些生物信息:weight(体重)、height(身高)和age(年龄)。

堆和栈

我尽量避免使用malloc和堆(如果可能的话)。我很高兴地认为,C 语言中传递参数的默认方式是按复制/值传递。抱歉,不是默认方式,而是唯一的方式。与 C++ 和 Pascal 相反,C 语言中没有按引用传递。你有一个可以按值传递的指针数据类型,这个值非常适合表示对象的引用。

这里的代码示例没有实际意义,只是为了说明讨论中的观点。

示例 1

point.h

struct point {
    unsigned x;
    unsigned y;
};

struct point point_new(unsigned, unsigned);
struct point point_move(struct point, int, int);

point.c

#include "point.h"

struct point point_new(unsigned x, unsigned y) {
    struct point t;
    t.x = x;
    t.y = y;
    return t;
}

struct point point_move(struct point t, int dx, int dy) {
    t.x = t.x + dx > 0 ? t.x + dx : 0;
    t.y = t.y + dy > 0 ? t.y + dy : 0;
    return t;
}

rect.h

struct rect {
    struct point p;
    unsigned w;
    unsigned h;
};

struct rect rect_new(struct point, unsigned, unsigned);
struct rect rect_move(struct rect, int, int);
struct rect rect_size(struct rect, unsigned, unsigned);

rect.c

#include "point.h"
#include "rect.h"

struct rect rect_new(struct point p, unsigned w, unsigned h) {
    struct rect t;
    t.p.x = p.x;
    t.p.y = p.y;
    t.w = w;
    t.h = h;
    return t;
}

struct rect rect_move(struct rect t, int dx, int dy) {
    return rect_new(point_move(t.p, dx, dy), t.w, t.h);
}

struct rect rect_size(struct rect t, unsigned w, unsigned h) {
    t.w = w;
    t.h = h;
    return t;
}

example1.c

#include "point.h"
#include "rect.h"

#include <stdio.h>

int main(void) {
    struct rect a = rect_new(point_new(5, 10), 20, 30);
    printf("rectangle at (%d, %d), width %d, height %d\n", a.p.x, a.p.y, a.w, a.h);

    a = rect_size(rect_move(a, 10, 5), 40, 50);
    printf("rectangle at (%d, %d), width %d, height %d\n", a.p.x, a.p.y, a.w, a.h);
    
    return 0;
}

compile

cl example1.c point.c rect.c

输出

rectangle at (5, 10), width 20, height 30
rectangle at (15, 15), width 40, height 50

在本文末尾的附录中,有关于如何在 Windows 上获取免费编译器并编译示例的信息。

我们来明确“调用者”和“被调用者”的概念。如果函数A从函数B的体内被调用,那么我们说函数B是调用者,函数A是被调用者。在调用栈上,被调用者总是在调用者的上方。

这里的关键是:类型为pointrect的对象作为被调用者的局部变量创建,并将它们返回给调用者,在调用者函数栈帧中再次作为局部变量。没有指针,没有堆分配,当main函数退出时,一切都会被干净地清理干净。

即使你使用了堆并且忘记释放分配的内存,所有东西也会被干净地清理干净,因为当程序终止时,运行该程序的操作系统进程也会终止,不会有内存泄漏。一开始就是虚拟内存,但你不想将带有内存泄漏的嵌入式设备送到太空。

每个 C 语言新手过一段时间都会注意到,“高手”们不再将整个对象传递给被调用者,而是只传递其指针。这通常被认为是更快、更有效率。结构体越大,传递指针比传递整个结构体更有效。你在这里同样可以使用这一点。

栈是计算机技术中最常用的数据结构。它非常重要,以至于人们在提高其效率方面做了很多工作。CPU 有硬件支持:堆栈指针、将数据移入和移出堆栈的特殊指令……如果你能指出任何在 CPU 缓存中的东西,那很可能就是堆栈。为每个对象在堆上分配内存,当你稍后解引用该对象时,将导致缓存未命中。

如果为了虚拟机的利益可以牺牲效率,我愿意为了效率而将整个结构体复制到堆栈上,我敢打赌 C 语言能比任何虚拟机做得更快。但是,栈上的大对象(千字节以上)会削弱其目的,因为你无法将它们中的许多放入 CPU 缓存。

函数调用以与所有其他事物相同的后进先出(First-In-Last-Out)方式堆叠在栈上,这意味着调用者的局部变量是真实的,你可以将它们的地址传递给被调用者。我们将向pointrect类添加一些使用此功能的函数。这是对某些语言中嵌套函数思想的一种“杂种化”。例如,Pascal 在嵌套函数时使用一个隐藏的参数,使其能够访问包含函数的局部变量。这是指向调用者栈帧的指针。

与另一个杂种化类似,它使用一个指向对象的隐藏参数,这样看起来y->f(x)就不是f(&y, x)了……

受人尊敬的大师 Qc Na 曾告诉他的学生 Anton:“对象是穷人的闭包”,以及“闭包是穷人的对象”。

C 语言中的多文件分解

有两种数据类型,pointrect(矩形)。它们各自由两个文件表示。一个是带有.h扩展名的头文件,另一个带有.c扩展名的文件在 C 语言术语中称为源文件。

第一个文件是接口,第二个文件是实现。这是 C 语言的公平玩法,由你决定。你可以切换文件的扩展名,一切都会正常工作,前提是你要在include语句中和调用编译器时进行切换。

C 语言中的头文件就像纯文本一样插入到源文件中#include指令所在的位置。从源文件生成一个新的临时文件,其中包含所有插入的头文件。这就是编译器将其翻译成二进制文件的东西。因此,它被称为翻译单元

这五个文件中的代码可以放在一个.c文件中,那么为什么我还要将其拆分成多个文件呢?
虽然人们似乎想将程序拆分以单独和分组逻辑内容,并创造某种乌托邦,但最有力的原因是,工作也可以拆分给多个人。每个人负责一个或多个源文件。

我用什么标准来这样划分它们,为什么我没有把所有_move函数放在一个文件里,把_new函数放在另一个文件里?
标准是数据。数据和代码是程序相等的组成部分,但数据更平等

你围绕数据类型组织函数和源文件,可以将每种数据类型及其功能视为一个编程库。无论你为某个类型添加什么新功能(例如这里的point),将其添加到point.c文件中,而不是在当前编码的任何其他文件中就地使用,这被认为是良好的实践。

数据类型以及所有操作该数据的函数称为一个类。仅通过其类型接口处理对象称为封装。

示例 1 封装性较差。不仅可以窥探和修改数据类型,而且函数中的支持不足以避免直接使用。另外,我干扰了point的封装。在函数rect_new中,当我设置rect.point的值时:t.p.x = p.xt.p.y = p.y。这里有危险。我应该写t.p = p

当一个项目扩展到 1500 个文件,其中 500 个以这种方式处理,并且point的某个工作方式发生变化时,我将不得不修改 500 个文件,而不是仅在point的实现中进行更改。

这里我使用头文件的方式有些奇怪。例如,如果你想创建一个rect数据类型供其他人使用,你不想让他们费心去编辑源文件本身,并正确地设置 include 指令的顺序。

rect类型依赖于point类型,如果将point.h头文件包含到rect.h头文件中,那么使用矩形的人就不必自己将rect的依赖项包含到他的源文件中。但这里有一个陷阱。C 语言不希望在编译单元中多次出现相同的类型声明或定义。

想象一下,一个人使用了rect,但他也想使用point,所以他将point.h文件包含到他的源文件中,但此时rect.h文件中包含的point.h会生效,你就有了同一东西的两个定义。

另一种情况是,一个人使用了rectcircle,所以她将rect.hcircle.h都包含到她的源文件中。现在rect.hcircle.h各自都包含point.h。错误:对名为point的东西的多次声明、早期声明或重定义。

防止这种情况的方法是使用宏保护,也称为 include guards、header guards 等。我坚信了解如何在不使用 inclusion guards 的情况下包含所有头文件是有益的,但 inclusion guards 是必需的。

此外,了解struct的裸声明用法也是好的,而使用typedef关键字可以使事情更方便。

示例 2

point.h

#ifndef POINT_H
#define POINT_H

typedef struct point {
    unsigned x;
    unsigned y;
} point;

point point_new(unsigned, unsigned);
point point_move(point, int, int);
void point_movep(point *, int, int);

#endif

point.c

#include "point.h"

point point_new(unsigned x, unsigned y) {
    struct point t;
    t.x = x;
    t.y = y;
    return t;
}

point point_move(point t, int dx, int dy) {
    t.x = t.x + dx > 0 ? t.x + dx : 0;
    t.y = t.y + dy > 0 ? t.y + dy : 0;
    return t;
}

void point_movep(point * t, int dx, int dy) {
    t->x = t->x + dx > 0 ? t->x + dx : 0;
    t->y = t->y + dy > 0 ? t->y + dy : 0;
}

rect.h

#include "point.h"

#ifndef RECT_H
#define RECT_H

typedef struct rect {
    point p;
    unsigned w;
    unsigned h;
} rect;

rect rect_new(point, unsigned, unsigned);
rect rect_move(rect, int, int);
rect rect_size(rect, unsigned, unsigned);
void rect_movep(rect *, int dx, int dy);
void rect_sizep(rect *, unsigned w, unsigned h);

#endif

rect.c

#include "rect.h"

rect rect_new(point p, unsigned w, unsigned h) {
    rect t;
    t.p.x = p.x;
    t.p.y = p.y;
    t.w = w;
    t.h = h;
    return t;
}

rect rect_move(rect t, int dx, int dy) {
    return rect_new(point_move(t.p, dx, dy), t.w, t.h);
}

rect rect_size(rect t, unsigned w, unsigned h) {
    t.w = w;
    t.h = h;
    return t;
}

void rect_movep(rect * t, int dx, int dy) {
    point_movep(&t->p, dx, dy);
}

void rect_sizep(rect * t, unsigned w, unsigned h) {
    t->w = w;
    t->h = h;
}

example2.c

#include "rect.h"
#include "point.h"

#include <stdio.h>

int main(void) {
    rect a = rect_new(point_new(5, 10), 20, 30);
    printf("rectangle at (%d, %d), width %d, height %d\n", a.p.x, a.p.y, a.w, a.h);

    a = rect_size(rect_move(a, 10, 5), 40, 50);
    printf("rectangle at (%d, %d), width %d, height %d\n", a.p.x, a.p.y, a.w, a.h);

    rect_movep(&a, -5, -5);
    rect_sizep(&a, 60, 70);
    printf("rectangle at (%d, %d), width %d, height %d\n", a.p.x, a.p.y, a.w, a.h);
    
    return 0;
}

输出

rectangle at (5, 10), width 20, height 30
rectangle at (15, 15), width 40, height 50
rectangle at (10, 10), width 60, height 70

#ifndef预处理器指令就像任何普通的if语句一样工作。如果条件为false,它将跳过下一个块。在预处理器术语中,这意味着从#ifndef#endif的文本不会进入编译单元。另一方面,如果宏POINT_H未定义,它将立即被定义,并且包含point类型定义的文本将进入:point.c、rect.cexample2.c的编译单元。

为了检查一下,我颠倒了point.hrect.hinclude指令,现在看起来好像rect是在point之前定义的。主要是,rect.h在第 1 行包含了point.h。当 C 预处理器到达example2.c中的#include "point.h"行时,由于一切都被保护起来,它将包含一个空的string、一个void、一个什么都没有……

字符串

C 字符串有很大的市场空间,可以专门写一本书。在这里,按值传递字符串的特殊情况值得关注。

要在 C 语言中按值传递字符串,你需要两样东西:

  1. 字符串应具有已知的固定大小
  2. 它应该嵌入到struct中。

前者是必需的,因为要按值将某物传递给函数,它必须是一个已定义的(defined)对象。编译器必须预先知道对象的尺寸,以便在函数的栈帧中预留所需的空间。

为了使其更像一个真实的示例,我决定像pointrect这样的对象拥有一个uuid string。还有什么比这更固定的吗?如果我决定放入一个完整的Person姓名而不是ID,没问题。我只需要选择一个任意大的字符串来表示它。比如 80 个字符。

有些名字不到 10 个字符,这样我就会浪费 70 个字节。有些名字需要 80 多个字符,谁在乎呢?你永远无法做到完美。这一切都是妥协,尤其是在 C 语言中处理string时。最常见的方法是指向在堆上分配的、以null结尾的stringchar指针。

我将使用这个 uuid 实现,并尊重作者:Paul J. Leach 和 Rich Salz。它依赖于Ronald L. Rivest 的 RSA Data Security, Inc. MD5 Message-Digest Algorithm 文档

带有版权声明的源文件包含在文章的存档中。我将把uuid.lib制作成静态库。在这里,只展示它们头文件的用法。

示例 3

point.h

#ifndef POINT_H
#define POINT_H

typedef struct {
    char id[40];
    unsigned x;
    unsigned y;
} point;

point point_new(unsigned, unsigned);
point point_move(point, int, int);

#endif

point.c

#include "point.h"
#include "sysdep.h"
#include "uuid.h"

#include <stdio.h>

point point_new(unsigned x, unsigned y) {
    long i;
    uuid_t u;
    point t;
    
    uuid_create(&u);
    sprintf(t.id, "%8.8x-%4.4x-%4.4x-%2.2x%2.2x-", u.time_low, u.time_mid,
        u.time_hi_and_version, u.clock_seq_hi_and_reserved,
        u.clock_seq_low);
    for (i = 0; i < 6; i++)
        sprintf(&t.id[24 + 2 * i], "%2.2x", u.node[i]);

    t.x = x;
    t.y = y;
    return t;
}

point point_move(point t, int dx, int dy) {
    t.x = t.x + dx > 0 ? t.x + dx : 0;
    t.y = t.y + dy > 0 ? t.y + dy : 0;
    return t;
}

rect.h

#include "point.h"

#ifndef RECT_H
#define RECT_H

typedef struct {
    char id[40];
    point p;
    unsigned w;
    unsigned h;
} rect;

rect rect_new(point, unsigned, unsigned);
rect rect_move(rect, int, int);
rect rect_size(rect, unsigned, unsigned);

#endif

rect.c

#include "rect.h"
#include "sysdep.h"
#include "uuid.h"

#include <stdio.h>

rect rect_new(point p, unsigned w, unsigned h) {
    long i;
    uuid_t u;
    rect t;

    uuid_create(&u);
    sprintf(t.id, "%8.8x-%4.4x-%4.4x-%2.2x%2.2x-", u.time_low, u.time_mid,
        u.time_hi_and_version, u.clock_seq_hi_and_reserved,
        u.clock_seq_low);
    for (i = 0; i < 6; i++)
        sprintf(&t.id[24 + 2 * i], "%2.2x", u.node[i]);

    t.p.x = p.x;
    t.p.y = p.y;
    t.w = w;
    t.h = h;
    return t;
}

rect rect_move(rect t, int dx, int dy) {
    return rect_new(point_move(t.p, dx, dy), t.w, t.h);
}

rect rect_size(rect t, unsigned w, unsigned h) {
    t.w = w;
    t.h = h;
    return t;
}

example3.c

#include "rect.h"
#include "point.h"

#include <stdio.h>

int main(void) {
    rect R = rect_new(point_new(5, 10), 20, 30);
    printf("rectangle R at (%d, %d), width %d, height %d\n", R.p.x, R.p.y, R.w, R.h);
    printf("rectangle R id %s\n", R.id);
    printf("point of rect R id %s\n", R.p.id);

    R = rect_size(rect_move(R, 10, 5), 40, 50);
    printf("rectangle R at (%d, %d), width %d, height %d\n", R.p.x, R.p.y, R.w, R.h);
    printf("rectangle R id %s\n", R.id);
    printf("point of rect R id %s\n", R.p.id);

    return 0;
}

compile

cl example3.c point.c rect.c ..\uuid\uuid.lib wsock32.lib

输出

rectangle R at (5, 10), width 20, height 30
rectangle R id 2afc6b63-e09a-11eb-babc-e837ef16bb95
point of rect R id Ю№
rectangle R at (15, 15), width 40, height 50
rectangle R id 2afeccc2-e09a-11eb-babc-e837ef16bb95
point of rect R id ╝№

它至少有两个问题。

rect中的point id是垃圾,我忘了复制id。如果我不断为point添加新属性,我将不得不记住在rect中添加代码。这很糟糕!我对point本已宽松的封装的蔑视害了我。

为了解决这个问题,不要写

t.p.id = p.id;
t.p.x = p.x;
t.p.y = p.y;

而是简单地写

t.p = p;

始终编写要做什么,而不是如何去做。让点自己处理自己。

请注意,t.p.id = p.id将无法按预期工作。这是一个错误。你不能像int那样按值分配数组。你必须将数组嵌入struct或将其强制转换为struct

第二个问题是rectid发生了变化。我选择过度设计rect_move函数。为了让它只有一行,我重用了point_moverect_new,但这有点过头了。过度设计是一种弱点。应该尽可能少地完成工作。

在这里,我可以使用那些不体面的过程之一,它们获取point的地址并修改其xy,像在使用更高级的语言中的嵌套函数一样玩游戏。因为point_movep过程的栈帧将位于rect_move函数栈帧的上方,所以它是安全的。

或者,我可以使用point_move函数将整个点从rect复制出来,修改副本的xy,然后将副本返回以替换rect中的旧point。这听起来更具函数式风格。

或者……稍后可以做一些更邪恶的事情。

关于“函数应有的最小尺寸是多少?”这个问题。在示例 3 中,point_move是两行,rect_move是一行……函数只有一行/两行的理由是什么?与“始终编写要做什么,而不是如何去做”相符的另一句格言是始终用领域语言进行编程。也就是说,看到“移动点”比看到t->x = t->x + dx > 0 ? t->x + dx : 0更好。

我对point_newrect_new中代码的丑陋感到有点厌烦。uuid_create过程之后的那些代码。它们是重复的,并且会继续在我创建的任何使用uuid.lib的数据类型中重复。也许是时候将它们放入自己的过程中,并将该过程放入新的源文件中,或者放入原始uuid库创建者的某个源文件中,这感觉有点不道德。

我使用“过程”一词来指代不返回任何内容并就地修改对象的特殊函数。**示例 2**中的那些void过程。如果将它们变成接受要修改对象的地址,并在完成后将其地址返回给调用者,以便可以链式调用函数,那么它们可能会更有用。

示例 4

id40.h

#ifndef ID40_H
#define ID40_H

typedef char id40[40];
typedef struct {
    id40 x;
} str40;

void id40_set(char *);

#endif

id40.c

#include "id40.h"
#include "sysdep.h"
#include "uuid.h"

#include <stdio.h>

void id40_set(id40 t) {
    long i;
    uuid_t u;
    
    uuid_create(&u);
    sprintf(t, "%8.8x-%4.4x-%4.4x-%2.2x%2.2x-", u.time_low, u.time_mid,
        u.time_hi_and_version, u.clock_seq_hi_and_reserved,
        u.clock_seq_low);
    for (i = 0; i < 6; i++)
        sprintf(&t[24 + 2 * i], "%2.2x", u.node[i]);
}

point.h

#include "id40.h"

#ifndef POINT_H
#define POINT_H

typedef struct {
    id40 id;
    unsigned x;
    unsigned y;
} point;

point point_new(unsigned, unsigned);

int point_equals(point, point);
int point_equalsp(point *, point *);

point point_move(point, int, int);
point * point_movep(point *, int, int);

#endif

point.c

#include "point.h"

point point_new(unsigned x, unsigned y) {
    point t;
    
    id40_set(t.id);
    t.x = x;
    t.y = y;
    return t;
}

int point_equals(point a, point b) {
    return a.x == b.x && a.y == b.y;
}

int point_equalsp(point * a, point * b) {
    return a == b;
}

point point_move(point t, int dx, int dy) {
    t.x = t.x + dx > 0 ? t.x + dx : 0;
    t.y = t.y + dy > 0 ? t.y + dy : 0;
    return t;
}

point * point_movep(point * t, int dx, int dy) {
    t->x = t->x + dx > 0 ? t->x + dx : 0;
    t->y = t->y + dy > 0 ? t->y + dy : 0;
    return t;
}

rect.h

#include "id40.h"
#include "point.h"

#ifndef RECT_H
#define RECT_H

typedef struct {
    char id[40];
    point p;
    unsigned w;
    unsigned h;
} rect;

rect rect_new(point, unsigned, unsigned);

int rect_equals(rect, rect);
int rect_equalsp(rect *, rect *);

rect rect_move(rect, int, int);
rect * rect_movep(rect *, int, int);

rect rect_size(rect, unsigned, unsigned);
rect * rect_sizep(rect *, unsigned, unsigned);

#endif

rect.c

#include "rect.h"

rect rect_new(point p, unsigned w, unsigned h) {
    rect t;

    id40_set(t.id);
    t.p = p;
    t.w = w;
    t.h = h;
    return t;
}

int rect_equals(rect a, rect b) {
    return point_equals(a.p, b.p) && a.w == b.w && a.h && b.h;
}

int rect_equalsp(rect * a, rect * b) {
    return a == b;
}

rect rect_move(rect t, int dx, int dy) {
    t.p = point_move(t.p, dx, dy);
    return t;
}

rect * rect_movep(rect * t, int dx, int dy) {
    point_movep(&t->p, dx, dy);
    return t;
}

rect rect_size(rect t, unsigned w, unsigned h) {
    t.w = w;
    t.h = h;
    return t;
}

rect * rect_sizep(rect * t, unsigned w, unsigned h) {
    t->w = w;
    t->h = h;
    return t;
}

example4.c

#include "rect.h"
#include "point.h"

#include <stdio.h>

int main(void) {
    rect R = rect_new(point_new(5, 10), 20, 30);
    printf("rectangle R at (%d, %d), width %d, height %d\n", R.p.x, R.p.y, R.w, R.h);
    printf("rectangle R id %s\n", R.id);
    printf("point of rectangle R id %s\n", R.p.id);

    R = rect_size(rect_move(R, 10, 5), 40, 50);
    printf("rectangle R at (%d, %d), width %d, height %d\n", R.p.x, R.p.y, R.w, R.h);
    printf("rectangle R id %s\n", R.id);
    printf("point of rectangle R id %s\n", R.p.id);
    
    {
        rect S = rect_new(R.p, 88, 77);
        rect * T = &S;
        printf("rectangle S at (%d, %d), width %d, height %d\n", S.p.x, S.p.y, S.w, S.h);
        printf("rectangle S id %s\n", S.id);
        printf("point of rect S id %s\n", S.p.id);
        printf("rectangle R and rectangle S are %s\n", 
                rect_equals(R, S) ? "equal" : "unequal");
        printf("points of rectangle R and S are %s\n", 
                point_equals(R.p, S.p) ? "equal" : "unequal");
        printf("rect R and rect S are %s object\n", 
                rect_equalsp(&R, &S) ? "the same" : "not the same");
        printf("points of rect R and S are %s object\n", 
                point_equalsp(&R.p, &S.p) ? "the same" : "not the same");
        *T = rect_new(R.p, R.w, R.h);
        printf("rectangle R and rectangle *T are %s\n", 
                rect_equals(R, *T) ? "equal" : "unequal");
        printf("rect R and rect *T are %s object\n", 
                rect_equalsp(&R, T) ? "the same" : "not the same");
        printf("rect S and rect *T are %s object\n", 
                rect_equalsp(&S, T) ? "the same" : "not the same");        
    }
    
    return 0;
}

compile

cl example4.c id40.c point.c rect.c ..\uuid\uuid.lib wsock32.lib

输出

rectangle R at (5, 10), width 20, height 30
rectangle R id 86122912-bf91-11eb-bddc-e1995e751a4c
point of rectangle R id 860fc623-bf91-11eb-bddc-e1995e751a4c
rectangle R at (15, 15), width 40, height 50
rectangle R id 86122912-bf91-11eb-bddc-e1995e751a4c
point of rectangle R id 860fc623-bf91-11eb-bddc-e1995e751a4c
rectangle S at (15, 15), width 88, height 77
rectangle S id 86122913-bf91-11eb-bddc-e1995e751a4c
point of rect S id 860fc623-bf91-11eb-bddc-e1995e751a4c
rectangle R and rectangle S are unequal
points of rectangle R and S are equal
rect R and rect S are not the same object
points of rect R and S are not the same object
rectangle R and rectangle *T are equal
rect R and rect *T are not the same object
rect S and rect *T are the same object

typedef只是一个名称别名,它不会创建新的二进制类型。为了在rect.h中说明这一点,使用了char id[40]而不是id40,编译器不会报错。

示例 4 中最有趣的是str40的类型声明,它本身在代码中已过时。我们需要声明这个嵌入了 40 个char的数组的struct,以便能够将一个数组分配给另一个数组。例如

*(str40 *)t.p.id = *(str40 *)p.id;

而不是处理struct中真实的数组,我们对数组进行类型转换。首先将其转换为上述struct的指针,然后解引用该指针以获取真实的struct。现在,我们已经按值复制了一个数组。

坦率地说,C 语言中t.p.idp.id的标识符代表的不是一个数组,而是一个常量地址。这就是为什么我们首先需要将该地址分配给一个指针,然后解引用该指针才能获得struct/数组的实际内容。

附录

让我们用一些命令行工具来完成这项工作。请注意,任何时候看到提到htons之类的构建错误,都需要在构建过程中包含一个名为ws2_32wsock32的库。

Embarcadero 免费 C++ 编译器

这是一个现代的 32 位 C/C++ 基于 Clang 的编译器,支持 C11。可以在[此处]下载。

将其解压到某个文件夹,例如C:\LANG,这样目录结构就是C:\LANG\BCC102\bin。接下来,我们需要将其添加到PATH系统变量。右键单击“我的电脑”,选择“属性”,然后选择“高级系统设置”。在“系统属性”窗口的“高级”选项卡下,有一个“环境变量”按钮可以点击。

如果你在“用户变量”列表中看到一个“Path”条目,请单击“编辑”。添加一个新值C:\LANG\BCC102\bin。如果不存在 Path 变量,请单击“新建”并添加变量名:“Path”,变量值:“C:\LANG\BCC102\bin”。

现在你可以打开命令提示符并输入bcc32x。如果一切正常,你将看到编译器显示其版本为 7.30 for Win32。暂时不要关闭窗口。从本文下载示例源文件并解压,例如放在C:\Source文件夹中。它里面有五个文件夹,目录结构是C:\Source\Example3 等。

将控制台中的目录更改为C:\Source\uuid

cd \Source\uuid

现在你需要编译源代码,然后创建uuid库。

bcc32c -c md5c.c sysdep.c uuid.c
tlib uuid.lib /u /a /C +md5c +sysdep +uuid

让我们进入一个示例的目录,创建一个可执行文件。

cd ..\example4
bcc32c example4.c id40.c point.c rect.c ..\uuid\uuid.lib

好了。Borland/Embarcadero 编译器不会抱怨未定义的引用htons

Mingw-w64

“Minimalist GNU for Windows”是一个免费开源软件开发环境,是 GNU Compiler Collection 的移植版本。对于那些想编译 64 位程序的人,可以在[TCL 的页面]找到打包了一些有用工具和库的独立编译器版本。

下载,执行,然后告诉自解压压缩包解压到C:\LANG文件夹。它会创建一个MinGW子文件夹。将C:\LANG\MinGW\bin目录添加到PATH环境变量,就像我们之前为 Embarcadero 编译器所做的那样。

打开命令提示符。输入gcc -v进行测试。这将显示 GNU C 编译器的版本,在撰写本文时是 9.2.0。

切换到\Source\uuid目录,让我们编译并创建uuid库。

gcc -c uuid.c md5c.c sysdep.c
ar ru libuuid.a uuid.o md5c.o sysdep.o

现在,就像我们之前做的那样,我们需要显式地在构建中包含libuuid.a静态库

gcc example4.c id40.c point.c rect.c ..\uuid\libuuid.a -o example4.exe

或者创建它的对象文件

gcc example4.c id40.c point.c rect.c ..\uuid\uuid.o ..\uuid\md5c.o ..\uuid\sysdep.o

在这里,链接器抱怨一些未定义的引用,称为__imp_ntohs……

为了解决这个问题,我们必须在构建过程中包含编译器自己的ws2_32wsock32库。

gcc example4.c id40.c point.c rect.c ..\uuid\libuuid.a -lwsock32 -o example4

gcc example4.c id40.c point.c rect.c ..\uuid\libuuid.a -lws2_32 -o example4

在 MinGW 编译器工具集的文件夹层次结构中的某个地方,有一个文件libwsock32.a可以完成这项工作。

Visual C++ Toolkit 2003

最后但同样重要的是,微软免费提供的与 Visual Studio .NET 2003(无 IDE)相同的 C/C++ 编译器。这是一个基本的 C89 编译器。要用于 win32 应用程序(适用于从 Windows 95 到最新的 Windows 10),你需要Platform SDK。Borland、MinGW、LCC、Peles's 和其他 Windows 编译器都包含自己的平台文件。

让我们武装这个工具,让它能造成“破坏”。首先,获取编译器。在 Google 上搜索文件VCToolkitSetup.exe

告诉安装向导安装到C:\LANG\VS2003\,这本身就会设置一个名为VCToolkitInstallDir的环境变量,但你需要将C:\LANG\VS2003\BIN添加到PATH,所以这样做。

现在你可以编译学术 C 代码,这些代码可以创建:链表、二叉树、矩阵乘法、将内容写入控制台……但你无法创建 win32 GUI 应用。从[CNET]下载 Windows Server 2003 R2 Platform SDK 的 IMG 格式。

使用类似7z的工具解压它,然后启动安装程序。选择自定义安装。尽管 Platform SDK 以 Y2K 标准来说很大,但我们实际上只需要它的一小部分(像windows.h这样的头文件和 VCToolkit 中缺少的一些构建工具)。告诉安装向导将文件放在我们获取编译器的同一个目录(C:\LANG\VS2003)中,这样我们就无需为 Windows 设置额外的环境变量。

当它到达“检查下面的选项以选择和取消选择单个功能”窗口时,通过单击主功能框来取消选择所有选项。这将用红叉标记它。现在打开“Microsoft Windows Core SDK”框,选择:“Build Environment (x86 32-bit)”和“Tools (AMD 64-bit)”。完成安装。

技术上来说,你现在有两个编译器:Visual C++ 2003 32 位编译器和 Visual C++ 2005 Express 64 位编译器。我们将坚持使用前者。将 AMD64 位工具添加到PATH,它们应该在C:\LANG\VS2003\Bin\win64\x86\AMD64,但在列表中放在C:\LANG\VS2003\BIN之后。

你需要向 Windows OS 添加两个新的环境变量:INCLUDELIB。它们的值分别是:C:\LANG\VS2003\INCLUDEC:\LANG\VS2003\LIB

再次打开命令提示符。转到你解压了示例的uuid目录。

cl -c uuid.c md5c.c sysdep.c
lib -nologo -out:uuid.lib uuid.obj md5c.obj sysdep.obj

让我们转到example4文件夹并构建它,这次包含wsock32.lib,而不等待链接器抱怨。

cl example4.c id40.c point.c rect.c ..\uuid\uuid.lib wsock32.lib

祝您编码愉快!

历史

  • 2021 年 7 月 9 日:初始版本
  • 2021 年 7 月 10 日:更新
© . All rights reserved.