c语言读书笔记

学人智库 时间:2018-01-16 我要投稿
【meiwen.anslib.com - 学人智库】

C语言是一门通用计算机编程语言,应用广泛。C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。大学网整理了C语言的读书笔记,欢迎大家阅读。

c语言读书笔记

《C 语言深度解剖》这本书是一本“解开程序员面试笔试的秘密”的好书。作者陈正冲老师提出“以含金量勇敢挑战国内外同类书籍”,确实,这本书中的知识点都是一些在面试中常见的考点,并且很多都是我们平常不注意的点,对于我们深入理解C语言确实很有帮助。

第1章关键字

1.register

虽然寄存器的速度非常快,但是使用register修饰符也有些限制的:register变量必须是能被CPU寄存器所接受的类型。

意味着register变量必须是一个单个的值,并且其长度应小于或等于整型的长度。而且register变量可能不存放在内存中,

所以不能用取址运算符“&”来获取register变量的地址。

2.static修饰符

(1)修饰变量

静态局部变量,在函数体里面定义的,就只能在这个函数里用了,同一个文档中的其他函数也用不了。由于被static修饰的变量总是存在内存的静态区,所以即使这个函数运行结束,这个静态变量的值还是不会被销毁,函数下次使用时仍然能用到这个值。

(2)修饰函数

第二个作用:修饰函数。函数前加static使得函数成为静态函数。但此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件(所以又称内部函数)。使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件中的函数同名。

关键字static有着不寻常的历史。起初,在C中引入关键字static是为了表示退出一个块后仍然存在的局部变量。随后,static在C中有了第二种含义:用来表示不能被其它文件访问的全局变量和函数。为了避免引入新的关键字,所以仍使用static关键字来表示这第二种含义。

3.if语句使用注意

先处理正常情况,再处理异常情况。

在编写代码是,要使得正常情况的执行代码清晰,确认那些不常发生的异常情况处理代码不会遮掩正常的执行路径。这样对于代码的可读性和性能都很重要。因为,if

语句总是需要做判断,而正常情况一般比异常情况发生的概率更大(否则就应该把异常正常调过来了),如果把执行概率更大的代码放到后面,也就意味着if语句将进行多次无谓的比较。

另外,非常重要的一点是,把正常情况的处理放在if后面,而不要放在else后面。当然这也符合把正常情况的处理放在前面的要求。

4.千万小心又小心使用void指针类型。

按照ANSI(AmericanNationalStandardsInstitute)标准,不能对void指针进行算法操作,即下列操作都是不合法的:

void*pvoid;

pvoid++;//ANSI:错误

pvoid+=1;//ANSI:错误

ANSI标准之所以这样认定,是因为它坚持:进行算法操作的指针必须是确定知道其指向数据类型大小的。也就是说必须知道内存目的地址的确切值。

例如:

int*pint;

pint++;//ANSI:正确

但是大名鼎鼎的GNU(GNU'sNotUnix的递归缩写)则不这么认定,它指定void*的算法操作与char*一致。因此下列语句在GNU编译器中皆正确:

pvoid++;//GNU:正确

pvoid+=1;//GNU:正确

在实际的程序设计中,为符合ANSI标准,并提高程序的可移植性,我们可以这样编写实现同样功能的代码:

void*pvoid;

(char*)pvoid++;//ANSI:正确;GNU:正确

(char*)pvoid+=1;//ANSI:错误;GNU:正确

GNU和ANSI还有一些区别,总体而言,GNU较ANSI更“开放”,提供了对更多语法的支持。但是我们在真实设计时,还是应该尽可能地符合ANSI标准。

5.const与宏

节省空间,避免不必要的内存分配,同时提高效率

编译器通常不为普通const只读变量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高。

例如:

#define M 3//宏常量

const int N=5;//此时并未将N放入内存中

......

inti=N;//此时为N分配内存,以后不再分配!

intI=M;//预编译期间进行宏替换,分配内存

intj=N;//没有内存分配

intJ=M;//再进行宏替换,又一次分配内存!

const定义的只读变量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的只读变量在程序运行过程中只有一份拷贝(因为它是全局的只读变量,存放在静态区),而#define定义的宏常量在内存中有若干个拷贝。#define宏是在预编译阶段进行替换,而const修饰的只读变量是在编译的时候确定其值。

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

6.最易变的关键字----volatile

volatile是易变的、不稳定的意思。很多人根本就没见过这个关键字,不知道它的存在。也有很多程序员知道它的存在,但从来没用过它。我对它有种“杨家有女初长成,养在深闺人未识”的感觉。volatile关键字和const一样是一种类型修饰符,用它修饰的变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

先看看下面的例子:

int i=10;

int j=i;//(1)语句

int k=i;//(2)语句

这时候编译器对代码进行优化,因为在(1)(2)两条语句中,i没有被用作左值。这时候编译器认为i的值没有发生改变,所以在(1)语句时从内存中取出i的值赋给j

之后,这个值并没有被丢掉,而是在(2)语句时继续用这个值给k赋值。编译器不会生成出汇编代码重新从内存里取i的值,这样提高了效率。但要注意:(1)(2)语句之间i没有被用作左值才行。

再看另一个例子:

volatile int i=10;

int j=i;//(3)语句

int k=i;//(4)语句

volatile关键字告诉编译器i是随时可能发生变化的,每次使用它的时候必须从内存中取出i的值,因而编译器生成的汇编代码会重新从i的地址处读取数据放在k中。

这样看来,如果i是一个寄存器变量或者表示一个端口数据或者是多个线程的共享数据,就容易出错,所以说volatile可以保证对特殊地址的稳定访问。

但是注意:在VC++6.0中,一般Debug模式没有进行代码优化,所以这个关键字的作用有可能看不出来。你可以同时生成Debug版和Release版的程序做个测试。

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

这个可以同时使用。

7.空结构体是有大小的

structstudent

{

}stu;

sizeof(stu)的值是多少呢?在VisualC++6.0上测试一下。

很遗憾,不是0,而是1。为什么呢?你想想,如果我们把structstudent看成一个模子的话,你能造出一个没有任何容积的模子吗?显然不行。编译器也是如此认为。编译器认为任何一种数据类型都有其大小,用它来定义一个变量能够分配确定大小的空间。既然如此,编译器就理所当然的认为任何一个结构体都是有大小的,哪怕这个结构体为空。那万一结构体真的为空,它的大小为什么值比较合适呢?假设结构体内只有一个char型的数据成员,那其大小为1byte(这里先不考虑内存对齐的情况).也就是说非空结构体类型数据最少需要占一个字节的空间,而空结构体类型数据总不能比最小的非空结构体类型数据所占的空间大吧。这就麻烦了,空结构体的大小既不能为0,也不能大于1,怎么办?定义为0.5个byte?但是内存地址的最小单位是1个byte,0.5个byte怎么处理?解决这个问题的最好办法就是折中,编译器理所当然的认为你构造一个结构体数据类型是用来打包一些数据成员的,而最小的数据成员需要1个byte,编译器为每个结构体类型数据至少预留1个byte的空间。所以,空结构体的大小就定位1个byte。

8. 大端与小端

在x86 系统下,输出的值为多少?

#include

int main()

{

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

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

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

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

return 0;

}

5和0x02000000

由于x86是小端方式,所以低位内容存放到了低位地址。图中每一种颜色代笔一个int型的内存分布。&a可以获得数组a的地址,也就是这儿的0xbfd46624, 所以&a+1的结果应该是0xbfd46638(即图中最下面红色部分)。对于代码中的ptr1由于其为int型指针,所以ptr[-1]的意思应该是取0xbfd46638地址之前的一个整型,即为a数组中的最后一个值5。而在计算ptr2的时候,(int)a是将整型地址a转换成了一个整型数,这样(int)a+1的结果就是0xbfd46625,然后再将其转化为int型指针,这样利用ptr2获得的数值就是从0xbfd46625开始的一个整型,即为0x02000000

10. 花括号

花括号每个人都见过,很简单吧。但曾经有一个学生问过我如下问题:

char a[10] = {“abcde”};

他不理解为什么这个表达式正确。我让他继续改一下这个例子:

char a[10] { = “abcde”};

问他这样行不行。那读者以为呢?为什么?

花括号的作用是什么呢?我们平时写函数,if、while、for、switch 语句等都用到了它,但有时又省略掉了它。简单来说花括号的作用就是打包。你想想以前用花括号是不是为了把一些语句或代码打个包包起来,使之形成一个整体,并与外界绝缘。这样理解的话,上面的问题就不是问题了。

11.再论 a 和&a 之间的区别

int main()

{

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

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

char (*p4)[5] = a;

return 0;

}

int main()

{

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

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

char (*p4)[3] = a;

return 0;

}

int main()

{

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

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

char (*p4)[10] = a;

return 0;

}

int a[5][5];

int (*p)[4];

p = a;

问&p[4][2] - &a[4][2]的值为多少?

12. 用 malloc 函数申请 0 字节内存

另外还有一个问题:用 malloc 函数申请 0 字节内存会返回 NULL 指针吗?

可以测试一下,也可以去查找关于 malloc 函数的说明文档。申请 0 字节内存,函数并不返回 NULL,而是返回一个正常的内存地址。但是你却无法使用这块大小为 0

的内存。这好尺子上的某个刻度,刻度本身并没有长度,只有某两个刻度一起才能量出长度。对于这一点一定要小心,因为这时候 if(NULL != p)语句校验将不起作用。

13. 不使用任何变量编写 strlen 函数

看到这里,也许有人会说,strlen 函数这么简单,有什么好讨论的。是的,我相信你能 熟练应用这个函数,也相信你能轻易的写出这个函数。但是如果我把要求提高一些呢:

不允许调用库函数,也不允许使用任何全局或局部变量编写 int my_strlen (char *strDest); 似乎问题就没有那么简单了吧?这个问题曾经在网络上讨论的比较热烈,我几乎是全程“观战” ,差点也忍不住手痒了。不过因为我的解决办法在我看到帖子时已经有人提出了, 所以作罢。

解决这个问题的办法由好几种,比如嵌套有编语言。因为嵌套汇编一般只在嵌入式底 层开发中用到,所以本书就不打算讨论 C 语言嵌套汇编的知识了。 有兴趣的读者,可以查找相关资料。 也许有的读者想到了用递归函数来解决这个问题。是的,你应该想得到,因为我把这 个问题放在讲解函数递归的时候讨论。

既然已经有了思路, 这个问题就很简单了。

代码如下:

int my_strlen( const char* strDest )

{

assert(NULL != strDest);

if ('' == *strDest)

{

return 0;

}

else

{

return (1 + my_strlen(++strDest));

}

}

第一步:用 assert 宏做入口校验。

第二步:确定参数传递过来的地址上的内存存储的是否为''。如果是,表明这是一个 空字符串,或者是字符串的结束标志。

第三步:如果参数传递过来的地址上的内存不为'',则说明这个地址上的内存上存储 的是一个字符。既然这个地址上存储了一个字符,那就计数为 1,然后将地址加 1 个 char类型元素的大小,然后再调用函数本身。如此循环,当地址加到字符串的结束标志符''时, 递归停止。

当然,同样是利用递归,还有人写出了更加简洁的代码:

int my_strlen( const char* strDest )

{

return *strDest?1+strlen(strDest+1):0;

}

这里很巧妙的利用了问号表达式, 但是没有做参数入口校验, 同时用*strDest 来代替('' == *strDest)也不是很好。所以,这种写法虽然很简洁,但不符合我们前面所讲的编码规范。

可以改写一下:

int my_strlen( const char* strDest )

{

assert(NULL != strDest);

return ('' != *strDest)?(1+my_strlen(strDest+1)):0;

}

上面的问题利用函数递归的特性就轻易的搞定了, 也就是说每调用一遍 my_strlen 函数, 其实只判断了一个字节上的内容。但是,如果传入的字符串很长的话,就需要连续多次函数调用,而函数调用的开销比循环来说要大得多,所以,递归的效率很低,递归的深度太大甚 至可能出现错误(比如栈溢出) 。所以,平时写代码,不到万不得已,尽量不要用递归。即便是要用递归,也要注意递归的层次不要太深,防止出现栈溢出的错误;同时递归的停止条 件一定要正确,否则,递归可能没完没了。

c语言读书笔记

我有一本c语言程序设计书,已经静静的躺着好长时间了,前几天朋友说,你这本书有些发黄了,看来是好长时间没有翻过了。

其实这本书是我初学电脑时朋友给的,但只看了几页就放在那儿了,那时没有自己的电脑,看起来很吃力。在朋友的提醒下,现在可以找一点时间去翻一翻,虽然没有什么价值,但学习也是一种乐趣......

记录第二天:

昨天详细读了一下第一章,感觉写的很详细,但我不知道书上的知识点是否全正确。

一、数据类型的基本概念

(1)数据类型规定了一个以值为其元素的集合。

(2)数据类型定义了一个运算集

(3)数据类型定义了数据在计算机内的存储及书写中的表示方式。

二、c语言的数据类型

数据包含常量和变量,他们都属于某个数据类型。

三、常量

常量是程序中其值不发生变化的量,常量有数、字符和字符串。在c中常量不需要类型说明就可以直接使用,除此之后c中还有一种表示常量的形式,称为符号常量。

(一)数

1、c中数有整数和小数

(1)整数

整数有十进制,八进制,十六进制,八进制以数字0开头,十六进制以0x开头,十进制以1-9中的一个开头。

整数有正负之分,分别在前面加上+,-符号,正数的符号+可以省略。

整数有短整数,整数,长整数之分,一般短整数16位,在我的机器上是short(16),int(32),long(32),对于整数的取值范围有符号的(-2的n位次方至2的n位次方减1),无符号的0到2的n位次方-1

长型数的书写方法:在数后面加一个L,如:0xffL

(2)实数

又叫浮点数,只有十进制的,实数有单精度和双精度之分,实数有两种表示形式,一种是小数,一种是指数。

小数:由整数部分,小数点,小数部分。

指数:由尾数(必须有),e/E,指数(必须是整数)

(二)字符常量

字符常量是用一对''括起来的单一字符,一个字符常量在计算机中占一个字节(8位),字符常量的值就是这个字符在所属字符集中的编码(如:ASCII码)。

字符常量中的单引号用作定界符,所以对于单引号的表示方法就得用另外一种方法表示:''' 用一个斜杠加一个'来转义一个单引号,对于斜框用来转义。

字符常量在计算机中是以编码的方式存放,所以说他实际是一个字节的整数,可以参与各种运算,如+,-,*,/,比较等。

(三)字符串常量

字符串常量是用双引号护起来的一串字符,字符的个数称其长度,字符串常量简称为字符串。

长度为n的字符串在计算机的存储中占用n+1个字节,最后多出来的那个存放一个NULL字符,ASCII编码是0,我们可以用''来表示这个字符。

任何一个字符串在机器内都是以结尾。如:abc,其实是'a','b','c',''的存储格式。

对于"这个双引号,由于用作定界符了,所以只能转义表示:"。

注意: 'A' , "A"这是两个不同的存储方式,一个是65,一个是65,0

(四)

一般的字符可以直接写出,但对于NULL,回车,新行,退格等字符如何书写?

对于这些字符我们可以用其它的方式表示出来,常用的有:

新行

回车

水平制表

v 垂直制表

退格

f 换页

a 响铃

" 双引号

' 单引号

NULL

ddd 1到3位八进制数表示一个字符

xdd 1到2位十六进制数表示一个字符

其实ddd,xdd就是字符的编码。

(五)符号常量

在c语言中可以对常量进行命名,即用符号代替常数值,该符号叫符号常量,符号常量一般用大写字母表示。

定义格式:

#define 符号常量名 常量

如:

#define NULL 0

#define EOF -1

#define PI 3.1415926

这里#define是预编译命令,每一个#define只能定义一个符号常量,且用一行书写,不用分号结尾。

例:

#include

#define PI 3.1415926

int main(void)

{

float area,r;

printf("请输入圆的半径:");

scanf("%f",&r);

area = PI*r*r;

printf("这个圆的面积是:%f ", area);

return 0;

}

使用符号常量的好处:

(1)增强程序的可读性。

如用PI,代表数学中的PI,用EOF代表文件尾,很直观。

(2)增强程序的可维护性

如果多处用到同一个常量,可以定义符号常量,维护时只修改一处即可。对于扩充和可移植一个程序时大有好处。

四、变量

变量是他的值可以发生变化的一种量,每一个变量都对应计算机中相应长度的存储单元,以存放变量所取的值。

如:

#include

int main(void)

{

int x = 0; //x的初值为0

x = 5; //x的值发生了变化

x = 7*5;

x = x+1;

printf("%d ",x);

return 0;

}

每一个变量都用一个名字(叫变量名)来表示,变量的名字实际上是内存单元的命名,变量的地址就是该内存单元的开始地址。变量的命名规则同用户自定义标识符。

任何一个变量都属于某一数据类型,如果他是整型量,则只能取整数值。

(一)基本数据类型

(1)、从长度上分有8位、16位、32位和64们珠。

(2)、从数据的符号来分,有无符号的,和有符号的。

(3)、按照数学性质来分,分为整型和实型。

c语言的基本数据类型表:

对于数的值域范围的算法:

有符号:[-2(位长度-1)次方到 2(位长度-1)次方-1]

无符号:[0到 2位长度次方]

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

类型标识符 名字 长度 范围

char 字符型 8 ASCII字符代码

unsigned char 无符号字符型 8 0-255

signed char 有符号字符型 8 -127:127

int 整型 16(和环境有关) 范围算法

unsigned int 无符号整型 同上 同上

signed int 有符号整数 同上 同上

short int 短整型 16(和环境有关) 同上

unsigned short int 无符号短整型 同上 同上

signed short int 有符号短整型 同上 同上

long int 长整型 32位(和环境有关) 同上

unsigned long int 无符号长整型 同上 同上

signed long int 有符号长整型 同上 同上

float 单精度浮点型 32

double 双精度浮点型 64

void 空类型

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

注:

(1)void类型有两种用法,一是指定函数的返回值的类型,一是用来设置类属指针。

(2)对于不同的环境,数据表示的范围不同,可以用sizeof(类型)来测试。

(二)变量的定义

变量的定义就是按照特定的方式为其指定标识,类型和长度等。程序中所用的每一个变量都必须先定义后引用,没有定义的变量是不能引用的。定义的目的是为编译程序提供所需的信息,以保证完成以下工作。

1、在编译时根据类型信息来检查程序中有无非法的数据结构。

2、根据类型信息来检查对变量施加的运算是否合理。如对两个浮点数不可以进行%运算。

3、编译时根据类型和长度信息对变量分配内存,确定数据在内存中的表示方式。

定义:

数据类型 变量或变量表;

如:

int year,date;

int x;

(三)变量的初始化

int x; 对于这样的变量只是指定了名字和数据类型,并没有给他初始值,但这并不表示变量没有值,他的值是当前内存单元中的数据,这个数据是没有意义的,引用时会产生莫名其妙的结果。

初始化:在定义时直接赋值,则称为初始化。

int x = 5;

double y = 3.4;

变量的初始化并不都是在编译阶段完成的,只有静态变量和外部变量是在编译阶段完成,而局部变量是在运行时初始化的。局部变量的初始化就是一个赋值语句。

int abc()

{

int a =3;

int b =4; //这是在运行时才初始化,而非编译时。

}

//补充

数据类型的转换

在c语言的表达式中,准许对不同类型的数值型数据进行某一操作,当不同类型的数据进行操作时,先将其转换成相同的数据类型,然后再进行操作。

有两种转换方式:隐式转换,显示转换。

一、隐式类型转换

隐式转换就是在编译时由编译程序按照一定规则自动完成。c语言规定的转换规则是由低级向高级转换,如果一个操作符带有两个不同类型的操作数,那么在操作之前先将较低的类型向高级转换,再进行运算。

注意:

1、所有的float类型都转换成double类型,提高运算精度。

2、在赋值语句中,如果=号左右两边的数据类型不同,则将赋值号右边的值转换为赋值号左边的数据类型。

3、在函数参数传递时,也发生数据类型转换。

4、char short自动转换成int再参与运算。

规则:

1、char short自动转换成int

2、float转换成double

3、低级到高级-> char short ->int ->unsigned-> long ->(float ->double)double

二、显式转换

显式转换又叫强制型转换,他不换默认规则,由程序员指定。

如:

int i;

i = i+3.124; //默认是i转成double,再运算,之后double转成int赋给i

显示转换: i = i+(int)3.124 //先把3.124转成int,再参与运算。

显示类型转换方法:(数据类型)表达式

如:(int) 3.14159; //把double -> int

[c语言读书笔记]