因为需要研究PostgreSQL源码,故需要了解C的工程知识,虽说C的基础很简单,语法集合很少,但是对于大型系统来说,但由于C缺少一些现代语言的包,类等概念,在源码经常看到一些约定俗成的“利用C来实现高级语言语法特性”的知识,在《C语言的495个问题》中,有详细的说明,下面我们就一起看看吧。
1 声明和初始化
基本类型
1.1 各类型区别
- 正确如下,已在mac和ubuntu中验证(书中说法是至少,但实际按如下记即可)
- char 1byte
- short int
- int 4 byte
- long int 8 byte
- long long 8 byte
# include <stdio.h>
int main() {
printf("char: %lu\n", sizeof(char)); // 1
printf("short int: %lu\n", sizeof(short int)); // 2
printf("int: %lu\n", sizeof(int)); // 4
printf("long: %lu\n", sizeof(long)); // 8
printf("long long: %lu\n", sizeof(long long)); // 8
return 0;
}
1.2 为什么不精确定义标准类型的大小
C语言相对低级,其设计理念认为类型大小应该有具体实现来决定, 虽然这很容易出矛盾
1.3 因为C没有精确定义标准类型大小,那么用typedef定义int16和int32是否能解决问题呢
是的,但其实标准头文件<inttypes.h>
中已经定义了int16_t
和uint_32_t
类型
1.4 新64位机上64位类型是什么样的
1.5 char *p1, p2
有什么问题
- 这样表示p1是指针,但p2是值。
- 应改为
char *p1, *p2;
1.6 malloc用法
正确方式如下, 注意malloc函数返回的是指针
#include <stdlib.h>
int main() {
char *p;
p = malloc(10);
return 0;
}
声明风格
1.7 全局变量和函数应该怎么定义&&声明?
- 可以声明多次,但只能定义一次。
- 一般在.c文件中定义,在.h文件·中声明,这样使用者只要引用了.h文件就拥有了声明。
- 变量声明是
extern int i;
- 函数声明是
extern int f();
- 变量定义是
int i = 0;
- 函数定义是
int f() {return 1;}
- 尤其是把外部函数的声明放在.h中,而不是.c中,防止外部函数原型变化了,但.c文件中忘记更改导致错误的函数原型贻害无穷。
- c文件一般也要引用对应的h文件,这样编译器可以检查c和h文件中声明与定义是否一致。
1.8 怎么在C中实现不透明数据类型?
见2.4
1.9 怎么生成半全局变量,就是那种只能被部分源文件中国的函数访问的变量?
- C原生不支持,
- 为了实现变量分包来管理, 有2种解决方案:
- 包内的变量都带固定前缀,“口头约定”使用者不能定义相同前缀的变量
- 变量用下划线开头
存储类型
1.10 static函数内的变量都必须是static的吗
尽量这样做
1.11 extern 函数声明的作用
- 可以当做一种格式上的提示,指明函数的定义可能在别的c文件中
- 其实
extern int f();
和int f();
没有区别
1.12 auto的作用
没啥用,过时了
类型定义typedef
1.13用户定义的类型,typedef
和#define
的区别?
- 尽量用typedef,因为能更好的处理指针类型
-
#define
主要是配合#ifdef
宏使用的
1.14 定义链表报错
- struct可以包含“指向 自己类型 的指针”,但要提前声明该struct类型
- 以下三种均可
struct node {
char *item;
struct node *next;
};
typedef struct node *NodePtr;
int main() {
NodePtr a;
NodePtr b;
return 1;
}
struct node;
typedef struct node *NodePtr;
struct node {
char *item;
NodePtr *next;
};
int main() {
NodePtr a;
NodePtr b;
return 1;
}
typedef struct node {
char *item;
struct node *next;
} *NodePtr;
int main() {
NodePtr a;
NodePtr b;
return 1;
}
1.15 定义一对相互引用的结构
以下两种均可
struct a {
int field;
struct b *bpointer;
};
struct b {
int bfield;
struct a *apointer;
};
int main() {
struct a aa;
struct b bb;
return 1;
}
struct a;
struct b;
typedef struct a *APTR;
typedef struct b *BPTR;
struct a {
int afield;
BPTR bpointer;
};
struct b {
int bfield;
APTR pointer;
};
int main() {
APTR aa;
BPTR bb;
return 1;
}
1.16 struct定义和typedef struct定义的区别
见2.1
1.17 typedef int (*funcptr) ();
是什么意思?
funcptr fp1, fp2;
int (*pf1) (), (*pf2) ();
1.18 const限定词
typedef char *charp;
const charp p;
const修饰的是指针p,而不是指针p所指向的变量
1.19 为什么数组 维度值中,不能用const值
const int n = 5;
int a[n];
是不允许,会报错
1.20 const char *p, char const *p, char *const p
的区别?
见11.10和1.21
复杂的声明
1.21 非常复杂的声明怎么理解,定义一个包含N个指向返回指向字符的指针的函数的指针的数组?
1.22 用C定义状态机
略
数组大小
1.23 能不能声明和传入数组大小一直的局部数组,或者有参数指定大小的参数数组?
不能
1.24 file1.c中定义extern数组,在file2.c中,为什么用sizeof取不到数组大小
// file1.c
int array[] = {1,2,3};
// file2.c
extern int array[];
- 未指定数组大小的extern数组,是不完全类型,不能用sizeof
- 有如下三种方案
// file1.c
int array[] = {1, 2, 3};
int arraysz = sizeof(array);
// file2.c
# include <stdlib.h>
# include <stdio.h>
extern int array[];
extern int arraysz;
int main() {
// int sz = sizeof(array);
// printf("%d\n", sz);
printf("%d\n", arraysz);
return 1;
}
// file1.h
#define ARRAYSZ 3
// file1.c
# include "file1.h"
int array[ARRAYSZ] = {1, 2, 3};
// file2.c
# include "file1.h"
# include <stdio.h>
extern int array[];
int main() {
printf("%d\n", ARRAYSZ);
return 1;
}
或者给数组末尾加哨兵位(如-1, NULL等)
声明问题
1.25 函数定义一次,调用一次,但编译器提示非法重复声明?
可能是函数没有声明,或者和引入的某头文件中的函数同名了。
1.26 void main()对吗
不对,见11.7
1.27 编译器包函数原型不匹配
见11.4
1.28 文件中第一个声明就爆出奇怪的语法错误,为什么
见10.9
1.29 编译器不允许我定义大数组,如double array[256][256]
见19.28和7.20
命名空间
1.30 怎么判断哪些标识符可以用,哪些被保留了
略 不要用下划线开头,容易出问题
初始化
1.31 没有显示初始化的值?全局变量初始化为0?
- static变量,未初始化时,C可以确保初始值是0
- 全局和局部变量,未初始化时,内容是未定义的垃圾内容,不能做任何有用的假设(包括用malloc和realloc申请的堆内存,也需要手动做初始化,否则也是未定义的垃圾内容)
1.32 为什么不能编译如下char[]代码
如果char[] a = "";
不行的话,可以用char *a = "";
或者char a[10]; strcpy(a, "");
1.33 编译器提示invalid initializers
不明白
1.34 以下初始化的区别
char[] a = "";
和char *a = "";
,字符串字面量是内存中只读区域的,有的编译器会控制其是否可写,所以对a的写操作是否报错,取决于编译器的种类
1.35 char{a[3]} = "abc"是合法的
对,是合法的
1.36 函数指针怎么初始化
extern int func();
int (*fp)() = func;
fp是指向函数的指针
1.37 能初始化union联合体吗
是,可以
2 struct,union和enum的区别
结构声明
2.1 struct和typedef struct的区别
用struct定义的话,使用时也得带上struct关键字,反之则不用
2.2 struct和typedef struct的使用
struct x {};
struct x a;
// 或
typedef struct x{} y;
y a;
// 或可以重名
typedef struct x{} x;
x a;
2.3 struct可以包含指向自己的指针么?
可以,见1.14和1.15
2.4 C语言,用什么方法实现抽象数据类型最好?
不明
2.5 C能否实现面向对象程序的一些好特性
- 把函数指针,放入struct,可以实现“类的方法”
- 继承:通过预处理器,或让积累的结构作为初始的子集
- 没有重载和覆盖
2.6 为啥声明报错
见11.6
2.7 struct内数组初始化很小的size,但实际装很大的容量,是一种通用的技术
通过struct,然后提供一个函数,可以初始化数组, 详细见数上的4个例子
2.8 struct变量作为函数的入参和出参,是合法的
这种方式传递的是浅拷贝,struct内的指针成员指向的还是原地址,而不是复制出新的一份儿
2.9 为什么不能用==和!=来做比较
遇到未使用的hole洞时会出问题,所以一般char*都是用strcmp比较而不是==
2.10 传参是struct时,内部发生了什么
- 传参是struct时,会把struct推到stack里,所以浪费stack空间
- 因此一般都传指针,而不是传struct
2.11 如何解说struct做传参,怎样创建无名的中间常量struct
可以用匿名struct
plotpoint((struct point){1,2});
或
plotpoint((struct point){.x=1, .y=2});
2.12 怎样读写文件
结构填充
2.13 内存对齐
- 当内存对齐时,很多机器能更高效的访问,因此编译器会的struct做内存对齐
- 比如按byte寻址的机器,2字节的short int必须放在偶数地址上,4byte的long int必须放在整数倍地址上
- 有些机器压根不能访问没有内存对齐的机器
- sizeof返回的是对齐后的值
2.15 如何确定域在struct内的字节偏移量
# define offsetof(type, f) ((size_t) \ ((char *)&((type *)0)->f -(char *)(type *) 0))
2.16 怎样在运行中用名字,访问结构中的域
offsetb = offsetof(struct a, b);
*(int *) ((char *)structp + offsetb) = value;
2.18 数组名可用作数组的基地址,但为啥struct不能这样呢?
- 数组,函数都如此
- 当你提到结构时,得到的是整个结构
2.19 程序运行正确,但退出时core dump了,为啥?
注意struct定义的末尾要加分号;
union
2.20 struct与union的区别
- union本质上是成员相互重叠的struct,可以从一种类型解释,亦可以从另一种类型解释。
- union的大小,是其最大成员的大小。
- struct的大小,是其所有成员大小之和。
- union和struct都有可能有内存对齐。
2.21 有办法初始化union吗
可以初始化union的任意成员
2.22 有没有办法跟踪union的哪个域,在使用?
没有
enum
2.23 enum和一组预处理的#define
有啥区别?
2.24 enum可移植吗
移植性很好
2.25 有什么显示enum符号的容易方法吗?
没有,可以自己写一个函数,把枚举常量值映射到字符串,或者用调试器
2.26 位域: 冒号和数字是什么意思
节省内存空间,用于struct内部
2.27 为什么大家喜欢用显示的掩码和位操作,而不是用位域
因为位域的移植性差
3 表达式
求值顺序
3.1 a[i] = i++
是未定义的
3.2 printf("%d\n", i++ * i++);
的结果出乎意料
尽量不要在一行里写多个i++之类的值,和人想象的结果可能有差异
3.3 i = i++
行为未定义
最好不要这么写
3.4 a ^= b ^= a ^= b
行为未定义
尽量不要这么写,在一个表达式中两次修改变量a的值,此行为是未定义的
3.5 不能通过括号强制执行计算顺序
只有部分作用
3.6 &&和||是有短路原则的
即若左边的子表达式即可决定最终结果,则右边的子表达式不会计算
如下是非常常见的正确用法
if (d != 0 && n / d > 0) {
// sth
}
if (p == NULL || *p == '\0') {
// sth
}
3.8 函数调用的参数,的求值顺序是不确定的
如下,可能f2()比f1()先执行
printf("%d %d", f1(), f2());
3.9 怎样避免写出“未定义”的表达式
书中的表述有些绕,就是尽量不要写类似a[i] = i++
之类的复杂表达式,避免歧义类似
3.11 写简单表达式的原则
- 一个表达式最多只修改一个对象
- 如果一个对象在一个表达式中出现一次以上,且在表达式中被修改,应确保每次的读操作都读取的是最终值
其他的表达式问题
3.13 i++与++i
-
i++
: i+1,返回的是原i值
-
++i
:i+1,返回的是+1后的值
3.15 比较三个数的大小
正确的方式是
if (a < b && b < c) {
}
而不是if(a<b<c),因为这样会比较0<c或1<c
3.16 类型溢出或截短
正确的方式是提前转换,如下是正确的
long int c = (long int) a * b;
// 或
long int c = (long int) a * (long int) b;
4 指针
基本的指针应用
4.1 指针的好处