所有指针类型与往往伴随出现的相关概念
- 指针:是一种特殊的数据类型,使用它可以定义指针变量,简称指针,它里面存储的是代表内存地址的无符号整数。
- 空指针:值为NULL的指针变量
- 对NULL解引用会产生段错误同时也是指针函数的错误标志,所以对来历不明的指针解引用前要先判断。
- NULL在一般系统是0地址,极小数的系统也可能是1,判断空指针时要使用NULL== ptr。
- 野指针:不知道指针变量中的地址是否合法,或者通过语义分析出地址非法,这种指针被称为野指针。
- 对野指针解引用可能会出现,一切正常、段错误、脏数据。
- 但野指针无法通过条件语句判断出来,只有不制造野指针才能避免野指针产生的错误。
- 野指针产生的错误具有未知性、潜伏性、随机性,所以它比空指针更危险。
- 指针数组:是一种特殊的数组,成员是指针变量,
类型* 数组名[len];
- 构建不规则的二维数组
- 字符串数组就是一种指针数组
- 数组指针:专门指向数组的指针,
类型 (*指针名)[所指向的数组的长度]。
- 它的进步值是整个数组的总字节数。
- 二维数组的数组名就是数组指针。
- 使用它可以把一块连续的内存当作n*m的二维数组使用。
- 数组指针一般与堆内存配合使用。
- 通用指针:void类型的指针变量
- 在C语言中它能与任意类型的指针进行转换,所以叫通用指针,是设计通用型函数的主要手段之一。
- 不能解引用,进步值是1。
- 在C++中已经基本不再使用,C++有更好的设计通用代码的方法(函数重载、模板)。
- 指针函数:返回值是指针类型的函数
- 返回NULL是这种函数的执行出错、失败标志。
- 调用这种函数时,要先判断返回值是否为空,这种既能知道函数执行是否出错,也能避免NULL产生的段错误。
- 函数指针:专门指向函数的指针变量
- 函数就是一段具有某项功能的代码,会被编译翻译成二进制指针存储在代码段。
- 函数名本质就是地址,是该函数在代码段的位置。
- 使用这函数指针可以把函数像数据一样在函数之间传递,形成回调模式,例如:qsort函数。
- 二级指针:指向指针的指针
- 它是一种特殊的指针变量,里面存储的是普通指针变量的地址。
- 在函数之间共享普通变量时使用一级指针,共享一级指针变量时,必须使用二级指针。
- 结构指针:指向结构变量的指针
- 一般结构的字节数较多,使用堆内存存储结构变量,所以必须使用结构指针配合。
- 由于结构的字节数较多,函数默认是值传递,使用结构指针进行传参能提高函数的传参效率。
- 结构变量.成员,结构指针->成员
- 结构成员指针:结构体中特殊成员,指针变量。
- 当结构体中的某类成员的数量不确定时,适合再分配一块堆内存,使用结构成员指针去指向它,再配合长度属性。
- 当结构体中有成员指针时,需要分配内存时需要两次,释放内存时也需要两次,结构变量不能直接写入文件,结构变量也不能再直接赋值(浅拷贝)。
- 柔性数组。
- 文件指针:fopen函数的返回值,后续操作文件的凭证。
- 指向一个结构体,里面有关于一个打开后的文件的各项属性(输入、输出缓冲区、文件位置指针、文件描述符)。
- 文件位置指针:指向着打开后的文件里面,即将要操作的位置。
- 它不一定是指针变量,可以就是个计数器,会随着读写操作自动移动。
- r、w,方式打开的文件,位置指针指向文件的开头,a方式打开的文件,位置指针指向文件的末尾。
- 常用的操作它的函数,fseek,ftell,rewind,它底层都调用lseek函数实现的。
- 内存碎片:已经释放,但无法被再分配出去、再次使用的小块内存。
- 它是由于内存的释放时间和分配时间不协调导致的。
- 是一种相对概念,不是绝对的。
- 频繁的分配、释放小块内存容易产生内存碎片。
- 尽量使用栈内存,尽量分配大块内存,进行内存碎片整理。
- 内存泄漏:已经不再使用但无法被释放的内存
- 单纯的内存泄漏不可怕,可怕的是反复的内存泄漏,可用的内存越来越少,直到系统死机。
- 进程结束后,内存碎片、泄漏都会得到解决,但服务器程序不能随意结束,需要长年累月的运行。
认识指针
什么是指针:
1、指针(pointer)是一种数据类型,使用它可以定义指针变量,简称指针,指针类型与int,float,char这些数据类型,最大的区别就在于指针变量中存储的并不是数据本身,而是存储了这个变量的内存地址。
2、指针变量中存储的是内存的地址,是一种无符号的整数。
3、通过指针变量中记录的内存地址,我们可以读取内存中所存储的数据,也也可以向,内存中写入数据。
4、一般使用%p以十六进制格式显示内存地址。
如何使用指针:
定义指针变量:
类型* 指针变量名;
1、指针变量中只记录了内存中某字节的地址编号,我们把它当作一个内存块的首地址,当使用指针变量访问内存时具体访问多少个字节,由指针变量的类型决定。
char* p; // 能访问1字节
short* p; // 能访问2字节
int* p; // 能访问4字节
2、普通变量与指针变量的用法不同,为了避免混用,所以指针变量的命名中往往会带ptr或者p来进行标识。
3、指针变量不能连续定义,一个*只能定义出一个指针变量。
int num1,num2,num3;
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、指针变量与普通变量一样,默认值是随机的(野指针),为了安全尽量给指针变量初始化,如果不知道该赋什么值,可以先初始化为NULL(空指针)。
给指针变量赋值:
指针变量 = 内存地址。
所谓的给指针变量赋值,就是给指针变量存储一个内存地址,如果该内存地址是非法的,当使用指针变量访问内存时就会出现段错误。
int* p = malloc(4); // 把堆内存的地址赋值给指针变量,参数为4代表分配了4个字节的内存
int* p = # // &计算出变量的内存地址(单目运算符)
注意:num变量的类型必须与p的类型相同
指针变量解引用:
*指针变量
指针变量赋值就是引用一块内存,解引用就是根据指针变量存储的内存地址,去访问内存,具体访问多少个字节由指针变量的类型决定。
如果指针变量中存储的是非法内存地址,该动作会出现段错误,要从指针变量赋值的步骤去解决。
int num = 1234;
//声明了一个int类型的指针并为其赋值为num变量的地址
int* p = #
//*p <=> num;
printf("%d",*p);
*p = 2345;
printf("%d\n",num);
//使用const修饰的变量是可以通过指针来进行访问和修改的,并不是绝对安全。
int main()
{
const int num = 1234;
int* p = (int*)#
*p = 6666;
printf("%d\n",num);
}
为什么要使用指针:
1、函数之间共享变量
函数之间的命名空间是互相独立,并且以赋值方式传参的,所以传参无法解决共享变量的问题。
全局变量虽然可以在函数之间共享,但过多使用全局变量可以会造成命名冲突和内存浪费,同时会提高代码阅读的难度。
当函数一个需要返回两个以上的参数时,就需要共享变量了。
2、使用指针变量可以节约函数的传参效率
函数之以赋值方式传参的,也就是内存的拷贝,把一个变量的内存内容拷贝给别一个变量,当变量的字节数比较大时(大于4字节),传参效率就很低,而传递变量的地址,只需要拷贝4字节内存(指针在不同操作系统中占用的内存大小:16位系统中是两字节,32位系统中是4字节,64位系统中是8字节) 。
使用time命令在linux系统下进行测算:
#include <stdio.h>
void func(long double f)
{
}
int main()
{
long double f = 3.14;
for(int i=0; i<1000000000; i++)
{
func(f);
f++;
}
}
/*
real 0m5.527s
user 0m5.523s
sys 0m0.004s
*/
void func(long double f)
{
}
int main()
{
long double f = 3.14;
for(int i=0; i<1000000000; i++)
{
func(f);
f++;
}
}
/*
real 0m2.553s
user 0m2.553s
sys 0m0.000s
*/
3、使用堆内存时必须与指针变量配合
堆内存无法取名字,标准库、操作系统提供内存分配接口的返回值都是内存地址,所以必须使用指针变量配合才能在分配完堆内存后获取到进行后续的使用。
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
注意:由于使用指针变量具有一定的危险,所以除了以上情况,不要轻易使用指针。
使用指针要注意的问题:
空指针:
指针变量中如果存储的是NULL,那么它就是空指针,因此操作系统规定程序不能访问该内存,只要访问就会产生段错误,同时也是返回值是指针类型的函数执行错误的标志。
如何避免空指针产生的段错误?
对来历不明的指针进行解引用前,要先判断是否是空指针。
1、当指针变量接收了函数的返回值,判断是否是空指针,既能避免访问空指针产生的段错误,也能知道该函数执行是否失败、出错。
2、如果设计的函数参数是指针变量,那么调用者传递实参就可能是空指针,对指针变量解引用前要先判断。
if(NULL == p) // 正确写法
{
}
if(p == NULL) // 错误写法,当少写一个等号时,就变成了给p赋值为空指针
{
}
if(!p) //大多数系统的NULL是0,但有少数系统的NULL是1
{
}
野指针:
产生了野指针,因为指针是局部变量,返回之后就已经被销毁了。
指针变量中存储的地址,无法确定是否是合法的内存地址,这种指针变量被称为野指针。
对野指针解引用的后果:
1、一切正常,指针变量恰好存储的是空闲的内存地址,概率不高。
2、段错误,存储的是非法的内存地址。
3、脏数据,存储的是其它变量的内存地址。
*应该在初始化的时候指明指针,如果先进行声明,再赋值,那么就有可能导致对野指针进行访问出现段错误。*
如何避免野指针产生的错误:
野指针无法判断出来,但所有的野指针都是人为制造出来的,所以要想避免野指针产生的错误,只能不制造野指针。
如何不制造野指针:
1、定义指针变量时一定要初始化,要么赋值一个合法的内存地址,要么初始化NULL。
类似于悬空引用,如果返回一个被销毁的局部变量的指针的话就是导致访问了非法内存地址可能导致段错误
2、**不返回局部变量、块变量的地址,当函数执行完毕后,局部变量和块变量就被销毁。**注意创建和查找的区别,创建的话必须要分配内存,不然给链表创建的就只是一块野指针,可能导致段错误。而查找的话是可以返回局部变量的
3、与堆内存配合的指针变量,当堆内存被释放、销毁,该指针变量要及时的赋值为NULL。
野指针比空指针的危害更大:
野指针产生的错误具有隐藏性、潜伏性、随机性,所以野指针比空指针危害更大。
指针的运算与进阶
指针的进步值与指针的运算:
指针变量里面存储的是整数,代表着内存的编号(每个整数都对应着一字节的内存)。
不把指针作为参数传入也可以直接修改参数的值。
#include <stdio.h>
void func(int num)
{
//先强制类型转换得到了num的指针的整数值,然后通过解引用修改了num的值
*(int*)num = 6666;
}
int main()
{
int num = 1234;
func((int)&num);
printf("%d\n",num);
}
指针的进步值:
指针变量中存储的其实是一个内存块的首地址,内存块的具体大小由指针变量的类型决定,当使用指针变量解引用访问内存时,实际访问的内存字节数叫做指针变量的进步值,也就是指针变量+1后的内存地址的变化。
#include <stdio.h>
int main()
{
char* p1 = NULL;
short* p2 = NULL;
int* p3 = NULL;
long long* p4 = NULL;
long double* p5 = NULL;
printf("%p %p %p %p %p\n",p1,p2,p3,p4,p5);
printf("%p %p %p %p %p\n",p1+1,p2+1,p3+1,p4+1,p5+1);
}
指针的运算:
指针变量存储就是是整数,理论上整数能使用的运算符,指针变量都可以使用,但只有以下运算才有意义:
指针+n = 指针所代表的整数+进步值*n
指针-n = 指针所代表的整数-进步值*n
指针1-指针2 = (指针1所代表的整数-指针2所代表的整数)/进步值
指针加减整数,就相当于以指针变量的进步值为单位前后移动,指针-指针可以计算出两个指针变量之间相隔多个元素。
#include <stdio.h>
int main()
{
int arr[10];
int* p1 = &arr[0];
int* p2 = &arr[9];
printf("%p %p\n",p2,p1);
printf("%d\n",p2-p1);
}
注意:
指针-指针,以及指针之间的相互运算,它们的类型必须相同,否则编译器会报错。
数组名与指针:
数组名在作为参数时就是指针:
1、数组名就是数组内存块的首地址,它是个常量地址(特殊的指针),而指针是一个变量,数组和指针本质上有区别。只有数组名作函数的参数时,才能蜕变成指针变量。
#include <stdio.h>
void show_arr(int* arr,size_t len)
{
// 地址
printf("%p\n",arr);
for(int i=0; i<len; i++)
{
// *(arr+i) <=> arr[i];
printf("%d\n",arr[i]);
}
}
int main(int argc,const char* argv[])
{
// 地址 常量 数组
int arr[] = {1,2,3,4,5,6};
printf("%p\n",arr);
printf("%d\n",sizeof(arr)/sizeof(arr[0]));
show_arr(arr,6);
for(int i=0; i<6; i++)
{
printf("%d ",*(arr+i));
}
}
2、指针变量可以使用[]解引用,数组名也可以*遍历,它们是等价的。
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
for(int i=0; i<10; i++)
{
// 指针[i] <=> *(指针+i)
printf("%d %d\n",arr[i],*(arr+i));
}
}
注意:如果定义<TYPE> arr[n]数组,数组名arr 就是 TYPE类型的地址。*
数组名与指针的相同点:
1、它们都是地址
2、它们都使用[],*去访问一块连续的内存
数组名与指针的不同点:
1、数组名是常量,而指针是变量
2、指针变量有它自己的存储空间,而数组名就是地址,它没有存储地址的内存。’
*指针变量有它自己的存储空间:当你声明一个指针变量时,例如 `int *ptr;`这个指针变量会占据一定的内存空间,这个内存空间用来存储指针变量所指向的内存地址。
数组名就是地址,它没有存储地址的内存,,数组名实际上是数组的第一个元素的内存地址。数组名本身不会占据额外的内存来存储地址,它只是一个符号或标识符,代表了数组的起始地址。*
3、指针变量与它的目标内存是指向关系,而数组名与它的目标内存是映射关系。
#include <stdio.h>
void show_arr(int arr[],size_t len)
{
printf("show:%p %p\n",arr,&arr);
}
int main(int argc,const char* argv[])
{
int arr[] = {1,2,3,4,5,6};
printf("main:%p %p\n",arr,&arr);
show_arr(arr,6);
}
通用指针:
我们一些通用的操作函数,它们的参数可能是任意类型的指针,但编译器规定不同类型的指针不能进行赋值,为了兼容各种类型的指针,C语言中设计了void类型的指针,它能与任意类型的指针互相转换,它能解决不同类型的指针参数的兼容性问题。
void* p1;
// void* 可以给任意类型的指针变量赋值
int* p2 = p1;
// 任意类型的指针可以给void*类型的指针赋值
void* p3 = p2;
通用操作的函数:
void bzero(void *s, size_t n);
功能:把内存块s的n个字节,赋值为0。
void *memset(void *s, int c, size_t n);
功能:把内存块s的n个字节,赋值为c(0~255)
void *memcpy(void *dest, const void *src, size_t n);
功能:从src内存块拷贝n个字节的内容到dest内存块
int memcmp(const void *s1, const void *s2, size_t n);
功能:比较s1和s2内存块的n个字节
s1 > s2 返回1
s1 < s2 返回-1
s1 == s2 返回0
注意:
void类型的指针变量的进步值是1。
void类型的指针变量不能解引用 ,必须转换成其它类型的指针才能解引用。
const与指针:
const int * p;
功能:保护指针变量所指向的内存不被*p修改,或者说不能对 *p 赋值
int const * p;
功能:同上
int* p1 = p; // 编译时会有警告
由于指针的使用存在一定的风险,所以函数的参数只要是指针,并且函数没有修改指针所指向的内存的需求,我们就应该给指针变量加上const 类型* 指针变量。
int * const p;
功能:保护指针变量p不被修改
const int * const p;
功能:既保存指针变量p不被修改,也保护指针变量指向的内存*p不被修改
int const * const p;
功能:同上
const int num;
// 指向const修饰的变量时,指针变量要用co他那个tnst修改,否则编译会有警告
const int* p = #
当使用数组作函数的参数时,数组就蜕变成了指针变量,为了防止指针改变指向导致数组无法使用,理论上我们应该使用 类型 const 指针变量 防止指针变量改变指向。*
与堆内存配合的指针变量也应该从一而终,这样定义 类型* const 指针变量 防止指针变量改指向,从面导致堆内存无法释放(防止产生内存泄漏)。
二级指针:
什么是二级指针:
一级指针存储的是普通变量的内存地址,二级指针存储的是指针变量内存地址。
根据指向内容的不同可以分为指向变量的指针和指向数组的指针。
定义二级指针:
类型* 一级指针;
类型** 二级指针;
注意:二级指针在使用方法上与一组指针不同,所以一般以pp结尾,让使用者从变量名上就能区别一级指针与二级指针。
二级指针的赋值:
二级指针 = &一级指针;
注意:给二级指针赋值的一级指针,它们的类型必须相同,否则编译时就会报错。
二级指针解引用:
二级指针 = &一级指针;
*二级指针 此时它等价于一级指针
**二级指针 此时它等价于 *一级指针
#include <stdio.h>
int main()
{
int num = 1234;
int num1 = 4567;
int* p = #
int** pp = &p;
*pp = &num1; // p = &num1;
printf("%d\n",*p);
**pp = 123456789; // num1 = 123456789;
printf("%d\n",num1);
}
二级指针的用处:
只有一个情况适合使用二级指针,那就是跨函数共享一级指针变量。
(很多人,包括我自己其实都对多级指针有恐惧症,所以只需要记住这个原则就行)
指针数组与数组指针:
什么是指针数组:
由指针变量构成的数组,也可以说它的身份是数组,成员是指针变量。
定义指针数组:
类型* 数组名[n];
就相当于定义了n个指针变量。
int* arr[10]; // 相当于定义了10个int*的指针变量
指针数组的用处:
1、构建不规则二维数组。
2、构建字符串数组。
#include <stdio.h>
int main()
{
int arr1[] = {6,1,2,3,4,5,6};
int arr2[] = {3,1,2,3};
int arr3[] = {5,1,2,3,4,5};
int arr4[] = {4,1,2,3,4};
int* arr[] = {arr1,arr2,arr3,arr4};
for(int i=0; i<4; i++)
{
for(int j=1; j<=arr[i][0]; j++)
{
printf("%d ",arr[i][j]);
}
printf("\n");
}
}
什么是数组指针:
专门指向数组的指针变量,它的进步值是整个数组的字节数。
定义数组指针:
类型 (*指针变量名) [n];
类型和n决定了 数组指针 指向的是什么样的数组。
#include <stdio.h>
int main()
{
int arr[5] = {1,2,3,4,5};
int (*p)[5] = &arr;
/*
arr <=> *p
arr[i] <=> (*p)[i]
*(arr+i) <=>*((*p)+i)
*/
// 输入结果是20
printf("%d\n",((int)(p+1))-((int)p));
printf("%p %p %d\n",p,p+1,*((int*)((int)(p+1)-4)));
}
数组指针的作用:
#include <stdio.h>
int main(int argc,const char* argv[])
{
int arr[4][5] = {
{11,12,13,14,15},
{21,22,23,24,25},
{31,32,33,34,35},
{41,42,43,44,45}
};
/*
int* p = (int*)arr;
for(int i=0; i<20; i++)
{
printf("%d ",p[i]);
if(0 == (i+1)%5)
printf("\n");
}
*/
int (*p)[4] = (void*)arr;;
for(int j=0; j<5; j++)
{
for(int i=0; i<4; i++)
{
//printf("%d ",*((*(p+j))+i));
printf("%d ",p[j][i]);
}
printf("\n");
}
}
#include <stdio.h>
// 使用数组指针可以把一块连续的内存当作二维数组使用,特别是与堆内存配合效果更佳
int main()
{
int arr[20] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20};
int (*p)[5] = (void*)arr;
for(int j=0; j<4; j++)
{
for(int i=0; i<5; i++)
{
//printf("%d ",*(*(p+j)+i));
printf("%d ",p[j][i]);
}
printf("\n");
}
}
区分数组名、指针、数组指针:
int arr[n];
arr 是 int*类型
*arr 是 int 类型
&arr[0] 也是 int*类型
&arr 是 int (*)[n]类型
arr[i] <=> *(arr+i)
int arr2[row][col];
arr2 是 int (*)[col]类型
*arr2 是 int* 类型
&arr 是 int (*)[n]类型
int (*p)[n] = &arr;
*p <=> arr
**arr2 是 int 类型
arr2[i][j] <=> *(*(arr2+i)+j)
&arr2[0] 是 int (*)[col]类型
*(arr2+0) <=> arr2[0]
&*(arr2+0) <=> &arr2[0]
&*(arr2+0) <=> arr2+0
&arr2[0][0] 是 int* 类型
函数指针:
函数名是什么:
函数就是一段具有某项功能的代码,它会被编译器编译成二进制指令存储在text内存段,函数名就是它在text内存段的首地址。
#include <stdio.h>
void func(void)
{
printf("我是函数func,我被调用了...\n");
}
int main()
{
printf("%p\n",func);
int num = 0x804843b;
((void(*)(void))num)();
}
什么是函数指针:
专门存储函数地址的指针变量叫函数指针。
定义函数指针:
1、先确定指向的函数的格式(函数声明)。
2、照抄函数声明。
3、用小括号包含函数名。
4、在函数名前加*
5、在函数名末尾加_fp,防止命名冲突。
6、用函数名给函数指针赋值后,函数指针就可以当作函数调用了。
7、函数指针变量在赋值时往往需要通过显示调用&函数名来获得地址,但往往可以省略不写。
#include <stdio.h>
void func(void)
{
printf("我是函数func,我被调用了...\n");
}
int main()
{
void (*func_fp)(void) = func;
func_fp();
}
函数指针的用处:
//区分指针函数与函数指针,根据使用的位置以及括号
int* Add(int a,int b); //声明一个返回值是int类型指针的函数
int (*Add)(int,int); //声明一个函数指针
函数指针可以让函数像数据一样在函数之间传递。
当我们实现一个数组的排序函数时,那么排序函数内部需要调用数组元素的比较函数,由于我们不知道待排序的数组是什么类型,也就无法自己实现数组元素的比较函数,那么我们可以在排序函数的参数列表中预留一个函数指针,当有人调我们的排序函数时,他就需要提供一个数组元素比较函数供我们调用,排序函数就可以为它的数组进行排序。
函数的这种调用模式就叫回调模式。
函数指针可以实现动态调用,而C++中的模板则是静态的。
void qsort(void *base,
size_t nmemb,
size_t size,
int (*compar)(const void *, const void *));
功能:为数组进行排序
base:数组的首地址
nmemb:数组的长度
size:数组成员的字节数
compar:调用者需要提供的数组元素的比较函数