c深度剖析.docx
《c深度剖析.docx》由会员分享,可在线阅读,更多相关《c深度剖析.docx(19页珍藏版)》请在冰点文库上搜索。
c深度剖析
✧1.指针可以进行强制类型转换;
✧非指针类型必须要先取变量地址然后再类型转换最后取值
✧Sizeof(int)*p
✧Void*
✧在C语言中,可以给无参数的函数传送任意类型的参数,
✧但是在C++编译器中编译同样的代码则会出错。
在C++中,不能向无参数的函数传送任何
✧参数,出错提示“'fun':
functiondoesnottake1parameters”。
✧所以,无论在C还是C++中,若函数不接受任何参数,一定要指明参数为void。
✧按照ANSI(AmericanNationalStandardsInstitute)标准,不能对void指针进行算法操作,
✧即下列操作都是不合法的:
✧void*pvoid;
✧pvoid++;//ANSI:
错误
✧pvoid+=1;//ANSI:
错误
✧ANSI标准之所以这样认定,是因为它坚持:
进行算法操作的指针必须是确定知道其指
✧向数据类型大小的。
也就是说必须知道内存目的地址的确切值。
✧典型的如内存操作函数memcpy和memset的函数原型分别为:
✧void*memcpy(void*dest,constvoid*src,size_tlen);
✧void*memset(void*buffer,intc,size_tnum);
✧这样,任何类型的指针都可以传入memcpy和memset中,这也真实地体现了内存操作
✧函数的意义,因为它操作的对象仅仅是一片内存,而不论这片内存是什么类型。
✧void不能代表一个真实的变量。
✧因为定义变量时必须分配内存空间,定义void类型变量,编译器到底分配多大的内存呢。
✧return语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时
✧被自动销毁。
✧const是constant的缩写,是恒定不变的意思,也翻译为常量、常数等。
很不幸,正是
✧因为这一点,很多人都认为被const修饰的值是常量。
这是不精确的,精确的说应该是只读
✧的变量,其值在编译时不能被使用,因为编译器在编译时不知道其存储的内容。
或许当初
✧这个关键字应该被替换为readonly
✧节省空间,避免不必要的内存分配,同时提高效率
✧编译器通常不为普通const只读变量分配存储空间,而是将它们保存在符号表中,这使
✧得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高。
✧//////////////////////////////////////////////////////////////////////////
✧#include
✧voidmain()
✧{
✧constinta=100;
✧intb[a];
✧b[0]=1;
✧printf("%d",b[0]);
✧
✧}
✧//////////////////////////////////////////////////////////////////////////
✧C语言里面这样定义是错的
✧//////////////////////////////////////////////////////////////////////////
✧#include
✧voidmain()
✧{
✧constinta=0,b=1,c=2;
✧intn;
✧scanf("%d",&n);
✧switch(n)
✧{
✧casea:
printf("good");break;
✧caseb:
printf("well");break;
✧casec:
printf("bad");break;
✧default:
break;
✧}
✧printf("%d",b[0]);
✧
✧}
✧//////////////////////////////////////////////////////////////////////////
✧同样是错的但是c++里面都是对的
✧最易变的关键字----volatile
✧volatile是易变的、不稳定的意思。
很多人根本就没见过这个关键字,不知道它的存在。
✧也有很多程序员知道它的存在,但从来没用过它。
我对它有种“杨家有女初长成,养在深闺
✧人未识”的感觉。
✧volatile关键字和const一样是一种类型修饰符,用它修饰的变量表示可以被某些编译器
✧未知的因素更改,比如操作系统、硬件或者其它线程等。
遇到这个关键字声明的变量,编
✧译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
✧volatile
✧inti=10;
✧intj=i;//(3)语句
✧intk=i;//(4)语句
✧volatile关键字告诉编译器i是随时可能发生变化的,每次使用它的时候必须从内存中取出i
✧的值,因而编译器生成的汇编代码会重新从i的地址处读取数据放在k中。
struct关键字
struct是个神奇的关键字,它将一些相关联的数据打包成一个整体,方便使用。
在网络协议、通信控制、嵌入式系统、驱动开发等地方,我们经常要传送的不是简单
的字节流(char型数组),而是多种数据组合起来的一个整体,其表现形式是一个结构体。
经验不足的开发人员往往将所有需要传送的内容依顺序保存在char型数组中,通过指针偏
移的方法传送网络报文等信息。
这样做编程复杂,易出错,而且一旦控制方式及通信协议
有所变化,程序就要进行非常细致的修改,非常容易出错。
这个时候只需要一个结构体就
能搞定。
平时我们要求函数的参数尽量不多于4个,如果函数的参数多于4个使用起来非
常容易出错(包括每个参数的意义和顺序都容易弄错),效率也会降低(与具体CPU有关,ARM
芯片对于超过4个参数的处理就有讲究,具体请参考相关资料)。
这个时候,可以用结构体
压缩参数个数。
大小端问题:
windows是小段;unix是大端
每个枚举常量的列举是用逗号,非分好
enum_type_name是自定义的一种数据数据类型名,而enum_variable_name为
enum_type_name类型的一个变量,也就是我们平时常说的枚举变量。
enumenum_type_name
{
ENUM_CONST_1,
ENUM_CONST_2,
...
ENUM_CONST_n
}enum_variable_name;
下面再看看枚举与#define宏的区别:
1),#define宏常量是在预编译阶段进行简单替换。
枚举常量则是在编译的时候确定其值。
2),一般在编译器里,可以调试枚举常量,但是不能调试宏常量。
3),枚举可以一次定义大量相关的常量,而#define宏一次只能定义一个。
4),sizof(enum)大小为整形的字节数4;
注释:
不允许嵌套
y=x/*p
y=x/*p,这是表示x除以p指向的内存里的值,把结果赋值为y?
我们可以在编译器
上测试一下,编译器提示出错。
实际上,编译器把/*当作是一段注释的开始,把/*后面的内容都当作注释内容,直到出
现*/为止。
这个表达式其实只是表示把x的值赋给y,/*后面的内容都当作注释。
但是,由
于没有找到*/,所以提示出错。
我们可以把上面的表达式修改一下
汇编语言是以分号为始开始翻译的:
a.对于全局数据(全局变量、常量定义等)必须要加注释。
b.当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。
c.注释的缩进要与代码的缩进一致。
d.注释代码段时应注重“为何做(why)”,而不是“怎么做(how)”。
e.数值的单位一定要注释。
f.对变量的范围给出注释。
g.对于函数的入口出口数据给出注释
左移和右移
左移运算符“<<”是双目运算符。
其功能把“<<”左边的运算数的各二进位全部左移若干
位,由“<<”右边的数指定移动的位数,高位丢弃,低位补0。
右移运算符“>>”是双目运算符。
其功能是把“>>”左边的运算数的各二进位全部右移若
干位,“>>”右边的数指定移动的位数。
但注意:
对于有符号数,在右移时,符号位将随同
移动。
当为正数时,最高位补0;而为负数时,符号位为1,最高位是补0或是补1取决
于编译系统的规定。
TurboC和很多系统规定为补1
0x01<<2+3;
结果为7吗?
测试一下。
结果为32?
别惊讶,32才是正确答案
加法优先级高于移位运算符
逻辑运算符》条件运算符》赋值运算符;
左移和右移的位数不能大于数据
的长度,不能小于
左移和右移的位数不能大于数据
的长度,不能小于0。
求余取模
最重要的一点,我们希望q*b+r==a,因为这是定义余数的关系。
2,如果我们改变a的正负号,我们希望q的符号也随之改变,但q的绝对值不会变。
3,当b>0时,我们希望保证r>=0且r
大多数编程语言选择了放弃第三条,而改为要求余数与
被除数的正负号相同。
这样性质1和性质2就可以得到满足。
大多数C语言编译器也都是
逗号运算符:
只能连接连个表达式,语句不可以!
3.1.3,用define宏定义注释符号?
不能定义注释符号
上面对define的使用都很简单,再看看下面的例子:
#defineBSC//
#defineBMC/*
#defineEMC*/
D),BSCmysingle-linecomment
E),BMCmymulti-linecommentEMC
D)和E)都错误,为什么呢?
因为注释先于预处理指令被处理,当这两行被展开成//…或
/*…*/时,注释已处理完毕,此时再出现//…或/*…*/自然错误.因此,试图用宏开始或结束一段
注释是不行的。
#undef
#undef是用来撤销宏定义的,用法如下:
#define
PI
3.141592654
…
//code
#undef
PI
3.4,#error预处理
#error预处理指令的作用是,编译程序时,只要遇到#error就会生成一个编译错误提
示消息,并停止编译。
其语法格式为:
#errorerror-message
注意,宏串error-message不用双引号包围。
遇到#error指令时,错误信息被显示,可能同时
还显示编译程序作者预先定义的其他内容。
关于系统所支持的error-message信息,请查找
相关资料,这里不浪费篇幅来做讨论。
3.5,#line预处理
#line的作用是改变当前行数和文件名称,它们是在编译程序中预先定义的标识符
命令的基本形式如下:
#linenumber["filename"]
其中[]内的文件名可以省略。
例如:
#line30a.h
其中,文件名a.h可以省略不写。
这条指令可以改变当前的行号和文件名,例如上面的这条预处理指令就可以改变当前的行号
为30,文件名是a.h。
初看起来似乎没有什么用,不过,他还是有点用的,那就是用在编译
器的编写中,我们知道编译器对C源码编译过程中会产生一些中间文件,通过这条指令,
可以保证文件名是固定的,不会被这些中间文件代替,有利于进行分析。
3.6,#pragma预处理
在所有的预处理指令中,#pragma指令可能是最复杂的了,它的作用是设定编译器的
状态或者是指示编译器完成一些特定的动作。
#pragma指令对每个编译器给出了一个方法,
在保持与C和C++语言完全兼容的情况下,给出主机或操作系统专有的特征。
依据定义,编译
指示是机器或操作系统专有的,且对于每个编译器都是不同的。
其格式一般为:
#pragmapara
其中para为参数,下面来看一些常用的参数。
3.6.1,#pragmamessage
message参数:
Message参数是我最喜欢的一个参数,它能够在编译信息输出窗
口中输出相应的信息,这对于源代码信息的控制是非常重要的。
其使用方法为:
#pragmamessage(“消息文本”)
当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来。
当我们在程序中定义了许多宏来控制源代码版本的时候,我们自己有可能都会忘记有没有
正确的设置这些宏,此时我们可以用这条指令在编译的时候就进行检查。
假设我们希望判
断自己有没有在源代码的什么地方定义了_X86这个宏可以用下面的方法
#ifdef_X86
#Pragmamessage(“_X86macroactivated!
”)
#endif
当我们定义了_X86这个宏以后,应用程序在编译时就会在编译输出窗口里显示“_
X86macroactivated!
”。
我们就不会因为不记得自己定义的一些特定的宏而抓耳挠腮了
。
3.6.2,#pragmacode_seg
另一个使用得比较多的pragma参数是code_seg。
格式如:
#pragmacode_seg(["section-name"[,"section-class"]])
它能够设置程序中函数代码存放的代码段,当我们开发驱动程序的时候就会使用到它。
3.6.3,#pragmaonce
#pragmaonce(比较常用)
只要在头文件的最开始加入这条指令就能够保证头文件被编译一次,这条指令实际上在
VisualC++6.0中就已经有了,但是考虑到兼容性并没有太多的使用它。
3.6.4,#pragmahdrstop
#pragmahdrstop表示预编译头文件到此为止,后面的头文件不进行预编译。
BCB可以
预编译头文件以加快链接的速度,但如果所有头文件都进行预编译又可能占太多磁盘空间,
所以使用这个选项排除一些头文件。
有时单元之间有依赖关系,比如单元A依赖单元B,所以单元B要先于单元A编译。
你可以用#pragmastartup指定编译优先级,如果使用了#pragmapackage(smart_init),BCB
就会根据优先级的大小先后编译。
3.6.5,#pragmaresource
#pragmaresource"*.dfm"表示把*.dfm文件中的资源加入工程。
*.dfm中包括窗体
外观的定义。
3.6.6,#pragmawarning
#pragmawarning(disable:
450734;once:
4385;error:
164)
等价于:
#pragmawarning(disable:
450734)//不显示4507和34号警告信息
#pragmawarning(once:
4385)
//4385号警告信息仅报告一次
#pragmawarning(error:
164)
//把164号警告信息作为一个错误。
同时这个pragmawarning也支持如下格式:
#pragmawarning(push[,n])
#pragmawarning(pop)
这里n代表一个警告等级(1---4)。
#pragmawarning(push)保存所有警告信息的现有的警告状态。
#pragmawarning(push,n)保存所有警告信息的现有的警告状态,并且把全局警告
等级设定为n。
#pragmawarning(pop)向栈中弹出最后一个警告信息,在入栈和出栈之间所作的
一切改动取消。
例如:
#pragmawarning(push)
#pragmawarning(disable:
4705)
#pragmawarning(disable:
4706)
#pragmawarning(disable:
4707)
//.......
#pragmawarning(pop)
在这段代码的最后,重新保存所有的警告信息(包括4705,4706和4707)。
3.6.7,#pragmacomment
#pragmacomment(...)
该指令将一个注释记录放入一个对象文件或可执行文件中。
常用的lib关键字,可以帮我们连入一个库文件。
比如:
#pragmacomment(lib,"user32.lib")
该指令用来将user32.lib库文件加入到本工程中。
linker:
将一个链接选项放入目标文件中,你可以使用这个指令来代替由命令行传入的或
者在开发环境中设置的链接选项,你可以指定/include选项来强制包含某个对象,例如:
#pragmacomment(linker,"/include:
__mySymbol")
3.6.8,#pragmapack
这里重点讨论内存对齐的问题和#pragmapack()的使用方法。
什么是内存对齐?
先看下面的结构:
structTestStruct1
{
charc1;
shorts;
charc2;
inti;
};
假设这个结构的成员在内存中是紧凑排列的,假设c1的地址是0,那么s的地址就应该
是1,c2的地址就是3,i的地址就是4。
也就是c1地址为00000000,s地址为00000001,c2
地址为00000003,i地址为00000004。
可是,我们在VisualC++6.0中写一个简单的程序:
structTestStruct1a;
printf("c1%p,s%p,c2%p,i%p\n",
(unsignedint)(void*)&a.c1-(unsignedint)(void*)&a,
(unsignedint)(void*)&a.s-(unsignedint)(void*)&a,
(unsignedint)(void*)&a.c2-(unsignedint)(void*)&a,
(unsignedint)(void*)&a.i-(unsignedint)(void*)&a);
运行,输出:
c100000000,s00000002,c200000004,i00000008。
为什么会这样?
这就是内存对齐而导致的问题。
#运算符
#也是预处理?
是的,你可以这么认为。
那怎么用它呢?
别急,先看下面例子:
#defineSQR(x)
printf("Thesquareof
x
is%d.\n",((x)*(x)));
如果这样使用宏:
SQR(8);
则输出为:
Thesquareof
x
is64.
注意到没有,引号中的字符x被当作普通文本来处理,而不是被当作一个可以被替换的语言
符号。
假如你确实希望在字符串中包含宏参数,那我们就可以使用“#”,它可以把语言符号转
化为字符串。
上面的例子改一改:
#defineSQR(x)
printf("Thesquareof
"#x"
is%d.\n",((x)*(x)));
//重点
Intmain()
{
inta[4]={1,2,3,4};
int*ptr1=(int*)(&a+1);
int*ptr2=(int*)((int)a+1);
printf("%x,%x",ptr1[-1],*ptr2);
return0;
}
无法把指针变量本身传递给一个函数
这很像孙悟空拔下一根猴毛变成自己的样子去忽悠小妖怪。
所以fun函数实际运行时,
用到的都是_p2这个变量而非p2本身。
如此,我们看下面的例子:
voidGetMemory(char*p,intnum)
{
p=(char*)malloc(num*sizeof(char));
}
intmain()
{
char*str=
NULL;
GetMemory(str,10);
strcpy(str,”hello”);
free(str);//free并没有起作用,内存泄漏
return0;
}
*(int*)&p----这是什么?
也许上面的例子过于简单,我们看看下面的例子:
voidFunction()
{
printf("Call
Function!
\n");
}
intmain()
{
void
(*p)();
*(int*)&p=(int)Function;
(*p)();
return0;
}
静态区:
保存自动全局变量和static变量(包括static全局和局部变量)。
静态区的内容
在总个程序的生命周期内都存在,由编译器在编译的时候分配。
栈:
保存局部变量。
栈上的内容只在函数的范围内存在,当函数运行结束,这些内容
也会自动被销毁。
其特点是效率高,但空间大小有限。
堆:
由malloc系列函数或new操作符分配的内存。
其生命周期由free或delete决定。
在没有释放之前一直存在,直到程序结束。
其特点是使用灵活,空间比较大,但容易出错
函数名与返回值类型在语义上不可冲突。
违反这条规则的典型代表就是C语言标准库函数getchar。
几乎没有一部名著没有提到
getchar函数,因为它实在太经典,太容易让人犯错误了。
所以,每一个有经验的作者都
会拿这个例子来警示他的读者,我这里也是如此:
charc;
c=getchar();
if(EOF==c)
{
…
}
按照getchar名字的意思,应该将变量c定义为char类型。
但是很不幸,getchar函数的
返回值却是int类型,其原型为:
intgetchar(void);
由于c是char类型的,取值范围是[-128,127],如果宏EOF的值在char的取值范围之外,
EOF的值将无法全部保存到c内,会发生截断,将EOF值的低8位保存到c里。
这样if语
句有可能总是失败。
这种潜在的危险,如果不是犯过一次错,肯怕很难发现。