从零到一学习C++(基础篇) 作者:羡鱼肘子
温馨提示1:本篇是记录我的学习经历,会有不少片面的认知,万分期待您的指正。
温馨提示2:本篇会尽量避免一些术语,尽量用更加通俗的语言介绍c++的基础,但术语也是很重要的。
温馨提示3:看本篇前可以先了解前篇的内容,知识体系会更加完整哦。
温馨提示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),返回 65. 对比两种写法
写法
代码示例
可读性
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
对比
特性 | typedef | using |
---|---|---|
可读性 | 复杂类型写法繁琐 | 更直观(类似变量赋值) |
模板别名 | 不支持 | 支持 |
函数指针等复杂类型 | 需要嵌套定义 | 直接定义,语法清晰 |
现代 C++ 推荐度 | 逐渐淘汰 | 优先使用 |
在下一篇我会继续学习其他的类型处理,希望我们可以一起进步