10W+字C语言硬核总结(四),值得阅读收藏!

简介: 10W+字C语言硬核总结(二),值得阅读收藏!

0.为什么使用指针


假如我们定义了 char a=’A’ ,当需要使用 ‘A’ 时,除了直接调用变量 a ,还可以定义 char *p=&a ,调用 a 的地址,即指向 a 的指针 p ,变量 a( char 类型)只占了一个字节,指针本身的大小由可寻址的字长来决定,指针 p 占用 4 个字节。


但如果要引用的是占用内存空间比较大东西,用指针也还是 4 个字节即可。


使用指针型变量在很多时候占用更小的内存空间。 变量为了表示数据,指针可以更好的传递数据,举个例子:


第一节课是 1 班语文, 2 班数学,第二节课颠倒过来, 1 班要上数学, 2 班要上语文,那么第一节课下课后需要怎样作调整呢?方案一:课间 1 班学生全都去 2 班, 2 班学生全都来 1 班,当然,走的时候要携带上书本、笔纸、零食……场面一片狼藉;方案二:两位老师课间互换教室。


显然,方案二更好一些,方案二类似使用指针传递地址,方案一将内存中的内容重新“复制”了一份,效率比较低。


在数据传递时,如果数据块较大,可以使用指针传递地址而不是实际数据,即提高传输速度,又节省大量内存。


一个数据缓冲区 char buf[100] ,如果其中 buf[0,1] 为命令号, buf[2,3] 为数据类型, buf[4~7] 为该类型的数值,类型为 int ,使用如下语句进行赋值:


*(short*)&buf[0]=DataId;
*(short*)&buf[2]=DataType;
*(int*)&buf[4]=DataValue;

数据转换,利用指针的灵活的类型转换,可以用来做数据类型转换,比较常用于通讯缓冲区的填充。


指针的机制比较简单,其功能可以被集中重新实现成更抽象化的引用数据形式


函数指针,形如: #define PMYFUN (void*)(int,int) ,可以用在大量分支处理的实例当中,如某通讯根据不同的命令号执行不同类型的命令,则可以建立一个函数指针数组,进行散转。


在数据结构中,链表、树、图等大量的应用都离不开指针。


1. 指针强化


1.1 指针是一种数据类型


操作系统将硬件和软件结合起来,给程序员提供的一种对内存使用的抽象,这种抽象机制使得程序使用的是虚拟存储器,而不是直接操作和使用真实存在的物理存储器。所有的虚拟地址形成的集合就是虚拟地址空间。


内存是一个很大的线性的字节数组,每个字节固定由 8 个二进制位组成,每个字节都有唯一的编号,如下图,这是一个 4G 的内存,他一共有 4x1024x1024x1024 = 4294967296 个字节,那么它的地址范围就是 0 ~ 4294967296 ,十六进制表示就是 0x00000000~0xffffffff ,当程序使用的数据载入内存时,都有自己唯一的一个编号,这个编号就是这个数据的地址。指针就是这样形成的。


1.1.1 指针变量


指针是一种数据类型,占用内存空间,用来保存内存地址。


void test01(){
 int* p1 = 0x1234;
 int*** p2 = 0x1111;
 printf("p1 size:%d\n",sizeof(p1));
 printf("p2 size:%d\n",sizeof(p2));
 //指针是变量,指针本身也占内存空间,指针也可以被赋值
 int a = 10;
 p1 = &a;
 printf("p1 address:%p\n", &p1);
 printf("p1 address:%p\n", p1);
 printf("a address:%p\n", &a);
}

1.1.2 野指针和空指针


1.1.2.1 空指针


标准定义了NULL指针,它作为一个特殊的指针变量,表示不指向任何东西。要使一个指针为NULL,可以给它赋值一个零值。为了测试一个指针百年来那个是否为NULL,你可以将它与零值进行比较。


对指针解引用操作可以获得它所指向的值。但从定义上看,NULL指针并未指向任何东西,因为对一个NULL指针因引用是一个非法的操作,在解引用之前,必须确保它不是一个NULL指针。


如果对一个NULL指针间接访问会发生什么呢?结果因编译器而异。 不允许向NULL和非法地址拷贝内存:


void test(){
 char *p = NULL;
 //给p指向的内存区域拷贝内容
 strcpy(p, "1111"); //err
 char *q = 0x1122;
 //给q指向的内存区域拷贝内容
 strcpy(q, "2222"); //err  
}

1.1.2.2 野指针


在使用指针时,要避免野指针的出现:


野指针指向一个已删除的对象或未申请访问受限内存区域的指针。与空指针不同,野指针无法通过简单地判断是否为 NULL避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。


什么情况下会导致野指针?


指针变量未初始化


任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。


指针释放后未置空


有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。


指针操作超越变量作用域


不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。


void test(){

int* p = 0x001; //未初始化

printf("%p\n",p);

*p = 100;

}

操作野指针是非常危险的操作,应该规避野指针的出现:


初始化时置 NULL


指针变量一定要初始化为NULL,因为任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的。


释放时置 NULL


当指针p指向的内存空间释放时,没有设置指针p的值为NULL。delete和free只是把内存空间释放了,但是并没有将指针p的值赋为NULL。通常判断一个指针是否合法,都是使用if语句测试该指针是否为NULL。


1.1.2.3 void*类型指针


void是一种特殊的指针类型,可以用来存放任意对象的地址。一个void指针存放着一个地址,这一点和其他指针类似。不同的是,我们对它到底储存的是什么对象的地址并不了解。


double a=2.3;
int b=5;
void *p=&a;
cout<<p<<endl;   //输出了a的地址
p=&b;
cout<<p<<endl;   //输出了b的地址
//cout<<*p<<endl;这一行不可以执行,void*指针只可以储存变量地址,不可以直接操作它指向的对象

由于void是空类型,只保存了指针的值,而丢失了类型信息,我们不知道他指向的数据是什么类型的,只指定这个数据在内存中的起始地址,如果想要完整的提取指向的数据,程序员就必须对这个指针做出正确的类型转换,然后再解指针。


1.1.2.4 void*数组和指针


同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝


数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的。指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。


数组所占存储空间的内存:sizeof(数组名) 数组的大小:sizeof(数组名)/sizeof(数据类型),在32位平台下,无论指针的类型是什么,sizeof(指针名)都是 4 ,在 64 位平台下,无论指针的类型是什么,sizeof(指针名)都是 8 。


数组名作为右值的时候,就是第一个元素的地址


int main(void)
{
    int arr[5] = {1,2,3,4,5};
    int *p_first = arr;
    printf("%d",*p_first);  //1
    return 0;
}

指向数组元素的指针 支持 递增 递减 运算。p= p+1意思是,让p指向原来指向的内存块的下一个相邻的相同类型的内存块。在数组中相邻内存就是相邻下标元素。


1.1.3 间接访问操作符


通过一个指针访问它所指向的地址的过程叫做间接访问,或者叫解引用指针,这个用于执行间接访问的操作符是*。


注意:对一个int类型指针解引用会产生一个整型值,类似地,对一个float指针解引用会产生了一个float类型的值。


int arr[5];

int *p = * (&arr);

int arr1[5][3] arr1 = int(*)[3]&arr1

1)在指针声明时,* 号表示所声明的变量为指针


2)在指针使用时,* 号表示操作指针所指向的内存空间


*相当通过地址(指针变量的值)找到指针指向的内存,再操作内存


*放在等号的左边赋值(给内存赋值,写内存)


*放在等号的右边取值(从内存中取值,读内存)


//解引用
void test01(){
 //定义指针
 int* p = NULL;
 //指针指向谁,就把谁的地址赋给指针
 int a = 10;
 p = &a;
 *p = 20;//*在左边当左值,必须确保内存可写
 //*号放右面,从内存中读值
 int b = *p;
 //必须确保内存可写
 char* str = "hello world!";
 *str = 'm';
 printf("a:%d\n", a);
 printf("*p:%d\n", *p);
 printf("b:%d\n", b);
}

1.1.4 指针的步长


指针是一种数据类型,是指它指向的内存空间的数据类型。指针所指向的内存空间决定了指针的步长。指针的步长指的是,当指针+1时候,移动多少字节单位。


思考如下问题:


int a = 0xaabbccdd;
unsigned int *p1 = &a;
unsigned char *p2 = &a;
//为什么*p1打印出来正确结果?
printf("%x\n", *p1);
//为什么*p2没有打印出来正确结果?
printf("%x\n", *p2);
//为什么p1指针+1加了4字节?
printf("p1  =%d\n", p1);
printf("p1+1=%d\n", p1 + 1);
//为什么p2指针+1加了1字节?
printf("p2  =%d\n", p2);
printf("p2+1=%d\n", p2 + 1);

1.1.5 函数与指针


1.1.5.1 函数的参数和指针


C语言中,实参传递给形参,是按值传递的,也就是说,函数中的形参是实参的拷贝份,形参和实参只是在值上面一样,而不是同一个内存数据对象。这就意味着:这种数据传递是单向的,即从调用者传递给被调函数,而被调函数无法修改传递的参数达到回传的效果。


void change(int a)
{
    a++;      //在函数中改变的只是这个函数的局部变量a,而随着函数执行结束,a被销毁。age还是原来的age,纹丝不动。
}
int main(void)
{
    int age = 60;
    change(age);
    printf("age = %d",age);   // age = 60
    return 0;
}

有时候我们可以使用函数的返回值来回传数据,在简单的情况下是可以的,但是如果返回值有其它用途(例如返回函数的执行状态量),或者要回传的数据不止一个,返回值就解决不了了。


传递变量的指针可以轻松解决上述问题。


void change(int* pa)
{
    (*pa)++;   //因为传递的是age的地址,因此pa指向内存数据age。当在函数中对指针pa解地址时,
               //会直接去内存中找到age这个数据,然后把它增1。
}
int main(void)
{
    int age = 160;
    change(&age);
    printf("age = %d",age);   // age = 61
    return 0;
}

比如指针的一个常见的使用例子:


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void swap(int *,int *);
int main()
{
    int a=5,b=10;
    printf("a=%d,b=%d\n",a,b);
    swap(&a,&b);
    printf("a=%d,b=%d\n",a,b);
    return 0;
}
void swap(int *pa,int *pb)
{
    int t=*pa;*pa=*pb;*pb=t;
}

在以上的例子中,swap函数的两个形参pa和pb可以接收两个整型变量的地址,并通过间接访问的方式修改了它指向变量的值。在main函数中调用swap时,提供的实参分别为&a,&b,这样就实现了pa=&a,pb=&b的赋值过程,这样在swap函数中就通过pa修改了 a 的值,通过pb修改了 b 的值。因此,如果需要在被调函数中修改主调函数中变量的值,就需要经过以下几个步骤:


定义函数的形参必须为指针类型,以接收主调函数中传来的变量的地址;


调用函数时实参为变量的地址;


在被调函数中使用*间接访问形参指向的内存空间,实现修改主调函数中变量值的功能。


相关文章
|
机器人 Linux C语言
C语言, C++ IO 总结. 一篇文章帮你透析缓冲区存在的意义, C, C++ IO的常见用法
C语言, C++ IO 总结. 一篇文章帮你透析缓冲区存在的意义, C, C++ IO的常见用法
C语言, C++ IO 总结. 一篇文章帮你透析缓冲区存在的意义, C, C++ IO的常见用法
|
C语言
c语言实现三子棋(内含阅读思路,简单易实现)
本文如果按顺序来阅读可能不太好接受,建议阅读顺序为,由test.c的逻辑顺序读下去,遇见具体函数的实现跳转到game.c中来理解
147 0
c语言实现三子棋(内含阅读思路,简单易实现)
|
存储 自然语言处理 算法
C语言学习前五章思维导图式总结(超详细,复习必备)
C语言学习前五章思维导图式总结(超详细,复习必备),源文件在 process  on(在线流程图)上面,同名,需要多多支持。
1039 1
C语言学习前五章思维导图式总结(超详细,复习必备)
|
算法 C语言
C语言第五章:循环结构程序设计总结。(超详细)
C语言第五章:循环结构程序设计总结。(超详细)
398 0
C语言第五章:循环结构程序设计总结。(超详细)
|
存储 人工智能 C语言
C语言第二章 数据类型,运算符和表达式总结【完美补充文字版】(超级详细)
C语言第二章 数据类型,运算符和表达式总结【完美补充文字版】(超级详细)
520 0
C语言第二章 数据类型,运算符和表达式总结【完美补充文字版】(超级详细)
|
存储 编译器 C语言
C语言专业总结(六)
C语言专业总结(六)
87 0
|
存储 编译器 C语言
C语言专业总结(五)
C语言专业总结(五)
69 0
|
搜索推荐 编译器 C语言
C语言专业总结(四)
C语言专业总结(四)
78 0
|
机器学习/深度学习 算法 编译器
C语言专业总结(三)
C语言专业总结(三)
100 0
|
C语言
C语言专业总结(二)
C语言专业总结(二)
134 0