1.内存
我们看到的内存条就是我们电脑的内存。
程序运行:首先定位到硬盘安装的程序,然后将数据复制到内存,最后再运行。
虚拟内存:当我们打开8个程序,占用3G内存时,我们的内存条只有2G,怎么办呢???此时你肯定会卡顿,没得说。。。。然后我们的操作系统(OS)将内存中暂时不用的数据放到硬盘中,硬盘的这块区域叫做虚拟内存。
2.字符编码
实现二进制与字母的一一对应。
ASCII:American Standard Code for Information Interchange
总共收集了128个字符,占用一个字节的低七位。
ASCII编码的出现,满足了美国英语的表达需求,但是对于其他国家的文字,还需要其他的编码方式。
为了能够使用原有的一些程序,各个国家在建立自己的编码体系时都囊括了ASCII。
中文字的编码:
GB2312→GBK→GB18030
GBK采用1~2个字节存储。
GB180303采用1、2、4个字节存储。
GBK中:
1)ASCII采用一个字节存储,最高位为0
例如:C、01000003
2)中文字采用两个字节存储,最高位都为1
例如:中、11010110 11010000
大家会问到了,GBK用在哪里?只要你是中文的windows,只要使用汉字的地方,到处都用到GBK。
汉字通过GBK编码,转换为二进制,才能存储起来。
使用时也是,将二进制通过GBK解码,生成文字,显示出来。
一般情况下,windows内核默认使用Unicode字符集,也可以改成默认使用GBK。
按这样说,中国用GBK,其他国用其他的,那么无法做到统一,也就不能交流了。因此,被称为万国码的Unicode字符集诞生了。
这里字符集不等于字符编码:
Unicode:字符集(它不考虑编码,只是规定字符和二进制一一对应关系)。
ASCII和GBK等:在规定字符集的同时,也确定了字符编码。所以叫GBK字符集也行,叫GBK字符编码也行。
Unicode字符集用到的字符编码有三种:
UTF-8:变长编码方式,1~6个字节;
只有一个字节,最高位为0,兼容ASCII
多个字节,第一个字节最高位依次几个1表示用了几个字节,其他字节开头为10
例如:11000011 10000011
用到了两个字节,其数据为11000011
从最左端开始,去掉数据的头,补成8位的即是数据。
UTF-16:变长+固定,用2、4字节存储;
UTF-32:定长编码方式,4个字节。
其中UTF-8兼容ASCII,UTF-16和UTF-32都不兼容ASCII。
在windows内核中大多采用了UTF-16
在Linux以及ios中大多采用了UTF-8
UTF:Unicode Transformation Format
有时候我们打开visual ssudio源文件时,会出现乱码的现象,这是由于源文件的保存默认使用的是本地编码,中国常用的GBK,在其他国可能就不适用。
3.生成可执行文件
1)编译
我们在写完c程序后,我们自己能看懂,感觉还不错,但是这个直接交给计算机,计算机就犯难了,因为它都是晶体管组成的,只能理解二进制。
所以,编译器将我们的代码写成0101的格式,并且链接后形成可执行文件,交给计算机执行。有时候你用文本打开可执行文件,你会发现,里面基本上都是0101之类的。
而且我们经常会说,编译出错了。
那么,为什么总是编译出错了?因为编译是翻译的过程,我们程序写错了,编译器无法翻译,那么它肯定会求救,也就是报错。
编译器总结:
windows的编译器:Visual C++,目前多集成于Visual Studio;
Linux的编译器:GUN组织开发的GCC;
Mac的编译器:LLVM/Clang被集成于Xcode。
编译后生成:
windows:目标文件(Object File),后缀为.obj
Linux:后缀为.o
2)链接
将obj和标准库、动态链接库组成一个可执行文件。
4.程序安装
安装分为4个步骤
1)可执行文件拷贝到指定目录;
2)将自己带的动态链接库(DLL)拷贝到系统目录(System32);
3)向注册表注册安装信息(拷贝了哪些文件,文件位置);
4)生成快捷方式。
5.数据类型
不管在什么系统下,short总为2字节。
16位和32位机,int为2字节;64位机,int为4字节。
一般情况下long为4字节,在linux中,long为8字节。
printf时,控制输出
1)%#,输出带有前缀。
%#x,输出的是16进制,且带有前缀0x。
2)数据存储
无符号位:直接存储数据的源码
有符号位:正数直接存储数据的源码,负数存储数据的补码。
在printf时,它会判断,如果是有符号的,且第一个字符是1(即为负数),那么输出时,它会把数据当成补码,然后计算其原码。
3)字符、字符串
单引号 ‘ ’ 表示字符
双引号 ” “ 表示字符串
char表示字符,它只能保存ASCII码中的字符。
它其实存储起来的数值是int,它能够做的是将字符转换成对应的ASCII码,然后保存成int数据。
char a1 = 'F';
int a2 = '70';
printf("%d,%c \n",a1,a1);
printf("%d,%c \n",a2,a2);
输出为
70,F
70,F
4)printf输出进阶
printf("%-6d %-6d\n", s1, s2);
-表示左对齐
6表示占6个字符宽度,不足用空格补全;如果输出超过6时,则无视6的限制
d表示10进制输出
printf("%.6d %4d\n", s1, s2);
.6表示,如果后面s1带有小数,则小数位为6位,不足用0补全;
如果后面s1是整数,则整数位为6位,不足,在头部用0补全,超过则无视限制。
5)scanf读取
读取结束后,可以用
scanf("%*[^\n]"); scanf("%*c");
来清空缓冲器。
如果需要读取特定的字符,且在读到其他字符后,读取结束。
scanf("%[spm]", str);
这里是指当输入时s、p或者m时,继续读取,否则就停止读取。
也可以写成
scanf("%[a-z]", str);
这样意味着只要是在a-z中间的字母(所有小写字母),都可以读取。
scanf("%[a-zA-Z0-9]", str);
这样意味着字母和数字都可以读取。
如果我们只是不想要数字,其他都要呢?
scanf("%[^0-9]", str);
6)gets和scanf
gets只可以读取字符串,并且认为空格也是字符串的一部分,结束标志是回车。
scanf可以读取多种数据,但是在遇到空格时,会认为字符串已经结束。
gets(str1);scanf("%s",str2)
6.缓冲区
定义:缓冲区/缓存,是内存空间的一部分。内存中预留一部分空间,用于输入输出数据的暂时保存。
用途:CPU向硬盘写入数据时,程序需要等待,用户出现卡顿现象。如果出现多次写入的情况,那么用户需要多次等待,卡顿十分明显。于是CPU先将输出数据放到缓存中,等所有数据都准备好后,统一放到缓冲区。
7.字符串 ‘\0’
在c语言中,字符串总是以’\0’作为结尾的。
那么在c语言中什么是字符串呢?
是 ” “ 双引号里面的内容。
例如:“abcdefg”,在赋值给字符串或者字符数组时,真正存储的为"abcdefg\0"
0表示整数,‘0’表示0字符,’\0’表示ASCII码值为0的字符,NUL。
字符数组:字符数组赋值有两种方式
1)元素单独赋值
char c[] = { ‘a’, ’ ‘, ‘p’, ‘c’, ‘o’};
这种因为是单个字符赋值,因此不用考虑某位有’\0’
这里c长度是5
2)将字符串赋值给字符数组
char d[] = {”iloveyou“};
这里自动加入了’\0’,因此d长度为9。
这里还要注意一点,这里只是定义存储空间的时候需要给’\0’预留位置,而不是说我们的字符串长度需要加1,在我们求字符串长度时,不需要加‘0’
#include <string.h>
strlen(d)
这里输出为8
字符串处理
1)strcat(str1,str2)
拼接两个字符串:string+catnate
将str1末尾的’\0’去掉,将str2接到str1末尾,储存地址仍为str1地址。
2)strcpy(str1,str2)
拷贝字符串:string+copy
将str2字符串以及’\0’,原封不动地拷贝到str1中。
3)strcmp(str1,str2)
比较两个字符串地ASCII,如果相同,继续比较,直至遇到不同的或者到末尾。
如果str1和str2相同,返回0;如果str1>str2,返回大于0的值,反之返回小于0的数。
8.数组
c语言是静态数组,意味着程序只能读取和修改元素,而不能插入和删除元素。想要插入或者删除元素,只能把元素从数组中读取出来,保存到重新创建的数组。
数组存在问题:越界和溢出
越界:数组的下标出现了负值或者大于数组的值,c语言并不会对越界进行检查,因此越界时能够正常编译,但是运行时可能会发生错误。
例如:a[-20] = -858993460
溢出:数组赋值时,超过了数组长度。
int b[3] = {a,b,c,d,e,f};
溢出时,只能保存3个字符,后面的全被舍弃。
变长数组
我们在使用数组时,有时会希望根据输入,我们的数组的大小是可以变化的。然而,在C99版本之前都是不支持的,数组在编译时就要开辟空间,因此数组大小必须是固定的,不能修改。
在C99中可以
int n;
scanf("%d", &n);
int arry[n];
数组指针
数组的名即为数组首地址。
例如:
int arr[] = {11,22,33,44};
其中*arr为11;
*(arr+1)为22;
arr[1]为22;
我们也可以用int* p = arr;
用p来代替arr,但是需要注意,指向数组的指针和数组虽然有时候可以通用,但是它们有本质区别。
p只是一个指针,而arr是一种特殊的指针,它指向数组首地址的同时,还具有数组的特性,记录着整个数组的尺寸。
sizeof§/sizeof(int*) = 1;
sizeog(arr)/sizeof(int) = 4;
数组下标[ ]
例如
a[i]它在编译时,会被转换为*(a+i);
因此在我们以后看到数组下标时,都可以自己进行转化,统一为一种表达方式,这样就不会乱掉了。
指针数组
储存指针的数组
它的每个元素都是指针,因此也可以认为,它里面的每个元素都可以表示是一个数组,是数组的结合。
数组指针
指向数组的指针
它本质上是指针,不是数组,所有定义时特备注意
int (a)[4];
这里是先定义a为指针,指向的数据类型是int[4];
意味着a的每一个元素有4个int
这里一定要记住,[ ]的优先级是大于 指针 的,因此必须要加括号。
*(P+1)表示第二个元素的首地址
*( * (P+1)+1)表示第二个元素里面的第二个元素。
9.C语言预处理
#include;#if;#elif;#endif
以井号开头的命令称为预处理命令。
1)系统调用预处理程序来实现不同系统的使用。
#if _WIN32 //如果是windows系统,则引入windows.h
#include <windows.h>
#elif __linux__ //如果是linux系统,则引入unistd.h
#include <unistd.h>
#endif
2)#include为文件包含命令,把头文件复制到当前文件
#include<>,编译器会在系统目录下查找头文件;
#include“ ”,编译器会在当前目录下查找头文件,没有找到再去系统目录下找;
因此,有时候我们用#include<>时,可能发现找不到自己定义的头文件。
3)宏定义
#define PI 3.1415926
宏定义是在预处理时,完成字符串替换。
在函数中也可以判断宏是否被定义
#ifdef 宏名
程序段
#endif
10.函数参数
一般情况下,传入函数的参数不会因函数内部的改变而改变。
需要传递参数地址。
例如要传递参数a
int* a = &a ;
viod max(int* a){
}
void mian{
max(&a);
}
传递数组时,需要传递其长度(因为在传递时只是传递的首地址,没传递长度)
int max(int a,int len){
}
void mian(){
int a[5];
int len = sizeof(a)/sizeof(int);
max(&a,len);
}
这里有两个对应,记住后,再也不拍带有指针的函数了。
1.函数定义的参数
这里是从主函数把参数赋值给函数,因此是函数的参数得到主函数的值。
因此是
int 函数参数 = 主函数赋值
如果是指针,那么是
int* 函数参数 = &主函数赋值
2.函数返回的参数
当函数内部的参数return时,是return给函数。
所以函数定义时
int 函数 = return的变量
return的是地址,则为int* 函数 = return的参数(数组/&变量)
指针初始化
指针在定义后,如果不初始化,该指针将不知道指向何处,不能直接对该指针进行操作。
指针初始化,可用空指针或者开辟空间:
1)空指针
int* a = NULL;
这里NULL为((viod*)0)
2)开辟空间
int *a = (int *)malloc(sizeof(int) * 20);
上面malloc开辟了4 * 20个字节的空间,但是没有说明空间存储的类型,所以需要在前面加一个强制转换(int *)。
这里指的是32位系统,int *为4个字节,在64位中,它为8个字节。不只是int,只要是指针,在32位系统中都是4字节,在64位系统中是8字节。
11.结构体
结构体是一个集合,将多个变量集合在一起,来表示一个物体具备的特性。
struct stu{
char *name; //姓名
int ID; //身份证号
int age; //年龄
char *sex; //性别
char *nation; //民族
};
结构体也是一种数据类型,可以定义变量
struct stu stu1, stu2;
struct stu {
……
……
……
……
}stu1, stu2;
也可以定义结构体数组
struct stu stu1[] = {
{“欧阳修”,135165488946542151,55,“男”,“汉族”},
{“欧阳修”,135165488946542151,55,“男”,“汉族”},
{“欧阳修”,135165488946542151,55,“男”,“汉族”},
{“欧阳修”,135165488946542151,55,“男”,“汉族”}
};
这里有个需要记住的定义,结构体指针的定义
这里一定要明确,说的是结构体指针,这也是一个数据类型
struct stu *ppstu1;
那么如何赋值呢?
ppstu1 = &stu1;
直接把其他结构体的首地址给它。这里需要注意,结构体和数组不同,数组的名表示数组的首地址,而结构体的名表示结构体整体,只有取地址才能取到首地址。
如果函数需要调用数组,可以这样定义
void max(struct stu *pp){
}
void main(){
struct stu stu1;
max(stu1);
}
12.大端小端
1.大端,数据高位放低地址
存放是从低地址开始的,一步一步往进存。
和字符串顺序处理类似.依次存放。
存放0x2021
内存地址 | 0x1000 | 0x1001 |
---|---|---|
存放内容 | 0x20 | 0x21 |
2.小端,数据低位放高地址
存放0x2021
内存地址 | 0x1000 | 0x1001 |
---|---|---|
存放内容 | 0x21 | 0x20 |
PC机是小端模式;51单片机是大端模式。
13.值不变 const
const int pi = 3.1415926;
此时我们便不能再给pi赋值了。
当应用到指针中,需要注意
const int *P;int const *P;此时指针可以变指向,但是指向的位置不可以再被赋值。
int * const P;此时指针指向为固定,里面的内容可以改变。
当应用到函数形参时
为了避免在参数传递时,在函数内部其值被修改。
int max(const int *a);此时在max函数中,指针a指向的数值不能被修改。
14.文件操作
打开文件并判断是否打开成功
FILE *fp;
if(fp = fopen("E:\\a.txt","rb") == NULL){
printf("Fail to open file!\n");
exit(0);
}
fopen必须添加读写权限
r(read):
w(write):
a(append):追加
+:读和写
t(text):文本文件
b(banary):二进制文件
rb读二进制文件;rt+读和写文本文件
关闭文件
fclose(fp);
读取文件的一个字节
ch = fgetc(fp);
每读一次,文件内的指针都会后移一个字节。
while((ch=fgetc(fp))!=EOF){
putchar(ch);
}
判断文件读取是否正确
if(ferror(fp)){
puts("读取出错");
}else{
puts("读取成功");
}
写入一个字节
char ch;
ch = 'w';
fputc(ch,fp);
读取字符串
char str[100];
fgets(str,100,fp);
该函数每次只能读取一行。
写字符串
char *str = "abc123456789";
srtact(str,"\n");
fputs(str,fp);
如果成功写入返回非负数,否则返回EOF
按数据块读写
int a[100],r[100];
int size = sizeof(int);
fp = fopen("D:\\a.txt","rb+");
fwrite(a,size,100,fp);
rewind(fp);
fread(r,size,100,fp);
size为定义的每个数据块的字节数。
使用fwrite和fread时,需要以二进制形式打开文件。
复制文件
首先系统开辟一块缓冲区,然后将需要复制的文件不断读入,每读取一次就将缓冲区内容写入文件。
//开辟缓冲区
int buffernume = 1024*4;\\缓冲区长度
char *buffer = (char *)malloc(buffernume);\\开辟缓冲区
int readnum;
if((FILE *a = fopen("D:\\read.txt","rb")) ==NULL || (FILE *a = fopen("D:\\write.txt","wb")) ==NULL){
prinf("Cannot open file");
exit(1);
}
while((readnum = fread(buffer,1,buffernume,a))>0){
fwrite(buffer,readnum,1,b);
}
15.调试技巧
1)代码带有调试信息,可以使用宏定义控制输出
定义总宏
#if (defined DEBUG) || (defined _DEBUG) //检测构建模式是否为调试模式
#define _DEBUG_printf
#endif
在函数中
#ifdef _DEBUG_printf
printf("调试信息为输出状态");
#endif
16.CPU运行
1)寄存器:CPU中的小储存部分,速度非常快,便于进行运算操作。
寄存器一般能存储32位/64位数据,CPU中有几十或上百寄存器。
2)CPU
CPU决定了数据处理和寻址能力,CPU一次能处理的数据大小由寄存器和数据总线宽度决定,CPU多少位=寄存器位数=总线宽度
16位处理器,一次性处理16位,数据总线16根
32位处理器,一次性处理32位,数据总线32根
64位处理器,一次性处理64位,数据总线64根
编译器分为两种编译模式,32位编译模式和64位编译模式
32位,一个指针/地址占4字节,32位,可访问2^32内存空间
0x
64位,一个指针/地址占8字节,64位,仅用虚拟地址低48位,可访问2^48内存空间
32位系统只能运行32位编译模式的程序
64位系统可以同时运行32位和64位
32位环境=32位CPU+32位的操作系统+32位程序
CPU在访问内存时,32位的CPU一次可读取32位,相当于4个字节,一次可读取4字节。因此其寻址步长为4字节,其一次寻址的首地址可以为0、4、8、12等,皆为4的倍数。
这里也就给出了,为什么要进行内存对齐。
例如要寻址一个int值,它是4字节的。如果它的首地址位于4,那么CPU寻址一次取出来就可以了。如果它的首地址位于6,那么CPU需要先寻址4,再寻址8,才能将int完全读取出来。
如果你写过程序,你会问,32位的CPU寻址为4的整数倍,那么64位的CPU寻址应该为8的整数倍。那为什么有些依然都是4的整数倍????因为这个是由编译器决定的,同时,你也可以再编译器中设置对齐方式。
17.进程、线程
进程:程序静态存储在磁盘上,当运行时,加载到内存中,便创建了一个或多个进程。
18.栈
1)栈(Stack):由内存自动分配和释放,函数调用时,为函数数据分配内存,调运结束后,释放函数的参数和函数的局部变量。(当然,这里的函数概念包括了main函数和main调用的函数)
2)栈底为ebp,栈顶esp,放入数据为入栈(Push),取出数据为弹出(Pop)
3)栈溢出:栈的内存有限(1M~8M),超过后发生溢出。栈的大小可以在编译器中设置。
19.变量储存类型
auto(自动)、static(静态)、register(寄存器)、extern(外部)
1)auto
该类形是默认的,不需要可以设置
auto int a = 5;
2)static
静态变量,放置在静态区域,全局变量默认放置在静态区域。
该变量只能被一次初始化。
该变量在初始化时开辟区域,开辟后就一直保留,直到程序执行完毕或者被释放。
因此在该变量的生命周期中,对其进行的任何操作都会保留下来,即保留当前状态,该性质在多线程编程时非常实用。
static int a = 1;
static int a = 0;
//这样是有问题的,因为a已经被初始化了,下面又初始化一次,无法完成,也就是说赋值的a=0也无法完成。
a = 2;
//这样没问题,可以改变值,此时a = 2。
3)register
寄存器变量,用于存储使用非常频繁的变量,减少从内存中读取所消耗的时间。
例如for循环的i
可以
register int i = 0;
for(i;i<100;i++){
}
4)extern
在变量使用前声明,用于告诉编译器,此变量在其他位置已经定义,可以找到后直接使用。
extern int a;
int main(){
}
栈和堆的区别
栈:在函数调用时,第一个进栈的是主函数中函数调用后的下一条指令的地址,然后函数的各个参数,在大多数的 C 编译器中,参数是从右往左入栈的,当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令。
堆:由malloc分配,由free释放,一般是在堆的头部用一个字节存放堆的大小,具体内容由程序员安排。
这么看来,栈是系统分配,用于存储程序的全过程;堆则是人为分配(malloc、free),用于保存函数操作中用到的一些内存空间。
一般情况下,栈就可以满足函数调用中的数据存储了,但是它是系统分配和释放的,有时不满足我们的要求。因此我们可以用堆,来自己嗨皮。
提到堆,需要提一下两个最常用的函数。
1)malloc分配内存空间
if((a = (int*)malloc(100*sizeof(int))) == NULL)
分配过程
①malloc先遍历空闲空间,找是否有大小合适的内存块,
堆内由于数据的存储和清楚,内存部分并非紧密排布,而是有的地方空闲,有的有数据。
②有空闲就分配,没有就向操作系统申请。
不连续的空闲空间将一段内存分成了多个内存块,每个内存块包含next指针,指向下一个内存块首地址,包含状态used,表示给内存块是否空闲。
有时为了方便,除了next和used外,内存块还存有pre指针,指向上一个内存块。
其中1和0代表used的状态,1为已占用,0为空闲。
2)free释放内存空间
程序释放掉内存块,内存块变得空闲,free将空间且相邻的内存块合并成一个大的内存块。
——-----------------------------------------------------
关于内存一些需要规避的问题
1)野指针
定义:指针在定义时没有被赋值,因此该指针指向是随机的,很可能指向没有访问权限的内存空间,因此在对指针指向位置赋值时,出现错误。
规避:不需要赋值时,初始化为NULL;指针指向的内存被free释放时,需要给指针初始化为NULL。
2)内存泄漏
定义:malloc已经分配的内存需要有指针指向它,如果没有指针指向(例如,malloc分配内存4时,指针p指向它;而后malloc分配内存5时,指针p改为指向5,那么内存4我们便无法找到了,只有当整个程序结束后,系统才会回收内存4)。
这个内存泄漏的危险主要体现在当我们使用大量的循环开辟内存空间时,如果不在每个循环后释放掉不必要或者重复的内存时,内存占用会一直增加。如果一次占用8M那么重复131072次,我们的1T硬盘就满了,更何况我们的4G或者8G的内存呢。
规避:使用内存分配时(malloc)一定要小心,每个分配对应一个指针,在程序结束时,用free释放。