一、指针
1.1 指针是什么
(1)指针是内存中的一个最小单元的编号,也就是地址。
(2)平时口语中的指针,通常情况下指的是指针变量,用来存放内存地址的变量。
(3)指针变量:里面存放的是指针(也就是地址),通过这个地址可以找到一个对应的内存单元。(地址是唯一标识一块地址空间的)
(4)一个内存单元对应一个字节
(5)指针变量的大小取决于一个地址存放的时候需要多大的空间;(见初始C语言部分)
(6)拓展:因为在32位机器上,地址用二进制表示有32位,在日常的使用当中很不方便,所以为了简要表示,在日常生活中我们会将地址进行十六进制转换,也就是二进制每四位对应十六进制一位,这样我们就可以用八位十六进制代替三十二位二进制,大大减少了复杂程度。(0x是十六进制的标志)
例:0x11223344——0001 0001 0010 0010 0011 0011 0100 0100
1.2 指针与指针类型
int main() {
int a = 0x11223344;//十六进制
int* pa = &a;
*pa = 0;
//结果为:a中11,22,33,44全部都改成了0;
int b = 0x11223344;
char* pc = (char*)&b;
*pc = 0;
//结果为:b中11,22,33,44,只有11变成了00,其余不变
return 0;
}
指针类型的意义一:
指针类型决定了指针在被解引用的时候访问几个字节。如果是int*的指针,则访问4个字节;如果是char*的指针,则访问1个字节;
int main() {
int a = 0x11223344;
int* pa = &a;
char* pc = (char*) & a;
printf(" pa =%p\n", pa); //0000009B091FFB14
printf("pa+1=%p\n", pa+1);//0000009B091FFB18
printf(" pc =%p\n", pc); //0000009B091FFB14
printf("pc+1=%p\n", pc+1);//0000009B091FFB15
return 0;
}
指针类型的意义二:
指针的类型决定了指针进行 + 1和 -1操作的时候(+-整数操作同理),跳过几个字节,即决定了指针的步长。
int main() {
int a = 0, b = 0;
int* pi = &a; //pi解引用访问4个字节,pi+1跳过4个字节;
float* pf = (float*) & b;//pf解引用访问4个字节,pf+1跳过4个字节;
*pi = 100;
*pf = 100.0;
//当上述两个指令运行的时候,最终a和b中的数据并不相同;
//因为int*默认存储整型数据,float*默认存储浮点型数据;
//整型和浮点型数据在内存中的存储上是不同的,对内存的解读方式也是不同的,因此存在差异。
return 0;
}
1.3 野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
1.3.1 指针未初始化
int main() {
int* p;
//p没有初始化,意味着没有明确的指向。
//一个局部变量没有初始化的话,放的是随机值:0xcccccccc
*p = 10;//非法访问内存,这里的指针就是野指针
return 0;
}
1.3.2 数组越界访问
int main() {
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i <= 10; i++) {
*p = i;//会出现数组的越界访问,因为i=10的时候访问i[10],越界了。
p++;
}
return 0;
}
1.3.3 指针指向的内存释放
int* test() {
int a = 10;
return &a;
}
//当进入函数之后,把a的地址赋予给指针变量p之后,因为a是局部变量,所以出函数销毁
//此刻p所指向的内存已经释放(销毁),所以此刻的p指针就是野指针。
int main() {
int* p = test();
return 0;
}
1.3.4 解决办法
(1)使用空指针null初始化指针,同时在运用指针之前进行判断指针是不是空指针,如果不是再使用。(使用空指针会导致程序崩溃)
(2)小心指针越界;
(3)小心指针指向的空间释放;
(4)避免返回局部变量的地址;
(5)指针还用之前检查有效性。
二、指针运算
2.1 指针+-整数
#define N 5
int main() {
float arr[N];
float* p;
for (p = &arr[0]; p < &arr[N];) {
*p++ = 0;
// *p = 0 ;
// p++;
}
return 0;
}
2.2 指针-指针
指针-指针表示的是指针和指针之间元素的个数;不是所有的指针都能相减,只有指向同一个空间的指针才能相减(这样的减法才有意义)
2.3 指针的关系运算
指针和指针是可以进行比较的,比如数组中循环时,可以用最后一个元素的地址当做限制条件,但是要记住标准规定了:
允许指向数组元素的指针与指向数组最后一个元素后面那个内存位置的指针进行比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
三、结构体
3.1 结构体的声明
结构是一些值的集合,这些值被称为成员变量。结构体的每个成员可以是不同类型的变量。
(1)结构体类型的声明:
struct tag{ 结构体关键字 结构体标签(可以看做是结构体名){
member-list; 成员列表(包含数据类型+成员名,如:char name[])
}variable-list; }变量列表(变量列表可有可无,一般都不加)
(2)结构体变量的创建:
struct tag a = { 0 } ;
#include<stdio.h>
struct Stu {
char name[10];
char sex[5];
char num[10];
};
struct Grp {
struct Stu p;
int chinese;
int math;
int english;
};
//支持结构体中继承另一个结构体,类似于c++的类。
int main() {
struct Stu s1 = { "张三","男","219077889" };
struct Grp s2 = { "李四","男","219177364",124,143,21 };
printf("%s %s %s\n", s1.name, s1.sex, s1.num);
printf("%s %s %s ", s2.p.name, s2.p.sex, s2.p.num);
printf("%d %d %d\n", s2.chinese, s2.math, s2.english);
return 0;
}
3.2 结构体传参
在使用结构体的时候,一般不适用printf直接打印,因为在代码编写的过程中可能会多次打印,所以一般会采用函数的形式来进行函数的打印。那么在这个时候就涉及到传参的问题,如下方代码所示两种传参形式和几种对应的打印形式。
#include<stdio.h>
struct Stu {
char name[10];
char sex[5];
char num[10];
};
void print1(struct Stu s) {
printf("%s %s %s\n", s.name, s.sex, s.num);//第一种
}
void print2(struct Stu* s) {
printf("%s %s %s\n", s->name, s->sex, s->num);//第二种
printf("%s %s %s\n", (*s).name, (*s).sex, (*s).num);//第三种
}
int main() {
struct Stu s1 = { "张三","男","219077889" };
printf("%s %s %s\n", s1.name, s1.sex, s1.num);//第四种
print1(s1);
print2(&s1);
return 0;
}
(1)注意:在函数传参时,参数是需要进行压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销较大,所以会导致性能的下降。
因此,使用第二种,传递参数,并且使用s->name的形式更加好。
(2)结论:结构体传参的时候,要传递结构体的地址