NO。1

1.

break 跳出当前循环 ,continue 结束当前循环,开始下一轮循环

break 关键字很重要,表示终止本层循环。现在这个例子只有一层循环,当代码执行到
break 时,循环便终止。
如果把break 换成continue 会是什么样子呢?continue 表示终止本次(本轮)循环。当
代码执行到continue 时,本轮循环终止,进入下一轮循环。
while(1)也有写成while(true) 或者while(1==1) 或者while((bool) 1)等形式的,效果一
样。
do-while 循环:先执行do 后面的代码,然后再判断while 后面括号里的值,如果为真,
循环开始;否则,循环不开始。其用法与while 循环没有区别,但相对较少用。


for 循环:for 循环可以很容易的控制循环次数,多用于事先知道循环次数的情况下。
留一个问题:在switch case 语句中能否使用continue 关键字?为什么?

不能continue 只能用在循环结构里 除非switch case 里有循环





2.

定义:所谓的定义就是(编译器)创建一个对象,为这个对象分配一块内存并给它取上一个名字,这个名字就是我们经常所说的变量名或对象名

什么是声明:有两重含义,如下:
第一重含义:告诉编译器,这个名字已经匹配到一块内存上了,比如extern

第二重含义:告诉编译器,我这个名字我先预定了,别的地方再也不能用它来作为变量名或对象名

定义声明最重要的区别:定义创建了对象并为这个对象分配了内存,声明没有分配内存

举个例子:
A)int i;(定义)
B)extern int i;(声明)(关于extern,后面解释)





3.

static面向过程的用法:

第一个作用:修饰变量。变量又分为局部和全局变量,但它们都存在内存的静态区。
静态全局变量,作用域仅限于变量被定义的文件中,其他文件即使用extern 声明也没法使用他
由于被static 修饰的变量总是存在内存的静态区,所以即使这个函数运行结束,这个静态变量的值还是不会被销毁,函数下次使用时仍然能用到这个值


第二个作用:修饰函数。函数前加static 使得函数成为静态函数。但此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件(所以又称内部函数)

还有static面向对象的用法?



4.

在32 位的系统上

short 咔出来的内存大小是2 个byte;

int 咔出来的内存大小是4 个byte;

long 咔出来的内存大小是4 个byte;

float 咔出来的内存大小是4 个byte;
double 咔出来的内存大小是8 个byte;

char 咔出来的内存大小是1 个byte。

(注意这里指一般情况,可能不同的平台还会有所不同,具体平台可以用sizeof 关键字测试一下)




5.

sizeof 是关键字不是函数,其实就算不知道它是否为32 个关键字之一时,我们也可以
借助编译器确定它的身份。看下面的例子:
int i=0;
A),sizeof(int); B),sizeof(i); C),sizeof int; D),sizeof i;
毫无疑问,32 位系统下A),B)的值为4。那C)的呢?D)的呢?
在32 位系统下,通过Visual C++6.0 或任意一编译器调试,我们发现D)的结果也为4。
咦?sizeof 后面的括号呢?没有括号居然也行,那想想,函数名后面没有括号行吗?由此轻
易得出sizeof 绝非函数。
好,再看C)。编译器怎么怎么提示出错呢?不是说sizeof 是个关键字,其后面的括号
可以没有么?那你想想sizeof int 表示什么啊?int 前面加一个关键字?类型扩展?明显不
正确,我们可以在int 前加unsigned,const 等关键字但不能加sizeof。好,记住:sizeof 在
计算变量所占空间大小时,括号可以省略,而计算类型(模子)大小时不能省略。一般情况下,
咱也别偷这个懒,乖乖的写上括号,继续装作一个“函数”,做一个“披着函数皮的关键字”。
做我的关键字,让人家认为是函数去吧。

int *p = NULL;
sizeof(p)的值是多少?=4
sizeof(*p)呢?=4

char* p = NULL;

sizeof(p) = 4;

sizeof(*p) = 1;


int a[100];
sizeof (a) 的值是多少?=400
sizeof(a[100])呢?//请尤其注意本例。=4
sizeof(&a)呢?=4
sizeof(&a[0])呢?=4


int b[100];
void fun(int b[100])//数组作为参数传递传递的是指针,sizeof是指针长度当把一个数组定义为函数参数时,可以选择把它定义为数组,也可以定义为指针,不管用哪种方法在函数内部获得的都是指针
{

sizeof(b);// sizeof (b) 的值是多少?=4

}


void fun(int (&b)[100])//c++传递按引用传递的话那么sizeof就是数组长度

{

sizeof(b);// sizeof (b) 的值是多少?=400

}



6.

把基本数据类型的最高位腾出来,用来存符号,同时约定如下:最高位如果是1,表明这个数是负数,其值为除最高位以外的剩余位的值添上这个“-”号;如果最高位是0,表明这个数是正数,其值为除最高位以外的剩余位的值。
这样的话,一个32位的signed int类型整数其值表示法范围为:- 231~231 -1;8 位的
char类型数其值表示的范围为- 27~27 -1。一个32位的unsigned int类型整数其值表示法
范围为:0~ 232 -1;8位的char类型数其值表示的范围为0~28 -1。同样我们的signed 关
键字也很宽恒大量,你也可以完全当它不存在,编译器缺省默认情况下数据为signed 类型
的。

上面的解释很容易理解,下面就考虑一下这个问题:
int main()
{

char a[1000];

int i;

for(i=0; i<1000; i++)

{

a[i] = -1-i;

}

printf("%d",strlen(a));

return 0;

}
此题看上去真的很简单,但是却鲜有人答对。答案是255。别惊讶,我们先分析分析。
for 循环内,当i 的值为0 时,a[0]的值为-1。关键就是-1 在内存里面如何存储。
我们知道在计算机系统中,数值一律用补码来表示(存储)。主要原因是使用补码,可
以将符号位和其它位统一处理;同时,减法也可按加法来处理。另外,两个用补码表示的数
相加时,如果最高位(符号位)有进位,则进位被舍弃。正数的补码与其原码一致;负数的
补码:符号位为1,其余位为该数绝对值的原码按位取反,然后整个数加1。

按照负数补码的规则,可以知道-1 的补码为0xff,-2 的补码为0xfe……当i 的值为127
时,a[127]的值为-128,而-128 是char 类型数据能表示的最小的负数。当i 继续增加,a[128]
的值肯定不能是-129。因为这时候发生了溢出,-129 需要9 位才能存储下来,而char 类型
数据只有8 位,所以最高位被丢弃。剩下的8 位是原来9 位补码的低8 位的值,即0x7f。
当i 继续增加到255 的时候,-256 的补码的低8 位为0。然后当i 增加到256 时,-257 的补
码的低8 位全为1,即低八位的补码为0xff,如此又开始一轮新的循环……
按照上面的分析,a[0]到a[254]里面的值都不为0,而a[255]的值为0。strlen 函数是计
算字符串长度的,并不包含字符串最后的‘\0’。而判断一个字符串是否结束的标志就是看
是否遇到‘\0’。如果遇到‘\0’,则认为本字符串结束。
分析到这里,strlen(a)的值为255 应该完全能理解了。这个问题的关键就是要明白char
类型默认情况下是有符号的,其表示的值的范围为[-128,127],超出这个范围的值会产生溢
出。另外还要清楚的就是负数的补码怎么表示。弄明白了这两点,这个问题其实就很简单了


留三个问题:
1),按照我们上面的解释,那-0 和+0 在内存里面分别怎么存储?
2),int i = -20;
unsigned j = 10;
i+j 的值为多少?为什么?

1、int和unsigned int运算时int会自动转成unsigned int 2、int和unsigned int都是4字节(32位情况下)存储,区别是int最高位是符号位,用来表示正负 3、负数用补码存储,-20存储为11111111111111111111111111101100,这个东西转为unsigned int后就是一个很大的数4294967276了,所以最后结果是4294967286 
所以-20转换为一个无符号数,会得到一个很大的数,因此若两数相加也会得到一个很大的数。例如:你可以试下试,那j=10 更改为j=30就会发生溢出问题。 


3) 下面的代码有什么问题?
unsigned int i ;
for (i=9;i>=0;i--)
{

printf("%u\n",i);

}

因为你定义的i是无符号整型(unsigned int),当i为0时,再减1,就变成了65535,永远不会小于0,所以循环的条件永远成立,是个死循环,改正方法是将unsigned i;改为int i; 还有就是如果你想只输出9到1,而不输出0的话,还可以将for(i=9;i>=0;i--)改为for(i=9;i>0;i--) 


7.

bool 变量与“零值”进行比较,采用

if(bTestFlag); if(!bTestFlag);

大家都知道if 语句是靠其后面的括号里的表达式的值来进行分支跳转的。表达式如果
为真,则执行if 语句后面紧跟的代码;否则不执行。那显然,本组的写法很好,既不会引
起误会,也不会由于TRUE 或FLASE 的不同定义值而出错。记住:以后写代码就得这样写。


float 变量与“零值”进行比较

if((fTestVal >= -EPSINON) && (fTestVal <= EPSINON)); //EPSINON 为定义好的精度

EPSINON 为定义好的精度,如果一个数落在[0.0-EPSINON,0.0+EPSINON] 这个闭区间
内,我们认为在某个精度内它的值与零值相等;否则不相等。扩展一下,把0.0 替换为你想
比较的任何一个浮点数,那我们就可以比较任意两个浮点数的大小了,当然是在某个精度
内。


指针变量与“零值”进行比较
int* p = NULL;//定义指针一定要同时初始化,指针与数组那章会详细讲解。

if(NULL == p); if(NULL != p);

这个写法才是正确的,但样子比较古怪。为什么要这么写呢?是怕漏写一个
“=”号:if(p = NULL),这个表达式编译器当然会认为是正确的,但却不是你要表达的意思。
所以,非常推荐这种写法。



8.

case 关键字后面的值有什么要求吗?
好,再问问:真的就这么简单吗?看看下面的问题:
Value1 的值为0.1 行吗?-0.1 呢?-1 呢?0.1+0.9 呢? 1+2 呢?3/2 呢?‘A’呢?“A”
呢?变量i(假设i 已经被初始化)呢?NULL 呢?等等。这些情形希望你亲自上机调试一
下,看看到底哪些行,哪些不行。
记住:case 后面只能是整型或字符型的常量或常量表达式



9.

void 的字面意思是“空类型”,void *则为“空类型指针”,void *可以指向任何类型的数据。

void 真正发挥的作用在于:
(1) 对函数返回的限定;
(2) 对函数参数的限定。

void *则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换:
void *p1;
int *p2;
p1 = p2;

如果函数没有返回值,那么应声明为void 类型
在C 语言中,凡不加返回值类型限定的函数,就会被编译器作为返回整型值处理。但
是许多程序员却误以为其为void 类型

因此,为了避免混乱,我们在编写C 程序时,对于任何函数都必须一个不漏地指定其
类型。如果函数没有返回值,一定要声明为void 类型。这既是程序良好可读性的需要,也
是编程规范性的要求


如果函数的参数可以是任意类型指针,那么应声明其参数为void *。

典型的如内存操作函数memcpy 和memset 的函数原型分别为:
void * memcpy(void *dest, const void *src, size_t len);
void * memset ( void * buffer, int c, size_t num );
这样,任何类型的指针都可以传入memcpy 和memset 中,这也真实地体现了内存操作
函数的意义,因为它操作的对象仅仅是一片内存,而不论这片内存是什么类型。如果memcpy
和memset 的参数类型不是void *,而是char *,那才叫真的奇怪了!这样的memcpy 和memset
明显不是一个“纯粹的,脱离低级趣味的”函数!



10.

char * Func(void)
{

char str[30];

return str;

}
str 属于局部变量,位于栈内存中,在Func 结束的时候被释放,所以返回str 将导致错误。
【规则1-38】return 语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时
被自动销毁。



11.

很多人都认为被const 修饰的值是常量。这是不精确的,精确的说应该是只读
的变量,其值在编译时不能被使用,因为编译器在编译时不知道其存储的内容


编译器通常不为普通const 只读变量分配存储空间,而是将它们保存在符号表中,这使
得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高。
例如:
#define M 3 //宏常量
const int N=5; //此时并未将N 放入内存中
......
int i=N; //此时为N 分配内存,以后不再分配!
int I=M; //预编译期间进行宏替换,分配内存
int j=N; //没有内存分配
int J=M; //再进行宏替换,又一次分配内存!
const 定义的只读变量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define
一样给出的是立即数,所以,const 定义的只读变量在程序运行过程中只有一份拷贝(因为
它是全局的只读变量,存放在静态区),而#define 定义的宏常量在内存中有若干个拷贝。
#define 宏是在预编译阶段进行替换,而const 修饰的只读变量是在编译的时候确定其值。

#define 宏没有类型,而const 修饰的只读变量具有特定的类型。


int const i=2; 或const int i=2;

int const a[5]={1, 2, 3, 4, 5};或
const int a[5]={1, 2, 3, 4, 5};

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 是指针指向的对象,不可变
intconst *p; //const修饰*p,p 是指针,*p 是指针指向的对象,不可变
int*const p; //const修饰p,p 不可变,p 指向的对象可变
constint*const p; //前一个const 修饰*p,后一个const 修饰p,指针p 和p 指向的对象
都不可变

const 修饰符也可以修饰函数的参数,当不希望这个参数值被函数体内意外改变时使
用。例如:
void Fun(const int i);
告诉编译器i 在函数体中的不能改变,从而防止了使用者的一些无意的或错误的修改。



12

volatile 关键字和const 一样是一种类型修饰符,用它修饰的变量表示可以被某些编译器
未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编
译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

再看另一个例子:
volatile int i=10;
int j = i;//(3)语句
int k = i;//(4)语句
volatile 关键字告诉编译器i 是随时可能发生变化的,每次使用它的时候必须从内存中取出i
的值,因而编译器生成的汇编代码会重新从i 的地址处读取数据放在k 中。
这样看来,如果i 是一个寄存器变量或者表示一个端口数据或者是多个线程的共享数
据,就容易出错,所以说volatile 可以保证对特殊地址的稳定访问。

留一个问题:const volatile int i=10;这行代码有没有问题?如果没有,那i 到底是什么
属性?

没问题,const和volatile这两个类型限定符不矛盾。const表示(运行时)常量语义:被const修饰的对象在所在的作用域无法进行修改操作,编译器对于试图直接修改const对象的表达式会产生编译错误。volatile表示“易变的”,即在运行期对象可能在当前程序上下文的控制流以外被修改(例如多线程中被其它线程修改;对象所在的存储器可能被多个硬件设备随机修改等情况):被volatile修饰的对象,编译器不会对这个对象的操作进行优化。一个对象可以同时被const和volatile修饰,表明这个对象体现常量语义,但同时可能被当前对象所在程序上下文意外的情况修改。 



13

extern 可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中(声明),下面的代码用到的这些变量或函数是外来的,不是本文件定义的,提示编译器遇到此变量和函数时在其他模块中寻找其定义




14

struct student
{
}stu;
sizeof(stu)的值是多少呢

不是0,而是1

假设结构体内只有一个char 型的数据
成员,那其大小为1byte(这里先不考虑内存对齐的情况).也就是说非空结构体类型数据最
少需要占一个字节的空间,而空结构体类型数据总不能比最小的非空结构体类型数据所占
的空间大吧

而最小的数据成员需要1 个byte,编译器为每个结构体类型数据至少预留1 个byte
的空间。所以,空结构体的大小就定位1 个byte。


结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员(了解),但结
构中的柔性数组成员前面必须至少一个其他成员。柔性数组成员允许结构中包含一个大小可
变的数组。。sizeof 返回的这种结构大小不包括柔性数组的内存。包含柔性数组成员的结构用
malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组
的预期大小。

typedef struct st_type
{

int i;

int a[];

}type_a;
这样我们就可以定义一个可变长的结构体, 用sizeof(type_a) 得到的只有4 , 就是
sizeof(i)=sizeof(int)。那个0 个元素的数组没有占用空间,而后我们可以进行变长操作了。通
过如下表达式给结构体分配内存:
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));


在C++里struct 关键字与class 关键字一般可以通用,只有一个很小的区别。struct 的成
员默认情况下属性是public 的,而class 成员却是private 的。很多人觉得不好记,其实很容
易。你平时用结构体时用public 修饰它的成员了吗?既然struct 关键字与class 关键字可以
通用,你也不要认为结构体内不能放函数了



15

union 维护足够的空间来置放多个数据成员中的“一种”,而不是为每一个数据成员配置
空间,在union 中所有的数据成员共用一个空间,同一时间只能储存其中一个数据成员,所
有的数据成员具有相同的起始地址。
例子如下:
union StateMachine
{

char character;

int number;

char *str;

double exp;

};
一个union 只配置一个足够大的空间以来容纳最大长度的数据成员,以上例而言,最大
长度是double 型态,所以StateMachine 的空间大小就是double 数据类型的大小。


下面再看一个例子:
union
{

int i;

char a[2];

}*p, u;
p =&u;
p->a[0] = 0x39;
p->a[1] = 0x38;
p.i 的值应该为多少呢?


这里需要考虑存储模式:大端模式和小端模式。
大端模式(Big_endian):字数据的高字节存储在低地址中,而字数据的低字节则存放
在高地址中。
小端模式(Little_endian):字数据的高字节存储在高地址中,而字数据的低字节则存放
在低地址中。

union 型数据所占的空间等于其最大的成员所占的空间。对union 型的成员的存取都是
相对于该联合体基地址的偏移量为0 处开始,也就是联合体的访问不论对哪个变量的存取都
是从union 的首地址位置开始。如此一解释,上面的问题是否已经有了答案呢?


上述问题似乎还比较简单,那来个有技术含量的:请写一个C 函数,若处理器是
Big_endian 的,则返回0;若是Little_endian 的,则返回1。
先分析一下,按照上面关于大小端模式的定义,假设int 类型变量i 被初始化为1。
以大端模式存储,其内存布局如下图:


以小端模式存储,其内存布局如下图:


变量i 占4 个字节,但只有一个字节的值为1,另外三个字节的值都为0。如果取出低
地址上的值为0,毫无疑问,这是大端模式;如果取出低地址上的值为1,毫无疑问,这是
小端模式。既然如此,我们完全可以利用union 类型数据的特点:所有成员的起始地址一致。
到现在,应该知道怎么写了吧?参考答案如下:
int checkSystem( )
{

union check

{

int i;

char ch;

} c;

c.i = 1;

return (c.ch ==1);

}
现在你可以用这个函数来测试你当前系统的存储模式了



16

typedef 的真正意思是给一个已经存在的数据类型(注意:是类型不是变量)取一个别
名,而非定义一个新的数据类型

在实际项目中,为了方便,可能很多数据类型(尤其是结构体之类的自定义数据类型)
需要我们重新取一个适用实际情况的别名。这时候typedef 就可以帮助我们。例如:
typedef struct student
{
//code
}Stu_st,*Stu_pst;//命名规则请参考本章前面部分
A),struct student stu1;和Stu_st stu1;没有区别。
B),struct student *stu2;和Stu_pst stu2;和Stu_st *stu2;没有区别。

好,下面再把typedef 与const 放在一起看看:
C),const Stu_pst stu3;
D),Stu_pst const stu4;
大多数初学者认为C)里const 修饰的是stu3 指向的对象;D)里const 修饰的是stu4
这个指针。很遗憾,C)里const 修饰的并不是stu3 指向的对象。那const 这时候到底修饰
的是什么呢?我们在讲解const int i 的时候说过const 放在类型名“int”前后都行;而const int
*p 与int * const p 则完全不一样。也就是说,我们看const 修饰谁都时候完全可以将数据类
型名视而不见,当它不存在。反过来再看“const Stu_pst stu3”,Stu_pst 是“struct student
{ } *”的别名, “struct student {} *”是一个整体。对于编译器来说,只认为
Stu_pst 是一个类型名,所以在解析的时候很自然的把“Stu_pst”这个数据类型名忽略掉。
现在知道const 到底修饰的是什么了吧?^_^。???

NO。2

1.

y = x为止。这个表达式其实只是表示把x 的值赋给y,,所以提示出错。
我们可以把上面的表达式修改一下:
y = x/ *p
或者
y = x/(*p)
这样的话,表达式的意思就是x 除以p 指向的内存里的值,把结果赋值为y 了。
也就是说只要斜杠(/)和星号(*)之间没有空格,都会被当作注释的开始。这一点一
定要注意。

出色注释的基本要求

【规则2-1】注释应当准确、易懂,防止有二义性。错误的注释不但无益反而有害。
【规则2-2】边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。
不再有用的注释要及时删除。
【规则2-3】注释是对代码的“提示”,而不是文档。程序中的注释应当简单明了,注释太
多了会让人眼花缭乱。
【规则2-4】一目了然的语句不加注释。
例如:i++;
多余的注释
【规则2-5】对于全局数据(全局变量、常量定义等)必须要加注释。
【规则2-6】注释采用英文,尽量避免在注释中使用缩写,特别是不常用缩写。
因为不一定所有的编译器都能显示中文,别人打开你的代码,你的注释也许是一团乱
码。还有,你的代码不一定是懂中文的人阅读。
【规则2-7】注释的位置应与被描述的代码相邻,可以与语句在同一行,也可以在上行,但
不可放在下方。同一结构中不同域的注释要对齐。
【规则2-8】当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于
阅读。
【规则2-9】注释的缩进要与代码的缩进一致。
【规则2-10】注释代码段时应注重“为何做(why)”,而不是“怎么做(how)”。
说明怎么做的注释一般停留在编程语言的层次,而不是为了说明问题。尽力阐述“怎么做”
的注释一般没有告诉我们操作的意图,而指明“怎么做”的注释通常是冗余的。
【规则2-11】数值的单位一定要注释。
注释应该说明某数值的单位到底是什么意思。比如:关于长度的必须说明单位是毫米,
米,还是千米等;关于时间的必须说明单位是时,分,秒,还是毫秒等。
【规则2-12】对变量的范围给出注释。
【规则2-13】对一系列的数字编号给出注释,尤其在编写底层驱动程序的时候(比如管脚
编号)。
【规则2-13】对于函数的入口出口数据给出注释。
关于函数的注释在函数那章有更详细的讨论。



2.

我们知道双引号引起来的都是字符串常量,单引号引起来的都是字符常量但初学者
还是容易弄错这两点。比如:‘a’和“a”完全不一样,在内存里前者占1 个byte,后者占2
个byte。""内部自己添加了一个'\0',所以长度加1
关于字符串常量在指针与数组那章将有更多的讨论。
这两个列子还好理解,再看看这三个:
1,‘1‘,“1”。
第一个是×××常数,32 位系统下占4 个byte;
第二个是字符常量,占1 个byte;
第三个是字符串常量,占2 个byte。
三者表示的意义完全不一样,所占的内存大小也不一样,初学者往往弄错。
字符在内存里是以ASCAII 码存储的,所以字符常量可以与×××常量或变量进行运算。
如:'A' + 1



3.

C 语言中位运算包括下面几种:注意要与逻辑运算符区别开!!
& 按位与
| 按位或
^ 按位异或
~ 取反
<< 左移
>> 右移

前4 种操作很简单,一般不会出错。但要注意按位运算符|和&与逻辑运算符||和&&完全
是两码事,别混淆了。


左移和右移
下面讨论一下左移和右移:
左移运算符“<<”是双目运算符。其功能把“<< ”左边的运算数的各二进位全部左移若干
位,由“<<”右边的数指定移动的位数,高位丢弃,低位补0。
右移运算符“>>”是双目运算符。其功能是把“>> ”左边的运算数的各二进位全部右移若
干位,“>>”右边的数指定移动的位数。但注意:对于有符号数,在右移时,符号位将随同
移动。当为正数时, 最高位补0;而为负数时,符号位为1,最高位是补0 或是补1 取决
于编译系统的规定。Turbo C 和很多系统规定为补1。


0x01<<2+3 的值为多少?
再看看下面的例子:
0x01<<2+3;
结果为7 吗?测试一下。结果为32?别惊讶,32 才是正确答案。因为“+”号的优先
级比移位运算符的优先级高
(关于运算符的优先级,我并不想在这里做过多的讨论,你几
乎可以在任何一本C 语言书上找到)。好,在32 位系统下,再把这个例子改写一下:
0x01<<2+30;或0x01<<2-3;
这样行吗?不行。一个整型数长度为32 位,左移32 位发生了什么事情?溢出!左移-1
位呢?反过来移?所以,左移和右移的位数是有讲究的。左移和右移的位数不能大于数据
的长度,不能小于0。






4.

++、--操作符
这绝对是一对让人头疼的兄弟。先来点简单的:
int i = 3;
(++i)+(++i)+(++i);
表达式的值为多少?15 吗?16 吗?18 吗?其实对于这种情况,C语言标准并没有作出
规定。有点编译器计算出来为18,因为i 经过3 次自加后变为6,然后3 个6 相加得18;
而有的编译器计算出来为16(比如Visual C++6.0),先计算前两个i 的和,这时候i 自加两
次,2 个i 的和为10,然后再加上第三次自加的i 得16。其实这些没有必要辩论,用到哪个
编译器写句代码测试就行了。但不会计算出15 的结果来的。
++、--作为前缀,我们知道是先自加或自减,然后再做别的运算;但是作为后缀时,到
底什么时候自加、自减?这是很多初学者迷糊的地方。假设i=0,看例子:
A),j =(i++,i++,i++);
B),for(i=0;i<10;i++)
{
//code
}
C),k = (i++)+ (i++)+ (i++);
你可以试着计算他们的结果。
A) 例子为逗号表达式,i 在遇到每个逗号后,认为本计算单位已经结束,i 这时候自加。
关于逗号表达式与“++”或“--”的连用,还有一个比较好的例子:
int x;
int i = 3;
x = (++i, i++, i+10);
问x 的值为多少?i 的值为多少?
按照上面的讲解,可以很清楚的知道,逗号表达式中,i 在遇到每个逗号后,认为本计算
单位已经结束,i 这时候自加。所以,本例子计算完后,i的值为5,x的值为15。
B) 例子i 与10 进行比较之后,认为本计算单位已经结束,i 这时候自加。
C) 例子i 遇到分号才认为本计算单位已经结束,i 这时候自加。
也就是说后缀运算是在本计算单位计算结束之后再自加或自减。C 语言里的计算单位大体分
为以上3 类。
留一个问题:
for(i=0,printf(“First=%d”,i);
i<10,printf(“Second=%d”,i);
i++,printf(“Third=%d”,i))
{
printf(“Fourth=%d”,i);
}
打印出什么结果?


上面的例子很简单,那我们把括号去掉看看:
int i = 3;
++i+++i+++i;
天啦!这到底是什么东西?好,我们先看看这个:a+++b 和下面哪个表达式想当:
A),a++ +b;
B),a+ ++b;

贪心法
C 语言有这样一个规则:每一个符号应该包含尽可能多的字符。也就是说,编译器将程
序分解成符号的方法是,从左到右一个一个字符地读入,如果该字符可能组成一个符号,
那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组
成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串
已不再可能组成一个有意义的符号。这个处理的策略被称为“贪心法”
。需要注意到是,除
了字符串与字符常量,符号的中间不能嵌有空白(空格、制表符、换行符等)。比如:==是
单个符号,而= =是两个等号。
按照这个规则可能很轻松的判断a+++b 表达式与a++ +b 一致。那++i+++i+++i;会被解
析成什么样子呢?希望读者好好研究研究。另外还可以考虑一下这个表达式的意思:
a+++++b;


5.

2/(-2)的值是多少?
除法运算在小学就掌握了的,这里还要讨论什么呢?别急,先计算下面这个例子:
2/(-2)的值为多少?2%(-2)的值呢?
如果与你想象的结果不一致,不要惊讶。我们先看看下面这些规则:
假定我们让a 除以b,商为q,余数为r:
q = a/b;
r = a%b;
这里不妨先假定b 大于0。
我们希望a、b、q、r 之间维持什么样的关系呢?
1,最重要的一点,我们希望q*b + r == a,因为这是定义余数的关系。
2,如果我们改变a 的正负号,我们希望q 的符号也随之改变,但q 的绝对值不会变。
3,当b>0 时,我们希望保证r>=0 且r<b。
这三条性质是我们认为整数除法和余数操作所应该具备的。但是,很不幸,它们不可
能同时成立。
先考虑一个简单的例子:3/2,商为1,余数也为1。此时,第一条性质得到了满足。
好,把例子稍微改写一下:(-3)/2 的值应该是多少呢?如果要满足第二条性质,答案应
该是-1。但是,如果是这样,余数就必定是-1,这样第三条性质就无法满足了。如果我们首
先满足第三条性质,即余数是1,这种情况下根据第一条性质,商应该为-2,那么第二条性
质又无法满足了。
上面的矛盾似乎无法解决。因此,C 语言或者其他语言在实现整数除法截断运算时,必
须放弃上述三条性质中的至少一条。大多数编程语言选择了放弃第三条,而改为要求余数与
被除数的正负号相同,商按实际情况来。这样性质1 和性质2 就可以得到满足。大多数C 语言编译器也都是
如此。

但是,C 语言的定义只保证了性质1,以及当a>=0 且b>0 时,保证|r|<|b|以及r>=0。后
面部分的保证与性质2 或性质3 比较起来,限制性要弱得多。
通过上面的解释,你是否能准确算出2/(-2)和2%(-2)的值呢?



7.



8.

第一章我们详细讨论了const 这个关键字,我们知道const 修饰的数据是有类型的,而
define 宏定义的数据没有类型。为了安全,我建议你以后在定义一些宏常数的时候用const
代替,编译器会给const 修饰的只读变量做类型校验,减少错误的可能。但一定要注意const
修饰的不是常量而是readonly 的变量,const 修饰的只读变量不能用来作为定义数组的维数,
也不能放在case 关键字后面。


除了定义宏常数之外,经常还用来定义字符串,尤其是路径:
#define ENG_PATH_1 E:\English\listen_to_this\listen_to_this_3

但是请注意:有的系统里规定路径的要用双反斜杠“\\”,比如:
#define ENG_PATH_4 E:\\English\\listen_to_this\\listen_to_this_3

用define 宏定义注释符号?
上面对define 的使用都很简单,再看看下面的例子:
#define BSC //
#define BMC
D),BSC my single-line comment
E),BMC my multi-line comment EMC
D)和E)都错误,为什么呢?因为注释先于预处理指令被处理,当这两行被展开成//…或
时,注释已处理完毕,此时再出现//…或自然错误.因此,试图用宏开始或结束一段
注释是不行的。


9.

条件编译
条件编译的功能使得我们可以按不同的条件去编译不同的程序部分,因而产生不同的目
标代码文件。这对于程序的移植和调试是很有用的。条件编译有三种形式,下面分别介绍:
第一种形式:
#ifdef 标识符
程序段1
#else
程序段2
#endif
它的功能是,如果标识符已被#define 命令定义过则对程序段1 进行编译;否则对程序段2
进行编译。如果没有程序段2(它为空),本格式中的#else 可以没有,即可以写为:
#ifdef 标识符
程序段
#endif
第二种形式:
#ifndef 标识符
程序段1
#else
程序段2
#endif
与第一种形式的区别是将“ifdef”改为“ifndef”。它的功能是,如果标识符未被#define 命令定
义过则对程序段1 进行编译,否则对程序段2 进行编译。这与第一种形式的功能正相反。
第三种形式:
#if 常量表达式
程序段1
#else
程序段2
#endif
它的功能是,如常量表达式的值为真(非0),则对程序段1 进行编译,否则对程序段2 进行
编译。因此可以使程序在不同条件下,完成不同的功能。
至于#elif 命令意义与else if 相同,它形成一个if else-if 阶梯状语句,可进行多种编译选择。


#error 预处理

#error 预处理指令的作用是,编译程序时,只要遇到#error 就会生成一个编译错误提
示消息,并停止编译。其语法格式为:
#error error-message
注意,宏串error-message 不用双引号包围。遇到#error 指令时,错误信息被显示,可能同时
还显示编译程序作者预先定义的其他内容。关于系统所支持的error-message 信息,请查找
相关资料,这里不浪费篇幅来做讨论


10.

这里重点讨论内存对齐的问题和#pragma pack()的使用方法。
什么是内存对齐?
先看下面的结构:
struct TestStruct1
{
char c1;
short s;
char c2;
int i;
};
假设这个结构的成员在内存中是紧凑排列的,假设c1 的地址是0,那么s 的地址就应该
是1,c2 的地址就是3,i 的地址就是4。也就是c1 地址为00000000, s 地址为00000001, c2
地址为00000003, i 地址为00000004。
可是,我们在Visual C++6.0 中写一个简单的程序:
struct TestStruct1 a;
printf("c1 %p, s %p, c2 %p, i %p\n",
(unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.i - (unsigned int)(void*)&a);
运行,输出:
c1 00000000, s 00000002, c2 00000004, i 00000008。

为什么会这样?这就是内存对齐而导致的问题。

字,双字,和四字在自然边界上不需要在内存中对齐。(对字,双字,和四字来说,自
然边界分别是偶数地址,可以被4 整除的地址,和可以被8 整除的地址。
)无论如何,为了
提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为
了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访
问。
一个字或双字操作数跨越了4 字节边界,或者一个四字操作数跨越了8 字节边界,被
认为是未对齐的,从而需要两次总线周期来访问内存。
一个字起始地址是奇数但却没有跨
越字边界被认为是对齐的,能够在一个总线周期中被访问。某些操作双四字的指令需要内
存操作数在自然边界上对齐。如果操作数没有对齐,这些指令将会产生一个通用保护异常。
双四字的自然边界是能够被16 整除的地址。其他的操作双四字的指令允许未对齐的访问
(不会产生通用保护异常),然而,需要额外的内存总线周期来访问内存中未对齐的数据。
缺省情况下,编译器默认将结构、栈中的成员数据进行内存对齐。因此,上面的程序输
出就变成了:c1 00000000, s 00000002, c2 00000004, i 00000008。编译器将未对齐的成员向后
移,将每一个都成员对齐到自然边界上,从而也导致了整个结构的尺寸变大。尽管会牺牲
一点空间(成员之间有部分内存空闲),但提高了性能。也正是这个原因,我们不可以断言
sizeof(TestStruct1)的结果为8。在这个例子中,sizeof(TestStruct1)的结果为12。

那么,能不能既达到提高性能的目的,又能节约一点空间呢?有一点小技巧可以使用。
比如我们可以将上面的结构改成:
struct TestStruct2
{
char c1;
char c2;
short s;
int i;
};
这样一来,每个成员都对齐在其自然边界上,从而避免了编译器自动对齐。在这个例
子中,sizeof(TestStruct2)的值为8。这个技巧有一个重要的作用,尤其是这个结构作为API
的一部分提供给第三方开发使用的时候。第三方开发者可能将编译器的默认对齐选项改变,
从而造成这个结构在你的发行的DLL 中使用某种对齐方式,而在第三方开发者哪里却使用
另外一种对齐方式。这将会导致重大问题。
比如,TestStruct1 结构,我们的DLL 使用默认对齐选项,对齐为
c1 00000000, s 00000002, c2 00000004, i 00000008,同时sizeof(TestStruct1)的值为12。
而第三方将对齐选项关闭,导致
c1 00000000, s 00000001, c2 00000003, i 00000004,同时sizeof(TestStruct1)的值为8。
除此之外我们还可以利用#pragma pack()来改变编译器的默认对齐方式(当然一般编译器

也提供了一些改变对齐方式的选项,这里不讨论)。
使用指令#pragma pack (n),编译器将按照n 个字节对齐。
使用指令#pragma pack (),编译器将取消自定义字节对齐方式。
在#pragma pack (n)和#pragma pack ()之间的代码按n 个字节对齐。
但是,成员对齐有一个重要的条件,即每个成员按自己的方式对齐.也就是说虽然指定了
按n 字节对齐,但并不是所有的成员都是以n 字节对齐。其对齐的规则是,每个成员按其类型
的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是n 字节)中较小的一个对齐,即:
min( n, sizeof( item )) 。并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空
字节。看如下例子:
#pragma pack(8)
struct TestStruct4
{
char a;
long b;
};
struct TestStruct5
{
char c;
TestStruct4 d;
long long e;
};
#pragma pack()
问题:
A),sizeof(TestStruct5) = ?
B), TestStruct5 的c 后面空了几个字节接着是d?
TestStruct4 中,成员a 是1 字节默认按1 字节对齐,指定对齐参数为8,这两个值中取1,a
按1 字节对齐;成员b 是4 个字节,默认是按4 字节对齐,这时就按4 字节对齐,所以
sizeof(TestStruct4)应该为8;
TestStruct5 中,c 和TestStruct4 中的a 一样,按1 字节对齐,而d 是个结构,它是8 个字节,它
按什么对齐呢?对于结构来说,它的默认对齐方式就是它的所有成员使用的对齐参数中最大
的一个, TestStruct4 的就是4.所以,成员d 就是按4 字节对齐.成员e 是8 个字节,它是默认按8
字节对齐,和指定的一样,所以它对到8 字节的边界上,这时,已经使用了12 个字节了,所以又添
加了4 个字节的空,从第16 个字节开始放置成员e.这时,长度为24,已经可以被8(成员e 按8
字节对齐)整除.这样,一共使用了24 个字节.内存布局如下(*表示空闲内存,1 表示使用内存。
单位为1byete):
a b
TestStruct4 的内存布局:1***,1111,
c TestStruct4.a TestStruct4.b d
TestStruct5 的内存布局: 1***, 1***, 1111, ****,11111111
这里有三点很重要:
首先,每个成员分别按自己的方式对齐,并能最小化长度。
其次,复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂
类型时,可以最小化长度。
然后,对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保
证每一项都边界对齐。
补充一下,对于数组,比如:char a[3];它的对齐方式和分别写3 个char 是一样的.也就是说
它还是按1 个字节对齐.如果写: typedef char Array3[3];Array3 这种类型的对齐方式还是按1
个字节对齐,而不是按它的长度。
但是不论类型是什么,对齐的边界一定是1,2,4,8,16,32,64....中的一个。
另外,注意别的#pragma pack 的其他用法:
#pragma pack(push) //保存当前对其方式到packing stack
#pragma pack(push,n) 等效于
#pragma pack(push)
#pragma pack(n) //n=1,2,4,8,16 保存当前对齐方式,设置按n 字节对齐
#pragma pack(pop) //packing stack 出栈,并将对其方式设置为出栈的对齐方

NO。3

1.

先看下面的例子:
int *p;

现在用sizeof 测试一下(32 位系统):sizeof(p)的值为4。嗯,这说明咔出p的这个模子大小为4个byte。显然,这个模子不是“int”,虽然它大小也为4。既然不是“int”那就一定是“int *”了。好,那现在我们可以这么理解这个定义:

一个“int *”类型的模子在内存上咔出了4个字节的空间,然后把这个4个字节大小的

空间命名为p,同时限定这4个字节的空间里面只能存储某个内存地址,即使你存入别的任

何数据,都将被当作地址处理,而且这个内存地址开始的连续4个字节上只能存储某个int

类型的数据。

这是一段咬文嚼字的说明,我们还是用图来解析一下:


如上图所示,我们把p 称为指针变量,p 里存储的内存地址处的内存称为p 所指向的内存。

指针变量p 里存储的任何数据都将被当作地址来处理。

我们可以简单的这么理解:一个基本的数据类型(包括结构体等自定义类型)加上“*”

号就构成了一个指针类型的模子。这个模子的大小是一定的,与“*”号前面的数据类型无

关。“*”号前面的数据类型只是说明指针所指向的内存里存储的数据类型。所以,在32位

系统下,不管什么样的指针类型,其大小都为4byte。可以测试一下sizeof(void *)。


int *p = NULL 和*p = NULL 有什么区别?

很多初学者都无法分清这两者之间的区别。我们先看下面的代码:

int *p= NULL;

这时候我们可以通过编译器查看p的值为0x00000000。这句代码的意思是:定义一个指针

变量p,其指向的内存里面保存的是int类型的数据;在定义变量p的同时把p 的值设置为

0x00000000,而不是把*p的值设置为0x00000000。这个过程叫做初始化,是在编译的时候

进行的。



明白了什么是初始化之后,再看下面的代码:

int *p;

*p =NULL;

同样,我们可以在编译器上调试这两行代码。第一行代码,定义了一个指针变量p,其指向

的内存里面保存的是int 类型的数据;但是这时候变量p 本身的值是多少不得而知,也就是

说现在变量p 保存的有可能是一个非法的地址。第二行代码,给*p 赋值为NULL,即给p

指向的内存赋值为NULL;但是由于p指向的内存可能是非法的,所以调试的时候编译器可

能会报告一个内存访问错误。这样的话,我们可以把上面的代码改写改写,使p指向一块合

法的内存:

int i =10;

int *p= &i;

*p =NULL;

在编译器上调试一下,我们发现p指向的内存由原来的10变为0了;而p 本身的值,即内

存地址并没有改变。

另外还有初学者在使用NULL的时候误写成null或Null等。这些都是不正确的,C语

言对大小写十分敏感啊。当然,也确实有系统也定义了null,其意思也与NULL没有区别,

但是你千万不用使用null,这会影响你代码的移植性



2.

先看下面的例子:

int a[5];

所有人都明白这里定义了一个数组,其包含了5个int型的数据。我们可以用a[0],a[1]

等来访问数组里面的每一个元素,那么这些元素的名字就是a[0],a[1]…吗?看下面的示意

图:


如上图所示,当我们定义一个数组a时,编译器根据指定的元素个数和元素的类型分配确定

大小(元素类型大小*元素个数)的一块内存,并把这块内存的名字命名为a。名字a一旦

与这块内存匹配就不能被改变。a[0],a[1]等为a的元素,但并非元素的名字。数组的每一个

元素都是没有名字的。那现在再来回答第一章讲解sizeof关键字时的几个问题:

sizeof(a)的值为sizeof(int)*5,32位系统下为20。

sizeof(a[0])的值为sizeof(int),32位系统下为4。

sizeof(a[5])的值在32位系统下为4。并没有出错,为什么呢?我们讲过sizeof是关键字

不是函数。函数求值是在运行的时候,而关键字sizeof求值是在编译的时候。虽然并不存在

a[5]这个元素,但是这里也并没有去真正访问a[5],而是仅仅根据数组元素的类型来确定其

值。所以这里使用a[5]并不会出错。

sizeof(&a[0])的值在32位系下为4,这很好理解。取元素a[0]的首地址。

sizeof(&a)的值在32位系统下也为4,这也很好理解。取数组a的首地址。

省政府和市政的区别----&a[0]和&a的区别

这里&a[0]和&a到底有什么区别呢?a[0]是一个元素,a是整个数组,虽然&a[0]和&a

的值一样,但其意义不一样。前者是数组首元素的首地址,而后者是数组的首地址。举个

例子:湖南的省政府在长沙,而长沙的市政府也在长沙。两个政府都在长沙,但其代表的

意义完全不同。这里也是同一个意思

数组名a 作为左值和右值的区别

简单而言,出现在赋值符“=”右边的就是右值,出现在赋值符“=”左边的就是左值。

比如,x=y。

左值:在这个上下文环境中,编译器认为x的含义是x所代表的地址。这个地址只有

编译器知道,在编译的时候确定,编译器在一个特定的区域保存这个地址,我们完全不必

考虑这个地址保存在哪里。

右值:在这个上下文环境中,编译器认为y的含义是y所代表的地址里面的内容。这

个内容是什么,只有到运行时才知道。

C 语言引入一个术语-----“可修改的左值”。意思就是,出现在赋值符左边的符号所代

表的地址上的内容一定是可以被修改的。换句话说,就是我们只能给非只读变量赋值。

既然已经明白左值和右值的区别,下面就讨论一下数组作为左值和右值的情况:

当a 作为右值的时候代表的是什么意思呢?很多书认为是数组的首地址,其实这是非常

错误的。a 作为右值时其意义与&a[0]是一样,代表的是数组首元素的首地址,而不是数组

的首地址。这是两码事。但是注意,这仅仅是代表,并没有一个地方(这只是简单的这么

认为,其具体实现细节不作过多讨论)来存储这个地址,也就是说编译器并没有为数组a

分配一块内存来存其地址,这一点就与指针有很大的差别。


&a 代表数组的首地址

a代表数组首元素的地址和&a[0]一样



a 作为右值,我们清楚了其含义,那作为左值呢?

a不能作为左值!这个错误几乎每一个学生都犯过。编译器会认为数组名作为左值代表

的意思是a 的首元素的首地址,但是这个地址开始的一块内存是一个总体,我们只能访问数

组的某个元素而无法把数组当一个总体进行访问。所以我们可以把a[i]当左值,而无法把a

当左值。其实我们完全可以把a当一个普通的变量来看,只不过这个变量内部分为很多小块,

我们只能通过分别访问这些小块来达到访问整个变量a的目的



3.

以指针的形式访问和以下标的形式访问
下面我们就详细讨论讨论它们之间似是而非的一些特点。例如,函数内部有如下定义:
A),char *p = “abcdef”;
B),char a[] = “123456”;


以指针的形式访问和以下标的形式访问指针
例子A)定义了一个指针变量p,p 本身在栈上占4 个byte,p 里存储的是一块内存的首
地址。这块内存在静态区,其空间大小为7 个byte,这块内存也没有名字。对这块内存的访
问完全是匿名的访问。比如现在需要读取字符‘e’,我们有两种方式:
1),以指针的形式:*(p+4)。先取出p 里存储的地址值,假设为0x0000FF00,然后加
上4 个字符的偏移量,得到新的地址0x0000FF04。然后取出0x0000FF04 地址上的值。
2),以下标的形式:p[4]。编译器总是把以下标的形式的操作解析为以指针的形式的操
作。p[4]这个操作会被解析成:先取出p 里存储的地址值,然后加上中括号中4 个元素的偏
移量,计算出新的地址,然后从新的地址中取出值。也就是说以下标的形式访问在本质上
与以指针的形式访问没有区别,只是写法上不同罢了


以指针的形式访问和以下标的形式访问数组
例子B)定义了一个数组a,a 拥有7 个char 类型的元素,其空间大小为7。数组a 本身
在栈上面
。对a 的元素的访问必须先根据数组的名字a 找到数组首元素的首地址,然后根据
偏移量找到相应的值。这是一种典型的“具名+匿名”访问。比如现在需要读取字符‘5’,
我们有两种方式:
1),以指针的形式:*(a+4)。a 这时候代表的是数组首元素的首地址,假设为0x0000FF00,
然后加上4 个字符的偏移量,得到新的地址0x0000FF04。然后取出0x0000FF04 地址上的
值。
2),以下标的形式:a[4]。编译器总是把以下标的形式的操作解析为以指针的形式的操
作。a[4]这个操作会被解析成:a 作为数组首元素的首地址,然后加上中括号中4 个元素的
偏移量,计算出新的地址,然后从新的地址中取出值。


由上面的分析,我们可以看到,指针和数组根本就是两个完全不一样的东西。只是它们
都可以“以指针形式”或“以下标形式”进行访问。
一个是完全的匿名访问,一个是典型
的具名+匿名访问。一定要注意的是这个“以XXX 的形式的访问”这种表达方式。
另外一个需要强调的是:上面所说的偏移量4 代表的是4 个元素,而不是4 个byte。只
不过这里刚好是char 类型数据1 个字符的大小就为1 个byte。记住这个偏移量的单位是元
素的个数而不是byte 数,在计算新地址时千万别弄错了。


通过上面的分析,相信你已经明白数组和指针的访问方式了,下面再看这个例子:
void main()
{

int a[5]={1,2,3,4,5};

int *ptr=(int *)(&a+1);

printf("%d,%d",*(a+1),*(ptr-1));

}

打印出来的值为多少呢? 这里主要是考查关于指针加减操作的理解。
对指针进行加1 操作,得到的是下一个元素的地址,而不是原有地址值直接加1。所以,
一个类型为T 的指针的移动,以sizeof(T) 为移动单位。因此,对上题来说,a 是一个一
维数组,数组中有5 个元素; ptr 是一个int 型的指针。

&a + 1: 取数组a 的首地址,该地址的值加上sizeof(a) 的值,即&a + 5*sizeof(int),也
就是下一个数组的首地址,显然当前指针已经越过了数组的界限。
(int *)(&a+1): 则是把上一步计算出来的地址,强制转换为int * 类型,赋值给ptr。
*(a+1): a,&a 的值是一样的,但意思不一样,a 是数组首元素的首地址,也就是a[0]的
首地址,&a 是数组的首地址,a+1 是数组下一元素的首地址,即a[1]的首地址,&a+1 是下一
个数组的首地址。所以输出2

*(ptr-1): 因为ptr 是指向a[5],并且ptr 是int * 类型,所以*(ptr-1) 是指向a[4] ,
输出5。

---------------------

注意:

int a[3] = {1,2,3};

sizeof(a) = 12;

sizeof(&a) = 4;

sizeof(&a[0]) = 4;


a和&a数值相同,代表的意义不同

a代表数组首元素地址,即&a[0],a+1指向数组第二个元素

而&a代表数组首地址,&a+1则指向下一个数组的首地址


-------------------


4.

文件1 中定义如下:
char a[100];
文件2 中声明如下(关于extern 的用法,以及定义和声明的区别,请复习第一章):
extern char *a;
这里,文件1 中定义了数组a,文件2 中声明它为指针。这有什么问题吗?平时不是总说数
组与指针相似,甚至可以通用吗?但是,很不幸,这是错误的。通过上面的分析我们也能
明白一些,但是“革命尚未成功,同志仍需努力”。你或许还记得我上面说过的话:数组就
是数组,指针就是指针,它们是完全不同的两码事!他们之间没有任何关系,只是经常穿
着相似的衣服来迷惑你罢了。

当你声明为extern char *a 时,编译器理所当然的认为a 是一个指针变量,在32 位系
统下,占4 个byte。这4 个byte 里保存了一个地址,这个地址上存的是字符类型数据。虽
然在文件1 中,编译器知道a 是一个数组,但是在文件2 中,编译器并不知道这点。大多数
编译器是按文件分别编译的,编译器只按照本文件中声明的类型来处理。所以,虽然a 实际
大小为100 个byte,但是在文件2 中,编译器认为a 只占4 个byte。


显然,按照上面的分析,我们把文件1 中定义的数组在文件2 中声明为指针会发生错误。
同样的,如果在文件1 中定义为指针,而在文件中声明为数组也会发生错误:
文件1
char *p = “abcdefg”;
文件2
extern char p[];


5.

指针和数组的对比
通过上面的分析,相信你已经知道数组与指针的的确确是两码事了。他们之间是不可
以混淆的,但是我们可以“以XXXX 的形式”访问数组的元素或指针指向的内容。以后一
定要确认你的代码在一个地方定义为指针,在别的地方也只能声明为指针;在一个的地方
定义为数组,在别的地方也只能声明为数组。切记不可混淆。
下面再用一个表来总结一下
指针和数组的特性:





6.

初学者总是分不出指针数组与数组指针的区别。其实很好理解:
指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身
决定。它是“储存指针的数组”的简称。
数组指针:首先它是一个指针,它指向一个数组。在32 位系统下永远是占4 个字节,
至于它指向的数组占多少字节,不知道。它是“指向数组的指针”的简称。


下面到底哪个是数组指针,哪个是指针数组呢:
A),int *p1[10];
B),int (*p2)[10];
每次上课问这个问题,总有弄不清楚的。这里需要明白一个符号之间的优先级问题。
“[]”的优先级比“*”要高。p1 先与“[]”结合,构成一个数组的定义,数组名为p1,int *
修饰的是数组的内容,即数组的每个元素。那现在我们清楚,这是一个数组,其包含10 个
指向int 类型数据的指针,即指针数组。至于p2 就更好理解了,在这里“()”的优先级比
“[]”高,“*”号和p2 构成一个指针的定义,指针变量名为p2,int 修饰的是数组的内容,
即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚p2 是一个指
针,它指向一个包含10 个int 类型数据的数组,即数组指针


再论a 和&a 之间的区别
既然这样,那问题就来了。前面我们讲过a 和&a 之间的区别,现在再来看看下面的代
码:
int main()
{

char a[5]={'A','B','C','D'};

char (*p3)[5] = &a;

char (*p4)[5] = a;

return 0;

}
上面对p3 和p4 的使用,哪个正确呢?p3+1 的值会是什么?p4+1 的值又会是什么?
毫无疑问,p3 和p4 都是数组指针,指向的是整个数组。&a 是整个数组的首地址,a
是数组首元素的首地址,其值相同但意义不同。在C 语言里,赋值符号“=”号两边的数据
类型必须是相同的,如果不同需要显示或隐式的类型转换。p3 这个定义的“=”号两边的数
据类型完全一致,而p4 这个定义的“=”号两边的数据类型就不一致了。左边的类型是指
向整个数组的指针,右边的数据类型是指向单个字符的指针。在Visual C++6.0 上给出如下
警告:warning C4047: 'initializing' : 'char (*)[5]' differs in levels of indirection from 'char *'。还好,
这里虽然给出了警告,但由于&a 和a 的值一样,而变量作为右值时编译器只是取变量的值,
所以运行并没有什么问题。不过我仍然警告你别这么用。

既然现在清楚了p3 和p4 都是指向整个数组的,那p3+1 和p4+1 的值就很好理解了。
但是如果修改一下代码,会有什么问题?p3+1 和p4+1 的值又是多少呢?
int main()
{

char a[5]={'A','B','C','D'};

char (*p3)[3] = &a;//vs2008不过编译

char (*p4)[3] = a;

return 0;

}
甚至还可以把代码再修改:
int main()
{

char a[5]={'A','B','C','D'};

char (*p3)[10] = &a;//vs2008不过编译

char (*p4)[10] = a;

return 0;

}
这个时候又会有什么样的问题?p3+1 和p4+1 的值又是多少?
上述几个问题,希望读者能仔细考虑考虑。


7.

地址的强制转换
先看下面这个例子:
struct Test
{

int Num;

char *pcName;

short sDate;

char cha[2];

short sBa[4];

}*p;
假设p 的值为0x100000。如下表表达式的值分别为多少?
p + 0x1 = 0x___ ?

int Num:0-3

char* pcName:4-7

short sDate:8-9

char cha[2]:10-11//有数组的话,对齐应该按照其元素类型的大小来对齐,这里按1对齐

short sBa[4]:12-20//这里按2对齐

sizeof(Test) = 20

(unsigned long)p + 0x1 = 0x___?
(unsigned int*)p + 0x1 = 0x___?
我相信会有很多人一开始没看明白这个问题是什么意思。其实我们再仔细看看,这个知识点
似曾相识。一个指针变量与一个整数相加减,到底该怎么解析呢?
还记得前面我们的表达式“a+1”与“&a+1”之间的区别吗?其实这里也一样。指针变
量与一个整数相加减并不是用指针变量里的地址直接加减这个整数。这个整数的单位不是
byte 而是元素的个数。所以:
p + 0x1 的值为0x100000+sizof(Test)*0x1。至于此结构体的大小为20byte,前面的章
节已经详细讲解过。所以p +0x1 的值为:0x100014。
(unsigned long)p + 0x1 的值呢?这里涉及到强制转换,将指针变量p 保存的值强制转换
成无符号的长整型数。任何数值一旦被强制转换,其类型就改变了。所以这个表达式其实就
是一个无符号的长整型数加上另一个整数。所以其值为:0x100001。
(unsigned int*)p + 0x1 的值呢?这里的p 被强制转换成一个指向无符号整型的指针。所
以其值为:0x100000+sizof(unsigned int)*0x1,等于0x100004。



上面这个问题似乎还没啥技术含量,下面就来个有技术含量的:

在x86 系统下,其值为多少?
int main()
{

int a[4]={1,2,3,4};

int *ptr1=(int *)(&a+1);

int *ptr2=(int *)((int)a+1);

printf("%x,%x",ptr1[-1],*ptr2);

return 0;

}
这是我讲课时一个学生问我的题,他在网上看到的,据说难倒了n 个人。我看题之后告诉他,
这些人肯定不懂汇编,一个懂汇编的人,这种题实在是小case。下面就来分析分析这个问题:
根据上面的讲解,&a+1 与a+1 的区别已经清楚。
ptr1:将&a+1 的值强制转换成int*类型,赋值给int* 类型的变量ptr,ptr1 肯定指到数
组a 的下一个int 类型数据了。ptr1[-1]被解析成*(ptr1-1),即ptr1 往后退4 个byte。所以其
值为0x4。
ptr2:按照上面的讲解,(int)a+1 的值是元素a[0]的第二个字节的地址。然后把这个地址
强制转换成int*类型的值赋给ptr2,也就是说*ptr2 的值应该为元素a[0]的第二个字节开始的
连续4 个byte 的内容。
其内存布局如下图:



好,问题就来了,这连续4 个byte 里到底存了什么东西呢?也就是说元素a[0],a[1]里面
的值到底怎么存储的。这就涉及到系统的大小端模式了,如果懂汇编的话,这根本就不是问
题。既然不知道当前系统是什么模式,那就得想办法测试。大小端模式与测试的方法在第一
章讲解union 关键字时已经详细讨论过了,请翻到彼处参看,这里就不再详述。我们可以用
下面这个函数来测试当前系统的模式。
int checkSystem( )
{

union check

{

int i;

char ch;

} c;

c.i = 1;

return (c.ch ==1);

}
如果当前系统为大端模式这个函数返回0;如果为小端模式,函数返回1。
也就是说如果此函数的返回值为1 的话,*ptr2 的值为0x2000000。
如果此函数的返回值为0 的话,*ptr2 的值为0x100。


8.

二级指针是经常用到的,尤其与二维数组在一起的时候更是令人迷糊。例如:
char **p;
定义了一个二级指针变量p。p 是一个指针变量,毫无疑问在32 位系统下占4 个byte。
它与一级指针不同的是,一级指针保存的是数据的地址,二级指针保存的是一级指针的地
址。下图帮助理解


我们试着给变量p 初始化:
A),p = NULL;
B),char *p2; p = &p2;
任何指针变量都可以被初始化为NULL(注意是NULL,不是NUL,更不是null),二
级指针也不例外。也就是说把指针指向数组的零地址。联想到前面我们把尺子比作内存,
如果把内存初始化为NULL,就相当于把指针指向尺子上0 毫米处,这时候指针没有任何内
存可用。
当我们真正需要使用p 的时候,就必须把一个一级指针的地址保存到p 中,所以B)的
赋值方式也是正确的。
给p 赋值没有问题,但怎么使用p 呢?这就需要我们前面多次提到的钥匙(“*”)。
第一步:根据p 这个变量,取出它里面存的地址。
第二步:找到这个地址所在的内存。
第三步:用钥匙打开这块内存,取出它里面的地址,*p 的值。
第四步:找到第二次取出的这个地址。
第五步:用钥匙打开这块内存,取出它里面的内容,这就是我们真正的数据,**p 的值。
我们在这里用了两次钥匙(“*”)才最终取出了真正的数据。也就是说要取出二级指针
所真正指向的数据,需要使用两次两次钥匙(“*”)。
至于超过二维的数组和超过二维的指针一般使用比较少,而且按照上面的分析方法同
样也可以很轻松的分析明白,这里就不再详细讨论。读者有兴趣的话,可以研究研


9.

能否向函数传递一个数组?
看例子:
void fun(char a[10])
{

char c = a[3];

}

int main()
{

char b[10] = “abcdefg”;

fun(b[10]);

return 0;

}
先看上面的调用,fun(b[10]);将b[10]这个数组传递到fun 函数。但这样正确吗?b[10]
是代表一个数组吗?

这是一个内存异常,我们分析分析其原因。其实这里至少有两个严重的错误。
第一:b[10]并不存在,在编译的时候由于没有去实际地址取值,所以没有出错,但是
在运行时,将计算b[10]的实际地址,并且取值。这时候发生越界错误。
第二:编译器的警告已经告诉我们编译器需要的是一个char*类型的参数,而传递过去
的是一个char 类型的参数,这时候fun 函数会将传入的char 类型的数据当地址处理,同样
会发生错误。(这点前面已经详细讲解)


无法向函数传递一个数组
我们完全可以验证一下:
void fun(char a[10])
{

int i = sizeof(a);

char c = a[3];

}
如果数组b 真正传递到函数内部,那i 的值应该为10。但是我们测试后发现i 的值竟然
为4!为什么会这样呢?难道数组b 真的没有传递到函数内部?是的,确实没有传递过去,
这是因为这样一条规则:
C 语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元
素首地址的指针。

这么做是有原因的。在C 语言中,所有非数组形式的数据实参均以传值形式(对实参
做一份拷贝并传递给被调用的函数,函数不能修改作为实参的实际变量的值,而只能修改
传递给它的那份拷贝)调用。然而,如果要拷贝整个数组,无论在空间上还是在时间上,
其开销都是非常大的。更重要的是,在绝大部分情况下,你其实并不需要整个数组的拷贝,
你只想告诉函数在那一刻对哪个特定的数组感兴趣。这样的话,为了节省时间和空间,提
高程序运行的效率,于是就有了上述的规则
。同样的,函数的返回值也不能是一个数组,
而只能是指针。这里要明确的一个概念就是:函数本身是没有类型的,只有函数的返回值
才有类型。很多书都把这点弄错了,甚至出现“XXX 类型的函数”这种说法。简直是荒唐
至极!


既然如此,我们完全可以把fun 函数改写成下面的样子:
void fun(char *p)
{

char c = p[3];//或者是char c = *(p+3);

}
同样,你还可以试试这样子:
void fun(char a[10])
{

char c = a[3];

}

int main()
{

char b[100] = “abcdefg”;

fun(b);

return 0;

}
运行完全没有问题。实际传递的数组大小与函数形参指定的数组大小没有关系。既然
如此,那我们也可以改写成下面的样子:
void fun(char a[ ])
{

char c = a[3];

}
改写成这样或许比较好,至少不会让人误会成只能传递一个10 个元素的数组。


能否把指针变量本身传递给一个函数
我们把上一节讨论的列子再改写一下:
void fun(char *p)
{

char c = p[3];//或者是char c = *(p+3);

}
int main()
{

char *p2 = “abcdefg”;

fun(p2);

return 0;

}

我们知道p2 是main 函数内的一个局部变量,它只在main 函数内部有效。这里需要
澄清一个问题:main 函数内的变量不是全局变量,而是局部变量,只不过它的生命周期和
全局变量一样长而已。全局变量一定是定义在函数外部的。初学者往往弄错这点。)
既然它
是局部变量,fun 函数肯定无法使用p2 的真身。那函数调用怎么办?好办:对实参做一份
拷贝并传递给被调用的函数。即对p2 做一份拷贝,假设其拷贝名为_p2。那传递到函数内
部的就是_p2 而并非p2 本身。


10.

无法把指针变量本身传递给一个函数
这很像孙悟空拔下一根猴毛变成自己的样子去忽悠小妖怪。所以fun 函数实际运行时,
用到的都是_p2 这个变量而非p2 本身。如此,我们看下面的例子:
void GetMemory(char * p, int num)
{

p = (char *)malloc(num*sizeof(char));

}
int main()
{

char *str = NULL;

GetMemory(str,10);

strcpy(str,”hello”);

free(str);//free 并没有起作用,内存泄漏

return 0;

}

在运行strcpy(str,”hello”)语句的时候发生错误。这时候观察str 的值,发现仍然为NULL。
也就是说str 本身并没有改变,我们malloc 的内存的地址并没有赋给str,而是赋给了_str。
而这个_str 是编译器自动分配和回收的,我们根本就无法使用。

所以想这样获取一块内存是
不行的。那怎么办? 两个办法:
第一:用return。

char * GetMemory(char * p, int num)
{

p = (char *)malloc(num*sizeof(char));

return p;

}
int main()
{

char *str = NULL;

str = GetMemory(str,10);

strcpy(str,”hello”);

free(str);

return 0;

}

这个方法简单,容易理解。
第二:用二级指针。
void GetMemory(char ** p, int num)
{

*p = (char *)malloc(num*sizeof(char));

return p;

}
in tmain()
{

char *str = NULL;

GetMemory(&str,10);

strcpy(str,”hello”);

free(str);

return 0;

}


注意,这里的参数是&str 而非str。这样的话传递过去的是str 的地址,是一个值。在函
数内部,用钥匙(“*”)来开锁:*(&str),其值就是str。所以malloc 分配的内存地址是真正
赋值给了str 本身。
另外关于malloc 和free 的具体用法,内存管理那章有详细讨论。


11.

二维数组参数与二维指针参数
前面详细分析了二维数组与二维指针,那它们作为参数时与不作为参数时又有什么区
别呢?看例子:
void fun(char a[3][4]);

我们按照上面的分析,完全可以把a[3][4]理解为一个一维数组a[3],其每个元素都是一
个含有4 个char 类型数据的数组。上面的规则,“C 语言中,当一维数组作为函数参数的时
候,编译器总是把它解析成一个指向其首元素首地址的指针。”在这里同样适用,也就是说
我们可以把这个函数声明改写为:
void fun(char (*p)[4]);
这里的括号绝对不能省略,这样才能保证编译器把p 解析为一个指向包含4 个char 类
型数据元素的数组,即一维数组a[3]的元素。
同样,作为参数时,一维数组“[]”号内的数字完全可以省略:
void fun(char a[ ][4]);
不过第二维的维数却不可省略,想想为什么不可以省略?
注意:如果把上面提到的声明void fun(char (*p)[4])中的括号去掉之后,声明“void fun
(char *p[4])”可以改写成:
void fun(char **p);
这是因为参数*p[4],对于p 来说,它是一个包含4 个指针的一维数组,同样把这个一维数
组也改写为指针的形式,那就得到上面的写法。


这里需要注意的是:C 语言中,当一维数组作为函数参数的时候,编译器总是把它解析
成一个指向其首元素首地址的指针。这条规则并不是递归的,也就是说只有一维数组才是
如此,当数组超过一维时,将第一维改写为指向数组首元素首地址的指针之后,后面的维
再也不可改写。
比如:a[3][4][5]作为参数时可以被改写为(*p)[4][5]。


12.

顾名思义,函数指针就是函数的指针。它是一个指针,指向一个函数。看例子:
A),char * (*fun1)(char * p1,char * p2);
B),char * *fun2(char * p1,char * p2);
C),char * fun3(char * p1,char * p2);

看看上面三个表达式分别是什么意思?
C):这很容易,fun3 是函数名,p1,p2 是参数,其类型为char *型,函数的返回值为char *
类型。
B):也很简单,与C)表达式相比,唯一不同的就是函数的返回值类型为char**,是个
二级指针。
A):fun1 是函数名吗?回忆一下前面讲解数组指针时的情形。我们说数组指针这么定
义或许更清晰:
int (*)[10] p;
再看看A)表达式与这里何其相似!明白了吧。这里fun1 不是什么函数名,而是一个
指针变量,它指向一个函数。这个函数有两个指针类型的参数,函数的返回值也是一个指
针。同样,我们把这个表达式改写一下:char * (*)(char * p1,char * p2) fun1; 这样子是不
是好看一些呢?只可惜编译器不这么想

我们看看下面的例子:
void Function()
{

printf("Call Function!\n");

}
int main()
{

void (*p)();

*(int*)&p=(int)Function;

(*p) ();

return 0;

}
这是在干什么?*(int*)&p=(int)Function;表示什么意思?
别急,先看这行代码:
void (*p)();
这行代码定义了一个指针变量p,p 指向一个函数,这个函数的参数和返回值都是void。
&p 是求指针变量p 本身的地址,这是一个32 位的二进制常数(32 位系统)。
(int*)&p 表示将地址强制转换成指向int 类型数据的指针。
(int)Function 表示将函数的入口地址强制转换成int 类型的数据。
分析到这里,相信你已经明白*(int*)&p=(int)Function;表示将函数的入口地址赋值给指
针变量p。
那么(*p) ();就是表示对函数的调用。
讲解到这里,相信你已经明白了。其实函数指针与普通指针没什么差别,只是指向的内
容不同而已。
使用函数指针的好处在于,可以将实现同一功能的多个模块统一起来标识,这样一来更
容易后期的维护,系统结构更加清晰。或者归纳为:便于分层设计、利于系统抽象、降低耦
合度以及使接口与实现分开。

函数指针数组
现在我们清楚表达式“char * (*pf)(char * p)”定义的是一个函数指针pf。既然pf 是一
个指针,那就可以储存在一个数组里。把上式修改一下:
char * (*pf[3])(char * p);
这是定义一个函数指针数组。它是一个数组,数组名为pf,数组内存储了3 个指向函数的
指针。这些指针指向一些返回值类型为指向字符的指针、参数为一个指向字符的指针的函
数。这念起来似乎有点拗口。不过不要紧,关键是你明白这是一个指针数组,是数组。
函数指针数组怎么使用呢?这里也给出一个非常简单的例子,只要真正掌握了使用方法,
再复杂的问题都可以应对。如下:
#include <stdio.h>
#include <string.h>
char * fun1(char * p)
{

printf("%s\n",p);

return p;

}
char * fun2(char * p)
{

printf("%s\n",p);

return p;

}
char * fun3(char * p)
{

printf("%s\n",p);

return p;

}
int main()
{

char * (*pf[3])(char * p);

pf[0] = fun1; // 可以直接用函数名

pf[1] = &fun2; // 可以用函数名加上取地址符

pf[2] = &fun3;

pf[0]("fun1");

pf[0]("fun2");

pf[0]("fun3");

return 0;

}

函数指针数组的指针
看着这个标题没发狂吧?函数指针就够一般初学者折腾了,函数指针数组就更加麻烦,
现在的函数指针数组指针就更难理解了。
其实,没这么复杂。前面详细讨论过数组指针的问题,这里的函数指针数组指针不就是
一个指针嘛。只不过这个指针指向一个数组,这个数组里面存的都是指向函数的指针。仅
此而已。
下面就定义一个简单的函数指针数组指针:
char * (*(*pf)[3])(char * p);
注意,这里的pf 和上一节的pf 就完全是两码事了。上一节的pf 并非指针,而是一个数组名;
这里的pf 确实是实实在在的指针。这个指针指向一个包含了3 个元素的数组;这个数字里
面存的是指向函数的指针;这些指针指向一些返回值类型为指向字符的指针、参数为一个
指向字符的指针的函数。这比上一节的函数指针数组更拗口。其实你不用管这么多,明白
这是一个指针就ok 了。其用法与前面讲的数组指针没有差别。下面列一个简单的例子:
#include <stdio.h>
#include <string.h>
char * fun1(char * p)
{

printf("%s\n",p);

return p;

}
char * fun2(char * p)
{

printf("%s\n",p);

return p;

}
char * fun3(char * p)
{

printf("%s\n",p);

return p;

}
int main()
{

char * (*a[3])(char * p);

char * (*(*pf)[3])(char * p);

pf = &a;

a[0] = fun1;

a[1] = &fun2;

a[2] = &fun3;

pf[0][0]("fun1");

pf[0][1]("fun2");

pf[0][2]("fun3");

return 0;

}

NO。4

.

这条栓“野指针”的链子就是这个“NULL”。定义指针变量
的同时最好初始化为NULL,用完指针之后也将指针变量的值设置为NULL。也就是说除了
在使用时,别的时间都把指针“栓”到0 地址处。这样它就老实了。

对于程序员,一般来说,我们可以简单的理解为内存分为三个部分:静态区,栈,堆。
很多书没有把把堆和栈解释清楚,导致初学者总是分不清楚。其实堆栈就是栈,而不是堆。
堆的英文是heap;栈的英文是stack,也翻译为堆栈。堆和栈都有自己的特性,这里先不做
讨论。再打个比方:一层教学楼,可能有外语教室,允许外语系学生和老师进入;还可能
有数学教师,允许数学系学生和老师进入;还可能有×××,允许校长进入。同样,
内存也是这样,内存的三个部分,不是所有的东西都能存进去的。
静态区:保存自动全局变量和static 变量(包括static 全局和局部变量)。静态区的内容
在总个程序的生命周期内都存在,由编译器在编译的时候分配。
栈:保存局部变量。栈上的内容只在函数的范围内存在,当函数运行结束,这些内容
也会自动被销毁。其特点是效率高,但空间大小有限。
堆:由malloc 系列函数或new 操作符分配的内存。其生命周期由free 或delete 决定。
在没有释放之前一直存在,直到程序结束。其特点是使用灵活,空间比较大,但容易出错。


2.

指针没有指向一块合法的内存
定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内存。
浅显的例子就不举了,这里举几个比较隐蔽的例子。
结构体成员指针未初始化
struct student
{

char *name;

int score;

}stu,*pstu;


int main()
{

strcpy(stu.name,"Jimy");

stu.score = 99;

return 0;

}
很多初学者犯了这个错误还不知道是怎么回事。这里定义了结构体变量stu,但是他没
想到这个结构体内部char *name 这成员在定义结构体变量stu 时,只是给name 这个指针变
量本身分配了4 个字节。name 指针并没有指向一个合法的地址,这时候其内部存的只是一
些乱码。所以在调用strcpy 函数时,会将字符串"Jimy"往乱码所指的内存上拷贝,而这块内
存name 指针根本就无权访问,导致出错。解决的办法是为name 指针malloc 一块空间

同样,也有人犯如下错误:
int main()
{

pstu = (struct student*)malloc(sizeof(struct student));

strcpy(pstu->name,"Jimy");

pstu->score = 99;

free(pstu);

return 0;

}
为指针变量pstu 分配了内存,但是同样没有给name 指针分配内存。错误与上面第一种
情况一样,解决的办法也一样。这里用了一个malloc 给人一种错觉,以为也给name 指针分
配了内存。

没有为结构体指针分配足够的内存
int main()
{

pstu = (struct student*)malloc(sizeof(struct student*));

strcpy(pstu->name,"Jimy");

pstu->score = 99;

free(pstu);

return 0;

}
为pstu 分配内存的时候,分配的内存大小不合适。这里把sizeof(struct student)误写为
sizeof(struct student*)。当然name 指针同样没有被分配内存。解决办法同上。

函数的入口校验
不管什么时候,我们使用指针之前一定要确保指针是有效的。
一般在函数入口处使用assert(NULL != p)对参数进行校验。在非参数的地方使用
if(NULL != p)来校验。但这都有一个要求,即p 在定义的同时被初始化为NULL 了
。比
如上面的例子,即使用if(NULL != p)校验也起不了作用,因为name 指针并没有被初始
化为NULL,其内部是一个非NULL 的乱码。
assert 是一个宏,而不是函数,包含在assert.h 头文件中。如果其后面括号里的值为假,
则程序终止运行,并提示出错;如果后面括号里的值为真,则继续运行后面的代码。这个
宏只在Debug 版本上起作用,而在Release 版本被编译器完全优化掉,这样就不会影响代码
的性能。

有人也许会问,既然在Release 版本被编译器完全优化掉,那Release 版本是不是就完
全没有这个参数入口校验了呢?这样的话那不就跟不使用它效果一样吗?
是的,使用assert 宏的地方在Release 版本里面确实没有了这些校验。但是我们要知道,
assert 宏只是帮助我们调试代码用的,它的一切作用就是让我们尽可能的在调试函数的时候
把错误排除掉,而不是等到Release 之后。它本身并没有除错功能。再有一点就是,参数出
现错误并非本函数有问题,而是调用者传过来的实参有问题。assert 宏可以帮助我们定位错
误,而不是排除错误


为指针分配的内存太小
为指针分配了内存,但是内存大小不够,导致出现越界错误。
char *p1 = “abcdefg”;
char *p2 = (char *)malloc(sizeof(char)*strlen(p1));
strcpy(p2,p1);
p1 是字符串常量,其长度为7 个字符,但其所占内存大小为8 个byte。初学者往往忘
了字符串常量的结束标志“\0”。这样的话将导致p1 字符串中最后一个空字符“\0”没有被
拷贝到p2 中。解决的办法是加上这个字符串结束标志符:

char *p2 = (char *)malloc(sizeof(char)*strlen(p1)+1*sizeof(char));
这里需要注意的是,只有字符串常量才有结束标志符。比如下面这种写法就没有结束标志符
了:
char a[7] = {‘a’,’b’,’c’,’d’,’e’,’f’,’g’};
另外,不要因为char 类型大小为1 个byte 就省略sizof(char)这种写法。这样只会使
你的代码可移植性下降。


也许这种严重的问题并不多见,但是也绝不能掉以轻心。所以在定义一个变量时,第
一件事就是初始化。你可以把它初始化为一个有效的值,比如:
int i = 10;
char *p = (char *)malloc(sizeof(char));
但是往往这个时候我们还不确定这个变量的初值,这样的话可以初始化为0 或NULL。
int i = 0;
char *p = NULL;
如果定义的是数组的话,可以这样初始化:
int a[10] = {0};
或者用memset 函数来初始化为0:
memset(a,0,sizeof(a));
memset 函数有三个参数,第一个是要被设置的内存起始地址;第二个参数是要被设置
的值;第三个参数是要被设置的内存大小,单位为byte。这里并不想过多的讨论memset 函
数的用法,如果想了解更多,请参考相关资料。
至于指针变量如果未被初始化,会导致if 语句或assert 宏校验失败。这一点,上面已有
分析。


3.

下面先看malloc 函数的原型:
(void *)malloc(int size)
malloc 函数的返回值是一个void 类型的指针,参数为int 类型数据,即申请分配的内存
大小,单位是byte。内存分配成功之后,malloc 函数返回这块内存的首地址。你需要一个指
针来接收这个地址。但是由于函数的返回值是void *类型的,所以必须强制转换成你所接收
的类型。也就是说,这块内存将要用来存储什么类型的数据
。比如:
char *p = (char *)malloc(100);

在堆上分配了100 个字节内存,返回这块内存的首地址,把地址强制转换成char *类型后赋
给char *类型的指针变量p。同时告诉我们这块内存将用来存储char 类型的数据。也就是说
你只能通过指针变量p 来操作这块内存。这块内存本身并没有名字,对它的访问是匿名访
问。
上面就是使用malloc 函数成功分配一块内存的过程。但是,每次你都能分配成功吗?
不一定。上面的对话,皇帝让户部侍郎查询是否还有足够的良田未被分配出去。使用malloc
函数同样要注意这点:如果所申请的内存块大于目前堆上剩余内存块(整块),则内存分配
会失败,函数返回NULL。注意这里说的“堆上剩余内存块”不是所有剩余内存块之和,因
为malloc 函数申请的是连续的一块内存。
既然malloc 函数申请内存有不成功的可能,那我们在使用指向这块内存的指针时,必
须用if(NULL != p)语句来验证内存确实分配成功了

用malloc 函数申请0 字节内存
另外还有一个问题:用malloc 函数申请0 字节内存会返回NULL 指针吗?
可以测试一下,也可以去查找关于malloc 函数的说明文档。申请0 字节内存,函数并
不返回NULL,而是返回一个正常的内存地址。但是你却无法使用这块大小为0 的内存
。这
好尺子上的某个刻度,刻度本身并没有长度,只有某两个刻度一起才能量出长度。对于这
一点一定要小心,因为这时候if(NULL != p)语句校验将不起作用。

free 函数看上去挺狠的,但它到底作了什么呢?其实它就做了一件事:斩断指针变量与
这块内存的关系。
比如上面的例子,我们可以说malloc 函数分配的内存块是属于p 的,因
为我们对这块内存的访问都需要通过p 来进行。free 函数就是把这块内存和p 之间的所有关
系斩断。从此p 和那块内存之间再无瓜葛。至于指针变量p 本身保存的地址并没有改变,
但是它对这个地址处的那块内存却已经没有所有权了。那块被释放的内存里面保存的值也
没有改变,只是再也没有办法使用了。

这就是free 函数的功能。按照上面的分析,如果对p 连续两次以上使用free 函数,肯
定会发生错误。因为第一使用free 函数时,p 所属的内存已经被释放,第二次使用时已经无
内存可释放了。关于这点,我上课时让学生记住的是:一定要一夫一妻制,不然肯定出错。
malloc 两次只free 一次会内存泄漏;malloc 一次free 两次肯定会出错。也就是说,在程序
中malloc 的使用次数一定要和free 相等,否则必有错误。这种错误主要发生在循环使用
malloc 函数时,往往把malloc 和free 次数弄错了


既然使用free 函数之后指针变量p 本身保存的地址并没有改变,那我们就需要重新把p
的值变为NULL:
p = NULL;
这个NULL 就是我们前面所说的“栓野狗的链子”。如果你不栓起来迟早会出问题的
。比如:
在free(p)之后,你用if(NULL != p)这样的校验语句还能起作用吗?
例如:
char *p = (char *)malloc(100);
strcpy(p, “hello”);
free(p);

if (NULL != p)
{

strcpy(p, “world”);
}
释放完块内存之后,没有把指针置NULL,这个指针就成为了“野指针”,也有书叫“悬
垂指针”。这是很危险的,而且也是经常出错的地方。所以一定要记住一条:free 完之后,
一定要给指针置NULL。

同时留一个问题:对NULL 指针连续free 多次会出错吗?为什么?如果让你来设计free
函数,你会怎么处理这个问题?


这里一般有三种情况:
第一种:就是上面所说的,free(p)之后,继续通过p 指针来访问内存。解决的办法
就是给p 置NULL。
第二种:函数返回栈内存。这是初学者最容易犯的错误。比如在函数内部定义了一个
数组,却用return 语句返回指向该数组的指针。解决的办法就是弄明白栈上变量的生命周期。
第三种:内存使用太复杂,弄不清到底哪块内存被释放,哪块没有被释放。解决的办
法是重新设计程序,改善对象之间的调用关系。
上面详细讨论了常见的六种错误及解决对策,希望读者仔细研读,尽量使自己对每种错
误发生的原因及预防手段烂熟于胸。一定要多练,多调试代码,同时多总结经验。


嗯嗯,笔记已经差不多了,貌似书中还有些bug

最近在朋友的推荐下读了《C语言深度剖析》(电子版),虽然这本书讲的知识点在《C程序设计语言》《C专家编程》《C的陷阱和缺陷》这三本书中都有,但是作者把这些知识点提炼到100多页的书中,而且讲得层层递进,逻辑性很好,确实让人钦佩!感谢作者免费在网上共享此书的电子版。

在阅读的过程中发现4.1.5小节存在错误,在网上搜了一下,作者好像没有在网上放电子版的勘误表。把这个问题放在这里,希望对只看此书电子版的初学者有帮助。

这个问题只在电子版中存在,已经和作者陈正冲兄联系过,问题在实体书上已经修正了。

原文如下:

===================================================================================

4.1.5,编译器的bug?

另外一个有意思的现象,在Visual C++ 6.0调试如下代码的时候却又发现一个古怪的问题:

int *p = (int *)0x12ff7c;

*p = NULL;

p = NULL;

在执行完第二条代码之后,发现p的值变为0x00000000了。按照我么上一节的解释,应该p的值不变,只是p指向的内存被赋值为0。难道我们讲错了吗?别急,再试试如下代码:

int i = 10;

int *p = (int *)0x12ff7c;

*p = NULL;

p = NULL;

通过调试,发现这样子的话,p的值没有变,而p指向的内存的值变为0了。这与我们前面讲解的完全一致。当然这里的i的地址刚好是0x12ff7c,但这并不能改变“*p = NULL;”这行代码的功能。

为了再次测试这个问题,我又调试了如下代码:

int i = 10;

int j = 100;

int *p = (int *)0x12ff78;

*p = NULL;

p = NULL;

这里0x12ff78刚好就是变量j的地址。这样的话一切正常,但是如果把“int j = 100;”这行代码删除的话,又出现上述的问题了。测试到这里我还是不甘心,编译器怎么能犯这种低级错误呢?于是又接着进行了如下测试:

unsigned int i = 10;

//unsigned int j = 100;

unsigned int *p = (unsigned int *)0x12ff78;

*p = NULL;

p = NULL;

得到的结果与上面完全一样。当然,我还是没有死心,又进行了如下测试:

char ch = 10;

char *p = (char *)0x12ff7c;

*p = NULL;

p = NULL;

这样子的话,完全正常。但当我删除掉第一行代码后再测试,这里的p的值并未变成0x00000000,而是变成了0x0012ff00,同时*p的值变成了0。这又是怎么回事呢?初学者是否认为这是编译器“良心发现”,把*p的值改写为0了。

如果你真这么认为,那就大错特错了。这里的*p还是地址0x12ff7c上的内容吗?显然不是,而是地址0x0012ff00上的内容。至于0x12ff7c为什么变成0x0012ff00,则是因为编译器认为这是把NULL赋值给char类型的内存,所以只是把指针变量p的低地址上的一个字节赋值为0。至于为什么是低地址,请参看前面讲解过大小端模式相关内容。

测试到这里,已经基本可以肯定这是Visual C++ 6.0的一个bug。所以平时一定不要迷信某个编译器,要相信自己的判断。当然,后面还会提到一个我认为的Visual C++ 6.0的一个bug。还有,这个小小的例子,你是否可以在多个编译器上测试测试呢?

======================================================================

分析

先不说别的,只要想一想:如果一个编译器对“*p = NULL;p = NULL;”这样的语句处理都出现错误的话,那这个编译器是无法正常使用的,因为这些语句太常见!更何况是VC 6.0这样一个广泛使用的编译器?所以这不太可能是VC的bug。

再看测试程序

int *p = (int *)0x12ff7c;

*p = NULL;

p = NULL;

这个变量地址是怎么来得呢?在书的4.1.4小节说了:

“……其实这很简单,我们可以先定义一个变量i,比如:

int i = 0;

变量i所处的内存肯定是可以被访问的。然后在编译器的watch窗口上观察&i的值不就知道其内存地址了么?这里我得到的地址是0x12ff7c,仅此而已(不同的编译器可能每次给变量i分配的内存地址不一样,而刚好Visual C++ 6.0每次都一样)。……”

通过这段话可以确定0x12ff7c是在栈上的地址,我们知道临时变量是分配在栈中的,在上面的代码中p是临时变量,int *p = (int *)0x12ff7c;这句话的结果如下:

也就是说p分配在了栈中,地址是0x12ff7c,并且p所指向的地址也是0x12ff7c,p是一个指向自身的指针,相当于:

int *p;

p =(int *)&p;

此时无论是操作p和*p的结果都是一样的:

*p = NULL; //p指向的内容变为0,也就是地址0x12ff7c里的内容变为0

p = NULL;//p指向0,也就是p所在地址0x12ff7c里存储0

后面出问题的测试程序:

unsigned int i = 10;

//unsigned int j = 100;

unsigned int *p = (unsigned int *)0x12ff78;

*p = NULL;

p = NULL;

和上面一样还是指向自身的指针:

char *p = (char *)0x12ff7c;

*p = NULL;

p = NULL;

p为char类型指针,结合x86的little endian,存储结构如下:

*p = NULL;//*p只是指向了0x12ff7c开始的第一个字节7c,7c被替换为00

和作者所说的结果是一致的。

结论:这是一个“指向自身的指针”的正常行为,不是编译器的bug。

多说两句:

栈是由操作系统管理的(没有操作系统的话,编译器也会解决这个问题),滥用指向栈的内容的指针,很可能会出现一些意想不到的问题,比如,几乎所有的C语言书籍都会提到函数返回指向栈的指针这一“经典错误