从零到一学习c++(基础篇--筑基期八-表达式)

  从零到一学习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++;  // 未定义行为

五、一点小建议

  1. 用括号明确优先级

    int val = (a + b) * c;  // 清晰表达意图
  2. 避免复杂表达式:拆分多步骤以提高可读性。

  3. 警惕隐式转换

    int x = 3.14;  // x=3,可能丢失精度
  4. 优先使用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):临时对象或字面量(如 42x + 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

为什么要用移动语义?

  • 省时间:如果箱子里的东西很多(比如一大段文字或一个大文件),复制会很慢,而直接搬过去就快多了。

  • 省资源:复制需要额外占用内存,而移动不需要。

注意事项

  1. 搬完后的箱子

    • 搬完之后,原来的箱子(s1)就空了,但你仍然可以继续用它(比如重新装东西)。

  2. 不要乱搬

    • 只有在确定不需要原来的东西时,才用 std::move。如果乱搬,可能会导致程序出错。

  3. 移动的实现

    • 只有那些支持“搬箱子”操作的类(比如 std::stringstd::vector)才能用 std::move。普通的类(比如 intdouble)没有“搬箱子”的功能。

小结一下

  • 右值引用 是用来“抓住”临时值的工具。

  • 移动语义 是把资源(比如内存、文件)从一个地方直接搬到另一个地方,而不是复制。

  • 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)和 类型推导autodecltype)显著增强了代码的灵活性和性能。

  • 始终关注 未定义行为 和 类型安全,优先使用现代特性(如范围for循环、结构化绑定)替代传统复杂写法。

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愚戏师

多谢道友

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值