更多课程 选择中心

C/C++培训
达内IT学院

400-111-8989

C语言指针是什么?C语言指针学习和用法详解

  • 发布:C++培训
  • 来源:学习笔记
  • 时间:2017-04-27 15:56

C语言指针

一、C语言指针是什么

指针可以说是集C语言精华之所在,一个C语言达人怎么可以不会指针呢指针(Pointer)就是内存的地址,C语言允许用一个变量来存放指针,这种变量称为指针变量。指针变量可以存放基本类型数据的地址,也可以存放数组、函数以及其他指针变量的地址。

程序在运行过程中需要的是数据和指令的地址,变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符:在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址;程序被编译和链接后,这些名字都会消失,取而代之的是它们对应的地址。
常见指针变量的定义
定义含义
int*p;p可以指向int类型的数据,也可以指向类似intarr[n]的数组。
int**p;p为二级指针,指向int*类型的数据。
int*p[n];p为指针数组。[]的优先级高于*,所以应该理解为int*(p[n]);
int(*p)[n];p为二维数组指针。
int*p();p是一个函数,它的返回值类型为int*。
int(*p)();p是一个函数指针,指向原型为intfunc()的函数。

1)指针变量可以进行加减运算,例如p++、p+i、p-=i。指针变量的加减运算并不是简单的加上或减去一个整数,而是跟指针指向的数据类型有关。

2)给指针变量赋值时,要将一份数据的地址赋给它,不能直接赋给一个整数,例如int*p=1000;是没有意义的,使用过程中一般会导致程序崩溃。

3)使用指针变量之前一定要初始化,否则就不能确定指针指向哪里,如果它指向的内存没有使用权限,程序就崩溃了。对于暂时没有指向的指针,建议赋值NULL。

4)两个指针变量可以相减。如果两个指针变量指向同一个数组中的某个元素,那么相减的结果就是两个指针之间相差的元素个数。

5)数组也是有类型的,数组名的本意是表示一组类型相同的数据。在定义数组时,或者和sizeof、&运算符一起使用时数组名才表示整个数组,表达式中的数组名会被转换为一个指向数组的指针。

C指针是C语言的灵魂

1. 它可以直接访问硬件,这是灵活性和效率的体现,程序离硬件越近自然效率越高,当然运用不当也可导致效率低下

2. 难掌握及太危险,如果对指针理解含混,访问过程不当易导致程序奔溃或隐藏潜在危险

3. 指针作用总的说是调高程序运行效率,原因是它对c语言中定义的各种数据结构进行地址传递,而不需要进行不断地进行值传递。理解起来可以联想一下数据共享与建立副本的区别。


学习C语言的指针既简单又有趣。通过指针,可以简化一些C编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。所以,想要成为一名优秀的C程序员,学习指针是很有必要的。

在计算机中,所有的数据都是存放在内存中的,一般把内存中的一个字节称为一个内存单元,不同的数据类型所占用的内存单元数不一样,如int占用4个字节,char占用1个字节。为了正确地访问这些内存单元,必须为每个内存单元编上号。每个内存单元的编号是唯一的,根据编号可以准确地找到该内存单元。

内存单元的编号叫做地址(Address),也称为指针(Pointer)。 

内存单元的指针和内存单元的内容是两个不同的概念。 可以用一个通俗的例子来说明它们之间的关系。我们用银行卡到ATM机取款时,系统会根据我们的卡号去查找账户信息,包括存取款记录、余额等,信息正确、余额足够的情况下才允许我们取款。在这里,卡号就是账户信息的指针, 存取款记录、余额等就是账户信息的内容。对于一个内存单元来说,单元的地址(编号)即为指针,其中存放的数据才是该单元的内容。

在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。因此,一个指针变量的值就是某个内存单元的地址或称为某内存单元的指针。

设有字符变量C,其内容为'K'(ASCII码为十进制数75),C占用了011A号单元(地址通常用十六进数表示)。设有指针变量P,内容为011A,这种情况我们称为P指向变量C,或说P是指向变量C的指针。

严格地说,一个指针是一个地址,是一个常量。而一个指针变量却可以被赋予不同的指针值,是变量。但常把指针变量简称为指针。为了避免混淆,本教程约定:“指针”是指地址,是常量,“指针变量”是指取值为地址的变量。定义指针的目的是为了通过指针去访问内存单元。

既然指针变量的值是一个地址,那么这个地址不仅可以是变量的地址,也可以是其它数据结构的地址。在一个指针变量中存放一个数组或一个函数的首地址有何意义呢?

因为数组或函数都是连续存放的。通过访问指针变量取得了数组或函数的首地址,也就找到了该数组或函数。这样一来,凡是出现数组,函数的地方都可以用一个指针变量来表示,只要该指针变量中赋予数组或函数的首地址即可。这样做,将会使程序的概念十分清楚,程序本身也精练、高效。

在C语言中,一种数据类型或数据结构往往都占有一组连续的内存单元。用“地址”这个概念并不能很好地描述一种数据类型或数据结构,而“指针”虽然实际上也是一个地址,但它却是一个数据结构的首地址,它是“指向”一个数据结构的,因而概念更为清楚,表示更为明确。 这也是引入“指针”概念的一个重要原因。

指针(Pointer)就是内存的地址,C语言允许用一个变量来存放指针,这种变量称为指针变量。指针变量可以存放基本类型数据的地址,也可以存放数组、函数以及其他指针变量的地址。

二、C语言指针用法总结

(1)关于指针与数组的存储
a、指针和数组在内存中的存储形式
数组p[N]创建时,对应着内存中一个数组空间的分配,其地址和容量在数组生命周期内一般不可改变。数组名p本身是一个常量,即分配数组空间的地址值,这个值在编译时会替换成一个常数,在运行时没有任何内存空间来存储这个值,它和数组长度一起存在于代码中(应该是符号表中),在链接时已经制定好了;而指针*p创建时,对应内存中这个指针变量的空间分配,至于这个空间内填什么值即这个指针变量的值是多少,要看它在程序中被如何初始化,这也决定了指针指向哪一块内存地址。

b、指针和数组的赋值与初始化
根据上文,一般情况下,数组的地址不能修改,内容可以修改;而指针的内容可以修改,指针指向的内容也可以修改,但这之前要为指针初始化。
如:
intp[5];
p=p+1;是不允许的
而p[0]=1;是可以的;
//
int*p;
p=p+1;是允许的
p[0]=1;是不允许的,因为指针没有初始化;
//
inti;
int*p=&i;
p[0]=1;是允许的;
对于字符指针还有比较特殊的情况。
如:
char*p="abc";
p[0]='d';是不允许的

为什么初始化了的字符指针不能改变其指向的内容呢?这是因为p指向的是"常量"字符串,字符串"abc"实际是存储在程序的静态存储区的,因此内容不能改变。这里常量字符串的地址确定在先,将指针指向其在后。

charp[]="abc";
p[0]='d';是允许的
这是因为,这个初始化实际上是把常量直接赋值给数组,即写到为数组分配的内存空间。这里数组内存分配在先,赋值在后。
(2)关于一些表达式的含义
char*p,**p,***p;
charp[],p[][],p[][][];
char*p[],*p[][],**p[],**p[][],*(*p)[],(**p)[],(**p)[][];
能清晰地知道以上表达式的含义吗?(知道的去死!)
第一组:char*p,**p,***p;
分别为char指针;char*指针,即指向char*类型数据地址的指针;char**指针,即指向char**类型数据的指针;他们都是占4字节空间的指针。
如:
charc='a';
char*p=&c;
char**p1=&p;
char***p2=&p1;
cout<<***p2<<endl;
第二组:charp[],p[][],p[][][];
分别为一维,二维和三维char型数组,即数组,数组的数组,<数组的数组>的数组。可以如下的方式进行初始化:
charpp[3]="ab";
charpp1[3][3]={"ab"};
charpp2[3][3][3]={{"ab"}};
现在我们尝试使用第二组三个数组名对应为第一组三个指针赋值,直接赋值的结果如下:
p=pp;//正确
p1=pp1;//错误
p2=pp2;//错误
为什么p1和p2的赋值会出错呢?原因是数组名为给指针赋值的规则不是递归的,即数组的数组可以为数组的指针赋值,而不可以为指针的指针赋值。这里先了解到这个抽象的规则,下面讲完第三组表达式,等我们知道数组的指针和指针的数组如何书写后再对这一问题举例说明。
第三组:char*p[],*p[][],**p[],**p[][],*(*p)[],(**p)[],(**p)[][];
这一类表达式的解析方法如下:
首先把整个表达式分为三部分,
数据类型和星号部分+p或括号内内容部分+中括号部分
如:char*(*p)[]分为乧har*,(*p)和[]
"char*"表示最内层存储的数据类型"(*p)"表示最外层指针"[]"表示中间层数组(维数=中括号数目),因此上式表示一个一维数组的指针p,数组中的元素的数据类型是指针char*。同理,char(**p)[][]表示,一个二维数组的指针的指针,数组元素的数据类型是char。这里如果表达式中间没有括号(如**p[]),则实际上是一个数组,如果最右没有中括号(如**p),则实际上是一个指针。下面通过赋值表达式来理解这些表达式的含义:
charc='a';
char*pc=&c;
char*p[3],*p1[3][3],**p2[3],**p3[3][3],*(*p4)[3],(**p5)[3],(**p6)[3][3],(*(*p7))[3];
p[1]=pc;
p1[0][0]=pc;
p2[0]=&pc;
p3[0][0]=&pc;
(*p4)[0]=pc;
(**p5)[0]=c;
(**p6)[0][0]=c;
(**p7)[0]=c;
注意,(*(*p7))[3]和(**p5)[3]是等价的。
这里再继续上一小节讲一下数组名给指针赋值的问题。
事实上可以对等赋值的数组和指针关系如下(-->表示"赋值给"):
数组-->指针:p[]-->*p
指针的数组-->指针的指针:*p[]-->**p
指针的指针的数组的-->指针的指针的指针:**p[]-->***p
。。。。。。

数组的数组-->数组的指针:p[][]-->(*p)[]
数组的数组的数组的-->数组的数组的指针:p[][][]-->(*p)[][]
总之,最外层的数组可以转换指针,往内层不递归。
(3)关于上述表达式的长度
求一个表达式的"长度",首先分清表达式实际表示的是一个数组还是一个指针;如果是指针,则长度为4byte;如果为数组则要计算实际存储的总元素个数和元素的数据类型。另外要注意要求的是数组元素个数还是数组总字节数;
如:
*(*p)[3][3]
由上文可知上式表示一个指针,因此长度为4byte;而
**p3[3][3]
表示一个二维数组,数组元素类型为指针的指针,因此长度为3*3*4=36;
注意,标准C中sizeof函数求得的是总字节数而非数组长度。
(4)关于函数的指针返回值和指针参数
指针作为返回值要注意的地方是不要返回局部数据的指针。
如:
char*fun(void)
{
chari='a';
return(&i);
}
调用函数fun得不到值'a',原因是函数返回后,局部数据(在栈中)被析构,数据内存被回收,指针指向的数据没有意义;
可以改为:
char*fun(void)
{
chari='a';
char*p=(char*)malloc(5);
If(p!=NULL){p[0]=i,p[1]='\0';}
return(p);
}
这里使用malloc分配了内存(在堆中)在函数返回后依然有效。
这里还没完,因为有一天我使用了下面的代码:
char*fun(void)
{
char*p="abc";
return(p);
}
发现虽然p定义为局部变量,但返回也是正确的。还记得上面讲到的常量字符串存储位置吗?指针p实际指向了静态数据区,这里的char*p相当于constchar*p,这样的数据在函数返回后是不会被立刻回收掉的。
指针作为参数主要用于修改指针指向的数据内容,但修改指针的值无效,

char*fun(char*p)
{
chari='a';
p=(char*)malloc(5);
p[0]=i;
returnp;
}
因为传递的是一个指针副本(形参指针和实参指针的值相同,但不是同一个指针),不会影响调用方的实参值。(诡异的vs2012貌似可以通过形参将实参的值改掉!不过还是不要冒这个险为好了)。
(5)关于const修饰符
const修饰符用于指针时也非常纠结。
首先要分清constchar*p和char*constp。
constchar*p是指向const对象的指针,即对象是只读的,而指针不是。使用const对象的指针要注意两点:
一是不能将其赋值给非const对象的指针,
如:
constchar*p;
char*p1=p;//不允许的
当然,直接使用非const指针指向const对象也是不合法的,
如:
constcharc;
char*p1=&c;//不允许的,
这是要避免通过上述p1改变const对象的值。
二是可以将非const对象的地址赋值给指向const对象的指针,但是试图使用这个指针改变变量的值是非法的,
如:
charc='a';
constchar*p=&c;//允许的
*p='b';//不允许的
char*constp是const指针,即指向对象可编辑,但指针本身不可修改,这一点类似于数组。
如:
charc='a';
char*constp=&c;
*p='b';//允许的
p++;//不允许的
区分两者的方法是,看const是否靠近指针名,如果是则为const指针,否则为const对象。这个助记方法的前提是char要和*号靠在一起,因为constchar*p=charconst*p。
另外,还有constchar*constp,自然是指向const对象的const指针了。
(6)关于指针函数
首先注意指针函数和函数指针的区别,前者是指"返回指针的函数",这在上文中有提到,而后者是指"指向函数的指针"。
函数指针的定义方法为,将"函数名"替换为"(*函数指针名)",
如:
指向一个声明为voidfun(inta)的函数指针可以定义为void(*pFun)(inta)或void(*pFun)(int),注意这里函数指针pFun只能指向和fun有相同返回类型(void)和参数类型(int)的一类函数,另外定义中()也不是摆设,去掉()会被看做是返回值为void*类型的函数声明。举个例子:
voidfun(inta)
{
cout<<a<<endl;
}
intmain()
{
void(*pFun)(int);
pFun=&fun;//(1)
*(pFun)(1);//(2)
}
事实上,上式中的(1)(2)行做如下几种替换也是正确的:
a、pFun=fun;
pFun(1);
b、pFun=&fun;
pFun(1);
c、pFun=fun;
*(pFun)(1);
如果有什么疑问的话,可以接着尝试用如下方式直接调用函数fun:
(*fun)(1);
运行的结果也是正确的!这要怎么解释呢?
其实,fun不仅仅作为函数名,它同pFun一样也是一个函数指针,只不过是函数指针常量。为了书写方便,c语言开发者允许将函数指针调用直接写成类似fun()的形式,同样函数指针变量赋值也可以写成类似pFun=&fun的形式。值得注意的是,函数声明的格式还是比较严格的,如:
voidfun(int);//不能写成void(*fun)(int)。
同样,
void(*pFun)(int);//不能写成voidpFun(int)。
为了方便,我们还可以定义函数指针类型,针对上述例子的定义方法如下:
typedefvoid(*PFUN)(int);
这样一来我们就可以用
PFUNpFun;
来声明一个函数指针了。
有了函数指针之后,函数的参数也可以设为某一类函数类型。
如:
typedefvoid(*PFUN)(int);
voidfun(inta)
{
cout<<a<<endl;
}
voidtopfun(PFUNf1,inta)
{
f1(a);
}
intmain()
{
topfun(fun,1);
return1;
}

三、怎么学好C语言指针

直接引用
chara;
a=10;
程序内部是怎么操作的呢?
其实,程序对变量的读写操作,实际上是对变量所在的存储空间进行写入或取出数据。就上面的代码而言,
系统会自动将变量名a转换为变量的存储地址,根据地址找到变量a的存储空间,然后再将数据10以2进制的形式放入变量a的存储空间中。
通过变量名引用变量,由系统自动完成变量名和其存储地址之间的转换,称为变量的"直接引用"方式


间接引用
如将变量a的地址存放在另一个变量中,比如存放在变量b中,然后通过变量b来间接引用变量a,间接读写变量a的值。这就是"间接引用"

总结一句:用来存放变量地址的变量,就称为"指针变量"。在上面的情况下,变量b就是个"指针变量",我们可以说指针变量b指向变量a。



指针的定义
一般形式:类名标识符*指针变量名;

int*p;
float*q;
"*"是一个说明符,用来说明这个变量是个指针变量,是不能省略的,但它不属于变量名的一部分
前面的类型标识符表示指针变量所指向的变量的类型,而且只能指向这种类型的变量

指针的初始化
inta=10;int*p=&a;floatb=2.3f;float*q;q=&b;

指针运算符
给指针指向的变量赋值
chara=10;

printf("修改前,a的值:%d\n",a);


//指针变量p指向变量a

char*p=&a;//这个*是定义指针的说明符


//通过指针变量p间接修改变量a的值

*p=9;//这个*是指针运算符,表示把9赋值给指针指向的地址a,也就相当于a=9;

//这里就是间接修改a的值

printf("修改后,a的值:%d",a);

取出指针所指向变量的值
chara=10;



char*p;

p=&a;


charvalue=*p;//根据p值(即变量a的地址)访问对应的存储空间,并取出存储的内容(即取出变量a的值),赋值给value

printf("取出a的值:%d",value);

使用注意
在指针变量没有指向确定地址之前,不要对它所指的内容赋值。下面的写法是错误的


int*p;
*p=10;//这是错误的
应该在指针变量指向一个确定的变量后再进行赋值。下面的写法才是正确的

//定义2个int型变量
inta=6,b;

//定义一个指向变量b的指针变量p
int*p;
p=&b;

//将a的值赋值给变量b
*p=a;
例子
交换两个字符变量的地址(改变实参的值)
voidswap(char*p,char*q)
{
chartemp=*p;
*p=*q;
*q=temp;

}

intmain(intargc,constchar*argv[])
{
chara='A',b='&';
swap(&a,&b);
printf("a=%cb=%c\n”,a,b);
}

用指针指向一维数组的元素
inta[2]={2,3};int*p=&a[0];*p=10;那么a[0]应该等于10
数组名a的地址与它的第一个元素的地址相同,所以p=&a[0]与p=a效果一样
指针来遍历数组元素
intary[]={1,2,3,4,5};
int*q=ary;
for(inti=0;i<5;i++)
{
//数组内元素内存地址是连续的存储方式,指针移动一个对应类型单位字节数(int、char...),则指向下一个元素值
//printf("数为:%d",*(q+i));//地址移动
//printf("数为:%d",*(ary+i));//地址移动
printf("数为:%d",*(q++));//q=q+1,指针指向的地址移动
//printf("数为:%d",*(ary++));//错误,常量不能赋值
}

数组、指针、函数参数
形参数组,实参指针
voidchange(intb[]){
b[0]=10;
}

intmain()
{
//定义一个int类型的数组
inta[4]={1,2,3,4};

int*p=a;

//将数组名a传入change函数中
change(p);

//查看a[0]
printf("a[0]=%d",a[0]);

return0;
}
形参指针,实参数组
voidchange(int*b){
b[0]=10;
//或者*b=10;
b[1]=11;
//或*(b+1)=11;
}

intmain()
{
//定义一个int类型的数组
inta[4]={1,2,3,4};

//将数组名a传入change函数中
change(a);

//查看a[0]
printf("a[0]=%d",a[0]);

return0;
}//可以看出,在很多情况下,指针和数组是可以相互切换使用的。但是,并不能说指针就等于数组

用指针遍历字符串的所有字符
charchs[]="abcde";
char*p;
p=chs;
for(;*p!='\0';p++)
{
printf("data:%c",*p);
}
printf("\n");

用指针直接指向字符串
char*p="abcde";
strlen("abde”);
函数在string.h中的声明
size_tstrlen(constchar*);
char*strcpy(char*,constchar*);//字符串拷贝函数
char*strcat(char*,constchar*);//字符串拼接函数
intstrcmp(constchar*,constchar*);//字符串比较函数
它们的参数都是指向字符变量的指针类型,因此可以传入指针变量或者数组名。

指针指向字符串的其他方式
1chars[10];
2s="mj";//编译器肯定报第2行的错,因为s是个常量,代表数组的首地址,不能进行赋值运算

1char*s="mj";
2
3*s="like";
第3行代码犯了2个错误:

第3行代码相当于把字符串"like"存进s指向的那一块内存空间,由第1行代码可以看出,s指向的是"mj"的首字符'm',
也就是说s指向的一块char类型的存储空间,只有1个字节,要"like"存进1个字节的空间内,肯定内存溢出
由第1行代码可以看出,指针s指向的是字符串常量"mj"!因此是不能再通过指针来修改字符串内容的!
就算是*s='A'这样"看起来似乎正确"的写法也是错误的,因为s指向的一个常量字符串,不允许修改它内部的字符。

chara[]="lmj";定义的是一个字符串变量!char*p=a;*p=‘L’;变量可以通过指针改变,常量不行
char*p2="lmj";定义的是一个字符串常量!

返回指针的函数
返回指针的函数的一般形式为:类型名*函数名(参数列表)

//将字符串str中的小写字母变成大写字母,并返回改变后的字符串

//注意的是:这里的参数要传字符串变量,不能传字符串常量

[plain]viewplaincopyprint?在CODE上查看代码片派生到我的代码片
char*upper(char*str){
//先保留最初的地址。因为等会str指向的位置会变来变去的。
char*dest=str;
//如果还不是空字符
while(*str!='\0'){
//如果是小写字母
if(*str>='a'&&*str<='z'){
//变为大写字母。小写和大写字母的ASCII值有个固定的差值
*str-='a'-'A';
}
//遍历下一个字符
str++;
}
//返回字符串
returndest;
}

intmain()
{
//定义一个字符串变量
charstr[]="lmj";
//调用函数
char*dest=upper(str);

printf("%s",dest);
printf("%s",str);
return0;
}

指向函数的指针
定义的一般形式:函数的返回值类型(*指针变量名)(形式参数1,形式参数2,...);

注意:形式参数的变量名可以省略,甚至整个形式参数列表都可以省略

intsum(inta,intb)
{
returna+b;
}
intmain()
{
int(*q)(inta,intb)=sum;//(inta,intb)可以写成(inta,int)或(int,int)或()
intresult=(*q)(2,5);//调用函数
printf("\n%d",result)
return0;
}
将函数作为参数
voidget(int(*q)(inta,charb),floatc){}

预约申请免费试听课

填写下面表单即可预约申请免费试听!怕钱不够?可就业挣钱后再付学费! 怕学不会?助教全程陪读,随时解惑!担心就业?一地学习,可全国推荐就业!

上一篇:要学好c++语言?看这基本书籍就够了
下一篇:C/C++语言考试知识点归纳分析

C语言创建windows窗口实例

C++回调函数是什么?

C++ shared_ptr和动态数组

C语言有哪些关键词,C语言44个关键词大全

  • 扫码领取资料

    回复关键字:视频资料

    免费领取 达内课程视频学习资料

  • 搜索抖音号

    搜索抖音号:1821685962

    免费领取达内课程视频学习资料

Copyright © 2021 Tedu.cn All Rights Reserved 京ICP备08000853号-56 京公网安备 11010802029508号 达内时代科技集团有限公司 版权所有

选择城市和中心
黑龙江省

吉林省

河北省

湖南省

贵州省

云南省

广西省

海南省