结构与指针
定义:
/* 定义 */ struct info // 定义了一个info结构,里面有两个成员num和name[5] { short num; // 两个成员的类型分别为short型和char型 char name[5]; }
struct info myinfo1,myinfo2,*p_info1,*p_info2; // 定义两个结构变量和两个指针变量 p_info1=&myinfo1; p_info2=&myinfo2; // 对指针变量进行初始化,让其分别指向结构变量的地址 p_info1->num=2; // 修改p_info1中的num值为2(用指针来访问对象所指的成员,用的是成员指针访问符->) strcpy((*p_info1).name,”good”); // 将”good”拷贝到p_info1中的name[5]中(用结构成员点的访问方式:用*p_info1访问myinfo1,再访问myinfo1中的name) *p_info2=*p_info1; // 把*p_info1中的全部赋给*p_info2(结构变量可以整体相互赋值)
请问:p_info2->num和p_info2->name[3]的值分别是多少?
答:p_info2->num=2;p_info2->name[3]=d;
请问:如果上面修改为p_info2=p_info1有什么影响?
答:这样p_info2不再指向myinfo2,而是和p_info1一样,也指向myinfo1!此时myinfo2中的数据是未初始化的,是乱的,因此会出错!
*p_info2=*p_info1和p_info2=p_info1的区别在于:
(1) 前者是把指针变量1指向的地址中的内容给指针变量2指向的地址;
(2) 后者是修改了指针变量本身中的内容,即把指针变量指向的地址修改了。
- 存取结构成员的方式:
(1) 通过结构变量名;
(2) 通过指向结构的指针和间接运算符(*);
(3) 通过指向结构的指针和指向成员运算符(->)。
- 等价引用:
myinfo.num
(*p_info).num
p_info->num
注意:操作符->只能使用在结构指针指向结构变量的情况下!
二重指针(指向指针的指针)
- 定义:
如果在一个指针变量中存放的是另一个变量的指针的地址,称该指针为指向指针的指针,即二重指针。
例如: int v=120, *q=&v, **p=&q; // q指针指向变量v,p二重指针指向指针q。即:**p=*(*p)=*q=v; // 访问v可用v, *q **p!
例1:类型不匹配的问题。
main() { int x,*q,**p; x=10; q=&x; p=&x; // 会出错!(类型不匹配:x的地址是变量的地址,不是地址的地址) printf(“%d”,**p); }
例2:指针数组和二重指针。
char *pc[]={“aaa”,”sss”,”ddd”}; char **ppc; ppc=pc; // pc是数组首地址,定义时再加一个*表示它“相当于”二重指针(即指针数组的第一个元素也是一个指针)
请问:ppc和pc可以相互赋值吗?
答:可以!类型匹配可以相互赋值!(pc是一个指针数组,pc是数组名,数组名本身就是地址,再加一个*就表示是一个二重指针)
心得:指针数组相当于二重指针!数组名“相当于”指针:因为数组名和指针一样都是表示地址,而“相当于并不真的是”表示数组名是常量。(参考:点击此处查看)
例3:用指针型指针输出数组元素。
/* 用指针型指针输出数组元素 */ /* 二重指针和指针数组结合使用 */ #include<stdio.h> #include<stdlib.h> int main() { int a[5]={1,3,5,7,9}; int *num[5]={&a[0],&a[1],&a[2],&a[3],&a[4]}; // 定义指针数组 int **p,i; // 定义二重指针 p=num; // 二重指针指向数组的首地址 for(i=0;i<5;i++) { printf("%d\t",**p); // 注意**p才是数据,*p还是地址! p++; } system("pause"); return 0; }
字符指针(操作字符串)
char *pch1=buffer1; // buffer1是一个字符数组,字符指针pch1指向该字符数组
printf(“%s”,pch1); // 输出字符串(输出字符指针,就是输出指针指向的字符串)
printf(“%c”,*pch1); // 输出字符指针指向的目标对象,就是输出指针指向的单个字符
区分:
printf—pch1(输出字符指针)
printf—*pch1(输出字符指针指向的对象)
char *p1=”good”;
char *p2=”good”;
printf(“%d”,p1-p2); // 输出值为0
解释:
因为字符串常量存储在数据区的const常量区,同一个字符串常量在内存中存在同一块内存,因此p1和p2都指向同一个”good”。
动态内存分配与释放——用指针按需分配、释放空间
- C语言中变量的内存分配方式:
(1)从静态存储区分配:全局变量和静态变量。
(2)栈(stack):函数参数值、局部变量等、以及执行函数调用时,系统在栈上为函数内的局部变量及形参分配内存,函数之行结束时,自动释放这些内存。
(3)堆(heap):程序运行期间,用动态内存分配函数来申请的内存都是从堆上分配的,动态内存的生存期是由程序员自己决定。
- 动态内存分配函数:
头文件 #include<stdlib.h>
(1) void* malloc(unsigned int size); // 向系统申请大小为size的内存块,把首地址返回,若申请不成功,则返回NULL。
malloc函数只有一个参数,就是要分配多大空间的字节数(需要自己计算)
int *p=(int *)malloc(10*sizeof(int)); // 分配10个整数空间
(2) void* calloc(unsigned int num,unsigned int size); // 向系统申请大小为num*size的内存块,把首地址返回,若申请不成功,则返回NULL。
calloc函数有两个参数,第一个参数是分配的元素的个数,第二个参数是每个元素占据的字节数,size这个参数是由sizeof()函数获得所需字节数,即size=sizeof()。因此不需要自己计算。
int *p=(int *)calloc(10,sizeof(int)); // 分配10个整数空间
注意:使用内存空间前,一定要判断是否分配成功!
if(p==NULL) // 使用分配的地址前,一定要判断是否分配成功! { printf(“malloc error\n”); return; }
分配1个整数(4个字节空间)
p=(int *)malloc(1*sizeof(int)); // 因为定义时的函数类型是void(空类型),因此通常需要用强制数据类型转换(int *)转换成所需的类型! p=(int *)calloc(1,sizeof(int)); // 强制转换成整型空间!
注意:void*不等于NULL!
NULL是空指针(无地址),在C中,NULL通常取0,常表示指针不指向任何地方。
void*表示指针指向的目标对象无数据类型,但它是有地址的(无数据类型)!
void*类型的指针可以指向任意类型的变量,通常强转(Type*)为其他类型。
例4:分配10个整数空间,并把1~10存储在这个空间内。
/* 分配10个整数空间,并把1~10存储在这个空间内 */ #include<stdio.h> #include<stdlib.h> int main() { int *p=(int*)malloc(10*sizeof(int)); // 先分配一块连续的整型空间,p指向空间的首地址 if(p==NULL) // 判断是否分配成功! { printf("内存分配失败"); return; } for(int i=0;i<10;++i) // 用p指针一个一个地存储数据 *(p+i)=i+1; system("pause"); return 0; }
- 动态内存释放函数:void free(void* p);
(1)释放由malloc或calloc申请的内存块;
(2)p是指向该内存的指针;
(3)free之后,系统标记p指针指向的这块内存没有被占用,可被重新分配;
(4)free之后,强制令p=NULL,避免后面误用这块内存。
例5:动态内存的分配与释放。
/* 动态内存分配与释放 */ #include<stdio.h> #include<stdlib.h> int main() { int *p=NULL,n,i; // 定义整型指针p double aver,sum; printf("How many students?"); scanf("%d",&n); // 获取要分配的个数 p=(int *)malloc(n*sizeof(int)); // p指针指向空间的首地址 if (p==NULL) { printf("No enough memory!\n"); exit(1); // 异常退出 } for(i=0;i<n;++i) { scanf("%d",&p[i]); // 读数据:读到p指针所指空间中 sum+=p[i]; // 求和 } aver=sum/n; // 计算平均值 printf("aver=%.1f\n",aver); free(p); // 使用完毕释放空间 system("pause"); return 0; }
例6:内存空间使用完毕,一定要释放!(malloc/calloc和free一定要成对使用!)
/* 内存空间不再使用时,一定要释放! */ #include<stdio.h> #include<stdlib.h> #define N 100000 int main() { int *p,i=0; while(1) { p=(int *)malloc(N*sizeof(int)); printf("%p ",p); *p=i++; if(p=NULL) break; } system("pause"); return 0; }
例7:指针在使用前一定要进行初始化!
/* 使用前要进行初始化 */ #include<stdio.h> #include<stdlib.h> int main() { int *p,i=0; p=(int *)malloc(sizeof(int)); if(p) printf("%d",*p); system("pause"); return 0; }
例8:指针的越界问题。
/* 指针越界 */ #include<stdio.h> #include<stdlib.h> #define N 10 int main() { int *p,i=0; p=(int *)malloc(N*sizeof(int)); // 分配了10个整数空间 if(p) { for(i=0;i<=N;++i,++p) // 输出11个(越界),但系统不会提示越界错误! { *p=i+1; printf("%d ",*p); } } system("pause"); return 0; }
例9:释放内存后仍继续使用内存空间,造成错误!
/* 释放内存后仍继续使用 */ #include<stdio.h> #include<stdlib.h> #define N 10 int main() { int *p,i=0; p=(int *)malloc(N*sizeof(int)); if(p) { for(i=0;i<N;++i,++p) { *p=i+1; printf("%d ",*p); } } free(p); *p=3; // 运行程序会报错!因为使用了不属于我们的内存空间。因此free之后一定记得令p=NULL,使用前再判断是否p==NULL! system("pause"); return 0; }
- 常见的内存错误:
(1)内存分配不成功,就进行使用;(用if判断)
(2)内存分配成功,但使用前没有进行初始化;(定义时令p=NULL)
(3)内存分配成功并初始化,但发生越界使用;(仔细检查)
(4)申请内存后没有及时释放;(注意配对使用的问题)
(5)释放内存后仍然继续使用。(注意free之后跟上p=NULL)
- 关于内存分配编程的建议原则:
(1)需要的时候才使用malloc(或calloc);
(2)malloc(或calloc)和free要配对使用,malloc(或calloc)在函数入口,free在函数出口;
(3)使用malloc(或calloc)时要检查函数返回值(用if判断是否是NULL);
(4)使用free函数之后,将指针设置为NULL;
(5)不要把局部变量的地址作为函数返回值返回。(函数结束后,局部变量和形参空间会被释放掉,因此如果把局部变量的地址作为函数返回值返回就相当于使用了free之后的内存空间一样,使用了不属于我们的空间!)
例10:找错误并修改。
/* 找出其中错误 */ #include<stdio.h> #include<stdlib.h> #define N 10 int main() { char data[]="There are some mistakes in the program"; char *point; char array[30]; int i,length; length=0; while(data[length]!='\0') length++; for(i=0;i<length;i++,point++) *point=data[i]; // point指针没有初始化就进行使用,会造成问题! // array数组长度30小于data存储的字符串长度 array=point; // array是数组名,是常量,不能被修改,不能被赋值! printf("%s\n",array); system("pause"); return 0; }
/* 作如下修改 */ #include<stdio.h> #include<stdlib.h> #define N 10 int main() { char data[]="There are some mistakes in the program"; char *point; char array[100]; // 定义一个数组 int i,length; length=0; while(data[length]!='\0') // 求字符串的长度 length++; point=array; // 给指针赋首地址 for(i=0;i<length;i++,point++) // 输出字符串 *point=data[i]; printf("%s\n",array); system("pause"); return 0; }
例11:使用指针,合并字符串s1和s2。
思路:
输入:两个字符串
输出:合并后的结果
算法分析:
两个字符指针分别指向两个字符串s1和s2。
指针p循环访问s1的字符,直到s1字符串尾部。
指针q依次访问字符串s2,直到s2访问到尾部,每访问一个字符,作如下操作:*p++=*q++(*q赋给*p,然后两个指针都做自增操作,一个语句完成了两个操作)。
/* 使用指针,合并字符串s1和s2 */ #include<stdio.h> #include<stdlib.h> int main() { char s1[50],s2[20]; scanf_s("%s%s",s1,20,s2,20); // 输入两个字符串 char *p=s1,*q=s2; while(*p!='\0') // 计算s1的长度 p++; // 第一个while循环结束,p此时指向字符串结束符 while(*q!='\0') // 将s2合并到s1的后面 *p++=*q++; // 将*q中的内容赋给*p,并且都自增 p='\0'; // 结束后,将字符串结束符拷贝到合并后的字符串最后面 printf("%s\n",s1); system("pause"); return 0; }
疑问:为什么后面会有“烫烫烫……”???
例12:输入两个字符串string1和string2,检查在string1中是否包含有string2。如果有,则输出string2在string1中的起始位置;如果没有,则输出“NO”;如果string2在string1中多次出现,则输出在string1中出现的次数以及每次出现的起始位置。
算法分析:
(1)需要对string1和string2进行循环,但不一定同时进行;
(2)string1开始循环后,判断string2的首字母是否与string1循环到的字母相同,若相同则两字符串同时进行循环,并记录开始的位置,然后判断string2中后面的字母是否和string1中的相同,不同则停止,相同则继续,直到string2循环完成。
(3)当前位置减去string2的长度就是string2的起始位置;
(4)string2可能多次出现,那么需要一个数组来保存多次出现的位置。
/* 检查在string1中是否包含有string2 */ #include<stdio.h> #include<stdlib.h> void main() // 访问类型为int时,可以省略int。如果是Dev C++,一定是int,VS下可以是void! // 改成void类型之后,最后不再加return返回值! { char string1[100],string2[10]; // 定义两个字符数组,长度分别不超过100和10 char *p,*q; // 定义指针遍历字符串 int locat[10]; // 定义数组保存出现的位置(假定2出现在1中的次数不超过10次) int j,len2,i=0,posit=0; // len2表示第二个字符串的长度 printf("请输入字符串1:\n"); gets_s(string1); // VS 2010之后,都采用加下划线的!和scanf_s一样! printf("请输入字符串2:\n"); gets_s(string2); q=string2; // q指向string2的首地址 for(j=0;*q!='\0';j++,q++) // 求第二个字符串的长度 len2=j+1; p=string1; // 指针赋首地址 q=string2; do // 循环进行判断 { if(*p!=*q) // 字符不相同的情况 { p++; posit++; } // 第一个字符相同,继续对后面字符进行判断 else { while((*q!='\0')&&(*q==*p)) { q++; p++; posit++; } if(*q=='\0') // 第二个字符串循环结束 { locat[i]=posit-len2; // 当前位置posit减去第二个字符串的长度就是在第一个字符串的起始位置??? i++; } } q=string2; // 结束一次循环则重新赋值为string2的首地址,进行多次重复循环 }while(*p!='\0'); // 一定要有分号! if(i<1) printf("NO"); else { printf("字符串1:%s\n字符串2:%s\n",string1,string2); printf("出现%d次,起始位置分别是:",i); for(j=0;j<i;j++) // 输出位置 printf("%d ",locat[j]); printf("\n"); } system("pause"); //return 0; }
getchar()函数:读取一个字符。
gets_s()函数:读取一个字符串,可无限读取,以回车结束,换行符会被丢弃,并在末尾加’\0’结束符
请问:输入字符串时,scanf_s和gets_s的区别?
答:scanf_s输入字符串时,如果中间有空格,我们会把它当做两个字符串输入(scanf_s会认为输入空格表示字符串结束)。如果想让它是一个字符串,推荐用gets_s函数(只有输入回车换行才认为字符串结束)。
疑问:为什么当前位置减去string2的长度就是起始位置?