首页 > 其他分享 >【C语言】一篇文章搞定C语言最难指针

【C语言】一篇文章搞定C语言最难指针

时间:2024-08-08 15:55:57浏览次数:18  
标签:搞定 变量 int C语言 地址 数组 printf 指针

目录

一、内存和地址

(1)什么是内存的地址

(2)如何寻找指定的内存地址

(3)CPU和内存传递数据的方式

二、指针变量和地址

(1)取地址操作符

(2)指针变量

(3)解引用操作符

(4)指针变量的大小

(5)指针变量的类型的意义

  ① 指针的解引用

② 指针 +/- 整数

③ void* 指针

三、const 修饰指针

(1)const 修饰变量

(2)const 修饰指针变量

四、指针的运算

(1)指针 +/- 整数

(2)指针-指针

(3)指针的关系运算

五、野指针

(1)形成野指针的原因

① 指针未初始化

② 指针访问数组元素越界

③ 指针指向的空间被释放

(2)避免野指针的方法

① 指针初始化

③ 不要返回局部变量的地址

④ 指针不用后及时置NULL,在使用指针前检查其有效性

六、assert 断言

七、传值调用和传址调用

八、数组名的理解

九、使用指针访问数组

十、一维数组传参的本质

十一、二级指针

十二、指针数组

(1)什么是指针数组

(2)指针数组模拟二维数组

十三、字符指针变量

十四、数组指针变量

十五、二维数组传参的本质

十六、函数指针变量

(1)函数指针变量的定义、初始化、使用

(2)练习题

(3)typedef 关键字

(4)函数指针数组

① 函数指针数组的定义和初始化

② 函数指针数组的应用(转移表)

(5)函数指针的应用(回调函数)

(6)回调函数的应用(qsort 函数的实现)

十七、结构体指针变量和 -> 操作符

十八、sizof 和 strlen 的对比

十九、数组和指针的笔试题解析

(1)一维数组

(2)字符数组

(3)二维数组(难点)

(4)指针运算

二十、不要小看日积月累的力量


一、内存和地址

(1)什么是内存的地址

        CPU处理数据时,会向内存读取数据和写入数据。因为内存比较昂贵,所以电脑的内存大小一般是8GB/16GB/32GB等;而外存比较便宜,一般是500GB/1T等。内存这么小,那么是如何高效管理的呢?内存被分为了一个个内存单元(相当于宿舍的房间),每个单元大小为1字节,及8 bit(相当于床位) ,每一个 bit 位上会存放1或者0(相当于住宿的人)。每一个单元的编号,就叫做地址(相当于房间号),在C语言中地址有一个新名字,叫做指针。

        因此,内存单元的编号 == 地址 == 指针

(2)如何寻找指定的内存地址

        计算机中并不是像在每个房间门口挂门牌号一样把每个内存单元的地址记录下来,而是通过硬件设计完成的。比如钢琴上的每一个键都没有记录是哪一个音,但是演奏者可以通过钢琴的硬件设计约定的规则知道是哪一个音。

(3)CPU和内存传递数据的方式

        计算机中有很多硬件单元,硬件单元之间要协调工作,协调工作至少要做到数据传递,数据的传递是通过 “线” 来完成的。CPU和内存之间传递地址数据,就是通过地址总线完成的。32位机器中有32根地址总线,每一根总线有两态1和0(电脉冲有无),那么32根总线就能表示2^32个地址;同理,64位机器中,有64根总线,能表示2^64个地址。

        1位十六进制数表示4位二进制数,8位十六进制数表示4 x 8 = 32位二进制数,图上是32位机器的内存。

        CPU向内存读取数据:控制总线写为R,CPU将想读的数据的内存地址通过地址总线传递给内存,内存根据该地址将对应的数据通过数据总线传递给CPU。

         CPU向内存写入数据:控制总线写为W,CPU将想写入的数据的存放地址通过地址总线传递给内存,将想写入的数据通过数据总线传递给内存,内存按地址写入数据。

二、指针变量和地址

(1)取地址操作符

        回到C语言,C语言中定义变量的本质是向内存申请存储空间。一个 int 类型的变量要申请 4个字节的内存空间存储数据,每 1 个字节的数据单独存放在一个内存单元里,因此它们都有单独的内存地址

        为了得到变量 a 的地址,可以使用取地址操作符&

        可以发现,变量 a 的地址是最低的一位字节的数据的地址。但是整型是 4 个字节,只知道一位字节的数据的地址,如何得到完整的数据呢?请看(5)指针变量类型的意义.指针的解引用 部分。

(2)指针变量

        地址也是一种数据,如上面的0X00CFFB3C,它也需要用变量存储起来,存储它的变量类型就是指针变量

        如上图,取出了变量 a 的地址值,存储到了指针变量p中,int* 就是指针变量 p 的类型。

        怎么理解 int* 呢?指针变量 p 存储的地址值,是指向整型变量 a 的。那么, * 就表示变量 p 是指针变量,int 就表示指针变量 p 是指向的 int 类型的变量。

(3)解引用操作符

        地址值存放在了指针变量里,在之后想要使用指针变量访问地址指向的变量,需要用到解引用操作符 * :

        从上图可以看到,通过指针变量 p 和解引用操作符*,成功修改了变量 a 的值。可以把 * 和 &看作是一对的,&用来取变量的地址,* 用来根据地址得到地址指向的变量。

        明明可以直接用变量 a 改值,为什么还要多此一举,用指针变量 p 间接改变量 a 的值呢?我打个比方,黑老大并不是所有事情都亲历亲为,有些事情他不能出面做,就会叫小弟去办事。用指针变量间接修改变量值,多了一种途径,写代码就会更加灵活。在后面的学习中(const 修饰变量、传值调用和传值调用),能够更深刻理解它的意义。

(4)指针变量的大小

        32位机器有32根地址线,每根地址线可以传递1 bit 的数字信号(0或1),那么 32 根线就有32 bit,即 4 个字节的二进制序列作为一个地址,故指针变量要存储地址需要 4 字节大小。

        同理,64位机器有64根地址线,那么就有 64 bit,即 8 个字节的二进制序列作为一个地址,故指针变量要存储地址需要 8 字节大小。

        代码验证:

(5)指针变量的类型的意义

  ① 指针的解引用

        看下面的代码:

        对于 int 类型的变量 a ,用 int* 类型的指针变量 p1 修改 a 的值,会修改 4 个字节的数据。

        对于 int 类型的变量 b ,用 char* 类型的指针变量 p2 修改 b 的值,会修改 1 个字节的数据。

        结论:指针的类型,决定了对指针解引用时,能操作的字节大小。

        比如 int* 类型的指针,能操作4个字节;而 char* 类型的指针,能操作1个字节。在讲取地址操作符时,说到了取的只是变量最低一个字节的地址,那么如何根据这个地址得到一个完整的变量值,就需要根据指针变量的类型来实现。

② 指针 +/- 整数

        因为变量 n 是 int 型的,取的地址应该存在 int* 类型的指针变量里,与char* 类型不兼容,所以在第220行用了强制转换,如果不强制转换编译器会有警告。

        从运行结果可以看到,对于 char* 类型的指针变量,每加减 1 ,地址会加减 1,即跳过 1 个字节长度的数据;对于 int* 类型的指针变量,每加减 1 地址会加减 4,即跳过 4 个字节长度的数据。

        结论:指针变量的类型,决定了指针变量移动一步的距离。

③ void* 指针

        void* 指针是无具体类型的指针,又叫泛型指针,它能接收任意类型的地址。但是他不能直接进行指针的解引用和加减整数的运算。

        从上图的运行结果可以看到,int 类型变量的地址赋值给 char* 类型的指针变量,会因为类型不兼容导致编译器报警告。而void* 没有这样的问题,它能接受任意类型的地址。

        从上图的运行结果可以看到,void* 虽然能接收任意类型的地址,但是不能进行指针的解引用操作和加减整数的运算。那么void* 的用处何在?在后面函数的参数部分会详细讲解。

三、const 修饰指针

(1)const 修饰变量

        想让一个变量值不可被修改,可以使用 const 修饰变量:

        从上图可以看到,编译器报错说 b 不可修改,但这只是语法层面上(有 const 修饰)的报错,变量 b 本质上还是变量,并不是常量。变量 b 被 const 修饰后,不能被直接修改,但是可以通过它的地址,去间接修改它的值(注:const 写变量类型前面、后面都可,通常是习惯写前面):

        这就是在讲解引用操作符的时候,说到的指针变量的意义的具体例子。黑老大变量 a 因为被const 修饰限制住了,所以没办法自己改值,就叫小弟指针变量 p 去修改。虽然这样可以成功修改,但是却破坏了语法规则,就是在乱来。因此,我们需要把指针变量也限制起来,让它不能通过地址修改值。

(2)const 修饰指针变量

  • const 放在 * 左边:修饰的是指针指向的内容,不能修改指针指向的内容,但是能修改指针的内容。
  • const 放在 * 右边:修饰的是指针的内容,不能修改指针的内容,但是能修改指针指向的内容。
  • * 两边都有 const :指针的内容和指针指向的内容都不能改。

        代码验证:

四、指针的运算

(1)指针 +/- 整数

        利用指针 + / - 整数打印数组:

        数组的下标增加,对应的地址也会随着增加。指针变量 p 的类型 int 与 arr 数组的元素的类型 int 匹配,因此指针变量 p 每加一,就会向后移动一个 arr 数组元素的距离。

(2)指针-指针

        利用指针 - 指针计算字符串的长度:

        因为在C语言中没有字符串类型,字符串本质上是字符数组,所以字符串 “abcd”传递过去的实际是数组的第一个字符的地址。因此,形参 s 是 char* 类型的。因为字符串末尾会自动加 '\0',标志字符串的结束,所以 my_strlen 函数中在地址指向 ' \0 ' 的时候就结束继续往后增加地址。

        在两个指针指向同一块空间的前提下,指针减指针的绝对值(也可以是 t - s)等于两个指针之间元素的个数。

(3)指针的关系运算

        利用指针的关系运算倒着打印数组:

五、野指针

        野指针就是指针指向的位置是不可知的(随机的)。当野指针访问指针指向的对象时,属于非法访问,如果对其中的内容进行修改,很容易引起程序崩溃。

(1)形成野指针的原因

① 指针未初始化

        指针未初始化,会给指针一个随机的地址。VS 要求非常严格,会直接报错;在小熊猫 C++ 上可以打印出给未初始化的指针分配的随机地址。

int main() {
	int* p; // 未初始化
	*p = 20;
	return 0;
}

② 指针访问数组元素越界

        数组越界,非法访问。

③ 指针指向的空间被释放

        因为局部变量的生命周期在 test 函数内定义时开始,退出 test 函数结束,所以指针 p 被赋值的地址,其对应的空间已经被释放。我测试了一下,还能在主函数反复打印出相同的地址和值,我的理解是,空间还在,但是没有权限拥有它了。就像在酒店订了一个房间,退房过后房间还在,里面的东西还在,人也能暴力进去,但是没有权限再入住。非法访问就像强行入住一样。

(2)避免野指针的方法

① 指针初始化

        如果明确知道指针指向的地方,就赋值为地址;如果指针不明确指向的地方,就赋值为NULL。NULL的值就是0,地址0是不能使用的,向 0 地址内读写会报错。补充:' \0 ' 的值也是0,字符 0 的值是48。在 VS 里按住 Ctrl 左击 NULL,可以看到NULL的定义:

        在C++里面NULL为0,在C里面NULL也为0,只不过类型被强制转换成了 void*。

② 指针不要越界访问

        指针不能指向没有被申请的内存空间,指针的访问超过了被申请的空间的范围,就是越界访问。

③ 不要返回局部变量的地址

④ 指针不用后及时置NULL,在使用指针前检查其有效性

        因为有个约定成俗的规则:指针被赋值为NULL就不能再访问。所以在指针不用后立即置NULL,表示不可访问;使用指针前判断是不是NULL,不是NULL才有效。

六、assert 断言

        宏 assert(),被称为断言,定义在头文件 <asser.h> 中。它的作用是:当括号内的表达式为真时,不做任何处理,正常运行;当表达式为假时,报错并终止运行。错误信息会写入标准错误流stderr,并打印在屏幕上,包括表达式、表达式所在文件名、表达式所在行号:

        如果确定代码没问题,不想用断言了,还可以在 #include<assert.h> 前定义一个宏 # define NDEBUG,无需改代码就能关闭断言机制。想再打开断言机制,注释掉 # define NDEBUG 就行了:

        断言的缺点就是,会有额外的检查,增加运行的时间。因此,断言仅在Debug版本中使用,便于排查错误;在 Release 版本中禁用,避免影响用户的使用体验。在VS中的 Release 版本里是直接把断言优化掉了。

七、传值调用和传址调用

        先看一个例题,交换两个变量的值:

       查看形参 x、y 和实参 a、b 的地址:

        可以看到形参 x 和实参 a 的地址不同,形参 y 和实参 b 的地址不同。因此,x 和 y 的交换并不会改变 a 和 b 的值,最终打印的结果当然是没有变化的。这种把变量本身传给函数的函数调用方式,就叫做传值调用

        那么怎么才能做到在调用函数里面更改主函数的值呢?用传值调用实现两个变量的交换失败,是因为在调用函数里面申请了新的变量空间;如果能将主函数变量的地址传递过去,就能间接修改主函数变量的值:

        可以看到变量 a 和 b 的值成功交换了,因为指针变量 x 指向的是变量 a ,指针变量 y 指向的是变量 b,这就是指针变量存在的必要性。这种把变量的地址传递给函数的函数调用方式,就叫做传址调用

        当只需要在调用函数里面做计算时,用传值调用;当还需要在调用函数中更改主函数的变量值时,用传址调用。

八、数组名的理解

        数组名就是数组首元素的地址:

        以下两种情况例外,数组名不是数组首元素地址,而是表示整个数组

  • sizeof(数组名):计算的是整个数组的大小,单位是字节。

  • &数组名:取的是整个数组的地址(与数组首元素地址有区别)。

        虽然 &arr 和 &arr[0]、arr 指向了同一个地址,但是 &arr 增加 1 增加了一个元素的距离(4字节),而&arr[0]、arr 增加 1 增加了一个数组的距离(十六进制88 - 60 = 28 = 十进制 40 字节)。在指针 +/-整数的部分讲到,指针的类型决定了指针增加 1 后跳过的距离,所以 &arr 是整型数组的地址,而 &arr[0]、arr 是整型数组元素的地址。对于存放 &arr 地址的指针类型,在数组指针变量部分有更详细的讲解。

九、使用指针访问数组

        在上一节讲到,数组名 arr 就是数组首元素的地址。因为 arr 是地址,所以可以把数组名赋值给指针变量 p ,在这里数组名 arr 就与指针变量 p 等价了。因此,在访问数组时,*(p + i)  等价于 *(arr + i),arr[ i ] 等价于 p[ i ]。

        arr[ i ] 和 p[ i ] 本质上就是 *(arr + i) 和 *(p + i),编译器在处理时,也会把前者替换成后者。又因为在加法里面  *(arr + i) 和 *(p + i) 等价于  *(i + arr) 和 *(i + p),所以使用 i [arr] 和 i [p] 访问数组编译器也不会报错的,但是这样不便于理解,不建议使用。

十、一维数组传参的本质

        将数组名作为实参传递给调用的函数,不能在函数内部计算数组的长度。因为在之前讲过,数组名就是数组首元素的地址,虽然在函数的形参部分可以写成数组的形式(方便理解),但本质上是指针,通过指针是不能计算数组的长度的。如下图所示,arr 作为数组首元素地址传递给函数 test,函数 test 内部的 arr 本质上是存放数组首元素地址的指针变量(形参部分本质是 char* 类型),指针变量的大小在 x86 环境下就是4个字节:

        又因为传递的是地址,并没有在函数内部重新创建新的数组,所以不需要在参数部分写明数组的长度,写了也没错。

十一、二级指针

        指针也是变量,是变量就会有地址,如果指针变量的地址想存储在变量里,就用二级指针变量。下图中 a 为 int 类型;一级指针 p 存放变量 a 的地址,* 表示 p 为指针,int 表示指向的对象为 整型;二级指针 pp 存放一级指针变量 p 的地址,* 表示 pp 为指针,int* 表示指向的对象为一级指针。

int main() {
	int a = 10;
	int* p = &a; 
	int** pp = &p;

	// p = &a; 等价于 *pp = &a
	// a = 10; 等价于 **pp = 10;

	return 0;
}

十二、指针数组

(1)什么是指针数组

        存放整数的数组叫做整数数组;存放字符的数组叫做字符数组;那么存放指针的数组就叫做指针数组。

// 整数数组
int arr1[5];

// 字符数组
char arr2[5];

// 指针数组
int a = 1;
int b = 2;
int c = 3;
int* arr3[5] = {&a, &b, &c};

(2)指针数组模拟二维数组

        p[i] 存放了数组首元素的地址,可以把 p[ i ] 看作指针 pi,pi[ j ] (p[ i ][ j ])就表示访问数组的元素。也可以写成 *(*(p + i) + j)。

        这只是模拟二维数组,但并不是二维数组,因为二维数组是连续存放的,而这三个数组 arr1、arr2、arr3并不是连续存放的。

十三、字符指针变量

        上面的代码中,并不是把字符串存储在了字符指针变量 p 里,而是把字符串的首字符地址存在了字符指针 p 里。对于数组,里面的内容是变量,是可以改变的;而字符串是一个常量,在内存中不可修改。为了禁止非法修改字符串的内容,在 * 前用了 const 修饰。

        看下面来自《剑指offer》的一道题,结果是什么?

#include <stdio.h>
int main()
{
    char str1[] = "hello bit.";
    char str2[] = "hello bit.";
    const char *str3 = "hello bit.";
    const char *str4 = "hello bit.";
    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;
}

        答案是:

        对于相同的常量,C/C++并不会开辟不同的内存空间。因为常量不需要修改,同样的内容没必要放在不同的内存空间里。而数组作为变量,是有可能被修改的,所以需要申请不同的内存空间。因此,str1 和 str2 存放的是不同的地址,str3 和 str4 存放的是相同的地址。 

十四、数组指针变量

        整形指针变量,存放的是整型变量的地址;浮点型指针变量,存放的是浮点型变量的地址;那么数组指针变量,就是存放数组的地址的指针变量

int (*p)[5]; // 数组指针变量
int arr[5] = {0};
p = &arr; // 数组指针初始化

int* arr[5]; // 指针数组

        对于数组指针变量:首先 p 得是个指针,所以要把 * 跟 p 捆绑在一起(因为[]比*的优先级高,所以要用小括号把*p括起来),其次这个指针指向的对象类型是 int[5]。这样就可以解释在数组名的理解这一节中遗留的 &arr 问题了,因为指针的类型决定了它加减整数跳过的距离,然后存放 &arr 地址的指针类型是 int[10],所以 &arr + 1能跳过整个数组的距离。

        p == &arr,那么 *p == *&arr == arr ,因此可以用 (*p)[ i ] 或者 *(*p + i) 来访问数组元素:

十五、二维数组传参的本质

        之前,当函数的实参是二维数组名时,我们是这样写函数的形参和遍历二维数组的:

// 二维数组形式的形参可以省略行数,不能省略列数
void test(int a[3][5], int r, int c) 
{
	int i = 0;
	int j = 0;
	for (i = 0; i < r; i++)
	{
		for (j = 0; j < c; j++)
		{
			printf("%d ", a[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;
}

        但是二维数组形式的形参写法只是为了好理解,本质上二维数组名传递过去的是一个地址。可以把二维数组看作是元素为一维数组的数组,二维数组的每一行就是一个一维数组。因为数组名是数组首元素的地址,所以二维数组名就是第一行一维数组的地址。数组的地址存放在数组指针当中,故 test 函数的形参本质上是 int (*p) [5] ,表示指针 p 指向 int [5] 类型的一维数组。

         之前学过,二维数组在内存中是连续存放的,数组元素的地址也会随着数组下标的增加而增加,而且指针的类型决定了指针 + / - 整数的移动距离。因为指针 p 是 int [5]类型的,所以每加1就会向后移动一个有5个int类型元素的数组的大小。

        如图 p + 0 表示第一行一维数组的地址;p + 1 表示第二行一维数组的地址;p + 2 表示第三行一维数组的地址。又结合数组指针变量的知识,*(p + 0)就表示第一行一维数组的数组名;*(p + 1)就表示第二行一维数组的数组名;*(p + 2)就表示第三行一维数组的数组名。

        上面的数组名记为 *(p + i),0 ≤ i ≤ 2。一维数组名 *(p + i) 就是一维数组的首元素的地址,一维数组的首元素地址存放在类型为 int 的指针里,那么 *(p + i) 每加1移动的距离就是一个 int 变量的大小,故 *(p + i) + j ,0 ≤ j ≤ 4 就表示一维数组中每个 int 类型的元素的地址,访问二维数组的元素就可以用 *(*(p + i) + j)。改写代码如下:

十六、函数指针变量

(1)函数指针变量的定义、初始化、使用

        变量可以取地址,数组可以取地址,函数也可以取地址:

        数组名表示数组首元素的地址,&数组名表示数组的地址。从上面的运行结果得出:不同于数组的是,函数名 和 &函数名都是函数的地址。函数的地址就用函数指针变量存储:

        在74、75行,函数指针 p1 和 p2 前面的 int 表示 Add 函数的返回值;(int, int)表示 Add函数的参数,里面的变量名可写可不写。

        我们通常调用 Add 函数是这样写的:Add(1, 2)。在上图74行将函数指针 p1 被初始化为Add,说明也可以用 p1(1, 2)的形式调用 Add 函数。因此 * 对于用函数指针调用函数就是一个摆设,它可以不写,也可以写很多个。

        再提个醒:

        上图的这种写法,*只是给 p 的,表示指针;而 q 不是指针。如果想想在一条语句中定义两个指针变量,应该这样写:int *p, *q。

(2)练习题

        下面两段代码均来自《C陷阱和缺陷》。

        代码1:

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

        解读:void (*) () 是函数指针类型,指向的函数的返回值为void,没有参数;(void (*) ())0 是将数字 0 强制转换为函数指针类型,在内存里地址也有是 0 的;(*(void (*)())0) 是写成解引用函数指针的形式,等价于函数名;最终整个语句的效果是调用内存地址0存放的函数。

        代码2:

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

        解读:void(*)(int) 是函数指针类型,指向的函数返回值为 void,有一个参数,类型为 int;如果把上面语句中的 signal(int , void(*)(int)) 去掉,会得到 void(*)(int) 函数指针类型;声明返回值为void(*)(int) 函数指针类型的函数是这样的语法形式:

返回值为 int* 类型的函数声明:
int* 函数名(参数列表);

返回值为 void(*)(int) 类型的函数声明:
void(* 函数名(参数列表) )(int) // 正确写法
void(*)(int) 函数名(参数列表) // 错误写法

        因此,代码2是,一个函数名为signal,返回值为 void(*)(int) 类型,参数列表为(int , void(*)(int)) 的函数声明语句。

(3)typedef 关键字

        代码2理解起来比较困难,但是可以简化代码2,让其更容易理解,用 typedef 关键字实现类型重命名

// 重命名 无符号 int 
typedef unsigned int uint;

// 重命名 数组指针
typedef int (*)[5] ptr_arr; // 错误写法
typedef int (* ptr_arr)[5]; // 正确写法

// 重命名 函数指针
typedef void (*)(int) ptr_fun; // 错误写法
typedef void (*  ptr_fun)(int); // 正确写法

        因此,可以把代码2改写为:

// 重命名
typedef void(* ptr_fun)(int);

// 函数声明
ptr_fun signal(int, pte_fun);

(4)函数指针数组

① 函数指针数组的定义和初始化

        先看下面代码:

        p 先和 [] 结合表示数组,数组元素的类型是 int (*)(int, int) 函数指针。

② 函数指针数组的应用(转移表)

        做一个练习,写一个计算器:

        不用函数指针数组版:

void menu() {
	printf("*************************\n");
	printf("***** 1.add   2.sub *****\n");
	printf("***** 3.mul   4.div *****\n");
	printf("*****     0.exit    *****\n");
	printf("*************************\n");
	printf("请选择:");
}
int add(int a, int b)
{
	return a + b;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a * b;
}
int div(int a, int b)
{
	return a / b;
}
int main()
{
	int x, y;
	int input = 0;
	int ret = 0;
	do
	{
		menu();
		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;
}

       可以看到这个代码非常冗余,在主函数中 switch 语句很长,如果想要加其它的计算,还会写得更长;并且想要减少或者增加计算功能还需要改写 switch 语句。

        使用函数指针数组版:

void menu() {
	printf("*************************\n");
	printf("***** 1.add   2.sub *****\n");
	printf("***** 3.mul   4.div *****\n");
	printf("*****     0.exit    *****\n");
	printf("*************************\n");
	printf("请选择:");
}
int add(int a, int b)
{
	return a + b;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a * b;
}
int div(int a, int b)
{
	return a / b;
}
int main()
{
	int x, y;
	int input = 0;
	int ret = 0;
	int (*ptr_arr[5])(int, int) = {0, add, sub, mul, div};
	do
	{
		menu();
		scanf("%d", &input);
		if (input >= 1 && input <= 4) {
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = ptr_arr[input](x, y);
			printf("ret = %d\n", ret);
		}
		else if (0 == input) {
			printf("退出程序\n");
			break;
		}
		else {
			printf("选择错误\n");
		}
	} while (input);
	return 0;
}

        函数指针数组的长度故意写成了5,因为这样可以让菜单的选项和数组下标对应起来。在之后需要增加或者减少函数,主函数内只需要改动函数指针数组定义和初始化的部分即可;并且这样写还大大减少了冗余的代码。这种函数指针数组实现了程序执行流程的转移,被称为转移表

(5)函数指针的应用(回调函数)

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

        回调函数不是由该函数的实现方直接调用,而是在特定事件或条件发生时,由另外的一方调用,用于对该事件或条件的相应。举一个简单的例子:

        主函数里将 add 作为参数传给函数 test,test 函数里用函数指针调用 add 函数,add 函数就是回调函数。

        add 函数不是实现方主函数直接调用的,而是由另一方 test 函数调用的。

        回到实现计算器的代码中,看看它的问题:

        框出来的几个部分都存在相同的代码,造成代码冗余。如何简化这个代码呢?如果把相同的代码提到 switch 语句外面(把输入操作数的部分放在输入选项后面)会造成逻辑错误,比如输入了不存在的选项9,也会要求输入操作数,所以这种办法行不通。

        框出来的几个部分只有函数调用的语句不同,所以我们可以使用回调函数,将要调用的函数作为参数传递给另一个函数,另一个函数用函数指针接收,通过函数指针调用函数。代码如下:

(6)回调函数的应用(qsort 函数的实现)

        qsort 函数能实现任意类型的数组排序,底层使用的排序算法是快速排序,定义在 <stdlib.h> 头文件中。它的函数原型为:

void qsort (void* base, // 待排序数组的首元素地址
            size_t num, // 待排序数组的元素个数
            size_t size, // 待排序数组的元素长度,单位为字节
            int (*compar)(const void*,const void*));
            // 函数指针,指向用于比较两个任意类型的元素的函数
            // p1指向的元素大于p2指向的元素,返回值 > 0
            // p1指向的元素小于p2指向的元素,返回值 < 0
            // p1指向的元素等于p2指向的元素,返回值 0
  •  qsort 函数,在VS中由微软实现。
  •  比较函数,由qsort函数的使用者实现。

        因此,用户想要使用qsort函数排序任意类型的数组,必须先自定义一个比较函数。在这里,我想要排序整型数组,就需要提供一个能比较两个整型变量的函数;想要排序结构体数组,就需要提供一个能比较两个结构体变量的函数。

        学生结构体的定义:

struct Stu {
	char name[20];
	int age;
};

        实现比较两个整型变量的函数:

int cmp_int(void* p1, void* p2) {
	return *(int*)p1 - *(int*)p2;
}

        实现比较两个结构体变量的函数(以年龄为准):

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

        实现比较两个结构体变量的函数(以名字为准):

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

        实现整型数组排序:

        实现学生结构体数组排序(以年龄为准):

        实现学生结构体数组排序(以名字为准):

       从上面的结果可以观察到,排好的序列是升序。想要降序,只需要改返回值的规则:

  • p1指向的元素大于p2指向的元素,返回值 < 0
  • p1指向的元素小于p2指向的元素,返回值 > 0

        那么,如何自定义一个qsort函数,实现对任意类型的数组排序呢?VS的编译器用的底层算法是快速排序,我想使用冒泡排序实现。首先看一下使用冒泡排序对整型数组排序:

         对于实现任意类型的数组排序,需要对下面三个部分进行改进:

  1.  参数的类型不能固定为int*,而是使用void*(可以接受任意类型的指针)。
  2.  并不是所有的数据都能通过关系运算符比较大小,比如结构体、字符串。因此要接收一个函数指针,该指针指向自定义的比较两个元素大小的函数。
  3.  交换两个变量的中间变量不能固定为 int*。

        那么,qsort函数原型中的 num(待排序数组的元素个数)是用来干嘛的呢?因为 base 指针以及 compar 指针的参数都是 void* 类型的,所以是不知道指针+/- 1移动的距离的。因此需要 num来告诉要移动多少个距离。对于 num 和 size 它们不可能有负数的情况,所以将它们定义为 size_t类型。用冒泡排序实现任意类型数组的排序实现如下:

        bubble_sort函数,实现冒泡排序任意类型数组:

void bubble_sort(void* base, size_t num, size_t size, int(*cmp)(const void*, const void*)) {
	int cnt = 0; // 执行趟数
	// 趟数。前面 sz - 1 趟排好 sz - 1 个数, 最后一个数自然排序好
	for (size_t i = 0; i < num - 1; i++) {
		cnt++;
		int flag = 0;
		// 一趟。j + 1 < sz - i
		for (size_t j = 0; j < num - i - 1; j++) {
			// 把 base 强制转换为 char类型指针,指针每 +/- 1移动1个字节长度
			// base 为数组首元素地址,首元素记为arr[0],那么它距离arr[j]就是 j * size 字节的长度
			// 因此,base + j * size 就可以表示arr[j]的地址
			if (cmp((char*)base + j * size, (char*)base + (j+1) * size) > 0) {
				swanp((char*)base + j * size, (char*)base + (j + 1) * size, size);
			    flag = 1;
            }
		}
		if (0 == flag)
			break;
	}
}

         swanp函数,实现交换两个数组元素:

void swanp(char* p1, char* p2, int size) {
	// 一个元素的大小为 size 字节,char型指针每 +/- 1移动1个字节长度
	// 那么,一个元素可以分成 size份,分成 size 次交换
	for (int i = 0; i < size; i++) {
		char t = *p1;
		*p1 = *p2;
		*p2 = t;
		// 到下一个字节的地址
		p1++;
		p2++;
	}
}

        test函数,测试int类型数组的排序:

void test() {
	int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
	int size = sizeof(arr[0]);
	int num = sizeof(arr) / size;

	bubble_sort(arr, num, size, cmp_int);
	print_int(arr, num);
}

        运行结果:

        这种在以后才确定数据类型的编程就叫做泛型编程,泛型指针 void* 就常用于泛型编程中。

十七、结构体指针变量和 -> 操作符

        我们知道,想在被调用函数中修改主函数的变量值,主函数中需要将变量的地址传递给被调用函数;但如果仅仅是在被调用函数中使用主函数的变量而不修改(如打印),就不需要传递变量的地址,只需要传递变量的值。

        可是,对于结构体变量,里面包含着许多成员信息,如果传递结构体变量的值,则会在被调用函数中创建新的复制版结构体变量,这会浪费很多时间和空间。因此,就算不在被调用函数中修改结构体变量的值,也建议传递结构体变量的地址,这样只需要申请一个指针类型大小的空间(4或8个字节)。

        在被调用函数中接收到结构体的指针,例如 struct s *p,访问指针 p 指向的结构体的成员,可以用:

(*p).menber

        也可以用指向结构体成员操作符“->”:

p -> menber

        这样可以简化结构体指针访问成员的代码。

十八、sizof 和 strlen 的对比

        sizeof 操作符详情看:http://t.csdnimg.cn/JIwwb。strlen 函数用于计算字符串的长度,遇到\0结束计算,如果字符串中没有\0就会一直向后访问(越界),最终得到一个随机的长度(在字符串后面内存中,\0所在的位置是不知道的):

        sizeof 和 strlen 的对比表:

十九、数组和指针的笔试题解析

        一定要记住!!!数组名就是数组首元素地址,以下两种情况除外(数组名表示整个数组):

  • sizeof(数组名):计算的是整个数组的内存空间大小。
  • &数组名:取出的是整个数组的地址。

(1)一维数组

        代码及解析:

// 指针类型所占内存大小:4字节(x86)/8字节(x64)
int a[] = {1,2,3};
printf("%zd\n",sizeof(a)); // 计算的是整个数组的大小 -- 3*4 = 12
printf("%zd\n",sizeof(a+0)); // 计算的是首元素的地址的大小 -- 4/8
printf("%zd\n",sizeof(*a)); // 计算的是首元素的大小 -- 4
printf("%zd\n",sizeof(a+1)); // 计算的是第二个元素的地址的大小 -- 4/8
printf("%zd\n",sizeof(a[1])); // 计算的是第二个元素的大小 -- 4
printf("%zd\n",sizeof(&a)); // 计算的是数组地址的大小 -- 4/8
printf("%zd\n",sizeof(*&a)); // & 和 * 抵消,计算的是整个数组的大小 -- 12
printf("%zd\n",sizeof(&a+1)); // &a是数组的地址,那么该句计算的是跳过数组后的地址的大小 -- 4/8
printf("%zd\n",sizeof(&a[0])); // 计算的是首元素地址的大小 -- 4/8
printf("%zd\n",sizeof(&a[0]+1)); // 计算的是第二个元素的地址的大小 -- 4/8

        运行结果(x64环境下):

(2)字符数组

        代码1:

char arr[] = {'a','b','c','d','e','f'};
printf("%zd\n", sizeof(arr)); // 整个数组的大小 -- 6
printf("%zd\n", sizeof(arr+0)); // 数组首元素地址的大小 -- 4/8
printf("%zd\n", sizeof(*arr)); // 数组首元素的大小 -- 1
printf("%zd\n", sizeof(arr[1])); // 第二个元素的大小 -- 1
printf("%zd\n", sizeof(&arr)); // 数组的地址的大小 -- 4/8
printf("%zd\n", sizeof(&arr+1)); // 跳过数组后的地址的大小 -- 4/8 
printf("%zd\n", sizeof(&arr[0]+1)); // 第二个元素的地址的大小 -- 4/8

        代码1 运行结果:

        代码2:

// 字符数组中没有\0,会越界寻找\0
// strlen函数原型:size_t strlen(const char* str);
char arr[] = {'a','b','c','d','e','f'};
printf("%zd\n", strlen(arr)); // 从数组首元素地址开始,随机值
printf("%zd\n", strlen(arr+0)); // 从数组首元素地址开始,随机值
printf("%zd\n", strlen(*arr)); // 以首元素的值'a'-97 为地址开始,非法访问,err
printf("%zd\n", strlen(arr[1])); // 以第二个元素的值 'b'-98 为地址开始,非法访问,err
printf("%zd\n", strlen(&arr)); // 数组的地址,强制转换为 char*,与数组首元素地址开始相同,随机值
printf("%zd\n", strlen(&arr+1)); // 以跳过数组的地址开始,强制转换为 char*,随机值
printf("%zd\n", strlen(&arr[0]+1)); // 以第二个元素的地址开始,随机值

        代码2 运行结果:

        代码3:

// 字符串末尾会自动加\0
char arr[] = "abcdef";
printf("%zd\n", sizeof(arr)); // 数组大小 --7
printf("%zd\n", sizeof(arr+0)); // 数组首地址大小 -- 4/8
printf("%zd\n", sizeof(*arr)); // 数组首元素大小 -- 1
printf("%zd\n", sizeof(arr[1])); // 数组第二个元素大小 -- 1
printf("%zd\n", sizeof(&arr)); // 数组的地址大小 -- 4/8
printf("%zd\n", sizeof(&arr+1)); // 跳过数组的地址大小 -- 4/8
printf("%zd\n", sizeof(&arr[0]+1)); // 第二个元素的地址大小 -- 4/8

        代码3 运行结果:

        代码4:

// 字符串末尾自动加\0,strlen计算长度不包含\0
char arr[] = "abcdef";
printf("%zd\n", strlen(arr)); // 以数组首元素地址开始 -- 6
printf("%zd\n", strlen(arr+0)); // 以数组首元素地址开始 -- 6
printf("%zd\n", strlen(*arr)); // 以数组首元素值 'a'-- 97 为地址开始,非法访问,err
printf("%zd\n", strlen(arr[1])); // 以数组首元素值 'b'-- 98 为地址开始,非法访问,err
printf("%zd\n", strlen(&arr)); // 数组地址,强制转换为 char*,效果与以数组首元素地址开始相同-- 6
printf("%zd\n", strlen(&arr+1)); // 跳过一个数组的地址开始(包括\0),随机值
printf("%zd\n", strlen(&arr[0]+1)); // 以第二个元素的地址开始 -- 5

        代码4 运行结果:

        代码5:

char *p = "abcdef";
printf("%zd\n", sizeof(p)); // 计算指针存储字的符串的首字符地址的大小 -- 4/8
printf("%zd\n", sizeof(p+1)); // 计算第二个字符地址的大小 -- 4/8
printf("%zd\n", sizeof(*p)); // 计算第一个字符的大小 -- 1
printf("%zd\n", sizeof(p[0])); // 计算第一个字符的大小 -- 1
printf("%zd\n", sizeof(&p)); // 计算指针p的地址的大小 -- 4/8
printf("%zd\n", sizeof(&p+1)); // 计算跳过一个指针p的地址的大小 -- 4/8 
// 假设 &p 存在 char** pp 中,那么 pp(&p) 每加一都会向后移动 char* 的大小
printf("%zd\n", sizeof(&p[0]+1)); // 计算第二个字符的地址的大小 -- 4/8

        代码5 运行结果:

        代码6:

// 字符串结尾自动加\0
char *p = "abcdef";
printf("%zd\n", strlen(p)); // 从第一个字符地址开始 -- 6
printf("%zd\n", strlen(p+1)); // 从第二字符地址开始 -- 5
printf("%zd\n", strlen(*p)); // 从第一个字符的值 'a'-97 为地址开始,非法访问,err
printf("%zd\n", strlen(p[0])); // 从第一个字符的值 'a'-97 为地址开始,非法访问,err
printf("%zd\n", strlen(&p)); // 从指针p的地址开始,随机值
printf("%zd\n", strlen(&p+1)); // 以指针p的地址起,然后跳过一个指针后的地址开始,随机值
printf("%zd\n", strlen(&p[0]+1)); // 从第二个字符的地址开始 -- 5

// 地址'a'-97在该程序中未被分配,所以是err报错
// 指针p的地址是分配给&p的,所以能访问,但是不确定\0的位置,故为随机值

        代码6 运行结果:

(3)二维数组(难点)

        代码:

// 二维数组可以看作一维数组为元素的数组,每一行就是一个一维数组
int a[3][4] = {0};
printf("%zd\n",sizeof(a)); // 单独的数组名,整个数组的大小 -- 3*4*sizeof(int) = 48
printf("%zd\n",sizeof(a[0][0])); // 第一行第一个元素的大小 -- 4
printf("%zd\n",sizeof(a[0])); // 单独的第一行的数组名,第一行的大小 -- 4*4 = 16
printf("%zd\n",sizeof(a[0]+1)); // 不是单独的数组名,a[0]表示第一行的首元素地址,a[0]+1是第一行第2个元素的地址的大小 -- 4/8
printf("%zd\n",sizeof(*(a[0]+1))); // 第一行第2个元素的大小 -- 4
printf("%zd\n",sizeof(a+1)); // 不是单独的数组名,a是二维数组首元素:第一行的地址,a+1是第二行的地址大小 -- 4/8
printf("%zd\n",sizeof(*(a+1))); // 第二行的大小 -- 16
printf("%zd\n",sizeof(&a[0]+1)); // 对第一行的数组名a[0]取地址,&a[0]就是第一行的地址,加1就是第二行的地址的大小 -- 4/8
printf("%zd\n",sizeof(*(&a[0]+1))); // 对第二行的地址解引用,第二行的大小 -- 16
printf("%zd\n",sizeof(*a)); // 不是单独的数组名,a是二维数组首元素第一行的地址,解引用就是第一行的大小 -- 16
printf("%zd\n",sizeof(a[3])); // 不存在越界,只是计算类型的大小,并不访问数据(比如sizeof(int))。第四行的数组名,第四行的大小 -- 16

        运行结果:

(4)指针运算(难点)

       下列题目的结果是什么?

        题目1:

#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;
}

        解析:

        &a是数组的地址(类型 int (*)[5]),则&a+1是&a向后跳过一个 int [5] 的长度;&a+1强制转换为 int*赋值给ptr,则ptr - 1 是ptr向前跳过一个 int 的长度,ptr-1解引用为5;a是数组首元素地址(类型 int*),则a+1是a向后跳过一个 int 的长度,a+1解引用为2

        运行结果:

        题目2:

// 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;
}

       解析:

        首先,在x86环境下,指针的大小为4字节。然后将地址0x100000强制类型转换为Test结构体指针类型,赋值给结构体指针p;第一个printf:p为 struct Test* 类型,p + 1是p向后跳过 struct Test 的长度,0x100000 + 0x000014(20的十六进制)= 0x100014;第二个printf:p为无符号长整型,不是指针,正常计算值,0x100000 + 0x1 = 0x100001;第三个printf:p为 unsigned int* 类型,p+1是p向后跳过 unsigned int 的长度,0x100000 + 0x000004 = 0x100004

        运行结果:

        题目3:

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

        解析:

        ()内的 , 表示逗号表达式,取最后一个表达式的值,代码中的初始化等价于 int a[3][2] ={ 1, 3, 5 }。a[0]是第一行的数组名,数组名是数组首元素的地址,即a[0]是元素 1 的地址,类型为 int*,赋值给 p ,p[0]是对p解引用,读取一个 int 长度的数组,得第一个元素值 1。

        运行结果:

        题目4:

// 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]);
}

       解析:

        指针p指向类型为 int[4] 的数组;a为二维数组名,表示首元素第一行一维数组的地址,类型为 int (*)[5]。

        p[4]为p向后跳过4个 int[4]的长度的数组名,表示第4行首元素的地址(类型为int*),p[4][2]表示第四行第二个元素,&p[4][2]是第四行第二个元素的地址;a[4]为a向后跳过4个int[5]的长度的数组名,表示第4行首元素的地址(类型为int*),a[4][2]表示第四行第二个元素,&a[4][2]是第四行第二个元素的地址。

        如图所示,&p[4][2] - &a[4][2] 就是指针 - 指针的问题,结果是两个指针间元素的的个数 -4。

第一个输出是%p,把-4转换为十六进制形式;第二个输出是%d,直接输出-4。

[-4]十进制

= [1000 0000 0000 0000 0000 0000 0000 0100]原

= [1111 1111 1111 1111 1111 1111 1111 1011]反

= [1111 1111 1111 1111 1111 1111 1111 1100]补

= [FFFFFFFC]十六进制

        运行结果:

        题目5:

#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));
}

       解析:

        &aa是二维数组的地址,&aa+1是a向后跳过一个 int[2][5]的长度,强制转换为 int*,赋值给ptr1,那么 ptr1 - 1是 ptr1向前移动一个 int 的长度,ptr1 - 1解引用为10。

        aa是二维数组名,表示数组首元素地址,即第一行一维数组的地址,类型为 int(*)[5],aa+1是a向后移动一个int[5]的长度,*(aa+1)就为第二行的一维数组名,表示第二行的首元素的地址,类型为 int*,赋值给ptr2,那么 ptr2 - 1是 ptr2向前移动一个 int 的长度,ptr2 - 2解引用为5。

        运行结果:

        题目6:

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

        解析:

         数组a的每一个元素,存储的是对应字符串的首字符地址;pa存储的是数组a的首元素地址,如图所示。最终*pa是字符串"at"的首字符地址,打印结果为字符串"at"。

        运行结果:

        题目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);
}

        解析:

        对于**++cpp:++cpp后指向的内容是c+2,因此*++cp为c+2,c+2再解引用,就是字符串"POINT"的首字符地址。这一句的打印结果为字符串"POINT"。

        对于*-- * ++cpp + 3:++cpp后指向的内容是c+1,因此*++cpp为c+1,c+1再前缀自减,得c,c解引用就是字符串"ENTER"的首字符地址,首字符地址 + 3就是字符E的地址。这一句的打印结果为字符串"ER"。

        对于*cpp[-2] + 3:等价于**(cpp - 2) + 3。cpp - 2指向的内容是c+3,因此*(cpp - 2)为c+3,c+3解引用就是字符串"FIRST"的首字符地址F,首字符地址+3就是字符S的地址。这一句的打印结果为字符串"ST"。

        cpp[-1][-1] + 1:等价于*(*(cpp - 1) - 1) + 1。cpp - 1指向的内容是c+2,因此*(cpp-1)为c+2,c+2 - 1得c+1,c+1解引用为字符串"NEW"的首字符地址,首字符地址再加1,就是字符E的地址。这一句的打印结果为字符串"EW"。

二十、不要小看日积月累的力量

        知道自己无知,是一个好的征兆,因为终于知道自己还有很多东西要学。这个过程必然会存在焦虑、绝望的心理。焦虑不能改变什么,只有一点点日积月累地学习,才能一步步攀登开悟之坡。

        放下高傲的姿态,静下心学习吧。

标签:搞定,变量,int,C语言,地址,数组,printf,指针
From: https://blog.csdn.net/2401_86272648/article/details/140828133

相关文章

  • C++ - 二级指针动态内存申请与释放
    C语言描述:#include"stdio.h"#include"stdlib.h"#include"assert.h"//二维数组内存申请int**createArray2D(introw,intclos){ int**pArray=(int**)malloc(sizeof(int*)*row); assert(pArray); for(inti=0;i<row;i++) { ......
  • 新手的第一个c语言小程序
      作为一个C语言的新手,我深知要想精通这门语言,就必须通过不断的练习来积累经验。因此,我决定从解决高中数学问题入手,编写我的第一个C语言小程序。  显然,高中的许多数学问题复杂难解,对于我这个初学者来说,理解答案本身就已经是一项挑战,更不用说用程序来求解了。所以,我选择了......
  • ERP帮助中心不会搭建?五步教你轻松搞定
    在企业管理中,ERP(企业资源计划)系统作为核心的信息管理工具,极大地提升了业务流程的自动化和集成度。然而,随着ERP系统功能的日益复杂,员工在使用过程中难免会遇到各种疑问和难题。这时,一个高效、易用的ERP帮助中心就显得尤为重要。它不仅能帮助员工快速解决问题,还能提升整体工作效......
  • C语言新手小白详细教程(6)函数
    希望文章能够给到初学的你一些启发~如果觉得文章对你有帮助的话,点赞+关注+收藏支持一下笔者吧~阅读指南:开篇说明为什么要使用函数?1.定义一个函数2.初步调用函数3.定义函数详解3.形式参数与实际参数4.使用return接收函数的返回值5.函数声明开篇说明截止目前,我......
  • C语言字符数组,字符指针,指针数组(字符串)的比较与使用
    参考文档https://blog.csdn.net/yuabcxiao/article/details/89600907 字符数组与字符指针在C语言中,可以用两种方法表示和存放字符串:(1)用字符数组存放一个字符串charstr[]="Iamhappy";(2)用字符指针指向一个字符串char*str="Iamhappy";字符数组#include<iostrea......
  • C语言 --- 指针
    目录1. 概念2.指针变量初始化2.1被调修改主调 2.2 指针变量的引用3.指针+一维整型数组3.1指针的运算4.指针+一维字符型数组4.1指针+字符串1. 概念指针就是地址 --- 内存单元的编号指针也是一种数据类型---这种数据类型专门用来处理地址......
  • C语言菜鸟入门·数据结构·链表超详细解析
     目录1. 单链表1.1 什么是单链表1.1.1  不带头节点的单链表1.1.2 带头结点的单链表1.2 单链表的插入1.2.1 按位序插入(1)带头结点(2)不带头结点1.2.2 指定结点的后插操作1.2.3 指定结点的前插操作1.3 单链表的删除1.3.1 按位序删除1.3.2 指......
  • new_d_array()函数接受一个int类型的参数和double类型的参数。该函数返回一个指针,指向
    /*下面是使用变参函数的一段程序:include<stdio.h>include<string.h>incude<stdarg.h>voidshow_array(constdoublear[],intn);double*new_d_array(intN,...);intmain(void){doublep1;doublep2;p1=new_d_array(5,1.2,2.3,3.4,4.5,5.6);p2=new_d_ar......
  • C语言----字符串的匹配
    字符串的匹配实例说明:        本实例实现对两个字符串进行匹配操作,即在第一个字符串中查找是否存在第二个字符串。如果字符串完全匹配,则提示匹配的信息,并显示第二个字符串在第一个字符串中的开始位置,否则提示不匹配。实现过程:        (1)在TC中创建一个C文......
  • 嵌入式初学-C语言-十七
    #接嵌入式初学-C语言-十六#函数的递归调用含义:在一个函数中直接或者间接调用了函数本身,称之为函数的递归调用//直接调用a()->a();//间接调用a()->b()->a();a()->b()->..->a();递归调用的本质:本是是一种循环结构,它不同于之前所学的while,do-while,for这......