《c语言深度剖析》第一章笔记

来源:互联网 发布:直播唱歌软件 编辑:程序博客网 时间:2024/05/23 00:52
《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 int2、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。
以大端模式存储,其内存布局如下图:


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


变量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 到底修饰的是什么了吧.