【Linux】学习-基础IO—下

news2024/12/25 1:36:40

Linux基础IO—上

重定向

通过上篇的学习,我们了解了文件描述符的分配规则是遍历指针数组,用没有被使用的最小下标作为新的文件描述符,也就是我们可以通过关闭三个标准流文件并使用他们原先所占用的0,1,2描述符。

那我们假设这样一种情况,我们向显示器中打印内容,但在此之前我们将显示器文件,也就是标准输出流关闭,然后再创建并打开一个新的文件,此时我们向显示器中打印的内容会到哪去呢?

int main()
{
  close(1);
  int fd = open("myfile", O_WRONLY | O_CREAT, 00644);
  if (fd < 0)
  {
    perror("open");
    return 1;
  }
  printf("fd: %d\n", fd);
  fflush(stdout);

  close(fd);
  exit(0);
}

image-20230920214947600

运行之后我们发现,无论运行多少变显示器上都不会打印出内容,但是通过查看文件内容发现本来应该输出到显示器的内容被写到了文件里

这种现象就叫输出重定向!

注意:printf默认输出的文件就是stdout,而进程默认会打开三个标准流,每次都是如此,并且标准输出流文件描述符也总是1。

也就是说,像printf,scanf,fprintf,fscanf这些函数在底层只认识文件描述符,比如stdout对应的文件描述符是1,他们只认识1,而不认识什么stdout,是通过stdout建立起联系的

因此,重定向的本质,其实是在OS内部,更改fd对应的文件指向!

接下来我们再看个追加重定向的例子,也就是先关闭文件描述符为1的文件,再以追加的方式打开一个文件,这时系统自动分配,完成重定向!

int main()
{
  close(1);
  int fd = open("myfile", O_WRONLY|O_APPEND|O_CREAT);
  if (fd < 0)
  {
    perror("open");
    return 1;
  }
  printf("hello append,my fd is: %d\n", fd);
  printf("hello append,my fd is: %d\n", fd);
  printf("hello append,my fd is: %d\n", fd);
  fflush(stdout);

  close(fd);
  exit(0);
}

image-20230921100805532

这就是追加重定向,但我们使用的时候,我们利用的是文件描述符的分配规则来完成的,也就是在打开文件前先一步关闭对应的想重定向的文件,这样的话导致了代码使用时非常不灵活,因此能不能有一种灵活的方法能够随时重定向呢?系统就给我们一个接口:dup2函数——duplicate file description

dup2函数

其实完成重定向的本质就是把fd_array中对应的内容覆盖掉,因为数组下标是固定的,只需要将需要完成重定向的文件描述符覆盖掉即可完成,在系统中就用这样的一个接口来帮助我们完成这件事

画图:占位

没什么比直接阅读man手册实在!

image-20230921101208937

newfd是oldfd的拷贝,也就是后者原本fd_array数组对应的fd下标里的内容如今换成了前者的内容,如果前者原本对应的内容不是有效的内容,则调用失败,而后者的文件并不会被关闭。必要时可以先关闭newfd对应的文件。

例如,我们要以上面举过的例子,我们要将原本输出到显示器上的内容重定向到文件中,那么oldfd对应的应该就是我们打开的文件对应的文件描述符fd,而newfd对应的就是显示器原本对应的内容1,这样子我们可以任意时候打开文件并任意时候完成重定向而不用预先关闭,非常灵活方便!

// test:  dup2
int main()
{
    int fd = open("./log", O_CREAT | O_RDWR,0666);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    close(1);
    dup2(fd, 1);
    for (;;)
    {
        char buf[1024] = {0};
        ssize_t read_size = read(0, buf, sizeof(buf) - 1);
        if (read_size < 0)
        {
            perror("read");
            break;
        }
        printf("%s", buf);
        fflush(stdout);
    }
    return 0;
}

image-20230921104802285

运行程序后可以明显发现我在键盘上敲入的内容并没有显示到显示器上,而查看文件内容后发现:

image-20230921104813285

已经被重定向到log文件中!

注意:fflush虽然参数是stdout,但由于他只认识文件描述符,而此时1的内容已经被覆盖了,因此每一次写入缓冲区后立马刷新到文件里,实现同步过程,而不是等到缓冲区满了或者程序正常退出时才一次性刷新到文件里。

image-20230921121517002

若调用成功,会返回new description,也就是后者原本占有的文件描述符,失败则返回-1。

myshell添加重定向功能

我们在Linux进程控制一章中,自模拟实现过一个简易的命令行解释器myshell,下面我们可以像bash命令行一样实现重定向 > < 以及 >> 的功能!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <assert.h>

#define NUM 1024
#define SIZE 32
#define SEP " "

char *g_argv[SIZE];
char sub[SIZE];
char cmd_line[NUM];

#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3

int redir_status = NONE_REDIR;
//新增重定向功能,这里进行命令行解析
char *Checkredir(char *start)
{
  // ls -a>log.txt
  assert(start);
  char *end = start + strlen(start) - 1;
  while (end >= start)
  {
    if (*end == '>')
    {
      // ls -a>>log.txt
      if (*(end - 1) == '>')
      {
        *(end - 1) = '\0';
        ++end;
        redir_status = APPEND_REDIR;
        break;
      }
      // ls -a>log.txt
      *end = '\0';
      ++end;
      redir_status = OUTPUT_REDIR;
      break;
    }
    else if (*end == '<')
    {
      // cat<log.txt
      redir_status = INPUT_REDIR;
      *end = '\0';
      ++end;
      break;
    }
    else
      --end;
  }

  if (end >= start)
  {
    return end;
  }
  else
    return NULL;
}

int main()
{

  while (1)
  {
    printf("[root@localhost myshell]# ");
    fflush(stdout);
    memset(cmd_line, '\0', sizeof cmd_line);

    if (fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
      continue;

    // ls -l -a\n\0 去除\n
    cmd_line[strlen(cmd_line) - 1] = '\0';
    char *ret = Checkredir(cmd_line);

    // 命令行解析: "ls -l -a\0\0" -> "ls" "-l" "-a"
    // strtok能做到此功能
    // 将解析出来的命令以及参数一个一个的放进指针数组中


    while (g_argv[index++] = strtok(NULL, SEP))
      ; // 放入NULL表示还要继续解析上一个解析的字符串

    // 开始让子进程用进程替换执行用户输入的命令:
    pid_t id = fork();
      //先判断是否有重定向
    if (id == 0)
    {
      if (ret)
      {
        // 不为空说明是需要重定向
        int fd = -1;
        switch (redir_status)
        {
        case INPUT_REDIR:
          fd = open(ret, O_RDONLY);
          assert(fd != -1);
          dup2(fd, 0);
          break;
        case OUTPUT_REDIR:
          fd = open(ret, O_WRONLY | O_TRUNC | O_CREAT, 0666);
          assert(fd != -1);
          dup2(fd, 1);
          break;
        case APPEND_REDIR:
          fd = open(ret, O_WRONLY | O_APPEND | O_CREAT, 0666);
          assert(fd != -1);
          dup2(fd, 1);
          break;
        default:
          perror("redirect error");
          break;
        }
      }
      // child
      printf("以下功能由子进程进行进程替换所实现!\n");

      // 由于需要调系统程序,因此需要自动搜索环境变量PATH
      execvp(g_argv[0], g_argv);
      exit(1);
      // 执行失败就退出,不会到下面去,和父进程互不干扰
    }

    // father
    // 父进程进行阻塞等待:
    int status = 0;
    pid_t res = waitpid(id, &status, 0);
    if (res > 0)
    {
      printf("wait successfully!! exit code:%d\n", WEXITSTATUS(status));
    }
  }

  return 0;
}

效果展示:

test_red1

stdout和stderr

在Linux基础IO—上中我们学习了默认打开的三个流,分别是stdout,stderr和stdin,这三个流本质上是文件指针,指向的文件分别是外设:显示器与键盘,其中stdout和stderr都是显示器,stdin是键盘,这也就说明,显示器这个文件是被打开了两次的,因为有两个文件指针都指向了他,他们都能向显示器文件做对应的io操作,那他们用法上有什么区别呢?

我们来看代码:

//test: stdout stderr
int main()
{
    //stdout->1
    printf("hello printf 1\n");
    fprintf(stdout,"hello fprintf 1\n");
    //stderr->2
    perror("hello printf 1\n");
    //write->1
    const char*s1="hello write 1\n";
    write(1,s1,strlen(s1));
    //write->2
    const char*s2="hello write 2\n";
    write(2,s2,strlen(s2));

    //cout->1
    std::cout<<"hello cout 1"<<std::endl;
    //cerr->2
    std::cerr<<"hello cerr 2"<<std::endl;
    return 0;
}

编译运行后输出:

image-20230925104102167

不管是1还是2都能在我们的显示器看到,也证明了stdout和stderr对应的都是显示器

下面我们对程序进行重定向,看看会有什么变化:

image-20230925104502898

重定向后我们发现2号文件仍然向显示器打印了,而1号文件就正常的重定向进了文件里

结论:重定向默认是对1号文件描述符进行重定向

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

也就是1号和2号分别打开的显示器是不会影响彼此的,其实就是显示器文件是被打开了两次!

若就想让1号和2号的都往同一个文件重定向:

image-20230925105711518

这样就成功让1号和2号都重定向进了文件

如何使用?

一般而言如果程序运行有可能有问题的话,建议使用strerr或者cerr来打印,如果是常规的内容文本,建议使用stdout,cout来打印。

为什么?

因为在库中,有这样一个函数:image-20230925110612471

errno,被称为错误码,当某个函数调用时失败或者程序出现一些问题时,错误码就会被设置,而使用对应的perror就能向2号stderr中输出被设置的错误码所对应的错误信息,若你不想他直接运行后会在显示器上打印出来,就可以指定2号并重定向进日志文件中,方便管理者查看错误信息。image-20230925110751151

而perror的底层正是调用了strerror函数,这个函数在上篇有介绍过,不再细说。

void my_perror(const char* msg)
{
    fprintf(stderr,"%s",msg,strerror(errno));
}

FILE

我们在Linux基础io—上提到过,C语言所提供的文件操作函数,类似于fopen之类的函数,他们对文件的操作是需要用到文件指针FILE的,而我们现在已经学习了系统调用接口是通过文件描述符实现进程与打开文件建立联系的,而FILE文件指针底层正是封装了文件描述符才得以使用,本质上都是对系统接口进行二次封装提高可用性

Linux的设计哲学正是一切皆文件,体现在操作系统的设计软件层面,但其实Linux的底层也是C语言写的。

而我们学习过,文件正是=文件内容+文件属性,想用C语言实现这种具有面向对象思想的模块,只有一个东西能实现,那就是结构体struct。

而面向对象思想中,除了封装有有成员的属性,还有成员方法,但结构体如何实现成员方法呢?可以用函数指针来建立成员方法与成员的联系。

现在我们可以来看一下我们一直所说的文件指针FILE*,其指向的FILE底层究竟是如何实现的:

image-20230921162643605

我们查文档后发现,FILE其实是结构体struct _IO_FILE的别名:

image-20230921162845534

再转到_IO_FILE文件下,我们发现了有一个名为 _fileno的整型,这个整型其实就是我们熟悉的文件描述符fd,而不仅包含了fd,它还包含了该文件fd对应的语言层面的缓冲区结构!!

既然这样,由于底层各式各样的硬件,他们对应的操作方式都不同,但是他们的核心方式都是系统接口,也就是外设想要访问系统亦或是系统想要访问外设,在底层都是通过read,write,open,close来完成的,而这些硬件又被描述组织成文件被封装进了FILE中,这样一来,系统看待文件的方式都统一成为了管理结构体

缓冲区

什么是缓冲区

缓冲区其实就是用户自己提供的一块内存空间——char buffer[SIZE]

他的作用就是用来提高整机效率,提高用户的响应速度

我们知道文件是存在磁盘上的,若每次我们进行文件操作时都要直接往磁盘里加载,他的速度是非常慢的且效率不高,因此我们通过内存来过度,等到必要的时候再统一写入磁盘,这样减少频繁的访问磁盘,明显加速系统速度,这种模式叫做写回模式(write-back)

那缓冲区写入磁盘的时机又是什么时候呢?也就是缓冲区的刷新策略是什么?

缓冲区刷新策略:

  • 立即刷新
  • 行刷新(行缓冲)-‘\n’
  • 满刷新(全缓冲)

特殊情况:

  • 用户强制刷新(fflush)
  • 进程退出

我们来看一个简单的例子:

int main()
{ 
  printf("hello Linux!");
  sleep(5); 
  return 0;
}

按照代码逻辑来看,执行情况应该是先打印出来,再停留五秒后退出

但实际执行并不是这样,而是停留了五秒后再打印出来然后退出

其实这就是C语言库提供的缓冲区,若没有达到缓冲区的刷新要求,字符串会暂时保存起来,最后进程要退出了才会达到要求然后刷出来,若需要先刷出来再停留五秒的效果,则需要加上‘\n’告诉缓冲区需要刷新了或者fflush强制刷新

了解完基本概念,我们来看一段奇怪的代码:

// test:buffer:缓冲区
int main()
{
    const char *msg0 = "hello printf\n";
    const char *msg1 = "hello fwrite\n";
    const char *msg2 = "hello write\n";
    printf("%s", msg0);
    fwrite(msg1, strlen(msg0), 1, stdout);
    write(1, msg2, strlen(msg2));
    fork();
    return 0;
}

运行结果为:

image-20230921172237920

正常按照顺序打印一次,没毛病,符合预期。但我们稍微改动一下,我们运行程序后,将其重定向到文件中:

image-20230921173110863

这就奇了怪了,同一份代码,只是改变了输出方向,怎么就有不同的结果呢?

我们还观察到,fwrite和printf还出现了两次,而write只出现了一次,怎么就这么巧了?

初步推断:

出现两次可以初步推断这个结果跟程序最后的fork()有关,而write系统调用接口只有一次,其他接口都是两次可以推断罪魁祸首并不会影响系统接口!!!

两个问题:

  1. 为什么会打印两次?
  2. 为什么write系统接口只打印了一次?

为什么会打印两次?

我们注意到,我们在打印时,是已经使用了缓冲区的刷新策略—行刷新策略,那么当执行到fork的时候,这时候打印函数已经执行完毕了,按照正常来说,已经是将数据从缓冲区刷出来的,但实际上确并没有刷新!也就是说,执行到fork函数的时候,数据仍然存在C语言库管理的缓冲区中,并且这部分数据是属于父进程的数据。

  • 那么为什么会没有刷新呢??这不已经达到刷新策略的要求了吗?

我们需要注意到,我们这次运行时作了不同的动作,也就是进行了输出重定向!

而我们的输出重定向,本质就是将向显示器上打印的东西,转而写入了存储在磁盘上的文件!这时候隐形中的缓冲区刷新策略就改变了,变成了全缓冲,那么对应的行刷新也就没有作用了!

让我们接着来解释为什么会打印两次的问题:

存在缓冲区中的父进程的数据,在执行到fork()函数时,子进程采用写时拷贝的方法来保持进程间的独立性,父子进程共享一份代码和数据,但一旦父子进程的代码和数据需要被修改时,子进程会在物理内存中拷贝一份父进程的数据,并修改自己所持有的数据。真相渐渐水落石出了:

  • 父进程刷新缓冲区,本质上是把数据写入系统的过程,也就是写的过程。会发生写时拷贝

  • 父进程在刷新缓冲区前,由于子进程的存在,为了不让父进程的刷新影响到子进程,子进程也要拷贝一份一模一样的父进程缓冲区中的数据,因此有两份数据出现了,若提前强制刷新,缓冲区没数据了,子进程自然也就拷贝不到了

为什么write系统接口只打印了一次?

  • 因为我们所谈的缓冲区,是C标准库维给我们提供的用户及缓冲区,是属于语言层面的,跟系统层面的不是一个东西,当然不会影响系统接口!但是write在系统内核中有内核对应的缓冲区,也并非直接写到磁盘上的!

那么我们所说的用户级缓冲区他又在哪里呢??

  • 在我们前面提到过的FILE结构体,也就是_IO_FILE中,其内部不仅封装了文件描述符,还封装了缓冲区的结构!

我们调用fflush接口时,我们只需要传文件指针,那么他又是怎么知道缓冲区在哪里的,要去哪里刷新呢??

  • *fflush中传的参数是FILE ,FILE 指向的FILE结构体内封装有缓冲区结构!

缓冲区总结

  • 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。

  • printf fwrite 库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。

  • 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后

  • 但是进程退出之后,会统一刷新,写入文件当中。

  • 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。

  • write 没有变化,说明没有所谓的缓冲

综上:printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。

模拟实现缓冲区

关于我们对缓冲区的现理解,我们简单的模拟一下打开文件后对应分配到的缓冲区的结构,增加理解:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

typedef struct MYFILE_ MYFILE;

struct MYFILE_
{
    int _fd;
    char buffer[1024];
    int end; // 缓冲区结尾
};

MYFILE *fopen_(const char *pathname, const char *mode)
{
    assert(pathname && mode);
    MYFILE *fp = NULL;
    if (strcmp(mode, "r") == 0)
    {
    }
    else if (strcmp(mode, "r+") == 0)
    {
    }
    else if (strcmp(mode, "w") == 0)
    {
        int fd = open(pathname, O_WRONLY | O_CREAT | O_TRUNC, 0666);
        if (fd >= 0)
        {
            //创建文件时会分配缓冲区
            fp = (MYFILE *)malloc(sizeof(MYFILE));
            memset(fp, 0, sizeof(MYFILE));
            fp->_fd = fd;
        }
    }
    else if (strcmp(mode, "w+") == 0)
    {
    }
    else if (strcmp(mode, "a") == 0)
    {
    }
    else if (strcmp(mode, "a+") == 0)
    {
    }
    else
    {
        // nothing
    }
}
void fputs_(const char *message, MYFILE *fp)
{
    assert(message);
    assert(fp);

    // hellofputs\0
    // fputs\0
    strcpy(fp->buffer + fp->end, message);
    fp->end += strlen(message);

    // 刷新策略
    if (fp->_fd == 0)
    {
        // stdin
    }
    else if (fp->_fd == 1)
    {
        // stdout
        if (fp->buffer[fp->end - 1] == '\n')
        { 
            write(fp->_fd, fp->buffer, fp->end);
            fp->end = 0;
        }
    }
    else if (fp->_fd == 2)
    {
        // stderror
    }
}
void fflush_(MYFILE *fp)
{
    assert(fp);
    if(fp->end!=0)
    {
      write(fp->_fd,fp->buffer,fp->end);
        //向磁盘中写入
      syncfs(fp->_fd);
      fp->end=0;
    }
}

void fclose_(MYFILE *fp)
{
    assert(fp);
    fflush_(fp);
    close(fp->_fd);
    free(fp);
}

int main()
{
   // close(1):debug
    MYFILE *fp = fopen_("test.txt", "w");
    assert(fp);

    fputs_("one\n", fp);
    sleep(1);
    fputs_("two", fp);
    sleep(1);
    fputs_("three", fp);
    sleep(1);

    fclose_(fp);
    return 0;
}

syncfs函数能够帮助我们将文件磁盘中正式写入

image-20230924211336264

只是简单模拟这个思路,底层真正想做到没有这么简单只为加深理解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1444112.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2024 年 6 款最佳 PDF 编辑器,您可以免费获得

PDF 作为与 Windows、iOS、Linux 和各种其他操作系统兼容的安全文档格式而享有盛誉。这种广泛的兼容性使 PDF 成为一种流行的选择&#xff0c;几乎每个用户都会在不同的环境中遇到 PDF 文件。无论是合同、发票、电子书、信用卡对账单、银行对账单、税务表格还是保险文件&#x…

【芯片设计- RTL 数字逻辑设计入门 番外篇 9 -- SOC 中PL端与PS端详细介绍】

文章目录 Programmable Logic and Processing SystemPL&#xff08;Programmable Logic&#xff09;特点PS和PL之间的协同设计和开发工具 Programmable Logic and Processing System 在系统级芯片&#xff08;SoC&#xff09;的上下文中&#xff0c;“PL” 通常指的是可编程逻…

第二节 zookeeper基础应用与实战

目录 1. Zookeeper命令操作 1.1 Zookeeper 数据模型 1.2 Zookeeper服务端常用命令 1.3 Zookeeper客户端常用命令 1.3.1 基本CRUD 1.3.2 创建临时&顺序节点 2. Zookeeper JavaAPI操作 2.1 Curator介绍 2.2 引入Curator 2.3 建立连接 2.4 添加节点 2.5 修改节点 …

Blazor SSR/WASM IDS/OIDC 单点登录授权实例3-服务端管理组件

目录: OpenID 与 OAuth2 基础知识Blazor wasm Google 登录Blazor wasm Gitee 码云登录Blazor SSR/WASM IDS/OIDC 单点登录授权实例1-建立和配置IDS身份验证服务Blazor SSR/WASM IDS/OIDC 单点登录授权实例2-登录信息组件wasmBlazor SSR/WASM IDS/OIDC 单点登录授权实例3-服务端…

导数的定义【高数笔记】

【含义】可以抽象成&#xff0c;在一个极其短的时间段内&#xff0c;温度差 / 时间差 【本质】瞬间的平均值 【分类】可以分成几类&#xff1f;每类需要注意的点 【导数存在的必要条件】 【导数与极限的关系】可以参考导数的定义的式子 【题型解法】分几个题型&#xff1f;每个…

【MySQL进阶之路】生产案例:每一个月左右MySQL就会出现性能抖动问题

欢迎关注公众号&#xff08;通过文章导读关注&#xff1a;【11来了】&#xff09;&#xff0c;及时收到 AI 前沿项目工具及新技术的推送&#xff01; 在我后台回复 「资料」 可领取编程高频电子书&#xff01; 在我后台回复「面试」可领取硬核面试笔记&#xff01; 文章导读地址…

Linux第45步_通过搭建“DNS服务器”学习图形化配置工具

学习的意义&#xff1a;通过搭建“DNS服务器”&#xff0c;来学习“图形化配置工具”。“DNS服务器”&#xff0c;我们用不到&#xff0c;但为后期移植linux系统服务&#xff0c;因为在移植系统时&#xff0c;需要用到这个“图形化配置工具”。 1、“menuconfig图形化配置工具…

贾玲的腹肌,你也可以拥有

​​​​​​​ 贾玲的腹肌&#xff0c;你也可以拥有 大年初一&#xff0c;有学员来给顾问老师拜年&#xff0c;聊起了现在春节档热门电影&#xff0c;贾玲导演的第二部作品《热辣滚烫》&#xff0c;也聊起了她瘦身100斤后的模样。 学员&#xff1a;贾玲瘦了 100 斤&#xff0…

c语言中的隐式类型转换

数据类型转化 我们在实际编程中&#xff0c;不管你是有意的还是无意的&#xff0c;有时候都会让两个不同类型的数据参与运算&#xff0c;编译器为了能够生成CPU可以正常 执行的指令&#xff0c;往往会对数据做类型转换&#xff0c;将两个不同类型的数据转换成同一种数据类型。…

C++重新入门-循环

目录 1.循环类型 while循环&#xff1a; for循环 基于范围的for循环(C11) do...while 循环 2.循环控制语句 3.无限循环 有的时候&#xff0c;可能需要多次执行同一块代码。一般情况下&#xff0c;语句是顺序执行的&#xff1a;函数中的第一个语句先执行&#xff0c;接着…

寒假 day10

1、请使用递归实现n! #include<stdio.h> #include<string.h> #include<stdlib.h>int fun(int m) {if(m0)return 1;else{return m*fun(m-1);} } int main(int argc, const char *argv[]) {int m;printf("please enter m:");scanf("%d",…

视觉SLAM十四讲学习笔记(二)三维空间刚体

哔哩哔哩课程连接&#xff1a;视觉SLAM十四讲ch3_哔哩哔哩_bilibili​ 目录 一、旋转矩阵 1 点、向量、坐标系 2 坐标系间的欧氏变换 3 变换矩阵与齐次坐标 二、实践&#xff1a;Eigen&#xff08;1&#xff09; 运行报错记录与解决 三、旋转向量和欧拉角 1 旋转向量 …

OpenCV-35 查找轮廓

一、 什么是图像轮廓 图像轮廓是具有相同颜色或灰度的连续点的曲线&#xff0c;轮廓在形状分析和物体的检测识别中很有用。 用于图形分析物体的识别和检测 注意点&#xff1a; 为了检测的准确性&#xff0c;需要先对图像进行二值化或Canny操作。画轮廓时会修改输入的图像&a…

fast.ai 深度学习笔记(六)

深度学习 2&#xff1a;第 2 部分第 12 课 原文&#xff1a;medium.com/hiromi_suenaga/deep-learning-2-part-2-lesson-12-215dfbf04a94 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 来自 fast.ai 课程的个人笔记。随着我继续复习课程以“真正”理解它&#xff0c;…

【EAI 016】VIMA: General Robot Manipulation with Multimodal Prompts

论文标题&#xff1a;VIMA: General Robot Manipulation with Multimodal Prompts 论文作者&#xff1a;Yunfan Jiang, Agrim Gupta, Zichen Zhang, Guanzhi Wang, Yongqiang Dou, Yanjun Chen, Li Fei-Fei, Anima Anandkumar, Yuke Zhu, Linxi Fan 作者单位&#xff1a;Stanfo…

什么是ROAS以及它如何衡量广告活动的有效性

有没有想过您的广告活动效果如何&#xff1f;想想 ROAS&#xff0c;即广告支出回报率。ROAS衡量的是每花一美元广告所产生的收入。虽然 ROAS 是一个强大的指标&#xff0c;可以为我们提供丰富的见解&#xff0c;但不应孤立地考虑它。本文将带你了解什么是 ROAS 以及它如何衡量广…

Qt中程序发布及常见问题

1、引言 当我们写好一个程序时通常需要发布给用户使用&#xff0c;那么在Qt中程序又是如何实现发布的呢&#xff0c;这里我就来浅谈一下qt中如何发布程序&#xff0c;以及发布程序时的常见问题。 2、发布过程 2.1、切换为release模式 当我们写qt程序时默认是debug模式&#x…

【制作100个unity游戏之23】实现类似七日杀、森林一样的生存游戏17(附项目源码)

本节最终效果演示 文章目录 本节最终效果演示系列目录前言制作木板UI直接复制和工具一样的即可检查背包是否有指定数量的空插槽 源码完结 系列目录 前言 欢迎来到【制作100个Unity游戏】系列&#xff01;本系列将引导您一步步学习如何使用Unity开发各种类型的游戏。在这第23篇…

解决Typora导出HTML不显示图片

解决Typora导出HTML不显示图片 产生原因 Typora导出HTML不显示图片&#xff0c;可能时图片存放在我们的硬盘中。 我们可以将markdown中的图片转化为base64格式&#xff0c;嵌入到html中。 解决步骤 首先&#xff0c;下载 TyporaToBase64.jar 密码:45jq 其次&#xff0c;将…

【数据库】索引的使用

【数据库】索引的使用 前言出发示例创建表Explain 查看sql执行计划where 查询解析无索引有索引 where oderBy 查询解析无索引有索引 总结 前言 在数据库设计过程中&#xff0c;常需要考虑性能&#xff0c;好的设计可以大大提高sql 语句的增删改查速度。在表的创建过程中&…