目录
(3).指针变量不再使用时候,及时置为NULL;使用之前检查指针有效性:
1.指针变量
在C语言中,创建变量要向内存申请空间.比如在学习scanf输入函数的时候其实就已经接触到指针了
下面两组代码是一样的效果
int a = 0;
scanf("%d", &a);
int a = 0;
int* p = &a;
scanf("%d", p);
&操作符需要取出变量的地址,但是在语句中,我们只是将其作为一个占位符,没有将它放到具体的变量中(没有将地址存放到变量中),如果后续我们要用到取出的地址,就要将地址存放一定起来,如何存放呢?当然就是存放在指针中,指针也是一种变量,用来专门存放地址的一种变量,存放在指针中的值都会理解为地址
I.内存和地址(补充):
- 计算机CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据,处理后的数据也会放回内存中.那这些空间是如何高效的管理呢?
- 其实就是把内存划分为一个个的内存单元,每个单元的大小取1个字节(补充计算机常见单位),一个字节可以放8个比特位,一个比特位可以存储一个二进制的1或者0. 为了方便查找数据,每个内存单元都有一个编号(就像门牌号),由这些0或1组成内存单元的编号,但是一般不以二进制(0/1)显示,以十六进制显示.有了这些内存单元的编号,CPU就可以快速的找到一个内存空间.在计算机中把这些内存的编号成为地址(就像生活中把门牌号叫做地址).C语言给地址起了新的名字:指针.利用指针,程序可以轻松地处理复杂的数据结构和动态分配的内存空间,提高了程序的灵活性和效率.
所以可以这样理解:内存单元==地址==指针
II.编址(补充):
- 那计算机是如何给内存中的每个内存单元分配唯一的地址的呢?这就不得不提到编址,这里做一个简单的介绍,当我们了解编址是给每个内存单元分配唯一地址的过程后,就可以更好的理解指针是如何工作的.
- CPU访问内存的某个字节空间,必须知道这个字节空间在内存的什么位置,因而内存中字节很多,所以要给内存编址,但是要注意的是,计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的.
- 硬件与硬件之间是靠线连接起来的,在CPU和内存之间也一样,两者要用线连起来.简单的理解编址,知道地址总线即可,对于不同位数的机器,可以这样理解,32位机器有32根地址总线,每根线只有两种态,表示0,1(电脉冲有无),那么一根线,就能表示2中含义,2根线4中含义.......32根地址线,就可以表示2^32种含义,每种含义代表一个地址.地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据通过数据总线传入CPU寄存器
III.二级指针
既然指针是变量,那指针也有地址,那指针的地址存放在哪呢?没错,就是二级指针
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
int** ppa = &pa;
return 0;
}
- *ppa通过对ppa中的地址解引用,这样找到的是pa,*ppa其实访问的就是pa
- **ppa先通过*ppa找到pa,然后对pa进行解引用操作:*pa,找到的就是a
- *ppa==pa **ppa==*(*ppa)==*pa==a
2.指针操作符
I.取地址符(&)
取地址符用于取出变量的地址,通过取地址符,可以将变量的地址赋值给指针变量.
int x=10;
int *ptr=&x;//ptr指向x的地址
II.解引用操作符(*)
解引用操作符用于访问指针所指向地址的值.通过解引用操作符,可以获取指针所指向的地址上储存的值.
int y=*ptr;//y的值为10,因为ptr指向变量x,*ptr获取x的值
III.成员访问操作符(->)
成员访问操作符用于通过指针访问结构体或类的成员.通过这个操作符,可以访问到其中的成员变量.
structure Person{
char name[20];
int age;
};
struct Person person;
struct Person *ptr = &person;
ptr->age=25//通过指针ptr访问结构体成员age
IV.下标访问操作符([])
下标操作符主要是用于通过指针访问数组元素.通过指针和下标操作符,可以访问数组中特定的元素
3.指针变量类型
I.字符指针变量
int main()
{
char ch = 'w';
char *pc = &ch;//取出ch的地址,指向ch的地址
*pc = 'w';//解引用获取w
return 0;
}
和字符相关的指针,这是个容易让人误解的代码:
int main()
{
const char* pstr = "hello bit.";//这⾥是把⼀个字符串放到pstr指针变量⾥了吗?
printf("%s\n", pstr);
return 0;
}
pstr事实上只是储存了"hello bit"中的首元素的地址,使用%s打印的时候,会往后一直打印知道遇到字符串结束符为止.
II.数组指针变量
类比的思想:
int *p[10];
int arr[10] = {0};
&arr;//得到的就是数组的地址
int(*p)[10] = &arr;//只初始化了一个
注意:
- 数组名就是首元素的地址,所以在代码中,p[0]就等于arr,但是需要注意两点数组名不是首元素的地址
-
sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
-
&数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的,后面讲指针运算会说到)
III.函数指针变量
当创建一个函数的时候,函数有没有地址呢?写一个简单的代码测试:
int Add(int x, int y)
{
return (x + y);
}
int main()
{
Add(3, 4);
printf("&Add==%p\n", &Add);
printf(" Add==%p\n", Add);
return 0;
}
通过调试代码可以看出:
函数也有地址,函数名就是函数的地址,当然也可以通过&函数名的方式获得函数的地址,同理函数的地址如果需要存放起来的话,就要用到函数指针变量.在调试窗口中,可以看到&Add的类型是int(*)(int,int),上面说到了去掉名字就是类型名,这里也是如此,函数指针的写法和数字指针非常相似,使用如下:
void test()
{
printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;
int Add(int x, int y)
{
return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的
int(*pf3)(int x,int y);
可以通过函数指针调用所指向的函数 :
#include <stdio.h>
int Add(int x, int y)
{
return x+y;
}
int main()
{
int(*pf3)(int, int) = Add;
printf("%d\n", (*pf3)(2, 3));
printf("%d\n", pf3(3, 5));
return 0;
}
结果:
IV.void型指针
在指针类型中有一种特殊的类型是void*类型的,可以理解为无具体类型的 指针(或者叫泛型指针),这种类型的解引用可以用来接收任意类型的地址.但也有局限性,void*类型的指针不能直接进行指针的+-整数和解引用的运算(要使用需进行类型转换).
#include <stdio.h>
int main()
{
int a = 10;
void* pa = &a;
*pa = 10;//这样的写法是错误的,如要使用pa,需要将其强制转换-->*(int*)pa
return 0;
}
- void*类型的指针,可以接受不同类型的地址,但是无法进行指针运算.
- 一般void*类型的指针是使用在函数参数部分,用来接收不同数据类型的地址.
4.野指针(悬空指针)
野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)
I.野指针成因:
-
(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(); //虽然能正常打印100,是因为在程序运行过程中,释放的内存还没有被覆盖,所以数据还保留在那个位置 printf("%d\n", *p); return 0; }
II.规避野指针
-
(1).初始化:
#include<stdio.h> int main() { int num=10; int *p1=# int *p2=NULL; return 0; }
-
(2).注意指针越界:
一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问.
-
(3).指针变量不再使用时候,及时置为NULL;使用之前检查指针有效性:
当指针变量指向一块区域的时候,我们通过指针访问该区域,后期不再使用这个指针访问空间的时候,我么可以把该指针置为NULL.
-
(4).避免返回局部变量地址:
如造成野指针的第三个例子,不要返回局部变量的地址.
5.Assert断言
assert.h头文件中定义了宏assert(),用于在运行时确保符合指定条件,如果不符合,就报错终止运行.这个宏常常被称为"断言".
assert(p!=NULL);
上面代码在程序运行到这行语句时,验证变量p是否等于NULL.如果不等于NULL;程序继续运行,否则就终止运行,并给出报错信息提示.
assert宏接受一个表达式作为参数.如果该表达式为真(返回值非零),assert()不会产生任何作用,程序继续运行.如果表达式为假(返回值为零),assert()就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号.
assert的好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭的assert()机制.如果已经确认程序没有任何问题,不需要做断言,就在#include<assert.h>的前面,定义一个宏DEBUG.
#define DEBEG
#include<assert.h>
6.Const修饰指针
变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以修改这个变量.但是如果希望一个变量加上一些限制,不能被修改,怎么做呢?这就是const的作用.
#include<stdio.h>
int main()
{
int m=0;
m=20;//m是可以修改的
const int n=0;
n=20;//n是不能被修改的
return 0;
}
上述代码中的n是不可以被修改的,其中n本质上是变量,只不过被const修饰后,在语法上加上了限制,只要对n进行修改,就不符合语法规则,就报错,致使无法直接修改n.
但是如果绕过n,使用n的地址去修改n就能做到,虽然这样是在打破语法规则
#include<stdio.h>
int main()
{
const int n=0;
printf("n=%d\n",n);
int *p=&n;
*p=20;
printf("n=%d",n);
return 0;
}
这样绕过n,通过利用地址的方式可以修改n的值,那有没有一种办法,能够让拿到地址也不能修改(就像上面的给变量加上const一样)呢?
void test1()
{
int n = 10;
int m = 20;
int *p = &n;//ok?
p = &m;//ok?
}
void test2()
{
int n = 10;
int m = 20;
const int* p = &n;
*p = 20;//ok?
p = &m;//ok?
}
void test3()
{
int n = 10;
int m = 20;
int* const p = &n;
*p = 20;//ok?
p = &m;
}
void test4()
{
int n = 10;
int m = 20;
int const * const p = &n;
*p = 20;//ok?
p = &n;//ok?
}
int main()
{
//测试无const修饰的情况
test1();
//测试const放在*左边的情况
test2();
//测试const放在*右边的情况
test3();
//测试*的左右两边都有const
test4();
}
在test1函数中,变量m,n和指针p都没有被const修饰,可以对它们的值进行修改,指针p可以指向n,也可以指向m.
test2函数将const放在*的左边,此时如果对指针p指向的内容进行修改,就会报错.
test3函数将const放在*的右边,此时如果改变指针变量p的内容,就会报错.
test4函数中,*的左边右边都有const修饰.,既不可以对p指向的内容进行修改,也不可以修改指针变量p的内容.
- 总结:
const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变.但是指针变量本身的内容可变. - const如果放在*的右边,修饰的是指针变量本身,保证指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变.
如果将代码放到VSstudio2022中,可以很好地看出问题:
7.指针大小
在C语言中,指针的大小取决于计算机架构和操作系统.通常情况下,指针的大小和计算机的处理器位数相关.
• 32位平台下地址是32个bit位,指针变量大小是4个字节
C语言中,指针的大小主要受到底层计算机硬件和操作系统的限制,不受指向的变量类型的影响.(一个整型变量的指针和一个字符型变量的指针,它们在内存中占用的空间大小是一样的(同一环境下)
8.指针运算
I.指针+-整数
这里的内容借助数组来说明,因为数组在内存中是连续存放的,只要知道第一个元素的地址,顺着就可以找到后面的所有元素.
先来看这样的一段代码:
#include<stdio.h>
int main()
{
int n = 10;
char* pc = (char*)&n;//pc的类型是字符类型
int* pi = &n;//pi的类型是整型的
printf("%p\n", &n);
printf("%p\n", &pc);
printf("%p\n", &pc+1);
printf("%p\n", &pi);
printf("%p\n", &pi+1);
}
运行结果:
从结果可看出,指针变量类型的差异带来的变化是:
- 指针的类型决定了指针向前或者向后走一步有多大(距离)
- 指针类型决定了对指针的解引用的时候有多大的权限(一次能操作几个字节)
比如char*的指针解引用就只能访问一个字节,int*的指针解引用就能访问四个字节.
开始提到的数组就可以利用指针的运算进行访问:
#include <stdio.h>
int main() {
int array[] = {10, 20, 30, 40, 50};
int *ptr = array; // 指向数组第一个元素的指针
// 使用指针进行数组元素的遍历和访问
for (int i = 0; i < 5; i++) {
printf("Element %d: %d\n", i, *ptr);
ptr++; // 指针后移,访问下一个元素
}
// 使用指针进行倒序遍历和访问数组元素
ptr = &array[4]; // 指向数组最后一个元素的指针
for (int i = 4; i >= 0; i--) {
printf("Element %d: %d\n", i, *ptr);
ptr--; // 指针前移,访问前一个元素
}
return 0;
}
在这里解释一下前面说到的 整个数组的地址和数组首元素的地址的区别:
#include <stdio.h>
int main() {
int arr[5] = { 1, 2, 3, 4, 5 };
int(* ptr_arr)[5] = &arr; // 指向整个数组的指针
int(*ptr_first)[1] = &arr[0]; // 指向数组首元素的指针
// 输出整个数组的地址和数组首元素的地址
printf("整个数组的地址:%p\n", (void*)ptr_arr);
printf("数组首元素的地址:%p\n", (void*)ptr_first);
// 对指向整个数组的指针执行+1操作
ptr_arr = ptr_arr + 1;
// 对指向数组首元素的指针执行+1操作
ptr_first = ptr_first + 1;
// 输出移动后的地址
printf("数组首元素的地址+1:%p\n", (void*)ptr_first);
printf("整个数组的地址+1:%p\n", (void*)ptr_arr);
return 0;
}
II.指针的关系运算
#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;
}
arr+sz表示的是arr中第sz个元素的下一个的地址,通过比较指针的方式遍历数组将其打印