C语言总结十:指针全网最详细总结-从入门到精通

发布时间:2024年01月17日

? ? ? ?C语言作为一种底层开发语言,是因为它可以直接访问内存,对内存单元进行操作,指针作为C语言的灵魂,提供了这种机会。C语言的各种语法其实从本质上都可以理解为通过指针对内存的操作,因此学好指针至关重要!本篇博客由浅入深详细的总结指针的所有内容,学完本篇博客,可以达到理解到运用的层次水平!

指针的本质:

? ? ? ? 指针其实就是指针变量,它用来保存内存单元的编号,也就是地址,那为什么要叫指针呢?是因为一个编号/地址对应一块内存单元,可理解为编号/地址指向一块内存单元,因此形象的把它叫做指针,我们使用指针其实就是使用指针变量存放的地址编号,通过对地址解引用操作,便可以拿到这块内存单元存放的数据,可以对这块内存单元的数据访问或者修改!!!

一、指针是什么

1.1 内存和地址

1.1.1 内存

? ? ? ?内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。其实内存的使用和现实生活中的空间使用本质上是一样的,设想有个生活中的案例:假设有一栋宿舍楼,把你放在楼里,楼上有100个房间,但是房间没有编号,你的一个朋友来找你玩,如果想找到你,就得挨个房子去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,如:

生活中,每个房间有了房间号,就能提高效率,能快速的找到房间。如果把上面的例子对照到计算机中,又是怎么样呢?

? ? ? 我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那我们买电脑的时候,电脑上内存是8GB/16GB/32GB等,那这些内存空间如何高效的管理呢? 那么我们就需要明白两个问题:

1、内存是怎么编号的?

2、一个内存单元是多大的空间?

? ? ? ? 目前微软的操作系统有32位(x86)和64位的,这里以32位为例,进行说明,32位代表有32根地址总线,本质上是物理线,由通电和断电进行表示(0/1),因此电信号便可以转化成数字信号(信息),即32位0或者1组成的二进制序列(占据4个字节),因此便会有2的32次方个组合,这也代表有2的32次方个内存单元!

? ? ?

总结:?
? ? ? ? 内存划分为一个个的内存单元,每个内存单元的大小取1个字节。其中,每个内存单元,相当于一个学生宿舍,一个人字节空间里面能放8个比特位,就好比学生们住的八人间,每个人是一个比特位。每个内存单元也都有一个编号(这个编号就相当于宿舍房间的门牌号),有了这个内存单元的编号,CPU就可以快速找到一个内存空间。生活中我们把门牌号也叫地址,在计算机中我们把内存单元的编号也称为地址。C语言中给地址起了新的名字叫:指针。指针是一个变量,它是用来存放的地址的!使用指针,其实是使用指针存放的地址,因此指针与地址等价!

? ? ? ?另外对于32位操作系统的4G内存单元又划分为不同的区域,分别用来存储不同类型的数据,相关知识在动态内存管理章节已经详细总结过。

?所以我们可以理解为: 内存单元的编号==地址==指针

1.1.2 如何理解编址

? ? ? CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节很多,所以需要给内存进行编址(就如同宿舍很多,需要给宿舍编号一样)。计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。

? ? ? 钢琴、吉他上面没有写上“都瑞咪发嗦啦”这样的信息,但演奏者照样能够准确找到每一个琴弦的每一个位置,这是为何?因为制造商已经在乐器硬件层面上设计好了,并且所有的演奏者都知道。本质是一种约定出来的共识!
硬件编址也是如此

? ? ? ?首先,必须理解,计算机内是有很多的硬件单元,而硬件单元是要互相协同工作的。所谓的协同,至少相互之间要能够进行数据传递。但是硬件与硬件之间是互相独立的,那么如何通信呢?答案很简单,用"线"连起来。而CPU和内存之间也是有大量的数据交互的,所以,两者必须也用线连起来。不过,我们今天关心一组线,叫做地址总线。

? ? ? ?我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表示0,1【电脉冲有无】,那么一根线,就能表示2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2^32种含义,每一种含义都代表一个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传入CPU内寄存器。

1.2? 指针变量和地址

1.2.1 取地址操作符&

#include <stdio.h>
int main()
{
   int num = 10;  //num占用4个字节
   &num;//取出num的地址
   //注:这里num的4个字节,每个字节都有地址,取出的是第一个字节的地址(较小的地址)
   printf("%p\n", &num);//打印地址,%p是以地址的形式打印
   return 0;
}

? ? ? 按照上面的图展示,会打印处理:0012ff44,&num取出的是num所占4个字节中地址较小的字节的地址。虽然整型变量占用4个字节,我们只要知道了第一个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。

? ? ? 注意地址是编号,它是一个数!!而不是变量!!取地址操作符&只能针对的是变量!

1.2.2 指针变量和解引用操作符*

(1)指针变量和解引用操作符*

? ? ? ?我们通过取地址操作符(&)拿到的地址是一个数值,比如:0x006FFD70,这个数值有时候也是需要存储起来,方便后期再使用的,那我们把这样的地址值存放在哪里呢?

答案是:指针变量中

? ? ? ?如何使用呢?在现实生活中,我们使用地址要找到一个房间,在房间里可以拿去或者存放物品C语言中其实也是一样的,我们只要拿到了地址(指针),就可以通过地址 (指针)找到地址()指向的对象,这里必须学习一个操作符叫解引用操作符*?

#include <stdio.h>
int main()
{
    int num = 10;//在内存中开辟一块空间
    int *p = &num;//这里我们对变量num,取出它的地址,可以使用&操作符。
    //num变量占用4个字节的空间,这里是将num的4个字节的第一个字节的地址存放在p变量
中,p就是一个指针变量。
    *p=20;          //相当于num=20;
   //通过解引用操作符,修改num的值
    return 0;
}

?

? ? ? 上面代码中就使用了解引用操作符,*p的意思就是通过p中存放的地址,找到指向的空间,*p其实就是a变量了;所以*p=20,这个操作符是把num改成了20。
? ? ? 有同学肯定在想,这里如果目的就是把a改成0的话,写成a = 0;不就完了,为啥非要使用指针呢?其实这里是把a的修改交给了pa来操作,这样对a的修改,就多了一种的途径,写代码就会更加灵活,后期慢慢就能理解了。

番外总结:

?

(2)拆解指针类型(认真理解指针的类型!!!非常重要

? ? ? 指针的类型为该指针变量所指向的数据的类型!!!

? ? ? 指针的定义方式为:指针类型? *指针变量名,因此想要准确的知道和区分指针的不同类型,必须要知道它所指向数据的类型!

? ? ?比如:整型指针,字符串指针,数组指针、函数指针,结构体指针,他们的侧重点都是后面的指针,即它们都是指针,只不过是指向不同数据类型的数据;

  1. 整型指针指的是指向整型数据的指针(保存整型变量的地址的指针);
  2. 字符串指针指的是指向字符串的指针(保存字符串首字符地址的指针);
  3. 数组指针指的是指向数组的指针(保存的是整个数组的地址);
  4. 函数指针指的是指向函数占用内存的首地址的指针(保存函数的入口地址)。
  5. 结构体指针指的是指向结构体的指针(保存的是结构体的地址)

注意区分:

  1. ? 数组名是数组首元素的地址,可以当作一个指针使用;
  2. ? 字符串名是字符串首字符的地址,可以当作是一个指针使用;
  3. ? 结构体名与数组名和字符串名不同,结构体名在任何表达式中他表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加取地址符&;

void*指针:

? ? ??在指针类型中有一种特殊的类型是void* 类型的,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但是也有局限性,void*类型的指针不能直接进行指针的+-整数和解引用的运算。

? ? ? void*类型的指针可以接收不同类型的地址,但是无法直接进行指针运算。

那么void*类型的指针到底有什么用呢?
? ? ? ? 一般void*类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得一个函数来处理多种类型的数据。通过指针强转即可使用!

1.2.3 指针变量的大小

? ? ? ?前面的内容我们了解到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产生的2进制序列当做一个地址,那么一个地址就是32个bit位,需要4个字节才能存储。如果指针变量是用来存放地址的,那么指针变的大小就得是4个字节的空间才可以。同理64位机器,假设有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储起来就需要8个字节的空间,指针变的大小就是8个字节。

  1. 在32位的机器上,地址是32个0或者1组成二进制序列,32个bit位,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
  2. 在64位机器上,如果有64个地址线,64个bit位,那一个指针变量的大小是8个字节,才能存放一个地址。
  3. 注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。?

二、指针类型的意义

2.1 对指针解引用的影响

?通过调试得到如下结果:

结论:

? ? ??指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)!

比如: char* 的指针解引用就只能访问一个字节,而int* 的指针的解引用就能访问四个字节。

2.2 对指针+-整数(指针加1的能力)的影响

总结:

? ? ?指针的类型决定了指针向前或者向后走一步有多大(距离)!指针加1的能力!

可以看出,char*类型的指针变量+1跳过1个字节,int*类型的指针变量+1跳过了4个字节。这就是指针变量的类型差异带来的变化。

三、const与指针的结合

const与指针配合使用的作用:

  1. 限制指针变量的指向
  2. 限制指针变量指向的数据
  3. 既要限制指针变量又限制指针变量指向的数据(双重限定)
  4. const与函数的形参(形参为指针)结合

3.1 const 限制指针变量的指向

? ? ? ?const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针解引用来改变,但是指针变量本身的内容可变(指针的指向可以发生变化,可以保存其他变量的地址)。

如:const int *ip; int const *ip;
上面两种写法都可以,一般使用第二种,限制指针变量指向的数据的意思就是指针可
以指向不同的变量(指针本身的值可以修改),但是不能用指针修改指针指向的数据的值,

3.2 const 限制指针变量指向的数据

? ? ? const如果放在*的右边,修饰的是指针变量本身,指针变量的内容不能修改,指针的指向不可以发生变化),但是指针指向的数据,可以通过指针解引用改变。所以 被 const 修饰的指针变量指针只能在定义时初始化,不能定义之后再赋值

3.3 const既要限制指针变量又限制指针变量指向的数据(双重限定)

? ? ?const既有放在*左边的也有放在右边的,此时,指针变量和指针变量指向数据的值都不能修改

3.4 const与函数的形参(形参为指针)结合

? ? ? ?如果函数的形参是一个指针,为了防止在函数内部修改指针指向的数据,就可以用const修饰!

用指针实现mystrlen()
#include<stdio.h>
#include <cassert>
int my_strlen1(const char* str)
{
	assert(str != NULL);
	int count = 0;
	while (*str != '\0')     //*str++ != '\0'
	{
		count++;
		str++;
	}
	return count;
}
指针实现my_strcat()
#include <cassert>
#include <stdio.h>
#include <string.h>
void my_strcat(char* str, const char* src)
{
	assert(str != NULL && src != NULL);
	while (*str++);                            //str 指针定位到\0,str走到\0停止
	str--;
	while (*str++ = *src++);                  //str 跟 src 指针同时向后遍历
}
指针实现my_strcpy()
#include <cassert>
void mystrcpy(char* str, const char* src)
{
	assert(str != NULL && src != NULL);
	while (*str)
	{
		*str++ = *src++;
	}
	*str = '\0';
}
指针实现my_strcmp()    
#include <cassert>  
int my_strcmp(const char* str, const char* src)
{
	assert(str != NULL && src != NULL);
	while (*str++ == *src++)
	{
		if (*str == '\0')
			return 0;
	}
	return *str > *src ? 1 : -1;
}

四、野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

4.1 造成野指针的原因

4.1.1 指针未初始化

#include<stdio.h>
int main()
{
	int* p;//指针未初始化
	*p = 20;
	return 0;
}

? ? ?上述代码就犯了一个严重的错误,即使用了未初始化的指针,此时指针p指向一片我们为止的空间,我们进行了指针操作使得指针p修改了所指向地址的四个字节的数据,这有可能造成严重的后果,在写代码时我们一定要避免使用未初始化的指针。?

4.1.2 指针越界访问

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int i = 0;
	for(i=0;i<=10;i++)
	{
		*p = 0;
		p++;
		//arr数组中只有十个变量,循环有十一次,当我们进行到第十一次时已经越界访问了,此时的指针p为野指针
	}
    return 0;
}

?arr数组中只有十个变量,循环有十一次,当我们进行到第十一次时已经越界访问了,此时的指针p为野指针?

4.1.3 指针指向的空间被释放

#include<stdio.h>
int *test()
{
	int n = 100;
	return &n;
}
 
int main()
{
	int* p = test();
	printf("%d", *p);
	return 0;
}

?上述代码看似好像没问题,实际上已经出现了野指针的问题,变量n定义在函数test中,所以它是一个局部变量,我们知道局部变量会在函数调用结束时被销毁即结束生命周期,此时我们想要打印的*p其实已经被销毁了,此时的指针p指向一个被释放的空间所以指针p是野指针。

4.2 如何规避野指针

4.2.1?指针初始化

#include<stdio.h>
int main()
{
	int n = 100;
	int* p = &n;	//1.如果知道指针指向哪里就赋值地址
	int* q = NULL;	//2.不知道指针指向哪里就让它指向NULL
    return 0;
}

如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL,NULL是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。

4.2.2 小心指针越界

一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。

4.2.3?指针指向空间释放,及时置NULL(如动态内存章节)

? ? ? ?当指针变量指向一块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的一个规则就是:只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL。
? ? ? 我们可以把野指针想象成野狗,野狗放任不管是非常危险的,所以我们可以找一棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓前来,就是把野指针暂时管理起来。
? ? ? 不过野狗即使拴起来我们也要绕着走,不能去挑逗野狗,有点危险;对于指针也是,在使用之前,我们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使用,如果不是我们再去使用。

4.2.4 避免返回局部变量的地址

不要返回局部变量的地址

4.2.5 指针使用之前检查有效性

常常使用assert()进行断言判断

? ? ??assert.h头文件定义了宏 assert(),用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被称为“断言”。

? ? ? ?用assert断言判断一个指针是否为空:

assert(p!=NULL)   或者 assert(p)

? ? ? ?上面代码在程序运行到这一行语句时,验证指针变量p是否等于NULL。如果确实不等于NULL,程序继续运行,否则就会终止运行,并且给出报错信息提示。

? ? ? assert()宏接受一个表达式作为参数。如果该表达式为真(返回值非零),assert不会产生任何作用,程序继续运行。如果该表达式为假(返回值为零),assert就会报错,在标准错误流stderr 中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。

? ? assert()的缺点是: 因为引入额外的检查,增加了程序的运行时间。一般我们可以在Debug中使用,在Release版本中选择禁用assert就行,在VS这样的集成开发环境中,在Release版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在Release版本不影响用户使用时程序的效率。

五、指针运算

指针的基本运算有三种,分别是:

  1. 指针+-整数
  2. 指针-指针
  3. 指针的关系运算

5.1 指针+- 整数

? ? ? 因为数组在内存中是连续存放的,只要知道第一个元素的地址,顺藤摸瓜就能找到后面的所有元素。那么便可以通过指针方式访问数组元素或者修改数组元素。

int arr[10]={1,2,3,4,5,6,7,8,9,10}

//方式1:指针自增的方式
#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int *p = &arr[0];   //或者int *p=arr;
	int i = 0;
	int len = sizeof(arr) / sizeof(arr[0]);
	for(i=0;i<len;i++)
	{
		printf("%d ", *p);
        p++;
	}
	return 0;
}


//方式2:指针加偏移量的方式
#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int *p = &arr[0];    //或者int *p=arr;
	int i = 0;
	int len = sizeof(arr) / sizeof(arr[0]);
	for(i=0;i<len;i++)
	{
		printf("%d ", *(p + i)); //p+i就是第i个元素下标的地址,也可以写作p[i]
	}
	return 0;
}

5.2 指针-指针

? ? ? ?指针-指针得到的是两个指针中间的元素个数。

需要注意的是两个相减的指针必须是指向同一块连续空间的。如果不为同一块连续空间,得到的值是无意义的,每次运行结果都不尽相同,我们无法估计会得到一个怎样的值。

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int* q = &arr[9];
	printf("%d", q - p);
	return 0;
}

练习:利用指针-指针的方式计算字符串的长度模拟strlen()函数


int my strlen(const char* str)
char*start = str;
while (*str != 0')
{
    str++;
}
return str - start;

5.3指针的关系运算

? ? ? ? 指针变量保存的是地址,是常量,是一个具体的数,对两个指针变量进行关系比较,其实比较的是地址的大小,对于在内存中连续存放的数据,便可以使用指针的关系运算,使得代码更加简洁和灵活。

#include <stdio.h>
int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int* p = arr;
    int* pend = arr + 9;
    while (p <= pend)
    {
        printf("%d  ", * p);
        p++;
    }
    return 0;
}

注意事项:

? ? ? 标准规定: 允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与 指向第一个元素之前的那个内存位置的指针进行比较。

? ? ??实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。

第一种写法
#include<stdio.h>
int main()
{
   int arr[5]={0};
   for(p = &arr[5]; p > &arr[0];)
   {
      *--p = 1;
   }
   return 0;
}

?

第二种写法
#include<stdio.h>
int main()
{
   int arr[5]={0};
   for(p = &arr[4]; p >= &values[0];p--;)
   {
      *p = 1;
   }
   return 0;
}

六、指针和数组

6.1 数组名的理解

#include <stdio.h>
int main()
{
    int arr[] = {1,2,3,4,5};
    printf("%p\n", arr);
    printf("%p\n", &arr[0]);
    printf("%d\n", &arr);
    return 0;
}

从上面的结果看可以知道:

? ? ? 数组名就是数组首元素(第一个元素)的地址,但是有两个例外:

(1)sizeof(数组名):sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
(2)&数组名:这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)

除此之外,任何地方使用数组名,数组名都表示首元素的地址。

? ? ?arr和&arr是不一样的,虽然值是相同的,但是类型是不同的,arr是数组首元素的地址,&arr是数组的地址,主要区别在于指针加1的能力不同!这在数组指针时有重要区别!

#include<stdio.h>
int main()
{
	int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
	printf(" arr	=%p\n", arr);
	printf(" arr+1	=%p\n", arr + 1);
	printf(" &arr[0]=%p\n", &arr[0]);
	printf("&arr[0]+1=%p\n", &arr[0] + 1);

	printf(" &arr	=%p\n", &arr);
	printf(" &arr+1	=%p\n", &arr + 1);
	
	return 0;
}

这里我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1相差4个字节,是因为&arr[0]和 arr都是首元素的地址,+1就是跳过一个元素。但是&arr和&arr+1相差40个字节,这就是因为&arr是数组的地址,+1操作是跳过整个数组的。?

6.2 一维数组传参的本质

? ? ? ?数组我们学过了,之前也讲了,数组是可以传递给函数的,这个小节我们讨论一下数组传参的本质。首先从一个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把函数传给一个函数后,函数内部求数组的元素个数吗?

#include<stdio.h>
void test(int arr[])
{
	int len = sizeof(arr) / sizeof(arr[0]);
	printf(" len=%d\n", len);
}
int main()
{
	int arr[10] = { 0 };
	int len = sizeof(arr) / sizeof(arr[0]);
	test(arr);
	printf(" len=%d", len);
	return 0;
}

? ? ? ?我们发现在函数内部是没有正确获得数组的元素个数。这就要学习数组传参的本质了,上个小节我们学习了:数组名是数组首元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参本质上传递的是数组首元素的地址。所以函数形参的部分理论上应该使用指针变量来接收首元素的地址。那么在函数内部我们写sizeof(arr)计算的是一个地址的大小(单位字节)而不是数组的大小(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。
? ? ? 正确做法是:在数组定义处,利用公式求出数组长度,然后通过参数传递给被调函数!

#include<stdio.h>
void test(int arr[],int len)
{
	int i = 0;
	for (i = 0; i < len; i++)
	{
		printf("%d ", *(arr + i));
	}
}
int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int len = sizeof(arr) / sizeof(arr[0]);
	test(arr,len);
	return 0;
}

6.3 指针方式访问数组

? ? ? 既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个就成为可能,这在数组章节详细总结过。

#include <stdio.h>
int main()
{
    int arr[] = {1,2,3,4,5,6,7,8,9,0};
    int *p = arr; //指针存放数组首元素的地址
    int len = sizeof(arr)/sizeof(arr[0]);
    for(i=0; i<len; i++)
   {
        printf("&arr[%d] = %p   <====> p+%d = %p\n", i, &arr[i], i, p+i);
   }
    return 0;
}

? ? 从上述结果可以看到:p+i 其实计算的是数组 arr 下标为i的地址!?

直接通过指针来访问数组。
int main()
{
   int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
   int *p = arr; //指针存放数组首元素的地址
   int len = sizeof(arr) / sizeof(arr[0]);
   int i = 0;
   for (i = 0; i<len; i++)
   {
      printf("%d ", *(p + i));
   }
   return 0;
}

6.4 加深理解?

#include <stdio.h>
int main()
{
    int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
    int* p = arr; //指针存放数组首元素的地址
    printf("%d\n", arr[2]);
    printf("%d\n", p[2]);   //因此,p[2]等价于*(p+2)

    //[]是一个操作符,2和arr是两个操作数,并且满足交换律
    printf("%d\n", arr[2]);
    printf("%d\n", 2[arr]);

    既然数组名就是数组首元素的地址,也就是一个指针,可以得到如下推论:
    arr[2]<->2[arr]<->*(arr+2)<->*(2+arr)<->*(p+2)<->*(2+p)<->p[2]
    上面的写法都是访问数组的第三个元素,常用的是:*(p+i)或者p[i]
    return 0;
}

七、二级指针

? ? ?指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里? 这就是二级指针,保存一级指针地址的指针变量 。

7.1 二级指针的运算

解引用一次:

? ? ? *ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa

int b = 20;
*ppa = &b;//等价于 pa = &b;

解引用两次:

? ??**ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a

**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;

八、字符串指针(是指针)

C语言表示一个字符串的方式在字符数组与字符串博客已经总结过,这里只简单介绍。

在指针的类型中我们知道有一种指针类型为字符指针 char*,一般使用:


int main()
{
   char ch = 'w';
   char *pc = &ch;
   *pc = 'w';
   return 0;
}

还有一种使用方式如下:

#include<stdio.h>
int main()
{
   const char* pstr = "hello bit";//这里是把一个字符串放到pstr指针变量里了吗?
   printf("%s\n", pstr);
   printf("%c\n",*pstr);
   return 0;
}

? ? ? 代码 const char* pstr = "hello bit"; 特别容易让同学以为是把字符串 hello bit放到字符指针 pstr 里了,但是/本质是把字符串 hello bit首字符h的地址放到了pstr中。

任何理解呢?

? ? ? ?pstr 指向了一个常量字符串,我们需要先知道pstr 是一个指针变量,它存放的内容一定是地址,大小一定为4/8个字节,这里的字符串已经超过了8个字节,所以一定不是存放的字符串,它存放的其实的这个字符串的首字符地址,也就是’h‘的地址,解释了为什么打印一个字符是 ’h‘。

同时需要注意:

? ? ? 不可以修改其中的内容!

? ? ? 这里的 pstr指针指向的是一个常量字符串,是不允许修改的,如果非要进行修改就会运行出错。程序运行后会直接挂掉。事实上:这里的常量字符串是储存在内存的常量区中的(只读),在里面的数据是不允许修改的。所以为了明确它不能被修改,我们尽量在前面加上 const 关键字修饰一下。这样如果我们不小心,将其修改了,编译器也会告诉我们报错的信息。

8.1 字符串常量在栈区和常量区创建的区别

#include<stdio.h>
int main()
{
	const char* p1 = "abcdef";//p1 指向 'a' 地址
	const char* p2 = "abcdef";//p2 指向 'a' 地址
 
	char arr1[] = "abcdef";//arr1 指向 'a' 地址
	char arr2[] = "abcdef";//arr2 指向 'a' 地址
 
//以下是为了判断是否两个地址是相等的
	if (p1 == p2)
	{
		printf("p1 == p2\n");
	}
	else
	{
		printf("p1 != p2\n");
	}
 
	if (arr1 == arr2)
	{
		printf("arr1 == arr1\n");
	}
	else
	{
		printf("arr1 != arr2\n");
	}
 
 
	return 0;
}

直观的看到p1 和 p2 指向的地址是相同的,arr1 和 arr2 指向的地址是不相同的。?

分析:

? ? ?p1 和 p2 指向的是常量字符串,是储存在常量区的,而常量的数据是只读的,不能被修改的,所以同一个数据不会被多次创建,只会在常量区进行一次数据的创建,相当于p1 、 p2 指向的是同一个内容。而arr1 和 arr2 是在栈区创建了两个不同的空间,其内容是用字符串来初始化的,所以两者的地址不同。

九、数组指针(是指针)

9.1 数组指针的概念理解

? ? ? ?数组指针是指针?还是数组? 答案是:指针。(侧重点在后面) 指针的类型是什么?数组类型!我们已经熟悉:

  1. 整形指针: int? *pint; 能够指向整形数据的指针。
  2. 浮点型指针: float? *pf; 能够指向浮点型数据的指针。
  3. 那数组指针应该是:能够指向数组的指针。数组指针指的是指向一个数组整体的指针,此时指针加1代表,偏移整个数组大小的字节对数组指针利用typedef进行重命名,可以简化使用方式!
//解释数组指针
#include <stdio.h>
int main()
{
     //定义一个包含3个元素的整型数组,注意下面两种写法一样,数组的类型就是 int [],数组名是arr
     int  arr[]={1,2,3};        
     int  []arr={1,2,3};
     //定义一个数组指针,指向整个数组,注意下面两种写法一样,它的类型就是 int (*)[3]
    //注意()和[]的顺序可以交换,由于[]的优先级高于(),因此,加上括号提高优先级(侧重点在于指针)
     int [3] (*parr)=&arr;    
     int (*parr) [3]=&arr;  //常用的方式
   
     return 0;
}
 
 
//利用typedef对数组指针进行重名,PARR是对int (*)[3]类型的重命名。PARR现在可以用来声明指向包含3个整数的数组的指针,而不必每次都写int (*)[3]。这样可以使代码更加清晰和易读。
//注意此时,数组指针的类型就是:int (*)[3],C语言规定*必须与变量名放在一起用,在进行重命名时,typedef  int (*)[3] PARR 就是错的,必须写成:typedef  int(*PARR) [3];
#include <stdio.h>
typedef  int(*PARR) [3];     //此时,PARR提升为类型名,代表这个类型i t (*)[3]的重命名
int main()
{
     int  arr[]={1,2,3};        
     //利用类型重命名,定义指针变量
     PARR p=&arr;        //注意&不可以去掉,此时代表的是整个数组的大小,与指针加1的能力相关!
     
     return 0;
}
 //举例理解
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
 
	int(*p)[10] = &arr;//一个整型数组元素为10个的指针
 
	return 0;
}

?解释:p 先和 * 结合,说明 p 是一个指针,然后再和[10]结合,说明 p 指向的是一个元素为10个的数组,int说明元素类型是整型。这里 p 指针的类型是:int (*)[10]。

9.2 利用数组指针访问二维数组

? ? ? ? 对于二维数组来说,数组名是首元素的地址,二维数组的首元素是第一行,因此二维数组名是第一行的地址,即是一个数组指针,指向二维数组的第一行

?

十、指针数组(是数组!)

10.1 指针数组的概念理解

? ? ? 指针数组是指针还是数组? 答案:是数组。是存放指针的数组(侧重点同样在后面)

? ? ? 数组我们已经知道整形数组,字符数组。那指针数组是怎样的? arr3是一个数组,有五个元素,每个元素是一个整形指针。 ?int* arr3[5];是什么?

整型指针数组

int main()
{
	int* arr[10];//整型指针数组
	char* ch[10];//字符指针数组
 
	return 0;
}

解释:int* arr[10]; arr先与[10]结合,说明arr是一个数组,剩下的int* 说明它其中的元素类型是int*.其他类型的指针数组解释方法和定义方法类似。

??

10.2 利用指针数组模拟二维数组

? ? ? ?二维数组可以看作是一维数组的特例,从这个角度理解,可以利用指针数组存放每一行的起始地址,便可以通过指针数组的方式来访问二维数组!

???????

番外篇:区别以下语句:

int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];   //它的类型是:int (* )[5] 数组指针类型

第一个:整型数组,数组类型为:int[5] ,数组名为:arr,存放5个整型数据的数组;

第二个:指针数组,数组类型为:int *[10],数组名为:parr1,存放10个整型指针的数组

第三个:数组指针,该指针能够指向一个数组,数组10个元素,每个元素的类型是int

第四个:parr3是一个存储数组指针的数组,该数组能能够存放10个数组指针,每个数组指针能够指向一个数组,数组5个元素,每个元素是int类型

十一、数组传参和指针传参

11.1 一维数组传参

#include <stdio.h>
void test(int arr[])//ok?正确
{}
void test(int arr[10])//ok?正确
{}
void test(int* arr)//ok?正确
{}
void test2(int* arr[20])//ok?  正确,数组名是数组首元素的地址,即int类型的地址
{}
void test2(int** arr)//ok?   正确,数组名是数组首元素的地址,即int*类型的地址
{}
int main()
{
   int arr[10] = {0};
   int *arr2[20] = {0};  //指针数组,每个元素都是 int*类型
   test(arr);
   test2(arr2);
}

? ? ? ?上述一维数组的传递参数写法均正确,数组传参本质上退化为地址/指针,这样才可以改变外部数据,是一种传址调用,有两种书写形式:第一种写成数组形式,数组元素个数可以不写(形式参数,并不会在内存开辟空间),第二种写成指针形式,根据传递数据的类型,用相同类型的指针接收即可。

11.2 二维数组传参

void test(int arr[3][5])//ok?正确
{}
void test(int arr[][])//ok?错误
{}
void test(int arr[][5])//ok?正确
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//ok?错误
{}
void test(int* arr[5])//ok?错误
{}
void test(int (*arr)[5])//ok?正确
{}
void test(int **arr)//ok?错误
{}

int main()
{
    int arr[3][5] = {0};
    test(arr);
}

? ? ? 数组传参本质上退化为地址/指针,这样才可以改变外部数据,是一种传址调用,有两种书写形式:第一种写成数组形式,二维数组元素行数可以不写,但是不可以省略列数,第二种写成指针形式,二维数组名是第一行的地址,是一个数组指针,指向第一行,同样应该用数组指针接收。

11.3?一级指针传参

#include <stdio.h>
void print(int *p, int len)
{
   int i = 0;
   for(i=0; i<len; i++)
   {
     printf("%d\n", *(p+i));
   }
}

int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9};
    int *p = arr;
    int len = sizeof(arr)/sizeof(arr[0]);
   //一级指针p,传给函数
   print(p, len);
   return 0;
}

当一个函数的参数部分为一级指针的时候,函数能接收什么参数?-会调用

???????void test1(int *p) {} //test1函数能接收什么参数? 
void test2(char* p) {} //test2函数能接收什么参数?

11.4 二级指针传参

#include <stdio.h>
void test(int** ptr)
{
     printf("num = %d\n", **ptr); 
}

int main()
{
    int n = 10;
    int*p = &n;
    int **pp = &p;
    test(pp);
    test(&p);
    return 0;
}

当函数的参数为二级指针的时候,可以接收什么参数?

void test(char **p)
{
 
}
int main()
{
   char c = 'b';
   char*pc = &c;
   char**ppc = &pc;
   char* arr[10];
   test(&pc);
   test(ppc);
   test(arr);//Ok?
   return 0;
}

总结:

十二、函数指针(是指针)

12.1 函数指针概念理解

? ? 函数指针是指指向函数的指针,此时函数名代表函数的入口地址,是一个地址。

#include <stdio.h>
void test()
{
    printf("hehe\n");
}
int main()
{
   printf("%p\n", test);
   printf("%p\n", &test);
   return 0;
}

上述表明:函数名和&函数名相同,都代表函数的入口地址!?

12.2 函数指针的使用

? ? ? 函数指针是指针,它是指向函数的指针,函数名既然是函数的入口地址,是一个指针,那么通过对该指针进行解引用,便可以找到这个函数,进而通过函数指针来调用函数!

#include <stdio.h>
int Add(int x,int y)
{
   return x+y;
}

int main()
{
   int a=3,b=5;
   int res1=Add(a,b);  //普通的函数调用

   printf("%p\n",&Add);
   printf("%p\n",Add);

   int (*pfun)(int ,int)=&Add;
   //int (*pfun)(int ,int)=Add;     //pfun和Add等价
   
   int res2=(*pfun)(a,b);        //先进行解引用找到该函数,在调用函数
   int res3=pfun(a,b);
   printf("%d  %d  %d",res2,res2,res3);
   return 0;
}

因此,调用函数有两种方式:第一种直接根据函数名调用,第二种根据函数指针调用。

int res1=Add(a,b);
int res3=pfun(a,b);

利用typedef对函数指针进行重命名:

//使用函数指针的方式来调用函数
 
#include <stdio.h>
//函数指针进行重命名,相当于对 int(*) (int x, int y)类型进行重命名为 Pfun 
typedef  int(*Pfun) (int x, int y);   //或者typedef  int(*Pfun) (int , int );
 
int add(int x,int y)
{
   return x+y;
}
 
int mul(int x,int y)
{
   return x*y;
}
 
int main()
{
   int a=1,b=2;
   Pfun p =add;      //使用类型重命名的函数指针,定义一个函数指针;
   int res=p(a,b);  //使用这个函数指针进行函数调用p(a,b);
   printf("%d\n",res);
 
   return 0;
}

12.3 适用场景

? ? ?通过函数指针可以调用同类型的函数,方便代码的维护和开发

12.4 两个有趣的代码

//代码1
(*(void (*)())0)();

//代码2
void (*signal(int , void(*)(int)))(int);

代码二可以简化写成如下的方式:

?????????

十三、函数指针数组(是数组)

13.1 函数指针数组的概念

?? 函数指针数组是数组,数组的数据类型为函数指针?,利用函数指针数组可以存放同类型的函数指针!

int Add(int x,int y)
{
   return x+y;
}

int Sub(int x,int y)
{
   return x-y;
}

int main()
{
   int (*pfun1)(int ,int )=Add;
   int (*pfun2)(int ,int )=Sub;
   int (*pfunArray[2])(int ,int )={Add,Sub}; //函数指针数组定义及初始化
   return 0;
}

如何理解?int (*pfunArray[2])(int ,int ),首先pfun先和 [ ] 结合,说明 pfun是数组,数组的内容是什么呢? 是 int (*)(int ,int) 类型的函数指针。

13.2 函数指针数组的应用场景

转移表

实现计算器
#include <stdio.h>
int add(int a, int b)
{
 return a + b;
}
int sub(int a, int b)
{
 return a - b;
}
int mul(int a, int b)
{
 return a*b;
}
int div(int a, int b)
{
 return a / b;
}
int main()
{
 int x, y;
 int input = 1;
 int ret = 0;
 do
 {
 printf( "*************************\n" );
 printf( " 1:add 2:sub \n" );
 printf( " 3:mul 4:div \n" );
 printf( "*************************\n" );
 printf( "请选择:" );
 scanf( "%d", &input);
 switch (input)
 {
 case 1:
 printf( "输入操作数:" );
 scanf( "%d %d", &x, &y);
 ret = add(x, y);
 printf( "ret = %d\n", ret);
 break;

 case 2:
 printf( "输入操作数:" );
 scanf( "%d %d", &x, &y);
 ret = sub(x, y);
 printf( "ret = %d\n", ret);
 break;

 case 3:
 printf( "输入操作数:" );
 scanf( "%d %d", &x, &y);
 ret = mul(x, y);
 printf( "ret = %d\n", ret);
 break;

 case 4:
 printf( "输入操作数:" );
 scanf( "%d %d", &x, &y);
 ret = div(x, y);
 printf( "ret = %d\n", ret);
 break;

 case 0:
 printf("退出程序\n");
 breark;

 default:
 printf( "选择错误\n" );
 break;
 }
 } while (input);
 
 return 0;
}

上述代码存在缺点:代码冗余,需要增加功能时,需要重新写函数加分支语句,不利于后期维护。

函数指针数组实现
#include <stdio.h>
int add(int a, int b)
{
   return a + b;
}
int sub(int a, int b)
{
   return a - b;
}
int mul(int a, int b)
{
   return a*b;
}
int div(int a, int b)
{
   return a / b;
}

int main()
{
    int x, y;
    int input = 1;
    int ret = 0;
    int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
    while (input)
    {
    printf( "*************************\n" );
    printf( " 1:add 2:sub \n" );
    printf( " 3:mul 4:div \n" );
    printf( "*************************\n" );
    printf( "请选择:" );
    scanf( "%d", &input);
    if ((input <= 4 && input >= 1))
    {
       printf( "输入操作数:" );
       scanf( "%d %d", &x, &y);
       ret = (p[input])(x, y);
    }
   else
    {
       printf( "输入有误\n" );
       printf( "ret = %d\n", ret);
    }
    return 0;
}

十四、指向函数指针数组的指针(是指针)

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

十五、回调函数

15.1 回调函数的基本概念

? ? ? ?回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当 这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调 用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

实现计算器
#include <stdio.h>
int add(int a, int b)
{
 return a + b;
}

int sub(int a, int b)
{
 return a - b;
}

int mul(int a, int b)
{
 return a*b;
}

int div(int a, int b)
{
 return a / b;
}

//回调函数,参数为函数指针
int calc(int (*pf)(int ,int))
{
   int x;
   int y;
   printf( "输入操作数:" );
   scanf( "%d %d", &x, &y);
   return pf(x,y);   //通过传入的函数指针,来调用函数
}

int main()
{
 int x, y;
 int input = 1;
 int ret = 0;
 do
 {
    printf( "*************************\n" );
    printf( " 1:add 2:sub \n" );
    printf( " 3:mul 4:div \n" );
    printf( "*************************\n" );
    printf( "请选择:" );
    scanf( "%d", &input);
 switch (input)
 {
 case 1:
 ret = calc(Add);
 printf( "ret = %d\n", ret);
 break;

 case 2:
 ret = calc(Sub);
 ret = sub(x, y);
 printf( "ret = %d\n", ret);
 break;

 case 3:
 ret = calc(Mul);
 printf( "ret = %d\n", ret);
 break;

 case 4:
 ret = calc(Div);
 printf( "ret = %d\n", ret);
 break;

 case 0:
 printf("退出程序\n");
 breark;

 default:
 printf( "选择错误\n" );
 break;
 }
 } while (input);
 
 return 0;
}

15.2 回调函数qsort库函数

qsort库函数可以实现任意类型数据的排序,底层实现的思想是冒泡排序思想。所在头文件为:stdlib.h

15.2.1函数原型如下:

15.2.2 参数说明:

15.2.3 使用方式:

#include <stdio.h>
#include<stdlib.h>
//qosrt函数的使用者得实现一个比较函数

int int_cmp(const void * p1, const void * p2)
{
 return (*( int *)p1 - *(int *) p2);
}

int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int i = 0;
    int len=sizeof(arr) / sizeof(arr[0]);
    qsort(arr, len, sizeof (arr[0]), int_cmp);
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
    {
       printf( "%d ", arr[i]);
    }
    printf("\n");
    return 0;
}

15.2.4 使用回调函数,模拟实现qsort(采用冒泡的方式)

自己实现的比较函数
int cmp_int(const void* e1, const void* e2)
{
	return (*(int*)e1 - *(int*)e2);
}



void Swap(char*buf1, char* buf2, int width)
{
	int i = 0;
	for (i = 0; i < width; i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}


void bubble_sort(void*base, int sz, int width, int(*cmp)(const void*e1, const void*e2))
{
	int i = 0;
	//趟数
	for (i = 0; i < sz - 1; i++)
	{
		int flag = 1;//假设数组是排好序
		//一趟冒泡排序的过程
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (cmp((char*)base+j*width, (char*)base+(j+1)*width)>0)
         //通过指针强转为char*再结合偏移量,找到相邻两个元素的起始地址,传递给比较函数
			{
				//交换
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
         //通过指针强转为char*再结合偏移量,逐字节交换数据
				flag = 0;
			}
		}
		if (flag == 1)
		{
			break;
		}
	}
}

补充:?

int main()
{
	int a = 10;
	//char* pa = &a;//int*
	void* pv = &a;//void*是无具体类型的指针,可以接受任意类型的地址
	//void*是无具体类型的指针,所以不能解引用操作,也不能+-整数
	return 0;
}

十六、指针和数组面试题的解析

总结: 数组名的意义:

  1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
  2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
  3. 除此之外所有的数组名都表示首元素的地址。

16.1 一维数组

//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a+0));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(a[1]));


printf("%d\n",sizeof(&a));
printf("%d\n",sizeof(*&a));
printf("%d\n",sizeof(&a+1));
printf("%d\n",sizeof(&a[0]));
printf("%d\n",sizeof(&a[0]+1));

第一组:

  1. 计算的是整个数组所占内存空间大小,???????4*4=16字节
  2. a+0代表第一个元素的地址,地址/指针只与平台有关系,4/8字节
  3. *解引用访问内存空间与类型有关,*a代表第一个元素所占内存空间,int类型,4字节
  4. a+1代表第二个元素的地址,地址/指针只与平台有关系,4/8字节
  5. a[1]代表访问第二个元素,int类型,4字节(a[1]等价于*(a+1))

第二组:

  1. &a代表整个数组的地址,地址/指针只与平台有关系,4/8字节
  2. *&a进行解引用,与类型有关(&a就是数组的地址,也就是数组指针),解引用访问整个数组空间大小,即4*4=16字节,说明*和&是相互抵消的
  3. &a+1,数组指针加1,一次跳过整个数组所占的内存空间,是数组后面空间的地址,地址/指针只与平台有关系,4/8字节
  4. &a[0]取出第一个元素的地址,地址/指针只与平台有关系,4/8字节(相当于&*(a+0))
  5. &a[0]+1取出的是第二个元素的地址,地址/指针只与平台有关系,4/8字节

???????16.2 字符数组或者字符串

//字符数组
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));


printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));

?

?第一组:sizeof计算的是占用内存空间的大小

  1. 计算的是整个数组所占内存空间大小,字符数组有6个元素,占用6个字节空间
  2. arr+0代表第一个字符的地址,地址/指针只与平台有关系,4/8字节
  3. *arr表示对首字符的地址进行解引用,与类型有关,char类型,1个字节
  4. arr[1]第二个字符占用的内存空间大小,为char类型,1个字节
  5. &arr代表整个字符数组的地址,地址/指针只与平台有关系,4/8字节
  6. &arr+1数组指针加1,一次跳过整个数组所占的内存空间,是数组后面空间的地址,地址/指针只与平台有关系,4/8字节
  7. &arr[0]+1代表第二个字符的地址,地址/指针只与平台有关系,4/8字节

?第二组:strlen计算的是字符串的长度,只看'\0'

  1. 此时arr代表首字符地址,strlen是计算\0之前的字符个数,因此它是随机值
  2. arr+0也是代表首字符的地址,strlen是计算\0之前的字符个数,因此它也是随机值
  3. 传参错误,对首字符的地址解引用,传递的是a字符,97,无法计算
  4. 传参错误,传递的是b字符,98,无法计算
  5. 传递的是数组的地址(数组指针),但仍然是一个地址,首字符的地址,strlen是计算\0之前的字符个数,因此它是随机值
  6. 数组的地址(数组指针)&arr+1,跳过整个字符数组,仍然是随机值;
  7. 从b的地址开始计算,仍然是随机值
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));



printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));

?

?第一组:sizeof计算的是占用内存空间的大小

  1. 计算的是整个数组所占内存空间大小,字符数组存在\0,有7个元素,占用7个字节空间
  2. arr+0代表第一个字符的地址,地址/指针只与平台有关系,4/8字节
  3. *arr表示对首字符的地址进行解引用,与类型有关,char类型,1个字节
  4. arr[1]第二个字符占用的内存空间大小,为char类型,1个字节
  5. &arr代表整个字符数组的地址,地址/指针只与平台有关系,4/8字节
  6. &arr+1数组指针加1,一次跳过整个数组所占的内存空间,是数组后面空间的地址,地址/指针只与平台有关系,4/8字节
  7. &arr[0]+1代表第二个字符的地址,地址/指针只与平台有关系,4/8字节

??第二组:strlen计算的是字符串的长度,只看'\0'

  1. 此时arr代表首字符地址,strlen是计算\0之前的字符个数,因此它是6
  2. arr+0也是代表首字符的地址,strlen是计算\0之前的字符个数,因此它也是6
  3. 传参错误,对首字符的地址解引用,传递的是a字符,97,无法计算
  4. 传参错误,传递的是b字符,98,无法计算
  5. 传递的是数组的地址(数组指针),但仍然是一个地址,首字符的地址,strlen是计算\0之前的字符个数,因此它是6
  6. 数组的地址(数组指针)&arr+1,跳过整个字符数组,后面字符未知,是随机值;
  7. 从b的地址开始计算,结果是5
char *p = "abcdef";
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(p+1));
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));
printf("%d\n", sizeof(&p));
printf("%d\n", sizeof(&p+1));
printf("%d\n", sizeof(&p[0]+1));


printf("%d\n", strlen(p));
printf("%d\n", strlen(p+1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p+1));
printf("%d\n", strlen(&p[0]+1));

?

??第一组:sizeof计算的是占用内存空间的大小

  1. 计算的是指针变量所占内存空间,地址/指针只与平台有关系,4/8字节
  2. 计算的是b字符地址占用内存空间大小,地址/指针只与平台有关系,4/8字节
  3. 对p进行解引用,访问的是首字符a占用内存空间大小,1个字节
  4. 计算的是首字符a占用内存空间大小,1个字节
  5. 计算的是指针变量地址占用内存空间大小,地址/指针只与平台有关系,4/8字节
  6. 计算的是跳过指针变量地址的下一个地址占用内存空间大小,地址/指针只与平台有关系,4/8字节
  7. 计算的是b字符地址占用内存空间大小,地址/指针只与平台有关系,4/8字节

?第二组:?strlen计算的是字符串的长度,只看'\0'

  1. 传递的是p保存的地址,也就是a字符的地址,strlen是计算\0之前的字符个数,因此它是6
  2. 传递的是第二个字符的地址b,strlen是计算\0之前的字符个数,因此它是5
  3. 传参错误,对首字符的地址解引用,传递的是a字符,97,无法计算
  4. 传参错误,传递的是b字符,98,无法计算
  5. 传递的是指针变量的地址,但仍然是一个地址,strlen是计算\0之前的字符个数,因此它是随机值
  6. 传递的是整个指针变量的地址(指针变量指针),跳过整个指针变量所占内存空间,后面字符未知,是随机值;
  7. 从b的地址开始计算,结果是5

16.3 二维数组

//二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a[0][0]));
printf("%d\n",sizeof(a[0]));
printf("%d\n",sizeof(a[0]+1));



printf("%d\n",sizeof(*(a[0]+1)));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(*(a+1)));
printf("%d\n",sizeof(&a[0]+1));
printf("%d\n",sizeof(*(&a[0]+1)));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a[3]));

?

?

十七、指针笔试题

17.1 笔试题1

#include<stdio.h>
int main()
{
   int a[5] = { 1, 2, 3, 4, 5 };
   int *ptr = (int *)(&a + 1);//数组指针类型强转为整型类型的指针
   printf( "%d,%d", *(a + 1), *(ptr - 1));//解引用访问
   return 0;
}
程序的结果是什么?

?

17.2 笔试题2

//由于还没学习结构体,这里告知结构体的大小是20个字节
struct Test
{
   int Num;
   char *pcName;
   short sDate;
   char cha[2];
   short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
#include<stdio.h>
int main()
{
   printf("%p\n", p + 0x1);   //结构体指针加1,偏移一个结构体类型所占字节数
   printf("%p\n", (unsigned long)p + 0x1);//相当于整数加1
   printf("%p\n", (unsigned int*)p + 0x1);  //加4
   return 0;
}

?

考察考察指针+1的能力!?

17.3?笔试题3

#include<stdio.h>
int main()
{
   int a[4] = { 1, 2, 3, 4 };
   int *ptr1 = (int *)(&a + 1);
   int *ptr2 = (int *)((int)a + 1);
   printf( "%x,%x", ptr1[-1], *ptr2);
   return 0;
}

?

?

17.4 笔试题4

#include <stdio.h>
int main()
{
   int a[3][2] = { (0, 1), (2, 3), (4, 5) };
   int *p;
   p = a[0];
   printf( "%d", p[0]);
   return 0;
}

?

?

总结:

? ? ? 对于复杂题目,一定要画图理解,在后期的数据结构学习中同样重要!?

以上便是指针全部内容,认真理解消化,一定会有极大的收获,可以留下你们点赞、关注、评论,您的支持是对我极大的鼓励,下期再见!?

文章来源:https://blog.csdn.net/weixin_47040031/article/details/135543959
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。