从零到一学习C++(基础篇) 作者:羡鱼肘子
温馨提示1:本篇是记录我的学习经历,会有不少片面的认知,万分期待您的指正。
温馨提示2:本篇会尽量用更加通俗的语言介绍c++的基础,用通俗的语言去解释术语,但不会再大白话了哦。常见,常看,常想,渐渐的就会发现术语也是很简单滴。
温馨提示3:看本篇前可以先了解前篇的内容,知识体系会更加完整哦。
温馨提示4:本篇想尝试一下双线穿插的方式来学习,在理解是什么的基础上再学习专业的表述,也许会有更好的学习效果
函数
一、函数基础
函数是什么呢?(函数就是“做事的工具”)
1. 函数的本质
作用:把一段代码打包成一个独立工具,可以重复使用。
打个比方:就像厨房里的电饭煲,你只需要按按钮(调用函数),它就能自动煮饭(执行代码)。
// 定义一个“加法工具” int add(int a, int b) { // a和b是原料(参数) return a + b; // 返回做好的饭菜(结果) } // 使用工具 int result = add(3, 5); // 把3和5放进去,得到8
1. 函数声明与定义
-
声明:指定函数名称、参数列表和返回类型(可省略形参名)。
-
定义:实现函数体,必须与声明一致。
// 声明
int add(int a, int b);
// 定义
int add(int a, int b) {
return a + b;
}
如何去构建函数这个工具呢?( 函数的三大要素)
(1) 参数传递:如何给工具“送原料”
值传递:复制一份原料给工具,工具内修改不影响原数据。
void change(int x) { x = 10; } // 修改的是副本 int a = 5; change(a); // a还是5,没变!
引用传递:直接操作原数据(贴标签)。
void change(int& x) { x = 10; } // 操作原数据 int a = 5; change(a); // a变成10!
指针传递:告诉工具原料的位置(地址)。(目前了解就好了哦)
void change(int* x) { *x = 10; } // 通过地址修改原数据 int a = 5; change(&a); // a变成10!
(2) 返回值:工具“给结果”
直接返回:把结果交给调用者。
int add(int a, int b) { return a + b; }
返回引用:返回原数据的“标签”(需确保数据生命周期)。
int& getMax(int& a, int& b) { return a > b ? a : b; } int x = 3, y = 5; getMax(x, y) = 10; // y被改为10
(3) 函数重载:同一个工具名,不同功能
条件:函数名相同,但参数类型或数量不同。
// 重载1:处理整数加法 int add(int a, int b) { return a + b; } // 重载2:处理浮点数加法 double add(double a, double b) { return a + b; }
2. 参数传递
-
值传递:拷贝实参,函数内修改不影响原值。
-
引用传递:直接操作实参,避免拷贝开销。
void swap(int& x, int& y) { // 引用传递
int temp = x;
x = y;
y = temp;
}
3. 返回类型
-
基础类型:直接返回。
-
返回引用:避免拷贝,但需确保返回对象生命周期。
const std::string& getLonger(const std::string& s1, const std::string& s2) {
return s1.size() > s2.size() ? s1 : s2;
}
4. 函数重载
-
同一作用域内,函数名相同但参数列表不同。
void print(int i) { /*...*/ }
void print(const std::string& s) { /*...*/ }
5. 内联函数
-
通过
inline
建议编译器内联展开,减少调用开销。
inline int max(int a, int b) { return a > b ? a : b; }
什么是内联函数呢?
内联函数(Inline Function) 是C++中一种优化程序性能的特性,旨在减少函数调用的开销。通过将函数体直接插入到每个调用处,避免频繁的函数跳转和栈操作,从而提升运行效率。
一、核心概念
1. 什么是内联函数?
定义:使用
inline
关键字修饰的函数。作用:建议编译器将函数代码直接“展开”到调用位置,省去函数调用的开销。
打个比方:就像将菜谱步骤直接写在烹饪流程图中,省去翻页查找菜谱的时间。
2. 普通函数 vs 内联函数
普通函数 内联函数 每次调用需跳转执行,存在栈开销。 代码直接插入调用处,无跳转开销。 适合复杂或低频调用的函数。 适合简单且高频调用的小函数。
二、如何使用内联函数?
1. 基本语法
inline int add(int a, int b) { return a + b; }
注意:
inline
是给编译器的“建议”,实际是否内联由编译器决定。2. 对比一下
普通函数调用:
int result = add(3, 5); // 跳转到add函数执行
编译后伪代码:
push 3, 5 到栈 -> call add -> 执行add -> 返回结果 -> 清理栈
内联函数调用:
int result = add(3,5);//等效int result = 3 + 5; // 直接替换为函数体
编译后效果:无跳转,直接计算。
三、内联函数的优缺点
优点
减少调用开销:适合频繁调用的小函数(如简单计算、访问类成员)。
避免宏的缺陷:相比宏(
#define
),内联函数有类型检查,更安全。
// 宏的问题:无类型检查,易出错 #define ADD(a, b) ((a) + (b)) ADD("hello", 5); // 编译通过但逻辑错误 // 内联函数:类型安全 inline int add(int a, int b) { return a + b; } add("hello", 5); // 编译报错
缺点
代码膨胀:若函数体较大或调用次数过多,会导致可执行文件变大。
编译器可能忽略:复杂函数(如含循环、递归)或调试模式下,编译器可能拒绝内联。
四、适用场景
短小函数:函数体简单(通常1-5行代码)。
inline int getX() const { return x; } // 类成员访问
高频调用:如循环内的数学运算。
for (int i = 0; i < 1e6; i++) { sum += add(i, i+1); // 内联优化显著 }
替代宏:需要类型安全的场景。
五、使用小建议
定义在头文件中:内联函数需在调用处可见,通常将定义放在头文件。
// math_utils.h inline int add(int a, int b) { return a + b; }
避免滥用:复杂函数内联可能导致性能下降。
编译器自主权:最终是否内联由编译器决定(可通过编译器选项干预)。
六、与宏的对比
特性 宏(#define) 内联函数 类型安全 ❌ 无类型检查,易出错。 ✅ 有类型检查,安全。 调试支持 ❌ 替换后难以跟踪。 ✅ 可调试,保留函数语义。 代码展开 预处理器简单文本替换。 编译器智能生成代码。 适用场景 简单代码替换(如常量、简单表达式)。 需要类型安全和逻辑封装的场景。
七、一个小例子
场景:游戏中的伤害计算
#include <iostream> // 内联函数:计算伤害 inline int calculateDamage(int attack, int defense) { return (attack * 2) - defense; } int main() { int playerAttack = 50; int enemyDefense = 30; // 高频调用内联函数 for (int i = 0; i < 1000; i++) { int damage = calculateDamage(playerAttack, enemyDefense); std::cout << "造成伤害:" << damage << std::endl; } return 0; }
优化效果:函数体被直接嵌入循环,省去千次函数调用开销。
小结一下
内联函数是优化高频调用小函数的利器,但需谨慎使用。
适用场景:短小、高频、需类型安全的代码。
避免场景:复杂函数或可能导致代码膨胀的情况。
二、现代C++函数特性增强(目前了解就行)
一些想说的话:常顾常新,让我们不断发掘学过的知识和新知识间的联系
1. 返回类型后置(C++11)
-
使用
auto
和->
后置返回类型,便于推导复杂类型。
auto divide(double a, double b) -> double {
return a / b;
}
2. constexpr
函数(C++11/14)(第三遍哦,还记得const吗?)
-
在编译期求值的函数,用于常量表达式。
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
int arr[factorial(5)]; // 编译期计算数组大小
3. Lambda表达式(C++11/14/17)(第二次见啦,是不是熟悉了一些呢)
-
匿名函数,支持捕获局部变量。
std::vector<int> vec = {1, 2, 3};
std::for_each(vec.begin(), vec.end(), [](int x) {
std::cout << x << " "; // 输出: 1 2 3
});
// C++14 泛型Lambda
auto adder = [](auto a, auto b) { return a + b; };
4. noexcept
说明符(C++11)
-
声明函数不抛出异常,优化性能。
void safe_function() noexcept { /* 保证不抛异常 */ }
5. 右值引用与移动语义(C++11)
-
优化资源管理,避免不必要的拷贝。
class Data {
public:
Data(Data&& other) noexcept { // 移动构造函数
ptr_ = other.ptr_;//偷资源
other.ptr_ = nullptr;
}
private:
int* ptr_;
};
右值引用与移动语义
( 强化理解,因为之前有学过,这里就只是提一下核心的内容)
右值引用与移动语义的核心思想
1. 什么是右值?
右值:临时值,用完就丢的东西。比如:
字面量:
42
、"hello"
。表达式结果:
a + b
、func()
的返回值。临时对象:函数返回的临时对象。
2. 什么是右值引用?
右值引用:用
&&
声明的引用,专门绑定到右值。int&& r = 42; // r 是右值引用,绑定到字面量 42
3. 什么是移动语义?
移动语义:将资源(如内存、文件句柄)从一个对象“偷”到另一个对象,避免不必要的拷贝。
打个比方:搬家时,直接把家具搬到新家,而不是重新买一套
std::move
的作用1. 什么是
std::move
?
作用:将左值强制转换为右值引用,表示资源可以被“偷”。
就像给搬家工人一个信号:“这些东西可以搬走”。
2. 示例
DynamicArray arr1(10); DynamicArray arr2 = std::move(arr1); // 将 arr1 的资源“偷”给 arr2
3. 温馨小贴士
std::move
不移动资源:它只是告诉编译器“这个对象可以被移动”。移动后对象状态:被移动的对象处于有效但未定义状态(通常置空)。
用一句话去记住:
右值引用是“偷资源”的工具,移动语义是“偷资源”的行为,
std::move
是“偷资源”的信号。
6. 可变参数模板(C++11/17)
-
接受任意数量和类型的参数。
template <typename... Args>
void log(Args&&... args) {
(std::cout << ... << args) << std::endl; // C++17 折叠表达式
}
log("Error:", 404, "at line", __LINE__);
7. if constexpr
(C++17)
-
编译期条件分支,简化模板代码。
template <typename T>
auto process(T value) {
if constexpr (std::is_arithmetic_v<T>) {
return value * 2;
} else {
return value.size();
}
}
8. consteval
函数(C++20)
-
强制函数在编译期执行。
consteval int square(int n) { return n * n; }
constexpr int x = square(5); // 编译期计算
三、看个小小栗子
场景:实现一个安全的资源管理函数
#include <memory>
#include <vector>
// 使用智能指针管理动态数组(C++11)
std::unique_ptr<int[]> createArray(int size) {
auto arr = std::make_unique<int[]>(size);
for (int i = 0; i < size; ++i) {
arr[i] = i * i;
}
return arr; // 移动语义自动转移所有权
}
// 结合Lambda和算法(C++11/14)
void processData(const std::vector<int>& data) {
int sum = 0;
std::for_each(data.begin(), data.end(), [&sum](int x) {
sum += x; // 捕获sum的引用
});
std::cout << "Sum: " << sum << std::endl;
}
// 使用可变参数模板记录日志(C++17)
template <typename... Args>
void log(Args&&... args) {
(std::cout << ... << args) << "\n"; // 折叠表达式
}
int main() {
auto arr = createArray(5); // 返回 unique_ptr
std::vector<int> vec = {1, 3, 5, 7};
processData(vec);
log("Array created with size:", 5); // 输出: Array created with size:5
return 0;
}
四、现代C++函数设计的一些小建议
-
优先使用RAII:用智能指针(
unique_ptr
/shared_ptr
)管理资源,替代裸new
/delete
。 -
利用移动语义:减少拷贝,提升性能(如返回容器或大型对象)。
-
编译期计算:使用
constexpr
和consteval
优化常量表达式。 -
异常安全:用
noexcept
声明不抛异常的函数,结合智能指针避免资源泄漏。 -
泛型编程:模板函数与可变参数模板实现灵活接口。
呼🤣终于学习完函数了,真的好难啊。下一篇,就开始学习类