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






1.50/5 (6投票s)
C 语言、纯函数、多文件、栈和堆
引言
函数的最佳大小是多少?一个函数应该做多少事情?应该有的最小尺寸是多少?一行代码有意义吗?我根据什么标准将函数归类到文件里?
这适合那些有比较扎实 C 语言学术背景的人。熟悉中等难度的算法任务,但缺乏实际编码经验。
纯函数
没有数据的函数是没有意义的。有些函数处理整个数据类型,有些则处理它的每一个基本方面。如果你有一个Person
数据类型;前者操作的是Persons
(复数),后者操作的是Person
(单数)的属性,例如:mass
(质量)、age
(年龄)、name
(姓名)、address
(地址)、picture
(图片)......
我发现了一个更间接的解决方案来解决我的“函数大小问题”,那就是纯函数。纯函数没有副作用。它们通过参数复制数据,进行任何必要的计算,然后返回结果。给定相同的参数,纯函数总是返回相同的结果。
纯函数只向外部世界返回一个值,它在不提及代码行数之类数量的情况下就说明了它的大小。你应该只有一个作用。你不应该在一个函数中使用指针来修改 5 个不同类型的外部对象。
一些基本值与其他值有关联,单独没有意义。它们代表一个逻辑实体。例如,一个三维点可以代表Person
的位置。将对象的point
位置作为point
返回,比编写三个不同的函数来返回它的x
、y
和z
坐标更实用。
Python 等语言能够从函数返回多个值,我认为 C 语言比这更合适。你为什么想从一个函数中返回Person
的:mass
、picture
和address
?你应该将一组代表逻辑实体的基本值,以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
是被调用者。在调用栈上,被调用者总是在调用者的上方。
这里的关键是:类型为point
和rect
的对象作为被调用者的局部变量创建,并将它们返回给调用者,在调用者函数栈帧中再次作为局部变量。没有指针,没有堆分配,当main
函数退出时,一切都会被干净地清理干净。
即使你使用了堆并且忘记释放分配的内存,所有东西也会被干净地清理干净,因为当程序终止时,运行该程序的操作系统进程也会终止,不会有内存泄漏。一开始就是虚拟内存,但你不想将带有内存泄漏的嵌入式设备送到太空。
每个 C 语言新手过一段时间都会注意到,“高手”们不再将整个对象传递给被调用者,而是只传递其指针。这通常被认为是更快、更有效率。结构体越大,传递指针比传递整个结构体更有效。你在这里同样可以使用这一点。
栈是计算机技术中最常用的数据结构。它非常重要,以至于人们在提高其效率方面做了很多工作。CPU 有硬件支持:堆栈指针、将数据移入和移出堆栈的特殊指令……如果你能指出任何在 CPU 缓存中的东西,那很可能就是堆栈。为每个对象在堆上分配内存,当你稍后解引用该对象时,将导致缓存未命中。
如果为了虚拟机的利益可以牺牲效率,我愿意为了效率而将整个结构体复制到堆栈上,我敢打赌 C 语言能比任何虚拟机做得更快。但是,栈上的大对象(千字节以上)会削弱其目的,因为你无法将它们中的许多放入 CPU 缓存。
函数调用以与所有其他事物相同的后进先出(First-In-Last-Out)方式堆叠在栈上,这意味着调用者的局部变量是真实的,你可以将它们的地址传递给被调用者。我们将向point
和rect
类添加一些使用此功能的函数。这是对某些语言中嵌套函数思想的一种“杂种化”。例如,Pascal 在嵌套函数时使用一个隐藏的参数,使其能够访问包含函数的局部变量。这是指向调用者栈帧的指针。
与另一个杂种化类似,它使用一个指向对象的隐藏参数,这样看起来y->f(x)
就不是f(&y, x)
了……
受人尊敬的大师 Qc Na 曾告诉他的学生 Anton:“对象是穷人的闭包”,以及“闭包是穷人的对象”。
C 语言中的多文件分解
有两种数据类型,point
和rect
(矩形)。它们各自由两个文件表示。一个是带有.h
扩展名的头文件,另一个带有.c
扩展名的文件在 C 语言术语中称为源文件。
第一个文件是接口,第二个文件是实现。这是 C 语言的公平玩法,由你决定。你可以切换文件的扩展名,一切都会正常工作,前提是你要在include
语句中和调用编译器时进行切换。
C 语言中的头文件就像纯文本一样插入到源文件中#include
指令所在的位置。从源文件生成一个新的临时文件,其中包含所有插入的头文件。这就是编译器将其翻译成二进制文件的东西。因此,它被称为翻译单元。
这五个文件中的代码可以放在一个.c
文件中,那么为什么我还要将其拆分成多个文件呢?
虽然人们似乎想将程序拆分以单独和分组逻辑内容,并创造某种乌托邦,但最有力的原因是,工作也可以拆分给多个人。每个人负责一个或多个源文件。
我用什么标准来这样划分它们,为什么我没有把所有_move
函数放在一个文件里,把_new
函数放在另一个文件里?
标准是数据。数据和代码是程序相等的组成部分,但数据更平等。
你围绕数据类型组织函数和源文件,可以将每种数据类型及其功能视为一个编程库。无论你为某个类型添加什么新功能(例如这里的point),将其添加到point.c文件中,而不是在当前编码的任何其他文件中就地使用,这被认为是良好的实践。
数据类型以及所有操作该数据的函数称为一个类。仅通过其类型接口处理对象称为封装。
示例 1 封装性较差。不仅可以窥探和修改数据类型,而且函数中的支持不足以避免直接使用。另外,我干扰了point
的封装。在函数rect_new
中,当我设置rect.point
的值时:t.p.x = p.x
,t.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会生效,你就有了同一东西的两个定义。
另一种情况是,一个人使用了rect
和circle
,所以她将rect.h和circle.h都包含到她的源文件中。现在rect.h和circle.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.c和example2.c的编译单元。
为了检查一下,我颠倒了point.h和rect.h的include
指令,现在看起来好像rect
是在point
之前定义的。主要是,rect.h在第 1 行包含了point.h。当 C 预处理器到达example2.c中的#include "point.h"
行时,由于一切都被保护起来,它将包含一个空的string
、一个void
、一个什么都没有……
字符串
C 字符串有很大的市场空间,可以专门写一本书。在这里,按值传递字符串的特殊情况值得关注。
要在 C 语言中按值传递字符串,你需要两样东西:
- 字符串应具有已知的固定大小
- 它应该嵌入到
struct
中。
前者是必需的,因为要按值将某物传递给函数,它必须是一个已定义的(defined)对象。编译器必须预先知道对象的尺寸,以便在函数的栈帧中预留所需的空间。
为了使其更像一个真实的示例,我决定像point
和rect
这样的对象拥有一个uuid string
。还有什么比这更固定的吗?如果我决定放入一个完整的Person
姓名而不是ID
,没问题。我只需要选择一个任意大的字符串来表示它。比如 80 个字符。
有些名字不到 10 个字符,这样我就会浪费 70 个字节。有些名字需要 80 多个字符,谁在乎呢?你永远无法做到完美。这一切都是妥协,尤其是在 C 语言中处理string
时。最常见的方法是指向在堆上分配的、以null
结尾的string
的char
指针。
我将使用这个 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
。
第二个问题是rect
的id
发生了变化。我选择过度设计rect_move
函数。为了让它只有一行,我重用了point_move
和rect_new
,但这有点过头了。过度设计是一种弱点。应该尽可能少地完成工作。
在这里,我可以使用那些不体面的过程之一,它们获取point
的地址并修改其x
和y
,像在使用更高级的语言中的嵌套函数一样玩游戏。因为point_movep
过程的栈帧将位于rect_move
函数栈帧的上方,所以它是安全的。
或者,我可以使用point_move
函数将整个点从rect
复制出来,修改副本的x
和y
,然后将副本返回以替换rect
中的旧point
。这听起来更具函数式风格。
或者……稍后可以做一些更邪恶的事情。
关于“函数应有的最小尺寸是多少?”这个问题。在示例 3 中,point_move
是两行,rect_move
是一行……函数只有一行/两行的理由是什么?与“始终编写要做什么,而不是如何去做”相符的另一句格言是始终用领域语言进行编程。也就是说,看到“移动点”比看到t->x = t->x + dx > 0 ? t->x + dx : 0
更好。
我对point_new
和rect_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.id
和p.id
的标识符代表的不是一个数组,而是一个常量地址。这就是为什么我们首先需要将该地址分配给一个指针,然后解引用该指针才能获得struct
/数组的实际内容。
附录
让我们用一些命令行工具来完成这项工作。请注意,任何时候看到提到htons
之类的构建错误,都需要在构建过程中包含一个名为ws2_32
或wsock32
的库。
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_32
或wsock32
库。
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 添加两个新的环境变量:INCLUDE
和LIB
。它们的值分别是:C:\LANG\VS2003\INCLUDE和C:\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 日:更新