我想知道只是将未初始化的变量传递给函数会导致未定义的行为吗?
对我来说这似乎很奇怪。
假设我们有以下代码:
void open_db(db* conn)
{
// Open database connection and store it in the conn
}
int main()
{
db* conn;
open_db(conn);
}
这对我来说似乎完全合法。它没有取消引用未初始化的变量,也没有对其状态进行中继。它只是将一个未初始化的指针传递给另一个函数,该函数通过operator new
或类似的东西在其中存储一些数据。
如果是UB,你能引用标准所说的确切位置吗?
对于其他类型如int
?
void foo(int bar)
{
// ...
}
int main()
{
int bar;
foo(bar); // UB?
}
答案 0 :(得分:4)
是UB,参数的类型无关紧要。 C99的相关位是:当您使用"自动存储持续时间"声明变量时。但是不要初始化它,它的值是不确定(6.2.4p5,6.7.8p10);任何使用不确定值的行为都会引起不确定的行为(J.2指的是6.2.4,6.7.8和6.8) 1 。
即使它不是UB(例如,如果conn
已初始化),这段代码也不会产生您希望它具有的效果。如上所述,open_db
无法修改其调用者中的变量 conn
。
无论conn
是否已初始化,代码的细微变化均有效,并且确实按照您的预期执行操作:
void open_db(db **conn)
{
*conn = internal_open_db();
}
int main()
{
db *conn;
open_db(&conn);
}
address of of 运算符,一元&
,是语言中为了应用于而不是引发未定义行为的极少数事情之一未初始化的变量,因为它不读取变量的值。它仅确定变量的内存位置。这是一个确定的值,可以安全地传递给open_db
(但请注意其类型签名已更改:它现在正在接收指向db
指针的指针。open_db
现在可以使用指针解除引用运算符(一元*
)将结果写入变量。
仅在C ++中,这种非常常见的模式会获得一些语法糖:
void open_db(db *&conn)
{
conn = internal_open_db();
}
int main()
{
db *conn;
open_db(conn);
}
将第二颗星更改为&符使conn
参数open_db
现在成为"参考"指针。它仍然是一个指针指针#34;但是编译器会根据需要为你填写&
和*
运算符。
1 对于我的同伴语言律师:附件J是非规范性的,我无法找到任何规范性声明,支持其断言使用不确定的值总是< / em> UB。 (如果我能找到一个关于它意味着什么的定义可能会有所帮助&#34;首先使用一个值&#34;我认为意图是任何触发6.3.2.1p2&#34;左值转换&#34的东西但是,我并不认为实际上曾经说过。)
&#34;不确定值的定义&#34;是&#34; 未指定的值或陷阱表示&#34 ;;使用未指定的值不会激活UB。使用陷阱表示确实会激发UB,但并非所有类型都有陷阱代表。 C11,但不是C99,在6.3.2.1p2中有一个句子,如果[代码读取一个值]自动存储持续时间的对象可以用寄存器存储类声明(从未使用过其地址),并且该对象未初始化,行为未定义&#34; - 但请注意,它没有使用术语“不确定的价值”#34;这里,它将规则限制为不采用地址的变量。
然而,C编译器绝对会将任何未初始化的变量视为UB,无论其类型是否具有陷阱代表或其地址是否已被采用,J.2肯定反映了委员会的意图,一些例子也是如此在第7条中,&#34;不确定&#34;单独出现 指出读取某个变量是UB。
答案 1 :(得分:2)
这会产生警告:
int a;
int b = a; //warning, a is uninitialized, but is USED to initialize b, UB
由于使用带有Uspecified Value的变量是UB,因此将未初始化的变量按值传递给函数会产生相同的结果,因为它涉及复制 - 但编译器不会进行任何检查,因此不会生成警告。
如果这不是一个惊人的例外,那肯定是未定义的行为。
答案 2 :(得分:1)
在C中:This thread涵盖了与阅读未初始化变量相关的所有问题,它有点复杂。
通过值将变量传递给函数显然需要读取它。
对于C ++ 14,请参阅this answer。
两种语言中的两个代码示例都是未定义的行为。
答案 3 :(得分:0)
在你给出的例子中
void open_db(db* conn)
{
// Open database connection and store it in the conn
}
int main()
{
db* conn;
open_db(conn);
}
conn
中的变量main
是未初始化的指针 。
然后将其副本传递给open_db
。您没有传递指针的地址,而是将未初始化的值作为地址传递。
这需要读取未初始化的地址,以填充db_conn
中使用的副本。
编译器可以自由识别这一点,并执行具有潜在未定义行为结果的读取(程序执行此类读取可能会崩溃)或编译器可能会忽略该副本并让db_conn
s conn
参数的定义不同。
根据我读过的其他评论,我相信你会想要聪明地说'#A;啊哈!但是我总是在conn
内初始化db_conn
,如果没有初始化它就永远不会读它&#34;。
好的......那是......反常。
void db_conn(db* conn)
{
db* new_conn = db_connection_helper();
if (!new_conn) {
log_error("Couldn't open database");
return;
}
log_success("Opened database");
conn = new_conn;
configure_db_connection(conn); // first read: guaranteed initialized
setup_stored_procedures(conn);
}
在此函数中,conn
按值传递,因此conn
是传递给我们的任何参数的副本。在db_conn
正文中对其进行的任何分配对调用者都是不可见的。
实际上,优化器很可能会对此代码进行处理
conn = new_conn;
configure_db_connection(conn); // first read: guaranteed initialized
setup_stored_procedures(conn);
作为
configure_db_connection(new_conn); // first read: guaranteed initialized
setup_stored_procedures(new_conn);
我们可以很容易地看到这个in the assembly
typedef struct db_t {} db;
extern db* db_conn_helper();
extern void db_configure(db*);
void db_conn1(db* conn)
{
db* new_conn = db_conn_helper();
if (!new_conn)
return;
conn = new_conn;
db_configure(conn);
}
void db_conn2(db* conn)
{
db* new_conn = db_conn_helper();
if (!new_conn)
return;
db_configure(new_conn);
}
产生
db_conn1(db_t*):
subq $8, %rsp
call db_conn_helper()
testq %rax, %rax
je .L1
movq %rax, %rdi
addq $8, %rsp
jmp db_configure(db_t*)
.L1:
addq $8, %rsp
ret
db_conn2(db_t*):
subq $8, %rsp
call db_conn_helper()
testq %rax, %rax
je .L5
movq %rax, %rdi
addq $8, %rsp
jmp db_configure(db_t*)
.L5:
addq $8, %rsp
ret
因此,这意味着如果您的代码尝试在main中使用db
,那么您仍然会看到未定义的行为:
int main() {
db* conn; // uninitialized
db_conn(conn); // passes uninitialized value
// our 'conn' is still uninitialized
query(conn, "SELECT \"undefined behavior\" FROM DUAL"); // UB
}
也许你的意思是
conn = db_conn(); // initializes conn
或
db_conn(&conn); // only undefined if db_conn tries to use *conn
这需要db_conn
来db**
。
db* conn => uninitialized
db** &conn => initialized pointer to an uninitialized db* pointer