C语言笔记(1.3版本,目前3.5w字)----未完待续

x33g5p2x  于2021-11-24 转载在 其他  
字(35.0k)|赞(0)|评价(0)|浏览(176)

前言

今天的笔记是1.3版本,想跟大家分享一下我的笔记,很明显,目前并没有完成,想要完整的完成,我还有很长一段路要走,其中也许会有一些值得大家借鉴的地方,具体还是因人而异吧,希望大家能够有所收获,后续我还会继续补充,这次呢,重点内容我将不作标注,因为针对每个人的的个人情况不同,所以重点内容也不尽相同,希望大家能够关注我(虽然写的不太好),里面有的是一些基础,有的不是,有的是一些易错点,有的是一些边角料,更新频率的话最少一周一次,直到彻底补充完整,如果大家觉得对自己有所帮助的话,希望大家点一波关注和小小的赞吧,谢谢大家的支持!

0、基础常识

(1)进制

1.\ddd表示1~3个八进制的数字,注意\071和\71表示的都是八进制的数字。

(2)变量与常量

**1.**局部变量是指代码块内的变量,全局变量是指代码块外定义的变量。

**2.**定义并且初始化变量的本质是先根据变量类型所占据的内存空间大小为依据开辟空间,然后把索要存储的数据的二进制补码形式存储在内存中,就像unsigned int 类型的变量也可以存储125一样,换句话说,无论什么类型,都可以互相存储,当然,前提是开辟的内存能够放的下,因为无论存储什么数据,存放的都是二进制补码形式,只有在输出时才会进行不同形式的转换,比如-128的补码在转换为原码形式符号位不变,其它位按位取反,(假设输出类型是unsigned int),然后加1,然后转换为十进制进行输出,而如果输出类型是signed int时,就直接把补码转换为十进制即可,至于一些运算就无关紧要了,因为都是以补码形式进行计算。

原码:数据的二进制位,最左端的位为符号位。

反码:符号位不变,源码按位取反。

补码:反码加1.

正数的原码、反码、补码相同,负数的反码为原码符号位不变,其它位按位取反,补码为反码加1。

整数在内存中存储的为补码。

**3.**C语言中的常量分为以下以下几种:字面常量、const 修饰的常变量(注意const修饰的最然不可被改变,但本质上仍为变量,不能在定义数组使放入[]内,因为[]内只能是常量)、#define 定义的标识符常量、枚举常量

枚举常量的定义方式

enum Sex { MALE, FEMALE, SECRET }; //括号中的MALE,FEMALE,SECRET是枚举常量(按照整数打印后数值为0 1 2)

**4.**当局部变量和全局变量同名的时候,局部变量优先使用,但一般在定义局部变量时不要和全局变量同名。

**6.**全局变量静态变量在编译期间就创建好了。

**7.**诸如strlen等的函数名可以作为变量名,但我们并不推荐!

**8.**define不是关键字,是一个预处理指令,由编译器实现,可以作为变量名,但关键字不可以。

**9.**变量的访问原则:

(1)不允许在同一个作用域中定义多个相同名称的变量,编译器会报错,显示变量重定义。

(2)允许在不同的作用域中定义多个不同名称的变量。

(3)不同作用域中定义的变量,在访问时采用就近原则。即局部优先原则。

(3)内存

**1.**int 和 long(int)一般都是八个字节,事实上对于long (int)的字节长度要求是大于或者等于int类型所占的字节数。

**2.**强制类型转换的格式是(类型) 变量名而不是 类型 (变量名),后者是ui变量的定义,编译器会显示对变量的重定义。

3.任何有值特性的均能用sizeof()求其所占的内存的大小,比如sizeof(10),编译器一般会把整数默认为是int 类型占据4个字节,把小数默认为是double占据8个字节。

(4)其它零零碎碎的点

1.C语言中是没有次方运算的,如果要进行次方运算需要运用pow()函数,^是异或运算符。

scanf格式输入要注意同步,scanf()运用时格式时非常严格的,代码格式和输入格式要严格对照。同时注意一点,如果定义普通变量之后未初始化是无法输出的,但如果在定义之后,用scanf()进行输入操作此时不初始化也没有问题,但我们通常并不建议这样操作,因为在编写程序时,我们赋的初值有时会具有某些含义,所以无论后续是否用scanf进行输入,最好都要初始化。

2.在定义变量时,因为编译器默认为我们输入的整数是int,输入的浮点数为double,所以在定义单精度浮点数时float a = 3.14f

3.浮点数在进行比较的时候,绝对不能用==进行直接比较,浮点数在存储的时候本身会有精度损失,进而导致结果可能会有各种细微的差别。

解决方案:#define EPSILON(精度的意思) 0.00001(自己设定的精度)

if ( (x - y) >-0.00001 && (x-y) < 0.00001) 或者 if(fabs(x-y)

实际上,系统已经给我们定义好了精度,即DBL_EPSILON 用法同上,判断两个数是否相等时是if(fabs(x-y)),如果判断某一个浮点数是否等于0时可用if(fabs(x)前面不能加=,即这个数不等于0)

4.区分0、\0、NULL ‘0按照整数进行输出后的结果是48(0的ascii码值)

事实上,运用printf进行输出时,格式为%d时数据都是一样的,均为0,但它们的类型是不同的。

int a = 0; int *a = NULL(类型为void ); (能够被操作符(+-/=等)两端连接起来的数据类型必须是一样的,如果不一样会发生报错或者警告)

int *p=NULL;

推荐if(NULL == p),而不推荐if(p==0)和if(p)和if(p==NULL)

5.按%p格式进行打印是用来打印地址的。

6.如何理解强制类型转换?

在将''123456''转换为整型时需要自己写函数或者使用相应的库函数,会改变内存中的数据。(真实的转化,并不等于强制类型转换)

强制类型转换并不改变内存中任一二进制位,只改变了我们如何去解释该二进制数字,没有改变内存中的数据。(强制类型转换的本质)

7.#define _CRT_SECURE_NO_WARNINGS要放在(头)文件的最前面或者采用#pragma warning(disable:4996) (后者可以不放在最前面)

8.使用getchar()函数时要注意输入缓冲区的存在,getchar()先检索输入缓冲区中有没有字符,如果没有,才会把输入的字符加载进去,尤其注意我们通常输入的间隔符可能就会因为我们代码的不严谨而被加载到缓冲区中。

9.正数的源反补相同,负数的源反补按照上面步骤进行转换!

10.在未声明时不能用其它.c文件里的全局变量,如果要使用,需要用extern(关键字,专门用来声明外部符号的,用法为 extern 类型 变量名,全局变量的作用域是整个工程,生命周期是在程序的运行期间,而static修饰了全局变量后,就使全局变量只能在定义的文件内使用,即使在其它文件中用extern声明后也不能使用)进行声明。

一个全局变量在整个工程的其它文件内部中可以被使用是因为全局变量具有外部链接属性,当一个全局变量被static修饰后,这个变量的外部链接属性就变为了外部链接属性就变成了内部链接属性,使得这个全局变量只能在自己的源文件内使用,其它文件不能使用,给人感觉作用域变小了,生命周期没有变化,存储位置也没有发生变化。

11.内存空间的单位是字节。

12.键盘输入的内容,或者往显示器中打印的内容,全部都是字符,从printf()的返回值即可得出,因为printf的返回值就是输出字符的数目。就像getchar()输入1234,就可以通过printf()进行输出后得到1234,事实上,1234是四个字符。

无论是scanf还是getchar,输入都是以字符形式进行的,不同的是,scanf会进行格式化存储。所以我们把显示器或者键盘叫做字符设备,因为输入的是字符,输出的还是字符。

13.任何C程序,在默认编译好之后,运行时,都会打开三种输入输出流:

stdin:标准输入 FILE* stdin 键盘

stdout:标准输出 FILE* stdout 显示器

stderr:标准错误 FILE*stderr 显示器

14.计算机中的释放空间到底是指的什么?

首先,删除数据并不是把所有的数据清0/1。因为无论是清0还是清1,都是一个写入的过程,如果是清0/1的话,写入和清空应该花费同样多的时间。

计算机中清空数据,只要设置该数据无效即可

15.了解编译和链接。

(1)什么是编译和链接?

编译是为了将函数变量等变成,o二进制的机器码格式,链接是为了将各个独立分开的二进制的函数链接起来形成一个整体的二进制可执行文件。

(2)编译和链接以什么为单位?

编译以文件为单位、链接以工程为单位。

编译器编译时会将所有源文件依次读出来,以每个文件为单位进行编译,因此编译不会考虑其他的文件,显然这样就简化了编译器的设计。

链接的时候实际上是把第一步编译生成的.o文件作为输入,然后将它们链接成一个一个可执行程序,第一步有多少.c文件,编译时就会有多少个.o文件,链接后多个.o文件就会变成一个可执行文件。

(3)三种链接属性:外链接、内链接、无链接

外链接:外链接就是需要的函数与变量可以在外部文件找到,通俗说就是可以被跨文件访问。

内链接:与外链接相反,需要的函数和变量在当前的文件的内部就可以找到,或者说具有内部链接属性的变量只能在文件内部被访问,static修饰全局变量和函数都是内链接的。

无链接:这个符号本身不参与链接,它跟链接没有关系,局部变量(auto、和被static修饰的局部变量)都是无链接的。

(4)函数和全局变量的命名冲突问题

extern修饰的全局函数和全局变量都是外链接的,这些函数和变量在整个程序的所有.c文件中都是可以被访问到的,因此对于外部链接的全局函数和全局变量来说,避免命名冲突是非常重要的,特别是在一个大型的工程项目中,不出现相同的名字是很难做到的。所以在C++中给出了解决方案,就是使用命名空间namespace的方式,通俗点就是给一个变量带上各个级别的前缀,不过C语言中并没有这种方法。但是C语言也有自己的解决方案,就是使用之前的外链接、内链接和无链接这三种属性的方法。

C语言的解决方法是这样的,我们将明显不会再其它C文件中引用的全局变量/函数,使用static修饰使其成为内链接,这样在将来链接时,即使2个.c文件有重名的全局函数/变量,只要其中一个或两个为内链接就不会冲突。当然这种解决方案在一定程度上解决了这个问题,但是并没有从根本上解决问题,因此留下了一些瑕疵,今后我们在用C语言写大型项目时要格外注意命名问题。

(5)运用上面的知识分析运用static修饰全局变量和全局函数

当我们使用static修饰全局变量和全局函数的时候,他们的作用范围就被锁在了本文件内,其它文件在链接时无法使用这些函数和全局变量,这就是由原来的外链接属性变成了内链接属性,同时有限避免了函数和全局变量的命名冲突问题。

16.定义域和生命周期的概念

作用域概念:指的是该变量的可以被正常访问的代码区域(区域性的概念)

生命周期的概念:变量的创建到变量的销毁之间的一个时间段(时间上的概念)

    1. 全局变量,是可以跨文件,被访问的。 2. 全局函数,是可以跨文件,被访问的。

18.源反补的转换方法

原码:直接将二进制按照正负数的形式翻译成二进制就可以。 反码:将原码的符号位不变,其他位依次按位取反就可以得到了。 补码:反码+1就得到补码。

19.在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理; 同 时,加法和减法也可以统一处理(CPU只有加法器)。此外,补码与原码相互转换,其运算过程是相同的,不 需要额外的硬件电路。计算也是用补码进行计算的!

20.printf()和scanf()在底层上也是由getchar和putchar实现的!

getchar函数是逐个字符进行读入,逐个字符进行输出的!如果进行相应的判定,自然也是逐个字符进行判定!下面例子即可证明。

21.在 16 位环境下,short 的长度为 2 个字节,int 也为 2 个字节,long 为 4 个字节。 16 位环境多用于单片机和低级嵌入式系统,在PC和服务器上已经见不到了。 对于 32 位的 Windows、Linux 和 Mac OS,short 的长度为 2 个字节,int 为 4 个字节,long 也为 4 个字节。

无论在32位还是64位的操作系统下。int 始终占据4个字节。

22.C语言语句可分为以下五类:

(1)表达式语句(例如 y=x+3;假设变量y和x均已定义)

(2)函数调用语句(MAX(x,y);假设函数MAX()已经定义)

(3)控制语句(if 、switch等等)

(4)复合语句(把多种语句复合在一起形成的语句)

(5)空语句(分号本身就可以作为一条语句,称为空语句 )

无论上述哪一种语句,都必须以分号结束。

23.有关于getchar和putchar

getchar的作用:

从一个流里读取一个字符,或者从标准输入(键盘)里面获取一个字符。

getchar的返回类型:int(存储文件结束标志-1)

putchar的作用:

写一个字符到流里面或者到标准输出(显示器)中。

putchar的输出类型:字符型

注意:在使用getchar时,如果前面输入了空格到了缓冲区中,不要忘了用getchar吸收缓冲区中的数据!

​​​​​​

ctrl+z就是文件结束标志,使getchar终止

24.C程序地址空间:

程序在编译之前,main()就已经先预定好了分配的空间,即先开辟了空间,在main函数中定义的变量,都是在这段空间中重新申请空间来存放的,即二次开辟。我们把给函数预先分配的这块空间叫做栈帧。

这些函数开辟的那块空间也叫栈帧。在其它局部函数中定义变量也是在相应的栈帧上开辟的。

注意:调用函数时相应的栈帧开辟,调用结束后函数返回,释放栈帧。

#include <stdio.h>
#include <windows.h>
char* show()
{
	char str[] = "hello bit";
	return str;
}
int main()
{
	char* s = show();
	printf("%s\n", s);
	system("pause");
	return 0;
}

释放栈帧之后,里面保存的数据并没有被清除掉,因为计算机意义上的删除只是使数据无效化,无效化指的是这段内存空间是可被覆盖的,换言之,栈帧释放掉后,内存中仍然存储着之前释放栈帧中相应的数据,数据并没有随着栈帧的释放而被清空,在这个函数中show()函数返回的时候,str仍然指向"hello world"这个字符串。

通过监视函数可以看到,在调用printf()函数之前,字符串"hello world"还存在于内存中,当调用完printf()函数之后,字符串"hello world"在内存中就不存在了。前者的原因是计算机并不清空数据,虽然这些数据无效了,但我们依旧可以看到(因为它们仍然存在于内存空间中),后者的原因是因为printf()也是函数,也遵循上面的规则,调用printf()形成新的栈帧,调用结束,释放新的栈帧,新的栈帧会覆盖旧的栈帧结构,进行覆盖之后,原先的hello world字符串自然就不存在了,或者说是被改变了,当然,因为前面旧的栈帧被释放了,即无效了,所以才能被覆盖。

虽然编译器在编译的时候,不会真正的调用我们定义的函数,但编译器会根据我们定义的变量(类型数量),来预估出我们需要的空间大小,进而根据空间大小来开辟相应的栈帧,这也是为什么我们能够通过sizeof()关键字来求某某一变量的大小,因为在编译器在预估空间大小的时候就用到了sizeof()关键字来预估变量的大小。,当然,在这个过程中并没有真正的开辟空间,只有在调用的时候才会在真正的开辟相应的栈帧。

递归的过程实际上就是不断开辟栈帧的过程,如果超过预估的空间大小就会形成所谓的栈溢出的现象。

为什么临时变量具有临时性?

绝大部分变量,都是在栈上开辟的,即在函数内定义的,函数调用,形成栈帧,函数调用结束,栈帧被释放,而通过栈帧结构赖以生存的临时变量自然也就无法继续存在了,也应该被释放掉,即可以被覆盖掉了。总结来说,就是栈帧结构在函数调用完毕后,需要被释放。形象来说,就是皮之不存,毛将焉附?

return 语句不可返回指向占栈内存的指针,因为该内存在函数体结束时将自动被销毁。

问题:return后面的值是如何返回的?

return 返回数据的时候,先将对应的数据放在寄存器中,然后再将寄存器中的数据放到要保存的栈帧的对应变量的空间中,即函数的返回值,通过寄存器的方式,返回给函数调用方,这个过程,大多时候是运用的eax通用寄存器。

问题:如果不接收return返回的数据们这算数据将存放在哪呢?

仍然会放在寄存器中,只不过不再进行接收操作,即将数据返回到调用方。

25.int *a,b;这样写的时候,前面的a是指针类型,后面的b是整型数据类型

当然,我们也可以这样写:int *a=NULL,b = 0;我们并不推荐上述这两种写法,推荐的是下面这种写法:

int *a = NULL;

int b = 0;

26.(1)C语言使用{}的方式非常类似于其它编程语言中begin和end的用法

(2)C语言的三个语言特性:指令、函数、语句。(重要的是区分指令与语句)

指令:预处理器执行的命令叫做指令。所有指令都是以字符#开始的。指令默认只占一行,当然也可以用续行符,每条指令的结尾没有分号或者其它特殊标记。

函数:被命名的可执行代码块。

语句:程序运行时执行的命令。

27.float可以存储很大的数据,但是float变量进行运算时通常比int型变量慢,更重要的是:float型变量所存储的数值往往只是实际数值的一个近似值。

(5)运算符

1.取模运算符%只能用于整数,即两侧只能是整数。

2.运算符优先级

3.&运算符取的永远是最低的那个地址。在C语言中,任何变量&都是最低地址开始。

4.在类型相同的情况下,对指针进行解引用,表示的就是指针所指向的目标。

int a = 10;
int *p = &a;
*p = 20;//*p就是a,但因为是在左端,是左值,所以代表的是那段空间,即把20放到a的那段空间中
int b =*p;//*p就是a,但因为在右端,所以代表的是内容,即20

5.当/两端都是整数的时候,执行的是整数的除法,两端只要由一个是浮点数,执行的就是浮点数的除法。

%操作符的两端都必须是整数。

移位操作符的两端都必须是整数。

6.算术右移与逻辑右移(到底是算术还是逻辑,取决于编译器,我们常见的编译器下都是算术右移)

注意:无论是左移还是右移,都是针对补码进行操作的。

算术右移:右边丢弃,左边补原来的符号位。

逻辑右移:右边丢弃,左边补0。

注意:移位操作的位的数值只能进行正整数的位移,而不能位移负整数的位移,这是C语言标准未定义的。

如何理解丢弃?

基本理解链: > 都是计算,都要在CPU中进行,可是参与移动的变量,是在内存中的。 所以需要先把数据移动到CPU内寄存器中,在进行移动。 那么,在实际移动的过程中,是在寄存器中进行的,即大小固定的单位内。那么,左移右移一定会有位置跑到"外边"的情况。

一段有趣的代码:

unsigned int a = -1;
a = a>>1;//最左端补0,补的符号位取决于数据自身的类型,即a的类型,unsigned int,而与内部保存什么数据无关。

7.sizeof()的()中既可以加变量也可以加类型,在加变量的时候可以不加括号,这也是sizeof是操作符而不是函数的原因,因为函数的()是绝对不可以省略的。

1、关键字

1.switch

(1)switch后面跟整型变量、常量、整型表达式(只能为整型或者字符型)

(2)switch语句中,switch语句本身没有判定和分支功能,case完成的是判定功能,而break完成的则是分支功能。一定不要忘记加上default!必须要带!(代码具有更好的健壮性)。这是作为一个优秀程序员的自我修养!

(3)case后面如果想要定义变量(注意是定义而不是赋值),需要加{},可以在代码块内进行定义,当然,在case语句中变量也只能在代码块内进行定义。在case之后可以执行多条语句,不用{}也可以,但最好加上,或者直接封装为函数。

(4)default可以放在任意位置,可以放在case前,case中间,case最后都没有任何的影响,但习惯上将default放在最后。

(5)case后面不要用return。虽然说编译器不会报错,但我们要搞清楚return 的作用,return的作用是直接退出程序,返回值为0,而break的作用是退出循环或者switch语句,我们要搞清楚这一点,如果你用了return并且成功匹配,那么程序就不会执行switch后面的语句,有兴趣自己试一下。

(6)不要在switch后面的括号内出现bool值。虽然说程序也不会报错,但我们并不推荐这样,因为()里面我们通常得出的是整型数值,bool类型可以正常运行的原因是c99和c90标准下的vs 2019把true默认为是1,把false默认为是0,这些同样是作为一个优秀程序员的自我修养!

(7)case后面要跟真的常量,const修饰的常变量是无法编译通过的。

(8)建议case后面要有好的布局方式,从小到大,以及把最容易匹配到的放在最前面。

(9)switch后面{}内的语句位于case和default外面的无法进行执行,无论是定义变量的语句还是其它如printf()之类的输出语句。

(10)用在switch中的关键字有default、case、break,但要记住continue永远都不能用在switch中,因为continue是用来结束本次循环然后进入下一次循环的,换言之,continue只能用在循环语句中。

2.关键字总览(不需要记,认识即可)

关键字

说明

auto 声明自动变量

short 声明短整型变量或函数

int 声明整型变量或函数

long 声明长整型变量或函数

float 声明浮点型变量或函数

double 声明双精度变量或函数

char 声明字符型变量或函数

(上述6为C语言内置的数据类型,除此之外,一律不是)

struct 声明结构体变量或函数

union 声明共用数据类型

enum 声明枚举类型,定义一组强相关性的常量
为什么使用枚举类型?

1、用常量来描述一组强相关性的事物

2、枚举类型一目了然,并且定义的变量具有自描述性,表达清晰,不需要再作过多解释,当然,我们通过define也能达到这种目的,但不如用enum方便,并且enum也能被编译器进行语法检查。

用法:

enum color
{
    RED,
    YELLO,
    BLACK,
    GREEN,
    BLUE,
};
int main()
{
    enum color c =RED;//定义枚举变量
    //下面为输出结果
    printf("%d\n",RED);//0
    printf("%d\n",YELLO);//1
    printf("%d\n",BLACK);//2
    printf("%d\n",GREEN);//3
    printf("%d\n",BLUE);//4
}

如果我们在定义枚举类型的过程中这样定义:

RED=10:那么在后面进行输出的时候,YELLO以及后面的会逐层加1。

输出之后的结果为:10,11,12,13,14 typedef 用以给数据类型取别名,即类型重命名

typedef struct stu;
{
    char name[20];
    int age;
    char sex;
}stu_t;//此处stu_t即为struct stu结构体数据类型的别名,可以用其来定义结构体变量
int main()
{
    stu_t s;//此处即为定义了一个struct stu结构体变量类型的变量,变量名为s
}

typedef int a[10];//此处的a已经成为了一个数组类型,即int [10]
int main()
{
    a b;//定义了一个数组类型的变量,这个数组中可以存储10个整形数据
}

注意点:必须以分号结尾

注意:

typedef int* int_p;
int main()
{
    int_p a,b;
    //问,此时a和b是什么类型?
    //答案是a和b均为指针类型
    //结论:typedef重命名后形成的是一种全新的数据类型,而不是简单的替换
    //因此也就不存在*和哪个变量先结合的问题
    //注意与int *a,b;进行区分,此时a是指针类型,b是整型类型,因为*先与a变量进行结合
    //问,如果我们想定义两个指针类型但是不想用typedef的方式还如何实现呢?
    int *a,*b;//此时变量a和b的数据类型都是整型指针类型
}

总结:typedef与#define的区别是什么?

#define ptr_t int*
int main()
{
    ptr_t a,b;
    //此时a是指针类型,而b是整型数据类型,与int *a,b;的效果是一模一样的
    return 0;
}

区别:#define秉承的是宏替换的原则,而typedef是类型重命名,然后形成了一个全新的数据类型

下面看下面一段代码

#include<stdio.h>
#define int32 int
type INT32 int
int main()
{
    unsigned IN32 a;//不会报错
    unsigned int32 b;//出现报错
    return 0;
}

结论:我们用typedef定义了一个新类型,此时其前面或者后面就不能再加上别的关键字,因为它本身就是一种独立的数据类型了,就像我们无法定义int char a;一样,即typedef定义得到的新类型必须也只能独立使用。但是#define却是可以的,因为#define知识简单的替换。

问:typedef static int s_int;这样定义到底行不行?

答案是不行,原因是存储类型的关键字再变量定义的时候只能出现一个

const 声明只读变量

(1)const放置的位置,可以放在变量之前,也可以放在变量之后。

例如:const int a = 10 ;也可以写成这样: int const a = 10 ;不过我们更加建议第一种写法,因为这是我们的书写习惯。

(2)const修饰的变量,不可直接被修改,直接修改就是下面这种例子,编译器肯定会报错!

#include<stdo.h>
int main()
{
    const int a = 0 ;
    a = 10 ;//这就是直接修改变量的值
    return 0 ;
}

下面是间接修改的方式(编译器不会报错且可以修改):

#include<stdio.h>
int main()
{
    const int a = 10; 
    int *p = &a;//当然,这个地方会出现警告,警告内容是左右两边类型不一致,因为右边是被const修饰的
    //这样修改即可消除警告:int *p = (int *)&a;(通过强制类型转换使左右两侧类型一致)
    *p = 20;
}

结论:const修饰的变量,并非真正意义上的不可修改的常量。

既然可以被间接修改,那么我们使用const修饰的意义是什么呢?

1、让编译器进行直接修改式检查,编译器在编译代码时,对于后续对const修饰的变量的相关语句进行语法检查,凡是后续对const修饰的变量进行直接修改的语句会进行报错。在某种程度上能够提前发现某些错误。

2、告诉其它程序员(正在改你代码或者正在阅读你代码的)这个变量不要对其进行修改,也属于一种自描述含义。

3、这是一段错误的代码:

#include<stdio.h>
int main()
{
    char *p = "hello";
    *p = 'H';
}

这段字符串并没有保存在栈帧中,而是保存在字符串常量区,此时的p变量是一个临时变量,指向这个字符串的起始地址,此时运行后会直接报错,这种情况下是真正意义上的不可被修改,是操作系统进行限制的,而不是C语言本身限制的。

const修饰变量是在编译期间保证我们的代码不可被修改,而不是在运行期间报错来让我们不可被修改的。

(3)const修饰

1.const修饰变量

2.const修饰数组
同const修饰变量,格式有如下两种:

const int arr[]={1,2,3,4,5}; int const att[]={1,2,3,4,5};

通过上述两种方式来可以定义或者说明一个只读数组。

3.const修饰指针

#include<stdio.h>
int main()
{
    int a  =10;
   1. const int *p =&a;/*     p指向的变量不可以直接被修改,p指针变量所存储的地址值可以被修改(p
   的指向不可以被修改)---- 此处const修饰的是*,而不是int ,因为关键字是不能用来修饰关键字    */
    //*p = 20;(可以正常运行) p = 100;(不可以正常运行)
   2. int const *p = &a;//与第一种的作用完全相同,但还是推荐的第一种
   3. int *const p=&a;/*      p指向的变量可以被修改,p指针变量所存储的地址值不可以被修改(p的指向不可以
   被修改)---此处const修饰的是p     */
   //*p= 20;(不可以正常运行) p = 100;(可以正常运行)
   4. const int *const p =&a;/*     p指向的变量不可以被修改,p指针所存储的地址不可以被修改(p的指向
   不可以被修改)---此处p和*都被修饰       */
   //*p = 20 ;(不可以正常运行) p = 100;(不可以正常运行)
    return 0;
}
int a = 10;
const int *p = &a;
int *q = p;
//上面这段代码会有警告,因为可以用*p修改变量a的值,如果想消除警告需要这样写:int *q = (int*)p;
int a = 10;
int *p = &a;
const int *q = p;
//这段代码不会有警告
//总结:一般我们把一个类型限制不是很严格的变量,赋给一个类型被严格限制的变量,编译器不会报错
//但如果我们把一个类型限制比较严格的变量,赋给一个类型限定不怎么严格的变量,便编译器一般会报错

4.修饰函数的参数

#include<stdio.h>
void show(const int *p)
{
    printf("value:%d\n",*p);
}
int main()
{
    int a = 10;
    int *p = &a;show(p);
    return 0;
}

在show()函数中,p=20;这段语句是无法正常运行的,因为在函数参数中用了const对进行了修饰,就是说当我们不希望某一个变量在函数中不可以被修改时就可以使用const来对某些变量进行修饰。

5.const修饰函数的返回值

#include<stdio.h>
const int*GetVal()
{
    static int a = 10;
    return &a;
}
int main()
{
    const int*p = GetVal();//这个地方必须加上前面的const来使左右的类型一致
    //*p = 100 ;程序会报错
}

const修饰函数的返回值表示不想通过指针的方式来修改函数的内部的变量。

一般内置类型返回,加const毫无意义。一般用const修饰的是指针。

unsigned 声明无符号类型变量或函数

signed 声明有符号类型变量或函数

extern 声明变量是在其他文件中声明
1.声明的时候不能再对变量进行赋值,也不开辟空间,用法为:extern 变量类型 变量名;

2.一般在头文件中进行声明,在声明变量的时候最好带上extern,因为不带的话容易混淆声明和定义;在声明函数的时候可以不带,当然,最好建议是带上。

register 声明寄存器变量

1.什么样的变量,可以采用register呢?

(1)局部的(全局会导致CPU寄存器被长时间占用,影响程序运行效率)

(2)不会被写入的(写入就需要写回内存,后续还要读取检测的话,register的使用将没有意义)

(3)高频被读取的(提高效率)

(4)如果要使用,不要大量使用,因为寄存器数量有限,而且并非每一次声明计算机都将变量存入内存中,程序员做的只是建议

2.register修饰的变量,无法取地址,因为地址是内存上的概念,而寄存器上没有地址的概念

static 声明静态变量

作用:

1、修饰变量

(1)修饰全局变量,该全局变量只能在本文件内被使用。

(2)修饰局部变量,变量的生命周期变成全局周期(和全局变量将一样,但作用域不变)(同时存储的空间发生变化,由原来的栈区到了全局区(静态区))

volatile 说明变量在程序执行中可被隐含地改变

用法:volatile 变量类型 变量名;

最本质的作用:保证内存可见性

不用volatile:在进行多线程时,另一个线程将flag改变了,变为0,但这个进程中的flag在编译器的优化后不会再从内存上进行读取了,将一直死循环,另一个线程中flag将内存中的flag改变后这个线程将不受任何影响。

用了volatile:另一个线程将flag改变后,这个线程中从内存中读取flag的时候读取到的flag的值为0,循环结束。即用了volatile之后CPU将不再对这段代码进行优化,即每次运行while(flag)都将从内存中调用flag。

总结:使用volatile这个关键字,就是不希望被编译器进行优化。达到稳定访问内存的目的。

其它问题:const volatile int a =10;

const是在编译期间起效果,volatile在编译期间主要影响编译器,形成不优化的代码,进而影响运行,故:编译和运行都起效果。

const要求你不要进行写入就可以,volatile意思是你读取的时候,每次都要从内存中读取,两者并不冲突。虽然volatile叫做易变关键字,但这里仅仅是描述它修饰的变量可能会发生变化,要编译器注意,并不是要求对应变量必须发生变化!

void 声明函数无返回值或无参数,声明无类型指针

(1)void类型

void无法用来定义变量,因为不确定该开辟多少空间。在vs中,sizeof(void)的结果是0.在linux中用sizeof(void)结果是1,void无法用来定义变量的原因除了内存空间大小不确定之外,更重要的原因就是vlid类型本身就被编译器解释为空类型,所以编译器就强制的不允许用其来定义变量。

定义变量的本质:开辟空间 而void作为空类型,理论上是不应该开辟空间的,即使开了空间,也仅仅作为一个占位符看待,所以,既然无法开辟空间,那么也就无法作为正常变量使用,既然无法使用,编译器干脆不让他定义变量。同样的,也无法通过void进行强制类型转换。

void修饰函数返回值:1、占位符,让用户明确不需要返回值 2、告知编译器返回值无法接int

void充当函数的形参列表:告知用户or编译器,该函数不需要传参。

结论:如果一个函数没有参数,就在()内加上void;如果一个函数不需要返回值或者没有返回值,就在返回值类型处加上void。

int test1()
{
    return 1;
}
int test2()
{
    return 2;
}
int main()
{
    test1(1,2,3,4);//编译器在vs上不会报错且不会有警告可以正常运行,在linux中一样可以正常编译。
    test2(1,2,3,4);//编译器在vs上不会报错但会有警告可以正常运行,但是在linux中无法正常编译。
    //上述两种情况对于传参时在栈区中开辟内存都没有任何的影响(在vs中)
}

(2)void指针类型(作用:通常用来设计通用接口)

void*是可以定义变量的,因为指针占据的内存大小是明确的。

void可以被任何类型的指针接收,同时void可以接收任意指针类型(常用),在库函数中,系统的接口设计上 ,尽量设计成通用接口,从而就收各种类型的数据,例如memset()函数。

#include<stdio.h>
int main()
{
    void *p =NULL;
    p++;
    p--;
    //上面这两种写法,编译器均会报错,因为不明确++或者--应该跨越的步长,换言之,如果想进行类似的++或者--操作
    //就需要明确指针指向的变量类型所占据的内存空间的大小,之前已经指出,void在vs中所占据的内存空间大小为0.且
    //无法被用来定义变量,所以自然无法用来进行++--的操作,但是在linux中是可以运行的,因为linux中明确了void
    //类型的大小为1。
}

*注意:C语言中无法对void 类型指针变量进行解引用,例如下面这段代码就会报错:

int main()
{
    int a = 10;
    void * p = &a;//因为void类型的指针可以接收任意类型的数据的指针(地址)
    
    *p=20;
    printf("%d",*p);
    //这两段代码都会报错,p是void*类型的,对p解引用,*p就是指向指针所指向的目标或者说类型,对p解引用
    //*p的类型就是void,而编译器无法通过void类型来解析其对应的空间及类型以及里面的数据,所以编译器会
    //报错,在Linux中同样无法正常的运行。因为在vs和Linux中均无法用void来定义变量
    return 0;
}

if 条件语句

else 条件语句否定分支(与 if 连用)

switch 用于开关语句

case 开关语句分支

for 一种循环语句

do 循环语句的循环体

while 循环语句的循环条件

goto 无条件跳转语句

goto语句的用法:

(1)goto 语句可用于跳出深嵌套循环goto语句可以往后跳,也可以往前跳,且一直往前执行

(2)goto只能在函数体内(代码块)跳转,不能跳到函数体外的函数,更不可能跳转到其它的文件内。即goto有局部作用域,需要在同一个栈内。 

(3)goto 语句标号由一个有效地标识符和符号";"组成,其中,标识符的命名规则与变量名称相同,即由字母、数字和下划线组成,且第一个字符必须是字母或下划线。执行goto语句后,程序就会跳转到语句标号处,并执行其后的语句。通常goto语句与if条件语句连用,但是,goto语句在给程序带来灵活性的同时,也会使得使程序结构层次不清,而且不易读,所以要合理运用该语句。

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

结束本次循环之后将进入到条件判定,而不是跳入到循环体的开头,例如while循环体中有一个continue,将跳到()中进行下一次的判定;do while语句中如果do后面的{}中有一个continue,将进入到后面的while()语句的()中进行下一次的判断;for(int i =0;i

break无论是用在switch还是循环中,均只能跳出一层switch或者一层循环。 default 开关语句中的“其他”分支 sizeof 计算数据类型长度 return 子程序返回语句(可以带参数,也可不带参数)(这段代码的具体解析看前面的C程序地址空间后面)

#include <stdio.h>
#include <windows.h>
char* show()
{
	char str[] = "hello bit";
	return str;
}
int main()
{
	char* s = show();
	printf("%s\n", s);//返回的那段空间被覆盖了
	system("pause");
	return 0;
}

关键字总体分类:

数据类型关键字(12个)

char :声明字符型变量或函数

short :声明短整型变量或函数

int : 声明整型变量或函数

long :声明长整型变量或函数

上面这四种类型统称为整型

signed :声明有符号类型变量或函数

unsigned :声明无符号类型变量或函数

float :声明浮点型变量或函数

double :声明双精度变量或函数

struct :声明结构体变量或函数

union :声明共用体(联合)数据类型

enum :声明枚举类型

void :声明函数无返回值或无参数,声明无类型指针 控制语句关键字(12个)

  1. 循环控制(5个)

for :一种循环语句

do :循环语句的循环体

while :循环语句的循环条件

break :跳出当前循环

continue :结束当前循环,开始下一轮循环

  1. 条件语句(3个)

if : 条件语句

else :条件语句否定分支

goto :无条件跳转语句

  1. 开关语句 (3个)

switch :用于开关语句

case :开关语句分支

default :开关语句中的“其他”分支

  1. 返回语句(1个)

return :函数返回语句(可以带参数,也看不带参数) 存储类型关键字(5个)

auto :声明自动变量,一般不使用

extern :声明变量是在其他文件中声明

register :声明寄存器变量

static :声明静态变量

typedef :用以给数据类型取别名

存储关键字,不可以同时出现,也就是说,在一个变量定义的时候,只能有一个。

其他关键字(3个)

const :声明只读变量

sizeof :计算数据类型长度

volatile :说明变量在程序执行中可被隐含地改变

3、函数

1.strlen()的返回类型最好是size_t,这是C语言自己设定的,打印的时候最好用%u,即无符号整型。这样写是最标准的!

2.在定义没有返回值的参数时,可以用void进行定义,甚至在函数中你也可以写入return 返回某一个值,此时编译器并不会报错,甚至调用函数也不会报错,但是只要你在主函数中用某一个变量来接收返回值,必定报错。

如果没有写确定的返回某一个值,只在函数中写了一个return,在return 这条语句之前的语句如果有返回值,就返回这个返回值,如果没有,就会返回随机数,这个结论只适用于局部函数的return,不适用于主函数中的return。

3.在函数调用进行值传递的时候,实参传递给形参,形参其实是实参的一份临时拷贝,所以对形参的修改,不会影响实参,如果想影响的话,可以通过址传递的方式。

4.函数的实参,可以是常量、变量、表达式、函数,但前提是这些要有确定的值。

5.c语言中可以不带返回类型,默认类型为int。但是我们最好要写上!对别人来讲,别人可能会认为我们忘了。

6.为什么在主函数中我们通常会加return 0语句?

main()函数的返回值类型必须是int,这样返回值才能传递给操作系统。

如果main()函数的最后没有写return语句的话,C99规定编译器要自动在生成的目标文件中(如exe文件)加入return 0,表示程序正常退出。不过我们还是应该在main()函数的最后加上return 语句,虽然没有必要,但这是一个好的习惯。

如果main函数的末尾没有return语句,程序仍然能终止。但是,许多编译器会产生一条警告信息(因为函数应该返回一个整数但是没有这么做)

返回值的作用:main()函数的返回值用于说明函数的退出状态。如果返回0,则代表程序正常退出,否则代表程序异常退出。

总结来说:return 0有两个作用:一是使main函数终止(从而结束程序),二是指出main函数的返回值是0。

7.在C语言中,任何函数传参,都会形成临时变量,包括指针变量。

8.fabs()与abs()的区别:

fabs()是对浮点谁求绝对值的,abs()是对整数求绝对值的。

9.函数必须保证先声明后使用,事实上,定义也是一种特殊的声明。

4、数组

1.数组在定义的时候可以选择不初始化,或者放0也可以。

2.if后面虽然说跟单挑语句时可以不带大括号,但我们在使用的时候,必须要带大括号,因为在条件判断后只跟一条语句的情况时相当少的。

3、数组的大小必须是常量,const修饰的常变量不行,因为其本质上还是变量。

4、数组的长度与字符串的长度不一样,数组的长度是指sizeof(arr)/sizeof(arr[0]),而字符串的长度是指用strlen()求出来的。

5、一种错误的数组初始化例子:

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

6、数组名代表整个数组的时候只有两种情况:(1)实参为数组名进行传参(2)sizeof()括号中为数组名

注意:数组传参的时候不许这样写:

int arr[10]={0};
Init_arr(arr[10]);//这样写是错误的,编译器会认为是传了下标为10的数组元素,而不是整个数组,如果要传整个
//数组的话只能这样写:Init_arr(arr);

相比于传参的时候,形参则可以像刚才那样写:

Init(arr[10]);//当然,[]中的10也可以不写

7.对于数组来说,去掉了数组名字即为变量的类型,例如int a[10];int [10]即为数组a的类型。

8.数组传参本质上传的是地址,因为传的是数组名,数组名即为首元素的地址,无论形参是写成数组形式还是指针形式。

9.数组名代表整个数组的时候只有两种情况:

(1)sizeof(数组名)

(2)&(数组名),此时取出来的是整个数组的地址。

除了这两种情况外,数组名均表示首元素的地址。因此,下面三行打印的结果是相同的:

int arr[10]={0};
printf("%p\n",arr);
printf("%p\n",&arr[0]);
printf("%p\n",&arr);

但是一旦进行下列操作就不一样了:

printf("%\p\n",arr+1);//arr是数组首元素的地址,加1即跨过了一个整型元素的大小,即4个字节
printf("%p\n",&arr[0]+1);//同上
printf("%p\n",&arr+1);//数组地址加1,跳过的是一个数组的大小

 5、指针

1.指针变量的名字不带*,我们通常所说的某某是一个指针,其实是一个指针本身,比如int p =&a; p是变量名,而不是p。

指针和指针变量,严格意义上来讲是两种概念,我们在C语言当中使用的严格意义上来讲并不是指针,而是指针变量。

指针:指针就是地址,就是一串代表地址的数字。

内存中的编址不需要开辟空间去存储,它是通过硬件电路来对内存进行编址的。

指针变量:是一个变量,用来保存地址。

2.指针的大小跟系统有关,32位的环境下有32根地址线,指针大小为4个字节,64位的环境下有64根地址线,指针大小为8个字节。(X86:编译出来为32位程序,X64编译出来为64位程序)。不是64位系统的情况下指针一定占8个字 节,关键是要按照64位方式编译

    • 操作符也叫解引用操作符,间接访问操作符。

4.指针可以认为是一种数据类型,也可以认为是定义出来的指针变量。

5.任何一个变量名,在不同的应用场景中,代表不同的含义!

int x ;
x = 10;//此处x指的是x的空间,侧重的是变量的属性,一般叫左值
int y =  x;//此处指的是x的内容,侧重的是数据的属性,一般叫右值
int a =10;
int *p = &a;
p = &b;//p指针变量的空间,代表的是指针变量的左值
q = p;//指的是p的内容,取的是p内部保存的地址数据,即右值

指针变量和普通变量在变量属性上,没有任何区别,只是保存的内容有些特别而已。

6.指针存在的价值,就是使CPU在进行寻址的时候,加快在内存中进行定位的效率。

int a = 10;
(int*) p = a;//不可以这样写,因为这样写编译器会认为这是强制类型转换,而强制类型准换的前提是变量已经存在
//已经存在的意思是已经在内存中定义好了,有了自己的空间
//补充:强制类型转换的模板是:(类型)变量名,而不是:类型(变量名)

8.内存被划分为一个有一个小的内存单元,一个内存单元即一个地址编号表的就是一个字节。

9.指针类型的意义是什么?

1.指针类型决定了解引用的时候一次能访问几个字节。如果是整型类型的指针,解引用能够访问4个字节,如果是字符类型的指针,解引用能够访问1个字节。

2.指针类型决定了指针向前或者向后走一步,走多大距离(单位是字节)

10.野指针

(1)概念

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

(2)成因

1.指针未初始化

#include <stdio.h>
int main()
{
    int* p;//局部变量指针未初始化,默认为随机值
    *p = 20;
    return 0;
}

指针变量的初始化有两种形式,要么指向空指针,即NULL,要么就执行某个确定的变量(已将在内存空间中开辟内存的变量)或者指向字符串常量。(此种方法也可以防止出现野指针)

问:为什么不能对真常量和表达式取地址?

因为常量和表达式值存在于代码中,不存在于堆栈中,因此没有地址。

但是在这个地方有个例外,就是字符串常量,我们可以对字符串常量取地址,并存放到指针变量中,且能进行输出打印。字符串常量和数组名一样,也是被编译器当成指针来对待的。它的值就是字符串的基地址。

2.指针越界访问

即跨过数组访问某一部分空间取访问未知的空间,我们并知道这段空间是否可以被使用,也不知道是否可以被访问。

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	int i = 0;
	for (i = 0; i <= 10; i++)
	{
		*(arr + i) = i;
		p++;
	}
	return 0;
}

此处就访问了数组外的内存。

3.指针指向的空间被释放

#include<stdio.h>
int* test()
{
	int a = 10;
	return &a;
}
int main()
{
	int* p = test();
	printf("%d\n", *p);//可以正常打印
	printf("%d\n", *p);//无法正常打印,此处涉及到栈帧,当然,编译器不会报错
	return 0;
}

(3)如何避免野指针?

1.指针初始化

2.小心指针越界

3.指针指向空间释放将指针置为NULL

4.避免返回局部变量的值

5.指针使用之前检查有效性

11.指针-指针

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	printf("%d\n", &arr[9] - &arr[0]);
	printf("%d\n", &arr[0] - &arr[9]);
	printf("%d\n", (char*)(&arr[9]) - (char*)(&arr[0]));
	printf("%d\n", (char*)(&arr[9]) - &arr[0]);
	printf("%d\n", &arr[9] - (char *)&arr[0]);
	return 0;
}

12.指针的关系运算

for (vp = &values[N_VALUES]; vp > &values[0];)
{
    *--vp = 0;
}
//修改后
for (vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--)
{
    *vp = 0;
}

注意:实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证 它可行。

标准规定: 允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与 指向第一个元素之前的那个内存位置的指针进行比较。

13.如何理解二级指针或者多级指针

int a = 0;

int *pa = &a;

int* *ppa = &pa;

int** *pppa = &ppa;

上述下划线代表了指针指向的类型,后面的*表明是该变量是一个指针变量。

6、结构体

1.定义结构体时,结构体中[]内的数字不能省略,必须明确指定,因为初始化才不用写。

2.给结构体变量赋初值时可以直接这样struct Stu zhangsan = {0};后面可以进行再赋值,不然打印出来的数据都是0。

3、结构体只能整体初始化,不能整体赋值,但是可以分别赋值,例如下面这段代码:

struct student
{
    char name[];
    int age;
    char sex;
    char addr[];
}
struct student x;//
x = {"zhangsan",19,'m',"china"};//编译器会报错
x.age = 18;//编译器不会报错
//字符串数组也有类似结构体的特征,只能被整体初始化,不能被整体赋值
x.name[]="zhangsan";//编译器会报错,只能采取下方的方式来写
strcpy(x.name,"zhangsan");//编译器可以正常运行通过

4、为什么结构体会有两种访问方式?

因为结构体在定义的地方访问,用.更方便一些,但是在传参的时候,用->更方便一些。

5、vs当中C语言程序要求结构体和联合体至少有一个成员,如果没有成员的话用sizeof求其大小编译器就会报错。

6.在linux的gcc编译器下可以定义空结构体变量,并用sizeof求其所占的内存大小与空结构体类型的大小一样,均为0。

7.柔性数组

定义方式:

struct stu
{
    int num;
    int a[0];//定义柔性数组的时候只能在结构体内定义,但最好不要把结构体的第一个元素定义为柔性数组
    //一般把柔性数组放到最后一个元素的位置上,就像在linux中就必须放到最后一个元素的位置上,即前面必须有有效元素
    //定义柔性数组时候的0也可以不写
    //并不是所有编译器都支持柔性数组,柔性数组的概念是C99标准中的
    //一般我们定义的柔性数组本身并不占据结构体内存的大小
}
int main()
{
    struct data *p=malloc(sizeof(struct data)+sizeof(int)*10);
    //开辟的内存空间的起始地址即为柔性数组的起始地址,开辟空间是在num后面开辟的
    p->num = 10;
    //柔性数组的使用
    for(int i = 0;i<p->num;i++)
    {
        p->arr[i] = i;
    }
    free(p);//将申请的内存空间释放掉
}

8.联合体

union un
{
    int a;
    char b;
}
int main()
{
    union un x;
    printf("%p\n",&x);
    printf("%p\n",&(x.a));
    //联合体的地址与联合体内定义的最大变量的地址是一样的
    printf("%p\n",&(x.b));
    //联合体内定义的较小的变量的地址与联合体的地址是一致的,即低地址处
    //上述三个的打印地址是完全一样的
    //联合体内,所有成员的起始地址都是一样的---b永远在a的低地址处
}

如果是第一种存储方案,即小端存储,if()后面的语句就会执行;如果是第二种存储方案,即大端存储,else后面的语句就会执行。通过该程序就可以判断计算机的存储方式。

联合体的内存对齐:

union un
{
    int a;
    char b[5];
}//sizeof(union un)==8;

按照道理来说,开辟五个空间即可实现我们的需求,但是因为内存对齐现象:联合体开辟内存空间的大小,必须能够整除联合体内的任何一个元素的大小,即5无法整除4,其中对于字符数组来说按照一个字符来算(只看int 或者char这些类型),所以开辟的内存空间大小为8,能够整除1和4。

7、数据的存储

1.整型所占的内存空间的大小并不都是四个字节

8、字符串

1.’\0'是字符串的结束标志,在用sizeof进行计算时也算一个字节,strlen()则不会把它计入(因为strlen()和printf函数在遇到'\0'就会自动停止了。(sizeof计算的的是变量所占内存空间的大小,而strlen()计算的是从变量的起始地址开始,到'\0'结束标志为止的字符的数目)。

2.字符串定义的两种方式:

char arr1[] = "bit";([]内可以加数字限定,但不要忘记'\0'也占用一个字符(’\0'编译器会自己加上)    char arr2[4] = {'b', 'i', 't'};    char arr3[] = {'b', 'i', 't', '\0'};(2和3两种定义方式要么在末尾加上’\0'为结束标志,要么加上字符串长度限定符(加限定符不要忘了还有'\0'为字符串结束标志,也占用一个字节长度,这个结束标志在限定了字符串长度后编译器会自己加上)

3.C语言有字符串。但是C语言没有字符串类型。

4.C语言中由空字符串,但是没有空字符,即在C语言程序中

'';//无法编译通过
"";//可以正常编译通过
sizeof(1);//4,因为是一个整型
sizeof("1");//2,是一个字符还有一个'\0'结束标志
sizeof('1');//4
sizeof("");//1,即字符串的结束标志'\0'
//C99标准的规定,'a'叫做整型字符常量,被看作是int型,而在C++中则是一个字节
char c = '1';//4个字节的数据写到了一个字节的空间里,即发生了截断
sizeof(c);//1

6.字符的截断问题

char c = 'abcd';//在向字符类型存储的时候发生了截断
//C99标准的规定,'a'叫做整型字符常量,被看作是int型,所以可以存放4个字符
printf("%c",c);//输出结果为d,即最后一个字符,数据发生截断的时候,永远是从最低的字节开始拿的
//这是一个大小端问题

7.'\0'与0

printf("%d\n",'\0');//打印在屏幕上的是0
printf("%d",'0');//打印在屏幕上的是48

8.相邻字符串具有自动连接特性

两个相邻的字符串计算机会当成一个字符串来处理。

9、符号

1.注释符号

基本注释注意事项:

(1)

#include <stdio.h>
#include <windows.h>
int main()
{
	int /* */ i; //正确
	char* s = "abcdefgh //hijklmn"; //正确
	//Is it a\
	valid comment? //正确
	in/* */t j; //报错
	system("pause");
	return 0;
}

注意:注释被替换是在预处理阶段实现的,注释被替换,本质是替换成空格,上述报错的那一句本质上应该是 in t i;编译器自然会报错,报错是在预处理阶段进行语法检查时出错的,出现了语法错误。

(2)

/*这是*/#/*一条*/define/*合法的*/ID/*预处理*/replacement/*指*/list/*令*/
//这段代码指的就是用replacement list替换ID
 /*这是*/int/*一条*/abcd/*合法的*/efg/*预处理*/replacement/*指*/list/*令*/

上述两段代码都能编译通过,这说明# 和 define之间可以带空格。

//是C++风格的注释,而/* */则是C语言风格的注释,前者可以一次写多个,不过从第一个//往后就都是注释的内容,

(3)

注意:/* */不能嵌套注释。/总是和离它最近的/进行匹配。例如:

/*
/*
*/
*/

最终第一行和第三行中的*/进行匹配,剩下最后一个*/。

(4)注意下面这段代码

int x = 10 ;
int y = 10;
int z = 5;
int *p = &z;
y = x/*p;

这种代码一定要注意,/*容易被编译器认为是注释,所以会报错。

解决方案有两种:

1.y = x / *p;即在/后面加一个空格,不要让/*连在一起。

2.y = x/(*p);(推荐用第二种)。

(5)条件编译

#include <stdio.h>
#include <windows.h>
#define MONEY 1(只有定义了前面的宏,ifdef到endif中间的这段代码才能够正常运行,未定义则跳过)
int main()
{
#ifdef MONEY
	printf("for test1\n"); //test1
	printf("for test2\n"); //test2
#endif
	system("pause");
	return 0;
}

2.也可以通过if(0)来进行注释,但并不推荐,严重不推荐。

2.接续符和转义符

(1)\的两种功能

1.续行

用法:

#include<stdio.h>
int main()
{
    int a = 0;
    int b = 0;
    int c = 0;
    if(0==a&&\
    0==b&&\
    0==c)//代码等同于if(0==a&&0==b&&0==c)
    return 0;
}

注意:可以在续行符前面加空格,但是不能在续行符后面加空格,即续行符后面不要加任何符号,\后直接回车到下一行

2.转义

1.字面转特殊:比如\n代表回车换行符

2.特殊转字面:比如我们想打印",只能这样写:printf(""");

区别\n与\r

回车代表的是光标回到当前行的最开始的位置,换行代表的是光标移动到下一行,是两个概念。

回车:\r

换行:\n(当然,在诸多编程语言中,实际上\n身兼两个功能,即回车与换行)

3.异或(^):遵守结合律、交换律。

4.整形提升

(1)什么是整型提升?

C的整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型 提升。

(2)整型提升的意义: 表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。

无论任何位运算符,目标都是要计算机进行计算的,而计算机中只有CPU具有运算能力(先这样简单理解),但计算的数据,都在内存中。故,计算之前(无论任何运算),都必须将数据从内存拿到CPU中,拿到CPU哪里呢?毫无疑问,在CPU 寄存器中。而寄存器本身,随着计算机位数的不同,寄存器的位数也不同。一般,在32位下,寄存器的位数是32位。可是,你的char类型数据,只有8比特位。读到寄存器中,只能填补低8位。总结来说,在计算机中进行的运算的数据都是以整型进行的,而且在编译期间sizeof的结果就已经出来了,而不是在运行期间才的出来的。

如何进行整型提升的呢?

整形提升是按照变量的数据类型的符号位来提升的。

//负数的整形提升
char c1 = -1;
//变量c1的二进制位(补码)中只有8个比特位:
//1111111
//因为 char 为有符号的 char
//所以整形提升的时候,高位补充符号位,即为1
//提升之后的结果是:
//11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
//变量c2的二进制位(补码)中只有8个比特位:
//00000001
//因为 char 为有符号的 char
//所以整形提升的时候,高位补充符号位,即为0
//提升之后的结果是:
//00000000000000000000000000000001
//无符号整形提升,高位补0

sizeof实例:

#include<stdio.h>
int main()
{
	char c = 0;
	printf("%d\n", sizeof(c));//1
	printf("%d\n", sizeof(~c));//4
	printf("%d\n", sizeof(c<<1));//4
	printf("%d\n", sizeof(c>>1));//4
   printf("%d\n",sizeof(+c));//4
   printf("%d\n",sizeof(-c));//4
   //上述这些例子均参与了运算,即发生了整型提升,所以结果会改变
   //即:只要short和short类型的变量参与了运算,就会发生整型提升
   printf("%d\n",sizeof(!c));//vs下是1,Linux下是4,特殊情况
	return 0;
}
int main()
{
    int a = 10;
    int b = 20;
    a + b;//表达式有两个属性:值属性、类型属性
    //30就是值属性
    //int即为类型属性,不需要计算即可得出,计算机能够推导出来
    //上面代码块中的sizeof()即利用的就是表达式的类型属性,并没有进行计算
}

结论:sizeof()括号内部的表达式是不参与运算的,计算机只是自动推导出了它的类型,进而得出了答案,且这个过程并不是在运行阶段进行的,在编译阶段就已经进行了(下面例子即可证明)

int main()
{
    short s = 20;
    int a = 5;
    printf("%d\n",sizeof(s = a + 4));//打印结果为2,发生了整型提升,但在存入s的过程中发生了截断
    //即最终的类型为s,最终结果放到谁里面去谁说了算
    printf("%d\n",s);//打印结果为20,可以得出:sizeof内部的表达式不会真正参与运算
    //因为sizeof在编译阶段就已经执行了,即在编译完成后sizeof(s = a + 4)就已经被2所替换
    //所以在运行期间自然不会再运行s = a + 4这段代码,因此s也不会被改变
    return 0;
}

计算实例:(计算机运算的过程)

int main()
{
    char a = 5;
    //5--00000000000000000000000000000101整型5的补码
    //5->char a中发生截断,即最终存入a中的数据为00000101
    char b = 126;
    //126--00000000000000000000000001111110
    //126->char b中发生截断,即最终存入的数据为01111110
    char c = a + b;
    //当a和b相加的时候,a和b都是char类型
    //表达式计算的时候就要发生整型提升
    //a--00000101(最高位为符号位,a的符号位为0,所以前面全部补0)
    //a--00000000000000000000000000000101
    //b--01111110(最高位为符号位,b的符号位为0,所以前面全部补0)
    //b--00000000000000000000000001111110
    //a+b--00000000000000000000000010000011
    //将a+b的值存入char c中,发生截断,此时c--10000011(补码)
    printf("%d\n",c);//问:此时c的输出结果为多少?
    //以整数形式打印,此时又会发生整型提升
    //c--11111111111111111111111110000011(补码)
    //c--10000000000000000000000001111100(反码)
    //c--10000000000000000000000001111101(原码)--(-125)
    //当在一个表达式中看到变量所占内存的大小没有达到整型所占的四个字节大小时,首先要发生提升,提升成
    //整型,在上面这个表达式中a和b时char类型,为1个字节,首先提升成整型
    return 0;
}

5.算术转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类 型,否则操作就无法进行。下面的层次体系称为寻常算术转换。

long double double float unsigned long int long int unsigned int int

如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运 算。(即下面的类型向上转换) 注意:算术转换要合理,要不然会有一些潜在的问题。

6.下标引用操作符:[ ]

1.实际上编译器在进行处理的时候还是把它当成指针来处理的。例如:arr[7]---------(arr+7)-----(7+arr)------7[arr]

arr[7]也可以写成7[arr]。(根据上面的推导得来的,利用了加法的结合律)

7.操作符的属性

复杂表达式的求值有三个影响的因素。 1. 操作符的优先级 2. 操作符的结合性 3. 是否控制求值顺序。 两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。

8.~按位取反的时候符号位也一样取反!

9.>>

unsigned int d = -1;
printf("%d\n",d >> 1);

这种在情况下前面是否补符号位要根据变量的数据类型来看,此处因为前面类型是unsigned int,所以计算机无论是算术右移还是逻辑右移前面都是补0,因为d是无符号类型,即它的首位不会被看成符号位,即算术右移还是逻辑右移的判定依据是变量的数据类型,和变量的内容无关。

10.前置与后置在没有变量使用这个++的变量的时候,这两者没有任何的区别。

11.贪心算法

C 语言有这样一个规则:每一个符号应该包含尽可能多的字符。也就是说,编译器将程 序分解成符号的方法是,从左到右一个一个字符地读入,如果该字符可能组成一个符号, 那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组 成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串 已不再可能组成一个有意义的符号。这个处理的策略被称为“贪心法”。需要注意到是,除 了字符串与字符常量,符号的中间不能嵌有空白(空格、制表符、换行符等)。比如:==是 单个符号,而= =是两个等号。

int main()
{
    int i = 0;
    printf("%d\n",a+++10);//由于遵守贪心算法:所以实际上的表达式应为:a++ +10
    return 0;
}

12.取整问题

1.0向取整(C语言默认的取整方案)

#include<stdio.h>
#include<windows.h>
int main()
{
	//本质是向0取整
   //trunc()函数也有这种作用,不过返回值是浮点数,而且必须引用math.h头文件
	int i = -2.9;
	int j = 2.9;
	printf("%d\n", i); //结果是:-2
	printf("%d\n", j); //结果是:2
	system("pause");
	return 0;
}

2.地板取整(向-无穷的方向取整)

要使用floor函数,返回值同样为double,且要引用math.h头文件

3.天花板取整(向+无穷的方向取整)

要使用ceil函数,返回值同样为double,且要引用math.h头文件。

4.四舍五入取整

需要引用round函数,返回值同样为double,且要引用math.h头文件。

13.取模问题

1.余数的定义:如果a和d是两个自然数,d非零,可以证明存在两个唯一的整数 q 和 r,满足 a = q*d + r , q 为整数,且0 ≤ |r|< |d|。其中,q 被称为商,r 被称为余数。

2.两种余数

由定义可知:-10%3=-1------>-10/3=-3------->3*(-3)+(-1)=(-10)(C语言中是这样的)

-10%3=2------->-10/3=-4------->4*(-3)+ 2=(-10)(python环境中是这样的)

解释C: -10 = (-3) * 3 + (-1)(负余数) 解释Python:-10 = (?)* 3 + 2,其中,可以推到出来,'?'必须是-4,即-10 = (-4)* 3 + 2,才能满足定义。(正余数)

所以,在不同语言,同一个计算表达式,负数“取模”结果是不同的。我们可以称之为分别叫做正余数 和 负余数

3.是什么决定了这种现象?

由上面的例子可以看出,具体余数r的大小,本质是取决于商q的。 而商,又取决谁呢?取决于除法计算的时候,取整规则。C语言中默认是0向取整,python中默认是-无穷的方向取整。

14.取余和取模一样吗?

本质 1 取整: 取余:尽可能让商,进行向0取整。 取模:尽可能让商,向-∞方向取整。 故: C中%,本质其实是取余。 Python中%,本质其实是取模。

理解链: 对任何一个大于0的数,对其进行0向取整和-∞取整,取整方向是一致的。故取模等价于取余 对任何一个小于0的数,对其进行0向取整和-∞取整,取整方向是相反的。故取模不等价于取余

同符号数据相除,得到的商,一定是正数,即大于0! 故,在对其商进行取整的时候,取模等价于取余。 本质 2 符号:

1.同符号取整 参与取余的两个数据,如果同符号,取模等价于取余

2.不同符号取整

#include<stdio.h>
#include <windows.h>
int main()
{
	printf("%d\n", -10 / 3); //结果:-3
	printf("%d\n\n", -10 % 3); //结果:-1 为什么? -10=(-3)*3+(-1)
	printf("%d\n", 10 / -3); //结果:-3
	printf("%d\n\n", 10 % -3); //结果:1 为什么?10=(-3)*(-3)+1
	system("pause");
	return 0;
}

已知商套定义比较容易算出余数。

10、动态内存管理

11、文件操作

12、程序的编译(预处理)

1.字符串宏常量

注意:

(1)要加双引号

(2)对某些可能构成转义的加上\,以防编译器误认为是转义字符

(3)如果一行放不下要用续行符

例如:

#define PATH "C:\Users\鹿九丸\Desktop\学习\

资料\c语言资料\c语言资料"

2.程序的翻译

(1)是什么?

将文本式的代码翻译成为二进制。

(2)为什么?

计算机只认识二进制

(3)如何翻译?

1.预处理-E:头文件展开,去注释,宏替换,条件编译(预处理结束后仍是C语言文件)

预处理具体定义:首先程序会被送交给预处理器。预处理器执行以#靠头的命令(通常称为指令)。预处理器有点类似与编辑器,它可以给程序添加内容,也可以对程序进行修改。

注意:先执行去注释,再进行宏替换。例如下面这段代码:

#include<stdio.h>
#define BSC //
int main()
{
	BSC printf("hello world\n");
	return 0;
}

下面我们来分析一下这两个过程:先去注释,BSC后面的//去掉了,然后再进行宏替换的时候,此时宏替换的意思是以空来替换BSC,此时代码就编成了下面这段代码:

int main()
{
    printf("hello world\n");//BSC被空替换
    return 0;
}

2.编译-S:将干净的C语言,编译成为汇编语言,即机器指令。注意:此时的程序还是不可以运行的!

3.汇编-C:将汇编语言转化成为可重定向的二进制文件(目标文件){可重定向:可被链接} 注意:此时程序仍然不可被执行,因为尚未链接

4.链接:将目标二进制文件与相关库链接,形成可执行程序(两种链接方式:静态链接(静态库)和动态链接(动态库))

为什么要有库?

(1)提高效率

(2)程序有更好的健壮性

3.用宏定义充当注释符号

由于去注释在宏定义之前,所以无法实现。

(1)宏无法替换字符串中的内容。

(2)“#define M 10"无法当成宏定义,也无法正常运行

(3)下面这种定义是可以的

#include <stdio.h>
//该宏最大的特征是,替换的不是单个变量/符号/表达式,而是多行代码
#define INIT_VALUE(a,b)\
a = 0;\
b = 0;
int main()
{
	int a = 100;
	int b = 200;
	printf("before: %d, %d\n", a, b);
	INIT_VALUE(a, b);//替换之后代码为:INIT_VALUE(a,b);;
	printf("after: %d, %d\n", a, b);
	return 0;
}

但是这种代码也 存在着一定的隐患,因为替换后有两个分号,下面这种情况会出现问题:

#include <stdio.h>
#define INIT_VALUE(a,b)\
a = 0;\
b = 0;
int main()
{
	int flag = 0;
	scanf("%d", &flag);
	int a = 100;
	int b = 200;
	printf("before: %d, %d\n", a, b);
	if (flag)
		INIT_VALUE(a, b);
	else
		printf("error!\n");
	printf("after: %d, %d\n", a, b);
	return 0;
}

注意:这段代码在预处理阶段不会报错,但是在编译阶段会报错。

很明显,此处是因为两个分号报错的,解决方法为:在if后面的语句加一个{}形成语句块。当然,这种修改方法并不够好,下面是更好的解决方法:

#include <stdio.h>
#define INIT_VALUE(a,b)\
do{a = 0; \
b = 0; }while(0)
int main()
{
	int flag = 0;
	scanf("%d", &flag);
	int a = 100;
	int b = 200;
	printf("before: %d, %d\n", a, b);
	if (flag)
		INIT_VALUE(a, b);
	else
		printf("error!\n");
	printf("after: %d, %d\n", a, b);
	return 0;
}

5.有关宏定义时的空格问题

(1)#和define之间可以存在空格,但我们并不推荐这样写。

(2)宏调用时也可以带空格,例如下面这段代码是没有问题的(依旧不推荐这样写)

#include<stdio.h>
#define FUN(x) x+1
int main()
{
    int a = 10;
    FUN (a);
    printf("%d\n",a);
}

6.宏定义的位置以及其作用域

(1)宏并非只能在main()函数的上面定义,可以在程序的任何位置进行定义,与是否在函数体内与函数体外定义没有任何的关系。

(2)宏的作用范围:从定义处开始,往后直到#undef(这个位置是绝对位置,而例如假如在某一非主函数定义的话,无论这个函数是否调用,在这个函数中进行的宏定义在主函数中均能在主函数中使用,前提是这个函数定义的位置处在主函数的上方。

(3)看下面着段代码

#include <stdio.h>
int main()
{
#define X 3
#define Y X*2
#undef X
#define X 2
	int z = Y;
	printf("%d\n", z);
	return 0;
}

最后的输出结果是4,而并非6,是因为最后x被宏定义成了2。

(4)再看下面这段代码

#include<stdio.h>
#define M 10
void fun()
{
	printf("%d\n", M);
}
int main()
{
#undef M
	fun();
	return 0;
}

仍然能在屏幕上正常输出10,原因是预处在函数编译之前。

7.条件编译

条件编译:代码裁剪的工具

本质:如果条件满足,就保留后面的这段语句(块),如果条件不满足,就直接去掉后面的这段语句。

为什么要有条件编译?

通过裁剪代码,快速实现某种目的(版本维护(free,收费),功能裁剪

使用条件编译的好处有哪些?

  1. 可以只保留当前最需要的代码逻辑,其他去掉。可以减少生成的代码大小 2. 可以写出跨平台的代码,让一个具体的业务,在不同平台编译的时候,可以有同样的表现

1.宏是否被定义vs宏为真还是为假

#define DEBUG  //宏被定义
#define DEBUG 1 //也叫宏被定义,并且1为真
#define DEBUG 0 //也叫宏被定义,并且0为假
//后面两个都已经默认了宏已经被定义

#ifdef:判定的是宏是否被定义,如果被定义了,后面直到#endif的内容会保留,反之就不会保留。

#ifndef:判定的是宏是否没有被定义,如果没有被定义,后面直到#endif的内容会保留,反之就不会保留。

#ifdef DEBUG
printf("hello debug\n");
#else
printf("hello unknow\n");
endif

上面这段内容的意思是,如果DEBUG被定义了,就在屏幕上输出hello debug,如果没定义DEBUG,就在屏幕上输出hello unknow。

2.#if的相关语句

双分支情况

#include<stdio.h>
#define C 1
int main()
{
#if C
	printf("hello C\n");
#else
	printf("hello other\n");
#endif
	return 0;
}

此处的if是判断宏存在并且为真还是假,如果为真,就执行后面的语句,如果为假,就执行else后面的语句。

如果未定义,就默认为假,如果定义为空,程序就会报错。

当然,我们可以通过右击项目项目名称,这是在vs中的,点击属性,点到C/C++处,点击预处理器,在预处理器定义后面自己进行宏定义,但要在原先的宏定义后面加个分号,然后再加自己的宏定义。在linux中可以通过命令行来进行。

多分支情况

#include<stdio.h>
#define VERSION 1
int main()
{
#if VERSION== 1
	printf("version 1\n");
#elif VERSION== 2
	printf("version 2\n");
#elif VERSION== 3
	printf("version 3\n");
#else
	printf("version other\n");
#endif
	return 0;
}

此时表达的就相当于if多分支语句。当然,==也可以用一些其他的操作符,比如

如何用#if来替换#ifdef?

#include<stdio.h>
#define VERSION 1
int main()
{
#if defined(VERSION)//如果向来模拟ifndef,在defined前面加个!即可
	printf("version\n");
#else
	printf("version other\n");
#endif
	return 0;
}

此处表示的意思就是如果VERSION被定义了,就会输出version,如果没有被定义就输出version other。

结论:#ifdef->#if defined()

#ifndef->#if !defined()

注意:上面无论以那种写法,都必须以#endif结束。

较为复杂的条件编译情况

#include<stdio.h>
#define C
#define CPP
int main()
{
#if defined(C) && defined(CPP)
	printf("hello c&cpp\n");
#else
	printf("hello other\n");
#endif
	return 0;
}

检测当C和CPP是否都被定义 ,如果都被定义,就输出hello c&cpp,否则就输出hello other。当然我们可以在两个判定条件左右可以加个()。

结论:条件编译时可以多条件进行级联的。

注意:条件编译也是可以嵌套的。

8.文件包含

什么叫做头文件展开?

把头文件的内容拷贝(也包括一些去注释、条件编译的选项及操作)到目标源文件。

为何所有头文件,都推荐写入下面代码?本质是为什么? #ifndef XXX #define XXX //TODO #endif

本质:为了防止头文件被重复包含(通过条件编译的形式实现)

9.其他的一些宏命令

(1)#error

在编译阶段编译器就报错,而不是在运行阶段。

结论:核心作用是可以进行自定义编译报错。

(2)#line 预处理

FILE:文件名 LINE:行号

(3)#pragma 预处理

#pragma message()作用:可以用来进行对代码中特定的符号(比如其他宏定义)进行是否存在进行编译时消息提醒

与error的区别:

#error会中止程序使程序无法运行,而#pragma会使程序正常运行,从作用上进行分析:

#error会使程序终止,并出现错误提示,而#pragma只会出现提示

10.#运算符

注意:括号内不可以是变量,里面的符号无论是什么,都被当成是字符串。(因为预处理是在最开始的过程,包括往开辟内存空间,往内存空间中存储数据都在运行期间进行的。

##运算符

作用:将##相连的两个符号,连接成为一个符号

注意:替换之后,只是一个符号,而并非是一个字符串

那么,符号和字符串的区别是什么呢?

char *p = "abcde";

p就是符号,起标识作用

abcde就是字符串,编译器常常认为变量名就是符号,编译器对其进行文本处理即可

相关文章