指针小课堂

目录

一.内存和地址

二.指针变量和地址

1.取地址操作符(&)

2.指针变量和解引⽤操作符(*)

2.1指针变量

2.2如何理解指针类型

2.3解引用操作符

[2.4 指针的解引用](#2.4 指针的解引用)

2.5.不同指针类型的运加减性质

2.5.1指针与整数相加:

2.5.2指针与整数相减:

2.5.3指针运算的实际地址:

三.void*指针

[四.const 修饰指针](#四.const 修饰指针)

[1. const 修饰指针所指向的对象](#1. const 修饰指针所指向的对象)

[2. const 修饰指针本身](#2. const 修饰指针本身)

[3. const 同时修饰指针和指针所指向的对象](#3. const 同时修饰指针和指针所指向的对象)

五.野指针

5.1野指针成应

5.2规避野指针

1.指针初始化

2.小心指针越界

3.指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性

4.避免返回局部变量的地址

六.assert断言

[使用 assert 的步骤](#使用 assert 的步骤)


一.内存和地址

说到内存,内存就像一栋宿舍楼,而每一楼都有这十几二十个房间,每个房间都能住好几个人。那如果我们需要寻找某一个房间的时候,我们需要怎么找呢?答案自然是通过房间号来找了。房间号我们也称之为地址,在计算机中我们把内存单元的编号也称为地址。C语⾔中给地址起了新的名字:指针。

相对的:
内存 相当于一栋宿舍楼

内存单元 相当于一个房间每个内存单元取1个字节

比特位 相当于一个学生 (1个字节能放8个比特位)

就像这样:

所以内存单元编号=地址=指针

二.指针变量和地址

1.取地址操作符(&)

++在C语⾔中创建变量其实就是向内存申请空间++

cpp 复制代码
#include<stdio.h>
int main() {
	int a = 10;
	printf("%p", &a);
	return 0;
}

我们打印出a的地址,如图:

再看一下他的内存所在,可以看到,其一共占用4个字节 ,因为a是int型 的,而且值也恰好是10(0a十六进制转换为十进制为10)。

2.指针变量和解引⽤操作符(*)

2.1指针变量

我们在上面介绍了&操作符,那我们拿到了地址要怎么存放呢?

答案是用指针变量

指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址。

cpp 复制代码
#include<stdio.h>
int main() {
	int a = 10;
	int* pa = &a;//指针变量存放地址
	printf("%p\n", &a);
	printf("%p", pa);
	return 0;
}

可以看到两地址都相等:

2.2如何理解指针类型

cpp 复制代码
    int a = 10;
	int* pa = &a;

' * '代表着pa是一个指针变量

int 则说明pa是整型类型

**(注意:*可以写在左边一点,也可以右边一点,都是正确的),**如:

cpp 复制代码
int *pa=&a;
int* pa=&a;

指针类型不仅仅只有int型的还有:

|----------|--------|-------------|
| 字符型指针 | char | char* p1 |
| 短整型指针 | short | short* p1 |
| 整型指针 | int | int* p1 |
| 长整型指针 | long | long* p1 |
| 单精度浮点型指针 | float | float* p1 |
| 双精度浮点型指针 | double | double* p1 |

(注意指针变量的类型 要与变量的基本类型相同)

对于这么多的类型,我们来查看一下他们的内存大小:

cpp 复制代码
#include<stdio.h>
int main()
{
	printf("%d\n", sizeof(char*));
	printf("%d\n", sizeof(short*));
	printf("%d\n", sizeof(int*));
	printf("%d\n", sizeof(long*));
	printf("%d\n", sizeof(float*));
	printf("%d\n",sizeof(double*));
	return 0;
}

不同平台的运行结果:

++总结:++

无论是哪一种平台下计算的结果,每种指针类型的内存大小都一样,都是4或8个字节。

2.3解引用操作符

解引用操作符 用于获取指针所指向的对象或变量的值
解引⽤操作符(*)。

两个例子:

cpp 复制代码
int x = 10;
int* ptr = &x; // ptr 是指向 x 的指针
int y = *ptr;  // 解引用 ptr,获取 x 的值,y 现在是 10
cpp 复制代码
int a = 100;
int* pa = &a;//pa指向a的地址
*pa = 0;//解引用pa,通过pa中存放的地址,找到指向的空间,
//*pa其实就是a变量了;所以*pa = 0,这个操作符是把a改成了0

至于为什么弄这么复杂,直接一点定义一个变量直接赋值,或者直接让它等于零就行了,为什么还有多此一举绕来绕去呢?

当然我们可以这么做,但是的话我们多一种方法多一种途径来给他赋值或者干嘛的,何乐而不为呢,学会之后届时我们写代码的时候就可以更加灵活了。

2.4 指针的解引用

对比下面两个代码:

(1)

cpp 复制代码
#include <stdio.h>
int main()
{
	int n = 0x11223344;十六进制转换为十进制结果为287454020
	int* pi = &n;
	printf("%p\n", pi);
	*pi = 0;
	printf("%d", n);//0
	return 0;
}

结果为:

pi在内存中的地址和字节:

(2)

cpp 复制代码
#include <stdio.h>
int main()
{
	int n = 0x11223344;//十六进制转换为十进制结果为287454020
	char* pc = (char*)&n;//强制转换为char*型
	printf("%p\n", pc);
	*pc = 0;
	printf("%d", n);//287453952
	return 0;
}

结果为:

'

pc在内存中的地址和字节:

++通过调试我们可以看到,代码(1)会将n的4个字节全部改为0,但是代码(2)只是将n的第⼀个字节改为0。++

++总结:++ 指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。 ⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。

2.5.不同指针类型的运加减性质

在许多编程语言中(例如C和C++),指针与整数相加或相减是一个常见的操作。这个操作可以用来在内存中遍历数组或数据结构。

2.5.1指针与整数相加
cpp 复制代码
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr; // ptr 指向数组的第一个元素,即 arr[0].注意单个数组名一般指向数组的第一个元素
ptr = ptr + 2; // 现在 ptr 指向 arr[2],即 30

++解释:++

  • 当你将一个指针与一个整数相加时,结果是一个新的指针,它指向原始指针指向的内存地址之后的某个位置。
  • 如果指针指向的是一个数组的元素,那么指针加上整数 n 将指向数组中从当前元素开始的第 n 个元素。
2.5.2指针与整数相减
cpp 复制代码
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = &arr[3]; // ptr 指向数组的第四个元素,即 arr[3]
ptr = ptr - 2; // 现在 ptr 指向 arr[1],即 20

++解释:++

  • 当你将一个指针与一个整数相减时,结果是一个新的指针,它指向原始指针指向的内存地址之前的某个位置。
  • 如果指针指向的是一个数组的元素,那么指针减去整数 n 将指向数组中从当前元素开始的第 n 个之前的元素。
2.5.3指针运算的实际地址

++解释:++

  • 由于指针运算考虑了指针所指向数据类型的大小**(sizeof(类型))** ,这意味着 ptr + 1 实际上是增加了 sizeof(类型) 个字节,而不是简单的增加 1
  • 例如,如果 ptr 是一个 int* 类型的指针,假设 int 类型占用 4 个字节,那么 ptr + 1 实际上是将 ptr 的地址增加了 4 个字节。

代码示例:

cpp 复制代码
#include <stdio.h>
int main()
{
 int n = 10;
 char *pc = (char*)&n;
 int *pi = &n;
 
 printf("%p\n", &n);
 printf("%p\n", pc);
 printf("%p\n", pc+1);
 printf("%p\n", pi);
 printf("%p\n", pi+1);
 return 0;
}

结果如图:

2.5.4指针运算的用法

遍历数组:指针运算可以用于遍历数组中的元素。

cpp 复制代码
#include <stdio.h>
int main()
{
	int arr[5] = { 10, 20, 30, 40, 50 };
	int* ptr = arr;
	for (int i = 0; i < 5; i++) {
		printf("%d ", *(ptr + i)); // 输出数组中的每一个元素
	}

	return 0;

结果如图:

指向结构体成员:指针运算还可以用于遍历结构体数组中的元素。

三.void*指针

void* 指针在C和C++中是一种通用指针类型 ,表示它可以指向任意类型的数据void* 指针本身不包含类型信息,只是一个内存地址,因此不能直接解引用进行指针运算

在将 void* 指针传递给其他函数时,通常需要将其转换为具体类型的指针。类型转换使用类型转换运算符 (type*)

cpp 复制代码
void* ptr;
int x = 10;
ptr = &x; // void* 指向 int 类型变量

int* intPtr = (int*)ptr; // 将 void* 转换为 int* 类型
printf("%d\n", *intPtr); // 解引用 int* 类型指针,输出 10
  • void* 指针常用于需要接受不同类型数据的函数参数。
  • 例如,一个通用的比较函数可以使用 void* 指针来比较不同类型的值:
cpp 复制代码
int compare(const void* a, const void* b) {
    return (*(int*)a - *(int*)b);
}

qsort(arr, 5, sizeof(int), compare); // 使用 qsort 排序 int 类型数组

注意事项:

不能直接解引用

  • 由于 void* 不包含类型信息,不能直接对其进行解引用操作。必须先将其转换为具体类型的指针,然后才能解引用。
  • 错误示例:
cpp 复制代码
void* ptr;
int x = 10;
ptr = &x;
// printf("%d\n", *ptr); // 错误:void* 不能直接解引用

不能进行指针运算

  • 由于 void* 指针没有确定的类型大小,不能进行指针算术运算(如 ptr + 1)。必须将其转换为具体类型指针后再进行运算。
  • 错误示例:
cpp 复制代码
void* ptr;
int arr[5] = {1, 2, 3, 4, 5};
ptr = arr;
// ptr++; // 错误:void* 不能进行指针运算

四.const 修饰指针

在C和C++中,const 修饰符可以用来修饰指针及其指向的对象。这可以用来确保代码中的某些值不会被意外修改。const 可以以几种不同的方式修饰指针

1. const 修饰指针所指向的对象

const 修饰指针所指向的对象 时(注意const在int*的左边),表示通过该指针不能修改所指向的对象 。这个声明可以解读为"指向 int 的指针是常量"。它意味着指针本身可以改变指向不同的地址,但不能通过该指针修改所指向的值。

cpp 复制代码
int x = 10;
int y = 20;
const int* ptr = &x; // ptr 指向 x

ptr = &y; // 可以改变 ptr 的指向
// *ptr = 30; // 错误:不能通过 ptr 修改 y 的值

2. const 修饰指针本身

const 修饰指针本身 时(注意const在int*的右边),表示指针本身是常量不能指向其他地址 。这个声明可以解读为"指针是一个常量,指向 int"。它意味着指针必须在声明时初始化,之后不能改变其指向,但可以通过指针修改所指向的对象的值。

cpp 复制代码
int x = 10;
int* const ptr = &x; // ptr 必须初始化

*ptr = 20; // 可以通过 ptr 修改 x 的值
// ptr = &y; // 错误:不能改变 ptr 的指向

3. const 同时修饰指针和指针所指向的对象

const 同时修饰指针和指针所指向的对象 时(注意int*两边都有const),表示指针所指向的对象不能被修改 。这个声明可以解读为"指向 int 的常量指针是常量"。它意味着指针必须在声明时初始化,之后不能改变其指向,也不能通过该指针修改所指向的对象的值。

cpp 复制代码
int x = 10;
const int* const ptr = &x; // ptr 必须初始化

// *ptr = 20; // 错误:不能通过 ptr 修改 x 的值
// ptr = &y; // 错误:不能改变 ptr 的指向

五.野指针

5.1野指针成应

(1). 指针未初始化

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

(2).指针越界访问

cpp 复制代码
#include <stdio.h>
int main()
{
 int arr[10] = {0};
 int *p = &arr[0];
 int i = 0;
 for(i=0; i<=11; i++)
 {
 //当指针指向的范围超出数组arr的范围时,p就是野指针
 *(p++) = i;
 }
 return 0;
}

(3).指针指向的空间释放

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

5.2规避野指针

1.指针初始化

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

初始化如下:

cpp 复制代码
#include <stdio.h>
int main()
{
 int num = 10;
 int*p1 = &num;
 int*p2 = NULL;
 
 return 0;
}
2**.小心**指针越界

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

3.指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的 时候,我们可以把该指针置为NULL

因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问, 同时使⽤指针之前可以判断指针是否为NULL

我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来, 就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓起来,就是把野指针暂时管理起 来。

不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我 们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去 使⽤。

cpp 复制代码
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++) = i;
 }
 //此时p已经越界了,可以把p置为NULL
 p = NULL;
 //下次使⽤的时候,判断p不为NULL的时候再使⽤
 //...
 p = &arr[0];//重新让p获得地址
 if(p != NULL) //判断
 {
 //...
 }
 return 0;
}
4.避免返回局部变量的地址
cpp 复制代码
#include<stdio.h>
int* test()
{
	int a = 0;//局部变量a出了test函数就会被销毁
	return &a;
}
int main()
{
	int* p = test();
	printf("%d\n",*p);
	return 0;
}

因为出了test函数,局部变量a就已经被销毁了,本来属于局部变量a的地址,现在却已经不是他的了。此时这块地址的指向是不确定的

六.assert断言

assert 断言是一种用于在开发和调试阶段检测程序错误的工具 。它在C和C++(以及其他编程语言如Python)中被广泛使用,以验证程序运行时的假设是否为真。如果断言失败程序会中止执行 ,并通常会显示错误信息

使用 assert 的步骤

  1. 包含头文件

    • 在使用 assert 之前,需要包含头文件 <assert.h>
  2. 使用 assert

    • assert 宏用于检查表达式是否为真。如果表达式为假,程序会终止并显示错误信息,包括表达式、文件名和行号。

基本运用:

cpp 复制代码
#include <stdio.h>
#include <assert.h>

int main() {
    int x = 5;
    assert(x == 5); // 如果 x 不等于 5,程序将终止

    printf("x is 5\n");

    x = 10;
    assert(x == 5); // 这一行将导致程序终止,因为 x 不等于 5

    printf("This line will not be executed\n");

    return 0;
}

检查指针是否为 NULL:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main() {
    int* ptr = (int*)malloc(sizeof(int));
    assert(ptr != NULL); // 检查内存分配是否成功

    *ptr = 42;
    printf("Value: %d\n", *ptr);

    free(ptr);
    ptr = NULL; // 释放内存并将指针置为 NULL

    assert(ptr == NULL); // 检查指针是否为 NULL

    return 0;
}

assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和 出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问 题,不需要再做断⾔,就在 #include 语句的前⾯,定义⼀个宏 NDEBUG 。

cpp 复制代码
#define NDEBUG
#include <assert.h>

然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移 除这条 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语 句。

完!

点个赞吧,感谢阅读!

相关推荐
QAQ小菜鸟30 分钟前
一、初识C语言(1)
c语言
何曾参静谧1 小时前
「C/C++」C/C++ 之 变量作用域详解
c语言·开发语言·c++
互联网打工人no11 小时前
每日一题——第一百二十一题
c语言
朱一头zcy2 小时前
C语言复习第9章 字符串/字符/内存函数
c语言
此生只爱蛋2 小时前
【手撕排序2】快速排序
c语言·c++·算法·排序算法
何曾参静谧3 小时前
「C/C++」C/C++ 指针篇 之 指针运算
c语言·开发语言·c++
lulu_gh_yu3 小时前
数据结构之排序补充
c语言·开发语言·数据结构·c++·学习·算法·排序算法
~yY…s<#>5 小时前
【刷题17】最小栈、栈的压入弹出、逆波兰表达式
c语言·数据结构·c++·算法·leetcode
EricWang13587 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
我是谁??7 小时前
C/C++使用AddressSanitizer检测内存错误
c语言·c++