起源
想学一下UNIX系统编程,所以把C重学一遍,C中指针关联甚多,是重点也是难点。下面是自己做的一个总结,希望对你有所帮助!
一、何为指针
1、C的特殊性
C是一门很特殊的语言,特殊的地方在于它可能对计算机是友好的,对程序员并不太友好。
常识上来说,我们是不需要知道一个变量在内存中的实际地址的,对于我们实现业务没有意义。但早期编写C的这帮计算机科学家个个聪明绝顶,而且做得是底层研究,他们熟悉汇编,更熟悉计算机硬件原理,他们只是想做一个抽象层,编出一门语言来简化系统软件开发。
现在来看,对于学习JS、Java或是Python的人来说,C的编写过于复杂了,不够简洁。不过反过来看,我们也不能太苛刻,相比汇编语言来说,C已经好太多了。
2、初识指针
说到指针,它的定义就是:用于存储变量的内存地址。比如一个变量int num = 0;
来说,代码执行的时候必然会给他分配内存,如果你是一个高端玩家,想要知道num储存在内存具体的那块地址上,那么可以用&num
来获得实际地址。
执行代码:
int num = 10;
printf("%d %p", num, &num);
执行结果大致为:
10 0060FF0C
那么获得一个内存地址有什么用呢?
我们先来看它在应用方面来讲最大的作用。
3、函数中变更变量的值
如果要实现两个int值互换,那么非常简单:
int num1 = 10;
int num2 = 20;
int temp = num2;
num2 = num1;
num1 = temp;
printf("%d %d",num1,num2);
如果要实现一个interchange函数呢,你可能想到:
void interchange(int num1, int num2){
int temp = num2;
num2 = num1;
num1 = temp;
}
在main函数中调用:
int main(void){
int num1 = 10;
int num2 = 20;
interchange(num1,num2);
printf("%d %d",num1,num2);
}
打印出来的结果很有可能不符合预期哦,实际上两个数并没有进行交换。
这是怎么回事儿呢?这是由C语言的特性所决定的,num1和num2实际上只传递了值,传递了副本,所以可以这么看:在interchange()函数中的num1和num2已经是独立的值,已经和main()中的两个数没啥关系了,他们怎么改变也不会影响到main()中的num1和num2了。
说道这里,我们遇到了第一个问题,怎么解决呢?需要指针登场了!
4、指针的基础知识
先看下指针的基础知识,然后我们可以用这些知识来解决上述问题。
先有一个表达式:int num = 10;
,指针的知识:
- 获得一个变量的指针:
&num
- 指针声明:
int * pnum = &num
- 解指针:
*pnum = num
这里容易让人迷惑的地方是指针声明和解指针用的都是*
号。
简单来说,&num
也是一个类型,是一个什么类型呢?指针类型。那么如何声明指针类型:int * pnum;
,pnum
就是一个指针类型。pnum
是一个指针,那么我想获取它的实际表达值怎么办?也就是怎么解这个指针呢?也用到*
号。所以*pnum
其实和num
是相等的。
如果感觉有点绕,请多读两遍。
5、利用指针解决问题
所以改良后的interchange()函数如下:
void interchange(int * num1, int * num2){
int temp = *num2;
*num2 = *num1;
*num1 = temp;
}
调用:
interchange(&num1, &num2);
interchange()函数用了指针和解指针的方式巧妙的改变了num1
和num2
变量的值。
二、数组与指针
定义一个数组,非常简单:
int powers[5] = {3,4,5,6,7};
下面开始引入数组与指针的关系了,如果你感觉魔幻,那么不是你的错,我刚开始也一样。
主要的规则有以下几点:
- 数组名本身就是一个指针,是数组首元素的地址。也就是说
*powers
等于powers[0]
- 指针+1,则指针的值递增它所指向类型的大小。也就是说
powers+1
等于&powers[1]
- 对于指针来说,可以用++操作,比如powers++
多举几个例子,感受一下:
powers = &powers[0]
powers + 2 = &powers[2]
*(powers + 2) = dates[2]
1、函数形参、指针与数组
数组可以理解为是一种特殊的指针,所以下面的四种函数原型是等价的:
int sum(int *ar, int n);
int sum(int *,int);
int sum(int ar[], int n);
int sum(int [],int);
2、指针与多维数组
先看一个多维数组的定义:
int zippo[4][2];
对这个多维数组进行分析:
zippo[0]
是一个占用一个int大小对象的地址,而zippo
是一个占用两个int大小对象的地址。他们两个都开始于同一个地址,所以zippo
和zippo[0]
的值相同- 解指针可以用
[]
运算符,也可以用**
运算符.*zippo
等于zippo[0]
,**zippo
等于zippo[0][0]
下面开始烧脑的内容,请说明以下两个表达式的不同:
int (* pz)[2]; //1
int * pax[2]; //2
对于表达式1来说,pz
指向了一个内含两个int类型值的数组。可能不太好理解,先来看int *pz
和int apz[2]
有何联系,pz
是一个指针类型,apz
也是一个指针类型,而且他们都是指向int类型值的指针,所以这样赋值也不会有什么错误:pz=apz
。通过这个实验,可以更加深刻的理解前面所说的数组可以理解为一种特殊的指针。
回来看,int (* pz)=int pz[*]
,那么int (* pz)[2]=int pz[*][2]
。
那么对于表达式2呢,分解来看,pax[2]
声明了一个包含了两个int类型值的数组,前面加上了指针声明的符号,所以int * pax[2]=int pax[2][*]
3、多维数组与函数
如果多维数组作为参数来传递,那么函数原型声明上也有要注意的一些地方,先来看正确的声明方式:
int sum(int (* ar)[4], int rows);
int sum(int ar[][4], int rows);
再来看错误的方式:
int sum(int ar[][], int rows);
为什么下面是错的呢,是因为编译器会把数组表示法转换为指针表示法,那么就必须知道ar所指向的对象大小。
三、字符串与指针
在C中,字符串是以空字符(\0)结尾的char类型数组。
那么我们很容易想到他的定义:
char mesg[11] = "hello world";
char * pmesg = "hello world";
根据前面数组的内容,这两种定义都是ok的。
那么他们有什么异同呢?还是说使用的时候任何情况下都可以看成是含义相同的?
这里又看出来C的复杂性,他们还是有很大差异的。主要的差异就是:char数组字符串声明的时候已经分配了指针空间的大小,在这里是sizeof(char) * 11;而char指针字符串没有分配具体的空间大小,这在某些情况下可能造成内存溢出。
可以先简单的记住这个规则:scanf()最好使用char数组字符串的声明方式。
四、结构与指针
C中的结构如:
struct node{
int num;
};
跟Java中的Class相似。
这里要讲到结构指针,作用和前面的函数中变更变量的值差不多。
先思考一个问题,如果一个结构作为入参传入一个函数中,函数中修改结构的值会影响到main()函数中结构的值么? 可能答案你已经猜出来了,是不能的。这里还是要借助指针来实现:
void struct_demo(){
struct node tn;
tn.num = 1;
struct_demo_swap(&tn);
printf("%d",tn.num);
}
void struct_demo_swap(struct node * temp){
temp->num = 10;
}
打印出来的结果是tn.num=10
。
注意,这里对于指针访问结构成员有两种方式:(*temp).num
或temp->num
五、函数与指针
在C中,原始就支持JS的闭包或Java8的Lamda表达式,也就是支持函数作为参数来传递。
在C中,函数也有地址,指向函数的指针中存储着函数代码的起始处的地址。
一个规则:函数名可以用于表示函数的地址。看代码:
void ToUpper(char *);
void (*pf)(char *);
pf = ToUpper;
可以看到第二行声明了一个函数指针叫pf
,这个函数指针定义了返回值和形参列表,只要是和他结构一样的,都可以赋值给他。这里pf = ToUpper;
没有什么问题。
这样的规则会存在一些小问题:*pf实际上表示ToUpper函数,而pf和函数名又可以互换,所以(*pf)("abc")和pf("abc")、ToUpper("abc")又都是等价的。
下面看一个函数指针最常用的用法,就是作为入参:
void show(void (* fp)(char *), char * str);
关于指针的知识就总结完了,希望大家有所收获!