在数组的最后一个元素之后将 '\0' 放入 char[] 是否安全?

Is it safe to put '\0' to char[] one after the last element of the array?

出于一些实际原因,我很感兴趣。我知道 C++ 在最后一个元素之后添加 '[=11=]',但是手动放置它安全吗?我听说过 undefined behavior,但是我很想知道 NULL 字符是否真的是内存中的下一个符号?

UPD:我明白了,没有代码片段我的问题不够清楚。 所以,我问这是否真的保存并且不会导致未定义的行为?

const char* a = "Hello";
a[5] = '[=10=]'; //this is wrong, will not compile on most compilers

char* b = new char[5];
b[5] = '[=10=]';

char c[5];
c[5] = '[=10=]';

您似乎混淆了一些细节。

首先,看一下这段代码(C++)的输出:

#include <iostream>

int main()
{
    char x[] = {'a','b','c'};
    std::cout << sizeof(x);
}

它打印 3,因为 x 是一个包含 3 个元素的数组。此处不会自动添加任何内容。尝试在 x[3] 处“添加”空终止符是不可能的,因为该数组只有 3 个元素(最后一个有效索引是 2)。尝试访问 x[3] 是未定义的行为。

我想你指的是 string literals。他们确实附加了 [=20=]

The null character ('[=33=]', L'[=33=]', char16_t(), etc) is always appended to the string literal: thus, a string literal "Hello" is a const char[6] holding the characters 'H', 'e', 'l', 'l', 'o', and '[=33=]'.

但是,这并不意味着您不能在字符串文字中插入空终止符。它只是不太实用,因为它破坏了空终止符终止字符串 (*):

的目的
#include <iostream>

int main()
{
    const char* x = "H[=11=]ello";
    std::cout << x << "\n";
    for (int i=0;i<7; ++i) std::cout << x[i];
}

Output is:

H
Hello

(空终止符不可打印)。

(*) 或者更确切地说:它是第一个终止字符串的 [=20=]。任何需要空终止字符串的函数都将在遇到第一个 [=20=] 时停止。


关于问题的更新....

const char* a = "Hello";
a[5] = '[=13=]';

字符串文字 "Hello" 的类型为 const char[6]。它已经有空终止符 space 并且 a[5] 已经等于 [=20=]。您不能将 [=20=] 分配给最后一个元素,因为字符串文字是常量,您无法修改它们。实际上该行甚至无法编译,因为 aconst char*.

另外两个

char* b = new char[5];
b[5] = '[=14=]';

char c[5];
c[5] = '[=14=]';

属于未定义行为,因为你正在越界访问数组。具有 5 个元素的数组中的最后一个有效索引是 4。


更新后很明显你的误会是空终止符是在什么时候添加的。这不是 char 数组的特性,但是当你写 "foo" 时,它的类型是 const char[4],已经有足够的 space 用于字符串 and 空终止符。

I know C++ adds '[=15=]' after the last element

这不是真的;至少缺少上下文,或者最坏的情况是误解。 字符串文字 末尾有一个隐式空终止符。字符串文字的隐式空终止符是数组的一部分。虽然它是字符串的最后一个字符 after,但它不是数组的最后一个元素 after;终止符 数组的最后一个元素。例如,字符串文字 "" 是一个包含一个元素(空终止符)的数组,而字符串文字 "a" 是一个包含两个元素('a' 和空终止符)的数组。 "[=14=]" 是一个包含两个元素(两个空终止符)的数组。

除字符串文字外,没有其他数组具有隐式空终止符。

Is it safe to put '[=16=]' to char[] one after the last element of the array?

在数组边界之外读取或写入内存的行为未定义。它与安全相反。


const char* a = "Hello";
a[5] = '[=10=]';

不允许通过指向 const 的指针进行赋值。这个程序是病式的,如果编译器拒绝接受这个程序是安全的。

char* b = new char[5];
b[5] = '[=11=]';
char c[5];
c[5] = '[=11=]';

索引 5 超出数组范围。行为未定义。很不安全。

在 C++ 中,如果声明一个由字符串文字初始化的字符数组,并且明确指定数组中元素的数量,则还需要考虑隐式存在于中的终止零字符“\0”字符串文字。例如

char s[6] = "Hello";
      ^^^

这个声明等同于

char s[6] = { 'H', 'e', 'l', 'l', 'o', '[=11=]' };

在 C 语言中,声明字符数组时可以忽略字符串文字的终止零字符。例如

char s[5] = "Hello";
      ^^^

这个声明等同于

char s[5] = { 'H', 'e', 'l', 'l', 'o' };

但在这种情况下,这样的字符数组不包含字符串。因此,例如在函数中使用这样的数组就像

puts( s ):

或者在大多数标准字符串函数中调用未定义的行为。

当然,如果您想通过字符串文字初始化数组并保证数组将包含字符串,则可以在不明确指定数组中元素数量的情况下声明数组。

char s[] = "Hello";

这个声明等同于

char s[] = { 'H', 'e', 'l', 'l', 'o', '[=16=]' };

并且声明的数组将恰好包含 6 个元素。

编辑: 在您将问题附加到此代码段后

const char* a = "Hello";
a[5] = '[=17=]';

char* b = new char[5];
b[5] = '[=17=]';

char c[5];
c[5] = '[=17=]';

那么你不能像这样使用指向对象的常量指针来更改对象

const char* a = "Hello";
a[5] = '[=18=]';

此外,如果您要删除限定符 const(在 C 中,字符串文字具有与 C++ 相反的非常量字符数组类型)

char* a = "Hello";

但是您不能更改字符串文字。尝试更改 C 和 C++ 中的字符串文字会导致未定义的行为。

在这部分代码片段中

char* b = new char[5];
b[5] = '[=20=]';

char c[5];
c[5] = '[=20=]';

您正在尝试访问超出分配数组的内存,因为索引的有效范围是 [0, 4],这再次调用未定义的行为。

你可以这样写

char* b = new char[5]();

char c[5] = {};

在这种情况下,数组将被零初始化。

或者你可以这样写

char* b = new char[5];
b[0] = '[=22=]';    // that is the same as *b = '[=22=]';

char c[5];
c[0] = '[=22=]';    // that is the same as *c = '[=22=]';

在这两种情况下,数组都将包含空字符串。

片段

const char* a = "Hello";
a[5] = '[=10=]';

甚至不编译;不是因为索引 5 超出范围,而是因为 a 被声明为指向常量内存。 “指向常量内存的指针”的意思是“我声明我不想写它”,所以语言和编译器都禁止它。

注意const的主要作用是声明程序员的意图。实际上,您是否可以写入它取决于。在您的示例中,尝试 - 在 const 转换之后 - 会使您的程序崩溃,因为现代编译器将字符文字放入只读内存中。

但请考虑:

#include <iostream>
using namespace std;

int main()
{
    // Writable memory. Initialized with zeroes (interpreted as a string it is empty).
    char writable[2] = {};

    // I_swear_I_wont_write_here points to writable memory 
    // but I solemnly declare not to write through it.
    const char* I_swear_I_wont_write_here = writable; 
    cout << "I_swear_I_wont_write_here: ->" << I_swear_I_wont_write_here << "<-\n";

    // I_swear_I_wont_write_here[1] = 'A'; // <-- Does not compile. I'm bound by the oath I took.

    // Screw yesterday's oaths and give me an A.
    // This is well defined and works. (It works because the memory
    // is actually writable.)
    const_cast<char*>(I_swear_I_wont_write_here)[0] = 'A';

    cout << "I_swear_I_wont_write_here: ->" << I_swear_I_wont_write_here << "<-\n";
}

声明一些东西 const 只是宣布你不想通过它来写;这并不意味着相关内存确实是不可写的,程序员可以自由地忽略声明,但必须通过强制转换明确地这样做。反之亦然,但不需要强制转换:欢迎您声明并坚持“此处无意写作”,不会造成任何伤害。