最近又重新认真的学习了一下C语言的基础,看到很多值得注意的地方,现在总结下来,共大家谈论,也方便以后查阅。
本文章适合已经掌握一定C语言基础的朋友!
声明和初始化
1. 如何决定使用哪种数值类型?
Int,long,short的关系,原则上long保存的数值范围要不小于int,而short不大于int.
一般情况下,如果要保存的数据>32767,或者<-32767,一般使用long.如果需要考虑空间,那么一般使用short,除此之外使用int。
Char和int:char可以看成“小”整数使用,但有时编译器会为char到int的转换生成额外的代码,另外符号扩展也是很容易出问题的地方。
Float和double:这两个类型也涉及到了空间/时间的关系,因为很多编译器在表达式求值的时候,需要将float转换为double进行运算。
Unsigned使用:当定义明确的溢出特征很重要而正负值不重要时使用。
其实C语言类型大小没有精确的定义,能保证的只有一下几点:
虽然C语言相对于其他高级语言依然是一个低级语言,但它还是认为对象的具体大小应该由具体的实现来决定。例如,int代表程序所在机器的自然字长,所以C语言不精确定义标准类型的大小。
在一般情况下,我们自己都精确定义C语言类型,而使用的方法都是类似为:
Typedef int int16
Typedef long int32
等等,这样我们可以直观的知道变量的长度,便于我们的使用,但这种方式需要说明的是:
1在某些机器上可能没有严格的对应关系,比如有36位的机器
2如果int16和int32表示的意思是:“至少这么长”,则没有什么意思,以为int和long类型本身就已经这么定义了。
3typedef对于字节顺序问题不能提供任何帮助。
4当然,也可以直接使用标准头文件<inttype.h>里面定义的类型,因为里面已经定义了标准类型名称int16_t和uint32_t等
关于64位类型的long long,C99规定其长度保证至少64位,编译器当然是能通过这种类型的变量的,其他编译器也有_longlong扩展_.当然也能实现16位的short,32为的int,64位的long int 。
关于指针的声明:
看三种情况:
(1): char* p1, p2;
(2): char *p1, p2;
(3): char *p1, *p2;
如果我们的设想是想声明两个字符型指针,那么第一种是明令禁止的(其实在任何情况都是禁止的,因为看不清这个编程人员究竟在想什么东西,呵呵)。第二种的结果是p1是指针,而p2不是,第三种才是我们需要的写法,当然是规范的写法!
通过第三种写法,我们声明了两个指针,现在想向内存申请空间,正确的写法:
P1 = (char *)malloc(100);
向内存申请了100个字符的空间。注意malloc返回的类型是void *,所以我们需要将其进行显式转换,在编程中我们一直杜绝的是隐式转换,这会把人逼疯的,你懂的…
当然前面的p1不能是*p1,因为malloc返回的是地址。
关于全局变量的定义和声明:
首先要声明:一定要尽可能的减少全局变量,因为1.他的名字会污染整个程序2.增加耦合度,难于维护(更关键)等。更多内容可以参看《effective c++》,《c++编程规范(101条)》第十八条等等。
在使用全局变量时,需要遵循下面的规则:
请在源文件(.c)中定义,在(.h)文件声明。
声明:
extern int i;
extern int f();
定义:
int i = 0;
int f(){
…
return 1;
}
定义变量的源文件也应该包含头文件。
这样做的目的是为了让编译器检查声明的一致性。
另外,永远不要把外部函数的原型放到源文件(.c)文件中,因为函数定义修改时,很容易忘记修改原型!
在c语言中不能实现只让部分源文件中部分函数使用的“半全局变量”。
2. extern c是什么意思?
只是为了能兼容C语言开发的函数
extern "C " 声明后面的内容是引用的是 C 程序版本 !
这里的关键是 C 版本 和 C++ 版本在编译的时候做的处理是不一样的,比如一个函数,C++ 版本的所增加的前缀和 C 版本的是不一样的,如果没有这样的声明,程序会都解释成 C++ 版本的,就会导致程序不能正常编译和连接。
当然这是C++的问题,当然这是一个很基础的问题,只是这个问题在笔试中太常出现了,所以一定要特别说明下!
3. 类型定义:
typedef和#define的区别:
一般说来使用typedef会更好,因为typedef能更好的使用指针,例如:
typedef char * String_t
#define String_d char *
String_t p1, p2;
String_d p3, p4;
结果是s1,s2,s3都是char *,而s4却是char
另外多说一点当我们需要常量时,一般也不需要#define的帮助,而是使用const。例如
const PI = 3.14
而不是 #define PI 3.14
使用typedef定义链表时正确的做法:
(1) typedef struct node{
char *item;
struct node *next;
} *NODEPTR;
(2) struct node;
typedef struct node *NODEPTR;
struct node{
char *item;
NODEPTR next;
};
(3) struct node{
char *item;
struct node *next;
};
typedef struct node *NODEPTR;
三种方式都可以,只是个人风格问题。
解释一个常见现象:
typedef int (*funcptr)();
这句话的意思:
它定义了一个类型funcptr,表示指向返回值为int型(参数未指明)的函数的指针。它可以用来声明一个或多个函数指针:
funcptr fp1,fp2;
等价于:int (*fp1)(), (*fp2)();
4. 命名空间
命名空间的规则比较麻烦,宗旨就是不要和系统默认的名字冲突,不要自己定义的名称重复!
接下来的可以选择查看:
要解释清楚命名空间,就不可避免的解释标示符:
标识符有三个属性:作用域,命名空间,连接类型。
ANSI命名规则:
结论:
上面扯这么多就是想说下面的结论:
5. 变量初始化
具有静态生存期的未初始化变量(包括数组和结构)---即在函数外声明的变量和静态存储类型的变量可以确保初始值为零,他们会执行特定的缺省初始化。
具有自动生存期的变量(即非静态的存储类型的局部变量)如果没有显式的初始化,则包含的是垃圾内容,这是我们极力避免的。
所以,我们一直强调变量定义就进行初始化!
结构、联合和枚举
1. 结构声明
考虑下面两个:
1struct x1{…};
2typedef struct{…} x2;
1声明了一个结构标签,2声明了一个类型定义,主要区别是2更显抽象,用户在使用时不必知道它是一个结构,在声明他的实例时也不需要struct关键字。
x2 a;
而1则需要struct关键字:
struct x1 b;
(当然在C++中,第一种方式是不需要struct关键字的)
2. 结构传递和返回
当结构作为函数参数传递的时候,通常会把整个结构都推入栈,需要多少空间就占用多少空间(正因为此,通常我们使用指针),当然有些好的编译器传递时就传递的是结构的指针,但为了保证是按值传递的含义,它们可能不得不在保留一个局部副本。
编译器通常会提供一个额外的“隐藏”参数,用于指向函数返回的结构。
3. 结构填充
当内存中的值合理对齐时,很多机器都能高效的访问内存。如在按照字节寻址的机器中,2字节的short int必须放在偶地址上,而4字节的long int必须放在4的整数倍数地址上。
上面的这样的情况要求十分苛刻,试想下面的结果:
struct {
char a;
int b;
}
结果显然,在char 和int中间有一个没有命名也没有使用的空洞,来确保int型域保持对齐。如果人工去布局这些空洞是不可能,有的编译器提供了某种扩展用于控制结构的扩充。
当然如果不十分在意那些空洞,我们没必要纠结在这个地方。
4. 结构和联合
联合本质上一个成员相互重叠的结构,某一时刻你只能使用一个成员,也可以从一个成员写入,从另一个成语读出,从而检查某种类型的转换等等,这很明显的和机器有关。联合的大小是其最大成员的大小。而结构是他的所有成员大小之和。
表达式
1. 求值顺序
考虑:
1:a[i] = i++;
这种命令简单一个字:烂!
虽然在某些编译器上依然能够通过,不过是严令禁止的东西。i++会让a[i]中的i的引用无法判断改引用是新值还是旧值,更让阅读这个程序的人感到头疼。
2:int i = 2;
printf(“%d”,i++*i++);
这个命令两个字:“更烂”!
或许以为这个结构无论怎么样也是6,可结果偏偏是4!
C语言的++和—运算符只能保证变量的更新会在表达式“完成”之前的某个时刻进行,而不能保证会在放弃变量原值之后和对表达式的其他部分进行计算之前立即进行。
3:int i = 3;
i = i++;
结果会根据编译器的不同而不同,有的是3,有的是4.
不解释
2. 求值顺序规则
在一条表达式中最好一个变量只修改一次,当然一条命令中一个变量可以修改两次,当你最好能有百分百的把握,确定在不同时刻不同编译器上也有相同的结果。
3. 例子:
int a = 1000;
int b = 1000;
long int c = a*b;
c的结果是1000,000吗?
不是!
因为a*b时,int就溢出!
可以进行显式的类型转换。
正确的做法:
1:long int c = (long int)a*b;
2: long int c = (long int)a*(long int)b;
4. 无符号和有符号
为了避免意外,在同一个表达式中如果同时有无符号和有符号的数值参与运算,请进行显式的转换。
指针
1. 指针的好处:
1动态分配数组
2对多个相似变量的一般访问
3按引用传递函数参数
4各种动态分配的数据结构,比如树和链表
5遍历数组
等等
2. *p++的意义
后缀++和--操作符本质上比前缀一元操作符的优先级更高,因此*p++和*(p++)等价,它字增p并且返回p自增之前所指向的值,要自增p指向的值,则使用(*p)++。
3. 使用指针作为函数的参数的方法:
例子:
void f(int **ptr)
{
int temp = 5;
*ptr = &temp;
}
…
…
int *iptr;
f(&iptr);// 使用iptr作为参数