有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你。
⽣活中,每个房间有了房间号,就能提⾼效率,能快速的找到房间。
其中,每个内存单元,相当于⼀个学⽣宿舍,一个字节空间里面能放8个比特位,就好比同学们 住的八人间,每个人是⼀个比特位。 每个内存单元也都有⼀个编号(这个编号就相当 于宿舍房间的门牌号),有了这个内存单元的编 号,CPU就可以快速找到一个内存空间。
硬件编址也是如此:
首先,必须理解,计算机内是有很多的硬件单元,而硬件单元是要互相协同工作的。所谓的协 同,至少相互之间要能够进行数据传递。但是硬件与硬件之间是互相独立的,那么如何通信呢?答案很简单,用"线"连起来。 而CPU和内存之间也是有大量的数据交互的,所以,两者必须也⽤线连起来。不过,我们今天关心⼀组线,叫做地址总线。
理解了内存和地址的关系,我们再回到C语言,在C语言中创建变量其实就是向内存申请空间,比如:
那我们如何能得到a的地址呢?这里就得学习⼀个操作符(&)-取地址操作符
int main()
{
int a = 0x11223344;
printf("%p\n", &a);
return 0;
}
虽然整型变量占用4个字节,我们只要知道了第⼀个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。
int main()
{
int a = 0x11223344;
int* pa = &a;
*pa = 0;
return 0;
}
指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。
我们看到pa的类型是 int* ,我们该如何理解指针的类型呢?
这里pa左边写的是 int* , * 是在说明pa是指针变量,而前面的int是在说明pa指向的是整型(int)类型的对象。
那如果有⼀个char类型的变量ch,ch的地址,要放在什么类型的指针变量中呢?
int main()
{
int a = 0x11223344;
int* pa = &a;
*pa = 0;
return 0;
}
int main()
{
int a = 0x11223344;
char* pa = &a;
*pa = 0;
return 0;
}
上面代码中,*pa 的意思就是通过pa中存放的地址,找到指向的空间,*pa其实就是a变量了;所以*pa = 0,这个操作符是把a改成了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;
}
对比下面2段代码,主要在调试时观察内存的变化。
//代码一
int main()
{
int a = 0x11223344;
int* pa = &a;
*pa = 0;
return 0;
}
//代码二
int main()
{
int a = 0x11223344;
char* pa = &a;
*pa = 0;
return 0;
}
先看⼀段代码,调试观察地址的变化。
#include
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
代码运行的结果如下:
我们可以看出,char* 类型的指针变量+1跳过1个字节,int* 类型的指针变量+1跳过了4个字节。 这就是指针变量的类型差异带来的变化。 结论:指针的类型决定了指针向前或者向后走一步有多大(距离)。
int main()
{
int a = 10;
char ch = 'w';
int* pa = &a;
char* pc = &a;//warning
void* pv = &a;//int*
void* pvc = &ch;//char*
//*pv = 20;//err -> void*类型的指针不能直接进行解引用操作
//pv++;//err -> void*类型的指针也不能进行+-1的操作
return 0;
}
这里我们可以看到, void* 类型的指针可以接收不同类型的地址,但是无法直接进行指针运算。
int main()
{
const int a = 10;//a不能被修改了,但是a的本质还是变量,const仅仅在语法上做了限制
//所以我们习惯称a为常变量
//a = 20;//修改-err
printf("%d\n", a);
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;
}
在此之前,我们要明确一下 p和*p之间的联系:
①p里边存放的是地址 (a的地址)
②p是变量,有自己的地址
③*p 是p指向的空间
int main()
{
const int a = 10;
//危险的代码
//int* pa = &a;
//*pa = 0;
//printf("%d\n", a);
//const可以用来修饰指针
const int* pa = &a;
//const修饰指针的时候,const可以放在*的左边也可以放在指针的右边
//*pa = 0;
printf("%d\n", a);
return 0;
}
int main()
{
const int a = 10;
const int* pa = &a;//等价于int const* pa = &a;const限制的是*pa
//*pa = 0;//err
int* const p = &a;//const限制的是p
*p = 0;
printf("a = %d\n", a);
return 0;
}
结论:const修饰指针变量的时候
• 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
int main()
{
//指针-指针 = 地址-地址
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", &arr[0] - &arr[9]);//9 - 指针-指针的绝对值是指针和指针之间的元素个数
//指针-指针运算的前提条件是:两个指针指向同一块空间
char ch[20] = { 0 };
printf("%d\n", &ch[0] - &arr[0]);//err
return 0;
}
int my_strlen(char* s)
{
int count = 0;
while (*s != '\0')
{
count++;
s++;
}
return count;
}
int main()
{
//strlen - 求字符串的长度 - 统计的是\0前面出现的字符的个数
int len = my_strlen("abc");//传入的是字符串的首地址
printf("%d\n", len);
return 0;
}
int my_strlen(char* s)
{
char* start = s;
while (*s != '\0')//'\0'的ASCII码值是0
{
s++;
}
return s - start;//指针-指针
}
int main()
{
//strlen - 求字符串的长度 - 统计的是\0前面出现的字符的个数
int len = my_strlen("abc");//传入的是字符串的首地址
printf("%d\n", len);
return 0;
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
//使用while循环打印arr的内容
int* p = &arr[0];
//arr是数组名,数组名其实是数组首元素地址,arr<==>&arr[0]
while (p < arr + sz)
{
printf("%d ", *p);
p++;
}
return 0;
}
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
1. 指针未初始化
int main()
{
//局部变量如果不初始化,变量的值是随机的
//全局变量如果不初始化,变量的值默认是0 - 静态区
//静态变量如果不初始化,变量的值默认是0 - 静态区
//存放在静态区的变量,如果不初始化,默认值是0
int* ptr;//野指针
*ptr = 20;//非法访问内存
return 0;
}
2. 指针越界访问
#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;
}
3. 指针指向的空间释放
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* ptr = test();
printf("hehe!\n");//为什么这里加了一句代码,*ptr的值就变了
printf("%d\n", *ptr);
return 0;
}
初始化如下:
#include
int main()
{
int num = 10;
int*p1 = #
int*p2 = NULL;
return 0;
}
int main()
{
int arr[10] = {1,2,3,4,5,67,7,8,9,10};
int *p = &arr[0];
for(i=0; i<10; i++)
{
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使⽤的时候,判断p不为NULL的时候再使⽤
//...
p = &arr[0];//重新让p获得地址
if(p != NULL) //判断
{
//...
}
return 0;
}
不要返回局部变量的地址。
assert.h 头⽂件定义了宏 assert() ,⽤于在运行时确保程序符合指定条件,如果不符合,就报错终止运⾏。这个宏常常被称为“断⾔”。
然后,重新编译程序,编译器就会禁用文件中所有的 assert() 语句。如果程序⼜出现问题,可以移 除这条 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启用了 assert() 语句。
assert() 的缺点是,因为引入了额外的检查,增加了程序的运行时间。
⼀般我们可以在 Debug 中使用,在 Release 版本中选择禁用 assert 就行,在 VS 这样的集成开发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在 Release 版本不影响用户使用时程序的效率。