C语言学习 — 指针知识细节说明
最近重新学习了一下C语言教学视频看了看,把一些知识点细节记录一下。
指针: 一种特殊的变量
- 指针是C语言中的变量 ,指针专用于保存程序元素的内存地址
- 可使用 * 操作符 通过指针访问程序元素本身
- 指针也有类型 , 指针类型由 数据类型 + * 构成
- 指针是变量,因此赋值时必须保证类型相同
- 指针变量保存的地址必须是有效地址
- 通过指针参数:
 能够实现函数交换变量的值
 能够从函数中“返回”多个值(return 只能返回一个值!)
数组
- 数组的本质是一片连续的内存
- 数组名并不是指针,只是代表了0号元素的地址,因此可以当做指针使用
- &a 与 a 在数值上相同,但是意义上不同
- C语言中的字符串常量的类型是 char*
- 当指针指向数组元素时, 才能进行指针运算  。
 i = *p++; p要指向数组元素时,这个语句才是正确的
函数
- 函数的本质是一段内存中的代码(占用一片连续内存)
- 函数拥有类型,函数类型由 返回类型  和 参数类型列表  组成:
  
- 函数名就是函数体代码的起始地址 (函数入口地址)
- 通过函数名调用函数, 本质为指定具体地址的跳转执行
- 因此,可定义指针,保存函数入口地址
函数指针( Type func (Type1 a, Type2 b))
- 函数名即入口地址, 类型为 Type (*) (Type1 ,Type2)
- 对于func 的函数, &func 与 func 数值相同,意义相同
- 指向函数的指针: Type (*pFunc)(Type1 ,Type2)=func;
- 函数指针的本质还是指针(变量,保存内存地址)
- 可定义函数指针参数,使用相同代码实现不同功能
函数指针的应用请看下面的例子
#include "stdio.h"
#include "string.h"
int add(int a, int b)
{
    return a + b;
}
int mul(int a, int b)
{
    return a * b;
}
int calculate(int a[], int len, int(*cal)(int, int))
{
    int ret = a[0];
    int i = 0;
    for(i=1; i<len; i++)
    {
        ret = cal(ret ,a[i]);//调用了add,    1.ret = add(ret, a[1])  ==>  add(a[0], a[1]) ==> a[0] + a[1]
         							 //   2.ret = add(ret, a[2])  ==> a[0] + a[1] + a[2]
    }
    return ret;
}
int main()
{
	int a[] = {1, 2, 3, 4, 5};
    int (*pFunc)(int, int) = NULL;
    pFunc = add;
    printf("%d\n",pFunc(1, 2));
    printf("%d\n",(*pFunc)(3, 4));
    pFunc = &mul;
    printf("%d\n",pFunc(5, 6));
    printf("%d\n",(*pFunc)(7, 8));
    printf("1 + ... + 5 = %d\n",calculate(a, 5, add));
    printf("1 * ... * 5 = %d\n",calculate(a, 5, mul));
	return 0;
}

再论数组参数
函数的数组形参退化为指针! 因此,不包含数组实参的长度信息。使用数组名调用时,传递的是 0号元素的地址。
请看下面的例子:
int demo(int arr[], int len) // 实际上函数变成这样了 int demo(int *arr, int len)
{
    int ret = 0;
    int i =0;
    
    // printf("demo: sizeof(arr) = %d\n", sizeof(arr));//gcc编译器这里编译不通过
    printf("arr = %p\n", arr); 
    while(i < len)
    {
        ret += *arr++; //*arr++ 这个算法只有在arr 是一个指针,并且指向数组元素时才正确,如果这里编译通过,说明arr是一个指针        
        i++;
    }
    return ret;
}
int main()
{
	int a[] = {1, 2, 3, 4, 5};
    // int v = *a++; 这句话编译不会通过,因为a是数组名,不是指针
    printf("a = %p\n", a);
    printf("return value: %d\n",demo(a, 5));
	return 0;
}

void指针类型
- void 类型是 基础类型 ,对应的指针类型为 void*
- void*是指针类型,其指针变量能够保存地址
- 通过void* 指针无法获取内存中的数据 (无长度信息)
char c = 0;
int i = 1;
float f = 2.0f;
double d = 3.0;
void* p = NULL;
double* pd = NULL;
int* pi = NULL;
/*
4个赋值语句都正确
void* 指针可以保存任意类型的地址
*/
p = &c;
p = &i;
p = &f;
p = &d; 
printf("%f\n, *p");// 错误的,无法访问
/*
void* 类型指针无法访问内存中的数据
void* 类型的变量可以合法的赋值给其他具体数据类型的指针变量
其他指针类型的变量不能相互赋值
*/
pd = p; //合法的
pi = P; //合法的
pd = pi ;// 错误的,不合法
堆空间的使用
- 工具箱:stdlib.h
- 申请: void* malloc(unsigned bytes)
- 归还: void free(void* p)
- malloc 申请内存后,应该判断是否申请成功 if(p != NULL)
注意:自己一直有一个问题,什么时候需要用到malloc申请堆空间的地址呢? 
 因为项目中基本上都没使用到过malloc,自己查了下网上的观点,总结一下:
 1、有时候,需要多大的空间事先并不知道,只有在程序运行时候才知道,所以需要使用malloc 动态分配内存空间,比如需要输入n个人员的资料,这个n不确定有多少的时候,就可以使用动态分配;
 2、但是,我可以说,我直接申请一个足够大的数组,大到完全可以存放一般情况下的人员资料情况, 这也可以的,只是这样的程序不是很“合理”,一般来说,栈空间容量是远小于堆空间,你在栈空间申请不能过大,而且过大往往大部分时候都是浪费。
 3、栈空间的数据在函数结束都就释放了,在堆空间中的东西,函数结束后还会存在,需要用户手动free,要不然函数结束后他还会存在。如果你想把这块内存在功能块之外用的时候,请用malloc,而不能用动态内存。不管这块内存多小
 4、普通项目中,确实直接用系统动态分配内存空间,足够了,因为现在一般项目中M0,M3的内核基本很少遇到内存不足的情况,内存不足,换个大内存的同类型= =!
int main()
{
	int* p = malloc(4 * sizeof(int));
	int i = 0;
	
	if( p != NULL)
	{
		for(i=0; i<4; i++)
		{
			p[i] = i * 10;
		}
		for(i=0; i<4; i++)
		{
			printf("%d\n", p[i]);
		}
		free(p);
	}
	return 0;
}

多元指针,指向指针的指针
Type V;
 Type* pv = &v;
 Type** ppv = &pv;
 Type*** pppv = &ppv;
int a = 0;
int b = 1;
int* p = &a;
int** pp = &p;
**pp = 2;  // a=2
*pp = &b; // p=&b
*p =3 ; // b=3
一个样例函数:
#include<stdio.h>
#include<stdlib.h>
int getDouble(double** pp, unsigned n)
{
	int ret = 0;
	double* pd = malloc(sizeof(double) * n);
	
	if(pd != NULL)
	{
        printf("pd = %p\n",pd);
		*pp = pd;
		ret = 1;
	}
	return ret;
}
int main()
{
    double* p = NULL;
    if(getDouble(&p, 5))
    {
        printf("p= %p\n",p);
        free(p);
    }
	return 0;
}

二维数组

int main()
{
	int b[][2] = {1,2,3,4};
	int(*pnb)[2] = b;  // b的类型是 int(*)[2]
	*pnb[1] = 30;
	printf("b[0][0] = %d\n",b[0][0]);
	printf("b[0][1] = %d\n",b[0][1]);
	printf("b[1][0] = %d\n",b[1][0]);
	printf("b[1][1] = %d\n",b[1][1]);
}

pnb指向了素组b, b是什么样子的数组呢?
 b: 0 号元素是 [1 ,2 ] 1号元素是 [3 ,4 ]
 所以 pnb[0] ——> [ 1, 2]
 pnb[1] ——> [ 3, 4]
 *pnb[1] 想通过地址来取内容, pnb[1]是一个素组,所以这个取得是pnb[1]数组的第0号元素,是[3 , 4]数组中的3,把3 变成30.
不同类型指针分析
- int p; //这是一个普通的整型变量;
- int *p; //首先从P 处开始,先与- *结合,所以说明P是一个指针,然后再与- int结合,说明指针所指向的内容的类型为int 型.所以P是一个返回整型数据的指针;
- int p[3]; //首先从P 处开始,先与- []结合,说明P 是一个数组,然后与- int结合,说明数组里的元素是整型的,所以P 是一个由整型数据组成的数组;
- int *p[3]; //首先从P 处开始,先与- []结合,因为其优先级比- *高,所以P是一个数组,然后再与- *结合,说明数组里的元素是指针类型,然后再与- int结合,说明指针所指向的内容的类型是整型的,所以P是一个由返回整型数据的指针所组成的数组;
- int (*p)[3]; //首先从P 处开始,先与- *结合,说明P是一个指针,然后再与- []结合 ( 与"()"这步可以忽略,只是为了改变优先级 ),说明指针所指向的内容是一个数组,然后再与- int结合,说明数组里的元素是整型的.所以P 是一个指向由整型数据组成的数组的指针;
- int **p; //首先从P开始,先与- *结合,说是P 是一个指针,然后再与- *结合,说明指针所指向的元素是指针,然后再与- int结合,说明该指针所指向的元素是整型数据,指向指针的指针;
- int p(int); //从P 处起,先与- ()结合,说明P是一个函数, 然后进入()里分析,说明该函数有一个整型变量的参数,然后再与- 外面的int结合,说明函数的返回值是一个整型数据 ;
- int(*p)(int); //从P 处开始,先与- *结合,说明P是一个指针,然后与- ()结合,说明指针指向的是一个函数,然后再与- ()里的int结合,说明函数有一个int 型的参数,再与- 最外层的int结合, 说明函数的返回类型是整型,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针;
- int *(*p(int))[3]; //从P 开始,先与- ()结合,说明P 是一个函数,然后进入()里面,与- int结合, 说明函数有一个整型变量参数, 然后再与外面的- *结合,说明函数返回的是一个指针, 然后到最外面一层,先与- []结合,说明返回的指针指向的是一个数组,然后再与- *结合,说明数组里的元素是指针,然后再与- int结合,说明指针指向的内容是整型数据.所以P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数。
- 点赞
- 收藏
- 关注作者
 
            
 
           
评论(0)