CS106L学习记录lec2

1. 流的本质:缓冲区

  • 流不仅用于传递数据,还会将数据转换为字符串或从字符串解析为变量类型(如 intdouble)。
  • 例如,输入时将用户的键盘输入解析为变量,输出时将变量转换为字符串写入控制台。
    和不加 ostringstream::ate 区别在于:开始的指针不在初始化的最后面,而是从头开始的,因此出现了覆盖的情况。
int main() {
    ostringstream oss("Ito En Green tea ");
    cout << oss.str() << endl;
    // 可以发现 16.9 的 double 由流自动转化为字符串了
    oss << 16.9 << " Ounce ";
    cout << oss.str() << endl;
    oss << "(Pack of " << 12 << ")";
    cout << oss.str() << endl;
    return 0;
}
// 输出:
// Ito En Green tea
// 16.9 Ounce n tea
// 16.9 Ounce (Pack of 12)

ostringstream::ate
流内部维护一个“写指针”(put pointer),指向下一次写入的位置。
当指定 ate 时:写指针被设置到缓冲区末尾。后续任何 << 操作都会从当前字符串的尾部开始追加,而不是覆盖开头的字符。

int main() {
    ostringstream oss("Ito En Green tea ", ostringstream::ate);
    cout << oss.str() << endl;
    // 可以发现 16.9 的 double 由流自动转化为字符串了
    oss << 16.9 << " Ounce ";
    cout << oss.str() << endl;
    oss << "(Pack of " << 12 << ")";
    cout << oss.str() << endl;
    return 0;
}
// 输出:
// Ito En Green tea
// Ito En Green tea 16.9 Ounce
// Ito En Green tea 16.9 Ounce (Pack of 12)

2. 为什么流可以直接读出数字和字符?

C++ 中的 istream(输入流,如 cinistringstream)重载了 >> 运算符,为所有基本数据类型intdoublecharstring 等)分别定义了专门的提取逻辑。

  • 当你写 iss >> result 时,编译器根据 result 的类型(比如 int)调用对应的 operator>>(int&) 版本。
  • 这个函数的内部实现会从流缓冲区中读取字符,并按照该类型的语法规则进行转换:
    • 对于 int:跳过前导空白(空格、换行等),然后读取可选的正负号,接着连续读取数字字符 0-9,直到遇到非数字字符停止,再将字符序列转换为二进制整数存入 result
    • 对于 double:类似,但还会读取小数点、指数符号 e/E 等。
    • 对于 char:不跳过空白(除非使用 noskipws),直接读取下一个字符(即使是空格或换行)。
    • 对于 string:默认跳过空白,然后读取连续的非空白字符直到下一个空白。
      例子:amount 为 int
int main() {
    istringstream iss("16.9 Ounce");
    int amount;
    iss >> amount;
    string unit;
    iss >> unit;
    cout << "amount=" << amount << ' ' << "unit=" << unit << endl;
    return 0;
}
// 输出:amount=16 unit=.9

例子:amount 为 double

int main() {
    istringstream iss("16.9 Ounce");
    double amount;
    iss >> amount;
    string unit;
    iss >> unit;
    cout << "amount=" << amount << ' ' << "unit=" << unit << endl;
    return 0;
}
// 输出:amount=16.9 unit=Ounce

3. 流的执行顺序与状态位

  • 流的提取是顺序执行的,一旦某个操作失败,后续操作都会被跳过。
  • 类型不匹配是常见的失败原因。解决方法是先检查流状态,或者先读取为字符串再手动解析。
  • 如果希望先读取字符串可以成功,应调整顺序:先 iss >> string(会读取 "abc11" 整个作为字符串),再 iss >> int(此时流已空,读取整数会失败)。
    示例
int main() {
    istringstream iss("abc11");
    int n = 0;
    string s = "initial";
    iss >> n;       // 失败,n保持0
    iss >> s;       // 由于流已失败,跳过,s保持"initial"
    cout << "n = " << n << endl;          // n = 0
    cout << "s = " << s << endl;          // s = initial
    cout << "fail: " << boolalpha << iss.fail() << endl; // fail: true
    return 0;
}

四种状态位的解释

  1. goodbit (G)
    • 表示流的状态正常,准备好进行读/写操作。
    • 默认状态:所有操作都正常进行。
  2. failbit (F)
    • 表示上一次操作失败,例如数据类型不匹配或无法打开文件。
    • 流被冻结:所有后续操作会被阻止,除非重置流状态。
  3. eofbit (E)
    • 表示已经到达文件或流的末尾(End Of File)。
    • 常见于从文件或输入流读取数据时。
  4. badbit (B)
    • 表示发生了不可恢复的错误,如读取的文件丢失或外部设备断开。
    • 一旦设置,需要重置流或关闭程序。

4. flush 与 endl

flush 的作用是刷新输出缓冲区,即强制将缓冲区中暂存的数据立即写入到实际的输出设备(如屏幕、文件)。
具体效果

  • 正常情况下,输出流会将数据暂存在内存缓冲区中,待缓冲区满或遇到换行符时才真正输出。这样做是为了提高效率(减少与慢速设备的交互次数)。
  • 调用 flush 后,缓冲区内所有积存的数据会被立刻、无条件地发送到输出目标,缓冲区被清空。
    std::endl 等价于:
<< '\n' << std::flush

即:输出一个换行符,然后刷新缓冲区

那么为什么 endl 要自动刷新缓冲区?
这是 C++ 设计时的选择:endl 的语义是“结束这一行并确保输出立即显示”。在交互式控制台或日志场景中,换行后通常希望内容立刻可见(比如打印提示信息、调试信息),所以 endl 结合了换行和刷新。

如果每次都刷新,会降低性能。
因为刷新缓冲区(flush)会强制与操作系统或硬件交互,如果频繁刷新(例如循环中每次输出都用 endl),程序会变慢。所以很多场合应该用 '\n' 代替 endl,只在必要时才刷新。

常见使用场景

  1. 交互式提示:确保用户看到提示信息后再执行后续操作,避免因缓冲导致提示延迟显示。
  2. 长时间任务进度显示:定期输出进度,避免因缓冲导致进度信息块状出现。
  3. 调试日志:在程序崩溃前立即输出关键日志,防止日志因缓冲区未刷新而丢失。
  4. 文件流:确保重要数据立即写入磁盘,降低异常断电或程序崩溃时的数据丢失风险。

5. 几个关键问题的答案

1) 程序何时提示用户输入?

C++ 标准库不会自动输出任何提示信息。程序“提示用户输入”的唯一方式是程序员主动使用 cout 等输出流打印一段文字(例如 cout << "请输入一个整数: ";)。
当执行到 cin >> variable 这样的输入语句时,程序会阻塞等待用户从标准输入(通常是键盘)输入数据并按下回车键。如果在此之前已经有 cout 输出的提示,提示内容是否会立即显示取决于输出流的缓冲策略(见下一问)。

2) 为什么 cout 操作不立即将输出打印到控制台?何时才打印?

  • 原因:为了性能,C++ 输出流通常使用缓冲区。数据先写入内存缓冲区,当满足一定条件时才真正调用操作系统接口输出到控制台(或文件),避免频繁的慢速 I/O 操作。
  • 打印时机(缓冲区被刷新)
    • 缓冲区满时自动刷新。
    • 遇到 std::endl(输出换行并刷新)、std::flush(仅刷新)或 std::ends(输出空字符并刷新)。
    • 显式调用 cout.flush()
    • 从 cin 读取输入时(对于终端,通常 cin 和 cout 被绑定在一起,读取前会自动刷新 cout 缓冲区)。
    • 程序正常结束时(main 返回或调用 exit)。

3) 每次 >> 操作时,位置指针是在 token 前跳过空白,还是在 token 后跳过空白?

标准行为(默认 skipws 标志为真):

  • 读取 token 之前:跳过前导空白字符(空格、制表符、换行符等)。
  • 读取 token 之后跳过后续空白。指针停在 token 后面第一个字符处(可能是空白,也可能是非空白)。

特例:当读取 char 类型时,默认不跳过任何空白(除非显式设置 noskipws)。也就是说,char 会原样读取下一个字符(包括空格、换行等)。
举例

istringstream iss("  123  456");
int n;
iss >> n;  // 跳过前导空格,读到'1',读完整数"123",指针停在第一个空格后('1'之后的空格)
char c;
iss >> c;  // 由于默认skipws,char也会跳过空白?错了——对于char,skipws不生效。实际上char会读取下一个字符(即空格)。但如果读取string,则skipws生效。

实际上,默认情况下 operator>>(char&) 不会跳过空白,而 operator>>(string&) 会跳过前导空白后读取连续非空白字符。所以回答需要区分类型。
更准确的结论

  • 对于算术类型(int, double等)和 string读取前跳过空白,读取后停止在第一个空白字符处(不跳过它)
  • 对于 char不跳过任何空白(指针位置不变,直接读取当前字符)。

4) >> 是否总是读取到空白为止?如果不是,举一个反例。

不是>> 对于不同类型有不同的词法规则,并非总是遇到空白才停止。以下反例说明它可能在遇到非空白的非法字符时停止:

// 示例:读取整数时遇到字母
istringstream iss("123abc");
int n;
iss >> n;   // 读取 "123",遇到 'a'(非数字,且不是空白)停止
// n = 123,流中剩余 "abc"

6. cin >> 与 getline 的典型陷阱

问题示例

void badWelcomeProgram() {
    string name, response;
    int age;
    cout << "What's your name? ";
    cin >> name;
    cout << "What's your age? ";
    cin >> age;
    cout << "Hello " << name << " (age " << age << ")" << "\n";
    cout << "Do you want to try again? ";
    cin >> response;
    cout << "You said: " << response << endl;
}

运行这个程序输入 jiang wen 后直接结束了,输出如下:

What's your name? jiang wen
What's your age? Hello jiang (age 0)
Do you want to try again? You said:

原因cin >> 读取的机制和输入缓冲区残留数据导致后续读取失败。
详细过程

  1. cin >> name
    • >> 以空白字符(包括空格、制表符 \t、换行符 \n、回车符 \r、换页符等)为分隔符,读取一个单词。
    • 用户输入 jiang wen 并回车,缓冲区内容为 jiang wen\n
    • cin >> name 读取 "jiang",缓冲区剩余 " wen\n"(注意开头的空格)。
  2. cin >> age
    • 执行到这里时,缓冲区仍有数据,不会等待用户输入。
    • >> 会跳过前导空白(空格、换行等),然后尝试读取一个整数。
    • 但缓冲区中下一个字符是 'w'(来自 "wen"),不是数字,读取失败
    • 流的状态标志 failbit 被设置,age 保持未初始化(输出 0 是未定义行为,实际可能是残留值)。
    • 缓冲区中的 "wen\n" 未被消耗。
  3. cin >> response
    • 由于流已经处于 fail 状态,后续任何 >> 操作都会直接忽略(不读取任何内容)。
    • response 保持空字符串。
  4. 输出结果
    • name 为 "jiang"age 为未初始化的垃圾值(常表现为 0),response 为空。

为什么没有等待输入年龄和尝试再次的输入?
因为 cin >> age 直接用了缓冲区中残留的 " wen",发现不是整数后立即失败,没有阻塞。后续 cin >> response 也因为流已失败而被跳过。

如何修复?

  • 使用 getline 读取整行(包含空格),避免单词拆分。
  • 在读取数字后清空缓冲区(例如 cin.ignore())。
  • 检查流状态,失败时清除错误并忽略无效输入。
    用 getline(cin, name) 替代 cin >> name,这样 name 就能得到 "jiang wen"(整行),且不会留下残留导致后续读取失败。

7. 为什么 cin >> 会这样,而 getline 正确?

cin >> 和 getline 的行为差异源于它们设计目的的不同,以及对输入缓冲区的处理方式不同

核心原因

  • cin >>(格式化输入):用于读取由空白字符分隔的 token。它会:
    1. 自动跳过前导空白(空格、制表符、换行等)。
    2. 读取直到遇到下一个空白字符,但将终止的空白字符留在缓冲区中(不消耗)。
    3. 若类型不匹配(如读取整数时遇到字母),设置 failbit,后续所有输入被跳过。
  • getline(非格式化输入):用于读取一整行文本。它会:
    1. 不跳过前导空白(除非你显式处理)。
    2. 读取直到遇到换行符 '\n'并将换行符从缓冲区中提取并丢弃(消耗掉)。
    3. 如果指定了分隔符,也可以读取到其他字符为止。
      例子
int main() {
    istringstream iss("16.9\n 24");
    double val;
    string line;
    iss >> val; // val = 16.9
    getline(iss, line); // line = "";
    getline(iss, line); // line = " 24";
    return 0;
}

对比实例:输入 "jiang wen" 并回车
用户输入 jiang wen 并敲回车后,缓冲区内容为:jiang wen\n\n 表示换行符)。

  • 使用 cin >> name 时:
    • cin >> name 跳过前导空白(无),读取 "jiang"(遇到空格停止),缓冲区剩余:wen\n(第一个字符是空格)。
    • 下一个 cin >> age 尝试读取整数。它先跳过前导空白(跳过空格),然后看到 'w',不是数字 → 读取失败,设置 failbit。缓冲区不变(wen\n 仍在)。
    • 之后所有 cin >> 都被跳过(因为流已失败),response 未读,末尾换行符未消费。
  • 使用 getline(cin, name) 时:
    • getline(cin, name) 读取整行,包括空格,直到遇到 '\n'。它会读取 "jiang wen",并将换行符 '\n' 从缓冲区移除。缓冲区变空。
    • 下一个 cin >> age 需要用户输入(因为缓冲区为空),程序正常等待输入。
    • 输入年龄后,再后续的 getline(cin, response) 或 cin >> response 也能正常工作,因为缓冲区没有被意外污染。
      为什么 cin >> 不自动消耗空白?
      性能考虑:让 >> 留下空白,后续 >> 可以快速跳过它们(因为大多数格式化输入需要跳过空白)。而 getline 的目标是读取精确的行内容,所以必须吃掉换行符。

8. 混合使用 cin >> 和 getline 的另一个问题

以下代码会有什么问题?

void badWelcomeProgram() {
    string name, response;
    int age;
    cout << "What's your name? ";
    getline(cin, name);
    cout << "What's your age? ";
    cin >> age;
    cout << "Hello " << name << " (age " << age << ")" << "\n";
    cout << "Do you want to try again? ";
    getline(cin, response);
    cout << "You said: " << response << endl;
}

问题描述

  1. getline(cin, name) 正常读取整行(包括空格),用户输入 John 并回车 → 缓冲区中的换行符 '\n' 被 getline 消费掉,缓冲区清空。
  2. cin >> age 等待用户输入整数(如 25),用户输入后按下回车。此时 cin >> age 读取整数 25,但换行符 '\n' 仍然留在缓冲区中
  3. 执行到 getline(cin, response) 时,它发现缓冲区中有一个残留的换行符 '\n'(来自上一步的回车),于是立即读取这个空行response 变成空字符串,不会等待用户输入
    运行结果示例
What's your name? John
What's your age? 25
Hello John (age 25)
Do you want to try again? You said:

可以看到程序没有等待用户输入是否重试,直接输出了 You said:(后面为空)。
根本原因总结

  • getline 读取并消耗换行符
  • cin >> 读取数据后不消耗末尾的换行符,它留在缓冲区中。
  • 当 cin >> 后紧跟 getline 时,getline 会直接看到那个换行符并返回空字符串。

这种问题在混合使用格式化输入(>>)和非格式化输入(getline)时极易出现,养成良好的习惯:每次使用 cin >> 读取基本类型后,如果需要用 getline 读取字符串,可以先调用 cin.ignore(),最好是不要混合使用。

解决方法:自己写一个 getInteger,而不是 cin >> age


int getInteger(const string& prompt) {
    while (true) {
        // 循环提示用户输入
        cout << prompt;
        string line; int result; char trash;
        if (!getline(cin, line)) {
            throw std::domain_error("something error.");
        }
        istringstream iss(line);
        if (iss >> result && !(iss >> trash)) {
            return result;
        }
        cout << "error, again." << endl;
    }
}
void badWelcomeProgram() {
    string name, response;
    int age;
    cout << "What's your name? ";
    getline(cin, name);
    cout << "What's your age? ";
    age = getInteger("What's your age? ");
    cout << "Hello " << name << " (age " << age << ")" << "\n";
    cout << "Do you want to try again? ";
    getline(cin, response);
    cout << "You said: " << response << endl;
}

9.auto

    auto name = "Avery";   // c-string,const char*

"Avery" 是字符串字面量,类型是 const char[6](包含结尾的 \0)。在 auto 推导中,数组会退化为指针,所以 name 的类型是 const char*(C 风格字符串指针)。

C 字符串两种写法 类型 内存位置 能不能修改内容
"hello" const char* 只读区 ❌ 不能改
char s[] = "hello" char[6] ✅ 随便改
auto 时,数组为什么会退化成指针?
在 C/C++ 里,数组本身不能被直接赋值、传递、拷贝。
只要你试图用 = 赋值给一个变量(包括 auto),数组就会自动变成指针。

    auto betterName1 = string{"Avery"};  // c++-string
    const auto& betterName2 = string{"Avery"};   //const string&

auto betterName1 = string{"Avery"}; 本质是一个副本

  1. 右边先创建一个临时 string
  2. 把临时对象拷贝给左边
  3. 得到一个全新、独立的字符串
  4. 可以修改

const auto& betterName2 = string{"Avery"}; 本质就是string{"Avery"}本身

  1. 边创建一个临时 string
  2. 用引用直接绑定它,不拷贝!
  3. 引用 = 别名,不产生新对象
  4. const不能修改

以下写法错误

auto& betterName2 = string{"Avery"};

C++ 规则:非 const 引用(&)不能绑定临时对象!
只有 const & 才能绑定临时对象。

  • 临时对象
    → 只能用 const & 绑定
    → 不能用 & 绑定
  • 有名变量
    → 用 &const & 都可以

10.引用

string tea = "Iao-En";
string copy = "Itb-En";
string& ref = tea;
ref = copy;                 // tea 变为 "Itb-En"
cout << "tea: " << tea << endl;   // 输出 tea: Itb-En
tea[1] = 'j';               // tea[1] 从 't' 改为 'j'
cout << ref << endl;        // ref 引用 tea,输出 Ijb-En

关键点:

  1. 引用一旦初始化,永远绑定到同一个对象,不能改变绑定。
    • string& ref = tea; 后,ref 始终是 tea 的别名。
  2. ref = copy; 执行的是对象赋值,等价于 tea = copy;
    • 将 copy 的当前内容拷贝到 tea 中(深拷贝),而不是让 ref 指向 copy
  3. 赋值后,tea 的内容变为 copy 的内容(本例中 copy 为 "Itb-En"),但 ref 仍然是 tea 的引用。
  4. 后续对 tea 的修改(如 tea[1] = 'j')会直接影响 ref 的输出

11.类型转换

static_cast

cpp提供的相对于c语言中的强制转换更加安全的转换

    int v1 = static_cast<int>(3.4);  
    double v2 = 6;                  

const_cast

    const int val1 = 3; 
    int *val2 = const_cast<int*> (&val1); 

功能

  • const_cast 用于添加或移除变量的 const 或 volatile 限定符。
  • 这里 &val1 的类型是 const int*(指向常量整数的指针)。const_cast<int*> 移除了 const,得到 int* 类型的 val2
posted @ 2026-06-06 21:22  zhujio  阅读(7)  评论(0)    收藏  举报