指针
编号即为地址即为指针,存放指针的变量成为:指针变量
int main()
{
int a = 10;
int* pa = &a; //拿到的是a的四个字节中的第一个字节的地址
*pa = 20;
return 0;
}
各类型指针
不同类型的指针大小相同
int main()
{
printf("%d\n", sizeof(char*));//4
printf("%d\n", sizeof(short*));//4
printf("%d\n", sizeof(int*));//4
printf("%d\n", sizeof(double*));//4
return 0;
}
指针类型的意义:
1.指针类型决定了:指针引用的权限有多大(int*指针可以访问操作四个字节,char*指针可以访问操作一个字节)
2.指针类型决定了,指针走一步,能走多远(步长)
野指针
概念:野指针就是指针指向的位置是不可知的(随机的,不确定的)
1.指针未初始化
int main()
{
int* p;//p是一个局部的指针变量,局部变量不初始化的话,默认时随机值
int* p = NULL;//不知道该初始化什么地址时先初始化为NULL,和int a=0;类似
*p = 20;//非法访问内存了
return 0;
}
2.指针访问越界
c语言本身不会检查数组是否越界
int main()
{
int arr[10] = { 0 };
int *p = arr;
int i = 0;
for (i - 0; i <= 11; i++)
{
//当指针指向的范围超过数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
3.指针指向的内存空间释放
int* test()
{
int a = 10;
return &a;//返回时内存就被释放了
}
int main()
{
int* p = test();
*p = 20;
return 0;
}
如何规避野指针:
1.指针初始化
2.小心指针越界
3.指针指向空间释放及时置NULL
4.指针使用之前检查有效性
//-----指针使用之前检查有效性
int main()
{
int* p = NULL;
/**p = 10;*///会报错,指针置为NULL时的内存用户不能对其进行操作
//修改
if (p != NULL)
{
*p = 10;
}
return 0;
}
指针的运算
指针+-整数
int main()
{
#define N_VALUES 5
float values[N_VALUES];
float *vp; //指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0;//指针+-整数
}
return 0;
}
int main()
{
#define N_VALUES 5
float values[N_VALUES];
float *vp; //指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0;//指针+-整数
}
//************************
for (vp = &values[0]; vp > &values[0];)
{
*--vp = 0;
}
for (vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
*vp = 0;
}
第二种写法应该尽量避免,不符合标准规定
标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的内存位置的指针比较,但是不允许指向第一个元素之前的那个内存位置的指针进行比较。
指针-指针
int main()
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
printf("%d\n", &arr[9] - &arr[0]);
return 0;
}
指针相减的前提:两个指针指向同一块空间
补充:
指针+指针无意义,类比日期+日期无意义
指针+/-数字有意义,类比日期+/-天数有意义
日期-日期有意义,类比日期-日期有意义
指针和数组
数组名是数组元素的地址
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%p <==> %p\n", &arr[i], p + i);//arr是首元素的地址,所以p+i就是这个数组下标为i的元素的地址
}
return 0;
}
打印
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d", *(p + i));
}//0 1 2 3 4 5 6 7 8 9
return 0;
}
拓展
int main()
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* p = arr;
//[]是一个操作符 2和arr是两个操作数 a+b=b+a加法交换律
//arr[2]<==>*(arr+2)<==>*(p+2)<==>*(2+p)<==>*(2+arr)==2[arr]
//2[arr]<==>*(2+arr)
printf("%d\n", 2[arr]);
printf("%d\n", arr[2]);
//本质arr[2]-->*(arr+2)-->*(2+arr)-->2[arr]
return 0;
}
二级指针
一级指针就是指向变量的指针,二级指针就是指向以及指针的指针,存放一级指针的地址
int main()
{
int a = 10;
int* pa = &a;//pa是指针变量,一级指针//*表明是指针,int表明目标的类型
//ppa就是一个二级指针变量
int** ppa = &pa;//pa也是个变量,&pa取出pa在内存中起始地址//*表示是指针,int*表明目标的类型
return 0;
}
指针数组
存放指针的数组
int main()
{
int arr[10];//整型数组 - 存放整形的数组就是整型数组
char ch[5];//字符数组 - 存放的是字符
//指针数组 - 存放指针的数组
int* parr[5];//整型指针的数组
char* pch[5];//字符型指针的数组
return 0;
}
结构体
结构体是什么?类比于数组之间的不同,数组是类型相同的元素的集合,结构体是类型不同的元素的集合。 主要用于描述复杂对象,比如描述一个学生 。
struct Stu //是个类型
{
//成员变量
char name[20];
int age;
char id[20];
}s1,s2;//这里创建的s1,s2也是结构体变量,是全局变量
int main()
{
struct Stu s;//由struct Stu类型创建的s对象//s是局部变量
return 0;
}
结构体成员的类型很多,可以是标量、数组、指针,甚至是其他结构体,当成员为其他结构体时的初始化和成员访问
struct B
{
char c;
short s;
double d;
};
struct Stu //是个类型
{
//成员变量
struct B sb;
char name[20];
int age;
char id[20];
}s1, s2;//这里创建的s1,s2也是结构体变量,是全局变量
int main()
{
struct Stu s;//由struct Stu类型创建的s对象//s是局部变量
struct Stu s = { { 'w', 20, 3.14 }, "张三", 30, "202005034" };//初始化
//用.和->操作符访问成员
printf("%c\n", s.sb.c);//打印W
struct Stu* ps = &s;
printf("%c\n", (*ps).sb.c);
printf("%c\n", ps->sb.c);//ps是指针可以直接用->访问成员,sb不是指针所以要用.访问成员
return 0;
}
传值调用和传址调用的对比
写一个打印函数打印s的内容
struct B
{
char c;
short s;
double d;
};
struct Stu //是个类型
{
//成员变量
struct B sb;
char name[20];
int age;
char id[20];
}s1, s2;//这里创建的s1,s2也是结构体变量,是全局变量
void print1(struct Stu t)
{
printf("%c %d %lf %s %d %s\n", t.sb.c, t.sb.s, t.sb.d, t.name, t.age, t.id);
}
void print2(struct Stu* ps)
{
printf("%c %d %lf %s %d %s\n", ps->sb.c, ps->sb.s, ps->sb.d, ps->name, ps->age, ps->id);
}
int main()
{
struct Stu s = { { 'w', 20, 3.14 }, "张三", 30, "202005034" };//初始化
//写一个打印函数打印s的内容
print1(s); //传值调用 传参是会开辟空间,造成浪费性能下降
print2(&s); //传址调用 仅仅传输地址,效率高 首选
return 0;
}
采用传址调用比传值调用好,节省资源开销,因为传参类似于压栈操作,具体原理如图
结论:结构体传参的时候,要传结构体的地址