目录
指针
指针是C语言的特色,为什么C语言会有指针?因为C语言是底层语言,底层语言时时刻刻不可避免的会与内存打交道,指针带来了操作内存的强大能力和编写代码的便利性,缺点是大大增加了代码的复杂性和阅读难度,且增大了bug出现的机率,从某些方面来说非常考验程序员的功底。所谓指针就是内存地址,一个内存单元为1字节,但是单元编号却很大,64位程序用8个字节编号,32位程序用4字节编号,4字节最大能表示4294967295,这就是为什么32位操作系统最大支持4G内存的原因,而8字节能表示的地址远远超过目前硬件的制造水平。内存控制器通过地址编号来存取内存单元,变量名在编译的时候会被翻译为内存地址,因此通过变量名存取称为直接存取,我们也可以将内存地址作为数据保存起来,记录这个编号的内存开销可能比其数据本身还要大,例如记录一个字符需要1字节,记录这个字符内存的地址可能需要8字节。程序启动后会向操作系统申请内存开销,变量所记录的地址,函数的定义等都被放入静态内存空间中,而函数运行时所需的内存开销,用户申请的空间属于动态内存空间。当我们保存了一个变量的地址后,除了通过变量名访问,还可以通过保存的地址来访问,也就是通过指针访问,这种方式称为间接访问。很明显,间接访问比直接访问需要的开销大,但带来的好处是增强了代码功能和灵活性。下面看看如何获取一个变量的内存地址,之前我们知道通过变量名访问可以获得变量储存的内容,如果要获取变量表示的内存地址,可以通过&符号来获取,例如:
printf("&a=%p,&f=%p\n",&a,&f);
32位应用程序用%d输出就够了,64位应用程序需要使用%ld输出,还可以选择一个专为输出指针定义的格式符%p,它会自动处理32位和64位地址并以固定长度输出。除了输出可见的内存地址,我们还可以定义一个指针变量,将这个地址作为它的值从而记录该地址,例如:
int p = &a;
其中p是变量名,int 是变量类型,也可以写作int* p,定义之后通过指针p就可以间接获取变量a的值,例如:
printf(“%d”,p);
使用p间接获取变量a的值称为指针运算,也可以通过p修改变量a的值,如p=100。指针运算时必须指定其数据类型,否则编译器不知道如何取值,进行指针位移时也必须确定指针类型,否则编译器不知道要移动多少字节。初始化指针变量时如果没有确定值,应该初始化为NULL,NULL是C语言中定义的常量,值为0,但是NULL不等同于0,它的类型是void*。NULL是一个安全的值,标准库中很多函数都会判断指针是否为空,如果为空不会执行任何操作,在进程的虚拟地址空间中有一段内存区域被称为保留区,这个区域不存储有效数据,也不能被用户程序访问,NULL 指向这块区域很容易检测到,如果你不将指针初始化为NULL,它可能指向一个未知的内存地址,对未知地址操作是非常危险的。
如果指针只能做一些基本数据类型的内存存取操作可以说没有什么用处,还不如直接用变量名存取减少开销,指针彰显威力的地方在于处理形参、数组、函数等高级问题,可以说C语言的灵活性大多来自于指针操作,如果没有指针C语言就是一个功能有限且非常死板的语言,做不了底层工作,有了指针就不一样了,因为可以直接操作内存,指针的用途可以说是讲不完的,但是一些基本用途是必须掌握的。
用于形参
形参是指针最基本的应用,实参按值传递,按值传递是数据克隆的过程,克隆后得副本数据,修改副本数据对原始数据不会产生影响,优点是安全,缺点是耗费资源多。形参传递数据地址,通过传递地址操作数据只会对同一份数据进行操作,优点是性能高,没有数据克隆这一过程,缺点是可能破坏原始数据,因此常常用常量指针来保护原始数据。
函数返回指针
既然指针可以用作形参,当然也可以用于函数的返回值,函数返回指针时书写格式如下:
int * fun(int a, int b)
{
int* p;
return p;
}
int* 表示返回类型为整数指针,由于栈中的数据是临时的,要避免返回函数内部创建的数据地址,但可以返回其它地址,例如传入的形参或堆上的地址,下面代码在函数中创建一个新的数组,然后返回它。
#include<stdio.h>
#include<stdlib.h>
int arr1[2]={
1,2};
int arr2[3]={
3,4,5};
int *arrCat(int arr1[2],int arr2[3])
{
int *arr=malloc(5);
int i=0;
for(i=0;i<5;i++) *(arr+i)=*((i<2?arr1:arr2)+(i<2?i:i-2));
return arr;
}
int main()
{
int *arr=arrCat(arr1,arr2);
for (int i = 0; i < 5; i++) printf("%d ",arr[i]);
free(arr);
}
函数arrCat连接两个传入的数组,在内部动态创建一个新数组并返回它,在main()中输出这个新数组后释放动态申请的空间。
处理数组
有了指针的概念后我们再来看数组概念就更明确了,数组名实际上是一个常量指针,不允许修改它的指向,但可以修改其指向数据的内容,它的类型为数据集合,但支持自动向下转型,它的值既是数据集合的首地址,也是首元素的地址,因为值相同才体现了自动转型带来的便利。在使用arr[i]时,arr被当作数据集合进行下标运算,而对于arr+i或直接使用arr来说arr被降级为元素类型。sizeof()在获取长度时会判断其类型,在sizeof(arr)被当作数据集合。将数组名作为地址赋值给指针和按形参传递都属于降级操作,自动转型一方面便于通过指针操作数组元素,另一方面也方便了形参传递。由于数组名既是数据集合也是首元素的地址,因此数组arr和&arr[0]地址相同,你可以定义一