C语言笔记(1.2版本,目前22000字)—-未完待续

目录

前言

0、基础常识

(1)进制

(2)变量与常量

(3)内存

(4)其它零零碎碎的点

(5)运算符

1、关键字

1.switch

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

2、分支和循环

3、函数

4、数组

5、指针

6、结构体

7、数据的存储

8、字符串

9、符号

10、动态内存管理

11、文件操作

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


前言

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

0、基础常识

(1)进制

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

(2)变量与常量

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

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

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

枚举常量的定义方式

enum Sex

{ MALE, FEMALE, SECRET };

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

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

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

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、、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.定义域和生命周期的概念

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

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

17. 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("%sn", 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;

(5)运算符

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

2.运算符优先级

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

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

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

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("%dn",RED);//0
    printf("%dn",YELLO);//1
    printf("%dn",BLACK);//2
    printf("%dn",GREEN);//3
    printf("%dn",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:%dn",*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<10;i++)循环语句{}中有了continue后将跳转到i++处。

break无论是用在switch还是循环中,均只能跳出一层switch或者一层循环。

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

sizeof 计算数据类型长度

return 子程序返回语句(可以带参数,也可不带参数)

#include <stdio.h>
#include <windows.h>
char* show()
{
	char str[] = "hello bit";
	return str;
}
int main()
{
	char* s = show();
	printf("%sn", 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 :结束当前循环,开始下一轮循环

2. 条件语句(3个)

if : 条件语句

else :条件语句否定分支

goto :无条件跳转语句

3. 开关语句 (3个)

switch :用于开关语句

case :开关语句分支

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

4. 返回语句(1个)

return :函数返回语句(可以带参数,也看不带参数)

存储类型关键字(5个)

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

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

register :声明寄存器变量

static :声明静态变量

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

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

其他关键字(3个)

const :声明只读变量

sizeof :计算数据类型长度

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

2、分支和循环

1.在使用if条件判断时需要注意一个小小的点,就是很多表达式和函数往往是由自己的返回值的,例如if(a=0)这样写的话,后面的代码块就不会执行,因为()内的返回值为0,所以不会执行。

2.if进行条件判断时,把常量放在左边

3.else匹配总是采取就近原则,与上面最近的一个if进行匹配。(所以带花括号)

if里面得到的最好是一个bool值,不要用赋值表达式之类的。

所有的else if语句最好是由else结束。

4.do while语句建议书写格式像这样:

do {

}while();这个语句的循环变量初始化在do之前!

5.while()执行的顺序和if()执行的顺序是一样的,不同的是执行完之后会返回到while()语句进行重新执行,前面的

定义并初始化是循环田间初始化,()里的内容是循环条件判定,{}里的内容是循环条件更新。for()循环使将条件初始化,条件判定,条件更新放在一个()内,语法结构比较紧凑。

6、在循环语句中最好是将循环次数较多的,放在最内层,循环次数最少的,放在最外层。这样可以使代码执行效率更高,就是使编译器跳转循环的次数尽可能地少。计算机在执行程序的时候,计算机内部,CPU中有很多数据使会缓存起来的,如果跨代码跳转过多的话(即外循环多,内循环少的情况下,会频繁的进行循环之间的跳转),就会导致缓存的数据不停的处于缓存和过期两个过程之中,这样会使计算机在存储数据的过程之中花费很多的时间。一般来说程序代码执行越密集,执行效率就越高,即计算机在加载程序的时候,附近的代码也会一块加载,所以内层循环次数高比外层循环次数多执行效率更高。

7.for循环区间最好是左闭右开,一般我们也会这样写,原因就是能很好的看出循环次数,例如for(int i = 0; i < 10;i++)能够很好的看出循环次数为10次,而如果写成for( int i = 0; i

11.for语句的控制表达式不能包含任何浮点类型的对象。浮点数存在精度损失,在判定时可能会出现一些问题。

12.在C语言中,不能采用数学中的表达式作为条件判断式,例如(18=18,得出来的逻辑值再跟60比较大小,与我们的初衷不符。

13、计算机把{}内的语句当作一条语句,所以我们如果if后面想跟多条语句的话,就要用{}括起来形成一个语句块。当然,即使是单条语句,也建议加上。

14.else总是跟最近的未配对的If进行匹配。

15.在写表达式进行判断的时候,特别是==,最好把变量写在右边,常量写在左边。

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

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

17.不要在for循环体内修改循环变量,以防程序失去控制。

18.for(;;)将构成死循环,条件判断恒为真,所以程序不会终止,但最好不要这样写!

19.goto语句:

for(...)    for(...)   {        for(...)       {            if(disaster)                goto error;       }   }    … error: if(disaster)         // 处理错误情况

在跳出多层循环时使用,事实上,,return 0;语句特能退出多层循环,但如果在主函数中直接执行return 语句则会直接结束程序。

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()函数的返回值用于说明函数的退出状态。如果返回0,则代表程序正常退出,否则代表程序异常退出。

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

4、数组

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

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

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

5、指针

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

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

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

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

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

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

3. * 操作符也叫解引用操作符,间接访问操作符。

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在进行寻址的时候,加快在内存中进行定位的效率。

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("%pn",&x);
    printf("%pn",&(x.a));
    //联合体的地址与联合体内定义的最大变量的地址是一样的
    printf("%pn",&(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、数据的存储

8、字符串

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

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

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

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

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)条件编译

1.

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

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

10、动态内存管理

11、文件操作

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

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>