理解内存、地址与指针之间的挂关系
我们假设这里有一栋宿舍楼,楼里有很多个房间,每个房间都有自己的门牌号,每个房间中又会有多个床位。你的朋友正在这栋宿舍楼中的某个房间的某个床位上等你,那么你必须要做的就是知道你朋友的门牌号这样就可以快速的找到你的朋友,把上面的例子对照到计算机中,又是怎样的呢?
我们知道CPU在处理数据的时候,这些数据都是在内存中读取的,处理后的数据也会返回到内存中,而我们买电脑的时候,电脑上的内存是8GB/16GB/32GB等,那这些内存空间如何⾼效的管理呢?
其实就是将内存划分为一个个的内存单元,每个内存单元的大小取一个字节,这里的每个内存单元都相当于一个房间,内存单元中的八个比特位就相当于房间里的八个床位,既然每个内存单元都可以看作是是一个房间,那么这个内存单元就应该有它的门牌号,有了这个门牌号CPU就可以快速找到它所对应的内存空间。在计算机中我们把内存单元的编号(门牌号)称为地址,C语言中给存储地址工具的起了个新名字:指针
编址与寻址(简单理解)
编址:
存储器是由一个个存储单元构成的,为了对存储器进行有效的管理,就需要对各个存储单元编上号,即给每个单元赋予一个地址码,这叫编址。经编址后,存储器在逻辑上便形成一个线性地址空间。
寻址:
取地址操作符&
#include <stdio.h> int main() { int a = 10; return 0; }
在c语言中我们创建变量的过程其实就是在向内存申请一片内存空间,以int a = 10为例,我们可以看到a向内存申请了四个字节用于存放整数10:
0x000000ABECAFF7C4
0x000000ABECAFF7C5
0x000000ABECAFF7C6
0x000000ABECAFF7C7
通过取地址操作符得到整型变量a的地址:
#include <stdio.h> int main() { int a = 10; printf("%p\n", &a); return 0; }
我们发现只取出了一个地址,这是因为取地址操作符获取地址时规定了只获取申请的最小地址
解引用操作符*
上面我们通过&拿到a的地址在内存空间中申请的最低的地址:0x000000ABECAFF7C4 ,这个数值(地址)有时候也是需要存储起来⽅便后期再使⽤的,我们将它存储在指针变量中。
#include <stdio.h> int main() { int a = 10; int* pa = &a;//取出a的地址并存储到指针变量pa中 return 0; }
指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址
通过*我们就可以改变a在内存空间中存储的值:
#include <stdio.h> int main() { int a = 100; int* pa = &a; //指针变量存储整型变量a的地址 *pa = 20; //通过对指针变量的解引用可以修改整型变量a内存空间中存储的值 printf("%d",a); return 0; }
指针变量的大小
32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。指针变量是⽤来存放地址的,那么指针变的⼤⼩就得是4个字节的空间才可以。
同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变的⼤⼩就是8个字节。
#include <stdio.h> //指针变量的⼤⼩取决于地址的⼤⼩ //32位平台下地址是32个bit位(即4个字节) //64位平台下地址是64个bit位(即8个字节) int main() { printf("%zd\n", sizeof(char *)); printf("%zd\n", sizeof(short *)); printf("%zd\n", sizeof(int *)); printf("%zd\n", sizeof(double *)); return 0; }
结论:
• 32位平台下地址是32个bit位,指针变量⼤⼩是4个字节
• 64位平台下地址是64个bit位,指针变量⼤⼩是8个字节
• 指针变量的⼤⼩和类型⽆关,只要是指针类型的变量,同平台下,⼤⼩相同
指针变量类型的意义
指针变量的⼤⼩和类型⽆关,只要是指针变量,在同⼀个平台下,⼤⼩都是⼀样的,为什么还要有各种各样的指针类型呢?
//代码一 #include <stdio.h> int main() { int n = 0x11223344; int *pi = &n; *pi = 0; return 0; } //代码二 #include <stdio.h> int main() { int n = 0x11223344; char *pc = (char *)&n; *pc = 0; return 0; }
调试后我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节改为0
结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)
#include <stdio.h> int main() { int n = 10; char* pc = (char*)&n; int* pi = &n; printf("&n = %p\n", &n); printf("pc = %p\n", pc); printf("pc+1 = %p\n", pc + 1); printf("pi = %p\n", pi); printf("pi+1 = %p\n", pi + 1); return 0; }
我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。 这就是指针变量的类型差异带来的变化
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤
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可以吗?(虽然这样其实并不符合语法规则)
#include <stdio.h> int main() { const int n = 0; printf("n = %d\n", n); int*p = &n; *p = 20; printf("n = %d\n", n); return 0; }
我们可以看到这⾥n被修改了,但是我们还是要思考⼀下,为什么n要被const修饰呢?就是为了不能被修改,如果p拿到n的地址就能修改n,这样就打破了const的限制,这是不合理的,所以应该让p拿到n的地址也不能修改n,那接下来怎么做呢?
const修饰指针变量
#include <stdio.h> 测试一 //void test1() //{ // int n = 10; // int m = 20; // int* p = &n; // *p = 20; //ok // p = &m; //ok // printf("%d\n", m); // printf("%d\n", n); //} 测试二 //void test2() //{ // int n = 10; // int m = 20; // const int* p = &n; // *p = 20; //no // p = &m; //ok // printf("%d\n", m); // printf("%d\n", n); //} 测试三 //void test3() //{ // int n = 10; // int m = 20; // int* const p = &n; // *p = 20; //ok // p = &m; //no //} 测试四 //void test4() //{ // int n = 10; // int m = 20; // int const* const p = &n; // *p = 20; //no // p = &m; //no //} int main() { //测试⽆const修饰的情况 /*test1();*/ 测试const放在*的左边情况 /*test2();*/ 测试const放在*的右边情况 /*test3();*/ 测试*的左右两边都有const /*test4();*/ return 0; }
结论:
- const在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本⾝的内容可变
- const在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变
!!!只要位于*左侧或者右侧即可,并不要求具体位置!!!
指针运算
指针-整数
数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素
#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]); for (i = 0; i < sz; i++) { printf("%d ", *(p + i));//p+i 这⾥就是指针+整数 } return 0; } //字符类型指针也一样 #include <stdio.h> int main() { char arr[] = "abcdef"; char* pc = &arr[0]; while (*pc != '\0') { printf("%c ", *pc); pc++; } return 0; }
注意在内存监视窗口中选择不同列时左侧的地址情况是不同的:
指针-指针
//指针-指针 #include <stdio.h> int my_strlen(char *s) { char *p = s; while(*p != '\0' ) p++; return p-s; } int main() { printf("%d\n", my_strlen("abc")); return 0; }
结论:(指针-指针)=(地址-地址),且两个指针必须指向同一空间,得到的值的绝对值,是指针和指针元素之间的个数
指针的运算关系
//指针的关系运算 #include <stdio.h> int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int *p = arr; //这里的数组名就相当于数组首元素地址 int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); while(p<arr+sz) //指针的⼤⼩⽐较 { printf("%d ", *p); //p指向某个地址,通过*p拿到该地址中存储的值 p++; //p++等于地址++ } return 0; }
野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
造成野指针的情况有三种:指针未初始化、指针的越界访问、指针指向的空间释放
指针变量未初始化
#include <stdio.h> int main() { int* p;//整型的指针变量未初始化,默认为随机值 *p = 20; return 0; } //结果报错
指针的越界访问
#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; }
指针访问越界就会导致栈溢出问题
指针指向的空间释放(下面程序运行后仍能输出200)
#include <stdio.h> int* test() //因为返回的是一个地址,所以返回类型应该是int*类型 { int n = 100; return &n; } int main() { int* p = test(); //用指针变量p接收返回回来的地址 *p = 200; //n出函数释放内存空间,但是p指针仍然保存了n内存空间的地址 //这时如果再使用*p=200就会出问题,此时p就为野指针 printf("%d\n", *p); return 0; }
通俗来讲就是:相当于你今天开了个住一晚的酒店房间,但是你第二天走后告诉另一个人这个房间还可以住,你让你朋友去住那个房间,虽然这个房间你还能进去但是里面的东西已经被保洁阿姨打扫过了没有你朋友住过的痕迹了。
如何规避野指针
主要是一些具体的操作方式,涉及因为检查不仔细导致的问题不予描述
指针的初始化
#include <stdio.h> int main() { int num = 10; int*p1 = # int*p2 = NULL;//当我们还没有规定该指针指向哪里的时候,即使将该指针赋值为NULL return 0; }
assert宏(断言)
作用:确保程序符合指定条件,如果不符合,就报错终⽌运行程序
包含头文件:assert.h
使用方式:assert(表达式);
表达式为真, assert() 不会产⽣任何作⽤程序继续运⾏
表达式为假, assert() 就会报错
好处:
1、⾃动标识⽂件和问题所在⾏号
当表达式为假时,assert()会在标准错误流 stderr 中自动写⼊⼀条错误信息:显⽰没有通过的表达式,以及该表达式所在文件的⽂件名和⾏号
#include <stdio.h> #include <assert.h> int main() { int* p = NULL; assert(p != NULL); return 0; }
拥有⽆需更改代码就能开启或关闭 assert宏的机制
如果已经确认程序没有问题,不需要再做断⾔,就在 #include 语句的前⾯,定义⼀个宏 NDEBUG :
#define NDEBUG #include <assert.h>
然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移除这条 #define NDBUG 指令(或者把它注释掉),再次编译,就重新启⽤了 assert() 语句。
缺点:因为引⼊了额外的检查,增加了程序的运⾏时间
⼀般我们只在debug版本中使⽤,这样有利于程序员排查问题,如果在rekease版本使用会影响⽤⼾使⽤时程序的效率
指针的使⽤和传址调⽤
传址调用
如果要写一个交换两个整型变量的值的函数,我们可能会这样写:
#include <stdio.h> void Swap1(int x, int y) { int tmp = x; x = y; y = tmp; } int main() { int a = 0; int b = 0; scanf_s("%d %d", &a, &b); printf("交换前:a=%d b=%d\n", a, b); Swap1(a, b); printf("交换后:a=%d b=%d\n", a, b); return 0; }
但是它们并未产生实际的调用效果,调试一下看看:
我们发现a的地址是0x000000b39e3bf884,b的地址是0x000000b39e3bf8a4,在调⽤Swap1函数时,将a和b的值传递给了Swap1函数,在Swap1函数内部创建了形参x和y来接收a和b的值,但是x的地址是0x000000b39e3bf860,y的地址是0x000000b39e3bf868,x和y确实接收到了a和b的值,不过x的地址和a的地址不⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间,那么在Swap1函数内部交换x和y的值,⾃然不会影响a和b,当Swap1函数调⽤结束后回到main函数,a和b的没法交换。Swap1函数在使⽤的时候,是把变量的值传递给了函数,这种调⽤函数的方式叫做:传值调⽤
结论:形参是实参的一份临时拷贝,对形参的修改不影响实参
传值调用
那怎么办呢?
我们现在要解决的就是当调⽤Swap1函数的时候,Swap1函数内部操作的就是main函数中的a和b,直接将a和b的值交换了。那么就需要使⽤指针了,在main函数中将a和b的地址传递给Swap函数,Swap1函数⾥边通过地址间接的操作main函数中的a和b就好了:
#include <stdio.h> void Swap1(int* px, int* py) { int tmp = 0; tmp = *px; *px = *py; *py = tmp; } int main() { int a = 0; int b = 0; scanf_s("%d %d", &a, &b); printf("交换前:a=%d b=%d\n", a, b); Swap1(&a, &b); printf("交换后:a=%d b=%d\n", a, b); return 0; }
我们可以发现,此时px、py与a、b的地址就相同了,这样就可以实现在Swap1函数中直接修改a和b的值,这种函数调⽤⽅式叫:传址调⽤
~over~