C语言指针的本质分析

本文详细探讨了C语言中指针与数组的本质及应用,包括指针的基本概念、运算规则、数组与指针的关系,以及它们在函数参数、内存操作中的作用。通过实例对比了指针与下标形式的效率,解析了数组参数退化为指针的现象。

@C指针的本质分析

回顾

  程序中的变量只是一段存储空间的别名,那么是不是必须通过这个别名才能使用这段存储空间?
  **一、思考**

这段程序输出什么?

      int i = 5int* p = &i;
      printf("%d,%p\n", i, p);
      *p = 10;
      printf("%d,%p\n", i, p);

*号的意义

一、在指针声明时,*号表示所声明的变量为指针
二、在指针使用时,*号表示取指针所指向的内存空间中的值

int i = 0;
int j = 0;
//指针声明
int* p = &i;
//取值
j = *p;

*号就好比一把钥匙,通过这边钥匙可以打开内存,并读取内存中的值;

传值调用与传址调用

一、指针是变量,因此可以声明指针参数
二、当一个函数内部需要改变实参的值时,则需要使用指针参数
三、函数调用时实参值将复制到形参
四、指针适用于复杂数据类型作为参数的函数

例子

1、传值调用

#include <stdio.h>

int swap(int a, int b)
{
	int c = 0;
	
	//典型的传值调用
	c = a;
	a = b;
	b = c;
	
}

int main()
{
	int aa = 1;
	int bb = 2;
	
	printf("aa = %d, bb = %d\n", aa, bb);
	
	swap(aa, bb);
	
	printf("aa = %d, bb = %d\n", aa, bb);
	
	return 0;
}

2、传址调用

#include <stdio.h>

int swap(int* a, int* b)
{
	int c = 0;
	
	//典型的传址调用
	c = *a;
	*a = *b;
	*b = c;
	
}

int main()
{
	int aa = 1;
	int bb = 2;
	
	printf("aa = %d, bb = %d\n", aa, bb);
	
	swap(&aa, &bb);
	
	printf("aa = %d, bb = %d\n", aa, bb);
	
	return 0;
}

常量与指针

const int* p;              //p可变,p指向的内容不可变
int const* p;              //p可变,p指向的内容不可变
int* const p;              //p不可变,p指向的内容可变
const int* const p         //p不可变,p指向的内容不可变

口诀:左数右指
当const出现在 * 号的左边时指针指向的数据为常量;
当const出现在 * 号的右边时指针本身就为常量;

#include <stdio.h>

int main()
{
	
	int i = 0;
	const int* p1 = &i;
	int const* p2 = &i;
	int* const p3 = &i;
	const int* const p4 = &i;
	
	*p1 = 1;     //compile error;
	p1 = NULL;   //ok
	
	*p2 = 2;     //compile error;
	p2 = NULL;   //ok
	
	*p3 = 3;     //ok
	p3 = NULL;   //compile error;
	
	*p4 = 4;     //compile error;
	p4 = NULL;   //compile error;
	
	return 0;
	
}

数组的概念

一、数组是相同类型的变量的有序集合

                       **数组示意图:定义了一个数组a[5],数组包含5个int类型的数据**                              
a[0]a[1]a[2]a[3]a[5]

这个表格中代表的是int a[5];有五个int类型的数据,并且每个元素都是int类型的数据;可以通过这个表格中可以看出有20个字节的空间的名字为a;a[0]、a[1]等元素都是a中的元素,并非元素的名字;数组中的元素是没有名字的。
其中a代表的数组第一个元素的起始地址。

数组的大小

一、数组在一片连续的内存空间中存储元素
二、数组元素的个数可以是显示或隐式指定

int a[5] = {1, 2};
int b[ ] = {1, 2}

数组地址与数组名

一、数组名代表数组首元素的地址
二、数组的地址需要用取地址符&才能得到
三、数组首元素的地址与数组的地址值相同;
四、数组首元素的地址与数组的地址是两个不同的概念

#include <stdio.h>

int main()
{
	
	int a[5] = {0};
	int b[]  = {1, 2};
	
	printf("a = %p\n", a);
	printf("&a = %p\n", &a);
	printf("&a[0] = %p\n", &a[0]);
	
	return 0;
	
}

数组的盲点

一、数组名可以看做一个常量指针
二、数组名 “指向” 的是内存中数值首元素的起始位置
三、数组名不包含数组的长度信息
四、在表达式中只能作为右值使用
五、只有在下列场合中数组名不能看着常量指针
1、数组名作为sizeof操作符的参数
2、数组名作为&运算符的参数

#include <stdio.h>



int main()
{
	
	int a[5] = {0};
	int b[2];
	int* p = NULL;
	
	p = a;
	
	printf("a = %p\n", a);
	printf("p = %p\n", p);
	printf("&p = %p\n", &p);
	printf("sizeof(a) = %d\n", sizeof(a));	
	printf("sizeof(p) = %d\n", sizeof(p));
	
	printf("\n");
	
	p = b;
	
	printf("b = %p\n", b);
	printf("p = %p\n", p);
	printf("&p = %p\n", &p);
	printf("sizeof(b) = %d\n", sizeof(b));	
	printf("sizeof(p) = %d\n", sizeof(p));
	
	//b = a;       //数组名只能作为右值(可以看成常量指针)
	
	return 0;
	
}

小结

一、数组是一片连续的内存空间
二、数组的地址和数组首元素的地址的意义不同
三、数组名在大多数情况下被当成常量指针处理
四、数组名其实并不是指针,不能将其等同于指针

数组的本质

一、数组是一片连续的内存空间
二、数组的空间大小为sizeof(array_type)* array_size
三、数组名可以看作指向数组第一个元素的常量指针

#include <stdio.h>



int main()
{
	int a[5] = {};
	int* p = NULL;
	
	printf("a = 0x%X\n", (unsigned int)(a));               //a = 0x53208C10   
	printf("a + 1 = 0x%X\n", (unsigned int)(a + 1));       //a + 1 = 0x53208C14   
	
	printf("p = 0x%X\n", (unsigned int)(p));               // p = 0x00
	printf("p + 1 = 0x%X\n", (unsigned int)(p + 1));       // p + 1 = 0x04
	
	return 0;
}

指针的运算

一、指针是一种特殊的变量,与整数的运算规则为

                         **p + n; <- ->(unsigned int)p +n*sizeof(*p);**

结论:
当指针p指向一个同类型的数组的元素时:p + 1将指向当前元素的下一个元素;
p - 1将指向当前元素的上一个元素。

二、指针之间只支持减法运算,参与减法的运算的指针类型必须相同;

p1-p2; <- ->((unsigned int)p1 -(unsigned int)p2)/sizeof(type))

注意:
1、只有当两个指针指向同一个数组的元素时,指针相减才有意义,其意义为指针所指元素的下标;
2、当两个指针指向的元素不在同一个数组中时,结果未定义;

三、指针的比较
1、指针也可以进行关系运算(<、<=、>、>=)
2、指针关系运算的前提是同时指向同一个数值中的元素;
3、任意两个指针之间的比较运算(==,!=)无限制;
4、参与比较运算的指针类型必须相同;

例题1

int main()
{

    char s1[] = {'H', 'e', 'l', 'l', 'o'};
    int i = 0;
    char s2[] = {'W', 'o', 'r', 'l', 'd'};
    char* p0 = s1;
    char* p1 = &s1[3];
    char* p2 = s2;
    int* p = &i;
	
    printf("%d\n", p0 - p1);
    printf("%d\n", p0 + p2);
    printf("%d\n", p0 - p2);
    printf("%d\n", p0 - p);
    printf("%d\n", p0 * p2);
    printf("%d\n", p0 / p2);
}

编译结果;
在这里插入图片描述

例题2

#include <stdio.h>

#define DIM(a) (sizeof(a) / sizeof(*a))

int main()
{

	char s[] = {'H','e', 'l', 'l', 'o'};
    char* pBegin = s;
	char* pEnd = s + DIM(s);
	char* p  = NULL;
	
	
	printf(" pBegin = %p\n", pBegin);
    printf(" pEnd = %p\n", pEnd);
    printf("Size: %ld\n", pEnd - pBegin);
	
	for(p = pBegin; p<pEnd; p++)
	{
		printf("%c", *p);
	}
	
	printf("\n");
	
	return 0;
}

运行结果
在这里插入图片描述

指针和数组分析

下标形式VS指针形式

一、指针以固定增量在数组中移动时,效率高于下标形式;
二、指针增量为1且硬件具有硬件增量模型时,效率更高;
三、下标形式与指针形式的转换

                   **a[n] <--> *(a + n)<--> *(n + a) <-->n[a]** 

注意:
现代编译器的生成代码优化率已经大大提高,在固定增量时,下标形式得到效率已经和指针形式相当;但从可读性和代码维护的角度来看,下标形式更优。

#include <stdio.h>


int main()
{
    int a[5] = {0};
    int* p = a;
    int i = 0;
    
    for(i=0; i<5; i++)
    {
        p[i] = i + 1;
    }
    
    for(i=0; i<5; i++)
    {
        printf("a[%d] = %d\n", i, *(a + i));
    }
    
    printf("\n");
    
    for(i=0; i<5; i++)
    {
        i[a] = i + 10;
    }
    
    for(i=0; i<5; i++)
    {
        printf("p[%d] = %d\n", i, p[i]);
    }
   
    return 0;
}

运行结果
在这里插入图片描述

数组参数

一、数组作为函数参数时,编译器将其编译成对应的指针

void f(int a[]); <--> void f(int* a)
void f(int a[5]); <--> void f(int* a)

结论:
一般情况下,当定义的函数中有数组参数时,需要定义另一个参数来标示数组的大小。

总结:
1、数组名和指针使用方式相同
1)数组的本质不是指针;
2)指针的本质不是数组;
2、数组名并不是数组的地址,而是数组首元素的地址;
3、函数的数组参数退化为指针。

#include <stdio.h>

void func1(char a[5])
{
    printf("In func1: sizeof(a) = %d\n", sizeof(a));  //大小为4个字节
    
    *a = 'a';
    
    a = NULL;
}

void func2(char b[])
{
    printf("In func2: sizeof(b) = %d\n", sizeof(b));   //大小为4个字节
    
    *b = 'b';
    
    b = NULL;
}

int main()
{
    char array[10] = {0};
    
    func1(array);
    
    printf("array[0] = %c\n", array[0]);        //打印为0
    
    func2(array);
    
    printf("array[0] = %c\n", array[0]);        //打印为0
    
    return 0;
}

编译是通过的

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值