指针初步认识
内存和地址
在电脑中CPU(中央处理器)在处理数据时,需要的数据是在内存中读取的,内存也分为一个个的内存单元空间,每一个内存单元空间单位是一个字节。
每一个内存单元有一个编号(相当于门牌号),有了这个编号,CPU可以快速找到这个单元储存空间。在计算器中这个编号就是个地址,在C语言中把这个地址又称为指针。
指针变量和地址
在C语言中创建一个变量就是向内存中申请一块空间
#include<stdio.h>
int main()
{
int a=10;
&a;//&是取地址操作符
printf("%p\n",&a);//%p打印地址占位符
return 0;
}
//取出的地址是一个变量需要储存起来就要用到指针变量
int main()
{
int a=10;
int *pa=&a;//取出的地址就放到指针变量pa中
return 0;
}
//pa左边写的是int*,*是说明pa是指针变量,而int说明pa指向的是整型类型的变量
解引用操作符(*)
#include<stdio.h>
int main()
{
int a=10;
int *pa=&a;
*pa=0;//这里*pa就是通过pa中存放的地址找到指向的空间则*pa就是a的变量
//所以*pa=0就是将a的值改为0,pa存放的就是a的地址
return 0;
}
指针变量的大小,在32位机器中设有32根地址总线,每根地址线出来的电信号转换成数字信号就是1或0,那我们把32根地址线产生的2进制序列当作一个地址,那么一个地址就是32bit,需要4个字节来储存,同理64位机器就是64bit,需要8个字节来储存
指针变量类型的意义
指针的解引用
#include<stdio.h>
//代码1
int main()
{
int n=0x11223344;//0x是十六进制的写法,一个十六进制的数字需要4位二进制数字表示
int *pa1=&n;
*pa1=0;
return 0;
}
//代码2
int main()
{
int n=0x11223344;
char *pa2=&n;
*pa2=0;
return 0;
}
都进行调试发现,1会将n中4个字节全部改为0.
2只是将n的第一个字节改为0;
结论:指针的类型决定了对指针解引用的权限(一次性能操作几个字节)。比如char*指针解引用只能访问一个字节,而int*指针能访问4个字节。
指针的+-整数
#include<stdio.h>
int main()
{
int n=10;
int *pa=&n;
char *pc=(char*)&n;//pa和pc存放的就是n变量的地址
printf("%p",&n);
printf("%p",pa);
printf("%p",pa+1);
printf("%p",pc);
printf("%p",pc+1);
//打印出来,char*指针变量+1就跳过了1个字节,而int*指针变量+1就跳过4个字节
return 0;
}
结论:指针类型决定了指针向前或者向后移动的距离大小
const修饰指针
const修饰变量
变量是可以修改的,如果把变量的地址交给指针变量,通过指针变量就可以修改变量,但是如果加上const就加上了限制条件就不能修改
#include<stdio.h>
//代码1
int main()
{
int n=0;
m=20;//m是可以修改的
printf("%d\n",m);
const int m=0;
m=10;//加上const就等于加上了限制,m就不能被修改
printf("%d\n",m);
return 0;
}
//代码2,如果绕过n,使用n的地址去修改n就能做到
int main()
{
const int n=0;
printf("%d\n",n);
int *pa=&n;
*pa=10;
printf("%d\n",n);
return 0;
}
const修是指针变量
#include<stdio.h>
void test1()
{
int n=10;
int m=20;
int*p=&n;
*p=20;
p=&m;
}
void test2()
{
int n=10;
int m=20;
const int *p=&n;
*p=20;//是否能改变
p=&m;//ok?
}
void test3()
{
int n=10;
int m=20;
int *const p=&n;
*p=20;//ok?
p=&m;//ok?
}
void test4()
{
int n=10;
int m=20;
const int *const p=&n;
*p=20;//ok?
p=&m;//ok?
}
int main()
{
//测试无const修饰情况
test1();
//测试const放在*左边情况
test2();
//测试const放在*右边情况
test3();
//测试两边都放const的情况
test4();
return 0;
}
结论:指针变量和指针指向的内容是不同的,const在修饰指针变量的时候
1. 如果const放在*的左侧,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本身的内容是可以的是指针改变的。
2. 如果const放在*的右侧,修饰变量本身,保证了指针变量内容不能改变,但是指针指向的内容,可以通过指针来改变。
指针运算
指针+-整数
例如:数组在内存中是连续存放的知道第一个地址就能知道后面所有元素
#include<stdio.h>
int main()
{
int arr[10]={1,2,3,4,5,6,7,8,9,0};
int *p=&arr[0];
int i=0;
int sz=sizeof(arr)/sizeof(arr[0]);
for(i=0;i<sz;i++)
{
printf("%d ",*(p+1));
}
return 0;
}
指针-指针
#include<stdio.h>
int test(char *s)
{
char *p=s;//取出第一个位置的地址
while(*p!='\0')
p++;
return p-s;//最后一个地址减去第一个地址就知道移动了几个位置也就可以知道有几个字符
}
int main()
{
printf("%d\n",test("abcde"));
return 0;
}
指针的运算关系
例如:打印数组元素
#include<stdio.h>
int main()
{
int arr[10]={1,2,3,4,5,6,7,8,9,10};
int sz=sizeof(arr)/sizeof(arr[0]);
int *p=&arr[0];
while(p<arr+sz)//指针大小比较
{
printf("%d ",*p);
p++;
}
return 0;
}
野指针
概念:野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)
野指针的成因
1.指针未初始化
#include<stdio.h>
int maini()
{
int *p;//没有指出是哪个变量的地址,默认是随机值
*p=20;
return 0;
}
2.指针越界访问
#include<stdio.h>
int main()
{
int arr[10]={0};
int *p=&arr[0];
for(int i=0;i<11;i++)//指针指向的范围超出了数组arr的限定范围就变成了野指针
{
*(p++)=i;
}
return 0;
}
3.指针指向的空间释放
#include<stdio.h>
int *test()
{
int n=100;
return &n;
}
int maain()
{
int *p=test();//局部变量在该函数创建之后,在出函数时变量就被销毁
printf("%d\n",*p);
return 0;
}
如何规避野指针
1.指针初始化
如果不知道指针指向哪里,可以给指针赋值NULL,NULL是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错
#include<stdio.h>
int main()
{
int num=10;
int *p=#
int *p1=NULL;
return 0;
}
2.指针不再使用时,及使用NULL,指针下次使用之前检查有效性
#include<stdio.h>
int main()
{
int arr[10]={1,2,3,4,5,6,7,8,9,10};
int*p=&arr[0];
for(int i=0;i<11;i++)
{
*(p++)=1;
}//此时指针已经越界了,可以把p置为NULL
p=NULL;
//下次使用的时候,判断p不为NULL再使用
p=&arr[0];//重新让p获得地址
if(p!=NULL)
{
//进行使用
}
return 0;
}
assert断言
assert.h头文件定义了assert(),用于在运行时确保程序符合指定条件,如果不符合就报错,对于程序限定了一个条件称为断言。其头文件是#include<assert.h>。
#include<stdio.h>
#include<assert.h>
int main()
{
int a=10;
int *p=&a;
assert(p!=NULL);
printf("%d\n",*p);
return 0;
}
如果确定代码没有问题不需要断言,就可以在#include<stdio.h>语句的前面,定义一个NDEBUG,然后重新编译程序,编译器就会禁用文件中所有的assert()语句。
指针的使用和传址调用
1.指针的使用
例如:strlen的实现,strlen是计算“\0”之前的字符个数
#include<stdio.h>
#include<string.h>
#include<assert.h>
size_t my_str(const char*s)
{
size_t count=0;
assert(s!=NULL);
while(*s!='\0')
{
count++;
s++;
}
return count;
}
int main()
{
char arr[]="zxbcnvm";
size_t ret=my_str(arr);//数组名是首元素的地址
printf("%zd\n",ret);
return 0;
}
2.传值调用和传址调用
例如:写一个函数,交换两个整型变量的值
//代码1
#include<stdio.h>
void Swap(int x,int y)
{
int tmp=0;
tmp=a;
a=b;
b=tmp;
}
int main()
{
int a=10;
int b=20;
printf("%d %d",a,b);//交换前
Swap(a,b);
printf("%d %d",a,b);//交换后
return 0;
}
//代码2
#include<stdio.h>
void Swap(int *pa,int *pb)
{
int tmp=0;
tmp=*pa;
*pa=*pb;
*pb=tmp;
}
int main()
{
int a=10;
int b=20;
printf("%d %d",a,b);//交换前
Swap(&a,&b);
printf("%d %d",a,b);//交换后
return 0;
}
结论:
代码1:实参传给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的改变不影响实参所以不能实现交换。
代码2:其中Swap函数是将变量的地址传给函数,这种函数调用是“传址调用”,可以让函数和主调函数建立真正的联系,在函数内部可以修改主调函数中的变量,编译器在创建变量时,是向计算机内部申请空间,其中就用一个个单位编号的地址来表示,地址是变量的基本表示形式。
数组名
数组名就是首元素的地址
#include<stdio.h>
int main()
{
int arr[10]={1,2,3,4,5,6,7,8,9,10};
printf("&arr[0]=%p",&arr[0]);
printf("arr=%p",arr);
return 0;
}
sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的字节大小。
&数组名,这里的数组名表示整个数组,取出的是整个数组的地址,除此之外,任何地方使用的数组名都是首元素的地址
//代码1
#include<stdio.h>
int main()
{
int arr[10]={1,2,3,4,5,6,7,8,9,10};
printf("%p",arr);
printf("%p\n",&arr);//二者打印的地址是一样的
return 0;
}
//代码2
int main()
{
int arr[10]={1,2,3,4,5,6,7,8,9,10};
printf("%p\n",arr);
printf("%p\n",arr+1);
printf("%p\n",&arr);
printf("%p\n",&arr+1);
return 0;
}
使用指针访问数组
#include<stdio.h>
int main()
{
int arr[10]={0};
int i=0;
int sz=sizeof(arr)/sizeof(arr[0]);
int *p=arr;
for(i=0;i<sz;i++)
{
scanf("%d",p+i);
//scanf("%d",arr+i);
}
for(i=0;i<sz;i++)
{
printf("%d ",*(p+1));
//这里数组名和p是等价的,我们可以用arr[i]来访问数组是否也可以用p[i]
//printf("%d ",p[i]);
}
return 0;
}
一维数组传参的本质
例如:能否用函数来计算元素个数
#include<stdio.h>
void test(int arr[])
{
int sz2=sizeof(arr)/sizeof(arr[0]);
printf("%d\n",sz2);
}
int main()
{
int arr[10]={1,2,3,4,5,6,7,8,9,10};
int sz=sizeof(arr)/sizeof(arr[0]);
printf("%d\n",sz);
test(arr);
return 0;
}
在数组传参的过程中,传递的也是数组名,数组名是首元素传递的也是首元素的的地址,所以在test函数中sizeof(arr),是计算首元素的字节大小,所以函数参数的本质是指针,不能计算数组元素的个数。
#include<stdio.h>
void test(int arr[])//void test(int *arr)
{
printf("%d\n",sizeof(arr));
}
int main()
{
int arr[10]={1,2,3,4,5,6,7,8,9,10};
test(arr);
return 0;
}
二级指针
指针变量也是变量,是变量就会有地址,那么储存指针的变量就叫做二级指针。
#include<stdio.h>
int main()
{
int a=10;
int *p=&a;
int **pp=&p;
return 0;
}
a的变量是10,地址是0x0012ff50
p的变量是0x0012ff50,地址是0x0012ff48
pp的变量是0x0012ff48,地址是0x0012ff40
指针数组
例如:用指针数组模拟二维数组
#include<stdio.h>
int main()
{
int arr1[]={1,2,3,4,5};
int arr2[]={2,3,4,5,6};
int arr3[]={3,4,5,6,7};
int *prr[]={arr1,arr2,arr3};
int i=0;
for(i=0;i<3;i++)
{
int j=0;
for(j=0;j<5;j++)
{
printf("%d ",prr[i][j]);
}
printf("\n");
}
return 0;
}
prr[i]是访问prr数组的元素,prr[i]找到的元素是指向整型一维数组,prr[i][j]就是整形一维数组的元素。
这个代码只是模拟了二维数组,实际上并不是二维数组,因为每一行并不是连续的。
冒泡排序
void stblt(int* arr, int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int flag=1;//假设这一趟已经有序
int j = 0;
for (j = 0; j < sz - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
flag=0;//如果发生交换就说明,存在无序
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
if(flag==1)//如果一趟都没有交换就说明已经有序,无需排序
break;
}
}
int main()
{
int arr[10] = { 2,1,4,5,6,3,7,9,8,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
stblt(arr, sz);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
字符指针变量
通常用char*来表示字符指针
#include<stdio.h>
int main()
{
char ch='w';
char *pc=&ch;//pc就是字符指针
*pc='w';
printf("%c\n",*pc);
return 0;
}
int main()
{
const char*p="zxcvbnm";//并不是把字符串存放在p中而是把第一个字符的地址存放在p中
printf("%c\n",*p);//打印的也是第一个字符
//可以把字符串想象成一个字符数组,但这个数组是不可以修改的
//当常量字符串出现在表达式中,它的值是第一个字符的地址
printf("%c\n",p[3]);
return 0;
}
数组指针变量
int (*p)[10];
p先和*结合,说明p是一个指针变量,然后指向的是一个大小为10个的整形的数组,所以p是一个指针,指向一个数组,叫数组指针。
int :p指向的数组元素是整形;(*p):p是数组指针的变量名;[10]:p指向数组的元素个数
#include<stdio.h>
int main()
{
int arr[10]={0};
int (*p)[10]=&arr;
printf("%p\n",p);
return 0;
}
二维数组传参的本质
二维数组可以看作是每个元素是一维数组的数组,那么二维数组的首元素就是第一行,是一个一维数组,二维数组传参的本质也是传递了地址,传递的是一维数组的地址
//代码1
#include<stdio.h>
void test(int [3][5],int r,int s)
{
int i=0;
for(i=0;i<r;i++)
{
int j=0;
for(j=0;j<s;j++)
{
printf("%d ",arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5]={{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};
test(arr,3,5);
return 0;
}
//代码2
#include<stdio.h>
void test(int (*arr),int r,int s)
{
int i=0;
for(i=0;i<r;i++)
{
int j=0;
for(j=0;j<s;j++)
{
printf("%d ",*(*(arr+i)+j));
}
printf("\n");
}
}
int main()
{
int arr[3][5]={{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};
test(arr,3,5);
return 0;
}
函数指针变量
函数指针变量是用来存放函数的地址的
int (*ps)(int x,int y);
int :ps指向的函数返回类型;ps:函数指针的变量名;(int x,int y):ps指向的函数参数类型
#include<stdio.h>
void Add(int x,int y)
{
return x+y;
}
int main()
{
//打印函数地址
printf("%p\n",Add);
printf("%p\n",&Add);
int (*ps)(int,int)=&Add;//ps就是函数指针变量
inr ret=Add(3,5);
printf("%d\n",ret);
int ret2=(*ps)(4,8);
printf("%d\n",ret2);
int ret3=ps(5,7);
printf("%d\n",ret3);
return 0;
}
typedef关键字
typedef是用来类型的重命名,可以将复杂的类型简单化
#include<stdio.h>
typedef unsigned int niue;
typedef int(*par)[10];//数组指针重命名
typedef int(*pas)(int,int);//函数指针重命名
int main()
{
unsigned int num;
niue num2;//num==num2
int (*p)[10];
par pa;//pa==p;
int (*ps)(int ,int);
pas pf;//pf==pf;
return 0;
}
qsort数组排序函数
其头文件为:#include<stdlib.h>
qsort(void* base, size_t num, size_t width, int(*compare)(const void* key, const void* element));
qsort(
// void* base,//base 指向了要排序的数组的第一个元素
// size_t num, //base指向的数组中的元素个数(待排序的数组的元素的个数)
// size_t size,//base指向的数组中元素的大小(单位是字节)
// int (*compar)(const void*p1, const void*p2)//函数指针 - 指针指向的函数是用来比较数组中的2个元素的
// );
compare函数:比较两个节点类型是否相同。返回值:-1,0,1.
-1:第一个自变量小于第二个
0:两个自变量相同
1:第一个自变量大于第二个自变量
#include<stdio.h>
#include<stdlib.h>
void Print(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int cmp(const void* p1, const void* p2)
{
return *(int*)p1 - *(int*)p2;
}
#include<stdlib.h>
int main()
{
int arr[10] = { 3,4,2,6,1,7,9,0,5,8 };
int sz = sizeof(arr) / sizeof(arr[0]);
Print(arr, sz);
qsort(arr, sz, sizeof(arr[0]), cmp);
Print(arr, sz);
return 0;
}