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;
}
四种状态位的解释
- goodbit (G)
- 表示流的状态正常,准备好进行读/写操作。
- 默认状态:所有操作都正常进行。
- failbit (F)
- 表示上一次操作失败,例如数据类型不匹配或无法打开文件。
- 流被冻结:所有后续操作会被阻止,除非重置流状态。
- eofbit (E)
- 表示已经到达文件或流的末尾(End Of File)。
- 常见于从文件或输入流读取数据时。
- badbit (B)
- 表示发生了不可恢复的错误,如读取的文件丢失或外部设备断开。
- 一旦设置,需要重置流或关闭程序。
4. flush 与 endl
flush 的作用是刷新输出缓冲区,即强制将缓冲区中暂存的数据立即写入到实际的输出设备(如屏幕、文件)。
具体效果:
- 正常情况下,输出流会将数据暂存在内存缓冲区中,待缓冲区满或遇到换行符时才真正输出。这样做是为了提高效率(减少与慢速设备的交互次数)。
- 调用
flush后,缓冲区内所有积存的数据会被立刻、无条件地发送到输出目标,缓冲区被清空。
std::endl等价于:
<< '\n' << std::flush
即:输出一个换行符,然后刷新缓冲区。
那么为什么 endl 要自动刷新缓冲区?
这是 C++ 设计时的选择:endl 的语义是“结束这一行并确保输出立即显示”。在交互式控制台或日志场景中,换行后通常希望内容立刻可见(比如打印提示信息、调试信息),所以 endl 结合了换行和刷新。
如果每次都刷新,会降低性能。
因为刷新缓冲区(flush)会强制与操作系统或硬件交互,如果频繁刷新(例如循环中每次输出都用 endl),程序会变慢。所以很多场合应该用 '\n' 代替 endl,只在必要时才刷新。
常见使用场景
- 交互式提示:确保用户看到提示信息后再执行后续操作,避免因缓冲导致提示延迟显示。
- 长时间任务进度显示:定期输出进度,避免因缓冲导致进度信息块状出现。
- 调试日志:在程序崩溃前立即输出关键日志,防止日志因缓冲区未刷新而丢失。
- 文件流:确保重要数据立即写入磁盘,降低异常断电或程序崩溃时的数据丢失风险。
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 >> 读取的机制和输入缓冲区残留数据导致后续读取失败。
详细过程:
cin >> name>>以空白字符(包括空格、制表符\t、换行符\n、回车符\r、换页符等)为分隔符,读取一个单词。- 用户输入
jiang wen并回车,缓冲区内容为jiang wen\n。 cin >> name读取"jiang",缓冲区剩余" wen\n"(注意开头的空格)。
cin >> age- 执行到这里时,缓冲区仍有数据,不会等待用户输入。
>>会跳过前导空白(空格、换行等),然后尝试读取一个整数。- 但缓冲区中下一个字符是
'w'(来自"wen"),不是数字,读取失败。 - 流的状态标志
failbit被设置,age保持未初始化(输出0是未定义行为,实际可能是残留值)。 - 缓冲区中的
"wen\n"未被消耗。
cin >> response- 由于流已经处于
fail状态,后续任何>>操作都会直接忽略(不读取任何内容)。 response保持空字符串。
- 由于流已经处于
- 输出结果
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。它会:- 自动跳过前导空白(空格、制表符、换行等)。
- 读取直到遇到下一个空白字符,但将终止的空白字符留在缓冲区中(不消耗)。
- 若类型不匹配(如读取整数时遇到字母),设置
failbit,后续所有输入被跳过。
getline(非格式化输入):用于读取一整行文本。它会:- 不跳过前导空白(除非你显式处理)。
- 读取直到遇到换行符
'\n',并将换行符从缓冲区中提取并丢弃(消耗掉)。 - 如果指定了分隔符,也可以读取到其他字符为止。
例子:
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;
}
问题描述:
getline(cin, name)正常读取整行(包括空格),用户输入John并回车 → 缓冲区中的换行符'\n'被getline消费掉,缓冲区清空。cin >> age等待用户输入整数(如25),用户输入后按下回车。此时cin >> age读取整数25,但换行符'\n'仍然留在缓冲区中。- 执行到
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"}; 本质是一个副本
- 右边先创建一个临时 string
- 把临时对象拷贝给左边
- 得到一个全新、独立的字符串
- 可以修改:
② const auto& betterName2 = string{"Avery"}; 本质就是string{"Avery"}本身
- 边创建一个临时 string
- 用引用直接绑定它,不拷贝!
- 引用 = 别名,不产生新对象
- 带
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
关键点:
- 引用一旦初始化,永远绑定到同一个对象,不能改变绑定。
string& ref = tea;后,ref始终是tea的别名。
ref = copy;执行的是对象赋值,等价于tea = copy;。- 将
copy的当前内容拷贝到tea中(深拷贝),而不是让ref指向copy。
- 将
- 赋值后,
tea的内容变为copy的内容(本例中copy为"Itb-En"),但ref仍然是tea的引用。 - 后续对
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。
浙公网安备 33010602011771号