指针类型相关的基础知识
- 计算机内存储器地址的编码方法
欲学好指针类型,首先要知道计算机内存储器地址的编码方法。计算机内存储器里存储的信息都是由1和0构成的二进制数来表示,每一位二进制数位是表示信息的最小单位,计算机技术规定在内存储器里用八位二进制数位来存储信息,这八位二进制数位叫一个字节,即在内存储器里是以字节为单位存储信息的,如大写英文字母A,在内存储器里用01000001表示。把能表示八位二进制数的存储空间叫一个字节存储单元。
- 存储单元的地址
C语言中数据有基本类型(字符型、整型、长整型、实型、枚举型)、构造类型、指针类型等。不同数据类型在内存中存储时,占用的字节数不同,如字符型需要1个字节空间,整型需要2个字节空间,长整型需要4个字节空间等。把不同数据类型存储时需要的N个字节看成一个整体,叫一个存储单元。对字符型N=1,整型N=2,长整型N=4等。对1个字节构成的存储单元,其字节单元地址编号就是该存储单元的地址,对多个(N>=2)字节构成的存储单元,其低字节单元地址为该存储单元的地址,也叫存储单元的首地址。
- 指针类型
语言中的指针类型有指针常量和指针变量。指针常量简称为指针,即存储单元的地址。可以认为配置不同容量的内存,其指针值的范围是不同的,但都是从0起始的。每个存储单元的指针值都是固定不变的。指针变量就是用来存放指针的变量。指针变量是一种较特别的变量,它的值是一些特定的整数值,不是任何整数(如负整数)都可以存放到指针变量,指针变量与整数加减运算也不同于数学中的运算方法。指针变量也有自己的内存空间,空间的大小由编译器决定,在Turbo C 2.0下是2个字节,在Visual VC++下是4个字节。
- 变量的指针
在编译C语言程序时,要在内存为各种变量分配相应的存储单元,相应的变量名称对应着存储单元的首地址,这首地址就是变量的指针。可以将一变量的指针赋给一指针变量(存到指针变量代表的存储单元中),这时就说指针变量指向了该变量(也可以说指针变量指向了该变量表示的存储单元)。对存储单元的存取操作即可以用变量名,也可以通过指向该变量的指针变量来进行,这要用到指针运算符“*”
- 指针变量类型和它所指向的数据类型
指针变量的类型以声明它时其前面的“*”为标志,无论指针变量指向何种数据类型,它所占用的内存空间大小是一定的(如2个字节),这与其他数据类型的变量所占有的内存空间大小一定一样,所以指针变量的类型就是指针类型,与它所指向的数据类型无关。有资料把可指向不同数据类型的指针变量说成有整型指针
(int*
)变量、实型指针(float *
)变量、字符型指针(char *
)变量等,如果非要这样说,理解成指针变量可指向什么数据类型变量就叫什么数据类型指针变量为好,如int *p
;的声明p可以指向整型变量,说p是整型指针变量为好。
声明指针变量时,还要声明允许指针变量指向的数据类型,这由声明指针变量时“*”前面的“基类型”决定。指针类型加减整数运算时移动的位置大小由指向的数据类型来决定。指向同一种数据类型的不同指针变量间可以进行加减或大小比较运算,指向不同数据类型的指针变量不能进行加减运算或大小比较运算。指针变量一旦被“基类型”声明可指向某种数据类型,就不能再指向其他别的数据类型。
指针类型中的指针变量还可进行自增自减运算,而指针不能。指针变量既可以是左值,也可以是右值,指针只能是右值。由指针、指针变量与运算符构成指针类型表达式。下面说的“&”作为单目运算符时与一个变量结合就构成指针表达式,如&a,&p等,而“*”作为单目运算符只可与指针类型结合,构成指针类型指向的数据类型表达式,如有:
int *p,a,b;
p=&a;
b=*p;
*p=10;
*&a=20;
其中“b=p;”的p就是整型表达式,也是一个整型变量,&a是指针类型表达式,*&a是一个整形表达式,也是一个整形变量。
- “&”和“*”运算符
C语言中有“&:按位与运算符,是双目运算符,结合性是由左到右”和“&:取地址运算符,是单目运算符,结合性是由右到左”,有“:乘法运算符,是双目运算符,结合性是由左到右”和“:指针运算符,是单目运算符,结合性是由右到左(“*”在声明指针变量时也用到了)”。
对于取地址运算符&只能与一个变量结合构成指针类型表达式,如有变量name,则&name就得到变量name的指针。
对于指针运算符*,意义是“取其指向的内容”,这里说“取其指向的内容”不是指存储单元里存放的值,而是表示指针变量指向的变量。在有的书中说“例如:&a为变量a的地址,*p为指针变量p所指向的存储单元的内容(即p所指向的变量的值)[1]”,这种说法值得商榷。笔者认为用“*p代表指针变量p所指向的存储单元(即p所指向的变量)”的说法比较合适,更直接说
p是一个变量,因为p可以是左值。如下代码:
void main()
{int a,*p;
p=&a;
*p=10;
printf(“%d,%d\n”,a,*p);
*&a=20;}
输出a和p的结果都为10,说明p与a等价。通过 *&a= 20;语句还可以改变a的值,也说明 &a与a等价,&a可以是左值。
- 数组的指针
C语言中的数组是一种线性构造型数据类型,特点是:
(1) 数组中数据元素是有序的。
(2) 数组中每一个元素都是具有相同数据类型的拥有不同下标的同名变量,每个元素用下标来访问。
(3) 数组在内存中按下标递增的次序在地址连续的一片内存区域中存放,最低的地址对应于第一个数组元素,最高的地址对应最后一个元素,程序运行中数组大小不可改变。
(4) 二维数组或多维数组可看成多重一维数组的结合。如二维数组可看成两个相结合的一维数组,即一维数组中的每个元素又是一个一维数组的数组名。
数组名表示数组的入口指针,每个数组元素的指针由取地址运算符&与每个元素构成指针表达式得到。
二维数组可看成由若干行,每行又由若干列元素组成的有序元素的集合。二维数组名表示数组的入口,指向第一行一整行,叫行指针,数组元素的指针指向每一个元素,叫列指针。其实一维数组名就是列指针,指向第一个元素,这也是一维数组与二维数组一个很重要的区别。行指针和列指针可以互相转换,用“&、”两个运算符可以达到转换的目的。“&”把列指针转成行指针,“”把行指针转成列指针,这种转换不改变指针值的大小,改变了指针变量的指向。例如有int
a[N][M];声明一个 N行M列的二维数组。a
是数组的入口指针,是行指针,a[0]、a[1]、a[2]是列指针,&a[0][0]是元素a[0][0]的指针,其中a、*a、&a[0]、a[0]、&a[0][0]的值相等,但a、&a[0]指向行,*a、a[0]、&a[0][0]指向列。a与&a[0]等价,a[0]与
&a[0][0]等价,*a与a[0]、&a[0][0]也等价。
对于二维数组的第i行有:a+i 与 &a[i]等价,*(a+i)与a[i]等价,但不能说a+i与a[i]等价,尽管它们的值相等,a+i是指向第i行的行指针,a[i]是指向第i行第0列元素的,即是指向a[i][0]的列指针。
二维数组中指向第i行第j列的指针可以是如下形式:&a[i][j]、a[i]+j和*(a+i)+j,对第i行第j列元素的存取可以用如下形式:a[i][j]、(a[i]+j)和(*(a+i)+j)。
a+i(i>=0)也相当于是指向第i行有M个元素的一维数组的指针。
a又相当一有N个指针元素的指针数组,a相当指向指针的指针,N个元素不是指针(下标)变量,而是指针[3-5]。
8用指针变量引用数组的元素
当指针变量指向数组时,可以用该指针变量引用数组的元素。
用指针变量引用一维数组的元素,既可以用指针法(这要用到“*”指针运算符),也可以用下标法(即把指针变量当成数组名),相对简单。
用指针变量引用二维数组的元素,相对复杂些,当指针变量指向数组时,既可以是指向行,也可以是指向列。当指针变量指向列,如果把指针变量当成数组名,就可以把二维数组当成一维数组来对待,一维数组元素的个数由二维数组的行数与列数的乘积得到。如下代码:
void main()
{int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
int (*p)[4];
p=a;/*p指向行 */
printf(“%d\n”,*(*(p+1)+2));
printf(“%d\n”,p[1][2]);
}
程序运行后输出a数组中a[1][2]元素的值。
void main()
{int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
int *p;
p=a[0];/*p指向列 */
printf(“%d\n”,*(p+6));
printf(“%d\n”,p[6]);
}
程序运行后也是输出 a[1][2]元素的值[3,6-7]。
指针与指针变量的概念,指针与地址运算符
指针
C语言中,任何数据都会占用内存单元的。计算机内存的每个存储位置都对应唯一的存储地址,而变量名作为变量数据存储区域所取的名字,代表的是一个内存编号,而这个内存编号对于用户来说是未知的。如定义:int a=12
。
变量名a代表某个内存单元,而变量值12则是这块内存单元里面的内容。在整个程序运行过程中,通过变量名来访问变量,变量名a所代表的这块内存单元不变,所改变的只是这块内存单元里面的值。
C语言也支持使用变量存储地址来对变量进行存取操作,要取得变量的存储地址,可使用取地址运算符&,如&a表示变量a的存储地址;而变量的存储地址就是指针。
指针变量
指针类型就是C语言中用于表示存储地址的数据类型,而专门用来存放变量存储地址的变量则称为指针变量
格式:数据类型 *指针变量名。跟普通变量不同的是,所有指针变量占用的内存单元大小是一样的,前面的数据类型代表的是指针变量所指向变量的数据类型。如以下代码: int a,*b;//a是一个int型变量,b是一个指向int型数据的指针变量
b=&a;//代表b指向a
使用指针变量的例子
#include <stdio.h>
int main()
{
int a = 100, b = 10; //定义整型a,b,并初始化
int *pointer_1, *pointer_2; //定义指向整型数据的指针变量pointer_1,pointer_2
pointer_1 = &a; //把a的地址赋给指针变量pointer_1
pointer_2 = &b; //把b的地址赋给指针变量pointer_2
printf("a=%d,b=%d\n", a, b);
printf("*pointer_1=%d,*pointer_2=%d\n", *pointer_1, *pointer_2);
return 0;
}
例子
double *pd ;
char *s1 , *s2 ;
在这里定义了三个指针变量pd、s1和s2,其中指针变量pd的基类型为double类型,在指针变量pd中,只能存放double类型变量的地址,指针变量s1和s2的基类型为char类型,在指针变量s1和s2中只能存放char类型变量的地址
int **p ;
以上是定义了一个指向指针的指针变量p,该指针变量p只能存放基类型为int类型的指针变量的地址。
int *pi , **p , k ;
以上语句是在同一语句中,同时定义了指针变量pi、指向指针的指针变量p和变量k,这是允许的。
指针变量的基类型的作用
任何一个指针变量都是用于存放它所指向变量的地址,只要能存放地址就可以了,为何还要区别不同的基类型呢?
其原理是:不同的数据类型变量,C语言系统为它们开辟的存储空间的字节数是不同的,
定义指针变量
定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号*,格式为:
类型名 *指针变量名;
类型 * 指针变量名=初值表达式;
其中,指针变量名是标识符,指针变量名之前的符号“*”,表示该变量是指针类型的。而最前面的“类型”,表示该指针变量能指向变量或函数的类型。初值表达式是一个地址表达式,如表达式中有某变量的地址表达式,则这个变量应是前面已定义的。
给指针变量赋值
使指针指向一个对象
- 通过求地址运算符(&)把一个变量的地址赋给指针变量
“&”是求地址运算符,该运算符为单目运算符,用于求变量的地址,且该变量必须为内存变量。
例如:
int k=1 , j =2 , *q1 , *q2 , *p ;
float x=4.5 ;
q1=&k ;
q2=&j ;
以上第三条语句,是把变量k的地址赋给了指针变量q1,使指针变量q1中存放了变量k的地址,或称指针变量q1指向了变量k。同理,以上第四条语句,是把变量j的地址赋给了指针变量q2,使指针变量q2中存放了变量j的地址,或称指针变量q2指向了变量j。
注意:在使用 & 运算符求变量的地址,并赋给指针变量时,一定要确保所求地址的变量数据类型与存放该变量地址的指针变量基类型一致。
- 同类型指针变量之间可以直接赋值
可以把指针变量的值赋给指针变量,但一定要确保这两个指针变量的基类型是相同的。
接上例:p=q1 ;
执行以上语句后,使指针变量p也存放了变量k的地址,也就是说指针变量p和q1同时指向了变量k
指针与地址运算符
&是取地址运算 对任意变量都可以进行取地址操作
*是取指针目标运算符
指针和指针变量的区别:
- 概念不同
“指针”是概念,“指针变量”是具体实现,指针也是一个变量,所以需要进行定义,而对于指针的定义,与一般变量一样。
- 存放地址不同
一个变量的(内存)地址称为该变量的“指针”,通过指针能找到以它为地址的内存单元。而指针变量是用来存放另一个变量的地址的(即指针)
指针和指针变量的关系
-
指针就是地址,地址就是指针。
-
地址就是内存单元的编号。
-
指针变量就是存放内存地址的变量。
-
指针和指针变量是两个不同的概念,但要注意的是,通常我们叙述时会把指针变量简称为指针,实际他们含义并不一样。
注:
指针里存的是100. 指针:地址 - 具体。
指针里存的是地址,指针:指针变量 -可变。
指针的好处:
1、直接访问硬件
2、快速传递数据(指针表示地址)
3、返回一个以上的值返回一个(数组或者结构体的指针)
4、表示复杂的数据结构(结构体)
5、方便处理字符串
6、指针有助于理解面向对象
C语言指针变量的运算(加法、减法和比较运算)
指针变量保存的是地址,而地址本质上是一个整数,所以指针变量可以进行部分运算,例如加法、减法、比较等,请看下面的代码:
#include <stdio.h>
int main(){
int a = 10, *pa = &a, *paa = &a;
double b = 99.9, *pb = &b;
char c = '@', *pc = &c;
//最初的值
printf("&a=%#X, &b=%#X, &c=%#X\n", &a, &b, &c);
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
//加法运算
pa++; pb++; pc++;
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
//减法运算
pa -= 2; pb -= 2; pc -= 2;
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
//比较运算
if(pa == paa){
printf("%d\n", *paa);
}else{
printf("%d\n", *pa);
}
return 0;
}
可以参考:
http://c.biancheng.net/view/1992.html
指针运算
- 赋值(=)
指针变量只能接受同类型的指针表达式的值,void*类型的指针可以接受任何类型的指针表达式的值。
例如:
char ch='d',*cp;
cp=&ch;
- 取地址(&)
&操作符可取变量地址,指针变量用于存放地址。
例如:
int x=30,*xp=&x;
printf("%d%p\n",x,xp);
- 间接访问()
操作符可取指针变量所指单元内容,称为间接引用指针。
*(取内容)和&(取地址)为互逆操作。
#include<stdio.h>
void main()
{ int x=10,y=20;
int * xp=&x,*yp=&y;
int z=*xp+*yp;
printf("%d%d\n",*xp,*yp);
*xp+=5;
printf("%d%d%d\n",*xp,*yp,z);
}
- 增1(++)和减1(–)
指针向后(高地址)移动1个单位:指针变量++或++指针变量。指针向前(低地址)移动1个单位:指针变量–或--指针变量。
例如:
int a[4]={10,25,36,48};
int * p=a;
printf("%d",*p);
p++;
printf("%d",*p++);
printf("%d\n",*++p)
;
4. 增1(++)和减1(–)
例如:
char b[10]="abcdef";
char * p=b;
printf("%c",*p++);
p++;p++;
printf("%c",*p--);
printf("%c\n",*--p);
- 加(+)和减(-)
指针向后(高地址)移动n个单位:指针表达式+n。
指针向前(低地址)移动n个单位:指针表达式-n。
double a[10]={0};
double * p1=a,* p2=p1+8;
p1++;--p2;
printf("%d%d\n",p2-p1,p1-p2);
例如:
#include<stdio.h>
void main()
{
char a[10]="ABCDEF";
int b[6]={1,3,5,7,9,11};
char * p1=a,* p2;
int * q1=b,* q2;
p2=p1+4;q2=q1+2;
printf("%c%c%c\n",*p1,*p2,*(p2-1));
printf("%d%d%d\n",*q1,*q2,*(q2+3));
}
- 加赋值(+=)和减赋值(-=)
指针向后(高地址)移动n个单位:指针表达式+=n。
指针向前(低地址)移动n个单位:指针表达式-=n。
例如:
int a[5]={2,4,6,8,10};
int * p1=a+1,* p2=a+4;
printf("%d%d\n",*p1,*p2);
p1+=2;p2-=3;
printf("%d%d\n",*p1,*p2);
- 比较
(==,!=,<,<=,>,>=)
判断一指针是否为空指针(==或!=)。
例:如果p是空指针,则…
if(p==0)…
if(p==Null)…
if(!p)…
例:如果p不是空指针,则…
if(p!=0)…
if(p!=Null)…
if(p)…
通过指针引用数组
一个变量有一个地址,一个数组包含若干元素,每个数组元素都在内存中占用存储单元,它们都有相应的地址。指针变量既然可以指向变量,当然也可以指向数组元素(把某一个元素的地址放在一个指针变量中)所谓数组的指针是指数组的起始地址,数组元素的指针是数组元素的地址。
可以用一个指针变量指向一个数组元素,例如:
int a[10] = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19};
int *p;
p = &a[0];
C语言规定,数组名代表数组的首地址,也就是第0号元素的地址。因此,下面两个语句等价:
p=&a[0];
p=a;
在定义指针变量时可以赋给初值:
int *p=&a[0];
它等效于:
int *p;
p=&a[0];
当然定义时也可以写成:
int *p=a;
从图中我们可以看出有以下关系:
p,a,&a[0]均指向同一单元,它们是数组a的首地址,也是0 号元素a[0]的首地址。应该说明的是p是变量,而a,&a[0]都是常量。在编程时应予以注意。
数组指针变量说明的一般形式为:
类型说明符 *指针变量名;
其中类型说明符表示所指数组的类型。从一般形式可以看出指向数组的指针变量和指向普通变量的指针变量的说明是相同的。
通过指针引用数组元素
C语言规定:如果指针变量p已指向数组中的一个元素,则p+1指向同一数组中的下一个元素。
引入指针变量后,就可以用两种方法来访问数组元素了。
如果p的初值为&a[0],则:
- ) p+i和a+i就是a[i]的地址,或者说它们指向a数组的第i个元素。
- ) (p+i)或(a+i)就是p+i或a+i所指向的数组元素,即a[i]。例如,(p+5)或(a+5)就是a[5]。
- ) 指向数组的指针变量也可以带下标,如p[i]与*(p+i)等价。
根据以上叙述,引用一个数组元素可以用:
4. ) 下标法,即用a[i]形式访问数组元素。在前面介绍数组时都是采用这种方法。
5. ) 指针法,即采用*(a+i)或*(p+i)形式,用间接访问的方法来访问数组元素,其中a是数组名,p是指向数组的指针变量,其处值p=a。
【例1】输出数组中的全部元素。(下标法)
#include <stdio.h>
int main(void)
{
int a[10], i;
for (i = 0; i < 10; i++)
a[i] = i;
for (i = 0; i < 5; i++)
printf("a[%d]=%d\n", i, a[i]);
}
【例2】输出数组中的全部元素。(通过数组名计算元素的地址,找出元素的值)
#include <stdio.h>
int main(void)
{
int a[10], i;
for (i = 0; i < 10; i++)
*(a + i) = i;
for (i = 0; i < 10; i++)
printf("a[%d]=%d\n", i, *(a + i));
}
几个注意的问题:
- 指针变量可以实现本身的值的改变。如p++是合法的;而a++是错误的。因为a是数组名,它是数组的首地址,是常量。
- 要注意指针变量的当前值。
- 从上例可以看出,虽然定义数组时指定它包含10个元素,但指针变量可以指到数组以后的内存单元,系统并不认为非法。
- p++,由于++和同优先级,结合方向自右而左,等价于*(p++)。
- (p++)与(++p)作用不同。若p的初值为a,则*(p++)等价a[0],*(++p)等价a[1]。
- (*p)++表示p所指向的元素值加1。 7) 如果p当前指向a数组中的第i个元素,则
*(p–)相当于a[i–];
*(++p)相当于a[++i];
*(–p)相当于a[–i]。
数组名作函数参数
#include <stdio.h>
int main(void)
{
void fun(int arr[],int n); //对fun函数声明
int array[10]; //定义array数组
…
fun(array,10); 用数组名做函数的参数
return 0;
}
void fun(int arr[],int n)
{
…
}
}
f(int arr[],int n);
{
……
……
}
array为实参数组名,arr为形参数组名。在学习指针变量之后就更容易理解这个问题了。数组名就是数组的首地址,实参向形参传送数组名实际上就是传送数组的地址,形参得到该地址后也指向同一数组。这就好象同一件物品有两个彼此不同的名称一样。
同样,指针变量的值也是地址,数组指针变量的值即为数组的首地址,当然也可作为函数的参数使用。
【例3】将数组a中的n个整数按相反顺序存放。
算法为:将a[0]与a[n-1]对换,再a[1]与a[n-2] 对换……,直到将a[(n-1/2)]与a[n-int((n-1)/2)]对换。今用循环处理此问题,设两个“位置指示变量”i和j,i的初值为0,j的初值为n-1。将a[i]与a[j]交换,然后使i的值加1,j的值减1,再将a[i]与a[j]交换,直到i=(n-1)/2为止,如图所示。
#include <stdio.h>
void inv(int x[], int n)
{
int temp, i, j, m = (n - 1) / 2;
for (i = 0; i <= m; i++)
{
j = n - 1 - i;
temp = x[i];
x[i] = x[j];
x[j] = temp;
}
return;
}
int main()
{
int i,
a[10] = {3, 7, 9, 11, 0, 6, 7, 5, 4, 2};
printf("The original array:\n");
for (i = 0; i < 10; i++)
printf("%d", a[i]);
printf("\n");
inv(a, 10);
printf("The array has benn inverted:\n");
for (i = 0; i < 10; i++)
printf("%d", a[i]);
printf("\n");
return 0;
}
对此程序可以作一些改动。将函数inv中的形参x改成指针变量
#include <stdio.h>
void inv(int *x, int n)
{
int *p, temp, *i, *j, m = (n - 1) / 2;
i = x;
j = x + n - 1;
p = x + m;
for (; i <= p; i++, j--)
{
temp = *i;
*i = *j;
*j = temp;
}
return;
}
int main()
{
int i, a[10] = {3, 7, 9, 11, 0, 6, 7, 5, 4, 2};
printf("The original array:\n");
for (i = 0; i < 10; i++)
printf("%d", a[i]);
printf("\n");
inv(a, 10);
printf("The array has benn inverted:\n");
for (i = 0; i < 10; i++)
printf("%d", a[i]);
printf("\n");
return 0;
}
指向多维数组的指针和指针变量
- 多维数组的地址
- 指向多维数组的指针变量
把二维数组a分解为一维数组a[0],a[1],a[2]之后,设p为指向二维数组的指针变量。可定义为:
int (p)[4]
它表示p是一个指针变量,它指向包含4个元素的一维数组。若指向第一个一维数组a[0],其值等于a,a[0],或&a[0][0]等。而p+i则指向一维数组a[i]。从前面的分析可得出(p+i)+j是二维数组i行j 列的元素的地址,而*(*(p+i)+j)则是i行j列元素的值。
二维数组指针变量说明的一般形式为:
类型说明符 (指针变量名)[长度]
其中“类型说明符”为所指数组的数据类型。“”表示其后的变量是指针类型。“长度”表示二维数组分解为多个一维数组时,一维数组的长度,也就是二维数组的列数。应注意“(*指针变量名)”两边的括号不可少,如缺少括号则表示是指针数组(本章后面介绍),意义就完全不同了。
通过指针引用字符串
一串以’\0’结尾的字符在C语言中被看作字符串
字符串常量是用双引号括起的以‘\0’结束的一串字符。
在程序开始运行时分配在全局数据区的文字常量区,存储在无名数组中。 相同字符串常量只有一个副本。
C语言表示字符串的两种表示法
字符数组
字符指针
- 在C语言中,字符串是存放在字符数组中的,想引用一个字符串,可以用以下两种方法
- 用字符数组存放一个字符串,可以通过数组名和下标引用字符串中的一个字符,也可以通过数组名和格式声明“%s”输出该字符串
#include <stdio.h>
//用字符数组存放一个字符串,可以通过数组名和下标引用字符串中一个字符串中一个字符,也可以通过数组名和格式声明“%s”输出该字符。
int main()
{
char string[] = "I love China!";
printf("%s\n", string);
printf("%c\n", string[7]);
return 0;
}
在栈中为string数组分配内存,"I love China!"看上去是字符串,其实是字符数组{‘I’,’ ‘,‘l’,‘o’,‘v’,‘e’,’ ‘,‘C’,‘h’,‘i’,‘n’,‘a’,’!’,’\0’}的简写形式
- 用字符串指针变量指向一个字符串常量,通过字符指针引用字符串常量
int main()
{
char *string = "I love China!";
printf("%s\n", string);
return 0;
}
- 把存放字符串的无名数组的首地址赋给string。
- char *string = “I love China!”; 等价于:
char *string;
string = “I love China!”;
char string[] = “I love China!”; 等价于: char string[20];
strcpy(string,“I love China!”);
“I love China!” 是常量字符串
const char *p; //p是指针变量,p指向的目标空间的内容不可变化 char * const p;
//p是指针常量,p的值不可变,但它指向目标的值可变
几种将字符型指针变量指向字符串的正确方法:
方法一:在定义字符型指针变量时为其赋初值一个字符串(初始化)。 char *p =“123”;
方法二: 先定义字符型指针变量,然后通过赋值语句让指针变量指向字符串。char *p;p =“123”;
实例中的字符串常量“132”,在程序中给出的是它在内存空间的首地址,因此可以通过赋值语句将这段无名储存区的首地址赋值给指针变量,使得指针指向该字符串。
方法三:先定义字符型指针变量,然后将指针变量赋一个有效的地址值(可以将指针赋值为一个字符数组的首地址,或者调用malloc函数为指针变量动态分配一段储存空间),最后调用strucy函数将字符串复制到指针所指向的这段存储空间,
char a[6],*p; char *p;
p=a; p=(char*)malloc(3*sizeof(char));
strcpy(p,"123") strcpy(p,"123")
在使用strcpy函数之前,指针p必须指向一个有效的存储空间,然后存储空间内存放字符串常量。如果指针只是定义,没有为其赋有效的地址值,这样的指针是不能拿来用
字符指针作为函数参数
例8.20 用函数调用实现字符串的复制
1. 用字符数组名作参数
函数的形参和实参可以分别用字符数组名或字符指针变量
#include <stdio.h>
void copy_stying(char from[], char to[])
{
int i = 0;
while ((from[i] = to[i]) != '\0')
{
++i;
}
}
int main()
{
char a[16] = "good good study";
char b[16] = "day day up";
char c[30];
copy_stying(b, a);
copy_stying(c, "copy successfully");
printf("string a:%s\nstring b:%s\n%s\n", a, b, c);
return 0;
}
2. 字符型指针变量作实参
copy_stying函数不变,在main函数中定义字符指针变量from和to,分别指向两个字符数组a,b;
int main()
{
char a[16] = "good good study";
char b[16] = "day day up";
char *pa = a, *pb = b;
printf("string a:%s\nstring b:%s\n\n", pa, pb);
copy_stying(pb, pa);
printf("string a:%s\nstring b:%s\n%s\n", pa, pb);
return 0;
}
3. 用字符指针变量作形参和实参
int main()
{
void copy_stying(char from[], char to[]);
char *a = "I am a teacher.";
char b[] = "You are a student.";
char *p = b;
printf("string a:%s\nstring b:%s\n\n", a, b);
printf("ncopy stying a to string b:\n");
copy_stying(a, p);
printf("string a:%s\nstring b:%s\n%s\n", a, b);
return 0;
}
void copy_stying(char from[], char to[])
{
for (; *from != '\0'; from++, to++)
{
*to = *from;
}
*to = '\0';
}
//进一步简化
while (*from++ = *to++);//不推荐 等价于
while ((*from++ = *to++)!= ′\0′); //推荐
//函数体中while语句也可以改用for语句:
for (;(*from=*to)!=0;++from,++to);// 推荐 等价于
for (;*from++=*to++;);//不推荐
用字符指针作为函数参数时,实参和形参的类型有以下几种关系
实参 | 形参 | 实参 | 形参 |
---|---|---|---|
字符数组名 | 字符数组名 | 字符指针变量 | 字符指针变量 |
字符数组名 | 字符指针变量 | 字符指针变量 | 字符数组名 |
使用字符指针变量和字符数组的比较
- 字符数组由若干个元素组成,每个元素中放一个字符,而字符指针变量中存放的是地址(字符串第一个字符的地址),绝不是将字符串放在字符指针变量中。
- 赋值方式。可以对字符指针变量赋值,但不能对数组名赋值。
可以采用下面方法对字符数指针变量赋值:
char *a a=*a ="I love China!";
不能用以下方法对字符指数组名赋值:
char str[14];
str[0]="I";
str="I love China!"; //数组名是地址,是常量,不能被赋值,非法。
- 初始化的含义。对指针变量赋初值:
char *a ="I love China!"; //定义字符指针变量a,并将字符串第一个元素地址赋给a
等价于
char *a a=*a ="I love China!";
面对数组的初始化:
char str[14]="I love China!";
不等价于
char str[14] ;
str[14]=="I love China!";
数组可以在定义时对各元素赋初值,但不能用赋值语句对字符数组中的全部元素整体赋值。
- 存储单元的内容。编译时为字符数组分配若干存储单元,以存放各元素的值,对字符指针变量,只分配一个存储单元。
#include<stdio.h>
int main()
{ char *a=”I love China!”;
a=a+7; //改变指针变量的值,即改变指针变量的指向
printf(“%s\n”,a); //输出从a指向的字符开始的字符串
return 0;
}
程序分析:指针变量a的值是可以变化的。printf函数输出字符串时,从指针变量a当时指向的元素开始,逐个输出各个字符,直到遇’\0’为止。而数组名虽然代表地址,但它是常量,值不能改变。下面做法错误的:
char str[]={"I love China!"};
str=str+7; //错误!
printf("%s",str);
- 指针变量的值可以改变的,而字符数组名代表一个固定的值(数组首元素的地址),不能改变。
- 字符数组的各元素的值是可以改变的(可以它们再赋值),但字符指针变量指向的字符串常量中的内容是不可以被取代的(不能对它们赋值)。
char a[] = "House"; //字符数组a初始化
char *b = "House"; //字符指针变量b指向字符串常量的第一个字符
a[2] = 'r'; //合法,r取代a数组元素a[2]的原值u
b[2] = 'r'; //非法,字符串常量不能改变
- 引用数组元素。对字符数组可以用下标法(用数组名和下标)引用一个数组元素(如a[5]),也可以用地址法(如*(a+5))引用数组元素a[5]。如果定义了字符指针变量p,并使它指向数组a的首元素,则可以用指针变量带下标的形式引用数组元素(如p[5]),同样,可以用地址法(如*(a+5))引用数组元素a[5]。
char *a="I love China!"; //定义指针变量a,指向字符串常量
则a[5]的值是字母’e’。
虽然并未定义数组a,但字符串在内存中是以字符数组形式存储的。a[5]按*(a+5)处理。
- 用指针变量指向一个格式字符串,可以用它代替printf函数中的格式字符串。
char *format;
format="a=%d,b=%f\n"; //使format指向一个字符串
printf(format,a,b);
因此只要改变指针变量format所指向的字符串,就可以改变输入输出的格式。这种printf函数称为可变格式输出函数。
也可以利用字符数组实现。例如:
char format[]="a=%d,b=%f\n";
printf(format,a,b);
但使用字符数组时,只能采用在定义数组时初始化或逐个对元素赋值的方法,而不能采用赋值语句对数组整体赋值,例如:
char format[];
format="a=%d,b=%f\n"; //非法
因此使用指针变量指向字符串的方法更为方便。
指针数组和多重指针
什么是指针数组
在C语言和C++等语言中,数组元素全为指针变量的数组称为指针数组,指针数组中的元素都必须具有相同的存储类型、指向相同数据类型的指针变量。指针数组比较适合用来指向若干个字符串,使字符串处理更加方便、灵活。
一维指针数组的定义形式为:
指针数组中的每一个元素均为指针,即有诸形如“ptr_array[i]”
的指针。由于数组元素均为指针,因此ptr_array[i]
是指第i+1个元素的指针。
例如二维指针数组的定义为:char *ptr_array[3][3]={{"asdx","qwer","fdsfaf"},{"44444","555","6666"},{"a78x","q3er","f2f"}};
指针数组举例
将若干个字符串按字母顺序(由小到大)输出
#include<stdio.h>
#include<string.h>
int main ()
{
void sort(char *name[],int n);
void print(char *name[],int n);
char *name[] = {"Follow me","BASIC","Great Wall","FORTRAN","Computer design"};
int n = 5;
sort(name,n);
print(name,n);
return 0;
}
void sort(char *name[],int n)
{
char *temp;
int i;
int j;
int k;
for(i = 0;i < n-1;i++)
{
k = i;
for(j = i+1;j < n;j++)
{
if(strcmp(name[k],name[j]) > 0)
{
k = j;
}
}
if(k != i)
{
temp = name[i];
name[i] = name[k];
name[k] = temp;
}
}
}
void print(char *name[],int n)
{
int i;
for(i = 0;i < n;i++)
{
printf("%s\n",name[i]);
}
}
指向指针数据的指针变量
指向指针数据的指针变量, 简称为指向指针的指针
指向指针的指针的定义
char **p;
运算符的结合性是从右到左, 因此**p相当于(*p)
(*p)前面部分是char *, 表明p是指向char *(即一个字符指针变量)的指针变量。
*p就是p所指向的那个指针变量,
例
p = name + 2;
printf("%x\n", *p);
printf("%s\n", *p);
例8.28 使用指向指针的指针输出各字符串。
int main()
{
char *name[] = {
"Follow me",
"BASIC",
"Great Wall",
"FORTRAN",
"Computer Design"
};
char **p;
int i;
for (i=0; i<5; i++)
{
p = name + i;
printf("%s\n", *p);
}
}
例8.29 一个指针数组的元素指向整型数据的简单例子
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 num[2],是a[2]的地址
**p 即num[2], 也就是a[2]的值5
指针数组作为main函数的形参
main()函数的形参
int main(int argc, char *argv[])
argc:命令行参数个数,包括命令字(可执行文件名)
argv:是一个指针数组,用来存放命令行中各个参数和命令字的字符串,并且规定:
argv[0]存放命令字
argv[1]存放命令行中第一个参数
argv[2]存放命令行中第二个参数
……
形参不一定命名为argc和argv
动态内存分配
什么是内存的动态分配
在c/c++语言中,编写程序有时不能确定数组应该定义为多大,因此这时在程序运行时要根据需要从系统中动态多地获得内存空间。所谓动态内存分配,就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不像数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。
怎样建立内存的动态分配
C语言允许建立内存动态分配区域,以存放一些临时用的数据,这些数据不必在程序的声明部分定义,也不必等到函数结束时才释放,而是需要时随时开辟,不需要时随时释放。C语言中,内存的动态分配是通过系统提供的库函数来实现的,主要有malloc、calloc和free,realloc 函数
malloc 函数
其函数原型为
void *malloc(unsigned int size);
其作用是在内存的动态存储区中分配一个长度为size的连续空间。此函数的返回值是分配区域的起始地址,或者说,此函数是一个指针型函数,返回的指针指向该分配域的开头位置。如:
malloc(100);/开辟 100 个字节的临时分配域,返回值为其第一个字节的地址/
注意指针的基类型为 void,即不指向任何类型的数据,只提供一个地址。如果此函数未能成功的执行(例如内存空间不足),则返回空指针(NULL)。
calloc函数
函数原型为
void *calloc(unsigned n, unsigned size)
其作用是在内存的动态存储区中分配n个长度为 size 的连续空间。函数返回一个指向分配区域的起始位置的指针;如果分配不成功,则返回NULL。
申请空间初始化为0
采用 calloc 函数可以为一维数组开辟动态存储空间,n 为数组元素个数,每个元素长度为 size,这就是动态数组。
如:p = calloc(50,4) /开辟 504个字节的临时分配域,把起始地址赋给指针变量p */
free 函数
函数原型为
void free(void*p);
其作用是释放指针 p 所指向的动态空间,使这部分空间能被其他变量使用。p 是最近一次调用 calloc 或malloc 函数时的返回值。free函数无返回值。
如:free§; /释放指针变量p指向的已分配的动态空间/
上述四个函数在头文件malloc.h和 stdlib.h 中定义。若要使用,必须在程序的开始将头文件用文件包含命令放到本文件中来。
realloc 函数
函数原型为
void *realloc(void *p, size_t size);
其作用是
重新分配size大小的空间
如分配不成功,返回NULL
已经通过molloc和calloc获得了动态空间
用来增加已分配空间的大小
防止内存泄露之道
- 在需要的时候才malloc,并尽量减少malloc的次数
- 能用自动变量解决的问题,就不要用malloc来解决
- malloc一般在大块内存分配和动态内存分配时使用
- malloc本身的执行效率就不高,所以过多的malloc会使程序性能下降
- 可以重复利用malloc申请到的内存
- 尽量让malloc和与之配套的free在一个函数内
- 尽量把malloc集中在函数的入口处,free集中在函数的出口处
void指针类型
ANSI新标准增加了一种"void"指针类型, 即可定义一个指针变量, 但不指定它是指向哪一种类型数据的。
void *则为"无类型指针",可以指向任何类型的数据。
float *p1;
int *p2;
p1 = p2;
其中p1 = p2语句会编译出错,提示"'=' : cannot convert from 'int *' to 'float *'"
必须改为:
p1 = (float *)p2;
而void *则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换:
void *p1;
int *p2;
p1 = p2;
下面的语句编译出错:
void *p1;
int *p2;
p2 = p1;
提示"’=’ : cannot convert from ‘void *’ to ‘int *’"。
void的使用
规则一:如果函数没有返回值,那么应声明为void类型
规则二:如果函数无参数,那么应声明其参数为void
规则三:小心使用void指针类型
规则四:如果函数的参数可以是任意类型指针,那么应声明其参数为void *
例题
- 下面的标识符是什么类型?
float (**def)[10];
def是个二级指针,它指向一个一维数组的指针,数组元素是float型。
double *(*gh)[10];
gh是一个指向一维数组的指针,数组元素是double*型。
double (*f[10])();
f是一个函数指针数组,数组元素都是指向函数的指针,函数类型是没有参数且返回 double的函数。
int * ((*b)[10];
和int*(b)[10]一样,b是一维数组的指针,数组元素类型为int。
long (*fun)(int)
fun是一个函数指针,指向一个有一个int形参并返回long的函数
- 请用变量a给出下面的含义
a)一个有10个指针的数组,该指针是指向一个整型数的(Anarrayof10pointerstointegers)
b)一个指向整型数的指针(Apointertoaninteger)
c)一个指向指针的的指针,它指向的指针是指向一个整型数(Apointertoapointertoaninteger)
d)一个有10个整型数的数组(Anarrayof10integers)
e)一个整型数(Aninteger)
f)一个指向有10个整型数数组的指针(Apointertoanarrayof10integers)
g)一个指向函数的指针,该函数有一个整型参数并返回一个整型数(Apointertoafunctionthattakesanintegerasanargumentandreturnsaninteger)
h)一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数(Anarrayoftenpointerstofunctionsthattakeanintegerargumentandreturnaninteger)
答案是:
a)inta[10];//Anarrayof10pointerstointegers
b)inta;//Apointertoaninteger
c)int**a;//Apointertoapointertoaninteger
d)int a[10];//Anarrayof10integers
e)int a;//Aninteger
f)int(*a)[10];//Apointertoanarrayof10integers
g)int(*a)(int);
h)int(*a[10])(int);