目录
理解内存、地址与指针之间的挂关系
编址与寻址(简单理解)
取地址操作符&
解引用操作符*
指针变量的大小
指针变量类型的意义
const修饰指针变量
const修饰变量
const修饰指针变量
指针运算
指针-整数
指针-指针
指针的运算关系
野指针
指针变量未初始化
指针的越界访问
指针指向的空间释放(下面程序运行后仍能输出200)
如何规避野指针
指针的初始化
assert宏(断言)
指针的使⽤和传址调⽤
传址调用
传值调用
我们假设这里有一栋宿舍楼,楼里有很多个房间,每个房间都有自己的门牌号,每个房间中又会有多个床位。你的朋友正在这栋宿舍楼中的某个房间的某个床位上等你,那么你必须要做的就是知道你朋友的门牌号这样就可以快速的找到你的朋友,把上面的例子对照到计算机中,又是怎样的呢?
我们知道CPU在处理数据的时候,这些数据都是在内存中读取的,处理后的数据也会返回到内存中,而我们买电脑的时候,电脑上的内存是8GB/16GB/32GB等,那这些内存空间如何⾼效的管理呢?
编址:
存储器是由一个个存储单元构成的,为了对存储器进行有效的管理,就需要对各个存储单元编上号,即给每个单元赋予一个地址码,这叫编址。经编址后,存储器在逻辑上便形成一个线性地址空间。
寻址:
存取数据时,必须先给出地址码,再由硬件电路译码找到数据所在地址,这叫寻址
#include
int main()
{
int a = 10;
return 0;
}
在c语言中我们创建变量的过程其实就是在向内存申请一片内存空间,以int a = 10为例,我们可以看到a向内存申请了四个字节用于存放整数10:
0x000000ABECAFF7C4
0x000000ABECAFF7C5
0x000000ABECAFF7C6
0x000000ABECAFF7C7
通过取地址操作符得到整型变量a的地址:
#include
int main()
{
int a = 10;
printf("%p\n", &a);
return 0;
}
我们发现只取出了一个地址,这是因为取地址操作符获取地址时规定了只获取申请的最小地址
#include
int main()
{
int a = 10;
int* pa = &a;//取出a的地址并存储到指针变量pa中
return 0;
}
指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址
通过*我们就可以改变a在内存空间中存储的值:
#include
int main()
{
int a = 100;
int* pa = &a; //指针变量存储整型变量a的地址
*pa = 20; //通过对指针变量的解引用可以修改整型变量a内存空间中存储的值
printf("%d",a);
return 0;
}
#include
//指针变量的⼤⼩取决于地址的⼤⼩
//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
int main()
{
int n = 0x11223344;
int *pi = &n;
*pi = 0;
return 0;
}
//代码二
#include
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
*pc = 0;
return 0;
}
结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)
#include
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;
}
#include
int main()
{
int m = 0;
m = 20;//m是可以修改的
const int n = 0;
n = 20;//n是不能被修改的
return 0;
}
#include
int main()
{
const int n = 0;
printf("n = %d\n", n);
int*p = &n;
*p = 20;
printf("n = %d\n", n);
return 0;
}
#include
测试一
//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
//指针+- 整数
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
int main()
{
char arr[] = "abcdef";
char* pc = &arr[0];
while (*pc != '\0')
{
printf("%c ", *pc);
pc++;
}
return 0;
}
注意在内存监视窗口中选择不同列时左侧的地址情况是不同的:
//指针-指针
#include
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
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
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)造成野指针的情况有三种: 指针未初始化、指针的越界访问、指针指向的空间释放
#include
int main()
{
int* p;//整型的指针变量未初始化,默认为随机值
*p = 20;
return 0;
} //结果报错
#include
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;
}
指针访问越界就会导致栈溢出问题
#include
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
int main()
{
int num = 10;
int*p1 = #
int*p2 = NULL;//当我们还没有规定该指针指向哪里的时候,即使将该指针赋值为NULL
return 0;
}
表达式为真, assert() 不会产⽣任何作⽤程序继续运⾏表达式为假, assert() 就会报错
1、⾃动标识⽂件和问题所在⾏号
当表达式为假时,assert()会在标准错误流 stderr 中自动写⼊⼀条错误信息:显⽰没有通过的表达式,以及该表达式所在文件的⽂件名和⾏号
#include
#include
int main()
{
int* p = NULL;
assert(p != NULL);
return 0;
}
拥有⽆需更改代码就能开启或关闭 assert宏的机制
如果已经确认程序没有问题,不需要再做断⾔,就在 #include语句的前⾯,定义⼀个宏 NDEBUG :
#define NDEBUG
#include
然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移除这条 #define NDBUG 指令(或者把它注释掉),再次编译,就重新启⽤了 assert() 语句。
⼀般我们只在debug版本中使⽤ ,这样有利于程序员排查问题,如果在rekease版本使用会影响⽤⼾使⽤时程序的效率
如果要写一个交换两个整型变量的值的函数,我们可能会这样写:
#include
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;
}
但是它们并未产生实际的调用效果,调试一下看看:
#include
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;
}