C语言指针难理解?看这篇,满满都是干货,没有废话(指针详解)

目录

1.指针

1.1 指针是什么?

1.2 指针变量和地址

1.2.1  & - 取地址操作符

1.3  *  -  解引用操作符

1.3.1  指针变量

1.3.2   拆解指针类型

1.3.3  解引用操作符 - *

1.4 指针变量的大小

2.指针变量的类型有什么意义

 2.1 指针的解引用

2.2  指针 +- 整数

2.3 void*   指针

3. const 修饰指针

3.1 const 修饰变量

3.2 const修饰指针变量

4. 指针的运算

4.1 指针 +- 整数的运算

4.2 指针 - 指针的运算

 4.3 指针的关系运算

5.野指针介绍

5.1 野指针的产生

5.2  规避野指针的方法

5.2.1  指针初始化

5.2.2 留意指针是否越界

5.2.3 当指针变量不再使用时,及时将变量值为NULL,指针使用之前检查有效性

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

6.assert 断言

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

7.1 模拟strlen的实现

7.2 传值调用和传址调用的区别

8.数组名理解 

8.1 数组名的理解

9.使用指针访问数组

10.一维数组传参的本质

11.冒泡排序

12.二级指针

13.指针数组

13.1 用指针数组实现二维数组

14.字符指针变量

15.数组指针变量

15.1 数组指针变量是什么?

15.2 数组指针变量初始化

16.二维数组传参的本质

17.函数指针变量

 17.2 函数指针变量的使用

​17.3 函数指针两个复杂例子

17.3.1 typedef  关键字

18.函数指针数组

19.函数指针数组的用法

20.回调函数

 21.qsort库函数的使用

21.1 使用qsort函数排序整型数据

21.2 使用qsort排序结构体

22.模拟实现qsort函数

23.sizeof 与 strlen 的区别

23.1  sizeof详解

23.2 strlen详解

23.3 sizeof 与 strlen对比

24. 数组和指针试题

24.1 一维数组

24.2 字符数组

24.3 二维数组

25.指针运算分析

 25.1 题型1

25.2 题型2

25.3 题型3

 25.4 题型4

25.5 题型5 

25.6 题型6

 25.7 题型7


1.指针

1.1 指针是什么?

指针就是一个变量,用来保存地址的

我们可以这样理解:内存单元的编号 = 地址 = 指针

32位机器上有32根地址总线,能表示2^32种不同的含义,每种含义都代表着一个地址。

1.2 指针变量和地址

1.2.1  & - 取地址操作符

我们在C语言中创建变量其实就是在向内存申请空间,比如:

int main()
{
    int p = 6;
    return 0;
}

 上述代码表示创建了整型变量a,向内存中申请4个字节,用于存放整型数据,并且每个字节都有地址。

那我们如何得到一个变量的地址呢?
使用 & -- 取地址操作符

int main()
{
    int p = 6;
    printf("%p\n",&p);//&p表示取出p的地址
    return 0;
}

这里&p取出的地址,是p占用的4个字节中最小字节的地址。

如下图所示:

这里的整型变量虽说占用了4个字节,只要知道第一个字节地址,就能计算出其他3个字节的地址。

1.3  *  -  解引用操作符

1.3.1  指针变量

通过 & - 取地址操作符得到的地址是一个数值,有时候我们需要用到这个数值,在后续使用的时候可以直接用,所以要先存储起来,这样的地址值我们存放在指针变量中。

例如:

int x = 6;
int* p = &x;//取出x的地址,存储在指针变量p中

注意:指针变量是一种变量,这种变量是用来存放地址的,一般存放在指针变量中的值都会理解为地址。

1.3.2   拆解指针类型

看这段代码,这里p的类型是 int*,怎样理解指针的类型?

int x = 6;
int* p = &x;

在p的左侧 类型是int*, * 在说明 p 是一个指针变量,而int是在说明 p 指向的是整型(int)类型的对象。

若有一个char类型的变量c,c的地址便存储在char*类型的指针变量中,如下:

char c = 'h';
char* pc = &c;

1.3.3  解引用操作符 - *

在C语言中,我们用指针变量存储的地址,可以使用解引用操作符找到指向的对象,拿到具体的数据。

#include <stdio.h>
int main()
{
    int x = 6;
    int* p = &x;
    printf("p = %d\n",*p):
    return 0;
}

在上述代码中,我们使用到了解引用操作符 - *  ,*p  表示用p存放的地址,找到指向的空间,这里*p和x指向的是同一块空间,所以上述代码可以打印*p 的值为 6;

1.4 指针变量的大小

C语言中,指针变量的大小取决于地址的大小

在32位的平台下,地址是32个bit,那么指针变量的大小就是4;

在64位的平台下,地址是64个bit,那么指针变量的大小就是8。

注意:

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

2.指针变量的类型有什么意义

通过上面的学习,我们知道了指针变量的大小在相同平台下,大小都是一样的,那么为什么还要分出各种类型呢?通过下面的学习,这个疑惑你就知道:

 2.1 指针的解引用

测试下面两段代码,观察在调试时内存的变化。

//测试代码1
#include <stdio.h>
int main()
{
    int x = "0x33445566";
    int* px = &x;
    *px = 0;
    return 0;
}
//测试代码2
#include <stdio.h>
int main()
{
    int x = "0x33445566";
    char* pc = (char*)&x;
    *pc = 0;
    return 0;
}

调试代码1结果: 

调试代码2的结果:

观察调试结果可以看到,代码1会将x的4个字节全部改成0,但代码2只会将x第一个字节修改为0.

 结论:指针的类型决定了,对指针解引用时一次能操作几个字节。例如:char*类型的指针解引用指针访问一个字节,但int*类型的指针解引用能访问4个字节。

2.2  指针 +- 整数

调试下面一段代码,观察地址在内存中的变化:

#include <stdio.h>
int main()
{
	int x = 6;
	int* px = (char*)&x;
	char* pc = (char*)&x;

	printf("&x =    %p\n", &x);
	printf("&px =   %p\n", px);
	printf("&px+1 = %p\n", px + 1);
	printf("&pc =   %p\n", pc);
	printf("&pc+1 = %p\n", pc + 1);

	return 0;
}

 代码运行结果如下图:

可以看到,int* 类型的指针变量+1,跳过了4个字节,而char* 类型的指针变量+1,只跳过了一个字节。这就是指针变量类型不同所产生的差异。指针+1,是指跳过指针指向的一个元素。指针可以+1,指针也可以-1。

结论:指针的类型决定了指针向前或向后跳过几个字节。

2.3 void*   指针

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

观察:

#include <stdio.h>
int main()
{
	int x = 6;
	int* px = &x;
	char* pc = &x;
	return 0;
}

上面的代码中,我们将一个int类型的变量地址赋值给一个char*类型的指针变量。编译器给了一个警告——类型不兼容。但使用void* 类型不会出现这种情况。

使用void* 类型指针接收地址:

#include <stdio.h>
int main()
{
	int x = 6;
	void* px = &x;
	void* pc = &x;

	*px = 55;
	*pc = 0;
	return 0;
}

代码编译结果:

 出现这种情况的原因是:使用void*类型的指针可以接收任何类型的地址,但是无法直接进行指针的运算。

一般 void* 类型的指针是用在函数参数的部分,可以用来接收不同数据类型的地址,使用这种方式编程可以大大缩减代码量(泛型编程),使用一个函数处理多种数据类型,在下面知识中会讲解到。

3. const 修饰指针

3.1 const 修饰变量

变量的值是可以修改的,把一个变量的地址存放到指针变量中,通过指针变量也可以修改这个变量。如果我们希望一个变量不被修改,就需要给它加上一些限制,这接下来要讲解的 const 的作用。

#include <stdio.h>
int main()
{
    int a = 10;
    a = 100;//a的值修改为100
    const int b = 0;
    b = 20;//b的值不能修改
    return 0;
}

上面的代码中,a的本质是一个变量,是可以被修改的,但是b被const修饰了,是一个常量,不能被修改,在编译器中我们对b进行修改,不符合语法规则,编译器会报错。

但是还是有方法去修改b的值,我们可以绕过b,直接使用b的地址,修改b就可以实现,虽然这种做法是在打破语法规则。

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

代码运行结果:

观察运行结果,a 确实被指针变量p修改了,可是我们使用const修饰a就是为了不让a被修改,但是指针变量p拿到a的地址就可以修改a,这样就破解了const的限制,显然这是不合理的,我们要做到p拿到a的地址也不能修改a,就得使用const修饰指针变量。

3.2 const修饰指针变量

const修饰指针变量,const可以放在 * 的左侧,也可以放在 * 的右侧,但意义是不一样的。

int p ;//没有被const修饰
const int * p;//const在 * 的左侧
int * const p;//const在 * 的右侧

分析下面一段代码:

#include <stdio.h>
//测试1 - 无const修饰的
void test1()
{
	int* a = 10;
	a = 20;//ok
	*a = 30;//ok
}

//测试2 - const在*左侧
void test2()
{
	const int* a = 10;
	int b = 20;
	*a = &b;//err
	a = 30;//ok

}

//测试3 - const在*右侧
void test3()
{
	int* const a = 10;
	int b = 20;
	*a = &b;//ok
	a = 30;//err
}

//测试4 - cosnt在*两侧
void test4()
{
	const int* const a = 10;
	int b = 20;
	*a = &b;//err
	a = 30;//err
}

int main()
{
	//测试代码1 - 无const修饰
	test1();
	//测试代码2 - const在 * 左侧
	test2();
	//测试代码3 - const在 * 右侧
	test3();
	//测试代码4 - const在 * 两侧
	test4();
	return 0;
}

通过上述代码测试,我们可以得出结论:

结论:const修饰指针变量时,

const在*左侧,修饰对象是指针指向的内容,确保指针指针的内容不能通过指针修改。

const在*右侧,修饰对象是指针变量本身,确保指针变量的内容不能修改,但是指针指向的内容,是可以通过指针修改。

4. 指针的运算

指针的基本运算有三种,分别是

  • 指针 +- 整数
  • 指针 - 指针
  • 指针的关系运算

4.1 指针 +- 整数的运算

数组在内存中的存储地址是连续的,只要知道第一个元素的地址,就可以计算后面所有元素的地址。

#include <stdio.h>
//指针 +- 整数的 运算
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int len = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;
	int i = 0;
	for (i = 0; i < len; i++)
	{
		printf("%d ", *(p + i));//(p+i)就是指针+整数的形式
	}
	return 0;
}

代码运行结果:

4.2 指针 - 指针的运算

#include <stdio.h>
int MyStrlen(char* str)
{
	char* p = str;
	while (*p != '\0')
	{
		p++;
	}
	return p - str;
}

//指针 - 指针
int main()
{
	char str[] = "abcdef";
	int ret = MyStrlen(str);
	printf("ret = %d\n", ret);

	return 0;
}

代码运行结果: 

 4.3 指针的关系运算

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

5.野指针介绍

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

5.1 野指针的产生

1.指针没有初始化

例如:

#include <stdio.h>
int main()
{
	int* a;
	*a = 20;//err
	return 0;
}

编译器报错:

2.指针越界访问

#include <stdio.h>
int main()
{
	int arr[6] = {0,};
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i <= 8; i++)
	{
		//当指针指向的范围超出数组arr的范围时,p就是野指针
		*p = i;
		p++;
	}
	return 0;
}

 3.指针指向的空间释放

#include <stdio.h>
int* test()
{
	int n = 20;//局部变量,作用域仅在函数内部
	return &n;
}

int main()
{
	int* r = test();//test结束后,*r就变成野指针
	printf("%d\n", *r);//这里的 *r 已经是野指针了
	return 0;
}

5.2  规避野指针的方法

5.2.1  指针初始化

在明确知道指针的指向对象就直接赋值(地址值),如果不知道明确指向,可以先赋值 NULL ,NULL是C语言中定义的一个标识符常量,它的值为0,0也是一个地址,但是这个地址是无法使用的,读写该地址,编译器会报错。

初始化的定义如下:

#include <stdio.h>
int main()
{
	int n = 6;
	int* p = &n;//方式一
	int* q = NULL;//方式二
	return 0;
}

5.2.2 留意指针是否越界

一个程序向内存申请多少空间,通过指针就能访问多少空间,但绝不能超出范围访问,否则就是越界访问。

5.2.3 当指针变量不再使用时,及时将变量值为NULL,指针使用之前检查有效性

前期用过的指针变量,后期不再使用时,及时置为NULL。有个规则是:只要指针指向的是NULL,就不再访问,同时在使用指针之前,可以判断指针是否为NULL。

用法如下:

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p = i;
		p++;
	}
	//代码执行到这里,p已越界了,此时可以把p置为NULL;
	p = NULL;
	p = &arr[0];//让p重新获取地址
	printf("%d ", p);
	if (p != NULL)
	{
		//… 
	}
	return 0;
}

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

6.assert 断言

宏assert()的头文件是<assert.h>,使用场景:在运行时确保程序符合指定的条件,如果不符合,就终止运行。这个宏常被称为”断言“。

assert(p != NULL);

上面这行代码,就可以用来在使用指针时,判断指针指向的是否时NULL,如果不等于NULL,则可以继续执行后面的代码,否则就会终止运行,并给出报错信息提示。

assert() 宏接收一个表达式作为参数。如果表达式为真(返回值非0),assert()就不产生作用,程序会继续执行。如果表达式为假(返回值为0),assert()就会报错。

assert()的使用让我们编写程序更加方便,使用assert()有几个好处:它不仅能自动识别文件和出问题的行数,还有一种无需更改代码就能开启或关闭assert()的机制。如果确定程序没有问题,无需再做断言,就可以在 #include <assert.h>语句前,定义一个宏NDEBG。

#define NDEBUG
#include <assert.h>

然后,再重新编译程序,编译器会禁用文件中所有的assert()语句。后续程序又出现问题,可以移除这条#defind NDEBUG,指令(也可以把它注释了),再次编译,就可以重新启用assert()语句。

使用assert()的缺点是:引入额外的检查,增加了程序的运行时间。

一般我们在Debug模式下使用,在Release版本中选择禁用asserty()语句就可以。在VS这个集成开发环境中,Release版本下,直接是优化掉了。

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

7.1 模拟strlen的实现

库函数strlen的功能是求字符串的长度,统计在'\0'之前出现的字符个数。

函数原型:

size_t strlen(const char* str);

计算方式为:参数str接字符串第一个字符位置,然后开始统计字符串中 '\0' 之前的字符个数,最后返回长度。

思考一下:如果我们要实现这个功能,就要从字符串的起始位置向后遍历,不是'\0',计数器就+1,遇到'\0'就停止。

#include <stdio.h>
#include <assert.h>
size_t MyStrlen(const char* s)
{
	int count = 0;
	assert(s);
	while (*s != '\0')
	{
		count++;
		s++;
	}
	return count;
}

int main()
{
	char str[] = "abcdef";
	int len = MyStrlen(str);
	printf("len = %d\n", len);//输出结果:len = 6
	return 0;
}

7.2 传值调用和传址调用的区别

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

//写一个函数,交换两个整型变量的值
#include <stdio.h>
void SwapNum(int a, int b)
{
	int tmp = a;
	a = b;
	b = tmp;
}

int main()
{
	int n = 2;
	int m = 4;
	printf("交换前:n = %d,m = %d\n", n, m);
	SwapNum(n, m);
	printf("交换后:n = %d,m = %d\n", n, m);
	return 0;
}

代码运行结果:

程序执行完并没有产生交换的效果是什么原因?

通过调试,我们知道在main函数内部创建的变量n、m与函数中接收的a、b的地址并不是同一个,当我们调用SwapNum函数时,将n和m的值传递给了SwapNum函数,SwapNum函数内创建了形参a和b接收n与m的值。这里a与b确实接收了n和m的值,但a和b的地址与n和m的地址无关,故在函数中交换a与b的值不会影响n和m,当函数结束调用后回到main函数中,a和b的值并没有发生变化,SwapNum函数这种把变量本身传递过去的方式,叫做传值调用。

结论:实参传值给形参,形参会单独创建一个临时变量接收实参,对形参的修改不会影响到实参。

如果我们实现当调用SwapNum函数时,SwapNum函数内部操作的地址和main函数n和m是 同一块空间,就可以使用指针。在main函数中直接将n和m的地址传给SwapNum函数,函数就可以间接操作main函数中的n和m,实现交换效果。

#include <stdio.h>
void SwapNum(int* a, int* b)
{
	char tmp = *a;
	*a = *b;
	*b = tmp;
}
int main()
{
	int n = 2;
	int m = 4;
	printf("交换前:n = %d,m = %d\n", n, m);
	SwapNum(&n, &m);
	printf("交换后:n = %d,m = %d\n", n, m);
	return 0;
}

代码运行结果:

这种把变量的地址传给函数的方式叫做传址调用。

结论:传址调用,可以让函数和主函数之间的地址建立真正的联系,在函数内部可修改主调函数中的变量。在以后的函数中只是需要主调函数中的变量值计算,就可使用传值调用;如果函数内部要修改主调函数中变量的值,就使用传址调用的方式。

8.数组名理解 

8.1 数组名的理解

先看一段代码:

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

运行结果:

这里我们发现数组名和数组首元素的地址是同一个,数组名就是数组首元素的地址。但有两个例外 :

  1. sizeof(数组名):sizeof 中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节(Byte).
  2. &数组名:这里的数组名表示整个数组,取地址取出的是整个数组的地址(注意:整个数组的地址和数组首元素的地址是有区别的)

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

通过下面这段代码可观察出区别:

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

代码运行结果:

arr和arr+1相差4个字节,&arr[0]和&arr[0]+1相差4个字节, 原因在于arr和&arr[0]都指向首元素的地址,+1就表示跳过一个元素。但&arr与&arr+1相差40个字节,是因为 &arr 表示数组的地址,+1就跳过整个数组。这就是arr和arr[0]以及&arr三者的区别。

9.使用指针访问数组

有前面知识的铺垫,结合数组的特点,我们可以方便的使用指针访问数组。

#include <stdio.h>
//方式一
void print1(int arr[], int sz)
{
	int i = 0;
	int* p = arr;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p+i));
	}
}

//方式二
void print2(int arr[], int sz)
{
	int i = 0;
	int* p = arr;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", p[i]);
	}
}

//方式三
void print3(int arr[], int sz)
{
	int i = 0;
	int* p = arr;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *p);
		p++;
	}
}

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int len = sizeof(arr) / sizeof(arr[0]);
	print1(arr, len);
	printf("\n");
	print2(arr, len);
	printf("\n");
	print3(arr, len);
	
	return 0;
}

运行结果: 

分析上述代码:数组名arr是数组首元素的地址,可赋值给p,所以在这里数组名和p是等价的,我们可以使用arr[i]访问数组,同理也可以使用p[i]访问数组。

同理 *(p + i) 与 p[i]也是等价的,arr[i]  等价于 *(arr + i),我们由此可以知道数组元素的访问时,也是转化成首元素的地址 + 偏移量求出元素的地址,然后进行解引用访问到元素本身。

10.一维数组传参的本质

一维数组传参的本质:本质上数组传参传的是数组首元素的地址。所以函数形参部分理论上应该使用指针变量来接收首元素的地址。由此我们知道在函数内部使用sizeof计算数组的大小是行不通的,在函数内部使用sizeof(arr)计算的是一个地址的大小(字节)并不是数组的大小(字节)。

#include <stdio.h>
void test(int* a)//参数是指针形式
{
	printf("%d\n", sizeof(a));//计算一个指针变量的大小
}

void test2(int a[])//参数是数组,本质上是指针
{
	printf("%d\n", sizeof(a));
}
int main()
{
	int arr[10] = { 0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("%d\n", sz);
	//指针传参
	test(arr);
	//数组传参
	test2(arr);
	return 0;
}

运行结果: 

总结:一维数组传参,形参部分可以是数组形式,也可以是指针的形式。 

11.冒泡排序

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

#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 tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}

void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int main()
{
	int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("排序前:");
	print_arr(arr, sz);
	bubble_sort(arr, sz);
	printf("排序后:");
	print_arr(arr, sz);

	return 0;
}
//优化之后
#include <stdio.h>
void bubble_sort(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int flag = 1;//假设这一趟是有序的
		int j = 0;
		for (j = 0; j < sz - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				flag = 0;//产生交换,说明无序
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
		if (flag == 1)//这一趟没交换说明全部有序,直接跳出循环
		{
			break;
		}
	}
	
}

void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int main()
{
	int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("排序前:");
	print_arr(arr, sz);
	bubble_sort(arr, sz);
	printf("排序后:");
	print_arr(arr, sz);

	return 0;
}

运行结果: 

12.二级指针

指针变量也是变量,是变量就有地址,指针变量的地址存放在二级指针中。

 二级指针运算:

*p  :对p中的地址进行解引用,找到的是a,*p2访问的是a。

int a = 100;
int* p = &a;

**p2 :先通过*p2找到p,再对p进行解引用:*p,找到a.

int a = 100;
int* p = &a;
int** p2 = &p;

13.指针数组

指针数组是指针还是数组呢?

类比:

整型数组 - 存放整型的数组;

字符数组 - 存放字符的数组;

那么指针数组 - 存放指针的数组。

指针数组的每个元素都是用来存放指针(地址)的。

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

13.1 用指针数组实现二维数组

#include <stdio.h>
int main()
{
	int arr1[4] = { 1,2,3,4 };
	int arr2[4] = { 2,3,4,5 };
	int arr3[4] = { 3,4,5,5 };
    //数组名是数组首元素的地址,类型是int*,可以存放到数组arr中
	int* arr[3] = { &arr1,&arr2,&arr3 };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 4; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

运行结果:

arr[i] 访问arr数组的元素,arr[i] 中的数组元素指向整型一维数组,arr[i][j]访问的是一维数组中的元素。

注意:上述代码虽模拟出二维数组的效果,实际上不是二维数组,因为每一行元素的地址不是连续的。

14.字符指针变量

指针类型中有一种指针类型为字符指针 char*

两种使用方式:

#include <stdio.h>
int main()
{
	//方式一
	char c = 'h';
	char* pc = &c;
	printf("pc = %c\n", *pc);
	*pc = 'y';
	printf("pc = %c\n", *pc);

	//方式二
	const char* cc = "hello world";
	printf("%s\n", cc);
	return 0;
}

运行结果

需要注意的是:这里的const char* cc = "hello world"; 并不是把字符串"hello world"放在字符指针cc中,本质上是把字符串的首字符地址放在字符指针中。

下面学习一下 "字符数组和字符指针"的区别:

#include <stdio.h>
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");
	else
		printf("str1 and str2 are not same\n");
	if(str3 == str4)
		printf("str3 and str4 are same\n");
	else
		printf("str3 and str4 are not same\n");
	return 0;
}

 运行结果:

这里str3和str4指向的是同一个常量字符。C/C++会把常量字符串存放到单独的一块内存区域,介个指针如果指向同一个字符串是,它们实际上会指向同一块内存地址。但如果是相同的常量字符串初始化不同的数组时,会开辟不同的内存区域。故str1和str地址不同,所以不相等;str3和str地址相同,所以相等。

15.数组指针变量

15.1 数组指针变量是什么?

指针数组是一种数组,数组中存放的是指针(地址)。

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

类比:

整型指针变量:int* p; 存放的是整型变量的地址,能够指向整型数据的指针。

字符指针变量:char* c;  存放的是字符变量的地址,能够指向字符数据的指针。

数组指针变量是:存放数据的地址,能够指向数组的指针变量。

数值指针变量的形式:

int (*p)[10];

分析:p先和 * 结合,说明p是一个指针变量,然后指向的是10个整数的数组,所以p是一个指针,指向一个数组,叫数组指针

注意:[ ] 的优先级要高于 * 号,所以必须加上 ( ) 来 保证 p 先和 * 结合。

15.2 数组指针变量初始化

数组指针变量是用来存放数组地址的,获取数组的地址使用 - &数组名。

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

如果要存放数组的地址,就得存放在数组指针变量中,例如:

int arr[4] = {1,2,3,4};
int (*p)[4] = &arr;

通过调试能看到 &arr 和 p的类型是完全一致的。

数组指针类型解析:

16.二维数组传参的本质

以前我们用二维数组传参给函数时,是下面这种写法:

#include <stdio.h>
void test(int arr[3][4], 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][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };
	test(arr, 3, 4);

	return 0;
}

能看到实参是二维数组,传给函数的形参也是二维数组的形式,是否还有其他写法?

有的,二维数组本质上每个元素是一维数组的数组,也就是二维数组的每个元素是一个一维数组。那么二维数组的首元素就是它的第一行(一维数组)。

数组名是首元素的地址我们可以得出,二维数组的数组名表示的是第一行的地址,是一维数组的地址 。上述代码中,第一行的一维数组是 int [4], 故第一行的地址的类型就是数组指针的类型int(*)[5]。二维数组传参本质上也是传递地址,传递的是第一行一维数组的地址,说明形参也是可写成指针形式。如:

#include <stdio.h>
void test(int(*p)[4], int r, int c)
{
	int i = 0;
	for (i = 0; i < r; i++)
	{
		int j = 0;
		for (j = 0; j < c; j++)
		{
			printf("%d ",*(*(p+i)+j) );
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };
	test(arr, 3, 4);
	return 0;
}

代码运行结果:

总结:二维数组传参,形参的部分可写成数组形式,也是写成指针形式。 

17.函数指针变量

概念:函数指针变量是用来存放函数地址的,通过地址能调用函数。

可以测试一个函数是否有地址:

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

运行结果:

函数的地址打印出来了,所以函数名就是函数的地址,也可以通过&函数名获取函数的地址。

如果要将函数的地址存储起来,就得创建指针变量,函数指针变量的写法和数组指针类似。

例如:

函数指针类型分析:

 

 17.2 函数指针变量的使用

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

#include <stdio.h>
int add(int a, int b)
{
	return a + b;
}

int main()
{
	int(*ptr)(int, int) = add;//方式一
	int(*ptr)(int, int) = &add;//方式二
	int ret = ptr(3, 4);
	printf("ret = %d\n", ret);
	return 0;
}

输出结果: 

17.3 函数指针两个复杂例子

代码1

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

首先区分各个括号(*  (void ( * ) (  ) ) 0 ) ( ),然后进行拆分,从中间拆得到void(*)(),表示这是一个无参无返回值的函数指针类型,放在0前面,表示把0强制类型转换成这个函数指针类型,即0变成函数指针,代码现在简化为(*0)(),其中0是函数指针类型,对0进行解引用找到这个函数,因为0是个函数指针类型void(*)( ),对0解引用传递的函数参数是空().

代码2

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

看这段代码,先分解一下,signal(int,void(*)(int))暂且用x代替,得到void(*x)(int),显然这个是一个函数指针的定义,说明signal(int,void(*)(int))是一个函数指针,signal是一个函数,它的返回值一个函数指针,它的参数类型是int,和函数指针void(*)(int)。

上面这种写法看着很复杂,可以使用typdef简化这段代码:typedef void (*n) (int);对这类函数指针重命名,最终的代码为 n signal(int,n);   看着是不是比上面代码更容易理解了。

17.3.1 typedef  关键字

typedef 是用来对类型重命名,把复杂类型,简单化。

例如:你可以把 unsigned int 类型写 uint.

typedef unsigned int uint;//把unsigned int 重命名为uint

当然指针类型也可以重命名,例如:将 char* 重命名为 ch:

typedef char* ch;

 数组指针重命名:

例如:将数组指针类型int(*)[4],需要重命名为arr_t,可以这样写: 

typedef int(*arr_t)[4];//新的类型名必须在*的右侧

函数指针类型的重命名也是一样的,例如:将void(*)(char)类型重命名为 pf_v,可以这样描述:

typedef void(*pf_v)(char);//新的类型名必须在*的右侧

18.函数指针数组

数组是一个存放相同数据类型的内存空间。把函数的地址存到一个数组中,那么这个数组被称为函数指针数组,这个数组的定义方式如下:

int (*p1[5])();

 分析:p1 先和 [ ] 结合,说明p1是数组,数组的内容int(*)() 类型的函数指针。

19.函数指针数组的用法

举例:实现一个计算器,一般的写法:

//实现一个计算器
#include <stdio.h>
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;
}

void menu()
{
	printf("-------------------------------\n");
	printf("--------- 1.add   2.sub -------\n");
	printf("--------- 3.mul   4.div -------\n");
	printf("--------- 0.exit        -------\n");
	printf("-------------------------------\n");
}
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	do
	{
		menu();
		printf("请选择功能:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret = add(x, y);
			printf("ret = %d\n", ret);
			break;
		case 2:
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret = sub(x, y);
			printf("ret = %d\n", ret);
			break;
		case 3:
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret = mul(x, y);
			printf("ret = %d\n", ret);
			break;
		case 4:
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret = div(x, y);
			printf("ret = %d\n", ret);
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("选择错误,重新选择\n");
			break;
		}
	} while (input);
	return 0;
}

我们使用函数数组的写法:

//实现一个计算器
#include <stdio.h>
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;
}

void menu()
{
	printf("-------------------------------\n");
	printf("--------- 1.add   2.sub -------\n");
	printf("--------- 3.mul   4.div -------\n");
	printf("--------- 0.exit        -------\n");
	printf("-------------------------------\n");
}
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	int(*pf[5])(int x, int y) = {0,add,sub,mul,div};
	do
	{
		menu();
		printf("请选择功能:");
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret = pf[input](x, y);
			printf("ret = %d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出计算器\n");
			break;
		}
		else
		{
			printf("选择错误,重新输入\n");
		}
	} while (input);

	return 0;
}

20.回调函数

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

如果把函数的指针(地址)作为参数传给另一个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。注意:回调函数并不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另一方调用,用来对该事件或条件进行响应。

还是用计算器举例,在上面将计算器的实现代码中,还是有一些代码是重复出现的,是否有更简化的写法。答案是有的,上述代码中只有调用的代码的逻辑上有差异,其他都类似,所以我们可以 把调用的函数的地址以参数的形式传递过去,使用函数指针进行接收,函数指针指向什么函数就调用什么函数,这里使用的就是回调函数的功能。

//实现一个计算器
#include <stdio.h>
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 Calc(int(*p)(int, int))
{
	int x = 0;
	int y = 0;
	int ret = 0;
	printf("请输入两个操作数:");
	scanf("%d %d", &x, &y);
	ret = p(x, y);
	printf("ret = %d\n", ret);
}
void menu()
{
	printf("-------------------------------\n");
	printf("--------- 1.add   2.sub -------\n");
	printf("--------- 3.mul   4.div -------\n");
	printf("--------- 0.exit        -------\n");
	printf("-------------------------------\n");
}
int main()
{
	int input = 0;
	
	do
	{
		menu();
		printf("请选择功能:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			Calc(add);
			break;
		case 2:
			Calc(sub);
			break;
		case 3:
			Calc(mul);
			break;
		case 4:
			Calc(div);
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("选择错误,重新选择\n");
			break;
		}
	} while (input);
	return 0;
}

 21.qsort库函数的使用

qsort函数可以对任意类型数据排序,头文件时<stdlib.h>

qsort函数原型:

void qsort( void *base, size_t num, size_t width, int (__cdecl *compare )(const void *elem1, const void *elem2 ) );

解释每个参数的含义:

  • void *base: 指向数组的起始地址值,通常该位置传入的时一个数组名
  • size_t num:表示当前数组的元素个数
  • size_t width:表示该数组中每个元素的大小(字节数)
  • int(_cdecl *compare)(const void* elem1,const void *elem2):指向比较函数的函数指针,决定了排序的顺序。

21.1 使用qsort函数排序整型数据

//qsort排序整型数据
#include <stdio.h>
int cmp(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;
}
void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int main()
{
	int arr[] = { 3,1,5,9,6,2,4,8,7,0 };
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("排序前:\n");
	print_arr(arr, sz);
	qsort(arr, sz, sizeof(arr[0]), cmp);
	printf("排序后:\n");
	print_arr(arr, sz);
	return 0;
}

代码运行结果: 

21.2 使用qsort排序结构体

#include <stdio.h>
#include <stdlib.h>

//定义结构体
struct Stu
{
	char name[20];//名字
	int age;//年龄
};


int cmp_stu_by_name(const void* p1, const void* p2)
{
	return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}

int cmp_stu_by_age(const void* p1, const void* p2)
{
	return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}

void print_Stu(struct Stu* s, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%s %d\n", s->name, s->age);
		s++;
	}
}

//按名字进行比较
void test1()
{
	struct Stu st[] = { {"张三",24},{"李四",20},{"王五",18} };
	int sz = sizeof(st) / sizeof(st[0]);
	qsort(st, sz, sizeof(st[0]), cmp_stu_by_name);
	print_Stu(st, sz);
}

//按年龄进行比较
void test2()
{
	struct Stu st[3] = { {"张三",24},{"李四",20},{"王五",18} };
	int sz = sizeof(st) / sizeof(st[0]);
	qsort(st, sz, sizeof(st[0]), cmp_stu_by_age);
	print_Stu(st, sz);
}

int main()
{
	
	//按名字进行比较
	//test1();
	//按年龄进行比较
	test2();
	return 0;
}

按年龄比较的运行结果:

按名字比较的运行结果:

22.模拟实现qsort函数

使用回调函数,模拟实现qsort - 使用冒泡形式。

#include <stdio.h>
int cmp_int(const void* p1, const void* p2)
{
	return (*(int*)p1) - (*(int*)p2);
}

void swap_arr(void* p1, void* p2,int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		char tmp = *((char*)p1 + i);
		*((char*)p1 + i) = *((char*)p2 + i);
		*((char*)p2 + i) = tmp;
	}
}
void bubble(void* base, int sz, int width, int(*cmp)(void*, void*))
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int j = 0;
		for(j = 0 ; j < sz - i - 1; j++)
		{
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			{
				swap_arr((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}

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

运行结果:

23.sizeof 与 strlen 的区别

23.1  sizeof详解

概念:计算变量所占内存空间大小,单位是字节。如果操作数是类型,那么计算的是使用类型创建的变量所占内存空间的大小

sizeof只计算占用内存空间的大小,不在乎内存中存放什么数据。

#include <stdio.h>
int main()
{
    int a = 0;
    printf("%d\n", sizeof(a));//4
    printf("%d\n", sizeof(int));//4
//sizeof()是单目操作符 ,如下面这行代码即使没有括号,照样不影响计算
    printf("%d\n", sizeof a);//4
	return 0;
}

23.2 strlen详解

strlen 是C语言库函数,作用:求字符串长度。

函数原型:

size_t strlen(const char* str):

统计的是从strlen函数的参数str 这个起始地址往后,'\0'之前字符串中字符的个数。

strlen 函数会一直查找'\0'字符,直到找到为止,所以可能会越界查找。

比较下面这两种字符串的写法:

#include <stdio.h>
int main()
{
	char arr1[5] = { 'h','e','l','l','o' };
	char arr2[] = "hello";

	printf("%d\n", sizeof(arr1));
	printf("%d\n", sizeof(arr2));
	printf("-----------------\n");
	printf("%d\n", strlen(arr1));
	printf("%d\n", strlen(arr2));
	return 0;
}

代码运行结果: 

23.3 sizeof 与 strlen对比

 sizeof


  1. sizeof 是操作符
  2. sizeof计算操作数所占内存的大小,单位是字节
  3. 不关注内存中存放什么数据

strlen


  1. strlen是库函数,使用需要包含头文件 string.h
  2. strlen是求字符串长度的,统计的是 '\0' 之前的字符的个数
  3. 关注内存是否有 '\0' ,没遇到'\0',会持续向后找,可能会越界

24. 数组和指针试题

24.1 一维数组

#include <stdio.h>

int main()
{
    int arr[] = {1,3,5,7};
    //sizeof(数组); --- 得到的是整个数组的大小
    printf("%d\n", sizeof(arr));//16
    //arr是首元素的地址 - 类型是int *, arr+0 还是首元素的地址,指针(地址)是4(32位平台) / 8(64位平台)
    printf("%d\n", sizeof(arr + 0));// 4 / 8
    //arr是首元素的地址,*arr = 首元素,int型大小 == 4个字节
    printf("%d\n", sizeof(*arr));// 4
    //arr是首元素的地址,类型是int*,arr+1 是跳过一个整型,arr+1 是第二个元素的地址,指针(地址)大小是4 / 8
    printf("%d\n", sizeof(arr + 1));// 4 / 8
    //arr[1]类型是int型,int型大小是4个字节
    printf("%d\n", sizeof(arr[1]));// 4
    //&arr是 数组的地址,数组的地址也是地址(指针),大小是4/8个字节
    printf("%d\n", sizeof(&arr));// 4/8
    //*& 相互抵消,sizeof(*&arr) == sizeof(arr) = 16个字节,计算的是数组的大小
    printf("%d\n", sizeof(*&arr));// 16
    //&arr是整个数组,&arr+1 跳过整个数组后面那个位置的地址,是地址,那么大小是4/8个字节
    printf("%d\n", sizeof(&arr + 1));// 4/8
    //&arr[0],表示首元素的地址,指针(地址)的大小是4/8个字节
    printf("%d\n", sizeof(&arr[0]));// 4/8
    //&arr[0] + 1 -- 数组第二个元素的地址,指针(地址)的大小是4/8个字节
    printf("%d\n", sizeof(&arr[0] + 1));// 4/8

    return 0;
}

运行结果:

24.2 字符数组

代码1:

#include <stdio.h>
int main()
{
	char arr[] = { 'h','e','l','l','o' };
	//arr是首元素的地址,数组中没有\0,就会导致越界访问,结果就是随机值
	printf("%d\n", strlen(arr));//随机值
	//arr + 0表示首元素的地址,数组中没有\0,就会导致越界访问,结果就是随机值
	printf("%d\n", strlen(arr + 0));//随机值
	//arr是首元素的地址,*arr就是首元素,这里是'h','h'的ASCII码值是‘104’,
	//相当于把104作为地址传给了strlen,strlen得到的就是野指针,代码是有问题的
	//printf("%d\n", strlen(*arr));//err
	//arr[1] -- 'e' -- 101,传给strlen函数也是有问题的
	//printf("%d\n", strlen(arr[1]));//err
	//&arr是数组的地址,起始位置是数组第一个元素的位置,随机值 x
	printf("%d\n", strlen(&arr));//随机值 x
	//数组的地址+1,跳过一个数组,指向后面的位置,随机值 x - 5
	printf("%d\n", strlen(&arr + 1));//随机值  x - 5
	//从第二个元素开始向后统计到'\0'前的位置
	printf("%d\n", strlen(&arr[0] + 1));//随机值 x - 1
	return 0;
}

比如:在我这运行的结果是这样的:

 代码2:

#include <stdio.h>
int main()
{
	char arr[] = { 'h','e','l','l','o' };
	//sizeof(数组名) ,数组名单独放在sizeof内部,计算的是数组的大小(单位:字节)
	printf("%d\n", sizeof(arr));//5
	//arr - 数组名,表示数组的首元素地址,arr+0还是首元素的地址,指针(地址)的大小是 4/8个字节
	printf("%d\n", sizeof(arr + 0));// 4/8
	//arr是首元素地址,*arr就表示首元素,大小是1个字节
	printf("%d\n", sizeof(*arr));//1
	//arr[1]表示第二个元素,大小是1个字节
	printf("%d\n", sizeof(arr[1]));//1
	//&arr 是数组的地址,数组的地址也是地址 ,指针(地址)的大小是 4/8个字节
	printf("%d\n", sizeof(&arr));// 4/8
	//&arr+ 1跳过一个字符型数组,指向的是数组后面的位置,4/8个字节
	printf("%d\n", sizeof(&arr + 1));// 4/8
	//&arr[0] 表示首元素的地址 + 1,表示指向第二个元素的地址,指针(地址) 的大小是 4/8个字节
	printf("%d\n", sizeof(&arr[0] + 1));// 4/8
	return 0;
}

运行结果: 

 代码3:

#include <stdio.h>
int main()
{
	char arr[] = "hello";
	//arr是数组名,单独放在sizeof内部,计算的是数组的总大小,包括'\0'
	printf("%d\n", sizeof(arr));//6个字节
	//arr表示数组首元素的地址,arr+0还是首元素的地址,指针(地址)的大小 -- 4/8个字节
	printf("%d\n", sizeof(arr + 0));// 4/8个字节
	//arr表示数组首元素地址,*arr表示首元素,char型 大小是是1个字节
	printf("%d\n", sizeof(*arr));//1个字节
	//arr[1]表示第二个元素,大小是1个字节
	printf("%d\n", sizeof(arr[1]));//1个字节
	//&arr 是数组的地址,指针(地址) 的大小是4/8个字节
	printf("%d\n", sizeof(&arr));// 4/8个字节
	//&arr 是数组的地址,&arr+1就是跳过整数组,指向数组后面的位置,还是地址,指针(地址)大大小是4/8个字节
	printf("%d\n", sizeof(&arr + 1));// 4/8个字节
	//&arr[0]是数组首元素的地址 + 1,表示第二个元素的地址,指针(地址)的大小是 -- 4/8个字节
	printf("%d\n", sizeof(&arr[0] + 1));// 4/8 个字节
	return 0;
}

运行结果:

代码4:

#include <stdio.h>
int main()
{
	char arr[] = "hello";
	//strlen 统计'\0'之前的字符
	printf("%d\n", strlen(arr));//5个字节
	//arr是元素的地址,arr+0还是首元素的地址,在'\0'之前有5个字符
	printf("%d\n", strlen(arr + 0));//5个字节
	//*arr -- 数组首元素,'h' -- 104 -->err
	//printf("%d\n", strlen(*arr));//err
	arr[1] -- 第二个元素,'e' -- 101 -->err
	//printf("%d\n", strlen(arr[1]));//err
	//&arr是数组的地址,从前向后找,5个字节
	printf("%d\n", strlen(&arr));//5个字节
	printf("%d\n", strlen(&arr + 1));//随机值
	//从第2个元素向后找到'\0'之前
	printf("%d\n", strlen(&arr[0] + 1));//4个字节
	return 0;
}

运行结果:

代码5:

int main()
{
	char* p = "hello";
	//p是指针变量,计算指针变量的大小,4/8个字节
	printf("%d\n", sizeof(p));// 4/8个字节
	//p+1是'e'的地址,指针(地址)的大小是4/8个字节
	printf("%d\n", sizeof(p + 1));// 4/8个字节
	//p的类型是char*,*p就是char类型,char类型大小是1个字节
	printf("%d\n", sizeof(*p));//1个字节
	//p[0 -->*p -->'a' ,大小是1个字节
	printf("%d\n", sizeof(p[0]));//1个字节、//&p是取出数组地址,指针(地址)的大小是4/8个字节
	printf("%d\n", sizeof(&p));// 4/8个字节
	//&p+1是跳过p指针变量,指向后面的地址,地址大小是4/8个字节
	printf("%d\n", sizeof(&p + 1));// 4/8个字节
	//&p[0] - 取出字符串首元素的地址,+1是第二个字符的地址,大小是4/8个字节
	printf("%d\n", sizeof(&p[0] + 1));// 4/8个字节
	return 0;
}

运行结果:

代码6:

#include <stdio.h>
int main()
{
	char* p = "hello";
	printf("%d\n", strlen(p));//5个字节
	printf("%d\n", strlen(p + 1));//4个字节
	//printf("%d\n", strlen(*p));//p是首元素的地址,*p是首元 -- 'h' -- 104 ;//err
	//printf("%d\n", strlen(p[0]));//p[0]是首元素 == *(p+0) == *p //err
	printf("%d\n", strlen(&p));///&p是指针变量p的地址,和字符串没关系,
	//从p这个指针变量的起始位置向后计数,p变量存放的地址是未知的,所以这是一个随机值
	printf("%d\n", strlen(&p + 1));//随机值
	printf("%d\n", strlen(&p[0] + 1));//4个字节 &p[0] 取出字符串首元素的地址,+1是第二个字符的位置,向后统计到'\0'之前的字符数
	return 0;
}

运行结果

24.3 二维数组

int main()
{
	int a[3][4] = { 0 };
	//a是数组名,单独放在sizeof内部,计算的是数组的大小,单位是字节 -- 48 = 3*4*sizeof(int);
	printf("%d\n", sizeof(a));//48

	//a[0][0]是第一行的数组名,大小是4个字节
	printf("%d\n", sizeof(a[0][0]));//4

	//a[0] 表示的是第一行的数组名,数组名单独放在sizeof内部,计算的是数组的总大小,16个字节
	printf("%d\n", sizeof(a[0]));//16

	//a[0] + 1 == a[0][0],数组第一行第一个元素的的地址,指针(地址)的大小是4/8个字节
	printf("%d\n", sizeof(a[0] + 1));//4/8

	//*(a[0]+1)表示第一行第二个元素,int型大小是4个字节
	printf("%d\n", sizeof(*(a[0] + 1)));// 4

	//a作为数组名并没有单独放在sizeof内部,a表示数组首元素的地址,是二维数组首元素的地址,
	//也就是第一行的地址,a+1跳过一行,指向了第二行,所以a+1是第二行的地址,a+1是数组指针,指针(地址)大小是4/8个字节
	printf("%d\n", sizeof(a + 1));// 4/8

	//a+1是第二行的地址,*(a+1)就是第二行,计算的是第二行的大小 - 16个字节
	printf("%d\n", sizeof(*(a + 1)));//16

	//a[0]是第一行的数组名,&a[0]取出的就是数组的地址,就是第一行的地址
	//&a[0]+1,就是第二行的地址,指针(地址)的大小是4/8个字节
	printf("%d\n", sizeof(&a[0] + 1));//4/8个字节

	//*(&a[0]+1) 表示对第二行的地址解引用,访问的是第二行,大小是16个字节
	printf("%d\n", sizeof(*(&a[0] + 1)));//16

	//a作为数组名没有单独放到sizeof内部,a表示数组首元素的地址,是二维数组首元素的地址,
	// 也就是第一行的地址,*a就是第一行,计算的是第一行的大小,16个字节
	printf("%d\n", sizeof(*a));//16

	//a[3]是第四行的数组名,单独放在sizeof内部,计算的是第四行的大小,16个字节
	printf("%d\n", sizeof(a[3]));//16
	//说明:a[3]可以不是真实存在,仅仅通过类型就能推断出长度。
	return 0;
}

运行结果:

数组名的含义:

  • sizeof(数组),这里的数组名表示整个数组,计算的是整个数组的大小。
  • &数组名,这里的数组表示整个数组,取出的是整个数组的地址。
  • 除此之外所有的数组名都表示首元素的地址。

25.指针运算分析

 25.1 题型1

#include <stdio.h>
int main()
{
	int arr[5] = { 1,2,3,4,5 };
	int* p = (int*)(&arr + 1);
	printf("%d %d\n", *(arr + 1), *(p - 1));//输出结果:2   5
	//分析:arr 表示的是首元素的地址,arr+1 表示第二个元素地址,解引用就是2
	//&arr表示数组的地址 + 1,跳过一个整型数组,指向数组后面的空间,-1指向数组最后一个元素
	return 0;
}

25.2 题型2

#include <stdio.h>
//在32位环境下,假设结构体的大小是20个字节
//下面程序输出的结果是什么
struct Test
{
	int Num;
	char* Name;
	short s;
	char c[2];
	short sh[4];
}*p = (struct Test*)0x100000;

//指针+-整数
int main()
{
	//结构体指针+1,表示跳过一个结构体 1 + 20 == 14(十六进制)
	//0x100000 + 14 = 0x100014
	printf("%p\n", p + 0x1);//00100014

	//p强制类型转换位无符号的整型,p+0x1 == p+1 == 1 (整型值+1,就是+1)
	//0x100000 + 1 -->0x100001
	printf("%p\n", (unsigned long)p + 0x1);//00100001
	
	//p强制类型转换成无符号整型指针,p+0x1 = p + 4 = 4
	//0x100001 + 4 --> 00100004
	printf("%p\n", (unsigned int*)p + 0x1);//00100004
	return 0;
}

运行结果:

25.3 题型3

#include <stdio.h>
int main()
{
	//存储在arr数组中的元素为3、7、11、0、0,  原因在于中间包裹元素的符号是()不是{ }
	//下面这种写法是逗号表达式,只会取最后出现的元素
	int arr[3][2] = { (1,3),(5,7),(9,11) };
	int* p;
	p = arr[0];
	printf("%d\n", p[0]);
	//arr[0] 是第一行的数组名,数组名表示首元素的地址
	//也就是&arr[0][0]的地址,这里取出的元素是 3
	//*(p+0) == *p
 	return 0;
}

 25.4 题型4

#include <stdio.h>
//在x86环境下,程序输出的结果是什么?
int main()
{
	int a[5][5];
	int(*p)[4];//p是一个数组指针,p指向的数字是四个整型元素
	p = a;
	printf("%p %d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);

	return 0;
}

运行结果:

代码分析:

25.5 题型5 

#include <stdio.h>
int main()
{
	int a[2][5] = { {1,2,3,4,5},{6,7,8,9,10} };
	int* p1 = (int*)(&a + 1);//&a表示数组地址,+1跳过整个数组,指向数组后面的空间
	int* p2 = (int*)(*(a + 1));//*(a+1) == a[1] == 6
	printf("%d %d", *(p1 - 1), *(p2 - 1));//10  5

	return 0;
}

代码分析:

25.6 题型6

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

代码分析:

 25.7 题型7

#include <stdio.h>
int main()
{
	char* c[] = { "ENTER","NEW","POINT","FIRST" };
	char** cp[] = { c + 3,c + 2,c + 1,c };
	char*** cpp = cp;
	printf("%s\n", **++cpp);
	printf("%s\n", *-- * ++cpp + 3);
	printf("%s\n", *cpp[-2] + 3);
	printf("%s\n", cpp[-1][-1] + 1);
	return 0;
}

代码分析:

看到这里,指针已经学习完了。相信你已经掌握了指针的相关概念,突破一个大知识点。学习是没有尽头的,祝大家学习进步,共勉~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值