从零到一学习c++(基础篇--筑基期三-const限定符)

 从零到一学习C++(基础篇) 作者:羡鱼肘子

 温馨提示1:本篇是记录我的学习经历,会有不少片面的认知,万分期待您的指正。

 温馨提示2:本篇会尽量避免一些术语,尽量用更加通俗的语言介绍c++的基础,但术语也是很重要的。

 温馨提示3:看本篇前可以先了解前篇的内容,知识体系会更加完整哦。

从零到一学习c++(基础篇--筑基期二)-优快云博客

 温馨提示4:本篇内的代码仅仅只是核心演示,并非完整代码哦,而且也不建议用中文变量名哦

 const限定符

在 C++ 中,const 限定符用于定义“不可修改”的常量或保护数据不被意外修改。它像一把“锁”,可以给变量、函数参数、返回值甚至类的成员函数添加“只读”属性。

1. 基本作用

  • 定义常量:替代宏(#define),定义类型安全的不可变值。

    const int MAX_LIFE = 100;  // MAX_LIFE 不可修改
    // MAX_LIFE = 200;         // ❌ 编译错误
  • 保护数据:避免误操作修改关键数据。

2. 常见用法

2.1 变量声明

  • 声明时必须初始化,后续无法修改。

    const double PI = 3.14159;
    // PI = 3.14;  // ❌ 禁止修改

2.2 指针与 const

  • 指向常量的指针(底层 const):指针指向的值不可修改。

    int num = 10;
    const int* ptr = #  // 通过 ptr 不能修改 num
    // *ptr = 20;          // ❌ 错误
    num = 20;              // ✅ 直接修改原变量是允许的
  • 指针本身是常量(顶层 const):指针的指向不可修改。

    int a = 5, b = 10;
    int* const ptr = &a;  // ptr 只能指向 a
    // ptr = &b;          // ❌ 错误
    *ptr = 15;            // ✅ 可以修改 a 的值

2.3 引用与 const

  • 常量引用:引用绑定后,不能通过该引用修改原值。

    int age = 25;
    const int& ref = age;  // ref 是 age 的只读别名
    // ref = 30;          // ❌ 错误
    age = 30;             // ✅ 直接修改原变量

2.4 函数参数

  • 保护参数不被修改:常用于传递大型对象(如结构体、类)。

    void printData(const std::string& data) {
        // data 是只读引用,无法被修改
        std::cout << data;
        // data.clear();  // ❌ 错误
    }

2.5 类的 const 成员函数(这部分暂时可以不用看)

  • 语义:承诺不修改类的成员变量(除非成员被 mutable 修饰)。

  • 语法:在成员函数参数列表后加 const

    class Player {
    private:
        int health;
        mutable int debugCounter;  // 允许在 const 函数中修改
    public:
        int getHealth() const {    // 不会修改成员变量
            // health = 100;       // ❌ 错误
            debugCounter++;        // ✅ 允许(mutable)
            return health;
        }
    };
    
    const Player p;
    p.getHealth();  // ✅ 只能调用 const 成员函数

 3.constexpr和常量表达式

  什么是常量表达式?

代码里写死的、运行前就能算出来的值

int arr[5];     // 这里的5就是常量表达式,代码写死的
int a = 3 + 4;  // 3+4也是常量表达式,编译时直接算成7

constexpr 是干什么的?

让编译器在编译代码的时候,就把某些东西算好,而不是等到程序运行时再算。就像做饭前先把菜切好(编译时准备),而不是边炒菜边切(运行时准备),这样炒菜更快。

 用 constexpr 的两种情况

(1) 修饰变量

  • 目的:告诉编译器“这个变量的值,必须在编译时就能确定”。

  • 例子

    constexpr int size = 10;          // 正确:直接写死
    int arr[size];                    // 可以用它定义数组大小
    
    // 错误例子:
    int x = 10;
    constexpr int y = x;             // 报错!因为x是运行时才能确定的值

(2) 修饰函数

  • 目的:让函数在编译时就能计算结果(如果参数是常量)。

  • 例子

    // 一个计算阶乘的函数
    constexpr int factorial(int n) {
      return (n <= 1) ? 1 : n * factorial(n - 1);
    }
    
    constexpr int result = factorial(5); // 编译时就算好120,运行时直接用

温馨小贴士: 

  • 不是所有代码都能放 constexpr 函数里:比如打印(cout)、读写文件这种运行时操作不行。

  • C++版本影响功能:C++11 的 constexpr 函数只能写一行代码,C++14 开始支持循环、变量等。

  • 初始化时,constexpr声明的变量必须用常量表达式初始化

constexpr 和 const 的区别

  • const:只表示“不能改”,但值可能是运行时确定的。

    int a = 5;
    const int b = a;     // 合法!但b的值是运行时确定的
    int arr[b];          // 报错!因为b不是编译时的常量
  • constexpr:必须让值在编译时确定。

    constexpr int c = 5; // 正确
    int arr[c];          // 合法!

 一句话理解constexpr

constexpr 就是告诉编译器:“这个变量(或函数)的值,你编译代码的时候直接帮我算好,别等到程序运行时再算!”——这样程序跑得更快,还能提前发现错误。

constexpr 指针 = "固定电话"

  • 特点:电话号码(地址)必须一开始就定死,不能换号。

  • 只能用固定地址初始化:比如全局变量(小区公共电话)、静态变量(你家座机)、字符串字面量(广告牌上的电话)。

  • 不能用临时地址:比如局部变量(街边小摊的电话,随时会变)。

int 小区公共电话 = 100;  // 全局变量,地址固定

void 例子() {
    static int 你家座机 = 200;  // 静态变量,地址固定
    constexpr int* 电话本 = &小区公共电话;  // 合法:地址确定
    constexpr int* 家里电话 = &你家座机;    // 合法
    
    int 路边摊 = 300;
    // constexpr int* 小摊电话 = &路边摊;  // 非法!地址每次运行可能变
}

指针自己不能动,但指向的东西可以改(重点哦)

  • constexpr 指针:像你的身份证号,一辈子不变。

  • 指向的内容:如果原对象允许修改(比如全局变量),可以随便改。

int 钱包余额 = 1000;  // 全局变量,可以修改
constexpr int* 我的钱包 = &钱包余额;  // 指针固定指向钱包

*我的钱包 = 500;  // 合法:改的是钱包里的钱
// 我的钱包 = nullptr;  // 非法!指针自己不能换目标

常见踩坑场景

  • 试图用 new 或 malloc

    // constexpr int* 坑1 = new int(10);  // 大坑!动态内存地址运行时才确定
  • 指向局部变量

    void 函数() {
        int 临时值 = 5;
        // constexpr int* 坑2 = &临时值;  // 大坑!局部变量地址编译时不确定
    }

小结

  • constexpr 指针:初始化时,必须指向“固定地址”(全局、静态、字符串)。

  • 指向的内容:是否可修改,取决于原对象是否用 const

  • 自己不能动:指针的值(地址)编译时定死,不能改。

类型别名(Type Alias)

在 C++ 中有两种方式定义类型别名:typedef(传统)和 using(C++11 引入的现代方式)。

1. 基础用法:给类型起外号

就像给朋友起昵称一样,类型别名让代码更简洁。

传统方式:typedef

typedef int 年龄;       // 现在 "年龄" 就是 int 的别名
typedef double 工资;    // "工资" 是 double 的别名

年龄 小明年龄 = 18;     // 等价于 int 小明年龄 = 18;
工资 月薪 = 10000.5;   // 等价于 double 月薪 = 10000.5;

现代方式:using(C++11+强力推荐哦)

using 年龄 = int;      // 效果和 typedef 相同,但语法更直观
using 工资 = double;

年龄 小红年龄 = 20;
工资 年薪 = 120000.8;

2. 简化复杂类型名称(下边的内容目前用不到,是可以跳过的)

当类型名称很长(比如函数指针、嵌套模板)时,别名能大幅提升可读性。

简化函数指针类型

// 原始写法:定义一个返回 int、参数为 double 的函数指针类型
typedef int (*旧式函数指针)(double); 
// 现代写法(更清晰)
using 新式函数指针 = int(*)(double);
// 使用别名
int 计算(double 输入) { return 输入 * 2; }
新式函数指针 指针 = &计算;  // 等价于 int (*指针)(double) = &计算;

这个会有点难理解我们一起来理解一下 

我们的代码的目标:定义一个函数指针类型

想象你想定义一个“指向某种函数的指针”类型。这种函数需要满足:

  • 返回值类型int

  • 参数类型double

也就是这种函数的通用格式:

int 函数名(double 参数) { ... }

2. 传统写法:typedef

用 typedef 定义别名时,语法有点反直觉:

typedef int (*旧式函数指针)(double); 

  • 分解一下

    • typedef:告诉编译器“我要定义一个类型别名”。

    • int (*旧式函数指针)(double)

      • 旧式函数指针 是类型别名名称。

      • (*旧式函数指针) 表示这是一个指针。

      • (double) 表示指向的函数接受一个 double 参数。

      • int 是函数的返回值类型。

效果
旧式函数指针 现在可以表示“指向 int(double) 函数的指针类型”。

3. 现代写法:using(C++11+)

用 using 更直观,类似变量赋值:

using 新式函数指针 = int(*)(double);

  • 分解一下

    • using 新式函数指针 =:声明一个类型别名。

    • int(*)(double):直接写出函数指针的类型。

      • int 是返回值。

      • (*) 表示指针。

      • (double) 是参数列表。

对比优势
不需要把别名名称嵌入复杂语法中,更易读(类似 int a = 5 的直观赋值)。

4. 如何使用这个别名?

假设有一个函数 计算,其格式正好是 int(double)

int 计算(double 输入) {
    return 输入 * 2;
}

用别名定义指针变量

新式函数指针 指针 = &计算; 

等价于:

int (*指针)(double) = &计算;  // 原始写法

调用函数指针

double 输入 = 3.14;
int 结果 = 指针(输入);  // 调用计算(3.14),返回 6

5. 对比两种写法

写法

代码示例

可读性

typedef

typedef int (*指针类型)(double)

名称嵌在中间,语法较绕

using

using 指针类型 = int(*)(double)

名称在左边,类似赋值操作

为什么要用类型别名?

  • 避免重复写复杂类型
    比如每次声明函数指针变量都要写 int(*变量名)(double),用别名后只需写 新式函数指针 变量名

  • 提高可维护性
    如果未来要修改函数签名(比如参数改成 float),只需修改别名定义,不用改所有使用的地方

简化模板类型

#include <vector>
#include <string>

// 用别名简化一个“字符串向量”
using 字符串列表 = std::vector<std::string>;

字符串列表 名字列表 = {"小明", "小红"};

3. 模板别名(C++11+ 的 using 独有功能)

using 可以定义模板别名,而 typedef 不能直接实现这一点。这是现代 C++ 推荐 using 的重要原因。

为模板添加默认参数

template<typename T>
using 动态数组 = std::vector<T>;  // 定义一个模板别名

动态数组<int> 数字列表 = {1, 2, 3};  // 等价于 std::vector<int>

简化嵌套模板

template<typename Key, typename Value>
using 字典 = std::map<Key, std::shared_ptr<Value>>;

// 使用
字典<std::string, int> 学生成绩 = {
  
  {"小明", std::make_shared<int>(90)}};

4. 作用域和访问权限

类型别名遵循常规作用域规则,可以在全局、类内或命名空间中使用。

类内定义别名

class 学生 {
public:
    using 分数类型 = float;  // 类内别名
    分数类型 数学分数 = 85.5;
};

学生::分数类型 语文分数 = 90.0;  // 通过类名访问

5. typedef vs using 对比

特性typedefusing
可读性复杂类型写法繁琐更直观(类似变量赋值)
模板别名不支持支持
函数指针等复杂类型需要嵌套定义直接定义,语法清晰
现代 C++ 推荐度逐渐淘汰优先使用

 在下一篇我会继续学习其他的类型处理,希望我们可以一起进步

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

愚戏师

多谢道友

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

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

打赏作者

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

抵扣说明:

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

余额充值