一、c语言的版本更迭
点击跳转
二、c语言的编译原理
关于编译器有很多种,比如VS用的cl,dev C++用的gcc等。
c语言从源码到exe可执行文件经历的过程(也就是编译原理)
点击此处获取更多信息
三、数据的类型
总的来说数据类型可以分为以下几种:整型和浮点型,具体分如下
| 类型 | 在内存中所占字节大小 |
|---|---|
| char | 1 |
| short (int) | 2 |
| int | 4 |
| long (int) | 4 |
| long long (int) | 8 |
| float | 4 |
| double | 8 |
| long double | 8 |
注意:第一个编译器所约定的数据类型大小是不同的,以上是在cl编译器(vs2015)中测试所得数据,可以用sizeof(type)来获取所占字节数大小
四、数据的输入输出
一般来说输出用printf()函数、输出用scanf()函数
这两个函数的参数比较特殊,参数个数可以是任意个:
先看看printf:
差不多就是这样的printf("xxxxx%X",x,x,x,x,x....)
这个没啥好说的,重点要记住的是字符格式,也就是%X,要记住,""后面有几个要输出的值,那么就要有几个%X与其相对应。
| 格式类型 | 解释 |
|---|---|
| %d | 输出10进制整数 |
| %x | 输出16进制整数 |
| %o | 输出8进制整数 |
| %f | 输出小数 |
| %c | 输出字符 |
| %u | 输出无符号数 |
| %s | 输出字符串 |
| %p | 按指针方式输出内存地址 |
| %e | 以指数形式输出单、双精度实数 |
在说的复杂一点:
格式字符的一般形式是:%【格式标志】【宽度】【.精度】格式类型
- 【】表示可选
- 宽度:就是输出的数据占多少位
- 精度:一般来说针对的是小数,精确到多少位(用精度的小数部分会4舍5入)
- 格式标志
-:默认是左对齐,前面加“-”,右对齐
+:输出“+”,输出正号
0(零):补0,所有空格全补0 %p=%08X
# 显示#x16进制前缀
显示#o 8进制前缀
scanf:与printf类似。
需要注意的是:后面的参数需要加个&,这个符号是用来寻找地址的,把这个地址找到,然后就可以用输入的方式替换这个地址的内存内容了。
关于转义字符:
c语言中,因为他的约定,一些符号无法输出,但现实中,这些字符在有些时候是需要输出的,因此转义字符就出现了:

五、数据在内存中的存储方式
六、ASCII码
在计算机中,本质上只能存放数字。为了解决这个矛盾,于是人们想到采用“约定”的方式来解决。也就是通过提前预定,某个特定的数字的值,对应了某个特别的符号。 这种约定,最常见的就是ASCII码表。

七、运算符
只介绍几个,其他很简单。
- 自增、自减(++、–):分两种情况,一种是左自增(减),一种是右自增(减),根本区别就是"先用后加,还是先加后用",比如
int a=1;
int b=1;
printf("%d",++a);//输出2
printf("%d",b++);//输出1
printf("%d",a);//输出2
printf("%d",b);//输出2
- 逗号运算符:“,”这个东西其实没啥用,知道就好,其结果就是返回最右面的数。形如(x,x,x,x)如:
int a =(6,3,6,9,5);
printf("%d",a);//输出5
八、c语言的控制语句
c语言的控制语句分三大类:循环、分支和跳转语句
循环:有三种
1.while
while(xxxx){
xxxxxx;
};
2.do while
do{
xxxx;
}while(xxxx);
对于do…while循环,同while循环非常类似。不同之处在于do…while循环的循环体,在条件判断之前,所以,不管怎样,do…while的循环体起码会执行一次。
3.for
for(int i=x;xxxx;i++){
xxxxxx;
}
分支:大体上有两种
1.if…else…
if(xxx){
xxxx;
}else{
xxxx;
}
2.switch
switch(){
case x:
xxxx;
break;
case x:
xxxx;
break;
default:
xxxx;
break;
}
continue:直接进入下一次的判断(不再执行本次之后的语句)
break:跳出整个语句
跳转
goto语句:说是不建议用,会破坏程序的结构化。个人感觉还是挺好的。
标签:
xxxx;
goto 标签;
就是说先定义个标签,当程序运行到goto语句时,会寻找goto后面的标签,跳转到那个位置。可以用于跳出多重循环。
九、函数
所谓的结构化程序设计,在C语言中就是通过将大问题,切分为若干个小问题,然后利用函数的模块化解决问题的。函数在c语言中是非常重要的。
在软件工程中,有一个很重要的原则,高内聚,低耦合。
他是判断软件设计好坏的标准,目的是使程序模块的可重用性、移植性大大增强。也就是说把需要实现的功能通过函数封装起来,最后把他们聚在一起。
函数的语法:
返回值类型 函数名(参数列表)
{
函数体代码
return 返回值;
}
- 参数:我们通过向函数传递参数,将信息带入给函数
- 返回值:函数通过返回值,将它的运算结果,传递到函数之外(return关键字用于返回返回值)
需要注意的是,在c语言中,程序是自上而下顺序执行的,所以要先写定义函数,之后才能调用,也就是说被调用的函数需要写在调用者的前面。不过一般来说,我们会先声明函数(有时也叫函数原型),这样就不用写在前面了。
声明方式:
返回值类型 函数名(参数列表);
另一点需要注意的是,函数中的参数是形参,而我们调用时所写的参数是实参。数据传递的方式是实参复制到形参,因为是局部变量,所以会开辟新的栈帧来存放变量,就算他们的变量名称相同,他们也是不一样的,也就是说,两个不同的函数之间可以存在相同名称的变量,他们互相不影响。
return的作用:
- 带出返回值
- 结束当前函数的流程,(提前)返回到调用方
所谓的函数,其实是内存中的一段机器码而已。所谓的函数调用,就是在不同的机器码中,进行跳跃。通过调用反编译窗口,我们其实可以发现,当调用return时,程序会跳转到调用函数的下一行
#include <stdio.h>
void a(int b) {
if (b>1)
{
printf("你输入的的比1大,返回主函数");
return;
}
else
{
printf("你输入的比1小");
return;
}
}
int main(void)
{
a(3);
return 0;
}

十、函数的调用过程
栈:在我们的函数调用过程中,程序会专门分配一块内存空间,用于存放与当前正在执行的函数有关的数据(比如局部变量、参数等),这块空间,我们称为“栈”。
栈帧:因为一个程序中,如果函数有嵌套调用关系,这些函数的局部变量、参数等信息都放置在栈中。编译器、操作系统已经为管理不同调用函数的栈,做了足够的工作,使得他们之间地址是不会冲突。
像这样,每一个具体调用的函数,一定会对应一块栈中具体的内存。那一块与调用函数对应的内存,我们就称为该函数的栈帧。
也就是说,栈中包括了栈帧,每多一个被调用的函数,栈中就会为其开辟一块栈帧。
—————————————————————————————————————————————————————————
函数具体的调用过程:
从函数的调用方(A函数)调用B函数(被调用方),调用的基本流程如下:
- 传递参数:将实参的值赋值到B函数的栈帧中
- 保存返回地址:将B函数执行完成后,需要跳转到的地址(返回地址)保存到B函数的栈帧中
- 流程转移到B函数
- 上一层函数的栈帧地址相关信息
- 申请B函数的局部变量空间(在B函数的栈帧中)
- 保存其它的环境信息
- 执行B函数的相关代码
- 通过寄存器eax将返回值带出
传递参数是按照一定的约定来进行的,默认的,不加申明的话,使用的是C约定。
在vs中,右击项目的属性可以看到,他默认用的是c约定。

来看一下他们的具体内容:
- c约定
1.从右往左传参 2.通过内存传参 3.调用方(传递实参的一方)平衡栈 - fastcall
1.从右往左传参 2.优先通过寄存器传参(速度快),如果不能满足,再通过内存传参 3.被调用方平衡栈 - stdcall
1.从右往左传参 2.通过内存传参 3.被调用方平衡栈
了解了上面的内容,递归也就好理解了。(每次调用自己,都会开辟一块新的栈帧)
局部变量与全局变量
如果要从一个源文件中调用另一个源文件中的全局变量,需要加上extern关键字,其作用就是声明全局变量,一般来说,全局变量的声明是放在头文件的。
十一、数组
定义:
类型 标识符[数组大小];
如
int a[5];
可以通过a[0]=1;来进行赋值。注意下标是从0开始的。
数组的本质就是在内存中开辟一块连续的空间。
可通过sizeof(数据名)来获取数组的类型大小。
数组的寻址公式。
直接打印数组名结果是其首在内存中的首地址。
而如果打印数组名+x(x为int类型的整数),是打印首地址+x*数据类型所占的字节数。
如:
int arr[5]={0};
printf("%p",arr);
printf("%p",arr+1);

可见是加了4个字节

公式
ary[i]的地址 = 数字意义上的ary + i*sizeof(元素类型)
实际上,编译器处理相关的C代码,最终就是通过数组寻址公式来处理数组中各个元素的引用和赋值的问题。
如果要引用某个数组元素底层是分成两步的:
- 根据数组寻址公式,找到那个元素的地址
- 根据解释方式,取出该处地址的值
二维数组和一维数组差不多,他们的底层都是一维的,只不过编译器巧妙的把其模拟成了二维的。
如:
int arr2[2][3]={{1,2,3},{5,6,7}};

十二、指针
十三、结构体
所谓结构体,就是将一些基本的类型封装起来,方便我们进行操作的一种结构,以提高程序的内聚性,属于非基本数据类型,通过struct关键字来定义通过点来进行成员相关的操作。
struct 名称{
基本类型数据...
};
如
struct bird{
int type;
int wings;
};
bird bird1;
bird1.type=1;
同时,结构体也可以做为参数来传递,但是,这样做并不好,实参传递到形参,实际是参数的复制。如果说其内容过大,可能会导致程序崩溃,而且直接用结构体进行传递,如果想改变结构体成员的内容,是没法改变的。一般来说,用结构体指针来进行参数的传递,因为通过指针来传递是不会开辟新的空间的。
结构体的指针访问成员用“->”这个符号。
结构体在内存中是按照成员存储的顺序来存放的,如下图

但是会有奇怪的事情发生


这是因为“内存对齐”的原因
所谓内存对齐,就是一种规定
- int变量应该在4的倍数的地址上
- double变量应该在8的倍数的地址上
- char变量应该在1的倍数的地址上
- short变量应该在2的倍数的地址上
也就是说每种类型的值要根据其大小来决定他们存放的位置,并且要以最大变量类型的字节倍数结束

第一个是int型的,第一个地址位看成0;
第二个是double的,要放在8的倍数位上,所以放在了中间的位置,
第三个是float类型的,4字节,要放在4的倍数开头上,所以在最下面,也就是“16”的位置,
又因为最大是double类型,所以要以8的倍数结尾,16往后存了4个字节到20,最靠近的就是24了,所以打印是24
这个东西是可以设置的。

默认就是按照最大字节对齐,如果设为1,就是不留空了,输出就是16字节了。
十四、预编译指令
宏:宏就是查找替换
语法:
#define 宏名 要替换的名
如
#define MYINT int
int main()
{
M a = 5;
print("%d",a);
return 0;
}
宏分为有参宏和无参宏,上面那种是没有参数的
有参宏:
如
#define p(x) printf(x)
上面的定义就可以把printf语句用p来进行替换
还可以这样
#define p(x) printf("hello"#x)
x前加“#”,就是宏的字符串化了,其实就是把前面p()中的x给加个双引号
如:

宏的taken化
用两个##
这个东西就是用参数变量来修改语句中的数据

宏的taken化主要用于定义结构逻辑相同但类型不同的函数的宏定义
条件编译指令
C语言中的条件编译,就是根据不同的条件,选择不同的编译路径。
#ifdef 某个宏
如果宏存在,则编译此块内容
#else
如果宏不存在,则编译此块内容
#endif
除了#ifdef,还有#ifndef这东西和ifdef刚刚相反,表示如果宏不存在,则编译此块内容
通过条件编译,防止头文件重复包含
//hanxiaoxiaoxiaoxiaohan这个东西只要唯一就行,自定义的
#ifndef hanxiaoxiaoxiaoxiaohan
#define hanxiaoxiaoxiaoxiaohan
//头文件中的正式内容
void MyFun(int x, int y);
#endif
如果来个一样的头文件
比如上面那样,叫他<my.h>吧,他们调用两次就是
//hanxiaoxiaoxiaoxiaohan这个东西只要唯一就行,自定义的
#ifndef hanxiaoxiaoxiaoxiaohan
#define hanxiaoxiaoxiaoxiaohan
//头文件中的正式内容
void MyFun(int x, int y);
#endif
//hanxiaoxiaoxiaoxiaohan这个东西只要唯一就行,自定义的
#ifndef hanxiaoxiaoxiaoxiaohan//第8行
#define hanxiaoxiaoxiaoxiaohan
//头文件中的正式内容
void MyFun(int x, int y);
#endif
第二行先进行一个判断,是否定义了hanxiaoxiaoxiaoxiaohan这个宏,发现没有,接这跑第三行就定义了这个宏,第八行又进行一次判断,是否定义了hanxiaoxiaoxiaoxiaohan这个宏,结果已经定义了,就不跑这一块了。
在后续的C语言中,提供了另外的预编译指令,也可以实现这种效果
#pragma once
十五、枚举enum
枚举和结构体类似,是自定义的数据类型。
枚举可以限制成员的赋值范围等操作,可以防止输入错误,提高效率。
其语法是:
enum 名称{
成员,//成员用“,”
...,
};
如:
enum WEEK
{
Mon=1,
Tue,
Wed
};
WEEK的成员本质上是整形类型的方式来存储的,这也是为什么他可以做为stwich()中的参数来传递的原因,与int类型是没有区别的,它的特殊之处是编译器会进行编译时的检查。
十六、共用体union
共用体与struct、enum都属于非基本数据类型。
共用体,就是共用同一块内存的结构。共用体的所有成员,共享一块内存。共用体变量的大小,由最大的成员决定(同时考虑对齐)。
共用体的好处,是可以使用同一块内存空间,表示不同类型的变量,在某些场景中,可以节约资源。
语法:
union 名{
成员;
...
};
如:
#include<stdio.h>
#include<string.h>
union MyUnion
{
int a;
char carr[10];
};
int main(int argc, char* argv[])
{
MyUnion my;
char arr[] = "hello";
strcpy(my.carr,arr);
printf("a=%d\n", my.a);
printf("carr=%s", my.carr);
return 0;
}

可以发现a和carr确实是共用一块内存的
十七、变量的作用域和生命周期
static关键字
如果使用static关键字修饰局部变量,那么就得到了静态局部变量。所谓的静态局部变量,只是被限制了作用域的全局变量而已。他是在全局区的。
静态局部变量的特点:
- 只初始化一次
- 一个函数被调用多次,其静态局部变量的值是共享的(不会被重新创建)
对于全局变量而言:
- 其在main之前就已经被初始化
- 在main返回之后才会被回收内存
- 被所有的函数共享
静态全局变量
static修饰全局变量,就得到静态全局变量。
只能在本源文件中使用。
静态函数
如果使用static修饰函数,就得到静态函数。静态函数其作用域被限定在当前文件
| 变量 | 生命周期 | 作用域 |
|---|---|---|
| 局部变量 | 进入函数开始,函数返回前结束 | 函数作用域 |
| 全局变量 | main之前开始,main之后结束 | 所有c文件 |
| 静态局部变量 | main之前开始,main之后结束 | 函数作用域 |
| 静态全局变量 | main之前开始,main之后结束 | 当前c文件 |
内存里面的一些结构:

堆可以动态的分配,
申请:
void *malloc( size_t size );
释放:
void free( void *memblock );
对于已经释放后的内存空间,仍然有指针指向它,是比较危险的,这种指针称为野指针。
申请释放之前可以做一些相关的判断,来避免野指针。
在进行判断时,可以写个模板,而不用在挨个去判断。
如:
LABEL_FREE: //跳转到这
//资源释放的出口
if (xxx1 != NULL)
{
free(xxx1);
xxx1= NULL;
}
if (xxx2!= NULL)
{
free(xxx2);
xxx2= NULL;
}
if (xxx3!= NULL)
{
free(xxx3);
xxx3= NULL;
}
在用goto语句,可以用于资源释放模板时的直接跳转。
十八、位运算
位运算就是针对二进制的数据的每一位(bit)进行的计算。因为硬件设计的原因,位运算的效率,往往会高于其他的普通运算。
按位与

按位或

按位取反(~)
- 原有为0的bit,变为1
- 原有为1的bit,变为0
异或:
切换开关,对一个数进行两次异或操作还是其本身,异或常用于加密算法

位移(<<,>>)
所谓的位移运算符,就是从bit的角度,去整体“移动数据”。
比如
3<<2
先将3转成二进制(以4位为例),为0011,左移两位1100,也就是12(超出高位的部分丢弃,而不足的低位部分补0)
无符号:
3>>2
先将3转成二进制(以4位为例),为0011,右移两位0000,也就是0(超出低位的部分丢弃,空缺出的高位补符号位 )
有符号:
3>>2
先将3转成二进制(以4位为例),为0011,右移两位1100,也就是0(超出低位的部分丢弃,空缺出的高位补符号位 )
左移1位,其实就是乘以2。
右移就是除以2;
位域
type xx:个数;
所谓的位域,就是将整型的变量的各个bit,进行切分,使得我们可以向操作成员那样,操作各个bit。
如:
struct tagTest
{
char cValue1 : 4;
char cValue2 : 4;
};
int main(int argc, char* argv[])
{
tagTest myObj;
myObj.cValue1 = 5;
myObj.cValue2 = 3;
return 0;
}

如图,在一个字节中先存了5,又存了3.
十九、文件操作
操作文件,属于操作系统的范畴,不同操作系统的文件组织方式是不一样的。
但为了统一标准,c言语封装不同操作系统之间的的不同,提供了统一的接口。

文件的打开和关闭
打开文件
FILE *fopen(char *filename, char *mode );
- filename:需要打开的文件的文件名
- mode:打开的属性(只读、只写、二进制……)
mode:
- "r":只读方式打开,文件必须存在,如果不存在,则打开失败
- "w":只写方式打开,文件如果不存在,则会创建新文件;如果文件已经存在,则会删除原文件,再创建新文件
- "r+":可读可写方式打开,文件必须存在,如果不存在,则打开失败
- "w+":可读可写方式打开,文件如果不存在,则会创建新文件;如果文件已经存在,则会删除原文件,再创建新文件
- "a":附加方式打开,写入的数据在文件末尾。不会覆盖掉原有已经存在的数据
- "a+":类似,可读可写方式打开
以上所有的属性,还可以与"b"组合使用,比如"rb", "rb+", "ab+"……
不与b组合,那么是以文本方式打开
与b组合,那么是以“二进制方式”打开
如:
#include<stdio.h>
int main(int argc, char* argv[])
{
FILE * fileopen = fopen("sp.txt","w");//w代表只能写,如果没有就创建,如果有就重新创建
return 0;
}

打开的文件,被封装为了一个FILE结构体。返回的文件结构体指针,就代表了文件本身。我们将通过操作这个指针,达到操作文件的目的。
关闭文件
int fclose( FILE *stream );
- stream :需要被关闭的文件所对应的指针
如:
#include<stdio.h>
int main(int argc, char* argv[])
{
FILE * fileopen = fopen("sp.txt","w");//w代表只能写,如果没有就创建,如果有就重新创建
if (fileopen == NULL)
{
printf("打开文件出错\r\n");
}
if (fileopen!=NULL)
{
fclose(fileopen);
fileopen = NULL;
printf("关闭文件成功\n");
}
return 0;
}
文件读写
写文件:
size_t fwrite( void *buffer, size_t size, size_t count, FILE *stream );
- buffer:将内存中的哪个地址处的内容写文件
- stream:写入到哪个文件中
- size与count的乘积,代表需要写入的字节数目
如:
#include<stdio.h>
#include<string.h>
int main(int argc, char* argv[])
{
FILE * fileopen = fopen("sp.txt","w");//w代表只能写,如果没有就创建,如果有就重新创建
if (fileopen == NULL)
{
printf("打开文件出错\r\n");
}
char carr[] = "hello world";
int result=fwrite(carr,1,strlen(carr),fileopen);
printf("成功写入:%d字节\r\n", result);
if (fileopen!=NULL)
{
fclose(fileopen);
fileopen = NULL;
printf("关闭文件成功");
}
return 0;
}

读文件
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
- stream:读取哪个文件
- buffer:读取到内存的那个位置
- size与count乘积:读取多少个字节
如:
#include<stdio.h>
#include<string.h>
int main(int argc, char* argv[])
{
FILE * fileopen = fopen("sp.txt","r");//w代表只能写,如果没有就创建,如果有就重新创建
if (fileopen == NULL)
{
printf("打开文件出错\r\n");
}
char carr[100] = "";
int result=fread(carr, 1, 100, fileopen);
printf("读入成功%d个字节\n", result);
printf("%s\n", carr);
if (fileopen!=NULL)
{
fclose(fileopen);
fileopen = NULL;
printf("关闭文件成功\n");
}
return 0;
}

前面我们提到文本方式与二进制方式
因为不同操作系统下的约定不一定相同,比如换行:
- Windows: \r\n
- Linux:\n
如果用文本方式,计算机可能会自动将"\n"替换为"\r\n",这样数据就会不准确,出现bug,所以在一般的文件操作时,尽量使用二进制方式读写文件。
文件缓存
当我们进行调试时,会发现当程序执行完int result=fwrite(carr,1,strlen(carr),fileopen);这一行时,并没有被马上写入文件,等到整个程序结束,数据却又写入了文件。这是因为缓冲机制的存在。
flush(pFile);
flush()就是用来刷新缓冲区。
在int result=fwrite(carr,1,strlen(carr),fileopen);下面加上flush(fileopen );就能即时刷新了。
1万+

被折叠的 条评论
为什么被折叠?



