1. 从“Hello World”到理解程序骨架
上一篇文章我们让第一个C++程序跑了起来,看到了那个经典的“Hello World”。但如果你只是照着敲了一遍,可能心里还是有点懵:#include <iostream>是什么?using namespace std;又是什么意思?int main()非得这么写吗?别急,咱们这就把程序的外壳一层层剥开,看看里面的构造。理解这些,比你多写几个程序更重要,这是你未来阅读任何C++代码的基础。
想象一下你要盖房子。int main()就是你的宅基地,是所有工程的起点和终点,没有它,你什么也建不了。#include <iostream>就像是去建材市场拉了一车预制好的门窗和砖块(cout、endl这些工具),没有这行代码,你就得自己从烧砖开始,那效率就太低了。而using namespace std;可以理解为,你告诉工地上的所有人:“以后从‘标准建材公司’(std)来的东西,直接叫名字就行,不用每次都加上公司名。” 比如,你可以直接说“把那个cout拿过来”,而不是说“把那个std::cout拿过来”。当然,有时候为了避免不同公司的产品重名,全称会更安全,这个我们后面会细说。
2. 预处理器指令:#include的幕后工作
让我们先聚焦在程序的第一行:#include <iostream>。这行代码不是C++语句,它是在你的程序被正式编译之前,由“预处理器”处理的指令。你可以把预处理器看作一个高级文本替换工具。
2.1#include到底做了什么?
当你写下#include <iostream>,预处理器的动作非常简单粗暴:它找到名为iostream的文件(这是一个“头文件”),然后把该文件里的所有内容原封不动地、一字不差地复制粘贴到你写#include的这个地方。
那么iostream文件里有什么呢?它包含了“输入输出流”相关的声明,特别是定义了cout、cin、endl这些对象和操作符该如何使用。编译器需要看到这些声明,才知道cout << “Hello World”这句代码是合法的,并且知道该生成什么样的机器指令来实现它。
2.2 尖括号<>与双引号“”的区别
这是新手常混淆的点,记住一个原则:查找路径不同。
#include <filename>:编译器会去系统标准库目录下寻找这个头文件。像iostream、vector、string这些C++标准库自带的头文件,都用尖括号。#include “filename”:编译器会先在当前项目源文件所在的目录下寻找,如果没找到,再去系统标准库目录找。你自己编写的头文件,通常用双引号。
举个例子:
#include <iostream> // 引入标准库的头文件 #include “myutils.h” // 引入我自己项目里写的头文件注意:在包含头文件时,确保路径和文件名正确。一个常见的错误是
#include <iostream.h>,这是非常古老的C++标准(C++98之前)的写法,现代C++(C++98及以后)标准库头文件都不带.h后缀。如果你在较新的编译器(如GCC 10+、MSVC 2019+)中这样写,会报错。
2.3 头文件的作用与防止重复包含
头文件(.h或.hpp)的核心作用是声明。它告诉编译器:“嘿,有这么一些函数、类、变量存在,它们的类型和名字是这样的,你先记着,具体实现(定义)在别处。” 这实现了接口与实现的分离,是模块化编程的基础。
由于头文件可能被多个源文件(.cpp)包含,为了防止同一段声明被重复粘贴导致编译错误,我们使用“包含守卫”或#pragma once。
包含守卫(传统,兼容性好):
// myheader.h #ifndef MYHEADER_H // 如果 MYHEADER_H 这个宏没有被定义过 #define MYHEADER_H // 那么就定义它,并包含以下内容 // 你的所有声明写在这里... #endif // MYHEADER_H当这个头文件第一次被包含时,
MYHEADER_H未定义,所以#ifndef条件为真,执行#define并包含内容。第二次被包含时,MYHEADER_H已定义,#ifndef条件为假,中间的内容就被跳过了。#pragma once(现代,简洁):// myheader.h #pragma once // 告诉编译器,这个文件只包含一次 // 你的所有声明写在这里...绝大多数现代编译器(GCC, Clang, MSVC)都支持它,写起来更简单。我个人在项目中更倾向于使用
#pragma once,除非有明确的跨平台兼容性顾虑。
3. 命名空间namespace:解决名字冲突的利器
随着程序变大,使用的库越来越多,难免会出现不同库定义了同名函数或类的情况。比如,一个图形库可能有个draw()函数,一个网络库也可能有个draw()函数。如果都直接使用,编译器就懵了。
命名空间就是给这些名字加一个“姓氏”,用来区分。
3.1 定义与使用
std是 C++ 标准库的命名空间。标准库里的所有东西(cout,vector,string...)都在这个“家族”里。
- 直接使用(带姓氏):
std::cout。::是作用域解析运算符,意思是“在...里面找”。std::cout就是“在std这个命名空间里找cout”。这种方式最清晰,永远不会冲突。 - 使用
using声明(引入单个名字):using std::cout;。这行代码之后,在当前作用域内,cout就特指std::cout,你可以直接写cout。但std里的其他名字(如cin,endl)仍需加std::。 - 使用
using指令(引入整个命名空间):using namespace std;。这是我们一开始用的方式。它告诉编译器:“在当前作用域,如果看到一个不认识的名字,先去std里找找看。” 这非常方便,但也最危险,因为它可能把你自定义的名字和标准库的名字搞混。
3.2 为什么using namespace std;在大型项目中不受欢迎?
想象一下,你写了一个自己的cout函数用来记录日志。如果你在文件开头写了using namespace std;,那么后面你调用cout时,编译器就分不清你到底想用标准库的cout还是你自己的cout了,这会导致二义性错误。
实操建议:
- 在小型练习、单个源文件的项目中,为了方便,在
.cpp文件开头使用using namespace std;问题不大。 - 在头文件(
.h)中,绝对不要使用using namespace std;或任何using指令。因为头文件会被多个源文件包含,你的这个“偷懒”行为会污染所有包含它的源文件的作用域,极易引发难以调试的命名冲突。 - 在大型项目、多人协作中,最推荐的做法是始终使用
std::前缀。虽然打字多一点,但代码清晰、安全,是专业C++程序员的习惯。对于特别长的命名空间(如boost::asio::ip::tcp),可以使用using声明引入常用的具体类型,例如using boost::asio::ip::tcp::socket;。
4. 主函数main():程序的唯一入口
int main()是每个可执行C++程序的必须且唯一的入口点。操作系统加载你的程序后,就从这里开始执行代码。
4.1main函数的两种标准形式
C++标准只定义了两种有效的main函数签名:
- 无参形式:
int main() - 带参形式:
int main(int argc, char* argv[])(或等价的int main(int argc, char** argv))
argc(argument count) 是命令行参数的数量,argv(argument vector) 是一个指针数组,指向每个参数字符串。argv[0]通常是程序自己的名字。
4.2 返回值int的意义
main函数的返回值会返回给操作系统,用来表示程序的退出状态。按照惯例:
- 返回 0:表示程序成功执行。
- 返回非 0 值:表示程序执行过程中出现了某种错误。不同的非零值可以代表不同的错误类型,这由程序员自己约定。
在 C++ 中,如果main函数执行到结尾的}而没有遇到return语句,编译器会自动为你生成return 0;。但为了代码清晰,显式地写出return 0;是一个好习惯。对于错误返回,则必须显式return。
4.3 关于void main()
在一些非常古老的编译器或特定嵌入式环境中,你可能会看到void main()。但这不符合 C++ 和 C 语言的标准。在现代标准的桌面或服务器编程中,使用void main()可能导致编译错误或未定义行为。请始终坚持使用int main()。
5. 输入与输出的初探:cin与cout
我们已经用过cout(character output) 进行输出。输入则使用cin(character input)。它们和运算符<<(流插入运算符) 和>>(流提取运算符) 配合使用。
5.1 基本使用
#include <iostream> #include <string> // 为了使用 std::string int main() { int age = 0; std::string name; std::cout << “请输入您的姓名和年龄:” << std::endl; std::cin >> name >> age; // 从标准输入(通常是键盘)读取数据 std::cout << “您好,” << name << “!您今年” << age << “岁。” << std::endl; return 0; }5.2 几个关键点与常见“坑”
>>运算符会跳过空白符:空格、制表符、换行符都被视为分隔符。对于上面的代码,输入“张三 25”或“张三(回车)25(回车)”都能正确读取。std::endlvs‘\n’:两者都能换行。但std::endl不仅输出换行符,还会刷新输出缓冲区。缓冲区是为了提高效率,将多次输出攒在一起再真正写入屏幕或文件。endl强制立即刷新。在需要确保信息立刻显示(如调试日志)时用endl,在纯粹需要换行且不关心缓冲时,用‘\n’效率稍高。- 读取字符串时的陷阱:
std::cin >> name;这种方式读取string,遇到空白符就会停止。如果你想读取一整行(包括空格),应该使用std::getline函数。
一个经典混合输入问题:如果你在std::string fullLine; std::getline(std::cin, fullLine); // 读取一行,直到遇到换行符cin >> age;之后立刻使用getline(cin, name);来读取名字,会发现getline直接读到了一个空行。这是因为cin >> age;读取了数字,但留下了后面的换行符在输入流中,getline一上来就遇到了这个换行符,于是认为读到了一个空行。解决方法:在cin >>之后、getline之前,使用std::cin.ignore()来忽略掉残留的换行符。std::cin >> age; std::cin.ignore(); // 忽略掉一个字符(通常是换行符) std::getline(std::cin, name);
6. 变量与基本数据类型:数据的容器和标签
程序的核心是处理数据。变量就是内存中一块存储空间的命名标签,数据类型则决定了这块空间的大小和解释方式。
6.1 C++ 基本内置类型
| 类型 | 含义 | 典型大小(64位系统) | 备注 |
|---|---|---|---|
bool | 布尔型 | 1字节 | 值只能是true或false |
char | 字符型 | 1字节 | 存储单个字符(如 ‘A’, ‘9’) |
int | 整型 | 4字节 | 最常用的整数类型 |
float | 单精度浮点型 | 4字节 | 约7位有效数字 |
double | 双精度浮点型 | 8字节 | 约15位有效数字,默认浮点类型 |
void | 无类型 | N/A | 通常用于函数返回值或指针 |
此外,还有修饰符可以改变基本类型的含义:signed(有符号,默认)、unsigned(无符号)、short(短)、long(长)。例如unsigned int,long long。
6.2 变量定义与初始化
- 定义:
int age;// 定义一个名为age的整型变量。此时它的值是未定义的(可能是任意值)。 - 初始化:在定义时给一个初始值。
- 拷贝初始化:
int age = 25; - 直接初始化:
int age(25);(对于类类型更高效) - 列表初始化(C++11推荐):
int age{25};或int age = {25};列表初始化的优势:它能防止“窄化转换”。例如
int x{3.14};会导致编译错误,因为double到int是窄化转换,而int x = 3.14;只会给出警告并截断。这有助于在编译期发现潜在错误。
- 拷贝初始化:
6.3auto关键字(C++11)
让编译器根据初始化表达式自动推导变量类型。
auto i = 42; // i 被推导为 int auto d = 3.14; // d 被推导为 double auto s = “hello”; // s 被推导为 const char* (注意!不是 std::string) auto s2 = std::string(“hello”); // s2 被推导为 std::stringauto在写复杂类型(如迭代器)时能极大简化代码,但初学者在简单类型上慎用,明确写出类型有助于加深对类型的理解。
7. 常量:不变的量
常量用于定义程序中不应被改变的值。
const对象:定义后其值不可修改。必须在定义时初始化。const int kMaxBufferSize = 1024; // kMaxBufferSize = 2048; // 错误!不能修改常量习惯上,常量名使用
k开头或全大写,以区分变量。constexpr对象(C++11):用于定义编译期常量。比const更严格,要求其值必须在编译阶段就能计算出来。constexpr int kArraySize = 100; // 正确,字面值常量 constexpr int kSize = kMaxBufferSize * 2; // 正确,由编译期常量计算得出 int user_input = 10; // constexpr int kDynamicSize = user_input; // 错误!user_input 不是编译期可知的constexpr能让编译器做更多优化,也是定义数组大小等场景的现代首选方式。
8. 作用域与生命周期:变量在哪里“活”着
作用域决定了变量名在代码的哪些区域可见。生命周期决定了变量在内存中何时创建、何时销毁。
8.1 局部作用域(块作用域)
在函数内部或{}块内定义的变量。它们在定义处被创建,在所在块结束时被销毁。
{ int x = 10; // x 进入作用域 std::cout << x << std::endl; } // x 离开作用域,被销毁 // std::cout << x << std::endl; // 错误!x 在此处不可见8.2 全局作用域
在所有函数和类之外定义的变量。它在程序启动前创建,程序结束时销毁。在整个程序文件中都可见(但其他源文件需要使用extern来声明它)。
int g_global_var = 100; // 全局变量 int main() { std::cout << g_global_var << std::endl; // 可以访问 return 0; }注意:尽量减少使用全局变量。它们破坏了函数的封装性,使得程序状态难以追踪和理解,是滋生 Bug 的温床。
8.3 命名空间作用域
在命名空间内定义的变量。
namespace MyNamespace { int value = 42; } int main() { std::cout << MyNamespace::value << std::endl; // 通过命名空间访问 return 0; }9. 运算符:操纵数据的工具
C++提供了丰富的运算符,除了算术运算符(+,-,*,/,%)、关系运算符(==,!=,<,>,<=,>=)、逻辑运算符(&&,||,!)这些基本的外,有几个需要特别注意:
- 自增/自减运算符:
++i(前缀) 和i++(后缀)。前缀式先增减后使用,后缀式先使用后增减。在不需要使用表达式值的场景下,推荐使用前缀式(++i),因为它通常性能稍好(对于内置类型没区别,但对于复杂的迭代器类型有区别)。 - 赋值与复合赋值:
=是赋值,+=,-=,*=,/=等是复合赋值。a += b等价于a = a + b,但前者更高效(对某些类型而言,只计算一次)。 - 条件运算符(三目运算符):
condition ? expr1 : expr2。它是if-else的表达式版本,可以用于赋值。int max = (a > b) ? a : b; - 逗号运算符:
,。按顺序执行其两边的表达式,并返回右边表达式的结果。优先级最低。常用于for循环的初始化或迭代部分。for(int i = 0, j = 10; i < j; ++i, --j) { ... } sizeof运算符:返回类型或对象所占的字节数。sizeof(int),sizeof(x)。注意它不是函数,是运算符。
10. 控制流:决定代码的执行路径
程序默认是顺序执行的。控制流语句让我们可以改变这个顺序。
10.1 条件语句:if,else if,else,switch
if-else是最常用的。if (score >= 90) { grade = ‘A’; } else if (score >= 80) { grade = ‘B’; } else { grade = ‘C’; }注意:
if后面的条件必须用括号()括起来。即使只有一条语句,也建议用{}括起来,避免“悬垂 else”等错误,提高代码可读性。switch用于基于一个整型或枚举类型的值进行多路分支。switch(choice) { case 1: std::cout << “You chose 1.” << std::endl; break; // 必须用 break 跳出,否则会“贯穿”到下一个 case case 2: std::cout << “You chose 2.” << std::endl; break; default: // 可选的,处理所有其他情况 std::cout << “Invalid choice.” << std::endl; break; }switch的陷阱:忘记写break是常见错误,会导致程序继续执行下一个case的语句,这可能不是你想要的(除非你刻意设计成“贯穿”)。
10.2 循环语句:while,do-while,for, 范围for
while:先判断条件,再执行循环体。while (condition) { // loop body }do-while:先执行一次循环体,再判断条件。循环体至少执行一次。do { // loop body } while (condition);for:最结构化的循环,将初始化、条件判断、迭代放在一行。
C++11引入了范围for (int i = 0; i < 10; ++i) { std::cout << i << std::endl; }for循环,用于遍历一个序列(如数组、vector、string)的所有元素,非常简洁。
如果想修改序列中的元素,需要使用引用:std::vector<int> vec = {1, 2, 3, 4, 5}; for (int num : vec) { // 遍历 vec,每个元素依次赋值给 num std::cout << num << “ ”; } // 输出:1 2 3 4 5for (int& num : vec) { num *= 2; // 将 vec 中每个元素乘以 2 }
10.3 跳转语句:break,continue,goto
break:跳出当前所在的switch或循环语句(while,do-while,for)。continue:跳过当前循环迭代中剩余的语句,直接进入下一次循环的条件判断。goto:无条件跳转到同一函数内的某个标签处。强烈不建议使用,因为它会严重破坏程序的结构化,使流程难以跟踪,是“面条代码”的元凶。现代编程中几乎没有任何必须使用goto的场景。
11. 数组与字符串:数据的集合
11.1 C风格数组
一组相同类型元素的连续存储。
int arr1[5]; // 定义一个包含5个int的数组,值未初始化(随机值) int arr2[5] = {1, 2, 3}; // 前三个元素初始化为1,2,3,后两个默认初始化为0 int arr3[] = {1, 2, 3, 4, 5}; // 编译器自动推断数组大小为5关键点:
- 数组大小必须是编译期常量表达式。
- 数组名在很多情况下会退化为指向其首元素的指针(
int*)。 - 访问元素:
arr[index],索引从0开始。 - 数组越界访问是未定义行为,可能导致程序崩溃或数据损坏,且编译器通常不报错!这是C/C++程序中最常见的错误来源之一。
11.2 C风格字符串
本质是字符数组,以空字符‘\0’作为结尾标识。
char str1[] = “Hello”; // 编译器会自动在末尾添加 ‘\0’,数组长度为6 char str2[10] = “World”; // 正确 char str3[] = {‘H’, ‘i’, ‘\0’}; // 手动添加 ‘\0’操作C风格字符串需要使用<cstring>头文件中的函数,如strlen,strcpy,strcat,strcmp等。这些函数需要手动管理内存且容易出错(缓冲区溢出)。
11.3 C++标准库string类型(推荐)
#include <string>后,可以使用std::string。它封装了字符序列,自动管理内存,提供了丰富的成员函数(如.size(),.append(),.find(),.substr()等),安全且易用。
std::string s1 = “Hello”; std::string s2(“World”); std::string s3 = s1 + “ “ + s2; // 字符串拼接,非常方便 std::cout << “Length: ” << s3.length() << std::endl;string与cin:如前所述,cin >> s1;会读取到空白符为止。要读取一行,请使用std::getline(std::cin, s1);。
12. 结构体struct:自定义数据聚合
当基本类型不够用,需要将多个相关的数据项组合成一个整体时,就用结构体。
// 定义结构体类型 struct Student { std::string name; int id; double score; }; // 注意这里的分号! int main() { // 创建结构体变量并初始化 Student stu1 = {“张三”, 1001, 95.5}; Student stu2; stu2.name = “李四”; stu2.id = 1002; stu2.score = 88.0; // 访问成员 std::cout << stu1.name << “’s score is ” << stu1.score << std::endl; return 0; }结构体在C++中与C语言最大的区别是,C++的结构体不仅可以有数据成员,还可以有成员函数(方法)、构造函数、访问控制等,几乎和class一样。我们会在面向对象部分详细讨论。
13. 实战练习与调试入门
理解了这么多概念,不动手写代码是记不住的。我们来完成一个综合小练习,并初步接触调试。
13.1 练习:简易学生成绩统计程序
目标:从用户输入读取若干个学生的姓名和成绩(假设成绩为整数),然后计算平均分、最高分、最低分,并输出所有学生信息。
#include <iostream> #include <string> #include <climits> // 用于 INT_MIN, INT_MAX struct Student { std::string name; int score; }; int main() { const int MAX_STUDENTS = 100; // 假设最多100名学生 Student students[MAX_STUDENTS]; int numStudents = 0; std::cout << “请输入学生数量:”; std::cin >> numStudents; std::cin.ignore(); // 忽略数字后的换行符,为后续 getline 做准备 if (numStudents <= 0 || numStudents > MAX_STUDENTS) { std::cout << “学生数量无效!” << std::endl; return 1; // 非零返回值表示错误 } int totalScore = 0; int maxScore = INT_MIN; // 初始化为最小整数 int minScore = INT_MAX; // 初始化为最大整数 Student* topStudent = nullptr; // 指向最高分学生的指针 Student* bottomStudent = nullptr; // 指向最低分学生的指针 for (int i = 0; i < numStudents; ++i) { std::cout << “请输入第 ” << (i + 1) << “ 位学生的姓名:”; std::getline(std::cin, students[i].name); std::cout << “请输入 ” << students[i].name << “ 的成绩:”; std::cin >> students[i].score; std::cin.ignore(); // 忽略成绩后的换行符 // 统计 totalScore += students[i].score; if (students[i].score > maxScore) { maxScore = students[i].score; topStudent = &students[i]; // 记录地址 } if (students[i].score < minScore) { minScore = students[i].score; bottomStudent = &students[i]; } } double averageScore = static_cast<double>(totalScore) / numStudents; // 输出结果 std::cout << “\n===== 成绩统计结果 =====” << std::endl; std::cout << “平均分:” << averageScore << std::endl; std::cout << “最高分:” << maxScore << “ (学生:” << (topStudent ? topStudent->name : “N/A”) << “)” << std::endl; std::cout << “最低分:” << minScore << “ (学生:” << (bottomStudent ? bottomStudent->name : “N/A”) << “)” << std::endl; std::cout << “\n===== 所有学生信息 =====” << std::endl; for (int i = 0; i < numStudents; ++i) { std::cout << “姓名:” << students[i].name << “, 成绩:” << students[i].score << std::endl; } return 0; }13.2 调试(Debugging)初探
程序写出来,很少能一次运行成功。学会调试是程序员最重要的技能之一。
- 理解编译错误:编译器(如g++)报错时,会给出错误行号和大概原因。从第一个错误开始看,因为后面的错误可能是由前面的错误引发的。常见的错误有:语法错误(缺分号、括号)、类型不匹配、未声明的标识符等。
- 使用打印调试法:在怀疑有问题的地方插入
std::cout语句,输出变量的值,观察程序执行流程和状态是否与预期一致。这是最简单直接的调试方法。 - 使用集成开发环境(IDE)的调试器:以 VSCode 为例(配置好 C++ 环境后):
- 在你关心的代码行号左侧点击,可以设置一个断点(红色圆点)。
- 按
F5启动调试(Debug),程序会在断点处暂停。 - 此时你可以:
- 单步执行:
F10(Step Over)执行当前行,不进入函数内部;F11(Step Into)进入被调用的函数内部。 - 观察变量:在左侧的“变量”窗口,或鼠标悬停在代码中的变量上,可以看到变量当前的值。
- 继续运行:
F5(Continue)继续运行直到下一个断点或程序结束。 - 停止调试:
Shift+F5。 通过调试器,你可以像“慢放电影”一样观察程序的每一步执行,精准定位逻辑错误。
- 单步执行:
14. 常见问题与排查技巧实录
这里汇总了初学者在这个阶段最容易踩的坑及其解决方法。
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
编译错误:‘cout’ was not declared in this scope | 1. 忘记#include <iostream>。2. 使用了 cout而不是std::cout,且没有using namespace std;。 | 1. 添加#include <iostream>。2. 使用 std::cout或在合适作用域内添加using std::cout;。 |
编译错误:expected ‘;’ before ‘}’ token | 在定义结构体、类、枚举或命名空间时,右花括号}后面漏掉了分号;。 | 在struct Student { ... };这类定义的右花括号后加上分号。 |
| 程序运行后一闪而过,看不到输出 | 程序在控制台输出后立即结束。在有些IDE(如直接双击运行)中,控制台窗口会随之关闭。 | 1. 在main函数return前加system(“pause”);(Windows) 或getchar();。2.更好的方法:在 IDE 中配置为“运行时不自动关闭控制台”,或直接在终端/命令行中运行程序。 |
使用cin >>读取字符串后,后面的getline读不到内容 | cin >>读取数字或单词后,将换行符留在了输入缓冲区,getline直接读取了这个换行符。 | 在cin >>之后、getline之前,使用cin.ignore()清空缓冲区中的换行符。 |
| 数组访问时程序崩溃或输出奇怪值 | 数组越界访问。访问了不属于数组的内存。 | 1. 仔细检查循环条件,确保索引i满足0 <= i < 数组大小。2. 考虑使用 std::vector代替原生数组,它提供了.at()方法(会进行边界检查)和.size()方法。 |
两个字符串用==比较,结果总是不对 | 对于 C风格字符串(char[]或char*),==比较的是指针地址,而不是字符串内容。 | 1. 使用 C 库函数strcmp(str1, str2),返回 0 表示相等。2.强烈推荐:使用 std::string,它重载了==运算符,可以直接比较内容。 |
switch语句中,多个case都被执行了 | 忘记在case分支的末尾写break;语句。 | 在每个case分支的代码块末尾添加break;,除非你确实需要“贯穿”到下一个case。 |
定义一个大数组(如int arr[1000000];)导致栈溢出 | 在函数内部(局部作用域)定义非常大的数组,会占用大量栈空间,而栈空间通常有限(几MB)。 | 1. 将数组定义为全局变量(在堆内存中)。 2. 使用 std::vector,它在堆上分配内存。3. 使用 new动态分配(但需记得delete)。 |
我个人在实际编码中的一个强烈习惯是:对于所有用户输入,都假设它可能是错误的。所以,在像cin >> numStudents;这样的操作后,我会检查cin的状态,或者对输入值进行有效性判断(如检查是否为正数、是否超过数组边界)。上面的示例代码中if (numStudents <= 0 || numStudents > MAX_STUDENTS)就是这种防御性编程思想的体现。它能极大提高程序的健壮性,避免因意外输入导致崩溃或安全漏洞。