目录
什么是指针?
野指针
造成野指针的原因:
如何避免野指针?
内存和指针
如何理解编址?
指针变量和地址
取地址操作符&
指针变量和解引用操作符
指针变量
如何拆解指针类型?
指针变量的大小
指针变量类型的意义
指针的解引用
指针+整数
void*指针
void* 类型的指针有什么用呢?
const修饰指针
const修饰变量
const修饰指针变量
指针运算
指针+-整数
指针-指针
指针的关系运算
assert断言
使用assert()有几个好处:
指针的使用和传址调用
strlen的模拟实现
传值调用和传址调用
什么是指针?
指针(pointer)是编程语言中的一个对象,利用地址,它的值直接指向存在电脑存储器中另一个地方的值,由于通过地址能找到所需的变量单位,可以说,地址指向该变量单位,因此,将地址形象化的称为“指针”,意思是,通过它能找到以它为地址的内存单元
指针是一个变量,存放内存单元的地址(编号)
#include <stdio.h>
int main()
{
int a = 10;//在内存中开辟一组空间
int* p = &a;//将a的地址存放在p变量中,p就是一个指针变量
return 0;
}
野指针
野指针:指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
造成野指针的原因:
- 指针未初始化;
- 指针越界访问;
- 指针指向内存空间释放;
如何避免野指针?
- 指针初始化;
如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL
NULL是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错
- 小心指针越界;
- 指针指向空间释放即使置NULL;指针使用之前检查有效性
当指针变量指向一块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL,因为约定俗成的一个规则就是:只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL
- 避免返回局部变量的地址
内存和指针
计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中
把内存划分为一个个的内存单元,每个内存单元的大小取1个字节
1byte=8bit(bit是最小的内存单元)
一个比特位可以存储一个2进制的位1或者0
内存单元的编号=地址=指针
如何理解编址?
首先,必须理解,计算机内有很多的硬件单元,这些硬件单元要互相协同工作。所谓的协作,至少相互之间要能够进行数据传递
但是,硬件与硬件之间是相互独立的,需要用“线”进行通信
CPU和内存之间也是有大量的数据交互的,所以两者必须用线连起来-地址总线
CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而内存中字节很多,需要给内存进行编址
计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的
32位机器有32根地址总线,每根线有两种状态,表示0或1(电脉冲有无),一根线表示2种含义,2根线表示4种含义,32根地址线表示2^32种含义,每一种含义代表一个地址
地址信息被下达给内存,在内存上就可以找到该地址对应的数据,将数据通过数据总线传入CPU内寄存器
指针变量和地址
取地址操作符&
在C语言中创建变量其实就是向内存申请空间
&a取出的是a所占4个字节中地址较小的字节地址
指针--地址
指针变量--存放地址的变量
指针变量和解引用操作符
指针变量
通过取地址操作符(&)拿到的地址是一个数值,这个数值需要存放在指针变量中
指针变量也是一种变量,这种变量是用来存放地址的,存放在指针变量中的值都会理解为地址
指针变量中存储的是一个地址,指向同类型的一块内存空间
如何拆解指针类型?
int a=10;
int* pa=&a;
pa左边写的是int*,*说明pa是指针变量,前面的int说明pa指向的是整型(int)类型的对象
int main()
{
int a = 10;
int* p = &a;
*p = 0;//*解引用操作符(间接访问操作符)
//*&a=0;
printf("%d", a);//0
return 0;
}
指针变量的大小
32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或0,把32根地址线产生的2进制序列当作一个地址,那么一个地址就是32个bit位,需要4个字节才能存储
如果指针变量是用来存放地址的,那么指针变量的大小就得是4个字节的空间才可以
同理,64位机器,假设有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储起来就需要8个字节的空间,指针变量的大小就是8个字节
指针变量--存放地址的
一个字节对应一个地址
指针用来存放地址,地址是唯一标示一块地址空间的
总结:
1.32位平台下地址是32个bit位,指针变量大小是4个字节
2.64位平台下地址是64个bit位,指针变量大小是8个字节
3.注意指针变量的大小和类型是无关的,只要是指针类型的变量,在相同的平台下大小都是相同的
指针变量类型的意义
指针的解引用
指针类型决定了指针进行解引用操作时,能够访问空间的大小(对指针解引用的时候有多大的权限--一次能操作几个字节)
int* p;*p能够访问4个字节
char* p;*p能够访问1个字节
doble* p;*p能够访问8个字节
指针+整数
指针类型决定了,指针走一步能走多远(指针的步长,单位是字节)
void*指针
void* 无具体类型的指针(或者泛型指针),这种类型的指针可以用来接受任意类型的地址,但也有局限性,void* 类型的指针不能进行指针的+-整数和解引用的运算
void* 类型的指针有什么用呢?
一般void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果,使得一个函数来处理多种类型的数据
const修饰指针
const修饰变量
变量不再被改变,变量有了常属性,本质仍是变量,常变量(在C++中,const修饰的变量就是常量)
如果要修改变量,就要通过指针来修改,这样就打破了const的限制,是不合理的,所以应该让变量的地址也不能被修改
const修饰指针变量
const修饰指针变量,可以放在*的左边,也可以放在*的右边,意义是不一样的
int main()
{
int a = 10;
int b = 20;
int* const p = &a;//const在*右边
//p=&b;//err
*p = 100;
printf("%d", a);
return 0;
}
- const限制的是指针变量本身,指针变量不能再指向其他变量了,但是可以通过指针变量,修改指针变量指向的内容
int main()
{
int a = 10;
int b = 20;
int const* p = &a;//const在*右边
printf("p=%d\n", p);
p=&b;
//*p = 100;//err
printf("p=%d", p);
return 0;
}
运行结果:
- const放在*左边,限制的是:指针指向的内容,不能通过指针来修改指向的内容,但是可以修改指针变量本身的值(修改指针变量的指向)
*两边可以同时加const
指针运算
指针的作用就是访问内存的
指针的基本运算有三种:
- 指针+-整数
- 指针-指针
- 指针的关系运算
指针+-整数
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);
p++;*/
//等同于下面一句:
printf("%d ", *(p + i));//p没有变化
}
return 0;
}
指针-指针
指针-指针的绝对值是:两个指针之间的元素个数;指针+指针无意义
计算的前提是:两个指针指向了同一块空间
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int my_strlen(char* str)
{
char* start = str;
while (*str != '\0')
{
str++;
}
return str - start;//指针-指针
}
int main()
{
//strlen-求字符串长度,‘\0’之前的字符个数
char arr[] = "abcdef";
int len = my_strlen(arr);
printf("%d", len);
return 0;
}
指针的关系运算
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;//&arr[0]
while (p < arr + sz)
{
printf("%d ", *p);
p++;
}
return 0;
}
assert断言
assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行,这个宏常常被称为“断言”
assert(p != NULL);
上面代码在程序运行到这一行语句时,验证变量是否等于NULL,如果不等于NULL,程序继续运行,否则就会终止运行,并且会给出报错信息提示
assert()宏接受一个表达式作为参数,如果该表达式为真(返回值非零),assert()不会产生任何作用,程序继续运行。如果该表达式为假(返回值为0),assert()就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号
使用assert()有几个好处:
assert()的使用对程序员是非常友好的,它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制。如果已经确认程序没有问题,不需要再做断言,就在#include <assert.h>语句的前面,定义一个宏NDEBUG
#define NDEBUG
#include <assert.h>
然后,重新编译程序,编译器就会禁用文件中所有的assert()语句,如果程序又出现问题,可以移除这条#define NDEBUG指令(或者把它注释掉),再次编译,这样就重新启用了assert()语句
assert的缺点是:因为引入了额外的检查,增加了程序的运行时间
一般我们可以在Debug中使用,在Release版本中选择禁用assert就行,在VS这样的集成开发环境中,在Release版本中,直接优化掉了,这样在Debug版本写有利于程序员排查问题,在Release版本不影响用户使用时程序的效率
指针的使用和传址调用
strlen的模拟实现
size_t my_strlen(const char* str)//size_t无符号的整型
{
size_t count = 0;
assert(str != NULL);
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
size_t len = my_strlen(arr);
printf("%d", len);
return 0;
}
传值调用和传址调用
下面看一个典型代码,曾几何时,我被它坑过:
void Swap(int x, int y)
{
int temp = x;
x = y;
y = temp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d,b=%d\n", a, b);
Swap(a, b);//传值调用
printf("交换后:a=%d,b=%d", a, b);
return 0;
}
当实参传递给形参的时候,形参是实参的一份临时拷贝,对形参的修改不会影响实参
如果要交换两个数的值,参考以下代码:
void Swap(int* pa, int* pb)
{
int temp = *pa;
*pa = *pb;
*pb = temp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d,b=%d\n", a, b);
Swap(&a, &b);//传址调用
printf("交换后:a=%d,b=%d", a, b);
return 0;
}