指针
计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节,char 占用 1 个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。
我们将内存中字节的编号称为地址(Address)或指针(Pointer)。
CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。
虽然变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符,但在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址。
数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量。
定义指针变量时必须带*,给指针变量赋值时不能带*。
指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:
使用指针是间接获取数据,使用变量名是直接获取数据,前者比后者的代价要高。
定义指针变量时的*和使用指针变量时的*意义完全不同。
注:
数组指针
指针变量加减运算的结果跟数据类型的长度有关,而不是简单地加 1 或减 1。
如果一个指针指向了数组,我们就称它为数组指针(Array Pointer)。
数组指针指向的是数组中的一个具体元素,而不是整个数组。
即数组名不代表整个数组,只代表数组首元素的地址
p+i 等价于arr+i,指向数组的第i个元素,即p+i为arr[i]的地址,*(p+i)为arr[i]
注:arr+i实际是错误的,因为arr是常量不是变量。
数组做函数形参时,比如fun(brr,10);,有个注意点:此时brr为数组名,而数组名为数组首元素地址,故在fun函数中操作arr[]数组元素,实际会对实参brr造成影响,比如在fun中执行arr[1]=3,此时实参brr[1]=3。根本原因在于arr[i]等于*(p+i)。
二维数组指针
对于下列数组int a[3][4],int *p=a;
1、数组名a为数组首地址,也即第0行首地址,是一个行指针。(*a表示第0行第0列首地址);
2、a+1(p+1,a[1])为数组第二行首地址,a+2(p+2,a[2])为第三行首地址;
3、*(a+1)或者*(p+1)或者*(a[1])表示取整个第一行的数据,注意是多个数据,放在表达式中会被转换成第一行首地址,即第一行第0列地址,即*(a+1)等价于a[1];
4、*(p+1)+1或者*(a+1)+1,表示第 1 行第 1 个元素的地址。因为*(p+1)单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址;
5、*(*(p+1)+1)表示第 1 行第 1 个元素的值;
6、a[i][j]或者*(a[i]+j)为第i行第j列数据,a[i]+j为第i行第j列元素地址,注:*a[2]表示第二行第0列的值,即*(a[2]+0);
7、*(*(a+i)+j)等价于*(a[i]+j),原因见4;
8、int (*p)[4]; 表示一个4列的行指针;p是一个数组指针(区别于指针数组)。
字符串指针
它们都可以使用%s输出整个字符串,都可以使用*或[ ]获取单个字符,这两种表示字符串的方式是不是就没有区别了呢?
有!它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。
内存权限的不同导致的一个明显结果就是,字符数组在定义后可以读取和修改每个字符,而对于第二种形式的字符串,一旦被定义后就只能读取不能修改,任何对它的赋值都是错误的。
我们将第二种形式的字符串称为字符串常量,意思很明显,常量只能读取不能写入。
这段代码能够正常编译和链接,但在运行时会出现段错误(Segment Fault)或者写入位置错误。
第2行代码是正确的,可以更改指针变量本身的指向;第3行代码是错误的,不能修改字符串中的字符。
字符串指针变量定义之后要赋初值,不然会造成未知的后果
总结一下,C语言有两种表示字符串的方法,一种是字符数组,另一种是字符串常量,它们在内存中的存储位置不同,使得字符数组可以读取和修改,而字符串常量只能读取不能修改。
指向函数的指针
一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。
函数指针的定义形式为:
returnType 为函数返回值类型,pointerNmae 为指针名称,param list 为函数参数列表。参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称,这一点和函数原型非常类似。
注:( )的优先级高于*,第一个括号不能省略,如果写作returnType *pointerName(param list);就成了函数原型,它表明函数的返回值类型为returnType *。
例如:
指向函数的指针的一个重要作用就是把函数地址作为参数传递到其他函数,如下:
指针函数
C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数
用指针作为函数返回值时需要注意的一点是,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误。
指针数组
如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。指针数组的定义形式一般为:
[ ]的优先级高于*,该定义形式应该理解为:
括号里面说明arrayName是一个数组,包含了length个元素,括号外面说明每个元素的类型为dataType *。
除了每个元素的数据类型不同,指针数组和普通数组在其他方面都是一样的。
相比于二维数组,指针数组可以做到每行长度不一样,更节省内存空间,如下:
指向指针数据的指针
即指向指针的指针,或者叫二级指针。
实际开发中会经常使用一级指针和二级指针,几乎用不到高级指针。
指针数组作main函数的形参
在某些情况下,main函数可以有参数,如下:
其中,argc和argv就是main函数的形参,它们是程序的”命令行参数”
argc表示argv的个数,argv是*char指针数组,数组中每一个元素(其值为指针)指向命令行中的一个字符串
动态内存分配和指向它的指针变量
非静态的局部变量是分配在内存中的动态存储区的,这个存储区是一个称为栈的区域;
C语言还允许建立内存动态分配区域,以存放一些临时用的数据,这些数据需要时随时开辟,不需要随时释放。这些数据是临时存放在一个特别的自由存储区,称为堆区
堆与栈的区别详见:堆与栈的理解与区别
对内存的动态分配是通过系统提供的库函数来实现的,主要有malloc,calloc,free,realloc这4个函数。
1、malloc函数
原型为:void *malloc (unsigned int size); 开辟一个size的连续空间并返回首地址,失败分会空指针(NULL)。
注意:指针的类型为void,即不指向任何类型的数据,只提供一个地址。
2、calloc函数
原型为:void *calloc(unsigned n, unsigned size); 开辟n个长度为size的连续空间,这个空间一般比较大,足以保存一个数组,即为动态数组,成功返回分配区域的起始地址,失败返回NULL。
3、free函数
原型为:void free(void *p); 释放指针变量p指向的动态空间。无返回值
4、realloc函数
原型为:void *realloc(void *p, unsigned int size); 将p指向的动态空间大小改变为size,p的值不变,如果重新分配不成功则返回NULL。
四个函数的声明在stdlib.h头文件中,使用时#include <stdlib.h>
void指针类型
单纯的提供一个地址,不指向确定类型的数据。
一句话总结
总结,出现指针的地方均可用地址代替!!!!