c语言中的指针是什么
很多学习C语言的新手来说,指针无疑是一个难点。但是,我觉得指针也是C语言特别重要的一个特性。那么下面一起来看看小编为大家精心推荐的c语言中的指针是什么,希望能够对您有所帮助。
为什么说指针是 C 语言的精髓?
“指”是什么意思?其实完全可以理解为指示的意思。比如,有一个物体,我们称之为A。正是这个物体,有了这么个称谓,我们才能够进行脱离这个物体的实体而进行一系列的交流。将一个物体的指示,是对这个物体的抽象。有了这种抽象能力,才有所谓的智慧和文明。所以这就是“指示”这种抽象方法的威力。
退化到C语言的指针,
指针是一段数据/指令(在冯诺易曼体系中,二者是相通,在同一空间中的)的指示。这是指示,也就是这段数据/指令的起始位置。但是数据/代码是需要一个解释的方法的。比如0x0001,可以作为一个整数,也可以作为作为一串指令,也可以作为一串字符,总之怎样解释都可以。
而C语言,在编译阶段,确定了这段数据/指令的“解释方法”。
例如,整型指针,表示的就是可以从这个指针p指向的位置开始解释,解释为一个整数。
一个函数指针,表示的就是可以从这个指针p指向的位置开始解释,解释为一段指令,对应的输入和输出以及返回值按照函数指针的类型,符合相应的要求。
综上,C语言的精髓是指针,但指针不仅仅是C语言的精髓,它是抽象的精髓。各个语言中都有类似的东西,例如函数,例如引用。
(引用和指针的区别,我的理解,不可以进行+/-偏移操作的指针,就是引用。随意偏移,很容易使得目标位置不符合其相应的意义,从而造成解释失败,进而崩溃。而增加了偏移功能的指针,好处是方便表述一堆具有相同类型的数据/指令,数组之类的就是这样的实例。)
同样的void类型的指针,也是C语言的特色。void型的指针,就是去掉了指定类型的指针,从而使得可以以任意解释方式,解释指针,这就带来了如上的潜在问题。但是也可以说,这个C语言的特有威力(我一般都把C语言的威力理解为这个)。这个带来的好处非常之灵活。因为可以使用统一的类型来表述所有类型的数据。带来的问题,和上面是类似的。就是如果解释方法不当,就会造成灾难性的后果。C语言的强制类型转换也是打破常规的指针解释.也有可能带来问题.
给大家举个例子
一、指针
1.指针就是存放地址的变量。在32位系统上,一个指针变量占用4个字节。在64位系统上,一个指针变量占用8个字节。
2.指针类型、取地址、解引用
1)指针类型
int* pa;
int *pa;
int * pa;
语义:pa是一个指针,该指针指向一个int型的数据,即pa存放一个int型数据的地址。
int* pa, pb; // pa是int*,pb是int
int *pa, *pb;
2)取地址——&
int a;
int* pa = &a; // pa指向a,a是pa的目标,pa是a的指针,pa中存放着a的地址。
3)解引用(取目标)——*
*pa = 100; // 将100赋值给pa的目标,即赋值给a
3.指针的用法
1)将指针作为函数的参数,传递变量的地址,进而在多个函数中访问相同的内存数据。
2)指针也可以作为函数的返回值,但是不要返回指向局部变量的指针。因为函数返回以后,其局部变量所占用的内存将随函数栈一起被释放,所得到的指针为野指针。
int foo (void) {
return 10;
}
a = foo ();
int* foo (void) {
...
return p;
}
int* foo (void); // 函数声明,声明一个没有参数,返回int*的foo反函数
int (*foo) (void); // 函数指针
void foo (void) {
}
没有返回值的函数。
解读 C 语言中的指针
一,基本概念
关于指针的基本概念,我就不详细介绍了,因为有许多书都介绍的很详细。这里我只介绍一部分。指针指向一个地址,而指针本身在大多数系统上都是一个无符号整数(在32bit机上是4byte,在64bit机上是8byte)。下面用一个例子来说明其机制:
在上面的例子中,先定义了一个指针p,它的类型是int,也就是说它只能指向一个int型的变量,而不能指向其他类型的变量。最后我们将a变量的地址赋给p。在这个过程中,涉及到两个内存块,一个是存放指针p的内存(用&p可得到内存地址),一个是存放a的值的内存块(用&a可以得到内存地址)。而第一个内存存的p的值经过赋值语句后也就是&a的值了。另外一个注意点是, *(星号)和变量类型以及变量名之间可以有任意个空格,也可以没有。比如下面三种方式都是一样的:
int a = 10;
int *p; //声明一个指针,但未初始化,此时为野指针
p = &a; //将a变量的地址赋给指针p
在上面的例子中,先定义了一个指针p,它的类型是int,也就是说它只能指向一个int型的变量,而不能指向其他类型的变量。最后我们将a变量的地址赋给p。在这个过程中,涉及到两个内存块,一个是存放指针p的内存(用&p可得到内存地址),一个是存放a的值的内存块(用&a可以得到内存地址)。而第一个内存存的p的值经过赋值语句后也就是&a的值了。另外一个注意点是, *(星号)和变量类型以及变量名之间可以有任意个空格,也可以没有。比如下面三种方式都是一样的:
int* a;
int * a;
int *a;
解读方法:
首先从标示符开始阅读,然后往右读,每遇到圆括号的右半边就调转阅读方向。重复这个过程直到整个声明解析完毕。需要注意的是,已经读过的部分在后续作为一个整体来看。
看下面一个例子:
int *a[3];
//首先a右边是[],说明a是一个具有3个元素的数组
//右边读完,则读左边。a左边是int*,说明a的元素是int类型的指针
int (*a)[3]
//首先,a右边是圆括号的右半边,转向,左边是一个*,说明a是一个指针
//遇到括号,再转向,是一个[],说明a是一个指向3个元素的数组的指针
//左边是int,说明元素类型是int
//所以,a是一个指向具有3个整型元素的数组的指针
int (*func)(int p);
//相同的方法,func首先是一个指针
//然后右边是一个括号,说明(func)是个函数,而func是指向这个函数的指针
//这个函数具有int类型的参数,返回值类型为int
int (*func[3])(int p);
//同理,func首先是一个具有3个元素的数组
//其次,func左边是一个*,说明func数组的元素是指针。要注意修饰的是func[3],而不是func。因为已经读过的部分在后面都作为一个整体来对待
//跳出第一个圆括号,右边又是一个圆括号,说明func数组的元素是函数类型的指针。这个函数具有int类型的参数和int型返回值
二,数组首地址a,&a,&a[0]
注:a,&a,&a[0]的含义虽然不同,但是他们三个的值是相等的!
以int a[3]为例说明:
a作为右值时,代表数组首元素的首地址,而非数组地址。 也就是a[0]的地址。int i = (a+1),这里a是右值,所以代表首元素的首地址,a+1代表下一个元素的首地址,即&a[1]。
a是整个数组的名字。所以sizeof(a)的值为sizeof(int) * 3 = 40,代表整个数组的大小。
&a即为取a的首地址,也即整个数组的首地址。所以sizeof(&a) = 4。 int p = (int)(&a+1)中的&a+1代表下一个数组的首地址,显然是越界的。
&a[0]代表首元素的首地址。 所以sizeof(&a[0]) = 4。
&a[3],很显然数组越界了,但它的sizeof是多少呢? 也是4,因为关键字sizeof求值是在编译的时候,虽然并不存在a[3]这个元素,但是这里并没有真正访问a[3],而是根据数组元素类型来确定其值的。所以sizeof(a[3])不会出错。
a[-1]代表什么意思?首先要明白下标的形式被编译器解析成指针的形式,即a[1]被解析成(a+1)。那么,a[-1]被解析成*(a-1)。
关于数组首元素的首地址和数组的首地址的区别:其实,数组首元素的首地址和数组首地址的值是相同的,即&a[0]和a(以及&a)是相等的,但是而这含义不一样。首元素的首地址加1后,是第二个元素的首地址(之所以一直说首地址,是因为有的类型存储时会占多个地址),但数组的首地址加1后是“下一个数组的地址”,这里的下一个数组只是为了说明加1时加了整个数组的大小,而不是一个元素的大小。
有一点比较容易混淆:a虽然代表整个数组,但(a+1)却代表下一个元素的首地址,即和(&a[0]+1)一样,下一个数组的形式为:(&a+1)。 下面以一个程序来说明:
#include<stdio.h>
int main()
{
int a[3] = {1, 2, 3};
printf("%ldn",sizeof(long unsigned int));
printf("*(a+1)=%dn",*(a+1));
printf("sizeof(a)=%ldn", sizeof(a));
printf("sizeof(&a[3])=%ldn", sizeof(&a[3]));
printf("a[-1]=%dt*(a-1)=%dn",a[-1],*(a-1));
printf("a=%pt&a=%pt&a[0]=%pn",a, &a,&a[0]);
printf("a=%pt(a+1)=%pt(&a+1)=%pn",a,(a+1),(&a+1));
return 0;
}
输出结果:
8
*(a+1)=2
sizeof(a)=12
sizeof(&a[3])=8
a[-1]=0 *(a-1)=0
a=0x7fffcb4cb980 &a=0x7fffcb4cb980 &a[0]=0x7fffcb4cb980
a=0x7fffcb4cb980 (a+1)=0x7fffcb4cb984 (&a+1)=0x7fffcb4cb98c
说明(下面的行数只计算main函数内有代码的行):
程序第1行定义了一个具有3个元素的整型数组。
第2行打印了long型的大小。因为我是64bit的,所以一个long是8byte。
第3行打印了*(a+1)的值,结果和a[1]的值相等。说明a虽然代表整个数组,但作为右值时,的确代表首元素的首地址。
第4行输出值为12,是整个数组的大小。
第5行打印了一个出界元素的大小,没有报错,验证了上面第5条。
第6行打印了a[-1]和*(a-1),输出值相等。验证了上面第6条。
第7行打印了a和&a[0],值相等。说明数组的首地址和首元素的首地址是相等的。
第8行打印了a,(a+1),(&a+1),由结果就可以看出首元素的首地址加1是加了一个数组元素的大小,而数组首地址加1是加了一个数组的大小。
三,指针数组和数组指针
指针数组: 首先它是一个数组,数组的元素是指针,也成为“存储指针的数组”。
数组指针: 首先它是一个指针,它指向一个数组,也可以理解为“数组的指针”。 也可以利用前面的“解读方法”去分析。
四,函数指针和指针函数
函数指针: 指向函数的指针变量。
指针函数: 带指针的函数,也就是返回指针的函数。
char * fun(char* a, char* b) //定义为指针函数
{
...
...
}
int main()
{
char* (*p)(char* p1, char* p2); //定义函数指针
p = &fun; //把函数地址赋给它
//p = fun; //这样写也行
(*p)("aa", "bb"); //使用函数指针
return 0;
}
五,指针常量和常量指针
const char* p1; //常量指针,指向常量的指针
char const* p2; //同上
char* const p3; //指针常量,指针是常量
怎么记?
可以先把类型名去掉,然后看const离谁近,就修饰谁。
也可以const在*左边的为常量指针,const在*右边的为指针常量。
三~五的万能钥匙
其实,关于“指针数组与数组指针、函数指针与指针函数、指针常量与常量指针”的判断,有一个万能钥匙。那就是根据我们强大的中文语法:前边是修饰词,后边才是主语。比如“指针数组”,前面的指针只是修饰词,后面的数组才是主语,所以它是一个数组。
六,野指针
野指针指没有确定指向的指针。造成野指针的情况有:
1. 指针变量创建但没有初始化。
2. 指针p被free或者delete之后,没有置为NULL。