C指针

指针是一种的变量的类型,存储的是另一个变量的内存地址。

每一个变量系统都需要分配内存来存放,有其对应的内存地址。

指针类型变量自身也是变量,也需要内存,也有地址。

指针定义

语法

类型 *变量名;

定义指针类型并赋值为空指针

int *p = NULL;

定义指针时,可以理解为定义了一个叫做*p的变量,p是这个变量的地址。

使用指针

通过取地址&运算符取一个变量的地址,可以将地址赋值给指针类型变量。

int i = 10;
int *p = &i;

假设内存地址范围是100~200,变量i的地址是100,变量p的地址是104: 变量i的值是10,变量p的值是100。

通过*运算符,可以取指针变量指向的值。

int i =10;
int *p = &i;
int j = *p;//10

假设变量j的地址是108: 将内存地址100的值取出来,是10,放到内存编号108的位置。

指针类型占用空间

所有类型的指针保存的都是地址,占用空间都是一致的,32位程序是4字节,64位程序是8字节。

不同类型指针意味者其指向的类型不同,影响指针的运算和通过指针的操作。

指针的本质

指针是一种类型,保存另一变量的地址。

通过指针类型和本身类型操作变量是两者不同方式。

本身类型是直接操作,不需要了解地址。

指针类型是间接操作,需要先通过地址定位,再进行操作。

功能 指针 直接
获取地址 p &i
获取取值 *p i

指针的作用

指针的存在是为了更好的编写代码,而不是为了为难使用者。

传递参数

思考一些,你需要一个函数,用来交换两个整型变量。

交换函数

void swap(int a,int b)
{
    int t = a;
    a = b;
    b = t;
}

调用

int x = 100,y=200;
swap(x,y);
printf("x=%d,y=%d",x,y);//x=100,y=200

由于形参是副本,对a,b的修改仅限于函数内部,无法影响到外部。

void swap(int* a,int* b)
{
    int t = *a;
    *a = *b;
    *b = t;
}
int x = 100,y=200;
swap(&x,&y);
printf("x=%d,y=%d",x,y);//x=200,y=100

指针可以按照地址传递参数,使函数可以影响调用者。

对于占用字节多的类型,使用指针可以提高效率。

效率

结构体通常占用较多的字节,采用指针的形式可以更高效的传递。

使用动态分配的内存

动态分配的内存是以指针的形式返回的,需要通过指针使用内存。

指针的运算

指针支持加法、减法、比较大小、比较相等运算。

指针和整数的加减法

指针的加减是将指针的指向移动到下一个或上一个内存位置。

指针移动1次,移动的字节数量是指针指向类型所占字节

int i = 100;
int *p = &i;//假设指针p的地址为100

int *q = p + 1;//q为100+sizeof(int)=104,指针移动字节数为针指向类型所占字节。

指针间的减法

指针间的减法可以计算两个内存地址间的间隔,以指针指向类型作为基本单位。

int i = 100;
int j = 200;
int offset = &i - &j;

指针间的减法一般用于数组、相邻内存,用来计算两个内存间间隔的元素数量。

指针的条件运算

指针间可以比较大小、比较相等运算

比较大小可以确定指针间的位置关系。 比较相等可以确定指针间的相等关系。

指针和常量

指针和常量可以同时修饰一个变量,根据修饰位置,分为不同类型。

常量指针

一个指针,指针指向一个常量,指针自身可以修改,指向的值不能修改。

int i = 100;
const int *p = &i;
//int const *p = &i;//效果一致

p=NULL;//可以修改自身
*p = 200;//编译错误,不能修改指针指向的值

常量指针只是禁止了通过指针修改指向的值,不要求指向的值真的是常量。

一般,对于无需修改的指针参数,会使用常量指针,以防错误的修改指向的值。

指针常量

一个指针,指针自身是常量。指针自身不能修改,指向的值可以修改。

int i = 100;
int *const p = &i;

p=NULL;//编译错误,不能修改指针自身
*p = 200;//可以修改指针指向的值

指针常量和一般常量类似,不能为变量自身多次赋值。

自行定义指针常量的情况较少。

常量指针常量

一个指针,自身是常量,指向的值也是常量。

int i = 100;
const int *const p = &i;

p=NULL;//编译错误,不能修改指针自身
*p = 200;//编译错误,不能修改指针指向的值

常量指针常量是两者的结合体,既不能修改指针自身,又不能修改指向的值。

区分

const和谁离的近,谁就是常量。

const靠近变量名,就是指针常量,靠近*就是常量指针。

无类型指针

void*类型的指针是无类型指针。

int i = 65536;
void* p = &i;
short* q = (short*)p;
short lo = *q;//访问变量i所在地址的低位的2字节
short hi = *(q+1);//访问变量i所在地址的高位的2字节

无类型指针不能进行加减运算,如果需要按照字节进行指针加减,可以使用char*类型。

无类型指针的作用

作为返回值,表示类型事先未知

动态内存分配malloc等函数返回的类型是void*,表示类型未知。调用者可以将其作为任何类型使用。

一般这类函数需要提供字节数量作为参数。

作为参数,表示函数可以处理任意类型

void*表示可以针对任意类型进行一定操作,比如按照字节比较大小。

一般这类函数需要同时提供类型所占用的字节作为参数。

指针之间的转换

任意类型指针之间都可以进行强制类型转换。

将一个类型的指针转换为另一个类型指针,指针保存的地址不变,指针的移动字节数量、赋值和读取类型变了。

int i = 65536;
short* p = (short*)&i;//将整数看作2个元素的短整型数组
short lo = *p;
short hi = *(p+1);

对指针类型进行转换时,需要明确两个类型之间字节关系。

指针数组和数组指针

指针数组

指针数组是一个数组,每个元素都是一个指针。

int* a[5];

a[0] = NULL;
a[4] = NULL;

a是一个数组,包含5个元素,每个元素都是int*类型,

数组指针

数组指针是一个指针,指向数组。

int (*p)[5] = NULL;
int a[5] = {1,2,3,4,5};
p = &a;
p[4];//4

指针数组指针

指针数组指针是一个指针,指向一个数组,数组的每个元素都是指针

int* (*p)[5] = NULL;

int* a[5];

p = &a;

p是一个指针,指向一个数组,该数组包含5个元素,每个元素都是int*类型。

数组指针数组

数组指针数组是一个数组,每个元素都是数组指针,指向数组。

int(*a[5])[5];

a是一个数组,包含5个元素,每个元素都是数组指针,该指针指向一个包含5个元素类型维int的数组。

总结

变量和谁先结合,变量的基本类型就是它。先和*结合,就是指针;先和[]结合,就是数组。

为了简化复杂和类型,可以使用typedef定义别名,语法和声明变量一致。

int*(*a[5])[5];

指针和一维数组

数组是一片连续的内存,通过下标操作不同位置。

指针可以指向一片连续的内存,通过加减操作不同位置。

数组和指针是两种操作内存的方式。

用指针访问一维数组

数组名是一个指针常量,指向的数组的首元素。

使用sizeof(数组名)获取的是整个数组的大小。

数组名每移动1个单位,移动1个元素。

使用指针来访问数组

int a[5] = {1,2,3,4,5};//a的类型是:int(*)[5]
int size=sizeof(a);//20
int *p1 = a;
int *p2 = &a[0];
int e1 = *a;//1 数组的第1个元素
int e2 = *(a+1)//2 数组的第2个元素

数组名取地址

数组名取地址的结果是一个一维数组指针。

数组指针每次移动,移动整个数组。

int a[] = {1,2,3,4,5};
int (*p)[5] = &a;
printf("%p,%p",p,p+1);//假设p为100,p+1为120

p是一个指向5个元素的数组指针,每移动1个单位,移动20个字节。

将指针当作数组使用

可以将指针当作一维数组操作。

int i = 100;
int *p = &i;
int j = p[0];

可以将指针p作为数组访问,但p不是数组,sizeof(p)获取的是指针的大小,是4或8。

结论

a[n]*(a+n)始终等价,即使n是负数。

数组、指针是访问内存数据的不同方式。在访问非首元素时,使用数组的形式更加简便。

指针和二维数组

二维数组的数组名同样是指针常量,指向第一行,是一维数组指针常量。

指向二维数组某一行的指针也被称作行指针

行指针每次移动一维数组元素数量。

二维数组取地址是指向二维数组的指针,每次移动是整个二维数组大小个字节。

二维数组首元素的地址是元素指针,每次移动是1个元素大小字节。

int a[2][3] = {1,2,3,4,5,6};

int(*p1)[3] = a;//指向2维数组的第1行,是一个1维数组,每次移动3个元素
int(*p2)[2][3] = &a;//指向一个2维数组,是2维数组指针,每次移动6个元素
int(*p3)[3] = &a[0];//指向2维数组的第1行,是一个1维数组指针,每次移动3个元素
int* p4 = &a[0][0];//指向2维数组首元素,是一个元素指针,每次移动1个元素
int a[2][3] = {1,2,3,4,5,6};
int (*p2)[3] = &a[0];
p2++;//p2现在指向数组的第2行
printf("%d\n",p2[0]);//4

a[m][n]等价于*(*(a+m)+n)

指针和多维数组

多维数组名称同样是指针常量,指向上一维度的第一个数组。

对多余维度取地址结果是多维数组指针,每次移动整个多维数组大小。

多维数组每个维度都可以取地址,取得结果是上一维度的数组指针,每次移动上一维度数组大小。

int a[2][2][2] = { 1,2,3,4,5,6,7,8 };

int(*p1)[2][2] = a;//2维数组指针,加减每次移动4个元素
int(*p2)[2][2][2] = &a;//3维数组指针,指向3维数组头部,每次移动8个元素
int(*p3)[2][2] = &a[0];//2维数组指针,指向2维数组头部,加减每次移动4个元素
int(*p4)[2] = &a[0][0];//1维数组指针,指向1维数组头部,加减每次移动2个元素
int* p5 = &a[0][0][0];//元素指针,加减每次移动1个元素

数组指针类型的转换

数组指针之间可以进行强制转换,将其以另一种类型来访问。

将1维数组作为二维数组访问

int a[] = { 1,2,3,4,5,6 };
int(*p)[2][3] = a;

将2维数组作为1维数组访问

int a[2][3] = { 1,2,3,4,5,6 };
int *p = a;

总之,只要了解存储结构,可以将数据以任意类型的指针来操作。指针提供了灵活性。

作为参数的数组

数组类型参数实际是指针类型,只是将其作为数组来访问。 数组类型参数的数组长度是没有意义的。 调用时,参数类型信息是有所丢失的。

void test_func_array(int a[5],int n)//第一个参数的长度5没有意义
{
	printf("%d\n", sizeof(a));//4或8。是指针类型的大小
}

void test_pointer_array()
{
	int a[] = { 1,2,3,4,5 };
	printf("%d\n",sizeof(a));//20。是整个数组的大小
	test_func_array(a, 5);//函数调用时,类型信息有丢失,从int(*)[5]下降为int*。
}

如果要保持数组参数的信息,需要使用数组指针。

void test_func_array2(int(*p)[5])
{
	printf("%d\n", sizeof(*p));//20
}

void test_pointer_array2()
{
	int a[] = { 1,2,3,4,5 };

	int(*p)[5] = a;

	printf("%d\n", sizeof(a));//20
	test_func_array2(a);
}

此时,参数信息没有丢失。不过,这也限制了数组需要固定长度,不利于通用算法。

一般,我们允许参数信息丢失,会另外定义一个参数传递数组长度。

函数指针

函数名是一个地址,使用函数指针可以保存函数地址。

可以向调用函数一样调用函数指针。

int func_add(int a,int b)
{
    return a + b;
}
int (*p)(int, int) = func_add;
int sum = p(100, 200);

函数指针的作用

函数指针可以将一段代码参数化,加强代码的复用。

void filter(int[] a,int n,int(*predicate)(int))
{
    int i=0;
    for(i<n;i++)
    {
        if(predicate && predicate(a[i]))
        {
            printf("%d\n",a[i]);
        }
    }
}

函数可以按照提供的条件打印,条件是未知的,由调用者传递的。

在函数内部通过函数指针来调用另一个函数,叫做回调

多重指针

指针定义语法:

类型 *变量名;

如果类型是指针,那么变量就是指向指针类型的指针,即多重指针。

一般常见的是2重,3重指针。

int i = 100;
int *p = &i;
int **p = &p;

多重指针的加减法运算,每次移动的单位是指针大小。

最常见的2中指针是使用字符串数组的情况。main函数的argv参数就是如此。

复杂类型

复杂类型的辨认

f()函数调用,a[]数组运算符优先级是最高的,如果一个变量后跟这两个运算符,就是函数定义或数组定义。

(*f)(),(*a)[]使用括号改变了优先级,两者都是指针,分别是函数指针和数组指针。

哪个运算符最先结合,变量就是哪个个类型。

复杂类型的读法

void* f();//函数声明,没有参数,返回值是void指针类型

void (*g)();//函数指针,指向一个没有参数,没有返回值的函数。

void (*h)(int (*)(void*, void*));//函数指针,函数包含一个参数,没有返回值。参数也是函数指针,包含两个void*类型参数,和int类型返回值。

int (*a)[10];//一个指针,指向包含10个int类型元素的数组

int* b[10];//一个数组,元素是int类型指针

void (*c[10])() ;//一个数组,元素是函数指针,函数没有参数和返回值

int* (*hh(int))[3];//函数声明,参数是int类型,返回值是数组指针,指向包含3个元素的数组,数组元素是int*类型

复杂类型的typedef

对于复杂类型,可以使用和定义变量一致的语法,使用typedef定义别名。 使用typedef定义别名后,更便于使用。

typedef void (*callback)();
typedef int (*p_int_10)[10];

callback c[10];//10个元素的数组,每个元素都是callback类型函数指针
p_int_10 a[10];//10个元素的数组,每个元素都是指向10个元素的数组指针