指针(1)
1.内存和地址
1.1内存
内存就好比一栋宿舍楼的一间一间的房间,而地址好比房间号,告诉你的朋友你的房间号才可以更快的找到你;在计算机中将内存划分为一个一个的内存单元,每个内存单元的大小是取1个字节。
计算机中常见的单位:
bit - 比特位
byte - 字节
KB
MB
GB
TB
PB
1 byte = 8 bit
1 KB = 1024 byte
1 MB = 1024 KB
1 GB = 1024 MB
1 TB = 1024 GB
1 PB =1024 TB
生活中我们把门牌号叫做地址,在计算机中我们把内存单元的编号叫做地址,在C语言中给地址起了新的名字:指针。
所以我们可以理解:
内存单元编号==地址==指针
1.2 该如何理解编址
CPU访问内存中的某个字节的空间,必须知道这个字节空间在内存的什么位置,又因为内存中的字节有很多,所以需要给内存进行编址(同宿舍编号一样)。
计算机中有许多的硬件单元,而硬件单元是相互协同工作的。用 "线" 将相互独立工作的硬件相连接起来,今天只了解一组线:地址总线。
32位机器有32根地址总线,每两根线有两种状态:0 和 1【电脉冲有无】,那么32根地址线就有2^32种含义,每种含义都代表一个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据再通过数据总线传入CPU内寄存器。
2.指针变量和地址
2.1取地址操作符(&)
在C语言中创建变量其实就是向内存申请空间,比如:
如何得到 a 的地址呢?
取地址取得的是 a 所占4个字节中地址较小的地址。只要知道第一个地址我们就可以顺藤摸瓜访问到4个字节的数据。
2.2 指针变量和解引用操作符(*)
2.2.1 指针变量
我们通过取地址操作符(&)得到了变量的地址,例如上面的例子中:0x010FFB68,但是这个16进制的数据也是需要存储起来的,这个时候就要用 指针变量 将 地址存储起来。
指针变量也是一种变量,这种变量是用来存放地址的,存放在指针变量中的值都会理解为地址
2.2.2 拆解指针变量类型
pa 左边的 int * 中的 * 说明 pa 是指针变量,而前面的 int 是在说明 pa 指向的是整型(int)类型的的对象。(int 指向的是在指针变量 pa 中所存储的内容是 int,也就是 a 的值(int 类型))。
char 类型也是一样的:
2.2.3 解引用操作符
在C语言中,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这个时候就要用到一个操作符叫做解引用操作符(*)
int main()
{
int a = 10;
int* pa = &a;
*pa = 20;// * 是解引用操作符,*pa 等价于a
//a=20;等价于重新给a赋值
printf("%d\n",a);
//char ch = 'w';
//char* pc = &ch;
return 0;
}
2.3 指针变量的大小
前面我们提到32位机器有32根地址总线,没跟地址线出来的电信号转换成数字信号后是1或者是0,我们将32根地址线产生的2进制序列当作一个地址,那么一个地址就是32个bit位,需要4个字节才能存储。如果指针变量是用来存放地址的,那么指针变量的大小就的是4个字节的空间才可以。
同样64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变量的大小就是8个字节。
结论:
32位平台下地址是32个bit位,指针变量大小是4个字节
64位平台下地址是32个bit位,指针变量大小是8个字节
指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。
3.指针变量类型的意义
前面的例子中我们发现不管是什么类型的指针变量,在同一个平台下,大小都是一样的,为什么还要有各种各样的类型呢?
从下面的代码就能感受到指针类型的意义:
int main()
{
int a = 0x11223344;
int* pa = &a;
*pa = 0; //将内存中的 44 33 22 11 全部改为 00 00 00 00
//这是因为 pa 是 int *的类型,一次访问4个字节,所以能将4个字节全部修改为0
//在调试中打开内存就可以看见 *pa 中的 * 的作用,
//对 pa 进行解引用,间接地来改变 a 的值
return 0;
}
在调试中查看,感受更为直观:
int * 类型的指针变量:
一下就操作了4个字节的大小的内容,将4个字节的内容全部置为0。
char * 的指针变量类型:
将 a 的指针类型 int * 转换为 char * 类型,在下一步的调试中会有区别吗?
从上面的调试中我们发现只改变了一个字节大小的内容,这就充分体现了指针变量类型的重要性。
结论:
指针类型决定了,对指针解引用的时候有多大的权限(一次能操作几个字节)char * 的指针解引用就只能访问1个字节,而 int * 的指针的解引用就能访问4个字节
3.2 指针 + - 整数
从上面的例子我们就可以看见虽然 pa 和 pc 指针变量大小是一样的,但是 +1 操作后的数据是完全不同的,这个便与指针类型有关了。
指针变量类型的意义在于:
1.对指针解引用的时候有多大的权限(一次能操作几个字节)
2.指针向前或者向后走一步有多大(距离)
3.3 void * 指针
在指针类型中有一种特殊的类型是 void * 类型的,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接收任意类型地址。但是也有局限性,void * 类型的指针不能直接进行指针的 + - 整数和解引用的运算。
int main()
{
int a = 5;
int* pa = &a;
char* pc = &a;//需要转换一下类型,因为&a的类型是 int *,编译的时候会报错
return 0;
}
会出现以下的报错:
但是用 void * 来接收的话,就不会出现这样的问题:
但是无法进行指针运算:
int main()
{
int a = 5;
void * pa = &a;
void * pc = &a;
*pa = 11;
*pc = 7;
return 0;
}
一般 void * 类型的指针是使用在函数的参数部分,用来接收不同类型的地址。
4.const修饰指针
4.1const 修饰变量
变量是可以被修改的,如果把变量的地址交给指针变量,那么也可以通过指针变量来修改这个表变量。如果我们不想让变量被修改,这个时候就要用上const了。
int main()
{
const int a = 8;// a 不能被修改了,但是a的本质还是变量,const仅仅是在语法上
//做了限制,所以我们习惯上叫a是常变量。
a = 15;// error 执行时就会出现错误 加上const a的值就不能被修改了
printf("a=%d\n", a);
return 0;
}
从上面的代码中我们可以看见,在 a 的前面加上了 const 后,后续对 a 进行修改会发现修改不了,但是我们将 a 的地址存放在指针变量中,通过指针变量来进行对 a 的值的修改,这样可以成功吗?
从上面的运行结果来看,是可以通过指针变量来对 a 进行修改的,这样就打破了 const 的限制,这是不合理的,应该让 pa 拿到 a 的地址也不能修改 a,那应该怎样编写代码呢?
4.2 const 修饰指针变量
一般来讲 const 修饰指针变量,可以放在 * 的左边,也可以放在 * 的右边,意义是不一样的。
int const * p;
int * const p;
int main()
{
const int a = 10;
//const 在 * 的左边
int const * pa=&a;//等价于 const int * pa =&a;
//*pa = 13;//error 这个时候就会出现错误:不能修改 *pa的值
int b = 17;
pa=&b;//ok
printf("%d\n", *pa);
return 0;
}
int main()
{
const int a = 10;
//const 在 * 的右边
int * const pa = &a; //不能修改 pa 的值
*pa = 13;//ok
int b = 17;
//pa = &b;//error
printf("%d\n", *pa);
return 0;
}
int main()
{
const int a = 10;
//const 在 * 的左右两边,不能修改 * pa(解引用对象的内容)的值和pa(地址)
int const *const pa = &a;
//*pa = 13;//error 这个时候就会出现错误:不能修改 *pa的值
int b = 17;
//pa = &b;//error 这个时候就会出现错误:不能修改 pa的值
printf("%d\n", *pa);
return 0;
}
结论:
- const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。
- const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
5.指针运算
指针的基本运算有的三种,分别是:
- 指针 + - 整数
- 指针 - 指针
- 指针的关系运算
5.1 指针 + - 整数
数组是在内存中连续存放的,只要知道第一个元素的地址,顺藤摸瓜就能访问到后面的所用元素。
int arr[10]={10,9,8,7,6,5,4,3,2,1};
int main()
{
int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };
int *p=&arr[0];
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p+i));//p+i 加的是 i * sizeof(int)
//p=0x001122ff10
//p+1=0x001122ff14
//p+2=0x001122ff18
//p+3=0x001122ff1c
}
return 0;
}
5.2 指针 - 指针
//指针-指针=地址-地址=元素的个数
int main()
{
int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };
printf("%d\n", &arr[9] - &arr[0]);
//指针-指针的绝对值是指针和指针之间的元素个数
//指针相减的运算在底层其实是(arr[9] - arr[0])/ sizeof(元素类型)
//指针-指针运算的前提条件是:两个指针指向同一块空间
char ch[6] = {0};
printf("%d\n", &ch[0] - &arr[0]);//error 但是输出的结果是 -16 why?
//有可能地址差位 -64 字节,若按 int 小计算元素个数:-64/4=-16
return 0;
}
指针 - 指针的作用??
可以简单地实现一下 strlen函数的作用:
int my_strlen(char * s)
{
char* start = s;
while (* s)// \0 的ASCII码值是0
{
s++;
}
return s - start; //指针-指针
}
int main()
{ //strlen 求字符串的长度—统计的是 \0 前面出现的字符的个数
//printf("%d\n", strlen("abcdefgh"));//8
int len = my_strlen("abcdefghi");
printf("%d\n", len);
return 0;
}
5.3 指针的关系运算
int main()
{
int arr[10] = {10,9,8,7,6,5,4,3,2,1};
int *p = &arr[0];
int sz = sizeof(arr)/sizeof(arr[0]);
while(p<arr+sz) //指针的⼤⼩⽐较
{
printf("%d ", *p);
p++;
}
return 0;
}
6.野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
6.1野指针成因
1.指针未初始化
int main()
{
int* ptr;//野指针
*ptr = 17;//非法访问内存
//printf("%d\n", *ptr);
return 0;
}
2.指针越界访问
int main()
{
int arr[5] = { 0 };
int i = 0;
int* p = arr;
for (i = 0; i < 10; i++)
{
* p = 1;
p++;
printf("%d ", arr[i]);
}
return 0;
}
3.指针指向的空间释放
int* test()
{
int a = 10;
//……
return &a;
}
int main()
{
int* p = test();
printf("hehe\n");//这里加了一句代码,*p 的值就变了 why?
printf("%d\n", *p);
return 0;
}
6.2如何规避野指针
6.2.1 指针初始化
如果明确知道指针指向哪里就直接赋值地址,如果不知道指针指向哪里,可以给指针赋值NULL。
NULL是C语言中定义的一个标识符常量,值是0,0也是地址,不过这个地址是无法使用的,读写该地址是会报错的。
int main()
{
int num = 19;
int* pn = #
int* p = NULL;
return 0 ;
}
6.2.2 指针越界
一个程序向内存申请了哪些空间,通过指针就只能访问到哪些空间,不能超出范围进行访问,超出了就是越界访问。
6.2.3 指针变量不再使用是,需要及时置为NULL,使用之前检查有效性
当指针变量指向一块区域时,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把指针置为NULL。因为一个规则是:只要是NULL的指针就不去访问,同时使用指针之前可以判断指针是否为NULL。
int main()
{
int arr[5] = { 0 };
int i = 0;
int* p = arr;
for (i = 0; i < 10; i++)
{
* p = 1;
p++;
printf("%d ", arr[i]);
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//使用p之前,先要判断p是否为空,不为NULL才可以使用
//……
p = &arr[0];//重新让 p 获得地址
if (p != NULL)//判断
{
//……
}
return 0;
}
6.2.4 避免返回局部变量的地址
造成野指针的第3个例子,不要返回局部变量的地址。
7.assert断言
assert.h 头文件定义了 宏 assert ( ),用于在运行时确保程序符合指定条件,如果不符合,就报错,终止运行。这个宏常常被称为 “断言”。
assert( p ! = NULL );
运行到这一行的语句时,验证变量 p 是否等于 NULL。如果确实不等于 NULL,程序就会继续运行,否则就会终止运行,并报错。
assert 宏 接收一个表达式作为参数。如果该表达式为真(返回值非0),assert()不会产生任何作用,程序继续运行。如果表达式为假(返回值为0),assert ( )就会报错,显示没有通过的表达式,以及包含这个表达式的文件名和行号。
assert 的好处:
- 能自动的表示文件和出问题的行号
- 无需更改代码就能开启或者关闭 assert()的机制
如果确认程序没有问题,不需要再做断言的操作了,就在 #include <assert.h> 语句前面,定义一个宏 NDEBUG。
#define NDEBUG
#include <assert.h>
使用上述的代码后在编译程序的话,编译器就会禁用掉文件中的所有 assert ( ) 语句。程序出现问题,就注释掉这条语句,再次编译,就重新启用了assert()语句。
assert ( ) 缺点:
assert ( ) 因为引入了额外的检查,就会增加程序的运行时间。
一般我们在 Debug 版本中使用,在 Release 版本中选择禁用 assert 就行。这样在 Debug 版本中写有利于程序员排查问题,在 Release 版本中不影响用户使用程序的效率。
8.指针的使用和传址调用
8.1 strlen 的模拟实现
库函数 strlen 的功能时求字符串长度,统计在 \0 前面的字符的个数。
函数原型如下:
size_t strlen (const char * str );
参数 str 接收一个字符串的起始地址,统计字符串中 \0 之前的个数,最终返回长度。
int my_strlen(char* s)
{
int count = 0;
while (*s != '\0')
{
count++;
s++;
}
return count;
}
int main()
{ //strlen 求字符串的长度—统计的是 \0 前面出现的字符的个数
//printf("%d\n", strlen("abcdefgh"));//8
int len = my_strlen("abcdefghi");
printf("%d\n", len);
return 0;
}
8.2 传值调用和传址调用
例如:用代码实现两个整数的交换
传值调用:
Swap1(int x, int y)
{
int z = 0;
z = x;
x = y;
y = z;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a,&b);
printf("交换前 a=%d b=%d\n", a, b);
//交换a、b的值
Swap1(a, b);//传值调用
printf("交换后 a=%d b=%d\n", a, b);
//运行代码会发现:ab的值并没有交换,这是因为在Swap1函数中进行了两数的交换
//xyz都属于局部变量,出函数即被销毁,所以传值调用中形式参数的改变不会影响
//实际参数的改变
return 0;
}
从上面的代码运行结果就可以看出来,并没有实现两个整数的交换。
结论:
实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参。
传值调用:
Swap2(int* x,int *y)//传过来的是地址,用指针来接收
{
int tmp = 0;
tmp = *x; //tmp=x
*x = *y; //x=y
*y = tmp; //y=tmp
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前 a=%d b=%d\n", a, b);
//交换a、b的值
Swap2(&a,&b);//传址调用
printf("交换后 a=%d b=%d\n", a, b);
return 0;
}
传值调用可以让函数和主函数之间建立真正的关系,可以实现在函数内部通过地址就可以修改主函数中的变量。所以希望在函数内部达到修改主调函数中的变量的值,就需要传址调用。