数组(Array)和链表(Linked List)是 C/C++ 中两种最基本的数据结构,各有特点和适用场景。
顺序存储,O(1)随机访问。
数组的大小在创建时确定,不能动态调整(除非使用 new
插入或删除元素时可能需要移动大量元素
使用:随机访问:缓冲区;顺序遍历但不频繁增删:静态列表;定长存储的数据结构:堆,栈
new
动态分配)。适用于 数据量固定且频繁访问 的情况,例如:
插入/删除高效,访问低效,额外空间开销。
使用:内存池:链表管理空闲内存块;动态集合:OS各种调度队列、图的邻接表;动态改变大小的:堆栈
适用于 数据量不固定且需要频繁插入/删除 的情况,例如:
std::list
实现的是双向链表,适用于哈希桶(unordered_map 的冲突处理)。特性 | 数组 (Array) | 链表 (Linked List) |
---|---|---|
存储方式 | 连续存储 | 链式存储(指针) |
访问速度 | O(1) (随机访问快) | O(n) (需要遍历) |
插入/删除 | O(n) (移动元素) | O(1) (修改指针) |
内存占用 | 无额外开销 | 每个节点需额外存储指针 |
大小伸缩 | 固定大小(可动态分配,但不易扩展) | 动态大小 |
适用场景 | 查找、固定大小数据 | 动态增删、链式存储 |
#include
int main() {
int arr[5] = {1, 2, 3, 4, 5}; // 静态数组,大小固定
std::cout << arr[2] << std::endl; // O(1) 访问
return 0;
}
#include
struct Node {
int data;
Node* next;
};
int main() {
Node* head = new Node{1, nullptr};
head->next = new Node{2, nullptr}; // 动态创建节点
std::cout << head->next->data << std::endl; // 访问第二个节点
return 0;
}
最终选择取决于应用场景,权衡时间复杂度和空间开销!
在 Linux(或其他类 UNIX 系统)中,全局变量和局部变量的存储位置不同,主要体现在进程的内存布局上。进程的内存通常分为以下几个主要区域:
------------------------------------------------
| 代码段(Text Segment) | → 存放程序的指令(代码),常量
------------------------------------------------
| 数据段(Data Segment) | → 存放全局变量和静态变量
| - 已初始化的全局变量和静态变量(.data)
| - 未初始化的全局变量和静态变量(.bss)
------------------------------------------------
| 堆(Heap) | → `malloc` / `new` 分配的动态内存
------------------------------------------------
| 共享库段(Shared Library Segment) | → 共享库映射的内存
------------------------------------------------
| 栈(Stack) | → 存放局部变量、函数调用信息
------------------------------------------------
全局变量存储在数据段(Data Segment),又细分为:
int a = 10;
)存放在 .data
段(数据段)。int b;
或 static int c;
)存放在 .bss
段(未初始化数据段)。static
关键字将其限制在当前文件。#include
int global_var = 10; // 已初始化,存于 .data 段
int uninit_global_var; // 未初始化,存于 .bss 段
int main() {
std::cout << global_var << std::endl;
return 0;
}
局部变量存储在**栈(Stack)**中。
#include
void testFunc() {
int localVar = 5; // 局部变量,存于栈
std::cout << localVar << std::endl;
} // 退出 testFunc 时 localVar 被销毁
int main() {
testFunc();
return 0;
}
⚠ 注意:如果局部变量的地址被返回并在外部使用,可能会访问已被释放的栈内存,导致未定义行为。
全局变量和局部变量是编译时分配的,而动态分配的变量存储在堆(Heap),通常使用 new
(C++)或 malloc
(C)分配。
#include
int* createVar() {
int* ptr = new int(10); // 在堆上分配
return ptr;
}
int main() {
int* p = createVar();
std::cout << *p << std::endl;
delete p; // 记得释放
return 0;
}
堆上的变量不会自动释放,必须手动 delete
(C++)或 free
(C)释放,否则会导致内存泄漏。
特性 | 全局变量 | 局部变量 |
---|---|---|
存储位置 | 数据段(.data/.bss) | 栈(Stack) |
生命周期 | 程序运行期间一直存在 | 函数调用结束后销毁 |
作用域 | 整个程序(或文件级别) | 定义它的函数/代码块 |
初始化 | 自动初始化(未初始化的设为 0) | 未初始化时值是随机的(栈上的旧值) |
多线程共享 | 可能会共享(需加锁) | 线程私有 |
访问速度 | 略慢(数据段访问) | 快(栈访问速度高) |
new/malloc
)由程序员管理,手动分配和释放,适用于动态数据。NULL
),但局部变量未初始化时值是随机的(栈上的旧数据)。选用策略:
最佳实践:尽量少用全局变量,局部变量优先,动态数据用智能指针(C++ std::unique_ptr
/ std::shared_ptr
)!
C 语言中的内存分配方式主要有 三种,分别是 静态分配、栈分配(自动分配)和堆分配(动态分配)。每种方式的特点和适用场景不同,下面详细讲解。
概念:
存储区域:
.data
段.bss
段"hello"
)优点:
缺点:
示例(静态分配的全局变量、静态变量):
#include
int global_var = 10; // 静态分配,存放于 .data 段
void func() {
static int static_var = 20; // 静态分配,存于 .data 段
printf("%d\n", static_var);
}
int main() {
printf("%d\n", global_var);
func();
return 0;
}
概念:
存储区域:
优点:
缺点:
示例(栈分配的局部变量):
#include
void func() {
int local_var = 10; // 局部变量,存于栈
printf("%d\n", local_var);
} // local_var 在函数结束时自动释放
int main() {
func();
return 0;
}
⚠ 注意:
如果返回局部变量的地址,可能会访问无效内存(栈已释放):
int* wrongFunc() {
int x = 42;
return &x; // ❌ 返回局部变量地址,x 在函数返回后被销毁
}
int main() {
int* p = wrongFunc();
printf("%d\n", *p); // ❌ 可能是垃圾值或崩溃
return 0;
}
概念:
malloc
/ calloc
/ realloc
/ free
在运行时动态申请和释放内存。存储区域:
free
释放。优点:
缺点:
函数 | 作用 | 失败时返回 |
---|---|---|
malloc(size) |
申请 size 字节的内存,但不会初始化 |
NULL |
calloc(n, size) |
申请 n * size 字节的内存,并初始化为 0 |
NULL |
realloc(ptr, new_size) |
调整 ptr 指向的内存大小 |
NULL |
free(ptr) |
释放 ptr 指向的内存 |
- |
示例(堆内存动态分配):
#include
#include // malloc, free
int main() {
int* p = (int*)malloc(sizeof(int)); // 申请 4 字节(int)
if (p == NULL) {
printf("内存分配失败\n");
return -1;
}
*p = 100; // 使用堆上的变量
printf("%d\n", *p);
free(p); // 释放内存
return 0;
}
⚠ 注意:
必须手动 free
,否则会导致内存泄漏。
避免“野指针”问题
:
int* p = (int*)malloc(10 * sizeof(int));
free(p);
p = NULL; // 避免 p 变成悬空指针
方式 | 存储位置 | 生命周期 | 访问速度 | 适用场景 | 释放方式 |
---|---|---|---|---|---|
静态分配 | 数据段(.data/.bss) | 程序全局 | 快 | 全局变量、常量、静态变量 | 自动释放 |
栈分配 | 栈(Stack) | 函数调用期间 | 最快 | 局部变量、函数参数 | 自动释放 |
堆分配 | 堆(Heap) | 需手动管理 | 慢(比栈慢) | 需动态管理的结构 | free() 释放 |
推荐策略: 能用栈就用栈,能用静态分配就用静态分配,只有在数据大小不确定时才用堆!
快速排序是一种**分治(Divide and Conquer)**思想的排序算法。其核心思路是:
选择基准(Pivot):从数组中选择一个元素作为基准(Pivot)。
分区(Partition)
:将数组
分成两部分
:
递归(Recursion):对左侧和右侧的子数组递归执行快速排序,直到子数组长度为 1 或 0。
假设对数组 arr[l...r]
进行排序:
#include
using namespace std;
// Lomuto 分区方法
int partition(int arr[], int l, int r) {
int pivot = arr[r]; // 选择最后一个元素作为基准
int i = l; // i 指向比 pivot 小的区域的末尾
for (int j = l; j < r; j++) {
if (arr[j] < pivot) {
swap(arr[i], arr[j]); // 交换,把小的移到前面
i++;
}
}
swap(arr[i], arr[r]); // 最后将 pivot 放到正确的位置
return i; // 返回 pivot 的索引
}
// 快速排序
void quickSort(int arr[], int l, int r) {
if (l < r) {
int pivotIndex = partition(arr, l, r); // 分区
quickSort(arr, l, pivotIndex - 1); // 递归排序左半部分
quickSort(arr, pivotIndex + 1, r); // 递归排序右半部分
}
}
int main() {
int arr[] = {10, 7, 8, 9, 1, 5};
int n = sizeof(arr) / sizeof(arr[0]);
quickSort(arr, 0, n - 1);
for (int x : arr) cout << x << " "; // 输出排序结果
return 0;
}
int hoarePartition(int arr[], int l, int r) {
int pivot = arr[l]; // 选择第一个元素作为基准
int i = l - 1, j = r + 1;
while (true) {
do { i++; } while (arr[i] < pivot); // 找到左侧大于 pivot 的元素
do { j--; } while (arr[j] > pivot); // 找到右侧小于 pivot 的元素
if (i >= j) return j; // 指针相遇,返回划分点
swap(arr[i], arr[j]);
}
}
void quickSort(int arr[], int l, int r) {
if (l < r) {
int pivotIndex = hoarePartition(arr, l, r);
quickSort(arr, l, pivotIndex);
quickSort(arr, pivotIndex + 1, r);
}
}
情况 | 时间复杂度 | 说明 |
---|---|---|
最好情况 | O(n log n) | 每次完美平分(基准值正好在中间)。 |
平均情况 | O(n log n) | 绝大多数情况下。 |
最坏情况 | O(n²) | 退化为冒泡排序(例如,每次选择的 pivot 总是最大/最小值)。 |
rand() % (r - l + 1) + l
作为 pivot 位置。(arr[l], arr[r], arr[mid])
三个数的中值作为 pivot,可减少极端情况影响。n < 10
),改用 插入排序(Insertion Sort),减少递归开销。快速排序 是一种 分治思想 的排序算法,平均时间复杂度 O(n log n),最坏情况 O(n²)(可优化)。
核心步骤
:
常见优化
:
应用场景
:
快排的精髓就在于“分区”!掌握好 partition 方法,快排就搞定了!
C++ 引用(Reference) 和 指针(Pointer) 都可以用于操作变量的地址,但它们在使用方式、特性和适用场景上有较大区别。
引用(Reference) | 指针(Pointer) | |
---|---|---|
定义方式 | int& ref = var; |
int* ptr = &var; |
访问值 | 直接使用 ref |
需要 *ptr 解引用 |
初始化 | 必须初始化 | 可以是 nullptr |
修改绑定 | 不能修改绑定 | 可以指向不同变量 |
是否为空 | 不存在 nullptr |
可以是 nullptr |
存储位置 | 变量的别名,无独立存储 | 独立存储,存放地址 |
#include
using namespace std;
int main() {
int a = 10;
int& ref = a; // ref 是 a 的别名
ref = 20; // 修改 ref = 修改 a
cout << a << endl; // 输出 20
return 0;
}
特点:
ref
是 a
的别名,ref
修改后 a
也会变。#include
using namespace std;
int main() {
int a = 10;
int* ptr = &a; // ptr 指向 a 的地址
*ptr = 20; // 通过指针修改 a
cout << a << endl; // 输出 20
return 0;
}
特点:
ptr
存的是 a
的地址,*ptr
修改值。nullptr
,稍后再赋值int a = 10;
int& ref = a; // ✅ 必须初始化
int* ptr; // ✅ 可以不初始化
int a = 10, b = 20;
int& ref = a;
ref = b; // ❌ 这不是修改引用,而是修改 a 的值为 b
int* ptr = &a;
ptr = &b; // ✅ 指针可以改变指向
nullptr
nullptr
nullptr
,用来表示“无效地址”int* ptr = nullptr; // ✅ 指针可以指向空
int& ref = nullptr; // ❌ 引用不能指向空
int a = 10;
int& ref = a;
int* ptr = &a;
cout << &ref << endl; // 输出 a 的地址
cout << &ptr << endl; // 指针本身有自己的地址
适用场景 | 选择 |
---|---|
需要动态管理对象(new/delete ) |
指针 |
作为函数参数(不希望修改指向) | 引用 |
需要表示“无效”状态 | 指针(nullptr) |
STL 容器中的迭代器 | 类似指针 |
void modifyRef(int& x) { x = 100; } // 引用传参
void modifyPtr(int* x) { if (x) *x = 100; } // 指针传参
int main() {
int a = 10;
modifyRef(a); // ✅ 直接修改 a
modifyPtr(&a); // ✅ 传递地址
}
方式 | 作用 |
---|---|
int* func() |
返回指针,可能返回 nullptr |
int& func() |
返回引用,必须保证返回值有效 |
int& getRef(int& x) { return x; } // 引用返回
int* getPtr(int& x) { return &x; } // 指针返回
int main() {
int a = 10;
int& ref = getRef(a); // ✅ 仍然是 a
int* ptr = getPtr(a); // ✅ 获取 a 的地址
}
⚠ 注意:不要返回局部变量的引用或指针!
int& badFunc() {
int x = 10;
return x; // ❌ 返回局部变量的引用
}
int* badFuncPtr() {
int x = 10;
return &x; // ❌ 返回局部变量地址
}
const
关键字const
指针const int* ptr1; // 不能修改指向的值
int* const ptr2; // 不能修改指向
const int* const ptr3; // 指向和值都不能改
const
引用void print(const int& x) { cout << x; } // 不能修改 x
对比项 | 引用(Reference) | 指针(Pointer) |
---|---|---|
是否必须初始化 | ✅ 必须 | ❌ 可以不初始化 |
是否可修改指向 | ❌ 不可修改 | ✅ 可以修改 |
是否可为 nullptr |
❌ 不能 | ✅ 允许 |
是否占用额外存储 | ❌ 不占 | ✅ 指针变量本身占空间 |
访问方式 | 直接使用 | * 解引用 |
适用场景 | 参数传递、对象别名 | 动态管理、可空指向 |
nullptr
,代码更简洁)引用 vs. 指针?要想高效编程,灵活运用才是关键!
多态(Polymorphism)是 C++ 面向对象编程(OOP) 的核心特性之一,它允许同一接口在不同对象上表现出不同的行为。
多态分为两类:
在实际开发中,基类提供通用接口,而派生类提供具体实现,这样可以在不修改调用代码的情况下,动态替换不同的实现。
示例:动物叫声(基类提供统一接口)
#include
using namespace std;
class Animal {
public:
virtual void makeSound() { cout << "Animal makes a sound" << endl; }
};
class Dog : public Animal {
public:
void makeSound() override { cout << "Woof! Woof!" << endl; }
};
class Cat : public Animal {
public:
void makeSound() override { cout << "Meow~" << endl; }
};
void playSound(Animal* animal) {
animal->makeSound(); // 调用的是子类的方法(运行时多态)
}
int main() {
Dog dog;
Cat cat;
playSound(&dog); // 输出 "Woof! Woof!"
playSound(&cat); // 输出 "Meow~"
return 0;
}
Animal*
统一管理不同类型的子类对象,不需要修改 playSound()
代码,即可支持新的动物类型,提高扩展性。✅ “面向接口编程,而不是面向实现编程”
✅ 让高层模块(如 playSound
)不依赖低层模块(Dog
、Cat
),只依赖抽象接口(Animal
)。
示例:数据库访问框架
class Database {
public:
virtual void connect() = 0;
};
class MySQL : public Database {
public:
void connect() override { cout << "Connecting to MySQL" << endl; }
};
class SQLite : public Database {
public:
void connect() override { cout << "Connecting to SQLite" << endl; }
};
void connectToDB(Database* db) {
db->connect(); // 依赖抽象接口,不依赖具体数据库类型
}
int main() {
MySQL mysql;
SQLite sqlite;
connectToDB(&mysql);
connectToDB(&sqlite);
return 0;
}
✅ 可以随时替换数据库(MySQL → SQLite)而不修改业务代码。
✅ 框架设计时,调用者只依赖抽象接口 Database
,实现解耦。
静态多态的实现方式:
class Vector {
public:
int x, y;
Vector(int a, int b) : x(a), y(b) {}
Vector operator+(const Vector& v) { // 重载 `+` 号运算符
return Vector(x + v.x, y + v.y);
}
};
int main() {
Vector v1(1, 2), v2(3, 4);
Vector v3 = v1 + v2; // 直接使用 `+` 号
cout << "v3: (" << v3.x << ", " << v3.y << ")" << endl;
}
✅ 运算符重载让自定义类型也可以使用自然的运算符,提高可读性。
优势 | 作用 |
---|---|
代码复用性高 | 通过基类指针调用子类方法,不需要修改调用代码 |
可扩展性强 | 添加新的子类时,不需要改动现有代码 |
解耦业务逻辑 | 调用者不需要知道具体实现,依赖抽象接口 |
易维护性 | 代码更清晰,减少冗余 |
场景 | 适用的多态类型 | 示例 |
---|---|---|
框架开发 | 运行时多态(虚函数) | 数据库、日志系统 |
插件系统 | 运行时多态(虚函数) | 浏览器插件 |
设计模式(策略模式) | 运行时多态(继承+虚函数) | AI 策略切换 |
泛型编程 | 编译时多态(模板) | std::sort() |
运算符重载 | 编译时多态(运算符重载) | + - * / |
函数重载 | 编译时多态(函数重载) | print(int) vs print(double) |
C++ 多态(Polymorphism)是让代码更灵活、扩展性更强的核心机制,在大型系统、框架开发、游戏开发等领域都有广泛应用。
✅ 运行时多态(虚函数):解耦、高扩展性,适用于面向对象架构
✅ 编译时多态(重载/模板):提高代码复用性,提升性能
✅ 合理利用多态,可使代码更清晰、可维护性更强!
不可以!
C++ 不允许 构造函数声明为 virtual
,因为它会导致一系列设计和实现上的问题。
示例(假设构造函数是虚的,可能会出问题):
class Base {
public:
virtual Base() { // ❌ 不允许
cout << "Base constructor" << endl;
}
};
问题:
Base
构造函数是虚函数,但对象还未创建,虚表未建立,无法调用子类的实现。多态的作用是让基类指针调用子类的方法,但构造函数的职责是初始化对象的状态。
如果构造函数是虚的,编译器无法确定应该调用哪个构造函数,导致设计冲突。
示例(错误示例,构造函数“虚拟化”没有意义):
class Base {
public:
virtual Base() { cout << "Base Constructor" << endl; } // ❌ 语法错误
};
如果需要“类似虚构造函数”的功能,可以使用 虚函数+工厂模式,间接实现**“虚拟构造”**的效果。
使用虚函数来创建对象,而不是直接调用构造函数。
class Base {
public:
virtual void show() { cout << "Base" << endl; }
virtual ~Base() = default; // 虚析构函数
static Base* create(); // 工厂方法
};
class Derived : public Base {
public:
void show() override { cout << "Derived" << endl; }
};
Base* Base::create() {
return new Derived(); // 通过工厂方法创建子类对象
}
int main() {
Base* obj = Base::create(); // 通过静态工厂方法创建对象
obj->show(); // 调用 Derived 的 show()
delete obj;
}
如果需要多态对象的复制,可以使用虚拟拷贝构造(Clone 模式):
class Base {
public:
virtual Base* clone() const = 0; // 纯虚克隆函数
virtual ~Base() = default;
};
class Derived : public Base {
public:
Derived* clone() const override { return new Derived(*this); }
};
int main() {
Base* obj = new Derived();
Base* objCopy = obj->clone(); // 复制对象(多态)
delete obj;
delete objCopy;
}
构造函数是否可以是虚的? | ❌ 不可以 |
---|---|
原因 1 | 虚函数依赖 vtable,但对象构造时 vtable 还未初始化 |
原因 2 | 构造函数的作用是初始化对象,而不是实现多态 |
原因 3 | C++ 设计原则:构造逻辑应当清晰,避免复杂的多态行为 |
替代方案 | 使用工厂模式 / 虚拟拷贝构造 |
构造函数不能是虚的,但可以通过工厂模式或克隆机制实现“类似虚拟构造”的效果!
抽象类(Abstract Class)是至少包含一个纯虚函数的类,它不能直接实例化,必须由子类实现所有纯虚函数后,才能创建对象。
class Abstract {
public:
virtual void func() = 0; // 纯虚函数
};
抽象类用于定义接口,让不同子类遵循相同规则。
class Animal {
public:
virtual void speak() = 0; // 纯虚函数
};
class Dog : public Animal {
public:
void speak() override { cout << "Woof!" << endl; }
};
class Cat : public Animal {
public:
void speak() override { cout << "Meow!" << endl; }
};
void makeSound(Animal* animal) {
animal->speak(); // 调用多态方法
}
int main() {
Dog d;
Cat c;
makeSound(&d); // Woof!
makeSound(&c); // Meow!
}
✅ 保证所有 Animal
子类都必须实现 speak()
,否则编译报错!
抽象类可以提供部分实现,子类只需实现特定的纯虚函数。
class Shape {
public:
virtual double area() = 0; // 纯虚函数
void show() { cout << "This is a shape." << endl; } // 具体实现
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() override { return 3.14 * radius * radius; }
};
int main() {
Circle c(5);
cout << c.area() << endl; // 78.5
c.show(); // This is a shape.
}
✅ Shape
不能实例化,但可以提供 show()
供子类复用。
抽象类可用于插件系统、回调机制等,例如 GUI 框架:
class Button {
public:
virtual void onClick() = 0; // 纯虚函数
};
class SubmitButton : public Button {
public:
void onClick() override { cout << "Submit form" << endl; }
};
void triggerClick(Button* btn) {
btn->onClick();
}
int main() {
SubmitButton btn;
triggerClick(&btn); // Submit form
}
✅ 保证所有按钮类都必须实现 onClick()
逻辑。
特性 | 描述 |
---|---|
含纯虚函数 (= 0 ) |
至少有一个纯虚函数的类就是抽象类 |
不可实例化 | 不能创建抽象类的对象 |
必须被子类实现 | 子类必须实现所有纯虚函数,否则仍然是抽象类 |
用于多态 | 通过基类指针调用子类的方法 |
应用场景 | 统一接口、框架设计、回调机制、代码复用 |
抽象类是面向对象设计的核心,能提高代码的可扩展性和复用性!
简单来说,C++ 的多态是通过 “基类指针/引用指向子类对象” 来实现的。原因如下:
多态的前提是继承,而基类指针/引用能存储子类对象的地址,这让 C++ 能通过 vtable(虚函数表) 动态调用子类的重写方法。
class Base {
public:
virtual void show() { cout << "Base show()" << endl; }
};
class Derived : public Base {
public:
void show() override { cout << "Derived show()" << endl; }
};
int main() {
Base* obj = new Derived(); // ✅ 基类指针指向子类
obj->show(); // 调用 Derived::show()(运行时多态)
delete obj;
}
✔ 这里的核心: Base* obj
指向 Derived
对象,调用 show()
时,通过 vtable 解析出子类的 show()
方法,实现动态绑定。
如果用 子类指针指向基类对象,会发生类型错误或未定义行为!
Base* b = new Base();
Derived* d = b; // ❌ 错误,不能把基类指针直接赋值给子类指针
主要问题:
Base
只包含自己的数据,Derived*
可能会访问不存在的成员,导致未定义行为。C++ 支持向上转型(Upcasting),即子类对象可以转换为基类指针/引用:
Derived d;
Base* b = &d; // ✅ OK,向上转型
b->show(); // 调用 Derived::show(),多态生效
b->show()
仍然调用 Derived::show()
。如果一定要把 基类指针转换为子类指针,必须显式转换:
Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base); // ✅ 向下转型
if (derived) {
derived->show(); // 运行时安全转换
}
dynamic_cast<>
只能用于带virtual
的类,并在运行时检查是否安全。
情况 | 能否转换? | 是否安全? | 原因 |
---|---|---|---|
子类对象 → 基类指针/引用(向上转型) | ✅ 可以 | ✅ 安全 | 继承保证基类部分存在 |
基类指针/引用 → 子类指针(向下转型) | 默认不行 | ⚠ 可能不安全 | 可能访问不存在的成员 |
基类指针 → 子类指针(dynamic_cast) | ✅ 可以 | ✅ 运行时检查 | dynamic_cast<> 可确保类型安全 |
多态依赖于基类指针/引用,因为它们能通过 vtable
解析子类的正确方法,反向转换是不安全的!
多态的底层实现依赖于 虚函数表(vtable)和虚表指针(vptr)。理解这些机制,可以帮助你更好地掌握 C++ 的对象模型和运行时行为。
当一个类包含虚函数时,编译器会创建一个 虚函数表(vtable),它是一个函数指针数组,存储了该类的所有虚函数地址。
每个含虚函数的类都会有一张vtable,每个对象都会有一个虚表指针(vptr),指向这张表。
#include
using namespace std;
class Base {
public:
virtual void show() { cout << "Base show()" << endl; }
};
class Derived : public Base {
public:
void show() override { cout << "Derived show()" << endl; }
};
int main() {
Base* obj = new Derived();
obj->show(); // 运行时多态
delete obj;
}
多态调用的流程:
obj->show()
时:
obj
是 Base*
,它指向 Derived 对象。obj->vptr
指向 Derived 的 vtable
。vtable
,找到 Derived::show()
的地址并执行它。假设 Base
有一个 int a
,Derived
继承 Base
并有 int b
,它们的内存布局如下:
地址 | 内容 |
---|---|
obj (Base* 指针) |
指向 Derived 对象 |
vptr |
指向 Derived 的 vtable |
Base::a |
继承自 Base |
Derived::b |
Derived 额外的成员 |
虚表(vtable
)内容如下:
Derived vtable |
---|
Derived::show() 的地址 |
说明
vptr
让 Base*
指针在调用 show()
时,动态绑定到 Derived::show()
。编译器自动插入代码来维护 vptr
,相当于如下:
class Base {
public:
void* vptr; // 编译器自动添加的隐藏成员
virtual void show();
};
class Derived : public Base {
public:
virtual void show();
};
void Base::show() { cout << "Base" << endl; }
void Derived::show() { cout << "Derived" << endl; }
void callShow(Base* obj) {
// 模拟 obj->show() 调用
(*(obj->vptr)[0])();
}
class Base {
public:
void show() { cout << "Base show()" << endl; } // 非虚函数
};
class Derived : public Base {
public:
void show() { cout << "Derived show()" << endl; }
};
int main() {
Base* obj = new Derived();
obj->show(); // 输出 Base show(),而不是 Derived show()
}
非虚函数直接在编译期静态绑定,调用 obj->show()
时,编译器只看指针的静态类型(Base*
),而不会检查实际对象的类型(Derived
)。
只有虚函数才通过 vtable 实现动态绑定!
机制 | 作用 |
---|---|
vtable(虚函数表) | 存储类的虚函数地址 |
vptr(虚表指针) | 每个对象的隐藏指针,指向 vtable |
多态调用过程 | 通过 vptr 找到 vtable,动态调用函数 |
非虚函数调用 | 静态绑定,编译期确定 |
虚函数调用 | 运行时绑定,通过 vtable 解析 |
C++ 的多态是通过 vtable + vptr 实现的,理解这个原理可以更深入地优化 C++ 代码!
在 C++ 中,拷贝操作指的是将一个对象的值复制到另一个对象。这个拷贝过程分为 浅拷贝(shallow copy)和 深拷贝(deep copy)。它们之间的差异在于对象中指针成员(或者资源)的处理方式。
浅拷贝是逐成员复制对象,即只复制对象的基本数据成员,如果对象中包含指针类型的成员变量,拷贝的只是指针值(地址),而不是指针指向的数据。
结果是原对象和拷贝对象将共享相同的内存或资源。
#include
using namespace std;
class ShallowCopy {
public:
int* ptr;
ShallowCopy(int value) {
ptr = new int(value);
}
// 浅拷贝构造函数
ShallowCopy(const ShallowCopy& other) {
ptr = other.ptr; // 仅复制指针的地址,而不是数据
}
~ShallowCopy() {
delete ptr; // 会删除共享的内存,导致悬挂指针
}
};
int main() {
ShallowCopy obj1(10);
ShallowCopy obj2 = obj1; // 浅拷贝
cout << *obj1.ptr << endl; // 输出:10
cout << *obj2.ptr << endl; // 输出:10
// 删除时,两个对象的 ptr 会指向同一内存,删除时会引发错误
}
问题:
obj1
和 obj2
都指向同一内存,销毁其中一个对象时,另一个对象的指针会变成悬空指针。深拷贝会复制对象中的所有成员,如果成员中包含指针类型的成员,它会分配新的内存并将数据复制到新内存中。也就是说,深拷贝会复制指针所指向的实际数据,从而使得原对象和拷贝对象之间的数据互不干扰。
#include
using namespace std;
class DeepCopy {
public:
int* ptr;
DeepCopy(int value) {
ptr = new int(value);
}
// 深拷贝构造函数
DeepCopy(const DeepCopy& other) {
ptr = new int(*other.ptr); // 分配新的内存,并复制值
}
~DeepCopy() {
delete ptr; // 正常删除独立的内存
}
};
int main() {
DeepCopy obj1(10);
DeepCopy obj2 = obj1; // 深拷贝
cout << *obj1.ptr << endl; // 输出:10
cout << *obj2.ptr << endl; // 输出:10
// 删除 obj1 和 obj2 不会相互影响
}
优点:
obj1
和 obj2
拷贝后各自拥有独立的内存,修改一个对象的 ptr
不会影响另一个对象。特性 | 浅拷贝(Shallow Copy) | 深拷贝(Deep Copy) |
---|---|---|
复制方式 | 逐成员复制,指针成员复制地址 | 复制所有数据,包括指针所指向的数据(内存) |
内存分配 | 不会分配新的内存,指针成员指向同一内存块 | 分配新的内存空间并复制数据 |
对象关系 | 原对象与拷贝对象共享同一块内存资源 | 原对象与拷贝对象的数据互不干扰,各自拥有独立内存 |
副作用 | 修改拷贝对象可能影响原对象,可能导致悬挂指针或内存泄漏 | 原对象与拷贝对象互不干扰,数据独立,避免副作用 |
性能开销 | 性能较高,操作简单,不涉及内存分配 | 性能较低,需要分配新内存并复制数据 |
拷贝类型 | 特点 | 适用场景 |
---|---|---|
浅拷贝 | 只复制指针地址,共享资源,可能引发副作用 | 简单对象,数据共享,不需要独立管理内存资源 |
深拷贝 | 完全复制数据,避免数据共享和副作用 | 包含动态分配内存的对象,需要独立管理资源 |
浅拷贝和深拷贝的选择取决于对象的结构和资源管理需求,深拷贝通常更安全,但也会带来性能开销。
在 Linux 系统中,有很多常用的命令和指令,帮助我们进行文件管理、系统监控、进程控制等操作。以下是一些常用的 Linux 指令:
ls
列出目录内容。
ls # 显示当前目录内容
ls -l # 显示详细信息
ls -a # 显示包括隐藏文件在内的所有文件
cd
切换当前目录。
cd /path/to/directory # 切换到指定目录
cd .. # 返回到上级目录
cd ~ # 切换到用户的 home 目录
pwd
显示当前工作目录的路径。
pwd
mkdir
创建目录。
mkdir new_folder # 创建一个新目录
mkdir -p /path/to/dir # 创建多级目录(如果父目录不存在则创建)
rmdir
删除空目录。
rmdir folder_name
rm
删除文件或目录。
rm file.txt # 删除文件
rm -r dir_name # 删除目录及其内容
rm -f file.txt # 强制删除文件,不提示
cp
复制文件或目录。
cp source.txt dest.txt # 复制文件
cp -r dir1/ dir2/ # 复制目录及其内容
mv
移动或重命名文件/目录。
mv old_name.txt new_name.txt # 重命名文件
mv file.txt /path/to/destination # 移动文件
touch
创建空文件,或更新文件的修改时间戳。
touch newfile.txt
cat
显示文件内容。
cat file.txt
more
分页显示文件内容。
more file.txt
less
分页显示文件内容,支持上下滚动。
less file.txt
head
显示文件的前几行。
head file.txt
head -n 20 file.txt # 显示前 20 行
tail
显示文件的最后几行。
tail file.txt
tail -n 20 file.txt # 显示最后 20 行
tail -f log.txt # 持续显示文件内容,常用于查看日志
find
查找文件或目录。
find /path -name "filename" # 按名称查找文件
find /path -type d -name "folder_name" # 查找目录
locate
快速查找文件(基于数据库索引)。
locate filename
which
查找命令的路径。
which ls # 查找 `ls` 命令的位置
top
显示系统的实时资源使用情况,特别是 CPU 和内存使用情况。
top
htop
top
的增强版本,提供更直观的界面(需要安装)。
htop
df
查看磁盘空间使用情况。
df -h # 以人类可读的方式显示磁盘使用情况
du
查看目录或文件的磁盘使用情况。
du -sh /path/to/dir # 显示指定目录的大小
du -h # 以人类可读的方式显示文件或目录的大小
free
显示内存使用情况。
free -h # 显示内存使用情况
whoami
显示当前登录的用户名。
whoami
chmod
更改文件或目录的权限。
chmod 755 file.txt # 设置权限 rwxr-xr-x
chmod +x script.sh # 给文件添加执行权限
chown
更改文件或目录的所有者。
chown user:group file.txt # 更改文件所有者和组
useradd
添加新用户。
useradd username
passwd
修改用户的密码。
passwd username
ps
显示当前系统中的进程。
ps aux # 显示所有进程
ps -ef # 显示所有进程的完整信息
kill
结束进程。
kill PID # 杀死指定进程
kill -9 PID # 强制杀死进程
pkill
按照进程名称结束进程。
pkill process_name
top
查看系统实时的进程和资源使用情况。
top
ping
检查网络连通性。
ping google.com
ifconfig
查看和配置网络接口(传统命令,现代系统用 ip
代替)。
ifconfig
ip
查看和配置网络接口(现代替代命令)。
ip addr show # 查看网络接口
ip link show # 查看网络接口状态
netstat
查看网络连接状态。
netstat -tuln # 查看所有监听的端口
curl
用于发送网络请求,下载文件。
curl http://example.com # 获取网页内容
curl -O http://example.com/file.txt # 下载文件
apt-get
(Debian/Ubuntu 系统)
管理软件包。
sudo apt-get update # 更新软件包列表
sudo apt-get install pkg # 安装软件包
sudo apt-get upgrade # 更新所有已安装的软件包
yum
(CentOS/RHEL 系统)
管理软件包。
sudo yum install pkg # 安装软件包
sudo yum update # 更新所有已安装的软件包
dpkg
(Debian/Ubuntu 系统)
用于安装 .deb
包。
sudo dpkg -i package.deb
这些指令覆盖了文件操作、系统监控、进程管理、网络操作等常见任务。熟悉这些命令将帮助你在 Linux 环境中高效工作。
Linux文件权限系统是确保系统安全和多用户管理的重要机制。以下是对其详细的分步解释:
权限类型:
用户分类:
ls -l 文件名
-rwxr-xr-- 1 user group 1024 Jun 1 10:00 file
-
普通文件,d
目录,l
链接等)。rwx
)。r-x
)。r--
)。rwxr-xr--
对应754。chmod
命令:
chmod 755 文件
→ 所有者rwx,组和其他r-x。chmod u+x,g-w,o=r 文件
→ 所有者加x,组减w,其他设为r。
+
(添加)、-
(删除)、=
(精确设置)。chmod -R 755 目录/
→ 递归应用权限。chown
:sudo chown 用户:组 文件
→ 修改所有者和所属组。chgrp
:sudo chgrp 组 文件
→ 仅修改所属组。默认权限 = 最大权限 - umask
。umask
→ 显示当前值。umask 002
→ 设置新值。chmod u+s 文件
或 chmod 4755
(4前缀)。s
(如rwsr-xr-x
)。chmod g+s 文件
或 chmod 2755
(2前缀)。s
(如rwxr-sr-x
)。chmod +t 目录
或 chmod 1777
(1前缀)。t
(如rwxrwxrwt
)。setfacl -m u:用户名:权限 文件
→ 添加用户权限。setfacl -m g:组名:权限 文件
→ 添加组权限。getfacl 文件
→ 查看ACL详情。setfacl -m u:alice:rwx 文件
→ 赋予alice完全权限。777
)。/etc/passwd
)权限应严格限制。chmod 2775 shared_dir/ && chmod +t shared_dir/
。chmod +x script.sh
。掌握Linux文件权限需理解基础权限、用户分类、修改命令及特殊权限的应用。合理配置权限是系统安全的关键,建议结合ACL实现复杂场景的权限管理,并始终遵循最小权限原则。操作关键系统文件前,务必进行备份或测试,避免误操作导致服务中断。
面试时,如果被问到是否写过 Shell 脚本,通常是在考察你对 Linux 系统的理解以及能否通过脚本来自动化常见任务。接下来,我会详细解释 Shell 脚本的用途、编写方法、执行方式等内容。
Shell 脚本是用于自动化任务的一种脚本语言,它通过调用一系列 Shell 命令来完成特定的任务。它可以帮助提高工作效率,减少手动操作,特别适合于系统管理和自动化。
常见的应用场景包括:
cron
)。Shell 脚本本质上是一个包含 Shell 命令的文本文件。你可以用任何文本编辑器(如 vim
、nano
或 gedit
)来编写脚本。
创建脚本文件: 使用 touch
命令或直接在编辑器中创建一个 .sh
后缀的文件。
touch myscript.sh
编写脚本内容: 使用文本编辑器打开文件并编写脚本内容。
#!/bin/bash # 指定脚本的解释器是 Bash
# 输出 "Hello, World!"
echo "Hello, World!"
解释:
#!/bin/bash
:指定脚本的解释器为 bash
(Bash 是常用的 Shell 类型之一)。echo
:是一个常用命令,用来打印输出。保存并退出编辑器。
注释
:使用
#
来注释代码,注释不会被执行。
# This is a comment
变量
:可以定义变量并在脚本中使用。
name="Alice"
echo "Hello, $name"
条件语句
:可以根据条件执行不同的命令。
if [ $name == "Alice" ]; then
echo "Welcome, Alice!"
else
echo "You are not Alice."
fi
循环
:可以使用循环执行多次任务。
for i in {1..5}
do
echo "Iteration $i"
done
Shell 脚本执行有两种常见方法:直接执行和通过 Bash 执行。
给脚本文件添加执行权限: 在 Linux 中,文件必须具备执行权限才能运行。使用 chmod
命令添加执行权限:
chmod +x myscript.sh
执行脚本: 使用 ./
来执行脚本:
./myscript.sh
bash
执行脚本如果不想设置执行权限,也可以通过 bash
来直接执行脚本:
bash myscript.sh
.sh
后缀命名。bash
命令来执行。如果面试中有类似问题,除了回答这些技术性细节外,也可以举一些实际的例子,像是你使用 Shell 脚本自动化部署、监控系统、备份数据等,展现你使用 Shell 脚本的经验和能力。
在 Linux 中,有多种命令可以用来查看文件的内容,具体可以根据需要查看文件的全部内容、部分内容或按特定格式查看。以下是一些常见的查看文件内容的命令:
cat
用于显示整个文件的内容。
查看文件的全部内容:
cat filename.txt
可以连接多个文件并显示:
cat file1.txt file2.txt
more
分页显示文件内容,支持向下滚动。
显示文件内容并分页:
more filename.txt
按空格键向下滚动一页,按 q
退出。
less
分页显示文件内容,支持上下滚动和搜索,功能比 more
更强大。
显示文件内容:
less filename.txt
按 q
退出,按上下方向键或 Page Up
、Page Down
键进行滚动。
head
显示文件的前面几行,默认显示前 10 行,可以通过 -n
选项指定行数。
显示文件的前 10 行:
head filename.txt
显示前 20 行:
head -n 20 filename.txt
tail
显示文件的最后几行,默认显示最后 10 行,可以通过 -n
选项指定行数。
显示文件的最后 10 行:
tail filename.txt
显示最后 20 行:
tail -n 20 filename.txt
使用
-f
选项持续输出文件的新增内容,常用于查看日志文件的实时更新:
tail -f filename.txt
nl
显示文件内容,并为每行加上行号。
显示文件并添加行号:
nl filename.txt
tac
反向显示文件内容,从文件的最后一行开始显示。
反向显示文件内容:
tac filename.txt
grep
用于搜索文件中的特定模式并显示匹配的行,可以用来查看包含特定内容的行。
查找包含某个单词的行:
grep "word" filename.txt
使用
-n
选项显示行号:
grep -n "word" filename.txt
awk
强大的文本处理工具,可以用于显示特定的列或行。
显示文件的第 1 列和第 2 列:
awk '{print $1, $2}' filename.txt
sed
流编辑器,用于处理和查看文件内容。
显示文件内容并将其通过
sed
处理(如替换字符串):
sed 's/old/new/g' filename.txt
cat
:显示整个文件内容。more
和 less
:分页显示内容,less
功能更强大。head
和 tail
:显示文件的前几行或后几行。grep
:按模式搜索并显示匹配的行。nl
:显示文件并加上行号。tac
:反向显示文件内容。awk
和 sed
:用于更复杂的文本处理和查看。根据具体需求,可以选择不同的命令来查看文件的内容。
在 Linux 或其他操作系统中,进程和线程是两个基本的概念,它们虽然都涉及执行单元,但在系统资源的分配、调度方式等方面有所不同。下面我会详细解释它们的区别,并且讨论多线程的作用。
特性 | 进程 | 线程 |
---|---|---|
资源分配 | 每个进程有独立的内存和资源 | 线程共享进程的内存和资源 |
开销 | 创建、销毁和上下文切换开销较大 | 创建、销毁和上下文切换开销较小 |
通信 | 通过进程间通信(IPC) | 通过共享内存或信号量等进行通信 |
独立性 | 进程相互独立 | 线程间共享资源,依赖于进程 |
执行单位 | 进程是操作系统调度的基本单位 | 线程是进程中的执行单元 |
多线程在现代操作系统中非常常见,特别是在需要提高并发性和响应性的应用中。多线程的作用主要体现在以下几个方面:
虽然多线程有很多优势,但在开发中也面临着一些挑战: