C语言指针(初阶)

news2024/11/25 2:35:06

文章目录

  • 1:内存与地址
    • 1.1内存
    • 1.2:如何理解编址
  • 2:指针变量与地址
    • 2.1:指针变量与解引用操作符
      • 2.1.1:指针变量
      • 2.1.2:如何拆解指针类型
      • 2.1.3:解引用操作符
    • 2.2:指针变量的大小
  • 3:指针变量类型的意义
    • 代码1
      • 解引用修改前
      • 解引用修改后
    • 代码2
      • 解引用修改前
      • 解引用修改后
  • 4:const修饰指针
    • 4.1:const修饰变量
    • 4.2:const修饰指针变量
      • 代码1
      • 代码2(const放在*左边)
      • 代码3(const放在*右边)
      • 代码4(const放在*左右两边)
  • 5:指针运算
    • 5.1:指针加减整数
      • 代码1
      • 代码2
    • 5.2:指针-指针
      • 代码1
      • 代码2
    • 5.3:指针的关系运算
  • 6:野指针
    • 6.1:野指针成因
      • 6.1.1:指针未初始化
      • 6.1.2:指针越界访问
      • 6.1.3:指针指向的空间释放
    • 6.2:如何规避野指针
      • 6.2.1:指针初始化
      • 6.2.2:小心指针越界
      • 6.2.3:指针变量不再使用时,及时置NULL,指针使用之前检查有效性
      • 6.2.4:避免返回局部空间的地址
  • 7:二级指针
  • 8:指针数组
    • 8.1:语法
    • 8.2:指针数组模拟实现二维数组
  • 9:assert断言

嘻嘻,家人们,今天咱们剖析一下指针,好啦,废话不多讲,开干!

1:内存与地址

1.1内存

在讲解内存与地址之前,我们先来看生活中的一个案例.

假设有一栋宿舍楼,把uu们放在楼里面,楼上有100个房间,但是房间没有编号,此时如果uu们的朋友来找uu们玩的话,就得挨个房间去找,这样子的话,效率很低,但是如果我们根据每个楼层与楼层的放假情况,给每个房间进行编号如:
一楼:1001,1002,1003…
二楼:2001,2002,2003…
有了房间号,在uu们的朋友得到uu们的房间号的前提下,这样子就可以快速地找到房间,从而找到uu们.
生活中,每个房间有了房间号,就能提高效率,能够快速地找到房间.

如果把上面的例子对照到计算机中,又会是怎样呢?

我们知道计算机上的CPU即中央处理器在处理数据的时候,所需要的数据是在内存中进行读取的,处理后的数据也会放回内存中,那我们买电脑的时候,电脑上的内存是8GB/16GB/32GB等,那这些内存空间如何高效地进行管理呢?其实在计算中,也是把内存划分为一个个的内存单元,每个内存单元大小取1个字节.那么一个字节是多大呢,我们来看计算机中常见的单位!

1:bit  --- 比特位                   
2:byte --- 字节 				1byte = 8bit               
3:KB						    1KB   = 1024byte 
4:MB							1MB   = 1024KB
5:GB							1GB   = 1024MB
6:TB							1TB   = 1024GB
7:PB							1PB   = 1024TB

PS:一个比特位可以存储一个2进制的1或者0

其中,每个内存单元,相当于一个学生宿舍,一个字节空间里头能够存放8个比特位,就我们uu们在高中住宿时的八人间,每个人是一个比特位,每个内存单元也都有一个编号(这个编号就相当于宿舍房间的门牌号),有了这个内存单元的编号,CPU就可以快速找到一个内存空间。生活中我们把门牌号叫做地址,在计算机中我们将内存单元的编号称为地址。C语言中给地址起了一个新的名字:指针。因此我们可以理解为:内存单元的编号 == 地址 == 指针

在这里插入图片描述

1.2:如何理解编址

在这里插入图片描述

CPU访问内存中的某个字节空间,必须要知道这个字节空间在内存的什么位置,而因为内存中字节很多,所以就需要给内存进行编址(就好比宿舍很多,需要给宿舍编号一样。
计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。首先,我们要理解计算机内是有很多的硬件单元,而硬件单元是要互相协同工作的。所谓的协同,至少相互之间要能够进行数据传递
但是硬件与硬件之间是互相独立的,那么该如何通信呢?很简单,用"链接"起来。而CPU和内存之间也是有大量的数据交互的,所以,两者之间也用线连接起来。不过,在这一章节,我们只关心一组线,叫做地址总线
可以这样子理解,32位的机器有32根地址总线,每一根线只有两态,表示0,1【电脉冲有无】,那么一根线,就能表示2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2的32次方种含义,每一种含义都代码一个地址。地址信息被下达给内存,在内存上,就可以找到该地址所对应的数据,将数据再通过数据总线传入到CPU寄存器。

2:指针变量与地址

理解了内存与地址的关系后,再回到C语言,在C语言中创建变量其实就是向内存申请空间,我们看下面这段代码.

#include <stdio.h>
int main()
{
	int value = 15;
	return 0;
}

在这里插入图片描述

在上面的代码中创建了整型变量value,内存中
申请4个字节,⽤于存放整数15,其中每个字节都
有地址,上图中4个字节的地址分别是:

0x0113FE28  
0x0113FE29    
0x0113FE2A    
0x0113FE2B

那么我们如何得到a的地址呢?这个时候就要使用到我们在之前所讲到的取地址操作符了,通过取地址操作符取出变量value的地址,我们看下面这段代码.

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

在这里插入图片描述

&value取出的是value所占4个字节中地址较小的字节的地址。虽然整型变量占用4个字节,但我们只要知道了第一个字节的地址,顺腾摸瓜就能访问到4个字节的数据.

2.1:指针变量与解引用操作符

2.1.1:指针变量

我们通过取地址操作符拿到的地址是一个数值,这个数值有时候也是需要存储起来,方便后期再使用的,那我们把这样的地址值存储在哪里呢?答案是:指针变量中.

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int a = 20;
	int* pa = &a;
	return 0;
}

在上述代码中,我们将变量a的地址存储在指针变量pa中,指针变量也是一种变量,这种变量是用来存放地址的,存放在指针变量中的值都将其理解为地址.

2.1.2:如何拆解指针类型

我们可以看到,pa的数据类型是int*,那么我们该如何理解指针的类型呢?

int a = 20;
int* pa = &a;

这里pa左边写的是int*,*是在说明pa是指针变量,而前面的int是在说明pa指向的是数据类型是整型数据.我们也下面这幅图来理解指针变量.

在这里插入图片描述

2.1.3:解引用操作符

我们将地址保存起来,在之后呢是需要去使用滴,那么如何去使用呢?在现实生活中,我们使用地址要找到一个房间,在房间里可以去拿或者存放物品.
那么同理,C语言也是一样的,我们只要拿到了地址,就可以通过地址去找到地址所指向的对象,这里呢就用到了我们之前所讲到的解引用操作符.

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int a = 100;
	int* pa = &a;
	*pa = 25;
	printf("%d\n", a);
	return 0;
}

在这里插入图片描述

在上述代码中就使用了解引用操作符,*pa的意思是通过pa存放的地址,找到所指向的空间, *pa其实就是变量a,因此 *pa = 25是将变量的值赋为25.
那么会有些uu有些疑惑,如果这里把变量a改成25的话,为什么不直接使用a = 25呢,为啥非要使用指针呢?其实这里就是把a的修改交给了pa操作,这样子对变量a的修改就多了一种方式,写代码时就会更加灵活.

2.2:指针变量的大小

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

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	printf("%d\n", sizeof(int*));
	printf("%d\n", sizeof(char*));
	printf("%d\n", sizeof(short*));
	printf("%d\n", sizeof(double*));
	return 0;
}

在这里插入图片描述
在这里插入图片描述

x86代表的是32位环境,我们可以清晰地看到,在32位环境下,指针变量的大小为4个字节,在64位环境下,指针变量的大小为8个字节且与指针变量的类型无关.

  1. 32位平台下地址是32个bit位,指针变量大小是4个字节
  2. 64位平台下地址是64个bit位,指针变量大小是8个字节
  3. 注意指针变量的大小和类型是无关的,只要是指针类型的变量,在相同的平台下,大小都是一样的.

3:指针变量类型的意义

我们说指针变量的大小与其类型无关,只要是指针变量,在同一个平台下,大小都是一样的,那么为什么还要有各种各样的指针类型呢?我们首先来看下面这两段代码.

代码1

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int a = 10;
	int* pa = &a;
	*pa = 20;
	return 0;
}

解引用修改前

在这里插入图片描述

解引用修改后

在这里插入图片描述

代码2

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int a = 0x11223344;
	char* pa = &a;
	*pa = 20;
	return 0;
}

解引用修改前

在这里插入图片描述

解引用修改后

在这里插入图片描述

我们对比代码1与代码2,当使用int * 类型去接收变量a的地址时,对其解引用修改,我们可以观察到此时变量a的4个字节都会被修改.而当使用char *类型去接收变量a的地址时,对其进行解引用修改,我们可以观察到此时变量a只有一个字节发生了变化.
因此,指针的类型决定了在对指针进行解引用操作时能够访问几个字节.

1:char * 类型的指针解引用访问1个字节.
2:short * 类型的指针解引用访问2个字节.
3:int * 类型的指针解引用访问4个字节.
4:double*类型的指针解引用访问8个字节.

4:const修饰指针

4.1:const修饰变量

变量是可以进行修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以修改这个变量,但是如果我们希望给一个变量加上一些限制,令其不能被修改,这样子该怎么做呢?这里就要讲到const这个关键字来修饰变量啦!

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int a = 20;
	a = 30;
	const int b = 25;
	//b不可被修改
	b = 0;
	return 0;
}

在这里插入图片描述

上述代码中,变量a是可以被修改的,而变量b是不可被修改的,b的本质是变量,只不过在被const修饰后,在语法上加了限制,只要对b进行了修改,此时不符合语法规则,就会发生报错,导致没法直接修改b.但是还有另外一种方法去修改b,来看下下面这段代码.

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	const int b = 25;
	int* pb = &b;
	printf("b = %d\n", b);
	*pb = 30;
	printf("b = %d\n", b);
	return 0;
} 

在这里插入图片描述

在上述代码中,我们没有直接修改b,而是通过b的地址,去进行修改b,但是这样子做实际上是在打破语法规则,因为将b被const修饰就是为了让其不能够被修改,如果pb拿到b的地址就能够修改b,这样子就打破了const的限制,这是不合理的,因此应该让pb拿到b的地址也不能够修改变量b,那么该如何做呢?这就涉及接下来要学习到的const修饰指针.

4.2:const修饰指针变量

博主将通过以下几段代码来讲解const修饰指针变量,首先来看代码1.

代码1

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void test1()
{
	int n = 10;
	int m = 20;
	int* p = &n;
	*p = 20;
	p = &m; 
}

int main()
{
	test1();
	return 0;
} 

在这里插入图片描述

首先是无const修饰,此时改变指针变量本身的内容以及改变指针变量所指向的内容都可以进行.

代码2(const放在*左边)

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void test2()
{
	int n = 10;
	int m = 20;
	const int* p = &n;
	*p = 20;
	p = &m; 
}

int main()
{
	test2();
	return 0;
} 

在这里插入图片描述

当const放在*左边时,可以观察到,此时能够改变指针变量本身的内容,但是不能通过指针变量去改变其所指向的内容.

代码3(const放在*右边)

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void test3()
{
	int n = 10;
	int m = 20;
	int* const p = &n;
	*p = 20;
	p = &m; 
}

int main()
{
	test3();
	return 0;
} 

在这里插入图片描述

当const放在*右边时,可以清晰地观察到,此时无法改变指针变量本身的内容,但是能够改变其所指向的内容.

代码4(const放在*左右两边)

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void test4()
{
	int n = 10;
	int m = 20;
	const int* const p = &n;
	*p = 20;
	p = &m; 
}

int main()
{
	test4();
	return 0;
} 

在这里插入图片描述

当const放在*左右两边时,可以清晰地观察到,此时既不能够改变指针变量本身的内容,又不能够改变其所指向的内容.

通过对比上面四段代码,我们可以得出以下结论

  1. const如果放在 * 的左边,修饰的是指针变量所指向的内容,此时无法通过指针改变指针所指向的内容,但是指针本身的内容可以改变.
  2. const如果放在 * 的右边,修饰的是指针变量本身,此时无法改变指针本身的内容,但是指针所指向的内容可以通过指针对其进行改变.
  3. const如果既在 * 的左边,又在 * 的右边,此时既修饰指针变量本身,又修饰指针变量所指向的内容,因此,既不能够通过指针改变指针变量所指向的内容,又不能够改变指针本身的内容.

5:指针运算

5.1:指针加减整数

之前我们了解到指针有各种各样的类型,其各种各样的类型决定了其在解引用时跳过几个字节,譬如整型指针在解引用时跳过4个字节,字符型指针在解引用时跳过1个字节等;除了这一作用外,指针类型还决定了其在加减整数时跳过几个字节,我们来看下面几段代码.

代码1

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int n = 10;
	char* p1 = (char *) & n;
	int* p2 = &n;

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

	printf("\n");

	printf("p1     = %p\n", p1);
	printf("p1 + 1 = %p\n", p1 + 1);

	printf("\n");

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

在这里插入图片描述

通过观察我们可以看到,char *类型的指针变量+1跳过1个字节,int 类型的指针变量+1跳过4个字节.那么同理我们也得出如下结论:
1:short * 类型的指针+1跳过2个字节.
2:double
类型的指针+1跳过8个字节.

代码2

在数组阶段,我们学习了可以通过元素下标去访问数组的元素且数组的元素之间是连续存储,只要找到了第一个元素的地址,就能够顺藤摸瓜找到后面的元素,那么因此我们也可以通过指针±整数来访问数组的元素.

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	//求出数组元素的大小
	int sz = sizeof(arr) / sizeof(arr[0]);

	int* p = arr;
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
} 

在这里插入图片描述

一般情况下,数组名代表着首元素地址,拿到了首元素地址后,当第一次+1,跳过4个字节,由于数组的元素之间是连续存储的,因此+1跳过4个字节访问到第二个元素,那么依次类推,+2跳过8个字节访问到第三个元素…

5.2:指针-指针

指针之间能够进行减法运算,但是有个前提:两个指针指向同一块区域且指针的类型是相同的.

代码1

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	printf("%d\n", &arr[9] - &arr[0]);
	printf("%d\n", &arr[0] - &arr[9]);
	return 0;
}

在这里插入图片描述

通过观察可以发现,指针-指针差值的绝对值为两个指针之间所间隔的元素个数.
那么根据这一特性,我们可以通过指针减去指针来模拟实现strlen函数.

字符串的结束标志是\0,而strlen函数是用于统计\0之前的字符个数.

代码2

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int mystrlen(char * str)
{
	char* begin = str;
	while (*str != '\0')
	{
		str++;
	}
	return str - begin;
}

int main()
{
	char arr[100] = "0";
	scanf("%s", arr);
	int result = mystrlen(arr);
	printf("%d\n", result);
	return 0;
}

在这里插入图片描述

这里博主就通过了指针-指针来模拟实现了strlen函数,首先通过begin变量来存储字符串首元素的地址,然后我们知道字符串的结束标志是\0,因此通过循环来令指针str不断变化,当str指向\0的时候,此时结束循环,由于begin存储了字符串首元素的地址,此时通过str - begin这样子我们就能够得到这两个指针之间所间隔的元素个数即\0之前的字符个数.

在这里插入图片描述

5.3:指针的关系运算

指针的关系运算常用于比较指针之间的地址以及判断该指针是否为NULL,指针的大小比较是基于两个指针所指向的内存地址在内存中的相对位置,可能uu们对此还有些迷惑,我们来看下面这段代码.

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;
	//指针的大小比较
	while (p < arr + sz)
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

在这里插入图片描述
在这里插入图片描述

上述代码是通过循环来访问数组中的元素,循环的终止条件是 p < arr + sz,两个指针进行比较是基于所指向的内存地址在内存中的相对位置.在这里,一开始p指针指向的是元素为1的位置,arr + sz即 arr + 10指向的是元素为10的下一个位置,指针p指向的内存地址在指针(arr + sz)指向的内存地址之前,因此 p < arr + sz,因此循环条件为真,进入循环,这样子周而复始,直到指针p指向的内存地址与指针(arr + sz)指向的内存地址相同时循环结束.
PS:指针的比较只有在指向同一个数组或同一个对象的成员时才有意义

6:野指针

对指针有了基本了解后,那么接下来博主将为大家介绍野指针,那么什么野指针呢? 野指针就是指针的位置是未知的(随机的、不正确的、没有明确限制的)

6.1:野指针成因

对野指针的概念有了一定了解后,接下来来学习下野指针的成因.博主将通过以下几段代码来讲解野指针的成因.

6.1.1:指针未初始化

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
	//局部变量指针未初始化,默认为随机值
	int* p;
	*p = 20;
	return 0;
}

在函数栈帧那一节我们学习过,局部变量在未进行初始化时,会被赋予随机值,这里的指针p由于为对其进行初始化,因此默认为随机值,那么它就是个野指针,因此在使用时编译器会发生报错.

在这里插入图片描述

6.1.2:指针越界访问

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	for (int i = 0; i < 11; i++)
	{
		*p = i;
		p++;
	}
	return 0;
}

在上述代码中,一开始指针p指向的数组首元素的地址,随着循环不断地递增,当变量i == 10时,此时指针p指向的位置就超出了数组arr的范围了,那么此时指针p为野指针,因此在对其进行使用时,编译器会发生报错.
在这里插入图片描述
在这里插入图片描述

6.1.3:指针指向的空间释放

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int * test()
{
	int n = 25;
	return &n;
}

int main()
{
	int* p = test();
	printf("%d\n", *p);
	return 0;
}

在函数栈帧那一节博主讲到过,函数每次调用都会开辟栈帧,当函数调用结束后,栈帧就会被销毁。在test函数中创建了局部变量 n,然后将局部变量n的地址返回了出去,我假设局部变量n的地址是0x11223344,当test函数调用结束后,此时栈帧销毁了,但是指针p指向的位置是不正确的,因为栈帧已经销毁了,已经将这块空间还给了操作系统,此时指针p那么指向的位置就不是属于自己的了,因此指针p为野指针.

在这里插入图片描述

6.2:如何规避野指针

学习了野指针的成因以后,那么该如何去避免野指针呢?有以下几种方式。

6.2.1:指针初始化

如果明确知道指针指向哪里就直接对其赋值地址,如果不知道指针应该指向哪里,可以给指针赋值为NULL,NULL是C语言中定义的一个标识符常量,值为0,0也是地址,这个地址是无法使用的,读写该地址时会发生报错.
在这里插入图片描述

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int n = 25;
	int* p1 = &n;
	int* p2 = NULL;
	return 0;
}

6.2.2:小心指针越界

一个程序向内存申请了哪些空间,通过指针也只能访问所对应的空间,不能够超出访问,超出了就是越界访问.

6.2.3:指针变量不再使用时,及时置NULL,指针使用之前检查有效性

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

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	for (int i = 0; i < 10; i++)
	{
		//虽然后置++的优先级高于解引用操作符,但是由于是先使用再++,因此一开始对p解引用得到是数值为1的元素.
		*p++ = i;
	}
	p = NULL;

	return 0;
}

譬如在这段代码中,当循环结束时,此时指针p指向的空间超出了arr的空间范围,那么此时我们要及时对其置NULL.

6.2.4:避免返回局部空间的地址

野指针成因的第三个例子.

7:二级指针

之前我们了解到指针是用来存储地址的,那么指针本身有没有地址呢?答案是有滴,指针变量也是变量,是变量就有地址,那么指针变量的地址就是存放在二级指针里头,我们来看下面这段代码

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int n = 25;
	int a = 20;
	int* p = &n;
	int** pp = &p;
	printf("&n  = %p\n", &n);
	printf("*pp = %p\n", *pp);
	//等价于 p = &a;
	*pp = &a;
	//等价于 a = 30
	**pp = 30;
	printf("&a  = %p\n", &a);
	printf("*pp = %p\n", *pp);
	printf("%d\n", **pp);
	printf("a = %d\n", a);
	return 0;
}


*pp就是通过对pp中的地址进行解引用,这样子找到的是p,pp其实访问的就是p.
**pp就是先通过
pp找到p,然后再通过对p进行解引用操作:*p,那么找到的就是a.

在这里插入图片描述
在这里插入图片描述

8:指针数组

指针数组是数组还是指针呢?我们可以来类比一下,
整型数组----->存放整型数据的数组
字符数组----->存放字符型数据的数据
那么因此同理我们就可以知道,指针数组----->存放指针的数组.

8.1:语法

int * 数组名[元素个数]
Eg:int * arr[5]

8.2:指针数组模拟实现二维数组

指针数组的每个元素都是指针,而每个指针又可以指向一块区域,因此,我们可以使用指针数组来模拟实现二维数组.

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int arr1[5] = {1,2,3,4,5};
	int arr2[5] = {6,7,8,9,10};
	int arr3[5] = {11,12,13,14,15};
	int* parr[3] = { arr1,arr2,arr3 };
	int Sz = sizeof(parr) / sizeof(parr[0]);
	int sz = sizeof(arr1) / sizeof(arr1[0]);
	for (int i = 0; i < Sz; i++)
	{
		for (int j = 0; j < sz; j++)
		{
			printf("%2d ", parr[i][j]);
			//printf("%2d ", (*(arr + i))[j]);
		}

		printf("\n");
	}
	return 0;
}

在这里插入图片描述
在这里插入图片描述

parr[i]访问的是parr数组的元素,parr[i]找到的数组元素指向的是整型一维数组,parr[i][j]就是整型一维数组中的元素,这就和二维数组访问元素是一样滴,上述的代码中模拟出了二维数组的效果,实际上并非完全是二维数组,我们知道二维数组在内存中是连续存储的,而上面的代码中每一行并非是连续的.

9:assert断言

讲完了指针相关的基础知识后,接下来博主将为大家介绍assert断言

assert.h头文件中,定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被称为"断言".我们来看下面这段代码.

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <assert.h>
int main()
{
	int* p = NULL;
	assert(p != NULL);
	return 0;
}

在这里插入图片描述

上面这段代码在程序运行到assert断言那一行语句时,会验证变量p是否等于NULL.如果确实不等于NULL,程序继续运行,否则就会终止运行,并且给出报错信息提示.
assert()宏接受一个表达式作为参数,若表达式为真(返回值为非0),assert()不会产生任何作用,程序继续运行。如果表达式为假(返回值为0),assert就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名与行号.

assert()的使用对程序员是非常友好的,使用assert有以下几个好处:

  1. 能自动标识文件和出问题的行号.
  2. 无需更改代码就能开启或关闭assert()的机制。

如果已经确认程序没有,则不需要再进行断言,就在#include <assert.h>语句的前面,定义一个宏NDEBUG.然后,重新编译程序,编译器就会禁用文件中所有的assert()语句。如果程序又出现问题,可以移除这条#defin NDEBUG指令,再次进行编译,这样就又重新启用了assert()语句.

#define  _CRT_SECURE_NO_WARNINGS
#define NDEBUG
#include <assert.h>
int main()
{
	int* p = NULL;
	assert(p != NULL);
	return 0;
}

在这里插入图片描述

assert() 的缺点是:引⼊了额外的检查,增加了程序的运⾏时间。
⼀般我们可以在debug中使⽤,在release版本中选择禁⽤assert就⾏,在VS这样的集成开发环境中,在release版本中,直接就是优化掉了。这样做的原因是,在debug版本写有利于程序员排查问题,在release版本不影响⽤⼾使⽤时程序的效率。

好啦,家人们,关于指针初阶这块的相关细节知识,博主就讲到这里了,如果uu们觉得博主讲的不错的话,请动动你们滴滴给博主点个赞,你们滴鼓励将成为博主源源不断滴动力!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1451717.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C语言期末项目-通讯录】-终级版本-可动态申请内存、可存储数据到文件(手把手详细过程,期末评分A+的项目,答辩辅助神博文,建议三连点赞收藏)

目录 ​编辑 前言&#xff1a; 1.项目功能需求分析 2.文件框架说明 3.程序主框架实现 4.创建联系人结构体类型和通讯录结构体类型 4.1创建通讯录 5.程序功能实现--封装功能函数实现不同功能 5.1通讯录初始化 5.2增加联系人 5.3显示所有联系人的信息 5.4删除指定…

BBC英式口语~发音练习~笔记整理

参考资料 原视频地址&#xff1a; https://www.bilibili.com/video/BV1D7411n7bS/?spm_id_from333.1245.0.0&vd_source5986fc7c8e6d754f3ca44233573aeaff 笔记图片

root MUSIC 算法补充说明

root MUSIC 算法补充说明 多项式求根root MUSIC 算法原理如何从 2 M − 2 2M-2 2M−2 个根中确定 K K K 个根从复数域上观察 2 M − 2 2M-2 2M−2 个根的分布 这篇笔记是上一篇关于 root MUSIC 笔记的补充。 多项式求根 要理解 root MUSIC 算法&#xff0c;需要理解多项式求…

DSA 经典数据结构与算法 学习心得和知识总结(三) |有向无环图及其应用

目录结构 注&#xff1a;提前言明 本文借鉴了以下博主、书籍或网站的内容&#xff0c;其列表如下&#xff1a; 1、参考书籍&#xff1a;《算法导论》第三版 就是这本被封神的杰作&#xff0c;就是它&#x1f926; 2、参考书籍&#xff1a;《数据结构》严奶奶版 3、参考书…

Java学习笔记------static

static 创建Javabean类 public class student {private int age;private String name;private String gender;public student() {}public student(int age, String name, String gender) {this.age age;this.name name;this.gender gender;}/*** 获取* return age*/public…

论文解读:Masked Generative Distillation

文章汇总 话题 知识蒸馏 创新点 带掩盖的生成式蒸馏 方法旨在通过学生的遮罩特征来生成老师的特征(通过遮盖学生部分的特征来生成老师的特征)&#xff0c;来帮助学生获得更好的表现 输入:老师:&#xff0c;学生:&#xff0c;输入:&#xff0c;标签:&#xff0c;超参数: 1:使…

CTFshow web(文件上传158-161)

web158 知识点&#xff1a; auto_append_file 是 PHP 配置选项之一&#xff0c;在 PHP 脚本执行结束后自动追加执行指定的文件。 当 auto_append_file 配置被设置为一个文件路径时&#xff0c;PHP 将在执行完脚本文件的所有代码后&#xff0c;自动加载并执行指定的文件。 这…

【springboot+vue项目(十四)】基于Oauth2的SSO单点登录(一)整体流程介绍

场景&#xff1a;现在有一个前后端分离的系统&#xff0c;前端框架使用vue-element-template&#xff0c;后端框架使用springbootspringSecurityJWTRedis&#xff08;登录部分&#xff09;现在需要接入到已经存在的第三方基于oauth2.0的非标准接口统一认证系统。 温馨提示&…

【STM32 CubeMX】I2C查询方式

文章目录 前言一、CubeMX配置IIC二、查询方式的使用2.1 分析一种情况2.2 Master模式2.3 Mem模式 总结 前言 在STM32 CubeMX环境中&#xff0c;I2C&#xff08;Inter-Integrated Circuit&#xff09;通信协议的查询方式是一种简单而常见的通信方式。通过查询方式&#xff0c;微…

代码随想录 Leetcode45. 跳跃游戏 II

题目&#xff1a; 代码(首刷看解析 2024年2月15日&#xff09;&#xff1a; class Solution { public:int jump(vector<int>& nums) {if (nums.size() 1) return 0;int res 0;int curDistance 0;int nextDistance 0;for (int i 0; i < nums.size(); i) {nex…

6、内网安全-横向移动WmiSmbCrackMapExecProxyChainsImpacket

用途&#xff1a;个人学习笔记&#xff0c;有所借鉴&#xff0c;欢迎指正&#xff01; 前言&#xff1a; 在内网环境中&#xff0c;主机192.168.3.31有外网网卡能出网&#xff0c;在取得该主机权限后上线&#xff0c;搭建web应用构造后门下载地址&#xff0c;利用该主机执行相…

Windows系统VMware创建多个CentOS7虚拟机 NAT网络配置 ssh连接

主要目标: 1.创建3个虚拟机, centos7系统 2.虚拟机之间互相访问 3.物理机访问各虚拟机, 通过xshell建立ssh连接 4.物理机网络变化时,仍能访问 用途: NoSQL课程使用, 课前环境搭建,个人备忘 基本信息&#xff1a; 物理机&#xff1a; windows 11 操作系统 虚拟机软件&#xff…

前端工程化面试题 | 10.精选前端工程化高频面试题

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

OpenCV Mat实例详解 三

OpenCV Mat实例详解 一、二介绍了&#xff0c;OpenCV Mat类构造函数及其公共属性。下面继续介绍OpenCV Mat类公有静态成员函数 OpenCV Mat类公有静态成员函数&#xff08;Static Public Member Functions&#xff09; static CV_NODISCARD_STD Mat diag (const Mat &d)&…

CSP-201903-2-二十四点

CSP-201903-2-二十四点 一、中缀表达式转后缀表达式 中缀表达式是一种常见的数学表达式书写方式&#xff0c;其中操作符位于相关的操作数之间&#xff0c;如 A B。而后缀表达式&#xff08;逆波兰表示法&#xff09;则是一种没有括号&#xff0c;操作符跟随操作数之后的表示…

TIM输出比较 P2

D触发器&#xff1f; 一、输出比较 二、PWM 1、简介 2、结构 三、外部设备 1.舵机 2.直流电机 我的理解是xO1 xIN1 & PWMx; xO2 xIN2 & PWMx;引入PWMx可以更方便的控制特定的电路。 四、函数学习 /*****单独设置输出比较极性*****/ void TIM_OC1PolarityConfig(…

CSS篇--transform

CSS篇–transform 使用transform属性实现元素的位移、旋转、缩放等效果 位移 // 语法 transform:translate(水平移动距离&#xff0c;垂直移动距离) translate() 如果只给一个值&#xff0c;表示x轴方法移动距离 单独设置某个方向的移动距离&#xff1a;translateX() transla…

Rust 基本环境安装

rust 基本介绍请看上一篇文章&#xff1a;rust 介绍 rustup 介绍 rustup 是 Rust 语言的安装器和版本管理工具。通过 rustup&#xff0c;可以轻松地安装 Rust 编译器&#xff08;rustc&#xff09;、标准库和文档。它也允许你切换不同的 Rust 版本或目标平台&#xff0c;以及…

Compose 自定义 - 数据转UI的三阶段(组合、布局、绘制)

一、概念 Compose 通过三个阶段把数据转化为UI&#xff1a;组合&#xff08;要显示什么&#xff09;、布局&#xff08;要显示在哪里&#xff09;、绘制&#xff08;如何渲染&#xff09;。 组合阶段 Compisition 界面首次渲染时会将可组合函数转化为一个个布局节点 Layout Nod…

【打工日常】使用docker部署linux-command解析搜索工具

一、linux-command介绍 linux-command工具是一个非盈利性的工具&#xff0c;里面记录了550 个 Linux 命令&#xff0c;内容包含 Linux 命令手册、详解、学习&#xff0c;是值得收藏的 Linux 命令速查手册。内容来自网络和网友的补充。 二、本次实践介绍 1. 本次实践简介 本次…