暂时未有相关云产品技术能力~
目录一、进程间通信介绍1.1 进程间通信概念1.2 为什么要有进程间通信1.3 进程间通信目的1.4 进程间通信分类1.5 进程间通信的本质二、管道2.1 什么是管道2.2 匿名管道2.2.1 pipe函数2.2.2 匿名管道的原理2.2.3 匿名管道的使用2.2.4 以文件描述符的角度看待2.2.5 匿名管道测试代码2.2.6 匿名管道读写规则2.2.7 匿名管道的特征2.2.8 基于匿名管道的进程池2.3 命名管道2.3.1 使用命令创建命名管道 2.3.2 命名管道的原理2.3.3 在程序中创建命名管道2.3.4 unlink函数2.3.5 使用命名管道实现serve&client通信2.3.6 匿名管道与命名管道的区别一、进程间通信介绍1.1 进程间通信概念 进程间通信就是在不同进程之间传播或交换信息,进程间通信简称IPC(Interprocess communication)1.2 为什么要有进程间通信为什么要有进程间通信??有时候我们是需要多进程协同的,去完成某种业务1.3 进程间通信目的数据传输:一个进程需要将它的数据发送给另一个进程资源共享:多个进程之间共享同样的资源通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变1.4 进程间通信分类(1)管道匿名管道命名管道(2)System V IPCSystem V 消息队列System V 共享内存System V 信号量(3)POSIX IPC消息队列共享内存信号量互斥量条件变量读写锁管道:管道是基于文件系统的,System V IPC:聚焦在本地通信,POSIX IPC:让通信可以跨主机1.5 进程间通信的本质进程间通信的本质就是:让不同的进程看到同一份资源两个进程间想要通信,就必须提供某一个资源,这个资源用于给两个进程之间进行通信。这个资源不能是进程的双方提供的,因为进程是具有独立性的,一个进程提供了资源,进行通信另一个进程必定会访问这个资源,这时就破坏了进程的独立性因此,这个资源只能由第三方提供,这个第三方就是OS,OS需要直接或间接给通信双方的进程提供 “内存空间”这个资源可以是OS中不同的模块提供,不同的模块提供的不同资源,造就了不同的通信种类(消息队列,共享内存,信号量...),因此出现了不同的通信方式所以,进程间想要通信,首先要看到同一份资源,看到同一份资源才会有通信二、管道2.1 什么是管道管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的一个数据流称为一个 “管道”比如,我们执行的这条 cat file | grep hello 命令,其中 “|” 就是管道其中,cat 命令和 grep 命令都是两个程序,当它们运行起来后就变成了两个进程,cat进程的数据传输到 “管道” 当中,grep进程再通过 “管道” 当中读取数据,至此便完成了数据的传输,两个进程就完成了通信管道又分匿名管道和命名管道2.2 匿名管道匿名管道用于进程间通信,且仅限于本地父子进程之间通信2.2.1 pipe函数pipe函数用于创建匿名管道,man查看pipe,pipe函数是一个系统调用man 2 pipe pipe 头文件:#include <unistd.h> 函数原型 int pipe(int pipefd[2]); 返回值 成功时返回0,调用失败时返回-1且错误码被设置 pipe函数的参数 pipefd[2] 是一个输出型参数,数组pipefd 用于返回两个指向管道读端和写端的文件描述符pipe函数的参数 pipefd[2] 是一个输出型参数,数组pipefd 用于返回两个指向管道读端和写端的文件描述符pipefd[0]是管道读端的文件描述符pipefd[1]是管道写端的文件描述符帮助记忆:0可以想象成嘴(读),1可以想象成笔(写) 因为匿名管道仅用于父子进程间通信,所以要使用匿名管道就要使用 fork函数2.2.2 匿名管道的原理匿名管道用于进程间通信,且仅限于本地父子进程之间通信进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现父子进程间通信的原理就是,让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信该文件资源是文件系统提供的,该文件资源就是匿名管道,该文件资源的操作方法与文件一致,也有自己的文件缓冲区注意:父子进程对该文件进行写入操作时,该文件缓冲区当中的数据不会发生写时拷贝,该文件资源由文件系统维护2.2.3 匿名管道的使用管道只能单向通信,不能双向通信。比如,一端是写入了,另一端就必须是读取,反过来也是,一端进行读取,另一端必须进行写入(1)父进程调用pipe函数创建管道(2)父进程进行创建子进程(3)父进程需要读取,父进程就需要关闭写端,子进程进行写入,子进程需要关闭读端(4)父进程需要写入,父进程就需要关闭读端,子进程进行读取,子进程需要关写读端注意:管道是单向通信的2.2.4 以文件描述符的角度看待站在文件描述符的角度看待匿名管道:(1)父进程调用pipe函数创建管道(2)父进程进行创建子进程(3)父进程需要读取,父进程就需要关闭写端,子进程进行写入,子进程需要关闭读端(4)父进程需要写入,父进程就需要关闭读端,子进程进行读取,子进程需要关写读端2.2.5 匿名管道测试代码以子进程写入,父进程读取为例#include <iostream> #include <unistd.h> #include <cstdio> #include <cassert> #include <cstring> #include <sys/types.h> #include <sys/wait.h> using namespace std; //子进程写入,父进程读取 int main() { // 第一步:创建管道文件,打开读写端 int fds[2]; int n = pipe(fds); assert(n == 0);//否则创建管道失败,直接断言 //创建子进程 pid_t id = fork(); assert(id >= 0);//否则创建子进程失败 //子进程通信代码--子进程写入 if(id == 0) { //关闭读端,写端打开 close(fds[0]); const char* s = "我是子进程,我正在给你发消息"; int cnt = 0; while(true) { ++cnt; char buffer[1024];//只能在子进程看到 snprintf(buffer, sizeof buffer, "child -> parent say: %s[%d][子进程pid:%d]", s, cnt, getpid()); write(fds[1], buffer, strlen(buffer)); sleep(3); if(cnt >= 10) break; } close(fds[1]); cout << "子进程关闭自己的写端" << endl; exit(0); } //父进程通信代码--父进程读取 close(fds[1]); while(true) { sleep(1); char buffer[1024]; ssize_t s = read(fds[0], buffer, sizeof(buffer)-1); if(s > 0)//读取到数据 { buffer[s] = '\0';//防止越界 cout << "Get Message# " << buffer << " | 父进程pid: " << getpid() << endl; } else if(s == 0) //读到文件结尾 { cout << "父进程读取完成" << endl; break; } } close(fds[0]); cout << "父进程的读端关闭" << endl; //等待子进程 int status = 0; n = waitpid(id, &status, 0); cout << "等待子进程pid->" << n << " : 退出信号:" << (status & 0x7F) << endl; return 0; }运行结果2.2.6 匿名管道读写规则读快,写慢。如果管道中没有数据,读端进程再进行读取,会阻塞当前正在读取的进程;如果写端不进行写入,读端进程会一直阻塞;读慢,写快。如果写端把管道写满了,再写就会对该进程进行阻塞,需要等待对方对管道内数据进行读取;如果读端不读取数据,写端进程会一直阻塞;写关闭,读取到0。如果写入进程关闭了写入fd,读取端将管道内的数据读完后,程序结束读关闭,写?如果读关闭,操作系统会给写端发送13号信号SIGPIPE,终止写端。(1)读快,写慢上面代码是读快,写慢这种情况(2)读慢,写快修改代码,修改sleep时间即可运行结果(3)写关闭,读取到0 写入一条消息,直接关闭写端运行结果(4)读关闭,写?读一次,直接把读端关闭 运行结果2.2.7 匿名管道的特征只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道管道提供流式服务(网络)一般而言,进程退出,管道释放,所以管道的生命周期随进程一般而言,内核会对管道操作进行同步与互斥(多线程)管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道2.2.8 基于匿名管道的进程池实现思路:父进程控制写端进行写入,子进程进行读取,读取命令码后执行相应的任务,父进程创建多个子进程代码: #include <iostream> #include <string> #include <vector> #include <ctime> #include <cassert> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> using namespace std; #define makeSeed() srand((unsigned long)time(nullptr) ^ getpid() ^ 0x363 ^ rand() % 1234) #define PROCESS_SUM 10 typedef void (*func_t)();//函数指针类型 //-------------------------------- 模拟一下子进程要完成的某种任务 --------------------- void downloadTask() { cout << getpid() << "执行下载任务\n" << endl; sleep(1); } void ioTask() { cout << getpid() << "执行io任务\n" << endl; sleep(1); } void flushTask() { cout << getpid() << "执行刷新任务\n" << endl; sleep(1); } void loadTaskFunc(vector<func_t>* out) { assert(out); out->push_back(downloadTask); out->push_back(ioTask); out->push_back(flushTask); } //-------------------------------- 以下代码是多进程代码 --------------------- class subEP //sub end point { public: subEP(pid_t subId, int writeFd) :_subId(subId) ,_writeFd(writeFd) { char nameBuffer[1024]; snprintf(nameBuffer, sizeof(nameBuffer), "preocess - %d [pid(%d) - fd(%d)]", _num++, _subId, _writeFd); _name = nameBuffer; } public: static int _num; string _name; pid_t _subId; int _writeFd; }; int subEP::_num = 0; int recvTask(int readFd) { int code = 0; ssize_t s = read(readFd, &code, sizeof code); if(s == sizeof(code))//读取正常 { return code; } else if(s <= 0)//读取出错 { return -1; } else { return 0; } } void createSubProcess(vector<subEP>* subs, vector<func_t>& funcMap) { //vector<int> deleteFd;//第一种方法:解决下一个子进程拷贝父进程读写端的问题 for(int i = 0; i < PROCESS_SUM; i++) { int fds[2]; int n = pipe(fds); assert(n == 0); (void)n; pid_t id = fork(); //子进程 if(id == 0) { // for(int i = 0; i < deleteFd.size(); i++) // close(deleteFd[i]); close(fds[1]); while(true) { //1.获取父进程发送的命令码,没有收到命令码,进行阻塞等待 int commandCode = recvTask(fds[0]); //2.执行任务 if(commandCode >= 0 && commandCode < funcMap.size()) { funcMap[commandCode](); } else if(commandCode == -1)//读取失败返回-1 { break; } } //子进程退出 exit(0); } //父进程 close(fds[0]); subEP sub(id, fds[1]); subs->push_back(sub);// //deleteFd.push_back(fds[1]); } } void sendTask(const subEP& process, int taskNum) { cout << "send tak num: " << taskNum << " send to -> " << process._name << endl; int n = write(process._writeFd, &taskNum, sizeof(taskNum)); assert(n == sizeof(int)); (void)n; } void loadBlanceContrl(vector<subEP>& subs, vector<func_t>& funcMap, int count) { int processSum = subs.size(); int taskSum = funcMap.size(); bool forever = (count == 0 ? true : false); while(true) { // 1. 随机选择一个子进程 int subIdx = rand() % processSum; // 2. 随机选择一个任务 int taskIdx = rand() % taskSum; // 3. 任务发送给选择的进程 sendTask(subs[subIdx], taskIdx); sleep(1); if(!forever) { count--; if(count == 0) break; } } //第二种方法:解决下一个子进程拷贝父进程读写端的问题 //写端退出,关闭读 for(int i = 0; i < processSum; i++) { close(subs[i]._writeFd); } } void waitProcess(vector<subEP> process) { int processSum = process.size(); for(int i = 0; i < processSum; i++) { waitpid(process[i]._subId, nullptr, 0); cout << "wait sub process success ..." << process[i]._subId << endl; } } int main() { //创建随机数 makeSeed(); // 1.建立子进程并建立和子进程通信的信道 // 1.1 加载方法任务表 vector<func_t> funcMap; loadTaskFunc(&funcMap); // 1.2 创建子进程,并且维护父子通信信道 vector<subEP> subs; createSubProcess(&subs, funcMap); // 2.父进程,控制子进程,负载均衡的向子进程发送命令码 int taskCnt = 5;//执行任务次数,为0时永远执行任务 loadBlanceContrl(subs, funcMap, taskCnt); // 3.回收子进程 waitProcess(subs); return 0; }运行结果小提示:以 .cpp .cxx .cc 结尾的都是C++的源文件 2.3 命名管道 匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间的通信,通常,一个管道由一个进程创建,然后该进程调用fork创建子进程,父子进程通过匿名管道进行通信。如果要实现两个毫不相关进程之间的通信,可以使用命名管道来做到2.3.1 使用命令创建命名管道使用 mkfifo 命令创建一个命名管道mkfifo 文件名 ps: mkfifo named_pipe可以看到,创建出来的文件的类型是 p ,代表该文件是命名管道文件命名管道也有自己的 inode,说明命名管道就是一个独立的文件使用这个命名管道文件,就能实现两个进程之间的通信了。我们在一个进程(进程A)中用 shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用 cat命令从命名管道当中进行读取现象:当进程A启动后,进程B会每秒从命名管道中读取一个字符串打印到显示器上这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信先测试往显示器上打印(shell脚本语言)cnt=0; while :; do echo "hello world -> $cnt"; let cnt++; sleep 2; done运行结果 输出重定向到管道里cnt=0; while :; do echo "hello world -> $cnt"; let cnt++; sleep 2; done > named_pipe注:脚本语言是一个进程,cat也是一个进程,两个进程毫无关系cat 进行输入重定向 ,向管道 named_pipe 读取数据cat < named_pipe运行结果之前我们说过,当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉,在这里就可以很好的得到验证:当我们终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时 bash 就会被操作系统杀掉,我们的云服务器也就退出了注意:命名管道的大小是不会改变的,都为0,因为数据都是在文件缓冲区 2.3.2 命名管道的原理命令管道用于实现两个毫不相关进程之间的通信进程间通信的本质就是,让不同的进程看到同一份资源,使用命令管道实现父子进程间通信的原理是:也是让两个父子进程先看到同一份被打开的文件资源,这个文件资源就是我们创建的命名管道两个毫不相关进程打开了同一个命名管道,此时这两个进程也就看到了同一份资源,进而就可以进行通信了,通信的数据依旧是在文件缓冲区里面,并且不会刷新到磁盘命名管道可以通过路径+名字标定唯一性,匿名管道是通过地址来标定唯一性的,这个地址没有名字,所以叫匿名管道2.3.3 在程序中创建命名管道在程序中创建命名管道使用也是使用 mkfifo,mkfifo 是命令,也是一个函数man 3 mkfifo 查看一下mkfifo函数的函数原型如下:int mkfifo(const char *pathname, mode_t mode);解释:头文件: #include <sys/types.h> #include <sys/stat.h> 声明: int mkfifo(const char *pathname, mode_t mode); 参数: (1)pathname mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件 注意: 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下 (2)mode mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限 返回值: 命名管道创建成功,返回0 命名管道创建失败,返回-1,错误码被设置注意:若想创建出来命名管道文件的权限值不受影响,则需要在创建文件前使用 umask 函数将文件默认掩码设置为0代码示例:#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #define FILE_NAME "named_pipe" int main() { umask(0); //将文件默认掩码设置为0 //使用mkfifo创建命名管道文件 int n = mkfifo(FILE_NAME, 0666); if (n < 0) { perror("mkfifo"); return -1; } return 0; }运行结果2.3.4 unlink函数上面的程序再次运行就会报错这是因为 mkfifo 函数创建管道是,如果管道已经存在,就不会创建,直接报错:文件已经存在如果我们想让程序运行结束,创建的管道也被删除,就要使用 unlink函数man 3 unlink 查看一下unlink 头文件: #include <unistd.h> 函数声明: int unlink(const char *path); 参数: 传入要被删除文件的名字 返回值: 删除成功返回 0 失败返回 -1 ,错误码被设置#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #define FILE_NAME "named_pipe" int main() { umask(0); //将文件默认掩码设置为0 //使用mkfifo创建命名管道文件 int n = mkfifo(FILE_NAME, 0666); if (n < 0) { perror("mkfifo"); return -1; } //删除管道文件 n = unlink(FILE_NAME); if(n < 0) { perror("unlink"); return -1; } else { printf("管道文件删除成功\n"); } return 0; }运行结果小提示:assert不用乱使用,意料之中使用assert,意料之外使用if判断 2.3.5 使用命名管道实现serve&client通信实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了共同的头文件:comm.hpp客户端和服务端共用一个头文件#pragma once #include <iostream> #include <string> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <fcntl.h> #include <cassert> #include <cstring> using namespace std; #define NAMED_PIPE "named_pipe" //创建命名管道 bool createFifo(const string& path) { umask(0); int n = mkfifo(path.c_str(), 0600); if(n == 0)//创建成功 { return true; } else//创建失败 { cout << "errno: " << "errno string: " << strerror(errno) << endl; return false; } } //删除命名管道 void removeFifo(const string& path) { int n = unlink(path.c_str()); assert(n == 0);//release下就没有了 (void)n; } 服务端的代码如下:(server.cc)#include "comm.hpp" int main() { //创建命名管道 bool r = createFifo(NAMED_PIPE); assert(r); (void)r; cout << "server begin" << endl; int rfd = open(NAMED_PIPE, O_RDONLY);//打开命名管道,服务端以读方式打开 if(rfd < 0) exit(-1); //read char buffer[1024]; while(true) { ssize_t s = read(rfd, buffer, sizeof(buffer) - 1); if(s > 0)//读取正常 { buffer[s] = '\0'; cout << "client -> server# " << buffer << endl; } else if(s == 0)//client退出,server也退出 { cout << "client quit, me too!" << endl; break; } else//读取错误 { cout << "error string: " << strerror(errno) << endl; break; } } //关闭文件描述符 close(rfd); //程序退出删除命名管道 removeFifo(NAMED_PIPE); cout << "server end" << endl; return 0; }服务端代码:(client.cc)#include "comm.hpp" int main() { cout << "client begin" << endl; int wfd = open(NAMED_PIPE, O_WRONLY);//打开命名管道,客户端以写的方式打开 if(wfd < 0) exit(-1); //write char buffer[1024]; while(true) { cout << "Please Say# "; fgets(buffer, sizeof(buffer), stdin);//输入信息 if(strlen(buffer) > 0) buffer[strlen(buffer) - 1] = 0;//去掉输入多余的 \n ssize_t n = write(wfd, buffer, strlen(buffer)); assert(n == strlen(buffer)); (void)n; } close(wfd); cout << "client end" << endl; return 0; }运行的时候,服务端先运行,然后客户端再运行,客户端不输入数据,服务端会一直阻塞等待2.3.6 匿名管道与命名管道的区别匿名管道由pipe函数创建并打开。命名管道由mkfifo函数创建,打开用openFIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义----------------我是分割线---------------文章暂时到这里就结束了,下一篇即将更新
目录一、指针为何物1.1 指针的概念1.2 指针的背后二、指针和指针类型2.1 指针的类型2.2 指针类型的意义 2.2.1 指针 +- 整数(加减)2.2.2 指针的解引用三、野指针3.1 野指针成因3.2 如何规避野指针四、指针运算4.1 指针+-整数4.2 指针-指针五、指针和数组六、二级指针七、指针数组一、指针为何物1.1 指针的概念 在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”,意思是通过它能找到以它为地址的内存单元 假设内存块为一栋大楼,每个内存单元为一间房子,每个指针就好比每间房子的门牌号,我们通过门牌号找到相应的房子,同理,我们可以通过指针找到相应的内存单元指针变量我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个变量就是指针变量指针理解的2个要点:指针是内存中一个最小单元的编号,也就是地址平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量总结:指针就是地址,口语中说的指针通常指的是指针变量例如:#include<stdio.h> int main() { int a = 5;//在内存中开辟一块空间用来存放a int* p = &a; //这里我们对变量a,取出它的地址,可以使用& 操作符。 //p 拿到了 a 的地址,把 a 的地址存放在变量 p 中,p就是一个指针变量 //对 p 进行解引用操作(也就是 *p)就能找到 a 的地址,相当于拿到了a printf("%d\n", a); printf("%d\n", *p); *p = 10;//对指针变量 p 进行解引用拿到 a 的地址,并对 a 的值进行了间接修改 printf("%d\n", a); printf("%d\n", *p); return 0; }运行结果在调试模式下查看所以说 *p 就相当于 a总结:指针变量,用来存放地址的变量(存放在指针中的值都被当成地址处理)----------------我是分割线--------------- 1.2 指针的背后一个小的单元到底是多大?答:1个字节地址如何编址?经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0)那么32根地址线产生的地址就会是:00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 00000000 00000000 00000000 00000002 ... ... ... 11111111 11111111 11111111 11111110 11111111 11111111 11111111 11111111这里就有 2 的 32 次方个地址 每个地址标识一个字节,那我们就可以给 4G 的空闲进行编址,(2^32Byte == 2^32/1024KB == 2^32/1024/1024MB == 2^32/1024/1024/1024GB == 4GB)同样的方法,64位机器,如果给64根地址线,那能编址多大空间,自己计算这里我们就明白: 在32位的机器上,地址是 32 个 0 或者 1 组成二进制序列,那地址就得用 4 个字节的空间来存储,所以一个指针变量的大小就应该是 4 个字节 那如果在 64 位机器上,如果有 64 个地址线,那一个指针变量的大小是 8 个字节,才能存放一个地址 总结:指针是用来存放地址的,地址是唯一标示一块地址空间的指针的大小在32位平台是4个字节,在64位平台是8个字节 ----------------我是分割线--------------- 二、指针和指针类型2.1 指针的类型 这里我们在讨论一下:指针的类型,我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢?准确的说:有的当有这样的代码:1. int num = 10; 2. p = &num;要将&num(num的地址)保存到p中,我们知道 p就是一个指针变量,那它的类型是怎样的呢我们给指针变量相应的类型char *pc = NULL; int *pi = NULL; short *ps = NULL; long *pl = NULL; float *pf = NULL; double *pd = NULL;这里可以看到,指针的定义方式是: type + *其实:char* 类型的指针是为了存放 char类型变量的地址short* 类型的指针是为了存放 short 类型变量的地址int* 类型的指针是为了存放 int 类型变量的地址 简单说,指针类型决定了指针在被解引用的时候访问几个字节如果是int*的指针,解引用访问 4个字节,如果是char*的指针,解引用访问 1个字节,依次推广到其他类型 ----------------我是分割线---------------2.2 指针类型的意义 2.2.1 指针 +- 整数(加减)#include <stdio.h> int main() { int n = 10; char* pc = (char*)&n;//强制转换成 char 类型 int* pi = &n; printf("%p\n", &n); printf("%p\n", pc); printf("%p\n", pc + 1); printf("%p\n", pi); printf("%p\n", pi + 1); return 0; }运行结果char* 类型的指针 pc+1 结果跳过了一个字节int* 类型的 指针 pi+1 结果跳过了四个字节(环境为 x86)这说明了 指针的类型决定了指针向前或者向后走一步有多大(距离)2.2.2 指针的解引用测试代码:#include <stdio.h> int main() { int n = 0x11223344; char* pc = (char*)&n; int* pi = &n; *pc = 0; //重点在调试的过程中观察内存的变化。 *pi = 0; //重点在调试的过程中观察内存的变化。 return 0; }在调试下查看*pc 赋值为 0 之后,n 中的值只有一个字节被改成了 0 *pi 赋值为 0 之后,n 中的所有字节均被改成了 0这说明了:指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节指针类型的意义总结:1、指针的类型决定了指针向前或者向后走一步有多大(距离)2、指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)---------------我是分割线---------------三、野指针野指针概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)3.1 野指针成因(a)指针未初始化#include <stdio.h> int main() { int* p;//局部变量指针未初始化,默认为随机值 *p = 20; return 0; }运行结果,编译器无法编译(b) 指针越界访问#include <stdio.h> int main() { int arr[10] = { 0 }; int* p = arr; int i = 0; for (i = 0; i <= 11; i++) { //当指针指向的范围超出数组arr的范围时,p就是野指针 *(p++) = i; } return 0; }运行结果(c)指针指向的空间释放动态内存开辟的时候讲解,这里可以简单提示一下3.2 如何规避野指针指针初始化小心指针越界指针指向空间释放即使置NULL避免返回局部变量的地址指针使用之前检查有效性 ----------------我是分割线---------------四、指针运算4.1 指针+-整数上面指针类型的意义里面已经讲过了,这不不谈了4.2 指针-指针测试代码#include<stdio.h> int my_strlen(char* s) { char* p = s; while (*p != '\0') p++; return p - s; } int main() { char s[] = "abcdef"; int ret = my_strlen(s); printf("%d\n", ret); return 0; }运行结果结果说明了指针 - 指针得到的是指针和指针之间的元素个数,但是要注意指针 - 指针的前提是,两个指针必须指向同一块空间还有一点要注意:指针 +指针没有意义 总结一下:指针和指针进行加减:两个指针不能进行加法运算,这是非法操作,因为进行加法后,得到的结果指向一个不知所向的地方,,而且毫无意义两个指针可以进行减法操作,但必须类型相同,一般用在数组方面标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较五、指针和数组 数组,是一块连续的空间,里面存放的是相同类型的元素,存储的方式是先使用低地址的空间,再使用高地址的空间,数组名表示的是数组首元素的地址(两种情况除外,后面讲)指针(变量)--- 是一个变量,存放的是地址这样写代码是可行的1. int arr[10] = {1,2,3,4,5,6,7,8,9,0}; 2. int *p = arr;//p存放的是数组首元素的地址 数组名当成地址存放到一个指针中,我们就使用指针来访问数组例如:#include <stdio.h> int main() { int i = 0; int arr[] = { 1,2,3,4,5,6,7,8,9,0 }; int* p = arr; //指针存放数组首元素的地址 int sz = sizeof(arr) / sizeof(arr[0]); for (i = 0; i < sz; i++) { printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p + i); } return 0; }运行结果p+i 其实计算的是数组 arr 下标为 i的地址,我们就可以直接通过指针来访问数组再如,使用指针打印数组#include <stdio.h> int main() { int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; int* p = arr; //指针存放数组首元素的地址 int sz = sizeof(arr) / sizeof(arr[0]); int i = 0; for (i = 0; i < sz; i++) { printf("%d ", *(p + i)); } return 0; } 运行结果六、二级指针指针变量也是变量,是变量就有地址,那指针变量的地址也可以存放在指针里面如图,这就是二级指针例如:1. int b = 20; 2. *pa = &b; //pa是指针变量(一级指针) 3. **ppa = &pa; ///ppa是一个二级指针*pa 通过对 pa中的地址进行解引用,这样找到的是 b , *pa 其实访问的就是 b **ppa 先通过 *pa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 b ----------------我是分割线--------------- 七、指针数组指针数组是指针还是数组?答案:是数组,是存放指针的数组数组我们已经知道整形数组,字符数组,比如1. int arr1[5]; 2. char arr2[6];指针数组:存放指针的数组就是指针数组,例如int* p[5];p 是一个数组,有五个元素,每个元素是一个整形指针最后,再进行总结一下int p;//这是一个普通的整型变量 int *p; //首先从 p 处开始先与 * 结合,所以说明 p 是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型;所以 p 是一个返回整型数据的指针int **p; //首先从 p 开始,先与*结合,说是 p 是一个指针(*p);然后再与 * 结合;说明指针所指向的元素是指针(**p),然后再与int 结合,说明该指针所指向的元素是整型数据int p[3] ; //首先从 p 处开始,先与 [] 结合,说明 p 是一个数组,然后与int 结合,说明数组里的元素是整型的,所以 p 是一个由整型数据组成的数组 int *p[3]; //首先从 p 处开始,先与 [] 结合,因为其优先级比 * 高;所以 p 是一个数组,然后再与 * 结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以 p 是一个由返回整型数据的指针所组成的数组 ----------------我是分割线--------------- 文章就到这里!
目录 一、线性表二、顺序表 2.1 顺序表概念及结构2.2 顺序表接口实现2.2.1 顺序表初始化2.2.2 顺序表的销毁2.2.3 顺序表的打印2.2.4 顺序表增加数据(插入,头插、尾插)2.2.5 顺序表删除数据(删除,头删、尾删)2.2.6 顺序表查找数据2.2.7 顺序表修改数据三、顺序表完整代码(C语言)3.1 SeqList.h3.2 SeqList.c3.3 Test.c 四、顺序表的优缺点 一、线性表在学习顺序表和链表之前,先要了解什么是线性表: 线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...文章所讲述的顺序表和下一篇链表便是线性表的一种。 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。例如:顺序表和链表 顺序表在逻辑结构上是连续的,物理结构上也是连续的;链表在逻辑结构上是连续的,但是在物理结构上不是连续的-------------------我是分割线------------------ 二、顺序表 2.1 顺序表概念及结构 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。顺序表一般可以分为:静态顺序表、动态顺序表(1)静态顺序表:使用定长数组存储元素。 #define N 100//对空间大小进行了限定 typedef int SLDataType;便于修改存储的数据类型 typedef struct SeqList { SLDataType arr[N];//定义需要的数组长度 int size;//记录数组的有效个数(已使用) }SL; //缺陷:空间给小了不够用,给大了可能浪费,不实用(2)动态顺序表:使用动态开辟的数组存储typedef int SLDataType;//便于修改存储的数据类型 typedef struct SeqList { SLDataType* arr;//指向动态开辟的数组,用于存放数据。空间不够则增容 int size;//数组的个数 int capacity;//容量大小 }SL;2.2 顺序表接口实现首先,我们要创建一个顺序表类型,该顺序表类型包括了顺序表的起始位置、记录顺序表内已有元素个数的计数器(size),以及记录当前顺序表的容量的变量(capacity),这里实现的是动态顺序表typedef int SLDataType;//便于修改存储的数据类型 //顺序表的动态储存 typedef struct SeqList { SLDataType* arr;//指向动态开辟的数组,用于存放数据 int size;//数组的个数 int capacity;//容量大小 }SL;2.2.1 顺序表初始化//初始化顺序表 void SeqListInit(SL* ps) { ps->arr = NULL; ps->size = 0; ps->capacity = 0; }2.2.2 顺序表的销毁顺序表所用的内存空间是动态开辟在堆区的,所以我们在使用完后需要及时对其进行释放,避免造成内存泄漏。//销毁顺序表 void SeqListDestroy(SL* ps) { assert(ps); if (ps->size) { free(ps->arr); ps->arr = NULL; ps->size = 0; ps->capacity = 0; } }2.2.3 顺序表的打印遍历即可//打印 void SeqListPrint(SL* ps) { assert(ps); int i = 0; for (i = 0; i < ps->size; i++) { printf("%d ", ps->arr[i]); } printf("\n"); }2.2.4 顺序表增加数据(插入,头插、尾插) 每次需要增加数据的时候,首先都应该先检查顺序表内元素个数是否已达顺序表容量上限。若已达上限,那么我们就需要先对顺序表进行扩容,然后才能增加数据。(1)插入:就是从两个数据中间直接直接插入一个新的数据(时间复杂度为O(n))//插入 void SeqListInsert(SL* ps, int pos, SLDataType n) { assert(ps); assert(pos >= 0 && pos <= ps->size); CheckCapacity(ps);//检查顺序表容量,不够则开辟新空间 //挪动数据 int end = ps->size - 1; while (end >= pos); { ps->arr[end + 1] = ps->arr[end]; end--; } ps->arr[pos] = n; ps->size++; }//检查顺序表容量 void CheckCapacity(SL* ps) { assert(ps); //进行扩容,两种情况,1、没有开辟空间 2、空间不足 if (ps->size == ps->capacity) { int NewCapacity = (ps->capacity == 0 ? 4 : ps->capacity * 2); SLDataType* tmp = (SLDataType*)realloc(ps->arr, NewCapacity * sizeof(SLDataType)); if (tmp == NULL) { printf("realloc fail\n"); //return; exit(-1); } ps->arr = tmp; ps->capacity = NewCapacity; } }(2)头插:顾名思义就是从头部插入一个新的数据(时间复杂度为O(n))这里有两种写法,推荐可以直接复用插入的代码//顺序表的头插 void SeqListPushFront(SL* ps, SLDataType n) { /*assert(ps); CheckCapacity(ps); int end = ps->size - 1; while (end >= 0) { ps->arr[end + 1] = ps->arr[end]; end--; } ps->arr[0] = n; ps->size++;*/ //两种都行,下面这种比较方便 SeqListInsert(ps, 0, n); }(3)尾插:这个比较简单,直接从尾上插入即可(时间复杂度为O(1))这里也是有两种写法,推荐可以直接复用插入的代码//尾插 void SeqListPushBack(SL* ps, SLDataType n) { /*assert(ps); CheckCapacity(ps); ps->arr[ps->size] = n; ps->size++;*/ //两种都行,下面这种比较方便 SeqListInsert(ps, ps->size, n); }2.2.5 顺序表删除数据(删除,头删、尾删)删除数据,其实可以理解为:从某个位置开始,数据依次向前覆盖。这样一来,该位置的数据就相当于删除了。(1)删除: 直接删除 pos 位置的值,向前覆盖(时间复度为O(n))//删除数据 void SeqListErase(SL* ps, int pos) { assert(ps); assert(pos >= 0 && pos < ps->size); int begin = pos; while (begin < ps->size - 1) { ps->arr[begin] = ps->arr[begin + 1]; begin++; } ps->size--; }(2)头删(时间复杂度为O(n))这里也是有两种写法,这里也是有两种写法,推荐可以直接复用删除的代码//头删, O(n) void SeqListPopFront(SL* ps) { /*assert(ps); assert(ps->size > 0); int begin = 1; while (begin < ps->size) { ps->arr[begin - 1] = ps->arr[begin]; begin++; } ps->size--;*/ //两种都行,下面这种比较方便 SeqListErase(ps, 0); }(3)尾删(时间复杂度为O(1))这里也是有两种写法,推荐可以直接复用删除的代码//尾删 void SeqListPopBack(SL* ps) { /*assert(ps); assert(ps->size > 0); ps->size--;*/ //两种都行,下面这种比较方便 SeqListErase(ps, ps->size - 1); }2.2.6 顺序表查找数据查找数据也相对简单,直接遍历一次顺序表即可,若找到了目标数据,则停止遍历,并返回该数据的下标//查找数据 int SeqListFind(SL* ps, SLDataType n) { assert(ps); int i = 0; for (i = 0; i < ps->size; i++) { if (ps->arr[i] == n) { return n;//找到了 } } return -1; //找不到 }2.2.7 顺序表修改数据修改数据,就直接对该位置的数据进行再次赋值即可。//修改数据 void SeqListModify(SL* ps, int pos, SLDataType n) { assert(ps); assert(pos >= 0 && pos < ps->size); ps->arr[pos] = n; }-------------------我是分割线------------------ 三、顺序表完整代码(C语言)分三个文件写SeqList.h(类型定义、接口函数声明、引用的头文件)SeqList.c(接口函数的实现)Test.c(主函数、测试顺序表各个接口功能)3.1 SeqList.h#pragma once//防止头文件被二次引用 //声明 #define _CRT_SECURE_NO_WARNINGS//vs编译器需要,可自行删去 #include <stdio.h> #include <stdlib.h> #include <assert.h> typedef int SLDataType;//便于修改存储的数据类型 //顺序表的动态储存 typedef struct SeqList { SLDataType* arr;//指向动态开辟的数组,用于存放数据 int size;//数组的个数 int capacity;//容量大小 }SL; //初始化顺序表 void SeqListInit(SL* ps); //打印 void SeqListPrint(SL* ps); //检查顺序表容量 void CheckCapacity(SL* ps); //销毁顺序表 void SeqListDestroy(SL* ps); //增 //顺序表的头插,O(n) void SeqListPushFront(SL* ps, SLDataType n); //尾插,O(1) void SeqListPushBack(SL* ps, SLDataType n); //插入数据 void SeqListInsert(SL* ps, int pos, SLDataType n); //删 //头删, O(n) void SeqListPopFront(SL* ps); //尾删, O(1) void SeqListPopBack(SL* ps); //删除数据 void SeqListErase(SL* ps, int pos); //查找数据 int SeqListFind(SL* ps, SLDataType n); //修改数据 void SeqListModify(SL* ps, int pos, SLDataType n);3.2 SeqList.c//函数接口实现 #include "SeqList.h" //打印 void SeqListPrint(SL* ps) { assert(ps); int i = 0; for (i = 0; i < ps->size; i++) { printf("%d ", ps->arr[i]); } printf("\n"); } //初始化顺序表 void SeqListInit(SL* ps) { ps->arr = NULL; ps->size = 0; ps->capacity = 0; } //检查顺序表容量 void CheckCapacity(SL* ps) { assert(ps); //进行扩容,两种情况,1、没有开辟空间 2、空间不足 if (ps->size == ps->capacity) { int NewCapacity = (ps->capacity == 0 ? 4 : ps->capacity * 2); SLDataType* tmp = (SLDataType*)realloc(ps->arr, NewCapacity * sizeof(SLDataType)); if (tmp == NULL) { printf("realloc fail\n"); //return; exit(-1); } ps->arr = tmp; ps->capacity = NewCapacity; } } //销毁顺序表 void SeqListDestroy(SL* ps) { assert(ps); if (ps->size) { free(ps->arr); ps->arr = NULL; ps->size = 0; ps->capacity = 0; } } //顺序表的头插 void SeqListPushFront(SL* ps, SLDataType n) { /*assert(ps); CheckCapacity(ps); int end = ps->size - 1; while (end >= 0) { ps->arr[end + 1] = ps->arr[end]; end--; } ps->arr[0] = n; ps->size++;*/ //两种都行,下面这种比较方便 SeqListInsert(ps, 0, n); } //尾插 void SeqListPushBack(SL* ps, SLDataType n) { /*assert(ps); CheckCapacity(ps); ps->arr[ps->size] = n; ps->size++;*/ //两种都行,下面这种比较方便 SeqListInsert(ps, ps->size, n); } //插入 void SeqListInsert(SL* ps, int pos, SLDataType n) { assert(ps); assert(pos >= 0 && pos <= ps->size); CheckCapacity(ps);//检查顺序表容量,不够则开辟新空间 //挪动数据 int end = ps->size - 1; while (end >= pos); { ps->arr[end + 1] = ps->arr[end]; end--; } ps->arr[pos] = n; ps->size++; } //头删, O(n) void SeqListPopFront(SL* ps) { /*assert(ps); assert(ps->size > 0); int begin = 1; while (begin < ps->size) { ps->arr[begin - 1] = ps->arr[begin]; begin++; } ps->size--;*/ //两种都行,下面这种比较方便 SeqListErase(ps, 0); } //尾删 void SeqListPopBack(SL* ps) { /*assert(ps); assert(ps->size > 0); ps->size--;*/ //两种都行,下面这种比较方便 SeqListErase(ps, ps->size - 1); } //删除数据 void SeqListErase(SL* ps, int pos) { assert(ps); assert(pos >= 0 && pos < ps->size); int begin = pos; while (begin < ps->size - 1) { ps->arr[begin] = ps->arr[begin + 1]; begin++; } ps->size--; } //查找数据 int SeqListFind(SL* ps, SLDataType n) { assert(ps); int i = 0; for (i = 0; i < ps->size; i++) { if (ps->arr[i] == n) { return n;//找到了 } } return -1; //找不到 } //修改数据 void SeqListModify(SL* ps, int pos, SLDataType n) { assert(ps); assert(pos >= 0 && pos < ps->size); ps->arr[pos] = n; }3.3 Test.c//测试函数 #include "SeqList.h" void SeqListTest1() { SL s1;//定义一个结构体 SeqListInit(&s1);//初始化 SeqListPushFront(&s1, 1); SeqListPushBack(&s1, 2); SeqListPushBack(&s1, 3); SeqListPrint(&s1); SeqListPopFront(&s1); SeqListPrint(&s1); SeqListDestroy(&s1); } void SeqListTest2() { SL s1; SeqListInit(&s1); SeqListPushFront(&s1, 1); SeqListPushBack(&s1, 2); SeqListPushBack(&s1, 3); SeqListPushFront(&s1, 0); SeqListPushBack(&s1, 4); SeqListPrint(&s1); SeqListPopBack(&s1); SeqListPrint(&s1); SeqListDestroy(&s1); } void SeqListTest3() { SL s1; SeqListInit(&s1); SeqListPushFront(&s1, 1); SeqListPushBack(&s1, 2); SeqListPushBack(&s1, 3); SeqListPrint(&s1); int find = SeqListFind(&s1, 1); if (find == -1) printf("找不到\n"); else printf("%d", find); } void SeqListTest4() { SL s1; SeqListInit(&s1); SeqListPushFront(&s1, 1); SeqListPushBack(&s1, 2); SeqListInsert(&s1, 2, 3); SeqListPrint(&s1); } int main() { SeqListTest1(); return 0; } 四、顺序表的优缺点(1)优点空间连续,可以按下标进行随机访问顺序表的 cpu 高速缓存命中率高(2)缺点空间不够需要增容,增容有一定的性能消耗,且可能存在一定的空间浪费增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。中间或头部的插入、删除,需要挪动数据,效率低,时间复杂度为O(N)-------------------我是分割线------------------文章就到这里
目录前言 一、基础知识1.1 什么是栈区?1.2 寄存器1.3 测试代码和一些其它的二、函数栈帧的创建和销毁的过程2.1 _tmainCRTStartup函数(调用main函数)栈帧的创建2.2 main函数栈帧的创建2.3 main函数内执行有效代码2.4 Add函数栈帧的创建2.5 Add函数内执行有效代码 2.6 Add函数栈帧的销毁2.7 main函数代码继续执行 三、所需反汇编代码总览四、总结前言 在前期的学习过程中,我们可能会有很多的困惑:局部变量是怎么创建的?为什么未初始化的局部变量的值是随机值?函数是如何传参的?以及传参的顺序是怎样的?形参和实参是什么关系?函数调用是怎么做的?函数调用结束后是怎么返回的?这里使用的环境是 Visual Studio 2019(原本想用 Visual Studio 2013 的,但是没有安装有),提示不要使用太过高级的编译器,因为越高级的编译器越不容易观察。同时这里需要注意的是在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,不是完全相同的,具体细节取决于编译器 一、基础知识1.1 什么是栈区?C/C++程序内存分配的几个区域:1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。3. 数据段(静态区)(static):存放全局变量、静态数据。程序结束后由系统释放。4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码今天文章的内容是关于栈区的,其他简单了解即可 接下来,补充一下栈的知识,了解到这就可以了,足够使用了栈区的使用是从高地址到低地址栈区的使用遵循先进后出,后进先出栈区的放置是从高地址往低地址放置:push 是压栈删除是从低往高删除:pop 是出栈接下来还要了解一个重要的东西,寄存器,寄存器整篇文章都在使用。 -------------------我是分割线------------------1.2 寄存器这里简单介绍一些寄存器,其它的先不要过多理解常见寄存器有eax、ebx、ecx、edx,这四个都当做通用寄存器,保留临时数据,ebp和esp较为特殊eax"累加器" 它是很多加法乘法指令的缺省寄存器。ebx"基地址"寄存器, 在内存寻址时存放基地址。ecx计数器,是重复(REP)前缀指令和LOOP指令的内定计数器。edx总是被用来放整数除法产生的余数。esi源索引寄存器edi目标索引寄存器ebp(栈底指针)"基址指针",存放的是地址,用来维护函数栈帧esp(栈顶指针)专门用作堆栈指针,存放的是地址,用来维护函数栈帧1.3 测试代码和一些其它的使用的测试代码(足够简单才好演示):#include<stdio.h> int Add(int x, int y) { int z = 0; z = x + y; return z; } int main() { int a = 10; int b = 20; int c = 0; c = Add(a, b); printf("%d\n", c); return 0; }接下来还有一些汇编代码的含义:mov:mov是数据传送指令(move的缩写),用于将一个数据从源地址传送到目标地址sub:减法,subtraction的缩写ea:Load effective address的缩写,取有效地址call:用于调用其他函数add:加法pop:出栈push:入栈或压栈测试编译器使用的是 VS2019,以调试一步步进行演示 准备工作已经做好,接下来开始演示。 -------------------我是分割线------------------二、函数栈帧的创建和销毁的过程每一个函数调用,都要在栈区创建一个空间2.1 _tmainCRTStartup函数(调用main函数)栈帧的创建先了解 main 函数是被谁调用的,按 F10 或 F11 进入调试模式,F10是逐过程,F11是逐语句,打开堆栈这时看到调用堆栈这个窗口按F10,按到return 0 时再按一次,调用栈堆会出现以下内容(我使用的VS2019 没有出现,可能编译器版本太高,优化掉了) 这时再看堆栈窗口发现 main 函数被 __tmainCRTStartup() 调用而 __tmainCRTStartup() 又被 mainCRTStartup() 调用观察C语言代码所对应的汇编代码,在调试状态下,右击鼠标转到反汇编转到汇编后,右键 取消符号名,方便查看阅读汇编代码先看以下汇编代码 编译器会先在栈区处开辟一部分空间给 __tmainCRTStartup() 和 _mainCRTStartup() 函数,并用 esp 和 ebp 维护,先看下这两个函数栈帧开辟情况:2.2 main函数栈帧的创建先看第一部分汇编代码,逐条语句解释push ebp //在栈顶开辟ebp寄存器对应的空间 mov ebp,esp //将esp的值传入ebp中(即将ebp指针移动到原本esp指向的位置) sub esp,0E4h //将esp的内容减去0E4h(将esp移动到原esp-0E4h的位置) push ebx //在栈顶放入ebx push esi //在栈顶放入esi push edi //在栈顶放入edi 此时进入main函数(也就是程序调试开始),首先要 push ebp 进行压栈,ebp 在 __tmainCRTStartup() 上面压栈观察esp ebp 地址的变化,在调试的监视里面查看,push ebp 之后,esp 指向的位置也随之改变 (地址减小)接下来是 mov ebp,esp ,将esp的值传入ebp中(即将ebp指针移动到esp指向的位置)接下来 sub esp,0E4h,将esp的内容减去0E4h(将esp移动到原esp-0E4h的位置,esp-0E4h地址减小) 接下来 push ebx ,在栈顶放入ebx,地址依旧减小接下来 push esi ,在栈顶放入 esi,地址依旧减小接下来 push edi ,在栈顶放入edi,地址依旧减小步骤演示图:接下来: lea edi,[ebp-24h]//将ebp-24h的地址放入edi mov ecx,9//将9放入ecx mov eax,0CCCCCCCCh//将0CCCCCCCCh放入eax rep stos dword ptr es:[edi]//将edi往下ecx个地址的数据全部初始化为0CCCCCCCChlea edi,[ebp-24h],把 ebp - 0E4h 这个地址加载到 edi 里顺便看一下,ebp-24h 的地址 接下来mov ecx,9,将9放入ecxmov eax,0CCCCCCCCh ,将0CCCCCCCCh放入eaxrep stos dword ptr es:[edi],将edi往下ecx个地址的数据全部初始化为0CCCCCCCCh说明:这里的数据全部是十六进制数字,数据后面的 h 直接忽略掉即可,它只是编译器十六进制的一种表示形式 这三步执行完,把 edi 这个位置开始向下的 9 行 dword 数据全部改为 0xcccccccc (word是2个字节,dword是4个字节),一共36个字节,一行四字节,共9 行 调试里打开内存监控,内存监控中的内存地址也是向上减小的步骤演示图:到这main函数的函数栈帧已经创建好了2.3 main函数内执行有效代码接下来开始初始化 a、b、c 局部变量,直接看mov初始化,上面两行不用理mov ecx,0ECC003h call 00EC131B int a = 10; mov dword ptr [ebp-8],0Ah int b = 20; mov dword ptr [ebp-14h],14h int c = 0; mov dword ptr [ebp-20h],0 mov dword ptr [ebp-8],0Ah ,把 0Ah(十进制为10) 放到 ebp-8 的位置mov dword ptr [ebp-14h],14h ,把 14h(20) 放到 ebp-14h的位置mov dword ptr [ebp-20h],0 ,把 0 放到 ebp-20h的位置 到这里a,b,c 已经初始化完成了步骤演示图:接下来: 1. mov eax,dword ptr [ebp-14h] 2. push eax 3. mov ecx,dword ptr [ebp-8] 4. push ecxmov eax,dword ptr [ebp-14h] ,把 ebp-14h 的值0000 0014(十进制是20)放到 eax 里去 push eax ,压栈 eax(20),esp指向的位置也随之改变 (地址减小) mov ecx,dword ptr [ebp-8] ,把 ebp-8 的值0000000a(十进制是10) 放到 ecx 里去push ecx ,压栈 ecx(10),esp指向的位置也随之改变 (地址减小) 步骤演示图:(截一部分,用不到的先不截)接下来为call 指令,按下F11,此时就正式进入Add函数内部 并为其开辟栈帧,详情见下文2.4 Add函数栈帧的创建按 F11,进入到 Add 函数 ,该add 函数地址不一定与main 函数地址相连,但是add 函数的地址一定在main 函数地址上面call 00EC131B call 指令调用 Add 函数,这里逐语句(F11)执行,发现这里竟然存储着下一条指令的地址,事实上 call 指令把下一条指令的地址压栈了(为了 Add 函数结束后能找回来),esp 地址也跟着变化进入 Add 函数前,会先为 Add 函数开辟函数栈帧,这这些操作跟先前main函数开辟函数栈帧操作一样,所以这里就不细谈了push ebp//将ebp上移 mov ebp,esp//将esp内容放入ebp(移动ebp) sub esp,0CCh//esp-0CCh(为Add开辟空间) push ebx//在栈顶放入ebx push esi//在栈顶放入esi push edi//在栈顶放入edi lea edi,[ebp-0Ch]//ebp-0Ch的空间 mov ecx,3//3存入ecx mov eax,0CCCCCCCCh//存入eax rep stos dword ptr es:[edi]//esp往下0ch的空间进行赋值首先,push ebp把ebp压栈到栈顶,再mov把esp赋给ebp,再sub,把esp-去0CCh,此步骤就是在为Add函数开辟空间,接着进行三次push,同main函数那样,同理,依旧是赋值为CCCCCCCC,详细过程不再赘述,跟上文main函数一样,如图所示:详细:简图:2.5 Add函数内执行有效代码 mov ecx,0ECC003h call 00EC131B int z = 0; mov dword ptr [ebp-8],0 //把 0 初始化到 ebp-8 的位置 z = x + y; mov eax,dword ptr [ebp+8] //把 ebp+8 的值 10 放到 eax 里 add eax,dword ptr [ebp+0Ch] //把 ebp+0ch 的值 20 和 eax 的值 10 相加 mov dword ptr [ebp-8],eax //把 eax 的值 30 放到 ebp-8(z) 里去 return z; mov eax,dword ptr [ebp-8] //把 ebp-8 的值 30 放到 eax 里去首先,把0放到ebp-8的位置上,接着mov把ebp+8的值放到eax里头去,此时eax就是10。再add给eax加上ebp+0ch,就是把20加进去,此时eax就是30,加完后再把eax(30)放到ebp-8里头去,最终的结果(30)放到z里头去。接下来就要进行返回了,也就是Add函数栈帧的销毁,见下文2.6 Add函数栈帧的销毁 return z; mov eax,dword ptr [ebp-8] //把ebp-8的值(30)放到eax里头去 } pop edi //出栈,释放为edi创建的栈区 pop esi //出栈,释放为esi创建的栈区 pop ebx //出栈,释放为exb创建的栈区 add esp,0CCh //为esp地址+0CCh,即退出Add程序的栈区空间 cmp ebp,esp 不理会 call 00EC1244 不理会 mov esp,ebp //ebp的值赋给esp,此时esp和ebp相同 pop ebp //弹出ebp ret //返回mov eax,dword ptr [ebp-8] ,把ebp-8的值(30)放到eax里头去pop edi ,出栈,释放为edi创建的栈区,地址开始增大pop esi ,出栈,释放为esi创建的栈区,地址继续增大pop ebx ,出栈,释放为exb创建的栈区,地址继续增大步骤演示图:add esp,0CCh ,为esp地址+0CCh,即退出Add程序的栈区空间 ,此时esp和ebp相同 步骤演示图:mov esp,ebp ,ebp的值赋给esp,此时esp和ebp依旧相同pop ebp ,弹出ebp,并将ebp所指向的main函数的起始地址赋值给了ebp指针,esp指针向高位移动,esp和ebp重新开始维护main函数的栈区空间 ret ,返回到main函数,在执行 ret 指令时,esp指针就指向了栈顶存放的call指令的下一条指令的地址, 步骤演示图:此时Add函数的栈帧算是真正销毁2.7 main函数代码继续执行 add esp,8 mov dword ptr [ebp-20h],eax printf("%d\n", c); mov eax,dword ptr [ebp-20h] push eax push 0EC7B30h call 00EC10D2 add esp,8 return 0; xor eax,eax add esp,8 ,而这一条指令的意思,是往esp里加8,即向高位移动,实际上这条指令就是在销毁我们的形参步骤演示图: mov dword ptr [ebp-20h],eax ,把eax的值放到ebp-20h上,而eax就是我们出Add函数时计算的和 接下来就是打印值和 main函数函数栈帧销毁,都与上面类似,这里不多做赘述 -------------------我是分割线------------------ 三、所需反汇编代码总览add 函数int Add(int x, int y) { push ebp mov ebp,esp sub esp,0CCh push ebx push esi push edi lea edi,[ebp-0Ch] mov ecx,3 mov eax,0CCCCCCCCh rep stos dword ptr es:[edi] mov ecx,0ECC003h call 00EC131B int z = 0; mov dword ptr [ebp-8],0 z = x + y; mov eax,dword ptr [ebp+8] add eax,dword ptr [ebp+0Ch] mov dword ptr [ebp-8],eax return z; mov eax,dword ptr [ebp-8] } pop edi pop esi pop ebx add esp,0CCh cmp ebp,esp call 00EC1244 mov esp,ebp pop ebp ret main函数 int main() { push ebp mov ebp,esp sub esp,0E4h push ebx push esi push edi lea edi,[ebp-24h] mov ecx,9 mov eax,0CCCCCCCCh rep stos dword ptr es:[edi] mov ecx,0ECC003h call 00EC131B int a = 10; mov dword ptr [ebp-8],0Ah int b = 20; mov dword ptr [ebp-14h],14h int c = 0; mov dword ptr [ebp-20h],0 c = Add(a, b); mov eax,dword ptr [ebp-14h] push eax mov ecx,dword ptr [ebp-8] push ecx call 00EC10B4 add esp,8 mov dword ptr [ebp-20h],eax printf("%d\n", c); mov eax,dword ptr [ebp-20h] push eax push 0EC7B30h call 00EC10D2 add esp,8 return 0; xor eax,eax } pop edi pop esi pop ebx add esp,0E4h cmp ebp,esp call 00EC1244 mov esp,ebp pop ebp ret 四、总结 1、局部变量是怎么创建的?首先为这个函数分配好栈帧空间,并初始化一部分空间为0xcccccccc,再为局部变量分配空间并初始化2、为什么未初始化的局部变量的值是随机值?在开辟好栈帧空间后,会初始化 0xcccccccc 这样的随机值,而局部变量的初始化操作就会将随机值覆盖3、函数是如何传参的?以及传参的顺序是怎样的?在调用函数前,会先将函数参数从后向前依次压栈,而进入函数后,它会通过指针的偏移量找到形参4、形参和实参是什么关系?形参是在压栈时开辟的空间,实参和形参只是值相同,空间是独立的。所以形参是实参的一份临时拷贝,改变形参不会改变实参5、函数调用是怎么做的?函数调用前,它会记住下一条指令的地址,这样做是为了函数结束后能回的来6、函数调用结束后是怎么返回的?函数调用结束会通过下一条指令的地址返回,这也是为什么要压栈下一条指令的地址。在返回前它会将计算好的值放在 eax 里 -------------------我是分割线------------------ 这里只是对函数栈帧的创建和销毁简单描述,需要更详细的百度即可写完这篇文章给我的感觉就是图真难画.... -------------------我是分割线------------------ 文章就先到这
目录一、面向过程和面向对象初步认识二、类的引入三、类的定义四、类的访问限定符及封装4.1 访问限定符4.2 封装五、类的作用域六、类的实例化七、类对象模型7.1 计算类对象的大小7.2 类对象的存储方式猜测八、this 指针8.1 this指针的引出8.2 this 指针的特性九、关于C++的代码风格一、面向过程和面向对象初步认识C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成比如:洗衣服这件事 ,面向过程是这样的,注重的是过程 面向对象是这样的,注重的是对象-------------------我是分割线------------------二、类的引入 C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现,会发现struct中也可以定义函数。比如,在C++中使用结构体(C++兼容C语言)//C++兼容C结构体的语法 struct ListNodeC { int val; struct ListNodeC* next; }; //C++同时也支持这种语法,因为在C++中struct已经升级成了类 struct ListNodeCpp { int val; ListNodeCpp* next; };在C++中,struct 中可以定义函数struct Stack { //C++在可以在结构体中定义函数 void Init() { a = 0; top = capacity = 0; } void Push(int x) { // ... } void Pop() { // ... } int* a; int top; int capacity; }; 上面结构体的定义,在C++中更喜欢用类(class)来代替 struct。 -------------------我是分割线------------------三、类的定义class className { // 类体:由成员函数和成员变量组成 }; // 一定要注意后面的分号class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。 类的两种定义方式:(1)声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理(函数代码量少,只有几行的情况)//声明和定义全部放在类中 class Person { //函数定义 void showInfo() { cout << _name << "-" << _sex << "-" << _age << endl; } //... //成员变量 char _name; char _sex; int _age; //... };(2)类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加 类名:: //声明放在类中的头文件 person.h 中 class Person { //函数声明 void showInfo(); //... //成员变量声明 char _name; char _sex; int _age; //... }; //函数定义放在 test.cpp 中 void Person::showInfo() { cout << _name << "-" << _sex << "-" << _age << endl; }一般情况下,采用第二种方式:小函数想成为内联函数 inline,直接在类里面定义即可如果是大函数,定义和声明应该分离 -------------------我是分割线------------------四、类的访问限定符及封装4.1 访问限定符C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用【访问限定符说明】public 修饰的成员在类外可以直接被访问protected 和 private 修饰的成员在类外不能直接被访问(此处 protected 和 private 是类似的)访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止如果后面没有访问限定符,作用域就到 } 即类结束。class 的默认访问权限为 private,struct 为 public (因为struct要兼容C)注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别在这里 privata 和 protected 没有啥区别,学到继承的时候才有区别代码示例:class Person { //公有,大家可以随便使用访问 public: //函数声明 void showInfo(); //... //私有,不能访问 private: //成员变量声明 char _name; char _sex; int _age; //... };-------------------我是分割线------------------面试题问题:C++中struct和class的区别是什么? 解答:C++需要兼容C语言,所以C++中 struct 可以当成结构体使用。另外C++中struct还可以用来定义类。和 class定义类是一样的,区别是 struct 定义的类默认访问权限是 public,class定义的类默认访问权限是 private。注意:在继承和模板参数列表位置,struct和class也有区别,后序给大家介绍 -------------------我是分割线------------------ 4.2 封装面向对象的三大特性封装、继承、多态。这里只是最重要的三大特性,其实面向对象不止这三种特性。在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互隐藏对象:设置 protected 和 private 域中的成员变量对外公开接口:设置 public 域中的开放成员函数封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件 对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可,同理封装也是如此在C++语言中实现封装可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用五、类的作用域类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域class Person { public: void PrintPersonInfo(); private: char _name[20]; char _gender[3]; int _age; }; // 这里需要指定PrintPersonInfo是属于Person这个类域 void Person::PrintPersonInfo() { cout << _name << " "<< _gender << " " << _age << endl; }六、类的实例化用类类型创建对象的过程,称为类的实例化(1)类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;比如入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息(2)一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间(3)做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子例如:七、类对象模型7.1 计算类对象的大小类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?class A { public: void PrintA() { cout<<_a<<endl; } private: char _a; };解释请看下面 7.2 类对象的存储方式猜测(1)第一种:对象中包含类的各个成员缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?(2)第二种:代码只保存一份,在对象中保存存放代码的地址这种也可行,但不是最优的,在后期学习C++中也会用到这种存储方式(3)第三种:只保存成员变量,成员函数存放在公共的代码段类的存储选择用这种方式通过下面的例子验证上面的猜想:// 类中既有成员变量,又有成员函数 class A1 { public: void f1() {} private: int _a; }; // 类中仅有成员函数 class A2 { public: void f2() {} }; // 类中什么都没有---空类 class A3 {}; int main() { cout << "A1:" << sizeof(A1) << endl; cout << "A2:" << sizeof(A2) << endl; cout << "A3:" << sizeof(A3) << endl; return 0; } 运行结果:一个类的大小,实际就是该类中”成员变量”之和(同结构体内存对齐,按照结构体内存对齐规则计算即可)注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。结构体内存对齐规则第一个成员在与结构体偏移量为0的地址处。其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为8结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。【面试题】1. 结构体怎么对齐? 为什么要进行内存对齐?2. 如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?3. 什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景这些在C语言已经解释过了,自己翻阅即可。 -------------------我是分割线------------------ 八、this 指针8.1 this指针的引出我们先来定义一个日期类 Dateclass Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1, d2; d1.Init(2022, 9, 25); d2.Init(2022, 9, 26); d1.Print(); d2.Print(); return 0; }运行结果:对于上述类,有这样的一个问题:Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?C++中通过引入 t this 指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成 比如,把 this 指针补充完善如下: 但实际代码不能补上,补上就会报错(上面补充方便理解),因为 this 指针对所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。 虽然实参和形参位置不能显示接受 this 指针(也就是上面写出来),但可以在成员函数内部使用 this 指针,如8.2 this 指针的特性this指针的类型:类型* const,即成员函数中,不能给this指针赋值。只能在“成员函数”的内部使用this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递 -------------------我是分割线------------------ 【面试题】1. this指针存在哪里?this 指针存在栈上,因为它是一个形参,它随着函数调用结束之后会销毁了2. this指针可以为空吗?(1)下面程序编译运行结果是?A、编译报错B、运行崩溃C、正常运行// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 class A { public: void Print() { cout << "Print()" << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }答案是C,正常运行解释:成员函数的地址不在对象中存储,存在公共代码段。这里调用成员函数,不会去访问 p 指向的空间,也就不存在空指针解引用了,这里只会把 p 传递给隐含的 this 指针,但是 Show 函数中也没有解引用 this 指针。所以这里选择 C 选项。(2)下面程序编译运行结果是?A、编译报错B、运行崩溃C、正常运行// 2.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 class A { public: void PrintA() { cout << _a << endl; } private: int _a; }; int main() { A* p = nullptr; p->PrintA(); return 0; }答案是B,运行崩溃解释:这里同上,Print 不在 p 里面,它在公共代码段,然后把 p 传递给隐含的 this 指针,至此没问题。但是在 Print 函数里访问成员变量时 _a 时会补充上 this->_a,而这个指针是一个空指针,所以崩了。所以这里选择 B 选项。九、关于C++的代码风格我们经常会在 C++ 代码里看到变量前加一个杠 _ 或者在变量后加一个杠 _,这其实是 C++ 的规范用法, 这里的价值在如上代码就体现了,如果不加杠,在初始化时就懵了 (year = year),加杠就代表它是成员变量,易于理解如果不加就是这种情况了,别人刚看到就晕菜了,这里的year、month、day到底是成员变量,还是函数形参?实属不易于理解 在C++代码中,主要有两种代码风格习惯单词和单词之间首字母大写间隔 -- 驼峰法 如 GetYear单词全部小写,单词之间_分割 如 get_year自己选择一种即可,这里推荐的代码风格:函数名、类名等所有单词首字母大写 如 DateMgr变量首字母小写,后面单词首字母大写 如dateMgr成员变量,首单词前面加_ 如 _dateMgr -------------------我是分割线------------------ 文章先到这,下篇即将更新
目录位移运算符1、移位运算符简介2、先要了解一点3、 << 左移运算符4、>>右移运算符5、警告6、最后位移运算符1、移位运算符简介在C语言中,移位运算符有双目移位运算符:<<(左移)和>>(右移)。左移运算是将一个二进制位的操作数按指定移动的位数向左移动,移出位被丢弃,右边移出的空位一律补0。右移运算是将一个二进制位的操作数按指定移动的位数向右移动,移出位被丢弃,左边移出的空位一律补0,或者补符号位,这由不同的机器而定。在使用补码作为机器数的机器中,正数的符号位为 0 ,负数的符号位为 1 。听不懂是吧,接下来且听我详解。2、先要了解一点首先,要了解原码、反码、补码(简单说一下)整数在内存中存储的形式是补码的二进制。整数的二进制表示:有3种(原码、反码、补码)原码:直接根据数值写出的二进制序列就是原码(32位)反码:原码的符号位不变,其他位按位取反就是反码补码:反码加1,就是补码对于正整数的原码、反码、补码都相同;负数是存放在二进制的补码中,负整数的原码、反码、补码都不相同。例如:1(正整数的原码、反码、补码都相同)1.原码:0000000 00000000 00000000 00000001 反码:0000000 00000000 00000000 00000001 补码:0000000 00000000 00000000 00000001最高位为0 ,也是符号位例如:-1(负整数的原码、反码、补码都相同)原码:10000000 00000000 00000000 00000001 反码:11111111 11111111 11111111 11111110(按位取反,符号位不变) 补码:11111111 11111111 11111111 11111111(反码加1) 最高位为1,也是符号位3、 << 左移运算符先上代码(只演示负整数的,看完正整数的也会了,正整数的比较简单)1.#include<stdio.h> int main() { int a = -5; int b = a << 1; printf("%d\n", a); printf("%d\n", b); return 0; }结果是 -10这是为什么呢,原因如下:规则:左移运算是将一个二进制位的操作数按指定移动的位数向左移动,移出位被丢弃,右边移出的空位一律补0。简单说就是:左边丢弃,右边补0先写出 -5 的补码1.原码:10000000 00000000 00000000 00000101 (最高位为1) 反码:11111111 11111111 11111111 11111010 (按位取反,符号位不变) 补码:11111111 11111111 11111111 11111011 (反码加1)补码向左移动一位,左边去掉,右边补0,如图:此时得到的是补码,还要反推原码才能打印补码:11111111 11111111 11111111 11110110 反码:11111111 11111111 11111111 11110101(补码 -1 得到反码) 原码:10000000 00000000 00000000 00001010(按位取反得到原码)1.此时得到的原码就可以打印了,结果是 -10注:此时的 a 没有改变,依旧是 -54、>>右移运算符先上代码(只演示负整数的,看完正整数的也会了,正整数的比较简单)1.#include<stdio.h> int main() { int a = -5; int b = a >> 1; printf("%d\n", a); printf("%d\n", b); return 0; }结果是:-3这是为什么,原因如下:右移运算是将一个二进制位的操作数按指定移动的位数向右移动,移出位被丢弃,左边移出的空位一律补0,或者补符号位,这由不同的机器而定。在使用补码作为机器数的机器中,正数的符号位为 0 ,负数的符号位为 1 。简单说就是:(分为 2 种)1. 逻辑右移左边用0填充,右边丢弃。2. 算术右移左边用原该值的符号位填充,右边丢弃。到底是逻辑右移还是算术右移取决于编译器我当前使用的编译器,它采用的是算术右移先写出 -5 的补码1.原码:10000000 00000000 00000000 00000101 (最高位为1) 反码:11111111 11111111 11111111 11111010 (按位取反,符号位不变) 补码:11111111 11111111 11111111 11111011 (反码加1)补码向右移动一位,右边丢弃,左边补符号位,如图:此时得到的是补码,还要反推原码才能打印1.补码:11111111 11111111 11111111 11111101 反码:11111111 11111111 11111111 11111100(补码 -1 得到反码) 原码:10000000 00000000 00000000 00000011(按位取反得到原码)此时得到的原码就可以打印了,结果是 -3注:此时的 a 没有改变,依旧是 -55、警告警告⚠:对于移位运算符,不要移动负数位,这个是标准未定义的。例如: int a = 5; int b = a >> -1 //error1.注:移位操作符的操作数只能是整数。6、最后文章结束,希望对你有帮助!
1.冒泡排序简介冒泡排序(Bubble Sort),是一种较简单的排序算法。它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。冒泡排序的核心思想是:相邻两个元素进行比较,如果不满足条件就交换,最后得到降序或升序。2.直接上冒泡例子废话不多说,直接上例子。例如一组无序的数{11,8,26,57,34,45,22,19},让它排成升序,具体实现如下:先上图如图 1 所示是对无序表的第一次起泡排序,最终将无序表中的最大值 57 找到并存储在表的最后一个位置。具体实现过程为:体实现过程为:首先 11 和 8 比较,由于 11>8,所以两者交换位置,即从(1)到(2)的转变;然后继续 11 和 26 进行比较,由于 11<26,所以不移动位置;(3)中 26 同 57 比较得知,两者也不需要移动位置;直至(4),57 同 34 进行比较,57>34 ,两者交换位置,如(5)所示;同样 57>45(5)、57>22(6)、57>19(7),所以经过一次冒泡排序,最终在无序表中找到一个最大值 57,第一次冒泡结束;由于 57 已经判断为最大值,所以第二次冒泡排序时就需要找出除 57 之外的无序表中的最大值,比较过程和第一次完全相同。经过第二次冒泡,最终找到了除 57 之外的又一个最大值 45,比较过程完全一样,这里不再描述。通过一趟趟的比较,一个个的“最大值”被找到并移动到相应位置,直到检测到表中数据已经有序,或者比较次数等同于表中含有记录的个数,排序结束,这就是起泡排序。3.冒泡排序代码如下(升序)1.#include<stdio.h> int main() { int i, j, max; int arr[] = { 11, 8, 26, 57, 34, 45, 22, 19 }; int sz = sizeof(arr) / sizeof(arr[0]);//数组元素个数 for (i = 0; i < sz-1; i++)//趟数,一共 sz-1 趟 { for (j = 0; j < sz - i - 1; j++)//每一趟冒泡的过程 { if (arr[j] > arr[j + 1]) //交换两个值 { max = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = max; } } } printf("排序后的数组为:\n"); for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } return 0; }4.冒泡排序优化1.冒泡排序优化文章说到代码还可以继续优化,是什么原因呢?我们观察发现当数组已经有序了(假设是升序),如{1,2,3,4,5,6,7,8},我们写的程序依旧继续进行下一轮的比较,直到所有的数进行比较、排序完,很明显后面的比较没有意义的这就会让这些代码的效率降低。在这种情况下,我们就不必要对有序的数进行排序,以此减少代码执行的次数,提高代码的效率。因此,我们可以设置一个 flags ,如果已经排好序了就令 flags==0 结束循环;如果不是有序的就令 flags==1 继续执行。2.代码如下(优化后)1.#include<stdio.h> int main() { int i, j, max; int flags; int arr[] = { 11, 8, 26, 57, 34, 45, 22, 19 }; int sz = sizeof(arr) / sizeof(arr[0]);//数组元素个数 for (i = 0; i < sz-1; i++)//趟数,一共 sz-1 趟 { flags = 0;// 每次开始冒泡前,初始化 flags 值为 0 for (j = 0; j < sz - i - 1; j++)//每一趟冒泡的过程 { if (arr[j] > arr[j + 1]) //交换两个值 { max = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = max; flags = 1;//不是有序,程序继续执行 } } if (flags == 0) { break;//有序,结束此次循环 } } printf("排序后的数组为:\n"); for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } return 0; }5.最最后文章到这结束了,希望这篇文章能帮助到你!感谢支持!!!
网上搜了好多,感觉这个挺好用的(亲测有效)直接给步骤吧:1.电脑win+r, 输入regedit.exe打开注册表 按照文件的层次关系依次找到(双击)“HKEY_LOCAL_MACHINE–>SOFTWARE–>Microsoft–>Windows–>CurrentVersion–>Explorer–>ShellIconOverlayIdentifiers”这一项将Tortoise相关的项都提到靠前的位置(重命名,在名称之前加8个空格就好了)next,按照步骤走就可以了,一直到这一项 ShellIconOverlayIdentifiers,在这个目录下找到下面方框框起来的,右键单击,选择重命名(不要删文件名,删了我就不懂了喔),在文件名前加8个空格(最少8个)改好后重启电脑,就可以看到小绿勾了,如图,这是我弄好的(显示出了小绿勾)注:本方法不代表能解决所有的红色、绿色小勾勾问题最后,文章就结束啦,希望对你有帮助!
2023年04月
有质量的技术内容:技术社区需要提供有价值的技术内容,包括教程、文章、案例等,这些内容应该是经过筛选和验证的,能够帮助开发者解决实际问题。
活跃的社区氛围:社区需要有活跃的讨论和交流氛围,开发者可以在这里分享自己的经验和问题,与其他开发者互动交流,共同学习成长。
专业的技术支持:技术社区需要提供专业的技术支持,能够解答开发者的问题和疑惑,提供技术建议和指导。