C Programmer
C Programmer
《c-for-java-programmers》PDF参考书的笔记。
语法细节
-
在类型系统方面,java中有byte和boolean类型,而C中没有。但是在现代C编译器中,库stdbool提供了更明确的boolean的类型。
-
在C和Java中,单引号和双引号的效果都是不同的 。
-
在C中,不能通过
+
号拼接两个字符串。 -
C中没有直接打印数组的函数,需要自己实现。
-
在C的函数参数中,带有
const
的参数意味着函数内部不会对该参数进行变更。 -
在C中,函数在被调用前需要进行声明,如果函数的实现在其他文件中,也需要通过函数声明进行引用。而在Java中,函数的实现顺序则没有要求。
-
定义结构体
struct point{}
。其中point
并不是类型名,而是type tag,struct point
才是一个完整的类型。 -
C中还有一种特殊的结构体类型,称为
union
。在union
结构中,不同的变量共享同一块内存空间。
union value { int i; float f; };
union value v;
v.i = 123; // Set v to bits of integer 123
printf("%f\n", v.f); // Interpret bits of 123 as float
- 在常见的Unix系统中,
union
被用于实现面向对象编程的部分特性。然而,在低级系统编程之外,你很少需要自己使用它们。
内存管理
C语言中有三种主要的内存分配方式:
- 自动(Automatic):在定义本地变量和函数参数时,编译器在程序进入相应的作用域时为其分配内存空间。
- 静态(Static):在程序开始运行时,为静态变量分配内存空间,这些变量在整个程序的生命周期内存在。
- 动态(Dynamic):直到程序运行时才确定所需空间大小,使用堆(heap)进行分配,通常通过函数如malloc()和free()来实现。则由程序员控制。
C语言的动态内存分配
本节的副作用:您会明白为什么开发 C++ 和 Java 等语言是为了让程序员不必一遍又一遍地拼写。
在C中,使用malloc
函数来动态分配内存,并返回一个指向所分配内存的指针,其类型为void *
。如果内存不足,则malloc
会返回NULL
。
当使用malloc
分配内存后,需要进行类型转换,将void *
指针转换为所需的类型。
释放内存的责任落在开发者手中,需要使用free
函数来手动释放已分配的内存。释放内存后,最好将指针设置为NULL
,以避免出现悬空指针的情况,即指针指向的地址非法。
int * ip = (int*) malloc (sizeof(int));
// ...
free(ip);
ip = NULL;
动态数组 Dynamic Arrays
数组内存的申请,则需要通过malloc
的变种calloc
。
int * numbers = (int*) calloc(3, sizeof(int));
char* letters= (char*) calloc(5, sizeof(int));
若要创建一个字符串的内存空间,可用如下的方式:
char* str = (char*)calloc(length, sizeof(char));
若要用如上方式分配字符串空间,需要开发者确保最后一位为空字符。另外,若要创建一个有字符串组成的数组,则需要分两步:
char **s_array = (char**)calloc(array_len, sizeof(char*));
for (int i=0; i < length; i++) {
s_array[i] = (char*)calloc(this_str_len, sizeof(char));
}
这个范式也同样适用于其他二维数据结构空间的分配。注意,free
是同样需要单独进行释放。
推荐的做法是,在处理真实的数组时,使用
[]
运算符,而在通用动态分配的对象上,使用*
指针。然而,需要注意的是,字符串对象是一个例外情况,因为字符串通常被表示为以空字符结尾的字符数组,因此字符串对象总是被char *
类型的指针指向。
动态数据结构 Dynamic Data Structures
假设定义结构体如下struct point{int x; int y;};
,在C中分配内存并定义实例的语法如下所示:
struct point* p = (struct point*)malloc(sizeof(struct point));
上述语法较为啰嗦,因此有了CPP语言,否则只能采用宏定义优化语句的表达。
若要get和set相关字段,需要解引用,或者使用->
语法:
(*p).x = 1;
(*p).y = 99;
p->x = 1;
p->y = 99;
在C中,结构体类型中的成员默认是公开的,即没有私有类型。这意味着结构体中的变量和函数都可以被外部访问到。当在结构体类型中创建函数时,为了避免命名冲突,建议在函数名中添加类型名称。例如,对于point
类型中获取x
的函数,可以命名为point_get_x
。此外,在结构体类型中还可以创建指向自身类型的指针,这样可以实现诸如链表等数据结构。如下所示:
struct node { int value; struct node* next; };
在C语言中,可以通过使用for
循环来遍历各种数据结构,包括但不限于链表、树和图。这种方法是非常通用的,因为它利用了数据结构的迭代性质。
然而,需要注意的是,尽管创建这些数据结构相对容易,但释放它们占用的内存空间可能具有挑战性。释放内存时需要小心确保不会出现内存泄漏或者释放了仍然被使用的内存,这可能需要仔细设计和实现释放内存的函数。
for (struct node *p=head; p != NULL; p=p->next) {
printf("%d\n", p->value);
}
函数指针 function pointers
可以通过&
获取函数指针,又因为不带括号的函数名不是函数调用,可以直接用于获取函数指针。
因此,对于函数
int times2(int x){
return x * 2;
}
可以通过times2
或者×2
得到函数指针。方法如下:
int (*fp) (int) = times2; // 或者×2
调用该方法的过程如下:
int i = 999;
int j = (*fp)(i);
函数指针在C语言中具有多种作用:
- 函数回调:可以将函数指针作为参数传递给其他函数,使得调用者可以决定在执行过程中调用哪个函数,从而实现灵活的回调机制。
- 多态性:通过函数指针,可以实现类似于面向对象编程语言中的多态性。例如,在结构体中保存函数指针,然后根据具体的实例调用相应的函数实现,从而在运行时实现不同的行为。
- 动态加载:函数指针可以用于动态加载库中的函数。通过在运行时将函数指针指向库中的函数,可以实现动态加载和调用外部函数的功能。
- 模块化设计:使用函数指针可以将程序分解为更小的模块,每个模块负责实现特定的功能。这样可以提高代码的可维护性和可扩展性。
总的来说,函数指针是C语言中实现灵活性和可复用性的重要工具之一,能够使程序更加模块化、灵活和可扩展。
定义新类型 Defining New Types
在Java等面向对象语言中,使用class定义,即创建了新的类型。在C中则是使用typedef
创建新的类型别名。这个别名可以用来替代已存在的类型,从而增强代码的可读性和可维护性。注意声明的顺序,这就和定义变量一样,最后才是新类型的名称。
typedef int Distance;
一个常见的用法:
typedef char* String;
这种重命名的方式使得代码更加清晰。
另外,typedef
可以定义结构体类型,
typedef struct node {
int value;
struct node* next;
} Node;
这样就可以直接使用Node
,而不是struct node
。也可以使用typedef定义一个指向struct
的指针类型*Node
,如下代码所示。不过注意,malloc
中不能直接写Node
,否则会分配指针类型大小的空间。
typedef struct node * Node;
struct node {
int value;
Node next;
};
// ...
Node head = (Node)malloc(sizeof(struct node));
typedef也适用于函数指针类型,如下代码所示。其中PFII中的四个字母分别表示Pointer 、Function、input中的int、output中的int。
typedef int (*PFII)(int);
PFII the_func = times2;
下面是pointer to 输入两个字符串,输出int的函数。
typedef int (*PFSSI)(char *, char*);
PFSSI strcmp, numcmp;
这种类型的typedef
常用于回调函数类型的声明,特别是当涉及到复杂的函数指针声明时,它可以使代码更易读、更易理解。通过为函数指针类型定义别名,可以使得回调函数的含义更加明确,而不会因为大量的括号和星号而使其含义混淆不清。
Sharing code: files and libraries
在C 程序中,所有以#
开头的语句是预处理指令。在C 编译前,C 预处理器会首先会对源代码进行预处理。
一个常见的header file包含的文件内容:
- 函数声明
- 变量声明(添加extern)
- 宏定义
- include其他文件的声明
用fgets
而不是gets
。gets
似乎容易受到恶意用户的攻击。
在使用scanf
时,同样需要考虑输入长度限制的问题。
若你不知道一行的最大长度,你需要增量读取(fgetc或者fread)和增量的内存分配(malloc和realloc)。
如果想在C中实现类似泛型的功能,需要使用C中的宏定义。(这也是C++开始的地方。
若要构建大型的C程序,推荐使用makefile和make命令。其基本组成如下:
hello: hello.c
cc -o hello hello.c
hello程序依赖于hello.c,若hello不是最新的,则执行第二行语句重新构建。
更常用的makefile内容如下所示:
PROG = scanner
SRCS = scanner.c util.c
OBJS = ${SRCS:%.c=%.o}
$(PROG): $(OBJS)
$(CC) -o $(PROG) $(OBJS)
clean:
-rm $(OBJS)
更多内容参考:https://www.gnu.org/software/make/
Debugging a C Program
-
使用
%p
或者0x%lx
打印指针地址,%lx
是16进制的,内容更简短。因为大多时候地址仅被用于判断两个指针地址是否相同。 -
使用gdb或者lldb对C程序进行debug,C程序在编译时需要添加
-g
选项,并最好添加-Wall -Werror
,这表示编译器报告所有的warnings,并让所有的warnings变成errors。 -
对于初学者,推荐增加
-std=c99
编译参数。 -
为应对内存泄漏,首先,谨慎设计动态数据结构。对于结构类型(struct)命名为Thing,包含一个构造器new_Thing,一个析构器free_Thing,使用getters和setters(Thing_get_x,Thing_set_x)。避免在构造器外进行类型casts,除非必要情况(例如模仿泛型)。
-
其次,使用valgrind对程序进行内存泄漏检查。valgrind.org
C Program实践的建议
如何开始构建一个项目?首先需要进行的就是设计阶段:
- 你的程序需要对什么信息进行追踪?
- 你的程序处理的是何种类型的东西?
- 这些东西的关系是什么?
以金融信息系统为例
设计阶段
你有多少钱,这些钱保存在哪里?
- 设置金融账号,账号可分为银行账号、信用卡账号等
- 用户持有这些账号
- 账号需要有所属的机构和单位
- 账号中应记录余额信息
我感觉这有点像设计关系型数据库。
实现阶段
- 基于struct创建类
- 设计函数,实现类中数据的getter和setter
- 实现解决问题所需要的功能和过程
本书最后的一个建议
除非迫不得已,不要执行free
命令。如果不执行free,则不会有悬垂指针的bugs。当然,其他bugs可能依然存在。对于小型的课程项目来说,不会用完内存空间的。