嵌入式开发核心知识点详解教程
嵌入式开发核心知识点详解教程
目录
- IO多路复用详解
- C++高级特性
- 进程管理
- 数据结构实现
- GPIO与串口通信
- 网络协议详解
IO多路复用详解
1. select、poll、epoll 深入对比
API层面对比
select:
#include <sys/select.h>int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);// 使用示例
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);struct timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;int ret = select(sockfd + 1, &readfds, NULL, NULL, &tv);
特点:
- 使用固定大小的
fd_set
(通常1024个文件描述符) - 有文件描述符数量上限(FD_SETSIZE)
- 每次调用都需要重新初始化
fd_set
- 跨平台性好(POSIX标准)
poll:
#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);struct pollfd {int fd; // 文件描述符short events; // 请求的事件(POLLIN、POLLOUT等)short revents; // 返回的事件
};// 使用示例
struct pollfd fds[2];
fds[0].fd = sockfd1;
fds[0].events = POLLIN;
fds[1].fd = sockfd2;
fds[1].events = POLLIN;int ret = poll(fds, 2, 5000); // 5秒超时
特点:
- 没有文件描述符数量上限(仅受系统资源限制)
- 仍需要遍历整个数组检查就绪事件
- 使用
pollfd
结构体更直观
epoll(Linux专属):
#include <sys/epoll.h>int epoll_create(int size); // 创建epoll实例
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);// 使用示例
int epfd = epoll_create(1);struct epoll_event ev, events[MAX_EVENTS];
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for(int i = 0; i < nfds; i++) {if(events[i].events & EPOLLIN) {// 处理可读事件}
}
特点:
- 通过内核维护事件表
- 不需要每次传递完整的文件描述符列表
- 只返回就绪的文件描述符
- 高效的事件通知机制
性能对比
特性 | select | poll | epoll |
---|---|---|---|
文件描述符数量 | 有上限(1024) | 无上限 | 无上限 |
性能 | O(n) | O(n) | O(1) |
每次调用开销 | 需要重建fd_set | 需要传递整个数组 | 只返回就绪fd |
内核支持 | 跨平台 | 跨平台 | Linux专属 |
适用场景 | 少量连接 | 中等连接 | 大量连接 |
性能分析:
select/poll的问题:
- 每次调用都需要将文件描述符集合从用户空间复制到内核空间
- 需要遍历整个集合查找就绪的文件描述符
- 当连接数增加时,性能线性下降
epoll的优势:
- 使用事件驱动模型,只有就绪的文件描述符才会被返回
- 通过内核维护的红黑树管理文件描述符
- 通过回调机制避免遍历
触发方式对比
水平触发(Level Triggered, LT):
// select和poll都是水平触发
// epoll默认也是水平触发// 特点:
// - 只要文件描述符处于就绪状态,就会一直通知
// - 如果没有完全处理完数据,下次调用仍会通知
// - 不会丢失事件,但可能会有重复通知
边缘触发(Edge Triggered, ET):
// 仅epoll支持边缘触发
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 设置边缘触发
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);// 特点:
// - 只在状态变化时通知一次
// - 必须一次性读取所有数据
// - 效率高,但编程复杂
// - 需要配合非阻塞IO使用
示例对比:
// 水平触发(LT)
char buf[1024];
int n = read(fd, buf, 100); // 只读100字节
// 下次epoll_wait仍会通知,因为还有数据未读// 边缘触发(ET)
char buf[1024];
while((n = read(fd, buf, sizeof(buf))) > 0) {// 必须循环读取直到返回EAGAIN
}
// 下次只有新数据到达才会通知
适用场景总结
select:
- ✅ 少量文件描述符(<100)
- ✅ 跨平台需求
- ✅ 超时精度要求高(微秒级)
- ❌ 不适合高并发服务器
poll:
- ✅ 中等数量文件描述符(100-1000)
- ✅ 需要跨平台
- ✅ 没有文件描述符数量限制
- ❌ 性能仍受连接数影响
epoll:
- ✅ 大量并发连接(>10000)
- ✅ 高性能服务器(Nginx、Redis)
- ✅ 长连接场景
- ❌ 仅限Linux平台
实际应用选择:
连接数 < 1000 → select/poll
连接数 > 1000 → epoll
跨平台需求 → select/poll
追求极致性能 → epoll + ET模式
C++高级特性
2. 面向对象三大特性
封装(Encapsulation)
概念:
将数据和操作数据的方法封装在一起,形成独立的对象。对象内部数据私有化,只能通过公共方法访问。
目的:
- 隐藏实现细节
- 保护数据不被随意修改
- 提高代码安全性
示例:
class BankAccount {
private:double balance; // 私有数据string accountNo;public:// 公共接口BankAccount(string no) : accountNo(no), balance(0) {}void deposit(double amount) {if(amount > 0) {balance += amount;}}bool withdraw(double amount) {if(amount > 0 && amount <= balance) {balance -= amount;return true;}return false;}double getBalance() const {return balance;}
};// 使用
BankAccount account("123456");
account.deposit(1000);
// account.balance = -100; // 错误!不能直接访问私有成员
account.withdraw(500);
cout << account.getBalance(); // 正确的访问方式
继承(Inheritance)
概念:
允许一个类(子类/派生类)从另一个类(父类/基类)继承属性和方法,实现代码复用。
继承方式:
class Animal {
protected:string name;int age;public:Animal(string n, int a) : name(n), age(a) {}void eat() {cout << name << " is eating." << endl;}virtual void speak() { // 虚函数,支持多态cout << "Animal sound" << endl;}
};// public继承:父类的public和protected成员在子类中保持原访问权限
class Dog : public Animal {
private:string breed;public:Dog(string n, int a, string b) : Animal(n, a), breed(b) {}void speak() override { // 重写父类虚函数cout << name << " barks: Woof!" << endl;}void fetch() {cout << name << " is fetching." << endl;}
};class Cat : public Animal {
public:Cat(string n, int a) : Animal(n, a) {}void speak() override {cout << name << " meows: Meow!" << endl;}
};// 使用
Dog dog("Buddy", 3, "Golden Retriever");
dog.eat(); // 继承自Animal
dog.speak(); // Dog的实现
dog.fetch(); // Dog特有的方法Cat cat("Whiskers", 2);
cat.eat(); // 继承自Animal
cat.speak(); // Cat的实现
多态(Polymorphism)
概念:
同一操作作用于不同对象时,可以有不同的行为表现。主要通过虚函数和函数重载实现。
运行时多态(虚函数):
class Shape {
public:virtual double area() const = 0; // 纯虚函数virtual void draw() const = 0;virtual ~Shape() {} // 虚析构函数
};class Circle : public Shape {
private:double radius;public:Circle(double r) : radius(r) {}double area() const override {return 3.14159 * radius * radius;}void draw() const override {cout << "Drawing Circle" << endl;}
};class Rectangle : public Shape {
private:double width, height;public:Rectangle(double w, double h) : width(w), height(h) {}double area() const override {return width * height;}void draw() const override {cout << "Drawing Rectangle" << endl;}
};// 多态的应用
void printShapeInfo(const Shape& shape) {shape.draw();cout << "Area: " << shape.area() << endl;
}int main() {Circle circle(5.0);Rectangle rect(4.0, 6.0);// 同一个函数,不同的行为printShapeInfo(circle); // 调用Circle的实现printShapeInfo(rect); // 调用Rectangle的实现// 指针的多态Shape* shapes[2];shapes[0] = new Circle(3.0);shapes[1] = new Rectangle(2.0, 4.0);for(int i = 0; i < 2; i++) {shapes[i]->draw();cout << "Area: " << shapes[i]->area() << endl;delete shapes[i];}return 0;
}
编译时多态(函数重载):
class Calculator {
public:// 函数重载int add(int a, int b) {return a + b;}double add(double a, double b) {return a + b;}int add(int a, int b, int c) {return a + b + c;}
};Calculator calc;
cout << calc.add(1, 2); // 调用int版本
cout << calc.add(1.5, 2.3); // 调用double版本
cout << calc.add(1, 2, 3); // 调用三参数版本
三大特性总结:
特性 | 关键字 | 作用 | 实现方式 |
---|---|---|---|
封装 | private/protected | 隐藏细节,保护数据 | 访问控制 |
继承 | : public/protected/private | 代码复用,建立关系 | 派生类 |
多态 | virtual | 接口统一,行为多样 | 虚函数/重载 |
3. fork vs vfork
基本区别
fork:
- 子进程完全拷贝父进程的数据段和堆栈
- 使用写时复制(Copy-On-Write, COW)优化
- 子进程和父进程执行顺序不确定
vfork:
- 子进程和父进程共享数据段
- 保证子进程先执行
- 子进程必须立即调用
exec()
或_exit()
详细对比
区别1:内存管理
#include <unistd.h>
#include <stdio.h>int global_var = 10;// fork示例
void test_fork() {int local_var = 20;pid_t pid = fork();if(pid == 0) {// 子进程global_var = 100;local_var = 200;printf("Child: global=%d, local=%d\n", global_var, local_var);} else {// 父进程sleep(1); // 等待子进程执行printf("Parent: global=%d, local=%d\n", global_var, local_var);// 输出:global=10, local=20(父子进程数据独立)}
}// vfork示例
void test_vfork() {int local_var = 20;pid_t pid = vfork();if(pid == 0) {// 子进程global_var = 100;local_var = 200;printf("Child: global=%d, local=%d\n", global_var, local_var);_exit(0); // 必须使用_exit()} else {// 父进程printf("Parent: global=%d, local=%d\n", global_var, local_var);// 输出:global=100, local=200(共享数据)}
}
区别2:执行顺序
// fork - 执行顺序不确定
pid_t pid = fork();
if(pid == 0) {printf("Child\n");
} else {printf("Parent\n");
}
// 可能输出:Child Parent 或 Parent Child// vfork - 保证子进程先执行
pid_t pid = vfork();
if(pid == 0) {printf("Child\n");_exit(0);
} else {printf("Parent\n");
}
// 一定输出:Child Parent
区别3:写时复制(COW)
// fork使用COW技术
pid_t pid = fork();
if(pid == 0) {// 子进程首次写入时才真正复制数据int x = 100; // 不触发复制(局部变量)global_var = 50; // 触发复制,复制全局数据段
} else {// 父进程数据不受影响
}
区别4:危险的vfork使用
// 错误示例1:vfork后使用return
pid_t pid = vfork();
if(pid == 0) {printf("Child\n");return 0; // 错误!会破坏父进程的栈
}// 错误示例2:子进程依赖父进程的后续操作
pid_t pid = vfork();
if(pid == 0) {// 子进程先执行,但需要父进程的数据// 此时父进程还未运行,可能导致死锁
}
// 父进程的操作// 正确示例:立即exec
pid_t pid = vfork();
if(pid == 0) {execl("/bin/ls", "ls", "-l", NULL);_exit(1); // exec失败时退出
}
使用建议
使用fork的场景:
// 1. 需要独立地址空间
pid_t pid = fork();
if(pid == 0) {// 子进程可以安全地修改数据modify_data();continue_execution();
}// 2. 父子进程都需要继续执行
pid_t pid = fork();
if(pid == 0) {handle_request(); // 子进程处理请求
} else {accept_connection(); // 父进程接受新连接
}
使用vfork的场景:
// 立即exec,不需要复制内存
pid_t pid = vfork();
if(pid == 0) {execl("/usr/bin/program", "program", arg1, arg2, NULL);_exit(1);
}
对比表:
特性 | fork | vfork |
---|---|---|
内存复制 | 使用COW延迟复制 | 不复制,共享内存 |
执行顺序 | 不确定 | 子进程先执行 |
性能 | 较慢(需要复制) | 较快 |
安全性 | 高(数据独立) | 低(共享数据) |
使用限制 | 无 | 必须立即exec或_exit |
推荐使用 | ✅ 一般场景 | ⚠️ 仅用于立即exec |
现代建议:
- vfork已过时,大多数情况应使用fork
- fork配合COW技术性能已经很好
- 如需创建新进程执行程序,使用
posix_spawn()
更安全
4. 深拷贝 vs 浅拷贝
概念对比
浅拷贝(Shallow Copy):
- 只复制指针的值
- 多个对象共享同一块动态分配的内存
- 可能导致内存泄漏或双重释放
深拷贝(Deep Copy):
- 复制指针指向的内存内容
- 为每个对象分配独立的内存空间
- 对象间互不影响
代码示例
#include <iostream>
#include <cstring>class String {
private:char* data;public:// 构造函数String(const char* str = "") {if(str) {data = new char[strlen(str) + 1];strcpy(data, str);} else {data = new char[1];data[0] = '\0';}std::cout << "Constructor: " << data << std::endl;}// 浅拷贝构造函数(默认)String(const String& other) {data = other.data; // 只复制指针std::cout << "Shallow Copy: " << data << std::endl;}// 深拷贝构造函数(正确)String(const String& other) {data = new char[strlen(other.data) + 1]; // 分配新内存strcpy(data, other.data); // 复制内容std::cout << "Deep Copy: " << data << std::endl;}// 析构函数~String() {std::cout << "Destructor: " << data << std::endl;delete[] data;}void print() const {std::cout << "Data: " << data << std::endl;}void modify(const char* newStr) {delete[] data;data = new char[strlen(newStr) + 1];strcpy(data, newStr);}
};
浅拷贝的问题
void test_shallow_copy() {String s1("Hello");String s2 = s1; // 浅拷贝// 问题1:共享同一块内存// s1.data和s2.data指向同一地址s1.modify("World");s2.print(); // 输出:World(s2也被修改了!)// 问题2:双重释放// 当s1和s2析构时,都会delete同一块内存// 导致程序崩溃!
}// 内存示意图
// 浅拷贝:
// s1.data ----→ ["Hello\0"]
// s2.data ----↗// 深拷贝:
// s1.data → ["Hello\0"]
// s2.data → ["Hello\0"] (独立的内存)
深拷贝的实现
class String {
private:char* data;public:// 深拷贝构造函数String(const String& other) {if(other.data) {// 1. 分配新内存data = new char[strlen(other.data) + 1];// 2. 复制内容strcpy(data, other.data);} else {data = nullptr;}}// 深拷贝赋值运算符String& operator=(const String& other) {if(this != &other) { // 防止自赋值// 1. 释放原有内存delete[] data;// 2. 分配新内存并复制if(other.data) {data = new char[strlen(other.data) + 1];strcpy(data, other.data);} else {data = nullptr;}}return *this;}// 移动构造函数(C++11)String(String&& other) noexcept {data = other.data;other.data = nullptr; // 转移所有权}// 移动赋值运算符String& operator=(String&& other) noexcept {if(this != &other) {delete[] data;data = other.data;other.data = nullptr;}return *this;}~String() {delete[] data;}
};// 使用示例
void test_deep_copy() {String s1("Hello");String s2 = s1; // 深拷贝s1.modify("World");s1.print(); // 输出:Worlds2.print(); // 输出:Hello(不受影响)// 析构时各自释放自己的内存,安全!
}
三/五/零法则
三法则(Rule of Three):
如果类需要自定义以下之一,通常需要自定义全部三个:
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
五法则(Rule of Five, C++11):
在三法则基础上增加:
4. 移动构造函数
5. 移动赋值运算符
零法则(Rule of Zero):
尽量使用智能指针等RAII类,避免手动管理内存
// 零法则示例
class String {
private:std::unique_ptr<char[]> data; // 使用智能指针size_t length;public:String(const char* str) {length = strlen(str);data = std::make_unique<char[]>(length + 1);strcpy(data.get(), str);}// 不需要自定义析构函数、拷贝/移动函数// 编译器生成的默认版本已经足够
};
实际应用场景
何时使用浅拷贝:
// 简单类型,无动态内存
class Point {int x, y;// 默认浅拷贝足够
};// 共享资源(配合引用计数)
class SharedData {std::shared_ptr<int> data;// 使用智能指针自动管理
};
何时必须深拷贝:
// 管理动态资源
class Image {unsigned char* pixels;int width, height;// 必须深拷贝pixels
};// 包含文件句柄等系统资源
class File {int fd;// 需要深拷贝或禁止拷贝
};
5. 函数重载(Function Overloading)
基本概念
定义:
在同一作用域内,可以定义多个名称相同但参数列表不同的函数。
要求:
- 函数名必须相同
- 参数列表必须不同(类型、个数或顺序)
- 返回值可以相同也可以不同
- 不能仅通过返回值区分重载
函数重载示例
#include <iostream>
using namespace std;class Math {
public:// 1. 参数个数不同int add(int a, int b) {cout << "add(int, int)" << endl;return a + b;}int add(int a, int b, int c) {cout << "add(int, int, int)" << endl;return a + b + c;}// 2. 参数类型不同double add(double a, double b) {cout << "add(double, double)" << endl;return a + b;}// 3. 参数顺序不同void print(int x, double y) {cout << "print(int, double): " << x << ", " << y << endl;}void print(double x, int y) {cout << "print(double, int): " << x << ", " << y << endl;}// 4. const修饰符不同(成员函数)void display() {cout << "display() - non-const" << endl;}void display() const {cout << "display() - const" << endl;}
};// 使用示例
int main() {Math math;cout << math.add(1, 2) << endl; // 调用add(int, int)cout << math.add(1, 2, 3) << endl; // 调用add(int, int, int)cout << math.add(1.5, 2.5) << endl; // 调用add(double, double)math.print(1, 2.5); // 调用print(int, double)math.print(1.5, 2); // 调用print(double, int)math.display(); // 调用非const版本const Math cmath;cmath.display(); // 调用const版本return 0;
}
函数重载的本质
名称修饰(Name Mangling):
编译器在编译时根据参数类型修饰函数名,使其在底层拥有不同的名称。
// C++源代码
void func(int x);
void func(double x);
void func(int x, int y);// 编译后的符号(示例)
_Z4funci // func(int)
_Z4funcd // func(double)
_Z4funcii // func(int, int)// 查看符号
// Linux: nm program | c++filt
// Windows: dumpbin /SYMBOLS program.obj
重载解析规则
编译器按以下顺序匹配函数:
1.#### 重载解析规则(续)
编译器按以下顺序匹配函数:
1. 精确匹配
void func(int x);
void func(double x);func(10); // 精确匹配 func(int)
func(10.5); // 精确匹配 func(double)
2. 类型提升
void func(int x);
void func(double x);func('a'); // char提升为int,匹配func(int)
func(true); // bool提升为int,匹配func(int)
3. 标准类型转换
void func(int x);
void func(double x);func(10L); // long转换为int,匹配func(int)
func(10.5f); // float转换为double,匹配func(double)
4. 用户定义的转换
class MyInt {
public:operator int() const { return value; }
private:int value;
};void func(int x);
MyInt mi;
func(mi); // 通过operator int()转换
5. 可变参数匹配
void func(int x);
void func(...); // 可变参数func(10); // 匹配func(int)
func("hello"); // 匹配func(...)
常见错误与陷阱
错误1:仅返回值不同
int func(int x);
double func(int x); // 错误!不能仅通过返回值区分// 编译器无法确定调用哪个
func(10); // 编译错误
错误2:默认参数引起歧义
void func(int x);
void func(int x, int y = 0);func(10); // 错误!两个函数都匹配,产生歧义
错误3:类型转换引起歧义
void func(int x);
void func(double x);func(10L); // 可能有歧义:long可以转换为int或double
正确示例:避免歧义
void func(int x);
void func(long x); // 明确区分
void func(double x);func(10); // 匹配func(int)
func(10L); // 匹配func(long)
func(10.5); // 匹配func(double)
模板与重载
// 1. 函数模板
template<typename T>
T max(T a, T b) {cout << "Template version" << endl;return (a > b) ? a : b;
}// 2. 普通重载函数(更特化)
int max(int a, int b) {cout << "Non-template version" << endl;return (a > b) ? a : b;
}int main() {max(1, 2); // 调用非模板版本(更匹配)max(1.5, 2.5); // 调用模板版本max<int>(1, 2); // 显式指定模板,调用模板版本return 0;
}
运算符重载
运算符重载也是函数重载的特殊形式:
class Complex {
private:double real, imag;public:Complex(double r = 0, double i = 0) : real(r), imag(i) {}// 重载 + 运算符Complex operator+(const Complex& other) const {return Complex(real + other.real, imag + other.imag);}// 重载 << 运算符(友元函数)friend ostream& operator<<(ostream& os, const Complex& c) {os << c.real << " + " << c.imag << "i";return os;}// 重载 == 运算符bool operator==(const Complex& other) const {return real == other.real && imag == other.imag;}
};// 使用
Complex c1(1, 2);
Complex c2(3, 4);
Complex c3 = c1 + c2; // 调用operator+
cout << c3 << endl; // 调用operator
if(c1 == c2) { } // 调用operator==
6. 智能指针(Smart Pointers)
概念
定义:
智能指针是C++中用于自动管理动态分配内存的类模板,封装了原始指针,通过RAII机制自动释放内存。
作用:
- 自动管理内存,防止内存泄漏
- 避免悬空指针
- 异常安全
三种智能指针
1. unique_ptr(独占所有权)
#include <memory>
#include <iostream>class Resource {
public:Resource() { std::cout << "Resource acquired" << std::endl; }~Resource() { std::cout << "Resource released" << std::endl; }void use() { std::cout << "Using resource" << std::endl; }
};void test_unique_ptr() {// 创建unique_ptrstd::unique_ptr<Resource> ptr1(new Resource());// 或使用make_unique(C++14,推荐)auto ptr2 = std::make_unique<Resource>();ptr1->use();// 不能拷贝,只能移动// std::unique_ptr<Resource> ptr3 = ptr1; // 错误!std::unique_ptr<Resource> ptr3 = std::move(ptr1); // 正确// ptr1现在为空if(!ptr1) {std::cout << "ptr1 is null" << std::endl;}// ptr3拥有资源ptr3->use();// 离开作用域,自动释放资源
}// 使用场景:函数返回动态对象
std::unique_ptr<Resource> createResource() {return std::make_unique<Resource>();
}// 数组版本
void test_unique_ptr_array() {std::unique_ptr<int[]> arr(new int[10]);arr[0] = 42;// 自动释放数组
}
2. shared_ptr(共享所有权)
#include <memory>void test_shared_ptr() {// 创建shared_ptrstd::shared_ptr<Resource> ptr1 = std::make_shared<Resource>();std::cout << "Reference count: " << ptr1.use_count() << std::endl; // 1{// 共享所有权std::shared_ptr<Resource> ptr2 = ptr1;std::cout << "Reference count: " << ptr1.use_count() << std::endl; // 2ptr1->use();ptr2->use();}// ptr2离开作用域,引用计数减1std::cout << "Reference count: " << ptr1.use_count() << std::endl; // 1// ptr1离开作用域,引用计数归零,资源被释放
}// 使用场景:多个对象共享同一资源
class Node {
public:int data;std::shared_ptr<Node> next;Node(int d) : data(d) {}~Node() { std::cout << "Node " << data << " destroyed" << std::endl; }
};void test_linked_list() {auto head = std::make_shared<Node>(1);head->next = std::make_shared<Node>(2);head->next->next = std::make_shared<Node>(3);// 自动管理整个链表的内存
}
3. weak_ptr(弱引用)
#include <memory>// 解决shared_ptr的循环引用问题
class Parent;
class Child;class Parent {
public:std::shared_ptr<Child> child;~Parent() { std::cout << "Parent destroyed" << std::endl; }
};class Child {
public:std::weak_ptr<Parent> parent; // 使用weak_ptr打破循环~Child() { std::cout << "Child destroyed" << std::endl; }
};void test_weak_ptr() {{auto parent = std::make_shared<Parent>();auto child = std::make_shared<Child>();parent->child = child;child->parent = parent; // weak_ptr不增加引用计数// 检查weak_ptr是否有效if(auto p = child->parent.lock()) {std::cout << "Parent is alive" << std::endl;}}// 正确释放:Parent和Child都被销毁
}// weak_ptr的典型用法
void observer_pattern() {std::shared_ptr<Resource> resource = std::make_shared<Resource>();std::weak_ptr<Resource> observer = resource;// 临时访问资源if(auto res = observer.lock()) {res->use();} else {std::cout << "Resource no longer exists" << std::endl;}resource.reset(); // 释放资源if(auto res = observer.lock()) {res->use();} else {std::cout << "Resource no longer exists" << std::endl; // 输出这个}
}
智能指针对比
类型 | 所有权 | 拷贝 | 引用计数 | 典型用途 |
---|---|---|---|---|
unique_ptr | 独占 | ❌ 只能移动 | 无 | 独占资源、工厂函数 |
shared_ptr | 共享 | ✅ 可拷贝 | 有 | 共享资源、容器中存储 |
weak_ptr | 不拥有 | ✅ 可拷贝 | 不计数 | 观察者、打破循环引用 |
实际应用示例
示例1:工厂模式
class Widget {
public:virtual void draw() = 0;virtual ~Widget() {}
};class Button : public Widget {
public:void draw() override {std::cout << "Drawing button" << std::endl;}
};// 工厂函数返回unique_ptr
std::unique_ptr<Widget> createWidget(const std::string& type) {if(type == "button") {return std::make_unique<Button>();}return nullptr;
}void use_factory() {auto widget = createWidget("button");if(widget) {widget->draw();}// 自动释放
}
示例2:缓存系统
#include <unordered_map>class Cache {
private:std::unordered_map<int, std::weak_ptr<Resource>> cache;public:std::shared_ptr<Resource> get(int id) {auto it = cache.find(id);if(it != cache.end()) {// 尝试提升weak_ptrif(auto res = it->second.lock()) {std::cout << "Cache hit" << std::endl;return res;}}// 缓存未命中,创建新资源std::cout << "Cache miss" << std::endl;auto res = std::make_shared<Resource>();cache[id] = res;return res;}
};
示例3:自定义删除器
// 管理C风格资源
void test_custom_deleter() {// 文件指针auto file = std::unique_ptr<FILE, decltype(&fclose)>(fopen("test.txt", "w"),fclose);if(file) {fprintf(file.get(), "Hello, World!");}// 自动调用fclose// Lambda删除器auto ptr = std::unique_ptr<int, std::function<void(int*)>>(new int(42),[](int* p) {std::cout << "Deleting " << *p << std::endl;delete p;});
}
进程管理
7. for循环中的fork()
问题分析
示例代码:
#include <unistd.h>
#include <stdio.h>int main() {for(int i = 0; i < 3; i++) {fork();printf("i=%d, pid=%d\n", i, getpid());}return 0;
}
问题:循环3次后,一共创建多少个进程?
答案:8个进程(包括父进程)
详细分析
第1次循环(i=0):
开始:1个进程↓ fork()
结束:2个进程(P0, P1)
第2次循环(i=1):
开始:2个进程(P0, P1)
P0 → fork() → P2
P1 → fork() → P3
结束:4个进程(P0, P1, P2, P3)
第3次循环(i=2):
开始:4个进程
P0 → fork() → P4
P1 → fork() → P5
P2 → fork() → P6
P3 → fork() → P7
结束:8个进程
计算公式
进程数 = 2^n
其中 n 为循环次数
推导过程:
- 第0次循环前:1个进程
- 第1次循环后:1 × 2 = 2个进程
- 第2次循环后:2 × 2 = 4个进程
- 第3次循环后:4 × 2 = 8个进程
- …
- 第n次循环后:2^n个进程
进程树可视化
循环0次:
P0循环1次:P0/P1循环2次:P0/ \P1 P2/P3循环3次:P0/ \P1 P2/ \ / \P3 P4 P5 P6/P7
实战示例
示例1:控制进程数量
int main() {int n = 3;for(int i = 0; i < n; i++) {pid_t pid = fork();if(pid == 0) {// 子进程:执行任务后退出printf("Child %d doing work\n", i);sleep(1);exit(0); // 子进程退出,不继续循环}}// 只有父进程到达这里// 等待所有子进程for(int i = 0; i < n; i++) {wait(NULL);}printf("All children done\n");return 0;
}
// 结果:创建3个子进程(而不是2^3=8个)
示例2:进程编号分析
void print_process_info() {for(int i = 0; i < 2; i++) {pid_t pid = fork();printf("Loop %d: PID=%d, Parent=%d, fork returned=%d\n",i, getpid(), getppid(), pid);}
}// 输出分析:
// Loop 0: PID=1000, Parent=999, fork returned=1001 (父进程)
// Loop 0: PID=1001, Parent=1000, fork returned=0 (子进程P1)
// Loop 1: PID=1000, Parent=999, fork returned=1002 (父进程)
// Loop 1: PID=1001, Parent=1000, fork returned=1003 (子进程P1)
// Loop 1: PID=1002, Parent=1000, fork returned=0 (子进程P2)
// Loop 1: PID=1003, Parent=1001, fork returned=0 (子进程P3)
常见面试题
题目1:
int main() {fork();fork();printf("Hello\n");return 0;
}
问:输出几次"Hello"?
答:4次(2^2 = 4个进程)
题目2:
int main() {for(int i = 0; i < 2; i++) {fork();}printf("X");return 0;
}
问:输出几个"X"?
答:4个(2^2 = 4个进程)
题目3:
int main() {fork() && fork();printf("Y");return 0;
}
问:输出几个"Y"?
答:3个
分析:
- 第一个fork():父进程(返回>0,继续)→ 子进程(返回0,短路,不执行第二个fork)
- 第二个fork():父进程执行 → 创建1个子进程
- 总共3个进程
数据结构实现
8. 用C语言实现栈
数组实现
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>#define MAX_SIZE 100typedef struct {int data[MAX_SIZE];int top; // 栈顶指针
} Stack;// 初始化栈
void initStack(Stack* s) {s->top = -1;
}// 判断栈是否为空
bool isEmpty(Stack* s) {return s->top == -1;
}// 判断栈是否已满
bool isFull(Stack* s) {return s->top == MAX_SIZE - 1;
}// 入栈
bool push(Stack* s, int value) {if(isFull(s)) {printf("Stack overflow!\n");return false;}s->data[++(s->top)] = value;return true;
}// 出栈
bool pop(Stack* s, int* value) {if(isEmpty(s)) {printf("Stack underflow!\n");return false;}*value = s->data[(s->top)--];return true;
}// 获取栈顶元素(不删除)
bool peek(Stack* s, int* value) {if(isEmpty(s)) {printf("Stack is empty!\n");return false;}*value = s->data[s->top];return true;
}// 获取栈的大小
int size(Stack* s) {return s->top + 1;
}// 测试
int main() {Stack s;initStack(&s);push(&s, 10);push(&s, 20);push(&s, 30);int value;peek(&s, &value);printf("Top: %d\n", value); // 30while(!isEmpty(&s)) {pop(&s, &value);printf("Popped: %d\n", value);}return 0;
}
链表实现
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>typedef struct Node {int data;struct Node* next;
} Node;typedef struct {Node* top;int size;
} Stack;// 初始化栈
void initStack(Stack* s) {s->top = NULL;s->size = 0;
}// 判断栈是否为空
bool isEmpty(Stack* s) {return s->top == NULL;
}// 入栈
bool push(Stack* s, int value) {Node* newNode = (Node*)malloc(sizeof(Node));if(!newNode) {printf("Memory allocation failed!\n");return false;}newNode->data = value;newNode->next = s->top;s->top = newNode;s->size++;return true;
}// 出栈
bool pop(Stack* s, int* value) {if(isEmpty(s)) {printf("Stack underflow!\n");return false;}Node* temp = s->top;*value = temp->data;s->top = temp->next;free(temp);s->size--;return true;
}// 获取栈顶元素
bool peek(Stack* s, int* value) {if(isEmpty(s)) {return false;}*value = s->top->data;return true;
}// 销毁栈
void destroyStack(Stack* s) {int value;while(!isEmpty(s)) {pop(s, &value);}
}// 获取栈的大小
int getSize(Stack* s) {return s->size;
}int main() {Stack s;initStack(&s);push(&s, 10);push(&s, 20);push(&s, 30);printf("Stack size: %d\n", getSize(&s));int value;while(!isEmpty(&s)) {pop(&s, &value);printf("Popped: %d\n", value);}destroyStack(&s);return 0;
}
栈的应用
应用1:括号匹配
bool isValid(const char* s) {Stack stack;initStack(&stack);for(int i = 0; s[i] != '\0'; i++) {char ch = s[i];if(ch == '(' || ch == '[' || ch == '{') {push(&stack, ch);} else {if(isEmpty(&stack)) return false;int top;pop(&stack, &top);if((ch == ')' && top != '(') ||(ch == ']' && top != '[') ||(ch == '}' && top != '{')) {return false;}}}return isEmpty(&stack);
}// 测试
printf("%d\n", isValid("()[]{}")); // 1 (true)
printf("%d\n", isValid("([)]")); // 0 (false)
printf("%d\n", isValid("{[()]}")); // 1 (true)
应用2:表达式求值(后缀表达式)
int evaluatePostfix(const char* expr) {Stack stack;initStack(&stack);for(int i = 0; expr[i] != '\0'; i++) {char ch = expr[i];if(ch >= '0' && ch <= '9') {push(&stack, ch - '0');} else if(ch == ' ') {continue;} else {int b, a;pop(&stack, &b);pop(&stack, &a);switch(ch) {case '+': push(&stack, a + b); break;case '-': push(&stack, a - b); break;case '*': push(&stack, a * b); break;case '/': push(&stack, a / b); break;}}}int result;pop(&stack, &result);return result;
}// 测试
printf("%d\n", evaluatePostfix("23+5*")); // (2+3)*5 = 25
GPIO与串口通信
9. GPIO工作模式
GPIO(General Purpose Input/Output):通用输入输出端口
8种工作模式
4种输入模式:
模式 | 说明 | 应用场景 |
---|---|---|
浮空输入 | 输入端悬空,电平不确定 | 外部有明确驱动的信号 |
上拉输入 | 内部上拉电阻,默认高电平 | 按键、开关(按下接地) |
下拉输入 | 内部下拉电阻,默认低电平 | 某些传感器 |
模拟输入 | ADC功能,读取模拟电压 | 温度传感器、电位器 |
4种输出模式:
模式 | 说明 | 应用场景 |
---|---|---|
开漏输出 | 只能输出低电平,需外部上拉 | I²C、单总线 |
开漏复用 | 开漏输出 + 外设控制 | I²C外设 |
推挽输出 | 可输出高/低电平 | LED、继电器 |
推挽复用 | 推挽输出 + 外设控制 | SPI、UART TX |
模式详解
开漏输出(Open-Drain):
VCC|
[R] 上拉电阻|
引脚 ---[NMOS]--- GNDNMOS导通:输出低电平
NMOS关断:引脚被上拉电阻拉高
应用:
- I²C总线(SCL、SDA)
- 实现线与功能
- 电平转换
推挽输出(Push-Pull):
VCC ---[PMOS]--- 引脚 ---[NMOS]--- GND输出高电平:PMOS导通,NMOS关断
输出低电平:PMOS关断,NMOS导通
应用:
- 驱动LED
- 普通数字输出
- SPI、UART等
STM32配置示例
#include "stm32f10x.h"// 配置GPIO为推挽输出(LED)
void GPIO_Config_LED(void) {GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOC, &GPIO_InitStructure);
}// 配置GPIO为上拉输入(按键)
void GPIO_Config_Button(void) {GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入GPIO_Init(GPIOA, &GPIO_InitStructure);
}// 配置GPIO为开漏输出(I²C)
void GPIO_Config_I2C(void) {GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; // SCL, SDAGPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏复用GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);
}
10. UART vs USART
基本概念
UART(Universal Asynchronous Receiver/Transmitter):
- Universal:通用
- Asynchronous:异步
- 仅支持异步通信
USART(Universal Synchronous/Asynchronous Receiver/Transmitter):
- 在UART基础上增加了 Synchronous(同步)
- 既支持异步,也支持同步通信
详细对比
特性 | UART | USART |
---|---|---|
通信方式 | 仅异步 | 异步+同步 |
时钟信号 | 无 | 同步模式有 |
数据速率 | 较低 | 同步模式更高 |
连接方式 | 点对点 | 点对点/多点 |
应用场景 | 串口调试、GPS、蓝牙 | 同步外设、高速通信 |
异步通信(UART/USART)
特点:
- 无时钟信号
- 通过起始位、停止位同步
- 双方约定波特率