【C语言指针超详解】-- 指针基础知识,笔试题讲解,你的疑惑都可以在这里得到答案

1.内存和地址

1.1.内存

对于内存和地址的讲解开始之前,我想用一个生活中的案例就行引入:

假设有一栋宿舍楼,你住在100个房间中的一间,你的一个朋友来找你玩,但是他如果想找到你,就得挨个房间去找,这样效率很低。这时,聪明的你立马想到可以给每一个房间编上号,如:

一楼:101,102,103......

二楼:201,202,203......

......

这样,每个房间都进行了编号,只要你告诉你的朋友你在房间的编号,他就可以无比快速的找到你。 

将上面这个例子对照到计算机中,实际上就是将内存划分为一个个内存单元,每个内存单元的大小取一个字节。

计算机中常见的单位(补充):

一个比特位可以存储一个二进制的0或1

其中,每个内存单元就相当于一个学生宿舍,一个字节空间中占八个比特位就相当于一间八人寝,每个人就是一个比特位,每个内存单元也都有一个编号,这个编号就相当于房间的门牌号,有了这个内存单元的编号,CPU就可以快速找到一个内存空间。

在生活中,我们把门牌号叫做地址,在计算机中我们把内存单元也叫做地址。但是在我们C语言中给地址起了一个新名字叫:指针。

所以,我们可以这样理解:内存单元的编号==地址==指针(即一个元素的指针就是它的地址)

1.2 理解编址

我们可以简单的理解为,32位的机器有32根地址总线,每根线只有两种态0,1(有无脉冲),所以每根线能表示两种含义,两根线就四种...所以32根地址线就能表示2^32种含义,且每种含义都代表一个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据通过数据总线传递给CPU内寄存器。

2. 指针变量和地址

2.1 取地址操作符(&)

理解了内存单元和地址的关系,我们回到C语言

创建变量的本质:是向内存中申请一块空间,用来存放数据,而这个空间的名字是a,a不是给计算机和编译器看的,而是给程序员自己看的。比如:

#include<stdio.h>
int main()
{
	int a=10;
	return 0;
}

比如上述代码就是创建了整型变量a,内存中申请四个字节,用于存放整数10,其中每个字节都有地址,上图中四个字节的地址分别是

1  0x00F3FB38

2  0x00F3FB39

3  0x00F3FB3A

4  0x00F3FB3B

那我们如何得到a的地址呢啊?

这里就会学习一个操作符(&)--取地址操作符

#include<stdio.h>
int main()
{
	int a = 10;
	&a;
	printf("%p\n", &a);
	return 0;
}

按照我画图的例子就会打印:0x00F3FB38

&a只会取出a所占的四个字节中较小的一个字节的地址,虽然整型变量a占用了四个字节,我们&a只知道了第一个字节的地址,但是编译器通过顺藤摸瓜访问到四个字节的数据也是可以的。

2.2 指针变量和解引用操作符(*)

2.2.1 指针变量

在前文中我们学习了取地址操作符(&),并通过&拿到的地址是一个数值,比如:0x00F3FB38,但是如果我们后文还需要使用这个数值,为了方便,我们就需要创建一个指针变量来存放它。

比如:

#include<stdio.h>
int main()
{
	int a = 10;
	int* p = &a;//取出a的地址存放在指针变量p中
	printf("%p\n", p);//打印p和打印&a具有同样的效果
	return 0;
}

这里的p就是指针变量,而指针变量也是一种变量,只不过这种变量是用来存放地址的,所以所有存放在指针变量中的值都会被理解为地址。

2.2.2 如何拆解指针类型

我们看到,定义指针变量p用到了int*,所以p的类型就是int*,我们该如何理解指针的类型呢?

int a = 10;
int * p = &a;

这里我们可以看到p的左边是int*,*是在说明p是指针变量,而前面的int就是在说明p是指向整型的指针变量。下面我们通过一幅图来理解

所以,如果有一个char类型的变量ch,那么ch应该放在什么类型的指针变量中呢?

char ch = 'A';
char * p = &ch;

2.2.3 解引用操作符

上文中引出指针变量时,说了是为了后续中还要使用这个地址才创建的指针变量,那么我们已经将地址存放在指针变量中了,那这时怎么使用呢?

C语言中,其实我们拿到了地址(指针),就可以通过地址(指针)找到所指向的对象,这里学习一个操作符叫解引用操作符(*)。

由这幅图可以知道,在第六行,我们打印*p得到的结果是10,而不是一个地址,说明这里使用的解引用操作符*起到了作用,而*p的意思就是通过p中存放的地址,来找到指向的空间,这里*p实际上就是a了,即*p==a。同时如果进行操作*p=0,这个操作符是把a的值改为了0。

但是我们为什么要这么写呢?我们往后继续学习就能发现其中的妙处了。

2.3 指针变量的大小

在前面的内容中我们已经知道,如果假设32位的机器有32根线,每根地址线出来的电信号转换城数字信号后是1或0,那我们把32根地址线产生的2进制序列当作一个地址,那么一个地址就是32个bit位,需要4个字节才能存储。

而指针变量是用来存放地址的,那么指针变量的大小就得是4个字节的空间才可以

也就是说

32位机器中,指针变量的大小是4个字节

64位机器中,指针变量的大小是8个字节 

 

!!!注意指针变量的大小和类型无关,只要指针类型的变量,在相同的平台下,大小都是相同的。

3. 指针变量类型的意义

注意指针变量的大小和类型无关,只要指针类型的变量,在相同的平台下,大小都是相同的,那么为什么还要有各种各样的指针类型呢?

其实是因为不同的指针类型有特殊意义,接下来我们继续学习

3.1 指针的解引用

#include<stdio.h>
int main()
{
	int n = 0x11223344;
	int* pi = &n;
	*pi = 0;
	return 0;
}
#include<stdio.h>
int main()
{
	int n = 0x11223344;
	char* pc = (char *)&n;
	*pc = 0;
	return 0;
}

观察这两段代码,通过调试我们可以看到,代码1会将n的四个字节全部改为0,但是代码2只是将n的第一个字节改为0

结论:指针的类型决定了对指针解引用的时候多大的权限(一次能操作几个字节)。比如:char*的指针的解引用就只能访问一个字节,而int*的指针的解引用救恩那个访问四个字节

3.2 指针+-整数

观察下面这段代码

#include<stdio.h>
int main()
{
	int n = 10;
	char* pc = (char*)&n;
	int* pi = &n;
	printf("&n   =%p\n", &n);
	printf("pc   =%p\n", pc);
	printf("pc+1 =%p\n", pc+1);
	printf("pi   =%p\n", pi);
	printf("pi+1 =%p\n", pi+1);
	return 0;
}

这里我们可以看出,char*类型的指针变量+1跳过一个字节,int*类型的指针变量+1跳过四个字节。这就是指针变量的类型差异带来的变化。同理,-1也是一样的。

结论:指针的类型决定了指针向前或向后走一步有多大(距离)

3.3 void*指针

在所有的指针类型中有一种极其特殊的类型是void*类型的,可以把它理解为无具体类型的指针(泛型指针),这种类型的指针可以用来接收任意类型的地址。但是它也有局限性,void*类型的指针不能直接进行指针的+-整数和解引用的运算。

如:

在上面的这段代码中,将一个int类型的变量的地址赋值给一个char*的指针变量。编译器给出了一个警告,这是因为类型不兼容。而使用void*类型就不会有这个问题。

如:

这里没有警告,说明void*类型的指针可以接收不同类型的指针,但是这里有一个报错“无法取消引用类型为“void”的操作数”说明无法直接进行指针运算。

所以,一般情况下,void*类型的指针是使用在函数的参数部分,用来接收不同类型的返回值,这样的设计可以使函数达到泛型的效果。使得一个函数可以处理多种类型的数据。在后文有详细的讲解。

4. 指针运算

基本运算:

a. 指针+-整数

b. 指针-指针

c. 指针的关系运算

 4.1 指针+-整数

因为数组在内存中是连续存放的,只要知道了第一个元素的地址,顺藤摸瓜就能找到后面的所有元素。

int arr[10] = {1,2,3,4,5,6,7,8,9,10};

代码演示:

#include<stdio.h>
//指针+整数
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* pi = &arr[0];
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(pi + i));//pi+i 这里就是指针+整数
	}
	return 0;
}

观察发现,在数组中,*(pi+i)就相当于arr[0+i]

4.2 指针-指针

#include<stdio.h>
//指针-指针
int my_strlen(char* s)
{
	char* pc = s;//将字符串的首地址传递给pc
	while (*pc != '\0')
	{
		pc++;
	}
	return pc - s;
}

int main()
{
	printf("%d\n", my_strlen("abc"));//运行结果3
	return 0;
}

这是一个运用指针-指针(具体实现在pc-s)来进行计算字符串长度。

4.3 指针的关系运算

#include<stdio.h>
//指针的关系运算
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* pi = &arr[0];
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	while (pi < arr + sz)//指针的大小比较
	{
		printf("%d ", *pi);
		pi++;
	}
	return 0;
}

5. const修饰指针

5.1 const修饰变量

变量是可以被修改的,即使是把变量的地址交给一个指针变量,通过指针变量也可以修改这个变量。但是我们希望一个变量加上一些限制,使得这个变量不能被修改,这应该怎么做呢?这就是const的作用。

例如:

#include<stdio.h>
int main()
{
	int m = 0;
	m = 20;
	const int n = 0;
	n = 20;
	return 0;
}

上述结果可以发现,n的本质仍然是变量,只不过被const修饰以后,在语法上加了限制,如果我们对n进行修改就不符合语法规则,就报错。

但是如果我们不直接针对变量n,而是绕过n,使用n的地址去修改n就能做到(虽然这里是打破语法规则)

#include<stdio.h>
int main()
{
	const int n = 0;
	printf("n = %d\n", n);
	int* pi = &n;
	*pi = 20;
	printf("n = %d\n", n);
	return 0;
}

可以看到,通过修改地址的方式确实把n的值修改了,但是大家仔细想一下,我们使用const的目的是什么?不就是让n的值不被修改吗,我们使用地址打破规则使得n的值被修改了,这是不符合我们的目的。所以我们应该让pi即使拿到n的地址也不能修改p,应该怎么做呢?

5.2 const修饰指针变量

一般情况下,const修饰指针变量时可以放在*的左边,也可以放在*的右边,意义是不一样的。

int * p;//不用const修饰
int const * p;//const放在*左边
int * const p;//const放在*右边

 我们来进行四组测试来具体观察一下有什么区别:

结论:const修饰指针变量时

1 const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量的本身的内容可变

2 const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变

6. 野指针

概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

6.1 形成野指针的原因

6.1.1 指针没有初始化

6.1.2 指针越界访问

6.1.3 指针指向的空间释放

6.2 规避野指针

6.2.1 指针初始化

a. 如果明确知道指针指向哪里就直接赋值;

b. 如果不知道指向哪里,就可以给指针赋值NULL(NULL是C语言中定义的一个标识符常量,值是0),大家可以把NULL直接理解为一个空指针。

这两种方式都是指针初始化。

6.2.2 避免指针越界

当一个程序内存申请了哪些空间,通过指针就只能访问哪些空间,不能超出访问范围。

6.2.3 指针变量不再使用时,及时用NULL置空,使用之前检查有效性

!!!特殊理解:野指针就是野狗,我们不能放任它不管,所以我们可以找一棵树将野狗拴起来,就很安全了,所以给指针及时赋值NULL就相当于把野狗拴起来。不过即使是被拴起来的野狗我们也不能去挑逗它,我们应该提前判断它的安全性。指针也是这样,使用之前判断是否为NULL,看看是不是拴起来的野狗。

NULL的使用演示:

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p++) = i;
	}
	p = NULL;//经过循环后,p已经越位了,将p置空
	p = &arr[0];//重新赋值
	if (p != NULL)//判断p是否为空值
	{
		//...
	}
	return 0;
}

6.2.4 避免返回局部变量的地址

7. assert断言

assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被定义为“断言”

assert(p!=NULL);

当代码运行到这儿时,验证p是否等于NULL。如果确实不等于NULL,程序将继续运行,否则将终止,并给出报错信息。

assert()宏接受一个表达式作为参数时,它只有一个作用,就是判断表达式是否为真,即表达式返回值不为0,这是assert将不会产生任何作为,程序继续运行,反之,assert就会报错

使用assert的好处:

a. 能自动标识文件和出问题的行号

b. 无需更改代码就能开启和关闭assert()的机制。

如果已经确认程序没有问题,不需要再做断言时,就在#include<assert.h>前面,定义一个宏NDEBUG

1 #define NDEBUG

2 #include<assert.h>

然后再重新编译程序时,编译器就会禁用所有的assert()语句。如果再有问题时,注释或移除#define NDEBUG就行。

缺点:

引入了额外的检查,增加了程序运行的时间。

一般我们只用在Debug版本中使用,在Releass版本中选择禁用就行,因为在VS这样的集成开发环境中,在Release版本中,直接就是优化掉了。所有在Debug版本有利于程序员发现问题,在Release中又不影响用户使用时的效率。

8. 指针的使用和传址调用

8.1 strlen的模拟实现

指针的使用讲解我们通过实例strlen的模拟实现来讲解

8.1.1 库函数strlen的功能

用来求字符串的长度,统计的是\0之前的字符个数

8.1.2 使用

size_t strlen(const char * str);

参数str接收一个字符串的起始地址,然后开始统计字符串中\0之前的字符个数,最终返回长度

8.1.3 模拟实现

在前文4.2指针-指针中已经有一个函数是求字符串长度,那就是一个对strlen模拟实现的方法,接下来这种方法将通过计数器的方法实现:

#include<stdio.h>
#include<assert.h>
int my_strlen(const char * str)
{
	//字符串的长度一定大于等于0,所以用size_t定义变量
	size_t count = 0;
    //断言检查:确保传入的指针不为空,防止空指针解引用
	assert(str != NULL);
	while (*str != '\0')
	{
		count++;//实现计数功能
		str++;
	}
	return count;
}

int main()
{
	char a[] = "abcdef";
	size_t len = my_strlen(a);
    //以无符号整数格式打印结果(%zu对应size_t类型)
	printf("%zu\n", len);
	return 0;
}

8.2 传值调用和传址调用

首先我们明白我们学习指针的目的,我们学习指针是为了运用指针更好的解决问题,那么问题来了,什么问题是非指针不可呢?

例如:写一个函数,交换两个整型变量的值

我们用已学过的知识可能写成这样的代码:

void swap(int x, int y)
{
	int t = 0;
	t = x;
	x = y;
	y = t;
}

int main()
{
	int a = 0;
	int b = 0;
	scanf("%d%d", &a, &b);
	printf("交换前:a=%d,b=%d\n", a, b);
	swap(a, b);
	printf("交换后:a=%d,b=%d\n", a, b);
	return 0;
}

通过运行结果发现,其实这段代码并没有产生交换的效果,为什么呢?

通过调试我们发现(这里不进行演示,大家下来可自行调试),此处主函数a,b是实参,将a,b传给函数swap内的x,y是形参,在swap内交换是不影响a,b(因为x,y与a,b的值虽然相同,但是他们的地址不同,也就是说函数在创建形参时是重新申请了一个空间来存放由实参传过来的值)--这就是传值调用。

所以这时我们用到指针

#include<stdio.h>
void swap(int* x, int* y)
{
	int t = 0;
	t = *x;
	*x = *y;
	*y = t;
}

int main()
{
	int a = 0;
	int b = 0;
	scanf("%d%d", &a, &b);
	printf("交换前:a=%d,b=%d\n", a, b);
	swap(&a, &b);//使用指针的话,传的是地址
	printf("交换后:a=%d,b=%d\n", a, b);
	return 0;
}

这里直接将a,b的地址传给swap的x,y,则交换时是直接针对a,b的地址进行的,则使得函数与主函数建立了联系,从而实现可以从函数内部修改主函数中的变量的目的--传址调用。

9. 数组名的理解

在前面4.1章节指针+-整数时,我们写过这样一段代码:

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* pi = &arr[0];

这里我们使用了&arr [ 0 ]的方式拿到了数组的第一个元素的地址。我们来观察下面一段代码:

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("&arr[0]  =%p\n", &arr[0]);
	printf("arr      =%p\n", arr);
	return 0;
}

通过结果,大家肯定有发现&arr [ 0 ]和arr,也就是数组首元素的地址和数组名打印出来的结果一模一样,所以我们就知道了,其实数组名就是数组首元素的地址

相信这时候有很多已经有很多人已经记住了这个结论“数组名就是数组首元素的地址,arr就是&arr[0]”,但是,真的是这样吗?确实是这样的,只不过这里有两个例外罢了。

细心的同学可能在我刚刚引入时,在提到4.1章节的指针+-整数的时候回去看了一眼时,在不经意间已经发现,其实我们已经使用过了数组名(arr),我们写出那段代码:

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz =sizeof(arr)/sizeof(arr[0]);

观察这段代码,如果按照我们刚刚讲的,数组名就是首元素地址,那么sz就应该等于1而不是其他值,但是事实上这里sz等于10,恰好等于了这个数组的元素个数。

经过我们反复思考,最终只能确定一件事不可能出错,那就是除数sizeof(arr[0])中,arr[0]一定是首元素,这是肯定的,而sizeof(arr[0])计算出了首元素的长度。但是这似乎也没有什么用啊。

这时我们换另一个角度思考这个问题。

在小学的时候相信我们都背过一个表达式:商品个数=总价÷商品单价。

恍然大悟,对应到我们的这段代码,sz不就是商品个数嘛,sizeof(arr [ 0 ])是首元素的长度,而一个数组中所有元素都是同一类型的,所以sizeof(arr [ 0 ])可以代表每一个元素的长度也就是商品单价,理所应当的,sizeof(arr)是不是就是总价呀。也就是说arr数组名在sizeof中代表的是整个数组,而sizeof(arr)计算的是整个数组的长度。

另一个例外是:&数组名

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("&arr[0] =%p\n", &arr[0]);
	printf("arr     =%p\n", arr);
	printf("&arr    =%p\n", &arr);
	return 0;
}

神不神奇,这三个打印结果居然一模一样,但是我都已经说了&arr是一种例外,那么arr和&arr肯定有区别,直接说出也无妨,这里的区别主要体现在他们各自+-整数上,运行下面代码

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("&arr[0]   = %p\n", &arr[0]);
	printf("&arr[0]+1 = %p\n", &arr[0] + 1);
	printf("arr       = %p\n", arr);
	printf("arr+1     = %p\n", arr + 1);
	printf("&arr      = %p\n", &arr);
	printf("&arr+1    = %p\n", &arr + 1);
	return 0;
}

通过运行结果可以发现,&arr[ 0 ]和&arr[ 0 ]+1相差4个字节,arr和arr+1相差4个字节,是因为&arr[0]和arr都是首元素的地址,+1就是跳过一个元素。但是&arr和&arr+1相差了40个字节,这就是因为&arr是数组的地址,+1跳过的是整个数组。

总的来说,数组名就是数组首元素的地址,但是有两个例外。

两个例外:

a. sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。

b. &数组名,这里的数组名也表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)。

!!!除此之外,其他任何地方使用数组名,数组名都表示首元素的地址。

10. 使用指针访问数组

有了前面知识的支持,并结合数组的特点,我们就可以很方便的使用指针访问数组了。

例如:用指针访问数组实现数组的输入和输出

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* pi = arr;
	for (i = 0; i < sz; i++)
	{
		scanf("%d", pi + i);
		//或者scanf("%d",arr+i);
		//或者scanf("%d",arr[i]);
		//这三种方式都能实现数组arr的输入
	}
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(pi + i));
		//printf("%d ",arr[i]);这种方式也能打印出来
	}
	return 0;
}

通过这个代码我们再分析,由于数组名arr是数组首元素的地址,可以赋值给pi,其实在这里数组名arr和pi在这里是等价的。那么既然我们可以用arr [ i ]来访问数组,是不是也可以用pi [ i ]来访问数组呢?观察下面这段代码:

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* pi = arr;
	for (i = 0; i < sz; i++)
	{
		scanf("%d", pi + i);
	}
	for (i = 0; i < sz; i++)
	{
		printf("%d ", pi[i]);//用pi[i]的方式实现数组的输出
		//printf("%d ", *(pi + i));
	}
	return 0;
}

在输出数组的地方我们可以看到,我们用pi [ i ]代替了*(pi + i),并且成功的实现了数组打印,所以在本质上pi [ i ]是等价于*( pi + i )。同理arr[i]等价于*(arr + i)

总结:

arr [ i ] == *( arr + i )                pi [ i ] == *( pi + i ) 
 

11. 一维数组传参的本质

我们在以前的学习中已经知道,数组是可以传递给函数的,那么数组的传递的本质是什么呢?

引入:创建一个函数test,将数组传给test,并在函数内实现计算数组的元素个数并输出

#include<stdio.h>

void test(int arr[])
{
	int sz2 = sizeof(arr) / sizeof(arr[0]);//计算传参后数组的元素个数
	printf("sz2=%d\n", sz2);
}

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz1 = sizeof(arr) / sizeof(arr[0]);//计算数组本来的元素个数
	printf("sz1=%d\n", sz1);
	test(arr);//将数组传给test
	return 0;
}

可以看到sz1的值与sz2的值是不同的,并且在x86和x64不同的环境下两次sz2的值也不同。所以我们发现在函数test内部是没有获得正确的元素个数的。

这就涉及到了传参的本质了,上一章中我们学习了“数组名就是首元素的地址”,那么在数值传参时,传递的是arr数组名,也就是说在本质上:数组传参传递的是数组首元素的地址

所以,由于传递的是地址,我们在函数形参的部分理论上来说是需要使用指针来接收这个地址的。这样的话就能解释函数内部sizeof(arr)其实计算的是一个地址的大小(字节)而不是整个数组的大小(字节),并且这也能解释为什么环境不同sz2的值为什么不同。正是因为数组传参的本质是地址,所以我们在一个函数内部是无法计算数组元素个数的。

#include<stdio.h>

void test1(int arr[])//写成数组形式,本质上是指针
{
	printf("%d\n", sizeof(arr));
}

void test2(int* arr)//将形参写作指针形式
{
	printf("%d\n", sizeof(arr));
}

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	test1(arr);
	test2(arr);
	return 0;
}

通过结果发现:

一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式

两种方式都是正确的,只不过本质上是指针的形式。

12. 冒泡排序

冒泡排序的核心思想就是:两两相邻的元素进行比较和交换。

先看代码,我再讲解

#include<stdio.h>

void bubble_sort(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int j = 0;
		for (j = 0; j < sz - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int t = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = t;
			}
		}
	}
}

int main()
{
	int arr[] = { 5,4,8,7,6,3,1,2,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

 

13. 二级指针

指针变量也是变量,只要是变量就有地址,即使他是用来存放地址的,那指针变量的地址存放在哪?

这就是二级指针

#include<stdio.h>
int main()
{
	int a = 10;
	int* pi = &a;
	int** ppi = &pi;
	return 0;
}

对于二级指针的运算:

*ppi通过对ppi中的地址进行解引用,这样找到的是pi,所以*ppi访问的就是pi。

**ppi先通过*ppi找到pi,再对pi进行解引用*pi,找到的是a

同理:三级,四级等更高级指针也是这样进行分析的

14. 指针数组

14.1 指针数组

回想一下,我们已经学过了两种数组

整型数组:int arr10 ; //存放的是整数的数组

字符数组:char arr10 ; //存放的是字符的数组

同理,指针数组就是存放指针的数组

int* arr[ 3 ]={ &a , &b , &c }; //int* arr[ 3 ]就是一个指针数组

如图

!!!指针数组的每一个元素都是地址,又可以指向一块区域。

14.2 用指针数组模拟二维数组

#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* parr[3] = { arr1,arr2,arr3 };//数组名是数组的首元素地址
	int i = 0;
	int j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 5; j++)
		{
			printf("%d ", parr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

parr[ i ]是访问parr数组的元素,parr[ i ]找到的数组元素其实指向了整型一维数组,parr[ i ][ j ]就是整型一维数组中的元素。

但是有上述代码模拟出的二维数组并非完全是二维数组,因为每一行并非连续的,如下图:

15. 字符指针变量

存放字符地址的指针变量

使用1:

#include<stdio.h>
int main()
{
	char ch = 'w';
	char* pc = &ch;
	*pc = 'w';
	return 0;
}

使用2:

#include<stdio.h>
int main()
{
	const char* pstr = "hello world.";
	printf("%s\n", pstr);
	return 0;
}

关于使用2,相信很多人和我的反应一样,“这写的什么啊”“是不是写错了啊”“指针不是存放的地址吗,为什么右边没有取地址操作费啊”。大家有这样的想法是完全正常的,毕竟我们在前面基本上没有遇见过表达式右边没有取地址操作符的情况,唯一一个还是放的数组名,但是这里是数组名已经是首元素的地址了。

诶,诶,诶,有没有想到什么,同样是没有取地址操作符,而数组名表示了首元素的地址,那么,我们字符指针变量是不是就是把右边字符串的首字符的地址存放到pstr内呢?答案是的

 上面这段代码的意思就是把一个常量字符串的首字符h的地址存放到指针变量pstr中。

实战分析:(大家先看代码思考再看后面的答案)

int main()
{
	char str1[] = "hello world.";
	char str2[] = "hello world.";
	const char* str3 = "hello world.";
	const char* str4 = "hello world.";
	if (str1 == str2)
		printf("str1 and str2 are same\n");//......1
	else
		printf("str1 and str2 are not same\n");//..2
	if(str3 == str4)
		printf("str3 and str4 are same\n");//......3
	else
		printf("str3 and str4 are not same\n");//..4
	return 0;
}

大家可以思考一下这段代码最终的输出是1 3,1 4还是2 3,2 4。

答案是:2 3

分析:

  1. str1 和 str2 的比较情况

    • str1 和 str2 属于字符数组,在内存里会分别为它们分配空间,用来存放字符串的副本。
    • 当使用 == 对这两个数组进行比较时,实际上比较的是它们的内存地址,并非字符串的内容。由于它们的内存地址不一样,所以比较结果为 false
  2. str3 和 str4 的比较情况

    • str3 和 str4 是指向字符串字面量的指针。
    • 通常情况下,相同的字符串字面量在内存中只会有一个实例。所以,str3 和 str4 这两个指针指向的是同一个内存地址(均指向‘h’的地址)。
    • 当使用 == 对这两个指针进行比较时,比较的就是它们所指向的地址,由于地址相同,因此比较结果为 true

16. 数组指针变量

16.1 数组指针变量是什么?

在13.指针数组一章节中,指针数组是一种数组,数组中存放的是地址。

那么数组指针变量是指针变量?还是数组?

答案:指针变量。

类比:

字符指针变量:char* p;//指向字符的指针变量,存放字符的地址

整型指针变量:int* p;//指向整型的指针变量,存放整型的地址

那么类比得到:数组指针变量:指向数组的指针变量,存放数组的地址

思考:观察下面两行代码,指出哪个是数组指针变量

int* p1[10];
int (*p2)[10];

答案:int (*p2)[10]是数组指针变量

解释:

  • 对于int* p1[10];:根据运算符优先级,[]优先级高于* 。所以p1先与[]结合,这表明p1是一个数组,数组元素个数为10 ,每个元素的类型是int* ,即指向int类型的指针,所以p1是指针数组 。
  • 对于int (*p2)[10];:这里p2()括起来后再与*结合,说明p2是一个指针 ,它指向的是一个包含10int类型元素的数组,符合数组指针的定义,所以p2是数组指针变量。

!!![ ]的优先级高于*,如果要定义一个数组指针变量,一定要加上()来保证p和*先结合。

16.2 数组指针变量的初始化

由于数组指针变量是用来存放数组地址的,那怎么获得数组的地址呢?就是通过我们之前学习的&数组名。

int arr[10]={0};
&arr;//得到的就是数组arr的地址

 所以如果要存放这个地址,就得存放在数组指针变量中

int (*p)[10]=&arr;

 通过调试我们也能发现&arr和p的类型是完全相同的。

数组指针类型解析:

int   (*p) [10] = &arr;

int--->p指向的数组的元素类型

*p--->p是数组指针变量名

10--->p指向数组的元素个数 

 17. 二维数组传参的本质

在过去,如果我们需要将二位数组传参给一个函数时,我们是这样进行的:

#include<stdio.h>
void test(int arr[3][5], int r, int c)
{
	int i = 0;
	for (i = 0; i < r; i++)
	{
		int j = 0;
		for (j = 0; j < c; 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;
}

我们先来讲解一下这个二维数组:二维数组其实可以看做是每个元素是一维数组的数组,也就是二维数组的每个元素是一个一维数组。那么二维数组的首元素就是第一行,是一个一维数组。如:

所以跟据这个规则,二维数组的数组名表示的就是第一行的地址,是一维数组的地址。跟据上面代码,第一行的一维数组的类型就是arr[ 5 ],所以第一行的地址的类型就是数组指针类型int(*)[ 5 ]。那就意味二维数组传参本质上也是传递了地址,传递的是第一行这个一维数组的地址,那么形参也可以写成指针的形式。 

另一个层面的理解:

内存布局角度

二维数组在内存中按行优先顺序连续存储 ,可看作 “数组的数组”,即每个元素本身又是一个数组。例如 int arr[3][4] ,可理解为 arr 是包含 3 个元素的数组,每个元素是一个包含 4 个 int 类型元素的一维数组 。

“数组到指针” 退化角度

当二维数组作为函数参数传递时,会发生 “数组到指针” 的退化:

  • 传递内容:实际传递的是二维数组第一行的地址 。因为数组名代表数组首元素地址,而二维数组首元素是第一行这个一维数组,所以传递的是指向第一行的指针。
  • 形参声明
    • 完整二维数组形式:如 void func(int arr[][4], int row) ,其中第二维大小必须明确指定,第一维大小可省略(函数内部无法通过这种声明准确得知二维数组行数) 。
    • 省略第一维大小形式:和上述类似,重点也是强调第二维大小明确 。
    • 数组指针形式: void func(int (*arr)[4], int row) , arr 是一个指向包含 4 个 int 元素的一维数组的指针,这种形式更能体现二维数组传参本质是传递数组指针 。这三种声明方式在编译器看来完全等价,都会被视为 int (*)[N]N 为第二维大小)类型的参数 。

由于这里较难理解,所以我给出了两个理解方向,但是总的来说二维数组传参,形参的部分可以是数组,也可以是指针形式。

18. 函数指针变量

18.1 函数指针变量的创建

跟据前面学习整型指针,数组指针的经验,我们可以得出:函数指针变量是存放函数地址的,指向的是函数(这也反映出了函数是具有地址的)

测试函数是否有地址:

#include<stdio.h>
void test()
{
	printf("hehe\n");
}

int main()
{
	printf("test:    %p\n", test);
	printf("&test:   %p\n", &test);
	return 0;
}

通过测试结果已经可以肯定函数是具有地址的,并且函数名就是函数的地址,当然通过&函数名的方式也可以得到函数的地址。

所以在这里我们要将函数的地址存放起来,就需要用到函数指针变量,函数指针变量和数组指针变量同样都是指针变量,所以二者的写法有异曲同工之妙。如下:

void test()
{
	printf("hehe\n");
}

void (*pf1)() = &test;
void (*pf2)() = test;

int Add(int x, int y)
{
	return x + y;
}

int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x,y写不写都是可以的

函数指针类型解析:

int (*pf3)(int x,int y):去掉pf3得到的int (*)(int x,int y)就是pf3函数指针变量的类型

           int          ---pf3指向函数的返回类型

          *pf3        ---pf3是函数指针变量

(int x,int y) ---pf3指向函数的参数类型和个数的交代

 18.2 函数指针变量的使用

eg1:通过函数指针调用指针指向的函数。

#include<stdio.h>
int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int (*pf3)(int, int) = Add;
	printf("%d\n", (*pf3)(2, 3));//利用函数指针调用指针指向的函数
	printf("%d\n", pf3(2, 3));//直接调用函数
}

eg2:

(*(void(*)())0)();

解释这段代码:

a.    void(*)()是一个函数指针类型,这个指针指向的函数没有参数,返回值类型是void

b. (void(*)())0 这段代码是在将0强制类型转换为这种函数指针类型,这就意味着0地址处有这么一种函数

c.    *(void(*)())0  对0地址处进行解引用,就能获取到位于地址 0 处的函数

d. (*(void(*)())0)() 这是一个函数调用操作,它会调用前面解引用得到的函数,并且在调用时不传递任何参数

eg3:

void (*signal(int ,void(*)(int)))(int);

 这段代码是标准 C 库中的 signal 函数声明,用于设置信号处理函数。其作用是注册一个信号处理函数,当特定信号发生时,系统将调用该函数

函数原型拆解

  1. 函数名称和参数

    • signal 是函数名,接受两个参数:
      • int signum:要捕获的信号编号(如 SIGINTSIGTERM 等)。
      • void(*func)(int):指向信号处理函数的指针,该函数接受一个 int 参数(信号编号),返回值为 void
  2. 返回值

    • signal 的返回值是一个函数指针,指向类型为 void(*)(int) 的函数。
    • 这个返回的函数指针是之前注册的信号处理函数(如果存在),或 SIG_DFL(默认处理)、SIG_IGN(忽略信号)

18.2.1 typedef关键字

typedef作用:用来重命名的,可以将复杂的类型简单化。

基本语法:typedef 原类型名 新类型名;

  • 原类型名:可以是基本类型(如 intchar)、指针、结构体、联合体、函数指针等。
  • 新类型名:自定义的别名,通常遵循类型命名约定(如以 _t 结尾)。

例如:当我们想写无符号整型数据类型 unsigned int时,我们又觉得他太长了写起来不方便,如果能简洁的写如:unit的话就方便了,那么我们可以:

typedef unsigned int unit;
//将unsigned int重命名为unit

指针类型重命名:

typedef int* ptr_t;
//将int*重新命名为ptr_t

数组指针重命名:

typedef int (*parr_t)[5];//新的类型名必须在*的右边
//将数组指针类型int(*)[5]重新命名为parr_t

函数指针重命名:

typedef void(*pf_t)(int);//新的类型名必须在*的右边
//将void(*)(int)重新命名为pf_t

所以对于17. 二维数组传参的本质中的eg3代码:void (*signal(int ,void(*)(int)))(int),我们可以简化为:

typedef void(*pf_t)(int);
pf_t signal(int ,pf_t);

19. 函数指针数组

在以前的学习中我们已经知道,数组是一个存放相同类型数据的存储空间,我们已经学习的指针数组,如:int* arr[10];//数组的每个元素是int*

所以如果要把函数的地址存到一个数组中,那这个数组就叫做函数指针数组

写法:int (*parr[4])(int ,int)={Add,Sub,Mul,Div}

!!!相当于就是将函数指针的指针变量写作为数组指针变量:p-->parr[4]

应用:转移表

20. 转移表

计算器的实现:利用代码实现加减乘除

#define _CRT_SECURE_NO_WARNINGS

#include<stdio.h>

void menu()
{
	printf("*****************************\n");
	printf("** 1.Add    2.Sub    3.Mul **\n");
	printf("**    4.Div       5.exit   **\n");
	printf("*****************************\n");
}

int Add(int x, int y)
{
	return x + y;
}

int Sub(int x, int y)
{
	return x - y;
}

int Mul(int x, int y)
{
	return x * y;
}

int Div(int x, int y)
{
	return x / y;
}

int main()
{
	int x, y;
	int input;
	int t = 0;
	do
	{
		menu();
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
			case 1:
				printf("请输入操作数:");
				scanf("%d %d", &x, &y);
				t = Add(x, y);
				printf("t=%d\n", t);
				break;
			case 2:
				printf("请输入操作数:");
				scanf("%d %d", &x, &y);
				t = Sub(x, y);
				printf("t=%d\n", t);
				break;
			case 3:
				printf("请输入操作数:");
				scanf("%d %d", &x, &y);
				t = Mul(x, y);
				printf("t=%d\n", t);
				break;
			case 4:
				printf("请输入操作数:");
				scanf("%d %d", &x, &y);
				t = Div(x, y);
				printf("t=%d\n", t);
				break;
			case 0:
				printf("退出程序\n");
			default:
				printf("选择错误\n");
				break;
		}
	} while (input);
	return 0;
}

跟据可以看出,我们已经代码实现了加减乘除,但是我们能发现我们实现这个功能时使用到了switch语句,在每一个case下都存在四行代码一样,这使得我们的代码行看起来过于臃肿,所以我们拥戴函数指针数组来简化一下:

#include<stdio.h>

void menu()
{
	printf("*****************************\n");
	printf("** 1.Add    2.Sub    3.Mul **\n");
	printf("**    4.Div       0.exit   **\n");
	printf("*****************************\n");
}

int Add(int x, int y)
{
	return x + y;
}

int Sub(int x, int y)
{
	return x - y;
}

int Mul(int x, int y)
{
	return x * y;
}

int Div(int x, int y)
{
	return x / y;
}

int main()
{
	int x, y;
	int input;
	int t = 0;
	do
	{
		menu();
		printf("请选择:");
		scanf("%d", &input);
		if (input == 0)
		{
			printf("退出计算器\n");
		}
		else if (input >= 1 && input <= 4)
		{
			int (*parr[5])(int, int) = { NULL,Add,Sub,Mul,Div };
			printf("请输入两个操作符:");
			scanf("%d %d", &x, &y);
			t = parr[input](x, y);
			printf("%d\n", t);
		}
	} while (input);
	return 0;
}

大家肯定有疑问,比如在函数数值数组部分为什么数组大小定义为5?并且为什么后面出现了一个NULL?在我们下意识的情况下,我们只有加减乘除四个函数,按道理来说确实应该只定义数组大小为4并且没有NULL啊,比如:

相信大多数人的第一反应就是这样,但是大家有没有想过一个问题,我们从键盘中输入的input是从0-4,是五个数字,并且在input=0时我们是要退出这个计算机的,所以input的范围变为1-4,而我们为了在 t = parr[input](x, y)调用函数指针数组时刚好用input的值,所以函数指针数组的元素就需要集体向后一步,然后在前面用空值NULL填充,如下图:

这就是函数指针数组的运用,可以帮助我们减小代码的臃肿 。

21. 回调函数

就是一个通过函数指针调用的函数。

如果你把函数的指针作为参数传递给另一个函数,当这个指针被用来调用其指向的函数时被调用的函数就是回调函数。

这里我们再次对上一章节的计算机函数进行优化。

同样的,优化只针对红色框switch语句进行:

 22. qsort的使用举例

qsort 是标准库提供的一个用于快速排序的函数,它可以排序任意类型的数据。

22.1 使用qsort排序整型数据

#include <stdio.h>
#include <stdlib.h>
 
// 比较函数,用于比较两个整数的大小
int compare(const void *a, const void *b) 
{
    return (*(int*)a - *(int*)b);
}
 
int main() {
    int arr[] = {5, 3, 8, 1, 2};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    //调用qsort函数
    qsort(arr, n, sizeof(arr[0]), compare);
 
    //打印数组
    for (int i = 0; i < n; i++) 
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
 
    return 0;
}

22.2 使用qsort排序结构数据

#include<stdio.h>
#include<stdlib.h>
 
//创建结构体变量 
 
struct Student
{
    char name[30];
    int age;
};
 
//写比较函数
 
int compare(const void* a,const void* b)
{
return (*(struct Student*)a).age-(*(struct Student*)b).age;
}
 
void printarr(struct Student arr[],int sz)
{
	int i=0;
	for(i=0;i<sz;i++)
	{
	printf("%s: %d\n",arr[i].name,arr[i].age);
	}
}
 
void test()
{
    struct Student arr[]= {{"zhangsan",20},{"lisi",18},{"wangwu",31}};
    //计算数组长度 
    int sz=sizeof(arr)/sizeof(arr[0]);
    //调用函数 
    qsort(arr,sz,sizeof(arr[0]),compare);
    //打印数组 
    printarr(arr,sz);
 
}
//主函数 
int main()
{
	//排序函数 
    test();
    return 0;
}

22.3 用冒泡排序模拟实现qsort函数

#include<stdio.h>
#include<stdlib.h>
 
struct student
{
    char name[30];
    int age;
};
 
void swap(char* buf1,char* buf2,int width)
{
	int i=0;
	char t=0;
	for(i=0;i<width;i++)
	{
		t=*buf1;
		*buf1=*buf2;
		*buf2=t;
		buf1++;
		buf2++;
	}
}
 
int compare(const void* a,const void* b)
{
	return (((struct student*)a)->age-((struct student*)b)->age);
}
 
void bubble_sort(void* base,size_t sz,size_t width,int(*compare)(const void* a,const void* b))
{
	int i=0;
	for(i=0;i<sz-1;i++)
	{
		int j=0;
		for(j=0;j<sz-i-1;j++)
		{
			if(compare((char*)base+j*width,(char*)base+(j+1)*width)>0)
			{
				swap((char*)base+j*width,(char*)base+(j+1)*width,width);
			}
		}
	}
}
 
void printarr(struct student arr[],int sz)
{
	int i=0;
	for(i=0;i<sz;i++)
	{
		printf("%s: %d\n",arr[i].name,arr[i].age);
	}
}
 
void test()
{
	struct student arr[]={{"zhangsan",20},{"lisi",18},{"wangwu",31}};
	int sz=sizeof(arr)/sizeof(arr[0]);
	bubble_sort(arr,sz,sizeof(arr[0]),compare);
	printarr(arr,sz);
}
 
int main()
{
	test();
	return 0;
}

qsort是C语言中可以直接调用的函数,大家参考上面22.1和22.2就可以基本使用了,如果大家想要更深入的理解这个函数以及我们是怎么一步一步将它模拟实现的,大家可以点击下面这个链接,这是一篇我专门为qsort写的文章,有兴趣的小伙伴可以去看看。

https://blog.csdn.net/2501_91996366/article/details/147902818?spm=1001.2014.3001.5501

23. sizeof和strlen的对比

23.1 sizeof(操作符)

功能:计算变量、类型或者表达式占用的内存字节数。

!!!只在乎内存空间的大小,不在乎内存中放的什么数据,只要是地址,大小就是4/8

sizeof(a+3.8)=8;//因为a+3.8的结果为double类型
//a为int类型,3.8为double类型,执行+时,自动将a转换为double类型

使用:sizeof(类型名)或者sizeof(表达式)

示例:

int arr[5];  
printf("%zu\n", sizeof(arr));  // 输出20(假设int占4字节)

返回值:返回的是size_t类型(无符号整数),表示字节数。

对字符串:

char str[] = "hello";  
printf("%zu\n", sizeof(str));  // 输出6(包含'\0')

对指针:对指针变量,返回指针本身的大小(4/8)

char *ptr = "hello";  
printf("%zu\n", sizeof(ptr));  // 输出4/8(32/64位系统)

对数组:返回整个数组的大小(元素个数*元素大小)

int arr[10];  
printf("%zu\n", sizeof(arr));  // 输出40(假设int占4字节)

 23.2 strlen(库函数)

功能:计算以‘\0’结尾的字符串的实际长度(不包含\0)

!!!必须包含头文件string.h

使用:strlen(字符串指针)

示例:

char str[] = "hello";  
printf("%zu\n", strlen(str));  // 输出5

返回值:返回size_t类型,表示字符串长度。

对字符串:只计算有效字符长度,不包含\0。

char str[] = "hello";  
printf("%zu\n", strlen(str));  // 输出5

指针:计算指针所指向的字符串长度。

char *ptr = "hello";  
printf("%zu\n", strlen(ptr));  // 输出5

对数组:如果数组以\0结尾,strlen会读取内存直到遇到\0,统计\0之前的数据。如果数组未以\0结尾,strlen会继续读取内存直到\0为止,而这个\0的位置是不确定的,所以将会产生随机数。

char arr[] = {'h', 'e', 'l', 'l', 'o'};  // 无'\0'  
printf("%zu\n", strlen(arr));  // 可能输出随机值(未定义行为)

23.3 总结

23.3.1 注意事项:

  • sizeof

    • 对函数参数中的数组,sizeof返回指针大小,而非数组大小。
    • 示例:
      void func(int arr[]) 
      {  
          printf("%zu\n", sizeof(arr));  // 输出8(指针大小)
      }
  • strlen

    • 必须确保字符串以\0结尾,否则会导致越界读取。
    • 示例:
      char str[5] = {'h', 'e', 'l', 'l', 'o'};  // 无'\0'  
      strlen(str);  // 错误:可能导致段错误

 23.3.2 总结:

特性sizeofstrlen
功能计算内存大小(字节)计算字符串长度(不含\0
返回值类型或变量的字节数字符串有效字符数
执行时机编译时运行时
\0的处理包含\0(如果是字符串字面量)不包含\0,且必须以\0结尾
典型场景内存分配、数组大小计算字符串操作(复制、打印等)

sizeofstrlen

1. sizeof是操作符

2. sizeof计算操作符所占内存的大小,单位是字节

3. 不关注内存中存放什么数据

1. strlen是库函数,使用需要包含头文件string.h

2. strlen是求字符串长度的,统计的是\0之前的字符个数

3. 关注内存中是否有\0,如果没有\0,就会持续往后找,可能会越界

(为什么是可能呢?因为\0的位置不确定,有可能这个\0刚好在最后一个字符的后面)

24. 重点:数组和指针笔试题解析

24.1 数组笔试题

24.1.1 一维数组

eg1:

int a[] = { 1,2,3,4 };
printf("%zu\n", sizeof(a));//16

a是数组名,数组名单独放在sizeof中,数组名表示整个数组,计算的是整个数组的大小,单位是字节

eg2:

int a[] = { 1,2,3,4 };
printf("%zu\n", sizeof(a+0));//4/8

a是数组名,因为a后面有+0,所以并没有单独放在sizeof内部,也没有&,所以a就是首元素的地址,a+0也是首元素的地址,所以sizeof(a+0)计算的是一个地址的大小,4/8

eg3:

int a[] = { 1,2,3,4 };
printf("%zu\n", sizeof(*a));//4

a是数组名,因为a前面有*,所以并没有单独放在sizeof内部,也没有&,所以a就是首元素的地址,*a就是首元素==a[0],就是整数1,sizeof(*a)就是四个字节

eg4:

int a[] = { 1,2,3,4 };
printf("%zu\n", sizeof(a+1));//4/8

a是数组名,并没有单独放在sizeof内部,所以a就是首元素的地址,a+1就是第二个元素的地址,所以sizeof(a+1)计算的就是地址的大小。

eg5:

int a[] = { 1,2,3,4 };
printf("%zu\n", sizeof(a[1]));//4

a[1]是第二个元素,大小是四个字节

eg6:

int a[] = { 1,2,3,4 };
printf("%zu\n", sizeof(&a));//4/8

&a取出的是数组a的地址,数组的地址也是地址,只要是地址那么大小就是4/8

eg7:

int a[] = { 1,2,3,4 };
printf("%zu\n", sizeof(*&a));//16

&a取出数组a的地址,他的类型是int(*)[4],对于数组指针解引用,访问的是这个数组,大小就是16个字节。另一种理解:*&a==a,sizeof(*&a)==sizeof(a)

eg8:

int a[] = { 1,2,3,4 };
printf("%zu\n", sizeof(&a+1));//4/8

&a取出数组a的地址,+1是跳过整个数组,所以&a+1就是跳过整个数组a的地址,是地址就是4/8

eg9:

int a[] = { 1,2,3,4 };
printf("%zu\n", sizeof(&a[0]));//4/8

a[0]是第一个元素,&a[0]就是第一个元素的地址,还是地址

eg10:

int a[] = { 1,2,3,4 };
printf("%zu\n", sizeof(&a[0]+1));//4/8

&a[0]是第一个元素的地址,+1后就是第二个元素的地址,还是地址

 24.1.2 字符数组

eg1:

int main()
{
	char a[] = { 'a','b','c','d','e','f' };
	printf("%zu\n", sizeof(a));//6,a表示整个数组,所以sizeof计算的是整个数组的大小
	printf("%zu\n", sizeof(a+0));//4/8,a是首元素的地址,a+0还是地址
	printf("%zu\n", sizeof(*a));//1,a是首元素的地址,*a是首元素,sizeof计算的是元素大小
	printf("%zu\n", sizeof(a[1]));//1,a[1]是首元素
	printf("%zu\n", sizeof(&a));//4/8,&a取出的是数组的地址
	printf("%zu\n", sizeof(&a+1));//4/8,&a+1是跳过数组后的地址
	printf("%zu\n", sizeof(&a[0]+1));//4/8,&a[0]+1是第二个元素的地址
	return 0;
}

eg2:

int main()
{
	char a[] = { 'a','b','c','d','e','f' };
	printf("%zu\n", strlen(a));//随机值,a是首元素的地址,字符串中无\0
	printf("%zu\n", strlen(a+0));//随机值,a是首元素的地址,+0还是首元素的地址,字符串中没有\0
	printf("%zu\n", strlen(*a));//非法访问,程序崩溃,a是首元素的地址,*a是首元素‘a’=97,是一个字符值,strlen 需要的是指针,而非字符值。
	printf("%zu\n", strlen(a[1]));//非法访问,a[1]==‘b’==98,是一个字符值,strlen 需要的是指针,而非字符值。
	printf("%zu\n", strlen(&a));//随机值,&a取出数组a的地址,从数组的地址也就是数组的起始位置开始向后统计字符串的长度找\0,但是无\0
	printf("%zu\n", strlen(&a+1));//随机值,&a+1跳过整个数组a后的地址找\0,但是无\0
	printf("%zu\n", strlen(&a[0]+1));//随机值,&a[0]+1是第二个元素的地址
	return 0;
}

接下来的练习题将不再挨个讲解,分析方法和上面一模一样,给出答案,大家自行思考

eg3:

int main()
{
	char a[] = "abcdef";
	printf("%zu\n", sizeof(a));//7
	printf("%zu\n", sizeof(a + 0));//4/8
	printf("%zu\n", sizeof(*a));//1
	printf("%zu\n", sizeof(a[1]));//1
	printf("%zu\n", sizeof(&a));//4/8
	printf("%zu\n", sizeof(&a + 1));//4/8
	printf("%zu\n", sizeof(&a[0] + 1));//4/8
	return 0;
}

eg4:

int main()
{
	char a[] = "abcdef";
	printf("%zu\n", strlen(a));//6
	printf("%zu\n", strlen(a + 0));//6
	printf("%zu\n", strlen(*a));//err
	printf("%zu\n", strlen(a[1]));//err
	printf("%zu\n", strlen(&a));//6
	printf("%zu\n", strlen(&a + 1));//随机值
	printf("%zu\n", strlen(&a[0] + 1));//5
	return 0;
}

eg5:

#include<stdio.h>
int main()
{
	char* p = "abcdef";
	printf("%zu\n", sizeof(p));//4/8
	printf("%zu\n", sizeof(p + 1));//4/8
	printf("%zu\n", sizeof(*p));//1
	printf("%zu\n", sizeof(p[0]));//1
	printf("%zu\n", sizeof(&p));//4/8
	printf("%zu\n", sizeof(&p + 1));//4/8
	printf("%zu\n", sizeof(&p[0] + 1));//4/8
	return 0;
}

eg6:

#include<stdio.h>
#include<string.h>
int main()
{
	char* p = "abcdef";
	printf("%zu\n", strlen(p));//6
	printf("%zu\n", strlen(p + 1));//5
	printf("%zu\n", strlen(*p));//err
	printf("%zu\n", strlen(p[0]));//err
	printf("%zu\n", strlen(&p));//随机值 
	printf("%zu\n", strlen(&p + 1));//随机值 
	printf("%zu\n", strlen(&p[0] + 1));//5
	return 0;
}

24.1.3 二维数组

int main()
{
	int a[3][4] = {0};
	printf("%zu\n",sizeof(a));//48,a为整个数组的大小 
	printf("%zu\n",sizeof(a[0][0]));//4,第一个元素的大小 
	printf("%zu\n",sizeof(a[0]));//16,a[0]这个数组的首元素,是第一行,单独放在sizeof的内部,a[0]表示第一行这个代码,所以a[0]计算的是第一行的大小 
	printf("%zu\n",sizeof(a[0]+1));//4/,a[0]就是第一行第一个元素的地址==&a[0][0],a[0]+1就是第一行第二个元素的地址 
	printf("%zu\n",sizeof(*(a[0]+1)));//4,*(a[0]+1)是第一行第二个元素 
	printf("%zu\n",sizeof(a+1));//4/8,a是二维数组的数组名,这里只能表示数组首元素的地址,也就是第一行的地址,a+1就是第二行的地址 
	printf("%zu\n",sizeof(*(a+1)));//16,*(a+1)==a[1],第二行的大小 
	printf("%zu\n",sizeof(&a[0]+1));//4/8,a[0]是第一行的数组名,&a[0]取出的是第一行的地址,&a[0]+1就是第二行的地址 
	printf("%zu\n",sizeof(*(&a[0]+1)));//16
	printf("%zu\n",sizeof(*a));//16,a是二维数组的数组名,这里只能表示首元素的地址,也就是第一行的地址,*a就是第一行 
	printf("%zu\n",sizeof(a[3]));16,sizeof在计算变量/数组的大小的时候,是通过类型来推导的,不会真实去访问内存空间,所以a[3]并没有越界访问,反而是跟据推导得出,a[3]和a[0]一样具有四个元素 
	return 0;
}

总之:在计算sizeof和strlen是都需要先行判断数组名a的意义,逐步刨析

数组名的意义:

a. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小

b. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址

c. 除此之外所有的数组名都表示为首元素的地址

24.2 指针运算笔试题 

eg1:

#include<stdio.h>
int main()
{
	int a[5] = { 1,2,3,4,5 };
	int* ptr = (int*)(&a + 1);
	printf("%d,%d", *(a + 1), *(ptr - 1));
	return 0;
}

eg2:

//在x86环境下
//假设结构体的大小是20字节
//程序输出的结果?
#include<stdio.h>
struct Test
{
	int Num;
	char* pcName;
	short sDate;
	char cha[2];
	short sBa[4];
}*p = (struct Test*)0x100000;
int main()
{
	printf("%p\n", p + 0x1);
	printf("%p\n", (unsigned long)p + 0x1);
	printf("%p\n", (unsigned int*)p + 0x1);
	return 0;
}

 

 eg3:

#include<stdio.h>
int main()
{
	int a[3][2] = { (0, 1), (2, 3), (4, 5) };
	int* p;
	p = a[0];
	printf("%d", p[0]);
	return 0;
}

 

eg4:

//假设在x86环境下
#include<stdio.h>
int main()
{
	int a[5][5];
	int(*p)[4];
	p = a;
	printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
	return 0;
}

 

eg5:

#include<stdio.h>
int main()
{
	int aa[2][5] = { 1,2,3,4,5,6,7,8,9,10 };
	int* ptr1 = (int*)(&aa + 1);
	int* ptr2 = (int*)(*(aa + 1));
	printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
	return 0;
}

eg6:

#include<stdio.h>
int main()
{
	char* a[] = { "work","at","alibaba" };
	char** pa = a;
	pa++;
	printf("%s\n", *pa);
	return 0;
}

好了,到这里,我们就已经学完了C语言指针的所有基础内容,并用我们所学习的知识分析了真实的笔试题,希望能对大家的学习有一定的帮助

最后,非常感谢大家能够看到这里,希望大家在以后的C语言学习之路上,不断探索,将指针这把利器运用的炉火纯青。

愿你我都能成为代码世界的吟游诗人。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值