作业:实现一个内存拷贝函数,把一块内存中的数据拷贝到另一块内存,考虑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);
复习:
使用指针要注意的问题:
空指针:值为NULL指针是空指针,系统规定空指针不能解引用,否则就会产生段错误。
为了避免空指针产生的段错误,要在解引用前对来历不明的指针进行判断:
1、函数的参数是指针,不要相信调用者。
2、函数的返回值是指针,C语言中约定,返回指针的函数执行出错时会返回空指针,判断这种指针既知道函数执行是否出错,了可以避免空指针产生的段错误。
注意:绝大多数系统的NULL值0地址,但个别系统的NULL是1地址
if(!p) 不通用
if(p == NULL) 少写一个等号时不会报名
if(NULL == p) 最正确的判断方法
野指针:不知道指针变量中存储的地址是否合法,这种指针叫野指针。
对野指针解引用可能产生的后果:段错误、脏数据、一切正常。
但野指针比空指针的危害更大,因为野指针产生的错误是隐藏的、潜伏的、随机的,出错后很难寻找、定位、重现。
不制造野指针才能避免使用野指针:
1、定义指针变量时要初始化,宁可赋值为NULL,也不产生野指针。
2、函数不要返回局部变量的地址。
3、堆内存释放后,与它配合的指针要及时赋值为NULL。
const与指针:
功能1:防止通过指针变量修改内存,保存它所指向的目标
const int* p;
int const * p;
提高函数传参效率时使用
功能2:保存指针变量不改变指向
int * const p;
传递内存块时使用
功能3:既保护指针变量也保护它指向的目标
const int * const p;
int const * const p;
指针的运算:
指针变量只有三种运行有意义:
指针+n 代表地址的整数+n*进步值
指针-n 代表地址的整数-n*进步值
指针-指针 (代表地址的整数-代表地址的整数)/进步值,两个指针类型必须相同。
指针加、减运行相当于指针前后移动,指针-指针可以计算出两个指针之间相隔多少个元素。
指针与数组名:
指针与数组名相同点:
1、都是地址,都代表者一块内存的首地址。
2、它们都可以使用 *、[] 支访问内存 。
指针与数组名不同点:
1、数组名是常量,指针是变量。
2、指针变量有4字节存储空间,里面存储的内存的指针,而数组名就是内存地址。
3、指针变量与它的内存块是指向关系,数组名与它的内存块是映射关系。
通用指针:
void类型的指针被称为通用指针,它能与任意类型的指针互相赋值,它不能解引用,进步值是1。
在C语言中:
int* ip;
void* vp;
vp = ip;
ip = vp;
在C++语言中:
vp = ip; // OK,为了兼容C语言中的函数
ip = vp; // NO C++类型检查严格
不同类型的指针不能互相赋值(因为解引用时所访问的内存字节数不同),编译器会产生警告。
但不同类型的指针、数组有一些通用的操作,如果:内存清理、内存拷贝、内存比较,排序、查找等。
当实现这类的功能函数时,调用者提供的实参可能是任意类型的数组、指针,为了避免产生警告需要把void类型的指针作为函数的形参,兼容所有类型的数组、指针,这样就避免了编译器产生警告。
指针数组:
指针数组是由指针变量组成的数组,它的成员是指针,但它的身份是数组。
int* arr[5]; // 相当于定义了5个int类型的指针变量
使用它可以构建出不规则的二维数组:
int arr1[9] = {9,1,2,3,4,5,6,7,8};
int arr2[5] = {5,1,2,3,4};
int arr3[4] = {4,1,2,3};
int arr4[7] = {7,1,2,3,4,5,6};
int arr5[8] = {8,1,2,3,4,5,6,7};
int arr[5] = {arr1,arr2,arr3,arr4,arr5};
for(int i=0; i<5; i++)
{
for(int j=1; j<arr[i][0]; j++)
{
printf(“%d “,arr[i][j]);
}
printf(”\n”);
}
备注:后续学习了字符串以后,字符串数组也会使用到指针数组。
数组指针:
专门用来指向数组的指针,它的进步值是整数数组的字节数,当我们对一维数组名取地址时获取到的就是数组指针。
定义数组指针:
int (*p)[5]; // 指向 int arr[5] 类型的数组
所以的二维数组其实是用一维数组模拟的,二维数组的内存块是连续的。
int arr[3][5] = {{1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15}};
// arr的类型 int (*p)[5];
int* p = (void*)arr;
for(int i=0; i<15; i++)
{
printf("%d ",p[i]);
}
使用数组指针可以把一维数组(一块连续的内存)当作二维数组使用,并且可以任意转换行数、列数:
int arr[18] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18};
int (*p1)[6] = (void*)arr;
for(int i=0; i<3; i++)
{
for(int j=0; j<6; j++)
{
printf("%2d ",p1[i][j]);
}
printf("\n");
}
指针与一维数组名、二维数组名、数组指针的关系:
int arr[5];
arr 是int*类型,一维数组名就是普通的指针,所以 arr[i] <=> *(arr+i);
&arr 是int (*p)[5]类型,一维数组取地址就是数组指针
int arr1[3][5];
arr1 是int(*p1)[5]类型,二维数组名就是数组指针
arr1[i][j] <=> *(*(arr1+i)+j)
arr1[i] <=> *(arr1+i) => int* 数组指针解一次引用得到的是普通指针
所以把一维数组名强制转换成数组指针后就可以当作二维数组使用,变量的排序顺序没有变,只是改变了指针的进步值。
指针函数:
指针函数就是返回值是指针类型的函数,没有什么要重点为关注的,主要是为了防止别人装13。
函数指针:
前提:一个函数就是一段代码,它会被翻译成二进制指令存储在text内存段中,函数名就它的地址,它记录着函数在text内存段的位置,只要知道的函数的地址和函数的类型就可以调用这个函数。
函数指针:专门存储函数地址的变量。
如何定义函数指针:
1、照抄函数的声明。
2、用小括号包括函数名。
3、在函数名前面加*,末尾加fp,防止命名冲突,然后用函数名为函数指针赋值。
4、函数指针(),就可以调用函数。
5、函数指针不可以解引用,或者说加()就是解引用。
函数指针有什么用:
可以把函数像数据一样在函数之间传递,可以让旧的函数调用新的函数,这种调用模式叫回调模式。
1、在设计旧函数(qsort)时留一个函数指针的参数(compar),在函数内对函数指针"解引用"(在qsort的内部会调用compar)。
2、当调用旧函数时需要传递给他一个函数地址(intcmp),这样我们传递的新函数就在旧函数内被调用了,例如:qsrot函数。
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
功能:通用的排序函数
base:数组的首地址,使用void*可以兼容任意类型的数组
nmemb:数组的长度
size:数组的每个元素的字节数
compar:负责比较数组成员的函数
原理:qsort以base为起点,每次切割size个字节,然后拆分出nmemb成员,把这些成员依次传递给compar比较,根据比较的结果对成员进行排序。
注意:作为调用者应该知道base是什么类型的数组,实现compar函数时就该知道把compar的两参数转换成什么类型的指针,然后解引用进行比较。
int intcmp(const void* p1,const void* p2)
{
if((int)p1 > (int)p2)
return 1;
if((int)p1 < (int)p2)
return -1;
return 0;
}
int main(int argc,const char* argv[])
{
int arr[10] = {1,3,5,6,7,8,9,0,2,4};
qsort(arr,10,4,intcmp);
for(int i=0; i<10; i++)
{
printf("%d ",arr[i]);
}
return 0;
}