c语言深入浅出

int a=100;
int * p=&a;//定义一个指针类型,&a取变量a的首地址
printf("%d",*p); 取出指针地址所对应的值
&是取地址符号是升维度的
*是取值符号是降维度的

所有的类型指针存储的都是内存地址,内存地址都是一个无符号十六进制整型数
prinft("%p",sizeof(int *));
prinft("%p",sizeof(char *));
prinft("%p",sizeof(long *));
prinft("%p",sizeof(float *));
prinft("%p",sizeof(double *));
prinft("%p",sizeof(long long *));
在32位操作系统下所有指针类型是4个字节大小
在64位操作系统下所有指针类型是8个字节大小

如下
char a='a';
int *p=&a;
printf("%d\n",*p);
这里a是字符1个字节 如果用int指针指向它 那么取出的值会按4个字节大小取出, 从首地址开始算起的连续4个字节大小的内存地址对应的数值,相同的如果用指针对它的值进行改变会同时改变占据4个字节大小内存的数值。所以定义指针类型一定要和变量的类型对应上

野指针
指针变量指向一个未知内容的空间
程序中允许存在野指针
操作系统0-255做为系统占用不允许访问操作的地址
操作野指针对应的内存空间可能报错

空指针
如:int *p=NULL;
NULL是一个值为0的宏常量:#define NULL ((void*)0)
是指内存地址编号为0的空间
操作空指针对应的空间一定会报错
空指针可以用在条件判断

万能指针
void *p
万能指针可以接收任意类型变量的内存地址
在通过万能指针修改变量的值时,需要转换成变量对应的指针类型
如下:
int a=10;
void *p=&a;
*(int*)p=100;
printf("%d\n”,*(int*)p);

const修饰指针的3种情况
1.const修饰指针类型
如:const int*p=&a;
可以修改指针变量的值
不可以修改指针指向内存空间的值
2.const修饰指针变量
如:int* const p=&a;
可以修改指针指向内存空间的值
不可以修改指向变量的值
3.const修饰指针类型和修饰指针变量(只读指针)
如:const int* const p=&a;
即不可以修改指针变量的值
也不可以修改指针指向内存空间的值
虽然一维指针不能修改,但是一维指针可以被二维指针所指向并可以修改一维指针的变量以及指向内存空间的值。以此类推,二维指针修改不了可以三维指针所指向并修改...

指针和数组
数组名字是数组的首元素地址,但它是一个常量
int a[]={1,2,3};
printf("a=%p\n",a);
a=10;//err,数组名只是常量,不能修改
int*p=a;
a[1]等价于*(p+1),其实数组就是一个特殊形式的指针
指针类型变量+1等同于内存地址+sizeof(int)
两个指针相减,得到的结果是两个指针的偏移量(即根据类型的字节大小进行偏移,如是int,那么偏移+1就是地址移动了4个字节大小)
所有的指针类型相减结果都是int类型

如在函数中定义一个形参为int类型的数组是不能使用sizeof准确计算出传入的数组字节大小
因为数组作为函数参数会退化为指针,实际上定义形参数组就是定义一个指针指向这个地址
而在这里使用sizeof计算传入数组的大小,计算的就是这个int指针类型大小,其实无论什么类型的指针它存储的就是无符号十六进制整型的内存地址,
根据操作系统位数,32位就是4个字节大小指针类型;64位就是8个字节大小指针类型
注意sizeof是编译时的指令,即在编译时期就计算好了类型所占的字节大小,而不是在运行时期动态计算

指针加减运算
加法运算
指针计算不是简单的整数相加
如果是一个int * ,+1的结果是增加一个int的大小
如果是一个char *,+1的结果是增加一个char的大小
指针的加减运算和指针的类型有关
指针操作数组时下标允许时负数
指针和指针之间可以相减,可以比较大小,可以逻辑判断,但不能相加,不能乘除取余等操作,因为没有意义

字符串拷贝
void my_strcopy(char* dest,char* ch)
{
  while(*dest++=*ch++);
}
while内执行顺序:
根据运算符优先级,
1.*ch
2.*dest
3.*dest=*ch
4.while条件判断是否真值为1
5.ch++
6.dest++
所以根据以上执行顺序可以将*ch的字符串拷贝到*dest中

指针数组
如:
int b[2]={1,2};
int *a[]={&b};
printf("%d\n",a[0][1]);//打印为2
指针数组,它是数组,数组的每个元素都是指针类型
指针数组是一个特殊的二维数组模型,即可以先通过a[0]下标取得一级指针b,再通过[1]下标取得一级指针指向的地址所对应的值
int* arr[3]={&a,&b,&c};
在数组中arr和&arr都是地址而且都是相同的地址,不同在于arr表示数组首元素的首地址,&arr表示整个数组的首地址,即含义不同
不管是指针数组还是普通数组都是属于数组,指针和数组最大的区别在于数组是指向数组自身的首地址,对自己开辟的连续内存空间上进行操作的,而指针是指向其它地址对其他地址所对应的内存空间进行操作的

指针数组与数组指针区别
指针数组:指针数组可以说成是”指针的数组”,首先这个变量是一个数组,其次,”指针”修饰这个数组,意思是说这个数组的所有元素都是指针类型,在32位系统中,指针占四个字节。
数组指针:数组指针可以说成是”数组的指针”,首先这个变量是一个指针,其次,”数组”修饰这个指针,意思是说这个指针存放着一个数组的首地址,或者说这个指针指向一个数组的首地址。 
根据上面的解释,可以了解到指针数组和数组指针的区别,因为二者根本就是不同类型的变量。
首先来定义一个数组指针,既然是指针,名字就叫pa
char (*pa)[4];
如果指针数组和数组指针这俩个变量名称一样就会是这样:char *pa[4]和char (*pa)[4],原来指针数组和数组指针的形成的根本原因就是运算符的优先级问题,所以定义变量是一定要注意这个问题,否则定义变量会有根本性差别!
pa是一个指针指向一个char [4]的数组,每个数组元素是一个char类型的变量,所以我们不妨可以写成:char[4] (*pa);这样就可以直观的看出pa的指向的类型,不过在编辑器中不要这么写,因为编译器根本不认识,这样写只是帮助我们理解。
既然pa是一个指针,存放一个数组的地址,那么在我们定义一个数组时,数组名称就是这个数组的首地址,那么这二者有什么区别和联系呢?
char a[4];
a是一个长度为4的字符数组,a是这个数组的首元素首地址。既然a是地址,pa是指向数组的指针,那么能将a赋值给pa吗?答案是不行的!因为a是数组首元素首地址,pa存放的却是数组首地址,a是char 类型,a+1,a的值会实实在在的加1,而pa是char[4]类型的,pa+1,pa则会加4,虽然数组的首地址和首元素首地址的值相同,但是两者操作不同,所以类型不匹配不能直接赋值,但是可以这样:pa = &a,pa相当与二维数组的行指针,现在它指向a[4]的地址。
备:以上红字是引用他人的观点,通过实践发现最后一段话所说的a不能直接赋值给pa是错误的观点,以下为我个人通过实践总结出的观点

数组指针的使用
举个简单的实践例子,用于抛砖引玉
以下深入理解左值和右值区别 有助于理解指针
int num=10;//定义变量的时候,左值num是地址,存储类型为int,右值是具体数据10,即num地址保存了一个int10
int *p=&num//定义指针变量的时候,左值*p是指针地址,存储类型是地址大小,而int是在*取值变量p操作时会根据int类型大小取出值,右值是num的地址,即指针地址上保存了一个地址
只要num和*p不是充当左值,而是充当右值或者读取变量或读取变量时的一系列操作(如*取值操作、加减等运算操作),那么num和p变量表示的就不是地址,可以是定义时等号右边的右值(即定义的num和p地址上保存的值),而*p则为取值操作。即也可以理解是降低了一个维度,num由变量地址降到了地址上对应的值,p指针变量地址降到了指针地址上对应的地址,*p是在指针地址降到对应的地址基础上再降低到地址上对应的值

int a[2]={1,2};//定义一个长度为2的一维数组 内存大小为2*int类型大小4=8字节
printf("%p\n",a+1);//按照int类型大小偏移 int类型大小4*偏移量1=4字节
printf("%p\n",&a+1);//按照一维数组类型大小偏移 数组长度2*int类型大小4*偏移量1=8字节
printf("%d\n", sizeof(a));//结果是8字节
printf("%d\n", sizeof(*a));//结果是4字节
printf("%d\n",sizeof(&a));//结果是4字节
printf("%d\n",sizeof(&a[0]));//结果是4字节
定义的时候a就是一维数组的首地址,读取变量a时就是一维数组中首元素的地址
如果定义的是多维数组,如三维数组的话,用*取值操作时可以降到二维数组首地址、一维数组首地址、一维数组首元素地址,而且三维数组首地址和二维数组首地址、一维数组首地址以及一维数组首元素地址是相同的地址,而不是取出地址上对应的值,只是表示的含义不同,所以这里的*取值操作实际就是含义上的降低维度,并非是降维到取出地址上对应的值。相反的有降维就会有升维,读取变量时&a就是从一维数组首元素地址升维到了一维数组首地址的含义。
小结:数组的降维和升维是含义上的降维和升维并没有取出地址对应的值,只有数组降到首元素地址再进行降维时,才是取出地址对应的值;sizeof计算变量所占的内存大小时,具有隐式添加&取地址功能(即先取出变量,再用&取出地址),所以 sizeof(a)中的a在右值或取值操作时是一维数组首元素地址,因为最后隐式添加&取地址,所以实际上是一维数组首地址,所占内存大小应为8字节。这就是sizeof计算内存大小的原理!
int a[2]={1,2};
int (*p)[2]=a;//定义一个一维数组指针
printf("%p\n", p+1);//偏移了8字节
printf("%p\n", *p+1);//偏移了4字节
这里已经定义好了长度为2的一维数组指针为接收类型,所以编译器是根据定义的类型操作变量,读取变量p降到了一维数组首地址,+1根据定义的一维数组长度偏移:长度2*int大小4*偏移量1=8字节;*p降到了一维数组的首元素地址,+1就是根据定义的是int类型的元素偏移:int大小4*偏移量1=4字节


int a[2]={1,2};
int (*p)[2]=&a;//编译器不会警告的完全兼容性写法,&a数组地址,a数组首元素地址
一维数组取值的两种方式,a[1]和*(a+1)
一维数组指针取值的两种方式,(*p)[1]和*(*p+1)
从上可以发现p等同于a,定义的时候p和a就是数组变量,加上*的*p就变成了数组指针。定义的a是一维数组首地址,p是一维数组指针地址,取值时变量a对应(降维)的是数组首元素地址,而变量p对应(降维)的是数组首地址,再加上*对应(降维)的是数组首元素地址。最后中括号的作用等同于根据下标索引偏移地址后,再用*取值操作取出地址上对应的值。其实中括号就是*这个取值操作符号的简写,便于人们理解
注:定义数组的时候中括号内的数值指的是数组长度,读取的时候中括号内的数值指的是下标索引,即地址的偏移量;中括号和*一样,在数组中降维的是含义不是取地址对应的值,当且只有在降维首元素地址时是取地址对应的值

这里开始进入复杂的实践例子,从上面的简单实例推出原理用于推理下面复杂的实例,从而真正掌握数组与指针的区别以及运用
int a[2][2][3] =
{
{{1,2,3},{4,5,6}},
{{7,8,9},{10,11,12}}
};

int (*p)[2][3]=&a[0];//&a[0]表示指向二维数组变量a的地址,这里无论赋值a[0]还是&a[0],后面不管是取值还是偏移地址都是一样的(因为是根据你定义的数组维度类型所决定的,而不是通过赋值,赋的是什么维度数组类型决定的,再说赋值只是赋一个地址根本没啥实际含义,无论你的地址存放的是什么类型的数组,我只根据自己定义的数组类型的指针操作你这个地址进行偏移或者取值)。只不过编译器会警告类型不兼容(因为&a[0]是二维数组首地址,a[0]是一维数组首地址,意义不一样,一个是二维数组类型;一个是一维数组类型),其实它们都是相同的地址。然而我们要的只是一个地址,与该地址上存储的是什么类型数据没有任何关系,在读取时用*取值操作时都只根据定义的类型去操作,并非是该地址上存储的类型。可以不必理会编译器警告

int (*p)[2][3]和int *p[2][3]类型的区别:
提示:()和[]优先级相同都属于一级优先级运算符,同级别优先级根据结合顺序计算,根据c语言的规则规定一级优先级是从左往右计算的,所以哪个在左哪个先算,*属于二级优先级所以比一级优先级的运算符都低

int *p[2][3]等价于int *(p[2][3]),可以根据运算符优先级得知int *p[2][3]是先定义p[2][3]数组然后是定义指针为整型,即定义了一组整型指针,也称指针数组,可理解为数组里存放了多个整型指针(在连续的内存中存放了多个整型指针)
注意所存的数据是一组指针地址,读取变量p进行偏移时,p是含义降维至数组中首元素地址,编译器根据定义的首元素地址是指针类型则偏移是按照地址大小偏移,32位系统偏移4,64位系统偏移8,在读取时用*取值操作p时会根据首元素地址取出对应的地址(即指针指向的地址)

int (*p)[2][3]也根据运算符优先级得知是先定义(*p)指针然后是定义数组为整型,即定义了指针指向整型数组,也称数组指针,理解为指针指向一个整型数组,而这个数组是长度为6个元素的二维数组
注意所存的数据是一个数组地址,在读取时用*取值操作p时是降维至(取出)数组地址,后地址偏移是根据定义的哪个维度数组就按照哪个维度数组大小偏移。
特殊情况例外,如果你定义的是二级指针二维数组,这时读取变量p时是降维到(取出)根据自身定义的一级指针二维数组类型的地址,这里取出的是定义时所赋值的地址,因为还是指针,所以偏移根据地址大小偏移。若再读取*取值操作p会根据一级指针二维数组地址降维至(取出)二维数组首地址,这时偏移就不是按照地址大小偏移,而是按照定义的二维数组大小偏移,还有就是在数组的基础上降维都是含义上的降维,并非取出地址对应的值。

实践如下:
printf("%d\n",*(p[1][0]));//取出的执行顺序:先读取p[1]时根据自身定义的二维数组类型的地址(即指向的地址)偏移后就是(含义降维至)一维数组地址,定义的二维数组长度2*3*sizeof(int)4*偏移量1=24字节大小,一维数组地址=&a[0]地址+24字节,后读取[0]根据一维数组的地址偏移后就是(含义降维至)数组首元素地址,首元素地址=一维数组地址24+0字节,总偏移量:24+0=24字节 再*()取出首元素地址对应的值,结果为7
printf("%d\n",(*p)[1][2]);//取出的执行顺序:先读取用*取值操作变量p含义降维至一维数组的地址,后读取[1]根据一维数组的地址偏移后就是(含义降维至)首元素地址  定义的一维数组长度3*sizeof(int)4*偏移量1=12字节偏移大小,首元素地址=一维数组地址+12字节,再[2]根据首元素地址偏移后取出元素地址对应的值 偏移后的元素地址=首元素地址+sizeof(int)4*偏移量2 总偏移量:12+8=20字节 结果为6
printf("%p\n", p );//表示指向的是自身定义的二维数组长度类型的首地址
printf("%p\n", *p);//表示取出指向一维数组的首地址
printf("%p\n", **p);//表示取出一维数组的首元素地址
printf("%p\n", ***p);//表示取出首元素地址对应的值
printf("%p\n", p+1);//偏移了24个字节大小的地址,因为读取变量p时是降维到(取出)自身定义的整型二维数组指针,所以根据定义int (*p)[2][3]得知二维数组大小是6个int字节大小,可得总偏移量=6*sizeof(int)=6*4*偏移量1=24字节,p+1地址=p地址+24字节大小
printf("%p\n", *p+1);//偏移了12个字节大小的地址,因为*p含义降维到了一维数组的指针,所以根据定义int (*p)[2][3]得知一维数组大小是3个int字节大小,可得总偏移量=3*sizeof(int)=3*4*偏移量1=12字节,*p+1地址=*p地址+12字节大小
小结:地址+十进制的正整型数值是根据读取变量指针是降维还是含义降维至自身定义的哪个类型就根据哪个类型大小*十进制整型数值(偏移量)得到偏移后的地址,而不是纯粹的与十进制整型数值相加

再举个高级难度形式的定义:
int (**p)[2][3]=&a[0];//定义一个二级指针二维数组
printf("%d\n",*(p[2][2]));//先读取p[2]根据自身定义的二级指针变量p指向(取出)一级指针地址,再偏移2后(降维至)取出二维数组首地址,结果地址为3,后[2]根据二维数组地址偏移2含义降维至一维数组首地址,偏移地址=二维数组长度2*3*int类型大小4*偏移量2=48字节,偏移后的地址就是下一维度的一维数组地址(即二维数组地址3+偏移48字节地址=一维数组地址51),再*()含义降维至一维数组首元素地址,没有偏移所以与一维数组首地址相同地址还是51.如果再来个**()则根据地址51降维(取出)对应的值,当然地址51上保存的值是读取不了的,普及下系统规定0-255是系统限制禁止访问读写的内存,就算可以读取也是一个乱码。

printf("%d\n",(*p)[0][2]);//先(*p)降维(取出)一级指针对应的二维数组首地址,结果地址为1,后读取[0]根据二维数组地址偏移0后含义降维至下一维的一维数组首地址,偏移地址=二维数组长度2*3*int类型大小4*偏移量0=0,一维数组首地址=二维数组地址1+偏移0=1,再读取[2]根据一维数组首地址偏移2含义降维至首元素地址,偏移地址=一维数组长度3*int类型大小4*偏移量2=24,首元素地址=一维数组地址1+偏移24=25

结论:指针数组严格上说其实不是指针,只是一个普通的指针类型数组变量,数组指针才是真正的指针;数组存放的是一组连续地址的值,数组指针存放的是一个地址; 读取时用*取值数组变量可以直接取出数组元素地址上的值,读取时用*取值数组指针是间接的根据自己定义的数组类型取出其他地址上所存放数组的值。 数组指针和指针数组就是把数组和指针联系在一起混合使用,而且通过*取值操作时,无论是数组还是数组指针,取出的可能是地址上对应的值,也可能是数组含义上的降维,地址不变,这是只有编译器认识的含义,它看得懂怎么定义怎么取值,最后我想到用反汇编看它怎么执行取值操作的,但然并卵,编译器直接把复杂的指针取值操作的偏移量计算成一个结果(直接算出地址偏移量大小)翻译给了汇编指令 根本不知道它是怎么处理的。上面是我个人从实践总结出来的理论,编译器对定义和取值的处理可能就是我总结的理论,也可能不是(与我理论的词汇和执行过程可能不完全一样,但是结果一定是一样的,就像数学的解题方法可以有千百种,但都能算出最终正确的结果;也像语文说的不管黑猫白猫,只要能抓到老鼠就是好猫),真想确保真实性的话,那要去看看c语言编译器的内部代码一切都会知晓了,我这也只是逆推过程!

指针数组作为main函数的形参
int main(int  argc,char* argv[]);
main函数是操作系统调用的,第一个参数标明argc数组的成员数量,argv数组是命令行参数的字符串数组(字符指针即是字符串)
argc代表命令行参数的数量,程序名字本身算一个参数
eg:
int main(int argc,char* argv[])
{
  if(argc<3)
  {
    printf("缺少参数\n");
    return -1;
  }
  for(int i=0;i<argc;i++)
  {
    prinf("%s\n",argv[i]);
  }
  return 0;
}

编译源文件生成exe程序 gcc -o 路径\程序名.exe 路径\程序名.c -std=c99 
其中c99是指定编译器版本,因为默认编译器是c89有些新语法并不支持
运行exe程序 路径\程序名.exe 参数1 参数2 参数3……

内存管理
作用域
C语言变量的作用域分为
代码作用域:代码块{},在函数代码块内使用{}叫匿名内部函数
函数作用域
文件作用域

局部变量
局部变量也叫auto自动变量,一般情况下代码快{}定义的变量都是自动变量,有如下特点:
在一个函数内定义,只在函数范围内有效
在复合语句中定义,只在复合语句中有效
随着函数调用的结束或复合语句的结束局部变量的生命周期也结束
如果没有赋初值,内容为随机

全局变量
在函数外定义,可被本文件及其他文件中的函数所共用,若其他文件中的函数调用此变量。需用extern声明
全局变量的生命周期和程序运行周期一样
不同文件的全局变量不可重名

静态局部变量
作用域:函数内部
生命周期:从程序创建到程序销毁 
存储位置:数据区

静态全局变量
作用域:定义所在的文件中
生命周期:从程序创建到程序销毁
存储位置:数据区

未初始化数据区
局部变量未初始化值为乱码
全局变量和静态变量未初始化值为0或者空NULL

全局函数
作用域:项目中所有文件
生命周期:从程序创建到程序销毁
存储位置:代码区

静态函数
作用域:定义所在文件中
生命周期:从程序创建到程序销毁
存储位置:代牧区

注意:
允许不同函数中可以使用相同变量名,它们代表不同的对象,分配不同的单元,互不干扰
统一源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用(就近原则)
所有的函数默认都是全局的,意味着所有的函数都不能重名,但如果是static函数,那么作用域是文件级的,所以不同的文件static函数名是可以相同的

代码区:程序执行二进制码(程序指令) 共享 只读的
数据区:初始化数据区、未初始化数据区、常量区
栈区:系统为每一个程序分配一个临时的空间,局部变量、函数参数、函数返回值、数组,栈区大小为1M,在windows中可以扩展到10M,在linux中可以扩展到16M
堆区:存储大数据,图片、音乐、视频,手动开辟 malloc,手动释放 free
注意:栈区常量和数据区常量区别,栈区常量可以通过指针修改,但数据区常量不行

注意:数组在栈区开辟的空间是从高地址到低地址,而存储数据是从低地址到高地址

堆空间的使用
eg:
int *p=(int*)malloc(sizeof(int)*10);//开辟堆空间,其中(int*)是强制类型转换,其实我想说的是这里的类型转换没啥鸟用,最终取出的数据是以接收类型int *p为导向,说白了此处是任意的类型转换都可以,只是个摆设便于程序员理解
for (int i = 0; i < 10; i++)
{
printf("%d\n",p[i]);//读取堆空间内容,不过是乱码
}
free(p);//释放堆空间,这里的p变为了野指针, 野指针指向一个已删除的对象或未申请访问受限内存区域的 指针
p=NULL;//避免野指针出现,防止被访问

内存处理函数
memset(p,0,40);//p是指针变量,40是字节大小,0是重置数据为0,如果p是int指针,重置数据为1的话,最后取出的内存数据是十六进制的01010101(按照int为4字节取出),所以它会按照指定字节大小将每个字节依据参数转化为2位的16进制数(一个内存地址对应一个字节,一个字节等于2位十六进制)

memcpy() 如果源地址和目标地址重叠可能会导致结果错误,即不是自己想要的结果,使用这个函数的时候只有程序员自己保证源地址和目标地址不重叠,或者使用memmove函数进行内存拷贝

mememove() 可以在内存重叠情况下使用,不影响结果,因为它会将源区域独自拷贝一份到一个新内存中,然后再拷贝到目标区域中,这样就不会发生内存重叠问题了

内存常见问题
数组下标越界:
char *p =(char*)malloc(sizeof(char)*11);
strcpy(p,“hello world”);//此字符串占用12字节包括“\0”,将该字符串拷贝到p指向的空间中导致运行时内存错误
printf("%s\n",p);
return 0;

野指针:
int *p=(int*)malloc(0);
printf("%p\n",p);
*p=100;
printf("%d\n",*p);
free(p);
return 0;

开辟空间和类型大小对应
int*p=(int*)malloc(10);
p[0]=123;
p[1]=456;
p[2]=789;
printf("%d\n",*p);
printf("%d\n",*(p+1));
printf("%d\n",*(p+2));
free(p);
return 0;

堆空间不允许多次释放
int*p=(int*)malloc(10);
free(p);
free(p);
return 0;

空指针允许多次释放
int*p=NULL;
free(p);
free(p);
return 0;

指针叠加 不断改变指针方向 释放会出错
int*p=(int*)malloc(10);
for(int i=0;i<10;i++)
{
    *p=i;
    p++;
}
free(p);
return 0;

二级指针对应的堆空间
int **p=(int**)malloc(sizeof(int*)*5);//开辟5个指针大小堆空间,存放的是一级指针地址
for(int i=0;i<5;i++)
{
    p[i]=(int*)malloc(sizeof(int)*3);//开辟3个int大小堆空间,存放的是整数
}
for(int i=0;i<5;i++)
{
    for(int j=0;j<3;j++)
    {
        scanf("%d",&p[i][j]);
    }
}
for(int i=0;i<5;i++)
{
    for(int j=0;j<3;j++)
    {
        printf("%d",p[i][j]);
    }
    printf("\n");
}
//释放内存由内向外,否则将无法释放内部的内存
for(int i=0;i<5;i++)
{
    free(p[i]);
}
free(p);

结构体定义
struct student
{
     char name[10];
     int age;
     int score;
};
结构体使用
struct student stu;//声明ms变量为自定义的mystruct结构体类型
stu.name="张三";//这里会报编译错误表达式必须是可修改的左值,因为name是数组名也是个常量不允许被赋值
strcpy(stu.name,"张三");//这里要使用拷贝赋值比较切合实际,另外使用下标一个个赋值也可以,但是赋值汉字时我们不可能一个个去查ASCII码,太麻烦了
stu.age=20;
stu.core=100;

结构体初始化赋值
struct student stu={“张三”,20,100};//这里初始化赋值可以直接赋值汉字就省去拷贝赋值操作了

全局变量结构体
struct student
{
     char name[10];
     int age;
     int score;
}stu;//这里的stu是作为全局变量的结构体

还可以直接赋值
struct student
{
     char name[10];
     int age;
     int score;
}stu={“张三”,20,100};



联合体定义和使用
union var
{
double a;
int b;
float c;
char d[10];
};

//联合体是一个能同一个存储空间存储不同类型数据的类型
//联合体长度是以最长成员的长度倍数决定,这里最长成员是double 8字节,而char d[10]为10字节, 
//所以是联合体长度是8*2=16字节
//联合体只对最后一个存入的成员类型有效,既数据完整性不受到影响,因为新存入的会覆盖现有的部分数据或全部数据,
//覆盖程度依据后存入的类型长度是否比之前的类型长度字节大
//联合体变量和它的成员的地址都是同一地址
union var test;
test.b = 20;
test.c = 3.14;//这里是最后存入的数据会影响前面存入的数据,所以读取该数据不会有误,而读取前面赋值的数据就有误了

枚举定义和使用
enum color
{
  blue,greed,pink,yellow
};
在枚举值中应列出所以的可用值,也称为me枚举元素
枚举值是常量,不允许在程序中用赋值语句对它进行赋值
枚举元素本身有系统定义了一个表示序号的数值从0开始s顺序定义0,1,2,4,...
可以给枚举值赋值整型常量,后面的数值根据所赋值的整型常量大小递增1
比如;
enum color
{
  blue,green=2,pink,yellow
};//整型常量分别为blue=0,green=2,pink=3,yellow=4

typedef的使用
主要用途 给类型取别名(实际上就是预处理期间替换代码的操作)
语法 typedef 原名 别名

1)简化struct关键字
typedef struct Person myPerson;
typedef struct Person
{
     char name[64];
     int age;
}myPerson;

2)区分数据类型
char * p1,p2;//类型分别是char* ,char
typedef char * PCHAR;//用typedef就可以表示p1 p2都是char*类型
PCHAR p1,p2;

3)提高代码移植性
typedf long long MYINT;//在不同系统下long long类型可能没有,这时只需要替换long long就可以了,提高了代码移植性
MYINT a;
MYINT b;

void的使用
1、无类型是不可以创建变量的
void a=10;//编译器直接报错,因为不知道给a分配多少内存空间
2、可以限定函数返回值
void func()
{
     return 10;//编译不报错,单独语句func();调用时会输出警告
}
3、限定函数参数列表
int func(void)
{
    return 10;//编译不报错,调用时会输出警告
}
4、void * 万能指针
void *p=NULL;
char * pChar=NULL;
pChar=p;//万能指针 可以不需要强制类型转换就可以给等号左边赋值

size的使用
1、sizeof本质不是函数,只是一个操作符,对于数据类型,sizeof必须使用(),但是对于变量,可以不加()
2、sizeof的返回值类型是unsigned int 无符号整型 当unsigned int和int类型数据做运算,编译器会将数据类型转换为unsigend
如 sizeof(int)-5,用做运算时,打印要用%u才显示无符号的整型,用%d显示的是负数
3、sizeof可以统计数组长度 当数组名作为函数参数时,会退化为指针,指向数组中第一个元素地址

变量的修改方式
1、直接修改
2、间接修改 通过指针对内存进行修改 对自定义数据类型进行修改

内存分区 
运行前:
代码区: 共享的、只读的;
数据区: data:已初始化的全局变量、静态变量、常量;bss:未初始化的全局变量、静态变量、常量
运行后:
栈区: 属于先进后出的数据结构 由编译器管理数据开辟和释放 变量的生命周期在该函数结束后自动释放掉
堆区:容量远远大于栈 没有先进后出这样的数据结构 由程序员管理开辟和管理释放

栈区注意事项
不要返回局部变量的地址,因为局部变量在函数执行之后就释放了,我们没有权限取操作释放后的内存

堆区的注意事项
在堆区开辟的数据,记得手动开辟,手动释放
如果在主调函数中没有给指针分配内存,那么被调用函数中需要利用高级指针给主调函数中指针分配内存,如下:
test03是主调函数

static和extern区别
static静态变量:编译阶段分配内存,只能在当前文件内使用,只初始化一次
extern全局变量,c语言下默认的全局变量前都隐藏的加了该关键字
const修饰的全局变量和局部变量
1、const修饰的全局变量,即使语法通过,但是运行时受到常量取的保护,运行失败
2、const修饰的局部变量
总结: 
全局变量 直接修改失败,间接修改失败 原因放在常量区,受到保护
局部变量 直接修改失败,间接修改成功 原因放在栈区
伪常量不可以初始化数组

字符串处理
不同的编译器可能有不同的处理方式
ANSI没有制定出标准
有些编译器可以修改字符串常量,有些不可以
有些编译器将相同的字符串常量看成同一个

宏函数
优点:以空间换时间

调用惯例

调用惯例(Calling Convention): 函数的调用方和被调用方对于函数如何调用需要有一个明确的约定,只有双方都遵守同样的约定,函数才能被正确的调用。

调用惯例一般会涉及到一下三个方面:

1 函数参数传递的顺序与方式

函数传递参数的方式有很多中,可以通过寄存器、栈和内存区域传递,不过最常见的是通过栈传递。函数的调用方先将参数压入栈中,函数自己再从栈中取出参数。对于有多个参数的函数,调用惯例要规定调用方将函数压入栈的顺序:是从左到右,还是从右到左。有些惯例还允许通过寄存器传递参数,以提高性能。

2 栈的维护方式

在函数压入栈之后,函数体会被调用,此后需要将被压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个工作既可以由调用方来做,也可以由函数来做。

3 名字修饰

为了链接的时候对调用管理进行区分,调用管理要对函数本身的名字进行修饰。不同的调用管理有不同的名字修饰策略。

      事实上,在c语言里,存在多个调用惯例,默认是cdecl。任何一个没有显示指定调用惯例的函数都是默认cdecl惯例,比如func函数的声明,它的完整写法是:int _cdecl func(int a ,int b),编译器编译时会自动加上

      注意:_cdecl不是标准的关键字,在不同编译器里可能有不同的写法,例如gcc里就不存在_cdecl这样的关键字,而是使用__attribute__ (( cdecl))

这里我们主要介绍的cdecl、stdcall、fastcall三种 c中 主要的调用惯例,还有pascal、naked call、thiscall调用管理。这三种调用的区别如下:

调用惯例 出栈方 参数传递 名字修饰
cdecl 函数调用方 从右至左压入栈 下划线+函数名
stdcall 函数本身 从右至左压入栈 下划线+函数名+@+参数的字节数
fastcall 函数本身

头两个DWORD(4字节)类型或者占更少

字节的参数被放入寄存器,其它剩下的参数

按从右至左的顺序压入栈

@+函数名+@+参数的字节数

变量传递分析
栈的生长方向以及内存存储方式
生长方向 栈底——高地址 栈顶——低地址
内存存储方式 高位节数据——高地址 低位节数据——低地址(小端对齐)

获取自定义数据类型中属性的偏移
offsetoff(结构体,属性)
还需引用头文件 #include<stddef.h>
eg:
输出结果:

指针做函数参数的输入输出特性
输入特性:主调函数中分配内存,被调函数中使用内存
eg:
test01是主调函数,func是被调函数
test02是主调函数,printString是被调函数
以上一个是栈区开辟的内存,一个是堆区开辟的内存
输出特性:被调函数分配内存,主调函数使用内存
eg:
test03主调函数,allocateSpace被调函数

内存对齐原则
1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,选取这两个中整数倍最小的那个值。
2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,选取这两个中整数倍最小的那个值。
3、结合1、2可推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。
4、#pragma pack默认对齐模数是8,也可以将对齐模数改为2的n次方
5、当结构体嵌套结构体时,只需要看子结构体中最大数据类型就可以了,即把它展开来看

函数指针的定义
1、先定义函数类型,再通过类型定义出函数指针
eg:
void func()
{
    printf("hello world\n");
}

1)
void test01()
{
typedef void(FUNC_TYPE)();
FUNC_TYPE *pFunc=func;
pFunc();
}

2)
void test02()
{
typedef void(*FUNC_TYPE)();
FUNC_TYPE pFunc=func;
pFunc();
}

2、直接定义函数指针变量
test03()
{
    void(*pFunc)()=func;
    pFunc();
}

函数指针的数组
void func1()
{
    printf("func1的调用\n");
}
void func2()
{
    printf("func2的调用\n");
}
void func3()
{
    printf("func3的调用\n");
}

void test04()
{
    //函数指针数组的定义
    void(*pFunc[3])();
    pFunc[0]=func1;
    pFunc[1]=func2;
    pFunc[2]=func3;
    for(int i=0;i<3;i++)
    {
         pFunc[i]();
    }
}

函数指针和指针函数的区别
函数指针是指向函数的指针
指针函数是函数的返回值是一个指针的函数

回调函数(函数指针做函数参数)
eg:
提供一个函数,可以将任意的数据类型打印出来
void printText(void *a,void(*myPrint)(void*))
{
       myPrint(a);
}

void myPrintInt(void * data)
{
    int *num=data;
    printf("%d\n",*num);
}

void test01()
{
    int a=100;
    printText(&a,myPrintInt);
}

struct Person
{
    char name[64];
    int age;
}
void myPrintPerson(void *data)
{
    strcut Person *p=data;
    printf("姓名:%s 年龄:%d\n",p->name,p->age);

}
void test02()
{
    struct Person p={"aaa",10};
    printText(&p,myPrintPerson);
}

以上test01和test02函数就是调用同一个PrintText函数实现不同的功能


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值