基础概念
1. 面向过程与面向对象的区别
面向过程编程
- 以解决问题的步骤为中心
- 程序分解为一组函数,每个函数负责特定任务
- 数据与函数分离
- 注重算法和流程控制
面向对象编程
- 以对象为核心
- 数据和操作封装在对象内部
- 通过定义类创建具体的对象实例
- 注重抽象、封装、继承和多态
主要区别
特性 | 面向过程 | 面向对象 |
---|---|---|
抽象层次 | 关注步骤和算法 | 关注对象和行为 |
数据封装性 | 数据与函数分离 | 数据与方法封装在一起 |
继承和多态 | 无继承和多态概念 | 核心特性 |
代码复用方式 | 模块化设计 | 类的继承和组合 |
📌 选择建议:大型项目适合面向对象(可维护性、扩展性更好),小型简单项目可选择面向过程(更直观高效)。
2. C和C++的区别
特性 | C语言 | C++ |
---|---|---|
编程范式 | 面向过程 | 面向对象(同时支持面向过程) |
对象支持 | 不支持类和对象 | 支持类、继承、多态等OOP特性 |
标准库 | 基本的输入输出、字符串处理 | 包含C标准库外,还有STL等更丰富的库 |
异常处理 | 无内置异常处理机制 | 支持try-catch异常处理 |
内存管理 | malloc/free | 除了malloc/free外,还有new/delete |
编译器兼容性 | C编译器一般不支持C++代码 | C++编译器通常可编译C代码 |
💡 核心区别:C++在C的基础上增加了面向对象的特性,提供更强大的抽象和封装能力。
3. static关键字的作用
1️⃣ 静态局部变量
- 生命周期与程序运行期间一致
- 不随函数调用结束而销毁
- 保留上次函数调用后的值
void increment() {
static int counter = 0; // 只在第一次调用初始化
counter++;
cout << "Counter: " << counter << endl;
}
int main() {
increment(); // 输出: Counter: 1
increment(); // 输出: Counter: 2
increment(); // 输出: Counter: 3
return 0;
}
2️⃣ 静态函数
- 只在当前文件范围内可见
- 限制函数作用域,避免命名冲突
// 在同一个文件中定义的静态函数
static void internalFunction() {
cout << "This is an internal function." << endl;
}
3️⃣ 静态全局变量
- 只能在声明它的源文件中访问
- 防止不同源文件之间的命名冲突
4️⃣ 静态类成员
- 属于类本身而非实例对象
- 可通过类名直接访问
- 所有实例共享同一个静态成员
class MyClass {
public:
static int count;
static void increaseCount() {
count++;
cout << "Count: " << count << endl;
}
};
int MyClass::count = 0; // 必须在类外初始化静态成员
int main() {
MyClass::increaseCount(); // 输出: Count: 1
MyClass::increaseCount(); // 输出: Count: 2
return 0;
}
4. const关键字的作用
1️⃣ 声明常量变量
const int MAX_VALUE = 100; // 声明一个不可修改的常量
2️⃣ 保护函数参数
void printMessage(const string& message) {
// message不会被修改
cout << message << endl;
}
3️⃣ 防止函数修改对象状态
class MyClass {
public:
void printValue() const { // const成员函数
cout << value << endl;
// 不能修改类的成员变量
}
private:
int value;
};
4️⃣ 限制返回值的修改
const int getValue() {
return 42; // 返回值不能被修改
}
🔑 const的价值:提高代码安全性并明确表达设计意图,帮助编译器优化。
5. synchronized关键字和volatile关键字区别
synchronized关键字
- C++没有直接对应Java中synchronized关键字
- C++使用互斥量(mutex)实现类似功能
- 用于保证临界区代码的互斥访问
volatile关键字
- 指示编译器不对变量进行优化
- 确保每次访问该变量都从内存读取或写入
- 用于处理多线程环境下共享数据、信号处理、硬件寄存器等场景
- 与Java不同,C++的volatile不能保证原子性、可见性或禁止重排序
volatile int shared_counter; // 告诉编译器不要优化对此变量的访问
⚠️ 注意:volatile本身不能保证线程安全,通常需要配合互斥量使用。
6. C语言中struct和union的区别
结构体(struct)
- 存储不同类型的数据成员
- 每个成员独立占用内存空间
- 结构体大小为所有成员大小之和(加上对齐填充)
- 可同时访问所有成员
联合体(union)
- 所有成员共享同一块内存空间
- 一次只能存储一个成员的值
- 大小等于最大成员的大小
- 修改一个成员会影响其他成员
struct Point {
int x; // 4字节
int y; // 4字节
}; // 总大小: 8字节
union Data {
int i; // 4字节
float f; // 4字节
char str[8]; // 8字节
}; // 总大小: 8字节(最大成员的大小)
💡 使用场景:union适用于需要在不同类型之间转换或节省内存空间的场景。
7. C++中struct和class的区别
特性 | struct | class |
---|---|---|
默认访问控制 | public | private |
成员函数默认修饰符 | public | private |
继承方式 | 默认public继承 | 默认private继承 |
使用习惯
- struct通常用于简单数据结构的定义
- class更常用于封装复杂对象及其行为
struct DataPoint {
int x, y; // 默认public
};
class Entity {
int id; // 默认private
public:
void setId(int value) { id = value; }
};
📝 编程风格:struct更适合POD(Plain Old Data)类型,class更适合需要封装、继承的场景。
8. 数组和指针的区别
特性 | 数组 | 指针 |
---|---|---|
内存分配 | 静态分配,固定大小 | 可动态分配内存 |
数据访问 | 使用下标 | 使用解引用(*) |
可修改性 | 数组名不可修改 | 指针可重新指向不同地址 |
函数参数传递 | 退化为指针 | 直接传递指针值 |
int arr[5] = {1, 2, 3, 4, 5}; // 静态数组
int* ptr = new int[5]; // 动态数组,通过指针访问
// 数组名是常量指针
// arr = ptr; // 错误:不能修改数组名
ptr = arr; // 正确:指针可以重新赋值
🔍 本质区别:数组名本质上是指向首元素的常量指针,而指针是变量,可以存储和修改内存地址。
9. 程序执行的过程
-
编译阶段
- 源代码(.cpp/.c)经过预处理、编译,生成目标文件(.o/.obj)
-
链接阶段
- 链接器将多个目标文件和库文件链接,生成可执行文件
-
加载阶段
- 操作系统将可执行文件加载到内存
- 为程序分配所需资源(内存空间、文件句柄等)
-
执行阶段
- CPU按指令序列执行程序
- 执行算术运算、逻辑判断、内存读写等操作
-
库调用阶段
- 程序调用系统库函数和运行时库函数
-
结束阶段
- 程序执行完毕,操作系统回收资源
- 返回执行结果
🔄 完整流程:代码 → 编译 → 链接 → 加载 → 执行 → 结束
10. C++中指针和引用的区别
特性 | 指针 | 引用 |
---|---|---|
定义方式 | 使用* (int* p) | 使用& (int& r) |
初始化 | 可以延迟初始化 | 必须在定义时初始化 |
空值 | 可以为nullptr | 必须引用有效对象 |
可改变性 | 可重新指向其他对象 | 一旦绑定不能改变 |
内存占用 | 占额外内存空间 | 不占额外空间 |
访问方式 | 需解引用(*p) | 直接访问® |
int value = 10;
int* ptr = &value; // 指针指向value
int& ref = value; // 引用绑定到value
*ptr = 20; // 通过指针修改
ref = 30; // 通过引用修改
int another = 50;
ptr = &another; // 指针可以重新指向
// ref = another; // 错误理解:这不是重新绑定,而是赋值操作
🌟 使用建议:引用通常用于函数参数和返回值,使代码更清晰;指针则适用于需要改变指向或处理空值的场景。
面向对象与高级特性
21. 引用与指针的详细比较
特性 | 引用(Reference) | 指针(Pointer) |
---|---|---|
本质 | 对象的别名 | 存储内存地址的变量 |
初始化 | 必须初始化且不能为空 | 可以延迟初始化,可为nullptr |
重新赋值 | 不能重新绑定到其他对象 | 可以改变指向的对象 |
多重间接性 | 不支持多级引用 | 支持多级指针 |
语法使用 | 使用更简洁 | 需要解引用操作 |
空值检查 | 不需要检查空值 | 使用前需要检查nullptr |
算术运算 | 不支持引用算术 | 支持指针算术(++、–等) |
// 引用示例
int original = 5;
int& ref = original; // 引用必须初始化
ref = 10; // 修改original的值为10
// 指针示例
int* ptr = nullptr; // 可以初始化为nullptr
ptr = &original; // 可以重新指向
*ptr = 15; // 通过解引用修改值
ptr++; // 指针算术,移动到下一个int位置
🌟 最佳实践:
- 优先使用引用作为函数参数,特别是不需要修改指向的情况
- 当需要表示"无对象"状态或需要改变指向时,使用指针
- 使用引用可以使代码更清晰、安全
22. 函数重载(Function Overloading)
函数重载的定义
- 允许在同一作用域中定义多个同名但参数不同的函数
- 编译器根据调用时的参数类型和数量匹配合适的函数
重载的条件
- 函数名相同
- 参数列表不同(类型、数量或顺序)
- 返回类型不同但参数相同的函数不构成重载
// 函数重载示例
int add(int a, int b) {
return a + b;
}
float add(float a, float b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
// 调用示例
int sum1 = add(5, 3); // 调用第一个函数
float sum2 = add(5.0f, 3.0f); // 调用第二个函数
int sum3 = add(5, 3, 2); // 调用第三个函数
重载解析过程
- 编译器找到所有同名函数
- 寻找参数完全匹配的函数
- 如果没有完全匹配,尝试通过类型转换找到最佳匹配
- 如果有多个可能的匹配,则产生二义性错误
注意事项
- 避免依赖隐式类型转换的重载
- 小心默认参数与重载结合使用
- C语言不支持函数重载
💡 优点:函数重载增强了代码可读性和易用性,使相似操作可以使用相同的函数名。
23. 类和对象
类(Class)的概念
- 用户定义的数据类型
- 描述具有相似特征和行为的对象的蓝图
- 封装了数据(属性)和操作数据的方法(函数)
对象(Object)的概念
- 类的实例
- 具有具体状态和行为的实体
- 占用内存空间
类的定义
class Student {
private:
// 数据成员(属性)
int id;
string name;
float score;
public:
// 构造函数
Student(int i, string n, float s) : id(i), name(n), score(s) {}
// 成员函数(方法)
void display() const {
cout << "ID: " << id << ", Name: " << name
<< ", Score: " << score << endl;
}
float getScore() const {
return score;
}
void setScore(float s) {
score = s;
}
};
对象的创建与使用
// 对象创建
Student alice(1001, "Alice", 92.5); // 栈上分配
Student* bob = new Student(1002, "Bob", 85.0); // 堆上分配
// 对象使用
alice.display(); // 使用点操作符访问
bob->display(); // 使用箭头操作符访问
alice.setScore(95.0); // 修改属性
float bobScore = bob->getScore(); // 获取属性
// 释放堆对象
delete bob;
类与对象的关系
- 类是对象的模板,对象是类的具体实例
- 一个类可以创建多个对象
- 每个对象有自己的数据成员副本,但共享成员函数
📝 面向对象的核心:类与对象是面向对象编程的基本单位,通过数据封装、继承和多态实现代码复用和模块化。
24. C++的访问修饰符
三种访问修饰符
-
public
- 可在任何地方访问
- 定义类的接口
- 通常用于成员函数和公共数据
-
protected
- 只能在当前类及其派生类中访问
- 对外部隐藏,但允许继承使用
- 通常用于被派生类需要的内部实现
-
private
- 只能在当前类内部访问
- 对外部和派生类都隐藏
- 通常用于内部实现细节
class Base {
public:
int publicVar; // 公有成员
void publicMethod() {
cout << "Public method" << endl;
privateMethod(); // 可以访问私有方法
}
protected:
int protectedVar; // 受保护成员
void protectedMethod() {
cout << "Protected method" << endl;
}
private:
int privateVar; // 私有成员
void privateMethod() {
cout << "Private method" << endl;
}
};
class Derived : public Base {
public:
void testAccess() {
publicVar = 1; // 可以访问公有成员
publicMethod(); // 可以访问公有方法
protectedVar = 2; // 可以访问受保护成员
protectedMethod(); // 可以访问受保护方法
// privateVar = 3; // 错误:不能访问私有成员
// privateMethod(); // 错误:不能访问私有方法
}
};
int main() {
Base b;
b.publicVar = 10; // 可以访问公有成员
b.publicMethod(); // 可以访问公有方法
// b.protectedVar = 20; // 错误:不能访问受保护成员
// b.protectedMethod(); // 错误:不能访问受保护方法
// b.privateVar = 30; // 错误:不能访问私有成员
// b.privateMethod(); // 错误:不能访问私有方法
}
友元(friend)机制
- 允许非成员函数或其他类访问私有和受保护成员
- 破坏了封装性,应谨慎使用
class MyClass {
private:
int secret;
friend void friendFunction(MyClass& obj); // 友元函数
friend class FriendClass; // 友元类
public:
MyClass(int s) : secret(s) {}
};
void friendFunction(MyClass& obj) {
cout << "Secret: " << obj.secret << endl; // 可以访问私有成员
}
class FriendClass {
public:
void reveal(MyClass& obj) {
cout << "Secret: " << obj.secret << endl; // 可以访问私有成员
}
};
🔐 封装原则:使用访问修饰符实现"数据隐藏",公有接口尽量简洁,私有实现尽可能多。
25. 虚函数和纯虚函数
虚函数(Virtual Function)
- 基类中使用
virtual
关键字声明的函数 - 派生类可以重写(override)虚函数
- 实现运行时多态性
- 通过虚函数表(vtable)和虚函数指针(vptr)实现
class Shape {
public:
virtual void draw() {
cout << "Drawing a shape" << endl;
}
virtual double area() {
return 0.0;
}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
// 重写虚函数
void draw() override {
cout << "Drawing a circle" << endl;
}
double area() override {
return 3.14159 * radius * radius;
}
};
纯虚函数(Pure Virtual Function)
- 在声明末尾使用
= 0
的虚函数 - 没有函数实现
- 含有纯虚函数的类称为抽象类,不能直接实例化
- 派生类必须实现所有纯虚函数,否则也是抽象类
class AbstractShape {
public:
// 纯虚函数
virtual void draw() = 0;
virtual double area() = 0;
// 普通虚函数
virtual void rotate(double angle) {
cout << "Rotating shape by " << angle << " degrees" << endl;
}
};
class Rectangle : public AbstractShape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
// 实现纯虚函数
void draw() override {
cout << "Drawing a rectangle" << endl;
}
double area() override {
return width * height;
}
// 可以选择重写普通虚函数
void rotate(double angle) override {
cout << "Rotating rectangle by " << angle << " degrees" << endl;
}
};
虚函数与纯虚函数的区别
特性 | 虚函数 | 纯虚函数 |
---|---|---|
声明方式 | virtual type name() | virtual type name() = 0 |
函数体 | 可以有默认实现 | 没有实现(在接口中) |
派生类重写 | 可选 | 必须 |
类的性质 | 可以实例化 | 不能实例化(抽象类) |
主要用途 | 提供多态行为和默认实现 | 定义接口规范 |
🔄 设计模式:虚函数用于实现"模板方法"模式;纯虚函数用于实现"策略"模式和"接口"概念。
26. C++中的继承
继承的基本概念
- 创建新类时复用现有类的属性和方法
- 已存在的类称为基类/父类,新类称为派生类/子类
- 子类继承父类的特性,并可添加新特性或修改现有特性
继承的语法
class Base {
// 基类成员
};
class Derived : [访问修饰符] Base {
// 派生类成员
};
继承类型
-
公有继承(public)
- 基类的公有成员在派生类中仍是公有的
- 基类的保护成员在派生类中仍是保护的
- 基类的私有成员在派生类中不可访问
-
保护继承(protected)
- 基类的公有和保护成员在派生类中变为保护成员
- 基类的私有成员在派生类中不可访问
-
私有继承(private)
- 基类的公有和保护成员在派生类中变为私有成员
- 基类的私有成员在派生类中不可访问
class Vehicle {
public:
void start() { cout << "Vehicle started" << endl; }
protected:
int speed;
private:
string engineType;
};
// 公有继承
class Car : public Vehicle {
public:
void drive() {
start(); // 可以访问基类公有方法
speed = 60; // 可以访问基类保护成员
// engineType = "V8"; // 错误:不能访问基类私有成员
}
};
// Car的对象可以直接调用Vehicle的公有方法
Car myCar;
myCar.start(); // 正确
多继承
- 一个类可以同时继承多个基类
- 可能导致菱形继承问题(Diamond Problem)
class A {
public:
void display() { cout << "Class A" << endl; }
};
class B {
public:
void display() { cout << "Class B" << endl; }
};
// 多继承
class C : public A, public B {
public:
// 解决二义性
void showDisplay() {
A::display(); // 调用A的display
B::display(); // 调用B的display
}
};
虚继承
- 解决菱形继承问题
- 确保共同基类只有一个实例
class Animal {
public:
int age;
};
class Mammal : virtual public Animal {
// Mammal特有成员
};
class Bird : virtual public Animal {
// Bird特有成员
};
class Bat : public Mammal, public Bird {
// 因为虚继承,只有一份Animal::age
};
🧩 设计原则:优先使用组合而非继承;必要时使用公有继承表示"是一个"关系;避免多继承导致的复杂性。
27. 析构函数
析构函数的作用
- 在对象销毁时自动调用
- 释放对象占用的资源
- 执行必要的清理操作
- 防止内存泄漏
析构函数的特点
- 名称为类名前加~
- 不带参数
- 没有返回类型
- 一个类只能有一个析构函数
- 如果未定义,编译器会生成默认析构函数
class Resource {
private:
int* data;
public:
// 构造函数
Resource(int size) {
cout << "Resource acquired" << endl;
data = new int[size];
}
// 析构函数
~Resource() {
cout << "Resource released" << endl;
delete[] data; // 释放动态分配的内存
}
};
void useResource() {
Resource r(10); // 构造函数被调用
// 使用资源...
} // 函数结束,r超出作用域,析构函数自动调用
虚析构函数
- 基类的析构函数应声明为virtual
- 确保使用基类指针删除派生类对象时调用正确的析构函数
- 防止资源泄漏
class Base {
public:
Base() { cout << "Base constructed" << endl; }
virtual ~Base() { cout << "Base destructed" << endl; }
};
class Derived : public Base {
private:
int* array;
public:
Derived() : Base() {
cout << "Derived constructed" << endl;
array = new int[10];
}
~Derived() {
cout << "Derived destructed" << endl;
delete[] array;
}
};
int main() {
Base* ptr = new Derived();
// 使用对象...
delete ptr; // 如果~Base()不是virtual,只会调用Base析构函数
}
RAII原则
- Resource Acquisition Is Initialization(资源获取即初始化)
- 在构造函数中获取资源,在析构函数中释放资源
- C++的核心资源管理策略
⚠️ 注意事项:
- 派生类的析构函数会自动调用基类的析构函数
- 使用多态时,基类的析构函数必须是虚函数
- 如果类包含动态分配的资源,必须定义析构函数
28. 友元函数(Friend Function)
友元函数的定义
- 不属于类的成员,但可以访问类的所有成员(包括私有成员)
- 在类内部使用friend关键字声明
- 破坏了封装性,应谨慎使用
友元函数的类型
- 普通友元函数:独立函数被声明为友元
- 友元成员函数:另一个类的成员函数被声明为友元
- 友元类:整个类及其所有成员都是友元
普通友元函数
class Box {
private:
double width, height, depth;
public:
Box(double w, double h, double d) : width(w), height(h), depth(d) {}
// 声明友元函数
friend double getVolume(const Box& b);
};
// 友元函数定义
double getVolume(const Box& b) {
// 可以直接访问私有成员
return b.width * b.height * b.depth;
}
友元成员函数
class Screen; // 前向声明
class ScreenManager {
public:
void clearScreen(Screen& s);
};
class Screen {
private:
string content;
public:
Screen(string text) : content(text) {}
// 将ScreenManager类的clearScreen方法声明为友元
friend void ScreenManager::clearScreen(Screen& s);
};
// 友元成员函数定义
void ScreenManager::clearScreen(Screen& s) {
s.content = ""; // 可以访问Screen的私有成员
}
友元类
class Node {
private:
int data;
Node* next;
// 声明友元类
friend class LinkedList;
public:
Node(int d) : data(d), next(nullptr) {}
};
class LinkedList {
public:
void addNode(Node* node, Node* newNode) {
// 可以访问Node的私有成员
newNode->next = node->next;
node->next = newNode;
}
};
友元的特性
- 友元关系不具有传递性:A是B的友元,B是C的友元,不意味着A是C的友元
- 友元关系不具有继承性:基类的友元不是派生类的友元
- 友元关系不是相互的:A是B的友元,不意味着B是A的友元
🔑 最佳实践:
- 友元通常用于运算符重载或需要高效访问私有成员的场景
- 尽量减少友元的使用,保持良好的封装性
- 友元可以增强灵活性,但过度使用会降低代码可维护性
29. 命名空间(Namespace)
命名空间的作用
- 避免命名冲突
- 组织和分类代码
- 控制名称可见性
- 提供模块化支持
定义命名空间
// 定义命名空间
namespace Mathematics {
const double PI = 3.14159;
double square(double x) {
return x * x;
}
class Complex {
// 复数类定义
};
}
使用命名空间
// 方法1:使用命名空间名称限定
double area = Mathematics::PI * radius * radius;
double squared = Mathematics::square(5.0);
// 方法2:using声明(导入特定名称)
using Mathematics::PI;
double area = PI * radius * radius;
// 方法3:using指令(导入整个命名空间)
using namespace Mathematics;
double area = PI * radius * radius;
double squared = square(5.0);
嵌套命名空间
namespace Outer {
void outerFunction() {
cout << "Outer function" << endl;
}
namespace Inner {
void innerFunction() {
cout << "Inner function" << endl;
}
}
}
// 访问嵌套命名空间
Outer::outerFunction();
Outer::Inner::innerFunction();
匿名命名空间
namespace {
// 匿名命名空间中的内容仅在当前文件可见
// 类似于static全局函数/变量
int privateVar = 10;
void privateFunction() {
cout << "Private function" << endl;
}
}
命名空间别名
namespace VeryLongNamespace {
void function() {
cout << "Function in long namespace" << endl;
}
}
// 创建别名
namespace VLN = VeryLongNamespace;
// 使用别名
VLN::function();
标准命名空间
- 标准库定义在std命名空间中
- 应避免在自己的代码中使用std作为命名空间名
⚠️ 最佳实践:
- 避免在头文件中使用
using namespace
指令- 优先使用命名空间名称限定或using声明
- 在大型项目中使用命名空间组织代码
- 嵌套命名空间反映层次结构
30. 模板(Template)
模板的作用
- 实现泛型编程
- 支持类型无关的代码
- 提高代码复用性
- 在编译时生成特定类型的代码
函数模板
- 创建可以处理不同类型参数的函数
- 编译器根据调用时的参数类型生成具体函数
// 函数模板定义
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
// 使用函数模板
int maxInt = max<int>(10, 20); // 显式指定类型
double maxDouble = max(3.14, 2.71); // 类型自动推导
类模板
- 创建可以适应不同数据类型的类
- 必须在实例化时指定具体类型
// 类模板定义
template <typename T>
class Stack {
private:
vector<T> elements;
public:
void push(const T& item) {
elements.push_back(item);
}
T pop() {
if (elements.empty()) {
throw runtime_error("Stack is empty");
}
T top = elements.back();
elements.pop_back();
return top;
}
bool isEmpty() const {
return elements.empty();
}
};
// 使用类模板
Stack<int> intStack; // 整数栈
Stack<string> strStack; // 字符串栈
intStack.push(10);
intStack.push(20);
cout << intStack.pop(); // 输出20
模板特化
- 为特定类型提供不同实现
- 分为全特化和偏特化
// 主模板
template <typename T>
class DataProcessor {
public:
void process(T data) {
cout << "Processing generic data: " << data << endl;
}
};
// 全特化(完全特化)
template <>
class DataProcessor<string> {
public:
void process(string data) {
cout << "Processing string data: " << data << endl;
}
};
// 偏特化(部分特化)
template <typename T>
class DataProcessor<T*> {
public:
void process(T* data) {
cout << "Processing pointer data pointing to: " << *data << endl;
}
};
非类型模板参数
- 除类型外,模板还可以接受常量值作为参数
// 非类型模板参数
template <typename T, int Size>
class Array {
private:
T data[Size];
public:
T& operator[](int index) {
return data[index];
}
int size() const {
return Size;
}
};
// 使用带有非类型参数的模板
Array<int, 5> intArray;
Array<double, 10> doubleArray;
模板元编程
- 在编译时执行的计算
- 使用模板递归和特化
// 编译时阶乘计算示例
template <int N>
struct Factorial {
static const int value = N * Factorial<N-1>::value;
};
// 特化终止条件
template <>
struct Factorial<0> {
static const int value = 1;
};
// 使用
cout << "5! = " << Factorial<5>::value << endl; // 编译时计算
🔍 模板优势:
- 类型安全:编译时检查类型
- 性能:无运行时开销
- 复用:同一代码适用于多种类型
- 灵活:可通过特化处理特殊情况
STL与高级功能
31. C++标准模板库(STL)
常用容器类型
容器 | 特点 | 适用场景 |
---|---|---|
vector | 动态数组,连续内存,尾部增删快 | 随机访问,尾部操作频繁 |
list | 双向链表,任意位置增删快 | 频繁在任意位置插入删除 |
deque | 双端队列,两端增删快 | 两端频繁操作 |
stack | 后进先出(LIFO)栈 | 需要LIFO操作顺序 |
queue | 先进先出(FIFO)队列 | 需要FIFO操作顺序 |
priority_queue | 优先队列,自动排序 | 需要维护元素优先级 |
set | 有序集合,不允许重复元素 | 需要有序且唯一的元素集合 |
map | 键值对映射,按键排序 | 需要通过键快速查找值 |
容器操作示例
// vector示例
vector<int> nums = {1, 2, 3, 4, 5};
nums.push_back(6); // 添加元素
nums[2] = 10; // 随机访问
for(int n : nums) { // 范围for循环遍历
cout << n << " ";
}
// map示例
map<string, int> scores;
scores["Alice"] = 95;
scores["Bob"] = 87;
for(const auto& pair : scores) {
cout << pair.first << ": " << pair.second << endl;
}
STL算法
- 包含在
<algorithm>
头文件中 - 常用算法:sort, find, binary_search, transform等
vector<int> v = {5, 3, 8, 1, 7};
// 排序
sort(v.begin(), v.end());
// 查找
auto it = find(v.begin(), v.end(), 7);
if(it != v.end()) {
cout << "Found: " << *it << endl;
}
🚀 核心优势:STL提供了高效、可复用、类型安全的数据结构和算法。
32. 异常处理(Exception Handling)
异常处理基本结构
- try块:包含可能抛出异常的代码
- catch块:捕获并处理异常
- throw语句:抛出异常
try {
// 可能抛出异常的代码
int* arr = new int[1000000000]; // 可能导致std::bad_alloc
if (!arr) {
throw "Memory allocation failed";
}
} catch (const std::bad_alloc& e) {
cout << "内存分配失败: " << e.what() << endl;
} catch (const char* msg) {
cout << "错误: " << msg << endl;
} catch (...) {
cout << "未知异常" << endl;
}
异常类层次结构
- std::exception:所有标准异常的基类
- 常见派生类:std::bad_alloc, std::runtime_error等
自定义异常类
class DivideByZeroException : public std::exception {
public:
const char* what() const noexcept override {
return "除数不能为零";
}
};
double divide(double a, double b) {
if (b == 0) {
throw DivideByZeroException();
}
return a / b;
}
⚠️ 注意:异常处理有一定性能开销,在性能关键路径上应谨慎使用。
33. 运算符重载(Operator Overloading)
运算符重载的作用
- 允许自定义类型使用C++内置的运算符
- 使代码更直观、易读
- 提供与内置类型一致的使用体验
重载方式
- 成员函数方式
- 全局函数方式(友元)
二元运算符重载
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 Complex operator*(const Complex& a, const Complex& b);
};
// 全局函数重载*
Complex operator*(const Complex& a, const Complex& b) {
return Complex(a.real * b.real - a.imag * b.imag,
a.real * b.imag + a.imag * b.real);
}
一元运算符重载
// 前置++
Complex& operator++() {
++real;
++imag;
return *this;
}
// 后置++
Complex operator++(int) {
Complex temp = *this;
++(*this);
return temp;
}
流运算符重载
// 输出流运算符
friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << c.real;
if (c.imag >= 0) {
os << "+" << c.imag << "i";
} else {
os << c.imag << "i";
}
return os;
}
📌 限制:不能重载的运算符包括:
.
,.*
,::
,?:
,sizeof
。应保持运算符的原始语义。
34. 虚拟继承(Virtual Inheritance)
菱形继承问题
- 当一个类通过多条路径继承自同一个基类时产生
- 导致派生类包含基类的多个副本
- 访问基类成员时产生二义性
class A {
public:
int data;
};
class B : public A { };
class C : public A { };
// 菱形继承
class D : public B, public C {
// D包含两份A::data
};
D d;
// d.data; // 错误:二义性
d.B::data = 1; // 必须指定路径
虚拟继承解决方案
- 使用
virtual
关键字指定虚拟继承 - 虚拟继承确保共同基类只有一个实例
class A {
public:
int data;
};
class B : virtual public A { }; // 虚拟继承
class C : virtual public A { }; // 虚拟继承
class D : public B, public C {
// D只包含一份A::data
};
D d;
d.data = 1; // 可以直接访问,没有二义性
💡 设计建议:如果可能,使用组合而非多继承来避免菱形继承问题。
35. 类型转换操作符和显式类型转换
类型转换操作符
- 允许自定义类型隐式转换为其他类型
- 没有参数和返回类型(返回类型隐含在操作符名称中)
class Fraction {
private:
int numerator;
int denominator;
public:
Fraction(int n, int d) : numerator(n), denominator(d) {}
// 转换为double的操作符
operator double() const {
return static_cast<double>(numerator) / denominator;
}
};
Fraction f(3, 4);
double d = f; // 隐式调用类型转换操作符
explicit关键字
- 防止隐式类型转换
- 只允许显式转换
class Integer {
private:
int value;
public:
// 显式构造函数
explicit Integer(int v) : value(v) {}
// 显式类型转换操作符
explicit operator int() const {
return value;
}
};
Integer i(42);
// int n = i; // 错误:禁止隐式转换
int n = static_cast<int>(i); // 正确:显式转换
C++类型转换操作符
-
static_cast
- 基本类型转换
- 继承层次间的上行转换
- 无运行时类型检查
-
dynamic_cast
- 继承层次间的安全下行转换
- 有运行时类型检查
- 要求有虚函数(RTTI)
-
const_cast
- 添加或移除const/volatile限定符
- 不能改变类型
-
reinterpret_cast
- 重新解释二进制位
- 危险,但有时必要
- 通常用于底层操作
⚠️ 最佳实践:优先使用static_cast,需要类型安全的多态转换时使用dynamic_cast,避免使用reinterpret_cast除非确实需要。
36. 智能指针(Smart Pointer)
智能指针的作用
- 自动管理动态内存
- 防止内存泄漏
- 实现RAII原则
std::unique_ptr
- 独占式所有权
- 不可复制,只能移动
- 资源有唯一的拥有者
std::unique_ptr<int> p1(new int(42));
// std::unique_ptr<int> p2 = p1; // 错误:不能复制
std::unique_ptr<int> p2 = std::move(p1); // 移动所有权,p1变为nullptr
// 推荐使用make_unique (C++14)
auto p3 = std::make_unique<int>(42);
std::shared_ptr
- 共享所有权
- 使用引用计数
- 当最后一个shared_ptr销毁时,资源被释放
std::shared_ptr<int> p1 = std::make_shared<int>(42);
std::shared_ptr<int> p2 = p1; // 共享所有权,引用计数增加
cout << "引用计数: " << p1.use_count() << endl; // 输出2
std::weak_ptr
- 与shared_ptr协作使用
- 不增加引用计数
- 用于解决shared_ptr循环引用问题
std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;
// 检查资源是否仍然存在
if (auto temp = wp.lock()) {
cout << *temp << endl; // 资源仍然存在
} else {
cout << "资源已被释放" << endl;
}
🔑 使用建议:
- 默认使用unique_ptr管理独占资源
- 需要共享所有权时使用shared_ptr
- 需要引用共享资源但不参与所有权时使用weak_ptr
37. C++11新特性
语言特性
-
auto关键字
- 自动类型推导
auto i = 42; // int auto d = 3.14; // double auto str = "hello"; // const char*
-
Lambda表达式
- 匿名函数
auto add = [](int a, int b) { return a + b; }; cout << add(3, 4); // 输出7 // 捕获变量 int x = 10; auto addX = [x](int a) { return a + x; };
-
Range-based for循环
vector<int> nums = {1, 2, 3, 4, 5}; for (auto num : nums) { cout << num << " "; }
-
nullptr
- 替代NULL,类型安全
int* p = nullptr;
-
强类型枚举
enum class Color { Red, Green, Blue }; Color c = Color::Red;
库特性
-
智能指针
- unique_ptr, shared_ptr, weak_ptr
-
std::move和右值引用
- 支持移动语义
std::vector<int> v1 = {1, 2, 3}; std::vector<int> v2 = std::move(v1); // 移动而非复制
-
并发支持
- std::thread, std::mutex, std::future等
🆕 重要性:C++11是C++标准的重大更新,极大提高了开发效率和代码质量。
38. 静态断言与动态断言
动态断言(assert)
- 在运行时检查条件
- 位于
<cassert>
头文件 - 在Debug模式下生效,Release模式通常被禁用
#include <cassert>
void divide(int a, int b) {
assert(b != 0); // 运行时检查
return a / b;
}
静态断言(static_assert)
- 在编译时检查条件
- C++11引入
- 条件必须是编译期常量表达式
- 无论Debug还是Release模式都有效
template <typename T>
void processData(T value) {
static_assert(std::is_integral<T>::value,
"只允许整型数据!");
// 处理数据...
}
// 编译期检查
static_assert(sizeof(int) == 4, "该平台上int不是4字节!");
区别
特性 | 动态断言 | 静态断言 |
---|---|---|
检查时机 | 运行时 | 编译时 |
条件类型 | 任何布尔表达式 | 常量表达式 |
模式依赖 | Debug模式有效 | 所有模式有效 |
错误消息 | 固定格式 | 自定义错误消息 |
💡 使用建议:
- 用静态断言检查编译期可确定的条件(类型特性、大小等)
- 用动态断言检查运行时条件(用户输入、函数参数等)
39. 析构函数为何是虚函数而构造函数不能是
析构函数为虚函数的原因
- 确保通过基类指针删除派生类对象时调用正确的析构函数
- 防止资源泄漏
- 支持多态删除
class Base {
public:
virtual ~Base() {
cout << "Base析构" << endl;
}
};
class Derived : public Base {
public:
~Derived() {
cout << "Derived析构" << endl;
}
};
// 正确的析构顺序
Base* ptr = new Derived();
delete ptr; // 先调用Derived析构,再调用Base析构
构造函数不能是虚函数的原因
-
概念矛盾:
- 虚函数调用依赖于vtable(虚函数表)
- vtable指针在构造函数中初始化
- 构造对象时,类型已确定,不需要虚机制
-
执行顺序:
- 构造时先构造基类部分,再构造派生类部分
- 构造基类部分时,派生类部分尚未存在
🔍 核心理念:构造是自下而上的创建过程,析构是自上而下的销毁过程。
40. const与#define的区别
#define的特点
- 预处理指令,在预处理阶段进行文本替换
- 无类型检查,只是简单的文本替换
- 不遵循作用域规则,全局生效
- 没有内存分配,不会出现在符号表中
#define PI 3.14159
#define MAX(a, b) ((a) > (b) ? (a) : (b))
const的特点
- 编译期常量,拥有类型信息
- 具有作用域,遵循变量的作用域规则
- 可以进行类型检查
- 可以调试,有内存分配(对于非内联常量)
const double PI = 3.14159;
const int MAX_ARRAY_SIZE = 100;
主要区别
特性 | const | #define |
---|---|---|
处理阶段 | 编译时 | 预处理时 |
类型检查 | 有 | 无 |
作用域 | 遵循块作用域 | 从定义点到文件结束 |
内存分配 | 可能分配内存 | 无内存分配 |
可调试性 | 可在调试器中查看 | 不可在调试器中查看 |
使用建议
- 一般情况下优先使用const
- 需要类型安全的常量定义用const
- 简单的文本替换可以用#define
- 对于复杂的宏定义功能,现代C++更建议使用constexpr函数或内联函数
📘 现代C++:在C++11之后,使用constexpr可以定义编译期常量表达式,比简单的const更强大,比#define更安全。
📚 学习资源
- 官方文档: cppreference.com
- 标准文档: ISO C++ 官方网站
- 推荐书籍:
- 《C++ Primer》
- 《Effective C++》
- 《Modern C++ Design》
📊 C++版本发展
版本 | 年份 | 主要特性 |
---|---|---|
C++98 | 1998 | STL, 异常处理 |
C++11 | 2011 | auto, lambda, 智能指针 |
C++14 | 2014 | make_unique, 泛型lambda |
C++17 | 2017 | 结构化绑定, std::optional |
C++20 | 2020 | 概念(concepts), 协程, 模块 |