资源描述
C语言基础学习笔记
(第一版修改)
丁炳亮
1数据类型和表达式
1.1计算机内数据存储方式
理解与测试:
计算机内部所有的数据都是以二进制的形式存储的。我们平常使用的有理数都是分正负数的,在计算机内部整型是通过最高位区分正负数,负数的最高位为1。如果直接用这种表达方式进行运算,那么运算前要先判断是无符号数还是有符号数,因为有符号数的最高位是符号位不能直接参与运晕,并且减法运算还得有专门的减法器。为了能把符号位参与运算且加法和减法能够统一处理我们用到了补码。
什么是补码?我们得先知道模的概念。模“模”是指一个计量系统的计数范围。如时钟等。计算机也可以看成一个计量机器,它也有一个计量范围,即都存在一个“模”。例如:
时钟的计量范围是0~11,模=12。表示n位的计算机计量范围是0~2^(n)-1,模=2^(n)。
“模”实质上是计量器产生“溢出”的量,它的值在计量器上表示不出来,计量器上只能表示出模的余数。任何有模的计量器,均可化减法为加法运算。例如:假设当前时针指向10点,而准确时间是6点,调整时间可有以下两种拨法:一种是倒拨4小时,即:10-4=6;另一种是顺拨8小时:10+8=12+6=6在以12模的系统中,加8和减4效果是一样的,因此凡是减4运算,都可以用加8来代替。对“模”而言,8和4互为补数。实际上以12模的系统中,11和1,10和2,9和3,7和5,6和6都有这个特性。共同的特点是两者相加等于模。
二进制中整数的补码求法是:正数的补码为原码,负数的补码是符号位不变其他位全部取反再整个数加1。我们可以通过下面的代码看下负整数在计算机内部的表示。
void f(int n)
{
unsigned int i;
for(i=1,i<<=15;i;i>>=1)
{
if(i&n)
printf("1");
else
printf("0");
}
printf("\n");
}
main()
{
int a=-0xff;
f(a);
getch();
}
输出的结果是1111111100000001。
1.2变量与常量
理解与测试:
1)类型声明
在计算机内部数据是以字节为单位存储的,但是我们需要的数据类型有很多种,每种数据类型所占字节和存储方式都不一样。类型声明的作用就是告诉计算机以哪种“格式”去读写该数据数据。
类型说明符 变量名1,变量名2......,变量名n;
类型说明符有基本类型(字符型、整数型、单/双精度型)、高级类型(结构体型、共用体型、枚举类型)、指针型等,其中指针类型包括指向基本类型的指针类型、指向高级类型的指针型和指向函数的指针类型(指向函数指针声明格式在后面章节)。
2)符号常量的定义
#define 标识符 常量;
使用该种方法定义符号常量一个是可以使代码便于阅读理解,另一个是方便代码内部多个相同常量的统一修改。
总结与注意
在写计算式的时候要考虑变量是否会越界。一般来说计算式子时是先强制转换成式子中最大存储空间的数据类型(不包括被赋值的变量),还要注意不同的类型数据在不同的编译器中所占的内存有可能是不一样的,例如有些编译器整型是占2个字节有些是占4个字节。同时还要考虑到符号的优先级和结合顺序,如果按符号的优先级和结合顺序运算过程中有越界的那么整个计算结果可能和预想的不一样了,例如int i=100;i = 500*i/i;最后i=-155。
1.3输出输入
理解与测试:
1)格式化输入输出函数可以按设定的格式和设定的数据类型接收和输出多个变量。控制格式和数据类型的是数据控制符。
2)字符和字符串可以用专门的输出\输入函数。主要有getch(); putch();getchar(); putchar(); gets(); puts();其中getch()和putch()是conio.h中声明,getch()不需要等待回车键就返回字符并立即执行下面一语句符,getch()不在屏幕显示输入的字符。getchar();putchar(); 在stdio.h中声明。getchar()读取键盘上的一个字符,立即返回并显示在屏幕上,需要等待回车键才能执行下一条语句。例如下面的代码:
int main(void)
{
char ch,i;
for(i=0;i<10;i++)
{
ch = getchar();
putchar(ch);
}
getch();
}
输入:I LOVE YOU
输出:I LOVE YOU
总结与注意
格式输入函数中的参数是变量存放的地址,所以变量前面要”&”符号。如果是数组则可以直接用数组名,因为数组名就是指向该数组首地址的指针常量。
1.4运算符号
理解与测试:
1)表达式中符号的运算顺序是有先后的,根据符号的优先级和结合性(左结合/右结合)来判定。如果是几个符号连在一起C内部是怎么去读的呢?有一个很简单的规则:每一个符号应该包含尽可能多的字符。也就是说,编译器读取符号的方法是,从左到右一个字符一个字符地读入,如果该字符可能组成一个符号,那么在读入下一个字符,直到与下个字符组合成的符号没有意义为止。例如:a---b;这个表达式等效于(a--)-b;
2)复合赋值符及表达式在赋值符“=”之前加上其他双目运算符号可以构成复合赋值符。如:+=、-=、*=、/=、%=。构成复合赋值表达式的一般格式为
变量 双目运算符 = 表达式
等价于
变量 = 变量 运算符 表达式
可以这么去理解,等号左边的相当于变量自增、自减、自除、自取余一个数,这个数由右边的表达式计算得到。如:int a=7,b=3,c=3;a+=b+=c+=5;运算后是a=18,b=11,c=8。
3)C中唯一一个三目运算符是条件运算符,由“?”和“:”构成。两个符号必须按下面的格式一起使用:
表达式 1?表达式2:表达式3
三个表达式可以是任意的合法表达式,条件运算符的运算规则如下:如果表达式1的值为真,那么整个表达式的值为表达式2的值,否则为表达式3的值。
4)sizeof()也是一个运算符号,它可以对一个数据类型或一个变量取类型长度。但是当一个数组名作为一个实参传递给函数时,在函数内部是不能用sizeof来获取该数组的长度的。因为数组名是一个指向数组首地址的指针,使用sizeof只能取得实参指针类型长度。
5)比较少用到的符号是“:”和“,”。冒号运算符在C中有三种用途,第一种是我们上面讲到的与“?”构成三目运算。第二种是在switch中放在常量表达式后面。第三种用法是在结构体中定义位域时用到。
struct 位域结构名
{ 位域列表 };
其中位域列表的形式为: 类型说明符 位域名:位域长度
例如:
struct bs
{
int a:8;
int b:2;
int c:6;
};
逗号运算符的功能是把两个表达式连在一起组成一个新的表达式,称为逗号表达式。其一般格式为
表达式1,表达式2,表达式3,...,表达式n;
其求值过程是自左向右求两个表达式的值,并以最后一个表达式的值作为整个逗号表达式的值。例如for(a=0,b=0,c=0;b<10,a<20;b++,a++)++c;运算后c=20,如果把中间的两个语句前后调换for(a=0,b=0,c=0;b<10,a<20;b++,a++)++c;运算后c=10。
6)不同的数据类型的量混合运算的时候,系统会临时把算式中长度小的数据类型自动转换成长度大的数据类型。在某些时候我们需要临时改变一个数据的类型或者说是读取“格式”,我们可以使用强制类型转换符,”(type)”。使用的格式一般如下:
(类型说明符)表达式
要得到一个数据类型的类型说明符很容易。只要知道如果声明一个给定类型的变量,那么就可以得到该数据类型的转换符就是把声明中变量名和声明的分号去掉。例如:(int)、(float)、(char *)、(int(*)())等等。
总结与注意
1)“==”和“=”这两个符号也容易在判断语句内出错,严重的还会出现死循环。
2)自增“++”和自减“--”可以作为前缀也可以作为后缀。作为变量前缀时,在表达式中每次取用变量值运算前先自增/自减,作为变量后缀时,在表达式中每次取用变量值运算后再自增/自减。例如:
main()
{
int i=2,j=1,n;
printf("%d\n",i++);
n = i++>j?i++:j;
printf("%d\n",n);
getch();
}
输出结果:
2
4
3)在写式子时要注意运算符号对变量的副作用,单目运算有“++”和“--”,双目运算有“=”。
4)位运算符号与逻辑符号容易混淆的有“&”和“&&”、“|”和“||”、“~”和“!”。前面两个可以这么记,位运算是单个的所以符号也是单个的。
5)在使用位运算符号时要主要如果是有符号数则是对他们的补码执行位运算。对于有符号的整数型使用左移“<<”右补0,使用右移“>>”左补1。如下代码:
void f(int n)
{
unsigned int i;
for(i=1,i<<=15;i;i>>=1)
{
if(i&n)
printf("1");
else
printf("0");
}
printf("\n");
}
main()
{
int a=-0xff;
f(a);
a>>=1;
f(a);
getch();
}
输出的结果是:
1111111100000001
1111111110000000
6)运算符优先级可以简单归纳如下几点:
①单目运算符比任何一个真正意义上的运算符的优先级高。
②逻辑运算符比关系运算符优先级高。
③移位运算符表算术运算符优先级低,但高于关系运算符。
2程序控制结构
2.1选择结构
理解与测试:
使用if-else多层嵌套时else是与上面同一层的最近if相结合的,在程序设计时有过多的分支最好要选择使用if-else,当然如果可以使用switch语句还是尽量选择switch语句或者使用其他能使代码简洁易懂的方法。如下面的代码:
main()
{
int a=10,b=6,c=7,d=8,e=9,f=10;
if(a>b)
{
printf("%d\n",a);
if(b>c)
{
printf("%d\n",b);
if(c>d)
{
printf("%d\n",c);
if(d>e)
{
printf("%d\n",d);
if(e>f)
{
printf("%d\n",e);
}
else
printf("%d\n",f);
}
}
}
}
getch();
}
这种代码又长又难看懂,更要命的是经常搞不懂下面的大括号对应上面的哪个大括号。改进后的代码如下
main()
{
int a=10,b=6,c=7,d=8,e=9,f=10;
while(1)
{
if(a<=b)break;
printf("%d\n",a);
if(b<=c)break;
printf("%d\n",b);
if(c<=d)break;
printf("%d\n",c);
if(d<=e)break;
printf("%d\n",d);
if(e<=f)
printf("%d\n",e);
else
printf("%d\n",f);
break;
}
getch();
}
第二种方法比第一种方法来看上去更干净,理解上差不了多少。
2)
总结与注意
switch语句中每一个分支结束都要有break;语句,如果没有是继续执行下面分支,可能会得到和预料不同的效果。还要注意,case后面跟的一定是整数型或符号型常量表达式,不能是变量,并且不用有相同值的变量表达式,不然会矛盾的。
2.2循环结构
理解与测试:
1)主要有for、while、do-while这三种结构循环体,其中最后一种是比较少用到的,因为一般情况都可以用前面两种替代。
2)循环体结构中括号的表达式要知道时候退出,有时候还要知道循环体运行多少次。在循环体内部也可以使用continue语句跳过本次循环,进入下一次循环,或者用break语句退出当前层循环。
总结与注意
循环结构中表达式的设计很重要。在for循环结构我们经常用到从0到x,循环体执行x+1次,可以写成for(i=0;i<x+1;i++)。有时候要考虑变量i是否会越界,例如:
for((int)i=10;i>0;i-=3);这个循环语句的运行结果一般不是我们想要的。把“==”不注意写成“=”也是容易造成死循环的结果。
3数组
3.1一维数组
理解与测试:
1)一维数组的定义如下:
类型说明符 数组名[常量表达式]
如果定义时就进行初始化,常量表达式可以省略。动态存储的数组没有初始化所有元素的值都为随机。静态存储的数据没有初始化所有的元素自动赋0。
2)一维数组名是一个指针常量,指向数组的首地址。由于是指针常量所以不能对其赋值,只能当指针引用。同理一个数组不能对另一个数组整体赋值。数组引用时下标是从0开始计算的。
3)字符型数组初始化可以直接赋值字符串。char s[]={“Happy”};或者char s[]=”Happy”;。
总结与注意
数组引用时下标不要越界,字符串数组初始化时,数组元素个数要比字符串多一个,因为字符串结束有个“\n”符,否则越界就会发生预料不到的结果。如下:
main()
{
char a[8]="abcdefgh";
printf("%s",a);
getch();
}
输出结果:
main()
{
int a[2],i;
for(i=0;i<8;i++)
scanf("%d",&a[i]);
getch();
}
上面这个代码当输入字符超过2个时可能出现无法运行的结果。
总结与注意
数组名作为函数参数时传递给形参的是数组的指针,而且在子函数中无法用sizeof取得数组的大小,所以有需要时可以用另一个参数传递数组的大小。数组作为形参时可以写成int f(int a[])或者int f(int * a)或者int f(int a[10]),他们三者都一样的,因为对于指针来说“[]”等效于 “*”,数组做形参不会传递数组大小,int a[10]等效于int a[]。数组作为实参时可以写成f(a)或者f(&a[0])或者,因为a[0]的地址就是数组的首地址。但是要注意f(a[10])将是把a[10]作为实参传递过去,a[10]的值赋值给指针使用是危险的。
3.2多维数组
理解与测试:
多维数组和一维数组相似,只是使用了多个下标。多维数组其实可以理解为数组的元素又是一个数组。例如,二维数组可以保存一个矩阵,三维数组可以保存多个矩阵。
4函数
4.1函数的定义和调用
理解与测试:
1)函数定义一般格式如下
函数类型 函数名(形参表)
{
声明部分;
语句部分;
return 表达式;
}
声明部分必须是放在函数的最前面,如果放在语句中则编译不能通过。
2)值参数是指,将实际参数的值传递到函数的形式参数中,这也是参数默认传递方式。函数调用中发生的数据传递是单向的,即只能把实际参数的只传递给形式参数,在函数调用过程中,形式参数发生改变,而实际参数中的值不会发生变化(形式参数可与实际参数同名)。引用参数是将实际参数的地址传递给函数的形式参数。
总结与注意
1)形参必须是变量,用于接收实参传递过来的值。实参可以常量、变量、表达式、函数调用等,无论实参是何种类型的量,在函数调用时都必须有确定的值。实参与形参,数量和类型要能一一对应。
2)函数如果在定义前使用,或者在其他文件中。那么必须先声明,再使用。
函数运行的时候是根据形参类型去读写形参变量的,而传递过来的实参会根据函数声明里的形参类型进行类型转换。如果声明的函数没有形参列表则传递的实参无法进行类型转换就直接把代表实参的二进制码直接复制给形参。如果实参和形参的数据类型不对,而函数运行时又是根据自己的形参类型去读取形参变量的,所以就会发生读取出错,严重的会使程序停止。
声明返回值类型是把数据自动转换成相应的类型才返回给调用者。声明函数的返回值和定义函数时的返回值不一样编译时会提示类型出错。在使用前没有声明或者声明的函数没有声明返回值则编译时默认的返回值都是int型,若该函数定义时声明的返回值不是int型,编译器就也会提示类型错误。
3)大多数C语言实现都是通过函数main的返回值来告诉操作系统该函数是否执行成功。一般是返回0表示成功,返回其他值则表示失败。如果main中没有return 0;返回的是一个int型数值,但是不能保证值为0。
4.2局部变量和全局变量
理解与测试:
1)全局变量也叫外部变量,它是定义在函数外部。它不属于那个函数,而属于一个源程序文件。局部变量是定义在函数内部,可以在函数的声明中定义,也可以在一个语句块中定义。变量的作用范围都是相对于同一层次来讲的。例如函数有声明变量,在函数的语句块内又有定义相同名称的变量,则起作用的是语句块内的变量,虽然它们名称相同但是互不影响。如下面的代码:
int k = 10;
void f(int i)
{
int k = i;
k++;
printf("%d\n",k);
}
main()
{
int k;
printf("%d\n",k);
f(10);
for(k=0;k<5;k++)
{
int k = 5;
printf("%d\n",k);
}
getch();
}
输出结果是:
1692
11
5
5
5
5
5
2)静态局部变量没有初始化的值为0。静态全局变量可以将作用范围限制在一个程序文件模块中。
3)如果要引用另一个源程序文件的全局变量我们可以在本源程序文件中使用extern声明另一个源程序文件的变量。就可以从“声明”开始的地方合法地使用该全局变量了。其一般格式如下
extern (类型名) 变量表;
其中类型名可以省略。
4.3内部函数与外部函数
在一个文件模块中要调用另一个文件模块的函数是,需要对函数进行外部声明。外部声明的一般格式为
extern 函数类型 函数名(参数表说明)
extern表示声明的函数是外部函数,它定义在其他文件模块中。
内部函数也称为静态函数,使用内部函数可以避免各文件模块函数的干扰,使不同的人可以分别编写不同的函数,而不用当心所用函数是否与其他文件模块中的函数重名。
4.4函数的递归与迭代
理解与测试:
1)在调用一个函数的过程中又直接或间接地调用该函数本身,称为函数的递归调用,带有递归调用的函数也称为递归函数。递归其实包括两个过程,一个是递推的过程,函数不断调用自己知道边界。另一个是回归过程,到了边界后一层层把值返回。递推的边界也叫递推出口,我们设计递推函数时一定要有递推出口,不然会进入死循环。
2)递推函数的设计首先要找到事物存在的递推关系。我暂且下一个定义:一个事物包含跟他同概念的事物。例如一根木棍截取一部分还是一根木棍,只是长度变小了。这种包含关系也是这样,被包含的事物概念没有变化只是数量不同而已。
我们要处理一个事物的规律,我们直接按这个规律去设计,在设计的过程中我们可以直接使用我们这个还没设计完的函数(提前使用)。例如要设计个程序把一根木棍截成多截,我们可以这么想,要把木棍截成多截只需要分两步,第一步把木棍截取一截,第二步把剩下的截成多截。因为剩下的还是一个木棍那么就可以调用函数自身来处理。但是我们没法理解它怎么能自己用自己呢,其实可以理解为里面使用的是它包含的跟它同概念函数而已,由于他们都是一样的规律所以就不用再去设计一个函数了,实际上他们确实不是同一个函数,只是同概念,因为在真正执行的过程中是利用了第一个函数的规律在再运行了一个相同概念的子函数,没必要去编写n个同概念的函数。如下图的包含关系。
a
蓝颜色矩形
b
蓝颜色矩形
c
蓝颜色矩形
d
蓝颜色矩形
e
3)下面用几个实例来做说明
求n!:([n!=1 n=0,1] [n!=n*(n-1)! n>1])
分析:像这种有递推公式的就有很明确的递推关系。我们的目的是要求n!,我们只需要两步,第一步是把n!看成n*(n-1)!。第二步求(n-1)!。由于n!和(n-1)!都是求某一个数的阶乘,所以调用函数本身来处理(n-1)!。我们的思路知道此为止,不要在一直想下面怎么处理,程序会按我们这思路自己往下递推的。
程序设计:f(int n)
一、if(n==1||n==0) n!=1; //设计边界
二、n!= n*f(n-1); //利用跟被包含的下一个同概念函数计算
//这里千万不要感觉f()没有写完就不能用,其实递归函数方便的
//就可以随便大胆的调用“自己”。
代码如下:
double fact(int n);
double fact1(int n);
main()
{
int n;
printf("Input n:");
scanf("%d",&n);
printf("%d! = %.0f\n",n,fact(n));
printf("%d! = %.0f\n",n,fact1(n));
getch();
}
double fact(int n)
{
double f;
if(n==1||n==0) //递归函数出口
f = 1;
else
f = n*fact(n-1);
return f;
}
汉诺塔问题:
分析:要把A上的n个盘借助C搬到B上,我们的只需要两步。第一步是把n-1个盘搬到C上,再把A上的最后一个盘搬到B。第二步是把C上的n-1个盘借助A搬到B上。由于把n个盘和把n-1个盘的规律相同,所以可以直接调用函数本身。
代码如下:
void hanio(int n,char a,char b,char c);
main()
{
int n;
printf("Input the number of disks:");
scanf("%d",&n);
printf("The steps for %d disks are:\n",n);
hanio(n,'A','B','C');
getch();
}
void hanio(int n,char a,char b,char c)
{
if(n==1) /* 递归函数出口 */
printf("%c-->%c\n",a,b);
else
{
hanio(n-1,a,c,b); /* 利用C把n-1个片从A移到B */
printf("%c-->%c\n",a,b);
hanio(n-1,c,b,a); /* 利用B把n-1个片从C移到A */
}
}
九连环解法:
分析:我们这边设计是可以解n连环的程序。九连环有个规律是要解或上第n个环必须解去n-1个环。我们解n个环只需要分成两步来处理,第一步就是把解下1个环。第二步就是把剩下n-1个环用解n个环的函数来处理。解一个环的过程是些解下n-2个环,再把第n个环解下,再安上n-2个环(剩下部分要都是未解开的环)。安n个环的步骤过程和解n个环的步骤过程相似。
代码如下:
#include "Stdio.h"
#include "Conio.h"
static int upstep = 0;
static int downstep = 0;
void UpRing(int n); /*加上函数说明,否则编译将会出一点小错误*/
void DownRing(int n) /*下环的逻辑就体现在这里*/
{
if(n>2) DownRing(n-2);
printf("DW:%d\t",n);++downstep;
if(n>2) UpRing(n-2);
if(n>1) DownRing(n-1);
}
void UpRing(int n) /*上环的逻辑则体现在这里*/
{
if(n>1) UpRing(n-1);
if(n>2) DownRing(n-2);
printf("UP:%d\t",n);++upstep;
if(n>2) UpRing(n-2);
}
main()
{
int n;
scanf(“%d”,&n);
puts("Starting...");
DownRing(n);
puts("\nEnding...");
printf("\nup = %d\tdown = %d\n",upstep,downstep);
getch();
}
4)迭代法也称辗转法,是一种不断用变量的旧值递推新值的过程,跟迭代法相对应的是直接法,即一次性解决问题。迭代法又分为精确迭代和近似迭代。“二分法”和“牛顿迭代法”属于近似迭代法。迭代法的思路和递推法相反,迭代是从开始往后面推导。例如计算n!用迭代法如下:
double fact1(int n)
{
double f=1;
int i;
if(n==0)
return 1;
else
{
for(i=1;i<=n;i++)
{
f = f*i;
}
}
return f;
}
总结与注意
1)用递归法的步骤是:一、把一个事物截去一部分剩下的的部分还是不是该事物如果是就可用递推函数来求解。二、找到截去一部分(剩下的概念上还是该事物)的方法。三、需要把事物截取成多部分函数实现就可以分成两步,首先是用上面的方法把事物截去一部分,最后就是把剩下的部分用这个截去多段事物的函数本身处理。
用if设定递归出口
截去事物的一部分
递归函数F
剩下部分用F处理
2)递归函数的运行是一个不断递推的过程,每一次递推都要把数据压入栈,如果递推的步骤过多可能就会使栈溢出。所以能用迭代尽量用迭代,一般来说能写出递推式的都可以用迭代。
double fact1(int n)
{
double f=1;
int i;
if(n==0)
return 1;
else
{
for(i=1;i<=n;i++)
{
f = f*i;
}
}
return f;
}
5指针
5.1指针与简单变量
理解与测试:
简单的讲指针就是地址。指针变量的定义如下:
基本类型 * 指针变量名;
指针定义完最好都进行初始化,初始化值可以为NULL。因为没有初始化的指针指向的地址时随机的,如果程序有引用是很容易出错的。
总结与注意
指针变量指向变量的首地址,指针引用指向的变量时是根据自己的类型来取变量值的,所以不能将不同类型的变量赋值个同一个指针,例如把一个整形指针赋值个符号指针,引用符号型指针指向的变量时只是整形的低字节。
5.2指针与数组
理解与测试:
1)数组名就是一个指向数组首地址的指针常量。对于指针数组符号’[]’相当于指针符号’*’,所以数组元素的地址有下面几种表示:
&a[0].......&a[i]
a................a+i
pa.............pa+i
从技术上可以说数组就相当于指针,但是在声明一个数组,C语言编译时会分配一段固定的连续空间,而声明一个指针却不会。
2)实际上C语言只有一维数组。多维数组其实可以理解为指针数组和普通数组的结合。例如,char a[2][5];可以理解为一个char *a[2]数组,里面的每个元素分别指向一个char b[5]数组。
在二维数组x中,数组名x表示数组的首地址,x[i]表示一个一维数组的名称,(也就是说x[i]不保存元素值),它只是一个地址。x和x[i]的值相等,但是*a!=*a[0],因为a是一个二维数组首地址指针,a[0]一个一维数组首地址指针,引用它们指向的变量时会根据自己类型性质定义变量的类型性质。如下代码:
void main()
{
char a[2][6]={"Hello","World"};
char * p;
p = a;
if(a==a[0])
printf("a==a[0]\n");
if(*a==*a[0])
printf("*a==*a[0]\n");
if(*p==*a[0])
printf("*p==*a[0]\n");
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a[0]));
getch();
}
运算结果:
a==a[0]
*p==*a[0]
12
6
x[i]表示数组的首地址,x[i][j]表示数组x[i]的第j+1个元素的值,&x[i][j]返回数组x[i]的第j+1个元素的地址。也可以使用*(x[i]+j)的方式表示第j+1个元素的值,而*(x[i]+j)又可表示为*(*(x+i)+j)。
3)指针的运算:赋值、间接运算、求地址、递增、递减、求差、比较。指针加上后减去一个整数偏移的长度等于这个整数乘以指针所指数据类型的长度。如下面的代码:
int main()
{
unsigned int i;
unsigned int *p = &i;
printf("%d %d",p,p+1);
getch();
}
运行输出:
-58 -56
5.3指针与字符串
理解与测试:
1)在C系统内部字符串当作一个一维数组,在内存中连续存放。结尾存放字符串结束符,就是ASCII码为0的空字符’\0’,所以字符串常量实质上是指向该字符串首字符的指针常量。如char * p = “point”。
数组名就是存放数组的首地址,如 char sa[]= ”array”;。
2)因为字符串名就是指针常量,使用指针数组来存放字符串指针常量可以方便操作多个字符串。由于数组名也是一个指针常量,所以指针数组名是一个指向指针的指针,可以用“**”来读取他被指向的变量。例如下面的代码:
void main()
{
char * a[2]={"Hello","World"};
printf("%s\n",a[0]);
printf("%c\n",**a);
getch();
}
输出的结果是:
Hello
H
3)字符串出来函数主要有输入输出函数:scanf(),printf(),gets(),puts。专门字符串出来函数:strcpy(目的字符串,原字符串),目的字符串是字符串指针,原字符串可以是字符串指针也可以是一个字符串常量。strcat(目的字符串,原字符串),把原字符串中的字符串连接到目的字符串中字符的后面,并删去原字符串后面的字符串结束符。本函数返回的是目的字符串的首地址。strcmp(字符串1,字符串2),按ASCII码顺序比较两个字符串,字符串1等于字符串2返回0,大于返回1,小于返回-1。strlen(字符串),计算字符串长度,不包括结束符。
4)输入命令时,在可执行文件(命令)名的后面可以跟一些参数,这些参数被称为命令行参数。主函数mian(int argc,char * argv[]);有两个参数,第一参数argc接受命令行参数(包括命令名)个数。第二个参数argv[]是个指针数组,接受以字符串常量形式存放的命令行参数(命令名本身也作为一个参数)。例如下面的代码(文件名:test.exe):
int main(int a,char *v[])
{
int k,s=0;
for(k=1;k<=a;k++)
s = s+atoi(v[k]);
printf("%d",s);
getch();
}
调出命令运行窗口(win7系统可以按windows键+R),输入文件的路径 参数,如下图
运行结果:
6
总结与注意
1)使用一维数组和字符串指针都可以处理字符串,但是由于数组名是指针常量,要改变数组sa所代表的字符串只能改变数组内容,而字符串指针可以重新指向其他字符串。
例如:char sa[]=”Hello”;sa = “World”;这是错误的。因为sa只是指针常量,不能赋值。
2)使用二维数组和指针数组都可以处理多个字符串,但是由于指针数组保存的只是指向字符串的首地址的指针变量,所以是不能去修改字符串中的字符。如:**a=’h’;是错误的。
二维数组保存的是整个字符串,它可以任意修改字符串的字符。如下代码:
void main()
{
char a[2][6]={"Hello","World"};
**a = 'h';
**(a+1) = 'w';
printf("%s %s\n",a[0],a[1]);
getch();
}
运行输出:
hello world
5.4指针与函数
理解与测试:
1)指针可以作为函数的返回值。
2)每个函数的定义经过编译后都占用一段内存单元,该函数名就代表函数的入口地址。我们可以定义一个指向函数的指针,通过函数指针来调用函数。函数指针的定义如下:
类型名(*变量名)();例如:int (*funptr)();
或者
类型名(*变量名)(形参类型名列表);例如:int (*funptr)(int,int);用这种方式定义函数指针在引用时输入的形参要和定义的函数指针后面的类型名列表数量和类型相一致。
3)函数指针数组的声明、赋值、使用和函数指针变量相似。
声明:类型名(*变量名[])()
总结与注意
1)定义和引用函数指针时不要忘记变量名外面的括号。
2)函数指针是一种指针,当引用指向的函数时形参个数是由函数
展开阅读全文