从零到一学习C++(基础篇) 作者:羡鱼肘子
温馨提示1:本篇是记录我的学习经历,会有不少片面的认知,万分期待您的指正。
温馨提示2:本篇会尽量用更加通俗的语言介绍c++的基础,用通俗的语言去解释术语,但不会再大白话了哦。常见,常看,常想,渐渐的就会发现术语也是很简单滴。
温馨提示3:看本篇前可以先了解前篇的内容,知识体系会更加完整哦。
从零到一学习c++(基础篇--筑基期八-表达式)-CSDN博客
温馨提示4:个人感觉对环境的适应还是挺重要的。
一、基础语句类型
1. 表达式语句:就是做一件事
简单来说:就像你每天做的小任务,比如“倒一杯水”“关灯”。
代码本质:任何表达式后加 ;
就是一个语句。
int x = 5; // 赋值表达式语句
x++; // 自增表达式语句
cout << "Hi"; // 输出表达式语句
2. 复合语句(块):打包多个任务
简单理解:把多个任务装进一个“盒子”({}
),比如“早晨流程”:{刷牙 → 洗脸 → 吃早餐}。
代码本质:用 {}
包裹多个语句,形成独立作用域。
{ // 开始一个块int x = 10; // 这个 x 只在块内有效cout << x << endl; // 输出 10
} // 块结束,x 被销毁
// cout << x; // 这里会报错,x 不存在了
3. 选择语句:条件判断
(1) if-else
:二选一
一个栗子:如果温度大于30° → 开空调;否则 → 不开。
还可以是这样if-else if-else (理论上可以有很多的else if 但是不建议这样写哦)
int temperature = 25;
if (temperature > 30) {cout << "开空调!";
} else if (temperature < 10) {cout << "穿羽绒服!";
} else {cout << "出门散步~";
}
(2) switch
:多选一
我们来看看饮料机:选择饮料机按钮:1→可乐,2→雪碧,其他→水。
int choice = 2;
switch (choice) {case 1:cout << "可乐";break; // 必须写 break,否则会继续执行下一个 case!case 2:cout << "雪碧";break;default:cout << "水";
}
4. 迭代语句:重复做某件事
(1) for
循环:明确次数
就像:做 10 个俯卧撑 → 从1数到 10 。
for (int i = 0; i < 10; i++) { // i从0到9cout << "第 " << i+1 << " 个俯卧撑!" << endl;
}
(2) while
循环:条件满足就一直做
就像:只要没吃饱 → 继续吃
int hunger = 80; // 饥饿值(0-100)
while (hunger > 30) {cout << "吃一口饭..." << endl;hunger -= 20;
}
do-while
:条件循环
就像:先吃一口看看饱了没,只要没吃饱 → 继续吃
do { /*...*/ } while (cond);
温馨小贴士:while VS do-while
核心区别
特性 while
循环do-while
循环执行顺序 先检查条件,条件满足才执行循环体 先执行一次循环体,再检查条件 最少执行次数 0 次(条件不满足时直接跳过) 至少 1 次(无论如何都会先执行一次) 适用场景 需要先检查条件再执行的情况 必须至少执行一次的情况 语法 while (条件) { ... }
do { ... } while (条件);
(注意分号)
对比
1.
while
循环:先检查,后行动
你准备去超市买苹果,但先检查钱包是否有钱:
有钱 → 进去买苹果。
没钱 → 直接回家,不进去。
2.
do-while
循环:先行动,后检查
你先冲进超市拿苹果,然后到收银台检查钱包:
有钱 → 结账离开。
没钱 → 放下苹果离开,但已经拿过苹果了。
场景1:用户输入验证
1.
while
循环要求用户必须输入正整数,否则重新输入:
int num; cout << "请输入一个正整数:"; cin >> num;// 先检查输入是否合法,不合法就循环要求重新输入 while (num <= 0) {cout << "输入错误!请重新输入:";cin >> num; }
需求:需要先写一遍输入代码,导致重复!
2.
do-while
循环优化代码,避免重复:
int num; do {cout << "请输入一个正整数:";cin >> num; } while (num <= 0); // 输入后检查条件
优势:无论用户第一次输入是否正确,至少会执行一次输入操作,代码更简洁。
关键细节
do-while
末尾必须有分号:do { ... } while (条件); // 分号不可少!
避免无限循环:
确保循环体内有改变条件的代码,例如:int i = 0; do {cout << i << endl;i++; // 必须修改条件,否则无限循环! } while (i < 5);
小结:如何选择?
用
while
:当可能不需要执行循环体时(例如遍历链表前检查是否为空)。用
do-while
:当必须至少执行一次循环体时(例如菜单交互、输入验证)。一句话记住:
while
:“先看路,再开车”。
do-while
:“先开车,再看路”。
(3) 范围 for
(C++11 现代特性)
就像:自动遍历购物清单里的每个商品。
vector<string> list = {"苹果", "牛奶", "面包"};
for (const auto& item : list) { // 自动遍历容器cout << "买了:" << item << endl;
}
5. 跳转语句:改变执行顺序
(1) break
:立刻停止
for (int i = 0; i < 100; i++) {if (i == 5) {break; // 当i=5时,直接跳出循环}cout << i << endl; // 只会输出0-4
}
(2) continue
:跳过当前,继续下次
for (int i = 0; i < 5; i++) {if (i == 2) {continue; // 跳过i=2的情况}cout << i << endl; // 输出0,1,3,4
}
(3) return
:从函数返回
int add(int a, int b) {return a + b; // 返回结果,函数结束
}
二、现代C++特性增强
1. 范围 for
循环(C++11)(底层是迭代器)
简化容器遍历,支持自定义类型(需实现 begin()
/end()
):
std::vector<int> vec{1, 2, 3};
for (const auto& elem : vec) { // 只读用 const auto&std::cout << elem;
}
2. 带初始化的 if
/switch
(C++17)
限制变量作用域,增强代码安全性:
if (auto it = m.find(key); it != m.end()) {// it 仅在块内可见
}
switch (int x = compute(); x) { /*...*/ }
温馨小贴士:
我们先来看看代码吧
1. 带初始化的
if
语句if (auto it = m.find(key); it != m.end()) {// it 仅在块内可见 }
功能:在
if
的条件部分声明变量并判断条件。关键点:
变量初始化:
auto it = m.find(key)
在条件中初始化迭代器it
(假设m
是map
或类似容器)。条件判断:
it != m.end()
检查是否找到键值对。作用域限制:
it
仅在if
块内可见,外部无法访问,避免命名污染。(这里是关键哦)对比传统写法:
auto it = m.find(key); // 变量暴露在外部作用域 if (it != m.end()) {// ... }
2. 带初始化的
switch
语句switch (int x = compute(); x) {case 1: /*...*/ break;case 2: /*...*/ break;default: /*...*/ }
功能:在
switch
的条件部分初始化变量并作为判断依据。关键点:
变量初始化:
int x = compute()
执行函数compute()
并将返回值赋给x
。判断依据:
switch
根据x
的值跳转到对应case
。作用域限制:
x
仅在switch
块内可见。(这里很重要)对比传统写法:
int x = compute(); // 变量暴露在外部作用域 switch (x) {// ... }
现代C++特性(C++17)(强化认知阶段)
1. 带初始化的条件语句
允许在
if
/switch
的条件中声明变量,语法为:
if (初始化语句; 条件) { ... }
switch (初始化语句; 变量) { ... }
核心优势:
作用域限制:变量仅在条件语句块内有效,避免命名冲突。
代码简洁性:将变量声明与条件判断合并,逻辑更紧凑。
安全性:防止变量在外部被误用。
2. 应用场景
if
语句:适用于需要临时变量进行条件判断的场景(如查找容器元素)。
switch
语句:适用于需要根据计算结果进行多分支选择的场景。
示例对比(传统 vs 现代)
传统写法
// if 示例 auto it = m.find(key); // 变量在外部作用域 if (it != m.end()) {// ... } // 此处仍可访问 it,可能引发误操作!// switch 示例 int x = compute(); // 变量在外部作用域 switch (x) {case 1: /*...*/ break; } // 此处仍可访问 x
现代写法(C++17)
// if 示例 if (auto it = m.find(key); it != m.end()) {// 仅在此块内可访问 it } // 此处无法访问 it,避免误用// switch 示例 switch (int x = compute(); x) {case 1: /*...*/ break; } // 此处无法访问 x
小结一下:
作用域控制:通过将变量声明限制在条件语句内,提升代码安全性。
代码可读性:将变量初始化与条件判断合并,逻辑更清晰。
适用性:适用于需要临时变量的条件判断场景。
3. constexpr if
(C++17)(目前作为了解内容,后续会结合项目来演示学习)
编译期条件判断,简化模板代码:
template <typename T>
void process(T val) {if constexpr (std::is_integral_v<T>) {// 仅对整型编译此分支} else {// 其他类型}
}
4. 结构化绑定(C++17)(这个很重要哦)
解包元组或结构体,常用于范围 for
:
pair<string, int> student = {"Tom", 85};
auto& [name, score] = student; // 解包 pair
cout << name << "得了" << score << "分";
5. 异常处理改进(作为了解,就是提到的话自己知道有这个东西就行)
-
noexcept
说明符(C++11):声明函数不抛出异常。 -
try
/catch
:现代C++推荐用智能指针等资源管理技术替代裸new
/delete
,减少异常风险。
温馨小贴士:(初步认知阶段,会有一点的困难,但是不要放弃哦,坚持下去会有结果的)
一、
noexcept
说明符(C++11)1. 核心作用
声明函数不抛出异常:告知编译器该函数不会抛出异常,允许编译器进行优化。
优化性能:编译器无需为
noexcept
函数生成异常处理代码,减少二进制体积。接口约束:调用方可以依赖此声明,无需处理该函数抛出的异常。
2. 语法与规则
void func() noexcept; // 不抛出任何异常 void func() noexcept(true); // 等价于 noexcept void func() noexcept(false); // 可能抛出异常
3. 示例分析
#include <stdexcept>// 传统函数可能抛出异常 int unsafe_divide(int a, int b) {if (b == 0) throw std::invalid_argument("除数不能为0");return a / b; }// 使用 noexcept 声明不抛异常(但实际可能抛出,需谨慎!) int safe_divide(int a, int b) noexcept {return a / b; // 若b=0,触发未定义行为(程序终止) }int main() {try {// unsafe_divide(10, 0); // 抛出异常,可以被捕获safe_divide(10, 0); // noexcept函数抛出异常,程序终止} catch (const std::exception& e) {std::cerr << "捕获异常: " << e.what() << std::endl;}return 0; }
4. 适用场景
移动构造函数/赋值运算符:标准库容器在重新分配内存时优先使用
noexcept
移动操作。高性能关键路径:确保函数不会因异常影响性能。
明确接口行为:例如析构函数默认
noexcept
,手动声明可增强可读性。
二、智能指针替代
new
/delete(后期会单独学习智能指针,这个是内存管理的基础是非常非常重要的)
1. 核心改进
资源自动释放:通过RAII(资源获取即初始化)管理内存,避免因异常导致内存泄漏。
异常安全:即使异常发生,智能指针的析构函数仍会释放资源。
2. 对比示例
传统写法(裸指针 +
try/catch
)void process_file() {int* data = new int[100]; // 动态分配内存try {// 可能抛出异常的操作read_file(data); // 假设 read_file 可能抛出异常process(data);} catch (const std::exception& e) {delete[] data; // 必须手动释放!throw; // 重新抛出异常}delete[] data; // 正常流程释放 }
问题:若
read_file
或process
抛出异常且未在catch
块中释放内存,会导致内存泄漏。
现代写法(智能指针)
#include <memory>void process_file() {auto data = std::make_unique<int[]>(100); // 使用 unique_ptrtry {read_file(data.get());process(data.get());} catch (const std::exception& e) {// 无需手动释放内存!unique_ptr 会在退出作用域时自动释放throw;} }
优势:无论是否发生异常,
unique_ptr
都会在离开作用域时自动释放内存。
3. 智能指针类型
类型
用途
异常安全性
std::unique_ptr
独占所有权,不可复制
离开作用域时自动释放内存
std::shared_ptr
共享所有权,引用计数管理
引用计数归零时自动释放内存
std::weak_ptr
解决
shared_ptr
循环引用问题不增加引用计数
三、综合应用示例
场景:读取文件并解析数据
#include <memory> #include <fstream> #include <vector> #include <stdexcept>// 使用 unique_ptr 管理文件资源 void read_and_process(const std::string& filename) {auto file = std::make_unique<std::ifstream>(filename);if (!file->is_open()) {throw std::runtime_error("无法打开文件");}std::vector<int> data;int value;while (*file >> value) {data.push_back(value);}if (data.empty()) {throw std::runtime_error("文件内容为空");}// 处理数据(假设 process_data 可能抛出异常)process_data(data); // noexcept(false) }int main() {try {read_and_process("data.txt");} catch (const std::exception& e) {std::cerr << "错误: " << e.what() << std::endl;return 1;}return 0; }
关键点分析
资源管理:
std::ifstream
由unique_ptr
管理,即使异常发生,文件句柄也会正确关闭。异常传播:所有可能的错误(如文件打开失败、数据为空)通过异常向上传递,由
main
统一处理。代码简洁性:无需在多个
catch
块中重复释放资源。
四、小结一下
1.
noexcept
的核心价值
性能优化:减少异常处理开销。
接口清晰化:明确函数是否可能抛出异常。
2. 智能指针的核心价值
资源安全:杜绝因异常导致的内存/资源泄漏。
代码简洁:避免手动
try/catch
和资源释放逻辑。3. 现代C++最佳实践
优先使用智能指针:替代裸
new
/delete
。合理使用
noexcept
:在明确不抛异常的函数上标记,但避免滥用。减少显式
try/catch
:依赖RAII和异常传播机制,而非深层嵌套的异常捕获。
6. 协程支持(C++20)(了解就好)
-
co_await
/co_return
:异步编程模型(需编译器支持)。generator<int> seq() {for (int i=0; ; ++i) {co_yield i; // 生成值并暂停} }
三、一些建议
-
优先使用范围
for
:简化遍历,避免迭代器错误。 -
限制变量作用域:如用带初始化的
if
/switch
。 -
编译期分支优化:用
constexpr if
替代运行时条件。 -
避免
goto
:用函数或结构化的控制流替代。
一句话理解语句
表达式语句:做一件小事(
x = 5;
)。复合语句:打包一组任务(
{...}
)。选择语句:根据条件选择路线(
if
/switch
)。循环语句:重复执行直到满足条件(
for
/while
)。跳转语句:改变执行顺序(
break
/return
)。
喔吼吼,又学完了一部分,太棒了!!!一起去在下一篇学习函数吧😀