1. 项目概述:初探C++的基石——变量
如果你刚打开C++这扇门,面对满屏的代码,可能会觉得有点眼花缭乱。别急,任何宏伟的建筑都是从一砖一瓦开始的,而在C++的世界里,“变量”就是最基础、也最重要的那块砖。今天我们不谈高深的算法,也不聊复杂的框架,就从一个最朴素的问题开始:如何在计算机里记住一个数字、一个名字,或者一个状态?答案就是变量。
简单来说,变量就是程序在运行时,用来存储数据的一块内存空间。你可以把它想象成一个带标签的盒子。标签就是变量的名字,比如age、price、isReady;而盒子里装的东西,就是变量的值,比如18、99.8、true。程序通过名字来找到这个盒子,读取或修改里面的内容。几乎所有你写的C++程序,都离不开变量的参与。无论是计算游戏角色的血量,还是处理用户输入的用户名,亦或是控制一个循环的次数,背后都是变量在默默工作。
理解变量,不仅仅是知道怎么声明一个int a = 10;,更重要的是理解其背后的机制:数据是如何被存放的,不同类型的“盒子”有什么规格限制,以及如何高效、安全地使用这些“盒子”。这直接关系到你写的程序是否健壮、高效。很多初学者遇到的“诡异”bug,比如计算结果不对、程序意外崩溃,追根溯源,往往是对变量的理解不够深入。因此,扎扎实实地学好变量,是C++入门路上最关键、也最值得投入时间的一步。无论你后续是想做游戏开发、高性能服务器,还是嵌入式系统,对变量的精准掌控都是你的核心能力。
2. 变量的核心概念与类型系统解析
2.1 变量声明与定义:从“计划”到“落实”
在C++中,使用一个变量前,你必须先告诉编译器它的存在。这个过程涉及两个紧密相关但又有区别的概念:声明(Declaration)和定义(Definition)。
声明更像是向编译器做一个“预告”:“喂,编译器,后面我会用一个叫totalScore的整数变量,你先记着这个名字。” 声明确定了变量的名字和类型,但并没有为它分配实际的内存空间。在单个源文件中,你可以多次声明同一个变量(尽管通常没必要)。
定义则是“预告”的落实。它同样告诉编译器变量的名字和类型,并且关键的一步是,它会导致编译器为这个变量分配内存空间。一个变量在整个程序中有且仅有一个定义(在同一个作用域内)。
对于大多数初学者而言,你写下的一行代码int score;通常既是声明也是定义。它引入了score这个名字,类型是int,并且编译器会为它预留出存放一个整数的内存。
extern int globalVar; // 这是一个声明(使用extern关键字)。它告诉编译器globalVar在其他地方(可能是另一个.cpp文件)定义了。 int localVar; // 这是一个定义。编译器会在此处为localVar分配内存。 localVar = 100; // 这是使用(赋值)已经定义好的变量。注意:
extern关键字对于初学者可能稍显陌生,它主要用于在多个源文件之间共享全局变量。在初学阶段,你大部分时间都在单个文件中操作,所以int a;这种形式就是最常见的定义方式。
2.2 基础数据类型:为数据选择合适的“容器”
C++提供了多种基础数据类型,你可以把它们理解为不同规格和用途的“盒子”。选对“盒子”类型,对于程序的正确性和效率至关重要。
整型 (Integer Types):用于存储整数。
int:最常用的整型,其大小(占用多少字节)通常与机器字长相关(例如在32/64位系统上常为4字节)。它是有符号的,可表示正负整数。short:短整型,比int小,常用于节省空间。long:长整型,通常不小于int。long long(C++11):更长的整型,用于处理非常大的整数。char:字符型,本质上也是存储小整数的整型,用于表示单个字符(如'A','1')。bool:布尔型,只有两个值:true(真,通常内部表示为1) 和false(假,通常内部表示为0)。
每种整型都有对应的无符号版本(在前面加
unsigned),如unsigned int,它只能表示非负整数,但正数表示范围比同尺寸的有符号类型大一倍。浮点型 (Floating-Point Types):用于存储带小数点的实数。
float:单精度浮点数,精度较低,占用4字节。double:双精度浮点数,精度高,是默认的浮点类型,占用8字节。long double:扩展精度浮点数,精度和大小因编译器而异。
无类型 (void):
void类型表示“无类型”或“空类型”。它不能用来定义普通变量(因为不知道分配多大内存),但主要用于函数返回值(表示函数不返回任何值)和指针类型(void*通用指针)。
如何选择类型?
- 默认整数用
int:除非有明确的内存限制或数值范围要求。 - 默认浮点数用
double:float的精度在多次运算后容易产生累积误差,double是更稳妥的选择。 - 明确无负数用
unsigned:比如年龄、数组下标、循环计数器(从0开始)。 - 字符和真假值用
char/bool。
2.3 类型修饰符与限定符:给“容器”加上特性
除了选择容器类型,你还可以给它加上一些“特性”。
- 符号性修饰符:
signed(有符号,默认)、unsigned(无符号)。只适用于整型。 - 长度修饰符:
short、long、long long。用于修饰整型,改变其存储大小和表示范围。
更重要的是一些限定符,它们定义了变量的行为属性:
const:常量限定符。被const修饰的变量,其值在初始化后不能再被修改。这是一个非常重要的安全特性,能防止意外修改。const double PI = 3.14159; // PI = 3.14; // 错误!不能修改 const 变量的值。使用
const是一种良好的编程习惯,它能向阅读代码的人清晰地传达“这个值不应该被改变”的意图。volatile:易变限定符。它告诉编译器,这个变量的值可能会被程序之外的代理(如硬件、中断服务程序、其他线程)改变,因此编译器不应对其做激进的优化(比如把变量值缓存到寄存器而不去内存读取)。在嵌入式或底层系统编程中常见。mutable(用于类成员):可变限定符。它允许在const成员函数内修改被mutable修饰的类成员变量。属于面向对象范畴,入门阶段了解即可。
2.4 变量命名规则与最佳实践
给变量起个好名字,其重要性不亚于写出正确的逻辑。混乱的命名(如a,b,c,tmp1)是“屎山代码”的起点。
C++命名规则(必须遵守):
- 名称只能由字母(a-z, A-Z)、数字(0-9)和下划线(_)组成。
- 名称的第一个字符必须是字母或下划线,不能是数字。
- 区分大小写。
myVar和myvar是两个不同的变量。 - 不能使用C++保留关键字(如
int,if,for,class)作为变量名。
命名最佳实践(强烈建议遵守):
- 见名知意:变量名应清晰描述其用途。
studentCount远比cnt或n要好。 - 采用一致的命名风格:
- 蛇形命名法 (snake_case):单词之间用下划线连接,如
total_score,is_game_over。常见于C++标准库和一些开源项目。 - 驼峰命名法 (camelCase):除第一个单词外,后续每个单词首字母大写,如
totalScore,isGameOver。 - 帕斯卡命名法 (PascalCase):每个单词首字母都大写,如
TotalScore,IsGameOver。在C++中通常用于类名和类型名。
- 蛇形命名法 (snake_case):单词之间用下划线连接,如
- 避免使用缩写:除非是像
idx(index),msg(message) 这样极其通用的缩写。 - 布尔变量:通常以
is,has,can等开头,如isRunning,hasPermission。 - 作用域小的变量:可以使用短名字,如循环计数器
i,j。
3. 变量的声明、定义与初始化实战
3.1 声明与定义的语法形式
理解了概念,我们来看看具体怎么写。变量的定义语法如下:数据类型 变量名;或者带初始化的定义:数据类型 变量名 = 初始值;数据类型 变量名 {初始值};// C++11引入的列表初始化,更安全
int age; // 定义了一个int型变量age,未初始化,其值是不确定的“垃圾值”。 double price = 19.99; // 定义并初始化了一个double型变量price。 char grade {'A'}; // 使用列表初始化定义了一个char型变量grade。 bool isPassed = true; // 定义并初始化了一个bool型变量isPassed。实操心得:务必养成定义时立即初始化的习惯!使用未初始化的变量是C/C++程序中一个非常常见且危险的错误来源,它会导致程序行为不可预测(读取到随机内存值),且难以调试。即使是打算稍后赋值,也最好先初始化为一个安全的默认值(如
int count = 0;)。
3.2 初始化的多种方式及其区别
C++提供了多种初始化方式,它们在某些细微场景下有区别:
拷贝初始化 (Copy Initialization):使用等号
=。int x = 5;这是最传统、直观的方式。
直接初始化 (Direct Initialization):使用圆括号
()。int x(5);对于简单类型,效果与拷贝初始化几乎相同。对于类对象,它调用的是匹配的构造函数。
列表初始化 (List Initialization):使用花括号
{}(C++11起)。int x{5}; int y = {5}; // 也可以这是C++现代编程中推荐的方式。它有一个关键优势:防止窄化转换(Narrowing Conversion)。即,如果初始值存在丢失信息的风险(如用浮点数初始化整数,或用大整数初始化小整数),编译器会报错或警告。
int a = 3.14; // 可能只给出警告,a的值是3(截断)。 int b{3.14}; // 错误!编译器会阻止这种可能导致信息丢失的初始化。 int c{3}; // 正确。使用
{}初始化能帮助你在编译期捕获更多潜在的类型错误。默认初始化 (Default Initialization):不提供显式初始值。
int x; // 默认初始化。对于内置类型(在函数内部),其值是未定义的。 std::string s; // 对于类类型,会调用其默认构造函数,s是一个空字符串。对于函数内的局部内置类型变量,默认初始化不会将其置零,访问它是危险的。
3.3 变量的作用域与生命周期
变量不是在任何地方都能被访问的,它有自己的“活动范围”和“生存时间”。
作用域 (Scope):变量在代码中可以被访问的区域。
- 局部作用域 (块作用域):在函数内部或
{}块内定义的变量。它们从定义点开始,到所在块的右花括号}结束。外部代码无法访问它们。void func() { int localVar = 10; // 局部变量 if (true) { int innerVar = 20; // 另一个更局部的变量 // 这里可以访问 localVar 和 innerVar } // 这里不能访问 innerVar,因为它已经“死”了 } // 这里不能访问 localVar - 全局作用域 (文件作用域):在所有函数和类之外定义的变量。它们在整个程序运行期间都存在,可以被当前文件(需要前置声明)或其他文件(使用
extern)访问。应谨慎使用全局变量,因为它们会破坏函数的封装性,导致代码难以理解和维护。
- 局部作用域 (块作用域):在函数内部或
生命周期 (Lifetime):变量从被创建(分配内存)到被销毁(释放内存)的时间段。
- 自动存储期:局部变量通常具有自动存储期。它们在进入作用域时创建,离开作用域时自动销毁。这是最常见的情况。
- 静态存储期:全局变量、使用
static关键字修饰的局部变量和类静态成员变量具有静态存储期。它们在程序开始时创建,程序结束时销毁。即使函数调用结束,static局部变量的值也会被保留。int globalVar = 0; // 静态存储期,全局变量 void counter() { static int count = 0; // 静态局部变量,只初始化一次 count++; std::cout << "Count: " << count << std::endl; } // 第一次调用 counter() 输出 Count: 1 // 第二次调用 counter() 输出 Count: 2 // count 的值在函数调用间得以保持 - 动态存储期:通过
new运算符在堆(Heap)上分配的内存,其生命周期由程序员手动控制(通过delete释放)。这部分内容涉及指针,是后续的重点。
理解作用域和生命周期,能帮助你避免“变量未定义”或“访问已释放内存”这类错误,并合理规划变量的定义位置。
4. 常量与符号常量:让不变的值更安全
在程序中,总有一些值是不应该被改变的,比如圆周率π、一天的小时数、程序的版本号等。使用常量来代表这些值,能极大提高代码的可读性、可维护性和安全性。
4.1 使用const定义常量
如前所述,const关键字用于定义常量。定义时必须进行初始化,且之后不能赋值。
const int MAX_BUFFER_SIZE = 1024; const double TAX_RATE = 0.08; const std::string COMPANY_NAME = "MyTech";使用const常量代替魔法数字(Magic Number,即直接出现在代码中的字面量)是基本的编程素养。比较下面两段代码:
// 糟糕的写法:魔法数字 area = 3.14159 * radius * radius; if (status == 1) { ... } // 良好的写法:使用常量 const double PI = 3.14159; const int STATUS_ACTIVE = 1; area = PI * radius * radius; if (status == STATUS_ACTIVE) { ... }后者清晰地表达了意图,修改常量值也只需在一处进行。
4.2 使用constexpr定义编译期常量 (C++11)
constexpr是C++11引入的更强大的常量限定符。它指定变量的值不仅是常量,而且必须是编译期就能确定的常量表达式。这意味着constexpr变量可以用于需要编译期常量的场合,比如数组大小、模板参数等。
constexpr int ARRAY_SIZE = 100; // 正确,100是编译期常量 int arr[ARRAY_SIZE]; // 可以用作数组大小 const int userInput = getUserInput(); // 假设这个函数运行时才返回值 constexpr int size = userInput; // 错误!userInput的值在编译期未知。 constexpr double computePi() { return 3.14159; } // constexpr函数 constexpr double PI = computePi(); // 正确,computePi()是constexpr函数,编译期可求值constvsconstexpr如何选?
- 如果你的常量值在编译期就能确定,并且可能用于需要编译期常量的上下文(如数组大小),优先使用
constexpr。 - 如果常量值需要在运行时才能确定(例如从文件或网络读取的配置),或者你只是简单地想防止变量被修改,使用
const。 - 在现代C++中,对于能使用
constexpr的场景,使用它是一个好习惯。
4.3 使用#define宏定义常量(及其弊端)
在C语言和早期C++中,常用预处理指令#define来定义常量。
#define MAX_USERS 100 #define PI 3.14159#define进行的是简单的文本替换,在编译前,所有出现MAX_USERS的地方都会被替换成100。
为什么不推荐使用#define定义常量?
- 没有类型检查:
#define只是文本替换,编译器不知道它的类型,容易引入难以察觉的类型错误。 - 没有作用域:
#define定义的宏从定义点开始到文件末尾(或遇到#undef)都有效,可能会意外污染其他部分的命名空间。 - 调试困难:调试器看到的符号是替换后的值(如
100),而不是MAX_USERS,不利于调试。 - 可能产生副作用:著名的例子
#define SQUARE(x) x*x,当调用SQUARE(a+1)时,会被替换成a+1*a+1,导致计算错误。
注意事项:在现代C++中,应尽量避免使用
#define来定义常量,使用const或constexpr是更安全、更现代的选择。#define在某些特定场景(如条件编译、防止头文件重复包含)仍有其用处,但那与定义常量无关。
5. 类型推导:让编译器帮你写类型 (C++11/14)
在C++11之后,你可以让编译器根据初始化表达式自动推导变量的类型,这减少了代码冗余,尤其在类型名很长(如迭代器)时非常有用。
5.1auto关键字
auto关键字用于自动类型推导。编译器会根据初始化表达式右侧的值,来确定auto变量的类型。
auto i = 42; // i 被推导为 int auto d = 3.14; // d 被推导为 double auto s = "hello"; // s 被推导为 const char* (注意:是指向字符串常量的指针) auto flag = true; // flag 被推导为 bool std::vector<int> vec = {1, 2, 3}; auto it = vec.begin(); // it 被推导为 std::vector<int>::iterator,省去了冗长的类型名使用auto的好处:
- 代码更简洁,尤其是面对复杂类型时。
- 避免因手写类型错误而导致的隐式转换。
- 当函数返回类型改变时,使用
auto接收的代码无需修改。
使用auto的注意事项:
auto变量必须初始化,否则编译器无法推导类型。- 要推导出引用类型,需要在
auto后加&。int x = 10; auto y = x; // y 是 int,是 x 的拷贝 auto& z = x; // z 是 int&,是 x 的引用,对 z 的修改会影响 x z = 20; // 现在 x 也变成了 20 - 要推导出常量类型,需要加
const。const int cx = 100; auto a = cx; // a 是 int (const 属性被剥离,因为 auto 默认推导值类型) const auto b = cx; // b 是 const int auto& c = cx; // c 是 const int&,因为 cx 是 const
5.2decltype关键字
decltype用于查询表达式的类型,它返回该表达式的声明类型,包括引用和常量限定符。
int x = 0; const int& crx = x; decltype(x) y; // y 的类型是 int decltype(crx) z = x; // z 的类型是 const int&,必须初始化 decltype(x + 5) sum; // sum 的类型是 int (x+5 表达式的类型) // 一个常见用法:在模板编程或需要精确匹配表达式类型时 template<typename T1, typename T2> auto add(T1 a, T2 b) -> decltype(a + b) { // 尾置返回类型,推导规则更精确 return a + b; }decltype比auto更“忠实”于原始表达式。auto推导类型时会忽略顶层const和引用(除非显式指定auto&或const auto),而decltype会保留它们。
5.3auto与decltype的适用场景
- 日常变量定义:当类型名冗长或显而易见时,使用
auto。例如迭代器、lambda表达式、复杂模板实例化结果。 - 需要精确类型匹配:当需要推导出的类型必须包含引用或常量性时,考虑使用
decltype或auto配合&/const。 - 泛型编程:在编写模板函数时,
decltype常用于尾置返回类型,以根据参数类型推导返回类型。
实操心得:不要滥用
auto。在类型清晰、能提高代码可读性(如int count = 0;)的地方,使用显式类型往往更好。auto的过度使用可能会让代码阅读者需要跳转到初始化处才能知道变量类型,降低可读性。一个平衡的原则是:让代码的意图清晰。如果类型名能更好地传达信息,就用显式类型;如果类型名冗长且无关紧要(如迭代器),就用auto。
6. 复合类型初探:从变量到数据的组织
基础变量可以存储单个数据。但现实世界的数据往往是成组出现的。C++提供了几种复合类型来组织数据,这里我们先简要介绍两种最基础的:数组和结构体,为后续深入学习指针和面向对象打下基础。
6.1 数组:存储同类型元素的集合
数组是一种用于存储固定大小、相同类型元素集合的数据结构。它在内存中是连续存放的。
声明与初始化:
// 声明一个包含5个整数的数组,未初始化 int scores[5]; // 声明并初始化 int primes[5] = {2, 3, 5, 7, 11}; // 完全初始化 int numbers[10] = {0}; // 第一个元素初始化为0,其余元素被默认初始化为0 int arr[] = {1, 2, 3}; // 编译器自动推断数组大小为3 // 访问数组元素,下标从0开始 scores[0] = 95; // 访问第一个元素 int secondPrime = primes[1]; // 访问第二个元素,值是3关键点:
- 大小固定:数组在定义时必须指定大小(或由初始化列表推断),且之后不能改变。
- 下标越界:C++不检查数组下标是否越界。访问
scores[5](一个只有5个元素的数组)是未定义行为,可能导致程序崩溃或数据损坏。这是数组最大的安全隐患。 - 数组名:在很多情况下,数组名会退化为指向其首元素的指针。这引出了指针与数组的紧密联系。
6.2 结构体:存储不同类型数据的集合
结构体(struct)允许你将多个不同类型的变量组合成一个单一的复合类型。这对于描述一个逻辑实体非常有用,比如一个学生、一本书、一个坐标点。
定义与使用:
// 定义一个名为Student的结构体类型 struct Student { int id; std::string name; double score; }; // 注意分号 // 声明结构体变量并初始化 Student stu1; // 默认初始化,成员有各自默认值(int未定义,string为空串) stu1.id = 1001; stu1.name = "Alice"; stu1.score = 92.5; // 使用花括号列表初始化 (C++11起,对struct也适用) Student stu2 = {1002, "Bob", 88.0}; Student stu3 {1003, "Charlie"}; // score被默认初始化为0.0 // 访问结构体成员使用点运算符 . std::cout << stu2.name << "'s score is " << stu2.score << std::endl;结构体将相关的数据封装在一起,使代码更有组织性。它是C++中“类”(class)的前身,在C++中,struct和class的主要区别默认的成员访问权限(struct默认public,class默认private)。
7. 常见问题、陷阱与调试技巧
7.1 未初始化变量
这是新手最常踩的坑之一。
int value; std::cout << value; // 危险!value的值是未定义的(垃圾值)。后果:程序行为不确定,可能每次运行结果都不同,极难调试。解决:定义变量时立即初始化。对于局部变量,这是一个必须养成的习惯。
7.2 变量作用域混淆
int main() { int x = 5; if (true) { int x = 10; // 内层作用域定义了新的x,遮蔽了外层的x std::cout << x; // 输出 10 } std::cout << x; // 输出 5 // 错误示例:试图访问不存在的变量 // for (int i = 0; i < 5; ++i) { ... } // std::cout << i; // 错误!i的作用域只在for循环体内。 }解决:清晰理解花括号{}划分的作用域。避免在嵌套作用域中使用相同名字的变量,除非有充分理由。
7.3 整数溢出与类型转换
short small = 32767; // short的最大值通常是32767 small = small + 1; // 溢出!结果是未定义的,通常是-32768(回绕) std::cout << small; // 输出可能是 -32768 unsigned int u = 0; u = u - 1; // 下溢!对于无符号数,结果是该类型能表示的最大值(4294967295)解决:选择足够大的数据类型来容纳可能的值。在进行运算时,注意隐式类型提升和转换规则。对于可能溢出的操作,进行前置检查。
7.4 浮点数精度问题
浮点数在计算机中是以二进制近似表示的,存在精度限制。
double a = 0.1; double b = 0.2; double c = a + b; std::cout << std::setprecision(20) << c << std::endl; // 可能输出 0.30000000000000004441 if (c == 0.3) { // 不要直接比较浮点数是否相等! // 这个条件很可能为 false }解决:比较浮点数时,不要使用==,而应该判断两数之差的绝对值是否小于一个很小的阈值(epsilon)。
const double EPSILON = 1e-10; if (std::fabs(c - 0.3) < EPSILON) { // 认为相等 }7.5 调试技巧:使用调试器观察变量
理论学习之外,熟练使用调试器是理解变量行为的利器。以VS Code配合GDB为例:
- 设置断点:在代码行号左侧点击,出现红点。
- 启动调试:按F5。
- 观察变量:
- 变量窗口:会自动显示当前作用域内的变量及其值。
- 监视窗口:可以添加任意表达式(如
a + b)进行持续观察。 - 悬停:在调试暂停时,将鼠标悬停在代码中的变量上,会显示其当前值。
- 单步执行:F10(逐过程)、F11(逐语句),观察变量值如何随着代码执行而改变。
通过调试器,你可以直观地看到变量的创建、赋值、销毁过程,以及作用域的实际效果,这对于理解抽象概念有巨大帮助。
8. 从理论到实践:一个简单的变量应用案例
让我们用一个简单的控制台小程序来串联本章的知识点。这个程序会模拟一个简单的学生成绩录入与统计。
#include <iostream> #include <string> #include <iomanip> // 用于格式化输出 int main() { // 使用 constexpr 定义编译期常量 constexpr int MAX_STUDENTS = 5; // 定义一个结构体类型 struct StudentRecord { int id; std::string name; double score; }; // 声明一个结构体数组 StudentRecord students[MAX_STUDENTS]; // 录入学生信息 std::cout << "请输入" << MAX_STUDENTS << "位学生的信息:\n"; for (int i = 0; i < MAX_STUDENTS; ++i) { // 循环计数器 i 是局部变量 std::cout << "\n学生 #" << (i + 1) << ":\n"; std::cout << " 学号: "; std::cin >> students[i].id; std::cout << " 姓名: "; std::cin >> students[i].name; // 简单输入,姓名不能有空格 std::cout << " 成绩: "; std::cin >> students[i].score; // 简单的输入验证(防止成绩为负数) if (students[i].score < 0.0) { std::cout << "警告:成绩不能为负数,已自动设置为0。\n"; students[i].score = 0.0; } } // 计算平均分和最高分 double totalScore = 0.0; // 务必初始化 double maxScore = 0.0; // 假设成绩非负 int topScorerIndex = 0; // 最高分学生的索引 for (int i = 0; i < MAX_STUDENTS; ++i) { totalScore += students[i].score; if (students[i].score > maxScore) { maxScore = students[i].score; topScorerIndex = i; // 更新索引 } } const double averageScore = totalScore / MAX_STUDENTS; // 使用 const // 输出结果 std::cout << std::fixed << std::setprecision(2); // 设置输出格式,保留两位小数 std::cout << "\n===== 成绩统计结果 =====\n"; std::cout << "平均分: " << averageScore << std::endl; std::cout << "最高分: " << maxScore << std::endl; std::cout << "最高分学生: " << students[topScorerIndex].name << " (学号: " << students[topScorerIndex].id << ")\n"; // 输出所有学生信息 std::cout << "\n所有学生信息:\n"; std::cout << std::setw(10) << "学号" << std::setw(15) << "姓名" << std::setw(10) << "成绩" << std::endl; std::cout << std::string(35, '-') << std::endl; for (const auto& stu : students) { // 使用 auto 和 范围for循环 (C++11) std::cout << std::setw(10) << stu.id << std::setw(15) << stu.name << std::setw(10) << stu.score << std::endl; } return 0; }这个案例涵盖了:
constexpr定义数组大小。struct定义复合数据类型。- 数组存储多个结构体。
- 局部变量、循环变量的使用。
const常量的使用。auto在范围for循环中的应用。- 变量的初始化、赋值、运算。
- 基本的输入/输出和格式化。
你可以尝试编译运行这个程序,并用调试器一步步跟踪,观察每个变量(如i,totalScore,students[i].score)在程序执行过程中的变化,这将极大地加深你对“变量的世界”的理解。