转:C语言深度解剖一
分类: C/C++
一.关键字
1.关于sizeof:
sizeof不是函数,而是关键字。sizeof在计算变量所占空间大小时,括号可以省略,而计算类型(模子)大小时不能省略。
2.定义和声明的区别:
定义和声明之间的区别,定义分配的内存,而声明没有。定义只能出现一次,而声明可以出现多次。
3.关于寄存器:
寄存器其实就是一块一块小的存储空间,只不过其存取速度要比内存快得多。
4.关于register:
register变量必须是一个单个的值,并且其长度应小于或等于整型的长度。而且register变量可能不存放在内存中, 所以不能用取址运算符 “&”来获取 register变量的地址。
5.关于static
(1)修饰变量:
静态全局变量,作用域仅限于变量被定义的文件中,其他文件即使用 extern声明也没法使用他。在定义之处前面的那些代码行也不能使用它。想要使用就得在前面再加 extern
静态局部变量:由于被 static修饰的变量总是存在内存的静态区,所以即使这个函数运行结束,这个静态变量的值还是不会被销毁,函数下次使用时仍然能用到这个值。
(2)修饰函数:此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件(所以又称内部函数)。
6.关于case语句:
case后面只能是整型或字符型的常量或常量表达式,但不能是实型。
7.关于循环语句:
应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环层的次数。
8.关于空类型和有类型:
“空类型”可以包容“有类型”,而“有类型”则不能包容“空类型”。如果函数无参数,那么应声明其参数为void。如:int function(void)。
9.关于void指针:
按照 ANSI(American National Standards Institute)标准,不能对void指针进行算法操作(如p++)。而GNU(GNU's Not Unix的递归缩写)则不这么认定,它指定void *的算法操作与char *一致。
注意:memcpy和memset函数返回的也是void *类型。return语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时被自动销毁。
10.关于const:
精确的说应该是只读的变量。编译器通常不为普通 const只读变量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高。
const定义的只读变量在程序运行过程中只有一份拷贝(因为它是全局的只读变量,存放在静态区),而#define定义的宏常量在内存中有若干个拷贝。
#define宏是在预编译阶段进行替换,而const修饰的只读变量是在编译的时候确定其值。
注意:const修饰的只读变量必须在定义的同时初始化。
const修饰指针:
const int *p; // p 可变,p指向的对象不可变
int const *p; // p可变,p指向的对象不可变
int *const p; //p不可变,p指向的对象可变
const int *const p; //指针p和p指向的对象都不可变
记忆和理解的方法:先忽略类型名(编译器解析的时候也是忽略类型名),看const离谁近。离谁近就修饰谁。如:
const int *p; //const修饰*p,p是指针,*p是指针指向的对象,不可变
int const *p;//const修饰*p,p是指针,*p是指针指向的对象,不可变
int *const p;//const修饰 p,p不可变,p 指向的对象可变
const int *const p; //前一个 const修饰*p,后一个const修饰p,指针p和p指向的对象都不可变
const修饰符也可以修饰函数的参数和返回值,当不希望这个值被函数体内意外改变时使用。
在另一连接文件中引用const只读变量:extern const int i;
const修饰的只读变量不能用来作为定义数组的维数,也不能放在case关键字后面。
11.关于define:
很多人误以为 define是关键字,其实它不是关键字。
12.关于volatile
例:int i=10;
int j = i;//(1)语句
int k = i;//(2)语句
在(1)语句时从内存中取出i的值赋给j之后,这个值并没有被丢掉,而是在(2)语句时继续用这个值给k赋值。
volatile int i=10;
int j = i;//(3)语句
int k = i;//(4)语句
volatile关键字告诉编译器i是随时可能发生变化的,每次使用它的时候必须从内存中取出i的值,因而编译器生成的汇编代码会重新从i的地址处读取数据放在k中。
所以说volatile可以保证对特殊地址的稳定访问。
13.关于struct:
(1)struct的大小:
struct student{
...
}stu;
编译器为每个结构体类型数据至少预留1个byte的空间。所以sizeof(stu)应该为1.
(2)柔性数组(flexible array):
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结构中的柔性数组成员前面必须至少一个其他成员。 柔性数组成员允许结构中包含一个大小可变的数组。sizeof返回的这种结构大小不包括柔性数组的内存。包含柔性数组成员的结构用malloc()函数进 行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。如:type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));但是这时候我们再用 sizeof(*p)测试结构体的大小,发现仍然为 4。因为模子的大小就已经确定不包含柔性数组的内存大小。只是说在使用柔性数组时需要把它当作结构体的一个成员,再说白点,柔性数组其实与结构体没什么关 系,只是“挂羊头卖狗肉”而已,算不得结构体的正式成员。需要说明的是:C89不支持这种东西,C99把它作为一种特例加入了标准。
14.关于struct与class:
在 C++里 struct关键字与class关键字一般可以通用,只有一个很小的区别。struct的成员默认情况下属性是public的,而class成员却是private的。
15.关于union:
union维护足够的空间来置放多个数据成员中的“一种”,而不是为每一个数据成员配置空间,在union中所有的数据成员共用一个空间,同一时间只能储存其中一个数据成员,所有的数据成员具有相同的起始地址。
在 C++里,union的成员默认属性页为public。union主要用来压缩空间。如果一些数据不可能在同一时间同时被用到,则可以使用 union。
union型数据所占的空间等于其最大的成员所占的空间。也就是联合体的访问不论对哪个变量的存取都是从union的首地址位置开始。
16.关于enum:
例子:enum enum_type_name
{
ENUM_CONST_1,
ENUM_CONST_2,
...
ENUM_CONST_n
}enum_variable_name;
enum_type_name 类型的变量enum_variable_name只能取值为花括号内的任何一个值,如果赋给该类型变量的值不在列表中,则会报错或者警告。这些成员都是常 量, 也就是我们平时所说的枚举常量 (常量一般用大写)。如果不赋值则会从被赋初值的那个常量开始依次加1,如果都没有赋值,它们的值从0开始依次递增1。
枚举与#define宏的区别:
#define宏常量是在预编译阶段进行简单替换。枚举常量则是在编译的时候确定其值。
枚举可以一次定义大量相关的常量,而#define宏一次只能定义一个。
17.关于typedef:
typedef的真正意思是给一个已经存在的数据类型(注意:是类型不是变量)取一个别名,而非定义一个新的数据类型。
易错点:
#define PCHAR char*
PCHAR p3,p4;
这里的p4却不是指针,仅仅是一个char类型的字符。这种错误很容易被忽略。
二.符号
1.关于编译器:
编译器的确会将注释剔除,但不是简单的剔除,而是用空格代替原来的注释。
2.关于注释:
(1)对于全局数据(全局变量、常量定义等)必须要加注释。
(2)注释的位置应与被描述的代码相邻,可以与语句在同一行,也可以在上行,但不可放在下方。
(3)注释的缩进要与代码的缩进一致。
(4)注释代码段时应注重“为何做(why)” ,而不是“怎么做(how)” 。
(5)对于函数的入口出口数据给出注释。
3.关于接续符:
C语言里以反斜杠(\)表示断行。编译器会将反斜杠剔除掉,跟在反斜杠后面的字符自动接续到前一行。但是注意:反斜杠之后不能有空格,反斜杠的下一行之前也不能有空格。即反斜杠作为接续符时,在本行其后面不能再有任何字符,空格都不行。
4.关于左右移:
左移和右移的位数不能大于数据的长度,否则溢出。
5.关于a+++b:
C 语言有这样一个规则:每一个符号应该包含尽可能多的字符。也就是说,编译器将程序分解成符号的方法是,从左到右一个一个字符地读入,如果该字符可能组成一 个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读 入的字符组成的字符串已不再可能组成一个有意义的符号。
6.关于优先级:
(1).的优先级高于*,->操作符用于消除这个问题
如:*p.f:对 p 取 f 偏移,作为指针,然后进行解除引用操作。*(p.f)
(2)[]高于*
如:int *ap[]:ap 是个元素为 int指针的数组int *(ap[])
(3)函数()高于*
如:int *fp():fp是个函数,返回int *,int *(fp())
(4)== 和!=高于位操作
(5)== 和!=高于赋值符
(6)算术运算符高于位移运算符
(7)逗号运算符在所有运算符中优先级最低
三.预处理
1.关于注释与预处理:
注释先于预处理指令被处理,因此,试图用宏开始或结束一段注释是不行的。
2.关于预处理的括号:
例:#define SQR (x) x * x
假设 x 的值是个表达式10+1,SQR (x)被替换后变成10+1*10+1,就不对了。要搞定它其实很简单,别吝啬括号就行了。
3.关于预处理的空格:
例:#define SUM (x)(x)+(x)
空格仅仅在定义的时候有效,在使用这个宏函数的时候,空格会被编译器忽略掉。也就是说,上一节定义好的宏函数 SUM(x)在使用的时候在 SUM 和(x)之间留有空格是没问题的。
4.关于#define和#undef:
例:#define PI 3.141592654
。。。。。
#undef PI
也就是说宏的生命周期从#define开始到#undef结束。
5.关于#include:
include支持相对路径,.代表当前目录,..代表上层目录。
6.关于#error:
#error预处理指令的作用是,编译程序时,只要遇到 #error 就会生成一个编译错误提示消息,并停止编译。其语法格式为:
#error error-message
注意,宏串 error-message不用双引号包围。
7.关于#line:
#line的作用是改变当前行数和文件名称,它们是在编译程序中预先定义的标识符命令的基本形式如下:
#line number["filename"]
其中[]内的文件名可以省略。
8.关于#pragma :
(1)#pragma message:
message 参数:它能够在编译信息输出窗口中输出相应的信息,这对于源代码信息的控制是非常重要的。其使用方法为:
#pragma message(“消息文本”)
当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来。
(2)#pragma code_seg:
#pragma code_seg( ["section-name"[,"section-class"] ] )
它能够设置程序中函数代码存放的代码段,当我们开发驱动程序的时候就会使用到它。
(3)#pragma once
#pragma once (比较常用)
只要在头文件的最开始加入这条指令就能够保证头文件被编译一次
(4)#pragma hdrstop
#pragma hdrstop表示预编译头文件到此为止,后面的头文件不进行预编译。有时单元之间有依赖关系,用#pragma startup指定编译优先级,如果使用了#pragma package(smart_init) ,就会根据优先级的大小先后编译。
(5)#pragma resource
#pragma resource "*.dfm"表示把*.dfm文件中的资源加入工程。*.dfm中包括窗体外观的定义。
(6)#pragma warning
(7)#pragma comment
#pragma comment(...)
该指令将一个注释记录放入一个对象文件或可执行文件中。
#pragma comment(lib, "user32.lib")
该指令用来将 user32.lib库文件加入到本工程中。
9.关于内存对齐:
一个字或双字操作数跨越了 4 字节边界,或者一个四字操作数跨越了8 字节边界,被认为是未对齐的。一个字起始地址是奇数但却没有跨越字边界被认为是对齐的。
用#pragma pack()来改变编译器的默认对齐方式:
使用指令#pragma pack (n),编译器将按照 n个字节对齐。
使用指令#pragma pack (),编译器将取消自定义字节对齐方式。
在#pragma pack (n)和#pragma pack ()之间的代码按 n个字节对齐。
对齐的规则是,每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是 n字节)中较小的一个对齐,并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节。
对于结构来说,它的默认对齐方式就是它的所有成员使用的对齐参数中最大的一个。
有三点很重要:
首先,每个成员分别按自己的方式对齐,并能最小化长度。
其次, 复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度。
然后,对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐。
但是不论类型是什么,对齐的边界一定是 1,2,4,8,16,32,64....中的一个。
10.关于#运算符:
例:#define SQR(x) printf("The square of x is %d.\n", ((x)*(x)));
如果这样使用宏:
SQR(8);
则输出为:
The square of x is 64.
假如你确实希望在字符串中包含宏参数,那我们就可以使用“#” ,它可以把语言符号转化为字符串。上面的例子改一改:
#define SQR(x) printf("The square of "#x" is %d.\n", ((x)*(x)));
再使用:
SQR(8);
则输出的是:
The square of 8 is 64
11.关于##预算符:
##运算符可以用于宏函数的替换部分。这个运算符把两个语言符号组合成单个语言符号。看例子:
#define XNAME(n) x ## n
如果这样使用宏:
XNAME(8)
则会被展开成这样:
x8
##就是个粘合剂,将前后两部分粘合起来。