c++ 为什么字符数组的最后一个字符被排除?

azpvetkf  于 2023-01-06  发布在  其他
关注(0)|答案(3)|浏览(167)
#include<iostream>
using namespace std;

int main()
{

    int n;
    cin>>n;
    cin.ignore();

    char arr[n+1];
    cin.getline(arr,n);
    cin.ignore();
    
    cout<<arr;

    return 0;
}
    • 输入:**

11
年度

    • 输出:**

年的
我已经为空字符提供了n +1,那么为什么最后一个字符被排除了呢?

ulydmbyx

ulydmbyx1#

您为数组分配了n+1个字符,但随后您告诉getline只有n个字符可用。

int n;
cin>>n;
cin.ignore();

char arr[n+1];
cin.getline(arr,n+1);  // change here
cin.ignore();
cout<<arr;
lmyy7pcs

lmyy7pcs2#

Per cppreference.com:
https://en.cppreference.com/w/cpp/io/basic_istream/getline
表现为 * UnformatedInputFunction 。构造并检查sentry对象后,从*this中提取字符,并将它们存储在s指向其第一个元素的数组中的连续位置,直到发生以下任一情况(按所示顺序测试):
1.输入序列中出现文件结束条件(在这种情况下,执行setstate(eofbit)
1.下一个可用字符c是由Traits::eq(c, delim)确定的分隔符。分隔符被提取(与basic_istream::get()不同)并向gcount()计数,但不被存储。
1.
* 已提取count-1个字符(在这种情况下,将执行setstate(failbit))。**
如果函数未提取字符(例如,如果count < 1),则执行setstate(failbit)

    • 在任何情况下,如果count > 0,则将空字符Chart()存储到数组的下一个连续位置,并更新gcount()。**

在您的例子中,n=11。您分配了n+1(12)个字符,但告诉getline()只有n(11)个字符可用,因此它只将n-1(10)个字符读入数组,然后在第11个字符中以'\0'结束数组。这就是为什么缺少最后一个字符的原因。

of the year
         ^
        10th char, stops here

当调用getline()时,您需要+1,以匹配您的实际数组大小:

cin.getline(arr,n+1);
sxpgvts3

sxpgvts33#

john的回答应该可以解决你的问题。Variable-length arrays(你的char arr[n+1])不是C++标准for justified reasons的一部分。然而,我花了几个小时的时间来超越问题的范围,创建...

C++ I/O学员指南

...和I/O,重点是I部分。不用担心,用C的方式来做!下面的代码片段应该用符合标准的C编译器来编译。

C++ I/O和标准库

文本输入

这是在C++中阅读UTF-8 encoded strings的推荐方法,UTF-8 encoded strings是最广泛使用的文本编码,我们将使用std::string进行存储,这是保存UTF-8编码字符串的实际方法,而std::getline用于读取本身。

#include <iostream> // std::cin, std::cout, std::ws
#include <string>   // std::string, std::getline

int main() {
    int size;
    // std::ws ignores all whitespace in the stream,
    // until the first non-whitespace character.
    // it's prettier and handles cases a simple .ignore() does not.
    std::cin >> size >> std::ws;

    std::string input;
    std::getline(std::cin, input);

    // This condition will most certainly be true (output will be 1).
    std::cout << (size == input.size()) << '\n';
}

std::string是动态分配的,或者你可能听说过,on the heap。这是一个很宽泛的主题,所以从这个给定的起点开始,你可以自由地冒险!这对我们有什么帮助?我们可以在堆上提前存储未知大小的字符串,因为我们总是可以重新分配一个更大的缓冲区!std::getline在读取输入时进行分配和重新分配,直到到达换行符。这样你就可以在不知道size的情况下阅读了。你的size变量很可能等于字符串的大小,假设这是一个学校练习,因为你可能没有学习过动态内存,所以输入长度是提供的。尽管有很好的理由--它很复杂,而且会不必要地分散对实际主题的注意力(算法、数据结构等)。请记住:std::string与C样式字符串不同,它不是以空结尾的,但您可以通过调用.c_str()方法从std::string获取以空结尾的C样式字符串。

二进制数据

什么是二进制数据?所有不是文本的数据:图像、视频、音乐、2003 MS Word文档(.doc的,等着看what .docx is)和许多其他的。习惯上把二进制存储为 raw bytes,这是表示数字的一种奇特方式。unsigned char是用于表示这些原始字节的C/C类型(C17为此引入了std::byte。为了处理来自二进制输入的数据,我们需要将其存储在内存中的某个地方--要么在堆栈上,要么在堆上。我们可以一次存储整个输入,但二进制文件被认为太大了(而且,实际上,想象一下一部电影的大小),所以我们通常是分块阅读,也就是说,我们一次只读有限的一部分(比如说256个字符,这是我们的 buffer),然后我们继续阅读,直到到达输入的末尾(通常称为end-of-file or, short, EOF)。根据经验,当缓冲区较小且为静态时(不需要调整大小,就像上面的字符串一样),我们可以把它存储在堆栈中。如果不满足这些条件中的任何一个,我们应该注意到 smalllarge 的概念是非常依赖于上下文的-编译器、操作系统、硬件、运行时环境(参见this thread on stack size limitsembedded systems)。您将选择的缓冲区大小也是特定于任务的,所以这里也没有规则。现在让我们看一些代码!

#include <array>   // std::array
#include <fstream> // std::ifstream, std::ofstream

int main() {
    // We open this file in binary mode.
    // The default mode may modify the input.
    std::ifstream input{"some_image.jpg", std::ios::binary};
    // 256 is our buffer size, unsigned char is the array type.
    // This is the C++ way of `unsigned char buffer[256]`.
    std::array<unsigned char, 256> buffer;

    while (input.read(buffer.data(), buffer.size())) {
        // Buffer is filled, do something with it
    }

    // At this point, either EOF is reached or an error occurred.
    if (input.eof()) {
        // Less characters than the buffer's size have been read.
        // .gcount() returns the number of characters read by
        // the last operation.
        const std::streamsize chunk_size = input.gcount();
        // Do something with these characters, as in the loop.
        // Valid range to access in the buffer is [0, chunk_size).
        // chunk_size can be 0, too. In that case, there is no more data
        // to handle.
    } else {
        // Some other failure, handle error.
    }
}

这段代码使用一个256字节的堆栈分配的小缓冲区来阅读文件。std::array通过它的方法使使用变得方便和安全-读取链接的文档!如果我们想使用一个大缓冲区(比如16 MB),我们用std::vector替换std::array

std::vector buffer(1 << 24); // 1 << 24 gives 16MB in bytes

其余的也是一样的。这里你也可以使用std::string,因为std::string并不暗示/强制输入的UTF-8编码。在代码中有一个容易区分二进制数据和文本数据的约定是很有用的。需要注意的是,以较小的块阅读使用较少的空间,但是花费更多的时间-从文件获取字节涉及X1 E12 F1 X和移动盘或电子,当从X1 E13 F1 X或X1 E14 F1 X阅读时,C++的fstream对象已经为你做了缓冲来加快读取速度,这通常是一个非常需要的优化,你会知道这是否会影响你。
另一件需要注意的事情是EOF和错误处理,使用.eof()方法。我们在文本输入检索中省略了错误处理,但是在这里,如果我们不想丢失数据,我们必须这样做。当达到EOF时,通常读取的字节数小于缓冲区大小。所以我们需要一种方法来知道缓冲区中有多少被数据填满了,这就是.gcount()告诉我们的,根据你所做的程序,如果缓冲区被部分填充,您可以将EOF错误视为“意外”错误(.gcount()返回非0值)-例如,根据假定在其之后创建的规则,读取的数据不完整,或者换句话说,在数据应该结束之前到达了文件的结尾。除此之外,EOF是一个条件,即所有文件在完全读取后都在。

C样式I/O

这可能看起来更接近于学校里教的东西。正如我们在上面解释的一般概念,这一节将在编码和代码解释方面更加丰富。我们仍然使用C作为一种语言,所以将使用C版本的C头文件和std命名空间-让下面的代码在C编译器中工作。用<something.h>替换<csomething>头文件,并从类型和函数中删除std::命名空间前缀。

文本输入

在C中,一个C流(std::cin,std::fstream等)的等价物是std::FILEFILE默认情况下是缓冲的,就像C流一样。我们将使用std::fscanf来读取输入的大小,它只是scanf,但它将你从中读取的流作为参数,std::fgets用于读取文本行。

#include <cstdio>  // std::FILE, std::fscanf, std::fgets, stdin
#include <cstring> // std::strcspn

// discard_whitespace does what std::ws did above.
// It consumes all whitespace before a non-whitespace
// character from stream f.
void discard_whitespace(std::FILE* f) {
    char discard;
    // The leading space in the format string
    // tells fscanf to consume all whitespace.
    std::fscanf(f, " %c", &discard);
}

int main() {
    int size;
    // stdin is a macro, doesn't have a namespace,
    // hence no std:: prefix.
    std::fscanf(stdin, "%d", &size);
    // fscanf, like std::cin, doesn't consume whitespace
    discard_whitespace(stdin);

    // Your school exercise will probably have a size limit for the input.
    // We consider it to be 256.
    const int SIZE_UPPER_BOUND = 256;
    // We add some extra bytes so the maximum length input can be accomodated.    
    // 1 is added for the null terminator of C-style strings.
    // The other 2 is because `fgets` will also read the newline,
    // which can be \n or \r\n, depending on OS. See explanation after code.
    char input[SIZE_UPPER_BOUND + 3];

    // The actual read - sizeof gets the size of our input buffer,
    // we don't have to write it twice.
    std::fgets(input, sizeof input, stdin);
    // fgets also reads the newline, unlike `std::getline` or
    // `std::cin.getline` - we have to remove it ourselves.
    input[std::strcspn(input, "\r\n")] = '\0';

    // This condition will be true, as in the C++ example.
    std::fprintf(stdin, "%d\n", std::strlen(input) == size);
}

让我们解开这个换行符删除。std::strcspn查找输入中任何给定字符的第一个位置。我们提供了\r\n,以支持UNIX(1米39英寸1 x)和Windows(\r\n)换行符-是的,它们是不同的,see Wikipedia, on "Newline"。通过添加空终止符,'\0',我们将字符串的结尾移动到换行符所在的位置,如果这是一个学校作业,我们可以假设输入是正确的,所以我们可以使用size + 1代替std::strcspn来删除换行符:

input[size + 1] = '\0';

当我们不知道输入的大小或者输入可能无效时,这个方法就不起作用了,作为一个优化技巧,注意std::strcspn返回的是行长度,在这个例子中,当你不知道大小,但是以后需要它时,你可以把std::strcspn的结果保存在一个变量中,然后用它代替std::strlen

// std::size_t is an unsigned integral type, used to represent
// array sizes and indexes in C/C++
const std::size_t input_size = std::strcspn(input, "\r\n");
input[input_size] = '\0';

你会看到一些人使用0NULL作为终止符。我建议不要这样做--与\0字面量不同,\0字面量是char类型,其他两个变体隐式地转换为char。如果你阅读了链接文档,你会发现NULL甚至是不正确的,根据规范,因为它只用于需要指针的上下文中。
fgets的另一个替代方法是fscanf。不过要小心线程--虽然简单的%s可能会这样做,但它会使您的代码容易受到buffer overflow exploits的攻击。也请参见this StackOverflow thread on disadvantages of scanf。让我们看看(安全的)代码:

std::fscanf(stdin, "%256[^\r\n]s", input);

该数字将输入大小限制为SIZE_UPPER_BOUND[^\r\n]告诉fscanf读取\r\n之前的所有字符。因为fscanf%s动词一起使用会消耗前导空格。fscanf的缺点是,您必须保持输入字符串的大小限制和缓冲区大小同步-除了building the format string dynamically之外,你没有办法动态地指定输入大小,这对于学校作业来说是多余的)。这在更大的代码库中是一个问题,但是对于一个文件,一次学校作业来说,这不是什么大问题,所以你可能更喜欢fscanf而不是fgets,因为它的工作量更小。fscanf也不会读取缓冲区中的换行符。

二进制数据

在C世界中,Cstd::cin.read的等价物是std::fread。代码将类似于C的对应物:

#include <cstdio>

int main() {
    // The second parameter is the file access mode.
    // In this case, it is read (r) binary (b).
    std::FILE* f = std::fopen("some_image.jpg", "rb");
    unsigned char buffer[256];

    std::size_t chunk_size;
    while (chunk_size = std::fread(buffer, sizeof buffer[0], sizeof buffer, f)) {
        // chunk_size == sizeof buffer, do something with the buffer
    }

    if (std::feof(f)) {
        // chunk_size != sizeof buffer, do something with buffer
        // or handle as error
    } else {
        // an error occurred, handle it
    }

    // We need to close the file, unlike in C++, where it is closed automatically.
    std::fclose(f);
}

std::fread的参数非常复杂:我读了文档。其他的一切看起来都非常类似于C的方式,从循环到错误处理。为什么呢?因为它实际上是一样的--我们只是使用了不同的另一个相似之处是C I/O在缺省情况下也被缓冲,就像C的一样。不同的是最后一行--对std::fclose的调用。我们在C代码中没有做任何类似的事情,对吧?不,记住C类有constructorsdestructors,这两个函数分别在变量lifetime的开头和结尾被自动调用,这两个函数允许我们实现RAIItechnique,它将自动执行资源管理(在构造函数中打开文件,在析构函数中关闭). RAII用于std::stringstd::vector(以及其他containerssmart pointers & others).换句话说,std::ifstream的析构函数在main()结束时关闭文件,就像我们在这里手动做的一样。

混合方法(??)

你想把两者结合起来吗?看起来是这样。让我们谈谈缺点:

  • 由于C++ I/O库的构建方式,与C相比,它在使用时更注重性能(一般来说,virtual函数调用和额外函数调用,尤其是当使用<<>>运算符& stream manipulators时,因为这些运算符中的每一个都是函数调用,与使用C库的单个普通函数调用/操作相比)。也请参见this StackOverflow thread on i/ostream speed。C++库也更冗长,尤其是在输出的情况下(听说过"chevrone hell"吗?)

  • C I/O库很容易使用不当/不安全,简洁的缩写命名使代码难以遵循,输出无法扩展以支持自定义类型(这是在C++中使用C风格I/O时的问题)。它还需要非常小心地正确处理动态缓冲区,因为在C中管理堆内存的唯一方法是malloc and free

  • 如果看到std::string的踪迹(我听说过),有些学校可能会惩罚你。

  • 使用C风格的类型(例如,char[N]代替std::array<char, N>)更容易--不需要包含头,因为类型是内置的原语,类型更少。在简短的一次性程序中,如学校的算法练习中,可能会更受欢迎。

记住这些,我们可以看看在读取文本和二进制时如何方便地将两者结合起来!

文本输入

我们将利用C风格类型的简洁性和C++ I/O库的易用性:

#include <iostream>

int main() {
    int size;
    std::cin >> size >> std::ws;

    const int SIZE_UPPER_BOUND = 256;
    char input[SIZE_UPPER_BOUND + 1];

    std::cin.getline(input, sizeof input);
    // Input done, solve the problem.
}

教师们不必为std::stringstd::getline的出现以及你在这个兔子洞里开始使用的所有标准库恶作剧而挠头;你,作为程序员,不必为了读取一个字符串和一个int而清理换行符结尾和记忆晦涩的格式说明符;专注于代码和解决问题,而不必调试输入读取逻辑。永远-它只是工作!

二进制数据

复杂的hierarchical tree of C++'s I/O library types让你感到害怕,干净的汇编输出让你很享受,just like Linus Torvalds,你仍然害怕手动管理内存,所以你选择了这个解决方案:

#include <cstdio>
#include <vector>

int main() {
    // The second parameter is the file access mode.
    // In this case, it is read (r) binary (b).
    std::FILE* f = std::fopen("some_image.jpg", "rb");
    std::vector<unsigned char> buffer(1 << 24);

    std::size_t chunk_size;
    while (chunk_size = std::fread(buffer.data(), sizeof buffer[0], buffer.size(), f)) {
        // use the buffer
    }

    if (std::feof(f)) {
        // handle EOF
    } else {
        // handle error
    }

    std::fclose(f);
}

奇怪的选择,因为你仍然手动管理文件的生命周期。虽然这可能不是最好的例子,但使用C++ RAII容器和C库并不罕见-内存安全是至关重要的。

琐事

你不需要的酷东西:

结论

I/O是CS基本概念、硬件和软件内部工作原理以及C++特性和怪癖的密集结合点。一次尽可能地吸收并专注于重要的东西,确保你建立在坚实的基础之上。

相关问题