从零到一学习C++(基础篇) 作者:羡鱼肘子
温馨提示1:本篇是记录我的学习经历,会有不少片面的认知,万分期待您的指正。
温馨提示2:本篇会尽量用更加通俗的语言介绍c++的基础,用通俗的语言去解释术语,但不会再大白话了哦,常见,常看,常想,渐渐的就会发现术语也是很简单滴。
温馨提示3:看本篇前可以先了解前篇的内容,知识体系会更加完整哦。
从零到一学习c++(基础篇--筑基期七-vector与迭代器)-CSDN博客
温馨提示4:自本篇开始会结合现代c++的特性进行学习,首先是因为通过前七篇的学习相信大家都有了一定的基础对于现代c++的理解不会过于艰难;其次是因为需要培养一种正确看待技术的思维,在技术领域永远没有一层不变的常胜将军,学会适应是很重要的。
表达式(基础版)
一、表达式的基本概念
1. 表达式是什么?
-
表达式(Expression) 由一个或多个 运算对象(Operand) 和 运算符(Operator) 组成,用于计算一个值。
-
表达式可以包含:
-
字面值(如
42
)、变量(如x
)、函数调用(如sqrt(2)
)。 -
运算符的组合(如
a + b * c
)。
-
2. 表达式的值类别
-
左值(lvalue):能出现在赋值语句左侧的表达式,代表一个持久的内存位置(如变量、解引用指针)。
int x = 5; // x是左值 *p = 10; // *p是左值
-
右值(rvalue):临时对象或字面值,不能出现在赋值语句左侧(如
3 + 4
的结果)。int y = x + 1; // x+1是右值
二、运算符详解
1. 算术运算符
-
+, -, *, /, %(取模)。
-
整数除法会截断小数部分:
5 / 3 // 结果为1(int类型) 5.0 / 2 // 结果为2.5(double类型)
-
取模运算要求操作数为整数:
10 % 3 // 结果为1
2. 递增和递减运算符
-
前置版本(++i, --i):先增减,再使用值。
-
后置版本(i++, i--):先使用原值,再增减。
int i = 0; int a = ++i; // a=1, i=1 int b = i++; // b=1, i=2
3. 逻辑与关系运算符
-
逻辑运算符:
&&
(逻辑与)、||
(逻辑或)、!
(逻辑非)。 -
关系运算符:
==
,!=
,<
,>
,<=
,>=
。 -
短路求值(Short-Circuit Evaluation):(这个很重要哦,我感觉是个坑点)
if (p != nullptr && *p == 42) { // 若p为nullptr,右侧不会执行(避免解引用空指针) }
4. 赋值运算符
-
=
是右结合的,允许多重赋值。int a, b; a = b = 0; // 等价于a=(b=0)
-
复合赋值运算符(如
+=
,-=
):a += 5; // 等价于a = a + 5
5. 条件运算符(三元运算符)
-
cond ? expr1 : expr2
:若cond
为真,返回expr1
,否则返回expr2
。int max = (a > b) ? a : b;
6. 位运算符
-
操作整数的二进制位:
&
(位与)、|
(位或)、^
(异或)、~
(取反)、<<
(左移)、>>
(右移)。unsigned char bits = 0b0011; bits <<= 2; // 结果0b1100
7. 其他运算符
-
逗号运算符:从左到右求值,返回最右侧表达式的值。
int i = (a = 3, a + 2); // i=5
-
sizeof运算符:返回类型或对象的大小(字节数)。
sizeof(int); // 通常是4 sizeof(arr); // 数组总字节数
三、类型转换(第二遍喽,强化理解)
-
1. 隐式类型转换
-
算术转换:运算符的运算对象会被转换为最宽的类型。
int a = 3; double b = 3.14; auto c=a + b; // int转为double,结果为double
-
数组到指针的转换:
int arr[5]; int *p = arr; // arr退化为指向首元素的指针
-
-
2. 显式类型转换
-
C风格转换:
(type)expr
(不推荐,可能不安全)。 -
C++命名转换:
-
static_cast
:通用的类型转换(如浮点转整数)。double d = 3.14; int i = static_cast<int>(d); // i=3
-
const_cast
:移除const
属性。const int *p = &x; int *q = const_cast<int*>(p);
-
reinterpret_cast
:低层重新解释(慎用)。 -
dynamic_cast
:用于多态类型的转换。
-
-
布尔转换:非零值转为
true
,零值转为false
。if (x) { ... } // 等价于if(x != 0)
-
四、求值顺序与陷阱
1. 优先级与结合律
-
优先级:决定运算符的运算顺序(如
*
优先级高于+
)。 -
结合律:优先级相同时的运算方向(如赋值运算符右结合)。
a = b = c; // 等价于a=(b=c)
2. 未定义的求值顺序
-
大多数运算符的运算对象求值顺序未定义。
int i = 0; cout << i << " " << ++i << endl; // 未定义行为
3. 避免未定义行为
-
不要在同一表达式中修改并访问同一变量:
int i = 0; i = i++; // 未定义行为
五、一点小建议
-
用括号明确优先级:
int val = (a + b) * c; // 清晰表达意图
-
避免复杂表达式:拆分多步骤以提高可读性。
-
警惕隐式转换:
int x = 3.14; // x=3,可能丢失精度
-
优先使用C++风格的类型转换(如
static_cast
)。
六、写个例子看看
#include <iostream>
using namespace std;
int main() {
// 算术运算与类型转换
int a = 10, b = 3;
double c = a / b; // c=3(整数除法)
double d = static_cast<double>(a) / b; // d=3.333...
// 条件运算符
int max = (a > b) ? a : b;
// 位运算
unsigned char flags = 0b0011;
flags |= 0b1000; // 设置第4位为1,结果0b1011
// 短路求值
int *p = nullptr;
if (p && *p == 10) { // p为nullptr,右侧不会执行
cout << *p << endl;
}
return 0;
}
哇,你真的超级不错呢!!!是超级有毅力的,给自己一个大大的赞吧👍 下一部分的难度会有点高了,准备好了吗?我们出发了
表达式(现代版)
关于表达式的内容有很多,但是我不准备在这里就面面俱到地学习,仅仅先学习一些比较常见和基础核心内容,其他的以后遇到了再学习,同时我也会复习前面的知识哦。我认为知识的学习就像是滚雪球,需要先有一个基础且牢固的核心然后不断滚动变大。
一、表达式基础回顾(结合现代C++特性)
1. 表达式的值类别(C++11 增强)
现代C++对值类别进行了更精细的划分:
-
左值(lvalue):具名对象,可持久存在(如变量、函数返回的左值引用)。
-
纯右值(prvalue):临时对象或字面量(如
42
、x + y
)。 -
将亡值(xvalue):即将被移动的对象(如
std::move(x)
)。 -
广义左值(glvalue):左值和将亡值的统称。
-
右值(rvalue):纯右值和将亡值的统称。
-
int x = 42; // x是左值 int&& rref = 42; // 42是纯右值 int&& y = std::move(x); // std::move(x)是将亡值
2. 类型推导(C++11 auto
和 decltype
)(第二遍咯,明确联系&&强化理解)
-
auto
:根据初始化表达式自动推导变量类型。(测试一下,现在还会不会呢?)auto a = 42; // a的类型是int auto b = 3.14; // b的类型是double auto c = "hello"; // c的类型是const char*
-
decltype
:推导表达式的类型(保留引用和顶层const)。(注意第二个是有括号的呢)int x = 10; decltype(x) y = x; // y的类型是int decltype((x)) z = x; // z的类型是int&(注意括号的影响)
二、现代C++中的新表达式特性
1. 范围for循环(C++11)
基于范围的 for
循环简化遍历容器:(注意底层哦,是迭代器,迭代器是泛化的指针)
std::vector<int> vec = {1, 2, 3};
for (auto& num : vec) { // auto推导为int&
num *= 2; // 修改容器元素
}
2. 常量表达式(constexpr
,C++11/14/17)
-
constexpr
变量:编译时求值的常量。(还记得constexpr吗?在const那篇里面哦,回想一下constexpr和const的区别?)constexpr int size = 10; // 编译时常量 int arr[size]; // 合法
-
constexpr
函数:若参数是编译时常量,则函数可在编译时求值。constexpr int square(int x) { return x * x; } int arr[square(5)]; // 数组大小为25
3. 统一初始化与初始化列表(C++11)
-
花括号初始化(Uniform Initialization):
int x{5}; // 直接初始化 std::vector<int> vec{1, 2, 3}; // 列表初始化
-
避免窄化转换:
int y = {3.14}; // 错误:double到int的窄化转换
4. 右值引用与移动语义(C++11)
-
右值引用(
&&
):绑定到临时对象,支持移动语义。std::string s1 = "Hello"; std::string s2 = std::move(s1); // s1的资源被移动到s2
-
移动构造函数/赋值运算符:提升资源管理效率。
温馨小贴士:
看着有些懵是不是呢?没关系我也一样,让我们一起仔细看看这究竟是个什么东西
1. 右值引用(&&):绑定到临时对象,支持移动语义。
右值引用 是 C++ 中的一种特殊引用,用
&&
表示。它的作用是“抓住”那些临时的、即将被丢弃的值(比如一个计算结果或一个临时变量)。移动语义 是右值引用的主要用途。它的核心思想是:如果一个东西(比如一块内存或一个文件)是临时的、马上就不用了,那我们可以直接把它的“所有权”转移给另一个变量,而不是重新复制一份。这样可以节省时间和资源。
2.
std::string s1 = "Hello";
这行代码创建了一个字符串变量
s1
,内容是"Hello"
。
s1
是一个普通的变量,它在内存中占了一块地方来存储"Hello"
。3.
std::string s2 = std::move(s1);
std::move
是一个工具,它的作用是告诉编译器:“这个东西(s1
)我不需要了,你可以把它里面的资源(比如内存)直接交给别人(s2
)。”这行代码的意思是:把
s1
的内容("Hello"
)直接“搬”到s2
里,而不是重新复制一份。搬完之后,
s1
就变成了一个“空壳”,里面没有内容了(但程序仍然可以正常销毁它或重新给它赋值)。我们来想个的例子
想象一下:
你有一个装满书的箱子(
s1
),箱子上写着"Hello"
。你朋友(
s2
)想要这个箱子里的书。如果你用“复制”的方式,你需要把箱子里的书一本一本抄一遍,然后把抄好的书给你朋友。这很费时间。
但如果你用“移动”的方式,你可以直接把整个箱子搬给你朋友,你自己手里就空了。这样既快又省力。
std::move
的作用就是告诉程序:“别抄了,直接把箱子搬过去吧!”用代码来表示我们的例子
#include <iostream> #include <string> int main() { std::string s1 = "Hello"; // 你有一个箱子,里面装着 "Hello" std::string s2 = std::move(s1); // 把箱子搬给朋友 s2 std::cout << "s1: " << s1 << std::endl; // 你的箱子现在空了 std::cout << "s2: " << s2 << std::endl; // 朋友的箱子里有 "Hello" return 0; }
输出:
s1: s2: Hello
为什么要用移动语义?
省时间:如果箱子里的东西很多(比如一大段文字或一个大文件),复制会很慢,而直接搬过去就快多了。
省资源:复制需要额外占用内存,而移动不需要。
注意事项
搬完后的箱子:
搬完之后,原来的箱子(
s1
)就空了,但你仍然可以继续用它(比如重新装东西)。不要乱搬:
只有在确定不需要原来的东西时,才用
std::move
。如果乱搬,可能会导致程序出错。移动的实现:
只有那些支持“搬箱子”操作的类(比如
std::string
、std::vector
)才能用std::move
。普通的类(比如int
、double
)没有“搬箱子”的功能。小结一下
右值引用 是用来“抓住”临时值的工具。
移动语义 是把资源(比如内存、文件)从一个地方直接搬到另一个地方,而不是复制。
std::move
是告诉程序:“别复制了,直接搬吧!”
后边的部分先了解有个印象就好
5. Lambda表达式(C++11)
匿名函数作为表达式的一部分:
auto add = [](int a, int b) { return a + b; };
int result = add(3, 4); // result=7
// 捕获局部变量
int x = 10;
auto func = [x](int y) { return x + y; };
6. 折叠表达式(C++17)
简化可变参数模板的操作:
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // 折叠表达式:args1 + args2 + ... + argsN
}
int total = sum(1, 2, 3, 4); // total=10
7. 结构化绑定(C++17)
从元组或结构体中解包多个值:
std::pair<int, std::string> p{42, "answer"};
auto [num, str] = p; // num=42, str="answer"
8. 三路比较运算符(C++20 <=>
)
简化多条件比较:
struct Point {
int x, y;
auto operator<=>(const Point&) const = default;
};
Point a{1, 2}, b{3, 4};
if (a < b) { ... } // 自动生成比较逻辑
三、现代C++中的类型转换
1. static_cast
vs dynamic_cast
-
static_cast
:编译时类型转换(无运行时检查)。double d = 3.14; int i = static_cast<int>(d); // i=3
-
dynamic_cast
:运行时类型检查(用于多态类型)。class Base { virtual void foo() {} }; class Derived : public Base {}; Base* b = new Derived; Derived* d = dynamic_cast<Derived*>(b); // 成功转换
2. std::move
和 std::forward
(C++11)
-
std::move
:将左值转换为右值引用(触发移动语义)。std::string s1 = "Hello"; std::string s2 = std::move(s1); // s1变为空
-
std::forward
:完美转发参数(保留值类别)。template<typename T> void wrapper(T&& arg) { process(std::forward<T>(arg)); // 保持左值/右值属性 }
四、表达式中的现代陷阱与最佳实践
1. auto
的陷阱
-
类型推导可能不符合预期:
auto x = {1, 2, 3}; // x的类型是std::initializer_list<int> auto y = 3.14; // y是double,而非float
2. 移动语义的误用
-
避免对局部变量多次
std::move
:std::string s = "test"; auto s1 = std::move(s); // s的资源被转移 auto s2 = std::move(s); // s已为空,s2也为空
3. Lambda捕获的注意事项
-
按值捕获与按引用捕获:
int x = 10; auto lambda1 = [x]() { return x; }; // 捕获x的副本 auto lambda2 = [&x]() { return x; }; // 捕获x的引用
4. 编译时计算的优势
-
优先使用
constexpr
:constexpr int factorial(int n) { return (n <= 1) ? 1 : n * factorial(n - 1); } int arr[factorial(5)]; // 编译时计算120
五、代码示例(现代C++特性)
#include <iostream>
#include <vector>
#include <utility>
int main() {
// 范围for循环与auto
std::vector<int> nums{1, 2, 3, 4, 5};
for (auto&& num : nums) { // 使用右值引用避免拷贝
std::cout << num << " ";
}
// 结构化绑定
auto [min, max] = std::minmax({3, 1, 4, 2, 5});
std::cout << "\nMin: " << min << ", Max: " << max << std::endl;
// Lambda表达式与折叠表达式
auto sum = [](auto... args) {
return (args + ...); // 折叠表达式求和
};
std::cout << "Sum: " << sum(1, 2, 3, 4) << std::endl;
// constexpr函数
constexpr int cube(int x) { return x * x * x; }
std::cout << "Cube of 3: " << cube(3) << std::endl;
return 0;
}
六、总结
-
传统表达式规则是C++的基础,现代C++通过新特性(如
auto
、移动语义、Lambda等)大幅提升了表达能力和效率。 -
值类别的细化(lvalue、prvalue、xvalue)是理解现代C++资源管理(移动语义)的关键。
-
编译时计算(
constexpr
)和 类型推导(auto
、decltype
)显著增强了代码的灵活性和性能。 -
始终关注 未定义行为 和 类型安全,优先使用现代特性(如范围for循环、结构化绑定)替代传统复杂写法。