当前位置: 首页 > news >正文

C语言指针详解(三)目录版

C语言指针详解(三)目录版

    • 1、字符指针变量
      • 1.1、字符指针变量的一般应用
      • 1.2、常量字符串
      • 1.3、常量字符串与普通字符串的区别
        • 1.3.1 常量字符串的不可修改性
        • 1.3.2 常量字符串的存储
    • 2、数组指针变量
      • 2.1、数组指针变量定义
      • 2.2、数组指针变量的初始化
    • 3、二维数组传参本质
    • 4、函数指针变量
      • 4.1、函数指针变量的创建
        • 4.1.1、验证函数地址的存在
        • 4.1.2、函数指针变量表达式
      • 4.2、函数指针变量的使用
      • 4.3、深入理解函数指针
        • 4.3.1、例题一
        • 4.3.2、例题二
        • 4.3.3、例题一解析
        • 4.3.4、例题二解析
        • 4.4、关键字 typedef 介绍
        • 4.4.1、typedef 表达式与实例演示
        • 4.4.2、define 与 typedef 的区别
    • 5、函数指针数组
    • 6、转移表

1、字符指针变量

我们知道若指针所指向的内容是字符,那么指针的类型就是字符指针类型 char* ,这个指针变量就是字符指针变量。

1.1、字符指针变量的一般应用

int main()
{char ch = 'a';char* pc = &ch;*pc = 'w';printf("%c", ch);return 0;
}

在这里插入图片描述

1.2、常量字符串

#include<stdio.h>
int main()
{const char* pt = "hello world.";//请思考这里是把一个字符串放到了 pt 指针当中了吗?printf("%s\n", pt);return 0;
}

如上面代码所示的 char* name = “字符串” 就是常量字符串的表达方式。那么我们思考一下注释中的问题:这里是把一个字符串放到了 pt 指针当中了吗?
答案是:否。毕竟前面是一个指针变量,其存储的应当为地址,故放入的是首字符的地址。因为字符是连续存放的,输出时系统会自动向后遍历,直至遇到 “/0”。
运行效果图:
在这里插入图片描述

1.3、常量字符串与普通字符串的区别

1.3.1 常量字符串的不可修改性

1.1 的代码和运行图中我们可见一般情况下的字符(字符串)是可以通过指针解引用来进行修改的。而常量字符串则不可修改,如下面代码示例演示

int main()
{char* pt = "hello world.";printf("%s\n", pt);*pt = "Hello World";return 0;
}

在这里插入图片描述
原因请见下文 1.3.2

1.3.2 常量字符串的存储

我们知道在存储位置中有栈区,堆区,静态区。其实在此之外还有一部分称为代码段。一般字符(字符串)存储在栈区中,而常量字符串则存储在代码段中。所以即使通过指针解引用也不能更改字符串内容。正因如此,当多个指针变量指向的常量字符串内容一致时,系统不会存储多份相同的常量字符串,而是会只存储一份,各个变量共用这一份常量字符串(即字符串地址相同)。为使各位对此更加清晰,我们通过以下代码和图解来进行进一步解析。

int main()
{char str1[] = "You are handsome !";char str2[] = "YOu are handsome !";char* str3 = "You are so beautiful";char* str4 = "You are so beautiful";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;
}

在这里插入图片描述
在这里插入图片描述

2、数组指针变量

2.1、数组指针变量定义

数组指针,顾名思义它是一种指针。我们已经熟知:
整型指针变量:int * p;存放的是整型变量的地址,指向整型数据的指针。
浮点型指针变量:float * p;存放的是浮点型变量的地址,指向浮点型数据的指针。
由上推知:数组指针变量应当存放的是数组的地址,指向数组的指针。
数组指针变量表达式:int (*p) [20];
在这个表达式中,p会先和*结合,表明 p 是一个指针变量,然后指针指向的是一个大小为20个整型变量的数组。
注意:因为 [ ] 的优先级要高于 * 号,所以必须加上 ()来保证 p 先和 * 结合

2.2、数组指针变量的初始化

既然数组指针变量存放的是数组的地址,那么我们就要将数组的地址给指针变量。数组地址:&数组名

int arr[10] = {0};//定义数组并初始化
int (*p) [10] = &arr;//将数组的地址赋值给指针变量 

在这里插入图片描述

3、二维数组传参本质

在数组章节中曾提及二维数组传参的如下写法:

#include <stdio.h>void print_arr(int arr[4][5], int a, int b)
{int i = 0;for (i = 0; i < a; i++){int j = 0;for (j = 0; j < b; j++){printf("%d", arr[i][j]);}printf("\n");}
}int main()
{int arr[4][5] = { {1,2,3,4,5},{2.3,4,5,6},{3,4,5,6,7},{4,5,6,7,8}};print_arr(arr, 4, 5);return 0;
}

在上述代码中,实参为二维数组,形参也为二维数组。那么下面来介绍另一种传参方式,使用数组指针进行传参。
二维数组在之前数组章节曾讲过,其可以看成每个元素是一维数组的数组,即二维数组的每个元素是一个一维数组,二维数组首元素就是第一行,是一个一维数组。
在这里插入图片描述
二维数组的数组名就是第一行的地址,是一个一维数组的地址,第一行一维数组类型为int [5],故其地址类型为int (*) [5];。如此也就证明二维数组传参实际上是传递了第一行一维数组的地址。由此可知,函数形参部分也可以写成指针形式。如下所示:

#include <stdio.h>void print_arr(int (*p) [5], int a, int b)//数组形参设置为数组指针
{int i = 0;for (i = 0; i < a; i++){int j = 0;for (j = 0; j < b; j++){printf("%d", *( * ( p + i ) + j ) );//深入理解系统对数组arr[ i ]的编译。// arr[ i ] = *(arr + i)}printf("\n");}
}int main()
{int arr[4][5] = { {1,2,3,4,5},{2.3,4,5,6},{3,4,5,6,7},{4,5,6,7,8} };print_arr(arr, 4, 5);return 0;
}

4、函数指针变量

4.1、函数指针变量的创建

根据上文内容,我们可以推断出函数指针变量是存储函数地址的变量,同时我们可以通过函数的地址来调用函数。

4.1.1、验证函数地址的存在

下面我们来验证函数地址的存在:

#include<stdio.h>void test()
{printf("hehe\n");
}int main()
{printf("test = %p\n", test);//函数名printf("&test = %p\n", &test);//取函数地址return 0;
}

在这里插入图片描述
由此说明函数存在地址,函数名与 &函数名 都可以得到函数的地址。

4.1.2、函数指针变量表达式

表达式如下:

int*pf)(int x , int y);
//或 int (*pf)(int , int)

在这里插入图片描述
去掉变量名即为其变量类型:

int*)(int x , int

4.2、函数指针变量的使用

在使用函数指针变量调用函数时,(*pf)()与 pf()两种方法都是可行的,因为二者表示的意思都一致,都是对应函数的地址。
代码演示:

#include<stdio.h>int div(int a , int b)
{return a / b;
}int main()
{int (*pf) (int, int) = div;printf("%d\n", (*pf)(4 , 2) );//调用方式一printf("%d\n", pf(4, 2));//调用方式二return 0;
}

在这里插入图片描述

4.3、深入理解函数指针

4.3.1、例题一
(*( void (*)() ) 0)();//请思考此行代码的意义

请思考上面代码的意义,解析见 4.3.3

4.3.2、例题二
void (*signal(int, void(*)(int)))(int);//请思考此行代码的意义

请思考上面代码得意义,解析见 4.3.4

4.3.3、例题一解析

在这里插入图片描述
入手关键点在 0 处,其前方应当为0的类型,( void (*) () ) 0 典型的强制类型转换,将0从整型 int 强制转换为函数指针类型 void ( * ) () ,从而 0 就成为一个函数指针变量,整体就是通过函数指针调用函数。

4.3.4、例题二解析

在这里插入图片描述
入手关键点在中间的函数名及其参数部分 signal(int, void(*)(int)) 我们可见在这一小部分中已经包含了signal函数的函数名和该函数内的两个参数的类型,但无参数名,根据C语言标准可知这应当是一个函数声明。故剩余部分为函数的返回类型,为 void (*) (int)函数指针类型。因为解引用操作符 * 后要跟名称,所以signal(int, void(*)(int))放在了 * 之后。综上,整体为一个函数声明

4.4、关键字 typedef 介绍
4.4.1、typedef 表达式与实例演示

typedef是用来类型的重命名的,可以简化复杂的类型,如 4.3.4 例题二中的函数指针类型。其重定义表达式如下:

typedef name1 name2;

name1 表示需要重定义类型名
name2 表示 name1 重定义后的类型名

具体演示如下:

#include<stdio.h>int div(int a , int b)
{return a / b;
}int main()
{typedef int(*son)(int, int);// * 后要跟名称,所以新的类型名要放在 * 后son pf = div;printf("%d\n", (*pf)(18 , 2) );printf("%d\n", pf(10, 2));return 0;
}

在这里插入图片描述

4.4.2、define 与 typedef 的区别

1、 本质区别:
#define 是预处理指令,用于文本替换。在编译之前,预处理器会直接将 #define 定义的宏替换成指定的文本。
typedef 是类型定义命令,用于为已存在的数据类型创建一个新的名字。
2.、作用范围:
#define 定义的宏没有作用域限制,一旦定义,就会一直有效,除非被 #undef 取消定义。
typedef 定义的类型有作用域限制,它遵循C语言的变量作用域规则。
3、 类型检查:
#define 不进行类型检查。它仅仅是在预处理阶段进行文本替换,所以不会检查替换后的类型是否正确。
typedef 会进行类型检查。当你使用 typedef 定义的新类型时,编译器会检查类型是否匹配。
4、 使用方式:
#define 可以用于定义常量、宏函数等,不仅仅限于类型。
typedef 仅用于定义类型的别名。
5、 内存分配:
#define 不会分配内存,因为它只是在预处理阶段进行文本替换。
typedef 本身也不分配内存,但它定义的类型在创建变量时会分配内存。

5、函数指针数组

我们知道数组是一个存储相同类型数据的存储空间。故函数指针数组就是一块连续的存储函数指针的空间。
表达式如下:

type (* name[number]();

name 会先和 [ ] 结合,表明 name 是数组,数组的内容是 type (*) () 类型的函数指针。(因为 * 后面要跟名称,所以 * 后面跟上数组名)

6、转移表

转移表是函数指针数组实例化的体现。
下面以简易计算器的改造为例:

//改造前的计算器
#include<stdio.h>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 = 1;int ret = 0;do{printf("**************\n");printf("1:add    2:sub\n");printf("3:mul    4:div\n");printf("0:exit        \n");printf("**************\n");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;
}

上述代码的条件分支中我们可以看到有许多重复的部分
在这里插入图片描述
这样就出现两个问题:

1、将这样重复的代码实现成函数。 2、这个函数又能完成不同的任务

如此我们有两种思路:

1、设计回调函数

2、引入函数指针数组来设计转移表。

在此我们以思路二进行改造(思路一请见《C语言指针详解(四)》)。首先我们要思考,设计转移表的话函数指针数组中的元素应当如何设置。我们一共自定义了 4 个函数,所以我们就把这四个函数的地址放入数组。具体演示如下:

#include<stdio.h>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 = 1;int ret = 0;int (*parr[5])(int , int) = { 0 , add , sub , mul , div };//转移表//之所以首元素为 0  是为了便于选择,使1-4都对应具体函数do{printf("**************\n");printf("1:add    2:sub\n");printf("3:mul    4:div\n");printf("0:exit        \n");printf("**************\n");printf("请选择: ");scanf("%d", &input);if (input <= 4 && input >= 1){printf("请输入操作数");scanf("%d %d", &x, &y);ret = (parr[input])(x, y);printf("ret = %d\n", ret);}else if (input == 0){printf("退出计算器\n");}else{printf("输入错误");}} while (input);return 0;
}

全文至此结束!!!
写作不易,不知各位老板能否给个一键三连或是一个免费的赞呢()(),这将是对我最大的肯定与支持!!!谢谢!!!()()

相关文章:

C语言指针详解(三)目录版

C语言指针详解&#xff08;三&#xff09;目录版 1、字符指针变量1.1、字符指针变量的一般应用1.2、常量字符串1.3、常量字符串与普通字符串的区别1.3.1 常量字符串的不可修改性1.3.2 常量字符串的存储 2、数组指针变量2.1、数组指针变量定义2.2、数组指针变量的初始化 3、二维…...

【AI资讯早报】AI科技前沿资讯概览:2024年8月6日早报

【AI资讯早报&#xff0c;感知未来】AI科技前沿资讯概览&#xff0c;涵盖了行业大会、技术创新、应用场景、行业动态等多个方面&#xff0c;全面展现了AI领域的最新发展动态和未来趋势。 1.【图像生成技术再突破】Midjourney V6.1震撼发布&#xff0c;人像生成质量跃上新台阶 …...

等保测评中的密码技术与密钥管理

在信息安全领域&#xff0c;等保测评&#xff08;信息安全等级保护测评&#xff09;是一项重要的安全评估活动&#xff0c;旨在评估信息系统的安全性&#xff0c;并根据评估结果给予相应的安全等级。这一过程中&#xff0c;密码技术与密钥管理发挥着至关重要的作用。本文将详细…...

go语言flag库学习

文章目录 flag基本创建使用方法正常声明全局变量指针短写 flag 基本创建使用方法 func String(name string, value string, usage string) *string func StringVar(p *string, name string, value string, usage string) 正常声明全局变量 package mainimport ("flag…...

2024年必备技能:智联招聘岗位信息采集技巧全解析

随着大数据时代的发展&#xff0c;精准定位职业机会成为程序员求职的关键。本文将深入解析如何利用Python高效采集智联招聘上的岗位信息&#xff0c;助你在2024年的职场竞争中脱颖而出。通过实战代码示例&#xff0c;揭示网络爬虫背后的秘密&#xff0c;让你轻松掌握这一必备技…...

《机器学习by周志华》学习笔记-决策树-02

1、剪枝处理(Pruning) 1.1、背景概念 上文「决策树01」的学习中,我们了解了著名的3种决策树算法ID3、C4.5、CART。这3种决策树算法最根本的核心就是根据特征选择离散属性作为节点来搭建树结构,运用搭好的结构进行推理。 剪枝(pruning)则就是将搭好的决策树去掉一些「非叶节…...

centos Python3.6升级3.8

CentOS系统上升级Python3.6到3.8版本。 步骤 1. 更新系统 在开始升级Python之前&#xff0c;首先需要确保系统是最新的。可以使用以下命令更新CentOS系统&#xff1a; sudo yum update 2. 安装依赖项 升级Python之前&#xff0c;需要安装一些依赖项。运行以下命令安装这些依赖…...

文章解读与仿真程序复现思路——电网技术EI\CSCD\北大核心《基于竞价空间预测的虚拟电厂日前竞价策略》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…...

Simulink模型开发中的一些自动化方法

随着Simulink模型的产品化开发进程&#xff0c;许多模型开发人员会关心模型的建模自动化问题。比如如何对模型中的元素进行批量查找和修改&#xff1b;如何构建自己的建模规则对模型进行检查&#xff1b;如何实现测试自动化等。在这些使用场景中我们都需要了解一些Simulink函数…...

RabbitMQ消费者消费消息失败处理

在我们开发分布式系统的过程中&#xff0c;RabbitMQ这样的消息队列无疑是实现微服务间通信的利器。然而&#xff0c;消息处理失败在所难免。当我们面临消费消息失败的情况时&#xff0c;该如何应对呢&#xff1f;在这篇博客中&#xff0c;我将带你深入探讨RabbitMQ消费者的消息…...

Apache Kylin分布式的分析数据仓库

Apache Kylin 是一个分布式的分析数据仓库&#xff0c;用于大数据上的超快在线分析处理 (OLAP)。它能够在大规模数据集上提供亚秒级的查询响应时间&#xff0c;并支持标准的 ANSI SQL 查询接口。Kylin 最初由 eBay 开发&#xff0c;后来捐赠给 Apache 软件基金会&#xff0c;现…...

informer中DeltaFIFO机制的实现分析与源码解读

informer中的DeltaFIFO机制的实现分析与源码解读 DeltaFIFO作为informer中重要组件&#xff0c;本文从源码层面了解是如何DelatFIFO是实现的。 DeltaFIFO的定义 找到delta_fifo.go的源码&#xff0c;位于client-go/tools/cache/delta_fifo.go 代码结构大致如下: store定义…...

树莓派下,centos7amr64下,搭建目标检测开发环境,java语言

在树莓派(Raspberry Pi)上使用CentOS 7 ARM64搭建基于Java的目标检测开发环境,可以按照以下步骤进行。需要注意的是,CentOS 7 ARM64的官方镜像可能不支持树莓派的某些硬件,因此你可能需要寻找第三方镜像或进行一些额外的配置。 1. 安装CentOS 7 ARM64 首先,确保你已经正…...

SpringBoot+Redis 发布与订阅

两个应用都引入 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artif…...

huggingface无法访问怎么办?一招教你解决,使用hf-mirror.com镜像站快速下载各种大模型

huggingface.co无法访问怎么办&#xff1f; 请访问 https://hf-mirror.com/ hf-mirror.com是一个旨在为中国国内的人工智能开发者提供更快更稳定下载服务的镜像站点&#xff0c;它镜像了Hugging Face的huggingface.co网站上的模型和数据集资源。由于网络环境和地理限制&#xf…...

如何用密码保护你的 WordPress 管理员 (wp-admin) 目录

在维护 WordPress 网站时&#xff0c;确保 wp-admin 目录安全是非常重要的。为该目录添加密码保护可以有效提高网站安全性&#xff0c;防止未经授权的访问。这篇文章将介绍实现这一目标的两种方法。 1.为什么要为 wp-admin 目录添加密码保护 WordPress 管理员后台是网站的核心…...

java 程序包org.junit.jupiter.api不存在

问题描述 正常启动springboot报错java 程序包org.junit.jupiter.api不存在。 问题分析 pom文件中缺少junit依赖&#xff0c;但是添加后问题仍然没解决&#xff0c;后面把test部分删掉解决问题。 解决方法 添加junit依赖 <dependency><groupId>junit</group…...

简单的docker学习 第4章 docker容器

第4章 Docker容器 4.1 容器基础 4.1.1 容器启动流程 通过 docker run 命令可以启动运行一个容器。该命令在执行时首先会在本地查找指定的镜像&#xff0c;如果找到了&#xff0c;则直接启动&#xff0c;否则会到镜像中心查找。如果镜像中心存在该镜像&#xff0c;则会下载到…...

零基础入门转录组数据分析——机器学习算法之SVM-RFE(筛选特征基因)

零基础入门转录组数据分析——机器学习算法之SVM-RFE&#xff08;筛选特征基因&#xff09; 目录 零基础入门转录组数据分析——机器学习算法之SVM-RFE&#xff08;筛选特征基因&#xff09;1. SVM-RFE基础知识2. SVM-RFE&#xff08;Rstudio&#xff09;——代码实操2. 1 数据…...

Python酷库之旅-第三方库Pandas(067)

目录 一、用法精讲 266、pandas.Series.dt.second属性 266-1、语法 266-2、参数 266-3、功能 266-4、返回值 266-5、说明 266-6、用法 266-6-1、数据准备 266-6-2、代码示例 266-6-3、结果输出 267、pandas.Series.dt.microsecond属性 267-1、语法 267-2、参数 …...

Spring快速学习

目录 IOC控制反转 引言 IOC案例 Bean的作用范围 Bean的实例化 bean生命周期 DI 依赖注入 setter注入 构造器注入 自动装配 自动装配的方式 注意事项; 集合注入 核心容器 容器的创建方式 Bean的三种获取方式 Bean和依赖注入相关总结 IOC/DI注解开发 注解开发…...

【Web开发手礼】探索Web开发的秘密(十五)-Vue2(2)AJAX、前后端分离、前端工程化

主要介绍了AJAX、前后端分离所需的YApi、前端工程化所需要的环境安装&#xff01;&#xff01;&#xff01; 目录 前言 AJAX ​原生Ajax Axios Axios入门 案例 前后端分离开发 YApi ​前端工程化 环境准备 总结 前言 主要介绍了AJAX、前后端分离所需的YApi、前端工…...

Phalco安装过程以及踩的一些坑(mac环境)

一 背景 公司用Phalcon框架好长时间了,中途发现了一些Phalcon使用的上的问题,于是想在本地搭建一套Phalcon的环境,方便排查问题使用。 二 Mac系统下的安装 看了很多说法,最终发现还是官网给力,安装Phalcon使用下列命令即可(前提条件是PHP已安装好,工具pecl也安装好了):…...

Ubuntu修改双系统默认启动顺序

1.打开grub的默认启动配置文件 sudo gedit /etc/default/grub# If you change this file, run update-grub afterwards to update # /boot/grub/grub.cfg. # For full documentation of the options in this file, see: # info -f grub -n Simple configurationGRUB_DEFAULT…...

高仲富:49岁搞AI,白天种菜卖菜,晚上学数学搞程序

这是《开发者说》的第13期&#xff0c;本期我们邀请的开发者是高仲富&#xff0c;曾是一位数学老师&#xff0c;自学成为一名程序员&#xff0c;在北京漂过&#xff0c;后逃回了成都&#xff0c;一边与病魔抗争&#xff0c;一边写代码&#xff0c;一写就是15年&#xff0c;制作…...

光线追踪(纹理映射)

最近在跟着ray trace in one week来学习光线追踪&#xff08;很多概念茅塞顿开&#xff09;做到一半想着记录一下&#xff08;比较随心&#xff09;上面是之前的效果。ray trace in one week Texture Coordinates for Spheres&#xff08;球体纹理坐标&#xff09; u, v 纹理…...

传统产品经理VS现在AI产品经理,你要学习的太多了,超详细收藏我这一篇就够了

传统产品经理想要转行成为AI产品经理&#xff0c;需要经历一系列的学习和实践过程。下面是一份详细的学习路线图&#xff0c;旨在帮助你顺利转型。 学习路线图 了解AI基础知识 AI概览&#xff1a;阅读《人工智能&#xff1a;一种现代的方法》这样的书籍&#xff0c;以获得对AI…...

C#使用Socket实现TCP服务器端

1、TCP服务器实现代码 using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks;namespace PtLib.TcpServer {public delegate void Tcp…...

MTK联发科MT8766/MT8166安卓核心板性能参数对

MT8766核心板 采用联发科四核2G主频芯片方案&#xff0c;国内4G全网通。12nm先进工艺&#xff0c;支持 Android9.0系统。 GPU 采用超强 IMG GE8300 ,主频600MHz。支持高速LPDDR4/X&#xff0c;主频高达1600MHz。支持EMMC5.1。标配 WIFI 802.11 ac/abgn&#xff0c;BT 5.0。 支持…...

ps绘制动图

ps绘制动图教程&#xff08;简易版&#xff09;-直播gif动态效果图 第一步 打开ps绘制几个简单的长方形 第二步 将图层转化为智能图层 第三部 在窗口找到时间轴创建时间轴 第五步 通过变换来鼠标控制图像的变化并打下结束点 第六部 通过图像中的图像大小控制gif的大小 第七部 …...