引用
引用的本质 —— 变量的别名
C++ 中,引用是一个变量的别名,一旦引用某个变量,操作引用就等于操作原变量。
语法
int a = 10;
int& ref = a; // ref 是 a 的引用,ref 与 a 是同一块内存
ref = 20; // 实际修改的是 a
引用必须初始化,不能为 null
一旦初始化,就不能再引用别的变量
不是拷贝,而是同一个实体
函数参数传递方式
值传递
拷贝一份实参数据传给形参
修改形参不会影响原始变量
void foo(int x) { x = 100; } // 修改的是副本
地址传递
传入变量地址(指针),通过 *ptr
修改原数据
void foo(int* x) { *x = 100; } // 修改的是实参
引用传递
直接传入别名,不拷贝、不取地址,语法简单
void foo(int& x) { x = 100; } // 修改的是实参
引用使用时的注意事项
1.引用必须初始化
int& r; // ❌ 错误:引用必须初始化
原因:引用不是对象,而是某个对象的别名,它必须在创建时绑定一块已有的内存。
2.引用一旦绑定不能更改绑定对象
int a = 10, b = 20;
int& r = a;
r = b; // ❌ 不是让 r 改为引用 b,而是把 b 的值赋给 a
因为 C++ 中引用在底层编译时就是一个隐式的指针常量(
int* const
),一旦初始化,就不能改变所指对象。
int& r = a;
// 本质:int* const r = &a; // const pointer,不能再指向 b
const
const
修饰的变量/对象,其值不可修改,也就是说它是只读的,编译器会强制限制赋值操作。
在底层,const
其实是编译期的保护机制,不影响变量的存储结构,但会限制你通过该名字修改数据的能力。
语法及用法
1️⃣ 基本用法(定义常量):
const int a = 10; // 定义常量 a,不能修改
// a = 20; // ❌ 错误:const 变量不可修改
2️⃣ const int v VS int const v
没有区别,两种写法是等价的,只是顺序不同
const int a = 10; // 表示 a 是一个 const 的 int
int const b = 20; // 同样表示 b 是一个 const 的 int
const 修饰指针的三种情况
① 指向常量的指针(不能改值,可以改地址)
const int* p = &a; // 或写作:int const* p = &a;
*p = 20; // ❌ 错误,不能修改所指内容
p = &b; // ✅ 可以修改指针本身
📌 含义:
*p
是const int
,不能通过p
修改它指向的值。
② 常量指针(可以改值,不能改地址)
int* const p = &a;
*p = 20; // ✅ 可以修改内容
p = &b; // ❌ 错误,不能改变 p 的指向
📌 含义:
p
是 const 的,不能改指向,但*p
是普通 int,可以改值。
③ 指向常量的常量指针(双重 const)
const int* const p = &a;
*p = 20; // ❌ 不行
p = &b; // ❌ 不行
📌 含义:
p
和*p
都是只读的,完全不能动。
const 在函数中的应用
用途:防止函数修改实参;保证返回值只读;用于成员函数中表示不修改对象
void print(const string& str) {
cout << str << endl;
// str = "xx"; // ❌ 不允许
}
const 修饰函数返回值
const int getValue() {
return 42;
}
返回值不能被修改(意义不大,通常用于对象引用或指针)。
成员函数 const 修饰(只读函数)
class Demo {
private:
int data;
public:
int get() const { // 表示此函数不会修改成员变量
return data;
}
};
const vs #define 的区别
特性 | const | #define |
---|---|---|
类型安全 | ✅ 有类型检查 | ❌ 无类型检查(纯文本替换) |
作用域 | ✅ 遵循作用域规则 | ❌ 全局替换 |
可调试性 | ✅ 可调试、有符号名 | ❌ 只是字面值,调试困难 |
编译检查 | ✅ 有 | ❌ 无 |
内存占用 | 可能分配空间(优化器处理) | 不占空间(文本替换) |
C 与 C++ 中 const 的差异
差异点 | C 语言 | C++ |
---|---|---|
语法支持 | 支持 const(C89 后) | 完全支持,语义更丰富 |
作用域 | 支持函数内/外 const | 同上 |
类成员支持 | ❌ 无类/对象 | ✅ 可用于类成员、成员函数等 |
函数参数传递 | 有时需手动加 const 避免修改 | 支持引用传递、const修饰灵活 |
const 应用(CAN通信类封装)
class CanMessage {
private:
const uint8_t id; // 固定ID,不可更改
uint8_t data[8];
public:
CanMessage(uint8_t msg_id) : id(msg_id) {}
void setData(const uint8_t* buf, size_t len) {
for (size_t i = 0; i < len && i < 8; ++i)
data[i] = buf[i]; // 可以改 data,但不能改 id
}
uint8_t getId() const {
return id; // const 成员函数,不修改类
}
};
const T、const T*、T *const、const T&、const T*& 的区别
写法 | 中文含义 |
---|---|
const T | 常量类型 T,值不可改 |
const T* | 指向常量 T 的指针 |
T *const | 常量指针,指针本身不可改 |
const T& | 对常量 T 的引用 |
const T*& | 指向常量 T 的指针的引用 |
T* const& | 常量指针的引用 |
1. const T
普通常量,值不可修改:
const int a = 10;
// a = 20; // ❌ 错误:a 是常量
2. const T* ptr
或写作:T const* ptr。
👉 “指向常量 T 的指针”,你不能通过 ptr
修改所指内容,但可以改指针本身。
int a = 5;
const int* ptr = &a; // 或 int const* ptr = &a;
*ptr = 10; // ❌ 错误
ptr = nullptr; // ✅ 可以改指针
[ptr] ---> [a: 5]
^ 内容不能改
3. T* const ptr
👉 “常量指针”:指针本身不可变,但可以通过它改内容。
int a = 10;
int* const ptr = &a;
*ptr = 20; // ✅
ptr = &b; // ❌ 错误:指针不能改向
[ptr - 固定不动] ---> [a: 20]
^ 但内容可改
4. const T& ref
👉 “常量引用”,即对常量的引用,不能通过引用修改绑定对象。
int a = 42;
const int& ref = a;
ref = 100; // ❌ 错误:不能修改 a
📌 常量引用允许绑定临时值:
const int& r = 10; // ✅ 合法,延长临时变量生命周期
5. const T*& ptrRef
👉 逐步解读:
-
const T*
:指向常量的指针 -
const T*&
:这个指针本身是引用类型
📌 意思是:引用一个“指向常量的指针”,你可以改变它所指向的地址(换对象),但不能通过它修改对象值。
int a = 5, b = 8;
const int* p = &a;
const int*& ref = p; // ✅ ref 是 p 的别名
*ref = 10; // ❌ 错误:ref 是指向 const 的指针,不能改值
ref = &b; // ✅ 改变了 p 的指向
[ref] = alias of [p] ---> [a: 5]
↘
[b: 8] 可重新绑定
6. T* const& ptrRef
👉 分析步骤:
-
T* const
:常量指针(指针本身不能改) -
T* const&
:对“常量指针”的引用
📌 意思是:引用一个“指针固定不动”的指针,可以通过它修改值,但不能改它指向别的对象。
int a = 10, b = 20;
int* const p = &a;
int* const& ref = p; // ref 是 p 的别名
*ref = 30; // ✅ 改 a 的值
ref = &b; // ❌ 错误:p 是 const 指针,不能改指向
[ref = p (固定)] ---> [a: 30]
^ 无法换到 b
总结
写法 | 中文意义 | 可否改值 | 可否改指向 | 可否改指针本身 |
---|---|---|---|---|
const T | 常量 T | ❌ | — | — |
const T* | 指向常量的指针 | ❌ | ✅ | ✅ |
T* const | 常量指针(地址不能变) | ✅ | ❌ | ❌ |
const T& | 对常量 T 的引用 | ❌ | ❌ | — |
const T*& | 指向常量对象的指针的引用 | ❌ | ✅ | ✅(改指向) |
T* const& | 常量指针的引用 | ✅ | ❌ | ❌ |
强制转换
C语言风格的强制类型转换
📌 语法:目标类型)(表达式)
int a = 10;
float f = (float)a; // 将 int 强制转换为 float
种写法 C 和 C++ 都支持,称为 C-style cast,简单直接,但无法明确语义和安全性
C++ 新增四种强制类型转换
类型转换形式 | 作用 | 用法示例 |
---|---|---|
static_cast | 基本类型转换、向上转型等 | static_cast<float>(x) |
const_cast | 添加或移除 const 修饰 | const_cast<char*>(p) |
reinterpret_cast | 重新解释底层字节(非常危险) | reinterpret_cast<int*>(ptr) |
dynamic_cast | 用于类的 RTTI(多态向下转型) | dynamic_cast<Derived*>(basePtr) |
1. static_cast<T>(expr)
-
最常用,类型安全
-
可用于:
-
基本数据类型之间的转换(
int → float
) -
指针的向上转换(
子类 → 父类
)
-
int a = 42;
float f = static_cast<float>(a); // 安全转换
2. const_cast<T>(expr)
-
用于 移除或添加 const 属性
-
通常用于修改函数参数中的
const
指针(⚠️ 非常危险)
void modify(const int* p) {
int* mod = const_cast<int*>(p); // 移除 const,尝试修改
*mod = 100; // ⚠️ 如果原对象是 const 定义的,行为未定义!
}
3. reinterpret_cast<T>(expr)
-
重新解释内存字节为另一种类型,最不安全的转换
-
用于底层编程、设备驱动、内存操作
int a = 0x12345678;
char* p = reinterpret_cast<char*>(&a); // 强制把 int* 变成 char*
4. dynamic_cast<T>(expr)
-
用于多态类型之间的 安全向下转型
-
必须有虚函数
-
转换失败时返回
nullptr
(指针)或抛异常(引用)
class Base { public: virtual void f() {} };
class Derived : public Base { };
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // ✅ 安全向下转换
如果
b
实际不是Derived*
,则d == nullptr
内联函数
内联函数是使用 inline
关键字修饰的函数,它建议编译器在编译阶段将函数的代码展开到调用处,从而减少函数调用的开销(如压栈/跳转/返回)。
语法
inline void say_hello() {
std::cout << "Hello!" << std::endl;
}
也可以写在类中,成员函数默认就被当作内联函数处理(如果函数体在类内定义)
class A {
public:
void show() { std::cout << "show()" << std::endl; } // 隐式 inline
};
内联的原理(底层机制)
函数正常调用(非 inline)
void f() { std::cout << "Hi"; }
void caller() {
f(); // 运行时跳转 → 开销大(函数调用栈、返回地址等)
}
函数内联后
void caller() {
std::cout << "Hi"; // 编译器在这里直接“拷贝粘贴”了 f() 的内容
}
inline 是“建议”不是命令
💡 编译器决定是否内联:
inline
只是告诉编译器 “我建议你内联这个函数”,最终是否展开代码,由编译器自己决定(优化器说了算)。
inline void foo(); // 只是建议,编译器可以忽略
📌 在现代 C++ 中,很多编译器(如 GCC/Clang)会自动根据函数规模、调用频率、编译优化选项(如
-O2
)决定是否内联。
函数重载
这是 C++ 为了解决函数命名冲突、提高接口友好度所引入的机制,它在面向对象、STL、泛型编程中非常常见。
在 C 语言中,函数名必须唯一。如果你有多个功能类似但参数不同的函数,就得起不同的名字:
void print_int(int a);
void print_float(float a);
void print_string(const char* str);
🧠 问题:函数名爆炸、管理困难、不直观!
在 C++ 中可以这样写
void print(int a);
void print(float a);
void print(const std::string& s);
✅ 函数名统一,接口更自然、易用、清晰。
✅ 编译器会根据参数个数、类型自动区分调用哪个版本。
语法
函数返回类型 函数名(参数列表);
👉 函数名相同,但参数个数或类型不同
#include <iostream>
using namespace std;
// 重载版本1:整型参数
void print(int a) {
cout << "int: " << a << endl;
}
// 重载版本2:浮点型参数
void print(float a) {
cout << "float: " << a << endl;
}
// 重载版本3:字符串参数
void print(const string& a) {
cout << "string: " << a << endl;
}
int main() {
print(10); // 调用 int 版本
print(3.14f); // 调用 float 版本
print("hello"s); // 调用 string 版本(注意 "s" 是 C++14 字面量)
}
注意事项
❌ 1. 不能仅凭返回值重载
int func(int x);
float func(int x); // ❌ 错误!与上面的函数只有返回类型不同
编译器在函数调用处只看参数列表,不看返回值 —— 因为返回值可以被忽略。
🔍 所以不能这么做,否则会出现:func(10); // 编译器不知道你是想要 int 还是 float 版本
✅ 2. 参数个数不同、类型不同可以重载
void sum(int a, int b);
void sum(int a, int b, int c);
✅ 3. const
修饰也能构成重载(特别是引用参数)
void display(string& s);
void display(const string& s); // ✅ 合法重载
区分 T
与 T&
与 const T&
void set(int); // 按值
void set(int&); // 非 const 引用
void set(const int&); // 常量引用
这些是三个不同的重载,使用时编译器根据实参来挑选最匹配的版本。
函数模板
基本语法
template<typename T>
T function_name(T a, T b) {
// 泛型代码
}
T
是模板类型参数,可以代表任意类型(如int
、float
、std::string
等)
也可以使用 class
替代 typename
,意义一样
template<class T>
T function_name(T a, T b);
示例:通用的最大值函数
#include <iostream>
using namespace std;
template<typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
cout << max(3, 5) << endl; // int
cout << max(3.14, 2.71) << endl; // double
cout << max('a', 'z') << endl; // char
}
📌 编译器在调用
max(a, b)
时会根据参数类型 自动推导 T 的类型并实例化函数版本。
模板的工作原理
函数模板不是普通函数,而是生成函数的“蓝图”。只有在调用模板时,编译器才会:
-
推导参数类型
-
生成具体类型的函数(模板实例化)
max(3, 5); // → 编译器生成 int max(int, int)
max(3.14, 2.71); // → 生成 double max(double, double)
限制与注意事项
❌ 1. 类型必须支持模板中使用的操作
template<typename T>
T add(T a, T b) {
return a + b; // 要求 T 支持 + 运算符
}
若某个类型不支持 + 运算,调用时会编译失败。
✅ 2.支持函数重载与模板共存
template<typename T>
T max(T a, T b) {
return a > b ? a : b;
}
int max(int a, int b, int c) { // 普通函数重载
return max(max(a, b), c);
}
📌 编译器优先匹配普通函数 → 再尝试模板函数。
🧠 3. 可以手动指定模板参数
max<int>(3, 7); // 显式指定 T = int
⚠️ 4. 不同类型参数时,不能自动推导
max(3, 3.14); // ❌ 编译错误:T 无法推导
✅ 解决方法
template<typename T1, typename T2>
auto max2(T1 a, T2 b) -> decltype(a > b ? a : b) {
return (a > b) ? a : b;
}
或使用 C++17 的模板自动推导特性。
函数默认参数
语法定义
void func(int param1, float param2 = 0.0f);
示例
#include <iostream>
using namespace std;
void greet(string name = "Guest", int times = 1) {
for (int i = 0; i < times; ++i)
cout << "Hello, " << name << "!" << endl;
}
int main() {
greet(); // 输出 Hello, Guest! ×1
greet("Alice"); // 输出 Hello, Alice! ×1
greet("Bob", 3); // 输出 Hello, Bob! ×3
}
默认参数必须在声明处指定
默认参数通常写在函数声明(头文件)中,不建议写在定义中,否则可能重复定义或报错
// 头文件中
void log(string msg, int level = 1);
// 源文件中
void log(string msg, int level) {
cout << "[L" << level << "] " << msg << endl;
}
✅ 推荐这样做:在声明处定义默认值,在定义处使用默认值就会引起冲突。
规则与注意事项
1. 一旦某个参数设置默认值,后面所有参数必须也有默认值
// ❌ 错误写法
void func(int a = 1, int b); // 编译错误:b 没有默认值
✅ 正确写法:
void func(int a, int b = 0); // ✅ 合法
⚠️ 2. 调用时只能“从左往右”省略
void print(int a = 1, int b = 2, int c = 3);
// ✅ 合法
print(); // a=1, b=2, c=3
print(10); // a=10, b=2, c=3
print(10, 20); // a=10, b=20, c=3
// ❌ 错误:不能只跳过中间的 b
print(, , 30); // 编译错误
🧠 必须连续从右往左省略参数
3. 给已有默认值的参数传值时,左边参数都必须传
void foo(int x = 1, int y = 2);
// ✅ 合法
foo(); // x=1, y=2
foo(10); // x=10, y=2
foo(10, 20); // x=10, y=20
// ❌ 不合法:不能只传 y
foo( , 20); // 错误!不能跳过 x