目录
C语言中引入指针的概念,其作用是用来访问内存,今天就来详细地了解一下指针的相关知识。
在介绍指针变量之前,我们得明确,数据都是记录在一个地址上的,不管数据如何变化,其地址是一成不变的。所以就算指针存储了一个变量的地址,改变指针的值,只能说明指针指向了别的地址上的数据,不能说明原本变量的地址发生变化。
指针变量:
1.指针的定义
计算机中处理数据时,得先去通过地址总线去寻找数据,这就得引入地址的概念。地址可以抽象成数据存放的位置。而指针就是用来存放地址。
1.1指针变量的申明
在C语言中 ,指针变量 = 指针指向的类型 + 变量名。如下:
int* a float* b double * c char* d
1.2指针变量的初始化
1、直接初始化
int* a = NULL; int* a = 0x11223344
2、指向某一变量(常用)
int a = 10; int* p = &a;
&是取址符号,就是取a的地址存储给p
2.指针变量的大小
指针变量就是用来存放数据的,但是指针变量的大小与指针指向的数据类型无关。
列如下面两种指针变量在同个操作系统,指针变量大小都是相同的。
int* a char& s;
指针变量大小取决于地址线的个数。
结论:32位环境中=下指针变量大小为4个字节;64位环境下为8个字节。
3.指针变量类型的意义
3.1指针的解引用
指针变量想通过地址找到其地址对应的数据得通过解引用 " * " 符号。
例如:
int a = 10; int* p = &a; int b = *p;
执行完后 b = 10
但是如果指针类型和指向的数据类型不匹配的话会出现错误。
如下面代码
int a = 123123;
char* p = &a;
*p = 0;
printf("%d", a);
输出结果为
122880
因为指针类型决定了指针进行解引用操作符的时候访问几个字节,也就是决定指针的权限!
所以我们在使用指针时,类型一定要相互匹配。
3.2void* 指针
4.const 修饰指针
4.1const 修饰变量
如果将一个变量前 + const 那么就给这个变量赋予了常属性,可以认为是常变量(本质还是变量但是无法直接修改变量的值)
如下面这段报错代码:
const int a = 10;
a = 2;
但是我们依然有办法去修改常变量的值,那就是借助指针去访问地址来进行修改。
如下面这段代码:
const int a = 10;
int* p = &a;
*p = 1;
但是这么做就破坏了想将 a 变成常量的做法。所以我们采用了const + 指针的用法。
4.2const 修饰指针变量
先说结论:const修饰指针变量的时候
1、const如果放在*的左边const int* p(int const* p),修饰的是指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。
2、const如果放在*的右边int* const p,修饰的是指针变量的本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
我们分成两种类型的const修饰指针进行展开解释
1.const int* p (int * const p)
int a = 10;
int b = 20;
const int* p = &a;
*p = 20; // error报错
p = &b; // 正常运行 p重新指向变量b,存储变量b的地址
2.int * const p
int a = 10;
int b = 20;
int * const p = &a;
*p = 20; // 正常运行,将a 的值变为20
p = &b; // error报错
5.指针运算
5.1指针 +- 整数
看下面这段代码
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
printf("pa = %p \n", pa);
printf("pa + 1 = %p \n", pa + 1);
printf("pc = %p\n", pc);
printf("pc + 1 = %p\n", pc + 1);
return 0;
}
执行结果为
pa = 00AFFAB4
pa + 1 = 00AFFAB8
pc = 00AFFAB4
pc + 1 = 00AFFAB5
我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。
公式:+/- 整数 --> 向左/右跳过 1 * sizeof(type) 个字节
结论:指针的类型决定了指针向前或者向后走一步有多大(距离),可以和数组的下标所联系在一起。意义便是当指针指向一个数组时,可以直接通过指针+整数的形式访问对应的下标数据。
下面就是用指针代替下标遍历数组的代码:
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *p = &arr[0];
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("%d ", *p);
p++;
}
执行结果:
1 2 3 4 5 6 7 8 9 10
关于指针解引用和前置后置++符号的综合运用可以参考下面文章
5.2 指针 - 指针
前提条件:两个指针指向了同一个空间(比如说数组)这样才有意义。
指针 - 指针 可以类比成 日期 - 日期 得到的是一个数,而这个数就是两个指针的地址差值。
5.3指针的关系运算
//指针的关系运算
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
while(p<arr+sz) //指针的⼤⼩⽐较
{
printf("%d ", *p);
p++;
}
return 0;
}
6.野指针
6.1野指针的成因
6.1.1.指针未初始化
不合法:
#include <stdio.h>
int main()
{
int *p; //局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
合法:
#include <stdio.h>
int main()
{
int a = 10;
int* p = &a;
*p = 20;
return 0;
}
6.1.2.指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* p = &arr[0]; // p 此时不是野指针
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i <= sz; i++) // i = sz 时越界访问
{
printf("%d ", *p);
p++;
}
return 0;
}
3.指针指向的空间释放
示例一:动态分配内存后释放
#include <stdio.h>
int main()
{
int* p = (int*)malloc(sizeof(int));
p[0] = 10;
printf("%d\n", *p);
free(p);
printf("%d\n", *p);
return 0;
}
运行结果为:
10
-572662307
示例二:函数栈帧创建和销毁
#include <stdio.h>
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
但是由于函数栈帧销毁只是进行指针的移动,并不是直接释放空间,所以仍然可以通过*p访问。
6.2如何避免野指针
6.2.1.指针初始化
要么直接将指针指向一个变量,要么将指针赋值为NULL ((void *)0)。如下面代码:
#include <stdio.h>
int main()
{
int a = 10;
int* p1 = &a;
int* p2 = NULL;
return 0;
}
但是需要注意的是如果赋值为 NULL 时,不可以直接解引用。只为 0 其实也是一个地址,但是计算机规定一个区域是给系统内核的,无法直接访问。
6.2.2.小心指针越界
明确一个程序向内存申请了多大的空间,避免指针越界,越界访问。
6.2.3.指针不用置NULL
7.assert断言
#include <stdio.h>
#include <assert.h>
int main()
{
int* p = NULL;
assert(p != NULL);
return 0;
}
执行结果:
7.1.assert优点
1. assert 出现错误的时候,直接会报错,指明在什么文件,哪一行,便于程序员直接找到错误的地方
2、如果已排查后,发现无错误,可以直接关闭 assert() 机制。只需要在assert头文件前加上一个宏 #define NDEBUG 就可以一键关闭。
7.2.assert缺点
引入了额外的检查,增加了程序的运行时间
一般我们在Debug版本中使用assert,在Release版本中选择禁用assert,在VS这样的集成开发环境中,在Release版本中,直接就是优化掉了。这样在Debug版本写有利于程序员排查问题,Release版本不影响用户使用时程序的效率。
8.指针的使用和传址调用
8.1.传值和传址
这里借助两个数值交换的函数功能进行阐述。
下面是普通的两数值交换的代码:
#include <stdio.h>
int main()
{
int a = 10, b = 20;
int tmp = a;
a = b;
b = tmp;
return 0;
}
而如果想采用函数实现该功能的话,普通的传参是满足不了的例如
#include <stdio.h>
void swap(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 10, b = 20;
swap(a, b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
执行结果为
a = 10, b = 20
由此可知普通传参是无法实现的
因为实参传递给形参,形参只是实参的一个临时拷贝,对形参的修改不会影响到实参。
下面是借助地址,传给函数进行两数值交换:
#include <stdio.h>
void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main()
{
int a = 10, b = 20;
swap(&a, &b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
执行结果为:
a = 20, b = 10
由此可知如果采用传址调用可以完成该功能。在函数中只需要解引用就可以远程的找到传入地址所对应的数据。
8.2.传值&传址
那么我们什么时候采用传值传参,什么时候采用传址传参。
1、传值调用
小结:
今天我们了解到了指针的基本用法,取址操作和解引用操作,了解了指针的基本运算规则,和使用 指针时的注意事项,以及使用习惯。也说明了指针在函数传参时的用法。
之后会出对指针的不同类型进行区分以及运用。
如果这篇文章对你有帮助的话,不妨点个免费的赞和关注,谢谢支持!