什么是指针:
1、指针是一种数据类型,使用它可以定义指针变量,简称指针。
2、指针变量中存储的是内存地址(整数,便签序号),可以通过它访问对应的内存
3、以32位系统为例,指针变量的需要占用4字节内存空间,取值范围是:0x00000000~0xffffffff,每个整数代表一个字节,最多可以访问4G内存(64位系统的指针变量则需要占用8字节内存空间)。
4、可以使用printf+%p 显示指针变量的值。
如何使用指针:
定义指针变量:类型* 指针变量名;
1、指针变量与普通变量的使用方法不同,因此为区别与普通变量,一般指针变量以p结尾。
2、指针变量不能连续定义,一个*只能定义出一个指针变量。
int* p1,p2; // p1是指针变量,p2普通的int类型变量
int *p1,*p2; // p1,p2都是指针变量
3、指针变量中存储的整数只代表一个字节,当通过指针变量访问内存时,具体访问多少个字节由指针变量的类型决定,也就是说指针变量中存储的是一块内存的首地址。
4、指针变量的默认值与普通变量一样是随机的(野指针),一般为了安全要给指针变量初始化,如果没有合适的内存地址可以先初始化NULL(空指针)。
给指针变量赋值:指针变量 = 内存地址;
// 定义指针变量时初始化
int* p = # // 获取变量的地址赋值给指针变量,num变量的类型要与指针变量相同,否则编译器会产生警告,在解引用时也可能产生段错误
// 定义完指针变量后再赋值
p = malloc(4); // 把堆内存的地址赋值给指针变量,堆内存的使用后续会详细讲解
根据指针变量中存储的内存地址访问内存(解引用):*指针变量
int num = 1234;
int* p = #
*p <=> num; // *p 等价于 num
指针变量解引用时,具体访问多个少字节数由指针变量的类型决定:
char* p; // *p 访问一个字节
short* p; // *p 访问两个字节
double* p; // *p 访问4个字节
指针变量解引用时可能出现段错误的原因:
1、指针变量中存储的是无效的内存地址(不在maps文件里的地址范围内)。
2、指针变量中存储的是具有只读权限的内存地址,通过指针变量修改内存时也会产生段错误。
const int num = 1234; // num使用的是代码段内存
int main()
{
int* p = (int*)#
*p = 6666; // 强行修改只读权限的内存,会产生段错误
}
3、指针变量中存储的是NULL,解引用时肯定产生段错误。
总结:当指针变量解引用时产生段错误,要在指针变量赋值时改正。
为什么要使用指针:
1、使用指针可以在函数之间共享变量
使用全局变量可以在函数之间共享变量,但使用全局变量容易造成命名冲突,并且在程序运行期间全局变量所使用的data、bss、text内存不会被释放,这多使用全局变量容易造成内存浪费,尽量少使用全局变量,最好不用。
当函数需要返回多个执行结果时,需要在函数之间共享变量,也就是共享变量地址,例如:scanf函数。
2、适当的使用指针可以提高函数的传参效率
c语言的函数传参是值传递(赋值、内存拷贝),比如:double,long double,long long,自定义类型(结构、联合、类),它们的字节数 > 4,如果直接传递变量的值则最少需要拷贝8字节内存数据,而传递变量的地址,只需要拷贝4字节内存数据。
// 以下代码传递变量的地址比直接传递变量的值要节约一半的时间
void func(long double* p)
{
}
int main()
{
long double f = 3.14;
for(int i=0; i<2000000000; i++)
{
func(&f);
f+=1;
}
}
3、使用堆内存必须与指针变量配合
当 int num; 语句时,系统会分配4字节内存(text、data、bss、stack),并让这4字节内存与变量名num建立联系,在之后的代码中使用num就相当于4字节内存,我们把这种功能叫做给内存取名字。
而堆内存无法取名字,当向系统申请一块内存时,系统会返回这块内存的首地址,这块内存无法与变量名建立联系,也就是无法取名字,需要指针变量存储内存的地址以便之后使用这块堆内存,所以说堆内存必须与指针变量配合使用。
指针的运算:
指针变量中存储的是代码内存地址的整数,理论所有整数能进行的运算,指针变量也能够运算,但只有以下三种运行对指针变量来说是有意义:
注意:指针变量的进步值就是指针变量解引用时所访问的字节数。
指针+n => 指针变量中的整数+进步值*n
int* p = 0;
p+3 得到的结果是12
指针-n => 指针变量中的整数-进步值*n
short* p = 8;
p - 3 得到的结果2
指针-指针 => (指针变量中的整数-指针变量中的整数)/进步值
double *p1 = 36 ,*p2 = 20;
p1 - p2 得到的结果是2
指针变量加减整数相当于指针变量前后移动,指针减指针可以计算出两地址之间相隔多个对象(两个指针变量的类型必需相同)。
使用指针要注意的问题:
空指针:值为NULL的指针变量叫空指针,系统规定不能对空指针解引用,否则就会产生段错误。
如何避免空指针产生的段错误:对来历不明的指针解引用前,先判断是否是空指针。
1、函数的返回值如果是指针类型,当函数执行出错、失败时会返回空指针。
2、当函数的参数是指针时,我们无法保证调用者传递的指针是否是空指针,所以在解引用前要先判断。
注意:大多数系统的NULL是0地址,但也有少数系统的NULL是1地址,所以判断空指针时
if(!p) // 不通用,容易让人误会p是布尔类型
{
}
if(p == NULL) // 当少写一个=时,编译器不会报错,就变成赋值NULL指针了,接下来肯定会产生段错误
{
}
if(NULL == p) // 完美
{
}
野指针:指针变量中存储的地址不知道是否"合法",这种指针叫野指针。
对野指针解引用可能产生的后果:
1、段错误
2、脏数据
3、一切正常
无法判断一个指针变量是否是野指针,并且野指针产生的错误具有隐藏性、潜伏性、随机性,所以野指针比空指针的危害更大。
如何避免野指针产生的危害:
前提:所有野指针都是人为制造出来的,所以只要不制造野指针就能避免野指针产生的危害。
1、定义指针变量时一定要初始化,宁可赋值为NULL,也不要产生野指针。
2、函数不要返回局部变量的地址,因为随着函数的执行结束,属性局部变量的内存会被释放,,因此这种情况返回的地址就是野指针。
3、当堆内存被释放后,与它配合的指针变量要及时赋值为NULL。
const与指针:
const与指针配合使用一共有5种写法,3种功能:
功能1:保护指针变量不被修改
int* const p;
功能2:保护指针变量指向的内存不被修改
const int* p;
int const * p;
功能3:既保护指针变量也保护指向变量所指向的内存不被修改
const int* const p;
int const * const p;
指针与数组名:
数组名:数组名就是个地址,所以数组作为函数的参数时才能蜕变成指针,它是数组内存块的首地址,也就是第一个元素的内存首地址,所以数组名可以使用解引用的方式访问数组中的每个元素。
int arr[] = {1,2,3,4,5}; // arr就是int*类型的地址
for(int i=0; i<5; i++)
{
printf("%d ",*(arr+i));
}
*(arr+i) <=> arr[i] 这两种写法是等价的。
数组名可以使用指针的语法,指针也可以使用数组名的语法。
int arr[] = {1,2,3,4,5}; // arr就是int*类型的地址
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类型的指针作为函数的参数。
指针函数:
返回值是指针类型的函数,没有什么需要重点关注,主要为了防止别人装13。
函数指针:
前提:一个函数就是一段代码,它会被翻译成二进制指令,存储在text内存段中,函数名就是它在text内存的首地址,我们只要知道函数的地址和函数的类型就能调用这个函数。
函数指针:专门存储函数地址的指针变量。
如何定义函数指针:
1、照抄函数的声明。
2、用小括号包括一个函数名。
3、在函数名前面加*,末尾加fp,防止命名冲突。
如何使用函数指针:
1、用函数名给函数指针赋值。
2、函数指针() 就可以调用函数。
函数指针有什么用:
可以把函数像数据一样在函数之间传递,这样可以做到让旧函数调用新函数,这种函数的调用模式叫回调模式,例如:qsort函数。
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
1、在设计sqort函数时预留一个函数指针的参数compar,在qsort函数内会用compar调用函数,用于比较数组中的每个元素。
2、当调用qsort函数时,调用者需要提数组首地址、数组长度、每个元素的字节数,根据这三个参数把数组以size字节为单位切割成nmemb个元素,然后调用compar对这些元素进行比较,根据比较的结果对数组进行排序。