C语言基础:一口气搞定所有指针问题!(包含二级指针,指针在数组和函数以及结构体中的相关问题)

1.指针
我们都知道,当我们在程序中书写一个变量时(如:int a),这时程序会在内存中开辟一个int大小的空间来存放a这个变量,对于程序来说,可以通过变量名找到为变量中的数据,但是计算机底层可不认识变量,那么CPU需要找到这个变量存储的空间,就需要一个标识,这个标识就是地址

地址:

在计算机运行时,数据会存放在内存中,内存会以字节为单位划分为多个存储空间,并且为每个字节默认设置一个对应的编号,这个编号就是地址。

地址只是计算机规定的一个值,所以不会占用内存的存储空间,地址显示的长度会根据系统及编译器的位数确定。(位数:在计算机底层通过0和1来表示计算机的状态,如果有两位,那么可以表示的状态就有00,01,10,11四种,如果三位则有8种,以此类推,32位可以表示2^32,64位则有2^64)

64位编译器显示的地址为16个16进制数,32位编译器显示的地址为8个16进制数。

因此我们便可以知道,程序在访问变量时,可以通过变量名访问,也可以通过地址访问。而通过地址访问的方式,我们在C语言中将其称之为:指针

指针的定义方式:

访问方式  *指针名;

引出指针后,指针便需要回答两个问题:

1.需要多大的空间来存储这个地址

2.如何通过地址访问空间

那么我们先来回答第一个:需要多大的空间来存储这个地址,也就是指针本身的大小,这里我们直接用程序演示:

#include<stdio.h>
int main() {
	char* a;
	printf("the len is:%zd",sizeof(a));
	return 0;
}

最后程序执行的结果是:

但是这里要注意,前面我们说过,存储空间的大小受系统位数的大小,可以看到我当前的编译器是默认使用64位进行编译的,所以大小是8。

我们这里可以切换到32位的环境下进行编译,可以发现结果显示为4。

因此我们便可以回答出第一个问题:要用多大的空间存储地址并不是程序决定,而是编译器决定,在不同的位数下,可以是4或者8。

我们知道了指针的大小,那么接下来我们需要回答一个问题:指针既然有大小,那是否可以使用int或long之类的类型来存储呢?答案是当然不行!指针前面的定义不是像变量一般表示大小,而是表示指针访问地址的方式,这样说可能有点抽象,我们用代码演示一下:

#include<stdio.h>
int main() {
	//申请一片空间
	char a[] = { 0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x40 };
	char* p = &a[3];
	printf("the value is:%x\n",*p);
	return 0;
}

结果如下:

这里是常规写法,我们可以很好理解,即指针p通过地址访问了数组第四个元素,获取到了其中的数据。但是以上的代码我们也可以写成这样:

#include<stdio.h>
int main() {
	//申请一片空间
	char a[] = { 0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x40 };
	char* p = &a[3];
	printf("the value is:%x\n", p[0]);
	return 0;
}

也会得到同样的效果。那么,我们可以试着改一些值:

#include<stdio.h>
int main() {
	//申请一片空间
	char a[] = { 0x33,0x34,0x35,0x36,0x37,0x38,0x39};
	char* p = &a[3];
	printf("the value is:%x\n", p[0]);
	printf("the value2 is:%x\n", p[1]);
	printf("the value3 is:%x\n", p[-1]);
	return 0;
}

运行发现,2和3在原来的基础上向上或向下偏移了一个单位。

底层原理:

数组开辟一道连续的内存空间,现在指针p通过数组下标为3的地址找到了其中的值,并以此为起始位置向上或向下按一个字节进行访问。

如图所示:

好,那么回到原来的问题上:指针前面的不是变量,而是对地址的访问方式,那么我们就可以来干点坏事:将char类型的访问方式改成int类型会怎样?

#include<stdio.h>
int main() {
	//申请一片空间
	char a[] = { 0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x40};
	char* p1 = &a[3];
	int* p2 = &a[3];
	printf("the p1:%p the p2:%p\n");
	printf("the value1:%x:%x\n",p1[0],p1[1]);
	printf("the value2:%x:%x\n",p2[0],p2[1]);
	return 0;
}

然后我们运行,就得到了这样的结果:

可以看到,因为int是按四个字节访问的,所以这里一次性直接读了4个数据作为一个数据输出。由此我们可以很直观的看到指针前面的访问方式对指针访问地址的影响。

那么还有一个问题:我这里是从第三个数开始访问,而后面再进四位时已经超出了数组的范围,这里引出一个知识点:数组只是开辟了一个连续的空间,并按首地址和访问类型进行加或减的操作,而不关心数组长度的限制,数组开辟的连续空间上下当然还有其他的连续内存空间,当然也可以进行访问,只是至于那片空间放了什么,我们就不知道了,因此要尽量避免出现这种问题。

初步了解了指针之后,接下来我们要引入一个新的指针:

指针的指针——二级指针。

二级指针的书写格式:  访问方式 **指针名;

即我们定义了这个指针,而这个指针又指向了一个指针,所以我们就可以通过这种结构访问多个指针指向的空间中存放的数据。前面我们知道,物理内存是一片连续的空间,当我们的指针指向了一个地址时,就可以用类似于p[1],p[-1]的方式进行上下偏移,从而访问一个一个连续的地址空间,而二级指针也与这种情况类似。当我们通过二级指针去访问地址空间时,发现访问的是一个指针,于是通过这个指针去访问他所指向的地址空间。当我们上下偏移时,同样也是如此。

理解:二级指针就像一个存放钥匙串的容器,你可以用里面的任何一个钥匙打开一片空间并获取里面的内容。基于这种特性,我们可以创造一种特殊的结构,即:

物理上不连续,但是逻辑上连续的结构。

接下来写代码来验证一下:

#include<stdio.h>
int main() {
	char* p1 = "C/C++";
	char* p2 = "java";
	char* p3 = "Linux";
	char* a[3];
	a[0] = p3;
	a[1] = p1;
	a[2] = p2;
	char** p = a;
	for (int i = 0; i < 3; i++) {
		printf("%s\n", p[i]);
	}
	return 0;
}

显示结果:

注意:这里在定义数组时可以看到我这里在数组名前加上了*,也就是定义了一个指针数组(指针数组:指针数组的本质上是数组,当数组中要存的数据为指针时,需要在数组名前加上*,否则会报错)

讲到了二维指针,接下来我,,引出一下二维数组的概念以及指针在二维数组中的应用。

二维数组:逻辑上是以行和列组成的二维空间来存储数据的一种形式。而物理上仍是在内存中开辟一段内存空间。如图所示:

那么,现在我们想要去访问一下这片空间的地址,就可以写成如下的代码:

#include<stdio.h>
int main() {
	char img[3][4];
	img[1][2] = 0x32;

	printf("%p:%p:%p\n", img, img + 1,img[1][2]);

	return 0;
}

结果如下:

那么现在,我想用一个指针去指向二维数组看看和一维数组的区别,那么就可以写出这样的代码:

#include<stdio.h>
int main() {
	char img[3][4];
	img[1][2] = 0x32;
	char* p = &img;
	printf("the img is: %p:%p:%p\n", img, img + 1,img[1][2]);
	printf("the *p is : %p:%p\n",p,p+1);
	return 0;
}

运行结果:

欸,这不对啊,为什么上面的一次性加了四个,而下面的只加了一个呢?原因是二维数组传的是行的首地址,执行的+1操作也是对行的+1操作,我们这里定义的是三行四列,所以一次性加了四个,而我们这里的指针是char类型的访问方式,是一个字节一个字节的访问,所以只加了1个。

那么我们如何让指针也能够一次性走四个呢,这里肯定就会有大聪明说了:我们把指针的访问方式定义成int类型不就好了?当然,这种做法在这种条件下是没问题的,但是万一以后我的列是5,6之类的呢?而且我们前面说过,数据类型所占的字节大小还与操作系统位数有关,所以这种做法显然很不靠谱,那么有什么靠谱的方法吗?当然有:

我们可以在指针后定义一个方括号来表示指针每次走的字节数,比如这里我想访问4个字节,就可以将指针定义成:char *p[4]=&img;

但是聪明的你一定很快就发现问题了:

我们之前讲过,这种书写格式不是定义了一个4个char类型空间大小的指针数组吗?没错的,这种写法就是指针数组,我们可以这么理解:当编译器看我们定义的变量时是先看右边再看左边的。

比如:int p; 编译器先看p的右边,发现是;截止,然后去看左边,发现是int,于是开辟四个字节大小的内存空间来存储。

int *p;一样先右后左,发现右边截止后往左看,发现*,将p升级为指针,然后看到int,以四个字节访问地址。

同样,我们来分析一下char *p[4];编译器向右看到了[4],于是将p升级为4个空间大小的数组,然后截止,往左边看,发下了*,于是再次将数组升级成指针数组,用于存放指针,再向左看到char,确定每次以1个字节大小访问。

那么了解了原因之后,我们回到原来的问题上:如何让指针以4个单位访问二维数组呢?

其实很简答,我们之前学运算优先级的时候知道,如果想让某个值优先计算,可以用()括住,这样()里的值就会优先计算。这里也一样,我们可以理解为[5]的优先级比*高,既然如此,我们

将*p先括起来。写成这样:char(*p)[5] = &img;

再来用上面的逻辑理解一下:p向右看时发现括号,于是截止然后向左看,发现*,于是将p升级为指针,然后从*p这个整体向右看,发现了[5],于是将指针升级为数组指针(注意:数组指针的本质是指针,用于访问二维数组的每行或三维数组的每个二维数组),每次访问5个单位,然后发现=号,将右值赋值给左值,随后截止,向左看,发现char,确定每次以1个字节进行访问。

接下来写代码验证:

#include<stdio.h>
int main() {
	char img[3][4];
	img[1][2] = 0x32;
	char(* p)[4] = &img;
	printf("the img is: %p:%p:%p\n", img, img + 1,img[1][2]);
	printf("the *p is : %p:%p\n",p,p+1);
	return 0;
}

结果如下:这次我们就能看到两个结果完全一致。

现在我们明白了如何用数组指针来访问二维数组的每行,同样,这种形式也可以用来访问三维数组的每个二维数组。

三维数组:在内存中按照最前面设置的个数开辟一个连续空间用来存放n个二维数组。

书写格式:char a[2][3][4];

理解:开辟了两个三行四列的二维数组。

这样说可能有点抽象,我们还是画一张图来表示:

同样的,我们想通过指针来访问三维数组,也需要定义一个数组指针。每一次访问一个二维数组大小的空间。例:

#include<stdio.h>
int main() {
	char img[2][3][4];
	img[0][1][2] = 0x32;
	char(* p)[3][4] = &img;
	printf("the img is: %p:%p:%p\n", img, img + 1,img[0][1][2]);
	printf("the *p is : %p:%p\n",p,p+1);
	return 0;
}

运行结果如下:

好了,关于指针在数组中的应用到这里大概就告一段落了,接下来我们要来看

指针在函数中的运用。

而在这之前,我们先来了解一下函数的基本特性:

首先来回忆一下数学上的函数规则:我们向函数中传入一个值,随后根据函数的一系列计算之后再返回一个值,即f(n) = N;

C语言中的函数和数学上类似,书写格式  : 返回类型  函数名(输入类型){  代码 }。

当我们定义一个函数时,函数也会在内存中开辟一片空间,但是预与数组的基本数据空间不同的是,函数空间是存储代码和指令的。

上面的书写格式我们称为函数的定义和初始化,即我们定义了函数名和函数的输入输出类型,且在方括号中书写代码表示函数实现的功能,而当我们只写一个函数声明而不写大括号时,我们则称之为函数的定义,当我们定义过函数之后,想用函数功能时只需要写处出 函数名();即可,这种行为被称为函数的调用。例:

#include<stdio.h>
void p(int n){
    for(int i = 0;i<n;i++){
    printf("Hello,World!");
}
}

int main(){
    int n = 5;
   p(n);

return 0;
}

这里我们定义了一个函数,该函数会根据输入的值来判定打印多上行“Hello,World!”,这里我们简单分析一下底层:系统先读到了p,向右看发现有(),升级为函数,同时确定输入和输出类型,随后我们就用大括号进行初始化,此时系统在内存中分配了一块代码空间来用于存放和执行代码,同时又分配了一块数据空间用来存储函数的局部变量,同时又分配了一块代小的空间用来接函数的传入值,同时要注意的是,函数具有只读性,在初始化之后我们后续只能调用函数,而不能对函数进行任何的修改操作。值得注意的是,函数的空间具有空间隔离性,即在函数内部发生的事与外部无关。如图所示:

因为空间不同,所以这里我变量名即使相同也没有事,同时这里我们还传入了参数,但是这里要注意的是,函数中传参的方式是拷贝,什么意思呢?前面我们已经说过函数的空间具有隔离性,我们在main函数中写了一个变量n,那么可以理解为这个n在main函数的数据区中,而我们将n传入给定义的p函数,则就相当于p中也开辟了一个数据区来存,而只是将main函数中的数据拷贝了一份放到p的空间中,这里最经典的问题是什么呢,就是在函数中对传入值进行修改的问题。

例:

#include<stdio.h>
void p(int n) {
	n *= 5;
	printf("the value in p is:%d\n",n);
}
int main() {
	int n = 5;
	p(5);
	printf("the value in main is:%d\n", n);
	return 0;
}

结果可以看到:

可以看到,因为两个函数的空间具有隔离性,所以在p中的操作和在main中无关,于是上层空间中main中的变量值没有改变。那么,如果我现在希望通过函数来更改上层空间传入的值该怎么办呢,聪明的你一定想到了:指针。既然我们函数的空间不同,那我直接让函数接收一个指针指向一个固定的地址空间,这样不就可以修改值了吗,没错,就是这样,但是在我们正式使用指针进行传参之前,我们先要了解一下注意事项:

#include<stdio.h>
void abc(int n,int *p) {
	n *= 5;
	printf("the value in p is:%d\n",n);
	printf("the p %x : *p %x\n",p,*p );
}
int main() {
	int n = 5;
	int* p = &n;
	abc(n,p);
	printf("the value in main is:%d\n", n);
	return 0;
}

结果:

这里可以直观的看到,p表示的是取其指向的地址号,而*p才是取其中的数据。

另外还要注意:对指针的赋值必须要先对变量进行赋值,再用指针去指向变量的地址,如果直接为指针赋值数字则表示指针要指向那块数字的地址,而我们知道C语言中有ACII码的概念,因此如果这么做,几乎100%会报错。另外,我们在定义函数时如果要求传入指针,则要在形参中明确表明是指针,形参不写指针而传入指针或者形参写了指针而传入不是指针的值都会报错。我们用实际代码看一下:

这里是直接传入了*p,上面说过,*p代表的是p指向的地址空间中的数据,而空间中的数据是5,传入函数时函数把它当成了指针去访问5的内存空间,故报错。

这里报错原因和上面相同,我们的形参是指针,而传入的是一个值,则函数会去访问这个值的内存空间,故报错。

这里我们形参不是指针而我们传入了一个指针,也报错。

说完了常见错误,接下来我们来一次正确示范:

#include<stdio.h>
void abc(int n,int *p) {
	n *= 5;
	printf("the value in p is:%d\n",n);
	printf("the p %x : *p %x\n",p,*p );
	*p = 100;
	printf("the value in p is:%d\n",*p);
}
int main() {
	int n = 5;
	int* p = &n;
	abc(n, p);
	printf("the value in main is:%d\n", *p);

	return 0;
}

结果如下:

现在,我们还需要思考一种情况:我们在对指针进行操作部时,有时不希望别人来修改我们的值,这时我们就要引入C语言中的一个语法:

const修饰符

我们可以理解为:一旦前面有const标记,则这个值便不可被修改。

这时我们就有了四种形式:const char *p1;char const* p2; char * const p3;

const char const *p4;看着是不是有点头疼,没事,我们按照上面的思路来分析一下。

const char *p;先向右看,看到“;”截止,向左看,先看到*,升级为指针,随后看到char,确定访问类型,随后又看到const,于是将一整个 *p变为只读。

而我们知道,*p代表的是指针指向空间中的数据,所以这里表示的就是指针指向空间中的数据不可被修改,但是指针指向的空间可以被修改。

例:

#include<stdio.h>

int main() {
	int n = 5;
	int x1 = 15;
	int x2 = 25;
	const int* p1 = &n;
	int const* p2 = &n;
	printf("the p1:%d\n",*p1);
	printf("the p2:%d\n",*p2);
	p1 = &x1;
	p2 = &x2;
	printf("the new p1:%d\n",*p1);
	printf("the new p2:%d\n",*p2);
	return 0;
}

结果如下:

这里我们同时书写了const int *p和int const *p;因为const修饰在*之前,所以这两种书写方式等价。

接下来我们再来看另外一种,即const在*之后。如:int * const p;

先来进行分析:我们从p向右看,看到“;”于是截止,再向左看,发现const,将p变为只读,随后发现*将p升级为指针,再看到int,确定以四个字节进行访问。

前面我们说过,单独的一个p表示的是指针指向的地址,而这里p被const修饰,所以显而易见地,我们不能修改指针所指向的地址,但是我们可以修改该地址中存放的数据。

例:

#include<stdio.h>

int main() {
	int n = 5;
	int x = 6;
	 int* const p1 = &n;
	int * const p2 = &x;
	printf("the p1:%d\n",*p1);
	printf("the p2:%d\n",*p2);
	*p1 = 15;
	*p2 = 25;
	printf("the new p1:%d\n",*p1);
	printf("the new p2:%d\n",*p2);
	return 0;
}

结果如下:

#include<stdio.h>
#include<string.h>
struct student {
	char name[15];
	int age;
	double score;
};

int main() {
	struct student stu;
	strcpy_s(stu.name,strlen("张三")+1, "张三");//这里不懂的有兴趣可以自行了解
	stu.age = 18;
	stu.score = 85.5;
	printf("the student name is :%s ,age is:%d   score is:%f",stu.name,stu.age,stu.score);
	return 0;
}

好了,指针在函数中的运用我们这里也告一段落,接下来我们又要引入一种新的结构:

结构体。

结构体的书写格式:

struct 结构名{

数据类型  变量名;

数据类型 变量名;

}

这种结构常用于书写一个特定的整体行为,比如一个学生行为中就包含姓名、年龄、身高等,如果挨个定义再组合就非常麻烦,所以C语言就我们提供了结构体的概念,现在我们用代码来看一下:

#include<stdio.h>
#include<string.h>
struct student {
	char name[15];
	int age;
	double score;
};

int main() {
	struct student stu;//struct必须要带上,可以理解为这里的struct student已经等同于类似int之类的变量名了
	strcpy_s(stu.name,strlen("张三")+1, "张三");//这里不懂的有兴趣可以自行了解
	stu.age = 18;
	stu.score = 85.5;
	printf("the student name is :%s ,age is:%d   score is:%f",stu.name,stu.age,stu.score);
	return 0;
}

结果如下:

结构体就相当于我们自己定义的变量,而作为变量,我们也可以将其作为函数的传入值。

#include<stdio.h>
#include<string.h>
struct student {
	char name[15];
	int age;
	double score;
};
 void info(struct student data){
	 printf("name:%s age:%d score:%f",data.name,data.age,data.score);
}

int main() {
	struct student stu;
	strcpy_s(stu.name,strlen("张三")+1, "张三");
	stu.age = 18;
	stu.score = 85.5;
	info(stu);
	return 0;
}

结果如下:

当然,在实际运用过程中,我们肯定需要涉及到在函数中修改结构体中的数值的问题,所以这个时候我们也可以用定义指针的方式,来向函数中传入一个

结构体指针

例:

#include<stdio.h>
#include<string.h>
struct student {
	char name[15];
	int age;
	double score;
};
 void info(struct student *data){//传入结构体指针
	 data->age = 24;//传入的是结构体指针后,访问结构体中的数据就改用->的形式
	 data->score = 11.45;
	 printf("in info  :  name:%s age:%d score:%f\n",data->name,data->age,data->score);
}

int main() {
	struct student stu;
	strcpy_s(stu.name,strlen("张三")+1, "张三");
	stu.age = 18;
	stu.score = 85.5;
	printf("in main  :  name:%s age:%d score:%f\n", stu.name, stu.age, stu.score);
	info(&stu);//因为指针要指向地址,所以这里要将stu的地址交给函数
	printf("in main new data is :  name:%s age:%d score:%f\n", stu.name, stu.age, stu.score);
	return 0;
}

运行结果:

好了,关于结构体的只是就差不多是这么多了,最后我们在补充一点:结构体的字节对其齐。

即结构体的空间大小是按照最大类型变量来计算的,如:

struct man{
    int what;
   short can;
    char  i;
    int  say;
        

}

那么底层就会做这么一件事:

这种存储行为就被叫做结构体的字节对齐。

最后,我们每次写struc 结构体名  的格式,如果觉得太麻烦,

可以用typedef关键字将其定义,这也是在之后的实际开发中运用最多的一种方式。

好了,关于C语言指针的所有内容到这里基本上就结束了。

如果对你有帮助的话可以留下一个免费的赞吗,本文章为学习时自行整理的笔记,如果有错误的地方欢迎各位大佬前来纠错或给出建议,对学习计算机感兴趣的同学也可以加入我的群聊一起交♂流哦~(群号:595586832)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值