c语言小球以任意角度反弹,《c语言深度剖析》第一章笔记

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 是指针指向的对象,不可变

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 修饰符也可以修饰函数的参数,当不希望这个参数值被函数体内意外改变时使

用。例如:

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。

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

c314a4d88005e11031a0ba7c1f242cd9.png

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

4035815956e2d501b97d7b8050dccef4.png

变量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

{ /*code*/} *”的别名, “struct student {/*code*/} *”是一个整体。对于编译器来说,只认为

Stu_pst 是一个类型名,所以在解析的时候很自然的把“Stu_pst”这个数据类型名忽略掉。

现在知道const 到底修饰的是什么了吧.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值