一、概述
本文不管C\C++的相关起源背景,我们只要知道C\C++是接近底层硬件的一种编码语言,不同于Python\Java,是编译形语言,其工作流是:
代码——预处理——编译——汇编——链接——可执行程序
其目标是将高级代码语言一步步转换到机器码(电子电路只能用0、1表示)。
C广泛用于嵌入式、操作系统内核、高性能应用
C++在C的基础上进阶为面对对象(OOP)的编程语言(C是面对过程的),更适合大规模和复杂软件的开发。(代码模块化和复用性强)
学习C++,主要就是学习语法基础和面对对象
简单的一个对照:
特性 | C语言 | C++ |
---|---|---|
范式 | 过程式编程 | 支持面向对象、泛型和过程式编程 |
复杂项目支持 | 较弱,代码管理困难 | 较强,模块化和类封装便于开发复杂系统 |
代码复用性 | 通过函数实现,复用性有限 | 通过继承、模板和多态提高复用性 |
标准库 | 标准函数库(如stdio, math) | 丰富的 STL(数据结构、算法等) |
底层操作 | 强,直接控制硬件 | 同样强,但通过封装和智能指针减少出错风险 |
学习曲线 | 简单,但指针等机制容易导致错误 | 更复杂,但提供现代特性帮助开发者减少低级错误 |
二、语法基础
2.1 数据类型
int double char bool float short long ……
unsigned'前缀'(eg:unsigned int)表示不支持负值,范围翻倍
- 整形以二进制补码形式存储(理解反码、补码)
- 浮点数存储分为符号位、指数部分、尾数部分(注意浮点数的精度问题比较时一般是控制在一个精度范围)
表示范围 | 有限,范围较小 | 表示范围大,可以存储极大或极小的数 |
精度 | 精确,适合计数和索引 | 存在精度损失,适合科学计算 |
存储结构 | 补码形式,简单高效 | IEEE 754标准,符号位+指数+尾数 |
运算效率 | 运算效率高 | 浮点运算较慢(需要专用浮点运算单元) |
典型用途 | 计数、索引、循环变量 | 科学计算、图形处理、金融计算等 |
- 自建数据类型:结构体struct \class\ enum\ typedef
- 空类型:void
2.2 关键字
auto define int if for while等保留字
2.3 变量
除保留字外用户自定义的变量代指,类似数学中的x*x = 5,这里的x就是一个代数变量。
(变量的命名规则:由数字、字母、下划线构成,不能以数字开头)
2.4 逻辑控制结构
程序就是将人类的思维表示成一种既定的高级代码语言,以最终能翻译成机器码由电路运算为目标。
程序是一种有输入有输出有逻辑的结构。
for循环、do while循环、while循环、if else分支选择、switch case分支选择、break continue goto跳转、函数调用(函数是将一个个逻辑块分割提取以供重复调用)等逻辑结构
2.5 数据结构和算法和设计模式
除开自己定义的数据结构和自己的逻辑代码封装的算法以及自己编写代码的模式,剩下的常用的数据结构和算法以及设计模式其实就是前人的编码积累,我们写代码就是利用这些积累完成一个个实际任务。(当然,你可以创新出更好的东西)
2.5.1 数据结构
和其他资料说的不同,我是认为数组、字符串、队列、循环队列、栈、二叉树、图这些都可以说是数据结构,有的是抽象的、有的是实际的数据类型有或封装好的标准库,一般情况下使用stl标准库就足够了.
数组eg:int a[10] ===》在内存中就是逻辑地址和物理地址都连续的一种结构(如果内存足够的话),调用第一个元素 a[0],最后一个元素a[9]。
队列eg:
#include <queue> //注意是先入先出
queue<int> a===>a.pop(),a.push()等操作
栈先入后出,二叉树、多叉树等等……
2.5.2 算法
我们一般讲算法,无非提到时间复杂度和空间复杂度
时间复杂度:
//时间复杂度就是程序步的逻辑表示的数量级
int k=0;
for(auto i=0;i<10;i++)
k++;
//这个就是一个循环,我们不关心具体循环多少次,这依然是一个O(n)的算法
//一个类似于 i++;的语言就是一个程序步
//我们这里可以直接k+=10;实现同样的功能,就是一个算法优化,优化成O(1)
//双循环就是O(n*2)
……
常见时间复杂度
输入规模 nnn | O(1) | O(log n) | O(n) | O(n log n) | O(n²) | O(n³) | O(2ⁿ) | O(n!) |
---|---|---|---|---|---|---|---|---|
10 | 1 | 3 | 10 | 33 | 100 | 1,000 | 1,024 | 3,628,800 |
20 | 1 | 4 | 20 | 87 | 400 | 8,000 | 1,048,576 | 2.43×10¹⁸ |
50 | 1 | 6 | 50 | 282 | 2,500 | 125,000 | 1.13×10¹⁵ | 超大 |
注意,经常将log2n(以2为底的对数)简写成logn
空间复杂度:
空间复杂度一般又涉及数据空间和调用栈空间,一个函数调用需要保存现场需要消耗栈空间,简单讲一下程序的内存空间构成:
+-------------------+ 高地址
| 内核空间 |
+-------------------+
| 栈区 | <-- 向低地址增长
+-------------------+
| 堆区 | <-- 向高地址增长
+-------------------+
| 未初始化区 (BSS) |
+-------------------+
| 初始化区 (Data) |
+-------------------+
| 代码区 (Text) |
+-------------------+ 低地址
每次函数调用都会在栈区中创建一个新的栈帧,用于存储该函数的运行现场。以下是栈帧中包含的主要内容:
- 函数的返回地址:记录函数返回时的程序执行位置。
- 函数的参数:传递给函数的参数值。
- 局部变量:函数内部定义的变量。
- 保存的寄存器值:保存调用函数前的寄存器状态,便于返回后恢复。
函数调用的过程:
- 进入函数时:分配栈帧,保存返回地址和局部变量等。
- 函数返回时:释放栈帧,恢复调用位置。
递归调用时,每次递归都会占用新的栈帧,递归过深会导致栈溢出
例子(时空转换):
// 时间换空间:递归计算斐波那契数列
int fibonacci(int n) {
if (n <= 1) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2); // 重复计算中间结果
}
// 空间换时间:动态规划计算斐波那契数列
int fibonacci(int n) {
vector<int> dp(n + 1); // 用数组存储中间结果
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
int main() {
int n = 10;
cout << "Fibonacci(" << n << ") = " << fibonacci(n) << endl;
return 0;
}
2.5.3 设计模式
主要的设计模式有20到30种,单例、工厂、简单工厂、适配器、装饰器、观察者等模式。大致可分为结构型、行为型、创建型。
设计模式是软件开发中的最佳实践,通过总结可复用的代码结构和解决方案来解决特定问题。它们具有很多优势:提高代码的可维护性、灵活性和可扩展性
相关具体设计模式自行查证。
三、程序的构成
一个程序的基本构成可以简单概括为变量和关键字两大核心元素,其中变量又可以细分为数据变量和函数变量。这些元素通过一定的逻辑组织(如数据结构、算法或设计模式)形成代码,以解决特定的问题。
程序的构成强调了逻辑的关键性:变量承载数据和操作的动态性,关键字提供语言规则的约束与指导。当两者协同工作,再结合数据结构和算法的设计,就能完成从输入到输出、从问题到解决方案的完整过程。这种系统性的组织使得代码不仅能实现功能,还能具备良好的可维护性和扩展性。
四、详解 C 系语言的恶魔:指针
C 系语言中的指针,因其强大的灵活性和对底层内存操作的控制能力,被视为一把双刃剑——它既是高效代码的基石,又是导致内存错误的罪魁祸首。此外,与指针管理形成对比的**垃圾回收(Garbage Collection, GC)**机制则提供了一种完全不同的内存管理思路。以下将详细解析垃圾回收与指针的定义、原理、优缺点,以及两者在内存管理中的实际应用。
垃圾回收(Garbage Collection, GC)
垃圾回收是一种自动内存管理机制,通过系统自动检测并释放不再使用的内存资源,开发者无需手动干预。其核心目标是简化内存管理,减少因人为疏忽导致的内存泄漏或重复释放问题。
常见的垃圾回收机制
引用计数法:
为每个对象维护一个引用计数,当对象被引用时计数器增加,被释放时计数器减少,计数器为 0 时对象会被回收。这种方法实时性较强,但难以处理循环引用的问题。标记-清除算法(Mark-Sweep):
通过标记存活对象,然后清除未标记对象来回收内存。优点是能处理循环引用,缺点是会导致内存碎片化且停顿时间较长。标记-压缩算法(Mark-Compact):
在标记-清除的基础上,移动存活对象以消除碎片。尽管内存利用率提高,但增加了额外的时间开销。分代收集(Generational GC):
将内存划分为年轻代和老年代,分别处理短生命周期和长生命周期的对象。通过不同的算法优化性能,是现代垃圾回收的常用策略。
垃圾回收的优缺点
优点:
- 自动化内存管理,降低开发负担。
- 提高代码的安全性,减少内存泄漏和悬挂指针问题。
- 开发者可以专注于业务逻辑,而无需处理复杂的内存释放流程。
缺点:
- 性能开销大,垃圾回收的过程可能造成程序的短暂卡顿。
- 缺乏灵活性,触发时机不可控。
- 占用更多内存以优化性能。
指针
指针是存储内存地址的变量,允许程序直接访问或操作内存。是 C/C++ 等语言的重要特性,但也因其复杂性成为许多程序错误的根源。
指针的优缺点
优点:
- 灵活性高:开发者可以精确控制内存分配和释放。
- 性能高:没有垃圾回收的额外开销。
- 底层能力强:能直接操作硬件或内存,是系统级开发的基础。
缺点:
- 容易出现悬挂指针(指向已释放内存)或野指针(未初始化)。
- 手动管理增加代码复杂性,容易发生内存泄漏。
- 不安全性高,可能导致越界访问或系统崩溃。
动态内存管理与指针
在 C/C++ 中,开发者使用 malloc
和 free
或 new
和 delete
对内存进行动态分配和释放。以动态数组为例:
#include <iostream>
using namespace std;
int main() {
int* arr = new int[5]; // 动态分配
for (int i = 0; i < 5; ++i) {
arr[i] = i * 10;
}
for (int i = 0; i < 5; ++i) {
cout << arr[i] << " ";
}
delete[] arr; // 手动释放内存
return 0;
}
垃圾回收与指针的对比
特性 | 垃圾回收(GC) | 指针 |
---|---|---|
内存分配 | 自动分配和释放 | 需手动管理内存 |
灵活性 | 灵活性低,但减少人为错误 | 灵活性高,可精确控制 |
性能 | 有额外开销,可能引起短暂停顿 | 性能高,无额外管理开销 |
安全性 | 自动化管理,减少悬挂指针问题 | 易出现悬挂指针、内存泄漏等问题 |
适用场景 | 高层开发(如 Java、Python) | 系统级开发(如操作系统、嵌入式) |
智能指针的综合应用
C++ 引入智能指针(如 std::shared_ptr
、std::unique_ptr
),在垃圾回收和指针之间找到平衡。智能指针通过引用计数等机制实现自动内存管理,同时保留了指针的高效性。例如:
#include <iostream>
#include <memory>
using namespace std;
int main() {
shared_ptr<int> ptr = make_shared<int>(10); // 智能指针自动管理内存
cout << "Value: " << *ptr << endl; // 输出 10
return 0; // 程序结束时自动释放内存
}
五、C++总结
以上四个板块就是我基于我的认知对C++相关的构成做的简单拆解,仅作为一个分部的回忆记录。也可作为大家认知程序代码构成的简单思路。
程序代码其实就是将我们的思维逻辑翻译成代码语言,翻译成功后实现了功能,在人类的逻辑上我们就会进一步的追求效率(空间效率和时间效率),这时候就靠数据结构和算法来实现,完成了优化后,我们又会考虑程序的迭代长久可读,这又涉及到设计模式,融汇所有的逻辑后,学习C++就是学习一套翻译模式。
C++后续的图形相关、网络相关、底层相关等等深入,其实就是了解一个又一个的代码库,你知道其逻辑,你了解其作用,然后解决相关的问题……