C++入门:从Hello World到变量、运算符与控制流

📅 2026/7/12 12:28:47
C++入门:从Hello World到变量、运算符与控制流
1. 从“Hello World”到理解程序骨架上一期我们聊了聊C的“前世今生”也把开发环境给搭好了。今天咱们就动真格的亲手写出第一个C程序并把它掰开揉碎了看看这行行代码背后到底藏着什么门道。很多新手觉得写个“Hello World”太简单看一眼就过去了其实这里面的每一个字符、每一个符号都是你理解C世界运行规则的敲门砖。咱们就从那个最经典的程序开始#include iostream int main() { std::cout Hello, World! std::endl; return 0; }别急着复制粘贴运行。咱们一行一行来我保证看完这部分你对程序的理解会比单纯跑出一个黑框框深刻得多。1.1 程序入口main函数的绝对核心首先请你盯着int main()这一行。在C里main函数拥有至高无上的地位——它是你写的程序开始执行的地方。操作系统加载你的程序后第一个找的就是它。你可以把它想象成一部电影的开场或者一栋大楼的主入口所有故事都从这里开始。int是main函数的返回类型。它表示这个函数执行完毕后会向操作系统返回一个整数。这个返回值通常用来告诉操作系统程序是正常结束还是出了什么错误。按照约定返回0通常表示“一切正常”。main后面的()里面是参数列表虽然我们这个简单的例子是空的但它其实可以接收来自命令行的参数比如int main(int argc, char* argv[])这个我们以后在需要和命令行交互时会详细讲。紧接着的一对花括号{和}定义了main函数的函数体。所有属于这个函数的代码都必须写在这对花括号里面。这是C语法规定的“作用域”边界。注意main函数有且只能有一个。一个项目里如果有多个.cpp文件也只能有一个文件里定义了main函数。它是整个程序的唯一起点。1.2 输入输出的桥梁#include iostream与std::cout现在看第一行#include iostream。这行代码不是给CPU执行的指令而是给编译器看的“预处理指令”。你可以把它理解成“抄作业”——在编译之前编译器会跑到一个叫“标准库”的地方找到名为iostream的文件Input/Output Stream输入输出流然后把里面的内容原封不动地“复制粘贴”到你当前这个文件的开头。为什么要“抄”它因为我们需要用到里面定义好的工具比如std::cout。iostream这个头文件里定义了所有处理标准输入键盘和标准输出屏幕所需的基础设施。没有这行#include编译器就不知道std::cout是个什么东西会直接报错。std::cout就是我们要用的核心工具。拆开看std这是一个“命名空间”。你可以把它想象成一个大家族标准库的姓氏。标准库里的所有东西比如cout,endl,string等都姓std。这是为了避免和咱们自己起的名字冲突。比如你自己写了个函数也叫cout如果没有命名空间编译器就分不清你到底想用哪个。::这是作用域解析运算符读作“作用域下的”。std::cout连起来就是“std这个家族里的cout成员”。cout念作 “C-out”它是“character output”字符输出的缩写代表“标准输出流”通常指向你的屏幕终端。那么std::cout Hello, World!这行代码在干什么在这里不是“左移”运算符虽然它在做数学运算时是而是被“重载”成了“流插入运算符”。它的作用是把右侧的内容字符串Hello, World!“插入”或“输送”到左侧的流cout中去。形象点说就像一条管道cout你用把数据Hello, World!推进管道数据就从管道的另一端你的屏幕流出来了。1.3 换行与刷新std::endl的微妙之处我们接着输送了std::endl。endl是 “end line” 的缩写它主要做两件事插入一个换行符\n让光标跳到下一行。刷新输出缓冲区。第二点非常关键也是新手容易忽略的。为了效率程序向屏幕或文件输出数据时并不是输出一个字符就立刻写一次屏幕而是先攒在内存里一个叫“缓冲区”的地方等缓冲区满了或者遇到特定的“刷新”指令时才一次性把数据写出去。std::endl在换行的同时会强制立刻刷新缓冲区确保你看到的信息是实时、完整的。那么能不能用\n代替endl呢绝大多数情况下可以。\n只是一个换行符它不负责强制刷新。对于cout到屏幕的输出系统通常会在缓冲区满、程序正常结束或遇到输入请求如cin时自动刷新。所以单纯为了换行用\n效率更高。但在一些实时日志、调试输出或者需要确保信息立刻可见的场景比如在多线程程序里使用endl进行强制刷新会更可靠。最后一行return 0;就是告诉操作系统“我main函数执行完了一切顺利返回状态码0”。这个分号;是C语句的结束标志绝大多数语句后面都需要它就像写句子要加句号一样。1.4 编译与运行从代码到可执行文件代码写好了怎么让它跑起来这背后是“编译-链接-运行”三步曲。编译你用编辑器写的.cpp文件是“源代码”人类能看懂但机器看不懂。编译器比如GCC, Clang, MSVC的工作就是把源代码翻译成机器能懂的“目标代码”通常是.o或.obj文件。这个过程会进行语法检查、语义分析等。如果代码有语法错误比如少了分号、括号不匹配就会在这一步报错。链接我们的程序用到了std::cout但这个函数的实现在哪里在标准库的预编译文件里比如libstdc.a或libc.dll。链接器的工作就是把我们编译好的目标文件和所有需要用到的库文件“链接”在一起组装成一个完整的、可以独立运行的可执行文件比如a.exe或a.out。运行操作系统加载这个可执行文件找到main函数开始执行里面的指令。在集成开发环境IDE如Visual Studio、Code::Blocks里你点一下“运行”或“构建”IDE就自动帮你完成了这三步。如果你在命令行下使用GCC对应的命令是g hello.cpp -o hello # 编译链接生成名为hello的可执行文件 ./hello # 运行这个可执行文件Linux/macOS hello.exe # 运行这个可执行文件Windows命令行2. 变量与数据类型程序记忆的基石程序不能只会说“Hello World”它得能记住东西、能算账。这就需要“变量”。变量就像一个个贴了名字的小盒子用来在程序运行期间存储数据。但C是个“强类型”语言你在声明一个盒子变量时必须明确告诉编译器这个盒子是专门用来装整数、小数还是字符的这就是“数据类型”。2.1 基本数据类型各司其职的“盒子”C提供了几种最基础的数据类型它们占用的内存大小和表示范围各不相同。选择合适的数据类型既能保证程序正确运行也能节省内存。数据类型中文名典型大小表示范围/用途示例int整型4字节-2,147,483,648 到 2,147,483,647int age 25;float单精度浮点型4字节约 ±3.4e±38 (6-7位有效数字)float price 19.99f;double双精度浮点型8字节约 ±1.7e±308 (15-16位有效数字)double pi 3.1415926535;char字符型1字节-128 到 127 (或 0 到 255)char grade A;bool布尔型1字节true(1) 或false(0)bool isReady true;声明与初始化int count; // 声明一个整型变量count此时它的值是未定义的垃圾值 count 10; // 赋值现在count的值是10 int total 100; // 声明的同时初始化推荐这种方式 float temperature 36.5f; // 注意给float赋值最好加‘f’后缀否则默认为double double distance 384400.5; // 天文数字或高精度计算用double char initial Z; // 字符用单引号 bool flag false;实操心得养成“声明即初始化”的好习惯。像int a;这样只声明不初始化如果后面忘记赋值就直接使用会读取到内存中的随机值垃圾值导致程序行为不可预测这种Bug非常隐蔽难查。直接写成int a 0;就安全多了。2.2 常量的力量const与宏定义有些值在程序运行过程中是不应该被改变的比如圆周率π、游戏窗口的宽度、一天的小时数。对于这种值我们应该用“常量”来定义。使用const关键字推荐const double PI 3.141592653589793; const int MAX_SCREEN_WIDTH 1920; const std::string GREETING Welcome!;用const定义的常量有类型检查更安全也便于编译器优化。一旦试图修改PI的值编译器会直接报错。使用#define宏定义传统C风格需谨慎#define PI 3.141592653589793 #define MAX_WIDTH 1920#define是预处理指令在编译前进行简单的文本替换。它没有类型也不占用内存空间在符号表里容易出错。例如#define SQUARE(x) x*x调用SQUARE(12)会被替换成12*12结果是5而不是期望的9。在现代C中对于常量应优先使用const。2.3 类型修饰符微调数据范围基本数据类型前面还可以加修饰符来改变它们的含义和范围。signed有符号的默认。表示该类型可以包含正数和负数。unsigned无符号的。表示该类型只包含非负数0和正数从而使正数范围扩大一倍。short短型。通常比int小。long长型。通常比int大。long long超长整型C11。常见组合short int smallNumber; // 通常2字节 unsigned int positiveOnly; // 0 到 4,294,967,295 long long bigNumber; // 通常8字节用于大整数计算 unsigned char byteData; // 0 到 255常用于处理原始二进制数据一个关键技巧sizeof运算符不同系统、不同编译器下数据类型的大小可能略有差异。想知道一个类型或变量在你当前环境下到底占多少字节用sizeof运算符std::cout Size of int: sizeof(int) bytes std::endl; std::cout Size of double: sizeof(double) bytes std::endl; int myVar; std::cout Size of myVar: sizeof(myVar) bytes std::endl;这能帮助你写出可移植性更好的代码。3. 运算符与表达式让程序“思考”和“计算”有了能存数据的变量下一步就是操作它们。运算符就是用来对变量和常量进行运算的符号。表达式则由运算符和操作数变量、常量组成最终会计算出一个值。3.1 算术运算符基础的数学计算就是咱们小学学的加减乘除外加取余。运算符描述示例结果加法int c 5 3;c 8-减法int c 5 - 3;c 2*乘法int c 5 * 3;c 15/除法int c 5 / 2;c 2(整数除法)%取模求余数int c 5 % 2;c 1特别注意整数除法当两个整数相除时结果仍然是整数小数部分会被直接丢弃不是四舍五入。5 / 2的结果是2不是2.5。如果想要得到浮点数结果至少需要将一个操作数转换为浮点类型double result 5.0 / 2; // 正确 result 2.5 double result (double)5 / 2; // 正确显式类型转换3.2 关系与逻辑运算符做出判断程序需要根据条件做出选择这就需要关系运算符比较大小和逻辑运算符组合条件。关系运算符运算符描述示例结果等于5 3false!不等于5 ! 3true大于5 3true小于5 3false大于等于5 5true小于等于5 3false逻辑运算符运算符描述示例结果逻辑与(53) (24)true(两者都为真)逻辑或!逻辑非!(53)false(取反)避坑指南是赋值运算符才是比较相等运算符。新手最常犯的错误之一就是写条件判断时把误写成例如if (x 5)这会把5赋值给x并且整个表达式的值就是5非零被视为true导致逻辑错误。有些编译器会对此给出警告。3.3 赋值与复合赋值运算符简洁高效除了基本的C还提供了一系列复合赋值运算符让代码更紧凑。int a 10; a 5; // 等价于 a a 5; 现在 a 15 a - 3; // 等价于 a a - 3; 现在 a 12 a * 2; // 等价于 a a * 2; 现在 a 24 a / 4; // 等价于 a a / 4; 现在 a 6 a % 5; // 等价于 a a % 5; 现在 a 13.4 自增自减运算符和--这两个运算符用于将变量的值增加1或减少1非常常用但前缀和后缀形式有细微差别。前缀式(i,--i)先自增/自减然后返回变化后的值。后缀式(i,i--)先返回变化前的值然后自增/自减。int x 5; int y x; // x先变成6然后y得到6 // 此时 x6, y6 int a 5; int b a; // b先得到5然后a变成6 // 此时 a6, b5在循环和迭代中i和i单独成句时效果一样但后者前缀式理论上效率稍高因为它不需要保存旧值。对于内置类型如int编译器会优化差别不大但对于复杂的自定义类型如迭代器使用前缀式i是更好的习惯。4. 程序的控制流顺序、分支与循环程序默认是顺序执行的一行接一行。但现实世界的逻辑需要选择和重复这就需要控制流语句来改变代码的执行顺序。4.1 分支语句if,else if,else与switchif语句最基本的条件判断。int score 85; if (score 90) { std::cout 优秀 std::endl; } else if (score 60) { std::cout 及格 std::endl; } else { std::cout 不及格 std::endl; }如果if或else后面只有一条语句花括号可以省略但强烈建议永远不要省略。省略花括号是许多潜在Bug的根源尤其是在后续添加代码时。switch语句用于基于一个整型或枚举类型的值进行多路分支。char grade B; switch (grade) { case A: std::cout 优秀 std::endl; break; // 必须用break跳出否则会“贯穿”执行下一个case case B: case C: std::cout 良好 std::endl; // B和C都执行这里 break; case D: std::cout 及格 std::endl; break; default: // 所有case都不匹配时执行 std::cout 无效等级 std::endl; break; }switch只能判断相等性不能判断范围如case score 60:是错的。break语句至关重要它用于退出整个switch块。如果忘记写break程序会继续执行下一个case的语句直到遇到break或switch结束这称为“case穿透”有时可利用但通常是错误来源。4.2 循环语句for,while,do-while循环用于重复执行一段代码。for循环当你知道循环要执行多少次时最适用。结构清晰初始化、条件判断、迭代。// 打印数字1到10 for (int i 1; i 10; i) { std::cout i ; } std::cout std::endl;for循环的三个表达式都可以为空for (;;)是一个无限循环。while循环当循环次数不确定取决于某个条件时使用。先判断后执行。// 计算1加到100 int sum 0; int i 1; while (i 100) { sum i; i; } std::cout Sum is: sum std::endl;do-while循环和while类似但它是先执行一次循环体再判断条件。因此循环体至少会执行一次。// 至少让用户输入一次密码 std::string password; do { std::cout 请输入密码: ; std::cin password; } while (password ! secret); std::cout 密码正确 std::endl;循环控制语句break立即终止当前所在的循环for,while,do-while,switch跳出循环体。continue跳过当前循环中剩余的语句直接进入下一次循环的条件判断for循环会先执行迭代表达式。// 找到第一个能被7整除的数就停止 for (int i 1; i 100; i) { if (i % 7 ! 0) { continue; // 不能被7整除跳过后面直接i } std::cout 找到: i std::endl; break; // 找到了终止整个循环 }4.3 作用域与生命周期变量在哪里“活”着这是一个极其重要的概念。变量的“作用域”指的是它在代码中可以被访问的区域。“生命周期”指的是它从被创建获得内存到被销毁释放内存的时间段。局部变量在函数内部或代码块{}内声明的变量。它们的作用域仅限于所在的函数或代码块生命周期从声明处开始到所在代码块结束时结束。void myFunction() { int localVar 42; // 局部变量 // 只能在这个函数内使用localVar } // 函数结束localVar被销毁 if (true) { int blockVar 10; // 代码块内的局部变量 // 只能在这个if块内使用blockVar } // if块结束blockVar被销毁 // 这里无法访问 blockVar全局变量在所有函数包括main之外声明的变量。它的作用域是整个程序文件生命周期从程序开始运行到程序结束。应谨慎使用全局变量因为它们破坏了函数的封装性使程序状态难以追踪和调试。#include iostream int globalCounter 0; // 全局变量 void increment() { globalCounter; } int main() { std::cout globalCounter std::endl; // 0 increment(); std::cout globalCounter std::endl; // 1 return 0; }理解作用域是避免命名冲突和内存管理问题的关键。尽量将变量的作用域限制在最小的必要范围内这是写出健壮代码的基本原则之一。