首先要知道,内存和地址是什么?
假设学生宿舍楼,或者酒店,你去住酒店,有100个房间
房间没有编号的话你找不到你住哪一间,你的房卡要一间间的尝试
效率很低,但是如果有编号的话,那你就会很快就能找到,效率高。
相当于你告诉你同学你的宿舍号多少,他就能很快找到你所在的宿舍。
当这个例子照搬到计算机当中就是计算机上的cpu在处理数据的时候,需要的数据是在内存当中读
取的,处理了之后数据也会放回去内存里面,电脑的内存现在一般都分为8g或者16g,32g这样,
内存里面分为一个个内存单元,每个内存单元的大小取一个字节。
一个内存单元想象成一个宿舍,一个宿舍里面算一个字节的话,那么一个宿舍就能住八个人,一个人是一个比特位。
每个内存单元也有一个编号,这个编号你就理解成宿舍门牌号,有个这个内存单元的编号,cpu就可以快速找到内存空间。
在计算机当中,内存单元的编号叫做地址,而c语言当中地址的名字叫做指针
1.指针变量和地址
首先创建一个变量a
#include <stdio.h>
int main() {
int a = 10;
return 0;
}
一个整型变量在内存中会申请4个字节,取出整型变量a的地址
怎么取出,使用&a就可以取出a的地址 如图所示
#include <stdio.h>
int main(){
int a = 10 ;
&a;//取出a的地址
printf("%p ",&a);
return 0;
}
这个是直接运行出来的结果,直接就是显示a在内存当中的地址,
我们在调试当中打开内存
红色部分框住的是,整型变量a在内存当中申请的地址,一共有四个字节,其中一个字节用来存放了10,
因为在十六进制当中,a是代表是10,所以是0a,其他三个字节没有存放内容,所以是00 00 00
这个显示方式是一列,也可以显示成4列
所以,操作符&就是取地址操作符
可以看到一列一列显示的图,&a取出来的是a所占4个字节当中地址较小的字节,那么这个时候
当你打印出来一个地址之后,可以顺藤摸瓜的找到剩下的三个字节地址
1.1指针变量和操作符
上面那个整型变量a拿到了他的地址,也是一个数值,这个数值需要存储起来后期使用
就好像酒店的房卡要放在你自己的背包里面保管好,而在c语言当中这个存放的背包
叫做指针变量
上代码
int main(){
int a = 10;
int *p = &a;
return 0;
}
这个就是取出来的地址存储到指针变量pa里面去,然后指针变量也是一种变量,用来放地址的,
所以放在指针变量里面的东西都可以直接理解为地址
2.拆解指针类型
刚才pa的类型是int*,
int *pa = &a;
pa左边有int*,*是说明pa是指针变量,那么前面的int就是说明pa指向的是整形类型的对象
举一反三,如果一个char类型的变量ch,ch的地址就放在char类型的指针变量当中
char ch = 'b' ;
char * pa = &ch ;
2.1解引用操作符
只要拿到了地址,那么就可以通过这个地址找到一个具体的东西,
那么这个地址就是指针的话,那么就可以通过指针,找到指针指向的对象
上代码
这个代码就是使用了引用操作符,*pa的意思就是,通过pa中存放的地址,找到指向的空间,
那么这个*pa其实就是变量a了,当这个*pa=0,就是这个操作符把a的值改成了0
2.2指针变量的大小
32位平台下地址是32个bit位,指针变量⼤⼩是4个字节
64位平台下地址是64个bit位,指针变量⼤⼩是8个字节
注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的
3.指针的解引用
先上代码1
int main()
{
int n = 0x12345678;
int* pa = &n;
*pa = 0;
return 0;
}
这个代码就是说,先给n假定一个地址,接着取出n的地址,用整型指针pa保存起来
保存起来之后,再通过修改指针所指向的内容来修改n的值
所谓 解引用就是通过指针所指的地址,再去找到地址上存放着的内容
先看右边的内存当中,这个字节确实是变成了12345678 ,在内存当中是反着的,
所以是78 56 34 12
通过调试得知,这个代码会将n的四个字节全部改为0
看代码2
int main()
{
int n = 0x12345678;
char* pa = (char*)&n;
*pa = 0;
return 0;
}
这个代码跟上个代码解释是一样的,主要变化就是,char类型的指针去修改地址的值的时候,
只修改了一个字节,而int类型可以修改四个字节
到这一步的时候跟代码1一样,也是修改了内存当中的地址 ,下面继续看图
我们可以看到,这个char类型的指针变量只修改了一个字节,那也就意味着,char类型的指针只能访问一个字节,而int类型的指针可以访问四个字节,那么也就是说:
指针的类型决定了对指针引用的时候,有多大的权限(一次能操作多少个字节)
4.const修饰指针
const是用来修饰变量的,如果需要一个变量加上限制做到不能被修改,那就用const
上代码
这个n被const修饰之后不能被修改,
但是如果绕过n,使用n的地址,再去修改n就可以做到了
怎么使用n的地址呢,通过取地址n,给到p,
这个*p就是保存了n的地址的指针,
也就是 说,指针p所指向的内容就是n所在的地址
如果想让p拿到n的地址也无法修改,也是可以做到的
如果const放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变
但是指针变量本身的内容可变
void test1()
{
int n = 10 ;
int m = 20 ;
const int *p = &n;
*p = 20;//此时编译器会报出警告,*p无法修改
p = &m;
如果const放在*的右边,修饰的是指针本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
void test1()
{
int n = 10;
int m = 20;
int *const p = &n ;
*p =20 ;
p = &m;//此时编译器会报警告,p值无法修改
5.指针的运算
5.1指针加减整数
int arr[10]={1,2,3,4,5,6,7,8,9,10};
数组在内存当中是连续存放的,只需要知道第一个元素的地址,顺藤摸瓜的就能找到后面元素的地址
上代码
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));
}
数组名原则上会被解释为指向该数组起始元素的指针,也就是说,arr数组,表达式arr的值就与a[0]
的地址一样。假如数组元素arr的元素类型为type型,那么不管多少元素个数是多少,表达式arr的
类型就是type*型。
当指针p指向这个数组中的元素的时候,
p+i为指向该数组元素后第i个元素
p-i为指向该数组元素前第i个元素
在一个不指向任何数组元素的指针上执行算术运算会导致未定义的行为。
此外,只有在两个指针指向同一个数组时,把它们相减才有意义。
示例:
我们在调试当中可以查看到内存当中数组里面的内容是连续存放的 如图
那么数组名在什么情况下不会被视为指向起始元素的指针呢?
在下述的两种情况下,数组名不会被视为指向起始元素的指针。
1.作为sizeof运算符的操作数出现时
sizeof(数组名)不会生成指向起始元素的指针的长度,而是生成数组整体的长度。
2.作为取址运算符&的操作数时出现
&数组名不是指向起始元素的指针的指针,而是指向数组整体的指针
也就是说,指向各元素的指针 p+i 和 &a[i] 是等价的。
当然&a[i]是指向元素a[i]的指针,其实就是a[i] 的地址。
6.野指针
1.指针未初始化
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
指针初始化 如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址 会报错。
空指针 是能够和指向对象的指针明确区分的"什么都不指向"的特殊指针。
表示空指针的对象是式宏,是成为空指针常量 NULL
空指针常量NULL 在<stddrf.h>中定义。
只需要在预处理命令中包含<stdio.h> 、<stdlib.h>、<string.h>、<time.h>中任意一个头文件,就可
以处理该宏定义。
如何规避野指针
如上所述 ,将指针初始化
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#include <stdio.h>
int main()
{
int num = 10;
int*p1 = #
int*p2 = NULL;
return 0;
}
在指针变量不再使用的时候,及时置为NULL,指针使用之前检查有效性
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;
}
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是 越界访问。
7.assert断言
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报 错终⽌运⾏。这个宏常常被称为“断⾔”。
assert(p != NULL);
上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。
如果确实不等于 NULL ,程序 继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。
assert() 宏接受⼀个表达式作为参数。
如果该表达式为真(返回值⾮零), assert() 不会产⽣ 任何作⽤,程序继续运⾏。
如果该表达式为假(返回值为零), assert() 就会报错,在标准错误 流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。
assert() 的使⽤对程序员是⾮常友好的,
使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和 出问题的⾏号,还有⼀种⽆需更改代码就能
开启或关闭 assert() 的机制。如果已经确认程序没有问 题,不需要再做断⾔,
就在 #include 语句的前⾯,定义⼀个宏 NDEBUG 。
#define NDEBUG
#include <assert.h>
剩下的内容将留在后续的文章当中,^_^