C语言中的指针(1)

目录

指针是什么?

指针变量

&和*操作符

指针变量的类型

指针变量的大小

指针变量类型的意义

void* 指针

const 修饰指针

const修饰变量

const修饰指针变量

指针运算

• 指针+- 整数

• 指针-指针

​编辑

​编辑

​编辑

• 指针的关系运算

野指针

概念

成因

指针未初始化

​编辑

指针越界访问

指针指向的空间释放

避免野指针

常见方法

assert断言

作用及使用

优点

缺点

使用注意


指针是什么?

计算机把内存划分为⼀个个的内存单元,每个内存单元的⼤⼩取1个字节(8个比特位)
每个内存单元(字节)都有⼀个编号(这个编号就相当于房间的⻔牌号),有了这个内存单元的编
号,CPU就可以快速找到⼀个内存空间。⽣活中我们把⻔牌号也叫地址,在计算机中我们 把内存单元的编号也称为地址。C语⾔中给地址起 了新的名字叫: 指针。
所以我们可以理解为:
内存单元的编号 == 地址 == 指针
补充一下常见计算机单位:
bit - ⽐特位 ( 比特位(bit)最小的数据单位,表示一个二进制数字,其值只能为0或1。比特位是 计算机内部数据存储的最小单位,用于表示最基本的数据级别。
Byte - 字节 (字节是计算机数据处理的 基本单位,主要用来解释信息。每个字节由8个二进制位(8个比特位)组成,习惯上用大写的B表示,字节表示更大的数据类型)
KB
MB
GB
TB
PB
……
换算关系:
1B yte = 8b it
1 KB = 1024B yte
1 MB = 1024 KB
1 GB = 1024 MB
1 TB = 1024 GB
1 PB = 1024 TB
……

指针变量

我们定义一个变量来专门存放另外一个变量的地址(指针),这个变量就是指针变量

&和*操作符

前面我们了解了操作符的知识,这里我们来详细讲解&和*这两个操作符。

&是取地址操作符,比如我们定义了一个整型变量a,我们知道整型是四个字节,在内存中以补码的形式存放,&a取出的是a所占4个字节中地址较⼩的字节的地址。

我们写一个简单的代码进行测试:

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

我们可以看到打印的是较小字节的地址,0x00F3FC68(前面的0x代表十六进制表示的地址)

那我们通过取地址操作符(&)拿到的地址是⼀个数值,⽐如:0x00F3FC68
这个数值有时候也是需要 存储起来,⽅便后期再使⽤的,我们把这样的地址值存放 指针变量 中。
#include<stdio.h>
int main()
{
	int a = 10;
	int* pointer = &a;//指针变量pointer
	return 0;
}

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

那么我们怎么样可以得到这个指针所指向的值呢?这个时候就需要另外一个操作符

*(解引用操作符)来进行操作,*pointer的意思就是通过pointer中存放的地址,找到指向的空间,

*pointer其实就是a变量。
#include<stdio.h>
int main()
{
	int a = 10;
	int* pointer = &a;//指针变量pointer
	printf("%d\n", *pointer);
	//*解引用操作符
	return 0;
}

我们可以通过对指针解引用来更改变量的值

#include<stdio.h>
int main()
{
	int a = 10;
	int* pointer = &a;//指针变量pointer
	*pointer = 100;
	printf("%d\n", a);
	printf("%d\n", *pointer);
	//*解引用操作符
	return 0;
}

指针变量的类型

一个整型变量的类型是int,一个浮点型变量类型是float……那么一个指针变量的类型是什么呢?

我们可以来监视一下:

我们可以看到指针变量也是有类型的,比如上面代码中指针变量pointer的类型是int*,如果是存放float类型数据的地址的指针变量类型是什么呢?

#include<stdio.h>
int main()
{
	int a = 10;
	int* pointer1 = &a;
	float b = 5.2f;
	//5.2编译器默认是double类型,使用float会报警告,我们可以在后面加上一个f
	float* pointer2 = &b;
	printf("%d\n", *pointer1);
	printf("%.2f\n", *pointer2);
	//%.2f保留小数点后面两位
	//*解引用操作符
	return 0;
}

我们可以看到指针变量pointer1类型是int*,指针变量pointer2类型是float*

我们可以分为两部分来理解指针变量的类型,以指针变量pointer为例:

* 是在说明pointer1是 指针变量 ,⽽前⾯的 int 是在说明pointer 指向的是整型(int) 类型的对象,我们一般把 int叫做它的 基类型

指针变量的大小

既然指针变量也是变量,那么它的大小与它的基类型有关系吗?

我们可以使用sizeof来计算一个变量或者类型的字节大小,那我们就可以使用sizeof来计算指针变量的大小

#include<stdio.h>
int main()
{
	int a = 10;
	int* pointer1 = &a;
	float b = 5.2f;
	float* pointer2 = &b;
	char c = 'c';
	char* pointer3 = &c;
	printf("%d\n", sizeof(pointer1));
	printf("%d\n", sizeof(pointer2));
	printf("%d\n", sizeof(pointer3));
	return 0;
}

我们发现相同环境下,不同类型的指针变量的大小是一样的,都是4或者8个字节。

这里先补充一个知识点:

32位机器(vs的x86环境)假设有32根地址总线,每根地址线出来的电信号转换成数字信号后 是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个比特位,需要4 个字节才能存储。

64位机器(vs的x64环境),假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变量的⼤⼩就是8个字节。

在vs中,x64表示64位环境,这个地址是64个比特位(8个字节)的

x86是32位环境,地址是32个比特位(4个字节)的

这两个环境显示的地址都是十六进制的,但是地址字节长度不一样

我们可以进行一个简单的验证


 

结论:
32位平台下地址是32个bit位,指针变量⼤⼩是4个字节
64位平台下地址是64个bit位,指针变量⼤⼩是8个字节
指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的(4或者8)

指针变量类型的意义

看到这里,有人可能会问既然指针变量的大小与类型无关,那么指定指针变量的类型有什么意义呢?事实上,不同类型的数据在内存中所占的字节数和存放方式是不同的,指定指针类型是有特殊意义的。

我们来看看下面两段代码中p在进行解引用操作后(*p=0)它在内存中有什么变化?

#include<stdio.h>
int main()
{
	int n = 0x11223344;
	int* p = &n;
	*p = 0;
	return 0;
}
#include<stdio.h>
int main()
{
	int n = 0x11223344;
	char* p = &n;
	*p = 0;
	return 0;
}

我们可以发现第一段代码全部置为0,而第二段代码只有低位字节44置为0.这是因为第一段代码指针变量p是int*类型的,而第二段代码指针变量p是char*类型的,所以这是跟指针变量的类型有关的。
我们可以得出结论: 指针的类型决定了对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)
char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。
我们来看看下一个例子:

//指针+-整数
#include<stdio.h>
int main()
{
	int n = 6;
	int* pi = &n;
	char* pc = &n;
	printf("%p\n", &n);
	printf("%p\n", pi);
	printf("%p\n", pi + 1);
	printf("%p\n", pc);
	printf("%p\n", pc + 1);
	return 0;
}

char* 类型的指针变量pc+1跳过1个字节, int* 类型的指针变量pi+1跳过4个字节, 这是因为指针变量的类型带来的变化。 指针+1,是跳过1个指针指向的元素。
我们可以得出结论: 指针类型决定了指针向前或者向后⾛⼀步有多⼤(距离)
结论:
1.指针类型决定了解引用返回多大的空间
2.指针类型决定了指针加减整数跳动的距离(决定步长)

void* 指针

指针类型中有⼀种特殊的类型是 void * 类型的,可以理解为 ⽆具体类型 的指针(泛型指 针),这种类型的指针可以⽤来 接受任意类型地址 ,但是也有局限性, void* 类型的指针不能直接进⾏指针的+-整数和解引⽤的运算。
#include<stdio.h>
int main()
{
	int n = 6;
	void* p = &n;
	*p = 10;
	p = p + 1;
	return 0;
}
我们可以看看编译器就报错, void* 类型的指针可以接收不同类型的地址,但是⽆法直接进⾏指针运算。
我们⼀般将 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以 实现泛型编程的效果,使得⼀个函数来处理多种类型的数据,在函数内部使用的时候我们可以把void*类型的指针进行强制类型转换。
比如下面这个代码:
#include<stdio.h>
int test(void* pi)
{
	int* tmp = (int*)pi;//void*强制类型转换为int*
	*tmp = 20;
	return *tmp;
}
int main()
{
	int a = 10;
	int* p = &a;
	int ret = test(p);
	printf("%d\n", ret);
	return 0;
}

const 修饰指针

const有什么作用呢?我们一起来看看

const修饰变量

#include<stdio.h>
int main()
{
	int a = 10;
	const int b = 100;
	a = 20;
	b = 200;
	return 0;
}

我们可以看到当用const修饰变量b时,如果想要修改b的值,编译器就会报错。所以const修饰的变量值不可以改变,如果我们以后希望一个变量的值不被更改,我们就可以使用const。

const修饰指针变量

const修饰指针变量的时候,当const在不同的位置会有不同的效果,比如下面这个代码:

#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	int d = 40;
	int* pa = &a;
	const int* pb = &b;
	int const* pc = &c;
	int* const pd = &d;
	*pa = 100;
	*pb = 200;
	*pc = 300;
	*pd = 400;
    pb = &a;
    pc = &a;
	pd = &a;
	return 0;
}

可以看到编译器在30,31,35报错,我们发现在修饰指针变量时,26,27行代码const在*号的左边,28行代码const在*号的右边,指针变量pb和pc指向的内容(值)不可以改变,但是指针变量本身可以改变,指针变量pd指向的内容(值)可以改变,但是指针变量本身不可以改变。

结论:

const如果放在 *的左边 ,修饰的是指针指向的内容,保证 指针指向的内容不能通过指针来改变 但是 指针变量本⾝的内容可变
const如果放在 *的右边 ,修饰的是指针变量本⾝,保证了 指针变量本身的内容不能修改 ,但是 指针指向的内容(值) 可以通过指针改变。

指针运算

指针的基本运算有三种,分别是:
指针+- 整数
指针-指针
指针的关系运算

指针+- 整数

数组在内存中是连续存放的,只要知道第⼀个元素的地址,就能找到后⾯的所有元素。
指针+-整数 不是跳过的一个字节,而是跳过的是【 sizeof(指针基类型)*(整数) 】个字节数。
例:如果把一维数组名给一个指针变量,那么我们就可以通过指针变量+整数找到想要的那个元素的地址。
#include<stdio.h>
int main()
{
	int arr[6] = { 1,2,3,4,5,6 };
	//下标         0 1 2 3 4 5
	int* pi = arr;//一维数组名是数组首元素地址
	printf("%d\n", *(pi + 1));//对pi+1进行解引用
	printf("%d\n", pi[1]);
	printf("%d\n", *(pi + 4));//对pi+4进行解引用
	printf("%d\n", pi[4]);
	return 0;
}

我们可以看到pi[1] 与*(pi+1)输出的是一样的结果 ,pi[4] 与 *(pi+4)输出的是一样的结果

事实上,pi[i]与*(pi+i)是等价的

指针-指针

我们可以先来看一个代码:

#include<stdio.h>
int main()
{
	int arr[6] = { 1,2,3,4,5,6 };
	//下标         0 1 2 3 4 5
	int* p1 = &arr[2];
	int* p2 = &arr[5];
	int ret = p2 - p1;
	printf("%d\n", ret);
	return 0;
}

我们可以看到输出结果为3,事实上,p2-p1的结果是p2-p1的值(12)(两个地址之差)除以数组元素的长度(4),我们可以理解为指针-指针的绝对值是指针之间的元素个数(序号差),前提是指针变量指向同一个数组中的元素。

结论:如果指针变量指向同一个数组中的元素,指针-指针的绝对值是指针之间的元素个数(序号差)。

我们可以利用这个来模拟实现strlen,strlen是求一个字符串的实际长度的(不包括‘\0’)遇到‘\0’就会停止。

strlen的使用

#include<stdio.h>
int main()
{
	char arr[] = "abcdef";
	printf("%d\n", strlen(arr));
	return 0;
}

模拟实现strlen

方法一:(指针-指针)

#include<stdio.h>
int my_strlen(char* p)
{
	char* start = p;
	char* end = p;//start和end最开始都是起始地址
	while (*end != '\0')//遇到'\0'停止
	{
		end++;
	}
	return end - start;
}
int main()
{
	char arr[] = "abcdef";
	printf("%d\n", my_strlen(arr));
	return 0;
}

我们可以看到运行结果是正确的。

方法二:计数

#include<stdio.h>
int My_strlen(char* p)
{
	int count = 0;
	while (*p != '\0')//遇到'\0'停止
	{
		count++;
		p++;
	}
	return count;
}
int main()
{
	char arr[] = "abcdef";
	int ret = My_strlen(arr);
	printf("字符串长度为:%d\n", ret);
	return 0;
}

指针的关系运算

指向一个一维数组的指针也可以进行比较,我们知道数组元素的地址是在内存中连续存放的,所以根据这个我们可以进行指针的关系运算

例:


//while循环实现
#include<stdio.h>
int main()
{
	int arr[6] = { 1,2,3,4,5,6 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* pi = arr;//一维数组名是首元素地址
	while (pi < arr + sz)
	{
		printf("%d ", *pi);
		pi++;
	}
	return 0;
}
//for循环实现
#include<stdio.h>
int main()
{
	int arr[6] = { 1,2,3,4,5,6 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* pi = arr;//一维数组名是首元素地址
	for (; pi < arr + sz; pi++)
	{
		printf("%d ", *pi);
	}
	return 0;
}

它们的运行结果是一样的。

接下来,我们来看看这一段代码

#include<stdio.h>
int main()
{
	int arr[6] = { 0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* pi = arr;//一维数组名是首元素地址
	while (pi < arr + sz)
	{
		scanf("%d", pi);
		pi++;
	}
	while (pi < arr + sz)
	{
		printf("%d ", *pi);
		pi++;
	}
	return 0;
}

这一段代码是想向数组输入元素,然后再进行输出

但是输入后并没有进行输出,为什么呢?

因为pi的值在第一次while循环的时候,已经加到了pi+sz,所以第二次while循环(pi<arr+sz)是不成立的,我们需要在第二次while循环前面手动将pi置为首元素地址。

正确的代码:

#include<stdio.h>
int main()
{
	int arr[6] = { 0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* pi = arr;//一维数组名是首元素地址
	while (pi < arr + sz)
	{
		scanf("%d", pi);
		pi++;
	}
	pi = &arr[0];//或者pi=arr;
	while (pi < arr + sz)
	{
		printf("%d ", *pi);
		pi++;
	}
	return 0;
}

所以在使用指针的时候,我们需要特别注意指针指向的对象。

野指针

概念

什么是野指针呢?
野指针是) 指针指向的位置是不可知的(随机的、不正确的、没有明确限制的) ,我们通过野指针的成因来更好地理解野指针的概念。

成因

指针未初始化

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

上面这个代码定义了一个指针变量p,但是没有对指针变量进行赋初值,这就会导致野指针,编译器也会报错。

所以使用指针我们需要初始化避免野指针,如果我们不知道指针应该指向哪⾥,可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。

#include<stdio.h>
int main()
{
	int a = 10;
	int* p1;
	p1 = &a;
	//int*p1=&a;
    int* p2=NULL;
	*p1 = 20;
	printf("%d\n", a);
	return 0;
}

指针越界访问

#include <stdio.h>
int main()
{
	int arr[6] = { 0 };
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i < 8; i++)
	{
		//当指针指向的范围超出数组arr的范围时,p就是野指针
		*(p++) = i;
		//p先使用再++,先进行解引用,将i赋给*p,p再++
	}
	return 0;
}

8显然大于数组arr的范围,这时候p就是野指针。

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

正确使用:

#include <stdio.h>
int main()
{
	int arr[6] = { 0 };
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i < 6; i++)
	{
		//当指针指向的范围超出数组arr的范围时,p就是野指针
		*(p++) = i;
		//p先使用再++,先进行解引用,将i赋给*p,p再++
	}
	p = arr;//p前面已经加到p+6
	for (i = 0; i < 6; i++)
	{
		printf("%d ", *(p++));
	}
	return 0;
}

指针指向的空间释放

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

这里一个test函数返回a的地址,但是结合前面的知识我们知道a是一个局部变量,当调用函数结束的时候,就会被释放,所以这个时候a的地址存放的内容就是不确定的了,p就是野指针。编译器就会报出警告。

避免野指针

常见方法

1.指针初始化
2.避免指针越界访问
3.避免返回局部变量的地址
4.我们还可以当指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性, 只要是NULL指针就不去访问, 同时使⽤指针之前可以判断指针是否为NULL。

assert断言

作用及使用
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报
错终⽌运⾏,这个宏常常被称为“断⾔。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣
任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误
stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。
#include<stdio.h>
#include<assert.h>//assert头文件
int main()
{
	int* p = NULL;
	assert(p != NULL);//验证变量 p 是否等于 NULL
	return 0;
}

我们可以看到指针变量p是等于NULL的,所以会在在标准错误stderr (屏幕)中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。

优点

1.能⾃动标识⽂件和出问题的⾏号,帮助程序员修改代码

2.有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。

如果已经确认程序没有问 题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG , 重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。

#include<stdio.h>
#define NDEBUG//不需要再做断言
#include<assert.h>//assert头文件
int main()
{
	int a = 10;
	int* p = &a;
	assert(p != NULL);//验证变量 p 是否等于 NULL
	printf("%d\n", *p);
	return 0;
}

自己调试会发现当前面有#define NDEBUG,就会跳过assert(p != NULL);这条语句。

如果程序⼜出现问题,可以移除这条 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启用assert() 句。

缺点
因为引⼊了额外的检查,增加了程序的运⾏时间。
使用注意

⼀般我们可以在 Debug 中使用,在 Release 版本中选择禁⽤ assert 执行。

VS 这样的集成开发环境中,在 Release 版本中,直接就是优化掉了。 这样在Debug版本写有利于程序员排查问题, Release 版本不影响用户使⽤时程序的效率。

  • 36
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值