对指针的概念以及的指针与C语言中数组、函数等的放在一起的使用的讲解
文章中所有的程序都是在VS2019下调试并且可以运行的
1.内存
1.1内存是什么
内存是计算机的重要部件之一。 它是外存与CPU进行沟通的桥梁,计算机中所有程序的运行都在内存中进行。
内存性能的强弱影响计算机整体发挥的水平。
内存(Memory)也称内存储器和主存储器,它用于暂时存放CPU中的运算数据,与硬盘等外部存储器交换的数据。
只要计算机开始运行,操作系统就会把需要运算的数据从内存调到CPU中进行运算。当运算完成,CPU将结果传送出来。
内存的运行也决定计算机整体运行快慢的程度。(百度百科)
1.2内存的特点
- 计算机内的存储部件,所有指令和数据都保存在内存内
- 速度快,但是掉电即失
- 可以随机访问
1.3内存的随机访问
计算机最小的存储单位是字节而最小的传输单位是bit
1byte = 8bit
只要指明要访问的内存单元的地址,就可以立即访问到该单元
地址是一个无符号整数,其字长一般与主机相同
- 内存中的每个字节都有唯一的一个地址
- 地址按字节编号,按类型分配空间
看下面程序(VS2019)
程序输出:
可以在内存中通过a的地址值访问到 a
因为vs2019生成的项目是32位的,所以看到地址值以十六进制显示有8位。如果以2进制显示的话就是4X8=32位,而却32位电脑有4G内存
它的计算方法就是:4x1024MBx1024KBx1024B
,1024为210,(210)3x22=232.所以是32位
1.4内存的寻址方式
1.4.1 直接访问
直接按变量地址(或变量名)来存取变量内容的访问方式
1.4.2 间接访问
通过指针变量来间接存取它所指向的变量的访问方式
图中左边的2000、2002等都是地址值】
C语言的指针就是为了使C语言可以实现间接访问。
2.指针变量
2.1 指针是什么
指针的实质就是一个变量和int a那些变量一样都是变量,只不过指针变量(指针是指针变量的简称)的类型和它们不一样,指针变量存的值也不一样
指针变量存储的是另外一个变量的地址
2.2 为什么需要指针
- 指针的出现时为了实现间接访问(汇编中都有间接访问),间接访问是CPU的一种寻址方式。
- CPU的间接访问是CPU设计时决定的,这个决定了汇编语言必须实现间接寻址,也决定了汇编之上的C语言也必须实现间接访问
- 其他的一些语言,比如Java,没有用到指针,那是因为语言本身帮我们封装好了。
- 可以通过指针修改函数中的变量值
2.3 指针的定义与使用
*
1.指针定义时,*结合前面的类型用于表示要定义的指针类型
2.指针解引用是,*p表示指向的变量的本身(给出指针指向地址上储存的值)
&
1.&+变量名表示这个变量的地址
#include<stdio.h>
int main() {
int a = 10; //定义一个整形变量a
printf("a = %d, &a = 0x%p\n", a, &a); //打印出a的值和a的地址值
int* p; //定义一个int* 型的指针,用来指向整形变量
p = &a; //给指针变量赋值,将变量的地址值赋给指针变量
//或者 int *p = &a;
*p = 555; //通过解引用访问值或者改变值
printf("修改后a = %d, p = 0x%p\n", a, p); //打印出a修改后的值,以及指针p的值
return 0;
}
运行结果:
当然每次给变量分配的地址都是随机的,所以下次运行a的地址会不同,但指针的值和a的地址值是相同的
2.4 如何判断指针变量的类型
从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型
int *ptr;//指针的类型是int*
char *ptr;//指针的类型是char*
int **ptr;//指针的类型是int**
int (*ptr)[3];//指针的类型是int(*)[3]
int *(*ptr)[4];//指针的类型是int*(*)[4]
2.5 如何判断指针指向的变量类型
当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型
int *ptr; //指针所指向的类型是int
char *ptr; //指针所指向的的类型是char
int **ptr; //指针所指向的的类型是int*
int (*ptr)[3]; //指针所指向的的类型是int()[3]
int *(*ptr)[4]; //指针所指向的的类型是int*()[4]
指针的类型,指针所指向变量的类型以及指针的值是非常重要的,遇到每一个指针变量都要非常清楚这几个
3. 左值与右值
左值与右值
定义很简单,在赋值运算符左边的为左值,在右边的为右值,比如
int a = 0;
int b = 10;
其中a,b都为左值,0,10为右值
当然变量既可以做左值也可以做右值
- 当一个变量做左值时,编译器认为这个变量符号的真实含义是这个变量对应的内存空间
- 当一个变量做右值时,这个变量符号的含义就是这个变量的值
int a = 10;
int b = a;
/* 现在再来理解一下赋值
int b = a;
此时b的含义为b所对应的内存空间,a为10。
这样就可以把a=10的值直接存到b的内存地址中,所以可以进行赋值
*/
4.野指针和空指针
4.1 野指针是什么
在定义指针后没有进行初始化的指针
4.2 野指针的危害
-
.野指针就是指向随机的位置或者不正确的位置的指针
-
指针变量在定义时未初始化,值也是随机的,指针变量指向了一个不可指向的地址值
-
野指针因为指向地址是不可预估的,所以会出现3种情况
- 第一种指向不可访问的(操作系统不允访问的地址)结果就是触发段错误
- 指向一个可用的,而且没什么特别意义的空间(比如曾经使用过但已经不用的栈空间或者堆空间)这时候程序运行不会出错,对当前程序也没有什么危害
- 指向了一个可用的空间,而且这个空间在程序正在被使用(比如程序中的一个变量a),野指针的解引用就会改变这个变量a的值,程序就会出错如果一个代码很长的项目中出现了这种错误将会很难找到。
4.3 怎么避免野指针
在对指针进行解引用之前,一定要确保指针指向一个绝对可用的空间
常用的做法:
- 定义指针时,同时初始化为NULL
- 解引用前,先去判断这个指针是不是NULL
- 指针使用完之后,将其赋值为NULL
- 在指针使用之前,将其赋值绑定给一个可用地址空间
#include<stdio.h>
int main() {
int a;
int* p = NULL; //定义指针时,同时初始化为NULL
p = &a; //在指针使用之前,将其赋值绑定给一个可用地址空间
//中间省略1000行代码
if (p != NULL) { //解引用前,先去判断这个指针是不是NULL
*p = 4;
}
p = NULL; //指针使用完之后,将其赋值为NULL
printf("%d", a);
return 0;
}
4.4 空指针和NULL
指向NULL或者说是0的指针为空指针
NULL在C/C++中的定义为
#ifdenf _cplusplus_ //定义这个符号表示当前是C/C++环境,这句话的意思是如果是C++环境
#define NULL 0 //NULL就是0
#else //如果是C
#define NULL (void *)0 //NULL为强制类型转换为void * 0
#endif
所以NULL的实质就是0,然后我们给指针赋初值为NULL,其实就是让指针指向0地址。
为什么指向0地址?
- 0地址作为一个特殊地址(我们认为指针指向这里就表示没有初始化)
- 这个0地址在一般的操作系统中都是不可被访问的,如果用户不检查是否等于NULL就去解引用,写出的代码运行后会直接触发段错误。可以更好的找到自己的代码错在哪里
5.const关键字与指针
5.1 const关键字
C语言中const代表着“不可变”,基本和常量一样不可修改,但是应用场景不一样。其实const指针指向的值也是可以改变的。所以它起到的仅仅是一个约束作用。可以通过内存改变const的值。主要讲解const运用在变量、指针以及函数参数。
5.2 const关键字的使用
const关键字的使用主要在三个方面
const char a=‘A’;
a=‘B’; //错误,变量a的值不可以修改。
此时代表变量a值不可改变,任何企图修改a变量值的语句(例如a=20;)都会报错。
1.const int *p; //p本身不是const的,而p指向的变量是const的
2.int const *p; //p本身不是const的,而p指向的变量是const的
3.int * const p; //p本身是const的,p指向的变量不是const的
4.const int * const p; //p本身是const的,p指向的变量也是const的
1.const int *p; //p本身不是const的,而p指向的变量是const的
#include<stdio.h>
int main() {
int a = 10;
int b = 0;
int const * p1 = &a;
int* p2 = &b;
//*p1 = 5; //会报错,报错原因为常量*p1的值不能改变
printf("a = %d, &a = %p\n", a, &a);
printf("&b = %p\n", &b);
p1 = p2; //会运行,并且p1的地址从指向变量a,变成指向变量b
printf("*p1 = %d, p = %p", *p1, p1);
/*运行结果:
a = 10, &a = 0133F944
&b = 0133F938
*p1 = 0, p = 0133F938
*/
return 0;
}
2.int const *p; //p本身不是const的,而p指向的变量是const的
#include<stdio.h>
int main() {
int a = 10;
int b = 2;
const int * p1 = &a;
int* p2 = &b;
//*p1 = 100; //不能运行,错误原因p1:不能给常量赋值
p1 = p2; //可以运行,和情况1相同.
printf("a = %d, &a = %p\n", a, &a);
printf("&b = %p\n", &b);
printf("*p1 = %d, p = %p", *p1, p1);
/*
* 运行结果:
a = 10, &a = 004FF898
&b = 004FF88C
*p1 = 2, p = 004FF88C
*/
return 0;
}
3.int * const p; //p本身是const的,p指向的变量不是const的
#include<stdio.h>
int main() {
int a = 10;
int b = 2;
int * const p1 = &a;
int* p2 = &b;
*p1 = 100; //可以运行,a的值被成功的改成了100
//p1 = p2; //不能运行,"p1":不能给常量赋值
printf("a = %d, &a = %p\n", a, &a);
printf("&b = %p\n", &b);
printf("*p1 = %d, p = %p", *p1, p1);
/*
* 运行结果:
a = 100, &a = 004FF8C4
&b = 004FF8B8
*p1 = 100, p = 004FF8C4
*/
return 0;
}
4.const int * const p; //p本身是const的,p指向的变量也是const的
#include<stdio.h>
int main() {
int a = 10;
int b = 2;
const int * const p1 = &a;
int* p2 = &b;
//*p1 = 100; //不能运行,"p1":不能给常量赋值
//p1 = p2; //不能运行,"p1":不能给常量赋值
printf("a = %d, &a = %p\n", a, &a);
printf("&b = %p\n", &b);
printf("*p1 = %d, p = %p", *p1, p1);
return 0;
}
总之,const在 *左边,表示指针指向的值不可以修改。const在 *右边,表示指针值(也就是指针指向的位置)不可以修改。
虽然加了这些很影响代码的可读性,但却大大加强了代码的安全性。在不该改的变量前加上const关键字
strcat(char *a,char const *b),将参数b指向的字符串,添加到参数a字符串的末尾。
此时,参数 *a值可以改变,但是表示参数 *b值不可改变
也就是表示字符串b不能变,只是单纯的和a连接到一起
6.指针和数组
6.1 变量名、变量、变量的值、变量类型
变量是一个地址,这个地址在编译器中决定具体数值。这个具体数值和变量名绑定(所以变量名又叫标识符,起一个标志作用。)。变量类型决定了这个地址的延续长度。比如int占四个字节。变量所在地址所储存的值叫变量的值。
6.2 回顾数组
(1)从内存角度来看,数组变量就是一个一次分配多个变量,而且这多个变量在内存中的存储单元是连续的
(2)分开定义多个变量(int a,b,c,d;)和定义个数组(int a[4];)都定义了4个int型变量,而且都是4个独立的变量。但是,单独定义出来的变量,地址不一定连续,所以数组的第一个优点就显现出来了,那就是更容易访问一组数据。
可以看出数组存储变量的地址是连续的。
6.3 int a[10]; a a[0] &a &a[0]
-
a是数组名,
- a做左值时表示整个数组所有的空间(也就是4X10=40byte的空间),又因为C语言规定数组操作时,要独立单个元素操作,所以a不能做左值。
- a做右值时,表示数组首元素的首地址(首地址就是起始地址 也就是相当于&a[0])
-
a[0]表示数组的首元素,也就是数组的第0个元素
- a[0]做左值时,表示数组第0个元素对应的内存空间
- a[0]做右值时,表示数组第0个元素的的值
-
&a,对数组名取地址表示什么?先看代码
运行结果都在注释里了,我们可以很容易看出,&a,a,a[0]都是数组的首地址,但是他们+1后&a的地址值就会改变。所以&a表示数组的地址,而a[0]和a表示的是数组首元素的首地址,虽然它们的值是一样的但是表达的意义有所不同,对&a进行加1就会到下一个数组,而不是下一个元素。数组指针的定义int (*p)[4] = &a;
。int *p = a;
这是数组名。
所以&a表示的是数组的地址,又因为多用于二维数组所以又别称为行指针,&a只能做右值不能做左值
4. &a[0],数组首元素的首地址和a[0]相似。
指针能表示两个维度的信息,第一个是内存当中的地址,第二个是访问内存的“尺度”。 数组名 a、数组首元素的地址 &a[0]、数组名取地址
&a,这三者在内存中其实是同一个地址,但访问内存的尺度有所不同,其中 a 和 &a[0] 是以 int 类型所占内存空间为尺度来访问内存,而
&a 是以数组 int a[4] 所占内存空间为尺度来访问内存。
因为C语言规定数组操作时,要独立单个元素操作,所以a不能做左值。因为这个非常重要的规定,我们在字符串中看到的更为明显,对一个字符串赋值
#include<stdio.h>
#include<string.h>
int main(){
char str[20];
char *pStr = str;
/*
*pStr = "123"; [Error] invalid conversion from 'const char*' to 'char' [-fpermissive]
会报错(报错原因自己百度,知道这样不行就可以了);
所以只能通过strcpy()函数来对字符串赋值
*/
strcpy(pStr,"123");
printf("%s",str);
return 0;
}
6.4 以指针方式访问数组元素
*(指针 + 偏移量)
#include<stdio.h>
int main() {
int a[4] = { 1,2,3,4 };
int* p = a;
printf("a[0] = %d\n", a[0]);
printf("*(p+0) = %d\n", *p);
printf("a[3] = %d\n", a[3]);
printf("*(p+3) = %d\n", *(p + 3));
getchar();
return 0;
/*
运行结果:
a[0] = 1
*(p+0) = 1
a[3] = 4
*(p+3) = 4
*/
}
6.5 指针数组与数组指针
6.5.1 指针数组与数组指针的概念
指针数组的实质是一个数组,这个数组里面存储的内容全部是指针变量
int *p[5];
数组指针的实质是一个指针,这个指针指向的是一个数组,通常用于二维数组中,所有又叫行指针
int (*p)[5];
6.5.2指针数组和数组指针的表达式
int a[4] = { 1,2,3,4 };
int* p[5]; //指针数组
int(*p)[4] = &a; //数组指针
/**
对于语句“int(*p)[4]”,“()”的优先级比“[]”高,“*”号和 p 构成一个指针的定义,
指针变量名为 p,而 int 修饰的是数组的内容,即数组的每个元素。
也就是说,p 是一个指针,它指向一个包含 4 个 int 类型数据的数组a
*/
#include<stdio.h>
int main() {
int a[4] = { 1,2,3,4 };
int* p1[5]; //指针数组
int(*p2)[4] = &a; //数组指针
printf("a = %p\n", a);
printf("p2 = %p\n", p2);
printf("&a = %p\n", &a);
printf("a+1 = %p\n", a+1);
printf("p2+1 = %p\n", p2+1);
printf("&a+1 = %p\n", &a+1);
getchar();
return 0;
/*
运行结果:
a = 006FFA4C
p2 = 006FFA4C
&a = 006FFA4C
a+1 = 006FFA50
p2+1 = 006FFA5C
&a+1 = 006FFA5C
*/
}
6.6 指针和二维数组
6.6.1 二维数组
二维数组在C语言程序中经常用来计算矩阵,行列式或者表示一个xy的坐标轴。其实,二维数组在内存中所存的数据也是连续的,就相当于把矩阵按一行一行的展开。
比如定义一个Array[3][5];
我们可以把这个定义理解为。定义了三个数组Array[0]、Array[1]、Array[2]
,每个数组又有五个元素。这样就体现出来刚刚看过的数组指针的作用,我们可以通过对数组指针的解引用来访问二维数组中的值
使用数组指针指向二维数组
#include<stdio.h>
#include<cstring>
int main() {
int a[2][3] = { 1, 2, 3,
4, 5, 6 };
int(*p)[3] = &a[0];
printf("&a[0][0] = %p\n", &a[0][0]);
printf("p = %p\n", p);
printf("&a[1][0] = %p\n", &a[1][0]);
printf("p+1 = %p\n", p + 1);
getchar();
return 0;
/*运行结果:
&a[0][0] = 00ECFC28
p = 00ECFC28
&a[1][0] = 00ECFC34
p+1 = 00ECFC34
*/
}
6.6.2 如何使用数组指针访问二维数组
#include<stdio.h>
#include<string.h>
int main() {
int a[2][3] = { 1, 2, 3,
4, 5, 6 };
int(*p1)[3] = a; //使用数组指针
printf("a[0][1] = %d\n", a[0][1]); //使用数组下标访问数组中的元素
printf("a[1][2] = %d\n", a[1][2]);
printf("*(*p1 + 1)= %d\n", *(*p1 + 1)); //使用数组指针访问
//这里的*(*p1 + 1)相当于*(*(p1 + 0) + 1)
printf("*(*(p1 + 1) + 2) = %d\n ", *(*(p1 + 1) + 2));
getchar();
/*运行结果:
a[0][1] = 2
a[1][2] = 6
*(*p1 + 1)= 2
*(*(p1 + 1) + 2) = 6
*/
return 0;
}
从程序中可以看出,使用数组指针可以进行访问二维数组。使用下标来访问数组元素都比较熟悉。程序中使用数组指针访问元素有两句代码一个是*(*p1 + 1)
,另一个是*(*(p1 + 1) + 2)
。这两句话该如何理解。
我们一步一步来看,主要分析*(*(p1 + 1) + 2)
,第一步是*(p1 + 1)
,p1是数组指针它现在的值是数组的地址也就是首元素的首地址,int(*p1)[3] = a; //使用数组指针
这是p1的定义,现在对p1+1也就是移动一个数组(3int元素的空间)然后对它解引用就是 a[1]的值,也就是二维数组a中的第二个数组的首地址。然后*(p1 + 1) + 2
这样指针就会指向a[1]中第3个元素,最后*(*(p1 + 1) + 2)
对它解引用就得到了它的值6.
总结:使用数组指针访问二维数组
int a[2][3];
int (*p)[3] = a;
a[i][j] == *(*(p + i) + j);
7. 指针和函数
7.1 经典例题 —交换问题
#include<stdio.h>
#include<cstring>
void swap1(int, int);
void swap2(int*, int*);
int main() {
int x = 3, y = 5;
/*swap1(x, y);*/ /*执行swap1(),虽然成功改变了函数中x和y的值,
但是原本主函数中的x和y的值依然没有变*/
swap2(&x, &y); //使用swap2(),成功改变了主函数中的x和y的值
printf("x = %d, y = %d\n", x, y);
return 0;
}
void swap1(int x, int y) {
int temp = 0;
temp = x;
x = y;
y = temp;
printf("函数中的x = %d, y = %d\n", x, y);
}
void swap2(int* x, int* y) {
int temp;
temp = *x;
*x = *y;
*y = temp;
}
为什么swap1不能改变主函数的值但swap2函数可以改变?
通过对代码的分析可以理解,首先理解函数中的形参和实参
- 形式参数(形参):定义函数名和函数体时需要用的参数,目的是用来接收调用该函数时传递的参数。
- 实际参数(实参):传递给被调用函数的值。
区别 | 形式参数(形参) | 实际参数(实参) |
---|---|---|
1 | :形参只能是变量,在被定义的函数中,必须指定形参的类型。 | 实参可以是常量、变量、表达式、函数等, |
2 | 没有确定的值 | 无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。 因此应预先用赋值,输入等办法使实参获得确定值。 |
3 | 形参变量在未出现函数调用时,并不占用内存,只在调用时才占用。调用结束后,将释放内存。 | 开辟内存存储数据 |
通过上面这个表格可以得知,虽然在主函数中将x,y的值传给了swap1()中的x,y但swap1中的x,y只能在函数调用时存在,所以函数调用完返回主函数并不会影响主函数中x,y的值
但是swap2()函数传入的是x和y的地址值。通过对地址值上的值进行修改,所以,不管x,y在哪个位置,x和y的值都会被改变
7.2 函数指针
7.2.1 函数指针的本质
- 函数指针的实质还是指针(指针变量),占四个字节(在32位系统中,所有指针都是4个字节)
- 函数指针、数组指针、普通指针本质上没有区别都存储了地址值,只不过他们指向的东西不同
- 函数的本质是一段代码,这一段代码在内存中是连续分布的(函数的{}中所有的语句),所以对于函数来讲,很关键的就是函数中的第一句代码所在的地址,这个地址就是所谓的函数地址,在C语言中用函数名这个符号来表示
- 函数指针其实就是一个普通变量,这个普通变量的类型是函数指针类型,它的值就是函数地址(也就是它的函数名这个符号在编译器中所对应的值)
7.2.2 函数指针的定义
#include<stdio.h>
#include<cstring>
void func(void);
int main() {
//也可以写成void (*pFunc)(void) = func;
void (*pFunc)(void); //func函数对应的函数指针类型 viod (*)(void);
pFunc = func;
printf("func = %p\npFunc = %p\n", func, pFunc);/* func = 000513C0
pFunc = 000513C0
func和pFunc所指向的地址相同都表示函数
func的函数地址
*/
pFunc(); /* 运行结果: ...
用函数指针可以调用该函数,代替函数名的作用
*/
return 0;
}
void func(void) {
printf("...\n");
}
- 函数名是数组名的区别:函数名做右值时,加不加&效果和意义都是一样的,但是数组名会不一样
函数指针的举例
#include<string.h>
#include <stdio.h>
//vs准备弃用strcpy的,安全性较低,所以微软提供了strcpy_s来代替,如果想继续使用strcpy的,main前面加上
#pragma warning(disable:4996)
int main(){
//char *strcpy(char *dst, const char *src); strcpy函数原型
char str1[20], str2[] = "123";
char* (*p)(char*, const char*) = strcpy;
p(str1, str2); //和直接使用strcpy()的效果一样。
printf("a = %s\n", str1);
return 0;
}
通过上面这段代码可以看到,如果定义一个函数指针,这个函数的声明比较复杂,那么在定义多个时就会很费时间,所以这个时候我们可以使用typedef来进行重命名
7.2.3 typedef关键字
- 用来定义或者重命名类型
- 编译器的原生类型(基础数据类型,比如int,double),用户自定义的类型,程序员自己定义的(结构体类型,数组类型)
- 如果自定义类型太长,定义多个变量时会浪费时间,所以使用typedef来重命名一个短点的名字
#include<string.h>
#include <stdio.h>
//vs准备弃用strcpy的,安全性较低,所以微软提供了strcpy_s来代替,如果想继续使用strcpy的,main前面加上
#pragma warning(disable:4996)
//使用typedef将原来很长的 char* (*)(char*, const char*)转换为pType;
typedef char* (*pType)(char*, const char*);
int main(){
//char *strcpy(char *dst, const char *src); strcpy函数原型
char str1[20], str2[] = "123";
pType p = strcpy;
p(str1, str2); //和直接使用strcpy()的效果一样。
printf("a = %s\n", str1);
return 0;
}
可以很明显的看出,这样定义多个函数指针的时候会节约很多。指针函数没什么好说的,无非就是返回值是一个指针也就是返回一个地址的函数。
typedef char* (*pType)(char*, const char*); //函数指针
typedef char* (*pType[5])(char*, const char*); //函数指针数组
typedef char* (*(*pType)[5])(char*, const char*); //函数指针数组指针
出一个小问题:int (*(*p5)(int*))[5]; 该怎么理解?
8.结构体和指针
8.1 结构体
首先我们为什么要用到结构体,我们都已经学了很多int char …等类型还学到了同类型元素构成的数组,但是,在我们实际应用中,每一种变量进行一次声明,再结合起来显然是不太实际的,类如一位学生的信息管理,他可能有,姓名(char),学号(int)成绩(float)等多种数据。如果把这些数据分别单独定义,就会特别松散、复杂,难以规划,因此我们需要把一些相关的变量组合起来,以一个整体形式对对象进行描述,这就是结构体的好处。
只有定义了结构体变量才分配地址,而结构体的定义是不分配空间的。
结构体中各成员的定义和之前的变量定义一样,但在定义时也不分配空间。
在链表中,我们需要使用stdlib头文件里的malloc函数来分配地址。这样可以通过free函数进行删除,比较好管理。现在不需要删除,所以不使用malloc。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
struct Student { //定义一个结构体Student
int age; //int型 年龄
char name[20]; //姓名
float score; //得分
};
int main() {
struct Student s1; //定义一个结构体变量s1
scanf("%d%s%f", &s1.age, s1.name, &s1.score);
printf("学生的姓名:%s\n学生的年龄:%d\n学生的成绩:%.2f\n", s1.name, s1.age, s1.score);
return 0;
}
当然结构体还可以定义结构体数组的变量,这里不做过多解释。可以看到如果想要访问到结构体中的元素的话,需要用结构体变量.元素
的形式,其中的 . 又被叫做 . 运算符。如果访问的元素是结构体的话则继续访问一直访问到最低一级的成员变量
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
struct Student { //定义一个结构体Student
int age; //int型 年龄
char name[20]; //姓名
float score; //得分
struct monitor{ //班长
char sex = 'F';
}m1; //定义一个班长的结构体变量m1
};
int main() {
struct Student s1; //定义一个结构体变量s1
printf("班长的性别:%c", s1.m1.sex);
return 0;
}
8.2 结构体指针
通常情况下,结构体指针一般用于链表、树等数据结构的书写
1.结构体指针的定义方法
1)struct 结构体名 *指针;
2)直接在定义结构体的时候添加结构体指针的声明
//在main()方法中定义
struct student *p1;//定义结构体指针
或者
struct student{
int num;
char name[20];
char sex;
}*p2;
struct student{
int num;
struct student *stu;
};
2.利用结构体指针的访问结构体变量(输出)
结构体指针的访问变量方法
1)p->结构体成员;
2)(*p).结构体成员;
#include<stdio.h>
#include<stdlib.h>
struct student {
int num;
char name[20];
int age;
};
int main() {
struct student s1 = { 1,"zzy",18 };
struct student* p = &s1;
printf("学号:%d\n姓名:%s\n年龄:%d\n", (*p).num, (*p).name, (*p).age);
printf("学号:%d\n姓名:%s\n年龄:%d\n", p->num, p->name, p->age);
return 0;
}
最后访问结果一样,只要注意使用指针时必须要使用->来访问
9.二重指针(指针的指针)
(1)二重指针也是指针变量,和普通指针的差别就是它指向的变量类型必须是一个一重指针,二重指针其实也是一种数据类型,编译器在编译时会根据二重指针的数据类型来做静态类型检查
(2)没有二重指针其实也可以(可以用一重指针代替),但是之所以会有二重指针(或者函数指针、数组指针)就是为了让编译器了解这个指针被定义的时,定义它的程序员希望这个指针被用来指向什么东西。编译器知道指针类型后可以帮我们做静态类型检查,编译的这种静态类型检查可以辅助程序员发现一些隐含性的错误,这是C语言给程序员提供的一种编译时的查错机制
9.1 二重指针的用法
(1)二重指针指向一重数组的地址
(2)二重指针指向指针数组
(3)实际中,二重指针的运用比较少,大部分就是和指针数组一起使用
(4)有的函数在传参时,为了通过函数内部改变外部的一个指针变量(用的很少)
#include<stdio.h>
void func(int** p, int* pCh) {
*p = pCh;
}
int main() {
int a = 0;
int b = 100;
int* p = &a;
printf("p: %d\n", *p); //打印的结果为p: 0 此时还是a的地址
func(&p, &b); //通过函数func后改变成b的地址
printf("func p: %d\n", *p); //func p: 100
return 0;
}
这里通过二重指针改变了主函数中的p指针的值,但是实践中用的很少
结语
通过本篇博客,大家可以了解到C语言中指针的大部分用法。当然还有一些更深奥的东西。也可以体现到C语言中指针是非常非常非常重要的!在遇到一个指针时,不要觉得它很复杂,记得先判断指针的类型,指针指向的变量的类型,然后在分层分析指针究竟指向了什么。当然多敲代码是最重要的…
函数指针答案
int (*(*p5)(int*))[5]; //——p5是个指针,指向一个具有一个int *型形参的函数,这个函数返回一个指向具有5个int元素的数组的指针。