C语言难点分析整理

 

这篇文章主要是介绍一些在复习C语言的过程中笔者个人认为比较重点的地方,较好的掌握这些重点会使对C的运用更加得心应手。此外会包括一些细节、易错的地方。涉及的主要内容包括:变量的作用域和存储类别、函数、数组、字符串、指针、文件、链表等。一些最基本的概念在此就不多作解释了,仅希望能有只言片语给同是C语言初学者的学习和上机过程提供一点点的帮助。

变量作用域和存储类别:

了解了基本的变量类型后,我们要进一步了解它的存储类别和变量作用域问题。

变量类别子类别
局部变量静态变量(离开函数,变量值仍保留)
自动变量
寄存器变量
全局变量静态变量(只能在本文件中用)
非静态变量(允许其他文件使用)

换一个角度

变量类别子类别
静态存储变量静态局部变量(函数)
静态全局变量(本文件)
非静态全局/外部变量(其他文件引用)
动态存储变量自动变量
寄存器变量
形式参数

extern型的存储变量在处理多文件问题时常能用到,在一个文件中定义extern型的变量即说明这个变量用的是其他文件的。顺便说一下,笔者在做课设时遇到out of memory的错误,于是改成做多文件,再把它include进来(注意自己写的*.h要用“”不用<>),能起到一定的效用。static型的在读程序写结果的试题中是个考点。多数时候整个程序会出现多个定义的变量在不同的函数中,考查在不同位置同一变量的值是多少。主要是遵循一个原则,只要本函数内没有定义的变量就用全局变量(而不是main里的),全局变量和局部变量重名时局部变量起作用,当然还要注意静态与自动变量的区别。

函数:

对于函数最基本的理解是从那个叫main的单词开始的,一开始总会觉得把语句一并写在main里不是挺好的么,为什么偏择出去。其实这是因为对函数还不够熟练,否则函数的运用会给我们编程带来极大的便利。我们要知道函数的返回值类型,参数的类型,以及调用函数时的形式。事先的函数说明也能起到一个提醒的好作用。所谓形参和实参,即在调用函数时写在括号里的就是实参,函数本身用的就是形参,在画流程图时用平行四边形表示传参。

函数的另一个应用例子就是递归了,笔者开始比较头疼的问题,反应总是比较迟钝,按照老师的方法,把递归的过程耐心准确的逐级画出来,学习的效果还是比较好的,会觉得这种递归的运用是挺巧的,事实上,著名的八皇后、汉诺塔等问题都用到了递归。

例子:
long fun( int n)
{
long s;
if (n==1||n==2) s=2;
else s=n -fun(n-1) ;
return s ;
}
main()
{
printf(" %ld ",fun(4));
}

数组:

分为一维数组和多维数组,其存储方式画为表格的话就会一目了然,其实就是把相同类型的变量有序的放在一起。因此,在处理比较多的数据时(这也是大多数的情况)数组的应用范围是非常广的。

具体的实际应用不便举例,而且绝大多数是与指针相结合的,笔者个人认为学习数组在更大程度上是为学习指针做一个铺垫。作为基础的基础要明白几种基本操作:即数组赋值、打印、排序(冒泡排序法和选择排序法)、查找。这些都不可避免的用到循环,如果觉得反应不过来,可以先一点点的把循环展开,就会越来越熟悉,以后自己编写一个功能的时候就会先找出内在规律,较好的运用了。另外数组做参数时,一维的[]里可以是空的,二维的第一个[]里可以是空的但是第二个[]中必须规定大小。

冒泡法排序函数:
void bubble( int a[] , int n)
{
int i,j,k;
for (i=1,i<n;i++)
for (j=0;j< n-i-1; j++)
if (a[j]>a[j+1])
{
k=a[j];
a[j]=a[j+1];
a[j+1]=k;
}
}

选择法排序函数:
void sort( int a[] , int n)
{
int i,j,k,t;
for (i=0,i< n-1 ;i++)
{
k=i ;
for ( j=i+1 ;j<n;j++)
if (a[k]<a[j]) k=j ;
if ( k!=i )
{
t=a[i];
a[i]=a[k];
a[k]=t;
}
}
}

折半查找函数(原数组有序):
void search( int a[] , int n, int x)
{
int left=0,right=n-1,mid,flag=0;
while ((flag==0)&&(left<=right))
{
mid=(left+right)/2 ;
if (x==a[mid])
{
printf(" %d%d ",x,mid);
flag =1;
}
else if (x<a[mid]) right=mid-1;
else left=mid+1 ;
}
}

相关常用的算法还有 判断回文,求阶乘,Fibanacci数列,任意进制转换,杨辉三角形计算 等等 。

字符串:

字符串其实就是一个数组(指针),在scanf的输入列中是不需要在前面加“&”符号的,因为字符数组名本身即代表地址。值得注意的是字符串末尾的‘\0‘,如果没有的话,字符串很有可能会不正常的打印。另外就是字符串的定义和赋值问题了,笔者有一次的比较综合的上机作业就是字符串打印老是乱码,上上下下找了一圈问题,最后发现是因为

char *name;

而不是

char name[10];

前者没有说明指向哪儿,更没有确定大小,导致了乱码的错误,印象挺深刻的。

另外,字符串的赋值也是需要注意的,如果是用字符指针的话,既可以定义的时候赋初值,即

char *a="Abcdefg";

也可以在赋值语句中赋值,即

char *a;
a=" Abcdefg ";

但如果是用字符数组的话,就只能在定义时整体赋初值,即char a[5]={"abcd"};而不能在赋值语句中整体赋值。

常用字符串函数列表如下,要会自己实现:

函数作用函数调用形式备注
字符串拷贝函数strcpy(char*,char *)后者拷贝到前者
字符串追加函数strcat(char*,char *)后者追加到前者后,返回前者,因此前者空间要足够大
字符串比较函数strcmp(char*,char *)前者等于、小于、大于后者时,返回0、正值、负值。注意,不是比较长度,是比较字符ASCII码的大小,可用于按姓名字母排序等。
字符串长度strlen(char *)返回字符串的长度,不包括‘\0‘.转义字符算一个字符。
字符串型->整型atoi(char *)
整型->字符串型itoa(int,char *,int)做课设时挺有用的
sprintf(char *,格式化输入)赋给字符串,而不打印出来。课设时用也比较方便

注: 对字符串是不允许做==或!=的运算的,只能用字符串比较函数

指针:

指针可以说是C语言中最关键的地方了,其实这个“指针”的名字对于这个概念的理解是十分形象的。首先要知道,指针变量的值(即指针变量中存放的值)是指针(即地址)。指针变量定义形式中:基本类型 *指针变量名 中的“*”代表的是这是一个指向该基本类型的指针变量,而不是内容的意思。在以后使用的时候,如*ptr=a时,“*”才表示ptr所指向的地址里放的内容是a。

指针比较典型又简单的一应用例子是两数互换,看下面的程序,

swap( int c, int d )
{
int t;
t=c;
c=d;
d=t;
}
main()
{
int a=2,b=3;
swap( a,b );
printf(“%d,%d”,a,b);
}

这是不能实现a和b的数值互换的,实际上只是形参在这个函数中换来换去,对实参没什么影响。现在,用指针类型的数据做为参数的话,更改如下:

swap(#3333FF *p1, int *p2)
{
int t;
t=*p1;
*p1=*p2;
*p2=t;
}
main()
{
int a=2,b=3;
int *ptr1,*ptr2;
ptr1=&a;
ptr2=&b;
swap(prt1,ptr2);
printf(“%d,%d”,a,b);
}

这样在swap中就把p1,p2 的内容给换了,即把a,b的值互换了。

指针可以执行 增、减运算 ,结合++运算符的法则,我们可以看到:

*++s

取指针变量加1以后的内容

*s++取指针变量所指内容后s再加1
(*s)++指针变量指的内容加1

指针和数组 实际上几乎是一样的,数组名可以看成是一个常量指针,一维数组中ptr=&b[0]则下面的表示法是等价的:

a[3]等价于*(a+3)
ptr[3]等价于*(ptr+3)

下面看一个用指针来自己实现atoi(字符串型->整型)函数:

int atoi( char *s)
{
int sign=1,m=0;
if (*s==‘+‘||*s==‘-‘) /*判断是否有符号*/
sign=(*s++==‘+‘ )?1:-1; /*用到三目运算符*/
while ( *s!=‘\0‘ ) /*对每一个字符进行操作*/
{
m=m*10+(*s-‘0‘);
s++; /*指向下一个字符*/
}
return m*sign;
}

指向多维数组的指针变量也是一个比较广泛的运用。例如数组a[3][4],a代表的实际是整个二维数组的首地址,即第0行的首地址,也就是一个指针变量。而a+1就不是简单的在数值上加上1了,它代表的不是a[0][1],而是第1行的首地址,&a[1][0]。

指针变量常用的用途还有把指针作为参数传递给其他函数,即 指向函数的指针 。
看下面的几行代码:

void Input(ST *);
void Output(ST *);
void Bubble(ST *);
void Find(ST *);
void Failure(ST *);
/*函数声明:这五个函数都是以一个指向ST型(事先定义过)结构的指针变量作为参数,无返回值。*/

void (*process[5])(ST *) ={Input,Output,Bubble,Find,Failure};
/*process被调用时提供5种功能不同的函数共选择(指向函数的指针数组)*/

printf( "\nChoose:\n?" );
scanf( "%d" ,&choice);
if (choice>=0&&choice<=4)
(*process[ choice ])(a); /*调用相应的函数实现不同功能*;/

总之,指针的应用是非常灵活和广泛的,不是三言两语能说完的,上面几个小例子只是个引子,实际编程中,会逐渐发现运用指针所能带来的便利和高效率。

文件:

函数调用形式说明
fopen("路径","打开方式")打开文件
fclose(FILE *)防止之后被误用
fgetc(FILE *)从文件中读取一个字符
fputc(ch,FILE *)把ch代表的字符写入这个文件里
fgets(FILE *)从文件中读取一行
fputs(FILE *)把一行写入文件中
fprintf(FILE *,"格式字符串",输出表列)把数据写入文件
fscanf(FILE *,"格式字符串",输入表列)从文件中读取
fwrite(地址,sizeof(),n,FILE *)把地址中n个sizeof大的数据写入文件里
fread(地址,sizeof(),n,FILE *)把文件中n个sizeof大的数据读到地址里
rewind(FILE *)把文件指针拨回到文件头
fseek(FILE *,x,0/1/2)移动文件指针。第二个参数是位移量,0代表从头移,1代表从当前位置移,2代表从文件尾移。
feof(FILE *)判断是否到了文件末尾

文件打开方式说明
r打开只能读的文件
w建立供写入的文件,如果已存在就抹去原有数据
a打开或建立一个把数据追加到文件尾的文件
r+打开用于更新数据的文件
w+建立用于更新数据的文件,如果已存在就抹去原有数据
a+打开或建立用于更新数据的文件,数据追加到文件尾

注: 以上用于文本文件的操作,如果是二进制文件就在上述字母后加“b”。

我们用文件最大的目的就是能让数据保存下来。因此在要用文件中数据的时候,就是要把数据读到一个结构(一般保存数据多用结构,便于管理)中去,再对结构进行操作即可。例如,文件aa.data中存储的是30个学生的成绩等信息,要遍历这些信息,对其进行成绩输出、排序、查找等工作时,我们就把这些信息先读入到一个结构数组中,再对这个数组进行操作。如下例:

#include <stdio.h>
#include <stdlib.h>
#define N 30

typedef struct student /*定义储存学生成绩信息的数组*/
{
char *name;
int chinese;
int maths;
int phy;
int total;
} ST ;

main()
{
ST a[N] ; /*存储N个学生信息的数组*/
FILE *fp ;
void (*process[3])(ST *) ={Output,Bubble,Find}; /*实现相关功能的三个函数*/
int choice,i=0;
Show();
printf( "\nChoose:\n?" );
scanf( "%d" ,&choice);
while (choice>=0&&choice<=2)
{
fp=fopen( "aa.dat" , "rb" ) ;
for (i=0; i<N ;i++)
fread(&a[i], sizeof (ST),1,fp); /*把文件中储存的信息逐个读到数组中去*/
fclose(fp);
(*process[choice]) (a ); /*前面提到的指向函数的指针,选择操作*/
printf( "\n" );
Show();
printf( "\n?" );
scanf( "%d" ,&choice);
}
}

void Show()
{
printf( "\n****Choices:****\n0.Display the data form\n1.Bubble it according to the total score\n2.Search\n3.Quit!\n" );
}

void Output( ST *a ) /*将文件中存储的信息逐个输出*/
{
int i,t=0;
printf( "Name Chinese Maths Physics Total\n" );
for (i=0;i<N;i++)
{
t=a[i].chinese+a[i].maths+a[i].phy;
a[i].total=t;
printf( "%4s%8d%8d%8d%8d\n" ,a[i].name,a[i].chinese,a[i].maths,a[i].phy,a[i].total);
}
}

void Bubble( ST *a) /*对数组进行排序,并输出结果*/
{
int i,pass;
ST m;
for (pass=0;pass <N-1 ;pass++)
for (i=0;i <N-1 ;i++)
if (a[i].total<a[i+1].total)
{
m=a[i]; /*结构互换*/
a[i]=a[i+1];
a[i+1]=m;
}
Output( a );
}

void Find (ST *a)
{
int i,t=1;
char m[20];
printf( "\nEnter the name you want:" );
scanf( "%s" ,m);
for (i=0;i<N;i++)
if ( !strcmp(m,a[i].name)) /*根据姓名匹配情况输出查找结果*/
{
printf( "\nThe result is:\n%s, Chinese:%d, Maths:%d, Physics:%d,Total:%d\n" ,m,a[i].chinese,a[i].maths,a[i].phy,a[i].total);
t=0 ;
}
if ( t )
printf( "\nThe name is not in the list!\n" );
}

链表:
链表是C语言中另外一个难点。牵扯到结点、动态分配空间等等。用结构作为链表的结点是非常适合的,例如:

struct node
{
int data;
struct node *next;
};

其中next是指向自身所在结构类型的指针,这样就可以把一个个结点相连,构成链表。

链表结构的一大优势就是动态分配存储,不会像数组一样必须在定义时确定大小,造成不必要的浪费。用malloc和free函数即可实现开辟和释放存储单元。其中,malloc的参数多用sizeof运算符计算得到。

链表的基本操作有: 正、反向建立链表;输出链表;删除链表中结点;在链表中插入结点 等等,都是要熟练掌握的,初学者通过 画图 的方式能比较形象地理解建立、插入等实现的过程。

typedef struct node
{
char data;
struct node *next;
} NODE ; /*结点*/

正向建立链表:
NODE *create()
{
char ch= ‘a‘ ;
NODE *p,*h=NULL,*q=NULL;
while (ch< ‘z‘ )
{
p= (NODE *)malloc( sizeof (NODE)) ; /*强制类型转换为指针*/
p->data=ch;
if (h==NULL) h=p;
else q->next=p ;
ch++;
q=p;
}
q->next=NULL; /*链表结束*/
return h;
}

逆向建立:

NODE *create()
{
char ch= ‘a‘ ;
NODE *p,*h=NULL;
while (ch<= ‘z‘ )
{
p= (NODE *)malloc( sizeof (NODE)) ;
p->data=ch;
p->next=h; /*不断地把head往前挪*/
h=p;
ch++;
}
return h;
}

用递归实现链表逆序输出:

void output(NODE *h)
{
if (h!=NULL)
{
output(h->next) ;
printf( "%c" ,h->data);
}
}

插入结点(已有升序的链表):

NODE *insert(NODE *h, int x)
{
NODE * new ,*front,*current=h;
while (current!=NULL&&(current->data<x)) /*查找插入的位置*/
{
front=current;
current=current->next;
}
new = (NODE *)malloc( sizeof (NODE)) ;
new ->data=x;
new ->next=current;
if (current==h) /*判断是否是要插在表头*/
h= new ;
else front->next= new ;
return h;
}

删除结点:

NODE * delete (NODE *h, int x)
{
NODE *q,*p=h;
while (p!=NULL&&(p->data!=x))
{
q=p;
p=p->next;
}
if (p->data==x) /*找到了要删的结点*/
{
if (p==h) /*判断是否要删表头*/
h=h->next;
else q->next=p->next;
free(p); /*释放掉已删掉的结点*/
}
return h;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目录 1. C 语言中的指针和内存泄漏 5 2. C语言难点分析整理 10 3. C语言难点 18 4. C/C++实现冒泡排序算法 32 5. C++中指针和引用的区别 35 6. const char*, char const*, char*const的区别 36 7. C中可变参数函数实现 38 8. C程序内存中组成部分 41 9. C编程拾粹 42 10. C语言中实现数组的动态增长 44 11. C语言中的位运算 46 12. 浮点数的存储格式: 50 13. 位域 58 14. C语言函数二维数组传递方法 64 15. C语言复杂表达式的执行步骤 66 16. C语言字符串函数大全 68 17. C语言宏定义技巧 89 18. C语言实现动态数组 100 19. C语言笔试-运算符和表达式 104 20. C语言编程准则之稳定篇 107 21. C语言编程常见问题分析 108 22. C语言编程易犯毛病集合 112 23. C语言缺陷与陷阱(笔记) 119 24. C语言防止缓冲区溢出方法 126 25. C语言高效编程秘籍 128 26. C运算符优先级口诀 133 27. do/while(0)的妙用 134 28. exit()和return()的区别 140 29. exit子程序终止函数与return的差别 141 30. extern与static存储空间矛盾 145 31. PC-Lint与C\C++代码质量 147 32. spirntf函数使用大全 158 33. 二叉树的数据结构 167 34. 位运算应用口诀和实例 170 35. 内存对齐与ANSI C中struct内存布局 173 36. 冒泡和选择排序实现 180 37. 函数指针数组与返回数组指针的函数 186 38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针 224 46. 标准C中字符串分割的方法 228 47. 汉诺塔源码 231 48. 洗牌算法 234 49. 深入理解C语言指针的奥秘 236 50. 游戏外挂的编写原理 254 51. 程序实例分析-为什么会陷入死循环 258 52. 空指针究竟指向了内存的哪个地方 260 53. 算术表达式的计算 265 54. 结构体对齐的具体含义 269 55. 连连看AI算法 274 56. 连连看寻路算法的思路 283 57. 重新认识:指向函数的指针 288 58. 链表的源码 291 59. 高质量的子程序 295 60. 高级C语言程序员测试必过的十六道最佳题目+答案详解 297 61. C语言常见错误 320 62. 超强的指针学习笔记 325 63. 程序员之路──关于代码风格 343 64. 指针、结构体、联合体的安全规范 346 65. C指针讲解 352 66. 关于指向指针的指针 368 67. C/C++ 误区一:void main() 373 68. C/C++ 误区二:fflush(stdin) 376 69. C/C++ 误区三:强制转换 malloc() 的返回值 380 70. C/C++ 误区四:char c = getchar(); 381 71. C/C++ 误区五:检查 new 的返回值 383 72. C 是 C++ 的子集吗? 384 73. C和C++的区别是什么? 387 74. 无条件循环 388 75. 产生随机数的方法 389 76. 顺序表及其操作 390 77. 单链表的实现及其操作 391 78. 双向链表 395 79. 程序员数据结构笔记 399 80. Hashtable和HashMap的区别 408 81. hash 表学习笔记 410 82. C程序设计常用算法源代码 412 83. C语言有头结点链表的经典实现 419 84. C语言惠通面试题 428 85. C语言常用宏定义 450

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值