当前位置: 首页 > news >正文

C语言指针(初阶)

文章目录

  • 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们觉得博主讲的不错的话,请动动你们滴滴给博主点个赞,你们滴鼓励将成为博主源源不断滴动力!

相关文章:

C语言指针(初阶)

文章目录 1:内存与地址1.1内存1.2:如何理解编址 2:指针变量与地址2.1:指针变量与解引用操作符2.1.1:指针变量2.1.2:如何拆解指针类型2.1.3:解引用操作符 2.2:指针变量的大小 3:指针变量类型的意义代码1解引用修改前解引用修改后 代码2解引用修改前解引用修改后 4:const修饰指针…...

Python循环语句——for循环的嵌套使用

一、引言 在Python编程中&#xff0c;循环是控制程序流程的重要工具&#xff0c;它允许我们重复执行某段代码&#xff0c;直到满足特定的条件为止。其中&#xff0c;for循环是Python中最常用的循环类型之一。而嵌套循环&#xff0c;即在一个循环内部再嵌套另一个循环&#xff…...

Java创建线程真的有三种方式吗?

(/≧▽≦)/~┴┴ 嗨~我叫小奥 ✨✨✨ &#x1f440;&#x1f440;&#x1f440; 个人博客&#xff1a;小奥的博客 &#x1f44d;&#x1f44d;&#x1f44d;&#xff1a;个人CSDN ⭐️⭐️⭐️&#xff1a;传送门 &#x1f379; 本人24应届生一枚&#xff0c;技术和水平有限&am…...

17-k8s控制器资源-job控制

job控制器&#xff1a;就是一次性任务的pod控制器&#xff0c;pod完成作业后不会重启&#xff0c;其重启策略是&#xff1a;Never 1&#xff0c;job控制器案例描述 启动一个pod&#xff0c;执行完成一个事件&#xff0c;然后pod关闭&#xff1b; 事件&#xff1a;计算π的值&a…...

lazarus:LCL 嵌入 fpwebview 组件,做一个简单浏览器

从 https://github.com/PierceNg/fpwebview 下载 fpwebview-master.zip 简单易用。 先请看 \fpwebview-master\README.md cd \lazarus\projects\fpwebview-master\demo\lclembed 修改 lclembed.lpr 如下&#xff0c;将 fphttpapp. 注释掉&#xff0c;因为我用不上 a simple…...

c++类和对象新手保姆级上手教学(上)

前言&#xff1a; c其实顾名思义就是c语言的升级版&#xff0c;很多刚学c的同学第一感觉就是比c语言难学很多&#xff0c;其实没错&#xff0c;c里的知识更加难以理解可以说杂且抽象&#xff0c;光是类和对象&#xff0c;看起来容易&#xff0c;但想完全吃透&#xff0c;真的挺…...

可变参数(c/c++)

目录 一、C语言版本 二、C的实现方法 2.1数据包 2.2sizeof...运算符 2.3可变参数模板的使用 2.4emplace_back() 有时候我们在编写函数时&#xff0c;可能不知道要传入的参数个数&#xff0c;类型 。比如我们要实现一个叠加函数&#xff0c;再比如c语言中的printf,c中的emp…...

【数据结构】图

文章目录 图1.图的两种存储结构2.图的两种遍历方式3.最小生成树的两种算法&#xff08;无向连通图一定有最小生成树&#xff09;4.单源最短路径的两种算法5.多源最短路径 图 1.图的两种存储结构 1. 图这种数据结构相信大家都不陌生&#xff0c;实际上图就是另一种多叉树&…...

32.3K Star,再见 Postman,这款开源 API 客户端更香

Hi&#xff0c;骚年&#xff0c;我是大 G&#xff0c;公众号「GitHub指北」会推荐 GitHub 上有趣有用的项目&#xff0c;一分钟 get 一个优秀的开源项目&#xff0c;挖掘开源的价值&#xff0c;欢迎关注。 使用 API 工具来调试接口是后端开发经常会使用的&#xff0c;之前一直…...

Python循环语句——continue和break

一、引言 在Python编程中&#xff0c;循环是常见的控制流语句&#xff0c;它允许我们重复执行一段代码&#xff0c;直到满足某个条件为止。而在循环中&#xff0c;continue和break是两个非常重要的控制语句&#xff0c;它们可以帮助我们更加灵活地控制循环的行为。 二、contin…...

C++面向对象程序设计-北京大学-郭炜【课程笔记(三)】

C面向对象程序设计-北京大学-郭炜【课程笔记&#xff08;三&#xff09;】 1、构造函数&#xff08;constructor&#xff09;1.1、基本概念 2、赋值构造函数2.1、基本概念2.1、复制构造函数起作用的三种情况2.2、常引用参数的使用 3、类型转换构造函数3.1、什么事类型转换构造函…...

Linux:搭建docker私有仓库(registry)

当我们内部需要存储镜像时候&#xff0c;官方提供了registry搭建好直接用&#xff0c;废话少说直接操作 1.下载安装docker 在 Linux 上安装 Docker Desktop |Docker 文档https://docs.docker.com/desktop/install/linux-install/安装 Docker 引擎 |Docker 文档https://docs.do…...

用HTML、CSS和JS打造绚丽的雪花飘落效果

目录 一、程序代码 二、代码原理 三、运行效果 一、程序代码 <!DOCTYPE html> <html><head><meta http-equiv"Content-Type" content"text/html; charsetGBK"><style>* {margin: 0;padding: 0;}#box {width: 100vw;heig…...

订餐|网上订餐系统|基于springboot的网上订餐系统设计与实现(源码+数据库+文档)

网上订餐系统目录 目录 基于springboot的网上订餐系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、用户功能模块的实现 &#xff08;1&#xff09;用户注册界面 &#xff08;2&#xff09;用户登录界面 &#xff08;3&#xff09;菜品详情界面 &#xff08…...

从零开始学howtoheap:解题西湖论剑Storm_note

how2heap是由shellphish团队制作的堆利用教程&#xff0c;介绍了多种堆利用技术&#xff0c;后续系列实验我们就通过这个教程来学习。环境可参见从零开始配置pwn环境&#xff1a;从零开始配置pwn环境&#xff1a;从零开始配置pwn环境&#xff1a;优化pwn虚拟机配置支持libc等指…...

Rust 基本环境安装

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

【电源】POE系统供电原理(二)

转载本博客文章&#xff0c;请注明出处 ​ 上一篇文章中&#xff0c;有提到POE系统工作原理及动态检测机制&#xff0c;下面我们继续介绍受电端PD技术及原理。POE供电系统包含PSE、PD及互联接口部分组成&#xff0c;如下图所示。 图1 POE供电系统 PSE控制器的主要作用&#xff…...

GPU独显下ubuntu屏幕亮度不能调节解决方法

GPU独显下屏幕亮度不能调节&#xff08;假设你已经安装了合适的nvidia显卡驱动&#xff09;&#xff0c;我试过修改 /etc/default/grub 的 GRUB_CMDLINE_LINUX_DEFAULT"quiet splash acpi_backlightvendor" &#xff0c;没用。修改和xorg.conf相关的文件&#xff0c;…...

Linux篇:网络基础1

一、网络基础&#xff1a;网络本质就是在获取和传输数据&#xff0c;而系统的本质是在加工和处理数据。 1、应用问题&#xff1a; ①如何处理发来的数据&#xff1f;—https/http/ftp/smtp ②长距离传输的数据丢失的问题&#xff1f;——TCP协议 ③如何定位的主机的问题&#…...

RK3568笔记十七:LVGL v8.2移植

若该文为原创文章&#xff0c;转载请注明原文出处。 本文介绍嵌入式轻量化图形库LVGL 8.2移植到Linux开发板ATK-RK3568上的步骤。 主要是参考大佬博客&#xff1a; LVGL v8.2移植到IMX6ULL开发板_lvgl移植到linux-CSDN博客 一、环境 1、平台&#xff1a;rk3568 2、开发板:…...

C#系列-C#访问MongoDB+redis+kafka(7)

目录 一、 C#中访问MongoDB. 二、 C#访问redis. 三、 C#访问kafka. C#中访问MongoDB 在C#中访问MongoDB&#xff0c;你通常会使用MongoDB官方提供的MongoDB C#/.NET Driver。这个驱动提供了丰富的API来执行CRUD&#xff08;创建、读取、更新、删除&#x…...

(12)Hive调优——count distinct去重优化

离线数仓开发过程中经常会对数据去重后聚合统计&#xff0c;count distinct使得map端无法预聚合&#xff0c;容易引发reduce端长尾&#xff0c;以下是count distinct去重调优的几种方式。 解决方案一&#xff1a;group by 替代 原sql 如下&#xff1a; #7日、14日的app点击的…...

记录 | 验证pytorch-cuda是否安装成功

检测程序如下&#xff1a; import torchprint(torch.__version__) print(torch.cuda.is_available()) 或者用终端 Shell&#xff0c;运行情况如下...

LeetCode 239.滑动窗口的最大值 Hot100 单调栈

给你一个整数数组 nums&#xff0c;有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回 滑动窗口中的最大值 。 示例 1&#xff1a; 输入&#xff1a;nums [1,3,-1,-3,5,3,6,7], k 3 输…...

463. Island Perimeter(岛屿的周长)

问题描述 给定一个 row x col 的二维网格地图 grid &#xff0c;其中&#xff1a;grid[i][j] 1 表示陆地&#xff0c; grid[i][j] 0 表示水域。 网格中的格子 水平和垂直 方向相连&#xff08;对角线方向不相连&#xff09;。整个网格被水完全包围&#xff0c;但其中恰好有…...

如何解决缓存和数据库的数据不一致问题

数据不一致问题是操作数据库和操作缓存值的过程中&#xff0c;其中一个操作失败的情况。实际上&#xff0c;即使这两个操作第一次执行时都没有失败&#xff0c;当有大量并发请求时&#xff0c;应用还是有可能读到不一致的数据。 如何更新缓存 更新缓存的步骤就两步&#xff0…...

linux系统下vscode portable版本的python环境搭建003:venv

这里写自定义目录标题 python安装方案一. 使用源码安装&#xff08;有[构建工具](https://blog.csdn.net/ResumeProject/article/details/136095629)的情况下&#xff09;方案二.使用系统包管理器 虚拟环境安装TESTCG 本文目的&#xff1a;希望在获得一个新的系统之后&#xff…...

使用TinyXML-2解析XML文件

一、XML介绍 当我们想要在不同的程序、系统或平台之间共享信息时&#xff0c;就需要一种统一的方式来组织和表示数据。XML&#xff08;EXtensible Markup Language&#xff0c;即可扩展标记语言&#xff09;是一种用于描述数据的标记语言&#xff0c;它让数据以一种结构化的方…...

Linux:docker在线仓库(docker hub 阿里云)基础操作

把镜像放到公网仓库&#xff0c;这样可以方便大家一起使用&#xff0c;当需要时直接在网上拉取镜像&#xff0c;并且你可以随时管理自己的镜像——删除添加或者修改。 1.docker hub仓库 2.阿里云加速 3.阿里云仓库 由于docker hub是国外的网站&#xff0c;国内的对数据的把控…...

C语言程序设计(第四版)—习题7程序设计题

目录 1.选择法排序。 2.求一批整数中出现最多的数字。 3.判断上三角矩阵。 4.求矩阵各行元素之和。 5.求鞍点。 6.统计大写辅音字母。 7.字符串替换。 8.字符串转换成十进制整数。 1.选择法排序。 输入一个正整数n&#xff08;1&#xff1c;n≤10&#xff09;&#xf…...

ZCC6982-同步升压充双节锂电池充电芯片

特性 ■高达 2A 的可调充电电流&#xff08;受实际散热和输入功率限制&#xff09; ■支持 8.4V、8.6V、8.7V、8.8V 的充满电压&#xff08;限QFN&#xff09; ■高达 28V 的输入耐压保护 ■高达 28V 的电池端耐压保护 ■宽输入工作电压范围&#xff1a;3.0V~6.5V ■峰值…...

定时器(基本定时器、通用定时器、高级定时器)

目录 一、基本定时器 二、通用定时器 三、高级定时器 一、基本定时器 1、作用&#xff1a;计时和计数。 二、通用定时器 1、除了有基本定时器的计时和计数功能外&#xff0c;主要有输入捕获和输出比较的功能&#xff0c;硬件主要由六大部分组成&#xff1a; ① 时钟源 ② 控…...

009集——磁盘详解——电脑数据如何存储在磁盘

很多人也知道数据能够保存是由于设备中有一个叫做「硬盘」的组件存在&#xff0c;但也有很多人不知道硬盘是怎样储存这些数据的。这里给大家讲讲其中的原理。 首先我们要明白的是&#xff0c;计算机中只有0和1&#xff0c;那么我们存入硬盘的数据&#xff0c;实际上也就是一堆0…...

鸿蒙开发-HarmonyOS UI架构

初步布局Index 当我们新建一个工程之后&#xff0c;首先会进入Index页。我们先简单的做一个文章列表的显示 class Article {title?: stringdesc?: stringlink?: string }Entry Component struct Index {State articles: Article[] []build() {Row() {Scroll() {Column() …...

Flutter 动画(显式动画、隐式动画、Hero动画、页面转场动画、交错动画)

前言 当前案例 Flutter SDK版本&#xff1a;3.13.2 显式动画 Tween({this.begin,this.end}) 两个构造参数&#xff0c;分别是 开始值 和 结束值&#xff0c;根据这两个值&#xff0c;提供了控制动画的方法&#xff0c;以下是常用的&#xff1b; controller.forward() : 向前…...

用HTML5 Canvas创造视觉盛宴——动态彩色线条效果

目录 一、程序代码 二、代码原理 三、运行效果 一、程序代码 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <!-- 声明文档类型为XHTML 1.0 Transitional -…...

云原生介绍与容器的基本概念

云原生介绍 1、云原生的定义 云原生为用户指定了一条低心智负担的、敏捷的、能够以可扩展、可复制的方式最大化地利用云的能力、发挥云的价值的最佳路径。 2、云原生思想两个理论 第一个理论基础是&#xff1a;不可变基础设施。 第二个理论基础是&#xff1a;云应用编排理…...

Flash存储

目录 一、MCU读写擦除Flash步骤 1、写flash步骤&#xff1a; 2、读flash步骤&#xff1a; 3、擦除flash步骤&#xff1a; 4、要注意的地方&#xff1a; 一、MCU读写擦除Flash步骤 1、写flash步骤&#xff1a; (1)解锁 2、读flash步骤&#xff1a; 3、擦除flash步骤&#x…...

Day 44 | 动态规划 完全背包、518. 零钱兑换 II 、 377. 组合总和 Ⅳ

完全背包 题目 文章讲解 视频讲解 完全背包和0-1背包的区别在于&#xff1a;物品是否可以重复使用 思路&#xff1a;对于完全背包问题&#xff0c;内层循环的遍历方式应该是从weight[i]开始一直遍历到V&#xff0c;而不是从V到weight[i]。这样可以确保每种物品可以被选择多次…...

使用PaddleNLP UIE模型提取上市公司PDF公告关键信息

项目地址&#xff1a;使用PaddleNLP UIE模型抽取PDF版上市公司公告 - 飞桨AI Studio星河社区 (baidu.com) 背景介绍 本项目将演示如何通过PDFPlumber库和PaddleNLP UIE模型&#xff0c;抽取公告中的相关信息。本次任务的PDF内容是破产清算的相关公告&#xff0c;目标是获取受理…...

软件工程师,OpenAI Sora驾到,快来围观

概述 近期&#xff0c;OpenAI在其官方网站上公布了Sora文生视频模型的详细信息&#xff0c;展示了其令人印象深刻的能力&#xff0c;包括根据文本输入快速生成长达一分钟的高清视频。Sora的强大之处在于其能够根据文本描述&#xff0c;生成长达60秒的视频&#xff0c;其中包含&…...

【Linux 04】编辑器 vim 详细介绍

文章目录 &#x1f308; Ⅰ 基本概念&#x1f308; Ⅱ 基本操作1. 进入 / 退出 vim2. vim 模式切换 &#x1f308; Ⅲ 命令模式1. 光标的移动2. 复制与粘贴3. 剪切与删除4. 撤销与恢复 &#x1f308; Ⅳ 底行模式1. 保存文件2. 查找字符3. 退出文件4. 替换内容5. 显示行号6. 外…...

KMP算法详解

1. 问题引入 链接&#xff1a;leetcode_28 题目&#xff1a;s1字符串是否包含s2字符串&#xff0c;如果包含返回s1中包含s2的最左开头位置&#xff0c;不包含返回-1 暴力方法就是s1的每个位置都做开头&#xff0c;然后去匹配s2整体&#xff0c;时间复杂度O(n*m) KMP算法可以…...

ubuntu22.04@laptop OpenCV Get Started: 013_contour_detection

ubuntu22.04laptop OpenCV Get Started: 013_contour_detection 1. 源由2. 应用Demo2.1 C应用Demo2.2 Python应用Demo 3. contour_approx应用3.1 读取图像并将其转换为灰度格式3.2 应用二进制阈值过滤算法3.3 查找对象轮廓3.4 绘制对象轮廓3.5 效果3.6 CHAIN_APPROX_SIMPLE v.s…...

[ai笔记5] 个人AI资讯助手实战

欢迎来到文思源想的ai空间&#xff0c;这是技术老兵重学ai以及成长思考的第5篇分享&#xff0c;也是把ai场景化应用的第一篇实操内容&#xff01; 既然要充分学习和了解ai&#xff0c;自然少不了要时常看看ai相关资讯&#xff0c;所以今天特地用字节的“扣子”做了一个ai的资讯…...

QT+OSG/osgEarth编译之八十九:osgdb_ply+Qt编译(一套代码、一套框架,跨平台编译,版本:OSG-3.6.5插件库osgdb_ply)

文章目录 一、osgdb_ply介绍二、文件分析三、pro文件四、编译实践一、osgdb_ply介绍 斯坦福三角形格式(Stanford Triangle Format)是一种用于存储三维模型数据的文件格式,也称为 PLY 格式。它最初由斯坦福大学图形实验室开发,用于存储和共享三维扫描和计算机图形数据。 P…...

机器人专题:我国机器人产业园区发展现状、问题、经验及建议

今天分享的是机器人系列深度研究报告&#xff1a;《机器人专题&#xff1a;我国机器人产业园区发展现状、问题、经验及建议》。 &#xff08;报告出品方&#xff1a;赛迪研究院&#xff09; 报告共计&#xff1a;26页 机器人作为推动工业化发展和数字中国建设的重要工具&…...

算法沉淀——哈希算法(leetcode真题剖析)

算法沉淀——哈希算法 01.两数之和02.判定是否互为字符重排03.存在重复元素04.存在重复元素 II05.字母异位词分组 哈希算法&#xff08;Hash Algorithm&#xff09;是一种将任意长度的输入&#xff08;也称为消息&#xff09;映射为固定长度的输出的算法。这个输出通常称为哈希…...

深入理解Redis哨兵原理

哨兵模式介绍 在深入理解Redis主从架构中Redis 的主从架构中&#xff0c;由于主从模式是读写分离的&#xff0c;如果主节点&#xff08;master&#xff09;挂了&#xff0c;那么将没有主节点来服务客户端的写操作请求&#xff0c;也没有主节点给从节点&#xff08;slave&#…...

MySQL-存储过程(PROCEDURE)

文章目录 1. 什么是存储过程&#xff1f;2. 存储过程的优点3. MySQL中的变量3.1 系统变量3.2 用户自定义变量3.3 局部变量 4. 存储过程的相关语法4.1 创建存储过程&#xff08;CREATE&#xff09;4.2 查看存储过程&#xff08;SHOW&#xff09;4.3 修改存储过程&#xff08;ALT…...