Bootstrap

指针小课堂

目录

一.内存和地址

二.指针变量和地址

1.取地址操作符(&)

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

2.1指针变量

2.2如何理解指针类型

2.3解引用操作符

2.4 指针的解引用

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

2.5.1指针与整数相加:

2.5.2指针与整数相减:

2.5.3指针运算的实际地址:

三.void*指针

四.const 修饰指针

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

2. const 修饰指针本身

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

五.野指针

5.1野指针成应

5.2规避野指针

1.指针初始化

2.小心指针越界

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

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

六.assert断言

使用 assert 的步骤


一.内存和地址

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

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

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

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

就像这样:

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

二.指针变量和地址

1.取地址操作符(&)

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

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

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

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

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

2.1指针变量

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

答案是用指针变量

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

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

可以看到两地址都相等:

2.2如何理解指针类型

    int a = 10;
	int* pa = &a;

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

int则说明pa是整型类型

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

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

  

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

字符型指针charchar* p1
短整型指针shortshort* p1
整型指针intint* p1
长整型指针longlong* p1
单精度浮点型指针floatfloat* p1
双精度浮点型指针doubledouble* p1

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

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

#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解引用操作符

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

解引⽤操作符(*)。

两个例子:

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

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

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

2.4 指针的解引用

对比下面两个代码:
(1)

#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)

#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指针与整数相加
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指针与整数相减
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 个字节。

代码示例:

#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指针运算的用法

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

#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*)

void* ptr;
int x = 10;
ptr = &x; // void* 指向 int 类型变量

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

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

注意事项:

       

       不能直接解引用

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

    不能进行指针运算

  • 由于 void* 指针没有确定的类型大小,不能进行指针算术运算(如 ptr + 1)。必须将其转换为具体类型指针后再进行运算。
  • 错误示例:
  • 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 x = 10;
int y = 20;
const int* ptr = &x; // ptr 指向 x

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

2. const 修饰指针本身

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

int x = 10;
int* const ptr = &x; // ptr 必须初始化

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

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

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

int x = 10;
const int* const ptr = &x; // ptr 必须初始化

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

五.野指针

5.1野指针成应

(1). 指针未初始化

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

(2).指针越界访问

#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).指针指向的空间释放

#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也是地址,这个地址是⽆法使⽤的,读写该地址 会报错。

初始化如下:

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

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

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

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

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

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

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

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.避免返回局部变量的地址

#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 宏用于检查表达式是否为真。如果表达式为假,程序会终止并显示错误信息,包括表达式、文件名和行号。

基本运用:

#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:

#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 。

#define NDEBUG
#include <assert.h>

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

完!

点个赞吧,感谢阅读!

;