复习:
什么是指针:
1、指针是一种数据类型,使用它可以定义指针变量,我们也把指针变量简称为指针。
2、指针变量中存储的是代表内存地址的整数(32位系统的指针变量是4字节,64系统的指针变量是8字节),整数范围:0x00000000~0xffffffff。
3、可以根据指针变量中存储的内存地址去访问对应的内存。
为什么使用指针:
1、使用指针可以跨函数共享蛮,这样可以获取函数的执行结果(return语句只能返回一个数据),比如:scanf("%d",&num);
2、使用指针可以提高函数的传参效率。
3、堆内存无法取名字,使用堆内存必须与指针配合。
如何使用指针:
定义指针变量:类型* 指针变量名;
1、指针变量与普通变量的取名规则一样,但使用方式完全不同,为了避免滥用,一般让指针变量以p结尾。
2、指针变量与普通变量一样默认值是随机的(野指针),为了安全要对指针变量初始化,如果不知道该赋值什么值,可以先赋值为NULL(空指针)。
3、指针变量不能连续定义:
int* p1,p2,p3; // p1是指针变量,p2,p3只是普通的int类型变量
int *p1,*p2,*p3; // p1,p2,p3都是指针变量
typedef int* intp;
intp p1,p2,p3; // p1,p2,p3都是指针变量
4、指针变量中记录的某个字节的内存地址,它代表着一个内存块的首地址。
5、通过指针变量访问内存时,具体访问多少个字节由指针变量的类型决定。
给指针变量赋值:指针变量 = 内存地址;
int* p = # // 获取普通变量的地址给指针变量初始化,指针变量的类型要与普通变量的类型相同,否则编译时会出现警告,并且在解引用时产生段错误或脏数据
p = malloc(4); // 从系统获取一块堆内存,把内存块的首地址赋值给指针变量
解引用:根据变量中存储的内存地址访问对应的内存,*指针变量。
int* p = #
*p <=> num;
对指针变量解引用时可能出现段错误、脏数据等情况:
原因1:赋值的内存地址是非法的,不在maps文件地址表范围内。
原因2:修改了text内存段的内容。
const int num = 1234;
int main()
{
int* p = #
printf("%d\n",*p); // 可以读
*p = 6666; // 肯定会出现段错误
}
解引用时访问的内存字节数由指针变量的类型决定:
char* p; *p // 访问1字节
short* p; *p // 访问2字节
int* p; *p // 访问4字节
double* p; *p // 访问8字节
*在C语言中有三个作用:
1、乘法运算
2、定义指针变量
3、对指针变量解引用
指针变量的输出:
1、以无符号整数方式显示,但编译器会有警告。
2、%p 以十六进制显示
使用指针要注意的问题:
空指针:值为NULL的指针变量叫空指针,系统规定不能对空指针解引用,否则会产生段错误。
如何避免空指针产生的段错误:对来历不明的指针解引用前,先判断是否是空指针。
1、当实现的函数的参数是指针,所以我们无法确定调用者传递过来的指针是否是空指针,因此在解引用前要先判断。
2、当函数的函数返回值是指针类型,如果函数执行出错则会返回空指针(空指针是函数执行出错一标志),对返回值进行判断,既可以知道函数执行是否出错,也可以避免对空指针解引用,比如:malloc执行出错误返回值是就空。
注意:大多系统的NULL是0地址,但是有少数系统的NULL是1地址,所以判断空指针时
if(!p) // 不通用,不建议这样写
{
}
if(p == NULL) // 这种写法有瑕疵,当=少写一个是编译器不会报错,就变成了赋值,虽p肯定变量成空指针,接下来解引用时肯定出现段错误
{
}
if(NULL == p) // 完美
{
}
野指针:不知道指针变量中存储的地址是否合法,这种指针叫野指针。
对野指针解引用时可能产生的后果:
1、段错误
2、脏数据
3、一切正常
野指针与空指针相比危害更大,因为无法判断一个指针是否是野指针,所以野指针的错误具有隐藏性、潜伏性、随机性,一旦出错很难寻找、定位、重现。
如何避免野指针产生的危害:
前提:所有的野指针都是人为制造出来的,所以只要不制造野指针,就不使用野指针。
1、定义指针变量一定要初始化,宁可赋值为NULL,不要产生野指针。
2、函数不要返回局部变量的地址,因为随着函数的执行结束,属于变量的内存会随即释放,因此这种情况返回的就是野指针。
3、当堆内存被释放后,与它配合的指针变量要及时赋值为NULL。
const与指针:
const与指针配合一共有5种写,3种功能:
功能1:保存指针变量所指向的内存不被p修改
const int p;
int const * p;
p = NULL; // OK
*p = 6666; // NO
当使用指针优化函数的传参效率时,变量就有被修改的风险,这种情况可以用const与指针配合,防止变量在被调用函数里修改。
功能2:保存指针变量不被修改,防止指针变量改变指向。
int * const p;
p = NULL; // NO
*p = 6666; // OK
当使用数组作函数的参数时,数组就脱变成了指针,要想让指针一直指向数组,可以使用const加以保护。
功能3:既保存指针变量也保存它指向的内存。
const int * const p;
int const * const p;
p = NULL; // NO
*p = 6666; // NO
指针的运算:
前提:指针变量中存储的是代表内存地址的整数,理论上整数可以使用的运算符,指针变量都可以使用,但只有指针加/减整数或者指针-指针才有意义。
指针的进步值:指针变量解引用时所访问内存字节数,也就是指针变量加1时前进的字节数。
指针+n 结果是代表地址的整数+进步值*n
int* p = 0;
p+3 得到的结果12
指针-n 结果是代表地址的整数-进步值*n
short* p = 10;
p-4 得到的结果是2
指针-指针 结果是(代表地址的整数-代表地址的整数)/进步值
int* p1 = 18;
int* p2 = 10;
p1-p2 得到的结果是2
注意:指针变量加/减一个整数时,相当于指针变量前后移动(以进步值为单位移动),指针-指针可以计算出两个地址之间隔子多少个元素(指针变量解引用的对象)。
指针与数组名:
数组名:数组名就是个地址,所以数组作为函数的参数时才能脱变成指针,它就是数组内存块的首地址,也是第一个元素的首地址,所以它可以使用解引用访问数组中的每个元素。
int arr[5] = {1,2,3,4,5}; // arr 的类型就是int*
for(int i=0; i<5; i++)
{
printf("%d ",*(arr+i));
}
*(arr+i) <=> arr[i] // 这两种用法是等价的
数组名可以使用指针的语法,指针也可以使用数组的语法:
int arr[5] = {1,2,3,4,5};
int* p = arr;
for(int i=0; i<5; i++)
{
printf("%d ",p[i]);
}
数组名与指针的相同点:
1、它们都是地址,代表着一块内存的首地址。
2、它们都可以使用 *、[] 访问内存中的数据。
数组名与指针的不同点:
1、数组名是常量,而是指针是变量。
2、指针变量有4字节的存储空间,用于存储内存地址,而数组名就是地址。
arr == &arr // 值相同,但类型不同
3、指针与它的内存块之间是指向关系,而数组名与内存块之间是映射关系。
通用指针:
通用指针指的是void类型的指针,它能与任意类型的指针进行转换,缺点是不能解引用,必须先转换成其它类型的指针才能解引用,进步值是1。
为什么使用通用指针:
不同类型的指针不能互相赋值(因为解引用时访问的字节数不同),编译器会产生警告。
但不同类型的指针、数组会有一些通用操作,比如:清理内存、内存拷贝、数组排序等。当实现这类的功能函数时,调用者提供的函数可能是任意类型指针可数组,此时就需要void类型的指针作函数的参数。
练习:实现一个清理数组的函数,把数组中的每个元素都赋值为零。
void clean_arr(void* addr,size_t size)
// size是数组的字节数,只要把数组中的每个字节赋值为0,那么每个元素就赋值为0了。
作业:实现一个内存拷贝函数,把一块内存中的数据拷贝到另一块内存,考虑dest与src有交集情况。
void mem_copy(void* dest,void* src,size_t size);
情况1:
int arr[6] = {1,2,3,4};
mem_copy(arr+2,arr,16);
情况2:
int arr[8] = {1,2,3,4,5,6,7,8};
mem_copy(arr,arr+3,20);