从零开始 / 一次只解释眼前的一步

指针:
地址、空值与有效访问。

写下一个星号之前,先找到它要访问的整数。追踪指针保存的位置、目标值与存活时间,再独立完成能处理空值的借用接口。

先备:基础01–055 小节 · 预计共 7 小时阅读、推演与编码合计

7小时是未经真人试学校准的设计估算。可分多次学习,遇到不清楚的地方保留预测、实际结果和疑问,之后再回修教材。手机可读图与做预测,编译需要电脑终端。

这一章怎样学

先读一小段,写下预测,再运行程序。每次只改一个条件,最后关掉示例,从空文件独立写一次。遇到错误,把第一条报错和自己的修复记下来;不用赶着把页面滚到底。

每次分别记录指针指向哪里、目标值是多少、这次访问是否有效。每章学习记录单独保留;本章完成不自动通过M1或G0。

上一章可以借一个名字访问已有整数。现在程序还需要表达“这次没有可借的对象”,或者先查看x,再去查看y。我们怎样把这份选择保存下来?更关键的是:非空地址为什么也不一定能读? 本章先学会分清两种赋值,再判断每一次访问是否有效。

先备是新05:引用与const。五节预计7小时,含推演与编码,尚未经真人试学校准。正常程序只借用已初始化、仍存活的具名int;数值变化限定在−100到100内。不输出具体地址,不做数组算术,不申请动态内存。后面一份局部地址错误程序只读源码与编译诊断,不执行。

06.1 指针对象与取地址 · 预估90分钟

怎样保存“要访问哪一个对象”

你已经会画整数格,也会在格子旁标一个引用名。现在要再画一个格子:它保存的不是那个整数的副本,而是找到目标对象的信息。本节先逐项读声明,再沿这条关系读取或修改整数。

对本章的具名整数x,表达式&x取得x的地址(address)。地址用于指明对象所在的位置;这里不需要知道它在某次运行中的数字。int* p{&x};建立名为p的指针对象(pointer object),它的类型是int*,初值是x的地址。读作“p保存一个指向int的指针值”。x是整数对象,p是另一个保存指针值的对象。

表达式*p沿p保存的关系找到那个int,称为间接访问或解引用(indirection / dereference)。p指向仍活着的x时,读取*p得到x的整数值;*p = 7;把7写入x。这不是把p本身改成整数7。

眼前的写法 在本章如何读
int& r{x}; 已学的引用声明,r给x一个别名
int* p{&x}; 指针声明,p的初值是x的地址
&x 取对象x的地址,&在这里是一个运算符
*p 访问指针当前指向的对象
x * 2 已学的两个整数相乘,和单独放在p前的*不同

同一个符号要结合位置读。声明里的*描述p的类型;表达式里的*要求沿指针找目标。表达式里的&取地址,不能把它读成“这里又声明了引用”。本章每条声明只写一个名字,避免把多个声明挤在一行里。

#include <iostream>

int main() {
    int x{4};
    int* p{&x};
    std::cout << *p << '\n';
    *p = 7;
    std::cout << x << ' ' << *p << '\n';
    return 0;
}

先创建x=4,再创建p并保存x的地址。第一条输出沿p找到x,得到4。赋值*p = 7改的是x,最后按x、*p输出7 7。全程只有一份整数;额外的p是指针对象,不是第二份int。

沿用新04的严格编译选项:保存文件后,可用clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror address.cpp -o address && ./address。本章故意错误的文件只执行编译命令,不接&& ./程序名,也不去启动上次留下的程序。

小练习:只把写入值7改成8,什么发生变化

第一行仍为4,第二行变为8 8。指针仍指向x;变化的是x中的整数。不要把p的格子写成8,也不要把图中的x标签当成真实数字地址。

06.2 改值与改指向 · 预估90分钟

下一次赋值应该擦掉哪一个格子

现在加入第二个整数y。任务可能是更新x,也可能是把观察位置移到y。两者都可以用赋值,但左边的名字决定究竟改动哪一个对象。

*p = 7;写的是目标整数;p = &y;写的是指针对象p,令它保存y的地址,称为改指向(retargeting)。后一个动作不读写x或y中的整数。之后再使用*p,才会沿新的关系访问y。引用r建立后不能改绑,普通指针p则可以保存另一个合适的地址。

完整例一:先更新x,再观察y

#include <iostream>

int main() {
    int x{4};
    int y{9};
    int* p{&x};
    *p = 7;
    p = &y;
    std::cout << x << ' ' << y << ' ' << *p << '\n';
    return 0;
}
执行到哪里 x和y中的整数 p指向谁
x、y创建后 x=4,y=9 p尚未创建
int* p{&x}之后 x=4,y=9 x
*p = 7之后 x=7,y=9 x
p = &y之后 x=7,y=9 y
输出x、y、*p 7 9 9 仍是y
停下来,看一次变化

擦掉整数,还是移动指针的指向

x4y9p尚未创建1 / 5 · 对象与指向x4y9p尚未创建1 / 5
1 · 建立x和y

两个整数对象保存4和9;p还没有建立。图中的对象名不是实际地址数字。

1 / 5
查看所有步骤的文字与数值
  1. 1 · 建立x和y

    x:4;y:9;p:尚未创建

    两个整数对象保存4和9;p还没有建立。图中的对象名不是实际地址数字。

  2. 2 · int* p{&x};

    x:4;y:9;p:指向x

    创建指针对象p并保存x的地址;没有复制x中的整数。

  3. 3 · *p = 7;

    x:7;y:9;p:仍指x

    沿p找到x,把7写入x;p自己的指向保持。

  4. 4 · p = &y;

    x:7;y:9;p:改指y

    现在改的是指针对象p;x保持7,y保持9。

  5. 5 · 输出x、y、*p

    x:7;y / *p:9;输出:7 9 9

    读取当前目标y,得到7 9 9;输出不修改任何对象。

阶梯第一步:预测后只换一个动作

不运行,先画完上表。再只把最后一次赋值p = &y;改为*p = y;,保留其他语句,重新画关系和输出。

提示与答案:赋值左边有没有星号

修改后先把x改7,再把y中的9写入x,p始终指向x。最终输出9 9 9。原程序是把p改指向y,x保持7;修改版是把x改成9,p不换目标。最后两个数都为9不能证明这两个程序做了同一件事。

两个指针也可能借同一个整数

int* q{p};创建独立的指针对象q,从p复制指针值:建立这一刻两者指向同一个x,没有再复制一份x。随后修改p的指向不会自动修改q。只要它们指向同一个仍存活的整数,经其中任一路径改值,另一条路也能观察到新值。这是别名(aliasing)在指针上的情形。

#include <iostream>

int main() {
    int x{4};
    int y{9};
    int* p{&x};
    int* q{p};
    *p = 7;
    p = &y;
    std::cout << x << ' ' << y << ' ' << *q << ' ' << *p << '\n';
    *q = 8;
    std::cout << x << ' ' << y << ' ' << *q << ' ' << *p << '\n';
    return 0;
}

两份整数x、y,加两份指针对象p、q。先经p把x改7,再让p去指y;q仍指x,因此首行按x、y、*q*p输出7 9 7 9。接着*q = 8改x,第二行是8 9 8 9。复制指针保存关系,后续不会让两个指针对象自动绑定在一起。

06.3 可选借用 · 预估90分钟

这次没有对象时,接口怎样表达

有时调用者能交来一个整数,有时没有。本节让同一个函数处理两条路径,并说清什么检查能防止没有目标时仍然取值。

nullptr是专门表示空指针值(null pointer value)的写法。int* p{nullptr};明确初始化p,但没有给它一个可访问的int。它不同于“p指向一个值恰好为0的整数”。指向0的对象仍有目标,解引用得到0;空指针没有这个目标,不能用*p读写。

本章用显式比较p == nullptrp != nullptr得到已学的bool条件。先判断,再决定是否进入含*p的路径。不要先输出*p,然后才补一个判空分支。比较不能替代初始化:不使用int* p;建立一个随后未经赋值就拿来比较的指针。

先限定接口的输入合同:传入nullptr,或者传入一个已初始化、仍存活的int的地址;借用期间该对象一直活着。在这个范围内,判空后即可访问。若调用者传来已失效地址,它已经违反输入合同,判空不会修复它;06.5专门推演这个失败。

完整例二:值与missing

函数形参int* p按值接收一个指针。调用print_if_present(&value)把value的地址交给p;调用print_if_present(nullptr)则把空值交给p。前者没有复制value中的整数,后者也没有创建一个虚构的整数。

#include <iostream>

void print_if_present(int* p) {
    if (p == nullptr) {
        std::cout << "missing" << '\n';
        return;
    }
    std::cout << *p << '\n';
}

int main() {
    int value{6};
    print_if_present(&value);
    print_if_present(nullptr);
    return 0;
}

第一次p保存value的地址,判空结果为false,跳过if块并输出6。第二次p为空,进入if块,输出文本missing并执行已学的return;。函数立即结束,不到达后面的*p。这条提前返回就是本例保护访问的控制流。

阶梯第二步:补上保护访问的那一步

盖住函数体,从空文件写void print_if_present(int* p)。要求空输入输出missing且立即返回,其他符合输入合同的指针输出目标值。main至少测试值为6、值为0和nullptr三种输入。

提示与答案:不要把目标的整数0当作缺失

检查的是p == nullptr。空路径输出missing后return,输出*p放在该路径之后。为三个输入依次调用时应得到6、0、missing三行。把原完整例的value从6改0,只会把第一行改为0,第二行仍是missing。

小练习:一个会运行的错误

下面任务要求“把y当前的整数写入p原来指向的x”。实现能编译,但未必满足任务。先写实际输出,再只改一条赋值修复它。

#include <iostream>

int main() {
    int x{4};
    int y{9};
    int* p{&x};
    p = &y;
    std::cout << x << ' ' << y << ' ' << *p << '\n';
    return 0;
}
找错与答案:观察到了9,是否等于已经改了x

实际输出4 9 9:p已指向y,x完全没变。把p = &y;改成*p = y;才会把9写入x,输出9 9 9。这个错误有确定的合法执行结果,和后面不能运行的悬空访问属于不同问题。

函数改了p,究竟改了谁

沿用新04的按值规则:每次调用都有独立参数对象,只是这次参数保存指针值。void set_seven(int* p)中的p和调用者的p是两份指针对象,最初指向同一目标。写*p可修改共同目标;写形参p只改本次的指针副本,不会使调用者的指针改指向。这不改变“按值参数独立”的规则。

#include <iostream>

void set_seven(int* p) {
    if (p != nullptr) {
        *p = 7;
    }
}

void point_elsewhere(int* p, int* other) {
    p = other;
    if (p != nullptr) {
        std::cout << *p << '\n';
    }
}

int main() {
    int x{4};
    int y{9};
    int* p{&x};
    set_seven(p);
    point_elsewhere(p, &y);
    std::cout << x << ' ' << y << ' ' << *p << '\n';
    return 0;
}

set_seven接收指向x的副本,判非空后把x改7。point_elsewhere中的p和other分别接收x与y的地址,p = other只让局部p改指y,所以函数内输出9。回到main,main自己的p仍指x,按x、y、*p输出7 9 7。函数名不能代替合同:这里故意显示“局部改指向”不会传播给调用者。

返回地址,与返回整数分开读

函数头int* positive_or_missing(int& value)int*描述返回的结果类型:交回一个指向int的指针值。引用参数value仍按新05绑定调用者的整数。return &value;交回那个整数的地址;return nullptr;交回空值。调用者可以用结果初始化自己的指针,不会因此获得第二份整数。

#include <iostream>

int* positive_or_missing(int& value) {
    if (value > 0) {
        return &value;
    }
    return nullptr;
}

int main() {
    int value{6};
    int* p{positive_or_missing(value)};
    if (p != nullptr) {
        std::cout << *p << '\n';
        value = 9;
        std::cout << *p << '\n';
    }
    int zero{0};
    int* absent{positive_or_missing(zero)};
    if (absent == nullptr) {
        std::cout << "missing" << '\n';
    }
    return 0;
}

输入value=6满足大于0,p接收main中value的地址。先输出6,再通过原名把value改9,下一次*p得到9。zero=0不满足条件,absent接收nullptr,于是输出missing。value与zero都属于main,执行这些访问时仍存活;函数返回不会结束它们的寿命。

追问:把输入value从6改成−2,会输出什么

p变为空值,第一段if被跳过,既不解引用p,也不执行里面的value=9。zero对应的分支仍输出missing,所以整个程序只输出一行missing。不要沿着没有进入的分支继续推算。

06.4 const的位置 · 预估60分钟

要固定目标,还是限制通过它写入

现在既有指针对象,又有被指向的整数。接口中的const可能限制其中不同的一层。本节先把两种允许操作分开,再看完整程序,不用凭“有const所以都不能变”来猜。

const int* read表示read可以改指向,但不能通过*read给目标赋值。它提供只读的int访问路径。int* const fixed表示fixed这个指针对象初始化后不能被赋予另一个指针值,但若目标int可写,就仍能通过*fixed修改它。const int* const both把两种限制同时加上:both不改指向,也不能经它写目标。

指针声明类型 能否给指针赋新地址 能否经星号给目标赋整数
int* 可以 可以,目标须是可写且仍存活的int
const int* 可以 不可以
int* const 不可以 可以,目标须是可写且仍存活的int
const int* const 不可以 不可以

位于*前的const限制通过指针访问的int;位于*后的const限制指针对象本身。只读路径仍不是快照:一个可写的x可以通过原名改值,read下一次读取会看到新值。const指针也不负责延长目标寿命,固定地址不等于保证它永远有效。

取得const整数的地址,只能在本章用提供只读访问的指针接收。把&frozen放进int*会丢掉已有的限制,编译器应当拒绝。这里不引入任何绕过const的转换。

#include <iostream>

int main() {
    int x{4};
    int y{9};
    const int* read{&x};
    x = 7;
    std::cout << *read << '\n';
    read = &y;
    std::cout << *read << '\n';
    int* const fixed{&x};
    *fixed = 8;
    const int* const both{&y};
    y = 10;
    std::cout << *fixed << ' ' << *both << '\n';
    const int frozen{6};
    const int* frozen_read{&frozen};
    std::cout << *frozen_read << '\n';
    return 0;
}

read先指向x,x经原名改7,第一行是7。read再改指y,第二行是9。fixed不能改指向,但*fixed = 8合法,x变8。both不能经自己写y,y却能通过原名变10,因此第三行8 10。最后frozen本身是const,frozen_read只读它,输出6。

以下三份短文件分别违反三条已讲过的限制。先指出错误语句和受限制的层,再只编译看诊断;它们没有预期运行输出。

#include <iostream>

int main() {
    int x{4};
    const int* read{&x};
    *read = 7;
    std::cout << *read << '\n';
    return 0;
}
#include <iostream>

int main() {
    int x{4};
    int y{9};
    int* const fixed{&x};
    fixed = &y;
    std::cout << *fixed << '\n';
    return 0;
}
#include <iostream>

int main() {
    const int fixed{6};
    int* write{&fixed};
    std::cout << *write << '\n';
    return 0;
}
小练习:三处拒绝分别怎样修,而且保留原来的限制

第一份不能写*read;若任务允许原对象改变,可改为x = 7,read继续只读,后面的*read输出7。第二份不能写fixed;若任务只是改x中的整数,可改为*fixed = y,它仍指x,输出9。第三份把接收类型改为const int* write,保留目标的只读性质,输出6。不要仅为编译成功删除所有const;应先说明任务到底允许什么操作。

06.5 最后一次合法访问 · 预估90分钟

目标什么时候不再可用

指针没有被改写,是否足以保证下次仍能读?现在把目标放入一个内层块,沿已学的局部寿命规则标出它存在的区间,再决定访问应放在哪一侧。

对本章普通局部int,退出它所在块时对象寿命与那段自动存储的使用期都结束。外层指针对象仍可能存在,但它保存过的关系不会让内层整数继续存活。指向已经结束的对象的指针称为悬空指针(dangling pointer);经它读取或写入不再有有效目标,属于未定义行为,不能为它指定一个正确输出。

下面采取主动收回借用的做法:在local仍活着时完成最后一次读取,并把外层p设为nullptr,再退出块。这只改变p,不销毁local,也不会自动清空其他指针副本;若另外保存了q,也必须单独避免让它越过目标的有效期。

#include <iostream>

int main() {
    int* p{nullptr};
    {
        int local{4};
        p = &local;
        std::cout << *p << '\n';
        p = nullptr;
    }
    if (p == nullptr) {
        std::cout << "missing" << '\n';
    }
    return 0;
}

p初始为空。内层创建local=4后把其地址交给p,输出4;接着p被清为空值;最后退出内层块,local结束。外层比较的是已经明确写入的空值,因此安全地输出missing。这段程序从未读取已经失效的指针值。

停下来,看一次变化

在目标结束前完成最后一次访问

pnullptrlocal尚未创建输出尚无1 / 6 · 寿命推演pnullptrlocal尚未创建输出尚无1 / 6
1 · int* p{nullptr};

外层p明确为空;内层local尚未创建,因此不能经p访问整数。

1 / 6
查看所有步骤的文字与数值
  1. 1 · int* p{nullptr};

    p:nullptr;local:尚未创建;输出:尚无

    外层p明确为空;内层local尚未创建,因此不能经p访问整数。

  2. 2 · 进入块,建立local=4

    p:nullptr;local:4,存活;输出:尚无

    现在local已初始化且存活;p仍为空。创建目标不会自动改变指针。

  3. 3 · p = &local;

    p:指向local;local:4,存活;输出:尚无

    local仍存活,把它的地址保存到p,此时可以沿p读取local。

  4. 4 · 输出*p

    p:指向local;local:4,存活;输出:4

    在目标仍活着时读取4,这是本程序最后一次间接访问local。

  5. 5 · p = nullptr;

    p:nullptr;local:4,仍存活;输出:4

    在退出块前主动收回p这条借用;local仍活着。该赋值不会自动处理别的指针副本。

  6. 6 · 退出块,再检查已写入的空值

    p:nullptr;local:已结束;输出:4 / missing

    local已经结束;p早已被写成nullptr。外层空路径输出missing,不读取失效地址。若省略清空语句,就不能用块外比较证明local仍活着。

为什么本章不运行“它看起来还非空”的实验

如果删去清空语句,再离开块,local不会自动保持存活,p也不会因为目标结束就被语言保证自动改成nullptr。由此不能反推“在块外比较p就能检查安全”:在这种自动存储已经结束的情形,连复制、输出或比较失效指针值都不是本章可依赖的可移植检查。只靠一次比较是否成功,不能证明目标仍存在。

有效访问要从指针的来源与目标寿命推导。已知输入只可能是nullptr或仍存活目标时,判空有用;来源可能已失效时,p != nullptr并没有建立“目标仍活着”的事实。图中“若漏清空,旧目标已结束”是源码寿命推演,不是一次实际比较的输出。

阶梯第三步:返回局部地址为什么留不住目标

#include <iostream>

int* bad_address() {
    int local{4};
    return &local;
}

int main() {
    int* p{bad_address()};
    std::cout << *p << '\n';
    return 0;
}

这份错误文件只编译诊断,不运行。bad_address建立自己的局部local;在return语句求地址时local还活着,但离开函数后,它已结束。main接收一个指针结果没有复制local中的4,更没有延长local的寿命。随后的std::cout << *p要求访问已经结束的目标,因此不能写“预期输出4”。

本机严格Clang可能将“返回局部地址”的警告提升为编译失败。诊断依工具与选项而定;即使另一组编译选项接受了它,错误访问仍没有变合法。对照新05已学的按值返回:若任务只是交回数字,可返回int值并由调用者保存独立结果;若任务必须返回借用,则目标必须在调用者使用期间持续存活,如06.3的main对象。

小练习:把错误函数改成返回整数,调用方需要一起改哪三处

返回类型从int*改为int,return &local改为return local;main用int value{bad_address()}保存结果并直接输出value,去掉解引用。名字也可以之后改成更合适的名称,但名字不是安全性的原因。结果值在local仍活着时取得,main保存独立的4。仅把接收方改成某种其他类型,不能使原来的局部地址有效。

阶梯第四步:独立选择一个仍存活的整数

关掉示例,从空文件写int* choose(bool first, int& left, int& right):first为true返回left的地址,否则返回right的地址。合同要求两个引用实参都是仍存活的可写int,且调用者保留它们直到用完结果。两个引用允许绑定同一个对象;此时两条分支都返回同一对象的地址,不需要额外分支。

main建立x=2、y=9,分别用true和false取得两个指针,输出2和9。再仅把y改10,第二个指针应读到10;第一个仍读2。用普通if检查x、y与两次借用读取,错误时输出文字并return 1。除了这组输入,再测试两个参数都传x的情况。先记录自己的实现和输出,再打开下面的参考。

提示与参考:返回借用,不返回局部副本的地址

choose的形参left/right已经借用main对象,&left&right取得的就是这些对象的地址。先按bool分支return,不在函数里创建一个局部saved然后返回它的地址。下面是2/9/10的完整参考文件;同对象追问可另写main,只把x同时传入两参数,确认两条分支都读到x的当前值。

#include <iostream>

int* choose(bool first, int& left, int& right) {
    if (first) {
        return &left;
    }
    return &right;
}

int main() {
    int x{2};
    int y{9};
    int* first{choose(true, x, y)};
    int* second{choose(false, x, y)};
    std::cout << *first << ' ' << *second << '\n';
    y = 10;
    std::cout << *second << '\n';
    if (*first != 2 || *second != 10 || x != 2 || y != 10) {
        std::cout << "wrong result" << '\n';
        return 1;
    }
    return 0;
}

输出为2 9,随后一行10,全部检查满足,退出码0。若两参数都借x,选择true或false都指x;之后x改7,两条借用都读7。它们没有复制整数,也没有成为拥有者。原程序中的y变化不会影响指向x的结果。

这一章的起点问题现在可以完整回答:指针值表示去哪里访问,判空只排除“没有目标”的那一种情况;真正的读取还要求那个目标仍存活且符合访问类型。每次先追踪指针从哪里来,再分别检查改指向、写目标和最后一次合法访问。能说明这三件事,才进入下一章的类与对象生命周期。

关掉参考,再做一次

把理解说出来

先完成正文的独立迁移,再回答下面六题。它们只检验本章内容;打开答案、编译成功或填写用时,都不会自动通过本章,更不代表通过 G0。

符号与逐行预测

int* p{&x}中的*和&分别做什么?随后*p=7,改的是哪一个对象?

对照推理与英文回答

声明中的*说明p的类型是指向int的指针;&x取得x的地址。表达式*p沿指针访问x,赋值改x中的整数,p仍保存原指向。不能把这里的&读成引用声明。

The star in the declaration makes p a pointer to int. The expression &x supplies the address of x. Assigning through *p changes x, while p still points to the same object.

复制与改指向追问

x=4、y=9,p指x,q复制p;*p=7后p改指y,四次读取x、y、*q、*p是什么?

对照推理与英文回答

结果为7、9、7、9。p与q是两份指针对象,建立时共用x,之后给p赋新地址不会改q。若再*q=8,x与*q变8,y与*p仍9。

Copying p into q copies the pointer value, not the integer object. Retargeting p leaves q pointing to x. The four reads are seven, nine, seven, and nine; a later write through q changes x.

空值与控制流找错

目标整数为0与p等于nullptr有什么区别?为什么在判空前输出*p不行?

对照推理与英文回答

值为0的存活整数有合法地址,可读到0。nullptr没有可解引用的int目标;先输出*p已经进行访问,后面的检查来不及保护它。有效目标或空值合同下,空路径必须return或用else避开解引用。

A pointer to a live integer containing zero still has a target. A null pointer has no integer target to read. Testing after dereferencing is too late; control flow must prevent the null path from reaching that access.

参数副本与结果

void f(int* p)按值接收地址,为什么写*p能影响调用者,而p=&y不会改变调用者指针?返回指针是否复制目标?

对照推理与英文回答

参数p是独立指针对象,最初可与调用者指向同一整数,因此*p写共同目标。给形参p赋新地址只改局部副本。返回指针也只是交回指针值;目标本身没有复制,之后能否用取决于其寿命。

The parameter is a separate pointer object initialized from the caller’s pointer value. Both can initially point to the same integer, so writing through the parameter changes that shared target. Retargeting the parameter changes only its local copy. Returning a pointer also copies a pointer value, not the target.

const分层与追问

const int*、int* const与const int* const各禁止什么?只读路径能否看到原对象经原名变化?

对照推理与英文回答

第一种禁止经指针写目标,允许改指向;第二种禁止改指针对象,允许写可写目标;第三种两者都禁止。可写目标经原名改变后,只读路径能读到新值,它不是快照。这些const都不保证目标一直存活。

Const before the star restricts writes through the pointer to the integer. Const after the star prevents reassignment of the pointer object. With both, both restrictions apply. A read-only path can still observe changes made through another valid path, and none of these forms extends the target lifetime.

寿命、反例与证据

返回局部地址后,即使程序曾打印4或判为非空,为什么不能算成功?choose返回调用者对象为何不同?

对照推理与英文回答

局部对象已经结束,返回地址不延寿,随后读取是无效访问,没有规定的正确结果。存储期结束后连比较或打印失效指针也不是可移植的有效性检测。本章只编译坏样本;choose借用的main整数在读取时仍活着,并由调用者保证有效期。

Returning a local address does not extend the local object’s lifetime. A later dereference has no guaranteed result, and comparing an invalid pointer is not a portable validity test. The choose function instead returns a pointer to an object kept alive by its caller. The lifetime contract, not one observed output, makes that later access valid.

和正文是同一份源码

示例文件

先自己输入和预测,卡住时再下载对照。文件名相同不代表内容相同;把它们放在单独的练习目录中,避免覆盖自己的作品。

标为故意编译报错的程序只检查对应诊断,不运行危险访问,也不从偶然输出猜结果。完整验证记录随本地构建保留;这里的程序没有调用加速卡。

可选的学习反馈

记下你真正花的时间

每完成一个学习时段,再填实际分钟。环境准备、阅读推演、独立编码和卡点排查分别记录,避免同一段时间重复计算。离开吃饭或做其他事情的时间不算进去。

记录只保存在你的浏览器,可导出给我复盘。留空表示尚未记录,不等于零耗时;页面停留时间不会自动计为学习。不要把开发者检查时间填进来。

尚无真实试学用时。

    按开始学习本章前的情况选择;已经会 C++ 时选择“会 C++”。起点随每条时段保存,之后改选不会重标旧记录。旧记录缺少起点时单独保留;已有基础者的用时不用于校准零基础预算。

      换设备:导入记录,或取回损坏的旧记录

      导入会合并时段,相同编号不重复累加;发生冲突会保留现有记录。

      阅读记录与课程验收分别保存。

      本章资料与查证

      本章独立解释所需读法;资料用于核对与补充。工具版本、操作系统和实际执行状态见自己的运行记录。