CS106L学习记录lec2

1. 流的本质:缓冲区

  • 流不仅用于传递数据,还会将数据转换为字符串或从字符串解析为变量类型(如 int、double)。
  • 例如,输入时将用户的键盘输入解析为变量,输出时将变量转换为字符串写入控制台。
    和不加 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(输入流,如 cin、istringstream)重载了 >> 运算符,为所有基本数据类型(int、double、char、string 等)分别定义了专门的提取逻辑。

  • 当你写 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  阅读(19)  评论(0)    收藏  举报