0%

一、理论讲解

数据库范式(Database Normalization)是组织数据库结构的一种方法,以减少数据冗余和提高数据完整性。范式是数据库设计的一组规则,分为不同的层次,每个层次解决特定类型的数据问题。主要的范式包括:

  1. **第一范式 (1NF)**:
    • 定义:表中的所有字段都应该是原子的,即不可再分的。
    • 要求:每个字段只包含一个值,消除重复的列。
    • 例子:如果一个表有一个字段用逗号分隔多个值,则不符合1NF。必须将这些值分解成独立的行或列。
  2. **第二范式 (2NF)**:
    • 定义:在满足1NF的基础上,消除部分依赖。
    • 要求:非主键字段必须完全依赖于主键,而不是依赖于主键的一部分(即消除部分依赖)。
    • 例子:如果一个表的主键由两个字段组成,而另一个字段只依赖于其中一个字段,则不符合2NF。需要将这些字段分解到不同的表中,使每个字段完全依赖于主键。
  3. **第三范式 (3NF)**:
    • 定义:在满足2NF的基础上,消除传递依赖。
    • 要求:非主键字段必须直接依赖于主键,而不能通过其他非主键字段间接依赖于主键(即消除传递依赖)。
    • 例子:如果一个表中有字段A依赖于主键,字段B依赖于字段A,则不符合3NF。需要将字段A和B分解到不同的表中,使每个字段直接依赖于主键。
  4. **巴斯-科德范式 (BCNF)**:
    • 定义:在满足3NF的基础上,处理更为严格的依赖关系。
    • 要求:每个非主键字段都必须完全依赖于候选键。
    • 例子:如果一个表中有多个候选键,而某个字段依赖于非主键的候选键,则需要进行进一步分解,确保所有字段都依赖于候选键。
  5. **第四范式 (4NF)**:
    • 定义:在满足BCNF的基础上,消除多值依赖。
    • 要求:每个字段必须依赖于主键,不允许存在多值依赖。
    • 例子:如果一个表中有一个字段依赖于另一个字段的多个值,则不符合4NF。需要将这些多值依赖的字段分解到不同的表中。
  6. **第五范式 (5NF)**:
    • 定义:在满足4NF的基础上,消除连接依赖。
    • 要求:所有的信息都应该可以通过原子表的自然连接来重构。
    • 例子:如果一个表中某些字段的组合信息依赖于另一个字段的组合信息,则不符合5NF。需要将这些字段进一步分解。

二、举例讲解

一个学校管理系统中的表,记录学生选课信息。初始表结构如下:

StudentID StudentName CourseID CourseName Instructor Grade
1 Alice 101 Math Dr. Smith A
1 Alice 102 English Dr. Brown B
2 Bob 101 Math Dr. Smith B
3 Charlie 103 History Dr. Green A

我们来一步步讲解各个范式,并规范化这个表。

第一范式 (1NF)

要求:所有字段都是原子的,不可再分。

初始表已经满足1NF,因为每个字段都包含单一值,不可再分。

第二范式 (2NF)

要求:在满足1NF的基础上,消除部分依赖。非主键字段必须完全依赖于主键,而不是依赖于主键的一部分。

当前表的主键是 StudentIDCourseID 的组合。但是:

  • StudentName 只依赖于 StudentID
  • CourseNameInstructor 只依赖于 CourseID

存在部分依赖,因此不满足2NF。我们需要将部分依赖的字段分解到不同的表中:

Students 表:

StudentID StudentName
1 Alice
2 Bob
3 Charlie

Courses 表:

CourseID CourseName Instructor
101 Math Dr. Smith
102 English Dr. Brown
103 History Dr. Green

Enrollments 表(存储学生选课和成绩信息):

StudentID CourseID Grade
1 101 A
1 102 B
2 101 B
3 103 A

现在每个表都满足2NF,因为所有非主键字段完全依赖于整个主键。

第三范式 (3NF)

要求:在满足2NF的基础上,消除传递依赖。非主键字段必须直接依赖于主键,而不能通过其他非主键字段间接依赖于主键。

假设我们在 Enrollments 表中添加了一个冗余字段 Instructor,表示授课教师:

StudentID CourseID Grade Instructor
1 101 A Dr. Smith
1 102 B Dr. Brown
2 101 B Dr. Smith
3 103 A Dr. Green

在这种情况下,Instructor 依赖于 CourseID,而 CourseID 是主键 StudentIDCourseID 的一部分。由于 Instructor 字段通过 CourseID 间接依赖于主键组合 StudentIDCourseID,这构成了传递依赖。因此,不满足3NF。

解决方法是移除 Instructor 字段,因为它可以从 Courses 表中得到:

StudentID CourseID Grade
1 101 A
1 102 B
2 101 B
3 103 A

巴斯-科德范式 (BCNF)

要求:在满足3NF的基础上,处理更为严格的依赖关系。每个非主键字段都必须完全依赖于候选键。

假设 Courses 表有以下结构:

CourseID CourseName Instructor RoomNumber
101 Math Dr. Smith 101
102 English Dr. Brown 102
103 History Dr. Green 103

如果 RoomNumber 依赖于 Instructor,则会违反BCNF。假设 Dr. Smith 总是在 Room 101 授课,那么 RoomNumber 应该依赖于 Instructor 而不是 CourseID

解决方法是分解 Courses 表:

Courses 表:

CourseID CourseName Instructor
101 Math Dr. Smith
102 English Dr. Brown
103 History Dr. Green

Instructors 表:

Instructor RoomNumber
Dr. Smith 101
Dr. Brown 102
Dr. Green 103

第四范式 (4NF)

要求:在满足BCNF的基础上,消除多值依赖。

假设我们有一个表记录学生的多种联系方式:

StudentID ContactType ContactValue
1 Email alice@example.com
1 Phone 123-456-7890
2 Email bob@example.com
2 Phone 098-765-4321

这里,ContactTypeContactValueStudentID 形成多值依赖。解决方法是分解成两个独立的表:

Emails 表:

StudentID Email
1 alice@example.com
2 bob@example.com

Phones 表:

StudentID Phone
1 123-456-7890
2 098-765-4321

第五范式 (5NF)

要求:在满足4NF的基础上,消除连接依赖。所有信息都应该可以通过原子表的自然连接来重构。

假设我们有一个表记录课程、教师和教材之间的关系:

CourseID Instructor Textbook
101 Dr. Smith Math Book
102 Dr. Brown English Book
103 Dr. Green History Book

假设每个课程可能有多个教师和多本教材,这样的关系会导致复杂的连接依赖。

解决方法是将关系分解为更小的表:

CourseInstructors 表:

CourseID Instructor
101 Dr. Smith
102 Dr. Brown
103 Dr. Green

CourseTextbooks 表:

CourseID Textbook
101 Math Book
102 English Book
103 History Book

InstructorTextbooks 表(仅在特定情况下需要,例如如果教材依赖于教师):

Instructor Textbook
Dr. Smith Math Book
Dr. Brown English Book
Dr. Green History Book

通过这些步骤,表结构满足了5NF。

1. 单例模式

属于创建型模式,确保一个类只有一个实例并提供一个全局访问点来访问该实例。单例模式常用于需要全局控制资源或共享资源的情况下,例如日志记录、配置管理、线程池等。

根据单例对象的创建时机分为饿汉模式和懒汉模式:

1.1 饿汉模式

在定义类的时候就创建了单例对象实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>

// 饿汉式单例
class Singleton {
public:
// 禁用拷贝构造、赋值操作
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

// 静态方法获取实例
static Singleton* getInstance() {
return instance;
}

void doSomething() {
std::cout << "Doing something" << std::endl;
}

private:
// 外部禁用构造函数
Singleton() = default;

// 静态实例
static Singleton* instance;
};

// 初始化静态实例
Singleton* Singleton::instance = new Singleton();

int main() {
Singleton* sl = Singleton::getInstance();
singleton->doSomething();

return 0;
}

1.2 懒汉模式

在第一次使用时才创建实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include <mutex>

// 懒汉式单例
class Singleton {
public:
// 静态方法获取实例
static Singleton* getInstance() {
// 双重检查锁定(Double-Checked Locking)
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
if (instance == nullptr) {
instance = new Singleton();
}
}
return instance;
}

void doSomething() {
std::cout << "Doing something" << std::endl;
}

private:
// 私有构造函数
Singleton() = default;

// 禁止拷贝构造、赋值操作
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

static Singleton* instance;
static std::mutex mtx;
};

// 初始化静态成员
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;

int main() {
Singleton* singleton = Singleton::getInstance();
singleton->doSomething();

return 0;
}

1.3 优缺点、适用场景

优点

  1. 唯一实例:确保一个类只有一个实例,节省资源。
  2. 全局访问:提供全局访问点,方便访问。
  3. 控制实例化:控制实例化过程,避免频繁创建和销毁对象。

缺点

  1. 并发问题:在多线程环境下,懒汉式单例可能会引起线程安全问题,需要额外处理。
  2. 可测试性差:单例模式使得类难以进行单元测试,因为依赖于全局状态。
  3. 隐藏依赖:使用单例模式可能隐藏类间的依赖关系,增加代码的复杂性。

适用场景

  • 需要控制实例数目的类,节省系统资源。
  • 需要提供一个全局访问点的场景,例如配置管理、日志管理、资源管理等。

2. 工厂模式

属于创建型模式定义一个创建对象的接口,让子类决定实例化哪一个类。工厂模式将对象的创建过程封装起来,使得客户端不需要知道具体创建的细节,从而提高系统的可扩展性和维护性。

工厂模式主要分为简单工厂模式工厂方法模式抽象工厂模式

2.1 简单工厂模式

使用一个工厂类来创建对象,工厂类包含一个创建方法,根据传入的参数来决定实例化哪一个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <memory>

// 产品接口
class Product {
public:
virtual void use() = 0;
virtual ~Product() = default;
};

// 具体产品A
class ProductA : public Product {
public:
void use() override {
std::cout << "Using ProductA" << std::endl;
}
};

// 具体产品B
class ProductB : public Product {
public:
void use() override {
std::cout << "Using ProductB" << std::endl;
}
};

// 简单工厂类
class SimpleFactory {
public:
enum ProductType { TYPE_A, TYPE_B };

static std::unique_ptr<Product> createProduct(ProductType type) {
switch (type) {
case TYPE_A: return std::make_unique<ProductA>();
case TYPE_B: return std::make_unique<ProductB>();
default: return nullptr;
}
}
};

int main() {
auto productA = SimpleFactory::createProduct(SimpleFactory::TYPE_A);
productA->use();

auto productB = SimpleFactory::createProduct(SimpleFactory::TYPE_B);
productB->use();

return 0;
}

2.2 工厂方法模式

使用一个抽象工厂接口和具体工厂类,每个具体工厂类负责创建一个具体产品对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <iostream>
#include <memory>

// 产品接口
class Product {
public:
virtual void use() = 0;
virtual ~Product() = default;
};

// 具体产品A
class ProductA : public Product {
public:
void use() override {
std::cout << "Using ProductA" << std::endl;
}
};

// 具体产品B
class ProductB : public Product {
public:
void use() override {
std::cout << "Using ProductB" << std::endl;
}
};

// 工厂接口
class Factory {
public:
virtual std::unique_ptr<Product> createProduct() = 0;
virtual ~Factory() = default;
};

// 具体工厂A
class FactoryA : public Factory {
public:
std::unique_ptr<Product> createProduct() override {
return std::make_unique<ProductA>();
}
};

// 具体工厂B
class FactoryB : public Factory {
public:
std::unique_ptr<Product> createProduct() override {
return std::make_unique<ProductB>();
}
};

int main() {
std::unique_ptr<Factory> factoryA = std::make_unique<FactoryA>();
auto productA = factoryA->createProduct();
productA->use();

std::unique_ptr<Factory> factoryB = std::make_unique<FactoryB>();
auto productB = factoryB->createProduct();
productB->use();

return 0;
}

2.3 抽象工厂模式

提供一个接口用于创建相关或依赖对象的家族,而无需明确指定具体类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include <iostream>
#include <memory>

// 产品A接口
class ProductA {
public:
virtual void use() = 0;
virtual ~ProductA() = default;
};

// 产品B接口
class ProductB {
public:
virtual void eat() = 0;
virtual ~ProductB() = default;
};

// 具体产品A1
class ProductA1 : public ProductA {
public:
void use() override {
std::cout << "Using ProductA1" << std::endl;
}
};

// 具体产品A2
class ProductA2 : public ProductA {
public:
void use() override {
std::cout << "Using ProductA2" << std::endl;
}
};

// 具体产品B1
class ProductB1 : public ProductB {
public:
void eat() override {
std::cout << "Eating ProductB1" << std::endl;
}
};

// 具体产品B2
class ProductB2 : public ProductB {
public:
void eat() override {
std::cout << "Eating ProductB2" << std::endl;
}
};

// 抽象工厂接口
class AbstractFactory {
public:
virtual std::unique_ptr<ProductA> createProductA() = 0;
virtual std::unique_ptr<ProductB> createProductB() = 0;
virtual ~AbstractFactory() = default;
};

// 具体工厂1
class ConcreteFactory1 : public AbstractFactory {
public:
std::unique_ptr<ProductA> createProductA() override {
return std::make_unique<ProductA1>();
}
std::unique_ptr<ProductB> createProductB() override {
return std::make_unique<ProductB1>();
}
};

// 具体工厂2
class ConcreteFactory2 : public AbstractFactory {
public:
std::unique_ptr<ProductA> createProductA() override {
return std::make_unique<ProductA2>();
}
std::unique_ptr<ProductB> createProductB() override {
return std::make_unique<ProductB2>();
}
};

int main() {
std::unique_ptr<AbstractFactory> factory1 = std::make_unique<ConcreteFactory1>();
auto productA1 = factory1->createProductA();
auto productB1 = factory1->createProductB();
productA1->use();
productB1->eat();

std::unique_ptr<AbstractFactory> factory2 = std::make_unique<ConcreteFactory2>();
auto productA2 = factory2->createProductA();
auto productB2 = factory2->createProductB();
productA2->use();
productB2->eat();

return 0;
}

2.4 模式选择

  • 简单工厂模式:适合产品种类少、创建逻辑简单、对扩展要求不高的场景。
  • 工厂方法模式:适合产品种类多、创建逻辑复杂、需要较高扩展性的场景。
  • 抽象工厂模式:适合需要创建一系列相关或相互依赖的产品、产品间存在依赖关系、需要较高灵活性和扩展性的场景。

2.5 优缺点、适用场景

优点:

  1. 解耦创建和使用:客户端不需要知道具体产品类的类名,只需要知道工厂接口。
  2. 提高可扩展性:增加新产品时,只需要添加相应的工厂和产品类,客户端代码不需要修改。
  3. 管理对象创建:集中管理对象的创建逻辑,使得创建过程更灵活、更易维护。

缺点:

  1. 增加复杂度:引入额外的工厂类和接口,增加了系统的复杂度。
  2. 类增多:每增加一个新的产品类型,可能需要增加相应的工厂类和接口,导致类的数量增多。

适用场景:

  1. 对象的创建需要灵活控制:例如创建对象的过程比较复杂,需要在创建时进行一些逻辑处理。
  2. 系统结构稳定:产品类和创建产品的工厂类比较固定,但需要经常增加新产品。
  3. 需要生成一系列相关的产品对象:例如在抽象工厂模式中,一个工厂可以创建多个相关的对象。

3. 原型模式

属于创建型模式,通过复制现有的实例来创建新的对象,而不是通过实例化类来创建对象。这样可以大大减少创建对象的开销,尤其是在对象创建成本较高时。原型模式的核心是提供一个接口,用于复制现有的对象。

原型模式的核心是一个克隆方法,一般使用抽象基类定义 clone 方法,然后具体类实现该方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#include <memory>

// 抽象原型类
class Prototype {
public:
virtual std::unique_ptr<Prototype> clone() const = 0;
virtual void use() const = 0;
virtual ~Prototype() = default;
};

// 具体原型类1
class ConcretePrototype1 : public Prototype {
public:
std::unique_ptr<Prototype> clone() const override {
return std::make_unique<ConcretePrototype1>(*this);
}

void use() const override {
std::cout << "Using ConcretePrototype1" << std::endl;
}
};

// 具体原型类2
class ConcretePrototype2 : public Prototype {
public:
std::unique_ptr<Prototype> clone() const override {
return std::make_unique<ConcretePrototype2>(*this);
}

void use() const override {
std::cout << "Using ConcretePrototype2" << std::endl;
}
};

int main() {
// 创建具体原型对象
std::unique_ptr<Prototype> prototype1 = std::make_unique<ConcretePrototype1>();
std::unique_ptr<Prototype> prototype2 = std::make_unique<ConcretePrototype2>();

// 克隆对象
std::unique_ptr<Prototype> clone1 = prototype1->clone();
std::unique_ptr<Prototype> clone2 = prototype2->clone();

// 使用克隆的对象
clone1->use();
clone2->use();

return 0;
}

优缺点及适用场景

优点:

  1. 性能提升:通过克隆现有对象而不是重新创建,可以提高性能,尤其是对象的创建过程比较复杂或耗时的时候。
  2. 动态扩展:可以在运行时动态创建对象,避免了依赖具体类。
  3. 简化对象创建:克隆对象的过程比通过构造函数创建对象的过程简单。

缺点:

  1. 深拷贝问题:对于包含指针或引用的对象,克隆过程可能需要实现深拷贝,增加了复杂性。
  2. 资源管理:克隆对象时需要处理资源的复制和管理,可能导致资源泄漏问题。
  3. 依赖具体实现:克隆过程依赖于具体类的实现,可能会引入不必要的依赖。

适用场景:

  1. 创建成本高的对象:对象的创建成本较高,通过克隆可以大大减少开销。
  2. 对象结构复杂:对象包含很多子对象或嵌套结构,克隆可以简化创建过程。
  3. 动态创建对象:需要在运行时根据已有对象动态创建新对象。

  1. 栈区(Stack)
    • 栈区用于存储函数的局部变量、函数参数、函数调用时的返回地址以及函数调用过程中的上下文信息。
    • 栈区是由编译器自动管理的,它的大小和生命周期都是由程序的函数调用情况决定的。
    • 栈区中的数据是连续存储的,变量的内存分配是按照后进先出(LIFO)的原则进行的。
  2. 堆区(Heap)
    • 堆区用于存储程序动态分配的内存,即通过 newmalloc 等运算符或函数进行的内存分配。
    • 堆区的内存空间通常由程序员手动管理,需要在不需要使用时手动释放,否则可能会导致内存泄漏。
    • 堆区的数据是不连续存储的,变量的内存分配是动态的,可以根据程序需要进行分配和释放。
  3. 全局区/静态区(Static/Global)
    • 全局区用于存储全局变量、静态变量、常量和字符串常量。
    • 全局区在程序启动时就会被分配,并在程序结束时释放,它的生命周期与整个程序的生命周期相同。
    • 全局区中的数据是静态分配的,变量的内存分配是固定的,不会随着函数的调用而改变。
  4. 常量区(Constants)
    • 常量区用于存储程序中的常量,例如字符串常量。
    • 常量区的数据是只读的,不能被修改。
    • 常量区通常是放在静态存储区域中的,因此也被称为静态常量区。
  5. 代码区(Code)
    • 代码区用于存储程序的可执行代码,包括函数的机器代码和指令。
    • 代码区通常是只读的,不能被修改。
    • 代码区在程序启动时就会被加载到内存中,因此也被称为只读存储区。

异常处理是一种在程序执行期间处理错误或异常情况的机制,它允许程序在发生错误时转移到错误处理代码,从而提高程序的健壮性和可靠性。在 C++ 中,异常处理通过 trythrowcatch 关键字来实现。

  1. try: 用于包含可能会引发异常的代码。程序在执行 try 块中的代码时,如果发生异常,将会跳转到匹配的 catch 块进行异常处理。try 块必须和至少一个 catch 块配对使用。
  2. throw: 用于在程序中手动抛出异常。它可以抛出任何类型的异常,包括基本数据类型、类对象、指针等。当程序执行到 throw 语句时,会立即跳转到匹配的 catch 块中,并执行相应的异常处理代码。
  3. catch:用于捕获并处理 try 块中抛出的异常。catch 块必须紧跟在 try 块之后,用于处理特定类型的异常或者所有类型的异常。catch 块中可以使用异常类型来捕获特定类型的异常,也可以使用省略号 ... 来捕获所有类型的异常。一个 try 块可以有多个 catch 块,每个 catch 块可以捕获不同类型的异常,但只有第一个匹配的 catch 块会被执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>

void processNumber(int num) {
if (num == 0) {
throw "Divide by zero exception";
} else if (num < 0) {
throw std::runtime_error("Negative number exception");
} else {
std::cout << "Result: " << 100 / num << std::endl;
}
}

int main() {
try {
processNumber(0);
} catch (const char* msg) {//捕获自定义的异常
std::cerr << "Caught exception: " << msg << std::endl;
} catch (const std::exception& e) {//捕获到任何继承自std::exception的异常,并使用 e.what()方法获取异常对象的描述信息
std::cerr << "Caught exception: " << e.what() << std::endl;
}

try {
processNumber(-5);
} catch (const char* msg) {
std::cerr << "Caught exception: " << msg << std::endl;
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}

try {
processNumber(10);
} catch (const char* msg) {
std::cerr << "Caught exception: " << msg << std::endl;
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}

return 0;
}

//运行结果
//Caught exception: Divide by zero exception
//Caught exception: Negative number exception
//Result: 10

  • auto: 用于自动类型推断,让编译器根据变量的初始化表达式推导出其类型。
  • const: 声明常量,使得变量的数值在初始化后不能被修改。
  • constexpr: 用于声明常量表达式,使得函数或变量在编译期间就能被计算出来。
1
2
3
4
5
6
7
8
9
10
11
12
constexpr int size = 10; // 声明常量

constexpr int square(int x) { // 声明函数
return x * x;
}

class MyClass { // 声明类的构造函数,在编译期间初始化对象
public:
constexpr MyClass(int x) : data(x) {}
private:
int data;
};
  • static: 声明静态变量或函数,使得它们在程序的整个生命周期内保持其存在,即使超出了其定义的作用域。
  • volatile: 告知编译器,某个变量的值可能会在程序的执行过程中被意外地改变,应该禁止对其进行某些优化。
  • extern: 声明一个变量或函数是在其他文件中定义的,使得在当前文件中可以引用它。
  • mutable: 用于声明类的成员变量,在const成员函数中允许修改这些变量。
1
2
3
4
5
6
7
8
9
10
11
class person
{
int m_A;
mutable int m_B;//特殊变量 在常函数里值也可以被修改
public:
void add() const//在函数里不可修改this指针指向的值 常量指针
{
m_A = 10;//错误 不可修改值,this已经被修饰为常量指针
m_B = 20;//正确
}
};
  • inline: 声明函数为内联函数,建议编译器在调用时将函数体直接插入到调用处,以减少函数调用的开销。
1
2
3
4
5
6
7
inline int add(int a, int b) {
return a + b;
}
int main() {
int result = add(3, 4);
return 0;
}
  • explicit : 用于修饰单参数的构造函数,防止隐式类型转换发生。
1
2
3
4
5
6
7
8
9
10
11
class MyClass {
public:
explicit MyClass(int x) : data(x) {}
private:
int data;
};
int main() {
MyClass obj = 10; // 错误,因为构造函数是explicit的
MyClass obj2(10); // 正确
return 0;
}
  • virtual: 声明虚函数,用于实现多态性,在派生类中可以重写基类的虚函数。
  • override: 用于在派生类中标记重写基类的虚函数。
1
2
3
4
5
6
7
8
9
10
11
12
class A{
public:
virtual void foo(){
cout << "father" << endl;
};
}
class B : public A{
public:
void foo() override{
cout << "son override" << endl;
};
}
  • final: 用于标记类不能被继承,或者虚函数不能被重写。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base{
public:
virtual void foo();
};

class A : public Base{
public:
void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
};

class B final : A // 指明B是不可以被继承的
{
public:
void foo() override; // Error: 在A中已经被final了
};

class C : B // 错误
{
};
  • nullptr: 表示空指针常量。
  • typename: 用于告知编译器某个名称是类型而不是变量。
  • try/catch/throw: 用于异常处理的关键字。
  • namespace: 声明命名空间,用于避免命名冲突。

一、定义

宏定义#define是用来进行文本替换的预处理指令。

  1. 宏名称:宏定义的名称可以是任何有效的标识符,类似于变量名或函数名,但通常会使用大写字母来表示,以便于与变量名或函数名区分开来。
  2. 替换文本:替换文本可以是任何合法的C、C++代码片段,可以是一个表达式、一个语句、甚至是一段复杂的代码块。替换文本中可以包含其他已经定义过的宏,也可以包含预处理运算符(如 ### 等)来进行字符串化或拼接等操作。

宏定义的几个常见应用包括:

  • 定义常量:通过宏定义可以方便地定义常量,如 #define PI 3.14159
  • 简化代码:通过宏定义可以将一些重复的代码片段定义为宏,以减少代码量,提高可读性,比如 #define MAX(x, y) ((x) > (y) ? (x) : (y)) 用来求两个数中的最大值。
  • 条件编译:宏定义还可以用于条件编译,通过定义或取消定义不同的宏,可以控制编译器在不同条件下编译不同的代码片段,实现跨平台编译等功能。

二、#define和const的区别

  • define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用。
  • 安全性
    • define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部的内容,要不然很容易出错。
    • const常量有数据类型,编译器可以对其进行类型安全检查。
  • 内存占用
    • define只是将宏名称进行替换,在内存中会产生多分相同的备份。const在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的的表达式计算出结果放入常量表
    • 宏替换发生在编译阶段之前,属于文本插入替换; const作用发生于编译过程中。
    • 宏不检查类型,const会检查数据类型。
    • 宏定义的数据没有分配内存空间,只是插入替换掉; const定义的变量只是值不能改变,但要分配内存空间。

三、#define和函数的区别

  • 编译过程
    • 宏在预处理阶段完成替换,之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用,执行起来更快;函数调用在运行时需要跳转到具体调用函数。
  • 安全性
    • 宏定义中替换的函数不会进行参数类型检查,因此可能会导致参数类型不匹配或难以发现的错误。
    • 代码中写的函数在编译时期会进行类型检查,可以帮助检测到参数类型不匹配等错误。
  • 作用域
    • 宏定义中的替换文本会直接插入到代码中,不会创建新的作用域。因此,在宏定义中定义的变量或函数名可能会与代码中的其他变量或函数名冲突。
    • 代码中写的函数会创建一个新的作用域,函数内部的变量和函数名不会与外部的变量和函数名冲突。

四、#define和typedef的区别

  • 宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
  • 宏替换发生在编译阶段之前,属于文本插入替换; typedef是编译的一部分。
  • 宏不检查类型; typedef会检查数据类型。
  • 宏不是语句,不在在最后加分号; typedef是语句,要加分号标识结束。

  1. sizeof是运算符,在编译时便得到结果;strlen是<string.h>库里的函数。
  2. strlen 函数用于计算以空字符 \0 结尾的字符串的长度,即字符串中的字符数(不包括空字符 \0

sizeof 操作符用于获取数据类型或对象在内存中的大小,单位为字节,对于数组,返回的是整个数组的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>

int main(){
const char* tmp = "stars";
char str[] = "stars";
char arr[10] = "spiderman";

printf("%d\n", sizeof(tmp));//8,计算的是char*的大小,64为系统中char*为8
printf("%d\n", strlen(tmp));//4,字符数

printf("%d\n", sizeof(str));//5,计算的是数组大小,即“stars\0”共五个元素,每个元素占1字节
printf("%d\n", strlen(str));//4,字符数

printf("%d\n", sizeof(arr));//10,数组大小1*10
printf("%d\n", strlen(arr));//9,字符数
return 0;
}

一、区别

  1. 类型安全性:
    • new/delete 是 C++ 中的操作符,它们能够调用对象的构造函数和析构函数,并且在动态分配内存时会自动计算所需的空间大小,因此是类型安全的。
    • malloc/free 是 C 语言中的函数,它们只是分配和释放内存块,不会调用任何构造函数或析构函数,也不会进行类型检查。因此,在 C++ 中使用 malloc/free 可能会导致内存泄漏或未定义的行为。
  2. 大小指定:
    • new 操作符根据指定的类型自动计算所需的内存大小,因此不需要显式指定分配的大小。
    • malloc 函数需要显式指定要分配的内存块的大小,它接受一个参数来指定所需的字节数。
  3. 返回类型:
    • new 操作符返回一个指向动态分配的对象的指针,类型为所分配对象的类型
    • malloc 函数返回一个指向动态分配的内存块的指针,类型为 void**需要进行显式的类型转换**。
  4. 构造和析构函数的调用:
    • new 操作符在分配内存后会调用对象的构造函数,用于初始化对象。
    • delete 操作符在释放内存前会调用对象的析构函数,用于清理对象。
    • malloc/free 不会调用任何构造或析构函数,它们只是分配和释放内存块。
  5. 异常处理:
    • new 操作符在分配失败时会抛出 std::bad_alloc 异常。
    • malloc 函数在分配失败时返回 NULL 指针,需要手动检查分配是否成功。
1
2
3
4
5
6
7
//动态分配了一个整数对象,并将其初始化为 5,然后将返回的指针赋值给指针变量 p1
int* p1 = new int(5);
delete p1;

//动态分配了一个大小为 sizeof(int) 字节的内存块,并将返回的指针强制转换为整型指针类型,然后将其赋值给指针变量 p2
int* p2 = (int*)malloc(sizeof(int));
free p2;

二、new和delete的实现原理

  1. 分配内存

  2. 在使用 new 运算符来创建一个对象时,编译器会调用一个名为 operator new 的函数来分配内存。这个函数负责从堆中分配足够大的、原始的、未命名的内存空间。

  3. 然后,编译器运行相应的构造函数,为这段内存传入初始值。

  4. 对象被分配了空间并构造完成,返回一个指向该对象的指针。如果分配失败,operator new 函数抛出 std::bad_alloc 异常

  5. 构造对象

​ 分配内存后,编译器会调用对应的构造函数来初始化对象。这确保了对象的状态是有效的,并且任何必要的资源(如动态分配的内存或文件句柄)都已经被正确初始化。

  1. 返回指针

​ 一旦对象被正确地构造,new 运算符将返回一个指向新分配的内存的指针。这个指针可以用于访问对象的成员变量和方法。

  1. 释放内存

​ 当使用 delete 运算符来释放对象时,编译器会调用一个名为 operator delete 的函数。这个函数负责释放之前分配的内存,并且在必要时调用对象的析构函数来清理资源。

三、调用free会发生什么

  1. 标记内存块为可用: 调用 free 函数会将动态分配的内存块标记为可用。这意味着该内存块现在可以被系统重新分配给其他程序或进程使用。但是,该内存块的内容不会被清除或修改,因此在释放后的内存块中仍然可能包含之前存储的数据。
  2. 释放内存块: 被释放的内存块会被添加到系统的内存空闲列表中,以便以后的内存分配请求可以使用它。
  3. 合并相邻的空闲内存块: 在释放内存块后,系统可能会检查相邻的空闲内存块,并尝试将它们合并成一个更大的内存块。这有助于减少内存碎片化,并提高内存的利用率。
  4. 返回给操作系统(部分情况下): 在某些操作系统中,当大块内存被释放时,系统可能会将该内存块返回给操作系统,以便它可以被重新分配给其他程序或进程使用。但是,这并不是所有操作系统都会立即执行的操作,具体取决于操作系统的内存管理策略和实现。
悬空指针

当使用 delete 运算符释放动态分配的内存时,系统会回收该内存块,并将其标记为可用的空闲内存。然而,指向这块内存的指针本身并没有被修改,它仍然保持原来的值,但这个地址不再有效,这就是所谓的悬空指针。因此,为了避免使用悬空指针,通常将 ptr 设置为 nullptr

野指针

野指针是指向未知或无效内存地址的指针。野指针一般是未初始化的指针、已经被释放的指针或者指向无效内存的指针

  • 如果一个指针变量未被显式初始化,它将包含一个随机的值,这个值可能是一个无效的内存地址。当尝试访问这个指针所指向的内存时,就会产生未定义的行为。因此,为了防止出错,对于指针初始化时都是赋值为nullptr,这样在使用时编译器就不会直接报错,产生非法内存访问。
  • 当一个动态分配的内存块被释放(如使用 deletefree),但之后仍然有指针指向这块内存,这些指向已经释放的内存的指针就称为野指针。
  • 有时指针可能会指向已经被销毁或无效的对象,尝试访问这样的指针就会产生未定义的行为,这也属于野指针的范畴。

  1. 栈(Stack):
    • 栈用于存储函数的局部变量、函数参数以及函数调用所需要的上下文信息。
    • 栈内存由系统自动分配和释放,由编译器管理。当一个函数被调用时,其所需的局部变量和参数被存储在栈上,当函数执行完毕时,这些变量的内存空间会被自动释放。
    • 栈的大小有限,通常较小,它们的大小在程序运行时是固定的,栈顶和栈底是预设好的,栈向栈底扩展。
    • 栈上的内存分配和释放速度比较快,不会有碎片。
  2. 堆(Heap):
    • 堆是用于动态内存分配的区域,用于存储程序运行时需要动态分配的数据,例如使用 malloc()calloc()new 等函数动态分配的内存。
    • 堆内存的分配和释放由程序员手动管理。程序员需要显式地分配内存并在不需要使用时手动释放内存,否则会产生内存泄漏。
    • 堆的大小通常比栈大得多,大小受操作系统的限制,通常由操作系统的虚拟内存大小决定,堆向高地址扩展是不连续的内存区域,大小可灵活调整。
    • 由于堆上的内存分配是动态的,需要维护额外的信息来跟踪已分配和未分配的内存块,因此堆上的内存分配和释放速度相对较慢,会产生内存碎片。

在 C 语言中,结构体的对齐方式是编译器根据平台和编译器的要求进行的一种内存对齐操作,其目的是为了提高内存访问的效率和性能。对齐方式可以通过编译器的选项或者特定的编译指令来控制,但通常情况下编译器会使用默认的对齐方式。

遵循的一般对齐规则:

  • 每个成员变量的起始地址都必须是其数据类型大小的倍数,如果不能则在前一个成员后面补充字节,默认的对齐大小是4:这意味着一个 int 类型的变量通常会从内存地址为 0、4、8、12 等处开始,而一个 double 类型的变量通常会从内存地址为 0、8、16、24 等处开始。
  • 结构体的总大小必须是其最大成员变量大小的倍数

例如下面这个例子:

1
2
3
4
5
struct A{
int a;
double b;
char c;
};

假设a的起始地址是0x0000,则a所占的内存空间为0x00000x0003,下一个地址0x004不是double的大小8的整数倍,故补零到0x0007。则b的内存为0x00080x0010。下一个地址0x0011为c的地址。共8+8+1=17字节,但总大小不是最大数据类型大小8的整数倍,故总大小为24。

而下面的例子:

1
2
3
4
5
struct A{
char a;
int b;
double c;
};

假设a的地址:0x00000x0001,下一个地址0x0002不是int大小4的整数倍,故b的地址:0x00040x0007,c的地址:0x0008~0x0010,共4+4+8=16个字节,且是最大数据类型大小8的整数倍,故总大小为16。