(笔记有很多在代码注释里)
目录
1.指针是什么
1.1指针概念
红色标注的0x00000400代表内存地址,绿色37,30代表数据
指针是个变量,存放内存单元的地址(编号),即0x00000400
1.2指针的大小
在32位平台是四个字节,在64位平台是八个字节
2.指针和指针类型
字节大小与地址编号与指针类型无关,如
int main()
{
printf("%p\n",sizeof( int*));
printf("%p\n", sizeof( double*));
printf("%p\n", sizeof( float*)); //运行结果都是8(我的电脑为64位操作台)
int a = 1;
int* pa1 = &a;
int* pa2 = &a;
printf("%p\n", pa1);
printf("%p", pa2);//运行结果都为00000069435CF614
return 0;
}
指针类型的意义在于操作指针(比如进行解引用,或进行加减时)时,不同类型的指针,操作的字节个数不一样。
2.1指针类型与解引用
不同类型的指针,在进行*p=1等操作时,所访问的内存字节个数不一样
int main()
{
int a = 0x66778899;
int* pa = &a;
char b = 0x11223344;
char* pb = &b;
*pa = 0;//内存中四个字节全部会变成00
*pb = 1;//内存中只有一个字节会变成01
return 0;
}
下面是具体的变化, 让我们对这段代码进行逐句调试,
先找出去a与b的地址,然后看这两个内存中值的变化,可以看出,运行完整个程序后,a地址中的值由
变成了
而b地址中的值由
变成了
可见指针类型决定了指针进行解引用操作的时候,能够访问的空间大小。
int*p,*p能够访问四个字节
char*p,*p能够访问一个字节
double*p,*p能够访问八个字节
2.2指针类型与运算
指针类型决定了:指针走一步走多远(指针的步长)
int main()
{
int a = 0x66778899;
char c = 'c';
int* pa = &a;
char* pc = &c;
printf("%p\n", pa); //结果为000000866BCFF544
printf("%p\n", (pa+1)); //结果为000000866BCFF548(加了四)
printf("%p\n", pc); //结果为000000866BCFF564
printf("%p\n", (pc+1)); //结果为000000866BCFF565(加了一)
return 0;
}
int*p:p+1---->4;
char*p:p+1--->1
double*p:p+1--->8
3.野指针
概念:野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)
3.1 野指针的成因
3.1.1指针未初始化
#include<stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p=20;
return 0;
}
3.1.2 指针越界访问
多出现于数组,超出所定义数组范围是便为野指针(如int a[10] ; int*pa=&a[10]便为野指针)
#include<stdio.h>
int main()
{
int a[10] = { 0 };
int *p = a;
for(int i=0;i<=11;i++)
{
*(p++) = 1; //当指针指向的范围超过数组a的范围时,p就是野指针
}
return 0;
}
3.1.3指针指向的空间释放
多出现于函数(函数中的变量为局部变量,会在函数结束时被销毁,所在地址会被内存回收,变为无主地址,再次访问时便是非法访问)
#include<stdio.h>
int* egg()
{
int a = 1; //a为局部变量,会在函数结束时被销毁,
//所在地址会被内存回收,变为无主地址
return &a;
}
int main()
{
int* p = egg();//此地址已不属于我们,
//现在的访问为非法访问
return 0;
}
3.2如何规避野指针
3.2.1记得把指针初始化
int*p=NULL //初始化指针,给指针赋值
3.2.2小心指针越界
使用数组指针时注意范围
3.2.3指针指向空间释放就置NULL
3.2.4指针使用之前检查有效性
int a=10;
int*pa=&a;
if(pa!=NULL) //先检查再使用
{
}
4.指针的运算
4.1指针+-整数
确定类型后,*pa++与pa++结果相同,都为将地址编码从&a[i]变为&a[i+1]
(但是printf时*pa输出的是对应数组值,而pa输出的是地址编号)
举个例子:
#include<stdio.h>
int main()
{
int a[10] = { 1,2,3,4,5,6,7,8,9,0 };
int* pa = a;
int size = sizeof(a) / sizeof(a[0]);
for(int i=0;i<size;i++)
{
printf("%d\t", *pa);
//若是printf("%d\t",pa)则会输出地址编号
pa++;
//或*pa++
}
return 0;
}
// 运行结果为1 2 3 4 5 6 7 8 9 0
pa++与*pa++运行结果均为
而printf("%d\n", pa)结果为
4.2指针-指针
结果为两个指针之间元素的个数
#include<stdio.h>
int main()
{
int a[10] = { 1,2,3,4,5,6,7,8,9,0 };
printf("%d", &a[9] - &a[0]);
//若&a[0]-&a[9]的话运行结果为-9
//若char b[5];
//&b[0]-&a[4]类型不同会报错
return 0;
}
用这个原理可自制strlen函数
#include<stdio.h>
int str_len(char b[]) //自制的用于测量数组长度的函数str_len
{
char* start =b; //获取数组第一个元素的地址编号
char* end=b;
for(int i=0;*end!='\0'; i++)
{
end++;
} //获取数组第二个元素的地址编号
return end - start;
}
int main()
{
char a[] ="helloworld";
int len = str_len(a); //运行结果与strlen()一样
printf("%d", len);
return 0;
}
4.3指针的关系运算
指针可以比较大小,a[i+1]的指针大于a[i]的指针
【注意】标准规定:允许数组元素的指针与与指向数组最后一个元素的后面那个指针进行比较,但是不允许与指向第一个元素之前的那个内存进行比较
如上图,p1可以和p2比较,但不能和p3比较
5.指针与数组
5.1数组名的意义
绝大多数时候,数组名代表着首元素地址,比如下面两个printf输出语句的结果相同
#include<stdio.h>
int main()
{
int a[10];
printf("%p", a);
printf("%p", &a[0]);
return 0;
}
除了两个例外:
1.&a-&数组名-此时数组名不代表首元素地址-数组名表示整个数组-&数组名-取出的是整个数组的地址
2.sizeof(a)-sizaof(数组名)-数组名表示整个数组-sizeof(数组名)计算的是整个数组的大小
#include<stdio.h>
int main()
{
int a[4];
printf("%p\n", a);
printf("%p\n", a+1); //与上面比增加了8
printf("%p\n", &a);
printf("%p\n", &a+1); //与上面比增加了32
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(a[0]));
return 0;
}
这段代码的输出结果如图
注意:指针数组与数组指针的区别,指针数组是元素全为指针的数组;数组指针是数组的指针
定义一维指针数组的一般形式为:
类型名 *数组名[数组长度]
不要写成
int(*p)[] //这是不对的
类型名中应包括符号“*”,如“int*”表示指向整型数据的数组类型
指针数组举例
#include<stdio.h>
int main()
{
int a = 1;
int b = 2;
int c = 3;
int* d[3] = { &a,&b,&c };//定义指针数组
for (int i=0;i<3;i++)
{
*(d[i]) = i - 1; //使用指针数组
}
printf("a=%d,b=%d,c=%d", a, b, c);
return 0;
}
5.2通过指针操作数组
通过指针可以改变与输出数组中的值
#include<stdio.h>
int main()
{
int a[10] = { 0 };
int* pa = a;
for (int i = 0; i < 10; i++)
*(pa + i) = i;
//通过指针改变数组中的值
for (int i = 0; i < 10; i++)
printf("%d\n", *(pa + i));
//通过指针输出数组中的值
return 0;
}
5.3指针与二维数组
二维数组和一维数组一样,都是分配连续的地址来存储的数据的
二维数组与指针的操作也和一维数组一样
#include<stdio.h>
int main()
{
int a[2][2] = { {1,2},{3,4} };
printf("%p\n", a);
//二维数组的数组名也代表首元素地址
int* b[4] = { &a[0][0],&a[0][1],&a[1][0],&a[1][1] };
for (int i = 0; i < 4; i++)
printf("%p\n", b[i]);
//看运行结果,可知二维数组的地址编码由左到右,由上至下依次增大
return 0;
}
6.二级指针
指向指针的指针,称为二级指针
int a=10
int*pa=&a;
int** ppa=&pa; //ppa就是二级指针
7.通过指针引用字符串
7.1 字符串的引用
(1)用字符数组存放一个字符串,可以通过数组名和下标应用一个字符串中的一个字符,也可以通过数组名和格式声明“%s”输出该字符串。
#include<stdio.h>
int main()
{
char string[] = "helloworld"; //定义字符串string
printf("%s\n", string); //用%s格式输出整个字符串
printf("%c", string[1]); //用%c格式输出一个字符数组元素
return 0;
}
(2)用字符指针变量指向一个字符串常量,通过字符指针变量引用
#include<stdio.h>
int main()
{
char string[] = "hello";
char* pstring = string; //定义指针并初始化
printf("%s", *pstring); //输出字符串
return 0;
}
7.2字符指针作函数参数
如果想把一个字符串从一个函数传递到另一个函数,可以用地址传递的方式
7.3字符指针变量和字符数组
(1)赋值方式:可以对字符指针变量赋值,但不能对数组名赋值
char*a;
a="hello"; //此程序将字符串首元素地址赋值给指针变量,合法
char str[14];
str[0]='h'; //给字符数组元素赋值,合法
str='hello'; //数组名是地址,是常量,不能被赋值,非法
(2)储存单元的内容:编译时为字符数组分配若干储存单元,以存放各数组的值,而对数组指针变量,只分配一个储存单元
(3)指针变量的值是可以改变的,而字符数组名代表一个固定的值(数组首元素地址),不能改变
(4)字符数组中各元素的值是可以改变的(可以对它们再赋值),但字符串指针变量指向的字符串常量中的内容是不可以被取代的(不能对它们再赋值)
char a[]="hello"; //字符数组a初始化
char *b="hello"; //字符指针变量b指向字符串常量的第一个字符
a[2]='r'; //合法,'r'取代啊a[2]原值‘l’
b[2]='r'; //非法,字符串常量不能改变
(5)引用数组元素:可用下标法(用数组名和下标,如a[2]),也可以用地址法(如*(a+2))。若定义了字符指针变量p,并使它指向数组a的首元素,则可以用指针变量带下标的形式引用数组元素(如p[2]),同样,可以用地址法(如*(p+2))引用数组元素a[2]
注意:若指针变量没有指向数组,此时输出p[2]或*(p+2)系统会输出指针变量p所指的字符后面5个字节的内容。
(6)用指针变量指向一个格式字符串,可以用它代替printf函数中的格式字符串
char*format;
format="a=%d,b=%d";
printf(format,a,b); //它相当于printf("a=%d,b=%d",a,b);
8.指向函数的指针
8.1什么是函数的指针
函数名就是函数的指针,它代表函数的起始地址
int(*p)(int,int);
类型名(*指针变量名)(函数参数系列);
8.2用函数指针调用函数
#include<stdio.h>
int main()
{
int a,b;
printf ("请输入:");
scanf_s("%d %d", &a, &b);
int max(int, int); //函数声明
int(*p)(int, int); //定义指向函数的指针p
p = max; //使p指向max函数
int c = ( * p)(a, b); //通过指针调用函数
printf("max=%d",c);
return 0;
}
int max(int a, int b)
{
if (a > b)
return a;
else
return b;
}
8.3用指向函数的指针作函数参数
指把函数的入口地址作为参数传递到其他函数,这样就能够在被调用的函数中使用实参函数
void arr(int(*a1)(int),int(*a2)(int,int)); //定义arr函数,形参是指向函数的指针变量
{
int a,b,i=1,j=2;
a=(*a1)(i); //调用*a1指向的函数
b=(*a2)(i,j); //调用*a2指向的函数
}
8.4返回指针值的函数
其一般形式为:
类型名 *函数名(参数表列)
8.5动态内存分配于指向它的指针变量
8.5.1什么是内存的动态分配
(1)全局参量分配在内存的静态储存区
(2)局部变量分配在内存的动态储存区,叫“栈”
(3)除此之外,C语言还允许建立内存动态分配区域,以存放一些临时用的数据,叫做“堆”
这些数据不必提前声明,也不必等到函数结束才释放,它们随时开辟随时释放
由于未提前声明,只能用指针引用
8.5.2怎么建立内存的动态分配
(1)malloc:
函数原型为:
void* malloc (unsigned int szie);
int size 类型为无符号整型(不允许为负数)
作用为在动态储存区中分配一个长度为size的连续空间,并返回所分配区域的第一个字节的地址;若内存不足开辟失败,则返回NULL
(2)calloc
函数原型为:
void *calloc(unsigned n,unsigned size);
作用为在动态储存区中分配n个长度为size的连续空间,一般为一维数组开辟动态储存空间
n为元素个数,size为元素长度
返回值同malloc
(3)realloc
函数原型为:
void *realloc(void*p,unsigned int size);
若已经用malloc于calloc分配了空间,可用realloc改变其大小
若重新分配不成功,返回NULL
(4)free
函数原型为:
void free(void*p);
作用为释放指针p变量所指向的空间
p应是最后一次调用malloc,calloc时得到的返回值
8.6void指针类型
定义一个基类型为void的指针变量(即void*型变量)
该变量不指向任何类型的数据,即指向空类型
9.参考资料
c程序设计(第五版)(谭浩强)
bilibili c语言编程学习 发布的视频
知乎用户 无际单片机 发表的文章